Merge pull request #4216 from dotCooCoo/fix/trace-rare-budget-blends

Image Trace: a cut-edge blend past the rare-color budget traces as its kept color
This commit is contained in:
BFlat
2026-10-26 10:43:55 +07:00
committed by GitHub
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