nbmorph 0.1.0__py3-none-any.whl

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/__init__.py ADDED
@@ -0,0 +1,14 @@
1
+ from .morphology import (
2
+ dilate_labels_spherical,
3
+ erode_labels_spherical,
4
+ open_labels_spherical,
5
+ close_labels_spherical,
6
+ smooth_labels_spherical,
7
+ )
8
+ from .mode import onlyzero_mode_box,onlyzero_mode_diamond, fast_mode
9
+ from .minmax import minimum_box, maximum_box, minimum_diamond, maximum_diamond
10
+ from .zero_edges import zero_label_edges_box, zero_label_edges_diamond
11
+ from .utils import cycle
12
+
13
+ # Define the package version
14
+ __version__ = "0.1.0"
nbmorph/box_kernel.py ADDED
@@ -0,0 +1,434 @@
1
+
2
+ import numba
3
+ import numpy as np
4
+ from .ops import choose_op
5
+
6
+
7
+ @numba.njit(inline="always")
8
+ def min18(v0, v1, v2, v3, v4, v5, v6, v7, v8, v9,
9
+ v10, v11, v12, v13, v14, v15, v16, v17):
10
+ return min(
11
+ min((v0, v1, v2, v3, v4, v5, v6, v7, v8)),
12
+ min((v9, v10, v11, v12, v13, v14, v15, v16, v17))
13
+ )
14
+
15
+ @numba.njit(inline="always")
16
+ def min27(v0, v1, v2, v3, v4, v5, v6, v7, v8, v9,
17
+ v10, v11, v12, v13, v14, v15, v16, v17,
18
+ v18, v19, v20, v21, v22, v23, v24, v25, v26):
19
+ return min((
20
+ min((v0, v1, v2, v3, v4, v5, v6, v7, v8)),
21
+ min((v9, v10, v11, v12, v13, v14, v15, v16, v17)),
22
+ max((v18, v19, v20, v21, v22, v23, v24, v25, v26))
23
+ ))
24
+
25
+ @numba.njit(inline="always")
26
+ def max18(v0, v1, v2, v3, v4, v5, v6, v7, v8, v9,
27
+ v10, v11, v12, v13, v14, v15, v16, v17):
28
+ return max(
29
+ max((v0, v1, v2, v3, v4, v5, v6, v7, v8)),
30
+ max((v9, v10, v11, v12, v13, v14, v15, v16, v17))
31
+ )
32
+
33
+ @numba.njit(inline="always")
34
+ def max27(v0, v1, v2, v3, v4, v5, v6, v7, v8, v9,
35
+ v10, v11, v12, v13, v14, v15, v16, v17,
36
+ v18, v19, v20, v21, v22, v23, v24, v25, v26):
37
+ return max((
38
+ max((v0, v1, v2, v3, v4, v5, v6, v7, v8)),
39
+ max((v9, v10, v11, v12, v13, v14, v15, v16, v17)),
40
+ max((v18, v19, v20, v21, v22, v23, v24, v25, v26))
41
+ ))
42
+
43
+ @numba.njit(inline="always")
44
+ def choose_op18(opname,
45
+ v0, v1, v2, v3, v4, v5, v6, v7, v8, v9,
46
+ v10, v11, v12, v13, v14, v15, v16, v17,
47
+ onlyzero):
48
+ if onlyzero and v0 >0: return v0
49
+ match opname:
50
+ case "min":
51
+ return min18(v0, v1, v2, v3, v4, v5, v6, v7, v8,
52
+ v9, v10, v11, v12, v13, v14, v15, v16, v17)
53
+ case "max":
54
+ return max18(v0, v1, v2, v3, v4, v5, v6, v7, v8,
55
+ v9, v10, v11, v12, v13, v14, v15, v16, v17)
56
+ case "zeroedges":
57
+ return v0 if min18(v0, v1, v2, v3, v4, v5, v6, v7, v8,
58
+ v9, v10, v11, v12, v13, v14, v15, v16, v17) == max18(v0, v1, v2, v3, v4, v5, v6, v7, v8,
59
+ v9, v10, v11, v12, v13, v14, v15, v16, v17) else 0
60
+
61
+ @numba.njit(inline="always")
62
+ def choose_op27(opname,
63
+ v0, v1, v2, v3, v4, v5, v6, v7, v8, v9,
64
+ v10, v11, v12, v13, v14, v15, v16, v17,v18,
65
+ v19, v20, v21, v22, v23, v24, v25, v26,
66
+ onlyzero=False):
67
+ if onlyzero and v0 >0: return v0
68
+ match opname:
69
+ case "min":
70
+ return min27(v0, v1, v2, v3, v4, v5, v6, v7, v8,
71
+ v9, v10, v11, v12, v13, v14, v15, v16, v17, v18,
72
+ v19, v20, v21, v22, v23, v24, v25, v26)
73
+ case "max":
74
+ return max27(v0, v1, v2, v3, v4, v5, v6, v7, v8,
75
+ v9, v10, v11, v12, v13, v14, v15, v16, v17, v18,
76
+ v19, v20, v21, v22, v23, v24, v25, v26)
77
+ case "zeroedges":
78
+ return v0 if min27(v0, v1, v2, v3, v4, v5, v6, v7, v8,
79
+ v9, v10, v11, v12, v13, v14, v15, v16, v17, v18,
80
+ v19, v20, v21, v22, v23, v24, v25, v26) == max27(v0, v1, v2, v3, v4, v5, v6, v7, v8,
81
+ v9, v10, v11, v12, v13, v14, v15, v16, v17, v18,
82
+ v19, v20, v21, v22, v23, v24, v25, v26) else 0
83
+
84
+
85
+ @numba.njit
86
+ def borders3x3x3(data, opname, out, onlyzero=False):
87
+ sz, sy, sx = data.shape
88
+
89
+ # --- Front Face (z=0) ---
90
+ z = 0
91
+ for y in range(1, sy - 1):
92
+ for x in range(1, sx - 1):
93
+ out[z, y, x] = choose_op18(opname, data[z, y, x],
94
+ # --- Middle Slice (z) ---
95
+ data[z, y - 1, x - 1], data[z, y - 1, x], data[z, y - 1, x + 1],
96
+ data[z, y, x - 1], data[z, y, x + 1],
97
+ data[z, y + 1, x - 1], data[z, y + 1, x], data[z, y + 1, x + 1],
98
+
99
+ # --- Bottom Slice (z+1) ---
100
+ data[z + 1, y - 1, x - 1], data[z + 1, y - 1, x], data[z + 1, y - 1, x + 1],
101
+ data[z + 1, y, x - 1], data[z + 1, y, x], data[z + 1, y, x + 1],
102
+ data[z + 1, y + 1, x - 1], data[z + 1, y + 1, x], data[z + 1, y + 1, x + 1],
103
+ onlyzero=onlyzero)
104
+
105
+ # --- Back Face (z=sz-1) ---
106
+ z = sz - 1
107
+ for y in range(1, sy - 1):
108
+ for x in range(1, sx - 1):
109
+ out[z, y, x] = choose_op18(opname,data[z, y, x],
110
+ # --- Top Slice (z-1) ---
111
+ data[z - 1, y - 1, x - 1], data[z - 1, y - 1, x], data[z - 1, y - 1, x + 1],
112
+ data[z - 1, y, x - 1], data[z - 1, y, x], data[z - 1, y, x + 1],
113
+ data[z - 1, y + 1, x - 1], data[z - 1, y + 1, x], data[z - 1, y + 1, x + 1],
114
+
115
+ # --- Middle Slice (z) ---
116
+ data[z, y - 1, x - 1], data[z, y - 1, x], data[z, y - 1, x + 1],
117
+ data[z, y, x - 1], data[z, y, x + 1],
118
+ data[z, y + 1, x - 1], data[z, y + 1, x], data[z, y + 1, x + 1],
119
+ onlyzero=onlyzero)
120
+
121
+ # --- Top Face (y=0) ---
122
+ y = 0
123
+ for z in range(1, sz - 1):
124
+ for x in range(1, sx - 1):
125
+ out[z, y, x] = choose_op18(opname, data[z, y, x],
126
+ # --- Top Slice (z-1) ---
127
+ data[z - 1, y, x - 1], data[z - 1, y, x], data[z - 1, y, x + 1],
128
+ data[z - 1, y + 1, x - 1], data[z - 1, y + 1, x], data[z - 1, y + 1, x + 1],
129
+
130
+ # --- Middle Slice (z) ---
131
+ data[z, y, x - 1], data[z, y, x + 1],
132
+ data[z, y + 1, x - 1], data[z, y + 1, x], data[z, y + 1, x + 1],
133
+
134
+ # --- Bottom Slice (z+1) ---
135
+ data[z + 1, y, x - 1], data[z + 1, y, x], data[z + 1, y, x + 1],
136
+ data[z + 1, y + 1, x - 1], data[z + 1, y + 1, x], data[z + 1, y + 1, x + 1],
137
+ onlyzero=onlyzero)
138
+
139
+ # --- Bottom Face (y=sy-1) ---
140
+ y = sy - 1
141
+ for z in range(1, sz - 1):
142
+ for x in range(1, sx - 1):
143
+ out[z, y, x] = choose_op18(opname,data[z, y, x],
144
+ # --- Top Slice (z-1) ---
145
+ data[z - 1, y - 1, x - 1], data[z - 1, y - 1, x], data[z - 1, y - 1, x + 1],
146
+ data[z - 1, y, x - 1], data[z - 1, y, x], data[z - 1, y, x + 1],
147
+
148
+ # --- Middle Slice (z) ---
149
+ data[z, y - 1, x - 1], data[z, y - 1, x], data[z, y - 1, x + 1],
150
+ data[z, y, x - 1], data[z, y, x + 1],
151
+
152
+ # --- Bottom Slice (z+1) ---
153
+ data[z + 1, y - 1, x - 1], data[z + 1, y - 1, x], data[z + 1, y - 1, x + 1],
154
+ data[z + 1, y, x - 1], data[z + 1, y, x], data[z + 1, y, x + 1],
155
+ onlyzero=onlyzero)
156
+
157
+ # --- Left Face (x=0) ---
158
+ x = 0
159
+ for z in range(1, sz - 1):
160
+ for y in range(1, sy - 1):
161
+ out[z, y, x] = choose_op18(opname, data[z, y, x],
162
+ # --- Top Slice (z-1) ---
163
+ data[z - 1, y - 1, x], data[z - 1, y - 1, x + 1],
164
+ data[z - 1, y, x], data[z - 1, y, x + 1],
165
+ data[z - 1, y + 1, x], data[z - 1, y + 1, x + 1],
166
+
167
+ # --- Middle Slice (z) ---
168
+ data[z, y - 1, x], data[z, y - 1, x + 1],
169
+ data[z, y, x + 1],
170
+ data[z, y + 1, x], data[z, y + 1, x + 1],
171
+
172
+ # --- Bottom Slice (z+1) ---
173
+ data[z + 1, y - 1, x], data[z + 1, y - 1, x + 1],
174
+ data[z + 1, y, x], data[z + 1, y, x + 1],
175
+ data[z + 1, y + 1, x], data[z + 1, y + 1, x + 1],
176
+ onlyzero=onlyzero)
177
+
178
+ # --- Right Face (x=sx-1) ---
179
+ x = sx - 1
180
+ for z in range(1, sz - 1):
181
+ for y in range(1, sy - 1):
182
+ out[z, y, x] = choose_op18(opname, data[z, y, x],
183
+ # --- Top Slice (z-1) ---
184
+ data[z - 1, y - 1, x - 1], data[z - 1, y - 1, x],
185
+ data[z - 1, y, x - 1], data[z - 1, y, x],
186
+ data[z - 1, y + 1, x - 1], data[z - 1, y + 1, x],
187
+
188
+ # --- Middle Slice (z) ---
189
+ data[z, y - 1, x - 1], data[z, y - 1, x],
190
+ data[z, y, x - 1],
191
+ data[z, y + 1, x - 1], data[z, y + 1, x],
192
+
193
+ # --- Bottom Slice (z+1) ---
194
+ data[z + 1, y - 1, x - 1], data[z + 1, y - 1, x],
195
+ data[z + 1, y, x - 1], data[z + 1, y, x],
196
+ data[z + 1, y + 1, x - 1], data[z + 1, y + 1, x],
197
+ onlyzero=onlyzero)
198
+
199
+ # --- Edges ---
200
+ # Front-Top Edge (z=0, y=0)
201
+ z, y = 0, 0
202
+ for x in range(1, sx - 1):
203
+ out[z, y, x] = choose_op(opname, (data[z, y, x],
204
+ data[z, y, x - 1], data[z, y, x + 1],
205
+ data[z, y + 1, x - 1], data[z, y + 1, x], data[z, y + 1, x + 1],
206
+ data[z + 1, y, x - 1], data[z + 1, y, x], data[z + 1, y, x + 1],
207
+ data[z + 1, y + 1, x - 1], data[z + 1, y + 1, x], data[z + 1, y + 1, x + 1],
208
+ ), onlyzero=onlyzero)
209
+
210
+ # Front-Bottom Edge (z=0, y=sy-1)
211
+ z, y = 0, sy - 1
212
+ for x in range(1, sx - 1):
213
+ out[z, y, x] = choose_op(opname, ( data[z, y, x],
214
+ data[z, y - 1, x - 1], data[z, y - 1, x], data[z, y - 1, x + 1],
215
+ data[z, y, x - 1], data[z, y, x + 1],
216
+ data[z + 1, y - 1, x - 1], data[z + 1, y - 1, x], data[z + 1, y - 1, x + 1],
217
+ data[z + 1, y, x - 1], data[z + 1, y, x], data[z + 1, y, x + 1],
218
+ ), onlyzero=onlyzero)
219
+
220
+ # Back-Top Edge (z=sz-1, y=0)
221
+ z, y = sz - 1, 0
222
+ for x in range(1, sx - 1):
223
+ out[z, y, x] = choose_op(opname, (data[z, y, x],
224
+ data[z - 1, y, x - 1], data[z - 1, y, x], data[z - 1, y, x + 1],
225
+ data[z - 1, y + 1, x - 1], data[z - 1, y + 1, x], data[z - 1, y + 1, x + 1],
226
+ data[z, y, x - 1], data[z, y, x + 1],
227
+ data[z, y + 1, x - 1], data[z, y + 1, x], data[z, y + 1, x + 1],
228
+ ), onlyzero=onlyzero)
229
+
230
+ # Back-Bottom Edge (z=sz-1, y=sy-1)
231
+ z, y = sz - 1, sy - 1
232
+ for x in range(1, sx - 1):
233
+ out[z, y, x] = choose_op(opname, (data[z, y, x],
234
+ data[z - 1, y - 1, x - 1], data[z - 1, y - 1, x], data[z - 1, y - 1, x + 1],
235
+ data[z - 1, y, x - 1], data[z - 1, y, x], data[z - 1, y, x + 1],
236
+ data[z, y - 1, x - 1], data[z, y - 1, x], data[z, y - 1, x + 1],
237
+ data[z, y, x - 1], data[z, y, x + 1],
238
+ ), onlyzero=onlyzero)
239
+
240
+ # Front-Left Edge (z=0, x=0)
241
+ z, x = 0, 0
242
+ for y in range(1, sy - 1):
243
+ out[z, y, x] = choose_op(opname, (data[z, y, x],
244
+ data[z, y - 1, x], data[z, y - 1, x + 1],
245
+ data[z, y, x + 1],
246
+ data[z, y + 1, x], data[z, y + 1, x + 1],
247
+ data[z + 1, y - 1, x], data[z + 1, y - 1, x + 1],
248
+ data[z + 1, y, x], data[z + 1, y, x + 1],
249
+ data[z + 1, y + 1, x], data[z + 1, y + 1, x + 1],
250
+ ), onlyzero=onlyzero)
251
+
252
+ # Front-Right Edge (z=0, x=sx-1)
253
+ z, x = 0, sx - 1
254
+ for y in range(1, sy - 1):
255
+ out[z, y, x] = choose_op(opname, (data[z, y, x],
256
+ data[z, y - 1, x - 1], data[z, y - 1, x],
257
+ data[z, y, x - 1],
258
+ data[z, y + 1, x - 1], data[z, y + 1, x],
259
+ data[z + 1, y - 1, x - 1], data[z + 1, y - 1, x],
260
+ data[z + 1, y, x - 1], data[z + 1, y, x],
261
+ data[z + 1, y + 1, x - 1], data[z + 1, y + 1, x],
262
+ ), onlyzero=onlyzero)
263
+
264
+ # Back-Left Edge (z=sz-1, x=0)
265
+ z, x = sz - 1, 0
266
+ for y in range(1, sy - 1):
267
+ out[z, y, x] = choose_op(opname, (data[z, y, x],
268
+ data[z - 1, y - 1, x], data[z - 1, y - 1, x + 1],
269
+ data[z - 1, y, x], data[z - 1, y, x + 1],
270
+ data[z - 1, y + 1, x], data[z - 1, y + 1, x + 1],
271
+ data[z, y - 1, x], data[z, y - 1, x + 1],
272
+ data[z, y, x + 1],
273
+ data[z, y + 1, x], data[z, y + 1, x + 1],
274
+ ), onlyzero=onlyzero)
275
+
276
+ # Back-Right Edge (z=sz-1, x=sx-1)
277
+ z, x = sz - 1, sx - 1
278
+ for y in range(1, sy - 1):
279
+ out[z, y, x] = choose_op(opname, (data[z, y, x],
280
+ data[z - 1, y - 1, x - 1], data[z - 1, y - 1, x],
281
+ data[z - 1, y, x - 1], data[z - 1, y, x],
282
+ data[z - 1, y + 1, x - 1], data[z - 1, y + 1, x],
283
+ data[z, y - 1, x - 1], data[z, y - 1, x],
284
+ data[z, y, x - 1],
285
+ data[z, y + 1, x - 1], data[z, y + 1, x],
286
+ ), onlyzero=onlyzero)
287
+
288
+ # Top-Left Edge (y=0, x=0)
289
+ y, x = 0, 0
290
+ for z in range(1, sz - 1):
291
+ out[z, y, x] = choose_op(opname, (data[z, y, x],
292
+ data[z - 1, y, x], data[z - 1, y, x + 1],
293
+ data[z - 1, y + 1, x], data[z - 1, y + 1, x + 1],
294
+ data[z, y, x + 1],
295
+ data[z, y + 1, x], data[z, y + 1, x + 1],
296
+ data[z + 1, y, x], data[z + 1, y, x + 1],
297
+ data[z + 1, y + 1, x], data[z + 1, y + 1, x + 1],
298
+ ), onlyzero=onlyzero)
299
+
300
+ # Top-Right Edge (y=0, x=sx-1)
301
+ y, x = 0, sx - 1
302
+ for z in range(1, sz - 1):
303
+ out[z, y, x] = choose_op(opname, (data[z, y, x],
304
+ data[z - 1, y, x - 1], data[z - 1, y, x],
305
+ data[z - 1, y + 1, x - 1], data[z - 1, y + 1, x],
306
+ data[z, y, x - 1],
307
+ data[z, y + 1, x - 1], data[z, y + 1, x],
308
+ data[z + 1, y, x - 1], data[z + 1, y, x],
309
+ data[z + 1, y + 1, x - 1], data[z + 1, y + 1, x],
310
+ ), onlyzero=onlyzero)
311
+
312
+ # Bottom-Left Edge (y=sy-1, x=0)
313
+ y, x = sy - 1, 0
314
+ for z in range(1, sz - 1):
315
+ out[z, y, x] = choose_op(opname, (data[z, y, x],
316
+ data[z - 1, y - 1, x], data[z - 1, y - 1, x + 1],
317
+ data[z - 1, y, x], data[z - 1, y, x + 1],
318
+ data[z, y - 1, x], data[z, y - 1, x + 1],
319
+ data[z, y, x + 1],
320
+ data[z + 1, y - 1, x], data[z + 1, y - 1, x + 1],
321
+ data[z + 1, y, x], data[z + 1, y, x + 1],
322
+ ), onlyzero=onlyzero)
323
+
324
+ # Bottom-Right Edge (y=sy-1, x=sx-1)
325
+ y, x = sy - 1, sx - 1
326
+ for z in range(1, sz - 1):
327
+ out[z, y, x] = choose_op(opname, (data[z, y, x],
328
+ data[z - 1, y - 1, x - 1], data[z - 1, y - 1, x],
329
+ data[z - 1, y, x - 1], data[z - 1, y, x],
330
+ data[z, y - 1, x - 1], data[z, y - 1, x],
331
+ data[z, y, x - 1],
332
+ data[z + 1, y - 1, x - 1], data[z + 1, y - 1, x],
333
+ data[z + 1, y, x - 1], data[z + 1, y, x],
334
+ ), onlyzero=onlyzero)
335
+
336
+ # --- Corners ---
337
+ # Front-Top-Left Corner (z=0, y=0, x=0)
338
+ out[0, 0, 0] = choose_op(opname, (data[0, 0, 0],
339
+ data[0, 0, 1],
340
+ data[0, 1, 0], data[0, 1, 1],
341
+ data[1, 0, 0], data[1, 0, 1],
342
+ data[1, 1, 0], data[1, 1, 1],
343
+ ), onlyzero=onlyzero)
344
+
345
+ # Front-Top-Right Corner (z=0, y=0, x=sx-1)
346
+ out[0, 0, sx - 1] = choose_op(opname, (data[0, 0, sx - 1],
347
+ data[0, 0, sx - 2],
348
+ data[0, 1, sx - 2], data[0, 1, sx - 1],
349
+ data[1, 0, sx - 2], data[1, 0, sx - 1],
350
+ data[1, 1, sx - 2], data[1, 1, sx - 1],
351
+ ), onlyzero=onlyzero)
352
+
353
+ # Front-Bottom-Left Corner (z=0, y=sy-1, x=0)
354
+ out[0, sy - 1, 0] = choose_op(opname, (data[0, sy - 1, 0],
355
+ data[0, sy - 2, 0], data[0, sy - 2, 1],
356
+ data[0, sy - 1, 1],
357
+ data[1, sy - 2, 0], data[1, sy - 2, 1],
358
+ data[1, sy - 1, 0], data[1, sy - 1, 1],
359
+ ), onlyzero=onlyzero)
360
+
361
+ # Front-Bottom-Right Corner (z=0, y=sy-1, x=sx-1)
362
+ out[0, sy - 1, sx - 1] = choose_op(opname, (data[0, sy - 1, sx - 1],
363
+ data[0, sy - 2, sx - 2], data[0, sy - 2, sx - 1],
364
+ data[0, sy - 1, sx - 2],
365
+ data[1, sy - 2, sx - 2], data[1, sy - 2, sx - 1],
366
+ data[1, sy - 1, sx - 2], data[1, sy - 1, sx - 1],
367
+ ), onlyzero=onlyzero)
368
+
369
+ # Back-Top-Left Corner (z=sz-1, y=0, x=0)
370
+ out[sz - 1, 0, 0] = choose_op(opname, (data[sz - 1, 0, 0],
371
+ data[sz - 2, 0, 0], data[sz - 2, 0, 1],
372
+ data[sz - 2, 1, 0], data[sz - 2, 1, 1],
373
+ data[sz - 1, 0, 1],
374
+ data[sz - 1, 1, 0], data[sz - 1, 1, 1],
375
+ ), onlyzero=onlyzero)
376
+
377
+ # Back-Top-Right Corner (z=sz-1, y=0, x=sx-1)
378
+ out[sz - 1, 0, sx - 1] = choose_op(opname, (data[sz - 1, 0, sx - 1],
379
+ data[sz - 2, 0, sx - 2], data[sz - 2, 0, sx - 1],
380
+ data[sz - 2, 1, sx - 2], data[sz - 2, 1, sx - 1],
381
+ data[sz - 1, 0, sx - 2],
382
+ data[sz - 1, 1, sx - 2], data[sz - 1, 1, sx - 1],
383
+ ), onlyzero=onlyzero)
384
+
385
+ # Back-Bottom-Left Corner (z=sz-1, y=sy-1, x=0)
386
+ out[sz - 1, sy - 1, 0] = choose_op(opname, (data[sz - 1, sy - 1, 0],
387
+ data[sz - 2, sy - 2, 0], data[sz - 2, sy - 2, 1],
388
+ data[sz - 2, sy - 1, 0], data[sz - 2, sy - 1, 1],
389
+ data[sz - 1, sy - 2, 0], data[sz - 1, sy - 2, 1],
390
+ data[sz - 1, sy - 1, 1],
391
+ ), onlyzero=onlyzero)
392
+
393
+ # Back-Bottom-Right Corner (z=sz-1, y=sy-1, x=sx-1)
394
+ out[sz - 1, sy - 1, sx - 1] = choose_op(opname, (data[sz - 1, sy - 1, sx - 1],
395
+ data[sz - 2, sy - 2, sx - 2], data[sz - 2, sy - 2, sx - 1],
396
+ data[sz - 2, sy - 1, sx - 2], data[sz - 2, sy - 1, sx - 1],
397
+ data[sz - 1, sy - 2, sx - 2], data[sz - 1, sy - 2, sx - 1],
398
+ data[sz - 1, sy - 1, sx - 2],
399
+ ), onlyzero=onlyzero)
400
+
401
+
402
+ @numba.njit(inline="always")
403
+ def op3x3x3(data, z, y, x, opname, onlyzero):
404
+ return choose_op27(opname,
405
+ # --- Top Slice (z-1) ---
406
+ data[z-1, y-1, x-1], data[z-1, y-1, x], data[z-1, y-1, x+1],
407
+ data[z-1, y, x-1], data[z-1, y, x], data[z-1, y, x+1],
408
+ data[z-1, y+1, x-1], data[z-1, y+1, x], data[z-1, y+1, x+1],
409
+
410
+ # --- Middle Slice (z) ---
411
+ data[z, y-1, x-1], data[z, y-1, x], data[z, y-1, x+1],
412
+ data[z, y, x-1], data[z, y, x] , data[z, y, x+1],
413
+ data[z, y+1, x-1], data[z, y+1, x], data[z, y+1, x+1],
414
+
415
+ # --- Bottom Slice (z+1) ---
416
+ data[z+1, y-1, x-1], data[z+1, y-1, x], data[z+1, y-1, x+1],
417
+ data[z+1, y, x-1], data[z+1, y, x], data[z+1, y, x+1],
418
+ data[z+1, y+1, x-1], data[z+1, y+1, x], data[z+1, y+1, x+1],
419
+ onlyzero=onlyzero
420
+ )
421
+
422
+
423
+ @numba.njit(parallel=True, cache=False)
424
+ def kernel3x3x3(data, opname, out=None, onlyzero=False):
425
+ sz, sy, sx = data.shape
426
+ if out is None:
427
+ out = np.zeros_like(data)
428
+ assert data.shape == out.shape
429
+ for z in numba.prange(1, sz-1):
430
+ for y in range(1, sy-1):
431
+ for x in range(1, sx-1):
432
+ out[z,y,x] = op3x3x3(data, z,y,x, opname, onlyzero)
433
+ borders3x3x3(data, opname, out, onlyzero=onlyzero)
434
+ return out
@@ -0,0 +1,44 @@
1
+ import numba
2
+ import numpy as np
3
+ from .ops import choose_op
4
+
5
+
6
+ @numba.njit(inline="always")
7
+ def clamp(index, size):
8
+ return max(0,index-1), min(index+1, size-1)
9
+
10
+ @numba.njit(parallel=True, cache=True)
11
+ def diamond_loop_padded(data, opname:str, out=None, onlyzero=False):
12
+ if out is None:
13
+ out = np.empty_like(data)
14
+ assert data.shape == out.shape
15
+ sz, sy, sx = data.shape
16
+ for z in numba.prange(sz):
17
+ zl, zr = clamp(z, sz)
18
+ for y in range(sy):
19
+ yl, yr = clamp(y, sy)
20
+ # first x slice:
21
+ x = 0
22
+ out[z,y,x] = choose_op(opname, (data[z,y,x],
23
+ data[zl,y,x], data[zr,y,x],
24
+ data[z,yl,x], data[z,yr,x],
25
+ data[z,y, min(x+1, sx-1)], data[z,y, x]),
26
+ onlyzero=onlyzero)
27
+ # middle slices
28
+ for x in range(1, sx-1):
29
+ out[z,y,x] = choose_op(opname, (data[z,y,x],
30
+ data[zl,y,x], data[zr,y,x],
31
+ data[z,yl,x], data[z,yr,x],
32
+ data[z,y,x-1], data[z,y,x+1]),
33
+ onlyzero=onlyzero)
34
+
35
+ #last slice, if more than 1 slice
36
+ if sx > 1:
37
+ x = sx -1
38
+ out[z,y,x] = choose_op(opname, (data[z,y,x],
39
+ data[zl,y,x], data[zr,y,x],
40
+ data[z,yl,x], data[z,yr,x],
41
+ data[z,y, x -1], data[z,y, x]),
42
+ onlyzero=onlyzero)
43
+
44
+ return out
nbmorph/minmax.py ADDED
@@ -0,0 +1,22 @@
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/mode.py ADDED
@@ -0,0 +1,389 @@
1
+ import numba
2
+ import numpy as np
3
+ from .minmax import maximum_box, maximum_diamond
4
+
5
+ @numba.njit
6
+ def fast_mode(a):
7
+ return fast_modeN(a, len(a))
8
+
9
+ @numba.njit(inline="always")
10
+ def fast_modeN(a, N):
11
+ """
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.
14
+
15
+ Args:
16
+ a (np.ndarray): The input 1D array.
17
+
18
+ Returns:
19
+ The mode of the array.
20
+ """
21
+ max_count = 0
22
+ current_count = 0
23
+ mode = a[0]
24
+ for i in range(N):
25
+ #if a[i] == mode: continue
26
+ current_count = 0
27
+ for j in range(N):
28
+ current_count += (a[i] == a[j])
29
+
30
+ if current_count > N/2:
31
+ return a[i]
32
+
33
+ elif current_count > max_count:
34
+ mode = a[i]
35
+ max_count = current_count
36
+
37
+ elif current_count == max_count and a[i] < mode:
38
+ mode = a[i]
39
+ return mode
40
+
41
+ @numba.njit(inline="always")
42
+ def _cs(a, b):
43
+ """Performs a compare-swap on two values."""
44
+ if a > b:
45
+ return b, a
46
+ else:
47
+ return a, b
48
+
49
+
50
+ @numba.njit(inline="always")
51
+ def sort26_network(
52
+ v0, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13,
53
+ v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25
54
+ ):
55
+ """
56
+ Sorts 26 elements using a pre-defined sorting network.
57
+ From https://bertdobbelaere.github.io/sorting_networks.html
58
+ """
59
+ 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
+ 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)
61
+ v0, v4 = _cs(v0, v4); v1, v6 = _cs(v1, v6); v2, v5 = _cs(v2, v5); v3, v7 = _cs(v3, v7); v8, v14 = _cs(v8, v14); v9, v16 = _cs(v9, v16); v10, v15 = _cs(v10, v15); v11, v17 = _cs(v11, v17); v18, v22 = _cs(v18, v22); v19, v24 = _cs(v19, v24); v20, v23 = _cs(v20, v23); v21, v25 = _cs(v21, v25)
62
+ v0, v18 = _cs(v0, v18); v1, v19 = _cs(v1, v19); v2, v20 = _cs(v2, v20); v3, v21 = _cs(v3, v21); v4, v22 = _cs(v4, v22); v5, v23 = _cs(v5, v23); v6, v24 = _cs(v6, v24); v7, v25 = _cs(v7, v25); v9, v12 = _cs(v9, v12); v13, v16 = _cs(v13, v16)
63
+ v3, v11 = _cs(v3, v11); v8, v9 = _cs(v8, v9); v10, v13 = _cs(v10, v13); v12, v15 = _cs(v12, v15); v14, v22 = _cs(v14, v22); v16, v17 = _cs(v16, v17)
64
+ v0, v8 = _cs(v0, v8); v1, v9 = _cs(v1, v9); v2, v14 = _cs(v2, v14); v6, v12 = _cs(v6, v12); v7, v15 = _cs(v7, v15); v10, v18 = _cs(v10, v18); v11, v23 = _cs(v11, v23); v13, v19 = _cs(v13, v19); v16, v24 = _cs(v16, v24); v17, v25 = _cs(v17, v25)
65
+ v1, v2 = _cs(v1, v2); v3, v18 = _cs(v3, v18); v4, v8 = _cs(v4, v8); v7, v22 = _cs(v7, v22); v17, v21 = _cs(v17, v21); v23, v24 = _cs(v23, v24)
66
+ v3, v14 = _cs(v3, v14); v4, v10 = _cs(v4, v10); v5, v18 = _cs(v5, v18); v7, v20 = _cs(v7, v20); v8, v13 = _cs(v8, v13); v11, v22 = _cs(v11, v22); v12, v17 = _cs(v12, v17); v15, v21 = _cs(v15, v21)
67
+ v1, v4 = _cs(v1, v4); v5, v6 = _cs(v5, v6); v7, v9 = _cs(v7, v9); v8, v10 = _cs(v8, v10); v15, v17 = _cs(v15, v17); v16, v18 = _cs(v16, v18); v19, v20 = _cs(v19, v20); v21, v24 = _cs(v21, v24)
68
+ v2, v5 = _cs(v2, v5); v3, v10 = _cs(v3, v10); v6, v14 = _cs(v6, v14); v9, v13 = _cs(v9, v13); v11, v19 = _cs(v11, v19); v12, v16 = _cs(v12, v16); v15, v22 = _cs(v15, v22); v20, v23 = _cs(v20, v23)
69
+ v2, v8 = _cs(v2, v8); v5, v7 = _cs(v5, v7); v6, v9 = _cs(v6, v9); v11, v12 = _cs(v11, v12); v13, v14 = _cs(v13, v14); v16, v19 = _cs(v16, v19); v17, v23 = _cs(v17, v23); v18, v20 = _cs(v18, v20)
70
+ v2, v4 = _cs(v2, v4); v3, v5 = _cs(v3, v5); v6, v11 = _cs(v6, v11); v7, v10 = _cs(v7, v10); v9, v16 = _cs(v9, v16); v12, v13 = _cs(v12, v13); v14, v19 = _cs(v14, v19); v15, v18 = _cs(v15, v18); v20, v22 = _cs(v20, v22); v21, v23 = _cs(v21, v23)
71
+ v3, v4 = _cs(v3, v4); v5, v8 = _cs(v5, v8); v6, v7 = _cs(v6, v7); v9, v11 = _cs(v9, v11); v10, v12 = _cs(v10, v12); v13, v15 = _cs(v13, v15); v14, v16 = _cs(v14, v16); v17, v20 = _cs(v17, v20); v18, v19 = _cs(v18, v19); v21, v22 = _cs(v21, v22)
72
+ v5, v6 = _cs(v5, v6); v7, v8 = _cs(v7, v8); v9, v10 = _cs(v9, v10); v11, v12 = _cs(v11, v12); v13, v14 = _cs(v13, v14); v15, v16 = _cs(v15, v16); v17, v18 = _cs(v17, v18); v19, v20 = _cs(v19, v20)
73
+ 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)
74
+
75
+ return v0, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25
76
+
77
+ @numba.njit(inline="always")
78
+ def sort6_network(v0, v1, v2, v3, v4, v5):
79
+ """
80
+ Sorts 6 elements using a pre-defined sorting network.
81
+ From https://bertdobbelaere.github.io/sorting_networks.html
82
+ """
83
+ v0, v5 = _cs(v0, v5)
84
+ v1, v3 = _cs(v1, v3)
85
+ v2, v4 = _cs(v2, v4)
86
+
87
+ v1, v2 = _cs(v1, v2)
88
+ v3, v4 = _cs(v3, v4)
89
+
90
+ v0, v3 = _cs(v0, v3)
91
+ v2, v5 = _cs(v2, v5)
92
+
93
+ v0, v1 = _cs(v0, v1)
94
+ v2, v3 = _cs(v2, v3)
95
+ v4, v5 = _cs(v4, v5)
96
+
97
+ v1, v2 = _cs(v1, v2)
98
+ v3, v4 = _cs(v3, v4)
99
+
100
+ return v0, v1, v2, v3, v4, v5
101
+
102
+ @numba.njit(inline="always")
103
+ def mode_diamond(data, z, y, x):
104
+ """
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
+ """
110
+
111
+ (v0, v1, v2, v3, v4, v5) = sort6_network(
112
+ data[z, y, x-1], data[z, y, x+1],
113
+ data[z, y-1, x], data[z, y+1, x],
114
+ data[z-1, y, x], data[z+1, y, x]
115
+ )
116
+
117
+ one = np.uint8(1)
118
+ l0 = one
119
+ l1 = (l0 + one) if v1 == v0 and v1 > 0 else one
120
+ l2 = (l1 + one) if v2 == v1 and v2 > 0 else one
121
+ l3 = (l2 + one) if v3 == v2 and v3 > 0 else one
122
+ l4 = (l3 + one) if v4 == v3 and v4 > 0 else one
123
+ l5 = (l4 + one) if v5 == v4 and v5 > 0 else one
124
+
125
+ def _update_max(len1, val1, len2, val2):
126
+ if len2 >= len1:
127
+ return len2, val2
128
+ return len1, val1
129
+
130
+ (l_max, v_mode) = _update_max(l0, v0, l1, v1)
131
+ (l_max, v_mode) = _update_max(l_max, v_mode, l2, v2)
132
+ (l_max, v_mode) = _update_max(l_max, v_mode, l3, v3)
133
+ (l_max, v_mode) = _update_max(l_max, v_mode, l4, v4)
134
+ (l_max, v_mode) = _update_max(l_max, v_mode, l5, v5)
135
+
136
+ #print(v_mode)
137
+ return v_mode
138
+
139
+
140
+ @numba.njit(inline="always")
141
+ def mode_box(data, z, y, x):
142
+ """
143
+ Calculates the mode of a 3x3x3 neighborhood by explicitly
144
+ accessing all 26 neighbors.
145
+ """
146
+
147
+ (v0, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11,
148
+ v12, v13, v14, v15, v16, v17, v18, v19, v20,
149
+ v21, v22, v23, v24, v25 ) = sort26_network(
150
+ # --- Top Slice (z-1) ---
151
+ data[z-1, y-1, x-1], data[z-1, y-1, x], data[z-1, y-1, x+1],
152
+ data[z-1, y, x-1], data[z-1, y, x], data[z-1, y, x+1],
153
+ data[z-1, y+1, x-1], data[z-1, y+1, x], data[z-1, y+1, x+1],
154
+
155
+ # --- Middle Slice (z) ---
156
+ data[z, y-1, x-1], data[z, y-1, x], data[z, y-1, x+1],
157
+ data[z, y, x-1] , data[z, y, x+1],
158
+ data[z, y+1, x-1], data[z, y+1, x], data[z, y+1, x+1],
159
+
160
+ # --- Bottom Slice (z+1) ---
161
+ data[z+1, y-1, x-1], data[z+1, y-1, x], data[z+1, y-1, x+1],
162
+ data[z+1, y, x-1], data[z+1, y, x], data[z+1, y, x+1],
163
+ data[z+1, y+1, x-1], data[z+1, y+1, x], data[z+1, y+1, x+1]
164
+ )
165
+
166
+ one = np.uint8(1)
167
+ l0 = one
168
+ l1 = (l0 + one) if v1 == v0 and v1>0 else one
169
+ l2 = (l1 + one) if v2 == v1 and v2>0 else one
170
+ l3 = (l2 + one) if v3 == v2 and v3>0 else one
171
+ l4 = (l3 + one) if v4 == v3 and v4>0 else one
172
+ l5 = (l4 + one) if v5 == v4 and v5>0 else one
173
+ l6 = (l5 + one) if v6 == v5 and v6>0 else one
174
+ l7 = (l6 + one) if v7 == v6 and v7>0 else one
175
+ l8 = (l7 + one) if v8 == v7 and v8>0 else one
176
+ l9 = (l8 + one) if v9 == v8 and v9>0 else one
177
+ l10 = (l9 + one) if v10 == v9 and v10>0 else one
178
+ l11 = (l10 + one) if v11 == v10 and v11>0 else one
179
+ l12 = (l11 + one) if v12 == v11 and v12>0 else one
180
+ l13 = (l12 + one) if v13 == v12 and v13>0 else one
181
+ l14 = (l13 + one) if v14 == v13 and v14>0 else one
182
+ l15 = (l14 + one) if v15 == v14 and v15>0 else one
183
+ l16 = (l15 + one) if v16 == v15 and v16>0 else one
184
+ l17 = (l16 + one) if v17 == v16 and v17>0 else one
185
+ l18 = (l17 + one) if v18 == v17 and v18>0 else one
186
+ l19 = (l18 + one) if v19 == v18 and v19>0 else one
187
+ l20 = (l19 + one) if v20 == v19 and v20>0 else one
188
+ l21 = (l20 + one) if v21 == v20 and v21>0 else one
189
+ l22 = (l21 + one) if v22 == v21 and v22>0 else one
190
+ l23 = (l22 + one) if v23 == v22 and v23>0 else one
191
+ l24 = (l23 + one) if v24 == v23 and v24>0 else one
192
+ l25 = (l24 + one) if v25 == v24 and v25>0 else one
193
+
194
+ def _update_max(len1, val1, len2, val2):
195
+ if len2 >= len1:
196
+ return len2, val2
197
+ return len1, val1
198
+
199
+ # Layer 1: 13 parallel comparisons
200
+ l1, v1 = _update_max(l0, v0, l1, v1)
201
+ l3, v3 = _update_max(l2, v2, l3, v3)
202
+ l5, v5 = _update_max(l4, v4, l5, v5)
203
+ l7, v7 = _update_max(l6, v6, l7, v7)
204
+ l9, v9 = _update_max(l8, v8, l9, v9)
205
+ l11, v11 = _update_max(l10, v10, l11, v11)
206
+ l13, v13 = _update_max(l12, v12, l13, v13)
207
+ l15, v15 = _update_max(l14, v14, l15, v15)
208
+ l17, v17 = _update_max(l16, v16, l17, v17)
209
+ l19, v19 = _update_max(l18, v18, l19, v19)
210
+ l21, v21 = _update_max(l20, v20, l21, v21)
211
+ l23, v23 = _update_max(l22, v22, l23, v23)
212
+ l25, v25 = _update_max(l24, v24, l25, v25)
213
+
214
+ # Layer 2: Winners from Layer 1 compete (6 parallel comparisons)
215
+ l3, v3 = _update_max(l1, v1, l3, v3)
216
+ l7, v7 = _update_max(l5, v5, l7, v7)
217
+ l11, v11 = _update_max(l9, v9, l11, v11)
218
+ l15, v15 = _update_max(l13, v13, l15, v15)
219
+ l19, v19 = _update_max(l17, v17, l19, v19)
220
+ l23, v23 = _update_max(l21, v21, l23, v23)
221
+ # l25, v25 are carried over
222
+
223
+ # Layer 3: 3 parallel comparisons
224
+ l7, v7 = _update_max(l3, v3, l7, v7)
225
+ l15, v15 = _update_max(l11, v11, l15, v15)
226
+ l23, v23 = _update_max(l19, v19, l23, v23)
227
+ # l25, v25 are carried over
228
+
229
+ # Layer 4: 2 parallel comparisons
230
+ l15, v15 = _update_max(l7, v7, l15, v15)
231
+ l25, v25 = _update_max(l23, v23, l25, v25)
232
+
233
+ # Layer 5: Final comparison
234
+ l25, v25 = _update_max(l15, v15, l25, v25)
235
+ return v25
236
+
237
+
238
+ @numba.njit(inline="always")
239
+ def load_box_stencil(data, z, y, x, sz,sy,sx, nbs):
240
+ z1 = -1 if z > 0 else 0; z2 = 2 if z < sz-1 else 1
241
+ y1 = -1 if y > 0 else 0; y2 = 2 if y < sy-1 else 1
242
+ x1 = -1 if x > 0 else 0; x2 = 2 if x < sx-1 else 1
243
+ nnz = 0
244
+ for i in range(z1,z2):
245
+ for j in range(y1,y2):
246
+ for k in range(x1,x2):
247
+ val = data[z + i, y + j, x + k]
248
+ nbs[nnz] = val
249
+ if val > 0:
250
+ nnz += 1
251
+ return nnz
252
+
253
+ @numba.njit(inline="always")
254
+ def load_diamond_stencil(data, z, y, x, sz, sy, sx, nbs):
255
+ """
256
+ Loads a diamond stencil into a neighbors array, counting only non-zero values.
257
+ The stencil includes the 6 direct neighbors.
258
+ Boundary conditions are handled by checking array dimensions.
259
+
260
+ Parameters:
261
+ - data: The 3D input NumPy array.
262
+ - z, y, x: The coordinates of the center point.
263
+ - sz, sy, sx: The dimensions of the data array.
264
+ - nbs: The NumPy array to load the neighbors into.
265
+
266
+ Returns:
267
+ - The number of non-zero values loaded.
268
+ """
269
+ nnz = 0
270
+
271
+ if z > 0:
272
+ val = data[z - 1, y, x]
273
+ nbs[nnz] = val
274
+ if val > 0:
275
+ nnz += 1
276
+
277
+ if z < sz - 1:
278
+ val = data[z + 1, y, x]
279
+ nbs[nnz] = val
280
+ if val > 0:
281
+ nnz += 1
282
+
283
+ if y > 0:
284
+ val = data[z, y - 1, x]
285
+ nbs[nnz] = val
286
+ if val > 0:
287
+ nnz += 1
288
+
289
+ if y < sy - 1:
290
+ val = data[z, y + 1, x]
291
+ nbs[nnz] = val
292
+ if val > 0:
293
+ nnz += 1
294
+
295
+ if x > 0:
296
+ val = data[z, y, x - 1]
297
+ nbs[nnz] = val
298
+ if val > 0:
299
+ nnz += 1
300
+
301
+ if x < sx - 1:
302
+ val = data[z, y, x + 1]
303
+ nbs[nnz] = val
304
+ if val > 0:
305
+ nnz += 1
306
+ return nnz
307
+
308
+ @numba.njit
309
+ def _mode_borders(data, out, stencil):
310
+ sz, sy, sx = data.shape
311
+ nbs = np.empty(17, dtype=data.dtype)
312
+
313
+ def process_point(z, y, x):
314
+ if data[z, y, x] > 0:
315
+ out[z, y, x] = data[z, y, x]
316
+ else:
317
+ if stencil=="box":
318
+ nnz = load_box_stencil(data, z, y, x, sz,sy,sx, nbs)
319
+ else:
320
+ nnz = load_diamond_stencil(data, z, y, x, sz,sy,sx, nbs)
321
+
322
+ out[z, y, x] = fast_modeN(nbs, nnz) * (nnz > 0)
323
+
324
+ # 1. Top and Bottom faces (Z-axis)
325
+ for z in [0, sz - 1]:
326
+ for y in range(sy):
327
+ for x in range(sx):
328
+ process_point(z, y, x)
329
+
330
+ # 2. Front and Back faces (Y-axis), excluding edges already done by Z-faces
331
+ for y in [0, sy - 1]:
332
+ for z in range(1, sz - 1): # Note: range starts at 1, ends at sz-2
333
+ for x in range(sx):
334
+ process_point(z, y, x)
335
+
336
+ # 3. Left and Right faces (X-axis), excluding edges already done by Z and Y faces
337
+ for x in [0, sx - 1]:
338
+ for z in range(1, sz - 1): # Note: range starts at 1
339
+ for y in range(1, sy - 1): # Note: range starts at 1
340
+ process_point(z, y, x)
341
+
342
+ return out
343
+
344
+ @numba.njit(parallel=True)
345
+ def _onlyzero_mode_box(data, out=None):
346
+ sz, sy, sx = data.shape
347
+ if out is None:
348
+ out = np.empty_like(data)
349
+ assert data.shape == out.shape
350
+ for z in numba.prange(1, sz-1):
351
+ for y in range(1, sy-1):
352
+ for x in range(1, sx-1):
353
+ if data[z,y,x]>0:
354
+ out[z,y,x] = data[z,y,x]
355
+ else:
356
+ out[z,y,x] = mode_box(data, z,y,x)
357
+ _mode_borders(data, out, stencil="box")
358
+ return out
359
+
360
+
361
+ @numba.njit(cache=True)
362
+ def onlyzero_mode_box(data, out=None):
363
+ if isinstance(data.dtype.type(0), bool):
364
+ return maximum_box(data, out, onlyzero=True)
365
+ else:
366
+ return _onlyzero_mode_box(data, out=out)
367
+
368
+ @numba.njit(parallel=True)
369
+ def _onlyzero_mode_diamond(data, out=None):
370
+ sz, sy, sx = data.shape
371
+ if out is None:
372
+ out = np.empty_like(data)
373
+ assert data.shape == out.shape
374
+ for z in numba.prange(1, sz-1):
375
+ for y in range(1, sy-1):
376
+ for x in range(1, sx-1):
377
+ if data[z,y,x]>0:
378
+ out[z,y,x] = data[z,y,x]
379
+ else:
380
+ out[z,y,x] = mode_diamond(data, z,y,x)
381
+ _mode_borders(data, out, stencil="diamond")
382
+ return out
383
+
384
+ @numba.njit(cache=True)
385
+ def onlyzero_mode_diamond(data, out=None):
386
+ if isinstance(data.dtype.type(0), bool):
387
+ return maximum_diamond(data, out, onlyzero=True)
388
+ else:
389
+ return _onlyzero_mode_diamond(data, out=out)
nbmorph/morphology.py ADDED
@@ -0,0 +1,131 @@
1
+ import numpy as np
2
+ import numba
3
+ from .mode import onlyzero_mode_box, onlyzero_mode_diamond
4
+ from .minmax import minimum_box, minimum_diamond
5
+ from .zero_edges import zero_label_edges_box, zero_label_edges_diamond
6
+ from .utils import cycle
7
+
8
+ @numba.njit(cache=True)
9
+ def dilate_labels_spherical(labels: np.ndarray, radius: int=1,
10
+ struct_sequence:str="DDB") -> np.ndarray:
11
+ """
12
+ Performs fast, quasi-spherical multilabel dilation on a CPU.
13
+ This approximates a spherical structuring element by alternating between a box (3x3x3)
14
+ and a diamond (6-connected) kernel for each iteration.
15
+ 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.
18
+
19
+ Returns:
20
+ The dilated 3D labeled image.
21
+ """
22
+ # Use two buffers that are swapped (ping-pong) to propagate labels iteratively
23
+ # We start by copying the original labels to both
24
+
25
+ # Initial setup: The original data is the first "pong".
26
+ pong = np.copy(labels)
27
+ ping = np.empty_like(pong)
28
+
29
+ for s in cycle(struct_sequence, radius):
30
+ # Determine which kernel to use for this iteration
31
+ if s=="B":
32
+ onlyzero_mode_box(pong, out=ping)
33
+ elif s=="D":
34
+ onlyzero_mode_diamond(pong, out=ping)
35
+
36
+ # --- Swap Buffers for the Next Iteration ---
37
+ # The ping of this step becomes the pong for the next one.
38
+ pong, ping = ping, pong
39
+ return pong
40
+
41
+ @numba.njit(cache=True)
42
+ def erode_labels_spherical(labels: np.ndarray, radius: int=1,
43
+ struct_sequence:str="DDB") -> np.ndarray:
44
+ """
45
+ Performs fast, quasi-spherical multilabel erosion on a CPU.
46
+
47
+ Args:
48
+ labels (np.ndarray): The input 3D labeled image.
49
+ radius (int, optional): The erosion radius. Defaults to 1.
50
+
51
+ Returns:
52
+ np.ndarray: The eroded labeled image.
53
+ """
54
+ assert radius > 0
55
+ pong = np.copy(labels)
56
+ ping = np.empty_like(pong)
57
+
58
+ if struct_sequence[0]=="D":
59
+ zero_label_edges_diamond(pong, out=ping)
60
+ elif struct_sequence[0]=="B":
61
+ zero_label_edges_box(pong, out=ping)
62
+
63
+ if radius==1:
64
+ return ping
65
+
66
+ for s in cycle(struct_sequence, radius)[1:]:
67
+ if s=="B":
68
+ minimum_box(ping, out=pong)
69
+ elif s=="D":
70
+ minimum_diamond(ping, out=pong)
71
+ ping, pong = pong, ping
72
+ return ping
73
+
74
+ #@numba.njit
75
+ def open_labels_spherical(labels: np.ndarray, radius: int=1, iterations:int=1) -> np.ndarray:
76
+ """
77
+ Performs a morphological opening on a labeled image.
78
+
79
+ Args:
80
+ labels (np.ndarray): The input 3D labeled image.
81
+ radius (int): The radius of the spherical structuring element.
82
+ iterations (int, optional): The number of iterations. Defaults to 1.
83
+
84
+ Returns:
85
+ np.ndarray: The opened labeled image.
86
+ """
87
+ out = np.copy(labels)
88
+ for i in range(iterations):
89
+ out = erode_labels_spherical(out, radius)
90
+ out = dilate_labels_spherical(out, radius)
91
+ return out
92
+
93
+ #@numba.njit
94
+ def close_labels_spherical(labels: np.ndarray, radius: int, iterations:int=1) -> np.ndarray:
95
+ """
96
+ Performs a morphological closing on a labeled image.
97
+
98
+ Args:
99
+ labels (np.ndarray): The input 3D labeled image.
100
+ radius (int): The radius of the spherical structuring element.
101
+ iterations (int, optional): The number of iterations. Defaults to 1.
102
+
103
+ Returns:
104
+ np.ndarray: The closed labeled image.
105
+ """
106
+ out = np.copy(labels)
107
+ #print("closing...")
108
+ 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()}")
113
+ return out
114
+
115
+ #@numba.njit
116
+ def smooth_labels_spherical(labels: np.ndarray, radius: int, iterations:int=1) -> np.ndarray:
117
+ """
118
+ Performs a morphological smoothing on a labeled image.
119
+
120
+ Args:
121
+ labels (np.ndarray): The input 3D labeled image.
122
+ radius (int): The radius of the spherical structuring element.
123
+ iterations (int, optional): The number of iterations. Defaults to 1.
124
+
125
+ Returns:
126
+ np.ndarray: The smoothed labeled image.
127
+ """
128
+ for i in range(iterations):
129
+ labels = open_labels_spherical(labels, radius)
130
+ labels = close_labels_spherical(labels, radius)
131
+ return labels
nbmorph/ops.py ADDED
@@ -0,0 +1,16 @@
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)
nbmorph/utils.py ADDED
@@ -0,0 +1,6 @@
1
+ import numpy as np
2
+ import numba
3
+
4
+ @numba.njit
5
+ def cycle(s: str, i:int):
6
+ return (s*i)[:i]
nbmorph/zero_edges.py ADDED
@@ -0,0 +1,25 @@
1
+ import numba
2
+ from .diamond_kernel import diamond_loop_padded
3
+ from .box_kernel import kernel3x3x3
4
+
5
+ @numba.njit(cache=True)
6
+ def zero_label_edges_box(a, out=None):
7
+ """
8
+ Set the edges of labels to zero.
9
+
10
+ Args:
11
+ a (np.ndarray): The input 3D labeled array.
12
+
13
+ Returns:
14
+ np.ndarray: The array with label edges set to zero.
15
+ """
16
+ return kernel3x3x3(a, opname="zeroedges", out=out)
17
+
18
+
19
+ @numba.njit(cache=True)
20
+ def zero_label_edges_diamond(data, out=None):
21
+ return diamond_loop_padded(data, out=out, opname="zeroedges")
22
+
23
+
24
+
25
+
@@ -0,0 +1,136 @@
1
+ Metadata-Version: 2.4
2
+ Name: nbmorph
3
+ Version: 0.1.0
4
+ Summary: A small package with Numba-accelerated morphological operations.
5
+ Author-email: Marius Causemann <mariusca@simula.no>
6
+ License: MIT License
7
+
8
+ Copyright (c) [2025] [Marius Causemann]
9
+
10
+ Permission is hereby granted, free of charge, to any person obtaining a copy
11
+ of this software and associated documentation files (the "Software"), to deal
12
+ in the Software without restriction, including without limitation the rights
13
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14
+ copies of the Software, and to permit persons to whom the Software is
15
+ furnished to do so, subject to the following conditions:
16
+
17
+ The above copyright notice and this permission notice shall be included in all
18
+ copies or substantial portions of the Software.
19
+
20
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
26
+ SOFTWARE.
27
+ Classifier: Programming Language :: Python :: 3
28
+ Classifier: License :: OSI Approved :: MIT License
29
+ Classifier: Operating System :: OS Independent
30
+ Classifier: Intended Audience :: Science/Research
31
+ Classifier: Topic :: Scientific/Engineering :: Image Processing
32
+ Requires-Python: >=3.8
33
+ Description-Content-Type: text/markdown
34
+ License-File: LICENSE
35
+ Requires-Dist: numpy
36
+ Requires-Dist: numba
37
+ Provides-Extra: test
38
+ Requires-Dist: pytest; extra == "test"
39
+ Requires-Dist: pytest-cov; extra == "test"
40
+ Requires-Dist: fastmorph; extra == "test"
41
+ Dynamic: license-file
42
+
43
+ # nbmorph
44
+
45
+ A small, Numba-accelerated Python package for morphological operations on 3D labeled images.
46
+
47
+ `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
+
49
+ ## Features
50
+
51
+ * **Numba-accelerated:** Operations are just-in-time compiled with Numba for high performance on CPUs.
52
+
53
+ * **3D Label Image Support:** All operations are designed for 3D labeled images (integer NumPy arrays).
54
+
55
+ * **Quasi-Spherical Structuring Elements:** Approximates spherical structuring elements by alternating between box and diamond kernels for dilation and erosion.
56
+
57
+ * **Core Morphological Operations:**
58
+
59
+ * `dilate_labels_spherical`: Expands the boundaries of labeled regions by assigning the mode of the neighborhood to background voxels.
60
+
61
+ * `erode_labels_spherical`: Shrinks the boundaries of labeled regions.
62
+
63
+ * `open_labels_spherical`: Removes small noise and thin protrusions (erosion followed by dilation).
64
+
65
+ * `close_labels_spherical`: Fills small holes within objects (dilation followed by erosion).
66
+
67
+ * `smooth_labels_spherical`: Smoothes object boundaries by performing an opening followed by a closing.
68
+
69
+ ![Effect of Morphological Smoothing](img/smoothing_effect.png)
70
+ *Demonstration of the smoothing effect with varying radii and iterations on a sample image.*
71
+
72
+
73
+ ## Installation
74
+
75
+ You can install `nbmorph` directly from the source directory using pip:
76
+
77
+ ```
78
+ pip install .
79
+
80
+ ```
81
+
82
+
83
+ ## Usage
84
+
85
+ Here is a basic example of how to use `nbmorph` to apply morphological smoothing to a 3D labeled image.
86
+
87
+ ```
88
+ import numpy as np
89
+ import nbmorph
90
+
91
+ # Create a sample 3D labeled image
92
+ # For example, a 5x5x5 cube of label 1 in a 10x10x10 volume
93
+ labels = np.zeros((10, 10, 10), dtype=np.uint16)
94
+ labels[2:7, 2:7, 2:7] = 1
95
+
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
+ ```
102
+
103
+ ## Operations
104
+
105
+ The core functions of the library are:
106
+
107
+ * `nbmorph.dilate_labels_spherical(labels, radius=1)`
108
+
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
+
117
+ ## Testing
118
+
119
+ Tests are written using `pytest`. To run the tests, first install the test dependencies and then run `pytest`:
120
+
121
+ ```
122
+ pip install .[test]
123
+ pytest
124
+ ```
125
+
126
+ ## Benchmarking
127
+
128
+ A benchmark script is included in the `scripts` directory:
129
+
130
+ ```
131
+ python scripts/benchmark.py
132
+ ```
133
+
134
+ ## License
135
+
136
+ This project is licensed under the MIT License - see the `LICENSE` file for details.
@@ -0,0 +1,14 @@
1
+ nbmorph/__init__.py,sha256=4xhgDT4cPSR8fLzdWGT2hZLvt_kACtKLVCnv2laXJc0,465
2
+ nbmorph/box_kernel.py,sha256=Sk9-2YJUBwCCP_ZnXvkS-_ILOUtSpOAqUMMbO1McLtE,18343
3
+ nbmorph/diamond_kernel.py,sha256=Pt6sEnX4MrZw3nB7UAOrcgbwG4ALGsJVqPGkyrhvJm0,1637
4
+ nbmorph/minmax.py,sha256=E_h7ZJRb-t2SFCYxZW3WwRdWkoQCAsOc65H1tfIlhWA,709
5
+ nbmorph/mode.py,sha256=rakgIWVdbX18Gv-gxZuaOwsDu6RfPdiGNnK9bvYRNtc,15271
6
+ nbmorph/morphology.py,sha256=krm3f4iBEcDy3XtumIvFpAlSOI8n2jo9Kgeij76Gp4E,4448
7
+ nbmorph/ops.py,sha256=ZEKwkLoxD821ou-wBlirfCefFWf7nUAnv4ktmkZ2BnA,412
8
+ nbmorph/utils.py,sha256=887MKCuXN1v1GjGeFBPCEqpvT6JWSaI_75mv9zeT0iY,91
9
+ nbmorph/zero_edges.py,sha256=zGSmFJcMEN-dywgpOyGYb_PtogRbvw572WDAg15aoTA,548
10
+ nbmorph-0.1.0.dist-info/licenses/LICENSE,sha256=sN9aXC3FxdLEbFQ-8mu0XUWeeFeKfPZ3NjuktXFj1EU,1076
11
+ nbmorph-0.1.0.dist-info/METADATA,sha256=NvvqPZAdH-6dcaDqMRvte0-a4VymJ_PeEvzecBipQbI,4812
12
+ nbmorph-0.1.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
13
+ nbmorph-0.1.0.dist-info/top_level.txt,sha256=gOvPcD_UiplCjFLWlaADUo8tRVaBXTqVZE6tjYyWRs4,8
14
+ nbmorph-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (80.9.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) [2025] [Marius Causemann]
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1 @@
1
+ nbmorph