Merge: mask a whole category, not just one instance
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> # Conflicts: # apps/darkroom-desktop/Cargo.toml # docs/traceability.md
This commit is contained in:
@@ -532,6 +532,38 @@ pub enum MaskSource {
|
||||
score: f32,
|
||||
},
|
||||
|
||||
/// Every pixel of one photographic category, from the scene model.
|
||||
///
|
||||
/// The counterpart to [`Self::Subject`], and the difference is the whole
|
||||
/// reason both exist. A subject is *one* instance — this dog, not that one
|
||||
/// — found by a COCO-trained instance model. A category is *all* the sky,
|
||||
/// or all the foliage, from an ADE20K-trained semantic model that has no
|
||||
/// notion of instances at all (docs/segmentation.md §16).
|
||||
///
|
||||
/// So this is what a global grade attaches to: lift the sky, desaturate
|
||||
/// the vegetation, warm the architecture. Asking it for "that person
|
||||
/// rather than the other two" is a category error — the model merged them
|
||||
/// before the mask ever existed, and [`Self::Subject`] is the source for
|
||||
/// that question.
|
||||
///
|
||||
/// Stored as identity like a subject, and for the same reason: the
|
||||
/// coverage is megabytes and is reproducible from the same model over the
|
||||
/// same image.
|
||||
Category {
|
||||
/// Which segmentation run produced it, so a layer can tell whether
|
||||
/// the name below still refers to something that was computed.
|
||||
signature: u64,
|
||||
/// The category name from `models/scene/categories.txt` — "sky",
|
||||
/// "vegetation".
|
||||
///
|
||||
/// A name rather than an index because the descriptor is editable: a
|
||||
/// category added to it would silently renumber every layer stored
|
||||
/// against an index, and the failure would be a mask quietly grading
|
||||
/// the wrong thing. A name that no longer exists is simply not found,
|
||||
/// and the layer reads as stale.
|
||||
name: String,
|
||||
},
|
||||
|
||||
/// A linear gradient — the graduated-filter mask.
|
||||
///
|
||||
/// Geometry is in **normalised source coordinates**, so it survives a crop,
|
||||
@@ -596,6 +628,7 @@ impl MaskSource {
|
||||
match self {
|
||||
Self::Regions { .. } => "regions",
|
||||
Self::Subject { .. } => "subject",
|
||||
Self::Category { .. } => "category",
|
||||
Self::Linear { .. } => "linear",
|
||||
Self::Radial { .. } => "radial",
|
||||
Self::Brush { .. } => "brush",
|
||||
@@ -825,9 +858,9 @@ impl MaskLayer {
|
||||
/// should offer to recompute rather than render it.
|
||||
pub fn is_stale(&self, current: u64) -> bool {
|
||||
match self.source {
|
||||
MaskSource::Regions { signature, .. } | MaskSource::Subject { signature, .. } => {
|
||||
signature != current
|
||||
}
|
||||
MaskSource::Regions { signature, .. }
|
||||
| MaskSource::Subject { signature, .. }
|
||||
| MaskSource::Category { signature, .. } => signature != current,
|
||||
// A gradient is geometry in normalised coordinates. It means the
|
||||
// same thing whatever was or was not detected, so nothing about a
|
||||
// new run can invalidate it. Painted strokes are the same: they are
|
||||
|
||||
@@ -971,6 +971,10 @@ fn write_mask(out: &mut String, version: &str, layer: &MaskLayer) {
|
||||
let _ = writeln!(out, "class = {class}");
|
||||
let _ = writeln!(out, "score = {}", format_value(*score));
|
||||
}
|
||||
MaskSource::Category { signature, name } => {
|
||||
let _ = writeln!(out, "signature = {signature}");
|
||||
let _ = writeln!(out, "category = {name}");
|
||||
}
|
||||
MaskSource::Linear {
|
||||
centre,
|
||||
angle,
|
||||
@@ -1124,6 +1128,7 @@ struct PartialMask {
|
||||
ids: Vec<u32>,
|
||||
index: u32,
|
||||
class: String,
|
||||
category: String,
|
||||
score: f32,
|
||||
centre: (f32, f32),
|
||||
radii: (f32, f32),
|
||||
@@ -1154,6 +1159,7 @@ impl PartialMask {
|
||||
level: 0,
|
||||
ids: Vec::new(),
|
||||
index: 0,
|
||||
category: String::new(),
|
||||
class: String::new(),
|
||||
score: 0.0,
|
||||
centre: (0.5, 0.5),
|
||||
@@ -1191,6 +1197,11 @@ impl PartialMask {
|
||||
self.ids.sort_unstable();
|
||||
self.ids.dedup();
|
||||
}
|
||||
// A category's own key rather than reusing `class`: both name a
|
||||
// thing the mask covers, but one is a COCO instance's label and
|
||||
// the other an entry in the scene descriptor, and a file that
|
||||
// conflated them would round-trip a subject into a category.
|
||||
"category" => self.category = value.to_string(),
|
||||
"index" => self.index = value.parse().unwrap_or(0),
|
||||
"class" => self.class = value.to_string(),
|
||||
"score" => self.score = value.parse().unwrap_or(0.0),
|
||||
@@ -1257,6 +1268,10 @@ impl PartialMask {
|
||||
class: self.class,
|
||||
score: self.score,
|
||||
},
|
||||
"category" => MaskSource::Category {
|
||||
signature: self.signature,
|
||||
name: self.category,
|
||||
},
|
||||
"linear" => MaskSource::Linear {
|
||||
centre: self.centre,
|
||||
angle: self.angle,
|
||||
|
||||
Reference in New Issue
Block a user