mirror of
https://github.com/visioncortex/vtracer.git
synced 2026-08-30 17:05:57 -07:00
Cutout: merge_diff 0 still merges identical-color faces
Making watershed's cutout native (merge_diff = 0) silently disabled the same-color merge too: merge_similar early-returned on a non-positive threshold, so two adjacent faces with the very same fill — e.g. after a palette snap — kept a useless boundary between them. A threshold of 0 now means 'merge only identical-color neighbours' (a boundary between two same-colored faces is never useful); negative disables merging entirely. The regression test snaps two boundary-sharing regions to the same palette entry with a third region between them in stack order, so the layer-level MergeAdjacent (consecutive runs only) cannot mask the mosaic merge: 3 faces with the bug, 2 with the fix. The zero-threshold unit test now asserts the new semantics, and negative-threshold identity.
This commit is contained in:
@@ -20,7 +20,8 @@ pub enum Compositing {
|
||||
fitter: Box<dyn SegmentFitter>,
|
||||
/// Merge flattened neighbours whose colors are within this diff —
|
||||
/// rejoins regions the stacked gradient layering had split. Usually
|
||||
/// the clustering gradient step; `0` disables merging.
|
||||
/// the clustering gradient step; `0` still merges identical-color
|
||||
/// neighbours, negative disables merging entirely.
|
||||
merge_diff: i32,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -297,7 +297,8 @@ impl Config {
|
||||
// clustering itself considers colors within one gradient step
|
||||
// to be the same region (`deepen_diff`). The watershed
|
||||
// hierarchy already decided every merge, so its partition
|
||||
// passes to the mosaic untouched.
|
||||
// keeps its shape — only identical-color neighbours (e.g.
|
||||
// after a palette snap) still collapse into one face.
|
||||
merge_diff: match self.clustering {
|
||||
Clustering::Watershed => 0,
|
||||
_ => self.layer_difference,
|
||||
|
||||
@@ -20,7 +20,8 @@ use super::{LabelMap, Segmentation};
|
||||
/// `merge_diff` is the color-difference threshold for
|
||||
/// [`LabelMap::merge_similar`]; pass the clustering `deepen_diff`
|
||||
/// (gradient step) so the flattened mosaic rejoins what only the stacked
|
||||
/// gradient layering had split. `0` disables merging.
|
||||
/// gradient layering had split. `0` still merges identical-color
|
||||
/// neighbours; negative disables merging entirely.
|
||||
pub fn compose_mosaic(seg: &Segmentation, fitter: &dyn SegmentFitter, merge_diff: i32) -> VectorDoc {
|
||||
let mut map = LabelMap::from_segmentation(seg);
|
||||
map.merge_similar(merge_diff);
|
||||
|
||||
@@ -100,9 +100,13 @@ impl LabelMap {
|
||||
/// over the adjacency graph, most-similar pairs first, with each merged
|
||||
/// region's color re-derived as the area-weighted mean so chains only
|
||||
/// combine while they genuinely stay within `max_diff`.
|
||||
///
|
||||
/// `max_diff == 0` still merges *identical*-color neighbours — a boundary
|
||||
/// between two same-colored faces is never useful. Pass a negative value
|
||||
/// to disable merging entirely.
|
||||
pub fn merge_similar(&mut self, max_diff: i32) {
|
||||
let n = self.paints.len();
|
||||
if max_diff <= 0 || n < 2 {
|
||||
if max_diff < 0 || n < 2 {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -545,10 +549,20 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn merge_similar_zero_threshold_is_identity() {
|
||||
let labels = vec![0, 1, 0, 1];
|
||||
let mut map = gray_grid(2, 2, labels.clone(), &[100, 101]);
|
||||
fn merge_similar_zero_threshold_merges_only_identical_colors() {
|
||||
// Regions 0 and 1 share a color; region 2 differs by one level. At
|
||||
// threshold 0 the identical pair merges, the near-identical one stays.
|
||||
#[rustfmt::skip]
|
||||
let mut map = gray_grid(3, 1, vec![0, 1, 2], &[100, 100, 101]);
|
||||
map.merge_similar(0);
|
||||
assert_eq!(map.paints.len(), 2, "identical neighbours merge at 0");
|
||||
assert_eq!(map.label(0, 0), map.label(1, 0));
|
||||
assert_ne!(map.label(1, 0), map.label(2, 0));
|
||||
|
||||
// A negative threshold disables merging entirely.
|
||||
let labels = vec![0, 1, 0, 1];
|
||||
let mut map = gray_grid(2, 2, labels.clone(), &[100, 100]);
|
||||
map.merge_similar(-1);
|
||||
assert_eq!(map.labels, labels);
|
||||
assert_eq!(map.paints.len(), 2);
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
//! absorption, and the hierarchy stack / cached re-cut behavior.
|
||||
|
||||
use vtracer::frontend::{Frontend, WatershedFrontend, WatershedHierarchy};
|
||||
use vtracer::{ColorImage, Clustering, Config, Hierarchical, Segmentation, Session};
|
||||
use vtracer::{Color, ColorImage, Clustering, Config, Hierarchical, Segmentation, Session};
|
||||
|
||||
fn image(w: usize, h: usize, f: impl Fn(usize, usize) -> (u8, u8, u8)) -> ColorImage {
|
||||
let mut pixels = Vec::with_capacity(w * h * 4);
|
||||
@@ -270,3 +270,37 @@ fn cutout_keeps_watershed_partition() {
|
||||
"watershed partition must pass to the mosaic unmerged"
|
||||
);
|
||||
}
|
||||
|
||||
/// …but identical-color neighbours still collapse into one face: regions that
|
||||
/// snap to the same palette entry and share a boundary must not keep a useless
|
||||
/// edge between them. (The dark region sits between them in stack order, so
|
||||
/// the layer-level `MergeAdjacent` cannot be the one doing the merging — only
|
||||
/// the mosaic's same-color merge can.)
|
||||
#[test]
|
||||
fn cutout_merges_identical_palette_faces() {
|
||||
let img = image(32, 32, |x, y| {
|
||||
if y < 16 {
|
||||
if x < 16 {
|
||||
(200, 200, 200) // A: top-left
|
||||
} else {
|
||||
(20, 20, 20) // C: top-right
|
||||
}
|
||||
} else {
|
||||
(180, 180, 180) // B: bottom, touches A
|
||||
}
|
||||
});
|
||||
let cfg = Config {
|
||||
clustering: Clustering::Watershed,
|
||||
hierarchical: Hierarchical::Cutout,
|
||||
watershed_detail: 255,
|
||||
filter_speckle: 0,
|
||||
palette: vec![Color::new(255, 255, 255), Color::new(0, 0, 0)],
|
||||
..Config::default()
|
||||
};
|
||||
let doc = cfg.build().unwrap().run(&img).unwrap();
|
||||
assert_eq!(
|
||||
doc.shapes.len(),
|
||||
2,
|
||||
"A and B snap to the same palette color and share a boundary — one face"
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user