org.lasarobotics.vision.detection.PrimitiveDetection.java Source code

Java tutorial

Introduction

Here is the source code for org.lasarobotics.vision.detection.PrimitiveDetection.java

Source

/*
 * Copyright (c) 2016 Arthur Pachachura, LASA Robotics, and contributors
 * MIT licensed
 *
 * Thank you to Brendan Hollaway from FTC team Venom for comment updates
 * to locateRectangles().
 */

package org.lasarobotics.vision.detection;

import org.lasarobotics.vision.detection.objects.Contour;
import org.lasarobotics.vision.detection.objects.Ellipse;
import org.lasarobotics.vision.detection.objects.Rectangle;
import org.lasarobotics.vision.image.Filter;
import org.lasarobotics.vision.util.MathUtil;
import org.opencv.core.Mat;
import org.opencv.core.MatOfPoint;
import org.opencv.core.MatOfPoint2f;
import org.opencv.imgproc.Imgproc;

import java.util.ArrayList;
import java.util.List;

/**
 * Implements primitive (ellipse, polygon) detection based on a custom, highly-robust (size and position invariant) version of the Hough transform
 */
public class PrimitiveDetection {

    private static final int THRESHOLD_CANNY = 75;
    private static final int APERTURE_CANNY = 3;
    private static final double MAX_COSINE_VALUE = 0.5;
    private static final double EPLISON_APPROX_TOLERANCE_FACTOR = 0.02;

    /**
     * Locate ellipses within an image
     *
     * @param grayImage Grayscale image
     * @return Ellipse locations
     */
    public static EllipseLocationResult locateEllipses(Mat grayImage) {
        Mat gray = grayImage.clone();

        Filter.downsample(gray, 2);
        Filter.upsample(gray, 2);

        Imgproc.Canny(gray, gray, 5, 75, 3, true);
        Filter.dilate(gray, 2);

        Mat cacheHierarchy = new Mat();

        List<MatOfPoint> contoursTemp = new ArrayList<>();
        //Find contours - the parameters here are very important to compression and retention
        Imgproc.findContours(gray, contoursTemp, cacheHierarchy, Imgproc.CV_RETR_TREE,
                Imgproc.CHAIN_APPROX_TC89_KCOS);

        //List contours
        List<Contour> contours = new ArrayList<>();
        for (MatOfPoint co : contoursTemp) {
            contours.add(new Contour(co));
        }

        //Find ellipses by finding fit
        List<Ellipse> ellipses = new ArrayList<>();
        for (MatOfPoint co : contoursTemp) {
            contours.add(new Contour(co));
            //Contour must have at least 6 points for fitEllipse
            if (co.toArray().length < 6)
                continue;
            //Copy MatOfPoint to MatOfPoint2f
            MatOfPoint2f matOfPoint2f = new MatOfPoint2f(co.toArray());
            //Fit an ellipse to the current contour
            Ellipse ellipse = new Ellipse(Imgproc.fitEllipse(matOfPoint2f));

            //Draw ellipse
            ellipses.add(ellipse);
        }

        return new EllipseLocationResult(contours, ellipses);
    }

    //TODO convert this to locatePolygons() with n sides
    //TODO see http://opencv-code.com/tutorials/detecting-simple-shapes-in-an-image/

    /**
     * Locate rectangles in an image
     *
     * @param grayImage Grayscale image
     * @return Rectangle locations
     */
    public RectangleLocationResult locateRectangles(Mat grayImage) {
        Mat gray = grayImage.clone();

        //Filter out some noise by halving then doubling size
        Filter.downsample(gray, 2);
        Filter.upsample(gray, 2);

        //Mat is short for Matrix, and here is used to store an image.
        //it is n-dimensional, but as an image, is two-dimensional
        Mat cacheHierarchy = new Mat();
        Mat grayTemp = new Mat();
        List<Rectangle> rectangles = new ArrayList<>();
        List<Contour> contours = new ArrayList<>();

        //This finds the edges using a Canny Edge Detector
        //It is sent the grayscale Image, a temp Mat, the lower detection threshold for an edge,
        //the higher detection threshold, the Aperture (blurring) of the image - higher is better
        //for long, smooth edges, and whether a more accurate version (but time-expensive) version
        //should be used (true = more accurate)
        //Note: the edges are stored in "grayTemp", which is an image where everything
        //is black except for gray-scale lines delineating the edges.
        Imgproc.Canny(gray, grayTemp, 0, THRESHOLD_CANNY, APERTURE_CANNY, true);
        //make the white lines twice as big, while leaving the image size constant
        Filter.dilate(gray, 2);

        List<MatOfPoint> contoursTemp = new ArrayList<>();
        //Find contours - the parameters here are very important to compression and retention
        //grayTemp is the image from which the contours are found,
        //contoursTemp is where the resultant contours are stored (note: color is not retained),
        //cacheHierarchy is the parent-child relationship between the contours (e.g. a contour
        //inside of another is its child),
        //Imgproc.CV_RETR_LIST disables the hierarchical relationships being returned,
        //Imgproc.CHAIN_APPROX_SIMPLE means that the contour is compressed from a massive chain of
        //paired coordinates to just the endpoints of each segment (e.g. an up-right rectangular
        //contour is encoded with 4 points.)
        Imgproc.findContours(grayTemp, contoursTemp, cacheHierarchy, Imgproc.CV_RETR_LIST,
                Imgproc.CHAIN_APPROX_SIMPLE);
        //MatOfPoint2f means that is a MatofPoint (Matrix of Points) represented by floats instead of ints
        MatOfPoint2f approx = new MatOfPoint2f();
        //For each contour, test whether the contour is a rectangle
        //List<Contour> contours = new ArrayList<>()
        for (MatOfPoint co : contoursTemp) {
            //converting the MatOfPoint to MatOfPoint2f
            MatOfPoint2f matOfPoint2f = new MatOfPoint2f(co.toArray());
            //converting the matrix to a Contour
            Contour c = new Contour(co);

            //Attempt to fit the contour to the best polygon
            //input: matOfPoint2f, which is the contour found earlier
            //output: approx, which is the MatOfPoint2f that holds the new polygon that has less vertices
            //basically, it smooths out the edges using the third parameter as its approximation accuracy
            //final parameter determines whether the new approximation must be closed (true=closed)
            Imgproc.approxPolyDP(matOfPoint2f, approx, c.arcLength(true) * EPLISON_APPROX_TOLERANCE_FACTOR, true);

            //converting the MatOfPoint2f to a contour
            Contour approxContour = new Contour(approx);

            //Make sure the contour is big enough, CLOSED (convex), and has exactly 4 points
            if (approx.toArray().length == 4 && Math.abs(approxContour.area()) > 1000 && approxContour.isClosed()) {

                //TODO contours and rectangles array may not match up, but why would they?
                contours.add(approxContour);

                //Check each angle to be approximately 90 degrees
                //Done by comparing the three points constituting the angle of each corner
                double maxCosine = 0;
                for (int j = 2; j < 5; j++) {
                    double cosine = Math.abs(MathUtil.angle(approx.toArray()[j % 4], approx.toArray()[j - 2],
                            approx.toArray()[j - 1]));
                    maxCosine = Math.max(maxCosine, cosine);
                }

                if (maxCosine < MAX_COSINE_VALUE) {
                    //Convert the points to a rectangle instance
                    rectangles.add(new Rectangle(approx.toArray()));
                }
            }
        }

        return new RectangleLocationResult(contours, rectangles);
    }

    /**
     * Contains the list of rectangles retrieved from locateRectangles()
     */
    public static class RectangleLocationResult {
        final List<Contour> contours;
        final List<Rectangle> ellipses;

        RectangleLocationResult(List<Contour> contours, List<Rectangle> ellipses) {
            this.contours = contours;
            this.ellipses = ellipses;
        }

        /**
         * Gets list of contours in the image , as processed by Canny detection
         *
         * @return List of contours in the image
         */
        public List<Contour> getContours() {
            return contours;
        }

        /**
         * Gets list of rectangles detected in the image
         *
         * @return List of rectangles detected in the image
         */
        public List<Rectangle> getRectangles() {
            return ellipses;
        }
    }

    /**
     * Contains the list of ellipses retrieved from locateEllipses()
     */
    public static class EllipseLocationResult {
        final List<Contour> contours;
        final List<Ellipse> ellipses;

        EllipseLocationResult(List<Contour> contours, List<Ellipse> ellipses) {
            this.contours = contours;
            this.ellipses = ellipses;
        }

        /**
         * Gets list of contours in the image, as processed by Canny detection
         *
         * @return List of contours in the image
         */
        public List<Contour> getContours() {
            return contours;
        }

        /**
         * Get list of ellipses located in the image
         *
         * @return List of ellipses detected in the image
         */
        public List<Ellipse> getEllipses() {
            return ellipses;
        }
    }
}