scirs2-ndimage 0.6.3

N-dimensional image processing module for SciRS2 (scirs2-ndimage)
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
# SciRS2 NDImage

[![crates.io](https://img.shields.io/crates/v/scirs2-ndimage.svg)](https://crates.io/crates/scirs2-ndimage)
[![License](https://img.shields.io/badge/license-Apache--2.0-blue.svg)](../LICENSE)
[![Documentation](https://img.shields.io/docsrs/scirs2-ndimage)](https://docs.rs/scirs2-ndimage)
[![Version](https://img.shields.io/badge/version-0.6.3-green)]()
[![Status](https://img.shields.io/badge/status-partial-yellow)]()
[![Tests](https://img.shields.io/badge/tests-1199%20passing-brightgreen)]()

**scirs2-ndimage** is the N-dimensional image processing crate for the [SciRS2](https://github.com/cool-japan/scirs) scientific computing library. It provides a comprehensive toolkit for filtering, morphology, interpolation, measurements, segmentation, and feature detection on arrays of arbitrary dimensionality, modeled after SciPy's `ndimage` module.

## What scirs2-ndimage Provides

Use scirs2-ndimage when you need to:

- Filter N-dimensional arrays (Gaussian, median, rank, bilateral, edge detection)
- Apply morphological operations to binary or grayscale images in any dimension
- Measure region properties (area, centroid, moments, Hu moments) after labeling
- Segment images with watershed, active contours, or graph cut methods
- Transform arrays geometrically (rotate, zoom, shift, affine transform)
- Analyze 3D volumetric data (medical images, electron microscopy)
- Process hyperspectral imagery
- Compute co-occurrence matrices and texture features
- Detect features (corners, edges, SIFT descriptors, HOG)
- Perform atlas-based segmentation

## Features (v0.6.3)

### Image Filtering
- **Gaussian Filters**: `gaussian_filter`, `gaussian_filter1d`, `gaussian_gradient_magnitude`, `gaussian_laplace`
- **Median Filter**: N-dimensional median filter with configurable footprint
- **Rank Filters**: Minimum, maximum, percentile, generic rank filter (full n-dimensional support)
- **Edge Detection**: Sobel, Prewitt, Laplacian, Scharr, Roberts cross-gradient
- **Bilateral Filter**: Edge-preserving bilateral filtering
- **Uniform Filter**: Box/uniform convolution filter
- **Generic Filter**: Apply any custom function over a sliding window
- **Convolution**: N-dimensional `convolve` and `convolve1d`
- **Boundary Modes**: `reflect`, `nearest`, `wrap`, `mirror`, `constant`
- **Fourier Filters**: Fourier Gaussian, uniform, ellipsoid, shift operations

### Morphological Operations
- **Binary Morphology**: Erosion, dilation, opening, closing, hit-or-miss transform, propagation
- **Hole Filling**: `fill_holes_2d` (2D, via morphological reconstruction) is real and working; the general N-D `binary_fill_holes` is currently a no-op stub (returns its input unchanged — see Known Issues)
- **Grayscale Morphology**: Erosion, dilation, opening, closing, top-hat (white/black), morphological gradient, Laplace
- **Distance Transforms**: Euclidean (EDT via Felzenszwalb-Huttenlocher O(n) algorithm), city-block, chessboard
- **Connected Components**: Labeling, find objects, remove small objects
- **Structuring Elements**: Generate disk (exact Euclidean disk in 2D; N > 2 dimensions fall back to a box shape), square, diamond, and arbitrary structuring elements (`iterate_structure`, which is meant to grow a structuring element over N iterations, is currently a no-op stub — see Known Issues)
- **Skeletonization**: Topological thinning to medial axis

### Image Measurements
- **Region Statistics**: Sum, mean, variance, standard deviation, min, max per label
- **Moments**: Raw moments, central moments, normalized moments, Hu moments (rotation-invariant)
- **Region Properties**: Area, perimeter, centroid, bounding box, eccentricity, orientation, principal axes
- **Center of Mass**: N-dimensional center of mass computation
- **Extrema**: Local and global minima/maxima with positions
- **Histograms**: Per-label histogram computation
- **Inertia Tensor**: Region inertia tensor for orientation analysis

### Image Segmentation
- **Thresholding**: Binary, Otsu's automatic, adaptive (mean/Gaussian)
- **Watershed**: Standard watershed and marker-controlled watershed
- **Active Contours**: Snakes with gradient vector flow (GVF)
- **Level Set Methods**: Chan-Vese segmentation (single and multi-phase)
- **Graph Cuts**: Max-flow/min-cut segmentation with interactive refinement
- **SLIC Superpixels**: Simple Linear Iterative Clustering (2D grayscale; `segmentation_advanced::superpixels_slic`)
- **Atlas-Based Segmentation**: Label fusion (majority voting, STAPLE, joint label fusion) over pre-registered atlas label volumes; no built-in registration

### Feature Detection
- **Edge Detection**: Canny edge detector, unified edge detection API
- **Corner Detection**: Harris corners, FAST corners
- **SIFT Descriptor Computation**: Scale-space keypoint detection and description
- **HOG (Histogram of Oriented Gradients)**: Cell-based gradient histogram features
- **Template Matching**: Normalized cross-correlation, zero-mean NCC
- **Gabor Filters**: 2D Gabor filter bank for texture analysis
- **Shape Analysis**: Moments-based shape descriptors, shape matching

### Geometric Interpolation
- **Map Coordinates**: Interpolate array at arbitrary coordinates (0th-5th order splines)
- **Affine Transform**: Apply an affine transformation matrix (exact 2x2 inversion in 2D; N-D beyond 2D uses a simplified diagonal-only approximation)
- **Shift**: Sub-pixel shift with spline interpolation
- **Rotate**: Array rotation about any axis (output keeps the input shape; the `reshape` option to grow the canvas is accepted but not yet honored)
- **Zoom**: Uniform zooming (N-dimensional, `zoom`); anisotropic per-axis zoom for 2D via `interpolation::zoom_optimized`
- **Spline Filter**: Pre-filter for spline interpolation (`spline_filter`, `spline_filter1d`)

> **Known stub**: `interpolation::geometric_transform` (general transform with a custom
> coordinate-mapping closure) is currently a no-op — it validates its arguments but its body
> just returns a copy of the input, ignoring the supplied mapping function entirely
> (`src/interpolation/transform.rs`). Use `map_coordinates`, `affine_transform`, `shift`,
> `rotate`, or `zoom` instead, which are genuinely implemented.

### 3D Volume Analysis
- **Volumetric Operations**: 3D morphology, filtering, distance transforms
- **3D Filters**: 3D Gaussian, Sobel, Laplacian, bilateral
- **Volume Measurements**: 3D region properties, surface area, Euler characteristic
- **Slice Processing**: Per-slice operations on 3D stacks

### Medical Image Processing
- **Frangi Vesselness**: Multi-scale vessel enhancement filter
- **Bone Enhancement**: Bone structure enhancement for CT data
- **Lung Nodule Detection**: Basic nodule candidate generation
- **DICOM-Compatible Arrays**: Works natively with 3D medical arrays

### Hyperspectral Image Analysis
- **Band Processing**: Per-band filtering and morphology
- **Spectral Indices**: NDVI, NDWI, and custom spectral index computation
- **Spectral Unmixing**: Linear unmixing of spectral signatures
- **Cloud Detection**: Cloud and shadow masking for satellite imagery
- **Pan-Sharpening**: Fusion of panchromatic and multispectral bands

### Co-occurrence Matrices and Texture
- **GLCM**: Gray-level co-occurrence matrix computation (2D and 3D)
- **Texture Features**: Contrast, correlation, energy, homogeneity from GLCM
- **LBP**: Local binary patterns
- **Gabor Feature Maps**: Multi-scale multi-orientation Gabor responses

### CNN-Inspired Feature Extraction (no neural-network training required)
- Gabor filter bank, HOG (Histogram of Oriented Gradients), simplified SIFT keypoint detection and descriptors (`deep_features` module)
- Heuristic ML-assisted detection utilities: `SemanticFeatureExtractor`, `ObjectProposalGenerator`, learned edge/keypoint descriptor configs
- Note: this crate does not depend on scirs2-neural; there is no wired-up hook for external deep-learning model integration today

## Installation

```toml
[dependencies]
scirs2-ndimage = "0.6.3"
```

For parallel processing and SIMD:

```toml
[dependencies]
scirs2-ndimage = { version = "0.6.3", features = ["parallel", "simd"] }
```

## Feature Flags

| Flag | Description |
|------|-------------|
| `parallel` | Enable Rayon-based multi-core parallel processing (recommended for arrays >10K elements) |
| `simd` | Enable SIMD vectorization for filters and morphological operations |
| `gpu` | GPU backend abstraction layer (device detection, buffer management). Does not auto-enable a specific backend |
| `cuda` | CUDA backend (`backend/cuda.rs`); kernel JIT compiles CUDA-C to PTX via the pure-Rust `oxicuda-nvrtc` crate (runtime `dlopen` of `libnvrtc`, zero build-time CUDA SDK dependency); experimental |
| `opencl` | OpenCL backend scaffolding; experimental |
| `metal` | Metal backend scaffolding (macOS only); experimental |
| `compression` | Pure-Rust compression for streaming/out-of-core I/O via `oxiarc-deflate`/`oxiarc-zstd`/`oxiarc-lz4` |

Note: the `gpu`/`cuda`/`opencl`/`metal` flags expose device-detection and buffer-management
scaffolding; generic GPU kernel dispatch (`backend::gpu_acceleration_framework`) currently
returns an explicit "not yet implemented" error rather than executing on real hardware, so
CPU code paths remain the primary, production-ready path.

## Quick Start

```rust
use scirs2_ndimage::{filters, morphology};
use scirs2_core::ndarray::Array2;

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let image = Array2::<f64>::from_shape_fn((100, 100), |(i, j)| {
        if (i > 30 && i < 70) && (j > 30 && j < 70) { 1.0 } else { 0.0 }
    });

    // Gaussian smoothing
    let smoothed = filters::gaussian_filter(&image, 2.0, None, None)?;

    // Morphological dilation (binary morphology operates on bool arrays)
    let binary = image.mapv(|v| v > 0.5).into_dyn();
    let struct_elem = morphology::disk_structure(3.0, None)?;
    let dilated =
        morphology::binary_dilation(&binary, Some(&struct_elem), None, None, None, None, None)?;

    println!("Image processed: {:?}, dilated: {:?}", smoothed.shape(), dilated.shape());
    Ok(())
}
```

## Comprehensive Examples

### Filtering

```rust
use scirs2_ndimage::filters;
use scirs2_core::ndarray::Array2;

fn filtering_example() -> Result<(), Box<dyn std::error::Error>> {
    let image = Array2::<f64>::from_shape_fn((256, 256), |(i, j)| {
        (i as f64 * 0.1).sin() * (j as f64 * 0.1).cos()
    });

    // Gaussian filter
    let gaussian = filters::gaussian_filter(&image, 2.0, None, None)?;

    // Median filter (rank-based, N-dimensional)
    let median = filters::median_filter(&image, &[5, 5], None)?;

    // Maximum filter
    let dilated = filters::maximum_filter(&image, &[3, 3], None)?;

    // Sobel edge detection
    let edges_x = filters::sobel(&image, 0, None)?;
    let edges_y = filters::sobel(&image, 1, None)?;

    // Custom generic filter (mean over 5x5 window)
    let mean_filtered = filters::generic_filter(
        &image, |window| window.iter().sum::<f64>() / window.len() as f64,
        &[5, 5], None, None,
    )?;

    println!("All filters applied");
    Ok(())
}
```

### Morphological Operations

```rust
use scirs2_ndimage::morphology;
use scirs2_core::ndarray::{Array, IxDyn};

fn morphology_example() -> Result<(), Box<dyn std::error::Error>> {
    // Binary morphology operates on `Array<bool, D>`; build the image directly
    // in dynamic-dimension (`IxDyn`) form so it matches the structuring element.
    let binary = Array::from_shape_fn(IxDyn(&[100, 100]), |idx| {
        idx[0] > 30 && idx[0] < 70 && idx[1] > 30 && idx[1] < 70
    });

    let disk = morphology::disk_structure(5.0, None)?;

    // Binary erosion, dilation, and opening take:
    // (input, structure, iterations, mask, border_value, origin, brute_force)
    let eroded = morphology::binary_erosion(&binary, Some(&disk), None, None, None, None, None)?;
    let dilated = morphology::binary_dilation(&binary, Some(&disk), None, None, None, None, None)?;

    // Opening removes small bright regions
    let opened = morphology::binary_opening(&binary, Some(&disk), None, None, None, None, None)?;

    // Distance transform (Euclidean, O(n) algorithm)
    let (distances, _indices) = morphology::distance_transform_edt(&binary, None, true, false)?;

    // Hit-or-miss for pattern detection (structure1, structure2, mask, border_value, origin1, origin2)
    let pattern = Array::from_shape_vec(IxDyn(&[3, 3]), vec![false, true, false, true, true, true, false, true, false])?;
    let hit_miss = morphology::binary_hit_or_miss(&binary, Some(&pattern), None, None, None, None, None)?;

    println!(
        "eroded={:?} dilated={:?} opened={:?} distances_present={} hit_miss={:?}",
        eroded.shape(), dilated.shape(), opened.shape(), distances.is_some(), hit_miss.shape()
    );

    Ok(())
}
```

### Region Measurements

```rust
use scirs2_ndimage::{measurements, moment_invariants, morphology};
use scirs2_core::ndarray::Array2;

fn measurement_example() -> Result<(), Box<dyn std::error::Error>> {
    let image = Array2::<f64>::from_shape_fn((100, 100), |(i, j)| {
        if (i as f64 - 50.0).hypot(j as f64 - 50.0) < 20.0 { 1.0 } else { 0.0 }
    });
    let binary = image.mapv(|v| v > 0.5);

    // Label connected components (label lives in `morphology`, not `measurements`)
    let (labels, num_labels) = morphology::label(&binary, None, None, None)?;
    println!("Found {} labeled region(s)", num_labels);

    // Region properties (2D: area, centroid, perimeter, eccentricity, orientation, ...)
    let props = measurements::regionprops_2d(&image, &labels)?;
    for region in &props {
        println!("Region {}: area={}, centroid={:?}",
            region.label, region.area, region.centroid);
    }

    // Hu moments (rotation-invariant descriptors) - lives in `moment_invariants`, is infallible
    let hu = moment_invariants::hu_moments(&image.view());
    println!("Hu moments: {:?}", hu);

    Ok(())
}
```

### Watershed Segmentation

```rust
use scirs2_ndimage::segmentation::watershed;
use scirs2_ndimage::filters;
use scirs2_core::ndarray::Array2;

fn watershed_example() -> Result<(), Box<dyn std::error::Error>> {
    let image = Array2::<f64>::zeros((200, 200));
    // ... populate image ...

    // Compute gradient magnitude as elevation map
    let grad_x = filters::sobel(&image, 0, None)?;
    let grad_y = filters::sobel(&image, 1, None)?;
    let gradient = grad_x.mapv(|v| v * v) + grad_y.mapv(|v| v * v);
    let gradient = gradient.mapv(f64::sqrt);

    // Markers: 0 = unknown region, unique positive integers = seed regions
    let mut markers = Array2::<i32>::zeros((200, 200));
    markers[[50, 50]] = 1;
    markers[[150, 150]] = 2;

    let labels = watershed(&gradient, &markers)?;
    println!("Watershed labels shape: {:?}", labels.shape());

    Ok(())
}
```

### 3D Volume Processing

```rust
use scirs2_ndimage::filters;
use scirs2_core::ndarray::Array3;

fn volume_example() -> Result<(), Box<dyn std::error::Error>> {
    let volume = Array3::<f64>::zeros((64, 256, 256));

    // 3D Gaussian smoothing
    let smoothed = filters::gaussian_filter(&volume, 1.5, None, None)?;

    // 3D rank filter
    let max_filtered = filters::maximum_filter(&volume, &[3, 3, 3], None)?;

    // 3D median filter
    let median = filters::median_filter(&volume, &[3, 3, 3], None)?;

    println!("3D volume processed: {:?}", smoothed.shape());
    Ok(())
}
```

### SLIC Superpixels

```rust
use scirs2_ndimage::segmentation_advanced::superpixels_slic;
use scirs2_core::ndarray::Array2;

fn slic_example() -> Result<(), Box<dyn std::error::Error>> {
    let image = Array2::<f64>::from_shape_fn((100, 100), |(i, j)| {
        ((i + j) as f64 / 200.0).sin()
    });

    // (image, n_segments, compactness) -> label array in [0, n_segments)
    let labels = superpixels_slic(&image, 100, 10.0)?;
    println!("Superpixel labels shape: {:?}", labels.shape());
    Ok(())
}
```

### Atlas-Based Segmentation

```rust
use scirs2_ndimage::segmentation::atlas::AtlasSegmentation;
use scirs2_core::ndarray::Array3;

fn atlas_example() -> Result<(), Box<dyn std::error::Error>> {
    // Pre-registered atlas label volumes (registration itself is not performed by this crate)
    let atlas_label_1 = Array3::<u32>::zeros((32, 32, 32));
    let atlas_label_2 = Array3::<u32>::zeros((32, 32, 32));

    // Default configuration fuses via majority voting; STAPLE and joint label
    // fusion are also available through `AtlasSegmentation::with_config`.
    let result = AtlasSegmentation::new().segment(&[atlas_label_1, atlas_label_2], None, None)?;
    println!("Fused label volume shape: {:?}", result.label.shape());
    Ok(())
}
```

## Performance

- **SIMD acceleration**: 2-4x speedup on supported filter and morphology operations
- **Parallel processing**: Linear scaling with CPU cores for large arrays (`parallel` feature)
- **O(n) distance transform**: Felzenszwalb-Huttenlocher separable EDT algorithm
- **Memory-efficient**: Chunked processing for images larger than available RAM
- **N-dimensional**: Consistent API and performance across 1D, 2D, 3D, and higher dimensions

## Test Coverage

Freshly measured via `cargo nextest run -p scirs2-ndimage` (2026-07-15):

| Mode | Result |
|------|--------|
| Default features | 1170 tests run: 1170 passed, 1 skipped, 0 failed |
| `--all-features` | 1199 tests run: 1199 passed, 3 skipped, 0 failed |

## Compatibility with SciPy ndimage

API is modeled after `scipy.ndimage`. Key equivalents:

| SciRS2 | SciPy |
|--------|-------|
| `filters::gaussian_filter()` | `scipy.ndimage.gaussian_filter()` |
| `filters::median_filter()` | `scipy.ndimage.median_filter()` |
| `filters::sobel()` | `scipy.ndimage.sobel()` |
| `morphology::binary_erosion()` | `scipy.ndimage.binary_erosion()` |
| `morphology::distance_transform_edt()` | `scipy.ndimage.distance_transform_edt()` |
| `morphology::label()` | `scipy.ndimage.label()` |
| `measurements::center_of_mass()` | `scipy.ndimage.center_of_mass()` |
| `interpolation::affine_transform()` | `scipy.ndimage.affine_transform()` |
| `interpolation::map_coordinates()` | `scipy.ndimage.map_coordinates()` |
| `interpolation::rotate()` | `scipy.ndimage.rotate()` |
| `interpolation::zoom()` | `scipy.ndimage.zoom()` |

## Known Issues

A handful of functions validate their arguments correctly but have a body that does not perform
the documented computation (no `todo!()`/`unimplemented!()` panic — they just silently return an
unchanged copy of the input or a hardcoded placeholder value), so they will not show up in a
naive stub scan. Confirmed as of 2026-07-15 (see `TODO.md` for detail and source locations):

- `interpolation::geometric_transform` — ignores its custom coordinate-mapping closure entirely
- `morphology::iterate_structure` — does not actually grow/iterate the structuring element
- `morphology::binary_fill_holes` (general N-D) — returns the input unchanged; use `fill_holes_2d` for 2D instead, which is a real, working implementation
- `peak_prominences` / `peak_widths` — return hardcoded placeholder values, not computed ones

Other known limitations:
- `interpolation::rotate`'s `reshape` option is accepted but not honored (output always keeps the input's shape)
- `interpolation::affine_transform` inverts the transformation matrix exactly in 2D; for N > 2 dimensions it falls back to a simplified diagonal-only approximation
- GPU feature flags (`gpu`/`cuda`/`opencl`/`metal`) provide device-detection/buffer scaffolding only; generic kernel dispatch returns an explicit "not yet implemented" error rather than running on hardware
- Bilateral filter performance degrades significantly for large kernel sizes (>21x21)
- Chan-Vese segmentation convergence depends strongly on `mu`/`lambda1`/`lambda2`; automatic initialization is not implemented
- SLIC superpixels (`segmentation_advanced::superpixels_slic`) are 2D grayscale only
- Atlas-based segmentation requires pre-registered atlas label volumes; no built-in registration is performed

## Documentation

Full API reference: [docs.rs/scirs2-ndimage](https://docs.rs/scirs2-ndimage)

## License

Licensed under the Apache License 2.0. See [LICENSE](../LICENSE) for details.