dcnum 0.11.1__tar.gz → 0.11.2__tar.gz
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-0.11.1 → dcnum-0.11.2}/CHANGELOG +9 -0
- {dcnum-0.11.1 → dcnum-0.11.2}/PKG-INFO +1 -1
- {dcnum-0.11.1 → dcnum-0.11.2}/dcnum/_version.py +2 -2
- {dcnum-0.11.1 → dcnum-0.11.2}/dcnum/feat/event_extractor_manager_thread.py +7 -1
- {dcnum-0.11.1 → dcnum-0.11.2}/dcnum/feat/feat_moments/mt_legacy.py +16 -13
- {dcnum-0.11.1 → dcnum-0.11.2}/dcnum/feat/feat_texture/tex_all.py +7 -4
- {dcnum-0.11.1 → dcnum-0.11.2}/dcnum/meta/ppid.py +1 -1
- {dcnum-0.11.1 → dcnum-0.11.2}/dcnum/read/hdf5_data.py +22 -13
- {dcnum-0.11.1 → dcnum-0.11.2}/dcnum/segm/segmenter.py +1 -1
- {dcnum-0.11.1 → dcnum-0.11.2}/dcnum/segm/segmenter_cpu.py +7 -1
- {dcnum-0.11.1 → dcnum-0.11.2}/dcnum/segm/segmenter_manager_thread.py +8 -0
- {dcnum-0.11.1 → dcnum-0.11.2}/dcnum.egg-info/PKG-INFO +1 -1
- {dcnum-0.11.1 → dcnum-0.11.2}/dcnum.egg-info/SOURCES.txt +1 -0
- {dcnum-0.11.1 → dcnum-0.11.2}/tests/conftest.py +3 -0
- {dcnum-0.11.1 → dcnum-0.11.2}/tests/test_feat_brightness.py +23 -0
- dcnum-0.11.2/tests/test_feat_haralick.py +120 -0
- dcnum-0.11.2/tests/test_feat_moments_based.py +108 -0
- {dcnum-0.11.1 → dcnum-0.11.2}/tests/test_read_hdf5.py +37 -10
- dcnum-0.11.2/tests/test_segm_thresh.py +139 -0
- dcnum-0.11.1/tests/test_segm_thresh.py → dcnum-0.11.2/tests/test_segmenter.py +80 -132
- dcnum-0.11.1/tests/test_feat_haralick.py +0 -33
- dcnum-0.11.1/tests/test_feat_moments_based.py +0 -52
- {dcnum-0.11.1 → dcnum-0.11.2}/.github/workflows/check.yml +0 -0
- {dcnum-0.11.1 → dcnum-0.11.2}/.github/workflows/deploy_pypi.yml +0 -0
- {dcnum-0.11.1 → dcnum-0.11.2}/.gitignore +0 -0
- {dcnum-0.11.1 → dcnum-0.11.2}/.readthedocs.yml +0 -0
- {dcnum-0.11.1 → dcnum-0.11.2}/LICENSE +0 -0
- {dcnum-0.11.1 → dcnum-0.11.2}/README.rst +0 -0
- {dcnum-0.11.1 → dcnum-0.11.2}/dcnum/__init__.py +0 -0
- {dcnum-0.11.1 → dcnum-0.11.2}/dcnum/feat/__init__.py +0 -0
- {dcnum-0.11.1 → dcnum-0.11.2}/dcnum/feat/feat_background/__init__.py +0 -0
- {dcnum-0.11.1 → dcnum-0.11.2}/dcnum/feat/feat_background/base.py +0 -0
- {dcnum-0.11.1 → dcnum-0.11.2}/dcnum/feat/feat_background/bg_roll_median.py +0 -0
- {dcnum-0.11.1 → dcnum-0.11.2}/dcnum/feat/feat_background/bg_sparse_median.py +0 -0
- {dcnum-0.11.1 → dcnum-0.11.2}/dcnum/feat/feat_brightness/__init__.py +0 -0
- {dcnum-0.11.1 → dcnum-0.11.2}/dcnum/feat/feat_brightness/bright_all.py +0 -0
- {dcnum-0.11.1 → dcnum-0.11.2}/dcnum/feat/feat_brightness/common.py +0 -0
- {dcnum-0.11.1 → dcnum-0.11.2}/dcnum/feat/feat_moments/__init__.py +0 -0
- {dcnum-0.11.1 → dcnum-0.11.2}/dcnum/feat/feat_moments/ct_opencv.py +0 -0
- {dcnum-0.11.1 → dcnum-0.11.2}/dcnum/feat/feat_texture/__init__.py +0 -0
- {dcnum-0.11.1 → dcnum-0.11.2}/dcnum/feat/feat_texture/common.py +0 -0
- {dcnum-0.11.1 → dcnum-0.11.2}/dcnum/feat/gate.py +0 -0
- {dcnum-0.11.1 → dcnum-0.11.2}/dcnum/feat/queue_event_extractor.py +0 -0
- {dcnum-0.11.1 → dcnum-0.11.2}/dcnum/meta/__init__.py +0 -0
- {dcnum-0.11.1 → dcnum-0.11.2}/dcnum/read/__init__.py +0 -0
- {dcnum-0.11.1 → dcnum-0.11.2}/dcnum/read/cache.py +0 -0
- {dcnum-0.11.1 → dcnum-0.11.2}/dcnum/read/const.py +0 -0
- {dcnum-0.11.1 → dcnum-0.11.2}/dcnum/segm/__init__.py +0 -0
- {dcnum-0.11.1 → dcnum-0.11.2}/dcnum/segm/segm_thresh.py +0 -0
- {dcnum-0.11.1 → dcnum-0.11.2}/dcnum/segm/segmenter_gpu.py +0 -0
- {dcnum-0.11.1 → dcnum-0.11.2}/dcnum/write/__init__.py +0 -0
- {dcnum-0.11.1 → dcnum-0.11.2}/dcnum/write/deque_writer_thread.py +0 -0
- {dcnum-0.11.1 → dcnum-0.11.2}/dcnum/write/queue_collector_thread.py +0 -0
- {dcnum-0.11.1 → dcnum-0.11.2}/dcnum/write/writer.py +0 -0
- {dcnum-0.11.1 → dcnum-0.11.2}/dcnum.egg-info/dependency_links.txt +0 -0
- {dcnum-0.11.1 → dcnum-0.11.2}/dcnum.egg-info/requires.txt +0 -0
- {dcnum-0.11.1 → dcnum-0.11.2}/dcnum.egg-info/top_level.txt +0 -0
- {dcnum-0.11.1 → dcnum-0.11.2}/docs/conf.py +0 -0
- {dcnum-0.11.1 → dcnum-0.11.2}/docs/extensions/github_changelog.py +0 -0
- {dcnum-0.11.1 → dcnum-0.11.2}/docs/index.rst +0 -0
- {dcnum-0.11.1 → dcnum-0.11.2}/docs/requirements.txt +0 -0
- {dcnum-0.11.1 → dcnum-0.11.2}/pyproject.toml +0 -0
- {dcnum-0.11.1 → dcnum-0.11.2}/setup.cfg +0 -0
- {dcnum-0.11.1 → dcnum-0.11.2}/tests/data/fmt-hdf5_cytoshot_full-features_2023.zip +0 -0
- {dcnum-0.11.1 → dcnum-0.11.2}/tests/data/fmt-hdf5_cytoshot_full-features_legacy_allev_2023.zip +0 -0
- {dcnum-0.11.1 → dcnum-0.11.2}/tests/helper_methods.py +0 -0
- {dcnum-0.11.1 → dcnum-0.11.2}/tests/requirements.txt +0 -0
- {dcnum-0.11.1 → dcnum-0.11.2}/tests/test_feat_background_bg_roll_median.py +0 -0
- {dcnum-0.11.1 → dcnum-0.11.2}/tests/test_init.py +0 -0
- {dcnum-0.11.1 → dcnum-0.11.2}/tests/test_ppid.py +0 -0
- {dcnum-0.11.1 → dcnum-0.11.2}/tests/test_ppid_segm.py +0 -0
- {dcnum-0.11.1 → dcnum-0.11.2}/tests/test_read_concat_hdf5.py +0 -0
- {dcnum-0.11.1 → dcnum-0.11.2}/tests/test_write_deque_writer_thread.py +0 -0
- {dcnum-0.11.1 → dcnum-0.11.2}/tests/test_write_writer.py +0 -0
|
@@ -1,3 +1,12 @@
|
|
|
1
|
+
0.11.2
|
|
2
|
+
- meta: increment pipeline ID (texture feature computation)
|
|
3
|
+
- fix: HDF5Data was not pickable
|
|
4
|
+
- fix: HDF5Data did not properly handle tables
|
|
5
|
+
- enh: add context manager for CPUSegmenter
|
|
6
|
+
- enh: record and log execution time of segmentation and feature extraction
|
|
7
|
+
- enh: properly handle border case for computing contour-moments
|
|
8
|
+
- enh: properly handle empty images/masks in haralick texture features
|
|
9
|
+
- tests: do not use numba's JIT during testing (coverage)
|
|
1
10
|
0.11.1
|
|
2
11
|
- fix: fix GPUSegmenter labeling
|
|
3
12
|
0.11.0
|
|
@@ -65,7 +65,8 @@ class EventExtractorManagerThread(threading.Thread):
|
|
|
65
65
|
self.label_array = np.ctypeslib.as_array(
|
|
66
66
|
self.fe_kwargs["label_array"]).reshape(
|
|
67
67
|
self.data.image.chunk_shape)
|
|
68
|
-
|
|
68
|
+
#: Time counter for feature extraction
|
|
69
|
+
self.t_count = 0
|
|
69
70
|
#: Whether debugging is enabled
|
|
70
71
|
self.debug = debug
|
|
71
72
|
|
|
@@ -100,6 +101,8 @@ class EventExtractorManagerThread(threading.Thread):
|
|
|
100
101
|
unavailable_slots = 0
|
|
101
102
|
time.sleep(.1)
|
|
102
103
|
|
|
104
|
+
t1 = time.monotonic()
|
|
105
|
+
|
|
103
106
|
# We have a chunk, process it!
|
|
104
107
|
chunk = self.slot_chunks[cur_slot]
|
|
105
108
|
# Populate the labeling array for the workers
|
|
@@ -123,8 +126,10 @@ class EventExtractorManagerThread(threading.Thread):
|
|
|
123
126
|
self.slot_states[cur_slot] = "w"
|
|
124
127
|
|
|
125
128
|
self.logger.debug(f"Extracted one chunk: {chunk}")
|
|
129
|
+
self.t_count += time.monotonic() - t1
|
|
126
130
|
|
|
127
131
|
chunks_processed += 1
|
|
132
|
+
|
|
128
133
|
if chunks_processed == self.data.image.num_chunks:
|
|
129
134
|
break
|
|
130
135
|
|
|
@@ -132,3 +137,4 @@ class EventExtractorManagerThread(threading.Thread):
|
|
|
132
137
|
self.fe_kwargs["finalize_extraction"].value = True
|
|
133
138
|
[w.join() for w in workers]
|
|
134
139
|
self.logger.debug("Finished extraction.")
|
|
140
|
+
self.logger.info(f"Extraction time: {self.t_count:.1f}s")
|
|
@@ -10,22 +10,25 @@ def moments_based_features(mask, pixel_size):
|
|
|
10
10
|
|
|
11
11
|
size = mask.shape[0]
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
13
|
+
empty = np.full(size, np.nan, dtype=np.float64)
|
|
14
|
+
deform = np.copy(empty)
|
|
15
|
+
size_x = np.copy(empty)
|
|
16
|
+
size_y = np.copy(empty)
|
|
17
|
+
pos_x = np.copy(empty)
|
|
18
|
+
pos_y = np.copy(empty)
|
|
19
|
+
area_msd = np.copy(empty)
|
|
20
|
+
area_ratio = np.copy(empty)
|
|
21
|
+
area_um = np.copy(empty)
|
|
22
|
+
aspect = np.copy(empty)
|
|
23
|
+
tilt = np.copy(empty)
|
|
24
|
+
inert_ratio_cvx = np.copy(empty)
|
|
25
|
+
inert_ratio_raw = np.copy(empty)
|
|
26
|
+
inert_ratio_prnc = np.copy(empty)
|
|
26
27
|
|
|
27
28
|
for ii in range(size):
|
|
28
29
|
cont_raw = contour_single_opencv(mask[ii])
|
|
30
|
+
if len(cont_raw.shape) < 2:
|
|
31
|
+
continue
|
|
29
32
|
mu_raw = cv2.moments(cont_raw)
|
|
30
33
|
|
|
31
34
|
# convex hull
|
|
@@ -4,16 +4,16 @@ import numpy as np
|
|
|
4
4
|
from .common import haralick_names
|
|
5
5
|
|
|
6
6
|
|
|
7
|
-
def haralick_texture_features(
|
|
7
|
+
def haralick_texture_features(
|
|
8
|
+
mask, image=None, image_bg=None, image_corr=None):
|
|
8
9
|
# make sure we have a boolean array
|
|
9
10
|
mask = np.array(mask, dtype=bool)
|
|
10
11
|
size = mask.shape[0]
|
|
11
12
|
|
|
12
13
|
# compute features if necessary
|
|
13
|
-
if image_bg is not None
|
|
14
|
+
if image_bg is not None and image is not None and image_corr is None:
|
|
14
15
|
# Background-corrected brightness values
|
|
15
|
-
|
|
16
|
-
image_corr = np.array(image, dtype=np.int16) - image_bg
|
|
16
|
+
image_corr = np.array(image, dtype=np.int16) - image_bg
|
|
17
17
|
|
|
18
18
|
tex_dict = {}
|
|
19
19
|
empty = np.full(size, np.nan, dtype=np.float64)
|
|
@@ -29,6 +29,9 @@ def haralick_texture_features(image, mask, image_bg=None, image_corr=None):
|
|
|
29
29
|
# -> maximum value should be as small as possible
|
|
30
30
|
# - set pixels outside contour to zero (ignored areas, see mahotas)
|
|
31
31
|
maski = mask[ii]
|
|
32
|
+
if not np.any(maski):
|
|
33
|
+
# The mask is empty (nan values)
|
|
34
|
+
continue
|
|
32
35
|
if image_corr.shape[0] == 1:
|
|
33
36
|
# We have several masks for one image.
|
|
34
37
|
imcoi = image_corr[0]
|
|
@@ -31,9 +31,6 @@ class HDF5Data:
|
|
|
31
31
|
if isinstance(path, h5py.File):
|
|
32
32
|
self.h5 = path
|
|
33
33
|
path = path.filename
|
|
34
|
-
else:
|
|
35
|
-
self.h5 = None # is set in __setstate__
|
|
36
|
-
self._cache_scalar = {}
|
|
37
34
|
self.__setstate__({"path": path,
|
|
38
35
|
"pixel_size": pixel_size,
|
|
39
36
|
"md5_5m": md5_5m,
|
|
@@ -69,7 +66,25 @@ class HDF5Data:
|
|
|
69
66
|
warnings.warn(f"Feature {feat} not cached (possibly slow)")
|
|
70
67
|
return self.h5["events"][feat]
|
|
71
68
|
|
|
69
|
+
def __getstate__(self):
|
|
70
|
+
return {"path": self.path,
|
|
71
|
+
"pixel_size": self.pixel_size,
|
|
72
|
+
"md5_5m": self.md5_5m,
|
|
73
|
+
"meta": self.meta,
|
|
74
|
+
"logs": self.logs,
|
|
75
|
+
"tables": self.tables,
|
|
76
|
+
"image_cache_size": self.image.cache_size
|
|
77
|
+
}
|
|
78
|
+
|
|
72
79
|
def __setstate__(self, state):
|
|
80
|
+
# Make sure these properties exist (we rely on __init__, because
|
|
81
|
+
# we want this class to be pickable and __init__ is not called by
|
|
82
|
+
# `pickle.load`.
|
|
83
|
+
if not hasattr(self, "_cache_scalar"):
|
|
84
|
+
self._cache_scalar = {}
|
|
85
|
+
if not hasattr(self, "h5"):
|
|
86
|
+
self.h5 = None
|
|
87
|
+
|
|
73
88
|
self.path = state["path"]
|
|
74
89
|
|
|
75
90
|
self.md5_5m = state["md5_5m"]
|
|
@@ -100,7 +115,10 @@ class HDF5Data:
|
|
|
100
115
|
alog = [ll.decode("utf") for ll in alog]
|
|
101
116
|
self.logs[key] = alog
|
|
102
117
|
for tab in h5.get("tables", []):
|
|
103
|
-
|
|
118
|
+
tabdict = {}
|
|
119
|
+
for tkey in h5["tables"][tab].dtype.fields.keys():
|
|
120
|
+
tabdict[tkey] = h5["tables"][tab][tkey]
|
|
121
|
+
self.tables[tab] = tabdict
|
|
104
122
|
|
|
105
123
|
if state["pixel_size"] is not None:
|
|
106
124
|
self.pixel_size = state["pixel_size"]
|
|
@@ -137,15 +155,6 @@ class HDF5Data:
|
|
|
137
155
|
|
|
138
156
|
self.image_corr = ImageCorrCache(self.image, self.image_bg)
|
|
139
157
|
|
|
140
|
-
def __getstate__(self):
|
|
141
|
-
return {"path": self.path,
|
|
142
|
-
"pixel_size": self.pixel_size,
|
|
143
|
-
"md5_5m": self.md5_5m,
|
|
144
|
-
"meta": self.meta,
|
|
145
|
-
"logs": self.logs,
|
|
146
|
-
"tables": self.tables,
|
|
147
|
-
}
|
|
148
|
-
|
|
149
158
|
@functools.cache
|
|
150
159
|
def __len__(self):
|
|
151
160
|
return self.h5.attrs["experiment:event count"]
|
|
@@ -179,7 +179,7 @@ class Segmenter(abc.ABC):
|
|
|
179
179
|
|
|
180
180
|
if fill_holes:
|
|
181
181
|
# Floodfill only works with uint8 (too small) or int32
|
|
182
|
-
if
|
|
182
|
+
if labels.dtype != np.int32:
|
|
183
183
|
labels = np.array(labels, dtype=np.int32)
|
|
184
184
|
#
|
|
185
185
|
# from scipy import ndimage
|
|
@@ -31,6 +31,12 @@ class CPUSegmenter(Segmenter, abc.ABC):
|
|
|
31
31
|
# Tells the workers to stop
|
|
32
32
|
self.mp_shutdown = mp.Value("i", 0)
|
|
33
33
|
|
|
34
|
+
def __enter__(self):
|
|
35
|
+
return self
|
|
36
|
+
|
|
37
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
38
|
+
self.join_workers()
|
|
39
|
+
|
|
34
40
|
def __getstate__(self):
|
|
35
41
|
# Copy the object's state from self.__dict__ which contains
|
|
36
42
|
# all our instance attributes. Always use the dict.copy()
|
|
@@ -47,7 +53,7 @@ class CPUSegmenter(Segmenter, abc.ABC):
|
|
|
47
53
|
return state
|
|
48
54
|
|
|
49
55
|
def __setstate__(self, state):
|
|
50
|
-
# Restore instance attributes
|
|
56
|
+
# Restore instance attributes
|
|
51
57
|
self.__dict__.update(state)
|
|
52
58
|
|
|
53
59
|
@staticmethod
|
|
@@ -71,6 +71,8 @@ class SegmenterManagerThread(threading.Thread):
|
|
|
71
71
|
self.slot_chunks = slot_chunks
|
|
72
72
|
#: List containing the segmented labels of each slot
|
|
73
73
|
self.labels_list = [None] * len(self.slot_states)
|
|
74
|
+
#: Time counter for segmentation
|
|
75
|
+
self.t_count = 0
|
|
74
76
|
#: Whether running in debugging mode
|
|
75
77
|
self.debug = debug
|
|
76
78
|
|
|
@@ -96,6 +98,8 @@ class SegmenterManagerThread(threading.Thread):
|
|
|
96
98
|
empty_slots = 0
|
|
97
99
|
time.sleep(.01)
|
|
98
100
|
|
|
101
|
+
t1 = time.monotonic()
|
|
102
|
+
|
|
99
103
|
# We have a free slot to compute the segmentation
|
|
100
104
|
labels = self.segmenter.segment_chunk(
|
|
101
105
|
image_data=self.image_data,
|
|
@@ -111,7 +115,11 @@ class SegmenterManagerThread(threading.Thread):
|
|
|
111
115
|
self.slot_states[cur_slot] = "e"
|
|
112
116
|
self.logger.debug(f"Segmented one chunk: {chunk}")
|
|
113
117
|
|
|
118
|
+
self.t_count += time.monotonic() - t1
|
|
119
|
+
|
|
114
120
|
# Cleanup
|
|
115
121
|
if isinstance(self.segmenter, CPUSegmenter):
|
|
116
122
|
# Join the segmentation workers.
|
|
117
123
|
self.segmenter.join_workers()
|
|
124
|
+
|
|
125
|
+
self.logger.info(f"Segmentation time: {self.t_count:.1f}s")
|
|
@@ -63,6 +63,7 @@ tests/test_ppid_segm.py
|
|
|
63
63
|
tests/test_read_concat_hdf5.py
|
|
64
64
|
tests/test_read_hdf5.py
|
|
65
65
|
tests/test_segm_thresh.py
|
|
66
|
+
tests/test_segmenter.py
|
|
66
67
|
tests/test_write_deque_writer_thread.py
|
|
67
68
|
tests/test_write_writer.py
|
|
68
69
|
tests/data/fmt-hdf5_cytoshot_full-features_2023.zip
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import atexit
|
|
2
|
+
import os
|
|
2
3
|
import shutil
|
|
3
4
|
import tempfile
|
|
4
5
|
import time
|
|
@@ -15,3 +16,5 @@ def pytest_configure(config):
|
|
|
15
16
|
"""
|
|
16
17
|
tempfile.tempdir = TMPDIR
|
|
17
18
|
atexit.register(shutil.rmtree, TMPDIR, ignore_errors=True)
|
|
19
|
+
# Disable JIT compiler during testing for coverage
|
|
20
|
+
os.environ.setdefault("NUMBA_DISABLE_JIT", "1")
|
|
@@ -31,3 +31,26 @@ def test_basic_brightness():
|
|
|
31
31
|
# control test
|
|
32
32
|
assert not np.allclose(h5["events"]["bright_perc_10"][:],
|
|
33
33
|
data["bright_perc_90"])
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def test_basic_brightness_single_image():
|
|
37
|
+
# This original file was generated with dcevent for reference.
|
|
38
|
+
path = retrieve_data(data_path /
|
|
39
|
+
"fmt-hdf5_cytoshot_full-features_2023.zip")
|
|
40
|
+
# Make data available
|
|
41
|
+
with h5py.File(path) as h5:
|
|
42
|
+
data = feat_brightness.brightness_features(
|
|
43
|
+
image=h5["events/image"][1][np.newaxis],
|
|
44
|
+
image_bg=h5["events/image_bg"][1][np.newaxis],
|
|
45
|
+
mask=h5["events/mask"][1][np.newaxis],
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
assert np.allclose(data["bright_bc_avg"][0],
|
|
49
|
+
-43.75497215592681,
|
|
50
|
+
atol=0, rtol=1e-10)
|
|
51
|
+
for feat in feat_brightness.brightness_names:
|
|
52
|
+
assert np.allclose(h5["events"][feat][1],
|
|
53
|
+
data[feat][0]), f"Feature {feat} mismatch!"
|
|
54
|
+
# control test
|
|
55
|
+
assert not np.allclose(h5["events"]["bright_perc_10"][1],
|
|
56
|
+
data["bright_perc_90"][0])
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import pathlib
|
|
2
|
+
|
|
3
|
+
import h5py
|
|
4
|
+
import numpy as np
|
|
5
|
+
|
|
6
|
+
from dcnum.feat import feat_texture
|
|
7
|
+
|
|
8
|
+
from helper_methods import retrieve_data
|
|
9
|
+
|
|
10
|
+
data_path = pathlib.Path(__file__).parent / "data"
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def test_basic_haralick():
|
|
14
|
+
# This original file was generated with dcevent for reference.
|
|
15
|
+
path = retrieve_data(data_path /
|
|
16
|
+
"fmt-hdf5_cytoshot_full-features_2023.zip")
|
|
17
|
+
# Make data available
|
|
18
|
+
with h5py.File(path) as h5:
|
|
19
|
+
ret_arr = feat_texture.haralick_texture_features(
|
|
20
|
+
image=h5["events/image"][:],
|
|
21
|
+
image_bg=h5["events/image_bg"][:],
|
|
22
|
+
mask=h5["events/mask"][:],
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
assert np.allclose(ret_arr["tex_asm_avg"][1],
|
|
26
|
+
0.001514295993357114,
|
|
27
|
+
atol=0, rtol=1e-10)
|
|
28
|
+
for feat in feat_texture.haralick_names:
|
|
29
|
+
assert np.allclose(h5["events"][feat],
|
|
30
|
+
ret_arr[feat])
|
|
31
|
+
# control test
|
|
32
|
+
assert not np.allclose(h5["events"]["tex_asm_avg"],
|
|
33
|
+
ret_arr["tex_asm_ptp"])
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def test_empty_image():
|
|
37
|
+
masks = np.array([
|
|
38
|
+
[0, 0, 0, 0, 0, 0],
|
|
39
|
+
[0, 0, 0, 0, 0, 0],
|
|
40
|
+
[0, 0, 1, 1, 0, 0],
|
|
41
|
+
[0, 0, 1, 1, 0, 0],
|
|
42
|
+
[0, 0, 0, 0, 0, 0],
|
|
43
|
+
[0, 0, 0, 0, 0, 0],
|
|
44
|
+
], dtype=bool)[np.newaxis]
|
|
45
|
+
image_corr = np.zeros(6*6, dtype=np.int16).reshape(1, 6, 6)
|
|
46
|
+
tex = feat_texture.haralick_texture_features(
|
|
47
|
+
image_corr=image_corr,
|
|
48
|
+
mask=masks,
|
|
49
|
+
)
|
|
50
|
+
assert np.allclose(tex["tex_con_avg"][0], 0)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def test_empty_mask():
|
|
54
|
+
masks = np.array([
|
|
55
|
+
[0, 0, 0, 0, 0, 0],
|
|
56
|
+
[0, 0, 0, 0, 0, 0],
|
|
57
|
+
[0, 0, 0, 0, 0, 0],
|
|
58
|
+
[0, 0, 0, 0, 0, 0],
|
|
59
|
+
[0, 0, 0, 0, 0, 0],
|
|
60
|
+
[0, 0, 0, 0, 0, 0],
|
|
61
|
+
], dtype=bool)[np.newaxis]
|
|
62
|
+
image_corr = np.arange(6*6, dtype=np.int16).reshape(1, 6, 6)
|
|
63
|
+
tex = feat_texture.haralick_texture_features(
|
|
64
|
+
image_corr=image_corr,
|
|
65
|
+
mask=masks,
|
|
66
|
+
)
|
|
67
|
+
assert np.isnan(tex["tex_con_avg"][0])
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def test_1d_mask_image():
|
|
71
|
+
masks = np.array([
|
|
72
|
+
[0, 0, 0, 0, 0, 0],
|
|
73
|
+
[0, 0, 0, 0, 0, 0],
|
|
74
|
+
[0, 0, 1, 0, 0, 0],
|
|
75
|
+
[0, 0, 1, 0, 0, 0],
|
|
76
|
+
[0, 0, 0, 0, 0, 0],
|
|
77
|
+
[0, 0, 0, 0, 0, 0],
|
|
78
|
+
], dtype=bool)[np.newaxis]
|
|
79
|
+
image_corr = np.arange(6*6, dtype=np.int16).reshape(1, 6, 6)
|
|
80
|
+
tex = feat_texture.haralick_texture_features(
|
|
81
|
+
image_corr=image_corr,
|
|
82
|
+
mask=masks,
|
|
83
|
+
)
|
|
84
|
+
assert np.isnan(tex["tex_con_avg"][0])
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def test_nd_mask_with_1d_image():
|
|
88
|
+
mask = np.array([
|
|
89
|
+
[0, 0, 0, 0, 0, 0],
|
|
90
|
+
[0, 0, 0, 0, 0, 0],
|
|
91
|
+
[0, 0, 1, 1, 0, 0],
|
|
92
|
+
[0, 0, 1, 1, 0, 0],
|
|
93
|
+
[0, 0, 0, 0, 0, 0],
|
|
94
|
+
[0, 0, 0, 0, 0, 0],
|
|
95
|
+
], dtype=bool)
|
|
96
|
+
masks = np.stack([mask, mask, mask, mask])
|
|
97
|
+
image_corr = np.arange(6*6, dtype=np.int16).reshape(1, 6, 6)
|
|
98
|
+
tex = feat_texture.haralick_texture_features(
|
|
99
|
+
image_corr=image_corr,
|
|
100
|
+
mask=masks,
|
|
101
|
+
)
|
|
102
|
+
assert len(tex["tex_con_avg"]) == 4
|
|
103
|
+
assert np.allclose(tex["tex_con_avg"][0], 27.75)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def test_simple_mask_image():
|
|
107
|
+
masks = np.array([
|
|
108
|
+
[0, 0, 0, 0, 0, 0],
|
|
109
|
+
[0, 0, 0, 0, 0, 0],
|
|
110
|
+
[0, 0, 1, 1, 0, 0],
|
|
111
|
+
[0, 0, 1, 1, 0, 0],
|
|
112
|
+
[0, 0, 0, 0, 0, 0],
|
|
113
|
+
[0, 0, 0, 0, 0, 0],
|
|
114
|
+
], dtype=bool)[np.newaxis]
|
|
115
|
+
image_corr = np.arange(6*6, dtype=np.int16).reshape(1, 6, 6)
|
|
116
|
+
tex = feat_texture.haralick_texture_features(
|
|
117
|
+
image_corr=image_corr,
|
|
118
|
+
mask=masks,
|
|
119
|
+
)
|
|
120
|
+
assert np.allclose(tex["tex_con_avg"][0], 27.75)
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import pathlib
|
|
2
|
+
|
|
3
|
+
import h5py
|
|
4
|
+
import numpy as np
|
|
5
|
+
|
|
6
|
+
from dcnum.feat import feat_moments
|
|
7
|
+
|
|
8
|
+
from helper_methods import retrieve_data
|
|
9
|
+
|
|
10
|
+
data_path = pathlib.Path(__file__).parent / "data"
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def test_moments_based_features():
|
|
14
|
+
# This original file was generated with dcevent for reference.
|
|
15
|
+
path = retrieve_data(data_path /
|
|
16
|
+
"fmt-hdf5_cytoshot_full-features_2023.zip")
|
|
17
|
+
feats = [
|
|
18
|
+
"deform",
|
|
19
|
+
"size_x",
|
|
20
|
+
"size_y",
|
|
21
|
+
"pos_x",
|
|
22
|
+
"pos_y",
|
|
23
|
+
"area_msd",
|
|
24
|
+
"area_ratio",
|
|
25
|
+
"area_um",
|
|
26
|
+
"aspect",
|
|
27
|
+
"tilt",
|
|
28
|
+
"inert_ratio_cvx",
|
|
29
|
+
"inert_ratio_raw",
|
|
30
|
+
"inert_ratio_prnc",
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
# Make data available
|
|
34
|
+
with h5py.File(path) as h5:
|
|
35
|
+
data = feat_moments.moments_based_features(
|
|
36
|
+
mask=h5["events/mask"][:],
|
|
37
|
+
pixel_size=0.2645
|
|
38
|
+
)
|
|
39
|
+
for feat in feats:
|
|
40
|
+
if feat.count("inert"):
|
|
41
|
+
rtol = 2e-5
|
|
42
|
+
atol = 1e-8
|
|
43
|
+
else:
|
|
44
|
+
rtol = 1e-5
|
|
45
|
+
atol = 1e-8
|
|
46
|
+
assert np.allclose(h5["events"][feat][:],
|
|
47
|
+
data[feat],
|
|
48
|
+
rtol=rtol,
|
|
49
|
+
atol=atol), f"Feature {feat} mismatch!"
|
|
50
|
+
# control test
|
|
51
|
+
assert not np.allclose(h5["events"]["inert_ratio_cvx"][:],
|
|
52
|
+
data["tilt"])
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def test_mask_0d():
|
|
56
|
+
masks = np.array([
|
|
57
|
+
[0, 0, 0, 0, 0, 0],
|
|
58
|
+
[0, 0, 0, 0, 0, 0],
|
|
59
|
+
[0, 0, 1, 0, 0, 0],
|
|
60
|
+
[0, 0, 0, 0, 0, 0],
|
|
61
|
+
[0, 0, 0, 0, 0, 0],
|
|
62
|
+
[0, 0, 0, 0, 0, 0],
|
|
63
|
+
], dtype=bool)[np.newaxis]
|
|
64
|
+
data = feat_moments.moments_based_features(
|
|
65
|
+
mask=masks,
|
|
66
|
+
pixel_size=0.2645
|
|
67
|
+
)
|
|
68
|
+
assert data["deform"].shape == (1,)
|
|
69
|
+
assert np.isnan(data["deform"][0])
|
|
70
|
+
assert np.isnan(data["area_um"][0])
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def test_mask_1d():
|
|
74
|
+
masks = np.array([
|
|
75
|
+
[0, 0, 0, 0, 0, 0],
|
|
76
|
+
[0, 0, 0, 0, 0, 0],
|
|
77
|
+
[0, 0, 1, 0, 0, 0],
|
|
78
|
+
[0, 0, 1, 0, 0, 0],
|
|
79
|
+
[0, 0, 0, 0, 0, 0],
|
|
80
|
+
[0, 0, 0, 0, 0, 0],
|
|
81
|
+
], dtype=bool)[np.newaxis]
|
|
82
|
+
data = feat_moments.moments_based_features(
|
|
83
|
+
mask=masks,
|
|
84
|
+
pixel_size=0.2645
|
|
85
|
+
)
|
|
86
|
+
assert data["deform"].shape == (1,)
|
|
87
|
+
assert np.isnan(data["deform"][0])
|
|
88
|
+
assert np.isnan(data["area_um"][0])
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def test_mask_2d():
|
|
92
|
+
masks = np.array([
|
|
93
|
+
[0, 0, 0, 0, 0, 0],
|
|
94
|
+
[0, 0, 0, 0, 0, 0],
|
|
95
|
+
[0, 0, 1, 1, 0, 0],
|
|
96
|
+
[0, 0, 1, 1, 0, 0],
|
|
97
|
+
[0, 0, 0, 0, 0, 0],
|
|
98
|
+
[0, 0, 0, 0, 0, 0],
|
|
99
|
+
], dtype=bool)[np.newaxis]
|
|
100
|
+
data = feat_moments.moments_based_features(
|
|
101
|
+
mask=masks,
|
|
102
|
+
pixel_size=0.2645
|
|
103
|
+
)
|
|
104
|
+
assert data["deform"].shape == (1,)
|
|
105
|
+
# This is the deformation of a square (compared to circle)
|
|
106
|
+
assert np.allclose(data["deform"][0], 0.11377307454724206)
|
|
107
|
+
# Without moments-based computation, this would be 4*pxsize=0.066125
|
|
108
|
+
assert np.allclose(data["area_um"][0], 0.06996025)
|
|
@@ -134,11 +134,8 @@ def test_pickling_state():
|
|
|
134
134
|
|
|
135
135
|
h5d1 = read.HDF5Data(path)
|
|
136
136
|
h5d1.pixel_size = 0.124
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
state2 = pickle.loads(pstate)
|
|
141
|
-
h5d2 = read.HDF5Data(**state2)
|
|
137
|
+
pstate = pickle.dumps(h5d1)
|
|
138
|
+
h5d2 = pickle.loads(pstate)
|
|
142
139
|
assert h5d1.md5_5m == h5d2.md5_5m
|
|
143
140
|
assert h5d1.md5_5m == h5d2.md5_5m
|
|
144
141
|
assert h5d1.pixel_size == h5d2.pixel_size
|
|
@@ -150,10 +147,40 @@ def test_pickling_state_logs():
|
|
|
150
147
|
data_path / "fmt-hdf5_cytoshot_full-features_legacy_allev_2023.zip")
|
|
151
148
|
h5d1 = read.HDF5Data(path)
|
|
152
149
|
h5d1.pixel_size = 0.124
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
state2 = pickle.loads(pstate)
|
|
157
|
-
h5d2 = read.HDF5Data(**state2)
|
|
150
|
+
pstate = pickle.dumps(h5d1)
|
|
151
|
+
h5d2 = pickle.loads(pstate)
|
|
152
|
+
assert h5d1.logs
|
|
158
153
|
for lk in h5d1.logs:
|
|
159
154
|
assert h5d1.logs[lk] == h5d2.logs[lk]
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def test_pickling_state_tables():
|
|
158
|
+
path = retrieve_data(
|
|
159
|
+
data_path / "fmt-hdf5_cytoshot_full-features_legacy_allev_2023.zip")
|
|
160
|
+
# The original file does not contain any tables, so we write
|
|
161
|
+
# generate a table
|
|
162
|
+
columns = ["alot", "of", "tables"]
|
|
163
|
+
ds_dt = np.dtype({'names': columns,
|
|
164
|
+
'formats': [float] * len(columns)})
|
|
165
|
+
tab_data = np.zeros((11, len(columns)))
|
|
166
|
+
tab_data[:, 0] = np.arange(11)
|
|
167
|
+
tab_data[:, 1] = 1000
|
|
168
|
+
tab_data[:, 2] = np.linspace(1, np.sqrt(2), 11)
|
|
169
|
+
rec_arr = np.rec.array(tab_data, dtype=ds_dt)
|
|
170
|
+
|
|
171
|
+
# add table to source file
|
|
172
|
+
with h5py.File(path, "a") as h5:
|
|
173
|
+
h5tab = h5.require_group("tables")
|
|
174
|
+
h5tab.create_dataset(name="sample_table",
|
|
175
|
+
data=rec_arr)
|
|
176
|
+
|
|
177
|
+
h5d1 = read.HDF5Data(path)
|
|
178
|
+
h5d1.pixel_size = 0.124
|
|
179
|
+
pstate = pickle.dumps(h5d1)
|
|
180
|
+
h5d2 = pickle.loads(pstate)
|
|
181
|
+
assert h5d1.tables
|
|
182
|
+
table = h5d1.tables["sample_table"]
|
|
183
|
+
assert len(table) == 3
|
|
184
|
+
for lk in table:
|
|
185
|
+
assert np.allclose(h5d1.tables["sample_table"][lk],
|
|
186
|
+
h5d2.tables["sample_table"][lk])
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import multiprocessing as mp
|
|
2
|
+
import pathlib
|
|
3
|
+
|
|
4
|
+
from dcnum import segm
|
|
5
|
+
import h5py
|
|
6
|
+
import numpy as np
|
|
7
|
+
from skimage import morphology
|
|
8
|
+
|
|
9
|
+
import pytest
|
|
10
|
+
|
|
11
|
+
from helper_methods import retrieve_data
|
|
12
|
+
|
|
13
|
+
data_path = pathlib.Path(__file__).parent / "data"
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def test_segm_thresh_basic():
|
|
17
|
+
"""Basic thresholding segmenter
|
|
18
|
+
|
|
19
|
+
The segmenter is equivalent to the old dcevent legacy segmenter with
|
|
20
|
+
the options legacy:t=-6^bl=0^bi=0^d=1:cle=1^f=1^clo=3
|
|
21
|
+
(no blur, no binaryops, clear borders, fill holes, closing disk 3).
|
|
22
|
+
Since in the dcevent pipeline, the data are gated and small objects
|
|
23
|
+
are removed, we have to do this here manually before comparing mask
|
|
24
|
+
images.
|
|
25
|
+
"""
|
|
26
|
+
path = retrieve_data(
|
|
27
|
+
data_path / "fmt-hdf5_cytoshot_full-features_legacy_allev_2023.zip")
|
|
28
|
+
|
|
29
|
+
# Get all the relevant information
|
|
30
|
+
with h5py.File(path) as h5:
|
|
31
|
+
image = h5["events/image"][:]
|
|
32
|
+
image_bg = h5["events/image_bg"][:]
|
|
33
|
+
mask = h5["events/mask"][:]
|
|
34
|
+
frame = h5["events/frame"][:]
|
|
35
|
+
|
|
36
|
+
# Concatenate the masks
|
|
37
|
+
frame_u, indices = np.unique(frame, return_index=True)
|
|
38
|
+
image_u = image[indices]
|
|
39
|
+
image_bg_u = image_bg[indices]
|
|
40
|
+
mask_u = np.zeros_like(image_u, dtype=bool)
|
|
41
|
+
for ii, fr in enumerate(frame):
|
|
42
|
+
idx = np.where(frame_u == fr)[0]
|
|
43
|
+
mask_u[idx] = np.logical_or(mask_u[idx], mask[ii])
|
|
44
|
+
|
|
45
|
+
image_u_c = np.array(image_u, dtype=int) - image_bg_u
|
|
46
|
+
|
|
47
|
+
sm = segm.segm_thresh.SegmentThresh(thresh=-6,
|
|
48
|
+
kwargs_mask={"closing_disk": 3})
|
|
49
|
+
for ii in range(len(frame_u)):
|
|
50
|
+
labels_seg = sm.segment_frame(image_u_c[ii])
|
|
51
|
+
mask_seg = np.array(labels_seg, dtype=bool)
|
|
52
|
+
# Remove small objects, because this is not implemented in the
|
|
53
|
+
# segmenter class as it would be part of gating.
|
|
54
|
+
mask_seg = morphology.remove_small_objects(mask_seg, min_size=10)
|
|
55
|
+
assert np.all(mask_seg == mask_u[ii]), f"masks not matching at {ii}"
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@pytest.mark.parametrize("worker_type", ["thread", "process"])
|
|
59
|
+
def test_segm_thresh_segment_batch(worker_type):
|
|
60
|
+
debug = worker_type == "thread"
|
|
61
|
+
path = retrieve_data(
|
|
62
|
+
data_path / "fmt-hdf5_cytoshot_full-features_legacy_allev_2023.zip")
|
|
63
|
+
|
|
64
|
+
# Get all the relevant information
|
|
65
|
+
with h5py.File(path) as h5:
|
|
66
|
+
image = h5["events/image"][:]
|
|
67
|
+
image_bg = h5["events/image_bg"][:]
|
|
68
|
+
mask = h5["events/mask"][:]
|
|
69
|
+
frame = h5["events/frame"][:]
|
|
70
|
+
|
|
71
|
+
# Concatenate the masks
|
|
72
|
+
frame_u, indices = np.unique(frame, return_index=True)
|
|
73
|
+
image_u = image[indices]
|
|
74
|
+
image_bg_u = image_bg[indices]
|
|
75
|
+
mask_u = np.zeros_like(image_u, dtype=bool)
|
|
76
|
+
for ii, fr in enumerate(frame):
|
|
77
|
+
idx = np.where(frame_u == fr)[0]
|
|
78
|
+
mask_u[idx] = np.logical_or(mask_u[idx], mask[ii])
|
|
79
|
+
|
|
80
|
+
image_u_c = np.array(image_u, dtype=int) - image_bg_u
|
|
81
|
+
|
|
82
|
+
sm = segm.segm_thresh.SegmentThresh(thresh=-6,
|
|
83
|
+
debug=debug,
|
|
84
|
+
kwargs_mask={"closing_disk": 3})
|
|
85
|
+
|
|
86
|
+
labels_seg = sm.segment_batch(image_u_c, start=0, stop=5)
|
|
87
|
+
assert labels_seg is sm.labels_array
|
|
88
|
+
assert np.all(np.array(labels_seg, dtype=bool) == sm.mask_array)
|
|
89
|
+
# tell workers to stop
|
|
90
|
+
sm.join_workers()
|
|
91
|
+
|
|
92
|
+
for ii in range(len(frame_u)):
|
|
93
|
+
mask_seg = np.array(labels_seg[ii], dtype=bool)
|
|
94
|
+
# Remove small objects, because this is not implemented in the
|
|
95
|
+
# segmenter class as it would be part of gating.
|
|
96
|
+
mask_seg = morphology.remove_small_objects(mask_seg, min_size=10)
|
|
97
|
+
assert np.all(mask_seg == mask_u[ii]), f"masks not matching at {ii}"
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
@pytest.mark.parametrize("worker_type", ["thread", "process"])
|
|
101
|
+
def test_segm_thresh_segment_batch_large(worker_type):
|
|
102
|
+
debug = worker_type == "thread"
|
|
103
|
+
|
|
104
|
+
# Create fake data
|
|
105
|
+
mask = np.zeros((121, 80, 200), dtype=bool)
|
|
106
|
+
mask[:, 10:71, 100:161] = morphology.disk(30).reshape(-1, 61, 61)
|
|
107
|
+
image = -10 * mask
|
|
108
|
+
|
|
109
|
+
sm = segm.segm_thresh.SegmentThresh(thresh=-6,
|
|
110
|
+
kwargs_mask={"closing_disk": 3},
|
|
111
|
+
debug=debug)
|
|
112
|
+
|
|
113
|
+
labels_seg_1 = np.copy(
|
|
114
|
+
sm.segment_batch(image, start=0, stop=101))
|
|
115
|
+
|
|
116
|
+
assert labels_seg_1.dtype == np.uint16 # uint8 is not enough
|
|
117
|
+
assert sm.mp_batch_index.value == 0
|
|
118
|
+
if worker_type == "thread":
|
|
119
|
+
assert len(sm._mp_workers) == 1
|
|
120
|
+
assert sm.mp_batch_worker.value == 1
|
|
121
|
+
else:
|
|
122
|
+
# This will fail if you have too many CPUs in your system
|
|
123
|
+
assert len(sm._mp_workers) == mp.cpu_count()
|
|
124
|
+
# Check whether all processes did their deeds
|
|
125
|
+
assert sm.mp_batch_worker.value == mp.cpu_count()
|
|
126
|
+
|
|
127
|
+
labels_seg_2 = np.copy(
|
|
128
|
+
sm.segment_batch(image, start=101, stop=121))
|
|
129
|
+
|
|
130
|
+
# tell workers to stop
|
|
131
|
+
sm.join_workers()
|
|
132
|
+
|
|
133
|
+
for ii in range(101):
|
|
134
|
+
mask_seg = np.array(labels_seg_1[ii], dtype=bool)
|
|
135
|
+
assert np.all(mask_seg == mask[ii]), f"masks not matching at {ii}"
|
|
136
|
+
|
|
137
|
+
for jj in range(101, 121):
|
|
138
|
+
mask_seg = np.array(labels_seg_2[jj - 101], dtype=bool)
|
|
139
|
+
assert np.all(mask_seg == mask[jj]), f"masks not matching at {jj}"
|
|
@@ -1,145 +1,36 @@
|
|
|
1
|
-
import multiprocessing as mp
|
|
2
1
|
import pathlib
|
|
3
2
|
|
|
4
3
|
from dcnum import segm
|
|
5
|
-
import h5py
|
|
6
4
|
import numpy as np
|
|
7
|
-
from skimage import morphology
|
|
8
|
-
|
|
9
|
-
import pytest
|
|
10
|
-
|
|
11
|
-
from helper_methods import retrieve_data
|
|
12
5
|
|
|
13
6
|
data_path = pathlib.Path(__file__).parent / "data"
|
|
14
7
|
|
|
15
8
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
mask = h5["events/mask"][:]
|
|
34
|
-
frame = h5["events/frame"][:]
|
|
35
|
-
|
|
36
|
-
# Concatenate the masks
|
|
37
|
-
frame_u, indices = np.unique(frame, return_index=True)
|
|
38
|
-
image_u = image[indices]
|
|
39
|
-
image_bg_u = image_bg[indices]
|
|
40
|
-
mask_u = np.zeros_like(image_u, dtype=bool)
|
|
41
|
-
for ii, fr in enumerate(frame):
|
|
42
|
-
idx = np.where(frame_u == fr)[0]
|
|
43
|
-
mask_u[idx] = np.logical_or(mask_u[idx], mask[ii])
|
|
44
|
-
|
|
45
|
-
image_u_c = np.array(image_u, dtype=int) - image_bg_u
|
|
46
|
-
|
|
47
|
-
sm = segm.segm_thresh.SegmentThresh(thresh=-6,
|
|
48
|
-
kwargs_mask={"closing_disk": 3})
|
|
49
|
-
for ii in range(len(frame_u)):
|
|
50
|
-
labels_seg = sm.segment_frame(image_u_c[ii])
|
|
51
|
-
mask_seg = np.array(labels_seg, dtype=bool)
|
|
52
|
-
# Remove small objects, because this is not implemented in the
|
|
53
|
-
# segmenter class as it would be part of gating.
|
|
54
|
-
mask_seg = morphology.remove_small_objects(mask_seg, min_size=10)
|
|
55
|
-
assert np.all(mask_seg == mask_u[ii]), f"masks not matching at {ii}"
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
@pytest.mark.parametrize("worker_type", ["thread", "process"])
|
|
59
|
-
def test_segm_thresh_segment_batch(worker_type):
|
|
60
|
-
debug = worker_type == "thread"
|
|
61
|
-
path = retrieve_data(
|
|
62
|
-
data_path / "fmt-hdf5_cytoshot_full-features_legacy_allev_2023.zip")
|
|
63
|
-
|
|
64
|
-
# Get all the relevant information
|
|
65
|
-
with h5py.File(path) as h5:
|
|
66
|
-
image = h5["events/image"][:]
|
|
67
|
-
image_bg = h5["events/image_bg"][:]
|
|
68
|
-
mask = h5["events/mask"][:]
|
|
69
|
-
frame = h5["events/frame"][:]
|
|
70
|
-
|
|
71
|
-
# Concatenate the masks
|
|
72
|
-
frame_u, indices = np.unique(frame, return_index=True)
|
|
73
|
-
image_u = image[indices]
|
|
74
|
-
image_bg_u = image_bg[indices]
|
|
75
|
-
mask_u = np.zeros_like(image_u, dtype=bool)
|
|
76
|
-
for ii, fr in enumerate(frame):
|
|
77
|
-
idx = np.where(frame_u == fr)[0]
|
|
78
|
-
mask_u[idx] = np.logical_or(mask_u[idx], mask[ii])
|
|
79
|
-
|
|
80
|
-
image_u_c = np.array(image_u, dtype=int) - image_bg_u
|
|
81
|
-
|
|
82
|
-
sm = segm.segm_thresh.SegmentThresh(thresh=-6,
|
|
83
|
-
debug=debug,
|
|
84
|
-
kwargs_mask={"closing_disk": 3})
|
|
85
|
-
|
|
86
|
-
labels_seg = sm.segment_batch(image_u_c, start=0, stop=5)
|
|
87
|
-
assert labels_seg is sm.labels_array
|
|
88
|
-
assert np.all(np.array(labels_seg, dtype=bool) == sm.mask_array)
|
|
89
|
-
# tell workers to stop
|
|
90
|
-
sm.join_workers()
|
|
91
|
-
|
|
92
|
-
for ii in range(len(frame_u)):
|
|
93
|
-
mask_seg = np.array(labels_seg[ii], dtype=bool)
|
|
94
|
-
# Remove small objects, because this is not implemented in the
|
|
95
|
-
# segmenter class as it would be part of gating.
|
|
96
|
-
mask_seg = morphology.remove_small_objects(mask_seg, min_size=10)
|
|
97
|
-
assert np.all(mask_seg == mask_u[ii]), f"masks not matching at {ii}"
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
@pytest.mark.parametrize("worker_type", ["thread", "process"])
|
|
101
|
-
def test_segm_thresh_segment_batch_large(worker_type):
|
|
102
|
-
debug = worker_type == "thread"
|
|
103
|
-
|
|
104
|
-
# Create fake data
|
|
105
|
-
mask = np.zeros((121, 80, 200), dtype=bool)
|
|
106
|
-
mask[:, 10:71, 100:161] = morphology.disk(30).reshape(-1, 61, 61)
|
|
107
|
-
image = -10 * mask
|
|
108
|
-
|
|
109
|
-
sm = segm.segm_thresh.SegmentThresh(thresh=-6,
|
|
110
|
-
kwargs_mask={"closing_disk": 3},
|
|
111
|
-
debug=debug)
|
|
112
|
-
|
|
113
|
-
labels_seg_1 = np.copy(
|
|
114
|
-
sm.segment_batch(image, start=0, stop=101))
|
|
115
|
-
|
|
116
|
-
assert labels_seg_1.dtype == np.uint16 # uint8 is not enough
|
|
117
|
-
assert sm.mp_batch_index.value == 0
|
|
118
|
-
if worker_type == "thread":
|
|
119
|
-
assert len(sm._mp_workers) == 1
|
|
120
|
-
assert sm.mp_batch_worker.value == 1
|
|
121
|
-
else:
|
|
122
|
-
# This will fail if you have too many CPUs in your system
|
|
123
|
-
assert len(sm._mp_workers) == mp.cpu_count()
|
|
124
|
-
# Check whether all processes did their deeds
|
|
125
|
-
assert sm.mp_batch_worker.value == mp.cpu_count()
|
|
126
|
-
|
|
127
|
-
labels_seg_2 = np.copy(
|
|
128
|
-
sm.segment_batch(image, start=101, stop=121))
|
|
129
|
-
|
|
130
|
-
# tell workers to stop
|
|
131
|
-
sm.join_workers()
|
|
132
|
-
|
|
133
|
-
for ii in range(101):
|
|
134
|
-
mask_seg = np.array(labels_seg_1[ii], dtype=bool)
|
|
135
|
-
assert np.all(mask_seg == mask[ii]), f"masks not matching at {ii}"
|
|
9
|
+
class MockImageData:
|
|
10
|
+
mask = np.array([
|
|
11
|
+
[0, 0, 0, 0, 0, 0, 0, 0],
|
|
12
|
+
[0, 0, 1, 1, 1, 0, 0, 0],
|
|
13
|
+
[0, 0, 1, 0, 1, 0, 0, 0], # filled, 1
|
|
14
|
+
[0, 0, 1, 1, 1, 0, 0, 0],
|
|
15
|
+
[0, 0, 0, 0, 0, 0, 0, 0],
|
|
16
|
+
[0, 0, 0, 0, 0, 0, 0, 0],
|
|
17
|
+
[0, 0, 0, 0, 1, 1, 1, 1],
|
|
18
|
+
[0, 0, 0, 0, 0, 1, 1, 1], # border, 2
|
|
19
|
+
[0, 0, 0, 0, 0, 1, 1, 1],
|
|
20
|
+
[0, 1, 1, 1, 0, 0, 0, 0],
|
|
21
|
+
[0, 0, 1, 1, 1, 0, 0, 0], # other, 3
|
|
22
|
+
[0, 0, 1, 1, 1, 0, 0, 0],
|
|
23
|
+
[0, 0, 1, 1, 1, 0, 0, 0],
|
|
24
|
+
[0, 0, 0, 0, 0, 0, 0, 0],
|
|
25
|
+
], dtype=bool)
|
|
136
26
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
27
|
+
def get_chunk(self, chunk_index):
|
|
28
|
+
image = np.array(-(10 + chunk_index) * self.mask, dtype=np.int16)
|
|
29
|
+
chunk = np.stack([image] * 100, dtype=np.int16)
|
|
30
|
+
return chunk
|
|
140
31
|
|
|
141
32
|
|
|
142
|
-
def
|
|
33
|
+
def test_segmenter_labeled_mask():
|
|
143
34
|
mask = np.array([
|
|
144
35
|
[0, 0, 0, 0, 0, 0, 0, 0],
|
|
145
36
|
[0, 0, 1, 1, 1, 0, 0, 0],
|
|
@@ -206,7 +97,7 @@ def test_segm_thresh_labeled_mask():
|
|
|
206
97
|
assert np.sum(labels4 == 3) == 12
|
|
207
98
|
|
|
208
99
|
|
|
209
|
-
def
|
|
100
|
+
def test_segmenter_labeled_mask_closing_disk():
|
|
210
101
|
mask = np.array([
|
|
211
102
|
[0, 0, 0, 0, 0, 0, 0, 0, 0],
|
|
212
103
|
[0, 0, 0, 0, 0, 0, 0, 0, 0],
|
|
@@ -275,3 +166,60 @@ def test_segm_thresh_labeled_mask_closing_disk():
|
|
|
275
166
|
assert np.sum(labels4 == 1) == 9
|
|
276
167
|
assert np.sum(labels4 == 2) == 8
|
|
277
168
|
assert np.sum(labels4 == 3) == 23
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def test_segmenter_labeled_mask_fill_holes_int32():
|
|
172
|
+
mask = np.array([
|
|
173
|
+
[0, 0, 0, 0, 0, 0, 0, 0],
|
|
174
|
+
[0, 0, 1, 1, 1, 0, 0, 0],
|
|
175
|
+
[0, 0, 1, 0, 1, 0, 0, 0], # filled, 1
|
|
176
|
+
[0, 0, 1, 1, 1, 0, 0, 0],
|
|
177
|
+
[0, 0, 0, 0, 0, 0, 0, 0],
|
|
178
|
+
[0, 0, 0, 0, 0, 0, 0, 0],
|
|
179
|
+
[0, 0, 0, 0, 1, 1, 1, 1],
|
|
180
|
+
[0, 0, 0, 0, 0, 1, 1, 1], # border, 2
|
|
181
|
+
[0, 0, 0, 0, 0, 1, 1, 1],
|
|
182
|
+
[0, 1, 1, 1, 0, 0, 0, 0],
|
|
183
|
+
[0, 0, 1, 1, 1, 0, 0, 0], # other, 3
|
|
184
|
+
[0, 0, 1, 1, 1, 0, 0, 0],
|
|
185
|
+
[0, 0, 1, 1, 1, 0, 0, 0],
|
|
186
|
+
[0, 0, 0, 0, 0, 0, 0, 0],
|
|
187
|
+
], dtype=bool)
|
|
188
|
+
|
|
189
|
+
sm1 = segm.segm_thresh.SegmentThresh(thresh=-6)
|
|
190
|
+
labels = np.array(sm1.segment_frame(-10 * mask), dtype=np.int64)
|
|
191
|
+
# sanity checks
|
|
192
|
+
assert labels.dtype == np.int64
|
|
193
|
+
assert labels.dtype != np.int32
|
|
194
|
+
labels_2 = sm1.process_mask(labels,
|
|
195
|
+
clear_border=False,
|
|
196
|
+
fill_holes=True,
|
|
197
|
+
closing_disk=False)
|
|
198
|
+
assert np.allclose(labels, labels_2)
|
|
199
|
+
assert labels_2.dtype == np.int32
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def test_segmenter_segment_chunk():
|
|
203
|
+
with segm.segm_thresh.SegmentThresh(thresh=-12, debug=True) as sm:
|
|
204
|
+
image_data = MockImageData()
|
|
205
|
+
labels_1 = np.copy(sm.segment_chunk(image_data, 0)) # below threshold
|
|
206
|
+
assert sm.image_array.min() == -10
|
|
207
|
+
labels_2 = np.copy(sm.segment_chunk(image_data, 10)) # above threshold
|
|
208
|
+
assert sm.image_array.min() == -20
|
|
209
|
+
assert np.all(labels_1 == 0)
|
|
210
|
+
assert not np.all(labels_2 == 0)
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def test_cpu_segmenter_getsetstate():
|
|
214
|
+
sm1 = segm.segm_thresh.SegmentThresh(thresh=-12, debug=True)
|
|
215
|
+
with segm.segm_thresh.SegmentThresh(thresh=-12, debug=True) as sm2:
|
|
216
|
+
image_data = MockImageData()
|
|
217
|
+
# Do some processing so that we have workers
|
|
218
|
+
sm2.segment_chunk(image_data, 0)
|
|
219
|
+
# get the state
|
|
220
|
+
state = sm2.__getstate__()
|
|
221
|
+
# set the state
|
|
222
|
+
sm1.__setstate__(state)
|
|
223
|
+
# and here we test for the raw data that was transferred
|
|
224
|
+
assert not np.all(sm1.image_array == sm2.image_array)
|
|
225
|
+
assert np.all(sm1.mp_image_raw == sm2.mp_image_raw)
|
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
import pathlib
|
|
2
|
-
|
|
3
|
-
import h5py
|
|
4
|
-
import numpy as np
|
|
5
|
-
|
|
6
|
-
from dcnum.feat import feat_texture
|
|
7
|
-
|
|
8
|
-
from helper_methods import retrieve_data
|
|
9
|
-
|
|
10
|
-
data_path = pathlib.Path(__file__).parent / "data"
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
def test_basic_haralick():
|
|
14
|
-
# This original file was generated with dcevent for reference.
|
|
15
|
-
path = retrieve_data(data_path /
|
|
16
|
-
"fmt-hdf5_cytoshot_full-features_2023.zip")
|
|
17
|
-
# Make data available
|
|
18
|
-
with h5py.File(path) as h5:
|
|
19
|
-
ret_arr = feat_texture.haralick_texture_features(
|
|
20
|
-
image=h5["events/image"][:],
|
|
21
|
-
image_bg=h5["events/image_bg"][:],
|
|
22
|
-
mask=h5["events/mask"][:],
|
|
23
|
-
)
|
|
24
|
-
|
|
25
|
-
assert np.allclose(ret_arr["tex_asm_avg"][1],
|
|
26
|
-
0.001514295993357114,
|
|
27
|
-
atol=0, rtol=1e-10)
|
|
28
|
-
for feat in feat_texture.haralick_names:
|
|
29
|
-
assert np.allclose(h5["events"][feat],
|
|
30
|
-
ret_arr[feat])
|
|
31
|
-
# control test
|
|
32
|
-
assert not np.allclose(h5["events"]["tex_asm_avg"],
|
|
33
|
-
ret_arr["tex_asm_ptp"])
|
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
import pathlib
|
|
2
|
-
|
|
3
|
-
import h5py
|
|
4
|
-
import numpy as np
|
|
5
|
-
|
|
6
|
-
from dcnum.feat import feat_moments
|
|
7
|
-
|
|
8
|
-
from helper_methods import retrieve_data
|
|
9
|
-
|
|
10
|
-
data_path = pathlib.Path(__file__).parent / "data"
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
def test_moments_based_features():
|
|
14
|
-
# This original file was generated with dcevent for reference.
|
|
15
|
-
path = retrieve_data(data_path /
|
|
16
|
-
"fmt-hdf5_cytoshot_full-features_2023.zip")
|
|
17
|
-
feats = [
|
|
18
|
-
"deform",
|
|
19
|
-
"size_x",
|
|
20
|
-
"size_y",
|
|
21
|
-
"pos_x",
|
|
22
|
-
"pos_y",
|
|
23
|
-
"area_msd",
|
|
24
|
-
"area_ratio",
|
|
25
|
-
"area_um",
|
|
26
|
-
"aspect",
|
|
27
|
-
"tilt",
|
|
28
|
-
"inert_ratio_cvx",
|
|
29
|
-
"inert_ratio_raw",
|
|
30
|
-
"inert_ratio_prnc",
|
|
31
|
-
]
|
|
32
|
-
|
|
33
|
-
# Make data available
|
|
34
|
-
with h5py.File(path) as h5:
|
|
35
|
-
data = feat_moments.moments_based_features(
|
|
36
|
-
mask=h5["events/mask"][:],
|
|
37
|
-
pixel_size=0.2645
|
|
38
|
-
)
|
|
39
|
-
for feat in feats:
|
|
40
|
-
if feat.count("inert"):
|
|
41
|
-
rtol = 2e-5
|
|
42
|
-
atol = 1e-8
|
|
43
|
-
else:
|
|
44
|
-
rtol = 1e-5
|
|
45
|
-
atol = 1e-8
|
|
46
|
-
assert np.allclose(h5["events"][feat][:],
|
|
47
|
-
data[feat],
|
|
48
|
-
rtol=rtol,
|
|
49
|
-
atol=atol), f"Feature {feat} mismatch!"
|
|
50
|
-
# control test
|
|
51
|
-
assert not np.allclose(h5["events"]["inert_ratio_cvx"][:],
|
|
52
|
-
data["tilt"])
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{dcnum-0.11.1 → dcnum-0.11.2}/tests/data/fmt-hdf5_cytoshot_full-features_legacy_allev_2023.zip
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|