001/** 002 * Copyright (c) 2011, The University of Southampton and the individual contributors. 003 * All rights reserved. 004 * 005 * Redistribution and use in source and binary forms, with or without modification, 006 * are permitted provided that the following conditions are met: 007 * 008 * * Redistributions of source code must retain the above copyright notice, 009 * this list of conditions and the following disclaimer. 010 * 011 * * Redistributions in binary form must reproduce the above copyright notice, 012 * this list of conditions and the following disclaimer in the documentation 013 * and/or other materials provided with the distribution. 014 * 015 * * Neither the name of the University of Southampton nor the names of its 016 * contributors may be used to endorse or promote products derived from this 017 * software without specific prior written permission. 018 * 019 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 020 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 021 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 022 * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 023 * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 024 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 025 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 026 * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 027 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 028 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 029 */ 030package org.openimaj.math.geometry.shape.util; 031 032import java.util.ArrayList; 033import java.util.Comparator; 034import java.util.List; 035import java.util.Set; 036import java.util.Stack; 037import java.util.TreeSet; 038 039import odk.lang.FastMath; 040 041import org.openimaj.math.geometry.point.Point2d; 042import org.openimaj.math.geometry.shape.Polygon; 043 044/** 045 * Graham Scan convex hull algorithm, based on the implementation by <a 046 * href="https://github.com/bkiers/GrahamScan">Bart Kiers</a>. 047 * 048 * @author Jonathon Hare (jsh2@ecs.soton.ac.uk) 049 * @author Bart Kiers 050 */ 051public final class GrahamScan { 052 /** 053 * An enum denoting a directional-turn between 3 Point2ds (vectors). 054 */ 055 protected static enum Turn { 056 CLOCKWISE, COUNTER_CLOCKWISE, COLLINEAR 057 } 058 059 /** 060 * Returns true iff all Point2ds in <code>Point2ds</code> are collinear. 061 * 062 * @param Point2ds 063 * the list of Point2ds. 064 * @return true iff all Point2ds in <code>Point2ds</code> are collinear. 065 */ 066 protected static boolean areAllCollinear(List<Point2d> Point2ds) { 067 068 if (Point2ds.size() < 2) { 069 return true; 070 } 071 072 final Point2d a = Point2ds.get(0); 073 final Point2d b = Point2ds.get(1); 074 075 for (int i = 2; i < Point2ds.size(); i++) { 076 077 final Point2d c = Point2ds.get(i); 078 079 if (getTurn(a, b, c) != Turn.COLLINEAR) { 080 return false; 081 } 082 } 083 084 return true; 085 } 086 087 /** 088 * Returns the convex hull of the Point2ds created from the list 089 * <code>Point2ds</code>. Note that the first and last Point2d in the 090 * returned <code>List<java.awt.Point2d></code> are the same Point2d. 091 * 092 * @param Point2ds 093 * the list of Point2ds. 094 * @return the convex hull of the Point2ds created from the list 095 * <code>Point2ds</code>. 096 */ 097 public static Polygon getConvexHull(List<Point2d> Point2ds) { 098 099 final List<Point2d> sorted = new ArrayList<Point2d>(getSortedPoint2dSet(Point2ds)); 100 101 if (sorted.size() <= 3) { 102 return new Polygon(Point2ds); 103 } 104 105 if (areAllCollinear(sorted)) { 106 return new Polygon(Point2ds); 107 } 108 109 final Stack<Point2d> stack = new Stack<Point2d>(); 110 stack.push(sorted.get(0)); 111 stack.push(sorted.get(1)); 112 113 for (int i = 2; i < sorted.size(); i++) { 114 115 final Point2d head = sorted.get(i); 116 final Point2d middle = stack.pop(); 117 final Point2d tail = stack.peek(); 118 119 final Turn turn = getTurn(tail, middle, head); 120 121 switch (turn) { 122 case COUNTER_CLOCKWISE: 123 stack.push(middle); 124 stack.push(head); 125 break; 126 case CLOCKWISE: 127 i--; 128 break; 129 case COLLINEAR: 130 stack.push(head); 131 break; 132 } 133 } 134 135 // close the hull 136 stack.push(sorted.get(0)); 137 138 return new Polygon(stack); 139 } 140 141 /** 142 * Returns the Point2ds with the lowest y coordinate. In case more than 1 143 * such Point2d exists, the one with the lowest x coordinate is returned. 144 * 145 * @param Point2ds 146 * the list of Point2ds to return the lowest Point2d from. 147 * @return the Point2ds with the lowest y coordinate. In case more than 1 148 * such Point2d exists, the one with the lowest x coordinate is 149 * returned. 150 */ 151 protected static Point2d getLowestPoint2d(List<Point2d> Point2ds) { 152 153 Point2d lowest = Point2ds.get(0); 154 155 for (int i = 1; i < Point2ds.size(); i++) { 156 157 final Point2d temp = Point2ds.get(i); 158 159 if (temp.getY() < lowest.getY() || (temp.getY() == lowest.getY() && temp.getX() < lowest.getX())) { 160 lowest = temp; 161 } 162 } 163 164 return lowest; 165 } 166 167 /** 168 * Returns a sorted set of Point2ds from the list <code>Point2ds</code>. The 169 * set of Point2ds are sorted in increasing order of the angle they and the 170 * lowest Point2d <tt>P</tt> make with the x-axis. If two (or more) Point2ds 171 * form the same angle towards <tt>P</tt>, the one closest to <tt>P</tt> 172 * comes first. 173 * 174 * @param Point2ds 175 * the list of Point2ds to sort. 176 * @return a sorted set of Point2ds from the list <code>Point2ds</code>. 177 * @see GrahamScan#getLowestPoint2d(java.util.List) 178 */ 179 protected static Set<Point2d> getSortedPoint2dSet(List<Point2d> Point2ds) { 180 181 final Point2d lowest = getLowestPoint2d(Point2ds); 182 183 final TreeSet<Point2d> set = new TreeSet<Point2d>(new Comparator<Point2d>() { 184 @Override 185 public int compare(Point2d a, Point2d b) { 186 187 if (a == b || a.equals(b)) { 188 return 0; 189 } 190 191 final double thetaA = FastMath.atan2(a.getY() - lowest.getY(), a.getX() - lowest.getX()); 192 final double thetaB = FastMath.atan2(b.getY() - lowest.getY(), b.getX() - lowest.getX()); 193 194 if (thetaA < thetaB) { 195 return -1; 196 } 197 else if (thetaA > thetaB) { 198 return 1; 199 } 200 else { 201 // collinear with the 'lowest' Point2d, let the Point2d 202 // closest to it come first 203 final double distanceA = FastMath.sqrt(((lowest.getX() - a.getX()) * (lowest.getX() - a 204 .getX())) + 205 ((lowest.getY() - a.getY()) * (lowest.getY() - a.getY()))); 206 final double distanceB = FastMath.sqrt(((lowest.getX() - b.getX()) * (lowest.getX() - b 207 .getX())) + 208 ((lowest.getY() - b.getY()) * (lowest.getY() - b.getY()))); 209 210 if (distanceA < distanceB) { 211 return -1; 212 } 213 else { 214 return 1; 215 } 216 } 217 } 218 }); 219 220 set.addAll(Point2ds); 221 222 return set; 223 } 224 225 /** 226 * Returns the GrahamScan#Turn formed by traversing through the ordered 227 * Point2ds <code>a</code>, <code>b</code> and <code>c</code>. More 228 * specifically, the cross product <tt>C</tt> between the 3 Point2ds 229 * (vectors) is calculated: 230 * 231 * <tt>(b.getX()-a.getX() * c.getY()-a.getY()) - (b.getY()-a.getY() * c.getX()-a.getX())</tt> 232 * 233 * and if <tt>C</tt> is less than 0, the turn is CLOCKWISE, if <tt>C</tt> is 234 * more than 0, the turn is COUNTER_CLOCKWISE, else the three Point2ds are 235 * COLLINEAR. 236 * 237 * @param a 238 * the starting Point2d. 239 * @param b 240 * the second Point2d. 241 * @param c 242 * the end Point2d. 243 * @return the GrahamScan#Turn formed by traversing through the ordered 244 * Point2ds <code>a</code>, <code>b</code> and <code>c</code>. 245 */ 246 protected static Turn getTurn(Point2d a, Point2d b, Point2d c) { 247 final double crossProduct = ((b.getX() - a.getX()) * (c.getY() - a.getY())) - 248 ((b.getY() - a.getY()) * (c.getX() - a.getX())); 249 250 if (crossProduct > 0) { 251 return Turn.COUNTER_CLOCKWISE; 252 } 253 else if (crossProduct < 0) { 254 return Turn.CLOCKWISE; 255 } 256 else { 257 return Turn.COLLINEAR; 258 } 259 } 260}