001/**
002 * Copyright (c) 2011, The University of Southampton and the individual contributors.
003 * All rights reserved.
004 *
005 * Redistribution and use in source and binary forms, with or without modification,
006 * are permitted provided that the following conditions are met:
007 *
008 *   *  Redistributions of source code must retain the above copyright notice,
009 *      this list of conditions and the following disclaimer.
010 *
011 *   *  Redistributions in binary form must reproduce the above copyright notice,
012 *      this list of conditions and the following disclaimer in the documentation
013 *      and/or other materials provided with the distribution.
014 *
015 *   *  Neither the name of the University of Southampton nor the names of its
016 *      contributors may be used to endorse or promote products derived from this
017 *      software without specific prior written permission.
018 *
019 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
020 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
021 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
022 * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
023 * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
024 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
025 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
026 * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
027 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
028 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
029 */
030package org.openimaj.image.pixel;
031
032import gnu.trove.list.array.TFloatArrayList;
033import gnu.trove.list.array.TIntArrayList;
034
035import java.util.ArrayList;
036import java.util.Collection;
037import java.util.HashSet;
038import java.util.LinkedHashSet;
039import java.util.List;
040import java.util.Set;
041
042import org.openimaj.image.FImage;
043import org.openimaj.image.processor.connectedcomponent.ConnectedComponentProcessor;
044import org.openimaj.image.renderer.ScanRasteriser;
045import org.openimaj.image.renderer.ScanRasteriser.ScanLineListener;
046import org.openimaj.math.geometry.point.Point2d;
047import org.openimaj.math.geometry.shape.Polygon;
048import org.openimaj.math.geometry.shape.RotatedRectangle;
049import org.openimaj.math.geometry.shape.Shape;
050
051/**
052 * This class represents a connected region within an image and provides methods
053 * for accessing and manipulating that region.
054 * <p>
055 * Nothing stops an unconnected component being constructed, but it is important
056 * to realise that some methods may return unexpected results (e.g. boundary
057 * tracing). If you are only dealing with unconnected pixel sets, use the
058 * {@link PixelSet} superclass instead.
059 *
060 * @author Jonathon Hare (jsh2@ecs.soton.ac.uk)
061 * @author David Dupplaw (dpd@ecs.soton.ac.uk)
062 */
063public class ConnectedComponent extends PixelSet {
064        /**
065         * For boundary representations of {@link ConnectedComponent}s, this enum
066         * determines and specifies how the boundary is calculated; either using a
067         * 4-connected rule, or an 8-connected rule.
068         *
069         * @author Jonathon Hare (jsh2@ecs.soton.ac.uk)
070         */
071        public enum ConnectMode {
072                /** 4-connected edges in the boundary representation */
073                CONNECT_4,
074                /** 8-connected edges in the boundary representation */
075                CONNECT_8;
076
077                /**
078                 * Get the neighbouring pixels
079                 *
080                 * @param image
081                 *            the image
082                 * @param x
083                 *            the x ordinate
084                 * @param y
085                 *            the y ordinate
086                 * @param bgThreshold
087                 *            the threshold for below which pixels should be ignored
088                 * @return the neighbouring pixels
089                 */
090                public List<Pixel> getNeighbours(FImage image, int x, int y, float bgThreshold) {
091                        final List<Pixel> neighbours = new ArrayList<Pixel>(this == CONNECT_8 ? 8 : 4);
092
093                        switch (this) {
094                        case CONNECT_8:
095                                if (x > 0 && y > 0 && image.pixels[y - 1][x - 1] > bgThreshold)
096                                        neighbours.add(new Pixel(x - 1, y - 1));
097                                if (x + 1 < image.width && y > 0 && image.pixels[y - 1][x + 1] > bgThreshold)
098                                        neighbours.add(new Pixel(x + 1, y - 1));
099                                if (x > 0 && y + 1 < image.height && image.pixels[y + 1][x - 1] > bgThreshold)
100                                        neighbours.add(new Pixel(x - 1, y + 1));
101                                if (x + 1 < image.width && y + 1 < image.height && image.pixels[y + 1][x + 1] > bgThreshold)
102                                        neighbours.add(new Pixel(x + 1, y + 1));
103                                // Note : no break, so we fall through...
104                        case CONNECT_4:
105                                if (x > 0 && image.pixels[y][x - 1] > bgThreshold)
106                                        neighbours.add(new Pixel(x - 1, y));
107                                if (x + 1 < image.width && image.pixels[y][x + 1] > bgThreshold)
108                                        neighbours.add(new Pixel(x + 1, y));
109                                if (y > 0 && image.pixels[y - 1][x] > bgThreshold)
110                                        neighbours.add(new Pixel(x, y - 1));
111                                if (y + 1 < image.height && image.pixels[y + 1][x] > bgThreshold)
112                                        neighbours.add(new Pixel(x, y + 1));
113                                break;
114                        }
115
116                        return neighbours;
117                }
118        }
119
120        /**
121         * Default constructor. Has an empty implementation.
122         */
123        public ConnectedComponent() {
124                super();
125        }
126
127        /**
128         * Construct a new connected component from the given shape. Pixels are
129         * created for the connected component that lie within the shape; but these
130         * pixels will not have specific values as they are not associated to any
131         * particular image.
132         *
133         * @param shape
134         *            The shape from which to construct the connected component.
135         */
136        public ConnectedComponent(Shape shape) {
137                fromShape(shape);
138        }
139
140        /**
141         * Construct a new connected component from the given polygon. Pixels are
142         * created for the connected component that lie within the shape; but these
143         * pixels will not have specific values as they are not associated to any
144         * particular image.
145         *
146         * @param poly
147         *            The polygon from which to construct the connected component.
148         */
149        public ConnectedComponent(Polygon poly) {
150                // FIXME: this can be improved if the scan-fill algorithm
151                // could be adapted to deal with holes.
152                if (poly.getNumInnerPoly() == 1) {
153                        ScanRasteriser.scanFill(poly.points, new ScanLineListener() {
154                                @Override
155                                public void process(int x1, int x2, int y) {
156                                        for (int x = x1; x <= x2; x++)
157                                                ConnectedComponent.this.addPixel(x, y);
158                                }
159                        });
160                } else {
161                        fromShape(poly);
162                }
163        }
164
165        /**
166         * Construct a rectangular {@link ConnectedComponent}. Pixels are created
167         * for the area within the rectangle but these will not have any specific
168         * value as they are not associated to any particular image.
169         *
170         * @param x
171         *            The top-left x-coordinate of the rectangle
172         * @param y
173         *            The top-left y-coordinate of the rectangle
174         * @param w
175         *            The width of the rectangle
176         * @param h
177         *            The height of the rectangle
178         */
179        public ConnectedComponent(int x, int y, int w, int h) {
180                super(x, y, w, h);
181        }
182
183        /**
184         * Constructs a connected component from a mask image. Pixels are created
185         * for areas within the mask image that are non-zero. Note that this may
186         * result in a connected component definition that is unconnected and some
187         * methods may not return expected results.
188         *
189         * @param img
190         *            The mask image to construct a connected component from.
191         */
192        public ConnectedComponent(int[][] img) {
193                super(img);
194        }
195
196        /**
197         * Constructs a connected component from a mask image. Pixels are created
198         * for areas within the mask image that are above the given threshold. Note
199         * that this may result in a connected component definition that is
200         * unconnected and some methods may not return expected results.
201         *
202         * @param mask
203         *            The mask image to construct a connected component from.
204         * @param thresh
205         *            the threshold value.
206         */
207        public ConnectedComponent(FImage mask, float thresh) {
208                super(mask, thresh);
209        }
210
211        /**
212         * Construct a connected component from the given set of {@link Pixel}s. The
213         * pixels are shallow copied into the connected component. If the pixels do
214         * not form a connected component then some methods in this class may not
215         * return expected results.
216         *
217         * @param pixels
218         *            A {@link Set} of {@link Pixel}s.
219         */
220        public ConnectedComponent(Set<Pixel> pixels) {
221                super(pixels);
222        }
223
224        /**
225         * Estimates how many vertices are required to encode the boundary with the
226         * given smoothness and window width. Basically it determines how many
227         * strong peaks there are in the boundary, where the peak strength is
228         * determined by the two parameters. The window width determines how many
229         * boundary points are considered when iterating through the binary, while
230         * the smoothness determines how smooth the resulting boundary
231         * representation can be.
232         *
233         * @param smoothWidth
234         *            The smoothness of the resulting boundary
235         * @param windowWidth
236         *            The number of edge points to consider
237         * @return The estimated number of vertices required to encode the boundary.
238         */
239        public int estimateNumberOfVertices(int smoothWidth, int windowWidth) {
240                final TFloatArrayList distances = calculateBoundaryDistanceFromCentre();
241
242                if (smoothWidth % 2 == 0)
243                        smoothWidth++;
244                if (windowWidth % 2 == 0)
245                        windowWidth++;
246
247                final int n = distances.size();
248                final float[] kernel = new float[windowWidth];
249                final float[] response = new float[n];
250
251                for (int i = 0; i < n; i++) {
252                        float sum = 0;
253                        for (int j = 0; j < smoothWidth; j++) {
254                                int k = i + j - (smoothWidth / 2);
255
256                                if (k < 0) {
257                                        k = n + k;
258                                } else if (k >= n) {
259                                        k = k - n;
260                                }
261                                sum += distances.get(k);
262                        }
263                        distances.set(i, sum / smoothWidth);
264                }
265
266                for (int i = 0; i < windowWidth; i++)
267                        kernel[i] = -(windowWidth / 2) + i;
268
269                for (int i = 0; i < n; i++) {
270                        float sum = 0;
271                        for (int j = 0; j < windowWidth; j++) {
272                                int k = i + j - (windowWidth / 2);
273
274                                if (k < 0) {
275                                        k = n + k;
276                                } else if (k >= n) {
277                                        k = k - n;
278                                }
279                                sum += kernel[j] * distances.get(k);
280                        }
281                        response[i] = sum;
282                }
283
284                int peaks = 0;
285                for (int i = 1; i < n; i++) {
286                        if (response[i - 1] >= 0 && response[i] < 0)
287                                peaks++;
288                }
289                if (response[n - 1] >= 0 && response[0] < 0)
290                        peaks++;
291
292                return peaks;
293        }
294
295        /**
296         *
297         * @param P0
298         * @param P1
299         * @param P2
300         * @return
301         */
302        protected int isLeft(Pixel P0, Pixel P1, Pixel P2) {
303                return (P1.x - P0.x) * (P2.y - P0.y) - (P2.x - P0.x) * (P1.y - P0.y);
304        }
305
306        /**
307         * Calculates the convex hull polygon for this connected component using
308         * Andrew's montone algorithm.
309         *
310         * @return The polygon defining the convex hull shape for this component.
311         */
312        public Polygon calculateConvexHull() {
313                return calculateConvexHull_AndrewsMontone();
314        }
315
316        /**
317         * Calculate the ratio of the area of the given connected component to the
318         * area of the connected component. This does not consider whether the areas
319         * overlap.
320         *
321         * @param ch
322         *            The connected component to test.
323         * @return The area ratio of the given connected component to this connected
324         *         component.
325         */
326        public double calculateAreaRatio(ConnectedComponent ch) {
327                return (double) calculateArea() / (double) ch.calculateArea();
328        }
329
330        /**
331         * Calculate the ratio of the area of the given polygon to the area of this
332         * connected component. This does not consider whether the areas overlap.
333         *
334         * @param ch
335         *            The polygon to test again.
336         * @return The area ratio of the given polygon to this connected component.
337         */
338        public double calculateAreaRatio(Polygon ch) {
339                return calculateAreaRatio(new ConnectedComponent(ch));
340        }
341
342        /**
343         * Calculate the ratio of the area of this component's convex hull to the
344         * actual area of this connected component. This gives an idea of how well
345         * the calculated convex hull fits the component. The value returned is a
346         * percentage (0-1).
347         *
348         * @return The area ratio of this component's convex hull its area.
349         */
350        public double calculatePercentageConvexHullFit() {
351                return calculateAreaRatio(calculateConvexHull());
352        }
353
354        /**
355         * Calculate convex hull using Melkman's algorithm. Based on
356         * http://softsurfer.com/Archive/algorithm_0203/algorithm_0203.htm
357         * <p>
358         * Copyright 2001, softSurfer (www.softsurfer.com) This code may be freely
359         * used and modified for any purpose providing that this copyright notice is
360         * included with it. SoftSurfer makes no warranty for this code, and cannot
361         * be held liable for any real or imagined damage resulting from its use.
362         * Users of this code must verify correctness for their application.
363         *
364         * @param V
365         *            List of pixels containing within the region
366         * @return A polygon defining the shape of the convex hull
367         */
368        protected Polygon calculateConvexHull_Melkman(List<Pixel> V) {
369                // initialize a deque D[] from bottom to top so that the
370                // 1st three vertices of V[] are a counterclockwise triangle
371                final int n = V.size();
372                final Pixel[] D = new Pixel[2 * n + 1];
373                int bot = n - 2, top = bot + 3; // initial bottom and top deque indices
374                D[bot] = D[top] = V.get(2); // 3rd vertex is at both bot and top
375                if (isLeft(V.get(0), V.get(1), V.get(2)) > 0) {
376                        D[bot + 1] = V.get(0);
377                        D[bot + 2] = V.get(1); // ccw vertices are: 2,0,1,2
378                } else {
379                        D[bot + 1] = V.get(1);
380                        D[bot + 2] = V.get(0); // ccw vertices are: 2,1,0,2
381                }
382
383                // compute the hull on the deque D[]
384                for (int i = 3; i < n; i++) { // process the rest of vertices
385                        // test if next vertex is inside the deque hull
386                        if ((isLeft(D[bot], D[bot + 1], V.get(i)) > 0) &&
387                                        (isLeft(D[top - 1], D[top], V.get(i)) > 0))
388                                continue; // skip an interior vertex
389
390                        // incrementally add an exterior vertex to the deque hull
391                        // get the rightmost tangent at the deque bot
392                        while (isLeft(D[bot], D[bot + 1], V.get(i)) <= 0)
393                                ++bot; // remove bot of deque
394                        D[--bot] = V.get(i); // insert V[i] at bot of deque
395
396                        // get the leftmost tangent at the deque top
397                        while (isLeft(D[top - 1], D[top], V.get(i)) <= 0)
398                                --top; // pop top of deque
399                        D[++top] = V.get(i); // push V[i] onto top of deque
400                }
401
402                // transcribe deque D[] to the output hull array H[]
403                final Polygon H = new Polygon();
404                final List<Point2d> vertices = H.getVertices();
405                for (int h = 0; h <= (top - bot); h++)
406                        vertices.add(D[bot + h]);
407
408                return H;
409        }
410
411        /**
412         * Calculate the convex hull using Andrew's monotone chain 2D convex hull
413         * algorithm.
414         *
415         * @return A polygon defining the shape of the convex hull.
416         */
417        protected Polygon calculateConvexHull_AndrewsMontone() {
418                if (this.calculateArea() == 1) {
419                        return new Polygon(this.pixels.iterator().next());
420                }
421
422                final List<Pixel> P = new ArrayList<Pixel>();
423
424                // sort
425                int minx = Integer.MAX_VALUE, maxx = Integer.MIN_VALUE, miny = Integer.MAX_VALUE, maxy = Integer.MIN_VALUE;
426
427                for (final Pixel p : pixels) {
428                        if (p.x < minx)
429                                minx = p.x;
430                        if (p.x > maxx)
431                                maxx = p.x;
432                        if (p.y < miny)
433                                miny = p.y;
434                        if (p.y > maxy)
435                                maxy = p.y;
436                }
437
438                for (int x = minx; x <= maxx; x++) {
439                        for (int y = miny; y <= maxy; y++) {
440                                final Pixel p = new Pixel(x, y);
441                                if (pixels.contains(p))
442                                        P.add(p);
443                        }
444                }
445
446                // the output array H[] will be used as the stack
447                int bot = 0, top = (-1); // indices for bottom and top of the stack
448                int i; // array scan index
449                final int n = P.size();
450
451                final Polygon poly = new Polygon();
452                final Pixel[] H = new Pixel[P.size()];
453
454                // Get the indices of points with min x-coord and min|max y-coord
455                final int minmin = 0;
456                int minmax;
457                final float xmin = P.get(0).x;
458                for (i = 1; i < n; i++)
459                        if (P.get(i).x != xmin)
460                                break;
461                minmax = i - 1;
462                if (minmax == n - 1) { // degenerate case: all x-coords == xmin
463                        H[++top] = P.get(minmin);
464                        if (P.get(minmax).y != P.get(minmin).y) // a nontrivial segment
465                                H[++top] = P.get(minmax);
466                        H[++top] = P.get(minmin); // add polygon endpoint
467
468                        for (int k = 0; k < top + 1; k++)
469                                poly.getVertices().add(H[k]);
470                        return poly;
471                }
472
473                // Get the indices of points with max x-coord and min|max y-coord
474                int maxmin;
475                final int maxmax = n - 1;
476                final float xmax = P.get(n - 1).x;
477                for (i = n - 2; i >= 0; i--)
478                        if (P.get(i).x != xmax)
479                                break;
480                maxmin = i + 1;
481
482                // Compute the lower hull on the stack H
483                H[++top] = P.get(minmin); // push minmin point onto stack
484                i = minmax;
485                while (++i <= maxmin) {
486                        // the lower line joins P[minmin] with P[maxmin]
487                        if (isLeft(P.get(minmin), P.get(maxmin), P.get(i)) >= 0 && i < maxmin)
488                                continue; // ignore P[i] above or on the lower line
489
490                        while (top > 0) // there are at least 2 points on the stack
491                        {
492                                // test if P[i] is left of the line at the stack top
493                                if (isLeft(H[top - 1], H[top], P.get(i)) > 0)
494                                        break; // P[i] is a new hull vertex
495                                else
496                                        top--; // pop top point off stack
497                        }
498                        H[++top] = P.get(i); // push P[i] onto stack
499                }
500
501                // Next, compute the upper hull on the stack H above the bottom hull
502                if (maxmax != maxmin) // if distinct xmax points
503                        H[++top] = P.get(maxmax); // push maxmax point onto stack
504                bot = top; // the bottom point of the upper hull stack
505                i = maxmin;
506                while (--i >= minmax) {
507                        // the upper line joins P[maxmax] with P[minmax]
508                        if (isLeft(P.get(maxmax), P.get(minmax), P.get(i)) >= 0 && i > minmax)
509                                continue; // ignore P[i] below or on the upper line
510
511                        while (top > bot) // at least 2 points on the upper stack
512                        {
513                                // test if P[i] is left of the line at the stack top
514                                if (isLeft(H[top - 1], H[top], P.get(i)) > 0)
515                                        break; // P[i] is a new hull vertex
516                                else
517                                        top--; // pop top point off stack
518                        }
519                        H[++top] = P.get(i); // push P[i] onto stack
520                }
521                if (minmax != minmin)
522                        H[++top] = P.get(minmin); // push joining endpoint onto stack
523
524                for (int k = 0; k < top + 1; k++)
525                        poly.getVertices().add(H[k]);
526                return poly;
527        }
528
529        /**
530         * Returns the next edge pixel when tracing a boundary in a 4-connected
531         * system.
532         *
533         * @param current
534         *            The current pixel
535         * @param lastdir
536         *            The last direction traversed
537         * @return The next pixel in the edge
538         */
539        protected Pixel nextEdgePixelACW4(Pixel current, int lastdir) {
540                return nextEdgePixelACW4(current, lastdir, null);
541        }
542
543        /**
544         * Returns the next edge pixel when tracing a boundary in a 4-connected
545         * system.
546         *
547         * @param current
548         *            The current pixel
549         * @param lastdir
550         *            The last direction traversed
551         * @param outer
552         *            A list to fill with the outer boundary
553         * @return The next pixel in the edge
554         */
555        protected Pixel nextEdgePixelACW4(Pixel current, int lastdir, List<Pixel> outer) {
556                final int startdir = (lastdir + 3) % 4;
557
558                final Pixel target[] = {
559                                new Pixel(current.x + 1, current.y), // dir 0
560                                new Pixel(current.x, current.y - 1), // dir 1
561                                new Pixel(current.x - 1, current.y), // dir 2
562                                new Pixel(current.x, current.y + 1) // dir 3
563                };
564
565                for (int i = 0; i < 4; i++) {
566                        int dir = startdir + i;
567                        if (dir >= 4)
568                                dir -= 4;
569
570                        if (pixels.contains(target[dir])) {
571                                return target[dir];
572                        } else if (outer != null)
573                                outer.add(target[dir]);
574                }
575
576                return current;
577        }
578
579        /**
580         * Returns the next edge pixel when tracing a boundary in an 8-connected
581         * system.
582         *
583         * @param current
584         *            The current pixel
585         * @param lastdir
586         *            The last direction traversed
587         * @return The next pixel in the edge
588         */
589        protected Pixel nextEdgePixelACW8(Pixel current, int lastdir) {
590                final int startdir = (lastdir + 7 - (lastdir % 2)) % 8;
591
592                final Pixel target[] = {
593                                new Pixel(current.x + 1, current.y), // dir 0
594                                new Pixel(current.x + 1, current.y - 1), // dir 1
595                                new Pixel(current.x, current.y - 1), // dir 2
596                                new Pixel(current.x - 1, current.y - 1), // dir 3
597                                new Pixel(current.x - 1, current.y), // dir 4
598                                new Pixel(current.x - 1, current.y + 1), // dir 5
599                                new Pixel(current.x, current.y + 1), // dir 6
600                                new Pixel(current.x + 1, current.y + 1) // dir 7
601                };
602
603                for (int i = 0; i < 8; i++) {
604                        int dir = startdir + i;
605                        if (dir >= 8)
606                                dir -= 8;
607
608                        if (pixels.contains(target[dir]))
609                                return target[dir];
610                }
611
612                return current;
613        }
614
615        /**
616         * For the two pixels, determines the 4-connected chain code that will move
617         * from the first pixel to the next. If the pixels are not adjacent the
618         * method returns -1.
619         *
620         * @param current
621         *            The current pixel
622         * @param next
623         *            The next pixel
624         * @return The Freeman 4-connected chain code
625         */
626        protected int code4(Pixel current, Pixel next) {
627                if (current.x - 1 == next.x)
628                        return 2;
629                if (current.y + 1 == next.y)
630                        return 3;
631                if (current.x + 1 == next.x)
632                        return 0;
633                if (current.y - 1 == next.y)
634                        return 1;
635
636                return -1;
637        }
638
639        /**
640         * For the two pixels, determines the 8-connected chain code that will move
641         * from the first pixel to the next. If the pixels are not adjacent or
642         * diagonal then the method returns -1.
643         *
644         * @param current
645         *            The current pixel
646         * @param next
647         *            The next pixel
648         * @return The Freeman 8-connected chain code
649         */
650        protected int code8(Pixel current, Pixel next) {
651                if (current.x + 1 == next.x && current.y == next.y)
652                        return 0;
653                if (current.x + 1 == next.x && current.y - 1 == next.y)
654                        return 1;
655                if (current.x == next.x && current.y - 1 == next.y)
656                        return 2;
657                if (current.x - 1 == next.x && current.y - 1 == next.y)
658                        return 3;
659                if (current.x - 1 == next.x && current.y == next.y)
660                        return 4;
661                if (current.x - 1 == next.x && current.y + 1 == next.y)
662                        return 5;
663                if (current.x == next.x && current.y + 1 == next.y)
664                        return 6;
665                if (current.x + 1 == next.x && current.y + 1 == next.y)
666                        return 7;
667
668                return -1;
669        }
670
671        /**
672         * Converts this connected component into a {@link Polygon} representation
673         * by performing a 4-connected boundary trace and converting the resulting
674         * pixels into vertices.
675         *
676         * @return A {@link Polygon} representing the inner boundary of the
677         *         component.
678         */
679        public Polygon toPolygon() {
680                final Polygon poly = new Polygon();
681
682                for (final Pixel p : getInnerBoundary(ConnectMode.CONNECT_4))
683                        poly.getVertices().add(p);
684
685                return poly;
686        }
687
688        /**
689         * Returns an ordered list of pixels that are on the inner boundary of the
690         * shape. That means that the boundary points are all within the region. The
691         * list is ordered such that adjacent boundary pixels are adjacent in the
692         * list. The first pixel in the list should be the same as
693         * {@link #topLeftMostPixel()}.
694         *
695         * @param mode
696         *            The {@link ConnectMode} to use.
697         * @return An ordered list of pixels defining the inner boundary
698         */
699        public List<Pixel> getInnerBoundary(ConnectMode mode) {
700                final List<Pixel> pset = new ArrayList<Pixel>();
701
702                final Pixel start = topLeftMostPixel();
703                Pixel current = start;
704                Pixel next;
705                int dir;
706
707                switch (mode) {
708                case CONNECT_4:
709                        dir = 3;
710                        while (true) {
711                                next = nextEdgePixelACW4(current, dir);
712                                if (pset.size() >= 2 && next.equals(pset.get(1)) && current.equals(start)) {
713                                        break;
714                                }
715
716                                dir = code4(current, next);
717                                pset.add(current);
718                                current = next;
719                        }
720                        break;
721                case CONNECT_8:
722                        dir = 7;
723                        while (true) {
724                                next = nextEdgePixelACW8(current, dir);
725
726                                if (pset.size() >= 2 && next.equals(pset.get(1)) && current.equals(start)) {
727                                        break;
728                                }
729
730                                dir = code8(current, next);
731                                pset.add(current);
732                                current = next;
733                        }
734                        break;
735                }
736
737                return pset;
738        }
739
740        /**
741         * Returns an ordered list of pixels that are on the outer boundary of the
742         * shape. That means that the boundary points are all outside of the region.
743         * The list is ordered such that adjacent boundary pixels are adjacent in
744         * the list.
745         *
746         * @return An ordered list of pixels defining the outer boundary
747         */
748        public List<Pixel> getOuterBoundary() {
749                final List<Pixel> pset = new ArrayList<Pixel>();
750                List<Pixel> outer = new ArrayList<Pixel>();
751
752                final Pixel start = topLeftMostPixel();
753                Pixel current = start;
754                Pixel next;
755                int dir = 3;
756
757                while (true) {
758                        next = nextEdgePixelACW4(current, dir, outer);
759                        if (pset.size() >= 2 && next.equals(pset.get(1)) && current.equals(start)) {
760                                break;
761                        }
762                        if (this.pixels.size() == 1)
763                                break;
764
765                        dir = code4(current, next);
766                        pset.add(current);
767                        current = next;
768                }
769
770                if (outer.size() > 4) {
771                        for (int i = 3; i > 0; i--) {
772                                boolean found = true;
773                                for (int j = 0; j < i; j++) {
774                                        if (!outer.get(j).equals(outer.get(outer.size() - i + j))) {
775                                                found = false;
776                                                break;
777                                        }
778                                }
779
780                                if (found) {
781                                        outer = outer.subList(0, outer.size() - i);
782                                        break;
783                                }
784                        }
785                }
786
787                return outer;
788        }
789
790        /**
791         * Calculates the Freeman chaincode for this connected component. The
792         * chaincode is returned as a list of direction codes defining the path of
793         * the boundary.
794         * <p>
795         * The Freeman chaincode is a means for encoding the paths between nodes on
796         * the boundary of a shape, thereby reducing the encoding of a shape to a
797         * single start coordinate and a list of direction codes. The Freeman
798         * direction codes are 0-4 for 4-connected boundaries and 0-7 for
799         * 8-connected boundaries.
800         *
801         * @param mode
802         *            4 or 8 connectivity
803         * @return the chain code
804         */
805        public TIntArrayList freemanChainCode(ConnectMode mode) {
806                final TIntArrayList code = new TIntArrayList();
807
808                final Pixel start = topLeftMostPixel();
809                Pixel current = start;
810                Pixel next;
811                int dir;
812
813                switch (mode) {
814                case CONNECT_8:
815                        dir = 7;
816                        while (!(next = nextEdgePixelACW8(current, dir)).equals(start)) {
817                                dir = code8(current, next);
818                                code.add(dir);
819                                current = next;
820                        }
821                        code.add(code8(current, next));
822                        break;
823                case CONNECT_4:
824                        dir = 3;
825                        while (!(next = nextEdgePixelACW4(current, dir)).equals(start)) {
826                                dir = code4(current, next);
827                                code.add(dir);
828                                current = next;
829                        }
830                        code.add(code4(current, next));
831                        break;
832                }
833
834                return code;
835        }
836
837        /**
838         * Process the given set of connected components with the given
839         * {@link ConnectedComponentProcessor}.
840         *
841         * @param components
842         *            The components to process
843         * @param p
844         *            The process to process the components with
845         */
846        public static void process(Collection<ConnectedComponent> components, ConnectedComponentProcessor p) {
847                for (final ConnectedComponent c : components)
848                        c.processInplace(p);
849        }
850
851        /**
852         * Process this connected component with the given
853         * {@link ConnectedComponentProcessor} and returns a new component
854         * containing the result.
855         *
856         * @param p
857         *            The processor to process this component with
858         * @return A new component containing the result.
859         */
860        public ConnectedComponent process(ConnectedComponentProcessor p) {
861                final ConnectedComponent tmp = clone();
862                p.process(tmp);
863                return tmp;
864        }
865
866        /**
867         * Process a connected component with the given
868         * {@link ConnectedComponentProcessor}. Side-affects this component.
869         *
870         * @param p
871         *            The processor to process this component with
872         * @return A reference to this connected component.
873         */
874        public ConnectedComponent processInplace(ConnectedComponentProcessor p) {
875                p.process(this);
876                return this;
877        }
878
879        /**
880         * Performs a flood fill on the given image starting at the given pixel. The
881         * result of the flood fill is returned as a {@link ConnectedComponent}.
882         *
883         * @param image
884         *            The image on which to perform a flood fill
885         * @param start
886         *            The start pixel to begin the flood
887         * @return A ConnectedComponent containing the resulting region.
888         */
889        public static ConnectedComponent floodFill(FImage image, Pixel start) {
890                final ConnectedComponent cc = new ConnectedComponent();
891                final float val = image.pixels[start.y][start.x];
892                final int[][] output = new int[image.height][image.width];
893
894                // Flood-fill (node, target-color, replacement-color):
895                // 1. Set Q to the empty queue.
896                // Queue<Pixel> queue = new LinkedList<Pixel>();
897                final LinkedHashSet<Pixel> queue = new LinkedHashSet<Pixel>();
898
899                // 2. If the color of node is not equal to target-color, return.
900                if (image.pixels[start.y][start.x] > val)
901                        return cc;
902
903                // 3. Add node to Q.
904                queue.add(start);
905
906                // 4. For each element n of Q:
907                while (queue.size() > 0) {
908                        // Pixel n = queue.poll();
909                        final Pixel n = queue.iterator().next();
910                        queue.remove(n);
911
912                        // 5. If the color of n is equal to target-color:
913                        if (image.pixels[n.y][n.x] <= val && output[n.y][n.x] != 1) {
914                                // 6. Set w and e equal to n.
915                                int e = n.x, w = n.x;
916                                // 7. Move w to the west until the color of the node to the west
917                                // of w no longer matches target-color.
918                                while (w > 0 && image.pixels[n.y][w - 1] <= val)
919                                        w--;
920
921                                // 8. Move e to the east until the color of the node to the east
922                                // of e no longer matches target-color.
923                                while (e < image.width - 1 && image.pixels[n.y][e + 1] <= val)
924                                        e++;
925
926                                // 9. Set the color of nodes between w and e to
927                                // replacement-color.
928                                for (int i = w; i <= e; i++) {
929                                        output[n.y][i] = 1;
930                                        cc.addPixel(i, n.y);
931
932                                        // 10. For each node n between w and e:
933                                        final int north = n.y - 1;
934                                        final int south = n.y + 1;
935                                        // 11. If the color of the node to the north of n is
936                                        // target-color, add that node to Q.
937                                        if (north >= 0 && image.pixels[north][i] <= val && output[north][i] != 1)
938                                                queue.add(new Pixel(i, north));
939                                        // If the color of the node to the south of n is
940                                        // target-color, add that node to Q.
941                                        if (south < image.height && image.pixels[south][i] <= val && output[south][i] != 1)
942                                                queue.add(new Pixel(i, south));
943                                }
944                                // 12. Continue looping until Q is exhausted.
945                        }
946                }
947                // 13. Return.
948                return cc;
949        }
950
951        /**
952         * {@inheritDoc}
953         *
954         * Performs a deep copy on the connected component; that is, all pixels are
955         * also cloned.
956         */
957        @Override
958        public ConnectedComponent clone() {
959                ConnectedComponent tmp;
960                try {
961                        tmp = (ConnectedComponent) super.clone();
962                        tmp.pixels = new HashSet<Pixel>();
963
964                        for (final Pixel p : pixels)
965                                tmp.pixels.add(p.clone());
966
967                        return tmp;
968                } catch (final CloneNotSupportedException e) {
969                        throw new RuntimeException(e);
970                }
971        }
972
973        /**
974         * Calculates the distance from the centroid of every pixel on the
975         * 8-connected boundary of this component. Returns a {@link TFloatArrayList}
976         * that contains the list of distances (in order of the boundary).
977         *
978         * @return A list ({@link TFloatArrayList}) of distances of boundary points
979         *         to the centroid.
980         */
981        public TFloatArrayList calculateBoundaryDistanceFromCentre() {
982                final TFloatArrayList distances = new TFloatArrayList();
983                final List<Pixel> bound = getInnerBoundary(ConnectMode.CONNECT_8);
984                final double[] centroid = calculateCentroid();
985
986                for (final Pixel p : bound) {
987                        final float dist = (float) Math.sqrt(((centroid[0] - p.x) * ((centroid[0] - p.x))) +
988                                        ((centroid[1] - p.y) * ((centroid[1] - p.y))));
989                        distances.add(dist);
990                }
991
992                return distances;
993        }
994
995        @Override
996        public String asciiHeader() {
997                return "ConnectedComponent";
998        }
999
1000        @Override
1001        public byte[] binaryHeader() {
1002                return "CC".getBytes();
1003        }
1004
1005        /**
1006         * Compute the aspect ratio of the oriented bounding box.
1007         *
1008         * @return the aspect ratio of the oriented bounding box.
1009         */
1010        public double calculateOrientatedBoundingBoxAspectRatio() {
1011                final RotatedRectangle r = toPolygon().minimumBoundingRectangle();
1012
1013                return r.height / r.width;
1014        }
1015
1016        /**
1017         * Calculates the polygon that defines the minimum bounding box that best
1018         * fits the connected component, at whatever angle that may be.
1019         *
1020         * @return A {@link RotatedRectangle} that defines the minimum bounding box.
1021         */
1022        public RotatedRectangle calculateOrientatedBoundingBox() {
1023                return this.toPolygon().minimumBoundingRectangle();
1024        }
1025}