340 lines
11 KiB
Rust
340 lines
11 KiB
Rust
//! Colour reduction and noise removal.
|
|
|
|
use crate::{Mode, Raster, TraceParams};
|
|
|
|
/// Label of a transparent (untraced) pixel.
|
|
pub const TRANSPARENT: u16 = u16::MAX;
|
|
|
|
/// A palette-indexed image.
|
|
#[derive(Clone, Debug, PartialEq)]
|
|
pub struct Quantized {
|
|
/// One palette index per pixel (row-major), or [`TRANSPARENT`].
|
|
pub labels: Vec<u16>,
|
|
pub palette: Vec<[u8; 3]>,
|
|
}
|
|
|
|
fn luma(p: [u8; 4]) -> u8 {
|
|
((p[0] as u32 * 299 + p[1] as u32 * 587 + p[2] as u32 * 114 + 500) / 1000) as u8
|
|
}
|
|
|
|
/// Reduce `img` to a palette according to `params.mode`.
|
|
pub fn quantize(img: &Raster, params: &TraceParams) -> Quantized {
|
|
let px = img.rgba.as_chunks::<4>().0.iter().map(|c| [c[0], c[1], c[2], c[3]]);
|
|
match params.mode {
|
|
Mode::BlackAndWhite => {
|
|
let t = params.threshold;
|
|
let labels = px
|
|
.map(|p| {
|
|
if p[3] < 128 {
|
|
TRANSPARENT
|
|
} else if luma(p) < t {
|
|
1
|
|
} else {
|
|
0
|
|
}
|
|
})
|
|
.collect();
|
|
Quantized { labels, palette: vec![[255, 255, 255], [0, 0, 0]] }
|
|
}
|
|
Mode::Grayscale => {
|
|
let mut hist = [0u64; 256];
|
|
for p in img.rgba.as_chunks::<4>().0 {
|
|
if p[3] >= 128 {
|
|
hist[luma([p[0], p[1], p[2], p[3]]) as usize] += 1;
|
|
}
|
|
}
|
|
let levels = kmeans_1d(&hist, params.colors.clamp(2, 256) as usize);
|
|
let mut lut = [0u16; 256];
|
|
for (v, slot) in lut.iter_mut().enumerate() {
|
|
*slot = nearest_1d(&levels, v as f64) as u16;
|
|
}
|
|
let labels = px.map(|p| if p[3] < 128 { TRANSPARENT } else { lut[luma(p) as usize] }).collect();
|
|
let palette = levels.iter().map(|&l| {
|
|
let g = l.round().clamp(0.0, 255.0) as u8;
|
|
[g, g, g]
|
|
});
|
|
compact(labels, palette.collect())
|
|
}
|
|
Mode::Color => {
|
|
// 15-bit colour histogram with per-bin sums (exact bin means).
|
|
let mut bins: Vec<Bin> = vec![Bin::default(); 1 << 15];
|
|
for p in img.rgba.as_chunks::<4>().0 {
|
|
if p[3] >= 128 {
|
|
let b = &mut bins[key(p[0], p[1], p[2])];
|
|
b.n += 1;
|
|
b.sum[0] += p[0] as u64;
|
|
b.sum[1] += p[1] as u64;
|
|
b.sum[2] += p[2] as u64;
|
|
}
|
|
}
|
|
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 k = params.colors.clamp(2, 256) as usize;
|
|
let centers = kmeans_rgb(&used, k);
|
|
let mut lut = vec![0u16; 1 << 15];
|
|
for (i, c, _) in &used {
|
|
lut[*i] = nearest_rgb(¢ers, *c) as u16;
|
|
}
|
|
let labels = px.map(|p| if p[3] < 128 { TRANSPARENT } else { lut[key(p[0], p[1], p[2])] }).collect();
|
|
let palette = centers.iter().map(|c| [c[0].round() as u8, c[1].round() as u8, c[2].round() as u8]).collect();
|
|
compact(labels, palette)
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Drop unused palette entries and merge duplicates.
|
|
fn compact(mut labels: Vec<u16>, palette: Vec<[u8; 3]>) -> Quantized {
|
|
let mut used = vec![false; palette.len()];
|
|
for &l in &labels {
|
|
if l != TRANSPARENT {
|
|
used[l as usize] = true;
|
|
}
|
|
}
|
|
let mut remap = vec![0u16; palette.len()];
|
|
let mut out: Vec<[u8; 3]> = vec![];
|
|
for (i, c) in palette.iter().enumerate() {
|
|
if !used[i] {
|
|
continue;
|
|
}
|
|
remap[i] = match out.iter().position(|o| o == c) {
|
|
Some(j) => j as u16,
|
|
None => {
|
|
out.push(*c);
|
|
(out.len() - 1) as u16
|
|
}
|
|
};
|
|
}
|
|
for l in &mut labels {
|
|
if *l != TRANSPARENT {
|
|
*l = remap[*l as usize];
|
|
}
|
|
}
|
|
Quantized { labels, palette: out }
|
|
}
|
|
|
|
#[derive(Clone, Copy, Default)]
|
|
struct Bin {
|
|
n: u64,
|
|
sum: [u64; 3],
|
|
}
|
|
|
|
impl Bin {
|
|
fn mean(&self) -> [f64; 3] {
|
|
let n = self.n.max(1) as f64;
|
|
[self.sum[0] as f64 / n, self.sum[1] as f64 / n, self.sum[2] as f64 / n]
|
|
}
|
|
}
|
|
|
|
fn key(r: u8, g: u8, b: u8) -> usize {
|
|
((r as usize >> 3) << 10) | ((g as usize >> 3) << 5) | (b as usize >> 3)
|
|
}
|
|
|
|
fn nearest_1d(levels: &[f64], v: f64) -> usize {
|
|
let mut best = (f64::INFINITY, 0);
|
|
for (i, &l) in levels.iter().enumerate() {
|
|
let d = (l - v).abs();
|
|
if d < best.0 {
|
|
best = (d, i);
|
|
}
|
|
}
|
|
best.1
|
|
}
|
|
|
|
/// 1-D k-means over a 256-bin histogram (Lloyd iterations, evenly spaced start).
|
|
fn kmeans_1d(hist: &[u64; 256], k: usize) -> Vec<f64> {
|
|
let lo = hist.iter().position(|&n| n > 0).unwrap_or(0) as f64;
|
|
let hi = hist.iter().rposition(|&n| n > 0).unwrap_or(255) as f64;
|
|
let mut levels: Vec<f64> = (0..k).map(|i| if k == 1 { lo } else { lo + (hi - lo) * i as f64 / (k - 1) as f64 }).collect();
|
|
for _ in 0..20 {
|
|
let mut sum = vec![0.0; k];
|
|
let mut cnt = vec![0.0; k];
|
|
for (v, &n) in hist.iter().enumerate() {
|
|
if n > 0 {
|
|
let j = nearest_1d(&levels, v as f64);
|
|
sum[j] += v as f64 * n as f64;
|
|
cnt[j] += n as f64;
|
|
}
|
|
}
|
|
let mut moved = false;
|
|
for j in 0..k {
|
|
if cnt[j] > 0.0 {
|
|
let m = sum[j] / cnt[j];
|
|
moved |= (m - levels[j]).abs() > 0.01;
|
|
levels[j] = m;
|
|
}
|
|
}
|
|
if !moved {
|
|
break;
|
|
}
|
|
}
|
|
levels
|
|
}
|
|
|
|
fn dist2(a: [f64; 3], b: [f64; 3]) -> f64 {
|
|
// Slightly perceptual weights.
|
|
let (dr, dg, db) = (a[0] - b[0], a[1] - b[1], a[2] - b[2]);
|
|
2.0 * dr * dr + 4.0 * dg * dg + 3.0 * db * db
|
|
}
|
|
|
|
fn nearest_rgb(centers: &[[f64; 3]], c: [f64; 3]) -> usize {
|
|
let mut best = (f64::INFINITY, 0);
|
|
for (i, &m) in centers.iter().enumerate() {
|
|
let d = dist2(m, c);
|
|
if d < best.0 {
|
|
best = (d, i);
|
|
}
|
|
}
|
|
best.1
|
|
}
|
|
|
|
/// Median cut for the initial palette, then weighted k-means refinement over histogram bins.
|
|
fn kmeans_rgb(used: &[(usize, [f64; 3], f64)], k: usize) -> Vec<[f64; 3]> {
|
|
if used.is_empty() {
|
|
return vec![[255.0; 3]];
|
|
}
|
|
// Median cut: repeatedly split the box with the largest weighted extent.
|
|
let mut boxes: Vec<Vec<usize>> = vec![(0..used.len()).collect()];
|
|
while boxes.len() < k {
|
|
let mut best: Option<(f64, usize, usize)> = None;
|
|
for (bi, b) in boxes.iter().enumerate() {
|
|
if b.len() < 2 {
|
|
continue;
|
|
}
|
|
let (mut lo, mut hi) = ([f64::INFINITY; 3], [f64::NEG_INFINITY; 3]);
|
|
let mut wsum = 0.0;
|
|
for &i in b {
|
|
for ch in 0..3 {
|
|
lo[ch] = lo[ch].min(used[i].1[ch]);
|
|
hi[ch] = hi[ch].max(used[i].1[ch]);
|
|
}
|
|
wsum += used[i].2;
|
|
}
|
|
let (ch, range) = (0..3).map(|c| (c, hi[c] - lo[c])).fold((0, -1.0), |a, b| if b.1 > a.1 { b } else { a });
|
|
if range <= 0.0 {
|
|
continue;
|
|
}
|
|
let score = range * wsum.sqrt();
|
|
if best.is_none_or(|x| score > x.0) {
|
|
best = Some((score, bi, ch));
|
|
}
|
|
}
|
|
let Some((_, bi, ch)) = best else { break };
|
|
let mut b = boxes.swap_remove(bi);
|
|
b.sort_by(|x, y| used[*x].1[ch].total_cmp(&used[*y].1[ch]));
|
|
let total: f64 = b.iter().map(|&i| used[i].2).sum();
|
|
let mut acc = 0.0;
|
|
let mut cut = 1;
|
|
for (j, &i) in b.iter().enumerate() {
|
|
acc += used[i].2;
|
|
if acc >= total / 2.0 {
|
|
cut = (j + 1).clamp(1, b.len() - 1);
|
|
break;
|
|
}
|
|
}
|
|
let tail = b.split_off(cut);
|
|
boxes.push(b);
|
|
boxes.push(tail);
|
|
}
|
|
let mut centers: Vec<[f64; 3]> = boxes
|
|
.iter()
|
|
.map(|b| {
|
|
let mut s = [0.0; 3];
|
|
let mut w = 0.0;
|
|
for &i in b {
|
|
for (acc, v) in s.iter_mut().zip(used[i].1) {
|
|
*acc += v * used[i].2;
|
|
}
|
|
w += used[i].2;
|
|
}
|
|
[s[0] / w, s[1] / w, s[2] / w]
|
|
})
|
|
.collect();
|
|
// Lloyd refinement.
|
|
for _ in 0..12 {
|
|
let mut sum = vec![[0.0; 3]; centers.len()];
|
|
let mut cnt = vec![0.0; centers.len()];
|
|
for (_, c, n) in used {
|
|
let j = nearest_rgb(¢ers, *c);
|
|
for ch in 0..3 {
|
|
sum[j][ch] += c[ch] * n;
|
|
}
|
|
cnt[j] += n;
|
|
}
|
|
let mut moved = 0.0f64;
|
|
for j in 0..centers.len() {
|
|
if cnt[j] > 0.0 {
|
|
let m = [sum[j][0] / cnt[j], sum[j][1] / cnt[j], sum[j][2] / cnt[j]];
|
|
moved = moved.max(dist2(m, centers[j]));
|
|
centers[j] = m;
|
|
}
|
|
}
|
|
if moved < 0.25 {
|
|
break;
|
|
}
|
|
}
|
|
centers
|
|
}
|
|
|
|
/// Merge 4-connected same-label components smaller than `min_area` pixels into their most common
|
|
/// neighbouring label. Transparent pixels are never relabelled.
|
|
pub fn denoise(labels: &mut [u16], w: usize, h: usize, min_area: usize) {
|
|
if min_area <= 1 || w == 0 || h == 0 {
|
|
return;
|
|
}
|
|
let mut seen = vec![false; w * h];
|
|
let mut stack: Vec<usize> = Vec::new();
|
|
let mut comp: Vec<usize> = Vec::new();
|
|
let mut neigh: Vec<(u16, u32)> = Vec::new();
|
|
for start in 0..w * h {
|
|
if seen[start] || labels[start] == TRANSPARENT {
|
|
continue;
|
|
}
|
|
let l = labels[start];
|
|
comp.clear();
|
|
neigh.clear();
|
|
stack.push(start);
|
|
seen[start] = true;
|
|
let mut small = true;
|
|
while let Some(i) = stack.pop() {
|
|
if small {
|
|
comp.push(i);
|
|
if comp.len() >= min_area {
|
|
small = false;
|
|
}
|
|
}
|
|
let (x, y) = (i % w, i / w);
|
|
let mut visit = |j: usize| {
|
|
let lj = labels[j];
|
|
if lj == l {
|
|
if !seen[j] {
|
|
seen[j] = true;
|
|
stack.push(j);
|
|
}
|
|
} else if lj != TRANSPARENT {
|
|
match neigh.iter_mut().find(|(n, _)| *n == lj) {
|
|
Some(e) => e.1 += 1,
|
|
None => neigh.push((lj, 1)),
|
|
}
|
|
}
|
|
};
|
|
if x > 0 {
|
|
visit(i - 1);
|
|
}
|
|
if x + 1 < w {
|
|
visit(i + 1);
|
|
}
|
|
if y > 0 {
|
|
visit(i - w);
|
|
}
|
|
if y + 1 < h {
|
|
visit(i + w);
|
|
}
|
|
}
|
|
if small && let Some(&(to, _)) = neigh.iter().max_by_key(|(n, c)| (*c, u16::MAX - *n)) {
|
|
for &i in &comp {
|
|
labels[i] = to;
|
|
}
|
|
}
|
|
}
|
|
}
|