mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-25 10:17:02 -06:00
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:
@@ -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());
|
||||
|
||||
@@ -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:?}");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user