相机标定知识梳理 (Camera Calibration Guide)


1. 为什么需要相机标定?

1.1 相机的"数学人设"

简单说:相机就是一个把 3D 世界"压扁"成 2D 图片的函数

真实世界 (3D)  ──[相机函数]──▶  图片 (2D)
     P(X,Y,Z)              p(u,v)

1.2 标定能解决什么问题?

应用场景 标定作用
畸变矫正 把弯曲的直线"拉直",提升图像质量
双目视觉 计算左右相机相对位姿,实现深度估计
三维重建 从多视角图像恢复场景 3D 结构
SLAM/导航 提供精确的相机内参,支撑位姿估计
AR/VR 实现虚拟物体与真实场景的精准对齐

💡 通俗理解:标定就是给相机"体检",搞清楚它的"内在性格"(焦距、畸变)和"外在姿态"(旋转平移)。


2. 小孔成像与坐标系全家桶 📐

2.1 小孔成像原理(理想模型)

        真实世界
            │
            ▼
    ┌───────────────┐
    │   小孔光圈    │ ← 光线直线传播
    └───────────────┘
            │
            ▼
    ┌───────────────┐
    │   成像平面    │ ← 倒立实像
    └───────────────┘

2.2 四大坐标系

坐标系 符号 原点 单位 物理意义
世界坐标系 $\{W\}$ 任意选定 米 (m) 描述物体在真实空间的位置
相机坐标系 $\{C\}$ 相机光心 米 (m) 以相机为中心的 3D 参考系
图像物理坐标系 $\{I\}$ 主点 (光轴交点) 毫米 (mm) 成像平面上的连续坐标
像素坐标系 $\{P\}$ 图像左上角 像素 (pixel) 数字图像中的离散行列索引

2.3 坐标转换链条

世界坐标 ──[外参: R, t]──▶ 相机坐标 ──[投影]──▶ 图像坐标 ──[内参: K]──▶ 像素坐标

2.3.1 世界 → 相机(刚体变换)

$$ \begin{bmatrix} X_c \\ Y_c \\ Z_c \end{bmatrix} = R \begin{bmatrix} X_w \\ Y_w \\ Z_w \end{bmatrix} + \mathbf{t} \quad \text{或} \quad \tilde{\mathbf{P}}_c = \begin{bmatrix} R & \mathbf{t} \\ \mathbf{0}^T & 1 \end{bmatrix} \tilde{\mathbf{P}}_w $$

$R \in SO(3)$ 是旋转矩阵,$\mathbf{t} \in \mathbb{R}^3$ 是平移向量,合称外参

2.3.2 相机 → 图像(透视投影)

$$ \begin{bmatrix} x \\ y \end{bmatrix} = f \cdot \begin{bmatrix} X_c / Z_c \\ Y_c / Z_c \end{bmatrix} \quad \Rightarrow \quad s \begin{bmatrix} x \\ y \\ 1 \end{bmatrix} = \begin{bmatrix} f & 0 & 0 \\ 0 & f & 0 \\ 0 & 0 & 1 \end{bmatrix} \begin{bmatrix} X_c \\ Y_c \\ Z_c \end{bmatrix} $$

$f$ 是焦距,$s$ 是尺度因子(齐次坐标特性)。

2.3.3 图像 → 像素(仿射变换)

$$ \begin{bmatrix} u \\ v \\ 1 \end{bmatrix} = \begin{bmatrix} 1/d_x & 0 & u_0 \\ 0 & 1/d_y & v_0 \\ 0 & 0 & 1 \end{bmatrix} \begin{bmatrix} x \\ y \\ 1 \end{bmatrix} $$

$d_x, d_y$ 是单个像素的物理尺寸,$(u_0, v_0)$ 是主点像素坐标。

2.3.4 终极合并:世界 → 像素

$$ s \tilde{\mathbf{p}} = K \cdot [R | \mathbf{t}] \cdot \tilde{\mathbf{P}}_w \quad \text{其中} \quad K = \begin{bmatrix} f_x & \gamma & u_0 \\ 0 & f_y & v_0 \\ 0 & 0 & 1 \end{bmatrix} $$

内参矩阵 $K$ 包含:$f_x = f/d_x$, $f_y = f/d_y$(等效焦距),$\gamma$(轴倾斜,通常≈0),$(u_0, v_0)$(主点)。


3. 镜头畸变

3.1 为什么会有畸变?

透镜不是完美的!光线经过透镜边缘时会发生非线性折射,导致成像点"跑偏"。

3.2 两大畸变类型

径向畸变 (Radial Distortion)

切向畸变 (Tangential Distortion)

3.3 完整畸变模型(合并径向+切向)

$$ \begin{bmatrix} u_{\text{dist}} \\ v_{\text{dist}} \end{bmatrix} = \begin{bmatrix} f_x \cdot x_{\text{dist}} + u_0 \\ f_y \cdot y_{\text{dist}} + v_0 \end{bmatrix} $$

💡 工程实践:标定过程就是求解 $[K, k_1, k_2, p_1, p_2]$ 这 9 个参数(鱼眼额外加 $k_3$)。


4. 张正友标定法

4.1 核心思想

平面标定板(如棋盘格)的多视角图像,通过单应性矩阵建立约束,分步求解内参+外参+畸变。

4.2 关键步骤拆解

Step 1: 单应性矩阵 (Homography)

Step 2: 内参约束方程

利用旋转矩阵列向量的正交性:

$$ \mathbf{h}_1^T K^{-T} K^{-1} \mathbf{h}_2 = 0, \quad \mathbf{h}_1^T K^{-T} K^{-1} \mathbf{h}_1 = \mathbf{h}_2^T K^{-T} K^{-1} \mathbf{h}_2 $$

其中 $\mathbf{h}_1, \mathbf{h}_2$ 是 $H$ 的前两列。

Step 3: 线性求解 + 非线性优化

  1. 线性初值:将多张图像的约束堆叠,用 SVD 求解 $B = K^{-T}K^{-1}$
  2. Cholesky 分解:从 $B$ 恢复内参 $K$
  3. 极大似然优化:用 Levenberg-Marquardt 算法最小化重投影误差: $$ \min_{K, R_i, \mathbf{t}_i, \mathbf{k}} \sum_{i,j} \left\| \mathbf{p}_{ij} - \pi(K, R_i, \mathbf{t}_i, \mathbf{k}, \mathbf{P}_j) \right\|^2 $$

    $\pi(\cdot)$ 是包含畸变的完整投影函数。

4.3 为什么不用纯非线性优化?

工程智慧:非线性优化对初值敏感!张正友法先用线性方法给个好初值,再精细优化,既稳定又高效。


5. 单目标定实战 (OpenCV + Python)

5.1 标定板选择指南

类型 推荐规格 注意事项
棋盘格 7×8 或 9×10 内角点 角点检测最成熟,首选
圆点阵列 间距均匀 适合高反光场景
Charuco 棋盘+ArUco 混合 部分遮挡仍可检测

备注:标定板应覆盖图像四角+中心,采集10~20 张不同姿态图像。

5.2 完整代码流程

import cv2
import numpy as np
import glob

# === 1. 准备标定板参数 ===
# 棋盘格内角点数量 (列, 行)
pattern_size = (9, 6)
# 每个方格的实际尺寸 (单位:米)
square_size = 0.025

# === 2. 采集图像并检测角点 ===
object_points = []  # 3D 世界坐标 (Z=0)
image_points = []   # 2D 像素坐标

# 生成标定板角点的世界坐标 (假设标定板在 Z=0 平面)
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)
objp *= square_size

# 遍历所有标定图像
for fname in glob.glob('calib_images/*.png'):
    img = cv2.imread(fname)
    gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
    
    # 检测角点 (亚像素精度)
    ret, corners = cv2.findChessboardCorners(gray, pattern_size, 
                                             cv2.CALIB_CB_ADAPTIVE_THRESH)
    if ret:
        # 亚像素优化
        criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 
                    30, 0.001)
        corners = cv2.cornerSubPix(gray, corners, (11, 11), (-1, -1), criteria)
        
        object_points.append(objp)
        image_points.append(corners)
        
        # 可视化检测效果 (可选)
        cv2.drawChessboardCorners(img, pattern_size, corners, ret)
        cv2.imshow('Detected', img)
        cv2.waitKey(500)

# === 3. 执行标定 ===
ret, K, dist, rvecs, tvecs = cv2.calibrateCamera(
    object_points,      # 3D 点集
    image_points,       # 2D 点集
    gray.shape[::-1],   # 图像尺寸 (width, height)
    None, None,         # 内参/畸变初值 (None=自动估计)
    flags=cv2.CALIB_FIX_K3 + cv2.CALIB_ZERO_TANGENT_DIST  # 约束条件
)

print(f"重投影误差 (RMS): {ret:.4f} pixels")
print(f"内参矩阵 K:\n{K}")
print(f"畸变系数: {dist.ravel()}")

# === 4. 畸变矫正演示 ===
def undistort_and_show(img, K, dist):
    # 方法 1: 直接矫正 (简单)
    undist = cv2.undistort(img, K, dist)
    
    # 方法 2: 映射表矫正 (高效,适合视频流)
    map1, map2 = cv2.initUndistortRectifyMap(K, dist, None, K, 
                                             img.shape[1::-1], cv2.CV_16SC2)
    undist_fast = cv2.remap(img, map1, map2, interpolation=cv2.INTER_LINEAR)
    
    # 并排显示对比
    result = np.hstack([img, undist])
    cv2.imshow('Original vs Undistorted', result)
    cv2.waitKey(0)

# 测试一张图
test_img = cv2.imread('calib_images/test.png')
undistort_and_show(test_img, K, dist)

5.3 标定效果评估技巧

# 🔍 评估 1: 重投影误差分布
errors = []
for i in range(len(object_points)):
    img_points_proj, _ = cv2.projectPoints(
        object_points[i], rvecs[i], tvecs[i], K, dist)
    error = cv2.norm(image_points[i], img_points_proj, cv2.NORM_L2) / len(img_points_proj)
    errors.append(error)

print(f"平均误差: {np.mean(errors):.3f}px, 最大误差: {np.max(errors):.3f}px")

# 🔍 评估 2: 外参可视化 (以相机为中心)
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D

fig = plt.figure(figsize=(8, 6))
ax = fig.add_subplot(111, projection='3d')

# 绘制相机坐标系
ax.quiver(0, 0, 0, 1, 0, 0, color='r', length=0.1, label='X')
ax.quiver(0, 0, 0, 0, 1, 0, color='g', length=0.1, label='Y')
ax.quiver(0, 0, 0, 0, 0, 1, color='b', length=0.1, label='Z')

# 绘制各次标定的标定板位置
for R_vec, t in zip(rvecs, tvecs):
    R, _ = cv2.Rodrigues(R_vec)  # 旋转向量→旋转矩阵
    # 标定板四个角点 (世界坐标)
    corners_3d = np.array([
        [0, 0, 0],
        [pattern_size[0]*square_size, 0, 0],
        [pattern_size[0]*square_size, pattern_size[1]*square_size, 0],
        [0, pattern_size[1]*square_size, 0]
    ]).T
    # 变换到相机坐标系
    corners_cam = R @ corners_3d + t
    ax.plot(corners_cam[0], corners_cam[1], corners_cam[2], 'k-')

ax.set_xlabel('X'); ax.set_ylabel('Y'); ax.set_zlabel('Z')
ax.set_title('Calibration Poses (Camera-Centered)')
ax.legend(); plt.tight_layout(); plt.show()

合格标准:平均重投影误差 < 0.5 像素,标定板姿态覆盖全视场角


6. 双目相机:从单眼到立体视觉

6.1 理想双目模型(行对齐)

左相机 ◄───── 基线 T ─────► 右相机
     │                      │
     ▼                      ▼
  图像左                 图像右
     │                      │
     └─── 同一特征点 ────┘
           │
           ▼
     视差 d = x_left - x_right
           │
           ▼
     深度 Z = f·T / d   ← 三角测量核心公式!

关键结论:深度 $Z$ 与视差 $d$ 成反比,且非线性——近距离精度高,远距离敏感。

6.2 对极几何:降低匹配搜索维度

核心概念图解

        3D 点 P
           │
     ┌─────┴─────┐
     ▼           ▼
  左极点 e_L   右极点 e_R
     │           │
     ▼           ▼
  左极线 l_L   右极线 l_R
术语 定义 工程意义
基线 (Baseline) 两相机光心连线 决定深度测量精度
极点 (Epipole) 基线与成像平面交点 极线汇聚点
极线 (Epipolar Line) 3D 点投影在两图像的对应线 将 2D 匹配降为 1D 搜索
极线约束 匹配点必在对应极线上 大幅提升匹配效率与鲁棒性

本征矩阵 (E) vs 基本矩阵 (F)

$$ \mathbf{p}_R^T E \mathbf{p}_L = 0 \quad \text{(相机坐标系)} \quad \mathbf{q}_R^T F \mathbf{q}_L = 0 \quad \text{(像素坐标系)} $$

$$ E = [\mathbf{t}]_\times R, \quad F = K_R^{-T} E K_L^{-1} $$

记忆口诀E 管外参,F 管像素E 是 F 的"去内参"版本

6.3 双目标定 + 立体校正流程

# === 1. 分别标定左右相机 (获取 K1, dist1, K2, dist2) ===
# ... (调用单目标定代码) ...

# === 2. 双目标定:求解相对位姿 [R, T] ===
flags = cv2.CALIB_FIX_INTRINSIC  # 固定已标定的内参
ret, K1, dist1, K2, dist2, R, T, E, F = cv2.stereoCalibrate(
    object_points, image_points1, image_points2,
    K1, dist1, K2, dist2, gray.shape[::-1],
    flags=flags, criteria=(cv2.TERM_CRITERIA_EPS+cv2.TERM_CRITERIA_MAX_ITER, 100, 1e-5)
)

# === 3. 立体校正:生成行对齐的映射表 ===
# 输出:校正旋转矩阵 R1, R2 + 投影矩阵 P1, P2 + 重投影矩阵 Q
R1, R2, P1, P2, Q, validPixROI1, validPixROI2 = cv2.stereoRectify(
    K1, dist1, K2, dist2, gray.shape[::-1], R, T,
    alpha=0,  # 0=裁剪黑边,1=保留全部像素
    newImageSize=(width, height)
)

# === 4. 生成畸变矫正 + 立体校正的映射表 ===
map1_L, map2_L = cv2.initUndistortRectifyMap(K1, dist1, R1, P1[:3,:3], 
                                             (width, height), cv2.CV_16SC2)
map1_R, map2_R = cv2.initUndistortRectifyMap(K2, dist2, R2, P2[:3,:3], 
                                             (width, height), cv2.CV_16SC2)

# === 5. 实时校正函数 (用于视频流) ===
def rectify_pair(img_L, img_R):
    undist_L = cv2.remap(img_L, map1_L, map2_L, cv2.INTER_LINEAR)
    undist_R = cv2.remap(img_R, map1_R, map2_R, cv2.INTER_LINEAR)
    return undist_L, undist_R

# === 6. 深度计算 (使用重投影矩阵 Q) ===
# 先计算视差图 (见第 7 节),然后:
disparity = stereo_match(rect_L, rect_R)  # 单位:像素
depth_map = cv2.reprojectImageTo3D(disparity, Q)  # 输出:XYZ 三维坐标

⚠️ 需要注意

  1. alpha=0 会裁剪图像边缘,但保证所有像素有效;alpha=1 保留全部像素但有黑边
  2. Q 矩阵的第四行 [0,0,-1/Tx, (cx_L-cx_R)/Tx] 编码了基线 $T_x$ 和主点差
  3. 校正后务必可视化极线验证:同一特征点应在同一行

7. 立体匹配:从视差到深度

7.1 匹配四步法(局部/全局通用框架)

校正图像 ──▶ [1. 代价计算] ──▶ [2. 代价聚合] ──▶ [3. 视差选择] ──▶ [4. 视差优化] ──▶ 视差图

Step 1: 匹配代价计算

方法 公式 特点
AD (绝对差) $C = I_L - I_R
SAD (窗口和) $C = \sum_{w} I_L - I_R
NCC (归一化相关) $C = \frac{\sum (I_L-\bar{I}_L)(I_R-\bar{I}_R)}{\sigma_L \sigma_R}$ 光照鲁棒,计算量大
Census 比特串 Hamming 距离 纹理鲁棒,适合弱光照

Step 2: 代价聚合

Step 3: 视差选择

Step 4: 视差优化

7.2 OpenCV BM vs SGBM 实战对比

# === BM (Block Matching) - 快速但精度一般 ===
bm = cv2.StereoBM_create(
    numDisparities=16*5,   # 视差范围 (需为 16 倍数)
    blockSize=15           # SAD 窗口大小 (奇数)
)
bm.setPreFilterCap(31)
bm.setTextureThreshold(10)
bm.setUniquenessRatio(10)  # 唯一性阈值 (%)
bm.setSpeckleWindowSize(100)  # 斑点过滤
bm.setSpeckleRange(32)

# === SGBM (Semi-Global BM) - 精度高,推荐默认使用 ===
sgbm = cv2.StereoSGBM_create(
    minDisparity=0,
    numDisparities=16*5,   # 同上
    blockSize=5,           # 通常 3~7
    P1=8*3*5**2,           # 小视差变化惩罚
    P2=32*3*5**2,          # 大视差变化惩罚
    disp12MaxDiff=1,       # 左右一致性检查阈值
    preFilterCap=63,
    uniquenessRatio=10,
    speckleWindowSize=100,
    speckleRange=32,
    mode=cv2.STEREO_SGBM_MODE_SGBM_3WAY  # 3 路径/5 路径/全路径
)

# === 计算视差图 ===
disparity = sgbm.compute(rect_L, rect_R).astype(np.float32) / 16.0  # SGBM 输出×16

# === 可视化 (注意:视差图需归一化显示) ===
disp_vis = cv2.normalize(disparity, None, 0, 255, cv2.NORM_MINMAX, cv2.CV_8U)
disp_vis = cv2.applyColorMap(disp_vis, cv2.COLORMAP_JET)
cv2.imshow('Disparity', disp_vis)

参数调优口诀

  • numDisparities:根据最近物体视差设定,宁大勿小(计算量线性增长)
  • blockSize:纹理丰富用小窗口 (35),弱纹理用大窗口 (915)
  • P1/P2:$P_2 > P_1$,比值越大视差图越平滑(但可能丢失细节)
  • uniquenessRatio:10~15 平衡精度与完整性

8. 鱼眼相机:超广角的特殊处理

8.1 为什么普通模型搞不定鱼眼?

针孔模型假设直线投影为直线,但鱼眼镜头视角>180°,必须用非线性投影模型

8.2 四种经典投影模型对比

模型 投影公式 $r = f(\theta)$ 特点 适用场景
等距投影 $r = f \cdot \theta$ 计算简单,最常用 通用鱼眼标定
等立体角 $r = 2f \cdot \sin(\theta/2)$ 保持面积比例 全景拼接
正交投影 $r = f \cdot \sin\theta$ 边缘压缩严重 特殊光学设计
体视投影 $r = 2f \cdot \tan(\theta/2)$ 保持角度不变 天文摄影

备注:90% 的鱼眼相机近似服从等距投影,后续标定以此为基础。

8.3 Kannala-Brandt (KB) 畸变模型(现代标准)

核心思想

将鱼眼成像分解为两步:

3D 点 ──▶ 单位球面投影 ──▶ 非线性畸变 ──▶ 像素坐标

数学表达

$$ \begin{aligned} \theta &= \arccos\left( \frac{Z_c}{\sqrt{X_c^2 + Y_c^2 + Z_c^2}} \right) \\ r_d &= \theta + k_1 \theta^3 + k_2 \theta^5 + k_3 \theta^7 + k_4 \theta^9 \\ x_d &= r_d \cdot \frac{X_c}{\sqrt{X_c^2 + Y_c^2}}, \quad y_d = r_d \cdot \frac{Y_c}{\sqrt{X_c^2 + Y_c^2}} \\ \begin{bmatrix} u \\ v \end{bmatrix} &= \begin{bmatrix} f_x \cdot x_d + \gamma y_d + u_0 \\ f_y \cdot y_d + v_0 \end{bmatrix} \end{aligned} $$

$k_1 \sim k_4$ 是 KB 畸变系数,$\theta$ 是入射角(非图像坐标!)。

8.4 鱼眼标定实战 (OpenCV fisheye 模块)

import cv2
import numpy as np

# === 1. 准备参数 (注意:用 fisheye 模块!) ===
pattern_size = (9, 6)
square_size = 0.025
flags = cv2.fisheye.CALIB_RECOMPUTE_EXTRINSIC + \
        cv2.fisheye.CALIB_CHECK_COND + \
        cv2.fisheye.CALIB_FIX_SKEW

# === 2. 角点检测 (同单目,但用 fisheye 接口) ===
object_points = []  # 3D 点
image_points = []   # 2D 点
# ... (检测代码同前,略) ...

# === 3. 鱼眼标定 ===
K = np.zeros((3, 3))
D = np.zeros((4, 1))  # [k1, k2, k3, k4]
rvecs = []
tvecs = []

rms, _, _, _, _ = cv2.fisheye.calibrate(
    object_points, image_points, gray.shape[::-1],
    K, D, rvecs, tvecs, flags,
    (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_COUNT, 100, 1e-6)
)

print(f"鱼眼标定误差: {rms:.4f}px")
print(f"内参:\n{K}")
print(f"KB 畸变系数: {D.ravel()}")

# === 4. 鱼眼畸变矫正 (两种模式) ===
def fisheye_undistort(img, K, D, balance=0.6, fov_scale=1.0):
    """
    balance: 0~1, 0=裁剪黑边最多但视场最大, 1=保留全部像素但视场小
    fov_scale: >1 放大视场 (可能插值), <1 缩小视场
    """
    # 计算新内参 (可选调整视场)
    new_K = cv2.fisheye.estimateNewCameraMatrixForUndistortRectify(
        K, D, img.shape[1::-1], np.eye(3), 
        balance=balance, fov_scale=fov_scale)
    
    # 生成映射表
    map1, map2 = cv2.fisheye.initUndistortRectifyMap(
        K, D, np.eye(3), new_K, img.shape[1::-1], cv2.CV_16SC2)
    
    # 执行矫正
    undist = cv2.remap(img, map1, map2, interpolation=cv2.INTER_LINEAR, 
                       borderMode=cv2.BORDER_REPLICATE)
    return undist, new_K

# 测试矫正效果
test_img = cv2.imread('fisheye_test.jpg')
undist, new_K = fisheye_undistort(test_img, K, D, balance=0.8)
cv2.imwrite('undistorted.jpg', undist)

8.5 鱼眼矫正的三种实用方案

方案 原理 优点 缺点 适用场景
棋盘格标定法 用标定板求解精确映射 精度高,数学严谨 需标定板,边缘拉伸严重 机器人导航、测量
横向展开法 以图像中心为视点环视展开 保留全部像素,无裁剪 几何失真,非真实视角 安防监控、全景预览
经纬度展开 按球面经纬度映射到矩形 无像素损失,规则网格 竖直/水平方向分别处理 天空/地面分割分析

💡 选择建议

  • 需要精确测量 → 用棋盘格标定 + 精确矫正
  • 只需视觉预览 → 用横向展开 (快速+直观)
  • 语义分割 → 用经纬度展开 (保持拓扑)

9.工程注意点与问题原因排查

标定数据采集 Checklist

- [ ] 标定板覆盖图像四角 + 中心 (避免中心密集)
- [ ] 采集 10~20 张,姿态变化 > 30° (旋转+平移)
- [ ] 标定板与相机距离:0.5m ~ 2m (覆盖工作距离)
- [ ] 光照均匀,避免反光/阴影/运动模糊
- [ ] 固定焦距!禁用自动对焦 (标定后勿变焦)
- [ ] 用 PNG 无损格式保存图像 (避免 JPEG 压缩伪影)

参数调试速查表

问题现象 可能原因 解决方案
重投影误差 > 1px 角点检测不准 / 图像模糊 cornerSubPix + 提高图像质量
标定板姿态单一 约束不足,参数耦合 增加旋转角度,尤其绕 X/Y 轴
鱼眼边缘矫正扭曲 $k_3/k_4$ 未启用 / 模型不匹配 CALIB_FIX_K3=0 + 检查投影模型
双目视差图噪声大 纹理弱 / 曝光不一致 用 Census 代价 + 直方图均衡预处理
深度图有"空洞" 遮挡 / 唯一性过滤过严 降低 uniquenessRatio + 用左右一致性填充