dcnum 0.13.2__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.
- dcnum/_version.py +2 -2
- dcnum/feat/__init__.py +2 -1
- dcnum/feat/event_extractor_manager_thread.py +67 -33
- dcnum/feat/feat_background/__init__.py +3 -12
- dcnum/feat/feat_background/base.py +80 -65
- dcnum/feat/feat_background/bg_copy.py +31 -0
- dcnum/feat/feat_background/bg_roll_median.py +38 -30
- dcnum/feat/feat_background/bg_sparse_median.py +96 -45
- dcnum/feat/feat_brightness/__init__.py +1 -0
- 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/__init__.py +1 -0
- dcnum/feat/feat_texture/tex_all.py +28 -1
- dcnum/feat/gate.py +92 -70
- dcnum/feat/queue_event_extractor.py +139 -70
- dcnum/logic/__init__.py +5 -0
- dcnum/logic/ctrl.py +794 -0
- dcnum/logic/job.py +184 -0
- dcnum/logic/json_encoder.py +19 -0
- dcnum/meta/__init__.py +1 -0
- dcnum/meta/paths.py +30 -0
- dcnum/meta/ppid.py +66 -9
- dcnum/read/__init__.py +1 -0
- dcnum/read/cache.py +109 -77
- dcnum/read/const.py +6 -4
- dcnum/read/hdf5_data.py +190 -31
- dcnum/read/mapped.py +87 -0
- dcnum/segm/__init__.py +6 -15
- dcnum/segm/segm_thresh.py +7 -14
- dcnum/segm/segm_torch/__init__.py +19 -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 +245 -96
- dcnum/segm/segmenter_manager_thread.py +39 -28
- dcnum/segm/{segmenter_cpu.py → segmenter_mpo.py} +137 -43
- dcnum/segm/segmenter_sto.py +110 -0
- dcnum/write/__init__.py +3 -1
- dcnum/write/deque_writer_thread.py +15 -5
- dcnum/write/queue_collector_thread.py +14 -17
- dcnum/write/writer.py +225 -55
- {dcnum-0.13.2.dist-info → dcnum-0.23.1.dist-info}/METADATA +4 -2
- dcnum-0.23.1.dist-info/RECORD +55 -0
- {dcnum-0.13.2.dist-info → dcnum-0.23.1.dist-info}/WHEEL +1 -1
- dcnum/feat/feat_moments/__init__.py +0 -3
- dcnum/segm/segmenter_gpu.py +0 -45
- dcnum-0.13.2.dist-info/RECORD +0 -40
- /dcnum/feat/{feat_moments/ct_opencv.py → feat_contour/contour.py} +0 -0
- {dcnum-0.13.2.dist-info → dcnum-0.23.1.dist-info}/LICENSE +0 -0
- {dcnum-0.13.2.dist-info → dcnum-0.23.1.dist-info}/top_level.txt +0 -0
|
@@ -1,24 +1,21 @@
|
|
|
1
1
|
import logging
|
|
2
|
-
import multiprocessing as mp
|
|
3
2
|
import queue
|
|
4
3
|
import time
|
|
5
4
|
|
|
6
5
|
import numpy as np
|
|
7
6
|
from scipy import ndimage
|
|
8
7
|
|
|
9
|
-
from
|
|
10
|
-
|
|
11
|
-
logger = logging.getLogger(__name__)
|
|
8
|
+
from ...read import HDF5Data
|
|
12
9
|
|
|
10
|
+
from .base import mp_spawn, Background
|
|
13
11
|
|
|
14
|
-
|
|
15
|
-
# and 'fork' on POSIX systems.
|
|
16
|
-
mp_spawn = mp.get_context('spawn')
|
|
12
|
+
logger = logging.getLogger(__name__)
|
|
17
13
|
|
|
18
14
|
|
|
19
15
|
class BackgroundSparseMed(Background):
|
|
20
16
|
def __init__(self, input_data, output_path, kernel_size=200,
|
|
21
17
|
split_time=1., thresh_cleansing=0, frac_cleansing=.8,
|
|
18
|
+
offset_correction=True,
|
|
22
19
|
compress=True, num_cpus=None):
|
|
23
20
|
"""Sparse median background correction with cleansing
|
|
24
21
|
|
|
@@ -61,6 +58,21 @@ class BackgroundSparseMed(Background):
|
|
|
61
58
|
Fraction between 0 and 1 indicating how many background images
|
|
62
59
|
must still be present after cleansing (in case the cleansing
|
|
63
60
|
factor is too large). Set to 1 to disable cleansing altogether.
|
|
61
|
+
offset_correction: bool
|
|
62
|
+
The sparse median background correction produces one median
|
|
63
|
+
image for multiple input frames (BTW this also leads to very
|
|
64
|
+
efficient data storage with HDF5 data compression filters). In
|
|
65
|
+
case the input frames are subject to frame-by-frame brightness
|
|
66
|
+
variations (e.g. flickering of the illumination source), it
|
|
67
|
+
is useful to have an offset value per frame that can then be
|
|
68
|
+
used in a later step to perform a more accurate background
|
|
69
|
+
correction. This offset is computed here by taking a 20px wide
|
|
70
|
+
slice from each frame (where the channel wall is located)
|
|
71
|
+
and computing the median therein relative to the computed
|
|
72
|
+
background image. The data are written to the "bg_off" feature
|
|
73
|
+
in the output file alongside "image_bg". To obtain the
|
|
74
|
+
corrected background image, add "image_bg" and "bg_off".
|
|
75
|
+
Set this to False if you don't need the "bg_off" feature.
|
|
64
76
|
compress: bool
|
|
65
77
|
Whether to compress background data. Set this to False
|
|
66
78
|
for faster processing.
|
|
@@ -76,7 +88,15 @@ class BackgroundSparseMed(Background):
|
|
|
76
88
|
kernel_size=kernel_size,
|
|
77
89
|
split_time=split_time,
|
|
78
90
|
thresh_cleansing=thresh_cleansing,
|
|
79
|
-
frac_cleansing=frac_cleansing
|
|
91
|
+
frac_cleansing=frac_cleansing,
|
|
92
|
+
offset_correction=offset_correction,
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
if kernel_size > len(self.input_data):
|
|
96
|
+
logger.warning(
|
|
97
|
+
f"The kernel size {kernel_size} is too large for input data"
|
|
98
|
+
f"size {len(self.input_data)}. Setting it to input data size!")
|
|
99
|
+
kernel_size = len(self.input_data)
|
|
80
100
|
|
|
81
101
|
#: kernel size used for median filtering
|
|
82
102
|
self.kernel_size = kernel_size
|
|
@@ -86,31 +106,34 @@ class BackgroundSparseMed(Background):
|
|
|
86
106
|
self.thresh_cleansing = thresh_cleansing
|
|
87
107
|
#: keep at least this many background images from the series
|
|
88
108
|
self.frac_cleansing = frac_cleansing
|
|
109
|
+
#: offset/flickering correction
|
|
110
|
+
self.offset_correction = offset_correction
|
|
89
111
|
|
|
90
112
|
# time axis
|
|
91
113
|
self.time = None
|
|
92
114
|
if self.h5in is not None:
|
|
93
|
-
|
|
115
|
+
hd = HDF5Data(self.h5in)
|
|
116
|
+
if "time" in hd:
|
|
94
117
|
# use actual time from dataset
|
|
95
|
-
self.time =
|
|
118
|
+
self.time = hd["time"][:]
|
|
96
119
|
self.time -= self.time[0]
|
|
97
|
-
elif "imaging:frame rate" in
|
|
98
|
-
fr =
|
|
99
|
-
if "frame" in
|
|
120
|
+
elif "imaging:frame rate" in hd.meta:
|
|
121
|
+
fr = hd.meta["imaging:frame rate"]
|
|
122
|
+
if "frame" in hd:
|
|
100
123
|
# compute time from frame rate and frame numbers
|
|
101
|
-
self.time =
|
|
124
|
+
self.time = hd["frame"] / fr
|
|
102
125
|
self.time -= self.time[0]
|
|
103
126
|
else:
|
|
104
127
|
# compute time using frame rate (approximate)
|
|
105
|
-
dur = self.
|
|
128
|
+
dur = self.image_count / fr * 1.5
|
|
106
129
|
logger.info(f"Approximating duration: {dur/60:.1f}min")
|
|
107
|
-
self.time = np.linspace(0, dur, self.
|
|
130
|
+
self.time = np.linspace(0, dur, self.image_count,
|
|
108
131
|
endpoint=True)
|
|
109
132
|
if self.time is None:
|
|
110
133
|
# No HDF5 file or no information therein; Make an educated guess.
|
|
111
|
-
dur = self.
|
|
134
|
+
dur = self.image_count / 3600 * 1.5
|
|
112
135
|
logger.info(f"Guessing duration: {dur/60:.1f}min")
|
|
113
|
-
self.time = np.linspace(0, dur, self.
|
|
136
|
+
self.time = np.linspace(0, dur, self.image_count,
|
|
114
137
|
endpoint=True)
|
|
115
138
|
|
|
116
139
|
#: duration of the measurement
|
|
@@ -149,11 +172,11 @@ class BackgroundSparseMed(Background):
|
|
|
149
172
|
#: queue for median computation jobs
|
|
150
173
|
self.queue = mp_spawn.Queue()
|
|
151
174
|
#: list of workers (processes)
|
|
152
|
-
self.workers = [
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
175
|
+
self.workers = [WorkerSparseMed(self.queue,
|
|
176
|
+
self.worker_counter,
|
|
177
|
+
self.shared_input_raw,
|
|
178
|
+
self.shared_output_raw,
|
|
179
|
+
self.kernel_size)
|
|
157
180
|
for _ in range(self.num_cpus)]
|
|
158
181
|
[w.start() for w in self.workers]
|
|
159
182
|
|
|
@@ -172,11 +195,13 @@ class BackgroundSparseMed(Background):
|
|
|
172
195
|
kernel_size: int = 200,
|
|
173
196
|
split_time: float = 1.,
|
|
174
197
|
thresh_cleansing: float = 0,
|
|
175
|
-
frac_cleansing: float = .8
|
|
198
|
+
frac_cleansing: float = .8,
|
|
199
|
+
offset_correction: bool = True,
|
|
200
|
+
):
|
|
176
201
|
"""Initialize user-defined properties of this class
|
|
177
202
|
|
|
178
203
|
This method primarily exists so that the CLI knows which
|
|
179
|
-
keyword
|
|
204
|
+
keyword arguments can be passed to this class.
|
|
180
205
|
|
|
181
206
|
Parameters
|
|
182
207
|
----------
|
|
@@ -194,6 +219,21 @@ class BackgroundSparseMed(Background):
|
|
|
194
219
|
Fraction between 0 and 1 indicating how many background images
|
|
195
220
|
must still be present after cleansing (in case the cleansing
|
|
196
221
|
factor is too large). Set to 1 to disable cleansing altogether.
|
|
222
|
+
offset_correction: bool
|
|
223
|
+
The sparse median background correction produces one median
|
|
224
|
+
image for multiple input frames (BTW this also leads to very
|
|
225
|
+
efficient data storage with HDF5 data compression filters). In
|
|
226
|
+
case the input frames are subject to frame-by-frame brightness
|
|
227
|
+
variations (e.g. flickering of the illumination source), it
|
|
228
|
+
is useful to have an offset value per frame that can then be
|
|
229
|
+
used in a later step to perform a more accurate background
|
|
230
|
+
correction. This offset is computed here by taking a 20px wide
|
|
231
|
+
slice from each frame (where the channel wall is located)
|
|
232
|
+
and computing the median therein relative to the computed
|
|
233
|
+
background image. The data are written to the "bg_off" feature
|
|
234
|
+
in the output file alongside "image_bg". To obtain the
|
|
235
|
+
corrected background image, add "image_bg" and "bg_off".
|
|
236
|
+
Set this to False if you don't need the "bg_off" feature.
|
|
197
237
|
"""
|
|
198
238
|
assert kernel_size > 0
|
|
199
239
|
assert split_time > 0
|
|
@@ -206,10 +246,7 @@ class BackgroundSparseMed(Background):
|
|
|
206
246
|
|
|
207
247
|
# Compute initial background images (populates self.bg_images)
|
|
208
248
|
for ii, ti in enumerate(self.step_times):
|
|
209
|
-
print(f"Computing background {ii / self.step_times.size:.0%}",
|
|
210
|
-
end="\r", flush=True)
|
|
211
249
|
self.process_second(ii, ti)
|
|
212
|
-
print("Computing background 100% ", flush=True)
|
|
213
250
|
|
|
214
251
|
if self.frac_cleansing != 1:
|
|
215
252
|
# The following algorithm finds background images that contain
|
|
@@ -271,16 +308,16 @@ class BackgroundSparseMed(Background):
|
|
|
271
308
|
f"`thresh_cleansing` or `frac_cleansing`. The new "
|
|
272
309
|
f"threshold is {thresh_fact / thresh}.")
|
|
273
310
|
|
|
274
|
-
logger.info(f"
|
|
311
|
+
logger.info(f"Cleansed {frac_remove:.2%}")
|
|
275
312
|
step_times = self.step_times[used]
|
|
276
313
|
bg_images = self.bg_images[used]
|
|
277
314
|
else:
|
|
278
|
-
logger.info("Background series cleansing disabled
|
|
315
|
+
logger.info("Background series cleansing disabled")
|
|
279
316
|
step_times = self.step_times
|
|
280
317
|
bg_images = self.bg_images
|
|
281
318
|
|
|
282
319
|
# Assign each frame to a certain background index
|
|
283
|
-
bg_idx = np.zeros(self.
|
|
320
|
+
bg_idx = np.zeros(self.image_count, dtype=int)
|
|
284
321
|
idx0 = 0
|
|
285
322
|
idx1 = None
|
|
286
323
|
for ii in range(len(step_times)):
|
|
@@ -292,26 +329,38 @@ class BackgroundSparseMed(Background):
|
|
|
292
329
|
# Fill up remainder of index array with last entry
|
|
293
330
|
bg_idx[idx1:] = ii
|
|
294
331
|
|
|
332
|
+
self.image_proc.value = 1
|
|
333
|
+
|
|
295
334
|
# Write background data
|
|
296
335
|
pos = 0
|
|
297
336
|
step = 1000
|
|
298
|
-
while pos < self.
|
|
299
|
-
stop = min(pos + step, self.
|
|
337
|
+
while pos < self.image_count:
|
|
338
|
+
stop = min(pos + step, self.image_count)
|
|
300
339
|
cur_slice = slice(pos, stop)
|
|
301
|
-
|
|
302
|
-
|
|
340
|
+
cur_bg_data = bg_images[bg_idx[cur_slice]]
|
|
341
|
+
self.writer.store_feature_chunk("image_bg", cur_bg_data)
|
|
342
|
+
if self.offset_correction:
|
|
343
|
+
# Record background offset correction "bg_off". We take a
|
|
344
|
+
# slice of 20px from the top of the image (there are normally
|
|
345
|
+
# no events here, only the channel walls are visible).
|
|
346
|
+
sh, sw = self.input_data.shape[1:]
|
|
347
|
+
roi_full = (slice(None), slice(0, 20), slice(0, sw))
|
|
348
|
+
roi_cur = (cur_slice, slice(0, 20), slice(0, sw))
|
|
349
|
+
val_bg = np.mean(cur_bg_data[roi_full], axis=(1, 2))
|
|
350
|
+
val_dat = np.mean(self.input_data[roi_cur], axis=(1, 2))
|
|
351
|
+
# background image = image_bg + bg_off
|
|
352
|
+
self.writer.store_feature_chunk("bg_off", val_dat - val_bg)
|
|
303
353
|
pos += step
|
|
304
354
|
|
|
305
|
-
def process_second(self,
|
|
355
|
+
def process_second(self,
|
|
356
|
+
ii: int,
|
|
357
|
+
second: float | int):
|
|
306
358
|
idx_start = np.argmin(np.abs(second - self.time))
|
|
307
359
|
idx_stop = idx_start + self.kernel_size
|
|
308
|
-
if idx_stop >= self.
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
idx_stop -= diff
|
|
313
|
-
idx_start -= diff
|
|
314
|
-
assert idx_start >= 0
|
|
360
|
+
if idx_stop >= self.image_count:
|
|
361
|
+
idx_stop = self.image_count
|
|
362
|
+
idx_start = max(0, idx_stop - self.kernel_size)
|
|
363
|
+
assert idx_stop - idx_start == self.kernel_size
|
|
315
364
|
|
|
316
365
|
# The following is equivalent to, but faster than:
|
|
317
366
|
# self.bg_images[ii] = np.median(self.input_data[idx_start:idx_stop],
|
|
@@ -344,12 +393,14 @@ class BackgroundSparseMed(Background):
|
|
|
344
393
|
|
|
345
394
|
self.bg_images[ii] = self.shared_output.reshape(self.image_shape)
|
|
346
395
|
|
|
396
|
+
self.image_proc.value = idx_stop / self.image_count
|
|
397
|
+
|
|
347
398
|
|
|
348
|
-
class
|
|
399
|
+
class WorkerSparseMed(mp_spawn.Process):
|
|
349
400
|
def __init__(self, job_queue, counter, shared_input, shared_output,
|
|
350
401
|
kernel_size, *args, **kwargs):
|
|
351
402
|
"""Worker process for median computation"""
|
|
352
|
-
super(
|
|
403
|
+
super(WorkerSparseMed, self).__init__(*args, **kwargs)
|
|
353
404
|
self.queue = job_queue
|
|
354
405
|
self.queue.cancel_join_thread()
|
|
355
406
|
self.counter = counter
|
|
@@ -4,16 +4,38 @@ import numpy as np
|
|
|
4
4
|
from .common import brightness_names
|
|
5
5
|
|
|
6
6
|
|
|
7
|
-
def brightness_features(image,
|
|
8
|
-
mask,
|
|
9
|
-
image_bg=None,
|
|
10
|
-
image_corr=None
|
|
7
|
+
def brightness_features(image: np.ndarray[np.uint8],
|
|
8
|
+
mask: np.ndarray[np.bool_],
|
|
9
|
+
image_bg: np.ndarray[np.uint8] = None,
|
|
10
|
+
image_corr: np.ndarray[np.int16] = None,
|
|
11
|
+
bg_off: float = None,
|
|
12
|
+
):
|
|
13
|
+
"""Compute brightness features
|
|
14
|
+
|
|
15
|
+
Parameters
|
|
16
|
+
----------
|
|
17
|
+
image: np.ndarray
|
|
18
|
+
2D array of "image" of shape (H, W)
|
|
19
|
+
mask: np.ndarray
|
|
20
|
+
3D array containing the N masks of shape (N, H, W)
|
|
21
|
+
image_bg: np.ndarray
|
|
22
|
+
2D array of "image_bg" of shape (H, W), required for computing
|
|
23
|
+
the "bg_med" feature.
|
|
24
|
+
image_corr: np.ndarray
|
|
25
|
+
2D array of (image - image_bg), which can be optionally passed
|
|
26
|
+
to this method. If not given, will be computed.
|
|
27
|
+
bg_off: float
|
|
28
|
+
Systematic offset value for correcting the brightness of the
|
|
29
|
+
background data which has an effect on "bright_bc_avg",
|
|
30
|
+
"bright_perc_10", "bright_perc_90", and "bg_med" (`bg_off` is
|
|
31
|
+
generated by sparsemed background correction).
|
|
32
|
+
"""
|
|
11
33
|
mask = np.array(mask, dtype=bool)
|
|
12
34
|
size = mask.shape[0]
|
|
13
35
|
|
|
14
36
|
br_dict = {}
|
|
15
|
-
for
|
|
16
|
-
br_dict[
|
|
37
|
+
for mkey in brightness_names:
|
|
38
|
+
br_dict[mkey] = np.full(size, np.nan, dtype=np.float64)
|
|
17
39
|
|
|
18
40
|
avg_sd = compute_avg_sd_masked_uint8(image, mask)
|
|
19
41
|
br_dict["bright_avg"][:] = avg_sd[:, 0]
|
|
@@ -36,6 +58,19 @@ def brightness_features(image,
|
|
|
36
58
|
br_dict["bright_perc_10"][:] = percentiles[:, 0]
|
|
37
59
|
br_dict["bright_perc_90"][:] = percentiles[:, 1]
|
|
38
60
|
|
|
61
|
+
if bg_off is not None:
|
|
62
|
+
# subtract the background offset for all values that are computed
|
|
63
|
+
# from background-corrected images
|
|
64
|
+
for mkey in ["bright_bc_avg", "bright_perc_10", "bright_perc_90"]:
|
|
65
|
+
if mkey in br_dict:
|
|
66
|
+
br_dict[mkey] -= bg_off
|
|
67
|
+
|
|
68
|
+
# add the background offset to all values that were computed from
|
|
69
|
+
# the background only
|
|
70
|
+
for pkey in ["bg_med"]:
|
|
71
|
+
if pkey in br_dict:
|
|
72
|
+
br_dict[pkey] += bg_off
|
|
73
|
+
|
|
39
74
|
return br_dict
|
|
40
75
|
|
|
41
76
|
|
|
@@ -2,11 +2,27 @@ import cv2
|
|
|
2
2
|
import numpy as np
|
|
3
3
|
|
|
4
4
|
|
|
5
|
-
from .
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
def moments_based_features(
|
|
5
|
+
from .contour import contour_single_opencv
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def moments_based_features(
|
|
9
|
+
mask: np.ndarray,
|
|
10
|
+
pixel_size: float,
|
|
11
|
+
ret_contour: bool = False,
|
|
12
|
+
):
|
|
13
|
+
"""Compute moment-based features for a mask image
|
|
14
|
+
|
|
15
|
+
Parameters
|
|
16
|
+
----------
|
|
17
|
+
mask: np.ndarray
|
|
18
|
+
3D stack of 2D boolean mask images to analyze
|
|
19
|
+
pixel_size: float
|
|
20
|
+
pixel size of the mask image in µm
|
|
21
|
+
ret_contour: bool
|
|
22
|
+
whether to also return the raw contour
|
|
23
|
+
"""
|
|
9
24
|
assert pixel_size is not None and pixel_size != 0
|
|
25
|
+
raw_contours = []
|
|
10
26
|
|
|
11
27
|
size = mask.shape[0]
|
|
12
28
|
|
|
@@ -42,9 +58,13 @@ def moments_based_features(mask, pixel_size):
|
|
|
42
58
|
for ii in range(size):
|
|
43
59
|
# raw contour
|
|
44
60
|
cont_raw = contour_single_opencv(mask[ii])
|
|
45
|
-
if
|
|
46
|
-
|
|
47
|
-
|
|
61
|
+
# only continue if the contour is valid
|
|
62
|
+
not_valid = len(cont_raw.shape) < 2 or cv2.contourArea(cont_raw) == 0
|
|
63
|
+
|
|
64
|
+
if ret_contour:
|
|
65
|
+
raw_contours.append(None if not_valid else cont_raw)
|
|
66
|
+
|
|
67
|
+
if not_valid:
|
|
48
68
|
continue
|
|
49
69
|
|
|
50
70
|
mu_raw = cv2.moments(cont_raw)
|
|
@@ -53,6 +73,7 @@ def moments_based_features(mask, pixel_size):
|
|
|
53
73
|
|
|
54
74
|
# convex hull
|
|
55
75
|
cont_cvx = np.squeeze(cv2.convexHull(cont_raw))
|
|
76
|
+
|
|
56
77
|
mu_cvx = cv2.moments(cont_cvx)
|
|
57
78
|
arc_cvx = np.float64(cv2.arcLength(cont_cvx, True))
|
|
58
79
|
|
|
@@ -110,7 +131,7 @@ def moments_based_features(mask, pixel_size):
|
|
|
110
131
|
# specify validity
|
|
111
132
|
valid[ii] = True
|
|
112
133
|
|
|
113
|
-
|
|
134
|
+
data = {
|
|
114
135
|
"area_msd": feat_area_msd,
|
|
115
136
|
"area_ratio": feat_area_ratio,
|
|
116
137
|
"area_um": feat_area_um,
|
|
@@ -131,3 +152,6 @@ def moments_based_features(mask, pixel_size):
|
|
|
131
152
|
"tilt": feat_tilt,
|
|
132
153
|
"valid": valid,
|
|
133
154
|
}
|
|
155
|
+
if ret_contour:
|
|
156
|
+
data["contour"] = raw_contours
|
|
157
|
+
return data
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
from typing import List
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def volume_from_contours(
|
|
7
|
+
contour: List[np.ndarray],
|
|
8
|
+
pos_x: np.ndarray,
|
|
9
|
+
pos_y: np.ndarray,
|
|
10
|
+
pixel_size: float):
|
|
11
|
+
"""Calculate the volume of a polygon revolved around an axis
|
|
12
|
+
|
|
13
|
+
The volume estimation assumes rotational symmetry.
|
|
14
|
+
|
|
15
|
+
Parameters
|
|
16
|
+
----------
|
|
17
|
+
contour: list of ndarrays of shape (N,2)
|
|
18
|
+
One entry is a 2D array that holds the contour of an event
|
|
19
|
+
pos_x: float ndarray of length N
|
|
20
|
+
The x coordinate(s) of the centroid of the event(s) [µm]
|
|
21
|
+
pos_y: float ndarray of length N
|
|
22
|
+
The y coordinate(s) of the centroid of the event(s) [µm]
|
|
23
|
+
pixel_size: float
|
|
24
|
+
The detector pixel size in µm.
|
|
25
|
+
|
|
26
|
+
Returns
|
|
27
|
+
-------
|
|
28
|
+
volume: float ndarray
|
|
29
|
+
volume in um^3
|
|
30
|
+
|
|
31
|
+
Notes
|
|
32
|
+
-----
|
|
33
|
+
The computation of the volume is based on a full rotation of the
|
|
34
|
+
upper and the lower halves of the contour from which the
|
|
35
|
+
average is then used.
|
|
36
|
+
|
|
37
|
+
The volume is computed radially from the center position
|
|
38
|
+
given by (`pos_x`, `pos_y`). For sufficiently smooth contours,
|
|
39
|
+
such as densely sampled ellipses, the center position does not
|
|
40
|
+
play an important role. For contours that are given on a coarse
|
|
41
|
+
grid, as is the case for deformability cytometry, the center position
|
|
42
|
+
must be given.
|
|
43
|
+
|
|
44
|
+
References
|
|
45
|
+
----------
|
|
46
|
+
- https://de.wikipedia.org/wiki/Kegelstumpf#Formeln
|
|
47
|
+
- Yields identical results to the Matlab script by Geoff Olynyk
|
|
48
|
+
<https://de.mathworks.com/matlabcentral/fileexchange/36525-volrevolve>`_
|
|
49
|
+
"""
|
|
50
|
+
# results are stored in a separate array initialized with nans
|
|
51
|
+
v_avg = np.zeros_like(pos_x, dtype=np.float64) * np.nan
|
|
52
|
+
|
|
53
|
+
for ii in range(pos_x.shape[0]):
|
|
54
|
+
# If the contour has less than 4 pixels, the computation will fail.
|
|
55
|
+
# In that case, the value np.nan is already assigned.
|
|
56
|
+
cc = contour[ii]
|
|
57
|
+
if cc is not None and cc.shape[0] >= 4:
|
|
58
|
+
# Center contour coordinates with given centroid
|
|
59
|
+
contour_x = cc[:, 0] - pos_x[ii] / pixel_size
|
|
60
|
+
contour_y = cc[:, 1] - pos_y[ii] / pixel_size
|
|
61
|
+
# Switch to r and z to follow notation of vol_revolve
|
|
62
|
+
# (In RT-DC the axis of rotation is x, but for vol_revolve
|
|
63
|
+
# we need the axis vertically)
|
|
64
|
+
contour_r = contour_y
|
|
65
|
+
contour_z = contour_x
|
|
66
|
+
|
|
67
|
+
# Compute right volume
|
|
68
|
+
# Which points are at negative r-values (r<0)?
|
|
69
|
+
inx_neg = np.where(contour_r < 0)
|
|
70
|
+
# These points will be shifted up to r=0 directly on the z-axis
|
|
71
|
+
contour_right = np.copy(contour_r)
|
|
72
|
+
contour_right[inx_neg] = 0
|
|
73
|
+
vol_right = vol_revolve(r=contour_right,
|
|
74
|
+
z=contour_z,
|
|
75
|
+
point_scale=pixel_size)
|
|
76
|
+
|
|
77
|
+
# Compute left volume
|
|
78
|
+
# Which points are at positive r-values? (r>0)?
|
|
79
|
+
idx_pos = np.where(contour_r > 0)
|
|
80
|
+
# These points will be shifted down to y=0 to build an x-axis
|
|
81
|
+
contour_left = np.copy(contour_r)
|
|
82
|
+
contour_left[idx_pos] = 0
|
|
83
|
+
# Now we still have negative r values, but vol_revolve needs
|
|
84
|
+
# positive values, so we flip the sign...
|
|
85
|
+
contour_left[:] *= -1
|
|
86
|
+
# ... but in doing so, we have switched to clockwise rotation,
|
|
87
|
+
# and we need to pass the array in reverse order
|
|
88
|
+
vol_left = vol_revolve(r=contour_left[::-1],
|
|
89
|
+
z=contour_z[::-1],
|
|
90
|
+
point_scale=pixel_size)
|
|
91
|
+
|
|
92
|
+
# Compute the average
|
|
93
|
+
v_avg[ii] = (vol_right + vol_left) / 2
|
|
94
|
+
|
|
95
|
+
return {"volume": v_avg}
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def vol_revolve(r, z, point_scale=1.):
|
|
99
|
+
r"""Calculate the volume of a polygon revolved around the Z-axis
|
|
100
|
+
|
|
101
|
+
This implementation yields the same results as the volRevolve
|
|
102
|
+
Matlab function by Geoff Olynyk (from 2012-05-03)
|
|
103
|
+
https://de.mathworks.com/matlabcentral/fileexchange/36525-volrevolve.
|
|
104
|
+
|
|
105
|
+
The difference here is that the volume is computed using (a much
|
|
106
|
+
more approachable) implementation using the volume of a truncated
|
|
107
|
+
cone (https://de.wikipedia.org/wiki/Kegelstumpf).
|
|
108
|
+
|
|
109
|
+
.. math::
|
|
110
|
+
|
|
111
|
+
V = \frac{h \cdot \pi}{3} \cdot (R^2 + R \cdot r + r^2)
|
|
112
|
+
|
|
113
|
+
Where :math:`h` is the height of the cone and :math:`r` and
|
|
114
|
+
`R` are the smaller and larger radii of the truncated cone.
|
|
115
|
+
|
|
116
|
+
Each line segment of the contour resembles one truncated cone. If
|
|
117
|
+
the z-step is positive (counter-clockwise contour), then the
|
|
118
|
+
truncated cone volume is added to the total volume. If the z-step
|
|
119
|
+
is negative (e.g. inclusion), then the truncated cone volume is
|
|
120
|
+
removed from the total volume.
|
|
121
|
+
|
|
122
|
+
Parameters
|
|
123
|
+
----------
|
|
124
|
+
r: 1d np.ndarray
|
|
125
|
+
radial coordinates (perpendicular to the z axis)
|
|
126
|
+
z: 1d np.ndarray
|
|
127
|
+
coordinate along the axis of rotation
|
|
128
|
+
point_scale: float
|
|
129
|
+
point size in your preferred units; The volume is multiplied
|
|
130
|
+
by a factor of `point_scale**3`.
|
|
131
|
+
|
|
132
|
+
Notes
|
|
133
|
+
-----
|
|
134
|
+
The coordinates must be given in counter-clockwise order,
|
|
135
|
+
otherwise the volume will be negative.
|
|
136
|
+
"""
|
|
137
|
+
r = np.atleast_1d(r)
|
|
138
|
+
z = np.atleast_1d(z)
|
|
139
|
+
|
|
140
|
+
# make sure we have a closed contour
|
|
141
|
+
if (r[-1] != r[0]) or (z[-1] != z[0]):
|
|
142
|
+
# We have an open contour - close it.
|
|
143
|
+
r = np.resize(r, len(r) + 1)
|
|
144
|
+
z = np.resize(z, len(z) + 1)
|
|
145
|
+
|
|
146
|
+
rp = r[:-1]
|
|
147
|
+
|
|
148
|
+
# array of radii differences: R - r
|
|
149
|
+
dr = np.diff(r)
|
|
150
|
+
# array of height differences: h
|
|
151
|
+
dz = np.diff(z)
|
|
152
|
+
|
|
153
|
+
# If we expand the function in the doc string with
|
|
154
|
+
# dr = R - r and dz = h, then we get three terms for the volume
|
|
155
|
+
# (as opposed to four terms in Olynyk's script). Those three terms
|
|
156
|
+
# all resemble area slices multiplied by the z-distance dz.
|
|
157
|
+
a1 = 3 * rp ** 2
|
|
158
|
+
a2 = 3 * rp * dr
|
|
159
|
+
a3 = dr ** 2
|
|
160
|
+
|
|
161
|
+
# Note that the formula for computing the volume is symmetric
|
|
162
|
+
# with respect to r and R. This means that it does not matter
|
|
163
|
+
# which sign dr has (R and r are always positive). Since this
|
|
164
|
+
# algorithm assumes that the contour is ordered counter-clockwise,
|
|
165
|
+
# positive dz means adding to the contour while negative dz means
|
|
166
|
+
# subtracting from the contour (see test functions for more details).
|
|
167
|
+
# Conveniently so, dz only appears one time in this formula, so
|
|
168
|
+
# we can take the sign of dz as it is (Otherwise, we would have
|
|
169
|
+
# to take the absolute value of every truncated cone volume and
|
|
170
|
+
# multiply it by np.sign(dz)).
|
|
171
|
+
v = np.pi / 3 * dz * np.abs(a1 + a2 + a3)
|
|
172
|
+
vol = np.sum(v) * point_scale ** 3
|
|
173
|
+
|
|
174
|
+
return vol
|
|
@@ -6,6 +6,34 @@ from .common import haralick_names
|
|
|
6
6
|
|
|
7
7
|
def haralick_texture_features(
|
|
8
8
|
mask, image=None, image_bg=None, image_corr=None):
|
|
9
|
+
"""Compute Haralick texture features
|
|
10
|
+
|
|
11
|
+
The following texture features are excluded
|
|
12
|
+
|
|
13
|
+
- feature 6 "Sum Average", which is equivalent to `2 * bright_bc_avg`
|
|
14
|
+
since dclab 0.44.0
|
|
15
|
+
- feature 10 "Difference Variance", because it has a functional
|
|
16
|
+
dependency on the offset value and since we do background correction,
|
|
17
|
+
we are not interested in it
|
|
18
|
+
- feature 14, because nobody is using it, it is not understood by
|
|
19
|
+
everyone what it actually is, and it is computationally expensive.
|
|
20
|
+
|
|
21
|
+
This leaves us with the following 11 texture features (22 if you count
|
|
22
|
+
avg and ptp):
|
|
23
|
+
https://earlglynn.github.io/RNotes/package/EBImage/Haralick-Textural-Features.html
|
|
24
|
+
|
|
25
|
+
- 1. `tex_asm`: (1) Angular Second Moment
|
|
26
|
+
- 2. `tex_con`: (2) Contrast
|
|
27
|
+
- 3. `tex_cor`: (3) Correlation
|
|
28
|
+
- 4. `tex_var`: (4) Variance
|
|
29
|
+
- 5. `tex_idm`: (5) Inverse Difference Moment
|
|
30
|
+
- 6. `tex_sva`: (7) Sum Variance
|
|
31
|
+
- 7. `tex_sen`: (8) Sum Entropy
|
|
32
|
+
- 8. `tex_ent`: (9) Entropy
|
|
33
|
+
- 9. `tex_den`: (11) Difference Entropy
|
|
34
|
+
- 10. `tex_f12`: (12) Information Measure of Correlation 1
|
|
35
|
+
- 11. `tex_f13`: (13) Information Measure of Correlation 2
|
|
36
|
+
"""
|
|
9
37
|
# make sure we have a boolean array
|
|
10
38
|
mask = np.array(mask, dtype=bool)
|
|
11
39
|
size = mask.shape[0]
|
|
@@ -22,7 +50,6 @@ def haralick_texture_features(
|
|
|
22
50
|
|
|
23
51
|
for ii in range(size):
|
|
24
52
|
# Haralick texture features
|
|
25
|
-
# https://gitlab.gwdg.de/blood_data_analysis/dcevent/-/issues/20
|
|
26
53
|
# Preprocessing:
|
|
27
54
|
# - create a copy of the array (don't edit `image_corr`)
|
|
28
55
|
# - add grayscale values (negative values not supported)
|