1use 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#[derive(Debug, Clone)]
20pub struct ImageQualityMetrics<T> {
21 pub psnr: T,
23 pub ssim: T,
25 pub mse: T,
27 pub rmse: T,
29 pub mae: T,
31 pub snr: T,
33 pub cnr: T,
35 pub entropy: T,
37 pub sharpness: T,
39 pub local_variance: T,
41}
42
43#[derive(Debug, Clone)]
45pub struct TextureMetrics<T> {
46 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 pub lbp_uniformity: T,
54 pub gabor_mean: T,
56 pub gabor_std: T,
57 pub fractal_dimension: T,
59}
60
61#[derive(Debug, Clone)]
63pub struct MultiScaleConfig {
64 pub num_scales: usize,
66 pub scale_factor: f64,
68 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#[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#[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()); }
145
146 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#[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 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; let c2: T = k2 * k2; let mu1 = reference.sum() / safe_usize_to_float(reference.len())?;
189 let mu2 = testimage.sum() / safe_usize_to_float(testimage.len())?;
190
191 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 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#[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#[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#[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#[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#[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 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()); }
310
311 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 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#[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 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 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#[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 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 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#[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 let glcmfeatures = compute_glcmfeatures(image, window)?;
451
452 let lbp_uniformity = compute_lbp_uniformity(image)?;
454
455 let (gabor_mean, gabor_std) = compute_gabortexturefeatures(image)?;
457
458 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#[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 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 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#[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 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 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#[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 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(), ¶ms, None, None)?;
610 all_responses.extend(response.iter().cloned());
611 }
612
613 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#[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 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 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 if log_scales.len() < 2 {
693 return Ok(safe_f64_to_float::<T>(1.5)?); }
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; Ok(safe_f64_to_float::<T>(fractal_dim)?)
710}
711
712#[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 let mut mse_sum = 0.0f32;
730 let mut mae_sum = 0.0f32;
731 let mut max_val = 0.0f32;
732
733 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 let psnr = if mse > 0.0 {
752 20.0 * (max_val * max_val / mse).log10()
753 } else {
754 f32::INFINITY
755 };
756
757 let ssim = compute_ssim_simd_f32(reference, testimage)?;
759
760 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#[cfg(feature = "simd")]
783#[allow(dead_code)]
784fn compute_ssim_simd_f32(
785 reference: &ArrayView2<f32>,
786 testimage: &ArrayView2<f32>,
787) -> NdimageResult<f32> {
788 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 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 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 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#[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 let mut sum = 0.0f32;
860 for &val in image.iter() {
861 sum += val;
862 }
863 let mean = sum / total_elements;
864
865 let mut m2_sum = 0.0f32; let mut m3_sum = 0.0f32; let mut m4_sum = 0.0f32; 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 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 } else {
896 0.0
897 };
898
899 Ok((mean, variance, skewness, kurtosis))
900}
901
902#[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 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 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 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#[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 #[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#[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 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 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 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 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 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#[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 let metrics = image_quality_assessment(¤timage.view(), ¤timage.view())?;
1164 results.push(metrics);
1165
1166 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 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}