mattdawkins's picture
Add model card
0803818 verified
|
Raw
History Blame Contribute Delete
6.87 kB
---
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.