第二章:相机模型与内参标定
相机标定,说白了就是搞清楚相机到底是怎么「看」世界的。我刚开始做视觉SLAM时,总觉得标定是件小事,随便找个棋盘格拍几张就完事了。结果呢?跑出来的轨迹歪歪扭扭,怎么调参数都没用。后来才明白——标定是地基,地基不稳,上层建筑全白搭。
这一章,我们就把相机模型和标定这件事彻底讲透。从针孔模型到畸变校正,从张正友标定法到Kalibr/OpenCV实战,一步到位。
核心要点:相机标定的本质,是求解从3D世界到2D图像的映射关系。这个映射包含两部分——几何投影(针孔模型)和光学畸变(畸变模型)。
2.1 针孔模型:最简单的相机
针孔模型是相机模型的基础。你想想看,一个小孔,光线穿过它,在后面的平面上成像——这就是最原始的相机。
数学上,针孔模型可以写成:
s * [u, v, 1]^T = K * [R|t] * [X, Y, Z, 1]^T
其中:
- (u, v) 是像素坐标
- (X, Y, Z) 是世界坐标
- K 是内参矩阵
- [R|t] 是外参矩阵
- s 是尺度因子
内参矩阵K长这样:
K = [fx, 0, cx;
0, fy, cy;
0, 0, 1]
fx, fy 是焦距(以像素为单位),cx, cy 是主点坐标。我在项目中遇到过一个问题:用OpenCV标定出来的cx, cy,理论上应该在图像中心附近,但有一次标出来偏了20多个像素。排查了半天,发现是标定板没放正,角度太单一了。
我的经验:标定时,标定板要覆盖整个视野,尤其是边缘区域。只拍中心区域,主点估计会严重偏差。
2.2 畸变模型:现实总是不完美
针孔模型是理想情况。现实中的镜头,尤其是广角镜头,会有明显的畸变。畸变主要分两种:
- 径向畸变:光线通过镜头边缘时弯曲,表现为「桶形」或「枕形」
- 切向畸变:镜头与成像平面不平行,表现为「倾斜」
数学上,畸变模型通常用多项式来描述:
x_distorted = x * (1 + k1*r^2 + k2*r^4 + k3*r^6) + 2*p1*x*y + p2*(r^2 + 2*x^2)
y_distorted = y * (1 + k1*r^2 + k2*r^4 + k3*r^6) + p1*(r^2 + 2*y^2) + 2*p2*x*y
其中 k1, k2, k3 是径向畸变系数,p1, p2 是切向畸变系数。
嗯,这里要注意:k3通常只在畸变很大的镜头(比如鱼眼)才需要。普通镜头用k1, k2就够了。我曾经在标定一个工业相机时,强行用了k3,结果标定结果反而更差——过拟合了。
避坑指南:我曾经在标定IMU+相机时,发现畸变参数总是不收敛。后来发现是标定板上的角点检测出了问题——光照不均匀,导致角点检测偏移了半个像素。解决办法:用均匀光源,或者拍多组不同光照下的图像。
2.3 张正友标定法:经典中的经典
张正友标定法,是相机标定领域最经典的方法。它的核心思想是:用一张平面棋盘格,从不同角度拍多张照片,然后求解内参。
为什么用平面?因为平面上的点,其Z坐标都是0,可以简化计算。具体步骤:
- 检测角点:在每张图像中检测棋盘格的角点
- 计算单应矩阵:每张图像对应一个单应矩阵H
- 求解内参:利用单应矩阵的约束,线性求解内参
- 优化:用非线性最小二乘法优化所有参数
我个人习惯用OpenCV的 cv::calibrateCamera() 函数,它已经实现了张正友法。但要注意,这个函数默认假设标定板是完美的——也就是说,棋盘格的角点坐标是精确已知的。如果你的标定板打印有误差,那标定结果也会跟着偏。
关键点:张正友法要求至少3张不同角度的图像。但实际中,我建议拍15-20张,覆盖不同角度、不同距离。角度变化要大,但不要太大——标定板与相机平面的夹角最好在45度以内,否则角点检测会失败。
2.4 使用OpenCV进行标定
OpenCV的标定流程,我直接给代码:
import cv2
import numpy as np
# 准备标定板的角点坐标
pattern_size = (9, 6) # 内角点数量
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 = [] # 3D点
imgpoints = [] # 2D点
# 遍历图像
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:
objpoints.append(objp)
imgpoints.append(corners)
# 标定
ret, mtx, dist, rvecs, tvecs = cv2.calibrateCamera(
objpoints, imgpoints, gray.shape[::-1], None, None
)
print("内参矩阵:\n", mtx)
print("畸变系数:\n", dist)
这段代码看起来简单,但实际用起来有几个坑:
- 标定板尺寸:代码中的
pattern_size是内角点数量,不是格子数量。9x6的棋盘格,内角点是8x5 - 图像数量:少于10张,标定结果不稳定。我一般拍20张左右
- 重投影误差:标定完成后,检查重投影误差。一般小于0.5像素就算合格。如果大于1像素,说明标定质量有问题
我的习惯:标定完成后,我会用 cv2.undistort() 对一张图像做畸变校正,肉眼检查校正效果。如果直线变直了,说明标定成功。如果直线还是弯的,那就得重新拍了。
2.5 使用Kalibr进行标定
Kalibr是ETH Zurich开发的标定工具,专门用于多传感器标定。它支持相机内参标定、相机-IMU外参标定等。
Kalibr的标定流程:
- 准备标定板:Aprilgrid或Checkerboard,需要提供标定板的YAML配置文件
- 录制数据:用rosbag录制图像数据
- 运行标定:使用kalibr_calibrate_cameras命令
标定板的YAML配置示例:
target_type: 'checkerboard'
targetCols: 6
targetRows: 7
rowSpacingMeters: 0.03
colSpacingMeters: 0.03
运行标定命令:
kalibr_calibrate_cameras \
--bag /path/to/data.bag \
--topics /camera/image_raw \
--models pinhole-radtan \
--target /path/to/target.yaml
Kalibr的优势在于:它支持多种相机模型(pinhole、omni、ds等)和多种畸变模型(radtan、equidistant、fov等)。而且,它还能同时标定多个相机——对于双目或多目系统,非常方便。
避坑指南:我曾经用Kalibr标定一个鱼眼相机,选了pinhole-equidistant模型,结果标定结果一直不收敛。后来发现,鱼眼相机应该用omni模型或者ds模型。选错模型,标定结果就是错的。
2.6 标定质量评估
标定完成后,怎么知道标定结果好不好?我一般看三个指标:
| 指标 | 合格标准 | 说明 |
|---|---|---|
| 重投影误差 | < 0.5像素 | 所有角点的平均重投影误差 |
| 内参稳定性 | 多次标定结果一致 | 用不同图像集标定,看内参是否稳定 |
| 畸变校正效果 | 直线变直 | 肉眼检查校正后的图像 |
嗯,这里要特别说一下:重投影误差不是越小越好。如果误差小于0.1像素,反而要警惕——可能是过拟合了。我见过有人标定出0.05像素的误差,但换一组图像,误差就变成0.8像素。这说明标定结果不稳定。
我的建议:标定完成后,用一组全新的图像做验证。如果验证集上的重投影误差和训练集上的差不多,那标定结果才是可靠的。
2.7 本章小结
相机标定,说白了就是求解内参和畸变参数。针孔模型是基础,畸变模型是补充,张正友法是经典解法。OpenCV适合快速上手,Kalibr适合多传感器系统。
我个人觉得,标定这件事,七分靠方法,三分靠经验。方法对了,标定结果不会差。但经验不足,容易踩坑。多拍几组数据,多检查几次结果,总没错。
最后,记住一句话:标定是SLAM系统的第一步,也是最重要的一步。这一步走稳了,后面的路就好走了。