"""Bounding boxes transformation functions."""
from __future__ import division
import numpy as np
__all__ = ['crop', 'flip', 'resize', 'translate']
[docs]def crop(bbox, crop_box=None, allow_outside_center=True):
"""Crop bounding boxes according to slice area.
This method is mainly used with image cropping to ensure bonding boxes fit
within the cropped image.
Parameters
----------
bbox : numpy.ndarray
Numpy.ndarray with shape (N, 4+) where N is the number of bounding boxes.
The second axis represents attributes of the bounding box.
Specifically, these are :math:`(x_{min}, y_{min}, x_{max}, y_{max})`,
we allow additional attributes other than coordinates, which stay intact
during bounding box transformations.
crop_box : tuple
Tuple of length 4. :math:`(x_{min}, y_{min}, width, height)`
allow_outside_center : bool
If `False`, remove bounding boxes which have centers outside cropping area.
Returns
-------
numpy.ndarray
Cropped bounding boxes with shape (M, 4+) where M <= N.
"""
bbox = bbox.copy()
if crop_box is None:
return bbox
if not len(crop_box) == 4:
raise ValueError(
"Invalid crop_box parameter, requires length 4, given {}".format(str(crop_box)))
if sum([int(c is None) for c in crop_box]) == 4:
return bbox
l, t, w, h = crop_box
left = l if l else 0
top = t if t else 0
right = left + (w if w else np.inf)
bottom = top + (h if h else np.inf)
crop_bbox = np.array((left, top, right, bottom))
if allow_outside_center:
mask = np.ones(bbox.shape[0], dtype=bool)
else:
centers = (bbox[:, :2] + bbox[:, 2:4]) / 2
mask = np.logical_and(crop_bbox[:2] <= centers, centers < crop_bbox[2:]).all(axis=1)
# transform borders
bbox[:, :2] = np.maximum(bbox[:, :2], crop_bbox[:2])
bbox[:, 2:4] = np.minimum(bbox[:, 2:4], crop_bbox[2:4])
bbox[:, :2] -= crop_bbox[:2]
bbox[:, 2:4] -= crop_bbox[:2]
mask = np.logical_and(mask, (bbox[:, :2] < bbox[:, 2:4]).all(axis=1))
bbox = bbox[mask]
return bbox
[docs]def flip(bbox, size, flip_x=False, flip_y=False):
"""Flip bounding boxes according to image flipping directions.
Parameters
----------
bbox : numpy.ndarray
Numpy.ndarray with shape (N, 4+) where N is the number of bounding boxes.
The second axis represents attributes of the bounding box.
Specifically, these are :math:`(x_{min}, y_{min}, x_{max}, y_{max})`,
we allow additional attributes other than coordinates, which stay intact
during bounding box transformations.
size : tuple
Tuple of length 2: (width, height).
flip_x : bool
Whether flip horizontally.
flip_y : type
Whether flip vertically.
Returns
-------
numpy.ndarray
Flipped bounding boxes with original shape.
"""
if not len(size) == 2:
raise ValueError("size requires length 2 tuple, given {}".format(len(size)))
width, height = size
bbox = bbox.copy()
if flip_y:
ymax = height - bbox[:, 1]
ymin = height - bbox[:, 3]
bbox[:, 1] = ymin
bbox[:, 3] = ymax
if flip_x:
xmax = width - bbox[:, 0]
xmin = width - bbox[:, 2]
bbox[:, 0] = xmin
bbox[:, 2] = xmax
return bbox
[docs]def resize(bbox, in_size, out_size):
"""Resize bouding boxes according to image resize operation.
Parameters
----------
bbox : numpy.ndarray
Numpy.ndarray with shape (N, 4+) where N is the number of bounding boxes.
The second axis represents attributes of the bounding box.
Specifically, these are :math:`(x_{min}, y_{min}, x_{max}, y_{max})`,
we allow additional attributes other than coordinates, which stay intact
during bounding box transformations.
in_size : tuple
Tuple of length 2: (width, height) for input.
out_size : tuple
Tuple of length 2: (width, height) for output.
Returns
-------
numpy.ndarray
Resized bounding boxes with original shape.
"""
if not len(in_size) == 2:
raise ValueError("in_size requires length 2 tuple, given {}".format(len(in_size)))
if not len(out_size) == 2:
raise ValueError("out_size requires length 2 tuple, given {}".format(len(out_size)))
bbox = bbox.copy()
x_scale = out_size[0] / in_size[0]
y_scale = out_size[1] / in_size[1]
bbox[:, 1] = y_scale * bbox[:, 1]
bbox[:, 3] = y_scale * bbox[:, 3]
bbox[:, 0] = x_scale * bbox[:, 0]
bbox[:, 2] = x_scale * bbox[:, 2]
return bbox
[docs]def translate(bbox, x_offset=0, y_offset=0):
"""Translate bounding boxes by offsets.
Parameters
----------
bbox : numpy.ndarray
Numpy.ndarray with shape (N, 4+) where N is the number of bounding boxes.
The second axis represents attributes of the bounding box.
Specifically, these are :math:`(x_{min}, y_{min}, x_{max}, y_{max})`,
we allow additional attributes other than coordinates, which stay intact
during bounding box transformations.
x_offset : int or float
Offset along x axis.
y_offset : int or float
Offset along y axis.
Returns
-------
numpy.ndarray
Translated bounding boxes with original shape.
"""
bbox = bbox.copy()
bbox[:, :2] += (x_offset, y_offset)
bbox[:, 2:4] += (x_offset, y_offset)
return bbox