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 ij.IJ; 012import ij.ImagePlus; 013import ij.process.ByteProcessor; 014import ij.process.FloatProcessor; 015import ij.process.ImageProcessor; 016 017import java.awt.geom.Point2D; 018import java.util.ArrayList; 019import java.util.Arrays; 020import java.util.List; 021import java.util.Locale; 022 023/** 024 * This class implements the Hough Transform for straight lines. 025 * MODIFIED PARAMETER SPACE (no negative RADIUS)! 026 * @author WB 027 * @version 2015/11/13 028 */ 029 030public class HoughTransformLinesPosRadius { 031 032 public static class Parameters { 033 /** Number of angular steps over [0, pi] */ 034 public int nAng = 256; 035 /** Number of radial steps (pos. radii only) */ 036 public int nRad = 256; 037 public boolean showProgress = true; 038 public boolean showCheckImage = true; 039 public boolean debug = false; 040 } 041 042 private final Parameters params; 043 044 private final int nAng; // number of steps for the angle (a = 0,...,2PI) 045 private final int nRad; // number of steps for the radius (r = 0,...,r_max) 046 047 private final int M, N; // size of the reference frame (image) 048 private final double xc, yc; // reference point (x/y-coordinate of image center) 049 050 private final double dAng; // increment of angle 051 private final double dRad; // increment of radius 052 private final int cRad; // array index for zero radius (r[cRad] = 0) 053 054 private final int[][] accumulator; // Hough accumulator array 055 private final int[][] accumulatorMax; // Hough accumulator, local maxima only 056 057 private final double[] cosTable; 058 private final double[] sinTable; 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 HoughTransformLinesPosRadius(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 list 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 HoughTransformLinesPosRadius(Point2D[] points, int M, int N, Parameters params) { 085 this(M, N, params); 086 process(points); 087 } 088 089 // -------------- non-public constructor ------------------------ 090 091 // used by public constructors (sets up all final members): 092 private HoughTransformLinesPosRadius(int M, int N, Parameters params) { 093 this.params = (params == null) ? new Parameters() : params; 094 this.M = M; 095 this.N = N; 096 this.xc = M / 2; 097 this.yc = N / 2; 098 this.nAng = params.nAng; 099 this.nRad = params.nRad; 100 this.dAng = 2 * Math.PI / nAng; // CHANGE 101 this.dRad = 0.5 * Math.sqrt(M * M + N * N) / nRad; 102 this.cRad = 0; // nRad / 2; // CHANGE 103 this.accumulator = new int[nAng][nRad]; // cells are initialized to zero! 104 this.accumulatorMax = new int[nAng][nRad]; 105 this.cosTable = makeCosTable(); 106 this.sinTable = makeSinTable(); 107 } 108 109 // -------------- public methods ------------------------ 110 111 public int getnRad() { 112 return nRad; 113 } 114 public int getnAng() { 115 return nAng; 116 } 117 118 /** 119 * Finds and returns the parameters of the strongest lines with 120 * a specified min. pixel count. All objects in the returned array 121 * are valid, but the array may be empty. 122 * Note: Could perhaps be implemented more efficiently with insert-sort. 123 * @param amin the minimum accumulator value for each line. 124 * @param maxLines maximum number of (strongest) lines to extract. 125 * @return a possibly empty array of {@link HoughLine} objects. 126 */ 127 public HoughLine[] getLines(int amin, int maxLines) { 128 findAccumulatorPeaks(); 129 // create an array of n blank HoughLine objects (with initial count = -1): 130 HoughLine[] lineArr = new HoughLine[maxLines]; 131 for (int i = 0; i < lineArr.length; i++) { 132 lineArr[i] = new HoughLine(0, 0, -1); 133 } 134 135 for (int ir = 0; ir < nRad; ir++) { 136 for (int ia = 0; ia < nAng; ia++) { 137 int hcount = accumulatorMax[ia][ir]; 138 if (hcount >= amin) { 139 HoughLine last = lineArr[lineArr.length - 1]; 140 // last holds the weakest line found so far - replace it? 141 if (hcount > last.count) { 142 last.angle = angleFromIndex(ia); // replace the weakest line 143 last.radius = radiusFromIndex(ir); 144 last.count = hcount; 145 // sort all lines for descending 'count': 146 Arrays.sort(lineArr); // more efficient with insert sort? 147 } 148 } 149 } 150 } 151 // lineArr is sorted by count (highest counts first). 152 // collect all lines with count >= minPts into a new list: 153 List<HoughLine> lineList = new ArrayList<HoughLine>(); 154 for (HoughLine hl : lineArr) { 155 if (hl.getCount() < amin) break; 156 lineList.add(hl); 157 } 158 // convert the list to an array and return: 159 return lineList.toArray(new HoughLine[0]); 160 } 161 162 /** 163 * @return The reference point used by this Hough transform. 164 */ 165 public Point2D getReferencePoint() { 166 return new Point2D.Double(xc, yc); 167 } 168 169 /** 170 * Calculates the actual angle (in radians) for angle index {@code ai} 171 * @param i angle index [0,...,nAng-1] 172 * @return Angle [0,...,PI] for angle index ai 173 */ 174 public double angleFromIndex(int i) { 175 return i * dAng; 176 } 177 178 /** 179 * Calculates the actual radius for radius index ri. 180 * @param j radius index [0,...,nRad-1]. 181 * @return Radius [-maxRad,...,maxRad] with respect to reference point (xc, yc). 182 */ 183 public double radiusFromIndex(int j) { 184 return (j - cRad) * dRad; 185 } 186 187 public int[][] getAccumulator() { 188 return accumulator; 189 } 190 191 public int[][] getAccumulatorMax() { 192 return accumulatorMax; 193 } 194 195 /** 196 * Creates and returns an image of the 2D accumulator array. 197 * @return A FloatProcessor (since accumulator values may be large). 198 */ 199 public FloatProcessor getAccumulatorImage() { 200 FloatProcessor fp = new FloatProcessor(nAng, nRad); 201 for (int ir = 0; ir < nRad; ir++) { 202 for (int ia = 0; ia < nAng; ia++) { 203 fp.setf(ia, ir, accumulator[ia][ir]); 204 } 205 } 206 fp.resetMinAndMax(); 207 return fp; 208 } 209 210 /** 211 * Creates and returns an image of the local maxima of the 212 * 2D accumulator array. 213 * @return A FloatProcessor (since accumulator values may be large). 214 */ 215 public FloatProcessor getAccumulatorMaxImage() { 216 FloatProcessor fp = new FloatProcessor(nAng, nRad); 217 for (int ir = 0; ir < nRad; ir++) { 218 for (int ia = 0; ia < nAng; ia++) { 219 fp.setf(ia, ir, accumulatorMax[ia][ir]); 220 } 221 } 222 fp.resetMinAndMax(); 223 return fp; 224 } 225 226 // -------------- nonpublic methods ------------------------ 227 228 private double[] makeCosTable() { 229 double[] cosTab = new double[nAng]; 230 for (int ia = 0; ia < nAng; ia++) { 231 double theta = dAng * ia; 232 cosTab[ia] = Math.cos(theta); 233 } 234 return cosTab; 235 } 236 237 private double[] makeSinTable() { 238 double[] sinTab = new double[nAng]; 239 for (int ia = 0; ia < nAng; ia++) { 240 double theta = dAng * ia; 241 sinTab[ia] = Math.sin(theta); 242 } 243 return sinTab; 244 } 245 246 private void process(ImageProcessor ip) { 247 ByteProcessor check = new ByteProcessor(M, N); 248 if (params.showProgress) IJ.showStatus("filling accumulator ..."); 249 for (int v = 0; v < N; v++) { 250 if (params.showProgress) IJ.showProgress(v, N); 251 for (int u = 0; u < M; u++) { 252 if ((0xFFFFFF & ip.get(u, v)) != 0) { // this is a foreground (edge) pixel - use ImageAccessor?? 253 doOnePoint(u, v); 254 check.putPixel(u, v, 128); 255 } 256 } 257 } 258 if (params.showProgress) 259 IJ.showProgress(1, 1); 260 if (params.showCheckImage) 261 (new ImagePlus("Check", check)).show(); 262 } 263 264 private void process(Point2D[] points) { 265 if (params.showProgress) IJ.showStatus("filling accumulator ..."); 266 for (int i = 0; i < points.length; i++) { 267 if (params.showProgress && i % 50 == 0) IJ.showProgress(i, points.length); 268 Point2D p = points[i]; 269 if (p != null) { 270 doOnePoint(p.getX(), p.getY()); 271 } 272 } 273 if (params.showProgress) IJ.showProgress(1, 1); 274 } 275 276 public int closestRadialIndex(double r) { 277 return cRad + (int) Math.rint(r / dRad); 278 } 279 280 private void doOnePoint(double u, double v) { 281 final double x = u - xc; 282 final double y = v - yc; 283 for (int ia = 0; ia < nAng; ia++) { 284// double theta = dAng * ai; 285// double r = x * Math.cos(theta) + y * Math.sin(theta); 286 double r = x * cosTable[ia] + y * sinTable[ia]; // sin/cos tables improve speed! 287 int ir = closestRadialIndex(r); //cRad + (int) Math.rint(r / dRad); 288 if (ir >= 0 && ir < nRad) { 289 //accumulator[ia][ir]++; 290 accumulator[ia][ir]+= 1; 291 } 292 } 293 } 294 295 // TODO: lines with ZERO radius cannot be detected!! 296 private void findAccumulatorPeaks() { 297 if (params.showProgress) IJ.showStatus("finding local maxima"); 298 int count = 0; 299 for (int ia = 0; ia < nAng; ia++) { 300 // angle index ai is treated cyclically: 301 final int a1 = (ia > 0) ? ia - 1 : nAng - 1; 302 final int a2 = (ia < nAng-1) ? ia + 1 : 0; 303 for (int ir = 1; ir < nRad - 1; ir++) { 304 int ha = accumulator[ia][ir]; 305 // this test is critical if 2 identical cell values 306 // appear next to each other! 307 boolean ismax = 308 ha > accumulator[a1][ir - 1] && 309 ha > accumulator[a1][ir] && 310 ha > accumulator[a1][ir + 1] && 311 ha > accumulator[ia][ir - 1] && 312 ha > accumulator[ia][ir + 1] && 313 ha > accumulator[a2][ir - 1] && 314 ha > accumulator[a2][ir] && 315 ha > accumulator[a2][ir + 1] ; 316 if (ismax) { 317 accumulatorMax[ia][ir] = ha; 318 count++; 319 } 320 } 321 } 322 if (params.debug) IJ.log("found maxima: " + count); 323 } 324 325 /** 326 * This class represents a straight line in Hessian normal form, 327 * i.e., x * cos(angle) + y * sin(angle) = radius. 328 * It is implemented as a non-static inner class of {@link HoughTransformLinesPosRadius} 329 * since its instances refer to the particular enclosing Hough transform object. 330 */ 331 public class HoughLine implements Comparable<HoughLine> { 332 private double angle; 333 private double radius; 334 private int count; 335 336 // public constructor (only available with an enclosing HoughTransformLines instance!) 337 public HoughLine(double angle, double radius, int count){ 338 this.angle = angle; 339 this.radius = radius; 340 this.count = count; 341 } 342 343 /** 344 * @return The angle of this line. 345 */ 346 public double getAngle() { 347 return angle; 348 } 349 350 /** 351 * @return The radius of this line with respect to the reference 352 * point (xc, yc) of the enclosing {@link HoughTransformLinesPosRadius} 353 * instance. 354 */ 355 public double getRadius() { 356 return radius; 357 } 358 359 /** 360 * @return The accumulator count associated with this line. 361 */ 362 public int getCount() { 363 return count; 364 } 365 366 public Point2D getReferencePoint() { 367 return HoughTransformLinesPosRadius.this.getReferencePoint(); 368 } 369 370 /** 371 * Returns the perpendicular distance between this line and the point (x, y). 372 * The result may be positive or negative, depending on which side of 373 * the line (x, y) is located. 374 * @param x x-coordinate of point position. 375 * @param y y-coordinate of point position. 376 * @return The perpendicular distance between this line and the point (x, y). 377 */ 378 public double getDistance(double x, double y) { 379 final double xs = x - xc; 380 final double ys = y - yc; 381 return Math.cos(angle) * xs + Math.sin(angle) * ys - radius; 382 } 383 384 /** 385 * Returns the perpendicular distance between this line and the point p. 386 * The result may be positive or negative, depending on which side of 387 * the line p is located. 388 * @param p point position. 389 * @return The perpendicular distance between this line and the point p. 390 */ 391 public double getDistance(Point2D p) { 392 return getDistance(p.getX(), p.getY()); 393 } 394 395 /** 396 * Required by the {@link Comparable} interface, used for sorting 397 * lines by their point count. 398 * @param hl2 another {@link HoughLine} instance. 399 */ 400 public int compareTo (HoughLine hl2){ 401 HoughLine hl1 = this; 402 if (hl1.count > hl2.count) 403 return -1; 404 else if (hl1.count < hl2.count) 405 return 1; 406 else 407 return 0; 408 } 409 410 public String toString() { 411 return String.format(Locale.US, "%s <angle = %.3f, radius = %.3f, count = %d>", 412 HoughLine.class.getSimpleName(), angle, radius, count); 413 } 414 415 } // end of class HoughLine 416 417} // end of class LinearHT 418 419 420 421 422 423 424