nbmorph 0.1.0__tar.gz → 0.2.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {nbmorph-0.1.0/src/nbmorph.egg-info → nbmorph-0.2.0}/PKG-INFO +27 -24
- {nbmorph-0.1.0 → nbmorph-0.2.0}/README.md +27 -24
- {nbmorph-0.1.0 → nbmorph-0.2.0}/pyproject.toml +1 -1
- {nbmorph-0.1.0 → nbmorph-0.2.0}/src/nbmorph/diamond_kernel.py +28 -0
- nbmorph-0.2.0/src/nbmorph/minmax.py +72 -0
- {nbmorph-0.1.0 → nbmorph-0.2.0}/src/nbmorph/mode.py +91 -15
- {nbmorph-0.1.0 → nbmorph-0.2.0}/src/nbmorph/morphology.py +30 -25
- nbmorph-0.2.0/src/nbmorph/ops.py +40 -0
- {nbmorph-0.1.0 → nbmorph-0.2.0}/src/nbmorph/utils.py +1 -1
- {nbmorph-0.1.0 → nbmorph-0.2.0}/src/nbmorph/zero_edges.py +5 -5
- {nbmorph-0.1.0 → nbmorph-0.2.0/src/nbmorph.egg-info}/PKG-INFO +27 -24
- nbmorph-0.1.0/src/nbmorph/minmax.py +0 -22
- nbmorph-0.1.0/src/nbmorph/ops.py +0 -16
- {nbmorph-0.1.0 → nbmorph-0.2.0}/LICENSE +0 -0
- {nbmorph-0.1.0 → nbmorph-0.2.0}/setup.cfg +0 -0
- {nbmorph-0.1.0 → nbmorph-0.2.0}/src/nbmorph/__init__.py +0 -0
- {nbmorph-0.1.0 → nbmorph-0.2.0}/src/nbmorph/box_kernel.py +0 -0
- {nbmorph-0.1.0 → nbmorph-0.2.0}/src/nbmorph.egg-info/SOURCES.txt +0 -0
- {nbmorph-0.1.0 → nbmorph-0.2.0}/src/nbmorph.egg-info/dependency_links.txt +0 -0
- {nbmorph-0.1.0 → nbmorph-0.2.0}/src/nbmorph.egg-info/requires.txt +0 -0
- {nbmorph-0.1.0 → nbmorph-0.2.0}/src/nbmorph.egg-info/top_level.txt +0 -0
- {nbmorph-0.1.0 → nbmorph-0.2.0}/tests/test_core.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: nbmorph
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: A small package with Numba-accelerated morphological operations.
|
|
5
5
|
Author-email: Marius Causemann <mariusca@simula.no>
|
|
6
6
|
License: MIT License
|
|
@@ -42,13 +42,16 @@ Dynamic: license-file
|
|
|
42
42
|
|
|
43
43
|
# nbmorph
|
|
44
44
|
|
|
45
|
+
[](https://github.com/MariusCausemann/nbmorph/actions/workflows/test.yaml)
|
|
46
|
+
[](https://badge.fury.io/py/nbmorph)
|
|
47
|
+
|
|
45
48
|
A small, Numba-accelerated Python package for morphological operations on 3D labeled images.
|
|
46
49
|
|
|
47
50
|
`nbmorph` provides a set of common morphological operations optimized for performance using Numba. It is designed to work with 3D NumPy arrays representing labeled image data, where different integer labels correspond to different objects.
|
|
48
51
|
|
|
49
52
|
## Features
|
|
50
53
|
|
|
51
|
-
* **Numba-accelerated:** Operations are just-in-time compiled with Numba for high performance on CPUs.
|
|
54
|
+
* **Numba-accelerated and multithreaded:** Operations are just-in-time compiled with Numba for high performance on CPUs.
|
|
52
55
|
|
|
53
56
|
* **3D Label Image Support:** All operations are designed for 3D labeled images (integer NumPy arrays).
|
|
54
57
|
|
|
@@ -67,52 +70,52 @@ A small, Numba-accelerated Python package for morphological operations on 3D lab
|
|
|
67
70
|
* `smooth_labels_spherical`: Smoothes object boundaries by performing an opening followed by a closing.
|
|
68
71
|
|
|
69
72
|

|
|
70
|
-
*Demonstration of the smoothing effect with varying radii and iterations on a sample image.*
|
|
73
|
+
*Demonstration of the smoothing effect with varying radii and iterations on a sample image. The smoothing is followed by a dilation operation to fill up the empty space.*
|
|
71
74
|
|
|
72
75
|
|
|
73
76
|
## Installation
|
|
74
77
|
|
|
75
|
-
You can install `nbmorph` directly from
|
|
78
|
+
You can install `nbmorph` directly from pypi using pip:
|
|
76
79
|
|
|
77
80
|
```
|
|
78
|
-
pip install
|
|
81
|
+
pip install nbmorph
|
|
79
82
|
|
|
80
83
|
```
|
|
81
84
|
|
|
82
85
|
|
|
83
86
|
## Usage
|
|
84
87
|
|
|
85
|
-
Here is a basic example of how to use `nbmorph` to apply
|
|
88
|
+
Here is a basic example of how to use `nbmorph` to apply operations to a 3D labeled image.
|
|
86
89
|
|
|
87
90
|
```
|
|
88
91
|
import numpy as np
|
|
89
92
|
import nbmorph
|
|
93
|
+
import numba
|
|
94
|
+
|
|
95
|
+
numba.set_num_threads(4)
|
|
90
96
|
|
|
91
97
|
# Create a sample 3D labeled image
|
|
92
|
-
# For example, a 5x5x5 cube of
|
|
98
|
+
# For example, a 5x5x5 cube of two different labels in a 10x10x10 volume
|
|
93
99
|
labels = np.zeros((10, 10, 10), dtype=np.uint16)
|
|
94
|
-
labels[2:7, 2:7, 2:
|
|
100
|
+
labels[2:7, 2:7, 2:5] = 1
|
|
101
|
+
labels[2:7, 2:7, 5:7] = 2
|
|
95
102
|
|
|
96
|
-
#
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
print("Smoothing complete. The smoothed labels are in the 'smoothed_labels' array.")
|
|
100
|
-
|
|
101
|
-
```
|
|
103
|
+
# First execution may take a while due to numba compilation
|
|
104
|
+
# Apply morphological erosion with a radius of 1
|
|
105
|
+
eroded_labels = nbmorph.erode_labels_spherical(labels, radius=1)
|
|
102
106
|
|
|
103
|
-
|
|
107
|
+
# Apply morphological dilation with a radius of 1
|
|
108
|
+
dilated_labels = nbmorph.dilate_labels_spherical(labels, radius=1)
|
|
104
109
|
|
|
105
|
-
|
|
110
|
+
# Apply morphological opening with a radius of 1
|
|
111
|
+
opened_labels = nbmorph.open_labels_spherical(labels, radius=1)
|
|
106
112
|
|
|
107
|
-
|
|
113
|
+
# Apply morphological closing with a radius of 1
|
|
114
|
+
closed_labels = nbmorph.close_labels_spherical(labels, radius=1)
|
|
108
115
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
* `nbmorph.close_labels_spherical(labels, radius=1, iterations=1)`
|
|
114
|
-
|
|
115
|
-
* `nbmorph.smooth_labels_spherical(labels, radius=1, iterations=1)`
|
|
116
|
+
# Apply morphological smoothing with a radius of 1
|
|
117
|
+
smoothed_labels = nbmorph.smooth_labels_spherical(labels, radius=1)
|
|
118
|
+
```
|
|
116
119
|
|
|
117
120
|
## Testing
|
|
118
121
|
|
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
# nbmorph
|
|
2
2
|
|
|
3
|
+
[](https://github.com/MariusCausemann/nbmorph/actions/workflows/test.yaml)
|
|
4
|
+
[](https://badge.fury.io/py/nbmorph)
|
|
5
|
+
|
|
3
6
|
A small, Numba-accelerated Python package for morphological operations on 3D labeled images.
|
|
4
7
|
|
|
5
8
|
`nbmorph` provides a set of common morphological operations optimized for performance using Numba. It is designed to work with 3D NumPy arrays representing labeled image data, where different integer labels correspond to different objects.
|
|
6
9
|
|
|
7
10
|
## Features
|
|
8
11
|
|
|
9
|
-
* **Numba-accelerated:** Operations are just-in-time compiled with Numba for high performance on CPUs.
|
|
12
|
+
* **Numba-accelerated and multithreaded:** Operations are just-in-time compiled with Numba for high performance on CPUs.
|
|
10
13
|
|
|
11
14
|
* **3D Label Image Support:** All operations are designed for 3D labeled images (integer NumPy arrays).
|
|
12
15
|
|
|
@@ -25,52 +28,52 @@ A small, Numba-accelerated Python package for morphological operations on 3D lab
|
|
|
25
28
|
* `smooth_labels_spherical`: Smoothes object boundaries by performing an opening followed by a closing.
|
|
26
29
|
|
|
27
30
|

|
|
28
|
-
*Demonstration of the smoothing effect with varying radii and iterations on a sample image.*
|
|
31
|
+
*Demonstration of the smoothing effect with varying radii and iterations on a sample image. The smoothing is followed by a dilation operation to fill up the empty space.*
|
|
29
32
|
|
|
30
33
|
|
|
31
34
|
## Installation
|
|
32
35
|
|
|
33
|
-
You can install `nbmorph` directly from
|
|
36
|
+
You can install `nbmorph` directly from pypi using pip:
|
|
34
37
|
|
|
35
38
|
```
|
|
36
|
-
pip install
|
|
39
|
+
pip install nbmorph
|
|
37
40
|
|
|
38
41
|
```
|
|
39
42
|
|
|
40
43
|
|
|
41
44
|
## Usage
|
|
42
45
|
|
|
43
|
-
Here is a basic example of how to use `nbmorph` to apply
|
|
46
|
+
Here is a basic example of how to use `nbmorph` to apply operations to a 3D labeled image.
|
|
44
47
|
|
|
45
48
|
```
|
|
46
49
|
import numpy as np
|
|
47
50
|
import nbmorph
|
|
51
|
+
import numba
|
|
52
|
+
|
|
53
|
+
numba.set_num_threads(4)
|
|
48
54
|
|
|
49
55
|
# Create a sample 3D labeled image
|
|
50
|
-
# For example, a 5x5x5 cube of
|
|
56
|
+
# For example, a 5x5x5 cube of two different labels in a 10x10x10 volume
|
|
51
57
|
labels = np.zeros((10, 10, 10), dtype=np.uint16)
|
|
52
|
-
labels[2:7, 2:7, 2:
|
|
58
|
+
labels[2:7, 2:7, 2:5] = 1
|
|
59
|
+
labels[2:7, 2:7, 5:7] = 2
|
|
53
60
|
|
|
54
|
-
#
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
print("Smoothing complete. The smoothed labels are in the 'smoothed_labels' array.")
|
|
58
|
-
|
|
59
|
-
```
|
|
61
|
+
# First execution may take a while due to numba compilation
|
|
62
|
+
# Apply morphological erosion with a radius of 1
|
|
63
|
+
eroded_labels = nbmorph.erode_labels_spherical(labels, radius=1)
|
|
60
64
|
|
|
61
|
-
|
|
65
|
+
# Apply morphological dilation with a radius of 1
|
|
66
|
+
dilated_labels = nbmorph.dilate_labels_spherical(labels, radius=1)
|
|
62
67
|
|
|
63
|
-
|
|
68
|
+
# Apply morphological opening with a radius of 1
|
|
69
|
+
opened_labels = nbmorph.open_labels_spherical(labels, radius=1)
|
|
64
70
|
|
|
65
|
-
|
|
71
|
+
# Apply morphological closing with a radius of 1
|
|
72
|
+
closed_labels = nbmorph.close_labels_spherical(labels, radius=1)
|
|
66
73
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
* `nbmorph.close_labels_spherical(labels, radius=1, iterations=1)`
|
|
72
|
-
|
|
73
|
-
* `nbmorph.smooth_labels_spherical(labels, radius=1, iterations=1)`
|
|
74
|
+
# Apply morphological smoothing with a radius of 1
|
|
75
|
+
smoothed_labels = nbmorph.smooth_labels_spherical(labels, radius=1)
|
|
76
|
+
```
|
|
74
77
|
|
|
75
78
|
## Testing
|
|
76
79
|
|
|
@@ -91,4 +94,4 @@ python scripts/benchmark.py
|
|
|
91
94
|
|
|
92
95
|
## License
|
|
93
96
|
|
|
94
|
-
This project is licensed under the MIT License - see the `LICENSE` file for details.
|
|
97
|
+
This project is licensed under the MIT License - see the `LICENSE` file for details.
|
|
@@ -5,10 +5,38 @@ from .ops import choose_op
|
|
|
5
5
|
|
|
6
6
|
@numba.njit(inline="always")
|
|
7
7
|
def clamp(index, size):
|
|
8
|
+
"""
|
|
9
|
+
Clamps an index to valid bounds for a 3x3 neighborhood.
|
|
10
|
+
|
|
11
|
+
This function calculates the valid range for a 3x3 neighborhood around a given index,
|
|
12
|
+
ensuring it stays within the array bounds.
|
|
13
|
+
|
|
14
|
+
Args:
|
|
15
|
+
index (int): The center index.
|
|
16
|
+
size (int): The size of the array dimension.
|
|
17
|
+
|
|
18
|
+
Returns:
|
|
19
|
+
Tuple[int, int]: The lower and upper bounds for the neighborhood.
|
|
20
|
+
"""
|
|
8
21
|
return max(0,index-1), min(index+1, size-1)
|
|
9
22
|
|
|
10
23
|
@numba.njit(parallel=True, cache=True)
|
|
11
24
|
def diamond_loop_padded(data, opname:str, out=None, onlyzero=False):
|
|
25
|
+
"""
|
|
26
|
+
Applies a morphological operation using a diamond (6-connected) kernel to a 3D array.
|
|
27
|
+
|
|
28
|
+
This function processes each voxel in the array, applying the specified operation
|
|
29
|
+
to its 6-connected neighbors. It handles boundary conditions by padding.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
data (np.ndarray): The input 3D array.
|
|
33
|
+
opname (str): The operation to perform ("min", "max", "zeroedges").
|
|
34
|
+
out (np.ndarray, optional): The output array. If None, a new array is created.
|
|
35
|
+
onlyzero (bool, optional): If True, only processes voxels with value 0. Defaults to False.
|
|
36
|
+
|
|
37
|
+
Returns:
|
|
38
|
+
np.ndarray: The processed 3D array.
|
|
39
|
+
"""
|
|
12
40
|
if out is None:
|
|
13
41
|
out = np.empty_like(data)
|
|
14
42
|
assert data.shape == out.shape
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
import numba
|
|
3
|
+
from .diamond_kernel import diamond_loop_padded
|
|
4
|
+
from .box_kernel import kernel3x3x3
|
|
5
|
+
|
|
6
|
+
@numba.njit(cache=True)
|
|
7
|
+
def minimum_box(a, out=None, onlyzero=False):
|
|
8
|
+
"""
|
|
9
|
+
Applies a 3x3x3 box minimum filter to a 3D array.
|
|
10
|
+
|
|
11
|
+
This function computes the minimum value within a 3x3x3 neighborhood for each voxel.
|
|
12
|
+
|
|
13
|
+
Args:
|
|
14
|
+
a (np.ndarray): The input 3D array.
|
|
15
|
+
out (np.ndarray, optional): The output array. If None, a new array is created.
|
|
16
|
+
onlyzero (bool, optional): If True, only processes voxels with value 0. Defaults to False.
|
|
17
|
+
|
|
18
|
+
Returns:
|
|
19
|
+
np.ndarray: The filtered 3D array.
|
|
20
|
+
"""
|
|
21
|
+
return kernel3x3x3(a, opname="min", out=out, onlyzero=onlyzero)
|
|
22
|
+
|
|
23
|
+
@numba.njit(cache=True)
|
|
24
|
+
def maximum_box(a, out=None, onlyzero=False):
|
|
25
|
+
"""
|
|
26
|
+
Applies a 3x3x3 box maximum filter to a 3D array.
|
|
27
|
+
|
|
28
|
+
This function computes the maximum value within a 3x3x3 neighborhood for each voxel.
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
a (np.ndarray): The input 3D array.
|
|
32
|
+
out (np.ndarray, optional): The output array. If None, a new array is created.
|
|
33
|
+
onlyzero (bool, optional): If True, only processes voxels with value 0. Defaults to False.
|
|
34
|
+
|
|
35
|
+
Returns:
|
|
36
|
+
np.ndarray: The filtered 3D array.
|
|
37
|
+
"""
|
|
38
|
+
return kernel3x3x3(a, opname="max", out=out, onlyzero=onlyzero)
|
|
39
|
+
|
|
40
|
+
@numba.njit(cache=True)
|
|
41
|
+
def minimum_diamond(data, out=None, onlyzero=False):
|
|
42
|
+
"""
|
|
43
|
+
Applies a diamond (6-connected) minimum filter to a 3D array.
|
|
44
|
+
|
|
45
|
+
This function computes the minimum value within a diamond-shaped neighborhood for each voxel.
|
|
46
|
+
|
|
47
|
+
Args:
|
|
48
|
+
data (np.ndarray): The input 3D array.
|
|
49
|
+
out (np.ndarray, optional): The output array. If None, a new array is created.
|
|
50
|
+
onlyzero (bool, optional): If True, only processes voxels with value 0. Defaults to False.
|
|
51
|
+
|
|
52
|
+
Returns:
|
|
53
|
+
np.ndarray: The filtered 3D array.
|
|
54
|
+
"""
|
|
55
|
+
return diamond_loop_padded(data, opname="min", out=out, onlyzero=onlyzero)
|
|
56
|
+
|
|
57
|
+
@numba.njit(cache=True)
|
|
58
|
+
def maximum_diamond(data, out=None, onlyzero=False):
|
|
59
|
+
"""
|
|
60
|
+
Applies a diamond (6-connected) maximum filter to a 3D array.
|
|
61
|
+
|
|
62
|
+
This function computes the maximum value within a diamond-shaped neighborhood for each voxel.
|
|
63
|
+
|
|
64
|
+
Args:
|
|
65
|
+
data (np.ndarray): The input 3D array.
|
|
66
|
+
out (np.ndarray, optional): The output array. If None, a new array is created.
|
|
67
|
+
onlyzero (bool, optional): If True, only processes voxels with value 0. Defaults to False.
|
|
68
|
+
|
|
69
|
+
Returns:
|
|
70
|
+
np.ndarray: The filtered 3D array.
|
|
71
|
+
"""
|
|
72
|
+
return diamond_loop_padded(data, opname="max", out=out, onlyzero=onlyzero)
|
|
@@ -4,19 +4,32 @@ from .minmax import maximum_box, maximum_diamond
|
|
|
4
4
|
|
|
5
5
|
@numba.njit
|
|
6
6
|
def fast_mode(a):
|
|
7
|
+
"""
|
|
8
|
+
Find the mode of a 1D array, ignoring zeros.
|
|
9
|
+
|
|
10
|
+
This is an O(n^2) algorithm, but fast on small data (len(a) < 50), as needed here.
|
|
11
|
+
|
|
12
|
+
Args:
|
|
13
|
+
a (np.ndarray): The input 1D array.
|
|
14
|
+
|
|
15
|
+
Returns:
|
|
16
|
+
The mode of the array.
|
|
17
|
+
"""
|
|
7
18
|
return fast_modeN(a, len(a))
|
|
8
19
|
|
|
9
20
|
@numba.njit(inline="always")
|
|
10
21
|
def fast_modeN(a, N):
|
|
11
22
|
"""
|
|
12
|
-
Find the mode of a 1D array, ignoring zeros.
|
|
13
|
-
|
|
23
|
+
Find the mode of the first N elements of a 1D array, ignoring zeros.
|
|
24
|
+
|
|
25
|
+
This is an O(n^2) algorithm, but fast on small data (N < 50), as needed here.
|
|
14
26
|
|
|
15
27
|
Args:
|
|
16
28
|
a (np.ndarray): The input 1D array.
|
|
29
|
+
N (int): The number of elements to consider.
|
|
17
30
|
|
|
18
31
|
Returns:
|
|
19
|
-
The mode of the array.
|
|
32
|
+
The mode of the first N elements of the array.
|
|
20
33
|
"""
|
|
21
34
|
max_count = 0
|
|
22
35
|
current_count = 0
|
|
@@ -40,7 +53,16 @@ def fast_modeN(a, N):
|
|
|
40
53
|
|
|
41
54
|
@numba.njit(inline="always")
|
|
42
55
|
def _cs(a, b):
|
|
43
|
-
"""
|
|
56
|
+
"""
|
|
57
|
+
Performs a compare-swap on two values.
|
|
58
|
+
|
|
59
|
+
Args:
|
|
60
|
+
a: First value.
|
|
61
|
+
b: Second value.
|
|
62
|
+
|
|
63
|
+
Returns:
|
|
64
|
+
Tuple containing the smaller value followed by the larger value.
|
|
65
|
+
"""
|
|
44
66
|
if a > b:
|
|
45
67
|
return b, a
|
|
46
68
|
else:
|
|
@@ -54,7 +76,15 @@ def sort26_network(
|
|
|
54
76
|
):
|
|
55
77
|
"""
|
|
56
78
|
Sorts 26 elements using a pre-defined sorting network.
|
|
57
|
-
|
|
79
|
+
|
|
80
|
+
This function implements a sorting network from https://bertdobbelaere.github.io/sorting_networks.html
|
|
81
|
+
to efficiently sort a fixed number of elements.
|
|
82
|
+
|
|
83
|
+
Args:
|
|
84
|
+
v0-v25: The 26 values to be sorted.
|
|
85
|
+
|
|
86
|
+
Returns:
|
|
87
|
+
Tuple containing the 26 input values in sorted order (ascending).
|
|
58
88
|
"""
|
|
59
89
|
v0, v1 = _cs(v0, v1); v2, v3 = _cs(v2, v3); v4, v5 = _cs(v4, v5); v6, v7 = _cs(v6, v7); v8, v9 = _cs(v8, v9); v10, v11 = _cs(v10, v11); v12, v13 = _cs(v12, v13); v14, v15 = _cs(v14, v15); v16, v17 = _cs(v16, v17); v18, v19 = _cs(v18, v19); v20, v21 = _cs(v20, v21); v22, v23 = _cs(v22, v23); v24, v25 = _cs(v24, v25)
|
|
60
90
|
v0, v2 = _cs(v0, v2); v1, v3 = _cs(v1, v3); v4, v6 = _cs(v4, v6); v5, v7 = _cs(v5, v7); v8, v10 = _cs(v8, v10); v9, v11 = _cs(v9, v11); v14, v16 = _cs(v14, v16); v15, v17 = _cs(v15, v17); v18, v20 = _cs(v18, v20); v19, v21 = _cs(v19, v21); v22, v24 = _cs(v22, v24); v23, v25 = _cs(v23, v25)
|
|
@@ -78,7 +108,15 @@ def sort26_network(
|
|
|
78
108
|
def sort6_network(v0, v1, v2, v3, v4, v5):
|
|
79
109
|
"""
|
|
80
110
|
Sorts 6 elements using a pre-defined sorting network.
|
|
81
|
-
|
|
111
|
+
|
|
112
|
+
This function implements a sorting network from https://bertdobbelaere.github.io/sorting_networks.html
|
|
113
|
+
to efficiently sort a fixed number of elements.
|
|
114
|
+
|
|
115
|
+
Args:
|
|
116
|
+
v0-v5: The 6 values to be sorted.
|
|
117
|
+
|
|
118
|
+
Returns:
|
|
119
|
+
Tuple containing the 6 input values in sorted order (ascending).
|
|
82
120
|
"""
|
|
83
121
|
v0, v5 = _cs(v0, v5)
|
|
84
122
|
v1, v3 = _cs(v1, v3)
|
|
@@ -102,11 +140,20 @@ def sort6_network(v0, v1, v2, v3, v4, v5):
|
|
|
102
140
|
@numba.njit(inline="always")
|
|
103
141
|
def mode_diamond(data, z, y, x):
|
|
104
142
|
"""
|
|
105
|
-
Calculates the mode of a diamond neighborhood.
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
(z, y+1, x), (z-1, y, x), (z+1, y, x).
|
|
109
|
-
|
|
143
|
+
Calculates the mode of a diamond neighborhood in a 3D array.
|
|
144
|
+
|
|
145
|
+
The diamond neighborhood includes the 6 direct (6-connected) neighbors of the center point:
|
|
146
|
+
(z, y, x-1), (z, y, x+1), (z, y-1, x), (z, y+1, x), (z-1, y, x), (z+1, y, x).
|
|
147
|
+
|
|
148
|
+
Args:
|
|
149
|
+
data (np.ndarray): The 3D input array.
|
|
150
|
+
z (int): Z-coordinate of the center point.
|
|
151
|
+
y (int): Y-coordinate of the center point.
|
|
152
|
+
x (int): X-coordinate of the center point.
|
|
153
|
+
|
|
154
|
+
Returns:
|
|
155
|
+
The mode (most frequent value) of the diamond neighborhood, ignoring zeros.
|
|
156
|
+
"""
|
|
110
157
|
|
|
111
158
|
(v0, v1, v2, v3, v4, v5) = sort6_network(
|
|
112
159
|
data[z, y, x-1], data[z, y, x+1],
|
|
@@ -140,9 +187,19 @@ def mode_diamond(data, z, y, x):
|
|
|
140
187
|
@numba.njit(inline="always")
|
|
141
188
|
def mode_box(data, z, y, x):
|
|
142
189
|
"""
|
|
143
|
-
Calculates the mode of a 3x3x3 neighborhood
|
|
144
|
-
|
|
145
|
-
|
|
190
|
+
Calculates the mode of a 3x3x3 neighborhood in a 3D array.
|
|
191
|
+
|
|
192
|
+
The neighborhood includes all 26 surrounding voxels of the center point.
|
|
193
|
+
|
|
194
|
+
Args:
|
|
195
|
+
data (np.ndarray): The 3D input array.
|
|
196
|
+
z (int): Z-coordinate of the center point.
|
|
197
|
+
y (int): Y-coordinate of the center point.
|
|
198
|
+
x (int): X-coordinate of the center point.
|
|
199
|
+
|
|
200
|
+
Returns:
|
|
201
|
+
The mode (most frequent value) of the 3x3x3 neighborhood, ignoring zeros.
|
|
202
|
+
"""
|
|
146
203
|
|
|
147
204
|
(v0, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11,
|
|
148
205
|
v12, v13, v14, v15, v16, v17, v18, v19, v20,
|
|
@@ -236,7 +293,26 @@ def mode_box(data, z, y, x):
|
|
|
236
293
|
|
|
237
294
|
|
|
238
295
|
@numba.njit(inline="always")
|
|
239
|
-
def load_box_stencil(data, z, y, x, sz,sy,sx, nbs):
|
|
296
|
+
def load_box_stencil(data, z, y, x, sz, sy, sx, nbs):
|
|
297
|
+
"""
|
|
298
|
+
Loads a 3x3x3 box stencil into a neighbors array, counting only non-zero values.
|
|
299
|
+
|
|
300
|
+
This function extracts the 26 neighbors of a center point in a 3D array, handling
|
|
301
|
+
boundary conditions appropriately.
|
|
302
|
+
|
|
303
|
+
Args:
|
|
304
|
+
data (np.ndarray): The 3D input array.
|
|
305
|
+
z (int): Z-coordinate of the center point.
|
|
306
|
+
y (int): Y-coordinate of the center point.
|
|
307
|
+
x (int): X-coordinate of the center point.
|
|
308
|
+
sz (int): Size of the array in Z-dimension.
|
|
309
|
+
sy (int): Size of the array in Y-dimension.
|
|
310
|
+
sx (int): Size of the array in X-dimension.
|
|
311
|
+
nbs (np.ndarray): Pre-allocated array to store the neighbor values.
|
|
312
|
+
|
|
313
|
+
Returns:
|
|
314
|
+
int: The number of non-zero values loaded into the neighbors array.
|
|
315
|
+
"""
|
|
240
316
|
z1 = -1 if z > 0 else 0; z2 = 2 if z < sz-1 else 1
|
|
241
317
|
y1 = -1 if y > 0 else 0; y2 = 2 if y < sy-1 else 1
|
|
242
318
|
x1 = -1 if x > 0 else 0; x2 = 2 if x < sx-1 else 1
|
|
@@ -12,12 +12,14 @@ def dilate_labels_spherical(labels: np.ndarray, radius: int=1,
|
|
|
12
12
|
Performs fast, quasi-spherical multilabel dilation on a CPU.
|
|
13
13
|
This approximates a spherical structuring element by alternating between a box (3x3x3)
|
|
14
14
|
and a diamond (6-connected) kernel for each iteration.
|
|
15
|
+
|
|
15
16
|
Args:
|
|
16
|
-
labels: The input 3D labeled image (must be an integer type).
|
|
17
|
-
radius: The dilation radius in voxels. Each iteration expands by one voxel.
|
|
17
|
+
labels (np.ndarray): The input 3D labeled image (must be an integer type).
|
|
18
|
+
radius (int, optional): The dilation radius in voxels. Each iteration expands by one voxel. Defaults to 1.
|
|
19
|
+
struct_sequence (str, optional): The sequence of structuring elements to use. "D" for diamond, "B" for box. Defaults to "DDB".
|
|
18
20
|
|
|
19
21
|
Returns:
|
|
20
|
-
The dilated 3D labeled image.
|
|
22
|
+
np.ndarray: The dilated 3D labeled image.
|
|
21
23
|
"""
|
|
22
24
|
# Use two buffers that are swapped (ping-pong) to propagate labels iteratively
|
|
23
25
|
# We start by copying the original labels to both
|
|
@@ -45,11 +47,12 @@ def erode_labels_spherical(labels: np.ndarray, radius: int=1,
|
|
|
45
47
|
Performs fast, quasi-spherical multilabel erosion on a CPU.
|
|
46
48
|
|
|
47
49
|
Args:
|
|
48
|
-
labels (np.ndarray): The input 3D labeled image.
|
|
49
|
-
radius (int, optional): The erosion radius. Defaults to 1.
|
|
50
|
+
labels (np.ndarray): The input 3D labeled image (must be an integer type).
|
|
51
|
+
radius (int, optional): The erosion radius in voxels. Each iteration shrinks by one voxel. Defaults to 1.
|
|
52
|
+
struct_sequence (str, optional): The sequence of structuring elements to use. "D" for diamond, "B" for box. Defaults to "DDB".
|
|
50
53
|
|
|
51
54
|
Returns:
|
|
52
|
-
np.ndarray: The eroded labeled image.
|
|
55
|
+
np.ndarray: The eroded 3D labeled image.
|
|
53
56
|
"""
|
|
54
57
|
assert radius > 0
|
|
55
58
|
pong = np.copy(labels)
|
|
@@ -71,18 +74,18 @@ def erode_labels_spherical(labels: np.ndarray, radius: int=1,
|
|
|
71
74
|
ping, pong = pong, ping
|
|
72
75
|
return ping
|
|
73
76
|
|
|
74
|
-
|
|
77
|
+
@numba.njit
|
|
75
78
|
def open_labels_spherical(labels: np.ndarray, radius: int=1, iterations:int=1) -> np.ndarray:
|
|
76
79
|
"""
|
|
77
80
|
Performs a morphological opening on a labeled image.
|
|
78
81
|
|
|
79
82
|
Args:
|
|
80
|
-
labels (np.ndarray): The input 3D labeled image.
|
|
81
|
-
radius (int): The radius of the spherical structuring element.
|
|
83
|
+
labels (np.ndarray): The input 3D labeled image (must be an integer type).
|
|
84
|
+
radius (int, optional): The radius of the spherical structuring element. Defaults to 1.
|
|
82
85
|
iterations (int, optional): The number of iterations. Defaults to 1.
|
|
83
86
|
|
|
84
87
|
Returns:
|
|
85
|
-
np.ndarray: The opened labeled image.
|
|
88
|
+
np.ndarray: The opened 3D labeled image.
|
|
86
89
|
"""
|
|
87
90
|
out = np.copy(labels)
|
|
88
91
|
for i in range(iterations):
|
|
@@ -90,42 +93,44 @@ def open_labels_spherical(labels: np.ndarray, radius: int=1, iterations:int=1) -
|
|
|
90
93
|
out = dilate_labels_spherical(out, radius)
|
|
91
94
|
return out
|
|
92
95
|
|
|
93
|
-
|
|
96
|
+
@numba.njit
|
|
94
97
|
def close_labels_spherical(labels: np.ndarray, radius: int, iterations:int=1) -> np.ndarray:
|
|
95
98
|
"""
|
|
96
99
|
Performs a morphological closing on a labeled image.
|
|
97
100
|
|
|
98
101
|
Args:
|
|
99
|
-
labels (np.ndarray): The input 3D labeled image.
|
|
102
|
+
labels (np.ndarray): The input 3D labeled image (must be an integer type).
|
|
100
103
|
radius (int): The radius of the spherical structuring element.
|
|
101
104
|
iterations (int, optional): The number of iterations. Defaults to 1.
|
|
102
105
|
|
|
103
106
|
Returns:
|
|
104
|
-
np.ndarray: The closed labeled image.
|
|
107
|
+
np.ndarray: The closed 3D labeled image.
|
|
105
108
|
"""
|
|
106
109
|
out = np.copy(labels)
|
|
107
|
-
#print("closing...")
|
|
108
110
|
for i in range(iterations):
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
out = erode_labels_spherical(out1, radius)
|
|
112
|
-
#print(f"after erode: {out.sum()}")
|
|
111
|
+
out = dilate_labels_spherical(out, radius)
|
|
112
|
+
out = erode_labels_spherical(out, radius)
|
|
113
113
|
return out
|
|
114
114
|
|
|
115
|
-
|
|
116
|
-
def smooth_labels_spherical(labels: np.ndarray, radius: int,
|
|
115
|
+
@numba.njit
|
|
116
|
+
def smooth_labels_spherical(labels: np.ndarray, radius: int,
|
|
117
|
+
iterations:int=1, dilate_radius:int=0) -> np.ndarray:
|
|
117
118
|
"""
|
|
118
119
|
Performs a morphological smoothing on a labeled image.
|
|
119
120
|
|
|
120
121
|
Args:
|
|
121
|
-
labels (np.ndarray): The input 3D labeled image.
|
|
122
|
+
labels (np.ndarray): The input 3D labeled image (must be an integer type).
|
|
122
123
|
radius (int): The radius of the spherical structuring element.
|
|
123
124
|
iterations (int, optional): The number of iterations. Defaults to 1.
|
|
125
|
+
dilate_radius (int, optional): Additional dilation radius to apply after smoothing. Defaults to 0.
|
|
124
126
|
|
|
125
127
|
Returns:
|
|
126
|
-
np.ndarray: The smoothed labeled image.
|
|
128
|
+
np.ndarray: The smoothed 3D labeled image.
|
|
127
129
|
"""
|
|
130
|
+
out = np.copy(labels)
|
|
128
131
|
for i in range(iterations):
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
+
out = open_labels_spherical(out, radius)
|
|
133
|
+
out = close_labels_spherical(out, radius)
|
|
134
|
+
if dilate_radius > 0:
|
|
135
|
+
out = dilate_labels_spherical(out, dilate_radius)
|
|
136
|
+
return out
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import numba
|
|
2
|
+
|
|
3
|
+
@numba.njit(inline="always")
|
|
4
|
+
def zero_if_not_allequal(nbs):
|
|
5
|
+
"""
|
|
6
|
+
Returns the first value if all values in the array are equal, otherwise returns 0.
|
|
7
|
+
|
|
8
|
+
This function is used for the "zeroedges" operation to identify boundary voxels.
|
|
9
|
+
|
|
10
|
+
Args:
|
|
11
|
+
nbs (np.ndarray): Array of neighbor values.
|
|
12
|
+
|
|
13
|
+
Returns:
|
|
14
|
+
int: The first value if all values are equal, otherwise 0.
|
|
15
|
+
"""
|
|
16
|
+
if max(nbs) == min(nbs):
|
|
17
|
+
return nbs[0]
|
|
18
|
+
else:
|
|
19
|
+
return 0
|
|
20
|
+
|
|
21
|
+
@numba.njit(inline="always")
|
|
22
|
+
def choose_op(opname, nbs, onlyzero):
|
|
23
|
+
"""
|
|
24
|
+
Selects and applies a morphological operation to a neighborhood of values.
|
|
25
|
+
|
|
26
|
+
This function dispatches to the appropriate operation based on the opname parameter.
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
opname (str): The operation to perform ("min", "max", "zeroedges").
|
|
30
|
+
nbs (np.ndarray): Array of neighbor values.
|
|
31
|
+
onlyzero (bool): If True, only processes voxels with value 0.
|
|
32
|
+
|
|
33
|
+
Returns:
|
|
34
|
+
The result of the applied operation.
|
|
35
|
+
"""
|
|
36
|
+
if onlyzero and nbs[0]>0: return nbs[0]
|
|
37
|
+
match opname:
|
|
38
|
+
case "min": return min(nbs)
|
|
39
|
+
case "max": return max(nbs)
|
|
40
|
+
case "zeroedges": return zero_if_not_allequal(nbs)
|
|
@@ -5,10 +5,14 @@ from .box_kernel import kernel3x3x3
|
|
|
5
5
|
@numba.njit(cache=True)
|
|
6
6
|
def zero_label_edges_box(a, out=None):
|
|
7
7
|
"""
|
|
8
|
-
|
|
8
|
+
Sets the edges of labels to zero using a 3x3x3 box neighborhood.
|
|
9
|
+
|
|
10
|
+
This function identifies voxels at the boundary of labeled regions and sets them to zero,
|
|
11
|
+
effectively eroding the labels by one voxel.
|
|
9
12
|
|
|
10
13
|
Args:
|
|
11
14
|
a (np.ndarray): The input 3D labeled array.
|
|
15
|
+
out (np.ndarray, optional): The output array. If None, a new array is created.
|
|
12
16
|
|
|
13
17
|
Returns:
|
|
14
18
|
np.ndarray: The array with label edges set to zero.
|
|
@@ -19,7 +23,3 @@ def zero_label_edges_box(a, out=None):
|
|
|
19
23
|
@numba.njit(cache=True)
|
|
20
24
|
def zero_label_edges_diamond(data, out=None):
|
|
21
25
|
return diamond_loop_padded(data, out=out, opname="zeroedges")
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: nbmorph
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: A small package with Numba-accelerated morphological operations.
|
|
5
5
|
Author-email: Marius Causemann <mariusca@simula.no>
|
|
6
6
|
License: MIT License
|
|
@@ -42,13 +42,16 @@ Dynamic: license-file
|
|
|
42
42
|
|
|
43
43
|
# nbmorph
|
|
44
44
|
|
|
45
|
+
[](https://github.com/MariusCausemann/nbmorph/actions/workflows/test.yaml)
|
|
46
|
+
[](https://badge.fury.io/py/nbmorph)
|
|
47
|
+
|
|
45
48
|
A small, Numba-accelerated Python package for morphological operations on 3D labeled images.
|
|
46
49
|
|
|
47
50
|
`nbmorph` provides a set of common morphological operations optimized for performance using Numba. It is designed to work with 3D NumPy arrays representing labeled image data, where different integer labels correspond to different objects.
|
|
48
51
|
|
|
49
52
|
## Features
|
|
50
53
|
|
|
51
|
-
* **Numba-accelerated:** Operations are just-in-time compiled with Numba for high performance on CPUs.
|
|
54
|
+
* **Numba-accelerated and multithreaded:** Operations are just-in-time compiled with Numba for high performance on CPUs.
|
|
52
55
|
|
|
53
56
|
* **3D Label Image Support:** All operations are designed for 3D labeled images (integer NumPy arrays).
|
|
54
57
|
|
|
@@ -67,52 +70,52 @@ A small, Numba-accelerated Python package for morphological operations on 3D lab
|
|
|
67
70
|
* `smooth_labels_spherical`: Smoothes object boundaries by performing an opening followed by a closing.
|
|
68
71
|
|
|
69
72
|

|
|
70
|
-
*Demonstration of the smoothing effect with varying radii and iterations on a sample image.*
|
|
73
|
+
*Demonstration of the smoothing effect with varying radii and iterations on a sample image. The smoothing is followed by a dilation operation to fill up the empty space.*
|
|
71
74
|
|
|
72
75
|
|
|
73
76
|
## Installation
|
|
74
77
|
|
|
75
|
-
You can install `nbmorph` directly from
|
|
78
|
+
You can install `nbmorph` directly from pypi using pip:
|
|
76
79
|
|
|
77
80
|
```
|
|
78
|
-
pip install
|
|
81
|
+
pip install nbmorph
|
|
79
82
|
|
|
80
83
|
```
|
|
81
84
|
|
|
82
85
|
|
|
83
86
|
## Usage
|
|
84
87
|
|
|
85
|
-
Here is a basic example of how to use `nbmorph` to apply
|
|
88
|
+
Here is a basic example of how to use `nbmorph` to apply operations to a 3D labeled image.
|
|
86
89
|
|
|
87
90
|
```
|
|
88
91
|
import numpy as np
|
|
89
92
|
import nbmorph
|
|
93
|
+
import numba
|
|
94
|
+
|
|
95
|
+
numba.set_num_threads(4)
|
|
90
96
|
|
|
91
97
|
# Create a sample 3D labeled image
|
|
92
|
-
# For example, a 5x5x5 cube of
|
|
98
|
+
# For example, a 5x5x5 cube of two different labels in a 10x10x10 volume
|
|
93
99
|
labels = np.zeros((10, 10, 10), dtype=np.uint16)
|
|
94
|
-
labels[2:7, 2:7, 2:
|
|
100
|
+
labels[2:7, 2:7, 2:5] = 1
|
|
101
|
+
labels[2:7, 2:7, 5:7] = 2
|
|
95
102
|
|
|
96
|
-
#
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
print("Smoothing complete. The smoothed labels are in the 'smoothed_labels' array.")
|
|
100
|
-
|
|
101
|
-
```
|
|
103
|
+
# First execution may take a while due to numba compilation
|
|
104
|
+
# Apply morphological erosion with a radius of 1
|
|
105
|
+
eroded_labels = nbmorph.erode_labels_spherical(labels, radius=1)
|
|
102
106
|
|
|
103
|
-
|
|
107
|
+
# Apply morphological dilation with a radius of 1
|
|
108
|
+
dilated_labels = nbmorph.dilate_labels_spherical(labels, radius=1)
|
|
104
109
|
|
|
105
|
-
|
|
110
|
+
# Apply morphological opening with a radius of 1
|
|
111
|
+
opened_labels = nbmorph.open_labels_spherical(labels, radius=1)
|
|
106
112
|
|
|
107
|
-
|
|
113
|
+
# Apply morphological closing with a radius of 1
|
|
114
|
+
closed_labels = nbmorph.close_labels_spherical(labels, radius=1)
|
|
108
115
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
* `nbmorph.close_labels_spherical(labels, radius=1, iterations=1)`
|
|
114
|
-
|
|
115
|
-
* `nbmorph.smooth_labels_spherical(labels, radius=1, iterations=1)`
|
|
116
|
+
# Apply morphological smoothing with a radius of 1
|
|
117
|
+
smoothed_labels = nbmorph.smooth_labels_spherical(labels, radius=1)
|
|
118
|
+
```
|
|
116
119
|
|
|
117
120
|
## Testing
|
|
118
121
|
|
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
import numpy as np
|
|
2
|
-
import numba
|
|
3
|
-
from .diamond_kernel import diamond_loop_padded
|
|
4
|
-
from .box_kernel import kernel3x3x3
|
|
5
|
-
|
|
6
|
-
@numba.njit(cache=True)
|
|
7
|
-
def minimum_box(a, out=None,onlyzero=False):
|
|
8
|
-
return kernel3x3x3(a, opname="min", out=out, onlyzero=onlyzero)
|
|
9
|
-
|
|
10
|
-
@numba.njit(cache=True)
|
|
11
|
-
def maximum_box(a, out=None,onlyzero=False):
|
|
12
|
-
return kernel3x3x3(a, opname="max", out=out, onlyzero=onlyzero)
|
|
13
|
-
|
|
14
|
-
@numba.njit(cache=True)
|
|
15
|
-
def minimum_diamond(data, out=None, onlyzero=False):
|
|
16
|
-
return diamond_loop_padded(data, opname="min", out=out, onlyzero=onlyzero)
|
|
17
|
-
|
|
18
|
-
@numba.njit(cache=True)
|
|
19
|
-
def maximum_diamond(data, out=None, onlyzero=False):
|
|
20
|
-
return diamond_loop_padded(data, opname="max", out=out, onlyzero=onlyzero)
|
|
21
|
-
|
|
22
|
-
|
nbmorph-0.1.0/src/nbmorph/ops.py
DELETED
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
import numba
|
|
2
|
-
|
|
3
|
-
@numba.njit(inline="always")
|
|
4
|
-
def zero_if_not_allequal(nbs):
|
|
5
|
-
if max(nbs) == min(nbs):
|
|
6
|
-
return nbs[0]
|
|
7
|
-
else:
|
|
8
|
-
return 0
|
|
9
|
-
|
|
10
|
-
@numba.njit(inline="always")
|
|
11
|
-
def choose_op(opname, nbs, onlyzero):
|
|
12
|
-
if onlyzero and nbs[0]>0: return nbs[0]
|
|
13
|
-
match opname:
|
|
14
|
-
case "min": return min(nbs)
|
|
15
|
-
case "max": return max(nbs)
|
|
16
|
-
case "zeroedges": return zero_if_not_allequal(nbs)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|