简介:这份资源面向计算机、人工智能、通信、物联网等专业的在校学生与教师,提供基于Python和OpenCV的单目与双目相机标定完整源码,可用于课程设计、毕业设计、大作业或项目立项演示,也适合零基础入门与进阶学习。压缩包共8个文件,以4个py脚本为核心,分别实现单目标定与双目立体标定流程,另附2个txt与2个md说明文档,交代运行环境、依赖与使用注意事项,整体约25KB,轻量易部署。目前已有436人学习下载,具备一定参考热度。代码经实际运行验证,稳定可靠,读者可据此理解相机内参、畸变系数、棋盘格角点检测及双目标定参数求解等关键环节,并在此基础上二次开发,扩展测距、三维重建等DIY功能。需注意解压后路径与项目名不要含中文,建议重命名为英文再运行,遇到问题可私信作者沟通。
1. 从一张畸变棋盘格说起:这套 Python + OpenCV 标定源码到底能干什么
很多人第一次做视觉项目,都会遇到一个反直觉的现象:明明用的是标称 6mm 焦距的工业镜头,测出来的距离却总是差那么几个像素,直线在画面边缘还会弯成弧线。这不是镜头坏了,而是相机内参和畸变系数没标定。这套基于 Python 和 OpenCV 的相机标定程序,解决的正是这个问题——它把单目相机标定和双目相机标定两套流程都封装好了,输入一组棋盘格图片,输出内参矩阵、畸变系数,双目还额外给出旋转矩阵、平移向量和极线校正映射。适合正在做毕业设计、课程设计,或者刚接手一个视觉测量、三维重建、双目测距项目的同学。你不需要从零推导张正友标定法的公式,但需要知道每张棋盘格该拍成什么样、参数怎么设、结果怎么判断合不合格。下面我按实际拆包复现的顺序,把这份源码的用法和坑一次讲清。
2. 单目标定:从棋盘格采集到内参矩阵输出
单目相机标定是整个流程的地基。双目标定本质上是在两个单目各自标定完成后,再求两个相机之间的相对位姿。所以先把单目跑通,后面双目的结果才可信。这一章从采集、代码结构到参数含义,把单目这条链路走完。
2.1 棋盘格采集:为什么你拍的图总是标定失败
标定失败十有八九不是代码问题,而是采集的图片不合格。OpenCV 的findChessboardCorners对棋盘格的完整性、清晰度、角度都有要求。常见做法是打印一张 9x6 或 11x8 的棋盘格,贴在硬质平板上,避免纸张弯曲。采集时注意几点:棋盘格要占画面三分之一以上,太小角点检测精度差;至少覆盖画面四个角和中心区域,只在中间拍十几张,边缘畸变系数会估不准;每张图棋盘格要有不同角度,倾斜 20 到 45 度之间比较理想,完全正对会让某些参数不可观测。
我一般会拍 20 到 30 张,宁多勿少,因为标定函数会自动筛选,但图太少一旦有几张检测失败就不够了。光照要均匀,避免反光和阴影压在角点上。如果是双目,左右相机要同时采集,保证同一时刻的棋盘格位姿一致,否则后面立体标定的外参就是错的。
提示:棋盘格内角点数量是 (列数-1) x (行数-1)。比如 10x7 的方格,内角点是 9x6,代码里 pattern_size 要填 (9,6),填错直接检测不到。
2.2 源码结构与单目标定核心代码
这份源码通常包含单目和双目两个入口脚本,外加一个标定工具模块。目录结构大致是mono_calibration.py、stereo_calibration.py、calibration_utils.py,以及存放图片的images/left、images/right文件夹。先看单目标定的核心逻辑,下面这段是我按源码结构整理的可复现版本:
import cv2 import numpy as np import glob # 棋盘格内角点数,按实际打印的格子减一填写 pattern_size = (9, 6) # 方格实际物理尺寸,单位毫米,用于输出平移向量的真实尺度 square_size = 25.0 # 生成棋盘格在世界坐标系下的三维点,Z=0平面 objp = np.zeros((pattern_size[0] * pattern_size[1], 3), np.float32) objp[:, :2] = np.mgrid[0:pattern_size[0], 0:pattern_size[1]].T.reshape(-1, 2) objp *= square_size objpoints = [] # 世界坐标点 imgpoints = [] # 图像像素点 images = glob.glob('images/left/*.jpg') for fname in images: img = cv2.imread(fname) gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) # 查找角点,第二个参数为亚像素优化窗口 ret, corners = cv2.findChessboardCorners(gray, pattern_size, None) if ret: # 亚像素级角点优化,提高标定精度 criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 30, 0.001) corners2 = cv2.cornerSubPix(gray, corners, (11, 11), (-1, -1), criteria) objpoints.append(objp) imgpoints.append(corners2) # 执行标定,返回内参、畸变、旋转、平移 ret, mtx, dist, rvecs, tvecs = cv2.calibrateCamera( objpoints, imgpoints, gray.shape[::-1], None, None) print("内参矩阵:\n", mtx) print("畸变系数:", dist.ravel()) print("重投影误差:", ret)逻辑上分三步:先构造棋盘格在世界坐标系下的理想三维点,Z 全部为 0;再对每张图检测角点并做亚像素优化,把图像点和世界点一一对应;最后交给calibrateCamera求解。参数里square_size决定平移向量的物理单位,如果你只关心像素级精度可以填 1.0,但要做真实测距就必须填实际毫米数。cornerSubPix的窗口 (11,11) 是常见值,图像分辨率高可以适当加大。返回的ret是重投影误差,单位像素,一般要求小于 0.5,超过 1.0 说明采集或角点检测有问题。
2.3 内参矩阵和畸变系数怎么读
标定输出的内参矩阵是三行三列,形式如下:
| 参数 | 含义 | 典型判断 |
|---|---|---|
| fx, fy | 焦距的像素表示 | 两者接近,差异大说明像素非方形或标定异常 |
| cx, cy | 主点坐标 | 应接近图像中心,偏离过大要检查 |
| k1, k2, p1, p2, k3 | 径向和切向畸变系数 | k1 通常为负,绝对值过大说明镜头畸变强 |
fx 和 fy 理论上应该相等,实际会有几个像素差异,属于正常。cx、cy 如果离图像中心超过图像宽高的 10%,多半是棋盘格覆盖区域太偏。畸变系数里 k1 是主导项,广角镜头 k1 绝对值可能到 -0.3 甚至更大,普通镜头在 -0.1 左右。p1、p2 是切向畸变,正常应该很小,如果很大说明镜头和传感器不平行或者图片有问题。
注意:畸变系数只有在标定图像覆盖了画面边缘时才有意义。如果你只在画面中心拍,k2、k3 会接近 0,但拿去校正边缘图像就会翻车。
3. 双目标定:外参求解与极线校正的完整链路
双目比单目多出来的部分,是求两个相机之间的旋转和平移关系,以及做极线校正让左右图像的对应点在同一水平线上。这一步做不好,后面做双目测距或者三维重建时,视差图会一塌糊涂。这一章把双目标定的输入要求、代码实现和结果验证讲透。
3.1 双目采集的同步要求
双目标定的前提是左右相机在同一时刻拍摄同一块棋盘格。如果两个相机是分开触发、手动按快门,那基本没法用,因为棋盘格在两次拍摄之间动了,外参就错了。常见做法是用一根同步线连接两个相机做硬件触发,或者用支持双目的采集卡。如果是 USB 相机,至少要用软件触发尽量同时抓帧,但精度会差一些。
采集时棋盘格要同时完整出现在左右画面里,且尽量覆盖两幅图的公共视野。左右相机各自的内参可以先用单目标定分别求出来,也可以直接交给双目标定函数一起优化。我一般会先分别单目标定,检查各自重投影误差合格后,再跑双目标定,这样出问题好定位是哪个相机的问题。
3.2 双目标定核心代码与参数
双目标定在单目的基础上,多了左右角点配对和stereoCalibrate调用。下面这段是核心逻辑:
import cv2 import numpy as np import glob pattern_size = (9, 6) square_size = 25.0 objp = np.zeros((pattern_size[0] * pattern_size[1], 3), np.float32) objp[:, :2] = np.mgrid[0:pattern_size[0], 0:pattern_size[1]].T.reshape(-1, 2) objp *= square_size objpoints = [] imgpoints_left = [] imgpoints_right = [] left_images = sorted(glob.glob('images/left/*.jpg')) right_images = sorted(glob.glob('images/right/*.jpg')) criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 30, 0.001) for lf, rf in zip(left_images, right_images): imgL = cv2.imread(lf) imgR = cv2.imread(rf) grayL = cv2.cvtColor(imgL, cv2.COLOR_BGR2GRAY) grayR = cv2.cvtColor(imgR, cv2.COLOR_BGR2GRAY) retL, cornersL = cv2.findChessboardCorners(grayL, pattern_size, None) retR, cornersR = cv2.findChessboardCorners(grayR, pattern_size, None) # 只有左右都检测到才配对使用 if retL and retR: cornersL = cv2.cornerSubPix(grayL, cornersL, (11, 11), (-1, -1), criteria) cornersR = cv2.cornerSubPix(grayR, cornersR, (11, 11), (-1, -1), criteria) objpoints.append(objp) imgpoints_left.append(cornersL) imgpoints_right.append(cornersR) # 先分别单目标定得到初始内参 retL, mtxL, distL, _, _ = cv2.calibrateCamera( objpoints, imgpoints_left, grayL.shape[::-1], None, None) retR, mtxR, distR, _, _ = cv2.calibrateCamera( objpoints, imgpoints_right, grayR.shape[::-1], None, None) # 立体标定,求解R和T ret, mtxL, distL, mtxR, distR, R, T, E, F = cv2.stereoCalibrate( objpoints, imgpoints_left, imgpoints_right, mtxL, distL, mtxR, distR, grayL.shape[::-1], flags=cv2.CALIB_FIX_INTRINSIC) print("旋转矩阵 R:\n", R) print("平移向量 T:\n", T.ravel()) print("立体标定重投影误差:", ret)关键点在stereoCalibrate的flags参数。CALIB_FIX_INTRINSIC表示固定前面单目标定得到的内参,只优化外参,适合内参已经标得比较准的情况。如果内参不确定,可以用默认 flags 让函数一起优化,但计算量大、对初值敏感。返回的 R 和 T 描述的是右相机相对于左相机的位姿,T 的模长就是基线距离,单位跟square_size一致。基线是双目测距的核心参数,T 的数值要跟实际卷尺量的基线对比,差太多说明标定有问题。
3.3 极线校正与结果验证
拿到 R 和 T 之后,还要做极线校正,让左右图像的极线水平对齐。OpenCV 提供stereoRectify和initUndistortRectifyMap两个函数完成这件事:
# 极线校正,计算校正后的投影矩阵和映射表 R1, R2, P1, P2, Q, roi1, roi2 = cv2.stereoRectify( mtxL, distL, mtxR, distR, grayL.shape[::-1], R, T, flags=cv2.CALIB_ZERO_DISPARITY, alpha=0) # 生成校正映射,后续对每帧图像做remap mapLx, mapLy = cv2.initUndistortRectifyMap( mtxL, distL, R1, P1, grayL.shape[::-1], cv2.CV_32FC1) mapRx, mapRy = cv2.initUndistortRectifyMap( mtxR, distR, R2, P2, grayR.shape[::-1], cv2.CV_32FC1) # 对测试图做校正,检查极线是否水平 imgL = cv2.imread('images/left/1.jpg') imgR = cv2.imread('images/right/1.jpg') rectL = cv2.remap(imgL, mapLx, mapLy, cv2.INTER_LINEAR) rectR = cv2.remap(imgR, mapRx, mapRy, cv2.INTER_LINEAR) cv2.imwrite('rect_left.jpg', rectL) cv2.imwrite('rect_right.jpg', rectR)alpha=0表示校正后只保留有效像素,边缘会被裁掉;alpha=1保留全部像素但会有黑边。Q是视差到三维的重投影矩阵,后面做测距会用到。验证方法是把校正后的左右图并排,画几条水平线,看同一物体在左右图中的对应点是否在同一水平线上。如果偏差明显,说明 R、T 不准或者采集时同步没做好。
| 验证项 | 合格标准 | 不合格的常见原因 |
|---|---|---|
| 单目重投影误差 | < 0.5 像素 | 棋盘格太小、角点模糊 |
| 双目重投影误差 | < 1.0 像素 | 左右不同步、配对错误 |
| 基线 T 模长 | 与实测接近 | square_size 填错 |
| 极线水平度 | 对应点同一行 | R、T 求解异常 |
4. 避坑与排查:标定跑不通时先看这几条
标定这件事,代码往往不是瓶颈,坑都在数据和参数上。下面这几条是我和身边人踩过的,按现象、原因、解决整理,遇到问题按顺序排查。
4.1 角点检测返回 False
现象是findChessboardCorners一直返回 False,一张图都检测不到。原因通常是pattern_size填错,把外角点数量当成了内角点,或者棋盘格打印比例不对导致方格不是正方形。解决方法是先用一张清晰的正面图单独测试,把pattern_size逐个试,同时确认棋盘格是黑白相间、方格等大。如果图片有反光或者棋盘格边缘被裁掉,也会检测失败,换图即可。
4.2 重投影误差过大
现象是标定能跑完,但ret超过 1.0 甚至好几。原因是采集图片质量差,比如棋盘格倾斜角度太单一、画面模糊、角点被遮挡。解决方法是剔除误差贡献最大的那几张图,重新采集覆盖更多角度和位置的图片。可以在代码里打印每张图的重投影误差,把明显偏大的删掉再标。
4.3 双目基线数值离谱
现象是输出的 T 向量模长跟实际基线差几倍甚至符号相反。原因是左右图片没有按顺序配对,或者square_size单位填错。解决方法是确认left_images和right_images排序后一一对应,检查square_size是否跟实际方格尺寸一致。如果 T 的符号反了,说明左右相机定义搞反了,交换左右图重新标定。
4.4 校正后图像黑边严重
现象是极线校正后图像边缘出现大片黑边,有效区域很小。原因是alpha=0裁掉了无效像素,加上畸变系数大,校正后有效区域本来就小。解决方法是把alpha改成 1 保留全部像素,或者调整stereoRectify的newImageSize参数放大输出尺寸。如果黑边大到影响使用,说明镜头畸变太强,考虑换镜头或者接受裁剪。
4.5 换分辨率后结果失效
现象是同一套相机,换了分辨率重新标定,内参完全对不上。原因是内参跟分辨率绑定,fx、fy、cx、cy 都是像素单位,分辨率变了必须重新标定。解决方法是固定项目使用的分辨率,标定和后续处理保持一致。如果必须换分辨率,可以按比例缩放内参作为初值,但精度会下降,最好重新标。
5. 进阶技巧:用 Q 矩阵做双目测距与精度验证
标定的最终目的通常是测量。拿到校正映射和 Q 矩阵后,就可以做双目测距了。这一步能反过来验证标定质量,如果测距误差大,说明前面标定有问题。下面这段是完整的测距验证流程:
import cv2 import numpy as np # 读取校正映射和Q矩阵(前面标定保存下来的) # 这里假设已经通过np.save保存了mapLx等参数 imgL = cv2.imread('rect_left.jpg') imgR = cv2.imread('rect_right.jpg') grayL = cv2.cvtColor(imgL, cv2.COLOR_BGR2GRAY) grayR = cv2.cvtColor(imgR, cv2.COLOR_BGR2GRAY) # SGBM立体匹配,参数需按图像调整 stereo = cv2.StereoSGBM_create( minDisparity=0, numDisparities=128, # 必须是16的倍数 blockSize=5, P1=8 * 3 * 5 ** 2, P2=32 * 3 * 5 ** 2, disp12MaxDiff=1, uniquenessRatio=10, speckleWindowSize=100, speckleRange=32 ) disparity = stereo.compute(grayL, grayR).astype(np.float32) / 16.0 # 用Q矩阵把视差图转成三维点云 points_3d = cv2.reprojectImageTo3D(disparity, Q) # 取画面中心区域某点的深度做验证 h, w = disparity.shape cx, cy = w // 2, h // 2 depth = points_3d[cy, cx, 2] print("中心点深度(mm):", depth)numDisparities决定视差搜索范围,太小测不了近处,太大会增加计算量,一般设 64 或 128。blockSize是匹配块大小,奇数,越大越平滑但边缘越糊。reprojectImageTo3D用 Q 矩阵把每个像素的视差转成三维坐标,Z 就是深度。验证方法是把棋盘格放在已知距离,比如 500mm,看输出的深度是否接近。如果误差在 1% 到 2% 以内,说明标定合格;误差超过 5%,回去检查基线和内参。
我一般会做一组不同距离的验证,从 300mm 到 1500mm 各测一次,画一条误差曲线。如果近距离准、远距离偏大,多半是基线标小了;如果整体偏移,可能是主点不准。这套流程走下来,标定结果到底能不能用,心里就有数了。从那以后我每次标定完,都强制走一遍已知距离验证,不再只看重投影误差那个数字。希望帮到你。
本文还有配套的精品资源,点击获取