senoquant 1.0.0b1__py3-none-any.whl → 1.0.0b3__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.
- senoquant/__init__.py +6 -2
- senoquant/_reader.py +1 -1
- senoquant/reader/core.py +201 -18
- senoquant/tabs/batch/backend.py +18 -3
- senoquant/tabs/batch/frontend.py +8 -4
- senoquant/tabs/quantification/features/marker/dialog.py +26 -6
- senoquant/tabs/quantification/features/marker/export.py +97 -24
- senoquant/tabs/quantification/features/marker/rows.py +2 -2
- senoquant/tabs/quantification/features/spots/dialog.py +41 -11
- senoquant/tabs/quantification/features/spots/export.py +163 -10
- senoquant/tabs/quantification/frontend.py +2 -2
- senoquant/tabs/segmentation/frontend.py +46 -9
- senoquant/tabs/segmentation/models/cpsam/model.py +1 -1
- senoquant/tabs/segmentation/models/default_2d/model.py +22 -77
- senoquant/tabs/segmentation/models/default_3d/model.py +8 -74
- senoquant/tabs/segmentation/stardist_onnx_utils/_csbdeep/tools/create_zip_contents.py +0 -0
- senoquant/tabs/segmentation/stardist_onnx_utils/onnx_framework/inspect/probe.py +13 -13
- senoquant/tabs/segmentation/stardist_onnx_utils/onnx_framework/stardist_libs.py +171 -0
- senoquant/tabs/spots/frontend.py +42 -5
- senoquant/tabs/spots/models/ufish/details.json +17 -0
- senoquant/tabs/spots/models/ufish/model.py +129 -0
- senoquant/tabs/spots/ufish_utils/__init__.py +13 -0
- senoquant/tabs/spots/ufish_utils/core.py +357 -0
- senoquant/utils.py +1 -1
- senoquant-1.0.0b3.dist-info/METADATA +161 -0
- {senoquant-1.0.0b1.dist-info → senoquant-1.0.0b3.dist-info}/RECORD +41 -28
- {senoquant-1.0.0b1.dist-info → senoquant-1.0.0b3.dist-info}/top_level.txt +1 -0
- ufish/__init__.py +1 -0
- ufish/api.py +778 -0
- ufish/model/__init__.py +0 -0
- ufish/model/loss.py +62 -0
- ufish/model/network/__init__.py +0 -0
- ufish/model/network/spot_learn.py +50 -0
- ufish/model/network/ufish_net.py +204 -0
- ufish/model/train.py +175 -0
- ufish/utils/__init__.py +0 -0
- ufish/utils/img.py +418 -0
- ufish/utils/log.py +8 -0
- ufish/utils/spot_calling.py +115 -0
- senoquant/tabs/spots/models/rmp/details.json +0 -61
- senoquant/tabs/spots/models/rmp/model.py +0 -499
- senoquant/tabs/spots/models/udwt/details.json +0 -103
- senoquant/tabs/spots/models/udwt/model.py +0 -482
- senoquant-1.0.0b1.dist-info/METADATA +0 -193
- {senoquant-1.0.0b1.dist-info → senoquant-1.0.0b3.dist-info}/WHEEL +0 -0
- {senoquant-1.0.0b1.dist-info → senoquant-1.0.0b3.dist-info}/entry_points.txt +0 -0
- {senoquant-1.0.0b1.dist-info → senoquant-1.0.0b3.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,499 +0,0 @@
|
|
|
1
|
-
"""RMP spot detector implementation."""
|
|
2
|
-
|
|
3
|
-
from __future__ import annotations
|
|
4
|
-
|
|
5
|
-
from contextlib import contextmanager
|
|
6
|
-
from dataclasses import dataclass
|
|
7
|
-
from typing import Iterable
|
|
8
|
-
|
|
9
|
-
import numpy as np
|
|
10
|
-
from scipy import ndimage as ndi
|
|
11
|
-
from skimage.filters import threshold_otsu
|
|
12
|
-
from skimage.measure import label
|
|
13
|
-
from skimage.morphology import opening, rectangle
|
|
14
|
-
from skimage.segmentation import watershed
|
|
15
|
-
from skimage.feature import peak_local_max
|
|
16
|
-
from skimage.transform import rotate
|
|
17
|
-
from skimage.util import img_as_ubyte
|
|
18
|
-
|
|
19
|
-
from ..base import SenoQuantSpotDetector
|
|
20
|
-
from senoquant.utils import layer_data_asarray
|
|
21
|
-
|
|
22
|
-
try:
|
|
23
|
-
import dask.array as da
|
|
24
|
-
except ImportError: # pragma: no cover - optional dependency
|
|
25
|
-
da = None # type: ignore[assignment]
|
|
26
|
-
|
|
27
|
-
try: # pragma: no cover - optional dependency
|
|
28
|
-
from dask.distributed import Client, LocalCluster
|
|
29
|
-
except ImportError: # pragma: no cover - optional dependency
|
|
30
|
-
Client = None # type: ignore[assignment]
|
|
31
|
-
LocalCluster = None # type: ignore[assignment]
|
|
32
|
-
|
|
33
|
-
try: # pragma: no cover - optional dependency
|
|
34
|
-
from dask_cuda import LocalCUDACluster
|
|
35
|
-
except ImportError: # pragma: no cover - optional dependency
|
|
36
|
-
LocalCUDACluster = None # type: ignore[assignment]
|
|
37
|
-
|
|
38
|
-
try: # pragma: no cover - optional dependency
|
|
39
|
-
import cupy as cp
|
|
40
|
-
from cucim.skimage.filters import threshold_otsu as gpu_threshold_otsu
|
|
41
|
-
from cucim.skimage.morphology import opening as gpu_opening, rectangle as gpu_rectangle
|
|
42
|
-
from cucim.skimage.transform import rotate as gpu_rotate
|
|
43
|
-
except ImportError: # pragma: no cover - optional dependency
|
|
44
|
-
cp = None # type: ignore[assignment]
|
|
45
|
-
gpu_threshold_otsu = None # type: ignore[assignment]
|
|
46
|
-
gpu_opening = None # type: ignore[assignment]
|
|
47
|
-
gpu_rectangle = None # type: ignore[assignment]
|
|
48
|
-
gpu_rotate = None # type: ignore[assignment]
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
Array2D = np.ndarray
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
def _normalize_image(image: np.ndarray) -> np.ndarray:
|
|
55
|
-
"""Normalize an image to float32 in [0, 1]."""
|
|
56
|
-
data = np.asarray(image, dtype=np.float32)
|
|
57
|
-
min_val = float(data.min())
|
|
58
|
-
max_val = float(data.max())
|
|
59
|
-
if max_val <= min_val:
|
|
60
|
-
return np.zeros_like(data, dtype=np.float32)
|
|
61
|
-
data = (data - min_val) / (max_val - min_val)
|
|
62
|
-
return np.clip(data, 0.0, 1.0)
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
def _pad_for_rotation(image: Array2D) -> tuple[Array2D, tuple[int, int]]:
|
|
66
|
-
"""Pad image to preserve content after rotations."""
|
|
67
|
-
nrows, ncols = image.shape[:2]
|
|
68
|
-
diagonal = int(np.ceil(np.sqrt(nrows**2 + ncols**2)))
|
|
69
|
-
|
|
70
|
-
rows_to_pad = int(np.ceil((diagonal - nrows) / 2))
|
|
71
|
-
cols_to_pad = int(np.ceil((diagonal - ncols) / 2))
|
|
72
|
-
|
|
73
|
-
padded_image = np.pad(
|
|
74
|
-
image,
|
|
75
|
-
((rows_to_pad, rows_to_pad), (cols_to_pad, cols_to_pad)),
|
|
76
|
-
mode="reflect",
|
|
77
|
-
)
|
|
78
|
-
|
|
79
|
-
return padded_image, (rows_to_pad, cols_to_pad)
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
def _rmp_opening(
|
|
83
|
-
input_image: Array2D,
|
|
84
|
-
structuring_element: Array2D,
|
|
85
|
-
rotation_angles: Iterable[int],
|
|
86
|
-
) -> Array2D:
|
|
87
|
-
"""Perform the RMP opening on an image."""
|
|
88
|
-
padded_image, (newy, newx) = _pad_for_rotation(input_image)
|
|
89
|
-
rotated_images = [
|
|
90
|
-
rotate(padded_image, angle, mode="reflect") for angle in rotation_angles
|
|
91
|
-
]
|
|
92
|
-
opened_images = [
|
|
93
|
-
opening(image, footprint=structuring_element, mode="reflect")
|
|
94
|
-
for image in rotated_images
|
|
95
|
-
]
|
|
96
|
-
rotated_back = [
|
|
97
|
-
rotate(image, -angle, mode="reflect")
|
|
98
|
-
for image, angle in zip(opened_images, rotation_angles)
|
|
99
|
-
]
|
|
100
|
-
|
|
101
|
-
stacked_images = np.stack(rotated_back, axis=0)
|
|
102
|
-
union_image = np.max(stacked_images, axis=0)
|
|
103
|
-
cropped = union_image[
|
|
104
|
-
newy : newy + input_image.shape[0],
|
|
105
|
-
newx : newx + input_image.shape[1],
|
|
106
|
-
]
|
|
107
|
-
return cropped
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
def _rmp_top_hat(
|
|
111
|
-
input_image: Array2D,
|
|
112
|
-
structuring_element: Array2D,
|
|
113
|
-
rotation_angles: Iterable[int],
|
|
114
|
-
) -> Array2D:
|
|
115
|
-
"""Return the top-hat (background subtracted) image."""
|
|
116
|
-
opened_image = _rmp_opening(input_image, structuring_element, rotation_angles)
|
|
117
|
-
return input_image - opened_image
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
def _compute_top_hat(input_image: Array2D, config: "RMPSettings") -> Array2D:
|
|
121
|
-
"""Compute the RMP top-hat response for a 2D image."""
|
|
122
|
-
denoising_se = rectangle(1, config.denoising_se_length)
|
|
123
|
-
extraction_se = rectangle(1, config.extraction_se_length)
|
|
124
|
-
rotation_angles = tuple(range(0, 180, config.angle_spacing))
|
|
125
|
-
|
|
126
|
-
working = (
|
|
127
|
-
_rmp_opening(input_image, denoising_se, rotation_angles)
|
|
128
|
-
if config.enable_denoising
|
|
129
|
-
else input_image
|
|
130
|
-
)
|
|
131
|
-
return _rmp_top_hat(working, extraction_se, rotation_angles)
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
def _binary_to_instances(mask: np.ndarray, start_label: int = 1) -> tuple[np.ndarray, int]:
|
|
135
|
-
"""Convert a binary mask to instance labels.
|
|
136
|
-
|
|
137
|
-
Parameters
|
|
138
|
-
----------
|
|
139
|
-
mask : numpy.ndarray
|
|
140
|
-
Binary mask where foreground pixels are non-zero.
|
|
141
|
-
start_label : int, optional
|
|
142
|
-
Starting label index for the output. Defaults to 1.
|
|
143
|
-
|
|
144
|
-
Returns
|
|
145
|
-
-------
|
|
146
|
-
numpy.ndarray
|
|
147
|
-
Labeled instance mask.
|
|
148
|
-
int
|
|
149
|
-
Next label value after the labeled mask.
|
|
150
|
-
"""
|
|
151
|
-
labeled = label(mask > 0)
|
|
152
|
-
if start_label > 1 and labeled.max() > 0:
|
|
153
|
-
labeled = labeled + (start_label - 1)
|
|
154
|
-
next_label = int(labeled.max()) + 1
|
|
155
|
-
return labeled.astype(np.int32, copy=False), next_label
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
def _watershed_instances(
|
|
159
|
-
image: np.ndarray,
|
|
160
|
-
binary: np.ndarray,
|
|
161
|
-
min_distance: int,
|
|
162
|
-
) -> np.ndarray:
|
|
163
|
-
"""Split touching spots using watershed segmentation."""
|
|
164
|
-
if not np.any(binary):
|
|
165
|
-
return np.zeros_like(binary, dtype=np.int32)
|
|
166
|
-
if not np.any(~binary):
|
|
167
|
-
labeled, _ = _binary_to_instances(binary)
|
|
168
|
-
return labeled
|
|
169
|
-
|
|
170
|
-
distance = ndi.distance_transform_edt(binary)
|
|
171
|
-
coordinates = peak_local_max(
|
|
172
|
-
distance,
|
|
173
|
-
labels=binary.astype(np.uint8),
|
|
174
|
-
min_distance=max(1, int(min_distance)),
|
|
175
|
-
exclude_border=False,
|
|
176
|
-
)
|
|
177
|
-
if coordinates.size == 0:
|
|
178
|
-
labeled, _ = _binary_to_instances(binary)
|
|
179
|
-
return labeled
|
|
180
|
-
|
|
181
|
-
peaks = np.zeros(binary.shape, dtype=bool)
|
|
182
|
-
peaks[tuple(coordinates.T)] = True
|
|
183
|
-
markers = label(peaks).astype(np.int32, copy=False)
|
|
184
|
-
if markers.max() == 0:
|
|
185
|
-
labeled, _ = _binary_to_instances(binary)
|
|
186
|
-
return labeled
|
|
187
|
-
|
|
188
|
-
labels = watershed(-distance, markers, mask=binary)
|
|
189
|
-
return labels.astype(np.int32, copy=False)
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
def _ensure_dask_available() -> None:
|
|
193
|
-
"""Ensure dask is installed for tiled execution."""
|
|
194
|
-
if da is None: # pragma: no cover - import guard
|
|
195
|
-
raise ImportError("dask is required for distributed spot detection.")
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
def _ensure_distributed_available() -> None:
|
|
199
|
-
"""Ensure dask.distributed is installed for distributed execution."""
|
|
200
|
-
if Client is None or LocalCluster is None: # pragma: no cover - import guard
|
|
201
|
-
raise ImportError("dask.distributed is required for distributed execution.")
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
def _ensure_cupy_available() -> None:
|
|
205
|
-
"""Ensure CuPy and cuCIM are installed for GPU execution."""
|
|
206
|
-
if (
|
|
207
|
-
cp is None
|
|
208
|
-
or gpu_threshold_otsu is None
|
|
209
|
-
or gpu_opening is None
|
|
210
|
-
or gpu_rectangle is None
|
|
211
|
-
or gpu_rotate is None
|
|
212
|
-
): # pragma: no cover - import guard
|
|
213
|
-
raise ImportError("cupy + cucim are required for GPU execution.")
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
def _dask_available() -> bool:
|
|
217
|
-
"""Return True when dask is available."""
|
|
218
|
-
return da is not None
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
def _distributed_available() -> bool:
|
|
222
|
-
"""Return True when dask.distributed is available."""
|
|
223
|
-
return Client is not None and LocalCluster is not None and da is not None
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
def _gpu_available() -> bool:
|
|
227
|
-
"""Return True when CuPy/cuCIM are available for GPU execution."""
|
|
228
|
-
return (
|
|
229
|
-
cp is not None
|
|
230
|
-
and gpu_threshold_otsu is not None
|
|
231
|
-
and gpu_opening is not None
|
|
232
|
-
and gpu_rectangle is not None
|
|
233
|
-
and gpu_rotate is not None
|
|
234
|
-
and da is not None
|
|
235
|
-
)
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
def _recommended_overlap(config: "RMPSettings") -> int:
|
|
239
|
-
"""Derive a suitable overlap from structuring-element sizes."""
|
|
240
|
-
lengths = [config.extraction_se_length]
|
|
241
|
-
if config.enable_denoising:
|
|
242
|
-
lengths.append(config.denoising_se_length)
|
|
243
|
-
return max(1, max(lengths) * 2)
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
@contextmanager
|
|
247
|
-
def _cluster_client(use_gpu: bool):
|
|
248
|
-
"""Yield a connected Dask client backed by a local cluster."""
|
|
249
|
-
_ensure_distributed_available()
|
|
250
|
-
|
|
251
|
-
use_cuda_cluster = bool(use_gpu and cp is not None and LocalCUDACluster is not None)
|
|
252
|
-
cluster_cls = LocalCUDACluster if use_cuda_cluster else LocalCluster
|
|
253
|
-
with cluster_cls() as cluster: # type: ignore[call-arg]
|
|
254
|
-
with Client(cluster) as client:
|
|
255
|
-
yield client
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
def _cpu_top_hat_block(block: np.ndarray, config: "RMPSettings") -> np.ndarray:
|
|
259
|
-
"""Return background-subtracted tile via the RMP top-hat pipeline."""
|
|
260
|
-
denoising_se = rectangle(1, config.denoising_se_length)
|
|
261
|
-
extraction_se = rectangle(1, config.extraction_se_length)
|
|
262
|
-
rotation_angles = tuple(range(0, 180, config.angle_spacing))
|
|
263
|
-
|
|
264
|
-
working = (
|
|
265
|
-
_rmp_opening(block, denoising_se, rotation_angles)
|
|
266
|
-
if config.enable_denoising
|
|
267
|
-
else block
|
|
268
|
-
)
|
|
269
|
-
top_hat = working - _rmp_opening(working, extraction_se, rotation_angles)
|
|
270
|
-
return np.asarray(top_hat, dtype=np.float32)
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
def _gpu_pad_for_rotation(image: "cp.ndarray") -> tuple["cp.ndarray", tuple[int, int]]:
|
|
274
|
-
nrows, ncols = image.shape[:2]
|
|
275
|
-
diagonal = int(cp.ceil(cp.sqrt(nrows**2 + ncols**2)).item())
|
|
276
|
-
rows_to_pad = int(cp.ceil((diagonal - nrows) / 2).item())
|
|
277
|
-
cols_to_pad = int(cp.ceil((diagonal - ncols) / 2).item())
|
|
278
|
-
padded = cp.pad(
|
|
279
|
-
image,
|
|
280
|
-
((rows_to_pad, rows_to_pad), (cols_to_pad, cols_to_pad)),
|
|
281
|
-
mode="reflect",
|
|
282
|
-
)
|
|
283
|
-
return padded, (rows_to_pad, cols_to_pad)
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
def _gpu_rmp_opening(
|
|
287
|
-
image: "cp.ndarray",
|
|
288
|
-
structuring_element: "cp.ndarray",
|
|
289
|
-
rotation_angles: Iterable[int],
|
|
290
|
-
) -> "cp.ndarray":
|
|
291
|
-
padded, (newy, newx) = _gpu_pad_for_rotation(image)
|
|
292
|
-
rotated = [gpu_rotate(padded, angle, mode="reflect") for angle in rotation_angles]
|
|
293
|
-
opened = [
|
|
294
|
-
gpu_opening(img, footprint=structuring_element, mode="reflect")
|
|
295
|
-
for img in rotated
|
|
296
|
-
]
|
|
297
|
-
rotated_back = [
|
|
298
|
-
gpu_rotate(img, -angle, mode="reflect")
|
|
299
|
-
for img, angle in zip(opened, rotation_angles)
|
|
300
|
-
]
|
|
301
|
-
|
|
302
|
-
stacked = cp.stack(rotated_back, axis=0)
|
|
303
|
-
union = cp.max(stacked, axis=0)
|
|
304
|
-
return union[newy : newy + image.shape[0], newx : newx + image.shape[1]]
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
def _gpu_top_hat(block: np.ndarray, config: "RMPSettings") -> np.ndarray:
|
|
308
|
-
"""CuPy-backed RMP top-hat for a single tile."""
|
|
309
|
-
_ensure_cupy_available()
|
|
310
|
-
|
|
311
|
-
gpu_block = cp.asarray(block, dtype=cp.float32)
|
|
312
|
-
denoising_se = gpu_rectangle(1, config.denoising_se_length)
|
|
313
|
-
extraction_se = gpu_rectangle(1, config.extraction_se_length)
|
|
314
|
-
rotation_angles = tuple(range(0, 180, config.angle_spacing))
|
|
315
|
-
|
|
316
|
-
working = (
|
|
317
|
-
_gpu_rmp_opening(gpu_block, denoising_se, rotation_angles)
|
|
318
|
-
if config.enable_denoising
|
|
319
|
-
else gpu_block
|
|
320
|
-
)
|
|
321
|
-
top_hat = working - _gpu_rmp_opening(working, extraction_se, rotation_angles)
|
|
322
|
-
return cp.asnumpy(top_hat).astype(np.float32, copy=False)
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
def _rmp_top_hat_tiled(
|
|
326
|
-
image: np.ndarray,
|
|
327
|
-
config: "RMPSettings",
|
|
328
|
-
chunk_size: tuple[int, int] = (1024, 1024),
|
|
329
|
-
overlap: int | None = None,
|
|
330
|
-
use_gpu: bool = False,
|
|
331
|
-
distributed: bool = False,
|
|
332
|
-
client: "Client | None" = None,
|
|
333
|
-
) -> np.ndarray:
|
|
334
|
-
"""Return the RMP top-hat image using tiled execution."""
|
|
335
|
-
_ensure_dask_available()
|
|
336
|
-
if use_gpu:
|
|
337
|
-
_ensure_cupy_available()
|
|
338
|
-
|
|
339
|
-
effective_overlap = _recommended_overlap(config) if overlap is None else overlap
|
|
340
|
-
|
|
341
|
-
if use_gpu:
|
|
342
|
-
|
|
343
|
-
def block_fn(block, block_info=None):
|
|
344
|
-
return _gpu_top_hat(block, config)
|
|
345
|
-
|
|
346
|
-
else:
|
|
347
|
-
|
|
348
|
-
def block_fn(block, block_info=None):
|
|
349
|
-
return _cpu_top_hat_block(block, config)
|
|
350
|
-
|
|
351
|
-
arr = da.from_array(image.astype(np.float32, copy=False), chunks=chunk_size)
|
|
352
|
-
result = arr.map_overlap(
|
|
353
|
-
block_fn,
|
|
354
|
-
depth=(effective_overlap, effective_overlap),
|
|
355
|
-
boundary="reflect",
|
|
356
|
-
dtype=np.float32,
|
|
357
|
-
trim=True,
|
|
358
|
-
)
|
|
359
|
-
|
|
360
|
-
if distributed:
|
|
361
|
-
_ensure_distributed_available()
|
|
362
|
-
if client is None:
|
|
363
|
-
with _cluster_client(use_gpu) as temp_client:
|
|
364
|
-
return temp_client.compute(result).result()
|
|
365
|
-
return client.compute(result).result()
|
|
366
|
-
|
|
367
|
-
return result.compute()
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
@dataclass(slots=True)
|
|
371
|
-
class RMPSettings:
|
|
372
|
-
"""Configuration for the RMP detector."""
|
|
373
|
-
|
|
374
|
-
denoising_se_length: int = 2
|
|
375
|
-
extraction_se_length: int = 10
|
|
376
|
-
angle_spacing: int = 5
|
|
377
|
-
auto_threshold: bool = True
|
|
378
|
-
manual_threshold: float = 0.05
|
|
379
|
-
enable_denoising: bool = True
|
|
380
|
-
use_3d: bool = False
|
|
381
|
-
|
|
382
|
-
class RMPDetector(SenoQuantSpotDetector):
|
|
383
|
-
"""RMP spot detector implementation."""
|
|
384
|
-
|
|
385
|
-
def __init__(self, models_root=None) -> None:
|
|
386
|
-
super().__init__("rmp", models_root=models_root)
|
|
387
|
-
|
|
388
|
-
def run(self, **kwargs) -> dict:
|
|
389
|
-
"""Run the RMP detector and return instance labels.
|
|
390
|
-
|
|
391
|
-
Parameters
|
|
392
|
-
----------
|
|
393
|
-
**kwargs
|
|
394
|
-
layer : napari.layers.Image or None
|
|
395
|
-
Image layer used for spot detection.
|
|
396
|
-
settings : dict
|
|
397
|
-
Detector settings keyed by the details.json schema.
|
|
398
|
-
|
|
399
|
-
Returns
|
|
400
|
-
-------
|
|
401
|
-
dict
|
|
402
|
-
Dictionary with ``mask`` key containing instance labels.
|
|
403
|
-
"""
|
|
404
|
-
layer = kwargs.get("layer")
|
|
405
|
-
if layer is None:
|
|
406
|
-
return {"mask": None, "points": None}
|
|
407
|
-
if getattr(layer, "rgb", False):
|
|
408
|
-
raise ValueError("RMP requires single-channel images.")
|
|
409
|
-
|
|
410
|
-
settings = kwargs.get("settings", {})
|
|
411
|
-
manual_threshold = float(settings.get("manual_threshold", 0.5))
|
|
412
|
-
manual_threshold = max(0.0, min(1.0, manual_threshold))
|
|
413
|
-
config = RMPSettings(
|
|
414
|
-
denoising_se_length=int(settings.get("denoising_kernel_length", 2)),
|
|
415
|
-
extraction_se_length=int(settings.get("extraction_kernel_length", 10)),
|
|
416
|
-
angle_spacing=int(settings.get("angle_spacing", 5)),
|
|
417
|
-
auto_threshold=bool(settings.get("auto_threshold", True)),
|
|
418
|
-
manual_threshold=manual_threshold,
|
|
419
|
-
enable_denoising=bool(settings.get("enable_denoising", True)),
|
|
420
|
-
use_3d=bool(settings.get("use_3d", False)),
|
|
421
|
-
)
|
|
422
|
-
|
|
423
|
-
if config.angle_spacing <= 0:
|
|
424
|
-
raise ValueError("Angle spacing must be positive.")
|
|
425
|
-
if config.denoising_se_length <= 0 or config.extraction_se_length <= 0:
|
|
426
|
-
raise ValueError("Structuring element lengths must be positive.")
|
|
427
|
-
|
|
428
|
-
data = layer_data_asarray(layer)
|
|
429
|
-
if data.ndim not in (2, 3):
|
|
430
|
-
raise ValueError("RMP expects 2D images or 3D stacks.")
|
|
431
|
-
|
|
432
|
-
normalized = _normalize_image(data)
|
|
433
|
-
if normalized.ndim == 3 and not config.use_3d:
|
|
434
|
-
raise ValueError("Enable 3D to process stacks.")
|
|
435
|
-
|
|
436
|
-
use_distributed = _distributed_available()
|
|
437
|
-
use_gpu = _gpu_available()
|
|
438
|
-
use_tiled = _dask_available() and (use_distributed or use_gpu)
|
|
439
|
-
|
|
440
|
-
if normalized.ndim == 2:
|
|
441
|
-
image_2d = normalized
|
|
442
|
-
if use_tiled:
|
|
443
|
-
top_hat = _rmp_top_hat_tiled(
|
|
444
|
-
image_2d,
|
|
445
|
-
config=config,
|
|
446
|
-
use_gpu=use_gpu,
|
|
447
|
-
distributed=use_distributed,
|
|
448
|
-
)
|
|
449
|
-
else:
|
|
450
|
-
top_hat = _compute_top_hat(image_2d, config)
|
|
451
|
-
|
|
452
|
-
threshold = (
|
|
453
|
-
threshold_otsu(top_hat)
|
|
454
|
-
if config.auto_threshold
|
|
455
|
-
else config.manual_threshold
|
|
456
|
-
)
|
|
457
|
-
binary = img_as_ubyte(top_hat > threshold)
|
|
458
|
-
labels = _watershed_instances(
|
|
459
|
-
top_hat,
|
|
460
|
-
binary > 0,
|
|
461
|
-
min_distance=max(1, config.extraction_se_length // 2),
|
|
462
|
-
)
|
|
463
|
-
return {"mask": labels}
|
|
464
|
-
|
|
465
|
-
top_hat_stack = np.zeros_like(normalized, dtype=np.float32)
|
|
466
|
-
if use_tiled and use_distributed:
|
|
467
|
-
with _cluster_client(use_gpu) as client:
|
|
468
|
-
for z in range(normalized.shape[0]):
|
|
469
|
-
top_hat_stack[z] = _rmp_top_hat_tiled(
|
|
470
|
-
normalized[z],
|
|
471
|
-
config=config,
|
|
472
|
-
use_gpu=use_gpu,
|
|
473
|
-
distributed=True,
|
|
474
|
-
client=client,
|
|
475
|
-
)
|
|
476
|
-
elif use_tiled:
|
|
477
|
-
for z in range(normalized.shape[0]):
|
|
478
|
-
top_hat_stack[z] = _rmp_top_hat_tiled(
|
|
479
|
-
normalized[z],
|
|
480
|
-
config=config,
|
|
481
|
-
use_gpu=use_gpu,
|
|
482
|
-
distributed=False,
|
|
483
|
-
)
|
|
484
|
-
else:
|
|
485
|
-
for z in range(normalized.shape[0]):
|
|
486
|
-
top_hat_stack[z] = _compute_top_hat(normalized[z], config)
|
|
487
|
-
|
|
488
|
-
threshold = (
|
|
489
|
-
threshold_otsu(top_hat_stack)
|
|
490
|
-
if config.auto_threshold
|
|
491
|
-
else config.manual_threshold
|
|
492
|
-
)
|
|
493
|
-
binary_stack = img_as_ubyte(top_hat_stack > threshold)
|
|
494
|
-
labels = _watershed_instances(
|
|
495
|
-
top_hat_stack,
|
|
496
|
-
binary_stack > 0,
|
|
497
|
-
min_distance=max(1, config.extraction_se_length // 2),
|
|
498
|
-
)
|
|
499
|
-
return {"mask": labels}
|
|
@@ -1,103 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "udwt",
|
|
3
|
-
"description": "Undecimated B3-spline wavelet spot detector",
|
|
4
|
-
"version": "0.1.0",
|
|
5
|
-
"order": 1,
|
|
6
|
-
"settings": [
|
|
7
|
-
{
|
|
8
|
-
"key": "ld",
|
|
9
|
-
"label": "Product threshold (ld)",
|
|
10
|
-
"type": "float",
|
|
11
|
-
"decimals": 2,
|
|
12
|
-
"min": 0.0,
|
|
13
|
-
"max": 10.0,
|
|
14
|
-
"default": 1.0
|
|
15
|
-
},
|
|
16
|
-
{
|
|
17
|
-
"key": "force_2d",
|
|
18
|
-
"label": "Force 2D wavelets for 3D",
|
|
19
|
-
"type": "bool",
|
|
20
|
-
"default": false
|
|
21
|
-
},
|
|
22
|
-
{
|
|
23
|
-
"key": "scale_1_enabled",
|
|
24
|
-
"label": "Enable scale 1",
|
|
25
|
-
"type": "bool",
|
|
26
|
-
"default": true
|
|
27
|
-
},
|
|
28
|
-
{
|
|
29
|
-
"key": "scale_1_sensitivity",
|
|
30
|
-
"label": "Scale 1 sensitivity",
|
|
31
|
-
"type": "float",
|
|
32
|
-
"decimals": 1,
|
|
33
|
-
"min": 1.0,
|
|
34
|
-
"max": 100.0,
|
|
35
|
-
"default": 100.0,
|
|
36
|
-
"enabled_by": "scale_1_enabled"
|
|
37
|
-
},
|
|
38
|
-
{
|
|
39
|
-
"key": "scale_2_enabled",
|
|
40
|
-
"label": "Enable scale 2",
|
|
41
|
-
"type": "bool",
|
|
42
|
-
"default": true
|
|
43
|
-
},
|
|
44
|
-
{
|
|
45
|
-
"key": "scale_2_sensitivity",
|
|
46
|
-
"label": "Scale 2 sensitivity",
|
|
47
|
-
"type": "float",
|
|
48
|
-
"decimals": 1,
|
|
49
|
-
"min": 1.0,
|
|
50
|
-
"max": 100.0,
|
|
51
|
-
"default": 100.0,
|
|
52
|
-
"enabled_by": "scale_2_enabled"
|
|
53
|
-
},
|
|
54
|
-
{
|
|
55
|
-
"key": "scale_3_enabled",
|
|
56
|
-
"label": "Enable scale 3",
|
|
57
|
-
"type": "bool",
|
|
58
|
-
"default": true
|
|
59
|
-
},
|
|
60
|
-
{
|
|
61
|
-
"key": "scale_3_sensitivity",
|
|
62
|
-
"label": "Scale 3 sensitivity",
|
|
63
|
-
"type": "float",
|
|
64
|
-
"decimals": 1,
|
|
65
|
-
"min": 1.0,
|
|
66
|
-
"max": 100.0,
|
|
67
|
-
"default": 100.0,
|
|
68
|
-
"enabled_by": "scale_3_enabled"
|
|
69
|
-
},
|
|
70
|
-
{
|
|
71
|
-
"key": "scale_4_enabled",
|
|
72
|
-
"label": "Enable scale 4",
|
|
73
|
-
"type": "bool",
|
|
74
|
-
"default": false
|
|
75
|
-
},
|
|
76
|
-
{
|
|
77
|
-
"key": "scale_4_sensitivity",
|
|
78
|
-
"label": "Scale 4 sensitivity",
|
|
79
|
-
"type": "float",
|
|
80
|
-
"decimals": 1,
|
|
81
|
-
"min": 1.0,
|
|
82
|
-
"max": 100.0,
|
|
83
|
-
"default": 100.0,
|
|
84
|
-
"enabled_by": "scale_4_enabled"
|
|
85
|
-
},
|
|
86
|
-
{
|
|
87
|
-
"key": "scale_5_enabled",
|
|
88
|
-
"label": "Enable scale 5",
|
|
89
|
-
"type": "bool",
|
|
90
|
-
"default": false
|
|
91
|
-
},
|
|
92
|
-
{
|
|
93
|
-
"key": "scale_5_sensitivity",
|
|
94
|
-
"label": "Scale 5 sensitivity",
|
|
95
|
-
"type": "float",
|
|
96
|
-
"decimals": 1,
|
|
97
|
-
"min": 1.0,
|
|
98
|
-
"max": 100.0,
|
|
99
|
-
"default": 100.0,
|
|
100
|
-
"enabled_by": "scale_5_enabled"
|
|
101
|
-
}
|
|
102
|
-
]
|
|
103
|
-
}
|