Uncap watershed details

This commit is contained in:
Chris Tsang
2026-08-04 10:21:32 +01:00
parent febcc28920
commit 6f163fcf7d
6 changed files with 24 additions and 18 deletions
+1 -1
View File
@@ -27,7 +27,7 @@ homepage = "http://www.visioncortex.org/vtracer"
repository = "https://github.com/visioncortex/vtracer/"
[workspace.dependencies]
visioncortex = "0.9.1"
visioncortex = "0.9.2"
# Schneider curve fitting for the simplify pass. visioncortex pins an old
# flo_curves internally for legacy reasons; we depend on the current one
# directly and convert at the call boundary.
+2 -2
View File
@@ -121,8 +121,8 @@ struct Args {
adaptive_t: Option<f64>,
/// Watershed clustering: hierarchy cut level (0..=255, higher = more regions).
#[arg(long, value_parser = clap::value_parser!(u8))]
watershed_detail: Option<u8>,
#[arg(long, value_parser = clap::value_parser!(u32))]
watershed_detail: Option<u32>,
}
fn parse_simplify_tolerance(s: &str) -> Result<f64, String> {
+3 -3
View File
@@ -172,7 +172,7 @@ impl PyConfig {
adaptive: bool,
adaptive_window: u32,
adaptive_t: f64,
watershed_detail: u8,
watershed_detail: u32,
) -> PyResult<Self> {
let palette = match palette {
Some(list) => list.iter().map(|s| parse_hex(s)).collect::<PyResult<_>>()?,
@@ -241,11 +241,11 @@ impl PyConfig {
}
#[getter]
fn watershed_detail(&self) -> u8 {
fn watershed_detail(&self) -> u32 {
self.inner.watershed_detail
}
#[setter]
fn set_watershed_detail(&mut self, v: u8) {
fn set_watershed_detail(&mut self, v: u32) {
self.inner.watershed_detail = v;
}
+5 -3
View File
@@ -80,7 +80,7 @@ pub struct SegmentKey {
binary_adaptive: bool,
binary_adaptive_window: u32,
binary_adaptive_t: f64,
watershed_detail: u8,
watershed_detail: u32,
}
/// High-level converter configuration. [`Config::build`] turns this into a
@@ -133,7 +133,7 @@ pub struct Config {
pub binary_adaptive_t: f64,
/// Watershed clustering: where to cut the hierarchy (0..=255). Higher
/// keeps more regions; 0 collapses the image to a single region.
pub watershed_detail: u8,
pub watershed_detail: u32,
}
impl Default for Config {
@@ -342,7 +342,9 @@ impl Config {
// apart (within a just-noticeable difference) from surviving
// as separate patches even at maximum detail.
merge_diff: match self.clustering {
Clustering::Watershed => ((255 - self.watershed_detail as i32) / 8).max(2),
Clustering::Watershed => {
(((255i64 - self.watershed_detail as i64) / 8).max(2)) as i32
}
_ => self.layer_difference,
},
},
+9 -5
View File
@@ -53,9 +53,10 @@ const ANCESTOR_AREA_BUDGET: usize = 3;
/// Watershed frontend: hierarchical watershed by volume, cut at `detail`.
#[derive(Debug, Clone)]
pub struct WatershedFrontend {
/// Detail level (0..=255): where to cut the hierarchy. Each +25.5 roughly
/// doubles the region count; 0 collapses the image to a single region.
pub detail: u8,
/// Detail level: where to cut the hierarchy. The normal range is 0..=255
/// each +25.5 roughly doubles the region count, 0 collapses the image to a
/// single region. Values above 255 are practically uncapped.
pub detail: u32,
/// Absorb regions smaller than this many pixels into their most
/// color-similar neighbour after the cut (0 = keep all).
pub min_area: usize,
@@ -268,7 +269,7 @@ impl WatershedHierarchy {
/// Cut the hierarchy at `detail` and emit the stacked [`Segmentation`].
/// Near-linear; safe to call repeatedly with different parameters.
pub fn cut(&self, img: &ColorImage, detail: u8, min_area: usize) -> Segmentation {
pub fn cut(&self, img: &ColorImage, detail: u32, min_area: usize) -> Segmentation {
let (w, h) = (self.width, self.height);
let n = w * h;
let m = self.mst.len();
@@ -279,7 +280,10 @@ impl WatershedHierarchy {
// merge a little more). The persistence distribution is extremely
// skewed — most merges are trivia at ≈ 0 — so the dial maps to a
// region *count*, exponentially: every +25.5 of detail doubles the
// target, from 1 region at 0 up to 1024 at 255.
// target, from 1 region at 0. The target saturates at the edge count,
// so a large detail (≥ 25.5·log2(pixels), e.g. ≥ 612 for a 4096²
// image) is practically uncapped: λ = min persistence, keeping every
// basin above the zero-persistence trivia.
let mut uf = Uf::new(n);
if m > 0 {
let target = (2f64).powf(detail as f64 / 25.5).round() as usize;
+4 -4
View File
@@ -78,7 +78,7 @@ fn regions(seg: &Segmentation) -> usize {
#[test]
fn flat_image_is_one_region() {
let img = image(24, 16, |_, _| (90, 120, 150));
for detail in [0u8, 128, 255] {
for detail in [0u32, 128, 255] {
let seg = WatershedFrontend {
detail,
min_area: 0,
@@ -127,7 +127,7 @@ fn detail_is_monotone() {
(r, g, 128)
});
let mut prev = 0usize;
for detail in [0u8, 64, 128, 192, 255] {
for detail in [0u32, 64, 128, 192, 255] {
let seg = WatershedFrontend {
detail,
min_area: 0,
@@ -202,7 +202,7 @@ fn hierarchy_recut_matches_one_shot() {
(((x * 5 + y * 3) % 200) as u8, ((x / 8) * 30) as u8, ((y / 8) * 40) as u8)
});
let hierarchy = WatershedHierarchy::build(&img).unwrap();
for detail in [64u8, 128, 200] {
for detail in [64u32, 128, 200] {
let recut = hierarchy.cut(&img, detail, 16);
let one_shot = WatershedFrontend {
detail,
@@ -231,7 +231,7 @@ fn session_recut_matches_one_shot() {
clustering: Clustering::Watershed,
..Config::default()
};
for detail in [128u8, 200, 64] {
for detail in [128u32, 200, 64] {
let cfg = Config {
watershed_detail: detail,
..base.clone()