001/******************************************************************************* 002 * This software is provided as a supplement to the authors' textbooks on digital 003 * image processing published by Springer-Verlag in various languages and editions. 004 * Permission to use and distribute this software is granted under the BSD 2-Clause 005 * "Simplified" License (see http://opensource.org/licenses/BSD-2-Clause). 006 * Copyright (c) 2006-2016 Wilhelm Burger, Mark J. Burge. All rights reserved. 007 * Visit http://imagingbook.com for additional details. 008 *******************************************************************************/ 009 010package imagingbook.pub.regions; 011 012import java.awt.Color; 013import java.awt.Point; 014import java.awt.Rectangle; 015import java.awt.geom.Point2D; 016import java.util.ArrayList; 017import java.util.Collections; 018import java.util.Formatter; 019import java.util.HashMap; 020import java.util.Iterator; 021import java.util.LinkedList; 022import java.util.List; 023import java.util.Locale; 024import java.util.Map; 025import java.util.NoSuchElementException; 026 027import ij.process.ByteProcessor; 028import ij.process.ColorProcessor; 029import ij.process.ImageProcessor; 030import ij.process.ShortProcessor; 031 032/** 033 * This class does the complete region labeling for a given image. 034 * It is abstract, because the implementation of some parts depends 035 * on the region labeling algorithm being used. 036 * 037 * @version 2016-11-08 038 */ 039public abstract class RegionLabeling { 040 041 static final int BACKGROUND = 0; 042 static final int FOREGROUND = 1; 043 static final int START_LABEL = 2; 044 045 protected final ImageProcessor ip; 046 protected final int width; 047 protected final int height; 048 049 protected final int[][] labelArray; 050 // label values in labelArray can be: 051 // 0 ... unlabeled 052 // -1 ... previously visited background pixel 053 // >0 ... valid label 054 055 protected int currentLabel; 056 protected int maxLabel; // the maximum label in the labels array 057 protected List<BinaryRegion> regions; 058 059 protected RegionLabeling(ByteProcessor ip) { 060 this.ip = ip; 061 width = ip.getWidth(); 062 height = ip.getHeight(); 063 labelArray = initialize(); 064 applyLabeling(); 065 collectRegions(); 066 } 067 068 protected int[][] initialize() { 069 int[][] lA = new int[width][height]; // label array 070 // set all pixels to either FOREGROUND or BACKGROUND (by thresholding) 071 for (int v = 0; v < height; v++) { 072 for (int u = 0; u < width; u++) { 073 lA[u][v] = (ip.getPixel(u, v) > 0) ? FOREGROUND : BACKGROUND; 074 } 075 } 076 return lA; 077 } 078 079 /** 080 * Get an unsorted list of all regions associated with this region labeling. 081 * @return the list of regions. 082 */ 083 public List<BinaryRegion> getRegions() { 084 return getRegions(false); // unsorted 085 } 086 087 /** 088 * Get a possibly sorted list of all regions associated with this region labeling. 089 * @param sort set {@code true} to sort regions by decreasing size. 090 * @return the list of regions. 091 */ 092 public List<BinaryRegion> getRegions(boolean sort) { 093 if (regions == null) 094 return Collections.emptyList(); 095 else { 096 List<BinaryRegion> rns = new ArrayList<BinaryRegion>(regions); 097 if (sort) { 098 Collections.sort(rns); 099 } 100 return rns; 101 } 102 } 103 104 // This method must be implemented by any real sub-class: 105 abstract void applyLabeling(); 106 107 // creates a container of BinaryRegion objects 108 // collects the region pixels from the label image 109 // and computes the statistics for each region 110 void collectRegions() { 111 BinaryRegion[] regionArray = new BinaryRegion[maxLabel + 1]; 112 for (int i = START_LABEL; i <= maxLabel; i++) { 113 regionArray[i] = new BinaryRegion(i); 114 } 115 for (int v = 0; v < height; v++) { 116 for (int u = 0; u < width; u++) { 117 int label = getLabel(u, v); 118 if (label >= START_LABEL && label <= maxLabel && regionArray[label] != null) { 119 regionArray[label].addPixel(u, v); 120 } 121 } 122 } 123 // create a list of regions to return, collect nonempty regions 124 List<BinaryRegion> regionList = new LinkedList<BinaryRegion>(); 125 for (BinaryRegion r: regionArray) { 126 if (r != null && r.getSize() > 0) { 127 r.update(); // compute the statistics for this region 128 regionList.add(r); 129 } 130 } 131 regions = regionList; 132 } 133 134 /** 135 * Get the label number for the specified image coordinate. 136 * @param u the horizontal coordinate. 137 * @param v the vertical coordinate. 138 * @return the label number for the given position. 139 */ 140 public int getLabel(int u, int v) { 141 if (u >= 0 && u < width && v >= 0 && v < height) 142 return labelArray[u][v]; 143 else 144 return BACKGROUND; 145 } 146 147 protected void setLabel(int u, int v, int label) { 148 if (u >= 0 && u < width && v >= 0 && v < height) 149 labelArray[u][v] = label; 150 } 151 152 protected void resetLabel() { 153 currentLabel = -1; 154 maxLabel = -1; 155 } 156 157 int getNextLabel() { 158 if (currentLabel < 1) 159 currentLabel = START_LABEL; 160 else 161 currentLabel = currentLabel + 1; 162 maxLabel = currentLabel; 163 return currentLabel; 164 } 165 166 int getMaxLabel() { 167 return maxLabel; 168 } 169 170 // -------------------------------------------------- 171 172 /** 173 * Utility method that creates an image of the internal 174 * label array. 175 * 176 * @param color set {@code false} to get a 16-bit grayscale image, 177 * {@code true} for an RGB (24-bit) color image. 178 * @return an image of the label array. 179 */ 180 public ImageProcessor makeLabelImage(boolean color) { 181 return (color) ? makeLabelImageColor() : makeLabelImageGray(); 182 } 183 184 private ColorProcessor makeLabelImageColor() { 185 int[] colorLUT = new int[maxLabel+1]; 186 for (int i = START_LABEL; i <= maxLabel; i++) { 187 colorLUT[i] = makeRandomColor(); 188 } 189 ColorProcessor cp = new ColorProcessor(width, height); 190 for (int v = 0; v < height; v++) { 191 for (int u = 0; u < width; u++) { 192 int lb = getLabel(u, v); 193 if (lb >= 0 && lb < colorLUT.length) { 194 cp.putPixel(u, v, colorLUT[lb]); 195 } 196 } 197 } 198 return cp; 199 } 200 201 private ShortProcessor makeLabelImageGray() { 202 ShortProcessor sp = new ShortProcessor(width, height); 203 for (int v = 0; v < height; v++) { 204 for (int u = 0; u < width; u++) { 205 int lb = getLabel(u, v); 206 sp.set(u, v, (lb >= 0) ? lb : 0); 207 } 208 } 209 sp.resetMinAndMax(); 210 return sp; 211 } 212 213 214 /** 215 * Find the region associated to the given label. 216 * @param label the label number. 217 * @return the associated region object or {@code null} if 218 * is does not exist. 219 * 220 * @param label the label number. 221 * @return the region object associated with the given label 222 * or {@code null} if it does not exist. 223 */ 224 public BinaryRegion findRegion(int label) { 225 if (label <= 0 || regions == null) return null; 226 BinaryRegion regn = null; 227 for (BinaryRegion r : regions) { 228 if (r.getLabel() == label) { 229 regn = r; 230 break; 231 } 232 } 233 return regn; 234 } 235 236 /** 237 * Finds the {@link BinaryRegion} instance associated with 238 * the given image position. 239 * @param u the horizontal position. 240 * @param v the vertical position. 241 * @return The associated {@link BinaryRegion} object or null if 242 * this {@link RegionLabeling} has no region at the given position. 243 */ 244 public BinaryRegion getRegion(int u, int v) { 245 int label = getLabel(u, v); 246 return findRegion(label); 247 } 248 249 protected int makeRandomColor() { 250 double saturation = 0.2; 251 double brightness = 0.2; 252 float h = (float) Math.random(); 253 float s = (float) (saturation * Math.random() + 1 - saturation); 254 float b = (float) (brightness * Math.random() + 1 - brightness); 255 return Color.HSBtoRGB(h, s, b); 256 } 257 258// private void printSummary() { 259// if (regions != null) { 260// IJ.log("Number of regions detected: " + regions.size()); 261// for (BinaryRegion r : regions) { 262// IJ.log(r.toString()); 263// } 264// } else 265// IJ.log("No regions found."); 266// } 267 268 269 // --------- Iteration over region pixels ----------------------------------- 270 271 /** 272 * @deprecated 273 * This method is not supported any longer. 274 * Instead use {@code Iterable<Point>} capability of {@link BinaryRegion} 275 * to iterate directly over region points. 276 * 277 * @param r the binary region. 278 * @return an {@link Iterable} to iterate over all region points. 279 */ 280 public Iterable<Point> getRegionPoints(final BinaryRegion r) { 281 return r.getRegionPoints(); 282 } 283 284 private class RegionPixelIterator implements Iterator<Point> { 285 final int label; // the corresponding region's label 286 final int uMin, uMax, vMin, vMax; // coordinates of region's bounding box 287 int uCur, vCur; // current pixel position 288 Point pNext; // coordinates of the next region pixel 289 boolean first; // control flag 290 291 RegionPixelIterator(BinaryRegion R) { 292 label = R.getLabel(); 293 first = true; 294 Rectangle bb = R.getBoundingBox(); 295 uMin = bb.x; 296 uMax = bb.x + bb.width; 297 vMin = bb.y; 298 vMax = bb.y + bb.height; 299 uCur = uMin; 300 vCur = vMin; 301 pNext = null; 302 } 303 304 /** 305 * Search from position (uCur, vCur) for the next valid region pixel. 306 * Return the next position as a Point or null if no such point can be found. 307 * Don't assume that (uCur, vCur) is a valid region pixel! 308 * 309 * @return the next point 310 */ 311 private Point findNext() { 312 // start search for next region pixel at (u,v): 313 int u = (first) ? uCur : uCur + 1; 314 int v = vCur; 315 first = false; 316 while (v <= vMax) { 317 while (u <= uMax) { 318 if (getLabel(u, v) == label) { // next pixel found (uses surrounding labeling) 319 uCur = u; 320 vCur = v; 321 return new Point(uCur, vCur); 322 } 323 u++; 324 } 325 v++; 326 u = uMin; 327 } 328 uCur = uMax + 1; // just to make sure we'll never enter the loop again 329 vCur = vMax + 1; 330 return null; // no next pixel found 331 } 332 333 @Override 334 public boolean hasNext() { 335 if (pNext != null) { // next element has been queried before but not consumed 336 return true; 337 } 338 else { 339 pNext = findNext(); // keep next pixel coordinates in pNext 340 return (pNext != null); 341 } 342 } 343 344 // Returns: the next element in the iteration 345 // Throws: NoSuchElementException - if the iteration has no more elements. 346 @Override 347 public Point next() { 348 if (pNext != null || hasNext()) { 349 Point pn = pNext; 350 pNext = null; // "consume" pNext 351 return pn; 352 } 353 else { 354 throw new NoSuchElementException(); 355 } 356 } 357 358 @Override 359 public void remove() { 360 throw new UnsupportedOperationException(); 361 } 362 363 } // end of RegionPixelIterator 364 365 366 /** 367 * This inner class of {@link RegionLabeling} represents a connected 368 * component or binary region. 369 * It supports iteration over the contained points, e.g., by 370 * <pre> 371 * BinaryRegion R = ...; 372 * for (Point p : R) { 373 * // process p ... 374 * } 375 * </pre> 376 */ 377 public class BinaryRegion implements Comparable<BinaryRegion>, Iterable<Point> { 378 private final int label; // the label of THIS region 379 private int size = 0; 380 private double xc = Double.NaN; 381 private double yc = Double.NaN; 382 private int left = Integer.MAX_VALUE; 383 private int right = -1; 384 private int top = Integer.MAX_VALUE; 385 private int bottom = -1; 386 387 private Contour outerContour; 388 private List<Contour> innerContours; 389 390 // summation variables used for various statistics 391 private long x1Sum = 0; 392 private long y1Sum = 0; 393 private long x2Sum = 0; 394 private long y2Sum = 0; 395 396 // ------- constructor -------------------------- 397 398 private BinaryRegion(int label) { 399 this.label = label; 400 this.outerContour = null; 401 this.innerContours = null; 402 } 403 404 /** 405 * Get the x-value of the region's centroid. 406 * @return the x-value of the region's centroid. 407 */ 408 public double getXc() { 409 return xc; 410 } 411 412 /** 413 * Get the y-value of the region's centroid. 414 * @return the y-value of the region's centroid. 415 */ 416 public double getYc() { 417 return yc; 418 } 419 420 /** 421 * Returns the sum of the x-values of the points 422 * contained in this region. 423 * 424 * @return the sum of x-values. 425 */ 426 public long getX1Sum() { 427 return x1Sum; 428 } 429 430 /** 431 * Returns the sum of the y-values of the points 432 * contained in this region. 433 * 434 * @return the sum of y-values. 435 */ 436 public long getY1Sum() { 437 return y1Sum; 438 } 439 440 /** 441 * Returns the sum of the squared x-values of the points 442 * contained in this region. 443 * 444 * @return the sum of squared x-values. 445 */ 446 public long getX2Sum() { 447 return x2Sum; 448 } 449 450 /** 451 * Returns the sum of the squared y-values of the points 452 * contained in this region. 453 * 454 * @return the sum of squared y-values. 455 */ 456 public long getY2Sum() { 457 return y2Sum; 458 } 459 460 // ------- public methods -------------------------- 461 462 /** 463 * Get the label associated with this region. 464 * @return the region label. 465 */ 466 public int getLabel() { 467 return this.label; 468 } 469 470 /** 471 * Get the size of this region. 472 * @return the number of region points. 473 */ 474 public int getSize() { 475 return this.size; 476 } 477 478 /** 479 * Get the x/y axes-parallel bounding box as a rectangle 480 * (including the extremal coordinates). 481 * @return the bounding box rectangle. 482 */ 483 public Rectangle getBoundingBox() { 484 if (right < 0) 485 return null; 486 else 487 return new Rectangle(left, top, right-left + 1, bottom - top + 1); 488 } 489 490 /** 491 * @deprecated 492 * Returns the centroid of this region as a 2D point. 493 * Use {@link getCenterPoint} or {@link getXc} and {@link getYc} instead. 494 * @return the centroid of this region. 495 */ 496 public Point2D getCenter() { 497 if (Double.isNaN(xc)) 498 return null; 499 else 500 return new Point2D.Double(xc, yc); 501 } 502 503 504 /** 505 * Returns the centroid of this region as a 2D point. 506 * See also {@link getXc}, {@link getYc}. 507 * 508 * @return the centroid of this region. 509 */ 510 public Point2D getCenterPoint() { 511 if (Double.isNaN(xc)) 512 return null; 513 else 514 return new Point2D.Double(xc, yc); 515 } 516 517 // iteration -------------------------------------- 518 519 @Override 520 public Iterator<Point> iterator() { 521 return new RegionPixelIterator(this); 522 } 523 524 /** 525 * @deprecated 526 * Replaced by BinaryRegion implementing {@code Iterable<Point>}. 527 * @return iterator to be used in a for-loop. 528 */ 529 public Iterable<Point> getRegionPoints() { 530 //return RegionLabeling.this.getRegionPoints(this); 531 return new Iterable<Point>() { // anonymous class! 532 public Iterator<Point> iterator() { 533 return new RegionPixelIterator(BinaryRegion.this); 534 } 535 }; 536 } 537 538 539 /** 540 * Use this method to add a single pixel to this region. Updates summation 541 * and boundary variables used to calculate various region statistics. 542 * 543 * @param u x-position 544 * @param v y-position 545 */ 546 protected void addPixel(int u, int v) { 547 size = size + 1; 548 x1Sum = x1Sum + u; 549 y1Sum = y1Sum + v; 550 x2Sum = x2Sum + u * u; 551 y2Sum = y2Sum + v * v; 552 if (u < left) left = u; 553 if (v < top) top = v; 554 if (u > right) right = u; 555 if (v > bottom) bottom = v; 556 } 557 558 /** 559 * Call this method to update the region's statistics. For now only the 560 * center coordinates (xc, yc) are updated. Add additional statements as 561 * needed to update your own region statistics. 562 */ 563 protected void update() { 564 if (size > 0) { 565 xc = (double) x1Sum / size; 566 yc = (double) y1Sum / size; 567 } 568 } 569 570 /** 571 * Get the (single) outer contour of this region. 572 * Points on an outer contour are arranged in clockwise 573 * order. 574 * @return the outer contour. 575 */ 576 public Contour getOuterContour() { 577 return outerContour; 578 } 579 580 protected void setOuterContour(Contour contr) { 581 outerContour = contr; 582 } 583 584 /** 585 * Get all inner contours of this region. 586 * Points on inner contours are arranged in counter-clockwise 587 * order. 588 * @return the list of inner contours. 589 */ 590 public List<Contour> getInnerContours() { 591 return innerContours; 592 } 593 594 protected void makeInnerContours() { 595 if (innerContours == null) { 596 innerContours = new LinkedList<Contour>(); 597 } 598 } 599 600 protected void addInnerContour(Contour contr) { 601 makeInnerContours(); 602 innerContours.add(contr); 603 } 604 605 public String toString() { 606 Formatter fm = new Formatter(new StringBuilder(), Locale.US); 607 fm.format("Region %d", label); 608 fm.format(", area = %d", size); 609 fm.format(", bounding box = (%d, %d, %d, %d)", left, top, right, bottom ); 610 fm.format(", centroid = (%.2f, %.2f)", xc, yc); 611 if (innerContours != null) 612 fm.format(", holes = %d", innerContours.size()); 613 String s = fm.toString(); 614 fm.close(); 615 return s; 616 } 617 618 // Compare method for sorting by region size (larger regions at front) 619 @Override 620 public int compareTo(BinaryRegion r2) { 621 return r2.size - this.size; 622 } 623 624 /* EXPERIMENTAL STUFF for attaching region properties dynamically: 625 * Properties can be used to hash results of region calculations 626 * to avoid multiple calculations. 627 * Currently, only 'double' values are supported. 628 * 629 * E.g. calculate major axis angle theta for region r, then do 630 * r.setProperty("angle", theta); 631 * and subsequently 632 * double theta = r.getProperty("angle"); 633 */ 634 635 private Map<String, Double> properties = null; 636 637 /** 638 * Sets the specified property of this region to the given value. 639 * @param name Chosen name of the property. 640 * @param val Value associated with this property. 641 */ 642 public void setProperty(String name, double val) { 643 if (properties == null) { 644 properties = new HashMap<String, Double>(); 645 } 646 properties.put(name, val); 647 } 648 649 /** 650 * Retrieves the specified region property. 651 * An {@link IllegalArgumentException} is thrown if the property 652 * is not defined for this region. 653 * 654 * @param name The name of the property. 655 * @return The value associated with the specified property. 656 */ 657 public double getProperty(String name) { 658 Double value; 659 if (properties == null || (value = properties.get(name)) == null) { 660 throw new IllegalArgumentException("Region property " + name + " is undefined."); 661 } 662 return value.doubleValue(); 663 } 664 665 /** 666 * Removes all properties attached to this region. 667 */ 668 public void clearProperties() { 669 properties.clear(); 670 } 671 672 673 674 } 675 676}