ngio 0.2.0a3__py3-none-any.whl → 0.2.0b1__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 (49) hide show
  1. ngio/__init__.py +4 -4
  2. ngio/common/__init__.py +12 -2
  3. ngio/common/_array_pipe.py +106 -0
  4. ngio/common/_axes_transforms.py +3 -2
  5. ngio/common/_dimensions.py +7 -0
  6. ngio/common/_masking_roi.py +158 -0
  7. ngio/common/_pyramid.py +16 -11
  8. ngio/common/_roi.py +74 -0
  9. ngio/common/_slicer.py +1 -2
  10. ngio/common/_zoom.py +5 -3
  11. ngio/hcs/__init__.py +2 -57
  12. ngio/hcs/plate.py +399 -0
  13. ngio/images/abstract_image.py +97 -28
  14. ngio/images/create.py +48 -29
  15. ngio/images/image.py +99 -46
  16. ngio/images/label.py +109 -92
  17. ngio/images/masked_image.py +259 -0
  18. ngio/images/omezarr_container.py +201 -64
  19. ngio/ome_zarr_meta/__init__.py +25 -13
  20. ngio/ome_zarr_meta/_meta_handlers.py +718 -69
  21. ngio/ome_zarr_meta/ngio_specs/__init__.py +8 -0
  22. ngio/ome_zarr_meta/ngio_specs/_channels.py +11 -0
  23. ngio/ome_zarr_meta/ngio_specs/_ngio_hcs.py +374 -2
  24. ngio/ome_zarr_meta/ngio_specs/_ngio_image.py +169 -119
  25. ngio/ome_zarr_meta/ngio_specs/_pixel_size.py +35 -3
  26. ngio/ome_zarr_meta/v04/__init__.py +17 -5
  27. ngio/ome_zarr_meta/v04/_v04_spec_utils.py +85 -12
  28. ngio/tables/__init__.py +2 -0
  29. ngio/tables/_validators.py +2 -4
  30. ngio/tables/backends/_anndata_utils.py +2 -1
  31. ngio/tables/backends/_anndata_v1.py +2 -1
  32. ngio/tables/backends/_json_v1.py +1 -1
  33. ngio/tables/tables_container.py +12 -2
  34. ngio/tables/v1/__init__.py +1 -2
  35. ngio/tables/v1/_feature_table.py +7 -5
  36. ngio/tables/v1/_generic_table.py +65 -11
  37. ngio/tables/v1/_roi_table.py +145 -27
  38. ngio/utils/_datasets.py +4 -2
  39. ngio/utils/_fractal_fsspec_store.py +3 -2
  40. ngio/utils/_logger.py +3 -1
  41. ngio/utils/_zarr_utils.py +25 -2
  42. {ngio-0.2.0a3.dist-info → ngio-0.2.0b1.dist-info}/METADATA +4 -1
  43. ngio-0.2.0b1.dist-info/RECORD +54 -0
  44. ngio/ome_zarr_meta/_generic_handlers.py +0 -320
  45. ngio/ome_zarr_meta/v04/_meta_handlers.py +0 -54
  46. ngio/tables/v1/_masking_roi_table.py +0 -175
  47. ngio-0.2.0a3.dist-info/RECORD +0 -54
  48. {ngio-0.2.0a3.dist-info → ngio-0.2.0b1.dist-info}/WHEEL +0 -0
  49. {ngio-0.2.0a3.dist-info → ngio-0.2.0b1.dist-info}/licenses/LICENSE +0 -0
ngio/hcs/plate.py ADDED
@@ -0,0 +1,399 @@
1
+ """A module for handling the Plate Collection in an OME-Zarr file."""
2
+
3
+ from ngio.images import OmeZarrContainer
4
+ from ngio.ome_zarr_meta import (
5
+ ImageInWellPath,
6
+ NgioPlateMeta,
7
+ NgioWellMeta,
8
+ find_plate_meta_handler,
9
+ find_well_meta_handler,
10
+ get_plate_meta_handler,
11
+ get_well_meta_handler,
12
+ )
13
+ from ngio.utils import (
14
+ AccessModeLiteral,
15
+ StoreOrGroup,
16
+ ZarrGroupHandler,
17
+ )
18
+
19
+
20
+ # Mock lock class that does nothing
21
+ class MockLock:
22
+ """A mock lock class that does nothing."""
23
+
24
+ def __enter__(self):
25
+ """Enter the lock."""
26
+ pass
27
+
28
+ def __exit__(self, exc_type, exc_value, traceback):
29
+ """Exit the lock."""
30
+ pass
31
+
32
+
33
+ class OmeZarrWell:
34
+ """A class to handle the Well Collection in an OME-Zarr file."""
35
+
36
+ def __init__(self, group_handler: ZarrGroupHandler) -> None:
37
+ """Initialize the LabelGroupHandler.
38
+
39
+ Args:
40
+ group_handler: The Zarr group handler that contains the Well.
41
+ """
42
+ self._group_handler = group_handler
43
+ self._meta_handler = find_well_meta_handler(group_handler)
44
+
45
+ @property
46
+ def meta_handler(self):
47
+ """Return the metadata handler."""
48
+ return self._meta_handler
49
+
50
+ @property
51
+ def meta(self):
52
+ """Return the metadata."""
53
+ return self._meta_handler.meta
54
+
55
+ def paths(self, acquisition: int | None = None) -> list[str]:
56
+ """Return the images paths in the well.
57
+
58
+ If acquisition is None, return all images paths in the well.
59
+ Else, return the images paths in the well for the given acquisition.
60
+
61
+ Args:
62
+ acquisition (int | None): The acquisition id to filter the images.
63
+ """
64
+ return self.meta.paths(acquisition)
65
+
66
+
67
+ class OmeZarrPlate:
68
+ """A class to handle the Plate Collection in an OME-Zarr file."""
69
+
70
+ def __init__(self, group_handler: ZarrGroupHandler) -> None:
71
+ """Initialize the LabelGroupHandler.
72
+
73
+ Args:
74
+ group_handler: The Zarr group handler that contains the Plate.
75
+ """
76
+ self._group_handler = group_handler
77
+ self._meta_handler = find_plate_meta_handler(group_handler)
78
+
79
+ def __repr__(self) -> str:
80
+ """Return a string representation of the plate."""
81
+ return f"Plate([rows x columns] ({len(self.rows)} x {len(self.columns)})"
82
+
83
+ @property
84
+ def meta_handler(self):
85
+ """Return the metadata handler."""
86
+ return self._meta_handler
87
+
88
+ @property
89
+ def meta(self):
90
+ """Return the metadata."""
91
+ return self._meta_handler.meta
92
+
93
+ @property
94
+ def columns(self) -> list[str]:
95
+ """Return the number of columns in the plate."""
96
+ return self.meta.columns
97
+
98
+ @property
99
+ def rows(self) -> list[str]:
100
+ """Return the number of rows in the plate."""
101
+ return self.meta.rows
102
+
103
+ @property
104
+ def acquisitions_names(self) -> list[str | None]:
105
+ """Return the acquisitions in the plate."""
106
+ return self.meta.acquisitions_names
107
+
108
+ @property
109
+ def acquisitions_ids(self) -> list[int]:
110
+ """Return the acquisitions ids in the plate."""
111
+ return self.meta.acquisitions_ids
112
+
113
+ @property
114
+ def wells_paths(self) -> list[str]:
115
+ """Return the wells paths in the plate."""
116
+ return self.meta.wells_paths
117
+
118
+ def get_well_path(self, row: str, column: int | str) -> str:
119
+ """Return the well path in the plate."""
120
+ return self.meta.get_well_path(row=row, column=column)
121
+
122
+ def get_image_path(self, row: str, column: int | str, path: str) -> str:
123
+ """Return the image path in the plate."""
124
+ well = self.get_well(row, column)
125
+ if path not in well.paths():
126
+ raise ValueError(f"Image {path} does not exist in well {row}{column}")
127
+ return f"{self.get_well_path(row, column)}/{path}"
128
+
129
+ def get_well(self, row: str, column: int | str) -> OmeZarrWell:
130
+ """Get a well from the plate.
131
+
132
+ Args:
133
+ row (str): The row of the well.
134
+ column (int | str): The column of the well.
135
+
136
+ Returns:
137
+ OmeZarrWell: The well.
138
+ """
139
+ well_path = self.meta.get_well_path(row=row, column=column)
140
+ group_handler = self._group_handler.derive_handler(well_path)
141
+ return OmeZarrWell(group_handler)
142
+
143
+ def get_wells(self) -> dict[str, OmeZarrWell]:
144
+ """Get all wells in the plate."""
145
+ wells = {}
146
+ for well_path in self.wells_paths:
147
+ group_handler = self._group_handler.derive_handler(well_path)
148
+ well = OmeZarrWell(group_handler)
149
+ wells[well_path] = well
150
+ return wells
151
+
152
+ def get_images(self, acquisition: int | None = None) -> list[OmeZarrContainer]:
153
+ """Get all images in the plate.
154
+
155
+ Args:
156
+ acquisition: The acquisition id to filter the images.
157
+ """
158
+ images = []
159
+ for well_path, well in self.get_wells().items():
160
+ for img_path in well.paths(acquisition):
161
+ full_path = f"{well_path}/{img_path}"
162
+ img_group_handler = self._group_handler.derive_handler(full_path)
163
+ images.append(OmeZarrContainer(img_group_handler))
164
+ return images
165
+
166
+ def get_well_images(
167
+ self, row: str, column: str | int, acquisition: int | None = None
168
+ ) -> list[OmeZarrContainer]:
169
+ """Get all images in a well.
170
+
171
+ Args:
172
+ row: The row of the well.
173
+ column: The column of the well.
174
+ acquisition: The acquisition id to filter the images.
175
+ """
176
+ well_path = self.meta.get_well_path(row=row, column=column)
177
+ group_handler = self._group_handler.derive_handler(well_path)
178
+ well = OmeZarrWell(group_handler)
179
+
180
+ images = []
181
+ for path in well.paths(acquisition):
182
+ image_path = f"{well_path}/{path}"
183
+ group_handler = self._group_handler.derive_handler(image_path)
184
+ images.append(OmeZarrContainer(group_handler))
185
+
186
+ return images
187
+
188
+ def _add_image(
189
+ self,
190
+ row: str,
191
+ column: int | str,
192
+ image_path: str,
193
+ acquisition_id: int | None = None,
194
+ acquisition_name: str | None = None,
195
+ atomic: bool = False,
196
+ ) -> StoreOrGroup:
197
+ """Add an image to an ome-zarr plate."""
198
+ if atomic:
199
+ plate_lock = self._group_handler.lock
200
+ else:
201
+ plate_lock = MockLock()
202
+
203
+ with plate_lock:
204
+ meta = self.meta
205
+ meta = meta.add_well(row, column, acquisition_id, acquisition_name)
206
+ self.meta_handler.write_meta(meta)
207
+ self.meta_handler._group_handler.clean_cache()
208
+
209
+ well_path = self.meta.get_well_path(row=row, column=column)
210
+ group_handler = self._group_handler.derive_handler(well_path)
211
+
212
+ if atomic:
213
+ well_lock = group_handler.lock
214
+ else:
215
+ well_lock = MockLock()
216
+
217
+ with well_lock:
218
+ attrs = group_handler.load_attrs()
219
+ if len(attrs) == 0:
220
+ # Initialize the well metadata
221
+ # if the group is empty
222
+ well_meta = NgioWellMeta.default_init()
223
+ meta_handler = get_well_meta_handler(group_handler, version="0.4")
224
+ else:
225
+ meta_handler = find_well_meta_handler(group_handler)
226
+ well_meta = meta_handler.meta
227
+
228
+ group_handler = self._group_handler.derive_handler(well_path)
229
+
230
+ well_meta = well_meta.add_image(path=image_path, acquisition=acquisition_id)
231
+ meta_handler.write_meta(well_meta)
232
+ meta_handler._group_handler.clean_cache()
233
+
234
+ return group_handler.get_group(image_path, create_mode=True)
235
+
236
+ def atomic_add_image(
237
+ self,
238
+ row: str,
239
+ column: int | str,
240
+ image_path: str,
241
+ acquisition_id: int | None = None,
242
+ acquisition_name: str | None = None,
243
+ ) -> StoreOrGroup:
244
+ """Parallel safe version of add_image."""
245
+ return self._add_image(
246
+ row=row,
247
+ column=column,
248
+ image_path=image_path,
249
+ acquisition_id=acquisition_id,
250
+ acquisition_name=acquisition_name,
251
+ atomic=True,
252
+ )
253
+
254
+ def add_image(
255
+ self,
256
+ row: str,
257
+ column: int | str,
258
+ image_path: str,
259
+ acquisition_id: int | None = None,
260
+ acquisition_name: str | None = None,
261
+ ) -> StoreOrGroup:
262
+ """Add an image to an ome-zarr plate."""
263
+ return self._add_image(
264
+ row=row,
265
+ column=column,
266
+ image_path=image_path,
267
+ acquisition_id=acquisition_id,
268
+ acquisition_name=acquisition_name,
269
+ atomic=False,
270
+ )
271
+
272
+ def _remove_well(
273
+ self,
274
+ row: str,
275
+ column: int | str,
276
+ atomic: bool = False,
277
+ ):
278
+ """Remove a well from an ome-zarr plate."""
279
+ if atomic:
280
+ plate_lock = self._group_handler.lock
281
+ else:
282
+ plate_lock = MockLock()
283
+
284
+ with plate_lock:
285
+ meta = self.meta
286
+ meta = meta.remove_well(row, column)
287
+ self.meta_handler.write_meta(meta)
288
+ self.meta_handler._group_handler.clean_cache()
289
+
290
+ def _remove_image(
291
+ self,
292
+ row: str,
293
+ column: int | str,
294
+ image_path: str,
295
+ atomic: bool = False,
296
+ ):
297
+ """Remove an image from an ome-zarr plate."""
298
+ well = self.get_well(row, column)
299
+
300
+ if atomic:
301
+ well_lock = well.meta_handler._group_handler.lock
302
+ else:
303
+ well_lock = MockLock()
304
+
305
+ with well_lock:
306
+ well_meta = well.meta
307
+ well_meta = well_meta.remove_image(path=image_path)
308
+ well.meta_handler.write_meta(well_meta)
309
+ well.meta_handler._group_handler.clean_cache()
310
+ if len(well_meta.paths()) == 0:
311
+ self._remove_well(row, column, atomic=atomic)
312
+
313
+ def atomic_remove_image(
314
+ self,
315
+ row: str,
316
+ column: int | str,
317
+ image_path: str,
318
+ ):
319
+ """Parallel safe version of remove_image."""
320
+ return self._remove_image(
321
+ row=row,
322
+ column=column,
323
+ image_path=image_path,
324
+ atomic=True,
325
+ )
326
+
327
+ def remove_image(
328
+ self,
329
+ row: str,
330
+ column: int | str,
331
+ image_path: str,
332
+ ):
333
+ """Remove an image from an ome-zarr plate."""
334
+ return self._remove_image(
335
+ row=row,
336
+ column=column,
337
+ image_path=image_path,
338
+ atomic=False,
339
+ )
340
+
341
+
342
+ def open_omezarr_plate(
343
+ store: StoreOrGroup,
344
+ cache: bool = False,
345
+ mode: AccessModeLiteral = "r+",
346
+ parallel_safe: bool = True,
347
+ ) -> OmeZarrPlate:
348
+ """Open an OME-Zarr plate.
349
+
350
+ Args:
351
+ store (StoreOrGroup): The Zarr store or group that stores the plate.
352
+ cache (bool): Whether to use a cache for the zarr group metadata.
353
+ mode (AccessModeLiteral): The
354
+ access mode for the image. Defaults to "r+".
355
+ parallel_safe (bool): Whether the group handler is parallel safe.
356
+ """
357
+ group_handler = ZarrGroupHandler(
358
+ store=store, cache=cache, mode=mode, parallel_safe=parallel_safe
359
+ )
360
+ return OmeZarrPlate(group_handler)
361
+
362
+
363
+ def create_empty_plate(
364
+ store: StoreOrGroup,
365
+ name: str,
366
+ images: list[ImageInWellPath] | None = None,
367
+ version: str = "0.4",
368
+ cache: bool = False,
369
+ overwrite: bool = False,
370
+ parallel_safe: bool = True,
371
+ ) -> OmeZarrPlate:
372
+ """Initialize and create an empty OME-Zarr plate."""
373
+ mode = "w" if overwrite else "w-"
374
+ group_handler = ZarrGroupHandler(
375
+ store=store, cache=True, mode=mode, parallel_safe=False
376
+ )
377
+ meta_handler = get_plate_meta_handler(group_handler, version=version)
378
+ plate_meta = NgioPlateMeta.default_init(
379
+ name=name,
380
+ version=version,
381
+ )
382
+ meta_handler.write_meta(plate_meta)
383
+
384
+ if images is not None:
385
+ plate = OmeZarrPlate(group_handler)
386
+ for image in images:
387
+ plate.add_image(
388
+ row=image.row,
389
+ column=image.column,
390
+ image_path=image.path,
391
+ acquisition_id=image.acquisition_id,
392
+ acquisition_name=image.acquisition_name,
393
+ )
394
+ return open_omezarr_plate(
395
+ store=store,
396
+ cache=cache,
397
+ mode="r+",
398
+ parallel_safe=parallel_safe,
399
+ )
@@ -8,9 +8,11 @@ import zarr
8
8
  from ngio.common import (
9
9
  ArrayLike,
10
10
  Dimensions,
11
+ RasterCooROI,
11
12
  WorldCooROI,
12
13
  consolidate_pyramid,
13
14
  get_pipe,
15
+ roi_to_slice_kwargs,
14
16
  set_pipe,
15
17
  )
16
18
  from ngio.ome_zarr_meta import (
@@ -19,10 +21,8 @@ from ngio.ome_zarr_meta import (
19
21
  LabelMetaHandler,
20
22
  PixelSize,
21
23
  )
22
- from ngio.utils import (
23
- NgioFileExistsError,
24
- ZarrGroupHandler,
25
- )
24
+ from ngio.tables import RoiTable
25
+ from ngio.utils import NgioFileExistsError, ZarrGroupHandler
26
26
 
27
27
  _image_handler = TypeVar("_image_handler", ImageMetaHandler, LabelMetaHandler)
28
28
 
@@ -156,19 +156,8 @@ class AbstractImage(Generic[_image_handler]):
156
156
  Returns:
157
157
  The array of the region of interest.
158
158
  """
159
- raster_roi = roi.to_raster_coo(
160
- pixel_size=self.pixel_size, dimensions=self.dimensions
161
- ).to_slices()
162
-
163
- for key in slice_kwargs.keys():
164
- if key in raster_roi:
165
- raise ValueError(
166
- f"Key {key} is already in the slice_kwargs. "
167
- "Ambiguous which one to use: "
168
- f"{key}={slice_kwargs[key]} or roi_{key}={raster_roi[key]}"
169
- )
170
- return self.get_array(
171
- axes_order=axes_order, mode=mode, **raster_roi, **slice_kwargs
159
+ return get_roi_pipe(
160
+ image=self, roi=roi, axes_order=axes_order, mode=mode, **slice_kwargs
172
161
  )
173
162
 
174
163
  def set_array(
@@ -209,18 +198,13 @@ class AbstractImage(Generic[_image_handler]):
209
198
  **slice_kwargs: The slices to set the patch.
210
199
 
211
200
  """
212
- raster_roi = roi.to_raster_coo(
213
- pixel_size=self.pixel_size, dimensions=self.dimensions
214
- ).to_slices()
201
+ return set_roi_pipe(
202
+ image=self, roi=roi, patch=patch, axes_order=axes_order, **slice_kwargs
203
+ )
215
204
 
216
- for key in slice_kwargs.keys():
217
- if key in raster_roi:
218
- raise ValueError(
219
- f"Key {key} is already in the slice_kwargs. "
220
- "Ambiguous which one to use: "
221
- f"{key}={slice_kwargs[key]} or roi_{key}={raster_roi[key]}"
222
- )
223
- self.set_array(patch=patch, axes_order=axes_order, **raster_roi, **slice_kwargs)
205
+ def build_image_roi_table(self, name: str = "image") -> RoiTable:
206
+ """Build the ROI table for an image."""
207
+ return build_image_roi_table(image=self, name=name)
224
208
 
225
209
 
226
210
  def consolidate_image(
@@ -238,3 +222,88 @@ def consolidate_image(
238
222
  consolidate_pyramid(
239
223
  source=image.zarr_array, targets=targets, order=order, mode=mode
240
224
  )
225
+
226
+
227
+ def get_roi_pipe(
228
+ image: AbstractImage,
229
+ roi: WorldCooROI,
230
+ axes_order: Collection[str] | None = None,
231
+ mode: Literal["numpy", "dask", "delayed"] = "numpy",
232
+ **slice_kwargs: slice | int | Iterable[int],
233
+ ) -> ArrayLike:
234
+ """Get a slice of the image.
235
+
236
+ Args:
237
+ image: The image to get the ROI.
238
+ roi: The region of interest to get the array.
239
+ axes_order: The order of the axes to return the array.
240
+ mode: The mode to return the array.
241
+ **slice_kwargs: The slices to get the array.
242
+
243
+ Returns:
244
+ The array of the region of interest.
245
+ """
246
+ slice_kwargs = roi_to_slice_kwargs(
247
+ roi=roi,
248
+ pixel_size=image.pixel_size,
249
+ dimensions=image.dimensions,
250
+ **slice_kwargs,
251
+ )
252
+ return get_pipe(
253
+ array=image.zarr_array,
254
+ dimensions=image.dimensions,
255
+ axes_order=axes_order,
256
+ mode=mode,
257
+ **slice_kwargs,
258
+ )
259
+
260
+
261
+ def set_roi_pipe(
262
+ image: AbstractImage,
263
+ roi: WorldCooROI,
264
+ patch: ArrayLike,
265
+ axes_order: Collection[str] | None = None,
266
+ **slice_kwargs: slice | int | Iterable[int],
267
+ ) -> None:
268
+ """Set a slice of the image.
269
+
270
+ Args:
271
+ image: The image to set the ROI.
272
+ roi: The region of interest to set the patch.
273
+ patch: The patch to set.
274
+ axes_order: The order of the axes to set the patch.
275
+ **slice_kwargs: The slices to set the patch.
276
+
277
+ """
278
+ slice_kwargs = roi_to_slice_kwargs(
279
+ roi=roi,
280
+ pixel_size=image.pixel_size,
281
+ dimensions=image.dimensions,
282
+ **slice_kwargs,
283
+ )
284
+ set_pipe(
285
+ array=image.zarr_array,
286
+ patch=patch,
287
+ dimensions=image.dimensions,
288
+ axes_order=axes_order,
289
+ **slice_kwargs,
290
+ )
291
+
292
+
293
+ def build_image_roi_table(image: AbstractImage, name: str = "image") -> RoiTable:
294
+ """Build the ROI table for an image."""
295
+ dim_z, dim_y, dim_x = (
296
+ image.dimensions.get("z", strict=False),
297
+ image.dimensions.get("y"),
298
+ image.dimensions.get("x"),
299
+ )
300
+ image_roi = RasterCooROI(
301
+ name=name,
302
+ x=0,
303
+ y=0,
304
+ z=0,
305
+ x_length=dim_x,
306
+ y_length=dim_y,
307
+ z_length=dim_z,
308
+ )
309
+ return RoiTable(rois=[image_roi.to_world_coo_roi(pixel_size=image.pixel_size)])