第3章:相机成像模型与标定
大家好,我是你们的老朋友。今天咱们来聊聊相机标定——这个在感知融合里绕不开的话题。
说实话,我刚入行那会儿,觉得相机标定就是拍几张棋盘格照片,跑个算法就完事了。后来在实际项目中吃过亏,才明白这里面门道不少。今天我就把这些年踩过的坑、积累的经验,一次性讲清楚。
3.1 针孔相机模型
先说说最基础的针孔相机模型。你想想看,相机成像的本质是什么?说白了,就是把三维世界里的点,投影到二维图像平面上。
针孔模型假设光线是直线传播的,穿过一个小孔后,在后面的成像平面上形成倒像。数学上,这个过程可以用一个3x4的投影矩阵来描述:
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是主点坐标。我在项目中遇到过,有些同学直接拿厂家给的标称值用,结果融合出来的点云总是对不上。嗯,这里要注意——内参一定要自己标定,因为镜头安装、温度变化都会影响实际值。
3.2 畸变模型
理想很丰满,现实很骨感。针孔模型假设镜头是完美的,但实际镜头都有畸变。主要分两种:
3.2.1 径向畸变
径向畸变是光线通过镜头边缘时发生的弯曲。离中心越远,畸变越明显。我见过最夸张的一次,是某款广角镜头,边缘畸变达到了15个像素——这种数据直接拿去用,融合出来的结果根本没法看。
径向畸变的数学模型:
x_distorted = x * (1 + k1*r^2 + k2*r^4 + k3*r^6)
y_distorted = y * (1 + k1*r^2 + k2*r^4 + k3*r^6)
其中r是点到主点的距离,k1、k2、k3是径向畸变系数。一般用k1、k2就够了,鱼眼镜头才需要k3。
3.2.2 切向畸变
切向畸变是因为镜头和成像平面不平行造成的。说白了,就是镜头装歪了。这种情况在量产车上比较常见,毕竟装配精度有限。
切向畸变的模型:
x_distorted = x + [2*p1*x*y + p2*(r^2 + 2*x^2)]
y_distorted = y + [p1*(r^2 + 2*y^2) + 2*p2*x*y]
p1、p2是切向畸变系数。
重要提醒:畸变参数一共5个(k1, k2, k3, p1, p2),但实际标定时,k3和p2往往很小。我建议先标定k1、k2、p1,如果重投影误差还大,再加k3和p2。
3.3 张正友标定法
说到相机标定,就不得不提张正友标定法。这个方法牛在哪里?它只需要你拍几张棋盘格照片,就能同时标定出内参和畸变参数。
核心思想是这样的:
- 假设棋盘格是平面(Z=0),建立世界坐标系
- 通过角点检测,得到图像坐标
- 利用单应性矩阵,求解内参初值
- 用最大似然估计,优化所有参数
我曾经在一个项目中,用张正友法标定了20台相机,每台拍15张照片,总共花了3个小时。结果发现有一台相机的重投影误差特别大——后来排查发现,是棋盘格贴歪了。所以啊,标定板的平整度至关重要。
3.4 相机内参标定实战
好了,理论说完了,咱们来点实际的。下面是我常用的标定流程:
3.4.1 准备工作
- 打印一张棋盘格(建议A3纸,格子数9x6或12x9)
- 贴在平整的硬板上(我一般用亚克力板)
- 准备一个光照均匀的环境
避坑指南:我曾经用普通打印纸贴在三合板上,结果纸受潮变形了,标定出来的内参偏差很大。后来改用防水相纸+铝塑板,再也没出过问题。
3.4.2 采集图像
拍摄时要注意:
- 棋盘格要占画面1/4到1/2
- 角度要多样:左右倾斜、上下倾斜、旋转
- 距离要变化:近、中、远各拍几张
- 至少拍15-20张
我习惯用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 = [] # 世界坐标
imgpoints = [] # 图像坐标
# 遍历图像
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)
# 亚像素精确化
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)
3.4.3 结果评估
标定完成后,一定要检查重投影误差:
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像素就算不错了。如果超过1个像素,建议检查标定板是否平整,或者重新采集图像。
我的经验:标定完成后,最好用一张没参与标定的图像做验证。把棋盘格角点投影到图像上,看看偏差大不大。这一步能帮你发现很多隐藏问题。
3.5 常见问题与解决方案
| 问题 | 可能原因 | 解决方案 |
|---|---|---|
| 重投影误差大 | 标定板不平、图像模糊 | 换硬质标定板、提高快门速度 |
| 内参不稳定 | 图像数量不够、角度单一 | 增加图像、丰富角度 |
| 畸变校正后图像变形 | 畸变参数过拟合 | 减少畸变参数(只用k1、k2) |
| 角点检测失败 | 光照不均、棋盘格太小 | 均匀光照、增大棋盘格 |
好了,这一章的内容就到这里。相机标定看似简单,但要做好其实不容易。我建议你多动手实践,多拍几组数据对比一下。下一章咱们聊聊激光雷达的标定,那个更有意思。
记住一句话:标定做不好,融合全白搞。共勉!
公众号:蓝海资料掘金营,微信deep3321