ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

相机标定与畸变矫正:从原理到工程实践的关键步骤

相机标定与畸变矫正:从原理到工程实践的关键步骤

1. 标定到底在解决什么问题,为什么不能跳过

相机标定与矫正,几乎是我见过的视觉项目里最容易被"临时抱佛脚"、又最容易埋雷的环节。很多同学一上来就急着跑算法、做检测,结果到了需要测量尺寸、恢复三维结构、做多目对齐的时候,发现画面里的物体形状是扭曲的,两条本该平行的线怎么都对不齐,这才想起来要找标定的资料。说实话,这个顺序基本是反的。

先想清楚一件事:相机本质上是把三维世界投射到二维平面的设备,但它不是理想的小孔成像,镜头的光学结构、装配误差、传感器平面的微小倾斜,都会让图像产生各种畸变。其实你可以把相机理解成一块哈哈镜——中心区域还算正常,越靠近边缘,直线会变弯、距离会被拉伸或压缩。标定要做的,就是给这块哈哈镜建立一张"矫正表",记录下它在每个位置把光线掰弯了多少,然后反着算回去,把图像还原成符合真实几何关系的画面。

我在某视觉检测项目里就吃过亏。当时一个负责测量零件孔径的Demo,在图像中心附近测量的结果还算准,一旦零件落在视野边缘,孔径测量值直接偏了将近两个百分点。后来排查了半天,才发现根本不是算法的问题,是压根没做畸变标定。重新标定并矫正之后,边缘区域的测量误差立刻回到了0.3%以内。那次之后我彻底明白了一个道理:标定不是"锦上添花",而是视觉测量系统里最基础的地基。

这篇文章面向的读者,我觉得有两类:一类是做视觉算法、机器视觉集成、三维重建、SLAM方向的同学,需要系统地把标定这件事搞清楚;另一类是已经在用OpenCV之类工具做开发,但只停留在"调个函数、出几个系数"阶段的工程师。我会尽量把原理、步骤、参数选择、坑点都串起来讲,保证看完之后你能独立完成一次完整且高质量的标定与矫正,而不是只跑通一个Demo。

2. 标定方案选型:棋盘格、圆点阵列还是ChArUco

标定的第一步其实是选靶标,也就是标定板。不同标定板的原理和应用场景差异很大,选错了后面会特别别扭。

2.1 三种主流标定板的适用场景

最常见的是棋盘格标定板。它的优点是角点检测算法极其成熟,OpenCV里的findChessboardCorners对棋盘格的鲁棒性很高,而且角点位置是两条黑白块的交叉点,亚像素精度好提,周围也没有歧义特征。缺点是棋盘格必须完整出现在画面里才能检测到全部角点,如果你用的镜头视场角比较小,或者标定板尺寸没买对,稍微靠近一点就出界了,采集效率会低不少。

圆点阵列标定板在某些场景下更实用。圆点的圆心检测不受透视投影影响,在倾斜视角下依然能保持较高的定位精度,而且Halcon等商业库对圆点标定板支持得很好。我自己用下来的感觉是,圆点阵列在大倾角、大畸变场景下鲁棒性更好,但如果图片模糊、光照不均匀,圆点分割容易出现粘连,反而比棋盘格更脆弱。

ChArUco板算是棋盘格和ArUco码的结合体。它在棋盘格每个格子里嵌入了独立的编码标记,优势是即使只有部分板子在画面里,也能通过编码识别出当前角点的全局编号,这对大标定板、近距离采集、以及需要标定大视场相机的场景非常友好。我在标定大靶面工业相机时,用ChArUco板明显比普通棋盘格省心,因为不用费劲保证整块板都在视场内,只要有足够多的有效角点就行。

2.2 标定板材质与尺寸怎么选

标定板要平整,这一点怎么强调都不过分。A4纸打印的棋盘格,贴在桌面或硬纸板上,短期做个算法验证问题不大,但你要是用在精密测量项目里,纸的轻微褶皱、热胀冷缩、受潮变形都会直接影响标定精度。我建议量产或精度要求高的项目直接用玻璃基底或铝基底的陶瓷标定板,热膨胀系数小,表面平整度能控制在微米级别。

尺寸选择有个简单逻辑:标定板上的方格边长,决定了你在工作距离上能否稳定提取角点。一般原则是让每个方格在图像里至少占15到20个像素,否则角点定位的亚像素精度会明显下降。具体做法是:先估算你正常工作距离下相机能覆盖的视野宽度,再除以10到20,作为单个方格的期望边长。比如视野宽度是400mm,你想让画面里有10到15个格子,那方格边长25mm左右会比较合适。

2.3 我推荐的标定板参数组合

这里直接给一个经过多次验证的参考配置:

  • 棋盘格推荐12x9或14x10的规格(内部角点数分别是11x8和13x9),保证画面中能同时出现足够多的特征点;
  • 方格边长根据实际工作距离而定,典型值在20mm到50mm之间;
  • 如果预算允许,选氧化铝陶瓷基板,或者表面覆有防反光涂层的玻璃板。

注意:使用棋盘格时,findChessboardCorners检测的是"内部角点",比如12x9格子的棋盘,检测到的角点数量是11x8。采集和分析时不要搞混这个数字,否则角点不会匹配。

3. 采集数据与运行标定的完整过程

标定板选好了,接下来就是数据采集,这一步的成败直接决定标定结果的准确性。很多人把标定想得太简单,以为拿着标定板随便拍几张就能出结果,实际上采集数据的质量远比算法本身更影响最终精度。

3.1 采集标定图像的关键要求

图像的"多样性"是核心。你要让标定板在画面里以不同姿态、不同位置、不同旋转角度出现,而不是只在正对着相机的地方拍几张。我一般是这样控制的:

  • 数量上,普通广角镜头和标准镜头至少拍15到20张,鱼眼或大畸变镜头建议30张以上;
  • 标定板平面与成像平面之间的夹角要有变化,从0度(正对)到大约30到45度之间都要覆盖,但不要太极端,夹角太大时角点提取容易失败;
  • 标定板要出现在画面的各个区域:左上、右上、中心、左下、右下,不要只集中在中间,否则边缘区域的畸变模型拟合不出来;
  • 距离也要有远近变化,模拟实际工作距离范围,这能让内参的估计更稳定;
  • 拍摄时确保标定板清晰,不要有运动模糊,这一点很多人容易忽视,模糊的角点坐标本身就是带误差的,标定出来的内参也一定不准。

另外有一点要特别提醒:标定时相机的光圈、焦距和对焦位置必须固定,一旦标定完再动光圈或变焦,内参就变了,前面所有工作全部作废。工业相机一般手动锁定光圈和焦距,但如果用了自动光圈镜头,必须切成手动模式再采集。

3.2 一张图解释内参、外参和畸变系数

标定最终输出的是三样东西:内参矩阵、畸变系数、以及每张标定图对应的外参。

内参矩阵是3x3矩阵,核心参数是fx、fy(分别代表x、y方向上的等效焦距,单位是像素)和cx、cy(主点坐标)。你可以把fx理解为"焦距除以单个像素的物理尺寸",它决定了物体在图像里放大多少倍。cx和cy是光轴与成像平面的交点,理想情况下在图像正中心,实际因为装配误差会有偏移。

畸变系数主要分两类。径向畸变是镜头光学结构造成的,表现为直线在图像边缘变成向内弯(桶形畸变)或向外弯(枕形畸变),通常用k1、k2、k3三个系数描述。切向畸变则是镜头和传感器装配不平行导致的,用p1、p2两个系数描述。OpenCV默认用的是包含k1、k2、p1、p2、k3的五参数模型。

外参描述的是每张标定图拍摄时刻,标定板坐标系与相机坐标系的相对位姿,也就是旋转向量和平移向量。单目标定里外参本身一般不是最终目的,但它是求解内参的中间产物,后续如果做手眼标定、多相机标定,外参就是必须品。

3.3 用代码跑通一次完整的标定流程

这一步我用OpenCV和Python来演示,这也是工程里最常用的一套组合。

import cv2 import numpy as np import glob # 棋盘格内部角点数 chessboard_size = (11, 8) square_size_mm = 25.0 # 准备角点的物理坐标,假设标定板放在Z=0平面上 pattern_points = np.zeros((chessboard_size[0] * chessboard_size[1], 3), np.float32) pattern_points[:, :2] = np.mgrid[0:chessboard_size[0], 0:chessboard_size[1]].T.reshape(-1, 2) pattern_points *= square_size_mm object_points = [] # 三维物理坐标 image_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: # 亚像素精化角点坐标 criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 30, 0.001) corners2 = cv2.cornerSubPix(gray, corners, (5, 5), (-1, -1), criteria) object_points.append(pattern_points) image_points.append(corners2) cv2.drawChessboardCorners(img, chessboard_size, corners2, ret) cv2.imshow('detect', img) cv2.waitKey(100) cv2.destroyAllWindows() # 标定主函数 ret, mtx, dist, rvecs, tvecs = cv2.calibrateCamera( object_points, image_points, gray.shape[::-1], None, None ) print("内参矩阵:\n", mtx) print("畸变系数:\n", dist) print("重投影误差:", ret) # 保存标定结果 np.savez('camera_calib.npz', mtx=mtx, dist=dist, rvecs=rvecs, tvecs=tvecs)

calibrateCamera内部实际上是在做一件什么事呢?它在同时优化内参、畸变系数和每一张图的外参,优化目标是让所有已知角点的三维坐标重投影到图像平面后,与检测到的二维角点坐标尽可能重合,这个重合误差就是重投影误差,也就是calibrateCamera返回的那个ret值。

角点检测失败时,findChessboardCorners会返回False,这种情况下要检查棋盘格是否完全在画面内、光照是否均匀、图像是否模糊。还有一个经验:棋盘格不要用太密的规格,比如15x15以上的,否则角点数量太多,检测速度慢且容易误检。

提示:calibrateCamera的重投影误差单位是像素,经验值一般在0.05到0.3像素之间属于优秀水平,0.3到0.6像素属于可用水平,超过0.6像素就需要排查采集数据或检测流程的问题了。

3.4 标定结果的判断标志

拿到内参和畸变系数之后,不要急着去矫正图像。先做个快速自检:打印重投影误差,看所有标定图的误差分布。如果某张图的误差明显高于其他图,说明这张图里角点检测不准或者标定板姿态太极端,把这张图剔除以后重新标定,往往误差就能明显降下来。

还有一个更直观的验证方法:用标定的畸变系数对棋盘格图像做畸变矫正,然后观察画面边缘的直线是否恢复平直。棋盘格天然提供了丰富的直线边缘,矫正后格子边缘应该是笔直的,如果还有明显扭曲,说明畸变模型没拟合好。

4. 从标定到矫正:畸变模型、映射方式与工程实现

标定只是拿到了"哈哈镜的矫正表",真正要出效果还必须把矫正表用起来。这里有两个层面的工作:一是选对畸变模型,二是选对矫正的实现方式。

4.1 普通镜头与鱼眼镜头的畸变模型差异

OpenCV里的calibrateCamera默认用的是普通镜头的畸变模型,适合透视相机、工业定焦镜头和绝大多数手机摄像头。但如果你的镜头是鱼眼镜头,视场角超过120度甚至到180度,畸变极其剧烈,普通多项式模型就拟合不过来了。

鱼眼镜头要专门用cv2.fisheye模块,它的畸变模型和普通模型不一样,采用的是等距投影模型,畸变系数也只有四个(k1、k2、k3、k4)。在我实际接触过的项目中,用普通模型去标鱼眼镜头,矫正后的边缘区域依然会严重扭曲,换用fisheye模块后效果立刻正常。所以第一步一定是确认镜头类型,再选匹配的标定接口。

4.2 矫正实现方式:一次映射还是两次映射

OpenCV里矫正图像有两种主流做法:

  • cv2.undistort(img, mtx, dist):一步到位,内部自动完成映射计算和重采样;
  • cv2.initUndistortRectifyMap+cv2.remap:先生成映射表,再对图像做重采样。

从效果上看两者差别不大,但性能上差距很大。在实时视频处理场景,每一帧都调用undistort会重复计算映射表,CPU开销明显偏高;而initUndistortRectifyMap只需要在初始化时计算一次映射表,之后每帧执行remap即可,效率高很多。工程上我基本只推荐第二种方案。

import cv2 import numpy as np # 加载标定结果 data = np.load('camera_calib.npz') mtx = data['mtx'] dist = data['dist'] # 生成映射表 h, w = 1080, 1920 new_mtx, roi = cv2.getOptimalNewCameraMatrix(mtx, dist, (w, h), alpha=0, (w, h)) mapx, mapy = cv2.initUndistortRectifyMap( mtx, dist, None, new_mtx, (w, h), cv2.CV_32FC1 ) # 对每帧执行映射 frame = cv2.imread('frame.jpg') undistorted = cv2.remap(frame, mapx, mapy, cv2.INTER_LINEAR)

getOptimalNewCameraMatrix里的alpha参数是有讲究的。alpha=0表示矫正后保留所有有效像素,但边缘会出现黑边,需要裁剪;alpha=1表示所有原图像素都保留,边缘不会裁切,但图像会被拉伸放大,分辨率利用率下降。我一般是设alpha=0,然后根据返回的roi做一次裁剪,把黑色区域去掉。裁剪时注意保留中心区域,不要因为裁剪过度而丢失原本需要观察的视场边角。

4.3 重映射的插值选择

remap的插值方式在大多数场景用INTER_LINEAR就够用了,速度和质量的平衡最好。如果矫正后图像要用于高精度测量,并且图像本身质量不错,可以尝试INTER_CUBIC,边缘细节保留更好,但速度会慢一些。INTER_NEAREST一般只用于快速预览,矫正后的几何精度不受插值方式影响(位置坐标是浮点数,插值只影响灰度/颜色取值),所以具体选择就看你对画质和帧率的要求。

4.4 矫正 ROI 裁剪的取舍

矫正后的图像因为畸变校正会丢失边缘信息,这是物理必然。实际项目里,如果你关心的目标基本位于画面中心区域,把边缘黑边裁掉问题不大;但要是在边缘区域也有检测需求,就不要一刀切地裁掉太多,保留部分边缘的轻微拉伸也比丢掉目标强。

我在一个工业检测项目里做过对比。当时为了保证视野覆盖率,矫正后没有按roi裁剪,而是直接保留了整幅图像,结果边缘区域的目标反而因为矫正拉伸而出现了轻微的形变,测量精度反而不如不矫正。这个经验提醒我:矫正在很多场景是必要手段,但不一定非要对整幅图做矫正不可——如果你只关心某个固定区域,可以直接用标定结果对该区域建立映射,而不用对全局做处理,计算量也小很多。

5. 标定误差来源与常见问题排查实录

这部分是真正的经验沉淀。下面这些坑,我基本都踩过,有些甚至踩过不止一遍。

5.1 标定误差偏大,应该从哪些方向排查

先给一张排查速查表,你照着顺序检查就行:

检查项优先级可能的问题处理方式
图像清晰度高运动模糊、对焦不准重新采集,使用小光圈增加景深,用支架固定相机
标定板平整度高纸板褶皱、贴膜气泡更换玻璃/陶瓷标定板,确保板面绝对平整
采集数量与姿态覆盖中姿态单一、集中在中心区域补拍不同角度、不同位置、不同距离的图像
角点检测精度中光照不均、反光、过曝调整光源方向,使用漫射光,避免高光反射
镜头类型选配低普通模型用于鱼眼镜头换用fisheye模块重新标定
标定板规格低内外角点数搞混核对findChessboardCorners输入参数

重投影误差大,高概率是图像质量和标定板平整度的问题,算法层面能优化的空间反而不大。有一次我怎么标都标不出低误差,最后发现是标定板贴在亚克力板上,亚克力有轻微弯曲,换了一块真正的陶瓷板之后,误差立刻从0.8像素降到0.15像素。

5.2 角点检测失败的几种典型场景

角点检测失败常见的有这么几种:

  • 棋盘格在画面中太小,角点之间像素距离不到10个像素,检测算法很难分辨;
  • 画面有强烈的局部高光或阴影,黑白格子的对比度被破坏;
  • 标定板姿态倾斜太厉害,格子产生严重的透视变形;
  • 分辩率太低或者图像被压缩过,边缘锯齿严重。

遇到这类问题,优先从采集端解决,不要死磕检测参数。调整光照、减小倾斜角、让标定板在画面中占比更大,通常比调findChessboardCorners的参数更有效。另外,拍摄时尽量让标定板占据画面的四分之一到三分之一以上,既保证角点清晰,又不会因为过大导致部分区域出画面。

5.3 矫正后图像边缘拉伸太厉害

矫正后边缘出现严重拉伸,通常是因为畸变系数太大,或者标定的畸变模型拟合过度了。一个常见错误是标定时把一些误检的角点也算进去了,导致畸变系数被异常大值拉偏。检查方式是打印畸变系数,如果k1的绝对值超过1左右,就要怀疑标定数据有问题了。

还有一种是使用alpha=1时画面被过度放大,边缘被拉伸,这属于参数选择问题。一般来说alpha=0更接近真实矫正效果,如果实在需要保留全视场信息,可以考虑把原图先缩小一点再矫正,这比强制在alpha=1下保留边缘更合理。

5.4 内参标定结果在不同分辨率下的复用问题

这里必须明确一点:内参矩阵的fx、fy和cx、cy都依赖图像分辨率。同一台相机,在1920x1080下标定出来的fx是2000像素,在1280x720下就会变成约1333像素,畸变系数是按归一化后的坐标表示的,理论上可以在不同分辨率下复用,但为了保险起见,分辨率变了最好重新标定,成本很低,别省这个事。

我在实际导出标定结果时,一般会顺带保存标定时的图像分辨率,方便后续检查。如果项目中途摄像头换了分辨率,我宁可直接把整条标定流程重跑一遍,也不去纠结能不能复用旧参数。这是规避很多隐性问题的笨办法,也是最可靠的办法。

5.5 标定结果在工程部署时的附加注意事项

标定不是在实验室里跑一次就完事的操作,工程上还有几个必须注意的坑:

  • 镜头在运输、安装过程中可能松动,安装到现场后务必要重新标定一次;
  • 使用自动对焦或变焦镜头时,标定参数只在当前焦距下有效,工业场景建议直接用定焦镜头并锁定光圈;
  • 相机会发热,长时间工作后热胀冷缩可能导致内参漂移,对亚像素精度测量有要求的项目,建议等相机预热半小时再标定;
  • 不要用在其他电脑上拷贝的标定参数直接部署到新机器上,除非你能确认镜头、分辨率、相机型号完全一致。

6. 我个人积累的几个标定小技巧

最后分享几个实际项目中摸索出来的技巧,篇幅不长,但都是能直接提升效率的点。

第一个技巧是采集标定图像时,把相机放在三脚架或固定支架上,移动标定板而不是移动相机。移动相机容易引入振动模糊,移动标定板则稳定得多。如果是手持相机采集,尽量用快门优先模式,保证快门速度足够快。

第二个技巧是采集时尽量让标定板在画面中出现不同的旋转角度。很多人习惯只做平移和倾斜,很少旋转标定板,但旋转角度越大,对畸变系数的约束越充分,尤其是切向畸变的拟合效果会好很多。我一般会在每个位置采集时顺时针和逆时针各旋转45度左右各拍一张。

第三个技巧是角点检测时启用了cv2.CALIB_CB_FAST_CHECK要注意,这个标志用于快速筛选棋盘格,能节省检测时间,但偶尔会把有效的棋盘格误判为无效。第一次标定时建议不启用它,确保每张图都尽量参与标定;如果后续每次都要实时检测棋盘格,再启用这个标志提高速度。

第四个技巧和标定板的清洁有关。玻璃标定板表面如果有指纹、灰尘或油渍,在光源照射下会产生细小的局部高光,角点检测精度会受影响。采集前用镜头纸擦拭标定板表面,这个动作虽然不起眼,但在高精度标定中影响非常显著。

关于相机标定这件事,我个人的体会是:它并不难,但特别考验耐心和细致程度。每一个步骤都有严格的物理依据,图省事的代价就是后续所有依赖相机模型的算法全都不准。按照本文的方法完整走一遍流程,你的标定结果应该能达到一个很稳的水平。如果后面要接着做双目标定、手眼标定或者三维重建,相信你会回来发现,当初认认真真把这步做好,是特别值得的一件事。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进