Merge pull request #2264 from andeecollard/fix/trace-grayscale-gradients

Image Trace: grayscale gradients use grey stops
This commit is contained in:
BFlat
2026-10-16 04:30:29 +07:00
committed by GitHub
3 changed files with 49 additions and 3 deletions
+14 -2
View File
@@ -291,6 +291,7 @@ pub struct TraceParams {
pub auto_grouping: bool,
/// Gradients (Grayscale and Color): an area whose colour changes evenly along one direction
/// is one path filled with a linear gradient ([`TracedPath::gradient`]), not bands of colour.
/// Grayscale finds them in the image's grays, and their stops are gray.
pub gradients: bool,
/// Create: trace areas as filled shapes.
pub fills: bool,
@@ -527,7 +528,17 @@ fn trace_paths(img: &Raster, params: &TraceParams, max_anchors: usize) -> Result
let smoothed = (params.mode != Mode::BlackAndWhite && quantize::continuous_tone(img)).then(|| smooth::smoothed(img));
let img = smoothed.as_ref().unwrap_or(img);
// Gradients are found first and kept out of the palette: no colour is spent on bands of them.
let found = (params.gradients && params.mode != Mode::BlackAndWhite).then(|| gradient::detect(img)).filter(|f| !f.1.is_empty());
// Grayscale finds them in the image's grays (as its flat colours are), so their stops are gray.
let grays = (params.gradients && params.mode == Mode::Grayscale).then(|| {
let mut grays = img.clone();
for p in grays.rgba.as_chunks_mut::<4>().0 {
let g = quantize::luma(*p);
p[..3].fill(g);
}
grays
});
let traced_as = grays.as_ref().unwrap_or(img);
let found = (params.gradients && params.mode != Mode::BlackAndWhite).then(|| gradient::detect(traced_as)).filter(|f| !f.1.is_empty());
let without = found.as_ref().map(|(of, _)| {
let mut rest = img.clone();
for (p, &g) in rest.rgba.as_chunks_mut::<4>().0.iter_mut().zip(of) {
@@ -542,7 +553,7 @@ fn trace_paths(img: &Raster, params: &TraceParams, max_anchors: usize) -> Result
// Each gradient is a palette entry of its own (its colour: the gradient's middle).
let mut gradients: Vec<Option<TracedGradient>> = vec![None; q.palette.len()];
if let Some((mut of, found)) = found {
gradient::settle_edges(img, &mut of, &found, &mut q.labels);
gradient::settle_edges(traced_as, &mut of, &found, &mut q.labels);
let first = q.palette.len();
for (l, &g) in q.labels.iter_mut().zip(&of) {
if g != u32::MAX {
@@ -552,6 +563,7 @@ fn trace_paths(img: &Raster, params: &TraceParams, max_anchors: usize) -> Result
q.palette.extend(found.iter().map(|g| g.color_at(g.start.midpoint(g.end)).map(|v| v.round().clamp(0.0, 255.0) as u8)));
gradients.extend(found.into_iter().map(Some));
}
drop(grays);
denoise(&mut q.labels, w, h, params.noise as usize);
// Pixel count per palette entry, and the box its pixels lie in.
let mut counts = vec![0usize; q.palette.len()];
+2 -1
View File
@@ -14,7 +14,8 @@ pub struct Quantized {
pub palette: Vec<[u8; 3]>,
}
fn luma(p: [u8; 4]) -> u8 {
/// A pixel's gray (Rec. 601 luma): what Grayscale traces it as.
pub(crate) fn luma(p: [u8; 4]) -> u8 {
((p[0] as u32 * 299 + p[1] as u32 * 587 + p[2] as u32 * 114 + 500) / 1000) as u8
}
+33
View File
@@ -1295,3 +1295,36 @@ fn stroke_caps_are_square_or_round_as_the_ink_is() {
assert!(line(true));
assert!(!line(false));
}
/// A blue-to-orange sky over flat colours, and a green-to-magenta ramp in a rectangle on them.
fn sky_and_ramp() -> Raster {
Raster::from_fn(240, 160, |x, y| {
let (fx, fy) = (f64::from(x), f64::from(y));
let mix = |a: [f64; 3], b: [f64; 3], t: f64| [0, 1, 2].map(|k| (a[k] + (b[k] - a[k]) * t).round() as u8);
if (20..100).contains(&x) && (100..150).contains(&y) {
let [r, g, b] = mix([40.0, 200.0, 60.0], [220.0, 40.0, 200.0], (fx - 20.0) / 80.0);
[r, g, b, 255]
} else if y >= 90 {
if x >= 140 { [200, 30, 40, 255] } else { [30, 120, 50, 255] }
} else {
let [r, g, b] = mix([30.0, 60.0, 200.0], [250.0, 140.0, 30.0], fy / 90.0);
[r, g, b, 255]
}
})
}
#[test]
fn grayscale_gradients_are_grey() {
let img = sky_and_ramp();
let params = TraceParams { mode: Mode::Grayscale, colors: 4, gradients: true, ..TraceParams::default() };
let res = trace(&img, &params);
let grey = |c: [u8; 3]| c[0] == c[1] && c[1] == c[2];
assert!(res.paths.iter().any(|p| p.gradient.is_some()), "a gradient is still found");
for p in &res.paths {
assert!(grey(p.color), "{:?}", p.color);
for stop in p.gradient.iter().flat_map(|g| &g.stops) {
assert!(grey(stop.1), "{:?}", p.gradient);
}
}
assert!(res.palette.iter().all(|&c| grey(c)), "{:?}", res.palette);
}