Source code for funlib.geometry.roi

from .coordinate import Coordinate
from .freezable import Freezable

import copy
import numbers
from typing import Iterable
import logging

logger = logging.getLogger(__file__)


[docs]class Roi(Freezable): """A rectangular region of interest, defined by an offset and a shape. Special Cases: An infinite/unbounded ROI: offset = (None, None, ...) shape = (None, None, ...) An empty ROI (e.g. output of intersecting two non overlapping Rois): offset = (None, None, ...) shape = (0, 0, ...) A ROI that only specifies a shape is not supported (just use Coordinate). There is no guessing size of offset or shape (expanding to number of dims of the other). Basic Operations: Addition/subtraction (Coordinate or int) - shifts the offset elementwise (alias for shift) Multiplication/division (Coordiante or int) - multiplies/divides the offset and the shape, elementwise Roi Operations: Intersect, union Similar to :class:`Coordinate`, supports simple arithmetics, e.g.:: roi = Roi((1, 1, 1), (10, 10, 10)) voxel_size = Coordinate((10, 5, 1)) roi * voxel_size = Roi((10, 5, 1), (100, 50, 10)) scale_shift = roi*voxel_size + 1 # == Roi((11, 6, 2), (101, 51, 11)) Args: offset (array-like of ``int``): The offset of the ROI. Entries can be ``None`` to indicate there is no offset (either unbounded or empty). shape (array-like of ``int``): The shape of the ROI. Entries can be ``None`` to indicate unboundedness. """ def __init__(self, offset, shape): self.__offset = Coordinate(offset) self.__shape = Coordinate(shape) self.freeze() self.__consolidate_offset() def squeeze(self, dim: int = 0): return Roi(self.offset.squeeze(dim), self.shape.squeeze(dim)) @property def offset(self): return self.__offset @offset.setter def offset(self, offset): """Set the offset of this Roi. Args: offset (array-like): The new offset. Entries can be ``None``` to indicate unboundedness or empty ROI. """ self.__offset = Coordinate(offset) self.__consolidate_offset() def get_offset(self): return self.offset def set_offset(self, new_offset): self.offset = new_offset @property def shape(self): return self.__shape @shape.setter def shape(self, shape): """Set the shape of this ROI. Args: shape (array-like or ``None``): The new shape. Entries can be ``None`` to indicate unboundedness. """ self.__shape = Coordinate(shape) self.__consolidate_offset() def get_shape(self): return self.shape def set_shape(self, new_shape): self.shape = new_shape def __consolidate_offset(self): """Ensure that offset and shape have same number of dimensions and offsets for unbounded or empty dimensions are None.""" assert ( self.__offset.dims == self.__shape.dims ), "offset dimension %d != shape dimension %d" % ( self.__offset.dims, self.__shape.dims, ) self.__offset = Coordinate( (o if s is not None else None for o, s in zip(self.__offset, self.__shape)) ) @property def begin(self): """Smallest coordinate inside ROI.""" return self.__offset def get_begin(self): return self.begin @property def end(self): """Smallest coordinate which is component-wise larger than any inside ROI.""" return self.__offset + self.__shape def get_end(self): return self.end @property def center(self): """Get the center of this ROI.""" return self.__offset + self.__shape / 2 def get_center(self): return self.center
[docs] def to_slices(self): """Get a ``tuple`` of ``slice`` that represent this ROI and can be used to index arrays.""" slices = [] for d in range(self.dims): if self.__shape[d] is None: s = slice(None, None) elif self.__shape[d] == 0: s = slice(None, 0) else: s = slice( int(self.__offset[d]), int(self.__offset[d] + self.__shape[d]) ) slices.append(s) return tuple(slices)
[docs] def get_bounding_box(self): """Alias for ``to_slices()``.""" return self.to_slices()
@property def dims(self): """The the number of dimensions of this ROI.""" return self.__shape.dims @property def size(self): """Get the volume of this ROI. Returns ``None`` if the ROI is unbounded.""" if self.unbounded: return None size = 1 for d in self.__shape: size *= d return size def get_size(self): return self.size @property def empty(self): """Test if this ROI is empty.""" return any([x is not None and x <= 0 for x in self.shape]) @property def unbounded(self): """Test if this ROI is unbounded.""" return None in self.__shape
[docs] def contains(self, other): """Test if this ROI contains ``other``, which can be another :class:`Roi`, :class:`Coordinate`, or ``tuple``.""" if isinstance(other, Roi): if other.empty: # gunpowder expects empty rois to contain empty return self.empty or self.contains(other.begin) return self.contains(other.begin) and self.contains(other.end - 1) elif isinstance(other, Iterable): axis_containment = [ (b is None or (p is not None and p >= b)) and (e is None or (p is not None and p < e)) for p, b, e in zip(other, self.begin, self.end) ] return len(axis_containment) == self.dims and all(axis_containment) else: raise Exception( f"cannot compute containment on object of type: {type(other)}" )
[docs] def intersects(self, other): """Test if this ROI intersects with another :class:`Roi`.""" assert self.dims == other.dims if self.empty or other.empty: return False # separated if at least one dimension is separated separated = any( [ # a dimension is separated if: # none of the shapes is unbounded (None not in [b1, b2, e1, e2]) and ( # either b1 starts after e2 (b1 >= e2) or # or b2 starts after e1 (b2 >= e1) ) for b1, b2, e1, e2 in zip(self.begin, other.begin, self.end, other.end) ] ) return not separated
[docs] def intersect(self, other): """Get the intersection of this ROI with another :class:`Roi`.""" if not self.intersects(other): return Roi((None,) * self.dims, (0,) * self.dims) # empty ROI begin = Coordinate( (self.__left_max(b1, b2) for b1, b2 in zip(self.begin, other.begin)) ) end = Coordinate( (self.__right_min(e1, e2) for e1, e2 in zip(self.end, other.end)) ) return Roi(begin, end - begin)
[docs] def union(self, other): """Get the union of this ROI with another :class:`Roi`.""" if self.empty: return other if other.empty: return self begin = Coordinate( (self.__left_min(b1, b2) for b1, b2 in zip(self.begin, other.begin)) ) end = Coordinate( (self.__right_max(e1, e2) for e1, e2 in zip(self.end, other.end)) ) return Roi(begin, end - begin)
[docs] def shift(self, by): """Shift this ROI.""" return Roi(self.__offset + by, self.__shape)
[docs] def snap_to_grid(self, voxel_size, mode="grow"): """Align a ROI with a given voxel size. Args: voxel_size (:class:`Coordinate` or ``tuple``): The voxel size of the grid to snap to. mode (string, optional): How to align the ROI if it is not a multiple of the voxel size. Available modes are 'grow', 'shrink', and 'closest'. Defaults to 'grow'. """ if not isinstance(voxel_size, Coordinate): voxel_size = Coordinate(voxel_size) assert ( voxel_size.dims == self.dims ), "dimension of voxel size does not match ROI" assert 0 not in voxel_size, "Voxel size cannot contain zero" if mode == "closest": begin_in_voxel = self.begin.round_division(voxel_size) end_in_voxel = self.end.round_division(voxel_size) elif mode == "grow": begin_in_voxel = self.begin.floor_division(voxel_size) end_in_voxel = self.end.ceil_division(voxel_size) elif mode == "shrink": begin_in_voxel = self.begin.ceil_division(voxel_size) end_in_voxel = self.end.floor_division(voxel_size) else: raise RuntimeError("Unknown mode %s for snap_to_grid" % mode) return Roi( begin_in_voxel * voxel_size, (end_in_voxel - begin_in_voxel) * voxel_size )
[docs] def grow(self, amount_neg=0, amount_pos=0): """Grow a ROI by the given amounts in each direction: Args: amount_neg (:class:`Coordinate` or ``int``): Amount (per dimension) to grow into the negative direction. Passing in a single integer grows that amount in all dimensions. Defaults to zero. amount_pos (:class:`Coordinate` or ``int``): Amount (per dimension) to grow into the positive direction. Passing in a single integer grows that amount in all dimensions. Defaults to zero. """ if isinstance(amount_neg, tuple): amount_neg = Coordinate(amount_neg) if isinstance(amount_pos, tuple): amount_pos = Coordinate(amount_pos) offset = self.__offset - amount_neg shape = self.__shape + amount_neg + amount_pos return Roi(offset, shape)
[docs] def copy(self): """Create a copy of this ROI.""" return copy.deepcopy(self)
def __left_min(self, x, y): # None is considered -inf if x is None or y is None: return None return min(x, y) def __left_max(self, x, y): # None is considered -inf if x is None: return y if y is None: return x return max(x, y) def __right_min(self, x, y): # None is considered +inf if x is None: return y if y is None: return x return min(x, y) def __right_max(self, x, y): # None is considered +inf if x is None or y is None: return None return max(x, y) def __add__(self, other): assert isinstance(other, Coordinate) or isinstance( other, numbers.Number ), "can only add number or Coordinate to Roi" return self.shift(other) def __sub__(self, other): assert isinstance(other, Coordinate) or isinstance( other, numbers.Number ), "can only subtract number or Coordinate from Roi" return self.shift(-other) def __mul__(self, other): assert isinstance(other, Coordinate) or isinstance( other, numbers.Number ), "can only multiply with a number or Coordinate" return Roi(self.__offset * other, self.__shape * other) def __div__(self, other): assert isinstance(other, Coordinate) or isinstance( other, numbers.Number ), "can only divide by a number or Coordinate" return Roi(self.__offset / other, self.__shape / other) def __truediv__(self, other): assert isinstance(other, Coordinate) or isinstance( other, numbers.Number ), "can only divide by a number or Coordinate" return Roi(self.__offset / other, self.__shape / other) def __floordiv__(self, other): assert isinstance(other, Coordinate) or isinstance( other, numbers.Number ), "can only divide by a number or Coordinate" return Roi(self.__offset // other, self.__shape // other) def __mod__(self, other): # pragma: py3 no cover assert isinstance(other, Coordinate) or isinstance( other, numbers.Number ), "can only mod by a number or Coordinate" return Roi(self.__offset % other, self.__shape % other) def __eq__(self, other): if isinstance(other, self.__class__): return self.__dict__ == other.__dict__ return NotImplemented def __ne__(self, other): if isinstance(other, self.__class__): return not self.__eq__(other) return NotImplemented def __repr__(self): if self.empty: return "[empty ROI]" slices = ", ".join( [ (str(b) if b is not None else "") + ":" + (str(e) if e is not None else "") for b, e in zip(self.begin, self.end) ] ) dims = ", ".join(str(a) if a is not None else "inf" for a in self.__shape) return "[" + slices + "] (" + dims + ")"