Let a mask cover a whole category, not just one instance
`MaskSource` could say "instance 3 of that segmentation run" but had no way
to say "the sky". Adding `Category { signature, name }` beside `Subject` is
what lets a local adjustment attach to a semantic category at all.
Stored as identity like a subject, and for the same reason: the coverage is
megabytes and is reproducible by running the same model over the same image,
so the sidecar carries what finds it again and the session carries pixels.
## A name rather than an index
An index would be smaller and would match `Subject`. It would also be a bug.
The grouping lives in `models/scene/categories.txt`, which is editable by
design — adding one category to it renumbers every category after it, and
every stored layer would silently start grading something else. A name that
no longer exists is simply not found and the layer reads as stale, which is
the failure that announces itself.
Staleness is otherwise identical to a subject's: the coverage buffer is in
the session, never the sidecar, so a signature from another run points at
pixels that were never computed.
## Two tests, and the second one caught a real shape
Round-tripping the name matters more than usual here, because the whole
argument for storing a name instead of an index is worthless if the sidecar
is what drops it.
The multi-word case is the one worth having: `category = swimming pool` is
written on one line, and a reader splitting on whitespace would have
truncated it to a category no model has — a layer that silently masks
nothing. `category` is also its own key rather than a reuse of `class`,
because a file conflating them would round-trip a subject into a category.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -616,3 +616,64 @@ fn show_a_sidecar() {
|
||||
|
||||
println!("\n{}", sidecar.to_text());
|
||||
}
|
||||
|
||||
/// A category mask is a name plus a signature, and both have to survive.
|
||||
///
|
||||
/// The name especially. `MaskSource::Category` stores one rather than an index
|
||||
/// precisely so that editing `models/scene/categories.txt` cannot repoint a
|
||||
/// stored layer — and that reasoning is worth nothing if the name is what the
|
||||
/// sidecar drops.
|
||||
#[test]
|
||||
fn category_masks_keep_their_name() {
|
||||
let mut graph = EditGraph::default_chain();
|
||||
|
||||
let mut sky = MaskLayer::new(
|
||||
"m1",
|
||||
MaskSource::Category {
|
||||
signature: 0x5EED_1234,
|
||||
name: "sky".into(),
|
||||
},
|
||||
);
|
||||
sky.name = "sky".into();
|
||||
sky.feather = 0.03;
|
||||
sky.set_param("exposure", ParamId("exposure"), 0.5);
|
||||
graph.masks_mut().push(sky);
|
||||
|
||||
// A multi-word name too: the writer emits `category = swimming pool` on one
|
||||
// line, and a reader that split on whitespace would silently truncate it to
|
||||
// a category no model has.
|
||||
let mut pool = MaskLayer::new(
|
||||
"m2",
|
||||
MaskSource::Category {
|
||||
signature: 0x5EED_1234,
|
||||
name: "swimming pool".into(),
|
||||
},
|
||||
);
|
||||
pool.set_param("saturation", ParamId("saturation"), -30.0);
|
||||
graph.masks_mut().push(pool);
|
||||
|
||||
let restored = round_trip(&graph);
|
||||
let layers = restored.masks().layers();
|
||||
assert_eq!(layers.len(), 2);
|
||||
assert_eq!(layers[0].source, graph.masks().layers()[0].source);
|
||||
assert_eq!(layers[1].source, graph.masks().layers()[1].source);
|
||||
assert!((layers[0].feather - 0.03).abs() < 1e-6);
|
||||
}
|
||||
|
||||
/// A category is stale when its run is, exactly as a subject is.
|
||||
///
|
||||
/// The coverage buffer lives in the session, not the sidecar, so a layer whose
|
||||
/// signature no longer matches is pointing at pixels that were never computed.
|
||||
/// Rendering it anyway would mask nothing and read as a broken adjustment.
|
||||
#[test]
|
||||
fn a_category_from_another_run_is_stale() {
|
||||
let layer = MaskLayer::new(
|
||||
"m1",
|
||||
MaskSource::Category {
|
||||
signature: 1,
|
||||
name: "sky".into(),
|
||||
},
|
||||
);
|
||||
assert!(layer.is_stale(2), "a different run must invalidate it");
|
||||
assert!(!layer.is_stale(1), "the run it was built against must not");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user