Add model card
Browse files
README.md
CHANGED
|
@@ -1,3 +1,171 @@
|
|
| 1 |
---
|
| 2 |
license: cc-by-4.0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3 |
---
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
---
|
| 2 |
license: cc-by-4.0
|
| 3 |
+
tags:
|
| 4 |
+
- onnx
|
| 5 |
+
- stereo
|
| 6 |
+
- stereo-matching
|
| 7 |
+
- computer-vision
|
| 8 |
+
- viame
|
| 9 |
---
|
| 10 |
+
|
| 11 |
+
# VIAME Stereo Template Matcher
|
| 12 |
+
|
| 13 |
+
Epipolar template matching for stereo correspondence, exported from
|
| 14 |
+
[VIAME](https://github.com/VIAME/VIAME) as a single ONNX graph. Given a set of points in
|
| 15 |
+
the left image of a calibrated stereo pair, it returns the matching points in the right
|
| 16 |
+
image, plus match scores.
|
| 17 |
+
|
| 18 |
+
This is VIAME's stereo measurement **method 1**
|
| 19 |
+
(`epipolar_template_matching`): for each source point it walks the epipolar curve implied
|
| 20 |
+
by the calibration, sampling candidate depths, and picks the best NCC (`TM_CCOEFF_NORMED`)
|
| 21 |
+
template match. Pair it with two-view triangulation to measure real-world lengths — the
|
| 22 |
+
usual application is measuring fish in stereo camera rigs.
|
| 23 |
+
|
| 24 |
+
**There are no learned weights.** The graph is pure geometry plus normalized
|
| 25 |
+
cross-correlation, which is why it is 89 KB. Nothing here was trained, and there is no
|
| 26 |
+
training data or evaluation set.
|
| 27 |
+
|
| 28 |
+
## Files
|
| 29 |
+
|
| 30 |
+
| File | Size | Notes |
|
| 31 |
+
| --- | --- | --- |
|
| 32 |
+
| `stereo_match.onnx` | 89 KB | opset 18, IR 8, float32 throughout |
|
| 33 |
+
|
| 34 |
+
## Baked-in constants
|
| 35 |
+
|
| 36 |
+
Two parameters are **frozen into the graph at export time** and are not runtime inputs:
|
| 37 |
+
|
| 38 |
+
| Constant | Value |
|
| 39 |
+
| --- | --- |
|
| 40 |
+
| `template_size` | 13 (13×13 NCC patch) |
|
| 41 |
+
| `num_samples` | 5000 (depth samples along the epipolar curve) |
|
| 42 |
+
|
| 43 |
+
These match `configs/pipelines/interactive_stereo_template.conf` in VIAME, which is what
|
| 44 |
+
the DIVE desktop interactive stereo service loads. Note they are **not** the export
|
| 45 |
+
script's own defaults (25 / 5000) — re-exporting without explicit flags produces a model
|
| 46 |
+
that loads identically but matches differently. To reproduce this exact file:
|
| 47 |
+
|
| 48 |
+
```bash
|
| 49 |
+
python plugins/onnx/export_stereo_mapping.py --model match \
|
| 50 |
+
--out stereo_match.onnx --template-size 13 --num-samples 5000
|
| 51 |
+
```
|
| 52 |
+
|
| 53 |
+
## Inputs
|
| 54 |
+
|
| 55 |
+
All float32. The world frame is the **left camera**, so the left camera is normally
|
| 56 |
+
`R_left = I`, `t_left = 0`, and the right camera carries the rig's relative pose.
|
| 57 |
+
Matrices are row-major.
|
| 58 |
+
|
| 59 |
+
| Name | Shape | Meaning |
|
| 60 |
+
| --- | --- | --- |
|
| 61 |
+
| `left_gray` | `[H, W]` | Left image, grayscale, 0–255 |
|
| 62 |
+
| `right_gray` | `[Hr, Wr]` | Right image, grayscale, 0–255 |
|
| 63 |
+
| `points_left` | `[P, 2]` | Source points `(x, y)` in left-image pixels |
|
| 64 |
+
| `K_left` | `[3, 3]` | Left intrinsics |
|
| 65 |
+
| `dist_left` | `[8]` | Left distortion `[k1, k2, p1, p2, k3, k4, k5, k6]`, zero-padded |
|
| 66 |
+
| `R_left` | `[3, 3]` | Left rotation (identity in the usual convention) |
|
| 67 |
+
| `t_left` | `[3]` | Left translation (zero in the usual convention) |
|
| 68 |
+
| `K_right` | `[3, 3]` | Right intrinsics |
|
| 69 |
+
| `dist_right` | `[8]` | Right distortion |
|
| 70 |
+
| `R_right` | `[3, 3]` | Rig rotation, left → right |
|
| 71 |
+
| `t_right` | `[3]` | Rig translation, in calibration units |
|
| 72 |
+
| `min_depth` | scalar | Near bound of the depth search |
|
| 73 |
+
| `max_depth` | scalar | Far bound of the depth search |
|
| 74 |
+
|
| 75 |
+
Grayscale must use BT.601 luma (`0.299 R + 0.587 G + 0.114 B`) to match the OpenCV
|
| 76 |
+
`BGR2GRAY` the reference implementation uses.
|
| 77 |
+
|
| 78 |
+
## Outputs
|
| 79 |
+
|
| 80 |
+
| Name | Shape | Meaning |
|
| 81 |
+
| --- | --- | --- |
|
| 82 |
+
| `right_points` | `[P, 2]` | Matched points in right-image pixels |
|
| 83 |
+
| `best_score` | `[P]` | Best NCC score |
|
| 84 |
+
| `second_score` | `[P]` | Best NCC score outside a `template_size` neighborhood of the winner |
|
| 85 |
+
|
| 86 |
+
## Acceptance thresholds
|
| 87 |
+
|
| 88 |
+
The graph deliberately does **not** apply a score threshold — it returns the best match
|
| 89 |
+
unconditionally, so the host decides what to accept. VIAME's two reference hosts disagree,
|
| 90 |
+
and both are defensible:
|
| 91 |
+
|
| 92 |
+
| Host | Threshold | Uniqueness ratio |
|
| 93 |
+
| --- | --- | --- |
|
| 94 |
+
| `interactive_stereo_template.conf` (DIVE desktop) | 0.5 | none |
|
| 95 |
+
| `plugins/onnx/run_epipolar_onnx.py` | 0.2 | 0.85 |
|
| 96 |
+
|
| 97 |
+
The uniqueness test, where used, rejects a match when
|
| 98 |
+
`second_score / best_score > ratio` — i.e. the winner was not clearly better than an
|
| 99 |
+
unrelated candidate elsewhere on the curve. Useful on repetitive texture.
|
| 100 |
+
|
| 101 |
+
## Search range
|
| 102 |
+
|
| 103 |
+
The graph takes a **depth** range, but disparity is usually the more natural way to think
|
| 104 |
+
about it. Convert with:
|
| 105 |
+
|
| 106 |
+
```
|
| 107 |
+
min_depth = fx * baseline / max_disparity
|
| 108 |
+
max_depth = fx * baseline / min_disparity
|
| 109 |
+
```
|
| 110 |
+
|
| 111 |
+
where `fx = K_left[0][0]` and `baseline = ||t_right||`.
|
| 112 |
+
|
| 113 |
+
A disparity range of **2–300 px** matches the DIVE desktop interactive stereo config, but
|
| 114 |
+
this is scene-dependent: VIAME's batch measurement pipelines ship 7–724 for other rigs. Too
|
| 115 |
+
wide invites false matches; too narrow misses the target entirely. Calibrate it to how far
|
| 116 |
+
the same object actually shifts between your two cameras.
|
| 117 |
+
|
| 118 |
+
## Usage
|
| 119 |
+
|
| 120 |
+
### Python
|
| 121 |
+
|
| 122 |
+
```python
|
| 123 |
+
import numpy as np, onnxruntime as ort
|
| 124 |
+
|
| 125 |
+
sess = ort.InferenceSession("stereo_match.onnx", providers=["CPUExecutionProvider"])
|
| 126 |
+
out = sess.run(None, {
|
| 127 |
+
"left_gray": left.astype(np.float32), # [H, W], 0-255
|
| 128 |
+
"right_gray": right.astype(np.float32),
|
| 129 |
+
"points_left": np.array([[330.4, 234.8]], np.float32),
|
| 130 |
+
"K_left": K1, "dist_left": d1, "R_left": np.eye(3, dtype=np.float32),
|
| 131 |
+
"t_left": np.zeros(3, np.float32),
|
| 132 |
+
"K_right": K2, "dist_right": d2, "R_right": R, "t_right": T,
|
| 133 |
+
"min_depth": np.float32(fx * baseline / 300),
|
| 134 |
+
"max_depth": np.float32(fx * baseline / 2),
|
| 135 |
+
})
|
| 136 |
+
right_points, best_score, second_score = out
|
| 137 |
+
accepted = best_score >= 0.5
|
| 138 |
+
```
|
| 139 |
+
|
| 140 |
+
### Browser / Node (onnxruntime-web)
|
| 141 |
+
|
| 142 |
+
The graph is small and CPU-only, so it runs comfortably in a browser via the WASM
|
| 143 |
+
execution provider — this is how [DIVE](https://github.com/Kitware/dive) warps a detection
|
| 144 |
+
from one camera to the other with no backend. A complete client-side implementation lives
|
| 145 |
+
in `client/dive-common/use/stereo/` (see
|
| 146 |
+
[Kitware/dive#1709](https://github.com/Kitware/dive/pull/1709)), covering calibration
|
| 147 |
+
parsing, the search-range conversion above, and two-view triangulation.
|
| 148 |
+
|
| 149 |
+
### Not a transformers.js model
|
| 150 |
+
|
| 151 |
+
This is a bespoke geometry graph, not a transformer. It has no `config.json`, no tokenizer
|
| 152 |
+
or image processor, and no architecture in the transformers.js registry, so `pipeline()` /
|
| 153 |
+
`AutoModel` will not load it. Use onnxruntime (or onnxruntime-web) directly.
|
| 154 |
+
|
| 155 |
+
## Swapping left and right
|
| 156 |
+
|
| 157 |
+
The graph always matches *left → right*. To warp a point annotated on the right camera,
|
| 158 |
+
invert the rig instead of swapping the inputs: the new world frame is the old right camera,
|
| 159 |
+
so `R' = Rᵀ` and `T' = -Rᵀ·T`, with the intrinsics and distortion swapped between sides.
|
| 160 |
+
|
| 161 |
+
## Accuracy
|
| 162 |
+
|
| 163 |
+
Validated against the VIAME C++ / Python reference implementation to roughly a quarter
|
| 164 |
+
pixel. Note the exporter's own verification reports a sub-pixel `right_points` difference
|
| 165 |
+
between eager PyTorch and onnxruntime (about 0.04 px at `template_size=13`) caused by
|
| 166 |
+
ties between equally-scoring epipolar candidates; scores match to ~1e-8.
|
| 167 |
+
|
| 168 |
+
## License and provenance
|
| 169 |
+
|
| 170 |
+
CC-BY-4.0. Produced by `plugins/onnx/export_stereo_mapping.py` in
|
| 171 |
+
[VIAME](https://github.com/VIAME/VIAME), whose core infrastructure is BSD-3-Clause.
|