首页 / 资讯中心 / 文章详情

张正友标定法详解:棋盘格照片如何算出相机内参与畸变

张正友标定法详解:棋盘格照片如何算出相机内参与畸变 ★ FEATURED ARTICLE
简介一套面向计算机视觉入门与进阶学习者的张正友标定法实现资料包。资源围绕单平面棋盘格模板系统呈现摄像机标定的完整技术路线从针孔模型与内参矩阵出发经特征点检测、单应性矩阵求解、内外参数估计再到畸变系数计算与图像校正。内容涵盖摄像机模型、角点检测、参数求解与畸变校正等核心环节配有m脚本、测试数据及说明文档可帮助读者理清从模板标定到实际应用的流程。压缩包共27个文件以10个m脚本为主辅以9个txt数据/说明、7个asv备份及1个caj论文整体仅85KB便于查阅已有501人学习下载适合代码研读与算法复现。除可直接运行的脚本函数外还包含多组测试数据、旋转与单应性计算工具、点归一化函数以及张正友原始文献便于对照理论逐行验证是从理论过渡到动手实践的高性价比参考资料。1. 张正友标定法是什么为什么棋盘格拍几张照片就能算出相机内参做视觉测量、机器人抓取或者三维重建的工程师多半绕不开相机标定。张正友标定法就是目前最主流的“用棋盘格照片求相机内参和畸变系数”的办法OpenCV 里calibrateCamera的默认算法就是它。它的价值在于不需要昂贵的标定设备只需要打印一张棋盘格从不同角度拍 10 到 20 张照片就能得到焦距、主点、畸变系数这些参数。刚接触的人会觉得这是“玄学”——明明每张照片里的角点位置都不一样为什么算出来的内参是固定的答案藏在单应矩阵分解里。这篇笔记适合两类人一是刚入行要自己标定相机的二是标定结果不稳定想排查原因的我会把原理、命令、参数和几个拿时间换来的坑一次讲完。2. 标定前的准备棋盘格规格、拍摄姿态与采集脚本2.1 棋盘格的物理尺寸与角点数量怎么选张正友标定法的输入是棋盘格的“角点”不是棋盘格的“格子”。OpenCV 里的findChessboardCorners找的是黑白格交界的内部角点。比如一张 9×6 的棋盘格指的不是 9 行 6 列格子而是内角点数为 9×6对应格子数量实际上是 10×7。打印的时候要区分PatternSize和SquareSize两个参数PatternSize内角点数例如(9, 6)这是代码里检测的目标数量。SquareSize每个格子的物理边长单位毫米例如30这是后续标定时世界坐标系下角点坐标的间隔。物理尺寸的选择要看你相机的视野。一般原则是在画面里每个格子至少占 15 到 20 个像素否则角点检测不稳定。我用过的组合里500 万像素相机配 30mm 格子的 9×6 棋盘格在距镜头 0.5 到 1 米范围内效果最好。如果棋盘格太小角点会被混叠吃掉如果太大近距离拍摄时边缘格子出画内角点数量不足直接失败。另一个容易被忽略的点是棋盘格必须打印在平面上。普通的 A4 纸会翘边建议贴在硬纸板或者铝塑板上。不要用覆膜反光的材料高光会导致局部过曝角点附近的梯度被抹平检测结果会跳。2.2 拍摄时覆盖视场与姿态变化的实用规则采集照片是整个标定里最影响成败的一步。经验法则是拍 15 到 20 张不要少于 10 张。棋盘格要出现在画面的不同位置左上、右上、中心、下边缘都要覆盖。姿态要有变化倾斜、俯仰、旋转都要有不能全是正对相机。保证棋盘格至少占画面 1/3 以上最好 1/2。对焦要清晰特别是边角处的格子不能糊。为什么姿态变化这么重要因为张正友标定法是从每张图的单应矩阵里解出内参的如果所有棋盘格都正对相机单应矩阵退化内参解不出来。我见过不少工程师拍了一堆“很完美”的正对照片标定结果焦距乱跳就是这个原因。采集时还要注意不要移动相机。标定求的是相机内参不是位姿相机应该保持固定只移动棋盘格。如果你用手持相机对着固定棋盘格变换角度拍出来的图也能标定但结果一致性会比“相机固定、棋盘格动”要差一些因为手持抖动引入了额外的运动模糊。2.3 用 OpenCV 的 findChessboardCorners 做初筛一段可复用的采集检查脚本拍完照片不要直接丢给标定函数先跑一遍角点检测把不合格的照片剔除。这个初筛能省掉后面调试的很多时间。我一般会写一个检查脚本批量读图、检测、绘制并保存结果import cv2 import glob pattern_size (9, 6) # 内角点数 (宽, 高) image_paths sorted(glob.glob(calib_images/*.jpg)) valid_images [] for path in image_paths: img cv2.imread(path) gray cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) found, corners cv2.findChessboardCorners( gray, pattern_size, flagscv2.CALIB_CB_ADAPTIVE_THRESH cv2.CALIB_CB_NORMALIZE_IMAGE ) if not found: print(f跳过: {path} 未找到完整棋盘) continue # 亚像素细化提升角点位置精度 corners_refined cv2.cornerSubPix( gray, corners, (11, 11), (-1, -1), (cv2.TERM_CRITERIA_EPS cv2.TERM_CRITERIA_MAX_ITER, 30, 0.001) ) valid_images.append((path, corners_refined)) cv2.drawChessboardCorners(img, pattern_size, corners_refined, found) out_path check/ path.split(/)[-1] cv2.imwrite(out_path, img) print(f合格图片数: {len(valid_images)} / {len(image_paths)})这段脚本的逻辑很简单先做粗检测再用cornerSubPix做亚像素细化。注意findChessboardCorners的flags里我加了ADAPTIVE_THRESH和NORMALIZE_IMAGE这两个 flag 能处理光照不均和过曝场景代价是速度稍慢但标定场景不在乎这点时间。(11, 11)是亚像素搜索窗口的一半尺寸如果棋盘格在画面里很小可以改成(5, 5)如果很大(21, 21)会慢但更稳。TERM_CRITERIA表示迭代 30 次或精度到 0.001 像素就停。跑完这个脚本如果合格图片数少于 10 张别急着标定回到拍摄阶段补拍。检查绘制图片时重点看角点是否贴在内角中心如果有一排角点偏移明显说明图像模糊或者畸变太重这张图建议丢掉。3. 从单应矩阵到内参张正友标定法的核心原理与数学脉络3.1 为什么把世界坐标系放在棋盘格平面上张正友标定法的一大聪明之处是它不需要你知道棋盘格在空间里的具体位姿只需要假设棋盘格平面是世界坐标系的 Z0 平面。这样一来棋盘格上每个角点的世界坐标(X, Y, 0)可以直接用SquareSize乘以索引得到比如第(i, j)个角点就是(i*30, j*30, 0)。相机成像模型把三维点投影到像素坐标公式是s * [u, v, 1]^T A * [R|t] * [X, Y, 0, 1]^T因为 Z0旋转矩阵的第三列被消掉原来的 3×4 外参矩阵变成了 3×3 的单应矩阵 H。每张棋盘格照片能估计出一个 H而 H 同时包含内参 A 和外参旋转和平移。这里的关键是不同照片的外参不同但内参 A 是同一个。用多张照片的 H 联立解方程就能把 A 分离出来。这是整个方法的核心思想用平面标定物把非线性的相机标定问题转化成一组线性方程先求闭式解再用最大似然优化精化。这也是为什么它比传统 DLT 方法更稳定因为平面棋盘格的角点提取容易做到亚像素精度。3.2 单应矩阵 H 如何分解出内参矩阵 A单应矩阵 H 是 3×3 的它把棋盘格平面上的点映射到图像平面。设 H 的三列是[h1, h2, h3]内参矩阵 A 是上三角矩阵包含焦距 fx、fy 和主点 cx、cy。根据旋转向量的正交性和模长相等约束可以得到两个关于 A 的基本方程h1^T * (A^{-T} * A^{-1}) * h2 0 h1^T * (A^{-T} * A^{-1}) * h1 h2^T * (A^{-T} * A^{-1}) * h2这里的A^{-T} * A^{-1}是一个对称矩阵张正友用B来表示它。因为 B 是对称的只有 6 个未知数所以一张照片能贡献两个方程。理论上 3 张照片就能解出 6 个未知数实际通常拍 10 张以上是为了抵抗噪声和数值误差。解出 B 之后再对 B 做 Cholesky 分解或者直接按元素反解就能得到内参矩阵 A。这一步在 OpenCV 里由cv2.calibrateCamera内部完成不需要你手写。但理解这个脉络能帮你排查问题如果照片数量少或者姿态单一B 的方程是欠约束的求出来的焦距自然不可信。3.3 畸变系数的引入与非线性优化让重投影误差说话线性求解出来的内参只是初始值因为没有考虑镜头畸变。张正友标定法使用了一个常用的畸变模型包含径向畸变 k1、k2以及可选 k3和切向畸变 p1、p2x_distorted x * (1 k1*r^2 k2*r^4 k3*r^6) 2*p1*x*y p2*(r^2 2*x^2) y_distorted y * (1 k1*r^2 k2*r^4 k3*r^6) p1*(r^2 2*y^2) 2*p2*x*y其中(x, y)是归一化相机坐标系下的坐标r^2 x^2 y^2。畸变系数先用最小二乘估计初始值然后把内参、外参、畸变系数一起扔进非线性优化器优化的目标是最小化所有角点的重投影误差sum( || m_ij - project(A, k, R_i, t_i, M_j) ||^2 )OpenCV 默认用 Levenberg-Marquardt 算法做这个优化。最终输出的retval就是每个角点的平均重投影误差单位是像素。这个值通常应该小于 0.5如果超过 1说明标定输入有问题。这一节想强调的是重投影误差是衡量标定质量的唯一直接指标但它不是万能的。误差小只代表“这些照片的内部一致性”好不代表真实世界精度高。后面避坑章节里我会讲一个误差很小但实际效果翻车的案例。4. 用 OpenCV 跑通最小标定流程从图片目录到相机参数文件4.1 calibrateCamera 的完整调用与参数含义有了合格角点标定主流程就很简单。下面是完整代码它读取上一步保存的角点数据计算世界坐标调用标定函数并打印结果import numpy as np import cv2 import json # 上面第 2 节筛选出来的角点列表每个元素是 (image_path, corners_refined) # 这里直接构造标准输入格式 square_size 30.0 # 棋盘格边长单位 mm pattern_size (9, 6) # 与角点检测时一致 obj_points [] # 世界坐标点集 img_points [] # 像素坐标点集 for img_path, corners in valid_images: # 生成棋盘格世界坐标z0 平面单位 mm 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) * square_size obj_points.append(objp) img_points.append(corners.reshape(-1, 1, 2)) # 相机分辨率可以从图片读取 img_sample cv2.imread(valid_images[0][0]) h, w img_sample.shape[:2] # 标定 ret, mtx, dist, rvecs, tvecs cv2.calibrateCamera( obj_points, img_points, (w, h), cameraMatrixNone, distCoeffsNone, flagscv2.CALIB_RATIONAL_MODEL # 启用 k1-k3, p1-p2 共 5 个畸变系数 ) print(f重投影误差: {ret:.4f} 像素) print(内参矩阵:\n, mtx) print(畸变系数:\n, dist.ravel()) # 保存为 JSON方便后续程序读取 calib_data { image_width: w, image_height: h, camera_matrix: mtx.tolist(), dist_coeffs: dist.ravel().tolist(), rms_error: ret } with open(camera_params.json, w) as f: json.dump(calib_data, f, indent2)参数说明calibrateCamera的第一个参数是每张图的世界坐标点列表第二个是像素坐标点列表这两个列表长度必须一致。第三个是图像尺寸(w, h)注意 OpenCV 里宽在前。cameraMatrixNone表示让算法自动估计内参如果你想固定主点位置可以传入一个初始矩阵并配合CALIB_FIX_PRINCIPAL_POINT使用但新手不建议这么做。CALIB_RATIONAL_MODEL会估计 5 个畸变系数普通镜头用None或者CALIB_RATIONAL_MODEL都行但如果镜头畸变很轻用 5 参数模型可能过拟合这时建议用默认 4 参数模型即去掉这个 flag。4.2 保存标定结果为 YAML/JSON内参矩阵与畸变系数怎么用保存成 JSON 是为了让其他程序方便加载。内参矩阵mtx的排布是[fx, 0, cx] [ 0, fy, cy] [ 0, 0, 1]其中fx f / dxfy f / dydx、dy是像素尺寸。如果你的像素是正方形fx和fy应该非常接近。如果两者相差超过 5%第一反应不是镜头问题而是标定板姿态覆盖不够或者照片里有严重的透视变形没被模型解释掉。畸变系数dist是个 1×5 的矩阵顺序是[k1, k2, p1, p2, k3]。在用 OpenCV 去畸变时直接传给cv2.undistort即可。注意不要弄混顺序我曾经把[k1, k2, k3, p1, p2]当成 OpenCV 的顺序传进去结果图像边缘扭曲换了 5 分钟才意识到。4.3 角点检测不完整时的处理亚像素参数与CALIB_CB_EXHAUSTIVE实战里经常遇到findChessboardCorners检测不到完整棋盘的情况。除了重拍还有两个可调的偏方。第一个是降低pattern_size的要求比如检测 8×5 的内角点如果完整 9×6 检测失败。这个做法能用但代价是标定板外圈的信息被丢弃导致视场边缘的畸变估计不准。适合应急不适合追求精度的场景。第二个是启用CALIB_CB_EXHAUSTIVE参数。OpenCV 4.x 里这个 flag 会做更密集的角点搜索速度慢但能提高检出率。代价是偶尔会误检跑完初筛脚本后一定要人眼检查绘制图。亚像素细化的窗口大小也要根据分辨率调整。500 万像素图里棋盘格单格约 30 像素窗口(11, 11)够用如果是 1200 万像素建议(21, 21)。窗口太小角点位置会偏窗口太大边缘处的非角点容易被拉进来。这个参数没有绝对最优我通常会在保存结果前用cornerSubPix反复细化两次把maxIter从 30 提到 50能换回零点几个像素的精度。5. 张正友标定法常见问题排查5 个让标定翻车的真实案例5.1 现象重投影误差小于 0.3但去畸变后图像边缘扭曲严重这是最隐蔽的坑。误差小通常意味着标定模型拟合得很好但边缘扭曲说明畸变模型没有覆盖真实镜头的变形或者边缘的角点参与太少了。原因是畸变系数主要由画面边缘的棋盘格决定如果你拍的图里棋盘格只在画面中心区域移动边缘畸变没有被充分激励算法就会低估畸变系数。解决方法是回到采集阶段画面边缘四角都要让棋盘格出现即使只有一半棋盘在画框内也可以。如果某些姿态下无法检测完整角点可以用 7×5 或者更小的内角点数量重新标但需要注意不同图像使用不同 pattern_size 可能需要分开处理。5.2 现象不同图片组合标出的焦距波动超过 5%同一台相机用 10 张图标定焦距是 1200换另外 10 张变成 1280这种波动说明标定输入不稳定。常见原因有两个一是照片中有运动模糊角点亚像素位置偏移二是姿态覆盖不够特别是绕光轴的旋转角度变化太少。处理办法是做一个滑窗测试从照片集里随机抽 2/3 的子集各标定 5 次看焦距的标准差。标准差如果大于焦距值的 1%就回到采集步骤补拍。这个测试没必要写复杂脚本手动多跑几次calibrateCamera就行。5.3 现象findChessboardCorners 明明能检测却报“Chessboard detection failed”这类错误通常发生在循环批量处理图片时。原因多半是图片列表里有非标定板照片、灰度图通道数不对、或者pattern_size和实际棋盘不匹配。我犯过的错是拍摄时把棋盘格照片里的某一张换成了不同格数的棋盘检测脚本的pattern_size没变结果有的图能检测、有的不能。解决方法是每张图检测前打印image.shape和检测状态发现异常先看一眼图片内容。另外要确保所有图片都是从同一块棋盘拍摄的不要混用不同规格的标定板。5.4 现象棋盘格只出现在画面中心时结果不稳定中心区域是畸变影响最小的区域也是单应矩阵条件数最差的区域。因为当棋盘格很小时单应矩阵对姿态角的变化不敏感内参解算的数值稳定性变差。从数学上看单应矩阵 H 的估计误差会被放大到内参上。解决办法是老规矩让棋盘格在画面不同位置倾斜尤其是画面四角。还有一个技巧拍摄时让棋盘格平面与相机光轴成 45 度左右的夹角这样角点分布能撑满更宽的视场范围观察到的畸变分量更丰富。5.5 现象鱼眼镜头直接套用 calibrateCamera 失败或误差巨大普通针孔模型加 5 参数畸变不能描述鱼眼镜头的强畸变。张正友标定法本身适用于普通镜头和轻微畸变的广角镜头鱼眼要用cv2.fisheye.calibrate它使用等距投影模型。如果你手上只有普通标定函数可以考虑缩小棋盘格在画面中的占比只标定中心区域但这样边缘畸变就不要想了。我遇到过一个更平常的问题手机前置摄像头外参矩阵在标定时出现负的 tz代表标定板在相机后面的伪解。这个需要检查图片是否有镜像翻转。OpenCV 的cv2.imread读出来是正常的但如果你的图集被处理过做了水平翻转角点坐标就和世界坐标对不上结果就翻车。检查方法是看第一张图的角点顺序是不是从左到右、从上到下排列。6. 让标定结果更可靠的一个技巧用重投影误差做质量门禁与自动化筛选最后分享一个我每次标定都会做的技巧不只输出一个总误差而是输出每一张图片的单独重投影误差用这个误差做质量门禁自动剔除贡献大的“坏图”。很多人只盯着calibrateCamera返回的总ret但总误差是平均过的一张坏图可能被其他好图平均掉。做法是在标定后对每个角点重新投影计算每张图的均方根误差def per_image_error(obj_points, img_points, rvecs, tvecs, mtx, dist): errors [] for i in range(len(obj_points)): proj, _ cv2.projectPoints(obj_points[i], rvecs[i], tvecs[i], mtx, dist) err np.mean(np.linalg.norm(proj[:, 0, :] - img_points[i][:, 0, :], axis1)) errors.append(err) return errors errs per_image_error(obj_points, img_points, rvecs, tvecs, mtx, dist) for idx, e in enumerate(errs): print(f图像 {idx}: 误差 {e:.3f} 像素)逻辑说明projectPoints用当前标定结果的内参、外参把世界坐标重新投影到图像再和检测到的像素坐标做差。误差大的图片说明要么角点提取有偏差要么该视角下模型拟合不好。一般以 0.8 像素作为门禁超过就剔除然后重新标定。这个技巧的另一个用法是评估新增照片的价值。标定完成后如果你还想补拍照片提高精度先拿新增照片算一下误差如果误差和当前平均水平差不多就加进去如果特别大说明姿态太极端或者有遮挡补了反而拉低整体精度。这比盲目加照片高效得多。另外验证标定质量不能只看重投影误差。一个更贴近实际需求的验证是在真实场景里测量用标定结果去畸变一张包含已知长度物体的图像用像素距离乘以比例算出物理长度和真实长度对比。这个做法的好处是能暴露“摄影测量意义上的精度”问题而重投影误差只能说明对棋盘格自身的拟合程度。我吃过亏的情况是重投影误差 0.2 像素但用去畸变图像做 0.5 米距离的尺寸测量误差到了 3 毫米最后查下来是棋盘格贴纸本身不平整角点世界坐标引入了几毫米偏差。所以标定板平面的平整度永远是最值得投资的硬件前提。我的习惯是每一批标定都保留原始图片、筛选后的角点、以及最终参数三个文件这样后续发现问题还能回头排查。这套流程从采集到出参数熟练后十分钟内能跑完但中间沉淀下来的这些边界条件和参数取舍才是真正决定标定质量的东西。希望这篇笔记能帮你少走几步弯路。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?
咨询建站