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.
- 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 +199 -49
- dcnum/logic/job.py +63 -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 +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 +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.1.dist-info}/METADATA +4 -2
- dcnum-0.23.1.dist-info/RECORD +55 -0
- {dcnum-0.17.0.dist-info → dcnum-0.23.1.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.1.dist-info}/LICENSE +0 -0
- {dcnum-0.17.0.dist-info → dcnum-0.23.1.dist-info}/top_level.txt +0 -0
|
@@ -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)
|
dcnum/feat/gate.py
CHANGED
|
@@ -20,7 +20,7 @@ class Gate:
|
|
|
20
20
|
Parameters
|
|
21
21
|
----------
|
|
22
22
|
data: .HDF5Data
|
|
23
|
-
|
|
23
|
+
dcnum data instance
|
|
24
24
|
online_gates: bool
|
|
25
25
|
set to True to enable gating with "online" gates stored
|
|
26
26
|
in the input file; online gates are applied in real-time
|
|
@@ -95,7 +95,7 @@ class Gate:
|
|
|
95
95
|
"""Return a unique gating pipeline identifier
|
|
96
96
|
|
|
97
97
|
The pipeline identifier is universally applicable and must
|
|
98
|
-
be backwards-compatible (future versions of
|
|
98
|
+
be backwards-compatible (future versions of dcnum will
|
|
99
99
|
correctly acknowledge the ID).
|
|
100
100
|
|
|
101
101
|
The gating pipeline ID is defined as::
|
|
@@ -14,7 +14,7 @@ from ..meta.ppid import kwargs_to_ppid, ppid_to_kwargs
|
|
|
14
14
|
from ..read import HDF5Data
|
|
15
15
|
|
|
16
16
|
from .feat_brightness import brightness_features
|
|
17
|
-
from .
|
|
17
|
+
from .feat_contour import moments_based_features, volume_from_contours
|
|
18
18
|
from .feat_texture import haralick_texture_features
|
|
19
19
|
from .gate import Gate
|
|
20
20
|
|
|
@@ -36,7 +36,7 @@ class QueueEventExtractor:
|
|
|
36
36
|
finalize_extraction: mp.Value,
|
|
37
37
|
invalid_mask_counter: mp.Value,
|
|
38
38
|
worker_monitor: mp.RawArray,
|
|
39
|
-
log_level: int =
|
|
39
|
+
log_level: int = None,
|
|
40
40
|
extract_kwargs: dict = None,
|
|
41
41
|
worker_index: int = None,
|
|
42
42
|
*args, **kwargs):
|
|
@@ -103,7 +103,7 @@ class QueueEventExtractor:
|
|
|
103
103
|
# it looks like we have the same PID as the parent process. We
|
|
104
104
|
# are setting up logging in `run`.
|
|
105
105
|
self.logger = None
|
|
106
|
-
self.log_level = log_level
|
|
106
|
+
self.log_level = log_level or logging.getLogger("dcnum").level
|
|
107
107
|
#: Shared array of length `len(data)` into which the number of
|
|
108
108
|
#: events per frame is written.
|
|
109
109
|
self.feat_nevents = feat_nevents
|
|
@@ -124,7 +124,7 @@ class QueueEventExtractor:
|
|
|
124
124
|
gate: Gate,
|
|
125
125
|
num_extractors: int,
|
|
126
126
|
log_queue: mp.Queue,
|
|
127
|
-
log_level: int =
|
|
127
|
+
log_level: int = None,
|
|
128
128
|
):
|
|
129
129
|
"""Get initialization arguments for :cass:`.QueueEventExtractor`
|
|
130
130
|
|
|
@@ -172,31 +172,52 @@ class QueueEventExtractor:
|
|
|
172
172
|
args["finalize_extraction"] = mp_spawn.Value("b", False)
|
|
173
173
|
args["invalid_mask_counter"] = mp_spawn.Value("L", 0)
|
|
174
174
|
args["worker_monitor"] = mp_spawn.RawArray("L", num_extractors)
|
|
175
|
-
args["log_level"] = log_level
|
|
175
|
+
args["log_level"] = log_level or logging.getLogger("dcnum").level
|
|
176
176
|
return args
|
|
177
177
|
|
|
178
178
|
def get_events_from_masks(self, masks, data_index, *,
|
|
179
179
|
brightness: bool = True,
|
|
180
180
|
haralick: bool = True,
|
|
181
|
+
volume: bool = True,
|
|
181
182
|
):
|
|
182
183
|
"""Get events dictionary, performing event-based gating"""
|
|
183
184
|
events = {"mask": masks}
|
|
184
185
|
image = self.data.image[data_index][np.newaxis]
|
|
185
186
|
image_bg = self.data.image_bg[data_index][np.newaxis]
|
|
186
187
|
image_corr = self.data.image_corr[data_index][np.newaxis]
|
|
188
|
+
if "bg_off" in self.data:
|
|
189
|
+
bg_off = self.data["bg_off"][data_index]
|
|
190
|
+
else:
|
|
191
|
+
bg_off = None
|
|
187
192
|
|
|
188
193
|
events.update(
|
|
189
194
|
moments_based_features(
|
|
190
195
|
masks,
|
|
191
|
-
pixel_size=self.data.pixel_size
|
|
196
|
+
pixel_size=self.data.pixel_size,
|
|
197
|
+
ret_contour=volume,
|
|
198
|
+
))
|
|
199
|
+
|
|
192
200
|
if brightness:
|
|
193
201
|
events.update(brightness_features(
|
|
194
|
-
mask=masks,
|
|
202
|
+
mask=masks,
|
|
203
|
+
image=image,
|
|
204
|
+
image_bg=image_bg,
|
|
205
|
+
bg_off=bg_off,
|
|
195
206
|
image_corr=image_corr
|
|
196
207
|
))
|
|
197
208
|
if haralick:
|
|
198
209
|
events.update(haralick_texture_features(
|
|
199
|
-
mask=masks,
|
|
210
|
+
mask=masks,
|
|
211
|
+
image=image,
|
|
212
|
+
image_corr=image_corr,
|
|
213
|
+
))
|
|
214
|
+
|
|
215
|
+
if volume:
|
|
216
|
+
events.update(volume_from_contours(
|
|
217
|
+
contour=events.pop("contour"), # remove contour from events!
|
|
218
|
+
pos_x=events["pos_x"],
|
|
219
|
+
pos_y=events["pos_y"],
|
|
220
|
+
pixel_size=self.data.pixel_size,
|
|
200
221
|
))
|
|
201
222
|
|
|
202
223
|
# gating on feature arrays
|
|
@@ -245,7 +266,7 @@ class QueueEventExtractor:
|
|
|
245
266
|
"""Return a unique feature extractor pipeline identifier
|
|
246
267
|
|
|
247
268
|
The pipeline identifier is universally applicable and must
|
|
248
|
-
be backwards-compatible (future versions of
|
|
269
|
+
be backwards-compatible (future versions of dcnum will
|
|
249
270
|
correctly acknowledge the ID).
|
|
250
271
|
|
|
251
272
|
The feature extractor pipeline ID is defined as::
|