4. 相机模型与标定:针孔相机模型、畸变模型、相机标定实战
说到视觉SLAM,相机就是我们的“眼睛”。但说实话,这双眼睛天生就有“近视”和“散光”。
我刚开始做VIO时,总觉得标定是浪费时间。直到有一次,算法在实验室跑得好好的,一到户外就飘得离谱。折腾了两天,最后发现是相机畸变参数没更新。嗯,从那以后,我再也不敢跳过标定了。
4.1 针孔相机模型:最朴素的成像原理
先聊聊最基础的针孔模型。说白了,就是小孔成像原理——光线穿过一个小孔,在后面的成像平面上倒立成像。
数学上怎么描述?我们用一个4x3的投影矩阵搞定:
// 针孔相机模型的核心公式
// 世界坐标系下的点 P(X,Y,Z) 投影到像素坐标系 (u,v)
// 其中 fx, fy 是焦距(像素单位),cx, cy 是光心偏移
u = fx * (X / Z) + cx
v = fy * (Y / Z) + cy
这里有个关键点:尺度不确定性。你想想看,一个物体离得远但尺寸大,和离得近但尺寸小,在图像上可能一模一样。这就是单目视觉无法直接获得深度信息的根本原因。
核心参数一览:
- 内参矩阵 K:包含 fx, fy, cx, cy,描述相机本身的属性
- 外参矩阵 [R|t]:描述相机在世界坐标系中的位姿
- 投影矩阵 P:内参 × 外参,完成3D→2D的映射
我在项目中遇到过一个问题:用手机摄像头做SLAM,发现不同焦距下内参变化很大。后来才意识到,手机摄像头有自动变焦功能,每次变焦内参都会变。所以做VIO时,一定要锁定焦距,或者实时更新内参。
4.2 畸变模型:为什么直线会变弯?
理想很丰满,现实很骨感。针孔模型假设镜头是完美的,但实际镜头都有畸变。我见过最夸张的情况——一个广角摄像头,画面边缘的直线弯成了S形。
畸变主要分两类:
| 畸变类型 | 产生原因 | 视觉效果 |
|---|---|---|
| 径向畸变 | 透镜形状不完美 | 直线向外或向内弯曲(桶形/枕形) |
| 切向畸变 | 透镜与成像平面不平行 | 图像倾斜、梯形失真 |
数学上,我们用多项式模型来矫正:
// 径向畸变矫正(k1, k2, k3 为径向畸变系数)
// r 为像素点到光心的距离
x_corrected = x * (1 + k1*r^2 + k2*r^4 + k3*r^6)
y_corrected = y * (1 + k1*r^2 + k2*r^4 + k3*r^6)
// 切向畸变矫正(p1, p2 为切向畸变系数)
x_corrected = x + [2*p1*x*y + p2*(r^2 + 2*x^2)]
y_corrected = y + [p1*(r^2 + 2*y^2) + 2*p2*x*y]
避坑指南:我曾经用OpenCV的默认参数标定一个鱼眼镜头,结果矫正后的图像边缘出现了严重的“振铃效应”。后来改用cv::fisheye模块才搞定。记住:普通镜头用cv::calibrateCamera,鱼眼镜头用cv::fisheye::calibrate,别混用。
4.3 相机标定实战:用OpenCV搞定
好了,理论说完了,咱们动手。我个人习惯用棋盘格标定板,因为角点检测稳定,而且OpenCV有现成的函数。
4.3.1 准备工作
- 打印一张棋盘格(建议10×7格,每个格子30mm)
- 用相机从不同角度拍摄20-30张照片
- 确保棋盘格在画面中占30%-70%的面积
我的经验:拍摄时让棋盘格尽量靠近画面边缘,因为畸变在边缘最明显。我见过有人只拍中心区域,标定出来的畸变参数几乎为零——这显然不对。
4.3.2 OpenCV标定代码
import cv2
import numpy as np
import glob
# 棋盘格参数
CHECKERBOARD = (9, 6) # 内角点数量
SQUARE_SIZE = 30.0 # 格子尺寸(mm)
# 准备世界坐标系中的点
objp = np.zeros((CHECKERBOARD[0]*CHECKERBOARD[1], 3), np.float32)
objp[:,:2] = np.mgrid[0:CHECKERBOARD[0], 0:CHECKERBOARD[1]].T.reshape(-1,2)
objp *= SQUARE_SIZE
# 存储所有图像的点
objpoints = [] # 3D点
imgpoints = [] # 2D点
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, CHECKERBOARD, None)
if ret:
objpoints.append(objp)
# 亚像素精确化
criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 30, 0.001)
corners2 = cv2.cornerSubPix(gray, corners, (11,11), (-1,-1), criteria)
imgpoints.append(corners2)
# 可视化
cv2.drawChessboardCorners(img, CHECKERBOARD, corners2, ret)
cv2.imshow('img', img)
cv2.waitKey(500)
cv2.destroyAllWindows()
# 执行标定
ret, mtx, dist, rvecs, tvecs = cv2.calibrateCamera(
objpoints, imgpoints, gray.shape[::-1], None, None
)
print("内参矩阵:\n", mtx)
print("畸变系数:\n", dist)
print("重投影误差:", ret)
4.3.3 评估标定结果
标定完别急着用,先看看重投影误差。我一般要求误差小于0.5像素,如果超过1像素,说明标定质量不行。
# 计算重投影误差
total_error = 0
for i in range(len(objpoints)):
imgpoints2, _ = cv2.projectPoints(objpoints[i], rvecs[i], tvecs[i], mtx, dist)
error = cv2.norm(imgpoints[i], imgpoints2, cv2.NORM_L2) / len(imgpoints2)
total_error += error
print("平均重投影误差:", total_error / len(objpoints))
标定质量检查清单:
- 重投影误差 < 0.5像素
- fx和fy的比值接近1(除非是特殊镜头)
- cx和cy接近图像中心
- 畸变系数不要太大(k1通常在-0.5到0.5之间)
4.4 用Matlab标定:懒人福音
如果你不想写代码,Matlab的Camera Calibrator是个好选择。我个人觉得它的可视化做得比OpenCV好,尤其是畸变矫正前后的对比图。
操作步骤很简单:
- 打开Matlab,输入
cameraCalibrator - 导入标定图片
- 输入棋盘格尺寸
- 点击Calibrate,等几秒钟
- 导出参数到工作区
Matlab会自动计算重投影误差,还会标出误差最大的图片——你可以直接删掉那些“坏图”,重新标定一次。
小技巧:标定完成后,用showExtrinsics函数看看相机和标定板的相对位置。如果所有标定板都集中在同一个方向,说明你拍摄的角度不够丰富,需要补拍。
4.5 本章知识体系
下面这张图总结了相机模型与标定的核心逻辑:
这张图把本章的核心内容串起来了。从针孔模型出发,加入畸变模型,最后通过标定得到精确参数。这些参数会直接喂给SLAM前端,用于特征点匹配和位姿估计。
好了,相机模型和标定就聊到这儿。记住一句话:标定做得好,SLAM跑得稳。别像我当年那样,在标定上偷懒,最后花更多时间debug。