简介:这是一套基于OpenCV实现张正友相机标定的完整工程代码,主要面向计算机视觉初学者、机器人导航与三维重建领域的开发者,解决相机内参与外参求解、畸变矫正等基础但关键的问题。工程基于VS2015编写,包含详细注释与一键运行脚本,覆盖从棋盘格图像读取、角点提取到标定结果评价与图像矫正的完整流程;同时附带可打印棋盘格PDF、10张手机拍摄标定图和20张摄像头拍摄标定图,无需额外采集即可体验整个标定过程。资源包共107个文件,以jpg测试图像、cpp源码、pdf棋盘格、exe可执行文件为主体,整体大小95.67MB,目录组织清晰,便于按模块研读。标定输出包括相机内参矩阵、旋转向量与平移向量,并自动计算重投影误差以评价精度,利用标定结果可直接对原始棋盘图进行去畸变矫正;实测所得标定参数与Matlab标定工具箱结果非常接近,适合作为理论学习的配套实践或项目预研的参考。该资源已有6817人学习下载,兼顾教学演示与实际应用,是快速掌握张正友标定法的优质材料。
1. 张正友标定:为什么 OpenCV 实现仍是当下最实用的内参求解路线
手头一个工业相机装上镜头后,画面边缘的直线全是弯的,用相机做尺寸测量、做机器人抓取定位,第一步就得把内参和畸变系数算准。张正友标定法是目前最省器材的解法——不需要昂贵的三维标定块,只要一张 A4 打印的棋盘格,从不同角度拍十几张图,交给 OpenCV 就能解出相机内参、畸变系数,还能顺带拿到每张图拍摄时相机相对棋盘格的位置姿态。这套方案的实际效果,直接决定后续去畸变、三维重建和位姿估计的精度,适合做机器视觉、SLAM 初始化、增强现实注册的开发者。本文就从原理、资源包结构、完整实现到踩坑记录,把这条路线完整拆开。
2. 标定原理与资源包结构:从棋盘格几何约束到内参矩阵的五个关键量
2.1 张氏标定的数学模型:一张棋盘格为什么能解出五个内参
传统标定需要一个精密加工的三维标定块,让相机拍摄已知空间坐标的角点,再用透视投影关系直接建立方程。张氏标定的巧妙之处在于把三维靶标降到了二维——棋盘格平面上的所有角点都满足 Z = 0,这样每个角点的三维坐标只剩 X 和 Y 两个自由度。三维世界坐标到像素坐标的投影矩阵此时退化为一个 3×3 的单应性矩阵 H,物理世界的平面点和像素点之间就构成了一个直接的射影映射。
具体来说,相机的针孔模型写成 s·m = K·[R|t]·M,其中 M 是世界坐标、m 是像素坐标、K 是内参矩阵。当 M 所在的平面是 Z = 0 时,旋转矩阵 R 的第三列被消掉,剩下的关系变成 s·m = H·M,H = λ·K·[r1 r2 t]。这个 H 就是每张棋盘格图片都能算出来的单应性矩阵。既然 H 已知,而 K 内又藏着 fx、fy、cx、cy、s 这五个量的信息,张正友证明了从单应性矩阵的约束条件中,可以把内参逐步分离出来。
每一张棋盘格照片提供的两个基本约束来自旋转矩阵的列向量 r1 和 r2 的单位正交性。因为 r1 和 r2 是真实旋转矩阵的前两列,它们满足 r1^T·r2 = 0 且 |r1| = |r2|。把 H 分解后代入这两个条件,每个条件就变成了一个关于内参矩阵 K 的二次方程。每一个视角提供两个方程,五个内参理论上至少需要三张不同角度的图像才能线性解出。实践中我到手最少拍 12 张,常规项目拍到 18 到 20 张,原因很简单:角点检测有像素级噪声,方程数量多了才能让最小二乘解稳定下来。
畸变模型的加入让问题从线性求解变成了非线性优化。张氏标定在得到线性初值后,会用 Levenberg-Marquardt 算法迭代最小化所有角点的重投影误差,优化变量包括内参、畸变系数 k1、k2、p1、p2 以及每张图的外参。也正因为存在这套优化机制,它不需要像 DLT 那样对每张图片单独求投影矩阵,而是把相机内参作为全局共享量做联合估计。这也是为什么同一组图片,张氏标定在 OpenCV 里的求解结果比早期直接求单应性矩阵的粗糙做法稳定得多。
2.2 资源包拿到手:棋盘格PDF、测试图片与标定脚本三件套怎么协同
典型的张正友标定资源包,通常包含三类内容:可打印的棋盘格 PDF、不同角度拍摄的测试图片集、以及一套基于 OpenCV 的标定脚本。三者的关系是一个完整的流水线——PDF 负责生成物理靶标,测试图片是输入数据,脚本完成角点提取、标定和参数输出。拿到资源包的第一步不是立刻跑代码,而是先做文件体检,我一般会按下面这个流程检查:
| 资源项 | 关键检查点 | 用途 |
|---|---|---|
| 棋盘格 PDF | 格子行列数、单格边长标注、是否含多个尺寸版本 | 决定 patternSize 参数与实际物理尺寸 |
| 测试图片集 | 数量是否 ≥ 10 张、分辨率是否一致、棋盘格是否完整入镜 | 决定标定方程约束是否充分 |
| 标定脚本 | 是否单文件、依赖 OpenCV 版本、是否含保存参数的逻辑 | 决定后续验证和复用方式 |
棋盘格 PDF 的坑在于打印缩放。大多数 PDF 是按 100% 比例设计的,但打印机的“适应页面”选项会擅自缩放,实际打印出来的格子边长和 PDF 标注值相差几个百分点。拿到打印好的棋盘格,我必做的一件事是用卡尺量三个不同位置的格子边长,取平均值填入脚本的 square_size 变量。这个值影响的是外参平移向量 t 的单位,内参 fx、fy 是像素单位下的焦距,不受影响;但如果后面要用标定结果做三维测量,比如算棋盘格到相机的距离,square_size 错了距离就全错了。
测试图片集的检查重点是角度覆盖。合格的测试集应该包含正拍、左右侧倾、上下俯仰、近拍和远拍几种情况,而且棋盘格不能总停留在画面中央。如果图片都是同一个视角拍的,单应性约束退化,内参解不唯一,重投影误差再低也是假象。另外图片分辨率必须一致,我遇到过资源包里混入一张尺寸不同的图,导致 imagePoints 和 objectPoints 的维度对不上,脚本在第一轮就报错。
脚本部分的代码组织方式各有不同。有的资源包把角点提取和标定放在一个脚本里跑完,有的则分成 extract_corners.py 和 calibrate.py 两个阶段。分步的好处是第一步提取角点后可以人工检查是否有误检,再进标定;单文件的省事但排错时容易一锅端。我习惯拿到脚本后先跑一遍原始数据和默认参数,确认能输出结果,再根据自己相机的分辨率、棋盘格尺寸去调整参数,而不是直接改代码。
3. OpenCV 标定全流程:角点提取、亚像素细化与 calibrateCamera 参数设置
3.1 第零步:准备棋盘格的行列数、物理尺寸与拍摄数量
实现之前最容易被忽略的三个参数是内角点数、格子实际边长和图片数量。棋盘格 PDF 上标注的是格子数,比如 10 列 × 7 行,而 OpenCV 的 findChessboardCorners 要求传入的是内角点数——也就是格子之间的交叉点数量,等于 9 × 6。这一点我踩过至少三次坑,每次都是拿着 10×7 的格子数直接填 patternSize,结果要么检测失败,要么画出来的角点连边错乱。
棋盘格物理尺寸的选择要兼顾提取稳定性和畸变覆盖。单格边长在 20mm 到 30mm 之间比较合适,太小了远拍时角点在图像里只有几个像素,亚像素细化也救不回来;太大了近拍时棋盘格超出画面,没法覆盖画面边缘。打印出来的棋盘格要贴在平整的硬板上,平面度直接影响标定精度。
拍摄数量的经验值通常取 15 到 20 张。我的拍摄分配策略是:先正拍一张,然后左右各转 15 度、30 度分别拍,再上下倾斜各拍几张,最后让棋盘格分别出现在画面左上、右上、左下、右下四个角落各拍一张。远近变化也很重要——保证棋盘格占画面宽度的 1/3 到 2/3 之间浮动。
3.2 角点提取与亚像素细化:findChessboardCorners 到 cornerSubPix 的用法
拿到测试图片后,先从灰度图走角点提取。OpenCV 的 findChessboardCorners 对光照鲁棒性还行,但反光、阴影和模糊都会导致检测失败,所以提取前可以用自适应直方图均衡化增强一下对比度。下面的代码是提取和细化角点的核心部分:
import cv2 import numpy as np # 棋盘格内角点数:10列x7行格子 -> (9, 6) pattern_size = (9, 6) # 亚像素细化的迭代终止条件:迭代30次或精度达到0.001像素 criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 30, 0.001) corner_list = [] for img_path in image_paths: img = cv2.imread(img_path) gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) # 寻找棋盘格内角点 ret, corners = cv2.findChessboardCorners(gray, pattern_size, None) if not ret: print(f"角点提取失败: {img_path}") continue # 亚像素级细化:窗口11x11,避免靠近边缘时越界 corners_refined = cv2.cornerSubPix( gray, corners, (11, 11), (-1, -1), criteria ) corner_list.append(corners_refined) # 可视化确认:把角点画回原图,人工检查排列顺序 img_show = img.copy() cv2.drawChessboardCorners(img_show, pattern_size, corners_refined, ret) cv2.imshow("check", img_show) cv2.waitKey(100)findChessboardCorners 返回的 ret 是布尔值,OpenCV 的策略是要么找到全部内角点,要么一个都不找,所以不用担心中途部分成功导致点数不一致。cornerSubPix 的原理是基于梯度正交性迭代优化角点位置,让角点在两个正交梯度方向上的坐标误差同时最小。winSize 是搜索窗口半径,取 11 对 1280×720 分辨率的图够用;如果图片分辨率更高,窗口可以适当加大到 15。zerzone 设为 (-1, -1) 表示窗口中心不设禁区。
drawChessboardCorners 画出来的是按行列顺序排列的彩色连线,这一步务必人眼确认。角点的提取顺序是先行后列、每行从左到右,如果棋盘格在画面中发生了旋转,OpenCV 也能按内部逻辑确定起始角点,但前提是棋盘格倾斜角度不要超过 45 度,否则容易出现 180 度翻转的歧义。
3.3 calibrateCamera 参数解说:从五个内参到畸变系数与重投影误差
角点收集完成后,进入标定阶段。objectPoints 是棋盘格在物理平面上的三维坐标,Z 全部为 0,X、Y 按格子间距生成;imagePoints 是每张图的亚像素角点;两者数量必须一致。下面这段代码给出了完整的标定调用方式:
square_size = 25.0 # 单格边长,单位mm,用卡尺实测后填入 objp = np.zeros((pattern_size[0] * pattern_size[1], 3), np.float32) # 生成物理坐标:每列x间隔25mm,每行y间隔25mm objp[:, :2] = np.mgrid[0:pattern_size[0], 0:pattern_size[1]].T.reshape(-1, 2) * square_size object_points = [] # 每张图一份相同的物理坐标 image_points = [] for corners_refined in corner_list: object_points.append(objp) image_points.append(corners_refined.reshape(-1, 2)) # 标定主函数,rms是重投影误差 rms, camera_matrix, dist_coeffs, rvecs, tvecs = cv2.calibrateCamera( object_points, image_points, gray.shape[::-1], # 传入(width, height) None, None ) print("重投影误差:", rms) print("内参矩阵:\n", camera_matrix) print("畸变系数:", dist_coeffs.ravel())calibrateCamera 默认求解的畸变系数是 k1、k2、p1、p2 四个,也就是只含两种径向畸变和两种切向畸变。如果镜头畸变比较大,比如廉价的广角 USB 摄像头,需要把 flags 参数加上 cv2.CALIB_RATIONAL_MODEL,这样会多解一个 k3。加了 k3 之后,dist_coeffs 变成 5 个元素,但求解的自由度变大,对图片数量的要求也更高,少于 15 张图时 k3 容易不收敛。
内参矩阵 camera_matrix 里的 fx 和 fy 是等效焦距,单位是像素,cx、cy 是主点坐标。对多数工业相机,fx 和 fy 理论值应该很接近,如果标定结果两者差了超过 5%,优先检查图片是否被缩放、裁剪或者输入给 calibrateCamera 的 imageSize 是否和实际图片尺寸一致。rvecs 和 tvecs 分别保存每张图的外参,也就是棋盘格坐标系相对相机坐标系的旋转向量和平移向量,长度必须等于成功提取角点的图片数。旋转向量是三维的,后续可以用 Rodrigues 转成 3×3 旋转矩阵。
rms 返回的是所有角点重投影误差的均方根,单位是像素。根据我的经验,rms 小于 0.1 属于非常理想,0.1 到 0.3 在工程上可用,超过 0.5 多半是角点检测、棋盘格平面度或参数设置有明显问题。需要强调的是,rms 只是全局平均指标,单张图可能误差很大但被平均掩盖,下一章专门讲怎么逐张验证。
4. 标定质量验证与去畸变输出:重投影误差之外还要看实测线条
4.1 重投影误差怎么看:RMS 数值不是唯一裁判
多数人标定完只看一个 rms,这就是把标定当成黑匣子的典型心态。rms 是所有图片角点误差的均方根,单张图的局部畸变误差会被稀释,一个 0.08 的 rms 背后可能藏着某张图的极大误差。我的做法是标定完成后,把每张图的平均重投影误差逐张打印出来,定位离群图片再决定是剔除还是重拍。下面这段代码利用标定输出的 rvecs、tvecs 做逐图重投影:
def per_image_error(object_points, image_points, rvecs, tvecs, camera_matrix, dist_coeffs): for i in range(len(object_points)): # 把物理坐标点投影到像素平面 proj, _ = cv2.projectPoints( object_points[i], rvecs[i], tvecs[i], camera_matrix, dist_coeffs ) error = np.sqrt(np.sum((proj - image_points[i]) ** 2, axis=1)) mean_err = error.mean() print(f"第{i:02d}张图 平均误差: {mean_err:.3f} pix, " f"最大误差: {error.max():.3f} pix")逐张输出后,凡是平均误差明显大于整体水平的图片,先检查是不是棋盘格边缘反光、纸张不平整或者画面模糊导致的角点质量下降。剔除个别烂图再重新标定,比保留所有图片靠最小二乘硬扛要有效得多。rms 的评价标准可以按这个表做一个快速判断:
| rms 范围 | 工程判断 | 建议动作 |
|---|---|---|
| < 0.1 | 优秀 | 直接进入去畸变与验证 |
| 0.1 ~ 0.3 | 可用 | 结合逐张误差分析,轻微优化 |
| 0.3 ~ 0.5 | 存疑 | 检查棋盘格平面度与角点检测 |
| > 0.5 | 失败 | 重新拍摄,校正参数设置 |
还有一类情况是 rms 很低但实际去畸变效果差:棋盘格图像全部集中在画面中央区域,边缘畸变没有被观测数据约束,畸变参数就成了无约束自由量,优化器只能给出数值上凑合的估计。所以第四步还有一个硬性检查——把棋盘格单独放在画面四个角落拍四张图,如果这批图的重投影误差普遍偏高,说明畸变模型和真实镜头不匹配,或者原本的畸变系数初始值选得不对。
4.2 基于标定结果做去畸变:initUndistortRectifyMap 与 remap 的参数陷阱
拿到内参和畸变系数后,最直接的验证手段是拿一张棋盘格图片做去畸变,观察边缘直线是否恢复平直。OpenCV 提供两种方式,一种是一步到位的 cv2.undistort,另一种是先计算映射表再用 remap 重采样。映射表方式更适合视频流和多张图重复使用,因为映射表只需计算一次,后面每帧只是查表重映射,省掉重复的坐标变换计算。
h, w = image.shape[:2] # alpha=1 保留所有像素视野,边缘可能出现黑边 new_camera_matrix, roi = cv2.getOptimalNewCameraMatrix( camera_matrix, dist_coeffs, (w, h), alpha=1, newImgSize=(w, h) ) # 计算像素坐标映射表,map1为x坐标映射,map2为y坐标映射 map1, map2 = cv2.initUndistortRectifyMap( camera_matrix, dist_coeffs, None, new_camera_matrix, (w, h), cv2.CV_32FC1 ) # 查表重映射,完成去畸变 undistorted_img = cv2.remap(image, map1, map2, cv2.INTER_LINEAR)getOptimalNewCameraMatrix 里的 alpha 参数是这里最容易翻车的点。alpha=0 表示去畸变后裁剪掉所有无效区域,输出图像只保留无畸变且有内容的部分,画面会轻微缩小;alpha=1 表示保留原始图像的全部视野范围,去畸变后边缘会露出黑色空白区域。对于视觉测量场景,我一般选 alpha=1,宁可有黑边也不裁剪视野,因为裁剪会改变图像分辨率与内参的对应关系,后续如果要用去畸变图继续做特征匹配,必须重新确认内参是否仍然适用。
这段代码里还有一个隐藏坑:initUndistortRectifyMap 的最后一个参数类型。很多人习惯用 cv2.CV_16SC2 觉得省内存,但对于亚像素精度要求高的场景,CV_32FC1 是更稳妥的选择,避免 map 坐标的量化误差。remap 的插值方式选 INTER_LINEAR 即可,选更高阶的 INTER_CUBIC 在边缘处容易产生过冲伪影。去畸变之后,可以用直线边缘的图片做个简单检查:把原图和去畸变图并排显示,观察画面边缘的直线是否变直。如果去畸变后边缘反而出现波浪形失真,通常是畸变系数的符号或量级出了问题,需要回到标定环节重新检查 flags 和图片质量。
5. 张氏标定避坑实录:四类翻车现场与对应解法
5.1 内角点数写错导致全部角点提取失败
现象:运行 findChessboardCorners 返回的 ret 全是 False,不管换哪张测试图片都提取不到角点;或者某些图虽然返回 True,但 drawChessboardCorners 画出来的连线顺序错乱。原因:patternSize 误填成了棋盘格的行列数而不是内角点数。比如 10×7 的棋盘格,内角点数量是 (9, 6),写成 (10, 7) 会让检测器在整个棋盘格范围内找不存在的 70 个角点,算法内部在多个尺度上都会失配。解决:先用一张图打印出 pattern_size 的每个候选值跑一遍 findChessboardCorners,从 (cols - 1, rows - 1) 开始试;确定正确内角点数后,再在批量处理时用 assert 保护,一旦有图检测失败就直接打印文件名终止,避免后面 imagePoints 数量不一致引起更隐蔽的错位。
5.2 所有棋盘格都在画面中心,标定误差虚低、去畸变翻车
现象:rms 输出 0.05 左右,看起来非常理想,但用标定结果去矫正一张画面边缘有很多直线的实拍图,边缘依旧是弯的,而且画面四角的畸变完全没有被修正。原因:所有棋盘格图像都集中在画面中央,角点数据只覆盖了图像中心区域。畸变系数本质上是描述像素坐标偏离理想针孔模型的程度,中心区域的径向畸变很小,数据里几乎没有边缘畸变的信息,优化器只能把边缘畸变参数推向一个任意值。解决:重新拍摄,把棋盘格分别放到画面左上、右上、左下、右下四个角落,每放一个位置拍两三张不同角度的图。同时检查每张图里棋盘格是否至少有 70% 面积位于画面边缘带,再重新标定并对比 rms 和去畸变效果。
5.3 打印纸没有贴平板,两次标定结果差了几个百分点
现象:同一台相机,用同一张棋盘格,上午标定的 fx 是 1423,下午重新标定变成 1385,差距超过 2.5%,而且每次 rms 都在 0.2 以内。原因:棋盘格打印纸直接平铺在桌面上,纸面有轻微波浪形翘曲。张氏标定假设靶标是一个严格平面,纸面翘曲等于给每个角点叠加了一个随机的几毫米级 Z 方向偏移,这些偏移被畸变和外参吸收,导致内参不稳定。解决:按照工业上常见的做法,棋盘格打印纸要平贴在刚性的亚克力板或铝板上,四角用胶带或磁吸固定。贴完板子后从侧面观察板面是否有弯曲,或者把棋盘格贴在平整桌面上用重物压住四个角,再做标定验证。
5.4 角点检测成功但标定时报 objectPoints 与 imagePoints 数量不匹配
现象:程序在 build objectPoints 阶段不报错,进入 calibrateCamera 后抛异常,提示 cannot reshape array of size 或者 objectPoints 的维度不一致。原因:某些图片在 findChessboardCorners 时返回了真,但后续处理没有统一 reshape,比如有的 corners 是 (54, 1, 2) 有的被处理成 (54, 2)。另一个常见原因是不同批次的图片分辨率不一致,导致 imagePoints 的数量虽然都是 54,但坐标尺度不同。解决:在收集角点的循环里,强制每个 corners 都执行 corners_refined.reshape(-1, 2),并断言 shape[0] 等于 pattern_size[0] * pattern_size[1];同时在读取图片后统一检查宽度高度,不一致的先 resize 或剔除。我在做多相机标定的时候,还遇到过把两台相机图片混到同一个角点列表里的情况,这类问题很难从报错直接看出来,所以每次批量操作前都要用一条 assert 保证所有图片的 shape 一致。
6. 进阶验证:用 drawChessboardCorners 配合外参绘制三维坐标轴,让标定结果眼见为实
标定完成后,内参和畸变系数已经落盘,但很多人不知道还能从 rvecs、tvecs 里榨出更多验证信息。一个很实用的进阶操作是:选一张棋盘格图片,用 solvePnP 重新求一次该图的外参,然后调用 cv2.drawFrameAxes 把棋盘格坐标系的三维坐标轴叠加到原图上。如果标定结果正确,坐标轴的 X、Y 轴会精确贴合棋盘格平面,Z 轴垂直于板面指向相机方向。
# 选第 5 张图做外参验证 idx = 5 ret, rvec, tvec = cv2.solvePnP( object_points[idx], image_points[idx], camera_matrix, dist_coeffs ) # 坐标轴长度设为1.5倍格子边长 axis_length = 1.5 * square_size img_axis = cv2.drawFrameAxes( img_orig.copy(), camera_matrix, dist_coeffs, rvec, tvec, axis_length )这张叠加了坐标轴的图有三个用途。第一,判断外参是否存在 180 度翻转:棋盘格坐标系 Z 轴应该指向相机,如果画出来的 Z 轴指向远离相机的方向,说明棋盘格角点顺序在检测时发生了整体旋转,需要重新检查 pattern_size 的传入顺序或者图像输入方向。第二,直观判断棋盘格平面与相机是否对称:坐标轴在画面的位置会随 tvec 变化,如果 tvec 对应的棋盘格位置与实际图像中棋盘格的位置明显错位,说明外参求解存在粗差。第三,这套坐标轴可视化可以直接迁移到后续的 AR 注册或机械臂手眼标定里——用同一个相机模型叠加虚拟物体时,如果虚拟物体不能稳定贴合棋盘格平面,基本可以肯定是内参标定有残留误差。
从那以后,我每次标定完都会强制走一遍“rms 检查 → 逐张误差分析 → 去畸变直线验证 → drawFrameAxes 可视化”这套流程,四步全过才把相机参数写进配置文件。任何一步出问题,我都优先怀疑棋盘格平面度和图片覆盖范围,而不是直接调优化参数。这套流程看着多,实际跑一遍不到五分钟,却能省下后面整周的在三维测量精度上排查的时间。希望帮到你。
本文还有配套的精品资源,点击获取