Image Trace: a cut-edge blend past the rare-color budget traces as its kept color

The loop that keeps rare colors stopped at the first color past the
budget of six, so a cut-edge blend listed after it was not recorded as
tracing as its rare color, even when that rare color was kept, and its
pixels went to the nearest color instead. The loop now goes on past the
budget and only stops keeping new rare colors. It moves into
`kept_rare`, which a new test calls with seven rare colors and a blend
of the first.

The test of an outline over a cut edge in the color of other art also
checks that the dark paths reach the top and the bottom of the disc,
which no bar comes near: before, the bars alone met its checks.
This commit is contained in:
dotCooCoo
2026-10-26 10:41:54 +07:00
committed by bflatastic
parent 8f95e0165f
commit 5d2a9099f1
2 changed files with 60 additions and 16 deletions
+28 -15
View File
@@ -1271,21 +1271,7 @@ fn rare_colours(img: &Raster, own: &[bool], choose: &[bool], hk: &HistKey, cente
*o = best.and_then(|b| b.2);
}
}
let mut out: Vec<[f64; 3]> = vec![];
let mut traced_as = vec![];
for (((_, _, c, _), o), &from) in chosen.iter().zip(&onto).zip(&other_of) {
if let Some(o) = o {
traced_as.push((*c, *o, from));
continue;
}
if out.len() >= RARE_BUDGET {
break;
}
let lab = oklab(*c);
if out.iter().all(|o| dist(oklab(*o), lab) >= RARE_APART) {
out.push(*c);
}
}
let (out, traced_as) = kept_rare(chosen.iter().zip(&onto).zip(&other_of).map(|(((_, _, c, _), o), &from)| (*c, *o, from)));
// The index in `centers` followed by `out` of a palette color or a rare color, or of the kept
// rare color one that is not kept traces as.
let index = |from: usize| -> Option<usize> {
@@ -1309,6 +1295,33 @@ fn rare_colours(img: &Raster, own: &[bool], choose: &[bool], hk: &HistKey, cente
(out, traced_as)
}
/// Of `colors` (a rare color, the rare color it traces as, and where the other color of its pair
/// comes from), largest region first: the rare colors kept, at most [`RARE_BUDGET`] and each at
/// least [`RARE_APART`] from the ones before it, and every color that traces as another, past the
/// budget too.
pub(crate) fn kept_rare(colors: impl IntoIterator<Item = ([f64; 3], Option<[f64; 3]>, Option<usize>)>) -> (Vec<[f64; 3]>, Vec<TracedAsColor>) {
let dist = |a: [f64; 3], b: [f64; 3]| (0..3).map(|k| (a[k] - b[k]).powi(2)).sum::<f64>().sqrt();
let mut out: Vec<[f64; 3]> = vec![];
let mut traced_as = vec![];
for (c, o, from) in colors {
if let Some(o) = o {
traced_as.push((c, o, from));
continue;
}
if out.len() >= RARE_BUDGET {
continue;
}
let lab = oklab(c);
if out.iter().all(|o| dist(oklab(*o), lab) >= RARE_APART) {
out.push(c);
}
}
(out, traced_as)
}
/// A color, the rare color it traces as, and where the other color of its pair comes from.
pub(crate) type TracedAsColor = ([f64; 3], [f64; 3], Option<usize>);
/// Whether `c` (sRGB) lies between two of `centers`, as anti-aliasing mixes them: within 12 levels
/// of the line between them, and at least an eighth of the way from each.
fn is_mix(c: [f64; 3], centers: &[[f64; 3]]) -> bool {
+32 -1
View File
@@ -2461,13 +2461,24 @@ fn automatic_traces_an_outline_over_a_cut_edge_in_the_color_of_other_art() {
.iter()
.any(|&(x0, y0, x1, y1)| (x0..x1).contains(&x) && (y0..y1).contains(&y))
};
// The dark paths reach the top and the bottom of the disc, which no bar comes near: the outline
// is traced, not only the bars.
use kurbo::Shape;
let target = quantize::tests_oklab([110, 80, 40]);
let dark_reaches = |res: &TraceResult, edge: &dyn Fn(kurbo::Rect) -> bool| {
res.paths.iter().any(|p| {
let o = quantize::tests_oklab(p.color);
(0..3).map(|k| (o[k] - target[k]).powi(2)).sum::<f64>().sqrt() <= 3.0 && edge(p.path.to_bezpath().bounding_box())
})
};
let mut failed = vec![];
for width in [1.5, 2.0, 2.5] {
let outline = move |x: f64, y: f64| (r(x, y) - 140.0).abs() < width / 2.0;
let img = cut_out_layers(&[(&disc, light, 1.0), (&block, blue, 1.0), (&bars, dark, 1.0), (&outline, dark, 0.8)]);
for detail in [85.0, 50.0] {
let res = trace(&img, &TraceParams { color_detail: detail, ..preset("Auto Color").unwrap() });
if distinct_colours(&res) != 3 || !has_colour(&res, [110, 80, 40], 3.0) || res.paths.len() > 24 {
let ring = dark_reaches(&res, &|b| b.y0 < 15.0) && dark_reaches(&res, &|b| b.y1 > 285.0);
if distinct_colours(&res) != 3 || !has_colour(&res, [110, 80, 40], 3.0) || !ring || res.paths.len() > 24 {
failed.push(format!("outline {width} px, color detail {detail}: {} paths, {:?}", res.paths.len(), res.palette));
}
}
@@ -2546,6 +2557,26 @@ fn automatic_keeps_an_opaque_line_between_two_kept_colors() {
assert!(has_colour(&res, [204, 190, 155], 3.0), "{:?}", res.palette);
}
#[test]
fn a_cut_edge_blend_past_the_rare_budget_still_traces_as_its_kept_color() {
// Seven distinct rare colors, largest first, fill the budget of six, and then the blend of the
// first one along a cut edge: the blend still traces as that kept color.
let rare: [[f64; 3]; 7] = [
[200.0, 30.0, 40.0],
[30.0, 160.0, 60.0],
[240.0, 200.0, 20.0],
[120.0, 40.0, 160.0],
[20.0, 200.0, 210.0],
[250.0, 120.0, 180.0],
[90.0, 60.0, 20.0],
];
let blend = [220.0, 120.0, 120.0];
let colors = rare.iter().map(|&c| (c, None, None)).chain([(blend, Some(rare[0]), None)]);
let (out, traced_as) = quantize::kept_rare(colors);
assert_eq!(out, rare[..6]);
assert_eq!(traced_as, [(blend, rare[0], None)]);
}
#[test]
fn noise_keeps_a_kept_rare_colours_regions_but_not_its_specks() {
// Labels: 0 background, 1 a rare colour kept on purpose (four islands of 4, 6, 9 and 2