1use std::{
28 array,
29 f32::consts::PI,
30 sync::atomic::Ordering,
31};
32
33use atomic_float::AtomicF32;
34use image::{
35 Rgb,
36 RgbImage,
37};
38use kiddo::{
39 float::kdtree::KdTree,
40 SquaredEuclidean,
41};
42use libblur::{
43 stack_blur_f32,
44 AnisotropicRadius,
45 BlurImageMut,
46 FastBlurChannels,
47 ThreadingPolicy,
48};
49use palette::{
50 color_difference::EuclideanDistance,
51 IntoColor,
52 Lab,
53 Srgb,
54};
55use rayon::{
56 iter::{
57 IndexedParallelIterator,
58 IntoParallelIterator,
59 IntoParallelRefIterator,
60 IntoParallelRefMutIterator,
61 ParallelIterator,
62 },
63 slice::{
64 ParallelSlice,
65 ParallelSliceMut,
66 },
67};
68use rustfft::{
69 num_complex::Complex,
70 num_traits::Zero,
71 FftPlanner,
72};
73
74use crate::{
75 dither::Sierra,
76 kmeans::parallel_kmeans,
77 private,
78 rgb_to_lab,
79 BitPaletteBuilder,
80 PaletteBuilder,
81 SixelEncoder,
82};
83
84pub type FocalSixelEncoderMono<D = Sierra> = SixelEncoder<FocalPaletteBuilder<2>, D>;
85pub type FocalSixelEncoder4<D = Sierra> = SixelEncoder<FocalPaletteBuilder<4>, D>;
86pub type FocalSixelEncoder8<D = Sierra> = SixelEncoder<FocalPaletteBuilder<8>, D>;
87pub type FocalSixelEncoder16<D = Sierra> = SixelEncoder<FocalPaletteBuilder<16>, D>;
88pub type FocalSixelEncoder32<D = Sierra> = SixelEncoder<FocalPaletteBuilder<32>, D>;
89pub type FocalSixelEncoder64<D = Sierra> = SixelEncoder<FocalPaletteBuilder<64>, D>;
90pub type FocalSixelEncoder128<D = Sierra> = SixelEncoder<FocalPaletteBuilder<128>, D>;
91pub type FocalSixelEncoder256<D = Sierra> = SixelEncoder<FocalPaletteBuilder<256>, D>;
92
93pub struct FocalPaletteBuilder<const PALETTE_SIZE: usize = 256>;
94
95impl<const PALETTE_SIZE: usize> private::Sealed for FocalPaletteBuilder<PALETTE_SIZE> {}
96
97impl<const PALETTE_SIZE: usize> PaletteBuilder for FocalPaletteBuilder<PALETTE_SIZE> {
98 const NAME: &'static str = "Focal";
99 const PALETTE_SIZE: usize = PALETTE_SIZE;
100
101 #[inline(never)]
102 fn build_palette(image: &RgbImage) -> Vec<Lab> {
103 let image_width = image.width();
104 let image_height = image.height();
105 let image = image.to_vec();
106
107 let mut pixels =
108 vec![<Lab>::new(0.0, 0.0, 0.0); image_width as usize * image_height as usize];
109
110 image.par_chunks(3).zip(&mut pixels).for_each(|(p, dest)| {
111 *dest = rgb_to_lab(Rgb([p[0], p[1], p[2]]));
112 });
113
114 let window_radius = (pixels.len() as f32).ln().clamp(2.0, 16.0) as u32 / 2;
115
116 let mut planner = FftPlanner::new();
117
118 let mut l_values = vec![0.0f32; pixels.len()];
119 let mut a_values = vec![0.0f32; pixels.len()];
120 let mut b_values = vec![0.0f32; pixels.len()];
121 pixels
122 .par_iter()
123 .copied()
124 .zip(&mut l_values)
125 .zip(&mut a_values)
126 .zip(&mut b_values)
127 .for_each(|(((lab, dest_l), dest_a), dest_b)| {
128 *dest_l = lab.l;
129 *dest_a = lab.a;
130 *dest_b = lab.b;
131 });
132
133 let l_saliency = compute_saliency(
134 &mut planner,
135 &l_values,
136 image_width,
137 image_height,
138 window_radius >> 1,
139 );
140
141 let a_saliency = compute_saliency(
142 &mut planner,
143 &a_values,
144 image_width,
145 image_height,
146 window_radius >> 1,
147 );
148
149 let b_saliency = compute_saliency(
150 &mut planner,
151 &b_values,
152 image_width,
153 image_height,
154 window_radius >> 1,
155 );
156
157 #[cfg(feature = "dump-l-saliency")]
158 {
159 let mut quant_l_saliency = vec![0; pixels.len()];
160
161 l_saliency
162 .spectral_residual
163 .par_iter()
164 .copied()
165 .zip(&mut quant_l_saliency)
166 .for_each(|(d, dest)| {
167 *dest = (d * u8::MAX as f32).round() as u8;
168 });
169 dump_intermediate("l_sr_aliency", &quant_l_saliency, image_width, image_height);
170
171 l_saliency
172 .mod_spectral_residual
173 .par_iter()
174 .copied()
175 .zip(&mut quant_l_saliency)
176 .for_each(|(d, dest)| {
177 *dest = (d * u8::MAX as f32).round() as u8;
178 });
179 dump_intermediate(
180 "l_mod_sr_saliency",
181 &quant_l_saliency,
182 image_width,
183 image_height,
184 );
185
186 l_saliency
187 .phase_spectrum
188 .par_iter()
189 .copied()
190 .zip(&mut quant_l_saliency)
191 .for_each(|(d, dest)| {
192 *dest = (d * u8::MAX as f32).round() as u8;
193 });
194 dump_intermediate(
195 "l_phase_saliency",
196 &quant_l_saliency,
197 image_width,
198 image_height,
199 );
200
201 l_saliency
202 .amplitude_spectrum
203 .par_iter()
204 .copied()
205 .zip(&mut quant_l_saliency)
206 .for_each(|(d, dest)| {
207 *dest = (d * u8::MAX as f32).round() as u8;
208 });
209 dump_intermediate(
210 "l_amplitude_saliency",
211 &quant_l_saliency,
212 image_width,
213 image_height,
214 );
215 }
216
217 #[cfg(feature = "dump-a-saliency")]
218 {
219 let mut quant_a_saliency = vec![0; pixels.len()];
220
221 a_saliency
222 .spectral_residual
223 .par_iter()
224 .copied()
225 .zip(&mut quant_a_saliency)
226 .for_each(|(d, dest)| {
227 *dest = (d * u8::MAX as f32).round() as u8;
228 });
229 dump_intermediate(
230 "a_sr_saliency",
231 &quant_a_saliency,
232 image_width,
233 image_height,
234 );
235
236 a_saliency
237 .mod_spectral_residual
238 .par_iter()
239 .copied()
240 .zip(&mut quant_a_saliency)
241 .for_each(|(d, dest)| {
242 *dest = (d * u8::MAX as f32).round() as u8;
243 });
244 dump_intermediate(
245 "a_mod_sr_saliency",
246 &quant_a_saliency,
247 image_width,
248 image_height,
249 );
250
251 a_saliency
252 .phase_spectrum
253 .par_iter()
254 .copied()
255 .zip(&mut quant_a_saliency)
256 .for_each(|(d, dest)| {
257 *dest = (d * u8::MAX as f32).round() as u8;
258 });
259 dump_intermediate(
260 "a_phase_saliency",
261 &quant_a_saliency,
262 image_width,
263 image_height,
264 );
265
266 a_saliency
267 .amplitude_spectrum
268 .par_iter()
269 .copied()
270 .zip(&mut quant_a_saliency)
271 .for_each(|(d, dest)| {
272 *dest = (d * u8::MAX as f32).round() as u8;
273 });
274 dump_intermediate(
275 "a_amplitude_saliency",
276 &quant_a_saliency,
277 image_width,
278 image_height,
279 );
280 }
281
282 #[cfg(feature = "dump-b-saliency")]
283 {
284 let mut quant_b_saliency = vec![0; pixels.len()];
285
286 b_saliency
287 .spectral_residual
288 .par_iter()
289 .copied()
290 .zip(&mut quant_b_saliency)
291 .for_each(|(d, dest)| {
292 *dest = (d * u8::MAX as f32).round() as u8;
293 });
294 dump_intermediate(
295 "b_sr_saliency",
296 &quant_b_saliency,
297 image_width,
298 image_height,
299 );
300
301 b_saliency
302 .mod_spectral_residual
303 .par_iter()
304 .copied()
305 .zip(&mut quant_b_saliency)
306 .for_each(|(d, dest)| {
307 *dest = (d * u8::MAX as f32).round() as u8;
308 });
309 dump_intermediate(
310 "b_mod_sr_saliency",
311 &quant_b_saliency,
312 image_width,
313 image_height,
314 );
315
316 b_saliency
317 .phase_spectrum
318 .par_iter()
319 .copied()
320 .zip(&mut quant_b_saliency)
321 .for_each(|(d, dest)| {
322 *dest = (d * u8::MAX as f32).round() as u8;
323 });
324 dump_intermediate(
325 "b_phase_saliency",
326 &quant_b_saliency,
327 image_width,
328 image_height,
329 );
330
331 b_saliency
332 .amplitude_spectrum
333 .par_iter()
334 .copied()
335 .zip(&mut quant_b_saliency)
336 .for_each(|(d, dest)| {
337 *dest = (d * u8::MAX as f32).round() as u8;
338 });
339 dump_intermediate(
340 "b_amplitude_saliency",
341 &quant_b_saliency,
342 image_width,
343 image_height,
344 );
345 }
346
347 let centroid = pixels
348 .par_iter()
349 .copied()
350 .reduce(|| <Lab>::new(0.0, 0.0, 0.0), |a, b| a + b)
351 / pixels.len() as f32;
352
353 let mut dists = vec![0.0f32; pixels.len()];
354 pixels
355 .par_iter()
356 .copied()
357 .zip(&mut dists)
358 .for_each(|(p, dest)| {
359 *dest = centroid.distance(p);
360 });
361
362 let max_dist = dists.par_iter().copied().reduce(|| f32::MIN, f32::max);
363 let min_dist = dists.par_iter().copied().reduce(|| f32::MAX, f32::min);
364 dists.par_iter_mut().for_each(|d| {
365 *d = (*d - min_dist) / (max_dist - min_dist).max(f32::EPSILON);
366 });
367
368 let mut local_dists = vec![0.0f32; pixels.len()];
369
370 (0..pixels.len())
371 .into_par_iter()
372 .zip(&mut local_dists)
373 .for_each(|(idx, dest)| {
374 let x = idx as isize % image_width as isize;
375 let y = idx as isize / image_height as isize;
376
377 let mut sum = 0.0;
378 let mut count = 0;
379 for dx in -(window_radius as isize)..=window_radius as isize {
380 for dy in -(window_radius as isize)..=window_radius as isize {
381 let x = x + dx;
382 let y = y + dy;
383
384 if (dx == 0 && dy == 0)
385 || x < 0
386 || y < 0
387 || x >= image_width as isize
388 || y >= image_height as isize
389 {
390 continue;
391 }
392 let jdx = y * image_width as isize + x;
393 sum += pixels[jdx as usize].distance(pixels[idx]);
394 count += 1;
395 }
396 }
397
398 *dest = sum / count as f32
399 });
400
401 let max_local_dist = local_dists
402 .par_iter()
403 .copied()
404 .reduce(|| f32::MIN, f32::max);
405 let min_local_dist = local_dists
406 .par_iter()
407 .copied()
408 .reduce(|| f32::MAX, f32::min);
409
410 local_dists.par_iter_mut().for_each(|d| {
411 *d = (*d - min_local_dist) / (max_local_dist - min_local_dist).max(f32::EPSILON);
412 });
413
414 #[cfg(feature = "dump-local-saliency")]
415 {
416 let mut quant_local_dists = vec![0; pixels.len()];
417 local_dists
418 .par_iter()
419 .copied()
420 .zip(&mut quant_local_dists)
421 .for_each(|(d, dest)| {
422 *dest = (d * u8::MAX as f32).round() as u8;
423 });
424
425 dump_intermediate("local_dists", &quant_local_dists, image_width, image_height);
426 }
427
428 let mut candidates = vec![(<Lab>::new(0.0, 0.0, 0.0), 0.0); pixels.len()];
429 pixels
430 .into_par_iter()
431 .zip(l_saliency.spectral_residual)
432 .zip(l_saliency.mod_spectral_residual)
433 .zip(l_saliency.phase_spectrum)
434 .zip(l_saliency.amplitude_spectrum)
435 .zip(a_saliency.spectral_residual)
436 .zip(a_saliency.mod_spectral_residual)
437 .zip(a_saliency.phase_spectrum)
438 .zip(a_saliency.amplitude_spectrum)
439 .zip(b_saliency.spectral_residual)
440 .zip(b_saliency.mod_spectral_residual)
441 .zip(b_saliency.phase_spectrum)
442 .zip(b_saliency.amplitude_spectrum)
443 .zip(local_dists)
444 .zip(dists)
445 .zip(&mut candidates)
446 .for_each(
447 |(
448 (
449 (
450 (
451 (
452 (
453 (
454 (
455 (
456 (
457 (((((lab, l_sr), l_msr), l_p), l_a), a_sr),
458 a_msr,
459 ),
460 a_p,
461 ),
462 a_a,
463 ),
464 b_sr,
465 ),
466 b_msr,
467 ),
468 b_p,
469 ),
470 b_a,
471 ),
472 local,
473 ),
474 global,
475 ),
476 dest,
477 )| {
478 let outliers = (0.5 * l_sr + 0.25 * a_sr + 0.25 * b_sr)
479 .max(0.5 * l_msr + 0.25 * a_msr + 0.25 * b_msr);
480 let edges = l_p * 0.5 + a_p * 0.25 + b_p * 0.25;
481 let blobs = l_a * 0.5 + a_a * 0.25 + b_a * 0.25;
482
483 let modifier = global.max(local);
484 let w = (outliers * 0.7 + edges * 0.2 + blobs * 0.1) * modifier;
485
486 *dest = (lab, w);
487 },
488 );
489
490 let min_weight = candidates
491 .par_iter()
492 .map(|(_, w)| *w)
493 .reduce(|| f32::MAX, f32::min);
494 let max_weight = candidates
495 .par_iter()
496 .map(|(_, w)| *w)
497 .reduce(|| f32::MIN, f32::max);
498 candidates.par_iter_mut().for_each(|(_, w)| {
499 *w = (*w - min_weight) / (max_weight - min_weight).max(f32::EPSILON);
500 });
501
502 #[cfg(feature = "dump-weights")]
503 {
504 let mut quant_candidates = vec![0; candidates.len()];
505 candidates
506 .par_iter()
507 .copied()
508 .zip(&mut quant_candidates)
509 .for_each(|((_, w), dest)| {
510 *dest = (w * u8::MAX as f32).round() as u8;
511 });
512
513 dump_intermediate("candidates", &quant_candidates, image_width, image_height);
514 }
515
516 o_means::<PALETTE_SIZE>(candidates)
517 }
518}
519
520struct Saliency {
521 spectral_residual: Vec<f32>,
522 mod_spectral_residual: Vec<f32>,
523 phase_spectrum: Vec<f32>,
524 amplitude_spectrum: Vec<f32>,
525}
526
527fn compute_saliency(
528 planner: &mut FftPlanner<f32>,
529 channel_values: &[f32],
530 image_width: u32,
531 image_height: u32,
532 window_radius: u32,
533) -> Saliency {
534 let buffer = {
535 let fft_row = planner.plan_fft_forward(image_width as usize);
536 let fft_col = planner.plan_fft_forward(image_height as usize);
537
538 let mut buffer = vec![Complex::zero(); channel_values.len()];
539 channel_values
540 .par_iter()
541 .copied()
542 .zip(&mut buffer)
543 .for_each(|(v, dest)| {
544 *dest = Complex { re: v, im: 0.0 };
545 });
546
547 buffer
548 .par_chunks_mut(image_width as usize)
549 .for_each(|chunk| {
550 fft_row.process(chunk);
551 });
552
553 let mut transpose = vec![Complex::zero(); buffer.len()];
554 (0..image_width as usize)
555 .into_par_iter()
556 .zip(transpose.par_chunks_mut(image_height as usize))
557 .for_each(|(x, col)| {
558 (0..image_height as usize)
559 .into_par_iter()
560 .zip(col)
561 .for_each(|(y, dest)| {
562 *dest = buffer[y * image_width as usize + x];
563 });
564 });
565
566 transpose
567 .par_chunks_mut(image_height as usize)
568 .for_each(|chunk| {
569 fft_col.process(chunk);
570 });
571
572 (0..image_height as usize)
573 .into_par_iter()
574 .zip(buffer.par_chunks_mut(image_width as usize))
575 .for_each(|(y, row)| {
576 (0..image_width as usize)
577 .into_par_iter()
578 .zip(row)
579 .for_each(|(x, dest)| {
580 *dest = transpose[x * image_height as usize + y];
581 });
582 });
583
584 transpose
585 };
586
587 let mut amplitude = vec![0.0f32; buffer.len()];
588 buffer
589 .par_iter()
590 .copied()
591 .zip(&mut amplitude)
592 .for_each(|(c, dest)| *dest = c.norm());
593
594 let pft = {
595 let mut ifft_buffer = vec![Complex::zero(); buffer.len()];
596
597 amplitude
598 .par_iter()
599 .copied()
600 .zip(&buffer)
601 .zip(&mut ifft_buffer)
602 .for_each(|((a, c), dest)| {
603 *dest = Complex {
604 re: if a > f32::EPSILON { c.re / a } else { 0.0 },
605 im: if a > f32::EPSILON { c.im / a } else { 0.0 },
606 };
607 });
608
609 let ifft_row = planner.plan_fft_inverse(image_width as usize);
610 let ifft_col = planner.plan_fft_inverse(image_height as usize);
611
612 ifft_buffer
613 .par_chunks_mut(image_height as usize)
614 .for_each(|chunk| {
615 ifft_col.process(chunk);
616 });
617
618 let mut transpose = vec![Complex::zero(); ifft_buffer.len()];
619 (0..image_width as usize)
620 .into_par_iter()
621 .zip(transpose.par_chunks_mut(image_height as usize))
622 .for_each(|(x, col)| {
623 (0..image_height as usize)
624 .into_par_iter()
625 .zip(col)
626 .for_each(|(y, dest)| {
627 *dest = ifft_buffer[y * image_width as usize + x];
628 });
629 });
630
631 transpose
632 .par_chunks_mut(image_height as usize)
633 .for_each(|chunk| {
634 ifft_row.process(chunk);
635 });
636
637 (0..image_height as usize)
638 .into_par_iter()
639 .zip(ifft_buffer.par_chunks_mut(image_width as usize))
640 .for_each(|(y, row)| {
641 (0..image_width as usize)
642 .into_par_iter()
643 .zip(row)
644 .for_each(|(x, dest)| {
645 *dest = transpose[x * image_height as usize + y];
646 });
647 });
648
649 let mut pft = vec![0.0f32; transpose.len()];
650
651 transpose
652 .par_iter()
653 .copied()
654 .zip(&mut pft)
655 .for_each(|(c, dest)| *dest = c.norm_sqr());
656
657 {
658 let mut pft =
659 BlurImageMut::borrow(&mut pft, image_width, image_height, FastBlurChannels::Plane);
660 stack_blur_f32(
661 &mut pft,
662 AnisotropicRadius::new(window_radius),
663 ThreadingPolicy::Adaptive,
664 )
665 .unwrap();
666 }
667
668 let max_pft = pft.par_iter().copied().reduce(|| f32::MIN, f32::max);
669 let min_pft = pft.par_iter().copied().reduce(|| f32::MAX, f32::min);
670 pft.par_iter_mut().for_each(|a| {
671 *a = (*a - min_pft) / (max_pft - min_pft).max(f32::EPSILON);
672 });
673
674 pft
675 };
676
677 let mut phase = vec![0.0f32; buffer.len()];
678 buffer
679 .into_par_iter()
680 .zip(&mut phase)
681 .for_each(|(c, dest)| *dest = c.arg());
682
683 let mut mod_amplitude = vec![0.0f32; phase.len()];
684 let max_amplitude = amplitude.par_iter().copied().reduce(|| f32::MIN, f32::max);
685 let min_amplitude = amplitude.par_iter().copied().reduce(|| f32::MAX, f32::min);
686 amplitude
687 .par_iter()
688 .copied()
689 .zip(&mut mod_amplitude)
690 .for_each(|(a, dest)| {
691 let x = ((a - min_amplitude) / (max_amplitude - min_amplitude).max(f32::EPSILON) - 0.5)
692 * 2.0;
693 *dest = ((x * PI / 2.0).sin().powi(15) / 2.0 + 0.5) * (max_amplitude - min_amplitude)
694 + min_amplitude
695 });
696
697 let asr = {
698 let mut ifft_buffer = vec![Complex::zero(); phase.len()];
699 mod_amplitude
700 .par_iter()
701 .copied()
702 .zip(&phase)
703 .zip(&mut ifft_buffer)
704 .for_each(|((a, p), dest)| {
705 *dest = Complex {
706 re: a * p.cos(),
707 im: a * p.sin(),
708 };
709 });
710
711 let ifft_row = planner.plan_fft_inverse(image_width as usize);
712 let ifft_col = planner.plan_fft_inverse(image_height as usize);
713 ifft_buffer
714 .par_chunks_mut(image_height as usize)
715 .for_each(|chunk| {
716 ifft_col.process(chunk);
717 });
718
719 let mut transpose = vec![Complex::zero(); ifft_buffer.len()];
720 (0..image_width as usize)
721 .into_par_iter()
722 .zip(transpose.par_chunks_mut(image_height as usize))
723 .for_each(|(x, col)| {
724 (0..image_height as usize)
725 .into_par_iter()
726 .zip(col)
727 .for_each(|(y, dest)| {
728 *dest = ifft_buffer[y * image_width as usize + x];
729 });
730 });
731
732 transpose
733 .par_chunks_mut(image_height as usize)
734 .for_each(|chunk| {
735 ifft_row.process(chunk);
736 });
737
738 (0..image_height as usize)
739 .into_par_iter()
740 .zip(ifft_buffer.par_chunks_mut(image_width as usize))
741 .for_each(|(y, row)| {
742 (0..image_width as usize)
743 .into_par_iter()
744 .zip(row)
745 .for_each(|(x, dest)| {
746 *dest = transpose[x * image_height as usize + y];
747 });
748 });
749
750 let mut asr = vec![0.0f32; transpose.len()];
751 transpose
752 .par_iter()
753 .copied()
754 .zip(&mut asr)
755 .for_each(|(c, dest)| *dest = c.norm_sqr());
756
757 {
758 let mut asr =
759 BlurImageMut::borrow(&mut asr, image_width, image_height, FastBlurChannels::Plane);
760 stack_blur_f32(
761 &mut asr,
762 AnisotropicRadius::new(window_radius),
763 ThreadingPolicy::Adaptive,
764 )
765 .unwrap();
766 }
767
768 let max_asr = asr.par_iter().copied().reduce(|| f32::MIN, f32::max);
769 let min_asr = asr.par_iter().copied().reduce(|| f32::MAX, f32::min);
770 asr.par_iter_mut().for_each(|a| {
771 *a = (*a - min_asr) / (max_asr - min_asr).max(f32::EPSILON);
772 });
773
774 asr
775 };
776
777 let sr = {
778 let mut log_amplitude = vec![0.0f32; phase.len()];
779 amplitude
780 .into_par_iter()
781 .zip(&mut log_amplitude)
782 .for_each(|(a, dest)| *dest = a.max(f32::EPSILON).ln());
783
784 let mut log_blurred = log_amplitude.clone();
785
786 {
787 let mut amplitude = BlurImageMut::borrow(
788 &mut log_blurred,
789 image_width,
790 image_height,
791 FastBlurChannels::Plane,
792 );
793 stack_blur_f32(
794 &mut amplitude,
795 AnisotropicRadius::new(window_radius),
796 ThreadingPolicy::Adaptive,
797 )
798 .unwrap();
799 }
800
801 let mut residual = vec![0.0f32; image_width as usize * image_height as usize];
802 log_amplitude
803 .into_par_iter()
804 .zip(log_blurred)
805 .zip(&mut residual)
806 .for_each(|((a, b), dest)| {
807 *dest = a - b;
808 });
809
810 let sr_buffer = {
811 let ifft_row = planner.plan_fft_inverse(image_width as usize);
812 let ifft_col = planner.plan_fft_inverse(image_height as usize);
813
814 let mut buffer = vec![Complex::zero(); residual.len()];
815 residual
816 .into_par_iter()
817 .zip(phase.par_iter())
818 .zip(&mut buffer)
819 .for_each(|((a, p), dest)| {
820 *dest = Complex {
821 re: a.exp() * p.cos(),
822 im: a.exp() * p.sin(),
823 };
824 });
825
826 buffer
827 .par_chunks_mut(image_height as usize)
828 .for_each(|chunk| {
829 ifft_col.process(chunk);
830 });
831
832 let mut transpose = vec![Complex::zero(); buffer.len()];
833 (0..image_width as usize)
834 .into_par_iter()
835 .zip(transpose.par_chunks_mut(image_height as usize))
836 .for_each(|(x, col)| {
837 (0..image_height as usize)
838 .into_par_iter()
839 .zip(col)
840 .for_each(|(y, dest)| {
841 *dest = buffer[y * image_width as usize + x];
842 });
843 });
844
845 transpose
846 .par_chunks_mut(image_height as usize)
847 .for_each(|chunk| {
848 ifft_row.process(chunk);
849 });
850
851 (0..image_height as usize)
852 .into_par_iter()
853 .zip(buffer.par_chunks_mut(image_width as usize))
854 .for_each(|(y, row)| {
855 (0..image_width as usize)
856 .into_par_iter()
857 .zip(row)
858 .for_each(|(x, dest)| {
859 *dest = transpose[x * image_height as usize + y];
860 });
861 });
862
863 transpose
864 };
865
866 let mut saliency = vec![0.0f32; image_width as usize * image_height as usize];
867 sr_buffer
868 .into_par_iter()
869 .zip(&mut saliency)
870 .for_each(|(c, dest)| *dest = c.norm_sqr());
871
872 {
873 let mut saliency = BlurImageMut::borrow(
874 &mut saliency,
875 image_width,
876 image_height,
877 FastBlurChannels::Plane,
878 );
879 stack_blur_f32(
880 &mut saliency,
881 AnisotropicRadius::new(window_radius),
882 ThreadingPolicy::Adaptive,
883 )
884 .unwrap();
885 }
886
887 let max_saliency = saliency.par_iter().copied().reduce(|| f32::MIN, f32::max);
888 let min_saliency = saliency.par_iter().copied().reduce(|| f32::MAX, f32::min);
889 saliency.par_iter_mut().for_each(|s| {
890 *s = (*s - min_saliency) / (max_saliency - min_saliency).max(f32::EPSILON);
891 });
892
893 saliency
894 };
895
896 let msr = {
897 let mut log_amplitude = vec![0.0f32; phase.len()];
898 mod_amplitude
899 .into_par_iter()
900 .zip(&mut log_amplitude)
901 .for_each(|(a, dest)| *dest = a.max(f32::EPSILON).ln());
902
903 let mut log_blurred = log_amplitude.clone();
904
905 {
906 let mut amplitude = BlurImageMut::borrow(
907 &mut log_blurred,
908 image_width,
909 image_height,
910 FastBlurChannels::Plane,
911 );
912 stack_blur_f32(
913 &mut amplitude,
914 AnisotropicRadius::new(window_radius),
915 ThreadingPolicy::Adaptive,
916 )
917 .unwrap();
918 }
919
920 let mut residual = vec![0.0f32; image_width as usize * image_height as usize];
921 log_amplitude
922 .into_par_iter()
923 .zip(log_blurred)
924 .zip(&mut residual)
925 .for_each(|((a, b), dest)| {
926 *dest = a - b;
927 });
928
929 let mut mod_phase = vec![0.0f32; phase.len()];
930
931 let max_phase = phase.par_iter().copied().reduce(|| f32::MIN, f32::max);
932 let min_phase = phase.par_iter().copied().reduce(|| f32::MAX, f32::min);
933
934 phase
935 .into_par_iter()
936 .zip(&mut mod_phase)
937 .for_each(|(p, dest)| {
938 let x = ((p - min_phase) / (max_phase - min_phase).max(f32::EPSILON) - 0.5) * 2.0;
939 *dest = ((x * PI / 2.0).sin().powi(15) / 2.0 + 0.5) * (max_phase - min_phase)
940 + min_phase;
941 });
942
943 let mut msr_buffer = vec![Complex::zero(); mod_phase.len()];
944 residual
945 .into_par_iter()
946 .zip(mod_phase)
947 .zip(&mut msr_buffer)
948 .for_each(|((a, p), dest)| {
949 *dest = Complex {
950 re: a * p.cos(),
951 im: a * p.sin(),
952 };
953 });
954
955 let ifft_row = planner.plan_fft_inverse(image_width as usize);
956 let ifft_col = planner.plan_fft_inverse(image_height as usize);
957 msr_buffer
958 .par_chunks_mut(image_height as usize)
959 .for_each(|chunk| {
960 ifft_col.process(chunk);
961 });
962
963 let mut transpose = vec![Complex::zero(); msr_buffer.len()];
964 (0..image_width as usize)
965 .into_par_iter()
966 .zip(transpose.par_chunks_mut(image_height as usize))
967 .for_each(|(x, col)| {
968 (0..image_height as usize)
969 .into_par_iter()
970 .zip(col)
971 .for_each(|(y, dest)| {
972 *dest = msr_buffer[y * image_width as usize + x];
973 });
974 });
975
976 transpose
977 .par_chunks_mut(image_height as usize)
978 .for_each(|chunk| {
979 ifft_row.process(chunk);
980 });
981
982 (0..image_height as usize)
983 .into_par_iter()
984 .zip(msr_buffer.par_chunks_mut(image_width as usize))
985 .for_each(|(y, row)| {
986 (0..image_width as usize)
987 .into_par_iter()
988 .zip(row)
989 .for_each(|(x, dest)| {
990 *dest = transpose[x * image_height as usize + y];
991 });
992 });
993
994 let mut msr = vec![0.0f32; transpose.len()];
995 transpose
996 .par_iter()
997 .copied()
998 .zip(&mut msr)
999 .for_each(|(c, dest)| *dest = c.norm_sqr());
1000
1001 {
1002 let mut msr =
1003 BlurImageMut::borrow(&mut msr, image_width, image_height, FastBlurChannels::Plane);
1004 stack_blur_f32(
1005 &mut msr,
1006 AnisotropicRadius::new(window_radius),
1007 ThreadingPolicy::Adaptive,
1008 )
1009 .unwrap();
1010 }
1011
1012 let max_msr = msr.par_iter().copied().reduce(|| f32::MIN, f32::max);
1013 let min_msr = msr.par_iter().copied().reduce(|| f32::MAX, f32::min);
1014 msr.par_iter_mut().for_each(|s| {
1015 *s = (*s - min_msr) / (max_msr - min_msr).max(f32::EPSILON);
1016 });
1017
1018 msr
1019 };
1020
1021 Saliency {
1022 spectral_residual: sr,
1023 mod_spectral_residual: msr,
1024 phase_spectrum: pft,
1025 amplitude_spectrum: asr,
1026 }
1027}
1028
1029#[cfg(any(
1030 feature = "dump-l-saliency",
1031 feature = "dump-a-saliency",
1032 feature = "dump-b-saliency",
1033 feature = "dump-local-saliency",
1034 feature = "dump-weights"
1035))]
1036fn dump_intermediate(name: &str, buffer: &[u8], width: u32, height: u32) {
1037 use std::hash::{
1038 BuildHasher,
1039 Hasher,
1040 RandomState,
1041 };
1042 let rand = BuildHasher::build_hasher(&RandomState::new()).finish();
1043 image::save_buffer(
1044 format!("{name}-{rand}.png"),
1045 buffer,
1046 width,
1047 height,
1048 image::ColorType::L8,
1049 )
1050 .unwrap()
1051}
1052
1053pub(crate) fn o_means<const PALETTE_SIZE: usize>(mut candidates: Vec<(Lab, f32)>) -> Vec<Lab> {
1054 let mut final_clusters = vec![];
1055
1056 loop {
1057 let (color, weight) = candidates
1058 .par_iter()
1059 .copied()
1060 .map(|(c, w)| (c * w, w))
1061 .reduce(
1062 || (<Lab>::new(0.0, 0.0, 0.0), 0.0),
1063 |a, b| (a.0 + b.0, a.1 + b.1),
1064 );
1065
1066 let centroid = color / weight.max(f32::EPSILON);
1067
1068 let var_dist = candidates
1069 .par_iter()
1070 .map(|(c, w)| c.distance_squared(centroid) * w)
1071 .sum::<f32>()
1072 / weight.max(f32::EPSILON);
1073
1074 let sigma = var_dist.sqrt() * 2.0;
1075
1076 let summaries = array::from_fn::<_, PALETTE_SIZE, _>(|_| {
1077 (
1078 [
1079 AtomicF32::new(0.0),
1080 AtomicF32::new(0.0),
1081 AtomicF32::new(0.0),
1082 ],
1083 AtomicF32::new(0.0),
1084 )
1085 });
1086
1087 candidates.par_iter().copied().for_each(|(color, weight)| {
1088 let rgb: Srgb = color.into_color();
1089 let slot = BitPaletteBuilder::<PALETTE_SIZE>::index(rgb.into_format());
1090 summaries[slot].0[0].fetch_add(rgb.red * weight, Ordering::Relaxed);
1091 summaries[slot].0[1].fetch_add(rgb.green * weight, Ordering::Relaxed);
1092 summaries[slot].0[2].fetch_add(rgb.blue * weight, Ordering::Relaxed);
1093 summaries[slot].1.fetch_add(weight, Ordering::Relaxed);
1094 });
1095
1096 let mut centroids = KdTree::<_, _, 3, 257, u32>::with_capacity(256);
1097
1098 for (idx, (color, weight)) in summaries.iter().enumerate() {
1099 let count = weight.load(Ordering::Relaxed);
1100 if count > 0.0 {
1101 let color = Srgb::new(
1102 color[0].load(Ordering::Relaxed) / count,
1103 color[1].load(Ordering::Relaxed) / count,
1104 color[2].load(Ordering::Relaxed) / count,
1105 );
1106 let color: Lab = color.into_color();
1107
1108 centroids.add(&[color.l, color.a, color.b], idx as u32);
1109 }
1110 }
1111
1112 let mut cluster_assignments = vec![-1; candidates.len()];
1113 candidates
1114 .par_iter()
1115 .copied()
1116 .zip(&mut cluster_assignments)
1117 .for_each(|((color, _), slot)| {
1118 let nearest =
1119 centroids.nearest_one::<SquaredEuclidean>(&[color.l, color.a, color.b]);
1120
1121 if nearest.distance <= sigma {
1122 *slot = nearest.item as i64;
1123 }
1124 });
1125
1126 let cluster_means = array::from_fn::<_, PALETTE_SIZE, _>(|_| {
1127 (
1128 [
1129 AtomicF32::new(0.0),
1130 AtomicF32::new(0.0),
1131 AtomicF32::new(0.0),
1132 ],
1133 AtomicF32::new(0.0),
1134 )
1135 });
1136
1137 cluster_assignments
1138 .par_iter()
1139 .enumerate()
1140 .for_each(|(idx, &slot)| {
1141 if slot > 0 {
1142 let w = candidates[idx].1;
1143 cluster_means[slot as usize].0[0]
1144 .fetch_add(candidates[idx].0.l * w, Ordering::Relaxed);
1145 cluster_means[slot as usize].0[1]
1146 .fetch_add(candidates[idx].0.a * w, Ordering::Relaxed);
1147 cluster_means[slot as usize].0[2]
1148 .fetch_add(candidates[idx].0.b * w, Ordering::Relaxed);
1149 cluster_means[slot as usize]
1150 .1
1151 .fetch_add(w, Ordering::Relaxed);
1152 }
1153 });
1154
1155 for _ in 0..100 {
1156 centroids = KdTree::<_, _, 3, 257, u32>::with_capacity(256);
1157
1158 for (cidx, (mean, count)) in cluster_means.iter().enumerate() {
1159 let count = count.load(Ordering::Relaxed);
1160 if count > 0.0 {
1161 centroids.add(
1162 &[
1163 mean[0].load(Ordering::Relaxed) / count,
1164 mean[1].load(Ordering::Relaxed) / count,
1165 mean[2].load(Ordering::Relaxed) / count,
1166 ],
1167 cidx as u32,
1168 );
1169 }
1170 }
1171
1172 let shifts = candidates
1173 .par_iter()
1174 .copied()
1175 .zip(&mut cluster_assignments)
1176 .map(|((color, w), slot)| {
1177 let nearest =
1178 centroids.nearest_one::<SquaredEuclidean>(&[color.l, color.a, color.b]);
1179
1180 let old_slot = *slot;
1181 *slot = if nearest.distance > sigma {
1182 -1
1183 } else {
1184 nearest.item as i64
1185 };
1186
1187 if old_slot == *slot {
1188 return false;
1189 }
1190
1191 if old_slot >= 0 {
1192 cluster_means[old_slot as usize].0[0]
1193 .fetch_sub(color.l * w, Ordering::Relaxed);
1194 cluster_means[old_slot as usize].0[1]
1195 .fetch_sub(color.a * w, Ordering::Relaxed);
1196 cluster_means[old_slot as usize].0[2]
1197 .fetch_sub(color.b * w, Ordering::Relaxed);
1198 cluster_means[old_slot as usize]
1199 .1
1200 .fetch_sub(w, Ordering::Relaxed);
1201 }
1202
1203 if *slot >= 0 {
1204 cluster_means[nearest.item as usize].0[0]
1205 .fetch_add(color.l * w, Ordering::Relaxed);
1206 cluster_means[nearest.item as usize].0[1]
1207 .fetch_add(color.a * w, Ordering::Relaxed);
1208 cluster_means[nearest.item as usize].0[2]
1209 .fetch_add(color.b * w, Ordering::Relaxed);
1210 cluster_means[nearest.item as usize]
1211 .1
1212 .fetch_add(w, Ordering::Relaxed);
1213 }
1214
1215 true
1216 })
1217 .filter(|shift| *shift)
1218 .count();
1219
1220 if shifts == 0 {
1221 break;
1222 }
1223 }
1224
1225 for (sum, weight) in cluster_means.iter() {
1226 let weight = weight.load(Ordering::Relaxed);
1227 if weight > 0.0 {
1228 final_clusters.push((
1229 Lab::new(
1230 sum[0].load(Ordering::Relaxed) / weight,
1231 sum[1].load(Ordering::Relaxed) / weight,
1232 sum[2].load(Ordering::Relaxed) / weight,
1233 ),
1234 weight,
1235 ));
1236 }
1237 }
1238
1239 let outliers = cluster_assignments
1240 .iter()
1241 .enumerate()
1242 .filter(|(_, &cluster)| cluster < 0)
1243 .map(|(i, _)| candidates[i])
1244 .collect::<Vec<_>>();
1245
1246 if outliers.is_empty() || outliers.len() == candidates.len() {
1247 final_clusters.extend(outliers);
1248 break;
1249 }
1250 candidates = outliers;
1251 }
1252
1253 if final_clusters.len() <= PALETTE_SIZE {
1254 final_clusters.into_iter().map(|(c, _)| c).collect()
1255 } else {
1256 parallel_kmeans::<PALETTE_SIZE>(&final_clusters).0
1257 }
1258}