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.color.edge;
011
012import ij.plugin.filter.Convolver;
013import ij.process.ByteProcessor;
014import ij.process.ColorProcessor;
015import ij.process.FloatProcessor;
016import ij.process.ImageProcessor;
017
018import java.awt.Point;
019import java.util.LinkedList;
020import java.util.List;
021import java.util.Stack;
022
023/**
024 * This class implements a Canny edge detector for grayscale and RGB images.
025 * @author W. Burger
026 * @version 2016/05/04
027 */
028public class CannyEdgeDetector extends ColorEdgeDetector {
029        
030        public static class Parameters {
031                /** Gaussian sigma (scale) */
032                public double gSigma = 2.0f;
033                /** High threshold (20% of max. edge magnitude) */
034                public double hiThr  = 20.0f;
035                /** Low threshold (5% of max. edge magnitude) */
036                public double loThr = 5.0f;
037                /** Set true to normalize gradient magnitude */
038                public boolean normGradMag = true;      
039                
040                /**
041                 * Checks the parameter set.
042                 * @return true if any invalid condition is found
043                 */
044                public boolean isInValid () { 
045                        return gSigma < 0.1f || loThr > hiThr;
046                }
047        }
048        
049        final Parameters params;
050        final ImageProcessor I;                         // original image
051        final int M, N;                                         // width and height of I
052        
053        FloatProcessor Emag = null;                             // gradient magnitude
054        FloatProcessor Enms = null;                             // non-max suppressed gradient magnitude
055        FloatProcessor Ex = null, Ey = null;    // edge normal vectors
056        ByteProcessor Ebin = null;                              // final (binary) edge image
057        List<List<Point>> traceList = null;             // list of edge traces
058        
059        // Constructor with default parameters:
060        public CannyEdgeDetector(ImageProcessor ip) {
061                this(ip, new Parameters());
062        }
063        
064        // Constructor with parameter object:
065        public CannyEdgeDetector(ImageProcessor I, Parameters params) {
066                if (params.isInValid()) throw new IllegalArgumentException();
067                this.params = params;
068                this.I = I;
069                M = I.getWidth();
070                N = I.getHeight();
071                findEdges();
072        }
073        
074        // do the work ...
075        void findEdges() {
076                if (I instanceof ColorProcessor) 
077                        makeGradientsAndMagnitudeColor();
078                else
079                        makeGradientsAndMagnitudeGray();
080                nonMaxSuppression();
081                //detectAndTraceEdges();
082        }
083        
084        //---------------------------------------------------------------------------
085        
086        void makeGradientsAndMagnitudeGray() {
087//              FloatProcessor If = (I instanceof FloatProcessor) ? 
088//                              (FloatProcessor) I.duplicate() :
089//                              (FloatProcessor) I.convertToFloat();
090                
091                FloatProcessor If = I.convertToFloatProcessor();        // always makes a copy
092                                
093                // apply a separable Gaussian filter to I
094                float[] gaussKernel = makeGaussKernel1d(params.gSigma);
095                Convolver conv = new Convolver();
096                conv.setNormalize(true);
097                conv.convolve(If, gaussKernel, gaussKernel.length, 1);
098                conv.convolve(If, gaussKernel, 1, gaussKernel.length);
099                
100                // calculate the gradients in X- and Y-direction
101                Ex = If;
102                Ey = (FloatProcessor) If.duplicate();
103                float[] gradKernel = {-0.5f, 0, 0.5f};
104                conv.setNormalize(false);
105                conv.convolve(Ex, gradKernel, gradKernel.length, 1);
106                conv.convolve(Ey, gradKernel, 1, gradKernel.length);
107                
108                Emag = new FloatProcessor(M, N);
109                float emax = 0;
110                for (int v = 0; v < N; v++) {
111                        for (int u = 0; u < M; u++) {
112                                float dx = Ex.getf(u,v);
113                                float dy = Ey.getf(u,v);
114                                float mag = (float) Math.hypot(dx, dy); // = (float) Math.sqrt(dx*dx + dy*dy);
115                                if (mag > emax) 
116                                        emax = mag;
117                                Emag.setf(u, v, mag);
118                        }
119                }
120                //IJ.log("Gray emax = " + emax);
121                
122                // normalize gradient magnitude 
123                if (params.normGradMag && emax > 0.001) 
124                        Emag.multiply(100.0/emax);
125        }
126        
127        void makeGradientsAndMagnitudeColor() {
128                FloatProcessor[] Irgb = rgbToFloatChannels((ColorProcessor)I);
129                FloatProcessor[] Ixrgb = new FloatProcessor[3];
130                FloatProcessor[] Iyrgb = new FloatProcessor[3];
131                
132                // apply a separable Gaussian filter to each RGB channel
133                float[] gaussKernel = makeGaussKernel1d(params.gSigma);
134                Convolver conv = new Convolver();
135                conv.setNormalize(true);
136                for (int i=0; i<Irgb.length; i++) {
137                        FloatProcessor I = Irgb[i];
138                        conv.convolve(I, gaussKernel, gaussKernel.length, 1);
139                        conv.convolve(I, gaussKernel, 1, gaussKernel.length);
140                        Ixrgb[i] = I;
141                        Iyrgb[i] = (FloatProcessor) I.duplicate();
142                }
143                
144                // calculate the gradients in X- and Y-direction for each RGB channel
145                float[] gradKernel = {-0.5f, 0, 0.5f};
146                conv.setNormalize(false);
147                for (int i = 0; i < Irgb.length; i++) {
148                        FloatProcessor Ix = Ixrgb[i];
149                        FloatProcessor Iy = Iyrgb[i];
150                        conv.convolve(Ix, gradKernel, gradKernel.length, 1);
151                        conv.convolve(Iy, gradKernel, 1, gradKernel.length);
152                }
153
154                // calculate gradient magnitude
155                Ex = new FloatProcessor(M, N);
156                Ey = new FloatProcessor(M, N);
157                Emag = new FloatProcessor(M, N);
158                float emax = 0;
159                for (int v = 0; v < N; v++) {
160                        for (int u = 0; u < M; u++) {
161                                float rx = Ixrgb[0].getf(u,v), ry = Iyrgb[0].getf(u,v);
162                                float gx = Ixrgb[1].getf(u,v), gy = Iyrgb[1].getf(u,v);
163                                float bx = Ixrgb[2].getf(u,v), by = Iyrgb[2].getf(u,v);
164                                float A = rx*rx + gx*gx + bx*bx;
165                                float B = ry*ry + gy*gy + by*by;
166                                float C = rx*ry + gx*gy + bx*by;
167                                float D = (float) Math.sqrt((A - B)*(A - B) + 4*C*C);
168                                
169                                float mag = (float) Math.sqrt(0.5*(A+B+D));
170                                if (mag > emax) emax = mag;
171                                Emag.setf(u, v, mag);
172                                Ex.setf(u, v, A - B + D);
173                                Ey.setf(u, v, 2*C);
174                        }
175                }
176                //IJ.log("RGB emax = " + emax);
177                // normalize gradient magnitude 
178                if (params.normGradMag && emax > 0.001) 
179                        Emag.multiply(100.0/emax);
180        }
181        
182        
183        //---------------------------------------------------------------------------
184        
185        void nonMaxSuppression() {
186                // perform non-maximum suppression along gradient direction
187                Enms = new FloatProcessor(M, N);
188                for (int v = 1; v < N - 1; v++) {
189                        for (int u = 1; u < M - 1; u++) {
190                                int s_theta = getOrientationSector(Ex.getf(u, v), Ey.getf(u, v));
191                                if (isLocalMaximum(Emag, u, v, s_theta, (float) params.loThr)) {
192                                        Enms.setf(u, v, Emag.getf(u, v)); // keep local maximum only
193                                }
194                        }
195                }
196        }
197        
198        void detectAndTraceEdges() {
199                if (Enms == null) {
200                        nonMaxSuppression();
201                }
202                Ebin = new ByteProcessor(M, N);
203                int color = 255;
204                traceList = new LinkedList<List<Point>>();
205                for (int v = 0; v < N; v++) {
206                        for (int u = 0; u < M; u++) {
207                                if (Enms.getf(u, v) >= params.hiThr && Ebin.get(u, v) == 0) { // unmarked edge point
208                                        List<Point> trace = traceAndThreshold(u, v, (float) params.loThr, color);
209                                        traceList.add(trace);
210                                }
211                        }
212                }
213        }
214        
215        // Determines if the gradient magnitude is a local maximum at position (u,v)
216        // in direction s_theta.
217        boolean isLocalMaximum(FloatProcessor gradMagnitude, int u, int v, int s_theta, float mMin) {
218                float mC = gradMagnitude.getf(u, v);
219                if (mC < mMin) {
220                        return false;
221                }
222                else {
223                        float mL = 0, mR = 0;
224                        switch (s_theta) {
225                        case 0 : 
226                                mL = gradMagnitude.getf(u-1, v);
227                                mR = gradMagnitude.getf(u+1, v);
228                                break;
229                        case 1 : 
230                                mL = gradMagnitude.getf(u-1, v-1);
231                                mR = gradMagnitude.getPixelValue(u+1, v+1);
232                                break;
233                        case 2 : 
234                                mL = gradMagnitude.getf(u, v-1);
235                                mR = gradMagnitude.getf(u, v+1);
236                                break;
237                        case 3 : 
238                                mL = gradMagnitude.getf(u-1, v+1);
239                                mR = gradMagnitude.getf(u+1, v-1);
240                                break;
241                        }
242                        return (mL <= mC && mC >= mR);
243                }
244        }
245        
246        ///Recursively collect and mark all pixels of an edge that are 8-connected to 
247        // (u0,v0) and have a gradient magnitude above loThr.
248        /**
249         * Recursively collect and mark all pixels of an edge that are 8-connected to 
250         * (u0,v0) and have a gradient magnitude above loThr.
251         * @param u0 start coordinate (x)
252         * @param v0 start coordinate (y)
253         * @param loThr low threshold (min. edge magnitude to continue tracing)
254         * @param markColor color used for marking pixels on edge trace
255         * @return a list of Point objects.
256         */
257        List<Point> traceAndThreshold(int u0, int v0, float loThr, int markColor) {
258                //IJ.log("  working point " + u + " " + v);
259                Stack<Point> pointStack = new Stack<Point>();
260                List<Point> pointList = new LinkedList<Point>();
261                pointStack.push(new Point(u0, v0));
262                while (!pointStack.isEmpty()) {
263                        Point p = pointStack.pop();
264                        int up = p.x;
265                        int vp = p.y;
266                        Ebin.putPixel(up, vp, markColor);       // mark this edge point
267                        pointList.add(p);
268                                
269                        int uL = Math.max(up - 1, 0);           // (up > 0) ? up-1 : 0;
270                        int uR = Math.min(up + 1, M - 1);       // (up < M-1) ? up+1 : M-1;
271                        int vT = Math.max(vp - 1, 0);           // (vp > 0) ? vp - 1 : 0;
272                        int vB = Math.min(vp + 1, N - 1);       // (vp < N-1) ? vp+1 : N-1;
273                        
274                        for (int u = uL; u <= uR; u++) {
275                                for (int v = vT; v <= vB; v++) {
276                                        if (Ebin.get(u, v) == 0 && Enms.getf(u, v) >= loThr) {
277                                                pointStack.push(new Point(u,v));
278                                        }
279                                }
280                        }
281                }
282                return pointList;
283        }
284        
285        private final float cosPi8 = (float) Math.cos(Math.PI/8);
286        private final float sinPi8 = (float) Math.sin(Math.PI/8);
287        
288        // returns the quantized orientation sector for vector (dx, dy)
289        int getOrientationSector(float dx, float dy) {
290                // rotate the gradient vector by PI/8
291                float dxR = cosPi8 * dx - sinPi8 * dy;
292                float dyR = sinPi8 * dx + cosPi8 * dy;  
293                // mirror vector (dxR,dyR) to [0,PI]
294                if (dyR < 0) {
295                        dxR = -dxR;
296                        dyR = -dyR;
297                }
298                // determine the octant for (dx, dy)
299                int s_theta;
300                if (dxR >= 0) { // dxR >= 0, dyR >= 0
301                        if (dxR >= dyR)
302                                s_theta = 0; // return 0;
303                        else
304                                s_theta = 1; // return 1;
305                }
306                else {  // dxR < 0, dyR >= 0
307                        if (-dxR < dyR)
308                                s_theta = 2; // return 2;
309                        else
310                                s_theta = 3; //return 3;
311                }
312                return s_theta;
313        }
314        
315        public FloatProcessor getEdgeMagnitude() {
316                return Emag;
317        }
318
319        public FloatProcessor getEdgeOrientation() {
320                FloatProcessor E_theta = new FloatProcessor(M, N);
321                for (int u = 0; u < M; u++) {
322                        for (int v = 0; v < N; v++) {
323                                double ex = Ex.getf(u, v);
324                                double ey = Ey.getf(u, v);
325                                float theta = (float) Math.atan2(ey, ex);
326                                E_theta.setf(u, v, theta);
327                        }
328                }
329                return E_theta;
330        }
331        
332        public ByteProcessor getEdgeBinary() {
333                if (Ebin == null) {
334                        detectAndTraceEdges();
335                }
336                return Ebin;
337        }
338        
339        public List<List<Point>> getEdgeTraces() {
340                if (traceList == null) {
341                        detectAndTraceEdges();
342                }
343                return traceList;
344        }
345        
346        //---------------------------------------------------------------------------
347
348        float[] makeGaussKernel1d(double sigma) {
349                // make 1D Gauss filter kernel large enough
350                int rad = (int) (3.5 * sigma);
351                int size = rad + rad + 1;
352                float[] kernel = new float[size]; // center cell = kernel[rad]
353                double sigma2 = sigma * sigma;
354                double scale = 1 / Math.sqrt(2 * Math.PI * sigma2);
355                for (int i = 0; i < size; i++) {
356                        double x = rad - i;
357                        kernel[i] = (float) (
358                        scale * Math.exp(-0.5 * (x * x) / sigma2)
359                        );
360                }
361                return kernel;
362        }
363        
364        // extract RGB channels of 'cp' as 3 float processors
365        FloatProcessor[] rgbToFloatChannels(ColorProcessor cp) {
366                int w = cp.getWidth();
367                int h = cp.getHeight();
368                FloatProcessor rp = new FloatProcessor(w, h);
369                FloatProcessor gp = new FloatProcessor(w, h);
370                FloatProcessor bp = new FloatProcessor(w, h);
371                int[] pixels = (int[]) cp.getPixels(); 
372                float[] rpix = (float[]) rp.getPixels();
373                float[] gpix = (float[]) gp.getPixels();
374                float[] bpix = (float[]) bp.getPixels();
375        for (int i=0; i<pixels.length; i++) {
376            int c = pixels[i];
377            rpix[i] = (c&0xff0000)>>16;
378                gpix[i] = (c&0xff00)>>8;
379                        bpix[i] =  c&0xff;
380        }
381        return new FloatProcessor[] {rp, gp, bp};
382        }
383        
384}