001/*----------------------------------------------------------------------------*/ 002/* Copyright (c) FIRST 2014. All Rights Reserved. */ 003/* Open Source Software - may be modified and shared by FRC teams. The code */ 004/* must be accompanied by the FIRST BSD license file in the root directory of */ 005/* the project. */ 006/*----------------------------------------------------------------------------*/ 007 008package edu.wpi.first.wpilibj.vision; 009 010import edu.wpi.first.wpilibj.image.ColorImage; 011import edu.wpi.first.wpilibj.image.HSLImage; 012import edu.wpi.first.wpilibj.image.NIVisionException; 013 014import java.io.DataInputStream; 015import java.io.IOException; 016import java.io.OutputStream; 017import java.net.InetSocketAddress; 018import java.net.Socket; 019import java.nio.ByteBuffer; 020 021import static com.ni.vision.NIVision.Image; 022import static com.ni.vision.NIVision.Priv_ReadJPEGString_C; 023import static edu.wpi.first.wpilibj.Timer.delay; 024 025/** 026 * Axis M1011 network camera 027 */ 028public class AxisCamera { 029 public enum WhiteBalance { 030 kAutomatic, 031 kHold, 032 kFixedOutdoor1, 033 kFixedOutdoor2, 034 kFixedIndoor, 035 kFixedFluorescent1, 036 kFixedFluorescent2, 037 } 038 039 public enum ExposureControl { 040 kAutomatic, 041 kHold, 042 kFlickerFree50Hz, 043 kFlickerFree60Hz, 044 } 045 046 public enum Resolution { 047 k640x480, 048 k480x360, 049 k320x240, 050 k240x180, 051 k176x144, 052 k160x120, 053 } 054 055 public enum Rotation { 056 k0, k180 057 } 058 059 private static final String[] kWhiteBalanceStrings = 060 {"auto", "hold", "fixed_outdoor1", "fixed_outdoor2", "fixed_indoor", 061 "fixed_fluor1", "fixed_fluor2",}; 062 063 private static final String[] kExposureControlStrings = 064 {"auto", "hold", "flickerfree50", "flickerfree60",}; 065 066 private static final String[] kResolutionStrings = 067 {"640x480", "480x360", "320x240", "240x180", "176x144", "160x120",}; 068 069 private static final String[] kRotationStrings = 070 {"0", "180",}; 071 072 private final static int kImageBufferAllocationIncrement = 1000; 073 074 private String m_cameraHost; 075 private Socket m_cameraSocket; 076 077 private ByteBuffer m_imageData = ByteBuffer.allocate(5000); 078 private final Object m_imageDataLock = new Object(); 079 private boolean m_freshImage = false; 080 081 private int m_brightness = 50; 082 private WhiteBalance m_whiteBalance = WhiteBalance.kAutomatic; 083 private int m_colorLevel = 50; 084 private ExposureControl m_exposureControl = ExposureControl.kAutomatic; 085 private int m_exposurePriority = 50; 086 private int m_maxFPS = 0; 087 private Resolution m_resolution = Resolution.k640x480; 088 private int m_compression = 50; 089 private Rotation m_rotation = Rotation.k0; 090 private final Object m_parametersLock = new Object(); 091 private boolean m_parametersDirty = true; 092 private boolean m_streamDirty = true; 093 094 private boolean m_done = false; 095 096 /** 097 * AxisCamera constructor 098 * 099 * @param cameraHost The host to find the camera at, typically an IP address 100 */ 101 public AxisCamera(String cameraHost) { 102 m_cameraHost = cameraHost; 103 m_captureThread.start(); 104 } 105 106 /** 107 * Return true if the latest image from the camera has not been retrieved by calling GetImage() yet. 108 * @return true if the image has not been retrieved yet. 109 */ 110 public boolean isFreshImage() { 111 return m_freshImage; 112 } 113 114 /** 115 * Get an image from the camera and store it in the provided image. 116 * 117 * @param image The imaq image to store the result in. This must be an HSL or RGB image. 118 * @return <code>true</code> upon success, <code>false</code> on a failure 119 */ 120 public boolean getImage(Image image) { 121 if (m_imageData.limit() == 0) { 122 return false; 123 } 124 125 synchronized (m_imageDataLock) { 126 Priv_ReadJPEGString_C(image, m_imageData.array()); 127 } 128 129 m_freshImage = false; 130 131 return true; 132 } 133 134 /** 135 * Get an image from the camera and store it in the provided image. 136 * 137 * @param image The image to store the result in. This must be an HSL or RGB image 138 * @return true upon success, false on a failure 139 */ 140 public boolean getImage(ColorImage image) { 141 return this.getImage(image.image); 142 } 143 144 /** 145 * Instantiate a new image object and fill it with the latest image from the camera. 146 * 147 * @return a pointer to an HSLImage object 148 */ 149 public HSLImage getImage() throws NIVisionException { 150 HSLImage image = new HSLImage(); 151 this.getImage(image); 152 return image; 153 } 154 155 /** 156 * Request a change in the brightness of the camera images. 157 * 158 * @param brightness valid values 0 .. 100 159 */ 160 public void writeBrightness(int brightness) { 161 if (brightness < 0 || brightness > 100) { 162 throw new IllegalArgumentException("Brightness must be from 0 to 100"); 163 } 164 165 synchronized (m_parametersLock) { 166 if (m_brightness != brightness) { 167 m_brightness = brightness; 168 m_parametersDirty = true; 169 } 170 } 171 } 172 173 /** 174 * @return The configured brightness of the camera images 175 */ 176 public int getBrightness() { 177 synchronized (m_parametersLock) { 178 return m_brightness; 179 } 180 } 181 182 /** 183 * Request a change in the white balance on the camera. 184 * 185 * @param whiteBalance Valid values from the <code>WhiteBalance</code> enum. 186 */ 187 public void writeWhiteBalance(WhiteBalance whiteBalance) { 188 synchronized (m_parametersLock) { 189 if (m_whiteBalance != whiteBalance) { 190 m_whiteBalance = whiteBalance; 191 m_parametersDirty = true; 192 } 193 } 194 } 195 196 /** 197 * @return The configured white balances of the camera images 198 */ 199 public WhiteBalance getWhiteBalance() { 200 synchronized (m_parametersLock) { 201 return m_whiteBalance; 202 } 203 } 204 205 /** 206 * Request a change in the color level of the camera images. 207 * 208 * @param colorLevel valid values are 0 .. 100 209 */ 210 public void writeColorLevel(int colorLevel) { 211 if (colorLevel < 0 || colorLevel > 100) { 212 throw new IllegalArgumentException("Color level must be from 0 to 100"); 213 } 214 215 synchronized (m_parametersLock) { 216 if (m_colorLevel != colorLevel) { 217 m_colorLevel = colorLevel; 218 m_parametersDirty = true; 219 } 220 } 221 } 222 223 /** 224 * @return The configured color level of the camera images 225 */ 226 public int getColorLevel() { 227 synchronized (m_parametersLock) { 228 return m_colorLevel; 229 } 230 } 231 232 /** 233 * Request a change in the camera's exposure mode. 234 * 235 * @param exposureControl A mode to write in the <code>Exposure</code> enum. 236 */ 237 public void writeExposureControl(ExposureControl exposureControl) { 238 synchronized (m_parametersLock) { 239 if (m_exposureControl != exposureControl) { 240 m_exposureControl = exposureControl; 241 m_parametersDirty = true; 242 } 243 } 244 } 245 246 /** 247 * @return The configured exposure control mode of the camera 248 */ 249 public ExposureControl getExposureControl() { 250 synchronized (m_parametersLock) { 251 return m_exposureControl; 252 } 253 } 254 255 /** 256 * Request a change in the exposure priority of the camera. 257 * 258 * @param exposurePriority Valid values are 0, 50, 100. 259 * 0 = Prioritize image quality 260 * 50 = None 261 * 100 = Prioritize frame rate 262 */ 263 public void writeExposurePriority(int exposurePriority) { 264 if (exposurePriority != 0 && exposurePriority != 50 && exposurePriority != 100) { 265 throw new IllegalArgumentException("Exposure priority must be 0, 50, or 100"); 266 } 267 268 synchronized (m_parametersLock) { 269 if (m_exposurePriority != exposurePriority) { 270 m_exposurePriority = exposurePriority; 271 m_parametersDirty = true; 272 } 273 } 274 } 275 276 /** 277 * @return The configured exposure priority of the camera 278 */ 279 public int getExposurePriority() { 280 synchronized (m_parametersLock) { 281 return m_exposurePriority; 282 } 283 } 284 285 /** 286 * Write the maximum frames per second that the camera should send 287 * Write 0 to send as many as possible. 288 * 289 * @param maxFPS The number of frames the camera should send in a second, exposure permitting. 290 */ 291 public void writeMaxFPS(int maxFPS) { 292 synchronized (m_parametersLock) { 293 if (m_maxFPS != maxFPS) { 294 m_maxFPS = maxFPS; 295 m_parametersDirty = true; 296 m_streamDirty = true; 297 } 298 } 299 } 300 301 /** 302 * @return The configured maximum FPS of the camera 303 */ 304 public int getMaxFPS() { 305 synchronized (m_parametersLock) { 306 return m_maxFPS; 307 } 308 } 309 310 /** 311 * Write resolution value to camera. 312 * 313 * @param resolution The camera resolution value to write to the camera. 314 */ 315 public void writeResolution(Resolution resolution) { 316 synchronized (m_parametersLock) { 317 if (m_resolution != resolution) { 318 m_resolution = resolution; 319 m_parametersDirty = true; 320 m_streamDirty = true; 321 } 322 } 323 } 324 325 /** 326 * @return The configured resolution of the camera (not necessarily the same 327 * resolution as the most recent image, if it was changed recently.) 328 */ 329 public Resolution getResolution() { 330 synchronized (m_parametersLock) { 331 return m_resolution; 332 } 333 } 334 335 /** 336 * Write the compression value to the camera. 337 * 338 * @param compression Values between 0 and 100. 339 */ 340 public void writeCompression(int compression) { 341 if (compression < 0 || compression > 100) { 342 throw new IllegalArgumentException("Compression must be from 0 to 100"); 343 } 344 345 synchronized (m_parametersLock) { 346 if (m_compression != compression) { 347 m_compression = compression; 348 m_parametersDirty = true; 349 m_streamDirty = true; 350 } 351 } 352 } 353 354 /** 355 * @return The configured compression level of the camera images 356 */ 357 public int getCompression() { 358 synchronized (m_parametersLock) { 359 return m_compression; 360 } 361 } 362 363 /** 364 * Write the rotation value to the camera. 365 * If you mount your camera upside down, use this to adjust the image for you. 366 * 367 * @param rotation A value from the {@link Rotation} enum 368 */ 369 public void writeRotation(Rotation rotation) { 370 synchronized (m_parametersLock) { 371 if (m_rotation != rotation) { 372 m_rotation = rotation; 373 m_parametersDirty = true; 374 m_streamDirty = true; 375 } 376 } 377 } 378 379 /** 380 * @return The configured rotation mode of the camera 381 */ 382 public Rotation getRotation() { 383 synchronized (m_parametersLock) { 384 return m_rotation; 385 } 386 } 387 388 /** 389 * Thread spawned by AxisCamera constructor to receive images from cam 390 */ 391 private Thread m_captureThread = new Thread(new Runnable() { 392 @Override 393 public void run() { 394 int consecutiveErrors = 0; 395 396 // Loop on trying to setup the camera connection. This happens in a background 397 // thread so it shouldn't effect the operation of user programs. 398 while (!m_done) { 399 String requestString = "GET /mjpg/video.mjpg HTTP/1.1\n" + 400 "User-Agent: HTTPStreamClient\n" + 401 "Connection: Keep-Alive\n" + 402 "Cache-Control: no-cache\n" + 403 "Authorization: Basic RlJDOkZSQw==\n\n"; 404 405 try { 406 m_cameraSocket = AxisCamera.this.createCameraSocket(requestString); 407 AxisCamera.this.readImagesFromCamera(); 408 consecutiveErrors = 0; 409 } catch (IOException e) { 410 consecutiveErrors++; 411 412 if (consecutiveErrors > 5) { 413 e.printStackTrace(); 414 } 415 } 416 417 delay(0.5); 418 } 419 } 420 }); 421 422 /** 423 * This function actually reads the images from the camera. 424 */ 425 private void readImagesFromCamera() throws IOException { 426 DataInputStream cameraInputStream = new DataInputStream(m_cameraSocket.getInputStream()); 427 428 while (!m_done) { 429 String line = cameraInputStream.readLine(); 430 431 if (line.startsWith("Content-Length: ")) { 432 int contentLength = Integer.valueOf(line.substring(16)); 433 434 /* Skip the next blank line */ 435 cameraInputStream.readLine(); 436 contentLength -= 4; 437 438 /* The next four bytes are the JPEG magic number */ 439 byte[] data = new byte[contentLength]; 440 cameraInputStream.readFully(data); 441 442 synchronized (m_imageDataLock) { 443 if (m_imageData.capacity() < data.length) { 444 m_imageData = ByteBuffer.allocate(data.length + kImageBufferAllocationIncrement); 445 } 446 447 m_imageData.clear(); 448 m_imageData.limit(contentLength); 449 m_imageData.put(data); 450 451 m_freshImage = true; 452 } 453 454 if (this.writeParameters()) { 455 break; 456 } 457 458 /* Skip the boundary and Content-Type header */ 459 cameraInputStream.readLine(); 460 cameraInputStream.readLine(); 461 } 462 } 463 464 m_cameraSocket.close(); 465 } 466 467 /** 468 * Send a request to the camera to set all of the parameters. This is called 469 * in the capture thread between each frame. This strategy avoids making lots 470 * of redundant HTTP requests, accounts for failed initial requests, and 471 * avoids blocking calls in the main thread unless necessary. 472 * <p> 473 * This method does nothing if no parameters have been modified since it last 474 * completely successfully. 475 * 476 * @return <code>true</code> if the stream should be restarted due to a 477 * parameter changing. 478 */ 479 private boolean writeParameters() { 480 if (m_parametersDirty) { 481 String request = "GET /axis-cgi/admin/param.cgi?action=update"; 482 483 synchronized (m_parametersLock) { 484 request += "&ImageSource.I0.Sensor.Brightness=" + m_brightness; 485 request += "&ImageSource.I0.Sensor.WhiteBalance=" + kWhiteBalanceStrings[m_whiteBalance.ordinal()]; 486 request += "&ImageSource.I0.Sensor.ColorLevel=" + m_colorLevel; 487 request += "&ImageSource.I0.Sensor.Exposure=" + kExposureControlStrings[m_exposureControl.ordinal()]; 488 request += "&ImageSource.I0.Sensor.ExposurePriority=" + m_exposurePriority; 489 request += "&Image.I0.Stream.FPS=" + m_maxFPS; 490 request += "&Image.I0.Appearance.Resolution=" + kResolutionStrings[m_resolution.ordinal()]; 491 request += "&Image.I0.Appearance.Compression=" + m_compression; 492 request += "&Image.I0.Appearance.Rotation=" + kRotationStrings[m_rotation.ordinal()]; 493 } 494 495 request += " HTTP/1.1\n"; 496 request += "User-Agent: HTTPStreamClient\n"; 497 request += "Connection: Keep-Alive\n"; 498 request += "Cache-Control: no-cache\n"; 499 request += "Authorization: Basic RlJDOkZSQw==\n\n"; 500 501 try { 502 Socket socket = this.createCameraSocket(request); 503 socket.close(); 504 505 m_parametersDirty = false; 506 507 if (m_streamDirty) { 508 m_streamDirty = false; 509 return true; 510 } else { 511 return false; 512 } 513 } catch (IOException | NullPointerException e) { 514 return false; 515 } 516 517 } 518 519 return false; 520 } 521 522 /** 523 * Create a socket connected to camera 524 * Used to create a connection for reading images and setting parameters 525 * 526 * @param requestString The initial request string to send upon successful connection. 527 * @return The created socket 528 */ 529 private Socket createCameraSocket(String requestString) throws IOException { 530 /* Connect to the server */ 531 Socket socket = new Socket(); 532 socket.connect(new InetSocketAddress(m_cameraHost, 80), 5000); 533 534 /* Send the HTTP headers */ 535 OutputStream socketOutputStream = socket.getOutputStream(); 536 socketOutputStream.write(requestString.getBytes()); 537 538 return socket; 539 } 540 541 @Override 542 public String toString() { 543 return "AxisCamera{" + 544 "FreshImage=" + isFreshImage() + 545 ", Brightness=" + getBrightness() + 546 ", WhiteBalance=" + getWhiteBalance() + 547 ", ColorLevel=" + getColorLevel() + 548 ", ExposureControl=" + getExposureControl() + 549 ", ExposurePriority=" + getExposurePriority() + 550 ", MaxFPS=" + getMaxFPS() + 551 ", Resolution=" + getResolution() + 552 ", Compression=" + getCompression() + 553 ", Rotation=" + getRotation() + 554 '}'; 555 } 556}