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.pipe.impl; 019 020import java.util.ArrayList; 021import java.util.List; 022import org.apache.commons.lang3.tuple.Pair; 023import org.opencv.core.Mat; 024import org.opencv.core.Point; 025import org.opencv.imgproc.Imgproc; 026import org.opencv.imgproc.Moments; 027import org.photonvision.vision.opencv.CVShape; 028import org.photonvision.vision.opencv.Contour; 029import org.photonvision.vision.pipe.CVPipe; 030 031public class FindCirclesPipe 032 extends CVPipe<Pair<Mat, List<Contour>>, List<CVShape>, FindCirclesPipe.FindCirclePipeParams> { 033 // Output vector of found circles. Each vector is encoded as 3 or 4 element floating-point vector 034 // (x,y,radius) or (x,y,radius,votes) . 035 private final Mat circles = new Mat(); 036 037 /** 038 * Runs the process for the pipe. The reason we need a separate pipe for circles is because if we 039 * were to use the FindShapes pipe, we would have to assume that any shape more than 10-20+ sides 040 * is a circle. Only issue with such approximation is that the user would no longer be able to 041 * track shapes with 10-20+ sides. And hence, in order to overcome this edge case, we can use 042 * HoughCircles which is more flexible and accurate for finding circles. 043 * 044 * @param in Input for pipe processing. 8-bit, single-channel, grayscale input image. 045 * @return Result of processing. 046 */ 047 @Override 048 protected List<CVShape> process(Pair<Mat, List<Contour>> in) { 049 circles.release(); 050 List<CVShape> output = new ArrayList<>(); 051 052 var diag = params.diagonalLengthPx; 053 var minRadius = (int) (params.minRadius * diag / 100.0); 054 var maxRadius = (int) (params.maxRadius * diag / 100.0); 055 056 Imgproc.HoughCircles( 057 in.getLeft(), 058 circles, 059 // Detection method, see #HoughModes. The available methods are #HOUGH_GRADIENT and 060 // #HOUGH_GRADIENT_ALT. 061 Imgproc.HOUGH_GRADIENT, 062 /*Inverse ratio of the accumulator resolution to the image resolution. 063 For example, if dp=1 , the accumulator has the same resolution as the input image. 064 If dp=2 , the accumulator has half as big width and height. For #HOUGH_GRADIENT_ALT the recommended value is dp=1.5, 065 unless some small very circles need to be detected. 066 */ 067 1.0, 068 params.minDist, 069 params.maxCannyThresh, 070 Math.max(1.0, params.accuracy), 071 minRadius, 072 maxRadius); 073 // Great, we now found the center point of the circle, and it's radius, but we have no idea what 074 // contour it corresponds to 075 // Each contour can only match to one circle, so we keep a list of unmatched contours around and 076 // only match against them 077 // This does mean that contours closer than allowableThreshold aren't matched to anything if 078 // there's a 'better' option 079 var unmatchedContours = in.getRight(); 080 for (int x = 0; x < circles.cols(); x++) { 081 // Grab the current circle we are looking at 082 double[] c = circles.get(0, x); 083 // Find the center points of that circle 084 double x_center = c[0]; 085 double y_center = c[1]; 086 087 for (Contour contour : unmatchedContours) { 088 // Grab the moments of the current contour 089 Moments mu = contour.getMoments(); 090 // Determine if the contour is within the threshold of the detected circle 091 // NOTE: This means that the centroid of the contour must be within the "allowable 092 // threshold" 093 // of the center of the circle 094 if (Math.abs(x_center - (mu.m10 / mu.m00)) <= params.allowableThreshold 095 && Math.abs(y_center - (mu.m01 / mu.m00)) <= params.allowableThreshold) { 096 // If it is, then add it to the output array 097 output.add(new CVShape(contour, new Point(c[0], c[1]), c[2])); 098 unmatchedContours.remove(contour); 099 break; 100 } 101 } 102 } 103 104 // Release everything we don't use 105 for (var c : unmatchedContours) c.release(); 106 107 return output; 108 } 109 110 public static class FindCirclePipeParams { 111 private final int allowableThreshold; 112 private final int minRadius; 113 private final int maxRadius; 114 private final int minDist; 115 private final int maxCannyThresh; 116 private final int accuracy; 117 private final double diagonalLengthPx; 118 119 /* 120 * @params minDist - Minimum distance between the centers of the detected circles. 121 * If the parameter is too small, multiple neighbor circles may be falsely detected in addition to a true one. If it is too large, some circles may be missed. 122 * 123 * @param maxCannyThresh -First method-specific parameter. In case of #HOUGH_GRADIENT and #HOUGH_GRADIENT_ALT, it is the higher threshold of the two passed to the Canny edge detector (the lower one is twice smaller). 124 * Note that #HOUGH_GRADIENT_ALT uses #Scharr algorithm to compute image derivatives, so the threshold value should normally be higher, such as 300 or normally exposed and contrasty images. 125 * 126 * 127 * @param allowableThreshold - When finding the corresponding contour, this is used to see how close a center should be to a contour for it to be considered THAT contour. 128 * Should be increased with lower resolutions and decreased with higher resolution 129 * */ 130 public FindCirclePipeParams( 131 int allowableThreshold, 132 int minRadius, 133 int minDist, 134 int maxRadius, 135 int maxCannyThresh, 136 int accuracy, 137 double diagonalLengthPx) { 138 this.allowableThreshold = allowableThreshold; 139 this.minRadius = minRadius; 140 this.maxRadius = maxRadius; 141 this.minDist = minDist; 142 this.maxCannyThresh = maxCannyThresh; 143 this.accuracy = accuracy; 144 this.diagonalLengthPx = diagonalLengthPx; 145 } 146 } 147}