1use 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#[derive(Debug, Clone)]
21pub struct ImageQualityMetrics<T> {
22 pub psnr: T,
24 pub ssim: T,
26 pub mse: T,
28 pub rmse: T,
30 pub mae: T,
32 pub snr: T,
34 pub cnr: T,
36 pub entropy: T,
38 pub sharpness: T,
40 pub local_variance: T,
42}
43
44#[derive(Debug, Clone)]
46pub struct TextureMetrics<T> {
47 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 pub lbp_uniformity: T,
55 pub gabor_mean: T,
57 pub gabor_std: T,
58 pub fractal_dimension: T,
60}
61
62#[derive(Debug, Clone)]
64pub struct MultiScaleConfig {
65 pub num_scales: usize,
67 pub scale_factor: f64,
69 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#[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#[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()); }
146
147 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#[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 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; let c2: T = k2 * k2; let mu1 = reference.sum() / safe_usize_to_float(reference.len())?;
190 let mu2 = testimage.sum() / safe_usize_to_float(testimage.len())?;
191
192 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 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#[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#[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#[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#[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#[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 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()); }
311
312 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 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#[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 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 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#[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 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 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#[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 let glcmfeatures = compute_glcmfeatures(image, window)?;
452
453 let lbp_uniformity = compute_lbp_uniformity(image)?;
455
456 let (gabor_mean, gabor_std) = compute_gabortexturefeatures(image)?;
458
459 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#[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 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 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#[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 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 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#[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 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(), ¶ms, None, None)?;
611 all_responses.extend(response.iter().cloned());
612 }
613
614 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#[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 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 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 if log_scales.len() < 2 {
694 return Ok(safe_f64_to_float::<T>(1.5)?); }
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; Ok(safe_f64_to_float::<T>(fractal_dim)?)
711}
712
713#[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 let mut mse_sum = 0.0f32;
731 let mut mae_sum = 0.0f32;
732 let mut max_val = 0.0f32;
733
734 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 let psnr = if mse > 0.0 {
753 20.0 * (max_val * max_val / mse).log10()
754 } else {
755 f32::INFINITY
756 };
757
758 let ssim = compute_ssim_simd_f32(reference, testimage)?;
760
761 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#[cfg(feature = "simd")]
784#[allow(dead_code)]
785fn compute_ssim_simd_f32(
786 reference: &ArrayView2<f32>,
787 testimage: &ArrayView2<f32>,
788) -> NdimageResult<f32> {
789 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 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 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 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#[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 let mut sum = 0.0f32;
861 for &val in image.iter() {
862 sum += val;
863 }
864 let mean = sum / total_elements;
865
866 let mut m2_sum = 0.0f32; let mut m3_sum = 0.0f32; let mut m4_sum = 0.0f32; 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 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 } else {
897 0.0
898 };
899
900 Ok((mean, variance, skewness, kurtosis))
901}
902
903#[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 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 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 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#[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 #[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#[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 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 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 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 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 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#[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 let metrics = image_quality_assessment(¤timage.view(), ¤timage.view())?;
1165 results.push(metrics);
1166
1167 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 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}