scirs2_signal/dwt2d_super_refined/
quality.rs

1//! Quality metrics and analysis functions for advanced-refined 2D wavelet transforms
2//!
3//! This module provides comprehensive quality assessment including perceptual metrics,
4//! compression analysis, frequency domain analysis, and edge preservation evaluation.
5
6use super::types::*;
7use crate::error::{SignalError, SignalResult};
8use scirs2_core::ndarray::{Array2, Array3};
9use statrs::statistics::Statistics;
10
11/// Compute comprehensive quality metrics for advanced-refined wavelet analysis
12pub 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    // Compute basic energy preservation
19    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    // Compute sparsity measure
28    let sparsity = compute_sparsity(&processing_result.coefficients);
29
30    // Compute compression ratio
31    let compression_ratio = estimate_compression_ratio(&processing_result.coefficients);
32
33    // Compute perceptual quality if enabled
34    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    // Compute edge preservation
45    let edge_preservation =
46        compute_multiscale_edge_preservation(original_image, &processing_result.coefficients)?;
47
48    // Compute frequency analysis if enabled
49    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    // Compute compression metrics if enabled
61    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
82/// Compute energy of subband at specific level and index
83pub 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    // For simplicity, compute energy for the entire level
97    // In practice, this would extract specific subband based on index
98    level_slice.iter().map(|&x| x * x).sum()
99}
100
101/// Compute entropy of subband at specific level and index
102pub 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    // Compute Shannon entropy
116    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
132/// Compute approximation energy from coefficients
133pub fn compute_approximation_energy(coefficients: &Array3<f64>) -> f64 {
134    // Approximation is typically at level 0
135    compute_subband_energy(coefficients, 0, 0)
136}
137
138/// Compute detail energy from coefficients
139pub fn compute_detail_energy(coefficients: &Array3<f64>) -> f64 {
140    let shape = coefficients.shape();
141    let mut total_energy = 0.0;
142
143    // Sum energy from all detail subbands (all levels except level 0, index 0)
144    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
153/// Compute total image energy
154pub fn compute_image_energy(image: &Array2<f64>) -> f64 {
155    image.iter().map(|&x| x * x).sum()
156}
157
158/// Estimate compression ratio from coefficients
159pub 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
174/// Compute sparsity measure
175pub 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
185/// Compute perceptual quality using reference image
186pub fn compute_perceptual_quality(
187    original_image: &Array2<f64>,
188    coefficients: &Array3<f64>,
189    quality_config: &QualityConfig,
190) -> SignalResult<f64> {
191    // Reconstruct image from coefficients
192    let reconstructed = reconstruct_image_from_coefficients(coefficients)?;
193
194    // Ensure same dimensions
195    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
203/// Compute structural similarity (SSIM)
204pub 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    // Compute means
220    let mean1 = image1.mean().unwrap_or(0.0);
221    let mean2 = image2.mean().unwrap_or(0.0);
222
223    // Compute variances and covariance
224    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    // SSIM constants
235    let c1 = 0.01 * 0.01;
236    let c2 = 0.03 * 0.03;
237
238    // Compute SSIM
239    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
249/// Compute Peak Signal-to-Noise Ratio
250pub 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    // Find maximum possible pixel value
260    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    // Compute MSE
266    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
282/// Compute multiscale edge preservation
283pub 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        // Detect edges at this scale
298        let orig_edges = detect_edges_sobel(original_image, scale)?;
299        let recon_edges = detect_edges_sobel(&reconstructed, scale)?;
300
301        // Compute correlation between edge maps
302        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
309/// Compute frequency domain analysis
310pub fn compute_frequency_analysis(coefficients: &Array3<f64>) -> SignalResult<FrequencyAnalysis> {
311    let shape = coefficients.shape();
312    let levels = shape[0];
313
314    // Compute spectral energy distribution across levels
315    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    // Find dominant frequencies (simplified - based on energy distribution)
325    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            // Frequency approximately inversely proportional to level
331            let freq = 1.0 / (2.0_f64.powi(level as i32 + 1));
332            dominant_frequencies.push(freq);
333        }
334    }
335
336    // Estimate frequency content preservation (simplified)
337    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    // Estimate aliasing artifacts (simplified)
345    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
355/// Estimate aliasing artifacts from spectral distribution
356fn estimate_aliasing_artifacts(spectral_distribution: &[f64]) -> f64 {
357    if spectral_distribution.len() < 2 {
358        return 0.0;
359    }
360
361    // Look for unexpected energy in high-frequency bands
362    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    // Aliasing metric: excessive high-frequency energy relative to expected decay
371    (high_freq_energy / total_energy).min(1.0)
372}
373
374/// Compute compression performance metrics
375pub fn compute_compression_metrics(coefficients: &Array3<f64>) -> SignalResult<CompressionMetrics> {
376    let theoretical_compression_ratio = estimate_compression_ratio(coefficients);
377
378    // Effective compression considers quantization effects
379    let effective_compression_ratio = theoretical_compression_ratio * 0.8; // Simplified
380
381    // Rate-distortion score (simplified implementation)
382    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
392/// Reconstruct image from coefficients (simplified)
393pub 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    // Simplified reconstruction: sum across levels (weighted by level)
412    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)); // Higher levels have less weight
416        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
426/// Resize image using bilinear interpolation
427pub 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            // Bilinear interpolation
463            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
475/// Detect edges using Sobel operator at given scale
476pub 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    // Sobel kernels
486    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            // Apply Sobel operators
495            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            // Compute gradient magnitude
504            edges[[y, x]] = (gx * gx + gy * gy).sqrt();
505        }
506    }
507
508    Ok(edges)
509}
510
511/// Compute correlation between edge maps
512pub 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
547/// Compute total energy from all coefficients
548fn 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); // Most coefficients are zero
572    }
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); // Should be very close to 1 for identical images
581    }
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            } // Step edge
599        });
600
601        let edges = detect_edges_sobel(&image, 1).unwrap();
602        assert_eq!(edges.dim(), image.dim());
603
604        // Check that edge is detected around the middle
605        let middle_row_energy: f64 = edges.row(4).iter().map(|&x| x * x).sum();
606        assert!(middle_row_energy > 0.0);
607    }
608}