mirror of
https://github.com/visioncortex/vtracer.git
synced 2026-09-12 15:16:08 -07:00
46a1b90ccd
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.
66 lines
2.5 KiB
Python
66 lines
2.5 KiB
Python
from typing import Optional
|
||
|
||
__version__: str
|
||
|
||
class Config:
|
||
"""Conversion configuration. Construct with keyword arguments or a preset,
|
||
mutate via properties, then call one of the ``convert_*`` methods."""
|
||
|
||
def __init__(
|
||
self,
|
||
clustering: str = "color-cluster", # "color-cluster" | "bw" | "watershed"
|
||
hierarchical: str = "stacked", # "stacked" | "cutout" (mosaic)
|
||
mode: str = "spline", # "pixel" | "polygon" | "spline"
|
||
filter_speckle: int = 4,
|
||
color_precision: int = 6,
|
||
layer_difference: int = 16,
|
||
corner_threshold: int = 60,
|
||
length_threshold: float = 4.0,
|
||
max_iterations: int = 10,
|
||
splice_threshold: int = 45,
|
||
path_precision: int = 2,
|
||
palette: Optional[list[str]] = None, # e.g. ["#112233", "#445566"]
|
||
max_colors: Optional[int] = None, # auto-quantize target
|
||
optimize: int = 1, # 0 | 1 | 2
|
||
binary_threshold: int = 128, # bw: fixed cutoff 0..=255
|
||
adaptive: bool = False, # bw: Bradley–Roth adaptive
|
||
adaptive_window: int = 0, # bw adaptive: window px (0 = auto)
|
||
adaptive_t: float = 15.0, # bw adaptive: % below local mean
|
||
watershed_detail: int = 128, # watershed: cut level 0..=255
|
||
) -> None: ...
|
||
|
||
@staticmethod
|
||
def bw() -> "Config": ...
|
||
@staticmethod
|
||
def poster() -> "Config": ...
|
||
@staticmethod
|
||
def photo() -> "Config": ...
|
||
|
||
clustering: str
|
||
hierarchical: str
|
||
mode: str
|
||
filter_speckle: int
|
||
color_precision: int
|
||
layer_difference: int
|
||
corner_threshold: int
|
||
length_threshold: float
|
||
max_iterations: int
|
||
splice_threshold: int
|
||
path_precision: Optional[int]
|
||
palette: list[str]
|
||
max_colors: Optional[int]
|
||
optimize: int
|
||
binary_threshold: int
|
||
adaptive: bool
|
||
adaptive_window: int
|
||
adaptive_t: float
|
||
watershed_detail: int
|
||
|
||
def convert_file(self, input_path: str, output_path: str) -> None: ...
|
||
def convert_bytes(self, data: bytes, format: Optional[str] = None) -> str: ...
|
||
def convert_pixels(self, rgba: bytes, width: int, height: int) -> str: ...
|
||
|
||
def convert_file(input_path: str, output_path: str, config: Optional[Config] = None) -> None: ...
|
||
def convert_bytes(data: bytes, config: Optional[Config] = None, format: Optional[str] = None) -> str: ...
|
||
def convert_pixels(rgba: bytes, width: int, height: int, config: Optional[Config] = None) -> str: ...
|