blob: f8c8b7dfad955218a194793469931400951051fe [file] [log] [blame]
jerrym6ebe6452013-02-18 03:00:31 +00001package org.frc971;
2
3import java.util.ArrayList;
4
5import com.googlecode.javacv.cpp.opencv_core;
6import com.googlecode.javacv.cpp.opencv_core.CvSize;
7import com.googlecode.javacv.cpp.opencv_core.IplImage;
8import com.googlecode.javacv.cpp.opencv_imgproc;
9import com.googlecode.javacv.cpp.opencv_imgproc.IplConvKernel;
10
11import edu.wpi.first.wpijavacv.DaisyExtensions;
12import edu.wpi.first.wpijavacv.WPIBinaryImage;
13import edu.wpi.first.wpijavacv.WPIColor;
14import edu.wpi.first.wpijavacv.WPIColorImage;
15import edu.wpi.first.wpijavacv.WPIContour;
16import edu.wpi.first.wpijavacv.WPIImage;
17import edu.wpi.first.wpijavacv.WPIPoint;
18import edu.wpi.first.wpijavacv.WPIPolygon;
19
20/**
21 * Vision target recognizer for FRC 2013.
22 *
23 * @author jerry
24 */
25public class Recognizer2013 implements Recognizer {
26
jerrymaa7a63b2013-02-18 06:31:22 +000027 // --- Constants that need to be tuned.
28 static final double kRoughlyHorizontalSlope = Math.tan(Math.toRadians(25));
29 static final double kRoughlyVerticalSlope = Math.tan(Math.toRadians(90 - 25));
30 static final double kMin1Hue = 55 - 1; // - 1 because cvThreshold() does > not >=
31 static final double kMax1Hue = 118 + 1;
32 static final double kMin1Sat = 80 - 1;
33 static final double kMin1Val = 69 - 1;
34 static final int kHoleClosingIterations = 3;
35 static final double kPolygonPercentFit = 12; // was 20
36
37 static final int kMinWidthAt320 = 35; // for high goal and middle goals
38
39 // These aspect ratios include the outside edges of the vision target tape.
40 static final double kHighGoalAspect = (21 + 8.0) / (54 + 8);
41 static final double kMiddleGoalAspect = (24 + 8.0) / (54 + 8);
42 static final double kMinAspect = kHighGoalAspect * 0.6;
43 static final double kMaxAspect = kMiddleGoalAspect * 1.4;
jerrym6ebe6452013-02-18 03:00:31 +000044
45 static final double kShooterOffsetDeg = 0;
46 static final double kHorizontalFOVDeg = 47.0;
47 static final double kVerticalFOVDeg = 480.0 / 640.0 * kHorizontalFOVDeg;
48
jerrymaa7a63b2013-02-18 06:31:22 +000049 // --- Colors for drawing indicators on the image.
jerrym6ebe6452013-02-18 03:00:31 +000050 private static final WPIColor reject1Color = WPIColor.GRAY;
51 private static final WPIColor reject2Color = WPIColor.YELLOW;
52 private static final WPIColor candidateColor = WPIColor.BLUE;
53 private static final WPIColor targetColor = new WPIColor(255, 0, 0);
54
55 // Show intermediate images for parameter tuning.
56 private final DebugCanvas thresholdedCanvas = new DebugCanvas("thresholded");
57 private final DebugCanvas morphedCanvas = new DebugCanvas("morphed");
58
jerrymaa7a63b2013-02-18 06:31:22 +000059 // Data to reuse for each frame.
jerrym6ebe6452013-02-18 03:00:31 +000060 private final DaisyExtensions daisyExtensions = new DaisyExtensions();
61 private final IplConvKernel morphKernel = IplConvKernel.create(3, 3, 1, 1,
62 opencv_imgproc.CV_SHAPE_RECT, null);
jerrym6ebe6452013-02-18 03:00:31 +000063 private final ArrayList<WPIPolygon> polygons = new ArrayList<WPIPolygon>();
jerrymaa7a63b2013-02-18 06:31:22 +000064
65 // Frame-size-dependent data to reuse for each frame.
66 private CvSize size = null;
jerrym6ebe6452013-02-18 03:00:31 +000067 private WPIColorImage rawImage;
68 private IplImage bin;
69 private IplImage hsv;
70 private IplImage hue;
71 private IplImage sat;
72 private IplImage val;
jerrymaa7a63b2013-02-18 06:31:22 +000073 private int minWidth;
jerrym6ebe6452013-02-18 03:00:31 +000074 private WPIPoint linePt1, linePt2; // crosshair endpoints
75
76 public Recognizer2013() {
77 }
78
79 @Override
80 public WPIImage processImage(WPIColorImage cameraImage) {
81 // (Re)allocate the intermediate images if the input is a different
82 // size than the previous image.
83 if (size == null || size.width() != cameraImage.getWidth()
84 || size.height() != cameraImage.getHeight()) {
85 size = opencv_core.cvSize(cameraImage.getWidth(),
86 cameraImage.getHeight());
87 rawImage = DaisyExtensions.makeWPIColorImage(
88 DaisyExtensions.getIplImage(cameraImage));
89 bin = IplImage.create(size, 8, 1);
90 hsv = IplImage.create(size, 8, 3);
91 hue = IplImage.create(size, 8, 1);
92 sat = IplImage.create(size, 8, 1);
93 val = IplImage.create(size, 8, 1);
jerrymaa7a63b2013-02-18 06:31:22 +000094 minWidth = (kMinWidthAt320 * cameraImage.getWidth() + 319) / 320;
jerrym6ebe6452013-02-18 03:00:31 +000095
96 int horizontalOffsetPixels = (int)Math.round(
97 kShooterOffsetDeg * size.width() / kHorizontalFOVDeg);
98 int x = size.width() / 2 + horizontalOffsetPixels;
99 linePt1 = new WPIPoint(x, size.height() - 1);
100 linePt2 = new WPIPoint(x, 0);
101 } else {
jerrymaa7a63b2013-02-18 06:31:22 +0000102 // Copy the camera image so it's safe to draw on.
jerrym6ebe6452013-02-18 03:00:31 +0000103 opencv_core.cvCopy(DaisyExtensions.getIplImage(cameraImage),
104 DaisyExtensions.getIplImage(rawImage));
105 }
106
107 IplImage input = DaisyExtensions.getIplImage(rawImage);
108
109 // Threshold the pixels in HSV color space.
110 // TODO(jerry): Do this in one pass of a pixel-processing loop.
jerrymaa7a63b2013-02-18 06:31:22 +0000111 opencv_imgproc.cvCvtColor(input, hsv, opencv_imgproc.CV_BGR2HSV_FULL);
jerrym6ebe6452013-02-18 03:00:31 +0000112 opencv_core.cvSplit(hsv, hue, sat, val, null);
113
114 // NOTE: Since red is at the end of the cyclic color space, you can OR
115 // a threshold and an inverted threshold to match red pixels.
116 // TODO(jerry): Use tunable constants instead of literals.
jerrymaa7a63b2013-02-18 06:31:22 +0000117 opencv_imgproc.cvThreshold(hue, bin, kMin1Hue, 255, opencv_imgproc.CV_THRESH_BINARY);
118 opencv_imgproc.cvThreshold(hue, hue, kMax1Hue, 255, opencv_imgproc.CV_THRESH_BINARY_INV);
119 opencv_imgproc.cvThreshold(sat, sat, kMin1Sat, 255, opencv_imgproc.CV_THRESH_BINARY);
120 opencv_imgproc.cvThreshold(val, val, kMin1Val, 255, opencv_imgproc.CV_THRESH_BINARY);
jerrym6ebe6452013-02-18 03:00:31 +0000121
122 // Combine the results to obtain a binary image which is mostly the
123 // interesting pixels.
124 opencv_core.cvAnd(hue, bin, bin, null);
125 opencv_core.cvAnd(bin, sat, bin, null);
126 opencv_core.cvAnd(bin, val, bin, null);
127
128 thresholdedCanvas.showImage(bin);
129
130 // Fill in gaps using binary morphology.
131 opencv_imgproc.cvMorphologyEx(bin, bin, null, morphKernel,
132 opencv_imgproc.CV_MOP_CLOSE, kHoleClosingIterations);
133
134 morphedCanvas.showImage(bin);
135
136 // Find contours.
jerrymaa7a63b2013-02-18 06:31:22 +0000137 //
138 // TODO(jerry): Request contours as a two-level hierarchy (blobs and
139 // holes)? The targets have known sizes and their holes have known,
140 // smaller sizes. This matters for distance measurement. OTOH it's moot
141 // if/when we use the vertical stripes for distance measurement.
jerrym6ebe6452013-02-18 03:00:31 +0000142 WPIBinaryImage binWpi = DaisyExtensions.makeWPIBinaryImage(bin);
jerrymaa7a63b2013-02-18 06:31:22 +0000143 WPIContour[] contours = daisyExtensions.findConvexContours(binWpi);
jerrym6ebe6452013-02-18 03:00:31 +0000144
jerrymaa7a63b2013-02-18 06:31:22 +0000145 // Simplify the contours to polygons and filter by size and aspect ratio.
146 //
147 // TODO(jerry): Also look for the two vertical stripe vision targets.
148 // They'll greatly increase the precision of measuring the distance. If
149 // both stripes are visible, they'll increase the accuracy for
150 // identifying the high goal.
jerrym6ebe6452013-02-18 03:00:31 +0000151 polygons.clear();
152 for (WPIContour c : contours) {
jerrymaa7a63b2013-02-18 06:31:22 +0000153 if (c.getWidth() >= minWidth) {
154 double ratio = ((double) c.getHeight()) / c.getWidth();
155 if (ratio >= kMinAspect && ratio <= kMaxAspect) {
156 polygons.add(c.approxPolygon(kPolygonPercentFit));
157// System.out.println(" Accepted aspect ratio " + ratio);
158 } else {
159// System.out.println(" Rejected aspect ratio " + ratio);
160 }
jerrym6ebe6452013-02-18 03:00:31 +0000161 }
162 }
163
jerrymaa7a63b2013-02-18 06:31:22 +0000164 // Pick the target with the highest center-point that matches yet more
165 // filter criteria.
jerrym6ebe6452013-02-18 03:00:31 +0000166 WPIPolygon bestTarget = null;
167 int highestY = Integer.MAX_VALUE;
168
169 for (WPIPolygon p : polygons) {
jerrymaa7a63b2013-02-18 06:31:22 +0000170 // TODO(jerry): Replace boolean filters with a scoring function?
jerrym6ebe6452013-02-18 03:00:31 +0000171 if (p.isConvex() && p.getNumVertices() == 4) { // quadrilateral
172 WPIPoint[] points = p.getPoints();
jerrymaa7a63b2013-02-18 06:31:22 +0000173 // Filter for polygons with 2 ~horizontal and 2 ~vertical sides.
jerrym6ebe6452013-02-18 03:00:31 +0000174 int numRoughlyHorizontal = 0;
175 int numRoughlyVertical = 0;
176 for (int i = 0; i < 4; ++i) {
177 double dy = points[i].getY() - points[(i + 1) % 4].getY();
178 double dx = points[i].getX() - points[(i + 1) % 4].getX();
179 double slope = Double.MAX_VALUE;
180 if (dx != 0) {
181 slope = Math.abs(dy / dx);
182 }
183
184 if (slope < kRoughlyHorizontalSlope) {
185 ++numRoughlyHorizontal;
186 } else if (slope > kRoughlyVerticalSlope) {
187 ++numRoughlyVertical;
188 }
189 }
190
jerrymaa7a63b2013-02-18 06:31:22 +0000191 if (numRoughlyHorizontal >= 2 && numRoughlyVertical == 2) {
jerrym6ebe6452013-02-18 03:00:31 +0000192 rawImage.drawPolygon(p, candidateColor, 2);
193
194 int pCenterX = p.getX() + p.getWidth() / 2;
195 int pCenterY = p.getY() + p.getHeight() / 2;
196
197 rawImage.drawPoint(new WPIPoint(pCenterX, pCenterY),
jerrymaa7a63b2013-02-18 06:31:22 +0000198 targetColor, 2);
jerrym6ebe6452013-02-18 03:00:31 +0000199 if (pCenterY < highestY) {
200 bestTarget = p;
201 highestY = pCenterY;
202 }
203 } else {
204 rawImage.drawPolygon(p, reject2Color, 1);
205 }
206 } else {
207 rawImage.drawPolygon(p, reject1Color, 1);
208 }
209 }
210
211 if (bestTarget != null) {
212 double w = bestTarget.getWidth();
213 double h = bestTarget.getHeight();
214 double x = bestTarget.getX() + w / 2;
215 double y = bestTarget.getY() + h / 2;
216
217 rawImage.drawPolygon(bestTarget, targetColor, 2);
218
219 System.out.println("Best target at (" + x + ", " + y + ") size "
220 + w + " x " + h);
221 } else {
222 System.out.println("No target found");
223 }
224
225 // Draw a crosshair
226 rawImage.drawLine(linePt1, linePt2, targetColor, 1);
227
228 daisyExtensions.releaseMemory();
229 //System.gc();
230
231 return rawImage;
232 }
233
234}