Skip to main content

scirs2_ndimage/
scipy_migration_layer.rs

1//! SciPy ndimage API compatibility layer for seamless migration
2//!
3//! This module provides a drop-in replacement API that closely mirrors SciPy's
4//! ndimage module, making it easy for users to migrate existing code with minimal
5//! changes. It includes parameter mapping, behavior matching, and compatibility
6//! warnings for any differences.
7
8use scirs2_core::ndarray::{Array, ArrayView2, Ix2, Ix3};
9use scirs2_core::numeric::{Float, FromPrimitive};
10
11use crate::error::{NdimageError, NdimageResult};
12use crate::filters::{gaussian_filter as internal_gaussian_filter, BorderMode};
13use crate::interpolation::BoundaryMode;
14use crate::measurements::center_of_mass as internal_center_of_mass;
15use crate::morphology::{
16    binary_dilation as internal_binary_dilation, binary_erosion as internal_binary_erosion,
17};
18
19/// SciPy ndimage compatibility layer
20pub struct SciPyCompatLayer {
21    /// Configuration for compatibility behavior
22    config: CompatibilityConfig,
23    /// Migration warnings
24    warnings: Vec<MigrationWarning>,
25}
26
27#[derive(Debug, Clone)]
28pub struct CompatibilityConfig {
29    /// Enable strict SciPy compatibility mode
30    pub strict_compatibility: bool,
31    /// Show migration warnings
32    pub show_warnings: bool,
33    /// Default data type for operations
34    pub default_dtype: String,
35    /// Default boundary mode
36    pub default_mode: String,
37    /// Enable performance optimizations that may differ from SciPy
38    pub enable_optimizations: bool,
39}
40
41impl Default for CompatibilityConfig {
42    fn default() -> Self {
43        Self {
44            strict_compatibility: true,
45            show_warnings: true,
46            default_dtype: "float64".to_string(),
47            default_mode: "reflect".to_string(),
48            enable_optimizations: false,
49        }
50    }
51}
52
53#[derive(Debug, Clone)]
54pub struct MigrationWarning {
55    /// Function name
56    pub function: String,
57    /// Warning message
58    pub message: String,
59    /// Suggested solution
60    pub suggestion: Option<String>,
61}
62
63impl SciPyCompatLayer {
64    /// Create a new SciPy compatibility layer
65    pub fn new(config: CompatibilityConfig) -> Self {
66        Self {
67            config,
68            warnings: Vec::new(),
69        }
70    }
71
72    /// Create with default configuration
73    pub fn default() -> Self {
74        Self::new(CompatibilityConfig::default())
75    }
76
77    /// Get migration warnings
78    pub fn get_warnings(&self) -> &[MigrationWarning] {
79        &self.warnings
80    }
81
82    /// Clear warnings
83    pub fn clear_warnings(&mut self) {
84        self.warnings.clear();
85    }
86}
87
88/// SciPy-compatible filter functions
89impl SciPyCompatLayer {
90    /// Gaussian filter with SciPy-compatible interface
91    ///
92    /// ```python
93    /// # SciPy usage:
94    /// scipy.ndimage.gaussian_filter(input, sigma, order=0, output=None,
95    ///                               mode='reflect', cval=0.0, truncate=4.0)
96    /// ```
97    pub fn gaussian_filter<T>(
98        &mut self,
99        input: ArrayView2<T>,
100        sigma: SigmaParam,
101        order: Option<OrderParam>,
102        mode: Option<&str>,
103        cval: Option<f64>,
104        truncate: Option<f64>,
105    ) -> NdimageResult<Array<T, Ix2>>
106    where
107        T: Float + FromPrimitive + Clone + Send + Sync,
108    {
109        // Handle parameter conversion
110        let sigma_tuple = self.convert_sigma_param(sigma)?;
111        let boundary_mode = self.convert_mode_param(mode)?;
112
113        // Check for unsupported parameters
114        if order.is_some() && order != Some(OrderParam::Single(0)) {
115            self.add_warning(
116                "gaussian_filter",
117                "Non-zero order parameter not yet supported, using order=0",
118            );
119        }
120
121        if cval.is_some() && cval != Some(0.0) {
122            self.add_warning(
123                "gaussian_filter",
124                "Custom cval not fully supported, using default boundary handling",
125            );
126        }
127
128        if truncate.is_some() && truncate != Some(4.0) {
129            self.add_warning(
130                "gaussian_filter",
131                "Custom truncate parameter not supported, using default value",
132            );
133        }
134
135        // Convert input to f64 array and sigma tuple to single value
136        let input_f64 = input.mapv(|x| x.to_f64().unwrap_or(0.0)).to_owned();
137        let sigma_single = (sigma_tuple.0 + sigma_tuple.1) / 2.0; // Use average of sigma values
138
139        // Convert BoundaryMode to BorderMode
140        let border_mode = match boundary_mode {
141            BoundaryMode::Constant => BorderMode::Constant,
142            BoundaryMode::Reflect => BorderMode::Reflect,
143            BoundaryMode::Mirror => BorderMode::Mirror,
144            BoundaryMode::Wrap => BorderMode::Wrap,
145            BoundaryMode::Nearest => BorderMode::Nearest,
146        };
147
148        // Call internal implementation
149        let result_f64 =
150            internal_gaussian_filter(&input_f64, sigma_single, Some(border_mode), None)?;
151
152        // Convert back to original type
153        let result = result_f64.mapv(|x| T::from_f64(x).unwrap_or(T::zero()));
154        Ok(result)
155    }
156
157    /// Median filter with SciPy-compatible interface
158    ///
159    /// ```python
160    /// # SciPy usage:
161    /// scipy.ndimage.median_filter(input, size=None, footprint=None, output=None,
162    ///                             mode='reflect', cval=0.0, origin=0)
163    /// ```
164    pub fn median_filter<T>(
165        &mut self,
166        input: ArrayView2<T>,
167        size: Option<SizeParam>,
168        footprint: Option<ArrayView2<bool>>,
169        mode: Option<&str>,
170        cval: Option<f64>,
171        origin: Option<OriginParam>,
172    ) -> NdimageResult<Array<T, Ix2>>
173    where
174        T: Float
175            + FromPrimitive
176            + std::fmt::Debug
177            + Clone
178            + Send
179            + Sync
180            + PartialOrd
181            + std::ops::AddAssign
182            + std::ops::DivAssign
183            + 'static,
184    {
185        let filter_size = self.convert_size_param(size, (3, 3))?;
186        let boundary_mode = self.convert_mode_param(mode)?;
187
188        if footprint.is_some() {
189            self.add_warning(
190                "median_filter",
191                "Custom footprint not yet supported, using rectangular window",
192            );
193        }
194
195        if origin.is_some() {
196            self.add_warning(
197                "median_filter",
198                "Origin parameter not supported, using center origin",
199            );
200        }
201
202        // Convert input to owned array and convert parameters
203        let input_owned = input.to_owned();
204        let size_slice = [filter_size.0, filter_size.1];
205
206        // Convert BoundaryMode to BorderMode
207        let border_mode = match boundary_mode {
208            BoundaryMode::Constant => BorderMode::Constant,
209            BoundaryMode::Reflect => BorderMode::Reflect,
210            BoundaryMode::Mirror => BorderMode::Mirror,
211            BoundaryMode::Wrap => BorderMode::Wrap,
212            BoundaryMode::Nearest => BorderMode::Nearest,
213        };
214
215        crate::filters::median_filter(&input_owned, &size_slice, Some(border_mode))
216    }
217
218    /// Uniform filter with SciPy-compatible interface
219    ///
220    /// ```python
221    /// # SciPy usage:
222    /// scipy.ndimage.uniform_filter(input, size=3, output=None, mode='reflect',
223    ///                              cval=0.0, origin=0)
224    /// ```
225    pub fn uniform_filter<T>(
226        &mut self,
227        input: ArrayView2<T>,
228        size: Option<SizeParam>,
229        mode: Option<&str>,
230        cval: Option<f64>,
231        origin: Option<OriginParam>,
232    ) -> NdimageResult<Array<T, Ix2>>
233    where
234        T: Float
235            + FromPrimitive
236            + std::fmt::Debug
237            + Clone
238            + Send
239            + Sync
240            + std::ops::AddAssign
241            + std::ops::DivAssign
242            + 'static,
243    {
244        let filter_size = self.convert_size_param(size, (3, 3))?;
245        let boundary_mode = self.convert_mode_param(mode)?;
246
247        if origin.is_some() {
248            self.add_warning(
249                "uniform_filter",
250                "Origin parameter not supported, using center origin",
251            );
252        }
253
254        // Convert input to owned array and convert parameters
255        let input_owned = input.to_owned();
256        let size_slice = [filter_size.0, filter_size.1];
257
258        // Convert BoundaryMode to BorderMode
259        let border_mode = match boundary_mode {
260            BoundaryMode::Constant => BorderMode::Constant,
261            BoundaryMode::Reflect => BorderMode::Reflect,
262            BoundaryMode::Mirror => BorderMode::Mirror,
263            BoundaryMode::Wrap => BorderMode::Wrap,
264            BoundaryMode::Nearest => BorderMode::Nearest,
265        };
266
267        crate::filters::uniform_filter(&input_owned, &size_slice, Some(border_mode), None)
268    }
269}
270
271/// SciPy-compatible morphology functions
272impl SciPyCompatLayer {
273    /// Binary erosion with SciPy-compatible interface
274    ///
275    /// ```python
276    /// # SciPy usage:
277    /// scipy.ndimage.binary_erosion(input, structure=None, iterations=1,
278    ///                              mask=None, output=None, border_value=0,
279    ///                              origin=0, brute_force=False)
280    /// ```
281    pub fn binary_erosion<T>(
282        &mut self,
283        input: ArrayView2<T>,
284        structure: Option<ArrayView2<bool>>,
285        iterations: Option<usize>,
286        mask: Option<ArrayView2<bool>>,
287        border_value: Option<bool>,
288        origin: Option<OriginParam>,
289        brute_force: Option<bool>,
290    ) -> NdimageResult<Array<bool, Ix2>>
291    where
292        T: Float + FromPrimitive + Clone + Send + Sync + PartialOrd,
293    {
294        // Convert input to binary
295        let binary_input = self.convert_to_binary(input);
296
297        // Use default 3x3 cross structure if none provided
298        let default_structure = Array::from_shape_vec(
299            (3, 3),
300            vec![false, true, false, true, true, true, false, true, false],
301        )
302        .expect("Operation failed");
303
304        let structure_elem = match structure {
305            Some(s) => s.to_owned(),
306            None => default_structure,
307        };
308
309        if iterations.is_some() && iterations != Some(1) {
310            self.add_warning(
311                "binary_erosion",
312                "Multiple iterations not yet optimized, using single iteration",
313            );
314        }
315
316        if mask.is_some() {
317            self.add_warning("binary_erosion", "Mask parameter not yet supported");
318        }
319
320        if origin.is_some() {
321            self.add_warning("binary_erosion", "Origin parameter not supported");
322        }
323
324        if brute_force.is_some() {
325            self.add_warning(
326                "binary_erosion",
327                "brute_force parameter not supported, using optimized algorithm",
328            );
329        }
330
331        // Apply multiple iterations if requested
332        let mut result = internal_binary_erosion(
333            &binary_input,
334            Some(&structure_elem),
335            None,
336            None,
337            None,
338            None,
339            None,
340        )?;
341
342        for _ in 1..iterations.unwrap_or(1) {
343            result = internal_binary_erosion(
344                &result,
345                Some(&structure_elem),
346                None,
347                None,
348                None,
349                None,
350                None,
351            )?;
352        }
353
354        Ok(result)
355    }
356
357    /// Binary dilation with SciPy-compatible interface
358    ///
359    /// ```python
360    /// # SciPy usage:
361    /// scipy.ndimage.binary_dilation(input, structure=None, iterations=1,
362    ///                               mask=None, output=None, border_value=0,
363    ///                               origin=0, brute_force=False)
364    /// ```
365    pub fn binary_dilation<T>(
366        &mut self,
367        input: ArrayView2<T>,
368        structure: Option<ArrayView2<bool>>,
369        iterations: Option<usize>,
370        mask: Option<ArrayView2<bool>>,
371        border_value: Option<bool>,
372        origin: Option<OriginParam>,
373        brute_force: Option<bool>,
374    ) -> NdimageResult<Array<bool, Ix2>>
375    where
376        T: Float + FromPrimitive + Clone + Send + Sync + PartialOrd,
377    {
378        let binary_input = self.convert_to_binary(input);
379
380        let default_structure = Array::from_shape_vec(
381            (3, 3),
382            vec![false, true, false, true, true, true, false, true, false],
383        )
384        .expect("Operation failed");
385
386        let structure_elem = match structure {
387            Some(s) => s.to_owned(),
388            None => default_structure,
389        };
390
391        if iterations.is_some() && iterations != Some(1) {
392            self.add_warning(
393                "binary_dilation",
394                "Multiple iterations not yet optimized, using single iteration",
395            );
396        }
397
398        if mask.is_some() {
399            self.add_warning("binary_dilation", "Mask parameter not yet supported");
400        }
401
402        // Apply multiple iterations if requested
403        let mut result = internal_binary_dilation(
404            &binary_input,
405            Some(&structure_elem),
406            None,
407            None,
408            None,
409            None,
410            None,
411        )?;
412
413        for _ in 1..iterations.unwrap_or(1) {
414            result = internal_binary_dilation(
415                &result,
416                Some(&structure_elem),
417                None,
418                None,
419                None,
420                None,
421                None,
422            )?;
423        }
424
425        Ok(result)
426    }
427
428    /// Distance transform with SciPy-compatible interface
429    ///
430    /// ```python
431    /// # SciPy usage:
432    /// scipy.ndimage.distance_transform_edt(input, sampling=None, return_distances=True,
433    ///                                      return_indices=False, distances=None, indices=None)
434    /// ```
435    pub fn distance_transform_edt<T>(
436        &mut self,
437        input: ArrayView2<T>,
438        sampling: Option<SamplingParam>,
439        return_distances: Option<bool>,
440        return_indices: Option<bool>,
441    ) -> NdimageResult<DistanceTransformResult<T>>
442    where
443        T: Float + FromPrimitive + Clone + Send + Sync + PartialOrd,
444    {
445        let binary_input = self.convert_to_binary(input);
446
447        if sampling.is_some() {
448            self.add_warning(
449                "distance_transform_edt",
450                "Sampling parameter not yet supported, using unit sampling",
451            );
452        }
453
454        if return_indices == Some(true) {
455            self.add_warning(
456                "distance_transform_edt",
457                "Returning _indices not yet supported",
458            );
459        }
460
461        let binary_input_dyn = binary_input.into_dyn();
462        let (distances_opt, _indices_opt) = crate::morphology::distance_transform_edt(
463            &binary_input_dyn,
464            None,  // sampling
465            true,  // return_distances
466            false, // return_indices
467        )?;
468
469        let _distances = distances_opt.ok_or_else(|| {
470            NdimageError::ComputationError("Failed to compute distances".to_string())
471        })?;
472
473        // Convert back to original type and convert to 2D
474        let result_2d = _distances
475            .into_dimensionality::<scirs2_core::ndarray::Ix2>()
476            .map_err(|_| {
477                NdimageError::ComputationError("Failed to convert distances back to 2D".to_string())
478            })?;
479        let result_array = result_2d.mapv(|v| T::from_f64(v).unwrap_or(T::zero()));
480
481        Ok(DistanceTransformResult {
482            distances: Some(result_array),
483            indices: None,
484        })
485    }
486}
487
488/// SciPy-compatible measurement functions
489impl SciPyCompatLayer {
490    /// Center of mass with SciPy-compatible interface
491    ///
492    /// ```python
493    /// # SciPy usage:
494    /// scipy.ndimage.center_of_mass(input, labels=None, index=None)
495    /// ```
496    pub fn center_of_mass<T>(
497        &mut self,
498        input: ArrayView2<T>,
499        labels: Option<ArrayView2<i32>>,
500        index: Option<IndexParam>,
501    ) -> NdimageResult<CenterOfMassResult>
502    where
503        T: Float
504            + FromPrimitive
505            + Clone
506            + Send
507            + Sync
508            + std::fmt::Debug
509            + std::ops::DivAssign
510            + scirs2_core::numeric::NumAssign
511            + 'static,
512    {
513        if labels.is_some() {
514            self.add_warning("center_of_mass", "Labels parameter not yet fully supported");
515        }
516
517        if index.is_some() {
518            self.add_warning("center_of_mass", "Index parameter not yet supported");
519        }
520
521        let com = internal_center_of_mass(&input.to_owned())?;
522        // Convert Vec<T> to (f64, f64)
523        if com.len() >= 2 {
524            let com_tuple = (
525                com[0].to_f64().unwrap_or(0.0),
526                com[1].to_f64().unwrap_or(0.0),
527            );
528            Ok(CenterOfMassResult::Single(com_tuple))
529        } else {
530            Err(NdimageError::ComputationError(
531                "Center of mass computation failed".to_string(),
532            ))
533        }
534    }
535
536    /// Label connected components with SciPy-compatible interface
537    ///
538    /// ```python
539    /// # SciPy usage:
540    /// scipy.ndimage.label(input, structure=None, output=None)
541    /// ```
542    pub fn label<T>(
543        &mut self,
544        input: ArrayView2<T>,
545        structure: Option<ArrayView2<bool>>,
546    ) -> NdimageResult<LabelResult>
547    where
548        T: Float + FromPrimitive + Clone + Send + Sync + PartialOrd,
549    {
550        let binary_input = self.convert_to_binary(input);
551
552        if structure.is_some() {
553            self.add_warning(
554                "label",
555                "Custom structure not yet supported, using default connectivity",
556            );
557        }
558
559        let (labeled, num_labels) = crate::morphology::label(
560            &binary_input,
561            None, // structure
562            None, // connectivity
563            None, // background
564        )?;
565
566        Ok(LabelResult {
567            labeled_array: labeled.mapv(|v| v as i32),
568            num_features: num_labels as i32,
569        })
570    }
571}
572
573// Parameter types and conversion functions
574impl SciPyCompatLayer {
575    fn convert_sigma_param(&self, sigma: SigmaParam) -> NdimageResult<(f64, f64)> {
576        match sigma {
577            SigmaParam::Single(s) => Ok((s, s)),
578            SigmaParam::Tuple(sx, sy) => Ok((sx, sy)),
579            SigmaParam::Array(arr) => {
580                if arr.len() == 1 {
581                    Ok((arr[0], arr[0]))
582                } else if arr.len() == 2 {
583                    Ok((arr[0], arr[1]))
584                } else {
585                    Err(NdimageError::InvalidInput(
586                        "Sigma must be scalar or 2-element array".to_string(),
587                    ))
588                }
589            }
590        }
591    }
592
593    fn convert_size_param(
594        &self,
595        size: Option<SizeParam>,
596        default: (usize, usize),
597    ) -> NdimageResult<(usize, usize)> {
598        match size {
599            None => Ok(default),
600            Some(SizeParam::Single(s)) => Ok((s, s)),
601            Some(SizeParam::Tuple(sx, sy)) => Ok((sx, sy)),
602            Some(SizeParam::Array(arr)) => {
603                if arr.len() == 1 {
604                    Ok((arr[0], arr[0]))
605                } else if arr.len() == 2 {
606                    Ok((arr[0], arr[1]))
607                } else {
608                    Err(NdimageError::InvalidInput(
609                        "Size must be scalar or 2-element array".to_string(),
610                    ))
611                }
612            }
613        }
614    }
615
616    fn convert_mode_param(&self, mode: Option<&str>) -> NdimageResult<BoundaryMode> {
617        let mode_str = mode.unwrap_or(&self.config.default_mode);
618        match mode_str {
619            "reflect" => Ok(BoundaryMode::Reflect),
620            "constant" => Ok(BoundaryMode::Constant),
621            "nearest" => Ok(BoundaryMode::Nearest),
622            "mirror" => Ok(BoundaryMode::Mirror),
623            "wrap" => Ok(BoundaryMode::Wrap),
624            _ => {
625                self.add_warning_const(
626                    "parameter_conversion",
627                    &format!("Unknown mode '{}', using default 'reflect'", mode_str),
628                );
629                Ok(BoundaryMode::Reflect)
630            }
631        }
632    }
633
634    fn convert_to_binary<T>(&self, input: ArrayView2<T>) -> Array<bool, Ix2>
635    where
636        T: Float + FromPrimitive + PartialOrd,
637    {
638        input.mapv(|x| x > T::zero())
639    }
640
641    fn add_warning(&mut self, function: &str, message: &str) {
642        if self.config.show_warnings {
643            self.warnings.push(MigrationWarning {
644                function: function.to_string(),
645                message: message.to_string(),
646                suggestion: None,
647            });
648        }
649    }
650
651    fn add_warning_const(&self, function: &str, message: &str) {
652        if self.config.show_warnings {
653            eprintln!("Warning in {}: {}", function, message);
654        }
655    }
656}
657
658// Parameter types for SciPy compatibility
659
660#[derive(Debug, Clone)]
661pub enum SigmaParam {
662    Single(f64),
663    Tuple(f64, f64),
664    Array(Vec<f64>),
665}
666
667#[derive(Debug, Clone)]
668pub enum SizeParam {
669    Single(usize),
670    Tuple(usize, usize),
671    Array(Vec<usize>),
672}
673
674#[derive(Debug, Clone, PartialEq)]
675pub enum OrderParam {
676    Single(usize),
677    Tuple(usize, usize),
678    Array(Vec<usize>),
679}
680
681#[derive(Debug, Clone)]
682pub enum OriginParam {
683    Single(isize),
684    Tuple(isize, isize),
685    Array(Vec<isize>),
686}
687
688#[derive(Debug, Clone)]
689pub enum SamplingParam {
690    Single(f64),
691    Tuple(f64, f64),
692    Array(Vec<f64>),
693}
694
695#[derive(Debug, Clone)]
696pub enum IndexParam {
697    Single(i32),
698    Array(Vec<i32>),
699}
700
701// Result types
702
703#[derive(Debug, Clone)]
704pub struct DistanceTransformResult<T> {
705    pub distances: Option<Array<T, Ix2>>,
706    pub indices: Option<Array<usize, Ix3>>, // (2, height, width) for 2D
707}
708
709#[derive(Debug, Clone)]
710pub enum CenterOfMassResult {
711    Single((f64, f64)),
712    Multiple(Vec<(f64, f64)>),
713}
714
715#[derive(Debug, Clone)]
716pub struct LabelResult {
717    pub labeled_array: Array<i32, Ix2>,
718    pub num_features: i32,
719}
720
721/// Global SciPy compatibility instance
722static mut SCIPY_COMPAT: Option<SciPyCompatLayer> = None;
723static INIT: std::sync::Once = std::sync::Once::new();
724
725/// Initialize global SciPy compatibility layer
726#[allow(dead_code)]
727pub fn init_scipy_compat() {
728    INIT.call_once(|| unsafe {
729        SCIPY_COMPAT = Some(SciPyCompatLayer::default());
730    });
731}
732
733/// Get global SciPy compatibility layer
734#[allow(dead_code)]
735#[allow(static_mut_refs)]
736fn get_scipy_compat() -> &'static mut SciPyCompatLayer {
737    init_scipy_compat();
738    unsafe { SCIPY_COMPAT.as_mut().expect("Operation failed") }
739}
740
741// Global convenience functions that match SciPy API exactly
742
743/// Gaussian filter (global function matching SciPy API)
744#[allow(dead_code)]
745pub fn gaussian_filter<T>(
746    input: ArrayView2<T>,
747    sigma: SigmaParam,
748    order: Option<OrderParam>,
749    mode: Option<&str>,
750    cval: Option<f64>,
751    truncate: Option<f64>,
752) -> NdimageResult<Array<T, Ix2>>
753where
754    T: Float + FromPrimitive + Clone + Send + Sync,
755{
756    get_scipy_compat().gaussian_filter(input, sigma, order, mode, cval, truncate)
757}
758
759/// Median filter (global function matching SciPy API)
760#[allow(dead_code)]
761pub fn median_filter<T>(
762    input: ArrayView2<T>,
763    size: Option<SizeParam>,
764    footprint: Option<ArrayView2<bool>>,
765    mode: Option<&str>,
766    cval: Option<f64>,
767    origin: Option<OriginParam>,
768) -> NdimageResult<Array<T, Ix2>>
769where
770    T: Float
771        + FromPrimitive
772        + std::fmt::Debug
773        + Clone
774        + Send
775        + Sync
776        + PartialOrd
777        + std::ops::AddAssign
778        + std::ops::DivAssign
779        + 'static,
780{
781    get_scipy_compat().median_filter(input, size, footprint, mode, cval, origin)
782}
783
784/// Binary erosion (global function matching SciPy API)
785#[allow(dead_code)]
786pub fn binary_erosion<T>(
787    input: ArrayView2<T>,
788    structure: Option<ArrayView2<bool>>,
789    iterations: Option<usize>,
790    mask: Option<ArrayView2<bool>>,
791    border_value: Option<bool>,
792    origin: Option<OriginParam>,
793    brute_force: Option<bool>,
794) -> NdimageResult<Array<bool, Ix2>>
795where
796    T: Float + FromPrimitive + Clone + Send + Sync + PartialOrd,
797{
798    get_scipy_compat().binary_erosion(
799        input,
800        structure,
801        iterations,
802        mask,
803        border_value,
804        origin,
805        brute_force,
806    )
807}
808
809/// Binary erosion for boolean arrays (global function matching SciPy API)
810#[allow(dead_code)]
811pub fn binary_erosion_bool(
812    input: ArrayView2<bool>,
813    structure: Option<ArrayView2<bool>>,
814    iterations: Option<usize>,
815    mask: Option<ArrayView2<bool>>,
816    border_value: Option<bool>,
817    origin: Option<OriginParam>,
818    brute_force: Option<bool>,
819) -> NdimageResult<Array<bool, Ix2>> {
820    // Convert boolean array to f64 for compatibility with existing implementation
821    let input_f64 = input.map(|&x| if x { 1.0f64 } else { 0.0f64 });
822    get_scipy_compat().binary_erosion(
823        input_f64.view(),
824        structure,
825        iterations,
826        mask,
827        border_value,
828        origin,
829        brute_force,
830    )
831}
832
833/// Distance transform EDT (global function matching SciPy API)
834#[allow(dead_code)]
835pub fn distance_transform_edt<T>(
836    input: ArrayView2<T>,
837    sampling: Option<SamplingParam>,
838    return_distances: Option<bool>,
839    return_indices: Option<bool>,
840) -> NdimageResult<DistanceTransformResult<T>>
841where
842    T: Float + FromPrimitive + Clone + Send + Sync + PartialOrd,
843{
844    get_scipy_compat().distance_transform_edt(input, sampling, return_distances, return_indices)
845}
846
847/// Center of mass (global function matching SciPy API)
848#[allow(dead_code)]
849pub fn center_of_mass<T>(
850    input: ArrayView2<T>,
851    labels: Option<ArrayView2<i32>>,
852    index: Option<IndexParam>,
853) -> NdimageResult<CenterOfMassResult>
854where
855    T: Float
856        + FromPrimitive
857        + Clone
858        + Send
859        + Sync
860        + std::fmt::Debug
861        + std::ops::DivAssign
862        + scirs2_core::numeric::NumAssign
863        + 'static,
864{
865    get_scipy_compat().center_of_mass(input, labels, index)
866}
867
868/// Label connected components (global function matching SciPy API)
869#[allow(dead_code)]
870pub fn label<T>(
871    input: ArrayView2<T>,
872    structure: Option<ArrayView2<bool>>,
873) -> NdimageResult<LabelResult>
874where
875    T: Float + FromPrimitive + Clone + Send + Sync + PartialOrd,
876{
877    get_scipy_compat().label(input, structure)
878}
879
880/// Get migration warnings
881#[allow(dead_code)]
882pub fn get_migration_warnings() -> Vec<MigrationWarning> {
883    get_scipy_compat().get_warnings().to_vec()
884}
885
886/// Clear migration warnings
887#[allow(dead_code)]
888pub fn clear_migration_warnings() {
889    get_scipy_compat().clear_warnings();
890}
891
892/// Display migration guide
893#[allow(dead_code)]
894pub fn display_migration_guide() {
895    println!(
896        r#"
897=== SciRS2 NDImage Migration Guide ===
898
899This compatibility layer provides SciPy-compatible APIs for easy migration.
900
901Basic Usage:
902    use scirs2_ndimage::scipy_migration_layer as ndimage;
903    
904    // Same API as SciPy
905    let result = ndimage::gaussian_filter(input, sigma, None, None, None, None)?;
906
907Key Differences:
9081. All functions return Result<T, NdimageError> for error handling
9092. Some advanced parameters may not be fully supported yet
9103. Performance characteristics may differ due to Rust optimizations
911
912Migration Steps:
9131. Replace `import scipy.ndimage` with `use scirs2_ndimage::scipy_migration_layer as ndimage;`
9142. Add error handling for function calls
9153. Check migration warnings for any unsupported features
9164. Test thoroughly and report any compatibility issues
917
918For full scirs2 performance, consider using the native APIs in other modules.
919"#
920    );
921}
922
923#[cfg(test)]
924mod tests {
925    use super::*;
926    use scirs2_core::ndarray::array;
927
928    #[test]
929    fn test_scipy_compat_creation() {
930        let compat = SciPyCompatLayer::default();
931        assert!(compat.warnings.is_empty());
932    }
933
934    #[test]
935    fn test_gaussian_filter_compat() {
936        let input = array![[1.0, 2.0, 3.0], [4.0, 5.0, 6.0], [7.0, 8.0, 9.0]];
937
938        let result = gaussian_filter(
939            input.view(),
940            SigmaParam::Single(1.0),
941            None,
942            None,
943            None,
944            None,
945        );
946
947        assert!(result.is_ok());
948    }
949
950    #[test]
951    fn test_binary_erosion_compat() {
952        let input = array![
953            [true, false, true],
954            [false, true, false],
955            [true, false, true]
956        ];
957
958        let result = binary_erosion_bool(input.view(), None, None, None, None, None, None);
959
960        assert!(result.is_ok());
961    }
962
963    #[test]
964    fn test_parameter_conversion() {
965        let compat = SciPyCompatLayer::default();
966
967        // Test sigma parameter conversion
968        let sigma1 = compat
969            .convert_sigma_param(SigmaParam::Single(2.0))
970            .expect("Operation failed");
971        assert_eq!(sigma1, (2.0, 2.0));
972
973        let sigma2 = compat
974            .convert_sigma_param(SigmaParam::Tuple(1.0, 2.0))
975            .expect("Operation failed");
976        assert_eq!(sigma2, (1.0, 2.0));
977
978        // Test size parameter conversion
979        let size1 = compat
980            .convert_size_param(Some(SizeParam::Single(5)), (3, 3))
981            .expect("Operation failed");
982        assert_eq!(size1, (5, 5));
983
984        let size2 = compat
985            .convert_size_param(None, (3, 3))
986            .expect("Operation failed");
987        assert_eq!(size2, (3, 3));
988    }
989
990    #[test]
991    fn test_migration_warnings() {
992        init_scipy_compat();
993        clear_migration_warnings();
994
995        let input = array![[1.0, 2.0], [3.0, 4.0]];
996
997        // This should generate a warning for unsupported parameter
998        let _ = gaussian_filter(
999            input.view(),
1000            SigmaParam::Single(1.0),
1001            Some(OrderParam::Single(1)), // Non-zero order should warn
1002            None,
1003            None,
1004            None,
1005        );
1006
1007        let warnings = get_migration_warnings();
1008        assert!(!warnings.is_empty());
1009    }
1010}