双目与结构光
概述
双目视觉和结构光是两种主要的深度感知技术。双目视觉模仿人类双眼通过视差计算深度,结构光通过投射已知图案来测量形变获取深度。本节深入介绍其数学原理、工程实现和性能对比。
双目视觉原理
基本几何
双目相机由两个水平排列的相机组成,通过三角测量计算深度:
左相机 O_L 右相机 O_R
*-------- B --------*
|\ /|
| \ / |
| \ / |
| \ P(X,Y,Z) |
| \ * / |
| \ / \ / |
|------*---*--------| 图像平面
x_L x_R
视差与深度
视差(Disparity)是同一点在左右图像中的水平位置差:
由相似三角形关系:
因此深度为:
其中:
- \(Z\):深度(米)
- \(f\):焦距(像素)
- \(B\):基线长度(米)
- \(d\):视差(像素)
深度分辨率
深度的最小分辨率(相邻视差值对应的深度差):
当 \(\Delta d = 1\)(整像素视差)时,深度分辨率随距离平方增长。
深度分辨率计算
参数:\(f=400\text{px}\),\(B=0.12\text{m}\)(ZED 2i)
- 在1m处:\(\Delta Z = \frac{1^2}{400 \times 0.12} = 0.021\text{m} = 2.1\text{cm}\)
- 在5m处:\(\Delta Z = \frac{25}{48} = 0.52\text{m} = 52\text{cm}\)
亚像素视差(0.1px精度)可将分辨率提高10倍。
深度误差分析
要提高深度精度的三条路径:
- 增大基线 \(B\):但近距离盲区也增大(\(Z_{\min} \approx \frac{fB}{d_{\max}}\))
- 增大焦距 \(f\):但FOV变小
- 减小视差误差 \(\sigma_d\):更好的匹配算法、亚像素插值
对极几何
对极约束
对于已标定的双目系统,左图中的一点对应到右图中时,必然落在对极线(Epipolar Line)上:
其中 \(\mathbf{F}\) 是基础矩阵(Fundamental Matrix)。
对于校正后的双目(rectified stereo),对极线变为水平线,搜索范围从2D降低到1D:
左图: 右图:
┌──────────────────┐ ┌──────────────────┐
│ *P_L │ │ *P_R │
│────────对极线─────│ → │────────对极线─────│
│ │ │ │
└──────────────────┘ └──────────────────┘
P_L和P_R在同一行上(y坐标相同)
校正(Rectification)
双目校正使两个相机的图像平面共面,且行对齐:
import cv2
import numpy as np
# 已知标定参数
K_left = np.array([[fx, 0, cx], [0, fy, cy], [0, 0, 1]])
K_right = np.array([[fx, 0, cx], [0, fy, cy], [0, 0, 1]])
D_left = np.array([k1, k2, p1, p2, k3])
D_right = np.array([k1, k2, p1, p2, k3])
R = rotation_matrix_3x3 # 右相机相对左相机的旋转
T = translation_vector # 右相机相对左相机的平移
# 立体校正
R1, R2, P1, P2, Q, roi1, roi2 = cv2.stereoRectify(
K_left, D_left, K_right, D_right,
image_size, R, T,
flags=cv2.CALIB_ZERO_DISPARITY,
alpha=0
)
# 生成映射表
map1_left, map2_left = cv2.initUndistortRectifyMap(
K_left, D_left, R1, P1, image_size, cv2.CV_16SC2
)
map1_right, map2_right = cv2.initUndistortRectifyMap(
K_right, D_right, R2, P2, image_size, cv2.CV_16SC2
)
# 应用校正
left_rectified = cv2.remap(left_img, map1_left, map2_left, cv2.INTER_LINEAR)
right_rectified = cv2.remap(right_img, map1_right, map2_right, cv2.INTER_LINEAR)
视差匹配算法
块匹配(Block Matching)
# OpenCV Semi-Global Block Matching
stereo = cv2.StereoSGBM_create(
minDisparity=0,
numDisparities=128, # 必须是16的倍数
blockSize=5, # 匹配窗口大小
P1=8 * 3 * 5**2, # 视差平滑参数1
P2=32 * 3 * 5**2, # 视差平滑参数2
disp12MaxDiff=1, # 左右一致性检查
uniquenessRatio=10, # 唯一性约束
speckleWindowSize=100, # 去斑点
speckleRange=32
)
# 计算视差图
disparity = stereo.compute(left_rectified, right_rectified)
disparity_float = disparity.astype(np.float32) / 16.0 # 去除定点小数
# 视差转深度
depth = (focal_length * baseline) / (disparity_float + 1e-6)
depth[disparity_float <= 0] = 0 # 无效视差
匹配算法对比
| 算法 | 精度 | 速度 | 特点 |
|---|---|---|---|
| BM (Block Matching) | 低 | 最快(~100fps) | 简单局部匹配 |
| SGBM (Semi-Global) | 中 | 中(~30fps) | 多方向代价聚合 |
| ELAS | 中高 | 中(~20fps) | 自适应支撑点 |
| AANet (深度学习) | 高 | 中(~20fps on GPU) | 端到端学习 |
| RAFT-Stereo (深度学习) | 最高 | 慢(~5fps on GPU) | 迭代光流 |
深度学习双目匹配
import torch
from raft_stereo import RAFTStereo
# 加载预训练模型
model = RAFTStereo(args)
model.load_state_dict(torch.load('raft-stereo.pth'))
model.eval()
with torch.no_grad():
# 输入左右图像
left = torch.from_numpy(left_img).permute(2,0,1).float().unsqueeze(0).cuda()
right = torch.from_numpy(right_img).permute(2,0,1).float().unsqueeze(0).cuda()
# 预测视差
_, disparity = model(left, right, iters=20)
disparity = disparity.squeeze().cpu().numpy()
结构光
原理
结构光通过向场景投射已知的光图案,通过观察图案的形变来计算深度:
投射器 相机
* *
|\ /|
| \ 已知图案 / |
| \ ↓↓↓ / |
| \ ||| / |
| \||| / |
======物体======
图案形变反映表面形状
散斑图案(Speckle Pattern)
RealSense D435i使用红外散斑投射:
- 投射随机红外点阵
- 左右IR相机观察散斑图案
- 图案的偏移量 = 视差
优势:在无纹理表面(白墙、桌面)也能获得深度
劣势:室外阳光会淹没红外散斑
编码结构光(Coded Pattern)
使用空间或时间编码的图案:
| 编码方式 | 图案数量 | 速度 | 精度 | 代表 |
|---|---|---|---|---|
| 二值编码 | 多帧 | 慢 | 高 | 工业三维扫描 |
| 格雷码 | \(\log_2 N\) 帧 | 中 | 高 | 结构光扫描仪 |
| 相移法 | 3-4帧 | 中 | 最高 | 精密测量 |
| 单帧编码 | 1帧 | 快 | 中 | Azure Kinect |
结构光深度计算
类似双目视觉,但一个"相机"替换为投射器:
其中 \(d_{\text{pattern}}\) 是图案在相机图像中的偏移量。
飞行时间(ToF)
原理
ToF相机发射调制光,测量光往返时间:
其中 \(c\) 是光速(\(3 \times 10^8\) m/s),\(\Delta t\) 是光往返时间。
连续波ToF(iToF)
实际中使用调制光的相位差测距:
发射信号:\(s(t) = A \cos(2\pi f_m t)\)
接收信号:\(r(t) = A' \cos(2\pi f_m t + \phi)\)
深度:
最大无歧义距离:
最大测量距离
调制频率 \(f_m = 20\text{MHz}\):
直接ToF(dToF)
使用SPAD(单光子雪崩二极管)直接测量光子飞行时间:
- 精度更高(mm级)
- 抗环境光更强
- 苹果LiDAR Scanner (iPad Pro) 使用dToF
- 成本较高
三种技术对比
| 特性 | 被动双目 | 主动双目 | 结构光 | ToF |
|---|---|---|---|---|
| 室外性能 | 好 | 中 | 差 | 中 |
| 无纹理表面 | 差 | 好 | 好 | 好 |
| 深度范围 | 0.5-20m | 0.3-10m | 0.2-4m | 0.1-8m |
| 精度@1m | 3-5mm | 1-2mm | <1mm | 1-3mm |
| 分辨率 | 高 | 高 | 高 | 低(VGA) |
| 帧率 | 高(100fps) | 高(90fps) | 低(30fps) | 高(60fps) |
| 计算量 | 大 | 中 | 小 | 最小 |
| 多设备干扰 | 无 | 可能 | 严重 | 可能 |
| 功耗 | 低 | 中 | 中 | 中 |
| 成本 | 中 | 中 | 中 | 高 |
实际应用选择
场景匹配
| 应用场景 | 推荐技术 | 理由 |
|---|---|---|
| 室内导航 | 主动双目 | 无纹理墙面需要投射 |
| 室外导航 | 被动双目 + LiDAR | 阳光下IR不可靠 |
| 机械臂抓取 | 结构光/主动双目 | 近距离高精度 |
| 人体跟踪 | ToF/主动双目 | 速度快、人体形状简单 |
| 3D扫描 | 结构光(多帧) | 最高精度 |
| 高速场景 | 被动双目/ToF | 帧率高、低延迟 |
点云融合
import open3d as o3d
# 从深度图生成点云
intrinsic = o3d.camera.PinholeCameraIntrinsic(
640, 480, fx, fy, cx, cy
)
# 多帧点云融合
volume = o3d.pipelines.integration.ScalableTSDFVolume(
voxel_length=0.005, # 5mm体素
sdf_trunc=0.02,
color_type=o3d.pipelines.integration.TSDFVolumeColorType.RGB8
)
for depth, color, pose in frames:
rgbd = o3d.geometry.RGBDImage.create_from_color_and_depth(
o3d.geometry.Image(color),
o3d.geometry.Image(depth),
depth_trunc=3.0
)
volume.integrate(rgbd, intrinsic, np.linalg.inv(pose))
# 提取网格
mesh = volume.extract_triangle_mesh()
mesh.compute_vertex_normals()
o3d.visualization.draw_geometries([mesh])
小结
- 双目深度公式 \(Z = fB/d\) 是立体视觉的核心
- 深度误差与距离平方成正比,基线和焦距决定精度上限
- 对极校正将2D搜索降为1D,是立体匹配的前提
- 结构光在无纹理表面优势明显,但室外受限
- ToF不依赖纹理和环境光,但分辨率较低
- 实际系统通常组合使用多种深度传感技术
参考资料
- Hartley, R., & Zisserman, A. Multiple View Geometry in Computer Vision
- Scharstein, D., & Szeliski, R. "A Taxonomy and Evaluation of Dense Two-Frame Stereo Correspondence Algorithms"
- Intel RealSense White Paper: Active IR Stereo
- Geng, J. "Structured-light 3D surface imaging: a tutorial"