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