简介:张正友相机标定OpenCV实现资料包,适合机器视觉、摄影测量等方向的初学者与工程师使用,依托OpenCV完整实现经典张正友标定算法,可解决相机内参、外参与畸变系数的精确求解问题。压缩包共107个文件,约95.67MB,主要包含60张手机及摄像头拍摄的标定测试图、可打印棋盘格PDF、VS2015工程源码、可执行exe和用于标定数据记录的txt/mat文件;源码带详细注释并支持一键运行,测试图片也可直接替换为自己的采集数据,工程解决方案文件可打开编译,便于结合OpenCV环境二次开发。该工程覆盖从读图、角点提取到标定结果输出与效果评价的完整流程,包含旋转平移矩阵、畸变矫正演示,标定前后棋盘图对比直观,标定结果与Matlab非常接近。已有6817人学习下载,无论是快速搭建标定实验环境,还是对照源码理解算法细节,都很实用。
1. 张正友相机标定:这套带棋盘格PDF与测试图片的OpenCV方案怎么用
最近在整理视觉测量项目时,拆了一套张正友相机标定资源:实现基于 OpenCV,里面带可打印的棋盘格 PDF 和一组实际拍摄的测试图片。做机器视觉的人都知道,标定结果的稳定性一半靠算法,另一半靠标定板。很多新手在网上随便找一张棋盘格图片打印就开拍,结果角点检测率低、重投影误差忽大忽小,整个流程跑起来像玄学。这套资源把最容易被忽略的两件事提前备好了:能直接打印的棋盘格文件,以及覆盖多角度、多距离的测试图片,让标定不再依赖“临时画板”。适合刚接触相机标定的开发者,也适合正在做机械臂抓取、AGV 导航或工业缺陷检测的人。下面按我实际操作顺序讲原理、参数、代码和踩坑记录。
2. 标定原理与棋盘格准备:为什么棋盘格能稳定解出内参
在跑代码之前,我习惯先把张正友标定的原理重新过一遍。每次标定翻车,大概率不是代码写错,而是对“棋盘格平面坐标”和“图像坐标”之间的关系理解不透。这里不堆公式,只讲工程上最需要记住的三个点:单应性模型、畸变模型、最少姿态数。
2.1 单应性、内参与畸变:一张棋盘格为什么能算出相机参数
张正友标定法把棋盘格平面当作世界坐标系的 z=0 平面。三维空间里一个已知点 P=[X,Y,0] 投影到图像上的过程,可以简化成单应性矩阵 H=K[r1 r2 t]。这里的 K 是内参矩阵,包含 fx、fy、cx、cy;r1、r2、t 是棋盘格在相机坐标系下的姿态,也就是外参。因为我们只关心棋盘格这一个平面,把三维标定转成了二维平面标定,这也是张正友方法比传统立体标定好用得多的原因:只需要拍摄不同姿态的平面棋盘格,不需要昂贵的 3D 标定块。
单应性矩阵 H 有 8 个自由度,一张姿态图能提供 8 个方程。未知数包括 5 个内参和当前姿态的 6 个外参,净贡献只有 2 个方程,所以最少需要 3 张姿态不同的图才能解出全部内参。实际操作里 3 张远远不够,因为还有噪声和亚像素定位误差,资源包里的测试图片一般会拍到十几张以上,靠冗余约束把误差压下去。
畸变部分是工程调试里更常用的概念。OpenCV 默认的畸变模型包含径向畸变系数 k1、k2 和切向畸变系数 p1、p2。径向畸变来自镜头曲率,表现在画面边缘的桶形或枕形变形;切向畸变主要来自镜头和感光芯片的装配误差。标定的 dist 输出顺序是 [k1, k2, p1, p2],如果使用高阶模型则是 [k1, k2, p1, p2, k3],使用去畸变接口时别搞反。
工程上还有一个容易被忽略的尺度问题:棋盘格上世界点的单位是 mm 还是 cm,对最后的内参结果没有本质影响。因为单应性里的尺度是耦合的,把世界坐标同时放大十倍,等价于换一把不同的尺子量标定板。所以用任意单位都能标定,但代码里必须固定同一个 square_size,否则每个角点的三维坐标发生平移,误差会跳。我一般把打印后的方格实际边长量出来填成毫米,这样后续做单目测量时,重建出的坐标直接用毫米表达,省去单位换算。
2.2 棋盘格规格与打印参数:内角点数、真实边长和纸张选择
资源包里的棋盘格 PDF 是按常见规格做的,内角点数量是 9×6。注意“内角点”不是“黑白格子数”:10×7 个格子围成的内部交点才是 9×6。OpenCV 里 CHECKERBOARD 参数填的是内角点数,填成格子数会导致检测失败,这是最高频的一个坑。
| 内角点数(OpenCV参数) | 黑白格子数 | 典型适用场景 |
|---|---|---|
| 7×5 | 8×6 | 小靶标,近距或小视野相机 |
| 9×6 | 10×7 | 通用镜头,工作距离300mm左右 |
| 12×9 | 13×10 | 大视野、远距离标定 |
打印参数上,以下几点直接影响角点可检测性。尽量用激光打印,喷墨打印的墨迹在纸张表面会产生轻微晕染,黑格边缘不够锐利,小尺寸格子在亚像素细化时误差会偏大。打印后不要用 PDF 的“默认尺寸”直接开拍,拿直尺或卡尺量五个以上方格的累计宽度,取平均边长填进代码。不同打印机的缩放可能差 1% 左右,这个误差会直接传导到重建尺寸里。纸张必须贴在刚性平面上,普通 A4 纸悬空时会有微小的弯曲,标定算法假设棋盘格是理想平面,纸张弯曲会被当成畸变吸收,内参精度就会下降。
方格边长建议在 20~30mm 之间。太小时角点间距不足,亚像素细化窗口里有效梯度太少;太大时标定板体积大,近距拍摄容易出画面。资源包里的棋盘格 PDF 只保留了棋盘格本身,没有额外角标和装饰文字,这是对的。装饰物会被 findChessboardCorners 当成干扰区域,影响二值化阈值判断,所以打印时也不要自己在边缘画标记。
2.3 资源包的文件组织与标定前置检查
这套资源解压后,大致是这样一个目录结构:
calib/ ├── board.pdf # 可打印棋盘格 ├── images/ # 测试图片 ├── calibrate.py # OpenCV 标定脚本 └── undistort_demo.py # 去畸变演示脚本不同分发版本的打包方式可能有差异,但核心项是固定的:棋盘格 PDF、一组测试图片、标定与去畸变脚本。拿到后先做三件事再跑代码:打开 board.pdf,确认内角点数量确实是 9×6;用直尺量出方格实测边长,比如 24.8mm;从 images 目录挑一张图,确认棋盘格完整出现在画面里,没有被裁掉四分之一。这三个检查做完,后面的代码才不会因为输入数据错误反复跑不通。
提示:测试图片如果来自手机,先确认拍摄时是固定焦距。自动对焦过程中标定出来的 fx 会漂移,重投影误差会异常大。
3. OpenCV 标定全流程实操:角点检测、内参输出与误差验证
资源包里的 Python 脚本跑通后,我按顺序把完整流程拆开讲一遍,从读图到评价标定质量,每一步都标了参数含义。这里用 Python + OpenCV 为例,结构最直观,C++ 迁移时接口名和参数顺序基本一致。
3.1 角点检测与亚像素细化:findChessboardCorners 参数怎么设
import cv2 import numpy as np import glob CHECKERBOARD = (9, 6) # 内角点数:列9,行6 SQUARE_SIZE = 25.0 # 打印后实测的方格边长,单位mm criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 30, 1e-4) objp = np.zeros((CHECKERBOARD[0] * CHECKERBOARD[1], 3), np.float32) objp[:, :2] = np.mgrid[0:CHECKERBOARD[0], 0:CHECKERBOARD[1]].T.reshape(-1, 2) objp *= SQUARE_SIZEobjp 在构建棋盘格世界坐标系里的 54 个交点,x、y 从 0 开始按格子索引递增,z 全部为 0。mgrid 生成坐标后通过 reshape 展平,顺序和 OpenCV 在图像里检测角点的顺序是对应的。后面的 objp *= SQUARE_SIZE 把“格子索引”换算成实际毫米坐标,这样重投影误差和三维重建结果里才带有真实尺度。
然后是逐图检测:
objpoints = [] imgpoints = [] for fname in sorted(glob.glob('images/*.jpg')): img = cv2.imread(fname) gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) ret, corners = cv2.findChessboardCorners(gray, CHECKERBOARD, None) if not ret: print('角点检测失败:', fname) continue corners2 = cv2.cornerSubPix(gray, corners, (11, 11), (-1, -1), criteria) objpoints.append(objp) imgpoints.append(corners2)findChessboardCorners 的第二个参数是内角点尺寸,第三个参数用于接收检测结果,传 None 即可。返回 False 时直接跳过该帧。如果超过三成图片检测失败,不要急着怀疑代码,先检查光照和棋盘格是否完整。
cornerSubPix 的窗口尺寸 (11,11) 不是越大越好。窗口超过方格在图像里像素长度的三分之一时,容易跨越黑白边界,亚像素迭代会收敛到错误梯度。建议取方格像素长度的 0.3~0.5 倍。终止条件 30 次迭代加 1e-4 像素 EPS 精度,对绝大多数 1200 万像素以下的图都够用。
为了直观排查,我把检测结果画出来看:
vis = cv2.drawChessboardCorners(img.copy(), CHECKERBOARD, corners2, ret) cv2.imshow('corners', vis) cv2.waitKey(0)drawChessboardCorners 会把按顺序检测到的角点用彩色圆圈标出来,一眼能看出是否有错位或漏检。现场标定时这一步比打印一堆误差数值更直观。
如果现场光照不均,可以在检测时追加两个 flags:
flags = cv2.CALIB_CB_ADAPTIVE_THRESH + cv2.CALIB_CB_NORMALIZE_IMAGE ret, corners = cv2.findChessboardCorners(gray, CHECKERBOARD, None, flags)ADAPTIVE_THRESH 让二值化不用全局固定阈值,应对局部光照不均;NORMALIZE_IMAGE 在阈值前先做归一化,等价于自动对比度拉伸。资源包里的测试图片在拍摄时控制了光照,一般不需要加 flags,但拿到现场用会经常用到。
3.2 主标定流程:calibrateCamera 的输入输出与参数含义
rms, mtx, dist, rvecs, tvecs = cv2.calibrateCamera( objpoints, imgpoints, gray.shape[::-1], None, None ) print('重投影RMS: {:.3f} px'.format(rms)) print('内参矩阵:\n', mtx) print('畸变系数:', dist.ravel())calibrateCamera 的五个返回值依次是重投影均方根误差、内参矩阵、畸变系数,以及每张图对应的旋转向量和平移向量。第三个参数 imageSize 是灰度的 (width, height) 顺序,直接用 gray.shape[::-1] 最不容易写错。
内参矩阵的物理意义要清楚。fx、fy 分别是水平和竖直方向以像素为单位的焦距,公式是 fx = f / dx,即物理焦距除以单个像素的物理宽度;cx、cy 是主点,理想情况接近图像中心,装配误差会让它偏离几十像素。做机械臂抓取这类从像素反算三维坐标的任务时,cx、cy 偏差几十像素足够让抓取位置偏出几毫米。畸变系数顺序是 k1、k2、p1、p2,如果带 k3 则排最后。p1、p2 通常比 k1、k2 小一个数量级,如果大于 0.1,说明镜头装配有问题,需要先检查镜头座。
rvecs、tvecs 里保存的是每张测试图片里棋盘格相对相机的姿态。rvecs 是旋转向量,需要用 Rodrigues 转成 3×3 旋转矩阵才能直观解读。不需要把所有外参打出来,但要关注不同图片之间的姿态差异是否明显。如果所有姿态都堆在一起,标定结果就会缺乏约束,第 4 章会展开说。
3.3 重投影误差:判断标定结果能不能用
OpenCV 返回的 rms 是一个整体评价,我还会单独算每张图的误差,用来定位哪张图拉低了整体精度:
def per_image_reprojection_error(objpoints, imgpoints, rvecs, tvecs, mtx, dist): for i in range(len(objpoints)): proj, _ = cv2.projectPoints( objpoints[i], rvecs[i], tvecs[i], mtx, dist ) err = cv2.norm(imgpoints[i], proj, cv2.NORM_L2) / len(proj) print('image {}: {:.3f} px'.format(i, err))projectPoints 把世界坐标按外参投影到图像平面,再和检测到的角点坐标做 L2 范数平均。误差单位是像素。阈值怎么定:整体 RMS 在 0.5px 以下能正常使用;0.3px 左右说明标定图质量很好;超过 1px 就先查方格尺寸、模糊图像和姿态多样性,不要急着堆图片数量。误差和数量不一定是线性关系,重复姿态加再多图也只是把同一个约束重复十遍。
4. 避坑排查:标定结果不稳定的五个直接原因与修正办法
标定脚本能跑通,不代表结果能用于测量。以下五个坑是我在不同项目里实际遇到的,前两个在测试图片上就能触发,后三个属于设备迁移和现场采集时的高频问题。每条按现象、原因、解决的顺序写。
4.1 角点检测失败集中在边缘:反光和棋盘格裁切
现象:多张图片中央都能检测出角点,棋盘格出现在画面边缘时 findChessboardCorners 返回 False,代码直接跳过该图,导致最终参与标定的全是“棋盘格在中央”的图。
原因主要有两个:一是纸板边缘处有环境光反射,黑格被高光洗掉后全局二值化分不清边界;二是棋盘格没有完全落在画面内,边缘倒数第二排格子的角点被截断。
解决方式是分两步走。先处理亮度过高的图片,在调用 findChessboardCorners 之前做灰度归一化,或者直接使用 CALIB_CB_NORMALIZE_IMAGE 标志;再检查采集时棋盘格的留白。图像四周至少留出两三个格子的空白区域,不要为了“拍大一点”把棋盘格顶到画面边缘。资源包里的测试图片如果有个别出现这种问题,直接丢弃就行。
4.2 全部角点检测成功但重投影误差超过1px:多半是 square_size 和实际不符
现象:ret 全部为 True,RMS 稳定在 1.2px 左右,增加图片数量也掉不下来。
原因排在首位的不是算法,而是坐标尺度不一致。比如 PDF 打印后实际方格边长是 24.6mm,代码里仍填 25.0mm,所有世界点坐标都偏大 1.6%,重投影误差会沿 X、Y 方向积累,表现就是误差整体偏大且稳定。
解决方法是拿直尺量 5 个相邻方格的累计宽度,除以 5 得到平均边长,把 SQUARE_SIZE 替换成实测值重新跑一遍。如果 RMS 从 1.2px 掉到 0.3px 以下,基本可以确认就是尺度问题。这个动作看似基础,却是“误差稳定在同一个固定值”时最值得优先排查的一项。
4.3 换分辨率后直接复用旧内参:边缘变形严重
现象:同一台相机,1920×1080 标定完成后,程序切换成 640×480 采集,直接套原来的 mtx 和 dist,画面边缘出现严重拉伸,测量位置完全偏移。
原因很直接:fx、fy 描述的是像素焦距,分辨率改变后每个像素对应的物理尺寸已经不同,焦距值必须成比例缩放;主点 cx、cy 也会随分辨率中心平移。直接复用旧参数等于告诉 OpenCV 一块 1920 宽的传感器却只给了 640 个像素,换算结果完全乱套。
解决方法是做一次线性缩放:
def scale_camera_matrix(mtx, old_size, new_size): sw = new_size[0] / old_size[0] sh = new_size[1] / old_size[1] m = mtx.copy() m[0, 0] *= sw m[1, 1] *= sh m[0, 2] *= sw m[1, 2] *= sh return m参数顺序是 (width, height)。缩放后 dist 不用变,因为畸变系数描述的是镜头光学特性,跟传感器分辨率无关。资源包里的测试图片是固定分辨率,换到工业相机时这个函数会频繁用到。
另外提醒一句:用自动对焦的 USB 相机或手机标定时,如果拍摄过程中镜头发生变焦,fx 也会漂移。这种漂移不会体现在单张图的重投影误差里,而是表现为前后两次标定结果不一致。标定前把相机切到手动对焦并固定,一个下午的返工就能省掉。
4.4 去畸变后画面边缘出现波浪形曲线:畸变阶数不够
现象:中心区域校正效果明显,但画面四周直线变成弯曲的波浪线,看起来比原图还夸张。
原因是默认畸变模型只带 k1、k2 两项径向畸变,对普通广角镜头够用,但对大广角甚至鱼眼镜头,高阶畸变占主导,二阶径向模型拟合不了,残余误差在图像边缘被放大。
解决方法是先判断镜头视场角。视场角在 100 度以内,可以沿用传统模型,把畸变系数扩展为 [k1, k2, p1, p2, k3] 六项或八项数组,同时保证标定板边缘姿态占一半以上,否则高阶项容易解成病态矩阵。视场角更大时,改用 cv2.fisheye 模块,它内置了更适合强径向畸变的模型。判断是否需要高阶模型有一个笨办法:看原始图像边缘的一条直线,如果弯得很厉害,基本可以确定二阶模型压不住。
4.5 图片拍了很多张,重投影误差反而变大:姿态重复导致约束不足
现象:角点全部检测成功,RMS 开始是 0.4px,往后面加图误差反而增大,去掉后面加的那批图又回到 0.3px。
原因是后加的图大量重复了已有姿态:都是棋盘格平行正对相机、只做前后平移。张正友方法靠不同姿态去约束内参,平行正对的图提供的约束几乎相同,对超定方程不仅没帮助,还会因为个别轻微模糊的图把整体误差抬高。
解决的思路是保证姿态多样性,而不是追求数量。每组拍 20 张左右,保证其中至少有:棋盘格正对相机、左右旋转 20~30 度、上下俯仰 20~30 度、棋盘格出现在画面四个角落的组合。资源包里的测试图片基本按这个规则拍的,可以直接对照检查自己的图片集。
5. 用测试图片做端到端验证:去畸变、直线度与物理尺寸换算
标定跑完后输出的是中间产物,还要用测试图片做端到端验证,确认参数真正适用于实际图像。这一章讲三个层面的验证方法:图片集质量、去畸变效果、物理尺寸换算。
5.1 什么样的图片集才是有效标定集
拿到一份标定图片集,先不要把全部图片丢进 calibrateCamera。有效标定集有三个条件:棋盘格完整出现在每张图;棋盘格在图中占的面积不能太小,否则角点定位可信度不高,一般要求长边方向至少占画面四分之一;姿态种类多,且边缘姿态足够。资源包里的测试图片按这个思路拍摄:有正对相机的,有左右倾斜的,还有棋盘格在左上角和右下角出现的情况。我一般把测试图片分成三类:正中占比约 40%、边缘占比约 40%、倾斜占比约 20%,这个比例和最终标定效果匹配得不错。
有拖影或模糊的图直接删。标定对模糊的容忍度比普通特征点提取更低,模糊图在亚像素细化阶段误差会指数上升,拖累整体 RMS。手持手机拍摄时,注意快门速度尽量快一些,最简单的方式是保证光线充足,并在拍完后放大检查边缘格子的清晰度。
5.2 去畸变对比:initUndistortRectifyMap 加 remap
资源包里的 undistort_demo.py 核心是这段:
h, w = gray.shape[:2] newcameramtx, roi = cv2.getOptimalNewCameraMatrix( mtx, dist, (w, h), 1, (w, h) ) mapx, mapy = cv2.initUndistortRectifyMap( mtx, dist, None, newcameramtx, (w, h), cv2.CV_32FC1 ) result = cv2.remap(img, mapx, mapy, cv2.INTER_LINEAR) x, y, w2, h2 = roi result = result[y:y + h2, x:x + w2]initUndistortRectifyMap 的第四个参数 newcameramtx 是去畸变后要采用的新内参矩阵。直接传原内参会得到一张边缘被拉直但视野缩水的图;传 getOptimalNewCameraMatrix 的返回值,可以在去畸变的同时尽量保留原始视野。这里的 alpha=1 表示保留全部原始像素范围,生成的结果四周可能带有黑色无信息区域;alpha=0 会直接裁剪掉黑色区域,但牺牲部分边缘像素。两种取值的取舍,下一章专门展开。
ROI 是有效像素区域,由 getOptimalNewCameraMatrix 在计算新内参时同时给出。如果裁剪这段区域,去畸变后的图像尺寸会小于原图,且主点必须同步调整,否则后续测量会整体偏移。
验证参数可靠性时,我会用去畸变后的棋盘格再做一次直线度检查:重新提取棋盘格角点,把每一行的角点连线,观察连线是否变成严格直线。这个验证方式的灵敏度比重投影误差更高,也是我每次标定后习惯走的最后一步。
5.3 像素当量换算:标定完怎么从像素拿到物理尺寸
标定结果最终要落到测量上。单目视觉里最简单的是“像素当量”法:在固定工作距离下,拍摄已知尺寸物体,用标定参数计算每个像素对应的物理尺寸。这个方法受距离影响大,只适合物距固定的场景,比如传送带上的尺寸检测。
更严谨的做法是利用标定外参结果做单应性测量:把棋盘格放在被测平面上,识别角点后用 cv2.findHomography 建立图像到平面的映射,再测量平面上任意两点的物理距离。因为单应性矩阵里包含棋盘格姿态,这种测量不要求物体严格垂直于光轴,精度能做到亚毫米级。验证方式很直接:量棋盘格上某个方格的物理边长,投影到图像坐标里取对应两点距离,和直接测量值对比,偏差在 0.5mm 以内就算正常。
6. 进阶技巧:用 alpha 参数解决去畸变黑洞并保住视野
去畸变后出现黑边是个很让人头痛的问题。现象很典型:一张正常拍摄的图像,remap 之后四角出现大块黑色无信息区域,像个相框,放到演示素材里非常难看。
问题来自去畸变算法的本质:畸变图像素映射时,目标图像边缘没有来源,OpenCV 就把空值填成黑色。想同时减少黑边又能保留尽量多的有效像素,关键是控制 getOptimalNewCameraMatrix 的 alpha 参数。alpha 是自由缩放系数,范围从 0 到 1:0 表示结果尽量只保留有效像素,四角裁剪最狠;1 表示保留完整原始视野,黑边往往最明显;0.5 是两者折中。
alpha = 0.5 newcameramtx, roi = cv2.getOptimalNewCameraMatrix( mtx, dist, (w, h), alpha, (w, h) ) mapx, mapy = cv2.initUndistortRectifyMap( mtx, dist, None, newcameramtx, (w, h), cv2.CV_32FC1 ) result = cv2.remap(img, mapx, mapy, cv2.INTER_LINEAR) x, y, w2, h2 = roi result_cropped = result[y:y + h2, x:x + w2] new_cx = newcameramtx[0, 2] - x new_cy = newcameramtx[1, 2] - y print('裁剪后主点: ({:.1f}, {:.1f})'.format(new_cx, new_cy))这段逻辑里最重要的是对主点的补偿。ROI 裁剪把图像坐标系的原点从 (0,0) 移到了 (x,y),主点也必须跟着平移,否则后面用像素坐标反算物理坐标时会出现一个固定偏移。裁剪之后的 new_cx、new_cy 才是最终写入配置文件的参数。
实际操作中我会这样选 alpha:如果做视觉定位且要求边缘视差小,alpha 直接给 0,接受视野损失;如果做视频演示或需要保留完整画面用于人工观察,alpha 给 1;需要平衡时给 0.5。也可以用组合思路:先用 alpha=1 得到完整校正图,确认边缘保留情况,再逐步减小 alpha 观察黑边回缩速度,选定合适值后把对应的 newcameramtx 固化到标定文件里。
有一次我在现场调机械臂抓取,用了 alpha=1 的校正图,结果末端视觉定位在画面边缘总是偏 4mm,查了半天才发现问题出在裁剪后主点没更新,而不是标定参数。从那以后每次跑完标定,我都会强制走一遍固定流程:记录原始分辨率、输出 alpha 与 ROI 坐标,把裁剪后的主点写进新配置,再用测试图片做一次直线度验证,四步同时通过后才把参数发给下游模块。这套流程帮我挡掉了不少现场返工。希望帮到你。
本文还有配套的精品资源,点击获取