立体视觉基础:对极几何、本征矩阵与基础矩阵、立体匹配
立体视觉,说白了就是让计算机拥有两只眼睛。我们人类靠双眼的视差来判断距离,计算机也一样。今天这一章,我们来聊聊立体视觉的核心原理——从几何约束到匹配算法,再到最终的深度图生成。
我个人习惯把立体视觉拆成三个层次:几何约束层(对极几何)、代数表达层(本征矩阵与基础矩阵)、计算实现层(立体匹配与深度计算)。这样理解起来会清晰很多。
1. 对极几何:两个相机之间的几何关系
先说说对极几何。你想想看,两个相机从不同角度拍同一个物体,它们之间有什么几何约束?
核心概念就三个:对极点、对极线、对极平面。左相机光心、右相机光心、空间点P,这三个点确定了一个平面——对极平面。这个平面与左右成像平面的交线,就是对极线。
嗯,这里要注意:左图上的一个点,在右图上对应的点一定在右图的某条对极线上。这就是对极约束。它把二维搜索降到了一维搜索,效率提升可不是一星半点。
对极约束的核心价值:
- 将匹配搜索从二维降为一维
- 减少误匹配的概率
- 为后续的矩阵推导提供几何基础
我在项目中遇到过一个问题:相机标定稍微有点偏差,对极线就不准了。后来我养成了一个习惯——每次做立体匹配前,先画几根对极线看看,确认标定没问题再往下走。
2. 本征矩阵与基础矩阵
这两个矩阵,说白了就是将对极几何约束用数学公式表达出来。
2.1 本征矩阵 E
本征矩阵描述的是已标定相机之间的几何关系。它只包含旋转和平移信息,不包含相机内参。
公式长这样:
E = [t]× R
其中:
- R 是旋转矩阵
- [t]× 是平移向量 t 的反对称矩阵
对于左图上的一个归一化坐标点 x₁,它在右图上的对应点 x₂ 满足:
x₂ᵀ E x₁ = 0
这个公式看着简单,但实际用起来坑不少。我记得有一次,我算出来的E矩阵怎么都不对,折腾了半天才发现是坐标归一化时忘了考虑畸变参数。
2.2 基础矩阵 F
基础矩阵是E的升级版——它把相机内参也考虑进去了。对于未标定或像素坐标的情况,用F矩阵更合适。
F = K₂⁻ᵀ E K₁⁻¹
其中:
- K₁、K₂ 分别是左右相机的内参矩阵
对于像素坐标 p₁ 和 p₂,约束关系为:
p₂ᵀ F p₁ = 0
我的经验:实际工程中,我更喜欢用基础矩阵F做初始估计,再用本征矩阵E做精细优化。因为F矩阵可以直接从匹配点对计算出来,不需要事先知道相机内参。
| 矩阵 | 输入坐标 | 是否需要内参 | 自由度 | 典型应用 |
|---|---|---|---|---|
| 本征矩阵 E | 归一化坐标 | 是 | 5 | 已标定系统、视觉SLAM |
| 基础矩阵 F | 像素坐标 | 否 | 7 | 未标定系统、图像匹配 |
3. 立体匹配:SGBM 与 BM
有了几何约束,接下来就是找对应点了。立体匹配算法有很多,但工程中最常用的就是BM和SGBM。
3.1 BM(块匹配)算法
BM算法,说白了就是拿左图的一个小窗口,去右图的对应极线上滑动,找最像的那个位置。
它的核心步骤:
- 代价计算:用SAD或SSD计算窗口相似度
- 代价聚合:简单求和(其实这一步BM做得比较粗糙)
- 视差计算:WTA(赢家通吃)策略
- 视差优化:左右一致性检查、中值滤波
避坑指南:我曾经在纹理稀疏的区域吃过亏——BM算法在那片区域几乎全错。后来我加了一个纹理置信度判断,纹理太弱的地方直接标记为无效视差。
3.2 SGBM(半全局块匹配)算法
SGBM是BM的升级版。它引入了全局能量函数的概念,但又不是全图优化——它沿着多个方向做动态规划,所以叫"半全局"。
能量函数长这样:
E(D) = Σ C(p, Dp) + Σ P₁·T(|Dp - Dq| = 1) + Σ P₂·T(|Dp - Dq| > 1)
其中:
- C(p, Dp) 是像素 p 在视差 Dp 下的匹配代价
- P₁、P₂ 是惩罚项
- T(·) 是条件判断函数
我个人习惯用OpenCV的SGBM实现,参数调优是关键:
import cv2
import numpy as np
# 创建SGBM对象
stereo = cv2.StereoSGBM_create(
minDisparity=0,
numDisparities=64, # 视差范围,建议16的倍数
blockSize=11, # 匹配窗口大小,奇数
P1=8 * 3 * 11 ** 2, # 视差变化1的惩罚
P2=32 * 3 * 11 ** 2, # 视差变化大于1的惩罚
disp12MaxDiff=1, # 左右一致性检查阈值
uniquenessRatio=10, # 唯一性比率
speckleWindowSize=100, # 斑点滤波窗口
speckleRange=32 # 斑点滤波范围
)
# 计算视差图
disparity = stereo.compute(img_left, img_right).astype(np.float32) / 16.0
参数调优心得:
- P2 一般设为 P1 的 4-8 倍
- blockSize 越大,视差图越平滑,但边缘会模糊
- numDisparities 不要设太大,64或128通常够用
- speckleWindowSize 用来去除小噪点,我一般设100-200
4. 视差图与深度图计算
有了视差图,深度图就是一步数学转换的事。
4.1 视差与深度的关系
这个公式很直观:
Z = (f * B) / d
其中:
- Z 是深度(单位:毫米)
- f 是相机焦距(单位:像素)
- B 是基线长度(单位:毫米)
- d 是视差值(单位:像素)
你看,深度和视差成反比。物体越近,视差越大;物体越远,视差越小。这也是为什么远处物体的深度精度会下降——视差变化一点点,深度就变化很大。
4.2 从视差图到深度图
代码实现很简单:
def disparity_to_depth(disparity, f, baseline):
"""
将视差图转换为深度图
参数:
disparity: 视差图 (像素)
f: 相机焦距 (像素)
baseline: 基线长度 (毫米)
返回:
depth: 深度图 (毫米)
"""
# 避免除零
valid = disparity > 0
depth = np.zeros_like(disparity, dtype=np.float32)
depth[valid] = (f * baseline) / disparity[valid]
# 设置最大深度阈值
max_depth = 10000 # 10米
depth[depth > max_depth] = 0
return depth
工程注意事项:
- 视差图需要做亚像素插值,否则深度图会有明显的量化噪声
- 深度图要做滤波处理,我常用双边滤波保边去噪
- 注意单位统一:f和baseline的单位要一致
- 无效视差区域(遮挡、纹理稀疏)要标记出来,不要强行计算
5. 实战经验总结
做立体视觉这几年,我踩过不少坑。这里分享几个关键点:
- 标定是基础:相机标定不准,后面的一切都是白搭。我每次换场景都会重新标定一次。
- 图像预处理很重要:直方图均衡化、高斯滤波,这些预处理步骤能显著提升匹配质量。
- 视差范围要合理:根据你的场景深度范围来设定,不要盲目设大。
- 后处理不能省:左右一致性检查、中值滤波、空洞填充,这些步骤能去掉大部分噪点。
嗯,立体视觉这块内容就这些。从对极几何的约束,到矩阵的数学表达,再到匹配算法的工程实现,每一步都有它的道理。做工程嘛,既要懂理论,也要会调参,两者缺一不可。