Record what the spec got wrong about the model that exists

docs/segmentation.md §4 priced arm B as costing a C dependency under the
NDK and treated that as most of the difference between the arms. It is not
a cost that has to be paid: `ort`'s `alternative-backend` disables its
linking entirely and `ort-tract` supplies the API from tract, which is pure
Rust. D13's "largest exception the policy would tolerate" turns out not to
be needed, and the answer generalises to the face pipeline — so D13's
runtime half is now answered and only its licensing half is open.

Three findings contradict §4 outright and are recorded as F4-F6 rather than
quietly designed around. There is no ADE20K-trained YOLO, so the shipped
vocabulary selects subjects and not stuff — "select the sky" comes from the
watershed or from nowhere. It is instance segmentation, so it partitions
nothing and two people come back as two instances. And tract cannot parse a
dynamic-shape export, which fixes the input at 640 square and makes tiling
the only route to more semantic resolution.

Arm C ships, but §8's criteria are not what decided it, and saying so
matters more than claiming the process worked. §8 asked for a two-
interaction margin over arm A on a traced corpus. That comparison was never
run: F4 and F5 changed what the arms are, and a model that recognises
subjects but has no word for sky cannot be a selection tool alone, while a
watershed cannot tell a person from the wall behind them. They stopped
being candidates and became complements.

What is *not* done is written down as plainly: the 24-image corpus is
untraced, so M1-M4 have no numbers and "this feels right" has not become
one. M5 is answered on one device only, and region ids now reach the
sidecar — so a cross-vendor divergence would mean a mask written on the
desktop meaning something else on Android. F3 stands.
This commit is contained in:
2026-08-22 08:39:17 +02:00
parent 9b4f0815e5
commit 12d320cf33
6 changed files with 395 additions and 115 deletions
+74 -64
View File
@@ -96,6 +96,10 @@ component MaskEntry inherits Rectangle {
}
Caption {
// Elided rather than wrapped: this sits beside two fixed
// targets in a row, and wrapping would give the rows of a
// stack different heights for no gain.
overflow: elide;
// Three states worth distinguishing, and each has a
// different remedy: stale needs the segmentation re-run,
// unadjusted needs a slider moved, and the ordinary case
@@ -194,6 +198,13 @@ export component MaskPanel inherits Rectangle {
background: Theme.surface;
// **Flat, deliberately.** An earlier version grouped each state into its
// own `if ...: VerticalLayout { ... }`. The nested layout under-reported
// its height, so the divider and the mask row below it were placed over
// the "Find regions" button — the sort of fault that is invisible in the
// source and obvious the moment anyone looks at the panel. Conditioning
// each child on its own keeps every element a direct child of one layout,
// which is the layout that measures them.
VerticalLayout {
padding: Theme.gap;
spacing: Theme.gap-sm;
@@ -207,73 +218,70 @@ export component MaskPanel inherits Rectangle {
if !root.enabled: Caption { text: "No image"; }
// --- the region map ------------------------------------------------
// --- the region map ---------------------------------------------
//
// Segmentation is a thing the user starts. Half a second of watershed
// and inference is too long to spend on every photograph opened, and
// most edits never need one.
if root.enabled && !root.segmented: VerticalLayout {
if root.enabled && !root.segmented: Caption {
text: "Find the regions in this photograph, so a mask can snap to them.";
// Wrapped, or this sentence's single-line width becomes the
// panel's minimum and levers the fixed 280px develop column open,
// taking every other panel's controls off the right edge with it.
wrap: word-wrap;
}
if root.enabled && !root.segmented: Button {
text: root.segmenting ? "Finding regions…" : "Find regions";
enabled: !root.segmenting;
primary: true;
clicked => { root.segment(); }
}
// The overlay the whole map is judged by, and the picking mode that
// makes a click mean "select" instead of "pan".
if root.enabled && root.segmented: HorizontalLayout {
spacing: Theme.gap-sm;
Caption {
text: "Find the regions in this photograph, so a mask can snap to them.";
Button {
text: "Overlay";
active: root.overlay;
clicked => { root.overlay-toggled(!root.overlay); }
}
Button {
text: root.segmenting ? "Finding regions…" : "Find regions";
enabled: !root.segmenting;
primary: true;
clicked => { root.segment(); }
text: "Select";
active: root.picking;
clicked => { root.picking-toggled(!root.picking); }
}
}
if root.enabled && root.segmented && root.picking: Caption {
text: "Click the photograph to select a region. Shift-click to add or remove.";
wrap: word-wrap;
}
// Granularity, labelled by what it does rather than by its number:
// "detail" is what a photographer is choosing between, where "300
// regions" is an implementation detail they would have to learn.
if root.enabled && root.segmented: SliderRow {
label: "Detail";
value: root.level;
default-value: 300;
minimum: 8;
maximum: 2000;
changed(v) => { root.level-changed(v); }
reset => { root.level-changed(300); }
}
// --- what the model found ----------------------------------------
if root.enabled && root.segmented && root.subjects.length > 0: Caption {
text: "Recognised";
}
if root.enabled && root.segmented: VerticalLayout {
spacing: Theme.gap-sm;
HorizontalLayout {
spacing: Theme.gap-sm;
// The overlay the whole map is judged by. Without it the
// granularity control is a number with no visible meaning.
Button {
text: "Overlay";
active: root.overlay;
clicked => { root.overlay-toggled(!root.overlay); }
}
Button {
text: "Select";
active: root.picking;
clicked => { root.picking-toggled(!root.picking); }
}
}
if root.picking: Caption {
text: "Click the photograph to select a region. Shift-click to add or remove.";
}
// Granularity. Labelled by what it does rather than by its number:
// "detail" is what a photographer is choosing between, where "300
// regions" is an implementation detail they would have to learn.
SliderRow {
label: "Detail";
value: root.level;
default-value: 300;
minimum: 8;
maximum: 2000;
changed(v) => { root.level-changed(v); }
reset => { root.level-changed(300); }
}
}
// --- what the model found -------------------------------------------
if root.enabled && root.segmented && root.subjects.length > 0: VerticalLayout {
spacing: 2px;
padding-top: Theme.gap-sm;
Caption { text: "Recognised"; }
for subject[i] in root.subjects: subject-row := TouchArea {
spacing: 0px;
for subject in root.subjects: subject-row := TouchArea {
height: Theme.touch-target;
mouse-cursor: pointer;
clicked => { root.add-subject(subject.index); }
@@ -285,6 +293,7 @@ export component MaskPanel inherits Rectangle {
text: subject.label;
emphasised: subject-row.has-hover;
horizontal-stretch: 1;
overflow: elide;
}
Value { text: round(subject.score * 100) + "%"; }
}
@@ -296,15 +305,15 @@ export component MaskPanel inherits Rectangle {
background: Theme.rule;
}
// --- the stack -------------------------------------------------------
// --- the stack -----------------------------------------------------
Caption { text: "Masks"; }
// Gradients need no segmentation, so they are offered whether or not
// one has been computed — a graduated filter over a sky is a local
// adjustment that never needed to know what a sky is.
HorizontalLayout {
spacing: Theme.gap-sm;
Caption { text: "Masks"; horizontal-stretch: 1; }
// Gradients need no segmentation, so they are offered whether or
// not one has been computed — a graduated filter over a sky is a
// local adjustment that never needed to know what a sky is.
Button {
text: "Linear";
enabled: root.enabled;
@@ -319,6 +328,7 @@ export component MaskPanel inherits Rectangle {
if root.enabled && root.masks.length == 0: Caption {
text: "No local adjustments. Select a region, or add a gradient.";
wrap: word-wrap;
}
for mask in root.masks: MaskEntry {
@@ -331,14 +341,14 @@ export component MaskPanel inherits Rectangle {
opacity-changed(v) => { root.mask-opacity-changed(mask.id, v); }
}
// The one thing the panel has to say about the adjust panel below
// it, because otherwise selecting a layer silently changes what those
// sliders mean — the single most confusing thing a scoped panel can
// do.
// The one thing this panel has to say about the adjust panel below it,
// because otherwise selecting a layer silently changes what those
// sliders mean — the most confusing thing a scoped panel can do.
if root.enabled && root.masks.length > 0: Caption {
text: root.editing-mask
? "The controls below adjust the selected mask."
: "The controls below adjust the whole photograph.";
wrap: word-wrap;
}
}
}