4、相机畸变模型:径向畸变(桶形/枕形)、切向畸变、畸变系数(k1,k2,k3,p1,p2)
聊完了相机成像的几何模型,咱们得面对一个现实问题——镜头不是完美的。
你想想看,一个理想的针孔相机,光线直来直去,成像干净利落。但真实镜头呢?它是一堆玻璃透镜的组合,光线穿过时会发生折射、偏移。结果就是——拍出来的图像,边缘的直线变弯了,或者物体看起来被拉伸了。
这就是畸变。做双目标定,如果不先把畸变干掉,后面的立体匹配、三维重建全是扯淡。我当年刚入行时,就吃过这个亏,以为标定完内参就完事了,结果重建出来的点云全是扭曲的……嗯,从那以后,我再也不敢跳过畸变校正这一步。
4.1 畸变从哪来?
畸变主要分两类:径向畸变和切向畸变。说白了,一个是镜头形状惹的祸,一个是装配工艺留下的坑。
核心观点:畸变是镜头物理特性的数学描述。标定的本质,就是找到一组系数,把畸变后的像素坐标“拉”回到理想位置。
4.2 径向畸变:桶形 vs 枕形
径向畸变,顾名思义,是沿着图像半径方向发生的变形。离图像中心越远,畸变越明显。
我习惯把它想象成一张照片被贴在一个球面上,然后再压平。中心区域还好,边缘就惨了。
- 桶形畸变:图像边缘向外“鼓”出来,像木桶的侧面。常见于广角镜头、鱼眼镜头。拍出来的直线,在画面边缘会向外弯曲。
- 枕形畸变:图像边缘向内“凹”进去,像枕头的形状。常见于长焦镜头。直线在边缘会向内弯曲。
为什么会这样?
因为镜头对不同角度的入射光线,放大率不一样。靠近光轴的区域,放大率稳定;越往边缘,光线偏折角度越大,放大率要么变大(枕形),要么变小(桶形)。
数学上,我们用多项式来建模这个变形:
x_corrected = x * (1 + k1*r² + k2*r⁴ + k3*r⁶)
y_corrected = y * (1 + k1*r² + k2*r⁴ + k3*r⁶)
其中,(x, y)是畸变后的像素坐标(归一化平面上的),(x_corrected, y_corrected)是校正后的坐标。r² = x² + y²,表示该点到图像中心的距离平方。
k1, k2, k3 就是径向畸变系数。
我的经验:对于普通工业镜头,一般只用k1和k2就够了。k3主要是给鱼眼镜头或者畸变特别大的镜头准备的。我在一个项目中用过一次k3,是因为那个镜头实在太廉价了,边缘畸变大到离谱。正常情况,你标定出来的k3如果特别大,先检查一下标定板有没有拍好。
4.3 切向畸变:镜头装歪了
切向畸变,也叫离心畸变。它的来源很直接——镜头的光学中心没有和成像传感器(CMOS/CCD)完全平行。
说白了,就是装配的时候,镜头拧歪了一点点,或者传感器贴片的时候偏了一点点。
这种畸变会让图像看起来像被“剪切”了一样。一个矩形物体,拍出来可能变成梯形,或者一边被拉伸一边被压缩。
数学表达如下:
x_corrected = x + [2*p1*x*y + p2*(r² + 2*x²)]
y_corrected = y + [p1*(r² + 2*y²) + 2*p2*x*y]
p1, p2 就是切向畸变系数。
注意:切向畸变的数值通常比径向畸变小一个数量级。如果你标定出来的p1、p2特别大(比如超过0.1),那大概率是镜头没装好,或者标定板本身就不平。我曾经遇到过一个案例,标定出来的p1高达0.3,折腾了半天,最后发现是镜头卡口松了……拧紧之后,p1直接掉到0.002。
4.4 畸变系数全家桶
在OpenCV等工具中,畸变系数通常用一个向量来表示:
dist_coeffs = [k1, k2, p1, p2, k3]
一共5个参数。顺序别搞反了,我见过有人把p1和k2写反,结果校正出来的图像像哈哈镜。
| 参数 | 含义 | 典型范围 | 备注 |
|---|---|---|---|
| k1 | 径向畸变一阶项 | -0.5 ~ 0.5 | 主要畸变来源,桶形为负,枕形为正 |
| k2 | 径向畸变二阶项 | -0.3 ~ 0.3 | 修正高阶畸变 |
| k3 | 径向畸变三阶项 | -0.1 ~ 0.1 | 鱼眼镜头常用,普通镜头可忽略 |
| p1 | 切向畸变第一项 | -0.01 ~ 0.01 | 镜头与传感器不平行 |
| p2 | 切向畸变第二项 | -0.01 ~ 0.01 | 同上 |
重要提醒:畸变系数是相对于归一化图像坐标的。也就是说,它和图像分辨率无关。同一个镜头,你用640×480标定出来的系数,直接用在1920×1080上,效果是一样的。但前提是——你没有改变镜头的焦距和光圈。
4.5 畸变校正的完整流程
在实际的双目标定中,畸变校正通常分两步走:
- 标定阶段:用棋盘格或圆点标定板,拍多张不同角度的照片,通过优化算法解算出k1,k2,k3,p1,p2。
- 校正阶段:用解算出的系数,对每一帧图像进行重映射(remap),把畸变像素“拉”回到正确位置。
OpenCV里,核心函数就两个:
# 标定
ret, mtx, dist, rvecs, tvecs = cv2.calibrateCamera(objpoints, imgpoints, gray.shape[::-1], None, None)
# 校正
undistorted = cv2.undistort(img, mtx, dist, None, newcameramtx)
或者更高效的方式——先计算映射表,然后一次性应用:
mapx, mapy = cv2.initUndistortRectifyMap(mtx, dist, None, newcameramtx, (w, h), 5)
undistorted = cv2.remap(img, mapx, mapy, cv2.INTER_LINEAR)
我个人习惯用第二种方式。因为在实际的双目系统中,左右目各需要一张映射表,提前算好存起来,运行时直接remap,效率高很多。
4.6 知识体系总览
下面这张图,帮你把本章的核心逻辑串起来:
4.7 避坑指南
最后,分享几个我踩过的坑:
- 标定板要覆盖整个视野:尤其是边缘和角落。如果只拍中心区域,畸变系数根本算不准。我曾经偷懒只拍了10张中心附近的图,结果校正后边缘反而更扭曲了。
- 不要用JPEG压缩太狠的图:压缩带来的块效应会影响角点检测精度。我习惯用BMP或者PNG,实在不行JPEG质量也得90以上。
- 畸变系数不是越大越好:如果标定出来的k1超过0.5,或者p1超过0.05,先别急着用。检查一下标定板是否平整、照片是否清晰、角点检测是否准确。很多时候是数据质量的问题,不是镜头的问题。
- 双目系统要分别标定畸变:左右目的畸变系数不一定相同。即使同一型号的两个镜头,装配误差也会导致p1,p2有差异。我习惯左右目各自独立标定,然后再做立体标定。
一个小技巧:标定完成后,可以用cv2.projectPoints把标定板上的角点投影回图像上,看看重投影误差。如果误差超过0.5像素,说明标定质量不够好,需要重新拍图。我一般要求重投影误差在0.1~0.3像素之间,才算合格。
好了,畸变模型就聊到这儿。记住一句话:畸变校正做不好,双目重建全白搞。下一章,咱们进入立体标定的核心——极线几何与本质矩阵。