refactor(trace): separate graph construction and quantization

This commit is contained in:
Dylan M. Taylor
2026-10-27 17:47:12 +00:00
parent 3d0188a5bc
commit 7b05e55a69
8 changed files with 405 additions and 270 deletions
+82 -55
View File
@@ -68,18 +68,26 @@ fn build_with_limits(g: &Grid, dist: &[f32], max_nodes: usize, max_branches: usi
{
return None;
}
let degree: Vec<u8> = (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, &degree, 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, &degree, &node_of, &mut graph, &mut visited, max_branches)?;
Some(graph)
}
fn build_nodes(g: &Grid, dist: &[f32], degree: &[u8], max_nodes: usize) -> Option<(Vec<usize>, Graph)> {
let dval = |i: usize| f64::from(dist.get(i).copied().unwrap_or(0.0)).sqrt();
let deg: Vec<u8> = (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<Point> = 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<Point> = 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 {
+6 -1
View File
@@ -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> {
+1 -1
View File
@@ -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)]
+1 -1
View File
@@ -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.
+1 -1
View File
@@ -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.
///
+1 -1
View File
@@ -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
+1 -1
View File
@@ -1,4 +1,4 @@
use super::*;
use super::{Palette, Raster, TraceParams, oklab};
#[derive(Clone, Copy, Default)]
pub(super) struct Bin {
+312 -209
View File
@@ -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<Vec<usize>>,
group_of: Vec<u16>,
}
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<usize>,
islands: Vec<usize>,
inner: Vec<usize>,
beside_left_out: Vec<bool>,
}
struct BlendContext {
from: Vec<usize>,
with: Vec<[f64; 3]>,
pairs: Vec<(usize, usize)>,
near_labs: Vec<[f64; 3]>,
counts_as: Vec<Option<usize>>,
}
pub(super) fn rare_colours(img: &Raster, own: &[bool], choose: &[bool], hk: &HistKey, centers: &[[f64; 3]]) -> (Vec<[f64; 3]>, Vec<TracedAs>) {
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, &center_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::<Vec<_>>());
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::<Vec<_>>());
// Each palette colour's main colour: its heaviest bin.
let mut peak: Vec<Option<(f64, [f64; 3])>> = vec![None; centers.len()];
for ((_, _, n), lab) in used.iter().zip(&used_labs) {
let j = nearest_lab(&center_labs, *lab);
if let Some(slot) = peak.get_mut(j)
&& slot.is_none_or(|(m, _)| *n > m)
let mut peak: Vec<Option<(f64, [f64; 3])>> = 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::<f64>().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<usize> = (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::<f64>().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<usize> = (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<usize>> = 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<u16> {
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<u16> {
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<usize>], used: &[(usize, [f64; 3], f64)], centers: &[[f64; 3]], metrics: &RegionMetrics) -> Vec<ChosenRare> {
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<usize>> = vec![vec![]; groups.len()];
let wanted: Vec<bool> = (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<Option<[f64; 3]>>, Vec<Option<usize>>) {
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<bool> = (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<Option<[f64; 3]>> = 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<Option<usize>> = 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<usize> = (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<Option<usize>> = (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<BlendContext> {
let from: Vec<usize> = (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<Option<usize>> = (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>, usize) {
let distance = |a: [f64; 3], b: [f64; 3]| (0..3).map(|channel| (a[channel] - b[channel]).powi(2)).sum::<f64>().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<usize>)> = 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::<f64>() / 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<usize> {
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<usize>)> {
let distance = |a: [f64; 3], b: [f64; 3]| (0..3).map(|channel| (a[channel] - b[channel]).powi(2)).sum::<f64>().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::<f64>();
let along = if squared > 0.0 {
(0..3).map(|channel| (color[channel] - other_color[channel]) * (edge_color[channel] - other_color[channel])).sum::<f64>() / 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<usize>)| 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<ChosenRare>,
onto: Vec<Option<[f64; 3]>>,
other_of: Vec<Option<usize>>,
centers: &[[f64; 3]],
) -> (Vec<[f64; 3]>, Vec<TracedAs>) {
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<usize> {
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