简介基于张正友棋盘格标定法的相机内参求解Python程序专为需要快速完成相机校准的开发者与视觉方向学习者准备。资源包共14个文件压缩包仅2.19MB包含10张棋盘格拍摄样例jpg、2个Python源码、1张png棋盘格图片及1份txt说明文档。其中主程序负责读取图片并计算内参辅助脚本可用来显示棋盘格配合手机或显示器即可采集标定图片使用门槛较低。内置11x10棋盘格图片可打印或投屏拍摄实际使用时只需在代码中修改图片文件夹路径和单个格子边长即可得到相机内参结果适合课程实验、机器人视觉及AR项目前期标定。已有907人学习下载资料体积小、结构清晰是一份能直接上手的相机内参标定工具包。1. Camera calibration 相机内参求取棋盘格 Python 程序直接可用视觉项目做到第一个坎往往不是模型跑不动而是相机拍出来的图没法映射到物理坐标。Camera calibration相机校准解决的就是这件事用棋盘格标定板通过一组已知几何关系的二维图像反推出相机光学中心的焦距、主点和镜头畸变系数也就是内参矩阵。这套内参后续无论是做物体测距、机械臂抓取还是把像素坐标转成相机坐标系都是绕不开的基础。用 Python 调 OpenCV 实现这套流程成本低、代码量小一块打印的棋盘格加上十到二十张照片就能完成标定。这篇内容面向的是一线脚本工程师和刚接触视觉测量的开发者按步骤跑完就能拿到可直接写进工程里的内参文件和重投影误差指标。2. 相机内参的几何意义为什么张正友法默认选用棋盘格作为靶标2.1 针孔模型下的内参矩阵包含哪几个需要求的参数相机标定理论上有非常多流派经典且常用的参考是张正友在 2000 年提出的平面标定法。这套方法的出发点是把三维世界坐标先变换到相机坐标系再投影到图像平面。整个变换融合了外参旋转矩阵 R、平移向量 t和内参焦距、主点、畸变。我们需要的“内参”指的是相机本身的固有属性通常用一个 3x3 矩阵表示K [[fx, 0, cx], [0, fy, cy], [0, 0, 1]]fx、fy 分别是 x、y 方向以像素为单位的焦距cx、cy 是主点坐标。大部分工业相机标定得到的内参矩阵里面fx 和 fy 会很接近但不会完全相等。实际做远心镜头或线扫相机时甚至会出现差异较大的情况所以不能只求一个焦距就完事。除内参矩阵外OpenCV 还返回畸变系数常见的是 5 参数模型k1、k2径向畸变、p1、p2切向畸变、k3高次径向畸变通常用于鱼眼或大畸变镜头。标定原理上棋盘格平面被看作 Z0 的特殊平面。假设世界坐标系中有一个角点的坐标为 (X, Y, 0)它成像到像素坐标 (u, v)。这个过程可以写作s * [u, v, 1]^T K * [r1, r2, t] * [X, Y, 1]^T这里 r1、r2 是旋转矩阵的前两列t 是平移向量s 是尺度因子。这个式子说明棋盘格平面到图像平面之间存在一个单应矩阵 H每拍一张不同角度的棋盘格就能估算出一个 H。多张图像的多组 H 联合约束即可求解出内参矩阵的唯一解。这也是为什么一张棋盘格照片没法完成标定的根本原因——它在数学上欠约束。2.2 棋盘格相比圆点标定板的优势以及角点提取的亚像素基础视觉标定的靶标除了棋盘格还有圆形阵列板、ArUco 板、ChArUco 板等。但在普通工程场景中棋盘格是最不容易出错的选择。原因是它的角点特征非常明确黑白方块相交形成的角点在图像中表现为梯度方向的突变。OpenCV 的findChessboardCorners算法能够直接定位到这些内角点配合cornerSubPix还能进入亚像素级精度。相比之下圆点板容易受透视变形影响椭圆中心的拟合误差在倾斜角度较大时会更明显。棋盘格在数学上还有一个优势就是角点的对应关系天然固定。一张棋盘格有确定的 rows 和 cols标定程序能够按顺序自动匹配每个角点的世界坐标Z0 平面上以毫米为单位等距排列不需要人工点选或编码识别。对于批量标定来说自动化程度很关键这也是开头说“可直接使用”的一个前提。棋盘格选择上有一个容易被新手忽略的点findChessboardCorners的 patternSize 参数指的是“内角点数量”而不是棋盘格块数。比如一张棋盘格有 10x7 个格子那么内角点数量是 (9, 6)。打印时可以数格子代码里传入的是格子数减一。这个细节我在 3.1 节会直接体现在代码参数里。3. Python 标定程序的最小可运行版本内参和畸变系数一次求出3.1 图像准备和角点提取核心代码逐行说明先用一个完整可跑的 Python 脚本作为骨架。假设你已经把棋盘格照片存放在./chess_images目录下照片格式为 jpg。棋盘格的内角点数量我们假定为 (9, 6)方形边长 25mm这两个参数需要根据自己打印的标定板改。import cv2 import numpy as np import glob # 棋盘格内角点数列 x 行 pattern_size (9, 6) # 每个方格的实际边长单位 mm 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 obj_points [] # 世界坐标系下的角点 img_points [] # 图像坐标系下的角点 images glob.glob(./chess_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) # 可视化角点确认检测无误 vis cv2.drawChessboardCorners(img, pattern_size, corners_refined, ret) cv2.imshow(corners, vis) cv2.waitKey(100) else: print(f[skip] {fname} 未检测到完整棋盘格) cv2.destroyAllWindows() print(f有效标定图像数量: {len(obj_points)})这段代码中有几个参数值得展开说。pattern_size是角点数量如果填成格子数量程序会直接找不到棋盘。square_size决定世界坐标尺度它的单位同时决定了内参焦距的单位工程上建议用毫米。(11, 11)是亚像素搜索窗口大小窗口太小在低分辨率图像上精度不足窗口太大计算变慢且容易跳到邻近角点。cv2.TERM_CRITERIA_EPS cv2.TERM_CRITERIA_MAX_ITER表示迭代终止条件同时依赖精度和次数30 次迭代和 0.001 像素精度是经验值低纹理图像可适当放松。3.2 calibrateCamera 一次调用拿到内参矩阵和畸变向量角点提取完成后进入核心求解阶段。OpenCV 的cv2.calibrateCamera内部集成了完整的张正友标定求解和极大似然优化单目相机场景下不需要自己推导矩阵分解。# 相机标定主函数 ret, mtx, dist, rvecs, tvecs cv2.calibrateCamera( obj_points, img_points, gray.shape[::-1], None, None ) print(内参矩阵 mtx:) print(mtx) print(畸变系数 dist (k1, k2, p1, p2, k3):) print(dist.ravel()) print(重投影总误差:, ret) # 保存结果直接存成 npy 或 yaml 都行 np.savez(camera_params.npz, mtxmtx, distdist, retret)gray.shape[::-1]表示图像尺寸注意是 (width, height) 的形式直接传(gray.shape[1], gray.shape[0])也一样。rvecs和tvecs是每张图对应的外参对纯内参标定来说它们属于中间产物但如果后面要做手眼标定或者位姿估计这两个值也需要保留。返回值ret是重投影均方根误差公式上是这样计算的error mean( sqrt( (u_proj - u_detect)^2 (v_proj - v_detect)^2 ) )经验判断标准是误差小于 0.1 像素说明标定质量极好小于 0.3 像素可接受大于 0.5 像素建议重新采集照片。这个指标直接反映内参求得好不好后面第 5 章会围绕它做验证。np.savez保存的是未压缩的二进制格式。工程上更通用的做法是把标定结果保存成 YAML 配置文件方便 C 程序或者其他语言读取。下面这段代码用同样数据输出一份 YAMLimport yaml data { camera_matrix: mtx.tolist(), dist_coeffs: dist.tolist(), reprojection_error: float(ret), image_size: [int(gray.shape[1]), int(gray.shape[0])], square_size_mm: square_size, } with open(camera_params.yaml, w) as f: yaml.dump(data, f, allow_unicodeTrue)这里把 image_size 也写进配置文件是很有必要的因为后续如果换了一台分辨率不同的相机内参必须重新标定不能直接复用。YAML 文件配合 OpenCV 的 FileStorage 格式也能互转cv2.FileStorage读写时注意矩阵要 reshape 成 3x3 或 1x5。4. 标定采集规范与 OpenCV 参数调试失败场景怎么排查4.1 采集照片的数量、角度和光照要求直接影响内参精度标定程序的代码只有几十行真正决定内参质量的是采集照片这一环节。最少需要 3 张不同姿态的棋盘格照片才能求出矩阵实际工程中建议采集 15~20 张。我这里给出了采集阶段的经验表格按这个标准执行重投影误差通常会比较稳定。采集要求推荐值原因图像数量15~20 张覆盖更多姿态组合减少单张异常权重棋盘格倾斜角与相机光轴呈 15°~45°太正对则无法解出焦距信息棋盘格占画面比例1/3 到 1/2保证角点像素足够亚像素提取才有精度焦距 / 光圈固定不变变焦或调光圈会改变内参光照均匀无高光反射高光导致黑白格对比度下降角点检测失败拍摄时的核心原则是“动静结合”棋盘格要占满画面的不同位置包括四个角落和中心。如果棋盘格永远只在画面中央内参矩阵的畸变项会解不准。同时记得把棋盘格贴在不反光的硬质板材上不要用手拿着边缘弯曲会让角点坐标产生系统性偏差。4.2 findChessboardCorners 检测失败的常见原因检测失败在标定流程中偶尔发生。一个典型的报错是findChessboardCorners返回 False。先从四方面检查第一棋盘格打印比例是否被缩放工具改动过打印后实际测量方格边长是否与 square_size 一致第二图片分辨率是否太低明显低于 640x480 时角点边缘模糊亚像素精化效果有限第三棋盘格在照片中是否被遮挡遮住边缘一行格子直接导致检测不完整第四图像有没有被手机或相机的美颜、HDR 功能处理过这类算法会让黑白交界处产生锯齿。OpenCV 提供两种优化手段。第一种是调整棋盘格检测预处理把 BGR 图转灰度后做一次自适应直方图均衡化# CLAHE 增强对比度 clahe cv2.createCLAHE(clipLimit2.0, tileGridSize(8, 8)) gray_eq clahe.apply(gray) ret, corners cv2.findChessboardCorners(gray_eq, pattern_size, None)第二种是传入CALIB_CB_ADAPTIVE_THRESH标志让 OpenCV 内部对图像做自适应二值化。如果极端情况下仍然检测不到可以考虑生成更高分辨率的棋盘格图片并换用抗反光的哑光纸打印。现场打出来的标签纸在灯光照射下容易反光角点处形成白色光斑会直接干扰检测。4.3 calibrateCamera 求解失败或结果异常的处理思路标定偶尔会遇到重投影误差很大、或者内参矩阵明显不合理的结果。比如 fx、fy 偏离标称值超过 20%或者 cx、cy 距离图像中心非常远这类结果在现场偶尔能看到。多数情况下问题出在照片姿态不够多样化。所有棋盘格都是近似正面朝向相机时外参之间的约束趋近于线性相关内参矩阵的解会落到局部极小值。另一个容易出问题的地方是相机分辨率与标定板大小比例不匹配。假设一张 1920x1080 的图里棋盘格只占了一小角那么角点之间的像素距离很短亚像素误差对结果的干扰会被放大。反过来如果棋盘格铺满全屏导致边缘溢出也会出现检测不全。比较合理的做法是棋盘格在画面中的宽度占到图像宽度的三分之一到二分之一这样既能保证角点数量也能让畸变区域充分参与计算。OpenCV 还有一组可选的 flags 参数用于控制优化过程这里放一个实际可用的组合calib_flags ( cv2.CALIB_FIX_K3 # 不优化 k3 cv2.CALIB_FIX_PRINCIPAL_POINT # 把主点固定在图像中心 ) ret, mtx, dist, rvecs, tvecs cv2.calibrateCamera( obj_points, img_points, gray.shape[::-1], None, None, flagscalib_flags )CALIB_FIX_K3适合畸变较小的普通镜头固定 k3 为 0 可以避免高阶项与 k1、k2 抢优化空间。CALIB_FIX_PRINCIPAL_POINT适合传感器中心与光轴基本对齐的相机降低参数自由度换来数值稳定性。这几项属于局部调优不是必须的。遇到结果明显异常时优先怀疑输入数据而不是优化标志。5. 标定结果的去畸变验证和内参落地的进阶用法5.1 用 undistort 检查标定效果直线是否变直内参是否准确有个非常直观的验证方法对一张有明显直线边缘的标定图像做去畸变观察图像边缘的直线是否恢复平直。实现上就一行核心调用h, w gray.shape newcameramtx, roi cv2.getOptimalNewCameraMatrix(mtx, dist, (w, h), 1, (w, h)) undistorted cv2.undistort(img, mtx, dist, None, newcameramtx) cv2.imwrite(undistorted.jpg, undistorted)getOptimalNewCameraMatrix的 alpha 参数决定输出图像保留多少边缘像素。alpha1 表示完全保留所有原始像素alpha0 表示裁掉畸变矫正后出现的黑色区域。标定验证场景建议用 alpha0 观察中心区域的直线恢复效果。如果直线边缘仍有明显弯曲说明畸变系数没有收敛可以考虑加入更多边缘位置的棋盘格照片重新标定。5.2 标定结果在视觉测量和 IMU 内参标定关联中的应用标定后的内参矩阵是后续视觉测量的基准。像素坐标转相机坐标时需要利用内参做逆变换x_cam (u - cx) / fx y_cam (v - cy) / fy这一步只是把像素转到归一化平面。如果要做三维重建或测距还必须有外参或深度信息。这里很容易出现理解偏差因为内参并不能直接把像素还原成毫米坐标它负责的是消除镜头自身带来的成像误差而不是解决图像到物理世界的尺度问题。热词里提到的“imu内参标定”和视觉标定在流程上有一定关联。视觉标定是求相机内参矩阵IMU 标定是求加速度计和陀螺仪的零偏、尺度因子和安装误差两者在很多融合定位项目里前后脚出现。常见做法是先用棋盘格标定相机内参再进行相机与 IMU 的外参标定。如果已有标定好的相机内参这一步可以直接复用不需要对同一相机重复标定。5.3 固定焦距场景下的简化重标定很多项目里相机装到设备上后就不再动它但温度变化或镜头松动可能导致内参轻微漂移。此时可以只采集 5~8 张棋盘格照片并且固定主点不参与优化只重新估计焦距和畸变。OpenCV 的 flag 组合可以这样写fast_flags ( cv2.CALIB_FIX_PRINCIPAL_POINT | cv2.CALIB_FIX_ASPECT_RATIO ) ret, mtx, dist, rvecs, tvecs cv2.calibrateCamera( obj_points, img_points, gray.shape[::-1], None, None, flagsfast_flags )这种做法牺牲了一部分标定完整度但能在现场快速复核内参是否仍然可靠。若重投影误差明显变大说明镜头物理状态已经发生改变这时候任何快速标定都不如把镜头重新固定好再做完整标定。标定只是一个工具最终目标始终是让视觉系统输出的坐标稳定可复现。本文还有配套的精品资源点击获取
