otsu2D 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.
otsu2D/__init__.py ADDED
@@ -0,0 +1,40 @@
1
+ """
2
+ Local 2D Otsu Thresholding for 3D Images
3
+ ==========================================
4
+
5
+ A high-performance implementation of local 2D Otsu thresholding for 3D grayscale images
6
+ using Numba acceleration.
7
+
8
+ Main Functions
9
+ --------------
10
+ getBinary : Apply local 2D Otsu thresholding to get binary mask
11
+ getThreshold : Compute 2D Otsu threshold for 1D intensity values and 1D local mean values
12
+
13
+ Example
14
+ -------
15
+ >>> import numpy as np
16
+ >>> from modifiedOtsu import getBinary
17
+ >>>
18
+ >>> # Create sample 3D image
19
+ >>> img = np.random.randint(0, 256, size=(50, 50, 50), dtype=np.uint8)
20
+ >>>
21
+ >>> # Get binary image
22
+ >>> binary = getBinary(img, window_size=(3, 3, 3))
23
+ """
24
+
25
+ from .core import getBinary, getThreshold
26
+
27
+ # Package metadata
28
+ __version__ = "0.1.0"
29
+ __author__ = "John Rick Manzanares"
30
+ __email__ = "jdolormanzanares@impan.pl"
31
+
32
+ # Define what gets imported with "from Otsu2D import *"
33
+ __all__ = [
34
+ "getBinary",
35
+ "getThreshold",
36
+ ]
37
+
38
+ # Optional: Add convenience functions or constants
39
+ DEFAULT_WINDOW_SIZE = (3, 3, 3)
40
+ DEFAULT_MEAN_WINDOW_SIZE = (3, 3, 3)
otsu2D/core.py ADDED
@@ -0,0 +1,429 @@
1
+ """
2
+ Core implementation of local 2D Otsu thresholding.
3
+ """
4
+
5
+ import numpy as np
6
+ from numba import njit, prange
7
+ from typing import Tuple
8
+
9
+ # ============================================================================
10
+ # Helper Functions (Internal)
11
+ # ============================================================================
12
+
13
+
14
+ @njit
15
+ def _padReflect(img: np.ndarray, pad_width: Tuple[int, int, int]) -> np.ndarray:
16
+ """
17
+ Manual reflect padding for 3D arrays (Numba-compatible).
18
+
19
+ Parameters
20
+ ----------
21
+ img : np.ndarray
22
+ 3D input array
23
+ pad_width : tuple of int
24
+ Padding size for each dimension (z_pad, y_pad, x_pad)
25
+
26
+ Returns
27
+ -------
28
+ np.ndarray
29
+ Padded array with reflect mode
30
+ """
31
+ z_pad, y_pad, x_pad = pad_width
32
+ z_max, y_max, x_max = img.shape
33
+
34
+ padded = np.zeros(
35
+ (z_max + 2 * z_pad, y_max + 2 * y_pad, x_max + 2 * x_pad), dtype=img.dtype
36
+ )
37
+
38
+ # Copy center
39
+ padded[z_pad : z_pad + z_max, y_pad : y_pad + y_max, x_pad : x_pad + x_max] = img
40
+
41
+ # Reflect padding for Z
42
+ for i in range(z_pad):
43
+ padded[z_pad - 1 - i, y_pad : y_pad + y_max, x_pad : x_pad + x_max] = img[
44
+ i, :, :
45
+ ]
46
+ padded[z_pad + z_max + i, y_pad : y_pad + y_max, x_pad : x_pad + x_max] = img[
47
+ z_max - 1 - i, :, :
48
+ ]
49
+
50
+ # Reflect padding for Y
51
+ for j in range(y_pad):
52
+ padded[:, y_pad - 1 - j, x_pad : x_pad + x_max] = padded[
53
+ :, y_pad + j, x_pad : x_pad + x_max
54
+ ]
55
+ padded[:, y_pad + y_max + j, x_pad : x_pad + x_max] = padded[
56
+ :, y_pad + y_max - 1 - j, x_pad : x_pad + x_max
57
+ ]
58
+
59
+ # Reflect padding for X
60
+ for k in range(x_pad):
61
+ padded[:, :, x_pad - 1 - k] = padded[:, :, x_pad + k]
62
+ padded[:, :, x_pad + x_max + k] = padded[:, :, x_pad + x_max - 1 - k]
63
+
64
+ return padded
65
+
66
+
67
+ @njit
68
+ def _getBinary(
69
+ img: np.ndarray,
70
+ window_size: Tuple[int, int, int],
71
+ mean_window_size: Tuple[int, int, int],
72
+ ) -> np.ndarray:
73
+ """
74
+ Internal Numba-compiled implementation.
75
+ No validation - assumes inputs are correct.
76
+ """
77
+
78
+ localMean = getLocalMean(img, mean_window_size)
79
+
80
+
81
+ z_win, y_win, x_win = window_size
82
+ z_pad, y_pad, x_pad = z_win // 2, y_win // 2, x_win // 2
83
+
84
+ padded = _padReflect(img, (z_pad, y_pad, x_pad))
85
+ paddedLocalMean = _padReflect(localMean, (z_pad, y_pad, x_pad))
86
+ binary = np.zeros_like(img, dtype=np.int32)
87
+ z_max, y_max, x_max = img.shape
88
+
89
+ for z in prange(z_max):
90
+ for y in range(y_max):
91
+ for x in range(x_max):
92
+ windowIntensity = padded[
93
+ z : z + z_win, y : y + y_win, x : x + x_win
94
+ ].ravel()
95
+ windowMean = paddedLocalMean[
96
+ z : z + z_win, y : y + y_win, x : x + x_win
97
+ ].ravel()
98
+ s_real, t_real = getThreshold(windowIntensity, windowMean)
99
+
100
+ # Element-wise comparison at each voxel
101
+ condition1 = img[z, y, x] > s_real
102
+ condition2 = localMean[z, y, x] > t_real
103
+
104
+ binary[z, y, x] = condition1 & condition2
105
+
106
+ return binary.astype(np.uint8) * 255
107
+
108
+
109
+ @njit
110
+ def getLocalMean(
111
+ image: np.ndarray, window_size: Tuple[int, int, int] = (3, 3, 3)
112
+ ) -> np.ndarray:
113
+ """
114
+ Compute local mean for a 3D image using integral image (fast) without padding.
115
+ Only voxels where the full window fits are computed.
116
+
117
+ Parameters
118
+ ----------
119
+ image : np.ndarray
120
+ 3D input image (z, y, x)
121
+ window_size : int
122
+ Neighborhood size for local mean calculation (should be odd)
123
+
124
+ Returns
125
+ -------
126
+ np.ndarray
127
+ Local mean image, same shape as input, with zeros at boundaries
128
+ where the full window does not fit. dtype=np.uint16
129
+ """
130
+ # z_max, y_max, x_max = image.shape
131
+ ws_z, ws_y, ws_x = window_size
132
+ pad = (ws_z // 2, ws_y // 2, ws_x // 2)
133
+ padded = _padReflect(image, pad)
134
+
135
+ z_max, y_max, x_max = padded.shape
136
+
137
+ # Compute integral image
138
+ integral = np.zeros((z_max, y_max, x_max), dtype=np.float64)
139
+ for z in range(z_max):
140
+ for y in range(y_max):
141
+ for x in range(x_max):
142
+ val = padded[z, y, x]
143
+ if z > 0:
144
+ val += integral[z - 1, y, x]
145
+ if y > 0:
146
+ val += integral[z, y - 1, x]
147
+ if x > 0:
148
+ val += integral[z, y, x - 1]
149
+ if z > 0 and y > 0:
150
+ val -= integral[z - 1, y - 1, x]
151
+ if z > 0 and x > 0:
152
+ val -= integral[z - 1, y, x - 1]
153
+ if y > 0 and x > 0:
154
+ val -= integral[z, y - 1, x - 1]
155
+ if z > 0 and y > 0 and x > 0:
156
+ val += integral[z - 1, y - 1, x - 1]
157
+ integral[z, y, x] = val
158
+
159
+ # Compute local mean
160
+ local_mean = np.zeros_like(image, dtype=np.float32)
161
+ z0, y0, x0 = pad
162
+
163
+ # Only compute where the full window fits
164
+ for z in range(ws_z - 1, z_max - z0):
165
+ for y in range(ws_y - 1, y_max - y0):
166
+ for x in range(ws_x - 1, x_max - x0):
167
+ z1, y1, x1 = z - z0, y - y0, x - x0
168
+ z2, y2, x2 = z + z0, y + y0, x + x0
169
+
170
+ s = integral[z2, y2, x2]
171
+ if z1 > 0:
172
+ s -= integral[z1 - 1, y2, x2]
173
+ if y1 > 0:
174
+ s -= integral[z2, y1 - 1, x2]
175
+ if x1 > 0:
176
+ s -= integral[z2, y2, x1 - 1]
177
+ if z1 > 0 and y1 > 0:
178
+ s += integral[z1 - 1, y1 - 1, x2]
179
+ if z1 > 0 and x1 > 0:
180
+ s += integral[z1 - 1, y2, x1 - 1]
181
+ if y1 > 0 and x1 > 0:
182
+ s += integral[z2, y1 - 1, x1 - 1]
183
+ if z1 > 0 and y1 > 0 and x1 > 0:
184
+ s -= integral[z1 - 1, y1 - 1, x1 - 1]
185
+
186
+ local_mean[z - z0, y - y0, x - x0] = s / (ws_z * ws_y * ws_x)
187
+
188
+ return local_mean.astype(np.uint16)
189
+
190
+
191
+ @njit
192
+ def cumSum(arr, axis=None):
193
+ """
194
+ Compute cumulative sum manually (Numba-compatible).
195
+ Supports axis=None, axis=0, and axis=1 (like np.cumsum).
196
+ """
197
+ rows, cols = arr.shape
198
+ out = np.zeros_like(arr)
199
+ if axis is None:
200
+ # Flattened cumulative sum
201
+ flat = arr.ravel()
202
+ out_flat = out.ravel()
203
+ total = 0.0
204
+ for i in range(flat.size):
205
+ total += flat[i]
206
+ out_flat[i] = total
207
+ return out
208
+ elif axis == 0:
209
+ # Cumulative sum along rows (down each column)
210
+ for j in range(cols):
211
+ total = 0.0
212
+ for i in range(rows):
213
+ total += arr[i, j]
214
+ out[i, j] = total
215
+ return out
216
+ elif axis == 1:
217
+ # Cumulative sum along columns (across each row)
218
+ for i in range(rows):
219
+ total = 0.0
220
+ for j in range(cols):
221
+ total += arr[i, j]
222
+ out[i, j] = total
223
+ return out
224
+ else:
225
+ raise ValueError("axis must be None, 0, or 1")
226
+
227
+ def _validate_window_size(name, window_size):
228
+ """
229
+ Validate that a window size argument is a tuple or list of three positive odd integers.
230
+
231
+ This function checks that the provided `window_size`:
232
+ - Is a tuple or list.
233
+ - Has exactly three elements corresponding to (z, y, x).
234
+ - Contains only integer values.
235
+ - Contains only positive values.
236
+ - Contains only odd values (since even sizes cannot be centered).
237
+
238
+ If any of these conditions are not met, a ValueError is raised with a descriptive
239
+ message that includes the variable name for easier debugging.
240
+
241
+ Parameters
242
+ ----------
243
+ name : str
244
+ The name of the variable being validated (used in error messages).
245
+ window_size : tuple or list of int
246
+ The window size to validate. Must contain exactly three positive odd integers.
247
+
248
+ Raises
249
+ ------
250
+ ValueError
251
+ If `window_size` is not a tuple or list of three positive odd integers.
252
+ """
253
+ if not isinstance(window_size, (tuple, list)):
254
+ raise ValueError(
255
+ f"{name} must be a tuple or list, got {type(window_size)}"
256
+ )
257
+
258
+ if len(window_size) != 3:
259
+ raise ValueError(
260
+ f"{name} must have 3 elements (z, y, x), got {len(window_size)}"
261
+ )
262
+
263
+ if not all(isinstance(w, (int, np.integer)) for w in window_size):
264
+ raise ValueError(f"{name} elements must be integers, got {window_size}")
265
+
266
+ if any(w <= 0 for w in window_size):
267
+ raise ValueError(f"{name} elements must be positive, got {window_size}")
268
+
269
+ if any(w % 2 == 0 for w in window_size):
270
+ z_win, y_win, x_win = window_size
271
+ raise ValueError(
272
+ f"{name} elements should be odd numbers for centered windows, "
273
+ f"got {window_size}. Consider using "
274
+ f"({z_win+1 if z_win%2==0 else z_win}, "
275
+ f"{y_win+1 if y_win%2==0 else y_win}, "
276
+ f"{x_win+1 if x_win%2==0 else x_win}) instead."
277
+ )
278
+
279
+ # ============================================================================
280
+ # Public API
281
+ # ============================================================================
282
+
283
+
284
+ @njit
285
+ def getThreshold(intensity_values: np.ndarray, local_mean_values: np.ndarray):
286
+ """
287
+ Compute 2D Otsu threshold for a local neighborhood.
288
+
289
+ Parameters:
290
+ intensity_values (1D array): Intensity values in the neighborhood
291
+ local_mean_values (1D array): Local mean values in the neighborhood
292
+
293
+ Returns:
294
+ s_real (int): Optimal intensity threshold
295
+ t_real (int): Optimal local mean threshold
296
+ """
297
+ # Build local 2D histogram
298
+ g_min, g_max = intensity_values.min(), intensity_values.max()
299
+ m_min, m_max = local_mean_values.min(), local_mean_values.max()
300
+
301
+ # Handle degenerate cases
302
+ if g_min == g_max or m_min == m_max:
303
+ return g_min, m_min
304
+
305
+ g_range = g_max - g_min + 1
306
+ m_range = m_max - m_min + 1
307
+
308
+ # Build histogram
309
+ hist_2d = np.zeros((g_range, m_range), dtype=np.uint32)
310
+ flat_g = intensity_values - g_min
311
+ flat_m = local_mean_values - m_min
312
+
313
+ for i in range(len(flat_g)):
314
+ hist_2d[flat_g[i], flat_m[i]] += 1
315
+
316
+ p = hist_2d.astype(np.float64) / hist_2d.sum()
317
+
318
+ # Global means for this neighborhood
319
+ i_vals = (np.arange(g_range) + g_min)[:, None]
320
+ j_vals = (np.arange(m_range) + m_min)[None, :]
321
+
322
+ mu_T0 = np.sum(i_vals * p)
323
+ mu_T1 = np.sum(j_vals * p)
324
+
325
+ # Cumulative sums
326
+ P_cum = cumSum(cumSum(p, axis=0), axis=1)
327
+ mu_i_cum = cumSum(cumSum(i_vals * p, axis=0), axis=1)
328
+ mu_j_cum = cumSum(cumSum(j_vals * p, axis=0), axis=1)
329
+
330
+ max_trace = 0
331
+ best_s = best_t = 0
332
+
333
+ for s in range(g_range - 1):
334
+ for t in range(m_range - 1):
335
+ P0 = P_cum[s, t]
336
+
337
+ if P0 <= 0 or P0 >= 1.0:
338
+ continue
339
+
340
+ mu_i = mu_i_cum[s, t]
341
+ mu_j = mu_j_cum[s, t]
342
+
343
+ numerator = (mu_i - P0 * mu_T0) ** 2 + (mu_j - P0 * mu_T1) ** 2
344
+ denominator = P0 * (1 - P0)
345
+ trace_SB = numerator / denominator
346
+
347
+ if trace_SB > max_trace:
348
+ max_trace = trace_SB
349
+ best_s, best_t = s, t
350
+
351
+ s_real = best_s + g_min
352
+ t_real = best_t + m_min
353
+
354
+ return s_real, t_real
355
+
356
+
357
+ def getBinary(
358
+ img: np.ndarray,
359
+ window_size: Tuple[int, int, int] = (3, 3, 3),
360
+ mean_window_size: Tuple[int, int, int] = (3, 3, 3),
361
+ ) -> np.ndarray:
362
+ """
363
+ Apply local 2D Otsu thresholding to a 3D grayscale image.
364
+
365
+ For each pixel, computes an optimal threshold based on the intensity
366
+ distribution in a local neighborhood window, then either returns the
367
+ threshold values or a binary mask.
368
+
369
+ Parameters
370
+ ----------
371
+ img : np.ndarray
372
+ 3D input image with integer dtype (e.g., uint8, uint16, int32).
373
+ Shape should be (z, y, x).
374
+ window_size : tuple of int
375
+ Local neighborhood size as (z_size, y_size, x_size).
376
+ Should be positive odd integers for centered windows.
377
+ delta : float, default=0.2
378
+ Contrast parameter for Otsu thresholding. Lower values are more
379
+ permissive, higher values require stronger contrast.
380
+
381
+ Returns
382
+ -------
383
+ np.ndarray
384
+ If binarize=True: Binary mask with same shape as input (dtype=uint8)
385
+ If binarize=False: Threshold map with same shape as input (dtype=int32)
386
+
387
+ Raises
388
+ ------
389
+ ValueError
390
+ If input validation fails (wrong dimensions, dtype, window_size, etc.)
391
+
392
+ Examples
393
+ --------
394
+ >>> import numpy as np
395
+ >>> from Otsu2D import getBinary
396
+ >>>
397
+ >>> # Create sample 3D grayscale image
398
+ >>> img = np.random.randint(0, 256, size=(50, 50, 50), dtype=np.uint8)
399
+ >>>
400
+ >>> # Get binary mask
401
+ >>> mask = getBinary(img, window_size=(5, 5, 5), delta=0.2, binarize=True)
402
+ >>> print(mask.shape, mask.dtype)
403
+ (50, 50, 50) uint8
404
+ >>>
405
+ >>> # Get threshold values
406
+ >>> thresholds = getBinary(img, window_size=(5, 5, 5), delta=0.2, binarize=False)
407
+ >>> print(thresholds.shape, thresholds.dtype)
408
+ (50, 50, 50) int32
409
+
410
+ Notes
411
+ -----
412
+ - Uses reflect padding at image boundaries
413
+ - Parallelized using Numba for high performance
414
+ - Input must be integer dtype (float images should be converted first)
415
+ """
416
+ # Validation
417
+ if img.ndim != 3:
418
+ raise ValueError(
419
+ f"Expected 3D image, got {img.ndim}D array with shape {img.shape}"
420
+ )
421
+
422
+ if img.size == 0:
423
+ raise ValueError("Input image is empty")
424
+
425
+ _validate_window_size("window_size", window_size)
426
+ _validate_window_size("mean_window_size", mean_window_size)
427
+
428
+ # Call Numba implementation
429
+ return _getBinary(img, window_size, mean_window_size)
otsu2D/runner.py ADDED
@@ -0,0 +1,173 @@
1
+ # ============================================================================
2
+ # Command Line Runner for local 2D Otsu Thresholding
3
+ # ============================================================================
4
+
5
+ import argparse
6
+ import numpy as np
7
+ import os
8
+ import matplotlib.pyplot as plt
9
+ from otsu2D import getBinary
10
+
11
+
12
+ def load_image(image_path: str) -> np.ndarray:
13
+ """
14
+ Load a 3D image from a file (.npy or .tif/.tiff).
15
+ """
16
+ if not os.path.exists(image_path):
17
+ raise FileNotFoundError(f"Image file not found: {image_path}")
18
+
19
+ ext = os.path.splitext(image_path)[1].lower()
20
+
21
+ if ext == ".npy":
22
+ img = np.load(image_path)
23
+ elif ext in [".tif", ".tiff"]:
24
+ try:
25
+ import tifffile
26
+ except ImportError:
27
+ raise ImportError(
28
+ "tifffile is required to read TIFF images. "
29
+ "Install it via 'pip install tifffile'."
30
+ )
31
+ img = tifffile.imread(image_path)
32
+ else:
33
+ raise ValueError(
34
+ f"Unsupported file format: {ext}. Use .npy or .tif/.tiff files."
35
+ )
36
+
37
+ if img.ndim != 3:
38
+ raise ValueError(f"Expected a 3D image, got shape {img.shape}")
39
+
40
+ if not np.issubdtype(img.dtype, np.integer):
41
+ img = img.astype(np.uint8)
42
+
43
+ return img
44
+
45
+
46
+ def save_output(binary: np.ndarray, image_path: str, outdir: str, fmt: str):
47
+ """
48
+ Save binary arrays to the specified directory and format.
49
+ Filenames follow: {input_filename}_{threshold/binary}.{format}
50
+ """
51
+ os.makedirs(outdir, exist_ok=True)
52
+
53
+ # Extract base name (without extension)
54
+ base_name = os.path.splitext(os.path.basename(image_path))[0]
55
+
56
+ bin_path = os.path.join(outdir, f"{base_name}_binary.{fmt}")
57
+
58
+ if fmt == "npy":
59
+ np.save(bin_path, binary)
60
+ print(f"Saved NPY files:\n {th_path}\n {bin_path}")
61
+
62
+ elif fmt in ["tif", "tiff"]:
63
+ try:
64
+ import tifffile
65
+ except ImportError:
66
+ print("tifffile not installed — cannot save as TIFF. Saving as NPY instead.")
67
+ np.save(bin_path.replace(".tiff", ".npy"), binary)
68
+ return
69
+ tifffile.imwrite(bin_path, binary.astype(np.uint8))
70
+ print(f"Saved TIFF files:\n {bin_path}")
71
+
72
+ else:
73
+ raise ValueError(f"Unsupported format: {fmt}. Choose 'npy' or 'tiff'.")
74
+
75
+
76
+ def show_results(img: np.ndarray, binary: np.ndarray, z_index: int = None):
77
+ """
78
+ Show a chosen or middle slice for binary output.
79
+ """
80
+ if z_index is None:
81
+ z_index = img.shape[0] // 2
82
+
83
+ if z_index < 0 or z_index >= img.shape[0]:
84
+ raise ValueError(f"Invalid z-index {z_index}. Must be in range [0, {img.shape[0]-1}]")
85
+
86
+ fig, axes = plt.subplots(1, 2, figsize=(12, 6))
87
+ axes[0].imshow(img[z_index], cmap="gray")
88
+ axes[0].set_title(f"Original (z={z_index})")
89
+ axes[1].imshow(binary[z_index], cmap="gray")
90
+ axes[1].set_title("Binary Mask")
91
+ for ax in axes:
92
+ ax.axis("off")
93
+
94
+ plt.tight_layout()
95
+ plt.show()
96
+
97
+
98
+ def main():
99
+ parser = argparse.ArgumentParser(
100
+ description="Run local 2D Otsu Thresholding on a 3D image."
101
+ )
102
+ parser.add_argument(
103
+ "--image",
104
+ type=str,
105
+ required=True,
106
+ help="Path to a 3D image file (.npy or .tif/.tiff)",
107
+ )
108
+ parser.add_argument(
109
+ "--window",
110
+ type=int,
111
+ nargs=3,
112
+ default=[3, 3, 3],
113
+ help="Intensity window size (z y x). Default: 3 3 3",
114
+ )
115
+ parser.add_argument(
116
+ "--mean_window",
117
+ type=int,
118
+ nargs=3,
119
+ default=[3, 3, 3],
120
+ help="Local window size (z y x). Default: 3 3 3",
121
+ )
122
+ parser.add_argument(
123
+ "--outdir",
124
+ type=str,
125
+ default="outputs",
126
+ help="Directory to save outputs (default: ./outputs)",
127
+ )
128
+ parser.add_argument(
129
+ "--format",
130
+ type=str,
131
+ default="npy",
132
+ choices=["npy", "tif", "tiff"],
133
+ help="Output file format (default: npy)",
134
+ )
135
+ parser.add_argument(
136
+ "--show",
137
+ action="store_true",
138
+ help="Display binary slices using matplotlib",
139
+ )
140
+ parser.add_argument(
141
+ "--slice",
142
+ type=int,
143
+ default=None,
144
+ help="Optional z-index to visualize (default: middle slice)",
145
+ )
146
+
147
+ args = parser.parse_args()
148
+
149
+ # Load image
150
+ print(f"Loading image: {args.image}")
151
+ img = load_image(args.image)
152
+ print(f"Image loaded with shape {img.shape} and dtype {img.dtype}")
153
+
154
+ # Run local 2D Otsu Thresholding
155
+ print("Running modified Otsu thresholding...")
156
+ binary = getBinary(img, window_size=tuple(args.window), mean_window_size=tuple(args.mean_window))
157
+
158
+ # Print results
159
+ print("------------------------------------------------------------")
160
+ print(f"Binary mask shape: {binary.shape}")
161
+
162
+ # Save outputs
163
+ save_output(binary, args.image, args.outdir, args.format)
164
+
165
+ # Show outputs if requested
166
+ if args.show:
167
+ show_results(img, binary, args.slice)
168
+
169
+ print("Done.")
170
+
171
+
172
+ if __name__ == "__main__":
173
+ main()
@@ -0,0 +1,78 @@
1
+ Metadata-Version: 2.4
2
+ Name: otsu2D
3
+ Version: 0.1.0
4
+ Summary: Numba-accelerated local 2D Otsu thresholding for 3D images.
5
+ Author-email: John Rick Manzanares <jdolormanzanares@impan.pl>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/jhnrckmnznrs/otsu2D
8
+ Project-URL: Issues, https://github.com/jhnrckmnznrs/otsu2D/issues
9
+ Requires-Python: >=3.9
10
+ Description-Content-Type: text/markdown
11
+ License-File: LICENSE
12
+ Requires-Dist: numpy
13
+ Requires-Dist: numba
14
+ Provides-Extra: tiff
15
+ Requires-Dist: tifffile; extra == "tiff"
16
+ Requires-Dist: matplotlib; extra == "tiff"
17
+ Dynamic: license-file
18
+
19
+ # 🧠 Local 2D Otsu Thresholding
20
+
21
+ **Local 2D Otsu Thresholding** is a **Numba-accelerated** implementation of a *local* 2D Otsu thresholding method for 3D images.
22
+
23
+ This algorithm adapts the 2D Otsu threshold within a sliding 3D window, enabling robust segmentation in datasets with spatially varying intensity distributions.
24
+
25
+ ---
26
+
27
+ ## ⚙️ Installation
28
+
29
+ Install the latest stable version from PyPI:
30
+
31
+ ```bash
32
+ pip install otsu2D
33
+ ```
34
+
35
+ ## 🚀 Example Usage
36
+
37
+ ```python
38
+ import numpy as np
39
+ from otsu2D import getBinary
40
+
41
+ # Create sample 3D image
42
+ img = np.random.randint(0, 256, size=(10, 10, 10), dtype=np.uint8)
43
+
44
+ # Get threshold map and binary image
45
+ binary = getBinary(img, window_size=(3, 3, 3), mean_window_size =(3,3,3))
46
+ ```
47
+
48
+ ## 🖥️ Command Line Usage
49
+
50
+ You can execute the script with custom parameters:
51
+
52
+ ```bash
53
+ python runner.py --shape 10 10 10 --window 3 3 3 --mean_window 3 3 3
54
+ ```
55
+
56
+ ## 📦 Dependencies
57
+
58
+ - NumPy
59
+ - Numba
60
+ - Matplotlib
61
+
62
+ Install dependencies with:
63
+
64
+ ```bash
65
+ pip install numpy numba matplotlib
66
+ ```
67
+
68
+ ## 📜 License
69
+
70
+ This project is licensed under the MIT License. See the LICENSE file for details.
71
+
72
+ ## 🤝 Contributing
73
+
74
+ Contributions are welcome! If you'd like to fix a bug, add a feature, or improve performance, please open a pull request or contact the maintainers.
75
+
76
+ ## 💬 Contact
77
+
78
+ For questions, issues, or feedback, open an issue on GitHub.
@@ -0,0 +1,9 @@
1
+ otsu2D/__init__.py,sha256=S42dZ0oQtaV9DfkhczsUUp0PzkubutoBSY8FiS87IK0,1044
2
+ otsu2D/core.py,sha256=h1WiULgAq9P7hlr-ZpxXUIG_fnXac689kXedyTbTRrE,13711
3
+ otsu2D/runner.py,sha256=8Fg7dFFGTf-0OoJPFTSDHvDSWerCI9jo2V7O0Zo4dkg,5018
4
+ otsu2d-0.1.0.dist-info/licenses/LICENSE,sha256=nf6nNGBSlKqD389gsU-_DPce5IOf-lk_MceXNyoDpMg,1078
5
+ otsu2d-0.1.0.dist-info/METADATA,sha256=s42ciZlPyr2dsJYq8u6gp4hIk1LWJOttll0jpgtMPmA,1959
6
+ otsu2d-0.1.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
7
+ otsu2d-0.1.0.dist-info/entry_points.txt,sha256=2CwJDiPCVVABvTzuwg-STlo0tuHB5eC66pJgQPribRM,47
8
+ otsu2d-0.1.0.dist-info/top_level.txt,sha256=7kMNOiAsJJarPaCNtiNc8tOainJbQj2PPpvkbAFHE-A,7
9
+ otsu2d-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,2 @@
1
+ [console_scripts]
2
+ otsu-2d = otsu2D.runner:main
@@ -0,0 +1,22 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 John Rick Manzanares
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.
22
+
@@ -0,0 +1 @@
1
+ otsu2D