Skip to main content

oxigdal_analytics/change/
detection.rs

1//! Change Detection Algorithms
2//!
3//! Implementations of various change detection methods for multi-temporal analysis.
4
5use crate::error::{AnalyticsError, Result};
6use scirs2_core::ndarray::{Array2, ArrayView2, ArrayView3};
7use scirs2_core::num_traits::Float;
8
9/// Change detection methods
10#[derive(Debug, Clone, Copy, PartialEq, Eq)]
11pub enum ChangeMethod {
12    /// Simple image differencing
13    Differencing,
14    /// Change Vector Analysis
15    CVA,
16    /// Principal Component Analysis
17    PCA,
18    /// Normalized difference
19    NormalizedDifference,
20}
21
22/// Change detection result
23#[derive(Debug, Clone)]
24pub struct ChangeResult {
25    /// Change magnitude map
26    pub magnitude: Array2<f64>,
27    /// Binary change map (based on threshold)
28    pub binary_map: Array2<bool>,
29    /// Threshold used for binary classification
30    pub threshold: f64,
31    /// Method used
32    pub method: ChangeMethod,
33    /// Additional statistics
34    pub stats: ChangeStats,
35}
36
37/// Change detection statistics
38#[derive(Debug, Clone)]
39pub struct ChangeStats {
40    /// Mean change magnitude
41    pub mean_change: f64,
42    /// Standard deviation of change
43    pub std_change: f64,
44    /// Minimum change value
45    pub min_change: f64,
46    /// Maximum change value
47    pub max_change: f64,
48    /// Number of changed pixels
49    pub n_changed: usize,
50    /// Percentage of changed pixels
51    pub percent_changed: f64,
52}
53
54/// Change detector
55pub struct ChangeDetector {
56    method: ChangeMethod,
57    threshold: Option<f64>,
58}
59
60impl ChangeDetector {
61    /// Create a new change detector
62    ///
63    /// # Arguments
64    /// * `method` - Change detection method
65    pub fn new(method: ChangeMethod) -> Self {
66        Self {
67            method,
68            threshold: None,
69        }
70    }
71
72    /// Set threshold for binary classification
73    pub fn with_threshold(mut self, threshold: f64) -> Self {
74        self.threshold = Some(threshold);
75        self
76    }
77
78    /// Detect changes between two images
79    ///
80    /// # Arguments
81    /// * `before` - Image before change (height × width × bands)
82    /// * `after` - Image after change (height × width × bands)
83    ///
84    /// # Errors
85    /// Returns error if images have different dimensions
86    pub fn detect(
87        &self,
88        before: &ArrayView3<f64>,
89        after: &ArrayView3<f64>,
90    ) -> Result<ChangeResult> {
91        if before.dim() != after.dim() {
92            return Err(AnalyticsError::dimension_mismatch(
93                format!("{:?}", before.dim()),
94                format!("{:?}", after.dim()),
95            ));
96        }
97
98        let magnitude = match self.method {
99            ChangeMethod::Differencing => self.image_differencing(before, after)?,
100            ChangeMethod::CVA => self.change_vector_analysis(before, after)?,
101            ChangeMethod::PCA => self.pca_change_detection(before, after)?,
102            ChangeMethod::NormalizedDifference => self.normalized_difference(before, after)?,
103        };
104
105        // Determine threshold if not provided
106        let threshold = self
107            .threshold
108            .unwrap_or_else(|| ThresholdOptimizer::otsu(&magnitude.view()).unwrap_or(0.0));
109
110        // Create binary change map
111        let binary_map = magnitude.mapv(|x| x > threshold);
112
113        // Calculate statistics
114        let stats = self.calculate_stats(&magnitude, &binary_map)?;
115
116        Ok(ChangeResult {
117            magnitude,
118            binary_map,
119            threshold,
120            method: self.method,
121            stats,
122        })
123    }
124
125    /// Simple image differencing
126    fn image_differencing(
127        &self,
128        before: &ArrayView3<f64>,
129        after: &ArrayView3<f64>,
130    ) -> Result<Array2<f64>> {
131        let (height, width, bands) = before.dim();
132        let mut magnitude = Array2::zeros((height, width));
133
134        for i in 0..height {
135            for j in 0..width {
136                let mut sum_sq = 0.0;
137                for b in 0..bands {
138                    let diff = after[[i, j, b]] - before[[i, j, b]];
139                    sum_sq += diff * diff;
140                }
141                magnitude[[i, j]] = sum_sq.sqrt();
142            }
143        }
144
145        Ok(magnitude)
146    }
147
148    /// Change Vector Analysis (CVA)
149    fn change_vector_analysis(
150        &self,
151        before: &ArrayView3<f64>,
152        after: &ArrayView3<f64>,
153    ) -> Result<Array2<f64>> {
154        // CVA computes the magnitude of change vector in feature space
155        let (height, width, bands) = before.dim();
156        let mut magnitude = Array2::zeros((height, width));
157
158        for i in 0..height {
159            for j in 0..width {
160                let mut sum_sq = 0.0;
161                for b in 0..bands {
162                    let diff = after[[i, j, b]] - before[[i, j, b]];
163                    sum_sq += diff * diff;
164                }
165                magnitude[[i, j]] = sum_sq.sqrt();
166            }
167        }
168
169        Ok(magnitude)
170    }
171
172    /// PCA-based change detection
173    fn pca_change_detection(
174        &self,
175        before: &ArrayView3<f64>,
176        after: &ArrayView3<f64>,
177    ) -> Result<Array2<f64>> {
178        let pca = PrincipalComponentAnalysis::new();
179        pca.detect_change(before, after)
180    }
181
182    /// Normalized difference
183    fn normalized_difference(
184        &self,
185        before: &ArrayView3<f64>,
186        after: &ArrayView3<f64>,
187    ) -> Result<Array2<f64>> {
188        let (height, width, bands) = before.dim();
189        let mut magnitude = Array2::zeros((height, width));
190
191        for i in 0..height {
192            for j in 0..width {
193                let mut sum_diff = 0.0;
194                let mut sum_sum = 0.0;
195
196                for b in 0..bands {
197                    let b_val = before[[i, j, b]];
198                    let a_val = after[[i, j, b]];
199                    sum_diff += (a_val - b_val).abs();
200                    sum_sum += a_val + b_val;
201                }
202
203                magnitude[[i, j]] = if sum_sum > f64::EPSILON {
204                    sum_diff / sum_sum
205                } else {
206                    0.0
207                };
208            }
209        }
210
211        Ok(magnitude)
212    }
213
214    /// Calculate change statistics
215    fn calculate_stats(
216        &self,
217        magnitude: &Array2<f64>,
218        binary_map: &Array2<bool>,
219    ) -> Result<ChangeStats> {
220        let n_pixels = magnitude.len();
221        let n_changed = binary_map.iter().filter(|&&x| x).count();
222
223        let mean_change = magnitude.sum() / (n_pixels as f64);
224        let variance = magnitude
225            .iter()
226            .map(|&x| (x - mean_change).powi(2))
227            .sum::<f64>()
228            / (n_pixels as f64);
229        let std_change = variance.sqrt();
230
231        let min_change = magnitude
232            .iter()
233            .copied()
234            .min_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal))
235            .unwrap_or(0.0);
236        let max_change = magnitude
237            .iter()
238            .copied()
239            .max_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal))
240            .unwrap_or(0.0);
241
242        Ok(ChangeStats {
243            mean_change,
244            std_change,
245            min_change,
246            max_change,
247            n_changed,
248            percent_changed: (n_changed as f64 / n_pixels as f64) * 100.0,
249        })
250    }
251}
252
253/// Image differencing utility
254pub struct ImageDifferencing;
255
256impl ImageDifferencing {
257    /// Compute absolute difference between two images
258    pub fn absolute_difference(
259        before: &ArrayView2<f64>,
260        after: &ArrayView2<f64>,
261    ) -> Result<Array2<f64>> {
262        if before.dim() != after.dim() {
263            return Err(AnalyticsError::dimension_mismatch(
264                format!("{:?}", before.dim()),
265                format!("{:?}", after.dim()),
266            ));
267        }
268
269        Ok((after - before).mapv(|x| x.abs()))
270    }
271
272    /// Compute ratio between two images
273    pub fn ratio(before: &ArrayView2<f64>, after: &ArrayView2<f64>) -> Result<Array2<f64>> {
274        if before.dim() != after.dim() {
275            return Err(AnalyticsError::dimension_mismatch(
276                format!("{:?}", before.dim()),
277                format!("{:?}", after.dim()),
278            ));
279        }
280
281        let mut ratio = Array2::zeros(before.dim());
282        for ((i, j), &b_val) in before.indexed_iter() {
283            let a_val = after[[i, j]];
284            ratio[[i, j]] = if b_val.abs() > f64::EPSILON {
285                a_val / b_val
286            } else {
287                0.0
288            };
289        }
290
291        Ok(ratio)
292    }
293}
294
295/// Change Vector Analysis
296pub struct ChangeVectorAnalysis;
297
298impl ChangeVectorAnalysis {
299    /// Compute change magnitude and direction
300    pub fn analyze(
301        before: &ArrayView3<f64>,
302        after: &ArrayView3<f64>,
303    ) -> Result<(Array2<f64>, Array2<f64>)> {
304        if before.dim() != after.dim() {
305            return Err(AnalyticsError::dimension_mismatch(
306                format!("{:?}", before.dim()),
307                format!("{:?}", after.dim()),
308            ));
309        }
310
311        let (height, width, bands) = before.dim();
312        let mut magnitude = Array2::zeros((height, width));
313        let mut direction = Array2::zeros((height, width));
314
315        for i in 0..height {
316            for j in 0..width {
317                let mut sum_sq = 0.0;
318                let mut diff_vec = Vec::with_capacity(bands);
319
320                for b in 0..bands {
321                    let diff = after[[i, j, b]] - before[[i, j, b]];
322                    diff_vec.push(diff);
323                    sum_sq += diff * diff;
324                }
325
326                magnitude[[i, j]] = sum_sq.sqrt();
327
328                // Calculate direction (angle in radians) for 2-band case
329                if bands == 2 {
330                    direction[[i, j]] = diff_vec[1].atan2(diff_vec[0]);
331                } else if bands >= 2 {
332                    // For multi-band, use first two bands
333                    direction[[i, j]] = diff_vec[1].atan2(diff_vec[0]);
334                }
335            }
336        }
337
338        Ok((magnitude, direction))
339    }
340}
341
342/// Principal Component Analysis for change detection
343pub struct PrincipalComponentAnalysis;
344
345impl PrincipalComponentAnalysis {
346    /// Create new PCA change detector
347    pub fn new() -> Self {
348        Self
349    }
350
351    /// Detect change using PCA
352    pub fn detect_change(
353        &self,
354        before: &ArrayView3<f64>,
355        after: &ArrayView3<f64>,
356    ) -> Result<Array2<f64>> {
357        if before.dim() != after.dim() {
358            return Err(AnalyticsError::dimension_mismatch(
359                format!("{:?}", before.dim()),
360                format!("{:?}", after.dim()),
361            ));
362        }
363
364        let (height, width, bands) = before.dim();
365
366        // Stack images for PCA
367        let n_pixels = height * width;
368        let mut stacked = Array2::zeros((n_pixels, bands * 2));
369
370        for b in 0..bands {
371            let before_band = before.slice(s![.., .., b]);
372            let after_band = after.slice(s![.., .., b]);
373
374            for (idx, (b_val, a_val)) in before_band.iter().zip(after_band.iter()).enumerate() {
375                stacked[[idx, b]] = *b_val;
376                stacked[[idx, b + bands]] = *a_val;
377            }
378        }
379
380        // Simplified PCA: compute variance along time dimension
381        let mut magnitude = Array2::zeros((height, width));
382        for i in 0..height {
383            for j in 0..width {
384                let idx = i * width + j;
385                let mut sum_sq = 0.0;
386                for b in 0..bands {
387                    let diff = stacked[[idx, b + bands]] - stacked[[idx, b]];
388                    sum_sq += diff * diff;
389                }
390                magnitude[[i, j]] = sum_sq.sqrt();
391            }
392        }
393
394        Ok(magnitude)
395    }
396}
397
398impl Default for PrincipalComponentAnalysis {
399    fn default() -> Self {
400        Self::new()
401    }
402}
403
404/// Threshold optimization
405pub struct ThresholdOptimizer;
406
407impl ThresholdOptimizer {
408    /// Otsu's method for automatic threshold selection
409    ///
410    /// # Arguments
411    /// * `data` - Change magnitude map
412    ///
413    /// # Errors
414    /// Returns error if computation fails
415    pub fn otsu(data: &ArrayView2<f64>) -> Result<f64> {
416        // Normalize data to 0-255 range for histogram
417        let min = data
418            .iter()
419            .copied()
420            .min_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal))
421            .ok_or_else(|| AnalyticsError::insufficient_data("Empty data"))?;
422        let max = data
423            .iter()
424            .copied()
425            .max_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal))
426            .ok_or_else(|| AnalyticsError::insufficient_data("Empty data"))?;
427
428        if (max - min).abs() < f64::EPSILON {
429            return Ok(min);
430        }
431
432        // Build histogram
433        const N_BINS: usize = 256;
434        let mut histogram = vec![0usize; N_BINS];
435
436        for &value in data.iter() {
437            let normalized = ((value - min) / (max - min) * 255.0).clamp(0.0, 255.0);
438            let bin = normalized as usize;
439            if bin < N_BINS {
440                histogram[bin] += 1;
441            }
442        }
443
444        // Find optimal threshold using Otsu's method
445        let total_pixels = data.len();
446        let mut sum = 0.0;
447        for (i, &count) in histogram.iter().enumerate() {
448            sum += (i as f64) * (count as f64);
449        }
450
451        let mut sum_b = 0.0;
452        let mut weight_b = 0;
453        let mut max_variance = 0.0;
454        let mut threshold_idx = 0;
455
456        for (t, &count) in histogram.iter().enumerate() {
457            weight_b += count;
458            if weight_b == 0 {
459                continue;
460            }
461
462            let weight_f = total_pixels - weight_b;
463            if weight_f == 0 {
464                break;
465            }
466
467            sum_b += (t as f64) * (count as f64);
468
469            let mean_b = sum_b / (weight_b as f64);
470            let mean_f = (sum - sum_b) / (weight_f as f64);
471
472            let variance = (weight_b as f64) * (weight_f as f64) * (mean_b - mean_f).powi(2);
473
474            if variance > max_variance {
475                max_variance = variance;
476                threshold_idx = t;
477            }
478        }
479
480        // Convert threshold back to original scale
481        let threshold = min + (threshold_idx as f64 / 255.0) * (max - min);
482
483        Ok(threshold)
484    }
485}
486
487// Import ndarray slice macro
488use scirs2_core::ndarray::s;
489
490#[cfg(test)]
491mod tests {
492    use super::*;
493    use scirs2_core::ndarray::Array;
494
495    #[test]
496    fn test_image_differencing() {
497        let before = Array::from_shape_vec((2, 2, 1), vec![1.0, 2.0, 3.0, 4.0])
498            .expect("Failed to create before array with shape (2, 2, 1)");
499        let after = Array::from_shape_vec((2, 2, 1), vec![2.0, 3.0, 4.0, 5.0])
500            .expect("Failed to create after array with shape (2, 2, 1)");
501
502        let detector = ChangeDetector::new(ChangeMethod::Differencing).with_threshold(0.5);
503        let result = detector
504            .detect(&before.view(), &after.view())
505            .expect("Change detection should succeed with valid inputs");
506
507        assert_eq!(result.magnitude.dim(), (2, 2));
508        assert!(result.stats.n_changed > 0);
509    }
510
511    #[test]
512    fn test_absolute_difference() {
513        let before = Array::from_shape_vec((2, 2), vec![1.0, 2.0, 3.0, 4.0])
514            .expect("Failed to create before array with shape (2, 2)");
515        let after = Array::from_shape_vec((2, 2), vec![2.0, 3.0, 4.0, 5.0])
516            .expect("Failed to create after array with shape (2, 2)");
517
518        let diff = ImageDifferencing::absolute_difference(&before.view(), &after.view())
519            .expect("Absolute difference computation should succeed");
520
521        assert_eq!(diff[[0, 0]], 1.0);
522        assert_eq!(diff[[1, 1]], 1.0);
523    }
524
525    #[test]
526    fn test_otsu_threshold() {
527        let data =
528            Array::from_shape_vec((3, 3), vec![1.0, 1.0, 1.0, 5.0, 5.0, 5.0, 10.0, 10.0, 10.0])
529                .expect("Failed to create data array with shape (3, 3)");
530
531        let threshold = ThresholdOptimizer::otsu(&data.view())
532            .expect("Otsu threshold computation should succeed");
533
534        assert!(threshold > 1.0 && threshold < 10.0);
535    }
536}