Virtual rectification for dense stereo
This page shows how to use the ray-field virtual rectification to obtain scanline-aligned stereo pairs (horizontal epipolars) so you can run a standard dense pipeline (BM/SGM/Census) exactly as in a pinhole setup.
The key idea is to build two dense warps (left/right) that map a virtual rectified pinhole
grid back to the source images using the calibrated ray-field and the stereo rig R,t.
Downstream code (rectification → disparity → depth) remains unchanged; only the remap is different.
Drop-in workflow (pinhole-compatible)
Calibrate a ray-field stereo rig (or any backend exposing
pixel -> direction).Build rectification maps with :mod:
stereocomplex.ray3d.rayfield_rectify:from stereocomplex.ray3d.rayfield_rectify import RectifyParams, build_virtual_rectify_maps, rectify_pair mapx_L, mapy_L, mapx_R, mapy_R, R_rect = build_virtual_rectify_maps( ray_L, ray_R, R_lr, t_lr, RectifyParams(width=W, height=H) ) I_L_rect, I_R_rect = rectify_pair(I_L, I_R, mapx_L, mapy_L, mapx_R, mapy_R)
Run any 1D dense matcher (BM/SGM/Census) on the rectified pair.
Convert disparity to depth either with a virtual pinhole
Qor with ray-intersection (the latter is more consistent with the ray-field).
The virtual camera intrinsics are chosen to maximize valid coverage; by default
fx'=fy'=0.9*W' and cx'=W'/2, cy'=H'/2. The rectified axes are built from the
baseline direction, so the epipolars become horizontal.
Implementation overview (maps for cv2.remap)
The rectification is implemented as a dense warp toward a virtual rectified pinhole camera.
For each pixel (u', v') in the rectified images:
Virtual ray: build a unit direction
d_rect = normalize([(u'-cx')/fx', (v'-cy')/fy', 1]).Physical rays: rotate
d_rectinto the left and right camera frames using the rectified axes derived from the baseline.Inverse mapping (direction → pixel): since the calibrated model is forward (
pixel → direction), each target direction must be inverted back to a source pixel(u,v):coarse init via a quantized inverse LUT (
lut_use/lut_quant),fallback init via a coarse image grid (
coarse_step),refinement via a few Gauss–Newton iterations minimizing the angular error between
dir(u,v)and the target direction.
Fill maps: write
mapx/mapyfor left and right; invalid pixels (direction outside the model FOV) are marked as-1and filled bycv2.remapborder policy.
The LUT and Newton refinement are internal details of build_virtual_rectify_maps, but they are the key
reason virtual rectification is practical: the dense maps are computed once per model, cached, then reused.
End-to-end example
The repository now ships a runnable demo that:
loads an exported ray-field stereo model,
builds virtual rectification maps,
rectifies one stereo pair,
runs a standard dense matcher on the rectified pair,
and reports the residual ChArUco vertical disparity before/after rectification.
If you already have an exported model (model.json + weights.npz), the demo is:
PYTHONPATH=src .venv/bin/python docs/examples/rayfield_virtual_rectification_demo.py \
dataset/v0_png --split train --scene scene_0000 --frame-id 0 \
--model models/scene0000_rayfield3d \
--out docs/assets/rayfield_virtual_rectify_demo
If you do not have an exported model yet, the same demo can calibrate a small one first and
store it under --out/model:
PYTHONPATH=src .venv/bin/python docs/examples/rayfield_virtual_rectification_demo.py \
dataset/v0_png --split train --scene scene_0000 --frame-id 0 \
--out docs/assets/rayfield_virtual_rectify_demo
The output directory contains:
left_raw.png,right_raw.pngleft_rectified.png,right_rectified.pngdisparity.pnganddisparity.npyrectify_maps.npzsummary.jsonwith rectification validity and residual vertical disparity statistics
For your own code, the core usage pattern is:
from pathlib import Path
from stereocomplex.api import load_stereo_central_rayfield
from stereocomplex.ray3d.rayfield_rectify import RectifyParams, build_virtual_rectify_maps, rectify_pair
model = load_stereo_central_rayfield(Path("models/scene0000_rayfield3d"))
ray_L = ... # any object exposing dir(u, v), width, height
ray_R = ...
params = RectifyParams(width=model.image_width_px, height=model.image_height_px)
mapx_L, mapy_L, mapx_R, mapy_R, _ = build_virtual_rectify_maps(ray_L, ray_R, model.R_RL, model.t_RL, params)
I_L_rect, I_R_rect = rectify_pair((I_L, I_R), (mapx_L, mapy_L, mapx_R, mapy_R), params)
The shipped demo script wraps StereoCentralRayFieldModel.left/right into that tiny adapter
automatically; you only need to write the adapter yourself if you integrate the rectifier into
custom code.
For a guided, cell-by-cell walkthrough, see the companion notebook:
examples/notebooks/03_rayfield_virtual_rectification.ipynb (with the linear export
examples/notebooks/03_rayfield_virtual_rectification.py).
Once you have I_L_rect and I_R_rect, you can drop them into any standard scanline matcher
such as cv2.StereoSGBM_create(...), then convert disparity to depth as usual.
Visual sanity check (synthetic pinhole)
Below is a synthetic check on a pinhole rig (baseline 10 cm, random 3D points, 640×480): the virtual rectification collapses vertical disparity to ~0 px, making the pair compatible with standard 1D matching.
.. figure:: assets/virtual_rectify_hist.png :alt: Histogram of vertical disparity before/after virtual rectification (synthetic pinhole) :width: 80%
Histogram of vertical disparity (v_L - v_R, pixels) before/after virtual rectification on a synthetic pinhole rig.
Notes and limits
The inversion
direction -> pixelis solved by a small Newton loop with finite-difference Jacobians; maps are meant to be precomputed and cached once per model. A coarse inverse LUT (quantized directions) is used as an initial guess before Newton, with a fallback on a coarse image grid if the LUT bin is empty.If a direction is outside the FOV of the ray-field, the corresponding rectified pixel is marked invalid (cv2.remap fills it with the border value).
For depth recovery, you can either use a virtual
Q(pinhole-like) or intersect the two rays associated with the rectified disparity (more exact for ray-field).