ngio 0.5.0b6__py3-none-any.whl → 0.5.1__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.
- ngio/common/_masking_roi.py +18 -5
- ngio/common/_pyramid.py +129 -30
- ngio/hcs/_plate.py +30 -24
- ngio/images/_abstract_image.py +182 -60
- ngio/images/_create_synt_container.py +5 -1
- ngio/images/_create_utils.py +69 -74
- ngio/images/_image.py +350 -86
- ngio/images/_label.py +39 -31
- ngio/images/_masked_image.py +2 -2
- ngio/images/_ome_zarr_container.py +263 -96
- ngio/io_pipes/_match_shape.py +10 -14
- ngio/io_pipes/_ops_slices.py +6 -4
- ngio/io_pipes/_ops_slices_utils.py +8 -7
- ngio/ome_zarr_meta/_meta_handlers.py +2 -26
- ngio/ome_zarr_meta/ngio_specs/__init__.py +2 -0
- ngio/ome_zarr_meta/ngio_specs/_axes.py +161 -58
- ngio/ome_zarr_meta/ngio_specs/_ngio_hcs.py +78 -32
- ngio/ome_zarr_meta/ngio_specs/_ngio_image.py +36 -0
- ngio/ome_zarr_meta/v04/_v04_spec.py +5 -22
- ngio/ome_zarr_meta/v05/_v05_spec.py +7 -23
- ngio/resources/__init__.py +1 -1
- ngio/resources/resource_model.py +1 -1
- ngio/tables/_tables_container.py +39 -7
- ngio/tables/v1/_roi_table.py +4 -4
- ngio/utils/_zarr_utils.py +8 -15
- {ngio-0.5.0b6.dist-info → ngio-0.5.1.dist-info}/METADATA +3 -2
- {ngio-0.5.0b6.dist-info → ngio-0.5.1.dist-info}/RECORD +29 -29
- {ngio-0.5.0b6.dist-info → ngio-0.5.1.dist-info}/WHEEL +0 -0
- {ngio-0.5.0b6.dist-info → ngio-0.5.1.dist-info}/licenses/LICENSE +0 -0
ngio/common/_masking_roi.py
CHANGED
|
@@ -99,7 +99,14 @@ def compute_slices(segmentation: np.ndarray) -> dict[int, tuple[slice, ...]]:
|
|
|
99
99
|
|
|
100
100
|
|
|
101
101
|
def lazy_compute_slices(segmentation: da.Array) -> dict[int, tuple[slice, ...]]:
|
|
102
|
-
"""Compute slices for each label in a segmentation.
|
|
102
|
+
"""Compute slices for each label in a segmentation using lazy evaluation.
|
|
103
|
+
|
|
104
|
+
Args:
|
|
105
|
+
segmentation: The dask segmentation array.
|
|
106
|
+
|
|
107
|
+
Returns:
|
|
108
|
+
A dictionary mapping label IDs to their bounding box slices.
|
|
109
|
+
"""
|
|
103
110
|
global_offsets = _compute_offsets(segmentation.chunks)
|
|
104
111
|
delayed_chunks = segmentation.to_delayed() # type: ignore
|
|
105
112
|
|
|
@@ -120,12 +127,18 @@ def compute_masking_roi(
|
|
|
120
127
|
pixel_size: PixelSize,
|
|
121
128
|
axes_order: Sequence[str],
|
|
122
129
|
) -> list[Roi]:
|
|
123
|
-
"""Compute
|
|
130
|
+
"""Compute ROIs for each label in a segmentation.
|
|
131
|
+
|
|
132
|
+
This function expects a 2D, 3D, or 4D segmentation array.
|
|
133
|
+
The axes order should match the segmentation dimensions.
|
|
124
134
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
135
|
+
Args:
|
|
136
|
+
segmentation: The segmentation array (2D, 3D, or 4D).
|
|
137
|
+
pixel_size: The pixel size metadata for coordinate conversion.
|
|
138
|
+
axes_order: The order of axes in the segmentation (e.g., 'zyx' or 'yx').
|
|
128
139
|
|
|
140
|
+
Returns:
|
|
141
|
+
A list of Roi objects, one for each unique label in the segmentation.
|
|
129
142
|
"""
|
|
130
143
|
if segmentation.ndim not in [2, 3, 4]:
|
|
131
144
|
raise NgioValueError("Only 2D, 3D, and 4D segmentations are supported.")
|
ngio/common/_pyramid.py
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import itertools
|
|
2
|
+
import math
|
|
1
3
|
from collections.abc import Callable, Mapping, Sequence
|
|
2
4
|
from typing import Any, Literal
|
|
3
5
|
|
|
@@ -195,7 +197,7 @@ ChunksLike = tuple[int, ...] | Literal["auto"]
|
|
|
195
197
|
ShardsLike = tuple[int, ...] | Literal["auto"]
|
|
196
198
|
|
|
197
199
|
|
|
198
|
-
def
|
|
200
|
+
def compute_shapes_from_scaling_factors(
|
|
199
201
|
base_shape: tuple[int, ...],
|
|
200
202
|
scaling_factors: tuple[float, ...],
|
|
201
203
|
num_levels: int,
|
|
@@ -215,7 +217,7 @@ def shapes_from_scaling_factors(
|
|
|
215
217
|
for _ in range(num_levels):
|
|
216
218
|
shapes.append(current_shape)
|
|
217
219
|
current_shape = tuple(
|
|
218
|
-
max(1,
|
|
220
|
+
max(1, math.floor(s / f))
|
|
219
221
|
for s, f in zip(current_shape, scaling_factors, strict=True)
|
|
220
222
|
)
|
|
221
223
|
return shapes
|
|
@@ -233,6 +235,7 @@ class PyramidLevel(BaseModel):
|
|
|
233
235
|
path: str
|
|
234
236
|
shape: tuple[int, ...]
|
|
235
237
|
scale: tuple[float, ...]
|
|
238
|
+
translation: tuple[float, ...]
|
|
236
239
|
chunks: ChunksLike = "auto"
|
|
237
240
|
shards: ShardsLike | None = None
|
|
238
241
|
|
|
@@ -247,6 +250,12 @@ class PyramidLevel(BaseModel):
|
|
|
247
250
|
if any(isinstance(s, float) and s < 0 for s in self.scale):
|
|
248
251
|
raise NgioValueError("Scale values must be positive.")
|
|
249
252
|
|
|
253
|
+
if len(self.translation) != len(self.shape):
|
|
254
|
+
raise NgioValueError(
|
|
255
|
+
"Translation must have the same length as shape "
|
|
256
|
+
f"({len(self.shape)}), got {len(self.translation)}"
|
|
257
|
+
)
|
|
258
|
+
|
|
250
259
|
if isinstance(self.chunks, tuple):
|
|
251
260
|
if len(self.chunks) != len(self.shape):
|
|
252
261
|
raise NgioValueError(
|
|
@@ -271,6 +280,63 @@ class PyramidLevel(BaseModel):
|
|
|
271
280
|
return self
|
|
272
281
|
|
|
273
282
|
|
|
283
|
+
def compute_scales_from_shapes(
|
|
284
|
+
shapes: Sequence[tuple[int, ...]],
|
|
285
|
+
base_scale: tuple[float, ...],
|
|
286
|
+
) -> list[tuple[float, ...]]:
|
|
287
|
+
scales = [base_scale]
|
|
288
|
+
scale_ = base_scale
|
|
289
|
+
for current_shape, next_shape in itertools.pairwise(shapes):
|
|
290
|
+
# This only works for downsampling pyramids
|
|
291
|
+
# The _check_order function (called before) ensures that the
|
|
292
|
+
# shapes are decreasing
|
|
293
|
+
_scaling_factor = tuple(
|
|
294
|
+
s1 / s2
|
|
295
|
+
for s1, s2 in zip(
|
|
296
|
+
current_shape,
|
|
297
|
+
next_shape,
|
|
298
|
+
strict=True,
|
|
299
|
+
)
|
|
300
|
+
)
|
|
301
|
+
scale_ = tuple(s * f for s, f in zip(scale_, _scaling_factor, strict=True))
|
|
302
|
+
scales.append(scale_)
|
|
303
|
+
return scales
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
def _compute_translations_from_shapes(
|
|
307
|
+
scales: Sequence[tuple[float, ...]],
|
|
308
|
+
base_translation: Sequence[float] | None,
|
|
309
|
+
) -> list[tuple[float, ...]]:
|
|
310
|
+
translations = []
|
|
311
|
+
if base_translation is None:
|
|
312
|
+
n_dim = len(scales[0])
|
|
313
|
+
base_translation = tuple(0.0 for _ in range(n_dim))
|
|
314
|
+
else:
|
|
315
|
+
base_translation = tuple(base_translation)
|
|
316
|
+
|
|
317
|
+
translation_ = base_translation
|
|
318
|
+
for _ in scales:
|
|
319
|
+
# TBD: How to update translation
|
|
320
|
+
# For now, we keep it constant but we should probably change it
|
|
321
|
+
# to reflect the shift introduced by downsampling
|
|
322
|
+
# translation_ = translation_ + _scaling_factor
|
|
323
|
+
translations.append(translation_)
|
|
324
|
+
return translations
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
def _compute_scales_from_factors(
|
|
328
|
+
base_scale: tuple[float, ...], scaling_factors: tuple[float, ...], num_levels: int
|
|
329
|
+
) -> list[tuple[float, ...]]:
|
|
330
|
+
precision_scales = []
|
|
331
|
+
current_scale = base_scale
|
|
332
|
+
for _ in range(num_levels):
|
|
333
|
+
precision_scales.append(current_scale)
|
|
334
|
+
current_scale = tuple(
|
|
335
|
+
s * f for s, f in zip(current_scale, scaling_factors, strict=True)
|
|
336
|
+
)
|
|
337
|
+
return precision_scales
|
|
338
|
+
|
|
339
|
+
|
|
274
340
|
class ImagePyramidBuilder(BaseModel):
|
|
275
341
|
levels: list[PyramidLevel]
|
|
276
342
|
axes: tuple[str, ...]
|
|
@@ -290,6 +356,7 @@ class ImagePyramidBuilder(BaseModel):
|
|
|
290
356
|
base_shape: tuple[int, ...],
|
|
291
357
|
base_scale: tuple[float, ...],
|
|
292
358
|
axes: tuple[str, ...],
|
|
359
|
+
base_translation: Sequence[float] | None = None,
|
|
293
360
|
chunks: ChunksLike = "auto",
|
|
294
361
|
shards: ShardsLike | None = None,
|
|
295
362
|
data_type: str = "uint16",
|
|
@@ -297,16 +364,39 @@ class ImagePyramidBuilder(BaseModel):
|
|
|
297
364
|
compressors: Any = "auto",
|
|
298
365
|
zarr_format: Literal[2, 3] = 2,
|
|
299
366
|
other_array_kwargs: Mapping[str, Any] | None = None,
|
|
367
|
+
precision_scale: bool = True,
|
|
300
368
|
) -> "ImagePyramidBuilder":
|
|
301
|
-
shapes
|
|
369
|
+
# Since shapes needs to be rounded to integers, we compute them here
|
|
370
|
+
# and then pass them to from_shapes
|
|
371
|
+
# This ensures that the shapes and scaling factors are consistent
|
|
372
|
+
# and avoids accumulation of rounding errors
|
|
373
|
+
shapes = compute_shapes_from_scaling_factors(
|
|
302
374
|
base_shape=base_shape,
|
|
303
375
|
scaling_factors=scaling_factors,
|
|
304
376
|
num_levels=len(levels_paths),
|
|
305
377
|
)
|
|
378
|
+
|
|
379
|
+
if precision_scale:
|
|
380
|
+
# Compute precise scales from shapes
|
|
381
|
+
# Since shapes are rounded to integers, the scaling factors
|
|
382
|
+
# may not be exactly the same as the input scaling factors
|
|
383
|
+
# Thus, we compute the scales from the shapes to ensure consistency
|
|
384
|
+
base_scale_ = compute_scales_from_shapes(
|
|
385
|
+
shapes=shapes,
|
|
386
|
+
base_scale=base_scale,
|
|
387
|
+
)
|
|
388
|
+
else:
|
|
389
|
+
base_scale_ = _compute_scales_from_factors(
|
|
390
|
+
base_scale=base_scale,
|
|
391
|
+
scaling_factors=scaling_factors,
|
|
392
|
+
num_levels=len(levels_paths),
|
|
393
|
+
)
|
|
394
|
+
|
|
306
395
|
return cls.from_shapes(
|
|
307
396
|
shapes=shapes,
|
|
308
|
-
base_scale=
|
|
397
|
+
base_scale=base_scale_,
|
|
309
398
|
axes=axes,
|
|
399
|
+
base_translation=base_translation,
|
|
310
400
|
levels_paths=levels_paths,
|
|
311
401
|
chunks=chunks,
|
|
312
402
|
shards=shards,
|
|
@@ -321,8 +411,9 @@ class ImagePyramidBuilder(BaseModel):
|
|
|
321
411
|
def from_shapes(
|
|
322
412
|
cls,
|
|
323
413
|
shapes: Sequence[tuple[int, ...]],
|
|
324
|
-
base_scale: tuple[float, ...],
|
|
414
|
+
base_scale: tuple[float, ...] | list[tuple[float, ...]],
|
|
325
415
|
axes: tuple[str, ...],
|
|
416
|
+
base_translation: Sequence[float] | None = None,
|
|
326
417
|
levels_paths: Sequence[str] | None = None,
|
|
327
418
|
chunks: ChunksLike = "auto",
|
|
328
419
|
shards: ShardsLike | None = None,
|
|
@@ -335,34 +426,42 @@ class ImagePyramidBuilder(BaseModel):
|
|
|
335
426
|
levels = []
|
|
336
427
|
if levels_paths is None:
|
|
337
428
|
levels_paths = tuple(str(i) for i in range(len(shapes)))
|
|
429
|
+
|
|
338
430
|
_check_order(shapes)
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
431
|
+
if isinstance(base_scale, tuple) and all(
|
|
432
|
+
isinstance(s, float) for s in base_scale
|
|
433
|
+
):
|
|
434
|
+
scales = compute_scales_from_shapes(shapes, base_scale)
|
|
435
|
+
elif isinstance(base_scale, list):
|
|
436
|
+
scales = base_scale
|
|
437
|
+
if len(scales) != len(shapes):
|
|
438
|
+
raise NgioValueError(
|
|
439
|
+
"Scales must have the same length as shapes "
|
|
440
|
+
f"({len(shapes)}), got {len(scales)}"
|
|
348
441
|
)
|
|
442
|
+
else:
|
|
443
|
+
raise NgioValueError(
|
|
444
|
+
"base_scale must be either a tuple of floats or a list of tuples "
|
|
445
|
+
" of floats."
|
|
349
446
|
)
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
447
|
+
|
|
448
|
+
translations = _compute_translations_from_shapes(scales, base_translation)
|
|
449
|
+
for level_path, shape, scale, translation in zip(
|
|
450
|
+
levels_paths,
|
|
451
|
+
shapes,
|
|
452
|
+
scales,
|
|
453
|
+
translations,
|
|
454
|
+
strict=True,
|
|
455
|
+
):
|
|
456
|
+
level = PyramidLevel(
|
|
457
|
+
path=level_path,
|
|
458
|
+
shape=shape,
|
|
459
|
+
scale=scale,
|
|
460
|
+
translation=translation,
|
|
461
|
+
chunks=chunks,
|
|
462
|
+
shards=shards,
|
|
463
|
+
)
|
|
464
|
+
levels.append(level)
|
|
366
465
|
other_array_kwargs = other_array_kwargs or {}
|
|
367
466
|
return cls(
|
|
368
467
|
levels=levels,
|
ngio/hcs/_plate.py
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"""A module for handling the Plate Sequence in an OME-Zarr file."""
|
|
2
2
|
|
|
3
3
|
import asyncio
|
|
4
|
-
import
|
|
4
|
+
import logging
|
|
5
5
|
from collections.abc import Sequence
|
|
6
6
|
from typing import Literal
|
|
7
7
|
|
|
@@ -48,6 +48,8 @@ from ngio.utils import (
|
|
|
48
48
|
ZarrGroupHandler,
|
|
49
49
|
)
|
|
50
50
|
|
|
51
|
+
logger = logging.getLogger(f"ngio:{__name__}")
|
|
52
|
+
|
|
51
53
|
|
|
52
54
|
def _try_get_table_container(
|
|
53
55
|
handler: ZarrGroupHandler, create_mode: bool = True
|
|
@@ -918,12 +920,10 @@ class OmeZarrPlate:
|
|
|
918
920
|
|
|
919
921
|
"""
|
|
920
922
|
if check_type is not None:
|
|
921
|
-
|
|
922
|
-
"The 'check_type' argument is deprecated
|
|
923
|
-
"ngio=0.
|
|
924
|
-
"type specific get_*table() methods."
|
|
925
|
-
DeprecationWarning,
|
|
926
|
-
stacklevel=2,
|
|
923
|
+
logger.warning(
|
|
924
|
+
"The 'check_type' argument is deprecated and will be removed in "
|
|
925
|
+
"ngio=0.6. Please use 'get_table_as' instead or one of the "
|
|
926
|
+
"type specific get_*table() methods."
|
|
927
927
|
)
|
|
928
928
|
return self.tables_container.get(name=name, strict=False)
|
|
929
929
|
|
|
@@ -1182,9 +1182,21 @@ def _create_empty_plate_from_meta(
|
|
|
1182
1182
|
meta: NgioPlateMeta,
|
|
1183
1183
|
overwrite: bool = False,
|
|
1184
1184
|
) -> ZarrGroupHandler:
|
|
1185
|
-
"""Create an empty OME-Zarr plate from metadata.
|
|
1185
|
+
"""Create an empty OME-Zarr plate from metadata.
|
|
1186
|
+
|
|
1187
|
+
Args:
|
|
1188
|
+
store: The Zarr store or group to create the plate in.
|
|
1189
|
+
meta: The plate metadata to use.
|
|
1190
|
+
overwrite: Whether to overwrite an existing plate.
|
|
1191
|
+
|
|
1192
|
+
Returns:
|
|
1193
|
+
The ZarrGroupHandler for the created plate.
|
|
1194
|
+
"""
|
|
1186
1195
|
mode = "w" if overwrite else "w-"
|
|
1187
|
-
|
|
1196
|
+
zarr_format = 2 if meta.plate.version == "0.4" else 3
|
|
1197
|
+
group_handler = ZarrGroupHandler(
|
|
1198
|
+
store=store, cache=True, mode=mode, zarr_format=zarr_format
|
|
1199
|
+
)
|
|
1188
1200
|
update_ngio_plate_meta(group_handler, meta)
|
|
1189
1201
|
return group_handler
|
|
1190
1202
|
|
|
@@ -1211,11 +1223,9 @@ def create_empty_plate(
|
|
|
1211
1223
|
overwrite (bool): Whether to overwrite the existing plate.
|
|
1212
1224
|
"""
|
|
1213
1225
|
if version is not None:
|
|
1214
|
-
|
|
1215
|
-
"The 'version' argument is deprecated
|
|
1216
|
-
"Please use 'ngff_version' instead."
|
|
1217
|
-
DeprecationWarning,
|
|
1218
|
-
stacklevel=2,
|
|
1226
|
+
logger.warning(
|
|
1227
|
+
"The 'version' argument is deprecated and will be removed in ngio=0.6. "
|
|
1228
|
+
"Please use 'ngff_version' instead."
|
|
1219
1229
|
)
|
|
1220
1230
|
ngff_version = version
|
|
1221
1231
|
plate_meta = NgioPlateMeta.default_init(
|
|
@@ -1268,11 +1278,9 @@ def derive_ome_zarr_plate(
|
|
|
1268
1278
|
overwrite (bool): Whether to overwrite the existing plate.
|
|
1269
1279
|
"""
|
|
1270
1280
|
if version is not None:
|
|
1271
|
-
|
|
1272
|
-
"The 'version' argument is deprecated
|
|
1273
|
-
"Please use 'ngff_version' instead."
|
|
1274
|
-
DeprecationWarning,
|
|
1275
|
-
stacklevel=2,
|
|
1281
|
+
logger.warning(
|
|
1282
|
+
"The 'version' argument is deprecated and will be removed in ngio=0.6. "
|
|
1283
|
+
"Please use 'ngff_version' instead."
|
|
1276
1284
|
)
|
|
1277
1285
|
ngff_version = version
|
|
1278
1286
|
|
|
@@ -1333,11 +1341,9 @@ def create_empty_well(
|
|
|
1333
1341
|
overwrite (bool): Whether to overwrite the existing well.
|
|
1334
1342
|
"""
|
|
1335
1343
|
if version is not None:
|
|
1336
|
-
|
|
1337
|
-
"The 'version' argument is deprecated
|
|
1338
|
-
"Please use 'ngff_version' instead."
|
|
1339
|
-
DeprecationWarning,
|
|
1340
|
-
stacklevel=2,
|
|
1344
|
+
logger.warning(
|
|
1345
|
+
"The 'version' argument is deprecated and will be removed in ngio=0.6. "
|
|
1346
|
+
"Please use 'ngff_version' instead."
|
|
1341
1347
|
)
|
|
1342
1348
|
ngff_version = version
|
|
1343
1349
|
group_handler = ZarrGroupHandler(
|