|
## 为什么要做相机标定?
我们所处的世界是**三维**的,而照片是**二维**的。相机可以看成一个函数:
```
输入:三维场景 → [ 相机函数 ] → 输出:二维灰度图
```
这个从 3D 到 2D 的过程**数学上不可逆**(丢掉了深度这一维)。**相机标定(Camera Calibration)** 的目的,就是找一个合适的数学模型,求出它的参数,让模型尽可能逼近真实成像过程。
### 标定的三大目标
| 目标 | 具体内容 | 工程意义 |
|------|----------|----------|
| 求内参 K | $f_x, f_y, c_x, c_y$ | 知道每个像素对应多少毫米,才能做测量 |
| 求畸变系数 | 径向 $(k_1,k_2,k_3)$ + 切向 $(p_1,p_2)$ | 矫正"桶形/枕形"变形,让直线变直 |
| 求外参 $(R,t)$ | 相机相对标定板的旋转和平移 | 双目视觉、视觉定位、SLAM |
### 一句话总结
**标定 = 给相机的"成像函数"建立精确数学模型,求出参数,逼近真实 + 反推世界。**
标定完成后,你手里有了一套"武器库":
| 参数 | 用途 |
|------|------|
| K 矩阵 | 单目视觉测距、3D 投影 |
| 畸变系数 | 图像去畸变(自动驾驶车道线检测必备) |
| 外参 $(R, t)$ | 双目立体匹配、手眼标定(机械臂抓取) |
|
|
## 小孔成像(针孔模型)原理
相机成像采用**针孔模型**,利用"光线直线传播"原理:
- 远处物体上的每一个点,向四周反射无数条光线;
- 由于光线直线传播,**只有一个方向的光线能穿过小孔**进入盒子;
- 这条光线在对面的感光传感器上形成一个光点,与物体上的点**一一对应、颜色一致**。
### 针孔模型的优缺点
- ✅ **优点**:模型简单、没有畸变,是相机成像的数学基础;
- ❌ **缺点**:图像很暗(每个点只有一条光线进入)。
加大孔径虽然提高亮度,却会让"一个点"变成"一个光斑",导致模糊。
### 理论最佳孔径
根据可见光波长,理论计算的针孔最佳直径约 **0.25mm**,对应光圈值约 **f/200**。实际相机用透镜组来兼顾亮度与清晰度。
### 成像示意图
```
3D 物体 小孔/透镜 虚拟像平面
P ─────────────── • ───────────── p'
│ (光心) │
└────────────── • ───────────── p ──┘ (感光芯片)
实际像平面
```
|
|
## 凸透镜:解决"暗"与"模糊"的矛盾
针孔相机太暗,加大孔径又模糊。**解决方案**:用凸透镜替代针孔。
### 凸透镜的作用
凸透镜能把物体上一点反射的**一小束光**重新汇聚成一个点:
- ✅ **增大亮度**:一束光(而非一条)进入相机;
- ✅ **恢复一一对应**:物体上一个点 → 像上一个点,不模糊。
这就是现代相机的基本工作原理。
### 代价:引入畸变
透镜的制造精度与组装工艺偏差,会让真实成像偏离理想针孔模型,产生**畸变**:
| 畸变类型 | 成因 | 典型表现 |
|----------|------|----------|
| **径向畸变** | 光线在远离透镜中心处比靠近中心处更弯曲 | 桶形(广角)、枕形(长焦) |
| **切向畸变** | 透镜与图像传感器平面不平行(安装偏差) | 图像相对"左下—右上"连线对称偏移 |
畸变属于**相机内参**的一部分,需要通过标定求出畸变系数后去畸变。
|
|
## 世界坐标系(World)
世界坐标系 $(X_W, Y_W, Z_W)$ 是目标物体位置的参考系,是**全局固定的参考基准**,用来描述"物体在哪里"。单位为长度单位(m)。
在双目视觉中世界坐标系有三大用途:
1. 标定时确定标定物的位置;
2. 作为系统参考系,给出两个相机相对世界坐标系的关系,从而求出相机之间的相对关系;
3. 作为重建得到三维坐标的容器,存放重建后物体的三维坐标。
> 🧠 **生活类比**:你说"天安门广场的纪念碑位于北纬 39.9°,海拔 45m"——这就是世界坐标系(客观存在)。
## 相机坐标系(Camera)
相机坐标系 $(X_C, Y_C, Z_C)$ 以相机的**光心**(凸透镜中心点)为原点,是相机站在自己角度衡量物体的坐标系:
- **$Z_C$ 轴**:与光轴平行,指向景深方向(镜头前方);
- **$X_C$ 轴**:通常指向图像水平方向;
- **$Y_C$ 轴**:通常指向图像垂直方向。
拍摄的物体需要在世界坐标系下,经**刚体变换**(旋转 + 平移)转到相机坐标系,然后再与图像坐标系发生关系。单位为长度单位(mm)。
> 🧠 **生活类比**:你拿出手机拍照,手机说"纪念碑在我的左前方 30°,距离 500m"——这就是相机坐标系(主观观察)。
|
|
## 图像坐标系(Image)
图像坐标系 $(x, y)$ 以**图像平面的中心**为坐标原点(光轴与成像平面的交点,即主点),单位是 **mm(物理单位)**。
- **$x$ 轴**:向右;
- **$y$ 轴**:向上(物理右手系)。
它描述成像过程中物体从相机坐标系到图像坐标系的**投影透射关系**,方便进一步得到像素坐标。
> 🧠 **类比**:胶片上有个点,距离胶片中心点(光轴)向右 5mm、向上 3mm——这就是图像坐标系。
## 像素坐标系(Pixel)
像素坐标系 $(u, v)$ 以**图像平面的左上角顶点**为原点,单位是 **pixel(离散单位)**。
- **$u$ 轴**:向右(列号);
- **$v$ 轴**:向下(行号,因为计算机从左上角逐行扫描)。
它描述物体成像后的像点在**数字图像**上的坐标,是我们真正从相机内读取到的信息。
> 🧠 **类比**:扫描仪把 1mm 对应成 100 个像素,规定图片左上角为 $(0,0)$。那个"向右 5mm、向上 3mm"的物理点,在像素坐标系下变成 $u=500, v=300$(从顶部向下数 300 行)。
### 图像→像素的转换公式
$$
u = \frac{x}{d_x} + u_0, \qquad v = \frac{y}{d_y} + v_0
$$
- $d_x, d_y$:每个像素在感光芯片上的物理宽/高(mm/pixel);
- $(u_0, v_0)$:主点(光轴中心)在像素坐标系下的位置。
|
|
## 四大坐标系终极对比表
| 比较维度 | 世界坐标系 | 相机坐标系 | 图像坐标系 | 像素坐标系 |
|----------|-----------|-----------|-----------|-----------|
| 英文简称 | World (W) | Camera (C) | Image (x,y) | Pixel (u,v) |
| 维度 | 3D | 3D | 2D | 2D |
| 单位 | 米/毫米(物理) | 米/毫米(物理) | 毫米(物理) | 像素(离散) |
| 原点 | 人为设定的参考点 | 相机光心 | 光轴与芯片交点(主点) | 图像左上角 |
| 轴向 | 自定义(通常 Z 向上) | Z 指景深,Y 向下(OpenCV) | X 向右,Y 向上 | X 向右,Y 向下 |
| 是否依赖相机 | ❌ 全局基准 | ✅ 相机位姿决定 | ✅ 相机内参决定 | ✅ 内参+分辨率 |
| 典型值 | (5m, 3m, 1.5m) | (2m, 1.2m, 8m) | (3.2mm, 2.1mm) | (640, 360) |
### 完整成像链路总图
```
世界坐标系 (Xw,Yw,Zw) [米/毫米]
↓ 外参变换(刚体变换: 旋转 R + 平移 t)
相机坐标系 (Xc,Yc,Zc) [米/毫米]
↓ 透视投影(小孔成像: 除以深度 Zc)
图像坐标系 (x, y) [毫米]
↓ 内参变换(缩放 + 平移)
像素坐标系 (u, v) [像素]
```
### 核心公式速查
| 步骤 | 公式 | 输入→输出 |
|------|------|-----------|
| 外参 | $P_C = R \cdot P_W + t$ | 世界→相机 |
| 投影 | $x = f \cdot X_C / Z_C,\ y = f \cdot Y_C / Z_C$ | 相机→图像 |
| 内参 | $u = x/d_x + u_0,\ v = y/d_y + v_0$ | 图像→像素 |
### 记忆口诀
> **世界是舞台,相机是眼睛,图像是底片,像素是网格。**
> **外参走位,内参缩放,除以深度,二维到手。**
|
|
## 易混淆的关键点辨析
### 1. Y 轴方向:朝上还是朝下?
| 坐标系 | Y 轴方向 | 原因 |
|--------|----------|------|
| 世界坐标系 | 通常朝上(右手定则) | 物理约定 |
| 相机坐标系(OpenCV) | **朝下** | 为了与图像像素坐标系对齐 |
| 图像坐标系(物理底片) | **朝上** | 物理右手系 |
| 像素坐标系(数字图像) | **朝下** | 计算机从左上角逐行扫描 |
> ⚠️ 这导致从世界到相机不仅是位移,还涉及坐标轴翻转。把"向上 3mm"映射成"从顶部向下数 N 行"。
### 2. 世界与相机坐标系之间没有缩放
两个 3D 坐标系之间**尺度一致**,只有**旋转 R 和平移 t**(刚体变换):
$$
P_C = R \times P_W + T
$$
### 3. 何时 $Z_W = 0$?
标定时通常假定标定板所在平面过世界原点且垂直于 $Z_W$ 轴($Z_W = 0$),目的是方便后续计算(消去 $r_3$ 列)。
### 4. 像素的"离散化"逻辑
公式里的 $(u, v)$ 是像素块**中心点**的整数坐标。物理层面光是连续打在芯片上的,但数学层面我们为了用矩阵公式计算,强行将连续坐标映射到离散整数像素——这就是"采样"的核心思想。
|
|
## 变换一:世界 → 相机(刚体变换 / 外参)
世界坐标系到相机坐标系**不会产生形变**,只进行**旋转 R 和平移 t**,这就是**刚体变换**。R 和 t 统称为**相机外参(extrinsic parameters)**。
$$
\begin{bmatrix} X_C \\ Y_C \\ Z_C \end{bmatrix} = R \begin{bmatrix} X_W \\ Y_W \\ Z_W \end{bmatrix} + t
$$
### 旋转矩阵 R
$R$ 是 3×3 的**正交单位矩阵**,有 3 个自由度(绕 x/y/z 三轴):
$$
R = \begin{bmatrix} r_{00} & r_{01} & r_{02} \\ r_{10} & r_{11} & r_{12} \\ r_{20} & r_{21} & r_{22} \end{bmatrix}
$$
**性质**:
- $R^T R = I$(正交性);
- $\det(R) = 1$(保持手性);
- $r_{ij}$ 表示相机第 $i$ 轴与世界第 $j$ 轴夹角的余弦。
以绕 z 轴旋转 $\theta$ 为例:
$$
R_z(\theta) = \begin{bmatrix} \cos\theta & -\sin\theta & 0 \\ \sin\theta & \cos\theta & 0 \\ 0 & 0 & 1 \end{bmatrix}
$$
### 平移向量 t
$t = [T_X, T_Y, T_Z]^T$ 描述相机光心在世界坐标系中的位置,可理解为两个坐标原点之间的距离。
### 外参的齐次形式
$$
\begin{bmatrix} X_C \\ Y_C \\ Z_C \\ 1 \end{bmatrix} = \underbrace{\begin{bmatrix} R & t \\ 0^T & 1 \end{bmatrix}}_{M_{ext}\,(4\times4)} \begin{bmatrix} X_W \\ Y_W \\ Z_W \\ 1 \end{bmatrix}
$$
> 📌 **外参共 6 个自由度**(旋转 3 + 平移 3),**随相机位姿改变**,与相机硬件无关。
|
|
## 变换二:相机 → 图像(透视投影)
从相机坐标系(3D)到图像坐标系(2D)是**透视投影**,基于三角形相似定理:
$$
x = f \cdot \frac{X_C}{Z_C}, \qquad y = f \cdot \frac{Y_C}{Z_C}
$$
其中 $f$ 是焦距(毫米)。
### 为什么"除以 $Z_C$"是灵魂?
**除以深度 $Z_C$** 是透视投影的核心——它解释了"近大远小":
- 远处物体 $Z_C$ 大 → 投影变小;
- 近处物体 $Z_C$ 小 → 投影变大。
### 齐次矩阵形式
$$
Z_C \begin{bmatrix} x \\ y \\ 1 \end{bmatrix} = \begin{bmatrix} f & 0 & 0 & 0 \\ 0 & f & 0 & 0 \\ 0 & 0 & 1 & 0 \end{bmatrix} \begin{bmatrix} X_C \\ Y_C \\ Z_C \\ 1 \end{bmatrix}
$$
由于齐次坐标的伸缩不变性,$Z_C[x,y,1]^T$ 与 $(x,y,1)^T$ 表示同一点。
### 物理直觉
想象一束光从物体点 $P$ 出发,穿过光心,打在虚拟像平面上。利用三角形相似($\triangle ABO_c \sim \triangle oCO_c$),就能得到上面的投影公式。
|
|
## 变换三:图像 → 像素(缩放 + 平移 / 内参)
图像坐标系(毫米)到像素坐标系(像素),通过**像素物理尺寸 $d_x, d_y$** 和**主点偏移 $(u_0, v_0)$** 建立联系:
$$
u = \frac{x}{d_x} + u_0, \qquad v = \frac{y}{d_y} + v_0
$$
### 内参矩阵 K
把缩放和平移打包成一个 3×3 上三角矩阵:
$$
K = \begin{bmatrix} f_x & 0 & u_0 \\ 0 & f_y & v_0 \\ 0 & 0 & 1 \end{bmatrix}
$$
其中 $f_x = f/d_x,\ f_y = f/d_y$ 称为**等效焦距**(单位:像素)。
### K 矩阵中 4 个核心参数
| 符号 | 名称 | 物理意义 | 典型值(1280×720) |
|------|------|----------|------|
| $f_x$ | X 轴等效焦距 | $f \times (1/d_x)$,用像素个数表示焦距 | ~800–1200 |
| $f_y$ | Y 轴等效焦距 | $f \times (1/d_y)$ | ~800–1200 |
| $u_0$ | 主点 X 坐标 | 光轴中心在图像上的列号 | ~640 |
| $v_0$ | 主点 Y 坐标 | 光轴中心在图像上的行号 | ~360 |
### 两个"为什么"
**为什么 $f_x \neq f_y$?** 像素不是正方形(宽高比≠1)或芯片安装略有倾斜,两者偏差通常 <2%。
**为什么 $(u_0,v_0)$ 不在图像正中央?** CMOS 封装有微小平移(±10–30 像素)、镜头光轴与芯片法线不完全重合(±5–20 像素)。
|
|
## 合并为一步:投影矩阵 P
把三类变换串联相乘,得到从世界坐标到像素坐标的**一步到位**公式:
$$
s \begin{bmatrix} u \\ v \\ 1 \end{bmatrix} = \underbrace{K}_{内参} \cdot \underbrace{\begin{bmatrix} R & t \end{bmatrix}}_{外参} \cdot \begin{bmatrix} X_W \\ Y_W \\ Z_W \\ 1 \end{bmatrix}
$$
其中 $s$ 是尺度因子(齐次坐标)。定义**投影矩阵 P**:
$$
P = K \cdot [R \mid t], \qquad \begin{bmatrix} u \\ v \\ 1 \end{bmatrix} \sim P \begin{bmatrix} X_W \\ Y_W \\ Z_W \\ 1 \end{bmatrix}
$$
其中 $\sim$ 表示齐次坐标的等价关系(可相差一个常数因子)。
### 完整展开(矩阵乘法)
$$
\begin{bmatrix} u \\ v \\ 1 \end{bmatrix} = \frac{1}{Z_C} \begin{bmatrix} f_x & 0 & u_0 \\ 0 & f_y & v_0 \\ 0 & 0 & 1 \end{bmatrix} \begin{bmatrix} r_{00} & r_{01} & r_{02} & t_x \\ r_{10} & r_{11} & r_{12} & t_y \\ r_{20} & r_{21} & r_{22} & t_z \end{bmatrix} \begin{bmatrix} X_W \\ Y_W \\ Z_W \\ 1 \end{bmatrix}
$$
### 实际工程注意
1. **畸变在前,K 在后**:K 矩阵本身**不包含畸变**,畸变是单独的一套系数 $(k_1,k_2,k_3,p_1,p_2)$。正确顺序是先在归一化坐标上加畸变,再用 K 投影到像素。
2. **分辨率变了,K 要跟着变**:若标定时 1920×1080,现在用 640×480 缩放图,$f_x, f_y, u_0, v_0$ 都要等比例缩放。
### 应用场景
- **3D 重建**:从多张图像恢复场景的 3D 结构;
- **物体定位**:确定图像中物体的 3D 位置;
- **增强现实(AR)**:将虚拟物体叠加到真实场景;
- **自动驾驶**:车道线检测、障碍物定位;
- **机器人视觉**:视觉 SLAM、导航。
|
|
## 畸变从哪来?
相机拍照时通过**透镜**把实物投影到像平面。理想情况下光线直线传播、落在"完美位置",但**透镜的物理特性**会引入两类偏差:
| 畸变类型 | 成因 | 典型表现 |
|----------|------|----------|
| **径向畸变**(Radial) | 光线在远离透镜中心处比靠近中心处更弯曲 | 桶形(广角)、枕形(长焦) |
| **切向畸变**(Tangential) | 透镜与图像传感器(CMOS)平面不平行(安装偏差) | 图像相对"左下—右上"连线对称偏移 |
> 📌 畸变属于**相机内参**的一部分(与相机硬件绑定,不随位姿改变),但通常单独列出,称为**畸变系数**。
## 畸变示意图
```
[ 正常无畸变 ] [ 桶形畸变 ] [ 枕形畸变 ]
┌─────────┐ ╭─────────╮ ┌─────────┐
│ │ │ │ │ │ │ ) ) ) ) │ │ / / / / │
│ │ │ │ │ │ │ ) ) ) ) │ │ / / / / │
│ │ │ │ │ │ │ ) ) ) ) │ │ / / / / │
└─────────┘ ╰─────────╯ └─────────┘
网格为直线 边缘向内缩 边缘向外涨
```
## 5 个畸变系数总览
径向 + 切向畸变模型一共有 **5 个畸变参数**,在 OpenCV 中固定顺序为:
$$
dist = [k_1,\ k_2,\ p_1,\ p_2,\ k_3]
$$
> ⚠️ 顺序不能搞错:前两个是径向 $k_1,k_2$,中间两个是切向 $p_1,p_2$,最后一个是径向 $k_3$。这是 `cv.calibrateCamera()` 输出 `distCoeffs` 的标准顺序。
|
|
## 径向畸变(Radial Distortion)
径向畸变**沿着透镜半径方向分布**,在普通镜头(尤其广角)中表现明显:
- **桶形畸变(Barrel)**:$k_1 < 0$,边缘点被"拉向"中心,直线向外弯成桶状,常见于广角镜头;
- **枕形畸变(Pincushion)**:$k_1 > 0$,边缘点被"推离"中心,直线向内弯成枕状,常见于长焦/变焦远摄端。
### 特点
径向畸变以光心为中心往外延伸,**越往外畸变越大**,与距离呈**非线性**关系。在光心附近几乎没有偏移。
### 数学模型(多项式近似)
$$
x_{corrected} = x \cdot (1 + k_1 r^2 + k_2 r^4 + k_3 r^6)
$$
$$
y_{corrected} = y \cdot (1 + k_1 r^2 + k_2 r^4 + k_3 r^6)
$$
其中:
- $(x, y)$ 是畸变点在像平面的**归一化坐标**(除以 $Z_C$ 后);
- $r^2 = x^2 + y^2$,到图像中心的距离平方;
- $k_1, k_2, k_3$ 是**径向畸变系数**。
### 系数的工程经验
| 系数 | 物理作用 | 典型值范围 | 影响判断 |
|------|----------|-----------|----------|
| $k_1$ | 主阶径向畸变 | -0.3 ~ +0.3 | $k_1<0$:桶形;$k_1>0$:枕形;$\|k_1\|<0.01$:可忽略 |
| $k_2$ | 二阶修正 | -0.1 ~ +0.1 | 修正 $k_1$ 在大畸变区的偏差 |
| $k_3$ | 三阶修正 | -0.1 ~ +0.1 | 主要针对鱼眼/超大畸变 |
> 🟢 **经验法则**:普通镜头用 $k_1, k_2$ 两项即可;鱼眼/超广角镜头需加 $k_3$。
|
|
## 切向畸变(Tangential Distortion)
切向畸变**几乎都由装配误差**导致——透镜被粘贴到镜头模组上时,与 CMOS 传感器平面存在微小夹角,导致成像位置整体偏移。
```
[ 零切向畸变 ] [ 有切向畸变 ]
Lens ║ sensor Lens ╲ sensor
(平行) (有夹角)
```
### 数学模型
$$
x_{corrected} = x + \big[2 p_1 x y + p_2(r^2 + 2x^2)\big]
$$
$$
y_{corrected} = y + \big[p_1(r^2 + 2y^2) + 2 p_2 x y\big]
$$
其中 $p_1, p_2$ 是**切向畸变系数**,典型值范围 -0.01 ~ +0.01。
> 🟢 **判断标准**:$|p_1|, |p_2|$ 都远小于 0.01 → 镜头组装良好;超过 0.05 → 需检查镜头/传感器安装。
## 完整畸变 + 投影流程(关键顺序)
课件特别强调了一个**易错点**:**畸变在前,K 在后**。
```
1. 拿到归一化点 (x, y) = (Xc/Zc, Yc/Zc)
↓
2. 计算畸变偏移量(径向 + 切向),加到 (x, y) → (x', y')
↓
3. 用内参矩阵 K 乘 → 得到最终像素坐标 (u, v)
```
## 去畸变方法
### 方法 1:`cv.undistort()`(简单)
```python
newcameramtx, roi = cv2.getOptimalNewCameraMatrix(mtx, dist, (w,h), 1, (w,h))
dst = cv2.undistort(img, mtx, dist, None, newcameramtx)
```
### 方法 2:映射表 + remap(适合实时视频流)
```python
map1, map2 = cv2.initUndistortRectifyMap(mtx, dist, None, newcameramtx, (w,h), cv2.CV_32FC1)
dst = cv2.remap(img, map1, map2, cv2.INTER_LINEAR)
```
## 为什么相机出厂不提前标定好?
| 原因 | 说明 |
|------|------|
| ① 个体差异 | 同型号镜头因镀膜/研磨/组装差异,每个畸变都不同 |
| ② 装配误差 | 镜头与 CMOS 相对位置每台都有细微差异 |
| ③ 温度影响 | 玻璃热胀冷缩、折射率变化,畸变系数漂移 |
| ④ 焦距可变 | 变焦镜头不同焦距畸变特性完全不同 |
| ⑤ 成本控制 | 逐个精密标定需要昂贵设备和时间 |
| ⑥ 场景差异 | 机器视觉需亚像素精度,手机拍照由 ISP 轻度矫正即可 |
> 📌 智能手机 ISP 用厂商预设的**通用矫正表**,对日常拍照够用,对精确 3D 重建远远不够。**必须用自己的相机重新标定。**
|
|
## 相机标定方法的三大分类
| 方法 | 原理 | 优点 | 缺点 | 代表算法 |
|------|------|------|------|----------|
| **传统标定法** | 基于已知标定物(棋盘格/圆点),利用物点-像点对应求参 | 精度高、鲁棒性强 | 需要标定物、流程繁琐 | Tsai、DLT、**张氏标定法** |
| **自标定法** | 不用标定物,依靠图像点之间的约束直接求参 | 灵活、无需标定物 | 鲁棒性差、精度低 | Kruppa 方程;适合低精度场合 |
| **主动视觉标定法** | 控制相机做已知运动(纯平移/绕光心旋转) | 算法简单、往往线性可解 | 设备昂贵、条件苛刻 | 精密云台相机 |
### 传统标定法对标定物的要求
1. 标定物特征部分与周围环境差别大,**特征容易分辨且提取方便**;
2. 标定物**稳定性高**,特征不随相机位置变换产生畸变;
3. 常见模板:**棋盘格、圆形、三角形**等。
> 🟢 **工业界主流**:传统标定法 + 棋盘格 + 张氏标定法。OpenCV 的 `cv.calibrateCamera()` 底层就是张氏法。
## 为什么选棋盘格?
1. **角点易检测**:黑白交界的角点有非常稳定的数学特征(梯度最大点);
2. **已知几何关系**:所有角点都在同一平面($Z_W=0$),相邻角点物理距离已知(如格子边长 30mm);
3. **覆盖全视野**:通过旋转/倾斜棋盘格,让光线从不同角度进入镜头,便于求解所有未知数。
|
|
## 张正友标定法(Zhang's Method)
张正友博士 1999 年在 ICCV 发表《*Flexible Camera Calibration By Viewing a Plane From Unknown Orientations*》,提出利用平面棋盘格进行相机标定的实用方法。
### 核心特点
- 只需一块**平面棋盘格**(成本低、易获取);
- **不需要知道棋盘格的运动**(标定板姿态未知);
- 从**不同角度**拍摄多张照片即可求解;
- 兼顾精度与灵活性,被 OpenCV/MATLAB 采用。
### 两步法精髓
**第一步:忽略畸变,求初始解(线性求解)**
假设没有畸变,从 $(X_W, Y_W, 0)$ 到 $(u,v)$ 的映射:
$$
s \begin{bmatrix} u \\ v \\ 1 \end{bmatrix} = K \begin{bmatrix} R & t \end{bmatrix} \begin{bmatrix} X_W \\ Y_W \\ 0 \\ 1 \end{bmatrix}
$$
因为 $Z_W=0$,第 3 列旋转向量 $r_3$ 可消掉,变成 **3×3 单应性矩阵 H**:
$$
H = K \begin{bmatrix} r_1 & r_2 & t \end{bmatrix}
$$
- 每张图提供一个 $H$(8 个自由度);
- 利用 $R$ 是旋转矩阵的性质($r_1, r_2$ 正交且单位长),每张图可列 2 个约束;
- **至少 2 张图**可求解,工程上通常拍 **15–20 张**保证稳定性;
- 用最小二乘法先求 K 初始值,再反推每张图的 $R, t$。
**第二步:加入畸变,非线性优化(精炼)**
把第一步求出的 $K, R, t, k_1, k_2, p_1, p_2$ 作为初始值,交给 **Levenberg-Marquardt 算法**,让**重投影误差**最小化:
$$
\text{重投影误差} = \frac{1}{N} \sum_i \| p_{detected}^{(i)} - p_{reprojected}^{(i)} \|
$$
即"检测到的角点"与"用标定参数反投影回去的角点"之间的像素距离。
|
|
## OpenCV 标定全流程(Python)
```python
import cv2
import numpy as np
import glob
# 1. 标定板物理尺寸
CHECKERBOARD = (9, 6) # 内角点数:横向 9,纵向 6
square_size = 25.0 # 格子边长 25mm
# 2. 生成角点世界坐标(Zw=0)
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 像素点
# 3. 读取棋盘格图片(至少 15 张不同角度)
for fname in glob.glob('calibration_images/*.jpg'):
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)
# 4. 执行标定
ret, mtx, dist, rvecs, tvecs = cv2.calibrateCamera(
objpoints, imgpoints, gray.shape[::-1], None, None)
print("内参矩阵 K:\n", mtx)
print("畸变系数 [k1,k2,p1,p2,k3]:\n", dist)
print("重投影误差:", ret)
# 5. 去畸变
h, w = img.shape[:2]
newcameramtx, roi = cv2.getOptimalNewCameraMatrix(mtx, dist, (w,h), 1, (w,h))
dst = cv2.undistort(img, mtx, dist, None, newcameramtx)
x, y, w2, h2 = roi
dst = dst[y:y+h2, x:x+w2]
cv2.imwrite('calibrated_result.jpg', dst)
```
### 返回值详解
| 返回值 | 形状 | 物理含义 | 单位 |
|--------|------|----------|------|
| `ret` | 标量 | **重投影误差**(平均像素误差) | 像素 |
| `mtx` | 3×3 | **内参矩阵 K** | 像素 |
| `dist` | 1×5 | **畸变系数** $[k_1,k_2,p_1,p_2,k_3]$ | 无量纲 |
| `rvecs` | N×3 | 每张图的**旋转向量**(Rodrigues 轴角) | 弧度 |
| `tvecs` | N×3 | 每张图的**平移向量** | mm/m |
> 🟢 `rvecs[i]` 是 Rodrigues 向量:**方向=旋转轴,模长=旋转角度**。用 `cv2.Rodrigues(rvecs[i])` 可转为旋转矩阵。
|
|
## 标定质量评估:重投影误差
$$
ret = \frac{1}{N} \sum_i \| p_{detected}^{(i)} - p_{reprojected}^{(i)} \|
$$
| ret 值 | 标定质量 | 工程建议 |
|--------|----------|----------|
| < 0.05 像素 | 优秀 | 可用于高精度测量 |
| 0.05 ~ 0.15 像素 | 良好 | 适用于大多数应用 |
| 0.15 ~ 0.5 像素 | 合格 | 需检查图片质量/角点检测 |
| > 0.5 像素 | 不合格 | 需重新拍摄标定图片 |
## 6 个避坑指南(血泪经验)
| 问题 | 后果 | 正确做法 |
|------|------|----------|
| 棋盘格照片太少 | 标定不稳定,误差大 | 至少拍 **15–20 张**,覆盖中心和四角 |
| 棋盘格都平行于镜头 | 无法求解焦距和主点 | 要**倾斜**(俯仰/偏航) |
| 格子太小或太大 | 角点检测不准 | 格子边长占图像宽度 **1/10 ~ 1/5** |
| 环境光照不均匀 | 角点检测失败 | 使用**漫反射光源** |
| 忽略畸变矫正 | 3D 重建出现"鱼眼"效应 | 必须用 `undistort()` 处理后续图片 |
| 分辨率前后不一致 | K 矩阵失效 | 标定分辨率必须与推理**严格一致** |
## 初学者练手四步法
1. **打印棋盘格**,贴平在硬纸板上;
2. **手机固定焦距**(不要变焦),从不同角度拍 20 张;
3. **跑上面的代码**,打印 K 矩阵和畸变系数;
4. **故意拍一张带直线的图**(如门框),用 `undistort` 矫正,观察"弯变直"的瞬间!
## 真实标定数据(建立量级感)
| 参数 | 标定前(猜测) | 标定后(实际) | 说明 |
|------|-------------|-------------|------|
| $f_x$ | 1000 | 985.32 | 焦距像素表示,约 1.5% 误差 |
| $f_y$ | 1000 | 987.65 | 与 $f_x$ 差异反映像素非正方形 |
| $c_x$ | 640 | 632.14 | 主点不在图像正中央 |
| $c_y$ | 360 | 348.75 | 制造装配误差的体现 |
| $k_1$ | 0 | -0.175 | 明显的桶形畸变 |
| $p_1$ | 0 | 0.0003 | 切向畸变很小,镜头组装较好 |
|
|
## 单目测距:小孔成像 + 相似三角形
### 核心铁律
> **单目相机无法仅凭一张图测出绝对距离!** 必须借助一个"已知尺寸的参照物"。
3D 到 2D 丢失了深度,单张图无法恢复。但若有**已知尺寸**物体(如车高 1.5m),就能用相似三角形算出距离。
### 公式推导
设物体实际高度 $H$(米)、图像物理高度 $h$(毫米)、焦距 $f$(毫米)、深度 $Z$(米)。由相似三角形:
$$
\frac{h}{f} = \frac{H}{Z} \quad \Longrightarrow \quad Z = \frac{f \times H}{h}
$$
我们手里只有像素坐标,利用 $f_x = f/d_x$ 换算:
$$
Z = \frac{f \times H}{h} = \frac{(f_x \cdot d_x) \times H}{(\text{pixels} \cdot d_x)} = \frac{f_x \times H}{\text{pixels}}
$$
$d_x$ 被约掉!**最终测距公式**:
$$
\boxed{\text{Distance} = \frac{f_x \times \text{Real\_Height}}{\text{Pixel\_Height}}}
$$
### 手算案例
相机 $f_x = 800$ 像素,轿车真实高度 $H = 1.5$ 米,照片中像素高度 150 像素:
$$
Z = \frac{800 \times 1.5}{150} = \frac{1200}{150} = 8 \text{ 米}
$$
### 单目的局限性
| 场景 | 解决方法 |
|------|----------|
| 测行人 | 假设平均身高 1.7m(小孩/高跟鞋有误差) |
| 测车辆 | 查车型数据库(轿车 1.5m,SUV 1.7m,卡车 3.5m) |
| 测任意物体 | 改用**双目视觉**(不需要先验尺寸) |
> 🟢 **为什么用高度而非宽度?** 物体水平方向易被遮挡(车被树挡一半),竖直方向通常更稳定。
### 进阶:逆透视映射(IPM)
检测到人的脚底位置 $(u,v)$,结合地面平面假设($Y_C=0$):
$$
X_C = \frac{(u - u_0) \times Z}{f_x}, \qquad Y_C = \frac{(v - v_0) \times Z}{f_y}
$$
这在自动驾驶中叫 **IPM**,用于车道线距离测量。
|
|
## 单目测距 Python 完整代码
```python
import cv2
import numpy as np
class DistanceMeasurer:
def __init__(self, K_matrix, dist_coeffs, known_height=1.7):
"""known_height: 目标真实高度(米),默认人 1.7 米"""
self.K = K_matrix
self.dist = dist_coeffs
self.known_height = known_height
self.fx = K_matrix[0, 0] # 等效焦距(像素)
def detect_object_height(self, image, bbox):
"""模拟目标检测,实际可用 YOLO"""
h, w = image.shape[:2]
newcameramtx, roi = cv2.getOptimalNewCameraMatrix(
self.K, self.dist, (w, h), 1, (w, h))
# 必须去畸变!否则测距不准
undistorted = cv2.undistort(image, self.K, self.dist, None, newcameramtx)
x1, y1, x2, y2 = bbox
pixel_height = y2 - y1
cv2.rectangle(undistorted, (x1, y1), (x2, y2), (0, 255, 0), 2)
return pixel_height, undistorted
def calculate_distance(self, pixel_height):
if pixel_height <= 0:
return float('inf')
return (self.fx * self.known_height) / pixel_height
if __name__ == "__main__":
# 模拟标定结果(实际请用 calibrateCamera 生成)
K = np.array([[800, 0, 640],
[0, 800, 360],
[0, 0, 1]], dtype=np.float32)
dist = np.array([0.1, -0.05, 0, 0, 0], dtype=np.float32)
measurer = DistanceMeasurer(K, dist, known_height=1.7)
img = cv2.imread("test_person.jpg")
bbox = (300, 200, 400, 550) # x1, y1, x2, y2
pixel_h, result = measurer.detect_object_height(img, bbox)
distance = measurer.calculate_distance(pixel_h)
text = f"Distance: {distance:.2f} meters"
cv2.putText(result, text, (50, 50), cv2.FONT_HERSHEY_SIMPLEX,
1, (0, 0, 255), 2)
cv2.imshow("Distance", result)
cv2.waitKey(0)
print(f"像素高度: {pixel_h} px, 估计距离: {distance:.2f} 米")
```
### 让测距更准的 4 个细节
1. **必须去畸变**:畸变让物体边缘弯曲,像素高度计算偏差;
2. **目标检测要准**:bbox 必须刚好包住物体(推荐 YOLOv8);
3. **测距取帧稳定**:单帧会抖动,工程中用**卡尔曼滤波**平滑;
4. **只测底部触地点**:用脚底到头顶的像素高度,远距离脚底被遮挡时误差剧增。
|
|
## 双目视觉:像人眼一样的视差测距
### 为什么两只眼睛能测距?
想象伸出食指放在眼前 30cm,交替闭左眼/右眼,手指相对背景会"跳跃"——这就是**视差(Disparity)**。
- 物体越近 → 左右眼偏移越大(视差大);
- 物体越远 → 偏移越小(视差小);
- 物体在无穷远 → 视差为 0(左右眼看同一位置)。
### 核心公式:三角测量
```
左光心 Ol ──── 基线 B ──── 右光心 Or
\ /
\ /
\ /
P (X, Y, Z) ← 物体
```
由相似三角形推导:
$$
\boxed{Z = \frac{B \times f}{d}}
$$
- $B$:基线距离(两相机光心间距,米);
- $f$:焦距(像素,标定得到的 $f_x$);
- $d = x_{left} - x_{right}$:视差(同一物理点在左右图的水平像素差);
- $Z$:物体深度(距离,米)。
> 🟢 **双目最大优势**:完全不需要知道物体多大!直接由视差算深度。是自动驾驶、机器人导航、AR 的核心技术。
### 单目 vs 双目对比
| 对比维度 | 单目测距 | 双目测距 |
|----------|----------|----------|
| 需要先验尺寸 | ✅ 必须知道物体真实高度 | ❌ 不需要 |
| 硬件成本 | 低(1 个相机) | 高(2 个相机 + 同步) |
| 标定复杂度 | 简单 | 复杂(需立体矫正) |
| 计算量 | 极低 | 高(立体匹配消耗大) |
| 精度 | 依赖尺寸先验 | 近距离高,远距离下降 |
| 适用场景 | 监控、交通卡口 | 自动驾驶、机器人导航、AR |
|
|
## 双目视觉的四大核心步骤
```
左图 + 右图
↓
① 双目标定(求两相机内参 K、畸变,及相对外参 R、T)
↓
② 立体矫正(把左右图"拉平",行对齐,匹配点只在同一水平线)
↓
③ 立体匹配(计算每个像素的视差 d,生成视差图)
↓
④ 三角测量(用 Z = B·f/d 把视差图转为深度图)
```
### ① 双目标定(比单目多一步)
求左右相机内参 $K_{left}, K_{right}$ 及相对外参(右相机相对左相机的 $R$ 和 $T$)。用 `cv.stereoCalibrate()`。
**关键**:$T$ 的第一个元素 $T_x$ 就是**基线距离 $B$**!
### ② 立体矫正(Bouguet 算法)
不矫正时,匹配搜索是二维(极慢)。矫正后光轴平行、匹配点落在同一水平线(极线对齐),搜索从 $O(n^2)$ 降到 $O(n)$:
```python
R1, R2, P1, P2, Q, _, _ = cv2.stereoRectify(K_l, D_l, K_r, D_r, size, R, T, alpha=1)
map1_l, map2_l = cv2.initUndistortRectifyMap(K_l, D_l, R1, P1, size, cv2.CV_32FC1)
rect_l = cv2.remap(gray_l, map1_l, map2_l, cv2.INTER_LINEAR)
```
### ③ 立体匹配(最核心、最难)
| 算法类型 | 代表 | 速度 | 精度 | 适用场景 |
|----------|------|------|------|----------|
| 局部匹配 | BM | 极快 | 低 | 低算力、实时避障 |
| 半全局 | **SGBM** | 中 | 高 | **最常用(OpenCV 推荐)** |
| 全局 | Graph Cut/BP | 极慢 | 最高 | 离线高精度重建 |
```python
stereo = cv2.StereoSGBM_create(
minDisparity=0, numDisparities=128, blockSize=11,
P1=8*3*11**2, P2=32*3*11**2,
disp12MaxDiff=1, uniquenessRatio=10,
speckleWindowSize=100, speckleRange=32)
# 注意:输出除以 16 才是真实视差
disparity = stereo.compute(rect_l, rect_r).astype(np.float32) / 16.0
```
### ④ 三角测量(视差 → 深度)
```python
fx = K_left[0, 0]
depth = (B * fx) / disparity
depth[disparity <= 0] = 0 # 无效视差置 0
```
> ⚠️ **物理限制**:视差误差 1 像素,远距离会导致几十米偏差,所以双目远距离精度下降。
### 双目 6 个工程痛点
| 痛点 | 解决方法 |
|------|----------|
| 纹理缺失(白墙空洞) | 加结构光(如 iPhone FaceID)或深度学习补全 |
| 基线 B 太小 | 增大基线(汽车左右摄像头间距 20cm+) |
| 标定不精确 | 多拍不同距离棋盘格,重投影误差 < 0.1 像素 |
| 光照不一致 | 自动曝光/增益控制,或直方图均衡化 |
| 计算量大 | 降分辨率,或 FPGA/GPU 加速的 BM |
| 视差范围设错 | $\text{numDisparities} > B \cdot f / Z_{min}$ |
### 进阶:深度图 → 3D 点云
```python
points_3d = cv2.reprojectImageTo3D(disparity, Q)
# 或手动计算
X = (u - cx) * Z / fx
Y = (v - cy) * Z / fy
```
|
|
## 深度学习如何革新立体匹配
传统立体匹配(BM/SGBM)依赖人工设计特征,在弱纹理、重复纹理、遮挡区域表现差。深度学习的核心思想:**让神经网络从海量数据中学习如何寻找匹配点**。
### 1. RAFT-Stereo:追求极致精度(迭代精炼)
**核心思想**:借鉴光流估计的迭代精炼思路。先从零生成粗略视差图,然后通过基于 **GRU(门控循环单元)** 的"更新算子",反复读取图像特征,多轮修正,**越修越准**。
**技术亮点**:
- **轻量化代价体**:避免传统 3D 卷积,仅用 2D 卷积和矩阵乘法,能处理百万像素大图;
- **跨尺度 GRU**:在不同分辨率特征图上同时传递信息,更好处理无纹理区域。
**江湖地位**:精度之王。在 Middlebury、ETH3D 长期霸榜,跨数据集泛化能力极强。
### 2. HITNet:追求速度与精度平衡(层级分块细化)
**核心思想**:不走巨大 3D 代价体,采用"由粗到精、逐块优化"策略。
**工作流程**:
1. **快速初始化**:多分辨率上快速生成初始视差猜测;
2. **分块假设**:图像划分为小块(Tile),每块带可学习特征,假设块内视差符合"倾斜平面";
3. **迭代传播**:通过 2D 几何传播和图像变形(Warping),小块不断交换信息,细化到像素级。
**江湖地位**:速度之星。在 KITTI 榜单所有快于 100ms 的方法中,精度排名第一。
### 技术对比(KITTI 2015)
| 算法 | 核心策略 | 误差 (D1-all) | 运行耗时 | 设计初衷 |
|------|----------|--------------|----------|----------|
| RAFT-Stereo | 迭代精炼 | 1.82% | ~0.38s | 追求极致精度和泛化 |
| HITNet | 层级分块细化 | 2.32% | ~0.06s | 追求实时高性能 (<100ms) |
> 🟢 **选型**:不苛求实时、追求最佳效果 → RAFT-Stereo;必须跑在车上/嵌入式 → HITNet。
### 上手实践
- **开源实现**:GitHub 搜 `princeton-vl/RAFT-Stereo`;
- **合成数据集**:Scene Flow(提供完美视差真值,用于预训练);
- **真实数据集**:KITTI、ETH3D、Middlebury(微调/评估);
- **部署**:GPU 发挥最佳;嵌入式(Jetson Nano)需剪枝、量化。
|
|
## 双目测距小车实战项目
集成:相机标定 → 立体矫正 → 视差计算 → 深度图 → 目标追踪 → 视觉伺服 → 运动控制。
### 硬件清单(总成本约 ¥800–1500)
| 部件 | 型号建议 | 用途 | 参考价 |
|------|----------|------|--------|
| 双目摄像头 | 双 USB 模组(C270×2 或 ELP 双目) | 采集左右图 | ¥200–400 |
| 主控板 | Raspberry Pi 4B 或 Jetson Nano | 运行 OpenCV | ¥300–800 |
| 舵机云台 | 2 轴 SG90×2 | 控制摄像头朝向 | ¥50–100 |
| 小车底盘 | 4 轮带电机驱动 | 移动平台 | ¥150–300 |
| 电源 | 12V 锂电池组 | 供电 | ¥100–200 |
### 软件架构
```
┌──────────────────────────────────────┐
│ 主循环(10-15 FPS) │
├──────────────────────────────────────┤
│ 左图 │ 右图 │
│ ↓ │ ↓ │
│ 立体矫正(Remap) │
│ ↓ │
│ SGBM/深度学习匹配 → 视差图 │
│ ↓ │
│ 视差→深度图(Z = B·f/d) │
│ ↓ │
│ 寻找最近物体(深度图最小值区域) │
│ ↓ │
│ 舵机 PID → 转动云台对准物体 │
│ ↓ │
│ 小车 PID → 前进/后退保持固定距离 │
└──────────────────────────────────────┘
```
### 核心代码骨架(双目深度计算)
```python
class StereoCamera:
def __init__(self, calib_file):
data = np.load(calib_file)
self.K_l, self.K_r = data['K_left'], data['K_right']
self.D_l, self.D_r = data['D_left'], data['D_right']
self.R, self.T = data['R'], data['T']
self.size = data['image_size']
self.B = abs(self.T[0][0]) # 基线(米)
self._build_rectify_maps()
self.matcher = cv2.StereoSGBM_create(
minDisparity=0, numDisparities=128, blockSize=11,
P1=8*3*11**2, P2=32*3*11**2,
disp12MaxDiff=1, uniquenessRatio=10,
speckleWindowSize=100, speckleRange=32)
def compute_depth(self, img_l, img_r):
g_l = cv2.cvtColor(img_l, cv2.COLOR_BGR2GRAY)
g_r = cv2.cvtColor(img_r, cv2.COLOR_BGR2GRAY)
rl = cv2.remap(g_l, self.m1l, self.m2l, cv2.INTER_LINEAR)
rr = cv2.remap(g_r, self.m1r, self.m2r, cv2.INTER_LINEAR)
disp = self.matcher.compute(rl, rr).astype(np.float32) / 16.0
fx = self.K_l[0, 0]
depth = (self.B * fx) / disp
depth[disp <= 0] = 0
return depth, rl, rr
```
### 性能优化避坑
| 问题 | 解决方法 |
|------|----------|
| 帧率太低 (<5FPS) | 降分辨率到 320×240,`numDisparities=64` |
| 深度图空洞 | 升级 HITNet 或加结构光 |
| 舵机抖动 | 减小 P 增益,5 帧滑动平均滤波 |
| 摄像头不同步 | 硬件触发双目(如 Intel RealSense) |
| 夜间噪声大 | 红外/白光补光灯 |
### 升级路径
| 难度 | 升级内容 |
|------|----------|
| ⭐ | 深度学习替换 SGBM(HITNet/RAFT-Stereo) |
| ⭐⭐ | 加 YOLOv8 目标分类,只对特定类别测距 |
| ⭐⭐⭐ | SLAM 建图导航(RTAB-Map) |
| ⭐⭐⭐⭐ | DeepSORT 多目标追踪 + 避障规划 |
|
|
## 几何视觉三大进阶方向
### 方向对比
| 对比维度 | 视觉 SLAM | 三维重建 (SfM) | 深度几何学习 |
|----------|-----------|---------------|--------------|
| 核心任务 | 实时定位+建图 | 从照片重建 3D 模型 | 神经网络学几何推理 |
| 数学要求 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐ |
| 编程要求 | ⭐⭐⭐⭐ (C++) | ⭐⭐⭐ (Python) | ⭐⭐⭐⭐ (Python+CUDA) |
| 实时性 | 要求极高 | 不要求实时 | 视应用而定 |
| 就业热门度 | 🔥🔥🔥🔥🔥 | 🔥🔥🔥 | 🔥🔥🔥🔥🔥 |
| 典型公司 | 大疆、百度 Apollo | 阿里、商汤、影石 | OpenAI、英伟达、特斯拉 |
| 推荐工具 | ORB-SLAM3、VINS-Mono | COLMAP、Open3D、NeRF | MiDaS、RAFT、Depth Anything |
### 第一阶段:几何视觉深化(2 周)
| 知识点 | 具体内容 | 资源 |
|--------|----------|------|
| 对极几何 | 本质矩阵 E、基础矩阵 F、极线约束、八点法 | 《MVG》第 9 章 |
| 三角测量 | 从两视图匹配点恢复 3D 坐标 | OpenCV `triangulatePoints()` |
| PnP 问题 | 已知 3D-2D 对应,求相机位姿 | OpenCV `solvePnP()` |
| 光束平差 (BA) | 同时优化所有相机位姿和 3D 点 | g2o、Ceres |
| 本质矩阵分解 | 从 E 分解 R 和 t(四种解需筛选) | OpenCV `recoverPose()` |
### 《视觉SLAM十四讲》核心章节地图
| 章节 | 重点 | 你的优势 |
|------|------|----------|
| 第 2 讲:初识 SLAM | 经典框架(前端/后端/回环/建图) | 有坐标系基础 |
| 第 3 讲:三维刚体运动 | 旋转矩阵、四元数、Eigen 库 | **完美衔接** |
| 第 4 讲:李群李代数 | 为何引入(对旋转求导做优化) | 理解目的即可 |
| 第 5 讲:相机与图像 | 针孔/畸变模型、双目 | **主场作战** |
| 第 6 讲:非线性优化 | 状态估计、最小二乘、Ceres/g2o | 抓住"建模"主线 |
### 通用技能树
| 技能 | 内容 | 资源 |
|------|------|------|
| 线性代数 | 矩阵求导、SVD、最小二乘 | 3Blue1Brown |
| 李群/李代数 | SO(3)、SE(3) 扰动模型 | 《十四讲》第 3 讲 |
| 数值优化 | 高斯牛顿、LM、稀疏矩阵 | Ceres 教程 |
| 概率与滤波 | 卡尔曼滤波、粒子滤波 | 《概率机器人》 |
| 编程语言 | Python + C++(工业级必须) | C++ Primer |
| 工具库 | OpenCV、Eigen、Pangolin、ROS | 官方文档 |
### 推荐学习路线(性价比最高)
- **Step 1(1 月)**:精读《十四讲》前 6 讲,练熟 OpenCV 和 Eigen;
- **Step 2(2 月)**:TUM 跑 ORB-SLAM3 + 手机拍照跑 COLMAP + 预训练 MiDaS;
- **Step 3(3–6 月)**:选一个方向,啃透 3–5 篇经典论文,逐行复现;
- **Step 4(无限期)**:结合硬件做完整应用,开源到 GitHub。
## 相机标定在 AI 知识体系中的位置
```
AI / 计算机视觉
│
┌───────────────┼───────────────┐
│ │ │
2D 视觉 3D 视觉 深度学习视觉
(分类/检测) (本系列主题) (单目深度/光流)
│
┌──────────┼──────────┐
│ │ │
SLAM 三维重建 测距/感知
(定位+建图) (SfM+MVS) (自动驾驶)
```
**相机标定是整个 3D 视觉的地基**——没有精确的内外参,所有三维重建、测距、SLAM、AR 应用都是空中楼阁。
|
|
## 项目总览:这是什么代码?
这是一份用 **OpenCV + 传统计算机视觉**实现的**车道线检测**项目(经典 Udacity 自动驾驶车道线作业),输入行车视频,输出标注了车道区域、曲率半径、车辆偏离距离的视频。
### 目录结构
```
laneDection/
├── main.py # 全部代码(标定+检测+视频处理)
├── camera_cal/ # 20 张棋盘格标定图 calibration1~20.jpg
├── test/ # 测试图片(直道、弯道)
├── project_video.mp4 # 输入视频
└── output.mp4 # 输出视频
```
### 核心处理流程(8 步流水线)
```
输入帧 ──①去畸变──> ②颜色+边缘提取 ──③透视变换(鸟瞰图)──> ④直方图定位车道
──⑤滑动窗口──> ⑥二次多项式拟合 ──⑦逆透视回投+填充 ──⑧算曲率/偏离──> 输出帧
```
### main.py 中的函数清单
| 函数 | 作用 | 对应流程 |
|------|------|----------|
| `cal_calibrate_params()` | 棋盘格标定,求内参+畸变系数 | ①前导 |
| `img_undistort()` | 图像去畸变 | ① |
| `pipeline()` | HLS颜色 + Sobel边缘 → 二值图 | ② |
| `cal_perspective_params()` | 求**透视变换矩阵** M 和 M_inverse | ③前导 |
| `img_perspect_transform()` | 执行透视变换 | ③ |
| `cal_line_param()` | 直方图+滑动窗口+多项式拟合 | ④⑤⑥ |
| `fill_lane_poly()` | 填充车道多边形 | ⑦ |
| `cal_radius()` | 计算曲率半径 | ⑧ |
| `cal_center_departure()` | 计算车辆偏离中心距离 | ⑧ |
| `process_image()` | 单帧完整处理(视频逐帧调用) | 全流程 |
### 数学知识依赖
| 概念 | 公式 | 用在哪 |
|------|------|--------|
| 相机内参 K | $K=\begin{bmatrix}f_x&0&c_x\\0&f_y&c_y\\0&0&1\end{bmatrix}$ | 标定 |
| 畸变模型 | $x_{dist} = x(1+k_1r^2+k_2r^4+k_3r^6)+\dots$ | 去畸变 |
| 透视变换 | $[x',y',w']^T = M\cdot[x,y,1]^T$ | 鸟瞰图 |
| 二次多项式 | $x = ay^2 + by + c$ | 车道线拟合 |
| 曲率半径 | $R = \frac{(1+(2ay+b)^2)^{3/2}}{|2a|}$ | 转弯半径 |
> 这份代码把"相机标定→图像处理→几何测量"串成一个完整工程,是理解 ADAS(高级驾驶辅助)的经典入门。
|
|
## 相机标定与去畸变
### 为什么先标定?
镜头有**径向畸变**(边缘桶形/枕形变形)和**切向畸变**(组装偏差),不矫正会让车道线弯曲、测量失真。标定就是求出**内参矩阵 K** 和**畸变系数 dist**。
### 关键代码逐行:`cal_calibrate_params()`
```python
def cal_calibrate_params(file_paths):
object_points = [] # 角点三维世界坐标
image_points = [] # 角点图像像素坐标
# 1. 生成棋盘格角点的真实世界坐标(Z=0平面)
objp = np.zeros((nx * ny, 3), np.float32)
objp[:, :2] = np.mgrid[0:nx, 0:ny].T.reshape(-1, 2)
```
- `nx=9, ny=6`:棋盘格**内角点**是 9 列 × 6 行(不是格子数!)
- `objp`:每个角点在世界坐标的位置,形如 $(0,0,0),(1,0,0),\dots,(8,5,0)$,Z=0 表示标定板是平面
- `.T.reshape(-1,2)`:转置后展平成 N×2,填入 objp 的 x,y 列
```python
for file_path in file_paths:
img = cv2.imread(file_path)
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
# 2. 棋盘格角点检测
rect, coners = cv2.findChessboardCorners(gray, (nx, ny), None)
if rect == True:
object_points.append(objp) # 每张图世界坐标相同
image_points.append(coners) # 每张图像素坐标不同
```
- `findChessboardCorners` 返回 `rect`(是否找到)和 `coners`(像素坐标)
- 收集 20 张图的对应点对
```python
# 3. 张氏标定法求解(OpenCV封装)
ret, mtx, dist, rvecs, tvecs = cv2.calibrateCamera(
object_points, image_points, gray.shape[::-1], None, None)
return ret, mtx, dist, rvecs, tvecs
```
返回值:`mtx`=内参矩阵 K,`dist`=畸变系数 $(k_1,k_2,p_1,p_2,k_3)$,`rvecs/tvecs`=每张图的外参(旋转向量+平移向量)。
### 去畸变:`img_undistort()`
```python
def img_undistort(img, mtx, dist):
return cv2.undistort(img, mtx, dist, None, mtx)
```
`cv2.undistort` 用标定出的 K 和 dist 反向矫正每个像素位置,把弯曲的直线"拉直"。
> **避坑**:`findChessboardCorners` 对部分标定图可能返回 `rect=False`(角点被遮挡或反光),代码用 `if rect==True` 过滤,只保留成功检测的图。这正是标定要拍多张图的原因。
|
|
## 车道线提取 pipeline(颜色+边缘)
### 为什么不用普通灰度边缘?
车道线有**黄/白**两种颜色,光照变化大。单纯灰度+边缘会受阴影、路面纹理干扰。这份 pipeline 用 **HLS 颜色空间的 S 通道**(饱和度)+ **Sobel 边缘**双管齐下。
### 关键代码逐行:`pipeline()`
```python
def pipeline(img, s_thresh=(170, 255), sx_thresh=(40, 200)):
img = np.copy(img)
# 1. RGB → HLS 颜色空间
hls = cv2.cvtColor(img, cv2.COLOR_RGB2HLS).astype(np.float)
l_chanel = hls[:, :, 1] # L 亮度
s_chanel = hls[:, :, 2] # S 饱和度
```
- **HLS**:Hue(色相)/Lightness(亮度)/Saturation(饱和度)。**S 通道对黄色车道线特别敏感**(黄色饱和度高)。
```python
# 2. 在 L 通道上做 x 方向 Sobel 边缘检测
sobelx = cv2.Sobel(l_chanel, cv2.CV_64F, 1, 0) # dx=1,dy=0 只测横向边缘
abs_sobelx = np.absolute(sobelx)
scaled_sobel = np.uint8(255 * abs_sobelx / np.max(abs_sobelx)) # 归一化到 0~255
# 二值化:边缘强度在阈值内 = 1
sxbinary = np.zeros_like(scaled_sobel)
sxbinary[(scaled_sobel >= sx_thresh[0]) & (scaled_sobel <= sx_thresh[1])] = 1
```
- **Sobel 算子**:用 $3\times3$ 卷积核 $\begin{bmatrix}-1&0&1\\-2&0&2\\-1&0&1\end{bmatrix}$ 近似计算 x 方向梯度。车道线近似竖直,横向梯度大,所以用 `dx=1,dy=0`。
- **归一化**:除以最大值再乘 255,压到 8 位整数便于阈值处理。
```python
# 3. S 通道颜色阈值(捕捉黄色车道线)
s_binary = np.zeros_like(s_chanel)
s_binary[(s_chanel >= s_thresh[0]) & (s_chanel <= s_thresh[1])] = 1
# 4. 合并:边缘 OR 颜色,且 L>100 排除暗区噪声
color_binary = np.zeros_like(sxbinary)
color_binary[((sxbinary == 1) | (s_binary == 1)) & (l_chanel > 100)] = 1
return color_binary
```
- **融合策略**:`sxbinary==1 | s_binary==1`(边缘或颜色命中即可),`& l_chanel>100`(亮度太低的区域大概率是阴影/噪声,抑制掉)。
> **设计要点**:返回的是**二值图**(0/1),后续透视变换和直方图统计都基于这个二值图。这是传统车道线检测的通用范式。
|
|
## 透视变换与鸟瞰图
### 为什么需要鸟瞰图(BEV)?
正常行车视角下,车道线是**透视收敛的梯形**(远处窄、近处宽),难以用简单曲线拟合。**透视变换**把它"拉平"成俯视图(鸟瞰图,Bird's Eye View),车道线变成两条**近似平行的直线**,拟合和测量都简单得多。
### 透视变换的数学原理
透视变换(单应性变换)用 $3\times3$ 矩阵 $M$:
$$\begin{bmatrix}x'\\y'\\w'\end{bmatrix} = M \cdot \begin{bmatrix}x\\y\\1\end{bmatrix}, \quad \text{实际像素} = \left(\frac{x'}{w'}, \frac{y'}{w'}\right)$$
$M$ 有 8 个自由度,由 **4 对对应点**即可求解。
### 关键代码:`cal_perspective_params()`
```python
def cal_perspective_params(img, points):
offset_x = 330
offset_y = 0
img_size = (img.shape[1], img.shape[0])
# src: 原图中梯形的 4 个顶点(手动选的车道区域)
src = np.float32(points)
# dst: 鸟瞰图中对应的矩形 4 个顶点
dst = np.float32([[offset_x, offset_y],
[img_size[0]-offset_x, offset_y],
[offset_x, img_size[1]-offset_y],
[img_size[0]-offset_x, img_size[1]-offset_y]])
# 正变换矩阵(原图→鸟瞰图) 和 逆变换矩阵(鸟瞰图→原图)
M = cv2.getPerspectiveTransform(src, dst)
M_inverse = cv2.getPerspectiveTransform(dst, src)
return M, M_inverse
```
`main.py` 中的 4 个源点(直道测试图):
```python
points = [[601,448], [683,448], [230,717], [1097,717]]
# 左上 右上 左下 右下
```
这 4 个点沿着车道线梯形选取:上面两点在远处车道线交汇处(y=448),下面两点在图像底部(y=717)。
### 执行变换:`img_perspect_transform()`
```python
def img_perspect_transform(img, M):
img_size = (img.shape[1], img.shape[0])
return cv2.warpPerspective(img, M, img_size)
```
`cv2.warpPerspective` 对每个输出像素用 $M^{-1}$ 反查源图位置并插值,得到平滑的鸟瞰图。
> **避坑**:源点 `points` 是**手动针对特定摄像头位置和视角标定**的,换车/换镜头必须重新选点。现代方案会用**逆透视变换(IPM)**配合相机外参自动计算。
|
|
## 滑动窗口与二次多项式拟合
这是车道线检测的**核心算法**,在鸟瞰图上定位车道线像素并拟合曲线。
### 第 1 步:直方图定位车道起点
```python
histogram = np.sum(binary_warped[:, :], axis=0) # 沿 y 方向求和,得 x 方向直方图
midpoint = np.int(histogram.shape[0] / 2)
leftx_base = np.argmax(histogram[:midpoint]) # 左半区峰值=左车道
rightx_base = np.argmax(histogram[midpoint:]) + midpoint # 右半区峰值=右车道
```
**原理**:鸟瞰图中车道线是竖直亮带,沿列求和后,x 直方图会在**两个车道线位置出现两个尖峰**。左半区最大值=左车道起点,右半区最大值=右车道起点。
### 第 2 步:滑动窗口向上搜索
```python
nwindows = 9 # 从下到上分 9 个窗口
window_height = np.int(binary_warped.shape[0] / nwindows)
margin = 100 # 窗口半宽
minpix = 50 # 窗口内至少 50 个白点才重新定位窗口中心
for window in range(nwindows):
# 计算当前窗口的 y 范围(从下往上)
win_y_low = binary_warped.shape[0] - (window+1) * window_height
win_y_high = binary_warped.shape[0] - window * window_height
# 左右窗口的 x 范围(以当前中心 ±margin)
win_xleft_low = leftx_current - margin
win_xleft_high = leftx_current + margin
# 统计窗口内的非零像素
good_left_inds = ((nonzeroy >= win_y_low) & (nonzeroy < win_y_high) &
(nonzerox >= win_xleft_low) & (nonzerox < win_xleft_high)).nonzero()[0]
# 像素够多 → 更新窗口中心为这些像素的均值
if len(good_left_inds) > minpix:
leftx_current = np.int(np.mean(nonzerox[good_left_inds]))
```
**直观理解**:从底部车道起点开始,画一个窗口,统计里面的白色像素;如果够多,把窗口中心移到这些像素的均值位置,然后窗口上移一格,重复。这样窗口会**跟随车道线的弯曲向上"爬"**。
### 第 3 步:二次多项式拟合
```python
left_fit = np.polyfit(lefty, leftx, 2) # x = a*y² + b*y + c
right_fit = np.polyfit(righty, rightx, 2)
```
- **为什么用 x=f(y) 而不是 y=f(x)?** 因为车道线近似竖直,一个 y 可能对应多个 x(弯道),用 x=f(y) 才是单值函数。
- `np.polyfit(y, x, 2)` 返回系数 $[a, b, c]$,拟合 $x = ay^2 + by + c$。
### 曲率半径计算
车道线拟合后可算**转弯半径**(判断弯急不急)。曲率半径公式:
$$R = \frac{\left(1 + (2ay + b)^2\right)^{3/2}}{|2a|}$$
代码中先把像素转换成真实世界米(`ym_per_pix=30/720`, `xm_per_pix=3.7/700`),再拟合再算曲率。
### 偏离距离计算
```python
center_depart = ((left_x+right_x)/2 - lane_center) * xm_per_pix
```
车道中心与图像中心的差 × 每像素米数 = 车辆偏离车道中心的距离(正=偏右,负=偏左)。
> **算法演进**:滑动窗口是 2017 年的方案。现代车道线检测用 **深度学习**(LaneNet、SCNN、CLRNet、LaneATT),端到端直接输出车道线,无需手动调阈值和选透视点。
|
OpenCV Camera Calibration
Wikipedia: Camera Resectioning
CMU: Camera Models
张正友标定法原始论文 (Zhang 1999, ICCV)
RAFT-Stereo (PyTorch)
OpenCV 立体视觉指南