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.

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 +222 -48
  16. dcnum/logic/job.py +85 -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 +23 -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.2.dist-info}/METADATA +4 -2
  42. dcnum-0.23.2.dist-info/RECORD +55 -0
  43. {dcnum-0.17.0.dist-info → dcnum-0.23.2.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.2.dist-info}/LICENSE +0 -0
  49. {dcnum-0.17.0.dist-info → dcnum-0.23.2.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
- dcevent data instance
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 dcevent will
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 .feat_moments import moments_based_features
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 = logging.INFO,
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 = logging.INFO,
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, image=image, image_bg=image_bg,
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, image=image, image_corr=image_corr
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 dcevent will
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::