简介:一份基于OpenCV实现张正友相机标定的完整工程资源,面向需要学习相机标定原理与工程实现的开发者、研究人员,解决从标定板制作、图像采集到相机内外参计算与畸变矫正的全流程问题。压缩包共107个文件,大小约95.67MB,内容涵盖VS2015工程源码(cpp、vcxproj、sln、bat自动化脚本等)、棋盘格打印PDF、10张手机拍摄与20张摄像头拍摄的标定测试图,以及标定得到的mat矩阵与txt文本结果、运行日志等,结构清晰便于按需取用。工程代码注释详细,支持一键运行,能够输出相机内参、外参、旋转和平移矩阵,并附带标定效果评价模块;同时可使用标定结果对原始棋盘图进行矫正,直观展示算法效果,且标定结果与Matlab非常接近。资源还提供了pdb、ilk等调试文件,方便读者跟踪执行流程;测试图像覆盖不同拍摄设备,增强了标定的泛化参考价值。目前已有6805人浏览学习,适合计算机视觉方向的初学者与进阶者参考。 搞视觉的人基本都逃不过相机标定这一关,尤其是做机器人抓取、SLAM、三维重建或者自动驾驶感知的,拿到的相机如果内参不准,后面所有计算都像用一把没校零的尺子量东西。张正友标定法配合OpenCV实现,是目前最常用、也最适合入门的一套方案——你只需要一块棋盘格、一台相机、十来张照片,就能把相机的内参、畸变系数和外参全部求出来。这篇文章我会把整个流程拆开讲清楚,从棋盘格打印PDF怎么生成,到测试图片怎么采集,再到OpenCV代码一步步怎么写,最后附上我实际跑标定踩过的坑和排查技巧。无论你是刚接触视觉的新手,还是想系统梳理一遍标定细节的从业者,这篇都值得看完再动手。
1. 为什么是张正友标定法
1.1 相机标定到底在标什么
相机成像可以简单理解成把三维世界投影到二维像素平面,但这个投影不是理想的小孔成像,镜头本身会引入畸变,CMOS安装也会有轻微偏差。标定的核心就是求出一组参数,用来描述“空间点三维坐标 -> 像素坐标”的映射关系。这组参数分两部分:内参(焦距、主点、畸变系数)描述相机自身属性;外参(旋转矩阵、平移向量)描述相机在世界坐标系下的位姿。
很多人觉得现在工厂出厂的相机都有标定数据,没必要自己标,但实际使用中镜头装上之后,法兰距、镜头个体差异、温度变化都会让出厂参数失效。尤其是用了工业相机搭配不同镜头,或者手机摄像头模组,标定几乎是必做的第一步。张正友标定法之所以流行,是因为它只需要一块平面棋盘格,不需要昂贵的标定设备,精度却足够大多数场景使用。
1.2 张正友标定法的核心思路
张正友博士在1998年提出的这个方法,巧妙在把三维标定问题简化成了二维问题。标定板是平面,所以棋盘格上所有角点的世界坐标z分量可以设为0,这样原本需要求解的单应性矩阵H就变成8个自由度,通过多张不同角度的照片,可以线性求解出内参和外参的初值,再做非线性优化得到精确解。
这种方法降低了对标定设备的要求,你不需要知道相机与标定板之间的精确距离,也不需要像传统两步法那样做复杂的三维靶标。你只需要不断变换棋盘格的姿态、角度、位置,拍下十几张照片,OpenCV就能算出结果。原理上它是介于传统标定和自标定之间的一种折中方案,精度比自标定高,操作又比传统标定简单,所以成了事实上的工业标准。
1.3 棋盘格为什么是首选
棋盘格的高对比度黑白方块让角点检测非常稳定,OpenCV的findChessboardCorners在多数光照条件下都能找到角点。而且棋盘格角点是两个方块的交叉点,亚像素级定位相对容易,配合cornerSubPix可以进一步把角点精度做到0.1像素以内。
相比圆点标定板,棋盘格的角点有明确的“顺序性”,OpenCV能自动按行列排序输出,所以代码实现起来很简单。另外,棋盘格对打印设备的精度要求不高,普通打印机加上硬质底板就能用,这点对个人开发者非常友好。你要是做高精度的工业视觉,可以买玻璃或陶瓷基板的标定板,但学习和验证阶段自己打印完全够用。
2. 标定前的准备:棋盘格与测试图片
2.1 自己生成棋盘格打印PDF
张正友标定用的棋盘格,严格来说要定义两个关键参数:内角点数(intersection points count)和方格边长。内角点数指的是黑白格子交叉点的数量,不是格子数量。例如一个10x7的格子阵列,内角点是9x6,OpenCV中通常写作(9, 6)。采集图片时,程序需要检测这些内角点,所以这个数必须和实际标定板完全一致,否则检测不到。
我用Python生成一张可以直接打印的棋盘格图片,核心代码如下:
import cv2 import numpy as np # 内角点数 (width, height) board_size = (9, 6) # 每个方格的边长,单位mm,对应打印后实际尺寸 square_size_mm = 25 # 每个方格在图片中的像素大小,决定图片分辨率 pixels_per_square = 120 margin = 60 # 四周留白,方便裁剪 cols = board_size[0] + 1 rows = board_size[1] + 1 img_w = cols * pixels_per_square + margin * 2 img_h = rows * pixels_per_square + margin * 2 chessboard = np.ones((img_h, img_w), dtype=np.uint8) * 255 for i in range(rows): for j in range(cols): if (i + j) % 2 == 0: x0 = margin + j * pixels_per_square y0 = margin + i * pixels_per_square chessboard[y0:y0 + pixels_per_square, x0:x0 + pixels_per_square] = 0 cv2.imwrite('chessboard.png', chessboard)生成图片后,打印时务必关掉“适应页面”或“缩放以适合”选项,按100%比例打印,然后量一下实际格子的边长,把square_size_mm改成实测值。因为纸张在打印过程中会有伸缩,如果你需要高精度标定,这一步不能省。打印好后把纸贴在平整的硬板上,不能有褶皱,否则角点坐标本身就带了误差,后面标定精度会大打折扣。
2.2 采集测试图片的要点
有了标定板,接下来就是采集图片。这是整个标定流程里最容易被忽视、却最影响效果的环节。我一般建议拍15到25张图,太少的话内参解算不稳定,太多也没有必要。拍摄时要遵循几个原则:
- 标定板要占画面面积的1/4到1/3以上,太小了角点检测会不稳定。
- 每一张照片里,标定板的角度、距离和位置尽量不同,要有左右倾斜、上下倾斜、远近变化、平移变化。
- 避免所有图片都让标定板正对着相机,否则外参退化,内参也解不准。
- 保证光照均匀,不要有强反光,不要有阴影压住角点。
- 手持标定板就行,相机固定,人移动标定板即可,也可以反过来,相机移动而标定板固定。
如果你用的是USB摄像头或手机相机,记得关闭自动对焦、自动曝光和自动白平衡,固定变焦和曝光参数后再拍摄。对焦模糊的图片直接删除,不要勉强用,因为亚像素角点检测依赖边缘锐度,模糊图会让角点定位产生偏差。我自己的经验是,把视频流打开,一边观察角点检测效果一边采集,会比盲拍一堆再筛选高效得多。
3. OpenCV标定代码实现与参数解析
3.1 角点检测与亚像素精细化
OpenCV标定的标准流程分三步:检测每张图的角点、亚像素精细化、传入calibrateCamera解算参数。第一步是基础,我通常这样写:
import cv2 import numpy as np import glob board_size = (9, 6) criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 30, 0.001) objp = np.zeros((board_size[0] * board_size[1], 3), np.float32) objp[:, :2] = np.mgrid[0:board_size[0], 0:board_size[1]].T.reshape(-1, 2)objp存放的是棋盘格角点的三维坐标。这里有一个常见的理解误区:为什么这些坐标不是实际的毫米数值?因为张正友标定法里,objp的尺度是可以任意缩放的,你写单位长度1、2、3和写25、50、75,最终得到的内参矩阵焦距会有对应倍数变化,但畸变系数和归一化坐标结果不变。所以更标准的做法是直接用实际物理尺寸,也就是乘以square_size_mm,这样得到的焦距单位就是像素,没什么问题。
接下来读取图片并找角点:
image_files = glob.glob('calib_images/*.jpg') objpoints = [] imgpoints = [] for fname in image_files: img = cv2.imread(fname) gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) ret, corners = cv2.findChessboardCorners(gray, board_size, None) if ret: corners_sub = cv2.cornerSubPix(gray, corners, (11, 11), (-1, -1), criteria) objpoints.append(objp) imgpoints.append(corners_sub) cv2.drawChessboardCorners(img, board_size, corners_sub, ret) cv2.imshow('check', img) cv2.waitKey(100) else: print(f'角点检测失败: {fname}') cv2.destroyAllWindows()findChessboardCorners返回的corners已经按行排列,顺序是从左上角开始横向扫描,这依赖于OpenCV内部的四边形拓扑分析。你可以打印一下corners的shape,通常是(54, 1, 2),对应54个角点,每个点两个坐标。角点检测失败时不要跳过就完事,最好看一下原因,见第5节。
3.2 调用calibrateCamera完成标定
角点全部检测完成后,用calibrateCamera一次性解算所有参数:
ret, mtx, dist, rvecs, tvecs = cv2.calibrateCamera( objpoints, imgpoints, gray.shape[::-1], None, None ) print('内参矩阵:') print(mtx) print('畸变系数:') print(dist) print('重投影误差:', ret)这里必须解释一下每个返回值的含义。mtx是3x3内参矩阵,形如:
[[fx, 0, cx], [ 0, fy, cy], [ 0, 0, 1]]fx和fy是等效焦距,单位是像素,理想情况下两者接近;cx和cy是主点坐标,理想情况下在图像中心附近,但实际会有偏移。dist是畸变系数,OpenCV默认包含5项(k1, k2, p1, p2, k3),前两个是径向畸变,中间两个是切向畸变,第五个是薄棱镜畸变的高阶项。rvecs和tvecs是每张图片对应的旋转向量和平移向量,也就是外参,每张图对应一组。
特别注意ret返回值,它是所有角点在归一化相机坐标系下的重投影均方根误差,单位是像素。这个值越小越好,一般低于0.5就算不错,低于0.1是相当好的结果。如果大于1,说明标定数据里有脏数据,需要排查。
3.3 为什么要用亚像素精细化
可能有人会问,直接用findChessboardCorners检测到的角点不行吗?实际上,初始检测的角点精度受图像分辨率、模糊程度影响,只能到像素级,而张正友标定法对标定板角点的坐标精度非常敏感,微小偏差会被放大到内参上。cornerSubPix通过迭代求取灰度梯度的交点,能把角点定位精度提高到亚像素级别。
这个函数需要几个参数:搜索窗口大小(11, 11)表示在每个角点周围11x11像素范围内微调;(-1, -1)表示没有零迭代区;criteria是迭代停止条件,一般用最大迭代次数30次和最小误差0.001。窗口大小不是越大越好,对于高分辨率图像可以用(15, 15),但窗口太大反而容易受到邻近角点影响。
如果你用的是灰度图,cornerSubPix直接处理即可。如果彩色图有严重的色差,可以转成灰度再操作,OpenCV内部计算也是基于灰度梯度的。还有一个小技巧:对图片先做一次cv2.cvtColor后,如果检测效果不好,可以尝试用cv2.adaptiveThreshold对图像做预处理,增强棋盘格边缘,但绝大多数场景下不需要。
4. 实操结果分析:怎么判断标定质量
4.1 读懂内参矩阵和畸变系数
标定完成后第一件事,不是记录参数,而是验证参数是否合理。内参矩阵里的fx和fy应该很接近,因为它们分别代表x、y方向归一化焦距,虽然镜头可能略有各向异性,但差值一般不会超过几个百分点。如果你用的是普通工业镜头,fx和fy相差10%以上,多半是标定板图片姿态覆盖不够,或者代码里objp的坐标方向定义错了。
主点cx、cy一般在图像中心附近,比如1280x960的图像,主点大约在(640, 480)附近,偏差几十上百个像素是正常的。如果主点跑到图像边缘甚至外面,那就说明标定失败,需要重新检查数据源。畸变系数k1、k2一般很小,在-0.1到0.1之间都算合理,如果出现几十几百的离谱值,基本是求解迭代出问题了。
更直观的验证方法是把标定结果用于图像去畸变,对比一下效果:
h, w = img.shape[:2] new_camera_mtx, roi = cv2.getOptimalNewCameraMatrix(mtx, dist, (w, h), 1, (w, h)) undistorted = cv2.undistort(img, mtx, dist, None, new_camera_mtx)这里可以观察画面边缘的直线是否被拉直。棋盘格的边、背景里的门框、墙线都是很好的参考。如果去畸变后边缘的弯曲消失,说明标定结果基本可信。
4.2 重投影误差与图像数量的关系
重投影误差是标定结果最直接的量化指标。它的计算思路是:用求解出的内参、外参和畸变系数,把每张标定板上的三维角点投影到像素平面,计算投影点与实际检测角点之间的距离。误差小于0.5像素说明内参和外参之间的一致性很好,但要注意,重投影误差小不代表你的相机内参一定准确,因为它只衡量“自洽性”。比如所有标定板都放在画面中央,重投影误差可能很小,但内参外推能力很差,换一个角度就不准了。
所以标定过程中,图片的数量和角度覆盖比单张图片的角点精度更重要。15到25张图已经是工程上的经验值,少于10张容易过拟合;超过30张如果没有新的角度信息,改善也有限。如果你发现重投影误差偏大,先检查是不是有某张图角点检测异常,把那张图剔除后重新标定,往往误差就能降下来。
5. 常见问题与排查技巧
5.1 角点检测失败的原因
findChessboardCorners返回False是新手最常见的卡点。我总结了几种高频原因:
- 棋盘格内角点数定义错误。比如实际棋盘格是10x7格子,内角点数是9x6,你却写了
(10, 7),那永远检测不到。 - 标定板在画面中太小,角点间距只有几个像素,OpenCV的四边形检测无法稳定识别。解决办法是让标定板靠近镜头,或者提高拍摄分辨率。
- 光照不均匀或者反光,导致某些角点对比度太低。可以尝试调整光源方向,或者用
cv2.adaptiveThreshold预处理。 - 图片模糊或者运动拖影,角点边缘不锐利。这种情况只能重新采集。
- 标定板没有完整出现在画面里,边缘的角点被裁掉。务必保证所有内角点都在画面内,且有少量边距。
排查时可以先对单张图做可视化,把检测到的角点画出来,确定是检测算法的问题还是图像本身的问题。
5.2 标定结果漂移、误差大怎么处理
如果重投影误差大于1像素,或者内参明显不合理,我建议按顺序做三件事:
第一,检查所有图片中,棋盘格姿态是否足够丰富。光有不同远近、没有旋转倾斜的图片,外参估计会病态,内参也不可靠。最好保证棋盘格在每张图里都有明显的朝向变化,比如绕x轴、y轴各转15到30度。第二,剔除角点检测质量差的图片。可以在findChessboardCorners之后,加一个cv2.cornerSubPix前后的角点坐标差值统计,差异超过0.5像素的图直接标记出来。第三,尝试降低畸变模型阶数。OpenCV默认用5个畸变系数,但普通镜头用3个或2个往往更稳定。代码里可以传calibrateCamera(..., flags=cv2.CALIB_ZERO_TANGENT_DIST)只估计径向畸变,或者用CALIB_FIX_K3固定k3为0,减少参数过多带来的波动。
5.3 双目、红外等扩展场景的注意点
很多人标定完单目之后想继续做双目,或者用红外相机。双目标定不是简单的两个单目拼起来,而是在单目内参基础上再做立体标定,用cv2.stereoCalibrate求出两个相机之间的旋转和平移。张正友标定法同样适用,但需要左右相机同时采集同一块棋盘格,所以采集时要保证标定板在左右画面中同时可见,且尽量覆盖公共视野。如果你用红外相机,注意红外光的波长会让镜头畸变跟可见光不同,必须单独标定,不能直接复用可见光的参数。
至于热搜词里提到的Kalibr标定,那是VIO/SLAM领域更常用的标定工具,它支持标定相机内参、相机与IMU之间的外参。原理上和张正友标定法同源,但对时间戳同步、IMU残差处理有额外要求。如果你在做SLAM,可以先按本文把单目内参和畸变系数标准,再用Kalibr完成协同标定,两者并不冲突。
6. 最后再分享一点实操体会
我在实际项目里标过几十台相机,最大的感受是:张正友标定法本身已经很成熟,出问题的地方基本都在数据采集端。与其反复调代码参数,不如多花时间把标定板贴平、把图片拍好、把角度覆盖到位。自己打印棋盘格时,千万不要用普通的喷墨纸直接平铺在桌上,稍微翘起一角,角点在空间上就不再共面,最终标定的主点会明显偏移。我一般会把打印好的格子纸用双面胶贴在亚克力板或者玻璃板上,边缘压平,再量一下实际尺寸填入代码。
另外,标定完别急着删除测试图片,我习惯把每张图的角点检测结果和重投影误差都保存下来,作为一份“数据报告”。如果后续发现某个镜头换过或者焦距动过,还能快速定位是哪一批图出了问题。最后再说一个小技巧:标定代码里把objp的坐标写成实际毫米值,不仅方便和机械臂、移液平台等对量的场景对接,也便于团队成员之间交叉验证结果。希望这份流程能帮你少踩几个坑,一次跑通标定。
本文还有配套的精品资源,点击获取