dclab 0.67.0__cp314-cp314-macosx_10_13_x86_64.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 dclab might be problematic. Click here for more details.
- dclab/__init__.py +41 -0
- dclab/_version.py +34 -0
- dclab/cached.py +97 -0
- dclab/cli/__init__.py +10 -0
- dclab/cli/common.py +237 -0
- dclab/cli/task_compress.py +126 -0
- dclab/cli/task_condense.py +223 -0
- dclab/cli/task_join.py +229 -0
- dclab/cli/task_repack.py +98 -0
- dclab/cli/task_split.py +154 -0
- dclab/cli/task_tdms2rtdc.py +186 -0
- dclab/cli/task_verify_dataset.py +75 -0
- dclab/definitions/__init__.py +79 -0
- dclab/definitions/feat_const.py +202 -0
- dclab/definitions/feat_logic.py +182 -0
- dclab/definitions/meta_const.py +252 -0
- dclab/definitions/meta_logic.py +111 -0
- dclab/definitions/meta_parse.py +94 -0
- dclab/downsampling.cpython-314-darwin.so +0 -0
- dclab/downsampling.pyx +230 -0
- dclab/external/__init__.py +4 -0
- dclab/external/packaging/LICENSE +3 -0
- dclab/external/packaging/LICENSE.APACHE +177 -0
- dclab/external/packaging/LICENSE.BSD +23 -0
- dclab/external/packaging/__init__.py +6 -0
- dclab/external/packaging/_structures.py +61 -0
- dclab/external/packaging/version.py +505 -0
- dclab/external/skimage/LICENSE +28 -0
- dclab/external/skimage/__init__.py +2 -0
- dclab/external/skimage/_find_contours.py +216 -0
- dclab/external/skimage/_find_contours_cy.cpython-314-darwin.so +0 -0
- dclab/external/skimage/_find_contours_cy.pyx +188 -0
- dclab/external/skimage/_pnpoly.cpython-314-darwin.so +0 -0
- dclab/external/skimage/_pnpoly.pyx +99 -0
- dclab/external/skimage/_shared/__init__.py +1 -0
- dclab/external/skimage/_shared/geometry.cpython-314-darwin.so +0 -0
- dclab/external/skimage/_shared/geometry.pxd +6 -0
- dclab/external/skimage/_shared/geometry.pyx +55 -0
- dclab/external/skimage/measure.py +7 -0
- dclab/external/skimage/pnpoly.py +53 -0
- dclab/external/statsmodels/LICENSE +35 -0
- dclab/external/statsmodels/__init__.py +6 -0
- dclab/external/statsmodels/nonparametric/__init__.py +1 -0
- dclab/external/statsmodels/nonparametric/_kernel_base.py +203 -0
- dclab/external/statsmodels/nonparametric/kernel_density.py +165 -0
- dclab/external/statsmodels/nonparametric/kernels.py +36 -0
- dclab/features/__init__.py +9 -0
- dclab/features/bright.py +81 -0
- dclab/features/bright_bc.py +93 -0
- dclab/features/bright_perc.py +63 -0
- dclab/features/contour.py +161 -0
- dclab/features/emodulus/__init__.py +339 -0
- dclab/features/emodulus/load.py +252 -0
- dclab/features/emodulus/lut_HE-2D-FEM-22.txt +16432 -0
- dclab/features/emodulus/lut_HE-3D-FEM-22.txt +1276 -0
- dclab/features/emodulus/lut_LE-2D-FEM-19.txt +13082 -0
- dclab/features/emodulus/pxcorr.py +135 -0
- dclab/features/emodulus/scale_linear.py +247 -0
- dclab/features/emodulus/viscosity.py +260 -0
- dclab/features/fl_crosstalk.py +95 -0
- dclab/features/inert_ratio.py +377 -0
- dclab/features/volume.py +242 -0
- dclab/http_utils.py +322 -0
- dclab/isoelastics/__init__.py +468 -0
- dclab/isoelastics/iso_HE-2D-FEM-22-area_um-deform.txt +2440 -0
- dclab/isoelastics/iso_HE-2D-FEM-22-volume-deform.txt +2635 -0
- dclab/isoelastics/iso_HE-3D-FEM-22-area_um-deform.txt +1930 -0
- dclab/isoelastics/iso_HE-3D-FEM-22-volume-deform.txt +2221 -0
- dclab/isoelastics/iso_LE-2D-FEM-19-area_um-deform.txt +2151 -0
- dclab/isoelastics/iso_LE-2D-FEM-19-volume-deform.txt +2250 -0
- dclab/isoelastics/iso_LE-2D-ana-18-area_um-deform.txt +1266 -0
- dclab/kde/__init__.py +1 -0
- dclab/kde/base.py +459 -0
- dclab/kde/contours.py +222 -0
- dclab/kde/methods.py +313 -0
- dclab/kde_contours.py +10 -0
- dclab/kde_methods.py +11 -0
- dclab/lme4/__init__.py +5 -0
- dclab/lme4/lme4_template.R +94 -0
- dclab/lme4/rsetup.py +204 -0
- dclab/lme4/wrapr.py +386 -0
- dclab/polygon_filter.py +398 -0
- dclab/rtdc_dataset/__init__.py +15 -0
- dclab/rtdc_dataset/check.py +902 -0
- dclab/rtdc_dataset/config.py +533 -0
- dclab/rtdc_dataset/copier.py +353 -0
- dclab/rtdc_dataset/core.py +896 -0
- dclab/rtdc_dataset/export.py +867 -0
- dclab/rtdc_dataset/feat_anc_core/__init__.py +24 -0
- dclab/rtdc_dataset/feat_anc_core/af_basic.py +75 -0
- dclab/rtdc_dataset/feat_anc_core/af_emodulus.py +160 -0
- dclab/rtdc_dataset/feat_anc_core/af_fl_max_ctc.py +133 -0
- dclab/rtdc_dataset/feat_anc_core/af_image_contour.py +113 -0
- dclab/rtdc_dataset/feat_anc_core/af_ml_class.py +102 -0
- dclab/rtdc_dataset/feat_anc_core/ancillary_feature.py +320 -0
- dclab/rtdc_dataset/feat_anc_ml/__init__.py +32 -0
- dclab/rtdc_dataset/feat_anc_plugin/__init__.py +3 -0
- dclab/rtdc_dataset/feat_anc_plugin/plugin_feature.py +329 -0
- dclab/rtdc_dataset/feat_basin.py +762 -0
- dclab/rtdc_dataset/feat_temp.py +102 -0
- dclab/rtdc_dataset/filter.py +263 -0
- dclab/rtdc_dataset/fmt_dcor/__init__.py +7 -0
- dclab/rtdc_dataset/fmt_dcor/access_token.py +52 -0
- dclab/rtdc_dataset/fmt_dcor/api.py +173 -0
- dclab/rtdc_dataset/fmt_dcor/base.py +299 -0
- dclab/rtdc_dataset/fmt_dcor/basin.py +73 -0
- dclab/rtdc_dataset/fmt_dcor/logs.py +26 -0
- dclab/rtdc_dataset/fmt_dcor/tables.py +66 -0
- dclab/rtdc_dataset/fmt_dict.py +103 -0
- dclab/rtdc_dataset/fmt_hdf5/__init__.py +6 -0
- dclab/rtdc_dataset/fmt_hdf5/base.py +192 -0
- dclab/rtdc_dataset/fmt_hdf5/basin.py +30 -0
- dclab/rtdc_dataset/fmt_hdf5/events.py +276 -0
- dclab/rtdc_dataset/fmt_hdf5/feat_defect.py +164 -0
- dclab/rtdc_dataset/fmt_hdf5/logs.py +33 -0
- dclab/rtdc_dataset/fmt_hdf5/tables.py +60 -0
- dclab/rtdc_dataset/fmt_hierarchy/__init__.py +11 -0
- dclab/rtdc_dataset/fmt_hierarchy/base.py +278 -0
- dclab/rtdc_dataset/fmt_hierarchy/events.py +146 -0
- dclab/rtdc_dataset/fmt_hierarchy/hfilter.py +140 -0
- dclab/rtdc_dataset/fmt_hierarchy/mapper.py +134 -0
- dclab/rtdc_dataset/fmt_http.py +102 -0
- dclab/rtdc_dataset/fmt_s3.py +354 -0
- dclab/rtdc_dataset/fmt_tdms/__init__.py +476 -0
- dclab/rtdc_dataset/fmt_tdms/event_contour.py +264 -0
- dclab/rtdc_dataset/fmt_tdms/event_image.py +220 -0
- dclab/rtdc_dataset/fmt_tdms/event_mask.py +62 -0
- dclab/rtdc_dataset/fmt_tdms/event_trace.py +146 -0
- dclab/rtdc_dataset/fmt_tdms/exc.py +37 -0
- dclab/rtdc_dataset/fmt_tdms/naming.py +151 -0
- dclab/rtdc_dataset/load.py +77 -0
- dclab/rtdc_dataset/meta_table.py +25 -0
- dclab/rtdc_dataset/writer.py +1019 -0
- dclab/statistics.py +226 -0
- dclab/util.py +176 -0
- dclab/warn.py +15 -0
- dclab-0.67.0.dist-info/METADATA +153 -0
- dclab-0.67.0.dist-info/RECORD +142 -0
- dclab-0.67.0.dist-info/WHEEL +6 -0
- dclab-0.67.0.dist-info/entry_points.txt +8 -0
- dclab-0.67.0.dist-info/licenses/LICENSE +283 -0
- dclab-0.67.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
"""Class for efficiently handling contour data"""
|
|
2
|
+
import numbers
|
|
3
|
+
import sys
|
|
4
|
+
import warnings
|
|
5
|
+
|
|
6
|
+
import numpy as np
|
|
7
|
+
|
|
8
|
+
from ...features import inert_ratio
|
|
9
|
+
|
|
10
|
+
from .exc import ContourIndexingError
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class ContourVerificationWarning(UserWarning):
|
|
14
|
+
pass
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class ContourColumn(object):
|
|
18
|
+
def __init__(self, rtdc_dataset):
|
|
19
|
+
"""A wrapper for ContourData that takes into account event offsets
|
|
20
|
+
|
|
21
|
+
Event offsets appear when the first event that is recorded in the
|
|
22
|
+
tdms files does not have a corresponding contour in the contour
|
|
23
|
+
text file.
|
|
24
|
+
"""
|
|
25
|
+
fname = self.find_contour_file(rtdc_dataset)
|
|
26
|
+
if fname is None:
|
|
27
|
+
self.identifier = None
|
|
28
|
+
else:
|
|
29
|
+
if sys.version_info[0] == 2:
|
|
30
|
+
self.identifier = str(fname).decode("utf-8")
|
|
31
|
+
else:
|
|
32
|
+
self.identifier = str(fname)
|
|
33
|
+
if fname is not None:
|
|
34
|
+
self._contour_data = ContourData(fname)
|
|
35
|
+
self._initialized = False
|
|
36
|
+
else:
|
|
37
|
+
self._contour_data = []
|
|
38
|
+
# prevent `determine_offset` to be called
|
|
39
|
+
self._initialized = True
|
|
40
|
+
self.frame = rtdc_dataset["frame"]
|
|
41
|
+
if "image" in rtdc_dataset:
|
|
42
|
+
self.image_shape = rtdc_dataset["image"].shape[1:]
|
|
43
|
+
else:
|
|
44
|
+
self.image_shape = None
|
|
45
|
+
# if they are set, these features are used for verifying the contour
|
|
46
|
+
self.pxfeat = {}
|
|
47
|
+
|
|
48
|
+
if "area_msd" in rtdc_dataset:
|
|
49
|
+
self.pxfeat["area_msd"] = rtdc_dataset["area_msd"]
|
|
50
|
+
if "pixel size" in rtdc_dataset.config["imaging"]:
|
|
51
|
+
px_size = rtdc_dataset.config["imaging"]["pixel size"]
|
|
52
|
+
for key in ["pos_x", "pos_y", "size_x", "size_y"]:
|
|
53
|
+
if key not in rtdc_dataset.features_innate:
|
|
54
|
+
# abort
|
|
55
|
+
self.pxfeat.clear()
|
|
56
|
+
break
|
|
57
|
+
self.pxfeat[key] = rtdc_dataset[key] / px_size
|
|
58
|
+
|
|
59
|
+
self.event_offset = 0
|
|
60
|
+
self._length = None
|
|
61
|
+
self.dtype = np.int16
|
|
62
|
+
|
|
63
|
+
def __getitem__(self, idx):
|
|
64
|
+
if not isinstance(idx, numbers.Integral):
|
|
65
|
+
raise NotImplementedError(
|
|
66
|
+
"The RTDC_TDMS data handler does not support indexing with "
|
|
67
|
+
"anything else than scalar integers. Please convert your data "
|
|
68
|
+
"to the .rtdc file format!")
|
|
69
|
+
|
|
70
|
+
if not self._initialized:
|
|
71
|
+
self.determine_offset()
|
|
72
|
+
idnew = idx-self.event_offset
|
|
73
|
+
cdata = None
|
|
74
|
+
if idnew < 0:
|
|
75
|
+
# No contour data
|
|
76
|
+
cdata = np.zeros((2, 2), dtype=int)
|
|
77
|
+
else:
|
|
78
|
+
# Assign contour based on stored frame index
|
|
79
|
+
frame_ist = self.frame[idx]
|
|
80
|
+
# Do not only check the exact frame, but +/- 2 events around it
|
|
81
|
+
for idn in [idnew, idnew-1, idnew+1, idnew-2, idnew+2]:
|
|
82
|
+
# check frame
|
|
83
|
+
try:
|
|
84
|
+
frame_soll = self._contour_data.get_frame(idn)
|
|
85
|
+
except IndexError:
|
|
86
|
+
# reached end of file
|
|
87
|
+
continue
|
|
88
|
+
if np.allclose(frame_soll, frame_ist, rtol=0):
|
|
89
|
+
cdata = self._contour_data[idn]
|
|
90
|
+
break
|
|
91
|
+
if cdata is None and self.image_shape and self.pxfeat:
|
|
92
|
+
# The frame is wrong, but the contour might be correct.
|
|
93
|
+
# We check that by verifying several features.
|
|
94
|
+
cdata2 = self._contour_data[idnew]
|
|
95
|
+
cont = np.zeros((self.image_shape[1], self.image_shape[0]))
|
|
96
|
+
cont[cdata2[:, 0], cdata2[:, 1]] = True
|
|
97
|
+
mm = inert_ratio.cont_moments_cv(cdata2)
|
|
98
|
+
|
|
99
|
+
if (np.allclose(self.pxfeat["size_x"][idx],
|
|
100
|
+
np.ptp(cdata2[:, 0]) + 1,
|
|
101
|
+
rtol=0, atol=1e-5)
|
|
102
|
+
and np.allclose(self.pxfeat["size_y"][idx],
|
|
103
|
+
np.ptp(cdata2[:, 1]) + 1,
|
|
104
|
+
rtol=0, atol=1e-5)
|
|
105
|
+
and np.allclose(mm["m00"],
|
|
106
|
+
self.pxfeat["area_msd"][idx],
|
|
107
|
+
rtol=0, atol=1e-5)
|
|
108
|
+
# atol=6 for positions, because the original positions
|
|
109
|
+
# are computed from the convex contour, which would be
|
|
110
|
+
# computed using cv2.convexHull(cdata2).
|
|
111
|
+
and np.allclose(self.pxfeat["pos_x"][idx],
|
|
112
|
+
mm["m10"]/mm["m00"],
|
|
113
|
+
rtol=0, atol=6)
|
|
114
|
+
and np.allclose(self.pxfeat["pos_y"][idx],
|
|
115
|
+
mm["m01"]/mm["m00"],
|
|
116
|
+
rtol=0, atol=6)):
|
|
117
|
+
cdata = cdata2
|
|
118
|
+
|
|
119
|
+
if cdata is None:
|
|
120
|
+
# No idea what went wrong, but we make the beste guess and
|
|
121
|
+
# issue a warning.
|
|
122
|
+
cdata = self._contour_data[idnew]
|
|
123
|
+
frame_c = self._contour_data.get_frame(idnew)
|
|
124
|
+
warnings.warn(
|
|
125
|
+
"Couldn't verify contour {} in {}".format(idx, self.identifier)
|
|
126
|
+
+ " (frame index {})!".format(frame_c),
|
|
127
|
+
ContourVerificationWarning
|
|
128
|
+
)
|
|
129
|
+
return cdata
|
|
130
|
+
|
|
131
|
+
def __len__(self):
|
|
132
|
+
if self._length is None:
|
|
133
|
+
length = len(self._contour_data)
|
|
134
|
+
if length:
|
|
135
|
+
if not self._initialized:
|
|
136
|
+
self.determine_offset()
|
|
137
|
+
length += self.event_offset
|
|
138
|
+
self._length = length
|
|
139
|
+
return self._length
|
|
140
|
+
|
|
141
|
+
@property
|
|
142
|
+
def shape(self):
|
|
143
|
+
return len(self), np.nan, 2
|
|
144
|
+
|
|
145
|
+
def determine_offset(self):
|
|
146
|
+
"""Determines the offset of the contours w.r.t. other data columns
|
|
147
|
+
|
|
148
|
+
Notes
|
|
149
|
+
-----
|
|
150
|
+
- the "frame" column of `rtdc_dataset` is compared to
|
|
151
|
+
the first contour in the contour text file to determine an
|
|
152
|
+
offset by one event
|
|
153
|
+
- modifies the property `event_offset` and sets `_initialized`
|
|
154
|
+
to `True`
|
|
155
|
+
"""
|
|
156
|
+
# In case of regular RTDC, the first contour is
|
|
157
|
+
# missing. In case of fRTDC, it is there, so we
|
|
158
|
+
# might have an offset. We find out if the first
|
|
159
|
+
# contour frame is missing by comparing it to
|
|
160
|
+
# the "frame" column of the rtdc dataset.
|
|
161
|
+
fref = self._contour_data.get_frame(0)
|
|
162
|
+
f0 = self.frame[0]
|
|
163
|
+
f1 = self.frame[1]
|
|
164
|
+
# Use allclose to avoid float/integer comparison problems
|
|
165
|
+
if np.allclose(fref, f0, rtol=0):
|
|
166
|
+
self.event_offset = 0
|
|
167
|
+
elif np.allclose(fref, f1, rtol=0):
|
|
168
|
+
self.event_offset = 1
|
|
169
|
+
else:
|
|
170
|
+
msg = "Contour data has unknown offset (frame {})!".format(fref)
|
|
171
|
+
raise ContourIndexingError(msg)
|
|
172
|
+
self._initialized = True
|
|
173
|
+
|
|
174
|
+
@staticmethod
|
|
175
|
+
def find_contour_file(rtdc_dataset):
|
|
176
|
+
"""Tries to find a contour file that belongs to an RTDC dataset
|
|
177
|
+
|
|
178
|
+
Returns None if no contour file is found.
|
|
179
|
+
"""
|
|
180
|
+
cont_id = rtdc_dataset.path.stem
|
|
181
|
+
cands = [c.name for c in rtdc_dataset._fdir.glob("*_contours.txt")]
|
|
182
|
+
cands = sorted(cands)
|
|
183
|
+
# Search for perfect matches, e.g.
|
|
184
|
+
# - M1_0.240000ul_s.tdms
|
|
185
|
+
# - M1_0.240000ul_s_contours.txt
|
|
186
|
+
for c1 in cands:
|
|
187
|
+
if c1.startswith(cont_id):
|
|
188
|
+
cfile = rtdc_dataset._fdir / c1
|
|
189
|
+
break
|
|
190
|
+
else:
|
|
191
|
+
# Search for M* matches with most overlap, e.g.
|
|
192
|
+
# - M1_0.240000ul_s.tdms
|
|
193
|
+
# - M1_contours.txt
|
|
194
|
+
for c2 in cands:
|
|
195
|
+
if (c2.split("_")[0] == rtdc_dataset._mid):
|
|
196
|
+
# Do not confuse with M10_contours.txt
|
|
197
|
+
cfile = rtdc_dataset._fdir / c2
|
|
198
|
+
break
|
|
199
|
+
else:
|
|
200
|
+
cfile = None
|
|
201
|
+
return cfile
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
class ContourData(object):
|
|
205
|
+
def __init__(self, fname):
|
|
206
|
+
"""Access an MX_contour.txt as a dictionary
|
|
207
|
+
|
|
208
|
+
Initialize this class with a *_contour.txt file.
|
|
209
|
+
The individual contours can be accessed like a
|
|
210
|
+
list (enumerated from 0 on).
|
|
211
|
+
"""
|
|
212
|
+
self._initialized = False
|
|
213
|
+
self.filename = fname
|
|
214
|
+
self._length = None
|
|
215
|
+
|
|
216
|
+
def __getitem__(self, idx):
|
|
217
|
+
cont = self.data[idx]
|
|
218
|
+
cont = cont.strip()
|
|
219
|
+
cont = cont.replace(")", "")
|
|
220
|
+
cont = cont.replace("(", "")
|
|
221
|
+
cont = cont.replace("(", "")
|
|
222
|
+
cont = cont.replace("\n", ",")
|
|
223
|
+
cont = cont.replace(" ", " ")
|
|
224
|
+
cont = cont.replace(" ", " ")
|
|
225
|
+
if len(cont) > 1:
|
|
226
|
+
_frame, cont = cont.split(" ", 1)
|
|
227
|
+
cont = cont.strip(" ,")
|
|
228
|
+
data = np.fromstring(cont, sep=",", dtype=np.uint16).reshape(-1, 2)
|
|
229
|
+
return data
|
|
230
|
+
|
|
231
|
+
def __len__(self):
|
|
232
|
+
if self._length is None:
|
|
233
|
+
self._length = len(self.data)
|
|
234
|
+
return self._length
|
|
235
|
+
|
|
236
|
+
def _index_file(self):
|
|
237
|
+
"""Open and index the contour file
|
|
238
|
+
|
|
239
|
+
This function populates the internal list of contours
|
|
240
|
+
as strings which will be available as `self.data`.
|
|
241
|
+
"""
|
|
242
|
+
with self.filename.open() as fd:
|
|
243
|
+
data = fd.read()
|
|
244
|
+
|
|
245
|
+
ident = "Contour in frame"
|
|
246
|
+
self._data = data.split(ident)[1:]
|
|
247
|
+
self._initialized = True
|
|
248
|
+
|
|
249
|
+
@property
|
|
250
|
+
def data(self):
|
|
251
|
+
"""Access self.data
|
|
252
|
+
If `self._index_file` has not been computed before, this
|
|
253
|
+
property will cause it to do so.
|
|
254
|
+
"""
|
|
255
|
+
if not self._initialized:
|
|
256
|
+
self._index_file()
|
|
257
|
+
return self._data
|
|
258
|
+
|
|
259
|
+
def get_frame(self, idx):
|
|
260
|
+
"""Return the frame number of a contour"""
|
|
261
|
+
cont = self.data[idx]
|
|
262
|
+
# previously was split using " ", but "(" is more general
|
|
263
|
+
frame = int(cont.strip().split("(", 1)[0])
|
|
264
|
+
return frame
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Class for efficiently handling image/video data
|
|
3
|
+
"""
|
|
4
|
+
import numbers
|
|
5
|
+
import pathlib
|
|
6
|
+
import sys
|
|
7
|
+
import warnings
|
|
8
|
+
|
|
9
|
+
import imageio
|
|
10
|
+
import numpy as np
|
|
11
|
+
|
|
12
|
+
from .exc import (InvalidVideoFileError, CorruptFrameWarning,
|
|
13
|
+
InitialFrameMissingWarning, SlowVideoWarning)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
ISWIN = sys.platform.startswith("win")
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class ImageColumn(object):
|
|
20
|
+
def __init__(self, rtdc_dataset):
|
|
21
|
+
"""A wrapper for ImageMap that takes into account event offsets
|
|
22
|
+
|
|
23
|
+
Event offsets appear when the first event that is recorded in the
|
|
24
|
+
tdms files does not have a corresponding cell image in the video
|
|
25
|
+
file.
|
|
26
|
+
"""
|
|
27
|
+
fname = self.find_video_file(rtdc_dataset)
|
|
28
|
+
self.identifier = fname
|
|
29
|
+
if fname is not None:
|
|
30
|
+
self._image_data = ImageMap(fname)
|
|
31
|
+
else:
|
|
32
|
+
self._image_data = []
|
|
33
|
+
conf = rtdc_dataset.config
|
|
34
|
+
self.event_offset = int(conf["fmt_tdms"]["video frame offset"])
|
|
35
|
+
self.video_file = fname
|
|
36
|
+
self._shape = None
|
|
37
|
+
self.dtype = np.uint8
|
|
38
|
+
|
|
39
|
+
def __getitem__(self, idx):
|
|
40
|
+
if not isinstance(idx, numbers.Integral):
|
|
41
|
+
raise NotImplementedError(
|
|
42
|
+
"The RTDC_TDMS data handler does not support indexing with "
|
|
43
|
+
"anything else than scalar integers. Please convert your data "
|
|
44
|
+
"to the .rtdc file format!")
|
|
45
|
+
|
|
46
|
+
idnew = int(idx-self.event_offset)
|
|
47
|
+
if idnew < 0:
|
|
48
|
+
# No data - show a dummy image instead
|
|
49
|
+
warnings.warn("Frame {} in {} ".format(idnew, self.identifier)
|
|
50
|
+
+ "is not defined; replacing with dummy image!",
|
|
51
|
+
InitialFrameMissingWarning)
|
|
52
|
+
cdata = self.dummy
|
|
53
|
+
else:
|
|
54
|
+
if hasattr(imageio.plugins.ffmpeg, "CannotReadFrameError"):
|
|
55
|
+
# imageio<2.5.0
|
|
56
|
+
excs = IndexError, imageio.plugins.ffmpeg.CannotReadFrameError
|
|
57
|
+
else:
|
|
58
|
+
# imageio>=2.5.0
|
|
59
|
+
excs = IndexError
|
|
60
|
+
try:
|
|
61
|
+
cdata = self._image_data[idnew]
|
|
62
|
+
except excs:
|
|
63
|
+
# The avi is corrupt. Return a dummy image.
|
|
64
|
+
warnings.warn("Frame {} in {} ".format(idnew, self.identifier)
|
|
65
|
+
+ "is corrupt; replacing with dummy image!",
|
|
66
|
+
CorruptFrameWarning)
|
|
67
|
+
cdata = self.dummy
|
|
68
|
+
return cdata
|
|
69
|
+
|
|
70
|
+
def __len__(self):
|
|
71
|
+
length = len(self._image_data)
|
|
72
|
+
if length:
|
|
73
|
+
length = length + self.event_offset
|
|
74
|
+
return length
|
|
75
|
+
|
|
76
|
+
@property
|
|
77
|
+
def dummy(self):
|
|
78
|
+
"""Returns a dummy image"""
|
|
79
|
+
cdata = np.zeros(self.shape[1:], dtype=np.uint8)
|
|
80
|
+
return cdata
|
|
81
|
+
|
|
82
|
+
@property
|
|
83
|
+
def shape(self):
|
|
84
|
+
if self._shape is None:
|
|
85
|
+
f0 = self._image_data[0].shape
|
|
86
|
+
self._shape = len(self), f0[0], f0[1]
|
|
87
|
+
return self._shape
|
|
88
|
+
|
|
89
|
+
@staticmethod
|
|
90
|
+
def find_video_file(rtdc_dataset):
|
|
91
|
+
"""Tries to find a video file that belongs to an RTDC dataset
|
|
92
|
+
|
|
93
|
+
Returns None if no video file is found.
|
|
94
|
+
"""
|
|
95
|
+
video = None
|
|
96
|
+
if rtdc_dataset._fdir.exists():
|
|
97
|
+
# Cell images (video)
|
|
98
|
+
videos = [v.name for v in rtdc_dataset._fdir.glob("*.avi")]
|
|
99
|
+
# Filter videos according to measurement number
|
|
100
|
+
meas_id = rtdc_dataset._mid
|
|
101
|
+
videos = [v for v in videos if v.split("_")[0] == meas_id]
|
|
102
|
+
videos.sort()
|
|
103
|
+
if len(videos) != 0:
|
|
104
|
+
# Defaults to first avi file
|
|
105
|
+
video = videos[0]
|
|
106
|
+
# g/q video file names. q comes first.
|
|
107
|
+
for v in videos:
|
|
108
|
+
if v.endswith("imag.avi"):
|
|
109
|
+
video = v
|
|
110
|
+
break
|
|
111
|
+
# add this here, because fRT-DC measurements also contain
|
|
112
|
+
# videos ..._proc.avi
|
|
113
|
+
elif v.endswith("imaq.avi"):
|
|
114
|
+
video = v
|
|
115
|
+
break
|
|
116
|
+
if video is None:
|
|
117
|
+
return None
|
|
118
|
+
else:
|
|
119
|
+
vpath = rtdc_dataset._fdir / video
|
|
120
|
+
if vpath.stat().st_size < 64:
|
|
121
|
+
# the video file is empty
|
|
122
|
+
raise InvalidVideoFileError("Bad video '{}'!".format(vpath))
|
|
123
|
+
else:
|
|
124
|
+
return vpath
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
class ImageMap(object):
|
|
128
|
+
def __init__(self, fname):
|
|
129
|
+
"""Access a video file of an RT-DC dataset
|
|
130
|
+
|
|
131
|
+
Initialize this class with a video file.
|
|
132
|
+
"""
|
|
133
|
+
fname = pathlib.Path(fname)
|
|
134
|
+
self._length = None
|
|
135
|
+
# video handle:
|
|
136
|
+
self._cap = None
|
|
137
|
+
# filename
|
|
138
|
+
if not fname.exists():
|
|
139
|
+
raise OSError("file does not exist: {}".format(fname))
|
|
140
|
+
self.filename = fname
|
|
141
|
+
|
|
142
|
+
def __del__(self):
|
|
143
|
+
if self._cap is not None and hasattr(self._cap, "_proc"):
|
|
144
|
+
if ISWIN and self._cap._proc is not None:
|
|
145
|
+
# This is a workaround for windows when pytest fails due
|
|
146
|
+
# to "OSError: [WinError 6] The handle is invalid",
|
|
147
|
+
# which is somehow related to the fact that "_proc.kill()"
|
|
148
|
+
# must be called twice (in "close()" and in this case) in
|
|
149
|
+
# order to terminate the process and due to the fact the
|
|
150
|
+
# we are not using the with-statement in combination
|
|
151
|
+
# with imageio.get_reader().
|
|
152
|
+
self._cap._proc.kill()
|
|
153
|
+
self._cap.close()
|
|
154
|
+
|
|
155
|
+
def __getitem__(self, idx):
|
|
156
|
+
"""Returns the requested frame from the video in gray scale"""
|
|
157
|
+
cap = self.video_handle
|
|
158
|
+
try:
|
|
159
|
+
cellimg = cap.get_data(idx)
|
|
160
|
+
except IndexError:
|
|
161
|
+
self._get_image_workaround_seek(idx)
|
|
162
|
+
else:
|
|
163
|
+
if np.all(cellimg == 0):
|
|
164
|
+
cellimg = self._get_image_workaround_seek(idx)
|
|
165
|
+
# Convert to grayscale
|
|
166
|
+
if len(cellimg.shape) == 3:
|
|
167
|
+
cellimg = np.array(cellimg[:, :, 0])
|
|
168
|
+
return cellimg
|
|
169
|
+
|
|
170
|
+
def __len__(self):
|
|
171
|
+
"""Returns the length of the video or `True` if the length cannot be
|
|
172
|
+
determined.
|
|
173
|
+
"""
|
|
174
|
+
if self._length is None:
|
|
175
|
+
cap = self.video_handle
|
|
176
|
+
if hasattr(cap, "count_frames"): # imageio>=2.5.0
|
|
177
|
+
length = cap.count_frames()
|
|
178
|
+
else: # imageio<2.5.0
|
|
179
|
+
try:
|
|
180
|
+
length = len(cap)
|
|
181
|
+
except TypeError:
|
|
182
|
+
# length is set to inf (it would generally still be
|
|
183
|
+
# possible to rescue a limted number of frames, but
|
|
184
|
+
# other parts of the dataset are probably also broken
|
|
185
|
+
# (aborted measurement), so for the sake of simplicity
|
|
186
|
+
# we stop here)
|
|
187
|
+
raise InvalidVideoFileError(
|
|
188
|
+
"Video file has unknown length '{}'!".format(
|
|
189
|
+
self.filename))
|
|
190
|
+
self._length = length
|
|
191
|
+
return self._length
|
|
192
|
+
|
|
193
|
+
def _get_image_workaround_seek(self, idx):
|
|
194
|
+
"""Same as __getitem__ but seek through the video beforehand
|
|
195
|
+
|
|
196
|
+
This is a workaround for an all-zero image returned by `imageio`.
|
|
197
|
+
"""
|
|
198
|
+
cap = self.video_handle
|
|
199
|
+
mult = 50
|
|
200
|
+
# get the first frame to be on the safe side
|
|
201
|
+
cap.get_data(101) # frame above 100 (don't know why)
|
|
202
|
+
cap.get_data(0) # seek to zero
|
|
203
|
+
for ii in range(idx//mult):
|
|
204
|
+
cap.get_data(ii*mult)
|
|
205
|
+
final = cap.get_data(idx)
|
|
206
|
+
if not np.all(final == 0):
|
|
207
|
+
# This means we succeeded
|
|
208
|
+
warnings.warn("Seeking video file does not work, used workaround "
|
|
209
|
+
+ "which is slow!", SlowVideoWarning)
|
|
210
|
+
return final
|
|
211
|
+
|
|
212
|
+
@property
|
|
213
|
+
def video_handle(self):
|
|
214
|
+
if self._cap is None:
|
|
215
|
+
try:
|
|
216
|
+
self._cap = imageio.get_reader(self.filename)
|
|
217
|
+
except OSError:
|
|
218
|
+
raise InvalidVideoFileError(
|
|
219
|
+
"Broken video file '{}'!".format(self.filename))
|
|
220
|
+
return self._cap
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"""Class for on-the-fly conversion of contours to masks"""
|
|
2
|
+
import numbers
|
|
3
|
+
|
|
4
|
+
import numpy as np
|
|
5
|
+
import scipy.ndimage as ndi
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class MaskColumn(object):
|
|
9
|
+
def __init__(self, rtdc_dataset):
|
|
10
|
+
"""Computes mask from contour data"""
|
|
11
|
+
self.contour = rtdc_dataset["contour"]
|
|
12
|
+
self.image = rtdc_dataset["image"]
|
|
13
|
+
self.identifier = self.contour.identifier
|
|
14
|
+
self.config = rtdc_dataset.config
|
|
15
|
+
self._shape = None
|
|
16
|
+
self._img_shape_cache = None
|
|
17
|
+
self.dtype = np.bool_
|
|
18
|
+
|
|
19
|
+
def __getitem__(self, idx):
|
|
20
|
+
if not isinstance(idx, numbers.Integral):
|
|
21
|
+
raise NotImplementedError(
|
|
22
|
+
"The RTDC_TDMS data handler does not support indexing with "
|
|
23
|
+
"anything else than scalar integers. Please convert your data "
|
|
24
|
+
"to the .rtdc file format!")
|
|
25
|
+
|
|
26
|
+
mask = np.zeros(self._img_shape, dtype=bool)
|
|
27
|
+
conti = self.contour[idx]
|
|
28
|
+
mask[conti[:, 1], conti[:, 0]] = True
|
|
29
|
+
ndi.binary_fill_holes(mask, output=mask)
|
|
30
|
+
return mask
|
|
31
|
+
|
|
32
|
+
def __len__(self):
|
|
33
|
+
if self._img_shape != (0, 0):
|
|
34
|
+
lc = len(self.contour)
|
|
35
|
+
else:
|
|
36
|
+
lc = 0
|
|
37
|
+
return lc
|
|
38
|
+
|
|
39
|
+
@property
|
|
40
|
+
def _img_shape(self):
|
|
41
|
+
if self._img_shape_cache is None:
|
|
42
|
+
"""Shape of one event image"""
|
|
43
|
+
cfgim = self.config["imaging"]
|
|
44
|
+
if self.image:
|
|
45
|
+
# get shape from image column
|
|
46
|
+
event_image_shape = self.image.shape[1:]
|
|
47
|
+
elif "roi size x" in cfgim and "roi size y" in cfgim:
|
|
48
|
+
# get shape from config (this is less reliable than getting
|
|
49
|
+
# the shape from the image; there were measurements with
|
|
50
|
+
# wrong config keys)
|
|
51
|
+
event_image_shape = (cfgim["roi size y"], cfgim["roi size x"])
|
|
52
|
+
else:
|
|
53
|
+
# no shape available
|
|
54
|
+
event_image_shape = (0, 0)
|
|
55
|
+
self._img_shape_cache = event_image_shape
|
|
56
|
+
return self._img_shape_cache
|
|
57
|
+
|
|
58
|
+
@property
|
|
59
|
+
def shape(self):
|
|
60
|
+
if self._shape is None:
|
|
61
|
+
self._shape = len(self), self._img_shape[0], self._img_shape[1]
|
|
62
|
+
return self._shape
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
"""Handling fluorescence trace data"""
|
|
2
|
+
import pathlib
|
|
3
|
+
import warnings
|
|
4
|
+
|
|
5
|
+
from nptdms import TdmsFile
|
|
6
|
+
import numpy as np
|
|
7
|
+
|
|
8
|
+
from ... import definitions as dfn
|
|
9
|
+
|
|
10
|
+
from . import naming
|
|
11
|
+
from .exc import InvalidTDMSFileFormatError, MultipleSamplesPerEventFound
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class TraceColumn(object):
|
|
15
|
+
def __init__(self, rtdc_dataset):
|
|
16
|
+
"""Prepares everything but does not load the trace data yet
|
|
17
|
+
|
|
18
|
+
The trace data is loaded when __getitem__, __len__, or __iter__
|
|
19
|
+
are called. This saves time and memory when the trace data is
|
|
20
|
+
not needed at all, e.g. for batch processing with DCscope.
|
|
21
|
+
"""
|
|
22
|
+
self._trace = None
|
|
23
|
+
self.mname = rtdc_dataset.path
|
|
24
|
+
self.identifier = self.mname
|
|
25
|
+
self.dtype = np.int16
|
|
26
|
+
|
|
27
|
+
def __getitem__(self, trace_key):
|
|
28
|
+
if trace_key not in dfn.FLUOR_TRACES:
|
|
29
|
+
msg = "Unknown fluorescence trace key: {}".format(trace_key)
|
|
30
|
+
raise ValueError(msg)
|
|
31
|
+
return self.trace.__getitem__(trace_key)
|
|
32
|
+
|
|
33
|
+
def __len__(self):
|
|
34
|
+
return self.trace.__len__()
|
|
35
|
+
|
|
36
|
+
def __iter__(self):
|
|
37
|
+
return self.trace.__iter__()
|
|
38
|
+
|
|
39
|
+
def __repr__(self):
|
|
40
|
+
tname = TraceColumn.find_trace_file(self.mname)
|
|
41
|
+
if self._trace is None:
|
|
42
|
+
addstr = "not loaded into memory"
|
|
43
|
+
else:
|
|
44
|
+
addstr = "loaded into memory"
|
|
45
|
+
|
|
46
|
+
if tname is None:
|
|
47
|
+
rep = "No trace data available!"
|
|
48
|
+
else:
|
|
49
|
+
rep = "Fluorescence trace data from file {}, <{}>".format(tname,
|
|
50
|
+
addstr)
|
|
51
|
+
return rep
|
|
52
|
+
|
|
53
|
+
def keys(self):
|
|
54
|
+
return self.trace.keys()
|
|
55
|
+
|
|
56
|
+
@property
|
|
57
|
+
def shape(self):
|
|
58
|
+
key0 = sorted(self.keys())[0]
|
|
59
|
+
return len(self), len(self[key0]), len(self[key0][0])
|
|
60
|
+
|
|
61
|
+
@property
|
|
62
|
+
def trace(self):
|
|
63
|
+
"""Initializes the trace data"""
|
|
64
|
+
if self._trace is None:
|
|
65
|
+
self._trace = self.load_trace(self.mname)
|
|
66
|
+
return self._trace
|
|
67
|
+
|
|
68
|
+
@staticmethod
|
|
69
|
+
def load_trace(mname):
|
|
70
|
+
"""Loads the traces and returns them as a dictionary
|
|
71
|
+
|
|
72
|
+
Currently, only loading traces from tdms files is supported.
|
|
73
|
+
This forces us to load the full tdms file into memory which
|
|
74
|
+
takes some time.
|
|
75
|
+
"""
|
|
76
|
+
tname = TraceColumn.find_trace_file(mname)
|
|
77
|
+
|
|
78
|
+
# Initialize empty trace dictionary
|
|
79
|
+
trace = {}
|
|
80
|
+
|
|
81
|
+
if tname is None:
|
|
82
|
+
pass
|
|
83
|
+
elif tname.suffix == ".tdms":
|
|
84
|
+
# Again load the measurement tdms file.
|
|
85
|
+
# This might increase memory usage, but it is cleaner
|
|
86
|
+
# when looking at code structure.
|
|
87
|
+
mdata = TdmsFile(str(mname))
|
|
88
|
+
try:
|
|
89
|
+
sampleids = mdata["Cell Track"]["FL1index"].data
|
|
90
|
+
except KeyError:
|
|
91
|
+
raise InvalidTDMSFileFormatError(
|
|
92
|
+
"No 'FL1index' column in '{}'!".format(tname))
|
|
93
|
+
|
|
94
|
+
# Check that sample IDs are always incremented with same
|
|
95
|
+
# sample size.
|
|
96
|
+
samples_per_event = np.unique(np.diff(sampleids))
|
|
97
|
+
if len(samples_per_event) > 1:
|
|
98
|
+
# This means the length of the fluorescence trace is not
|
|
99
|
+
# a constant. According to Philipp, this means the trace
|
|
100
|
+
# cannot be used.
|
|
101
|
+
warnings.warn("Ignoring trace data of '{}' ".format(tname)
|
|
102
|
+
+ "due to multiple values for samples per "
|
|
103
|
+
+ "event: {}".format(samples_per_event),
|
|
104
|
+
MultipleSamplesPerEventFound)
|
|
105
|
+
else:
|
|
106
|
+
# Load the trace data. The traces file is usually larger than
|
|
107
|
+
# the measurement file.
|
|
108
|
+
tdata = TdmsFile(str(tname))
|
|
109
|
+
for trace_key in dfn.FLUOR_TRACES:
|
|
110
|
+
group, ch = naming.tr_data_map[trace_key]
|
|
111
|
+
try:
|
|
112
|
+
trdat = tdata[group][ch].data
|
|
113
|
+
except KeyError:
|
|
114
|
+
pass
|
|
115
|
+
else:
|
|
116
|
+
if trdat is not None and trdat.size != 0:
|
|
117
|
+
# Split the input trace data into equally-spaced
|
|
118
|
+
# sections (we already tested that sampleids is
|
|
119
|
+
# equally-spaced).
|
|
120
|
+
spe = sampleids[1] - sampleids[0]
|
|
121
|
+
if (trdat.size % spe) == 0:
|
|
122
|
+
# this is the ideal case
|
|
123
|
+
trace_array = trdat.reshape(
|
|
124
|
+
trdat.size // spe, -1)
|
|
125
|
+
else:
|
|
126
|
+
# this is bad, but we allow it
|
|
127
|
+
trace_array = np.split(trdat, trdat.size//spe)
|
|
128
|
+
trace[trace_key] = trace_array
|
|
129
|
+
|
|
130
|
+
return trace
|
|
131
|
+
|
|
132
|
+
@staticmethod
|
|
133
|
+
def find_trace_file(mname):
|
|
134
|
+
"""Tries to find the traces tdms file name
|
|
135
|
+
|
|
136
|
+
Returns None if no trace file is found.
|
|
137
|
+
"""
|
|
138
|
+
mname = pathlib.Path(mname)
|
|
139
|
+
tname = None
|
|
140
|
+
|
|
141
|
+
if mname.exists():
|
|
142
|
+
cand = mname.with_name(mname.name[:-5] + "_traces.tdms")
|
|
143
|
+
if cand.exists():
|
|
144
|
+
tname = cand
|
|
145
|
+
|
|
146
|
+
return tname
|