dcnum 0.17.0__py3-none-any.whl → 0.23.1__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.

Files changed (49) hide show
  1. dcnum/_version.py +2 -2
  2. dcnum/feat/__init__.py +1 -1
  3. dcnum/feat/event_extractor_manager_thread.py +34 -25
  4. dcnum/feat/feat_background/base.py +22 -26
  5. dcnum/feat/feat_background/bg_copy.py +18 -12
  6. dcnum/feat/feat_background/bg_roll_median.py +20 -10
  7. dcnum/feat/feat_background/bg_sparse_median.py +55 -7
  8. dcnum/feat/feat_brightness/bright_all.py +41 -6
  9. dcnum/feat/feat_contour/__init__.py +4 -0
  10. dcnum/feat/{feat_moments/mt_legacy.py → feat_contour/moments.py} +32 -8
  11. dcnum/feat/feat_contour/volume.py +174 -0
  12. dcnum/feat/feat_texture/tex_all.py +28 -1
  13. dcnum/feat/gate.py +2 -2
  14. dcnum/feat/queue_event_extractor.py +30 -9
  15. dcnum/logic/ctrl.py +199 -49
  16. dcnum/logic/job.py +63 -2
  17. dcnum/logic/json_encoder.py +2 -0
  18. dcnum/meta/ppid.py +17 -3
  19. dcnum/read/__init__.py +1 -0
  20. dcnum/read/cache.py +100 -78
  21. dcnum/read/const.py +6 -4
  22. dcnum/read/hdf5_data.py +146 -23
  23. dcnum/read/mapped.py +87 -0
  24. dcnum/segm/__init__.py +6 -3
  25. dcnum/segm/segm_thresh.py +6 -18
  26. dcnum/segm/segm_torch/__init__.py +19 -0
  27. dcnum/segm/segm_torch/segm_torch_base.py +125 -0
  28. dcnum/segm/segm_torch/segm_torch_mpo.py +71 -0
  29. dcnum/segm/segm_torch/segm_torch_sto.py +88 -0
  30. dcnum/segm/segm_torch/torch_model.py +95 -0
  31. dcnum/segm/segm_torch/torch_postproc.py +93 -0
  32. dcnum/segm/segm_torch/torch_preproc.py +114 -0
  33. dcnum/segm/segmenter.py +181 -80
  34. dcnum/segm/segmenter_manager_thread.py +38 -30
  35. dcnum/segm/{segmenter_cpu.py → segmenter_mpo.py} +116 -44
  36. dcnum/segm/segmenter_sto.py +110 -0
  37. dcnum/write/__init__.py +2 -1
  38. dcnum/write/deque_writer_thread.py +9 -1
  39. dcnum/write/queue_collector_thread.py +8 -14
  40. dcnum/write/writer.py +128 -5
  41. {dcnum-0.17.0.dist-info → dcnum-0.23.1.dist-info}/METADATA +4 -2
  42. dcnum-0.23.1.dist-info/RECORD +55 -0
  43. {dcnum-0.17.0.dist-info → dcnum-0.23.1.dist-info}/WHEEL +1 -1
  44. dcnum/feat/feat_moments/__init__.py +0 -4
  45. dcnum/segm/segmenter_gpu.py +0 -64
  46. dcnum-0.17.0.dist-info/RECORD +0 -46
  47. /dcnum/feat/{feat_moments/ct_opencv.py → feat_contour/contour.py} +0 -0
  48. {dcnum-0.17.0.dist-info → dcnum-0.23.1.dist-info}/LICENSE +0 -0
  49. {dcnum-0.17.0.dist-info → dcnum-0.23.1.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 `segment_approach` implementation.
22
- mask_postprocessing = False
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
- Debugging parameters
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.segment_approach)
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, :] = True
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 dcevent will
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 `segment_approach`, e.g.::
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 kwargs_mask is None and kwargs.get("kwargs_mask", None) is None:
125
- raise KeyError("`kwargs_mask` must be either specified as "
126
- "keyword argument to this method or as a key "
127
- "in `kwargs`!")
128
- if kwargs_mask is None:
129
- # see check above (kwargs_mask may also be {})
130
- kwargs_mask = kwargs.pop("kwargs_mask")
131
- # Start with the default mask kwargs defined for this subclass
132
- kwargs_mask_used = copy.deepcopy(cls.mask_default_kwargs)
133
- kwargs_mask_used.update(kwargs_mask)
134
- code = cls.get_ppid_code()
135
- csegm = kwargs_to_ppid(cls, "segment_approach", kwargs)
136
- cmask = kwargs_to_ppid(cls, "process_mask", kwargs_mask_used)
137
- return ":".join([code, csegm, cmask])
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
- code, pp_kwargs, pp_kwargs_mask = segm_ppid.split(":")
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="segment_approach",
182
+ method="segment_algorithm",
152
183
  ppid=pp_kwargs)
153
- kwargs["kwargs_mask"] = ppid_to_kwargs(cls=cls,
154
- method="process_mask",
155
- ppid=pp_kwargs_mask)
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 with same number)
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
- return labels
236
-
237
- def segment_chunk(self, image_data, chunk):
238
- """Return the integer labels for one `image_data` chunk"""
239
- data = image_data.get_chunk(chunk)
240
- return self.segment_batch(data)
241
-
242
- def segment_frame(self, image):
243
- """Return the integer label image for `index`"""
244
- segm_wrap = self.segment_frame_wrapper()
245
- # obtain mask or label
246
- mol = segm_wrap(image)
247
- if mol.dtype == bool:
248
- # convert mask to label
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=mol,
284
+ input=labels_dilated > 0,
251
285
  structure=ndi.generate_binary_structure(2, 2))
252
- else:
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 segment_frame_wrapper(self):
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.segment_approach,
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.segment_approach
312
+ segm_wrap = self.segment_algorithm
268
313
  return segm_wrap
269
314
 
270
- @staticmethod
271
315
  @abc.abstractmethod
272
- def segment_approach(image):
273
- """Perform segmentation and return integer label or binary mask image
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 the approach the subclasses implement.
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 segment_batch(self, data, start=None, stop=None):
280
- """Return the integer labels for an entire batch"""
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 .segmenter_cpu import CPUSegmenter
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
- debug: bool = False,
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
- debug:
42
- Whether to run in debugging mode (more verbose messages and
43
- CPU-based segmentation is done in one single thread instead
44
- of in multiple subprocesses).
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
- cur_slot = 0
84
- empty_slots = 0
84
+ unavailable_slots = 0
85
+ found_free_slot = False
85
86
  # Wait for a free slot to perform segmentation (compute labels)
86
- while True:
87
- # - "e" there is data from the segmenter (the extractor
88
- # can take it and process it)
89
- # - "s" the extractor processed the data and is waiting
90
- # for the segmenter
91
- if self.slot_states[cur_slot] != "e":
92
- # It's the segmenters turn. Note that we use '!= "e"',
93
- # because the initial value is "\x00".
94
- break
95
- else:
96
- # Try another slot.
97
- empty_slots += 1
98
- cur_slot = (cur_slot + 1) % num_slots
99
- if empty_slots >= num_slots:
100
- # There is nothing to do, try to avoid 100% CPU
101
- empty_slots = 0
102
- time.sleep(.01)
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 one chunk: {chunk}")
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, CPUSegmenter):
132
+ if isinstance(self.segmenter, MPOSegmenter):
125
133
  # Join the segmentation workers.
126
134
  self.segmenter.join_workers()
127
135