mirror of
https://github.com/visioncortex/vtracer.git
synced 2026-09-28 06:51:21 -07:00
Add watershed clustering: hierarchical watershed frontend
An alternative region-forming frontend selected by --clustering watershed (the color_mode field is replaced by clustering: color-cluster | bw | watershed across CLI, Rust, Python, and Node — it selects the algorithm, not a color space). The algorithm is the watershed hierarchy by volume on the 4-adjacency pixel graph, implemented from the papers: Cousty, Bertrand, Najman, Couprie, "Watershed Cuts: Minimum Spanning Forests and the Drop of Water Principle", IEEE TPAMI 31(8), 2009. Najman, Cousty, Perret, "Playing with Kruskal", ISMM 2013. Edge weights are the max per-channel color difference between adjacent pixels (no gradient image); a counting-sorted Kruskal pass builds the binary partition tree as a flat parents array; a leaves-to-root pass computes subtree area and volume; each merge's persistence (the volume of the smaller side, plateau-corrected) becomes its MST edge's saliency; and cutting the hierarchy is single-linkage over MST edges below the cut level. Watershed cuts label every pixel — no watershed-line pixel class — so the output is a strict, gapless partition that drops straight into both stacked and cutout modes. Integer arithmetic and flat u32 arrays throughout; deterministic across platforms; ~66 ms on a 1400x775 photo. The one dial, --watershed-detail (0..=255), maps exponentially to a target region count (each +25.5 doubles it) since the persistence distribution is far too skewed for a linear threshold. filter_speckle absorbs undersized basins into their most color-similar neighbour rather than dropping them, preserving the partition. The largest region is emitted first as a solid full-canvas background layer so stacked mode keeps its seam-free overdraw; the mosaic flatten is unaffected. Tests: partition invariant (disjoint masks tiling the canvas), detail monotonicity, min-area absorption, determinism, watershed cases in the pipeline/golden suites, stacked-vs-cutout interior equivalence, a watershed seam test, and SegmentKey coverage for the new params.
This commit is contained in:
@@ -28,7 +28,7 @@ use pyo3::exceptions::{PyIOError, PyValueError};
|
||||
use pyo3::prelude::*;
|
||||
|
||||
use ::vtracer::{
|
||||
Color, ColorImage, ColorMode, Config as CoreConfig, FitMode, Hierarchical, Preset,
|
||||
Color, ColorImage, Clustering, Config as CoreConfig, FitMode, Hierarchical, Preset,
|
||||
};
|
||||
|
||||
// --- string <-> enum helpers -------------------------------------------------
|
||||
@@ -37,10 +37,11 @@ fn parse<T: std::str::FromStr<Err = String>>(s: &str) -> PyResult<T> {
|
||||
s.parse().map_err(PyValueError::new_err)
|
||||
}
|
||||
|
||||
fn color_mode_str(m: ColorMode) -> &'static str {
|
||||
match m {
|
||||
ColorMode::Color => "color",
|
||||
ColorMode::Binary => "bw",
|
||||
fn clustering_str(c: Clustering) -> &'static str {
|
||||
match c {
|
||||
Clustering::ColorCluster => "color-cluster",
|
||||
Clustering::Binary => "bw",
|
||||
Clustering::Watershed => "watershed",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -129,7 +130,7 @@ impl PyConfig {
|
||||
impl PyConfig {
|
||||
#[new]
|
||||
#[pyo3(signature = (
|
||||
color_mode = "color",
|
||||
clustering = "color-cluster",
|
||||
hierarchical = "stacked",
|
||||
mode = "spline",
|
||||
filter_speckle = 4,
|
||||
@@ -147,10 +148,11 @@ impl PyConfig {
|
||||
adaptive = false,
|
||||
adaptive_window = 0,
|
||||
adaptive_t = 15.0,
|
||||
watershed_detail = 128,
|
||||
))]
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn new(
|
||||
color_mode: &str,
|
||||
clustering: &str,
|
||||
hierarchical: &str,
|
||||
mode: &str,
|
||||
filter_speckle: usize,
|
||||
@@ -168,6 +170,7 @@ impl PyConfig {
|
||||
adaptive: bool,
|
||||
adaptive_window: u32,
|
||||
adaptive_t: f64,
|
||||
watershed_detail: u8,
|
||||
) -> PyResult<Self> {
|
||||
let palette = match palette {
|
||||
Some(list) => list.iter().map(|s| parse_hex(s)).collect::<PyResult<_>>()?,
|
||||
@@ -175,7 +178,7 @@ impl PyConfig {
|
||||
};
|
||||
Ok(Self {
|
||||
inner: CoreConfig {
|
||||
color_mode: parse(color_mode)?,
|
||||
clustering: parse(clustering)?,
|
||||
hierarchical: parse(hierarchical)?,
|
||||
mode: parse(mode)?,
|
||||
filter_speckle,
|
||||
@@ -193,6 +196,7 @@ impl PyConfig {
|
||||
binary_adaptive: adaptive,
|
||||
binary_adaptive_window: adaptive_window,
|
||||
binary_adaptive_t: adaptive_t,
|
||||
watershed_detail,
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -224,15 +228,24 @@ impl PyConfig {
|
||||
// --- properties ---
|
||||
|
||||
#[getter]
|
||||
fn color_mode(&self) -> &'static str {
|
||||
color_mode_str(self.inner.color_mode)
|
||||
fn clustering(&self) -> &'static str {
|
||||
clustering_str(self.inner.clustering)
|
||||
}
|
||||
#[setter]
|
||||
fn set_color_mode(&mut self, v: &str) -> PyResult<()> {
|
||||
self.inner.color_mode = parse(v)?;
|
||||
fn set_clustering(&mut self, v: &str) -> PyResult<()> {
|
||||
self.inner.clustering = parse(v)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[getter]
|
||||
fn watershed_detail(&self) -> u8 {
|
||||
self.inner.watershed_detail
|
||||
}
|
||||
#[setter]
|
||||
fn set_watershed_detail(&mut self, v: u8) {
|
||||
self.inner.watershed_detail = v;
|
||||
}
|
||||
|
||||
#[getter]
|
||||
fn hierarchical(&self) -> &'static str {
|
||||
hierarchical_str(self.inner.hierarchical)
|
||||
@@ -432,11 +445,11 @@ impl PyConfig {
|
||||
fn __repr__(&self) -> String {
|
||||
let c = &self.inner;
|
||||
format!(
|
||||
"Config(color_mode='{}', hierarchical='{}', mode='{}', filter_speckle={}, \
|
||||
"Config(clustering='{}', hierarchical='{}', mode='{}', filter_speckle={}, \
|
||||
color_precision={}, layer_difference={}, corner_threshold={}, length_threshold={}, \
|
||||
max_iterations={}, splice_threshold={}, path_precision={:?}, palette={} colors, \
|
||||
max_colors={:?}, optimize={})",
|
||||
color_mode_str(c.color_mode),
|
||||
clustering_str(c.clustering),
|
||||
hierarchical_str(c.hierarchical),
|
||||
mode_str(c.mode),
|
||||
c.filter_speckle,
|
||||
|
||||
Reference in New Issue
Block a user