celldetective 1.4.2__py3-none-any.whl → 1.5.0b0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (151) hide show
  1. celldetective/__init__.py +25 -0
  2. celldetective/__main__.py +62 -43
  3. celldetective/_version.py +1 -1
  4. celldetective/extra_properties.py +477 -399
  5. celldetective/filters.py +192 -97
  6. celldetective/gui/InitWindow.py +541 -411
  7. celldetective/gui/__init__.py +0 -15
  8. celldetective/gui/about.py +44 -39
  9. celldetective/gui/analyze_block.py +120 -84
  10. celldetective/gui/base/__init__.py +0 -0
  11. celldetective/gui/base/channel_norm_generator.py +335 -0
  12. celldetective/gui/base/components.py +249 -0
  13. celldetective/gui/base/feature_choice.py +92 -0
  14. celldetective/gui/base/figure_canvas.py +52 -0
  15. celldetective/gui/base/list_widget.py +133 -0
  16. celldetective/gui/{styles.py → base/styles.py} +92 -36
  17. celldetective/gui/base/utils.py +33 -0
  18. celldetective/gui/base_annotator.py +900 -767
  19. celldetective/gui/classifier_widget.py +6 -22
  20. celldetective/gui/configure_new_exp.py +777 -671
  21. celldetective/gui/control_panel.py +635 -524
  22. celldetective/gui/dynamic_progress.py +449 -0
  23. celldetective/gui/event_annotator.py +2023 -1662
  24. celldetective/gui/generic_signal_plot.py +1292 -944
  25. celldetective/gui/gui_utils.py +899 -1289
  26. celldetective/gui/interactions_block.py +658 -0
  27. celldetective/gui/interactive_timeseries_viewer.py +447 -0
  28. celldetective/gui/json_readers.py +48 -15
  29. celldetective/gui/layouts/__init__.py +5 -0
  30. celldetective/gui/layouts/background_model_free_layout.py +537 -0
  31. celldetective/gui/layouts/channel_offset_layout.py +134 -0
  32. celldetective/gui/layouts/local_correction_layout.py +91 -0
  33. celldetective/gui/layouts/model_fit_layout.py +372 -0
  34. celldetective/gui/layouts/operation_layout.py +68 -0
  35. celldetective/gui/layouts/protocol_designer_layout.py +96 -0
  36. celldetective/gui/pair_event_annotator.py +3130 -2435
  37. celldetective/gui/plot_measurements.py +586 -267
  38. celldetective/gui/plot_signals_ui.py +724 -506
  39. celldetective/gui/preprocessing_block.py +395 -0
  40. celldetective/gui/process_block.py +1678 -1831
  41. celldetective/gui/seg_model_loader.py +580 -473
  42. celldetective/gui/settings/__init__.py +0 -7
  43. celldetective/gui/settings/_cellpose_model_params.py +181 -0
  44. celldetective/gui/settings/_event_detection_model_params.py +95 -0
  45. celldetective/gui/settings/_segmentation_model_params.py +159 -0
  46. celldetective/gui/settings/_settings_base.py +77 -65
  47. celldetective/gui/settings/_settings_event_model_training.py +752 -526
  48. celldetective/gui/settings/_settings_measurements.py +1133 -964
  49. celldetective/gui/settings/_settings_neighborhood.py +574 -488
  50. celldetective/gui/settings/_settings_segmentation_model_training.py +779 -564
  51. celldetective/gui/settings/_settings_signal_annotator.py +329 -305
  52. celldetective/gui/settings/_settings_tracking.py +1304 -1094
  53. celldetective/gui/settings/_stardist_model_params.py +98 -0
  54. celldetective/gui/survival_ui.py +422 -312
  55. celldetective/gui/tableUI.py +1665 -1701
  56. celldetective/gui/table_ops/_maths.py +295 -0
  57. celldetective/gui/table_ops/_merge_groups.py +140 -0
  58. celldetective/gui/table_ops/_merge_one_hot.py +95 -0
  59. celldetective/gui/table_ops/_query_table.py +43 -0
  60. celldetective/gui/table_ops/_rename_col.py +44 -0
  61. celldetective/gui/thresholds_gui.py +382 -179
  62. celldetective/gui/viewers/__init__.py +0 -0
  63. celldetective/gui/viewers/base_viewer.py +700 -0
  64. celldetective/gui/viewers/channel_offset_viewer.py +331 -0
  65. celldetective/gui/viewers/contour_viewer.py +394 -0
  66. celldetective/gui/viewers/size_viewer.py +153 -0
  67. celldetective/gui/viewers/spot_detection_viewer.py +341 -0
  68. celldetective/gui/viewers/threshold_viewer.py +309 -0
  69. celldetective/gui/workers.py +304 -126
  70. celldetective/log_manager.py +92 -0
  71. celldetective/measure.py +1895 -1478
  72. celldetective/napari/__init__.py +0 -0
  73. celldetective/napari/utils.py +1025 -0
  74. celldetective/neighborhood.py +1914 -1448
  75. celldetective/preprocessing.py +1620 -1220
  76. celldetective/processes/__init__.py +0 -0
  77. celldetective/processes/background_correction.py +271 -0
  78. celldetective/processes/compute_neighborhood.py +894 -0
  79. celldetective/processes/detect_events.py +246 -0
  80. celldetective/processes/measure_cells.py +565 -0
  81. celldetective/processes/segment_cells.py +760 -0
  82. celldetective/processes/track_cells.py +435 -0
  83. celldetective/processes/train_segmentation_model.py +694 -0
  84. celldetective/processes/train_signal_model.py +265 -0
  85. celldetective/processes/unified_process.py +292 -0
  86. celldetective/regionprops/_regionprops.py +358 -317
  87. celldetective/relative_measurements.py +987 -710
  88. celldetective/scripts/measure_cells.py +313 -212
  89. celldetective/scripts/measure_relative.py +90 -46
  90. celldetective/scripts/segment_cells.py +165 -104
  91. celldetective/scripts/segment_cells_thresholds.py +96 -68
  92. celldetective/scripts/track_cells.py +198 -149
  93. celldetective/scripts/train_segmentation_model.py +324 -201
  94. celldetective/scripts/train_signal_model.py +87 -45
  95. celldetective/segmentation.py +844 -749
  96. celldetective/signals.py +3514 -2861
  97. celldetective/tracking.py +30 -15
  98. celldetective/utils/__init__.py +0 -0
  99. celldetective/utils/cellpose_utils/__init__.py +133 -0
  100. celldetective/utils/color_mappings.py +42 -0
  101. celldetective/utils/data_cleaning.py +630 -0
  102. celldetective/utils/data_loaders.py +450 -0
  103. celldetective/utils/dataset_helpers.py +207 -0
  104. celldetective/utils/downloaders.py +197 -0
  105. celldetective/utils/event_detection/__init__.py +8 -0
  106. celldetective/utils/experiment.py +1782 -0
  107. celldetective/utils/image_augmenters.py +308 -0
  108. celldetective/utils/image_cleaning.py +74 -0
  109. celldetective/utils/image_loaders.py +926 -0
  110. celldetective/utils/image_transforms.py +335 -0
  111. celldetective/utils/io.py +62 -0
  112. celldetective/utils/mask_cleaning.py +348 -0
  113. celldetective/utils/mask_transforms.py +5 -0
  114. celldetective/utils/masks.py +184 -0
  115. celldetective/utils/maths.py +351 -0
  116. celldetective/utils/model_getters.py +325 -0
  117. celldetective/utils/model_loaders.py +296 -0
  118. celldetective/utils/normalization.py +380 -0
  119. celldetective/utils/parsing.py +465 -0
  120. celldetective/utils/plots/__init__.py +0 -0
  121. celldetective/utils/plots/regression.py +53 -0
  122. celldetective/utils/resources.py +34 -0
  123. celldetective/utils/stardist_utils/__init__.py +104 -0
  124. celldetective/utils/stats.py +90 -0
  125. celldetective/utils/types.py +21 -0
  126. {celldetective-1.4.2.dist-info → celldetective-1.5.0b0.dist-info}/METADATA +1 -1
  127. celldetective-1.5.0b0.dist-info/RECORD +187 -0
  128. {celldetective-1.4.2.dist-info → celldetective-1.5.0b0.dist-info}/WHEEL +1 -1
  129. tests/gui/test_new_project.py +129 -117
  130. tests/gui/test_project.py +127 -79
  131. tests/test_filters.py +39 -15
  132. tests/test_notebooks.py +8 -0
  133. tests/test_tracking.py +232 -13
  134. tests/test_utils.py +123 -77
  135. celldetective/gui/base_components.py +0 -23
  136. celldetective/gui/layouts.py +0 -1602
  137. celldetective/gui/processes/compute_neighborhood.py +0 -594
  138. celldetective/gui/processes/measure_cells.py +0 -360
  139. celldetective/gui/processes/segment_cells.py +0 -499
  140. celldetective/gui/processes/track_cells.py +0 -303
  141. celldetective/gui/processes/train_segmentation_model.py +0 -270
  142. celldetective/gui/processes/train_signal_model.py +0 -108
  143. celldetective/gui/table_ops/merge_groups.py +0 -118
  144. celldetective/gui/viewers.py +0 -1354
  145. celldetective/io.py +0 -3663
  146. celldetective/utils.py +0 -3108
  147. celldetective-1.4.2.dist-info/RECORD +0 -123
  148. /celldetective/{gui/processes → processes}/downloader.py +0 -0
  149. {celldetective-1.4.2.dist-info → celldetective-1.5.0b0.dist-info}/entry_points.txt +0 -0
  150. {celldetective-1.4.2.dist-info → celldetective-1.5.0b0.dist-info}/licenses/LICENSE +0 -0
  151. {celldetective-1.4.2.dist-info → celldetective-1.5.0b0.dist-info}/top_level.txt +0 -0
@@ -1,337 +1,378 @@
1
- from skimage.measure._regionprops import RegionProperties, regionprops, _cached, _props_to_dict, _infer_number_of_required_args
1
+ from skimage.measure._regionprops import (
2
+ RegionProperties,
3
+ regionprops,
4
+ _cached,
5
+ _props_to_dict,
6
+ _infer_number_of_required_args,
7
+ )
2
8
  import numpy as np
3
9
  import inspect
4
10
  import json
5
11
  import os
6
12
  from scipy.ndimage import find_objects
13
+ from celldetective.log_manager import get_logger
14
+
15
+ logger = get_logger(__name__)
7
16
 
8
17
  abs_path = os.sep.join([os.path.split(os.path.dirname(os.path.realpath(__file__)))[0]])
9
18
 
10
- with open(os.sep.join([abs_path, 'regionprops', 'props.json'])) as f:
11
- PROPS = json.load(f)
19
+ with open(os.sep.join([abs_path, "regionprops", "props.json"])) as f:
20
+ PROPS = json.load(f)
12
21
 
13
22
  COL_DTYPES = {
14
- 'area': float,
15
- 'area_bbox': float,
16
- 'area_convex': float,
17
- 'area_filled': float,
18
- 'axis_major_length': float,
19
- 'axis_minor_length': float,
20
- 'bbox': int,
21
- 'centroid': float,
22
- 'centroid_local': float,
23
- 'centroid_weighted': float,
24
- 'centroid_weighted_local': float,
25
- 'coords': object,
26
- 'coords_scaled': object,
27
- 'eccentricity': float,
28
- 'equivalent_diameter_area': float,
29
- 'euler_number': int,
30
- 'extent': float,
31
- 'feret_diameter_max': float,
32
- 'image': object,
33
- 'image_convex': object,
34
- 'image_filled': object,
35
- 'image_intensity': object,
36
- 'inertia_tensor': float,
37
- 'inertia_tensor_eigvals': float,
38
- 'intensity_max': float,
39
- 'intensity_mean': float,
40
- 'intensity_min': float,
41
- 'intensity_std': float,
42
- 'label': int,
43
- 'moments': float,
44
- 'moments_central': float,
45
- 'moments_hu': float,
46
- 'moments_normalized': float,
47
- 'moments_weighted': float,
48
- 'moments_weighted_central': float,
49
- 'moments_weighted_hu': float,
50
- 'moments_weighted_normalized': float,
51
- 'num_pixels': int,
52
- 'orientation': float,
53
- 'perimeter': float,
54
- 'perimeter_crofton': float,
55
- 'slice': object,
56
- 'solidity': float,
23
+ "area": float,
24
+ "area_bbox": float,
25
+ "area_convex": float,
26
+ "area_filled": float,
27
+ "axis_major_length": float,
28
+ "axis_minor_length": float,
29
+ "bbox": int,
30
+ "centroid": float,
31
+ "centroid_local": float,
32
+ "centroid_weighted": float,
33
+ "centroid_weighted_local": float,
34
+ "coords": object,
35
+ "coords_scaled": object,
36
+ "eccentricity": float,
37
+ "equivalent_diameter_area": float,
38
+ "euler_number": int,
39
+ "extent": float,
40
+ "feret_diameter_max": float,
41
+ "image": object,
42
+ "image_convex": object,
43
+ "image_filled": object,
44
+ "image_intensity": object,
45
+ "inertia_tensor": float,
46
+ "inertia_tensor_eigvals": float,
47
+ "intensity_max": float,
48
+ "intensity_mean": float,
49
+ "intensity_min": float,
50
+ "intensity_std": float,
51
+ "label": int,
52
+ "moments": float,
53
+ "moments_central": float,
54
+ "moments_hu": float,
55
+ "moments_normalized": float,
56
+ "moments_weighted": float,
57
+ "moments_weighted_central": float,
58
+ "moments_weighted_hu": float,
59
+ "moments_weighted_normalized": float,
60
+ "num_pixels": int,
61
+ "orientation": float,
62
+ "perimeter": float,
63
+ "perimeter_crofton": float,
64
+ "slice": object,
65
+ "solidity": float,
57
66
  }
58
67
 
59
68
  OBJECT_COLUMNS = [col for col, dtype in COL_DTYPES.items() if dtype == object]
60
69
  PROP_VALS = set(PROPS.values())
61
70
 
62
71
  _require_intensity_image = (
63
- 'image_intensity',
64
- 'intensity_max',
65
- 'intensity_mean',
66
- 'intensity_median',
67
- 'intensity_min',
68
- 'intensity_std',
69
- 'moments_weighted',
70
- 'moments_weighted_central',
71
- 'centroid_weighted',
72
- 'centroid_weighted_local',
73
- 'moments_weighted_hu',
74
- 'moments_weighted_normalized',
72
+ "image_intensity",
73
+ "intensity_max",
74
+ "intensity_mean",
75
+ "intensity_median",
76
+ "intensity_min",
77
+ "intensity_std",
78
+ "moments_weighted",
79
+ "moments_weighted_central",
80
+ "centroid_weighted",
81
+ "centroid_weighted_local",
82
+ "moments_weighted_hu",
83
+ "moments_weighted_normalized",
75
84
  )
76
85
 
77
86
 
78
87
  class CustomRegionProps(RegionProperties):
79
-
80
- """
81
- From https://github.com/scikit-image/scikit-image/blob/main/skimage/measure/_regionprops.py with a modification to not mask the intensity image itself before measurements
82
- """
83
-
84
- def __init__(self, channel_names, *args, **kwargs):
85
-
86
- self.channel_names = channel_names
87
- if isinstance(self.channel_names, np.ndarray):
88
- self.channel_names = list(self.channel_names)
89
- super().__init__(*args, **kwargs)
90
-
91
-
92
- def __getattr__(self, attr):
93
-
94
- if self.channel_names is not None and self._multichannel:
95
- assert len(self.channel_names)==self._intensity_image.shape[-1],'Mismatch between provided channel names and the number of channels in the image...'
96
-
97
- if attr == "__setstate__":
98
- return self.__getattribute__(attr)
99
-
100
- if self._intensity_image is None and attr in _require_intensity_image:
101
- raise AttributeError(
102
- f"Attribute '{attr}' unavailable when `intensity_image` "
103
- f"has not been specified."
104
- )
105
- if attr in self._extra_properties:
106
- func = self._extra_properties[attr]
107
- n_args = _infer_number_of_required_args(func)
108
- # determine whether func requires intensity image
109
- if n_args == 2:
110
- if self._intensity_image is not None:
111
- if self._multichannel:
112
- arg_dict = dict(inspect.signature(func).parameters)
113
- if self.channel_names is not None and 'target_channel' in arg_dict:
114
- multichannel_list = [np.nan for i in range(self.image_intensity.shape[-1])]
115
- len_output = 1
116
- default_channel = arg_dict['target_channel']._default
117
-
118
- if default_channel in self.channel_names:
119
-
120
- idx = self.channel_names.index(default_channel)
121
- res = func(self.image, self.image_intensity[..., idx])
122
- if isinstance(res, tuple):
123
- len_output = len(res)
124
- else:
125
- len_output = 1
126
-
127
- if len_output > 1:
128
- multichannel_list = [[np.nan]*len_output for c in range(len(self.channel_names))]
129
- multichannel_list[idx] = res
130
- else:
131
- multichannel_list = [np.nan for c in range(len(self.channel_names))]
132
- multichannel_list[idx] = res
133
-
134
- else:
135
- print(f'Warning... Channel required by custom measurement ({default_channel}) could not be found in your data...')
136
-
137
- return np.stack(multichannel_list, axis=-1)
138
- else:
139
- multichannel_list = [
140
- func(self.image, self.image_intensity[..., i])
141
- for i in range(self.image_intensity.shape[-1])
142
- ]
143
- return np.stack(multichannel_list, axis=-1)
144
- else:
145
- return func(self.image, self.image_intensity)
146
- else:
147
- raise AttributeError(
148
- f'intensity image required to calculate {attr}'
149
- )
150
- elif n_args == 1:
151
- return func(self.image)
152
- else:
153
- raise AttributeError(
154
- f'Custom regionprop function\'s number of arguments must '
155
- f'be 1 or 2, but {attr} takes {n_args} arguments.'
156
- )
157
- elif attr in PROPS and attr.lower() == attr:
158
- if (
159
- self._intensity_image is None
160
- and PROPS[attr] in _require_intensity_image
161
- ):
162
- raise AttributeError(
163
- f"Attribute '{attr}' unavailable when `intensity_image` "
164
- f"has not been specified."
165
- )
166
- # retrieve deprecated property (excluding old CamelCase ones)
167
- return getattr(self, PROPS[attr])
168
- else:
169
- raise AttributeError(f"'{type(self)}' object has no attribute '{attr}'")
170
-
171
- @property
172
- @_cached
173
- def image_intensity(self):
174
- if self._intensity_image is None:
175
- raise AttributeError('No intensity image specified.')
176
- image = (
177
- self.image
178
- if not self._multichannel
179
- else np.expand_dims(self.image, self._ndim)
180
- )
181
- return self._intensity_image[self.slice]
182
-
183
- def regionprops(label_image, intensity_image=None,cache=True,channel_names=None,*,extra_properties=None,spacing=None,offset=None):
184
-
185
- """
186
- From https://github.com/scikit-image/scikit-image/blob/main/skimage/measure/_regionprops.py with a modification to use CustomRegionProps
187
- """
188
-
189
- if label_image.ndim not in (2, 3):
190
- raise TypeError('Only 2-D and 3-D images supported.')
191
-
192
- if not np.issubdtype(label_image.dtype, np.integer):
193
- if np.issubdtype(label_image.dtype, bool):
194
- raise TypeError(
195
- 'Non-integer image types are ambiguous: '
196
- 'use skimage.measure.label to label the connected '
197
- 'components of label_image, '
198
- 'or label_image.astype(np.uint8) to interpret '
199
- 'the True values as a single label.'
200
- )
201
- else:
202
- raise TypeError('Non-integer label_image types are ambiguous')
203
-
204
- if offset is None:
205
- offset_arr = np.zeros((label_image.ndim,), dtype=int)
206
- else:
207
- offset_arr = np.asarray(offset)
208
- if offset_arr.ndim != 1 or offset_arr.size != label_image.ndim:
209
- raise ValueError(
210
- 'Offset should be an array-like of integers '
211
- 'of shape (label_image.ndim,); '
212
- f'{offset} was provided.'
213
- )
214
-
215
- regions = []
216
-
217
- objects = find_objects(label_image)
218
- for i, sl in enumerate(objects):
219
- if sl is None:
220
- continue
221
-
222
- label = i + 1
223
-
224
- props = CustomRegionProps(
225
- channel_names,
226
- sl,
227
- label,
228
- label_image,
229
- intensity_image,
230
- cache,
231
- spacing=spacing,
232
- extra_properties=extra_properties,
233
- offset=offset_arr,
234
- )
235
- regions.append(props)
236
-
237
- return regions
238
-
239
-
240
- def _props_to_dict(regions, properties=('label', 'bbox'), separator='-'):
241
-
242
-
243
- out = {}
244
- n = len(regions)
245
- for prop in properties:
246
- r = regions[0]
247
- # Copy the original property name so the output will have the
248
- # user-provided property name in the case of deprecated names.
249
- orig_prop = prop
250
- # determine the current property name for any deprecated property.
251
- prop = PROPS.get(prop, prop)
252
- rp = getattr(r, prop)
253
- if prop in COL_DTYPES:
254
- dtype = COL_DTYPES[prop]
255
- else:
256
- func = r._extra_properties[prop]
257
- # dtype = _infer_regionprop_dtype(
258
- # func,
259
- # intensity=r._intensity_image is not None,
260
- # ndim=r.image.ndim,
261
- # )
262
- dtype = np.float64
263
-
264
- # scalars and objects are dedicated one column per prop
265
- # array properties are raveled into multiple columns
266
- # for more info, refer to notes 1
267
- if np.isscalar(rp) or prop in OBJECT_COLUMNS or dtype is np.object_:
268
- column_buffer = np.empty(n, dtype=dtype)
269
- for i in range(n):
270
- column_buffer[i] = regions[i][prop]
271
- out[orig_prop] = np.copy(column_buffer)
272
- else:
273
- # precompute property column names and locations
274
- modified_props = []
275
- locs = []
276
- for ind in np.ndindex(np.shape(rp)):
277
- modified_props.append(separator.join(map(str, (orig_prop,) + ind)))
278
- locs.append(ind if len(ind) > 1 else ind[0])
279
-
280
- # fill temporary column data_array
281
- n_columns = len(locs)
282
- column_data = np.empty((n, n_columns), dtype=dtype)
283
- for k in range(n):
284
- # we coerce to a numpy array to ensure structures like
285
- # tuple-of-arrays expand correctly into columns
286
- rp = np.asarray(regions[k][prop])
287
- for i, loc in enumerate(locs):
288
- column_data[k, i] = rp[loc]
289
-
290
- # add the columns to the output dictionary
291
- for i, modified_prop in enumerate(modified_props):
292
- out[modified_prop] = column_data[:, i]
293
- return out
294
-
295
-
296
- def regionprops_table(label_image,intensity_image=None,properties=('label', 'bbox'),*,cache=True,separator='-',extra_properties=None,spacing=None,channel_names=None):
297
-
298
- """
299
- From https://github.com/scikit-image/scikit-image/blob/main/skimage/measure/_regionprops.py
300
- """
301
- regions = regionprops(
302
- label_image,
303
- intensity_image=intensity_image,
304
- cache=cache,
305
- extra_properties=extra_properties,
306
- spacing=spacing,
307
- channel_names=channel_names,
308
- )
309
- if extra_properties is not None:
310
- properties = list(properties) + [prop.__name__ for prop in extra_properties]
311
- if len(regions) == 0:
312
- ndim = label_image.ndim
313
- label_image = np.zeros((3,) * ndim, dtype=int)
314
- label_image[(1,) * ndim] = 1
315
- if intensity_image is not None:
316
- intensity_image = np.zeros(
317
- label_image.shape + intensity_image.shape[ndim:],
318
- dtype=intensity_image.dtype,
319
- )
320
- regions = regionprops(
321
- label_image,
322
- intensity_image=intensity_image,
323
- cache=cache,
324
- extra_properties=extra_properties,
325
- spacing=spacing,
326
- channel_names=channel_names,
327
- )
328
- out_d = _props_to_dict(regions, properties=properties, separator=separator)
329
- return {k: v[:0] for k, v in out_d.items()}
330
-
331
- good_props = []
332
- for prop in properties:
333
- nan_test = [np.isnan(getattr(r,prop)) for r in regions]
334
- if not np.all(nan_test):
335
- good_props.append(prop)
336
-
337
- return _props_to_dict(regions, properties=good_props, separator=separator)
88
+ """
89
+ From https://github.com/scikit-image/scikit-image/blob/main/skimage/measure/_regionprops.py with a modification to not mask the intensity image itself before measurements
90
+ """
91
+
92
+ def __init__(self, channel_names, *args, **kwargs):
93
+
94
+ self.channel_names = channel_names
95
+ if isinstance(self.channel_names, np.ndarray):
96
+ self.channel_names = list(self.channel_names)
97
+ super().__init__(*args, **kwargs)
98
+
99
+ def __getattr__(self, attr):
100
+
101
+ if self.channel_names is not None and self._multichannel:
102
+ assert (
103
+ len(self.channel_names) == self._intensity_image.shape[-1]
104
+ ), "Mismatch between provided channel names and the number of channels in the image..."
105
+
106
+ if attr == "__setstate__":
107
+ return self.__getattribute__(attr)
108
+
109
+ if self._intensity_image is None and attr in _require_intensity_image:
110
+ raise AttributeError(
111
+ f"Attribute '{attr}' unavailable when `intensity_image` "
112
+ f"has not been specified."
113
+ )
114
+ if attr in self._extra_properties:
115
+ func = self._extra_properties[attr]
116
+ n_args = _infer_number_of_required_args(func)
117
+ # determine whether func requires intensity image
118
+ if n_args == 2:
119
+ if self._intensity_image is not None:
120
+ if self._multichannel:
121
+ arg_dict = dict(inspect.signature(func).parameters)
122
+ if (
123
+ self.channel_names is not None
124
+ and "target_channel" in arg_dict
125
+ ):
126
+ multichannel_list = [
127
+ np.nan for i in range(self.image_intensity.shape[-1])
128
+ ]
129
+ len_output = 1
130
+ default_channel = arg_dict["target_channel"]._default
131
+
132
+ if default_channel in self.channel_names:
133
+
134
+ idx = self.channel_names.index(default_channel)
135
+ res = func(self.image, self.image_intensity[..., idx])
136
+ if isinstance(res, tuple):
137
+ len_output = len(res)
138
+ else:
139
+ len_output = 1
140
+
141
+ if len_output > 1:
142
+ multichannel_list = [
143
+ [np.nan] * len_output
144
+ for c in range(len(self.channel_names))
145
+ ]
146
+ multichannel_list[idx] = res
147
+ else:
148
+ multichannel_list = [
149
+ np.nan for c in range(len(self.channel_names))
150
+ ]
151
+ multichannel_list[idx] = res
152
+
153
+ else:
154
+ print(
155
+ f"Warning... Channel required by custom measurement ({default_channel}) could not be found in your data..."
156
+ )
157
+
158
+ return np.stack(multichannel_list, axis=-1)
159
+ else:
160
+ multichannel_list = [
161
+ func(self.image, self.image_intensity[..., i])
162
+ for i in range(self.image_intensity.shape[-1])
163
+ ]
164
+ return np.stack(multichannel_list, axis=-1)
165
+ else:
166
+ return func(self.image, self.image_intensity)
167
+ else:
168
+ raise AttributeError(
169
+ f"intensity image required to calculate {attr}"
170
+ )
171
+ elif n_args == 1:
172
+ return func(self.image)
173
+ else:
174
+ raise AttributeError(
175
+ f"Custom regionprop function's number of arguments must "
176
+ f"be 1 or 2, but {attr} takes {n_args} arguments."
177
+ )
178
+ elif attr in PROPS and attr.lower() == attr:
179
+ if (
180
+ self._intensity_image is None
181
+ and PROPS[attr] in _require_intensity_image
182
+ ):
183
+ raise AttributeError(
184
+ f"Attribute '{attr}' unavailable when `intensity_image` "
185
+ f"has not been specified."
186
+ )
187
+ # retrieve deprecated property (excluding old CamelCase ones)
188
+ return getattr(self, PROPS[attr])
189
+ else:
190
+ raise AttributeError(f"'{type(self)}' object has no attribute '{attr}'")
191
+
192
+ @property
193
+ @_cached
194
+ def image_intensity(self):
195
+ if self._intensity_image is None:
196
+ raise AttributeError("No intensity image specified.")
197
+ image = (
198
+ self.image
199
+ if not self._multichannel
200
+ else np.expand_dims(self.image, self._ndim)
201
+ )
202
+ return self._intensity_image[self.slice]
203
+
204
+
205
+ def regionprops(
206
+ label_image,
207
+ intensity_image=None,
208
+ cache=True,
209
+ channel_names=None,
210
+ *,
211
+ extra_properties=None,
212
+ spacing=None,
213
+ offset=None,
214
+ ):
215
+ """
216
+ From https://github.com/scikit-image/scikit-image/blob/main/skimage/measure/_regionprops.py with a modification to use CustomRegionProps
217
+ """
218
+
219
+ if label_image.ndim not in (2, 3):
220
+ raise TypeError("Only 2-D and 3-D images supported.")
221
+
222
+ if not np.issubdtype(label_image.dtype, np.integer):
223
+ if np.issubdtype(label_image.dtype, bool):
224
+ raise TypeError(
225
+ "Non-integer image types are ambiguous: "
226
+ "use skimage.measure.label to label the connected "
227
+ "components of label_image, "
228
+ "or label_image.astype(np.uint8) to interpret "
229
+ "the True values as a single label."
230
+ )
231
+ else:
232
+ raise TypeError("Non-integer label_image types are ambiguous")
233
+
234
+ if offset is None:
235
+ offset_arr = np.zeros((label_image.ndim,), dtype=int)
236
+ else:
237
+ offset_arr = np.asarray(offset)
238
+ if offset_arr.ndim != 1 or offset_arr.size != label_image.ndim:
239
+ raise ValueError(
240
+ "Offset should be an array-like of integers "
241
+ "of shape (label_image.ndim,); "
242
+ f"{offset} was provided."
243
+ )
244
+
245
+ regions = []
246
+
247
+ objects = find_objects(label_image)
248
+ for i, sl in enumerate(objects):
249
+ if sl is None:
250
+ continue
251
+
252
+ label = i + 1
253
+
254
+ props = CustomRegionProps(
255
+ channel_names,
256
+ sl,
257
+ label,
258
+ label_image,
259
+ intensity_image,
260
+ cache,
261
+ spacing=spacing,
262
+ extra_properties=extra_properties,
263
+ offset=offset_arr,
264
+ )
265
+ regions.append(props)
266
+
267
+ return regions
268
+
269
+
270
+ def _props_to_dict(regions, properties=("label", "bbox"), separator="-"):
271
+
272
+ out = {}
273
+ n = len(regions)
274
+ for prop in properties:
275
+ r = regions[0]
276
+ # Copy the original property name so the output will have the
277
+ # user-provided property name in the case of deprecated names.
278
+ orig_prop = prop
279
+ # determine the current property name for any deprecated property.
280
+ prop = PROPS.get(prop, prop)
281
+ rp = getattr(r, prop)
282
+ if prop in COL_DTYPES:
283
+ dtype = COL_DTYPES[prop]
284
+ else:
285
+ func = r._extra_properties[prop]
286
+ # dtype = _infer_regionprop_dtype(
287
+ # func,
288
+ # intensity=r._intensity_image is not None,
289
+ # ndim=r.image.ndim,
290
+ # )
291
+ dtype = np.float64
292
+
293
+ # scalars and objects are dedicated one column per prop
294
+ # array properties are raveled into multiple columns
295
+ # for more info, refer to notes 1
296
+ if np.isscalar(rp) or prop in OBJECT_COLUMNS or dtype is np.object_:
297
+ column_buffer = np.empty(n, dtype=dtype)
298
+ for i in range(n):
299
+ column_buffer[i] = regions[i][prop]
300
+ out[orig_prop] = np.copy(column_buffer)
301
+ else:
302
+ # precompute property column names and locations
303
+ modified_props = []
304
+ locs = []
305
+ for ind in np.ndindex(np.shape(rp)):
306
+ modified_props.append(separator.join(map(str, (orig_prop,) + ind)))
307
+ locs.append(ind if len(ind) > 1 else ind[0])
308
+
309
+ # fill temporary column data_array
310
+ n_columns = len(locs)
311
+ column_data = np.empty((n, n_columns), dtype=dtype)
312
+ for k in range(n):
313
+ # we coerce to a numpy array to ensure structures like
314
+ # tuple-of-arrays expand correctly into columns
315
+ rp = np.asarray(regions[k][prop])
316
+ for i, loc in enumerate(locs):
317
+ column_data[k, i] = rp[loc]
318
+
319
+ # add the columns to the output dictionary
320
+ for i, modified_prop in enumerate(modified_props):
321
+ out[modified_prop] = column_data[:, i]
322
+ return out
323
+
324
+
325
+ def regionprops_table(
326
+ label_image,
327
+ intensity_image=None,
328
+ properties=("label", "bbox"),
329
+ *,
330
+ cache=True,
331
+ separator="-",
332
+ extra_properties=None,
333
+ spacing=None,
334
+ channel_names=None,
335
+ ):
336
+ """
337
+ From https://github.com/scikit-image/scikit-image/blob/main/skimage/measure/_regionprops.py
338
+ """
339
+ regions = regionprops(
340
+ label_image,
341
+ intensity_image=intensity_image,
342
+ cache=cache,
343
+ extra_properties=extra_properties,
344
+ spacing=spacing,
345
+ channel_names=channel_names,
346
+ )
347
+ if extra_properties is not None:
348
+ properties = list(properties) + [prop.__name__ for prop in extra_properties]
349
+ if len(regions) == 0:
350
+ ndim = label_image.ndim
351
+ label_image = np.zeros((3,) * ndim, dtype=int)
352
+ label_image[(1,) * ndim] = 1
353
+ if intensity_image is not None:
354
+ intensity_image = np.zeros(
355
+ label_image.shape + intensity_image.shape[ndim:],
356
+ dtype=intensity_image.dtype,
357
+ )
358
+ regions = regionprops(
359
+ label_image,
360
+ intensity_image=intensity_image,
361
+ cache=cache,
362
+ extra_properties=extra_properties,
363
+ spacing=spacing,
364
+ channel_names=channel_names,
365
+ )
366
+ out_d = _props_to_dict(regions, properties=properties, separator=separator)
367
+ return {k: v[:0] for k, v in out_d.items()}
368
+
369
+ good_props = []
370
+ for prop in properties:
371
+ try:
372
+ nan_test = [np.isnan(getattr(r, prop)) for r in regions]
373
+ if not np.all(nan_test):
374
+ good_props.append(prop)
375
+ except AttributeError:
376
+ logger.warning(f"Could not measure {prop}... Skip...")
377
+
378
+ return _props_to_dict(regions, properties=good_props, separator=separator)