blob: f8c8b7dfad955218a194793469931400951051fe [file] [log] [blame]
package org.frc971;
import java.util.ArrayList;
import com.googlecode.javacv.cpp.opencv_core;
import com.googlecode.javacv.cpp.opencv_core.CvSize;
import com.googlecode.javacv.cpp.opencv_core.IplImage;
import com.googlecode.javacv.cpp.opencv_imgproc;
import com.googlecode.javacv.cpp.opencv_imgproc.IplConvKernel;
import edu.wpi.first.wpijavacv.DaisyExtensions;
import edu.wpi.first.wpijavacv.WPIBinaryImage;
import edu.wpi.first.wpijavacv.WPIColor;
import edu.wpi.first.wpijavacv.WPIColorImage;
import edu.wpi.first.wpijavacv.WPIContour;
import edu.wpi.first.wpijavacv.WPIImage;
import edu.wpi.first.wpijavacv.WPIPoint;
import edu.wpi.first.wpijavacv.WPIPolygon;
/**
* Vision target recognizer for FRC 2013.
*
* @author jerry
*/
public class Recognizer2013 implements Recognizer {
// --- Constants that need to be tuned.
static final double kRoughlyHorizontalSlope = Math.tan(Math.toRadians(25));
static final double kRoughlyVerticalSlope = Math.tan(Math.toRadians(90 - 25));
static final double kMin1Hue = 55 - 1; // - 1 because cvThreshold() does > not >=
static final double kMax1Hue = 118 + 1;
static final double kMin1Sat = 80 - 1;
static final double kMin1Val = 69 - 1;
static final int kHoleClosingIterations = 3;
static final double kPolygonPercentFit = 12; // was 20
static final int kMinWidthAt320 = 35; // for high goal and middle goals
// These aspect ratios include the outside edges of the vision target tape.
static final double kHighGoalAspect = (21 + 8.0) / (54 + 8);
static final double kMiddleGoalAspect = (24 + 8.0) / (54 + 8);
static final double kMinAspect = kHighGoalAspect * 0.6;
static final double kMaxAspect = kMiddleGoalAspect * 1.4;
static final double kShooterOffsetDeg = 0;
static final double kHorizontalFOVDeg = 47.0;
static final double kVerticalFOVDeg = 480.0 / 640.0 * kHorizontalFOVDeg;
// --- Colors for drawing indicators on the image.
private static final WPIColor reject1Color = WPIColor.GRAY;
private static final WPIColor reject2Color = WPIColor.YELLOW;
private static final WPIColor candidateColor = WPIColor.BLUE;
private static final WPIColor targetColor = new WPIColor(255, 0, 0);
// Show intermediate images for parameter tuning.
private final DebugCanvas thresholdedCanvas = new DebugCanvas("thresholded");
private final DebugCanvas morphedCanvas = new DebugCanvas("morphed");
// Data to reuse for each frame.
private final DaisyExtensions daisyExtensions = new DaisyExtensions();
private final IplConvKernel morphKernel = IplConvKernel.create(3, 3, 1, 1,
opencv_imgproc.CV_SHAPE_RECT, null);
private final ArrayList<WPIPolygon> polygons = new ArrayList<WPIPolygon>();
// Frame-size-dependent data to reuse for each frame.
private CvSize size = null;
private WPIColorImage rawImage;
private IplImage bin;
private IplImage hsv;
private IplImage hue;
private IplImage sat;
private IplImage val;
private int minWidth;
private WPIPoint linePt1, linePt2; // crosshair endpoints
public Recognizer2013() {
}
@Override
public WPIImage processImage(WPIColorImage cameraImage) {
// (Re)allocate the intermediate images if the input is a different
// size than the previous image.
if (size == null || size.width() != cameraImage.getWidth()
|| size.height() != cameraImage.getHeight()) {
size = opencv_core.cvSize(cameraImage.getWidth(),
cameraImage.getHeight());
rawImage = DaisyExtensions.makeWPIColorImage(
DaisyExtensions.getIplImage(cameraImage));
bin = IplImage.create(size, 8, 1);
hsv = IplImage.create(size, 8, 3);
hue = IplImage.create(size, 8, 1);
sat = IplImage.create(size, 8, 1);
val = IplImage.create(size, 8, 1);
minWidth = (kMinWidthAt320 * cameraImage.getWidth() + 319) / 320;
int horizontalOffsetPixels = (int)Math.round(
kShooterOffsetDeg * size.width() / kHorizontalFOVDeg);
int x = size.width() / 2 + horizontalOffsetPixels;
linePt1 = new WPIPoint(x, size.height() - 1);
linePt2 = new WPIPoint(x, 0);
} else {
// Copy the camera image so it's safe to draw on.
opencv_core.cvCopy(DaisyExtensions.getIplImage(cameraImage),
DaisyExtensions.getIplImage(rawImage));
}
IplImage input = DaisyExtensions.getIplImage(rawImage);
// Threshold the pixels in HSV color space.
// TODO(jerry): Do this in one pass of a pixel-processing loop.
opencv_imgproc.cvCvtColor(input, hsv, opencv_imgproc.CV_BGR2HSV_FULL);
opencv_core.cvSplit(hsv, hue, sat, val, null);
// NOTE: Since red is at the end of the cyclic color space, you can OR
// a threshold and an inverted threshold to match red pixels.
// TODO(jerry): Use tunable constants instead of literals.
opencv_imgproc.cvThreshold(hue, bin, kMin1Hue, 255, opencv_imgproc.CV_THRESH_BINARY);
opencv_imgproc.cvThreshold(hue, hue, kMax1Hue, 255, opencv_imgproc.CV_THRESH_BINARY_INV);
opencv_imgproc.cvThreshold(sat, sat, kMin1Sat, 255, opencv_imgproc.CV_THRESH_BINARY);
opencv_imgproc.cvThreshold(val, val, kMin1Val, 255, opencv_imgproc.CV_THRESH_BINARY);
// Combine the results to obtain a binary image which is mostly the
// interesting pixels.
opencv_core.cvAnd(hue, bin, bin, null);
opencv_core.cvAnd(bin, sat, bin, null);
opencv_core.cvAnd(bin, val, bin, null);
thresholdedCanvas.showImage(bin);
// Fill in gaps using binary morphology.
opencv_imgproc.cvMorphologyEx(bin, bin, null, morphKernel,
opencv_imgproc.CV_MOP_CLOSE, kHoleClosingIterations);
morphedCanvas.showImage(bin);
// Find contours.
//
// TODO(jerry): Request contours as a two-level hierarchy (blobs and
// holes)? The targets have known sizes and their holes have known,
// smaller sizes. This matters for distance measurement. OTOH it's moot
// if/when we use the vertical stripes for distance measurement.
WPIBinaryImage binWpi = DaisyExtensions.makeWPIBinaryImage(bin);
WPIContour[] contours = daisyExtensions.findConvexContours(binWpi);
// Simplify the contours to polygons and filter by size and aspect ratio.
//
// TODO(jerry): Also look for the two vertical stripe vision targets.
// They'll greatly increase the precision of measuring the distance. If
// both stripes are visible, they'll increase the accuracy for
// identifying the high goal.
polygons.clear();
for (WPIContour c : contours) {
if (c.getWidth() >= minWidth) {
double ratio = ((double) c.getHeight()) / c.getWidth();
if (ratio >= kMinAspect && ratio <= kMaxAspect) {
polygons.add(c.approxPolygon(kPolygonPercentFit));
// System.out.println(" Accepted aspect ratio " + ratio);
} else {
// System.out.println(" Rejected aspect ratio " + ratio);
}
}
}
// Pick the target with the highest center-point that matches yet more
// filter criteria.
WPIPolygon bestTarget = null;
int highestY = Integer.MAX_VALUE;
for (WPIPolygon p : polygons) {
// TODO(jerry): Replace boolean filters with a scoring function?
if (p.isConvex() && p.getNumVertices() == 4) { // quadrilateral
WPIPoint[] points = p.getPoints();
// Filter for polygons with 2 ~horizontal and 2 ~vertical sides.
int numRoughlyHorizontal = 0;
int numRoughlyVertical = 0;
for (int i = 0; i < 4; ++i) {
double dy = points[i].getY() - points[(i + 1) % 4].getY();
double dx = points[i].getX() - points[(i + 1) % 4].getX();
double slope = Double.MAX_VALUE;
if (dx != 0) {
slope = Math.abs(dy / dx);
}
if (slope < kRoughlyHorizontalSlope) {
++numRoughlyHorizontal;
} else if (slope > kRoughlyVerticalSlope) {
++numRoughlyVertical;
}
}
if (numRoughlyHorizontal >= 2 && numRoughlyVertical == 2) {
rawImage.drawPolygon(p, candidateColor, 2);
int pCenterX = p.getX() + p.getWidth() / 2;
int pCenterY = p.getY() + p.getHeight() / 2;
rawImage.drawPoint(new WPIPoint(pCenterX, pCenterY),
targetColor, 2);
if (pCenterY < highestY) {
bestTarget = p;
highestY = pCenterY;
}
} else {
rawImage.drawPolygon(p, reject2Color, 1);
}
} else {
rawImage.drawPolygon(p, reject1Color, 1);
}
}
if (bestTarget != null) {
double w = bestTarget.getWidth();
double h = bestTarget.getHeight();
double x = bestTarget.getX() + w / 2;
double y = bestTarget.getY() + h / 2;
rawImage.drawPolygon(bestTarget, targetColor, 2);
System.out.println("Best target at (" + x + ", " + y + ") size "
+ w + " x " + h);
} else {
System.out.println("No target found");
}
// Draw a crosshair
rawImage.drawLine(linePt1, linePt2, targetColor, 1);
daisyExtensions.releaseMemory();
//System.gc();
return rawImage;
}
}