简介:面向计算机视觉与图像处理开发者,围绕鱼眼相机去畸变这一经典问题,提供经纬度矫正法与棋盘格矫正法的完整代码与实现思路;经纬度法通过球面坐标映射展开图像,棋盘格法则借助OpenCV fisheye模块标定内参与畸变系数,两条路线均配有可直接运行的Python脚本。压缩包共18个文件,以Python脚本为主,涵盖两种矫正算法、工具函数与主程序,另有HTML页面用于结果展示与交互预览,jpg图片作为输入样例与矫正效果对比,md/txt文档说明运行依赖和注意事项,整体大小仅4.89MB,轻量易上手。资源基于OpenCV fisheye模块,代码清晰完整,读者可直接运行主程序观察矫正过程,配合样例图深入理解经纬度法映射原理与棋盘格法标定参数校正。除两种基础方法外,还提供了立体标定和双经度模型等拓展内容,便于算法选型时评估边缘拉伸、细节保留和三维重建精度。目前已有141人学习下载,适合计算机视觉初学者快速上手,也为工程开发者提供可复用的去畸变工具脚本。 接了个真实项目,要用鱼眼相机做车周障碍物测距。相机装好后第一件事就是把鱼眼图去畸变成普通透视图。我一开始直接用OpenCV的fisheye::undistortImage,结果边缘还是弯、中心也不准,后来老老实实把标定、映射表、参数计算整个流程重走了一遍,才把问题理顺。这篇文章就把我实际用下来的鱼眼相机去畸变算法、代码和踩坑点整理出来,给同样困在“鱼眼图怎么变正常”里的朋友一个能直接抄作业的参考。
鱼眼相机去畸变,说起来就一句话:把镜头造成的非线性几何变形救回来。但真做起来,涉及成像模型、标定精度、映射方式、插值策略好几个环节,任何一个失手,出来的图都像哈哈镜。适合谁看?想真正落地鱼眼去畸变、而不是只会调一个API的工程师,这篇可以帮你在动手前把全链路理清楚。
1. 鱼眼镜头为什么非去畸变不可
1.1 投影模型决定了成像规律
普通相机(针孔模型)满足的是r = f * tan(θ),θ是入射光线与光轴的夹角。当θ接近90度时,tan(θ)直接爆炸,所以普通镜头的视场角做不大,通常在60度以内。鱼眼镜头要拍出180度甚至更大的视场角,必须换投影函数。
常用的鱼眼投影模型有这么几种:
- 等距投影:
r = f * θ - 等立体角投影:
r = 2 * f * sin(θ/2) - 正交投影:
r = f * sin(θ) - 体视投影:
r = 2 * f * tan(θ/2)
最常用的是等距投影,也是OpenCV鱼眼模型的基础。因为r和θ近似线性,所以视角越大,图像边缘的画面就被压缩得越厉害,直线在图像里也就弯得越明显。去畸变本质上就是要把这种被非线性压缩的投影关系重新映射回我们习惯的透视关系。
1.2 为什么不能直接用小孔模型去矫正
很多人拿到鱼眼图第一反应是用cv2.undistort(),传几个径向畸变系数k1, k2, p1, p2进去。但小孔模型下的Brown-Conrady畸变模型只适合畸变较小的镜头,它的多项式展开在极坐标系下对大角度偏差的拟合能力不够。鱼眼镜头的入射角动辄90度以上,用普通模型强行拟合,边缘会留下很大的残差。
专业鱼眼标定用的是Kannala-Brandt模型,畸变向量包含4个参数k1, k2, k3, k4,本质是入射角的多项式展开,能把大角度区域描述得更准。所以第一步就要分清:你是鱼眼相机,就老老实实用cv2.fisheye这一套,别和小孔模型的参数混用。
2. 标定:畸变参数的来源
去畸变算法的输入,除了原始图像,就是相机的内参矩阵K和畸变系数向量D。这两个东西不是凭空造的,必须通过标定获得。
2.1 标定板选择与图片采集
棋盘格和圆点格都行,但鱼眼镜头视场角大,边缘畸变严重,棋盘格角落容易漏检。我实测下来,圆点阵列板在鱼眼图像里的角点提取稳定性更高一些。
采集图片时有几个硬性要求:
- 数量至少20张,建议30张左右,太少的话
k3, k4这类高阶参数会估计得很飘。 - 标定板要出现在画面的边缘和角落,这四个区域是畸变最严重的,不覆盖到的话,标定出的参数只能矫正中心区域。
- 板子要倾斜、旋转、远近都拍,尽量让每张图里的角点位置分布均匀。
- 板子必须贴在平整的硬板上,不要拿张软纸就拍,局部弯曲会直接污染角点坐标。
另外,相机光圈和焦距在标定过程中不能动,我见过有人标定到一半去调了焦距,结果所有参数全部作废。
2.2 标定输出的参数含义
用cv2.fisheye.calibrate标完后,拿到的是:
K:3x3的内参矩阵,包含fx, fy, cx, cy,其中cx, cy是主点位置,在鱼眼相机里往往不在图像正中心,这个后面去畸变时要用到。D:4个畸变系数k1, k2, k3, k4,对应不同阶数的入射角多项式项。rvecs, tvecs:每张标定板的外参,重投影误差的计算依赖它们。
重投影误差建议控制在0.1像素以内,如果超过0.3像素,说明有标定板检测错误或者采集素材有问题,先回头修数据,不要继续往下走。
3. 代码实现:从内参到去畸变图
环境是OpenCV 4.8.0,Python版和C++版我都写了,下面的核心逻辑两边通用,只列举Python实现,C++版的remap接口一样。
3.1 标定代码
import cv2 import numpy as np import glob # 棋盘格内角点数量 CHECKERBOARD = (6, 9) criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 30, 0.001) objp = np.zeros((CHECKERBOARD[0] * CHECKERBOARD[1], 3), np.float32) objp[:, :2] = np.mgrid[0:CHECKERBOARD[0], 0:CHECKERBOARD[1]].T.reshape(-1, 2) objpoints = [] # 世界坐标系中的点 imgpoints = [] # 图像坐标系中的点 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, CHECKERBOARD, None) if ret: objpoints.append(objp) corners2 = cv2.cornerSubPix(gray, corners, (5, 5), (-1, -1), criteria) imgpoints.append(corners2) # 鱼眼相机标定 N_OK = len(objpoints) K = np.zeros((3, 3)) D = np.zeros((4, 1)) rvecs = [np.zeros((1, 1, 3), dtype=np.float64) for _ in range(N_OK)] tvecs = [np.zeros((1, 1, 3), dtype=np.float64) for _ in range(N_OK)] rms, _, _, _, _ = cv2.fisheye.calibrate( objpoints, imgpoints, gray.shape[::-1], K, D, rvecs, tvecs, cv2.fisheye.CALIB_RECOMPUTE_EXTRINSIC + cv2.fisheye.CALIB_CHECK_COND, criteria ) print("重投影误差:", rms) print("内参矩阵 K:\n", K) print("畸变系数 D:\n", D)3.2 去畸变核心流程
拿到K和D之后,去畸变有两种做法:一步到位用fisheye.undistortImage,或者分步走estimateNewCameraMatrixForUndistortRectify+initUndistortRectifyMap+remap。我强烈推荐分步走,因为你要控制去畸变后的视野大小和裁剪方式,一步到位的接口把参数都封装死了,不好调。
def undistort_fisheye(img, K, D, balance=0.0, dim2=None, dim3=None): h, w = img.shape[:2] # 新图像尺寸 if dim2 is None: dim2 = (w, h) # 估算新内参矩阵 new_K = cv2.fisheye.estimateNewCameraMatrixForUndistortRectify( K, D, (w, h), np.eye(3), balance=balance ) # 可选的输出尺寸 if dim3 is None: dim3 = dim2 # 生成映射表 map1, map2 = cv2.fisheye.initUndistortRectifyMap( K, D, np.eye(3), new_K, dim2, cv2.CV_16SC2 ) # 重映射 undistorted_img = cv2.remap( img, map1, map2, interpolation=cv2.INTER_LINEAR, borderMode=cv2.BORDER_CONSTANT ) return undistorted_img, new_Kbalance参数是最值得玩的旋钮。它的取值范围是0到1:
balance=0:只保留无畸变区域,图像边缘会被大量裁剪,视野变小,但画面里没有黑色空洞。balance=1:尽量保留原始视野,去畸变后边缘会有较大的空白区域,需要用黑色填充。- 实际项目我一般从0.4开始试,在视野和画面完整度之间找平衡。
3.3 参数计算与内部原理
很多人不理解initUndistortRectifyMap生成了什么。其实它是为输出图像里的每一个像素位置(u, v),根据new_K反算出一个归一化坐标,再用畸变模型计算出该点对应原图像素的位置(u', v'),存进map1, map2。之后remap做的事情就是按表取像素,所以这是一次性开销,实时性要求高的场景可以先算好映射表,之后每帧只执行remap。
新内参矩阵new_K也不是随便给的。estimateNewCameraMatrixForUndistortRectify会基于balance自动调整主点和焦距,保证输出图像的视场角落在合理范围。如果你手动指定new_K,比如把焦距放大1.2倍,输出画面就相当于把中心区域放大,视野变窄但细节更清晰;缩小焦距则视野变大,边缘畸变也会更明显。根据我的经验,做测距项目时焦距必须和实际物理焦距匹配,不然距离解算会出偏差。
4. 常见问题与排查技巧实录
4.1 去畸变后边缘出现大片黑角
这是最常遇到的问题。原因很简单:去畸变后原来的边缘像素被映射到了更远的位置,图像四周自然空出来了。解决办法就是调balance,往1的方向加,画面会逐渐饱满。如果balance=1还不够,可以缩小输出图像的尺寸比例,比如把dim3设为(int(w * scale), int(h * scale)),相当于输出时做了个缩放,黑色区域会被裁到画面外。
4.2 直线还是弯的,标定好像没用
如果标定流程跑完了、去畸变参数也用了,但画面里的直线依然有明显的弯曲,优先检查三件事:
- 重投影误差是否过大,过大说明标定板数据本身有问题。
K和D是否真的来自当前这台相机,固件不同、镜头批次不同,参数都可能变。- 采集图片是否覆盖了边缘,只拍了中心区域的标定板,畸变系数必然不准确。
还有一种隐蔽情况:如果你用的OpenCV版本较老,fisheye模块的estimateNewCameraMatrixForUndistortRectify对balance的实现有细微差别,导致输出的new_K不理想。升级到4.x以上基本能解决。
4.3 去畸变后的图像中心偏移
鱼眼相机的主点cx, cy往往不在图像正中心,这在标定结果里直接能看到。如果去畸变后主体偏了,说明你在生成映射表的时候传入的R矩阵不对。正常情况R应该传入单位阵np.eye(3),表示不做旋转矫正。如果你的相机安装时有俯仰或偏航角,这个R可以换成旋转矩阵,做一步到位的视角矫正,这相当于把去畸变和相机姿态补偿合并了。
4.4 实时性能不够
remap本身很快,主要开销在插值计算上。如果性能不足,可以降插值方法,从INTER_CUBIC降到INTER_LINEAR,一般肉眼看不出差别。更重要的是把映射表缓存起来,不要每帧都调initUndistortRectifyMap,这个函数比remap本身贵得多。
# 缓存映射表,只做一次 map1, map2 = cv2.fisheye.initUndistortRectifyMap( K, D, np.eye(3), new_K, dim2, cv2.CV_16SC2 ) # 之后的每一帧 undistorted = cv2.remap(frame, map1, map2, INTER_LINEAR)| 错误现象 | 可能原因 | 排查方法 |
|---|---|---|
| 边缘黑角过多 | balance过小或输出尺寸过大 | 调大balance,或缩小dim3 |
| 输出图中心依然弯曲 | 标定数据未覆盖边缘 | 补充边缘位置的标定板图片 |
| 图像整体偏移 | 主点估计不准或R矩阵不为单位阵 | 检查K中的cx/cy,确认R是I |
| 标定重投影误差高 | 标定板弯曲或角点亚像素提取得差 | 换平板、检查角点检测结果 |
5. 实操中那些文档里不会写的经验
代码能跑通只是第一步。我实际调过多次之后,有几点特别想说的经验。
关于标定素材,宁缺毋滥。我一开始贪多,拍了50张图,结果其中10张是模糊的,标出来的参数反而比30张干净的还差。后来我加了个简单筛选,用findChessboardCorners检测成功后,再算一下棋盘格的清晰度响应,对过低直接跳过。
关于去畸变后的用途,一定要明确是给人看还是给算法看。给人看的话,画面完整度和直观感受更重要,balance可以调大,甚至边缘留点黑边也能接受;给深度学习模型做测距或识别的话,去畸变模型和训练数据要一致,训练集用鱼眼图就去畸变后再训练,不要一边用原图训练一边拿矫正图做推理,效果会差很多。
关于坐标映射,很多工程师忽略了一点:去畸变不只是输出一张能看的图,像素坐标和实际物理坐标的对应关系才是自动驾驶、机器人领域真正关心的。去畸变后如果用new_K做后续的像素到射线方向还原,方向向量才是准的。如果只是把去畸变图送去深度学习模型做检测,那么检测框坐标需要再映射回原图,这个过程要用remap的逆映射,千万别自己拿K和new_K硬推,容易出错。
最后说一个可以提升效率的思路:固定安装的相机在标定完成后,畸变参数D和内参矩阵K基本不会变,所以映射表完全可以在程序启动时算一次,存成一个map.npz文件,之后每次运行直接加载。这样不仅能省去启动时的计算耗时,还能保证每帧处理时只走一次remap,性能余量会大很多。
鱼眼相机的去畸变看起来是一个很小的点,但整条链路走通之后,几乎所有的图像前处理、目标检测、测距定位都能在更标准的透视图像上展开,省掉后续大量补畸变的麻烦。我的体会是:标定不能偷懒,映射表一定缓存,参数调整以实际场景效果为准,不要迷信某个固定值。希望这篇文章能让你少走几步弯路。
本文还有配套的精品资源,点击获取