From 0c58a0a705230331bb41ac014ffe0cd9eb655839 Mon Sep 17 00:00:00 2001 From: Thad House Date: Sun, 23 Sep 2018 21:20:12 -0700 Subject: [PATCH] Repackage CameraServer classes (#1321) This is necessary for modularization. Move the wpilibj CameraServer classes to the cameraserver package. Move the edu.wpi.first.wpilibj.vision package to edu.wpi.first.vision. To avoid code breakage, add deprecated copies of the wpilibj classes to the wpilibj jar. --- .../CameraServer.java | 2 +- .../CameraServerShared.java | 2 +- .../CameraServerSharedStore.java | 2 +- .../{wpilibj => }/vision/VisionPipeline.java | 2 +- .../{wpilibj => }/vision/VisionRunner.java | 4 +- .../{wpilibj => }/vision/VisionThread.java | 2 +- .../edu/wpi/first/vision/package-info.java | 88 ++ .../edu/wpi/first/wpilibj/CameraServer.java | 780 ++++++++++++++++++ .../java/edu/wpi/first/wpilibj/RobotBase.java | 2 + .../first/wpilibj/vision/VisionPipeline.java | 29 + .../first/wpilibj/vision/VisionRunner.java | 131 +++ .../first/wpilibj/vision/VisionThread.java | 50 ++ .../first/wpilibj/vision/package-info.java | 1 + .../wpilibj/examples/axiscamera/Robot.java | 2 +- .../examples/intermediatevision/Robot.java | 2 +- .../wpilibj/examples/quickvision/Robot.java | 2 +- 16 files changed, 1091 insertions(+), 10 deletions(-) rename cameraserver/src/main/java/edu/wpi/first/{wpilibj => cameraserver}/CameraServer.java (99%) rename cameraserver/src/main/java/edu/wpi/first/{wpilibj => cameraserver}/CameraServerShared.java (96%) rename cameraserver/src/main/java/edu/wpi/first/{wpilibj => cameraserver}/CameraServerSharedStore.java (97%) rename cameraserver/src/main/java/edu/wpi/first/{wpilibj => }/vision/VisionPipeline.java (95%) rename cameraserver/src/main/java/edu/wpi/first/{wpilibj => }/vision/VisionRunner.java (98%) rename cameraserver/src/main/java/edu/wpi/first/{wpilibj => }/vision/VisionThread.java (98%) create mode 100644 cameraserver/src/main/java/edu/wpi/first/vision/package-info.java create mode 100644 wpilibj/src/main/java/edu/wpi/first/wpilibj/CameraServer.java create mode 100644 wpilibj/src/main/java/edu/wpi/first/wpilibj/vision/VisionPipeline.java create mode 100644 wpilibj/src/main/java/edu/wpi/first/wpilibj/vision/VisionRunner.java create mode 100644 wpilibj/src/main/java/edu/wpi/first/wpilibj/vision/VisionThread.java rename {cameraserver => wpilibj}/src/main/java/edu/wpi/first/wpilibj/vision/package-info.java (99%) diff --git a/cameraserver/src/main/java/edu/wpi/first/wpilibj/CameraServer.java b/cameraserver/src/main/java/edu/wpi/first/cameraserver/CameraServer.java similarity index 99% rename from cameraserver/src/main/java/edu/wpi/first/wpilibj/CameraServer.java rename to cameraserver/src/main/java/edu/wpi/first/cameraserver/CameraServer.java index bbfbd93fc8..8d66c99583 100644 --- a/cameraserver/src/main/java/edu/wpi/first/wpilibj/CameraServer.java +++ b/cameraserver/src/main/java/edu/wpi/first/cameraserver/CameraServer.java @@ -5,7 +5,7 @@ /* the project. */ /*----------------------------------------------------------------------------*/ -package edu.wpi.first.wpilibj; +package edu.wpi.first.cameraserver; import java.util.ArrayList; import java.util.Arrays; diff --git a/cameraserver/src/main/java/edu/wpi/first/wpilibj/CameraServerShared.java b/cameraserver/src/main/java/edu/wpi/first/cameraserver/CameraServerShared.java similarity index 96% rename from cameraserver/src/main/java/edu/wpi/first/wpilibj/CameraServerShared.java rename to cameraserver/src/main/java/edu/wpi/first/cameraserver/CameraServerShared.java index 2463c8894a..32a4d01ff2 100644 --- a/cameraserver/src/main/java/edu/wpi/first/wpilibj/CameraServerShared.java +++ b/cameraserver/src/main/java/edu/wpi/first/cameraserver/CameraServerShared.java @@ -5,7 +5,7 @@ /* the project. */ /*----------------------------------------------------------------------------*/ -package edu.wpi.first.wpilibj; +package edu.wpi.first.cameraserver; public interface CameraServerShared { diff --git a/cameraserver/src/main/java/edu/wpi/first/wpilibj/CameraServerSharedStore.java b/cameraserver/src/main/java/edu/wpi/first/cameraserver/CameraServerSharedStore.java similarity index 97% rename from cameraserver/src/main/java/edu/wpi/first/wpilibj/CameraServerSharedStore.java rename to cameraserver/src/main/java/edu/wpi/first/cameraserver/CameraServerSharedStore.java index 7fce82086f..c0cf2bb092 100644 --- a/cameraserver/src/main/java/edu/wpi/first/wpilibj/CameraServerSharedStore.java +++ b/cameraserver/src/main/java/edu/wpi/first/cameraserver/CameraServerSharedStore.java @@ -5,7 +5,7 @@ /* the project. */ /*----------------------------------------------------------------------------*/ -package edu.wpi.first.wpilibj; +package edu.wpi.first.cameraserver; public final class CameraServerSharedStore { private static CameraServerShared cameraServerShared; diff --git a/cameraserver/src/main/java/edu/wpi/first/wpilibj/vision/VisionPipeline.java b/cameraserver/src/main/java/edu/wpi/first/vision/VisionPipeline.java similarity index 95% rename from cameraserver/src/main/java/edu/wpi/first/wpilibj/vision/VisionPipeline.java rename to cameraserver/src/main/java/edu/wpi/first/vision/VisionPipeline.java index 67315a556e..6df10e7754 100644 --- a/cameraserver/src/main/java/edu/wpi/first/wpilibj/vision/VisionPipeline.java +++ b/cameraserver/src/main/java/edu/wpi/first/vision/VisionPipeline.java @@ -5,7 +5,7 @@ /* the project. */ /*----------------------------------------------------------------------------*/ -package edu.wpi.first.wpilibj.vision; +package edu.wpi.first.vision; import org.opencv.core.Mat; diff --git a/cameraserver/src/main/java/edu/wpi/first/wpilibj/vision/VisionRunner.java b/cameraserver/src/main/java/edu/wpi/first/vision/VisionRunner.java similarity index 98% rename from cameraserver/src/main/java/edu/wpi/first/wpilibj/vision/VisionRunner.java rename to cameraserver/src/main/java/edu/wpi/first/vision/VisionRunner.java index e36694d70f..24ffcda560 100644 --- a/cameraserver/src/main/java/edu/wpi/first/wpilibj/vision/VisionRunner.java +++ b/cameraserver/src/main/java/edu/wpi/first/vision/VisionRunner.java @@ -5,13 +5,13 @@ /* the project. */ /*----------------------------------------------------------------------------*/ -package edu.wpi.first.wpilibj.vision; +package edu.wpi.first.vision; import org.opencv.core.Mat; import edu.wpi.cscore.CvSink; import edu.wpi.cscore.VideoSource; -import edu.wpi.first.wpilibj.CameraServerSharedStore; +import edu.wpi.first.cameraserver.CameraServerSharedStore; /** * A vision runner is a convenient wrapper object to make it easy to run vision pipelines diff --git a/cameraserver/src/main/java/edu/wpi/first/wpilibj/vision/VisionThread.java b/cameraserver/src/main/java/edu/wpi/first/vision/VisionThread.java similarity index 98% rename from cameraserver/src/main/java/edu/wpi/first/wpilibj/vision/VisionThread.java rename to cameraserver/src/main/java/edu/wpi/first/vision/VisionThread.java index 5eeece8f04..576cb9672b 100644 --- a/cameraserver/src/main/java/edu/wpi/first/wpilibj/vision/VisionThread.java +++ b/cameraserver/src/main/java/edu/wpi/first/vision/VisionThread.java @@ -5,7 +5,7 @@ /* the project. */ /*----------------------------------------------------------------------------*/ -package edu.wpi.first.wpilibj.vision; +package edu.wpi.first.vision; import edu.wpi.cscore.VideoSource; diff --git a/cameraserver/src/main/java/edu/wpi/first/vision/package-info.java b/cameraserver/src/main/java/edu/wpi/first/vision/package-info.java new file mode 100644 index 0000000000..e2e5c62767 --- /dev/null +++ b/cameraserver/src/main/java/edu/wpi/first/vision/package-info.java @@ -0,0 +1,88 @@ +/*----------------------------------------------------------------------------*/ +/* Copyright (c) 2016-2018 FIRST. All Rights Reserved. */ +/* Open Source Software - may be modified and shared by FRC teams. The code */ +/* must be accompanied by the FIRST BSD license file in the root directory of */ +/* the project. */ +/*----------------------------------------------------------------------------*/ + +/** + * Classes in the {@code edu.wpi.first.vision} package are designed to + * simplify using OpenCV vision processing code from a robot program. + * + *

An example use case for grabbing a yellow tote from 2015 in autonomous: + *
+ *


+ * public class Robot extends IterativeRobot
+ *     implements VisionRunner.Listener<MyFindTotePipeline> {
+ *
+ *      // A USB camera connected to the roboRIO.
+ *      private {@link edu.wpi.cscore.VideoSource VideoSource} usbCamera;
+ *
+ *      // A vision pipeline. This could be handwritten or generated by GRIP.
+ *      // This has to implement {@link edu.wpi.first.vision.VisionPipeline}.
+ *      // For this example, assume that it's perfect and will always see the tote.
+ *      private MyFindTotePipeline findTotePipeline;
+ *      private {@link edu.wpi.first.vision.VisionThread} findToteThread;
+ *
+ *      // The object to synchronize on to make sure the vision thread doesn't
+ *      // write to variables the main thread is using.
+ *      private final Object visionLock = new Object();
+ *
+ *      // The pipeline outputs we want
+ *      private boolean pipelineRan = false; // lets us know when the pipeline has actually run
+ *      private double angleToTote = 0;
+ *      private double distanceToTote = 0;
+ *
+ *     {@literal @}Override
+ *      public void {@link edu.wpi.first.vision.VisionRunner.Listener#copyPipelineOutputs
+ *          copyPipelineOutputs(MyFindTotePipeline pipeline)} {
+ *          synchronized (visionLock) {
+ *              // Take a snapshot of the pipeline's output because
+ *              // it may have changed the next time this method is called!
+ *              this.pipelineRan = true;
+ *              this.angleToTote = pipeline.getAngleToTote();
+ *              this.distanceToTote = pipeline.getDistanceToTote();
+ *          }
+ *      }
+ *
+ *     {@literal @}Override
+ *      public void robotInit() {
+ *          usbCamera = CameraServer.getInstance().startAutomaticCapture(0);
+ *          findTotePipeline = new MyFindTotePipeline();
+ *          findToteThread = new VisionThread(usbCamera, findTotePipeline, this);
+ *      }
+ *
+ *     {@literal @}Override
+ *      public void autonomousInit() {
+ *          findToteThread.start();
+ *      }
+ *
+ *     {@literal @}Override
+ *      public void autonomousPeriodic() {
+ *          double angle;
+ *          double distance;
+ *          synchronized (visionLock) {
+ *              if (!pipelineRan) {
+ *                  // Wait until the pipeline has run
+ *                  return;
+ *              }
+ *              // Copy the outputs to make sure they're all from the same run
+ *              angle = this.angleToTote;
+ *              distance = this.distanceToTote;
+ *          }
+ *          if (!aimedAtTote()) {
+ *              turnToAngle(angle);
+ *          } else if (!droveToTote()) {
+ *              driveDistance(distance);
+ *          } else if (!grabbedTote()) {
+ *              grabTote();
+ *          } else {
+ *              // Tote was grabbed and we're done!
+ *              return;
+ *          }
+ *      }
+ *
+ * }
+ * 
+ */ +package edu.wpi.first.vision; diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/CameraServer.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/CameraServer.java new file mode 100644 index 0000000000..a25ab2ee8a --- /dev/null +++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/CameraServer.java @@ -0,0 +1,780 @@ +/*----------------------------------------------------------------------------*/ +/* Copyright (c) 2016-2018 FIRST. All Rights Reserved. */ +/* Open Source Software - may be modified and shared by FRC teams. The code */ +/* must be accompanied by the FIRST BSD license file in the root directory of */ +/* the project. */ +/*----------------------------------------------------------------------------*/ + +package edu.wpi.first.wpilibj; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Hashtable; +import java.util.Map; +import java.util.concurrent.atomic.AtomicInteger; + +import edu.wpi.cscore.AxisCamera; +import edu.wpi.cscore.CameraServerJNI; +import edu.wpi.cscore.CvSink; +import edu.wpi.cscore.CvSource; +import edu.wpi.cscore.MjpegServer; +import edu.wpi.cscore.UsbCamera; +import edu.wpi.cscore.VideoEvent; +import edu.wpi.cscore.VideoException; +import edu.wpi.cscore.VideoListener; +import edu.wpi.cscore.VideoMode; +import edu.wpi.cscore.VideoMode.PixelFormat; +import edu.wpi.cscore.VideoProperty; +import edu.wpi.cscore.VideoSink; +import edu.wpi.cscore.VideoSource; +import edu.wpi.first.cameraserver.CameraServerSharedStore; +import edu.wpi.first.networktables.EntryListenerFlags; +import edu.wpi.first.networktables.NetworkTable; +import edu.wpi.first.networktables.NetworkTableEntry; +import edu.wpi.first.networktables.NetworkTableInstance; + +/** + * Singleton class for creating and keeping camera servers. + * Also publishes camera information to NetworkTables. + * + * @deprecated Replaced with edu.wpi.first.cameraserver.CameraServer + */ +@SuppressWarnings("PMD.TooManyMethods") +@Deprecated +public final class CameraServer { + public static final int kBasePort = 1181; + + @Deprecated + public static final int kSize640x480 = 0; + @Deprecated + public static final int kSize320x240 = 1; + @Deprecated + public static final int kSize160x120 = 2; + + private static final String kPublishName = "/CameraPublisher"; + private static CameraServer server; + + /** + * Get the CameraServer instance. + */ + public static synchronized CameraServer getInstance() { + if (server == null) { + server = new CameraServer(); + } + return server; + } + + private final AtomicInteger m_defaultUsbDevice; + private String m_primarySourceName; + private final Map m_sources; + private final Map m_sinks; + private final Map m_tables; // indexed by source handle + private final NetworkTable m_publishTable; + private final VideoListener m_videoListener; //NOPMD + private final int m_tableListener; //NOPMD + private int m_nextPort; + private String[] m_addresses; + + @SuppressWarnings("JavadocMethod") + private static String makeSourceValue(int source) { + switch (VideoSource.getKindFromInt(CameraServerJNI.getSourceKind(source))) { + case kUsb: + return "usb:" + CameraServerJNI.getUsbCameraPath(source); + case kHttp: { + String[] urls = CameraServerJNI.getHttpCameraUrls(source); + if (urls.length > 0) { + return "ip:" + urls[0]; + } else { + return "ip:"; + } + } + case kCv: + // FIXME: Should be "cv:", but LabVIEW dashboard requires "usb:". + // https://github.com/wpilibsuite/allwpilib/issues/407 + return "usb:"; + default: + return "unknown:"; + } + } + + @SuppressWarnings("JavadocMethod") + private static String makeStreamValue(String address, int port) { + return "mjpg:http://" + address + ":" + port + "/?action=stream"; + } + + @SuppressWarnings({"JavadocMethod", "PMD.AvoidUsingHardCodedIP"}) + private synchronized String[] getSinkStreamValues(int sink) { + // Ignore all but MjpegServer + if (VideoSink.getKindFromInt(CameraServerJNI.getSinkKind(sink)) != VideoSink.Kind.kMjpeg) { + return new String[0]; + } + + // Get port + int port = CameraServerJNI.getMjpegServerPort(sink); + + // Generate values + ArrayList values = new ArrayList<>(m_addresses.length + 1); + String listenAddress = CameraServerJNI.getMjpegServerListenAddress(sink); + if (!listenAddress.isEmpty()) { + // If a listen address is specified, only use that + values.add(makeStreamValue(listenAddress, port)); + } else { + // Otherwise generate for hostname and all interface addresses + values.add(makeStreamValue(CameraServerJNI.getHostname() + ".local", port)); + for (String addr : m_addresses) { + if ("127.0.0.1".equals(addr)) { + continue; // ignore localhost + } + values.add(makeStreamValue(addr, port)); + } + } + + return values.toArray(new String[0]); + } + + @SuppressWarnings({"JavadocMethod", "PMD.AvoidUsingHardCodedIP"}) + private synchronized String[] getSourceStreamValues(int source) { + // Ignore all but HttpCamera + if (VideoSource.getKindFromInt(CameraServerJNI.getSourceKind(source)) + != VideoSource.Kind.kHttp) { + return new String[0]; + } + + // Generate values + String[] values = CameraServerJNI.getHttpCameraUrls(source); + for (int j = 0; j < values.length; j++) { + values[j] = "mjpg:" + values[j]; + } + + // Look to see if we have a passthrough server for this source + for (VideoSink i : m_sinks.values()) { + int sink = i.getHandle(); + int sinkSource = CameraServerJNI.getSinkSource(sink); + if (source == sinkSource + && VideoSink.getKindFromInt(CameraServerJNI.getSinkKind(sink)) == VideoSink.Kind.kMjpeg) { + // Add USB-only passthrough + String[] finalValues = Arrays.copyOf(values, values.length + 1); + int port = CameraServerJNI.getMjpegServerPort(sink); + finalValues[values.length] = makeStreamValue("172.22.11.2", port); + return finalValues; + } + } + + return values; + } + + @SuppressWarnings({"JavadocMethod", "PMD.AvoidUsingHardCodedIP"}) + private synchronized void updateStreamValues() { + // Over all the sinks... + for (VideoSink i : m_sinks.values()) { + int sink = i.getHandle(); + + // Get the source's subtable (if none exists, we're done) + int source = CameraServerJNI.getSinkSource(sink); + if (source == 0) { + continue; + } + NetworkTable table = m_tables.get(source); + if (table != null) { + // Don't set stream values if this is a HttpCamera passthrough + if (VideoSource.getKindFromInt(CameraServerJNI.getSourceKind(source)) + == VideoSource.Kind.kHttp) { + continue; + } + + // Set table value + String[] values = getSinkStreamValues(sink); + if (values.length > 0) { + table.getEntry("streams").setStringArray(values); + } + } + } + + // Over all the sources... + for (VideoSource i : m_sources.values()) { + int source = i.getHandle(); + + // Get the source's subtable (if none exists, we're done) + NetworkTable table = m_tables.get(source); + if (table != null) { + // Set table value + String[] values = getSourceStreamValues(source); + if (values.length > 0) { + table.getEntry("streams").setStringArray(values); + } + } + } + } + + @SuppressWarnings("JavadocMethod") + private static String pixelFormatToString(PixelFormat pixelFormat) { + switch (pixelFormat) { + case kMJPEG: + return "MJPEG"; + case kYUYV: + return "YUYV"; + case kRGB565: + return "RGB565"; + case kBGR: + return "BGR"; + case kGray: + return "Gray"; + default: + return "Unknown"; + } + } + + /// Provide string description of video mode. + /// The returned string is "{width}x{height} {format} {fps} fps". + @SuppressWarnings("JavadocMethod") + private static String videoModeToString(VideoMode mode) { + return mode.width + "x" + mode.height + " " + pixelFormatToString(mode.pixelFormat) + + " " + mode.fps + " fps"; + } + + @SuppressWarnings("JavadocMethod") + private static String[] getSourceModeValues(int sourceHandle) { + VideoMode[] modes = CameraServerJNI.enumerateSourceVideoModes(sourceHandle); + String[] modeStrings = new String[modes.length]; + for (int i = 0; i < modes.length; i++) { + modeStrings[i] = videoModeToString(modes[i]); + } + return modeStrings; + } + + @SuppressWarnings({"JavadocMethod", "PMD.CyclomaticComplexity"}) + private static void putSourcePropertyValue(NetworkTable table, VideoEvent event, boolean isNew) { + String name; + String infoName; + if (event.name.startsWith("raw_")) { + name = "RawProperty/" + event.name; + infoName = "RawPropertyInfo/" + event.name; + } else { + name = "Property/" + event.name; + infoName = "PropertyInfo/" + event.name; + } + + NetworkTableEntry entry = table.getEntry(name); + try { + switch (event.propertyKind) { + case kBoolean: + if (isNew) { + entry.setDefaultBoolean(event.value != 0); + } else { + entry.setBoolean(event.value != 0); + } + break; + case kInteger: + case kEnum: + if (isNew) { + entry.setDefaultDouble(event.value); + table.getEntry(infoName + "/min").setDouble( + CameraServerJNI.getPropertyMin(event.propertyHandle)); + table.getEntry(infoName + "/max").setDouble( + CameraServerJNI.getPropertyMax(event.propertyHandle)); + table.getEntry(infoName + "/step").setDouble( + CameraServerJNI.getPropertyStep(event.propertyHandle)); + table.getEntry(infoName + "/default").setDouble( + CameraServerJNI.getPropertyDefault(event.propertyHandle)); + } else { + entry.setDouble(event.value); + } + break; + case kString: + if (isNew) { + entry.setDefaultString(event.valueStr); + } else { + entry.setString(event.valueStr); + } + break; + default: + break; + } + } catch (VideoException ignored) { + // ignore + } + } + + @SuppressWarnings({"JavadocMethod", "PMD.UnusedLocalVariable", "PMD.ExcessiveMethodLength", + "PMD.NPathComplexity"}) + private CameraServer() { + m_defaultUsbDevice = new AtomicInteger(); + m_sources = new Hashtable<>(); + m_sinks = new Hashtable<>(); + m_tables = new Hashtable<>(); + m_publishTable = NetworkTableInstance.getDefault().getTable(kPublishName); + m_nextPort = kBasePort; + m_addresses = new String[0]; + + // We publish sources to NetworkTables using the following structure: + // "/CameraPublisher/{Source.Name}/" - root + // - "source" (string): Descriptive, prefixed with type (e.g. "usb:0") + // - "streams" (string array): URLs that can be used to stream data + // - "description" (string): Description of the source + // - "connected" (boolean): Whether source is connected + // - "mode" (string): Current video mode + // - "modes" (string array): Available video modes + // - "Property/{Property}" - Property values + // - "PropertyInfo/{Property}" - Property supporting information + + // Listener for video events + m_videoListener = new VideoListener(event -> { + switch (event.kind) { + case kSourceCreated: { + // Create subtable for the camera + NetworkTable table = m_publishTable.getSubTable(event.name); + m_tables.put(event.sourceHandle, table); + table.getEntry("source").setString(makeSourceValue(event.sourceHandle)); + table.getEntry("description").setString( + CameraServerJNI.getSourceDescription(event.sourceHandle)); + table.getEntry("connected").setBoolean( + CameraServerJNI.isSourceConnected(event.sourceHandle)); + table.getEntry("streams").setStringArray(getSourceStreamValues(event.sourceHandle)); + try { + VideoMode mode = CameraServerJNI.getSourceVideoMode(event.sourceHandle); + table.getEntry("mode").setDefaultString(videoModeToString(mode)); + table.getEntry("modes").setStringArray(getSourceModeValues(event.sourceHandle)); + } catch (VideoException ignored) { + // Do nothing. Let the other event handlers update this if there is an error. + } + break; + } + case kSourceDestroyed: { + NetworkTable table = m_tables.get(event.sourceHandle); + if (table != null) { + table.getEntry("source").setString(""); + table.getEntry("streams").setStringArray(new String[0]); + table.getEntry("modes").setStringArray(new String[0]); + } + break; + } + case kSourceConnected: { + NetworkTable table = m_tables.get(event.sourceHandle); + if (table != null) { + // update the description too (as it may have changed) + table.getEntry("description").setString( + CameraServerJNI.getSourceDescription(event.sourceHandle)); + table.getEntry("connected").setBoolean(true); + } + break; + } + case kSourceDisconnected: { + NetworkTable table = m_tables.get(event.sourceHandle); + if (table != null) { + table.getEntry("connected").setBoolean(false); + } + break; + } + case kSourceVideoModesUpdated: { + NetworkTable table = m_tables.get(event.sourceHandle); + if (table != null) { + table.getEntry("modes").setStringArray(getSourceModeValues(event.sourceHandle)); + } + break; + } + case kSourceVideoModeChanged: { + NetworkTable table = m_tables.get(event.sourceHandle); + if (table != null) { + table.getEntry("mode").setString(videoModeToString(event.mode)); + } + break; + } + case kSourcePropertyCreated: { + NetworkTable table = m_tables.get(event.sourceHandle); + if (table != null) { + putSourcePropertyValue(table, event, true); + } + break; + } + case kSourcePropertyValueUpdated: { + NetworkTable table = m_tables.get(event.sourceHandle); + if (table != null) { + putSourcePropertyValue(table, event, false); + } + break; + } + case kSourcePropertyChoicesUpdated: { + NetworkTable table = m_tables.get(event.sourceHandle); + if (table != null) { + try { + String[] choices = CameraServerJNI.getEnumPropertyChoices(event.propertyHandle); + table.getEntry("PropertyInfo/" + event.name + "/choices").setStringArray(choices); + } catch (VideoException ignored) { + // ignore + } + } + break; + } + case kSinkSourceChanged: + case kSinkCreated: + case kSinkDestroyed: + case kNetworkInterfacesChanged: { + m_addresses = CameraServerJNI.getNetworkInterfaces(); + updateStreamValues(); + break; + } + default: + break; + } + }, 0x4fff, true); + + // Listener for NetworkTable events + // We don't currently support changing settings via NT due to + // synchronization issues, so just update to current setting if someone + // else tries to change it. + m_tableListener = NetworkTableInstance.getDefault().addEntryListener(kPublishName + "/", + event -> { + String relativeKey = event.name.substring(kPublishName.length() + 1); + + // get source (sourceName/...) + int subKeyIndex = relativeKey.indexOf('/'); + if (subKeyIndex == -1) { + return; + } + String sourceName = relativeKey.substring(0, subKeyIndex); + VideoSource source = m_sources.get(sourceName); + if (source == null) { + return; + } + + // get subkey + relativeKey = relativeKey.substring(subKeyIndex + 1); + + // handle standard names + String propName; + if ("mode".equals(relativeKey)) { + // reset to current mode + event.getEntry().setString(videoModeToString(source.getVideoMode())); + return; + } else if (relativeKey.startsWith("Property/")) { + propName = relativeKey.substring(9); + } else if (relativeKey.startsWith("RawProperty/")) { + propName = relativeKey.substring(12); + } else { + return; // ignore + } + + // everything else is a property + VideoProperty property = source.getProperty(propName); + switch (property.getKind()) { + case kNone: + return; + case kBoolean: + // reset to current setting + event.getEntry().setBoolean(property.get() != 0); + return; + case kInteger: + case kEnum: + // reset to current setting + event.getEntry().setDouble(property.get()); + return; + case kString: + // reset to current setting + event.getEntry().setString(property.getString()); + return; + default: + return; + } + }, EntryListenerFlags.kImmediate | EntryListenerFlags.kUpdate); + } + + /** + * Start automatically capturing images to send to the dashboard. + * + *

You should call this method to see a camera feed on the dashboard. + * If you also want to perform vision processing on the roboRIO, use + * getVideo() to get access to the camera images. + * + *

The first time this overload is called, it calls + * {@link #startAutomaticCapture(int)} with device 0, creating a camera + * named "USB Camera 0". Subsequent calls increment the device number + * (e.g. 1, 2, etc). + */ + public UsbCamera startAutomaticCapture() { + UsbCamera camera = startAutomaticCapture(m_defaultUsbDevice.getAndIncrement()); + CameraServerSharedStore.getCameraServerShared().reportUsbCamera(camera.getHandle()); + return camera; + } + + /** + * Start automatically capturing images to send to the dashboard. + * + *

This overload calls {@link #startAutomaticCapture(String, int)} with + * a name of "USB Camera {dev}". + * + * @param dev The device number of the camera interface + */ + public UsbCamera startAutomaticCapture(int dev) { + UsbCamera camera = new UsbCamera("USB Camera " + dev, dev); + startAutomaticCapture(camera); + CameraServerSharedStore.getCameraServerShared().reportUsbCamera(camera.getHandle()); + return camera; + } + + /** + * Start automatically capturing images to send to the dashboard. + * + * @param name The name to give the camera + * @param dev The device number of the camera interface + */ + public UsbCamera startAutomaticCapture(String name, int dev) { + UsbCamera camera = new UsbCamera(name, dev); + startAutomaticCapture(camera); + CameraServerSharedStore.getCameraServerShared().reportUsbCamera(camera.getHandle()); + return camera; + } + + /** + * Start automatically capturing images to send to the dashboard. + * + * @param name The name to give the camera + * @param path The device path (e.g. "/dev/video0") of the camera + */ + public UsbCamera startAutomaticCapture(String name, String path) { + UsbCamera camera = new UsbCamera(name, path); + startAutomaticCapture(camera); + CameraServerSharedStore.getCameraServerShared().reportUsbCamera(camera.getHandle()); + return camera; + } + + /** + * Start automatically capturing images to send to the dashboard from + * an existing camera. + * + * @param camera Camera + */ + public void startAutomaticCapture(VideoSource camera) { + addCamera(camera); + VideoSink server = addServer("serve_" + camera.getName()); + server.setSource(camera); + } + + /** + * Adds an Axis IP camera. + * + *

This overload calls {@link #addAxisCamera(String, String)} with + * name "Axis Camera". + * + * @param host Camera host IP or DNS name (e.g. "10.x.y.11") + */ + public AxisCamera addAxisCamera(String host) { + return addAxisCamera("Axis Camera", host); + } + + /** + * Adds an Axis IP camera. + * + *

This overload calls {@link #addAxisCamera(String, String[])} with + * name "Axis Camera". + * + * @param hosts Array of Camera host IPs/DNS names + */ + public AxisCamera addAxisCamera(String[] hosts) { + return addAxisCamera("Axis Camera", hosts); + } + + /** + * Adds an Axis IP camera. + * + * @param name The name to give the camera + * @param host Camera host IP or DNS name (e.g. "10.x.y.11") + */ + public AxisCamera addAxisCamera(String name, String host) { + AxisCamera camera = new AxisCamera(name, host); + // Create a passthrough MJPEG server for USB access + startAutomaticCapture(camera); + CameraServerSharedStore.getCameraServerShared().reportAxisCamera(camera.getHandle()); + return camera; + } + + /** + * Adds an Axis IP camera. + * + * @param name The name to give the camera + * @param hosts Array of Camera host IPs/DNS names + */ + public AxisCamera addAxisCamera(String name, String[] hosts) { + AxisCamera camera = new AxisCamera(name, hosts); + // Create a passthrough MJPEG server for USB access + startAutomaticCapture(camera); + CameraServerSharedStore.getCameraServerShared().reportAxisCamera(camera.getHandle()); + return camera; + } + + /** + * Get OpenCV access to the primary camera feed. This allows you to + * get images from the camera for image processing on the roboRIO. + * + *

This is only valid to call after a camera feed has been added + * with startAutomaticCapture() or addServer(). + */ + public CvSink getVideo() { + VideoSource source; + synchronized (this) { + if (m_primarySourceName == null) { + throw new VideoException("no camera available"); + } + source = m_sources.get(m_primarySourceName); + } + if (source == null) { + throw new VideoException("no camera available"); + } + return getVideo(source); + } + + /** + * Get OpenCV access to the specified camera. This allows you to get + * images from the camera for image processing on the roboRIO. + * + * @param camera Camera (e.g. as returned by startAutomaticCapture). + */ + public CvSink getVideo(VideoSource camera) { + String name = "opencv_" + camera.getName(); + + synchronized (this) { + VideoSink sink = m_sinks.get(name); + if (sink != null) { + VideoSink.Kind kind = sink.getKind(); + if (kind != VideoSink.Kind.kCv) { + throw new VideoException("expected OpenCV sink, but got " + kind); + } + return (CvSink) sink; + } + } + + CvSink newsink = new CvSink(name); + newsink.setSource(camera); + addServer(newsink); + return newsink; + } + + /** + * Get OpenCV access to the specified camera. This allows you to get + * images from the camera for image processing on the roboRIO. + * + * @param name Camera name + */ + public CvSink getVideo(String name) { + VideoSource source; + synchronized (this) { + source = m_sources.get(name); + if (source == null) { + throw new VideoException("could not find camera " + name); + } + } + return getVideo(source); + } + + /** + * Create a MJPEG stream with OpenCV input. This can be called to pass custom + * annotated images to the dashboard. + * + * @param name Name to give the stream + * @param width Width of the image being sent + * @param height Height of the image being sent + */ + public CvSource putVideo(String name, int width, int height) { + CvSource source = new CvSource(name, VideoMode.PixelFormat.kMJPEG, width, height, 30); + startAutomaticCapture(source); + return source; + } + + /** + * Adds a MJPEG server at the next available port. + * + * @param name Server name + */ + public MjpegServer addServer(String name) { + int port; + synchronized (this) { + port = m_nextPort; + m_nextPort++; + } + return addServer(name, port); + } + + /** + * Adds a MJPEG server. + * + * @param name Server name + */ + public MjpegServer addServer(String name, int port) { + MjpegServer server = new MjpegServer(name, port); + addServer(server); + return server; + } + + /** + * Adds an already created server. + * + * @param server Server + */ + public void addServer(VideoSink server) { + synchronized (this) { + m_sinks.put(server.getName(), server); + } + } + + /** + * Removes a server by name. + * + * @param name Server name + */ + public void removeServer(String name) { + synchronized (this) { + m_sinks.remove(name); + } + } + + /** + * Get server for the primary camera feed. + * + *

This is only valid to call after a camera feed has been added + * with startAutomaticCapture() or addServer(). + */ + public VideoSink getServer() { + synchronized (this) { + if (m_primarySourceName == null) { + throw new VideoException("no camera available"); + } + return getServer("serve_" + m_primarySourceName); + } + } + + /** + * Gets a server by name. + * + * @param name Server name + */ + public VideoSink getServer(String name) { + synchronized (this) { + return m_sinks.get(name); + } + } + + /** + * Adds an already created camera. + * + * @param camera Camera + */ + public void addCamera(VideoSource camera) { + String name = camera.getName(); + synchronized (this) { + if (m_primarySourceName == null) { + m_primarySourceName = name; + } + m_sources.put(name, camera); + } + } + + /** + * Removes a camera by name. + * + * @param name Camera name + */ + public void removeCamera(String name) { + synchronized (this) { + m_sources.remove(name); + } + } +} diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/RobotBase.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/RobotBase.java index 304595a090..ed978492e0 100644 --- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/RobotBase.java +++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/RobotBase.java @@ -15,6 +15,8 @@ import java.nio.file.Files; import java.util.function.Supplier; import edu.wpi.cscore.CameraServerJNI; +import edu.wpi.first.cameraserver.CameraServerShared; +import edu.wpi.first.cameraserver.CameraServerSharedStore; import edu.wpi.first.hal.FRCNetComm.tInstances; import edu.wpi.first.hal.FRCNetComm.tResourceType; import edu.wpi.first.hal.HAL; diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/vision/VisionPipeline.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/vision/VisionPipeline.java new file mode 100644 index 0000000000..2c0f8f2904 --- /dev/null +++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/vision/VisionPipeline.java @@ -0,0 +1,29 @@ +/*----------------------------------------------------------------------------*/ +/* Copyright (c) 2016-2018 FIRST. All Rights Reserved. */ +/* Open Source Software - may be modified and shared by FRC teams. The code */ +/* must be accompanied by the FIRST BSD license file in the root directory of */ +/* the project. */ +/*----------------------------------------------------------------------------*/ + +package edu.wpi.first.wpilibj.vision; + +import org.opencv.core.Mat; + +/** + * A vision pipeline is responsible for running a group of + * OpenCV algorithms to extract data from an image. + * + * @see VisionRunner + * @see VisionThread + * + * @deprecated Replaced with edu.wpi.first.vision.VisionPipeline + */ +@Deprecated +public interface VisionPipeline { + /** + * Processes the image input and sets the result objects. + * Implementations should make these objects accessible. + */ + void process(Mat image); + +} diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/vision/VisionRunner.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/vision/VisionRunner.java new file mode 100644 index 0000000000..701b86ea9c --- /dev/null +++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/vision/VisionRunner.java @@ -0,0 +1,131 @@ +/*----------------------------------------------------------------------------*/ +/* Copyright (c) 2016-2018 FIRST. All Rights Reserved. */ +/* Open Source Software - may be modified and shared by FRC teams. The code */ +/* must be accompanied by the FIRST BSD license file in the root directory of */ +/* the project. */ +/*----------------------------------------------------------------------------*/ + +package edu.wpi.first.wpilibj.vision; + +import org.opencv.core.Mat; + +import edu.wpi.cscore.CvSink; +import edu.wpi.cscore.VideoSource; +import edu.wpi.first.cameraserver.CameraServerSharedStore; + +/** + * A vision runner is a convenient wrapper object to make it easy to run vision pipelines + * from robot code. The easiest way to use this is to run it in a {@link VisionThread} + * and use the listener to take snapshots of the pipeline's outputs. + * + * @see VisionPipeline + * @see VisionThread + * @see vision + * + * @deprecated Replaced with edu.wpi.first.vision.VisionRunner + */ +@Deprecated +public class VisionRunner

{ + private final CvSink m_cvSink = new CvSink("VisionRunner CvSink"); + private final P m_pipeline; + private final Mat m_image = new Mat(); + private final Listener m_listener; + private volatile boolean m_enabled = true; + + /** + * Listener interface for a callback that should run after a pipeline has processed its input. + * + * @param

the type of the pipeline this listener is for + */ + @FunctionalInterface + public interface Listener

{ + /** + * Called when the pipeline has run. This shouldn't take much time to run because it will delay + * later calls to the pipeline's {@link VisionPipeline#process process} method. Copying the + * outputs and code that uses the copies should be synchronized on the same mutex to + * prevent multiple threads from reading and writing to the same memory at the same time. + * + * @param pipeline the vision pipeline that ran + */ + void copyPipelineOutputs(P pipeline); + + } + + /** + * Creates a new vision runner. It will take images from the {@code videoSource}, send them to + * the {@code pipeline}, and call the {@code listener} when the pipeline has finished to alert + * user code when it is safe to access the pipeline's outputs. + * + * @param videoSource the video source to use to supply images for the pipeline + * @param pipeline the vision pipeline to run + * @param listener a function to call after the pipeline has finished running + */ + public VisionRunner(VideoSource videoSource, P pipeline, Listener listener) { + this.m_pipeline = pipeline; + this.m_listener = listener; + m_cvSink.setSource(videoSource); + } + + /** + * Runs the pipeline one time, giving it the next image from the video source specified + * in the constructor. This will block until the source either has an image or throws an error. + * If the source successfully supplied a frame, the pipeline's image input will be set, + * the pipeline will run, and the listener specified in the constructor will be called to notify + * it that the pipeline ran. + * + *

This method is exposed to allow teams to add additional functionality or have their own + * ways to run the pipeline. Most teams, however, should just use {@link #runForever} in its own + * thread using a {@link VisionThread}.

+ */ + public void runOnce() { + Long id = CameraServerSharedStore.getCameraServerShared().getRobotMainThreadId(); + + if (id != null && Thread.currentThread().getId() == id.longValue()) { + throw new IllegalStateException( + "VisionRunner.runOnce() cannot be called from the main robot thread"); + } + runOnceInternal(); + } + + private void runOnceInternal() { + long frameTime = m_cvSink.grabFrame(m_image); + if (frameTime == 0) { + // There was an error, report it + String error = m_cvSink.getError(); + CameraServerSharedStore.getCameraServerShared().reportDriverStationError(error); + } else { + // No errors, process the image + m_pipeline.process(m_image); + m_listener.copyPipelineOutputs(m_pipeline); + } + } + + /** + * A convenience method that calls {@link #runOnce()} in an infinite loop. This must + * be run in a dedicated thread, and cannot be used in the main robot thread because + * it will freeze the robot program. + * + *

Do not call this method directly from the main thread.

+ * + * @throws IllegalStateException if this is called from the main robot thread + * @see VisionThread + */ + public void runForever() { + Long id = CameraServerSharedStore.getCameraServerShared().getRobotMainThreadId(); + + if (id != null && Thread.currentThread().getId() == id.longValue()) { + throw new IllegalStateException( + "VisionRunner.runForever() cannot be called from the main robot thread"); + } + while (m_enabled && !Thread.interrupted()) { + runOnceInternal(); + } + } + + /** + * Stop a RunForever() loop. + */ + public void stop() { + m_enabled = false; + } +} diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/vision/VisionThread.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/vision/VisionThread.java new file mode 100644 index 0000000000..5184ba0fb3 --- /dev/null +++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/vision/VisionThread.java @@ -0,0 +1,50 @@ +/*----------------------------------------------------------------------------*/ +/* Copyright (c) 2016-2018 FIRST. All Rights Reserved. */ +/* Open Source Software - may be modified and shared by FRC teams. The code */ +/* must be accompanied by the FIRST BSD license file in the root directory of */ +/* the project. */ +/*----------------------------------------------------------------------------*/ + +package edu.wpi.first.wpilibj.vision; + +import edu.wpi.cscore.VideoSource; + +/** + * A vision thread is a special thread that runs a vision pipeline. It is a daemon thread; + * it does not prevent the program from exiting when all other non-daemon threads + * have finished running. + * + * @see VisionPipeline + * @see VisionRunner + * @see Thread#setDaemon(boolean) + * + * @deprecated Replaced with edu.wpi.first.vision.VisionThread + */ +@Deprecated +public class VisionThread extends Thread { + /** + * Creates a vision thread that continuously runs a {@link VisionPipeline}. + * + * @param visionRunner the runner for a vision pipeline + */ + public VisionThread(VisionRunner visionRunner) { + super(visionRunner::runForever, "WPILib Vision Thread"); + setDaemon(true); + } + + /** + * Creates a new vision thread that continuously runs the given vision pipeline. This is + * equivalent to {@code new VisionThread(new VisionRunner<>(videoSource, pipeline, listener))}. + * + * @param videoSource the source for images the pipeline should process + * @param pipeline the pipeline to run + * @param listener the listener to copy outputs from the pipeline after it runs + * @param

the type of the pipeline + */ + public

VisionThread(VideoSource videoSource, + P pipeline, + VisionRunner.Listener listener) { + this(new VisionRunner<>(videoSource, pipeline, listener)); + } + +} diff --git a/cameraserver/src/main/java/edu/wpi/first/wpilibj/vision/package-info.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/vision/package-info.java similarity index 99% rename from cameraserver/src/main/java/edu/wpi/first/wpilibj/vision/package-info.java rename to wpilibj/src/main/java/edu/wpi/first/wpilibj/vision/package-info.java index 94d7101a38..01c8d73be9 100644 --- a/cameraserver/src/main/java/edu/wpi/first/wpilibj/vision/package-info.java +++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/vision/package-info.java @@ -85,4 +85,5 @@ * } * */ +@java.lang.Deprecated package edu.wpi.first.wpilibj.vision; diff --git a/wpilibjExamples/src/main/java/edu/wpi/first/wpilibj/examples/axiscamera/Robot.java b/wpilibjExamples/src/main/java/edu/wpi/first/wpilibj/examples/axiscamera/Robot.java index c860cf080f..ee4b42451c 100644 --- a/wpilibjExamples/src/main/java/edu/wpi/first/wpilibj/examples/axiscamera/Robot.java +++ b/wpilibjExamples/src/main/java/edu/wpi/first/wpilibj/examples/axiscamera/Robot.java @@ -15,7 +15,7 @@ import org.opencv.imgproc.Imgproc; import edu.wpi.cscore.AxisCamera; import edu.wpi.cscore.CvSink; import edu.wpi.cscore.CvSource; -import edu.wpi.first.wpilibj.CameraServer; +import edu.wpi.first.cameraserver.CameraServer; import edu.wpi.first.wpilibj.TimedRobot; /** diff --git a/wpilibjExamples/src/main/java/edu/wpi/first/wpilibj/examples/intermediatevision/Robot.java b/wpilibjExamples/src/main/java/edu/wpi/first/wpilibj/examples/intermediatevision/Robot.java index 8910eb2ae8..526e849e1f 100644 --- a/wpilibjExamples/src/main/java/edu/wpi/first/wpilibj/examples/intermediatevision/Robot.java +++ b/wpilibjExamples/src/main/java/edu/wpi/first/wpilibj/examples/intermediatevision/Robot.java @@ -15,7 +15,7 @@ import org.opencv.imgproc.Imgproc; import edu.wpi.cscore.CvSink; import edu.wpi.cscore.CvSource; import edu.wpi.cscore.UsbCamera; -import edu.wpi.first.wpilibj.CameraServer; +import edu.wpi.first.cameraserver.CameraServer; import edu.wpi.first.wpilibj.TimedRobot; /** diff --git a/wpilibjExamples/src/main/java/edu/wpi/first/wpilibj/examples/quickvision/Robot.java b/wpilibjExamples/src/main/java/edu/wpi/first/wpilibj/examples/quickvision/Robot.java index 1b40754e2c..9816f137e9 100644 --- a/wpilibjExamples/src/main/java/edu/wpi/first/wpilibj/examples/quickvision/Robot.java +++ b/wpilibjExamples/src/main/java/edu/wpi/first/wpilibj/examples/quickvision/Robot.java @@ -7,7 +7,7 @@ package edu.wpi.first.wpilibj.examples.quickvision; -import edu.wpi.first.wpilibj.CameraServer; +import edu.wpi.first.cameraserver.CameraServer; import edu.wpi.first.wpilibj.TimedRobot; /**