2026/10/25 12:40:56

张正友标定法详解:从棋盘格照片到相机内参的完整流程

张正友标定法详解:从棋盘格照片到相机内参的完整流程 简介一份围绕张正友标定法的学习与实现资料包面向计算机视觉初学者和正在使用MATLAB标定工具箱的开发者。该方法由清华大学张正友教授提出是计算机视觉中应用最广泛的摄像机标定技术之一。资料涵盖摄像机模型、棋盘格角点检测、内外参求解与畸变校正等核心内容适合在三维重建、机器人导航、增强现实等场景中快速上手。压缩包共二十七份文件以.m源码、.txt数据与说明、.asv备份脚本为主另含一篇.caj格式的原始文献可配合代码理解理论推导。资源整体仅约八十五KB轻量易用已有五百零一人学习。包内附有张正友原始论文及多组棋盘格测试数据并提供了Zhang.m、testhomo.m等可跑通的标定示例脚本便于复现从特征检测到参数优化的完整流程是一份不可多得的入门实操参考。1. 用棋盘格照片恢复相机几何张正友标定法解决什么做视觉测量、机器人抓取或者双目立体匹配的人早晚会遇到同一个问题相机拿到手的图像是二维的但你要从里面算出三维坐标。这一步绕不开相机内参和畸变系数而这些参数不是说明书里给的是标定出来的。张正友标定法就是目前工业界最常用的相机标定方案它只需要一张打印的棋盘格从不同角度拍十几张照片就能算出焦距、主点、畸变系数和外参精度足够应付绝大多数机器视觉项目。它的价值在于把标定从玄学变成了可重复的流程不需要昂贵的靶标设备一个普通打印机加一个稳定的摄像头就能开始干活。这个方法适合谁做视觉定位、尺寸测量、AR 姿态估计、机器人手眼标定的工程师以及被畸变搞得头疼的初学者。它不需要你懂很深的射影几何但需要你理解几个关键约束否则拿到 OpenCV 跑完代码输出一堆数字也不知道对不对。下面我从数学前提讲到可复现的 Python 流程最后把我在实际项目中踩过的坑一并交代。2. 标定前先搞清楚张氏法的数学前提与三个关键约束2.1 单应性矩阵为什么用平面棋盘格就能标定张正友标定法的核心巧妙之处在于它不需要三维标定块只需要一个平面棋盘格。这背后的数学支撑是单应性矩阵。空间中一个平面上的点经过相机成像映射到图像平面上这个过程可以用一个 3×3 的单应矩阵 H 来整体描述。因为棋盘格是平面的所有角点的 Z 坐标都可以定义为 0这样世界坐标到像素坐标的变换矩阵就从 3×4 降成了 3×3方程组少了一列未知量求解难度大幅下降。这个降维处理是整个方法的基石。普通三维标定需要精确的三维靶标坐标精度要求极高而张氏法用一个平面把世界坐标系的 Z 轴直接按在棋盘格平面上。每个角点的世界坐标只需要知道它在第几行第几列配合你设定的格子物理边长就能计算出来。这样你不需要用高精度机床去加工标定件打印一张棋盘格贴在平板上就够了成本从几万元降到了几十元。在实际操作中单应性矩阵并不需要我们手动去解。你检测到图像中的角点像素坐标又知道了这些角点的世界坐标OpenCV 内部的findHomography会自动帮你计算 H。我们真正要做的是理解 H 里面携带着内参和外参的信息而且不同姿态的棋盘格照片H 是不同的但内参是固定的。张氏法的第二步就是利用多张照片的 H 联立方程把内参单独解出来这就是为什么需要至少三张不同角度的照片。2.2 内参外参与畸变一张照片能解出什么解不出什么相机的成像模型可以被拆成两部分线性部分和非线性部分。线性部分是针孔模型内参包括 fx、fy用像素表示的焦距、cx、cy主点即光轴在图像上的投影点外参则是相机相对世界坐标系的旋转 R 和平移 t。非线性部分是畸变最常见的是径向畸变 k1、k2、k3 和切向畸变 p1、p2它们修正的是镜头制造和装配带来的像点偏移。这里有一个容易搞混的点一张棋盘格照片能不能同时解出内参和外参答案是可以但解出来的内参是不准的。因为单应性矩阵本身有 8 个自由度它包含了内参和外参的组合信息单张照片无法把内参和外参分离开。当你用多张不同角度的照片做联合优化时内参不变外参每张不同方程的约束变多了内参才能被稳定地解出来。这就是为什么「拍一张照片就想标定完」是不可能的。畸变系数是另一个容易翻车的点。k1 描述的是镜头最明显的枕形或桶形畸变k2 是更高阶的修正k3 通常只在畸变严重的广角镜头上才需要。不要把 k3 当成默认项对普通工业镜头k3 不仅不会提升精度还会让方程变得不稳定因为高阶项会和你采集的照片角度分布强耦合。我在实际项目中只有在重投影误差明显偏大且确认是畸变模型不够时才会开启 k3。2.3 最少照片数怎么定从数学约束推导出实践下限张氏法理论上最少需要 3 张不同姿态的照片才能解出完整内参但实际工程中 3 张是绝对不够的。原因在于相机的内参有 4 个未知数fx, fy, cx, cy畸变至少还要 k1、k2、p1、p2 这 4 个加在一起 8 个未知量。每张棋盘格照片能提供的独立约束是有限的而且角点检测本身存在亚像素级的噪声照片越少噪声对结果的影响越大。我一般会建议至少拍 15 张理想状态是 2025 张。这 15 张不是随便拍的它们要覆盖不同的空间角度左右倾斜、上下俯仰、平面旋转还要包含距离相机远近不同的画面。一个常见的误区是把棋盘格放在同一个平面上平移着拍这样看起来有十几张图其实它们提供的是几乎重复的约束标定结果不会比拍 3 张强多少。关于角度分布怎么设计下面在拍摄章节会展开讲。注意OpenCV 的calibrateCamera函数不会挡你你只给 3 张图它也能跑通并返回结果但这个结果通常错得离谱且没有任何警告。所以在流程上第一件事就是检查样本数是否达标。3. 用 OpenCV 复现张正友标定从拍图到重投影误差的完整流程3.1 拍摄数据的取舍棋盘格数量、角度分配与光照要求动手写代码之前先花 20 分钟把数据拍好这一步占了标定质量的 70%。棋盘格的角点数量建议选 9 行 × 12 列这类非对称布局也就是内角点是 8×11每行每列格数不一样。之所以不用正方形图案是因为棋盘格方向需要唯一性——如果行数和列数一样旋转 90 度后检测到的角点坐标可能对不上标定直接翻车。拍摄时让棋盘格充满画面的 1/3 到 1/2太小了角点检测不稳定太大了容易超出对焦范围导致边缘角点模糊。角度分配有一个简单的口诀前、后、左、右、上、下、斜 45 度每个方向拍两三张再把棋盘格拿远拿近各拍几张。你可以在一个房间里轻松完成这个数据采集但要注意保持相机固定移动棋盘格而不是移动相机这样外参变化更丰富标定稳定性更好。光照条件是新手最容易忽略的。棋盘格上的黑色格子会反光白色格子会过曝这两者都会导致角点检测偏移几个像素。我常用的做法是用 LED 面板灯或窗户的自然光避免点光源直射棋盘格。如果现场条件不允许优先保证白色格子不过曝因为过曝会让白色区域变成一坨死白角点检测算法在这个区域会迷失方向。拍摄时用手持棋盘格即可但手要稳运动模糊的图片直接丢弃不要自欺欺人。3.2 角点检测与亚像素细化核心函数的参数与判错方法角点检测是标定里第一个容易出错的环节。OpenCV 提供了findChessboardCorners来检测棋盘格的内角点它需要三个关键参数输入的灰度图、棋盘格的内部角点数量例如 (8, 11)、检测标志位。检测成功后函数会返回一个角点坐标列表通常我会紧接着做亚像素细化用cornerSubPix把角点精度从整数像素提升到亚像素级别这一步对最终标定精度的影响非常显著。import cv2 import numpy as np # 棋盘格内角点数量12列 x 9行 → 内角点是(11, 8) corners_count (11, 8) criteria (cv2.TERM_CRITERIA_EPS cv2.TERM_CRITERIA_MAX_ITER, 30, 0.001) # 读取图像并转为灰度 img cv2.imread(checkerboard_01.jpg) gray cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) # 检测棋盘格角点 ret, corners cv2.findChessboardCorners(gray, corners_count, None) if ret: # 亚像素细化窗口大小设为(11,11)搜索区域(-1,-1)表示用窗口本身大小 corners_refined cv2.cornerSubPix(gray, corners, (11, 11), (-1, -1), criteria) cv2.drawChessboardCorners(img, corners_count, corners_refined, ret) cv2.imwrite(corners_visualized.jpg, img)这段代码里criteria是迭代终止条件30 是最大迭代次数0.001 是精度阈值。cornerSubPix的窗口大小选 (11, 11) 是基于经验窗口太小抗噪能力差窗口太大又可能把相邻角点卷进来。检测失败时findChessboardCorners返回retFalse这时不要在代码里默默跳过而要把对应的文件名打印出来手动看一下这张图到底出了什么问题。还有一个关键参数藏在findChessboardCorners的 flags 里默认情况下它是 0。如果你的图片有轻微的镜头畸变导致棋盘格边缘的角点检测失败可以尝试加cv2.CALIB_CB_ADAPTIVE_THRESH或cv2.CALIB_CB_FAST_CHECK前者会做自适应阈值处理改善光照不均匀的图片后者是快速检查会跳过明显不包含棋盘格的图像节省计算时间。3.3 标定主流程calibrateCamera 的输入输出与参数调优当所有图像的角点都检测成功后就进入核心标定环节。你需要准备两个列表objpoints是世界坐标系下的角点坐标imgpoints是图像坐标系下的角点坐标。世界坐标的构建方式很简单——假设每个格子边长是 30 毫米第一个角点是 (0,0,0)第二个是 (30,0,0)以此类推Z 轴恒为 0。# 准备世界坐标假设格子边长 30mm内角点 11x8 objp np.zeros((11 * 8, 3), np.float32) objp[:, :2] np.mgrid[0:11, 0:8].T.reshape(-1, 2) * 30.0 # 收集所有图像的角点 objpoints [] # 每张图的世界坐标 imgpoints [] # 每张图的像素坐标 # 假设你已经通过 findChessboardCorners 获得了所有有效图像的角点 # 每成功检测一张图就 append 一次 # objpoints.append(objp) # imgpoints.append(corners_refined) # 执行标定flags0 表示用默认的畸变模型k1,k2,p1,p2 ret, mtx, dist, rvecs, tvecs cv2.calibrateCamera( objpoints, imgpoints, gray.shape[::-1], None, None, flags0 ) print(内参矩阵:\n, mtx) print(畸变系数:, dist.ravel()) print(重投影误差 RMS:, ret)这段代码里flags0是默认配置使用 4 个畸变参数也是我推荐大多数项目使用的起点。如果你想固定主点 cx、cy 不动可以加上cv2.CALIB_FIX_PRINCIPAL_POINT但这么做需要你对镜头光心位置有把握否则会引入系统误差。另一个重要标志是cv2.CALIB_ZERO_TANGENT_DIST它会把切向畸变 p1、p2 固定为零适用于大部分镜头装配质量较好的工业相机如果标定后发现切向畸变值很大反而是镜头装配歪了的信号这时候才应该解除这个限制。rvecs和tvecs是每张照片对应的外参旋转向量和平移向量分别表示棋盘格相对于相机的位置和朝向。注意它们是长度等于照片数的列表不是单个值。如果你用这张标定结果去做后续的姿态估计需要确认 OpenCV 的坐标系约定X 向右Y 向下Z 向前这和很多机器人手眼标定里的坐标系方向不一样转换时不要记错符号。3.4 重投影误差的读法验证标定质量的第一步calibrateCamera返回的第一个值ret是重投影均方根误差它表示所有角点的观测像素位置和根据标定结果重新计算的投影位置之间的平均距离单位是像素。这个数值是判断标定质量最直观的依据但它只告诉你整体的平均水平不告诉你哪张图出问题了。我的一组参考阈值RMS 小于 0.3 像素说明标定质量良好0.30.8 像素可以接受但值得检查是否有某张图角点检测不准大于 1 像素标定结果不能信任你需要回到数据采集阶段排查原因。注意RMS 低不代表内参一定准确因为如果某次角点检测的误差是系统性的比如所有图像都有同样的偏差方向内参会以错误的方式吸收这些误差RMS 依然好看这种情况要靠外参一致性和校正后图像的直线度来辅助验证。这次标定得到的mtx和dist就是你要拿去给undistort函数用的参数。在正式投入项目之前至少要做一步验证用标定参数把一张包含直线边缘的照片做畸变校正看直线是否真的变直了。如果校正后的边缘出现波浪形说明畸变模型和真实镜头不匹配这通常指向畸变系数阶数选择不当或者某张关键图片的角点误差太大。4. 标定板设计与坐标系约定一张 A4 纸背后的精度玄学4.1 棋盘格规格选择格子尺寸、行列数与打印精度棋盘格的设计直接关系到角点检测的成功率和标定精度。格子尺寸的选择原则是让每个格子成像后占据 1530 像素太小的格子导致角点区域的图像信息不足影响亚像素细化的精度太大的格子虽然检测容易但受限于画面大小你能拍到的角点数量变少标定约束减弱。举个例子如果你用的是 1080p 的普通工业相机视场约 60 厘米宽打印 A4 大小的棋盘格格子边长 2535 毫米是比较合适的区间。行列数的选择要考虑两个约束。第一行列数不能相等避免旋转对称性第二行列数不能太接近否则在小角度旋转时方向判定也可能出错。我常用的是 9 列 × 12 行或者 8 列 × 11 行这些组合在 OpenCV 的角点检测中表现稳定。打印时使用激光打印机比喷墨更可靠喷墨打印的黑色格子边缘经常有毛刺在亚像素级别会被检测为偏移几个微米的假角点。打印后要把棋盘格贴在绝对平整的硬板上。常见做法是贴在铝板或亚克力板上不要用纸板或泡沫板它们会弯曲。如果贴上去后发现表面有气泡或褶皱直接重新打印不要幻想后期的算法能补偿物理不平整——标定板本身的平面度误差是系统性的它会让所有照片的内参产生一致的偏置而这种偏置不会体现在重投影误差上非常难以察觉。4.2 世界坐标系的两种建法为什么我推荐用毫米做单位构建objpoints时有两种常见做法一种是用格子数量做单位即第一个角点是 (0,0)第二个是 (1,0)不引入物理尺寸另一种是用实际物理尺寸做单位即格子边长 30 毫米第二个角点是 (30,0)。这两种方式在数学上是等价的因为单位只会影响平移向量 t 的尺度内参和畸变不受影响。但我推荐你用物理尺寸因为后续做三维重建或尺寸测量时外参可以直接用于计算实际空间距离。如果用格子数做单位你还需要额外做一个比例换算这个环节容易引入错误。单位选择是毫米、厘米还是米都可以但要注意保持统一。值得提醒的是如果你用calibrateCamera的返回值去解算棋盘格在相机坐标系下的位姿tvecs的单位和你构建objpoints的单位一致这一点在写后续代码时很容易踩坑。关于objpoints的构建方式上面的代码块用了np.mgrid它是等间距网格生成的简洁写法。这个数组一旦生成所有位置关系就确定下来了你在采集每张照片时并不需要重新创建只需要对每张成功检测到角点的图片append一次同样的objp即可。4.3 标定板的物理平整度最容易被忽略的系统误差物理平整度是张正友标定法里最大的隐形杀手。算法从底层就假设标定板是理想平面哪怕你的棋盘格打印得完美无缺一旦贴在弯曲的底板上你就在往系统里注入一个固定的伪误差。这个问题最气人的地方在于它不会让重投影误差显著变大因为所有照片的角点位置都偏移了大致相同的方向内参会以「错误的最优解」方式吸收这些偏移得到看起来合理但实际不可用的参数。验证平整度有一个简单方法标定完之后把棋盘格放在桌面上用标定得到的外参计算棋盘格平面和桌面法线的夹角如果两次摆放位置得到的夹角差超过几度说明标定板不是平的或者标定结果不可靠。另一个验证方法是把标定板翻面用背面朝相机再拍几张并用同一套标定参数做验证看看重投影误差的变化是否在正常范围内。注意不要在标定中途更换标定板。哪怕只是多了一张纸贴在下面棋盘格的平面位置就变了所有已经采集的照片和后续照片将不再属于同一几何系统必须从头重拍。5. 张正友标定法常见问题排查角点误检、过度校正与边缘模糊5.1 现象角点检测错位导致内参明显偏离最典型的症状是标定后内参的 fx、fy 偏离镜头标称值超过 10%或者主点 cx、cy 不在图像中心附近。原因几乎总是指向某一两张图片的角点检测结果错位了可能是边缘的角点被误检到相邻位置也可能是反光导致的亚像素偏移。解决方法是把中标定的每一张图都做角点可视化通过drawChessboardCorners输出带有角点标记的图片人工扫一遍。如果你发现只有一两张图出错直接删除对应样本重新标定即可。但如果很多图都出错就要回到拍摄环节检查焦点和光照。另一个隐蔽原因是亚像素细化参数不对cornerSubPix的窗口大小 (11, 11) 在大分辨图像上可能偏小可以增大到 (15, 15) 再试不过这会显著增加计算时间。5.2 现象畸变校正后图像边缘出现波浪畸变校正后画面中的直线变成了曲线通常是波浪形或者 S 形。原因大多是过度校正也就是畸变系数选的阶数偏高比如你开了 k3 但镜头本身的畸变很小。k3 会让畸变曲线在图像边缘产生反向弯折这是高阶多项式在高次项主导下的典型行为。解决思路把畸变模型降回 k1、k2 两个径向参数加两个切向参数重新标定。或者检查一下是否有一两张照片里棋盘格只出现在画面边缘这些照片会让算法过度拟合边缘畸变。对于普通镜头k1 和 k2 完全足够k3 是为鱼眼镜头和超广角准备的不要拿工业定焦镜头去硬套。5.3 现象同一相机两次标定结果差异大同一台相机同一批数据两次标定得到的内参差出 3% 以上这通常不是你手动操作的问题而是标定样本集本身缺乏多样性。如果所有照片里棋盘格的朝向角度都很接近外参的自由度没有充分激发内参在优化空间里就不是唯一极值点算法每次收敛到不同的局部最优结果自然不稳定。解决方法是扩充样本集的姿态分布特别是加入大幅度的倾斜照片让棋盘格平面与图像平面夹角小于 45 度甚至接近 30 度。这能大幅强化内参解的唯一性。在实际项目中我标定完一组数据后会立即随机抽一半样本重新标定对比两次的内参差异如果差异大于 1%就直接重拍数据不浪费时间在调参上。5.4 现象棋盘格部分区域反光导致检测失败反光和过曝是角点检测的天敌黑色格子反射灯光时会形成亮斑算法在亮斑边缘提取角点位置偏移几个像素是家常便饭。解决方法是改变光源位置让光线从侧面入射而不是从相机同侧直射棋盘格。如果无法改变光照拍摄时稍微转动棋盘格的角度让反光区域移出棋盘格范围。更严重的反光问题需要从材质入手市面上卖的哑光覆膜标定板比普通打印纸反光少很多。如果你只有普通打印纸可以试着把标定板放到阴影里拍摄同时适当降低曝光时间保证白色格子不发白。过曝是没有任何后期手段能救回来的数据采集阶段就把曝光压住。6. 让标定结果更稳样本筛选、白平衡统一与可视化验证技巧6.1 用样本筛选把误差大的照片剔除出数据集calibrateCamera只给你一个总的 RMS它不会告诉你哪张图拖了后腿。我的做法是在完成初标后遍历所有样本用标定得到的mtx、dist结合每张图的外参逐张计算重投影误差均值把误差显著高于整体水平的照片挑出来看它是否在角度、光照或清晰度上有问题。# 用初标结果逐张计算重投影误差 mean_errors [] for i in range(len(objpoints)): # 用当前外参把世界点投影到图像平面 proj_points, _ cv2.projectPoints( objpoints[i], rvecs[i], tvecs[i], mtx, dist ) error cv2.norm(imgpoints[i], proj_points, cv2.NORM_L2) / len(objpoints[i]) mean_errors.append(error) # 输出所有图片的误差并按大小排序 for idx, err in sorted(enumerate(mean_errors), keylambda x: x[1], reverseTrue): print(f图片 {idx:02d} 平均误差: {err:.4f} 像素)运行这段代码后你会看到误差分布通常是不均匀的个别图片的误差可能是整体均值的三到五倍。这些图片在整体优化里是「噪声源」它们会拉扯内参偏向错误的方向。简单有效地处理方式是设置一个阈值比如超过整体均值两倍的样本直接剔除然后用剩下的样本重新标定一次。注意每次剔完要重新跑完整标定流程因为内参变了误差计算也要跟着变。这种筛选不是一劳永逸的它需要做一到两轮。在数据采集时拍 25 张经过两轮筛选后保留 1820 张是正常比例。如果一轮筛完才发现大量图片被剔说明拍摄阶段的问题比较大回到第 3 章重新拍不要靠筛选来救命。6.2 统一白平衡与曝光工程细节对亚像素精度的影响在工业检测项目里相机的自动曝光和自动白平衡对标定来说是敌人。自动曝光会让不同照片中棋盘格区域的平均亮度波动黑色格子在不同亮度下的亚像素边缘位置会有微小差异这些差异叠加起来会让角点检测的重复性变差。同样的道理白平衡变化会影响 Bayer 插值后的色彩边缘虽然这对灰度角点检测的影响相对小但在高精度项目中依然不可忽视。正确的做法是先把相机设为手动曝光、手动白平衡固定增益然后在整个标定过程中保持光照环境不变。你可能觉得这很麻烦但实际上只需要在标定前把参数调好后面就不再动它。这组固定的曝光参数不一定在每张照片里都完美曝光只要棋盘格区域不过曝不欠曝角点检测就能正常运转。还有一个小技巧要注意不要用 JPEG 格式做标定数据。JPEG 的有损压缩会在黑白格子交界处产生振铃效应亚像素角点位置会被轻微扰动。如果相机输出 RGB 或 Bayer 原始数据直接存成 PNG 再拿来标定这对精度的影响在工业视觉的严苛要求下是能察觉的。如果你的标定结果总是徘徊在阈值边缘这一个细节可能就是压死精度的那根稻草。6.3 把重投影误差做成可视化验证图数值层面过关了还不够我最后总会做一张可视化验证图把棋盘格的原图、检测到的角点、以及用标定参数重新投影的角点一起画出来观察两者的差值方向和大小。这会暴露数值指标看不出来的系统偏差。举个例子如果大多数点的误差方向一致地朝右上方偏移说明标定板本身有微小的旋转或倾斜或者某个外参的初始值偏差较大。import matplotlib.pyplot as plt # 选择一张代表性图片做可视化 i 10 # 换成你想检查的图片索引 proj_points, _ cv2.projectPoints(objpoints[i], rvecs[i], tvecs[i], mtx, dist) plt.figure(figsize(12, 8)) plt.imshow(cv2.cvtColor(cv2.imread(checkerboard_01.jpg), cv2.COLOR_BGR2RGB)) # 观测角点画绿点重投影角点画红点 plt.scatter(imgpoints[i][:, 0, 0], imgpoints[i][:, 0, 1], s4, cgreen, markero, labeldetected) plt.scatter(proj_points[:, 0, 0], proj_points[:, 0, 1], s4, cred, markerx, labelprojected) # 画误差方向线放大 20 倍方便观察 for j in range(len(objpoints[i])): dx (proj_points[j, 0, 0] - imgpoints[i][j, 0, 0]) * 20 dy (proj_points[j, 0, 0] - imgpoints[i][j, 0, 0]) * 20 plt.arrow(imgpoints[i][j, 0, 0], imgpoints[i][j, 0, 1], dx, dy, colorblue, alpha0.5, head_width2) plt.legend() plt.axis(off) plt.show()如果红色叉子和绿色圆点几乎重合箭头方向杂乱无章且长度很短说明标定质量过硬。如果箭头呈现明显的放射状或同心圆状分布这可能和畸变模型的残余有关考虑是否要调整畸变阶数。这张图也是你和同事、客户沟通标定质量的最直观凭证比甩一个 RMS 值更有说服力。这个方向做久了你会发现标定的最后一步不是算法而是操作习惯。我现在每次标定都会在文件夹里保存原始图像、角点可视化图和重投影误差图三份文件存档下来一旦后续项目出现测量偏差可以随时回溯问题环节。这个习惯帮我在很多项目里节省了大量排查时间希望你从第一次项目起就养成它希望帮到你。本文还有配套的精品资源点击获取