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}