1use scirs2_core::ndarray::{Array2, Array3, ArrayView2, ArrayView3};
7use scirs2_core::numeric::{Float, FromPrimitive};
8use std::fmt::Debug;
9
10use crate::error::{NdimageError, NdimageResult};
11use crate::utils::{safe_f64_to_float, safe_float_to_f64, safe_usize_to_float};
12
13use crate::filters::{gaussian_filter, median_filter};
14use crate::interpolation::{zoom, InterpolationOrder};
15use crate::measurements::{center_of_mass, central_moments, moments};
16use crate::morphology::label;
17use crate::morphology::{binary_closing, binary_opening, grey_opening};
18
19pub mod medical {
21 use super::*;
22
23 #[derive(Clone, Debug)]
25 pub struct VesselEnhancementParams {
26 pub scales: Vec<f64>,
28 pub alpha: f64, pub beta: f64, pub gamma: f64, }
33
34 impl Default for VesselEnhancementParams {
35 fn default() -> Self {
36 Self {
37 scales: vec![1.0, 2.0, 3.0, 4.0],
38 alpha: 0.5,
39 beta: 0.5,
40 gamma: 15.0,
41 }
42 }
43 }
44
45 pub fn frangi_vesselness<T>(
47 image: &ArrayView2<T>,
48 params: Option<VesselEnhancementParams>,
49 ) -> NdimageResult<Array2<f64>>
50 where
51 T: Float + FromPrimitive + Debug + Send + Sync + 'static,
52 {
53 let params = params.unwrap_or_default();
54 let (height, width) = image.dim();
55 let mut vesselness = Array2::<f64>::zeros((height, width));
56
57 let img = image.mapv(|x| x.to_f64().unwrap_or(0.0));
59
60 for &scale in ¶ms.scales {
62 let smoothed = gaussian_filter(&img, scale, None, None)?;
64 let hessian = compute_hessian_2d(&smoothed.view(), scale)?;
65
66 for i in 0..height {
68 for j in 0..width {
69 let hxx = hessian.0[[i, j]];
70 let hxy = hessian.1[[i, j]];
71 let hyy = hessian.2[[i, j]];
72
73 let trace = hxx + hyy;
75 let det = hxx * hyy - hxy * hxy;
76 let discriminant = trace * trace - 4.0 * det;
77
78 if discriminant >= 0.0 {
79 let sqrt_disc = discriminant.sqrt();
80 let lambda1 = (trace + sqrt_disc) / 2.0;
81 let lambda2 = (trace - sqrt_disc) / 2.0;
82
83 let (l1, l2) = if lambda1.abs() > lambda2.abs() {
85 (lambda1, lambda2)
86 } else {
87 (lambda2, lambda1)
88 };
89
90 if l2 < 0.0 {
92 let rb = l1.abs() / l2.abs().max(1e-10);
94 let s = (l1 * l1 + l2 * l2).sqrt();
95
96 let v = (1.0 - (-rb * rb / (2.0 * params.beta * params.beta)).exp())
97 * (-s * s / (2.0 * params.gamma * params.gamma)).exp();
98
99 vesselness[[i, j]] = vesselness[[i, j]].max(v);
100 }
101 }
102 }
103 }
104 }
105
106 Ok(vesselness)
107 }
108
109 fn compute_hessian_2d(
111 image: &ArrayView2<f64>,
112 scale: f64,
113 ) -> NdimageResult<(Array2<f64>, Array2<f64>, Array2<f64>)> {
114 let (height, width) = image.dim();
115 let mut hxx = Array2::zeros((height, width));
116 let mut hxy = Array2::zeros((height, width));
117 let mut hyy = Array2::zeros((height, width));
118
119 let norm = scale * scale;
121
122 for i in 2..height - 2 {
123 for j in 2..width - 2 {
124 hxx[[i, j]] = (image[[i, j + 1]] - 2.0 * image[[i, j]] + image[[i, j - 1]]) * norm;
126 hyy[[i, j]] = (image[[i + 1, j]] - 2.0 * image[[i, j]] + image[[i - 1, j]]) * norm;
127 hxy[[i, j]] =
128 (image[[i + 1, j + 1]] - image[[i + 1, j - 1]] - image[[i - 1, j + 1]]
129 + image[[i - 1, j - 1]])
130 * norm
131 / 4.0;
132 }
133 }
134
135 Ok((hxx, hxy, hyy))
136 }
137
138 pub fn enhance_bone_structure<T>(
140 image: &ArrayView2<T>,
141 kernel_size: usize,
142 ) -> NdimageResult<Array2<T>>
143 where
144 T: Float
145 + FromPrimitive
146 + Debug
147 + Send
148 + Sync
149 + std::ops::AddAssign
150 + std::ops::DivAssign
151 + scirs2_core::ndarray::ScalarOperand
152 + 'static,
153 {
154 let structure = crate::morphology::disk_structure(kernel_size as f64, None)?;
156 let structure_2d = structure.into_dimensionality::<scirs2_core::ndarray::Ix2>()?;
157 let opened = grey_opening(
158 &image.to_owned(),
159 None,
160 Some(&structure_2d),
161 None,
162 None,
163 None,
164 )?;
165 let top_hat = image.to_owned() - opened;
166
167 let two = safe_f64_to_float::<T>(2.0)?;
169 let enhanced = image.to_owned() + top_hat * two;
170
171 Ok(enhanced)
172 }
173
174 pub fn detect_lung_nodules<T>(
176 ct_slice: &ArrayView2<T>,
177 min_size: usize,
178 max_size: usize,
179 ) -> NdimageResult<Vec<Nodule>>
180 where
181 T: Float
182 + FromPrimitive
183 + Debug
184 + Send
185 + Sync
186 + scirs2_core::numeric::NumAssign
187 + std::ops::DivAssign
188 + 'static,
189 {
190 let mut nodules = Vec::new();
191
192 let threshold = safe_f64_to_float::<T>(-500.0)?; let lung_mask = ct_slice.mapv(|x| x > threshold);
195
196 let cleaned = binary_closing(&lung_mask, None, Some(3), None, None, None, None)?;
198 let cleaned = binary_opening(&cleaned, None, Some(2), None, None, None, None)?;
199
200 let (labels, num_features) = label(&cleaned, None, None, None)?;
202
203 for i in 1..=num_features {
205 let component_mask = labels.mapv(|x| x == i);
206 let size = component_mask.iter().filter(|&&x| x).count();
207
208 if size >= min_size && size <= max_size {
209 let com = center_of_mass(&ct_slice.to_owned())?;
211
212 let coords: Vec<(usize, usize)> = component_mask
214 .indexed_iter()
215 .filter(|(_, &val)| val)
216 .map(|((y, x), _)| (y, x))
217 .collect();
218
219 let cy = com[0].to_f64().unwrap_or(0.0);
220 let cx = com[1].to_f64().unwrap_or(0.0);
221 let mean_radius = coords
222 .iter()
223 .map(|&(y, x)| {
224 let dy = y as f64 - cy;
225 let dx = x as f64 - cx;
226 (dy * dy + dx * dx).sqrt()
227 })
228 .sum::<f64>()
229 / coords.len() as f64;
230
231 let radius_variance = coords
232 .iter()
233 .map(|&(y, x)| {
234 let dy = y as f64 - cy;
235 let dx = x as f64 - cx;
236 let r = (dy * dy + dx * dx).sqrt();
237 (r - mean_radius).powi(2)
238 })
239 .sum::<f64>()
240 / coords.len() as f64;
241
242 let circularity = 1.0 / (1.0 + radius_variance / mean_radius.powi(2));
243
244 nodules.push(Nodule {
245 center: (cy, cx),
246 size,
247 circularity,
248 mean_intensity: ct_slice
249 .indexed_iter()
250 .filter(|((y, x), _)| component_mask[[*y, *x]])
251 .map(|(_, &val)| safe_float_to_f64(val).unwrap_or(0.0))
252 .sum::<f64>()
253 / size as f64,
254 });
255 }
256 }
257
258 Ok(nodules)
259 }
260
261 #[derive(Clone, Debug)]
263 pub struct Nodule {
264 pub center: (f64, f64),
265 pub size: usize,
266 pub circularity: f64,
267 pub mean_intensity: f64,
268 }
269}
270
271pub mod satellite {
273 use super::*;
274
275 pub fn compute_ndvi<T>(
277 red_band: &ArrayView2<T>,
278 nir_band: &ArrayView2<T>,
279 ) -> NdimageResult<Array2<f64>>
280 where
281 T: Float + FromPrimitive,
282 {
283 if red_band.dim() != nir_band.dim() {
284 return Err(NdimageError::DimensionError(
285 "Red and NIR bands must have same dimensions".into(),
286 ));
287 }
288
289 let (height, width) = red_band.dim();
290 let mut ndvi = Array2::zeros((height, width));
291
292 for i in 0..height {
293 for j in 0..width {
294 let red = red_band[[i, j]].to_f64().unwrap_or(0.0);
295 let nir = nir_band[[i, j]].to_f64().unwrap_or(0.0);
296
297 let denominator = nir + red;
298 if denominator.abs() > 1e-10 {
299 ndvi[[i, j]] = (nir - red) / denominator;
300 } else {
301 ndvi[[i, j]] = 0.0;
302 }
303 }
304 }
305
306 Ok(ndvi)
307 }
308
309 pub fn detect_water_bodies<T>(
311 green_band: &ArrayView2<T>,
312 nir_band: &ArrayView2<T>,
313 threshold: Option<f64>,
314 ) -> NdimageResult<Array2<bool>>
315 where
316 T: Float + FromPrimitive,
317 {
318 let ndwi = compute_ndwi(green_band, nir_band)?;
320
321 let threshold = threshold.unwrap_or(0.3);
323 let water_mask = ndwi.mapv(|x| x > threshold);
324
325 let cleaned = binary_opening(&water_mask, None, Some(2), None, None, None, None)?;
327 let cleaned = binary_closing(&cleaned, None, Some(3), None, None, None, None)?;
328
329 Ok(cleaned)
330 }
331
332 fn compute_ndwi<T>(
334 green_band: &ArrayView2<T>,
335 nir_band: &ArrayView2<T>,
336 ) -> NdimageResult<Array2<f64>>
337 where
338 T: Float + FromPrimitive,
339 {
340 if green_band.dim() != nir_band.dim() {
341 return Err(NdimageError::DimensionError(
342 "Green and NIR bands must have same dimensions".into(),
343 ));
344 }
345
346 let (height, width) = green_band.dim();
347 let mut ndwi = Array2::zeros((height, width));
348
349 for i in 0..height {
350 for j in 0..width {
351 let green = green_band[[i, j]].to_f64().unwrap_or(0.0);
352 let nir = nir_band[[i, j]].to_f64().unwrap_or(0.0);
353
354 let denominator = green + nir;
355 if denominator.abs() > 1e-10 {
356 ndwi[[i, j]] = (green - nir) / denominator;
357 } else {
358 ndwi[[i, j]] = 0.0;
359 }
360 }
361 }
362
363 Ok(ndwi)
364 }
365
366 pub fn detect_clouds<T>(
368 image: &ArrayView3<T>, brightness_threshold: f64,
370 temperature_threshold: Option<f64>,
371 ) -> NdimageResult<Array2<bool>>
372 where
373 T: Float + FromPrimitive,
374 {
375 if image.dim().2 < 3 {
376 return Err(NdimageError::InvalidInput(
377 "Image must have at least 3 spectral bands".into(),
378 ));
379 }
380
381 let (height, width, _) = image.dim();
382 let mut cloud_mask = Array2::default((height, width));
383
384 for i in 0..height {
386 for j in 0..width {
387 let brightness = (0..3)
388 .map(|k| image[[i, j, k]].to_f64().unwrap_or(0.0))
389 .sum::<f64>()
390 / 3.0;
391
392 if brightness > brightness_threshold {
393 cloud_mask[[i, j]] = true;
394 }
395 }
396 }
397
398 if let Some(temp_thresh) = temperature_threshold {
400 if image.dim().2 > 3 {
401 for i in 0..height {
403 for j in 0..width {
404 let temp = image[[i, j, 3]].to_f64().unwrap_or(0.0);
405 if cloud_mask[[i, j]] && temp > temp_thresh {
406 cloud_mask[[i, j]] = false; }
408 }
409 }
410 }
411 }
412
413 let cleaned = binary_closing(&cloud_mask, None, Some(5), None, None, None, None)?;
415
416 Ok(cleaned)
417 }
418
419 pub fn pan_sharpen<T>(
421 panimage: &ArrayView2<T>,
422 multi_spectral: &ArrayView3<T>,
423 method: PanSharpenMethod,
424 ) -> NdimageResult<Array3<T>>
425 where
426 T: Float
427 + FromPrimitive
428 + Debug
429 + Send
430 + Sync
431 + scirs2_core::ndarray::ScalarOperand
432 + std::ops::Mul<Output = T>
433 + std::ops::AddAssign
434 + std::ops::DivAssign
435 + 'static,
436 {
437 let (pan_h, pan_w) = panimage.dim();
438 let (ms_h, ms_w, num_bands) = multi_spectral.dim();
439
440 let scale_y = pan_h as f64 / ms_h as f64;
442 let scale_x = pan_w as f64 / ms_w as f64;
443
444 match method {
445 PanSharpenMethod::IHS => {
446 let mut sharpened = Array3::zeros((pan_h, pan_w, num_bands));
448
449 for band in 0..num_bands {
451 let ms_band = multi_spectral.slice(scirs2_core::ndarray::s![.., .., band]);
452 let upsampled = zoom(
453 &ms_band.to_owned(),
454 T::from_f64(scale_x).ok_or_else(|| {
455 NdimageError::InvalidInput("Failed to convert scale factor".into())
456 })?, Some(InterpolationOrder::Cubic),
458 None,
459 None,
460 None,
461 )?;
462 sharpened
463 .slice_mut(scirs2_core::ndarray::s![.., .., band])
464 .assign(&upsampled);
465 }
466
467 let mut intensity = Array2::zeros((pan_h, pan_w));
469 for i in 0..pan_h {
470 for j in 0..pan_w {
471 let sum: T = (0..num_bands)
472 .map(|k| sharpened[[i, j, k]])
473 .fold(T::zero(), |a, b| a + b);
474 intensity[[i, j]] = sum / safe_usize_to_float(num_bands)?;
475 }
476 }
477
478 for i in 0..pan_h {
480 for j in 0..pan_w {
481 let ratio = if intensity[[i, j]] > safe_f64_to_float::<T>(1e-10)? {
482 panimage[[i, j]] / intensity[[i, j]]
483 } else {
484 T::one()
485 };
486
487 for k in 0..num_bands {
488 sharpened[[i, j, k]] = sharpened[[i, j, k]] * ratio;
489 }
490 }
491 }
492
493 Ok(sharpened)
494 }
495
496 PanSharpenMethod::Brovey => {
497 let mut sharpened = Array3::zeros((pan_h, pan_w, num_bands));
499
500 for band in 0..num_bands {
502 let ms_band = multi_spectral.slice(scirs2_core::ndarray::s![.., .., band]);
503 let upsampled = zoom(
504 &ms_band.to_owned(),
505 T::from_f64(scale_x).ok_or_else(|| {
506 NdimageError::InvalidInput("Failed to convert scale factor".into())
507 })?, Some(InterpolationOrder::Cubic),
509 None,
510 None,
511 None,
512 )?;
513
514 let mut ms_sum = Array2::zeros((ms_h, ms_w));
516 for k in 0..num_bands {
517 ms_sum += &multi_spectral.slice(scirs2_core::ndarray::s![.., .., k]);
518 }
519
520 let sum_upsampled = zoom(
522 &ms_sum.to_owned(),
523 T::from_f64(scale_x).ok_or_else(|| {
524 NdimageError::InvalidInput("Failed to convert scale factor".into())
525 })?,
526 Some(InterpolationOrder::Cubic),
527 None,
528 None,
529 None,
530 )?;
531
532 for i in 0..pan_h {
534 for j in 0..pan_w {
535 if sum_upsampled[[i, j]] > safe_f64_to_float::<T>(1e-10)? {
536 sharpened[[i, j, band]] =
537 upsampled[[i, j]] * panimage[[i, j]] / sum_upsampled[[i, j]];
538 } else {
539 sharpened[[i, j, band]] = upsampled[[i, j]];
540 }
541 }
542 }
543 }
544
545 Ok(sharpened)
546 }
547 }
548 }
549
550 #[derive(Clone, Debug)]
552 pub enum PanSharpenMethod {
553 IHS, Brovey, }
556}
557
558pub mod microscopy {
560 use super::*;
561
562 #[derive(Clone, Debug)]
564 pub struct CellSegmentationParams {
565 pub min_area: usize,
567 pub max_area: usize,
569 pub threshold_method: ThresholdMethod,
571 pub cleanup_iterations: usize,
573 }
574
575 impl Default for CellSegmentationParams {
576 fn default() -> Self {
577 Self {
578 min_area: 50,
579 max_area: 5000,
580 threshold_method: ThresholdMethod::Otsu,
581 cleanup_iterations: 2,
582 }
583 }
584 }
585
586 #[derive(Clone, Debug)]
587 pub enum ThresholdMethod {
588 Otsu,
589 Adaptive,
590 Fixed(f64),
591 }
592
593 pub fn segment_cells<T>(
595 image: &ArrayView2<T>,
596 params: Option<CellSegmentationParams>,
597 ) -> NdimageResult<(Array2<i32>, Vec<CellInfo>)>
598 where
599 T: Float + FromPrimitive + Debug + Send + Sync + scirs2_core::numeric::NumAssign + 'static,
600 {
601 let params = params.unwrap_or_default();
602
603 let binary = match params.threshold_method {
605 ThresholdMethod::Otsu => {
606 let (_thresholded, threshold_val) =
607 crate::segmentation::otsu_threshold(&image.to_owned(), 256)?;
608 image.mapv(|x| x > threshold_val)
609 }
610 ThresholdMethod::Adaptive => crate::segmentation::adaptive_threshold(
611 &image.to_owned(),
612 21,
613 crate::segmentation::AdaptiveMethod::Gaussian,
614 safe_f64_to_float::<T>(5.0)?,
615 )?,
616 ThresholdMethod::Fixed(thresh) => {
617 let thresh_t = safe_f64_to_float::<T>(thresh)?;
618 image.mapv(|x| x > thresh_t)
619 }
620 };
621
622 let mut cleaned = binary;
624 for _ in 0..params.cleanup_iterations {
625 cleaned = binary_opening(&cleaned, None, Some(3), None, None, None, None)?;
626 cleaned = binary_closing(&cleaned, None, Some(3), None, None, None, None)?;
627 }
628
629 let (labels, num_cells) = label(&cleaned, None, None, None)?;
631
632 let mut cell_info = Vec::new();
634 let mut filtered_labels = Array2::zeros(labels.dim());
635 let mut new_label = 1;
636
637 for i in 1..=num_cells {
638 let mask = labels.mapv(|x| x == i);
639 let area = mask.iter().filter(|&&x| x).count();
640
641 if area >= params.min_area && area <= params.max_area {
642 let com = center_of_mass(&image.to_owned())?;
644 let central_moments_result = central_moments(
645 &mask.mapv(|x| {
646 if x {
647 safe_f64_to_float::<T>(1.0).unwrap_or(T::one())
648 } else {
649 T::zero()
650 }
651 }),
652 2,
653 None,
654 )?;
655
656 let m00 = central_moments_result[0]; let m20 = central_moments_result[6]; let m02 = central_moments_result[2]; let m11 = central_moments_result[4]; let a = m20 / m00;
664 let b = safe_f64_to_float::<T>(2.0)? * m11 / m00;
665 let c = m02 / m00;
666
667 let discriminant = (a - c) * (a - c) + b * b;
668 let zero_t = T::zero();
669 let eccentricity = if discriminant > zero_t {
670 let sqrt_disc = discriminant.sqrt();
671 let two_t = safe_f64_to_float::<T>(2.0)?;
672 let lambda1 = (a + c + sqrt_disc) / two_t;
673 let lambda2 = (a + c - sqrt_disc) / two_t;
674
675 if lambda1 > zero_t {
676 let one_t = T::one();
677 (one_t - lambda2 / lambda1).sqrt()
678 } else {
679 zero_t
680 }
681 } else {
682 zero_t
683 };
684
685 for ((y, x), &val) in labels.indexed_iter() {
687 if val == i {
688 filtered_labels[[y, x]] = new_label;
689 }
690 }
691
692 let center_tuple = if com.len() >= 2 {
693 (
694 safe_float_to_f64(com[0]).unwrap_or(0.0),
695 safe_float_to_f64(com[1]).unwrap_or(0.0),
696 )
697 } else {
698 (0.0, 0.0)
699 };
700
701 cell_info.push(CellInfo {
702 label: new_label,
703 area,
704 center: center_tuple,
705 eccentricity: safe_float_to_f64(eccentricity).unwrap_or(0.0),
706 mean_intensity: image
707 .indexed_iter()
708 .filter(|((y, x), _)| mask[[*y, *x]])
709 .map(|(_, &val)| safe_float_to_f64(val).unwrap_or(0.0))
710 .sum::<f64>()
711 / area as f64,
712 });
713
714 new_label += 1;
715 }
716 }
717
718 Ok((filtered_labels, cell_info))
719 }
720
721 #[derive(Clone, Debug)]
723 pub struct CellInfo {
724 pub label: i32,
725 pub area: usize,
726 pub center: (f64, f64),
727 pub eccentricity: f64,
728 pub mean_intensity: f64,
729 }
730
731 pub fn detect_nuclei<T>(
733 dapi_channel: &ArrayView2<T>,
734 min_size: usize,
735 max_size: usize,
736 ) -> NdimageResult<(Array2<i32>, usize)>
737 where
738 T: Float
739 + FromPrimitive
740 + Debug
741 + Send
742 + Sync
743 + std::ops::AddAssign
744 + std::ops::DivAssign
745 + scirs2_core::numeric::NumAssign
746 + 'static,
747 {
748 let denoised = median_filter(&dapi_channel.to_owned(), &[3, 3], None)?;
750
751 let structure = crate::morphology::disk_structure(10.0, None)?;
753 let structure_2d = structure.into_dimensionality::<scirs2_core::ndarray::Ix2>()?;
754 let background = grey_opening(&denoised, None, Some(&structure_2d), None, None, None)?;
755 let enhanced = &denoised - &background;
756
757 let (binary_t, threshold_value) = crate::segmentation::otsu_threshold(&enhanced, 256)?;
759
760 let binary = binary_t.mapv(|x| x > threshold_value);
762
763 let filled = crate::morphology::binary_fill_holes(&binary, None, None)?;
765
766 let cleaned = crate::morphology::remove_small_objects(&filled, min_size, None)?;
768
769 let (labels_usize, num_features) = label(&cleaned, None, None, None)?;
771
772 let mut labels = Array2::<i32>::zeros(labels_usize.dim());
774 let mut valid_count = 0;
775
776 for i in 1..=num_features {
777 let nucleus_size = labels_usize.iter().filter(|&&x| x == i).count();
778 if nucleus_size >= min_size && nucleus_size <= max_size {
779 valid_count += 1;
780 for ((y, x), &val) in labels_usize.indexed_iter() {
782 if val == i {
783 labels[[y, x]] = i as i32;
784 }
785 }
786 }
787 }
788
789 Ok((labels, valid_count))
790 }
791
792 pub fn colocalization_analysis<T>(
794 channel1: &ArrayView2<T>,
795 channel2: &ArrayView2<T>,
796 threshold1: Option<T>,
797 threshold2: Option<T>,
798 ) -> NdimageResult<ColocalizationMetrics>
799 where
800 T: Float + FromPrimitive,
801 {
802 if channel1.dim() != channel2.dim() {
803 return Err(NdimageError::DimensionError(
804 "Channels must have same dimensions".into(),
805 ));
806 }
807
808 let thresh1 = threshold1.unwrap_or_else(|| {
810 let mean = channel1.sum() / safe_usize_to_float(channel1.len()).unwrap_or(T::one());
811 let std = channel1.std(T::zero());
812 mean + std
813 });
814
815 let thresh2 = threshold2.unwrap_or_else(|| {
816 let mean = channel2.sum() / safe_usize_to_float(channel2.len()).unwrap_or(T::one());
817 let std = channel2.std(T::zero());
818 mean + std
819 });
820
821 let mask1 = channel1.mapv(|x| x > thresh1);
823 let mask2 = channel2.mapv(|x| x > thresh2);
824
825 let overlap = mask1
827 .iter()
828 .zip(mask2.iter())
829 .filter(|(&a, &b)| a && b)
830 .count();
831
832 let area1 = mask1.iter().filter(|&&x| x).count();
833 let area2 = mask2.iter().filter(|&&x| x).count();
834
835 let mut m1 = 0.0;
837 let mut m2 = 0.0;
838 let mut sum1 = 0.0;
839 let mut sum2 = 0.0;
840
841 for ((y, x), &val1) in channel1.indexed_iter() {
842 let val2 = channel2[[y, x]];
843
844 if mask1[[y, x]] {
845 sum1 += safe_float_to_f64(val1).unwrap_or(0.0);
846 if mask2[[y, x]] {
847 m1 += safe_float_to_f64(val1).unwrap_or(0.0);
848 }
849 }
850
851 if mask2[[y, x]] {
852 sum2 += safe_float_to_f64(val2).unwrap_or(0.0);
853 if mask1[[y, x]] {
854 m2 += safe_float_to_f64(val2).unwrap_or(0.0);
855 }
856 }
857 }
858
859 let manders_m1 = if sum1 > 0.0 { m1 / sum1 } else { 0.0 };
860 let manders_m2 = if sum2 > 0.0 { m2 / sum2 } else { 0.0 };
861
862 let mean1 = safe_float_to_f64(
864 channel1.sum() / safe_usize_to_float(channel1.len()).unwrap_or(T::one()),
865 )
866 .unwrap_or(0.0);
867 let mean2 = safe_float_to_f64(
868 channel2.sum() / safe_usize_to_float(channel2.len()).unwrap_or(T::one()),
869 )
870 .unwrap_or(0.0);
871
872 let mut cov = 0.0;
873 let mut var1 = 0.0;
874 let mut var2 = 0.0;
875
876 for ((y, x), &val1) in channel1.indexed_iter() {
877 if mask1[[y, x]] || mask2[[y, x]] {
878 let v1 = safe_float_to_f64(val1).unwrap_or(0.0) - mean1;
879 let v2 = safe_float_to_f64(channel2[[y, x]]).unwrap_or(0.0) - mean2;
880
881 cov += v1 * v2;
882 var1 += v1 * v1;
883 var2 += v2 * v2;
884 }
885 }
886
887 let pearson = if var1 > 0.0 && var2 > 0.0 {
888 cov / (var1.sqrt() * var2.sqrt())
889 } else {
890 0.0
891 };
892
893 Ok(ColocalizationMetrics {
894 overlap_coefficient: overlap as f64 / (area1.min(area2) as f64).max(1.0),
895 manders_m1,
896 manders_m2,
897 pearson_correlation: pearson,
898 overlap_area: overlap,
899 })
900 }
901
902 #[derive(Clone, Debug)]
904 pub struct ColocalizationMetrics {
905 pub overlap_coefficient: f64,
906 pub manders_m1: f64,
907 pub manders_m2: f64,
908 pub pearson_correlation: f64,
909 pub overlap_area: usize,
910 }
911}
912
913#[cfg(test)]
914mod tests {
915 use super::*;
916 use scirs2_core::ndarray::arr2;
917
918 #[test]
919 fn test_ndvi() {
920 let red = arr2(&[[0.1, 0.2, 0.3], [0.2, 0.3, 0.4], [0.3, 0.4, 0.5]]);
921
922 let nir = arr2(&[[0.5, 0.6, 0.7], [0.6, 0.7, 0.8], [0.7, 0.8, 0.9]]);
923
924 let ndvi =
925 satellite::compute_ndvi(&red.view(), &nir.view()).expect("compute_ndvi should succeed");
926
927 for &val in ndvi.iter() {
929 assert!(val >= -1.0 && val <= 1.0);
930 assert!(val > 0.0); }
932 }
933
934 #[test]
935 fn test_frangi_vesselness() {
936 let mut image = Array2::zeros((50, 50));
938
939 for i in 24..26 {
941 for j in 10..40 {
942 image[[i, j]] = 1.0;
943 }
944 }
945
946 for i in 10..40 {
948 for j in 24..26 {
949 image[[i, j]] = 1.0;
950 }
951 }
952
953 let vesselness = medical::frangi_vesselness(&image.view(), None)
954 .expect("frangi_vesselness should succeed");
955
956 assert!(vesselness[[25, 25]] > 0.0);
958 }
959
960 #[test]
961 fn test_cell_segmentation() {
962 let mut image = Array2::zeros((100, 100));
964
965 for cy in [25, 75] {
967 for cx in [25, 75] {
968 for i in 0..100 {
969 for j in 0..100 {
970 let dy = i as f64 - cy as f64;
971 let dx = j as f64 - cx as f64;
972 let r = (dy * dy + dx * dx).sqrt();
973
974 if r < 10.0 {
975 image[[i, j]] = 1.0;
976 }
977 }
978 }
979 }
980 }
981
982 let (labels, cells) =
983 microscopy::segment_cells(&image.view(), None).expect("segment_cells should succeed");
984
985 assert_eq!(cells.len(), 4); assert!(labels.into_iter().max() == Some(4));
987 }
988}