3. OpenCV鱼眼相机标定
好,咱们进入实操环节。前面聊了那么多理论,什么径向畸变、切向畸变、等距投影模型…… 说实话,光看公式容易晕。我自己的经验是,动手标定一次,比看十遍都管用。
这一章,我们就用 OpenCV 把鱼眼相机的内参和畸变系数给求出来。整个过程分三步:准备棋盘格、检测角点、求解参数。每一步都有坑,我会把踩过的坑都指出来。
3.1 棋盘格标定板:不是随便打印一张纸就行
标定板是标定的“尺子”。尺子不准,后面全白搭。
OpenCV 的鱼眼标定函数 cv::fisheye::calibrate() 默认支持棋盘格(Chessboard)和对称圆点阵。我个人强烈推荐用棋盘格,因为角点检测的鲁棒性更好,尤其是在鱼眼图像边缘严重畸变的区域。
3.1.1 棋盘格的关键参数
| 参数 | 说明 | 我的建议 |
|---|---|---|
| 内角点数量 | 棋盘格内部交叉点的行列数(如 9×6) | 不要用太少的点(如 4×3),建议 8×6 或 9×7 |
| 方格尺寸 | 每个小方格的实际物理边长(单位:mm) | 用游标卡尺量,别信打印店的标称值 |
| 材质 | 纸张、亚克力板、陶瓷板 | 室内用亚克力板,平整度好;室外用陶瓷板 |
| 图案精度 | 打印分辨率、边缘是否清晰 | 至少 600 DPI,边缘模糊会导致亚像素角点偏移 |
⚠️ 我曾经踩过的坑: 有一次我用普通的 A4 纸打印棋盘格,贴在泡沫板上。结果标定出来的焦距偏差了 3% 以上。后来发现是泡沫板本身不平,在鱼眼镜头下,微小的弯曲都会被放大。从那以后,我标定鱼眼只用 3mm 厚的亚克力板。
3.1.2 世界坐标系的建立
标定时,我们把棋盘格平面定义为 Z=0 的平面。每个内角点的世界坐标就是 (col * square_size, row * square_size, 0)。这里有个细节:行列顺序必须和角点检测时的顺序一致,否则标定出来的外参方向是反的。
# 生成棋盘格的世界坐标点
pattern_size = (9, 6) # 内角点数量:9列 x 6行
square_size = 30.0 # 方格边长:30mm
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
3.2 角点检测:鱼眼图像的难点在这里
普通相机的角点检测用 cv::findChessboardCorners() 就够了。但鱼眼图像不一样——边缘区域的棋盘格被严重拉伸和扭曲,直接调用函数大概率会失败。
3.2.1 为什么鱼眼图像角点检测容易失败?
- 畸变太大:边缘的棋盘格可能被压缩成一条线,角点特征消失。
- 光照不均:鱼眼镜头视角大,画面边缘亮度通常比中心暗很多。
- 遮挡:标定板稍微倾斜,边缘的角点就可能被镜头筒遮挡。
3.2.2 我的检测策略
我一般分两步走:
- 粗检测:先用
cv::findChessboardCorners()尝试,如果失败,对图像做预处理——直方图均衡化 + 自适应阈值。 - 亚像素精化:用
cv::cornerSubPix()把角点精度提升到亚像素级别。这一步对鱼眼标定尤其重要,因为畸变模型对像素误差非常敏感。
import cv2
import numpy as np
def detect_chessboard_corners(img, pattern_size):
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
# 尝试直接检测
ret, corners = cv2.findChessboardCorners(gray, pattern_size, None)
if not ret:
# 如果失败,做预处理
clahe = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8,8))
gray_eq = clahe.apply(gray)
ret, corners = cv2.findChessboardCorners(gray_eq, pattern_size, None)
if ret:
# 亚像素精化
criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 30, 0.001)
corners = cv2.cornerSubPix(gray, corners, (5,5), (-1,-1), criteria)
return ret, corners
💡 小技巧: 拍摄标定图像时,让棋盘格尽量覆盖画面的各个区域——中心、边缘、角落、上下左右。我一般拍 20~30 张,确保每个区域都有 3 张以上的有效检测。
3.3 内参矩阵与畸变系数求解
角点检测完成后,就可以调用 cv::fisheye::calibrate() 了。这个函数是 OpenCV 专门为鱼眼相机设计的,它使用的是 等距投影模型(Equidistant Projection Model),也就是我们上一章讲的那个。
3.3.1 函数接口与参数
retval, K, D, rvecs, tvecs = cv2.fisheye.calibrate(
objectPoints, # 世界坐标点列表
imagePoints, # 图像角点列表
image_size, # 图像尺寸 (width, height)
K, # 内参矩阵(输出)
D, # 畸变系数(输出)
rvecs, # 旋转向量(输出)
tvecs, # 平移向量(输出)
flags, # 标定选项
criteria # 迭代终止条件
)
3.3.2 关键参数说明
| 参数 | 含义 | 推荐值 |
|---|---|---|
flags |
标定选项,控制哪些参数固定、哪些优化 | cv2.fisheye.CALIB_RECOMPUTE_EXTRINSIC + cv2.fisheye.CALIB_CHECK_COND |
criteria |
迭代终止条件 | (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 30, 1e-6) |
K |
内参矩阵,3×3 | 初始值用图像中心估算焦距 |
D |
畸变系数,4×1(k1, k2, k3, k4) | 初始值全零 |
3.3.3 输出结果解读
标定完成后,你会得到两个核心输出:
- 内参矩阵 K:包含焦距 (fx, fy) 和主点 (cx, cy)。对于鱼眼相机,主点通常不在图像正中心,因为镜头安装有偏差。
- 畸变系数 D:四个参数 k1, k2, k3, k4。注意,OpenCV 的鱼眼模型只用 4 个径向畸变参数,没有切向畸变参数(p1, p2)。
🔍 如何判断标定结果好不好?
- 重投影误差:一般应小于 0.5 像素。如果大于 1.0 像素,说明角点检测有问题,或者标定板不平。
- 畸变曲线:画出畸变率随入射角变化的曲线,应该平滑单调。如果有拐点或突变,说明模型不匹配。
- 校正后图像:直线应该变成直线。如果边缘还有弯曲,说明标定精度不够。
3.4 完整标定流程(SVG 流程图)
下面这张图是我自己总结的标定流程,每一步都有对应的代码和检查点:
3.5 避坑指南:我踩过的那些坑
标定鱼眼相机,说难不难,说简单也不简单。我把自己踩过的坑列出来,你遇到了可以直接对照:
- 坑一:标定板不够大。鱼眼镜头视角大,小标定板只能覆盖画面中心区域,边缘的畸变参数根本拟合不出来。我建议标定板的边长至少占图像短边的 60%。
- 坑二:图像数量不够。少于 15 张图像,标定结果很不稳定。我一般拍 25 张,确保棋盘格在画面各个位置都出现过。
- 坑三:忽略亚像素精化。直接用整数像素坐标去标定,重投影误差很难低于 1.0 像素。亚像素精化这一步不能省。
- 坑四:标定板弯曲。这个前面说过了,用亚克力板或者玻璃板,别用纸板。
💡 我的个人习惯: 标定完成后,我会用
cv::fisheye::stereoCalibrate() 做一次双目标定验证——把左右相机的标定结果放在一起优化,能发现单目标定中不易察觉的系统误差。