Give a mask an edge treatment, and find out the watershed has none worth having

Two things, and the second is why the first matters more than expected.

Mask layers gain a feather, a falloff curve and a morphology, all defined
against a signed distance from the boundary rather than as separate
features — one exact distance field answers "how soft" and "how far" at
once, so dilation is a threshold at -r, erosion one at +r, and closing and
opening are one of each in sequence. The compound pair costs a second
distance field, which is why they are named rather than presented as a
radius that happens to be signed. Types, defaults and sidecar round-trip
only; the field itself is next.

`edge-feather` and `edge-falloff`, not `feather` and `falloff`, because a
radial mask already writes `feather` for the fraction of its radius it
ramps over. Same word, different quantity, different units — sharing the
key would have made an existing file ambiguous.

The diagnostic that provoked this is committed as an ignored test, because
"does the ladder land on things a person means" is the question S15 exists
to answer and it should not depend on whoever still has the script. On
bus.jpg it answers badly: 35,075 regions at blur 2 over an 810x1080 frame,
and cutting that to 400 gives *one* region covering nearly the whole
picture plus 399 noise specks. Not over-segmentation — collapse. Almost
every saddle is near zero, so the merge order joins everything meaningful
before it joins anything spurious, and a global cut spends its entire
budget on grain.

So the granularity ladder does not currently work on a photograph, and the
region masks built on it inherit that. Recorded rather than worked around:
the next commits move local masking onto the model's instances, where the
edge treatment above is what makes a quarter-resolution mask usable.
This commit is contained in:
2026-08-22 08:39:17 +02:00
parent 12d320cf33
commit b1433ad4a9
4 changed files with 340 additions and 4 deletions
+14 -3
View File
@@ -213,7 +213,7 @@ export component MaskPanel inherits Rectangle {
HorizontalLayout {
PanelHeading { text: "LOCAL"; }
Rectangle { horizontal-stretch: 1; }
if root.segmented: Value { text: root.region-count + " regions"; }
if root.segmented: Value { text: root.level + " / " + root.region-count; }
}
if !root.enabled: Caption { text: "No image"; }
@@ -264,19 +264,30 @@ export component MaskPanel inherits Rectangle {
// 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.
// **The ceiling is the region count, not a constant.** It was 2000,
// and a photograph that segments into more than that had the finest
// part of its own ladder unreachable — the slider simply stopped
// before the regions did.
if root.enabled && root.segmented: SliderRow {
label: "Detail";
hint: "How finely a click divides the picture, out of "
+ root.region-count + " regions the watershed found.";
value: root.level;
default-value: 300;
minimum: 8;
maximum: 2000;
maximum: max(root.region-count, 8);
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";
// Says where these came from and how they differ from a region.
// A list of four beside a count of three thousand invites exactly
// one question, and the panel should answer it rather than
// provoke it.
text: "Subjects the model recognised. Click one to select the whole thing.";
wrap: word-wrap;
}
if root.enabled && root.segmented: VerticalLayout {