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.
63 lines
2.2 KiB
Bash
Executable File
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)."
|