001package edu.wpi.first.wpilibj; 002 003import java.io.DataInputStream; 004import java.io.DataOutputStream; 005import java.io.IOException; 006import java.io.OutputStream; 007import java.net.InetSocketAddress; 008import java.net.ServerSocket; 009import java.net.Socket; 010import java.nio.ByteBuffer; 011import java.nio.ByteOrder; 012import java.util.ArrayDeque; 013import java.util.ArrayList; 014import java.util.Deque; 015import java.util.List; 016import java.util.NoSuchElementException; 017import java.util.concurrent.atomic.AtomicBoolean; 018 019import com.ni.vision.NIVision; 020import com.ni.vision.NIVision.Image; 021import com.ni.vision.NIVision.RawData; 022import com.ni.vision.VisionException; 023 024import edu.wpi.first.wpilibj.DriverStation; 025import edu.wpi.first.wpilibj.Timer; 026import edu.wpi.first.wpilibj.vision.USBCamera; 027 028//replicates CameraServer.cpp in java lib 029 030public class CameraServer { 031 032 private static final int kPort = 1180; 033 private static final byte[] kMagicNumber = { 0x01, 0x00, 0x00, 0x00 }; 034 private static final int kSize640x480 = 0; 035 private static final int kSize320x240 = 1; 036 private static final int kSize160x120 = 2; 037 private static final int kHardwareCompression = -1; 038 private static final String kDefaultCameraName = "cam1"; 039 private static final int kMaxImageSize = 200000; 040 private static CameraServer server; 041 042 public static CameraServer getInstance() { 043 if (server == null) { 044 server = new CameraServer(); 045 } 046 return server; 047 } 048 049 private Thread serverThread; 050 private int m_quality; 051 private boolean m_autoCaptureStarted; 052 private boolean m_hwClient = true; 053 private USBCamera m_camera; 054 private CameraData m_imageData; 055 private Deque<ByteBuffer> m_imageDataPool; 056 057 private class CameraData { 058 RawData data; 059 int start; 060 public CameraData(RawData d, int s) { 061 data = d; 062 start = s; 063 } 064 } 065 066 private CameraServer() { 067 m_quality = 50; 068 m_camera = null; 069 m_imageData = null; 070 m_imageDataPool = new ArrayDeque<>(3); 071 for (int i = 0; i < 3; i++) { 072 m_imageDataPool.addLast(ByteBuffer.allocateDirect(kMaxImageSize)); 073 } 074 serverThread = new Thread(new Runnable() { 075 public void run() { 076 try { 077 serve(); 078 } catch (IOException e) { 079 // do stuff here 080 } catch (InterruptedException e) { 081 // do stuff here 082 } 083 } 084 }); 085 serverThread.setName("CameraServer Send Thread"); 086 serverThread.start(); 087 } 088 089 private synchronized void setImageData(RawData data, int start) { 090 if (m_imageData != null && m_imageData.data != null) { 091 m_imageData.data.free(); 092 if (m_imageData.data.getBuffer() != null) 093 m_imageDataPool.addLast(m_imageData.data.getBuffer()); 094 m_imageData = null; 095 } 096 m_imageData = new CameraData(data, start); 097 notifyAll(); 098 } 099 100 /** 101 * Manually change the image that is served by the MJPEG stream. This can be 102 * called to pass custom annotated images to the dashboard. Note that, for 103 * 640x480 video, this method could take between 40 and 50 milliseconds to 104 * complete. 105 * 106 * This shouldn't be called if {@link #startAutomaticCapture} is called. 107 * 108 * @param image 109 * The IMAQ image to show on the dashboard 110 */ 111 public void setImage(Image image) { 112 // handle multi-threadedness 113 114 /* Flatten the IMAQ image to a JPEG */ 115 116 RawData data = NIVision.imaqFlatten(image, 117 NIVision.FlattenType.FLATTEN_IMAGE, 118 NIVision.CompressionType.COMPRESSION_JPEG, 10 * m_quality); 119 ByteBuffer buffer = data.getBuffer(); 120 boolean hwClient; 121 122 synchronized (this) { 123 hwClient = m_hwClient; 124 } 125 126 /* Find the start of the JPEG data */ 127 int index = 0; 128 if (hwClient) { 129 while (index < buffer.limit() - 1) { 130 if ((buffer.get(index) & 0xff) == 0xFF 131 && (buffer.get(index + 1) & 0xff) == 0xD8) 132 break; 133 index++; 134 } 135 } 136 137 if (buffer.limit() - index - 1 <= 2) { 138 throw new VisionException("data size of flattened image is less than 2. Try another camera! "); 139 } 140 141 setImageData(data, index); 142 } 143 144 /** 145 * Start automatically capturing images to send to the dashboard. 146 * You should call this method to just see a camera feed on the dashboard 147 * without doing any vision processing on the roboRIO. {@link #setImage} 148 * shouldn't be called after this is called. 149 * This overload calles {@link #startAutomaticCapture(String)} with the 150 * default camera name 151 */ 152 public void startAutomaticCapture() { 153 startAutomaticCapture(USBCamera.kDefaultCameraName); 154 } 155 156 /** 157 * Start automatically capturing images to send to the dashboard. 158 * 159 * You should call this method to just see a camera feed on the dashboard 160 * without doing any vision processing on the roboRIO. {@link #setImage} 161 * shouldn't be called after this is called. 162 * 163 * @param cameraName 164 * The name of the camera interface (e.g. "cam1") 165 */ 166 public void startAutomaticCapture(String cameraName) { 167 USBCamera camera = new USBCamera(cameraName); 168 camera.openCamera(); 169 startAutomaticCapture(camera); 170 } 171 172 public synchronized void startAutomaticCapture(USBCamera camera) { 173 if (m_autoCaptureStarted) return; 174 m_autoCaptureStarted = true; 175 m_camera = camera; 176 177 m_camera.startCapture(); 178 179 Thread captureThread = new Thread(new Runnable() { 180 @Override 181 public void run() { 182 capture(); 183 } 184 }); 185 captureThread.setName("Camera Capture Thread"); 186 captureThread.start(); 187 } 188 189 protected void capture() { 190 Image frame = NIVision.imaqCreateImage(NIVision.ImageType.IMAGE_RGB, 0); 191 while (true) { 192 boolean hwClient; 193 ByteBuffer dataBuffer = null; 194 synchronized (this) { 195 hwClient = m_hwClient; 196 if (hwClient) { 197 dataBuffer = m_imageDataPool.removeLast(); 198 } 199 } 200 201 try { 202 if (hwClient && dataBuffer != null) { 203 // Reset the image buffer limit 204 dataBuffer.limit(dataBuffer.capacity() - 1); 205 m_camera.getImageData(dataBuffer); 206 setImageData(new RawData(dataBuffer), 0); 207 } else { 208 m_camera.getImage(frame); 209 setImage(frame); 210 } 211 } catch (VisionException ex) { 212 DriverStation.reportError("Error when getting image from the camera: " + ex.getMessage(), true); 213 if (dataBuffer != null) { 214 synchronized (this) { 215 m_imageDataPool.addLast(dataBuffer); 216 Timer.delay(.1); 217 } 218 } 219 } 220 } 221 } 222 223 224 225 /** 226 * check if auto capture is started 227 * 228 */ 229 public synchronized boolean isAutoCaptureStarted() { 230 return m_autoCaptureStarted; 231 } 232 233 /** 234 * Sets the size of the image to use. Use the public kSize constants 235 * to set the correct mode, or set it directory on a camera and call 236 * the appropriate autoCapture method 237 * @param size The size to use 238 */ 239 public synchronized void setSize(int size) { 240 if (m_camera == null) return; 241 switch (size) { 242 case kSize640x480: 243 m_camera.setSize(640, 480); 244 break; 245 case kSize320x240: 246 m_camera.setSize(320, 240); 247 break; 248 case kSize160x120: 249 m_camera.setSize(160, 120); 250 break; 251 } 252 } 253 254 /** 255 * Set the quality of the compressed image sent to the dashboard 256 * 257 * @param quality 258 * The quality of the JPEG image, from 0 to 100 259 */ 260 public synchronized void setQuality(int quality) { 261 m_quality = quality > 100 ? 100 : quality < 0 ? 0 : quality; 262 } 263 264 /** 265 * Get the quality of the compressed image sent to the dashboard 266 * 267 * @return The quality, from 0 to 100 268 */ 269 public synchronized int getQuality() { 270 return m_quality; 271 } 272 273 /** 274 * Run the M-JPEG server. 275 * 276 * This function listens for a connection from the dashboard in a background 277 * thread, then sends back the M-JPEG stream. 278 * 279 * @throws IOException if the Socket connection fails 280 * @throws InterruptedException if the sleep is interrupted 281 */ 282 protected void serve() throws IOException, InterruptedException { 283 284 ServerSocket socket = new ServerSocket(); 285 socket.setReuseAddress(true); 286 InetSocketAddress address = new InetSocketAddress(kPort); 287 socket.bind(address); 288 289 while (true) { 290 try { 291 Socket s = socket.accept(); 292 293 DataInputStream is = new DataInputStream(s.getInputStream()); 294 DataOutputStream os = new DataOutputStream(s.getOutputStream()); 295 296 int fps = is.readInt(); 297 int compression = is.readInt(); 298 int size = is.readInt(); 299 300 if (compression != kHardwareCompression) { 301 DriverStation.reportError("Choose \"USB Camera HW\" on the dashboard", false); 302 s.close(); 303 continue; 304 } 305 306 // Wait for the camera 307 synchronized (this) { 308 System.out.println("Camera not yet ready, awaiting image"); 309 if (m_camera == null) wait(); 310 m_hwClient = compression == kHardwareCompression; 311 if (!m_hwClient) setQuality(100 - compression); 312 else if (m_camera != null) m_camera.setFPS(fps); 313 setSize(size); 314 } 315 316 long period = (long) (1000 / (1.0 * fps)); 317 while (true) { 318 long t0 = System.currentTimeMillis(); 319 CameraData imageData = null; 320 synchronized (this) { 321 wait(); 322 imageData = m_imageData; 323 m_imageData = null; 324 } 325 326 if (imageData == null) continue; 327 // Set the buffer position to the start of the data, 328 // and then create a new wrapper for the data at 329 // exactly that position 330 imageData.data.getBuffer().position(imageData.start); 331 byte[] imageArray = new byte[imageData.data.getBuffer().remaining()]; 332 imageData.data.getBuffer().get(imageArray, 0, imageData.data.getBuffer().remaining()); 333 334 // write numbers 335 try { 336 os.write(kMagicNumber); 337 os.writeInt(imageArray.length); 338 os.write(imageArray); 339 os.flush(); 340 long dt = System.currentTimeMillis() - t0; 341 342 if (dt < period) { 343 Thread.sleep(period - dt); 344 } 345 } catch (IOException | UnsupportedOperationException ex) { 346 DriverStation.reportError(ex.getMessage(), true); 347 break; 348 } finally { 349 imageData.data.free(); 350 if (imageData.data.getBuffer() != null) { 351 synchronized (this) { 352 m_imageDataPool.addLast(imageData.data.getBuffer()); 353 } 354 } 355 } 356 } 357 } catch (IOException ex) { 358 DriverStation.reportError(ex.getMessage(), true); 359 continue; 360 } 361 } 362 } 363}