Skip to main content

scirs2_ndimage/
domain_specific.rs

1//! Domain-specific imaging functions
2//!
3//! This module provides specialized image processing functions for different domains:
4//! medical imaging, satellite/remote sensing, and microscopy.
5
6use 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
19/// Medical imaging functions
20pub mod medical {
21    use super::*;
22
23    /// Parameters for vessel enhancement
24    #[derive(Clone, Debug)]
25    pub struct VesselEnhancementParams {
26        /// Scales at which to compute vesselness
27        pub scales: Vec<f64>,
28        /// Frangi filter parameters
29        pub alpha: f64, // Plate-like structures suppression
30        pub beta: f64,  // Blob-like structures suppression
31        pub gamma: f64, // Background suppression
32    }
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    /// Enhance blood vessels using Frangi filter
46    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        // Convert to f64
58        let img = image.mapv(|x| x.to_f64().unwrap_or(0.0));
59
60        // Compute vesselness at each scale
61        for &scale in &params.scales {
62            // Compute Hessian matrix components
63            let smoothed = gaussian_filter(&img, scale, None, None)?;
64            let hessian = compute_hessian_2d(&smoothed.view(), scale)?;
65
66            // Compute eigenvalues at each pixel
67            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                    // Eigenvalues of 2x2 symmetric matrix
74                    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                        // Order eigenvalues by magnitude
84                        let (l1, l2) = if lambda1.abs() > lambda2.abs() {
85                            (lambda1, lambda2)
86                        } else {
87                            (lambda2, lambda1)
88                        };
89
90                        // Frangi vesselness measure
91                        if l2 < 0.0 {
92                            // Dark vessels on bright background
93                            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    /// Compute Hessian matrix components
110    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        // Scale-normalized second derivatives
120        let norm = scale * scale;
121
122        for i in 2..height - 2 {
123            for j in 2..width - 2 {
124                // Second derivatives using central differences
125                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    /// Bone structure enhancement using morphological operations
139    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        // Top-hat transform to enhance bright structures
155        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        // Enhance contrast
168        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    /// Lung nodule detection (simplified)
175    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        // Threshold to segment lung tissue
193        let threshold = safe_f64_to_float::<T>(-500.0)?; // Typical HU value for lung tissue
194        let lung_mask = ct_slice.mapv(|x| x > threshold);
195
196        // Apply morphological operations to clean up
197        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        // Find connected components
201        let (labels, num_features) = label(&cleaned, None, None, None)?;
202
203        // Analyze each component
204        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                // Compute properties
210                let com = center_of_mass(&ct_slice.to_owned())?;
211
212                // Simple circularity measure
213                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    /// Detected nodule information
262    #[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
271/// Satellite and remote sensing imaging functions
272pub mod satellite {
273    use super::*;
274
275    /// Compute Normalized Difference Vegetation Index (NDVI)
276    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    /// Detect water bodies using spectral indices
310    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        // Compute Normalized Difference Water Index (NDWI)
319        let ndwi = compute_ndwi(green_band, nir_band)?;
320
321        // Apply threshold
322        let threshold = threshold.unwrap_or(0.3);
323        let water_mask = ndwi.mapv(|x| x > threshold);
324
325        // Clean up small patches
326        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    /// Compute Normalized Difference Water Index (NDWI)
333    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    /// Cloud detection in satellite imagery
367    pub fn detect_clouds<T>(
368        image: &ArrayView3<T>, // Multi-spectral image
369        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        // Simple brightness test (clouds are bright in visible bands)
385        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        // Thermal test if thermal band is available
399        if let Some(temp_thresh) = temperature_threshold {
400            if image.dim().2 > 3 {
401                // Assume 4th band is thermal
402                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; // Not a cloud if too warm
407                        }
408                    }
409                }
410            }
411        }
412
413        // Morphological cleaning
414        let cleaned = binary_closing(&cloud_mask, None, Some(5), None, None, None, None)?;
415
416        Ok(cleaned)
417    }
418
419    /// Pan-sharpening: merge high-resolution panchromatic with low-resolution multispectral
420    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        // Compute scale factor
441        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                // Intensity-Hue-Saturation method
447                let mut sharpened = Array3::zeros((pan_h, pan_w, num_bands));
448
449                // Upsample multispectral to pan resolution
450                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                        })?, // Use single scale factor
457                        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                // Compute intensity from multispectral
468                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                // Replace intensity with pan
479                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                // Brovey transform
498                let mut sharpened = Array3::zeros((pan_h, pan_w, num_bands));
499
500                // Upsample and apply Brovey transform
501                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                        })?, // Use single scale factor
508                        Some(InterpolationOrder::Cubic),
509                        None,
510                        None,
511                        None,
512                    )?;
513
514                    // Compute sum of all bands at low resolution
515                    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                    // Upsample sum
521                    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                    // Apply Brovey transform
533                    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    /// Pan-sharpening method
551    #[derive(Clone, Debug)]
552    pub enum PanSharpenMethod {
553        IHS,    // Intensity-Hue-Saturation
554        Brovey, // Brovey transform
555    }
556}
557
558/// Microscopy imaging functions
559pub mod microscopy {
560    use super::*;
561
562    /// Parameters for cell segmentation
563    #[derive(Clone, Debug)]
564    pub struct CellSegmentationParams {
565        /// Minimum cell area in pixels
566        pub min_area: usize,
567        /// Maximum cell area in pixels
568        pub max_area: usize,
569        /// Threshold method
570        pub threshold_method: ThresholdMethod,
571        /// Morphological cleanup iterations
572        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    /// Segment cells in microscopy images
594    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        // Apply threshold
604        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        // Morphological cleanup
623        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        // Label connected components
630        let (labels, num_cells) = label(&cleaned, None, None, None)?;
631
632        // Analyze each cell
633        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                // Compute cell properties
643                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                // Compute eccentricity from central moments
657                // For 2D with order=2: indices are M_00(0), M_01(1), M_02(2), M_10(3), M_11(4), M_12(5), M_20(6), M_21(7), M_22(8)
658                let m00 = central_moments_result[0]; // μ_00 (total mass)
659                let m20 = central_moments_result[6]; // μ_20
660                let m02 = central_moments_result[2]; // μ_02
661                let m11 = central_moments_result[4]; // μ_11
662
663                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                // Update filtered labels
686                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    /// Information about a segmented cell
722    #[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    /// Detect and count nuclei in fluorescence microscopy
732    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        // Preprocess with median filter to reduce noise
749        let denoised = median_filter(&dapi_channel.to_owned(), &[3, 3], None)?;
750
751        // Enhance nuclei using top-hat transform
752        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        // Threshold using Otsu's method
758        let (binary_t, threshold_value) = crate::segmentation::otsu_threshold(&enhanced, 256)?;
759
760        // Convert to bool array
761        let binary = binary_t.mapv(|x| x > threshold_value);
762
763        // Fill holes in nuclei
764        let filled = crate::morphology::binary_fill_holes(&binary, None, None)?;
765
766        // Remove small objects
767        let cleaned = crate::morphology::remove_small_objects(&filled, min_size, None)?;
768
769        // Label nuclei
770        let (labels_usize, num_features) = label(&cleaned, None, None, None)?;
771
772        // Convert usize labels to i32 and filter by size
773        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                // Copy this nucleus to the output with i32 label
781                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    /// Colocalization analysis for multi-channel microscopy
793    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        // Apply thresholds
809        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        // Create masks
822        let mask1 = channel1.mapv(|x| x > thresh1);
823        let mask2 = channel2.mapv(|x| x > thresh2);
824
825        // Compute overlap
826        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        // Compute Manders coefficients
836        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        // Compute Pearson correlation
863        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    /// Colocalization analysis results
903    #[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        // Check NDVI values are in expected range
928        for &val in ndvi.iter() {
929            assert!(val >= -1.0 && val <= 1.0);
930            assert!(val > 0.0); // Should be positive for healthy vegetation
931        }
932    }
933
934    #[test]
935    fn test_frangi_vesselness() {
936        // Create a simple vessel-like structure
937        let mut image = Array2::zeros((50, 50));
938
939        // Horizontal vessel
940        for i in 24..26 {
941            for j in 10..40 {
942                image[[i, j]] = 1.0;
943            }
944        }
945
946        // Vertical vessel
947        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        // Check that vessel regions have high response
957        assert!(vesselness[[25, 25]] > 0.0);
958    }
959
960    #[test]
961    fn test_cell_segmentation() {
962        // Create synthetic cell image
963        let mut image = Array2::zeros((100, 100));
964
965        // Add some circular "cells"
966        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); // Should detect 4 cells
986        assert!(labels.into_iter().max() == Some(4));
987    }
988}