CV Stereo Disparity (BM)
Faster Than SGBM, Full of Holes, Still Worth Having
- left
- right
- disparity
- valid
- valid_fraction
Block matching is the original stereo algorithm and it makes no apologies: compare a small window on one scanline against windows on the other, take the best SAD score, move on. No global smoothness, no propagation between scanlines, no second pass. It runs several times faster than SGBM and gives you a disparity map with holes punched through every low-texture region - flat walls, sky, road, blurry backgrounds.
That's the deal, and it's a fine deal for three jobs: a fast preview while you tune, an honest speed/quality comparison, and scenes that are richly textured where BM does surprisingly well.
How it works
cv2.StereoBM is another class API the generated wrappers can't reach, so the node creates it, configures it from its widgets, computes, and drops it. The node clamps block_size to odd within 5..51 (and no larger than the image, which would raise), so you can't crash it from the UI.
Two BM-specific knobs are worth understanding.
texture_threshold is why BM leaves holes. It rejects a block whose summed x-gradient is below the threshold - a flat wall has no texture to match, so BM says "no idea" rather than guessing. Set it to 0 and every block gets accepted, which mostly means noise. And note the units: it's a sum over the whole SAD window, so it only bites in the thousands, not at the default 10. The widget ceiling of 163863 is block_size² × prefilter_cap at the node's own maxima - above that, BM would reject literally every pixel, so the range is set to the point where the parameter stops meaning anything.
prefilter handles the two cameras disagreeing about exposure. x-Sobel (the default, and OpenCV's own) applies an x-gradient clipped to prefilter_cap; the older normalized response is more forgiving of a smooth brightness difference between lenses but weaker on repetitive texture.
num_disparities, min_disparity, uniqueness_ratio, speckle_window_size, speckle_range and disp12_max_diff work as they do in SGBM.
The inputs and outputs that matter
left and right - rectified frames, epipolar lines horizontal. The node grayscales internally and resizes right to match left if needed, so don't be clever with pre-processing that changes one of them. Feed unrectified images and nothing complains; you just get garbage.
Outputs: disparity (HxW float32 in pixels), valid (uint8 0/255 mask), and - nicely - valid_fraction, the share of pixels that produced a value. That last one is the headline number when you're comparing BM against SGBM on your own footage; run both and read it before you trust either map. Preview with Preview CV Array in normalize or heatmap mode.
Any of this only means something after rectification: cv2_stereoRectify + initUndistortRectifyMap + cv2_remap, following CV Stereo Calibrate (Chessboard) or CV Stereo Rectify (Uncalibrated).
Install
ComfyUI Manager → search ComfyUI CV (publisher bmad4ever), or:
cd ComfyUI/custom_nodes
git clone https://github.com/bmad4ever/comfyui_cv
pip install "opencv-contrib-python-headless~=5.0.0.93"
Restart afterwards. Python ≥ 3.12 and a recent ComfyUI built on the V3 node API. Keep the contrib wheel even though this node is core cv2 - a plain opencv-python install shares site-packages/cv2 and quietly strips the contrib submodules, taking other nodes in the pack with it. tools/repair_opencv_contrib.py --check / --apply is the recovery tool.
workflows/58_stereo_bm_vs_sgbm.json wires BM, SGBM and the WLS variant off the same rectified pair, which is exactly the comparison this node exists for. Sample inputs come in via 01_install_example_inputs.json (run it, then reload the page).
Where it bites
The classic own-goal is judging BM on a flat-lit scene and concluding stereo depth is garbage. BM's holes are honest - it's telling you those blocks are untextured - and they're why SGBM (or WLS-filtered SGBM) is the default in every serious pipeline. Use BM when you want it fast or when you're sanity-checking something more sophisticated.
And the standard pack caveat: bmad4ever's README states plainly that this is a personal, heavily LLM-assisted project, not production-ready, with no support promised and stereo settings tuned to one dataset. The BM wrapper is faithful to OpenCV; the numbers you tune around it are on you.
Inputs (12)
| Name | Type | Default | Description |
|---|---|---|---|
| left | NPARRAY,IMAGE | Left rectified frame (grayscaled internally). Accepts a ComfyUI IMAGE/MASK directly (frame 0 of a batch) or an NPARRAY. Arithmetic ops (add, multiply, etc.) process the full IMAGE batch when both inputs have the same batch size. | |
| right | NPARRAY,IMAGE | Right rectified frame; resized to left if sizes differ. Accepts a ComfyUI IMAGE/MASK directly (frame 0 of a batch) or an NPARRAY. Arithmetic ops (add, multiply, etc.) process the full IMAGE batch when both inputs have the same batch size. | |
| num_disparities | INT | 6416–512 | Disparity search range (0..num_disparities), rounded up to a multiple of 16. Larger covers nearer objects / wider baselines but is slower. |
| block_size | INT | 155–51 | SAD window size in pixels; forced ODD and clamped to 5..51 (StereoBM rejects anything else). Larger = smoother but blurs depth edges; 15-21 is typical. |
| min_disparity | INT | 0-256–256 | Smallest disparity to search from (usually 0). |
| texture_threshold | INT | 100–163863 | Reject a block whose texture (summed x-gradient) is below this - the main reason BM leaves holes on flat walls. 0 accepts every block (noisy). It is a SUM over the whole SAD window, so it only bites in the thousands: the ceiling above which every pixel is rejected is block_size^2 * prefilter_cap (about 7000 at the defaults, 163863 at block_size 51 / prefilter_cap 63). |
| uniqueness_ratio | INT | 100–100 | Margin (%) by which the best match must beat the runner-up to be accepted. |
| prefilteropt | COMBO | x-Sobel | Pre-filter applied before matching to remove lighting differences between the two cameras. x-Sobel (an x gradient, clipped to prefilter_cap) is what OpenCV itself defaults to and the usual choice. normalized response is the older local brightness normalisation kept from the legacy C API - it is more tolerant of a smooth exposure difference between the cameras but weaker on repetitive texture. |
| prefilter_capopt | INT | 311–63 | Pre-filtered values are clipped to +/- this. |
| speckle_window_sizeopt | INT | 1000–1000 | Largest smooth disparity blob treated as speckle noise and invalidated (0 = off). |
| speckle_rangeopt | INT | 20–64 | Max disparity variation within a speckle component. |
| disp12_max_diffopt | INT | -1-1–256 | Max allowed left-right consistency error in pixels (-1 = do not run the left-right check). |
Outputs (3)
| Name | Type | Description |
|---|---|---|
| disparity | NPARRAY | HxW float32 disparity in pixels; rejected pixels are below min_disparity (see 'valid'). |
| valid | NPARRAY | uint8 0/255 mask of pixels with a valid disparity - BM rejects many more than SGBM, so always check it. |
| valid_fraction | FLOAT | Fraction of pixels with a valid disparity (0..1) - the headline number when comparing BM against SGBM. |