Image Trace: Flat Logo finds a bar two pixels wide and three pixels long in its own color

The check for colors the palette cannot explain counted a pixel of a
bar two pixels wide only when its neighbors on both sides along the
bar were bar pixels. In a bar three pixels long only the two pixels of
its middle row passed, so its color had fewer than the three pixels a
color needs and was not added to the palette. A pixel now counts when
it lies at either end or in the middle of three such pixels in a row,
so all six pixels of a 2 × 3 bar count.
This commit is contained in:
dotCooCoo
2026-10-24 13:00:06 -07:00
parent 8bf090b253
commit 678b22ec73
2 changed files with 80 additions and 36 deletions
+37 -8
View File
@@ -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());
+43 -28
View File
@@ -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:?}");
}
}