2. 相机成像原理:针孔模型、内参矩阵、畸变校正

好,咱们进入正题。深度估计这事儿,说白了就是让车「看懂」距离。但在这之前,你得先搞明白相机是怎么「看」世界的。我刚开始做ADAS那会儿,总觉得相机成像就是个拍照,后来踩了坑才明白——不懂成像原理,你连标定都做不好。

2.1 针孔模型:最朴素的成像方式

相机成像,本质上是个几何投影问题。最简单的模型就是针孔模型。你想想看,一个暗箱,前面戳个小孔,光线穿过小孔在后面的感光元件上成像。嗯,就是这么朴素。

但这里有个关键点:小孔越小,图像越清晰,但进光量也越少。我在项目中遇到过用针孔相机做实验,结果光线暗一点,图像全是噪点,深度估计根本没法做。所以实际相机都用透镜来聚光,但数学模型还是基于针孔模型。

针孔模型的数学表达很简单:

// 三维世界点 (X, Y, Z) 投影到成像平面 (x, y)
x = f * X / Z
y = f * Y / Z

这里的 f 是焦距,Z 是物体到相机的距离。注意看,距离 Z 在分母上。这意味着什么?物体越远,成像越小。这就是深度估计的物理基础。

核心理解:单目深度估计之所以难,就是因为从单张图里,你没法直接知道 Z。一个近的小物体和一个远的大物体,在图像上可能一模一样。这就是所谓的「尺度模糊性」。

2.2 内参矩阵:从物理世界到像素坐标

刚才的公式给出的是物理坐标(毫米),但咱们最终要的是像素坐标(行列号)。这中间需要一个转换——内参矩阵。

内参矩阵长这样:

K = [fx,  0, cx
     0,  fy, cy
     0,   0,  1]

其中:

  • fx, fy:焦距在 x 和 y 方向上的像素尺度。说白了,就是焦距对应多少个像素。
  • cx, cy:光心在图像上的像素坐标。一般不是正中心,因为装配有误差。

我个人习惯把内参矩阵理解成「翻译官」——它把物理世界的长度翻译成像素。举个例子,fx=1000 意味着 1 毫米的物理长度,在图像上对应 1000 个像素。

避坑指南:我曾经在标定一台车载相机时,发现 fx 和 fy 差了好几十。一开始以为是标定板没放平,折腾了两天。后来才发现,是相机传感器的像素不是正方形。所以,千万别假设 fx = fy,除非你用的是高端工业相机。

完整的投影公式是:

// 齐次坐标形式
[u, v, 1]^T = K * [X/Z, Y/Z, 1]^T

// 展开后
u = fx * X/Z + cx
v = fy * Y/Z + cy

你看,这里还是有个 Z 在分母上。所以,单目相机本质上丢失了深度信息。你只能得到方向(X/Z, Y/Z),得不到绝对距离。

2.3 畸变校正:让直线变回直线

现实中的相机都有透镜,透镜就会引入畸变。最常见的两种:

  • 径向畸变:图像边缘的直线变弯了,像鱼眼效果。越靠近边缘越明显。
  • 切向畸变:透镜和成像平面不平行,导致图像倾斜。

我记得第一次做双目深度估计时,没做畸变校正就直接跑立体匹配。结果出来的深度图,边缘全是错的,车都变形了。后来才意识到——畸变不校正,深度估计就是扯淡

畸变校正的数学模型:

// 径向畸变(k1, k2, k3 是畸变系数)
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)

// 切向畸变(p1, p2 是畸变系数)
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]

这里 r 是像素到光心的距离。你想想看,离中心越远,畸变越大。所以车载相机一般把感兴趣区域放在图像中心,避开边缘畸变大的地方。

重要提醒:畸变校正不是一次性的。相机受到震动、温度变化,畸变参数会漂移。我建议每半年重新标定一次,尤其是装在车上的相机。我曾经见过一台车,跑了半年后,畸变参数变了 5%,深度估计误差直接翻倍。

2.4 标定流程:从理论到实践

理论说完了,咱们聊聊实操。标定内参和畸变系数,最常用的方法是张正友标定法。流程很简单:

  1. 打印一张棋盘格标定板
  2. 从不同角度拍 15-20 张照片
  3. 用 OpenCV 的 calibrateCamera() 函数计算

代码示例:

import cv2
import numpy as np

# 准备棋盘格角点坐标
objp = np.zeros((6*9, 3), np.float32)
objp[:, :2] = np.mgrid[0:9, 0:6].T.reshape(-1, 2)

# 存储所有图像的点
objpoints = []  # 世界坐标
imgpoints = []  # 像素坐标

# 遍历图像
for fname in images:
    img = cv2.imread(fname)
    gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
    ret, corners = cv2.findChessboardCorners(gray, (9, 6), 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)

个人经验:标定时,我习惯让标定板占图像面积的 1/3 到 1/2。太小了角点检测不准,太大了容易超出视野。另外,一定要保证标定板在图像边缘也有出现,否则边缘畸变校正不好。

2.5 总结:为什么这些对深度估计重要?

好,咱们捋一捋:

概念 对深度估计的影响
针孔模型 告诉你深度 Z 在分母上,单目天生缺尺度
内参矩阵 把物理坐标转像素坐标,是标定的核心输出
畸变校正 不校正的话,边缘深度误差能到 20% 以上

说白了,相机成像是深度估计的「地基」。地基没打好,上面盖的楼再漂亮也得塌。我见过太多人一上来就调网络、跑模型,结果效果不好,回头一看——标定参数都是错的。

嗯,这一章就到这儿。下一章咱们聊聊双目视觉的几何原理,那才是真正能算深度的硬核内容。