001/* 002 * Copyright (C) Photon Vision. 003 * 004 * This program is free software: you can redistribute it and/or modify 005 * it under the terms of the GNU General Public License as published by 006 * the Free Software Foundation, either version 3 of the License, or 007 * (at your option) any later version. 008 * 009 * This program is distributed in the hope that it will be useful, 010 * but WITHOUT ANY WARRANTY; without even the implied warranty of 011 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 012 * GNU General Public License for more details. 013 * 014 * You should have received a copy of the GNU General Public License 015 * along with this program. If not, see <https://www.gnu.org/licenses/>. 016 */ 017 018package org.photonvision.vision.camera.USBCameras; 019 020import edu.wpi.first.cscore.UsbCamera; 021import edu.wpi.first.cscore.VideoException; 022import edu.wpi.first.cscore.VideoMode; 023import edu.wpi.first.cscore.VideoProperty; 024import edu.wpi.first.math.MathUtil; 025import edu.wpi.first.util.PixelFormat; 026import java.util.ArrayList; 027import java.util.Arrays; 028import java.util.Collections; 029import java.util.HashMap; 030import java.util.List; 031import java.util.Optional; 032import java.util.stream.Collectors; 033import org.photonvision.common.configuration.CameraConfiguration; 034import org.photonvision.common.configuration.ConfigManager; 035import org.photonvision.vision.camera.CameraQuirk; 036import org.photonvision.vision.processes.VisionSourceSettables; 037 038public class GenericUSBCameraSettables extends VisionSourceSettables { 039 // We need to remember the last exposure set when exiting 040 // auto exposure mode so we can restore it 041 protected double lastExposureRaw = -1; 042 043 // Some cameras need logic where we re-apply brightness after 044 // changing exposure 045 protected int lastBrightness = -1; 046 047 protected VideoProperty exposureAbsProp = null; 048 protected VideoProperty autoExposureProp = null; 049 protected VideoProperty wbTempProp = null; 050 051 protected double minExposure = 1; 052 protected double maxExposure = 80000; 053 054 protected double minWhiteBalanceTemp = 1; 055 protected double maxWhiteBalanceTemp = 4000; 056 protected int lastWhiteBalanceTemp = 4000; 057 058 protected static final int PROP_AUTO_EXPOSURE_ENABLED = 3; 059 protected static final int PROP_AUTO_EXPOSURE_DISABLED = 1; 060 061 protected UsbCamera camera; 062 protected CameraConfiguration configuration; 063 064 public GenericUSBCameraSettables(CameraConfiguration configuration, UsbCamera camera) { 065 super(configuration); 066 067 this.configuration = configuration; 068 this.camera = camera; 069 070 // TODO - how should this work post-refactor??? 071 if (!configuration.cameraQuirks.hasQuirk(CameraQuirk.StickyFPS)) { 072 if (!videoModes.isEmpty()) { 073 setVideoMode(videoModes.get(0)); // fixes double FPS set 074 } 075 } 076 } 077 078 protected void setUpWhiteBalanceProperties() { 079 wbTempProp = findProperty("white_balance_temperature", "WhiteBalance").orElse(null); 080 if (wbTempProp != null) { 081 this.minWhiteBalanceTemp = wbTempProp.getMin(); 082 this.maxWhiteBalanceTemp = wbTempProp.getMax(); 083 } 084 } 085 086 protected void setUpExposureProperties() { 087 // Photonvision needs to be able to control absolute exposure. Make sure we can 088 // first. 089 var expProp = 090 findProperty( 091 "raw_exposure_absolute", "raw_exposure_time_absolute", "exposure", "raw_Exposure"); 092 093 if (expProp.isEmpty()) { 094 logger.warn("Could not find exposure property"); 095 return; 096 } else { 097 exposureAbsProp = expProp.get(); 098 this.minExposure = exposureAbsProp.getMin(); 099 this.maxExposure = exposureAbsProp.getMax(); 100 } 101 102 // Photonvision needs to be able to control auto exposure. Make sure we can 103 // first. 104 var autoExpProp = findProperty("exposure_auto", "auto_exposure"); 105 106 if (expProp.isPresent()) { 107 exposureAbsProp = expProp.get(); 108 this.minExposure = exposureAbsProp.getMin(); 109 this.maxExposure = exposureAbsProp.getMax(); 110 } 111 112 if (autoExpProp.isPresent()) { 113 autoExposureProp = autoExpProp.get(); 114 } 115 } 116 117 public void setAllCamDefaults() { 118 // Common settings for all cameras to attempt to get their image 119 // as close as possible to what we want for image processing 120 softSet("image_stabilization", 0); // No image stabilization, as this will throw off odometry 121 softSet("power_line_frequency", 2); // Assume 60Hz USA 122 softSet("scene_mode", 0); // no presets 123 softSet("exposure_metering_mode", 0); 124 softSet("exposure_dynamic_framerate", 0); 125 softSet("focus_auto", 0); 126 softSet("focus_absolute", 0); // Focus into infinity 127 } 128 129 @Override 130 public void setWhiteBalanceTemp(double tempNumber) { 131 if (wbTempProp == null) { 132 // bail 133 return; 134 } 135 136 try { 137 int temp = (int) Math.round(tempNumber); 138 139 softSet("white_balance_automatic", 0); 140 141 int propVal = (int) MathUtil.clamp(temp, minWhiteBalanceTemp, maxWhiteBalanceTemp); 142 143 logger.debug( 144 "Setting property " 145 + wbTempProp.getName() 146 + " to " 147 + propVal 148 + " (user requested " 149 + temp 150 + " degrees)"); 151 152 wbTempProp.set(propVal); 153 154 this.lastWhiteBalanceTemp = temp; 155 156 } catch (VideoException e) { 157 logger.error("Failed to set camera exposure!", e); 158 } 159 } 160 161 @Override 162 public void setAutoWhiteBalance(boolean autoWB) { 163 logger.debug("Setting auto white balance to " + autoWB); 164 165 if (autoWB) { 166 // Seems to be a rpi-specific property? 167 softSet("white_balance_automatic", 1); 168 } else { 169 softSet("white_balance_automatic", 0); 170 171 if (wbTempProp != null) { 172 wbTempProp.set(this.lastWhiteBalanceTemp); 173 } 174 } 175 } 176 177 public void setAutoExposure(boolean cameraAutoExposure) { 178 logger.debug("Setting auto exposure to " + cameraAutoExposure); 179 180 if (!cameraAutoExposure) { 181 // Pick a bunch of reasonable setting defaults for vision processing 182 softSet("auto_exposure_bias", 0); 183 softSet("iso_sensitivity_auto", 0); // Disable auto ISO adjustment 184 softSet("iso_sensitivity", 0); // Manual ISO adjustment 185 if (autoExposureProp != null) autoExposureProp.set(PROP_AUTO_EXPOSURE_DISABLED); 186 187 // Most cameras leave exposure time absolute at the last value from their AE 188 // algorithm. 189 // Set it back to the exposure slider value 190 setExposureRaw(this.lastExposureRaw); 191 192 } else { 193 // Pick a bunch of reasonable setting to make the picture nice-for-humans 194 softSet("auto_exposure_bias", 12); 195 softSet("iso_sensitivity_auto", 1); 196 softSet("iso_sensitivity", 1); // Manual ISO adjustment by default 197 if (autoExposureProp != null) autoExposureProp.set(PROP_AUTO_EXPOSURE_ENABLED); 198 } 199 } 200 201 @Override 202 public double getMinExposureRaw() { 203 return minExposure; 204 } 205 206 @Override 207 public double getMaxExposureRaw() { 208 return maxExposure; 209 } 210 211 @Override 212 public void setExposureRaw(double exposureRaw) { 213 if (exposureRaw >= 0.0) { 214 try { 215 if (autoExposureProp != null) autoExposureProp.set(PROP_AUTO_EXPOSURE_DISABLED); 216 217 int propVal = (int) MathUtil.clamp(exposureRaw, minExposure, maxExposure); 218 219 logger.debug( 220 "Setting property " 221 + exposureAbsProp.getName() 222 + " to " 223 + propVal 224 + " (user requested " 225 + exposureRaw 226 + " μs)"); 227 228 exposureAbsProp.set(propVal); 229 230 this.lastExposureRaw = exposureRaw; 231 232 } catch (VideoException e) { 233 logger.error("Failed to set camera exposure!", e); 234 } 235 } 236 } 237 238 @Override 239 public void setBrightness(int brightness) { 240 try { 241 camera.setBrightness(brightness); 242 this.lastBrightness = brightness; 243 } catch (VideoException e) { 244 logger.error("Failed to set camera brightness!", e); 245 } 246 } 247 248 @Override 249 public void setGain(int gain) { 250 softSet("gain_automatic", 0); 251 softSet("gain", gain); 252 } 253 254 @Override 255 public VideoMode getCurrentVideoMode() { 256 return camera 257 .getVideoMode(); // This returns the current video mode even if the camera is disconnected 258 } 259 260 @Override 261 public void setVideoModeInternal(VideoMode videoMode) { 262 try { 263 if (videoMode == null) { 264 logger.error("Got a null video mode! Doing nothing..."); 265 return; 266 } 267 if (camera.setVideoMode(videoMode)) logger.debug("Failed to set video mode!"); 268 } catch (Exception e) { 269 logger.error("Failed to set video mode!", e); 270 } 271 } 272 273 private void cacheVideoModes() { 274 videoModes = new HashMap<>(); 275 List<VideoMode> videoModesList = new ArrayList<>(); 276 try { 277 VideoMode[] modes; 278 279 modes = camera.enumerateVideoModes(); 280 281 for (VideoMode videoMode : modes) { 282 // Filter grey modes 283 if (videoMode.pixelFormat == PixelFormat.kGray 284 || videoMode.pixelFormat == PixelFormat.kUnknown) { 285 continue; 286 } 287 288 if (configuration.cameraQuirks.hasQuirk(CameraQuirk.FPSCap100)) { 289 if (videoMode.fps > 100) { 290 continue; 291 } 292 } 293 294 videoModesList.add(videoMode); 295 } 296 } catch (Exception e) { 297 logger.error("Exception while enumerating video modes!", e); 298 videoModesList = List.of(); 299 } 300 301 // Sort by resolution 302 var sortedList = 303 videoModesList.stream() 304 .distinct() // remove redundant video mode entries 305 .sorted(((a, b) -> (b.width + b.height) - (a.width + a.height))) 306 .collect(Collectors.toList()); 307 Collections.reverse(sortedList); 308 309 // On vendor cameras, respect blacklisted indices 310 var indexBlacklist = 311 ConfigManager.getInstance().getConfig().getHardwareConfig().blacklistedResIndices; 312 for (int badIdx : indexBlacklist) { 313 sortedList.remove(badIdx); 314 } 315 316 for (VideoMode videoMode : sortedList) { 317 videoModes.put(sortedList.indexOf(videoMode), videoMode); 318 } 319 320 // If after all that we still have no video modes, not much we can do besides 321 // throw up our hands 322 if (videoModes.isEmpty()) { 323 logger.info("Camera " + camera.getPath() + " has no video modes supported by PhotonVision"); 324 } 325 } 326 327 @Override 328 public HashMap<Integer, VideoMode> getAllVideoModes() { 329 if (!cameraPropertiesCached) { 330 // Device hasn't connected at least once, best I can do is given up 331 logger.warn("Device hasn't connected, cannot enumerate video modes"); 332 return new HashMap<>(); 333 } 334 335 return videoModes; 336 } 337 338 /** 339 * Forgiving "set this property" action. Produces a debug message but skips properties if they 340 * aren't supported Errors if the property exists but the set fails. 341 * 342 * @param property 343 * @param value 344 */ 345 protected void softSet(String property, int value) { 346 VideoProperty prop = camera.getProperty(property); 347 if (prop.getKind() == VideoProperty.Kind.kNone) { 348 logger.debug("No property " + property + " for " + camera.getName() + " , skipping."); 349 } else { 350 try { 351 prop.set(value); 352 } catch (VideoException e) { 353 logger.error("Failed to set " + property + " for " + camera.getName() + " !", e); 354 } 355 } 356 } 357 358 /** 359 * Returns the first property with a name in the list. Useful to find gandolf property that goes 360 * by many names in different os/releases/whatever 361 * 362 * @param options 363 * @return 364 */ 365 protected Optional<VideoProperty> findProperty(String... options) { 366 VideoProperty retProp = null; 367 boolean found = false; 368 for (var option : options) { 369 retProp = camera.getProperty(option); 370 if (retProp.getKind() != VideoProperty.Kind.kNone) { 371 // got em 372 found = true; 373 break; 374 } 375 } 376 377 if (!found) { 378 logger.warn( 379 "Expected at least one of the following properties to be available: " 380 + Arrays.toString(options)); 381 retProp = null; 382 } 383 384 return Optional.ofNullable(retProp); 385 } 386 387 @Override 388 public double getMaxWhiteBalanceTemp() { 389 return maxWhiteBalanceTemp; 390 } 391 392 @Override 393 public double getMinWhiteBalanceTemp() { 394 return minWhiteBalanceTemp; 395 } 396 397 @Override 398 public void onCameraConnected() { 399 super.onCameraConnected(); 400 401 logger.info("Caching cscore properties"); 402 403 // Now that our device is actually connected, we can enumerate properties/video 404 // modes 405 setUpExposureProperties(); 406 setUpWhiteBalanceProperties(); 407 cacheVideoModes(); 408 409 setAllCamDefaults(); 410 411 calculateFrameStaticProps(); 412 } 413}