Add sliders for adjusting the HSV thresholds, making tuning practical.
Here's a good cut at tuning those parameters and some of the other parameters.
The recognizer really works!
It can be further tuned and refined.

git-svn-id: https://robotics.mvla.net/svn/frc971/2013/trunk/src@4129 f308d9b7-e957-4cde-b6ac-9a88185e7312
diff --git a/971CV/src/org/frc971/Recognizer.java b/971CV/src/org/frc971/Recognizer.java
index 556cff0..c8818b7 100644
--- a/971CV/src/org/frc971/Recognizer.java
+++ b/971CV/src/org/frc971/Recognizer.java
@@ -9,6 +9,18 @@
  * @author jerry

  */

 public interface Recognizer {

+

+    /**

+     * Sets the HSV filter to allow H in [minHue .. maxHue], S >= minSat,

+     * V >= minVal.

+     */

+    public void setHSVRange(int minHue, int maxHue, int minSat, int minVal);

+

+    public int getHueMin();

+    public int getHueMax();

+    public int getSatMin();

+    public int getValMin();

+

     /**

      * Processes a camera image, returning an image to display for targeting

      * and debugging, e.g. with cross-hairs and marked targets.

diff --git a/971CV/src/org/frc971/Recognizer2013.java b/971CV/src/org/frc971/Recognizer2013.java
index f8c8b7d..e560f1e 100644
--- a/971CV/src/org/frc971/Recognizer2013.java
+++ b/971CV/src/org/frc971/Recognizer2013.java
@@ -25,14 +25,14 @@
 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 double kRoughlyHorizontalSlope = Math.tan(Math.toRadians(30));

+    static final double kRoughlyVerticalSlope = Math.tan(Math.toRadians(90 - 30));

+    private int min1Hue;

+    private int max1Hue;

+    private int min1Sat;

+    private int min1Val;

+    static final int kHoleClosingIterations = 2;

+    static final double kPolygonPercentFit = 12;

 

     static final int kMinWidthAt320 = 35; // for high goal and middle goals

 

@@ -74,9 +74,26 @@
     private WPIPoint linePt1, linePt2; // crosshair endpoints

 

     public Recognizer2013() {

+	setHSVRange(70, 106, 137, 27);

     }

 

     @Override

+    public void setHSVRange(int minHue, int maxHue, int minSat, int minVal) {

+	min1Hue = minHue - 1; // - 1 because cvThreshold() does > instead of >=

+	max1Hue = maxHue + 1;

+	min1Sat = minSat - 1;

+	min1Val = minVal - 1;

+    }

+    @Override

+    public int getHueMin() { return min1Hue + 1; }

+    @Override

+    public int getHueMax() { return max1Hue - 1; }

+    @Override

+    public int getSatMin() { return min1Sat - 1; }

+    @Override

+    public int getValMin() { return min1Val - 1; }

+

+    @Override

     public WPIImage processImage(WPIColorImage cameraImage) {

 	// (Re)allocate the intermediate images if the input is a different

 	// size than the previous image.

@@ -114,10 +131,10 @@
         // 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);

+        opencv_imgproc.cvThreshold(hue, bin, min1Hue, 255, opencv_imgproc.CV_THRESH_BINARY);

+        opencv_imgproc.cvThreshold(hue, hue, max1Hue, 255, opencv_imgproc.CV_THRESH_BINARY_INV);

+        opencv_imgproc.cvThreshold(sat, sat, min1Sat, 255, opencv_imgproc.CV_THRESH_BINARY);

+        opencv_imgproc.cvThreshold(val, val, min1Val, 255, opencv_imgproc.CV_THRESH_BINARY);

 

         // Combine the results to obtain a binary image which is mostly the

         // interesting pixels.

diff --git a/971CV/src/org/frc971/VisionTuner.java b/971CV/src/org/frc971/VisionTuner.java
index 159cafd..757b46c 100644
--- a/971CV/src/org/frc971/VisionTuner.java
+++ b/971CV/src/org/frc971/VisionTuner.java
@@ -1,11 +1,17 @@
 package org.frc971;

 

+import java.awt.BorderLayout;

+import java.awt.GridLayout;

 import java.awt.event.KeyEvent;

 import java.io.File;

 import java.io.IOException;

 

 import javax.imageio.ImageIO;

+import javax.swing.JPanel;

+import javax.swing.JSlider;

 import javax.swing.WindowConstants;

+import javax.swing.event.ChangeEvent;

+import javax.swing.event.ChangeListener;

 

 import com.googlecode.javacv.CanvasFrame;

 

@@ -34,14 +40,64 @@
 public class VisionTuner {

     private String[] testImageFilenames;

     private WPIColorImage[] testImages;

-    private final CanvasFrame cameraFrame = new CanvasFrame("Camera");

     private int currentIndex = 0;

     private Recognizer recognizer = new Recognizer2013();

 

+    private final CanvasFrame cameraFrame = new CanvasFrame("Camera");

+    private final JPanel panel = new JPanel();

+    private final JSlider hueMinSlider = new JSlider();

+    private final JSlider hueMaxSlider = new JSlider();

+    private final JSlider satMinSlider = new JSlider();

+    private final JSlider valMinSlider = new JSlider();

+

     public VisionTuner(String[] imageFilenames) {

         cameraFrame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);

 

         loadTestImages(imageFilenames);

+

+        cameraFrame.getContentPane().add(panel, BorderLayout.SOUTH);

+        panel.setLayout(new GridLayout(0, 2, 0, 0));

+

+        ChangeListener sliderListener = new ChangeListener() {

+            @Override

+	    public void stateChanged(ChangeEvent e) {

+        	System.out.println("New HSV range ["

+        		+ hueMinSlider.getValue() + " .. "

+        		+ hueMaxSlider.getValue() + "], ["

+        		+ satMinSlider.getValue() + " .. 255], ["

+        		+ valMinSlider.getValue() + " .. 255]");

+        	recognizer.setHSVRange(

+        		hueMinSlider.getValue(), hueMaxSlider.getValue(),

+        		satMinSlider.getValue(),

+        		valMinSlider.getValue());

+        	processCurrentImage();

+            }

+        };

+

+        hueMinSlider.setValue(recognizer.getHueMin());

+        hueMinSlider.setToolTipText("minimum HSV hue");

+        hueMinSlider.setMaximum(255);

+        panel.add(hueMinSlider);

+

+        hueMaxSlider.setValue(recognizer.getHueMax());

+        hueMaxSlider.setToolTipText("maximum HSV hue");

+        hueMaxSlider.setMaximum(255);

+        panel.add(hueMaxSlider);

+

+        satMinSlider.setValue(recognizer.getSatMin());

+        satMinSlider.setToolTipText("minimum HSV color saturation");

+        satMinSlider.setMaximum(255);

+        panel.add(satMinSlider);

+

+        valMinSlider.setValue(recognizer.getValMin());

+        valMinSlider.setToolTipText("minimum HSV brightness value");

+        valMinSlider.setMaximum(255);

+        panel.add(valMinSlider);

+

+        hueMinSlider.addChangeListener(sliderListener);

+        hueMaxSlider.addChangeListener(sliderListener);

+        satMinSlider.addChangeListener(sliderListener);

+        valMinSlider.addChangeListener(sliderListener);

     }

 

     /**

@@ -51,6 +107,7 @@
     private void loadTestImages(String[] imageFilenames) {

 	testImageFilenames = imageFilenames;

 	testImages = new WPIColorImage[testImageFilenames.length];

+	currentIndex = 0;

 

 	for (int i = 0; i < testImageFilenames.length; i++) {

             String imageFilename = testImageFilenames[i];