a_sixel/
focal.rs

1//! Use weighted pixels based on the image's spectral properties to provided
2//! weighted input to k-means.
3//!
4//! Weights are computed using the following steps:
5//! 1. Compute the saliency maps for each channel (L, a, b) using a combination
6//!    of the following:
7//!    - Spectral residual is the difference between the blurred natural log of
8//!      the amplitude calculated by the FFT and the natural log of the
9//!      amplitude reconstructed from the inverse FFT using the original phase
10//!      spectrum.
11//!    - Modulated spectral residual is the same as the above, but the amplitude
12//!      and phase have flattened mid values.
13//!    - Phase spectrum is the phase of the FFT.
14//!    - Amplitude spectrum is the amplitude of the FFT modulated to have a
15//!      flattened midrange.
16//! 2. Compute the average distance of each pixel to the centroid of the image
17//!    transformed to the range \[0, 1].
18//! 3. Compute the average local distance of each pixel to its neighbors within
19//!    a window of size ln(pixels.len()) / 2. This is used to help identify
20//!    edges and features in the image.
21//! 4. Compute the final weight for each pixel as a combination of the saliency
22//!    maps and the local distance: lerp(a, sr.max(mod_sr), p, ld) where sr is
23//!    the spectral residual, mod_sr is the modulated spectral residual, p is
24//!    the phase spectrum, a is the amplitude spectrum, and ld is the local
25//!    distance.
26
27use 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}