modifiedOtsu 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.
- modifiedOtsu/__init__.py +39 -0
- modifiedOtsu/core.py +291 -0
- modifiedOtsu/runner.py +179 -0
- modifiedotsu-0.1.0.dist-info/METADATA +88 -0
- modifiedotsu-0.1.0.dist-info/RECORD +9 -0
- modifiedotsu-0.1.0.dist-info/WHEEL +5 -0
- modifiedotsu-0.1.0.dist-info/entry_points.txt +2 -0
- modifiedotsu-0.1.0.dist-info/licenses/LICENSE +22 -0
- modifiedotsu-0.1.0.dist-info/top_level.txt +1 -0
modifiedOtsu/__init__.py
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
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 Otsu thresholding to get binary mask or thresholds
|
|
11
|
+
getThreshold : Compute Otsu threshold for 1D intensity values
|
|
12
|
+
|
|
13
|
+
Example
|
|
14
|
+
-------
|
|
15
|
+
>>> import numpy as np
|
|
16
|
+
>>> from modifiedOtsu import getMask
|
|
17
|
+
>>>
|
|
18
|
+
>>> # Create sample 3D image
|
|
19
|
+
>>> img = np.random.randint(0, 256, size=(50, 50, 50), dtype=np.uint8)
|
|
20
|
+
>>>
|
|
21
|
+
>>> # Get threshold mask and binary image
|
|
22
|
+
>>> threshold, binary = getMask(img, window_size=(3, 3, 3), delta=0.2)
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
from .core import getMask, 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 modifiedOtsu import *"
|
|
33
|
+
__all__ = [
|
|
34
|
+
"getBinary",
|
|
35
|
+
"getThreshold",
|
|
36
|
+
]
|
|
37
|
+
|
|
38
|
+
# Optional: Add convenience functions or constants
|
|
39
|
+
DEFAULT_WINDOW_SIZE = (3, 3, 3)
|
modifiedOtsu/core.py
ADDED
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Core implementation of modified Otsu thresholding.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import numpy as np
|
|
6
|
+
from numba import njit, prange
|
|
7
|
+
from typing import Tuple
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
# ============================================================================
|
|
11
|
+
# Helper Functions (Internal)
|
|
12
|
+
# ============================================================================
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@njit
|
|
16
|
+
def _padReflect(img: np.ndarray, pad_width: Tuple[int, int, int]) -> np.ndarray:
|
|
17
|
+
"""
|
|
18
|
+
Manual reflect padding for 3D arrays (Numba-compatible).
|
|
19
|
+
|
|
20
|
+
Parameters
|
|
21
|
+
----------
|
|
22
|
+
img : np.ndarray
|
|
23
|
+
3D input array
|
|
24
|
+
pad_width : tuple of int
|
|
25
|
+
Padding size for each dimension (z_pad, y_pad, x_pad)
|
|
26
|
+
|
|
27
|
+
Returns
|
|
28
|
+
-------
|
|
29
|
+
np.ndarray
|
|
30
|
+
Padded array with reflect mode
|
|
31
|
+
"""
|
|
32
|
+
z_pad, y_pad, x_pad = pad_width
|
|
33
|
+
z_max, y_max, x_max = img.shape
|
|
34
|
+
|
|
35
|
+
padded = np.zeros(
|
|
36
|
+
(z_max + 2 * z_pad, y_max + 2 * y_pad, x_max + 2 * x_pad), dtype=img.dtype
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
# Copy center
|
|
40
|
+
padded[z_pad : z_pad + z_max, y_pad : y_pad + y_max, x_pad : x_pad + x_max] = img
|
|
41
|
+
|
|
42
|
+
# Reflect padding for Z
|
|
43
|
+
for i in range(z_pad):
|
|
44
|
+
padded[z_pad - 1 - i, y_pad : y_pad + y_max, x_pad : x_pad + x_max] = img[
|
|
45
|
+
i, :, :
|
|
46
|
+
]
|
|
47
|
+
padded[z_pad + z_max + i, y_pad : y_pad + y_max, x_pad : x_pad + x_max] = img[
|
|
48
|
+
z_max - 1 - i, :, :
|
|
49
|
+
]
|
|
50
|
+
|
|
51
|
+
# Reflect padding for Y
|
|
52
|
+
for j in range(y_pad):
|
|
53
|
+
padded[:, y_pad - 1 - j, x_pad : x_pad + x_max] = padded[
|
|
54
|
+
:, y_pad + j, x_pad : x_pad + x_max
|
|
55
|
+
]
|
|
56
|
+
padded[:, y_pad + y_max + j, x_pad : x_pad + x_max] = padded[
|
|
57
|
+
:, y_pad + y_max - 1 - j, x_pad : x_pad + x_max
|
|
58
|
+
]
|
|
59
|
+
|
|
60
|
+
# Reflect padding for X
|
|
61
|
+
for k in range(x_pad):
|
|
62
|
+
padded[:, :, x_pad - 1 - k] = padded[:, :, x_pad + k]
|
|
63
|
+
padded[:, :, x_pad + x_max + k] = padded[:, :, x_pad + x_max - 1 - k]
|
|
64
|
+
|
|
65
|
+
return padded
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
@njit(parallel=True)
|
|
69
|
+
def _getMask(
|
|
70
|
+
img: np.ndarray, window_size: Tuple[int, int, int], delta: float
|
|
71
|
+
) -> np.ndarray:
|
|
72
|
+
"""
|
|
73
|
+
Internal Numba-compiled implementation.
|
|
74
|
+
No validation - assumes inputs are correct.
|
|
75
|
+
"""
|
|
76
|
+
z_win, y_win, x_win = window_size
|
|
77
|
+
z_pad, y_pad, x_pad = z_win // 2, y_win // 2, x_win // 2
|
|
78
|
+
|
|
79
|
+
padded = _padReflect(img, (z_pad, y_pad, x_pad))
|
|
80
|
+
mask = np.zeros_like(img, dtype=np.int32)
|
|
81
|
+
z_max, y_max, x_max = img.shape
|
|
82
|
+
|
|
83
|
+
for z in prange(z_max):
|
|
84
|
+
for y in range(y_max):
|
|
85
|
+
for x in range(x_max):
|
|
86
|
+
window = padded[z : z + z_win, y : y + y_win, x : x + x_win].ravel()
|
|
87
|
+
mask[z, y, x] = getThreshold(window, delta)
|
|
88
|
+
|
|
89
|
+
return mask, (img > mask)
|
|
90
|
+
|
|
91
|
+
# ============================================================================
|
|
92
|
+
# Public API
|
|
93
|
+
# ============================================================================
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
@njit
|
|
97
|
+
def getThreshold(values: np.ndarray, delta: float = 0.2) -> int:
|
|
98
|
+
"""
|
|
99
|
+
Compute Otsu threshold for a 1D array of intensity values.
|
|
100
|
+
|
|
101
|
+
This implementation assumes integer input values. The algorithm bins values
|
|
102
|
+
by their integer representation and computes the optimal threshold that
|
|
103
|
+
maximizes between-class variance.
|
|
104
|
+
|
|
105
|
+
Parameters
|
|
106
|
+
----------
|
|
107
|
+
values : np.ndarray
|
|
108
|
+
1D array of integer intensity values
|
|
109
|
+
delta : float, default=0.2
|
|
110
|
+
Contrast threshold. If the relative difference between foreground
|
|
111
|
+
and background means is below this threshold, the maximum value + 1
|
|
112
|
+
is returned (indicating insufficient contrast).
|
|
113
|
+
|
|
114
|
+
Returns
|
|
115
|
+
-------
|
|
116
|
+
int
|
|
117
|
+
The optimal threshold value, or max(values) + 1 if contrast is too low
|
|
118
|
+
|
|
119
|
+
Notes
|
|
120
|
+
-----
|
|
121
|
+
The algorithm:
|
|
122
|
+
1. Creates a histogram of input values
|
|
123
|
+
2. Iterates through possible thresholds
|
|
124
|
+
3. Computes between-class variance for each threshold
|
|
125
|
+
4. Returns the threshold with maximum variance
|
|
126
|
+
5. If contrast is too low (|mB - mF| / mB <= delta), returns max + 1
|
|
127
|
+
|
|
128
|
+
Examples
|
|
129
|
+
--------
|
|
130
|
+
>>> values = np.array([10, 10, 10, 20, 20, 30, 30, 30], dtype=np.int32)
|
|
131
|
+
>>> threshold = getThreshold(values, delta=0.2)
|
|
132
|
+
"""
|
|
133
|
+
mn = values.min()
|
|
134
|
+
mx = values.max()
|
|
135
|
+
|
|
136
|
+
if mx == mn:
|
|
137
|
+
return int(mx)
|
|
138
|
+
|
|
139
|
+
binned_values = (values - mn).astype(np.int32)
|
|
140
|
+
hist = np.bincount(binned_values, minlength=mx - mn + 1)
|
|
141
|
+
bin_edges = np.arange(len(hist)) + mn
|
|
142
|
+
|
|
143
|
+
total = values.size
|
|
144
|
+
sum_total = np.sum(bin_edges * hist)
|
|
145
|
+
sumB = 0.0
|
|
146
|
+
wB = 0
|
|
147
|
+
maximum = 0.0
|
|
148
|
+
threshold = 0
|
|
149
|
+
best_mB, best_mF = 0.0, 0.0
|
|
150
|
+
|
|
151
|
+
for i in range(len(hist)):
|
|
152
|
+
wB += hist[i]
|
|
153
|
+
if wB == 0:
|
|
154
|
+
continue
|
|
155
|
+
wF = total - wB
|
|
156
|
+
if wF == 0:
|
|
157
|
+
break
|
|
158
|
+
|
|
159
|
+
sumB += bin_edges[i] * hist[i]
|
|
160
|
+
mB = sumB / wB
|
|
161
|
+
mF = (sum_total - sumB) / wF
|
|
162
|
+
between = wB * wF * (mB - mF) ** 2
|
|
163
|
+
|
|
164
|
+
if between > maximum:
|
|
165
|
+
maximum = between
|
|
166
|
+
threshold = bin_edges[i]
|
|
167
|
+
best_mB = mB
|
|
168
|
+
best_mF = mF
|
|
169
|
+
|
|
170
|
+
# Force threshold to max if contrast is too low
|
|
171
|
+
if best_mB == 0 or abs(best_mB - best_mF) / best_mB <= delta:
|
|
172
|
+
threshold = mx + 1
|
|
173
|
+
|
|
174
|
+
return int(threshold)
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def getMask(
|
|
178
|
+
img: np.ndarray,
|
|
179
|
+
window_size: Tuple[int, int, int] = (3, 3, 3),
|
|
180
|
+
delta: float = 0.2
|
|
181
|
+
) -> np.ndarray:
|
|
182
|
+
"""
|
|
183
|
+
Apply local Otsu thresholding to a 3D grayscale image.
|
|
184
|
+
|
|
185
|
+
For each pixel, computes an optimal threshold based on the intensity
|
|
186
|
+
distribution in a local neighborhood window, then either returns the
|
|
187
|
+
threshold values or a binary mask.
|
|
188
|
+
|
|
189
|
+
Parameters
|
|
190
|
+
----------
|
|
191
|
+
img : np.ndarray
|
|
192
|
+
3D input image with integer dtype (e.g., uint8, uint16, int32).
|
|
193
|
+
Shape should be (z, y, x).
|
|
194
|
+
window_size : tuple of int
|
|
195
|
+
Local neighborhood size as (z_size, y_size, x_size).
|
|
196
|
+
Should be positive odd integers for centered windows.
|
|
197
|
+
delta : float, default=0.2
|
|
198
|
+
Contrast parameter for Otsu thresholding. Lower values are more
|
|
199
|
+
permissive, higher values require stronger contrast.
|
|
200
|
+
|
|
201
|
+
Returns
|
|
202
|
+
-------
|
|
203
|
+
np.ndarray
|
|
204
|
+
If binarize=True: Binary mask with same shape as input (dtype=uint8)
|
|
205
|
+
If binarize=False: Threshold map with same shape as input (dtype=int32)
|
|
206
|
+
|
|
207
|
+
Raises
|
|
208
|
+
------
|
|
209
|
+
ValueError
|
|
210
|
+
If input validation fails (wrong dimensions, dtype, window_size, etc.)
|
|
211
|
+
|
|
212
|
+
Examples
|
|
213
|
+
--------
|
|
214
|
+
>>> import numpy as np
|
|
215
|
+
>>> from modifiedOtsu import getMask
|
|
216
|
+
>>>
|
|
217
|
+
>>> # Create sample 3D grayscale image
|
|
218
|
+
>>> img = np.random.randint(0, 256, size=(50, 50, 50), dtype=np.uint8)
|
|
219
|
+
>>>
|
|
220
|
+
>>> # Get binary mask
|
|
221
|
+
>>> mask = getMask(img, window_size=(5, 5, 5), delta=0.2, binarize=True)
|
|
222
|
+
>>> print(mask.shape, mask.dtype)
|
|
223
|
+
(50, 50, 50) uint8
|
|
224
|
+
>>>
|
|
225
|
+
>>> # Get threshold values
|
|
226
|
+
>>> thresholds = getMask(img, window_size=(5, 5, 5), delta=0.2, binarize=False)
|
|
227
|
+
>>> print(thresholds.shape, thresholds.dtype)
|
|
228
|
+
(50, 50, 50) int32
|
|
229
|
+
|
|
230
|
+
Notes
|
|
231
|
+
-----
|
|
232
|
+
- Uses reflect padding at image boundaries
|
|
233
|
+
- Parallelized using Numba for high performance
|
|
234
|
+
- Input must be integer dtype (float images should be converted first)
|
|
235
|
+
"""
|
|
236
|
+
# Validation
|
|
237
|
+
if img.ndim != 3:
|
|
238
|
+
raise ValueError(
|
|
239
|
+
f"Expected 3D image, got {img.ndim}D array with shape {img.shape}"
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
if not np.issubdtype(img.dtype, np.integer):
|
|
243
|
+
raise ValueError(
|
|
244
|
+
f"Expected integer dtype (e.g., uint8, uint16, int32), got {img.dtype}. "
|
|
245
|
+
f"Convert using img.astype(np.uint8) or similar."
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
if not isinstance(window_size, (tuple, list)):
|
|
249
|
+
raise ValueError(
|
|
250
|
+
f"window_size must be a tuple or list, got {type(window_size)}"
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
if len(window_size) != 3:
|
|
254
|
+
raise ValueError(
|
|
255
|
+
f"window_size must have 3 elements (z, y, x), got {len(window_size)}"
|
|
256
|
+
)
|
|
257
|
+
|
|
258
|
+
if not all(isinstance(w, (int, np.integer)) for w in window_size):
|
|
259
|
+
raise ValueError(f"window_size elements must be integers, got {window_size}")
|
|
260
|
+
|
|
261
|
+
if any(w <= 0 for w in window_size):
|
|
262
|
+
raise ValueError(f"window_size elements must be positive, got {window_size}")
|
|
263
|
+
|
|
264
|
+
if any(w % 2 == 0 for w in window_size):
|
|
265
|
+
z_win, y_win, x_win = window_size
|
|
266
|
+
raise ValueError(
|
|
267
|
+
f"window_size elements should be odd numbers for centered windows, "
|
|
268
|
+
f"got {window_size}. Consider using "
|
|
269
|
+
f"({z_win+1 if z_win%2==0 else z_win}, "
|
|
270
|
+
f"{y_win+1 if y_win%2==0 else y_win}, "
|
|
271
|
+
f"{x_win+1 if x_win%2==0 else x_win}) instead."
|
|
272
|
+
)
|
|
273
|
+
|
|
274
|
+
z_max, y_max, x_max = img.shape
|
|
275
|
+
z_win, y_win, x_win = window_size
|
|
276
|
+
if z_win > z_max or y_win > y_max or x_win > x_max:
|
|
277
|
+
raise ValueError(
|
|
278
|
+
f"window_size {window_size} is larger than image dimensions {img.shape}"
|
|
279
|
+
)
|
|
280
|
+
|
|
281
|
+
if not isinstance(delta, (int, float)):
|
|
282
|
+
raise ValueError(f"delta must be a number, got {type(delta)}")
|
|
283
|
+
|
|
284
|
+
if delta < 0:
|
|
285
|
+
raise ValueError(f"delta must be non-negative, got {delta}")
|
|
286
|
+
|
|
287
|
+
if img.size == 0:
|
|
288
|
+
raise ValueError("Input image is empty")
|
|
289
|
+
|
|
290
|
+
# Call Numba implementation
|
|
291
|
+
return _getMask(img, window_size, delta)
|
modifiedOtsu/runner.py
ADDED
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
# ============================================================================
|
|
2
|
+
# Command Line Runner for Modified Otsu Thresholding
|
|
3
|
+
# ============================================================================
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import numpy as np
|
|
7
|
+
import os
|
|
8
|
+
import matplotlib.pyplot as plt
|
|
9
|
+
from modifiedOtsu import getMask
|
|
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(threshold: np.ndarray, binary: np.ndarray, image_path: str, outdir: str, fmt: str):
|
|
47
|
+
"""
|
|
48
|
+
Save threshold and 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
|
+
th_path = os.path.join(outdir, f"{base_name}_threshold.{fmt}")
|
|
57
|
+
bin_path = os.path.join(outdir, f"{base_name}_binary.{fmt}")
|
|
58
|
+
|
|
59
|
+
if fmt == "npy":
|
|
60
|
+
np.save(th_path, threshold)
|
|
61
|
+
np.save(bin_path, binary)
|
|
62
|
+
print(f"Saved NPY files:\n {th_path}\n {bin_path}")
|
|
63
|
+
|
|
64
|
+
elif fmt in ["tif", "tiff"]:
|
|
65
|
+
try:
|
|
66
|
+
import tifffile
|
|
67
|
+
except ImportError:
|
|
68
|
+
print("tifffile not installed — cannot save as TIFF. Saving as NPY instead.")
|
|
69
|
+
np.save(th_path.replace(".tiff", ".npy"), threshold)
|
|
70
|
+
np.save(bin_path.replace(".tiff", ".npy"), binary)
|
|
71
|
+
return
|
|
72
|
+
tifffile.imwrite(th_path, threshold.astype(np.uint16))
|
|
73
|
+
tifffile.imwrite(bin_path, binary.astype(np.uint8))
|
|
74
|
+
print(f"Saved TIFF files:\n {th_path}\n {bin_path}")
|
|
75
|
+
|
|
76
|
+
else:
|
|
77
|
+
raise ValueError(f"Unsupported format: {fmt}. Choose 'npy' or 'tiff'.")
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def show_results(img: np.ndarray, threshold: np.ndarray, binary: np.ndarray, z_index: int = None):
|
|
81
|
+
"""
|
|
82
|
+
Show a chosen or middle slice for threshold and binary output.
|
|
83
|
+
"""
|
|
84
|
+
if z_index is None:
|
|
85
|
+
z_index = img.shape[0] // 2
|
|
86
|
+
|
|
87
|
+
if z_index < 0 or z_index >= img.shape[0]:
|
|
88
|
+
raise ValueError(f"Invalid z-index {z_index}. Must be in range [0, {img.shape[0]-1}]")
|
|
89
|
+
|
|
90
|
+
fig, axes = plt.subplots(1, 3, figsize=(12, 4))
|
|
91
|
+
axes[0].imshow(img[z_index], cmap="gray")
|
|
92
|
+
axes[0].set_title(f"Original (z={z_index})")
|
|
93
|
+
axes[1].imshow(threshold[z_index], cmap="gray")
|
|
94
|
+
axes[1].set_title("Local Thresholds")
|
|
95
|
+
axes[2].imshow(binary[z_index], cmap="gray")
|
|
96
|
+
axes[2].set_title("Binary Mask")
|
|
97
|
+
for ax in axes:
|
|
98
|
+
ax.axis("off")
|
|
99
|
+
|
|
100
|
+
plt.tight_layout()
|
|
101
|
+
plt.show()
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def main():
|
|
105
|
+
parser = argparse.ArgumentParser(
|
|
106
|
+
description="Run Modified Otsu Thresholding on a 3D image."
|
|
107
|
+
)
|
|
108
|
+
parser.add_argument(
|
|
109
|
+
"--image",
|
|
110
|
+
type=str,
|
|
111
|
+
required=True,
|
|
112
|
+
help="Path to a 3D image file (.npy or .tif/.tiff)",
|
|
113
|
+
)
|
|
114
|
+
parser.add_argument(
|
|
115
|
+
"--window",
|
|
116
|
+
type=int,
|
|
117
|
+
nargs=3,
|
|
118
|
+
default=[3, 3, 3],
|
|
119
|
+
help="Local window size (z y x). Default: 3 3 3",
|
|
120
|
+
)
|
|
121
|
+
parser.add_argument(
|
|
122
|
+
"--delta",
|
|
123
|
+
type=float,
|
|
124
|
+
default=0.2,
|
|
125
|
+
help="Contrast delta parameter (default: 0.2)",
|
|
126
|
+
)
|
|
127
|
+
parser.add_argument(
|
|
128
|
+
"--outdir",
|
|
129
|
+
type=str,
|
|
130
|
+
default="outputs",
|
|
131
|
+
help="Directory to save outputs (default: ./outputs)",
|
|
132
|
+
)
|
|
133
|
+
parser.add_argument(
|
|
134
|
+
"--format",
|
|
135
|
+
type=str,
|
|
136
|
+
default="npy",
|
|
137
|
+
choices=["npy", "tif", "tiff"],
|
|
138
|
+
help="Output file format (default: npy)",
|
|
139
|
+
)
|
|
140
|
+
parser.add_argument(
|
|
141
|
+
"--show",
|
|
142
|
+
action="store_true",
|
|
143
|
+
help="Display threshold and binary slices using matplotlib",
|
|
144
|
+
)
|
|
145
|
+
parser.add_argument(
|
|
146
|
+
"--slice",
|
|
147
|
+
type=int,
|
|
148
|
+
default=None,
|
|
149
|
+
help="Optional z-index to visualize (default: middle slice)",
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
args = parser.parse_args()
|
|
153
|
+
|
|
154
|
+
# Load image
|
|
155
|
+
print(f"Loading image: {args.image}")
|
|
156
|
+
img = load_image(args.image)
|
|
157
|
+
print(f"Image loaded with shape {img.shape} and dtype {img.dtype}")
|
|
158
|
+
|
|
159
|
+
# Run Modified Otsu Thresholding
|
|
160
|
+
print("Running modified Otsu thresholding...")
|
|
161
|
+
threshold, binary = getMask(img, window_size=tuple(args.window), delta=args.delta)
|
|
162
|
+
|
|
163
|
+
# Print results
|
|
164
|
+
print("------------------------------------------------------------")
|
|
165
|
+
print(f"Threshold map shape: {threshold.shape}")
|
|
166
|
+
print(f"Binary mask shape: {binary.shape}")
|
|
167
|
+
|
|
168
|
+
# Save outputs
|
|
169
|
+
save_output(threshold, binary, args.image, args.outdir, args.format)
|
|
170
|
+
|
|
171
|
+
# Show outputs if requested
|
|
172
|
+
if args.show:
|
|
173
|
+
show_results(img, threshold, binary, args.slice)
|
|
174
|
+
|
|
175
|
+
print("Done.")
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
if __name__ == "__main__":
|
|
179
|
+
main()
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: modifiedOtsu
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Numba-accelerated local modified 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/modifiedOtsu
|
|
8
|
+
Project-URL: Issues, https://github.com/jhnrckmnznrs/modifiedOtsu/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 Modified Otsu Thresholding
|
|
20
|
+
|
|
21
|
+
**Local Modified Otsu Thresholding** is a **Numba-accelerated** implementation of a *local, contrast-adaptive* Otsu thresholding method for 3D images.
|
|
22
|
+
|
|
23
|
+
This algorithm adapts the Otsu threshold within a sliding 3D window, enabling robust segmentation in datasets with spatially varying intensity distributions.
|
|
24
|
+
|
|
25
|
+
The method is inspired by the segmentation approach described in **Subsection 2.2.2** of the article:
|
|
26
|
+
|
|
27
|
+
> *Segmentation-based tracking of macrophages in 2D + time microscopy movies inside a living animal*
|
|
28
|
+
>
|
|
29
|
+
> *Computers in Biology and Medicine, 153 (2023), 106499*
|
|
30
|
+
>
|
|
31
|
+
> [https://doi.org/10.1016/j.compbiomed.2022.106499](https://doi.org/10.1016/j.compbiomed.2022.106499)
|
|
32
|
+
|
|
33
|
+
If you use this implementation in your research, please cite the original work that inspired this algorithm.
|
|
34
|
+
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
## ⚙️ Installation
|
|
38
|
+
|
|
39
|
+
Install the latest stable version from PyPI:
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
pip install modifiedOtsu
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## 🚀 Example Usage
|
|
46
|
+
|
|
47
|
+
```python
|
|
48
|
+
import numpy as np
|
|
49
|
+
from modifiedOtsu import getMask
|
|
50
|
+
|
|
51
|
+
# Create sample 3D image
|
|
52
|
+
img = np.random.randint(0, 256, size=(10, 10, 10), dtype=np.uint8)
|
|
53
|
+
|
|
54
|
+
# Get threshold map and binary image
|
|
55
|
+
threshold, binary = getMask(img, window_size=(3, 3, 3), delta=0.2)
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## 🖥️ Command Line Usage
|
|
59
|
+
|
|
60
|
+
You can execute the script with custom parameters:
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
python runner.py --shape 10 10 10 --window 3 3 3 --delta 0.2
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## 📦 Dependencies
|
|
67
|
+
|
|
68
|
+
- NumPy
|
|
69
|
+
- Numba
|
|
70
|
+
- Matplotlib
|
|
71
|
+
|
|
72
|
+
Install dependencies with:
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
pip install numpy numba matplotlib
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## 📜 License
|
|
79
|
+
|
|
80
|
+
This project is licensed under the MIT License. See the LICENSE file for details.
|
|
81
|
+
|
|
82
|
+
## 🤝 Contributing
|
|
83
|
+
|
|
84
|
+
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.
|
|
85
|
+
|
|
86
|
+
## 💬 Contact
|
|
87
|
+
|
|
88
|
+
For questions, issues, or feedback, open an issue on GitHub.
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
modifiedOtsu/__init__.py,sha256=4RwEI6MGz0hNpPRQ3Fh9xGgQqmtfGPT3pf-ErYzEXe4,1032
|
|
2
|
+
modifiedOtsu/core.py,sha256=2iJNrTAwpxJCPQfS3WR4cOLih3Q4Z7l_R1mzawchDBE,9029
|
|
3
|
+
modifiedOtsu/runner.py,sha256=BFXSiqm7L7l0GZAuDygZ_R8PY_odAoZypLuaVanqEe8,5475
|
|
4
|
+
modifiedotsu-0.1.0.dist-info/licenses/LICENSE,sha256=nf6nNGBSlKqD389gsU-_DPce5IOf-lk_MceXNyoDpMg,1078
|
|
5
|
+
modifiedotsu-0.1.0.dist-info/METADATA,sha256=xbPoLCMlJP42CGF1qkwCZe-8RfH8xCVTKfJOBQ5WiBM,2484
|
|
6
|
+
modifiedotsu-0.1.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
7
|
+
modifiedotsu-0.1.0.dist-info/entry_points.txt,sha256=8PFdo6g2IrSC_Nid05rnpq7IWxkMBtlfyTQEdWJQbnk,59
|
|
8
|
+
modifiedotsu-0.1.0.dist-info/top_level.txt,sha256=kF51f_OBYTVQtFubEaNsrNW9R7HNUWKP_UYTAag-spM,13
|
|
9
|
+
modifiedotsu-0.1.0.dist-info/RECORD,,
|
|
@@ -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
|
+
modifiedOtsu
|