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