Bind a category layer to its own distance field
A category mask showed nothing and its adjustment covered the whole
photograph. Both from one line: the loop in `MaskPass::rasterise` picks a
distance field by matching `layer.source`, that match named only `Subject`,
and a `Category` layer fell through to `_ => (&self.empty_subject, 0)` — a
1x1 placeholder. No field, so nothing to draw and nothing to confine the
adjustment.
The comment three lines above the arm I missed describes the failure I then
shipped:
an absent mask that defaults to "everything" would apply the
adjustment to the whole photograph
There are *two* matches on `layer.source` in that loop — one choosing the
field, one building the params. Adding the category to the second and not
the first compiles, runs, and is wrong in exactly the way the first one
warns about.
## Also: a missing mask must still be the right size
Both model-backed arms of `ensure_subject_fields` used `unwrap_or_default`,
which yields an empty `Vec` when the coverage is gone. `SubjectMasks::upload`
rejects a wrong-sized field and fails the whole batch, so `self.subjects`
becomes `None` and *every* layer in the stack loses its mask — one stale
reference silently unmasking the others.
Pre-existing, and it mattered less when the only model-backed source was a
subject: an instance index goes missing rarely. A category name goes missing
whenever the descriptor is edited, which is a thing the descriptor exists to
allow. A full-size empty field costs one layer instead of all of them.
Neither of these is reachable from a test on this machine — both live past a
GPU adapter and a real segmentation — so they surfaced the only way they
could, by someone opening the app and looking.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
+14
-6
@@ -579,13 +579,21 @@ impl MaskPass {
|
||||
// fields are built per layer, in this same order, because two
|
||||
// layers over one subject can carry different morphology.
|
||||
let subject = match &layer.source {
|
||||
MaskSource::Subject { .. } => match subjects.filter(|s| slot < s.len()) {
|
||||
Some(s) => (s, slot),
|
||||
None => {
|
||||
log::warn!("mask layer {} has no distance field; skipping", layer.id);
|
||||
continue;
|
||||
// Category alongside Subject: both are model coverage turned
|
||||
// into a distance field, both are built per layer in this same
|
||||
// order, and leaving a category out of here is precisely the
|
||||
// failure the comment above warns about — it binds the 1x1
|
||||
// placeholder, so the mask covers everything and the
|
||||
// adjustment silently goes global.
|
||||
MaskSource::Subject { .. } | MaskSource::Category { .. } => {
|
||||
match subjects.filter(|s| slot < s.len()) {
|
||||
Some(s) => (s, slot),
|
||||
None => {
|
||||
log::warn!("mask layer {} has no distance field; skipping", layer.id);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
_ => (&self.empty_subject, 0),
|
||||
};
|
||||
|
||||
|
||||
+22
-22
File diff suppressed because one or more lines are too long
@@ -1860,7 +1860,12 @@ impl DevelopSession {
|
||||
)
|
||||
.distance
|
||||
})
|
||||
.unwrap_or_default(),
|
||||
// Full size, never `unwrap_or_default`: an empty vec is a
|
||||
// wrong-sized field, `SubjectMasks::upload` rejects the
|
||||
// whole batch on one, and every other layer in the stack
|
||||
// then loses its mask too. One stale name should cost one
|
||||
// layer, not all of them.
|
||||
.unwrap_or_else(|| vec![-1.0; pw * ph]),
|
||||
MaskSource::Subject { index, .. } => seg
|
||||
.instance_mask(*index as usize)
|
||||
.map(|coverage| {
|
||||
@@ -1876,7 +1881,7 @@ impl DevelopSession {
|
||||
)
|
||||
.distance
|
||||
})
|
||||
.unwrap_or_default(),
|
||||
.unwrap_or_else(|| vec![-1.0; pw * ph]),
|
||||
// A placeholder of the right size, so the slot indices line up
|
||||
// with `active()` whatever mix of sources the stack holds.
|
||||
_ => vec![-1.0; pw * ph],
|
||||
|
||||
Reference in New Issue
Block a user