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.image.pixel; 031 032import gnu.trove.list.array.TFloatArrayList; 033import gnu.trove.list.array.TIntArrayList; 034 035import java.util.ArrayList; 036import java.util.Collection; 037import java.util.HashSet; 038import java.util.LinkedHashSet; 039import java.util.List; 040import java.util.Set; 041 042import org.openimaj.image.FImage; 043import org.openimaj.image.processor.connectedcomponent.ConnectedComponentProcessor; 044import org.openimaj.image.renderer.ScanRasteriser; 045import org.openimaj.image.renderer.ScanRasteriser.ScanLineListener; 046import org.openimaj.math.geometry.point.Point2d; 047import org.openimaj.math.geometry.shape.Polygon; 048import org.openimaj.math.geometry.shape.RotatedRectangle; 049import org.openimaj.math.geometry.shape.Shape; 050 051/** 052 * This class represents a connected region within an image and provides methods 053 * for accessing and manipulating that region. 054 * <p> 055 * Nothing stops an unconnected component being constructed, but it is important 056 * to realise that some methods may return unexpected results (e.g. boundary 057 * tracing). If you are only dealing with unconnected pixel sets, use the 058 * {@link PixelSet} superclass instead. 059 * 060 * @author Jonathon Hare (jsh2@ecs.soton.ac.uk) 061 * @author David Dupplaw (dpd@ecs.soton.ac.uk) 062 */ 063public class ConnectedComponent extends PixelSet { 064 /** 065 * For boundary representations of {@link ConnectedComponent}s, this enum 066 * determines and specifies how the boundary is calculated; either using a 067 * 4-connected rule, or an 8-connected rule. 068 * 069 * @author Jonathon Hare (jsh2@ecs.soton.ac.uk) 070 */ 071 public enum ConnectMode { 072 /** 4-connected edges in the boundary representation */ 073 CONNECT_4, 074 /** 8-connected edges in the boundary representation */ 075 CONNECT_8; 076 077 /** 078 * Get the neighbouring pixels 079 * 080 * @param image 081 * the image 082 * @param x 083 * the x ordinate 084 * @param y 085 * the y ordinate 086 * @param bgThreshold 087 * the threshold for below which pixels should be ignored 088 * @return the neighbouring pixels 089 */ 090 public List<Pixel> getNeighbours(FImage image, int x, int y, float bgThreshold) { 091 final List<Pixel> neighbours = new ArrayList<Pixel>(this == CONNECT_8 ? 8 : 4); 092 093 switch (this) { 094 case CONNECT_8: 095 if (x > 0 && y > 0 && image.pixels[y - 1][x - 1] > bgThreshold) 096 neighbours.add(new Pixel(x - 1, y - 1)); 097 if (x + 1 < image.width && y > 0 && image.pixels[y - 1][x + 1] > bgThreshold) 098 neighbours.add(new Pixel(x + 1, y - 1)); 099 if (x > 0 && y + 1 < image.height && image.pixels[y + 1][x - 1] > bgThreshold) 100 neighbours.add(new Pixel(x - 1, y + 1)); 101 if (x + 1 < image.width && y + 1 < image.height && image.pixels[y + 1][x + 1] > bgThreshold) 102 neighbours.add(new Pixel(x + 1, y + 1)); 103 // Note : no break, so we fall through... 104 case CONNECT_4: 105 if (x > 0 && image.pixels[y][x - 1] > bgThreshold) 106 neighbours.add(new Pixel(x - 1, y)); 107 if (x + 1 < image.width && image.pixels[y][x + 1] > bgThreshold) 108 neighbours.add(new Pixel(x + 1, y)); 109 if (y > 0 && image.pixels[y - 1][x] > bgThreshold) 110 neighbours.add(new Pixel(x, y - 1)); 111 if (y + 1 < image.height && image.pixels[y + 1][x] > bgThreshold) 112 neighbours.add(new Pixel(x, y + 1)); 113 break; 114 } 115 116 return neighbours; 117 } 118 } 119 120 /** 121 * Default constructor. Has an empty implementation. 122 */ 123 public ConnectedComponent() { 124 super(); 125 } 126 127 /** 128 * Construct a new connected component from the given shape. Pixels are 129 * created for the connected component that lie within the shape; but these 130 * pixels will not have specific values as they are not associated to any 131 * particular image. 132 * 133 * @param shape 134 * The shape from which to construct the connected component. 135 */ 136 public ConnectedComponent(Shape shape) { 137 fromShape(shape); 138 } 139 140 /** 141 * Construct a new connected component from the given polygon. Pixels are 142 * created for the connected component that lie within the shape; but these 143 * pixels will not have specific values as they are not associated to any 144 * particular image. 145 * 146 * @param poly 147 * The polygon from which to construct the connected component. 148 */ 149 public ConnectedComponent(Polygon poly) { 150 // FIXME: this can be improved if the scan-fill algorithm 151 // could be adapted to deal with holes. 152 if (poly.getNumInnerPoly() == 1) { 153 ScanRasteriser.scanFill(poly.points, new ScanLineListener() { 154 @Override 155 public void process(int x1, int x2, int y) { 156 for (int x = x1; x <= x2; x++) 157 ConnectedComponent.this.addPixel(x, y); 158 } 159 }); 160 } else { 161 fromShape(poly); 162 } 163 } 164 165 /** 166 * Construct a rectangular {@link ConnectedComponent}. Pixels are created 167 * for the area within the rectangle but these will not have any specific 168 * value as they are not associated to any particular image. 169 * 170 * @param x 171 * The top-left x-coordinate of the rectangle 172 * @param y 173 * The top-left y-coordinate of the rectangle 174 * @param w 175 * The width of the rectangle 176 * @param h 177 * The height of the rectangle 178 */ 179 public ConnectedComponent(int x, int y, int w, int h) { 180 super(x, y, w, h); 181 } 182 183 /** 184 * Constructs a connected component from a mask image. Pixels are created 185 * for areas within the mask image that are non-zero. Note that this may 186 * result in a connected component definition that is unconnected and some 187 * methods may not return expected results. 188 * 189 * @param img 190 * The mask image to construct a connected component from. 191 */ 192 public ConnectedComponent(int[][] img) { 193 super(img); 194 } 195 196 /** 197 * Constructs a connected component from a mask image. Pixels are created 198 * for areas within the mask image that are above the given threshold. Note 199 * that this may result in a connected component definition that is 200 * unconnected and some methods may not return expected results. 201 * 202 * @param mask 203 * The mask image to construct a connected component from. 204 * @param thresh 205 * the threshold value. 206 */ 207 public ConnectedComponent(FImage mask, float thresh) { 208 super(mask, thresh); 209 } 210 211 /** 212 * Construct a connected component from the given set of {@link Pixel}s. The 213 * pixels are shallow copied into the connected component. If the pixels do 214 * not form a connected component then some methods in this class may not 215 * return expected results. 216 * 217 * @param pixels 218 * A {@link Set} of {@link Pixel}s. 219 */ 220 public ConnectedComponent(Set<Pixel> pixels) { 221 super(pixels); 222 } 223 224 /** 225 * Estimates how many vertices are required to encode the boundary with the 226 * given smoothness and window width. Basically it determines how many 227 * strong peaks there are in the boundary, where the peak strength is 228 * determined by the two parameters. The window width determines how many 229 * boundary points are considered when iterating through the binary, while 230 * the smoothness determines how smooth the resulting boundary 231 * representation can be. 232 * 233 * @param smoothWidth 234 * The smoothness of the resulting boundary 235 * @param windowWidth 236 * The number of edge points to consider 237 * @return The estimated number of vertices required to encode the boundary. 238 */ 239 public int estimateNumberOfVertices(int smoothWidth, int windowWidth) { 240 final TFloatArrayList distances = calculateBoundaryDistanceFromCentre(); 241 242 if (smoothWidth % 2 == 0) 243 smoothWidth++; 244 if (windowWidth % 2 == 0) 245 windowWidth++; 246 247 final int n = distances.size(); 248 final float[] kernel = new float[windowWidth]; 249 final float[] response = new float[n]; 250 251 for (int i = 0; i < n; i++) { 252 float sum = 0; 253 for (int j = 0; j < smoothWidth; j++) { 254 int k = i + j - (smoothWidth / 2); 255 256 if (k < 0) { 257 k = n + k; 258 } else if (k >= n) { 259 k = k - n; 260 } 261 sum += distances.get(k); 262 } 263 distances.set(i, sum / smoothWidth); 264 } 265 266 for (int i = 0; i < windowWidth; i++) 267 kernel[i] = -(windowWidth / 2) + i; 268 269 for (int i = 0; i < n; i++) { 270 float sum = 0; 271 for (int j = 0; j < windowWidth; j++) { 272 int k = i + j - (windowWidth / 2); 273 274 if (k < 0) { 275 k = n + k; 276 } else if (k >= n) { 277 k = k - n; 278 } 279 sum += kernel[j] * distances.get(k); 280 } 281 response[i] = sum; 282 } 283 284 int peaks = 0; 285 for (int i = 1; i < n; i++) { 286 if (response[i - 1] >= 0 && response[i] < 0) 287 peaks++; 288 } 289 if (response[n - 1] >= 0 && response[0] < 0) 290 peaks++; 291 292 return peaks; 293 } 294 295 /** 296 * 297 * @param P0 298 * @param P1 299 * @param P2 300 * @return 301 */ 302 protected int isLeft(Pixel P0, Pixel P1, Pixel P2) { 303 return (P1.x - P0.x) * (P2.y - P0.y) - (P2.x - P0.x) * (P1.y - P0.y); 304 } 305 306 /** 307 * Calculates the convex hull polygon for this connected component using 308 * Andrew's montone algorithm. 309 * 310 * @return The polygon defining the convex hull shape for this component. 311 */ 312 public Polygon calculateConvexHull() { 313 return calculateConvexHull_AndrewsMontone(); 314 } 315 316 /** 317 * Calculate the ratio of the area of the given connected component to the 318 * area of the connected component. This does not consider whether the areas 319 * overlap. 320 * 321 * @param ch 322 * The connected component to test. 323 * @return The area ratio of the given connected component to this connected 324 * component. 325 */ 326 public double calculateAreaRatio(ConnectedComponent ch) { 327 return (double) calculateArea() / (double) ch.calculateArea(); 328 } 329 330 /** 331 * Calculate the ratio of the area of the given polygon to the area of this 332 * connected component. This does not consider whether the areas overlap. 333 * 334 * @param ch 335 * The polygon to test again. 336 * @return The area ratio of the given polygon to this connected component. 337 */ 338 public double calculateAreaRatio(Polygon ch) { 339 return calculateAreaRatio(new ConnectedComponent(ch)); 340 } 341 342 /** 343 * Calculate the ratio of the area of this component's convex hull to the 344 * actual area of this connected component. This gives an idea of how well 345 * the calculated convex hull fits the component. The value returned is a 346 * percentage (0-1). 347 * 348 * @return The area ratio of this component's convex hull its area. 349 */ 350 public double calculatePercentageConvexHullFit() { 351 return calculateAreaRatio(calculateConvexHull()); 352 } 353 354 /** 355 * Calculate convex hull using Melkman's algorithm. Based on 356 * http://softsurfer.com/Archive/algorithm_0203/algorithm_0203.htm 357 * <p> 358 * Copyright 2001, softSurfer (www.softsurfer.com) This code may be freely 359 * used and modified for any purpose providing that this copyright notice is 360 * included with it. SoftSurfer makes no warranty for this code, and cannot 361 * be held liable for any real or imagined damage resulting from its use. 362 * Users of this code must verify correctness for their application. 363 * 364 * @param V 365 * List of pixels containing within the region 366 * @return A polygon defining the shape of the convex hull 367 */ 368 protected Polygon calculateConvexHull_Melkman(List<Pixel> V) { 369 // initialize a deque D[] from bottom to top so that the 370 // 1st three vertices of V[] are a counterclockwise triangle 371 final int n = V.size(); 372 final Pixel[] D = new Pixel[2 * n + 1]; 373 int bot = n - 2, top = bot + 3; // initial bottom and top deque indices 374 D[bot] = D[top] = V.get(2); // 3rd vertex is at both bot and top 375 if (isLeft(V.get(0), V.get(1), V.get(2)) > 0) { 376 D[bot + 1] = V.get(0); 377 D[bot + 2] = V.get(1); // ccw vertices are: 2,0,1,2 378 } else { 379 D[bot + 1] = V.get(1); 380 D[bot + 2] = V.get(0); // ccw vertices are: 2,1,0,2 381 } 382 383 // compute the hull on the deque D[] 384 for (int i = 3; i < n; i++) { // process the rest of vertices 385 // test if next vertex is inside the deque hull 386 if ((isLeft(D[bot], D[bot + 1], V.get(i)) > 0) && 387 (isLeft(D[top - 1], D[top], V.get(i)) > 0)) 388 continue; // skip an interior vertex 389 390 // incrementally add an exterior vertex to the deque hull 391 // get the rightmost tangent at the deque bot 392 while (isLeft(D[bot], D[bot + 1], V.get(i)) <= 0) 393 ++bot; // remove bot of deque 394 D[--bot] = V.get(i); // insert V[i] at bot of deque 395 396 // get the leftmost tangent at the deque top 397 while (isLeft(D[top - 1], D[top], V.get(i)) <= 0) 398 --top; // pop top of deque 399 D[++top] = V.get(i); // push V[i] onto top of deque 400 } 401 402 // transcribe deque D[] to the output hull array H[] 403 final Polygon H = new Polygon(); 404 final List<Point2d> vertices = H.getVertices(); 405 for (int h = 0; h <= (top - bot); h++) 406 vertices.add(D[bot + h]); 407 408 return H; 409 } 410 411 /** 412 * Calculate the convex hull using Andrew's monotone chain 2D convex hull 413 * algorithm. 414 * 415 * @return A polygon defining the shape of the convex hull. 416 */ 417 protected Polygon calculateConvexHull_AndrewsMontone() { 418 if (this.calculateArea() == 1) { 419 return new Polygon(this.pixels.iterator().next()); 420 } 421 422 final List<Pixel> P = new ArrayList<Pixel>(); 423 424 // sort 425 int minx = Integer.MAX_VALUE, maxx = Integer.MIN_VALUE, miny = Integer.MAX_VALUE, maxy = Integer.MIN_VALUE; 426 427 for (final Pixel p : pixels) { 428 if (p.x < minx) 429 minx = p.x; 430 if (p.x > maxx) 431 maxx = p.x; 432 if (p.y < miny) 433 miny = p.y; 434 if (p.y > maxy) 435 maxy = p.y; 436 } 437 438 for (int x = minx; x <= maxx; x++) { 439 for (int y = miny; y <= maxy; y++) { 440 final Pixel p = new Pixel(x, y); 441 if (pixels.contains(p)) 442 P.add(p); 443 } 444 } 445 446 // the output array H[] will be used as the stack 447 int bot = 0, top = (-1); // indices for bottom and top of the stack 448 int i; // array scan index 449 final int n = P.size(); 450 451 final Polygon poly = new Polygon(); 452 final Pixel[] H = new Pixel[P.size()]; 453 454 // Get the indices of points with min x-coord and min|max y-coord 455 final int minmin = 0; 456 int minmax; 457 final float xmin = P.get(0).x; 458 for (i = 1; i < n; i++) 459 if (P.get(i).x != xmin) 460 break; 461 minmax = i - 1; 462 if (minmax == n - 1) { // degenerate case: all x-coords == xmin 463 H[++top] = P.get(minmin); 464 if (P.get(minmax).y != P.get(minmin).y) // a nontrivial segment 465 H[++top] = P.get(minmax); 466 H[++top] = P.get(minmin); // add polygon endpoint 467 468 for (int k = 0; k < top + 1; k++) 469 poly.getVertices().add(H[k]); 470 return poly; 471 } 472 473 // Get the indices of points with max x-coord and min|max y-coord 474 int maxmin; 475 final int maxmax = n - 1; 476 final float xmax = P.get(n - 1).x; 477 for (i = n - 2; i >= 0; i--) 478 if (P.get(i).x != xmax) 479 break; 480 maxmin = i + 1; 481 482 // Compute the lower hull on the stack H 483 H[++top] = P.get(minmin); // push minmin point onto stack 484 i = minmax; 485 while (++i <= maxmin) { 486 // the lower line joins P[minmin] with P[maxmin] 487 if (isLeft(P.get(minmin), P.get(maxmin), P.get(i)) >= 0 && i < maxmin) 488 continue; // ignore P[i] above or on the lower line 489 490 while (top > 0) // there are at least 2 points on the stack 491 { 492 // test if P[i] is left of the line at the stack top 493 if (isLeft(H[top - 1], H[top], P.get(i)) > 0) 494 break; // P[i] is a new hull vertex 495 else 496 top--; // pop top point off stack 497 } 498 H[++top] = P.get(i); // push P[i] onto stack 499 } 500 501 // Next, compute the upper hull on the stack H above the bottom hull 502 if (maxmax != maxmin) // if distinct xmax points 503 H[++top] = P.get(maxmax); // push maxmax point onto stack 504 bot = top; // the bottom point of the upper hull stack 505 i = maxmin; 506 while (--i >= minmax) { 507 // the upper line joins P[maxmax] with P[minmax] 508 if (isLeft(P.get(maxmax), P.get(minmax), P.get(i)) >= 0 && i > minmax) 509 continue; // ignore P[i] below or on the upper line 510 511 while (top > bot) // at least 2 points on the upper stack 512 { 513 // test if P[i] is left of the line at the stack top 514 if (isLeft(H[top - 1], H[top], P.get(i)) > 0) 515 break; // P[i] is a new hull vertex 516 else 517 top--; // pop top point off stack 518 } 519 H[++top] = P.get(i); // push P[i] onto stack 520 } 521 if (minmax != minmin) 522 H[++top] = P.get(minmin); // push joining endpoint onto stack 523 524 for (int k = 0; k < top + 1; k++) 525 poly.getVertices().add(H[k]); 526 return poly; 527 } 528 529 /** 530 * Returns the next edge pixel when tracing a boundary in a 4-connected 531 * system. 532 * 533 * @param current 534 * The current pixel 535 * @param lastdir 536 * The last direction traversed 537 * @return The next pixel in the edge 538 */ 539 protected Pixel nextEdgePixelACW4(Pixel current, int lastdir) { 540 return nextEdgePixelACW4(current, lastdir, null); 541 } 542 543 /** 544 * Returns the next edge pixel when tracing a boundary in a 4-connected 545 * system. 546 * 547 * @param current 548 * The current pixel 549 * @param lastdir 550 * The last direction traversed 551 * @param outer 552 * A list to fill with the outer boundary 553 * @return The next pixel in the edge 554 */ 555 protected Pixel nextEdgePixelACW4(Pixel current, int lastdir, List<Pixel> outer) { 556 final int startdir = (lastdir + 3) % 4; 557 558 final Pixel target[] = { 559 new Pixel(current.x + 1, current.y), // dir 0 560 new Pixel(current.x, current.y - 1), // dir 1 561 new Pixel(current.x - 1, current.y), // dir 2 562 new Pixel(current.x, current.y + 1) // dir 3 563 }; 564 565 for (int i = 0; i < 4; i++) { 566 int dir = startdir + i; 567 if (dir >= 4) 568 dir -= 4; 569 570 if (pixels.contains(target[dir])) { 571 return target[dir]; 572 } else if (outer != null) 573 outer.add(target[dir]); 574 } 575 576 return current; 577 } 578 579 /** 580 * Returns the next edge pixel when tracing a boundary in an 8-connected 581 * system. 582 * 583 * @param current 584 * The current pixel 585 * @param lastdir 586 * The last direction traversed 587 * @return The next pixel in the edge 588 */ 589 protected Pixel nextEdgePixelACW8(Pixel current, int lastdir) { 590 final int startdir = (lastdir + 7 - (lastdir % 2)) % 8; 591 592 final Pixel target[] = { 593 new Pixel(current.x + 1, current.y), // dir 0 594 new Pixel(current.x + 1, current.y - 1), // dir 1 595 new Pixel(current.x, current.y - 1), // dir 2 596 new Pixel(current.x - 1, current.y - 1), // dir 3 597 new Pixel(current.x - 1, current.y), // dir 4 598 new Pixel(current.x - 1, current.y + 1), // dir 5 599 new Pixel(current.x, current.y + 1), // dir 6 600 new Pixel(current.x + 1, current.y + 1) // dir 7 601 }; 602 603 for (int i = 0; i < 8; i++) { 604 int dir = startdir + i; 605 if (dir >= 8) 606 dir -= 8; 607 608 if (pixels.contains(target[dir])) 609 return target[dir]; 610 } 611 612 return current; 613 } 614 615 /** 616 * For the two pixels, determines the 4-connected chain code that will move 617 * from the first pixel to the next. If the pixels are not adjacent the 618 * method returns -1. 619 * 620 * @param current 621 * The current pixel 622 * @param next 623 * The next pixel 624 * @return The Freeman 4-connected chain code 625 */ 626 protected int code4(Pixel current, Pixel next) { 627 if (current.x - 1 == next.x) 628 return 2; 629 if (current.y + 1 == next.y) 630 return 3; 631 if (current.x + 1 == next.x) 632 return 0; 633 if (current.y - 1 == next.y) 634 return 1; 635 636 return -1; 637 } 638 639 /** 640 * For the two pixels, determines the 8-connected chain code that will move 641 * from the first pixel to the next. If the pixels are not adjacent or 642 * diagonal then the method returns -1. 643 * 644 * @param current 645 * The current pixel 646 * @param next 647 * The next pixel 648 * @return The Freeman 8-connected chain code 649 */ 650 protected int code8(Pixel current, Pixel next) { 651 if (current.x + 1 == next.x && current.y == next.y) 652 return 0; 653 if (current.x + 1 == next.x && current.y - 1 == next.y) 654 return 1; 655 if (current.x == next.x && current.y - 1 == next.y) 656 return 2; 657 if (current.x - 1 == next.x && current.y - 1 == next.y) 658 return 3; 659 if (current.x - 1 == next.x && current.y == next.y) 660 return 4; 661 if (current.x - 1 == next.x && current.y + 1 == next.y) 662 return 5; 663 if (current.x == next.x && current.y + 1 == next.y) 664 return 6; 665 if (current.x + 1 == next.x && current.y + 1 == next.y) 666 return 7; 667 668 return -1; 669 } 670 671 /** 672 * Converts this connected component into a {@link Polygon} representation 673 * by performing a 4-connected boundary trace and converting the resulting 674 * pixels into vertices. 675 * 676 * @return A {@link Polygon} representing the inner boundary of the 677 * component. 678 */ 679 public Polygon toPolygon() { 680 final Polygon poly = new Polygon(); 681 682 for (final Pixel p : getInnerBoundary(ConnectMode.CONNECT_4)) 683 poly.getVertices().add(p); 684 685 return poly; 686 } 687 688 /** 689 * Returns an ordered list of pixels that are on the inner boundary of the 690 * shape. That means that the boundary points are all within the region. The 691 * list is ordered such that adjacent boundary pixels are adjacent in the 692 * list. The first pixel in the list should be the same as 693 * {@link #topLeftMostPixel()}. 694 * 695 * @param mode 696 * The {@link ConnectMode} to use. 697 * @return An ordered list of pixels defining the inner boundary 698 */ 699 public List<Pixel> getInnerBoundary(ConnectMode mode) { 700 final List<Pixel> pset = new ArrayList<Pixel>(); 701 702 final Pixel start = topLeftMostPixel(); 703 Pixel current = start; 704 Pixel next; 705 int dir; 706 707 switch (mode) { 708 case CONNECT_4: 709 dir = 3; 710 while (true) { 711 next = nextEdgePixelACW4(current, dir); 712 if (pset.size() >= 2 && next.equals(pset.get(1)) && current.equals(start)) { 713 break; 714 } 715 716 dir = code4(current, next); 717 pset.add(current); 718 current = next; 719 } 720 break; 721 case CONNECT_8: 722 dir = 7; 723 while (true) { 724 next = nextEdgePixelACW8(current, dir); 725 726 if (pset.size() >= 2 && next.equals(pset.get(1)) && current.equals(start)) { 727 break; 728 } 729 730 dir = code8(current, next); 731 pset.add(current); 732 current = next; 733 } 734 break; 735 } 736 737 return pset; 738 } 739 740 /** 741 * Returns an ordered list of pixels that are on the outer boundary of the 742 * shape. That means that the boundary points are all outside of the region. 743 * The list is ordered such that adjacent boundary pixels are adjacent in 744 * the list. 745 * 746 * @return An ordered list of pixels defining the outer boundary 747 */ 748 public List<Pixel> getOuterBoundary() { 749 final List<Pixel> pset = new ArrayList<Pixel>(); 750 List<Pixel> outer = new ArrayList<Pixel>(); 751 752 final Pixel start = topLeftMostPixel(); 753 Pixel current = start; 754 Pixel next; 755 int dir = 3; 756 757 while (true) { 758 next = nextEdgePixelACW4(current, dir, outer); 759 if (pset.size() >= 2 && next.equals(pset.get(1)) && current.equals(start)) { 760 break; 761 } 762 if (this.pixels.size() == 1) 763 break; 764 765 dir = code4(current, next); 766 pset.add(current); 767 current = next; 768 } 769 770 if (outer.size() > 4) { 771 for (int i = 3; i > 0; i--) { 772 boolean found = true; 773 for (int j = 0; j < i; j++) { 774 if (!outer.get(j).equals(outer.get(outer.size() - i + j))) { 775 found = false; 776 break; 777 } 778 } 779 780 if (found) { 781 outer = outer.subList(0, outer.size() - i); 782 break; 783 } 784 } 785 } 786 787 return outer; 788 } 789 790 /** 791 * Calculates the Freeman chaincode for this connected component. The 792 * chaincode is returned as a list of direction codes defining the path of 793 * the boundary. 794 * <p> 795 * The Freeman chaincode is a means for encoding the paths between nodes on 796 * the boundary of a shape, thereby reducing the encoding of a shape to a 797 * single start coordinate and a list of direction codes. The Freeman 798 * direction codes are 0-4 for 4-connected boundaries and 0-7 for 799 * 8-connected boundaries. 800 * 801 * @param mode 802 * 4 or 8 connectivity 803 * @return the chain code 804 */ 805 public TIntArrayList freemanChainCode(ConnectMode mode) { 806 final TIntArrayList code = new TIntArrayList(); 807 808 final Pixel start = topLeftMostPixel(); 809 Pixel current = start; 810 Pixel next; 811 int dir; 812 813 switch (mode) { 814 case CONNECT_8: 815 dir = 7; 816 while (!(next = nextEdgePixelACW8(current, dir)).equals(start)) { 817 dir = code8(current, next); 818 code.add(dir); 819 current = next; 820 } 821 code.add(code8(current, next)); 822 break; 823 case CONNECT_4: 824 dir = 3; 825 while (!(next = nextEdgePixelACW4(current, dir)).equals(start)) { 826 dir = code4(current, next); 827 code.add(dir); 828 current = next; 829 } 830 code.add(code4(current, next)); 831 break; 832 } 833 834 return code; 835 } 836 837 /** 838 * Process the given set of connected components with the given 839 * {@link ConnectedComponentProcessor}. 840 * 841 * @param components 842 * The components to process 843 * @param p 844 * The process to process the components with 845 */ 846 public static void process(Collection<ConnectedComponent> components, ConnectedComponentProcessor p) { 847 for (final ConnectedComponent c : components) 848 c.processInplace(p); 849 } 850 851 /** 852 * Process this connected component with the given 853 * {@link ConnectedComponentProcessor} and returns a new component 854 * containing the result. 855 * 856 * @param p 857 * The processor to process this component with 858 * @return A new component containing the result. 859 */ 860 public ConnectedComponent process(ConnectedComponentProcessor p) { 861 final ConnectedComponent tmp = clone(); 862 p.process(tmp); 863 return tmp; 864 } 865 866 /** 867 * Process a connected component with the given 868 * {@link ConnectedComponentProcessor}. Side-affects this component. 869 * 870 * @param p 871 * The processor to process this component with 872 * @return A reference to this connected component. 873 */ 874 public ConnectedComponent processInplace(ConnectedComponentProcessor p) { 875 p.process(this); 876 return this; 877 } 878 879 /** 880 * Performs a flood fill on the given image starting at the given pixel. The 881 * result of the flood fill is returned as a {@link ConnectedComponent}. 882 * 883 * @param image 884 * The image on which to perform a flood fill 885 * @param start 886 * The start pixel to begin the flood 887 * @return A ConnectedComponent containing the resulting region. 888 */ 889 public static ConnectedComponent floodFill(FImage image, Pixel start) { 890 final ConnectedComponent cc = new ConnectedComponent(); 891 final float val = image.pixels[start.y][start.x]; 892 final int[][] output = new int[image.height][image.width]; 893 894 // Flood-fill (node, target-color, replacement-color): 895 // 1. Set Q to the empty queue. 896 // Queue<Pixel> queue = new LinkedList<Pixel>(); 897 final LinkedHashSet<Pixel> queue = new LinkedHashSet<Pixel>(); 898 899 // 2. If the color of node is not equal to target-color, return. 900 if (image.pixels[start.y][start.x] > val) 901 return cc; 902 903 // 3. Add node to Q. 904 queue.add(start); 905 906 // 4. For each element n of Q: 907 while (queue.size() > 0) { 908 // Pixel n = queue.poll(); 909 final Pixel n = queue.iterator().next(); 910 queue.remove(n); 911 912 // 5. If the color of n is equal to target-color: 913 if (image.pixels[n.y][n.x] <= val && output[n.y][n.x] != 1) { 914 // 6. Set w and e equal to n. 915 int e = n.x, w = n.x; 916 // 7. Move w to the west until the color of the node to the west 917 // of w no longer matches target-color. 918 while (w > 0 && image.pixels[n.y][w - 1] <= val) 919 w--; 920 921 // 8. Move e to the east until the color of the node to the east 922 // of e no longer matches target-color. 923 while (e < image.width - 1 && image.pixels[n.y][e + 1] <= val) 924 e++; 925 926 // 9. Set the color of nodes between w and e to 927 // replacement-color. 928 for (int i = w; i <= e; i++) { 929 output[n.y][i] = 1; 930 cc.addPixel(i, n.y); 931 932 // 10. For each node n between w and e: 933 final int north = n.y - 1; 934 final int south = n.y + 1; 935 // 11. If the color of the node to the north of n is 936 // target-color, add that node to Q. 937 if (north >= 0 && image.pixels[north][i] <= val && output[north][i] != 1) 938 queue.add(new Pixel(i, north)); 939 // If the color of the node to the south of n is 940 // target-color, add that node to Q. 941 if (south < image.height && image.pixels[south][i] <= val && output[south][i] != 1) 942 queue.add(new Pixel(i, south)); 943 } 944 // 12. Continue looping until Q is exhausted. 945 } 946 } 947 // 13. Return. 948 return cc; 949 } 950 951 /** 952 * {@inheritDoc} 953 * 954 * Performs a deep copy on the connected component; that is, all pixels are 955 * also cloned. 956 */ 957 @Override 958 public ConnectedComponent clone() { 959 ConnectedComponent tmp; 960 try { 961 tmp = (ConnectedComponent) super.clone(); 962 tmp.pixels = new HashSet<Pixel>(); 963 964 for (final Pixel p : pixels) 965 tmp.pixels.add(p.clone()); 966 967 return tmp; 968 } catch (final CloneNotSupportedException e) { 969 throw new RuntimeException(e); 970 } 971 } 972 973 /** 974 * Calculates the distance from the centroid of every pixel on the 975 * 8-connected boundary of this component. Returns a {@link TFloatArrayList} 976 * that contains the list of distances (in order of the boundary). 977 * 978 * @return A list ({@link TFloatArrayList}) of distances of boundary points 979 * to the centroid. 980 */ 981 public TFloatArrayList calculateBoundaryDistanceFromCentre() { 982 final TFloatArrayList distances = new TFloatArrayList(); 983 final List<Pixel> bound = getInnerBoundary(ConnectMode.CONNECT_8); 984 final double[] centroid = calculateCentroid(); 985 986 for (final Pixel p : bound) { 987 final float dist = (float) Math.sqrt(((centroid[0] - p.x) * ((centroid[0] - p.x))) + 988 ((centroid[1] - p.y) * ((centroid[1] - p.y)))); 989 distances.add(dist); 990 } 991 992 return distances; 993 } 994 995 @Override 996 public String asciiHeader() { 997 return "ConnectedComponent"; 998 } 999 1000 @Override 1001 public byte[] binaryHeader() { 1002 return "CC".getBytes(); 1003 } 1004 1005 /** 1006 * Compute the aspect ratio of the oriented bounding box. 1007 * 1008 * @return the aspect ratio of the oriented bounding box. 1009 */ 1010 public double calculateOrientatedBoundingBoxAspectRatio() { 1011 final RotatedRectangle r = toPolygon().minimumBoundingRectangle(); 1012 1013 return r.height / r.width; 1014 } 1015 1016 /** 1017 * Calculates the polygon that defines the minimum bounding box that best 1018 * fits the connected component, at whatever angle that may be. 1019 * 1020 * @return A {@link RotatedRectangle} that defines the minimum bounding box. 1021 */ 1022 public RotatedRectangle calculateOrientatedBoundingBox() { 1023 return this.toPolygon().minimumBoundingRectangle(); 1024 } 1025}