diff --git a/crates/trace/src/centerline/graph.rs b/crates/trace/src/centerline/graph.rs index 81f37d9d5..a358918cd 100644 --- a/crates/trace/src/centerline/graph.rs +++ b/crates/trace/src/centerline/graph.rs @@ -68,18 +68,26 @@ fn build_with_limits(g: &Grid, dist: &[f32], max_nodes: usize, max_branches: usi { return None; } + let degree: Vec = (0..n).map(|i| if g.on.get(i) == Some(&true) { g.degree(i) as u8 } else { 0 }).collect(); + let (node_of, mut graph) = build_nodes(g, dist, °ree, max_nodes)?; + let mut visited = vec![false; n]; + add_node_branches(g, dist, &node_of, &mut graph, &mut visited, max_branches)?; + add_ring_branches(g, dist, °ree, &node_of, &mut graph, &mut visited, max_branches)?; + Some(graph) +} + +fn build_nodes(g: &Grid, dist: &[f32], degree: &[u8], max_nodes: usize) -> Option<(Vec, Graph)> { let dval = |i: usize| f64::from(dist.get(i).copied().unwrap_or(0.0)).sqrt(); - let deg: Vec = (0..n).map(|i| if g.on.get(i) == Some(&true) { g.degree(i) as u8 } else { 0 }).collect(); - let mut node_of = vec![usize::MAX; n]; + let mut node_of = vec![usize::MAX; g.on.len()]; let mut graph = Graph::default(); // Ends on their own; touching junction pixels together, deepest pixel first on ties. for i in g.interior() { - let degree = *deg.get(i)?; - if g.on.get(i) != Some(&true) || degree == 2 || *node_of.get(i)? != usize::MAX { + let pixel_degree = *degree.get(i)?; + if g.on.get(i) != Some(&true) || pixel_degree == 2 || *node_of.get(i)? != usize::MAX { continue; } // Isolated pixels cannot form a stroke; leave them to the existing fill path. - if degree == 0 { + if pixel_degree == 0 { continue; } if graph.nodes.len() >= max_nodes.min(MAX_NODES) { @@ -88,40 +96,45 @@ fn build_with_limits(g: &Grid, dist: &[f32], max_nodes: usize, max_branches: usi let id = graph.nodes.len(); *node_of.get_mut(i)? = id; let mut members = vec![i]; - if degree > 2 { - let mut k = 0; - while let Some(&p) = members.get(k) { - k += 1; - for q in g.neighbours(p) { - if *node_of.get(q)? == usize::MAX && *deg.get(q)? > 2 { - *node_of.get_mut(q)? = id; - members.push(q); + if pixel_degree > 2 { + let mut cursor = 0; + while let Some(&pixel) = members.get(cursor) { + cursor += 1; + for neighbour in g.neighbours(pixel) { + if *node_of.get(neighbour)? == usize::MAX && *degree.get(neighbour)? > 2 { + *node_of.get_mut(neighbour)? = id; + members.push(neighbour); } } } } let best = members.into_iter().max_by(|&a, &b| dval(a).total_cmp(&dval(b)).then(b.cmp(&a)))?; - graph.nodes.push(Node { pos: g.centre(best), r: (dval(best) - 0.5).max(0.5), junction: degree > 2 }); + graph.nodes.push(Node { pos: g.centre(best), r: (dval(best) - 0.5).max(0.5), junction: pixel_degree > 2 }); } + Some((node_of, graph)) +} + +fn add_node_branches(g: &Grid, dist: &[f32], node_of: &[usize], graph: &mut Graph, visited: &mut [bool], max_branches: usize) -> Option<()> { + let dval = |i: usize| f64::from(dist.get(i).copied().unwrap_or(0.0)).sqrt(); let mut seen_edge = BTreeSet::new(); - let mut visited = vec![false; n]; - for p in g.interior() { - let a = *node_of.get(p)?; - if a == usize::MAX { + let n = visited.len(); + for pixel in g.interior() { + let node = *node_of.get(pixel)?; + if node == usize::MAX { continue; } - for q in g.neighbours(p) { - let b = *node_of.get(q)?; - if b == a { + for neighbour in g.neighbours(pixel) { + let next_node = *node_of.get(neighbour)?; + if next_node == node { continue; } - if b != usize::MAX { - if seen_edge.insert((p.min(q), p.max(q))) { + if next_node != usize::MAX { + if seen_edge.insert((pixel.min(neighbour), pixel.max(neighbour))) { graph.push_branch( Branch { - pts: vec![graph.nodes.get(a)?.pos, graph.nodes.get(b)?.pos], - d: vec![dval(p), dval(q)], - ends: [Some(a), Some(b)], + pts: vec![graph.nodes.get(node)?.pos, graph.nodes.get(next_node)?.pos], + d: vec![dval(pixel), dval(neighbour)], + ends: [Some(node), Some(next_node)], closed: false, }, max_branches, @@ -129,66 +142,80 @@ fn build_with_limits(g: &Grid, dist: &[f32], max_nodes: usize, max_branches: usi } continue; } - if *visited.get(q)? { + if *visited.get(neighbour)? { continue; } - let mut px = vec![p]; - let (mut prev, mut cur) = (p, q); + let mut pixels = vec![pixel]; + let (mut previous, mut current) = (pixel, neighbour); let mut guard = n; - while *node_of.get(cur)? == usize::MAX && guard > 0 { + while *node_of.get(current)? == usize::MAX && guard > 0 { guard -= 1; - *visited.get_mut(cur)? = true; - px.push(cur); - let Some(next) = g.neighbours(cur).find(|&m| m != prev && (visited.get(m) == Some(&false) || node_of.get(m) != Some(&usize::MAX))) - else { + *visited.get_mut(current)? = true; + pixels.push(current); + let Some(next) = g.neighbours(current).find(|&candidate| { + candidate != previous && (visited.get(candidate) == Some(&false) || node_of.get(candidate) != Some(&usize::MAX)) + }) else { break; }; - (prev, cur) = (cur, next); + (previous, current) = (current, next); } if guard == 0 { return None; } - let end = (*node_of.get(cur)? != usize::MAX).then_some(*node_of.get(cur)?); + let end = (*node_of.get(current)? != usize::MAX).then_some(*node_of.get(current)?); if end.is_some() { - px.push(cur); + pixels.push(current); } - let mut pts: Vec = px.iter().map(|&i| g.centre(i)).collect(); - let d = px.into_iter().map(dval).collect(); - *pts.first_mut()? = graph.nodes.get(a)?.pos; - if let Some(e) = end { - *pts.last_mut()? = graph.nodes.get(e)?.pos; + let mut points: Vec = pixels.iter().map(|&i| g.centre(i)).collect(); + let distances = pixels.into_iter().map(dval).collect(); + *points.first_mut()? = graph.nodes.get(node)?.pos; + if let Some(end) = end { + *points.last_mut()? = graph.nodes.get(end)?.pos; } - graph.push_branch(Branch { pts, d, ends: [Some(a), end], closed: false }, max_branches)?; + graph.push_branch(Branch { pts: points, d: distances, ends: [Some(node), end], closed: false }, max_branches)?; } } - // Rings never reached from a node. - for s in g.interior() { - if g.on.get(s) != Some(&true) || deg.get(s) != Some(&2) || *visited.get(s)? || *node_of.get(s)? != usize::MAX { + Some(()) +} + +fn add_ring_branches( + g: &Grid, + dist: &[f32], + degree: &[u8], + node_of: &[usize], + graph: &mut Graph, + visited: &mut [bool], + max_branches: usize, +) -> Option<()> { + let dval = |i: usize| f64::from(dist.get(i).copied().unwrap_or(0.0)).sqrt(); + let n = visited.len(); + for start in g.interior() { + if g.on.get(start) != Some(&true) || degree.get(start) != Some(&2) || *visited.get(start)? || *node_of.get(start)? != usize::MAX { continue; } - let mut px = vec![]; - let (mut prev, mut cur) = (usize::MAX, s); + let mut pixels = vec![]; + let (mut previous, mut current) = (usize::MAX, start); let mut closed = false; let mut guard = n; while guard > 0 { guard -= 1; - *visited.get_mut(cur)? = true; - px.push(cur); - let Some(next) = g.neighbours(cur).find(|&m| m != prev && visited.get(m) == Some(&false)) else { - closed = g.neighbours(cur).any(|m| m == s) && px.len() > 2; + *visited.get_mut(current)? = true; + pixels.push(current); + let Some(next) = g.neighbours(current).find(|&candidate| candidate != previous && visited.get(candidate) == Some(&false)) else { + closed = g.neighbours(current).any(|neighbour| neighbour == start) && pixels.len() > 2; break; }; - (prev, cur) = (cur, next); + (previous, current) = (current, next); } if guard == 0 { return None; } graph.push_branch( - Branch { pts: px.iter().map(|&i| g.centre(i)).collect(), d: px.into_iter().map(dval).collect(), ends: [None, None], closed }, + Branch { pts: pixels.iter().map(|&i| g.centre(i)).collect(), d: pixels.into_iter().map(dval).collect(), ends: [None, None], closed }, max_branches, )?; } - Some(graph) + Some(()) } impl Graph { diff --git a/crates/trace/src/layers.rs b/crates/trace/src/layers.rs index bf5bfc402..3c9467d76 100644 --- a/crates/trace/src/layers.rs +++ b/crates/trace/src/layers.rs @@ -3,7 +3,12 @@ use super::params::{Method, Mode, TraceParams}; use super::pipeline::bw_tones; use super::pipeline::{TracedPath, is_ignored}; use super::raster::Raster; -use super::*; +use super::{ + AtomicUsize, Crack, Loop, Ordering, PathData, Quantized, TRANSPARENT, TracedGradient, TracedShape, centerline, contour, fit, logo, shapes, + trace_mask, +}; +#[cfg(test)] +use super::{denoise, quantize}; /// What tracing one colour layer needs. pub(crate) struct Layers<'a> { diff --git a/crates/trace/src/params.rs b/crates/trace/src/params.rs index 5503a84ff..6ce6ce241 100644 --- a/crates/trace/src/params.rs +++ b/crates/trace/src/params.rs @@ -1,4 +1,4 @@ -use super::*; +use super::{Deserialize, Serialize, logo}; /// Tracing mode (Illustrator's Mode popup). #[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] diff --git a/crates/trace/src/pipeline.rs b/crates/trace/src/pipeline.rs index 6d3ea27f5..b857e73ae 100644 --- a/crates/trace/src/pipeline.rs +++ b/crates/trace/src/pipeline.rs @@ -1,7 +1,7 @@ use super::layers::{Layers, parallel_map_at_most}; use super::params::{Method, Mode, TraceParams}; use super::raster::{Raster, TraceError}; -use super::*; +use super::{AtomicUsize, PathData, Quantized, TRANSPARENT, TracedGradient, TracedShape, gradient, group, logo, quantize, smooth}; /// One traced shape: an outer contour plus its holes, filled with `color`, or (with `stroke`) /// lines stroked with it. diff --git a/crates/trace/src/quantize/cut_edge.rs b/crates/trace/src/quantize/cut_edge.rs index 2e47e64ca..5f0a30568 100644 --- a/crates/trace/src/quantize/cut_edge.rs +++ b/crates/trace/src/quantize/cut_edge.rs @@ -1,5 +1,5 @@ use super::palette::{Bin, HistKey, Used, centers, median_cut, nearest_lab}; -use super::*; +use super::{Quantized, Raster, TRANSPARENT, luma, oklab}; /// `q` with its blend pixels settled, and the colours that leaves unused dropped. /// diff --git a/crates/trace/src/quantize/denoise.rs b/crates/trace/src/quantize/denoise.rs index 4ca5c29e9..cb40ffa4f 100644 --- a/crates/trace/src/quantize/denoise.rs +++ b/crates/trace/src/quantize/denoise.rs @@ -1,5 +1,5 @@ +use super::TRANSPARENT; use super::rare::RARE_ISLAND; -use super::*; /// Merge whole 4-connected regions, smallest first, into their most common neighbouring label. /// The merge order does not depend on `min_area`: a higher threshold continues the same sequence diff --git a/crates/trace/src/quantize/palette.rs b/crates/trace/src/quantize/palette.rs index 43363b286..8f89a7c96 100644 --- a/crates/trace/src/quantize/palette.rs +++ b/crates/trace/src/quantize/palette.rs @@ -1,4 +1,4 @@ -use super::*; +use super::{Palette, Raster, TraceParams, oklab}; #[derive(Clone, Copy, Default)] pub(super) struct Bin { diff --git a/crates/trace/src/quantize/rare.rs b/crates/trace/src/quantize/rare.rs index 69069e48b..d835c5759 100644 --- a/crates/trace/src/quantize/rare.rs +++ b/crates/trace/src/quantize/rare.rs @@ -1,5 +1,5 @@ use super::palette::{Bin, HistKey, TracedAs, Used, labs, nearest_lab}; -use super::*; +use super::{Raster, oklab}; /// Automatic: the most rare colours added to a flat picture's count. const RARE_BUDGET: usize = 6; @@ -50,287 +50,390 @@ const CUT_EDGE_MIX_FROM: f64 = 0.45; /// salient colours that are rare by area follows the idea of salient-colour preservation in /// palette reduction (Fukuda et al. 2019, doi:10.1117/12.2521607). The rule and its numbers are /// VectorCraft's own. +type ChosenRare = (usize, f64, [f64; 3], usize); + +struct RareGroups { + used: Vec<(usize, [f64; 3], f64)>, + groups: Vec>, + group_of: Vec, +} + +struct RasterGroups<'a> { + width: usize, + height: usize, + pixels: &'a [[u8; 4]], + own: &'a [bool], + choose: &'a [bool], + hk: &'a HistKey, + group_of: &'a [u16], +} + +struct RegionMetrics { + largest: Vec, + islands: Vec, + inner: Vec, + beside_left_out: Vec, +} + +struct BlendContext { + from: Vec, + with: Vec<[f64; 3]>, + pairs: Vec<(usize, usize)>, + near_labs: Vec<[f64; 3]>, + counts_as: Vec>, +} + pub(super) fn rare_colours(img: &Raster, own: &[bool], choose: &[bool], hk: &HistKey, centers: &[[f64; 3]]) -> (Vec<[f64; 3]>, Vec) { - let px = img.rgba.as_chunks::<4>().0; - if centers.is_empty() || own.len() != px.len() { + let pixels = img.rgba.as_chunks::<4>().0; + if centers.is_empty() || own.len() != pixels.len() { return (vec![], vec![]); } + let center_labs = labs(centers); + let groups = rare_groups(pixels, own, hk, ¢er_labs); + if groups.groups.is_empty() { + return (vec![], vec![]); + } + let (width, height) = (img.width as usize, img.height as usize); + let raster = RasterGroups { width, height, pixels, own, choose, hk, group_of: &groups.group_of }; + let metrics = region_metrics(&raster, groups.groups.len()); + let chosen = choose_rare_groups(&groups.groups, &groups.used, centers, &metrics); + if chosen.is_empty() { + return (vec![], vec![]); + } + let (onto, other_of) = trace_edge_blends(&raster, &chosen, centers, &metrics); + reindex_traced(chosen, onto, other_of, centers) +} + +fn rare_groups(pixels: &[[u8; 4]], own: &[bool], hk: &HistKey, center_labs: &[[f64; 3]]) -> RareGroups { // The picture's own (not blended) pixels by histogram bin. let mut bins = vec![Bin::default(); 1 << 15]; - for (p, _) in px.iter().zip(own).filter(|(p, f)| **f && p[3] >= 128) { - bins[hk.key(p[0], p[1], p[2])].add(p); + for (pixel, _) in pixels.iter().zip(own).filter(|(pixel, own)| **own && pixel[3] >= 128) { + bins[hk.key(pixel[0], pixel[1], pixel[2])].add(pixel); } - let used: Vec<(usize, [f64; 3], f64)> = bins.iter().enumerate().filter(|(_, b)| b.n > 0).map(|(i, b)| (i, b.mean(), b.n as f64)).collect(); - let used_labs = labs(&used.iter().map(|u| u.1).collect::>()); - let center_labs = labs(centers); + let used: Vec<(usize, [f64; 3], f64)> = + bins.iter().enumerate().filter(|(_, bin)| bin.n > 0).map(|(i, bin)| (i, bin.mean(), bin.n as f64)).collect(); + let used_labs = labs(&used.iter().map(|entry| entry.1).collect::>()); // Each palette colour's main colour: its heaviest bin. - let mut peak: Vec> = vec![None; centers.len()]; - for ((_, _, n), lab) in used.iter().zip(&used_labs) { - let j = nearest_lab(¢er_labs, *lab); - if let Some(slot) = peak.get_mut(j) - && slot.is_none_or(|(m, _)| *n > m) + let mut peak: Vec> = vec![None; center_labs.len()]; + for ((_, _, count), lab) in used.iter().zip(&used_labs) { + let index = nearest_lab(center_labs, *lab); + if let Some(slot) = peak.get_mut(index) + && slot.is_none_or(|(largest, _)| *count > largest) { - *slot = Some((*n, *lab)); + *slot = Some((*count, *lab)); } } - let dist = |a: [f64; 3], b: [f64; 3]| (0..3).map(|k| (a[k] - b[k]).powi(2)).sum::().sqrt(); - let apart = |lab: [f64; 3]| center_labs.iter().chain(peak.iter().flatten().map(|(_, l)| l)).all(|c| dist(lab, *c) >= RARE_APART); - // The bins far from every colour, heaviest first, grouped: a bin joins the first group whose - // heaviest bin is nearer than RARE_APART. - let mut far: Vec = (0..used.len()).filter(|&u| used_labs.get(u).is_some_and(|l| apart(*l))).collect(); + let distance = |a: [f64; 3], b: [f64; 3]| (0..3).map(|channel| (a[channel] - b[channel]).powi(2)).sum::().sqrt(); + let far_from_palette = + |lab: [f64; 3]| center_labs.iter().chain(peak.iter().flatten().map(|(_, lab)| lab)).all(|center| distance(lab, *center) >= RARE_APART); + // Group far bins by their heaviest member, in descending weight order. + let mut far: Vec = (0..used.len()).filter(|&index| used_labs.get(index).is_some_and(|lab| far_from_palette(*lab))).collect(); far.sort_by(|&a, &b| used[b].2.total_cmp(&used[a].2).then(a.cmp(&b))); let mut group_of = vec![u16::MAX; 1 << 15]; let mut groups: Vec> = vec![]; - for u in far { - let (Some(lab), Some(&(bin, ..))) = (used_labs.get(u), used.get(u)) else { continue }; - let at = groups.iter().position(|g| g.first().and_then(|&f| used_labs.get(f)).is_some_and(|l| dist(*l, *lab) < RARE_APART)); - let g = match at { - Some(g) => g, + for index in far { + let (Some(lab), Some(&(bin, ..))) = (used_labs.get(index), used.get(index)) else { continue }; + let group = groups + .iter() + .position(|members| members.first().and_then(|&first| used_labs.get(first)).is_some_and(|first| distance(*first, *lab) < RARE_APART)); + let group = match group { + Some(group) => group, None if groups.len() < 64 => { groups.push(vec![]); groups.len() - 1 } None => continue, }; - if let Some(members) = groups.get_mut(g) { - members.push(u); + if let Some(members) = groups.get_mut(group) { + members.push(index); } - group_of[bin] = g as u16; + group_of[bin] = group as u16; } - if groups.is_empty() { - return (vec![], vec![]); - } - // The largest 4-connected region of each group's own pixels. - let (w, h) = (img.width as usize, img.height as usize); - let group_at = |i: usize| -> Option { - let p = px.get(i)?; - (own.get(i).copied().unwrap_or(false) && p[3] >= 128).then(|| group_of[hk.key(p[0], p[1], p[2])]).filter(|&g| g != u16::MAX) + RareGroups { used, groups, group_of } +} + +fn group_at(pixels: &[[u8; 4]], own: &[bool], hk: &HistKey, group_of: &[u16], index: usize) -> Option { + let pixel = pixels.get(index)?; + (own.get(index).copied().unwrap_or(false) && pixel[3] >= 128) + .then(|| group_of.get(hk.key(pixel[0], pixel[1], pixel[2])).copied().unwrap_or(u16::MAX)) + .filter(|&group| group != u16::MAX) +} + +fn region_metrics(raster: &RasterGroups<'_>, group_count: usize) -> RegionMetrics { + let RasterGroups { width, height, pixels, own, choose, hk, group_of } = *raster; + let mut metrics = RegionMetrics { + largest: vec![0; group_count], + islands: vec![0; group_count], + inner: vec![0; group_count], + beside_left_out: vec![false; group_count], }; - let mut largest = vec![0usize; groups.len()]; - let mut islands = vec![0usize; groups.len()]; - // Pixels of a group whose four neighbours are of it too: an edge's blend is a line one pixel - // wide and has next to none. - let mut inner = vec![0usize; groups.len()]; - // Groups with a pixel beside a pixel that `choose` leaves out (see `choosing_pixels`): the blend - // test of `own` saw no neighbor on that side. - let mut beside_left_out = vec![false; groups.len()]; - let left_out = |j: usize| px.get(j).is_some_and(|p| p[3] >= 128) && !choose.get(j).copied().unwrap_or(true); - let mut seen = vec![false; w * h]; + let left_out = |index: usize| pixels.get(index).is_some_and(|pixel| pixel[3] >= 128) && !choose.get(index).copied().unwrap_or(true); + let mut seen = vec![false; width * height]; let mut stack = vec![]; - for start in 0..w * h { - let Some(g) = group_at(start).filter(|_| !seen[start]) else { continue }; + for start in 0..width * height { + let Some(group) = group_at(pixels, own, hk, group_of, start).filter(|_| !seen[start]) else { continue }; seen[start] = true; stack.push(start); let mut size = 0; - while let Some(i) = stack.pop() { + while let Some(index) = stack.pop() { size += 1; - let (x, y) = (i % w, i / w); + let (x, y) = (index % width, index / width); let mut alike = 0; - for j in [(x > 0).then(|| i - 1), (x + 1 < w).then(|| i + 1), (y > 0).then(|| i - w), (y + 1 < h).then(|| i + w)].into_iter().flatten() { - if group_at(j) == Some(g) { + for neighbour in [ + (x > 0).then(|| index - 1), + (x + 1 < width).then(|| index + 1), + (y > 0).then(|| index - width), + (y + 1 < height).then(|| index + width), + ] + .into_iter() + .flatten() + { + if group_at(pixels, own, hk, group_of, neighbour) == Some(group) { alike += 1; - if !seen[j] { - seen[j] = true; - stack.push(j); + if !seen[neighbour] { + seen[neighbour] = true; + stack.push(neighbour); } } } if alike == 4 - && let Some(n) = inner.get_mut(g as usize) + && let Some(count) = metrics.inner.get_mut(group as usize) { - *n += 1; + *count += 1; } - if let Some(b) = beside_left_out.get_mut(g as usize).filter(|b| !**b) { - let (x0, y0, x1, y1) = (x.saturating_sub(1), y.saturating_sub(1), (x + 1).min(w - 1), (y + 1).min(h - 1)); - *b = (y0..=y1).any(|ny| (x0..=x1).any(|nx| left_out(ny * w + nx))); + if let Some(beside) = metrics.beside_left_out.get_mut(group as usize).filter(|beside| !**beside) { + let (left, top, right, bottom) = (x.saturating_sub(1), y.saturating_sub(1), (x + 1).min(width - 1), (y + 1).min(height - 1)); + *beside = (top..=bottom).any(|near_y| (left..=right).any(|near_x| left_out(near_y * width + near_x))); } } - if let Some(l) = largest.get_mut(g as usize) { - *l = (*l).max(size); + if let Some(largest) = metrics.largest.get_mut(group as usize) { + *largest = (*largest).max(size); } - if let Some(n) = islands.get_mut(g as usize).filter(|_| size >= RARE_ISLAND) { - *n += 1; + if let Some(islands) = metrics.islands.get_mut(group as usize).filter(|_| size >= RARE_ISLAND) { + *islands += 1; } } - // Coherent and flat groups, largest region first. - let mut chosen: Vec<(usize, f64, [f64; 3], usize)> = vec![]; - for (g, members) in groups.iter().enumerate() { - let weight: f64 = members.iter().filter_map(|&u| used.get(u)).map(|u| u.2).sum(); - let heaviest = members.first().and_then(|&u| used.get(u)).map_or(0.0, |u| u.2); - let region = largest.get(g).copied().unwrap_or(0); - let coherent = region >= RARE_REGION || islands.get(g).is_some_and(|&n| n >= RARE_ISLANDS); + metrics +} + +fn choose_rare_groups(groups: &[Vec], used: &[(usize, [f64; 3], f64)], centers: &[[f64; 3]], metrics: &RegionMetrics) -> Vec { + let mut chosen = Vec::new(); + for (group, members) in groups.iter().enumerate() { + let weight: f64 = members.iter().filter_map(|&index| used.get(index)).map(|entry| entry.2).sum(); + let heaviest = members.first().and_then(|&index| used.get(index)).map_or(0.0, |entry| entry.2); + let region = metrics.largest.get(group).copied().unwrap_or(0); + let coherent = region >= RARE_REGION || metrics.islands.get(group).is_some_and(|&count| count >= RARE_ISLANDS); if !coherent || heaviest < 0.5 * weight || weight <= 0.0 { continue; } let mut mean = [0.0; 3]; - for u in members.iter().filter_map(|&u| used.get(u)) { - for (m, c) in mean.iter_mut().zip(u.1) { - *m += c * u.2 / weight; + for entry in members.iter().filter_map(|&index| used.get(index)) { + for (mean_channel, channel) in mean.iter_mut().zip(entry.1) { + *mean_channel += channel * entry.2 / weight; } } - // Not an edge's blend of two colours that the blend test let through (a thin ring of - // anti-aliasing between two bands is coherent too): a colour that lies between two others - // is kept only with an inside, pixels whose neighbours are all of it. - if is_mix(mean, centers) && inner.get(g).is_none_or(|&n| n < RARE_ISLAND) { + // A color lying between two others needs a solid inside, not just a thin blended rim. + if is_mix(mean, centers) && metrics.inner.get(group).is_none_or(|&count| count < RARE_ISLAND) { continue; } - chosen.push((region, weight, mean, g)); + chosen.push((region, weight, mean, group)); } chosen.sort_by(|a, b| b.0.cmp(&a.0).then(b.1.total_cmp(&a.1)).then(a.2.partial_cmp(&b.2).unwrap_or(std::cmp::Ordering::Equal))); - // Beside the pixels that choose no color, the blend test of `own` does not see the - // anti-aliasing between a palette color and a rare color drawn along the cut edge. There, a - // color without an inside that lies between two colors, one of them another kept rare color, - // and at least CUT_EDGE_MIX_FROM of the way from the other to it, is that anti-aliasing when at - // least one in CUT_EDGE_MIX_TOUCH of its pixels has pixels of both colors around it: it is not - // kept, and it traces as that rare color (the nearer one when there are several). Art that - // reaches a rim drawn along the edge touches it in a few places only, and the rim keeps its - // color. The smallest regions are tested first, so that of two such colors, one stays, and a - // color found to trace as another counts as that color. - let tested = |g: usize| beside_left_out.get(g).copied().unwrap_or(false) && inner.get(g).is_none_or(|&n| n < RARE_ISLAND); - // The pixels of each color tested, gathered in one pass over the image. - let mut pixels_of: Vec> = vec![vec![]; groups.len()]; - let wanted: Vec = (0..groups.len()).map(|g| tested(g) && chosen.iter().any(|c| c.3 == g)).collect(); + chosen +} + +fn trace_edge_blends( + raster: &RasterGroups<'_>, + chosen: &[ChosenRare], + centers: &[[f64; 3]], + metrics: &RegionMetrics, +) -> (Vec>, Vec>) { + let RasterGroups { width, height, pixels, own, choose, hk, group_of } = *raster; + let group_count = metrics.largest.len(); + let tested = |group: usize| { + metrics.beside_left_out.get(group).copied().unwrap_or(false) && metrics.inner.get(group).is_none_or(|&count| count < RARE_ISLAND) + }; + let wanted: Vec = (0..group_count).map(|group| tested(group) && chosen.iter().any(|candidate| candidate.3 == group)).collect(); + let mut pixels_of = vec![vec![]; group_count]; if wanted.contains(&true) { - for i in 0..w * h { - if let Some(g) = group_at(i).map(usize::from).filter(|&g| wanted.get(g).copied().unwrap_or(false)) - && let Some(p) = pixels_of.get_mut(g) + for index in 0..width * height { + if let Some(group) = + group_at(pixels, own, hk, group_of, index).map(usize::from).filter(|&group| wanted.get(group).copied().unwrap_or(false)) + && let Some(pixels) = pixels_of.get_mut(group) { - p.push(i); + pixels.push(index); } } } - let mut onto: Vec> = vec![None; chosen.len()]; - // For each color in `onto`, where the color on the other side of it comes from (see `from` - // below), or `None` when the color traces as the rare color alone. - let mut other_of: Vec> = vec![None; chosen.len()]; - for k in (0..chosen.len()).rev() { - let Some(&(_, _, c, g)) = chosen.get(k) else { continue }; - if !tested(g) { + let mut onto = vec![None; chosen.len()]; + let mut other_of = vec![None; chosen.len()]; + for index in (0..chosen.len()).rev() { + let Some(&(_, _, color, group)) = chosen.get(index) else { continue }; + if !tested(group) { continue; } - // The palette colors, then the other rare colors still kept, and where each comes from: a - // palette color's index, or the number of palette colors plus the index in `chosen`. - let from: Vec = (0..centers.len()) - .chain(chosen.iter().zip(&onto).enumerate().filter(|(o, (_, t))| *o != k && t.is_none()).map(|(o, _)| centers.len() + o)) - .collect(); - let with: Vec<[f64; 3]> = centers - .iter() - .copied() - .chain(from.iter().skip(centers.len()).filter_map(|&f| chosen.get(f.checked_sub(centers.len())?)).map(|x| x.2)) - .collect(); - let pairs: Vec<(usize, usize)> = (0..with.len()) - .flat_map(|a| (a + 1..with.len()).map(move |b| (a, b))) - .filter(|&(a, b)| matches!((with.get(a), with.get(b)), (Some(&x), Some(&y)) if is_mix(c, &[x, y]))) - .collect(); - if pairs.is_empty() { - continue; + let Some(context) = blend_context(index, color, chosen, &onto, centers) else { continue }; + let Some(group_pixels) = pixels_of.get(group) else { continue }; + let (both, pixel_count) = blend_neighbour_counts(group_pixels, width, height, pixels, choose, &context); + let best = best_blend_target(color, chosen, centers, metrics, &context, &both, pixel_count); + if let Some(target) = onto.get_mut(index) { + *target = best.map(|candidate| candidate.1); } - // Which of `with` the pixels in the 3 × 3 around the color's own pixels are: the nearest - // among `with`, the color itself and the colors that trace as one of `with`. A pixel that - // chooses no color counts only within RARE_APART of the one it is nearest to (beyond the - // cut edge, an outline over the edge shows its own color). - let mut near = with.clone(); - near.push(c); - let mut counts_as: Vec> = (0..with.len()).map(Some).chain([None]).collect(); - for (x, t) in chosen.iter().zip(&onto) { - if let Some(t) = t { - near.push(x.2); - counts_as.push(with.iter().position(|w| w == t)); - } + if let Some(other) = other_of.get_mut(index) { + *other = best.and_then(|candidate| candidate.2); } - let near_labs = labs(&near); - // For each pair, how many of the color's own pixels have pixels of both around them. - let mut both = vec![0usize; pairs.len()]; - let mut pixels = 0usize; - let mut around = vec![false; with.len()]; - for &i in pixels_of.get(g).map_or(&[][..], Vec::as_slice) { - let (x, y) = (i % w, i / w); - pixels += 1; - around.fill(false); - for ny in y.saturating_sub(1)..=(y + 1).min(h - 1) { - for nx in x.saturating_sub(1)..=(x + 1).min(w - 1) { - let j = ny * w + nx; - let Some(p) = px.get(j).filter(|p| p[3] >= 128) else { continue }; - let o = oklab([p[0], p[1], p[2]].map(f64::from)); - let n = nearest_lab(&near_labs, o); - if !choose.get(j).copied().unwrap_or(false) && near_labs.get(n).is_none_or(|l| dist(*l, o) >= RARE_APART) { - continue; - } - if let Some(&Some(a)) = counts_as.get(n) - && let Some(t) = around.get_mut(a) - { - *t = true; - } - } - } - for (n, &(a, b)) in pairs.iter().enumerate() { - if around.get(a).copied().unwrap_or(false) - && around.get(b).copied().unwrap_or(false) - && let Some(v) = both.get_mut(n) + } + (onto, other_of) +} + +fn blend_context(index: usize, color: [f64; 3], chosen: &[ChosenRare], onto: &[Option<[f64; 3]>], centers: &[[f64; 3]]) -> Option { + let from: Vec = (0..centers.len()) + .chain( + chosen + .iter() + .zip(onto) + .enumerate() + .filter(|(other, (_, target))| *other != index && target.is_none()) + .map(|(other, _)| centers.len() + other), + ) + .collect(); + let with: Vec<[f64; 3]> = centers + .iter() + .copied() + .chain(from.iter().skip(centers.len()).filter_map(|&source| chosen.get(source.checked_sub(centers.len())?)).map(|entry| entry.2)) + .collect(); + let pairs: Vec<(usize, usize)> = (0..with.len()) + .flat_map(|a| (a + 1..with.len()).map(move |b| (a, b))) + .filter(|&(a, b)| matches!((with.get(a), with.get(b)), (Some(&left), Some(&right)) if is_mix(color, &[left, right]))) + .collect(); + if pairs.is_empty() { + return None; + } + let mut near = with.clone(); + near.push(color); + let mut counts_as: Vec> = (0..with.len()).map(Some).chain([None]).collect(); + for (candidate, target) in chosen.iter().zip(onto) { + if let Some(target) = target { + near.push(candidate.2); + counts_as.push(with.iter().position(|other| other == target)); + } + } + Some(BlendContext { from, with, pairs, near_labs: labs(&near), counts_as }) +} + +fn blend_neighbour_counts( + group_pixels: &[usize], + width: usize, + height: usize, + pixels: &[[u8; 4]], + choose: &[bool], + context: &BlendContext, +) -> (Vec, usize) { + let distance = |a: [f64; 3], b: [f64; 3]| (0..3).map(|channel| (a[channel] - b[channel]).powi(2)).sum::().sqrt(); + let mut both = vec![0; context.pairs.len()]; + let mut pixel_count = 0; + let mut around = vec![false; context.with.len()]; + for &index in group_pixels { + let (x, y) = (index % width, index / width); + pixel_count += 1; + around.fill(false); + for near_y in y.saturating_sub(1)..=(y + 1).min(height - 1) { + for near_x in x.saturating_sub(1)..=(x + 1).min(width - 1) { + let neighbour = near_y * width + near_x; + let Some(pixel) = pixels.get(neighbour).filter(|pixel| pixel[3] >= 128) else { continue }; + let lab = oklab([pixel[0], pixel[1], pixel[2]].map(f64::from)); + let nearest = nearest_lab(&context.near_labs, lab); + if !choose.get(neighbour).copied().unwrap_or(false) + && context.near_labs.get(nearest).is_none_or(|candidate| distance(*candidate, lab) >= RARE_APART) { - *v += 1; - } - } - } - let lab = oklab(c); - // The nearest rare color it traces as, with where the other color of its pair comes from - // when its pixels split between the two (see below). - let mut best: Option<(f64, [f64; 3], Option)> = None; - for (n, &(a, b)) in pairs.iter().enumerate() { - if both.get(n).is_none_or(|&v| v == 0 || v * CUT_EDGE_MIX_TOUCH < pixels) { - continue; - } - for (e, f) in [(a, b), (b, a)].into_iter().filter(|&(e, _)| e >= centers.len()) { - let (Some(&x), Some(&y)) = (with.get(e), with.get(f)) else { continue }; - // How far `c` lies from the other color toward this one. - let d2: f64 = (0..3).map(|q| (x[q] - y[q]).powi(2)).sum(); - let t = if d2 > 0.0 { (0..3).map(|q| (c[q] - y[q]) * (x[q] - y[q])).sum::() / d2 } else { 0.0 }; - if t < CUT_EDGE_MIX_FROM { continue; } - let d = dist(oklab(x), lab); - if best.is_none_or(|b| d < b.0) { - // A rare color along the cut edge with a region of RARE_REGION pixels is a line - // of its own there: the anti-aliasing beside it splits between it and the other - // color. Otherwise the anti-aliasing is most of a thin line, or the part of a - // line over the edge that lies inside it, and traces as the rare color alone. - let body = from - .get(e) - .and_then(|&m| chosen.get(m.checked_sub(centers.len())?)) - .is_some_and(|r| r.0 >= RARE_REGION && beside_left_out.get(r.3).copied().unwrap_or(false)); - best = Some((d, x, from.get(f).copied().filter(|_| body))); + if let Some(&Some(group)) = context.counts_as.get(nearest) + && let Some(is_near) = around.get_mut(group) + { + *is_near = true; } } } - if let Some(o) = onto.get_mut(k) { - *o = best.map(|b| b.1); - } - if let Some(o) = other_of.get_mut(k) { - *o = best.and_then(|b| b.2); + for (pair_index, &(a, b)) in context.pairs.iter().enumerate() { + if around.get(a).copied().unwrap_or(false) + && around.get(b).copied().unwrap_or(false) + && let Some(count) = both.get_mut(pair_index) + { + *count += 1; + } } } - 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 { - let Some(m) = from.checked_sub(centers.len()) else { return Some(from) }; - let (_, _, x, _) = chosen.get(m)?; - let x = onto.get(m).copied().flatten().unwrap_or(*x); - Some(centers.len() + out.iter().position(|o| *o == x)?) + (both, pixel_count) +} + +fn best_blend_target( + color: [f64; 3], + chosen: &[ChosenRare], + centers: &[[f64; 3]], + metrics: &RegionMetrics, + context: &BlendContext, + both: &[usize], + pixel_count: usize, +) -> Option<(f64, [f64; 3], Option)> { + let distance = |a: [f64; 3], b: [f64; 3]| (0..3).map(|channel| (a[channel] - b[channel]).powi(2)).sum::().sqrt(); + let lab = oklab(color); + let mut best = None; + for (pair_index, &(a, b)) in context.pairs.iter().enumerate() { + if both.get(pair_index).is_none_or(|&count| count == 0 || count * CUT_EDGE_MIX_TOUCH < pixel_count) { + continue; + } + for (edge, other) in [(a, b), (b, a)].into_iter().filter(|&(edge, _)| edge >= centers.len()) { + let (Some(&edge_color), Some(&other_color)) = (context.with.get(edge), context.with.get(other)) else { continue }; + let squared = (0..3).map(|channel| (edge_color[channel] - other_color[channel]).powi(2)).sum::(); + let along = if squared > 0.0 { + (0..3).map(|channel| (color[channel] - other_color[channel]) * (edge_color[channel] - other_color[channel])).sum::() / squared + } else { + 0.0 + }; + if along < CUT_EDGE_MIX_FROM { + continue; + } + let separation = distance(oklab(edge_color), lab); + if best.is_none_or(|candidate: (f64, [f64; 3], Option)| separation < candidate.0) { + // A substantial rare region along the edge keeps the identity of its other side. + let has_body = context + .from + .get(edge) + .and_then(|&source| chosen.get(source.checked_sub(centers.len())?)) + .is_some_and(|rare| rare.0 >= RARE_REGION && metrics.beside_left_out.get(rare.3).copied().unwrap_or(false)); + best = Some((separation, edge_color, context.from.get(other).copied().filter(|_| has_body))); + } + } + } + best +} + +fn reindex_traced( + chosen: Vec, + onto: Vec>, + other_of: Vec>, + centers: &[[f64; 3]], +) -> (Vec<[f64; 3]>, Vec) { + let (output, traced) = kept_rare(chosen.iter().zip(&onto).zip(&other_of).map(|(((_, _, color, _), target), &other)| (*color, *target, other))); + let output_index = |source: usize| -> Option { + let Some(index) = source.checked_sub(centers.len()) else { return Some(source) }; + let (_, _, color, _) = chosen.get(index)?; + let color = onto.get(index).copied().flatten().unwrap_or(*color); + Some(centers.len() + output.iter().position(|candidate| *candidate == color)?) }; - // A color whose rare color is not kept (past the budget) is left out. - let traced_as = traced_as + let traced = traced .into_iter() - .filter_map(|(c, o, from)| { - let rare = centers.len() + out.iter().position(|x| *x == o)?; - let other = match from { - Some(f) => index(f)?, + .filter_map(|(color, target, other)| { + let rare = centers.len() + output.iter().position(|candidate| *candidate == target)?; + let other = match other { + Some(source) => output_index(source)?, None => rare, }; - Some((c, rare, other)) + Some((color, rare, other)) }) .collect(); - (out, traced_as) + (output, traced) } /// Of `colors` (a rare color, the rare color it traces as, and where the other color of its pair