This stage also exists as a plain-language page with diagrams.
Stage 02 — Coverage
Turns an anti-aliased raster into a scalar coverage field with an explicit, per-pixel uncertainty, instead of a bitmap to be thresholded.
Source: crates/inkvec-trace/src/coverage.rs, with coverage/oversample.rs and
coverage/resample.rs (submodules re-exported from coverage, coverage.rs:58-61)
Entry points: bilevel_coverage() (coverage.rs:433), estimate_noise() (coverage.rs:356),
intake_scale() (coverage.rs:832), ringing_score() (coverage.rs:627),
oversample_factor() (coverage/oversample.rs:118), downsample_to() (coverage/resample.rs:31)
Pipeline position: the intake measurements every later stage is tuned by (the module's own
list, coverage.rs:35-51). The bilevel front end (trace_bilevel, inkvec-trace/src/lib.rs:140-146)
calls bilevel_coverage directly and traces its field. The Quality colour front end
(trace_color_full_with_alpha, inkvec-trace/src/lib.rs:299) does not build a CoverageField — it
takes the noise estimate (estimate_noise, inkvec-trace/src/lib.rs:325), the edge width
(intake_scale, inkvec-trace/src/lib.rs:358) and the ringing score (ringing_score,
inkvec-trace/src/lib.rs:363) and feeds them into palette extraction (stage 03); the native-alpha
path makes the same three measurements (native.rs:828-833). Later, planar::refine_subpixel
(planar.rs:438) computes each boundary point's uncertainty in vertex_sigma
(planar.rs:1159-1217), which re-derives the coverage-gradient formula this module documents
rather than sharing a CoverageField object. Fast mode measures none of the three: its front end
hands the shared stages NOISE_FLOOR as the noise (fast/front.rs:152-163). inkvec-cli's intake
reads intake_scale and oversample_factor to price its options in the raster's own units
(price_in_raster_units, inkvec-cli/src/lib.rs:396-508) and resamples with downsample_to.
There is no stopwatch mark named coverage. In the Quality path the stopwatch starts after the
noise estimate (inkvec-trace/src/lib.rs:338), so that estimate falls under no mark, while the
edge-width and ringing measurements fall inside the palette mark (inkvec-trace/src/lib.rs:423).
What problem this solves
An anti-aliased pixel at the edge of a shape is not noise, and it is not a blurry copy of a
sharp original. It is a measurement: the fraction of that pixel's area the foreground
colour covers. The module doc comment (coverage.rs:1-33) states the stakes plainly — reading
that pixel as a measurement rather than thresholding it away is, in the authors' words, "the
single largest lever in this project", and cites the failure this fixes: on the thin_features
benchmark, VTracer recovers 2 of 11 elements and no parameter setting recovers more, because
"the information is destroyed before any tunable stage runs" (coverage.rs:6-8, referencing
docs/M0-BASELINE.md §3).
Recovering the fraction is only half the job. The other half — and the reason this module is worth a stage of its own rather than a one-line helper — is recovering how much that fraction should be trusted, per pixel, and handing that number downstream so every later stage can decide how hard to try without being told separately.
Inputs and outputs
Input: Rgba (coverage.rs:174-183) — straight (unpremultiplied) RGBA floats in [0, 1],
row-major, four floats per pixel.
Output: CoverageField (coverage.rs:83-125):
| field | type | meaning |
|---|---|---|
width, height |
usize |
dimensions |
data |
Vec<f32> |
coverage a at each pixel centre, [0, 1] |
sigma_alpha |
f64 |
standard deviation of the coverage estimate, from pixel noise |
sigma_model |
f64 |
irreducible positional error of level-set extraction itself, in pixels |
fg, bg |
[f32; 3] |
the sRGB foreground/background colours coverage was measured against |
saturation |
f64 |
confidence, in [0, 1], that fg/bg were actually observed |
Two derived methods matter downstream:
gradient_magnitude(x, y)(coverage.rs:142-152) — central-difference|grad a|at the pixel nearest(x, y), no interpolation; neighbours outside the field are clamped to its edge.position_sigma(p)(coverage.rs:154-171) — positional uncertainty of a boundary point, in pixels: the quantity every later stage of the bilevel path actually consumes (throughcontour.rs:302-313).
The module also provides four image-wide measurements that are not part of the field:
estimate_noise (pixel noise), intake_scale (edge width), ringing_score (compression
ringing) and oversample_factor (surplus pixels). Each is described below.
How it works
Inverting the measurement
For a boundary between foreground F and background B, an observed pixel is the coverage
mixture
P = a*F + (1-a)*B
Projecting P onto the F - B axis inverts this:
a = dot(P - B, F - B) / |F - B|^2
This is a least-squares estimate across all three colour channels at once (coverage.rs:10-18),
not a single luminance threshold — the module doc comment is explicit that using all three
channels is deliberate, not incidental. In code this is bilevel_coverage's central map
(coverage.rs:487-493):
let n = (p[0] - bg[0]) * d[0] + (p[1] - bg[1]) * d[1] + (p[2] - bg[2]) * d[2]; (n as f64 / dd).clamp(0.0, 1.0) as f32
where d = F - B and dd = |F - B|^2.
Estimating F and B
F and B cannot simply be the darkest and lightest pixels in the image — a single JPEG
overshoot or stray speck would then define the colour axis for the whole image
(coverage.rs:419-423, coverage.rs:441-442). bilevel_coverage instead:
- Composites the image over white and computes luminance
0.2126*R + 0.7152*G + 0.0722*B(coverage.rs:435-439, Rec. 709 coefficients). - Sorts luminance and takes a small plateau —
0.1%of pixels, clamped to[1, max(n-1, 1)](coverage.rs:460-462) — from each extreme, rather than a flat percentile. The code comment notes a flat 2% percentile is "too conservative: on a shape only a few pixels wide it lands inside the anti-aliased ramp and reports a foreground far lighter than the truth" (coverage.rs:442-444). - Averages the RGB of the pixels whose luminance lies within 15% of the range
hi - loof each extreme (coverage.rs:464-466,mean_rgb_whereatcoverage.rs:549-577) to getfg(darker) andbg(lighter). If no pixel qualifies, the fallback is black forfgand white forbg.
Two degenerate inputs are answered rather than divided by zero: an empty image returns an
empty field (coverage.rs:448-459), and a uniform one (F == B) returns an all-zero field with
sigma_alpha = 0 and saturation = 1 (coverage.rs:470-484), which used to put NaN in every
sample. Both are pinned by tests (coverage.rs:1120-1146).
Two error-propagation steps
This is the part the module doc comment (coverage.rs:20-33) calls out as mattering
downstream, and it is worth reproducing exactly because the rest of the tracer's adaptive
behaviour is a consequence of it, not a separate design choice:
Step 1 — pixel noise to coverage noise. Pixel noise of standard deviation sigma_pixel
propagates through the projection linearly:
sigma_a = sigma_pixel / |F - B|
A faint boundary — small |F - B| — is measured badly, and the arithmetic says so directly: the
same noise divided by a smaller contrast gives a larger sigma_a.
Step 2 — coverage noise to positional noise. The boundary is extracted as the level set
a = 0.5. For an implicit curve, positional uncertainty along the gradient direction is
sigma_position = sigma_a / |grad a|
A crisp edge has a steep coverage gradient and localizes to a fraction of a pixel; a soft or blurred edge has a shallow gradient and does not.
Composing the two:
sigma = sigma_pixel / (|F - B| * |grad a|)
in pixels. This is the per-point σ_k of the fitter's objective in inkvec-fit,
E = ½·χ² + λ·P with χ² = Σ_k (d_k / σ_k)² (inkvec-fit/src/lib.rs:9-20): a point's miss is
weighed against its own measured uncertainty. Because sigma already contains the boundary's own
measured confidence, "the places we are allowed to simplify hard are exactly the places we
measured badly" (coverage.rs:32-33) — adaptive simplification needs no separate heuristic
layered on top.
The module doc comment says the same: "the per-point sigma of inkvec-fit's chi-squared term,
each miss weighed by 1/sigma^2" (coverage.rs:30-31). It used to word this as feeding "the
tau * sigma admissibility envelope in inkvec-fit". That per-point straightness test
(|d_k| <= tau * sigma_k, described at inkvec-fit/src/lib.rs:74-80) is no longer how the
shipping fitter works: the cone
that implemented it was removed as a correctness bug, and the test survives only as the reference
is_admissible (inkvec-fit/src/lib.rs:530-549), which "nothing in the shipping path calls"
(inkvec-fit/src/lib.rs:92-98). tau still scales tolerances elsewhere in the fitter, for
example the corner test of the decimation grid (r_j > τ², inkvec-fit/src/decimate.rs:21-28).
sigma_model and why noise alone is not enough
CoverageField::sigma_model (default DEFAULT_SIGMA_MODEL = 0.05, coverage.rs:63-81) is added
in quadrature to the noise-derived sigma in position_sigma (coverage.rs:161-171):
noise.hypot(self.sigma_model).clamp(1e-3, MAX_SIGMA)
The doc comment on the field (coverage.rs:94-106) explains why this exists: propagating pixel
noise alone gives "an absurdly optimistic answer on clean synthetic input" — with zero noise,
sigma_a and therefore sigma_position tend to zero, and the fit is told it knows the boundary
to a thousandth of a pixel. It does not. Extracting a boundary as the 0.5 level set of a
bilinearly-interpolated field carries its own systematic error: the true shape need not be
exactly representable by the interpolant, and the interpolant is not the true reconstruction
filter. The total is sqrt(noise^2 + model^2), a resolution limit no amount of clean input
removes. The stated derivation: "Measured on analytic circles (see the inkvec-trace tests),
level-set extraction lands within roughly 0.05px, which is the default" (coverage.rs:104-105).
That test is not in this module's own #[cfg(test)] block; it lives in
crates/inkvec-trace/tests/subpixel.rs. recovers_circle_radius_to_sub_pixel_accuracy
(subpixel.rs:107-127) renders an analytic circle, runs it through the full trace_bilevel
pipeline (which performs this module's level-set extraction), fits a circle to the recovered
contour, and asserts radius and centre error under 0.1 px;
accuracy_holds_as_the_shape_shrinks_toward_the_pixel_grid (subpixel.rs:154-167) repeats
this down to a 6px radius with a 0.15 px bound. Both measure the full contour-then-fit
pipeline rather than isolating sigma_model alone, and their asserted bounds (0.1–0.15 px)
are a looser envelope than the doc comment's "roughly 0.05px" — consistent with it, not an
independent re-derivation of the exact figure.
The constant's own doc comment (coverage.rs:66-80) records how much it decides. Combined in
quadrature in planar.rs, it dominates on the corpus: "79% of the gate set's 86,060 boundary
points come out between 0.050 and 0.060", so for most of a traced image the fitter's tolerance is
this constant. It is not an independent dial, though: chi² weights are 1/sigma², so scaling
every sigma by k is exactly lambda -> k²·lambda, and the measurement agrees — raising it to
0.10 moved the gate to dE00 +39.00% / ratio −10.28%, and --lambda-scale 4.0, the algebraically
equivalent change, to +44.34% / −11.68%, "the same frontier". Fidelity against compactness is
therefore tuned with --lambda-scale; this constant is documented as a property of the
extraction method.
When the gradient is at or below 1e-6, position_sigma sets the noise term to
MAX_SIGMA = 4.0 pixels (coverage.rs:162-168), and the total is clamped to [0.001, 4] px
(coverage.rs:170): a plateau where the level set is genuinely unlocalizable yields "the 4 px
ceiling rather than an infinity that would poison the fit" (coverage.rs:158-160).
saturation and unidentifiability
fg and bg are only trustworthy if some pixel in the image is actually fully inside the
shape. bilevel_coverage measures this geometrically rather than colorimetrically, because a
colorimetric measure would be circular — normalization maps the darkest observation to 1.0 by
construction, so "how many pixels are near 1.0" always answers "plenty" (coverage.rs:501-503).
The non-circular question: how many covered pixels (a >= 0.5) are strictly interior, with
all four orthogonal neighbours also covered (coverage.rs:506-525; a neighbour outside the image
counts as uncovered)? For a resolved shape, most covered pixels are interior; for a sub-pixel
stroke, none are, because it never fills a whole pixel with margin to spare.
saturation = interior / inside (1.0 if inside == 0)
The saturation doc comment (coverage.rs:116-124) states plainly what low saturation means: the
colour axis is genuinely unidentifiable. "A 0.3px black stroke and a 1px grey stroke produce
the same pixels, and no amount of processing separates them." The honest response, and the one
implemented, is not to guess a width and assert it — it is to widen sigma_alpha so downstream
stages simplify rather than trust a number that cannot be known:
let confidence_penalty = (1.0 / saturation.max(0.05)).min(8.0); sigma_alpha: sigma_pixel / contrast * confidence_penalty,
(coverage.rs:536, coverage.rs:541). The penalty is capped at 8x and the floor on saturation
in the division is 0.05, so the penalty itself is bounded; no stated derivation is given for
either the 0.05 floor or the 8.0 cap specifically (see Open questions).
estimate_noise — a low quantile of the Laplacian
estimate_noise(gray, w, h) (coverage.rs:319-358, body in estimate_noise_at,
coverage.rs:360-397) estimates per-channel pixel noise from a single grayscale array:
sigma = max(Q_q(|L|) / z_q / sqrt(Σ k_i²), NOISE_FLOOR)
(coverage.rs:330-332), in three steps:
- Compute the discrete Laplacian at every interior pixel:
4*c - left - right - up - down(coverage.rs:368-379), the kernel namedLAPLACIAN_KERNEL = [4.0, -1.0, -1.0, -1.0, -1.0](coverage.rs:300-306). The Laplacian annihilates smooth content, so what survives in a flat region is noise (coverage.rs:323). - Take a low quantile of the absolute values, the 10th percentile (
NOISE_QUANTILE = 0.10,coverage.rs:409-410), read by selection at indexfloor(n·0.10)(coverage.rs:393-394,kth_smallestatcoverage.rs:399-407). It used to be the median (MAD), and the reason it changed is recorded beside the read (coverage.rs:380-392): the median is robust only while edges are rare. "On a labyrinth of three-pixel strokes the median |Laplacian| IS an edge response: 53 display levels of 'noise' where the trace's own flat interiors say 0.57", and since the palette's same-ink test and the fit tolerance divide by this, "the trace ran two orders of magnitude too loose and strokes merged into blobs". The tenth percentile is still noise while a tenth of the image is flat. Measured over 246 icons at 128, 512 and 1024 px, every output was byte-identical, because on clean art both readings sit on the floor; JPEG and added grain improve. The testa_noiseless_edge_dense_picture_reports_no_noise(coverage.rs:1206-1227) reproduces it with three-pixel stripes: the estimate must stay under 2 levels, and the median reading is checked to read over 10. - Convert: divide by
Z10 = 0.12566(coverage.rs:412-415), the 10% point of a unit half-normal (Phi^-1(0.55)), which plays the roleMAD_TO_SIGMA = 0.6745played for the median (that constant is now test-only,coverage.rs:308-310), and by the kernel's noise gain, computed rather than written as a literal:LAPLACIAN_KERNEL.iter().map(|c| c * c).sum::<f64>().sqrt()(coverage.rs:395) — the root of the sum of the kernel's squared coefficients,sqrt(4^2 + 1 + 1 + 1 + 1) = sqrt(20). The result is floored atNOISE_FLOOR = 0.5/255(coverage.rs:396, constant atcoverage.rs:312-317). An image smaller than 3x3, or a buffer shorter thanw * h, returns1/255(coverage.rs:365-367).
The kernel's squared-coefficient sum is 20, not 6, and the function's doc comment
(coverage.rs:342-355) records why the gain is computed: until 2026-09-08 the divisor was the
literal sqrt(6), and that was a bug — sqrt(6) is the noise gain of the one-dimensional second
difference [1, -2, 1] (whose squared coefficients do sum to 6), not of the four-neighbour 2-D
Laplacian actually used here, which sums to 20. Every noise estimate before the fix was too large
by sqrt(20/6) = 1.83x, measured at 1.832x against synthetic noise of known sigma
(coverage.rs:346-348). The error stayed invisible because NOISE_FLOOR binds across the whole
corpus: "clean renders and JPEG-damaged files alike measure exactly 0.50/255"
(coverage.rs:350-352). Two regression tests pin it: the_kernel_constant_matches_the_loop
(coverage.rs:1229-1240) asserts the kernel is [4.0, -1.0, -1.0, -1.0, -1.0], sums to zero, and
has gain sqrt(20); noise_estimate_recovers_a_known_sigma (coverage.rs:1176-1204) generates
Gaussian noise of 2, 5 and 12 levels on a 400x400 image and asserts the estimate is within
[0.9, 1.1) of the truth, with a failure message that names the old 1.83x ratio explicitly.
The 0.5/255 floor is stated on the constant itself (coverage.rs:312-317): "callers divide by
it: color::extract_palette_mdl tests (nearest / sigma_noise)^2. Half a quantisation step is
the smallest deviation an 8-bit file could even represent." The test
noise_estimate_floors_at_half_a_level (coverage.rs:1242-1249) pins it. The Quality colour path
reports a floored estimate as a saturated measurement (inkvec-trace/src/lib.rs:318-335), and on a
soft intake later raises the noise to the residual measured against the labels
(regularize::residual_sigma, inkvec-trace/src/lib.rs:434-496; see stage 03).
ringing_score — compression ringing, read from the pixels
ringing_score(rgb, width, height) (coverage.rs:579-713) fills the gap that both of its
neighbours disclaim (coverage.rs:581-591): intake_scale reads 1.00 on a JPEG because
compression adds ringing rather than width, and estimate_noise cannot see it either, because an
icon is mostly empty and its low quantile of the Laplacian is zero. "Measured over 150 rendered
SVGs at four qualities, the shipped estimate is the same constant 0.00196 for a clean render, a
quality-85 JPEG and a quality-35 JPEG, while the true deviation from the clean original rises
0.0000, 0.0042, 0.0083, 0.0115."
The statistic is where the energy sits: anti-aliasing hugs a boundary and dies within a pixel
or two, while ringing sits in a band a few pixels out, where clean vector art is exactly flat
(coverage.rs:593-595). With L the 4-neighbour Laplacian of Rec. 709 luminance and d the
chamfer 3-4 distance to the nearest pixel whose gradient exceeds 24/255
(chamfer_distance_to_edges, coverage.rs:715-788):
core = { |L_i| : d_i <= 1 px } ring = { |L_i| : 3 px < d_i <= 7 px }
ratio = P90(ring) / P50(core)
hot = ring pixels with |L| > P60(ring)
score = ratio · (#hot right/down neighbour pairs whose L changes sign) / (#hot pairs)
(coverage.rs:613-621, constants at coverage.rs:628-636). The ratio is dimensionless and
stays monotone in compression where raw ring energy did not: measured 0.000 clean, 0.179 at q85,
0.215 at q60, 0.238 at q35 (coverage.rs:600-604). The ratio alone fires on 24% of clean brand
logos, because a gradient puts real signal in the ring, so it is multiplied by the sign
alternation rate (Gibbs ringing oscillates pixel to pixel, smooth artwork does not); "at the
shipped threshold it takes clean brand logos from 19% to 3.8% while still catching 82 to 91% of
JPEG. Measured across benchmark datasets" (coverage.rs:606-611). It returns 0 — the safe answer,
since the guard it feeds is only switched on by positive evidence — with no edge, under 9x9, a
short buffer, or fewer than 64 samples in either set or 64 hot pairs (coverage.rs:623-626). The
thresholds it is compared against (SOFT_RINGING, SOFT_RINGING_LARGE, RINGING_MIN_DIM) are
the palette's, color.rs:500-533 (stage 03). Tests: ringing_is_seen_in_the_ring_and_nowhere_else
(coverage.rs:956-1001) and ringing_score_is_safe_on_degenerate_input (coverage.rs:1003-1012).
bilevel_coverage and intake_scale
bilevel_coverage(img) (coverage.rs:417-547) is the entry point that ties the above together:
estimate fg/bg, project every pixel onto the axis to get data, measure saturation,
estimate sigma_pixel via estimate_noise on the composited-over-white luminance
(coverage.rs:532), and combine into sigma_alpha with the saturation penalty.
intake_scale(rgb, width, height) (coverage.rs:790-882) answers a different question: how many
raster pixels does one unit of genuine edge detail occupy? Its doc comment (coverage.rs:793-799)
gives the reason it is not the image size: a native render at 1024 traces in 1.67 s and finds 29
regions, the same content upsampled from 128 to 1024 takes 55.6 s and finds 1207. A ramp of height
d over w pixels has first difference d/w and second difference ~d/w^2, so the ratio of
first to second difference recovers w directly, independent of contrast (coverage.rs:807-811).
Concretely, over every run of three pixels along a row or a column of the channel mean
(r+g+b)/3 (coverage.rs:813-822):
w_obs = max(|d0|, |d1|) / |d1 − d0| kept when max(|d0|, |d1|) > 2/255
scale = max(1, median(clamp(w_obs, 0.25, 64)))
so flat interiors and single-pixel noise do not vote (EDGE_FLOOR, coverage.rs:833-834), a
straight ramp (d1 = d0) is skipped, fewer than 16 votes returns 1.0 (coverage.rs:876-878),
and the answer is at least 1.0 (coverage.rs:881). Measured behaviour (coverage.rs:824-828):
exactly 1.00 for native renders at 128, 512 and 1024; 2.39 and 4.00 for 4x and 8x Lanczos
upsamples; 1.70 and 3.00 for Gaussian blur of 1.0 and 2.0; JPEG reads 1.00. Its consumers are the
palette's soft-intake gate (SOFT_INTAKE_EDGE = 1.75, stage 03), --intake-scale
(normalise_intake, inkvec-cli/src/lib.rs:128-177) and the edge-width gate in
price_in_raster_units (inkvec-cli/src/lib.rs:430-441).
downsample_to and box_downsample_rgba8
downsample_to(img, nw, nh) (coverage/resample.rs:16-41) area-averages an image down: an exact
box filter, each target pixel the area-weighted average of the source pixels under its footprint,
with fractional weights on the boundary pixels (box_resample, coverage/resample.rs:79-183). It
lives in the resample submodule with its 8-bit twin box_downsample_rgba8
(coverage/resample.rs:43-77), which reads the decoder's buffer directly for the decode-time
--max-dim cap (load.rs:443, load.rs:500, load.rs:552). Its other callers are inkvec-cli's intake: --intake-scale (inkvec-cli/src/lib.rs:174),
--max-dim (inkvec-cli/src/lib.rs:366) and the soft-intake reduction
(inkvec-cli/src/soft_intake.rs:266). The unblock pre-pass no longer uses it: the lattice
inverse keeps one source pixel per cell (PixelGrid::reduce, inkvec-cli/src/alpha/unblock.rs:155-195). oversample_factor does not use it; it box-averages its own k x k
blocks (coverage/oversample.rs:129-149).
Its doc comment (coverage/resample.rs:23-25) states why it averages in premultiplied colour
and then un-premultiplies: averaging straight colour would let a transparent pixel's stored (and
arbitrary) RGB bleed into an opaque neighbour as a dark halo, which downstream "would become a
traced contour that is not in the artwork." The test
downsample_does_not_bleed_colour_from_transparent_pixels (coverage/resample.rs:200-231) pins
this: a 4x4 image of alternating opaque-white and transparent-black pixels downsamples to 2x2 with
every colour channel above 0.99, not grey.
oversample_factor — the round trip
oversample_factor(rgb, width, height) (coverage/oversample.rs:81-180) answers yet another,
distinct question from intake_scale: not how wide an edge is, but whether the raster's pixel
count exceeds what its content needs. It moved out of coverage.rs unchanged on 2026-10-02, with
its constants and tests, and is re-exported at the same public path (coverage/oversample.rs:1-8).
Its doc comment explains why it exists alongside intake_scale rather than replacing it:
intake_scale is "the wrong measure for tolerances, because a super-resolution model defeats
it -- it returns a sharp edge at high resolution, so the raster reads as barely oversampled when
it carries four times the pixels the drawing needs. Measured on a real brand mark upscaled 4x:
edge width 2.00, where the answer is 4" (coverage/oversample.rs:86-90). oversample_factor "asks
the question directly instead": an oversampled raster's surplus pixels can be thrown away and put
back, "indifferent to whether the surplus pixels are crisp or blurred, asking only whether they
say anything" (coverage/oversample.rs:92-96).
For k in 2, 4, 8 (coverage/oversample.rs:98-105): box-average k x k blocks (the trailing
width mod k columns and rows are dropped), resample back to full size bilinearly — pixel centre
x maps to (x + 0.5)/k − 0.5 in the small image, clamped to its edge — and take the mean
absolute error over all pixels and channels, in 8-bit levels. A k passes when that error
- stays under
OVERSAMPLE_TOL = 3.0(coverage/oversample.rs:10-24, test atcoverage/oversample.rs:169-171), and - is at most
OVERSAMPLE_KEEP = 0.5offlat_error, the error of erasing everything but the per-channel median colour (coverage/oversample.rs:26-53, test atcoverage/oversample.rs:172-176).
The answer is the largest k that passes with every smaller k also passing. The search stops
once the small image would be under 8 px on a side, and anything under 16x16 (or a buffer shorter
than width * height) returns 1 (coverage/oversample.rs:119-128).
flat_error (coverage/oversample.rs:55-79) is the yardstick of the second test:
flat_error = Σ_p Σ_c |rgb_p,c − m_c| · 255 / (3 · width · height), m_c = per-channel median
The median minimises the mean absolute error over constants, so this is how much an image loses
when everything but its one most typical colour is erased. It is 0 for a flat image, is computed
once (when the first k passes the absolute test, coverage/oversample.rs:173), and costs one
O(n) selection per channel.
Why the second test exists. OVERSAMPLE_TOL is a mean over every pixel, so it is diluted by
flat area (coverage/oversample.rs:29-34): "a 38 px² disc on a 144 px canvas, erased outright,
costs 0.47 levels against the 3.0 allowed, so it read as 8x oversampled, the speckle floor went up
64x to 128 px² and the disc was removed." A round trip that keeps the drawing loses a small share
of it; one that erases a shape loses about all of it. The margins, measured 2026-10-02 as the
round-trip error over the flat error at every factor the absolute test accepts
(coverage/oversample.rs:35-44):
- the corpus icons of the screen, held_a and held_b sets at 512 and 1024 px read at most 0.31 and
0.17 (both
twemoji/1f7eb, a square that fills the canvas), every other icon at most 0.18; at 256 px the same square reads 0.56 at /8 and is the one corpus icon the rule moves (8x to 4x); - 897 stress and test inputs over 128 px move only in three variants of that square;
- the 38 px² disc reads 0.36 at /2, 0.77 at /4 and 1.60 at /8, so it is 2x, not 8x.
The second test only ever lowers the answer, and on a flat image both errors are 0 and the answer
is unchanged (coverage/oversample.rs:107-111). The method is labelled in the code
(coverage/oversample.rs:46-52): Inspired by the relative error measures of forecast
evaluation, a method's error divided by a benchmark method's (Hyndman, R. J. & Koehler, A. B.
(2006), "Another look at measures of forecast accuracy", International Journal of Forecasting
22(4):679-688, doi:10.1016/j.ijforecast.2006.03.001). Here the method is the round trip and the
benchmark the best constant image under the same absolute error, the per-channel median. "The
half is our choice: a majority of the detail kept, with the margins above on both sides."
Tests: a_lone_small_shape_is_not_read_as_eight_times_oversampled
(coverage/oversample.rs:200-213) asserts the native 38 px² disc reads at most 2, the same kind of
disc upscaled 4x still reads at least 4, and a flat 64x64 image reads 8;
flat_error_is_the_mean_deviation_from_the_median (coverage/oversample.rs:215-225);
a_native_raster_is_not_oversampled and an_upscaled_raster_is_oversampled
(coverage.rs:1313-1325); and end to end, a_lone_small_shape_on_a_near_empty_canvas_is_drawn
(inkvec-cli/tests/pipeline.rs:343-364).
It returns 1 for most native renders at the corpus's 128 px (212 of the 246 screen icons); at
512 px nearly every native render reads 2 to 8, "which is the speckle floor scaling that caller
wants" (coverage/oversample.rs:113-117). Its callers (coverage/oversample.rs:4-5) are
inkvec-cli's price_in_raster_units (inkvec-cli/src/lib.rs:396-508, the call at
inkvec-cli/src/lib.rs:433) and --content-units (content_scale,
inkvec-cli/src/units.rs:33-78, the call at inkvec-cli/src/units.rs:76). In
price_in_raster_units, with r the factor and R = 128 px the reference extent, precision is
scaled by r only when the edge width exceeds the soft-intake threshold, min-area by r² and
lambda by r only when the longest side exceeds R (inkvec-cli/src/lib.rs:379-392); Fast mode
reads neither measurement (inkvec-cli/src/lib.rs:414-418).
Constants and thresholds
| name | value | controls | stated derivation |
|---|---|---|---|
DEFAULT_SIGMA_MODEL |
0.05 px |
floor added in quadrature to noise-derived positional sigma | "Measured on analytic circles" (coverage.rs:104-105); the test lives in inkvec-trace/tests/subpixel.rs:107-167, not in this module — see above. Its doc (coverage.rs:66-80) adds that 79% of 86,060 gate boundary points sit at 0.050-0.060, and that changing it is a lambda change (0.10 ≈ --lambda-scale 4.0) |
MAX_SIGMA (in position_sigma) |
4.0 px; total clamped to [0.001, 4] |
ceiling on positional uncertainty when the gradient vanishes | no stated derivation; described only as a ceiling "rather than an infinity that would poison the fit" (coverage.rs:158-162) |
plateau fraction (in bilevel_coverage) |
0.001 (0.1%) |
how much of the luminance extreme is trusted to define fg/bg |
qualitative only: chosen because a flat 2% percentile "lands inside the anti-aliased ramp" on thin shapes (coverage.rs:441-444, used at coverage.rs:460); no sweep cited for 0.1% specifically |
| extreme-band tolerance | 0.15 (15% of hi - lo) |
which pixels near each luminance extreme are averaged into fg/bg |
no stated derivation (coverage.rs:465-466) |
LAPLACIAN_KERNEL |
[4, -1, -1, -1, -1] |
the noise estimate's 4-neighbour kernel | the loop's own arithmetic, pinned by test (coverage.rs:300-306, coverage.rs:1229-1240) |
| Laplacian noise gain | sqrt(20), computed from LAPLACIAN_KERNEL (coverage.rs:395) |
corrects for the 4-neighbour Laplacian's noise gain in estimate_noise |
derived and pinned by test: root of the kernel's squared-coefficient sum; a hardcoded sqrt(6) (the 1-D second difference's gain) was a bug, fixed 2026-09-08 — see above |
NOISE_QUANTILE |
0.10 |
the quantile of the absolute Laplacian read as the noise level | measured: the median read 53 levels on a noiseless labyrinth where flat interiors say 0.57; 246 icons at 128/512/1024 px byte-identical after the change (coverage.rs:380-392, coverage.rs:409-410) |
Z10 |
0.12566 |
divides the 10th percentile to give a Gaussian sigma | derived: Phi^-1(0.55), the 10% point of the unit half-normal (coverage.rs:412-415) |
MAD_TO_SIGMA |
0.6745, test-only |
the old median-to-sigma conversion, kept for the regression test | standard statistical constant (coverage.rs:308-310) |
NOISE_FLOOR |
0.5/255 |
minimum estimate_noise can return |
half an 8-bit quantisation step; callers divide by it (coverage.rs:312-317, test at coverage.rs:1242-1249) |
| short-input noise | 1/255 |
what estimate_noise returns under 3x3 or for a short buffer |
no stated derivation (coverage.rs:365-367) |
confidence_penalty floor |
saturation.max(0.05) |
prevents an unbounded penalty when saturation is near zero |
no stated derivation (coverage.rs:536) |
confidence_penalty cap |
8.0 |
ceiling on the sigma inflation from low saturation | no stated derivation (coverage.rs:536) |
EDGE_FLOOR (ringing_score) |
24/255 |
gradient above which a pixel is an edge the ring is measured around | stated purpose, no numeric derivation (coverage.rs:628-629) |
core / ring band (ringing_score) |
core ≤ 1 px; ring 3–7 px | where anti-aliasing and ringing are read | the ring is fixed in pixels because ringing comes from an 8x8 DCT block and does not scale (coverage.rs:630-634; reason at color.rs:517-521) |
MIN_SAMPLES (ringing_score) |
64 |
fewest core, ring or hot-pair samples for an answer other than 0 | stated purpose: "Too few samples on either side and the percentiles mean nothing" (coverage.rs:635-636) |
EDGE_FLOOR (intake_scale) |
2.0/255 |
minimum first difference counted as a real edge, not noise | stated purpose, no numeric derivation (coverage.rs:833-834) |
MAX_W (intake_scale) |
64.0 |
clamp on a single edge-width observation | stated purpose (guards the degenerate straight-ramp case), no numeric derivation (coverage.rs:835-837) |
MIN_W (intake_scale) |
0.25 |
lower clamp on a single edge-width observation | no stated derivation (coverage.rs:838) |
| minimum edge observations | 16 |
below this, intake_scale returns 1.0 rather than trusting a thin sample |
no stated derivation (coverage.rs:876-878) |
OVERSAMPLE_TOL |
3.0 (mean abs. error, 8-bit levels) |
absolute round-trip error threshold for oversample_factor |
measured, and the doc comment is explicit it cannot cleanly separate native from oversampled on its own: "the lowest native reading at /2 is 1.29 ... while the 4x upscale this exists for reads 2.12 at /4" (coverage/oversample.rs:13-17); it is only safe to use downstream of the intake_scale gate, never as a gate itself (coverage/oversample.rs:19-23) |
OVERSAMPLE_KEEP |
0.5 (of flat_error) |
largest share of the image's detail a round trip may lose and still count as lossless | measured margins (2026-10-02): corpus icons at 512/1024 px at most 0.31/0.17, the 38 px² disc 0.36/0.77/1.60 at /2, /4, /8; "the half is our choice" (coverage/oversample.rs:26-53) |
minimum size for oversample_factor |
16x16, and sw/sh >= 8 at each step |
avoids measuring on too little data | no stated derivation (coverage/oversample.rs:119-128) |
Failure modes and edge cases
- Sub-pixel features are unidentifiable, not merely hard. When every feature in the image is
narrower than a pixel,
saturationis low andfg/bgcannot be trusted as measured — see thesaturationdiscussion above. The module's answer is to inflate uncertainty, not to guess. - Ringing is invisible to two of the three detectors.
intake_scalemeasures edge width; JPEG and other lossy codecs do not widen edges, they add oscillation in flat regions beside them.estimate_noisereads a low quantile of the Laplacian, and ringing occupies a band next to the edge while most of the image stays flat — the quantile never leaves its floor. The testringing_does_not_widen_an_edge(coverage.rs:1264-1308) pins both blind spots deliberately: on a synthetic ringing edge,intake_scalereads underSOFT_INTAKE_EDGE_FOR_TEST = 1.75(coverage.rs:1292-1297, constant atcoverage.rs:1310-1311), andestimate_noisereads exactly the0.5/255floor on both the ringing image and a clean control (coverage.rs:1304-1307). This is documented as a real regression: "The regression this module let through on 2026-09-08 ... The palette's noise guard was gated on this function alone, so a compressed logo measured a native 1.15 px, the guard stayed off, and the tracer fitted the encoder's ringing as artwork" (coverage.rs:1264-1269). Two signals were added rather than asking these two a question they cannot answer. The first is file-level,lossy_container(load.rs:58-96), which reads the container format itself (JPEG always lossy; WebP'sVP8/VP8LRIFF tag; PNG/GIF/BMP/TIFF always lossless). Two pixel-level detectors were tried before it and failed for the stated reason: "the Laplacian of a lossily-coded flat region and the Laplacian of a cleanly-rendered 8-bit colour ramp are the same size. A clean radial gradient measured a higher 'damage' score than a q50 flat icon" (load.rs:69-73). The container is defeated by a JPEG re-saved as PNG, which is the case the second signal,ringing_score, exists for: it reads where the Laplacian energy sits and whether it alternates in sign, not how large it is (see above). - A single JPEG overshoot or stray speck cannot hijack the colour axis — the plateau
percentile in
bilevel_coverageexists specifically to prevent this (coverage.rs:441-444). - A transparent pixel's stored colour cannot bleed into a downsample — see the halo-bug discussion above and its pinned test.
- A lone small shape on a near-empty canvas used to read as oversampled. Before the relative
test, the absolute round-trip error was diluted by the empty canvas: the 38 px² disc on a
144 px canvas read 8x, the CLI raised the speckle floor 64-fold to 128 px², and the disc was
removed.
OVERSAMPLE_KEEPfixes that case (see above). It does not address a small dot beside a large shape: the drawing's factor still sets the floor for the dot. Measured when the fix was made (2026-10-02): a big disc with a 38 px² dot on a 144 px canvas reads 4x, a speckle floor of 32 px², so the 38 px² dot survives and a 30 px² dot would not. That is the designed behaviour of the redundancy floor rather than this measurement's error. oversample_factoris not a substitute forintake_scale, and vice versa, and the doc comments on each say so explicitly:oversample_factorcatches an SR model producing a sharp edge on surplus pixels;intake_scalecatches a soft or blurred edge, whichoversample_factor's round-trip test alone would not reliably flag as oversampled versus genuinely band-limited native content (coverage/oversample.rs:13-23).
Environment overrides
Since the settings cleanup (CHANGELOG, 0.2.0, Changed) the engine reads its environment through one helper (inkvec_core::env): a switch is off when unset, empty or 0, and every variable is read once per process. Variables marked removed below are gone (their defaults are constants now); those marked research build are read only by a binary built with --features research. The full list, with what is left and why, is docs/internal/env-vars.md.
No INKVEC_* environment variable is read anywhere in the module (coverage.rs,
coverage/oversample.rs, coverage/resample.rs). INKVEC_NOISE_MEDIAN (removed) once switched
estimate_noise back to the median; its doc comment names it, and the regression test now reads
the median directly (coverage.rs:360-363). Callers downstream read variables that consume this
module's outputs: INKVEC_PALDBG prints the edge width, ringing score and the noise guard it
opened (inkvec-trace/src/lib.rs:396-401), and INKVEC_NOISE_SIGMAS (removed) and
INKVEC_SAME_INK_DE00 (removed) used to override the palette's use of them — see
03-palette.md.
Open questions
DEFAULT_SIGMA_MODEL's analytic-circle test — resolved.coverage.rs's own#[cfg(test)] mod testshas no test ofsigma_modelagainst a circle, but the test does exist, ininkvec-trace/tests/subpixel.rs:107-167— see thesigma_modeldiscussion above.- The Laplacian noise-gain factor — resolved. It is no longer
sqrt(6). The code computessqrt(20)fromLAPLACIAN_KERNEL(coverage.rs:395); see theestimate_noisediscussion for the fix and its regression tests. confidence_penalty's cap of8.0and floor of0.05(coverage.rs:536) have no stated derivation — no sweep or measured figure is cited, unlike most other constants in this module.- The
0.1%plateau,15%extreme-band tolerance,EDGE_FLOOR = 2.0/255,MAX_W = 64.0,MIN_W = 0.25, and the16-observation minimum inintake_scale, andringing_score's24/255edge floor and64-sample minimum, are qualitatively justified (what would go wrong without them) but none carries a specific measured value the wayNOISE_QUANTILE,OVERSAMPLE_TOLorOVERSAMPLE_KEEPdo. These read as reasonable engineering guesses rather than swept constants. - The colour path never constructs a
CoverageField, and its sigma is a separate copy. It callsestimate_noise(inkvec-trace/src/lib.rs:325) andintake_scale(inkvec-trace/src/lib.rs:358) itself, andvertex_sigma(planar.rs:1159-1217) re-derives thesigma_noise / contrast / |grad a|formula and addsDEFAULT_SIGMA_MODELin quadrature by hand rather than callingCoverageField::position_sigma. The two already differ in detail:vertex_sigmareads the coverage change across one pixel along the boundary normal on the unmixing axis (floored at 0.001), clamps the result to[0.02, 2]px, and applies the--simplify-faintinflation;position_sigmauses the field's central difference and clamps to[0.001, 4]px. Nothing ties a change to one to the other. - The module doc comment still names the
tau * sigmaenvelope (coverage.rs:30-33) as the consumer ofsigma. The shipping fitter usessigmaas the χ² weight instead (see above); the sentence is out of date, not the arithmetic.