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; 031 032import java.util.ArrayList; 033import java.util.Collection; 034import java.util.Collections; 035import java.util.Iterator; 036import java.util.List; 037 038import org.openimaj.citation.annotation.Reference; 039import org.openimaj.citation.annotation.ReferenceType; 040import org.openimaj.math.geometry.line.Line2d; 041import org.openimaj.math.geometry.point.Point2d; 042import org.openimaj.math.geometry.point.Point2dImpl; 043import org.openimaj.math.geometry.point.PointList; 044import org.openimaj.math.geometry.shape.util.PolygonUtils; 045import org.openimaj.math.geometry.shape.util.RotatingCalipers; 046 047import Jama.Matrix; 048 049/** 050 * A polygon, modelled as a list of vertices. Polygon extends {@link PointList}, 051 * so the vertices are the underlying {@link PointList#points}, and they are 052 * considered to be joined in order. 053 * 054 * @author Jonathon Hare (jsh2@ecs.soton.ac.uk) 055 */ 056public class Polygon extends PointList implements Shape 057{ 058 /** 059 * Polygons can contain other polygons. If the polygon is representing a 060 * shape, then the inner polygons can represent holes in the polygon or 061 * other polygons within the polygon. 062 */ 063 private List<Polygon> innerPolygons = new ArrayList<Polygon>(); 064 065 /** If this polygon is a hole within another polygon, this is set to true */ 066 private boolean isHole = false; 067 068 /** 069 * Constructs an empty polygon to which vertices may be added. 070 */ 071 public Polygon() 072 { 073 this(false); 074 } 075 076 /** 077 * Constructs an empty polygon to which vertices may be added. The boolean 078 * parameter determines whether this polygon will represent a hole (rather 079 * than a solid). 080 * 081 * @param representsHole 082 * Whether the polygon will represent a hole. 083 */ 084 public Polygon(boolean representsHole) 085 { 086 this.isHole = representsHole; 087 } 088 089 /** 090 * Construct a Polygon from vertices 091 * 092 * @param vertices 093 * the vertices 094 */ 095 public Polygon(Point2d... vertices) { 096 super(vertices); 097 } 098 099 /** 100 * Construct a Polygon from vertices 101 * 102 * @param vertices 103 * the vertices 104 */ 105 public Polygon(Collection<? extends Point2d> vertices) { 106 this(vertices, false); 107 } 108 109 /** 110 * Construct a Polygon from the vertices, possibly copying the vertices 111 * first 112 * 113 * @param vertices 114 * the vertices 115 * @param copy 116 * should the vertices be copied 117 */ 118 public Polygon(Collection<? extends Point2d> vertices, boolean copy) { 119 super(vertices, copy); 120 } 121 122 /** 123 * Get the vertices of the polygon 124 * 125 * @return the vertices 126 */ 127 public List<Point2d> getVertices() { 128 return points; 129 } 130 131 /** 132 * Get the number of vertices 133 * 134 * @return the number of vertices 135 */ 136 public int nVertices() { 137 if (isClosed()) 138 return points.size() - 1; 139 return points.size(); 140 } 141 142 /** 143 * Is the polygon closed (i.e. is the last vertex equal to the first)? 144 * 145 * @return true if closed; false if open 146 */ 147 public boolean isClosed() { 148 if (points.size() > 0 && points.get(0).getX() == points.get(points.size() - 1).getX() 149 && points.get(0).getY() == points.get(points.size() - 1).getY()) 150 return true; 151 return false; 152 } 153 154 /** 155 * Close the polygon if it's not already closed 156 */ 157 public void close() { 158 if (!isClosed() && points.size() > 0) 159 points.add(points.get(0)); 160 } 161 162 /** 163 * Open the polygon if it's closed 164 */ 165 public void open() { 166 if (isClosed() && points.size() > 1) 167 points.remove(points.size() - 1); 168 } 169 170 /** 171 * Test whether the point p is inside the polygon using the winding rule 172 * algorithm. Also tests whether the point is in any of the inner polygons. 173 * If the inner polygon represents a hole and the point is within that 174 * polygon then the point is not within this polygon. 175 * 176 * @param point 177 * the point to test 178 * @return true if the point is inside; false otherwise 179 */ 180 @Override 181 public boolean isInside(Point2d point) { 182 final boolean isClosed = isClosed(); 183 if (!isClosed) 184 close(); 185 186 boolean isOdd = false; 187 188 for (int pp = 0; pp < getNumInnerPoly(); pp++) 189 { 190 final List<Point2d> v = getInnerPoly(pp).getVertices(); 191 int j = v.size() - 1; 192 for (int i = 0; i < v.size(); i++) { 193 if (v.get(i).getY() < point.getY() && v.get(j).getY() >= point.getY() || 194 v.get(j).getY() < point.getY() && v.get(i).getY() >= point.getY()) 195 { 196 if (v.get(i).getX() + (point.getY() - v.get(i).getY()) / 197 (v.get(j).getY() - v.get(i).getY()) * (v.get(j).getX() - v.get(i).getX()) < point.getX()) 198 { 199 isOdd = !isOdd; 200 } 201 } 202 j = i; 203 } 204 } 205 206 if (!isClosed) 207 open(); 208 return isOdd; 209 } 210 211 /** 212 * {@inheritDoc} 213 */ 214 @Override 215 public Polygon clone() { 216 final Polygon clone = new Polygon(); 217 clone.setIsHole(isHole); 218 219 for (final Point2d p : points) 220 clone.points.add(p.copy()); 221 222 for (final Polygon innerPoly : innerPolygons) 223 clone.addInnerPolygon(innerPoly.clone()); 224 225 return clone; 226 } 227 228 /** 229 * Calculates the difference between two polygons and returns a new polygon. 230 * It assumes that the given polygon and this polygon have the same number 231 * of vertices. 232 * 233 * @param p 234 * the polygon to subtract. 235 * @return the difference polygon 236 */ 237 public Polygon difference(Polygon p) 238 { 239 final List<Point2d> v = new ArrayList<Point2d>(); 240 241 for (int i = 0; i < points.size(); i++) 242 v.add(new Point2dImpl( 243 points.get(i).getX() - p.getVertices().get(i).getX(), 244 points.get(i).getY() - p.getVertices().get(i).getY())); 245 246 final Polygon p2 = new Polygon(v); 247 for (int i = 0; i < innerPolygons.size(); i++) 248 p2.addInnerPolygon(innerPolygons.get(i).difference( 249 p2.getInnerPoly(i + 1))); 250 251 return p2; 252 } 253 254 @Override 255 public double calculateArea() { 256 return Math.abs(calculateSignedArea()); 257 } 258 259 /** 260 * Calculate the area of the polygon. This does not take into account holes 261 * in the polygon. 262 * 263 * @return the area of the polygon 264 */ 265 public double calculateSignedArea() { 266 final boolean closed = isClosed(); 267 double area = 0; 268 269 if (!closed) 270 close(); 271 272 // TODO: This does not take into account the winding 273 // rule and therefore holes 274 for (int k = 0; k < points.size() - 1; k++) { 275 final float ik = points.get(k).getX(); 276 final float jk = points.get(k).getY(); 277 final float ik1 = points.get(k + 1).getX(); 278 final float jk1 = points.get(k + 1).getY(); 279 280 area += ik * jk1 - ik1 * jk; 281 } 282 283 if (!closed) 284 open(); 285 286 return 0.5 * area; 287 } 288 289 /** 290 * Calls {@link Polygon#intersectionArea(Shape, int)} with 1 step per pixel 291 * dimension. Subsequently this function returns the shared whole pixels of 292 * this polygon and that. 293 * 294 * @param that 295 * @return intersection area 296 */ 297 @Override 298 public double intersectionArea(Shape that) { 299 return this.intersectionArea(that, 1); 300 } 301 302 /** 303 * Return an estimate for the area of the intersection of this polygon and 304 * another polygon. For each pixel step 1 is added if the point is inside 305 * both polygons. For each pixel, perPixelPerDimension steps are taken. 306 * Subsequently the intersection is: 307 * 308 * sumIntersections / (perPixelPerDimension * perPixelPerDimension) 309 * 310 * @param that 311 * @return normalised intersection area 312 */ 313 @Override 314 public double intersectionArea(Shape that, int nStepsPerDimension) { 315 final Rectangle overlapping = this.calculateRegularBoundingBox().overlapping(that.calculateRegularBoundingBox()); 316 if (overlapping == null) 317 return 0; 318 double intersection = 0; 319 final double step = Math.max(overlapping.width, overlapping.height) / (double) nStepsPerDimension; 320 double nReads = 0; 321 for (float x = overlapping.x; x < overlapping.x + overlapping.width; x += step) { 322 for (float y = overlapping.y; y < overlapping.y + overlapping.height; y += step) { 323 final boolean insideThis = this.isInside(new Point2dImpl(x, y)); 324 final boolean insideThat = that.isInside(new Point2dImpl(x, y)); 325 nReads++; 326 if (insideThis && insideThat) { 327 intersection++; 328 } 329 } 330 } 331 332 return (intersection / nReads) * (overlapping.width * overlapping.height); 333 } 334 335 /** 336 * {@inheritDoc} 337 * 338 * @see org.openimaj.math.geometry.shape.Shape#asPolygon() 339 */ 340 @Override 341 public Polygon asPolygon() { 342 return this; 343 } 344 345 /** 346 * Add a vertex to the polygon 347 * 348 * @param x 349 * x-coordinate of the vertex 350 * @param y 351 * y-coordinate of the vertex 352 */ 353 public void addVertex(float x, float y) { 354 points.add(new Point2dImpl(x, y)); 355 } 356 357 /** 358 * Add a vertex to the polygon 359 * 360 * @param pt 361 * coordinate of the vertex 362 */ 363 public void addVertex(Point2d pt) { 364 points.add(pt); 365 } 366 367 /** 368 * Iterates through the vertices and rounds all vertices to round integers. 369 * Side-affects this polygon. 370 * 371 * @return this polygon 372 */ 373 public Polygon roundVertices() 374 { 375 final Iterator<Point2d> i = this.iterator(); 376 while (i.hasNext()) 377 { 378 final Point2d p = i.next(); 379 final Point2dImpl p2 = new Point2dImpl((int) p.getX(), (int) p.getY()); 380 381 int xx = -1; 382 if ((xx = this.points.indexOf(p2)) != -1 && 383 this.points.get(xx) != p) 384 i.remove(); 385 else 386 { 387 p.setX(p2.x); 388 p.setY(p2.y); 389 } 390 } 391 392 for (final Polygon pp : innerPolygons) 393 pp.roundVertices(); 394 395 return this; 396 } 397 398 /** 399 * Return whether this polygon has no vertices or not. 400 * 401 * @return TRUE if this polygon has no vertices 402 */ 403 public boolean isEmpty() 404 { 405 return this.points.isEmpty() && innerPolygons.isEmpty(); 406 } 407 408 /** 409 * Returns the number of inner polygons in this polygon including this 410 * polygon. 411 * 412 * @return the number of inner polygons in this polygon. 413 */ 414 public int getNumInnerPoly() 415 { 416 return innerPolygons.size() + 1; 417 } 418 419 /** 420 * Get the inner polygon at the given index. Note that index 0 will return 421 * this polygon, while index i+1 will return the inner polygon i. 422 * 423 * @param index 424 * the index of the polygon to get 425 * @return The inner polygon at the given index. 426 */ 427 public Polygon getInnerPoly(int index) 428 { 429 if (index == 0) 430 return this; 431 return innerPolygons.get(index - 1); 432 } 433 434 /** 435 * Add an inner polygon to this polygon. If there is no main polygon defined 436 * (the number of vertices is zero) then the given inner polygon will become 437 * the main polygon if the <code>inferOuter</code> boolean is true. If this 438 * variable is false, the inner polygon will be added to the list of inner 439 * polygons for this polygon regardless of whether a main polygon exists. 440 * When the main polygon is inferred from the given polygon, the vertices 441 * are copied into this polygon's vertex list. 442 * 443 * @param p 444 * The inner polygon to add 445 * @param inferOuter 446 * Whether to infer the outer polygon from this inner one 447 */ 448 public void addInnerPolygon(Polygon p, boolean inferOuter) 449 { 450 if (!inferOuter) 451 { 452 this.innerPolygons.add(p); 453 } 454 else 455 { 456 if (this.points.size() == 0) 457 { 458 this.points.addAll(p.points); 459 this.isHole = p.isHole; 460 } 461 else 462 { 463 this.addInnerPolygon(p, false); 464 } 465 } 466 } 467 468 /** 469 * Add an inner polygon to this polygon. If there is no main polygon defined 470 * (the number of vertices is zero) then the given inner polygon will become 471 * the main polygon. 472 * 473 * @param p 474 * The inner polygon to add 475 */ 476 public void addInnerPolygon(Polygon p) 477 { 478 this.addInnerPolygon(p, true); 479 } 480 481 /** 482 * Returns the list of inner polygons. 483 * 484 * @return the list of inner polygons 485 */ 486 public List<Polygon> getInnerPolys() 487 { 488 return this.innerPolygons; 489 } 490 491 /** 492 * Set whether this polygon represents a hole in another polygon. 493 * 494 * @param isHole 495 * Whether this polygon is a whole. 496 */ 497 public void setIsHole(boolean isHole) 498 { 499 this.isHole = isHole; 500 } 501 502 /** 503 * Returns whether this polygon is representing a hole in another polygon. 504 * 505 * @return Whether this polygon is representing a hole in another polygon. 506 */ 507 public boolean isHole() 508 { 509 return this.isHole; 510 } 511 512 /** 513 * {@inheritDoc} 514 * 515 * @see java.lang.Object#equals(java.lang.Object) 516 */ 517 @Override 518 public boolean equals(Object obj) 519 { 520 return (obj instanceof Polygon) && 521 this.equals((Polygon) obj); 522 } 523 524 /** 525 * Specific equals method for polygons where the polgyons are tested for the 526 * vertices being in the same order. If the vertices are not in the vertex 527 * list in the same manner but are in the same order (when wrapped around) 528 * the method will return true. So the triangles below will return true: 529 * 530 * {[1,1],[2,2],[1,2]} and {[1,2],[1,1],[2,2]} 531 * 532 * @param p 533 * The polygon to test against 534 * @return TRUE if the polygons are the same. 535 */ 536 public boolean equals(Polygon p) 537 { 538 if (isHole() != p.isHole()) 539 return false; 540 if (this.points.size() != p.points.size()) 541 return false; 542 if (this.isEmpty() && p.isEmpty()) 543 return true; 544 545 final int i = this.points.indexOf(p.points.get(0)); 546 if (i == -1) 547 return false; 548 549 final int s = this.points.size(); 550 for (int n = 0; n < s; n++) 551 { 552 if (!p.points.get(n).equals(this.points.get((n + i) % s))) 553 return false; 554 } 555 556 return true; 557 } 558 559 /** 560 * {@inheritDoc} 561 * 562 * @see java.lang.Object#hashCode() 563 */ 564 @Override 565 public int hashCode() 566 { 567 return points.hashCode() * (isHole() ? -1 : 1); 568 } 569 570 /** 571 * Displays the complete list of vertices unless the number of vertices is 572 * greater than 10 - then a sublist is shown of 5 from the start and 5 from 573 * the end separated by ellipses. 574 * 575 * {@inheritDoc} 576 * 577 * @see java.lang.Object#toString() 578 */ 579 @Override 580 public String toString() 581 { 582 final StringBuffer sb = new StringBuffer(); 583 if (isHole()) 584 sb.append("H"); 585 final int len = 10; 586 if (points.size() < len) 587 sb.append(points.toString()); 588 else 589 sb.append(points.subList(0, len / 2).toString() + "..." + 590 points.subList(points.size() - len / 2, points.size()) 591 .toString()); 592 593 if (innerPolygons.size() > 0) 594 { 595 sb.append("\n - " + innerPolygons.size() + " inner polygons:"); 596 for (final Polygon ip : innerPolygons) 597 sb.append("\n + " + ip.toString()); 598 } 599 600 return sb.toString(); 601 } 602 603 /** 604 * Returns the intersection of this polygon and the given polygon. 605 * 606 * @param p2 607 * The polygon to intersect with. 608 * @return The resulting polygon intersection 609 */ 610 public Polygon intersect(Polygon p2) 611 { 612 // FIXME: PolygonUtils should handle inner polys itself, but it seems 613 // there are bugs... This is a hack to work around those problems 614 // without having to understand the ins and outs of how the GPC works, 615 // but it should really be fixed properly in the future! 616 final Polygon clone = new PolygonUtils().intersection(new Polygon(this.points), p2); 617 clone.setIsHole(isHole); 618 619 for (final Polygon innerPoly : innerPolygons) 620 clone.addInnerPolygon(innerPoly.intersect(p2)); 621 622 return clone; 623 } 624 625 /** 626 * Returns the union of this polygon and the given polygon. 627 * 628 * @param p2 629 * The polygon to union with. 630 * @return The resulting polygon union 631 */ 632 public Polygon union(Polygon p2) 633 { 634 return new PolygonUtils().union(this, p2); 635 } 636 637 /** 638 * Returns the XOR of this polygon and the given polygon. 639 * 640 * @param p2 641 * The polygon to XOR with. 642 * @return The resulting polygon 643 */ 644 public Polygon xor(Polygon p2) 645 { 646 return new PolygonUtils().xor(this, p2); 647 } 648 649 /** 650 * Reduce the number of vertices in a polygon. This implementation is based 651 * on a modified Ramer-Douglas–Peucker algorithm that is designed to work 652 * with polygons rather than polylines. The implementation searches for two 653 * initialisation points that are approximatatly maximally distant, then 654 * performs the polyline algorithm on the two segments, and finally ensures 655 * that the joins in the segments are consistent with the given epsilon 656 * value. 657 * 658 * @param eps 659 * distance value below which a vertex can be ignored 660 * @return new polygon that approximates this polygon, but with fewer 661 * vertices 662 */ 663 public Polygon reduceVertices(double eps) 664 { 665 if (eps == 0 || nVertices() <= 3) 666 return this.clone(); 667 668 int prevStartIndex = 0; 669 int startIndex = 0; 670 for (int init = 0; init < 3; init++) { 671 double dmax = 0; 672 prevStartIndex = startIndex; 673 674 final Point2d first = points.get(startIndex); 675 for (int i = 0; i < points.size(); i++) { 676 final double d; 677 d = Line2d.distance(first, points.get(i)); 678 679 if (d > dmax) { 680 startIndex = i; 681 dmax = d; 682 } 683 } 684 } 685 686 if (prevStartIndex > startIndex) { 687 final int tmp = prevStartIndex; 688 prevStartIndex = startIndex; 689 startIndex = tmp; 690 } 691 692 final List<Point2d> l1 = new ArrayList<Point2d>(); 693 l1.addAll(points.subList(prevStartIndex, startIndex + 1)); 694 695 final List<Point2d> l2 = new ArrayList<Point2d>(); 696 l2.addAll(points.subList(startIndex, points.size())); 697 l2.addAll(points.subList(0, prevStartIndex + 1)); 698 699 final Polygon newPoly = new Polygon(); 700 final List<Point2d> line1 = ramerDouglasPeucker(l1, eps); 701 final List<Point2d> line2 = ramerDouglasPeucker(l2, eps); 702 newPoly.points.addAll(line1.subList(0, line1.size() - 1)); 703 newPoly.points.addAll(line2.subList(0, line2.size() - 1)); 704 705 // deal with the joins 706 // if (newPoly.nVertices() > 3) { 707 // Point2d r1 = null, r2 = null; 708 // Point2d p0 = newPoly.points.get(newPoly.points.size() - 1); 709 // Point2d p1 = newPoly.points.get(0); 710 // Point2d p2 = newPoly.points.get(1); 711 // 712 // Line2d l = new Line2d(p0, p2); 713 // Line2d norm = l.getNormal(p1); 714 // Point2d intersect = l.getIntersection(norm).intersectionPoint; 715 // if (intersect != null && Line2d.distance(p1, intersect) <= eps) { 716 // // remove p1 717 // r1 = p1; 718 // } 719 // 720 // p0 = newPoly.points.get(line1.size() - 1); 721 // p1 = newPoly.points.get(line1.size()); 722 // p2 = newPoly.points.get((line1.size() + 1) >= newPoly.size() ? 0 : 723 // (line1.size() + 1)); 724 // 725 // l = new Line2d(p0, p2); 726 // norm = l.getNormal(p1); 727 // intersect = l.getIntersection(norm).intersectionPoint; 728 // if (intersect != null && Line2d.distance(p1, intersect) <= eps) { 729 // // remove p2 730 // r2 = p2; 731 // } 732 // 733 // if (r1 != null) { 734 // newPoly.points.remove(r1); 735 // } 736 // if (newPoly.nVertices() > 3 && r2 != null) { 737 // newPoly.points.remove(r2); 738 // } 739 // } 740 741 if (!newPoly.isClockwise()) { 742 Collections.reverse(newPoly.points); 743 } 744 745 for (final Polygon ppp : innerPolygons) 746 newPoly.addInnerPolygon(ppp.reduceVertices(eps)); 747 748 return newPoly; 749 } 750 751 /** 752 * Ramer Douglas Peucker polyline algorithm 753 * 754 * @param p 755 * the polyline to simplify 756 * @param eps 757 * distance value below which a vertex can be ignored 758 * @return the simplified polyline 759 */ 760 private static List<Point2d> ramerDouglasPeucker(List<Point2d> p, double eps) { 761 // Find the point with the maximum distance 762 double dmax = 0; 763 int index = 0; 764 final int end = p.size() - 1; 765 final Line2d l = new Line2d(p.get(0), p.get(end)); 766 for (int i = 1; i < end - 1; i++) { 767 final double d; 768 769 final Point2d p1 = p.get(i); 770 final Line2d norm = l.getNormal(p1); 771 final Point2d p2 = l.getIntersection(norm).intersectionPoint; 772 if (p2 == null) 773 continue; 774 d = Line2d.distance(p1, p2); 775 776 if (d > dmax) { 777 index = i; 778 dmax = d; 779 } 780 } 781 782 final List<Point2d> newPoly = new ArrayList<Point2d>(); 783 784 // If max distance is greater than epsilon, recursively simplify 785 if (dmax > eps) { 786 // Recursively call RDP 787 final List<Point2d> line1 = ramerDouglasPeucker(p.subList(0, index + 1), eps); 788 final List<Point2d> line2 = ramerDouglasPeucker(p.subList(index, end + 1), eps); 789 newPoly.addAll(line1.subList(0, line1.size() - 1)); 790 newPoly.addAll(line2); 791 } else { 792 newPoly.add(p.get(0)); 793 newPoly.add(p.get(end)); 794 } 795 796 // Return the result 797 return newPoly; 798 } 799 800 /** 801 * Apply a 3x3 transform matrix to a copy of the polygon and return it 802 * 803 * @param transform 804 * 3x3 transform matrix 805 * @return the transformed polygon 806 */ 807 @Override 808 public Polygon transform(Matrix transform) { 809 final List<Point2d> newVertices = new ArrayList<Point2d>(); 810 811 for (final Point2d p : points) { 812 final Matrix p1 = new Matrix(3, 1); 813 p1.set(0, 0, p.getX()); 814 p1.set(1, 0, p.getY()); 815 p1.set(2, 0, 1); 816 817 final Matrix p2_est = transform.times(p1); 818 819 final Point2d out = new Point2dImpl((float) (p2_est.get(0, 0) / p2_est.get(2, 0)), 820 (float) (p2_est.get(1, 0) / p2_est.get(2, 0))); 821 822 newVertices.add(out); 823 } 824 825 final Polygon p = new Polygon(newVertices); 826 for (final Polygon pp : innerPolygons) 827 p.addInnerPolygon(pp.transform(transform)); 828 return p; 829 } 830 831 /** 832 * Compute the regular (oriented to the axes) bounding box of the polygon. 833 * 834 * @return the regular bounding box as [x,y,width,height] 835 */ 836 @Override 837 public Rectangle calculateRegularBoundingBox() { 838 float xmin = Float.MAX_VALUE, xmax = -Float.MAX_VALUE, ymin = Float.MAX_VALUE, ymax = -Float.MAX_VALUE; 839 840 for (int pp = 0; pp < getNumInnerPoly(); pp++) 841 { 842 final Polygon ppp = getInnerPoly(pp); 843 for (final Point2d p : ppp.getVertices()) { 844 final float px = p.getX(); 845 final float py = p.getY(); 846 if (px < xmin) 847 xmin = px; 848 if (px > xmax) 849 xmax = px; 850 if (py < ymin) 851 ymin = py; 852 if (py > ymax) 853 ymax = py; 854 855 } 856 } 857 858 return new Rectangle(xmin, ymin, xmax - xmin, ymax - ymin); 859 } 860 861 /** 862 * Translate the polygons position 863 * 864 * @param x 865 * x-translation 866 * @param y 867 * y-translation 868 */ 869 @Override 870 public void translate(float x, float y) { 871 for (int pp = 0; pp < getNumInnerPoly(); pp++) 872 { 873 final Polygon ppp = getInnerPoly(pp); 874 for (final Point2d p : ppp.getVertices()) { 875 p.setX(p.getX() + x); 876 p.setY(p.getY() + y); 877 } 878 } 879 } 880 881 /** 882 * Scale the polygon by the given amount about (0,0). Scalefactors between 0 883 * and 1 shrink the polygon. 884 * 885 * @param sc 886 * the scale factor. 887 */ 888 @Override 889 public void scale(float sc) { 890 for (int pp = 0; pp < getNumInnerPoly(); pp++) 891 { 892 final Polygon ppp = getInnerPoly(pp); 893 for (final Point2d p : ppp.getVertices()) { 894 p.setX(p.getX() * sc); 895 p.setY(p.getY() * sc); 896 } 897 } 898 } 899 900 /** 901 * Scale the polygon only in the x-direction by the given amount about 902 * (0,0). Scale factors between 0 and 1 will shrink the polygon 903 * 904 * @param sc 905 * The scale factor 906 * @return this polygon 907 */ 908 @Override 909 public Polygon scaleX(float sc) 910 { 911 for (int pp = 0; pp < getNumInnerPoly(); pp++) 912 { 913 final Polygon ppp = getInnerPoly(pp); 914 for (final Point2d p : ppp.getVertices()) { 915 p.setX(p.getX() * sc); 916 } 917 } 918 return this; 919 } 920 921 /** 922 * Scale the polygon only in the y-direction by the given amount about 923 * (0,0). Scale factors between 0 and 1 will shrink the polygon 924 * 925 * @param sc 926 * The scale factor 927 * @return this polygon 928 */ 929 @Override 930 public Polygon scaleY(float sc) 931 { 932 for (int pp = 0; pp < getNumInnerPoly(); pp++) 933 { 934 final Polygon ppp = getInnerPoly(pp); 935 for (final Point2d p : ppp.getVertices()) { 936 p.setY(p.getY() * sc); 937 } 938 } 939 return this; 940 } 941 942 /** 943 * Scale the polygon by the given amount about (0,0). Scale factors between 944 * 0 and 1 shrink the polygon. 945 * 946 * @param scx 947 * the scale factor in the x direction 948 * @param scy 949 * the scale factor in the y direction. 950 * @return this polygon 951 */ 952 @Override 953 public Polygon scaleXY(float scx, float scy) 954 { 955 for (int pp = 0; pp < getNumInnerPoly(); pp++) 956 { 957 final Polygon ppp = getInnerPoly(pp); 958 for (final Point2d p : ppp.getVertices()) { 959 p.setX(p.getX() * scx); 960 p.setY(p.getY() * scy); 961 } 962 } 963 return this; 964 } 965 966 /** 967 * Scale the polygon by the given amount about the given point. Scalefactors 968 * between 0 and 1 shrink the polygon. 969 * 970 * @param centre 971 * the centre of the scaling operation 972 * @param sc 973 * the scale factor 974 */ 975 @Override 976 public void scale(Point2d centre, float sc) { 977 this.translate(-centre.getX(), -centre.getY()); 978 for (int pp = 0; pp < getNumInnerPoly(); pp++) 979 { 980 final Polygon ppp = getInnerPoly(pp); 981 for (final Point2d p : ppp.getVertices()) { 982 p.setX(p.getX() * sc); 983 p.setY(p.getY() * sc); 984 } 985 } 986 this.translate(centre.getX(), centre.getY()); 987 } 988 989 /** 990 * Calculate the centroid of the polygon. 991 * 992 * @return calls {@link #calculateFirstMoment()}; 993 */ 994 @Override 995 public Point2d calculateCentroid() { 996 final double[] pt = calculateFirstMoment(); 997 return new Point2dImpl((float) pt[0], (float) pt[1]); 998 } 999 1000 /** 1001 * Treating the polygon as a continuous piecewise function, calculate 1002 * exactly the first moment. This follows working presented by Carsten 1003 * Steger in "On the Calculation of Moments of Polygons" , 1004 * 1005 * @return the first moment 1006 */ 1007 @Reference( 1008 author = { "Carsten Steger" }, 1009 title = "On the Calculation of Moments of Polygons", 1010 type = ReferenceType.Techreport, 1011 month = "August", 1012 year = "1996", 1013 url = "http://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.29.8765&rep=rep1&type=pdf" 1014 ) 1015 public double[] calculateFirstMoment() { 1016 final boolean closed = isClosed(); 1017 1018 if (!closed) 1019 close(); 1020 1021 double area = 0; 1022 double ax = 0; 1023 double ay = 0; 1024 // TODO: This does not take into account the winding 1025 // rule and therefore holes 1026 for (int k = 0; k < points.size() - 1; k++) { 1027 final float xk = points.get(k).getX(); 1028 final float yk = points.get(k).getY(); 1029 final float xk1 = points.get(k + 1).getX(); 1030 final float yk1 = points.get(k + 1).getY(); 1031 1032 final float shared = xk * yk1 - xk1 * yk; 1033 area += shared; 1034 ax += (xk + xk1) * shared; 1035 ay += (yk + yk1) * shared; 1036 } 1037 1038 if (!closed) 1039 open(); 1040 1041 area *= 0.5; 1042 1043 return new double[] { ax / (6 * area), ay / (6 * area) }; 1044 } 1045 1046 /** 1047 * Treating the polygon as a continuous piecewise function, calculate 1048 * exactly the second moment. This follows working presented by Carsten 1049 * Steger in "On the Calculation of Moments of Polygons" , 1050 * 1051 * @return the second moment as an array with values: (axx,axy,ayy) 1052 */ 1053 @Reference( 1054 author = { "Carsten Steger" }, 1055 title = "On the Calculation of Moments of Polygons", 1056 type = ReferenceType.Techreport, 1057 month = "August", 1058 year = "1996", 1059 url = "http://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.29.8765&rep=rep1&type=pdf" 1060 ) 1061 public double[] calculateSecondMoment() { 1062 final boolean closed = isClosed(); 1063 final double area = calculateSignedArea(); 1064 1065 if (!closed) 1066 close(); 1067 1068 double axx = 0; 1069 double ayy = 0; 1070 double axy = 0; 1071 // TODO: This does not take into account the winding 1072 // rule and therefore holes 1073 for (int k = 0; k < points.size() - 1; k++) { 1074 final float xk = points.get(k).getX(); 1075 final float yk = points.get(k).getY(); 1076 final float xk1 = points.get(k + 1).getX(); 1077 final float yk1 = points.get(k + 1).getY(); 1078 1079 final float shared = xk * yk1 - xk1 * yk; 1080 axx += (xk * xk + xk * xk1 + xk1 * xk1) * shared; 1081 ayy += (yk * yk + yk * yk1 + yk1 * yk1) * shared; 1082 axy += (2 * xk * yk + xk * yk1 + xk1 * yk + 2 * xk1 * yk1) * shared; 1083 } 1084 1085 if (!closed) 1086 open(); 1087 1088 return new double[] { 1089 axx / (12 * area), 1090 axy / (24 * area), 1091 ayy / (12 * area) 1092 }; 1093 } 1094 1095 /** 1096 * Treating the polygon as a continuous piecewise function, calculate 1097 * exactly the centralised second moment. This follows working presented by 1098 * Carsten Steger in "On the Calculation of Moments of Polygons" , 1099 * 1100 * @return the second moment as an array with values: (axx,axy,ayy) 1101 */ 1102 @Reference( 1103 author = { "Carsten Steger" }, 1104 title = "On the Calculation of Moments of Polygons", 1105 type = ReferenceType.Techreport, 1106 month = "August", 1107 year = "1996", 1108 url = "http://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.29.8765&rep=rep1&type=pdf" 1109 ) 1110 public double[] calculateSecondMomentCentralised() { 1111 final double[] firstMoment = this.calculateFirstMoment(); 1112 final double[] secondMoment = this.calculateSecondMoment(); 1113 1114 return new double[] { 1115 secondMoment[0] - firstMoment[0] * firstMoment[0], 1116 secondMoment[1] - firstMoment[0] * firstMoment[1], 1117 secondMoment[2] - firstMoment[1] * firstMoment[1], 1118 }; 1119 1120 } 1121 1122 /** 1123 * Calculates the principle direction of the connected component. This is 1124 * given by 1125 * <code>0.5 * atan( (M<sub>20</sub>-M<sub>02</sub>) / 2 * M<sub>11</sub> )</code> 1126 * so results in an angle between -PI and +PI. 1127 * 1128 * @return The principle direction (-PI/2 to +PI/2 radians) of the connected 1129 * component. 1130 */ 1131 public double calculateDirection() { 1132 final double[] secondMoment = calculateSecondMomentCentralised(); 1133 final double u20 = secondMoment[0]; 1134 final double u02 = secondMoment[1]; 1135 final double u11 = secondMoment[2]; 1136 final double theta = 0.5 * Math.atan2((2 * u11), (u20 - u02)); 1137 1138 return theta; 1139 } 1140 1141 /** 1142 * Using 1143 * {@link EllipseUtilities#ellipseFromCovariance(float, float, Matrix, float)} 1144 * and the {@link #calculateSecondMomentCentralised()} return the Ellipse 1145 * best fitting the shape of this polygon. 1146 * 1147 * @return {@link Ellipse} defining the shape of the polygon 1148 */ 1149 public Ellipse toEllipse() { 1150 final double[] secondMoment = calculateSecondMomentCentralised(); 1151 final double u20 = secondMoment[0]; 1152 final double u11 = secondMoment[1]; 1153 final double u02 = secondMoment[2]; 1154 final Point2d center = calculateCentroid(); 1155 final Matrix sm = new Matrix(new double[][] { 1156 new double[] { u20, u11 }, 1157 new double[] { u11, u02 }, 1158 }); 1159 // Used the sqrt(3) as the scale, not sure why. This is not correct. 1160 // Find the correct value! 1161 return EllipseUtilities.ellipseFromCovariance( 1162 center.getX(), 1163 center.getY(), 1164 sm, 1165 (float) Math.sqrt(3) 1166 ); 1167 } 1168 1169 /** 1170 * Test if the outer polygon is convex. 1171 * 1172 * @return true if the outer polygon is convex; false if non-convex or less 1173 * than two vertices 1174 */ 1175 @Override 1176 public boolean isConvex() { 1177 final boolean isOriginallyClosed = this.isClosed(); 1178 if (isOriginallyClosed) 1179 this.open(); 1180 1181 final int size = size(); 1182 1183 if (size < 3) 1184 return false; 1185 1186 float res = 0; 1187 for (int i = 0; i < size; i++) { 1188 final Point2d p = points.get(i); 1189 final Point2d tmp = points.get((i + 1) % size); 1190 final Point2dImpl v = new Point2dImpl(); 1191 v.x = tmp.getX() - p.getX(); 1192 v.y = tmp.getY() - p.getY(); 1193 final Point2d u = points.get((i + 2) % size); 1194 1195 if (i == 0) // in first loop direction is unknown, so save it in res 1196 res = u.getX() * v.y - u.getY() * v.x + v.x * p.getY() - v.y * p.getX(); 1197 else 1198 { 1199 final float newres = u.getX() * v.y - u.getY() * v.x + v.x * p.getY() - v.y * p.getX(); 1200 if ((newres > 0 && res < 0) || (newres < 0 && res > 0)) 1201 return false; 1202 } 1203 } 1204 1205 if (isOriginallyClosed) 1206 close(); 1207 1208 return true; 1209 } 1210 1211 /** 1212 * Test if this has no inner polygons or sub-parts 1213 * 1214 * @see Polygon#getInnerPolys() 1215 * 1216 * @return true if this is polygon has no inner polygons; false otherwise. 1217 */ 1218 public boolean hasNoInnerPolygons() { 1219 return innerPolygons == null || innerPolygons.size() == 0; 1220 } 1221 1222 @Override 1223 public double calculatePerimeter() { 1224 final Point2d p1 = points.get(0); 1225 float p1x = p1.getX(); 1226 float p1y = p1.getY(); 1227 1228 Point2d p2 = points.get(points.size() - 1); 1229 float p2x = p2.getX(); 1230 float p2y = p2.getY(); 1231 1232 double peri = Line2d.distance(p1x, p1y, p2x, p2y); 1233 for (int i = 1; i < this.points.size(); i++) { 1234 p2 = points.get(i); 1235 p2x = p2.getX(); 1236 p2y = p2.getY(); 1237 peri += Line2d.distance(p1x, p1y, p2x, p2y); 1238 p1x = p2x; 1239 p1y = p2y; 1240 } 1241 1242 return peri; 1243 } 1244 1245 /** 1246 * Test (outer) polygon path direction 1247 * 1248 * @return true is the outer path direction is clockwise w.r.t OpenIMAJ 1249 * coordinate system 1250 */ 1251 public boolean isClockwise() { 1252 double signedArea = 0; 1253 for (int i = 0; i < this.points.size() - 1; i++) { 1254 final float x1 = points.get(i).getX(); 1255 final float y1 = points.get(i).getY(); 1256 final float x2 = points.get(i + 1).getX(); 1257 final float y2 = points.get(i + 1).getY(); 1258 1259 signedArea += (x1 * y2 - x2 * y1); 1260 } 1261 1262 final float x1 = points.get(points.size() - 1).getX(); 1263 final float y1 = points.get(points.size() - 1).getY(); 1264 final float x2 = points.get(0).getX(); 1265 final float y2 = points.get(0).getY(); 1266 signedArea += (x1 * y2 - x2 * y1); 1267 1268 return signedArea >= 0; 1269 } 1270 1271 /** 1272 * Calculate convex hull using Melkman's algorithm. This is faster than the 1273 * {@link #calculateConvexHull()} method, but will only work for simple 1274 * polygons (those that don't self-intersect) 1275 * <p> 1276 * Based on http://softsurfer.com/Archive/algorithm_0203/algorithm_0203.htm, 1277 * but modified (fixed) to deal with the problem of the initialisation 1278 * points potentially being co-linear. 1279 * <p> 1280 * Copyright 2001, softSurfer (www.softsurfer.com) This code may be freely 1281 * used and modified for any purpose providing that this copyright notice is 1282 * included with it. SoftSurfer makes no warranty for this code, and cannot 1283 * be held liable for any real or imagined damage resulting from its use. 1284 * Users of this code must verify correctness for their application. 1285 * 1286 * @return A polygon defining the shape of the convex hull 1287 */ 1288 public Polygon calculateConvexHullMelkman() { 1289 if (this.points.size() <= 3) 1290 return new Polygon(this.points); 1291 1292 final int n = this.points.size(); 1293 int i = 1; 1294 while (i + 1 < n && isLeft(points.get(0), points.get(i), points.get(i + 1)) == 0) 1295 i++; 1296 1297 if (n - i <= 3) 1298 return new Polygon(this.points); 1299 1300 // initialize a deque D[] from bottom to top so that the 1301 // 1st three vertices of V[] are a counterclockwise triangle 1302 final Point2d[] D = new Point2d[2 * n + 1]; 1303 int bot = n - 2, top = bot + 3; // initial bottom and top deque indices 1304 D[bot] = D[top] = points.get(i + 1); // 3rd vertex is at both bot and 1305 // top 1306 if (isLeft(points.get(0), points.get(i), points.get(i + 1)) > 0) { 1307 D[bot + 1] = points.get(0); 1308 D[bot + 2] = points.get(i); // ccw vertices are: 2,0,1,2 1309 } else { 1310 D[bot + 1] = points.get(i); 1311 D[bot + 2] = points.get(0); // ccw vertices are: 2,1,0,2 1312 } 1313 1314 i += 2; 1315 1316 // compute the hull on the deque D[] 1317 for (; i < n; i++) { // process the rest of vertices 1318 // test if next vertex is inside the deque hull 1319 if ((isLeft(D[bot], D[bot + 1], points.get(i)) > 0) && 1320 (isLeft(D[top - 1], D[top], points.get(i)) > 0)) 1321 continue; // skip an interior vertex 1322 1323 // incrementally add an exterior vertex to the deque hull 1324 // get the rightmost tangent at the deque bot 1325 while (isLeft(D[bot], D[bot + 1], points.get(i)) <= 0) 1326 ++bot; // remove bot of deque 1327 D[--bot] = points.get(i); // insert V[i] at bot of deque 1328 1329 // get the leftmost tangent at the deque top 1330 while (isLeft(D[top - 1], D[top], points.get(i)) <= 0) 1331 --top; // pop top of deque 1332 D[++top] = points.get(i); // push V[i] onto top of deque 1333 } 1334 1335 // transcribe deque D[] to the output hull array H[] 1336 final Polygon H = new Polygon(); 1337 final List<Point2d> vertices = H.getVertices(); 1338 for (int h = 0; h <= (top - bot); h++) 1339 vertices.add(D[bot + h]); 1340 1341 return H; 1342 } 1343 1344 private float isLeft(Point2d P0, Point2d P1, Point2d P2) { 1345 return (P1.getX() - P0.getX()) * (P2.getY() - P0.getY()) - (P2.getX() - 1346 P0.getX()) * (P1.getY() - P0.getY()); 1347 } 1348 1349 /** 1350 * Compute the minimum size rotated bounding rectangle that contains this 1351 * shape using the rotating calipers approach. 1352 * 1353 * @see org.openimaj.math.geometry.shape.Shape#minimumBoundingRectangle() 1354 * @see RotatingCalipers#getMinimumBoundingRectangle(Polygon, boolean) 1355 */ 1356 @Override 1357 public RotatedRectangle minimumBoundingRectangle() { 1358 return RotatingCalipers.getMinimumBoundingRectangle(this, false); 1359 } 1360 1361 /** 1362 * Compute the minimum size rotated bounding rectangle that contains this 1363 * shape using the rotating calipers approach. 1364 * 1365 * @see RotatingCalipers#getMinimumBoundingRectangle(Polygon, boolean) 1366 * 1367 * @param assumeSimple 1368 * can the algorithm assume the polygon is simple and use an 1369 * optimised (Melkman's) convex hull? 1370 * 1371 * @return the minimum bounding box 1372 */ 1373 public RotatedRectangle minimumBoundingRectangle(boolean assumeSimple) { 1374 return RotatingCalipers.getMinimumBoundingRectangle(this, assumeSimple); 1375 } 1376 1377 /** 1378 * Find the closest point on the polygon to the given point 1379 * 1380 * @param pt 1381 * the point 1382 * @return the closest point 1383 */ 1384 public Point2d closestPoint(Point2d pt) { 1385 final boolean closed = isClosed(); 1386 1387 if (!closed) 1388 close(); 1389 1390 final float x = pt.getX(); 1391 final float y = pt.getY(); 1392 float minDist = Float.MAX_VALUE; 1393 final Point2dImpl min = new Point2dImpl(); 1394 final Point2dImpl tpt = new Point2dImpl(); 1395 1396 for (int k = 0; k < points.size() - 1; k++) { 1397 final float vx = points.get(k).getX(); 1398 final float vy = points.get(k).getY(); 1399 final float wx = points.get(k + 1).getX(); 1400 final float wy = points.get(k + 1).getY(); 1401 1402 // Return minimum distance between line segment vw and point p 1403 final float l2 = (wx - vx) * (wx - vx) + (wy - vy) * (wy - vy); 1404 1405 if (l2 == 0.0) { 1406 tpt.x = vx; 1407 tpt.y = vy; 1408 } else { 1409 final float t = ((x - vx) * (wx - vx) + (y - vy) * (wy - vy)) / l2; 1410 1411 if (t < 0.0) { 1412 tpt.x = vx; 1413 tpt.y = vy; 1414 } else if (t > 1.0) { 1415 tpt.x = wx; 1416 tpt.y = wy; 1417 } else { 1418 tpt.x = vx + t * (wx - vx); 1419 tpt.y = vy + t * (wy - vy); 1420 } 1421 } 1422 1423 final float dist = (float) Line2d.distance(x, y, tpt.x, tpt.y); 1424 if (dist < minDist) { 1425 minDist = dist; 1426 min.x = tpt.x; 1427 min.y = tpt.y; 1428 } 1429 } 1430 1431 if (!closed) 1432 open(); 1433 1434 return min; 1435 } 1436}