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.
@@ -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 a ROIs for each label in a segmentation.
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
- This function expects a 2D or 3D segmentation array.
126
- And this function expects the axes order to be 'zyx' or 'yx'.
127
- Other axes orders are not supported.
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 shapes_from_scaling_factors(
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, int(s / f))
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 = shapes_from_scaling_factors(
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=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
- scale_ = base_scale
340
- for i, (path, shape) in enumerate(zip(levels_paths, shapes, strict=True)):
341
- levels.append(
342
- PyramidLevel(
343
- path=path,
344
- shape=shape,
345
- scale=scale_,
346
- chunks=chunks,
347
- shards=shards,
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
- if i + 1 < len(shapes):
351
- # This only works for downsampling pyramids
352
- # The _check_order function ensures that
353
- # shapes are decreasing
354
- next_shape = shapes[i + 1]
355
- scaling_factor = tuple(
356
- s1 / s2
357
- for s1, s2 in zip(
358
- shape,
359
- next_shape,
360
- strict=True,
361
- )
362
- )
363
- scale_ = tuple(
364
- s * f for s, f in zip(scale_, scaling_factor, strict=True)
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 warnings
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
- warnings.warn(
922
- "The 'check_type' argument is deprecated, and will be removed in "
923
- "ngio=0.3. Use 'get_table_as' instead or one of the "
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
- group_handler = ZarrGroupHandler(store=store, cache=True, mode=mode)
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
- warnings.warn(
1215
- "The 'version' argument is deprecated, and will be removed in ngio=0.3. "
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
- warnings.warn(
1272
- "The 'version' argument is deprecated, and will be removed in ngio=0.3. "
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
- warnings.warn(
1337
- "The 'version' argument is deprecated, and will be removed in ngio=0.3. "
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(