标定的意义与小孔成像
## 为什么要做相机标定? 我们所处的世界是**三维**的,而照片是**二维**的。相机可以看成一个函数: ``` 输入:三维场景 → [ 相机函数 ] → 输出:二维灰度图 ``` 这个从 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 重建远远不够。**必须用自己的相机重新标定。**
张氏标定与OpenCV
## 相机标定方法的三大分类 | 方法 | 原理 | 优点 | 缺点 | 代表算法 | |------|------|------|------|----------| | **传统标定法** | 基于已知标定物(棋盘格/圆点),利用物点-像点对应求参 | 精度高、鲁棒性强 | 需要标定物、流程繁琐 | 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 立体视觉指南