相机内参标定与畸变矫正:原理、实操与避坑指南 1. 项目概述1.1 内参标定到底在解决什么问题做视觉相关开发的人早晚都会碰到相机标定这件事。不管是做测量、识别、机器人抓取还是AR叠加、三维重建第一步几乎都是先把相机内参和畸变系数标定出来。如果你跳过这一步直接去跑算法后面大概率会被各种莫名其妙的偏差坑到怀疑人生。简单来说相机内参描述的是“三维世界中的一个点投影到二维像素图像上时映射关系是什么样”。而这套映射关系并不是完美的直线投影因为镜头本身有加工误差、装配误差光线穿过镜片后会发生弯曲这就引入了畸变。我们标定的目的就是把这两块东西精确地求出来——一组内参矩阵加一组畸变系数有了它们才能对图像做畸变矫正或者在多个相机之间做几何对齐。这个项目适合以下人群刚接触OpenCV标定流程的学生、做工业视觉测量方案的同学、做SLAM或三维重建的开发者以及那些被“标定结果不稳定、矫正完图像边缘还是弯”折腾过的工程师。说白了只要你的代码里用到过calibrateCamera这个函数这篇文章都值得看完。我写这篇内容不只是把公式贴出来然后说“你跑一遍就行”而是把每个环节背后的原理、每一步操作的原因、每个坑的成因都拆开讲这样你在自己操作的时候才能判断“这个结果到底对不对”。1.2 内参、外参和畸变是三条线别搅在一起很多初学者搞不清内参和外参的区别导致标定的时候参数含义混乱。我这里先给出一个最直白的区分方式内参是相机自己的属性跟它在空间里怎么摆、放哪儿无关。它描述的是光心到像素平面的转换关系包括焦距fx, fy和主点cx, cy。外参是相机坐标系相对于某个世界坐标系的位姿即旋转矩阵R和平移向量t。同一个相机在不同的拍摄位置外参完全不一样但内参不变。畸变系数则是对镜头非理想成像的修正项跟内参一样也是相机自身的固定属性。标定的核心任务就是估算内参和畸变系数。外参虽然也会在标定过程中被算出来但那是针对“标定板处于某个位置”这一特定时刻的临时值标定完成后通常不会被用于后续任务除非你用的是固定外参的多目系统。我有一个习惯在任何项目开始之前先用文字把这三个概念写在代码注释里提醒自己。因为后边代码里如果同时出现cameraMatrix、distCoeffs、rvecs、tvecs命名再模糊一点特别容易把内参外参搞反。2. 相机成像模型从三维到像素的那条线2.1 针孔模型用一个小孔解释整个投影过程相机成像的底层模型现在绝大多数情况下仍然采用针孔模型Pinhole Camera Model。它的思想很简单假设光线从三维空间中的某一点出发穿过镜头中心落在传感器平面上形成一个像点。小孔成像里那个小孔在相机模型里对应的就是“光心”也叫相机坐标系的原点。这个模型最经典的表现形式就是相似三角形关系。一个三维点在世界坐标系中的坐标是(X, Y, Z)在相机坐标系里同样也是(X, Y, Z)。投影到图像物理坐标系中的坐标是(x, y)根据相似三角形可以直接写出x f * X / Z y f * Y / Z这里的f是焦距单位是毫米。注意这个公式里有除法除以Z意味着离相机越远的物体在成像平面上越小这就是透视效应的数学本质。但直接这样用不够方便因为最终我们得到的是像素坐标不是物理坐标。所以要引入像素坐标系的概念把“毫米”转换成“像素”。这中间还涉及芯片感光元件的尺寸、像素排列密度等物理参数。为了工程方便这些都被融合进了内参矩阵。2.2 四个坐标系从世界到像素的四步走如果你完整看过相机模型的相关资料一定会碰到四个坐标系世界坐标系、相机坐标系、图像物理坐标系、图像像素坐标系。这里我把它们一条线串起来。第一步世界坐标转相机坐标。这一步通过外参完成也就是Xc R * Xw t其中R是3x3旋转矩阵t是3x1平移向量。世界坐标系是我们自定义的标定板上那个角点就是世界的原点。拍摄一张标定板照片时板子相对相机的位置被R和t记录下来了。第二步相机坐标转图像物理坐标。这一步根据针孔模型利用相似三角形求得图像平面上的毫米坐标也就是上面说的x f * Xc / Zc。第三步图像物理坐标转像素坐标。这一步是加上主点偏移和像素尺寸换算u fx * x cx v fy * y cy注意这里的fx和fy已经是“像素单位下的焦距”它们等于物理焦距除以单个像素的物理尺寸。因为像素通常不是正方形所以fx和fy不一定严格相等这是很多标定结果里两个值略有差异的原因。把三步合一可以写成经典的投影公式齐次坐标形式s * [u, v, 1]^T K * [R | t] * [X, Y, Z, 1]^T其中K就是内参矩阵K [fx, 0, cx; 0, fy, cy; 0, 0, 1]这一步公式虽然短但它是整个标定理论的骨架。我建议你自己推一遍把每个字母对应的物理含义写清楚比如cx、cy是光心在像素坐标系中的坐标不是图像中心坐标。这俩只有在镜头完美对准芯片中心时才相等实际装配中通常差几个到几十个像素。2.3 内参矩阵背后的物理意义很多时候我们只关心把矩阵解出来然后去undistort但如果你想知道标定结果合不合理就必须能够解读这个矩阵里每个数值的物理意义。先说fx和fy。它们表示焦距在像素宽和高方向上的尺度。举个例子如果一台相机的物理焦距是4mm传感器上每个像素的宽度是2微米那么fx就是4 / 0.002 2000。如果你的图像分辨率是1920x1080视野大约96°那fx差不多就在900到1000这个量级。拿到标定结果后可以先粗算一下fx、fy跟画面分辨率是否匹配。如果你标出来fx8000分辨率只有2000像素那大概率是标定板没有覆盖全画面或者角点提取出了问题。cx和cy是主点坐标。正常情况下它位于图像中心附近但通常会有几个像素的偏差这属于镜头模组装配误差的正常范围。如果你的cx偏离中心超过图像宽度的5%甚至10%就要警惕了可能是标定板只拍了边缘区域或者畸变模型选用不当导致的补偿偏差。还有一点需要强调内参矩阵是“归一化”视角下的产物它只跟相机本体有关不随着图像缩放或者裁剪而变化吗答案是会变。如果你对图像做了resize比如从1080p缩到720p内参矩阵必须等比例修改否则投影关系就错了。同理如果你对图像做了裁剪cx和cy也必须整体平移修正。这一点在做图像预处理流水线的时候极易被忽略。3. 畸变从哪来径向畸变、切向畸变与多项式拟合3.1 畸变不是可选项它是真实世界的默认配置很多人第一次用OpenCV的getOptimalNewCameraMatrix时会对“要不要做畸变矫正”感到无所谓觉得不矫正好像图像也还能看。但一旦涉及测量、3D定位、拼接、AR贴合畸变不矫正误差就会直接呈现在结果里。畸变主要分两大类径向畸变和切向畸变。径向畸变是沿着从图像中心到边缘的射线方向发生的变形。它主要由镜头曲面形状的非理想性导致。典型表现是画面中的直线被掰弯了向图像中心收缩的叫桶形畸变向四周膨胀的叫枕形畸变。广角镜头一般桶形畸变明显长焦镜头可能枕形畸变更重。切向畸变则是成像平面与镜头不平行导致的表现是图像上某些区域的物体看起来有“拉伸倾斜”感棋盘格的边缘不再是纯直线而是带有轻微的扭曲。切向畸变通常比径向畸变小很多但是如果镜头装配工艺差也不能忽略。在OpenCV的经典模型里径向畸变用r0处附近的泰勒展开来拟合x_distorted x * (1 k1*r^2 k2*r^4 k3*r^6) y_distorted y * (1 k1*r^2 k2*r^4 k3*r^6)切向畸变用两个系数p1和p2拟合x_distorted x [2*p1*x*y p2*(r^2 2*x^2)] y_distorted y [p1*(r^2 2*y^2) 2*p2*x*y]这里的x, y是归一化平面的坐标r是该点到中心的距离。3.2 为什么非要五次多项式对畸变曲线形状的解释有些朋友会有疑问为何模型要用r的2次、4次、6次方为什么不是一次、三次这背后有两个原因。第一径向畸变的对称性。镜头的光学曲面大致是旋转对称的畸变是关于光轴对称的也就是畸变量只与距离r的平方有关而不是r本身因为r的方向变化会改变畸变方向而大小只跟距离有关。r的偶次幂恰好满足这种对称性。第二高阶项主要用于拟合图像边缘的剧烈形变。普通的镜头用k1、k2两个系数基本就够了但大广角、鱼眼等边缘畸变严重的镜头需要引入k3来拟合更复杂的弯曲形状。实际上如果你标定后重投影误差依然很大其中一个排查方向就是看看畸变模型的阶数是否够用。需要留意的是OpenCV经典的径向-切向多项式模型只适合普通工业镜头和消费类相机。鱼眼镜头有自己独立的fisheye模型那里面用的是等距投影模型并且畸变多项式公式完全不同。如果你拿广角镜头的最终矫正效果差别急着怪算法先确认用的标定接口和模型是否匹配。3.3 畸变矫正后的“双眼皮”现象实操中最常见的困惑是为什么畸变矫正之后图像边缘会出现一块块模糊、拉伸的区域这是因为矫正时把弯曲的像素重新映射到规则的网格上边缘区域原本被压缩的信息被拉伸开来相当于重新采样。如果畸变特别大边缘区域没有足够的原始像素信息就会出现明显的模糊甚至空洞。这是物理限制不是bug。处理办法是如果只关心中心区域可以直接用裁剪后的中心区作为有效区域如果必须保留全画面那就接受边缘分辨率下降的事实。不要指望畸变矫正能“无中生有”地恢复信息。此外从工程经验上说畸变系数不是越大越“好”而是越能真实反映镜头特性越好。过拟合的畸变系数能让你当前的标定序列重投影误差很低但换一组图片立刻崩溃。所以在评估标定结果的时候不能只盯着重投影误差这一个指标还要看畸变系数的数值是否在合理范围内。比如k1如果是-0.5甚至-1对普通工业镜头来说就属于偏大的值k2和k3如果数量级跳到离谱多半是标定数据本身出了问题。4. 实操用张正友标定法完成一次可靠的标定4.1 标定板的选择与生成说到具体操作最有名的方案就是张正友标定法。它由张正友博士在1998年提出核心思想是拍摄不同位姿的平面标定板从多张图像中恢复出内参和畸变系数。现在的OpenCV和MATLAB里的标定工具基本都是基于这个方法的变体。标定板最常用的是棋盘格因为它角点提取稳定、抗干扰能力强。棋盘格生成很简单OpenCV自带的findCirclesGrid是圆形点阵findChessboardCorners对应的是棋盘格。两者选哪个取决于你的场景。有一个容易被忽略的细节棋盘格角点不是一个具体的黑白交界点而是四个格子交会的那个顶点。所以提取时要求棋盘格必须是“完整、连续的”不能有遮挡否则角点邻域的梯度信息被破坏提取精度就会下降。生成标定板时打印尺寸务必精确。不要图省事随便缩放打印打印完后拿卡尺实际量一下方格边长。因为后面计算标定结果时需要输入squareSize方格边长单位为毫米大小差1毫米算出来的fx可能偏百分之几直接影响测量精度。另外建议选择平整度高的材料做标定板比如贴在玻璃板或者铝板上。不要把纸随便放桌上拍纸张的弯曲会让标定结果产生“假的畸变”后期矫正时反而把直线矫正成曲线。4.2 采图要求少而精覆盖全画面张正友标定法对拍摄的标定板图像有位姿要求。很多人拍个三五张就开始跑结果误差很大。通常建议采集15~25张不同位姿的图片位姿越多样求解越稳定。具体来说要注意这几点标定板在画面中的姿态不要太单一要有旋转、俯仰、平移各种变化。标定板的倾斜角度不要让角点排列过于密集到无法区分否则角点检测会失败。标定板不能只出现在画面中心。中心区域的畸变很小如果全部图像都集中在中心畸变系数根本没法标定出来。必须让标定板占据图像四周区域让畸变在图像边缘得到充分采样。不同图像中标定板的距离要有变化。虽然张正友标定法不要求标定板与相机距离已知但多距离的数据能够更好地约束焦距的尺度一致性。避免运动模糊和过曝、欠曝。角点提取依赖图像梯度模糊和过曝会降低角点定位精度。还有一点如果相机分辨率高标定板太小会导致每个方格只占几个像素角点定位精度大打折扣。反过来标定板太大又容易超出画面。我个人的经验是让单个方格在画面里至少占20个像素以上比较稳妥。4.3 用OpenCV跑通标定主流程并解读关键输出下面我给出一个完整流程的伪代码框架并用注释说明每一步的作用。这里用Python的OpenCV接口举例C版本对应函数名也类似。import cv2 import numpy as np import glob # 棋盘格规格内角点数量不是格子数量 # 例棋盘是10x7的格子则内部角点是9x6 pattern_size (9, 6) square_size 20.0 # 单位毫米务必实际测量 # 准备3D世界点假设棋盘格在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 [] # 世界坐标点 img_points [] # 像素坐标点 images glob.glob(calib_imgs/*.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) # 标定 ret, mtx, dist, rvecs, tvecs cv2.calibrateCamera( obj_points, img_points, gray.shape[::-1], None, None ) # 重投影误差 total_error 0 for i in range(len(obj_points)): imgpoints_reproj, _ cv2.projectPoints(obj_points[i], rvecs[i], tvecs[i], mtx, dist) error cv2.norm(img_points[i], imgpoints_reproj, cv2.NORM_L2) / len(imgpoints_reproj) total_error error print(平均重投影误差: , total_error / len(obj_points)) # 保存内参和畸变系数 np.savez(camera_params.npz, mtxmtx, distdist)这段代码基本覆盖了标准流程。注意几个关键点findChessboardCorners的输入是pattern_size代表“内角点”数不是格子数。假设你的棋盘格是10x7的格子那么内部角点就是9x6。如果搞反了检测大概率失败或者角点顺序错乱。cornerSubPix这一步不是可选项。它对初步检测到的角点做亚像素精化利用局部灰度梯度来优化坐标精度。跳过了这一步角点精度可能差0.2到0.5像素别看这零点几像素最终重投影误差可能从0.1直接飙到0.8甚至更高。标定结果里最核心的两个输出是mtx和dist。mtx是3x3内参矩阵dist是畸变系数向量通常包含(k1, k2, p1, p2, k3)。在OpenCV新版本里dist可能包含更多系数比如Rational模型但默认经典模型就是这五个。4.4 畸变矫正与画幅裁剪的平衡标定完之后下一个动作通常是做畸变矫正。OpenCV提供两个函数undistort和initUndistortRectifyMap配合remap。简单粗暴的做法是cv2.undistort(img, mtx, dist)但如果你要处理视频流或者批量图片建议预先计算映射表然后用remap做逐帧映射速度会快很多。h, w img.shape[:2] newcameramtx, roi cv2.getOptimalNewCameraMatrix(mtx, dist, (w, h), 1, (w, h)) mapx, mapy cv2.initUndistortRectifyMap(mtx, dist, None, newcameramtx, (w, h), cv2.CV_32FC1) dst cv2.remap(img, mapx, mapy, cv2.INTER_LINEAR)这里getOptimalNewCameraMatrix的alpha参数需要专门说明。alpha0表示矫正后的图像会裁剪掉边缘的黑边和无效区域保证输出的所有像素都来自原图的有效区域但图像尺寸可能小于原图或者有效区域收缩。alpha1表示保留所有原图像素边缘可能出现黑边或者拉伸变形。实际项目中alpha一般取0到1之间的值根据应用需求来调。比如做测量建议用较小的alpha来避免边缘变形区域对结果产生干扰做全景拼接或者视觉SLAM可能会偏向保留更大的视野范围。另外矫正之后如果要继续用标定出的内参做投影计算不能用原来的mtx和dist而应该用getOptimalNewCameraMatrix返回的newcameramtx。因为矫正过程本身改变了像素坐标和归一化坐标之间的映射关系内参也要对应调整。这一条很多人踩坑投影公式里用的内参还是旧的结果三维点和图像点对不上调了半天发现是内参没换。4.5 标定结果评估不要只看误差数值重投影误差是张正友标定法最常用的质量指标。它的含义是用一组求解出的内外参把标定板的世界坐标投回图像平面求投影像素点和实际检测像素点的距离。理想情况下误差接近0实际一般在0.05到0.3像素之间工业相机配合高精度标定板甚至能做到0.03。但重投影误差低不代表标定结果可靠。还需要检查图像残差分布。将每一张图的角点残差向量画出来如果存在明显的方向性比如全部朝同一方向偏移说明可能有系统性的标定问题比如标定板平面度差或者相机内部有震动。不同图片的子集标定结果一致性。打乱图片顺序、随机取一半图片再标一遍看内参波动是否特别大。如果波动大说明标定数据内部不够一致需要补拍图片。畸变系数的合理性。把标定结果里的k1、k2、p1、p2跟同类型镜头的典型数值范围对比如果数量级明显离谱就要排查数据采集是否存在问题。5. 常见问题与排查技巧实录5.1 重投影误差大问题出在哪如果标定完重投影误差大于0.5像素先别急着换算法模型。按照我的经验按以下顺序排查八九成问题能解决第一检查pattern_size是不是内角点数不要填成格子数。第二检查标定板方格边长square_size是否量准。第三检查所有用于标定的图像中标定板是否全部在画面的不同位置都有覆盖尤其是边缘区域。第四检查是否有图像存在轻微模糊和反光或者标定板有局部遮挡。第五检查cornerSubPix是否执行了并且窗口大小是否合理。第六检查是否有重复的、位姿几乎一模一样的图片这类图片过多会稀释有效的几何约束。我遇到过一个很典型的情况标定板纸张没贴平边缘翘起重投影误差维持在0.4左右怎么精调角点检测参数都压不下去。后来换了一块贴玻璃板上的标定板误差直接降到0.08。原因就是纸张在物理上不是一个严格平面用平面模型去拟合一个“非平面”物体误差自然下不去。5.2 角点检测失败或角点顺序乱跳findChessboardCorners偶尔会失败尤其是大广角、强透视、倾斜角度过大的场景。如果报错或返回False可以试试以下办法。把图像先做一次直方图均衡化或者自适应阈值处理增强棋盘的对比度。尤其在一些表面反光不均匀的板子上这一步效果明显。也可以调大findChessboardCorners的flags参数比如使用cv2.CALIB_CB_ADAPTIVE_THRESH和cv2.CALIB_CB_NORMALIZE_IMAGE组合增加检测鲁棒性。如果角点检测出来但顺序错乱常见原因是棋盘格在图像中旋转角度过大建议增加棋盘格边缘留白区域并尽量保证棋盘有足够的边界隔离避免把无关纹理误判为棋盘。还有一种情况你用的标定板是圆形点阵但图案是黑白相间的检测圆形格的时候如果要获取亚像素精度需要使用findCirclesGrid的SYMMETRIC_GRID或ASYMMETRIC_GRID标志流程上比棋盘格更容易出现对称性或者排序问题。所以如果没有特殊需求优先选棋盘格。5.3 矫正之后图像四周出现波浪形边缘这个现象很常见尤其是广角镜头标定后图像边缘会出现向外翻或者波浪形的变形。大概率是标定板边缘区域采图不够导致畸变系数拟合不稳定。畸变多项式在r比较大的区域外推能力很弱如果你采集的标定板点位没有覆盖图像边缘算法就会在这些区域“自由发挥”矫正结果自然很随机。解决办法额外拍摄多张标定板贴紧画面四个角和四条边的图像。注意不要让标定板超出画面否则角点提取会失败越靠近边缘越好保证畸变最严重的区域有充分的观测样本。还有一种情况是畸变模型阶数不足。普通镜头用5参数模型足够但如果镜头畸变确实比较奇怪可以尝试在calibrateCamera里指定cv2.CALIB_RATIONAL_MODEL启用包含k4、k5、k6在内的扩展模型。不过这也有代价多出来的自由度在某些数据组合下会退化导致内参波动变大所以非必要不建议一上来就用Rational模型。5.4 标定时内参结果波动很大怎么处理每次标定结果都不一样fx、cx可能差几十个像素这说明标定数据对求解问题的约束不足。最常见的原因是拍的图像数太少或者位姿变化范围太小。比如你站在同一位置只改变标定板的角度和距离但相机本身没动这种数据容易让外参之间有很强的相关性内参就变得不稳定。增加数据多样性的做法有左右平移拍摄、靠近和远离拍摄、俯仰和偏航交错拍摄。尽量让标定板相对相机的位姿覆盖到各个方向。一个检查技巧是标定完成后把外参rvecs、tvecs在画面中的分布可视化出来如果所有外参都挤在一起说明你的采图方式太单一了。5.5 相机换焦距或变焦后内参还是原来那组吗这一步想强调一个原则定焦镜头标定一次后只要镜头没有被拆装、磕碰内参基本稳定可以长期使用。但变焦镜头和液态镜头等可变光学参量的镜头每改变一次焦距或者对焦距离内参都会变。对焦距离变化对畸变的影响尤其明显所以如果你做的是变焦相机系统要么保证成像时参数固定并对应标定要么就必须做实时标定或者查表。在工业场景中很多视觉系统要求更换镜头后重新标定而不是沿用旧参数。我见过有人把旧相机的标定结果复制给新相机用结果测量的物体尺寸偏差一直对不上排查了半天才发现内参没重标。这是一个看着低级但实际很容易犯的错误。6. 最后再分享一点实操心得做相机标定这几年我最大的感受是标定算法本身已经很成熟了多数情况下问题都出在数据采集和模型理解上。只要你把标定板贴平、把图像采全、把方格尺寸量准、把角点精化步骤跑到位结果一般不会差。有一些小技巧值得记下来。拍图时尽量避免阳光直射或者强光点光源标定板上的反光会让角点区域过曝亚像素精度直接下降。工业现场如果环境光不可控可以考虑用背光照明或者亚光材质的标定板。保存标定时的图像分辨率信息。如果你中途改了分辨率返回值不会但你的代码如果忽略了分辨率变更内参会悄悄出错。我习惯把分辨率直接放在日志输出里方便后期排查。不要过分追求重投影误差极小。误差在0.1到0.3像素之间都是正常水平如果在0.05以下确实值得高兴但也要检查是不是存在过拟合。更重要的是把标定出的畸变系数拿去矫正一张测试图肉眼观察直线是否变直比单纯看误差数字更直观。最后如果做的是多相机系统比如双目测距或者3D重建标定相机内参之后还要走立体标定把两个相机之间的相对外参也求出来。常见流程是先单目标定再双目标定再通过stereoRectify做极线矫正。内参质量直接影响后续立体标定的精度所以第一步千万不能省。