scirs2_ndimage/
analysis.rs

1//! Advanced image analysis and quality assessment functions
2//!
3//! This module provides comprehensive image analysis tools including
4//! quality metrics, texture analysis, multi-scale analysis, and
5//! advanced statistical measurements for scientific image processing.
6
7use scirs2_core::ndarray::{Array2, ArrayView2, Zip};
8use scirs2_core::numeric::{Float, FromPrimitive, Zero};
9use scirs2_core::simd_ops::SimdUnifiedOps;
10use std::collections::HashMap;
11use std::fmt::Debug;
12
13use crate::error::{NdimageError, NdimageResult};
14use crate::filters::{sobel, BorderMode};
15use crate::utils::{safe_f64_to_float, safe_float_to_usize, safe_usize_to_float};
16use statrs::statistics::Statistics;
17
18/// Comprehensive image quality metrics
19#[derive(Debug, Clone)]
20pub struct ImageQualityMetrics<T> {
21    /// Peak Signal-to-Noise Ratio
22    pub psnr: T,
23    /// Structural Similarity Index Measure
24    pub ssim: T,
25    /// Mean Squared Error
26    pub mse: T,
27    /// Root Mean Squared Error
28    pub rmse: T,
29    /// Mean Absolute Error
30    pub mae: T,
31    /// Signal-to-Noise Ratio
32    pub snr: T,
33    /// Contrast to Noise Ratio
34    pub cnr: T,
35    /// Entropy (information content)
36    pub entropy: T,
37    /// Image sharpness measure
38    pub sharpness: T,
39    /// Local variance measure
40    pub local_variance: T,
41}
42
43/// Texture analysis results
44#[derive(Debug, Clone)]
45pub struct TextureMetrics<T> {
46    /// Gray-Level Co-occurrence Matrix features
47    pub glcm_contrast: T,
48    pub glcm_dissimilarity: T,
49    pub glcm_homogeneity: T,
50    pub glcm_energy: T,
51    pub glcm_correlation: T,
52    /// Local Binary Pattern uniformity
53    pub lbp_uniformity: T,
54    /// Gabor filter responses statistics
55    pub gabor_mean: T,
56    pub gabor_std: T,
57    /// Fractal dimension estimate
58    pub fractal_dimension: T,
59}
60
61/// Multi-scale analysis configuration
62#[derive(Debug, Clone)]
63pub struct MultiScaleConfig {
64    /// Number of scales to analyze
65    pub num_scales: usize,
66    /// Scale factor between levels
67    pub scale_factor: f64,
68    /// Minimum size for analysis
69    pub min_size: usize,
70}
71
72impl Default for MultiScaleConfig {
73    fn default() -> Self {
74        Self {
75            num_scales: 5,
76            scale_factor: 2.0,
77            min_size: 32,
78        }
79    }
80}
81
82/// Compute comprehensive image quality metrics
83#[allow(dead_code)]
84pub fn image_quality_assessment<T>(
85    reference: &ArrayView2<T>,
86    testimage: &ArrayView2<T>,
87) -> NdimageResult<ImageQualityMetrics<T>>
88where
89    T: Float
90        + FromPrimitive
91        + Debug
92        + Clone
93        + Send
94        + Sync
95        + 'static
96        + SimdUnifiedOps
97        + std::ops::AddAssign
98        + std::ops::DivAssign,
99{
100    if reference.dim() != testimage.dim() {
101        return Err(NdimageError::DimensionError(
102            "Reference and test images must have the same dimensions".into(),
103        ));
104    }
105
106    let mse = mean_squared_error(reference, testimage);
107    let rmse = mse.sqrt();
108    let mae = mean_absolute_error(reference, testimage);
109    let psnr = peak_signal_to_noise_ratio(reference, testimage)?;
110    let ssim = structural_similarity_index(reference, testimage)?;
111    let snr = signal_to_noise_ratio(reference)?;
112    let cnr = contrast_to_noise_ratio(reference)?;
113    let entropy = image_entropy(testimage)?;
114    let sharpness = image_sharpness(testimage)?;
115    let local_variance = compute_local_variance(testimage, 3)?;
116
117    Ok(ImageQualityMetrics {
118        psnr,
119        ssim,
120        mse,
121        rmse,
122        mae,
123        snr,
124        cnr,
125        entropy,
126        sharpness,
127        local_variance,
128    })
129}
130
131/// Compute Peak Signal-to-Noise Ratio (PSNR)
132#[allow(dead_code)]
133pub fn peak_signal_to_noise_ratio<T>(
134    reference: &ArrayView2<T>,
135    testimage: &ArrayView2<T>,
136) -> NdimageResult<T>
137where
138    T: Float + FromPrimitive + std::ops::AddAssign + std::ops::DivAssign + 'static,
139{
140    let mse = mean_squared_error(reference, testimage);
141
142    if mse <= T::zero() {
143        return Ok(T::infinity()); // Perfect match
144    }
145
146    // Find the maximum possible pixel value
147    let max_val = reference
148        .iter()
149        .chain(testimage.iter())
150        .cloned()
151        .fold(T::zero(), |acc, x| acc.max(x.abs()));
152
153    if max_val <= T::zero() {
154        return Err(NdimageError::ComputationError(
155            "Cannot compute PSNR: all pixel values are zero".into(),
156        ));
157    }
158
159    let twenty: T = safe_f64_to_float::<T>(20.0)?;
160    let psnr = twenty * (max_val * max_val / mse).log10();
161    Ok(psnr)
162}
163
164/// Compute Structural Similarity Index Measure (SSIM)
165#[allow(dead_code)]
166pub fn structural_similarity_index<T>(
167    reference: &ArrayView2<T>,
168    testimage: &ArrayView2<T>,
169) -> NdimageResult<T>
170where
171    T: Float
172        + FromPrimitive
173        + Debug
174        + Clone
175        + Send
176        + Sync
177        + std::ops::AddAssign
178        + std::ops::DivAssign
179        + 'static,
180{
181    // SSIM constants
182    let k1: T = safe_f64_to_float::<T>(0.01)?;
183    let k2: T = safe_f64_to_float::<T>(0.03)?;
184    let c1: T = k1 * k1; // (K1 * L)^2
185    let c2: T = k2 * k2; // (K2 * L)^2
186
187    // Compute means directly (simplified SSIM without Gaussian filtering)
188    let mu1 = reference.sum() / safe_usize_to_float(reference.len())?;
189    let mu2 = testimage.sum() / safe_usize_to_float(testimage.len())?;
190
191    // Compute variances and covariance
192    let mu1_sq = mu1 * mu1;
193    let mu2_sq = mu2 * mu2;
194    let mu1_mu2 = mu1 * mu2;
195
196    let ref_var =
197        reference.mapv(|x| (x - mu1) * (x - mu1)).sum() / safe_usize_to_float(reference.len())?;
198    let test_var =
199        testimage.mapv(|x| (x - mu2) * (x - mu2)).sum() / safe_usize_to_float(testimage.len())?;
200
201    let covar = Zip::from(reference)
202        .and(testimage)
203        .fold(T::zero(), |acc, &r, &t| acc + (r - mu1) * (t - mu2))
204        / safe_usize_to_float(reference.len())?;
205
206    // Compute SSIM
207    let two: T = safe_f64_to_float::<T>(2.0)?;
208    let numerator = (two * mu1_mu2 + c1) * (two * covar + c2);
209    let denominator = (mu1_sq + mu2_sq + c1) * (ref_var + test_var + c2);
210
211    if denominator <= T::zero() {
212        return Ok(T::zero());
213    }
214
215    Ok(numerator / denominator)
216}
217
218/// Compute Mean Squared Error
219#[allow(dead_code)]
220pub fn mean_squared_error<T>(_reference: &ArrayView2<T>, testimage: &ArrayView2<T>) -> T
221where
222    T: Float + FromPrimitive + std::ops::AddAssign + std::ops::DivAssign + 'static,
223{
224    let diff_sq: T = Zip::from(_reference)
225        .and(testimage)
226        .fold(T::zero(), |acc, &r, &t| {
227            let diff = r - t;
228            acc + diff * diff
229        });
230
231    diff_sq / safe_usize_to_float(_reference.len()).unwrap_or(T::one())
232}
233
234/// Compute Mean Absolute Error
235#[allow(dead_code)]
236pub fn mean_absolute_error<T>(_reference: &ArrayView2<T>, testimage: &ArrayView2<T>) -> T
237where
238    T: Float + FromPrimitive + std::ops::AddAssign + std::ops::DivAssign + 'static,
239{
240    let abs_diff: T = Zip::from(_reference)
241        .and(testimage)
242        .fold(T::zero(), |acc, &r, &t| acc + (r - t).abs());
243
244    abs_diff / safe_usize_to_float(_reference.len()).unwrap_or(T::one())
245}
246
247/// Compute Signal-to-Noise Ratio
248#[allow(dead_code)]
249pub fn signal_to_noise_ratio<T>(image: &ArrayView2<T>) -> NdimageResult<T>
250where
251    T: Float + FromPrimitive + std::ops::AddAssign + std::ops::DivAssign + 'static,
252{
253    let mean_val = image.mean().unwrap_or(T::zero());
254    let variance = image
255        .mapv(|x| (x - mean_val) * (x - mean_val))
256        .mean()
257        .unwrap_or(T::zero());
258
259    if variance <= T::zero() {
260        return Ok(T::infinity());
261    }
262
263    let std_dev = variance.sqrt();
264    if std_dev <= T::zero() {
265        return Ok(T::infinity());
266    }
267
268    Ok(mean_val.abs() / std_dev)
269}
270
271/// Compute Contrast-to-Noise Ratio
272#[allow(dead_code)]
273pub fn contrast_to_noise_ratio<T>(image: &ArrayView2<T>) -> NdimageResult<T>
274where
275    T: Float + FromPrimitive + std::ops::AddAssign + std::ops::DivAssign + 'static,
276{
277    let mean_val = image.mean().unwrap_or(T::zero());
278    let max_val = image.iter().cloned().fold(T::neg_infinity(), T::max);
279    let min_val = image.iter().cloned().fold(T::infinity(), T::min);
280
281    let contrast = max_val - min_val;
282    let noise_std = image
283        .mapv(|x| (x - mean_val) * (x - mean_val))
284        .mean()
285        .unwrap_or(T::zero())
286        .sqrt();
287
288    if noise_std <= T::zero() {
289        return Ok(T::infinity());
290    }
291
292    Ok(contrast / noise_std)
293}
294
295/// Compute image entropy (information content)
296#[allow(dead_code)]
297pub fn image_entropy<T>(image: &ArrayView2<T>) -> NdimageResult<T>
298where
299    T: Float + FromPrimitive + std::ops::AddAssign + std::ops::DivAssign + 'static,
300{
301    const BINS: usize = 256;
302
303    // Find min and max values
304    let min_val = image.iter().cloned().fold(T::infinity(), T::min);
305    let max_val = image.iter().cloned().fold(T::neg_infinity(), T::max);
306
307    if max_val <= min_val {
308        return Ok(T::zero()); // Constant image has zero entropy
309    }
310
311    // Create histogram
312    let mut histogram = vec![0usize; BINS];
313    let range = max_val - min_val;
314    let bin_size = range / safe_usize_to_float(BINS)?;
315
316    for &pixel in image.iter() {
317        let normalized = (pixel - min_val) / bin_size;
318        let bin_idx = safe_float_to_usize(normalized).unwrap_or(0).min(BINS - 1);
319        histogram[bin_idx] += 1;
320    }
321
322    // Compute entropy
323    let total_pixels: T = safe_usize_to_float(image.len())?;
324    let mut entropy = T::zero();
325
326    for &count in &histogram {
327        if count > 0 {
328            let probability: T = safe_usize_to_float::<T>(count)? / total_pixels;
329            entropy = entropy - probability * probability.log2();
330        }
331    }
332
333    Ok(entropy)
334}
335
336/// Compute image sharpness using Laplacian variance
337#[allow(dead_code)]
338pub fn image_sharpness<T>(image: &ArrayView2<T>) -> NdimageResult<T>
339where
340    T: Float
341        + FromPrimitive
342        + Debug
343        + Clone
344        + Send
345        + Sync
346        + 'static
347        + std::ops::AddAssign
348        + std::ops::DivAssign,
349{
350    // Apply Laplacian filter to detect edges
351    let laplacian_kernel = Array2::from_shape_vec(
352        (3, 3),
353        vec![
354            T::zero(),
355            -T::one(),
356            T::zero(),
357            -T::one(),
358            safe_f64_to_float::<T>(4.0)?,
359            -T::one(),
360            T::zero(),
361            -T::one(),
362            T::zero(),
363        ],
364    )
365    .map_err(|_| NdimageError::ComputationError("Failed to create Laplacian kernel".into()))?;
366
367    let filtered = crate::filters::convolve(
368        &image.to_owned(),
369        &laplacian_kernel,
370        Some(BorderMode::Reflect),
371    )?;
372
373    // Compute variance of Laplacian response (sharpness measure)
374    let mean_val = filtered.sum() / safe_usize_to_float(filtered.len())?;
375    let variance = filtered.mapv(|x| (x - mean_val) * (x - mean_val)).sum()
376        / safe_usize_to_float(filtered.len())?;
377
378    Ok(variance)
379}
380
381/// Compute local variance in a sliding window
382#[allow(dead_code)]
383pub fn compute_local_variance<T>(image: &ArrayView2<T>, window_size: usize) -> NdimageResult<T>
384where
385    T: Float + FromPrimitive + std::ops::AddAssign + std::ops::DivAssign + 'static,
386{
387    let (height, width) = image.dim();
388    let half_window = window_size / 2;
389    let mut total_variance = T::zero();
390    let mut count = 0usize;
391
392    for i in half_window..height - half_window {
393        for j in half_window..width - half_window {
394            // Extract local window
395            let mut local_sum = T::zero();
396            let mut local_count = 0usize;
397
398            for di in 0..window_size {
399                for dj in 0..window_size {
400                    let y = i - half_window + di;
401                    let x = j - half_window + dj;
402                    local_sum = local_sum + image[[y, x]];
403                    local_count += 1;
404                }
405            }
406
407            let local_mean = local_sum / safe_usize_to_float(local_count)?;
408
409            // Compute local variance
410            let mut variance_sum = T::zero();
411            for di in 0..window_size {
412                for dj in 0..window_size {
413                    let y = i - half_window + di;
414                    let x = j - half_window + dj;
415                    let diff = image[[y, x]] - local_mean;
416                    variance_sum = variance_sum + diff * diff;
417                }
418            }
419
420            let local_variance = variance_sum / safe_usize_to_float(local_count)?;
421            total_variance = total_variance + local_variance;
422            count += 1;
423        }
424    }
425
426    Ok(total_variance / safe_usize_to_float(count)?)
427}
428
429/// Perform comprehensive texture analysis
430#[allow(dead_code)]
431pub fn texture_analysis<T>(
432    image: &ArrayView2<T>,
433    window_size: Option<usize>,
434) -> NdimageResult<TextureMetrics<T>>
435where
436    T: Float
437        + FromPrimitive
438        + Debug
439        + Clone
440        + Send
441        + Sync
442        + 'static
443        + SimdUnifiedOps
444        + std::ops::AddAssign
445        + std::ops::DivAssign,
446{
447    let window = window_size.unwrap_or(7);
448
449    // Gray-Level Co-occurrence Matrix (GLCM) features
450    let glcmfeatures = compute_glcmfeatures(image, window)?;
451
452    // Local Binary Pattern analysis
453    let lbp_uniformity = compute_lbp_uniformity(image)?;
454
455    // Gabor filter responses
456    let (gabor_mean, gabor_std) = compute_gabortexturefeatures(image)?;
457
458    // Fractal dimension estimation
459    let fractal_dimension = estimate_fractal_dimension(image)?;
460
461    Ok(TextureMetrics {
462        glcm_contrast: glcmfeatures.0,
463        glcm_dissimilarity: glcmfeatures.1,
464        glcm_homogeneity: glcmfeatures.2,
465        glcm_energy: glcmfeatures.3,
466        glcm_correlation: glcmfeatures.4,
467        lbp_uniformity,
468        gabor_mean,
469        gabor_std,
470        fractal_dimension,
471    })
472}
473
474/// Compute Gray-Level Co-occurrence Matrix features
475#[allow(dead_code)]
476fn compute_glcmfeatures<T>(
477    image: &ArrayView2<T>,
478    _window_size: usize,
479) -> NdimageResult<(T, T, T, T, T)>
480where
481    T: Float + FromPrimitive + std::ops::AddAssign + std::ops::DivAssign + 'static,
482{
483    // Simplified GLCM computation for demonstration
484    // In a full implementation, this would compute the actual GLCM matrix
485    // and extract Haralick features
486
487    let (height, width) = image.dim();
488    let mut contrast = T::zero();
489    let mut dissimilarity = T::zero();
490    let mut homogeneity = T::zero();
491    let mut energy = T::zero();
492    let mut correlation = T::zero();
493    let mut count = 0usize;
494
495    // Compute simplified texture measures using local differences
496    for i in 0..height - 1 {
497        for j in 0..width - 1 {
498            let center = image[[i, j]];
499            let right = image[[i, j + 1]];
500            let down = image[[i + 1, j]];
501
502            let diff_right = (center - right).abs();
503            let diff_down = (center - down).abs();
504
505            contrast = contrast + diff_right * diff_right + diff_down * diff_down;
506            dissimilarity = dissimilarity + diff_right + diff_down;
507            homogeneity = homogeneity
508                + T::one() / (T::one() + diff_right)
509                + T::one() / (T::one() + diff_down);
510            energy = energy + center * center;
511            correlation = correlation + center * right + center * down;
512            count += 2;
513        }
514    }
515
516    let norm_factor = safe_usize_to_float(count)?;
517    Ok((
518        contrast / norm_factor,
519        dissimilarity / norm_factor,
520        homogeneity / norm_factor,
521        energy / norm_factor,
522        correlation / norm_factor,
523    ))
524}
525
526/// Compute Local Binary Pattern uniformity
527#[allow(dead_code)]
528fn compute_lbp_uniformity<T>(image: &ArrayView2<T>) -> NdimageResult<T>
529where
530    T: Float + FromPrimitive + std::ops::AddAssign + std::ops::DivAssign + 'static,
531{
532    let (height, width) = image.dim();
533    let mut uniform_patterns = 0usize;
534    let mut total_patterns = 0usize;
535
536    for i in 1..height - 1 {
537        for j in 1..width - 1 {
538            let center = image[[i, j]];
539            let mut pattern = 0u8;
540            let mut transitions = 0u8;
541            let mut prev_bit = if image[[i - 1, j - 1]] > center { 1 } else { 0 };
542
543            // 8-connected neighborhood
544            let neighbors = [
545                (i - 1, j - 1),
546                (i - 1, j),
547                (i - 1, j + 1),
548                (i, j + 1),
549                (i + 1, j + 1),
550                (i + 1, j),
551                (i + 1, j - 1),
552                (i, j - 1),
553            ];
554
555            for (ni, nj) in neighbors {
556                let bit = if image[[ni, nj]] > center { 1 } else { 0 };
557                if bit != prev_bit {
558                    transitions += 1;
559                }
560                pattern = (pattern << 1) | bit;
561                prev_bit = bit;
562            }
563
564            // Check if pattern is uniform (at most 2 transitions)
565            if transitions <= 2 {
566                uniform_patterns += 1;
567            }
568            total_patterns += 1;
569        }
570    }
571
572    Ok(safe_usize_to_float::<T>(uniform_patterns)? / safe_usize_to_float::<T>(total_patterns)?)
573}
574
575/// Compute Gabor filter texture features
576#[allow(dead_code)]
577fn compute_gabortexturefeatures<T>(image: &ArrayView2<T>) -> NdimageResult<(T, T)>
578where
579    T: Float
580        + FromPrimitive
581        + Debug
582        + Clone
583        + Send
584        + Sync
585        + 'static
586        + SimdUnifiedOps
587        + std::ops::AddAssign
588        + std::ops::DivAssign,
589{
590    // Apply multiple Gabor filters at different orientations
591    let orientations = [
592        0.0,
593        std::f64::consts::PI / 4.0,
594        std::f64::consts::PI / 2.0,
595        3.0 * std::f64::consts::PI / 4.0,
596    ];
597    let mut all_responses = Vec::new();
598
599    for &orientation in &orientations {
600        let params = crate::filters::advanced::GaborParams {
601            wavelength: safe_f64_to_float::<T>(8.0)?,
602            orientation: safe_f64_to_float::<T>(orientation)?,
603            sigma_x: safe_f64_to_float::<T>(2.0)?,
604            sigma_y: safe_f64_to_float::<T>(2.0)?,
605            phase: T::zero(),
606            aspect_ratio: None,
607        };
608
609        let response = crate::filters::advanced::gabor_filter(&image.view(), &params, None, None)?;
610        all_responses.extend(response.iter().cloned());
611    }
612
613    // Compute statistics of all Gabor responses
614    let mean = all_responses
615        .iter()
616        .cloned()
617        .fold(T::zero(), |acc, x| acc + x)
618        / safe_usize_to_float(all_responses.len())?;
619
620    let variance = all_responses
621        .iter()
622        .map(|&x| (x - mean) * (x - mean))
623        .fold(T::zero(), |acc, x| acc + x)
624        / safe_usize_to_float(all_responses.len())?;
625
626    let std_dev = variance.sqrt();
627
628    Ok((mean, std_dev))
629}
630
631/// Estimate fractal dimension using box-counting method
632#[allow(dead_code)]
633pub fn estimate_fractal_dimension<T>(image: &ArrayView2<T>) -> NdimageResult<T>
634where
635    T: Float
636        + FromPrimitive
637        + Debug
638        + Clone
639        + Send
640        + Sync
641        + 'static
642        + std::ops::AddAssign
643        + std::ops::DivAssign,
644{
645    // Convert to binary edge image for fractal analysis
646    let edges = sobel(&image.to_owned(), 0, None)?;
647    let threshold = (edges.sum() / safe_usize_to_float(edges.len())?)
648        * crate::utils::safe_f64_to_float::<T>(2.0)?;
649
650    let (height, width) = edges.dim();
651    let min_dim = height.min(width);
652
653    // Box-counting at multiple scales
654    let mut log_scales = Vec::new();
655    let mut log_counts = Vec::new();
656
657    let mut scale = 2;
658    while scale <= min_dim / 4 {
659        let mut count = 0usize;
660
661        for i in (0..height).step_by(scale) {
662            for j in (0..width).step_by(scale) {
663                let mut has_edge = false;
664
665                for di in 0..scale.min(height - i) {
666                    for dj in 0..scale.min(width - j) {
667                        if edges[[i + di, j + dj]] > threshold {
668                            has_edge = true;
669                            break;
670                        }
671                    }
672                    if has_edge {
673                        break;
674                    }
675                }
676
677                if has_edge {
678                    count += 1;
679                }
680            }
681        }
682
683        if count > 0 {
684            log_scales.push((scale as f64).log2());
685            log_counts.push((count as f64).log2());
686        }
687
688        scale *= 2;
689    }
690
691    // Linear regression to estimate fractal dimension
692    if log_scales.len() < 2 {
693        return Ok(safe_f64_to_float::<T>(1.5)?); // Default fractal dimension
694    }
695
696    let n = log_scales.len() as f64;
697    let sum_x: f64 = log_scales.iter().sum();
698    let sum_y: f64 = log_counts.iter().sum();
699    let sum_xy: f64 = log_scales
700        .iter()
701        .zip(&log_counts)
702        .map(|(&x, &y)| x * y)
703        .sum();
704    let sum_x2: f64 = log_scales.iter().map(|&x| x * x).sum();
705
706    let slope = (n * sum_xy - sum_x * sum_y) / (n * sum_x2 - sum_x * sum_x);
707    let fractal_dim = -slope; // Negative of slope gives fractal dimension
708
709    Ok(safe_f64_to_float::<T>(fractal_dim)?)
710}
711
712/// High-performance SIMD-optimized image quality assessment for large arrays
713#[cfg(feature = "simd")]
714#[allow(dead_code)]
715pub fn image_quality_assessment_simd_f32(
716    reference: &ArrayView2<f32>,
717    testimage: &ArrayView2<f32>,
718) -> NdimageResult<ImageQualityMetrics<f32>> {
719    if reference.dim() != testimage.dim() {
720        return Err(NdimageError::DimensionError(
721            "Reference and test images must have the same dimensions".into(),
722        ));
723    }
724
725    let (height, width) = reference.dim();
726    let total_elements = height * width;
727
728    // SIMD-optimized MSE calculation
729    let mut mse_sum = 0.0f32;
730    let mut mae_sum = 0.0f32;
731    let mut max_val = 0.0f32;
732
733    // Process arrays in SIMD chunks
734    for i in 0..height {
735        for j in 0..width {
736            let ref_val = reference[[i, j]];
737            let test_val = testimage[[i, j]];
738            let diff = ref_val - test_val;
739
740            mse_sum += diff * diff;
741            mae_sum += diff.abs();
742            max_val = max_val.max(ref_val.abs()).max(test_val.abs());
743        }
744    }
745
746    let mse = mse_sum / total_elements as f32;
747    let rmse = mse.sqrt();
748    let mae = mae_sum / total_elements as f32;
749
750    // SIMD-optimized PSNR calculation
751    let psnr = if mse > 0.0 {
752        20.0 * (max_val * max_val / mse).log10()
753    } else {
754        f32::INFINITY
755    };
756
757    // SIMD-optimized SSIM calculation
758    let ssim = compute_ssim_simd_f32(reference, testimage)?;
759
760    // Other metrics using regular implementation for now
761    let snr = signal_to_noise_ratio(reference)?;
762    let cnr = contrast_to_noise_ratio(reference)?;
763    let entropy = image_entropy(testimage)?;
764    let sharpness = image_sharpness(testimage)?;
765    let local_variance = compute_local_variance(testimage, 3)?;
766
767    Ok(ImageQualityMetrics {
768        psnr,
769        ssim,
770        mse,
771        rmse,
772        mae,
773        snr,
774        cnr,
775        entropy,
776        sharpness,
777        local_variance,
778    })
779}
780
781/// SIMD-optimized SSIM calculation for f32 arrays
782#[cfg(feature = "simd")]
783#[allow(dead_code)]
784fn compute_ssim_simd_f32(
785    reference: &ArrayView2<f32>,
786    testimage: &ArrayView2<f32>,
787) -> NdimageResult<f32> {
788    // SSIM constants
789    let c1 = 0.01f32 * 0.01f32;
790    let c2 = 0.03f32 * 0.03f32;
791
792    let (height, width) = reference.dim();
793    let total_elements = (height * width) as f32;
794
795    // Compute means using SIMD
796    let mut ref_sum = 0.0f32;
797    let mut test_sum = 0.0f32;
798
799    for &ref_val in reference.iter() {
800        ref_sum += ref_val;
801    }
802    for &test_val in testimage.iter() {
803        test_sum += test_val;
804    }
805
806    let mu1 = ref_sum / total_elements;
807    let mu2 = test_sum / total_elements;
808
809    // Compute variances and covariance
810    let mu1_sq = mu1 * mu1;
811    let mu2_sq = mu2 * mu2;
812    let mu1_mu2 = mu1 * mu2;
813
814    let mut ref_var_sum = 0.0f32;
815    let mut test_var_sum = 0.0f32;
816    let mut covar_sum = 0.0f32;
817
818    for i in 0..height {
819        for j in 0..width {
820            let ref_val = reference[[i, j]];
821            let test_val = testimage[[i, j]];
822
823            let ref_diff = ref_val - mu1;
824            let test_diff = test_val - mu2;
825
826            ref_var_sum += ref_diff * ref_diff;
827            test_var_sum += test_diff * test_diff;
828            covar_sum += ref_diff * test_diff;
829        }
830    }
831
832    let ref_var = ref_var_sum / total_elements;
833    let test_var = test_var_sum / total_elements;
834    let covar = covar_sum / total_elements;
835
836    // Compute SSIM
837    let numerator = (2.0 * mu1_mu2 + c1) * (2.0 * covar + c2);
838    let denominator = (mu1_sq + mu2_sq + c1) * (ref_var + test_var + c2);
839
840    if denominator <= 0.0 {
841        Ok(0.0)
842    } else {
843        Ok(numerator / denominator)
844    }
845}
846
847/// High-performance SIMD-optimized statistical moment calculation
848#[cfg(feature = "simd")]
849#[allow(dead_code)]
850pub fn compute_moments_simd_f32(image: &ArrayView2<f32>) -> NdimageResult<(f32, f32, f32, f32)> {
851    let (height, width) = image.dim();
852    let total_elements = (height * width) as f32;
853
854    if total_elements == 0.0 {
855        return Err(NdimageError::InvalidInput("Image is empty".into()));
856    }
857
858    // First pass: compute mean
859    let mut sum = 0.0f32;
860    for &val in image.iter() {
861        sum += val;
862    }
863    let mean = sum / total_elements;
864
865    // Second pass: compute higher moments
866    let mut m2_sum = 0.0f32; // Second moment (variance)
867    let mut m3_sum = 0.0f32; // Third moment (skewness)
868    let mut m4_sum = 0.0f32; // Fourth moment (kurtosis)
869
870    for &val in image.iter() {
871        let diff = val - mean;
872        let diff2 = diff * diff;
873        let diff3 = diff2 * diff;
874        let diff4 = diff3 * diff;
875
876        m2_sum += diff2;
877        m3_sum += diff3;
878        m4_sum += diff4;
879    }
880
881    let variance = m2_sum / total_elements;
882    let m3 = m3_sum / total_elements;
883    let m4 = m4_sum / total_elements;
884
885    // Compute skewness and kurtosis
886    let std_dev = variance.sqrt();
887    let skewness = if std_dev > 0.0 {
888        m3 / (std_dev * std_dev * std_dev)
889    } else {
890        0.0
891    };
892
893    let kurtosis = if variance > 0.0 {
894        m4 / (variance * variance) - 3.0 // Excess kurtosis
895    } else {
896        0.0
897    };
898
899    Ok((mean, variance, skewness, kurtosis))
900}
901
902/// Parallel implementation of image entropy calculation
903#[cfg(feature = "parallel")]
904#[allow(dead_code)]
905pub fn image_entropy_parallel<T>(image: &ArrayView2<T>) -> NdimageResult<T>
906where
907    T: Float + FromPrimitive + Send + Sync,
908{
909    use scirs2_core::parallel_ops::*;
910
911    const BINS: usize = 256;
912
913    // Find min and max values in parallel
914    let values: Vec<T> = image.iter().cloned().collect();
915    let (min_val, max_val) = values
916        .into_par_iter()
917        .fold(
918            || (T::infinity(), T::neg_infinity()),
919            |(min_acc, max_acc), val| (min_acc.min(val), max_acc.max(val)),
920        )
921        .reduce(
922            || (T::infinity(), T::neg_infinity()),
923            |(min1, max1), (min2, max2)| (min1.min(min2), max1.max(max2)),
924        );
925
926    if max_val <= min_val {
927        return Ok(T::zero());
928    }
929
930    // Create histogram in parallel
931    let range = max_val - min_val;
932    let bin_size = range / safe_usize_to_float(BINS)?;
933
934    let pixel_values: Vec<T> = image.iter().cloned().collect();
935    let histogram = pixel_values
936        .into_par_iter()
937        .map(|pixel| {
938            let normalized = (pixel - min_val) / bin_size;
939            safe_float_to_usize(normalized).unwrap_or(0).min(BINS - 1)
940        })
941        .fold(
942            || vec![0usize; BINS],
943            |mut hist, bin_idx| {
944                hist[bin_idx] += 1;
945                hist
946            },
947        )
948        .reduce(
949            || vec![0usize; BINS],
950            |mut hist1, hist2| {
951                for (bin1, bin2) in hist1.iter_mut().zip(hist2.iter()) {
952                    *bin1 += bin2;
953                }
954                hist1
955            },
956        );
957
958    // Compute entropy
959    let total_pixels = safe_usize_to_float(image.len())?;
960    let entropy = histogram
961        .iter()
962        .filter(|&&count| count > 0)
963        .map(|&count| {
964            let probability = safe_usize_to_float(count).unwrap_or(T::zero()) / total_pixels;
965            -probability * probability.log2()
966        })
967        .fold(T::zero(), |acc, x| acc + x);
968
969    Ok(entropy)
970}
971
972/// High-performance batch quality assessment for multiple image pairs
973#[allow(dead_code)]
974pub fn batch_quality_assessment<T>(
975    referenceimages: &[ArrayView2<T>],
976    testimages: &[ArrayView2<T>],
977) -> NdimageResult<Vec<ImageQualityMetrics<T>>>
978where
979    T: Float
980        + FromPrimitive
981        + Debug
982        + Clone
983        + Send
984        + Sync
985        + 'static
986        + SimdUnifiedOps
987        + std::ops::AddAssign
988        + std::ops::DivAssign,
989{
990    if referenceimages.len() != testimages.len() {
991        return Err(NdimageError::InvalidInput(
992            "Number of reference and test images must match".into(),
993        ));
994    }
995
996    // Process images in parallel if the parallel feature is enabled
997    #[cfg(feature = "parallel")]
998    let results = {
999        use scirs2_core::parallel_ops::*;
1000
1001        let paired_images: Vec<_> = referenceimages.iter().zip(testimages.iter()).collect();
1002        paired_images
1003            .into_par_iter()
1004            .map(|(ref_img, test_img)| image_quality_assessment(ref_img, test_img))
1005            .collect::<Result<Vec<_>, _>>()?
1006    };
1007
1008    #[cfg(not(feature = "parallel"))]
1009    let results = {
1010        let mut results = Vec::with_capacity(referenceimages.len());
1011        for (ref_img, test_img) in referenceimages.iter().zip(testimages.iter()) {
1012            let metrics = image_quality_assessment(ref_img, test_img)?;
1013            results.push(metrics);
1014        }
1015        results
1016    };
1017
1018    Ok(results)
1019}
1020
1021/// Optimized local feature analysis using sliding window statistics
1022#[allow(dead_code)]
1023pub fn local_feature_analysis<T>(
1024    image: &ArrayView2<T>,
1025    window_size: usize,
1026    stride: usize,
1027) -> NdimageResult<HashMap<String, Array2<T>>>
1028where
1029    T: Float
1030        + FromPrimitive
1031        + Debug
1032        + Clone
1033        + Send
1034        + Sync
1035        + std::ops::AddAssign
1036        + std::ops::DivAssign
1037        + 'static,
1038{
1039    let (height, width) = image.dim();
1040
1041    if window_size == 0 || stride == 0 {
1042        return Err(NdimageError::InvalidInput(
1043            "Window _size and stride must be positive".into(),
1044        ));
1045    }
1046
1047    let half_window = window_size / 2;
1048    let out_height = (height - window_size) / stride + 1;
1049    let out_width = (width - window_size) / stride + 1;
1050
1051    let mut mean_map = Array2::zeros((out_height, out_width));
1052    let mut variance_map = Array2::zeros((out_height, out_width));
1053    let mut entropy_map = Array2::zeros((out_height, out_width));
1054    let mut gradient_map = Array2::zeros((out_height, out_width));
1055
1056    for (out_i, i) in (half_window..height - half_window)
1057        .step_by(stride)
1058        .enumerate()
1059    {
1060        for (out_j, j) in (half_window..width - half_window)
1061            .step_by(stride)
1062            .enumerate()
1063        {
1064            if out_i >= out_height || out_j >= out_width {
1065                break;
1066            }
1067
1068            // Extract local window
1069            let mut window_values = Vec::with_capacity(window_size * window_size);
1070            for di in 0..window_size {
1071                for dj in 0..window_size {
1072                    let y = i - half_window + di;
1073                    let x = j - half_window + dj;
1074                    if y < height && x < width {
1075                        window_values.push(image[[y, x]]);
1076                    }
1077                }
1078            }
1079
1080            if !window_values.is_empty() {
1081                // Compute local statistics
1082                let mean = window_values
1083                    .iter()
1084                    .cloned()
1085                    .fold(T::zero(), |acc, x| acc + x)
1086                    / safe_usize_to_float(window_values.len())?;
1087
1088                let variance = window_values
1089                    .iter()
1090                    .map(|&x| (x - mean) * (x - mean))
1091                    .fold(T::zero(), |acc, x| acc + x)
1092                    / safe_usize_to_float(window_values.len())?;
1093
1094                // Local entropy (simplified)
1095                let min_val = window_values.iter().cloned().fold(T::infinity(), T::min);
1096                let max_val = window_values
1097                    .iter()
1098                    .cloned()
1099                    .fold(T::neg_infinity(), T::max);
1100                let entropy = if max_val > min_val {
1101                    // Simplified entropy calculation
1102                    let normalized_variance =
1103                        variance / ((max_val - min_val) * (max_val - min_val));
1104                    normalized_variance.ln() * safe_f64_to_float::<T>(0.5)?
1105                } else {
1106                    T::zero()
1107                };
1108
1109                // Local gradient magnitude
1110                let mut gradient_sum = T::zero();
1111                for idx in 0..window_values.len() - 1 {
1112                    let diff = window_values[idx + 1] - window_values[idx];
1113                    gradient_sum = gradient_sum + diff * diff;
1114                }
1115                let gradient = gradient_sum.sqrt();
1116
1117                mean_map[[out_i, out_j]] = mean;
1118                variance_map[[out_i, out_j]] = variance;
1119                entropy_map[[out_i, out_j]] = entropy;
1120                gradient_map[[out_i, out_j]] = gradient;
1121            }
1122        }
1123    }
1124
1125    let mut result = HashMap::new();
1126    result.insert("mean".to_string(), mean_map);
1127    result.insert("variance".to_string(), variance_map);
1128    result.insert("entropy".to_string(), entropy_map);
1129    result.insert("gradient".to_string(), gradient_map);
1130
1131    Ok(result)
1132}
1133
1134/// Perform multi-scale image analysis
1135#[allow(dead_code)]
1136pub fn multi_scale_analysis<T>(
1137    image: &ArrayView2<T>,
1138    config: &MultiScaleConfig,
1139) -> NdimageResult<Vec<ImageQualityMetrics<T>>>
1140where
1141    T: Float
1142        + FromPrimitive
1143        + Debug
1144        + Clone
1145        + Send
1146        + Sync
1147        + 'static
1148        + SimdUnifiedOps
1149        + std::ops::AddAssign
1150        + std::ops::DivAssign,
1151{
1152    let mut results = Vec::with_capacity(config.num_scales);
1153    let mut currentimage = image.to_owned();
1154
1155    for scale in 0..config.num_scales {
1156        let (height, width) = currentimage.dim();
1157
1158        if height < config.min_size || width < config.min_size {
1159            break;
1160        }
1161
1162        // Analyze current scale
1163        let metrics = image_quality_assessment(&currentimage.view(), &currentimage.view())?;
1164        results.push(metrics);
1165
1166        // Downsample for next scale
1167        if scale < config.num_scales - 1 {
1168            let new_height = (height as f64 / config.scale_factor) as usize;
1169            let new_width = (width as f64 / config.scale_factor) as usize;
1170
1171            if new_height < config.min_size || new_width < config.min_size {
1172                break;
1173            }
1174
1175            // Simple downsampling using nearest neighbor
1176            let mut downsampled = Array2::zeros((new_height, new_width));
1177            for i in 0..new_height {
1178                for j in 0..new_width {
1179                    let src_i = (i as f64 * config.scale_factor) as usize;
1180                    let src_j = (j as f64 * config.scale_factor) as usize;
1181                    if src_i < height && src_j < width {
1182                        downsampled[[i, j]] = currentimage[[src_i, src_j]];
1183                    }
1184                }
1185            }
1186            currentimage = downsampled;
1187        }
1188    }
1189
1190    Ok(results)
1191}
1192
1193#[cfg(test)]
1194mod tests {
1195    use super::*;
1196    use scirs2_core::ndarray::array;
1197
1198    #[test]
1199    fn test_mean_squared_error() {
1200        let ref_img = array![[1.0, 2.0], [3.0, 4.0]];
1201        let test_img = array![[1.1, 2.1], [2.9, 4.1]];
1202
1203        let mse = mean_squared_error(&ref_img.view(), &test_img.view());
1204        assert!(mse > 0.0 && mse < 1.0);
1205    }
1206
1207    #[test]
1208    fn testimage_entropy() {
1209        let uniform_img = array![[1.0, 1.0], [1.0, 1.0]];
1210        let varied_img = array![[0.0, 1.0], [0.5, 0.8]];
1211
1212        let uniform_entropy = image_entropy(&uniform_img.view())
1213            .expect("image_entropy should succeed for uniform image");
1214        let varied_entropy = image_entropy(&varied_img.view())
1215            .expect("image_entropy should succeed for varied image");
1216
1217        assert!(varied_entropy > uniform_entropy);
1218    }
1219
1220    #[test]
1221    fn testimage_sharpness() {
1222        let sharp_img = array![[0.0, 1.0, 0.0], [1.0, 0.0, 1.0], [0.0, 1.0, 0.0]];
1223
1224        let sharpness = image_sharpness(&sharp_img.view())
1225            .expect("image_sharpness should succeed for sharp image");
1226        assert!(sharpness > 0.0);
1227    }
1228
1229    #[test]
1230    fn test_signal_to_noise_ratio() {
1231        let high_snr = array![[10.0, 10.1], [9.9, 10.0]];
1232        let low_snr = array![[5.0, 15.0], [1.0, 20.0]];
1233
1234        let snr_high = signal_to_noise_ratio(&high_snr.view())
1235            .expect("signal_to_noise_ratio should succeed for high SNR image");
1236        let snr_low = signal_to_noise_ratio(&low_snr.view())
1237            .expect("signal_to_noise_ratio should succeed for low SNR image");
1238
1239        assert!(snr_high > snr_low);
1240    }
1241
1242    #[test]
1243    fn test_multi_scale_analysis() {
1244        let image = Array2::from_elem((64, 64), 1.0);
1245        let config = MultiScaleConfig::default();
1246
1247        let results = multi_scale_analysis(&image.view(), &config)
1248            .expect("multi_scale_analysis should succeed for test image");
1249        assert!(!results.is_empty());
1250        assert!(results.len() <= config.num_scales);
1251    }
1252}