Image Trace: blend settling caps its pairs of label changes at the size of a label copy

The pairs a band or group of strips keeps for its changed labels now grow
by doubling only up to the count that fits in a copy of its labels, so they
never take more memory than that copy. The next change moves them into the
copy.

- Tests: most of a share changing gives the same labels as when few change,
  for both settle passes and every share size (the copy path); both settle
  passes return a picture with no pixels unchanged.
This commit is contained in:
dotCooCoo
2026-11-14 08:42:53 -08:00
parent 4aa2147fba
commit 2046c646fa
2 changed files with 54 additions and 11 deletions
+2 -2
View File
@@ -374,9 +374,9 @@ mod tests {
[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>> = [1, 3 * 173 + 5, all].iter().map(|&s| own_colours_among_by(&img, &opaque, s)).collect();
// One row per share, a few rows per share, and the whole picture as one.
let own: Vec<Vec<bool>> = [1, 3 * w as usize + 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]));
+52 -9
View File
@@ -89,7 +89,8 @@ fn settled_by(mut q: Quantized, img: &Raster, own: &[bool], per_share: usize) ->
/// 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.
/// that. The pairs take at most as much memory as the copy, and a part holds both only while it
/// moves the pairs into the copy.
struct Changes {
len: usize,
few: Vec<(usize, u16)>,
@@ -110,16 +111,23 @@ impl Changes {
}
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;
}
// The most pairs that take no more memory than a copy of the part's labels. The pairs grow
// by doubling up to that many, and no further.
let most = self.len * size_of::<u16>() / size_of::<(usize, u16)>();
if self.few.len() < most {
if self.few.len() == self.few.capacity() {
self.few.reserve_exact(self.few.capacity().max(4).min(most - self.few.len()));
}
self.copy = Some(copy);
self.few.push((at, to));
return;
}
let mut copy = labels();
for (at, to) in std::mem::take(&mut self.few).into_iter().chain([(at, to)]) {
if let Some(l) = copy.get_mut(at) {
*l = to;
}
}
self.copy = Some(copy);
}
fn is_empty(&self) -> bool {
@@ -789,6 +797,7 @@ mod tests {
changes.set(at * 3, 99, || labels.clone());
}
assert!(changes.copy.is_none());
assert!(changes.few.capacity() <= pairs, "the pairs take no more memory than a copy of the labels");
changes.set(63, 98, || labels.clone());
assert!(changes.few.is_empty() && changes.copy.is_some());
changes.set(1, 97, || labels.clone());
@@ -834,6 +843,40 @@ mod tests {
}
}
#[test]
fn blend_pixels_settle_the_same_when_most_of_a_share_changes() {
// Columns of red and blue own pixels, two blend pixels between each pair: one nearer to red
// and one nearer to blue, both labeled with an olive. Half the pixels of each band of rows and
// of each group of strips change, more than the eighth at which a share's changes are copied
// out (see [`Changes`]). 44 columns, so that the last strip of 7 is short.
let (w, h) = (44u32, 5u32);
let (red, blue) = ([200u8, 30, 40], [20u8, 60, 180]);
let mix = |t: f64| [0, 1, 2].map(|k| (f64::from(red[k]) + t * (f64::from(blue[k]) - f64::from(red[k]))).round() as u8);
let img = Raster::from_fn(w, h, |x, _| {
let c = [red, mix(0.3), mix(0.7), blue][x as usize % 4];
[c[0], c[1], c[2], 255]
});
let own: Vec<bool> = (0..w * h).map(|i| matches!(i % w % 4, 0 | 3)).collect();
let labels = (0..w * h).map(|i| [0, 2, 2, 1][(i % w % 4) as usize]).collect();
let q = Quantized { labels, palette: vec![red, blue, [120, 120, 60]] };
let expected = Quantized { labels: (0..w * h).map(|i| [0, 0, 1, 1][(i % w % 4) as usize]).collect(), palette: vec![red, blue] };
let all = (w * h) as usize;
for s in [1, 3 * 7 * h as usize, all] {
assert_eq!(settled_by(q.clone(), &img, &own, s), expected, "settled, {s} pixels per share");
assert_eq!(settled_wide_by(q.clone(), &img, &own, false, s), expected, "settled_wide, {s} pixels per share");
}
}
#[test]
fn a_picture_with_no_pixels_settles_to_itself() {
let q = Quantized { labels: vec![], palette: vec![[0, 0, 0], [255, 255, 255]] };
for (w, h) in [(u32::MAX, 0), (0, u32::MAX)] {
let img = Raster::new(w, h, vec![]).unwrap();
assert_eq!(settled_by(q.clone(), &img, &[], PIXELS_PER_SHARE), q, "settled, {w} x {h}");
assert_eq!(settled_wide_by(q.clone(), &img, &[], false, PIXELS_PER_SHARE), q, "settled_wide, {w} x {h}");
}
}
#[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