# 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()

@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 self.size == 0

@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)

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 + ")"
```