VIAME Stereo Template Matcher
Epipolar template matching for stereo correspondence, exported from VIAME as a single ONNX graph. Given a set of points in the left image of a calibrated stereo pair, it returns the matching points in the right image, plus match scores.
This is VIAME's stereo measurement method 1
(epipolar_template_matching): for each source point it walks the epipolar curve implied
by the calibration, sampling candidate depths, and picks the best NCC (TM_CCOEFF_NORMED)
template match. Pair it with two-view triangulation to measure real-world lengths β the
usual application is measuring fish in stereo camera rigs.
There are no learned weights. The graph is pure geometry plus normalized cross-correlation, which is why it is 89 KB. Nothing here was trained, and there is no training data or evaluation set.
Files
| File | Size | Notes |
|---|---|---|
stereo_match.onnx |
89 KB | opset 18, IR 8, float32 throughout |
Baked-in constants
Two parameters are frozen into the graph at export time and are not runtime inputs:
| Constant | Value |
|---|---|
template_size |
13 (13Γ13 NCC patch) |
num_samples |
5000 (depth samples along the epipolar curve) |
These match configs/pipelines/interactive_stereo_template.conf in VIAME, which is what
the DIVE desktop interactive stereo service loads. Note they are not the export
script's own defaults (25 / 5000) β re-exporting without explicit flags produces a model
that loads identically but matches differently. To reproduce this exact file:
python plugins/onnx/export_stereo_mapping.py --model match \
--out stereo_match.onnx --template-size 13 --num-samples 5000
Inputs
All float32. The world frame is the left camera, so the left camera is normally
R_left = I, t_left = 0, and the right camera carries the rig's relative pose.
Matrices are row-major.
| Name | Shape | Meaning |
|---|---|---|
left_gray |
[H, W] |
Left image, grayscale, 0β255 |
right_gray |
[Hr, Wr] |
Right image, grayscale, 0β255 |
points_left |
[P, 2] |
Source points (x, y) in left-image pixels |
K_left |
[3, 3] |
Left intrinsics |
dist_left |
[8] |
Left distortion [k1, k2, p1, p2, k3, k4, k5, k6], zero-padded |
R_left |
[3, 3] |
Left rotation (identity in the usual convention) |
t_left |
[3] |
Left translation (zero in the usual convention) |
K_right |
[3, 3] |
Right intrinsics |
dist_right |
[8] |
Right distortion |
R_right |
[3, 3] |
Rig rotation, left β right |
t_right |
[3] |
Rig translation, in calibration units |
min_depth |
scalar | Near bound of the depth search |
max_depth |
scalar | Far bound of the depth search |
Grayscale must use BT.601 luma (0.299 R + 0.587 G + 0.114 B) to match the OpenCV
BGR2GRAY the reference implementation uses.
Outputs
| Name | Shape | Meaning |
|---|---|---|
right_points |
[P, 2] |
Matched points in right-image pixels |
best_score |
[P] |
Best NCC score |
second_score |
[P] |
Best NCC score outside a template_size neighborhood of the winner |
Acceptance thresholds
The graph deliberately does not apply a score threshold β it returns the best match unconditionally, so the host decides what to accept. VIAME's two reference hosts disagree, and both are defensible:
| Host | Threshold | Uniqueness ratio |
|---|---|---|
interactive_stereo_template.conf (DIVE desktop) |
0.5 | none |
plugins/onnx/run_epipolar_onnx.py |
0.2 | 0.85 |
The uniqueness test, where used, rejects a match when
second_score / best_score > ratio β i.e. the winner was not clearly better than an
unrelated candidate elsewhere on the curve. Useful on repetitive texture.
Search range
The graph takes a depth range, but disparity is usually the more natural way to think about it. Convert with:
min_depth = fx * baseline / max_disparity
max_depth = fx * baseline / min_disparity
where fx = K_left[0][0] and baseline = ||t_right||.
A disparity range of 2β300 px matches the DIVE desktop interactive stereo config, but this is scene-dependent: VIAME's batch measurement pipelines ship 7β724 for other rigs. Too wide invites false matches; too narrow misses the target entirely. Calibrate it to how far the same object actually shifts between your two cameras.
Usage
Python
import numpy as np, onnxruntime as ort
sess = ort.InferenceSession("stereo_match.onnx", providers=["CPUExecutionProvider"])
out = sess.run(None, {
"left_gray": left.astype(np.float32), # [H, W], 0-255
"right_gray": right.astype(np.float32),
"points_left": np.array([[330.4, 234.8]], np.float32),
"K_left": K1, "dist_left": d1, "R_left": np.eye(3, dtype=np.float32),
"t_left": np.zeros(3, np.float32),
"K_right": K2, "dist_right": d2, "R_right": R, "t_right": T,
"min_depth": np.float32(fx * baseline / 300),
"max_depth": np.float32(fx * baseline / 2),
})
right_points, best_score, second_score = out
accepted = best_score >= 0.5
Browser / Node (onnxruntime-web)
The graph is small and CPU-only, so it runs comfortably in a browser via the WASM
execution provider β this is how DIVE warps a detection
from one camera to the other with no backend. A complete client-side implementation lives
in client/dive-common/use/stereo/ (see
Kitware/dive#1709), covering calibration
parsing, the search-range conversion above, and two-view triangulation.
Not a transformers.js model
This is a bespoke geometry graph, not a transformer. It has no config.json, no tokenizer
or image processor, and no architecture in the transformers.js registry, so pipeline() /
AutoModel will not load it. Use onnxruntime (or onnxruntime-web) directly.
Swapping left and right
The graph always matches left β right. To warp a point annotated on the right camera,
invert the rig instead of swapping the inputs: the new world frame is the old right camera,
so R' = Rα΅ and T' = -Rα΅Β·T, with the intrinsics and distortion swapped between sides.
Accuracy
Validated against the VIAME C++ / Python reference implementation to roughly a quarter
pixel. Note the exporter's own verification reports a sub-pixel right_points difference
between eager PyTorch and onnxruntime (about 0.04 px at template_size=13) caused by
ties between equally-scoring epipolar candidates; scores match to ~1e-8.
License and provenance
CC-BY-4.0. Produced by plugins/onnx/export_stereo_mapping.py in
VIAME, whose core infrastructure is BSD-3-Clause.