1use 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
19pub struct SciPyCompatLayer {
21 config: CompatibilityConfig,
23 warnings: Vec<MigrationWarning>,
25}
26
27#[derive(Debug, Clone)]
28pub struct CompatibilityConfig {
29 pub strict_compatibility: bool,
31 pub show_warnings: bool,
33 pub default_dtype: String,
35 pub default_mode: String,
37 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 pub function: String,
57 pub message: String,
59 pub suggestion: Option<String>,
61}
62
63impl SciPyCompatLayer {
64 pub fn new(config: CompatibilityConfig) -> Self {
66 Self {
67 config,
68 warnings: Vec::new(),
69 }
70 }
71
72 pub fn default() -> Self {
74 Self::new(CompatibilityConfig::default())
75 }
76
77 pub fn get_warnings(&self) -> &[MigrationWarning] {
79 &self.warnings
80 }
81
82 pub fn clear_warnings(&mut self) {
84 self.warnings.clear();
85 }
86}
87
88impl SciPyCompatLayer {
90 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 let sigma_tuple = self.convert_sigma_param(sigma)?;
111 let boundary_mode = self.convert_mode_param(mode)?;
112
113 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 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; 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 let result_f64 =
150 internal_gaussian_filter(&input_f64, sigma_single, Some(border_mode), None)?;
151
152 let result = result_f64.mapv(|x| T::from_f64(x).unwrap_or(T::zero()));
154 Ok(result)
155 }
156
157 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 let input_owned = input.to_owned();
204 let size_slice = [filter_size.0, filter_size.1];
205
206 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 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 let input_owned = input.to_owned();
256 let size_slice = [filter_size.0, filter_size.1];
257
258 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
271impl SciPyCompatLayer {
273 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 let binary_input = self.convert_to_binary(input);
296
297 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 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 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 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 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, true, false, )?;
468
469 let _distances = distances_opt.ok_or_else(|| {
470 NdimageError::ComputationError("Failed to compute distances".to_string())
471 })?;
472
473 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
488impl SciPyCompatLayer {
490 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 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 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, None, None, )?;
565
566 Ok(LabelResult {
567 labeled_array: labeled.mapv(|v| v as i32),
568 num_features: num_labels as i32,
569 })
570 }
571}
572
573impl 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#[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#[derive(Debug, Clone)]
704pub struct DistanceTransformResult<T> {
705 pub distances: Option<Array<T, Ix2>>,
706 pub indices: Option<Array<usize, Ix3>>, }
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
721static mut SCIPY_COMPAT: Option<SciPyCompatLayer> = None;
723static INIT: std::sync::Once = std::sync::Once::new();
724
725#[allow(dead_code)]
727pub fn init_scipy_compat() {
728 INIT.call_once(|| unsafe {
729 SCIPY_COMPAT = Some(SciPyCompatLayer::default());
730 });
731}
732
733#[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#[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#[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#[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#[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 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#[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#[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#[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#[allow(dead_code)]
882pub fn get_migration_warnings() -> Vec<MigrationWarning> {
883 get_scipy_compat().get_warnings().to_vec()
884}
885
886#[allow(dead_code)]
888pub fn clear_migration_warnings() {
889 get_scipy_compat().clear_warnings();
890}
891
892#[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 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 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 let _ = gaussian_filter(
999 input.view(),
1000 SigmaParam::Single(1.0),
1001 Some(OrderParam::Single(1)), None,
1003 None,
1004 None,
1005 );
1006
1007 let warnings = get_migration_warnings();
1008 assert!(!warnings.is_empty());
1009 }
1010}