mirror of
https://github.com/visioncortex/vtracer.git
synced 2026-09-25 21:41:21 -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>,
|
fitter: Box<dyn SegmentFitter>,
|
||||||
/// Merge flattened neighbours whose colors are within this diff —
|
/// Merge flattened neighbours whose colors are within this diff —
|
||||||
/// rejoins regions the stacked gradient layering had split. Usually
|
/// 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,
|
merge_diff: i32,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -297,7 +297,8 @@ impl Config {
|
|||||||
// clustering itself considers colors within one gradient step
|
// clustering itself considers colors within one gradient step
|
||||||
// to be the same region (`deepen_diff`). The watershed
|
// to be the same region (`deepen_diff`). The watershed
|
||||||
// hierarchy already decided every merge, so its partition
|
// 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 {
|
merge_diff: match self.clustering {
|
||||||
Clustering::Watershed => 0,
|
Clustering::Watershed => 0,
|
||||||
_ => self.layer_difference,
|
_ => self.layer_difference,
|
||||||
|
|||||||
@@ -20,7 +20,8 @@ use super::{LabelMap, Segmentation};
|
|||||||
/// `merge_diff` is the color-difference threshold for
|
/// `merge_diff` is the color-difference threshold for
|
||||||
/// [`LabelMap::merge_similar`]; pass the clustering `deepen_diff`
|
/// [`LabelMap::merge_similar`]; pass the clustering `deepen_diff`
|
||||||
/// (gradient step) so the flattened mosaic rejoins what only the stacked
|
/// (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 {
|
pub fn compose_mosaic(seg: &Segmentation, fitter: &dyn SegmentFitter, merge_diff: i32) -> VectorDoc {
|
||||||
let mut map = LabelMap::from_segmentation(seg);
|
let mut map = LabelMap::from_segmentation(seg);
|
||||||
map.merge_similar(merge_diff);
|
map.merge_similar(merge_diff);
|
||||||
|
|||||||
@@ -100,9 +100,13 @@ impl LabelMap {
|
|||||||
/// over the adjacency graph, most-similar pairs first, with each merged
|
/// over the adjacency graph, most-similar pairs first, with each merged
|
||||||
/// region's color re-derived as the area-weighted mean so chains only
|
/// region's color re-derived as the area-weighted mean so chains only
|
||||||
/// combine while they genuinely stay within `max_diff`.
|
/// 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) {
|
pub fn merge_similar(&mut self, max_diff: i32) {
|
||||||
let n = self.paints.len();
|
let n = self.paints.len();
|
||||||
if max_diff <= 0 || n < 2 {
|
if max_diff < 0 || n < 2 {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -545,10 +549,20 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn merge_similar_zero_threshold_is_identity() {
|
fn merge_similar_zero_threshold_merges_only_identical_colors() {
|
||||||
let labels = vec![0, 1, 0, 1];
|
// Regions 0 and 1 share a color; region 2 differs by one level. At
|
||||||
let mut map = gray_grid(2, 2, labels.clone(), &[100, 101]);
|
// 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);
|
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.labels, labels);
|
||||||
assert_eq!(map.paints.len(), 2);
|
assert_eq!(map.paints.len(), 2);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
//! absorption, and the hierarchy stack / cached re-cut behavior.
|
//! absorption, and the hierarchy stack / cached re-cut behavior.
|
||||||
|
|
||||||
use vtracer::frontend::{Frontend, WatershedFrontend, WatershedHierarchy};
|
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 {
|
fn image(w: usize, h: usize, f: impl Fn(usize, usize) -> (u8, u8, u8)) -> ColorImage {
|
||||||
let mut pixels = Vec::with_capacity(w * h * 4);
|
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"
|
"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