├── README.txt └── python ├── qhull_2d.py ├── bbox_test.py └── min_bounding_rect.py /README.txt: -------------------------------------------------------------------------------- 1 | # 2 | # Test program to find minimum-area bounding rectangle of a set of 2D points 3 | # 4 | # Currently implemented in Python. I will upload the Matlab version later. 5 | # 6 | # 7 | # Copyright (c) 2013, David Butterworth, University of Queensland 8 | # All rights reserved. 9 | # 10 | 11 | Installation: 12 | Download all source code from the /python directory to your local computer. 13 | 14 | Test the program: 15 | $ python ./bbox_test.py 16 | 17 | The test program includes definitions for some simple polygons. 18 | You can define your own Nx2 numpy array with your own data. 19 | 20 | Tested with Python 2.6.5 on Ubuntu 10.04.4 21 | Results verified using Matlab 22 | -------------------------------------------------------------------------------- /python/qhull_2d.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | # Compute the convex hull of a set of 2D points 4 | # A Python implementation of the qhull algorithm 5 | # 6 | # Tested with Python 2.6.5 on Ubuntu 10.04.4 7 | 8 | # Copyright (c) 2008 Dave (www.literateprograms.org) 9 | # 10 | # Permission is hereby granted, free of charge, to any person obtaining a copy 11 | # of this software and associated documentation files (the "Software"), to deal 12 | # in the Software without restriction, including without limitation the rights 13 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 14 | # copies of the Software, and to permit persons to whom the Software is 15 | # furnished to do so, subject to the following conditions: 16 | # 17 | # The above copyright notice and this permission notice shall be included in 18 | # all copies or substantial portions of the Software. 19 | # 20 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 21 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 22 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 23 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 24 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 25 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 26 | # IN THE SOFTWARE. 27 | 28 | from __future__ import division 29 | from numpy import * 30 | 31 | link = lambda a,b: concatenate((a,b[1:])) 32 | edge = lambda a,b: concatenate(([a],[b])) 33 | 34 | def qhull2D(sample): 35 | def dome(sample,base): 36 | h, t = base 37 | dists = dot(sample-h, dot(((0,-1),(1,0)),(t-h))) 38 | outer = repeat(sample, dists>0, 0) 39 | if len(outer): 40 | pivot = sample[argmax(dists)] 41 | return link(dome(outer, edge(h, pivot)), 42 | dome(outer, edge(pivot, t))) 43 | else: 44 | return base 45 | if len(sample) > 2: 46 | axis = sample[:,0] 47 | base = take(sample, [argmin(axis), argmax(axis)], 0) 48 | return link(dome(sample, base), dome(sample, base[::-1])) 49 | else: 50 | return sample 51 | 52 | -------------------------------------------------------------------------------- /python/bbox_test.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | # Test program to find minimum-area bounding rectangle 4 | # 5 | # Un-comment one of the arrays to test with a rectangle or 5-point polygon 6 | # Some solutions to this problem will fail with simple shapes! 7 | # 8 | # Tested with Python 2.6.5 on Ubuntu 10.04.4 9 | # Results verified using Matlab 10 | 11 | # Copyright (c) 2013, David Butterworth, University of Queensland 12 | # All rights reserved. 13 | # 14 | # Redistribution and use in source and binary forms, with or without 15 | # modification, are permitted provided that the following conditions are met: 16 | # 17 | # * Redistributions of source code must retain the above copyright 18 | # notice, this list of conditions and the following disclaimer. 19 | # * Redistributions in binary form must reproduce the above copyright 20 | # notice, this list of conditions and the following disclaimer in the 21 | # documentation and/or other materials provided with the distribution. 22 | # * Neither the name of the Willow Garage, Inc. nor the names of its 23 | # contributors may be used to endorse or promote products derived from 24 | # this software without specific prior written permission. 25 | # 26 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 27 | # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 28 | # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 29 | # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE 30 | # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 31 | # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 32 | # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 33 | # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 34 | # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 35 | # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 36 | # POSSIBILITY OF SUCH DAMAGE. 37 | 38 | from numpy import * 39 | 40 | from qhull_2d import * 41 | from min_bounding_rect import * 42 | 43 | if __name__ == "__main__": 44 | # 45 | # Un-comment one of these shapes below: 46 | # 47 | 48 | # Square 49 | #xy_points = 10*array([(x,y) for x in arange(10) for y in arange(10)]) 50 | 51 | # Random points 52 | #xy_points = 100*random.random((32,2)) 53 | 54 | # A rectangle 55 | #xy_points = array([ [0,0], [1,0], [1,2], [0,2], [0,0] ]) 56 | 57 | # A rectangle, with 5th outlier 58 | xy_points = array([ [0,0], [1,0], [1.5,1], [1,2], [0,2], [0,0] ]) 59 | 60 | #--------------------------------------------------------------------------# 61 | 62 | # Find convex hull 63 | hull_points = qhull2D(xy_points) 64 | 65 | # Reverse order of points, to match output from other qhull implementations 66 | hull_points = hull_points[::-1] 67 | 68 | print 'Convex hull points: \n', hull_points, "\n" 69 | 70 | # Find minimum area bounding rectangle 71 | (rot_angle, area, width, height, center_point, corner_points) = minBoundingRect(hull_points) 72 | 73 | print "Minimum area bounding box:" 74 | print "Rotation angle:", rot_angle, "rad (", rot_angle*(180/math.pi), "deg )" 75 | print "Width:", width, " Height:", height, " Area:", area 76 | print "Center point: \n", center_point # numpy array 77 | print "Corner points: \n", corner_points, "\n" # numpy array 78 | 79 | 80 | -------------------------------------------------------------------------------- /python/min_bounding_rect.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | # Find the minimum-area bounding box of a set of 2D points 4 | # 5 | # The input is a 2D convex hull, in an Nx2 numpy array of x-y co-ordinates. 6 | # The first and last points points must be the same, making a closed polygon. 7 | # This program finds the rotation angles of each edge of the convex polygon, 8 | # then tests the area of a bounding box aligned with the unique angles in 9 | # 90 degrees of the 1st Quadrant. 10 | # Returns the 11 | # 12 | # Tested with Python 2.6.5 on Ubuntu 10.04.4 13 | # Results verified using Matlab 14 | 15 | # Copyright (c) 2013, David Butterworth, University of Queensland 16 | # All rights reserved. 17 | # 18 | # Redistribution and use in source and binary forms, with or without 19 | # modification, are permitted provided that the following conditions are met: 20 | # 21 | # * Redistributions of source code must retain the above copyright 22 | # notice, this list of conditions and the following disclaimer. 23 | # * Redistributions in binary form must reproduce the above copyright 24 | # notice, this list of conditions and the following disclaimer in the 25 | # documentation and/or other materials provided with the distribution. 26 | # * Neither the name of the Willow Garage, Inc. nor the names of its 27 | # contributors may be used to endorse or promote products derived from 28 | # this software without specific prior written permission. 29 | # 30 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 31 | # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 32 | # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 33 | # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE 34 | # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 35 | # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 36 | # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 37 | # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 38 | # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 39 | # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 40 | # POSSIBILITY OF SUCH DAMAGE. 41 | 42 | from numpy import * 43 | import sys # maxint 44 | 45 | def minBoundingRect(hull_points_2d): 46 | #print "Input convex hull points: " 47 | #print hull_points_2d 48 | 49 | # Compute edges (x2-x1,y2-y1) 50 | edges = zeros( (len(hull_points_2d)-1,2) ) # empty 2 column array 51 | for i in range( len(edges) ): 52 | edge_x = hull_points_2d[i+1,0] - hull_points_2d[i,0] 53 | edge_y = hull_points_2d[i+1,1] - hull_points_2d[i,1] 54 | edges[i] = [edge_x,edge_y] 55 | #print "Edges: \n", edges 56 | 57 | # Calculate edge angles atan2(y/x) 58 | edge_angles = zeros( (len(edges)) ) # empty 1 column array 59 | for i in range( len(edge_angles) ): 60 | edge_angles[i] = math.atan2( edges[i,1], edges[i,0] ) 61 | #print "Edge angles: \n", edge_angles 62 | 63 | # Check for angles in 1st quadrant 64 | for i in range( len(edge_angles) ): 65 | edge_angles[i] = abs( edge_angles[i] % (math.pi/2) ) # want strictly positive answers 66 | #print "Edge angles in 1st Quadrant: \n", edge_angles 67 | 68 | # Remove duplicate angles 69 | edge_angles = unique(edge_angles) 70 | #print "Unique edge angles: \n", edge_angles 71 | 72 | # Test each angle to find bounding box with smallest area 73 | min_bbox = (0, sys.maxint, 0, 0, 0, 0, 0, 0) # rot_angle, area, width, height, min_x, max_x, min_y, max_y 74 | print "Testing", len(edge_angles), "possible rotations for bounding box... \n" 75 | for i in range( len(edge_angles) ): 76 | 77 | # Create rotation matrix to shift points to baseline 78 | # R = [ cos(theta) , cos(theta-PI/2) 79 | # cos(theta+PI/2) , cos(theta) ] 80 | R = array([ [ math.cos(edge_angles[i]), math.cos(edge_angles[i]-(math.pi/2)) ], [ math.cos(edge_angles[i]+(math.pi/2)), math.cos(edge_angles[i]) ] ]) 81 | #print "Rotation matrix for ", edge_angles[i], " is \n", R 82 | 83 | # Apply this rotation to convex hull points 84 | rot_points = dot(R, transpose(hull_points_2d) ) # 2x2 * 2xn 85 | #print "Rotated hull points are \n", rot_points 86 | 87 | # Find min/max x,y points 88 | min_x = nanmin(rot_points[0], axis=0) 89 | max_x = nanmax(rot_points[0], axis=0) 90 | min_y = nanmin(rot_points[1], axis=0) 91 | max_y = nanmax(rot_points[1], axis=0) 92 | #print "Min x:", min_x, " Max x: ", max_x, " Min y:", min_y, " Max y: ", max_y 93 | 94 | # Calculate height/width/area of this bounding rectangle 95 | width = max_x - min_x 96 | height = max_y - min_y 97 | area = width*height 98 | #print "Potential bounding box ", i, ": width: ", width, " height: ", height, " area: ", area 99 | 100 | # Store the smallest rect found first (a simple convex hull might have 2 answers with same area) 101 | if (area < min_bbox[1]): 102 | min_bbox = ( edge_angles[i], area, width, height, min_x, max_x, min_y, max_y ) 103 | # Bypass, return the last found rect 104 | #min_bbox = ( edge_angles[i], area, width, height, min_x, max_x, min_y, max_y ) 105 | 106 | # Re-create rotation matrix for smallest rect 107 | angle = min_bbox[0] 108 | R = array([ [ math.cos(angle), math.cos(angle-(math.pi/2)) ], [ math.cos(angle+(math.pi/2)), math.cos(angle) ] ]) 109 | #print "Projection matrix: \n", R 110 | 111 | # Project convex hull points onto rotated frame 112 | proj_points = dot(R, transpose(hull_points_2d) ) # 2x2 * 2xn 113 | #print "Project hull points are \n", proj_points 114 | 115 | # min/max x,y points are against baseline 116 | min_x = min_bbox[4] 117 | max_x = min_bbox[5] 118 | min_y = min_bbox[6] 119 | max_y = min_bbox[7] 120 | #print "Min x:", min_x, " Max x: ", max_x, " Min y:", min_y, " Max y: ", max_y 121 | 122 | # Calculate center point and project onto rotated frame 123 | center_x = (min_x + max_x)/2 124 | center_y = (min_y + max_y)/2 125 | center_point = dot( [ center_x, center_y ], R ) 126 | #print "Bounding box center point: \n", center_point 127 | 128 | # Calculate corner points and project onto rotated frame 129 | corner_points = zeros( (4,2) ) # empty 2 column array 130 | corner_points[0] = dot( [ max_x, min_y ], R ) 131 | corner_points[1] = dot( [ min_x, min_y ], R ) 132 | corner_points[2] = dot( [ min_x, max_y ], R ) 133 | corner_points[3] = dot( [ max_x, max_y ], R ) 134 | #print "Bounding box corner points: \n", corner_points 135 | 136 | #print "Angle of rotation: ", angle, "rad ", angle * (180/math.pi), "deg" 137 | 138 | return (angle, min_bbox[1], min_bbox[2], min_bbox[3], center_point, corner_points) # rot_angle, area, width, height, center_point, corner_points 139 | 140 | --------------------------------------------------------------------------------