1use scirs2_core::ndarray::Dimension;
10use std::collections::HashMap;
11
12use crate::error::NdimageError;
13
14#[derive(Debug, Clone)]
16pub struct PyArrayInfo {
17 pub shape: Vec<usize>,
19 pub dtype: String,
21 pub strides: Vec<isize>,
23 pub contiguous: bool,
25}
26
27#[derive(Debug, Clone)]
29pub struct PyError {
30 pub error_type: String,
32 pub message: String,
34 pub context: Option<HashMap<String, String>>,
36}
37
38impl From<NdimageError> for PyError {
39 fn from(error: NdimageError) -> Self {
40 match error {
41 NdimageError::InvalidInput(msg) => PyError {
42 error_type: "ValueError".to_string(),
43 message: msg,
44 context: None,
45 },
46 NdimageError::DimensionError(msg) => PyError {
47 error_type: "ValueError".to_string(),
48 message: format!("Dimension , error: {}", msg),
49 context: None,
50 },
51 NdimageError::ComputationError(msg) => PyError {
52 error_type: "RuntimeError".to_string(),
53 message: msg,
54 context: None,
55 },
56 NdimageError::MemoryError(msg) => PyError {
57 error_type: "MemoryError".to_string(),
58 message: msg,
59 context: None,
60 },
61 _ => PyError {
63 error_type: "RuntimeError".to_string(),
64 message: format!("{}", error),
65 context: None,
66 },
67 }
68 }
69}
70
71#[derive(Debug, Clone)]
73pub struct PyParameter {
74 pub name: String,
76 pub param_type: String,
78 pub default: Option<String>,
80 pub description: String,
82 pub required: bool,
84}
85
86#[derive(Debug, Clone)]
88pub struct PyFunction {
89 pub name: String,
91 pub description: String,
93 pub parameters: Vec<PyParameter>,
95 pub return_type: String,
97 pub examples: Vec<String>,
99}
100
101pub mod array_conversion {
103 use super::*;
104
105 pub fn array_to_py_info<T, D>(
107 array: &scirs2_core::ndarray::ArrayBase<scirs2_core::ndarray::OwnedRepr<T>, D>,
108 ) -> PyArrayInfo
109 where
110 T: 'static,
111 D: Dimension,
112 {
113 let shape = array.shape().to_vec();
114 let strides = array.strides().iter().map(|&s| s as isize).collect();
115
116 let dtype = if std::any::TypeId::of::<T>() == std::any::TypeId::of::<f32>() {
117 "float32".to_string()
118 } else if std::any::TypeId::of::<T>() == std::any::TypeId::of::<f64>() {
119 "float64".to_string()
120 } else if std::any::TypeId::of::<T>() == std::any::TypeId::of::<i32>() {
121 "int32".to_string()
122 } else if std::any::TypeId::of::<T>() == std::any::TypeId::of::<i64>() {
123 "int64".to_string()
124 } else {
125 "unknown".to_string()
126 };
127
128 PyArrayInfo {
129 shape,
130 dtype,
131 strides,
132 contiguous: array.is_standard_layout(),
133 }
134 }
135
136 pub fn validate_array_compatibility<T>(info: &PyArrayInfo) -> Result<(), PyError>
138 where
139 T: 'static,
140 {
141 let expected_dtype = if std::any::TypeId::of::<T>() == std::any::TypeId::of::<f32>() {
143 "float32"
144 } else if std::any::TypeId::of::<T>() == std::any::TypeId::of::<f64>() {
145 "float64"
146 } else if std::any::TypeId::of::<T>() == std::any::TypeId::of::<i32>() {
147 "int32"
148 } else if std::any::TypeId::of::<T>() == std::any::TypeId::of::<i64>() {
149 "int64"
150 } else {
151 return Err(PyError {
152 error_type: "TypeError".to_string(),
153 message: "Unsupported array data type".to_string(),
154 context: None,
155 });
156 };
157
158 if info.dtype != expected_dtype {
159 return Err(PyError {
160 error_type: "TypeError".to_string(),
161 message: format!("Expected dtype '{}', got '{}'", expected_dtype, info.dtype),
162 context: None,
163 });
164 }
165
166 let total_elements: usize = info.shape.iter().product();
168 if total_elements > 1_000_000_000 {
169 return Err(PyError {
170 error_type: "MemoryError".to_string(),
171 message: "Array too large for processing".to_string(),
172 context: None,
173 });
174 }
175
176 Ok(())
177 }
178}
179
180pub mod api_spec {
182 use super::*;
183
184 pub fn generate_filter_api_specs() -> Vec<PyFunction> {
186 vec![
187 PyFunction {
188 name: "gaussian_filter".to_string(),
189 description: "Apply Gaussian filter to n-dimensional array.".to_string(),
190 parameters: vec![
191 PyParameter {
192 name: "input".to_string(),
193 param_type: "array_like".to_string(),
194 default: None,
195 description: "Input array to filter".to_string(),
196 required: true,
197 },
198 PyParameter {
199 name: "sigma".to_string(),
200 param_type: "float or sequence of floats".to_string(),
201 default: None,
202 description: "Standard deviation for Gaussian kernel".to_string(),
203 required: true,
204 },
205 PyParameter {
206 name: "mode".to_string(),
207 param_type: "str".to_string(),
208 default: Some("'reflect'".to_string()),
209 description:
210 "Boundary mode ('reflect', 'constant', 'nearest', 'mirror', 'wrap')"
211 .to_string(),
212 required: false,
213 },
214 ],
215 return_type: "ndarray".to_string(),
216 examples: vec![
217 ">>> import scirs2_ndimage as ndi".to_string(),
218 ">>> result = ndi.gaussian_filter(image, sigma=1.0)".to_string(),
219 ],
220 },
221 PyFunction {
222 name: "median_filter".to_string(),
223 description: "Apply median filter to n-dimensional array.".to_string(),
224 parameters: vec![
225 PyParameter {
226 name: "input".to_string(),
227 param_type: "array_like".to_string(),
228 default: None,
229 description: "Input array to filter".to_string(),
230 required: true,
231 },
232 PyParameter {
233 name: "size".to_string(),
234 param_type: "int or sequence of ints".to_string(),
235 default: None,
236 description: "Size of the median filter window".to_string(),
237 required: true,
238 },
239 ],
240 return_type: "ndarray".to_string(),
241 examples: vec![">>> result = ndi.median_filter(image, size=3)".to_string()],
242 },
243 ]
244 }
245
246 pub fn generate_morphology_api_specs() -> Vec<PyFunction> {
248 vec![PyFunction {
249 name: "binary_erosion".to_string(),
250 description: "Multidimensional binary erosion with given structuring element."
251 .to_string(),
252 parameters: vec![
253 PyParameter {
254 name: "input".to_string(),
255 param_type: "array_like".to_string(),
256 default: None,
257 description: "Binary array to be eroded".to_string(),
258 required: true,
259 },
260 PyParameter {
261 name: "structure".to_string(),
262 param_type: "array_like, optional".to_string(),
263 default: Some("None".to_string()),
264 description: "Structuring element for erosion".to_string(),
265 required: false,
266 },
267 ],
268 return_type: "ndarray".to_string(),
269 examples: vec![">>> result = ndi.binary_erosion(binary_image)".to_string()],
270 }]
271 }
272
273 pub fn generate_python_docs() -> String {
275 let mut docs = String::new();
276
277 docs.push_str("# SciRS2 NDImage Python API\n\n");
278 docs.push_str("## Filters\n\n");
279
280 for func in generate_filter_api_specs() {
281 docs.push_str(&format!("### {}\n\n", func.name));
282 docs.push_str(&format!("{}\n\n", func.description));
283 docs.push_str("**Parameters:**\n\n");
284
285 for param in &func.parameters {
286 let req_str = if param.required {
287 " (required)"
288 } else {
289 " (optional)"
290 };
291 let default_str = param
292 .default
293 .as_ref()
294 .map(|d| format!(", default: {}", d))
295 .unwrap_or_default();
296
297 docs.push_str(&format!(
298 "- `{}` (*{}*{}{}) - {}\n",
299 param.name, param.param_type, req_str, default_str, param.description
300 ));
301 }
302
303 docs.push_str(&format!("\n**Returns:** {}\n\n", func.return_type));
304
305 if !func.examples.is_empty() {
306 docs.push_str("**Examples:**\n\n```python\n");
307 for example in &func.examples {
308 docs.push_str(&format!("{}\n", example));
309 }
310 docs.push_str("```\n\n");
311 }
312 }
313
314 docs.push_str("## Morphology\n\n");
315
316 for func in generate_morphology_api_specs() {
317 docs.push_str(&format!("### {}\n\n", func.name));
318 docs.push_str(&format!("{}\n\n", func.description));
319 }
321
322 docs
323 }
324}
325
326pub mod binding_examples {
328
329 pub fn example_gaussian_filter_binding() -> String {
332 r#"
333#[pyfunction]
334#[pyo3(signature = (input, sigma, mode="reflect"))]
335#[allow(dead_code)]
336fn gaussian_filter(
337 py: Python,
338 input: &PyArray<f64, Ix2>,
339 sigma: f64,
340 mode: Option<&str>,
341) -> PyResult<Py<PyArray<f64, Ix2>>> {
342 let input_array = input.readonly();
343 let input_view = input_array.as_array();
344
345 let boundary_mode = match mode.unwrap_or("reflect") {
346 "reflect" => BoundaryMode::Reflect,
347 "constant" => BoundaryMode::Constant(0.0),
348 "nearest" => BoundaryMode::Nearest,
349 "mirror" => BoundaryMode::Mirror,
350 "wrap" => BoundaryMode::Wrap_ => return Err(PyValueError::new_err("Invalid boundary mode")),
351 };
352
353 let result = crate::filters::gaussian_filter(&input_view, sigma, Some(boundary_mode))
354 .map_err(|e| PyRuntimeError::new_err(e.to_string()))?;
355
356 Ok(result.to_pyarray(py).to_owned())
357}
358"#
359 .to_string()
360 }
361
362 pub fn example_median_filter_binding() -> String {
364 r#"
365#[pyfunction]
366#[allow(dead_code)]
367fn median_filter(
368 py: Python,
369 input: &PyArray<f64, Ix2>,
370 size: usize,
371) -> PyResult<Py<PyArray<f64, Ix2>>> {
372 let input_array = input.readonly();
373 let input_view = input_array.as_array();
374
375 let result = crate::filters::median_filter(&input_view, size)
376 .map_err(|e| PyRuntimeError::new_err(e.to_string()))?;
377
378 Ok(result.to_pyarray(py).to_owned())
379}
380"#
381 .to_string()
382 }
383
384 pub fn generate_module_definition() -> String {
386 r#"
387#[pymodule]
388#[allow(dead_code)]
389fn scirs2_ndimage(py: Python, m: &PyModule) -> PyResult<()> {
390 // Filters submodule
391 let filters_module = PyModule::new(py, "filters")?;
392 filters_module.add_function(wrap_pyfunction!(gaussian_filter, filters_module)?)?;
393 filters_module.add_function(wrap_pyfunction!(median_filter, filters_module)?)?;
394 m.add_submodule(filters_module)?;
395
396 // Morphology submodule
397 let morphology_module = PyModule::new(py, "morphology")?;
398 morphology_module.add_function(wrap_pyfunction!(binary_erosion, morphology_module)?)?;
399 morphology_module.add_function(wrap_pyfunction!(binary_dilation, morphology_module)?)?;
400 m.add_submodule(morphology_module)?;
401
402 // Measurements submodule
403 let measurements_module = PyModule::new(py, "measurements")?;
404 measurements_module.add_function(wrap_pyfunction!(label, measurements_module)?)?;
405 measurements_module.add_function(wrap_pyfunction!(center_of_mass, measurements_module)?)?;
406 m.add_submodule(measurements_module)?;
407
408 Ok(())
409}
410"#
411 .to_string()
412 }
413}
414
415pub mod setup {
417
418 pub fn generate_setup_py() -> String {
420 r#"
421from setuptools import setup
422from pyo3_setuptools_rust import Pyo3RustExtension, build_rust
423
424setup(
425 name="scirs2-ndimage",
426 version="0.1.0",
427 author="SciRS2 Team",
428 author_email="contact@scirs2.org",
429 description="High-performance N-dimensional image processing library",
430 long_description=open("README.md").read(),
431 long_description_content_type="text/markdown",
432 url="https://github.com/cool-japan/scirs",
433 rust_extensions=[
434 Pyo3RustExtension(
435 "scirs2_ndimage._rust",
436 binding="pyo3",
437 debug=False,
438 )
439 ],
440 packages=["scirs2_ndimage"],
441 zip_safe=False,
442 python_requires=">=3.7",
443 install_requires=[
444 "numpy>=1.19.0",
445 ],
446 classifiers=[
447 "Development Status :: 5 - Production/Stable",
448 "Intended Audience :: Science/Research",
449 "License :: OSI Approved :: Apache Software License",
450 "Programming Language :: Python :: 3",
451 "Programming Language :: Python :: 3.7",
452 "Programming Language :: Python :: 3.8",
453 "Programming Language :: Python :: 3.9",
454 "Programming Language :: Python :: 3.10",
455 "Programming Language :: Python :: 3.11",
456 "Programming Language :: Rust",
457 "Topic :: Scientific/Engineering",
458 "Topic :: Scientific/Engineering :: Image Processing",
459 ],
460 cmdclass={"build_rust": build_rust},
461)
462"#
463 .to_string()
464 }
465
466 pub fn generate_init_py() -> String {
468 r#"
469'"'
470SciRS2 NDImage - High-performance N-dimensional image processing
471==============================================================
472
473A comprehensive library for n-dimensional image processing with SciPy-compatible APIs
474and Rust performance.
475
476Submodules
477----------
478filters : Filtering operations (Gaussian, median, etc.)
479morphology : Morphological operations (erosion, dilation, etc.)
480measurements : Measurements and analysis functions
481interpolation : Interpolation and geometric transformations
482segmentation : Image segmentation algorithms
483features : Feature detection algorithms
484
485Examples
486--------
487>>> import scirs2_ndimage as ndi
488>>> import numpy as np
489>>> image = np.random.random((100, 100))
490>>> filtered = ndi.gaussian_filter(image, sigma=1.0)
491>>> binary = image > 0.5
492>>> eroded = ndi.binary_erosion(binary)
493'"'
494
495from ._rust import *
496from . import filters, morphology, measurements, interpolation, segmentation, features
497
498__version__ = "0.1.0"
499__author__ = "SciRS2 Team"
500
501# Expose commonly used functions at the top level
502from .filters import gaussian_filter, median_filter, uniform_filter
503from .morphology import binary_erosion, binary_dilation, binary_opening, binary_closing
504from .measurements import label, center_of_mass, find_objects
505from .features import canny, sobel_edges
506
507__all__ = [
508 "gaussian_filter",
509 "median_filter",
510 "uniform_filter",
511 "binary_erosion",
512 "binary_dilation",
513 "binary_opening",
514 "binary_closing",
515 "label",
516 "center_of_mass",
517 "find_objects",
518 "canny",
519 "sobel_edges",
520]
521"#
522 .to_string()
523 }
524
525 pub fn generate_install_instructions() -> String {
527 r#"
528# SciRS2 NDImage Python Installation Guide
529
530## Prerequisites
531
5321. **Rust**: Install Rust from https://rustup.rs/
5332. **Python**: Python 3.7 or later
5343. **Dependencies**:
535 ```bash
536 pip install setuptools-rust pyo3-setuptools-rust numpy
537 ```
538
539## Building from Source
540
5411. Clone the repository:
542 ```bash
543 git clone https://github.com/cool-japan/scirs.git
544 cd scirs/scirs2-ndimage
545 ```
546
5472. Build and install:
548 ```bash
549 pip install .
550 ```
551
5523. For development installation:
553 ```bash
554 pip install -e .
555 ```
556
557## Usage
558
559```python
560import scirs2_ndimage as ndi
561import numpy as np
562
563# Create sample data
564image = np.random.random((100, 100))
565
566# Apply Gaussian filter
567filtered = ndi.gaussian_filter(image, sigma=1.0)
568
569# Binary morphology
570binary = image > 0.5
571eroded = ndi.binary_erosion(binary)
572
573# Feature detection
574edges = ndi.canny(image, sigma=1.0, low_threshold=0.1, high_threshold=0.2)
575```
576
577## Performance Notes
578
579- SciRS2 NDImage leverages Rust's performance for computational kernels
580- SIMD optimizations are automatically enabled when available
581- Parallel processing is used for large arrays
582- Memory usage is optimized through zero-copy operations where possible
583
584## Compatibility
585
586This package provides a SciPy-compatible API, making it a drop-in replacement
587for many `scipy.ndimage` functions with improved performance.
588"#
589 .to_string()
590 }
591}
592
593#[cfg(test)]
594mod tests {
595 use super::*;
596 use scirs2_core::ndarray::array;
597
598 #[test]
599 fn test_array_to_py_info() {
600 let arr = array![[1.0f64, 2.0], [3.0, 4.0]];
601 let info = array_conversion::array_to_py_info(&arr);
602
603 assert_eq!(info.shape, vec![2, 2]);
604 assert_eq!(info.dtype, "float64");
605 assert!(info.contiguous);
606 }
607
608 #[test]
609 fn test_validate_array_compatibility() {
610 let info = PyArrayInfo {
611 shape: vec![10, 10],
612 dtype: "float64".to_string(),
613 strides: vec![8, 80],
614 contiguous: true,
615 };
616
617 let result = array_conversion::validate_array_compatibility::<f64>(&info);
618 assert!(result.is_ok());
619 }
620
621 #[test]
622 fn test_invalid_dtype_compatibility() {
623 let info = PyArrayInfo {
624 shape: vec![10, 10],
625 dtype: "float32".to_string(),
626 strides: vec![4, 40],
627 contiguous: true,
628 };
629
630 let result = array_conversion::validate_array_compatibility::<f64>(&info);
631 assert!(result.is_err());
632 }
633
634 #[test]
635 fn test_error_conversion() {
636 let ndimage_error = NdimageError::InvalidInput("Test error".to_string());
637 let py_error: PyError = ndimage_error.into();
638
639 assert_eq!(py_error.error_type, "ValueError");
640 assert_eq!(py_error.message, "Test error");
641 }
642
643 #[test]
644 fn test_api_spec_generation() {
645 let specs = api_spec::generate_filter_api_specs();
646 assert!(!specs.is_empty());
647
648 let gaussian_spec = specs.iter().find(|s| s.name == "gaussian_filter");
649 assert!(gaussian_spec.is_some());
650
651 let spec = gaussian_spec.expect("Operation failed");
652 assert!(!spec.parameters.is_empty());
653 assert!(spec.parameters.iter().any(|p| p.name == "input"));
654 assert!(spec.parameters.iter().any(|p| p.name == "sigma"));
655 }
656
657 #[test]
658 fn test_python_docs_generation() {
659 let docs = api_spec::generate_python_docs();
660 assert!(docs.contains("# SciRS2 NDImage Python API"));
661 assert!(docs.contains("gaussian_filter"));
662 assert!(docs.contains("Parameters:"));
663 }
664}