├── .github └── workflows │ └── rust.yml ├── .gitignore ├── CHANGELOG.md ├── Cargo.toml ├── LICENSE ├── README.md ├── examples └── quantize.rs └── src ├── lib.rs └── math.rs /.github/workflows/rust.yml: -------------------------------------------------------------------------------- 1 | name: Rust CI 2 | on: 3 | push: 4 | branches: [ master ] 5 | pull_request: 6 | branches: [ master ] 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | strategy: 11 | matrix: 12 | rust: ["1.36.0", stable, beta, nightly] 13 | command: [build, test] 14 | steps: 15 | - uses: actions/checkout@v2 16 | - run: rustup default ${{ matrix.rust }} 17 | - name: build 18 | run: > 19 | cargo build --verbose 20 | - name: test 21 | if: ${{ matrix.rust != '1.36.0' }} 22 | run: > 23 | cargo test --tests --benches 24 | # TODO: add criterion benchmarks? 25 | rustfmt: 26 | runs-on: ubuntu-latest 27 | steps: 28 | - uses: actions/checkout@v2 29 | - uses: actions-rs/toolchain@v1 30 | with: 31 | toolchain: stable 32 | override: true 33 | components: rustfmt 34 | - name: Run rustfmt check 35 | uses: actions-rs/cargo@v1 36 | with: 37 | command: fmt 38 | args: -- --check 39 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | Cargo.lock 3 | .* 4 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 1.1.0 2 | 3 | - Unify with `image::math::nq` as per https://github.com/image-rs/image/issues/1338 (https://github.com/image-rs/color_quant/pull/10) 4 | - A new method `lookup` from `image::math::nq` is added 5 | - More references in docs 6 | - Some style improvements and better names for functions borrowed from `image::math::nq` 7 | - Replace the internal `clamp!` macro with the `clamp` function (https://github.com/image-rs/color_quant/pull/8) 8 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "color_quant" 3 | license = "MIT" 4 | version = "1.1.0" 5 | authors = ["nwin "] 6 | readme = "README.md" 7 | description = "Color quantization library to reduce n colors to 256 colors." 8 | repository = "https://github.com/image-rs/color_quant.git" 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 PistonDevelopers 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Color quantization library 2 | This library provides a color quantizer based on the [NEUQUANT](https://scientificgems.wordpress.com/stuff/neuquant-fast-high-quality-image-quantization/) 3 | quantization algorithm by Anthony Dekker. 4 | 5 | ### Usage 6 | 7 | let data = vec![0; 40]; 8 | let nq = color_quant::NeuQuant::new(10, 256, &data); 9 | let indixes: Vec = data.chunks(4).map(|pix| nq.index_of(pix) as u8).collect(); 10 | let color_map = nq.color_map_rgba(); 11 | 12 | -------------------------------------------------------------------------------- /examples/quantize.rs: -------------------------------------------------------------------------------- 1 | #![feature(array_chunks)] 2 | 3 | extern crate color_quant; 4 | 5 | use color_quant::NeuQuant; 6 | use std::fs::write; 7 | 8 | fn main() { 9 | let header = b"P6\n256\n256\n255\n"; 10 | let mut pixels = vec![0; 4 * 256 * 256]; 11 | let mut r: u8 = 0; 12 | let mut g: u8 = 0; 13 | let mut b: u8 = 0; 14 | 15 | for (i, p) in pixels.iter_mut().enumerate() { 16 | match i % 4 { 17 | 0 => *p = r, 18 | 1 => *p = g, 19 | 2 => { 20 | *p = b; 21 | if let Some(next_r) = r.checked_add(1) { 22 | r = next_r; 23 | continue; 24 | } 25 | r = 0; 26 | if let Some(next_g) = g.checked_add(1) { 27 | g = next_g; 28 | continue; 29 | } 30 | g = 0; 31 | b += 1 32 | } 33 | 3 => *p = 255, 34 | _ => unreachable!(), 35 | } 36 | } 37 | let raw = &header 38 | .into_iter() 39 | .chain( 40 | pixels 41 | .array_chunks::<4>() 42 | .map(|[r, g, b, _a]| [r, g, b]) 43 | .flatten(), 44 | ) 45 | .copied() 46 | .collect::>(); 47 | write("img.ppm", raw).expect("Failed to write"); 48 | let nq = NeuQuant::new(10, 256, &pixels); 49 | 50 | let quantized = &header 51 | .into_iter() 52 | .copied() 53 | .chain( 54 | pixels 55 | .array_chunks::<4>() 56 | .map(|&[r, g, b, a]| { 57 | let mut color = [r, g, b, a]; 58 | nq.map_pixel(&mut color[..]); 59 | let [r, g, b, _a] = color; 60 | [r, g, b] 61 | }) 62 | .flatten(), 63 | ) 64 | .collect::>(); 65 | write("quantized.ppm", quantized).expect("Failed to write"); 66 | } 67 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | /* 2 | NeuQuant Neural-Net Quantization Algorithm by Anthony Dekker, 1994. 3 | See "Kohonen neural networks for optimal colour quantization" 4 | in "Network: Computation in Neural Systems" Vol. 5 (1994) pp 351-367. 5 | for a discussion of the algorithm. 6 | See also http://members.ozemail.com.au/~dekker/NEUQUANT.HTML 7 | 8 | Incorporated bugfixes and alpha channel handling from pngnq 9 | http://pngnq.sourceforge.net 10 | 11 | Copyright (c) 2014 The Piston Developers 12 | 13 | Permission is hereby granted, free of charge, to any person obtaining a copy 14 | of this software and associated documentation files (the "Software"), to deal 15 | in the Software without restriction, including without limitation the rights 16 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 17 | copies of the Software, and to permit persons to whom the Software is 18 | furnished to do so, subject to the following conditions: 19 | 20 | The above copyright notice and this permission notice shall be included in 21 | all copies or substantial portions of the Software. 22 | 23 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 24 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 25 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 26 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 27 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 28 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 29 | THE SOFTWARE. 30 | 31 | NeuQuant Neural-Net Quantization Algorithm 32 | ------------------------------------------ 33 | 34 | Copyright (c) 1994 Anthony Dekker 35 | 36 | NEUQUANT Neural-Net quantization algorithm by Anthony Dekker, 1994. 37 | See "Kohonen neural networks for optimal colour quantization" 38 | in "Network: Computation in Neural Systems" Vol. 5 (1994) pp 351-367. 39 | for a discussion of the algorithm. 40 | See also http://members.ozemail.com.au/~dekker/NEUQUANT.HTML 41 | 42 | Any party obtaining a copy of these files from the author, directly or 43 | indirectly, is granted, free of charge, a full and unrestricted irrevocable, 44 | world-wide, paid up, royalty-free, nonexclusive right and license to deal 45 | in this software and documentation files (the "Software"), including without 46 | limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, 47 | and/or sell copies of the Software, and to permit persons who receive 48 | copies from any such party to do so, with the only requirement being 49 | that this copyright notice remain intact. 50 | 51 | */ 52 | 53 | //! # Color quantization library 54 | //! 55 | //! This library provides a color quantizer based on the [NEUQUANT](http://members.ozemail.com.au/~dekker/NEUQUANT.HTML) 56 | //! 57 | //! Original literature: Dekker, A. H. (1994). Kohonen neural networks for 58 | //! optimal colour quantization. *Network: Computation in Neural Systems*, 5(3), 351-367. 59 | //! [doi: 10.1088/0954-898X_5_3_003](https://doi.org/10.1088/0954-898X_5_3_003) 60 | //! 61 | //! See also 62 | //! 63 | //! ## Usage 64 | //! 65 | //! ``` 66 | //! let data = vec![0; 40]; 67 | //! let nq = color_quant::NeuQuant::new(10, 256, &data); 68 | //! let indixes: Vec = data.chunks(4).map(|pix| nq.index_of(pix) as u8).collect(); 69 | //! let color_map = nq.color_map_rgba(); 70 | //! ``` 71 | 72 | #![forbid(unsafe_code)] 73 | #![no_std] 74 | 75 | extern crate alloc; 76 | 77 | use alloc::{vec, vec::Vec}; 78 | use core::cmp::{max, min}; 79 | 80 | mod math; 81 | use crate::math::{abs, clamp_round}; 82 | 83 | const CHANNELS: usize = 4; 84 | 85 | const RADIUS_DEC: i32 = 30; // factor of 1/30 each cycle 86 | 87 | const ALPHA_BIASSHIFT: i32 = 10; // alpha starts at 1 88 | const INIT_ALPHA: i32 = 1 << ALPHA_BIASSHIFT; // biased by 10 bits 89 | 90 | const GAMMA: f64 = 1024.0; 91 | const BETA: f64 = 1.0 / GAMMA; 92 | const BETAGAMMA: f64 = BETA * GAMMA; 93 | 94 | // four primes near 500 - assume no image has a length so large 95 | // that it is divisible by all four primes 96 | const PRIMES: [usize; 4] = [499, 491, 487, 503]; 97 | 98 | pub enum ControlFlow { 99 | Break, 100 | Continue, 101 | } 102 | 103 | impl ControlFlow { 104 | fn is_break(self) -> bool { 105 | if let ControlFlow::Break = self { 106 | true 107 | } else { 108 | false 109 | } 110 | } 111 | } 112 | 113 | #[derive(Clone, Copy)] 114 | struct Quad { 115 | r: T, 116 | g: T, 117 | b: T, 118 | a: T, 119 | } 120 | 121 | type Neuron = Quad; 122 | type Color = Quad; 123 | 124 | pub struct NeuQuant { 125 | network: Vec, 126 | colormap: Vec, 127 | netindex: Vec, 128 | bias: Vec, // bias and freq arrays for learning 129 | freq: Vec, 130 | samplefac: i32, 131 | netsize: usize, 132 | } 133 | 134 | impl NeuQuant { 135 | /// Creates a new neuronal network and trains it with the supplied data. 136 | /// 137 | /// Pixels are assumed to be in RGBA format. 138 | /// `colors` should be $>=64$. `samplefac` determines the faction of 139 | /// the sample that will be used to train the network. Its value must be in the 140 | /// range $[1, 30]$. A value of $1$ thus produces the best result but is also 141 | /// slowest. $10$ is a good compromise between speed and quality. 142 | pub fn new(samplefac: i32, colors: usize, pixels: &[u8]) -> Self { 143 | let netsize = colors; 144 | let mut this = NeuQuant { 145 | network: Vec::with_capacity(netsize), 146 | colormap: Vec::with_capacity(netsize), 147 | netindex: vec![0; 256], 148 | bias: Vec::with_capacity(netsize), 149 | freq: Vec::with_capacity(netsize), 150 | samplefac: samplefac, 151 | netsize: colors, 152 | }; 153 | this.init(pixels); 154 | this 155 | } 156 | 157 | /// Initializes the neuronal network and trains it with the supplied data. 158 | /// 159 | /// This method gets called by `Self::new`. 160 | pub fn init(&mut self, pixels: &[u8]) { 161 | self.network.clear(); 162 | self.colormap.clear(); 163 | self.bias.clear(); 164 | self.freq.clear(); 165 | let freq = (self.netsize as f64).recip(); 166 | for i in 0..self.netsize { 167 | let tmp = (i as f64) * 256.0 / (self.netsize as f64); 168 | // Sets alpha values at 0 for dark pixels. 169 | let a = if i < 16 { i as f64 * 16.0 } else { 255.0 }; 170 | self.network.push(Neuron { 171 | r: tmp, 172 | g: tmp, 173 | b: tmp, 174 | a: a, 175 | }); 176 | self.colormap.push(Color { 177 | r: 0, 178 | g: 0, 179 | b: 0, 180 | a: 255, 181 | }); 182 | self.freq.push(freq); 183 | self.bias.push(0.0); 184 | } 185 | self.learn(pixels); 186 | self.build_colormap(); 187 | self.build_netindex(); 188 | } 189 | 190 | /// Maps the rgba-pixel in-place to the best-matching color in the color map. 191 | #[inline(always)] 192 | pub fn map_pixel(&self, pixel: &mut [u8]) { 193 | assert!(pixel.len() == 4); 194 | let (r, g, b, a) = (pixel[0], pixel[1], pixel[2], pixel[3]); 195 | let i = self.search_netindex(b, g, r, a); 196 | pixel[0] = self.colormap[i].r as u8; 197 | pixel[1] = self.colormap[i].g as u8; 198 | pixel[2] = self.colormap[i].b as u8; 199 | pixel[3] = self.colormap[i].a as u8; 200 | } 201 | 202 | /// Finds the best-matching index in the color map. 203 | /// 204 | /// `pixel` is assumed to be in RGBA format. 205 | #[inline(always)] 206 | pub fn index_of(&self, pixel: &[u8]) -> usize { 207 | assert!(pixel.len() == 4); 208 | let (r, g, b, a) = (pixel[0], pixel[1], pixel[2], pixel[3]); 209 | self.search_netindex(b, g, r, a) 210 | } 211 | 212 | /// Lookup pixel values for color at `idx` in the colormap. 213 | pub fn lookup(&self, idx: usize) -> Option<[u8; 4]> { 214 | self.colormap 215 | .get(idx) 216 | .map(|p| [p.r as u8, p.g as u8, p.b as u8, p.a as u8]) 217 | } 218 | 219 | /// Returns the RGBA color map calculated from the sample. 220 | pub fn color_map_rgba(&self) -> Vec { 221 | let mut map = Vec::with_capacity(self.netsize * 4); 222 | for entry in &self.colormap { 223 | map.push(entry.r as u8); 224 | map.push(entry.g as u8); 225 | map.push(entry.b as u8); 226 | map.push(entry.a as u8); 227 | } 228 | map 229 | } 230 | 231 | /// Returns the RGBA color map calculated from the sample. 232 | pub fn color_map_rgb(&self) -> Vec { 233 | let mut map = Vec::with_capacity(self.netsize * 3); 234 | for entry in &self.colormap { 235 | map.push(entry.r as u8); 236 | map.push(entry.g as u8); 237 | map.push(entry.b as u8); 238 | } 239 | map 240 | } 241 | 242 | /// Return the Alpha channel of the color map calculated from the sample. 243 | /// It's useful for transparency used in the tRNS chunk of the PNG format. 244 | pub fn color_map_alpha(&self) -> Vec { 245 | let mut map = Vec::with_capacity(self.netsize); 246 | for entry in &self.colormap { 247 | map.push(entry.a as u8); 248 | } 249 | map 250 | } 251 | 252 | /// Move neuron i towards biased (a,b,g,r) by factor alpha 253 | fn salter_single(&mut self, alpha: f64, i: i32, quad: Quad) { 254 | let n = &mut self.network[i as usize]; 255 | n.b -= alpha * (n.b - quad.b); 256 | n.g -= alpha * (n.g - quad.g); 257 | n.r -= alpha * (n.r - quad.r); 258 | n.a -= alpha * (n.a - quad.a); 259 | } 260 | 261 | /// Move neuron adjacent neurons towards biased (a,b,g,r) by factor alpha 262 | fn alter_neighbour(&mut self, alpha: f64, rad: i32, i: i32, quad: Quad) { 263 | let lo = max(i - rad, 0); 264 | let hi = min(i + rad, self.netsize as i32); 265 | let mut j = i + 1; 266 | let mut k = i - 1; 267 | let mut q = 0; 268 | 269 | while (j < hi) || (k > lo) { 270 | let rad_sq = rad as f64 * rad as f64; 271 | let alpha = (alpha * (rad_sq - q as f64 * q as f64)) / rad_sq; 272 | q += 1; 273 | if j < hi { 274 | let p = &mut self.network[j as usize]; 275 | p.b -= alpha * (p.b - quad.b); 276 | p.g -= alpha * (p.g - quad.g); 277 | p.r -= alpha * (p.r - quad.r); 278 | p.a -= alpha * (p.a - quad.a); 279 | j += 1; 280 | } 281 | if k > lo { 282 | let p = &mut self.network[k as usize]; 283 | p.b -= alpha * (p.b - quad.b); 284 | p.g -= alpha * (p.g - quad.g); 285 | p.r -= alpha * (p.r - quad.r); 286 | p.a -= alpha * (p.a - quad.a); 287 | k -= 1; 288 | } 289 | } 290 | } 291 | 292 | /// Search for biased BGR values 293 | /// finds closest neuron (min dist) and updates freq 294 | /// finds best neuron (min dist-bias) and returns position 295 | /// for frequently chosen neurons, freq[i] is high and bias[i] is negative 296 | /// bias[i] = gamma*((1/self.netsize)-freq[i]) 297 | fn contest(&mut self, b: f64, g: f64, r: f64, a: f64) -> i32 { 298 | use core::f64; 299 | 300 | let mut bestd = f64::MAX; 301 | let mut bestbiasd: f64 = bestd; 302 | let mut bestpos = -1; 303 | let mut bestbiaspos: i32 = bestpos; 304 | 305 | for i in 0..self.netsize { 306 | let bestbiasd_biased = bestbiasd + self.bias[i]; 307 | let mut dist; 308 | let n = &self.network[i]; 309 | dist = abs(n.b - b); 310 | dist += abs(n.r - r); 311 | if dist < bestd || dist < bestbiasd_biased { 312 | dist += abs(n.g - g); 313 | dist += abs(n.a - a); 314 | if dist < bestd { 315 | bestd = dist; 316 | bestpos = i as i32; 317 | } 318 | let biasdist = dist - self.bias[i]; 319 | if biasdist < bestbiasd { 320 | bestbiasd = biasdist; 321 | bestbiaspos = i as i32; 322 | } 323 | } 324 | self.freq[i] -= BETA * self.freq[i]; 325 | self.bias[i] += BETAGAMMA * self.freq[i]; 326 | } 327 | self.freq[bestpos as usize] += BETA; 328 | self.bias[bestpos as usize] -= BETAGAMMA; 329 | return bestbiaspos; 330 | } 331 | 332 | /// Main learning loop 333 | /// Note: the number of learning cycles is crucial and the parameters are not 334 | /// optimized for net sizes < 26 or > 256. 1064 colors seems to work fine 335 | fn learn(&mut self, pixels: &[u8]) { 336 | let initrad: i32 = self.netsize as i32 / 8; // for 256 cols, radius starts at 32 337 | let radiusbiasshift: i32 = 6; 338 | let radiusbias: i32 = 1 << radiusbiasshift; 339 | let init_bias_radius: i32 = initrad * radiusbias; 340 | let mut bias_radius = init_bias_radius; 341 | let alphadec = 30 + ((self.samplefac - 1) / 3); 342 | let lengthcount = pixels.len() / CHANNELS; 343 | let samplepixels = lengthcount / self.samplefac as usize; 344 | // learning cycles 345 | let n_cycles = match self.netsize >> 1 { 346 | n if n <= 100 => 100, 347 | n => n, 348 | }; 349 | let delta = match samplepixels / n_cycles { 350 | 0 => 1, 351 | n => n, 352 | }; 353 | let mut alpha = INIT_ALPHA; 354 | 355 | let mut rad = bias_radius >> radiusbiasshift; 356 | if rad <= 1 { 357 | rad = 0 358 | }; 359 | 360 | let mut pos = 0; 361 | let step = *PRIMES 362 | .iter() 363 | .find(|&&prime| lengthcount % prime != 0) 364 | .unwrap_or(&PRIMES[3]); 365 | 366 | let mut i = 0; 367 | while i < samplepixels { 368 | let (r, g, b, a) = { 369 | let p = &pixels[CHANNELS * pos..][..CHANNELS]; 370 | (p[0] as f64, p[1] as f64, p[2] as f64, p[3] as f64) 371 | }; 372 | 373 | let j = self.contest(b, g, r, a); 374 | 375 | let alpha_ = (1.0 * alpha as f64) / INIT_ALPHA as f64; 376 | self.salter_single(alpha_, j, Quad { b, g, r, a }); 377 | if rad > 0 { 378 | self.alter_neighbour(alpha_, rad, j, Quad { b, g, r, a }) 379 | }; 380 | 381 | pos += step; 382 | while pos >= lengthcount { 383 | pos -= lengthcount 384 | } 385 | 386 | i += 1; 387 | if i % delta == 0 { 388 | alpha -= alpha / alphadec; 389 | bias_radius -= bias_radius / RADIUS_DEC; 390 | rad = bias_radius >> radiusbiasshift; 391 | if rad <= 1 { 392 | rad = 0 393 | }; 394 | } 395 | } 396 | } 397 | 398 | /// initializes the color map 399 | fn build_colormap(&mut self) { 400 | for i in 0usize..self.netsize { 401 | self.colormap[i].b = clamp_round(self.network[i].b); 402 | self.colormap[i].g = clamp_round(self.network[i].g); 403 | self.colormap[i].r = clamp_round(self.network[i].r); 404 | self.colormap[i].a = clamp_round(self.network[i].a); 405 | } 406 | } 407 | 408 | /// Insertion sort of network and building of netindex[0..255] 409 | fn build_netindex(&mut self) { 410 | let mut previouscol = 0; 411 | let mut startpos = 0; 412 | 413 | for i in 0..self.netsize { 414 | let mut p = self.colormap[i]; 415 | let mut q; 416 | let mut smallpos = i; 417 | let mut smallval = p.g as usize; // index on g 418 | // find smallest in i..netsize-1 419 | for j in (i + 1)..self.netsize { 420 | q = self.colormap[j]; 421 | if (q.g as usize) < smallval { 422 | // index on g 423 | smallpos = j; 424 | smallval = q.g as usize; // index on g 425 | } 426 | } 427 | q = self.colormap[smallpos]; 428 | // swap p (i) and q (smallpos) entries 429 | if i != smallpos { 430 | ::core::mem::swap(&mut p, &mut q); 431 | self.colormap[i] = p; 432 | self.colormap[smallpos] = q; 433 | } 434 | // smallval entry is now in position i 435 | if smallval != previouscol { 436 | self.netindex[previouscol] = (startpos + i) >> 1; 437 | for j in (previouscol + 1)..smallval { 438 | self.netindex[j] = i 439 | } 440 | previouscol = smallval; 441 | startpos = i; 442 | } 443 | } 444 | let max_netpos = self.netsize - 1; 445 | self.netindex[previouscol] = (startpos + max_netpos) >> 1; 446 | for j in (previouscol + 1)..256 { 447 | self.netindex[j] = max_netpos 448 | } // really 256 449 | } 450 | /// Search for best matching color 451 | fn search_netindex(&self, b: u8, g: u8, r: u8, a: u8) -> usize { 452 | let mut best_dist = core::i32::MAX; 453 | let first_guess = self.netindex[g as usize]; 454 | let mut best_pos = first_guess; 455 | let mut i = best_pos; 456 | 457 | #[inline] 458 | fn sqr_dist(a: i32, b: u8) -> i32 { 459 | let dist = a - b as i32; 460 | dist * dist 461 | } 462 | 463 | { 464 | let mut cmp = |i| { 465 | let Quad { 466 | r: pr, 467 | g: pg, 468 | b: pb, 469 | a: pa, 470 | } = self.colormap[i]; 471 | let mut dist = sqr_dist(pg, g); 472 | if dist > best_dist { 473 | // If the green is less than optimal, break. 474 | // Seems to be arbitrary choice from the original implementation, 475 | // but can't change this w/o invasive changes since 476 | // we also sort by green. 477 | return ControlFlow::Break; 478 | } 479 | // otherwise, continue searching through the colormap. 480 | dist += sqr_dist(pr, r); 481 | if dist >= best_dist { 482 | return ControlFlow::Continue; 483 | } 484 | dist += sqr_dist(pb, b); 485 | if dist >= best_dist { 486 | return ControlFlow::Continue; 487 | } 488 | dist += sqr_dist(pa, a); 489 | if dist >= best_dist { 490 | return ControlFlow::Continue; 491 | } 492 | best_dist = dist; 493 | best_pos = i; 494 | ControlFlow::Continue 495 | }; 496 | while i < self.netsize { 497 | i = if cmp(i).is_break() { break } else { i + 1 }; 498 | } 499 | // this j < C is a cheat to avoid the bounds check, as when the loop reaches 0 500 | // it will wrap to usize::MAX. Assume that self.netsize < usize::MAX, otherwise 501 | // using a lot of memory. This also must be true since netsize < isize::MAX < 502 | // usize::MAX, otherwise it would fail while allocating a Vec since they can be at 503 | // most isize::MAX elements. 504 | let mut j = first_guess.wrapping_sub(1); 505 | while j < self.netsize { 506 | j = if cmp(j).is_break() { 507 | break; 508 | } else { 509 | j.wrapping_sub(1) 510 | }; 511 | } 512 | } 513 | 514 | best_pos 515 | } 516 | } 517 | -------------------------------------------------------------------------------- /src/math.rs: -------------------------------------------------------------------------------- 1 | //! These implementations are based on `num-traits`' [`FloatCore`]. 2 | //! They have been adapted to the particular needs of `color_quant` and refined 3 | //! through [feedback]. 4 | //! 5 | //! [`FloatCore`]: https://docs.rs/num-traits/0.2.19/num_traits/float/trait.FloatCore.html 6 | //! [feedback]: https://github.com/image-rs/color_quant/pull/24#discussion_r2083587462 7 | 8 | #[inline] 9 | pub(crate) fn abs(a: f64) -> f64 { 10 | if a.is_sign_positive() { 11 | a 12 | } else if a.is_sign_negative() { 13 | -a 14 | } else { 15 | core::f64::NAN 16 | } 17 | } 18 | 19 | #[inline] 20 | pub(crate) fn clamp_round(a: f64) -> i32 { 21 | if a < 0. { 22 | 0 23 | } else if a > 255. { 24 | 255 25 | } else { 26 | (a + 0.5) as i32 27 | } 28 | } 29 | --------------------------------------------------------------------------------