mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-24 09:46:59 -06:00
Merge pull request #4008 from dotCooCoo/fix/trace-two-pixel-bar
Image Trace: Flat Logo finds a bar two pixels wide in its own color
This commit is contained in:
@@ -336,24 +336,71 @@ 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
|
||||
/// 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.
|
||||
fn bar_ground(at: &impl Fn(isize, isize) -> Option<[f64; 3]>, x: isize, y: isize, c: [f64; 3]) -> Option<[f64; 3]> {
|
||||
let bar = |q: Option<[f64; 3]>| q.is_some_and(|q| dist2(q, c) <= 12.0);
|
||||
for ((ax, ay), (bx, by)) in [((1, 0), (0, 1)), ((0, 1), (1, 0))] {
|
||||
for s in [-1, 1] {
|
||||
// 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))
|
||||
});
|
||||
if flat {
|
||||
return Some(g);
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// Colours the current palette cannot explain. `residual` is each pixel's error after the best blend
|
||||
/// of the known classes (see `unmix`); a solid run of pixels (a pixel and its four neighbours agree)
|
||||
/// with a large error is a colour that is missing, such as the small accent that was too few
|
||||
/// pixels to be found at first. Groups of at least three such pixels at least 25 away from every
|
||||
/// known colour are returned, most common first.
|
||||
/// pixels to be found at first. A flat bar two pixels wide on a ground within 25 of a known color
|
||||
/// ([`bar_ground`]) has no such run: its pixels count when their error is large, and also when their
|
||||
/// color lies less than two thirds as far from that known color as from every other known color (a
|
||||
/// gray bar on white beside black art), where the bar would otherwise be traced as the ground.
|
||||
/// Groups of at least three such pixels at least 25 away from every known color are returned, most
|
||||
/// common first.
|
||||
pub(crate) fn unexplained_colours(pix: &[Px], w: usize, h: usize, residual: &[f32], known: &[[u8; 3]]) -> Vec<[u8; 3]> {
|
||||
let mut pal: Vec<[f64; 3]> = known.iter().map(|c| c.map(f64::from)).collect();
|
||||
pal.push([255.0, 255.0, 255.0]);
|
||||
let at = |x: usize, y: usize| pix.get(y * w + x).filter(|p| p[3] > 0.98).map(rgb255);
|
||||
let at_signed = |x: isize, y: isize| {
|
||||
let (x, y) = (usize::try_from(x).ok()?, usize::try_from(y).ok()?);
|
||||
if x < w && y < h { at(x, y) } else { None }
|
||||
};
|
||||
let mut groups: Vec<([f64; 3], usize)> = Vec::new();
|
||||
for y in 1..h.saturating_sub(1) {
|
||||
for x in 1..w.saturating_sub(1) {
|
||||
if residual.get(y * w + x).copied().unwrap_or(0.0) < 0.0064 {
|
||||
let large = residual.get(y * w + x).copied().unwrap_or(0.0) >= 0.0064;
|
||||
let Some(c) = at(x, y) else { continue };
|
||||
if pal.iter().any(|q| dist2(*q, c) < 625.0) {
|
||||
continue;
|
||||
}
|
||||
let Some(c) = at(x, y) else { continue };
|
||||
let uniform = [(x - 1, y), (x + 1, y), (x, y - 1), (x, y + 1)].iter().all(|&(xx, yy)| at(xx, yy).is_some_and(|q| dist2(q, c) < 100.0));
|
||||
if !uniform || pal.iter().any(|q| dist2(*q, c) < 625.0) {
|
||||
let missing = match (uniform, isize::try_from(x), isize::try_from(y)) {
|
||||
(true, ..) => large,
|
||||
(false, Ok(sx), Ok(sy)) => bar_ground(&at_signed, sx, sy, c).is_some_and(|g| {
|
||||
// The known color of the ground, and every other known color more than 1.5 times as far from the bar.
|
||||
let Some(ground) = pal.iter().copied().find(|q| dist2(*q, g) < 625.0) else { return false };
|
||||
large || pal.iter().filter(|q| dist2(**q, ground) >= 625.0).all(|q| 2.25 * dist2(c, ground) < dist2(c, *q))
|
||||
}),
|
||||
_ => false,
|
||||
};
|
||||
if !missing {
|
||||
continue;
|
||||
}
|
||||
match groups.iter_mut().find(|(m, _)| dist2(*m, c) < 400.0) {
|
||||
|
||||
@@ -556,6 +556,45 @@ fn sparse_marks_on_paper_are_not_promoted_to_colours() {
|
||||
}
|
||||
}
|
||||
|
||||
/// Bars two pixels wide (whole pixels of one color) on white beside a black disc, with the colors
|
||||
/// 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.
|
||||
#[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:?}");
|
||||
}
|
||||
assert_eq!(colour_at(Point::new(25.0, 28.0)), Some(INK), "bar {bar:?}: the disc; {paths:?}");
|
||||
}
|
||||
}
|
||||
|
||||
/// Three rows of thin anti-aliased "lettering" (rings, slants and bars 1.6 px wide) in red, blue
|
||||
/// and green on white. The lighter shades along the strokes' anti-aliased edges are blends of an
|
||||
/// ink with the paper, not colours of their own: the palette is the three inks and white.
|
||||
|
||||
Reference in New Issue
Block a user