scirs2_signal/dwt2d_super_refined/
quality.rs1use super::types::*;
7use crate::error::{SignalError, SignalResult};
8use scirs2_core::ndarray::{Array2, Array3};
9use statrs::statistics::Statistics;
10
11pub fn compute_advanced_refined_quality_metrics(
13 original_image: &Array2<f64>,
14 processing_result: &ProcessingResult,
15 decomposition_tree: &DecompositionTree,
16 quality_config: &QualityConfig,
17) -> SignalResult<AdvancedRefinedQualityMetrics> {
18 let original_energy = compute_image_energy(original_image);
20 let coeffs_energy = compute_total_coefficients_energy(&processing_result.coefficients);
21 let energy_preservation = if original_energy > 0.0 {
22 coeffs_energy / original_energy
23 } else {
24 0.0
25 };
26
27 let sparsity = compute_sparsity(&processing_result.coefficients);
29
30 let compression_ratio = estimate_compression_ratio(&processing_result.coefficients);
32
33 let perceptual_quality = if quality_config.compute_perceptual_metrics {
35 compute_perceptual_quality(
36 original_image,
37 &processing_result.coefficients,
38 quality_config,
39 )?
40 } else {
41 0.0
42 };
43
44 let edge_preservation =
46 compute_multiscale_edge_preservation(original_image, &processing_result.coefficients)?;
47
48 let frequency_analysis = if quality_config.compute_frequency_analysis {
50 compute_frequency_analysis(&processing_result.coefficients)?
51 } else {
52 FrequencyAnalysis {
53 spectral_energy_distribution: Vec::new(),
54 dominant_frequencies: Vec::new(),
55 frequency_content_preservation: 0.0,
56 aliasing_artifacts: 0.0,
57 }
58 };
59
60 let compression_metrics = if quality_config.compute_compression_metrics {
62 compute_compression_metrics(&processing_result.coefficients)?
63 } else {
64 CompressionMetrics {
65 theoretical_compression_ratio: compression_ratio,
66 effective_compression_ratio: compression_ratio,
67 rate_distortion_score: 0.0,
68 }
69 };
70
71 Ok(AdvancedRefinedQualityMetrics {
72 perceptual_quality,
73 energy_preservation,
74 sparsity,
75 compression_ratio,
76 edge_preservation,
77 frequency_analysis,
78 compression_metrics,
79 })
80}
81
82pub fn compute_subband_energy(coefficients: &Array3<f64>, level: usize, index: usize) -> f64 {
84 let shape = coefficients.shape();
85 if level >= shape[0] {
86 return 0.0;
87 }
88
89 let level_slice = coefficients.slice(scirs2_core::ndarray::s![level, .., ..]);
90 let total_elements = level_slice.len();
91
92 if total_elements == 0 {
93 return 0.0;
94 }
95
96 level_slice.iter().map(|&x| x * x).sum()
99}
100
101pub fn compute_subband_entropy(coefficients: &Array3<f64>, level: usize, index: usize) -> f64 {
103 let shape = coefficients.shape();
104 if level >= shape[0] {
105 return 0.0;
106 }
107
108 let level_slice = coefficients.slice(scirs2_core::ndarray::s![level, .., ..]);
109 let level_vec: Vec<f64> = level_slice.iter().cloned().collect();
110
111 if level_vec.is_empty() {
112 return 0.0;
113 }
114
115 let sum_abs: f64 = level_vec.iter().map(|&x| x.abs()).sum();
117 if sum_abs == 0.0 {
118 return 0.0;
119 }
120
121 let mut entropy = 0.0;
122 for &coeff in &level_vec {
123 let prob = coeff.abs() / sum_abs;
124 if prob > 1e-12 {
125 entropy -= prob * prob.ln();
126 }
127 }
128
129 entropy
130}
131
132pub fn compute_approximation_energy(coefficients: &Array3<f64>) -> f64 {
134 compute_subband_energy(coefficients, 0, 0)
136}
137
138pub fn compute_detail_energy(coefficients: &Array3<f64>) -> f64 {
140 let shape = coefficients.shape();
141 let mut total_energy = 0.0;
142
143 for level in 0..shape[0] {
145 for index in 1..4 {
146 total_energy += compute_subband_energy(coefficients, level, index);
147 }
148 }
149
150 total_energy
151}
152
153pub fn compute_image_energy(image: &Array2<f64>) -> f64 {
155 image.iter().map(|&x| x * x).sum()
156}
157
158pub fn estimate_compression_ratio(coefficients: &Array3<f64>) -> f64 {
160 let total_coeffs = coefficients.len();
161 if total_coeffs == 0 {
162 return 1.0;
163 }
164
165 let non_zero_coeffs = coefficients.iter().filter(|&&x| x.abs() > 1e-12).count();
166
167 if non_zero_coeffs == 0 {
168 return f64::INFINITY;
169 }
170
171 total_coeffs as f64 / non_zero_coeffs as f64
172}
173
174pub fn compute_sparsity(coefficients: &Array3<f64>) -> f64 {
176 let total_coeffs = coefficients.len();
177 if total_coeffs == 0 {
178 return 0.0;
179 }
180
181 let zero_coeffs = coefficients.iter().filter(|&&x| x.abs() <= 1e-12).count();
182 zero_coeffs as f64 / total_coeffs as f64
183}
184
185pub fn compute_perceptual_quality(
187 original_image: &Array2<f64>,
188 coefficients: &Array3<f64>,
189 quality_config: &QualityConfig,
190) -> SignalResult<f64> {
191 let reconstructed = reconstruct_image_from_coefficients(coefficients)?;
193
194 if reconstructed.dim() != original_image.dim() {
196 let resized = resize_image_bilinear(&reconstructed, original_image.dim())?;
197 return compute_structural_similarity(original_image, &resized);
198 }
199
200 compute_structural_similarity(original_image, &reconstructed)
201}
202
203pub fn compute_structural_similarity(
205 image1: &Array2<f64>,
206 image2: &Array2<f64>,
207) -> SignalResult<f64> {
208 if image1.dim() != image2.dim() {
209 return Err(SignalError::ValueError(
210 "Images must have the same dimensions for SSIM calculation".to_string(),
211 ));
212 }
213
214 let (height, width) = image1.dim();
215 if height == 0 || width == 0 {
216 return Ok(0.0);
217 }
218
219 let mean1 = image1.mean().unwrap_or(0.0);
221 let mean2 = image2.mean().unwrap_or(0.0);
222
223 let var1 = image1.var(0.0);
225 let var2 = image2.var(0.0);
226
227 let mut covariance = 0.0;
228 for ((i, j), &val1) in image1.indexed_iter() {
229 let val2 = image2[[i, j]];
230 covariance += (val1 - mean1) * (val2 - mean2);
231 }
232 covariance /= (height * width) as f64;
233
234 let c1 = 0.01 * 0.01;
236 let c2 = 0.03 * 0.03;
237
238 let numerator = (2.0 * mean1 * mean2 + c1) * (2.0 * covariance + c2);
240 let denominator = (mean1 * mean1 + mean2 * mean2 + c1) * (var1 + var2 + c2);
241
242 if denominator > 0.0 {
243 Ok(numerator / denominator)
244 } else {
245 Ok(0.0)
246 }
247}
248
249pub fn compute_peak_snr(image: &Array2<f64>, coefficients: &Array3<f64>) -> SignalResult<f64> {
251 let reconstructed = reconstruct_image_from_coefficients(coefficients)?;
252
253 if reconstructed.dim() != image.dim() {
254 return Err(SignalError::ValueError(
255 "Reconstructed image dimensions don't match original".to_string(),
256 ));
257 }
258
259 let max_val = image.iter().cloned().fold(0.0f64, f64::max);
261 if max_val == 0.0 {
262 return Ok(f64::INFINITY);
263 }
264
265 let mut mse = 0.0;
267 let total_pixels = image.len();
268
269 for (i, (&orig, &recon)) in image.iter().zip(reconstructed.iter()).enumerate() {
270 let diff = orig - recon;
271 mse += diff * diff;
272 }
273 mse /= total_pixels as f64;
274
275 if mse == 0.0 {
276 Ok(f64::INFINITY)
277 } else {
278 Ok(20.0 * (max_val * max_val / mse).log10())
279 }
280}
281
282pub fn compute_multiscale_edge_preservation(
284 original_image: &Array2<f64>,
285 coefficients: &Array3<f64>,
286) -> SignalResult<f64> {
287 let reconstructed = reconstruct_image_from_coefficients(coefficients)?;
288
289 if reconstructed.dim() != original_image.dim() {
290 return Ok(0.0);
291 }
292
293 let mut total_preservation = 0.0;
294 let scales = vec![1, 2, 4];
295
296 for &scale in &scales {
297 let orig_edges = detect_edges_sobel(original_image, scale)?;
299 let recon_edges = detect_edges_sobel(&reconstructed, scale)?;
300
301 let correlation = compute_edge_correlation(&orig_edges, &recon_edges)?;
303 total_preservation += correlation;
304 }
305
306 Ok(total_preservation / scales.len() as f64)
307}
308
309pub fn compute_frequency_analysis(coefficients: &Array3<f64>) -> SignalResult<FrequencyAnalysis> {
311 let shape = coefficients.shape();
312 let levels = shape[0];
313
314 let mut spectral_energy_distribution = Vec::with_capacity(levels);
316 for level in 0..levels {
317 let level_energy = compute_subband_energy(coefficients, level, 0)
318 + compute_subband_energy(coefficients, level, 1)
319 + compute_subband_energy(coefficients, level, 2)
320 + compute_subband_energy(coefficients, level, 3);
321 spectral_energy_distribution.push(level_energy);
322 }
323
324 let mut dominant_frequencies = Vec::new();
326 let total_energy: f64 = spectral_energy_distribution.iter().sum();
327
328 for (level, &energy) in spectral_energy_distribution.iter().enumerate() {
329 if energy / total_energy > 0.1 {
330 let freq = 1.0 / (2.0_f64.powi(level as i32 + 1));
332 dominant_frequencies.push(freq);
333 }
334 }
335
336 let frequency_content_preservation = if total_energy > 0.0 {
338 let high_freq_energy: f64 = spectral_energy_distribution.iter().skip(levels / 2).sum();
339 high_freq_energy / total_energy
340 } else {
341 0.0
342 };
343
344 let aliasing_artifacts = estimate_aliasing_artifacts(&spectral_energy_distribution);
346
347 Ok(FrequencyAnalysis {
348 spectral_energy_distribution,
349 dominant_frequencies,
350 frequency_content_preservation,
351 aliasing_artifacts,
352 })
353}
354
355fn estimate_aliasing_artifacts(spectral_distribution: &[f64]) -> f64 {
357 if spectral_distribution.len() < 2 {
358 return 0.0;
359 }
360
361 let total_energy: f64 = spectral_distribution.iter().sum();
363 if total_energy == 0.0 {
364 return 0.0;
365 }
366
367 let high_freq_start = spectral_distribution.len() / 2;
368 let high_freq_energy: f64 = spectral_distribution.iter().skip(high_freq_start).sum();
369
370 (high_freq_energy / total_energy).min(1.0)
372}
373
374pub fn compute_compression_metrics(coefficients: &Array3<f64>) -> SignalResult<CompressionMetrics> {
376 let theoretical_compression_ratio = estimate_compression_ratio(coefficients);
377
378 let effective_compression_ratio = theoretical_compression_ratio * 0.8; let sparsity = compute_sparsity(coefficients);
383 let rate_distortion_score = sparsity * theoretical_compression_ratio;
384
385 Ok(CompressionMetrics {
386 theoretical_compression_ratio,
387 effective_compression_ratio,
388 rate_distortion_score,
389 })
390}
391
392pub fn reconstruct_image_from_coefficients(
394 coefficients: &Array3<f64>,
395) -> SignalResult<Array2<f64>> {
396 let shape = coefficients.shape();
397 if shape.len() != 3 || shape[0] == 0 {
398 return Err(SignalError::ValueError(
399 "Invalid coefficients shape for reconstruction".to_string(),
400 ));
401 }
402
403 let (_, height, width) = (shape[0], shape[1], shape[2]);
404
405 if height == 0 || width == 0 {
406 return Err(SignalError::ValueError(
407 "Invalid image dimensions for reconstruction".to_string(),
408 ));
409 }
410
411 let mut reconstructed = Array2::zeros((height, width));
413
414 for level in 0..shape[0] {
415 let weight = 1.0 / (2.0_f64.powi(level as i32)); for y in 0..height {
417 for x in 0..width {
418 reconstructed[[y, x]] += weight * coefficients[[level, y, x]];
419 }
420 }
421 }
422
423 Ok(reconstructed)
424}
425
426pub fn resize_image_bilinear(
428 image: &Array2<f64>,
429 target_size: (usize, usize),
430) -> SignalResult<Array2<f64>> {
431 let (src_height, src_width) = image.dim();
432 let (target_height, target_width) = target_size;
433
434 if target_height == 0 || target_width == 0 {
435 return Err(SignalError::ValueError(
436 "Target size must be positive".to_string(),
437 ));
438 }
439
440 if src_height == 0 || src_width == 0 {
441 return Ok(Array2::zeros(target_size));
442 }
443
444 let mut resized = Array2::zeros(target_size);
445
446 let scale_y = src_height as f64 / target_height as f64;
447 let scale_x = src_width as f64 / target_width as f64;
448
449 for y in 0..target_height {
450 for x in 0..target_width {
451 let src_y = y as f64 * scale_y;
452 let src_x = x as f64 * scale_x;
453
454 let y0 = src_y.floor() as usize;
455 let x0 = src_x.floor() as usize;
456 let y1 = (y0 + 1).min(src_height - 1);
457 let x1 = (x0 + 1).min(src_width - 1);
458
459 let dy = src_y - y0 as f64;
460 let dx = src_x - x0 as f64;
461
462 let val = (1.0 - dy) * (1.0 - dx) * image[[y0, x0]]
464 + (1.0 - dy) * dx * image[[y0, x1]]
465 + dy * (1.0 - dx) * image[[y1, x0]]
466 + dy * dx * image[[y1, x1]];
467
468 resized[[y, x]] = val;
469 }
470 }
471
472 Ok(resized)
473}
474
475pub fn detect_edges_sobel(image: &Array2<f64>, scale: usize) -> SignalResult<Array2<f64>> {
477 let (height, width) = image.dim();
478
479 if height < 3 || width < 3 {
480 return Ok(Array2::zeros((height, width)));
481 }
482
483 let mut edges = Array2::zeros((height, width));
484
485 let sobel_x = [[-1.0, 0.0, 1.0], [-2.0, 0.0, 2.0], [-1.0, 0.0, 1.0]];
487 let sobel_y = [[-1.0, -2.0, -1.0], [0.0, 0.0, 0.0], [1.0, 2.0, 1.0]];
488
489 for y in scale..(height - scale) {
490 for x in scale..(width - scale) {
491 let mut gx = 0.0;
492 let mut gy = 0.0;
493
494 for ky in 0..3 {
496 for kx in 0..3 {
497 let pixel = image[[y + ky - 1, x + kx - 1]];
498 gx += pixel * sobel_x[ky][kx];
499 gy += pixel * sobel_y[ky][kx];
500 }
501 }
502
503 edges[[y, x]] = (gx * gx + gy * gy).sqrt();
505 }
506 }
507
508 Ok(edges)
509}
510
511pub fn compute_edge_correlation(edges1: &Array2<f64>, edges2: &Array2<f64>) -> SignalResult<f64> {
513 if edges1.dim() != edges2.dim() {
514 return Err(SignalError::ValueError(
515 "Edge maps must have the same dimensions".to_string(),
516 ));
517 }
518
519 let n = edges1.len();
520 if n == 0 {
521 return Ok(0.0);
522 }
523
524 let mean1 = edges1.mean().unwrap_or(0.0);
525 let mean2 = edges2.mean().unwrap_or(0.0);
526
527 let mut numerator = 0.0;
528 let mut var1 = 0.0;
529 let mut var2 = 0.0;
530
531 for (&val1, &val2) in edges1.iter().zip(edges2.iter()) {
532 let diff1 = val1 - mean1;
533 let diff2 = val2 - mean2;
534 numerator += diff1 * diff2;
535 var1 += diff1 * diff1;
536 var2 += diff2 * diff2;
537 }
538
539 let denominator = (var1 * var2).sqrt();
540 if denominator > 0.0 {
541 Ok(numerator / denominator)
542 } else {
543 Ok(0.0)
544 }
545}
546
547fn compute_total_coefficients_energy(coefficients: &Array3<f64>) -> f64 {
549 coefficients.iter().map(|&x| x * x).sum()
550}
551
552#[cfg(test)]
553mod tests {
554 use super::*;
555 use scirs2_core::ndarray::Array3;
556
557 #[test]
558 fn test_compute_image_energy() {
559 let image = Array2::from_shape_fn((4, 4), |(i, j)| (i + j) as f64);
560 let energy = compute_image_energy(&image);
561 assert!(energy > 0.0);
562 }
563
564 #[test]
565 fn test_compute_sparsity() {
566 let mut coefficients = Array3::zeros((2, 4, 4));
567 coefficients[[0, 0, 0]] = 1.0;
568 coefficients[[0, 1, 1]] = 2.0;
569
570 let sparsity = compute_sparsity(&coefficients);
571 assert!(sparsity > 0.9); }
573
574 #[test]
575 fn test_structural_similarity() {
576 let image1 = Array2::from_shape_fn((8, 8), |(i, j)| (i + j) as f64);
577 let image2 = image1.clone();
578
579 let ssim = compute_structural_similarity(&image1, &image2).unwrap();
580 assert!((ssim - 1.0).abs() < 0.01); }
582
583 #[test]
584 fn test_resize_image_bilinear() {
585 let image = Array2::from_shape_fn((4, 4), |(i, j)| (i * j) as f64);
586 let resized = resize_image_bilinear(&image, (8, 8)).unwrap();
587
588 assert_eq!(resized.dim(), (8, 8));
589 }
590
591 #[test]
592 fn test_detect_edges_sobel() {
593 let image = Array2::from_shape_fn((8, 8), |(i, j)| {
594 if i < 4 {
595 0.0
596 } else {
597 1.0
598 } });
600
601 let edges = detect_edges_sobel(&image, 1).unwrap();
602 assert_eq!(edges.dim(), image.dim());
603
604 let middle_row_energy: f64 = edges.row(4).iter().map(|&x| x * x).sum();
606 assert!(middle_row_energy > 0.0);
607 }
608}