资讯详情 张正友标定法+OpenCV实战:相机内参标定与畸变校正指南
📅 2026/10/12 0:43:11
简介这份资源是张正友相机标定法的OpenCV完整实现工程面向计算机视觉初学者、图像处理课程学习者以及需要快速搭建标定实验的开发者用于解决相机内参、外参求解与镜头畸变矫正问题。压缩包共93个文件、约13.83MB包含2个cpp源码文件、vcxproj与sln工程文件、可执行程序以及14张不同角度的bmp标定图、14张jpg图像和1张棋盘图另附标定结果与数据txt文本工程结构清晰、注释详尽可一键运行。程序完整覆盖棋盘角点提取、标定板与图像坐标关系建立、相机参数计算、旋转与平移矩阵输出、重投影误差评价及原始棋盘图矫正等环节读者可据此理解标定全流程并直接复用到3D重建、增强现实等场景。目前已有85人学习下载适合作为课程实验或项目起步的参考范例。1. 张正友标定法配 OpenCV从一张棋盘图到可用的相机内参你手里有一台相机拍出来的图总带着说不清的畸变做测量、做三维重建、做视觉定位时误差大得离谱问题往往不在算法而在相机内参压根没标定过。张正友标定法就是解决这件事的经典方案打印一张棋盘图从不同角度拍十几张用 OpenCV 算出相机的内参矩阵和畸变系数。它不需要昂贵的标定板也不需要精密位移台一张 A4 纸加一台普通相机就能跑通。这篇笔记面向想真正把相机标定落地的人——不管你是刚装好 OpenCV 的新手还是被畸变折腾过的老手下面从棋盘图怎么准备、代码怎么写、参数怎么调、坑在哪一步步讲清楚。2. 棋盘图与标定原理为什么一张纸就能算出相机内参2.1 张正友标定法到底在算什么相机成像可以用一个简单的模型描述世界坐标系里的三维点经过相机外参旋转和平移变换到相机坐标系再经过内参矩阵投影到像素平面。内参矩阵长这样K [[fx, 0, cx], [ 0, fy, cy], [ 0, 0, 1]]fx、fy 是焦距在 x、y 方向的像素表示cx、cy 是主点光轴与像平面的交点的像素坐标。畸变系数则描述镜头带来的径向和切向偏移OpenCV 里通常用 5 个参数 k1、k2、p1、p2、k3 表示。张正友标定法的巧妙之处在于棋盘图上的角点在世界坐标系里的位置是已知的假设棋盘在 Z0 平面上每个角点在图像里的像素位置可以检测出来这样就建立了一组 3D-2D 对应关系。单张棋盘图能提供足够的约束求解单应矩阵多张不同姿态的棋盘图联立就能把内参和畸变系数解出来。它不需要知道棋盘在空间中的精确位姿只需要知道棋盘格子的物理尺寸。常见做法是拍 10 到 20 张棋盘覆盖图像的不同区域、不同倾斜角度。张正友原论文里用 5 个以上姿态就能得到稳定解但实际工程里我一般会拍到 15 张以上因为姿态多样性直接决定标定结果的鲁棒性。2.2 棋盘图的制作与拍摄要点棋盘图不是随便找张图打印就行。几个硬性要求第一棋盘格必须是黑白分明、边缘锐利的方格格子数量建议 9x6 或 11x8内角点数量不是格子数。OpenCV 的findChessboardCorners检测的是内角点也就是黑白格交界处的交叉点。如果你打印的是 10x7 的格子内角点就是 9x6。第二打印后要贴在硬质平板上。纸张弯曲会让角点世界坐标的平面假设失效标定出来的畸变系数会偏。我一般用亚克力板或硬纸板用胶棒把打印纸完全贴合边角不能翘。第三拍摄时棋盘要覆盖图像的不同区域。不要只拍中间要拍到四个角和边缘因为畸变在边缘最明显边缘没有角点的话畸变系数约束不足。同时要有不同倾斜角度纯正对相机的棋盘图对解算外参帮助有限。第四光照要均匀避免反光和阴影。棋盘格局部过曝或过暗都会导致角点检测失败或偏移。室内用柔光灯从两侧打光比较稳。第五拍摄距离要覆盖实际使用范围。如果你标定的是近距离工作的相机就不要在很远的地方拍棋盘否则标定出的焦距和实际工作距离不匹配。提示棋盘格尺寸用尺子量准比如每格 25mm这个数值要填进代码里的square_size参数量错了标定结果整体尺度就错了。2.3 用 OpenCV 检测角点的最小代码下面这段代码是标定流程的第一步读入棋盘图检测角点并可视化确认检测是否正确。import cv2 import numpy as np import glob # 棋盘内角点数量行数 x 列数注意是内角点不是格子数 pattern_size (9, 6) # 每格物理尺寸单位 mm根据实际打印尺寸填写 square_size 25.0 # 准备世界坐标系下的角点Z0 平面上的网格 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 obj_points [] # 3D 点 img_points [] # 2D 点 images glob.glob(calib_images/*.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) corners_refined cv2.cornerSubPix(gray, corners, (11, 11), (-1, -1), criteria) obj_points.append(objp) img_points.append(corners_refined) # 可视化按任意键看下一张 cv2.drawChessboardCorners(img, pattern_size, corners_refined, ret) cv2.imshow(corners, img) cv2.waitKey(500) else: print(f角点检测失败: {fname}) cv2.destroyAllWindows()逻辑说明findChessboardCorners返回布尔值和角点像素坐标角点顺序是从左上到右下按行排列。cornerSubPix在初步角点附近做亚像素迭代把精度从整像素提升到亚像素级这一步对最终标定精度影响很大不要省。criteria里 30 是最大迭代次数0.001 是收敛阈值窗口 (11,11) 是搜索区域一般不用改。参数说明pattern_size必须和棋盘实际内角点数一致写错了检测直接失败。square_size只影响世界坐标的尺度不影响内参矩阵的数值但如果你后续要做三维测量这个值必须准确。3. 完整标定程序从角点检测到内参输出3.1 标定主流程与 calibrateCamera 参数角点收集完成后调用cv2.calibrateCamera一次性解算内参、畸变系数和每张图的外参。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 obj_points [] img_points [] h, w 0, 0 images glob.glob(calib_images/*.jpg) for fname in images: img cv2.imread(fname) gray cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) h, w gray.shape[:2] ret, corners cv2.findChessboardCorners(gray, pattern_size, None) if ret: criteria (cv2.TERM_CRITERIA_EPS cv2.TERM_CRITERIA_MAX_ITER, 30, 0.001) corners_refined cv2.cornerSubPix(gray, corners, (11, 11), (-1, -1), criteria) obj_points.append(objp) img_points.append(corners_refined) # 标定返回 RMS 重投影误差、内参矩阵、畸变系数、旋转向量、平移向量 ret, mtx, dist, rvecs, tvecs cv2.calibrateCamera( obj_points, img_points, (w, h), None, None ) print(RMS 重投影误差:, ret) print(内参矩阵:\n, mtx) print(畸变系数:, dist.ravel()) # 保存结果 np.savez(calib_result.npz, mtxmtx, distdist, rvecsrvecs, tvecstvecs)逻辑说明calibrateCamera的输入是所有图像的 3D-2D 对应点、图像尺寸输出内参和畸变。RMS 重投影误差是衡量标定质量的核心指标它表示把 3D 点按标定结果投影回图像后与检测到的 2D 点之间的平均像素距离。这个值越小越好一般小于 0.5 像素算不错小于 0.3 像素算很好。如果超过 1 像素说明有图像角点检测不准或者姿态太单一。参数说明(w, h)是图像尺寸必须和实际图像一致否则主点坐标会错。None, None表示不预设内参初值让 OpenCV 自己算。如果你知道大致焦距可以传入初值加速收敛但一般不需要。3.2 用重投影误差逐张排查坏图整体 RMS 达标不代表每张图都没问题。我习惯逐张算重投影误差把误差大的图挑出来看。import cv2 import numpy as np data np.load(calib_result.npz) mtx, dist data[mtx], data[dist] rvecs, tvecs data[rvecs], data[tvecs] # 重新加载角点数据实际使用时建议把 obj_points/img_points 也保存下来 # 这里假设你已经有了 obj_points 和 img_points for i in range(len(obj_points)): img_points_proj, _ cv2.projectPoints( obj_points[i], rvecs[i], tvecs[i], mtx, dist ) error cv2.norm(img_points[i], img_points_proj, cv2.NORM_L2) / len(img_points_proj) print(f第 {i} 张图重投影误差: {error:.4f} 像素)逻辑说明projectPoints把世界坐标点按标定得到的外参和内参投影到图像平面再和实际检测的角点算 L2 距离。逐张看误差如果某张图误差明显高于其他图通常是那张图角点检测有偏移或者棋盘在拍摄时动了、纸张不平。参数说明cv2.norm的NORM_L2是欧氏距离除以角点数得到平均每点误差。这个值在 0.1 到 0.3 之间比较正常。3.3 畸变校正与效果验证标定完要验证畸变校正效果最直接的方法是用undistort处理一张原图看直线是否变直。import cv2 import numpy as np data np.load(calib_result.npz) mtx, dist data[mtx], data[dist] img cv2.imread(calib_images/test.jpg) h, w img.shape[:2] # 方法一直接 undistort undistorted cv2.undistort(img, mtx, dist, None, mtx) # 方法二先 getOptimalNewCameraMatrix 再 remap可控制保留区域 new_mtx, roi cv2.getOptimalNewCameraMatrix(mtx, dist, (w, h), 1, (w, h)) undistorted2 cv2.undistort(img, mtx, dist, None, new_mtx) cv2.imwrite(undistorted.jpg, undistorted) cv2.imwrite(undistorted_optimal.jpg, undistorted2)逻辑说明undistort内部先算映射表再重映射。getOptimalNewCameraMatrix的 alpha 参数控制保留多少边缘像素alpha0 只保留有效区域会裁掉黑边alpha1 保留所有像素但边缘会有黑边。实际工程里如果后续要做目标检测一般用 alpha0 裁掉黑边避免黑边干扰。参数说明new_mtx是调整后的内参矩阵和原始mtx不同后续如果要用校正后的图像做三维重建必须用new_mtx而不是mtx。4. 避坑与排查标定翻车的五个血泪经验4.1 角点检测失败或检测到错误数量现象findChessboardCorners返回 False或者返回 True 但角点顺序错乱。原因棋盘格对比度不够、光照不均、棋盘倾斜角度过大导致透视变形严重、棋盘格尺寸和pattern_size不匹配。还有一种常见情况是棋盘图边缘被裁掉角点不完整。解决先确认pattern_size填的是内角点数而不是格子数。用cv2.imshow逐张看检测结果失败的图单独拿出来分析。如果是个别图失败直接剔除如果大面积失败重新拍。拍摄时保证棋盘完整出现在画面内四周留白。4.2 RMS 误差很小但实际校正效果很差现象标定报告 RMS 0.2 像素但用undistort校正后直线还是弯的。原因标定用的棋盘图姿态太单一全部正对相机或者全部在图像中央。这种情况下内参和畸变系数之间存在耦合数学上误差小但外推到边缘区域就失效。解决重新拍确保棋盘覆盖图像四个角和边缘倾斜角度多样化。我一般要求至少 5 张图里棋盘出现在不同象限至少 3 张有明显倾斜。4.3 畸变系数符号或量级异常现象标定出的 k1 是正数且很大或者 k1、k2 量级差了好几个数量级。原因棋盘图纸张弯曲、拍摄时相机移动导致运动模糊、角点亚像素精化失败。另外如果棋盘格物理尺寸填错虽然不影响内参但会影响外参尺度间接影响畸变估计。解决检查棋盘是否平整拍摄时用三脚架或稳定支撑。重新跑角点检测确认每张图的角点都精确落在黑白格交叉点上。如果个别图角点偏移剔除后重新标定。4.4 不同次标定结果差异大现象同一台相机两次标定得到的内参矩阵差别超过 5%。原因标定过程本身有随机性尤其是图像数量少、姿态覆盖差的时候。另外如果两次标定用的棋盘图物理尺寸不同或者图像分辨率不同结果也会不同。解决增加图像数量到 20 张以上覆盖更多姿态。标定完成后保存结果后续使用固定内参不要每次用都重新标。如果必须重新标用同一套拍摄规范。4.5 用标定结果做 solvePnP 时位姿解算不稳定现象用solvePnP算相机位姿结果跳动大或者和实际不符。原因solvePnP依赖准确的内参和畸变系数。如果标定结果本身有偏差位姿解算会被放大。另外solvePnP用的 3D 点如果和标定时世界坐标系定义不一致也会出错。解决先验证标定结果用projectPoints检查重投影误差。确认solvePnP输入的 3D 点坐标和标定时objp的坐标系一致。如果用的是校正后的图像solvePnP要传入new_mtx而不是原始mtx并且畸变系数传 0。5. 进阶技巧用标定结果做单目测距与精度验证标定不是终点内参和畸变系数是后续所有视觉任务的基础。这里给一个用标定结果做单目测距的验证方法顺便检验标定精度。假设相机已经标定现在要测一个已知物理尺寸的物体到相机的距离。用相似三角形原理import cv2 import numpy as np data np.load(calib_result.npz) mtx, dist data[mtx], data[dist] # 已知物体实际宽度 W在图像中检测到的像素宽度 P # 距离 D (W * fx) / P fx mtx[0, 0] def estimate_distance(real_width_mm, pixel_width): return (real_width_mm * fx) / pixel_width # 示例实际宽度 100mm图像中占 200 像素 d estimate_distance(100.0, 200.0) print(f估计距离: {d:.2f} mm)逻辑说明这个公式假设物体平面平行于像平面实际使用中如果物体有倾斜需要先做透视校正。fx 从标定结果里取所以标定精度直接决定测距精度。参数说明real_width_mm是物体实际物理宽度pixel_width是在图像里量出来的像素宽度。这个方法的误差来源包括标定误差、像素测量误差、物体倾斜。在 1 米以内标定良好的相机测距误差可以控制在 2% 以内。验证标定精度的另一个方法是拍一张已知尺寸的棋盘图用undistort校正后重新检测角点看角点间距是否和物理尺寸一致。如果偏差超过 1%说明标定还有问题。我自己的习惯是每次标定完一定拿一张没参与标定的棋盘图做验证算重投影误差。如果验证图的误差比标定图的平均误差大很多说明标定过拟合了姿态覆盖不够得重拍。这个习惯帮我省了很多事后返工的时间。希望帮到你。本文还有配套的精品资源点击获取