File size: 6,870 Bytes
d72fed5 0803818 d72fed5 0803818 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 | ---
license: cc-by-4.0
tags:
- onnx
- stereo
- stereo-matching
- computer-vision
- viame
---
# VIAME Stereo Template Matcher
Epipolar template matching for stereo correspondence, exported from
[VIAME](https://github.com/VIAME/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:
```bash
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
```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](https://github.com/Kitware/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](https://github.com/Kitware/dive/pull/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](https://github.com/VIAME/VIAME), whose core infrastructure is BSD-3-Clause.
|