在此感謝 Darel Rex Finley,本文只是對其c代碼在java上的延伸
很早以前項目中有一需求,須要用一條閉合曲線將離散座標點勾勒出來,根據Darel Rex Finley的程序,實現了最小凸多邊形邊界查找(關於凸多邊形及凹多邊形的定義見 凸多邊形 及 凹多邊形)java
現將實現過程整理以下:算法
首先創建離散點類工具
/** * <p> * <b>離散點</b> * <p> * <pre> * 離散點 * </pre> * * @author ManerFan 2015年4月10日 */ public class Point { /** * x座標 */ private double x; /** * y座標 */ private double y; /** * 邊界查找算法中 是否被找到 */ boolean founded = false; /** Constructor Getters & Setters */ }
爲更方便的實現算法,建立離散點操做工具類flex
/** * <p> * <b>離散點計算工具</b> * <p> * <pre> * 離散點計算工具 * * y * ↑ · · * │ · · · * │ · · · · * │ · · * —│————————————→ x * </pre> * * @author ManerFan 2015年4月9日 */ public class DiscretePointUtil { /** * <p> * <b>查找離散點集中的(min_x, min_Y) (max_x, max_Y)</b> * <p> * <pre> * 查找離散點集中的(min_x, min_Y) (max_x, max_Y) * </pre> * * @author ManerFan 2015年4月9日 * @param points 離散點集 * @return [(min_x, min_Y), (max_x, max_Y)] */ public static Point[] calMinMaxDots(final List<Point> points) { if (null == points || points.isEmpty()) { return null; } double min_x = points.get(0).getX(), max_x = points.get(0).getX(); double min_y = points.get(0).getY(), max_y = points.get(0).getY(); /* 這裏存在優化空間,能夠使用並行計算 */ for (Point point : points) { if (min_x > point.getX()) { min_x = point.getX(); } if (max_x < point.getX()) { max_x = point.getX(); } if (min_y > point.getY()) { min_y = point.getY(); } if (max_y < point.getY()) { max_y = point.getY(); } } Point ws = new Point(min_x, min_y); Point en = new Point(max_x, max_y); return new Point[] { ws, en }; } /** * <p> * <b>求矩形面積平方根</b> * <p> * <pre> * 以兩個點做爲矩形的對角線上的兩點,計算其面積的平方根 * </pre> * * @author ManerFan 2015年4月9日 * @param ws 西南點 * @param en 東北點 * @return 矩形面積平方根 */ public static double calRectAreaSquare(Point ws, Point en) { if (null == ws || null == en) { return .0; } /* 爲防止計算面積時float溢出,先計算各邊平方根,再相乘 */ return Math.sqrt(Math.abs(ws.getX() - en.getX())) * Math.sqrt(Math.abs(ws.getY() - en.getY())); } /** * <p> * <b>求兩點之間的長度</b> * <p> * <pre> * 求兩點之間的長度 * </pre> * * @author ManerFan 2015年4月10日 * @param ws 西南點 * @param en 東北點 * @return 兩點之間的長度 */ public static double calLineLen(Point ws, Point en) { if (null == ws || null == en) { return .0; } if (ws.equals(en)) { return .0; } double a = Math.abs(ws.getX() - en.getX()); // 直角三角形的直邊a double b = Math.abs(ws.getY() - en.getY()); // 直角三角形的直邊b double min = Math.min(a, b); // 短直邊 double max = Math.max(a, b); // 長直邊 /** * 爲防止計算平方時float溢出,作以下轉換 * √(min²+max²) = √((min/max)²+1) * abs(max) */ double inner = min / max; return Math.sqrt(inner * inner + 1.0) * max; } /** * <p> * <b>求兩點間的中心點</b> * <p> * <pre> * 求兩點間的中心點 * </pre> * * @author ManerFan 2015年4月10日 * @param ws 西南點 * @param en 東北點 * @return 兩點間的中心點 */ public static Point calCerter(Point ws, Point en) { if (null == ws || null == en) { return null; } return new Point(ws.getX() + (en.getX() - ws.getX()) / 2.0, ws.getY() + (en.getY() - ws.getY()) / 2.0); } /** * <p> * <b>計算向量角</b> * <p> * <pre> * 計算兩點組成的向量與x軸正方向的向量角 * </pre> * * @author ManerFan 2015年4月17日 * @param s 向量起點 * @param d 向量終點 * @return 向量角 */ public static double angleOf(Point s, Point d) { double dist = calLineLen(s, d); if (dist <= 0) { return .0; } double x = d.getX() - s.getX(); // 直角三角形的直邊a double y = d.getY() - s.getY(); // 直角三角形的直邊b if (y >= 0.) { /* 1 2 象限 */ return Math.acos(x / dist); } else { /* 3 4 象限 */ return Math.acos(-x / dist) + Math.PI; } } /** * <p> * <b>修正角度</b> * <p> * <pre> * 修正角度到 [0, 2PI] * </pre> * * @author ManerFan 2015年4月17日 * @param angle 原始角度 * @return 修正後的角度 */ public static double reviseAngle(double angle) { while (angle < 0.) { angle += 2 * Math.PI; } while (angle >= 2 * Math.PI) { angle -= 2 * Math.PI; } return angle; } }
算法的實現思路,簡要以下優化
$$ (\overrightarrow{Ax}) $$url
$$ (\overrightarrow{AB}) $$spa
$$ (\overrightarrow{BC}) $$code
思路圖,簡要以下 blog
實現程序見下get
/** * <p> * <b>最小(凸)包圍邊界查找</b> * <p> * <pre> * 最小(凸)包圍邊界查找 * * Minimum Bounding Polygon (Convex Hull; Smallest Enclosing A Set of Points) * <b><a href="http://alienryderflex.com/smallest_enclosing_polygon/">©2009 Darel Rex Finley.</a></b> * * y * ↑ · · * │ · · · * │ · · · · * │ · · * —│————————————→ x * * </pre> * * @author ManerFan 2015年4月17日 */ public class MinimumBoundingPolygon { public static LinkedList<Point> findSmallestPolygon(List<Point> ps) { if (null == ps || ps.isEmpty()) { return null; } Point corner = findStartPoint(ps); if (null == corner) { return null; } double minAngleDif, oldAngle = 2 * Math.PI; LinkedList<Point> bound = new LinkedList<>(); do { minAngleDif = 2 * Math.PI; bound.add(corner); Point nextPoint = corner; double nextAngle = oldAngle; for (Point p : ps) { if (p.founded) { // 已被加入邊界鏈表的點 continue; } if (p.equals(corner)) { // 重合點 /*if (!p.equals(bound.getFirst())) { p.founded = true; }*/ continue; } double currAngle = DiscretePointUtil.angleOf(corner, p); /* 當前向量與x軸正方向的夾角 */ double angleDif = DiscretePointUtil.reviseAngle(oldAngle - currAngle); /* 兩條向量之間的夾角(順時針旋轉的夾角) */ if (angleDif < minAngleDif) { minAngleDif = angleDif; nextPoint = p; nextAngle = currAngle; } } oldAngle = nextAngle; corner = nextPoint; corner.founded = true; } while (!corner.equals(bound.getFirst())); /* 判斷邊界是否閉合 */ return bound; } /** 查找起始點(保證y最大的狀況下、儘可能使x最小的點) */ private static Point findStartPoint(List<Point> ps) { if (null == ps || ps.isEmpty()) { return null; } Point p = ps.get(0); ListIterator<Point> iter = ps.listIterator(); while (iter.hasNext()) { Point point = iter.next(); if (point.getY() > p.getY() || (point.getY() == p.getY() && point.getX() < p.getX())) { /* 找到最靠上靠左的點 */ p = point; } } return p; } }
結合上邊的幾張圖,想必不難看懂
如下附上一張實際效果圖