mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-02 22:20:50 -06:00
Trace: colour mode keeps close dark tones apart
The colour histogram used the top 5 bits of each channel, so tones less
than 8 values apart always shared a bin and could never get their own
palette entry, however many colours were asked for. A dark, low-contrast
photo (a book-cover texture with RGB 33..66) traced to 5 colours when 12
were requested, and eight grey stripes 32..39 traced to one:
vectorcraft-cli run --in stripes-32-39.png --cmd select.all \
--cmd imageTrace.makeAndExpand \
--params '{"params":{"mode":"color","colors":8,"noise":0,"method":"overlapping"}}'
-> {"anchors":4,"colors":1,"id":4,"paths":1}
The histogram keeps its 32x32x32 bins, now fitted to each channel's range
among the opaque pixels: a channel spanning at most 32 values keeps every
value apart, a full-range channel keeps the old 5-bit bins.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
committed by
bflatastic
co-authored by
Claude Opus 5.5
parent
44f3141c3d
commit
4268b8edd5
@@ -57,11 +57,13 @@ pub fn quantize(img: &Raster, params: &TraceParams) -> Quantized {
|
||||
}
|
||||
// Logo mode traces without quantising; asked directly, it quantises like Color.
|
||||
Mode::Color | Mode::Logo => {
|
||||
// 15-bit colour histogram with per-bin sums (exact bin means).
|
||||
// 15-bit colour histogram with per-bin sums (exact bin means), fitted to the image's
|
||||
// range per channel so that close tones (a dark, low-contrast photo) stay apart.
|
||||
let hk = HistKey::new(img);
|
||||
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])];
|
||||
let b = &mut bins[hk.key(p[0], p[1], p[2])];
|
||||
b.n += 1;
|
||||
b.sum[0] += p[0] as u64;
|
||||
b.sum[1] += p[1] as u64;
|
||||
@@ -74,7 +76,7 @@ pub fn quantize(img: &Raster, params: &TraceParams) -> Quantized {
|
||||
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 labels = px.map(|p| if p[3] < 128 { TRANSPARENT } else { lut[hk.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)
|
||||
}
|
||||
@@ -124,8 +126,45 @@ impl Bin {
|
||||
}
|
||||
}
|
||||
|
||||
fn key(r: u8, g: u8, b: u8) -> usize {
|
||||
((r as usize >> 3) << 10) | ((g as usize >> 3) << 5) | (b as usize >> 3)
|
||||
/// The colour histogram's bin index: 5 bits per channel over that channel's range in the image.
|
||||
/// A channel spanning at most 32 values keeps every value apart; a full-range one keeps the
|
||||
/// plain top 5 bits.
|
||||
struct HistKey {
|
||||
lo: [u8; 3],
|
||||
shift: [u8; 3],
|
||||
}
|
||||
|
||||
impl HistKey {
|
||||
/// Fit the bins to the opaque pixels (alpha >= 128) of `img`.
|
||||
fn new(img: &Raster) -> Self {
|
||||
let (mut lo, mut hi) = ([u8::MAX; 3], [0u8; 3]);
|
||||
for p in img.rgba.as_chunks::<4>().0 {
|
||||
if p[3] >= 128 {
|
||||
for ch in 0..3 {
|
||||
lo[ch] = lo[ch].min(p[ch]);
|
||||
hi[ch] = hi[ch].max(p[ch]);
|
||||
}
|
||||
}
|
||||
}
|
||||
let mut shift = [0u8; 3];
|
||||
for ch in 0..3 {
|
||||
if lo[ch] > hi[ch] {
|
||||
// No opaque pixel: nothing is looked up.
|
||||
lo[ch] = 0;
|
||||
continue;
|
||||
}
|
||||
let range = hi[ch] - lo[ch];
|
||||
while shift[ch] < 3 && (range >> shift[ch]) > 31 {
|
||||
shift[ch] += 1;
|
||||
}
|
||||
}
|
||||
Self { lo, shift }
|
||||
}
|
||||
|
||||
fn key(&self, r: u8, g: u8, b: u8) -> usize {
|
||||
let c = |v: u8, ch: usize| (v.saturating_sub(self.lo[ch]) >> self.shift[ch]).min(31) as usize;
|
||||
(c(r, 0) << 10) | (c(g, 1) << 5) | c(b, 2)
|
||||
}
|
||||
}
|
||||
|
||||
fn nearest_1d(levels: &[f64], v: f64) -> usize {
|
||||
|
||||
Reference in New Issue
Block a user