mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-29 12:17:14 -06:00
refactor(trace): separate graph construction and quantization
This commit is contained in:
@@ -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, °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<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 {
|
||||
|
||||
@@ -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,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,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,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,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,4 +1,4 @@
|
||||
use super::*;
|
||||
use super::{Palette, Raster, TraceParams, oklab};
|
||||
|
||||
#[derive(Clone, Copy, Default)]
|
||||
pub(super) struct Bin {
|
||||
|
||||
+312
-209
@@ -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, ¢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::<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(¢er_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
|
||||
|
||||
Reference in New Issue
Block a user