a_sixel/
focal.rs

1//! Use weighted pixels based on the image's spectral properties to provided
2//! weighted input to kmeans.
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::f32::consts::PI;
28
29use image::{
30    Rgb,
31    RgbImage,
32};
33use libblur::{
34    stack_blur_f32,
35    AnisotropicRadius,
36    BlurImageMut,
37    FastBlurChannels,
38    ThreadingPolicy,
39};
40use palette::{
41    color_difference::EuclideanDistance,
42    Lab,
43};
44use rayon::{
45    iter::{
46        IndexedParallelIterator,
47        IntoParallelIterator,
48        IntoParallelRefIterator,
49        IntoParallelRefMutIterator,
50        ParallelIterator,
51    },
52    slice::{
53        ParallelSlice,
54        ParallelSliceMut,
55    },
56};
57use rustfft::{
58    num_complex::Complex,
59    num_traits::Zero,
60    FftPlanner,
61};
62
63use crate::{
64    dither::Sierra,
65    kmeans::parallel_kmeans,
66    private,
67    rgb_to_lab,
68    PaletteBuilder,
69    SixelEncoder,
70};
71
72pub type FocalSixelEncoderMono<D = Sierra> = SixelEncoder<FocalPaletteBuilder<2>, D>;
73pub type FocalSixelEncoder4<D = Sierra> = SixelEncoder<FocalPaletteBuilder<4>, D>;
74pub type FocalSixelEncoder8<D = Sierra> = SixelEncoder<FocalPaletteBuilder<8>, D>;
75pub type FocalSixelEncoder16<D = Sierra> = SixelEncoder<FocalPaletteBuilder<16>, D>;
76pub type FocalSixelEncoder32<D = Sierra> = SixelEncoder<FocalPaletteBuilder<32>, D>;
77pub type FocalSixelEncoder64<D = Sierra> = SixelEncoder<FocalPaletteBuilder<64>, D>;
78pub type FocalSixelEncoder128<D = Sierra> = SixelEncoder<FocalPaletteBuilder<128>, D>;
79pub type FocalSixelEncoder256<D = Sierra> = SixelEncoder<FocalPaletteBuilder<256>, D>;
80
81pub struct FocalPaletteBuilder<const PALETTE_SIZE: usize = 256>;
82
83impl<const PALETTE_SIZE: usize> private::Sealed for FocalPaletteBuilder<PALETTE_SIZE> {}
84
85impl<const PALETTE_SIZE: usize> PaletteBuilder for FocalPaletteBuilder<PALETTE_SIZE> {
86    const PALETTE_SIZE: usize = PALETTE_SIZE;
87
88    #[inline(never)]
89    fn build_palette(image: &RgbImage) -> Vec<Lab> {
90        let image_width = image.width();
91        let image_height = image.height();
92        let image = image.to_vec();
93
94        let mut pixels =
95            vec![<Lab>::new(0.0, 0.0, 0.0); image_width as usize * image_height as usize];
96
97        image.par_chunks(3).zip(&mut pixels).for_each(|(p, dest)| {
98            *dest = rgb_to_lab(Rgb([p[0], p[1], p[2]]));
99        });
100
101        let window_radius = (pixels.len() as f32).ln().clamp(2.0, 16.0) as u32 / 2;
102
103        let mut planner = FftPlanner::new();
104
105        let mut l_values = vec![0.0f32; pixels.len()];
106        let mut a_values = vec![0.0f32; pixels.len()];
107        let mut b_values = vec![0.0f32; pixels.len()];
108        pixels
109            .par_iter()
110            .copied()
111            .zip(&mut l_values)
112            .zip(&mut a_values)
113            .zip(&mut b_values)
114            .for_each(|(((lab, dest_l), dest_a), dest_b)| {
115                *dest_l = lab.l;
116                *dest_a = lab.a;
117                *dest_b = lab.b;
118            });
119
120        let l_saliency = compute_saliency(
121            &mut planner,
122            &l_values,
123            image_width,
124            image_height,
125            window_radius >> 1,
126        );
127
128        let a_saliency = compute_saliency(
129            &mut planner,
130            &a_values,
131            image_width,
132            image_height,
133            window_radius >> 1,
134        );
135
136        let b_saliency = compute_saliency(
137            &mut planner,
138            &b_values,
139            image_width,
140            image_height,
141            window_radius >> 1,
142        );
143
144        #[cfg(feature = "dump_l_saliency")]
145        {
146            let mut quant_l_saliency = vec![0; pixels.len()];
147
148            l_saliency
149                .spectral_residual
150                .par_iter()
151                .copied()
152                .zip(&mut quant_l_saliency)
153                .for_each(|(d, dest)| {
154                    *dest = (d * u8::MAX as f32).round() as u8;
155                });
156            dump_intermediate("l_sr_aliency", &quant_l_saliency, image_width, image_height);
157
158            l_saliency
159                .mod_spectral_residual
160                .par_iter()
161                .copied()
162                .zip(&mut quant_l_saliency)
163                .for_each(|(d, dest)| {
164                    *dest = (d * u8::MAX as f32).round() as u8;
165                });
166            dump_intermediate(
167                "l_mod_sr_saliency",
168                &quant_l_saliency,
169                image_width,
170                image_height,
171            );
172
173            l_saliency
174                .phase_spectrum
175                .par_iter()
176                .copied()
177                .zip(&mut quant_l_saliency)
178                .for_each(|(d, dest)| {
179                    *dest = (d * u8::MAX as f32).round() as u8;
180                });
181            dump_intermediate(
182                "l_phase_saliency",
183                &quant_l_saliency,
184                image_width,
185                image_height,
186            );
187
188            l_saliency
189                .amplitude_spectrum
190                .par_iter()
191                .copied()
192                .zip(&mut quant_l_saliency)
193                .for_each(|(d, dest)| {
194                    *dest = (d * u8::MAX as f32).round() as u8;
195                });
196            dump_intermediate(
197                "l_amplitude_saliency",
198                &quant_l_saliency,
199                image_width,
200                image_height,
201            );
202        }
203
204        #[cfg(feature = "dump_a_saliency")]
205        {
206            let mut quant_a_saliency = vec![0; pixels.len()];
207
208            a_saliency
209                .spectral_residual
210                .par_iter()
211                .copied()
212                .zip(&mut quant_a_saliency)
213                .for_each(|(d, dest)| {
214                    *dest = (d * u8::MAX as f32).round() as u8;
215                });
216            dump_intermediate(
217                "a_sr_saliency",
218                &quant_a_saliency,
219                image_width,
220                image_height,
221            );
222
223            a_saliency
224                .mod_spectral_residual
225                .par_iter()
226                .copied()
227                .zip(&mut quant_a_saliency)
228                .for_each(|(d, dest)| {
229                    *dest = (d * u8::MAX as f32).round() as u8;
230                });
231            dump_intermediate(
232                "a_mod_sr_saliency",
233                &quant_a_saliency,
234                image_width,
235                image_height,
236            );
237
238            a_saliency
239                .phase_spectrum
240                .par_iter()
241                .copied()
242                .zip(&mut quant_a_saliency)
243                .for_each(|(d, dest)| {
244                    *dest = (d * u8::MAX as f32).round() as u8;
245                });
246            dump_intermediate(
247                "a_phase_saliency",
248                &quant_a_saliency,
249                image_width,
250                image_height,
251            );
252
253            a_saliency
254                .amplitude_spectrum
255                .par_iter()
256                .copied()
257                .zip(&mut quant_a_saliency)
258                .for_each(|(d, dest)| {
259                    *dest = (d * u8::MAX as f32).round() as u8;
260                });
261            dump_intermediate(
262                "a_amplitude_saliency",
263                &quant_a_saliency,
264                image_width,
265                image_height,
266            );
267        }
268
269        #[cfg(feature = "dump_b_saliency")]
270        {
271            let mut quant_b_saliency = vec![0; pixels.len()];
272
273            b_saliency
274                .spectral_residual
275                .par_iter()
276                .copied()
277                .zip(&mut quant_b_saliency)
278                .for_each(|(d, dest)| {
279                    *dest = (d * u8::MAX as f32).round() as u8;
280                });
281            dump_intermediate(
282                "b_sr_saliency",
283                &quant_b_saliency,
284                image_width,
285                image_height,
286            );
287
288            b_saliency
289                .mod_spectral_residual
290                .par_iter()
291                .copied()
292                .zip(&mut quant_b_saliency)
293                .for_each(|(d, dest)| {
294                    *dest = (d * u8::MAX as f32).round() as u8;
295                });
296            dump_intermediate(
297                "b_mod_sr_saliency",
298                &quant_b_saliency,
299                image_width,
300                image_height,
301            );
302
303            b_saliency
304                .phase_spectrum
305                .par_iter()
306                .copied()
307                .zip(&mut quant_b_saliency)
308                .for_each(|(d, dest)| {
309                    *dest = (d * u8::MAX as f32).round() as u8;
310                });
311            dump_intermediate(
312                "b_phase_saliency",
313                &quant_b_saliency,
314                image_width,
315                image_height,
316            );
317
318            b_saliency
319                .amplitude_spectrum
320                .par_iter()
321                .copied()
322                .zip(&mut quant_b_saliency)
323                .for_each(|(d, dest)| {
324                    *dest = (d * u8::MAX as f32).round() as u8;
325                });
326            dump_intermediate(
327                "b_amplitude_saliency",
328                &quant_b_saliency,
329                image_width,
330                image_height,
331            );
332        }
333
334        let centroid = pixels.par_iter().copied().reduce(
335            || <Lab>::new(0.0, 0.0, 0.0),
336            |mut acc, color| {
337                acc.l += color.l;
338                acc.a += color.a;
339                acc.b += color.b;
340                acc
341            },
342        ) / pixels.len() as f32;
343
344        let mut dists = vec![0.0f32; pixels.len()];
345        pixels
346            .par_iter()
347            .copied()
348            .zip(&mut dists)
349            .for_each(|(lab, dest)| {
350                *dest = lab.distance(centroid);
351            });
352
353        let max_dist = dists.par_iter().copied().reduce(|| f32::MIN, f32::max);
354        let min_dist = dists.par_iter().copied().reduce(|| f32::MAX, f32::min);
355        dists.par_iter_mut().for_each(|d| {
356            *d = (*d - min_dist) / (max_dist - min_dist).max(f32::EPSILON);
357        });
358
359        #[cfg(feature = "dump_dist_saliency")]
360        {
361            let mut quant_dists = vec![0; pixels.len()];
362            dists
363                .par_iter()
364                .copied()
365                .zip(&mut quant_dists)
366                .for_each(|(d, dest)| {
367                    *dest = (d * u8::MAX as f32).round() as u8;
368                });
369
370            dump_intermediate("dists", &quant_dists, image_width, image_height);
371        }
372
373        let mut local_dists = vec![0.0f32; pixels.len()];
374
375        (0..pixels.len())
376            .into_par_iter()
377            .zip(&mut local_dists)
378            .for_each(|(idx, dest)| {
379                let x = idx as isize % image_width as isize;
380                let y = idx as isize / image_height as isize;
381
382                let mut sum = 0.0;
383                let mut count = 0;
384                for dx in -(window_radius as isize)..=window_radius as isize {
385                    for dy in -(window_radius as isize)..=window_radius as isize {
386                        let x = x + dx;
387                        let y = y + dy;
388
389                        if (dx == 0 && dy == 0)
390                            || x < 0
391                            || y < 0
392                            || x >= image_width as isize
393                            || y >= image_height as isize
394                        {
395                            continue;
396                        }
397                        let jdx = y * image_width as isize + x;
398                        sum += pixels[jdx as usize].distance(pixels[idx]);
399                        count += 1;
400                    }
401                }
402
403                *dest = sum / count as f32
404            });
405
406        let min_local_dist = local_dists
407            .par_iter()
408            .copied()
409            .reduce(|| f32::MAX, f32::min);
410        let max_local_dist = local_dists
411            .par_iter()
412            .copied()
413            .reduce(|| f32::MIN, f32::max);
414
415        local_dists.par_iter_mut().for_each(|d| {
416            *d = (*d - min_local_dist) / (max_local_dist - min_local_dist).max(f32::EPSILON);
417        });
418
419        #[cfg(feature = "dump_local_saliency")]
420        {
421            let mut quant_local_dists = vec![0; pixels.len()];
422            local_dists
423                .par_iter()
424                .copied()
425                .zip(&mut quant_local_dists)
426                .for_each(|(d, dest)| {
427                    *dest = (d * u8::MAX as f32).round() as u8;
428                });
429
430            dump_intermediate("local_dists", &quant_local_dists, image_width, image_height);
431        }
432
433        let avg_local_dist =
434            local_dists.par_iter().copied().sum::<f32>() / local_dists.len() as f32;
435        let avg_dist = dists.par_iter().copied().sum::<f32>() / dists.len() as f32;
436
437        let mut candidates = vec![(<Lab>::new(0.0, 0.0, 0.0), 0.0); pixels.len()];
438        pixels
439            .into_par_iter()
440            .zip(l_saliency.spectral_residual)
441            .zip(l_saliency.mod_spectral_residual)
442            .zip(l_saliency.phase_spectrum)
443            .zip(l_saliency.amplitude_spectrum)
444            .zip(a_saliency.spectral_residual)
445            .zip(a_saliency.mod_spectral_residual)
446            .zip(a_saliency.phase_spectrum)
447            .zip(a_saliency.amplitude_spectrum)
448            .zip(b_saliency.spectral_residual)
449            .zip(b_saliency.mod_spectral_residual)
450            .zip(b_saliency.phase_spectrum)
451            .zip(b_saliency.amplitude_spectrum)
452            .zip(local_dists)
453            .zip(&mut candidates)
454            .for_each(
455                |(
456                    (
457                        (
458                            (
459                                (
460                                    (
461                                        (
462                                            (
463                                                ((((((lab, l_sr), l_msr), l_p), l_a), a_sr), a_msr),
464                                                a_p,
465                                            ),
466                                            a_a,
467                                        ),
468                                        b_sr,
469                                    ),
470                                    b_msr,
471                                ),
472                                b_p,
473                            ),
474                            b_a,
475                        ),
476                        local,
477                    ),
478                    dest,
479                )| {
480                    let outlier_outlier = l_msr.max(a_msr).max(b_msr);
481                    let outliers = l_sr.max(a_sr).max(b_sr).max(outlier_outlier);
482                    let edges = l_p.max(a_p).max(b_p);
483                    let blobs = l_a.max(a_a).max(b_a);
484
485                    // Lerp between blobs <=> outliers <=> edges using local distance. Local
486                    // distance is *high* for edges and features and *low* for blobs/planes of
487                    // colors. If it's neither, then it's probably important if it's an outlier.
488                    let w = if local <= 0.5 {
489                        blobs + 2.0 * local * (outliers - blobs)
490                    } else {
491                        outliers + 2.0 * (local - 0.5) * (edges - outliers)
492                    };
493
494                    *dest = (lab, w.powf(0.25 + (avg_local_dist + avg_dist) * 4.0));
495                },
496            );
497
498        #[cfg(feature = "dump_weights")]
499        {
500            let mut quant_candidates = vec![0; candidates.len()];
501            candidates
502                .par_iter()
503                .copied()
504                .zip(&mut quant_candidates)
505                .for_each(|((_, w), dest)| {
506                    *dest = (w * u8::MAX as f32).round() as u8;
507                });
508
509            dump_intermediate("candidates", &quant_candidates, image_width, image_height);
510        }
511
512        parallel_kmeans::<PALETTE_SIZE>(&candidates)
513    }
514}
515
516struct Saliency {
517    spectral_residual: Vec<f32>,
518    mod_spectral_residual: Vec<f32>,
519    phase_spectrum: Vec<f32>,
520    amplitude_spectrum: Vec<f32>,
521}
522
523fn compute_saliency(
524    planner: &mut FftPlanner<f32>,
525    channel_values: &[f32],
526    image_width: u32,
527    image_height: u32,
528    window_radius: u32,
529) -> Saliency {
530    let buffer = {
531        let fft_row = planner.plan_fft_forward(image_width as usize);
532        let fft_col = planner.plan_fft_forward(image_height as usize);
533
534        let mut buffer = vec![Complex::zero(); channel_values.len()];
535        channel_values
536            .par_iter()
537            .copied()
538            .zip(&mut buffer)
539            .for_each(|(v, dest)| {
540                *dest = Complex { re: v, im: 0.0 };
541            });
542
543        buffer
544            .par_chunks_mut(image_width as usize)
545            .for_each(|chunk| {
546                fft_row.process(chunk);
547            });
548
549        let mut transpose = vec![Complex::zero(); buffer.len()];
550        (0..image_width as usize)
551            .into_par_iter()
552            .zip(transpose.par_chunks_mut(image_height as usize))
553            .for_each(|(x, col)| {
554                (0..image_height as usize)
555                    .into_par_iter()
556                    .zip(col)
557                    .for_each(|(y, dest)| {
558                        *dest = buffer[y * image_width as usize + x];
559                    });
560            });
561
562        transpose
563            .par_chunks_mut(image_height as usize)
564            .for_each(|chunk| {
565                fft_col.process(chunk);
566            });
567
568        (0..image_height as usize)
569            .into_par_iter()
570            .zip(buffer.par_chunks_mut(image_width as usize))
571            .for_each(|(y, row)| {
572                (0..image_width as usize)
573                    .into_par_iter()
574                    .zip(row)
575                    .for_each(|(x, dest)| {
576                        *dest = transpose[x * image_height as usize + y];
577                    });
578            });
579
580        transpose
581    };
582
583    let mut amplitude = vec![0.0f32; buffer.len()];
584    buffer
585        .par_iter()
586        .copied()
587        .zip(&mut amplitude)
588        .for_each(|(c, dest)| *dest = c.norm());
589
590    let pft = {
591        let mut ifft_buffer = vec![Complex::zero(); buffer.len()];
592
593        amplitude
594            .par_iter()
595            .copied()
596            .zip(&buffer)
597            .zip(&mut ifft_buffer)
598            .for_each(|((a, c), dest)| {
599                *dest = Complex {
600                    re: if a > f32::EPSILON { c.re / a } else { 0.0 },
601                    im: if a > f32::EPSILON { c.im / a } else { 0.0 },
602                };
603            });
604
605        let ifft_row = planner.plan_fft_inverse(image_width as usize);
606        let ifft_col = planner.plan_fft_inverse(image_height as usize);
607
608        ifft_buffer
609            .par_chunks_mut(image_height as usize)
610            .for_each(|chunk| {
611                ifft_col.process(chunk);
612            });
613
614        let mut transpose = vec![Complex::zero(); ifft_buffer.len()];
615        (0..image_width as usize)
616            .into_par_iter()
617            .zip(transpose.par_chunks_mut(image_height as usize))
618            .for_each(|(x, col)| {
619                (0..image_height as usize)
620                    .into_par_iter()
621                    .zip(col)
622                    .for_each(|(y, dest)| {
623                        *dest = ifft_buffer[y * image_width as usize + x];
624                    });
625            });
626
627        transpose
628            .par_chunks_mut(image_height as usize)
629            .for_each(|chunk| {
630                ifft_row.process(chunk);
631            });
632
633        (0..image_height as usize)
634            .into_par_iter()
635            .zip(ifft_buffer.par_chunks_mut(image_width as usize))
636            .for_each(|(y, row)| {
637                (0..image_width as usize)
638                    .into_par_iter()
639                    .zip(row)
640                    .for_each(|(x, dest)| {
641                        *dest = transpose[x * image_height as usize + y];
642                    });
643            });
644
645        let mut pft = vec![0.0f32; transpose.len()];
646
647        transpose
648            .par_iter()
649            .copied()
650            .zip(&mut pft)
651            .for_each(|(c, dest)| *dest = c.norm_sqr());
652
653        {
654            let mut pft =
655                BlurImageMut::borrow(&mut pft, image_width, image_height, FastBlurChannels::Plane);
656            stack_blur_f32(
657                &mut pft,
658                AnisotropicRadius::new(window_radius),
659                ThreadingPolicy::Adaptive,
660            )
661            .unwrap();
662        }
663
664        let max_pft = pft.par_iter().copied().reduce(|| f32::MIN, f32::max);
665        let min_pft = pft.par_iter().copied().reduce(|| f32::MAX, f32::min);
666        pft.par_iter_mut().for_each(|a| {
667            *a = (*a - min_pft) / (max_pft - min_pft).max(f32::EPSILON);
668        });
669
670        pft
671    };
672
673    let mut phase = vec![0.0f32; buffer.len()];
674    buffer
675        .into_par_iter()
676        .zip(&mut phase)
677        .for_each(|(c, dest)| *dest = c.arg());
678
679    let mut mod_amplitude = vec![0.0f32; phase.len()];
680    let max_amplitude = amplitude.par_iter().copied().reduce(|| f32::MIN, f32::max);
681    let min_amplitude = amplitude.par_iter().copied().reduce(|| f32::MAX, f32::min);
682    amplitude
683        .par_iter()
684        .copied()
685        .zip(&mut mod_amplitude)
686        .for_each(|(a, dest)| {
687            let x = ((a - min_amplitude) / (max_amplitude - min_amplitude).max(f32::EPSILON) - 0.5)
688                * 2.0;
689            *dest = ((x * PI / 2.0).sin().powi(15) / 2.0 + 0.5) * (max_amplitude - min_amplitude)
690                + min_amplitude
691        });
692
693    let asr = {
694        let mut ifft_buffer = vec![Complex::zero(); phase.len()];
695        mod_amplitude
696            .par_iter()
697            .copied()
698            .zip(&phase)
699            .zip(&mut ifft_buffer)
700            .for_each(|((a, p), dest)| {
701                *dest = Complex {
702                    re: a * p.cos(),
703                    im: a * p.sin(),
704                };
705            });
706
707        let ifft_row = planner.plan_fft_inverse(image_width as usize);
708        let ifft_col = planner.plan_fft_inverse(image_height as usize);
709        ifft_buffer
710            .par_chunks_mut(image_height as usize)
711            .for_each(|chunk| {
712                ifft_col.process(chunk);
713            });
714
715        let mut transpose = vec![Complex::zero(); ifft_buffer.len()];
716        (0..image_width as usize)
717            .into_par_iter()
718            .zip(transpose.par_chunks_mut(image_height as usize))
719            .for_each(|(x, col)| {
720                (0..image_height as usize)
721                    .into_par_iter()
722                    .zip(col)
723                    .for_each(|(y, dest)| {
724                        *dest = ifft_buffer[y * image_width as usize + x];
725                    });
726            });
727
728        transpose
729            .par_chunks_mut(image_height as usize)
730            .for_each(|chunk| {
731                ifft_row.process(chunk);
732            });
733
734        (0..image_height as usize)
735            .into_par_iter()
736            .zip(ifft_buffer.par_chunks_mut(image_width as usize))
737            .for_each(|(y, row)| {
738                (0..image_width as usize)
739                    .into_par_iter()
740                    .zip(row)
741                    .for_each(|(x, dest)| {
742                        *dest = transpose[x * image_height as usize + y];
743                    });
744            });
745
746        let mut asr = vec![0.0f32; transpose.len()];
747        transpose
748            .par_iter()
749            .copied()
750            .zip(&mut asr)
751            .for_each(|(c, dest)| *dest = c.norm_sqr());
752
753        {
754            let mut asr =
755                BlurImageMut::borrow(&mut asr, image_width, image_height, FastBlurChannels::Plane);
756            stack_blur_f32(
757                &mut asr,
758                AnisotropicRadius::new(window_radius),
759                ThreadingPolicy::Adaptive,
760            )
761            .unwrap();
762        }
763
764        let max_asr = asr.par_iter().copied().reduce(|| f32::MIN, f32::max);
765        let min_asr = asr.par_iter().copied().reduce(|| f32::MAX, f32::min);
766        asr.par_iter_mut().for_each(|a| {
767            *a = (*a - min_asr) / (max_asr - min_asr).max(f32::EPSILON);
768        });
769
770        asr
771    };
772
773    let sr = {
774        let mut log_amplitude = vec![0.0f32; phase.len()];
775        amplitude
776            .into_par_iter()
777            .zip(&mut log_amplitude)
778            .for_each(|(a, dest)| *dest = a.max(f32::EPSILON).ln());
779
780        let mut log_blurred = log_amplitude.clone();
781
782        {
783            let mut amplitude = BlurImageMut::borrow(
784                &mut log_blurred,
785                image_width,
786                image_height,
787                FastBlurChannels::Plane,
788            );
789            stack_blur_f32(
790                &mut amplitude,
791                AnisotropicRadius::new(window_radius),
792                ThreadingPolicy::Adaptive,
793            )
794            .unwrap();
795        }
796
797        let mut residual = vec![0.0f32; image_width as usize * image_height as usize];
798        log_amplitude
799            .into_par_iter()
800            .zip(log_blurred)
801            .zip(&mut residual)
802            .for_each(|((a, b), dest)| {
803                *dest = a - b;
804            });
805
806        let sr_buffer = {
807            let ifft_row = planner.plan_fft_inverse(image_width as usize);
808            let ifft_col = planner.plan_fft_inverse(image_height as usize);
809
810            let mut buffer = vec![Complex::zero(); residual.len()];
811            residual
812                .into_par_iter()
813                .zip(phase.par_iter())
814                .zip(&mut buffer)
815                .for_each(|((a, p), dest)| {
816                    *dest = Complex {
817                        re: a.exp() * p.cos(),
818                        im: a.exp() * p.sin(),
819                    };
820                });
821
822            buffer
823                .par_chunks_mut(image_height as usize)
824                .for_each(|chunk| {
825                    ifft_col.process(chunk);
826                });
827
828            let mut transpose = vec![Complex::zero(); buffer.len()];
829            (0..image_width as usize)
830                .into_par_iter()
831                .zip(transpose.par_chunks_mut(image_height as usize))
832                .for_each(|(x, col)| {
833                    (0..image_height as usize)
834                        .into_par_iter()
835                        .zip(col)
836                        .for_each(|(y, dest)| {
837                            *dest = buffer[y * image_width as usize + x];
838                        });
839                });
840
841            transpose
842                .par_chunks_mut(image_height as usize)
843                .for_each(|chunk| {
844                    ifft_row.process(chunk);
845                });
846
847            (0..image_height as usize)
848                .into_par_iter()
849                .zip(buffer.par_chunks_mut(image_width as usize))
850                .for_each(|(y, row)| {
851                    (0..image_width as usize)
852                        .into_par_iter()
853                        .zip(row)
854                        .for_each(|(x, dest)| {
855                            *dest = transpose[x * image_height as usize + y];
856                        });
857                });
858
859            transpose
860        };
861
862        let mut saliency = vec![0.0f32; image_width as usize * image_height as usize];
863        sr_buffer
864            .into_par_iter()
865            .zip(&mut saliency)
866            .for_each(|(c, dest)| *dest = c.norm_sqr());
867
868        {
869            let mut saliency = BlurImageMut::borrow(
870                &mut saliency,
871                image_width,
872                image_height,
873                FastBlurChannels::Plane,
874            );
875            stack_blur_f32(
876                &mut saliency,
877                AnisotropicRadius::new(window_radius),
878                ThreadingPolicy::Adaptive,
879            )
880            .unwrap();
881        }
882
883        let max_saliency = saliency.par_iter().copied().reduce(|| f32::MIN, f32::max);
884        let min_saliency = saliency.par_iter().copied().reduce(|| f32::MAX, f32::min);
885        saliency.par_iter_mut().for_each(|s| {
886            *s = (*s - min_saliency) / (max_saliency - min_saliency).max(f32::EPSILON);
887        });
888
889        saliency
890    };
891
892    let msr = {
893        let mut log_amplitude = vec![0.0f32; phase.len()];
894        mod_amplitude
895            .into_par_iter()
896            .zip(&mut log_amplitude)
897            .for_each(|(a, dest)| *dest = a.max(f32::EPSILON).ln());
898
899        let mut log_blurred = log_amplitude.clone();
900
901        {
902            let mut amplitude = BlurImageMut::borrow(
903                &mut log_blurred,
904                image_width,
905                image_height,
906                FastBlurChannels::Plane,
907            );
908            stack_blur_f32(
909                &mut amplitude,
910                AnisotropicRadius::new(window_radius),
911                ThreadingPolicy::Adaptive,
912            )
913            .unwrap();
914        }
915
916        let mut residual = vec![0.0f32; image_width as usize * image_height as usize];
917        log_amplitude
918            .into_par_iter()
919            .zip(log_blurred)
920            .zip(&mut residual)
921            .for_each(|((a, b), dest)| {
922                *dest = a - b;
923            });
924
925        let mut mod_phase = vec![0.0f32; phase.len()];
926
927        let max_phase = phase.par_iter().copied().reduce(|| f32::MIN, f32::max);
928        let min_phase = phase.par_iter().copied().reduce(|| f32::MAX, f32::min);
929
930        phase
931            .into_par_iter()
932            .zip(&mut mod_phase)
933            .for_each(|(p, dest)| {
934                let x = ((p - min_phase) / (max_phase - min_phase).max(f32::EPSILON) - 0.5) * 2.0;
935                *dest = ((x * PI / 2.0).sin().powi(15) / 2.0 + 0.5) * (max_phase - min_phase)
936                    + min_phase;
937            });
938
939        let mut msr_buffer = vec![Complex::zero(); mod_phase.len()];
940        residual
941            .into_par_iter()
942            .zip(mod_phase)
943            .zip(&mut msr_buffer)
944            .for_each(|((a, p), dest)| {
945                *dest = Complex {
946                    re: a * p.cos(),
947                    im: a * p.sin(),
948                };
949            });
950
951        let ifft_row = planner.plan_fft_inverse(image_width as usize);
952        let ifft_col = planner.plan_fft_inverse(image_height as usize);
953        msr_buffer
954            .par_chunks_mut(image_height as usize)
955            .for_each(|chunk| {
956                ifft_col.process(chunk);
957            });
958
959        let mut transpose = vec![Complex::zero(); msr_buffer.len()];
960        (0..image_width as usize)
961            .into_par_iter()
962            .zip(transpose.par_chunks_mut(image_height as usize))
963            .for_each(|(x, col)| {
964                (0..image_height as usize)
965                    .into_par_iter()
966                    .zip(col)
967                    .for_each(|(y, dest)| {
968                        *dest = msr_buffer[y * image_width as usize + x];
969                    });
970            });
971
972        transpose
973            .par_chunks_mut(image_height as usize)
974            .for_each(|chunk| {
975                ifft_row.process(chunk);
976            });
977
978        (0..image_height as usize)
979            .into_par_iter()
980            .zip(msr_buffer.par_chunks_mut(image_width as usize))
981            .for_each(|(y, row)| {
982                (0..image_width as usize)
983                    .into_par_iter()
984                    .zip(row)
985                    .for_each(|(x, dest)| {
986                        *dest = transpose[x * image_height as usize + y];
987                    });
988            });
989
990        let mut msr = vec![0.0f32; transpose.len()];
991        transpose
992            .par_iter()
993            .copied()
994            .zip(&mut msr)
995            .for_each(|(c, dest)| *dest = c.norm_sqr());
996
997        {
998            let mut msr =
999                BlurImageMut::borrow(&mut msr, image_width, image_height, FastBlurChannels::Plane);
1000            stack_blur_f32(
1001                &mut msr,
1002                AnisotropicRadius::new(window_radius),
1003                ThreadingPolicy::Adaptive,
1004            )
1005            .unwrap();
1006        }
1007
1008        let max_msr = msr.par_iter().copied().reduce(|| f32::MIN, f32::max);
1009        let min_msr = msr.par_iter().copied().reduce(|| f32::MAX, f32::min);
1010        msr.par_iter_mut().for_each(|s| {
1011            *s = (*s - min_msr) / (max_msr - min_msr).max(f32::EPSILON);
1012        });
1013
1014        msr
1015    };
1016
1017    Saliency {
1018        spectral_residual: sr,
1019        mod_spectral_residual: msr,
1020        phase_spectrum: pft,
1021        amplitude_spectrum: asr,
1022    }
1023}
1024
1025#[cfg(any(
1026    feature = "dump_l_saliency",
1027    feature = "dump_a_saliency",
1028    feature = "dump_b_saliency",
1029    feature = "dump_dist_saliency",
1030    feature = "dump_local_saliency",
1031    feature = "dump_weights"
1032))]
1033fn dump_intermediate(name: &str, buffer: &[u8], width: u32, height: u32) {
1034    use std::hash::{
1035        BuildHasher,
1036        Hasher,
1037        RandomState,
1038    };
1039    let rand = BuildHasher::build_hasher(&RandomState::new()).finish();
1040    image::save_buffer(
1041        format!("{name}-{rand}.png"),
1042        buffer,
1043        width,
1044        height,
1045        image::ColorType::L8,
1046    )
1047    .unwrap()
1048}