A pixel is a measurement, not a smudge
Every image made of pixels has a jagged secret at its edges. Draw a black circle on white and zoom into its boundary: the pixels there are not black or white, they are grey — some ninety percent black, some ten percent, a whole gradient of in-between. Most software treats that grey band as a nuisance: blur to hide, or threshold to erase it, snapping every pixel back to solid black or solid white.
inkvec treats it as information instead. A pixel that is seventy percent covered by the circle is not an ambiguous pixel — it is a precise statement that the true edge of the circle passes through that pixel in a particular place. Read correctly, the grey band tells you where the boundary sits to a fraction of a pixel, not merely which whole pixel it is closest to.
a = (P − B)·(F − B) / |F − B|² — project the observed pixel colour
P onto the axis between the foreground colour F and the
background colour B. Source: crates/inkvec-trace/src/coverage.rs:1-33.Where this stops working is honest, not silent: a feature narrower
than a pixel — a hairline stroke, say — never produces a fully-covered pixel at all,
so the darkest thing observed is already a blend. inkvec detects that condition
(saturation, in coverage.rs) and widens its own uncertainty
rather than guessing a width and stating it confidently.
One objective, everywhere
A vectoriser has to make hundreds of small decisions per image: is this smear one colour or two? Is this curve a circle or four short cubic segments? Is this almost-symmetric shape meant to be exactly symmetric? inkvec answers all of them with the same yardstick, an old idea from statistics called the minimum description length principle, stated here as one sum:
The rule
cost = 0.5 × (how badly this model fits the pixels) + λ × (how many numbers it takes to write down)
A model — a flat colour, a gradient, a circle, a cubic curve — is kept only when it earns its keep: it must lower the fitting error by more than it costs in parameters. λ (lambda) sets the exchange rate, and it is not a knob turned by eye — it is derived from the canvas size and the coordinate precision the SVG will be written to, because that precision is literally how many numbers a coordinate takes to state.
This is why a circle in the source art comes back as <circle
cx cy r/> rather than four approximate curves: three numbers beat twelve
whenever the pixels genuinely support a circle, and lose whenever they don't. It is
why a smooth gradient does not get chopped into eight flat-coloured bands, and why a
speck of single-pixel noise gets ignored rather than drawn.
The shape of the pipeline
Twenty-odd named steps, but they fall into four phases a reader can hold in mind at once: get a clean, correctly-scaled raster in; decide what regions and colours are in it; turn each region's boundary into curves; write the document.
00-overview.md. Every arrow between boxes is a real handoff of data —
never a re-read of the original pixels — except the boundary solve and the curve
fitter, which both go back to the image on purpose (see below).Two moments where the pipeline deliberately looks at the image twice
Most stages move forward only: decide, hand off, never revisit. Two stages break that rule on purpose, because a decision made one point at a time cannot see what a decision made about every point at once can.
First, after every boundary point has been placed by its own one-dimensional argument — "slide along my own normal until I read 0.5 coverage" — the whole boundary is solved again, all points at once, so that the picture the vector document would render actually matches the source pixel for pixel, not just point for point. A single point cannot tell that its neighbours moving would change what its own pixel should read; the joint solve can.
Second, once curves are fitted, primitive shapes (circles, ellipses, rounded rectangles) are tried against the same measured points and kept only when they cost fewer numbers than the general-purpose curve fit for the same accuracy — the objective described above, applied one more time.
Why shapes share an edge instead of each carrying their own
Picture a red square touching a blue square. The everyday way to draw that in SVG is two independent shapes, each with its own outline — and the two outlines have to agree, pixel for pixel, along the line where they touch, or a hairline gap of background shows through (a seam), or the two shapes overlap and double up the geometry along that edge (overdraw).
crates/inkvec-trace/src/planar.rs:1-18, which measures a
published open-source tracer sitting at 1.64× the ideal amount of overdrawn
geometry against a target of 1.00×.This is also why editing an inkvec output tends to behave the way a human-drawn one does: dragging one shared corner moves both shapes that meet there, instead of moving one and leaving a gap next to the other.
What "sub-pixel" actually buys
Because the pipeline never rounds a boundary to the nearest whole pixel — it keeps a real-valued position and an honest uncertainty for every point, all the way through — a stroke or a gap narrower than a single pixel is not automatically lost. Ordinary thresholding-based tracers cannot represent a shape that never reaches full coverage anywhere; a coverage-measuring one can, because "0.3 of a pixel wide" is still a perfectly good number to fit a curve to.