跳到主要内容SGBM 半全局块匹配算法流程详解 | 极客日志C++AI算法
SGBM 半全局块匹配算法流程详解
SGBM 算法通过多路径动态规划近似全局能量最小化,实现密集视差估计。核心流程涵盖预处理、代价计算、聚合、视差提取及后处理。 Sobel 滤波、BT/Census 代价体构建、DP 聚合策略及左右一致性检查等关键步骤,提供 OpenCV 参数调优与工程优化技巧,适用于自动驾驶、机器人导航等深度感知场景。
DotNetGuy6 浏览 SGBM 半全局块匹配算法流程详解
1. 算法概述
1.1 SGBM 的本质
SGBM (Semi-Global Block Matching) 是 Heiko Hirschmüller 提出的 SGM 算法的工程化实现,核心思想是通过多路径动态规划近似全局能量最小化。
- 全局匹配: 考虑整张图像的一致性约束
- 半全局近似: 用多个 1D 路径代替 2D 全局优化,降低复杂度
- 块匹配: 结合局部块匹配的鲁棒性
1.2 算法复杂度
- 时间复杂度: O(W × H × D × P)
- W, H: 图像宽高
- D: 视差搜索范围 (numDisparities)
- P: 路径数量 (通常 5 或 8)
- 空间复杂度: O(W × H × D)
2. 完整算法流程
输入:左右校正图像
预处理:Sobel 滤波
代价计算:BT/Census
代价聚合:多路径 DP
视差计算:WTA + 亚像素
后处理:一致性检查
流程概览:
| 步骤 | 输入 | 输出 | 核心操作 | OpenCV 参数 |
|---|
| 1. 预处理 | 左右图像 | 滤波后图像 | Sobel 梯度 | preFilterCap |
| 2. 代价计算 | 滤波图像 | 代价体 C(x,y,d) | BT 距离/Census | blockSize |
| 3. 代价聚合 | 代价体 | 聚合代价 S(x,y,d) | 多路径 DP | P1, P2 |
| 4. 视差计算 | 聚合代价 | 粗视差图 | WTA | numDisparities |
| 5. 后处理 | 粗视差图 | 精细视差图 | 滤波 | uniquenessRatio |
3. 步骤 1: 预处理 (Pre-processing)
3.1 目的
- 减少光照影响: 归一化亮度差异
- 增强纹理: 提升边缘和细节的匹配可靠性
3.2 Sobel 预滤波
OpenCV 对左右图像分别进行水平方向的 Sobel 梯度计算:
I'(x, y) = clip(∂I(x,y)/∂x, -preFilterCap, +preFilterCap)
数学展开:
∂I/∂x ≈ I(x+1, y) - I(x-1, y)
代码等效实现:
void {
cv::(src, dst, CV_16S, , , );
( y = ; y < dst.rows; y++) {
( x = ; x < dst.cols; x++) {
val = dst.<>(y, x);
dst.<>(y, x) = cv::<>(std::(-preFilterCap, std::(preFilterCap, ()val)));
}
}
}
preFilterXSobel
(const cv::Mat& src, cv::Mat& dst, int preFilterCap)
Sobel
1
0
3
for
int
0
for
int
0
short
at
short
at
short
saturate_cast
short
max
min
int
3.3 参数影响
- preFilterCap = 31: 适用于室内、光照均匀场景
- preFilterCap = 63: 适用于室外、光照变化大的场景
- 过小 → 梯度信息不足;过大 → 噪声增加
4. 步骤 2: 代价计算 (Cost Computation)
4.1 代价体构建
构建 3D 代价体 (Cost Volume):
C : R^(W×H×D) where D = numDisparities
对于每个像素 (x, y) 和视差 d,计算左右图像的匹配代价。
4.2 方法 1: BT (Birchfield-Tomasi) 距离
公式:
C_BT(x, y, d) = (1/N) * Σ_(i,j)∈W ρ(I_L(x+i, y+j), I_R(x+i-d, y+j))
其中 ρ 为鲁棒代价函数:
ρ(a, b) = min(|a - b|, |a - b^-|, |a - b^+|)
b^- = (I_R(x-d) + I_R(x-d-1))/2, b^+ = (I_R(x-d) + I_R(x-d+1))/2
float computeBT(int x, int y, int d, const Mat& left, const Mat& right) {
float cost = 0;
int halfBlock = blockSize / 2;
for (int dy = -halfBlock; dy <= halfBlock; dy++) {
for (int dx = -halfBlock; dx <= halfBlock; dx++) {
int xl = x + dx;
int yl = y + dy;
int xr = xl - d;
if (xr < 0 || xr >= right.cols) continue;
float IL = left.at<uchar>(yl, xl);
float IR = right.at<uchar>(yl, xr);
float IR_minus = (right.at<uchar>(yl, xr) + right.at<uchar>(yl, xr - 1)) / 2.0;
float IR_plus = (right.at<uchar>(yl, xr) + right.at<uchar>(yl, xr + 1)) / 2.0;
float diff = std::min({std::abs(IL - IR), std::abs(IL - IR_minus), std::abs(IL - IR_plus)});
cost += diff;
}
}
return cost / ((blockSize * blockSize));
}
4.3 方法 2: Census 变换
更鲁棒于光照变化,OpenCV 在某些模式下使用。
- 对每个像素的邻域进行二值化编码
- 使用 Hamming 距离比较左右图像的 Census 串
公式:
ξ(x, y) = ⊗_(i,j)∈W 1_I(x+i, y+j) > I(x, y)
C_Census(x, y, d) = Hamming(ξ_L(x, y), ξ_R(x-d, y))
uint64_t computeCensus(int x, int y, const Mat& img) {
uint64_t census = 0;
uchar center = img.at<uchar>(y, x);
for (int dy = -4; dy <= 4; dy++) {
for (int dx = -4; dx <= 4; dx++) {
if (dx == 0 && dy == 0) continue;
uchar neighbor = img.at<uchar>(y + dy, x + dx);
census <<= 1;
if (neighbor > center) census |= 1;
}
}
return census;
}
int hammingDistance(uint64_t a, uint64_t b) {
return __builtin_popcountll(a ^ b);
}
4.4 代价体示意图
代价体 C[x, y, d]: d (视差) ↓
0 1 2 ... 63
x=0 [12, 45, 78, ..., 23]
x=1 [34, 12, 56, ..., 89]
...
每个 C(x, y, d) 存储像素 (x, y) 在视差 d 下的匹配代价。
5. 步骤 3: 代价聚合 (Cost Aggregation)
5.1 核心思想
这是 SGBM 的灵魂步骤。通过多个方向的路径进行动态规划,将局部代价聚合成考虑全局约束的代价。
5.2 单路径动态规划公式
对于路径方向 r,递推计算累积代价:
L_r(p, d): 沿路径 r 到像素 p 视差为 d 的最小累积代价
p-r: 路径 r 方向上的前一个像素
C(p, d): 当前像素的原始匹配代价
P1: 视差变化 ±1 的惩罚(平滑项)
P2: 视差大幅变化的惩罚(断裂项)
最后的归一化项防止数值溢出
5.3 路径方向
| 模式 | 路径数量 | 方向角度 | 计算量 | 质量 |
|---|
MODE_SGBM | 5 | 0°, 45°, 90°, 135°, 垂直加强 | 低 | 中 |
MODE_HH | 8 | 全方向 | 高 | 高 |
MODE_SGBM_3WAY | 8 | 3 方向×3 遍历 | 中 | 较高 |
8 方向示意图:
135° 90° 45° ↖ ↑ ↗
180° ← [p] → 0°
↙ ↓ ↘
225° 270° 315°
5.4 动态规划详细过程
void aggregateCostPath(const Mat& cost, Mat& L, int direction, int P1, int P2) {
int rows = cost.rows;
int cols = cost.cols;
int dispRange = cost.size[2];
int dx = direction_dx[direction];
int dy = direction_dy[direction];
for (int y = 0; y < rows; y++) {
for (int x = 0; x < cols; x++) {
int prev_x = x - dx;
int prev_y = y - dy;
if (prev_x < 0 || prev_x >= cols || prev_y < 0 || prev_y >= rows) {
for (int d = 0; d < dispRange; d++) {
L.at<float>(y, x, d) = cost.at<float>(y, x, d);
}
continue;
}
float prev_min = FLT_MAX;
for (int d = 0; d < dispRange; d++) {
prev_min = std::min(prev_min, L.at<float>(prev_y, prev_x, d));
}
for (int d = 0; d < dispRange; d++) {
float C_curr = cost.at<float>(y, x, d);
float cost0 = L.at<float>(prev_y, prev_x, d);
float cost1 = (d > 0) ? L.at<float>(prev_y, prev_x, d - 1) + P1 : FLT_MAX;
float cost2 = (d < dispRange - 1) ? L.at<float>(prev_y, prev_x, d + 1) + P1 : FLT_MAX;
float cost3 = prev_min + P2;
float min_cost = std::min({cost0, cost1, cost2, cost3});
L.at<float>(y, x, d) = C_curr + min_cost - prev_min;
}
}
}
}
5.5 多路径聚合
将所有路径的累积代价求和:
S(p, d) = Σ_r=1^P L_r(p, d)
void aggregateAllPaths(const Mat& cost, Mat& S, int P1, int P2) {
int numPaths = 8;
S = Mat::zeros(cost.size(), CV_32F);
for (int r = 0; r < numPaths; r++) {
Mat L_r;
aggregateCostPath(cost, L_r, r, P1, P2);
S += L_r;
}
}
5.6 自适应惩罚系数
在图像边缘处降低 P2,允许视差断裂:
P2_adaptive(p, p-r) = P2 / max(|∇I_p|, |∇I_{p-r}|, ε)
float getAdaptiveP2(int x, int y, int prev_x, int prev_y, const Mat& gradX, const Mat& gradY, float P2_base) {
float grad1 = std::abs(gradX.at<short>(y, x)) + std::abs(gradY.at<short>(y, x));
float grad2 = std::abs(gradX.at<short>(prev_y, prev_x)) + std::abs(gradY.at<short>(prev_y, prev_x));
float maxGrad = std::max({grad1, grad2, 1.0f});
return P2_base / maxGrad;
}
6. 步骤 4: 视差计算 (Disparity Computation)
6.1 WTA (Winner-Takes-All) 策略
选择累积代价最小的视差作为最终视差:
D(x, y) = argmin_d∈[0, D_max] S(x, y, d)
cv::Mat computeDisparityWTA(const Mat& S) {
int rows = S.rows;
int cols = S.cols;
int dispRange = S.size[2];
Mat disparity(rows, cols, CV_16S);
for (int y = 0; y < rows; y++) {
for (int x = 0; x < cols; x++) {
float minCost = FLT_MAX;
int bestDisp = 0;
for (int d = 0; d < dispRange; d++) {
float cost = S.at<float>(y, x, d);
if (cost < minCost) {
minCost = cost;
bestDisp = d;
}
}
disparity.at<short>(y, x) = bestDisp * 16;
}
}
return disparity;
}
6.2 亚像素增强 (Subpixel Refinement)
数学原理:
假设代价函数在最优视差附近呈二次曲线:
S(d) ≈ a(d - d_0)^2 + b
通过三点 (d-1, S_{d-1}), (d, S_d), (d+1, S_{d+1}) 拟合,得到:
d_sub = d - (S(d-1) - S(d+1)) / (2(S(d-1) - 2S(d) + S(d+1)))
float subpixelInterpolation(float cost_prev, float cost_curr, float cost_next) {
float numerator = cost_prev - cost_next;
float denominator = 2.0 * (cost_prev - 2.0 * cost_curr + cost_next);
if (std::abs(denominator) < 1e-6) {
return 0.0f;
}
float delta = numerator / denominator;
return std::max(-1.0f, std::min(1.0f, delta));
}
void refineDisparity(Mat& disparity, const Mat& S) {
int dispRange = S.size[2];
for (int y = 0; y < disparity.rows; y++) {
for (int x = 0; x < disparity.cols; x++) {
short d16 = disparity.at<short>(y, x);
int d = d16 / 16;
if (d <= 0 || d >= dispRange - 1) continue;
float cost_prev = S.at<float>(y, x, d - 1);
float cost_curr = S.at<float>(y, x, d);
float cost_next = S.at<float>(y, x, d + 1);
float delta = subpixelInterpolation(cost_prev, cost_curr, cost_next);
disparity.at<short>(y, x) = (d + delta) * 16;
}
}
}
效果对比:
像素级:d = 10 → 深度误差 ±5cm (假设基线 100mm, 焦距 500px)
亚像素:d = 10.3125 → 深度误差 ±0.3cm (提升 16 倍)
7. 步骤 5: 后处理 (Post-processing)
7.1 左右一致性检查 (Left-Right Consistency Check)
- 计算右视差图 D_R (将左右图像交换再计算)
- 对于左图像素 (x, y),其对应右图像素为 (x - D_L(x, y), y)
- 若两者视差不一致,则标记为无效
公式:
Valid = { 1 if |D_L(x, y) - D_R(x - D_L(x, y), y)| ≤ disp12MaxDiff
0 otherwise }
void leftRightCheck(Mat& dispLeft, const Mat& dispRight, int disp12MaxDiff) {
for (int y = 0; y < dispLeft.rows; y++) {
for (int x = 0; x < dispLeft.cols; x++) {
short dL = dispLeft.at<short>(y, x);
if (dL == -16) continue;
int d = dL / 16;
int x_right = x - d;
if (x_right < 0 || x_right >= dispRight.cols) {
dispLeft.at<short>(y, x) = -16;
continue;
}
short dR = dispRight.at<short>(y, x_right);
if (std::abs(dL - dR) > disp12MaxDiff * 16) {
dispLeft.at<short>(y, x) = -16;
}
}
}
}
disp12MaxDiff = 1: 严格模式,适用于高质量标定
disp12MaxDiff = 2: 宽松模式,适用于有畸变的场景
disp12MaxDiff = -1: 禁用检查(不推荐)
7.2 唯一性检查 (Uniqueness Ratio Check)
目的: 过滤匹配不唯一的像素(多个视差代价接近)。
公式:
Uniqueness = (S_2ndmin - S_min) / S_min × 100
若 Uniqueness < uniquenessRatio,则标记为无效。
void uniquenessCheck(Mat& disparity, const Mat& S, int uniquenessRatio) {
int dispRange = S.size[2];
for (int y = 0; y < disparity.rows; y++) {
for (int x = 0; x < disparity.cols; x++) {
short d16 = disparity.at<short>(y, x);
if (d16 == -16) continue;
int d = d16 / 16;
float minCost = S.at<float>(y, x, d);
float secondMinCost = FLT_MAX;
for (int d2 = 0; d2 < dispRange; d2++) {
if (d2 == d) continue;
float cost = S.at<float>(y, x, d2);
if (cost < secondMinCost) {
secondMinCost = cost;
}
}
float ratio = (secondMinCost - minCost) / minCost * 100.0;
if (ratio < uniquenessRatio) {
disparity.at<short>(y, x) = -16;
}
}
}
}
uniquenessRatio = 5: 非常严格,会产生较多空洞
uniquenessRatio = 10: 平衡模式(推荐)
uniquenessRatio = 15: 宽松模式,保留更多匹配
7.3 斑点滤波 (Speckle Filtering)
- 使用连通域标记找到所有连通区域
- 计算每个区域的大小和内部视差方差
- 若区域大小 < speckleWindowSize 且方差 > speckleRange,则标记为噪声
void speckleFilter(Mat& disparity, int speckleWindowSize, int speckleRange) {
if (speckleWindowSize == 0) return;
Mat labels, stats, centroids;
int numComponents = cv::connectedComponentsWithStats(disparity > 0,
labels, stats, centroids, 8);
for (int i = 1; i < numComponents; i++) {
int area = stats.at<int>(i, cv::CC_STAT_AREA);
if (area < speckleWindowSize) {
std::vector<short> disparities;
for (int y = 0; y < disparity.rows; y++) {
for (int x = 0; x < disparity.cols; x++) {
if (labels.at<int>(y, x) == i) {
disparities.push_back(disparity.at<short>(y, x) / 16);
}
}
}
float mean = std::accumulate(disparities.begin(), disparities.end(), 0.0) / disparities.size();
float variance = 0;
for (auto d : disparities) {
variance += (d - mean) * (d - mean);
}
float stddev = std::sqrt(variance / disparities.size());
if (stddev > speckleRange) {
for (int y = 0; y < disparity.rows; y++) {
for (int x = 0; x < disparity.cols; x++) {
if (labels.at<int>(y, x) == i) {
disparity.at<short>(y, x) = -16;
}
}
}
}
}
}
}
speckleWindowSize = 100: 过滤小于 100 像素的连通区域
speckleRange = 2: 区域内视差变化超过 2 则视为噪声
- 设为 0 可禁用斑点滤波(提升速度)
7.4 中值滤波 (可选)
对最终视差图进行 3×3 或 5×5 中值滤波平滑:
cv::medianBlur(disparity, disparity, 5);
8. 完整代码示例
8.1 基础调用
#include <opencv2/opencv.hpp>
#include <opencv2/calib3d.hpp>
int main() {
cv::Mat leftRect = cv::imread("left_rect.png", cv::IMREAD_GRAYSCALE);
cv::Mat rightRect = cv::imread("right_rect.png", cv::IMREAD_GRAYSCALE);
int minDisparity = 0;
int numDisparities = 128;
int blockSize = 5;
int P1 = 8 * blockSize * blockSize;
int P2 = 32 * blockSize * blockSize;
cv::Ptr<cv::StereoSGBM> sgbm = cv::StereoSGBM::create(
minDisparity, numDisparities, blockSize, P1, P2, 1,
63,
10,
100,
32,
cv::StereoSGBM::MODE_SGBM_3WAY
);
cv::Mat disparity16S;
sgbm->compute(leftRect, rightRect, disparity16S);
cv::Mat disparity32F;
disparity16S.convertTo(disparity32F, CV_32F, 1.0 / 16.0);
cv::Mat validMask = disparity16S > 0;
disparity32F.setTo(0, ~validMask);
cv::Mat disparityVis;
cv::normalize(disparity32F, disparityVis, 0, 255, cv::NORM_MINMAX, CV_8U);
cv::applyColorMap(disparityVis, disparityVis, cv::COLORMAP_JET);
cv::imshow("Disparity Map", disparityVis);
cv::waitKey(0);
float baseline = 0.120;
float focal = 718.856;
cv::Mat depth = (baseline * focal) / disparity32F;
return 0;
}
8.2 高级参数调优
class SGBMTuner {
public:
static cv::Ptr<cv::StereoSGBM> createForIndoor() {
return cv::StereoSGBM::create(
0, 128, 3, 72,
288,
1,
31,
10,
50,
1,
cv::StereoSGBM::MODE_HH
);
}
static cv::Ptr<cv::StereoSGBM> createForOutdoor() {
return cv::StereoSGBM::create(
0, 192, 7, 392,
1568,
2,
63,
15,
200,
2,
cv::StereoSGBM::MODE_SGBM_3WAY
);
}
static cv::Ptr<cv::StereoSGBM> createForRealtime() {
return cv::StereoSGBM::create(
0, 64, 5, 200, 800, -1,
63, 5, 0,
0,
cv::StereoSGBM::MODE_SGBM
);
}
};
8.3 视差转深度
cv::Mat disparityToDepth(const cv::Mat& disparity, float baseline, float focal, float minDepth = 0.1, float maxDepth = 10.0) {
cv::Mat depth = cv::Mat::zeros(disparity.size(), CV_32F);
for (int y = 0; y < disparity.rows; y++) {
for (int x = 0; x < disparity.cols; x++) {
float disp = disparity.at<float>(y, x);
if (disp <= 0) {
depth.at<float>(y, x) = 0;
continue;
}
float d = (baseline * focal) / disp;
if (d < minDepth || d > maxDepth) {
depth.at<float>(y, x) = 0;
} else {
depth.at<float>(y, x) = d;
}
}
}
return depth;
}
9. 工程优化技巧
9.1 性能优化
cv::Rect roi(100, 100, 800, 600);
cv::Mat dispROI;
sgbm->compute(leftRect(roi), rightRect(roi), dispROI);
cv::Mat leftSmall, rightSmall;
cv::resize(leftRect, leftSmall, cv::Size(), 0.5, 0.5);
cv::resize(rightRect, rightSmall, cv::Size(), 0.5, 0.5);
cv::resize(disparity, disparity, leftRect.size());
disparity *= 2;
9.2 参数自适应
int computeOptimalBlockSize(const cv::Mat& image) {
cv::Mat grad;
cv::Sobel(image, grad, CV_16S, 1, 1);
cv::Scalar meanGrad = cv::mean(grad);
if (meanGrad[0] > 50) {
return 3;
} else if (meanGrad[0] > 20) {
return 5;
} else {
return 7;
}
}
9.3 错误诊断
| 问题 | 可能原因 | 解决方案 |
|---|
| 视差图全黑 | 未进行极线校正 | 使用 stereoRectify + remap |
| 大量空洞 | uniquenessRatio 过小 | 增大到 10-15 |
| 过度平滑 | P2 过大 | 降低到 4 × P1 |
| 噪点多 | P1 过小 | 增大到 8 × blockSize² |
| 边缘断裂 | 未启用自适应 P2 | 使用 MODE_HH 模式 |
| 计算慢 | 路径过多 | 改用 MODE_SGBM (5 路径) |
9.4 质量评估
float evaluateDisparity(const cv::Mat& computed, const cv::Mat& groundTruth, float threshold = 1.0) {
int badPixels = 0;
int totalPixels = 0;
for (int y = 0; y < computed.rows; y++) {
for (int x = 0; x < computed.cols; x++) {
float gt = groundTruth.at<float>(y, x);
if (gt == 0) continue;
float comp = computed.at<float>(y, x);
if (std::abs(comp - gt) > threshold) {
badPixels++;
}
totalPixels++;
}
}
return (float)badPixels / totalPixels * 100.0;
}
10. 总结
10.1 算法特点
| 优点 | 缺点 |
|---|
| 密集匹配 (90%+ 覆盖率) | 计算量大 (实时需优化) |
| 边缘保持好 | 弱纹理区域易失败 |
| 抗噪声能力强 | 参数敏感 |
| 亚像素精度 | 需要良好的极线校正 |
10.2 适用场景
- ✅ 自动驾驶: 道路障碍物检测
- ✅ 机器人导航: SLAM 中的深度估计
- ✅ 工业检测: 3D 尺寸测量
- ✅ AR/VR: 实时深度感知
- ❌ 高速运动: 帧率要求 > 60fps (考虑 GPU 加速)
- ❌ 极弱纹理: 如白墙 (需结合结构光)
10.3 进阶方向
- 深度学习融合: 使用 CNN 计算初始代价体 (如 PSMNet)
- 时序一致性: 利用视频序列前后帧约束
- GPU 加速: CUDA 实现路径聚合并行化
- 自适应参数: 根据场景动态调整 P1, P2
参考文献
- Hirschmüller, H. (2007). 'Stereo Processing by Semiglobal Matching and Mutual Information'. IEEE TPAMI, 30(2), 328-341.
- OpenCV Documentation: StereoSGBM Class Reference
- Birchfield, S., & Tomasi, C. (1998). 'A Pixel Dissimilarity Measure That Is Insensitive to Image Sampling'. IEEE TPAMI, 20(4), 401-406.
相关免费在线工具
- 加密/解密文本
使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,online
- RSA密钥对生成器
生成新的随机RSA私钥和公钥pem证书。 在线工具,RSA密钥对生成器在线工具,online
- Mermaid 预览与可视化编辑
基于 Mermaid.js 实时预览流程图、时序图等图表,支持源码编辑与即时渲染。 在线工具,Mermaid 预览与可视化编辑在线工具,online
- 随机西班牙地址生成器
随机生成西班牙地址(支持马德里、加泰罗尼亚、安达卢西亚、瓦伦西亚筛选),支持数量快捷选择、显示全部与下载。 在线工具,随机西班牙地址生成器在线工具,online
- Gemini 图片去水印
基于开源反向 Alpha 混合算法去除 Gemini/Nano Banana 图片水印,支持批量处理与下载。 在线工具,Gemini 图片去水印在线工具,online
- Base64 字符串编码/解码
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online