1use crate::error::{AnalyticsError, Result};
6use scirs2_core::ndarray::{Array2, ArrayView2, ArrayView3};
7use scirs2_core::num_traits::Float;
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq)]
11pub enum ChangeMethod {
12 Differencing,
14 CVA,
16 PCA,
18 NormalizedDifference,
20}
21
22#[derive(Debug, Clone)]
24pub struct ChangeResult {
25 pub magnitude: Array2<f64>,
27 pub binary_map: Array2<bool>,
29 pub threshold: f64,
31 pub method: ChangeMethod,
33 pub stats: ChangeStats,
35}
36
37#[derive(Debug, Clone)]
39pub struct ChangeStats {
40 pub mean_change: f64,
42 pub std_change: f64,
44 pub min_change: f64,
46 pub max_change: f64,
48 pub n_changed: usize,
50 pub percent_changed: f64,
52}
53
54pub struct ChangeDetector {
56 method: ChangeMethod,
57 threshold: Option<f64>,
58}
59
60impl ChangeDetector {
61 pub fn new(method: ChangeMethod) -> Self {
66 Self {
67 method,
68 threshold: None,
69 }
70 }
71
72 pub fn with_threshold(mut self, threshold: f64) -> Self {
74 self.threshold = Some(threshold);
75 self
76 }
77
78 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 let threshold = self
107 .threshold
108 .unwrap_or_else(|| ThresholdOptimizer::otsu(&magnitude.view()).unwrap_or(0.0));
109
110 let binary_map = magnitude.mapv(|x| x > threshold);
112
113 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 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 fn change_vector_analysis(
150 &self,
151 before: &ArrayView3<f64>,
152 after: &ArrayView3<f64>,
153 ) -> Result<Array2<f64>> {
154 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 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 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 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
253pub struct ImageDifferencing;
255
256impl ImageDifferencing {
257 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 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
295pub struct ChangeVectorAnalysis;
297
298impl ChangeVectorAnalysis {
299 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 if bands == 2 {
330 direction[[i, j]] = diff_vec[1].atan2(diff_vec[0]);
331 } else if bands >= 2 {
332 direction[[i, j]] = diff_vec[1].atan2(diff_vec[0]);
334 }
335 }
336 }
337
338 Ok((magnitude, direction))
339 }
340}
341
342pub struct PrincipalComponentAnalysis;
344
345impl PrincipalComponentAnalysis {
346 pub fn new() -> Self {
348 Self
349 }
350
351 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 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 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
404pub struct ThresholdOptimizer;
406
407impl ThresholdOptimizer {
408 pub fn otsu(data: &ArrayView2<f64>) -> Result<f64> {
416 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 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 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 let threshold = min + (threshold_idx as f64 / 255.0) * (max - min);
482
483 Ok(threshold)
484 }
485}
486
487use 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}