Skip to main content

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