001/*
002 * Copyright (C) Photon Vision.
003 *
004 * This program is free software: you can redistribute it and/or modify
005 * it under the terms of the GNU General Public License as published by
006 * the Free Software Foundation, either version 3 of the License, or
007 * (at your option) any later version.
008 *
009 * This program is distributed in the hope that it will be useful,
010 * but WITHOUT ANY WARRANTY; without even the implied warranty of
011 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
012 * GNU General Public License for more details.
013 *
014 * You should have received a copy of the GNU General Public License
015 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
016 */
017
018package org.photonvision.vision.opencv;
019
020import java.util.Arrays;
021import java.util.Collection;
022import java.util.Comparator;
023import org.jetbrains.annotations.Nullable;
024import org.opencv.core.*;
025import org.opencv.imgproc.Imgproc;
026import org.opencv.imgproc.Moments;
027import org.photonvision.common.util.math.MathUtils;
028
029public class Contour implements Releasable {
030    public static final Comparator<Contour> SortByMomentsX =
031            Comparator.comparingDouble(
032                    (contour) -> contour.getMoments().get_m10() / contour.getMoments().get_m00());
033
034    public final MatOfPoint mat;
035
036    private Double area = Double.NaN;
037    private Double perimeter = Double.NaN;
038    private MatOfPoint2f mat2f = null;
039    private RotatedRect minAreaRect = null;
040    private Rect boundingRect = null;
041    private Moments moments = null;
042
043    private MatOfPoint2f convexHull = null;
044    private MatOfPoint2f approxPolyDp = null;
045
046    public Contour(MatOfPoint mat) {
047        this.mat = mat;
048    }
049
050    public Contour(Rect2d box) {
051        // no easy way to convert a Rect2d to Mat, diy it. Order is tl tr br bl
052        this.mat =
053                new MatOfPoint(
054                        box.tl(),
055                        new Point(box.x + box.width, box.y),
056                        box.br(),
057                        new Point(box.x, box.y + box.height));
058    }
059
060    public MatOfPoint2f getMat2f() {
061        if (mat2f == null) {
062            mat2f = new MatOfPoint2f(mat.toArray());
063            mat.convertTo(mat2f, CvType.CV_32F);
064        }
065        return mat2f;
066    }
067
068    public MatOfPoint2f getConvexHull() {
069        if (this.convexHull == null) {
070            var ints = new MatOfInt();
071            Imgproc.convexHull(mat, ints);
072            this.convexHull = Contour.convertIndexesToPoints(mat, ints);
073            ints.release();
074        }
075        return convexHull;
076    }
077
078    public MatOfPoint2f getApproxPolyDp(double epsilon, boolean closed) {
079        if (this.approxPolyDp == null) {
080            approxPolyDp = new MatOfPoint2f();
081            Imgproc.approxPolyDP(getConvexHull(), approxPolyDp, epsilon, closed);
082        }
083        return approxPolyDp;
084    }
085
086    @Nullable
087    public MatOfPoint2f getApproxPolyDp() {
088        return this.approxPolyDp;
089    }
090
091    public double getArea() {
092        if (Double.isNaN(area)) {
093            area = Imgproc.contourArea(mat);
094        }
095        return area;
096    }
097
098    public double getPerimeter() {
099        if (Double.isNaN(perimeter)) {
100            perimeter = Imgproc.arcLength(getMat2f(), true);
101        }
102        return perimeter;
103    }
104
105    public RotatedRect getMinAreaRect() {
106        if (minAreaRect == null) {
107            minAreaRect = Imgproc.minAreaRect(getMat2f());
108        }
109        return minAreaRect;
110    }
111
112    public Rect getBoundingRect() {
113        if (boundingRect == null) {
114            boundingRect = Imgproc.boundingRect(mat);
115        }
116        return boundingRect;
117    }
118
119    public Moments getMoments() {
120        if (moments == null) {
121            moments = Imgproc.moments(mat);
122        }
123        return moments;
124    }
125
126    public Point getCenterPoint() {
127        return getMinAreaRect().center;
128    }
129
130    public boolean isEmpty() {
131        return mat.empty();
132    }
133
134    public boolean isIntersecting(
135            Contour secondContour, ContourIntersectionDirection intersectionDirection) {
136        boolean isIntersecting = false;
137
138        if (intersectionDirection == ContourIntersectionDirection.None) {
139            isIntersecting = true;
140        } else {
141            try {
142                MatOfPoint2f intersectMatA = new MatOfPoint2f();
143                MatOfPoint2f intersectMatB = new MatOfPoint2f();
144
145                mat.convertTo(intersectMatA, CvType.CV_32F);
146                secondContour.mat.convertTo(intersectMatB, CvType.CV_32F);
147
148                RotatedRect a = Imgproc.fitEllipse(intersectMatA);
149                RotatedRect b = Imgproc.fitEllipse(intersectMatB);
150                double mA = MathUtils.toSlope(a.angle);
151                double mB = MathUtils.toSlope(b.angle);
152                double x0A = a.center.x;
153                double y0A = a.center.y;
154                double x0B = b.center.x;
155                double y0B = b.center.y;
156                double intersectionX = ((mA * x0A) - y0A - (mB * x0B) + y0B) / (mA - mB);
157                double intersectionY = (mA * (intersectionX - x0A)) + y0A;
158                double massX = (x0A + x0B) / 2;
159                double massY = (y0A + y0B) / 2;
160                switch (intersectionDirection) {
161                    case None -> {}
162                    case Up -> {
163                        if (intersectionY < massY) isIntersecting = true;
164                    }
165                    case Down -> {
166                        if (intersectionY > massY) isIntersecting = true;
167                    }
168                    case Left -> {
169                        if (intersectionX < massX) isIntersecting = true;
170                    }
171                    case Right -> {
172                        if (intersectionX > massX) isIntersecting = true;
173                    }
174                }
175                intersectMatA.release();
176                intersectMatB.release();
177            } catch (Exception e) {
178                // defaults to false
179            }
180        }
181
182        return isIntersecting;
183    }
184
185    // TODO: refactor to do "infinite" contours ???????
186    public static Contour groupContoursByIntersection(
187            Contour firstContour, Contour secondContour, ContourIntersectionDirection intersection) {
188        if (areIntersecting(firstContour, secondContour, intersection)) {
189            return combineContours(firstContour, secondContour);
190        } else {
191            return null;
192        }
193    }
194
195    public static boolean areIntersecting(
196            Contour firstContour,
197            Contour secondContour,
198            ContourIntersectionDirection intersectionDirection) {
199        return firstContour.isIntersecting(secondContour, intersectionDirection)
200                || secondContour.isIntersecting(firstContour, intersectionDirection);
201    }
202
203    public static Contour combineContours(Contour... contours) {
204        return combineContourList(Arrays.asList(contours));
205    }
206
207    public static Contour combineContourList(Collection<Contour> contours) {
208        var points = new MatOfPoint();
209
210        for (var contour : contours) {
211            points.push_back(contour.mat);
212        }
213
214        var finalContour = new Contour(points);
215
216        boolean contourEmpty = finalContour.isEmpty();
217        return contourEmpty ? null : finalContour;
218    }
219
220    @Override
221    public void release() {
222        if (mat != null) mat.release();
223        if (mat2f != null) mat2f.release();
224        if (convexHull != null) convexHull.release();
225        if (approxPolyDp != null) approxPolyDp.release();
226    }
227
228    public static MatOfPoint2f convertIndexesToPoints(MatOfPoint contour, MatOfInt indexes) {
229        int[] arrIndex = indexes.toArray();
230        Point[] arrContour = contour.toArray();
231        Point[] arrPoints = new Point[arrIndex.length];
232
233        for (int i = 0; i < arrIndex.length; i++) {
234            arrPoints[i] = arrContour[arrIndex[i]];
235        }
236
237        var hull = new MatOfPoint2f();
238        hull.fromArray(arrPoints);
239        return hull;
240    }
241}