Dense Stereo

Once a pair is rectified, mvg::dense::match_block produces a per-pixel disparity map. It is a pure-Rust matcher: no OpenCV and no GPU.

Block Matching

The matcher scores each pixel and each candidate disparity with the zero-mean normalized cross-correlation (ZNCC) over a square window, which tolerates a per-camera gain and bias. It selects the winner per pixel, refines it to sub-pixel precision with a parabola through the correlation peak, and filters with left-right consistency, a uniqueness check and a minimum correlation. Pixels that fail a check are NaN in the result.

#![allow(unused)]
fn main() {
use vision_calibration::mvg::dense::{match_block, BlockMatchOptions, GrayImage};

// `left` / `right` are rectified grayscale images: `rect.rectify_image(0, ..)`
// and `rect.rectify_image(1, ..)` of the raw pair (see Stereo from a Calibration).
let disp = match_block(&left, &right, &BlockMatchOptions {
    min_disparity: 0,
    num_disparities: 64,   // search [min, min + num)
    block_size: 9,         // odd; larger = smoother, fewer matches near edges
    ..Default::default()
})?;
// disp.get(x, y) is x_left − x_right in pixels, or NaN where the matcher abstains.
}
from vision_calibration import mvg

left = rect.rectify_image(0, raw_left, 640, 480)     # (H, W) uint8 arrays
right = rect.rectify_image(1, raw_right, 640, 480)
disp = mvg.match_block(left, right, mvg.BlockMatchOptions(num_disparities=64, block_size=9))
# disp is a (H, W) float32 array: x_left − x_right in pixels, NaN where the matcher abstains.

GrayImage::new(width, height, data) wraps row-major 8-bit pixels. The options and their defaults:

OptionDefaultMeaning
min_disparity, num_disparities0, 64the searched disparity range [min, min + num)
block_size7square window side in pixels; odd
min_correlation0.5reject a pixel whose best ZNCC is below this
uniqueness_ratio0.03reject a pixel unless its best ZNCC beats the best non-adjacent alternative by this margin
lr_consistency, lr_max_diffon, 1.0 pxthe left-right cross-check and its tolerance
subpixelonparabolic refinement of the peak
semi_global, sgm_p1, sgm_p2off, 0.1, 1.5semi-global aggregation (below)

match_block returns an MvgError::InvalidInput for images of different sizes, an even or zero block_size, a non-positive num_disparities, or images smaller than the window. The result is a DisparityMap with get, is_valid, valid_count and valid_range.

Semi-Global Matching

For low-texture surfaces (the flat interiors of chessboard squares, for instance) set semi_global: true. The matching cost is then aggregated along 8 paths with the sgm_p1 penalty for a one-pixel disparity step (small, so slanted surfaces survive) and the sgm_p2 penalty for larger jumps (large, so depth discontinuities survive; sgm_p2 must be at least sgm_p1). Disparity then propagates into regions block matching leaves blank: on a real chessboard rig coverage rose from about 21 % to about 49 %.

Semi-global matching is strictly additive: block matching stays the default. In SGM mode min_correlation and uniqueness_ratio (which gate on the raw per-pixel correlation) are bypassed; the left-right consistency check and the disparity window are the filters. It costs extra time and memory.

Disparity to Depth

For a rectified pair with shared focal length (pixels) and baseline , a disparity gives depth . StereoRectification converts for you (point, the matrix disparity_to_depth, and the disparity's covariance; see Stereo from a Calibration). The desktop app's Depth workspace uses it and renders depth and a reprojected point cloud (see Inspecting Results).

Try It

A runnable demo writes inspectable PNGs (left, right, ground truth, estimate and error) to target/fixtures/:

cargo run -p vision-calibration-bench --example dense_synth --features tier-b --release