arcadia-microscopy-tools 0.2.4__py3-none-any.whl → 0.2.5__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.
@@ -3,7 +3,7 @@ from arcadia_microscopy_tools.channels import Channel
3
3
  from arcadia_microscopy_tools.microscopy import MicroscopyImage
4
4
  from arcadia_microscopy_tools.pipeline import ImageOperation, Pipeline, PipelineParallelized
5
5
 
6
- __version__ = "0.2.2"
6
+ __version__ = "0.2.5"
7
7
 
8
8
  __all__ = [
9
9
  "Channel",
@@ -34,8 +34,6 @@ DEFAULT_INTENSITY_PROPERTY_NAMES = [
34
34
  "intensity_std",
35
35
  ]
36
36
 
37
- OutlineExtractorMethod = Literal["cellpose", "skimage"]
38
-
39
37
 
40
38
  def _process_mask(
41
39
  mask_image: BoolArray | Int64Array,
@@ -68,7 +66,9 @@ def _extract_outlines_cellpose(label_image: Int64Array) -> list[Float64Array]:
68
66
  Returns:
69
67
  List of arrays, one per cell, containing outline coordinates in (y, x) format.
70
68
  """
71
- return outlines_list(label_image, multiprocessing=False)
69
+ outlines = outlines_list(label_image, multiprocessing=False)
70
+ # Cellpose returns (x, y) coordinates, flip to (y, x) to match standard (row, col) format
71
+ return [outline[:, [1, 0]] if len(outline) > 0 else outline for outline in outlines]
72
72
 
73
73
 
74
74
  def _extract_outlines_skimage(label_image: Int64Array) -> list[Float64Array]:
@@ -91,8 +91,6 @@ def _extract_outlines_skimage(label_image: Int64Array) -> list[Float64Array]:
91
91
  contours = ski.measure.find_contours(cell_mask, level=0.5)
92
92
  if contours:
93
93
  main_contour = max(contours, key=len)
94
- # Flip from (x, y) to (y, x) to match cellpose format
95
- main_contour = main_contour[:, [1, 0]]
96
94
  outlines.append(main_contour)
97
95
  else:
98
96
  # Include empty array to maintain alignment with cell labels
@@ -112,7 +110,8 @@ class SegmentationMask:
112
110
  {DAPI: array, FITC: array}
113
111
  remove_edge_cells: Whether to remove cells touching image borders. Defaults to True.
114
112
  outline_extractor: Outline extraction method ("cellpose" or "skimage").
115
- Defaults to "cellpose".
113
+ Defaults to "cellpose". In practice, cellpose is ~2x faster but skimage handles
114
+ vertically oriented cell outlines better.
116
115
  property_names: List of property names to compute. If None, uses
117
116
  DEFAULT_CELL_PROPERTY_NAMES.
118
117
  intensity_property_names: List of intensity property names to compute.
@@ -122,7 +121,7 @@ class SegmentationMask:
122
121
  mask_image: BoolArray | Int64Array
123
122
  intensity_image_dict: Mapping[Channel, UInt16Array] | None = None
124
123
  remove_edge_cells: bool = True
125
- outline_extractor: OutlineExtractorMethod = "cellpose"
124
+ outline_extractor: Literal["cellpose", "skimage"] = "cellpose"
126
125
  property_names: list[str] | None = field(default=None)
127
126
  intensity_property_names: list[str] | None = field(default=None)
128
127
 
@@ -133,8 +132,10 @@ class SegmentationMask:
133
132
  raise TypeError("mask_image must be a numpy array")
134
133
  if self.mask_image.ndim != 2:
135
134
  raise ValueError("mask_image must be a 2D array")
136
- if self.mask_image.min() < 0:
135
+ if np.any(self.mask_image < 0):
137
136
  raise ValueError("mask_image must have non-negative values")
137
+ if self.mask_image.max() == 0:
138
+ raise ValueError("mask_image contains no cells (all values are 0)")
138
139
 
139
140
  # Validate intensity_image dict if provided
140
141
  if self.intensity_image_dict is not None:
@@ -186,14 +187,20 @@ class SegmentationMask:
186
187
 
187
188
  Returns:
188
189
  List of arrays, one per cell, containing outline coordinates in (y, x) format.
189
- Returns empty list if no cells found.
190
+
191
+ Raises:
192
+ ValueError: If no cells are found in the mask.
193
+
194
+ Note:
195
+ The cellpose method is ~2x faster in general but skimage handles
196
+ vertically oriented cells/outlines better.
190
197
  """
191
198
  if self.num_cells == 0:
192
- return []
199
+ raise ValueError("No cells found in mask. Cannot extract cell outlines.")
193
200
 
194
201
  if self.outline_extractor == "cellpose":
195
202
  return _extract_outlines_cellpose(self.label_image)
196
- else: # Must be "skimage" due to Literal type
203
+ else: # must be "skimage" due to Literal type
197
204
  return _extract_outlines_skimage(self.label_image)
198
205
 
199
206
  @cached_property
@@ -205,24 +212,16 @@ class SegmentationMask:
205
212
 
206
213
  For multichannel intensity images, property names are suffixed with the channel name:
207
214
  - DAPI: "intensity_mean_DAPI"
208
- - FITC: "intensity_mean_FITC"
215
+ - FITC: "intensity_max_FITC"
209
216
 
210
217
  Returns:
211
218
  Dictionary mapping property names to arrays of values (one per cell).
212
- Returns empty dict if no cells found.
219
+
220
+ Raises:
221
+ ValueError: If no cells are found in the mask.
213
222
  """
214
223
  if self.num_cells == 0:
215
- empty_props = (
216
- {property_name: np.array([]) for property_name in self.property_names}
217
- if self.property_names
218
- else {}
219
- )
220
- # Add empty intensity properties if requested
221
- if self.intensity_image_dict and self.intensity_property_names:
222
- for channel in self.intensity_image_dict:
223
- for prop_name in self.intensity_property_names:
224
- empty_props[f"{prop_name}_{channel.name}"] = np.array([])
225
- return empty_props
224
+ raise ValueError("No cells found in mask. Cannot extract cell properties.")
226
225
 
227
226
  # Extract morphological properties (no intensity image needed)
228
227
  # Only compute extra properties if explicitly requested
@@ -232,12 +231,18 @@ class SegmentationMask:
232
231
  if self.property_names and "volume" in self.property_names:
233
232
  extra_props.append(volume)
234
233
 
234
+ # Compute cell properties
235
235
  properties = ski.measure.regionprops_table(
236
236
  self.label_image,
237
237
  properties=self.property_names,
238
238
  extra_properties=extra_props,
239
239
  )
240
240
 
241
+ if "centroid-0" in properties:
242
+ properties["centroid_y"] = properties.pop("centroid-0")
243
+ if "centroid-1" in properties:
244
+ properties["centroid_x"] = properties.pop("centroid-1")
245
+
241
246
  # Extract intensity properties for each channel
242
247
  if self.intensity_image_dict and self.intensity_property_names:
243
248
  for channel, intensities in self.intensity_image_dict.items():
@@ -248,7 +253,7 @@ class SegmentationMask:
248
253
  )
249
254
  # Add channel suffix to property names
250
255
  for prop_name, prop_values in channel_props.items():
251
- properties[f"{prop_name}_{channel.name}"] = prop_values
256
+ properties[f"{prop_name}_{channel.name.lower()}"] = prop_values
252
257
 
253
258
  return properties
254
259
 
@@ -260,6 +265,9 @@ class SegmentationMask:
260
265
  Array of shape (num_cells, 2) with centroid coordinates.
261
266
  Each row is [y_coordinate, x_coordinate] for one cell.
262
267
  Returns empty (0, 2) array with warning if "centroid" not in property_names.
268
+
269
+ Raises:
270
+ ValueError: If no cells are found in the mask.
263
271
  """
264
272
  if self.property_names and "centroid" not in self.property_names:
265
273
  warnings.warn(
@@ -270,8 +278,8 @@ class SegmentationMask:
270
278
  )
271
279
  return np.array([]).reshape(0, 2)
272
280
 
273
- yc = self.cell_properties["centroid-0"]
274
- xc = self.cell_properties["centroid-1"]
281
+ yc = self.cell_properties["centroid_y"]
282
+ xc = self.cell_properties["centroid_x"]
275
283
  return np.array([yc, xc], dtype=float).T
276
284
 
277
285
  def convert_properties_to_microns(
@@ -281,34 +289,28 @@ class SegmentationMask:
281
289
  """Convert cell properties from pixels to microns.
282
290
 
283
291
  Applies appropriate scaling factors based on the dimensionality of each property:
284
- - Linear measurements (1D): multiplied by pixel_size_um
285
- - Area measurements (2D): multiplied by pixel_size_um²
286
- - Volume measurements (3D): multiplied by pixel_size_um³
287
- - Dimensionless properties: unchanged
292
+ - Linear measurements (1D): multiplied by pixel_size_um, keys suffixed with "_um"
293
+ - Area measurements (2D): multiplied by pixel_size_um², keys suffixed with "_um2"
294
+ - Volume measurements (3D): multiplied by pixel_size_um³, keys suffixed with "_um3"
295
+ - Dimensionless properties: unchanged, keys unchanged
288
296
 
289
297
  Args:
290
298
  pixel_size_um: Pixel size in microns.
291
299
 
292
300
  Returns:
293
- Dictionary with the same keys as cell_properties but with values
301
+ Dictionary with keys renamed to include units and values
294
302
  converted to micron units where applicable.
295
303
 
296
304
  Note:
297
- Properties like 'label', 'circularity', 'eccentricity', 'solidity',
298
- and 'orientation' are dimensionless and remain unchanged.
299
- Intensity properties (intensity_mean, intensity_max, etc.) are also
300
- dimensionless and remain unchanged.
301
- Tensor properties (inertia_tensor, inertia_tensor_eigvals) are scaled
302
- as 2D quantities (pixel_size_um²).
305
+ Properties like 'label', 'circularity', 'eccentricity', 'solidity', and 'orientation'
306
+ are dimensionless and remain unchanged. Intensity properties (intensity_mean,
307
+ intensity_max, etc.) are also dimensionless and remain unchanged. Centroid coordinates
308
+ (centroid_y, centroid_x) remain in pixel coordinates as they represent image positions.
309
+ Tensor properties (inertia_tensor, inertia_tensor_eigvals) are scaled as 2D quantities
310
+ (pixel_size_um²) and suffixed with "_um2".
303
311
  """
304
312
  # Define which properties need which scaling
305
- linear_properties = {
306
- "perimeter",
307
- "axis_major_length",
308
- "axis_minor_length",
309
- "centroid-0",
310
- "centroid-1",
311
- }
313
+ linear_properties = {"perimeter", "axis_major_length", "axis_minor_length"}
312
314
  area_properties = {"area", "area_convex"}
313
315
  volume_properties = {"volume"}
314
316
  tensor_properties = {"inertia_tensor", "inertia_tensor_eigvals"}
@@ -316,13 +318,13 @@ class SegmentationMask:
316
318
  converted = {}
317
319
  for prop_name, prop_values in self.cell_properties.items():
318
320
  if prop_name in linear_properties:
319
- converted[prop_name] = prop_values * pixel_size_um
321
+ converted[f"{prop_name}_um"] = prop_values * pixel_size_um
320
322
  elif prop_name in area_properties:
321
- converted[prop_name] = prop_values * (pixel_size_um**2)
323
+ converted[f"{prop_name}_um2"] = prop_values * (pixel_size_um**2)
322
324
  elif prop_name in volume_properties:
323
- converted[prop_name] = prop_values * (pixel_size_um**3)
325
+ converted[f"{prop_name}_um3"] = prop_values * (pixel_size_um**3)
324
326
  elif prop_name in tensor_properties:
325
- converted[prop_name] = prop_values * (pixel_size_um**2)
327
+ converted[f"{prop_name}_um2"] = prop_values * (pixel_size_um**2)
326
328
  else:
327
329
  # Intensity-related, dimensionless, or label - no conversion
328
330
  converted[prop_name] = prop_values
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: arcadia-microscopy-tools
3
- Version: 0.2.4
3
+ Version: 0.2.5
4
4
  Summary: Python package for processing large-scale microscopy datasets generated by Arcadia's imaging suite
5
5
  License: MIT License
6
6
 
@@ -1,7 +1,7 @@
1
- arcadia_microscopy_tools/__init__.py,sha256=e6hSuo_4-fBlPEt6M9dMvNPH-HcDWboWSy6Vhp5_WzI,440
1
+ arcadia_microscopy_tools/__init__.py,sha256=HmMHWrlDh8pAB6f_an39kuDiujpPl4UBAvL0eBd1Bdo,440
2
2
  arcadia_microscopy_tools/blending.py,sha256=Y5xuius1tHRKIEfueUjZ7qGlBK02arEYqrzRRhEdSTI,7112
3
3
  arcadia_microscopy_tools/channels.py,sha256=sE54mJoJnFIMowO_qRG4lx-s_LOaVO10tuxpuVadJg8,6854
4
- arcadia_microscopy_tools/masks.py,sha256=Dot0hhJAE2O4HDlad5o6-z4FW646S_WRHT-Ua9MDxec,15272
4
+ arcadia_microscopy_tools/masks.py,sha256=112486kqgMwNxLvTk9FBj8Rdo-cMOaOpToFD1_RwUQ0,15800
5
5
  arcadia_microscopy_tools/metadata_structures.py,sha256=Bb4UXgiNuJcOITNGV_4hGR09HaN8Wt7heET4bXmNcw0,3481
6
6
  arcadia_microscopy_tools/microplate.py,sha256=df6HTeQdYQRD7rYKubx8_FWOZ1BbJVoyg7lYySHJQOU,8298
7
7
  arcadia_microscopy_tools/microscopy.py,sha256=gPvMVKukGkBY74Ajy71JOd7DfZOoCEfiE40qkBW-C-I,11313
@@ -25,7 +25,7 @@ arcadia_microscopy_tools/tests/data/example-pbmc.nd2,sha256=gqVP7cGePBJk45xRpyXa
25
25
  arcadia_microscopy_tools/tests/data/example-timelapse.nd2,sha256=KHCubkVWmkRRmJabhfINx_aTwNi5nVUl-IiYNKQsJ9Y,827392
26
26
  arcadia_microscopy_tools/tests/data/example-zstack.nd2,sha256=j70DrFhRTwRgzAAJivVM3mCho05YVgsqJwTmPBobRYo,606208
27
27
  arcadia_microscopy_tools/tests/data/known-metadata.yml,sha256=_ZIE04MnoLpZtG-6e8ZytYnmAkGh0Q7-2AwSP3v6rQk,1886
28
- arcadia_microscopy_tools-0.2.4.dist-info/METADATA,sha256=xFEXOcRN4v4pDmp8gK-iIkWhEL0v6zkwDT6ZiO1x6oA,5007
29
- arcadia_microscopy_tools-0.2.4.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
30
- arcadia_microscopy_tools-0.2.4.dist-info/licenses/LICENSE,sha256=5pPae5U0NNXysjBv3vjoquhhoCqTTi1Zh0SehM_IXHI,1072
31
- arcadia_microscopy_tools-0.2.4.dist-info/RECORD,,
28
+ arcadia_microscopy_tools-0.2.5.dist-info/METADATA,sha256=y2JLq5xdW2Anen9vmLV0LN4nOOrPSacQLvAmj3Q6IzA,5007
29
+ arcadia_microscopy_tools-0.2.5.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
30
+ arcadia_microscopy_tools-0.2.5.dist-info/licenses/LICENSE,sha256=5pPae5U0NNXysjBv3vjoquhhoCqTTi1Zh0SehM_IXHI,1072
31
+ arcadia_microscopy_tools-0.2.5.dist-info/RECORD,,