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 *******************************************************************************/ 009package imagingbook.pub.hough; 010 011import java.awt.geom.Point2D; 012import java.util.ArrayList; 013import java.util.Arrays; 014import java.util.List; 015import java.util.Locale; 016 017import ij.IJ; 018import ij.process.FloatProcessor; 019import ij.process.ImageProcessor; 020 021/** 022 * This class implements the Hough Transform for straight lines. 023 * This version fixes the problem with vertical lines (theta = 0). 024 * TODO: merge the two implementations, add bias correction 025 * @author WB 026 * @version 2015/11/13 027 */ 028public class HoughTransformLines { 029 030 public static class Parameters { 031 /** Number of angular steps over [0, pi] */ 032 public int nAng = 256; 033 /** Number of radial steps in each pos/neg direction (accum. size = 2 * nRad + 1) */ 034 public int nRad = 128; 035 public boolean showProgress = true; 036 public boolean showCheckImage = true; 037 public boolean debug = false; 038 } 039 040 private final Parameters params; 041 042 private final int nAng; // number of angular steps over [0, pi] 043 private final int nRad; // number of radial steps in each pos/neg direction 044 045 private final int M, N; // size of the reference frame (image) 046 private final double xc, yc; // reference point (x/y-coordinate of image center) 047 048 private final double dAng; // increment of angle 049 private final double dRad; // increment of radius 050 private final int cRad; // array index representing the zero radius 051 052 private final int accWidth; // width of the accumulator array (angular direction) 053 private final int accHeight; // height of the accumulator array (radial direction) 054 private final int[][] accumulator; // accumulator array 055 private final int[][] accumulatorMax; // accumulator, with local maxima only 056 057 private final double[] cosTable; // tabulated cosine values 058 private final double[] sinTable; // tabulated sine values 059 060 // -------------- public constructor(s) ------------------------ 061 062 /** 063 * Creates a new Hough transform from the image I. 064 * @param I input image, relevant (edge) points have pixel 065 * values greater 0. 066 * @param params parameter object. 067 */ 068 public HoughTransformLines(ImageProcessor I, Parameters params) { 069 this(I.getWidth(), I.getHeight(), params); 070 process(I); 071 } 072 073 /** 074 * Creates a new Hough transform from a sequence of 2D points. 075 * Parameters M, N are only used to specify the reference point 076 * (usually at the center of the image). 077 * Use this constructor if the relevant image points are collected 078 * separately. 079 * @param points an array of 2D points. 080 * @param M width of the corresponding image plane. 081 * @param N height of the corresponding image plane. 082 * @param params parameter object. 083 */ 084 public HoughTransformLines(Point2D[] points, int M, int N, Parameters params) { 085 this(M, N, params); 086 process(points); 087 } 088 089 090 // Non-public constructor used by public constructors (to set up all final members variables. 091 private HoughTransformLines(int M, int N, Parameters params) { 092 this.params = (params == null) ? new Parameters() : params; 093 this.M = M; 094 this.N = N; 095 this.xc = M / 2; // integer value 096 this.yc = N / 2; // integer value 097 this.nAng = params.nAng; 098 this.nRad = params.nRad; 099 this.dAng = Math.PI / nAng; 100 this.dRad = 0.5 * Math.sqrt(M * M + N * N) / nRad; // nRad radial steps over half the diagonal length 101 this.cRad = nRad; 102 this.accWidth = nAng; 103 this.accHeight = nRad + 1 + nRad; 104 this.accumulator = new int[accWidth][accHeight]; // cells are initialized to zero! 105 this.accumulatorMax = new int[accWidth][accHeight]; 106 this.cosTable = makeCosTable(); 107 this.sinTable = makeSinTable(); 108 } 109 110 // -------------- public methods ------------------------ 111 112 /** 113 * Finds and returns the parameters of the strongest lines with 114 * a specified min. pixel count. All objects in the returned array 115 * are valid, but the array may be empty. 116 * Note: Could perhaps be implemented more efficiently with insert-sort. 117 * @param amin the minimum accumulator value for each line. 118 * @param maxLines maximum number of (strongest) lines to extract. 119 * @return a possibly empty array of {@link HoughLine} objects. 120 */ 121 public HoughLine[] getLines(int amin, int maxLines) { 122 findLocalMaxima(); 123 // create an array of n blank HoughLine objects (with initial count = -1): 124 HoughLine[] lineArr = new HoughLine[maxLines]; 125 for (int i = 0; i < lineArr.length; i++) { 126 lineArr[i] = new HoughLine(); // new HoughLine(0.0, 0.0, -1); 127 } 128 129 for (int ri = 0; ri < accHeight; ri++) { 130 for (int ai = 0; ai < accWidth; ai++) { 131 int hcount = accumulatorMax[ai][ri]; 132 if (hcount >= amin) { 133 HoughLine last = lineArr[lineArr.length - 1]; 134 // last holds the weakest line found so far - replace it? 135 if (hcount > last.count) { 136 last.angle = angleFromIndex(ai); // replace the weakest line 137 last.radius = radiusFromIndex(ri); 138 last.count = hcount; 139 // sort all lines for descending 'count': 140 Arrays.sort(lineArr); // more efficient with insert sort? 141 } 142 } 143 } 144 } 145 // lineArr is sorted by count (highest counts first). 146 // collect all lines with count >= minPts into a new list: 147 List<HoughLine> lineList = new ArrayList<HoughLine>(); 148 for (HoughLine hl : lineArr) { 149 if (hl.getCount() < amin) break; 150 lineList.add(hl); 151 } 152 // convert the list to an array and return: 153 return lineList.toArray(new HoughLine[0]); 154 } 155 156 /** 157 * @return The reference point used by this Hough transform. 158 */ 159 public Point2D getReferencePoint() { 160 return new Point2D.Double(xc, yc); 161 } 162 163 /** 164 * Calculates the actual angle (in radians) for angle index {@code ai} 165 * @param ai angle index [0,...,nAng-1] 166 * @return Angle [0,...,PI] for angle index ai 167 */ 168 public double angleFromIndex(int ai) { 169 return ai * dAng; 170 } 171 172 /** 173 * Calculates the actual radius for radius index ri. 174 * @param ri radius index [0,...,nRad-1]. 175 * @return Radius [-maxRad,...,maxRad] with respect to reference point (xc, yc). 176 */ 177 public double radiusFromIndex(int ri) { 178 return (ri - cRad) * dRad; 179 } 180 181 private int radiusToIndex(double rad) { 182 return cRad + (int) Math.rint(rad / dRad); 183 } 184 185// public int[][] getAccumulator() { 186// return accumulator; 187// } 188 189// public int[][] getAccumulatorMax() { 190// return accumulatorMax; 191// } 192 193 /** 194 * Creates and returns an image of the 2D accumulator array. 195 * @return A FloatProcessor (since accumulator values may be large). 196 */ 197 public FloatProcessor getAccumulatorImage() { 198 return new FloatProcessor(accumulator); 199 } 200 201 /** 202 * Creates and returns an image of the extended 2D accumulator array, 203 * produced by adding a vertically mirrored copy of the accumulator 204 * to its right end. 205 * @return A FloatProcessor (since accumulator values may be large). 206 */ 207 public FloatProcessor getAccumulatorImageExtended() { 208 FloatProcessor fp = new FloatProcessor(2 * accWidth, accHeight); 209 for (int ai = 0; ai < accWidth; ai++) { 210 for (int ri = 0; ri < accHeight; ri++) { 211 fp.setf(ai, ri, accumulator[ai][ri]); 212 if (ri > 0) { 213 fp.setf(accWidth + ai, ri, accumulator[ai][accHeight - ri]); 214 } 215 } 216 } 217 fp.resetMinAndMax(); 218 return fp; 219 } 220 221 222 /** 223 * Creates and returns an image of the local maxima of the 224 * 2D accumulator array. 225 * @return A FloatProcessor (since accumulator values may be large). 226 */ 227 public FloatProcessor getAccumulatorMaxImage() { 228 return new FloatProcessor(accumulatorMax); 229 } 230 231 // -------------- nonpublic methods ------------------------ 232 233 private double[] makeCosTable() { 234 double[] cosTab = new double[nAng]; 235 for (int ai = 0; ai < nAng; ai++) { 236 double angle = dAng * ai; 237 cosTab[ai] = Math.cos(angle); 238 } 239 return cosTab; 240 } 241 242 private double[] makeSinTable() { 243 double[] sinTab = new double[nAng]; 244 for (int ai = 0; ai < nAng; ai++) { 245 double angle = dAng * ai; 246 sinTab[ai] = Math.sin(angle); 247 } 248 return sinTab; 249 } 250 251 private void process(ImageProcessor ip) { 252// ByteProcessor check = new ByteProcessor(M, N); 253 if (params.showProgress) IJ.showStatus("filling accumulator ..."); 254 for (int v = 0; v < N; v++) { 255 if (params.showProgress) IJ.showProgress(v, N); 256 for (int u = 0; u < M; u++) { 257 if ((0xFFFFFF & ip.get(u, v)) != 0) { // this is a foreground (edge) pixel - use ImageAccessor?? 258 processPoint(u, v); 259// check.putPixel(u, v, 128); 260 } 261 } 262 } 263 if (params.showProgress) 264 IJ.showProgress(1, 1); 265// if (params.showCheckImage) 266// (new ImagePlus("Check", check)).show(); 267 } 268 269 private void process(Point2D[] points) { 270 if (params.showProgress) IJ.showStatus("filling accumulator ..."); 271 for (int i = 0; i < points.length; i++) { 272 if (params.showProgress && i % 50 == 0) IJ.showProgress(i, points.length); 273 Point2D p = points[i]; 274 if (p != null) { 275 processPoint(p.getX(), p.getY()); 276 } 277 } 278 if (params.showProgress) IJ.showProgress(1, 1); 279 } 280 281 282 private void processPoint(double u, double v) { 283 final double x = u - xc; 284 final double y = v - yc; 285 for (int ai = 0; ai < accWidth; ai++) { 286// double theta = dAng * ai; 287// double r = x * Math.cos(theta) + y * Math.sin(theta); 288 double r = x * cosTable[ai] + y * sinTable[ai]; // sin/cos tables improve speed! 289 int ri = radiusToIndex(r); // cRad + (int) Math.rint(r / dRad); - changed 290 if (ri >= 0 && ri < accHeight) { 291 accumulator[ai][ri]++; 292 } 293 } 294 } 295 296 297 /** 298 * This version considers that the accumulator is not truly 299 * periodic but the continuation at its right boundary is 300 * vertically mirrored. 301 */ 302 private void findLocalMaxima() { 303 if (params.showProgress) IJ.showStatus("finding local maxima"); 304 int count = 0; 305 for (int aC = 0; aC < accWidth; aC++) { // center angle index 306 // angle index ai is treated cyclically but the accumulator 307 // must be mirrored vertically at boundaries: 308 int a0 = aC - 1; // left angle index 309 boolean mL = false; // left vertical mirror flag 310 if (a0 < 0) { 311 a0 = accWidth - 1; 312 mL = true; // left vertical mirror required 313 } 314 int a1 = aC + 1; // right angle index 315 boolean mR = false; // right vertical mirror flag 316 if (a1 >= accWidth) { 317 a1 = 0; 318 mR = true; // right vertical mirror required 319 } 320 321 for (int rC = 1; rC < accHeight - 1; rC++) { 322 // do we need to swap vertical on either side? 323 int r0 = (mL) ? accHeight - rC + 1 : rC - 1; 324 int r1 = (mR) ? accHeight - rC - 1 : rC + 1; 325 int vC = accumulator[aC][rC]; 326 // this test is critical if 2 identical cell values 327 // appear next to each other! 328 boolean ismax = 329 vC > accumulator[a1][rC] && // 0 330 vC > accumulator[a1][r0] && // 1 331 vC > accumulator[aC][r0] && // 2 332 vC > accumulator[a0][r0] && // 3 333 vC > accumulator[a0][rC] && // 4 334 vC > accumulator[a0][r1] && // 5 335 vC > accumulator[aC][r1] && // 6 336 vC > accumulator[a1][r1] ; // 7 337 if (ismax) { 338 accumulatorMax[aC][rC] = vC; 339 count++; 340 if (params.debug && vC > 50) { 341 IJ.log("found max at " + aC + " / " + rC); 342 } 343 } 344 } 345 } 346 if (params.debug) IJ.log("found maxima: " + count); 347 } 348 349 350 // old version 351// private void findLocalMaxima() { 352// if (params.showProgress) IJ.showStatus("finding local maxima"); 353// int count = 0; 354// for (int ia = 0; ia < accWidth; ia++) { 355// // angle index ai is treated cyclically: 356// final int a1 = (ia > 0) ? ia - 1 : accWidth - 1; 357// final int a2 = (ia < accWidth - 1) ? ia + 1 : 0; 358// for (int ir = 1; ir < accHeight - 1; ir++) { 359// int ha = accumulator[ia][ir]; 360// // this test is critical if 2 identical cell values 361// // appear next to each other! 362// boolean ismax = 363// ha > accumulator[a1][ir - 1] && 364// ha > accumulator[a1][ir] && 365// ha > accumulator[a1][ir + 1] && 366// ha > accumulator[ia][ir - 1] && 367// ha > accumulator[ia][ir + 1] && 368// ha > accumulator[a2][ir - 1] && 369// ha > accumulator[a2][ir] && 370// ha > accumulator[a2][ir + 1] ; 371// if (ismax) { 372// accumulatorMax[ia][ir] = ha; 373// count++; 374// if (params.debug && ha > 50){ 375// IJ.log("found max at " + ia + " / " + ir); 376// } 377// } 378// } 379// } 380// if (params.debug) IJ.log("found maxima: " + count); 381//} 382 383 /** 384 * This class represents a straight line in Hessian normal form, 385 * i.e., x * cos(angle) + y * sin(angle) = radius. 386 * It is implemented as a non-static inner class of {@link HoughTransformLines} 387 * since its instances refer to the particular enclosing Hough transform object. 388 */ 389 public class HoughLine implements Comparable<HoughLine> { 390 private double angle; 391 private double radius; 392 private int count; 393 394 private HoughLine() { 395 this(0.0, 0.0, -1); 396 } 397 398 /** 399 * Public constructor (only available with an enclosing HoughTransformLines instance!) 400 * 401 * @param angle angle 402 * @param radius radius 403 * @param count count 404 */ 405 public HoughLine(double angle, double radius, int count) { 406 this.angle = angle; 407 this.radius = radius; 408 this.count = count; 409 } 410 411 /** 412 * @return The angle of this line. 413 */ 414 public double getAngle() { 415 return angle; 416 } 417 418 /** 419 * @return The radius of this line with respect to the reference 420 * point (xc, yc) of the enclosing {@link HoughTransformLines} 421 * instance. 422 */ 423 public double getRadius() { 424 return radius; 425 } 426 427 /** 428 * @return The accumulator count associated with this line. 429 */ 430 public int getCount() { 431 return count; 432 } 433 434 public Point2D getReferencePoint() { 435 // Note that the reference point is a property of the 436 // containing HoughTransform object: 437 return HoughTransformLines.this.getReferencePoint(); 438 } 439 440 /** 441 * Returns the perpendicular distance between this line and the point (x, y). 442 * The result may be positive or negative, depending on which side of 443 * the line (x, y) is located. 444 * @param x x-coordinate of point position. 445 * @param y y-coordinate of point position. 446 * @return The perpendicular distance between this line and the point (x, y). 447 */ 448 public double getDistance(double x, double y) { 449 final double xs = x - xc; 450 final double ys = y - yc; 451 return Math.cos(angle) * xs + Math.sin(angle) * ys - radius; 452 } 453 454 /** 455 * Returns the perpendicular distance between this line and the point p. 456 * The result may be positive or negative, depending on which side of 457 * the line p is located. 458 * @param p point position. 459 * @return The perpendicular distance between this line and the point p. 460 */ 461 public double getDistance(Point2D p) { 462 return getDistance(p.getX(), p.getY()); 463 } 464 465 /** 466 * This is a brute-force drawing method which simply marks all 467 * image pixels that are sufficiently close to the HoughLine hl. 468 * The drawing color for ip must be previously set. 469 * @param ip The ImageProcessor to draw to. 470 * @param thickness The thickness of the lines to be drawn. 471 */ 472 public void draw(ImageProcessor ip, double thickness) { 473 final int w = ip.getWidth(); 474 final int h = ip.getHeight(); 475 final double dmax = 0.5 * thickness; 476 for (int u = 0; u < w; u++) { 477 for (int v = 0; v < h; v++) { 478 // get the distance between (u,v) and the line hl: 479 double d = Math.abs(this.getDistance(u, v)); 480 if (d <= dmax) { 481 ip.drawPixel(u, v); 482 } 483 } 484 } 485 486 } 487 488 /** 489 * Required by the {@link Comparable} interface, used for sorting 490 * lines by their point count. 491 * @param hl2 another {@link HoughLine} instance. 492 */ 493 public int compareTo (HoughLine hl2){ 494 HoughLine hl1 = this; 495 if (hl1.count > hl2.count) 496 return -1; 497 else if (hl1.count < hl2.count) 498 return 1; 499 else 500 return 0; 501 } 502 503 public String toString() { 504 return String.format(Locale.US, "%s <angle = %.3f, radius = %.3f, count = %d>", 505 HoughLine.class.getSimpleName(), angle, radius, count); 506 } 507 508 } // end of class HoughLine 509 510} // end of class LinearHT 511 512