bioimage_py.wrapper

On-the-fly transformation sources (wrappers).

 1"""On-the-fly transformation sources (wrappers)."""
 2from .affine import AffineSource
 3from .base import (
 4    MultiTransformationSource,
 5    SimpleTransformationSource,
 6    SimpleTransformationWithHaloSource,
 7    TransformationSource,
 8    WrapperSource,
 9    register_wrapper,
10    wrapper_from_spec,
11)
12from .generic import ExpandDimsSource, NormalizeSource, PadSource, RoiSource, ThresholdSource
13from .resize import ResizedSource
14
15__all__ = [
16    "WrapperSource",
17    "SimpleTransformationSource",
18    "SimpleTransformationWithHaloSource",
19    "TransformationSource",
20    "MultiTransformationSource",
21    "ThresholdSource",
22    "NormalizeSource",
23    "RoiSource",
24    "PadSource",
25    "ExpandDimsSource",
26    "AffineSource",
27    "ResizedSource",
28    "register_wrapper",
29    "wrapper_from_spec",
30]
class WrapperSource(bioimage_py.sources.base.Source):
27class WrapperSource(Source):
28    """Base class for wrappers that delegate metadata to the wrapped source.
29
30    Subclasses implement :meth:`__getitem__` (the transform) and :meth:`_params` (the
31    keyword arguments needed to rebuild the wrapper). The output dtype may differ from the
32    wrapped source, so subclasses can override :attr:`dtype`.
33    """
34
35    def __init__(self, source: SourceLike) -> None:
36        self._source = as_source(source)
37
38    @property
39    def source(self) -> Source:
40        """The wrapped source."""
41        return self._source
42
43    @property
44    def _wrapped_sources(self) -> Tuple[Source, ...]:
45        """The wrapped source(s). Single-source wrappers wrap exactly one; override for many."""
46        return (self._source,)
47
48    def _setitem(self, roi: Tuple[slice, ...], value: np.ndarray) -> None:
49        raise TypeError("Wrapper sources are read-only.")
50
51    @property
52    def writable(self) -> bool:
53        """Wrapper sources are read-only."""
54        return False
55
56    @property
57    def shape(self) -> Tuple[int, ...]:
58        return self._source.shape
59
60    @property
61    def dtype(self) -> np.dtype:
62        return self._source.dtype
63
64    @property
65    def chunks(self) -> Optional[Tuple[int, ...]]:
66        return self._source.chunks
67
68    @property
69    def shards(self) -> Optional[Tuple[int, ...]]:
70        return self._source.shards
71
72    def _params(self) -> dict:
73        """Return the keyword arguments needed to reconstruct this wrapper."""
74        return {}
75
76    @classmethod
77    def _from_wrapped(cls, sources: Sequence[Source], params: dict) -> "WrapperSource":
78        """Reconstruct the wrapper from its wrapped source(s) and params.
79
80        The default rebuilds a single-source wrapper as ``cls(source, **params)``. Multi-source
81        wrappers override this to consume all reconstructed sources.
82        """
83        return cls(sources[0], **params)
84
85    def to_spec(self) -> SourceSpec:
86        """Return a spec recording the wrapper class, its params, and the wrapped spec(s)."""
87        params = dict(self._params())
88        params["cls"] = type(self).__name__
89        wrapped_specs = [s.to_spec() for s in self._wrapped_sources]
90        # Single-source wrappers store a single spec; multi-source wrappers store a list.
91        wrapped = wrapped_specs[0] if len(wrapped_specs) == 1 else wrapped_specs
92        return SourceSpec(kind="wrapper", params=params, wrapped=wrapped)

Base class for wrappers that delegate metadata to the wrapped source.

Subclasses implement __getitem__() (the transform) and _params() (the keyword arguments needed to rebuild the wrapper). The output dtype may differ from the wrapped source, so subclasses can override dtype.

source: bioimage_py.sources.Source
38    @property
39    def source(self) -> Source:
40        """The wrapped source."""
41        return self._source

The wrapped source.

writable: bool
51    @property
52    def writable(self) -> bool:
53        """Wrapper sources are read-only."""
54        return False

Wrapper sources are read-only.

shape: Tuple[int, ...]
56    @property
57    def shape(self) -> Tuple[int, ...]:
58        return self._source.shape
dtype: numpy.dtype
60    @property
61    def dtype(self) -> np.dtype:
62        return self._source.dtype
chunks: Optional[Tuple[int, ...]]
64    @property
65    def chunks(self) -> Optional[Tuple[int, ...]]:
66        return self._source.chunks

The chunk shape of the underlying array, or None if unchunked.

shards: Optional[Tuple[int, ...]]
68    @property
69    def shards(self) -> Optional[Tuple[int, ...]]:
70        return self._source.shards

The shard shape of the underlying array, or None if unsharded.

def to_spec(self) -> bioimage_py.sources.SourceSpec:
85    def to_spec(self) -> SourceSpec:
86        """Return a spec recording the wrapper class, its params, and the wrapped spec(s)."""
87        params = dict(self._params())
88        params["cls"] = type(self).__name__
89        wrapped_specs = [s.to_spec() for s in self._wrapped_sources]
90        # Single-source wrappers store a single spec; multi-source wrappers store a list.
91        wrapped = wrapped_specs[0] if len(wrapped_specs) == 1 else wrapped_specs
92        return SourceSpec(kind="wrapper", params=params, wrapped=wrapped)

Return a spec recording the wrapper class, its params, and the wrapped spec(s).

@register_wrapper
class SimpleTransformationSource(bioimage_py.wrapper.WrapperSource):
108@register_wrapper
109class SimpleTransformationSource(WrapperSource):
110    """Apply a value-only transformation to the wrapped source on read.
111
112    The transformation depends only on the data values, not on coordinates, so its signature is
113    ``transformation(block)``. The callable is captured in the spec (it round-trips via cloudpickle),
114    so prefer picklable callables; concrete wrappers (e.g. :class:`ThresholdSource`) instead store
115    simple parameters and rebuild the callable in their constructor.
116
117    Args:
118        source: The wrapped source-like object.
119        transformation: The value transformation to apply to each read block.
120        with_channels: Whether the wrapped source has a leading channel axis. If set, that axis is
121            hidden from this wrapper's shape and passed through to the transformation on read.
122        dtype: The output dtype. Defaults to the wrapped source's dtype.
123    """
124
125    def __init__(self, source: SourceLike, transformation: Callable, *,
126                 with_channels: bool = False, dtype: Optional[np.dtype] = None) -> None:
127        super().__init__(source)
128        if not callable(transformation):
129            raise ValueError("Expect the transformation to be callable.")
130        self._transformation = transformation
131        self._with_channels = bool(with_channels)
132        self._dtype = None if dtype is None else np.dtype(dtype)
133
134    @property
135    def shape(self) -> Tuple[int, ...]:
136        return self._source.shape[1:] if self._with_channels else self._source.shape
137
138    @property
139    def ndim(self) -> int:
140        return self._source.ndim - 1 if self._with_channels else self._source.ndim
141
142    @property
143    def dtype(self) -> np.dtype:
144        return self._dtype if self._dtype is not None else self._source.dtype
145
146    @property
147    def chunks(self) -> Optional[Tuple[int, ...]]:
148        src_chunks = self._source.chunks
149        if src_chunks is None:
150            return None
151        return src_chunks[1:] if self._with_channels else src_chunks
152
153    def _getitem(self, roi: Tuple[slice, ...]) -> np.ndarray:
154        """Return the transformed data at ``roi``."""
155        index = (slice(None),) + tuple(roi) if self._with_channels else roi
156        return self._transformation(self._source[index])
157
158    def _params(self) -> dict:
159        """Return the keyword arguments needed to reconstruct this wrapper."""
160        return {"transformation": self._transformation, "with_channels": self._with_channels,
161                "dtype": self._dtype}

Apply a value-only transformation to the wrapped source on read.

The transformation depends only on the data values, not on coordinates, so its signature is transformation(block). The callable is captured in the spec (it round-trips via cloudpickle), so prefer picklable callables; concrete wrappers (e.g. ThresholdSource) instead store simple parameters and rebuild the callable in their constructor.

Args: source: The wrapped source-like object. transformation: The value transformation to apply to each read block. with_channels: Whether the wrapped source has a leading channel axis. If set, that axis is hidden from this wrapper's shape and passed through to the transformation on read. dtype: The output dtype. Defaults to the wrapped source's dtype.

SimpleTransformationSource( source: 'SourceLike', transformation: Callable, *, with_channels: bool = False, dtype: Optional[numpy.dtype] = None)
125    def __init__(self, source: SourceLike, transformation: Callable, *,
126                 with_channels: bool = False, dtype: Optional[np.dtype] = None) -> None:
127        super().__init__(source)
128        if not callable(transformation):
129            raise ValueError("Expect the transformation to be callable.")
130        self._transformation = transformation
131        self._with_channels = bool(with_channels)
132        self._dtype = None if dtype is None else np.dtype(dtype)
shape: Tuple[int, ...]
134    @property
135    def shape(self) -> Tuple[int, ...]:
136        return self._source.shape[1:] if self._with_channels else self._source.shape
ndim: int
138    @property
139    def ndim(self) -> int:
140        return self._source.ndim - 1 if self._with_channels else self._source.ndim

Number of dimensions.

dtype: numpy.dtype
142    @property
143    def dtype(self) -> np.dtype:
144        return self._dtype if self._dtype is not None else self._source.dtype
chunks: Optional[Tuple[int, ...]]
146    @property
147    def chunks(self) -> Optional[Tuple[int, ...]]:
148        src_chunks = self._source.chunks
149        if src_chunks is None:
150            return None
151        return src_chunks[1:] if self._with_channels else src_chunks

The chunk shape of the underlying array, or None if unchunked.

@register_wrapper
class SimpleTransformationWithHaloSource(bioimage_py.wrapper.SimpleTransformationSource):
164@register_wrapper
165class SimpleTransformationWithHaloSource(SimpleTransformationSource):
166    """Apply a value-only transformation that needs a halo, reading through it on each block.
167
168    Like :class:`SimpleTransformationSource`, but the read region is extended by ``halo`` on each
169    spatial axis (clamped to the data bounds) before the transformation, and the result is cropped
170    back to the requested region. Use this for transformations whose output near a block boundary
171    depends on neighbouring values (e.g. a local filter).
172
173    Args:
174        source: The wrapped source-like object.
175        transformation: The value transformation to apply to each (haloed) read block.
176        halo: The per-axis halo, with one entry per spatial axis.
177        with_channels: Whether the wrapped source has a leading channel axis (see
178            :class:`SimpleTransformationSource`).
179        dtype: The output dtype. Defaults to the wrapped source's dtype.
180    """
181
182    def __init__(self, source: SourceLike, transformation: Callable, halo: Sequence[int], *,
183                 with_channels: bool = False, dtype: Optional[np.dtype] = None) -> None:
184        super().__init__(source, transformation, with_channels=with_channels, dtype=dtype)
185        halo = tuple(int(h) for h in halo)
186        if len(halo) != self.ndim:
187            raise ValueError(f"Expect halo of length {self.ndim}, got {len(halo)}.")
188        self._halo = halo
189
190    def _extend_halo(self, roi: Tuple[slice, ...]) -> Tuple[Tuple[slice, ...], Tuple[slice, ...]]:
191        """Extend ``roi`` by the halo (clamped to bounds) and return it with the local crop."""
192        shape = self.shape
193        extended_index, local_crop = [], []
194        for sl, ha, sh in zip(roi, self._halo, shape):
195            idx_start = int(sl.start) if sl.start is not None else 0
196            idx_stop = int(sl.stop) if sl.stop is not None else sh
197            start = max(idx_start - ha, 0)
198            stop = min(idx_stop + ha, sh)
199            extended_index.append(slice(start, stop))
200            crop_len = stop - start
201            left_halo = idx_start - start
202            right_halo = stop - idx_stop
203            local_crop.append(slice(left_halo, crop_len - right_halo))
204        return tuple(extended_index), tuple(local_crop)
205
206    def _getitem(self, roi: Tuple[slice, ...]) -> np.ndarray:
207        """Return the transformed data at ``roi``, reading through the halo."""
208        index, local_crop = self._extend_halo(roi)
209        if self._with_channels:
210            index = (slice(None),) + index
211            local_crop = (slice(None),) + local_crop
212        out = self._transformation(self._source[index])
213        return out[local_crop]
214
215    def _params(self) -> dict:
216        """Return the keyword arguments needed to reconstruct this wrapper."""
217        params = super()._params()
218        params["halo"] = self._halo
219        return params

Apply a value-only transformation that needs a halo, reading through it on each block.

Like SimpleTransformationSource, but the read region is extended by halo on each spatial axis (clamped to the data bounds) before the transformation, and the result is cropped back to the requested region. Use this for transformations whose output near a block boundary depends on neighbouring values (e.g. a local filter).

Args: source: The wrapped source-like object. transformation: The value transformation to apply to each (haloed) read block. halo: The per-axis halo, with one entry per spatial axis. with_channels: Whether the wrapped source has a leading channel axis (see SimpleTransformationSource). dtype: The output dtype. Defaults to the wrapped source's dtype.

SimpleTransformationWithHaloSource( source: 'SourceLike', transformation: Callable, halo: Sequence[int], *, with_channels: bool = False, dtype: Optional[numpy.dtype] = None)
182    def __init__(self, source: SourceLike, transformation: Callable, halo: Sequence[int], *,
183                 with_channels: bool = False, dtype: Optional[np.dtype] = None) -> None:
184        super().__init__(source, transformation, with_channels=with_channels, dtype=dtype)
185        halo = tuple(int(h) for h in halo)
186        if len(halo) != self.ndim:
187            raise ValueError(f"Expect halo of length {self.ndim}, got {len(halo)}.")
188        self._halo = halo
@register_wrapper
class TransformationSource(bioimage_py.wrapper.WrapperSource):
222@register_wrapper
223class TransformationSource(WrapperSource):
224    """Apply a coordinate-aware transformation to the wrapped source on read.
225
226    The transformation may depend on both the data values and the read coordinates, so its
227    signature is ``transformation(block, roi)`` where ``roi`` is the tuple of slices being read.
228
229    Args:
230        source: The wrapped source-like object.
231        transformation: The transformation to apply, called as ``transformation(block, roi)``.
232        dtype: The output dtype. Defaults to the wrapped source's dtype.
233    """
234
235    def __init__(self, source: SourceLike, transformation: Callable, *,
236                 dtype: Optional[np.dtype] = None) -> None:
237        super().__init__(source)
238        if not callable(transformation):
239            raise ValueError("Expect the transformation to be callable.")
240        self._transformation = transformation
241        self._dtype = None if dtype is None else np.dtype(dtype)
242
243    @property
244    def dtype(self) -> np.dtype:
245        return self._dtype if self._dtype is not None else self._source.dtype
246
247    def _getitem(self, roi: Tuple[slice, ...]) -> np.ndarray:
248        """Return the transformed data at ``roi`` (a full tuple of concrete slices)."""
249        return self._transformation(self._source[roi], roi)
250
251    def _params(self) -> dict:
252        """Return the keyword arguments needed to reconstruct this wrapper."""
253        return {"transformation": self._transformation, "dtype": self._dtype}

Apply a coordinate-aware transformation to the wrapped source on read.

The transformation may depend on both the data values and the read coordinates, so its signature is transformation(block, roi) where roi is the tuple of slices being read.

Args: source: The wrapped source-like object. transformation: The transformation to apply, called as transformation(block, roi). dtype: The output dtype. Defaults to the wrapped source's dtype.

TransformationSource( source: 'SourceLike', transformation: Callable, *, dtype: Optional[numpy.dtype] = None)
235    def __init__(self, source: SourceLike, transformation: Callable, *,
236                 dtype: Optional[np.dtype] = None) -> None:
237        super().__init__(source)
238        if not callable(transformation):
239            raise ValueError("Expect the transformation to be callable.")
240        self._transformation = transformation
241        self._dtype = None if dtype is None else np.dtype(dtype)
dtype: numpy.dtype
243    @property
244    def dtype(self) -> np.dtype:
245        return self._dtype if self._dtype is not None else self._source.dtype
@register_wrapper
class MultiTransformationSource(bioimage_py.wrapper.WrapperSource):
256@register_wrapper
257class MultiTransformationSource(WrapperSource):
258    """Apply a transformation jointly to blocks read from several equally-shaped sources.
259
260    The transformation depends only on the data values, so its signature is
261    ``transformation(*blocks)`` (or ``transformation(blocks)`` when ``apply_to_list`` is set). All
262    wrapped sources must have the same shape.
263
264    Args:
265        transformation: The transformation to apply to the per-source blocks.
266        sources: The wrapped source-like objects (at least one).
267        apply_to_list: Whether the blocks are passed as a single list argument instead of being
268            splatted as individual arguments.
269        dtype: The output dtype. Defaults to the first source's dtype.
270    """
271
272    def __init__(self, transformation: Callable, *sources: SourceLike,
273                 apply_to_list: bool = False, dtype: Optional[np.dtype] = None) -> None:
274        if not callable(transformation):
275            raise ValueError("Expect the transformation to be callable.")
276        if len(sources) == 0:
277            raise ValueError("Expect at least one source.")
278        self._sources: List[Source] = [as_source(s) for s in sources]
279        if any(s.shape != self._sources[0].shape for s in self._sources[1:]):
280            raise ValueError("All sources must have the same shape.")
281        # The first source backs the inherited metadata (shape / chunks / shards).
282        super().__init__(self._sources[0])
283        self._transformation = transformation
284        self._apply_to_list = bool(apply_to_list)
285        self._dtype = None if dtype is None else np.dtype(dtype)
286
287    @property
288    def _wrapped_sources(self) -> Tuple[Source, ...]:
289        return tuple(self._sources)
290
291    @property
292    def dtype(self) -> np.dtype:
293        return self._dtype if self._dtype is not None else self._sources[0].dtype
294
295    def _getitem(self, roi: Tuple[slice, ...]) -> np.ndarray:
296        """Return the joint transformation of all sources at ``roi``."""
297        inputs = [s[roi] for s in self._sources]
298        return self._transformation(inputs) if self._apply_to_list else self._transformation(*inputs)
299
300    def _params(self) -> dict:
301        """Return the keyword arguments needed to reconstruct this wrapper."""
302        return {"transformation": self._transformation, "apply_to_list": self._apply_to_list,
303                "dtype": self._dtype}
304
305    @classmethod
306    def _from_wrapped(cls, sources: Sequence[Source], params: dict) -> "MultiTransformationSource":
307        """Rebuild from all wrapped sources (transformation comes first, positionally)."""
308        params = dict(params)
309        transformation = params.pop("transformation")
310        return cls(transformation, *sources, **params)

Apply a transformation jointly to blocks read from several equally-shaped sources.

The transformation depends only on the data values, so its signature is transformation(*blocks) (or transformation(blocks) when apply_to_list is set). All wrapped sources must have the same shape.

Args: transformation: The transformation to apply to the per-source blocks. sources: The wrapped source-like objects (at least one). apply_to_list: Whether the blocks are passed as a single list argument instead of being splatted as individual arguments. dtype: The output dtype. Defaults to the first source's dtype.

MultiTransformationSource( transformation: Callable, *sources: 'SourceLike', apply_to_list: bool = False, dtype: Optional[numpy.dtype] = None)
272    def __init__(self, transformation: Callable, *sources: SourceLike,
273                 apply_to_list: bool = False, dtype: Optional[np.dtype] = None) -> None:
274        if not callable(transformation):
275            raise ValueError("Expect the transformation to be callable.")
276        if len(sources) == 0:
277            raise ValueError("Expect at least one source.")
278        self._sources: List[Source] = [as_source(s) for s in sources]
279        if any(s.shape != self._sources[0].shape for s in self._sources[1:]):
280            raise ValueError("All sources must have the same shape.")
281        # The first source backs the inherited metadata (shape / chunks / shards).
282        super().__init__(self._sources[0])
283        self._transformation = transformation
284        self._apply_to_list = bool(apply_to_list)
285        self._dtype = None if dtype is None else np.dtype(dtype)
dtype: numpy.dtype
291    @property
292    def dtype(self) -> np.dtype:
293        return self._dtype if self._dtype is not None else self._sources[0].dtype
@register_wrapper
class ThresholdSource(bioimage_py.wrapper.SimpleTransformationSource):
13@register_wrapper
14class ThresholdSource(SimpleTransformationSource):
15    """Threshold the wrapped source on read.
16
17    ``operator(source[roi], threshold)`` is returned as a boolean array (``operator`` defaults to
18    ``numpy.greater``, i.e. ``source[roi] > threshold``).
19
20    Args:
21        source: The wrapped source-like object.
22        threshold: The threshold value.
23        operator: The comparison operator applied as ``operator(block, threshold)``.
24    """
25
26    def __init__(self, source: SourceLike, threshold: float, operator: callable = np.greater) -> None:
27        self._threshold = float(threshold)
28        self._operator = operator
29        super().__init__(source, lambda block: operator(block, self._threshold), dtype=np.dtype(bool))
30
31    @property
32    def threshold(self) -> float:
33        """The threshold value."""
34        return self._threshold
35
36    def _params(self) -> dict:
37        """Return the keyword arguments needed to reconstruct this wrapper."""
38        return {"threshold": self._threshold, "operator": self._operator}

Threshold the wrapped source on read.

operator(source[roi], threshold) is returned as a boolean array (operator defaults to numpy.greater, i.e. source[roi] > threshold).

Args: source: The wrapped source-like object. threshold: The threshold value. operator: The comparison operator applied as operator(block, threshold).

ThresholdSource( source: 'SourceLike', threshold: float, operator: <built-in function callable> = <ufunc 'greater'>)
26    def __init__(self, source: SourceLike, threshold: float, operator: callable = np.greater) -> None:
27        self._threshold = float(threshold)
28        self._operator = operator
29        super().__init__(source, lambda block: operator(block, self._threshold), dtype=np.dtype(bool))
threshold: float
31    @property
32    def threshold(self) -> float:
33        """The threshold value."""
34        return self._threshold

The threshold value.

@register_wrapper
class NormalizeSource(bioimage_py.wrapper.SimpleTransformationSource):
41@register_wrapper
42class NormalizeSource(SimpleTransformationSource):
43    """Normalize the wrapped source to ``[0, 1]`` on read.
44
45    Each read block is independently normalized by its own min and max. Note that this means a
46    block-wise read is *not* equivalent to a single whole-array read (the normalization is
47    block-local), matching elf's ``NormalizeWrapper``.
48
49    Args:
50        source: The wrapped source-like object.
51        dtype: The output (floating point) dtype.
52        with_channels: Whether the wrapped source has a leading channel axis.
53    """
54
55    eps = 1.0e-6
56
57    def __init__(self, source: SourceLike, dtype: str = "float32", with_channels: bool = False) -> None:
58        self._norm_dtype = np.dtype(dtype)
59        super().__init__(source, self._normalize, dtype=self._norm_dtype, with_channels=with_channels)
60
61    def _normalize(self, block: np.ndarray) -> np.ndarray:
62        """Normalize a block to ``[0, 1]`` using its own min and max."""
63        block = block.astype(self._norm_dtype)
64        block -= block.min()
65        block /= (block.max() + self.eps)
66        return block
67
68    def _params(self) -> dict:
69        """Return the keyword arguments needed to reconstruct this wrapper."""
70        return {"dtype": str(self._norm_dtype), "with_channels": self._with_channels}

Normalize the wrapped source to [0, 1] on read.

Each read block is independently normalized by its own min and max. Note that this means a block-wise read is not equivalent to a single whole-array read (the normalization is block-local), matching elf's NormalizeWrapper.

Args: source: The wrapped source-like object. dtype: The output (floating point) dtype. with_channels: Whether the wrapped source has a leading channel axis.

NormalizeSource( source: 'SourceLike', dtype: str = 'float32', with_channels: bool = False)
57    def __init__(self, source: SourceLike, dtype: str = "float32", with_channels: bool = False) -> None:
58        self._norm_dtype = np.dtype(dtype)
59        super().__init__(source, self._normalize, dtype=self._norm_dtype, with_channels=with_channels)
eps = 1e-06
@register_wrapper
class RoiSource(bioimage_py.wrapper.WrapperSource):
108@register_wrapper
109class RoiSource(WrapperSource):
110    """Restrict the wrapped source to a region of interest.
111
112    This is a read-and-write view: reads and writes are offset into the parent source, so writing
113    through a :class:`RoiSource` updates the parent (when the parent is writable). For block-wise
114    *distributed* output, the ROI offset must be chunk-aligned in the parent, otherwise concurrent
115    writes to a shared parent chunk can corrupt it (the runner aligns blocks to this wrapper's
116    shape, not the parent's). For an unaligned or read-only ROI, use it purely as an input view.
117
118    Args:
119        source: The wrapped source-like object.
120        roi: The region of interest as a tuple of ints / slices over the source. Integer entries
121            select a single index along that axis; missing trailing axes default to the full extent.
122        squeeze: Whether to drop axes selected by an integer entry from the wrapper's shape and
123            output (and require matching input on write). Defaults to False.
124    """
125
126    def __init__(self, source: SourceLike, roi: Sequence[Union[int, slice]], squeeze: bool = False) -> None:
127        super().__init__(source)
128        self._orig_roi = tuple(roi)
129        self._squeeze = bool(squeeze)
130        self._roi, roi_squeeze = _normalize_construction_roi(self._orig_roi, self._source.shape)
131        # Axes introduced as singletons by an integer roi entry; only dropped when squeeze is set.
132        self._squeeze_axes = roi_squeeze if self._squeeze else ()
133        self._kept_axes = tuple(ax for ax in range(len(self._roi)) if ax not in self._squeeze_axes)
134
135    @property
136    def shape(self) -> Tuple[int, ...]:
137        return tuple(self._roi[ax].stop - self._roi[ax].start for ax in self._kept_axes)
138
139    @property
140    def ndim(self) -> int:
141        return len(self._kept_axes)
142
143    @property
144    def chunks(self) -> Optional[Tuple[int, ...]]:
145        src_chunks = self._source.chunks
146        if src_chunks is None:
147            return None
148        return tuple(min(src_chunks[ax], self._roi[ax].stop - self._roi[ax].start) for ax in self._kept_axes)
149
150    @property
151    def shards(self) -> Optional[Tuple[int, ...]]:
152        # A sub-region does not in general share the parent's shard grid.
153        return None
154
155    @property
156    def writable(self) -> bool:
157        """A roi view is writable when its parent is."""
158        return self._source.writable
159
160    def _map_roi_to_source(self, roi: Tuple[slice, ...]) -> Tuple[slice, ...]:
161        """Map a roi over the kept (reduced) axes into a full index of the parent source."""
162        full_index = list(self._roi)  # Default to the construction roi (singleton on squeezed axes).
163        for sl, ax in zip(roi, self._kept_axes):
164            offset = self._roi[ax].start
165            start = (int(sl.start) if sl.start is not None else 0) + offset
166            stop = (int(sl.stop) if sl.stop is not None else (self._roi[ax].stop - offset)) + offset
167            full_index[ax] = slice(start, stop)
168        return tuple(full_index)
169
170    def _getitem(self, roi: Tuple[slice, ...]) -> np.ndarray:
171        """Return the parent data for the (reduced) region ``roi``."""
172        out = self._source[self._map_roi_to_source(roi)]
173        if self._squeeze_axes:
174            out = np.squeeze(out, axis=self._squeeze_axes)
175        return out
176
177    def _setitem(self, roi: Tuple[slice, ...], value: np.ndarray) -> None:
178        """Write ``value`` into the corresponding region of the parent source."""
179        value = np.asarray(value)
180        if self._squeeze_axes:
181            value = np.expand_dims(value, axis=self._squeeze_axes)
182        self._source[self._map_roi_to_source(roi)] = value
183
184    def _params(self) -> dict:
185        """Return the keyword arguments needed to reconstruct this wrapper."""
186        return {"roi": self._orig_roi, "squeeze": self._squeeze}

Restrict the wrapped source to a region of interest.

This is a read-and-write view: reads and writes are offset into the parent source, so writing through a RoiSource updates the parent (when the parent is writable). For block-wise distributed output, the ROI offset must be chunk-aligned in the parent, otherwise concurrent writes to a shared parent chunk can corrupt it (the runner aligns blocks to this wrapper's shape, not the parent's). For an unaligned or read-only ROI, use it purely as an input view.

Args: source: The wrapped source-like object. roi: The region of interest as a tuple of ints / slices over the source. Integer entries select a single index along that axis; missing trailing axes default to the full extent. squeeze: Whether to drop axes selected by an integer entry from the wrapper's shape and output (and require matching input on write). Defaults to False.

RoiSource( source: 'SourceLike', roi: Sequence[Union[int, slice]], squeeze: bool = False)
126    def __init__(self, source: SourceLike, roi: Sequence[Union[int, slice]], squeeze: bool = False) -> None:
127        super().__init__(source)
128        self._orig_roi = tuple(roi)
129        self._squeeze = bool(squeeze)
130        self._roi, roi_squeeze = _normalize_construction_roi(self._orig_roi, self._source.shape)
131        # Axes introduced as singletons by an integer roi entry; only dropped when squeeze is set.
132        self._squeeze_axes = roi_squeeze if self._squeeze else ()
133        self._kept_axes = tuple(ax for ax in range(len(self._roi)) if ax not in self._squeeze_axes)
shape: Tuple[int, ...]
135    @property
136    def shape(self) -> Tuple[int, ...]:
137        return tuple(self._roi[ax].stop - self._roi[ax].start for ax in self._kept_axes)
ndim: int
139    @property
140    def ndim(self) -> int:
141        return len(self._kept_axes)

Number of dimensions.

chunks: Optional[Tuple[int, ...]]
143    @property
144    def chunks(self) -> Optional[Tuple[int, ...]]:
145        src_chunks = self._source.chunks
146        if src_chunks is None:
147            return None
148        return tuple(min(src_chunks[ax], self._roi[ax].stop - self._roi[ax].start) for ax in self._kept_axes)

The chunk shape of the underlying array, or None if unchunked.

shards: Optional[Tuple[int, ...]]
150    @property
151    def shards(self) -> Optional[Tuple[int, ...]]:
152        # A sub-region does not in general share the parent's shard grid.
153        return None

The shard shape of the underlying array, or None if unsharded.

writable: bool
155    @property
156    def writable(self) -> bool:
157        """A roi view is writable when its parent is."""
158        return self._source.writable

A roi view is writable when its parent is.

@register_wrapper
class PadSource(bioimage_py.wrapper.WrapperSource):
189@register_wrapper
190class PadSource(WrapperSource):
191    """Right-pad the wrapped source on read.
192
193    The wrapper's shape is the source shape grown by ``pad_width`` per axis. Reads that extend past
194    the source are filled by :func:`numpy.pad`. Only right-padding is supported (the source occupies
195    the lower corner of the padded space). This is a read-only view.
196
197    Args:
198        source: The wrapped source-like object.
199        pad_width: The number of elements to append along each axis.
200        mode: The padding mode passed to :func:`numpy.pad`.
201    """
202
203    def __init__(self, source: SourceLike, pad_width: Sequence[int], mode: str = "constant") -> None:
204        super().__init__(source)
205        if len(pad_width) != self._source.ndim:
206            raise ValueError(f"Expect pad_width of length {self._source.ndim}, got {len(pad_width)}.")
207        self._pad_width = tuple(int(p) for p in pad_width)
208        self._src_shape = self._source.shape
209        self._mode = mode
210
211    @property
212    def shape(self) -> Tuple[int, ...]:
213        return tuple(sh + pw for sh, pw in zip(self._src_shape, self._pad_width))
214
215    def _getitem(self, roi: Tuple[slice, ...]) -> np.ndarray:
216        """Return the (possibly right-padded) data at ``roi``."""
217        local_pad, local_index = [], []
218        for sl, sh, psh in zip(roi, self._src_shape, self.shape):
219            start = int(sl.start) if sl.start is not None else 0
220            stop = int(sl.stop) if sl.stop is not None else psh
221            overhang_start = max(0, start - sh)
222            overhang_stop = max(0, stop - sh)
223            if overhang_start > 0:
224                raise NotImplementedError("PadSource only supports right-padding.")
225            elif overhang_stop > 0:
226                local_pad.append(overhang_stop)
227                local_index.append(slice(start, sh))
228            else:
229                local_pad.append(0)
230                local_index.append(slice(start, stop))
231
232        out = self._source[tuple(local_index)]
233        if any(lpad > 0 for lpad in local_pad):
234            pad_width = tuple((0, lpad) for lpad in local_pad)
235            out = np.pad(out, pad_width, mode=self._mode)
236        return out
237
238    def _params(self) -> dict:
239        """Return the keyword arguments needed to reconstruct this wrapper."""
240        return {"pad_width": self._pad_width, "mode": self._mode}

Right-pad the wrapped source on read.

The wrapper's shape is the source shape grown by pad_width per axis. Reads that extend past the source are filled by numpy.pad(). Only right-padding is supported (the source occupies the lower corner of the padded space). This is a read-only view.

Args: source: The wrapped source-like object. pad_width: The number of elements to append along each axis. mode: The padding mode passed to numpy.pad().

PadSource( source: 'SourceLike', pad_width: Sequence[int], mode: str = 'constant')
203    def __init__(self, source: SourceLike, pad_width: Sequence[int], mode: str = "constant") -> None:
204        super().__init__(source)
205        if len(pad_width) != self._source.ndim:
206            raise ValueError(f"Expect pad_width of length {self._source.ndim}, got {len(pad_width)}.")
207        self._pad_width = tuple(int(p) for p in pad_width)
208        self._src_shape = self._source.shape
209        self._mode = mode
shape: Tuple[int, ...]
211    @property
212    def shape(self) -> Tuple[int, ...]:
213        return tuple(sh + pw for sh, pw in zip(self._src_shape, self._pad_width))
@register_wrapper
class ExpandDimsSource(bioimage_py.wrapper.WrapperSource):
243@register_wrapper
244class ExpandDimsSource(WrapperSource):
245    """Insert one or more singleton axes into the wrapped source on read.
246
247    The inverse of a squeezing :class:`RoiSource`: the wrapper presents the wrapped source with
248    extra length-1 axes inserted at ``axis``, following :func:`numpy.expand_dims` semantics (``axis``
249    refers to positions in the *expanded* result). Use this to promote e.g. a 2d image to a 3d
250    volume ``(1, y, x)`` without materializing a copy. This is a read-only view.
251
252    Args:
253        source: The wrapped source-like object.
254        axis: Position(s) in the expanded result at which to insert a new singleton axis. May be a
255            single int or a sequence of ints; negative values count from the end.
256    """
257
258    def __init__(self, source: SourceLike, axis: Union[int, Sequence[int]] = 0) -> None:
259        super().__init__(source)
260        self._axis = axis
261        axes = (axis,) if isinstance(axis, (int, np.integer)) else tuple(int(a) for a in axis)
262        out_ndim = self._source.ndim + len(axes)
263        normalized = []
264        for a in axes:
265            a = int(a)
266            if a < 0:
267                a += out_ndim
268            if not 0 <= a < out_ndim:
269                raise ValueError(f"axis {axis} is out of bounds for expanded ndim {out_ndim}.")
270            normalized.append(a)
271        if len(set(normalized)) != len(normalized):
272            raise ValueError(f"repeated axis in {axis}.")
273        self._new_axes = tuple(sorted(normalized))
274        # Axes of the expanded shape that map back to the wrapped source (in order).
275        self._kept_axes = tuple(ax for ax in range(out_ndim) if ax not in self._new_axes)
276
277    @property
278    def ndim(self) -> int:
279        return self._source.ndim + len(self._new_axes)
280
281    @property
282    def shape(self) -> Tuple[int, ...]:
283        out = [1] * self.ndim
284        for ax, sh in zip(self._kept_axes, self._source.shape):
285            out[ax] = sh
286        return tuple(out)
287
288    @property
289    def chunks(self) -> Optional[Tuple[int, ...]]:
290        src_chunks = self._source.chunks
291        if src_chunks is None:
292            return None
293        out = [1] * self.ndim
294        for ax, c in zip(self._kept_axes, src_chunks):
295            out[ax] = c
296        return tuple(out)
297
298    @property
299    def shards(self) -> Optional[Tuple[int, ...]]:
300        # Inserting singleton axes does not in general preserve the parent's shard grid.
301        return None
302
303    def _getitem(self, roi: Tuple[slice, ...]) -> np.ndarray:
304        """Return the wrapped data at ``roi`` with the singleton axes inserted.
305
306        ``roi`` is a full tuple of slices over the expanded shape (the inserted axes are
307        ``slice(0, 1)``); read the wrapped source over the kept axes, then re-insert the axes.
308        """
309        src_index = tuple(roi[ax] for ax in self._kept_axes)
310        return np.expand_dims(self._source[src_index], self._new_axes)
311
312    def _params(self) -> dict:
313        """Return the keyword arguments needed to reconstruct this wrapper."""
314        return {"axis": self._axis}

Insert one or more singleton axes into the wrapped source on read.

The inverse of a squeezing RoiSource: the wrapper presents the wrapped source with extra length-1 axes inserted at axis, following numpy.expand_dims() semantics (axis refers to positions in the expanded result). Use this to promote e.g. a 2d image to a 3d volume (1, y, x) without materializing a copy. This is a read-only view.

Args: source: The wrapped source-like object. axis: Position(s) in the expanded result at which to insert a new singleton axis. May be a single int or a sequence of ints; negative values count from the end.

ExpandDimsSource(source: 'SourceLike', axis: Union[int, Sequence[int]] = 0)
258    def __init__(self, source: SourceLike, axis: Union[int, Sequence[int]] = 0) -> None:
259        super().__init__(source)
260        self._axis = axis
261        axes = (axis,) if isinstance(axis, (int, np.integer)) else tuple(int(a) for a in axis)
262        out_ndim = self._source.ndim + len(axes)
263        normalized = []
264        for a in axes:
265            a = int(a)
266            if a < 0:
267                a += out_ndim
268            if not 0 <= a < out_ndim:
269                raise ValueError(f"axis {axis} is out of bounds for expanded ndim {out_ndim}.")
270            normalized.append(a)
271        if len(set(normalized)) != len(normalized):
272            raise ValueError(f"repeated axis in {axis}.")
273        self._new_axes = tuple(sorted(normalized))
274        # Axes of the expanded shape that map back to the wrapped source (in order).
275        self._kept_axes = tuple(ax for ax in range(out_ndim) if ax not in self._new_axes)
ndim: int
277    @property
278    def ndim(self) -> int:
279        return self._source.ndim + len(self._new_axes)

Number of dimensions.

shape: Tuple[int, ...]
281    @property
282    def shape(self) -> Tuple[int, ...]:
283        out = [1] * self.ndim
284        for ax, sh in zip(self._kept_axes, self._source.shape):
285            out[ax] = sh
286        return tuple(out)
chunks: Optional[Tuple[int, ...]]
288    @property
289    def chunks(self) -> Optional[Tuple[int, ...]]:
290        src_chunks = self._source.chunks
291        if src_chunks is None:
292            return None
293        out = [1] * self.ndim
294        for ax, c in zip(self._kept_axes, src_chunks):
295            out[ax] = c
296        return tuple(out)

The chunk shape of the underlying array, or None if unchunked.

shards: Optional[Tuple[int, ...]]
298    @property
299    def shards(self) -> Optional[Tuple[int, ...]]:
300        # Inserting singleton axes does not in general preserve the parent's shard grid.
301        return None

The shard shape of the underlying array, or None if unsharded.

@register_wrapper
class AffineSource(bioimage_py.wrapper.WrapperSource):
 24@register_wrapper
 25class AffineSource(WrapperSource):
 26    """Apply an affine transformation to the wrapped source on read.
 27
 28    The transformation is given either by ``affine_matrix`` or by individual parameters for
 29    ``scale`` / ``rotation`` / ``shear`` / ``translation`` (exactly one of the two ways).
 30
 31    Args:
 32        source: The wrapped source-like object (2D or 3D).
 33        shape: The output shape. Defaults to the wrapped source's shape.
 34        affine_matrix: The matrix defining the affine transformation, mapping output to input
 35            coordinates, of shape ``(ndim + 1, ndim + 1)``.
 36        scale: The scale factors (used when ``affine_matrix`` is not given).
 37        rotation: The rotation angles in degrees (used when ``affine_matrix`` is not given).
 38        shear: The shear angles in degrees (not implemented correctly yet; leave unset).
 39        translation: The translation vector (used when ``affine_matrix`` is not given).
 40        order: The interpolation order, supports orders 0 to 5. Use ``0`` (nearest) for label data.
 41        fill_value: The value used for output coordinates that map outside the input.
 42        anti_aliasing: Whether to Gaussian pre-smooth the input before sampling to avoid aliasing
 43            when downsampling. Recommended for intensity data; leave ``False`` for labels.
 44    """
 45
 46    def __init__(
 47        self,
 48        source: SourceLike,
 49        shape: Optional[Tuple[int, ...]] = None,
 50        *,
 51        affine_matrix: Optional[np.ndarray] = None,
 52        scale: Optional[List[float]] = None,
 53        rotation: Optional[List[float]] = None,
 54        shear: Optional[List[float]] = None,
 55        translation: Optional[List[float]] = None,
 56        order: int = 0,
 57        fill_value: Number = 0,
 58        anti_aliasing: bool = False,
 59    ) -> None:
 60        super().__init__(source)
 61        ndim = self._source.ndim
 62        if ndim not in (2, 3):
 63            raise ValueError(f"AffineSource supports 2d or 3d data, got {ndim}d.")
 64        if not 0 <= int(order) <= 5:
 65            raise ValueError(f"order must be in [0, 5], got {order}.")
 66
 67        have_matrix = affine_matrix is not None
 68        have_parameter = any(p is not None for p in (scale, rotation, shear, translation))
 69        if have_matrix == have_parameter:
 70            raise ValueError("Pass exactly one of affine_matrix or the scale/rotation/shear/translation parameters.")
 71
 72        if have_matrix:
 73            matrix = np.asarray(affine_matrix, dtype="float64")
 74        else:
 75            matrix = compute_affine_matrix(scale, rotation, shear, translation)
 76        if matrix.shape != (ndim + 1, ndim + 1):
 77            raise ValueError(f"Invalid affine matrix shape {matrix.shape}, expected {(ndim + 1, ndim + 1)}.")
 78
 79        self._matrix = matrix
 80        self._shape = self._source.shape if shape is None else tuple(int(s) for s in shape)
 81        self._order = int(order)
 82        self._fill_value = fill_value
 83        self._anti_aliasing = bool(anti_aliasing)
 84
 85    @property
 86    def shape(self) -> Tuple[int, ...]:
 87        """The output (transformed) shape."""
 88        return self._shape
 89
 90    @property
 91    def matrix(self) -> np.ndarray:
 92        """The affine matrix mapping output to input coordinates."""
 93        return self._matrix
 94
 95    @property
 96    def shards(self) -> Optional[Tuple[int, ...]]:
 97        # An affine of the input does not map onto the input's shard grid.
 98        return None
 99
100    def _getitem(self, roi: Tuple[slice, ...]) -> np.ndarray:
101        """Return the affine-transformed data for the output region ``roi``."""
102        sigma = None
103        if self._anti_aliasing:
104            aa = np.asarray(
105                bic.transformation.compute_anti_aliasing_sigma(self._matrix, self._source.ndim),
106                dtype="float64",
107            )
108            if np.any(aa > 0):
109                sigma = aa
110        return transform_subvolume_affine(
111            self._source, self._matrix, roi, order=self._order,
112            fill_value=self._fill_value, sigma=sigma,
113        )
114
115    def _params(self) -> dict:
116        """Return the keyword arguments needed to reconstruct this wrapper."""
117        return {
118            "shape": self._shape,
119            "affine_matrix": self._matrix.tolist(),
120            "order": self._order,
121            "fill_value": self._fill_value,
122            "anti_aliasing": self._anti_aliasing,
123        }

Apply an affine transformation to the wrapped source on read.

The transformation is given either by affine_matrix or by individual parameters for scale / rotation / shear / translation (exactly one of the two ways).

Args: source: The wrapped source-like object (2D or 3D). shape: The output shape. Defaults to the wrapped source's shape. affine_matrix: The matrix defining the affine transformation, mapping output to input coordinates, of shape (ndim + 1, ndim + 1). scale: The scale factors (used when affine_matrix is not given). rotation: The rotation angles in degrees (used when affine_matrix is not given). shear: The shear angles in degrees (not implemented correctly yet; leave unset). translation: The translation vector (used when affine_matrix is not given). order: The interpolation order, supports orders 0 to 5. Use 0 (nearest) for label data. fill_value: The value used for output coordinates that map outside the input. anti_aliasing: Whether to Gaussian pre-smooth the input before sampling to avoid aliasing when downsampling. Recommended for intensity data; leave False for labels.

AffineSource( source: 'SourceLike', shape: Optional[Tuple[int, ...]] = None, *, affine_matrix: Optional[numpy.ndarray] = None, scale: Optional[List[float]] = None, rotation: Optional[List[float]] = None, shear: Optional[List[float]] = None, translation: Optional[List[float]] = None, order: int = 0, fill_value: numbers.Number = 0, anti_aliasing: bool = False)
46    def __init__(
47        self,
48        source: SourceLike,
49        shape: Optional[Tuple[int, ...]] = None,
50        *,
51        affine_matrix: Optional[np.ndarray] = None,
52        scale: Optional[List[float]] = None,
53        rotation: Optional[List[float]] = None,
54        shear: Optional[List[float]] = None,
55        translation: Optional[List[float]] = None,
56        order: int = 0,
57        fill_value: Number = 0,
58        anti_aliasing: bool = False,
59    ) -> None:
60        super().__init__(source)
61        ndim = self._source.ndim
62        if ndim not in (2, 3):
63            raise ValueError(f"AffineSource supports 2d or 3d data, got {ndim}d.")
64        if not 0 <= int(order) <= 5:
65            raise ValueError(f"order must be in [0, 5], got {order}.")
66
67        have_matrix = affine_matrix is not None
68        have_parameter = any(p is not None for p in (scale, rotation, shear, translation))
69        if have_matrix == have_parameter:
70            raise ValueError("Pass exactly one of affine_matrix or the scale/rotation/shear/translation parameters.")
71
72        if have_matrix:
73            matrix = np.asarray(affine_matrix, dtype="float64")
74        else:
75            matrix = compute_affine_matrix(scale, rotation, shear, translation)
76        if matrix.shape != (ndim + 1, ndim + 1):
77            raise ValueError(f"Invalid affine matrix shape {matrix.shape}, expected {(ndim + 1, ndim + 1)}.")
78
79        self._matrix = matrix
80        self._shape = self._source.shape if shape is None else tuple(int(s) for s in shape)
81        self._order = int(order)
82        self._fill_value = fill_value
83        self._anti_aliasing = bool(anti_aliasing)
shape: Tuple[int, ...]
85    @property
86    def shape(self) -> Tuple[int, ...]:
87        """The output (transformed) shape."""
88        return self._shape

The output (transformed) shape.

matrix: numpy.ndarray
90    @property
91    def matrix(self) -> np.ndarray:
92        """The affine matrix mapping output to input coordinates."""
93        return self._matrix

The affine matrix mapping output to input coordinates.

shards: Optional[Tuple[int, ...]]
95    @property
96    def shards(self) -> Optional[Tuple[int, ...]]:
97        # An affine of the input does not map onto the input's shard grid.
98        return None

The shard shape of the underlying array, or None if unsharded.

@register_wrapper
class ResizedSource(bioimage_py.wrapper.WrapperSource):
 23@register_wrapper
 24class ResizedSource(WrapperSource):
 25    """Resize the wrapped source to a target shape on read.
 26
 27    Args:
 28        source: The wrapped source-like object (2D or 3D).
 29        shape: The target shape for the resized source.
 30        order: The interpolation order, supports orders 0 to 5 (see
 31            ``bioimage_cpp.transformation.affine_transform``). Use ``0`` (nearest) for label data.
 32        anti_aliasing: Whether to Gaussian pre-smooth the input before sampling to avoid aliasing
 33            when downsampling. Recommended for intensity image data; leave ``False`` for labels.
 34        fill_value: The value used for output coordinates that map outside the input.
 35    """
 36
 37    def __init__(self, source: SourceLike, shape: Tuple[int, ...], *, order: int = 0,
 38                 anti_aliasing: bool = False, fill_value: float = 0) -> None:
 39        super().__init__(source)
 40        src_shape = self._source.shape
 41        if len(shape) != len(src_shape):
 42            raise ValueError(
 43                f"shape {tuple(shape)} must match the wrapped source dimensionality {len(src_shape)}."
 44            )
 45        if len(src_shape) not in (2, 3):
 46            raise ValueError(f"ResizedSource supports 2d or 3d data, got {len(src_shape)}d.")
 47        if not 0 <= int(order) <= 5:
 48            raise ValueError(f"order must be in [0, 5], got {order}.")
 49        self._shape = tuple(int(s) for s in shape)
 50        self._order = int(order)
 51        self._anti_aliasing = bool(anti_aliasing)
 52        self._fill_value = fill_value
 53        # Per-axis scale and the homogeneous affine matrix mapping output -> input coordinates.
 54        self._scale = [ish / float(osh) for ish, osh in zip(src_shape, self._shape)]
 55        self._matrix = np.diag(self._scale + [1.0])
 56        self._is_bool = np.dtype(self._source.dtype) == np.dtype(bool)
 57
 58    @property
 59    def shape(self) -> Tuple[int, ...]:
 60        """The target (resized) shape."""
 61        return self._shape
 62
 63    @property
 64    def scale(self) -> Tuple[float, ...]:
 65        """The per-axis scale factors (input size / output size)."""
 66        return tuple(self._scale)
 67
 68    @property
 69    def chunks(self) -> Optional[Tuple[int, ...]]:
 70        """The wrapped chunks scaled into the output space, or ``None`` if unchunked."""
 71        src_chunks = self._source.chunks
 72        if src_chunks is None:
 73            return None
 74        return tuple(
 75            max(1, int(ceil(c * osh / float(ish))))
 76            for c, ish, osh in zip(src_chunks, self._source.shape, self._shape)
 77        )
 78
 79    @property
 80    def shards(self) -> Optional[Tuple[int, ...]]:
 81        """Resized sources are unsharded (input-space shards do not map to the output)."""
 82        return None
 83
 84    def _getitem(self, roi: Tuple[slice, ...]) -> np.ndarray:
 85        """Return the resized data for the output region ``roi``."""
 86        ndim = len(self._shape)
 87        out_start = [int(sl.start) if sl.start is not None else 0 for sl in roi]
 88        out_stop = [int(sl.stop) if sl.stop is not None else self._shape[i]
 89                    for i, sl in enumerate(roi)]
 90        src_shape = self._source.shape
 91
 92        # The input region this output region samples from (exact for a diagonal scale matrix).
 93        in_start_f = [self._scale[i] * out_start[i] for i in range(ndim)]
 94        in_stop_f = [self._scale[i] * out_stop[i] for i in range(ndim)]
 95
 96        # Anti-aliasing sigma (per input axis), used to pre-smooth and to size the read halo.
 97        sigma = None
 98        if self._anti_aliasing:
 99            aa = np.asarray(bic.transformation.compute_anti_aliasing_sigma(self._matrix, ndim),
100                            dtype="float64")
101            if np.any(aa > 0):
102                sigma = aa
103
104        # Read halo: interpolation taps (order + 1) plus the smoothing extent.
105        halo = [self._order + 1] * ndim
106        if sigma is not None:
107            halo = [h + sigma_to_halo(float(s), self._order) for h, s in zip(halo, sigma)]
108
109        in_start = [max(0, int(floor(s)) - h) for s, h in zip(in_start_f, halo)]
110        in_stop = [min(int(ish), int(ceil(s)) + h) for s, h, ish in zip(in_stop_f, halo, src_shape)]
111        in_bb = tuple(slice(sta, sto) for sta, sto in zip(in_start, in_stop))
112        in_region = np.asarray(self._source[in_bb])
113        if self._is_bool:
114            in_region = in_region.astype("uint8")
115
116        # Shift the affine into the local frames: local output coords -> local input coords.
117        local_matrix = self._matrix.copy()
118        local_matrix[:ndim, ndim] = [self._scale[i] * out_start[i] - in_start[i]
119                                     for i in range(ndim)]
120        local_bb = tuple(slice(0, sto - sta) for sta, sto in zip(out_start, out_stop))
121
122        if sigma is None:
123            res = bic.transformation.affine_transform(
124                in_region, local_matrix, bounding_box=local_bb, order=self._order,
125                fill_value=self._fill_value,
126            )
127        else:
128            res = bic.transformation.resample(
129                in_region, local_matrix, bounding_box=local_bb, order=self._order,
130                fill_value=self._fill_value, anti_aliasing_sigma=sigma,
131            )
132
133        if self._is_bool:
134            res = res.astype(bool)
135        return res
136
137    def _params(self) -> dict:
138        """Return the keyword arguments needed to reconstruct this wrapper."""
139        return {
140            "shape": self._shape,
141            "order": self._order,
142            "anti_aliasing": self._anti_aliasing,
143            "fill_value": self._fill_value,
144        }

Resize the wrapped source to a target shape on read.

Args: source: The wrapped source-like object (2D or 3D). shape: The target shape for the resized source. order: The interpolation order, supports orders 0 to 5 (see bioimage_cpp.transformation.affine_transform). Use 0 (nearest) for label data. anti_aliasing: Whether to Gaussian pre-smooth the input before sampling to avoid aliasing when downsampling. Recommended for intensity image data; leave False for labels. fill_value: The value used for output coordinates that map outside the input.

ResizedSource( source: 'SourceLike', shape: Tuple[int, ...], *, order: int = 0, anti_aliasing: bool = False, fill_value: float = 0)
37    def __init__(self, source: SourceLike, shape: Tuple[int, ...], *, order: int = 0,
38                 anti_aliasing: bool = False, fill_value: float = 0) -> None:
39        super().__init__(source)
40        src_shape = self._source.shape
41        if len(shape) != len(src_shape):
42            raise ValueError(
43                f"shape {tuple(shape)} must match the wrapped source dimensionality {len(src_shape)}."
44            )
45        if len(src_shape) not in (2, 3):
46            raise ValueError(f"ResizedSource supports 2d or 3d data, got {len(src_shape)}d.")
47        if not 0 <= int(order) <= 5:
48            raise ValueError(f"order must be in [0, 5], got {order}.")
49        self._shape = tuple(int(s) for s in shape)
50        self._order = int(order)
51        self._anti_aliasing = bool(anti_aliasing)
52        self._fill_value = fill_value
53        # Per-axis scale and the homogeneous affine matrix mapping output -> input coordinates.
54        self._scale = [ish / float(osh) for ish, osh in zip(src_shape, self._shape)]
55        self._matrix = np.diag(self._scale + [1.0])
56        self._is_bool = np.dtype(self._source.dtype) == np.dtype(bool)
shape: Tuple[int, ...]
58    @property
59    def shape(self) -> Tuple[int, ...]:
60        """The target (resized) shape."""
61        return self._shape

The target (resized) shape.

scale: Tuple[float, ...]
63    @property
64    def scale(self) -> Tuple[float, ...]:
65        """The per-axis scale factors (input size / output size)."""
66        return tuple(self._scale)

The per-axis scale factors (input size / output size).

chunks: Optional[Tuple[int, ...]]
68    @property
69    def chunks(self) -> Optional[Tuple[int, ...]]:
70        """The wrapped chunks scaled into the output space, or ``None`` if unchunked."""
71        src_chunks = self._source.chunks
72        if src_chunks is None:
73            return None
74        return tuple(
75            max(1, int(ceil(c * osh / float(ish))))
76            for c, ish, osh in zip(src_chunks, self._source.shape, self._shape)
77        )

The wrapped chunks scaled into the output space, or None if unchunked.

shards: Optional[Tuple[int, ...]]
79    @property
80    def shards(self) -> Optional[Tuple[int, ...]]:
81        """Resized sources are unsharded (input-space shards do not map to the output)."""
82        return None

Resized sources are unsharded (input-space shards do not map to the output).

def register_wrapper( cls: Type[WrapperSource]) -> Type[WrapperSource]:
21def register_wrapper(cls: Type["WrapperSource"]) -> Type["WrapperSource"]:
22    """Class decorator registering a wrapper for spec-based reconstruction."""
23    _WRAPPER_REGISTRY[cls.__name__] = cls
24    return cls

Class decorator registering a wrapper for spec-based reconstruction.

def wrapper_from_spec( spec: bioimage_py.sources.SourceSpec) -> WrapperSource:
 95def wrapper_from_spec(spec: SourceSpec) -> WrapperSource:
 96    """Reconstruct a wrapper source from its spec."""
 97    params = dict(spec.params)
 98    cls_name = params.pop("cls")
 99    cls = _WRAPPER_REGISTRY.get(cls_name)
100    if cls is None:
101        raise ValueError(f"Unknown wrapper class {cls_name!r}; it must be registered with @register_wrapper.")
102    wrapped = spec.wrapped
103    specs = wrapped if isinstance(wrapped, (list, tuple)) else [wrapped]
104    sources = [from_spec(s) for s in specs]
105    return cls._from_wrapped(sources, params)

Reconstruct a wrapper source from its spec.