拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

相机内参标定与张正友标定法:从成像模型到重投影误差实战解析

相机内参标定与张正友标定法:从成像模型到重投影误差实战解析 说实话很多做视觉、SLAM、三维重建的朋友最开始都是在网上随便找个棋盘格图片跑一遍OpenCV的calibrateCamera拿到一个内参矩阵和几个畸变系数然后就当任务完成了。但等到换相机、换分辨率、或者发现重投影误差怎么都压不下去的时候才开始回头琢磨相机内参标定到底标的是什么张正友标定法又是怎么推出来的这篇就把相机内参标定理论篇做个完整梳理重点是张正友标定法从成像模型到数学求解的完整链路把每个关键公式背后的物理含义和约束逻辑讲清楚。文章不会只停留在“调用函数”的层面而是告诉你每一步在做什么、为什么这么做、做的时候有哪些坑。适合刚入门视觉的初学者也适合用过OpenCV标定但没系统捋过原理的工程师。1. 为什么非标定不可相机内参到底在解决什么问题1.1 针孔模型下的成像关系从世界坐标到像素坐标任何相机标定本质都是在求解一个从三维世界到二维图像平面的映射关系。最经典的模型就是针孔相机模型它把相机的成像过程简化成了一个小孔和一块成像平面光线从三维空间中的物体表面反射经过光心小孔投影到成像平面上形成一个倒立的像。这个过程写成齐次坐标形式就是大家都很熟悉的那条链路Z_c * [u, v, 1]^T K * [R | t] * [X_w, Y_w, Z_w, 1]^T左边是像素坐标右边是相机外参旋转矩阵R和平移向量t把世界坐标变换到相机坐标再经过内参矩阵K把相机坐标投影到像素平面。其中内参矩阵K长得这样K [fx, gamma, cx, 0, fy, cy, 0, 0, 1 ]fx和fy是焦距在像素单位下的表达等于物理焦距除以像元尺寸cx和cy是主点坐标光轴与成像平面的交点理想情况下是图像中心gamma是像素坐标系的倾斜系数现代相机几乎都为0。这里我不厌其烦地强调一个很多人会忽略的点内参矩阵描述的是相机坐标到像素坐标的映射它只与相机自身的物理结构有关和场景里有什么物体、物体在哪里完全无关。这也是为什么我们可以用一块已知尺寸的标定板在不同位置、不同角度拍很多张照片来求解同一个K因为K是固定不变的每张照片里变化的是外参。1.2 内参矩阵和畸变系数标定到底标的是什么东西有了内参矩阵是不是就标定完了并没有。真实的镜头不是完美的小孔光线经过镜头时会发生折射导致实际成像点偏离理想投影点这就是畸变。畸变分两类径向畸变和切向畸变。径向畸变主要来自镜头的曲面形状表现为图像边缘的直线会弯曲。桶形畸变就是中心向外鼓枕形畸变是边缘向内收。切向畸变则是因为镜头与成像平面不完全平行表现为图像在某个方向上被拉伸或压缩。工程上畸变模型通常用下面的多项式描述x_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。k1、k2、k3是径向畸变系数有的模型只用k1、k2p1、p2是切向畸变系数。所以一次完整的相机内参标定输出结果应该是内参矩阵K的4个参数fx, fy, cx, cygamma一般取0 畸变系数k1, k2, p1, p2, k3 每张标定板图像的外参。这里有个容易混淆的概念大部分教材说“内参标定”输出的是K和畸变系数但实际标定过程中每张图的外参R和t也会被同时求解出来。外参是某张图中标定板相对于相机的位姿是“过程产物”标定完如果不需要就丢掉。不过在很多半自动标定场景比如手眼标定、结构光投影仪标定里这些外参反而是要用的所以标定结果里保留一份也是有价值的。1.3 什么时候需要重新标定相机内参不是一劳永逸的相机内参虽然只与相机自身结构有关但这个“自身结构”是会被改变的。以下几个方面基本都需要重新标定镜头变焦变焦镜头的焦距变了fx、fy和畸变系数都会跟着变。分辨率切换同一款相机的不同分辨率模式主点位置和焦距的像素表达都会变不过等比例缩放内参有时可以近似但不推荐。调焦或拧镜头手动调焦会改变镜片组间距畸变和主点都会有变化。温度变化高端工业相机会有温漂但一般情况下不必考虑。我见过很多项目摄像头换了分辨率后直接拿旧内参去算结果三维测量误差大到不可接受最后排查一圈发现就是内参没更新。所以标定不是一次性工作而是传感器维护流程里的一部分每次工作条件变化后建议重新走一遍。2. 张正友标定法的数学骨架从单应性矩阵到内参求解张正友标定法发表于1998年被广泛应用于各类视觉系统标定。它的核心贡献在于在不需要昂贵标定设备如多轴转台、3D标定块的情况下只需要一块平面标定板拍几张不同姿态的照片就能线性地求解出内参初值再通过非线性优化做精化。这属于典型的“平面标定法”和传统的“立体标定法用多个平面组成3D标定块”相比成本低、操作灵活、精度也有保障。2.1 为什么偏偏用平面标定板单应性矩阵是怎么来的前面讲到相机成像公式其中世界坐标是在三维空间里的。但标定板只有一块板是一个平面所以可以假设标定板所在平面的Z0。这个假设是整个张正友标定法的第一步也是最容易被忽略的巧妙之处。当Z_w0时世界坐标到像素坐标的映射就变成s * [u, v, 1]^T K * [r1, r2, t] * [X_w, Y_w, 1]^T这里r1、r2是旋转矩阵R的前两列。左边的3x3矩阵就是单应性矩阵HH K * [r1, r2, t]单应性矩阵描述的是同一平面在两个视角之间的投影关系。在这里它把“标定板平面上的一个点”直接映射到“图像平面上的一个像素”中间跳过了深度维度。这个3x3矩阵有8个自由度因为尺度模糊通常令h331或者做归一化而平面上的棋盘格角点每个角点可以提供u和v两个约束方程所以理论上只要4个不共线的角点就能求解出一个H。实际OpenCV中用的是findHomography但它会采集棋盘格上所有角点做DLT直接线性变换再通过RANSAC或最小二乘来求解H。每张不同姿态的标定板图片就是一个不同的H。这里要强调一个直觉单应性矩阵H是张正友标定法的“中间变量”它把内参K和外参“未知的混合体”一起打包了。单看一张图我们无法把H拆出K和[R|t]因为对同一个H来说K和外参可以有无穷多种组合方式。所以下一步的关键是利用多张图片之间的几何约束来解开这个“混合体”。2.2 从单应性矩阵反推内参正交约束才是最关键的抓手现在的问题变成已知每张图片各自算出了一个H如何从这些H中解出唯一的K张正友标定法的高明之处在于它利用了旋转矩阵R的一个天然性质旋转矩阵是正交矩阵它的列向量之间正交且每个列向量的模长为1。把H写成列向量形式H [h1, h2, h3]那么根据H K * [r1, r2, t]有h1 λ * K * r1 h2 λ * K * r2 h3 λ * K * t其中λ是尺度因子。由此可得r1 λ * K^(-1) * h1 r2 λ * K^(-1) * h2因为r1和r2是正交单位向量所以得到两个重要约束r1^T * r2 0 h1^T * K^(-T) * K^(-1) * h2 0 |r1| |r2| h1^T * K^(-T) * K^(-1) * h1 h2^T * K^(-T) * K^(-1) * h2这两个约束就是张正友标定法求解内参的“钥匙”。它们都不依赖外参的绝对数值只建立在内参矩阵K和观测到的H列向量之间的关系上。令B K^(-T) * K^(-1)B是一个3x3对称矩阵实际只有6个独立元素可以排列成一个向量b。于是上面的两个约束每个都可以写成关于b的线性方程。把不同视角的约束叠起来会得到一个形如V * b 0的线性系统。每个视角提供两个方程b有6个未知数其实有尺度模糊需要7个约束才能确定比例所以至少需要3张不同姿态的标定板图像才能线性求解出b。这就是为什么很多标定教程会说“最少拍3张但建议拍10到20张”的原因。3张只是数学上可解的下限实际为了抑制噪声和角点检测误差必须用更多图片让方程组超定再通过最小二乘得到一个统计意义上的最优解。解出B之后内参矩阵K可以通过Cholesky分解或直接公式拆解得到。虽然OpenCV没有暴露这个细节但理解B的构造就能明白一件事标定板拍得越多、姿态越多样V矩阵的条件数越好b的解就越稳定。这也是为什么图片数量少、姿态单一的时候标定出来的fx和fy总是奇奇怪怪的原因。2.3 畸变系数的估计先忽略再微调的非线性优化思路线性求解出K和外参初值后理论上就可以投影了。但前面说过真实镜头还有畸变。张正友标定法处理畸变的思路非常工程化先忽略畸变用线性解出初始的内外参再把畸变系数加进来通过非线性优化整体精修一遍。这个思路本质上就是“分步求解全局优化”的典型套路。它的好处是线性解给了非线性迭代一个足够接近全局最优的起始点避免了迭代发散到局部极小值。如果一开始就把所有参数内参、外参、畸变系数一起扔给非线性优化器参数空间维度太高优化很容易跑飞。最终的优化目标就是经典的重投影误差最小化min Σ_i Σ_j || m_ij - m_hat(K, R_i, t_i, k1, k2, p1, p2, k3, M_j) ||^2意思就是用当前估计的内外参和畸变系数把每个棋盘格角点重新投影到图像平面计算投影点和实际检测到的角点之间的像素距离然后不断调整参数让所有点的总距离最小。因为是典型的非线性最小二乘问题Levenberg-Marquardt算法几乎是标配。顺带说一个细节畸变系数为什么要在非线性优化阶段引入而不是直接线性求解因为畸变模型是非线性的没法写成线性的约束方程。所以只能把它放在优化框架里用迭代的方式逼近。OpenCV的calibrateCamera内部就是这套流程只是默认帮你跑完了。2.4 为什么至少需要三张不同姿态的图退化情况的数学解释刚才说至少需要3张图才能线性求解B那如果只拍一张或者两张会发生什么直观理解是单应性矩阵H的8个自由度里“吸收”了一部分内参信息但同时也吸收了外参信息。单张图只能给出一组H它能提供的关于B的线性约束只有2个方程而B有6个未知数显然欠定。两张图则是4个方程依然欠定。只有当3张图提供6个方程时方能凑齐约束数。但这里有一个隐藏的陷阱也是很多新手标定失败的原因3张“不同姿态”的图指的是标定板与相机之间姿态差异足够大如果三张图基本是同一个姿态比如都是正对相机只做了平移没有旋转那么它们提供的约束几乎是线性相关的。数学上这些方程构成的V矩阵会接近奇异解出来的b毫无意义。现实中我更建议最少拍10张以上并且保证姿态有明显旋转越“歪”越好。3. 实操流程与关键细节从棋盘格到重投影误差理论知识说完下面把实操的每一步拆开讲讲。3.1 标定板的制作与选择精度从源头开始张正友标定法的前提是“已知平面标定板上特征点的世界坐标”。所以标定板上的图案必须满足两个条件特征点坐标足够精确、特征点检测足够稳定。最常见的两种标定板是棋盘格和圆点阵列棋盘格角点位置通过黑白格交界处计算检测算法成熟OpenCV自带的findChessboardCorners专门干这个精度高。缺点是如果镜头畸变太大边缘的格子可能变形严重导致无法识别。圆点阵列圆心的质心检测精度高抗边缘畸变能力强但需要确定圆心对应关系处理起来比棋盘格稍微麻烦一点。不管选哪种有几点通用经验打印标定板时建议用高精度打印机不要用普通家用喷墨打印机随便打一版。格子尺寸的误差会直接内化成标定误差且无法事后修正。如果要求高建议去打印店用激光打印在高光相纸上便宜且精度可控。标定板要贴在绝对平整的平面上。我踩过最大的坑就是把标定纸贴在快递盒纸板上结果照片拍出来边缘角点全部往下偏重投影误差0.3像素怎么都压不下去。打印后必须实际测量格子边长而不是用设计值。因为这个值会作为标定板的物理尺度输入给算法直接影响fx、fy的绝对数值。关于棋盘格尺寸的选择没有一个绝对标准一般看使用场景。如果是手机广角镜头建议格子在12mm到30mm之间标定板整体不要太小如果是工业镜头视野比较小可以适当做小一点。反正角点在图像里的像素尺寸不能太小否则角点提取精度会下降。3.2 图像采集规范角度、数量、光照一个都不能少图像采集是整个标定流程里最容易被敷衍、但对结果影响最大的环节。这里把采集要点列一下数量至少10到15张宁多勿少。如果你发现重投影误差一直很飘先加图片数量别急着改参数。姿态标定板要倾斜、旋转、远近交替。比较有效的采集方式是“先正面、再向四个角方向倾斜45度左右、再远近变化”。姿态越丰富B矩阵求解越稳定。覆盖范围标定板在画面中的位置要覆盖到整个图像特别是边缘和四角。因为畸变在边缘最明显如果标定板永远只在画面中央那边缘的畸变系数就完全标不出来。光照保证标定板表面光照均匀不要有高光反射。棋盘格是高对比度图案如果有反光或阴影角点检测会偏移。清晰度拍照时要保证图像清晰不能有运动模糊。用手持相机拍的时候建议连拍几帧然后挑最清晰的。用工业相机的话确认曝光时间不要太长。实际项目里如果相机是固定的比如装在生产线上的你可以手持标定板在视野里变换姿态。注意标定板平面不要和像平面平行且完全不旋转这样退化问题非常严重。相反把标定板倾斜一个明显的角度哪怕只是让相机仰视、俯视板面都能显著提高标定质量。3.3 基于OpenCV的完整标定流程ImageWatch提示一下不同OpenCV版本API细节略有差异但主流程基本一致。下面是一个标准的标定脚本框架import cv2 import numpy as np import glob # 1. 设置棋盘格参数 chessboard_size (9, 6) # 内角点数量不是格子数量 square_size 0.025 # 实际格子边长单位米 # 2. 生成标定板世界坐标Z0平面 objp np.zeros((chessboard_size[0] * chessboard_size[1], 3), np.float32) objp[:, :2] np.mgrid[0:chessboard_size[0], 0:chessboard_size[1]].T.reshape(-1, 2) objp * square_size # 3. 收集角点 obj_points [] # 世界坐标 img_points [] # 像素坐标 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, chessboard_size, None) if ret: obj_points.append(objp) corners_sub cv2.cornerSubPix(gray, corners, (5, 5), (-1, -1), (cv2.TERM_CRITERIA_EPS cv2.TERM_CRITERIA_MAX_ITER, 30, 0.001)) img_points.append(corners_sub) cv2.drawChessboardCorners(img, chessboard_size, corners_sub, ret) cv2.imshow(corners, img) cv2.waitKey(100) cv2.destroyAllWindows() # 4. 计算内参和畸变 ret, mtx, dist, rvecs, tvecs cv2.calibrateCamera( obj_points, img_points, gray.shape[::-1], None, None ) print(内参矩阵:\n, mtx) print(畸变系数:\n, dist) print(重投影误差: , ret) # 5. 可选去畸变 img cv2.imread(calib_images/sample.jpg) h, w img.shape[:2] newcameramtx, roi cv2.getOptimalNewCameraMatrix(mtx, dist, (w, h), 1, (w, h)) dst cv2.undistort(img, mtx, dist, None, newcameramtx)这段代码的核心点棋盘格尺寸(9, 6)指的是内角点数量不是格子数量。也就是说这张棋盘格有10x7个格子。角点数量和格子数量很容易搞混用错了findChessboardCorners直接返回False。square_size是每个格子的实际物理尺寸单位随意只要保持一致就行。后续fx、fy的单位会和这个尺寸保持一致。cornerSubPix是亚像素级角点精化。这一步很关键直接决定了重投影误差的极限。如果跳过了这一步标定精度会明显下降。calibrateCamera返回的ret就是重投影误差单位是像素。通常小于0.1像素是优秀0.1到0.3之间可接受大于0.5就要检查采集过程是否有问题了。但这里我要提醒一个容易踩的坑OpenCV的calibrateCamera默认使用CALIB_USE_INTRINSIC_GUESS为False的流程也就是不依赖任何初始内参猜测直接从零用线性法估计。如果你的相机是广角镜头畸变太大建议在flags参数里传入cv2.CALIB_RATIONAL_MODEL来启用k3并使用CALIB_FIX_K3之类的约束逐步调整。不要一开始就放飞所有畸变参数不然畸变系数可能被优化到奇怪的方向。3.4 标定结果怎么评估重投影误差和直观验证标定精度怎么判断重投影误差是第一个指标但不是唯一指标。重投影误差指的是把已知世界坐标的角点用标定出来的内外参投影回图像平面投影点和实际检测到的角点之间的平均像素距离。这个值直接反映了标定模型和标定板角点检测的一致性。但要注意重投影误差低不代表标定准它只能说明“模型拟合了这组观测数据”。如果数据本身有问题比如标定板不平整、角点检测有系统偏移重投影误差一样会很低。所以还需要做两个额外验证去畸变视觉验证把一张有明显直线的照片做undistort观察直线是否变直。如果边缘弯曲明显说明畸变系数没标好。三维重建验证用一个已知尺寸的物体比如标尺放在相机前用标定后的相机测其像素尺寸和物理尺寸是否匹配间接验证焦距和主点的正确性。另一个常见做法是拍一份“验证集”也就是标定时不参与计算的独立照片用标定的内参和验证集角点去估计外参再算重投影误差。这个误差会更真实地反映标定的泛化能力如果验证集误差远大于训练集误差说明过拟合了。4. 常见问题与排查技巧实录4.1 重投影误差一直下不去的几个隐藏原因很多人标定完一看重投影误差0.5甚至1.0像素第一反应是“算法不行”实际上绝大多数情况下是采集的数据质量不行。我总结了几次排查经验按出现的频率排序标定板不平整。这是最容易被忽略的。标定板一旦有弯曲角点在三维空间中的真实位置就偏离了“Z0平面”的假设模型自然拟合不好。排查方法换一块比较硬的铝板或亚克力板重新贴标定纸。角点检测精度不够。如果图片有轻微模糊、压缩痕迹JPEG压缩率太高、或者反光造成灰度拉伸findChessboardCorners出来的角点坐标会有像素级偏差。这时需要检查采集的原始图质量而不是急着改代码。采集的图像姿态过于单一。比如全部是近似正对相机的姿态那B矩阵求解非常不稳定重投影误差也会飘。排查方法重新采一组包含明显旋转和远近变化的图。标定板尺寸输入错误。用9x6的板子程序里却设成10x7角点检测依然能成功但obj_points和img_points的对应关系是错的会导致标定结果完全不对。这种问题用重投影误差往往看不出来因为内参可以“适应”扭曲的对应关系但标定出的fx会明显不合理。图片数量太少。低于6张内参求解就处于欠定或临界状态结果随机性很大。建议至少10张以上。4.2 焦距畸变系数异常的对症处理标定出来的fx和fy如果和理论值差很多通常有几个原因角点检测时棋盘格自身方向搞反了。这个在长条形标定板上很容易出现导致坐标系的对齐错乱。解决方法是检查imshow输出的检测结果确保每个角点都对应正确的世界坐标索引。标定板姿态不够丰富导致fx和fy解耦不充分。fx和fy分别对应水平方向和垂直方向的焦距如果标定板只在某个方向旋转那么另一个方向的焦距约束会不足。这也是为什么倾斜姿态要尽量多样化的原因。畸变系数过大特别大的k1或k2比如超过1通常意味着初始化失败或者优化进入了局部最优。张正友标定法的线性解阶段假设的畸变为零然后用优化阶段来修正畸变。如果畸变很大线性解可能已经偏离全局最优太远导致迭代收敛到错误解。这时候可以尝试用cv2.CALIB_FIX_K1之类的约束先固定某些畸变系数先求出内参再逐步放开畸变系数。4.3 不同场景下的标定策略与工具选型到这里为止说的都是张正友标定法配合棋盘格在多视角下执行的标准流程。但实际工程里场景不同标定的策略也要变。简单固定相机场景比如单目测距用OpenCV的calibrateCamera就够了棋盘格10到15张只要按照前面说的姿态拍好结果就不会差。手持相机且标定板不好固定可以用AprilTag或Charuco标定板。Charuco的优势是即使标定板被部分遮挡也能通过ArUco码恢复角点位置特别适合视频流实时标定。广角/鱼眼镜头普通针孔模型配5参数畸变模型已经不够用了。OpenCV有独立的fisheye模块用的是等距投影模型加不同形式的畸变模型参数更多、标定流程也更讲究。直接用普通calibrateCamera去标鱼眼镜头重投影误差会非常难看。高精度视觉测量比如激光三角测量、高精度三维重建建议用更高阶的标定方案比如基于多次曝光和多重姿态的Polynomial模型或者使用专业标定工具如Kalibr、MRPT做联合标定。张正友标定法在这里更适合作为初值估计而不是最终输出。工具层面纯标准针孔模型标定用OpenCV的C/Python都行多相机标定用Kalibr依赖ROS环境但效果很好如果是工业相机厂商提供的SDK比如海康威视、Basler里自带标定工具也可以作为交叉验证。最后再分享一个小技巧标定完成后不管是存成yaml还是json都要记录当时的图像分辨率。因为内参矩阵和畸变系数都是针对特定分辨率而言的换了分辨率后直接复用轻则精度受损重则整个三维测量结果直接错乱。我见过不少项目在这一点上栽过跟头先把分辨率写进配置文件里能省掉很多后续排查时间。
返回列表