第二章:相机模型与标定
做视觉SLAM这么多年,我始终觉得相机模型和标定是入门的第一道坎。说白了,你连相机怎么成像的都不清楚,后面那些特征匹配、三角化、位姿估计,全都是空中楼阁。今天咱们就把这块硬骨头啃下来。
2.1 针孔相机模型
先聊聊最经典的针孔相机模型。你想想看,一个暗箱,前面戳个小孔,外面的光线穿过小孔在后面的感光元件上成像——这就是最朴素的相机原理。
数学上怎么描述?其实就四个坐标系的事:世界坐标系、相机坐标系、图像坐标系、像素坐标系。我刚开始学的时候,老被这四个坐标系绕晕。后来想通了,其实就是把三维空间里的点,一步步映射到二维像素上。
核心公式就一个:
// 从相机坐标系到像素坐标
u = fx * X/Z + cx
v = fy * Y/Z + cy
这里fx、fy是焦距参数,cx、cy是主点偏移。说白了,就是告诉你三维点投影到图像上,到底落在哪个像素位置。
关键点:针孔模型假设光线是直线传播的。但现实中的镜头不是小孔,是透镜组。这就引出了畸变问题。
2.2 畸变模型
现实中的镜头,尤其是广角镜头,拍出来的照片边缘都是弯的。这就是畸变。我在项目中遇到过最夸张的一次,用鱼眼镜头做AR导航,边缘的建筑物全歪了,根本没法用。
畸变主要分两类:
- 径向畸变:镜头形状导致的,越靠近边缘越明显。用k1、k2、k3三个参数描述。
- 切向畸变:镜头和感光元件不平行导致的。用p1、p2两个参数描述。
矫正公式也不复杂:
// 径向畸变矫正
x_corrected = x * (1 + k1*r² + k2*r⁴ + k3*r⁶)
y_corrected = y * (1 + k1*r² + k2*r⁴ + k3*r⁶)
// 切向畸变矫正
x_corrected = x + [2*p1*x*y + p2*(r² + 2*x²)]
y_corrected = y + [p1*(r² + 2*y²) + 2*p2*x*y]
注意:畸变参数不是越多越好。我见过有人用5个径向参数,结果过拟合了,标定结果反而更差。一般用3个径向+2个切向就够了。
2.3 张正友标定法
说到标定,就绕不开张正友。这个方法太经典了,用一张棋盘格照片就能搞定内参和外参。我当年读这篇论文的时候,就觉得这思路真巧妙——用平面标定板代替三维标定物,大大降低了操作门槛。
核心思路是这样的:
- 拍十几张不同角度的棋盘格照片
- 提取每张照片的角点坐标
- 利用单应性矩阵求解内参初值
- 考虑畸变,用非线性优化精化所有参数
嗯,这里要注意,棋盘格的大小要精确测量。我吃过这个亏——有一次标定完发现内参不对,查了半天,原来是棋盘格格子尺寸量错了2毫米。
个人经验:拍照时让棋盘格占画面1/3到1/2,角度变化要丰富。我习惯拍20张左右,覆盖画面的各个区域。太少容易欠约束,太多也没必要。
2.4 标定工具使用与实操
理论说完了,咱们上手实操。我最常用的工具是OpenCV的标定模块,稳定、高效、文档全。
先看看代码怎么写:
import cv2
import numpy as np
import glob
# 准备棋盘格角点坐标
chessboard_size = (9, 6) # 内角点数量
square_size = 25.0 # 格子边长,单位mm
# 生成世界坐标系中的角点坐标
objp = np.zeros((chessboard_size[0]*chessboard_size[1], 3), np.float32)
objp[:,:2] = np.mgrid[0:chessboard_size[0],
0:chessboard_size[1]].T.reshape(-1,2)
objp *= square_size
# 存储所有图片的角点
objpoints = [] # 世界坐标
imgpoints = [] # 像素坐标
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, chessboard_size, 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)
# 执行标定
ret, mtx, dist, rvecs, tvecs = cv2.calibrateCamera(
objpoints, imgpoints, gray.shape[::-1], None, None)
print("内参矩阵:\n", mtx)
print("畸变系数:\n", dist)
跑完标定,一定要做两件事:
- 重投影误差检查:一般小于0.5像素就算合格。我见过0.1像素的,那说明标定质量很高。
- 去畸变效果验证:拿一张标定板照片,看看边缘的直线是不是变直了。
避坑指南:我曾经用手机拍标定板,自动对焦没关,结果每张照片的焦距都不一样。标定出来的内参乱七八糟。记住:标定过程中焦距必须固定。
下面这张图是我整理的相机标定知识体系,帮你理清思路:
标定完事之后,记得保存结果。我一般存成YAML格式,方便后续读取:
%YAML:1.0
---
image_width: 1920
image_height: 1080
camera_matrix:
rows: 3
cols: 3
data: [718.5, 0, 960.2, 0, 719.3, 540.1, 0, 0, 1]
distortion_coefficients:
rows: 1
cols: 5
data: [-0.283, 0.074, -0.001, 0.002, 0.0]
好了,相机模型和标定这块就聊到这儿。记住一句话:标定是SLAM的基石,地基打不牢,楼盖得再高也得塌。下次咱们聊特征点提取和匹配,那又是另一番天地了。