diff --git a/crates/trace/src/lib.rs b/crates/trace/src/lib.rs index 96441adf..00aec1b7 100644 --- a/crates/trace/src/lib.rs +++ b/crates/trace/src/lib.rs @@ -8,7 +8,10 @@ //! histogram as the [`Palette`] asks: median cut to a colour count followed by weighted k-means //! (Limited), median cut until every colour box is tight (Full Tone), one of those two by //! whether the image is flat art (Automatic), or the most used colours of a swatch library -//! (Document Library). Transparent pixels are left out. +//! (Document Library). Transparent pixels are left out. The palette is made of the picture's own +//! colours: the blends that anti-aliasing and blur leave along edges take none of it, colours that +//! noise made of one colour are merged, and a blend pixel goes to one of the two colours it lies +//! between (see `quantize`). //! 2. **Remove noise**: 4-connected same-colour components smaller than `noise` pixels are merged //! into their most common neighbouring colour. //! 3. **Follow boundaries**: for each colour layer, the pixel-crack edges between inside and diff --git a/crates/trace/src/quantize.rs b/crates/trace/src/quantize.rs index e6a15174..b3daa4d1 100644 --- a/crates/trace/src/quantize.rs +++ b/crates/trace/src/quantize.rs @@ -1,4 +1,5 @@ -//! Colour reduction and noise removal. +//! Colour reduction and noise removal. Colours are compared in Oklab, where equal distances look +//! equally different. use crate::{Mode, Palette, Raster, TraceParams}; @@ -37,12 +38,18 @@ pub fn quantize(img: &Raster, params: &TraceParams) -> Quantized { Quantized { labels, palette: vec![[255, 255, 255], [0, 0, 0]] } } Mode::Grayscale => { - let mut hist = [0u64; 256]; - for p in img.rgba.as_chunks::<4>().0 { - if p[3] >= 128 { - hist[luma([p[0], p[1], p[2], p[3]]) as usize] += 1; + let own = own_colours(img); + let histogram = |only_own: bool| { + let mut hist = [0u64; 256]; + for (p, &f) in img.rgba.as_chunks::<4>().0.iter().zip(&own) { + if p[3] >= 128 && (f || !only_own) { + hist[luma([p[0], p[1], p[2], p[3]]) as usize] += 1; + } } - } + hist + }; + let (every, calm) = (histogram(false), histogram(true)); + let hist = if palette_from_own(calm.iter().sum::() as f64, every.iter().sum::() as f64) { calm } else { every }; let levels = kmeans_1d(&hist, params.colors.clamp(2, 256) as usize); let mut lut = [0u16; 256]; for (v, slot) in lut.iter_mut().enumerate() { @@ -53,36 +60,251 @@ pub fn quantize(img: &Raster, params: &TraceParams) -> Quantized { let g = l.round().clamp(0.0, 255.0) as u8; [g, g, g] }); - compact(labels, palette.collect()) + settled(merge_mixed(compact(labels, palette.collect()), img.width as usize, img.height as usize), img, &own) } // Logo mode traces without quantising; asked directly, it quantises like Color. Mode::Color | Mode::Logo => { // 15-bit colour histogram with per-bin sums (exact bin means), fitted to the image's // range per channel so that close tones (a dark, low-contrast photo) stay apart. let hk = HistKey::new(img); - let mut bins: Vec = vec![Bin::default(); 1 << 15]; - for p in img.rgba.as_chunks::<4>().0 { + let own = own_colours(img); + let (mut every, mut calm) = (vec![Bin::default(); 1 << 15], vec![Bin::default(); 1 << 15]); + for (p, &f) in img.rgba.as_chunks::<4>().0.iter().zip(&own) { if p[3] >= 128 { - let b = &mut bins[hk.key(p[0], p[1], p[2])]; - b.n += 1; - b.sum[0] += p[0] as u64; - b.sum[1] += p[1] as u64; - b.sum[2] += p[2] as u64; + let k = hk.key(p[0], p[1], p[2]); + every[k].add(p); + if f { + calm[k].add(p); + } } } - let used: Vec = bins.iter().enumerate().filter(|(_, b)| b.n > 0).map(|(i, b)| (i, b.mean(), b.n as f64)).collect(); - let centers = color_palette(&used, params); - let mut lut = vec![0u16; 1 << 15]; - for (i, c, _) in &used { - lut[*i] = nearest_rgb(¢ers, *c) as u16; - } - let labels = px.map(|p| if p[3] < 128 { TRANSPARENT } else { lut[hk.key(p[0], p[1], p[2])] }).collect(); - let palette = centers.iter().map(|c| [c[0].round() as u8, c[1].round() as u8, c[2].round() as u8]).collect(); - compact(labels, palette) + let used = + |bins: &[Bin]| -> Vec { bins.iter().enumerate().filter(|(_, b)| b.n > 0).map(|(i, b)| (i, b.mean(), b.n as f64)).collect() }; + let (every, calm) = (used(&every), used(&calm)); + let count = |u: &[Used]| u.iter().map(|b| b.2).sum::(); + let from = if palette_from_own(count(&calm), count(&every)) { &calm } else { &every }; + // The picture with `colors` colours asked for; fewer come back when it has fewer, or + // when noise made several of one (see `merge_mixed`). + let reduce = |colors: u32| { + let centers = color_palette(from, &TraceParams { colors, ..params.clone() }); + let mut lut = vec![0u16; 1 << 15]; + let center_labs = labs(¢ers); + for (i, c, _) in &every { + lut[*i] = nearest_lab(¢er_labs, oklab(*c)) as u16; + } + let labels = + img.rgba.as_chunks::<4>().0.iter().map(|p| if p[3] < 128 { TRANSPARENT } else { lut[hk.key(p[0], p[1], p[2])] }).collect(); + let palette = centers.iter().map(|c| [c[0].round() as u8, c[1].round() as u8, c[2].round() as u8]).collect(); + let q = compact(labels, palette); + // A swatch library's colours are used as they are. + if params.palette == Palette::DocumentLibrary && !params.swatches.is_empty() { + q + } else { + merge_mixed(q, img.width as usize, img.height as usize) + } + }; + let q = reduce(params.colors); + settled(q, img, &own) } } } +/// The opaque pixels whose colour is the picture's own, not a blend: a pixel is a blend when it +/// lies between the two pixels on opposite sides of it (left and right, above and below, or +/// across a diagonal) that differ by more than 20 levels, which is what anti-aliasing and blur +/// leave along an edge. A line one pixel wide is no blend (the pixels on its two sides are alike), +/// nor is noise. +fn own_colours(img: &Raster) -> Vec { + let (w, h) = (img.width as isize, img.height as isize); + let px = img.rgba.as_chunks::<4>().0; + let at = |x: isize, y: isize| { + let p = (x >= 0 && y >= 0 && x < w && y < h).then(|| px.get((y * w + x) as usize)).flatten()?; + (p[3] >= 128).then(|| [f64::from(p[0]), f64::from(p[1]), f64::from(p[2])]) + }; + let mut out = Vec::with_capacity(px.len()); + for y in 0..h { + for x in 0..w { + let Some(p) = at(x, y) else { + out.push(false); + continue; + }; + let blend = [(1, 0), (0, 1), (1, 1), (1, -1)].into_iter().any(|(dx, dy)| { + let (Some(a), Some(b)) = (at(x - dx, y - dy), at(x + dx, y + dy)) else { return false }; + let d = [0, 1, 2].map(|k| b[k] - a[k]); + let len2: f64 = d.iter().map(|v| v * v).sum(); + if len2 <= 400.0 { + return false; + } + let t = (0..3).map(|k| (p[k] - a[k]) * d[k]).sum::() / len2; + let off2: f64 = (0..3).map(|k| (p[k] - a[k] - t * d[k]).powi(2)).sum(); + t > 0.06 && t < 0.94 && off2 < len2 / 16.0 + }); + out.push(!blend); + } + } + out +} + +/// Whether the palette is made from the picture's own colours alone (`own` of `all` opaque +/// pixels), so that the blends along edges take none of its colours. A picture with next to none +/// is all soft edges: there every pixel counts. +fn palette_from_own(own: f64, all: f64) -> bool { + own >= 0.05 * all && own >= 16.0 +} + +/// sRGB (0–255) to Oklab × 100 (Björn Ottosson, "A perceptual color space for image processing", +/// 2020): equal distances look equally different, and 2 or 3 is about the least one sees. +fn oklab(c: [f64; 3]) -> [f64; 3] { + let lin = c.map(|v| { + let v = (v / 255.0).clamp(0.0, 1.0); + if v <= 0.04045 { v / 12.92 } else { ((v + 0.055) / 1.055).powf(2.4) } + }); + let l = (0.412_221_470_8 * lin[0] + 0.536_332_536_3 * lin[1] + 0.051_445_992_9 * lin[2]).cbrt(); + let m = (0.211_903_498_2 * lin[0] + 0.680_699_545_1 * lin[1] + 0.107_396_956_6 * lin[2]).cbrt(); + let s = (0.088_302_461_9 * lin[0] + 0.281_718_837_6 * lin[1] + 0.629_978_700_5 * lin[2]).cbrt(); + [ + 100.0 * (0.210_454_255_3 * l + 0.793_617_785 * m - 0.004_072_046_8 * s), + 100.0 * (1.977_998_495_1 * l - 2.428_592_205 * m + 0.450_593_709_9 * s), + 100.0 * (0.025_904_037_1 * l + 0.782_771_766_2 * m - 0.808_675_766 * s), + ] +} + +/// `q` with the colours that noise made of one colour merged, largest mix first. +/// +/// Asked for more colours than a picture has, k-means splits a flat colour along its noise, and +/// the two halves would trace as specks of one all over the other. Such halves are close (under 30 +/// levels apart: noise is about as strong in a dark colour as in a light one, so the distance is +/// taken in RGB) and thoroughly mixed: the pixels of the lesser one have, on average, more than +/// half a neighbour of the other each; or they are closer still (under 12 levels) and mixed in +/// patches (compression blocks). The bands of a gradient are close too, but each keeps to itself +/// and they stay apart. +fn merge_mixed(q: Quantized, w: usize, h: usize) -> Quantized { + let k = q.palette.len(); + if k < 2 || q.labels.len() != w * h { + return q; + } + // How many pixels each colour has, and how many neighbouring pairs each pair of colours. + let mut count = vec![0.0f64; k]; + let mut touch = vec![0.0f64; k * k]; + let label = |i: usize| q.labels.get(i).map(|&l| l as usize).filter(|&l| l < k); + for i in 0..w * h { + let Some(a) = label(i) else { continue }; + count[a] += 1.0; + for j in [(i % w + 1 < w).then_some(i + 1), (i + w < w * h).then_some(i + w)].into_iter().flatten() { + if let Some(b) = label(j).filter(|&b| b != a) { + touch[a * k + b] += 1.0; + touch[b * k + a] += 1.0; + } + } + } + let mut color: Vec<[f64; 3]> = q.palette.iter().map(|c| c.map(f64::from)).collect(); + // Which colour each has merged into (itself when none). + let mut into: Vec = (0..k).collect(); + loop { + let mut best: Option<(f64, usize, usize)> = None; + for a in 0..k { + for b in a + 1..k { + if into[a] != a || into[b] != b || count[a] <= 0.0 || count[b] <= 0.0 { + continue; + } + let mixed = touch[a * k + b] / count[a].min(count[b]); + let apart = (0..3).map(|c| (color[a][c] - color[b][c]).powi(2)).sum::().sqrt(); + if (mixed > 0.5 && apart < 30.0 || mixed > 0.15 && apart < 12.0) && best.is_none_or(|x| mixed > x.0) { + best = Some((mixed, a, b)); + } + } + } + let Some((_, a, b)) = best else { break }; + color[a] = [0, 1, 2].map(|c| (color[a][c] * count[a] + color[b][c] * count[b]) / (count[a] + count[b])); + count[a] += count[b]; + count[b] = 0.0; + into[b] = a; + for c in 0..k { + if c != a && c != b { + touch[a * k + c] += touch[b * k + c]; + touch[c * k + a] = touch[a * k + c]; + } + } + } + if into.iter().enumerate().all(|(i, &t)| i == t) { + return q; + } + // A colour may have merged into one that merged again: follow each to where it ended up. + for i in 0..k { + let mut to = into[i]; + while into[to] != to { + to = into[to]; + } + into[i] = to; + } + let labels = q.labels.iter().map(|&l| into.get(l as usize).map_or(l, |&t| t as u16)).collect(); + compact(labels, color.iter().map(|c| [c[0].round() as u8, c[1].round() as u8, c[2].round() as u8]).collect()) +} + +/// `q` with its blend pixels settled, and the colours that leaves unused dropped. +/// +/// A pixel on an edge is a blend of the colours on its two sides, and the palette colour nearest +/// to a blend can be a third colour altogether (halfway from navy to yellow lies an olive that is +/// nearer to a green elsewhere in the picture), which would trace as a sliver of that colour along +/// the edge. A blend pixel (see [`own_colours`]) whose colour is none of those of the picture's +/// own pixels within two pixels of it takes the side it is nearer to of the pair of those colours +/// it is best a blend of, when that explains it twice as well as its own colour does. With one +/// colour around it, it keeps its own. +fn settled(mut q: Quantized, img: &Raster, own: &[bool]) -> Quantized { + let (w, h) = (img.width as usize, img.height as usize); + if q.labels.len() != w * h || own.len() != w * h { + return q; + } + let before = q.labels.clone(); + let color = |l: u16| q.palette.get(l as usize).map(|c| c.map(f64::from)); + let dist = |a: [f64; 3], b: [f64; 3]| (0..3).map(|k| (a[k] - b[k]).powi(2)).sum::().sqrt(); + let mut changed = false; + for i in 0..w * h { + let mine_l = before[i]; + if own[i] || mine_l == TRANSPARENT { + continue; + } + let (x, y) = (i % w, i / w); + // The colours of the picture's own pixels within two pixels (seldom more than three). + let mut near: Vec = Vec::new(); + for yy in y.saturating_sub(2)..=(y + 2).min(h - 1) { + for xx in x.saturating_sub(2)..=(x + 2).min(w - 1) { + let j = yy * w + xx; + if own[j] && before[j] != TRANSPARENT && !near.contains(&before[j]) { + near.push(before[j]); + } + } + } + let (Some(p), Some(mine)) = (img.rgba.get(i * 4..i * 4 + 3).map(|p| [f64::from(p[0]), f64::from(p[1]), f64::from(p[2])]), color(mine_l)) + else { + continue; + }; + if near.len() < 2 || near.contains(&mine_l) { + continue; + } + let mut best: Option<(f64, u16)> = None; + for (n, &a) in near.iter().enumerate() { + for &b in &near[n + 1..] { + let (Some(ca), Some(cb)) = (color(a), color(b)) else { continue }; + let d = [0, 1, 2].map(|k| cb[k] - ca[k]); + let len2: f64 = d.iter().map(|v| v * v).sum(); + let t = if len2 > 0.0 { ((0..3).map(|k| (p[k] - ca[k]) * d[k]).sum::() / len2).clamp(0.0, 1.0) } else { 0.0 }; + let off = dist(p, [0, 1, 2].map(|k| ca[k] + t * d[k])); + if best.is_none_or(|x| off < x.0) { + best = Some((off, if t < 0.5 { a } else { b })); + } + } + } + if let Some((off, to)) = best + && off < 0.5 * dist(p, mine) + { + q.labels[i] = to; + changed = true; + } + } + if changed { compact(q.labels, q.palette) } else { q } +} + /// Drop unused palette entries and merge duplicates. fn compact(mut labels: Vec, palette: Vec<[u8; 3]>) -> Quantized { let mut used = vec![false; palette.len()]; @@ -120,6 +342,12 @@ struct Bin { } impl Bin { + fn add(&mut self, p: &[u8; 4]) { + self.n += 1; + for (sum, v) in self.sum.iter_mut().zip(p) { + *sum += u64::from(*v); + } + } fn mean(&self) -> [f64; 3] { let n = self.n.max(1) as f64; [self.sum[0] as f64 / n, self.sum[1] as f64 / n, self.sum[2] as f64 / n] @@ -208,16 +436,11 @@ fn kmeans_1d(hist: &[u64; 256], k: usize) -> Vec { levels } -fn dist2(a: [f64; 3], b: [f64; 3]) -> f64 { - // Slightly perceptual weights. - let (dr, dg, db) = (a[0] - b[0], a[1] - b[1], a[2] - b[2]); - 2.0 * dr * dr + 4.0 * dg * dg + 3.0 * db * db -} - -fn nearest_rgb(centers: &[[f64; 3]], c: [f64; 3]) -> usize { +/// The index of the colour of `labs` (Oklab) nearest to `c` (Oklab): the one that looks most like it. +fn nearest_lab(labs: &[[f64; 3]], c: [f64; 3]) -> usize { let mut best = (f64::INFINITY, 0); - for (i, &m) in centers.iter().enumerate() { - let d = dist2(m, c); + for (i, m) in labs.iter().enumerate() { + let d = (m[0] - c[0]).powi(2) + (m[1] - c[1]).powi(2) + (m[2] - c[2]).powi(2); if d < best.0 { best = (d, i); } @@ -225,6 +448,10 @@ fn nearest_rgb(centers: &[[f64; 3]], c: [f64; 3]) -> usize { best.1 } +fn labs(colors: &[[f64; 3]]) -> Vec<[f64; 3]> { + colors.iter().map(|c| oklab(*c)).collect() +} + /// The most colours Full Tone (and Automatic on continuous-tone images) makes. pub const FULL_TONE_MAX: usize = 256; @@ -295,8 +522,9 @@ fn library_colors(used: &[Used], swatches: &[[u8; 3]], k: usize) -> Vec<[f64; 3] } } let mut weight = vec![0.0; all.len()]; + let all_labs = labs(&all); for (_, c, n) in used { - if let Some(w) = weight.get_mut(nearest_rgb(&all, *c)) { + if let Some(w) = weight.get_mut(nearest_lab(&all_labs, oklab(*c))) { *w += n; } } @@ -321,60 +549,73 @@ fn kmeans_rgb(used: &[Used], k: usize) -> Vec<[f64; 3]> { refine(used, centers(&median_cut(used, k, 0.0))) } -/// Median cut: repeatedly split the box with the largest weighted extent, while there are fewer -/// than `k` boxes and some box spans more than `spread` levels of a channel. +/// Divisive clustering in the manner of median cut, but each box is cut where that leaves its two +/// sides most alike within themselves (the cut, along one channel, with the largest between-side +/// variance: Otsu's criterion), and the box cut next is the one whose cut gains most. A cut at the +/// median, as in classic median cut, splits the largest colour of flat art in two along its noise +/// and leaves two smaller colours in one box. Boxes are cut while there are fewer than `k` and +/// some box spans more than `spread` levels of a channel. fn median_cut(used: &[Used], k: usize, spread: f64) -> Vec { if used.is_empty() { return vec![ColorBox { mean: [255.0; 3], weight: 0.0, peak: 0.0 }]; } - let mut boxes: Vec> = vec![(0..used.len()).collect()]; - while boxes.len() < k { - let mut best: Option<(f64, usize, usize)> = None; - for (bi, b) in boxes.iter().enumerate() { - if b.len() < 2 { - continue; - } - let (mut lo, mut hi) = ([f64::INFINITY; 3], [f64::NEG_INFINITY; 3]); - let mut wsum = 0.0; - for &i in b { - for ch in 0..3 { - lo[ch] = lo[ch].min(used[i].1[ch]); - hi[ch] = hi[ch].max(used[i].1[ch]); + /// A box's bins and its best cut: (gain, bins sorted along the cut's channel, bins on the first side). + struct Cut { + bins: Vec, + best: Option<(f64, Vec, usize)>, + } + let plan = |bins: Vec| { + let mut best: Option<(f64, Vec, usize)> = None; + let widest = (0..3) + .map(|ch| { + let v = bins.iter().map(|&i| used[i].1[ch]); + v.clone().fold(f64::NEG_INFINITY, f64::max) - v.fold(f64::INFINITY, f64::min) + }) + .fold(0.0, f64::max); + if bins.len() >= 2 && widest > spread.max(0.0) { + for ch in 0..3 { + let mut sorted = bins.clone(); + sorted.sort_by(|x, y| used[*x].1[ch].total_cmp(&used[*y].1[ch])); + let (total, sum): (f64, f64) = sorted.iter().fold((0.0, 0.0), |a, &i| (a.0 + used[i].2, a.1 + used[i].1[ch] * used[i].2)); + let (mut w, mut s) = (0.0, 0.0); + let mut cut: Option<(f64, usize)> = None; + for (j, &i) in sorted.iter().enumerate().take(sorted.len() - 1) { + w += used[i].2; + s += used[i].1[ch] * used[i].2; + let rest = total - w; + if w > 0.0 && rest > 0.0 { + let gain = w * rest / total * (s / w - (sum - s) / rest).powi(2); + if cut.is_none_or(|c| gain > c.0) { + cut = Some((gain, j + 1)); + } + } + } + if let Some((gain, at)) = cut + && best.as_ref().is_none_or(|b| gain > b.0) + { + best = Some((gain, sorted, at)); } - wsum += used[i].2; - } - let (ch, range) = (0..3).map(|c| (c, hi[c] - lo[c])).fold((0, -1.0), |a, b| if b.1 > a.1 { b } else { a }); - if range <= spread.max(0.0) { - continue; - } - let score = range * wsum.sqrt(); - if best.is_none_or(|x| score > x.0) { - best = Some((score, bi, ch)); } } - let Some((_, bi, ch)) = best else { break }; - let mut b = boxes.swap_remove(bi); - b.sort_by(|x, y| used[*x].1[ch].total_cmp(&used[*y].1[ch])); - let total: f64 = b.iter().map(|&i| used[i].2).sum(); - let mut acc = 0.0; - let mut cut = 1; - for (j, &i) in b.iter().enumerate() { - acc += used[i].2; - if acc >= total / 2.0 { - cut = (j + 1).clamp(1, b.len() - 1); - break; - } - } - let tail = b.split_off(cut); - boxes.push(b); - boxes.push(tail); + Cut { bins, best } + }; + let mut boxes = vec![plan((0..used.len()).collect())]; + while boxes.len() < k { + let gain = |c: &Cut| c.best.as_ref().map_or(f64::NEG_INFINITY, |b| b.0); + let Some(bi) = (0..boxes.len()).filter(|&i| boxes[i].best.is_some()).max_by(|&a, &b| gain(&boxes[a]).total_cmp(&gain(&boxes[b]))) else { + break; + }; + let Some((_, mut sorted, at)) = boxes.swap_remove(bi).best else { break }; + let tail = sorted.split_off(at); + boxes.push(plan(sorted)); + boxes.push(plan(tail)); } boxes .iter() .map(|b| { let mut s = [0.0; 3]; let (mut w, mut peak) = (0.0, 0.0f64); - for &i in b { + for &i in &b.bins { for (acc, v) in s.iter_mut().zip(used[i].1) { *acc += v * used[i].2; } @@ -386,13 +627,16 @@ fn median_cut(used: &[Used], k: usize, spread: f64) -> Vec { .collect() } -/// Lloyd refinement of `centers` over the bins `used`. +/// Lloyd refinement of `centers` over the bins `used`: each bin goes to the centre that looks most +/// like it (nearest in Oklab), and each centre becomes the mean colour of its bins. fn refine(used: &[Used], mut centers: Vec<[f64; 3]>) -> Vec<[f64; 3]> { + let bins = labs(&used.iter().map(|u| u.1).collect::>()); for _ in 0..12 { + let center_labs = labs(¢ers); let mut sum = vec![[0.0; 3]; centers.len()]; let mut cnt = vec![0.0; centers.len()]; - for (_, c, n) in used { - let j = nearest_rgb(¢ers, *c); + for ((_, c, n), lab) in used.iter().zip(&bins) { + let j = nearest_lab(¢er_labs, *lab); for ch in 0..3 { sum[j][ch] += c[ch] * n; } @@ -402,7 +646,7 @@ fn refine(used: &[Used], mut centers: Vec<[f64; 3]>) -> Vec<[f64; 3]> { for j in 0..centers.len() { if cnt[j] > 0.0 { let m = [sum[j][0] / cnt[j], sum[j][1] / cnt[j], sum[j][2] / cnt[j]]; - moved = moved.max(dist2(m, centers[j])); + moved = moved.max((0..3).map(|ch| (m[ch] - centers[j][ch]).powi(2)).sum()); centers[j] = m; } } diff --git a/crates/trace/src/tests.rs b/crates/trace/src/tests.rs index a728b91d..81ae9ef6 100644 --- a/crates/trace/src/tests.rs +++ b/crates/trace/src/tests.rs @@ -859,3 +859,88 @@ fn no_path_of_a_noisy_picture_leaves_the_picture() { } } } + +/// A deterministic value in `-1.0..1.0` for a pixel. +fn jitter(x: u32, y: u32, salt: u32) -> f64 { + let mut v = x.wrapping_mul(0x9E37_79B1) ^ y.wrapping_mul(0x85EB_CA77) ^ salt.wrapping_mul(0xC2B2_AE3D); + v ^= v >> 15; + v = v.wrapping_mul(0x2C1B_3C6D); + v ^= v >> 12; + f64::from(v & 0xFFFF) / 32768.0 - 1.0 +} + +/// Three flat colours (a navy field, a yellow disc, a green bar) drawn anti-aliased, with `noise` +/// levels of noise per channel. +fn three_colour_art(noise: f64) -> Raster { + const NAVY: [f64; 3] = [20.0, 40.0, 90.0]; + const YELLOW: [f64; 3] = [250.0, 200.0, 40.0]; + const GREEN: [f64; 3] = [40.0, 160.0, 90.0]; + Raster::from_fn(120, 120, |x, y| { + let mut sum = [0.0; 3]; + for i in 0..16 { + let (sx, sy) = (f64::from(x) + (f64::from(i % 4) + 0.5) / 4.0, f64::from(y) + (f64::from(i / 4) + 0.5) / 4.0); + let c = if Point::new(sx, sy).distance(Point::new(60.3, 45.6)) <= 30.0 { + YELLOW + } else if (20.4..100.7).contains(&sx) && (90.3..108.6).contains(&sy) { + GREEN + } else { + NAVY + }; + for k in 0..3 { + sum[k] += c[k] / 16.0; + } + } + let c = |k: usize| (sum[k] + noise * jitter(x, y, k as u32)).round().clamp(0.0, 255.0) as u8; + [c(0), c(1), c(2), 255] + }) +} + +fn sixteen_colors() -> TraceParams { + preset("16 Colors").unwrap() +} + +#[test] +fn more_colours_than_flat_art_has_trace_its_own_colours() { + for noise in [0.0, 8.0] { + let res = trace(&three_colour_art(noise), &sixteen_colors()); + assert_eq!(res.palette.len(), 3, "noise {noise}: {:?}", res.palette); + assert_eq!(res.paths.len(), 3, "noise {noise}"); + for want in [[20, 40, 90], [250, 200, 40], [40, 160, 90]] { + assert!(res.palette.iter().any(|c| (0..3).all(|k| c[k].abs_diff(want[k]) <= 3)), "noise {noise}: no {want:?} in {:?}", res.palette); + } + } +} + +#[test] +fn a_blend_of_two_colours_is_no_sliver_of_a_third() { + // Halfway from navy to yellow lies an olive nearer to the green of the bar than to either. + let res = trace(&three_colour_art(0.0), &TraceParams { colors: 3, noise: 0, ..sixteen_colors() }); + let green: Vec<&TracedPath> = res.paths.iter().filter(|p| p.color[1] > p.color[0] && p.color[1] > p.color[2]).collect(); + assert_eq!(green.len(), 1, "the bar, and nothing around the disc"); + let b = green[0].path.bounds().unwrap(); + assert!(b.y0 > 89.0 && b.y1 < 110.0, "{b:?}"); +} + +#[test] +fn hairlines_keep_their_colour_in_color_mode() { + // Black lines one pixel wide on white: every black pixel is on an edge, none is a blend. + let img = Raster::from_fn(80, 80, |x, y| if x % 10 == 4 || y % 10 == 4 { BLACK } else { WHITE }); + let q = quantize(&img, &TraceParams { mode: Mode::Color, colors: 6, ..TraceParams::default() }); + let black = q.palette.iter().position(|c| *c == [0, 0, 0]).unwrap() as u16; + assert_eq!(q.palette.len(), 2); + assert!(q.palette.contains(&[255, 255, 255])); + assert_eq!(q.labels.iter().filter(|&&l| l == black).count(), 8 * 80 * 2 - 64); +} + +#[test] +fn the_bands_of_a_gradient_stay_apart() { + let img = Raster::from_fn(256, 64, |x, y| [x as u8, (x / 2 + y) as u8, 200 - (x / 2) as u8, 255]); + let q = quantize(&img, &sixteen_colors()); + assert_eq!(q.palette.len(), 16); + // With noise as strong as the bands are apart, the bands still follow the gradient. + let noisy = Raster::from_fn(256, 64, |x, y| { + let c = |v: u32, k: u32| (f64::from(v) + 6.0 * jitter(x, y, k)).round().clamp(0.0, 255.0) as u8; + [c(x, 0), c(x / 2 + y, 1), c(200 - x / 2, 2), 255] + }); + assert!(quantize(&noisy, &sixteen_colors()).palette.len() >= 12); +} diff --git a/docs/image-trace-quality.md b/docs/image-trace-quality.md index a747d4ca..aef211c3 100644 --- a/docs/image-trace-quality.md +++ b/docs/image-trace-quality.md @@ -1,6 +1,6 @@ # Image Trace quality -> **Last reviewed:** 2026-10-13 · **Last updated:** 2026-10-13 · **Change:** minor (sub-pixel edges in every mode: outlines 0.31 to 0.09 px off for Default, 0.27 to 0.06 for Black and White Logo) +> **Last reviewed:** 2026-10-13 · **Last updated:** 2026-10-13 · **Change:** minor (the palette: 16 Colors on flat art 70 to 4.2 paths, High Fidelity Photo 3,636 to 639) What Image Trace (`crates/trace`) makes of images whose right answer is known, measured by a harness and compared with the numbers before each change. Nothing here is a judgement by eye, and nothing is compared @@ -72,17 +72,17 @@ Each preset on the inputs it is meant for, all sizes and variants. | Technical Drawing | polygons, star, letter A, lines | 2.66 | 13.45 | 0.201 | 1.37 | 105.76 | 4.8 | 32 | 2.23 | 100 | 85 | 5.9 | – | – | – | – | 0.1 | 0.0 | | Flat Logo | two touching halves, four-colour badge | 0.43 | 3.53 | 3.106 | 88.50 | 0.78 | 69.9 | 352 | 38.92 | 38 | 80 | 237.0 | 0.0 | 0 | 13.8 | – | 59.1 | 0.2 | -### Now (sub-pixel edges) +### Now (sub-pixel edges and the palette) | preset | inputs | ΔE | edge ΔE | off mean | off max | missed max | paths | anchors | × art | lines % | corners % | kinks | odd colours | odd px | gap px | overlap px | specks | outside px | |:--|:--|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:| | Default | circle, ellipses, badge with a hole, letter D, polygons, star, letter A | 2.60 | 10.12 | 0.091 | 1.02 | 2.17 | 1.9 | 18 | 1.81 | 29 | 68 | 0.4 | – | – | 22.2 | 22.4 | 0.0 | 0.0 | -| High Fidelity Photo | gradient, photo-like | 2.50 | 2.81 | – | – | – | 3636.4 | 81597 | – | 25 | – | – | – | – | 0.0 | – | 995.3 | 1.3 | -| Low Fidelity Photo | gradient, photo-like | 2.59 | 2.87 | – | – | – | 128.3 | 7277 | – | 37 | – | – | – | – | 0.0 | – | 0.1 | 0.4 | -| 3 Colors | two touching halves | 4.86 | 8.48 | 0.332 | 2.02 | 89.49 | 2.1 | 38 | 6.33 | 11 | 29 | 2.6 | 1.6 | 15527 | 50.3 | 40.4 | 0.0 | 0.0 | -| 6 Colors | two touching halves, four-colour badge | 0.61 | 4.88 | 4.442 | 83.47 | 30.03 | 21.2 | 473 | 53.89 | 45 | 95 | 344.2 | 2.4 | 279 | 68.6 | 79.0 | 0.0 | 0.2 | -| 16 Colors | two touching halves, four-colour badge | 0.60 | 5.32 | 5.084 | 85.85 | 4.16 | 69.9 | 1743 | 196.62 | 49 | 98 | 1376.4 | 5.9 | 612 | 152.8 | 141.8 | 0.2 | 0.6 | -| Shades of Gray | gradient, photo-like | 9.29 | 9.12 | – | – | – | 90.7 | 3183 | – | 51 | – | – | – | – | 0.0 | – | 5.3 | 0.8 | +| High Fidelity Photo | gradient, photo-like | 2.37 | 2.59 | – | – | – | 638.7 | 25604 | – | 29 | – | – | – | – | 0.0 | – | 151.5 | 0.5 | +| Low Fidelity Photo | gradient, photo-like | 2.67 | 2.93 | – | – | – | 55.3 | 5019 | – | 36 | – | – | – | – | 0.0 | – | 0.0 | 0.2 | +| 3 Colors | two touching halves | 0.43 | 4.32 | 0.087 | 0.74 | 0.56 | 2.0 | 9 | 1.57 | 39 | 100 | 0.0 | 0.0 | 0 | 48.6 | 29.4 | 0.0 | 0.0 | +| 6 Colors | two touching halves, four-colour badge | 0.56 | 5.01 | 0.137 | 1.09 | 1.14 | 4.0 | 50 | 3.15 | 40 | 88 | 3.6 | 0.0 | 0 | 39.0 | 35.7 | 0.0 | 0.0 | +| 16 Colors | two touching halves, four-colour badge | 0.55 | 4.89 | 0.133 | 1.09 | 1.15 | 4.2 | 66 | 3.97 | 41 | 88 | 4.4 | 0.0 | 0 | 34.5 | 26.4 | 0.0 | 0.0 | +| Shades of Gray | gradient, photo-like | 9.33 | 9.15 | – | – | – | 72.8 | 2681 | – | 50 | – | – | – | – | 0.0 | – | 1.9 | 0.8 | | Black and White Logo | circle, ellipses, badge with a hole, letter D, polygons, star, letter A | 2.58 | 9.39 | 0.064 | 1.00 | 2.18 | 1.4 | 27 | 2.73 | 40 | 67 | 0.3 | – | – | – | – | 0.0 | 0.0 | | Sketched Art | circle, ellipses, badge with a hole, letter D, lines | 2.71 | 12.11 | 0.195 | 1.00 | 105.54 | 1.8 | 17 | 1.49 | 16 | 86 | 0.0 | – | – | – | – | 0.0 | 0.0 | | Silhouettes | circle, ellipses, badge with a hole, letter D | 2.98 | 19.28 | 0.532 | 2.24 | 2.24 | 1.2 | 14 | 1.65 | 12 | 79 | 0.1 | – | – | – | – | 0.0 | 0.0 | @@ -90,7 +90,7 @@ Each preset on the inputs it is meant for, all sizes and variants. | Technical Drawing | polygons, star, letter A, lines | 2.53 | 9.47 | 0.086 | 0.99 | 105.85 | 4.8 | 57 | 3.73 | 69 | 74 | 0.7 | – | – | – | – | 0.3 | 0.0 | | Flat Logo | two touching halves, four-colour badge | 0.43 | 3.53 | 3.106 | 88.50 | 0.78 | 69.9 | 352 | 38.92 | 38 | 80 | 237.0 | 0.0 | 0 | 13.8 | – | 59.1 | 0.2 | -The sections below are the numbers now (sub-pixel edges). +The sections below are the numbers now (sub-pixel edges and the palette). ## 1. Each preset on the inputs it is meant for @@ -99,12 +99,12 @@ All sizes and variants (clean, aliased, blurred, noisy, scanned) of those inputs | preset | inputs | ΔE | edge ΔE | off mean | off max | missed max | paths | anchors | × art | lines % | corners % | kinks | odd colours | odd px | gap px | overlap px | specks | outside px | |:--|:--|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:| | Default | circle, ellipses, badge with a hole, letter D, polygons, star, letter A | 2.60 | 10.12 | 0.091 | 1.02 | 2.17 | 1.9 | 18 | 1.81 | 29 | 68 | 0.4 | – | – | 22.2 | 22.4 | 0.0 | 0.0 | -| High Fidelity Photo | gradient, photo-like | 2.50 | 2.81 | – | – | – | 3636.4 | 81597 | – | 25 | – | – | – | – | 0.0 | – | 995.3 | 1.3 | -| Low Fidelity Photo | gradient, photo-like | 2.59 | 2.87 | – | – | – | 128.3 | 7277 | – | 37 | – | – | – | – | 0.0 | – | 0.1 | 0.4 | -| 3 Colors | two touching halves | 4.86 | 8.48 | 0.332 | 2.02 | 89.49 | 2.1 | 38 | 6.33 | 11 | 29 | 2.6 | 1.6 | 15527 | 50.3 | 40.4 | 0.0 | 0.0 | -| 6 Colors | two touching halves, four-colour badge | 0.61 | 4.88 | 4.442 | 83.47 | 30.03 | 21.2 | 473 | 53.89 | 45 | 95 | 344.2 | 2.4 | 279 | 68.6 | 79.0 | 0.0 | 0.2 | -| 16 Colors | two touching halves, four-colour badge | 0.60 | 5.32 | 5.084 | 85.85 | 4.16 | 69.9 | 1743 | 196.62 | 49 | 98 | 1376.4 | 5.9 | 612 | 152.8 | 141.8 | 0.2 | 0.6 | -| Shades of Gray | gradient, photo-like | 9.29 | 9.12 | – | – | – | 90.7 | 3183 | – | 51 | – | – | – | – | 0.0 | – | 5.3 | 0.8 | +| High Fidelity Photo | gradient, photo-like | 2.37 | 2.59 | – | – | – | 638.7 | 25604 | – | 29 | – | – | – | – | 0.0 | – | 151.5 | 0.5 | +| Low Fidelity Photo | gradient, photo-like | 2.67 | 2.93 | – | – | – | 55.3 | 5019 | – | 36 | – | – | – | – | 0.0 | – | 0.0 | 0.2 | +| 3 Colors | two touching halves | 0.43 | 4.32 | 0.087 | 0.74 | 0.56 | 2.0 | 9 | 1.57 | 39 | 100 | 0.0 | 0.0 | 0 | 48.6 | 29.4 | 0.0 | 0.0 | +| 6 Colors | two touching halves, four-colour badge | 0.56 | 5.01 | 0.137 | 1.09 | 1.14 | 4.0 | 50 | 3.15 | 40 | 88 | 3.6 | 0.0 | 0 | 39.0 | 35.7 | 0.0 | 0.0 | +| 16 Colors | two touching halves, four-colour badge | 0.55 | 4.89 | 0.133 | 1.09 | 1.15 | 4.2 | 66 | 3.97 | 41 | 88 | 4.4 | 0.0 | 0 | 34.5 | 26.4 | 0.0 | 0.0 | +| Shades of Gray | gradient, photo-like | 9.33 | 9.15 | – | – | – | 72.8 | 2681 | – | 50 | – | – | – | – | 0.0 | – | 1.9 | 0.8 | | Black and White Logo | circle, ellipses, badge with a hole, letter D, polygons, star, letter A | 2.58 | 9.39 | 0.064 | 1.00 | 2.18 | 1.4 | 27 | 2.73 | 40 | 67 | 0.3 | – | – | – | – | 0.0 | 0.0 | | Sketched Art | circle, ellipses, badge with a hole, letter D, lines | 2.71 | 12.11 | 0.195 | 1.00 | 105.54 | 1.8 | 17 | 1.49 | 16 | 86 | 0.0 | – | – | – | – | 0.0 | 0.0 | | Silhouettes | circle, ellipses, badge with a hole, letter D | 2.98 | 19.28 | 0.532 | 2.24 | 2.24 | 1.2 | 14 | 1.65 | 12 | 79 | 0.1 | – | – | – | – | 0.0 | 0.0 | @@ -121,36 +121,36 @@ All sizes and variants (clean, aliased, blurred, noisy, scanned) of those inputs | Default | thin lines | 0.56 | 4.77 | 0.059 | 0.48 | 105.57 | 3.3 | 22 | 1.12 | 42 | – | – | – | – | 49.1 | 34.0 | 0.0 | 0.0 | | Default | flat colour art | 19.95 | 18.92 | 0.197 | 0.64 | 179.52 | 1.5 | 22 | 1.32 | 15 | 0 | 0.0 | – | – | 24.3 | 23.0 | 0.0 | 0.0 | | Default | gradient, photo | 39.13 | 38.78 | – | – | – | 3.2 | 698 | – | 57 | – | – | – | – | 31.2 | 40.7 | 0.0 | 0.3 | -| High Fidelity Photo | curved shapes | 0.08 | 3.65 | 0.243 | 1.07 | 0.58 | 9.9 | 187 | 24.84 | 28 | 50 | 0.3 | 8.7 | 299 | 0.0 | – | 3.8 | 0.0 | -| High Fidelity Photo | polygons | 0.13 | 4.80 | 0.160 | 1.14 | 2.43 | 3.3 | 173 | 16.11 | 29 | 78 | 0.1 | 1.7 | 87 | 0.0 | – | 0.0 | 0.0 | -| High Fidelity Photo | thin lines | 0.73 | 8.66 | 0.218 | 0.93 | 45.32 | 13.0 | 498 | 24.90 | 37 | – | – | 6.7 | 532 | 0.0 | – | 7.0 | 0.0 | -| High Fidelity Photo | flat colour art | 0.09 | 2.56 | 0.351 | 1.08 | 0.62 | 48.2 | 497 | 41.54 | 53 | 83 | 9.0 | 23.2 | 1119 | 0.0 | – | 19.2 | 0.0 | -| High Fidelity Photo | gradient, photo | 2.58 | 2.90 | – | – | – | 2926.5 | 87619 | – | 27 | – | – | – | – | 0.0 | – | 792.5 | 0.9 | -| Low Fidelity Photo | curved shapes | 0.09 | 4.45 | 0.219 | 1.02 | 0.50 | 5.0 | 55 | 6.69 | 34 | 100 | 6.7 | 3.8 | 261 | 0.0 | – | 0.0 | 0.0 | -| Low Fidelity Photo | polygons | 0.15 | 5.82 | 0.172 | 0.97 | 2.43 | 3.3 | 55 | 5.12 | 33 | 80 | 0.2 | 1.7 | 81 | 0.0 | – | 0.0 | 0.0 | -| Low Fidelity Photo | thin lines | 0.80 | 9.43 | 0.238 | 0.82 | 45.33 | 5.0 | 182 | 9.08 | 48 | – | – | 1.3 | 497 | 0.0 | – | 0.0 | 0.0 | -| Low Fidelity Photo | flat colour art | 0.11 | 3.30 | 0.323 | 1.04 | 0.56 | 20.0 | 142 | 10.37 | 61 | 100 | 35.7 | 7.0 | 940 | 0.0 | – | 0.0 | 0.0 | -| Low Fidelity Photo | gradient, photo | 2.67 | 2.97 | – | – | – | 96.2 | 8623 | – | 36 | – | – | – | – | 0.0 | – | 0.0 | 0.4 | -| 3 Colors | curved shapes | 0.47 | 4.85 | 0.128 | 1.06 | 0.52 | 2.8 | 30 | 3.42 | 20 | 100 | 1.7 | 1.0 | 86 | 35.0 | 41.9 | 0.0 | 0.0 | -| 3 Colors | polygons | 0.62 | 4.45 | 0.080 | 1.03 | 1.50 | 2.4 | 28 | 2.58 | 43 | 81 | 0.0 | 0.4 | 16 | 10.9 | 13.4 | 0.0 | 0.0 | -| 3 Colors | thin lines | 0.74 | 7.45 | 0.129 | 1.06 | 45.26 | 5.0 | 60 | 3.00 | 37 | – | – | 3.7 | 1123 | 84.3 | 53.1 | 0.0 | 0.0 | -| 3 Colors | flat colour art | 3.77 | 6.61 | 0.191 | 1.35 | 89.46 | 2.3 | 26 | 1.90 | 34 | 38 | 0.3 | 1.5 | 15138 | 35.8 | 28.5 | 0.2 | 0.0 | -| 3 Colors | gradient, photo | 7.85 | 7.91 | – | – | – | 4.3 | 611 | – | 47 | – | – | – | – | 23.6 | 30.1 | 0.0 | 0.2 | -| 6 Colors | curved shapes | 0.11 | 4.81 | 0.186 | 0.96 | 0.50 | 3.6 | 56 | 6.76 | 25 | 100 | 2.8 | 1.8 | 155 | 30.9 | 29.4 | 0.0 | 0.0 | -| 6 Colors | polygons | 0.12 | 4.81 | 0.128 | 0.80 | 1.51 | 3.1 | 56 | 5.23 | 35 | 81 | 0.2 | 1.1 | 47 | 22.2 | 20.6 | 0.0 | 0.0 | -| 6 Colors | thin lines | 0.91 | 9.26 | 0.160 | 0.86 | 45.36 | 4.0 | 132 | 6.60 | 40 | – | – | 1.7 | 409 | 53.6 | 44.0 | 0.0 | 0.0 | -| 6 Colors | flat colour art | 0.19 | 2.98 | 0.216 | 1.29 | 0.51 | 5.0 | 60 | 5.27 | 56 | 100 | 3.0 | 1.5 | 170 | 27.9 | 39.3 | 0.0 | 0.0 | -| 6 Colors | gradient, photo | 5.50 | 5.70 | – | – | – | 15.5 | 1421 | – | 38 | – | – | – | – | 54.3 | 75.6 | 0.0 | 0.2 | -| 16 Colors | curved shapes | 0.09 | 4.14 | 0.200 | 0.97 | 0.50 | 4.2 | 91 | 11.37 | 29 | 100 | 5.3 | 2.5 | 168 | 21.8 | 21.7 | 0.0 | 0.0 | -| 16 Colors | polygons | 0.13 | 5.20 | 0.138 | 0.97 | 2.43 | 3.1 | 81 | 7.53 | 33 | 81 | 0.3 | 1.1 | 52 | 15.3 | 18.5 | 0.0 | 0.0 | -| 16 Colors | thin lines | 0.79 | 9.20 | 0.187 | 0.82 | 45.35 | 4.0 | 287 | 14.35 | 35 | – | – | 1.3 | 362 | 24.9 | 22.6 | 0.0 | 0.0 | -| 16 Colors | flat colour art | 0.12 | 3.16 | 0.279 | 0.99 | 0.45 | 7.5 | 127 | 10.37 | 61 | 100 | 7.5 | 3.0 | 321 | 59.9 | 31.8 | 0.0 | 0.0 | -| 16 Colors | gradient, photo | 3.23 | 3.58 | – | – | – | 72.0 | 4652 | – | 30 | – | – | – | – | 165.5 | 258.6 | 0.0 | 0.8 | -| Shades of Gray | curved shapes | 0.11 | 4.71 | 0.216 | 1.00 | 0.59 | 5.1 | 49 | 6.02 | 30 | 100 | 5.9 | 3.8 | 252 | 0.0 | – | 0.0 | 0.0 | -| Shades of Gray | polygons | 0.11 | 4.32 | 0.147 | 0.80 | 1.39 | 3.3 | 45 | 4.09 | 36 | 82 | 0.0 | 1.7 | 76 | 0.0 | – | 0.0 | 0.0 | -| Shades of Gray | thin lines | 0.81 | 8.80 | 0.239 | 0.70 | 45.31 | 5.0 | 141 | 7.03 | 44 | – | – | 1.3 | 499 | 0.0 | – | 0.0 | 0.0 | -| Shades of Gray | flat colour art | 7.35 | 10.90 | 0.357 | 1.42 | 179.46 | 13.3 | 121 | 10.08 | 42 | 50 | 30.8 | 12.7 | 39181 | 0.0 | – | 0.0 | 0.0 | -| Shades of Gray | gradient, photo | 9.73 | 9.60 | – | – | – | 82.0 | 3181 | – | 51 | – | – | – | – | 0.0 | – | 2.2 | 0.8 | +| High Fidelity Photo | curved shapes | 0.04 | 1.74 | 0.026 | 0.16 | 0.16 | 1.2 | 20 | 2.17 | 21 | 100 | 0.0 | 0.0 | 0 | 0.0 | – | 0.0 | 0.0 | +| High Fidelity Photo | polygons | 0.05 | 1.92 | 0.031 | 0.51 | 1.34 | 1.7 | 17 | 1.50 | 87 | 88 | 0.0 | 0.0 | 0 | 0.0 | – | 0.0 | 0.0 | +| High Fidelity Photo | thin lines | 0.17 | 2.58 | 0.135 | 0.85 | 4.31 | 33.3 | 264 | 13.20 | 46 | – | – | 20.7 | 425 | 0.0 | – | 3.3 | 0.0 | +| High Fidelity Photo | flat colour art | 0.08 | 2.31 | 0.033 | 0.20 | 0.20 | 4.0 | 31 | 2.50 | 38 | 100 | 0.0 | 0.0 | 0 | 0.0 | – | 0.0 | 0.0 | +| High Fidelity Photo | gradient, photo | 2.46 | 2.67 | – | – | – | 428.3 | 22245 | – | 30 | – | – | – | – | 0.0 | – | 54.5 | 0.3 | +| Low Fidelity Photo | curved shapes | 0.06 | 2.65 | 0.045 | 0.27 | 0.27 | 1.2 | 14 | 1.53 | 19 | 100 | 0.0 | 0.0 | 0 | 0.0 | – | 0.0 | 0.0 | +| Low Fidelity Photo | polygons | 0.06 | 2.25 | 0.037 | 0.51 | 1.34 | 1.7 | 14 | 1.25 | 86 | 88 | 0.0 | 0.0 | 0 | 0.0 | – | 0.0 | 0.0 | +| Low Fidelity Photo | thin lines | 0.47 | 4.44 | 0.140 | 0.86 | 45.38 | 5.0 | 37 | 1.85 | 61 | – | – | 1.0 | 265 | 0.0 | – | 0.0 | 0.0 | +| Low Fidelity Photo | flat colour art | 0.09 | 2.61 | 0.048 | 0.27 | 0.27 | 4.0 | 25 | 1.88 | 36 | 100 | 0.0 | 0.0 | 0 | 0.0 | – | 0.0 | 0.0 | +| Low Fidelity Photo | gradient, photo | 2.77 | 3.04 | – | – | – | 52.7 | 5273 | – | 35 | – | – | – | – | 0.0 | – | 0.0 | 0.2 | +| 3 Colors | curved shapes | 0.06 | 2.65 | 0.046 | 0.33 | 0.24 | 1.8 | 16 | 1.77 | 16 | 100 | 0.0 | 0.0 | 0 | 18.7 | 15.2 | 0.0 | 0.0 | +| 3 Colors | polygons | 0.06 | 2.25 | 0.037 | 0.51 | 1.34 | 2.0 | 15 | 1.35 | 86 | 88 | 0.0 | 0.0 | 0 | 5.1 | 4.7 | 0.0 | 0.0 | +| 3 Colors | thin lines | 0.44 | 4.40 | 0.107 | 0.90 | 45.38 | 5.0 | 44 | 2.22 | 49 | – | – | 2.0 | 290 | 23.8 | 27.4 | 0.0 | 0.0 | +| 3 Colors | flat colour art | 1.32 | 3.85 | 0.113 | 0.90 | 60.22 | 2.5 | 24 | 1.77 | 46 | 100 | 0.0 | 1.0 | 9919 | 30.3 | 31.0 | 0.0 | 0.0 | +| 3 Colors | gradient, photo | 7.49 | 7.60 | – | – | – | 5.0 | 801 | – | 50 | – | – | – | – | 30.2 | 45.2 | 0.0 | 0.2 | +| 6 Colors | curved shapes | 0.06 | 2.38 | 0.042 | 0.28 | 0.24 | 1.8 | 17 | 1.86 | 17 | 100 | 0.0 | 0.0 | 0 | 17.9 | 14.3 | 0.0 | 0.0 | +| 6 Colors | polygons | 0.05 | 2.07 | 0.034 | 0.51 | 1.34 | 2.0 | 16 | 1.42 | 86 | 88 | 0.0 | 0.0 | 0 | 3.9 | 3.9 | 0.0 | 0.0 | +| 6 Colors | thin lines | 0.50 | 4.54 | 0.096 | 0.86 | 45.38 | 4.0 | 32 | 1.60 | 53 | – | – | 1.7 | 398 | 18.5 | 22.8 | 0.0 | 0.0 | +| 6 Colors | flat colour art | 0.12 | 2.66 | 0.044 | 0.25 | 0.20 | 3.0 | 28 | 2.00 | 43 | 100 | 0.0 | 0.0 | 0 | 21.9 | 21.2 | 0.0 | 0.0 | +| 6 Colors | gradient, photo | 5.03 | 5.30 | – | – | – | 20.7 | 1735 | – | 40 | – | – | – | – | 61.6 | 102.9 | 0.0 | 0.2 | +| 16 Colors | curved shapes | 0.05 | 2.26 | 0.038 | 0.26 | 0.23 | 1.8 | 18 | 1.93 | 17 | 100 | 0.0 | 0.0 | 0 | 14.9 | 12.5 | 0.0 | 0.0 | +| 16 Colors | polygons | 0.05 | 2.02 | 0.034 | 0.51 | 1.34 | 2.0 | 16 | 1.44 | 86 | 88 | 0.0 | 0.0 | 0 | 3.3 | 4.2 | 0.0 | 0.0 | +| 16 Colors | thin lines | 0.41 | 3.87 | 0.103 | 0.86 | 23.65 | 8.7 | 50 | 2.52 | 51 | – | – | 5.7 | 305 | 18.4 | 25.4 | 0.0 | 0.0 | +| 16 Colors | flat colour art | 0.12 | 2.53 | 0.041 | 0.24 | 0.20 | 3.0 | 28 | 2.07 | 44 | 100 | 0.0 | 0.0 | 0 | 19.8 | 19.5 | 0.0 | 0.0 | +| 16 Colors | gradient, photo | 3.12 | 3.44 | – | – | – | 50.7 | 3313 | – | 34 | – | – | – | – | 121.9 | 183.8 | 0.0 | 0.2 | +| Shades of Gray | curved shapes | 0.06 | 2.72 | 0.046 | 0.48 | 0.65 | 1.2 | 14 | 1.56 | 18 | 92 | 0.0 | 0.0 | 0 | 0.0 | – | 0.0 | 0.0 | +| Shades of Gray | polygons | 0.05 | 1.97 | 0.033 | 0.42 | 1.26 | 1.7 | 14 | 1.21 | 89 | 91 | 0.0 | 0.0 | 0 | 0.0 | – | 0.0 | 0.0 | +| Shades of Gray | thin lines | 0.49 | 4.80 | 0.142 | 0.86 | 45.38 | 9.0 | 54 | 2.70 | 63 | – | – | 6.0 | 472 | 0.0 | – | 0.0 | 0.0 | +| Shades of Gray | flat colour art | 7.34 | 10.80 | 0.384 | 1.40 | 179.38 | 12.5 | 108 | 9.54 | 43 | 50 | 30.0 | 11.2 | 39147 | 0.0 | – | 0.0 | 0.0 | +| Shades of Gray | gradient, photo | 9.73 | 9.61 | – | – | – | 81.8 | 3187 | – | 51 | – | – | – | – | 0.0 | – | 2.2 | 0.8 | | Black and White Logo | curved shapes | 0.04 | 1.52 | 0.024 | 0.50 | 0.60 | 1.2 | 21 | 2.25 | 21 | 75 | 0.0 | – | – | – | – | 0.0 | 0.0 | | Black and White Logo | polygons | 0.04 | 1.83 | 0.030 | 0.45 | 1.23 | 1.7 | 15 | 1.34 | 89 | 90 | 0.0 | – | – | – | – | 0.0 | 0.0 | | Black and White Logo | thin lines | 0.50 | 4.22 | 0.065 | 0.50 | 77.21 | 10.3 | 66 | 3.30 | 65 | – | – | – | – | – | – | 0.0 | 0.0 | @@ -191,36 +191,36 @@ All sizes and variants (clean, aliased, blurred, noisy, scanned) of those inputs | Default | blurred | 0.82 | 16.65 | 0.078 | 0.86 | 2.17 | 1.9 | 26 | 2.50 | 7 | 19 | 1.6 | – | – | 23.0 | 23.6 | 0.0 | 0.0 | | Default | noisy | 1.74 | 4.55 | 0.052 | 0.57 | 1.23 | 1.9 | 16 | 1.64 | 38 | 67 | 0.0 | – | – | 16.2 | 15.1 | 0.0 | 0.0 | | Default | scanned | 15.06 | 20.24 | 0.134 | 0.87 | 2.17 | 1.9 | 26 | 2.53 | 0 | 25 | 1.0 | – | – | 20.8 | 24.7 | 0.0 | 0.0 | -| High Fidelity Photo | clean | 2.58 | 2.90 | – | – | – | 2926.5 | 87619 | – | 27 | – | – | – | – | 0.0 | – | 792.5 | 0.9 | -| High Fidelity Photo | aliased | 2.55 | 2.88 | – | – | – | 2686.0 | 56883 | – | 22 | – | – | – | – | 0.0 | – | 695.5 | 0.9 | -| High Fidelity Photo | blurred | 2.05 | 2.50 | – | – | – | 1486.5 | 37982 | – | 22 | – | – | – | – | 0.0 | – | 205.5 | 1.3 | -| High Fidelity Photo | noisy | 3.10 | 3.37 | – | – | – | 7294.5 | 118138 | – | 26 | – | – | – | – | 0.0 | – | 2051.5 | 0.6 | -| High Fidelity Photo | scanned | 2.04 | 2.22 | – | – | – | 5208.5 | 95324 | – | 24 | – | – | – | – | 0.0 | – | 1637.0 | 0.4 | -| Low Fidelity Photo | clean | 2.67 | 2.97 | – | – | – | 96.2 | 8623 | – | 36 | – | – | – | – | 0.0 | – | 0.0 | 0.4 | -| Low Fidelity Photo | aliased | 2.62 | 2.91 | – | – | – | 125.5 | 5195 | – | 34 | – | – | – | – | 0.0 | – | 0.0 | 0.2 | -| Low Fidelity Photo | blurred | 2.67 | 3.13 | – | – | – | 151.5 | 4525 | – | 33 | – | – | – | – | 0.0 | – | 0.0 | 0.2 | -| Low Fidelity Photo | noisy | 2.75 | 2.94 | – | – | – | 106.5 | 7104 | – | 39 | – | – | – | – | 0.0 | – | 0.0 | 0.2 | -| Low Fidelity Photo | scanned | 2.07 | 2.19 | – | – | – | 226.0 | 8249 | – | 40 | – | – | – | – | 0.0 | – | 0.5 | 0.2 | -| 3 Colors | clean | 4.62 | 7.50 | 0.106 | 0.35 | 89.46 | 1.3 | 11 | 1.78 | 13 | 33 | 0.0 | 0.7 | 10577 | 33.4 | 32.3 | 0.0 | 0.0 | +| High Fidelity Photo | clean | 2.46 | 2.67 | – | – | – | 428.3 | 22245 | – | 30 | – | – | – | – | 0.0 | – | 54.5 | 0.3 | +| High Fidelity Photo | aliased | 2.32 | 2.54 | – | – | – | 239.0 | 15797 | – | 28 | – | – | – | – | 0.0 | – | 25.5 | 0.3 | +| High Fidelity Photo | blurred | 1.84 | 2.29 | – | – | – | 406.0 | 23561 | – | 24 | – | – | – | – | 0.0 | – | 39.0 | 0.1 | +| High Fidelity Photo | noisy | 2.84 | 3.01 | – | – | – | 1455.0 | 39709 | – | 20 | – | – | – | – | 0.0 | – | 479.0 | 0.5 | +| High Fidelity Photo | scanned | 2.19 | 2.29 | – | – | – | 1086.0 | 33427 | – | 38 | – | – | – | – | 0.0 | – | 353.5 | 0.3 | +| Low Fidelity Photo | clean | 2.77 | 3.04 | – | – | – | 52.7 | 5273 | – | 35 | – | – | – | – | 0.0 | – | 0.0 | 0.2 | +| Low Fidelity Photo | aliased | 2.83 | 3.08 | – | – | – | 64.0 | 3798 | – | 36 | – | – | – | – | 0.0 | – | 0.0 | 0.2 | +| Low Fidelity Photo | blurred | 2.47 | 2.92 | – | – | – | 40.5 | 3229 | – | 31 | – | – | – | – | 0.0 | – | 0.0 | 0.2 | +| Low Fidelity Photo | noisy | 2.99 | 3.17 | – | – | – | 72.5 | 6536 | – | 38 | – | – | – | – | 0.0 | – | 0.0 | 0.2 | +| Low Fidelity Photo | scanned | 2.09 | 2.19 | – | – | – | 52.0 | 5754 | – | 41 | – | – | – | – | 0.0 | – | 0.0 | 0.2 | +| 3 Colors | clean | 0.07 | 2.53 | 0.051 | 0.24 | 0.14 | 2.0 | 10 | 1.61 | 39 | 100 | 0.0 | 0.0 | 0 | 40.0 | 25.5 | 0.0 | 0.0 | | 3 Colors | aliased | 0.18 | 9.81 | 0.236 | 0.74 | 0.56 | 2.0 | 7 | 1.17 | 39 | 100 | 0.0 | 0.0 | 0 | 123.2 | 82.4 | 0.0 | 0.0 | -| 3 Colors | blurred | 6.92 | 10.46 | 0.848 | 2.02 | 87.75 | 4.0 | 122 | 20.33 | 0 | 0 | 10.0 | 4.0 | 25697 | 51.3 | 37.6 | 0.0 | 0.0 | -| 3 Colors | noisy | 7.21 | 9.91 | 0.112 | 0.31 | 89.49 | 1.0 | 12 | 2.00 | 0 | 0 | 0.0 | 1.0 | 25398 | 37.8 | 18.2 | 0.0 | 0.0 | -| 3 Colors | scanned | 5.86 | 6.64 | 0.814 | 1.49 | 88.54 | 4.0 | 93 | 15.50 | 0 | 0 | 8.0 | 4.0 | 25863 | 39.3 | 47.4 | 0.0 | 0.0 | -| 6 Colors | clean | 0.19 | 2.98 | 0.216 | 1.29 | 0.51 | 5.0 | 60 | 5.27 | 56 | 100 | 3.0 | 1.5 | 170 | 27.9 | 39.3 | 0.0 | 0.0 | +| 3 Colors | blurred | 0.33 | 7.80 | 0.088 | 0.21 | 0.16 | 2.0 | 10 | 1.67 | 39 | 100 | 0.0 | 0.0 | 0 | 29.7 | 18.6 | 0.0 | 0.0 | +| 3 Colors | noisy | 0.71 | 1.81 | 0.049 | 0.23 | 0.08 | 2.0 | 9 | 1.50 | 39 | 100 | 0.0 | 0.0 | 0 | 35.7 | 9.7 | 0.0 | 0.0 | +| 3 Colors | scanned | 1.57 | 3.23 | 0.082 | 0.34 | 0.22 | 2.0 | 11 | 1.83 | 39 | 100 | 0.0 | 0.0 | 0 | 31.9 | 18.9 | 0.0 | 0.0 | +| 6 Colors | clean | 0.12 | 2.66 | 0.044 | 0.25 | 0.20 | 3.0 | 28 | 2.00 | 43 | 100 | 0.0 | 0.0 | 0 | 21.9 | 21.2 | 0.0 | 0.0 | | 6 Colors | aliased | 0.31 | 10.53 | 0.291 | 1.00 | 1.00 | 3.0 | 26 | 1.91 | 42 | 100 | 0.0 | 0.0 | 0 | 82.6 | 74.0 | 0.0 | 0.0 | -| 6 Colors | blurred | 0.76 | 6.94 | 0.685 | 1.52 | 1.02 | 10.0 | 204 | 22.77 | 43 | 100 | 23.5 | 6.0 | 976 | 49.1 | 39.8 | 0.0 | 0.0 | -| 6 Colors | noisy | 1.20 | 3.58 | 20.366 | 79.78 | 30.03 | 99.0 | 2064 | 245.68 | 30 | 100 | 1822.5 | 3.0 | 297 | 191.1 | 209.1 | 0.0 | 0.2 | -| 6 Colors | scanned | 1.47 | 4.15 | 9.100 | 83.47 | 0.94 | 21.5 | 835 | 91.04 | 30 | 62 | 554.5 | 3.0 | 172 | 73.9 | 112.0 | 0.0 | 0.2 | -| 16 Colors | clean | 0.12 | 3.16 | 0.279 | 0.99 | 0.45 | 7.5 | 127 | 10.37 | 61 | 100 | 7.5 | 3.0 | 321 | 59.9 | 31.8 | 0.0 | 0.0 | +| 6 Colors | blurred | 0.62 | 9.41 | 0.249 | 1.09 | 1.14 | 6.0 | 115 | 6.33 | 42 | 50 | 9.5 | 0.0 | 0 | 32.6 | 48.3 | 0.0 | 0.0 | +| 6 Colors | noisy | 0.85 | 2.50 | 0.048 | 0.26 | 0.18 | 3.0 | 28 | 2.01 | 33 | 100 | 0.0 | 0.0 | 0 | 29.7 | 22.5 | 0.0 | 0.0 | +| 6 Colors | scanned | 1.76 | 4.63 | 0.240 | 1.09 | 0.94 | 7.0 | 101 | 5.81 | 31 | 62 | 15.5 | 0.0 | 0 | 62.7 | 41.7 | 0.0 | 0.0 | +| 16 Colors | clean | 0.12 | 2.53 | 0.041 | 0.24 | 0.20 | 3.0 | 28 | 2.07 | 44 | 100 | 0.0 | 0.0 | 0 | 19.8 | 19.5 | 0.0 | 0.0 | | 16 Colors | aliased | 0.30 | 10.39 | 0.274 | 1.00 | 1.00 | 3.0 | 34 | 2.34 | 43 | 100 | 0.0 | 0.0 | 0 | 77.3 | 51.7 | 0.0 | 0.0 | -| 16 Colors | blurred | 0.59 | 8.94 | 0.949 | 2.58 | 1.73 | 26.5 | 421 | 35.52 | 57 | 100 | 102.5 | 10.0 | 1155 | 149.8 | 121.6 | 0.0 | 0.0 | -| 16 Colors | noisy | 1.31 | 4.52 | 17.093 | 82.18 | 4.16 | 238.5 | 4758 | 593.71 | 29 | 100 | 4008.5 | 4.5 | 607 | 313.9 | 328.8 | 0.0 | 0.2 | -| 16 Colors | scanned | 1.63 | 3.93 | 16.438 | 85.85 | 3.01 | 199.0 | 6606 | 713.67 | 29 | 88 | 5501.0 | 18.0 | 1561 | 349.0 | 394.9 | 1.5 | 0.6 | -| Shades of Gray | clean | 9.73 | 9.60 | – | – | – | 82.0 | 3181 | – | 51 | – | – | – | – | 0.0 | – | 2.2 | 0.8 | -| Shades of Gray | aliased | 9.73 | 9.58 | – | – | – | 80.0 | 2438 | – | 54 | – | – | – | – | 0.0 | – | 3.0 | 0.8 | -| Shades of Gray | blurred | 9.69 | 9.39 | – | – | – | 62.0 | 1652 | – | 50 | – | – | – | – | 0.0 | – | 1.0 | 0.7 | -| Shades of Gray | noisy | 9.79 | 9.52 | – | – | – | 132.0 | 3813 | – | 51 | – | – | – | – | 0.0 | – | 9.0 | 0.8 | -| Shades of Gray | scanned | 6.64 | 6.56 | – | – | – | 115.0 | 4832 | – | 48 | – | – | – | – | 0.0 | – | 17.5 | 0.5 | +| 16 Colors | blurred | 0.62 | 9.37 | 0.250 | 1.08 | 1.15 | 7.0 | 157 | 8.43 | 44 | 50 | 11.5 | 0.0 | 0 | 27.8 | 38.2 | 0.0 | 0.0 | +| 16 Colors | noisy | 0.85 | 2.34 | 0.042 | 0.27 | 0.22 | 3.0 | 32 | 2.27 | 34 | 100 | 0.0 | 0.0 | 0 | 23.4 | 15.6 | 0.0 | 0.0 | +| 16 Colors | scanned | 1.74 | 4.56 | 0.243 | 1.09 | 0.96 | 7.5 | 156 | 8.53 | 32 | 62 | 19.0 | 0.0 | 0 | 53.3 | 20.8 | 0.0 | 0.0 | +| Shades of Gray | clean | 9.73 | 9.61 | – | – | – | 81.8 | 3187 | – | 51 | – | – | – | – | 0.0 | – | 2.2 | 0.8 | +| Shades of Gray | aliased | 9.73 | 9.58 | – | – | – | 80.0 | 2429 | – | 54 | – | – | – | – | 0.0 | – | 3.0 | 0.8 | +| Shades of Gray | blurred | 9.69 | 9.39 | – | – | – | 62.0 | 1644 | – | 50 | – | – | – | – | 0.0 | – | 1.0 | 0.7 | +| Shades of Gray | noisy | 9.79 | 9.50 | – | – | – | 72.5 | 2703 | – | 50 | – | – | – | – | 0.0 | – | 2.0 | 0.2 | +| Shades of Gray | scanned | 6.87 | 6.77 | – | – | – | 49.5 | 2430 | – | 44 | – | – | – | – | 0.0 | – | 1.0 | 0.2 | | Black and White Logo | clean | 0.04 | 1.65 | 0.027 | 0.50 | 1.23 | 1.4 | 18 | 1.86 | 50 | 86 | 0.0 | – | – | – | – | 0.0 | 0.0 | | Black and White Logo | aliased | 0.39 | 20.41 | 0.176 | 1.00 | 1.34 | 1.4 | 24 | 2.47 | 53 | 98 | 0.0 | – | – | – | – | 0.0 | 0.0 | | Black and White Logo | blurred | 0.81 | 16.37 | 0.047 | 0.80 | 2.18 | 1.4 | 39 | 3.78 | 23 | 19 | 1.0 | – | – | – | – | 0.0 | 0.0 | @@ -282,88 +282,89 @@ One trace at a time on this machine (all cores); memory is the peak allocated ab | preset | image | megapixels | result | |:--|:--|--:|:--| -| Default | photo-like | 1 | 0.08 s, 29 paths, 7947 anchors, 15 MB | +| Default | photo-like | 1 | 0.05 s, 29 paths, 7947 anchors, 15 MB | | Default | four-colour badge | 1 | 0.03 s, 3 paths, 106 anchors, 15 MB | -| High Fidelity Photo | photo-like | 1 | 2.05 s, 51279 paths, 1476202 anchors, 209 MB | -| High Fidelity Photo | four-colour badge | 1 | 0.06 s, 147 paths, 1700 anchors, 111 MB | -| Low Fidelity Photo | photo-like | 1 | 0.19 s, 612 paths, 91647 anchors, 133 MB | -| Low Fidelity Photo | four-colour badge | 1 | 0.05 s, 85 paths, 548 anchors, 86 MB | -| 3 Colors | photo-like | 1 | 0.05 s, 9 paths, 5249 anchors, 22 MB | -| 3 Colors | four-colour badge | 1 | 0.04 s, 6 paths, 128 anchors, 21 MB | -| 6 Colors | photo-like | 1 | 0.05 s, 53 paths, 13454 anchors, 38 MB | -| 6 Colors | four-colour badge | 1 | 0.04 s, 7 paths, 153 anchors, 39 MB | -| 16 Colors | photo-like | 1 | 0.13 s, 398 paths, 49771 anchors, 89 MB | -| 16 Colors | four-colour badge | 1 | 0.05 s, 16 paths, 527 anchors, 89 MB | -| Shades of Gray | photo-like | 1 | 0.10 s, 447 paths, 31068 anchors, 52 MB | -| Shades of Gray | four-colour badge | 1 | 0.04 s, 36 paths, 400 anchors, 52 MB | -| Black and White Logo | photo-like | 1 | 0.06 s, 71 paths, 9067 anchors, 9 MB | +| High Fidelity Photo | photo-like | 1 | 0.42 s, 4298 paths, 260070 anchors, 143 MB | +| High Fidelity Photo | four-colour badge | 1 | 0.05 s, 7 paths, 66 anchors, 34 MB | +| Low Fidelity Photo | photo-like | 1 | 0.14 s, 245 paths, 52047 anchors, 118 MB | +| Low Fidelity Photo | four-colour badge | 1 | 0.05 s, 7 paths, 54 anchors, 34 MB | +| 3 Colors | photo-like | 1 | 0.07 s, 11 paths, 7582 anchors, 22 MB | +| 3 Colors | four-colour badge | 1 | 0.04 s, 4 paths, 79 anchors, 21 MB | +| 6 Colors | photo-like | 1 | 0.09 s, 143 paths, 20207 anchors, 38 MB | +| 6 Colors | four-colour badge | 1 | 0.05 s, 5 paths, 89 anchors, 33 MB | +| 16 Colors | photo-like | 1 | 0.12 s, 403 paths, 39143 anchors, 95 MB | +| 16 Colors | four-colour badge | 1 | 0.05 s, 5 paths, 92 anchors, 33 MB | +| Shades of Gray | photo-like | 1 | 0.12 s, 447 paths, 31045 anchors, 53 MB | +| Shades of Gray | four-colour badge | 1 | 0.05 s, 36 paths, 292 anchors, 51 MB | +| Black and White Logo | photo-like | 1 | 0.05 s, 71 paths, 9067 anchors, 9 MB | | Black and White Logo | four-colour badge | 1 | 0.02 s, 1 paths, 58 anchors, 8 MB | -| Sketched Art | photo-like | 1 | 0.06 s, 5 paths, 3207 anchors, 9 MB | -| Sketched Art | four-colour badge | 1 | 0.03 s, 2 paths, 145 anchors, 8 MB | -| Silhouettes | photo-like | 1 | 0.05 s, 2 paths, 851 anchors, 11 MB | -| Silhouettes | four-colour badge | 1 | 0.03 s, 1 paths, 29 anchors, 9 MB | -| Line Art | photo-like | 1 | 0.07 s, 54 paths, 4751 anchors, 9 MB | -| Line Art | four-colour badge | 1 | 0.03 s, 1 paths, 243 anchors, 8 MB | -| Technical Drawing | photo-like | 1 | 0.07 s, 221 paths, 10646 anchors, 9 MB | +| Sketched Art | photo-like | 1 | 0.05 s, 5 paths, 3207 anchors, 9 MB | +| Sketched Art | four-colour badge | 1 | 0.02 s, 2 paths, 145 anchors, 8 MB | +| Silhouettes | photo-like | 1 | 0.04 s, 2 paths, 851 anchors, 11 MB | +| Silhouettes | four-colour badge | 1 | 0.02 s, 1 paths, 29 anchors, 9 MB | +| Line Art | photo-like | 1 | 0.05 s, 54 paths, 4751 anchors, 9 MB | +| Line Art | four-colour badge | 1 | 0.02 s, 1 paths, 243 anchors, 8 MB | +| Technical Drawing | photo-like | 1 | 0.06 s, 221 paths, 10646 anchors, 9 MB | | Technical Drawing | four-colour badge | 1 | 0.02 s, 1 paths, 58 anchors, 8 MB | -| Flat Logo | photo-like | 1 | refused after 0.10 s (not flat art), 40 MB | -| Flat Logo | four-colour badge | 1 | 0.76 s, 5 paths, 78 anchors, 146 MB | -| Default | photo-like | 4 | 0.22 s, 48 paths, 20717 anchors, 60 MB | -| Default | four-colour badge | 4 | 0.12 s, 3 paths, 148 anchors, 64 MB | -| High Fidelity Photo | photo-like | 4 | refused after 3.98 s (over 2,000,000 anchors), 818 MB | -| High Fidelity Photo | four-colour badge | 4 | 0.22 s, 225 paths, 2758 anchors, 530 MB | -| Low Fidelity Photo | photo-like | 4 | 0.52 s, 1499 paths, 196617 anchors, 493 MB | -| Low Fidelity Photo | four-colour badge | 4 | 0.19 s, 88 paths, 780 anchors, 352 MB | -| 3 Colors | photo-like | 4 | 0.20 s, 22 paths, 13682 anchors, 88 MB | -| 3 Colors | four-colour badge | 4 | 0.14 s, 5 paths, 153 anchors, 84 MB | -| 6 Colors | photo-like | 4 | 0.21 s, 100 paths, 32737 anchors, 152 MB | -| 6 Colors | four-colour badge | 4 | 0.15 s, 7 paths, 191 anchors, 152 MB | -| 16 Colors | photo-like | 4 | 0.40 s, 1236 paths, 130559 anchors, 393 MB | -| 16 Colors | four-colour badge | 4 | 0.22 s, 17 paths, 917 anchors, 324 MB | -| Shades of Gray | photo-like | 4 | 0.37 s, 1289 paths, 83191 anchors, 199 MB | -| Shades of Gray | four-colour badge | 4 | 0.19 s, 41 paths, 450 anchors, 197 MB | -| Black and White Logo | photo-like | 4 | 0.26 s, 208 paths, 25916 anchors, 36 MB | -| Black and White Logo | four-colour badge | 4 | 0.10 s, 1 paths, 64 anchors, 36 MB | -| Sketched Art | photo-like | 4 | 0.26 s, 9 paths, 6875 anchors, 36 MB | -| Sketched Art | four-colour badge | 4 | 0.10 s, 2 paths, 279 anchors, 36 MB | -| Silhouettes | photo-like | 4 | 0.20 s, 2 paths, 2133 anchors, 44 MB | -| Silhouettes | four-colour badge | 4 | 0.10 s, 1 paths, 32 anchors, 36 MB | -| Line Art | photo-like | 4 | 0.23 s, 127 paths, 12595 anchors, 36 MB | +| Flat Logo | photo-like | 1 | refused after 0.09 s (not flat art), 40 MB | +| Flat Logo | four-colour badge | 1 | 0.65 s, 5 paths, 78 anchors, 146 MB | +| Default | photo-like | 4 | 0.21 s, 48 paths, 20717 anchors, 60 MB | +| Default | four-colour badge | 4 | 0.10 s, 3 paths, 148 anchors, 64 MB | +| High Fidelity Photo | photo-like | 4 | 1.75 s, 27583 paths, 1089750 anchors, 784 MB | +| High Fidelity Photo | four-colour badge | 4 | 0.22 s, 7 paths, 93 anchors, 126 MB | +| Low Fidelity Photo | photo-like | 4 | 0.46 s, 764 paths, 138552 anchors, 469 MB | +| Low Fidelity Photo | four-colour badge | 4 | 0.22 s, 7 paths, 74 anchors, 126 MB | +| 3 Colors | photo-like | 4 | 0.27 s, 22 paths, 16576 anchors, 88 MB | +| 3 Colors | four-colour badge | 4 | 0.17 s, 4 paths, 122 anchors, 84 MB | +| 6 Colors | photo-like | 4 | 0.30 s, 358 paths, 54088 anchors, 153 MB | +| 6 Colors | four-colour badge | 4 | 0.18 s, 5 paths, 116 anchors, 128 MB | +| 16 Colors | photo-like | 4 | 0.41 s, 1475 paths, 113487 anchors, 393 MB | +| 16 Colors | four-colour badge | 4 | 0.17 s, 5 paths, 124 anchors, 128 MB | +| Shades of Gray | photo-like | 4 | 0.38 s, 1289 paths, 83191 anchors, 212 MB | +| Shades of Gray | four-colour badge | 4 | 0.25 s, 37 paths, 428 anchors, 197 MB | +| Black and White Logo | photo-like | 4 | 0.22 s, 208 paths, 25916 anchors, 36 MB | +| Black and White Logo | four-colour badge | 4 | 0.09 s, 1 paths, 64 anchors, 36 MB | +| Sketched Art | photo-like | 4 | 0.22 s, 9 paths, 6875 anchors, 36 MB | +| Sketched Art | four-colour badge | 4 | 0.09 s, 2 paths, 279 anchors, 36 MB | +| Silhouettes | photo-like | 4 | 0.18 s, 2 paths, 2133 anchors, 44 MB | +| Silhouettes | four-colour badge | 4 | 0.09 s, 1 paths, 32 anchors, 36 MB | +| Line Art | photo-like | 4 | 0.21 s, 127 paths, 12595 anchors, 36 MB | | Line Art | four-colour badge | 4 | 0.09 s, 1 paths, 465 anchors, 36 MB | | Technical Drawing | photo-like | 4 | 0.23 s, 931 paths, 33946 anchors, 36 MB | -| Technical Drawing | four-colour badge | 4 | 0.10 s, 1 paths, 64 anchors, 36 MB | -| Flat Logo | photo-like | 4 | refused after 0.12 s (not flat art), 40 MB | -| Flat Logo | four-colour badge | 4 | 0.73 s, 5 paths, 69 anchors, 146 MB | -| Default | photo-like | 16 | 1.19 s, 127 paths, 46199 anchors, 256 MB | -| Default | four-colour badge | 16 | 0.50 s, 3 paths, 284 anchors, 240 MB | -| High Fidelity Photo | photo-like | 16 | refused after 8.58 s (over 2,000,000 anchors), 3320 MB | -| High Fidelity Photo | four-colour badge | 16 | 1.11 s, 438 paths, 4419 anchors, 2404 MB | -| Low Fidelity Photo | photo-like | 16 | 2.12 s, 8558 paths, 699119 anchors, 1910 MB | -| Low Fidelity Photo | four-colour badge | 16 | 1.18 s, 105 paths, 1089 anchors, 1556 MB | -| 3 Colors | photo-like | 16 | 1.08 s, 87 paths, 32157 anchors, 352 MB | -| 3 Colors | four-colour badge | 16 | 0.70 s, 6 paths, 227 anchors, 336 MB | -| 6 Colors | photo-like | 16 | 0.98 s, 476 paths, 86962 anchors, 608 MB | -| 6 Colors | four-colour badge | 16 | 0.68 s, 11 paths, 290 anchors, 600 MB | -| 16 Colors | photo-like | 16 | 1.55 s, 7432 paths, 407078 anchors, 1574 MB | -| 16 Colors | four-colour badge | 16 | 0.86 s, 23 paths, 1707 anchors, 1348 MB | -| Shades of Gray | photo-like | 16 | 1.24 s, 5996 paths, 256732 anchors, 789 MB | -| Shades of Gray | four-colour badge | 16 | 1.03 s, 41 paths, 681 anchors, 684 MB | -| Black and White Logo | photo-like | 16 | 1.20 s, 1128 paths, 77629 anchors, 144 MB | -| Black and White Logo | four-colour badge | 16 | 0.46 s, 1 paths, 103 anchors, 128 MB | +| Technical Drawing | four-colour badge | 4 | 0.09 s, 1 paths, 64 anchors, 36 MB | +| Flat Logo | photo-like | 4 | refused after 0.10 s (not flat art), 40 MB | +| Flat Logo | four-colour badge | 4 | 0.65 s, 5 paths, 69 anchors, 146 MB | +| Default | photo-like | 16 | 1.13 s, 127 paths, 46199 anchors, 256 MB | +| Default | four-colour badge | 16 | 0.51 s, 3 paths, 284 anchors, 240 MB | +| High Fidelity Photo | photo-like | 16 | refused after 5.01 s (over 2,000,000 anchors), 3055 MB | +| High Fidelity Photo | four-colour badge | 16 | 1.00 s, 7 paths, 100 anchors, 504 MB | +| Low Fidelity Photo | photo-like | 16 | 1.60 s, 3759 paths, 363242 anchors, 1757 MB | +| Low Fidelity Photo | four-colour badge | 16 | 1.01 s, 7 paths, 79 anchors, 504 MB | +| 3 Colors | photo-like | 16 | 1.30 s, 118 paths, 41993 anchors, 352 MB | +| 3 Colors | four-colour badge | 16 | 0.77 s, 4 paths, 179 anchors, 336 MB | +| 6 Colors | photo-like | 16 | 1.34 s, 1677 paths, 155283 anchors, 601 MB | +| 6 Colors | four-colour badge | 16 | 0.79 s, 5 paths, 118 anchors, 500 MB | +| 16 Colors | photo-like | 16 | 1.73 s, 8717 paths, 394936 anchors, 1571 MB | +| 16 Colors | four-colour badge | 16 | 0.82 s, 5 paths, 120 anchors, 512 MB | +| Shades of Gray | photo-like | 16 | 1.50 s, 5996 paths, 256732 anchors, 789 MB | +| Shades of Gray | four-colour badge | 16 | 1.26 s, 38 paths, 593 anchors, 692 MB | +| Black and White Logo | photo-like | 16 | 1.16 s, 1128 paths, 77629 anchors, 144 MB | +| Black and White Logo | four-colour badge | 16 | 0.42 s, 1 paths, 103 anchors, 128 MB | | Sketched Art | photo-like | 16 | 1.13 s, 15 paths, 17817 anchors, 144 MB | -| Sketched Art | four-colour badge | 16 | 0.47 s, 2 paths, 478 anchors, 128 MB | -| Silhouettes | photo-like | 16 | 1.36 s, 3 paths, 4891 anchors, 176 MB | -| Silhouettes | four-colour badge | 16 | 0.53 s, 1 paths, 44 anchors, 144 MB | -| Line Art | photo-like | 16 | 1.20 s, 713 paths, 64481 anchors, 144 MB | -| Line Art | four-colour badge | 16 | 0.49 s, 1 paths, 786 anchors, 128 MB | -| Technical Drawing | photo-like | 16 | 1.45 s, 4578 paths, 114292 anchors, 144 MB | -| Technical Drawing | four-colour badge | 16 | 0.48 s, 1 paths, 103 anchors, 128 MB | -| Flat Logo | photo-like | 16 | refused after 0.13 s (not flat art), 40 MB | -| Flat Logo | four-colour badge | 16 | 1.09 s, 5 paths, 67 anchors, 146 MB | +| Sketched Art | four-colour badge | 16 | 0.45 s, 2 paths, 478 anchors, 128 MB | +| Silhouettes | photo-like | 16 | 1.37 s, 3 paths, 4891 anchors, 176 MB | +| Silhouettes | four-colour badge | 16 | 0.54 s, 1 paths, 44 anchors, 144 MB | +| Line Art | photo-like | 16 | 1.22 s, 713 paths, 64481 anchors, 144 MB | +| Line Art | four-colour badge | 16 | 0.45 s, 1 paths, 786 anchors, 128 MB | +| Technical Drawing | photo-like | 16 | 1.37 s, 4578 paths, 114292 anchors, 144 MB | +| Technical Drawing | four-colour badge | 16 | 0.43 s, 1 paths, 103 anchors, 128 MB | +| Flat Logo | photo-like | 16 | refused after 0.11 s (not flat art), 40 MB | +| Flat Logo | four-colour badge | 16 | 0.67 s, 5 paths, 67 anchors, 146 MB | ## Revision history | date | change | |:--|:--| +| 2026-10-13 | The palette: made of the picture's own colours, noise-made colours merged, blend pixels settled, cuts where colours differ most, colours compared in Oklab | | 2026-10-13 | Sub-pixel edges and the outline fitter in Black and White, Grayscale and Color; the harness gains an aliased variant and an "outside px" column, so the baseline was measured again with it (same tracer, main at `baf9693f`) | | 2026-10-12 | The harness and the baseline: every preset measured before any change to the tracer |