Commit Graph

40 Commits

Author SHA1 Message Date
Chris Tsang 2ef4bfb619 Hide the spline fine-tuning flags from CLI help
--corner-threshold, --segment-length, and --splice-threshold are still
accepted but no longer listed, and their -c/-l/-s short forms are gone:
the defaults serve virtually every conversion, and --simplify supersedes
them as the knob that actually moves output size (sweeping segment
length 3.5..=10 shifts the sample photo by 25% alone but under 2% once
simplify is on). README options block synced with the new help text.
2026-07-27 23:04:00 +01:00
Chris Tsang ef9496f792 Add a curve-simplification stage (--simplify), paper.js style
A new pipeline slot between curve fitting and composition: CurvePasses
rewrite each fitted contour, so mosaic mode transforms every shared
boundary segment exactly once and the tessellation stays seam-free by
construction. SimplifyCurves re-fits each smooth run between corners
with the fewest cubics within the tolerance (Schneider's algorithm via
a current flo_curves — visioncortex's copy is pinned to an old one and
block-splits at 200 points), with tangents from the chain's own ends,
corners kept in place, junction endpoints pinned bit-for-bit, and rings
seamed at their sharpest junction. Off by default; polylines pass
through untouched. Cityscape at tolerance 1: 229 -> 138 KB stacked,
103 -> 36 KB watershed cutout, with render diffs under golden noise.

CurveFitter now returns Vec<FittedGeom> (promoted from mosaic::fit) so
stacked contours flow through the same pass machinery; the optimizer's
SimplifyPass is renamed CleanupPass to free the word.
2026-07-27 22:30:05 +01:00
Chris Tsang 46a1b90ccd 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.
2026-07-27 14:45:25 +01:00
Chris Tsang f6c8a139a4 update screenshot
Rust / test (push) Has been cancelled
Rust / wasm-safety (core) (push) Has been cancelled
Rust / Node package (push) Has been cancelled
2026-07-26 00:31:10 +01:00
Chris Tsang abe21658dc Make filter_speckle a finish-phase filter, tunable without re-clustering
Speckle removal moves out of the frontends into Segmentation::filter_speckle,
applied in the finish phase. The color frontend now clusters with
good_min_area = 0 and the binary frontend emits every cluster, so the cached
segmentation retains all regions and the speckle threshold can be retuned via
finish() with no re-clustering. Pipeline gains a speckle_area field
(Config sets it from filter_speckle^2).

Frontend structs drop their filter_speckle_area field. Output on clean images
is unchanged (golden/equivalence pass unblessed); noisy images are filtered
downstream instead of during clustering. Adds a test tuning filter_speckle on
one cached segmentation.

Add finish-phase thin-strand filter (restores thread-like rejection)

good_min_area = 0 disabled visioncortex's thread-like rejection (which was
gated on good_min_area > 0). Reintroduce it in our repo as a finish-phase
step: Segmentation::filter_thin drops regions whose perimeter >= area
(average thickness under ~2px), using the same Shape::image_boundary_list
metric so the heuristic matches. It's toggleable on a cached segmentation
(Config::filter_thin, on by default), unlike the clustering-time version.

Exposed via CLI --keep-thin, Python filter_thin, and Node filterThin. Adds
RegionMask::perimeter/is_thin and a reuse test toggling it on one cached
segmentation. Clean-image goldens are unaffected (large regions aren't thin).

README: document binary thresholding, --keep-thin, and finish-phase filters

Add the new CLI flags (--threshold, --adaptive, --adaptive-window,
--adaptive-t, --keep-thin) to the options block and "New in 1.0"; note that
--optimize is encoding-only (precision is --path-precision) and that speckle/
thin filtering run after clustering. Add adaptive-threshold examples for CLI,
Python, and Node.

Return speckle/thin filtering to clustering (fix gum-tree regression)

good_min_area is visioncortex's clustering `deepen` gate, not a speckle
post-filter: it decides whether a small or thread-like patch is absorbed
into its nearest-color neighbour or kept as its own layer, and it enables
the thread-like rejection (perimeter < area). An earlier change set it to 0
to make filter_speckle "retunable downstream", which disabled the thin
check and reshaped the whole hierarchy — dissolving gradient-boundary
structure that clustering is meant to absorb. The Gum Tree preset's central
trunk vanished at gradient-step ~26 where the pre-1.0 path held it to 128.
Clean-logo goldens couldn't exercise it, so it shipped green.

Follow the proven webapp model instead: speckle lives inside clustering
(good_min_area = filter_speckle^2 for the colour frontend; a post-cluster
size gate for the binary frontend). The segment/finish split stays — it is
the progressive model (cluster once, re-run colour/curve/optimize cheaply);
clustering params (speckle, colour precision, layer difference, binary
threshold) re-segment.

Remove the downstream band-aids: Segmentation::filter_speckle/filter_thin,
RegionMask::perimeter/is_thin, the pipeline speckle_area/filter_thin fields,
Config::filter_thin, CLI --keep-thin, Python filter_thin, Node filterThin.
Update the two reuse tests that encoded the wrong contract and the binary
threshold test to use BinaryFrontend::min_area. All goldens, equivalence,
progress, and reuse tests pass.
2026-07-25 16:08:10 +01:00
Chris Tsang 1e3895318e README: surface the 1.0 packages (crates.io, PyPI, npm)
Rust / test (push) Has been cancelled
Rust / wasm-safety (core) (push) Has been cancelled
Rust / Node package (push) Has been cancelled
Add package badges, a Packages overview table, and note the npm
build's supported input formats (PNG/JPEG/GIF/BMP/WebP).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-24 18:21:09 +01:00
Chris Tsang 87b9660ed3 Scope the npm package to @visioncortex/vtracer
Publish under the @visioncortex npm org. Add publishConfig.access=public for
the scoped package and update the install/require examples in both READMEs.
2026-07-24 16:30:28 +01:00
Chris Tsang 749f0df0bd Add nodejs: WebAssembly Node package with no native dependency
New nodejs/ package: a wasm-bindgen crate (vtracer-wasm) built with wasm-pack
that wraps the vtracer framework, plus a thin JS layer for file I/O. Image
decoding (png/jpeg/gif/bmp via the image crate) runs in wasm too, so the
package has zero native dependencies — no sharp, no node-gyp.

No separate general-purpose wasm crate: the Node package directly owns and
wraps the wasm. Excluded from the cargo workspace (wasm-bindgen cdylib), built
with wasm-pack.

JS API (camelCase options): convertBuffer, convertPixels, convertFile,
convertFileSync — each taking an Options object (preset, colorMode,
hierarchical/cutout mosaic, mode, palette, maxColors, optimize, ...). Ships
index.d.ts types and a node smoke test. README updated.

Verified: builds to wasm32-unknown-unknown; `node test.js` passes; output
matches the CLI/Python bindings (253 paths on the tank sample).
2026-07-24 13:08:53 +01:00
Chris Tsang f76aed78b2 Add vtracer-py: Python bindings with a rich API
New crates/vtracer-py (pyo3 + maturin, abi3) wrapping the vtracer framework.
Rather than a thin CLI-style wrapper, it exposes a mutable `Config` class with
named properties and `bw`/`poster`/`photo` preset constructors, plus three
input paths — `convert_file`, `convert_bytes` (encoded image, optional format),
and `convert_pixels` (raw RGBA8) — available as `Config` methods and
module-level functions. Palette is a list of `#rrggbb` strings; bad inputs
raise ValueError.

The core crate stays pure: image decoding lives here. The crate is excluded
from the cargo workspace (pyo3 extension-module cdylibs don't link libpython,
which breaks `cargo test` at the root) and is built with maturin. Ships a
vtracer.pyi type stub. README updated.
2026-07-24 11:53:47 +01:00
Chris Tsang 57d768e37e Update README CLI docs for the 1.0 command app
Replace the 0.6.x help block with the current options (kebab-case flags,
positional input/output, filter-speckle 0..=128), document the new
capabilities (positional args, seam-free mosaic cutout, fixed palette /
auto-quantize, output optimization levels), and refresh the usage examples.
2026-07-24 11:39:14 +01:00
Chris Tsang 3a24c2f755 0.6.6 2026-01-28 18:21:44 +00:00
Chris Tsang efa4351b2c Update README.md 2024-09-27 10:43:47 +01:00
Chris Tsang a46292b5ed Update README.md 2024-09-27 10:43:02 +01:00
Chris Tsang ac0a89e08a README 2024-04-20 16:24:41 +01:00
Chris Tsang e4897dfe99 README 2024-04-20 16:16:57 +01:00
Chris Tsang 4544ca740d README 2024-04-20 16:14:21 +01:00
Chris Tsang 725adf5364 Update README.md 2024-03-30 14:46:52 +00:00
Chris Tsang fa7d021055 Readme 2023-11-12 20:39:25 +00:00
Chris Tsang ac93bd4a51 Edit 2023-09-24 11:08:30 +01:00
Chris Tsang e62f071b34 Edit 2023-09-23 11:52:36 +01:00
Chris Tsang 884092a5b9 Edit 2023-09-23 11:49:54 +01:00
Chris Tsang 5f5a6c6648 README 2023-09-23 11:45:36 +01:00
Chris Tsang 022018beb2 0.6.1 2023-09-23 11:12:34 +01:00
Chris Tsang cc39c8c5ca README 2023-09-23 11:06:18 +01:00
Chris Tsang d5dfa9fd73 Readme 2023-07-24 22:55:26 +08:00
Chris Tsang 971bffa948 Mentions Aliyun
Foot note: I was in touch with one of their engineers
2022-11-10 17:53:12 +08:00
Chris Tsang 7be6882d6b Readme 2022-10-14 00:32:02 +08:00
Chris Tsang 386a0bcaab Filestar 2022-10-14 00:29:13 +08:00
Oskar Skuteli 2448dbb3ba fix docs typo (#25) 2022-09-25 15:06:19 +08:00
Chris Tsang 0b292ad35f Readme 2021-07-24 16:48:47 +08:00
Chris Tsang e0dac19c31 Article link 2021-07-24 15:42:35 +08:00
Chris Tsang d18d4f8b81 Readme 2021-07-24 00:00:59 +08:00
Chris Tsang c1c09d964c Readme 2021-07-23 23:35:54 +08:00
Chris Tsang b2c95a50cd Update README.md 2021-03-01 21:13:24 +08:00
Chris Tsang 0c27d1f06a Update README.md 2021-01-25 00:53:01 +08:00
Chris Tsang 9d67b7c554 Release 2020-12-18 00:14:55 +08:00
Chris Tsang fae37a709e Update README.md 2020-12-09 14:51:15 +08:00
Chris Tsang 5c9d5cd757 Example for pixel art 2020-12-09 14:36:07 +08:00
Chris Tsang 7ea4600176 Readme 2020-11-15 16:54:46 +08:00
Chris Tsang 20187e0993 0.2.0 2020-11-15 16:27:37 +08:00