news 2026/10/22 6:48:07

相机标定与图像校准:从原理到双目测距实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
相机标定与图像校准:从原理到双目测距实战

1. 相机标定与图像校准:从“为什么画面是歪的”说起

做过计算机视觉项目的人,大概率都经历过这样的场景:摄像头架好了,代码跑通了,但测出来的距离总是差那么几厘米,或者拼接出来的全景图怎么都对不齐。你反复检查算法逻辑,最后发现问题根本不在代码,而在那个被你忽略的环节——相机标定与图像校准。

这两个词听起来像是实验室里的高深术语,但实际上,它们是一切视觉测量、三维重建、图像拼接、机器人导航的“地基”。简单来说,相机标定就是搞清楚你的相机到底“怎么看世界”——它的焦距是多少、光心偏在哪里、镜头有没有畸变;而图像校准则是根据标定结果,把相机拍到的“歪曲”画面修正成符合数学模型的“标准”画面。没有这一步,后面所有的测量和识别都是在沙地上盖楼。

这篇文章适合谁看?如果你正在做双目测距、结构光扫描、SLAM、全景拼接,或者只是想让自己的USB摄像头测出来的尺寸更准一点,那这篇内容就是为你准备的。我会从底层原理讲起,把标定板的选型、拍摄技巧、参数计算、代码实现、踩坑经验全部拆开揉碎,让你看完就能上手操作。全文基于OpenCV和Python生态展开,但思路适用于任何视觉库。

2. 核心原理拆解:相机模型到底在描述什么

2.1 针孔模型:用一个小孔解释整个成像过程

相机标定的理论基础是针孔成像模型。你可以把相机想象成一个暗盒,前面开了一个极小的孔,光线穿过小孔后在后面的感光元件上形成倒立的像。这个模型虽然简单,但它抓住了成像的核心几何关系:三维空间中的点、相机光心、图像平面上的点,三者共线。

在数学上,这个关系用一个3×4的矩阵描述,也就是投影矩阵。它由两部分组成:内参矩阵和外参矩阵。内参矩阵描述相机自身的属性——焦距、光心位置、像素纵横比;外参矩阵描述相机在世界坐标系中的位置和朝向。标定的过程,就是通过已知的世界坐标点和对应的图像坐标点,反推出这些参数。

这里有个关键点:内参是相机固有的,标定一次可以长期使用(除非你动了焦距或摔了相机);外参是每次改变相机位置后都需要重新计算的。很多人把两者混为一谈,导致标定结果无法复用,这是新手常犯的错误。

2.2 畸变模型:为什么直线拍出来是弯的

理想针孔模型假设光线沿直线传播,但真实镜头是透镜组合,边缘光线会发生弯曲,导致图像产生畸变。畸变主要分两类:径向畸变和切向畸变。

径向畸变让图像呈现“桶形”或“枕形”变形,离图像中心越远越明显。你可以拿手机拍一张方格纸,如果边缘的格子线向外鼓,那就是桶形畸变;向内凹则是枕形畸变。切向畸变则是由于镜头与感光元件不平行导致的,表现为图像在某一个方向上被“剪切”。

OpenCV的标定模型用五个参数来描述畸变:k1、k2、k3用于径向畸变,p1、p2用于切向畸变。对于大多数普通镜头,用k1、k2、p1、p2四个参数就够了;如果是广角或鱼眼镜头,k3甚至更高阶项才够用。这里有个经验:不要盲目追求高阶畸变模型,参数越多对标定数据的要求越高,如果标定板图像数量不足,高阶项反而会引入不稳定。

2.3 标定到底在解什么方程

从数学角度看,标定是一个非线性优化问题。我们已知的是标定板上角点的世界坐标(因为标定板制作精度很高,可以认为世界坐标是准确的),以及这些角点在图像中的像素坐标。未知的是内参矩阵、畸变系数、以及每张图像对应的外参。

OpenCV采用的策略是:先用张正友标定法求一个线性近似解,再通过Levenberg-Marquardt算法迭代优化,最小化重投影误差。重投影误差是指:用当前估计的参数,把世界坐标点投影到图像上,得到的像素位置与实际检测到的像素位置之间的欧氏距离。这个误差越小,说明标定参数越准确。

注意:重投影误差不是越小越好。如果误差小到离谱(比如0.01像素),很可能是过拟合了,实际使用时反而偏差大。一般来说,平均重投影误差在0.1到0.5像素之间是比较健康的范围。

3. 标定实操全流程:从打印标定板到拿到参数

3.1 标定板的选择与制作

标定板是标定的基准,它的精度直接决定标定结果的上限。常见的有棋盘格和圆点阵列两种。棋盘格的角点检测精度高,OpenCV支持成熟,是大多数场景的首选;圆点阵列对模糊和噪声更鲁棒,适合工业环境,但需要额外的椭圆中心检测算法。

棋盘格的规格选择有讲究。格子数量太少,约束不足,标定结果不稳定;格子太多,图像边缘的格子容易拍不全或畸变过大。我的经验是:内角点数量在6×9到8×11之间比较合适。比如一个8×11的棋盘格,表示有8行11列内角点,实际打印出来大概是A4纸大小。

制作标定板时,平整度比打印精度更重要。我试过用普通A4纸打印后贴在硬纸板上,结果纸面轻微起伏导致重投影误差忽大忽小。后来改用亚克力板或铝板粘贴,误差立刻稳定下来。如果你只是做精度要求不高的项目,用平板电脑显示棋盘格也可以,但要注意屏幕反光和刷新率问题。

提示:标定板打印后,用游标卡尺实测几个格子的边长,取平均值作为square_size参数。不要直接相信打印机设置的尺寸,缩放误差可能达到1%以上。

3.2 拍摄环节的“三三制”原则

拍摄标定图像是整个过程里最容易被轻视、却最影响结果的环节。我总结了一个“三三制”原则:三个距离、三个角度、三个位置。

三个距离是指:近、中、远三种物距各拍若干张。近距离图像能约束畸变参数,远距离图像能约束焦距。三个角度是指:标定板分别绕X轴、Y轴、Z轴旋转一定角度,一般建议每个轴至少±30度。三个位置是指:标定板出现在图像的左、中、右(或上、中、下)区域,确保覆盖整个视场。

具体拍多少张?理论上3张就能解出参数,但实际中我建议至少15到20张。数量太少,参数不稳定;数量太多,检测失败的图像会拖累整体。拍摄时要注意:标定板在图像中要完整可见,不要被裁切;光照要均匀,避免反光和阴影;标定板不要运动模糊,快门速度要够快。

注意:很多人喜欢把标定板举在相机正前方拍一堆,这样拍出来的图像角度单一,畸变参数根本解不出来。一定要让标定板“动起来”,倾斜、旋转、远近变化都要有。

3.3 角点检测与亚像素优化

拍完图像后,下一步是用OpenCV的findChessboardCorners函数检测角点。这个函数返回的是粗略的像素坐标,精度在整数像素级别。为了达到亚像素精度,需要调用cornerSubPix进行迭代优化。

亚像素优化的原理是:在角点附近的小窗口内,利用图像灰度梯度拟合一个曲面,找到曲面极值点作为角点位置。窗口大小一般设为11×11或15×15,太大容易受邻近角点干扰,太小则对噪声敏感。迭代终止条件通常设为最大30次迭代或精度达到0.001像素。

这里有个实操细节:在调用cornerSubPix之前,最好先对图像做一次高斯模糊,可以显著提高角点检测的稳定性。但模糊核不要太大,3×3或5×5就够了,否则会抹掉角点细节。

import cv2 import numpy as np # 棋盘格内角点数量 pattern_size = (8, 11) # 世界坐标中的角点位置 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) objpoints = [] # 三维点 imgpoints = [] # 二维点 criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 30, 0.001) for fname in image_files: img = cv2.imread(fname) gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) ret, corners = cv2.findChessboardCorners(gray, pattern_size, None) if ret: corners_refined = cv2.cornerSubPix(gray, corners, (11, 11), (-1, -1), criteria) objpoints.append(objp) imgpoints.append(corners_refined)

3.4 参数求解与结果评估

有了世界坐标和图像坐标的对应点对,就可以调用calibrateCamera求解参数了。这个函数返回内参矩阵、畸变系数、旋转向量、平移向量,以及平均重投影误差。

评估标定质量不能只看重投影误差。我通常会做三件事:第一,看误差分布,如果某些图像的误差特别大,说明那张图的角点检测有问题,应该剔除后重新标定;第二,看内参的合理性,焦距应该在像素量级(比如500到2000之间),光心应该在图像中心附近;第三,做实际验证,用标定参数去校正一张新拍的图像,看直线是否变直。

ret, mtx, dist, rvecs, tvecs = cv2.calibrateCamera( objpoints, imgpoints, gray.shape[::-1], None, None ) print(f"重投影误差: {ret:.4f}") print(f"内参矩阵:\n{mtx}") print(f"畸变系数: {dist}")

4. 图像校准:把“歪”的画面掰直

4.1 去畸变的核心逻辑

图像校准的本质是像素重映射。对于输出图像中的每一个像素位置,根据畸变模型反算出它在原始图像中的对应位置,然后通过插值获取像素值。这个过程叫去畸变。

OpenCV提供了undistort函数,一行代码就能完成去畸变。但它有个缺点:每次都要重新计算映射表,速度慢。更高效的做法是用initUndistortRectifyMap预先计算映射表,然后用remap函数应用映射。映射表只与相机参数和图像尺寸有关,可以保存下来重复使用。

# 预先计算映射表 mapx, mapy = cv2.initUndistortRectifyMap( mtx, dist, None, mtx, (w, h), cv2.CV_32FC1 ) # 应用映射 undistorted = cv2.remap(img, mapx, mapy, cv2.INTER_LINEAR)

4.2 插值方法的选择

重映射时,计算出的原始坐标通常不是整数,需要插值。OpenCV支持最近邻、双线性、双三次等插值方法。双线性插值是速度和质量的平衡点,适合大多数场景;双三次插值质量更好但更慢,适合对画质要求高的场合;最近邻插值速度最快但会产生锯齿,一般不用。

这里有个容易被忽略的点:去畸变后,图像边缘会出现“黑边”,因为原始图像中那些区域没有对应的像素。如果你做图像拼接,这些黑边会干扰特征匹配。解决办法是裁剪或者设置边界填充模式。我通常会在去畸变后裁剪掉边缘5%到10%的区域,虽然损失了一点视场,但后续处理更干净。

4.3 立体校正:让两个相机“对齐”

如果你做的是双目视觉,图像校准还多了一步:立体校正。双目相机的两个镜头不可能完全平行,导致左右图像中的对应点不在同一行上,这会大大增加立体匹配的难度。立体校正就是通过旋转两个相机的虚拟姿态,让它们的图像平面共面且行对齐。

OpenCV的stereoRectify函数可以计算校正变换矩阵,然后对左右图像分别做重映射。校正后,你可以在左图中选一个点,在右图的同一行附近就能找到对应点,这就是所谓的“极线约束”。这一步对双目测距的精度和速度都至关重要。

提示:立体校正前,需要先分别标定左右相机,再通过stereoCalibrate计算两个相机之间的旋转和平移关系。双目标定的拍摄要求更高,标定板必须同时出现在左右图像中,且要覆盖整个视场的各个区域。

5. 常见问题与排查技巧实录

5.1 重投影误差过大怎么办

重投影误差大是最常见的问题,原因通常有三类:标定板不平整、角点检测不准、图像数量或角度不足。

排查顺序应该是:先检查标定板是否平整,用手按压四个角看是否有翘曲;再检查角点检测结果,把检测到的角点画在图像上看是否准确;最后检查图像分布,如果所有图像都是正面拍摄,那畸变参数肯定解不准。我遇到过一次误差高达2像素的情况,最后发现是标定板打印后受潮变形了,换了一块亚克力板立刻降到0.3像素。

5.2 去畸变后图像反而更“怪”了

有时候去畸变后的图像看起来比原图还奇怪,直线确实变直了,但整体形状变得很夸张。这通常是因为畸变系数过大,可能是标定数据不足导致过拟合。解决办法是:减少畸变参数数量,比如只用k1和k2,把k3设为0;或者增加标定图像数量,让优化更稳定。

另一个可能的原因是内参矩阵中的焦距估计错误。如果焦距估计得太大,去畸变后图像会被过度放大;焦距太小则会被压缩。检查方法是用标定参数计算一下视场角,看是否与相机规格书相符。

5.3 标定参数能不能跨分辨率使用

很多人会问:我在1080p下标的参数,能不能用在720p的图像上?答案是不能直接使用,但可以换算。内参矩阵中的焦距和光心都是以像素为单位的,分辨率改变后,这些值需要按比例缩放。具体来说,如果分辨率从1920×1080降到1280×720,缩放因子是1280/1920=0.667,那么fx、fy、cx、cy都要乘以这个因子。畸变系数是无量纲的,可以保持不变。

但要注意,这种换算假设相机的视场角没有变化,只是像素密度变了。如果相机在改变分辨率时做了裁剪或缩放,那换算就不准了,最好重新标定。

5.4 常见问题速查表

问题现象可能原因排查方法解决措施
重投影误差大于1像素标定板不平整按压标定板检查翘曲更换硬质基板
去畸变后边缘严重拉伸畸变系数过大检查k3是否被过度拟合减少畸变参数数量
角点检测失败光照不均或模糊查看灰度图和检测结果改善光照,提高快门速度
双目校正后行不对齐双目标定不准检查极线误差重新拍摄双目标定图像
标定结果无法复现相机焦距被改变检查镜头是否松动固定镜头后重新标定

6. 进阶技巧与实战经验

6.1 用多相机标定提升整体精度

如果你有多个相机需要协同工作,比如全景拼接或环绕视图,单独标定每个相机再拼接往往会有累积误差。更好的做法是多相机联合标定:让一个标定板同时出现在多个相机的视场中,把所有相机的参数放在一个优化问题里一起求解。这样可以利用相机之间的约束关系,显著提升整体一致性。

实现上,可以用calibrateCamera分别标定每个相机的内参,然后用stereoCalibrate两两标定外参,最后用光束法平差(Bundle Adjustment)做全局优化。OpenCV没有直接提供多相机联合标定的函数,但可以用calib3d模块的组合来实现。

6.2 在线标定与自标定

传统标定需要专门的标定板,但在某些场景下(比如自动驾驶、无人机),你无法停下来摆标定板。这时候就需要在线标定或自标定技术。自标定不需要已知的标定物,而是利用图像中的特征点(比如道路标线、建筑物边缘)之间的几何约束来估计相机参数。

自标定的精度通常不如传统标定,但在相机参数发生缓慢变化时(比如温度漂移),可以用自标定做在线修正。我试过用车道线的平行和垂直约束来修正焦距,在温度变化10度的情况下,能把测距误差控制在2%以内。

6.3 标定结果的保存与版本管理

标定参数是项目的核心资产,一定要做好保存和版本管理。我习惯把内参、畸变系数、图像尺寸、标定日期、重投影误差写成一个YAML或JSON文件,和代码一起提交到版本控制系统。每次更换相机或调整镜头后,都要重新标定并更新文件,同时在注释里写明变更原因。

提示:不要直接把标定参数硬编码在代码里。我见过一个项目,标定参数写死在源码中,后来换了相机,代码里改了但测试环境没更新,导致线上测距全部偏差。用配置文件管理,改起来方便,也不容易出错。

6.4 标定精度的极限在哪里

标定精度受限于多个因素:标定板的制作精度、角点检测的亚像素精度、图像的数量和分布、优化算法的收敛性。在理想条件下,普通工业相机的标定重投影误差可以做到0.1像素以下,测距精度可以达到物距的千分之一左右。

但实际中,温度变化是影响标定精度的隐形杀手。镜头和相机机身的热胀冷缩会导致焦距和光心发生微小变化,虽然每度变化只有几个微米,但在高精度测量中不可忽略。如果项目对精度要求极高,建议在恒温环境下标定和使用,或者定期做在线修正。

7. 从标定到应用:一个完整的双目测距案例

7.1 系统搭建与数据采集

假设我们要搭建一个双目测距系统,基线距离10厘米,用于测量0.5米到3米范围内的物体。相机选用两个同型号的全局快门相机,镜头焦距6毫米,分辨率1280×720。

标定板选用8×11的棋盘格,方格边长25毫米,打印在A4纸上后粘贴在铝板上。采集数据时,让标定板在双目视场中移动,覆盖近中远三个距离,每个距离拍5到8张,总共约20组图像。注意标定板要同时完整出现在左右图像中,且角度要有变化。

7.2 双目标定与校正

先用calibrateCamera分别标定左右相机的内参和畸变系数,然后用stereoCalibrate计算两个相机之间的旋转矩阵R和平移向量T。最后用stereoRectify计算校正变换,并用initUndistortRectifyMap生成映射表。

ret, mtx1, dist1, mtx2, dist2, R, T, E, F = cv2.stereoCalibrate( objpoints, imgpoints1, imgpoints2, mtx1, dist1, mtx2, dist2, image_size, flags=cv2.CALIB_FIX_INTRINSIC ) R1, R2, P1, P2, Q, _, _ = cv2.stereoRectify( mtx1, dist1, mtx2, dist2, image_size, R, T, alpha=0 ) map1x, map1y = cv2.initUndistortRectifyMap( mtx1, dist1, R1, P1, image_size, cv2.CV_32FC1 ) map2x, map2y = cv2.initUndistortRectifyMap( mtx2, dist2, R2, P2, image_size, cv2.CV_32FC1 )

7.3 视差计算与深度恢复

校正后,左右图像的行已经对齐。用立体匹配算法(如SGBM)计算视差图,然后通过公式深度 = 焦距 × 基线 / 视差恢复三维坐标。这里的焦距和基线都来自标定结果,所以标定精度直接决定测距精度。

实测下来,在1米距离处,标定良好的双目系统测距误差可以控制在5毫米以内。如果误差偏大,优先检查标定质量,而不是怀疑匹配算法。我踩过的坑是:标定板拍摄时倾斜角度不够,导致外参估计不准,后来增加了大角度倾斜的图像,误差立刻降了一半。

7.4 实际应用中的注意事项

双目系统在实际使用中,震动是最大的敌人。相机一旦发生相对位移,标定参数就失效了。所以相机支架一定要刚性足够,最好用金属件而不是塑料件。如果无法避免震动,可以考虑加入在线自标定模块,定期修正外参。

另外,光照变化也会影响立体匹配的效果。在暗光环境下,图像噪声增大,视差图会出现大量空洞。解决办法是增加主动照明,或者使用对光照更鲁棒的匹配算法。我通常会在镜头旁加一个LED补光灯,成本不高但效果立竿见影。

8. 我个人的几条实操心得

标定这件事,说难不难,说简单也不简单。工具和算法都是现成的,但细节决定成败。我做了这么多项目,最大的体会是:标定板的质量比算法重要,拍摄的多样性比数量重要,验证的严谨性比参数本身重要。

很多人标定完看到重投影误差很小就以为万事大吉,结果实际使用时发现测距偏差很大。问题往往出在标定时的场景和实际使用场景不一致——标定时在近距离拍,使用时在远距离测;标定时光照均匀,使用时逆光严重。所以,标定时的条件要尽量模拟实际使用条件,这样才能保证参数在实际场景中有效。

还有一点:不要迷信“一次标定,终身使用”。相机摔过、镜头拧过、温度剧烈变化过,都应该重新标定。我习惯在项目里加一个标定检查流程,每次开机后拍一张标准参照物,如果测量值与预期偏差超过阈值,就提示需要重新标定。这个习惯帮我避免了好几次现场翻车。

最后分享一个小技巧:如果你手头没有合适的标定板,可以用一个平板电脑显示棋盘格图案,但要把屏幕亮度调到最高,并且关闭自动亮度调节。拍摄时注意避免屏幕反光,最好在柔光环境下进行。实测下来,这种方法在紧急情况下也能得到可用的标定结果,重投影误差可以控制在0.5像素以内。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/21 7:24:10

D435i 与 VINS-mono 标定实战:从内参到外参的完整避坑指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/21 17:51:10

机械臂智能坦克车从零搭建:设计思路、实操要点与避坑指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/21 16:04:47

生成函数实战指南:OGF/EGF到NTT的计数题全解

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/18 2:09:17

2S锂电池BMS均衡方案:硬件前端+MCU状态机实战解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/19 12:01:01

QEMU引导程序实战:从512字节汇编到操作系统启动链路

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/21 15:10:08

STM32+华为云IoT+Qt:从设备端到云端的智能家居实战全解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华