第三节:相机内参与外参——内参矩阵、畸变系数、外参(旋转与平移)的物理意义
好,咱们接着聊。上一节我们把相机模型和坐标系理清了,这一节要啃的,是真正决定「图像怎么来的」和「怎么把它矫正回去」的核心参数——内参和外参。
说实话,我刚开始接触车载摄像头标定时,最头疼的就是这些矩阵和系数。总觉得它们是一堆冰冷的数字。直到有一次,我在实车上调试鸟瞰图拼接,发现左右两个摄像头的图像怎么都对不齐……折腾了两天,最后发现是外参里的平移向量写错了符号。嗯,从那以后,我对这些参数的物理意义就再也不敢马虎了。
一、内参矩阵——相机自己的「基因」
内参矩阵,说白了就是描述相机内部光学特性的。它决定了现实世界中的一个点,是怎么投影到图像传感器上的。
我习惯把内参矩阵写成这样:
K = [[fx, 0, cx],
[ 0, fy, cy],
[ 0, 0, 1]]
这里面每个参数都有明确的物理意义:
- fx, fy:焦距,单位是像素。你想想看,焦距越长,画面拉得越近,物体在图像上就越大。fx和fy通常很接近,但不完全相等——因为传感器像素不一定是正方形的。
- cx, cy:主点坐标,单位也是像素。理论上应该在图像正中心,但实际装配总有偏差。我在项目中遇到过一台摄像头,cx偏了将近10个像素,如果不矫正,鸟瞰图边缘会明显扭曲。
关键理解:内参矩阵把相机坐标系下的3D点 (Xc, Yc, Zc) 映射到图像像素坐标 (u, v)。公式很简单:
u = fx * Xc / Zc + cx
v = fy * Yc / Zc + cy
注意那个除以 Zc——这就是透视投影的核心,近大远小。
二、畸变系数——镜头不是完美的
理想情况下,镜头应该把直线投影成直线。但现实中的镜头,尤其是车载用的广角镜头,会产生畸变。我见过最夸张的,画面边缘的建筑物都弯成弧形了。
畸变主要分两类:
| 畸变类型 | 物理原因 | 表现 | 常用系数 |
|---|---|---|---|
| 径向畸变 | 镜头曲率不完美 | 直线变弯,像鱼眼效果 | k1, k2, k3 |
| 切向畸变 | 镜头与传感器不平行 | 画面倾斜、梯形失真 | p1, p2 |
为什么会这样?径向畸变是因为光线通过透镜边缘时折射角度更大,说白了就是镜头边缘的「放大率」和中心不一样。切向畸变则纯粹是装配公差——镜头装歪了。
我曾经调试过一个项目,畸变矫正后图像边缘还是有明显的「波浪感」。查了半天,发现是只用了k1和k2,忽略了k3。对于大广角镜头,k3其实很重要。避坑指南:如果镜头视场角超过120度,建议用5个畸变系数(k1,k2,k3,p1,p2)。
我的小技巧:标定完成后,先别急着用。把矫正后的图像显示出来,看看棋盘格的边缘是不是直线。如果还有弯曲,说明畸变系数没标准。我一般会多拍几组不同角度的标定板,取平均结果。
三、外参——相机在车上的「位置和姿势」
内参管的是相机自己怎么成像,外参管的是相机在车上的安装位置和朝向。外参由两部分组成:旋转矩阵 R 和平移向量 t。
3.1 旋转矩阵 R
旋转矩阵描述的是相机坐标系相对于车体坐标系的朝向。说白了,就是相机朝哪边看。
举个例子:前视摄像头通常朝车头方向,但实际安装时可能有俯仰角(pitch)、偏航角(yaw)和翻滚角(roll)。这三个角度组合起来,就构成了旋转矩阵。
R = Rz(yaw) * Ry(pitch) * Rx(roll)
我习惯用欧拉角来理解,但实际代码里都是用矩阵运算。注意顺序很重要——先转哪个轴,结果不一样。
警告:旋转矩阵必须是正交矩阵,即 R * R^T = I。如果标定出来的 R 不满足这个条件,说明标定有问题。我曾经遇到过标定程序输出非正交矩阵,导致鸟瞰图拼接时出现「撕裂」现象。
3.2 平移向量 t
平移向量描述的是相机光心在车体坐标系下的位置。单位通常是米。
比如:
t = [x, y, z]^T
其中:
- x:左右方向,正方向通常指向车辆右侧
- y:前后方向,正方向通常指向车头
- z:上下方向,正方向通常指向上方
我记得有一次,同事把平移向量的符号搞反了,结果鸟瞰图里车辆显示在道路外面。你想想看,如果相机装在车顶,z应该是正值,结果写成负值,那整个画面就翻到地底下去了。
四、内参和外参怎么配合使用?
把内参和外参结合起来,就得到了完整的投影公式:
s * [u, v, 1]^T = K * [R | t] * [Xw, Yw, Zw, 1]^T
其中:
- s 是尺度因子(深度)
- K 是内参矩阵
- [R | t] 是外参矩阵
- [Xw, Yw, Zw] 是世界坐标系下的3D点
这个公式,就是整个视觉几何的基石。鸟瞰图生成、车道线检测、障碍物测距……全都离不开它。
实战建议:在写代码时,我习惯把内参和外参分开存储。内参是相机本身的属性,换到另一辆车上通常不变。外参则每辆车都不一样,甚至同一辆车重新安装摄像头后也要重新标定。
五、一个小例子——用OpenCV读取标定结果
假设你已经用棋盘格标定好了相机,得到了内参和畸变系数。下面这段代码展示了怎么用它们做畸变矫正:
import cv2
import numpy as np
# 假设这是标定得到的内参和畸变系数
K = np.array([[718.856, 0, 607.193],
[0, 718.856, 185.217],
[0, 0, 1]], dtype=np.float32)
dist = np.array([-0.286, 0.081, 0.001, -0.002, -0.019], dtype=np.float32)
# 读取一张畸变图像
img = cv2.imread('distorted.jpg')
h, w = img.shape[:2]
# 获取矫正后的映射矩阵
new_K, roi = cv2.getOptimalNewCameraMatrix(K, dist, (w, h), 1, (w, h))
# 执行畸变矫正
undistorted = cv2.undistort(img, K, dist, None, new_K)
# 裁剪掉矫正后产生的黑边
x, y, w_roi, h_roi = roi
undistorted = undistorted[y:y+h_roi, x:x+w_roi]
cv2.imwrite('undistorted.jpg', undistorted)
这段代码我用了无数次。注意那个 getOptimalNewCameraMatrix 函数——它可以根据你的需求调整矫正后的视野范围。参数 1 表示保留全部视野,但边缘会有黑边。如果你不想看到黑边,可以改成 0,但会损失一些边缘像素。
避坑指南:我曾经直接用 cv2.undistort 而不指定 new_K,结果矫正后的图像比原图小了一圈,导致后续的鸟瞰图坐标全部偏移。记住:一定要用 getOptimalNewCameraMatrix 来调整内参。
六、总结一下
内参和外参,说白了就是回答两个问题:
- 内参:相机自己怎么「看」世界的?——焦距、主点、畸变
- 外参:相机在车上「站」在哪、朝哪看?——旋转、平移
这两个东西搞清楚了,后面的鸟瞰图生成、车道线检测,就都是数学推导和代码实现的问题了。下一节,我们会用这些参数,真正开始做鸟瞰图变换。
嗯,今天就到这里。记住:标定参数是视觉系统的「地基」,地基不稳,上面盖什么都白搭。