dcnum 0.11.0__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.

Files changed (74) hide show
  1. {dcnum-0.11.0 → dcnum-0.11.2}/CHANGELOG +11 -0
  2. {dcnum-0.11.0 → dcnum-0.11.2}/PKG-INFO +1 -1
  3. {dcnum-0.11.0 → dcnum-0.11.2}/dcnum/_version.py +2 -2
  4. {dcnum-0.11.0 → dcnum-0.11.2}/dcnum/feat/event_extractor_manager_thread.py +7 -1
  5. {dcnum-0.11.0 → dcnum-0.11.2}/dcnum/feat/feat_moments/mt_legacy.py +16 -13
  6. {dcnum-0.11.0 → dcnum-0.11.2}/dcnum/feat/feat_texture/tex_all.py +7 -4
  7. {dcnum-0.11.0 → dcnum-0.11.2}/dcnum/meta/ppid.py +1 -1
  8. {dcnum-0.11.0 → dcnum-0.11.2}/dcnum/read/hdf5_data.py +22 -13
  9. {dcnum-0.11.0 → dcnum-0.11.2}/dcnum/segm/segmenter.py +5 -3
  10. {dcnum-0.11.0 → dcnum-0.11.2}/dcnum/segm/segmenter_cpu.py +7 -1
  11. {dcnum-0.11.0 → dcnum-0.11.2}/dcnum/segm/segmenter_gpu.py +16 -3
  12. {dcnum-0.11.0 → dcnum-0.11.2}/dcnum/segm/segmenter_manager_thread.py +8 -0
  13. {dcnum-0.11.0 → dcnum-0.11.2}/dcnum.egg-info/PKG-INFO +1 -1
  14. {dcnum-0.11.0 → dcnum-0.11.2}/dcnum.egg-info/SOURCES.txt +1 -0
  15. {dcnum-0.11.0 → dcnum-0.11.2}/tests/conftest.py +3 -0
  16. {dcnum-0.11.0 → dcnum-0.11.2}/tests/test_feat_brightness.py +23 -0
  17. dcnum-0.11.2/tests/test_feat_haralick.py +120 -0
  18. dcnum-0.11.2/tests/test_feat_moments_based.py +108 -0
  19. {dcnum-0.11.0 → dcnum-0.11.2}/tests/test_read_hdf5.py +37 -10
  20. dcnum-0.11.2/tests/test_segm_thresh.py +139 -0
  21. dcnum-0.11.0/tests/test_segm_thresh.py → dcnum-0.11.2/tests/test_segmenter.py +80 -132
  22. dcnum-0.11.0/tests/test_feat_haralick.py +0 -33
  23. dcnum-0.11.0/tests/test_feat_moments_based.py +0 -52
  24. {dcnum-0.11.0 → dcnum-0.11.2}/.github/workflows/check.yml +0 -0
  25. {dcnum-0.11.0 → dcnum-0.11.2}/.github/workflows/deploy_pypi.yml +0 -0
  26. {dcnum-0.11.0 → dcnum-0.11.2}/.gitignore +0 -0
  27. {dcnum-0.11.0 → dcnum-0.11.2}/.readthedocs.yml +0 -0
  28. {dcnum-0.11.0 → dcnum-0.11.2}/LICENSE +0 -0
  29. {dcnum-0.11.0 → dcnum-0.11.2}/README.rst +0 -0
  30. {dcnum-0.11.0 → dcnum-0.11.2}/dcnum/__init__.py +0 -0
  31. {dcnum-0.11.0 → dcnum-0.11.2}/dcnum/feat/__init__.py +0 -0
  32. {dcnum-0.11.0 → dcnum-0.11.2}/dcnum/feat/feat_background/__init__.py +0 -0
  33. {dcnum-0.11.0 → dcnum-0.11.2}/dcnum/feat/feat_background/base.py +0 -0
  34. {dcnum-0.11.0 → dcnum-0.11.2}/dcnum/feat/feat_background/bg_roll_median.py +0 -0
  35. {dcnum-0.11.0 → dcnum-0.11.2}/dcnum/feat/feat_background/bg_sparse_median.py +0 -0
  36. {dcnum-0.11.0 → dcnum-0.11.2}/dcnum/feat/feat_brightness/__init__.py +0 -0
  37. {dcnum-0.11.0 → dcnum-0.11.2}/dcnum/feat/feat_brightness/bright_all.py +0 -0
  38. {dcnum-0.11.0 → dcnum-0.11.2}/dcnum/feat/feat_brightness/common.py +0 -0
  39. {dcnum-0.11.0 → dcnum-0.11.2}/dcnum/feat/feat_moments/__init__.py +0 -0
  40. {dcnum-0.11.0 → dcnum-0.11.2}/dcnum/feat/feat_moments/ct_opencv.py +0 -0
  41. {dcnum-0.11.0 → dcnum-0.11.2}/dcnum/feat/feat_texture/__init__.py +0 -0
  42. {dcnum-0.11.0 → dcnum-0.11.2}/dcnum/feat/feat_texture/common.py +0 -0
  43. {dcnum-0.11.0 → dcnum-0.11.2}/dcnum/feat/gate.py +0 -0
  44. {dcnum-0.11.0 → dcnum-0.11.2}/dcnum/feat/queue_event_extractor.py +0 -0
  45. {dcnum-0.11.0 → dcnum-0.11.2}/dcnum/meta/__init__.py +0 -0
  46. {dcnum-0.11.0 → dcnum-0.11.2}/dcnum/read/__init__.py +0 -0
  47. {dcnum-0.11.0 → dcnum-0.11.2}/dcnum/read/cache.py +0 -0
  48. {dcnum-0.11.0 → dcnum-0.11.2}/dcnum/read/const.py +0 -0
  49. {dcnum-0.11.0 → dcnum-0.11.2}/dcnum/segm/__init__.py +0 -0
  50. {dcnum-0.11.0 → dcnum-0.11.2}/dcnum/segm/segm_thresh.py +0 -0
  51. {dcnum-0.11.0 → dcnum-0.11.2}/dcnum/write/__init__.py +0 -0
  52. {dcnum-0.11.0 → dcnum-0.11.2}/dcnum/write/deque_writer_thread.py +0 -0
  53. {dcnum-0.11.0 → dcnum-0.11.2}/dcnum/write/queue_collector_thread.py +0 -0
  54. {dcnum-0.11.0 → dcnum-0.11.2}/dcnum/write/writer.py +0 -0
  55. {dcnum-0.11.0 → dcnum-0.11.2}/dcnum.egg-info/dependency_links.txt +0 -0
  56. {dcnum-0.11.0 → dcnum-0.11.2}/dcnum.egg-info/requires.txt +0 -0
  57. {dcnum-0.11.0 → dcnum-0.11.2}/dcnum.egg-info/top_level.txt +0 -0
  58. {dcnum-0.11.0 → dcnum-0.11.2}/docs/conf.py +0 -0
  59. {dcnum-0.11.0 → dcnum-0.11.2}/docs/extensions/github_changelog.py +0 -0
  60. {dcnum-0.11.0 → dcnum-0.11.2}/docs/index.rst +0 -0
  61. {dcnum-0.11.0 → dcnum-0.11.2}/docs/requirements.txt +0 -0
  62. {dcnum-0.11.0 → dcnum-0.11.2}/pyproject.toml +0 -0
  63. {dcnum-0.11.0 → dcnum-0.11.2}/setup.cfg +0 -0
  64. {dcnum-0.11.0 → dcnum-0.11.2}/tests/data/fmt-hdf5_cytoshot_full-features_2023.zip +0 -0
  65. {dcnum-0.11.0 → dcnum-0.11.2}/tests/data/fmt-hdf5_cytoshot_full-features_legacy_allev_2023.zip +0 -0
  66. {dcnum-0.11.0 → dcnum-0.11.2}/tests/helper_methods.py +0 -0
  67. {dcnum-0.11.0 → dcnum-0.11.2}/tests/requirements.txt +0 -0
  68. {dcnum-0.11.0 → dcnum-0.11.2}/tests/test_feat_background_bg_roll_median.py +0 -0
  69. {dcnum-0.11.0 → dcnum-0.11.2}/tests/test_init.py +0 -0
  70. {dcnum-0.11.0 → dcnum-0.11.2}/tests/test_ppid.py +0 -0
  71. {dcnum-0.11.0 → dcnum-0.11.2}/tests/test_ppid_segm.py +0 -0
  72. {dcnum-0.11.0 → dcnum-0.11.2}/tests/test_read_concat_hdf5.py +0 -0
  73. {dcnum-0.11.0 → dcnum-0.11.2}/tests/test_write_deque_writer_thread.py +0 -0
  74. {dcnum-0.11.0 → dcnum-0.11.2}/tests/test_write_writer.py +0 -0
@@ -1,3 +1,14 @@
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)
10
+ 0.11.1
11
+ - fix: fix GPUSegmenter labeling
1
12
  0.11.0
2
13
  - feat: introduce GPUSegmenter base class
3
14
  - fix: handle bytes-values in HDF5 attributes
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: dcnum
3
- Version: 0.11.0
3
+ Version: 0.11.2
4
4
  Summary: numerics toolbox for imaging deformability cytometry
5
5
  Author: Paul Müller
6
6
  Maintainer-email: Paul Müller <dev@craban.de>
@@ -1,4 +1,4 @@
1
1
  # file generated by setuptools_scm
2
2
  # don't change, don't track in version control
3
- __version__ = version = '0.11.0'
4
- __version_tuple__ = version_tuple = (0, 11, 0)
3
+ __version__ = version = '0.11.2'
4
+ __version_tuple__ = version_tuple = (0, 11, 2)
@@ -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
- deform = np.zeros(size, dtype=float)
14
- size_x = np.zeros(size, dtype=float)
15
- size_y = np.zeros(size, dtype=float)
16
- pos_x = np.zeros(size, dtype=float)
17
- pos_y = np.zeros(size, dtype=float)
18
- area_msd = np.zeros(size, dtype=float)
19
- area_ratio = np.zeros(size, dtype=float)
20
- area_um = np.zeros(size, dtype=float)
21
- aspect = np.zeros(size, dtype=float)
22
- tilt = np.zeros(size, dtype=float)
23
- inert_ratio_cvx = np.zeros(size, dtype=float)
24
- inert_ratio_raw = np.zeros(size, dtype=float)
25
- inert_ratio_prnc = np.zeros(size, dtype=float)
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(image, mask, image_bg=None, image_corr=None):
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 or image_corr 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
- if image_corr is None:
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]
@@ -8,7 +8,7 @@ import pathlib
8
8
 
9
9
  #: Increment this string if there are breaking changes that make
10
10
  #: previous pipelines unreproducible.
11
- DCNUM_PPID_GENERATION = "2"
11
+ DCNUM_PPID_GENERATION = "3"
12
12
 
13
13
 
14
14
  def compute_pipeline_hash(bg_id, seg_id, feat_id, gate_id,
@@ -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
- self.tables[tab] = h5["tables"][key][:]
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"]
@@ -173,11 +173,13 @@ class Segmenter(abc.ABC):
173
173
  labels_uint8 = np.array(labels, dtype=np.uint8)
174
174
  labels_dilated = cv2.dilate(labels_uint8, element)
175
175
  labels_eroded = cv2.erode(labels_dilated, element)
176
- labels, _ = ndi.label(labels_eroded > 0)
176
+ labels, _ = ndi.label(
177
+ input=labels_eroded > 0,
178
+ structure=ndi.generate_binary_structure(2, 2))
177
179
 
178
180
  if fill_holes:
179
181
  # Floodfill only works with uint8 (too small) or int32
180
- if not labels.dtype == np.int32:
182
+ if labels.dtype != np.int32:
181
183
  labels = np.array(labels, dtype=np.int32)
182
184
  #
183
185
  # from scipy import ndimage
@@ -206,7 +208,7 @@ class Segmenter(abc.ABC):
206
208
  mol = segm_wrap(image)
207
209
  if mol.dtype == bool:
208
210
  # convert mask to label
209
- labels, num_labels = ndi.label(
211
+ labels, _ = ndi.label(
210
212
  input=mol,
211
213
  structure=ndi.generate_binary_structure(2, 2))
212
214
  else:
@@ -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 (i.e., filename and lineno).
56
+ # Restore instance attributes
51
57
  self.__dict__.update(state)
52
58
 
53
59
  @staticmethod
@@ -2,6 +2,7 @@ import abc
2
2
  import pathlib
3
3
 
4
4
  import numpy as np
5
+ import scipy.ndimage as ndi
5
6
 
6
7
 
7
8
  from .segmenter import Segmenter
@@ -10,7 +11,7 @@ from .segmenter import Segmenter
10
11
  class GPUSegmenter(Segmenter, abc.ABC):
11
12
  mask_postprocessing = False
12
13
 
13
- def __init__(self, model_file, *args, **kwargs):
14
+ def __init__(self, model_file=None, *args, **kwargs):
14
15
  super(GPUSegmenter, self).__init__(*args, **kwargs)
15
16
  self.model_path = self._get_model_path(model_file)
16
17
 
@@ -28,6 +29,18 @@ class GPUSegmenter(Segmenter, abc.ABC):
28
29
  stop = len(image_data)
29
30
 
30
31
  image_slice = image_data[start:stop]
31
- segm = self.segment_frame_wrapper(self.model_path)
32
+ segm = self.segment_frame_wrapper()
32
33
 
33
- return segm(image_slice)
34
+ labels = segm(image_slice)
35
+
36
+ # Make sure we have integer labels
37
+ if labels.dtype == bool:
38
+ new_labels = np.zeros_like(labels, dtype=np.uint16)
39
+ for ii in range(len(labels)):
40
+ ndi.label(
41
+ input=labels[ii],
42
+ output=new_labels[ii],
43
+ structure=ndi.generate_binary_structure(2, 2))
44
+ labels = new_labels
45
+
46
+ return labels
@@ -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")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: dcnum
3
- Version: 0.11.0
3
+ Version: 0.11.2
4
4
  Summary: numerics toolbox for imaging deformability cytometry
5
5
  Author: Paul Müller
6
6
  Maintainer-email: Paul Müller <dev@craban.de>
@@ -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
- state = h5d1.__getstate__()
138
- pstate = pickle.dumps(state)
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
- state = h5d1.__getstate__()
154
- pstate = pickle.dumps(state)
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])