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 org.openimaj.math.geometry.point.Point2d; 040import org.openimaj.math.geometry.shape.Polygon; 041 042import net.jafama.FastMath; 043 044/** 045 * Graham Scan convex hull algorithm, based on the implementation by 046 * <a 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 returned 090 * <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 such 143 * 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 such 148 * Point2d exists, the one with the lowest x coordinate is returned. 149 */ 150 protected static Point2d getLowestPoint2d(List<Point2d> Point2ds) { 151 152 Point2d lowest = Point2ds.get(0); 153 154 for (int i = 1; i < Point2ds.size(); i++) { 155 156 final Point2d temp = Point2ds.get(i); 157 158 if (temp.getY() < lowest.getY() || (temp.getY() == lowest.getY() && temp.getX() < lowest.getX())) { 159 lowest = temp; 160 } 161 } 162 163 return lowest; 164 } 165 166 /** 167 * Returns a sorted set of Point2ds from the list <code>Point2ds</code>. The set 168 * of Point2ds are sorted in increasing order of the angle they and the lowest 169 * Point2d <tt>P</tt> make with the x-axis. If two (or more) Point2ds form the 170 * same angle towards <tt>P</tt>, the one closest to <tt>P</tt> comes first. 171 * 172 * @param Point2ds 173 * the list of Point2ds to sort. 174 * @return a sorted set of Point2ds from the list <code>Point2ds</code>. 175 * @see GrahamScan#getLowestPoint2d(java.util.List) 176 */ 177 protected static Set<Point2d> getSortedPoint2dSet(List<Point2d> Point2ds) { 178 179 final Point2d lowest = getLowestPoint2d(Point2ds); 180 181 final TreeSet<Point2d> set = new TreeSet<Point2d>(new Comparator<Point2d>() { 182 @Override 183 public int compare(Point2d a, Point2d b) { 184 185 if (a == b || a.equals(b)) { 186 return 0; 187 } 188 189 final double thetaA = FastMath.atan2(a.getY() - lowest.getY(), a.getX() - lowest.getX()); 190 final double thetaB = FastMath.atan2(b.getY() - lowest.getY(), b.getX() - lowest.getX()); 191 192 if (thetaA < thetaB) { 193 return -1; 194 } else if (thetaA > thetaB) { 195 return 1; 196 } else { 197 // collinear with the 'lowest' Point2d, let the Point2d 198 // closest to it come first 199 final double distanceA = FastMath.sqrt(((lowest.getX() - a.getX()) * (lowest.getX() - a 200 .getX())) + 201 ((lowest.getY() - a.getY()) * (lowest.getY() - a.getY()))); 202 final double distanceB = FastMath.sqrt(((lowest.getX() - b.getX()) * (lowest.getX() - b 203 .getX())) + 204 ((lowest.getY() - b.getY()) * (lowest.getY() - b.getY()))); 205 206 if (distanceA < distanceB) { 207 return -1; 208 } else { 209 return 1; 210 } 211 } 212 } 213 }); 214 215 set.addAll(Point2ds); 216 217 return set; 218 } 219 220 /** 221 * Returns the GrahamScan#Turn formed by traversing through the ordered Point2ds 222 * <code>a</code>, <code>b</code> and <code>c</code>. More specifically, the 223 * cross product <tt>C</tt> between the 3 Point2ds (vectors) is calculated: 224 * 225 * <tt>(b.getX()-a.getX() * c.getY()-a.getY()) - (b.getY()-a.getY() * c.getX()-a.getX())</tt> 226 * 227 * and if <tt>C</tt> is less than 0, the turn is CLOCKWISE, if <tt>C</tt> is 228 * more than 0, the turn is COUNTER_CLOCKWISE, else the three Point2ds are 229 * COLLINEAR. 230 * 231 * @param a 232 * the starting Point2d. 233 * @param b 234 * the second Point2d. 235 * @param c 236 * the end Point2d. 237 * @return the GrahamScan#Turn formed by traversing through the ordered Point2ds 238 * <code>a</code>, <code>b</code> and <code>c</code>. 239 */ 240 protected static Turn getTurn(Point2d a, Point2d b, Point2d c) { 241 final double crossProduct = ((b.getX() - a.getX()) * (c.getY() - a.getY())) - 242 ((b.getY() - a.getY()) * (c.getX() - a.getX())); 243 244 if (crossProduct > 0) { 245 return Turn.COUNTER_CLOCKWISE; 246 } else if (crossProduct < 0) { 247 return Turn.CLOCKWISE; 248 } else { 249 return Turn.COLLINEAR; 250 } 251 } 252}