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).
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.
Pipeline::segment runs only the frontend (the expensive clustering) and
returns a reusable Segmentation; Pipeline::finish re-runs just color
fitting, compositing, and optimization over a cached segmentation. This
restores the old stage-reuse workflow: cluster once, then tune curve-fit
or color-fit params without repaying clustering. Both have
*_with_progress variants; run/run_with_progress now compose the two
(one-shot path moves the owned segmentation, so it adds no clone).
finish clones the segmentation internally (color fitting mutates it), so
the cached copy stays pristine across many finish calls. Segmentation and
VectorDoc are re-exported at the crate root.
BinaryFrontend gains a Threshold enum: Fixed(u8) (now tunable, was
hardcoded to 128) and Adaptive { window, t } — Bradley–Roth adaptive
thresholding computed via visioncortex's SummedAreaTable, O(pixels)
regardless of window size, for images with uneven lighting. Both use a
shared (r+g+b)/3 intensity so they agree on "dark"; the grayscale
checker_bw golden is unaffected.
Exposed through Config and all bindings: CLI (--threshold, --adaptive,
--adaptive-window, --adaptive-t), Python (constructor kwargs + getters/
setters), and the Node package (binaryThreshold, adaptive, adaptiveWindow,
adaptiveT). Adds tests covering fixed tunability and adaptive recovering
locally-dark marks under a brightness gradient that a global cutoff can't.
Pipeline::run_with_progress(img, &CancelToken, &mut on_progress)
publishes per-phase Progress and aborts (Error::Cancelled) when the
token trips. The color-cluster frontend now drives visioncortex's
IncrementalBuilder so clustering reports fine-grained progress and
checks cancellation between batches; run() delegates to the new path,
which is output-identical since Runner::run() is that same tick loop.
Intended for native desktop apps (Tauri/egui/iced): run on a worker
thread, hand the UI a CancelToken clone for a cancel button, and
forward progress to a bar. Replaces the old browser-only cooperative
tick() API, which existed only because the main thread couldn't block.
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>
image-webp is pure Rust and compiles to wasm32; brings the npm
package's input formats closer to the CLI/Python surface. Wasm grows
~115 KB (486 KB -> 601 KB). Also gitignore nodejs/.npmrc.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- CHANGELOG: document the 1.0 framework rewrite
- CI: build Python wheels only on release tags + manual dispatch,
not on every push/PR
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The spline fitter's least-squares cubic fit (flo_curves, f64) diverges by ULPs
across architectures: on x86_64 vs arm64 an arc decomposes into slightly
different control points, changing the SVG bytes with no real geometry change
(verified with an x86_64 emulation: 0 pixels differ by >40, worst channel
delta 20 — visually identical). Byte-exact golden comparison is therefore
inappropriate for the spline output.
Render both the stored golden and the produced SVG (resvg) and diff pixels,
tolerating a tiny fraction for sub-pixel boundary flips. Encoding-agnostic and
architecture-robust, while still catching genuine regressions (which move
boundaries by whole pixels).
Publish under the @visioncortex npm org. Add publishConfig.access=public for
the scoped package and update the install/require examples in both READMEs.
vtracer only decodes input, but image's default features pulled a full AV1
encoder (ravif/rav1e) and OpenEXR into the CLI binary and the Python wheel.
Restrict to decode-only input formats (png, jpeg, gif, bmp, webp, tiff, ico,
pnm, tga, qoi). The release binary drops from ~3.23 MB to ~2.46 MB and builds
faster; supported inputs are unchanged in practice (avif decode was never in
image's defaults anyway).
visioncortex 0.9.0 is on crates.io, so the workspace now uses the registry
version instead of the local `../visioncortex` path (a [patch.crates-io]
example is left in a comment for local visioncortex development).
CI no longer needs the adjacent visioncortex checkout / published-crate caveats:
rust.yml, python.yml, and release.yml build from a single checkout.
- rust.yml: modernize (checkout@v4), build/test the whole workspace, add a
wasm32 core build check and a Node package build+test job. Checks out
visioncortex next to the repo so the local path dependency resolves.
- python.yml: build crates/vtracer-py (was the deleted cmdapp/); note that
manylinux builds need visioncortex 0.9.0 published to crates.io.
- release.yml: note the same visioncortex prerequisite for the CLI binary.
nodejs/scripts/publish.mjs builds the wasm (wasm-pack), runs the smoke test,
then `npm publish` to a configurable registry (default a local one at
http://localhost:4873; override via --registry= or NPM_REGISTRY). Supports
--dry-run. Wired as `npm run publish:local`.
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).
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.
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.
`vtracer in.png out.svg` now works alongside the `-i/--input` and
`-o/--output` flags. Input/output become optional positionals plus the
existing flags; an explicit flag wins over the positional, and a clear error
is shown if neither is given.
Feature — mosaic spline segment fitter (crates/vtracer/src/mosaic/fit.rs):
open-path cubic fitting for boundary segments, reusing the now-public
visioncortex primitives (PathSimplify::limit_penalties for symmetric,
gap-free staircase removal; open-path SubdivideSmooth::{find_corners,
subdivide_keep_corners,find_splice_points}; fit_points_with_bezier per splice
slice). Matches stacked spline curve quality; endpoints pinned to lattice
nodes so shared boundaries stay seam-free.
Fix — stacked mode punched holes in cluster masks (to_image_with_hole .. true);
stacked must trace solid layers and occlude by paint-order overdraw (false).
Holes left the layer below exposed as hairline seams.
Fix — the relative SVG writer measured a subpath's opening `m` from the last
vertex instead of the subpath start (SVG resets the current point to the start
after Z), misplacing holes / extra subpaths at optimize=1/2.
Tests — new tests/equivalence.rs: stacked-vs-mosaic interior agreement (all
fitters) and a seam guard (a full-coverage image must render fully opaque).
svg round-trip test (absolute vs relative encode identical geometry). mosaic
spline endpoint-pinning test. Regenerated goldens; added disc_mosaic_spline.
resvg added as a dev-dependency (test-only; not compiled for wasm).
Drop unused MosaicOptions placeholder
The strict/seam-stroke mitigations aren't needed — the mosaic geometry is
already gapless and seam-free. Remove the no-op MosaicOptions struct and thread
it out of Compositing::Mosaic and compose_mosaic.
Documents the systematic verification that the 1.0 pipeline reproduces 0.6.x
stacked output byte-for-byte: 475 parameter configurations (full per-parameter
sweeps + randomized interactions, pixel/polygon/spline, color/bw), geometry
compared against the 0.6.x cmdapp reference at path-precision 8. Zero geometry
mismatches (worst deviation 1e-8). Records the two bugs found and fixed during
verification (stacked hole-punching; relative-writer subpath origin), the
intentional differences (compact SVG encoding, empty-path omission), and the
reproduction procedure.
Implements the topological mosaic pipeline from docs/design/mosaic.md, turning
`--hierarchical cutout` into a true gapless tessellation instead of the old
re-cluster-and-retrace fake.
LabelMap (flatten Segmentation top-down)
→ boundary-graph extraction (integer-exact: corners, node rule, segment
and ring tracing on the pixel-corner lattice)
→ face assembly (left-region successor rule; winding falls out,
so each region is one nonzero-fill path)
→ fit each segment ONCE (shared by both adjacent faces, reversed
exactly → byte-identical shared boundaries)
→ compose per-region paths
Backends: PixelSegmentFitter (exact reference) and PolygonSegmentFitter
(symmetric open Douglas-Peucker collapsing staircases to the crack midline).
The spline segment fitter is still pending; mosaic + spline currently falls
back to polygon.
Compositing now owns its fitter (Stacked(CurveFitter) / Mosaic(SegmentFitter)).
Tests: single region, vertical split, T-junction, checkerboard pinch, nested
rings, border-touching, and a pixel round-trip property test over 40 random
maps (rasterize composed faces == input label map). Plus two mosaic goldens.
12 synthetic-image cases covering every stage: all three fitters, holes,
region adjacency, hierarchical layering, binary mode, fixed-palette and
auto-quantize color fitting, and the three optimizer/writer levels.
Fixtures are built from in-code images, not the JPEG samples, because JPEG
decoding is image-crate-version dependent and would make goldens fragile.
Regenerate after an intentional change with VTRACER_BLESS=1.
Superseded by crates/vtracer (framework) + crates/vtracer-cli. Verified the
new pipeline reproduces cmdapp's geometry and colors byte-for-byte (PNG always;
JPEG once the image-crate decoder is held constant), so the old crate is
retired. Drop its now-stale workspace exclude entry. Git history preserves it.