mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-24 17:56:50 -06:00
Merge pull request #2264 from andeecollard/fix/trace-grayscale-gradients
Image Trace: grayscale gradients use grey stops
This commit is contained in:
+14
-2
@@ -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()];
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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, ¶ms);
|
||||
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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user