diff --git a/crates/trace/src/logo/palette.rs b/crates/trace/src/logo/palette.rs index 157889372..f5700f66a 100644 --- a/crates/trace/src/logo/palette.rs +++ b/crates/trace/src/logo/palette.rs @@ -338,8 +338,9 @@ pub(crate) fn fold_tones(pix: &[Px], mut palette: Vec<[u8; 3]>) -> Vec<[u8; 3]> /// The color of the ground a flat bar two pixels wide lies on, if the pixel at (`x`, `y`) is one of /// the bar's. Across the bar (down or across the image) the pixel and one neighbor are the bar, and -/// the pixels on either side of the two are the ground; along the bar, the pixel's neighbors on -/// both sides are bar and ground in the same way. Every bar pixel lies within √12, about 3.5 (in +/// the pixels on either side of the two are the ground; along the bar, three pixels in a row that +/// include the pixel (at either end or in the middle) are bar and ground in the same way, so all +/// six pixels of a bar three pixels long count. Every bar pixel lies within √12, about 3.5 (in /// RGB), of the pixel's color; the ground's pixels lie within 10 (in RGB) of the first one and at /// least 25 from the pixel's color. `at` is the opaque color at a pixel, `None` outside the image or where it is /// not opaque. @@ -350,12 +351,15 @@ fn bar_ground(at: &impl Fn(isize, isize) -> Option<[f64; 3]>, x: isize, y: isize // Across the bar: ground, the pixel, its partner, ground. let Some(g) = at(x + 2 * s * bx, y + 2 * s * by) else { continue }; let ground = |q: Option<[f64; 3]>| q.is_some_and(|q| dist2(q, g) < 100.0 && dist2(q, c) >= 625.0); - let flat = (-1..=1).all(|k| { - let (px, py) = (x + k * ax, y + k * ay); - bar(at(px, py)) - && bar(at(px + s * bx, py + s * by)) - && ground(at(px - s * bx, py - s * by)) - && ground(at(px + 2 * s * bx, py + 2 * s * by)) + // Along the bar: three pixels in a row, the pixel at either end of them or in the middle. + let flat = (-1..=1).any(|o| { + (o - 1..=o + 1).all(|k| { + let (px, py) = (x + k * ax, y + k * ay); + bar(at(px, py)) + && bar(at(px + s * bx, py + s * by)) + && ground(at(px - s * bx, py - s * by)) + && ground(at(px + 2 * s * bx, py + 2 * s * by)) + }) }); if flat { return Some(g); @@ -500,6 +504,31 @@ mod tests { assert_eq!(fold_fringes(groups), vec![[30, 90, 200], [210, 40, 50], [240, 150, 40]]); } + #[test] + fn a_bar_two_pixels_wide_and_three_long_is_an_unexplained_color() { + // A red bar two pixels wide on white beside a black block, three and four pixels long, + // lying across and down the image. Only the bar three pixels long needs its end pixels + // counted to reach the three pixels a color needs. + const RED: [u8; 3] = [210, 40, 50]; + for long in [3, 4] { + for across in [true, false] { + let (w, h) = (16, 12); + let pix = blocks(w, h, |x, y| { + let (along, side) = if across { (x, y) } else { (y, x) }; + if x < 3 { + [0, 0, 0, 255] + } else if (6..6 + long).contains(&along) && (5..7).contains(&side) { + [RED[0], RED[1], RED[2], 255] + } else { + [255, 255, 255, 255] + } + }); + let residual = vec![1.0; w * h]; + assert_eq!(unexplained_colours(&pix, w, h, &residual, &[[0, 0, 0]]), vec![RED], "{long} long, across: {across}"); + } + } + } + #[test] fn nothing_to_find_in_empty_or_blank_images() { assert!(auto_palette(&[], 0, 0, 6).is_empty()); diff --git a/crates/trace/src/logo/tests.rs b/crates/trace/src/logo/tests.rs index 95b1916aa..1631ff164 100644 --- a/crates/trace/src/logo/tests.rs +++ b/crates/trace/src/logo/tests.rs @@ -560,38 +560,53 @@ fn sparse_marks_on_paper_are_not_promoted_to_colours() { /// found: no pixel of a bar has a 3 × 3 neighborhood of its own color, and a gray bar is a blend of /// black and white. Light grays, a red and a pale yellow are found and traced in their own color. A /// gray bar darker than about `#9B9B9B` stays black: a black stroke 1 to 1.6 px wide on the pixel -/// grid gives the same pixels. +/// grid gives the same pixels. A single bar two pixels wide and three pixels long, down or across +/// the image, is found in its color too. #[test] fn a_bar_two_pixels_wide_beside_black_art_is_found_in_its_color() { const INK: [u8; 3] = [4, 4, 3]; - for (bar, want) in [ - ([170u8, 170, 170], None), - ([200, 200, 200], None), - ([230, 230, 230], None), - ([210, 40, 50], None), - ([250, 230, 150], None), - ([128, 128, 128], Some(INK)), - ([51, 51, 51], Some(INK)), - ] { - let img = Raster::from_fn(120, 60, |x, y| { - let (fx, fy) = (f64::from(x) + 0.5, f64::from(y) + 0.5); - let c = if (fx - 25.0).hypot(fy - 28.0) <= 18.0 { - INK - } else if ((50..52).contains(&x) && (8..50).contains(&y)) || ((60..110).contains(&x) && (30..32).contains(&y)) { - bar - } else { - [255, 255, 255] - }; - [c[0], c[1], c[2], 255] - }); - let res = trace(&img, &crate::preset("Flat Logo").unwrap()); - let colour_at = |p: Point| res.paths.iter().rev().find(|t| t.path.to_bezpath().winding(p) != 0).map(|t| t.color); - let paths: Vec<_> = res.paths.iter().map(|t| (t.color, t.pixels)).collect(); - let want = want.unwrap_or(bar); - for at in [Point::new(51.0, 20.0), Point::new(51.0, 40.0), Point::new(70.0, 31.0), Point::new(100.0, 31.0)] { - assert_eq!(colour_at(at), Some(want), "bar {bar:?}: {at:?}; {paths:?}"); + let layouts = [ + ( + vec![(50..52, 8..50), (60..110, 30..32)], + vec![Point::new(51.0, 20.0), Point::new(51.0, 40.0), Point::new(70.0, 31.0), Point::new(100.0, 31.0)], + ), + (vec![(50..52, 20..23)], vec![Point::new(51.0, 21.5)]), + (vec![(70..73, 30..32)], vec![Point::new(71.5, 31.0)]), + ]; + for (bars, points) in &layouts { + for (bar, want) in [ + ([170u8, 170, 170], None), + ([200, 200, 200], None), + ([230, 230, 230], None), + ([210, 40, 50], None), + ([250, 230, 150], None), + ([128, 128, 128], Some(INK)), + ([51, 51, 51], Some(INK)), + ] { + if bars.len() == 1 && bar == [128, 128, 128] { + // Half ink and half paper, a single bar three pixels long is traced as paper. + continue; + } + let img = Raster::from_fn(120, 60, |x, y| { + let (fx, fy) = (f64::from(x) + 0.5, f64::from(y) + 0.5); + let c = if (fx - 25.0).hypot(fy - 28.0) <= 18.0 { + INK + } else if bars.iter().any(|(xs, ys)| xs.contains(&x) && ys.contains(&y)) { + bar + } else { + [255, 255, 255] + }; + [c[0], c[1], c[2], 255] + }); + let res = trace(&img, &crate::preset("Flat Logo").unwrap()); + let colour_at = |p: Point| res.paths.iter().rev().find(|t| t.path.to_bezpath().winding(p) != 0).map(|t| t.color); + let paths: Vec<_> = res.paths.iter().map(|t| (t.color, t.pixels)).collect(); + let want = want.unwrap_or(bar); + for &at in points { + assert_eq!(colour_at(at), Some(want), "bars {bars:?}, bar {bar:?}: {at:?}; {paths:?}"); + } + assert_eq!(colour_at(Point::new(25.0, 28.0)), Some(INK), "bars {bars:?}, bar {bar:?}: the disc; {paths:?}"); } - assert_eq!(colour_at(Point::new(25.0, 28.0)), Some(INK), "bar {bar:?}: the disc; {paths:?}"); } }