ngio 0.4.0a4__py3-none-any.whl → 0.4.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/_dimensions.py +209 -189
- ngio/common/_roi.py +2 -2
- ngio/experimental/iterators/__init__.py +0 -2
- ngio/experimental/iterators/_abstract_iterator.py +246 -26
- ngio/experimental/iterators/_feature.py +84 -41
- ngio/experimental/iterators/_image_processing.py +7 -36
- ngio/experimental/iterators/_mappers.py +48 -0
- ngio/experimental/iterators/_segmentation.py +7 -38
- ngio/images/_abstract_image.py +60 -5
- ngio/images/_image.py +2 -0
- ngio/images/_label.py +2 -0
- ngio/images/_masked_image.py +22 -17
- ngio/io_pipes/__init__.py +29 -3
- ngio/io_pipes/_io_pipes.py +93 -18
- ngio/io_pipes/_io_pipes_masked.py +17 -10
- ngio/io_pipes/_io_pipes_roi.py +10 -1
- ngio/io_pipes/_io_pipes_types.py +56 -0
- ngio/io_pipes/_ops_axes.py +199 -1
- ngio/io_pipes/_ops_slices.py +255 -27
- ngio/io_pipes/_ops_slices_utils.py +196 -0
- ngio/io_pipes/_ops_transforms.py +1 -1
- ngio/io_pipes/_zoom_transform.py +11 -6
- ngio/ome_zarr_meta/__init__.py +0 -2
- ngio/ome_zarr_meta/ngio_specs/__init__.py +0 -2
- ngio/ome_zarr_meta/ngio_specs/_axes.py +7 -131
- ngio/utils/_datasets.py +5 -0
- {ngio-0.4.0a4.dist-info → ngio-0.4.1.dist-info}/METADATA +14 -14
- {ngio-0.4.0a4.dist-info → ngio-0.4.1.dist-info}/RECORD +30 -28
- ngio/io_pipes/_io_pipes_utils.py +0 -299
- {ngio-0.4.0a4.dist-info → ngio-0.4.1.dist-info}/WHEEL +0 -0
- {ngio-0.4.0a4.dist-info → ngio-0.4.1.dist-info}/licenses/LICENSE +0 -0
ngio/io_pipes/_ops_slices.py
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import math
|
|
2
|
+
from collections.abc import Mapping, Sequence
|
|
2
3
|
from typing import TypeAlias, assert_never
|
|
3
4
|
from warnings import warn
|
|
4
5
|
|
|
@@ -7,10 +8,20 @@ import numpy as np
|
|
|
7
8
|
import zarr
|
|
8
9
|
from pydantic import BaseModel, ConfigDict
|
|
9
10
|
|
|
11
|
+
from ngio.common._dimensions import Dimensions
|
|
12
|
+
from ngio.io_pipes._ops_slices_utils import compute_slice_chunks
|
|
13
|
+
from ngio.ome_zarr_meta.ngio_specs import Axis
|
|
10
14
|
from ngio.utils import NgioValueError
|
|
11
15
|
|
|
16
|
+
SlicingInputType: TypeAlias = slice | Sequence[int] | int | None
|
|
12
17
|
SlicingType: TypeAlias = slice | tuple[int, ...] | int
|
|
13
18
|
|
|
19
|
+
##############################################################
|
|
20
|
+
#
|
|
21
|
+
# "SlicingOps" model
|
|
22
|
+
#
|
|
23
|
+
##############################################################
|
|
24
|
+
|
|
14
25
|
|
|
15
26
|
def _int_boundary_check(value: int, shape: int) -> int:
|
|
16
27
|
"""Ensure that the integer value is within the boundaries of the array shape."""
|
|
@@ -63,26 +74,41 @@ class SlicingOps(BaseModel):
|
|
|
63
74
|
|
|
64
75
|
on_disk_axes: tuple[str, ...]
|
|
65
76
|
on_disk_shape: tuple[int, ...]
|
|
66
|
-
|
|
77
|
+
on_disk_chunks: tuple[int, ...]
|
|
78
|
+
slicing_tuple: tuple[SlicingType, ...]
|
|
67
79
|
model_config = ConfigDict(extra="forbid", arbitrary_types_allowed=True)
|
|
68
80
|
|
|
69
81
|
@property
|
|
70
|
-
def normalized_slicing_tuple(self) ->
|
|
82
|
+
def normalized_slicing_tuple(self) -> tuple[SlicingType, ...]:
|
|
71
83
|
"""Normalize the slicing tuple to be within the array shape boundaries."""
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
84
|
+
return _slicing_tuple_boundary_check(
|
|
85
|
+
slicing_tuple=self.slicing_tuple,
|
|
86
|
+
array_shape=self.on_disk_shape,
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
@property
|
|
90
|
+
def slice_axes(self) -> tuple[str, ...]:
|
|
91
|
+
"""The axes after slicing."""
|
|
92
|
+
in_memory_axes = []
|
|
93
|
+
for ax, sl in zip(self.on_disk_axes, self.slicing_tuple, strict=True):
|
|
94
|
+
if isinstance(sl, int):
|
|
95
|
+
continue
|
|
96
|
+
in_memory_axes.append(ax)
|
|
97
|
+
return tuple(in_memory_axes)
|
|
98
|
+
|
|
99
|
+
def slice_chunks(self) -> set[tuple[int, ...]]:
|
|
100
|
+
"""The required to read or write the slice."""
|
|
101
|
+
return compute_slice_chunks(
|
|
102
|
+
shape=self.on_disk_shape,
|
|
103
|
+
chunks=self.on_disk_chunks,
|
|
104
|
+
slicing_tuple=self.normalized_slicing_tuple,
|
|
105
|
+
)
|
|
78
106
|
|
|
79
107
|
def get(self, ax_name: str, normalize: bool = False) -> SlicingType:
|
|
80
108
|
"""Get the slicing tuple."""
|
|
81
109
|
slicing_tuple = (
|
|
82
110
|
self.slicing_tuple if not normalize else self.normalized_slicing_tuple
|
|
83
111
|
)
|
|
84
|
-
if slicing_tuple is None:
|
|
85
|
-
return slice(None)
|
|
86
112
|
if ax_name not in self.on_disk_axes:
|
|
87
113
|
return slice(None)
|
|
88
114
|
ax_index = self.on_disk_axes.index(ax_name)
|
|
@@ -126,12 +152,16 @@ def _check_tuple_in_slicing_tuple(
|
|
|
126
152
|
return ax, first_tuple
|
|
127
153
|
|
|
128
154
|
|
|
155
|
+
##############################################################
|
|
156
|
+
#
|
|
157
|
+
# Slicing implementations
|
|
158
|
+
#
|
|
159
|
+
##############################################################
|
|
160
|
+
|
|
161
|
+
|
|
129
162
|
def get_slice_as_numpy(zarr_array: zarr.Array, slicing_ops: SlicingOps) -> np.ndarray:
|
|
163
|
+
"""Get a slice of a zarr array as a numpy array."""
|
|
130
164
|
slicing_tuple = slicing_ops.normalized_slicing_tuple
|
|
131
|
-
if slicing_tuple is None:
|
|
132
|
-
# Base case, no slicing, return the full array
|
|
133
|
-
return zarr_array[...]
|
|
134
|
-
|
|
135
165
|
# Find if the is any tuple in the slicing tuple
|
|
136
166
|
# If there is one we need to handle it differently
|
|
137
167
|
ax, first_tuple = _check_tuple_in_slicing_tuple(slicing_tuple)
|
|
@@ -149,12 +179,9 @@ def get_slice_as_numpy(zarr_array: zarr.Array, slicing_ops: SlicingOps) -> np.nd
|
|
|
149
179
|
|
|
150
180
|
|
|
151
181
|
def get_slice_as_dask(zarr_array: zarr.Array, slicing_ops: SlicingOps) -> da.Array:
|
|
182
|
+
"""Get a slice of a zarr array as a dask array."""
|
|
152
183
|
da_array = da.from_zarr(zarr_array)
|
|
153
184
|
slicing_tuple = slicing_ops.normalized_slicing_tuple
|
|
154
|
-
if slicing_tuple is None:
|
|
155
|
-
# Base case, no slicing, return the full array
|
|
156
|
-
return da_array[...]
|
|
157
|
-
|
|
158
185
|
# Find if the is any tuple in the slicing tuple
|
|
159
186
|
# If there is one we need to handle it differently
|
|
160
187
|
ax, first_tuple = _check_tuple_in_slicing_tuple(slicing_tuple)
|
|
@@ -177,11 +204,6 @@ def set_slice_as_numpy(
|
|
|
177
204
|
slicing_ops: SlicingOps,
|
|
178
205
|
) -> None:
|
|
179
206
|
slice_tuple = slicing_ops.normalized_slicing_tuple
|
|
180
|
-
if slice_tuple is None:
|
|
181
|
-
# Base case, no slicing, write the full array
|
|
182
|
-
zarr_array[...] = patch
|
|
183
|
-
return
|
|
184
|
-
|
|
185
207
|
ax, first_tuple = _check_tuple_in_slicing_tuple(slice_tuple)
|
|
186
208
|
if ax is None:
|
|
187
209
|
# Base case, no tuple in the slicing tuple
|
|
@@ -195,17 +217,31 @@ def set_slice_as_numpy(
|
|
|
195
217
|
zarr_array[_sub_slice] = np.take(patch, indices=i, axis=ax)
|
|
196
218
|
|
|
197
219
|
|
|
220
|
+
def handle_int_set_as_dask(
|
|
221
|
+
patch: da.Array,
|
|
222
|
+
slicing_tuple: tuple[SlicingType, ...],
|
|
223
|
+
) -> tuple[da.Array, tuple[SlicingType, ...]]:
|
|
224
|
+
"""Handle the case where the slicing tuple contains integers.
|
|
225
|
+
|
|
226
|
+
In this case we need to expand the patch array to match the slicing tuple.
|
|
227
|
+
"""
|
|
228
|
+
new_slicing_tuple = list(slicing_tuple)
|
|
229
|
+
for i, sl in enumerate(slicing_tuple):
|
|
230
|
+
if isinstance(sl, int):
|
|
231
|
+
patch = da.expand_dims(patch, axis=i)
|
|
232
|
+
new_slicing_tuple[i] = slice(sl, sl + 1)
|
|
233
|
+
return patch, tuple(new_slicing_tuple)
|
|
234
|
+
|
|
235
|
+
|
|
198
236
|
def set_slice_as_dask(
|
|
199
237
|
zarr_array: zarr.Array, patch: da.Array, slicing_ops: SlicingOps
|
|
200
238
|
) -> None:
|
|
201
239
|
slice_tuple = slicing_ops.normalized_slicing_tuple
|
|
202
|
-
if slice_tuple is None:
|
|
203
|
-
# Base case, no slicing, write the full array
|
|
204
|
-
da.to_zarr(arr=patch, url=zarr_array)
|
|
205
|
-
return
|
|
206
240
|
ax, first_tuple = _check_tuple_in_slicing_tuple(slice_tuple)
|
|
241
|
+
patch, slice_tuple = handle_int_set_as_dask(patch, slice_tuple)
|
|
207
242
|
if ax is None:
|
|
208
243
|
# Base case, no tuple in the slicing tuple
|
|
244
|
+
# assert False
|
|
209
245
|
da.to_zarr(arr=patch, url=zarr_array, region=slice_tuple)
|
|
210
246
|
return
|
|
211
247
|
|
|
@@ -216,3 +252,195 @@ def set_slice_as_dask(
|
|
|
216
252
|
sub_patch = da.take(patch, indices=i, axis=ax)
|
|
217
253
|
sub_patch = da.expand_dims(sub_patch, axis=ax)
|
|
218
254
|
da.to_zarr(arr=sub_patch, url=zarr_array, region=_sub_slice)
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
##############################################################
|
|
258
|
+
#
|
|
259
|
+
# Builder functions
|
|
260
|
+
#
|
|
261
|
+
##############################################################
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def _try_to_slice(value: Sequence[int]) -> slice | tuple[int, ...]:
|
|
265
|
+
"""Try to convert a list of integers into a slice if they are contiguous.
|
|
266
|
+
|
|
267
|
+
- If the input is empty, return an empty tuple.
|
|
268
|
+
- If the input is sorted, and contains contiguous integers,
|
|
269
|
+
return a slice from the minimum to the maximum integer.
|
|
270
|
+
- Otherwise, return the input as a tuple.
|
|
271
|
+
|
|
272
|
+
This is useful for optimizing array slicing operations
|
|
273
|
+
by allowing the use of slices when possible, which can be more efficient.
|
|
274
|
+
"""
|
|
275
|
+
if not value:
|
|
276
|
+
raise NgioValueError("Ngio does not support empty sequences as slice input.")
|
|
277
|
+
|
|
278
|
+
if not all(isinstance(i, int) for i in value):
|
|
279
|
+
_value = []
|
|
280
|
+
for i in value:
|
|
281
|
+
try:
|
|
282
|
+
_value.append(int(i))
|
|
283
|
+
except Exception as e:
|
|
284
|
+
raise NgioValueError(
|
|
285
|
+
f"Invalid value {i} of type {type(i)} in sequence {value}"
|
|
286
|
+
) from e
|
|
287
|
+
value = _value
|
|
288
|
+
# If the input is not sorted, return it as a tuple
|
|
289
|
+
max_input = max(value)
|
|
290
|
+
min_input = min(value)
|
|
291
|
+
assert min_input >= 0, "Input must contain non-negative integers"
|
|
292
|
+
|
|
293
|
+
if sorted(value) == list(range(min_input, max_input + 1)):
|
|
294
|
+
return slice(min_input, max_input + 1)
|
|
295
|
+
|
|
296
|
+
return tuple(value)
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
def _remove_channel_slicing(
|
|
300
|
+
slicing_dict: dict[str, SlicingInputType],
|
|
301
|
+
dimensions: Dimensions,
|
|
302
|
+
) -> dict[str, SlicingInputType]:
|
|
303
|
+
"""This utility function removes the channel selection from the slice kwargs.
|
|
304
|
+
|
|
305
|
+
if ignore_channel_selection is True, it will remove the channel selection
|
|
306
|
+
regardless of the dimensions. If the ignore_channel_selection is False
|
|
307
|
+
it will fail.
|
|
308
|
+
"""
|
|
309
|
+
if dimensions.is_multi_channels:
|
|
310
|
+
return slicing_dict
|
|
311
|
+
|
|
312
|
+
if "c" in slicing_dict:
|
|
313
|
+
slicing_dict.pop("c", None)
|
|
314
|
+
return slicing_dict
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
def _check_slicing_virtual_axes(slice_: SlicingInputType) -> bool:
|
|
318
|
+
"""Check if the slice_ is compatible with virtual axes.
|
|
319
|
+
|
|
320
|
+
Virtual axes are axes that are not present in the actual data,
|
|
321
|
+
such as time or channel axes in some datasets.
|
|
322
|
+
So the only valid slices for virtual axes are:
|
|
323
|
+
- None: means all data along the axis
|
|
324
|
+
- 0: means the first element along the axis
|
|
325
|
+
- slice([0, None], [1, None])
|
|
326
|
+
"""
|
|
327
|
+
if slice_ is None or slice_ == 0:
|
|
328
|
+
return True
|
|
329
|
+
if isinstance(slice_, slice):
|
|
330
|
+
if slice_.start is None and slice_.stop is None:
|
|
331
|
+
return True
|
|
332
|
+
if slice_.start == 0 and slice_.stop is None:
|
|
333
|
+
return True
|
|
334
|
+
if slice_.start is None and slice_.stop == 0:
|
|
335
|
+
return True
|
|
336
|
+
if slice_.start == 0 and slice_.stop == 1:
|
|
337
|
+
return True
|
|
338
|
+
if isinstance(slice_, Sequence):
|
|
339
|
+
if len(slice_) == 1 and slice_[0] == 0:
|
|
340
|
+
return True
|
|
341
|
+
return False
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
def _clean_slicing_dict(
|
|
345
|
+
dimensions: Dimensions,
|
|
346
|
+
slicing_dict: Mapping[str, SlicingInputType],
|
|
347
|
+
remove_channel_selection: bool = False,
|
|
348
|
+
) -> dict[str, SlicingInputType]:
|
|
349
|
+
"""Clean the slicing dict.
|
|
350
|
+
|
|
351
|
+
This function will:
|
|
352
|
+
- Validate that the axes in the slicing_dict are present in the dimensions.
|
|
353
|
+
- Make sure that the slicing_dict uses the on-disk axis names.
|
|
354
|
+
- Check for duplicate axis names in the slicing_dict.
|
|
355
|
+
- Clean up channel selection if the dimensions
|
|
356
|
+
"""
|
|
357
|
+
clean_slicing_dict: dict[str, SlicingInputType] = {}
|
|
358
|
+
for axis_name, slice_ in slicing_dict.items():
|
|
359
|
+
axis = dimensions.axes_handler.get_axis(axis_name)
|
|
360
|
+
if axis is None:
|
|
361
|
+
# Virtual axes should be allowed to be selected
|
|
362
|
+
# Common use case is still allowing channel_selection
|
|
363
|
+
# When the zarr has not channel axis.
|
|
364
|
+
if not _check_slicing_virtual_axes(slice_):
|
|
365
|
+
raise NgioValueError(
|
|
366
|
+
f"Invalid axis selection:{axis_name}={slice_}. "
|
|
367
|
+
f"Not found on the on-disk axes {dimensions.axes}."
|
|
368
|
+
)
|
|
369
|
+
# Virtual axes can be safely ignored
|
|
370
|
+
continue
|
|
371
|
+
if axis.name in clean_slicing_dict:
|
|
372
|
+
raise NgioValueError(
|
|
373
|
+
f"Duplicate axis {axis.name} in slice kwargs. "
|
|
374
|
+
"Please provide unique axis names."
|
|
375
|
+
)
|
|
376
|
+
clean_slicing_dict[axis.name] = slice_
|
|
377
|
+
|
|
378
|
+
if remove_channel_selection:
|
|
379
|
+
clean_slicing_dict = _remove_channel_slicing(
|
|
380
|
+
slicing_dict=clean_slicing_dict, dimensions=dimensions
|
|
381
|
+
)
|
|
382
|
+
return clean_slicing_dict
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
def _normalize_slicing_tuple(
|
|
386
|
+
axis: Axis,
|
|
387
|
+
slicing_dict: dict[str, SlicingInputType],
|
|
388
|
+
) -> SlicingType:
|
|
389
|
+
"""Normalize the slicing dict to tuple.
|
|
390
|
+
|
|
391
|
+
Since the slicing dict can contain different types of values
|
|
392
|
+
We need to normalize them to more predictable types.
|
|
393
|
+
The output types are:
|
|
394
|
+
- slice
|
|
395
|
+
- int
|
|
396
|
+
- tuple of int (for non-contiguous selection)
|
|
397
|
+
"""
|
|
398
|
+
axis_name = axis.name
|
|
399
|
+
if axis_name not in slicing_dict:
|
|
400
|
+
# If no slice is provided for the axis, use a full slice
|
|
401
|
+
return slice(None)
|
|
402
|
+
|
|
403
|
+
value = slicing_dict[axis_name]
|
|
404
|
+
if value is None:
|
|
405
|
+
return slice(None)
|
|
406
|
+
if isinstance(value, slice) or isinstance(value, int):
|
|
407
|
+
return value
|
|
408
|
+
elif isinstance(value, Sequence):
|
|
409
|
+
# If a contiguous sequence of integers is provided,
|
|
410
|
+
# convert it to a slice for simplicity.
|
|
411
|
+
# Alternatively, it will be converted to a tuple of ints
|
|
412
|
+
return _try_to_slice(value)
|
|
413
|
+
|
|
414
|
+
raise NgioValueError(
|
|
415
|
+
f"Invalid slice definition {value} of type {type(value)}. "
|
|
416
|
+
"Allowed types are: int, slice, sequence of int or None."
|
|
417
|
+
)
|
|
418
|
+
|
|
419
|
+
|
|
420
|
+
def build_slicing_ops(
|
|
421
|
+
*,
|
|
422
|
+
dimensions: Dimensions,
|
|
423
|
+
slicing_dict: dict[str, SlicingInputType] | None,
|
|
424
|
+
remove_channel_selection: bool = False,
|
|
425
|
+
) -> SlicingOps:
|
|
426
|
+
"""Assemble slices to be used to query the array."""
|
|
427
|
+
slicing_dict = slicing_dict or {}
|
|
428
|
+
_slicing_dict = _clean_slicing_dict(
|
|
429
|
+
dimensions=dimensions,
|
|
430
|
+
slicing_dict=slicing_dict,
|
|
431
|
+
remove_channel_selection=remove_channel_selection,
|
|
432
|
+
)
|
|
433
|
+
|
|
434
|
+
slicing_tuple = tuple(
|
|
435
|
+
_normalize_slicing_tuple(
|
|
436
|
+
axis=axis,
|
|
437
|
+
slicing_dict=_slicing_dict,
|
|
438
|
+
)
|
|
439
|
+
for axis in dimensions.axes_handler.axes
|
|
440
|
+
)
|
|
441
|
+
return SlicingOps(
|
|
442
|
+
on_disk_axes=dimensions.axes_handler.axes_names,
|
|
443
|
+
on_disk_shape=dimensions.shape,
|
|
444
|
+
on_disk_chunks=dimensions.chunks,
|
|
445
|
+
slicing_tuple=slicing_tuple,
|
|
446
|
+
)
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
from collections.abc import Iterable, Iterator
|
|
2
|
+
from itertools import product
|
|
3
|
+
from typing import TypeAlias, TypeVar
|
|
4
|
+
|
|
5
|
+
from ngio.utils import NgioValueError, ngio_logger
|
|
6
|
+
|
|
7
|
+
T = TypeVar("T")
|
|
8
|
+
|
|
9
|
+
##############################################################
|
|
10
|
+
#
|
|
11
|
+
# Check slice overlaps
|
|
12
|
+
#
|
|
13
|
+
##############################################################
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _pairs_stream(iterable: Iterable[T]) -> Iterator[tuple[T, T]]:
|
|
17
|
+
# Same as combinations but yields pairs as soon as they are generated
|
|
18
|
+
seen: list[T] = []
|
|
19
|
+
for a in iterable:
|
|
20
|
+
for b in seen:
|
|
21
|
+
yield b, a
|
|
22
|
+
seen.append(a)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
SlicingType: TypeAlias = slice | tuple[int, ...] | int
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def check_elem_intersection(s1: SlicingType, s2: SlicingType) -> bool:
|
|
29
|
+
"""Compare if two SlicingType elements intersect.
|
|
30
|
+
|
|
31
|
+
If they are a slice, check if they overlap.
|
|
32
|
+
If they are integers, check if they are equal.
|
|
33
|
+
If they are tuples, check if they have any common elements.
|
|
34
|
+
"""
|
|
35
|
+
if not isinstance(s1, type(s2)):
|
|
36
|
+
raise NgioValueError(
|
|
37
|
+
f"Slices must be of the same type. Got {type(s1)} and {type(s2)}"
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
if isinstance(s1, slice) and isinstance(s2, slice):
|
|
41
|
+
# Handle slice objects
|
|
42
|
+
start1, stop1, step1 = s1.start or 0, s1.stop or float("inf"), s1.step or 1
|
|
43
|
+
start2, stop2, step2 = s2.start or 0, s2.stop or float("inf"), s2.step or 1
|
|
44
|
+
|
|
45
|
+
if step1 is not None and step2 != 1:
|
|
46
|
+
raise NotImplementedError(
|
|
47
|
+
"Intersection for slices with step != 1 is not implemented"
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
if step2 is not None and step1 != 1:
|
|
51
|
+
raise NotImplementedError(
|
|
52
|
+
"Intersection for slices with step != 1 is not implemented"
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
return not (stop1 <= start2 or stop2 <= start1)
|
|
56
|
+
elif isinstance(s1, int) and isinstance(s2, int):
|
|
57
|
+
# Handle integer indices
|
|
58
|
+
return s1 == s2
|
|
59
|
+
elif isinstance(s1, tuple) and isinstance(s2, tuple):
|
|
60
|
+
if set(s1) & set(s2):
|
|
61
|
+
return True
|
|
62
|
+
return False
|
|
63
|
+
else:
|
|
64
|
+
raise TypeError("Unsupported slice type")
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def check_slicing_tuple_intersection(
|
|
68
|
+
s1: tuple[SlicingType, ...], s2: tuple[SlicingType, ...]
|
|
69
|
+
) -> bool:
|
|
70
|
+
"""For a tuple of SlicingType, check if all elements intersect."""
|
|
71
|
+
if len(s1) != len(s2):
|
|
72
|
+
raise NgioValueError("Slices must have the same length")
|
|
73
|
+
return all(check_elem_intersection(a, b) for a, b in zip(s1, s2, strict=True))
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def check_if_regions_overlap(slices: Iterable[tuple[SlicingType, ...]]) -> bool:
|
|
77
|
+
"""Check for overlaps in a list of slicing tuples using brute-force method.
|
|
78
|
+
|
|
79
|
+
This is O(n^2) and not efficient for large lists.
|
|
80
|
+
Returns True if any overlaps are found.
|
|
81
|
+
"""
|
|
82
|
+
for it, (si, sj) in enumerate(_pairs_stream(slices)):
|
|
83
|
+
overalap = check_slicing_tuple_intersection(si, sj)
|
|
84
|
+
if overalap:
|
|
85
|
+
return True
|
|
86
|
+
|
|
87
|
+
if it == 10_000:
|
|
88
|
+
ngio_logger.warning(
|
|
89
|
+
"Performance Warning check_for_overlaps is O(n^2) and may be slow for "
|
|
90
|
+
"large numbers of regions."
|
|
91
|
+
)
|
|
92
|
+
return False
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
##############################################################
|
|
96
|
+
#
|
|
97
|
+
# Check chunk overlaps
|
|
98
|
+
#
|
|
99
|
+
##############################################################
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _normalize_slice(slc: slice, size: int) -> tuple[int, int]:
|
|
103
|
+
if slc.step not in (None, 1):
|
|
104
|
+
raise NgioValueError(f"Only step=1 slices supported, got step={slc.step}")
|
|
105
|
+
start = 0 if slc.start is None else slc.start
|
|
106
|
+
stop = size if slc.stop is None else slc.stop
|
|
107
|
+
if start < 0 or stop < 0:
|
|
108
|
+
raise NgioValueError("Negative slice bounds are not supported")
|
|
109
|
+
# clamp to [0, size]
|
|
110
|
+
start = min(start, size)
|
|
111
|
+
stop = min(stop, size)
|
|
112
|
+
if start > stop:
|
|
113
|
+
# empty selection
|
|
114
|
+
return (0, 0)
|
|
115
|
+
return start, stop
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _chunk_indices_for_axis(sel: SlicingType, size: int, csize: int) -> list[int]:
|
|
119
|
+
"""From a selection for a single axis, return the list chunk indices touched."""
|
|
120
|
+
if isinstance(sel, slice):
|
|
121
|
+
start, stop = _normalize_slice(sel, size)
|
|
122
|
+
if start >= stop: # empty
|
|
123
|
+
return []
|
|
124
|
+
first = start // csize
|
|
125
|
+
last = (stop - 1) // csize
|
|
126
|
+
return list(range(first, last + 1))
|
|
127
|
+
|
|
128
|
+
if isinstance(sel, int):
|
|
129
|
+
if sel < 0 or sel >= size:
|
|
130
|
+
raise IndexError(f"index {sel} out of bounds for axis of size {size}")
|
|
131
|
+
return [sel // csize]
|
|
132
|
+
|
|
133
|
+
if isinstance(sel, tuple):
|
|
134
|
+
if not sel:
|
|
135
|
+
return []
|
|
136
|
+
chunks_hit = {}
|
|
137
|
+
for v in sel:
|
|
138
|
+
if not isinstance(v, int):
|
|
139
|
+
raise TypeError("Only integers allowed inside tuple selections")
|
|
140
|
+
if v < 0 or v >= size:
|
|
141
|
+
raise IndexError(f"index {v} out of bounds for axis of size {size}")
|
|
142
|
+
chunks_hit[v // csize] = None
|
|
143
|
+
return sorted(chunks_hit.keys())
|
|
144
|
+
|
|
145
|
+
raise TypeError(f"Unsupported index type: {type(sel)!r}")
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def compute_slice_chunks(
|
|
149
|
+
shape: tuple[int, ...],
|
|
150
|
+
chunks: tuple[int, ...],
|
|
151
|
+
slicing_tuple: tuple[SlicingType, ...],
|
|
152
|
+
) -> set[tuple[int, ...]]:
|
|
153
|
+
"""Compute the set of chunk coordinates touched by `slicing_tuple`.
|
|
154
|
+
|
|
155
|
+
Args:
|
|
156
|
+
shape: overall array shape (s1, s2, ...)
|
|
157
|
+
chunks: chunk shape (c1, c2, ...)
|
|
158
|
+
slicing_tuple: tuple of slices, ints, or tuples of ints
|
|
159
|
+
"""
|
|
160
|
+
if len(slicing_tuple) != len(shape):
|
|
161
|
+
raise NgioValueError(
|
|
162
|
+
f"key must have {len(shape)} items, got {len(slicing_tuple)}"
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
per_axis_chunks: list[list[int]] = [
|
|
166
|
+
_chunk_indices_for_axis(sel, size, csize)
|
|
167
|
+
for sel, size, csize in zip(slicing_tuple, shape, chunks, strict=True)
|
|
168
|
+
]
|
|
169
|
+
|
|
170
|
+
# If any axis yields no chunks, the overall selection is empty.
|
|
171
|
+
if any(len(ax) == 0 for ax in per_axis_chunks):
|
|
172
|
+
return set()
|
|
173
|
+
|
|
174
|
+
return {tuple(idx) for idx in product(*per_axis_chunks)}
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def check_if_chunks_overlap(
|
|
178
|
+
slices: Iterable[tuple[SlicingType, ...]],
|
|
179
|
+
shape: tuple[int, ...],
|
|
180
|
+
chunks: tuple[int, ...],
|
|
181
|
+
) -> bool:
|
|
182
|
+
"""Check for overlaps in a list of slicing tuples using brute-force method.
|
|
183
|
+
|
|
184
|
+
This is O(n^2) and not efficient for large lists.
|
|
185
|
+
Returns True if any overlaps are found.
|
|
186
|
+
"""
|
|
187
|
+
slices_chunks = (compute_slice_chunks(shape, chunks, si) for si in slices)
|
|
188
|
+
for it, (si, sj) in enumerate(_pairs_stream(slices_chunks)):
|
|
189
|
+
if si & sj:
|
|
190
|
+
return True
|
|
191
|
+
if it == 10_000:
|
|
192
|
+
ngio_logger.warning(
|
|
193
|
+
"Performance Warning check_for_chunks_overlaps is O(n^2) and may be "
|
|
194
|
+
"slow for large numbers of regions."
|
|
195
|
+
)
|
|
196
|
+
return False
|
ngio/io_pipes/_ops_transforms.py
CHANGED
|
@@ -4,8 +4,8 @@ from typing import Protocol
|
|
|
4
4
|
import dask.array as da
|
|
5
5
|
import numpy as np
|
|
6
6
|
|
|
7
|
+
from ngio.io_pipes._ops_axes import AxesOps
|
|
7
8
|
from ngio.io_pipes._ops_slices import SlicingOps
|
|
8
|
-
from ngio.ome_zarr_meta.ngio_specs._axes import AxesOps
|
|
9
9
|
|
|
10
10
|
|
|
11
11
|
class TransformProtocol(Protocol):
|
ngio/io_pipes/_zoom_transform.py
CHANGED
|
@@ -10,8 +10,8 @@ from ngio.common._zoom import (
|
|
|
10
10
|
dask_zoom,
|
|
11
11
|
numpy_zoom,
|
|
12
12
|
)
|
|
13
|
+
from ngio.io_pipes._ops_axes import AxesOps
|
|
13
14
|
from ngio.io_pipes._ops_slices import SlicingOps
|
|
14
|
-
from ngio.ome_zarr_meta import AxesOps
|
|
15
15
|
|
|
16
16
|
|
|
17
17
|
class BaseZoomTransform:
|
|
@@ -55,12 +55,17 @@ class BaseZoomTransform:
|
|
|
55
55
|
axes_ops: AxesOps,
|
|
56
56
|
slicing_ops: SlicingOps,
|
|
57
57
|
) -> tuple[int, ...]:
|
|
58
|
-
assert len(array_shape) == len(axes_ops.
|
|
58
|
+
assert len(array_shape) == len(axes_ops.output_axes)
|
|
59
59
|
|
|
60
60
|
target_shape = []
|
|
61
|
-
for shape, ax_name in zip(array_shape, axes_ops.
|
|
61
|
+
for shape, ax_name in zip(array_shape, axes_ops.output_axes, strict=True):
|
|
62
62
|
ax_type = self._input_dimensions.axes_handler.get_axis(ax_name)
|
|
63
|
-
if ax_type is
|
|
63
|
+
if ax_type is None:
|
|
64
|
+
# Unknown axis can only be a virtual axis
|
|
65
|
+
# So we set it to 1
|
|
66
|
+
target_shape.append(1)
|
|
67
|
+
continue
|
|
68
|
+
elif ax_type.axis_type == "channel":
|
|
64
69
|
# Do not scale channel axis
|
|
65
70
|
target_shape.append(shape)
|
|
66
71
|
continue
|
|
@@ -81,10 +86,10 @@ class BaseZoomTransform:
|
|
|
81
86
|
axes_ops: AxesOps,
|
|
82
87
|
slicing_ops: SlicingOps,
|
|
83
88
|
) -> tuple[int, ...]:
|
|
84
|
-
assert len(array_shape) == len(axes_ops.
|
|
89
|
+
assert len(array_shape) == len(axes_ops.output_axes)
|
|
85
90
|
|
|
86
91
|
target_shape = []
|
|
87
|
-
for shape, ax_name in zip(array_shape, axes_ops.
|
|
92
|
+
for shape, ax_name in zip(array_shape, axes_ops.output_axes, strict=True):
|
|
88
93
|
ax_type = self._input_dimensions.axes_handler.get_axis(ax_name)
|
|
89
94
|
if ax_type is not None and ax_type.axis_type == "channel":
|
|
90
95
|
# Do not scale channel axis
|
ngio/ome_zarr_meta/__init__.py
CHANGED
|
@@ -14,7 +14,6 @@ from ngio.ome_zarr_meta._meta_handlers import (
|
|
|
14
14
|
)
|
|
15
15
|
from ngio.ome_zarr_meta.ngio_specs import (
|
|
16
16
|
AxesHandler,
|
|
17
|
-
AxesOps,
|
|
18
17
|
Dataset,
|
|
19
18
|
ImageInWellPath,
|
|
20
19
|
NgffVersions,
|
|
@@ -29,7 +28,6 @@ from ngio.ome_zarr_meta.ngio_specs import (
|
|
|
29
28
|
|
|
30
29
|
__all__ = [
|
|
31
30
|
"AxesHandler",
|
|
32
|
-
"AxesOps",
|
|
33
31
|
"Dataset",
|
|
34
32
|
"ImageInWellPath",
|
|
35
33
|
"ImageMetaHandler",
|
|
@@ -8,7 +8,6 @@ This models can be tr
|
|
|
8
8
|
|
|
9
9
|
from ngio.ome_zarr_meta.ngio_specs._axes import (
|
|
10
10
|
AxesHandler,
|
|
11
|
-
AxesOps,
|
|
12
11
|
AxesSetup,
|
|
13
12
|
Axis,
|
|
14
13
|
AxisType,
|
|
@@ -46,7 +45,6 @@ from ngio.ome_zarr_meta.ngio_specs._pixel_size import PixelSize
|
|
|
46
45
|
|
|
47
46
|
__all__ = [
|
|
48
47
|
"AxesHandler",
|
|
49
|
-
"AxesOps",
|
|
50
48
|
"AxesSetup",
|
|
51
49
|
"Axis",
|
|
52
50
|
"AxisType",
|