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}