Mask the subject the model found, not the regions underneath it
The watershed hierarchy does not survive a photograph, so local masking stops depending on it. A layer can now be one recognised object, and the object's own coverage is the mask. `Options::watershed` defaults off. It costs ~80 ms plus a full-resolution readback to produce a ladder that collapses, and paying that on every photograph buys a control that misleads. Kept switchable rather than deleted: the passes and the hierarchy are correct in themselves and it is the merge criterion that fails, which is a change to one function. Masks now rasterise in **source** space at proxy resolution and are sampled by the composed shader after the framing map. That fixes a real bug: they were rasterised in output space, so zooming slid the photograph underneath a mask that stayed pinned to the viewport, and cropping moved every adjustment to a different part of the picture. Doing it this way also leaves the framing map in exactly one place — a second copy in the mask shader would have been a second thing to keep in step, failing only when straightened. A subject is stored as identity, not pixels: the mask is megabytes and is reproducible by running the same model over the same image, so the sidecar carries the index, the class and the score, and the session carries the pixels. The class is there to be checked — if instance 3 comes back a "car" where it was a "dog", something changed and the layer is stale rather than silently masking the wrong thing. The overlay now draws instances and is transparent everywhere else. The region version covered every pixel and so hid the photograph it was drawn over; the question it exists to answer is whether an outline follows the subject, which you can only answer by seeing both. `examples/local.rs` is the worked example: subject in colour with the rest monochrome, and the subject lifted out of its background. Run on a 5472x3648 CR2 it finds two people and two cars, and the colour-pop keeps her hat and hair while the wall and grass behind go grey.
This commit is contained in:
@@ -821,6 +821,7 @@ export component AppWindow inherits Window {
|
||||
in property <bool> segmenting: false;
|
||||
in property <int> segmentation-level: 300;
|
||||
in property <int> segmentation-regions: 0;
|
||||
in property <bool> has-regions: false;
|
||||
in property <bool> editing-mask: false;
|
||||
|
||||
callback segment-image();
|
||||
@@ -1903,6 +1904,7 @@ in property <bool> panel-visible: true;
|
||||
segmenting: root.segmenting;
|
||||
level: root.segmentation-level;
|
||||
region-count: root.segmentation-regions;
|
||||
has-regions: root.has-regions;
|
||||
overlay: root.overlay-on;
|
||||
picking: root.region-picking;
|
||||
editing-mask: root.editing-mask;
|
||||
|
||||
+29
-6
@@ -171,6 +171,13 @@ export component MaskPanel inherits Rectangle {
|
||||
/// How many regions the ladder is currently cut to.
|
||||
in property <int> level: 300;
|
||||
in property <int> region-count: 0;
|
||||
/// Whether a region hierarchy exists to click into.
|
||||
///
|
||||
/// Off in the ordinary case. The watershed's ladder collapses on a
|
||||
/// photograph, so the controls that drive it are hidden rather than shown
|
||||
/// doing nothing — a slider that changes no outcome is worse than an
|
||||
/// absent one, because it invites the user to blame themselves.
|
||||
in property <bool> has-regions: false;
|
||||
|
||||
/// Draw the false-coloured region map over the photograph.
|
||||
in property <bool> overlay: false;
|
||||
@@ -213,7 +220,12 @@ export component MaskPanel inherits Rectangle {
|
||||
HorizontalLayout {
|
||||
PanelHeading { text: "LOCAL"; }
|
||||
Rectangle { horizontal-stretch: 1; }
|
||||
if root.segmented: Value { text: root.level + " / " + root.region-count; }
|
||||
if root.segmented && root.has-regions: Value {
|
||||
text: root.level + " / " + root.region-count;
|
||||
}
|
||||
if root.segmented && !root.has-regions: Value {
|
||||
text: root.subjects.length + (root.subjects.length == 1 ? " subject" : " subjects");
|
||||
}
|
||||
}
|
||||
|
||||
if !root.enabled: Caption { text: "No image"; }
|
||||
@@ -224,7 +236,7 @@ export component MaskPanel inherits Rectangle {
|
||||
// and inference is too long to spend on every photograph opened, and
|
||||
// most edits never need one.
|
||||
if root.enabled && !root.segmented: Caption {
|
||||
text: "Find the regions in this photograph, so a mask can snap to them.";
|
||||
text: "Find the subjects in this photograph, so a mask can follow one.";
|
||||
// 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.
|
||||
@@ -232,7 +244,7 @@ export component MaskPanel inherits Rectangle {
|
||||
}
|
||||
|
||||
if root.enabled && !root.segmented: Button {
|
||||
text: root.segmenting ? "Finding regions…" : "Find regions";
|
||||
text: root.segmenting ? "Looking…" : "Find subjects";
|
||||
enabled: !root.segmenting;
|
||||
primary: true;
|
||||
clicked => { root.segment(); }
|
||||
@@ -256,11 +268,16 @@ export component MaskPanel inherits Rectangle {
|
||||
}
|
||||
}
|
||||
|
||||
if root.enabled && root.segmented && root.picking: Caption {
|
||||
if root.enabled && root.segmented && root.picking && root.has-regions: Caption {
|
||||
text: "Click the photograph to select a region. Shift-click to add or remove.";
|
||||
wrap: word-wrap;
|
||||
}
|
||||
|
||||
if root.enabled && root.segmented && root.picking && !root.has-regions: Caption {
|
||||
text: "Click a subject in the photograph to mask it.";
|
||||
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.
|
||||
@@ -268,7 +285,7 @@ export component MaskPanel inherits Rectangle {
|
||||
// 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 {
|
||||
if root.enabled && root.segmented && root.has-regions: SliderRow {
|
||||
label: "Detail";
|
||||
hint: "How finely a click divides the picture, out of "
|
||||
+ root.region-count + " regions the watershed found.";
|
||||
@@ -286,7 +303,7 @@ export component MaskPanel inherits Rectangle {
|
||||
// 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.";
|
||||
text: "Click one to mask it. The outline is the model's, so soften or grow it below.";
|
||||
wrap: word-wrap;
|
||||
}
|
||||
|
||||
@@ -311,6 +328,12 @@ export component MaskPanel inherits Rectangle {
|
||||
}
|
||||
}
|
||||
|
||||
if root.enabled && root.segmented && root.subjects.length == 0: Caption {
|
||||
text: "Nothing recognised. The model knows people, animals and vehicles — "
|
||||
+ "a landscape has no subject for it to find. Add a gradient instead.";
|
||||
wrap: word-wrap;
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
height: 1px;
|
||||
background: Theme.rule;
|
||||
|
||||
Reference in New Issue
Block a user