Files
DarkRoom/tools/export-seg-model.sh
T
dtourolle 0da8271836 Let the model say what a thing is and the watershed say where it ends
Local masking needs to know where an image's regions are. The watershed
spike (S15 arm A) found the boundaries but had no idea what any of them
enclosed; its coarse levels were geometric accidents. This adds the other
half and the thing that joins them.

`core/dr-segment` is where region reasoning now lives — the hierarchy moves
out of `dr-gpu`, which keeps only the pixel passes that are genuinely
shaders. The new crate is device-free and, without its default features,
model-free too: 20 of its tests need neither an adapter nor 11 MB of
weights.

Arm B runs YOLO26n-seg through `ort`. D13 framed inference as a choice
between `ort`'s C++ runtime and the pure-Rust dependency policy; that was a
false choice. `ort`'s `alternative-backend` feature unlinks the C entirely
and `ort-tract` supplies the API from tract, which is pure Rust. Measured
before committing to it: zero unsupported operators, 420 ms for 640x640,
and correct masks on bus.jpg. No NDK problem to solve, so D13's largest
tolerated exception is not needed.

Arm C is `prior.rs`, and it ships because the two arms fail in opposite
directions. Instance membership re-weights the merge saddles, so region
pairs the model believes share an object merge early and pairs straddling
its edge merge late. No boundary moves — only the order in which they
dissolve — which is how the result stays pixel-accurate at every level
while its coarse levels become named things.

Two things the spec assumed that turned out to be false, both recorded in
models/LICENCE.md: there is no usable ADE20K-trained YOLO, so the shipped
vocabulary is COCO's 80 subjects and *stuff* like sky and foliage must come
from arm A; and tract cannot parse a dynamic-shape export, so the graph's
input is fixed and tiling is the only route to more semantic resolution.

Weights are AGPL-3.0, which GPLv3 §13 permits and which makes the combined
work effectively AGPL. Deliberate, not accidental. They live in Git LFS,
and a build script fails with an instruction rather than embedding a
pointer file when the clone lacks them.
2026-08-22 08:39:16 +02:00

63 lines
2.2 KiB
Bash
Executable File

#!/usr/bin/env bash
# Re-export the segmentation model that ships in core/dr-segment/models/.
#
# The .onnx is committed (D14), so this is not part of any build — it exists so
# the committed artefact is reproducible rather than a binary someone once
# produced and nobody can regenerate. Run it when bumping the model.
#
# ./tools/export-seg-model.sh
#
# Requires `uv`. Everything else is fetched into a throwaway venv.
#
# ## Why these export flags
#
# `dynamic=False` is not a default we failed to change: **tract cannot parse
# the dynamic-shape graph at all**, failing shape inference on the neck's
# Concat. A fixed input shape is a hard requirement of the pure-Rust backend
# (see the workspace manifest for why that backend was chosen), and it is what
# makes the tiling option in `semantic.rs` the only route to more resolution.
#
# `imgsz=640` square rather than a rectangle matched to 3:2: one graph has to
# serve portrait, landscape, square crops and panoramas. A landscape-shaped
# graph trades letterbox waste on 3:2 for worse waste on everything else.
set -euo pipefail
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO="$(cd "${HERE}/.." && pwd)"
OUT="${REPO}/core/dr-segment/models"
MODEL="${1:-yolo26n-seg}"
WORK="$(mktemp -d)"
trap 'rm -rf "${WORK}"' EXIT
echo "==> exporting ${MODEL} in ${WORK}"
cd "${WORK}"
uv venv --python 3.12 venv
VIRTUAL_ENV="${WORK}/venv" uv pip install ultralytics onnx onnxslim
VIRTUAL_ENV="${WORK}/venv" "${WORK}/venv/bin/python" - "${MODEL}" <<'PY'
import sys, json
from ultralytics import YOLO
name = sys.argv[1]
m = YOLO(f"{name}.pt")
path = m.export(format="onnx", opset=17, simplify=True, imgsz=640, dynamic=False)
print("ONNX:", path)
# The class names travel with the model rather than being retyped into Rust —
# a hand-copied vocabulary is a silent mismatch waiting to happen when the
# model is bumped.
with open("classes.json", "w") as f:
json.dump([m.names[i] for i in range(len(m.names))], f, indent=1)
print("classes:", len(m.names))
PY
mkdir -p "${OUT}"
cp "${WORK}/${MODEL}.onnx" "${OUT}/${MODEL}.onnx"
cp "${WORK}/classes.json" "${OUT}/${MODEL}.classes.json"
echo "==> wrote:"
ls -la "${OUT}"
echo
echo "Remember: these weights are AGPL-3.0 (see ${OUT}/LICENCE.md)."