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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nbmorph
3
- Version: 0.1.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
+ [![Tests](https://github.com/MariusCausemann/nbmorph/actions/workflows/test.yaml/badge.svg)](https://github.com/MariusCausemann/nbmorph/actions/workflows/test.yaml)
46
+ [![PyPI version](https://badge.fury.io/py/nbmorph.svg)](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
  ![Effect of Morphological Smoothing](img/smoothing_effect.png)
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 the source directory using pip:
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 morphological smoothing to a 3D labeled image.
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 label 1 in a 10x10x10 volume
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:7] = 1
100
+ labels[2:7, 2:7, 2:5] = 1
101
+ labels[2:7, 2:7, 5:7] = 2
95
102
 
96
- # Apply morphological smoothing with a radius of 1
97
- smoothed_labels = nbmorph.smooth_labels_spherical(labels, radius=1)
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
- ## Operations
107
+ # Apply morphological dilation with a radius of 1
108
+ dilated_labels = nbmorph.dilate_labels_spherical(labels, radius=1)
104
109
 
105
- The core functions of the library are:
110
+ # Apply morphological opening with a radius of 1
111
+ opened_labels = nbmorph.open_labels_spherical(labels, radius=1)
106
112
 
107
- * `nbmorph.dilate_labels_spherical(labels, radius=1)`
113
+ # Apply morphological closing with a radius of 1
114
+ closed_labels = nbmorph.close_labels_spherical(labels, radius=1)
108
115
 
109
- * `nbmorph.erode_labels_spherical(labels, radius=1)`
110
-
111
- * `nbmorph.open_labels_spherical(labels, radius=1, iterations=1)`
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
+ [![Tests](https://github.com/MariusCausemann/nbmorph/actions/workflows/test.yaml/badge.svg)](https://github.com/MariusCausemann/nbmorph/actions/workflows/test.yaml)
4
+ [![PyPI version](https://badge.fury.io/py/nbmorph.svg)](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
  ![Effect of Morphological Smoothing](img/smoothing_effect.png)
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 the source directory using pip:
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 morphological smoothing to a 3D labeled image.
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 label 1 in a 10x10x10 volume
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:7] = 1
58
+ labels[2:7, 2:7, 2:5] = 1
59
+ labels[2:7, 2:7, 5:7] = 2
53
60
 
54
- # Apply morphological smoothing with a radius of 1
55
- smoothed_labels = nbmorph.smooth_labels_spherical(labels, radius=1)
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
- ## Operations
65
+ # Apply morphological dilation with a radius of 1
66
+ dilated_labels = nbmorph.dilate_labels_spherical(labels, radius=1)
62
67
 
63
- The core functions of the library are:
68
+ # Apply morphological opening with a radius of 1
69
+ opened_labels = nbmorph.open_labels_spherical(labels, radius=1)
64
70
 
65
- * `nbmorph.dilate_labels_spherical(labels, radius=1)`
71
+ # Apply morphological closing with a radius of 1
72
+ closed_labels = nbmorph.close_labels_spherical(labels, radius=1)
66
73
 
67
- * `nbmorph.erode_labels_spherical(labels, radius=1)`
68
-
69
- * `nbmorph.open_labels_spherical(labels, radius=1, iterations=1)`
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.
@@ -2,7 +2,7 @@
2
2
 
3
3
  [project]
4
4
  name = "nbmorph"
5
- version = "0.1.0"
5
+ version = "0.2.0"
6
6
  authors = [
7
7
  { name="Marius Causemann", email="mariusca@simula.no" },
8
8
  ]
@@ -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. This is an O(n^2) algorithm,
13
- but fast on small data (len(a) < 50), as needed here.
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
- """Performs a compare-swap on two values."""
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
- From https://bertdobbelaere.github.io/sorting_networks.html
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
- From https://bertdobbelaere.github.io/sorting_networks.html
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
- The neighborhood includes its 6 direct neighbors:
107
- (z, y, x-1), (z, y, x+1), (z, y-1, x),
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 by explicitly
144
- accessing all 26 neighbors.
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
- #@numba.njit
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
- #@numba.njit
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
- out1 = dilate_labels_spherical(out, radius)
110
- #print(f"after dilate: {out1.sum()}")
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
- #@numba.njit
116
- def smooth_labels_spherical(labels: np.ndarray, radius: int, iterations:int=1) -> np.ndarray:
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
- labels = open_labels_spherical(labels, radius)
130
- labels = close_labels_spherical(labels, radius)
131
- return labels
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)
@@ -3,4 +3,4 @@ import numba
3
3
 
4
4
  @numba.njit
5
5
  def cycle(s: str, i:int):
6
- return (s*i)[:i]
6
+ return (s*i)[:i]
@@ -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
- Set the edges of labels to zero.
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.1.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
+ [![Tests](https://github.com/MariusCausemann/nbmorph/actions/workflows/test.yaml/badge.svg)](https://github.com/MariusCausemann/nbmorph/actions/workflows/test.yaml)
46
+ [![PyPI version](https://badge.fury.io/py/nbmorph.svg)](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
  ![Effect of Morphological Smoothing](img/smoothing_effect.png)
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 the source directory using pip:
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 morphological smoothing to a 3D labeled image.
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 label 1 in a 10x10x10 volume
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:7] = 1
100
+ labels[2:7, 2:7, 2:5] = 1
101
+ labels[2:7, 2:7, 5:7] = 2
95
102
 
96
- # Apply morphological smoothing with a radius of 1
97
- smoothed_labels = nbmorph.smooth_labels_spherical(labels, radius=1)
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
- ## Operations
107
+ # Apply morphological dilation with a radius of 1
108
+ dilated_labels = nbmorph.dilate_labels_spherical(labels, radius=1)
104
109
 
105
- The core functions of the library are:
110
+ # Apply morphological opening with a radius of 1
111
+ opened_labels = nbmorph.open_labels_spherical(labels, radius=1)
106
112
 
107
- * `nbmorph.dilate_labels_spherical(labels, radius=1)`
113
+ # Apply morphological closing with a radius of 1
114
+ closed_labels = nbmorph.close_labels_spherical(labels, radius=1)
108
115
 
109
- * `nbmorph.erode_labels_spherical(labels, radius=1)`
110
-
111
- * `nbmorph.open_labels_spherical(labels, radius=1, iterations=1)`
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
-
@@ -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