mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-15 07:10:13 +00:00
Image Trace: blend settling keeps its changes within the labels' size
Each band of rows (settled) and each group of strips (settled_wide) now keeps its changed labels as pixel and label pairs only until the pairs would take more memory than a copy of that part's labels, and then keeps the copy with the changes made. settled_wide splits the picture into ranges of columns instead of first listing the start of every strip. - The tests of settled and settled_wide move into cut_edge.rs, and both functions are private again. - A new test checks that the changes switch to a copy at that size and give the same labels either way. - parallel_map is imported from the crate root, as in deblock.rs and smooth.rs.
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
//! Colour reduction and noise removal. Colours are compared in Oklab, where equal distances look
|
||||
//! equally different.
|
||||
|
||||
use crate::{Mode, Palette, Raster, TraceParams};
|
||||
use crate::{Mode, Palette, Raster, TraceParams, parallel_map};
|
||||
|
||||
mod cut_edge;
|
||||
mod denoise;
|
||||
@@ -10,8 +10,6 @@ mod rare;
|
||||
|
||||
use cut_edge::{choosing_pixels, compact, settled, settled_wide, settles_wide};
|
||||
pub(crate) use cut_edge::{continuous_tone, settle_cut_edges};
|
||||
#[cfg(test)]
|
||||
use cut_edge::{settled_by, settled_wide_by};
|
||||
pub use denoise::denoise;
|
||||
pub(crate) use denoise::denoise_keeping;
|
||||
pub use palette::FULL_TONE_MAX;
|
||||
@@ -250,7 +248,7 @@ fn over_rows<R: Send>(w: usize, h: usize, per_share: usize, f: impl Fn(std::ops:
|
||||
if bands.len() <= 1 {
|
||||
return bands.into_iter().flat_map(&f).collect();
|
||||
}
|
||||
crate::layers::parallel_map(&bands, |rows| f(rows.clone())).into_iter().flatten().collect()
|
||||
parallel_map(&bands, |rows| f(rows.clone())).into_iter().flatten().collect()
|
||||
}
|
||||
|
||||
/// Whether the palette is made from the picture's own colours alone (`own` of `all` opaque
|
||||
@@ -361,60 +359,27 @@ fn merge_mixed(q: Quantized, w: usize, h: usize, keep: &[[u8; 3]]) -> Quantized
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// Rings of four colors around a point off the center, blended over three pixels at each edge,
|
||||
/// with a little noise, and a band of half-transparent pixels along the bottom.
|
||||
fn rings(w: u32, h: u32) -> Raster {
|
||||
let colors = [[200.0, 30.0, 40.0], [20.0, 60.0, 180.0], [240.0, 210.0, 50.0], [25.0, 25.0, 25.0]];
|
||||
Raster::from_fn(w, h, |x, y| {
|
||||
let r = (f64::from(x) - 0.4 * f64::from(w)).hypot(f64::from(y) - 0.55 * f64::from(h)) / 9.0;
|
||||
let (ring, t) = (r.floor() as usize, ((r.fract() - 0.8) / 0.2).clamp(0.0, 1.0));
|
||||
let (a, b) = (colors[ring % 4], colors[(ring + 1) % 4]);
|
||||
let noise = f64::from((x * 7 + y * 13) % 5) - 2.0;
|
||||
let c = [0, 1, 2].map(|k| (a[k] + t * (b[k] - a[k]) + noise).clamp(0.0, 255.0) as u8);
|
||||
[c[0], c[1], c[2], if y + 2 >= h { 160 } else { 255 }]
|
||||
})
|
||||
}
|
||||
|
||||
/// `img` labeled with the palette color nearest to each pixel (none for a transparent one).
|
||||
fn nearest_labels(img: &Raster, palette: &[[u8; 3]]) -> Quantized {
|
||||
let labels = img
|
||||
.rgba
|
||||
.as_chunks::<4>()
|
||||
.0
|
||||
.iter()
|
||||
.map(|p| {
|
||||
let d = |c: &[u8; 3]| (0..3).map(|k| (i32::from(p[k]) - i32::from(c[k])).pow(2)).sum::<i32>();
|
||||
(0..palette.len()).min_by_key(|&i| d(&palette[i])).map_or(TRANSPARENT, |i| i as u16)
|
||||
})
|
||||
.collect();
|
||||
Quantized { labels, palette: palette.to_vec() }
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn passes_over_every_pixel_give_the_same_result_however_the_picture_is_shared_out() {
|
||||
// Odd sizes, so that the last band of rows and the last strip of columns are short.
|
||||
let img = rings(173, 97);
|
||||
let all = img.width as usize * img.height as usize;
|
||||
// One row (or one strip) per share, a few rows per share, and the whole picture as one.
|
||||
let shares = [1, 3 * 173 + 5, all];
|
||||
fn blend_pixels_are_found_the_same_however_the_picture_is_shared_out() {
|
||||
// Slanted stripes of red and blue, blended over three pixels at each edge, with a little
|
||||
// noise and a band of half-transparent pixels along the bottom. Odd sizes, so that the last
|
||||
// band of rows is short.
|
||||
let (w, h) = (173u32, 97u32);
|
||||
let (red, blue) = ([200.0, 30.0, 40.0], [20.0, 60.0, 180.0]);
|
||||
let img = Raster::from_fn(w, h, |x, y| {
|
||||
let s = (x + y / 2) % 24;
|
||||
let t = f64::from(s.saturating_sub(9).min(3)) / 3.0 - f64::from(s.saturating_sub(21).min(3)) / 3.0;
|
||||
let noise = f64::from((x * 7 + y * 13) % 5) - 2.0;
|
||||
let c = [0, 1, 2].map(|k| (red[k] + t * (blue[k] - red[k]) + noise).clamp(0.0, 255.0) as u8);
|
||||
[c[0], c[1], c[2], if y + 2 >= h { 160 } else { 255 }]
|
||||
});
|
||||
let all = w as usize * h as usize;
|
||||
// One row per share, a few rows per share, and the whole picture as one.
|
||||
let opaque: Vec<bool> = img.rgba.as_chunks::<4>().0.iter().map(|p| p[3] >= 128).collect();
|
||||
let own: Vec<Vec<bool>> = shares.iter().map(|&s| own_colours_among_by(&img, &opaque, s)).collect();
|
||||
let own: Vec<Vec<bool>> = [1, 3 * 173 + 5, all].iter().map(|&s| own_colours_among_by(&img, &opaque, s)).collect();
|
||||
assert_eq!(own[0].len(), all);
|
||||
assert!(own[0].iter().any(|&o| !o), "the edges have blend pixels");
|
||||
assert!(own.iter().all(|o| *o == own[0]));
|
||||
// Two colors for each blend, and an olive that the blends lie nearer to than to their sides.
|
||||
let palette = [[200, 30, 40], [20, 60, 180], [240, 210, 50], [25, 25, 25], [120, 120, 60]];
|
||||
let q = nearest_labels(&img, &palette);
|
||||
let own = &own[0];
|
||||
for (name, settle) in [
|
||||
("settled", &(|s| settled_by(q.clone(), &img, own, s)) as &dyn Fn(usize) -> Quantized),
|
||||
("settled_wide", &|s| settled_wide_by(q.clone(), &img, own, false, s)),
|
||||
("settled_wide gray", &|s| settled_wide_by(q.clone(), &img, own, true, s)),
|
||||
] {
|
||||
let out: Vec<Quantized> = shares.iter().map(|&s| settle(s)).collect();
|
||||
assert_ne!(out[0], q, "{name} settles some blend pixels");
|
||||
assert!(out.iter().all(|o| *o == out[0]), "{name}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -424,10 +389,5 @@ mod tests {
|
||||
assert!(over_rows(5, 0, PIXELS_PER_SHARE, |_| vec![0u8]).is_empty());
|
||||
let img = Raster::new(0, u32::MAX, vec![]).unwrap();
|
||||
assert!(own_colours_among(&img, &[]).is_empty());
|
||||
// No strip for each of the columns of a picture with no rows.
|
||||
let img = Raster::new(u32::MAX, 0, vec![]).unwrap();
|
||||
let q = Quantized { labels: vec![], palette: vec![[0, 0, 0], [255, 255, 255]] };
|
||||
assert_eq!(settled_wide_by(q.clone(), &img, &[], false, PIXELS_PER_SHARE), q);
|
||||
assert_eq!(settled_by(q.clone(), &img, &[], PIXELS_PER_SHARE), q);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use super::palette::{Bin, HistKey, Used, centers, median_cut, nearest_lab};
|
||||
use super::{Mode, PIXELS_PER_SHARE, Palette, Quantized, Raster, TRANSPARENT, TraceParams, luma, oklab, over_rows};
|
||||
use crate::layers::parallel_map;
|
||||
use crate::parallel_map;
|
||||
|
||||
/// `q` with its blend pixels settled, and the colours that leaves unused dropped.
|
||||
///
|
||||
@@ -16,19 +16,20 @@ pub(super) fn settled(q: Quantized, img: &Raster, own: &[bool]) -> Quantized {
|
||||
}
|
||||
|
||||
/// [`settled`], with the rows worked on in bands of about `per_share` pixels (see [`over_rows`]).
|
||||
pub(super) fn settled_by(mut q: Quantized, img: &Raster, own: &[bool], per_share: usize) -> Quantized {
|
||||
fn settled_by(mut q: Quantized, img: &Raster, own: &[bool], per_share: usize) -> Quantized {
|
||||
let (w, h) = (img.width as usize, img.height as usize);
|
||||
if q.labels.len() != w * h || own.len() != w * h {
|
||||
return q;
|
||||
}
|
||||
// Each pixel is settled by the labels as they were before any changed, so the bands of rows
|
||||
// are settled apart and the changes made afterwards.
|
||||
// Each pixel is settled by the labels as they were before any label changed. Each band of rows
|
||||
// keeps its changes (see [`Changes`]), and they are made once every band is done.
|
||||
let before = &q.labels;
|
||||
let color = |l: u16| q.palette.get(l as usize).map(|c| c.map(f64::from));
|
||||
let dist = |a: [f64; 3], b: [f64; 3]| (0..3).map(|k| (a[k] - b[k]).powi(2)).sum::<f64>().sqrt();
|
||||
let changes: Vec<(usize, u16)> = over_rows(w, h, per_share, |rows| {
|
||||
let mut changes = vec![];
|
||||
for i in rows.start * w..rows.end * w {
|
||||
let bands: Vec<(usize, Changes)> = over_rows(w, h, per_share, |rows| {
|
||||
let (first, end) = (rows.start * w, rows.end * w);
|
||||
let mut band = Changes::new(end - first);
|
||||
for i in first..end {
|
||||
let mine_l = before[i];
|
||||
if own[i] || mine_l == TRANSPARENT {
|
||||
continue;
|
||||
@@ -67,22 +68,73 @@ pub(super) fn settled_by(mut q: Quantized, img: &Raster, own: &[bool], per_share
|
||||
if let Some((off, to)) = best
|
||||
&& off < 0.5 * dist(p, mine)
|
||||
{
|
||||
changes.push((i, to));
|
||||
band.set(i - first, to, || before.get(first..end).unwrap_or_default().to_vec());
|
||||
}
|
||||
}
|
||||
changes
|
||||
if band.is_empty() { vec![] } else { vec![(first, band)] }
|
||||
});
|
||||
if changes.is_empty() {
|
||||
if bands.is_empty() {
|
||||
return q;
|
||||
}
|
||||
for (i, to) in changes {
|
||||
if let Some(l) = q.labels.get_mut(i) {
|
||||
*l = to;
|
||||
}
|
||||
for (first, band) in bands {
|
||||
band.each(|at, to| {
|
||||
if let Some(l) = q.labels.get_mut(first + at) {
|
||||
*l = to;
|
||||
}
|
||||
});
|
||||
}
|
||||
compact(q.labels, q.palette)
|
||||
}
|
||||
|
||||
/// The labels a pass changes in one part of a picture, `len` pixels long, counting each pixel from
|
||||
/// the part's first. They are kept as pairs of a pixel and its new label until the pairs would take
|
||||
/// more memory than a copy of the part's labels, and as that copy, with the changes made, after
|
||||
/// that. A part takes at most about as much memory as a copy of its labels either way.
|
||||
struct Changes {
|
||||
len: usize,
|
||||
few: Vec<(usize, u16)>,
|
||||
copy: Option<Vec<u16>>,
|
||||
}
|
||||
|
||||
impl Changes {
|
||||
fn new(len: usize) -> Self {
|
||||
Changes { len, few: vec![], copy: None }
|
||||
}
|
||||
|
||||
/// Changes the label of the pixel `at` to `to`. `labels` gives the part's labels as they were
|
||||
/// before any changed, for the copy.
|
||||
fn set(&mut self, at: usize, to: u16, labels: impl FnOnce() -> Vec<u16>) {
|
||||
if let Some(copy) = &mut self.copy {
|
||||
if let Some(l) = copy.get_mut(at) {
|
||||
*l = to;
|
||||
}
|
||||
return;
|
||||
}
|
||||
self.few.push((at, to));
|
||||
if self.few.len() * size_of::<(usize, u16)>() > self.len * size_of::<u16>() {
|
||||
let mut copy = labels();
|
||||
for (at, to) in std::mem::take(&mut self.few) {
|
||||
if let Some(l) = copy.get_mut(at) {
|
||||
*l = to;
|
||||
}
|
||||
}
|
||||
self.copy = Some(copy);
|
||||
}
|
||||
}
|
||||
|
||||
fn is_empty(&self) -> bool {
|
||||
self.few.is_empty() && self.copy.is_none()
|
||||
}
|
||||
|
||||
/// `f` of each changed pixel and its new label (of every pixel of the part, once it is a copy).
|
||||
fn each(self, mut f: impl FnMut(usize, u16)) {
|
||||
match self.copy {
|
||||
Some(copy) => copy.into_iter().enumerate().for_each(|(at, to)| f(at, to)),
|
||||
None => self.few.into_iter().for_each(|(at, to)| f(at, to)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The most colors (Color) or grays (Grayscale) a palette may ask for to have its blend pixels
|
||||
/// settled by [`settled_wide`] rather than [`settled`]. The limited-color presets ask for 3 to 16
|
||||
/// colors.
|
||||
@@ -146,36 +198,40 @@ pub(super) fn settled_wide(q: Quantized, img: &Raster, own: &[bool], gray: bool)
|
||||
|
||||
/// [`settled_wide`], with its strips of columns worked on in groups of about `per_share` pixels
|
||||
/// (at least one strip), spread over the available cores (one thread on the web).
|
||||
pub(super) fn settled_wide_by(mut q: Quantized, img: &Raster, own: &[bool], gray: bool, per_share: usize) -> Quantized {
|
||||
fn settled_wide_by(mut q: Quantized, img: &Raster, own: &[bool], gray: bool, per_share: usize) -> Quantized {
|
||||
let (w, h) = (img.width as usize, img.height as usize);
|
||||
if q.labels.len() != w * h || own.len() != w * h || w == 0 || h == 0 {
|
||||
return q;
|
||||
}
|
||||
// Each pixel is settled by the labels as they were before any changed, so the strips are
|
||||
// settled apart and the changes made afterwards. Strips are grouped so that each group holds
|
||||
// about `per_share` pixels.
|
||||
// Each pixel is settled by the labels as they were before any label changed. Each group of
|
||||
// strips keeps its changes (see [`Changes`]), and they are made once every group is done.
|
||||
let strip = BLEND_STRIP.min(w).max(1);
|
||||
let per_group = (per_share / (strip * h).max(1)).max(1);
|
||||
let starts: Vec<usize> = (0..w).step_by(strip).collect();
|
||||
let groups: Vec<&[usize]> = starts.chunks(per_group).collect();
|
||||
let settle = |starts: &&[usize]| settled_wide_strips(&q, img, own, gray, starts);
|
||||
let changes: Vec<(usize, u16)> =
|
||||
if groups.len() <= 1 { groups.iter().flat_map(settle).collect() } else { parallel_map(&groups, settle).into_iter().flatten().collect() };
|
||||
if changes.is_empty() {
|
||||
let cols = strip.saturating_mul(per_group);
|
||||
let groups: Vec<std::ops::Range<usize>> = (0..w).step_by(cols).map(|x| x..x.saturating_add(cols).min(w)).collect();
|
||||
let settle = |cols: &std::ops::Range<usize>| (cols.clone(), settled_wide_strips(&q, img, own, gray, cols.clone()));
|
||||
let mut settled: Vec<(std::ops::Range<usize>, Changes)> =
|
||||
if groups.len() <= 1 { groups.iter().map(settle).collect() } else { parallel_map(&groups, settle) };
|
||||
settled.retain(|(_, changes)| !changes.is_empty());
|
||||
if settled.is_empty() {
|
||||
return q;
|
||||
}
|
||||
for (i, to) in changes {
|
||||
if let Some(l) = q.labels.get_mut(i) {
|
||||
*l = to;
|
||||
}
|
||||
for (cols, group) in settled {
|
||||
let gw = cols.len().max(1);
|
||||
group.each(|at, to| {
|
||||
if let Some(l) = q.labels.get_mut((at / gw) * w + cols.start + at % gw) {
|
||||
*l = to;
|
||||
}
|
||||
});
|
||||
}
|
||||
compact(q.labels, q.palette)
|
||||
}
|
||||
|
||||
/// The labels that [`settled_wide`] changes in the strips of [`BLEND_STRIP`] columns that start at
|
||||
/// the columns `starts`, with the pixel each one is at.
|
||||
fn settled_wide_strips(q: &Quantized, img: &Raster, own: &[bool], gray: bool, starts: &[usize]) -> Vec<(usize, u16)> {
|
||||
/// The labels that [`settled_wide`] changes in the columns `cols` (whole strips of [`BLEND_STRIP`]
|
||||
/// columns, or the picture's last columns), each pixel counted row by row across those columns.
|
||||
fn settled_wide_strips(q: &Quantized, img: &Raster, own: &[bool], gray: bool, cols: std::ops::Range<usize>) -> Changes {
|
||||
let (w, h) = (img.width as usize, img.height as usize);
|
||||
let gw = cols.len();
|
||||
let before = &q.labels;
|
||||
let colors: Vec<[f64; 3]> = q.palette.iter().map(|c| c.map(f64::from)).collect();
|
||||
let dist2 = |a: [f64; 3], b: [f64; 3]| (0..3).map(|k| (a[k] - b[k]).powi(2)).sum::<f64>();
|
||||
@@ -186,7 +242,7 @@ fn settled_wide_strips(q: &Quantized, img: &Raster, own: &[bool], gray: bool, st
|
||||
let span = 2 * BLEND_REACH + 1;
|
||||
let strip = BLEND_STRIP.min(w).max(1);
|
||||
let pixel = |p: &[u8]| if gray { [f64::from(luma([p[0], p[1], p[2], p[3]])); 3] } else { [f64::from(p[0]), f64::from(p[1]), f64::from(p[2])] };
|
||||
let mut changes = vec![];
|
||||
let mut settled = Changes::new(gw * h);
|
||||
let mut rows = vec![0u64; span * strip * words];
|
||||
let mut counts = vec![0u32; q.palette.len()];
|
||||
let mut window = vec![0u64; words];
|
||||
@@ -231,8 +287,8 @@ fn settled_wide_strips(q: &Quantized, img: &Raster, own: &[bool], gray: bool, st
|
||||
};
|
||||
let mut near: Vec<u16> = Vec::new();
|
||||
let mut by_distance: Vec<(f64, u16)> = Vec::new();
|
||||
for (x0, y) in starts.iter().flat_map(|&x0| (0..h).map(move |y| (x0, y))) {
|
||||
let x1 = (x0 + strip).min(w);
|
||||
for (x0, y) in cols.clone().step_by(strip).flat_map(|x0| (0..h).map(move |y| (x0, y))) {
|
||||
let x1 = (x0 + strip).min(cols.end);
|
||||
if y == 0 {
|
||||
for yy in 0..BLEND_REACH.min(h) {
|
||||
row_colors(yy, x0, x1, &mut rows);
|
||||
@@ -314,11 +370,13 @@ fn settled_wide_strips(q: &Quantized, img: &Raster, own: &[bool], gray: bool, st
|
||||
&& !pair.contains(&mine_l)
|
||||
&& (off2 < BETTER_FIT * dist2(p, mine) || (between && off2 < ON_THE_LINE * ON_THE_LINE * apart2))
|
||||
{
|
||||
changes.push((i, to));
|
||||
settled.set(y * gw + x - cols.start, to, || {
|
||||
(0..h).flat_map(|yy| before.get(yy * w + cols.start..yy * w + cols.end).unwrap_or_default()).copied().collect()
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
changes
|
||||
settled
|
||||
}
|
||||
|
||||
/// Whether `img` is continuous tone (a photo, a gradient) rather than flat art, however noisy
|
||||
@@ -708,6 +766,74 @@ fn edge_blend_of(own: [f64; 3], beside: [u8; 3], alpha: u8) -> bool {
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// Rings of four colors around a point off the center, blended over three pixels at each edge,
|
||||
/// with a little noise, and a band of half-transparent pixels along the bottom.
|
||||
fn rings(w: u32, h: u32) -> Raster {
|
||||
let colors = [[200.0, 30.0, 40.0], [20.0, 60.0, 180.0], [240.0, 210.0, 50.0], [25.0, 25.0, 25.0]];
|
||||
Raster::from_fn(w, h, |x, y| {
|
||||
let r = (f64::from(x) - 0.4 * f64::from(w)).hypot(f64::from(y) - 0.55 * f64::from(h)) / 9.0;
|
||||
let (ring, t) = (r.floor() as usize, ((r.fract() - 0.8) / 0.2).clamp(0.0, 1.0));
|
||||
let (a, b) = (colors[ring % 4], colors[(ring + 1) % 4]);
|
||||
let noise = f64::from((x * 7 + y * 13) % 5) - 2.0;
|
||||
let c = [0, 1, 2].map(|k| (a[k] + t * (b[k] - a[k]) + noise).clamp(0.0, 255.0) as u8);
|
||||
[c[0], c[1], c[2], if y + 2 >= h { 160 } else { 255 }]
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn changes_are_copied_out_once_their_pairs_would_take_more_memory_than_the_labels() {
|
||||
let labels: Vec<u16> = (0..64).collect();
|
||||
let pairs = labels.len() * size_of::<u16>() / size_of::<(usize, u16)>();
|
||||
let mut changes = Changes::new(labels.len());
|
||||
for at in 0..pairs {
|
||||
changes.set(at * 3, 99, || labels.clone());
|
||||
}
|
||||
assert!(changes.copy.is_none());
|
||||
changes.set(63, 98, || labels.clone());
|
||||
assert!(changes.few.is_empty() && changes.copy.is_some());
|
||||
changes.set(1, 97, || labels.clone());
|
||||
let mut out = labels.clone();
|
||||
changes.each(|at, to| out[at] = to);
|
||||
let mut expected = labels.clone();
|
||||
(0..pairs).for_each(|at| expected[at * 3] = 99);
|
||||
(expected[63], expected[1]) = (98, 97);
|
||||
assert_eq!(out, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn blend_pixels_settle_the_same_however_the_picture_is_shared_out() {
|
||||
// Odd sizes, so that the last band of rows and the last strip of columns are short.
|
||||
let img = rings(173, 97);
|
||||
let all = img.width as usize * img.height as usize;
|
||||
let opaque: Vec<bool> = img.rgba.as_chunks::<4>().0.iter().map(|p| p[3] >= 128).collect();
|
||||
let own = super::super::own_colours_among(&img, &opaque);
|
||||
// Two colors for each blend, and an olive that the blends lie nearer to than to their sides,
|
||||
// each pixel labeled with the nearest of them (none for a transparent one).
|
||||
let palette = vec![[200, 30, 40], [20, 60, 180], [240, 210, 50], [25, 25, 25], [120, 120, 60]];
|
||||
let labels = img
|
||||
.rgba
|
||||
.as_chunks::<4>()
|
||||
.0
|
||||
.iter()
|
||||
.map(|p| {
|
||||
let d = |c: &[u8; 3]| (0..3).map(|k| (i32::from(p[k]) - i32::from(c[k])).pow(2)).sum::<i32>();
|
||||
(0..palette.len()).min_by_key(|&i| d(&palette[i])).map_or(TRANSPARENT, |i| i as u16)
|
||||
})
|
||||
.collect();
|
||||
let q = Quantized { labels, palette };
|
||||
// One row (or one strip) per share, a few rows per share, and the whole picture as one.
|
||||
let shares = [1, 3 * 173 + 5, all];
|
||||
for (name, settle) in [
|
||||
("settled", &(|s| settled_by(q.clone(), &img, &own, s)) as &dyn Fn(usize) -> Quantized),
|
||||
("settled_wide", &|s| settled_wide_by(q.clone(), &img, &own, false, s)),
|
||||
("settled_wide gray", &|s| settled_wide_by(q.clone(), &img, &own, true, s)),
|
||||
] {
|
||||
let out: Vec<Quantized> = shares.iter().map(|&s| settle(s)).collect();
|
||||
assert_ne!(out[0], q, "{name} settles some blend pixels");
|
||||
assert!(out.iter().all(|o| *o == out[0]), "{name}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_gray_blend_takes_the_closer_of_two_pairs_it_lies_between_despite_rounding() {
|
||||
// Gray 105 in the middle, with own pixels of gray 50 to its left, 200 to its right and black
|
||||
|
||||
Reference in New Issue
Block a user