dcnum 0.17.0__py3-none-any.whl → 0.23.2__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.
Potentially problematic release.
This version of dcnum might be problematic. Click here for more details.
- dcnum/_version.py +2 -2
- dcnum/feat/__init__.py +1 -1
- dcnum/feat/event_extractor_manager_thread.py +34 -25
- dcnum/feat/feat_background/base.py +22 -26
- dcnum/feat/feat_background/bg_copy.py +18 -12
- dcnum/feat/feat_background/bg_roll_median.py +20 -10
- dcnum/feat/feat_background/bg_sparse_median.py +55 -7
- dcnum/feat/feat_brightness/bright_all.py +41 -6
- dcnum/feat/feat_contour/__init__.py +4 -0
- dcnum/feat/{feat_moments/mt_legacy.py → feat_contour/moments.py} +32 -8
- dcnum/feat/feat_contour/volume.py +174 -0
- dcnum/feat/feat_texture/tex_all.py +28 -1
- dcnum/feat/gate.py +2 -2
- dcnum/feat/queue_event_extractor.py +30 -9
- dcnum/logic/ctrl.py +222 -48
- dcnum/logic/job.py +85 -2
- dcnum/logic/json_encoder.py +2 -0
- dcnum/meta/ppid.py +17 -3
- dcnum/read/__init__.py +1 -0
- dcnum/read/cache.py +100 -78
- dcnum/read/const.py +6 -4
- dcnum/read/hdf5_data.py +146 -23
- dcnum/read/mapped.py +87 -0
- dcnum/segm/__init__.py +6 -3
- dcnum/segm/segm_thresh.py +6 -18
- dcnum/segm/segm_torch/__init__.py +23 -0
- dcnum/segm/segm_torch/segm_torch_base.py +125 -0
- dcnum/segm/segm_torch/segm_torch_mpo.py +71 -0
- dcnum/segm/segm_torch/segm_torch_sto.py +88 -0
- dcnum/segm/segm_torch/torch_model.py +95 -0
- dcnum/segm/segm_torch/torch_postproc.py +93 -0
- dcnum/segm/segm_torch/torch_preproc.py +114 -0
- dcnum/segm/segmenter.py +181 -80
- dcnum/segm/segmenter_manager_thread.py +38 -30
- dcnum/segm/{segmenter_cpu.py → segmenter_mpo.py} +116 -44
- dcnum/segm/segmenter_sto.py +110 -0
- dcnum/write/__init__.py +2 -1
- dcnum/write/deque_writer_thread.py +9 -1
- dcnum/write/queue_collector_thread.py +8 -14
- dcnum/write/writer.py +128 -5
- {dcnum-0.17.0.dist-info → dcnum-0.23.2.dist-info}/METADATA +4 -2
- dcnum-0.23.2.dist-info/RECORD +55 -0
- {dcnum-0.17.0.dist-info → dcnum-0.23.2.dist-info}/WHEEL +1 -1
- dcnum/feat/feat_moments/__init__.py +0 -4
- dcnum/segm/segmenter_gpu.py +0 -64
- dcnum-0.17.0.dist-info/RECORD +0 -46
- /dcnum/feat/{feat_moments/ct_opencv.py → feat_contour/contour.py} +0 -0
- {dcnum-0.17.0.dist-info → dcnum-0.23.2.dist-info}/LICENSE +0 -0
- {dcnum-0.17.0.dist-info → dcnum-0.23.2.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
from typing import Tuple
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def preprocess_images(images: np.ndarray,
|
|
7
|
+
norm_mean: float | None,
|
|
8
|
+
norm_std: float | None,
|
|
9
|
+
image_shape: Tuple[int, int] = None,
|
|
10
|
+
):
|
|
11
|
+
"""Transform image data to something torch models expect
|
|
12
|
+
|
|
13
|
+
The transformation includes:
|
|
14
|
+
- normalization (division by 255, subtraction of mean, division by std)
|
|
15
|
+
- cropping and padding of the input images to `image_shape`. For padding,
|
|
16
|
+
the median of each *individual* image is used.
|
|
17
|
+
- casting the input images to four dimensions
|
|
18
|
+
(batch_size, 1, height, width) where the second axis is "channels"
|
|
19
|
+
|
|
20
|
+
Parameters
|
|
21
|
+
----------
|
|
22
|
+
images:
|
|
23
|
+
Input image array (batch_size, height_in, width_in). If this is a
|
|
24
|
+
2D image, it will be reshaped to a 3D image with a batch_size of 1.
|
|
25
|
+
norm_mean:
|
|
26
|
+
Mean value used for standard score data normalization, i.e.
|
|
27
|
+
`normalized = `(images / 255 - norm_mean) / norm_std`; Set
|
|
28
|
+
to None to disable normalization.
|
|
29
|
+
norm_std:
|
|
30
|
+
Standard deviation used for standard score data normalization;
|
|
31
|
+
Set to None to disable normalization (see above).
|
|
32
|
+
image_shape
|
|
33
|
+
Image shape for which the model was created (height, width).
|
|
34
|
+
If the image shape does not match the input image shape, then
|
|
35
|
+
the input images are padded/cropped to fit the image shape of
|
|
36
|
+
the model.
|
|
37
|
+
|
|
38
|
+
Returns
|
|
39
|
+
-------
|
|
40
|
+
image_proc:
|
|
41
|
+
3D array with preprocessed image data of shape
|
|
42
|
+
(batch_size, 1, height, width)
|
|
43
|
+
"""
|
|
44
|
+
if len(images.shape) == 2:
|
|
45
|
+
# Insert indexing axis (batch dimension)
|
|
46
|
+
images = images[np.newaxis, :, :]
|
|
47
|
+
|
|
48
|
+
batch_size = images.shape[0]
|
|
49
|
+
|
|
50
|
+
# crop and pad the images based on what the model expects
|
|
51
|
+
image_shape_act = images.shape[1:]
|
|
52
|
+
if image_shape is None:
|
|
53
|
+
# model fits perfectly to input data
|
|
54
|
+
image_shape = image_shape_act
|
|
55
|
+
|
|
56
|
+
# height
|
|
57
|
+
hdiff = image_shape_act[0] - image_shape[0]
|
|
58
|
+
ht = abs(hdiff) // 2
|
|
59
|
+
hb = abs(hdiff) - ht
|
|
60
|
+
# width
|
|
61
|
+
wdiff = image_shape_act[1] - image_shape[1]
|
|
62
|
+
wl = abs(wdiff) // 2
|
|
63
|
+
wr = abs(wdiff) - wl
|
|
64
|
+
# helper variables
|
|
65
|
+
wpad = wdiff < 0
|
|
66
|
+
wcrp = wdiff > 0
|
|
67
|
+
hpad = hdiff < 0
|
|
68
|
+
hcrp = hdiff > 0
|
|
69
|
+
|
|
70
|
+
# The easy part is the cropping
|
|
71
|
+
if hcrp or wcrp:
|
|
72
|
+
# define slices for width and height
|
|
73
|
+
slice_hc = slice(ht, -hb) if hcrp else slice(None, None)
|
|
74
|
+
slice_wc = slice(wl, -wr) if wcrp else slice(None, None)
|
|
75
|
+
img_proc = images[:, slice_hc, slice_wc]
|
|
76
|
+
else:
|
|
77
|
+
img_proc = images
|
|
78
|
+
|
|
79
|
+
# The hard part is the padding
|
|
80
|
+
if hpad or wpad:
|
|
81
|
+
# compute median for each original input image
|
|
82
|
+
img_med = np.median(images, axis=(1, 2))
|
|
83
|
+
# broadcast the median array from 1D to 3D
|
|
84
|
+
img_med = img_med[:, None, None]
|
|
85
|
+
|
|
86
|
+
# define slices for width and height
|
|
87
|
+
slice_hp = slice(ht, -hb) if hpad else slice(None, None)
|
|
88
|
+
slice_wp = slice(wl, -wr) if wpad else slice(None, None)
|
|
89
|
+
|
|
90
|
+
# empty padding image stack with the shape required for the model
|
|
91
|
+
img_pad = np.empty(shape=(batch_size, image_shape[0], image_shape[1]),
|
|
92
|
+
dtype=np.float32)
|
|
93
|
+
# fill in original data
|
|
94
|
+
img_pad[:, slice_hp, slice_wp] = img_proc
|
|
95
|
+
# fill in background data for height
|
|
96
|
+
if hpad:
|
|
97
|
+
img_pad[:, :ht, :] = img_med
|
|
98
|
+
img_pad[:, -hb:, :] = img_med
|
|
99
|
+
# fill in background data for width
|
|
100
|
+
if wpad:
|
|
101
|
+
img_pad[:, :, :wl] = img_med
|
|
102
|
+
img_pad[:, :, -wr:] = img_med
|
|
103
|
+
# Replace img_norm
|
|
104
|
+
img_proc = img_pad
|
|
105
|
+
|
|
106
|
+
if norm_mean is None or norm_std is None:
|
|
107
|
+
# convert to float32
|
|
108
|
+
img_norm = img_proc.astype(np.float32)
|
|
109
|
+
else:
|
|
110
|
+
# normalize images
|
|
111
|
+
img_norm = (img_proc.astype(np.float32) / 255 - norm_mean) / norm_std
|
|
112
|
+
|
|
113
|
+
# Add a "channels" axis for the ML models.
|
|
114
|
+
return img_norm[:, np.newaxis, :, :]
|
dcnum/segm/segmenter.py
CHANGED
|
@@ -13,13 +13,25 @@ from skimage import morphology
|
|
|
13
13
|
from ..meta.ppid import kwargs_to_ppid, ppid_to_kwargs
|
|
14
14
|
|
|
15
15
|
|
|
16
|
+
class SegmenterNotApplicableError(BaseException):
|
|
17
|
+
"""Used to indicate when a dataset cannot be segmented with a segmenter"""
|
|
18
|
+
def __init__(self, segmenter_class, reasons_list):
|
|
19
|
+
super(SegmenterNotApplicableError, self).__init__(
|
|
20
|
+
f"The dataset cannot be segmented with the "
|
|
21
|
+
f"'{segmenter_class.get_ppid_code()}' segmenter: "
|
|
22
|
+
f"{', '.join(reasons_list)}"
|
|
23
|
+
)
|
|
24
|
+
self.reasons_list = reasons_list
|
|
25
|
+
self.segmenter_class = segmenter_class
|
|
26
|
+
|
|
27
|
+
|
|
16
28
|
class Segmenter(abc.ABC):
|
|
17
29
|
#: Required hardware ("cpu" or "gpu") defined in first-level subclass.
|
|
18
30
|
hardware_processor = "none"
|
|
19
31
|
#: Whether to enable mask post-processing. If disabled, you should
|
|
20
32
|
#: make sure that your mask is properly defined and cleaned or you
|
|
21
|
-
#: have to call `process_mask` in your `
|
|
22
|
-
mask_postprocessing =
|
|
33
|
+
#: have to call `process_mask` in your `segment_algorithm` implementation.
|
|
34
|
+
mask_postprocessing = True
|
|
23
35
|
#: Default keyword arguments for mask post-processing. See `process_mask`
|
|
24
36
|
#: for available options.
|
|
25
37
|
mask_default_kwargs = {}
|
|
@@ -31,21 +43,27 @@ class Segmenter(abc.ABC):
|
|
|
31
43
|
kwargs_mask: Dict = None,
|
|
32
44
|
debug: bool = False,
|
|
33
45
|
**kwargs):
|
|
34
|
-
"""Base segmenter
|
|
46
|
+
"""Base segmenter class
|
|
47
|
+
|
|
48
|
+
This is the base segmenter class for the multiprocessing operation
|
|
49
|
+
segmenter :class:`.MPOSegmenter` (multiple subprocesses are spawned
|
|
50
|
+
and each of them works on a queue of images) and the single-threaded
|
|
51
|
+
operation segmenter :class:`.STOSegmenter` (e.g. for batch
|
|
52
|
+
segmentation on a GPU).
|
|
35
53
|
|
|
36
54
|
Parameters
|
|
37
55
|
----------
|
|
38
56
|
kwargs_mask: dict
|
|
39
57
|
Keyword arguments for mask post-processing (see `process_mask`)
|
|
40
58
|
debug: bool
|
|
41
|
-
|
|
59
|
+
Enable debugging mode (e.g. CPU segmenter runs in one thread)
|
|
42
60
|
kwargs:
|
|
43
61
|
Additional, optional keyword arguments for `segment_batch`.
|
|
44
62
|
"""
|
|
45
63
|
self.debug = debug
|
|
46
64
|
self.logger = logging.getLogger(__name__).getChild(
|
|
47
65
|
self.__class__.__name__)
|
|
48
|
-
spec = inspect.getfullargspec(self.
|
|
66
|
+
spec = inspect.getfullargspec(self.segment_algorithm)
|
|
49
67
|
#: custom keyword arguments for the subclassing segmenter
|
|
50
68
|
self.kwargs = spec.kwonlydefaults or {}
|
|
51
69
|
self.kwargs.update(kwargs)
|
|
@@ -68,10 +86,8 @@ class Segmenter(abc.ABC):
|
|
|
68
86
|
def get_border(shape):
|
|
69
87
|
"""Cached boolean image with outer pixels set to True"""
|
|
70
88
|
border = np.zeros(shape, dtype=bool)
|
|
71
|
-
border[0, :] = True
|
|
72
|
-
border[-1
|
|
73
|
-
border[:, 0] = True
|
|
74
|
-
border[:, -1] = True
|
|
89
|
+
border[[0, -1], :] = True
|
|
90
|
+
border[:, [0, -1]] = True
|
|
75
91
|
return border
|
|
76
92
|
|
|
77
93
|
@staticmethod
|
|
@@ -84,7 +100,7 @@ class Segmenter(abc.ABC):
|
|
|
84
100
|
"""Return a unique segmentation pipeline identifier
|
|
85
101
|
|
|
86
102
|
The pipeline identifier is universally applicable and must
|
|
87
|
-
be backwards-compatible (future versions of
|
|
103
|
+
be backwards-compatible (future versions of dcnum will
|
|
88
104
|
correctly acknowledge the ID).
|
|
89
105
|
|
|
90
106
|
The segmenter pipeline ID is defined as::
|
|
@@ -92,7 +108,7 @@ class Segmenter(abc.ABC):
|
|
|
92
108
|
KEY:KW_APPROACH:KW_MASK
|
|
93
109
|
|
|
94
110
|
Where KEY is e.g. "legacy" or "watershed", and KW_APPROACH is a
|
|
95
|
-
list of keyword arguments for `
|
|
111
|
+
list of keyword arguments for `segment_algorithm`, e.g.::
|
|
96
112
|
|
|
97
113
|
thresh=-6^blur=0
|
|
98
114
|
|
|
@@ -121,25 +137,40 @@ class Segmenter(abc.ABC):
|
|
|
121
137
|
get_ppid: Same method for class instances
|
|
122
138
|
"""
|
|
123
139
|
kwargs = copy.deepcopy(kwargs)
|
|
124
|
-
if
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
140
|
+
if cls.mask_postprocessing:
|
|
141
|
+
if kwargs_mask is None and kwargs.get("kwargs_mask", None) is None:
|
|
142
|
+
raise KeyError("`kwargs_mask` must be either specified as "
|
|
143
|
+
"keyword argument to this method or as a key "
|
|
144
|
+
"in `kwargs`!")
|
|
145
|
+
if kwargs_mask is None:
|
|
146
|
+
# see check above (kwargs_mask may also be {})
|
|
147
|
+
kwargs_mask = kwargs.pop("kwargs_mask")
|
|
148
|
+
# Start with the default mask kwargs defined for this subclass
|
|
149
|
+
kwargs_mask_used = copy.deepcopy(cls.mask_default_kwargs)
|
|
150
|
+
kwargs_mask_used.update(kwargs_mask)
|
|
151
|
+
elif kwargs_mask:
|
|
152
|
+
raise ValueError(f"The segmenter '{cls.__name__}' does not "
|
|
153
|
+
f"support mask postprocessing, but 'kwargs_mask' "
|
|
154
|
+
f"was provided: {kwargs_mask}")
|
|
155
|
+
|
|
156
|
+
ppid_parts = [
|
|
157
|
+
cls.get_ppid_code(),
|
|
158
|
+
kwargs_to_ppid(cls, "segment_algorithm", kwargs),
|
|
159
|
+
]
|
|
160
|
+
|
|
161
|
+
if cls.mask_postprocessing:
|
|
162
|
+
ppid_parts.append(
|
|
163
|
+
kwargs_to_ppid(cls, "process_mask", kwargs_mask_used))
|
|
164
|
+
|
|
165
|
+
return ":".join(ppid_parts)
|
|
138
166
|
|
|
139
167
|
@staticmethod
|
|
140
168
|
def get_ppkw_from_ppid(segm_ppid):
|
|
141
169
|
"""Return keyword arguments for this pipeline identifier"""
|
|
142
|
-
|
|
170
|
+
ppid_parts = segm_ppid.split(":")
|
|
171
|
+
code = ppid_parts[0]
|
|
172
|
+
pp_kwargs = ppid_parts[1]
|
|
173
|
+
|
|
143
174
|
for cls_code in get_available_segmenters():
|
|
144
175
|
if cls_code == code:
|
|
145
176
|
cls = get_available_segmenters()[cls_code]
|
|
@@ -148,11 +179,13 @@ class Segmenter(abc.ABC):
|
|
|
148
179
|
raise ValueError(
|
|
149
180
|
f"Could not find segmenter '{code}'!")
|
|
150
181
|
kwargs = ppid_to_kwargs(cls=cls,
|
|
151
|
-
method="
|
|
182
|
+
method="segment_algorithm",
|
|
152
183
|
ppid=pp_kwargs)
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
184
|
+
if cls.mask_postprocessing:
|
|
185
|
+
pp_kwargs_mask = ppid_parts[2]
|
|
186
|
+
kwargs["kwargs_mask"] = ppid_to_kwargs(cls=cls,
|
|
187
|
+
method="process_mask",
|
|
188
|
+
ppid=pp_kwargs_mask)
|
|
156
189
|
return kwargs
|
|
157
190
|
|
|
158
191
|
@staticmethod
|
|
@@ -169,8 +202,8 @@ class Segmenter(abc.ABC):
|
|
|
169
202
|
|
|
170
203
|
Parameters
|
|
171
204
|
----------
|
|
172
|
-
labels: 2d integer ndarray
|
|
173
|
-
Labeled input (contains blobs
|
|
205
|
+
labels: 2d integer or boolean ndarray
|
|
206
|
+
Labeled input (contains blobs consisting of unique numbers)
|
|
174
207
|
clear_border: bool
|
|
175
208
|
clear the image boarder using
|
|
176
209
|
:func:`skimage.segmentation.clear_border`
|
|
@@ -181,6 +214,12 @@ class Segmenter(abc.ABC):
|
|
|
181
214
|
if > 0, perform a binary closing with a disk
|
|
182
215
|
of that radius in pixels
|
|
183
216
|
"""
|
|
217
|
+
if labels.dtype == bool:
|
|
218
|
+
# Convert mask image to labels
|
|
219
|
+
labels, _ = ndi.label(
|
|
220
|
+
input=labels,
|
|
221
|
+
structure=ndi.generate_binary_structure(2, 2))
|
|
222
|
+
|
|
184
223
|
if clear_border:
|
|
185
224
|
#
|
|
186
225
|
# from skimage import segmentation
|
|
@@ -196,25 +235,6 @@ class Segmenter(abc.ABC):
|
|
|
196
235
|
continue
|
|
197
236
|
labels[labels == li] = 0
|
|
198
237
|
|
|
199
|
-
# scikit-image is too slow for us here. So we use OpenCV.
|
|
200
|
-
# https://github.com/scikit-image/scikit-image/issues/1190
|
|
201
|
-
|
|
202
|
-
if closing_disk:
|
|
203
|
-
#
|
|
204
|
-
# from skimage import morphology
|
|
205
|
-
# morphology.binary_closing(
|
|
206
|
-
# mask,
|
|
207
|
-
# footprint=morphology.disk(closing_disk),
|
|
208
|
-
# out=mask)
|
|
209
|
-
#
|
|
210
|
-
element = Segmenter.get_disk(closing_disk)
|
|
211
|
-
labels_uint8 = np.array(labels, dtype=np.uint8)
|
|
212
|
-
labels_dilated = cv2.dilate(labels_uint8, element)
|
|
213
|
-
labels_eroded = cv2.erode(labels_dilated, element)
|
|
214
|
-
labels, _ = ndi.label(
|
|
215
|
-
input=labels_eroded > 0,
|
|
216
|
-
structure=ndi.generate_binary_structure(2, 2))
|
|
217
|
-
|
|
218
238
|
if fill_holes:
|
|
219
239
|
# Floodfill only works with uint8 (too small) or int32
|
|
220
240
|
if labels.dtype != np.int32:
|
|
@@ -226,58 +246,139 @@ class Segmenter(abc.ABC):
|
|
|
226
246
|
# Floodfill algorithm fills the background image and
|
|
227
247
|
# the resulting inversion is the image with holes filled.
|
|
228
248
|
# This will destroy labels (adding 2,147,483,647 to background)
|
|
249
|
+
# Since floodfill will use the upper left corner of the image as
|
|
250
|
+
# a seed, we have to make sure it is set to background. We set
|
|
251
|
+
# a line of pixels in the upper channel wall to zero to be sure.
|
|
252
|
+
labels[0, :] = 0
|
|
253
|
+
# ...and a 4x4 pixel region in the top left corner.
|
|
254
|
+
labels[1, :2] = 0
|
|
229
255
|
cv2.floodFill(labels, None, (0, 0), 2147483647)
|
|
230
256
|
mask = labels != 2147483647
|
|
231
257
|
labels, _ = ndi.label(
|
|
232
258
|
input=mask,
|
|
233
259
|
structure=ndi.generate_binary_structure(2, 2))
|
|
234
260
|
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
#
|
|
261
|
+
if closing_disk:
|
|
262
|
+
# scikit-image is too slow for us here. So we use OpenCV.
|
|
263
|
+
# https://github.com/scikit-image/scikit-image/issues/1190
|
|
264
|
+
#
|
|
265
|
+
# from skimage import morphology
|
|
266
|
+
# morphology.binary_closing(
|
|
267
|
+
# mask,
|
|
268
|
+
# footprint=morphology.disk(closing_disk),
|
|
269
|
+
# out=mask)
|
|
270
|
+
#
|
|
271
|
+
element = Segmenter.get_disk(closing_disk)
|
|
272
|
+
# Note: erode/dilate not implemented for int32
|
|
273
|
+
labels_uint8 = np.array(labels, dtype=np.uint8)
|
|
274
|
+
# Historically, we would like to do a closing (dilation followed
|
|
275
|
+
# by erosion) on the image data where lower brightness values
|
|
276
|
+
# meant "we have an event". However, since we are now working
|
|
277
|
+
# with labels instead of image data (0 is background and labels
|
|
278
|
+
# are enumerated with integers), high "brightness" values are
|
|
279
|
+
# actually the event. Thus, we have to perform an opening
|
|
280
|
+
# (erosion followed by dilation) of the label image.
|
|
281
|
+
labels_eroded = cv2.erode(labels_uint8, element)
|
|
282
|
+
labels_dilated = cv2.dilate(labels_eroded, element)
|
|
249
283
|
labels, _ = ndi.label(
|
|
250
|
-
input=
|
|
284
|
+
input=labels_dilated > 0,
|
|
251
285
|
structure=ndi.generate_binary_structure(2, 2))
|
|
252
|
-
|
|
253
|
-
labels = mol
|
|
254
|
-
# optional postprocessing
|
|
255
|
-
if self.mask_postprocessing:
|
|
256
|
-
labels = self.process_mask(labels, **self.kwargs_mask)
|
|
286
|
+
|
|
257
287
|
return labels
|
|
258
288
|
|
|
289
|
+
@staticmethod
|
|
290
|
+
@abc.abstractmethod
|
|
291
|
+
def segment_algorithm(image):
|
|
292
|
+
"""The segmentation algorithm implemented in the subclass
|
|
293
|
+
|
|
294
|
+
Perform segmentation and return integer label or binary mask image
|
|
295
|
+
"""
|
|
296
|
+
|
|
259
297
|
@functools.cache
|
|
260
|
-
def
|
|
298
|
+
def segment_algorithm_wrapper(self):
|
|
299
|
+
"""Wraps `self.segment_algorithm` to only accept an image
|
|
300
|
+
|
|
301
|
+
The static method `self.segment_algorithm` may optionally accept
|
|
302
|
+
keyword arguments `self.kwargs`. This wrapper returns the
|
|
303
|
+
wrapped method that only accepts the image as an argument. This
|
|
304
|
+
makes sense if you want to unify
|
|
305
|
+
"""
|
|
261
306
|
if self.kwargs:
|
|
262
307
|
# For segmenters that accept keyword arguments.
|
|
263
|
-
segm_wrap = functools.partial(self.
|
|
308
|
+
segm_wrap = functools.partial(self.segment_algorithm,
|
|
264
309
|
**self.kwargs)
|
|
265
310
|
else:
|
|
266
311
|
# For segmenters that don't accept keyword arguments.
|
|
267
|
-
segm_wrap = self.
|
|
312
|
+
segm_wrap = self.segment_algorithm
|
|
268
313
|
return segm_wrap
|
|
269
314
|
|
|
270
|
-
@staticmethod
|
|
271
315
|
@abc.abstractmethod
|
|
272
|
-
def
|
|
273
|
-
"""
|
|
316
|
+
def segment_batch(self, images, start=None, stop=None, bg_off=None):
|
|
317
|
+
"""Return the integer labels for an entire batch
|
|
274
318
|
|
|
275
|
-
This is
|
|
319
|
+
This is implemented in the MPO and STO segmenters.
|
|
276
320
|
"""
|
|
277
321
|
|
|
322
|
+
def segment_chunk(self, image_data, chunk, bg_off=None):
|
|
323
|
+
"""Return the integer labels for one `image_data` chunk
|
|
324
|
+
|
|
325
|
+
This is a wrapper for `segment_batch`.
|
|
326
|
+
|
|
327
|
+
Parameters
|
|
328
|
+
----------
|
|
329
|
+
image_data:
|
|
330
|
+
Instance of dcnum's :class:`.BaseImageChunkCache` with
|
|
331
|
+
the methods `get_chunk` and `get_chunk_slice`.
|
|
332
|
+
chunk: int
|
|
333
|
+
Integer identifying the chunk in `image_data` to segment
|
|
334
|
+
bg_off: ndarray
|
|
335
|
+
Optional 1D array with same length as `image_data` that holds
|
|
336
|
+
additional background offset values that should be subtracted
|
|
337
|
+
from the image data before segmentation. Should only be
|
|
338
|
+
used in combination with segmenters that have
|
|
339
|
+
`requires_background_correction` set to True.
|
|
340
|
+
"""
|
|
341
|
+
images = image_data.get_chunk(chunk)
|
|
342
|
+
if bg_off is not None:
|
|
343
|
+
bg_off_chunk = bg_off[image_data.get_chunk_slice(chunk)]
|
|
344
|
+
else:
|
|
345
|
+
bg_off_chunk = None
|
|
346
|
+
return self.segment_batch(images, bg_off=bg_off_chunk)
|
|
347
|
+
|
|
278
348
|
@abc.abstractmethod
|
|
279
|
-
def
|
|
280
|
-
"""Return the integer
|
|
349
|
+
def segment_single(self, image):
|
|
350
|
+
"""Return the integer label for one image
|
|
351
|
+
|
|
352
|
+
This is implemented in the MPO and STO segmenters.
|
|
353
|
+
"""
|
|
354
|
+
|
|
355
|
+
@classmethod
|
|
356
|
+
def validate_applicability(cls,
|
|
357
|
+
segmenter_kwargs: Dict,
|
|
358
|
+
meta: Dict = None,
|
|
359
|
+
logs: Dict = None):
|
|
360
|
+
"""Validate the applicability of this segmenter for a dataset
|
|
361
|
+
|
|
362
|
+
Parameters
|
|
363
|
+
----------
|
|
364
|
+
segmenter_kwargs: dict
|
|
365
|
+
Keyword arguments for the segmenter
|
|
366
|
+
meta: dict
|
|
367
|
+
Dictionary of metadata from an :class:`HDF5Data` instance
|
|
368
|
+
logs: dict
|
|
369
|
+
Dictionary of logs from an :class:`HDF5Data` instance
|
|
370
|
+
|
|
371
|
+
Returns
|
|
372
|
+
-------
|
|
373
|
+
applicable: bool
|
|
374
|
+
True if the segmenter is applicable to the dataset
|
|
375
|
+
|
|
376
|
+
Raises
|
|
377
|
+
------
|
|
378
|
+
SegmenterNotApplicableError
|
|
379
|
+
If the segmenter is not applicable to the dataset
|
|
380
|
+
"""
|
|
381
|
+
return True
|
|
281
382
|
|
|
282
383
|
|
|
283
384
|
@functools.cache
|
|
@@ -8,7 +8,7 @@ import numpy as np
|
|
|
8
8
|
from ..read.cache import HDF5ImageCache, ImageCorrCache
|
|
9
9
|
|
|
10
10
|
from .segmenter import Segmenter
|
|
11
|
-
from .
|
|
11
|
+
from .segmenter_mpo import MPOSegmenter
|
|
12
12
|
|
|
13
13
|
|
|
14
14
|
class SegmenterManagerThread(threading.Thread):
|
|
@@ -17,7 +17,7 @@ class SegmenterManagerThread(threading.Thread):
|
|
|
17
17
|
image_data: HDF5ImageCache | ImageCorrCache,
|
|
18
18
|
slot_states: mp.Array,
|
|
19
19
|
slot_chunks: mp.Array,
|
|
20
|
-
|
|
20
|
+
bg_off: np.ndarray = None,
|
|
21
21
|
*args, **kwargs):
|
|
22
22
|
"""Manage the segmentation of image data
|
|
23
23
|
|
|
@@ -38,10 +38,10 @@ class SegmenterManagerThread(threading.Thread):
|
|
|
38
38
|
slot_chunks:
|
|
39
39
|
For each slot in `slot_states`, this shared array defines
|
|
40
40
|
on which chunk in `image_data` the segmentation took place.
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
41
|
+
bg_off:
|
|
42
|
+
1d array containing additional background image offset values
|
|
43
|
+
that are added to each background image before subtraction
|
|
44
|
+
from the input image
|
|
45
45
|
|
|
46
46
|
Notes
|
|
47
47
|
-----
|
|
@@ -65,6 +65,9 @@ class SegmenterManagerThread(threading.Thread):
|
|
|
65
65
|
self.segmenter = segmenter
|
|
66
66
|
#: Image data which is being segmented
|
|
67
67
|
self.image_data = image_data
|
|
68
|
+
#: Additional, optional background offset
|
|
69
|
+
self.bg_off = (
|
|
70
|
+
bg_off if self.segmenter.requires_background_correction else None)
|
|
68
71
|
#: Slot states
|
|
69
72
|
self.slot_states = slot_states
|
|
70
73
|
#: Current slot chunk index for the slot states
|
|
@@ -73,40 +76,45 @@ class SegmenterManagerThread(threading.Thread):
|
|
|
73
76
|
self.labels_list = [None] * len(self.slot_states)
|
|
74
77
|
#: Time counter for segmentation
|
|
75
78
|
self.t_count = 0
|
|
76
|
-
#: Whether running in debugging mode
|
|
77
|
-
self.debug = debug
|
|
78
79
|
|
|
79
80
|
def run(self):
|
|
80
81
|
num_slots = len(self.slot_states)
|
|
81
82
|
# We iterate over all the chunks of the image data.
|
|
82
83
|
for chunk in self.image_data.iter_chunks():
|
|
83
|
-
|
|
84
|
-
|
|
84
|
+
unavailable_slots = 0
|
|
85
|
+
found_free_slot = False
|
|
85
86
|
# Wait for a free slot to perform segmentation (compute labels)
|
|
86
|
-
while
|
|
87
|
-
#
|
|
88
|
-
#
|
|
89
|
-
#
|
|
90
|
-
#
|
|
91
|
-
|
|
92
|
-
#
|
|
93
|
-
#
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
87
|
+
while not found_free_slot:
|
|
88
|
+
# We sort the slots according to the slot chunks so that we
|
|
89
|
+
# always process the slot with the smallest slot chunk number
|
|
90
|
+
# first. Initially, the slot_chunks array is filled with
|
|
91
|
+
# zeros, but we populate it here.
|
|
92
|
+
for cur_slot in np.argsort(self.slot_chunks):
|
|
93
|
+
# - "e" there is data from the segmenter (the extractor
|
|
94
|
+
# can take it and process it)
|
|
95
|
+
# - "s" the extractor processed the data and is waiting
|
|
96
|
+
# for the segmenter
|
|
97
|
+
if self.slot_states[cur_slot] != "e":
|
|
98
|
+
# It's the segmenter's turn. Note that we use '!= "e"',
|
|
99
|
+
# because the initial value is "\x00".
|
|
100
|
+
found_free_slot = True
|
|
101
|
+
break
|
|
102
|
+
else:
|
|
103
|
+
# Try another slot.
|
|
104
|
+
unavailable_slots += 1
|
|
105
|
+
if unavailable_slots >= num_slots:
|
|
106
|
+
# There is nothing to do, try to avoid 100% CPU
|
|
107
|
+
unavailable_slots = 0
|
|
108
|
+
time.sleep(.1)
|
|
103
109
|
|
|
104
110
|
t1 = time.monotonic()
|
|
105
111
|
|
|
106
112
|
# We have a free slot to compute the segmentation
|
|
107
113
|
labels = self.segmenter.segment_chunk(
|
|
108
114
|
image_data=self.image_data,
|
|
109
|
-
chunk=chunk
|
|
115
|
+
chunk=chunk,
|
|
116
|
+
bg_off=self.bg_off,
|
|
117
|
+
)
|
|
110
118
|
|
|
111
119
|
# TODO: make this more memory efficient (pre-shared mp.Arrays?)
|
|
112
120
|
# Store labels in a list accessible by the main thread
|
|
@@ -116,12 +124,12 @@ class SegmenterManagerThread(threading.Thread):
|
|
|
116
124
|
# This must be done last: Let the extractor know that this
|
|
117
125
|
# slot is ready for processing.
|
|
118
126
|
self.slot_states[cur_slot] = "e"
|
|
119
|
-
self.logger.debug(f"Segmented
|
|
127
|
+
self.logger.debug(f"Segmented chunk {chunk} in slot {cur_slot}")
|
|
120
128
|
|
|
121
129
|
self.t_count += time.monotonic() - t1
|
|
122
130
|
|
|
123
131
|
# Cleanup
|
|
124
|
-
if isinstance(self.segmenter,
|
|
132
|
+
if isinstance(self.segmenter, MPOSegmenter):
|
|
125
133
|
# Join the segmentation workers.
|
|
126
134
|
self.segmenter.join_workers()
|
|
127
135
|
|