1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303
|
#!/usr/bin/env python
"""
A Bounding Box object and assorted utilities , subclassed from a numpy array
"""
import numpy as N
class BBox(N.ndarray):
"""
A Bounding Box object:
Takes Data as an array. Data is any python sequence that can be turned into a
2x2 numpy array of floats:
[[MinX, MinY ],
[MaxX, MaxY ]]
It is a subclass of numpy.ndarray, so for the most part it can be used as
an array, and arrays that fit the above description can be used in its place.
Usually created by the factory functions:
asBBox
and
fromPoints
"""
def __new__(subtype, data):
"""
Takes Data as an array. Data is any python sequence that can be turned into a
2x2 numpy array of floats:
[[MinX, MinY ],
[MaxX, MaxY ]]
You don't usually call this directly. BBox objects are created with the factory functions:
asBBox
and
fromPoints
"""
arr = N.array(data, N.float)
arr.shape = (2,2)
if arr[0,0] > arr[1,0] or arr[0,1] > arr[1,1]:
# note: zero sized BB OK.
raise ValueError("BBox values not aligned: \n minimum values must be less that maximum values")
return N.ndarray.__new__(subtype, shape=arr.shape, dtype=arr.dtype, buffer=arr)
def Overlaps(self, BB):
"""
Overlap(BB):
Tests if the given Bounding Box overlaps with this one.
Returns True is the Bounding boxes overlap, False otherwise
If they are just touching, returns True
"""
if N.isinf(self).all() or N.isinf(BB).all():
return True
if ( (self[1,0] >= BB[0,0]) and (self[0,0] <= BB[1,0]) and
(self[1,1] >= BB[0,1]) and (self[0,1] <= BB[1,1]) ):
return True
else:
return False
def Inside(self, BB):
"""
Inside(BB):
Tests if the given Bounding Box is entirely inside this one.
Returns True if it is entirely inside, or touching the
border.
Returns False otherwise
"""
if ( (BB[0,0] >= self[0,0]) and (BB[1,0] <= self[1,0]) and
(BB[0,1] >= self[0,1]) and (BB[1,1] <= self[1,1]) ):
return True
else:
return False
def PointInside(self, Point):
"""
Inside(BB):
Tests if the given Point is entirely inside this one.
Returns True if it is entirely inside, or touching the
border.
Returns False otherwise
Point is any length-2 sequence (tuple, list, array) or two numbers
"""
if Point[0] >= self[0,0] and \
Point[0] <= self[1,0] and \
Point[1] <= self[1,1] and \
Point[1] >= self[0,1]:
return True
else:
return False
def Merge(self, BB):
"""
Joins this bounding box with the one passed in, maybe making this one bigger
"""
if self.IsNull():
self[:] = BB
elif N.isnan(BB).all(): ## BB may be a regular array, so I can't use IsNull
pass
else:
if BB[0,0] < self[0,0]: self[0,0] = BB[0,0]
if BB[0,1] < self[0,1]: self[0,1] = BB[0,1]
if BB[1,0] > self[1,0]: self[1,0] = BB[1,0]
if BB[1,1] > self[1,1]: self[1,1] = BB[1,1]
return None
def IsNull(self):
return N.isnan(self).all()
## fixme: it would be nice to add setter, too.
def _getLeft(self):
return self[0,0]
Left = property(_getLeft)
def _getRight(self):
return self[1,0]
Right = property(_getRight)
def _getBottom(self):
return self[0,1]
Bottom = property(_getBottom)
def _getTop(self):
return self[1,1]
Top = property(_getTop)
def _getWidth(self):
return self[1,0] - self[0,0]
Width = property(_getWidth)
def _getHeight(self):
return self[1,1] - self[0,1]
Height = property(_getHeight)
def _getCenter(self):
return self.sum(0) / 2.0
Center = property(_getCenter)
### This could be used for a make BB from a bunch of BBs
#~ def _getboundingbox(bboxarray): # lrk: added this
#~ # returns the bounding box of a bunch of bounding boxes
#~ upperleft = N.minimum.reduce(bboxarray[:,0])
#~ lowerright = N.maximum.reduce(bboxarray[:,1])
#~ return N.array((upperleft, lowerright), N.float)
#~ _getboundingbox = staticmethod(_getboundingbox)
## Save the ndarray __eq__ for internal use.
Array__eq__ = N.ndarray.__eq__
def __eq__(self, BB):
"""
__eq__(BB) The equality operator
A == B if and only if all the entries are the same
"""
if self.IsNull() and N.isnan(BB).all(): ## BB may be a regular array, so I can't use IsNull
return True
else:
return self.Array__eq__(BB).all()
def asBBox(data):
"""
returns a BBox object.
If object is a BBox, it is returned unaltered
If object is a numpy array, a BBox object is returned that shares a
view of the data with that array. The numpy array should be of the correct
format: a 2x2 numpy array of floats:
[[MinX, MinY ],
[MaxX, MaxY ]]
"""
if isinstance(data, BBox):
return data
arr = N.asarray(data, N.float)
return N.ndarray.__new__(BBox, shape=arr.shape, dtype=arr.dtype, buffer=arr)
def fromPoints(Points):
"""
fromPoints (Points).
reruns the bounding box of the set of points in Points. Points can
be any python object that can be turned into a numpy NX2 array of Floats.
If a single point is passed in, a zero-size Bounding Box is returned.
"""
Points = N.asarray(Points, N.float).reshape(-1,2)
arr = N.vstack( (Points.min(0), Points.max(0)) )
return N.ndarray.__new__(BBox, shape=arr.shape, dtype=arr.dtype, buffer=arr)
def fromBBArray(BBarray):
"""
Builds a BBox object from an array of Bounding Boxes.
The resulting Bounding Box encompases all the included BBs.
The BBarray is in the shape: (Nx2x2) where BBarray[n] is a 2x2 array that represents a BBox
"""
#upperleft = N.minimum.reduce(BBarray[:,0])
#lowerright = N.maximum.reduce(BBarray[:,1])
# BBarray = N.asarray(BBarray, N.float).reshape(-1,2)
# arr = N.vstack( (BBarray.min(0), BBarray.max(0)) )
BBarray = N.asarray(BBarray, N.float).reshape(-1,2,2)
arr = N.vstack( (BBarray[:,0,:].min(0), BBarray[:,1,:].max(0)) )
return asBBox(arr)
#return asBBox( (upperleft, lowerright) ) * 2
def NullBBox():
"""
Returns a BBox object with all NaN entries.
This represents a Null BB box;
BB merged with it will return BB.
Nothing is inside it.
"""
arr = N.array(((N.nan, N.nan),(N.nan, N.nan)), N.float)
return N.ndarray.__new__(BBox, shape=arr.shape, dtype=arr.dtype, buffer=arr)
def InfBBox():
"""
Returns a BBox object with all -inf and inf entries
"""
arr = N.array(((-N.inf, -N.inf),(N.inf, N.inf)), N.float)
return N.ndarray.__new__(BBox, shape=arr.shape, dtype=arr.dtype, buffer=arr)
class RectBBox(BBox):
"""
subclass of a BBox that can be used for a rotated Rectangle
contributed by MArco Oster (marco.oster@bioquant.uni-heidelberg.de)
"""
def __new__(self, data, edges=None):
return BBox.__new__(self, data)
def __init__(self, data, edges=None):
''' assume edgepoints are ordered such you can walk along all edges with left rotation sense
This may be:
left-top
left-bottom
right-bottom
right-top
or any rotation.
'''
BBox.BBox(data)
self.edges = np.asarray(edges)
print "new rectbbox created"
def ac_leftOf_ab(self, a, b, c):
ab = np.array(b) - np.array(a)
ac = np.array(c) - np.array(a)
return (ac[0]*ab[1] - ac[1]*ab[0]) <= 0
def PointInside(self, point):
print "point inside called"
for edge in xrange(4):
if self.ac_leftOf_ab(self.edges[edge],
self.edges[(edge+1)%4],
point):
continue
else:
return False
return True
|