Module vipy.geometry
Expand source code Browse git
import math
import numpy as np
from itertools import product
from vipy.util import try_import, istuple, isnumpy, isnumber, tolist
from vipy.linalg import columnvector
import warnings
try:
import ujson as json # faster
except ImportError:
import json
def covariance_to_ellipse(cov):
"""2x2 covariance matrix to ellipse (major_axis_length, minor_axis_length, angle_in_radians)"""
assert isnumpy(cov) and cov.shape == (2,2), "Invalid input"
(d,V) = np.linalg.eig(cov)
return np.array((d[0], d[1], math.atan2(V[1,0], V[0,0]))) # (major_axis_len, minor_axis_len, angle_in_radians)
def dehomogenize(p):
"""Convert 3x1 homogenous point (x,y,h) to 2x1 non-homogenous point (x/h, y/h)"""
assert isnumpy(p)
if p.ndim == 1:
assert len(p) == 3
return p[0:2] / p[2]
elif p.ndim == 2:
assert isnumpy(p) and p.shape[0] == 3, "Invalid input"
p = columnvector(p) if p.ndim == 1 else p
return p[0:-1, :] / p[-1,:]
else:
return ValueError('p must be 1d or 2d')
def homogenize(p):
"""Convert 2xN non-homogenous points (x,y) to 3xN non-homogenous point (x, y, 1)"""
assert isnumpy(p)
if p.ndim == 1:
return np.hstack( (p, 1) )
elif p.ndim == 2:
assert p.shape[0] == 2, "Invalid input"
p = columnvector(p) if p.ndim == 1 else p
return np.vstack((p, np.ones_like(p[-1])))
else:
return ValueError('p must be 1d or 2d')
def apply_homography(H,p):
"""Apply a 3x3 homography H to non-homogenous point p and return a transformed point """
assert isnumpy(H) and isnumpy(p) and H.shape == (3,3) and p.shape[0] == 2, "Invalid input"
return dehomogenize(np.dot(H, homogenize(p)))
def similarity_transform_2x3(c=(0,0), r=0, s=1):
"""Return a 2x3 similarity transform with rotation r (radians), scale s and origin c=(x,y)"""
assert istuple(c) and len(c) == 2 and isnumber(r) and isnumber(s), "Invalid input"
deg = r * 180. / math.pi
a = s * np.cos(r)
b = s * np.sin(r)
(x,y) = (c[0], c[1])
return np.array([[a, b, (1 - a) * x - b * y], [-b, a, b * x + (1 - a) * y]])
def similarity_transform(txy=(0,0), r=0, s=1):
"""Return a 3x3 similarity transformation with translation tuple txy=(x,y), rotation r (radians, scale=s"""
assert istuple(txy) and len(txy) == 2 and isnumber(r) and isnumber(s), "Invalid input"
R = np.mat([[np.cos(r), -np.sin(r), 0], [np.sin(r), np.cos(r), 0], [0,0,1]])
S = np.mat([[s,0,0], [0, s, 0], [0,0,1]])
T = np.mat([[0,0,txy[0]], [0,0,txy[1]], [0,0,0]])
return S * R + T # composition
def affine_transform(txy=(0,0), r=0, sx=1, sy=1, kx=0, ky=0):
"""Compose and return a 3x3 affine transformation for translation txy=(0,0), rotation r (radians), scalex=sx, scaley=sy, shearx=kx, sheary=ky.
Usage:
```python
A = vipy.geometry.affine_transform(r=np.pi/4)
vipy.image.Image(array=vipy.geometry.imtransform(im.array(), A), colorspace='float')
```
Equivalently:
```python
im = vipy.image.RandomImage().affine_transform(A)
```
"""
assert istuple(txy) and len(txy) == 2 and isnumber(r) and isnumber(sx) and isnumber(sy) and isnumber(kx) and isnumber(ky), "Invalid input"
R = np.mat([[np.cos(r), -np.sin(r), 0], [np.sin(r), np.cos(r), 0], [0,0,1]])
S = np.mat([[sx,0,0], [0, sy, 0], [0,0,1]])
K = np.mat([[1,ky,0], [kx,1,0], [0,0,1]])
T = np.mat([[0,0,txy[0]], [0,0,txy[1]], [0,0,0]])
return K * S * R + T # composition
def random_affine_transform(txy=((0,1),(0,1)), r=(0,1), sx=(0.1,1), sy=(0.1,1), kx=(0.1,1), ky=(0.1,1)):
"""Return a random 3x3 affine transformation matrix for the provided ranges, inputs must be tuples"""
assert istuple(txy) and istuple(txy[0]) and istuple(txy[1]) and istuple(r) and istuple(sx) and istuple(sy) and istuple(kx) and istuple(ky), "Invalid input"
uniform_random_in_range = lambda t: np.random.uniform(t[0], t[1])
return affine_transform(txy=(uniform_random_in_range(txy[0]), uniform_random_in_range(txy[1])),
r=uniform_random_in_range(r),
sx=uniform_random_in_range(sx),
sy=uniform_random_in_range(sy),
kx=uniform_random_in_range(kx),
ky=uniform_random_in_range(ky))
def imtransform(img, A, border='zero'):
"""Transform an numpy array image (MxNx3) following the affine or similiarity transformation A"""
assert isnumpy(img) and isnumpy(A), "invalid input"
assert border in ['zero', 'replicate']
try_import('cv2', 'opencv-python'); import cv2
borderMode = cv2.BORDER_REPLICATE if border=='replicate' else cv2.BORDER_CONSTANT
if A.shape == (3,3):
return cv2.warpPerspective(img, A, (img.shape[1], img.shape[0]), borderMode=borderMode)
else:
return cv2.warpAffine(img, A, (img.shape[1], img.shape[0]), borderMode=borderMode)
def normalize(x, eps=1E-16):
"""Given a vector x, return the vector unit normalized as float64"""
assert isnumpy(x), "Invalid input"
return x / (np.linalg.norm(x.astype(np.float64)) + eps)
def imagebox(shape):
return BoundingBox(xmin=0, ymin=0, width=shape[1], height=shape[0])
class BoundingBox(object):
"""Core bounding box class with flexible constructors in this priority order:
(xmin,ymin,xmax,ymax)
(xmin,ymin,width,height)
(centroid[0],centroid[1],width,height)
(xcentroid,ycentroid,width,height)
xywh=(xmin,ymin,width,height)
ulbr=(xmin,ymin,xmax,ymax)
bounding rectangle of binary mask image"""
#__slots__ = ['_xmin', '_ymin', '_xmax', '_ymax'] # This is not backwards compatible
def __init__(self, xmin=None, ymin=None, xmax=None, ymax=None, centroid=None, xcentroid=None, ycentroid=None, width=None, height=None, mask=None, xywh=None, ulbr=None, ulbrdict=None):
if ulbrdict is not None:
self.__dict__ = ulbrdict # equivalent to (but faster)
#self._xmin = ulbrdict['_xmin']
#self._ymin = ulbrdict['_ymin']
#self._xmax = ulbrdict['_xmax']
#self._ymax = ulbrdict['_ymax']
elif xmin is not None and ymin is not None and xmax is not None and ymax is not None:
if not (isnumber(xmin) and isnumber(ymin) and isnumber(xmax) and isnumber(ymax)):
raise ValueError('Box coordinates must be integers or floats not "%s"' % str(type(xmin)))
self._xmin = float(xmin)
self._ymin = float(ymin)
self._xmax = float(xmax)
self._ymax = float(ymax)
elif xmin is not None and ymin is not None and width is not None and height is not None:
if not (isnumber(xmin) and isnumber(ymin) and isnumber(width) and isnumber(height)):
raise ValueError('Box coordinates must be integers or floats not "%s"' % str(type(width)))
self._xmin = float(xmin)
self._ymin = float(ymin)
self._xmax = self._xmin + float(width)
self._ymax = self._ymin + float(height)
elif centroid is not None and width is not None and height is not None:
if not (len(centroid) == 2 and isnumber(centroid[0]) and isnumber(centroid[1]) and isnumber(width) and isnumber(height)):
raise ValueError('Invalid box coordinates')
self._xmin = float(centroid[0]) - float(width) / 2.0
self._ymin = float(centroid[1]) - float(height) / 2.0
self._xmax = float(centroid[0]) + float(width) / 2.0
self._ymax = float(centroid[1]) + float(height) / 2.0
elif xcentroid is not None and ycentroid is not None and width is not None and height is not None:
#if not (isnumber(xcentroid) and isnumber(ycentroid) and isnumber(width) and isnumber(height)):
# raise ValueError('Box coordinates must be integers or floats')
self._xmin = float(xcentroid) - (float(width) / 2.0)
self._ymin = float(ycentroid) - (float(height) / 2.0)
self._xmax = float(xcentroid) + (float(width) / 2.0)
self._ymax = float(ycentroid) + (float(height) / 2.0)
elif xywh is not None:
self.xywh(xywh)
elif ulbr is not None:
self.ulbr(ulbr)
elif mask is not None:
# Bounding rectangle of non-zero pixels in a binary mask image
if not isnumpy(mask) or np.sum(mask) == 0:
raise ValueError('Mask input must be numpy array with at least one non-zero entry')
imx = np.sum(mask, axis=0)
imy = np.sum(mask, axis=1)
self._xmin = np.argwhere(imx > 0)[0]
self._ymin = np.argwhere(imy > 0)[0]
self._xmax = np.argwhere(imx > 0)[-1]
self._ymax = np.argwhere(imy > 0)[-1]
else:
raise ValueError('invalid constructor input')
@classmethod
def cast(cls, bb, flush=False):
assert isinstance(bb, BoundingBox)
bb.__class__ = BoundingBox
if flush:
bb.__dict__ = {k:v for (k,v) in bb.__dict__.items() if k in ['_xmin', '_ymin', '_xmax', '_ymax']}
return bb
@classmethod
def from_json(cls, s):
d = json.loads(s) if not isinstance(s, dict) else s
d = {'_'+k if not k.startswith('_') else k:v for (k,v) in d.items()} # from prettyjson (add "_" prefix to attributes)
return cls(ulbrdict=d)
def dict(self):
"""Return a python dictionary containing the relevant serialized attributes suitable for JSON encoding"""
return self.json(encode=False)
def __json__(self):
"""Serialization method for json package"""
return self.json(encode=True)
def json(self, encode=True):
d = {k.lstrip('_'):v for (k,v) in self.__dict__.items()} # prettyjson (remove "_" prefix to attributes)
return json.dumps(d) if encode else d
def clone(self):
return BoundingBox(xmin=self._xmin, xmax=self._xmax, ymin=self._ymin, ymax=self._ymax)
def bbclone(self):
return BoundingBox(xmin=self._xmin, xmax=self._xmax, ymin=self._ymin, ymax=self._ymax)
def __eq__(self, other):
"""Bounding box equality (integer resolution of corners)"""
return isinstance(other, BoundingBox) and self.clone().int().xywh() == other.clone().int().xywh()
def __neq__(self, other):
"""Bounding box non-equality"""
return not self.__eq__(other)
def __repr__(self):
return str('<vipy.geometry.boundingbox: xmin=%s, ymin=%s, width=%s, height=%s>' % (self.xmin(), self.ymin(), self._width(), self._height()))
def __str__(self):
return self.__repr__()
def xmin(self, x=None):
"""x coordinate of upper left corner of box, x-axis is image column"""
self._xmin = self._xmin if x is None else x
return self._xmin if x is None else self
def ul(self):
"""Upper left coordinate (x,y)"""
return (self._xmin, self._ymin)
def ulx(self):
"""Upper left coordinate (x)"""
return self.ul()[0]
def uly(self):
"""Upper left coordinate (y)"""
return self.ul()[1]
def ur(self):
"""Upper right coordinate (x,y)"""
return (self._xmax, self._ymin)
def urx(self):
"""Upper right coordinate (x)"""
return self.ur()[0]
def ury(self):
"""Upper right coordinate (y)"""
return self.ur()[1]
def ll(self):
"""Lower left coordinate (x,y), synonym for bl()"""
return (self._xmin, self._ymax)
def bl(self):
"""Bottom left coordinate (x,y), synonym for ll()"""
return (self._xmin, self._ymax)
def blx(self):
"""Bottom left coordinate (x)"""
return self.bl()[0]
def bly(self):
"""Bottom left coordinate (y)"""
return self.bl()[1]
def lr(self):
"""Lower right coordinate (x,y), synonym for br()"""
return (self._xmax, self._ymax)
def br(self):
"""Bottom right coordinate (x,y), synonym for lr()"""
return (self._xmax, self._ymax)
def brx(self):
"""Bottom right coordinate (x)"""
return self.br()[0]
def bry(self):
"""Bottom right coordinate (y)"""
return self.br()[1]
def ymin(self, y=None):
"""y coordinate of upper left corner of box, y-axis is image row, set if provided"""
self._ymin = self._ymin if y is None else y
return self._ymin if y is None else self
def xmax(self, x=None):
"""x coordinate of lower right corner of box, x-axis is image column"""
self._xmax = self._xmax if x is None else x
return self._xmax if x is None else self
def ymax(self, y=None):
"""y coordinate of lower right corner of box, y-axis is image row"""
self._ymax = self._ymax if y is None else y
return self._ymax if y is None else self
def upperleft(self):
"""Return the (x,y) upper left corner coordinate of the box"""
return (self.xmin(), self.ymin())
def bottomleft(self):
"""Return the (x,y) lower left corner coordinate of the box"""
return (self.xmin(), self.ymax())
def upperright(self):
"""Return the (x,y) upper right corner coordinate of the box"""
return (self.xmax(), self.ymin())
def bottomright(self):
"""Return the (x,y) lower right corner coordinate of the box"""
return (self.xmax(), self.ymax())
def isinteger(self):
return (isinstance(self._xmin, int) and
isinstance(self._ymin, int) and
isinstance(self._xmax, int) and
isinstance(self._ymax, int))
def int(self):
"""Convert corners to integer with rounding, in-place update"""
(w,h) = (int(np.round(self._width())), int(np.round(self._height())))
self._xmin = int(np.round(self._xmin))
self._ymin = int(np.round(self._ymin))
self._xmax = int(np.round(self._xmax))
self._ymax = int(np.round(self._ymax))
if w != self._width():
self.right(w - self._width()) # preserve aspect ratio due to rounding by +/- right side of box
if h != self._height():
self.bottom(h-self._height()) # preserve aspect ratio due to rounding by +/- bottom of box
return self
def float(self):
"""Convert corners to float"""
self._xmin = float(self._xmin)
self._ymin = float(self._ymin)
self._xmax = float(self._xmax)
self._ymax = float(self._ymax)
return self
def significant_digits(self, n):
"""Convert corners to have at most n significant digits for efficient JSON storage"""
assert isinstance(n, int) and n>=0
self._xmin = round(self._xmin, n)
self._ymin = round(self._ymin, n)
self._xmax = round(self._xmax, n)
self._ymax = round(self._ymax, n)
return self
def translate(self, dx=0, dy=0):
"""Translate the bounding box by dx in x and dy in y"""
self._xmin = self._xmin + dx
self._ymin = self._ymin + dy
self._xmax = self._xmax + dx
self._ymax = self._ymax + dy
return self
def to_origin(self):
"""Translate the bounding box so that (xmin, ymin) = (0,0)"""
return self.translate(-self.xmin(), -self.ymin())
def set_origin(self, other):
"""Set the origin of the coordinates of this bounding box to be relative to the upper left of the other bounding box"""
assert isinstance(other, BoundingBox), "Invalid BoundingBox() input of type '%s'" % str(type(other))
return self.translate(other.xmin(), other.ymin())
def offset(self, dx=0, dy=0):
"""Alias for translate"""
return self.translate(dx, dy)
def invalid(self):
"""Is the box a valid bounding box?"""
#is_undefined = np.isnan(self._xmin) or np.isnan(self._ymin) or np.isnan(self._xmax) or np.isnan(self._ymax)
is_valid = ((self._xmax - self._xmin) >= 0) and ((self._ymax - self._ymin) >= 0) # if nan, will return False
return not is_valid
def valid(self):
return not self.invalid()
def isvalid(self):
return not self.invalid()
def isdegenerate(self):
return self.invalid()
def isnonnegative(self):
return (self.xmin() >= 0 and
self.ymin() >= 0 and
self.xmax() >= 0 and
self.ymax() >= 0)
def width(self):
return self._xmax - self._xmin
def _width(self):
"""Alias for `vipy.geometry.BoundingBox.width`, useful for multiple inheritance with ambiguous width"""
return self._xmax - self._xmin
def setwidth(self, w):
"""Set new width keeping centroid constant"""
if w <= 0:
raise ValueError('invalid width')
worig = (self._xmax - self._xmin)
self._xmax += float((w - worig) / 2.0)
self._xmin -= float((w - worig) / 2.0)
return self
def setheight(self, h):
"""Set new height keeping centroid constant"""
if h <= 0:
raise ValueError('invalid height')
horig = self._ymax - self._ymin
self._ymax += float((h - horig) / 2.0)
self._ymin -= float((h - horig) / 2.0)
return self
def height(self):
return self._ymax - self._ymin
def _height(self):
"""Alias for `vipy.geometry.BoundingBox.height`, useful for multiple inheritance with ambiguous height"""
return self._ymax - self._ymin
def centroid(self, c=None):
"""(x,y) tuple of centroid position of bounding box"""
return self._centroid(c)
def _centroid(self, c=None):
"""(x,y) tuple of centroid position of bounding box"""
if c is None:
(height, width) = (self._ymax-self._ymin, self._xmax-self._xmin)
return (self._xmin + (float(width) / 2.0), self._ymin + (float(height) / 2.0))
else:
assert len(c) == 2
(height, width) = (self._ymax-self._ymin, self._xmax-self._xmin)
self._xmin = float(c[0]) - (width / 2.0)
self._ymin = float(c[1]) - (height / 2.0)
self._xmax = float(c[0]) + (width / 2.0)
self._ymax = float(c[1]) + (height / 2.0)
return self
def x_centroid(self):
return self._centroid()[0]
def xcentroid(self):
"""Alias for x_centroid()"""
return self._centroid()[0]
def centroid_x(self):
"""Alias for x_centroid()"""
return self._centroid()[0]
def y_centroid(self):
return self._centroid()[1]
def ycentroid(self):
"""Alias for y_centroid()"""
return self._centroid()[1]
def centroid_y(self):
"""Alias for y_centroid()"""
return self._centroid()[1]
def _area(self):
"""Return the area=width*height of the bounding box, internal method useful for multiple inheritance"""
(height, width) = (self._ymax-self._ymin, self._xmax-self._xmin)
return width * height if (height>0 and width>0) else 0
def area(self):
"""Return the area=width*height of the bounding box"""
return self._area()
def to_xywh(self, xywh=None):
"""Return bounding box corners as (x,y,width,height) tuple"""
if xywh is None:
(height, width) = (self._ymax-self._ymin, self._xmax-self._xmin)
return tuple([self._xmin, self._ymin, width, height])
else:
assert len(xywh) == 4, "Invalid (xmin,ymin,width,height) input"
self._xmin = float(xywh[0])
self._ymin = float(xywh[1])
self._xmax = float(self._xmin + xywh[2])
self._ymax = float(self._ymin + xywh[3])
return self
def xywh(self, xywh_=None):
"""Alias for to_xywh"""
return self.to_xywh(xywh_)
def cxywh(self, cxywh=None):
"""Return or set bounding box corners as (centroidx,centroidy,width,height) tuple"""
if cxywh is None:
(height, width) = (self._ymax-self._ymin, self._xmax-self._xmin)
return tuple([self.x_centroid(), self.y_centroid(), width, height])
else:
assert len(cxywh) == 4, "Invalid (xcentroid, ycentroid, width, height) input"
return self._centroid( (cxywh[0], cxywh[1]) ).setwidth(cxywh[2]).setheight(cxywh[3])
def ulbr(self, ulbr=None):
"""Return bounding box corners as upper left, bottom right (xmin, ymin, xmax, ymax)"""
if ulbr is None:
return (self._xmin, self._ymin, self._xmax, self._ymax)
else:
assert len(ulbr) == 4, "Invalid (xmin,ymin,xmax,ymax) input"
self._xmin = float(ulbr[0])
self._ymin = float(ulbr[1])
self._xmax = float(ulbr[2])
self._ymax = float(ulbr[3])
return self
def to_ulbr(self, ulbr=None):
"""Alias for ulbr()"""
return self.ulbr(ulbr)
def dx(self, bb):
"""Offset bounding box by same xmin as provided box"""
return bb._xmin - self._xmin
def dy(self, bb):
"""Offset bounding box by ymin of provided box"""
return bb._ymin - self._ymin
def sqdist(self, bb):
"""Squared Euclidean distance between upper left corners of two bounding boxes"""
assert isinstance(bb, BoundingBox), "Invalid BoundingBox() input of type '%s'" % str(type(bb))
return np.power(self.dx(bb), 2.0) + np.power(self.dy(bb), 2.0)
def dist(self, bb):
"""Distance between centroids of two bounding boxes"""
assert isinstance(bb, BoundingBox), "Invalid BoundingBox() input of type '%s'" % str(type(bb))
return np.sqrt(np.sum(np.square(np.array(bb._centroid()) - np.array(self._centroid()))))
def pdist(self, bb, sigma=None):
"""Normalized Gaussian distance in [0,1] between centroids of two bounding boxes, where 0 is far and 1 is same with sigma=maxdim() of this box"""
assert isinstance(bb, BoundingBox), "Invalid BoundingBox() input of type '%s'" % str(type(bb))
return np.exp(-self.sqdist(bb)/(float(2*self.maxdim()*self.maxdim()) if sigma is None else float(2.0*sigma*sigma)))
def iou(self, bb, area=None, otherarea=None):
"""area of intersection / area of union"""
assert bb is None or isinstance(bb, BoundingBox), "Invalid BoundingBox() input of type '%s'" % str(type(bb))
if bb is None:
return 0
w = min(self._xmax, bb._xmax) - max(self._xmin, bb._xmin)
if w <= 0:
return 0 # invalid (no overlap), early exit
h = min(self._ymax, bb._ymax) - max(self._ymin, bb._ymin)
if h <= 0:
return 0 # invalid (no overlap), early exit
area_intersection = w * h
area_union = ((self.area() if area is None else area) +
(bb.area() if otherarea is None else otherarea) -
area_intersection)
return (area_intersection / float(area_union)) if area_union > 0 else 0
def intersection_over_union(self, bb):
"""Alias for iou"""
return self.iou(bb)
def area_of_intersection(self, bb, strict=True):
"""area of intersection"""
if strict:
assert isinstance(bb, BoundingBox), "Invalid BoundingBox() input of type '%s'" % str(type(bb))
w = min(self._xmax, bb._xmax) - max(self._xmin, bb._xmin)
if w <= 0:
return 0 # invalid (no overlap), early exit
h = min(self._ymax, bb._ymax) - max(self._ymin, bb._ymin)
if h <= 0:
return 0 # invalid (no overlap), early exit
return w*h
def area_of_union(self, bb):
return self.area() + bb.area() - self.area_of_intersection(bb)
def cover(self, bb):
"""Fraction of this bounding box intersected by other bbox (bb).
.. note::
- Cover is often more useful than `vipy.geometry.BoundingBox.iou` as a measure of overlap due to bounding box distortion from partially occluded object proposals.
- For example, an object proposal of a person may generate a smaller box (e.g. just the torso) when the lower body is occluded whereas a track will have the full body box.
- `vipy.geometry.BoundingBox.maxcover` is a better measure of assignment in this case.
"""
a = float(self._area())
return (self.area_of_intersection(bb) / a) if a>0 else 0
def maxcover(self, bb, area=None, otherarea=None):
"""The maximum cover of self to bb and bb to self"""
aoi = self.area_of_intersection(bb, strict=False)
(area, otherarea) = (self.area() if area is None else area, bb.area() if otherarea is None else otherarea)
return float(max((aoi/area) if area>0 else 0, (aoi/otherarea) if otherarea>0 else 0))
def shapeiou(self, bb, area=None, otherarea=None):
"""Shape IoU is the IoU with the upper left corners aligned. This measures the deformation of the two boxes by removing the effect of translation"""
#return self.iou(bb.clone().translate(dx=self._xmin-bb._xmin, dy=self._ymin-bb._ymin)) # equivalent to
assert isinstance(bb, BoundingBox), "Invalid input - must be BoundingBox()"
w = min(self._xmax, bb._xmax + (self._xmin-bb._xmin)) - max(self._xmin, bb._xmin + (self._xmin-bb._xmin))
h = min(self._ymax, bb._ymax + (self._ymin-bb._ymin)) - max(self._ymin, bb._ymin + (self._ymin-bb._ymin))
area_intersection = w * h
area_union = ((self.area() if area is None else area) +
(bb.area() if otherarea is None else otherarea)
- area_intersection)
return (area_intersection / float(area_union)) if area_union>0 else 0
def intersection(self, bb, strict=True):
"""Intersection of two bounding boxes, throw an error on degeneracy of intersection result (if strict=True)"""
assert isinstance(bb, BoundingBox), "Invalid BoundingBox() input of type '%s'" % str(type(bb))
self._xmin = max(bb._xmin, self._xmin)
self._ymin = max(bb._ymin, self._ymin)
self._xmax = min(bb._xmax, self._xmax)
self._ymax = min(bb._ymax, self._ymax)
if strict and self.isdegenerate():
raise ValueError('Degenerate intersection for bounding boxes "%s" and "%s"' % (str(bb), str(self)))
return self
def hasintersection(self, bb, iou=None, cover=None, maxcover=None, bbcover=None, area=None, otherarea=None, gate=0):
"""Return true if self and bb overlap by any amount, or by the cover threshold (if provided) or the iou threshold (if provided). This is a convenience function that allows for shared computation for fast non-maximum suppression."""
if not (((self._xmax if self._xmax < bb._xmax else bb._xmax) - (self._xmin if self._xmin > bb._xmin else bb._xmin)) > (-gate) and
((self._ymax if self._ymax < bb._ymax else bb._ymax) - (self._ymin if self._ymin > bb._ymin else bb._ymin)) > (-gate)): # faster than min(x,y)-max(x,y)
return False # does not intersect
elif maxcover is not None or iou is not None or cover is not None or bbcover is not None:
aoi = self.area_of_intersection(bb, strict=False)
otherarea = otherarea if otherarea is not None else (bb.area() if (maxcover is not None or bbcover is not None or iou is not None) else 0)
area = area if area is not None else (self.area() if (maxcover is not None or cover is not None or iou is not None) else 0)
return (((maxcover is not None) and (max(aoi/area, aoi/otherarea) > maxcover)) or
((iou is not None) and ((aoi / (area+otherarea-aoi)) >= iou)) or
((cover is not None) and ((aoi / area) >= cover)) or
((bbcover is not None) and ((aoi / otherarea) >= bbcover)))
else:
return True
def union(self, bb):
"""Union of one or more bounding boxes with this box"""
bblist = tolist(bb)
assert all([isinstance(bb, BoundingBox) for bb in bblist]), "Invalid BoundingBox() input"
self._xmin = min([bb._xmin for bb in bblist] + [self._xmin])
self._ymin = min([bb._ymin for bb in bblist] + [self._ymin])
self._xmax = max([bb._xmax for bb in bblist] + [self._xmax])
self._ymax = max([bb._ymax for bb in bblist] + [self._ymax])
return self
def isinside(self, bb):
"""Is this boundingbox fully within the provided bounding box?"""
assert isinstance(bb, BoundingBox)
return self.hasintersection(bb) and self.cover(bb) == 1.0
def ispointinside(self, p):
"""Is the 2D point p=(x,y) inside this boundingbox, or is the p=boundingbox() inside this bounding box?"""
assert len(p) == 2, "Invalid 2D point=(x,y) input"
return (p[0] >= self._xmin) and (p[1] >= self._ymin) and (p[0] <= self._xmax) and (p[1] <= self._ymax)
def dilate(self, scale=1):
"""Change scale of bounding box keeping centroid constant"""
assert isnumber(scale), "Invalid input"
w = (self._xmax - self._xmin)
h = (self._ymax - self._ymin)
c = self._centroid()
old_x = self._xmin
old_y = self._ymin
new_x = (float(w) / 2.0) * scale
new_y = (float(h) / 2.0) * scale
self._xmin = c[0] - new_x
self._ymin = c[1] - new_y
self._xmax = c[0] + new_x
self._ymax = c[1] + new_y
return self
def dilatepx(self, px):
"""Dilate by a given pixel amount on all sides, keeping centroid constant"""
self._xmin = self._xmin - px
self._ymin = self._ymin - px
self._xmax = self._xmax + px
self._ymax = self._ymax + px
return self
def dilate_height(self, scale=1):
"""Change scale of bounding box in y direction keeping centroid constant"""
h = self._height()
c = self._centroid()
self._ymin = c[1] - (float(h) / 2.0) * scale
self._ymax = c[1] + (float(h) / 2.0) * scale
return self
def dilate_width(self, scale=1):
"""Change scale of bounding box in x direction keeping centroid constant"""
w = self._xmax - self._xmin
c = self._centroid()
self._xmin = c[0] - (float(w) / 2.0) * scale
self._xmax = c[0] + (float(w) / 2.0) * scale
return self
def top(self, dy):
"""Make top of box taller (closer to top of image) by an offset dy"""
self._ymin = self._ymin - dy
return self
def bottom(self, dy):
"""Make bottom of box taller (closer to bottom of image) by an offset dy"""
self._ymax = self._ymax + dy
return self
def left(self, dx):
"""Make left of box wider (closer to left side of image) by an offset dx"""
self._xmin = self._xmin - dx
return self
def right(self, dx):
"""Make right of box wider (closer to right side of image) by an offset dx"""
self._xmax = self._xmax + dx
return self
def rescale(self, scale=1):
"""Multiply the box corners by a scale factor"""
self._xmin = scale * self._xmin
self._ymin = scale * self._ymin
self._xmax = scale * self._xmax
self._ymax = scale * self._ymax
return self
def scalex(self, scale=1):
"""Multiply the box corners in the x dimension by a scale factor"""
self._xmin = scale * self._xmin
self._xmax = scale * self._xmax
return self
def scaley(self, scale=1):
"""Multiply the box corners in the y dimension by a scale factor"""
self._ymin = scale * self._ymin
self._ymax = scale * self._ymax
return self
def resize(self, width, height):
"""Change the aspect ratio width and height of the box"""
self.setwidth(width)
self.setheight(height)
return self
def rot90cw(self, H, W):
"""Rotate a bounding box such that if an image of size (H,W) is rotated 90 deg clockwise, the boxes align"""
(x,y,w,h) = self.xywh()
(blx, bly) = self.bottomleft()
return self.xywh((H - bly, blx, h, w))
def rot90ccw(self, H, W):
"""Rotate a bounding box such that if an image of size (H,W) is rotated 90 deg clockwise, the boxes align"""
(x,y,w,h) = self.xywh()
(urx, ury) = self.upperright()
return self.xywh((ury, W - urx, h, w))
def fliplr(self, img=None, width=None):
"""Flip the box left/right consistent with fliplr of the provided img (or consistent with the image width)"""
if img is not None:
assert isnumpy(img), "Invalid numpy image input"
width = img.shape[1]
else:
assert isnumber(width), "Invalid width"
(x,y,w,h) = self.xywh()
self._xmin = width - self._xmax
self._xmax = self._xmin + w
return self
def flipud(self, img=None, height=None):
"""Flip the box up/down consistent with flipud of the provided img (or consistent with the image height)"""
if img is not None:
assert isnumpy(img), "Invalid numpy image input"
height = img.shape[0]
else:
assert height is not None and isnumber(height), "Invalid height"
(x,y,w,h) = self.xywh()
self._ymin = height - self._ymax
self._ymax = self._ymin + h
return self
def imscale(self, im):
"""Given a vipy.image object im, scale the box to be within [0,1], relative to height and width of image"""
w = (1.0 / float(im.width()))
h = (1.0 / float(im.height()))
self._xmin = w * self._xmin
self._ymin = h * self._ymin
self._xmax = w * self._xmax
self._ymax = h * self._ymax
return self
def maxsquare(self):
"""Set the bounding box to be square by setting width and height to the maximum dimension of the box, keeping centroid constant"""
(height, width) = (self._ymax-self._ymin, self._xmax-self._xmin)
if width != height:
dim = float(max(width, height))
c = self._centroid()
self._xmin = c[0] - (dim / 2.0)
self._ymin = c[1] - (dim / 2.0)
self._xmax = c[0] + (dim / 2.0)
self._ymax = c[1] + (dim / 2.0)
return self
def maxsquareif(self, do):
return self.maxsquare() if do else self
def issquare(self):
return np.allclose(self._height(), self._width())
def iseven(self):
"""Are all corners even number integers?"""
return (isinstance(self.xmin(), int) and self.xmin() % 2 == 0 and
isinstance(self.ymin(), int) and self.ymin() % 2 == 0 and
isinstance(self.xmax(), int) and self.xmax() % 2 == 0 and
isinstance(self.ymax(), int) and self.ymax() % 2 == 0)
def even(self):
"""Force all corners to be even number integers. This is helpful for FFMPEG crop filters."""
self.int()
self._xmin = (self._xmin // 2) * 2
self._ymin = (self._ymin // 2) * 2
self._xmax = (self._xmax // 2) * 2
self._ymax = (self._ymax // 2) * 2
return self
def minsquare(self):
"""Set the bounding box to be square by setting width and height to the minimum dimension of the box, keeping centroid constant"""
(height, width) = (self._ymax-self._ymin, self._xmax-self._xmin)
if width != height:
dim = float(min(width, height))
c = self._centroid()
self._xmin = c[0] - (dim / 2.0)
self._ymin = c[1] - (dim / 2.0)
self._xmax = c[0] + (dim / 2.0)
self._ymax = c[1] + (dim / 2.0)
return self
def hasoverlap(self, img=None, width=None, height=None):
"""Does the bounding box intersect with the provided image rectangle?"""
if img is not None:
assert isnumpy(img), "Invalid image input"
(width, height) = (img.shape[1], img.shape[0])
else:
assert width is not None and height is not None, "Invalid width and height - both must be provided"
assert isnumber(width) and isnumber(height), "Invalid width and height - both must be numbers"
return self.area_of_intersection(BoundingBox(xmin=0, ymin=0, width=width, height=height)) > 0
def isinterior(self, width, height, border=1.0):
"""Is this boundingbox fully within the provided image rectangle?
* If border in [0,1], then the image is dilated by a border percentage prior to computing interior, useful to check if self is near the image edge
* If border=0.8, then the image rectangle is dilated by 80% (smaller) keeping the centroid constant.
"""
assert border > 0 and border <= 1, "Border must be a dilation fraction of the image, such that the image centroid is constant and the sides are dilated by a scale [0,1]"
return self.isinside(imagebox((height, width)).dilate(border))
def iminterior(self, W, H):
"""Transform bounding box to be interior to the image rectangle with shape (W,H).
Transform is applyed by computing smallest (dx,dy) translation that it is interior to the image rectangle, then clip to the image rectangle if it is too big to fit
"""
assert self.intersection(BoundingBox(xmin=0, ymin=0, width=W, height=H)).area() > 0, "Bounding box must intersect image rectangle"
self.translate(dx=0 if self.xmin()>0 else -self.xmin(),
dy=0 if self.ymin()>0 else -self.ymin())
self.translate(dx=0 if self.xmax()<W else -(W-self.xmax()),
dy=0 if self.ymax()<H else -(H-self.ymax()))
return self.imclip(width=W, height=H)
def imclip(self, img=None, width=None, height=None):
"""Clip bounding box to image rectangle [0,0,width,height] or img.shape=(width, height) and, throw an exception on an invalid box"""
if img is not None:
assert isnumpy(img), "Invalid numpy image input"
(height, width) = (img.shape[0], img.shape[1])
else:
assert width is not None and height is not None, "Invalid width and height - both must be provided"
assert isnumber(width) and isnumber(height), "Invalid width and height - both must be numbers"
return self.intersection(BoundingBox(xmin=0, ymin=0, width=width, height=height), strict=True)
def imclipshape(self, W, H):
"""Clip bounding box to image rectangle [0,0,W-1,H-1], throw an exception on an invalid box"""
return self.imclip(width=W, height=H)
def convexhull(self, fr):
"""Given a set of points [[x1,y1],[x2,xy],...], return the bounding rectangle, typecast to float"""
self._xmin = float(np.min(fr[:,0]))
self._ymin = float(np.min(fr[:,1]))
self._xmax = float(np.max(fr[:,0]))
self._ymax = float(np.max(fr[:,1]))
return self
def aspectratio(self):
"""Return the aspect ratio (width/height) of the box"""
(height, width) = (self._ymax-self._ymin, self._xmax-self._xmin)
assert height > 0
return float(width) / float(height)
def shape(self):
"""Return the (height, width) tuple for the box shape"""
return (self._ymax-self._ymin, self._xmax-self._xmin)
def _shape(self):
"""Return the (height, width) tuple for the box shape"""
return (self._ymax-self._ymin, self._xmax-self._xmin)
def mindimension(self):
"""Return min(width, height) typecast to float"""
return float(np.min(self._shape()))
def mindim(self):
"""Return min(width, height) typecast to float"""
return float(np.min(self._shape()))
def maxdim(self):
"""Return max(width, height) typecast to float"""
return float(np.max(self._shape()))
def ellipse(self):
"""Convert the boundingbox to a vipy.geometry.Ellipse object"""
(xcenter,ycenter) = self._centroid()
return Ellipse(self._width() / 2.0, self._height() / 2.0, xcenter, ycenter, 0)
def average(self, other):
"""Compute the average bounding box between self and other, and set self to the average. Other may be a singleton bounding box or a list of bounding boxes"""
assert all([isinstance(bb, BoundingBox) for bb in tolist(other)]), "Invalid input - must be BoundingBox"
return self.ulbr(np.mean( [self.ulbr()] + [bb.ulbr() for bb in tolist(other)], axis=0))
def averageshape(self, other):
"""Compute the average bounding box width and height between self and other. Other may be a singleton bounding box or a list of bounding boxes"""
assert all([isinstance(bb, BoundingBox) for bb in tolist(other)]), "Invalid input - must be BoundingBox"
(xmin, ymin, xmax, ymax) = np.mean( [self.ulbr()] + [bb.ulbr() for bb in tolist(other)], axis=0)
self.setwidth(xmax-xmin)
self.setheight(ymax-ymin)
return self
def medianshape(self, other):
"""Compute the median bounding box width and height between self and other. Other may be a singleton bounding box or a list of bounding boxes"""
assert all([isinstance(bb, BoundingBox) for bb in tolist(other)]), "Invalid input - must be BoundingBox"
(height, width) = np.median( [self._shape()] + [bb._shape() for bb in tolist(other)], axis=0)
self.setwidth(width)
self.setheight(height)
return self
def shapedist(self, other):
"""L1 distance between (width,height) of two boxes"""
assert isinstance(other, BoundingBox), "Invalid input - must be BoundingBox()"
return np.abs(self._width()-other._width()) + np.abs(self._height()-other._height())
def affine(self, A):
"""Apply an 2x3 affine transformation to the box centroid.
.. note:: This transformation is performed on the centroid and not the box corners, so the box will still be rectilinear after the transform
"""
assert isnumpy(A) and A.shape == (2,3), "A must be a 2x3 affine transformation matrix"
return self._centroid(np.dot(A, homogenize(np.array(self._centroid()))))
def projective(self, A):
"""Apply an 3x3 projective transformation to the box centroid.
.. note:: This transformation is performed on the centroid and not the box corners, so the box will still be rectilinear after the transform
"""
assert isnumpy(A) and A.shape == (3,3), "A must be a 3x3 affine transformation matrix"
return self._centroid(dehomogenize(np.dot(A, homogenize(np.array(self._centroid())))))
def crop(self, img):
"""Crop an HxW 2D numpy image, HxWxC 3D numpy image, or NxHxWxC 4D numpy image array using this bounding box applied to HxW dimensions. Crop is performed in-place. """
assert isnumpy(img) and img.ndim in [2,3,4]
assert self.isinteger(), "Box corners must be integer - try calling self.int()"
if img.ndim == 2:
return img[self.ymin():self.ymax(), self.xmin():self.xmax()] # HxW
elif img.ndim == 3:
return img[self.ymin():self.ymax(), self.xmin():self.xmax(), :] # HxWxC
else:
return img[:, self.ymin():self.ymax(), self.xmin():self.xmax(), :] # NxHxWxC
def confidence(self):
"""Bounding boxes do not have confidences, use vipy.object.Detection()"""
return None
def grid(self, rows, cols):
"""Split a bounding box into the smallest grid of non-overlapping bounding boxes such that the union is the original box"""
(w,h) = (self.width()/cols, self.height()/rows)
return [BoundingBox(xmin=x, ymin=y, width=w, height=h) for x in np.arange(self._xmin, self._xmax, w) for y in np.arange(self._ymin, self._ymax, h)]
class Ellipse():
__slots__ = ['_major', '_minor', '_xcenter', '_ycenter', '_phi']
def __init__(self, semi_major, semi_minor, xcenter, ycenter, phi):
"""Ellipse parameterization, for length of semimajor (half width of ellipse) and semiminor axis (half height), center point and angle phi in radians"""
self._major = semi_major
self._minor = semi_minor
self._xcenter = xcenter
self._ycenter = ycenter
self._phi = phi
def __repr__(self):
return str('<vipy.geometry.ellipse: semimajor=%s, semiminor=%s, xcenter=%s, ycenter=%s, phi=%s (rad)>' % (self._major, self._minor, self._xcenter, self._ycenter, self._phi))
def dict(self):
return {'semimajor':self._major, 'semiminor':self._minor, 'xcenter':self._xcenter, 'ycenter':self._ycenter, 'phi':self._phi}
def area(self):
"""Area of ellipse"""
return math.pi * self._major * self._minor
def center(self):
"""Return centroid"""
return (self._xcenter, self._ycenter)
def centroid(self):
"""Alias for center"""
return self.center()
def _centroid(self):
"""Alias for center useful for overloaded methods"""
return (self._xcenter, self._ycenter)
def axes(self):
"""Return the (major,minor) axis lengths"""
return (self._major, self._minor)
def angle(self):
"""Return the angle phi (in degrees)"""
return (self._phi * 180 / math.pi)
def rescale(self, scale):
"""Scale ellipse by scale factor"""
assert isnumber(scale), "Invalid input"
self._major *= scale
self._minor *= scale
self._xcenter *= scale
self._ycenter *= scale
return self
def boundingbox(self):
""" Estimate an equivalent bounding box based on scaling to a common area.
Note, this does not factor in rotation.
(c*l)*(c*w) = a_e --> c = sqrt(a_e / a_r) """
assert self._phi == 0, "This function does not currently factor in rotation"
bbox = BoundingBox(width=2 * self._major, height=2 * self._minor, xcentroid=self._xcenter, ycentroid=self._ycenter)
a_r = bbox.area()
c = (self.area() / a_r) ** 0.5
bbox2 = bbox.clone().dilate(c)
return bbox2
def inside(self, x, y=None):
"""Return true if a point p=(x,y) is inside the ellipse"""
p = (x,y) if y is not None else x
assert len(p) == 2, "Invalid input"
assert self._phi == 0, "inside only currently supported for phi=0"
return ((np.square(p[0] - self._xcenter) / np.square(self._major)) + (np.square(p[1] - self._ycenter) / np.square(self._minor))) <= 1
def mask(self):
"""Return a binary mask of size equal to the bounding box such that the pixels correspond to the interior of the ellipse"""
(H,W) = (int(np.round(2 * self._minor)), int(np.round(2 * self._major)))
img = np.zeros((H,W), dtype=bool)
for (y,x) in product(range(0,H), range(0,W)):
img[y,x] = self.inside(x,y)
return img
def union(bblist):
"""Return the union of a list of vipy.geometry.BoundingBox"""
return bblist[0].clone().union(bblist)
def RandomBox():
"""Return a random `vipy.geometry.BoundindBox` for unit testing"""
return BoundingBox(xmin=np.random.rand(), ymin=np.random.rand(), width=10*np.random.rand(), height=10*np.random.rand())
Functions
def RandomBox()
-
Return a random
vipy.geometry.BoundindBox
for unit testingExpand source code Browse git
def RandomBox(): """Return a random `vipy.geometry.BoundindBox` for unit testing""" return BoundingBox(xmin=np.random.rand(), ymin=np.random.rand(), width=10*np.random.rand(), height=10*np.random.rand())
def affine_transform(txy=(0, 0), r=0, sx=1, sy=1, kx=0, ky=0)
-
Compose and return a 3x3 affine transformation for translation txy=(0,0), rotation r (radians), scalex=sx, scaley=sy, shearx=kx, sheary=ky.
Usage:
A = vipy.geometry.affine_transform(r=np.pi/4) vipy.image.Image(array=vipy.geometry.imtransform(im.array(), A), colorspace='float')
Equivalently:
im = vipy.image.RandomImage().affine_transform(A)
Expand source code Browse git
def affine_transform(txy=(0,0), r=0, sx=1, sy=1, kx=0, ky=0): """Compose and return a 3x3 affine transformation for translation txy=(0,0), rotation r (radians), scalex=sx, scaley=sy, shearx=kx, sheary=ky. Usage: ```python A = vipy.geometry.affine_transform(r=np.pi/4) vipy.image.Image(array=vipy.geometry.imtransform(im.array(), A), colorspace='float') ``` Equivalently: ```python im = vipy.image.RandomImage().affine_transform(A) ``` """ assert istuple(txy) and len(txy) == 2 and isnumber(r) and isnumber(sx) and isnumber(sy) and isnumber(kx) and isnumber(ky), "Invalid input" R = np.mat([[np.cos(r), -np.sin(r), 0], [np.sin(r), np.cos(r), 0], [0,0,1]]) S = np.mat([[sx,0,0], [0, sy, 0], [0,0,1]]) K = np.mat([[1,ky,0], [kx,1,0], [0,0,1]]) T = np.mat([[0,0,txy[0]], [0,0,txy[1]], [0,0,0]]) return K * S * R + T # composition
def apply_homography(H, p)
-
Apply a 3x3 homography H to non-homogenous point p and return a transformed point
Expand source code Browse git
def apply_homography(H,p): """Apply a 3x3 homography H to non-homogenous point p and return a transformed point """ assert isnumpy(H) and isnumpy(p) and H.shape == (3,3) and p.shape[0] == 2, "Invalid input" return dehomogenize(np.dot(H, homogenize(p)))
def covariance_to_ellipse(cov)
-
2x2 covariance matrix to ellipse (major_axis_length, minor_axis_length, angle_in_radians)
Expand source code Browse git
def covariance_to_ellipse(cov): """2x2 covariance matrix to ellipse (major_axis_length, minor_axis_length, angle_in_radians)""" assert isnumpy(cov) and cov.shape == (2,2), "Invalid input" (d,V) = np.linalg.eig(cov) return np.array((d[0], d[1], math.atan2(V[1,0], V[0,0]))) # (major_axis_len, minor_axis_len, angle_in_radians)
def dehomogenize(p)
-
Convert 3x1 homogenous point (x,y,h) to 2x1 non-homogenous point (x/h, y/h)
Expand source code Browse git
def dehomogenize(p): """Convert 3x1 homogenous point (x,y,h) to 2x1 non-homogenous point (x/h, y/h)""" assert isnumpy(p) if p.ndim == 1: assert len(p) == 3 return p[0:2] / p[2] elif p.ndim == 2: assert isnumpy(p) and p.shape[0] == 3, "Invalid input" p = columnvector(p) if p.ndim == 1 else p return p[0:-1, :] / p[-1,:] else: return ValueError('p must be 1d or 2d')
def homogenize(p)
-
Convert 2xN non-homogenous points (x,y) to 3xN non-homogenous point (x, y, 1)
Expand source code Browse git
def homogenize(p): """Convert 2xN non-homogenous points (x,y) to 3xN non-homogenous point (x, y, 1)""" assert isnumpy(p) if p.ndim == 1: return np.hstack( (p, 1) ) elif p.ndim == 2: assert p.shape[0] == 2, "Invalid input" p = columnvector(p) if p.ndim == 1 else p return np.vstack((p, np.ones_like(p[-1]))) else: return ValueError('p must be 1d or 2d')
def imagebox(shape)
-
Expand source code Browse git
def imagebox(shape): return BoundingBox(xmin=0, ymin=0, width=shape[1], height=shape[0])
def imtransform(img, A, border='zero')
-
Transform an numpy array image (MxNx3) following the affine or similiarity transformation A
Expand source code Browse git
def imtransform(img, A, border='zero'): """Transform an numpy array image (MxNx3) following the affine or similiarity transformation A""" assert isnumpy(img) and isnumpy(A), "invalid input" assert border in ['zero', 'replicate'] try_import('cv2', 'opencv-python'); import cv2 borderMode = cv2.BORDER_REPLICATE if border=='replicate' else cv2.BORDER_CONSTANT if A.shape == (3,3): return cv2.warpPerspective(img, A, (img.shape[1], img.shape[0]), borderMode=borderMode) else: return cv2.warpAffine(img, A, (img.shape[1], img.shape[0]), borderMode=borderMode)
def normalize(x, eps=1e-16)
-
Given a vector x, return the vector unit normalized as float64
Expand source code Browse git
def normalize(x, eps=1E-16): """Given a vector x, return the vector unit normalized as float64""" assert isnumpy(x), "Invalid input" return x / (np.linalg.norm(x.astype(np.float64)) + eps)
def random_affine_transform(txy=((0, 1), (0, 1)), r=(0, 1), sx=(0.1, 1), sy=(0.1, 1), kx=(0.1, 1), ky=(0.1, 1))
-
Return a random 3x3 affine transformation matrix for the provided ranges, inputs must be tuples
Expand source code Browse git
def random_affine_transform(txy=((0,1),(0,1)), r=(0,1), sx=(0.1,1), sy=(0.1,1), kx=(0.1,1), ky=(0.1,1)): """Return a random 3x3 affine transformation matrix for the provided ranges, inputs must be tuples""" assert istuple(txy) and istuple(txy[0]) and istuple(txy[1]) and istuple(r) and istuple(sx) and istuple(sy) and istuple(kx) and istuple(ky), "Invalid input" uniform_random_in_range = lambda t: np.random.uniform(t[0], t[1]) return affine_transform(txy=(uniform_random_in_range(txy[0]), uniform_random_in_range(txy[1])), r=uniform_random_in_range(r), sx=uniform_random_in_range(sx), sy=uniform_random_in_range(sy), kx=uniform_random_in_range(kx), ky=uniform_random_in_range(ky))
def similarity_transform(txy=(0, 0), r=0, s=1)
-
Return a 3x3 similarity transformation with translation tuple txy=(x,y), rotation r (radians, scale=s
Expand source code Browse git
def similarity_transform(txy=(0,0), r=0, s=1): """Return a 3x3 similarity transformation with translation tuple txy=(x,y), rotation r (radians, scale=s""" assert istuple(txy) and len(txy) == 2 and isnumber(r) and isnumber(s), "Invalid input" R = np.mat([[np.cos(r), -np.sin(r), 0], [np.sin(r), np.cos(r), 0], [0,0,1]]) S = np.mat([[s,0,0], [0, s, 0], [0,0,1]]) T = np.mat([[0,0,txy[0]], [0,0,txy[1]], [0,0,0]]) return S * R + T # composition
def similarity_transform_2x3(c=(0, 0), r=0, s=1)
-
Return a 2x3 similarity transform with rotation r (radians), scale s and origin c=(x,y)
Expand source code Browse git
def similarity_transform_2x3(c=(0,0), r=0, s=1): """Return a 2x3 similarity transform with rotation r (radians), scale s and origin c=(x,y)""" assert istuple(c) and len(c) == 2 and isnumber(r) and isnumber(s), "Invalid input" deg = r * 180. / math.pi a = s * np.cos(r) b = s * np.sin(r) (x,y) = (c[0], c[1]) return np.array([[a, b, (1 - a) * x - b * y], [-b, a, b * x + (1 - a) * y]])
def union(bblist)
-
Return the union of a list of vipy.geometry.BoundingBox
Expand source code Browse git
def union(bblist): """Return the union of a list of vipy.geometry.BoundingBox""" return bblist[0].clone().union(bblist)
Classes
class BoundingBox (xmin=None, ymin=None, xmax=None, ymax=None, centroid=None, xcentroid=None, ycentroid=None, width=None, height=None, mask=None, xywh=None, ulbr=None, ulbrdict=None)
-
Core bounding box class with flexible constructors in this priority order: (xmin,ymin,xmax,ymax) (xmin,ymin,width,height) (centroid[0],centroid[1],width,height) (xcentroid,ycentroid,width,height) xywh=(xmin,ymin,width,height) ulbr=(xmin,ymin,xmax,ymax) bounding rectangle of binary mask image
Expand source code Browse git
class BoundingBox(object): """Core bounding box class with flexible constructors in this priority order: (xmin,ymin,xmax,ymax) (xmin,ymin,width,height) (centroid[0],centroid[1],width,height) (xcentroid,ycentroid,width,height) xywh=(xmin,ymin,width,height) ulbr=(xmin,ymin,xmax,ymax) bounding rectangle of binary mask image""" #__slots__ = ['_xmin', '_ymin', '_xmax', '_ymax'] # This is not backwards compatible def __init__(self, xmin=None, ymin=None, xmax=None, ymax=None, centroid=None, xcentroid=None, ycentroid=None, width=None, height=None, mask=None, xywh=None, ulbr=None, ulbrdict=None): if ulbrdict is not None: self.__dict__ = ulbrdict # equivalent to (but faster) #self._xmin = ulbrdict['_xmin'] #self._ymin = ulbrdict['_ymin'] #self._xmax = ulbrdict['_xmax'] #self._ymax = ulbrdict['_ymax'] elif xmin is not None and ymin is not None and xmax is not None and ymax is not None: if not (isnumber(xmin) and isnumber(ymin) and isnumber(xmax) and isnumber(ymax)): raise ValueError('Box coordinates must be integers or floats not "%s"' % str(type(xmin))) self._xmin = float(xmin) self._ymin = float(ymin) self._xmax = float(xmax) self._ymax = float(ymax) elif xmin is not None and ymin is not None and width is not None and height is not None: if not (isnumber(xmin) and isnumber(ymin) and isnumber(width) and isnumber(height)): raise ValueError('Box coordinates must be integers or floats not "%s"' % str(type(width))) self._xmin = float(xmin) self._ymin = float(ymin) self._xmax = self._xmin + float(width) self._ymax = self._ymin + float(height) elif centroid is not None and width is not None and height is not None: if not (len(centroid) == 2 and isnumber(centroid[0]) and isnumber(centroid[1]) and isnumber(width) and isnumber(height)): raise ValueError('Invalid box coordinates') self._xmin = float(centroid[0]) - float(width) / 2.0 self._ymin = float(centroid[1]) - float(height) / 2.0 self._xmax = float(centroid[0]) + float(width) / 2.0 self._ymax = float(centroid[1]) + float(height) / 2.0 elif xcentroid is not None and ycentroid is not None and width is not None and height is not None: #if not (isnumber(xcentroid) and isnumber(ycentroid) and isnumber(width) and isnumber(height)): # raise ValueError('Box coordinates must be integers or floats') self._xmin = float(xcentroid) - (float(width) / 2.0) self._ymin = float(ycentroid) - (float(height) / 2.0) self._xmax = float(xcentroid) + (float(width) / 2.0) self._ymax = float(ycentroid) + (float(height) / 2.0) elif xywh is not None: self.xywh(xywh) elif ulbr is not None: self.ulbr(ulbr) elif mask is not None: # Bounding rectangle of non-zero pixels in a binary mask image if not isnumpy(mask) or np.sum(mask) == 0: raise ValueError('Mask input must be numpy array with at least one non-zero entry') imx = np.sum(mask, axis=0) imy = np.sum(mask, axis=1) self._xmin = np.argwhere(imx > 0)[0] self._ymin = np.argwhere(imy > 0)[0] self._xmax = np.argwhere(imx > 0)[-1] self._ymax = np.argwhere(imy > 0)[-1] else: raise ValueError('invalid constructor input') @classmethod def cast(cls, bb, flush=False): assert isinstance(bb, BoundingBox) bb.__class__ = BoundingBox if flush: bb.__dict__ = {k:v for (k,v) in bb.__dict__.items() if k in ['_xmin', '_ymin', '_xmax', '_ymax']} return bb @classmethod def from_json(cls, s): d = json.loads(s) if not isinstance(s, dict) else s d = {'_'+k if not k.startswith('_') else k:v for (k,v) in d.items()} # from prettyjson (add "_" prefix to attributes) return cls(ulbrdict=d) def dict(self): """Return a python dictionary containing the relevant serialized attributes suitable for JSON encoding""" return self.json(encode=False) def __json__(self): """Serialization method for json package""" return self.json(encode=True) def json(self, encode=True): d = {k.lstrip('_'):v for (k,v) in self.__dict__.items()} # prettyjson (remove "_" prefix to attributes) return json.dumps(d) if encode else d def clone(self): return BoundingBox(xmin=self._xmin, xmax=self._xmax, ymin=self._ymin, ymax=self._ymax) def bbclone(self): return BoundingBox(xmin=self._xmin, xmax=self._xmax, ymin=self._ymin, ymax=self._ymax) def __eq__(self, other): """Bounding box equality (integer resolution of corners)""" return isinstance(other, BoundingBox) and self.clone().int().xywh() == other.clone().int().xywh() def __neq__(self, other): """Bounding box non-equality""" return not self.__eq__(other) def __repr__(self): return str('<vipy.geometry.boundingbox: xmin=%s, ymin=%s, width=%s, height=%s>' % (self.xmin(), self.ymin(), self._width(), self._height())) def __str__(self): return self.__repr__() def xmin(self, x=None): """x coordinate of upper left corner of box, x-axis is image column""" self._xmin = self._xmin if x is None else x return self._xmin if x is None else self def ul(self): """Upper left coordinate (x,y)""" return (self._xmin, self._ymin) def ulx(self): """Upper left coordinate (x)""" return self.ul()[0] def uly(self): """Upper left coordinate (y)""" return self.ul()[1] def ur(self): """Upper right coordinate (x,y)""" return (self._xmax, self._ymin) def urx(self): """Upper right coordinate (x)""" return self.ur()[0] def ury(self): """Upper right coordinate (y)""" return self.ur()[1] def ll(self): """Lower left coordinate (x,y), synonym for bl()""" return (self._xmin, self._ymax) def bl(self): """Bottom left coordinate (x,y), synonym for ll()""" return (self._xmin, self._ymax) def blx(self): """Bottom left coordinate (x)""" return self.bl()[0] def bly(self): """Bottom left coordinate (y)""" return self.bl()[1] def lr(self): """Lower right coordinate (x,y), synonym for br()""" return (self._xmax, self._ymax) def br(self): """Bottom right coordinate (x,y), synonym for lr()""" return (self._xmax, self._ymax) def brx(self): """Bottom right coordinate (x)""" return self.br()[0] def bry(self): """Bottom right coordinate (y)""" return self.br()[1] def ymin(self, y=None): """y coordinate of upper left corner of box, y-axis is image row, set if provided""" self._ymin = self._ymin if y is None else y return self._ymin if y is None else self def xmax(self, x=None): """x coordinate of lower right corner of box, x-axis is image column""" self._xmax = self._xmax if x is None else x return self._xmax if x is None else self def ymax(self, y=None): """y coordinate of lower right corner of box, y-axis is image row""" self._ymax = self._ymax if y is None else y return self._ymax if y is None else self def upperleft(self): """Return the (x,y) upper left corner coordinate of the box""" return (self.xmin(), self.ymin()) def bottomleft(self): """Return the (x,y) lower left corner coordinate of the box""" return (self.xmin(), self.ymax()) def upperright(self): """Return the (x,y) upper right corner coordinate of the box""" return (self.xmax(), self.ymin()) def bottomright(self): """Return the (x,y) lower right corner coordinate of the box""" return (self.xmax(), self.ymax()) def isinteger(self): return (isinstance(self._xmin, int) and isinstance(self._ymin, int) and isinstance(self._xmax, int) and isinstance(self._ymax, int)) def int(self): """Convert corners to integer with rounding, in-place update""" (w,h) = (int(np.round(self._width())), int(np.round(self._height()))) self._xmin = int(np.round(self._xmin)) self._ymin = int(np.round(self._ymin)) self._xmax = int(np.round(self._xmax)) self._ymax = int(np.round(self._ymax)) if w != self._width(): self.right(w - self._width()) # preserve aspect ratio due to rounding by +/- right side of box if h != self._height(): self.bottom(h-self._height()) # preserve aspect ratio due to rounding by +/- bottom of box return self def float(self): """Convert corners to float""" self._xmin = float(self._xmin) self._ymin = float(self._ymin) self._xmax = float(self._xmax) self._ymax = float(self._ymax) return self def significant_digits(self, n): """Convert corners to have at most n significant digits for efficient JSON storage""" assert isinstance(n, int) and n>=0 self._xmin = round(self._xmin, n) self._ymin = round(self._ymin, n) self._xmax = round(self._xmax, n) self._ymax = round(self._ymax, n) return self def translate(self, dx=0, dy=0): """Translate the bounding box by dx in x and dy in y""" self._xmin = self._xmin + dx self._ymin = self._ymin + dy self._xmax = self._xmax + dx self._ymax = self._ymax + dy return self def to_origin(self): """Translate the bounding box so that (xmin, ymin) = (0,0)""" return self.translate(-self.xmin(), -self.ymin()) def set_origin(self, other): """Set the origin of the coordinates of this bounding box to be relative to the upper left of the other bounding box""" assert isinstance(other, BoundingBox), "Invalid BoundingBox() input of type '%s'" % str(type(other)) return self.translate(other.xmin(), other.ymin()) def offset(self, dx=0, dy=0): """Alias for translate""" return self.translate(dx, dy) def invalid(self): """Is the box a valid bounding box?""" #is_undefined = np.isnan(self._xmin) or np.isnan(self._ymin) or np.isnan(self._xmax) or np.isnan(self._ymax) is_valid = ((self._xmax - self._xmin) >= 0) and ((self._ymax - self._ymin) >= 0) # if nan, will return False return not is_valid def valid(self): return not self.invalid() def isvalid(self): return not self.invalid() def isdegenerate(self): return self.invalid() def isnonnegative(self): return (self.xmin() >= 0 and self.ymin() >= 0 and self.xmax() >= 0 and self.ymax() >= 0) def width(self): return self._xmax - self._xmin def _width(self): """Alias for `vipy.geometry.BoundingBox.width`, useful for multiple inheritance with ambiguous width""" return self._xmax - self._xmin def setwidth(self, w): """Set new width keeping centroid constant""" if w <= 0: raise ValueError('invalid width') worig = (self._xmax - self._xmin) self._xmax += float((w - worig) / 2.0) self._xmin -= float((w - worig) / 2.0) return self def setheight(self, h): """Set new height keeping centroid constant""" if h <= 0: raise ValueError('invalid height') horig = self._ymax - self._ymin self._ymax += float((h - horig) / 2.0) self._ymin -= float((h - horig) / 2.0) return self def height(self): return self._ymax - self._ymin def _height(self): """Alias for `vipy.geometry.BoundingBox.height`, useful for multiple inheritance with ambiguous height""" return self._ymax - self._ymin def centroid(self, c=None): """(x,y) tuple of centroid position of bounding box""" return self._centroid(c) def _centroid(self, c=None): """(x,y) tuple of centroid position of bounding box""" if c is None: (height, width) = (self._ymax-self._ymin, self._xmax-self._xmin) return (self._xmin + (float(width) / 2.0), self._ymin + (float(height) / 2.0)) else: assert len(c) == 2 (height, width) = (self._ymax-self._ymin, self._xmax-self._xmin) self._xmin = float(c[0]) - (width / 2.0) self._ymin = float(c[1]) - (height / 2.0) self._xmax = float(c[0]) + (width / 2.0) self._ymax = float(c[1]) + (height / 2.0) return self def x_centroid(self): return self._centroid()[0] def xcentroid(self): """Alias for x_centroid()""" return self._centroid()[0] def centroid_x(self): """Alias for x_centroid()""" return self._centroid()[0] def y_centroid(self): return self._centroid()[1] def ycentroid(self): """Alias for y_centroid()""" return self._centroid()[1] def centroid_y(self): """Alias for y_centroid()""" return self._centroid()[1] def _area(self): """Return the area=width*height of the bounding box, internal method useful for multiple inheritance""" (height, width) = (self._ymax-self._ymin, self._xmax-self._xmin) return width * height if (height>0 and width>0) else 0 def area(self): """Return the area=width*height of the bounding box""" return self._area() def to_xywh(self, xywh=None): """Return bounding box corners as (x,y,width,height) tuple""" if xywh is None: (height, width) = (self._ymax-self._ymin, self._xmax-self._xmin) return tuple([self._xmin, self._ymin, width, height]) else: assert len(xywh) == 4, "Invalid (xmin,ymin,width,height) input" self._xmin = float(xywh[0]) self._ymin = float(xywh[1]) self._xmax = float(self._xmin + xywh[2]) self._ymax = float(self._ymin + xywh[3]) return self def xywh(self, xywh_=None): """Alias for to_xywh""" return self.to_xywh(xywh_) def cxywh(self, cxywh=None): """Return or set bounding box corners as (centroidx,centroidy,width,height) tuple""" if cxywh is None: (height, width) = (self._ymax-self._ymin, self._xmax-self._xmin) return tuple([self.x_centroid(), self.y_centroid(), width, height]) else: assert len(cxywh) == 4, "Invalid (xcentroid, ycentroid, width, height) input" return self._centroid( (cxywh[0], cxywh[1]) ).setwidth(cxywh[2]).setheight(cxywh[3]) def ulbr(self, ulbr=None): """Return bounding box corners as upper left, bottom right (xmin, ymin, xmax, ymax)""" if ulbr is None: return (self._xmin, self._ymin, self._xmax, self._ymax) else: assert len(ulbr) == 4, "Invalid (xmin,ymin,xmax,ymax) input" self._xmin = float(ulbr[0]) self._ymin = float(ulbr[1]) self._xmax = float(ulbr[2]) self._ymax = float(ulbr[3]) return self def to_ulbr(self, ulbr=None): """Alias for ulbr()""" return self.ulbr(ulbr) def dx(self, bb): """Offset bounding box by same xmin as provided box""" return bb._xmin - self._xmin def dy(self, bb): """Offset bounding box by ymin of provided box""" return bb._ymin - self._ymin def sqdist(self, bb): """Squared Euclidean distance between upper left corners of two bounding boxes""" assert isinstance(bb, BoundingBox), "Invalid BoundingBox() input of type '%s'" % str(type(bb)) return np.power(self.dx(bb), 2.0) + np.power(self.dy(bb), 2.0) def dist(self, bb): """Distance between centroids of two bounding boxes""" assert isinstance(bb, BoundingBox), "Invalid BoundingBox() input of type '%s'" % str(type(bb)) return np.sqrt(np.sum(np.square(np.array(bb._centroid()) - np.array(self._centroid())))) def pdist(self, bb, sigma=None): """Normalized Gaussian distance in [0,1] between centroids of two bounding boxes, where 0 is far and 1 is same with sigma=maxdim() of this box""" assert isinstance(bb, BoundingBox), "Invalid BoundingBox() input of type '%s'" % str(type(bb)) return np.exp(-self.sqdist(bb)/(float(2*self.maxdim()*self.maxdim()) if sigma is None else float(2.0*sigma*sigma))) def iou(self, bb, area=None, otherarea=None): """area of intersection / area of union""" assert bb is None or isinstance(bb, BoundingBox), "Invalid BoundingBox() input of type '%s'" % str(type(bb)) if bb is None: return 0 w = min(self._xmax, bb._xmax) - max(self._xmin, bb._xmin) if w <= 0: return 0 # invalid (no overlap), early exit h = min(self._ymax, bb._ymax) - max(self._ymin, bb._ymin) if h <= 0: return 0 # invalid (no overlap), early exit area_intersection = w * h area_union = ((self.area() if area is None else area) + (bb.area() if otherarea is None else otherarea) - area_intersection) return (area_intersection / float(area_union)) if area_union > 0 else 0 def intersection_over_union(self, bb): """Alias for iou""" return self.iou(bb) def area_of_intersection(self, bb, strict=True): """area of intersection""" if strict: assert isinstance(bb, BoundingBox), "Invalid BoundingBox() input of type '%s'" % str(type(bb)) w = min(self._xmax, bb._xmax) - max(self._xmin, bb._xmin) if w <= 0: return 0 # invalid (no overlap), early exit h = min(self._ymax, bb._ymax) - max(self._ymin, bb._ymin) if h <= 0: return 0 # invalid (no overlap), early exit return w*h def area_of_union(self, bb): return self.area() + bb.area() - self.area_of_intersection(bb) def cover(self, bb): """Fraction of this bounding box intersected by other bbox (bb). .. note:: - Cover is often more useful than `vipy.geometry.BoundingBox.iou` as a measure of overlap due to bounding box distortion from partially occluded object proposals. - For example, an object proposal of a person may generate a smaller box (e.g. just the torso) when the lower body is occluded whereas a track will have the full body box. - `vipy.geometry.BoundingBox.maxcover` is a better measure of assignment in this case. """ a = float(self._area()) return (self.area_of_intersection(bb) / a) if a>0 else 0 def maxcover(self, bb, area=None, otherarea=None): """The maximum cover of self to bb and bb to self""" aoi = self.area_of_intersection(bb, strict=False) (area, otherarea) = (self.area() if area is None else area, bb.area() if otherarea is None else otherarea) return float(max((aoi/area) if area>0 else 0, (aoi/otherarea) if otherarea>0 else 0)) def shapeiou(self, bb, area=None, otherarea=None): """Shape IoU is the IoU with the upper left corners aligned. This measures the deformation of the two boxes by removing the effect of translation""" #return self.iou(bb.clone().translate(dx=self._xmin-bb._xmin, dy=self._ymin-bb._ymin)) # equivalent to assert isinstance(bb, BoundingBox), "Invalid input - must be BoundingBox()" w = min(self._xmax, bb._xmax + (self._xmin-bb._xmin)) - max(self._xmin, bb._xmin + (self._xmin-bb._xmin)) h = min(self._ymax, bb._ymax + (self._ymin-bb._ymin)) - max(self._ymin, bb._ymin + (self._ymin-bb._ymin)) area_intersection = w * h area_union = ((self.area() if area is None else area) + (bb.area() if otherarea is None else otherarea) - area_intersection) return (area_intersection / float(area_union)) if area_union>0 else 0 def intersection(self, bb, strict=True): """Intersection of two bounding boxes, throw an error on degeneracy of intersection result (if strict=True)""" assert isinstance(bb, BoundingBox), "Invalid BoundingBox() input of type '%s'" % str(type(bb)) self._xmin = max(bb._xmin, self._xmin) self._ymin = max(bb._ymin, self._ymin) self._xmax = min(bb._xmax, self._xmax) self._ymax = min(bb._ymax, self._ymax) if strict and self.isdegenerate(): raise ValueError('Degenerate intersection for bounding boxes "%s" and "%s"' % (str(bb), str(self))) return self def hasintersection(self, bb, iou=None, cover=None, maxcover=None, bbcover=None, area=None, otherarea=None, gate=0): """Return true if self and bb overlap by any amount, or by the cover threshold (if provided) or the iou threshold (if provided). This is a convenience function that allows for shared computation for fast non-maximum suppression.""" if not (((self._xmax if self._xmax < bb._xmax else bb._xmax) - (self._xmin if self._xmin > bb._xmin else bb._xmin)) > (-gate) and ((self._ymax if self._ymax < bb._ymax else bb._ymax) - (self._ymin if self._ymin > bb._ymin else bb._ymin)) > (-gate)): # faster than min(x,y)-max(x,y) return False # does not intersect elif maxcover is not None or iou is not None or cover is not None or bbcover is not None: aoi = self.area_of_intersection(bb, strict=False) otherarea = otherarea if otherarea is not None else (bb.area() if (maxcover is not None or bbcover is not None or iou is not None) else 0) area = area if area is not None else (self.area() if (maxcover is not None or cover is not None or iou is not None) else 0) return (((maxcover is not None) and (max(aoi/area, aoi/otherarea) > maxcover)) or ((iou is not None) and ((aoi / (area+otherarea-aoi)) >= iou)) or ((cover is not None) and ((aoi / area) >= cover)) or ((bbcover is not None) and ((aoi / otherarea) >= bbcover))) else: return True def union(self, bb): """Union of one or more bounding boxes with this box""" bblist = tolist(bb) assert all([isinstance(bb, BoundingBox) for bb in bblist]), "Invalid BoundingBox() input" self._xmin = min([bb._xmin for bb in bblist] + [self._xmin]) self._ymin = min([bb._ymin for bb in bblist] + [self._ymin]) self._xmax = max([bb._xmax for bb in bblist] + [self._xmax]) self._ymax = max([bb._ymax for bb in bblist] + [self._ymax]) return self def isinside(self, bb): """Is this boundingbox fully within the provided bounding box?""" assert isinstance(bb, BoundingBox) return self.hasintersection(bb) and self.cover(bb) == 1.0 def ispointinside(self, p): """Is the 2D point p=(x,y) inside this boundingbox, or is the p=boundingbox() inside this bounding box?""" assert len(p) == 2, "Invalid 2D point=(x,y) input" return (p[0] >= self._xmin) and (p[1] >= self._ymin) and (p[0] <= self._xmax) and (p[1] <= self._ymax) def dilate(self, scale=1): """Change scale of bounding box keeping centroid constant""" assert isnumber(scale), "Invalid input" w = (self._xmax - self._xmin) h = (self._ymax - self._ymin) c = self._centroid() old_x = self._xmin old_y = self._ymin new_x = (float(w) / 2.0) * scale new_y = (float(h) / 2.0) * scale self._xmin = c[0] - new_x self._ymin = c[1] - new_y self._xmax = c[0] + new_x self._ymax = c[1] + new_y return self def dilatepx(self, px): """Dilate by a given pixel amount on all sides, keeping centroid constant""" self._xmin = self._xmin - px self._ymin = self._ymin - px self._xmax = self._xmax + px self._ymax = self._ymax + px return self def dilate_height(self, scale=1): """Change scale of bounding box in y direction keeping centroid constant""" h = self._height() c = self._centroid() self._ymin = c[1] - (float(h) / 2.0) * scale self._ymax = c[1] + (float(h) / 2.0) * scale return self def dilate_width(self, scale=1): """Change scale of bounding box in x direction keeping centroid constant""" w = self._xmax - self._xmin c = self._centroid() self._xmin = c[0] - (float(w) / 2.0) * scale self._xmax = c[0] + (float(w) / 2.0) * scale return self def top(self, dy): """Make top of box taller (closer to top of image) by an offset dy""" self._ymin = self._ymin - dy return self def bottom(self, dy): """Make bottom of box taller (closer to bottom of image) by an offset dy""" self._ymax = self._ymax + dy return self def left(self, dx): """Make left of box wider (closer to left side of image) by an offset dx""" self._xmin = self._xmin - dx return self def right(self, dx): """Make right of box wider (closer to right side of image) by an offset dx""" self._xmax = self._xmax + dx return self def rescale(self, scale=1): """Multiply the box corners by a scale factor""" self._xmin = scale * self._xmin self._ymin = scale * self._ymin self._xmax = scale * self._xmax self._ymax = scale * self._ymax return self def scalex(self, scale=1): """Multiply the box corners in the x dimension by a scale factor""" self._xmin = scale * self._xmin self._xmax = scale * self._xmax return self def scaley(self, scale=1): """Multiply the box corners in the y dimension by a scale factor""" self._ymin = scale * self._ymin self._ymax = scale * self._ymax return self def resize(self, width, height): """Change the aspect ratio width and height of the box""" self.setwidth(width) self.setheight(height) return self def rot90cw(self, H, W): """Rotate a bounding box such that if an image of size (H,W) is rotated 90 deg clockwise, the boxes align""" (x,y,w,h) = self.xywh() (blx, bly) = self.bottomleft() return self.xywh((H - bly, blx, h, w)) def rot90ccw(self, H, W): """Rotate a bounding box such that if an image of size (H,W) is rotated 90 deg clockwise, the boxes align""" (x,y,w,h) = self.xywh() (urx, ury) = self.upperright() return self.xywh((ury, W - urx, h, w)) def fliplr(self, img=None, width=None): """Flip the box left/right consistent with fliplr of the provided img (or consistent with the image width)""" if img is not None: assert isnumpy(img), "Invalid numpy image input" width = img.shape[1] else: assert isnumber(width), "Invalid width" (x,y,w,h) = self.xywh() self._xmin = width - self._xmax self._xmax = self._xmin + w return self def flipud(self, img=None, height=None): """Flip the box up/down consistent with flipud of the provided img (or consistent with the image height)""" if img is not None: assert isnumpy(img), "Invalid numpy image input" height = img.shape[0] else: assert height is not None and isnumber(height), "Invalid height" (x,y,w,h) = self.xywh() self._ymin = height - self._ymax self._ymax = self._ymin + h return self def imscale(self, im): """Given a vipy.image object im, scale the box to be within [0,1], relative to height and width of image""" w = (1.0 / float(im.width())) h = (1.0 / float(im.height())) self._xmin = w * self._xmin self._ymin = h * self._ymin self._xmax = w * self._xmax self._ymax = h * self._ymax return self def maxsquare(self): """Set the bounding box to be square by setting width and height to the maximum dimension of the box, keeping centroid constant""" (height, width) = (self._ymax-self._ymin, self._xmax-self._xmin) if width != height: dim = float(max(width, height)) c = self._centroid() self._xmin = c[0] - (dim / 2.0) self._ymin = c[1] - (dim / 2.0) self._xmax = c[0] + (dim / 2.0) self._ymax = c[1] + (dim / 2.0) return self def maxsquareif(self, do): return self.maxsquare() if do else self def issquare(self): return np.allclose(self._height(), self._width()) def iseven(self): """Are all corners even number integers?""" return (isinstance(self.xmin(), int) and self.xmin() % 2 == 0 and isinstance(self.ymin(), int) and self.ymin() % 2 == 0 and isinstance(self.xmax(), int) and self.xmax() % 2 == 0 and isinstance(self.ymax(), int) and self.ymax() % 2 == 0) def even(self): """Force all corners to be even number integers. This is helpful for FFMPEG crop filters.""" self.int() self._xmin = (self._xmin // 2) * 2 self._ymin = (self._ymin // 2) * 2 self._xmax = (self._xmax // 2) * 2 self._ymax = (self._ymax // 2) * 2 return self def minsquare(self): """Set the bounding box to be square by setting width and height to the minimum dimension of the box, keeping centroid constant""" (height, width) = (self._ymax-self._ymin, self._xmax-self._xmin) if width != height: dim = float(min(width, height)) c = self._centroid() self._xmin = c[0] - (dim / 2.0) self._ymin = c[1] - (dim / 2.0) self._xmax = c[0] + (dim / 2.0) self._ymax = c[1] + (dim / 2.0) return self def hasoverlap(self, img=None, width=None, height=None): """Does the bounding box intersect with the provided image rectangle?""" if img is not None: assert isnumpy(img), "Invalid image input" (width, height) = (img.shape[1], img.shape[0]) else: assert width is not None and height is not None, "Invalid width and height - both must be provided" assert isnumber(width) and isnumber(height), "Invalid width and height - both must be numbers" return self.area_of_intersection(BoundingBox(xmin=0, ymin=0, width=width, height=height)) > 0 def isinterior(self, width, height, border=1.0): """Is this boundingbox fully within the provided image rectangle? * If border in [0,1], then the image is dilated by a border percentage prior to computing interior, useful to check if self is near the image edge * If border=0.8, then the image rectangle is dilated by 80% (smaller) keeping the centroid constant. """ assert border > 0 and border <= 1, "Border must be a dilation fraction of the image, such that the image centroid is constant and the sides are dilated by a scale [0,1]" return self.isinside(imagebox((height, width)).dilate(border)) def iminterior(self, W, H): """Transform bounding box to be interior to the image rectangle with shape (W,H). Transform is applyed by computing smallest (dx,dy) translation that it is interior to the image rectangle, then clip to the image rectangle if it is too big to fit """ assert self.intersection(BoundingBox(xmin=0, ymin=0, width=W, height=H)).area() > 0, "Bounding box must intersect image rectangle" self.translate(dx=0 if self.xmin()>0 else -self.xmin(), dy=0 if self.ymin()>0 else -self.ymin()) self.translate(dx=0 if self.xmax()<W else -(W-self.xmax()), dy=0 if self.ymax()<H else -(H-self.ymax())) return self.imclip(width=W, height=H) def imclip(self, img=None, width=None, height=None): """Clip bounding box to image rectangle [0,0,width,height] or img.shape=(width, height) and, throw an exception on an invalid box""" if img is not None: assert isnumpy(img), "Invalid numpy image input" (height, width) = (img.shape[0], img.shape[1]) else: assert width is not None and height is not None, "Invalid width and height - both must be provided" assert isnumber(width) and isnumber(height), "Invalid width and height - both must be numbers" return self.intersection(BoundingBox(xmin=0, ymin=0, width=width, height=height), strict=True) def imclipshape(self, W, H): """Clip bounding box to image rectangle [0,0,W-1,H-1], throw an exception on an invalid box""" return self.imclip(width=W, height=H) def convexhull(self, fr): """Given a set of points [[x1,y1],[x2,xy],...], return the bounding rectangle, typecast to float""" self._xmin = float(np.min(fr[:,0])) self._ymin = float(np.min(fr[:,1])) self._xmax = float(np.max(fr[:,0])) self._ymax = float(np.max(fr[:,1])) return self def aspectratio(self): """Return the aspect ratio (width/height) of the box""" (height, width) = (self._ymax-self._ymin, self._xmax-self._xmin) assert height > 0 return float(width) / float(height) def shape(self): """Return the (height, width) tuple for the box shape""" return (self._ymax-self._ymin, self._xmax-self._xmin) def _shape(self): """Return the (height, width) tuple for the box shape""" return (self._ymax-self._ymin, self._xmax-self._xmin) def mindimension(self): """Return min(width, height) typecast to float""" return float(np.min(self._shape())) def mindim(self): """Return min(width, height) typecast to float""" return float(np.min(self._shape())) def maxdim(self): """Return max(width, height) typecast to float""" return float(np.max(self._shape())) def ellipse(self): """Convert the boundingbox to a vipy.geometry.Ellipse object""" (xcenter,ycenter) = self._centroid() return Ellipse(self._width() / 2.0, self._height() / 2.0, xcenter, ycenter, 0) def average(self, other): """Compute the average bounding box between self and other, and set self to the average. Other may be a singleton bounding box or a list of bounding boxes""" assert all([isinstance(bb, BoundingBox) for bb in tolist(other)]), "Invalid input - must be BoundingBox" return self.ulbr(np.mean( [self.ulbr()] + [bb.ulbr() for bb in tolist(other)], axis=0)) def averageshape(self, other): """Compute the average bounding box width and height between self and other. Other may be a singleton bounding box or a list of bounding boxes""" assert all([isinstance(bb, BoundingBox) for bb in tolist(other)]), "Invalid input - must be BoundingBox" (xmin, ymin, xmax, ymax) = np.mean( [self.ulbr()] + [bb.ulbr() for bb in tolist(other)], axis=0) self.setwidth(xmax-xmin) self.setheight(ymax-ymin) return self def medianshape(self, other): """Compute the median bounding box width and height between self and other. Other may be a singleton bounding box or a list of bounding boxes""" assert all([isinstance(bb, BoundingBox) for bb in tolist(other)]), "Invalid input - must be BoundingBox" (height, width) = np.median( [self._shape()] + [bb._shape() for bb in tolist(other)], axis=0) self.setwidth(width) self.setheight(height) return self def shapedist(self, other): """L1 distance between (width,height) of two boxes""" assert isinstance(other, BoundingBox), "Invalid input - must be BoundingBox()" return np.abs(self._width()-other._width()) + np.abs(self._height()-other._height()) def affine(self, A): """Apply an 2x3 affine transformation to the box centroid. .. note:: This transformation is performed on the centroid and not the box corners, so the box will still be rectilinear after the transform """ assert isnumpy(A) and A.shape == (2,3), "A must be a 2x3 affine transformation matrix" return self._centroid(np.dot(A, homogenize(np.array(self._centroid())))) def projective(self, A): """Apply an 3x3 projective transformation to the box centroid. .. note:: This transformation is performed on the centroid and not the box corners, so the box will still be rectilinear after the transform """ assert isnumpy(A) and A.shape == (3,3), "A must be a 3x3 affine transformation matrix" return self._centroid(dehomogenize(np.dot(A, homogenize(np.array(self._centroid()))))) def crop(self, img): """Crop an HxW 2D numpy image, HxWxC 3D numpy image, or NxHxWxC 4D numpy image array using this bounding box applied to HxW dimensions. Crop is performed in-place. """ assert isnumpy(img) and img.ndim in [2,3,4] assert self.isinteger(), "Box corners must be integer - try calling self.int()" if img.ndim == 2: return img[self.ymin():self.ymax(), self.xmin():self.xmax()] # HxW elif img.ndim == 3: return img[self.ymin():self.ymax(), self.xmin():self.xmax(), :] # HxWxC else: return img[:, self.ymin():self.ymax(), self.xmin():self.xmax(), :] # NxHxWxC def confidence(self): """Bounding boxes do not have confidences, use vipy.object.Detection()""" return None def grid(self, rows, cols): """Split a bounding box into the smallest grid of non-overlapping bounding boxes such that the union is the original box""" (w,h) = (self.width()/cols, self.height()/rows) return [BoundingBox(xmin=x, ymin=y, width=w, height=h) for x in np.arange(self._xmin, self._xmax, w) for y in np.arange(self._ymin, self._ymax, h)]
Subclasses
Static methods
def cast(bb, flush=False)
-
Expand source code Browse git
@classmethod def cast(cls, bb, flush=False): assert isinstance(bb, BoundingBox) bb.__class__ = BoundingBox if flush: bb.__dict__ = {k:v for (k,v) in bb.__dict__.items() if k in ['_xmin', '_ymin', '_xmax', '_ymax']} return bb
def from_json(s)
-
Expand source code Browse git
@classmethod def from_json(cls, s): d = json.loads(s) if not isinstance(s, dict) else s d = {'_'+k if not k.startswith('_') else k:v for (k,v) in d.items()} # from prettyjson (add "_" prefix to attributes) return cls(ulbrdict=d)
Methods
def affine(self, A)
-
Apply an 2x3 affine transformation to the box centroid.
Note: This transformation is performed on the centroid and not the box corners, so the box will still be rectilinear after the transform
Expand source code Browse git
def affine(self, A): """Apply an 2x3 affine transformation to the box centroid. .. note:: This transformation is performed on the centroid and not the box corners, so the box will still be rectilinear after the transform """ assert isnumpy(A) and A.shape == (2,3), "A must be a 2x3 affine transformation matrix" return self._centroid(np.dot(A, homogenize(np.array(self._centroid()))))
def area(self)
-
Return the area=width*height of the bounding box
Expand source code Browse git
def area(self): """Return the area=width*height of the bounding box""" return self._area()
def area_of_intersection(self, bb, strict=True)
-
area of intersection
Expand source code Browse git
def area_of_intersection(self, bb, strict=True): """area of intersection""" if strict: assert isinstance(bb, BoundingBox), "Invalid BoundingBox() input of type '%s'" % str(type(bb)) w = min(self._xmax, bb._xmax) - max(self._xmin, bb._xmin) if w <= 0: return 0 # invalid (no overlap), early exit h = min(self._ymax, bb._ymax) - max(self._ymin, bb._ymin) if h <= 0: return 0 # invalid (no overlap), early exit return w*h
def area_of_union(self, bb)
-
Expand source code Browse git
def area_of_union(self, bb): return self.area() + bb.area() - self.area_of_intersection(bb)
def aspectratio(self)
-
Return the aspect ratio (width/height) of the box
Expand source code Browse git
def aspectratio(self): """Return the aspect ratio (width/height) of the box""" (height, width) = (self._ymax-self._ymin, self._xmax-self._xmin) assert height > 0 return float(width) / float(height)
def average(self, other)
-
Compute the average bounding box between self and other, and set self to the average. Other may be a singleton bounding box or a list of bounding boxes
Expand source code Browse git
def average(self, other): """Compute the average bounding box between self and other, and set self to the average. Other may be a singleton bounding box or a list of bounding boxes""" assert all([isinstance(bb, BoundingBox) for bb in tolist(other)]), "Invalid input - must be BoundingBox" return self.ulbr(np.mean( [self.ulbr()] + [bb.ulbr() for bb in tolist(other)], axis=0))
def averageshape(self, other)
-
Compute the average bounding box width and height between self and other. Other may be a singleton bounding box or a list of bounding boxes
Expand source code Browse git
def averageshape(self, other): """Compute the average bounding box width and height between self and other. Other may be a singleton bounding box or a list of bounding boxes""" assert all([isinstance(bb, BoundingBox) for bb in tolist(other)]), "Invalid input - must be BoundingBox" (xmin, ymin, xmax, ymax) = np.mean( [self.ulbr()] + [bb.ulbr() for bb in tolist(other)], axis=0) self.setwidth(xmax-xmin) self.setheight(ymax-ymin) return self
def bbclone(self)
-
Expand source code Browse git
def bbclone(self): return BoundingBox(xmin=self._xmin, xmax=self._xmax, ymin=self._ymin, ymax=self._ymax)
def bl(self)
-
Bottom left coordinate (x,y), synonym for ll()
Expand source code Browse git
def bl(self): """Bottom left coordinate (x,y), synonym for ll()""" return (self._xmin, self._ymax)
def blx(self)
-
Bottom left coordinate (x)
Expand source code Browse git
def blx(self): """Bottom left coordinate (x)""" return self.bl()[0]
def bly(self)
-
Bottom left coordinate (y)
Expand source code Browse git
def bly(self): """Bottom left coordinate (y)""" return self.bl()[1]
def bottom(self, dy)
-
Make bottom of box taller (closer to bottom of image) by an offset dy
Expand source code Browse git
def bottom(self, dy): """Make bottom of box taller (closer to bottom of image) by an offset dy""" self._ymax = self._ymax + dy return self
def bottomleft(self)
-
Return the (x,y) lower left corner coordinate of the box
Expand source code Browse git
def bottomleft(self): """Return the (x,y) lower left corner coordinate of the box""" return (self.xmin(), self.ymax())
def bottomright(self)
-
Return the (x,y) lower right corner coordinate of the box
Expand source code Browse git
def bottomright(self): """Return the (x,y) lower right corner coordinate of the box""" return (self.xmax(), self.ymax())
def br(self)
-
Bottom right coordinate (x,y), synonym for lr()
Expand source code Browse git
def br(self): """Bottom right coordinate (x,y), synonym for lr()""" return (self._xmax, self._ymax)
def brx(self)
-
Bottom right coordinate (x)
Expand source code Browse git
def brx(self): """Bottom right coordinate (x)""" return self.br()[0]
def bry(self)
-
Bottom right coordinate (y)
Expand source code Browse git
def bry(self): """Bottom right coordinate (y)""" return self.br()[1]
def centroid(self, c=None)
-
(x,y) tuple of centroid position of bounding box
Expand source code Browse git
def centroid(self, c=None): """(x,y) tuple of centroid position of bounding box""" return self._centroid(c)
def centroid_x(self)
-
Alias for x_centroid()
Expand source code Browse git
def centroid_x(self): """Alias for x_centroid()""" return self._centroid()[0]
def centroid_y(self)
-
Alias for y_centroid()
Expand source code Browse git
def centroid_y(self): """Alias for y_centroid()""" return self._centroid()[1]
def clone(self)
-
Expand source code Browse git
def clone(self): return BoundingBox(xmin=self._xmin, xmax=self._xmax, ymin=self._ymin, ymax=self._ymax)
def confidence(self)
-
Bounding boxes do not have confidences, use vipy.object.Detection()
Expand source code Browse git
def confidence(self): """Bounding boxes do not have confidences, use vipy.object.Detection()""" return None
def convexhull(self, fr)
-
Given a set of points [[x1,y1],[x2,xy],…], return the bounding rectangle, typecast to float
Expand source code Browse git
def convexhull(self, fr): """Given a set of points [[x1,y1],[x2,xy],...], return the bounding rectangle, typecast to float""" self._xmin = float(np.min(fr[:,0])) self._ymin = float(np.min(fr[:,1])) self._xmax = float(np.max(fr[:,0])) self._ymax = float(np.max(fr[:,1])) return self
def cover(self, bb)
-
Fraction of this bounding box intersected by other bbox (bb).
Note
- Cover is often more useful than
BoundingBox.iou()
as a measure of overlap due to bounding box distortion from partially occluded object proposals. - For example, an object proposal of a person may generate a smaller box (e.g. just the torso) when the lower body is occluded whereas a track will have the full body box.
BoundingBox.maxcover()
is a better measure of assignment in this case.
Expand source code Browse git
def cover(self, bb): """Fraction of this bounding box intersected by other bbox (bb). .. note:: - Cover is often more useful than `vipy.geometry.BoundingBox.iou` as a measure of overlap due to bounding box distortion from partially occluded object proposals. - For example, an object proposal of a person may generate a smaller box (e.g. just the torso) when the lower body is occluded whereas a track will have the full body box. - `vipy.geometry.BoundingBox.maxcover` is a better measure of assignment in this case. """ a = float(self._area()) return (self.area_of_intersection(bb) / a) if a>0 else 0
- Cover is often more useful than
def crop(self, img)
-
Crop an HxW 2D numpy image, HxWxC 3D numpy image, or NxHxWxC 4D numpy image array using this bounding box applied to HxW dimensions. Crop is performed in-place.
Expand source code Browse git
def crop(self, img): """Crop an HxW 2D numpy image, HxWxC 3D numpy image, or NxHxWxC 4D numpy image array using this bounding box applied to HxW dimensions. Crop is performed in-place. """ assert isnumpy(img) and img.ndim in [2,3,4] assert self.isinteger(), "Box corners must be integer - try calling self.int()" if img.ndim == 2: return img[self.ymin():self.ymax(), self.xmin():self.xmax()] # HxW elif img.ndim == 3: return img[self.ymin():self.ymax(), self.xmin():self.xmax(), :] # HxWxC else: return img[:, self.ymin():self.ymax(), self.xmin():self.xmax(), :] # NxHxWxC
def cxywh(self, cxywh=None)
-
Return or set bounding box corners as (centroidx,centroidy,width,height) tuple
Expand source code Browse git
def cxywh(self, cxywh=None): """Return or set bounding box corners as (centroidx,centroidy,width,height) tuple""" if cxywh is None: (height, width) = (self._ymax-self._ymin, self._xmax-self._xmin) return tuple([self.x_centroid(), self.y_centroid(), width, height]) else: assert len(cxywh) == 4, "Invalid (xcentroid, ycentroid, width, height) input" return self._centroid( (cxywh[0], cxywh[1]) ).setwidth(cxywh[2]).setheight(cxywh[3])
def dict(self)
-
Return a python dictionary containing the relevant serialized attributes suitable for JSON encoding
Expand source code Browse git
def dict(self): """Return a python dictionary containing the relevant serialized attributes suitable for JSON encoding""" return self.json(encode=False)
def dilate(self, scale=1)
-
Change scale of bounding box keeping centroid constant
Expand source code Browse git
def dilate(self, scale=1): """Change scale of bounding box keeping centroid constant""" assert isnumber(scale), "Invalid input" w = (self._xmax - self._xmin) h = (self._ymax - self._ymin) c = self._centroid() old_x = self._xmin old_y = self._ymin new_x = (float(w) / 2.0) * scale new_y = (float(h) / 2.0) * scale self._xmin = c[0] - new_x self._ymin = c[1] - new_y self._xmax = c[0] + new_x self._ymax = c[1] + new_y return self
def dilate_height(self, scale=1)
-
Change scale of bounding box in y direction keeping centroid constant
Expand source code Browse git
def dilate_height(self, scale=1): """Change scale of bounding box in y direction keeping centroid constant""" h = self._height() c = self._centroid() self._ymin = c[1] - (float(h) / 2.0) * scale self._ymax = c[1] + (float(h) / 2.0) * scale return self
def dilate_width(self, scale=1)
-
Change scale of bounding box in x direction keeping centroid constant
Expand source code Browse git
def dilate_width(self, scale=1): """Change scale of bounding box in x direction keeping centroid constant""" w = self._xmax - self._xmin c = self._centroid() self._xmin = c[0] - (float(w) / 2.0) * scale self._xmax = c[0] + (float(w) / 2.0) * scale return self
def dilatepx(self, px)
-
Dilate by a given pixel amount on all sides, keeping centroid constant
Expand source code Browse git
def dilatepx(self, px): """Dilate by a given pixel amount on all sides, keeping centroid constant""" self._xmin = self._xmin - px self._ymin = self._ymin - px self._xmax = self._xmax + px self._ymax = self._ymax + px return self
def dist(self, bb)
-
Distance between centroids of two bounding boxes
Expand source code Browse git
def dist(self, bb): """Distance between centroids of two bounding boxes""" assert isinstance(bb, BoundingBox), "Invalid BoundingBox() input of type '%s'" % str(type(bb)) return np.sqrt(np.sum(np.square(np.array(bb._centroid()) - np.array(self._centroid()))))
def dx(self, bb)
-
Offset bounding box by same xmin as provided box
Expand source code Browse git
def dx(self, bb): """Offset bounding box by same xmin as provided box""" return bb._xmin - self._xmin
def dy(self, bb)
-
Offset bounding box by ymin of provided box
Expand source code Browse git
def dy(self, bb): """Offset bounding box by ymin of provided box""" return bb._ymin - self._ymin
def ellipse(self)
-
Convert the boundingbox to a vipy.geometry.Ellipse object
Expand source code Browse git
def ellipse(self): """Convert the boundingbox to a vipy.geometry.Ellipse object""" (xcenter,ycenter) = self._centroid() return Ellipse(self._width() / 2.0, self._height() / 2.0, xcenter, ycenter, 0)
def even(self)
-
Force all corners to be even number integers. This is helpful for FFMPEG crop filters.
Expand source code Browse git
def even(self): """Force all corners to be even number integers. This is helpful for FFMPEG crop filters.""" self.int() self._xmin = (self._xmin // 2) * 2 self._ymin = (self._ymin // 2) * 2 self._xmax = (self._xmax // 2) * 2 self._ymax = (self._ymax // 2) * 2 return self
def fliplr(self, img=None, width=None)
-
Flip the box left/right consistent with fliplr of the provided img (or consistent with the image width)
Expand source code Browse git
def fliplr(self, img=None, width=None): """Flip the box left/right consistent with fliplr of the provided img (or consistent with the image width)""" if img is not None: assert isnumpy(img), "Invalid numpy image input" width = img.shape[1] else: assert isnumber(width), "Invalid width" (x,y,w,h) = self.xywh() self._xmin = width - self._xmax self._xmax = self._xmin + w return self
def flipud(self, img=None, height=None)
-
Flip the box up/down consistent with flipud of the provided img (or consistent with the image height)
Expand source code Browse git
def flipud(self, img=None, height=None): """Flip the box up/down consistent with flipud of the provided img (or consistent with the image height)""" if img is not None: assert isnumpy(img), "Invalid numpy image input" height = img.shape[0] else: assert height is not None and isnumber(height), "Invalid height" (x,y,w,h) = self.xywh() self._ymin = height - self._ymax self._ymax = self._ymin + h return self
def float(self)
-
Convert corners to float
Expand source code Browse git
def float(self): """Convert corners to float""" self._xmin = float(self._xmin) self._ymin = float(self._ymin) self._xmax = float(self._xmax) self._ymax = float(self._ymax) return self
def grid(self, rows, cols)
-
Split a bounding box into the smallest grid of non-overlapping bounding boxes such that the union is the original box
Expand source code Browse git
def grid(self, rows, cols): """Split a bounding box into the smallest grid of non-overlapping bounding boxes such that the union is the original box""" (w,h) = (self.width()/cols, self.height()/rows) return [BoundingBox(xmin=x, ymin=y, width=w, height=h) for x in np.arange(self._xmin, self._xmax, w) for y in np.arange(self._ymin, self._ymax, h)]
def hasintersection(self, bb, iou=None, cover=None, maxcover=None, bbcover=None, area=None, otherarea=None, gate=0)
-
Return true if self and bb overlap by any amount, or by the cover threshold (if provided) or the iou threshold (if provided). This is a convenience function that allows for shared computation for fast non-maximum suppression.
Expand source code Browse git
def hasintersection(self, bb, iou=None, cover=None, maxcover=None, bbcover=None, area=None, otherarea=None, gate=0): """Return true if self and bb overlap by any amount, or by the cover threshold (if provided) or the iou threshold (if provided). This is a convenience function that allows for shared computation for fast non-maximum suppression.""" if not (((self._xmax if self._xmax < bb._xmax else bb._xmax) - (self._xmin if self._xmin > bb._xmin else bb._xmin)) > (-gate) and ((self._ymax if self._ymax < bb._ymax else bb._ymax) - (self._ymin if self._ymin > bb._ymin else bb._ymin)) > (-gate)): # faster than min(x,y)-max(x,y) return False # does not intersect elif maxcover is not None or iou is not None or cover is not None or bbcover is not None: aoi = self.area_of_intersection(bb, strict=False) otherarea = otherarea if otherarea is not None else (bb.area() if (maxcover is not None or bbcover is not None or iou is not None) else 0) area = area if area is not None else (self.area() if (maxcover is not None or cover is not None or iou is not None) else 0) return (((maxcover is not None) and (max(aoi/area, aoi/otherarea) > maxcover)) or ((iou is not None) and ((aoi / (area+otherarea-aoi)) >= iou)) or ((cover is not None) and ((aoi / area) >= cover)) or ((bbcover is not None) and ((aoi / otherarea) >= bbcover))) else: return True
def hasoverlap(self, img=None, width=None, height=None)
-
Does the bounding box intersect with the provided image rectangle?
Expand source code Browse git
def hasoverlap(self, img=None, width=None, height=None): """Does the bounding box intersect with the provided image rectangle?""" if img is not None: assert isnumpy(img), "Invalid image input" (width, height) = (img.shape[1], img.shape[0]) else: assert width is not None and height is not None, "Invalid width and height - both must be provided" assert isnumber(width) and isnumber(height), "Invalid width and height - both must be numbers" return self.area_of_intersection(BoundingBox(xmin=0, ymin=0, width=width, height=height)) > 0
def height(self)
-
Expand source code Browse git
def height(self): return self._ymax - self._ymin
def imclip(self, img=None, width=None, height=None)
-
Clip bounding box to image rectangle [0,0,width,height] or img.shape=(width, height) and, throw an exception on an invalid box
Expand source code Browse git
def imclip(self, img=None, width=None, height=None): """Clip bounding box to image rectangle [0,0,width,height] or img.shape=(width, height) and, throw an exception on an invalid box""" if img is not None: assert isnumpy(img), "Invalid numpy image input" (height, width) = (img.shape[0], img.shape[1]) else: assert width is not None and height is not None, "Invalid width and height - both must be provided" assert isnumber(width) and isnumber(height), "Invalid width and height - both must be numbers" return self.intersection(BoundingBox(xmin=0, ymin=0, width=width, height=height), strict=True)
def imclipshape(self, W, H)
-
Clip bounding box to image rectangle [0,0,W-1,H-1], throw an exception on an invalid box
Expand source code Browse git
def imclipshape(self, W, H): """Clip bounding box to image rectangle [0,0,W-1,H-1], throw an exception on an invalid box""" return self.imclip(width=W, height=H)
def iminterior(self, W, H)
-
Transform bounding box to be interior to the image rectangle with shape (W,H).
Transform is applyed by computing smallest (dx,dy) translation that it is interior to the image rectangle, then clip to the image rectangle if it is too big to fitExpand source code Browse git
def iminterior(self, W, H): """Transform bounding box to be interior to the image rectangle with shape (W,H). Transform is applyed by computing smallest (dx,dy) translation that it is interior to the image rectangle, then clip to the image rectangle if it is too big to fit """ assert self.intersection(BoundingBox(xmin=0, ymin=0, width=W, height=H)).area() > 0, "Bounding box must intersect image rectangle" self.translate(dx=0 if self.xmin()>0 else -self.xmin(), dy=0 if self.ymin()>0 else -self.ymin()) self.translate(dx=0 if self.xmax()<W else -(W-self.xmax()), dy=0 if self.ymax()<H else -(H-self.ymax())) return self.imclip(width=W, height=H)
def imscale(self, im)
-
Given a vipy.image object im, scale the box to be within [0,1], relative to height and width of image
Expand source code Browse git
def imscale(self, im): """Given a vipy.image object im, scale the box to be within [0,1], relative to height and width of image""" w = (1.0 / float(im.width())) h = (1.0 / float(im.height())) self._xmin = w * self._xmin self._ymin = h * self._ymin self._xmax = w * self._xmax self._ymax = h * self._ymax return self
def int(self)
-
Convert corners to integer with rounding, in-place update
Expand source code Browse git
def int(self): """Convert corners to integer with rounding, in-place update""" (w,h) = (int(np.round(self._width())), int(np.round(self._height()))) self._xmin = int(np.round(self._xmin)) self._ymin = int(np.round(self._ymin)) self._xmax = int(np.round(self._xmax)) self._ymax = int(np.round(self._ymax)) if w != self._width(): self.right(w - self._width()) # preserve aspect ratio due to rounding by +/- right side of box if h != self._height(): self.bottom(h-self._height()) # preserve aspect ratio due to rounding by +/- bottom of box return self
def intersection(self, bb, strict=True)
-
Intersection of two bounding boxes, throw an error on degeneracy of intersection result (if strict=True)
Expand source code Browse git
def intersection(self, bb, strict=True): """Intersection of two bounding boxes, throw an error on degeneracy of intersection result (if strict=True)""" assert isinstance(bb, BoundingBox), "Invalid BoundingBox() input of type '%s'" % str(type(bb)) self._xmin = max(bb._xmin, self._xmin) self._ymin = max(bb._ymin, self._ymin) self._xmax = min(bb._xmax, self._xmax) self._ymax = min(bb._ymax, self._ymax) if strict and self.isdegenerate(): raise ValueError('Degenerate intersection for bounding boxes "%s" and "%s"' % (str(bb), str(self))) return self
def intersection_over_union(self, bb)
-
Alias for iou
Expand source code Browse git
def intersection_over_union(self, bb): """Alias for iou""" return self.iou(bb)
def invalid(self)
-
Is the box a valid bounding box?
Expand source code Browse git
def invalid(self): """Is the box a valid bounding box?""" #is_undefined = np.isnan(self._xmin) or np.isnan(self._ymin) or np.isnan(self._xmax) or np.isnan(self._ymax) is_valid = ((self._xmax - self._xmin) >= 0) and ((self._ymax - self._ymin) >= 0) # if nan, will return False return not is_valid
def iou(self, bb, area=None, otherarea=None)
-
area of intersection / area of union
Expand source code Browse git
def iou(self, bb, area=None, otherarea=None): """area of intersection / area of union""" assert bb is None or isinstance(bb, BoundingBox), "Invalid BoundingBox() input of type '%s'" % str(type(bb)) if bb is None: return 0 w = min(self._xmax, bb._xmax) - max(self._xmin, bb._xmin) if w <= 0: return 0 # invalid (no overlap), early exit h = min(self._ymax, bb._ymax) - max(self._ymin, bb._ymin) if h <= 0: return 0 # invalid (no overlap), early exit area_intersection = w * h area_union = ((self.area() if area is None else area) + (bb.area() if otherarea is None else otherarea) - area_intersection) return (area_intersection / float(area_union)) if area_union > 0 else 0
def isdegenerate(self)
-
Expand source code Browse git
def isdegenerate(self): return self.invalid()
def iseven(self)
-
Are all corners even number integers?
Expand source code Browse git
def iseven(self): """Are all corners even number integers?""" return (isinstance(self.xmin(), int) and self.xmin() % 2 == 0 and isinstance(self.ymin(), int) and self.ymin() % 2 == 0 and isinstance(self.xmax(), int) and self.xmax() % 2 == 0 and isinstance(self.ymax(), int) and self.ymax() % 2 == 0)
def isinside(self, bb)
-
Is this boundingbox fully within the provided bounding box?
Expand source code Browse git
def isinside(self, bb): """Is this boundingbox fully within the provided bounding box?""" assert isinstance(bb, BoundingBox) return self.hasintersection(bb) and self.cover(bb) == 1.0
def isinteger(self)
-
Expand source code Browse git
def isinteger(self): return (isinstance(self._xmin, int) and isinstance(self._ymin, int) and isinstance(self._xmax, int) and isinstance(self._ymax, int))
def isinterior(self, width, height, border=1.0)
-
Is this boundingbox fully within the provided image rectangle?
- If border in [0,1], then the image is dilated by a border percentage prior to computing interior, useful to check if self is near the image edge
- If border=0.8, then the image rectangle is dilated by 80% (smaller) keeping the centroid constant.
Expand source code Browse git
def isinterior(self, width, height, border=1.0): """Is this boundingbox fully within the provided image rectangle? * If border in [0,1], then the image is dilated by a border percentage prior to computing interior, useful to check if self is near the image edge * If border=0.8, then the image rectangle is dilated by 80% (smaller) keeping the centroid constant. """ assert border > 0 and border <= 1, "Border must be a dilation fraction of the image, such that the image centroid is constant and the sides are dilated by a scale [0,1]" return self.isinside(imagebox((height, width)).dilate(border))
def isnonnegative(self)
-
Expand source code Browse git
def isnonnegative(self): return (self.xmin() >= 0 and self.ymin() >= 0 and self.xmax() >= 0 and self.ymax() >= 0)
def ispointinside(self, p)
-
Is the 2D point p=(x,y) inside this boundingbox, or is the p=boundingbox() inside this bounding box?
Expand source code Browse git
def ispointinside(self, p): """Is the 2D point p=(x,y) inside this boundingbox, or is the p=boundingbox() inside this bounding box?""" assert len(p) == 2, "Invalid 2D point=(x,y) input" return (p[0] >= self._xmin) and (p[1] >= self._ymin) and (p[0] <= self._xmax) and (p[1] <= self._ymax)
def issquare(self)
-
Expand source code Browse git
def issquare(self): return np.allclose(self._height(), self._width())
def isvalid(self)
-
Expand source code Browse git
def isvalid(self): return not self.invalid()
def json(self, encode=True)
-
Expand source code Browse git
def json(self, encode=True): d = {k.lstrip('_'):v for (k,v) in self.__dict__.items()} # prettyjson (remove "_" prefix to attributes) return json.dumps(d) if encode else d
def left(self, dx)
-
Make left of box wider (closer to left side of image) by an offset dx
Expand source code Browse git
def left(self, dx): """Make left of box wider (closer to left side of image) by an offset dx""" self._xmin = self._xmin - dx return self
def ll(self)
-
Lower left coordinate (x,y), synonym for bl()
Expand source code Browse git
def ll(self): """Lower left coordinate (x,y), synonym for bl()""" return (self._xmin, self._ymax)
def lr(self)
-
Lower right coordinate (x,y), synonym for br()
Expand source code Browse git
def lr(self): """Lower right coordinate (x,y), synonym for br()""" return (self._xmax, self._ymax)
def maxcover(self, bb, area=None, otherarea=None)
-
The maximum cover of self to bb and bb to self
Expand source code Browse git
def maxcover(self, bb, area=None, otherarea=None): """The maximum cover of self to bb and bb to self""" aoi = self.area_of_intersection(bb, strict=False) (area, otherarea) = (self.area() if area is None else area, bb.area() if otherarea is None else otherarea) return float(max((aoi/area) if area>0 else 0, (aoi/otherarea) if otherarea>0 else 0))
def maxdim(self)
-
Return max(width, height) typecast to float
Expand source code Browse git
def maxdim(self): """Return max(width, height) typecast to float""" return float(np.max(self._shape()))
def maxsquare(self)
-
Set the bounding box to be square by setting width and height to the maximum dimension of the box, keeping centroid constant
Expand source code Browse git
def maxsquare(self): """Set the bounding box to be square by setting width and height to the maximum dimension of the box, keeping centroid constant""" (height, width) = (self._ymax-self._ymin, self._xmax-self._xmin) if width != height: dim = float(max(width, height)) c = self._centroid() self._xmin = c[0] - (dim / 2.0) self._ymin = c[1] - (dim / 2.0) self._xmax = c[0] + (dim / 2.0) self._ymax = c[1] + (dim / 2.0) return self
def maxsquareif(self, do)
-
Expand source code Browse git
def maxsquareif(self, do): return self.maxsquare() if do else self
def medianshape(self, other)
-
Compute the median bounding box width and height between self and other. Other may be a singleton bounding box or a list of bounding boxes
Expand source code Browse git
def medianshape(self, other): """Compute the median bounding box width and height between self and other. Other may be a singleton bounding box or a list of bounding boxes""" assert all([isinstance(bb, BoundingBox) for bb in tolist(other)]), "Invalid input - must be BoundingBox" (height, width) = np.median( [self._shape()] + [bb._shape() for bb in tolist(other)], axis=0) self.setwidth(width) self.setheight(height) return self
def mindim(self)
-
Return min(width, height) typecast to float
Expand source code Browse git
def mindim(self): """Return min(width, height) typecast to float""" return float(np.min(self._shape()))
def mindimension(self)
-
Return min(width, height) typecast to float
Expand source code Browse git
def mindimension(self): """Return min(width, height) typecast to float""" return float(np.min(self._shape()))
def minsquare(self)
-
Set the bounding box to be square by setting width and height to the minimum dimension of the box, keeping centroid constant
Expand source code Browse git
def minsquare(self): """Set the bounding box to be square by setting width and height to the minimum dimension of the box, keeping centroid constant""" (height, width) = (self._ymax-self._ymin, self._xmax-self._xmin) if width != height: dim = float(min(width, height)) c = self._centroid() self._xmin = c[0] - (dim / 2.0) self._ymin = c[1] - (dim / 2.0) self._xmax = c[0] + (dim / 2.0) self._ymax = c[1] + (dim / 2.0) return self
def offset(self, dx=0, dy=0)
-
Alias for translate
Expand source code Browse git
def offset(self, dx=0, dy=0): """Alias for translate""" return self.translate(dx, dy)
def pdist(self, bb, sigma=None)
-
Normalized Gaussian distance in [0,1] between centroids of two bounding boxes, where 0 is far and 1 is same with sigma=maxdim() of this box
Expand source code Browse git
def pdist(self, bb, sigma=None): """Normalized Gaussian distance in [0,1] between centroids of two bounding boxes, where 0 is far and 1 is same with sigma=maxdim() of this box""" assert isinstance(bb, BoundingBox), "Invalid BoundingBox() input of type '%s'" % str(type(bb)) return np.exp(-self.sqdist(bb)/(float(2*self.maxdim()*self.maxdim()) if sigma is None else float(2.0*sigma*sigma)))
def projective(self, A)
-
Apply an 3x3 projective transformation to the box centroid.
Note: This transformation is performed on the centroid and not the box corners, so the box will still be rectilinear after the transform
Expand source code Browse git
def projective(self, A): """Apply an 3x3 projective transformation to the box centroid. .. note:: This transformation is performed on the centroid and not the box corners, so the box will still be rectilinear after the transform """ assert isnumpy(A) and A.shape == (3,3), "A must be a 3x3 affine transformation matrix" return self._centroid(dehomogenize(np.dot(A, homogenize(np.array(self._centroid())))))
def rescale(self, scale=1)
-
Multiply the box corners by a scale factor
Expand source code Browse git
def rescale(self, scale=1): """Multiply the box corners by a scale factor""" self._xmin = scale * self._xmin self._ymin = scale * self._ymin self._xmax = scale * self._xmax self._ymax = scale * self._ymax return self
def resize(self, width, height)
-
Change the aspect ratio width and height of the box
Expand source code Browse git
def resize(self, width, height): """Change the aspect ratio width and height of the box""" self.setwidth(width) self.setheight(height) return self
def right(self, dx)
-
Make right of box wider (closer to right side of image) by an offset dx
Expand source code Browse git
def right(self, dx): """Make right of box wider (closer to right side of image) by an offset dx""" self._xmax = self._xmax + dx return self
def rot90ccw(self, H, W)
-
Rotate a bounding box such that if an image of size (H,W) is rotated 90 deg clockwise, the boxes align
Expand source code Browse git
def rot90ccw(self, H, W): """Rotate a bounding box such that if an image of size (H,W) is rotated 90 deg clockwise, the boxes align""" (x,y,w,h) = self.xywh() (urx, ury) = self.upperright() return self.xywh((ury, W - urx, h, w))
def rot90cw(self, H, W)
-
Rotate a bounding box such that if an image of size (H,W) is rotated 90 deg clockwise, the boxes align
Expand source code Browse git
def rot90cw(self, H, W): """Rotate a bounding box such that if an image of size (H,W) is rotated 90 deg clockwise, the boxes align""" (x,y,w,h) = self.xywh() (blx, bly) = self.bottomleft() return self.xywh((H - bly, blx, h, w))
def scalex(self, scale=1)
-
Multiply the box corners in the x dimension by a scale factor
Expand source code Browse git
def scalex(self, scale=1): """Multiply the box corners in the x dimension by a scale factor""" self._xmin = scale * self._xmin self._xmax = scale * self._xmax return self
def scaley(self, scale=1)
-
Multiply the box corners in the y dimension by a scale factor
Expand source code Browse git
def scaley(self, scale=1): """Multiply the box corners in the y dimension by a scale factor""" self._ymin = scale * self._ymin self._ymax = scale * self._ymax return self
def set_origin(self, other)
-
Set the origin of the coordinates of this bounding box to be relative to the upper left of the other bounding box
Expand source code Browse git
def set_origin(self, other): """Set the origin of the coordinates of this bounding box to be relative to the upper left of the other bounding box""" assert isinstance(other, BoundingBox), "Invalid BoundingBox() input of type '%s'" % str(type(other)) return self.translate(other.xmin(), other.ymin())
def setheight(self, h)
-
Set new height keeping centroid constant
Expand source code Browse git
def setheight(self, h): """Set new height keeping centroid constant""" if h <= 0: raise ValueError('invalid height') horig = self._ymax - self._ymin self._ymax += float((h - horig) / 2.0) self._ymin -= float((h - horig) / 2.0) return self
def setwidth(self, w)
-
Set new width keeping centroid constant
Expand source code Browse git
def setwidth(self, w): """Set new width keeping centroid constant""" if w <= 0: raise ValueError('invalid width') worig = (self._xmax - self._xmin) self._xmax += float((w - worig) / 2.0) self._xmin -= float((w - worig) / 2.0) return self
def shape(self)
-
Return the (height, width) tuple for the box shape
Expand source code Browse git
def shape(self): """Return the (height, width) tuple for the box shape""" return (self._ymax-self._ymin, self._xmax-self._xmin)
def shapedist(self, other)
-
L1 distance between (width,height) of two boxes
Expand source code Browse git
def shapedist(self, other): """L1 distance between (width,height) of two boxes""" assert isinstance(other, BoundingBox), "Invalid input - must be BoundingBox()" return np.abs(self._width()-other._width()) + np.abs(self._height()-other._height())
def shapeiou(self, bb, area=None, otherarea=None)
-
Shape IoU is the IoU with the upper left corners aligned. This measures the deformation of the two boxes by removing the effect of translation
Expand source code Browse git
def shapeiou(self, bb, area=None, otherarea=None): """Shape IoU is the IoU with the upper left corners aligned. This measures the deformation of the two boxes by removing the effect of translation""" #return self.iou(bb.clone().translate(dx=self._xmin-bb._xmin, dy=self._ymin-bb._ymin)) # equivalent to assert isinstance(bb, BoundingBox), "Invalid input - must be BoundingBox()" w = min(self._xmax, bb._xmax + (self._xmin-bb._xmin)) - max(self._xmin, bb._xmin + (self._xmin-bb._xmin)) h = min(self._ymax, bb._ymax + (self._ymin-bb._ymin)) - max(self._ymin, bb._ymin + (self._ymin-bb._ymin)) area_intersection = w * h area_union = ((self.area() if area is None else area) + (bb.area() if otherarea is None else otherarea) - area_intersection) return (area_intersection / float(area_union)) if area_union>0 else 0
def significant_digits(self, n)
-
Convert corners to have at most n significant digits for efficient JSON storage
Expand source code Browse git
def significant_digits(self, n): """Convert corners to have at most n significant digits for efficient JSON storage""" assert isinstance(n, int) and n>=0 self._xmin = round(self._xmin, n) self._ymin = round(self._ymin, n) self._xmax = round(self._xmax, n) self._ymax = round(self._ymax, n) return self
def sqdist(self, bb)
-
Squared Euclidean distance between upper left corners of two bounding boxes
Expand source code Browse git
def sqdist(self, bb): """Squared Euclidean distance between upper left corners of two bounding boxes""" assert isinstance(bb, BoundingBox), "Invalid BoundingBox() input of type '%s'" % str(type(bb)) return np.power(self.dx(bb), 2.0) + np.power(self.dy(bb), 2.0)
def to_origin(self)
-
Translate the bounding box so that (xmin, ymin) = (0,0)
Expand source code Browse git
def to_origin(self): """Translate the bounding box so that (xmin, ymin) = (0,0)""" return self.translate(-self.xmin(), -self.ymin())
def to_ulbr(self, ulbr=None)
-
Alias for ulbr()
Expand source code Browse git
def to_ulbr(self, ulbr=None): """Alias for ulbr()""" return self.ulbr(ulbr)
def to_xywh(self, xywh=None)
-
Return bounding box corners as (x,y,width,height) tuple
Expand source code Browse git
def to_xywh(self, xywh=None): """Return bounding box corners as (x,y,width,height) tuple""" if xywh is None: (height, width) = (self._ymax-self._ymin, self._xmax-self._xmin) return tuple([self._xmin, self._ymin, width, height]) else: assert len(xywh) == 4, "Invalid (xmin,ymin,width,height) input" self._xmin = float(xywh[0]) self._ymin = float(xywh[1]) self._xmax = float(self._xmin + xywh[2]) self._ymax = float(self._ymin + xywh[3]) return self
def top(self, dy)
-
Make top of box taller (closer to top of image) by an offset dy
Expand source code Browse git
def top(self, dy): """Make top of box taller (closer to top of image) by an offset dy""" self._ymin = self._ymin - dy return self
def translate(self, dx=0, dy=0)
-
Translate the bounding box by dx in x and dy in y
Expand source code Browse git
def translate(self, dx=0, dy=0): """Translate the bounding box by dx in x and dy in y""" self._xmin = self._xmin + dx self._ymin = self._ymin + dy self._xmax = self._xmax + dx self._ymax = self._ymax + dy return self
def ul(self)
-
Upper left coordinate (x,y)
Expand source code Browse git
def ul(self): """Upper left coordinate (x,y)""" return (self._xmin, self._ymin)
def ulbr(self, ulbr=None)
-
Return bounding box corners as upper left, bottom right (xmin, ymin, xmax, ymax)
Expand source code Browse git
def ulbr(self, ulbr=None): """Return bounding box corners as upper left, bottom right (xmin, ymin, xmax, ymax)""" if ulbr is None: return (self._xmin, self._ymin, self._xmax, self._ymax) else: assert len(ulbr) == 4, "Invalid (xmin,ymin,xmax,ymax) input" self._xmin = float(ulbr[0]) self._ymin = float(ulbr[1]) self._xmax = float(ulbr[2]) self._ymax = float(ulbr[3]) return self
def ulx(self)
-
Upper left coordinate (x)
Expand source code Browse git
def ulx(self): """Upper left coordinate (x)""" return self.ul()[0]
def uly(self)
-
Upper left coordinate (y)
Expand source code Browse git
def uly(self): """Upper left coordinate (y)""" return self.ul()[1]
def union(self, bb)
-
Union of one or more bounding boxes with this box
Expand source code Browse git
def union(self, bb): """Union of one or more bounding boxes with this box""" bblist = tolist(bb) assert all([isinstance(bb, BoundingBox) for bb in bblist]), "Invalid BoundingBox() input" self._xmin = min([bb._xmin for bb in bblist] + [self._xmin]) self._ymin = min([bb._ymin for bb in bblist] + [self._ymin]) self._xmax = max([bb._xmax for bb in bblist] + [self._xmax]) self._ymax = max([bb._ymax for bb in bblist] + [self._ymax]) return self
def upperleft(self)
-
Return the (x,y) upper left corner coordinate of the box
Expand source code Browse git
def upperleft(self): """Return the (x,y) upper left corner coordinate of the box""" return (self.xmin(), self.ymin())
def upperright(self)
-
Return the (x,y) upper right corner coordinate of the box
Expand source code Browse git
def upperright(self): """Return the (x,y) upper right corner coordinate of the box""" return (self.xmax(), self.ymin())
def ur(self)
-
Upper right coordinate (x,y)
Expand source code Browse git
def ur(self): """Upper right coordinate (x,y)""" return (self._xmax, self._ymin)
def urx(self)
-
Upper right coordinate (x)
Expand source code Browse git
def urx(self): """Upper right coordinate (x)""" return self.ur()[0]
def ury(self)
-
Upper right coordinate (y)
Expand source code Browse git
def ury(self): """Upper right coordinate (y)""" return self.ur()[1]
def valid(self)
-
Expand source code Browse git
def valid(self): return not self.invalid()
def width(self)
-
Expand source code Browse git
def width(self): return self._xmax - self._xmin
def x_centroid(self)
-
Expand source code Browse git
def x_centroid(self): return self._centroid()[0]
def xcentroid(self)
-
Alias for x_centroid()
Expand source code Browse git
def xcentroid(self): """Alias for x_centroid()""" return self._centroid()[0]
def xmax(self, x=None)
-
x coordinate of lower right corner of box, x-axis is image column
Expand source code Browse git
def xmax(self, x=None): """x coordinate of lower right corner of box, x-axis is image column""" self._xmax = self._xmax if x is None else x return self._xmax if x is None else self
def xmin(self, x=None)
-
x coordinate of upper left corner of box, x-axis is image column
Expand source code Browse git
def xmin(self, x=None): """x coordinate of upper left corner of box, x-axis is image column""" self._xmin = self._xmin if x is None else x return self._xmin if x is None else self
def xywh(self, xywh_=None)
-
Alias for to_xywh
Expand source code Browse git
def xywh(self, xywh_=None): """Alias for to_xywh""" return self.to_xywh(xywh_)
def y_centroid(self)
-
Expand source code Browse git
def y_centroid(self): return self._centroid()[1]
def ycentroid(self)
-
Alias for y_centroid()
Expand source code Browse git
def ycentroid(self): """Alias for y_centroid()""" return self._centroid()[1]
def ymax(self, y=None)
-
y coordinate of lower right corner of box, y-axis is image row
Expand source code Browse git
def ymax(self, y=None): """y coordinate of lower right corner of box, y-axis is image row""" self._ymax = self._ymax if y is None else y return self._ymax if y is None else self
def ymin(self, y=None)
-
y coordinate of upper left corner of box, y-axis is image row, set if provided
Expand source code Browse git
def ymin(self, y=None): """y coordinate of upper left corner of box, y-axis is image row, set if provided""" self._ymin = self._ymin if y is None else y return self._ymin if y is None else self
class Ellipse (semi_major, semi_minor, xcenter, ycenter, phi)
-
Ellipse parameterization, for length of semimajor (half width of ellipse) and semiminor axis (half height), center point and angle phi in radians
Expand source code Browse git
class Ellipse(): __slots__ = ['_major', '_minor', '_xcenter', '_ycenter', '_phi'] def __init__(self, semi_major, semi_minor, xcenter, ycenter, phi): """Ellipse parameterization, for length of semimajor (half width of ellipse) and semiminor axis (half height), center point and angle phi in radians""" self._major = semi_major self._minor = semi_minor self._xcenter = xcenter self._ycenter = ycenter self._phi = phi def __repr__(self): return str('<vipy.geometry.ellipse: semimajor=%s, semiminor=%s, xcenter=%s, ycenter=%s, phi=%s (rad)>' % (self._major, self._minor, self._xcenter, self._ycenter, self._phi)) def dict(self): return {'semimajor':self._major, 'semiminor':self._minor, 'xcenter':self._xcenter, 'ycenter':self._ycenter, 'phi':self._phi} def area(self): """Area of ellipse""" return math.pi * self._major * self._minor def center(self): """Return centroid""" return (self._xcenter, self._ycenter) def centroid(self): """Alias for center""" return self.center() def _centroid(self): """Alias for center useful for overloaded methods""" return (self._xcenter, self._ycenter) def axes(self): """Return the (major,minor) axis lengths""" return (self._major, self._minor) def angle(self): """Return the angle phi (in degrees)""" return (self._phi * 180 / math.pi) def rescale(self, scale): """Scale ellipse by scale factor""" assert isnumber(scale), "Invalid input" self._major *= scale self._minor *= scale self._xcenter *= scale self._ycenter *= scale return self def boundingbox(self): """ Estimate an equivalent bounding box based on scaling to a common area. Note, this does not factor in rotation. (c*l)*(c*w) = a_e --> c = sqrt(a_e / a_r) """ assert self._phi == 0, "This function does not currently factor in rotation" bbox = BoundingBox(width=2 * self._major, height=2 * self._minor, xcentroid=self._xcenter, ycentroid=self._ycenter) a_r = bbox.area() c = (self.area() / a_r) ** 0.5 bbox2 = bbox.clone().dilate(c) return bbox2 def inside(self, x, y=None): """Return true if a point p=(x,y) is inside the ellipse""" p = (x,y) if y is not None else x assert len(p) == 2, "Invalid input" assert self._phi == 0, "inside only currently supported for phi=0" return ((np.square(p[0] - self._xcenter) / np.square(self._major)) + (np.square(p[1] - self._ycenter) / np.square(self._minor))) <= 1 def mask(self): """Return a binary mask of size equal to the bounding box such that the pixels correspond to the interior of the ellipse""" (H,W) = (int(np.round(2 * self._minor)), int(np.round(2 * self._major))) img = np.zeros((H,W), dtype=bool) for (y,x) in product(range(0,H), range(0,W)): img[y,x] = self.inside(x,y) return img
Methods
def angle(self)
-
Return the angle phi (in degrees)
Expand source code Browse git
def angle(self): """Return the angle phi (in degrees)""" return (self._phi * 180 / math.pi)
def area(self)
-
Area of ellipse
Expand source code Browse git
def area(self): """Area of ellipse""" return math.pi * self._major * self._minor
def axes(self)
-
Return the (major,minor) axis lengths
Expand source code Browse git
def axes(self): """Return the (major,minor) axis lengths""" return (self._major, self._minor)
def boundingbox(self)
-
Estimate an equivalent bounding box based on scaling to a common area. Note, this does not factor in rotation. (cl)(c*w) = a_e –> c = sqrt(a_e / a_r)
Expand source code Browse git
def boundingbox(self): """ Estimate an equivalent bounding box based on scaling to a common area. Note, this does not factor in rotation. (c*l)*(c*w) = a_e --> c = sqrt(a_e / a_r) """ assert self._phi == 0, "This function does not currently factor in rotation" bbox = BoundingBox(width=2 * self._major, height=2 * self._minor, xcentroid=self._xcenter, ycentroid=self._ycenter) a_r = bbox.area() c = (self.area() / a_r) ** 0.5 bbox2 = bbox.clone().dilate(c) return bbox2
def center(self)
-
Return centroid
Expand source code Browse git
def center(self): """Return centroid""" return (self._xcenter, self._ycenter)
def centroid(self)
-
Alias for center
Expand source code Browse git
def centroid(self): """Alias for center""" return self.center()
def dict(self)
-
Expand source code Browse git
def dict(self): return {'semimajor':self._major, 'semiminor':self._minor, 'xcenter':self._xcenter, 'ycenter':self._ycenter, 'phi':self._phi}
def inside(self, x, y=None)
-
Return true if a point p=(x,y) is inside the ellipse
Expand source code Browse git
def inside(self, x, y=None): """Return true if a point p=(x,y) is inside the ellipse""" p = (x,y) if y is not None else x assert len(p) == 2, "Invalid input" assert self._phi == 0, "inside only currently supported for phi=0" return ((np.square(p[0] - self._xcenter) / np.square(self._major)) + (np.square(p[1] - self._ycenter) / np.square(self._minor))) <= 1
def mask(self)
-
Return a binary mask of size equal to the bounding box such that the pixels correspond to the interior of the ellipse
Expand source code Browse git
def mask(self): """Return a binary mask of size equal to the bounding box such that the pixels correspond to the interior of the ellipse""" (H,W) = (int(np.round(2 * self._minor)), int(np.round(2 * self._major))) img = np.zeros((H,W), dtype=bool) for (y,x) in product(range(0,H), range(0,W)): img[y,x] = self.inside(x,y) return img
def rescale(self, scale)
-
Scale ellipse by scale factor
Expand source code Browse git
def rescale(self, scale): """Scale ellipse by scale factor""" assert isnumber(scale), "Invalid input" self._major *= scale self._minor *= scale self._xcenter *= scale self._ycenter *= scale return self