mirror of
https://github.com/PhotonVision/photonvision
synced 2026-06-23 01:21:40 +00:00
Move to using Absolute Exposure Range (#1352)
Uses logic in https://github.com/PhotonVision/photon-libcamera-gl-driver/pull/16 to push the ov9281 down to its true minimum exposure. Updates UI to list the exposure settings in ~~microseconds.~~ Native units - not everyone works in microseconds. Does its darndest to actually try to set the exposure in ~~microseconds.~~ Native Units. To do this... Lifecam is funky when doing this - [cscore limits the exposure settings to certain quantized values](https://github.com/wpilibsuite/allwpilib/blob/main/cscore/src/main/native/linux/UsbCameraImpl.cpp#L129). Add a new camera quirk to allow that. ~~Updated camera quirks to re-evaluate every camera load (rather than recalling from settings - this shouldn't be necessary)~~ This should be rolled back, needed for arducam type selection. Updated camera quirk matching logic to make PID/VID optional, and basename optional (and only match trailing characters). This enables mirroring CSCore's logic for identifying lifecams by name. Updated the USBCamera to primarily use cscore's exposed property names. Since camera manufacturers use a potpourri of names for the same thing.... For nice-to-have settings: new soft-set logic to try all possible names, but gracefully pass if the property isn't there. For required settings: Search a list for the first setting that's supported, fail if none are supported. More logging of camera properties to help debug. Note: most of this work is because cscore doesn't directly expose a massaged exposure-setting-absolute API (and, given what we've seen, probably _shouldn't_, this struggle is not for the faint of heart). --------- Co-authored-by: Matt <matthew.morley.ca@gmail.com>
This commit is contained in:
@@ -389,15 +389,17 @@ const setSelectedVideoFormat = (format: VideoFormat) => {
|
||||
<v-row v-if="isCalibrating">
|
||||
<v-col cols="12" class="pt-0">
|
||||
<pv-slider
|
||||
v-model="useCameraSettingsStore().currentPipelineSettings.cameraExposure"
|
||||
v-model="useCameraSettingsStore().currentPipelineSettings.cameraExposureRaw"
|
||||
:disabled="useCameraSettingsStore().currentCameraSettings.pipelineSettings.cameraAutoExposure"
|
||||
label="Exposure"
|
||||
tooltip="Directly controls how much light is allowed to fall onto the sensor, which affects apparent brightness"
|
||||
:min="0"
|
||||
:max="100"
|
||||
tooltip="Directly controls how long the camera shutter remains open. Units are dependant on the underlying driver."
|
||||
:min="useCameraSettingsStore().minExposureRaw"
|
||||
:max="useCameraSettingsStore().maxExposureRaw"
|
||||
:slider-cols="8"
|
||||
:step="0.1"
|
||||
@input="(args) => useCameraSettingsStore().changeCurrentPipelineSetting({ cameraExposure: args }, false)"
|
||||
:step="1"
|
||||
@input="
|
||||
(args) => useCameraSettingsStore().changeCurrentPipelineSetting({ cameraExposureRaw: args }, false)
|
||||
"
|
||||
/>
|
||||
<pv-slider
|
||||
v-model="useCameraSettingsStore().currentPipelineSettings.cameraBrightness"
|
||||
|
||||
@@ -13,32 +13,32 @@ const tempSettingsStruct = ref<CameraSettingsChangeRequest>({
|
||||
|
||||
const arducamSelectWrapper = computed<number>({
|
||||
get: () => {
|
||||
if (tempSettingsStruct.value.quirksToChange.ArduOV9281) return 1;
|
||||
else if (tempSettingsStruct.value.quirksToChange.ArduOV2311) return 2;
|
||||
else if (tempSettingsStruct.value.quirksToChange.ArduOV9782) return 3;
|
||||
if (tempSettingsStruct.value.quirksToChange.ArduOV9281Controls) return 1;
|
||||
else if (tempSettingsStruct.value.quirksToChange.ArduOV2311Controls) return 2;
|
||||
else if (tempSettingsStruct.value.quirksToChange.ArduOV9782Controls) return 3;
|
||||
else return 0;
|
||||
},
|
||||
set: (v) => {
|
||||
switch (v) {
|
||||
case 1:
|
||||
tempSettingsStruct.value.quirksToChange.ArduOV9281 = true;
|
||||
tempSettingsStruct.value.quirksToChange.ArduOV2311 = false;
|
||||
tempSettingsStruct.value.quirksToChange.ArduOV9782 = false;
|
||||
tempSettingsStruct.value.quirksToChange.ArduOV9281Controls = true;
|
||||
tempSettingsStruct.value.quirksToChange.ArduOV2311Controls = false;
|
||||
tempSettingsStruct.value.quirksToChange.ArduOV9782Controls = false;
|
||||
break;
|
||||
case 2:
|
||||
tempSettingsStruct.value.quirksToChange.ArduOV9281 = false;
|
||||
tempSettingsStruct.value.quirksToChange.ArduOV2311 = true;
|
||||
tempSettingsStruct.value.quirksToChange.ArduOV9782 = false;
|
||||
tempSettingsStruct.value.quirksToChange.ArduOV9281Controls = false;
|
||||
tempSettingsStruct.value.quirksToChange.ArduOV2311Controls = true;
|
||||
tempSettingsStruct.value.quirksToChange.ArduOV9782Controls = false;
|
||||
break;
|
||||
case 3:
|
||||
tempSettingsStruct.value.quirksToChange.ArduOV9281 = false;
|
||||
tempSettingsStruct.value.quirksToChange.ArduOV2311 = false;
|
||||
tempSettingsStruct.value.quirksToChange.ArduOV9782 = true;
|
||||
tempSettingsStruct.value.quirksToChange.ArduOV9281Controls = false;
|
||||
tempSettingsStruct.value.quirksToChange.ArduOV2311Controls = false;
|
||||
tempSettingsStruct.value.quirksToChange.ArduOV9782Controls = true;
|
||||
break;
|
||||
default:
|
||||
tempSettingsStruct.value.quirksToChange.ArduOV9281 = false;
|
||||
tempSettingsStruct.value.quirksToChange.ArduOV2311 = false;
|
||||
tempSettingsStruct.value.quirksToChange.ArduOV9782 = false;
|
||||
tempSettingsStruct.value.quirksToChange.ArduOV9281Controls = false;
|
||||
tempSettingsStruct.value.quirksToChange.ArduOV2311Controls = false;
|
||||
tempSettingsStruct.value.quirksToChange.ArduOV9782Controls = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -74,15 +74,15 @@ const interactiveCols = computed(() =>
|
||||
<template>
|
||||
<div>
|
||||
<pv-slider
|
||||
v-model="useCameraSettingsStore().currentPipelineSettings.cameraExposure"
|
||||
v-model="useCameraSettingsStore().currentPipelineSettings.cameraExposureRaw"
|
||||
:disabled="useCameraSettingsStore().currentCameraSettings.pipelineSettings.cameraAutoExposure"
|
||||
label="Exposure"
|
||||
tooltip="Directly controls how much light is allowed to fall onto the sensor, which affects apparent brightness"
|
||||
:min="0"
|
||||
:max="100"
|
||||
tooltip="Directly controls how long the camera shutter remains open. Units are dependant on the underlying driver."
|
||||
:min="useCameraSettingsStore().minExposureRaw"
|
||||
:max="useCameraSettingsStore().maxExposureRaw"
|
||||
:slider-cols="interactiveCols"
|
||||
:step="0.1"
|
||||
@input="(args) => useCameraSettingsStore().changeCurrentPipelineSetting({ cameraExposure: args }, false)"
|
||||
:step="1"
|
||||
@input="(args) => useCameraSettingsStore().changeCurrentPipelineSetting({ cameraExposureRaw: args }, false)"
|
||||
/>
|
||||
<pv-slider
|
||||
v-model="useCameraSettingsStore().currentPipelineSettings.cameraBrightness"
|
||||
|
||||
@@ -68,6 +68,12 @@ export const useCameraSettingsStore = defineStore("cameraSettings", {
|
||||
},
|
||||
isCSICamera(): boolean {
|
||||
return this.currentCameraSettings.isCSICamera;
|
||||
},
|
||||
minExposureRaw(): number {
|
||||
return this.currentCameraSettings.minExposureRaw;
|
||||
},
|
||||
maxExposureRaw(): number {
|
||||
return this.currentCameraSettings.maxExposureRaw;
|
||||
}
|
||||
},
|
||||
actions: {
|
||||
@@ -102,6 +108,8 @@ export const useCameraSettingsStore = defineStore("cameraSettings", {
|
||||
})),
|
||||
completeCalibrations: d.calibrations,
|
||||
isCSICamera: d.isCSICamera,
|
||||
minExposureRaw: d.minExposureRaw,
|
||||
maxExposureRaw: d.maxExposureRaw,
|
||||
pipelineNicknames: d.pipelineNicknames,
|
||||
currentPipelineIndex: d.currentPipelineIndex,
|
||||
pipelineSettings: d.currentPipelineSettings,
|
||||
|
||||
@@ -64,7 +64,9 @@ export interface PipelineSettings {
|
||||
hueInverted: boolean;
|
||||
outputShowMultipleTargets: boolean;
|
||||
contourSortMode: number;
|
||||
cameraExposure: number;
|
||||
cameraExposureRaw: number;
|
||||
cameraMinExposureRaw: number;
|
||||
cameraMaxExposureRaw: number;
|
||||
offsetSinglePoint: { x: number; y: number };
|
||||
cameraBrightness: number;
|
||||
offsetDualPointAArea: number;
|
||||
@@ -97,7 +99,7 @@ export type ConfigurablePipelineSettings = Partial<
|
||||
// Omitted settings are changed for all pipeline types
|
||||
export const DefaultPipelineSettings: Omit<
|
||||
PipelineSettings,
|
||||
"cameraGain" | "targetModel" | "ledMode" | "outputShowMultipleTargets" | "cameraExposure" | "pipelineType"
|
||||
"cameraGain" | "targetModel" | "ledMode" | "outputShowMultipleTargets" | "cameraExposureRaw" | "pipelineType"
|
||||
> = {
|
||||
offsetRobotOffsetMode: RobotOffsetPointMode.None,
|
||||
streamingFrameDivisor: 0,
|
||||
@@ -151,7 +153,7 @@ export const DefaultReflectivePipelineSettings: ReflectivePipelineSettings = {
|
||||
targetModel: TargetModel.InfiniteRechargeHighGoalOuter,
|
||||
ledMode: true,
|
||||
outputShowMultipleTargets: false,
|
||||
cameraExposure: 6,
|
||||
cameraExposureRaw: 6,
|
||||
pipelineType: PipelineType.Reflective,
|
||||
|
||||
contourFilterRangeY: 2,
|
||||
@@ -182,7 +184,7 @@ export const DefaultColoredShapePipelineSettings: ColoredShapePipelineSettings =
|
||||
targetModel: TargetModel.InfiniteRechargeHighGoalOuter,
|
||||
ledMode: true,
|
||||
outputShowMultipleTargets: false,
|
||||
cameraExposure: 20,
|
||||
cameraExposureRaw: 20,
|
||||
pipelineType: PipelineType.ColoredShape,
|
||||
|
||||
erode: false,
|
||||
@@ -222,7 +224,7 @@ export const DefaultAprilTagPipelineSettings: AprilTagPipelineSettings = {
|
||||
targetModel: TargetModel.AprilTag6p5in_36h11,
|
||||
ledMode: false,
|
||||
outputShowMultipleTargets: true,
|
||||
cameraExposure: 20,
|
||||
cameraExposureRaw: 20,
|
||||
pipelineType: PipelineType.AprilTag,
|
||||
|
||||
hammingDist: 0,
|
||||
@@ -264,7 +266,7 @@ export const DefaultArucoPipelineSettings: ArucoPipelineSettings = {
|
||||
cameraGain: 75,
|
||||
outputShowMultipleTargets: true,
|
||||
targetModel: TargetModel.AprilTag6p5in_36h11,
|
||||
cameraExposure: -1,
|
||||
cameraExposureRaw: -1,
|
||||
cameraAutoExposure: true,
|
||||
ledMode: false,
|
||||
pipelineType: PipelineType.Aruco,
|
||||
@@ -299,7 +301,7 @@ export const DefaultObjectDetectionPipelineSettings: ObjectDetectionPipelineSett
|
||||
targetModel: TargetModel.InfiniteRechargeHighGoalOuter,
|
||||
ledMode: true,
|
||||
outputShowMultipleTargets: false,
|
||||
cameraExposure: 6,
|
||||
cameraExposureRaw: 6,
|
||||
confidence: 0.9,
|
||||
nms: 0.45,
|
||||
box_thresh: 0.25
|
||||
|
||||
@@ -148,15 +148,18 @@ export interface CameraCalibrationResult {
|
||||
export enum ValidQuirks {
|
||||
AWBGain = "AWBGain",
|
||||
AdjustableFocus = "AdjustableFocus",
|
||||
ArduOV9281 = "ArduOV9281",
|
||||
ArduOV2311 = "ArduOV2311",
|
||||
ArduOV9782 = "ArduOV9782",
|
||||
InnoOV9281Controls = "InnoOV9281Controls",
|
||||
ArduOV9281Controls = "ArduOV9281Controls",
|
||||
ArduOV2311Controls = "ArduOV2311Controls",
|
||||
ArduOV9782Controls = "ArduOV9782Controls",
|
||||
ArduCamCamera = "ArduCamCamera",
|
||||
CompletelyBroken = "CompletelyBroken",
|
||||
FPSCap100 = "FPSCap100",
|
||||
Gain = "Gain",
|
||||
PiCam = "PiCam",
|
||||
StickyFPS = "StickyFPS"
|
||||
StickyFPS = "StickyFPS",
|
||||
LifeCamControls = "LifeCamControls",
|
||||
PsEyeControls = "PsEyeControls"
|
||||
}
|
||||
|
||||
export interface QuirkyCamera {
|
||||
@@ -190,6 +193,9 @@ export interface CameraSettings {
|
||||
|
||||
cameraQuirks: QuirkyCamera;
|
||||
isCSICamera: boolean;
|
||||
|
||||
minExposureRaw: number;
|
||||
maxExposureRaw: number;
|
||||
}
|
||||
|
||||
export interface CameraSettingsChangeRequest {
|
||||
@@ -289,7 +295,9 @@ export const PlaceholderCameraSettings: CameraSettings = {
|
||||
StickyFPS: false
|
||||
}
|
||||
},
|
||||
isCSICamera: false
|
||||
isCSICamera: false,
|
||||
minExposureRaw: 1,
|
||||
maxExposureRaw: 100
|
||||
};
|
||||
|
||||
export enum CalibrationBoardTypes {
|
||||
|
||||
@@ -58,6 +58,8 @@ export interface WebsocketCameraSettingsUpdate {
|
||||
pipelineNicknames: string[];
|
||||
videoFormatList: WebsocketVideoFormat;
|
||||
cameraQuirks: QuirkyCamera;
|
||||
minExposureRaw: number;
|
||||
maxExposureRaw: number;
|
||||
}
|
||||
export interface WebsocketNTUpdate {
|
||||
connected: boolean;
|
||||
|
||||
@@ -178,5 +178,7 @@ public class PhotonConfiguration {
|
||||
public boolean isFovConfigurable = true;
|
||||
public QuirkyCamera cameraQuirks;
|
||||
public boolean isCSICamera;
|
||||
public double minExposureRaw;
|
||||
public double maxExposureRaw;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -71,6 +71,23 @@ public class MathUtils {
|
||||
return nanos / 1000;
|
||||
}
|
||||
|
||||
/**
|
||||
* Constrain a value to only take on certain values. Pick the next-highest allowed value in the
|
||||
* array if in-between.
|
||||
*
|
||||
* @param value value to quantize
|
||||
* @param allowableSteps sorted array of the allowed values
|
||||
* @return quantized value
|
||||
*/
|
||||
public static int quantize(int value, int[] allowableSteps) {
|
||||
for (int step : allowableSteps) {
|
||||
if (value <= step) {
|
||||
return step;
|
||||
}
|
||||
}
|
||||
return allowableSteps[allowableSteps.length - 1];
|
||||
}
|
||||
|
||||
public static double map(
|
||||
double value, double in_min, double in_max, double out_min, double out_max) {
|
||||
return (value - in_min) * (out_max - out_min) / (in_max - in_min) + out_min;
|
||||
|
||||
@@ -20,8 +20,10 @@ package org.photonvision.vision.camera;
|
||||
public enum CameraQuirk {
|
||||
/** Camera settable for controllable image gain */
|
||||
Gain,
|
||||
/** For the Raspberry Pi Camera */
|
||||
PiCam,
|
||||
/** Only certain discrete exposure settings work */
|
||||
LifeCamControls,
|
||||
/** Auto-Exposure property uses 1/0, rather than 3/1 */
|
||||
PsEyeControls,
|
||||
/** Cap at 100FPS for high-bandwidth cameras */
|
||||
FPSCap100,
|
||||
/** Separate red/blue gain controls available */
|
||||
@@ -35,14 +37,16 @@ public enum CameraQuirk {
|
||||
/** Camera is an arducam. This means it shares VID/PID with other arducams (ew) */
|
||||
ArduCamCamera,
|
||||
/**
|
||||
* Camera is an arducam ov9281 which has a funky exposure issue where it is defined in v4l as
|
||||
* Camera is an arducam USB ov9281 which has a funky exposure issue where it is defined in v4l as
|
||||
* 1-5000 instead of 1-75
|
||||
*/
|
||||
ArduOV9281,
|
||||
ArduOV9281Controls,
|
||||
/** Dummy quirk to tell OV2311 from OV9281 */
|
||||
ArduOV2311,
|
||||
/*
|
||||
* Camera is an arducam ov9782 which has specific exposure ranges and needs a specific white balance issue
|
||||
ArduOV2311Controls,
|
||||
ArduOV9782Controls,
|
||||
/**
|
||||
* Camera is innomaker USB OV9281 which also has incorrect v4l exposure times Real range is more
|
||||
* like 0-500
|
||||
*/
|
||||
ArduOV9782
|
||||
InnoOV9281Controls,
|
||||
}
|
||||
|
||||
@@ -74,6 +74,17 @@ public class FileVisionSource extends VisionSource {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void remakeSettables() {
|
||||
// Nothing to do, settables for this type of VisionSource should never be remade.
|
||||
return;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasLEDs() {
|
||||
return false; // Assume USB cameras do not have photonvision-controlled LEDs
|
||||
}
|
||||
|
||||
private static class FileSourceSettables extends VisionSourceSettables {
|
||||
private final VideoMode videoMode;
|
||||
|
||||
@@ -93,7 +104,7 @@ public class FileVisionSource extends VisionSource {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setExposure(double exposure) {}
|
||||
public void setExposureRaw(double exposureRaw) {}
|
||||
|
||||
public void setAutoExposure(boolean cameraAutoExposure) {}
|
||||
|
||||
@@ -117,5 +128,15 @@ public class FileVisionSource extends VisionSource {
|
||||
public HashMap<Integer, VideoMode> getAllVideoModes() {
|
||||
return videoModes;
|
||||
}
|
||||
|
||||
@Override
|
||||
public double getMinExposureRaw() {
|
||||
return 1f;
|
||||
}
|
||||
|
||||
@Override
|
||||
public double getMaxExposureRaw() {
|
||||
return 100f;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,6 +40,9 @@ public class LibcameraGpuSettables extends VisionSourceSettables {
|
||||
|
||||
private final LibCameraJNI.SensorModel sensorModel;
|
||||
|
||||
private double minExposure = 1;
|
||||
private double maxExposure = 80000;
|
||||
|
||||
private ImageRotationMode m_rotationMode = ImageRotationMode.DEG_0;
|
||||
|
||||
public final Object CAMERA_LOCK = new Object();
|
||||
@@ -100,6 +103,12 @@ public class LibcameraGpuSettables extends VisionSourceSettables {
|
||||
// TODO need to add more video modes for new sensors here
|
||||
|
||||
currentVideoMode = (FPSRatedVideoMode) videoModes.get(0);
|
||||
|
||||
if (sensorModel == LibCameraJNI.SensorModel.OV9281) {
|
||||
minExposure = 7;
|
||||
} else if (sensorModel == LibCameraJNI.SensorModel.OV5647) {
|
||||
minExposure = 560;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -114,34 +123,20 @@ public class LibcameraGpuSettables extends VisionSourceSettables {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setExposure(double exposure) {
|
||||
if (exposure < 0.0 || lastAutoExposureActive) {
|
||||
public void setExposureRaw(double exposureRaw) {
|
||||
if (exposureRaw < 0.0 || lastAutoExposureActive) {
|
||||
// Auto-exposure is active right now, don't set anything.
|
||||
return;
|
||||
}
|
||||
|
||||
// Store the exposure for use when we need to recreate the camera.
|
||||
lastManualExposure = exposure;
|
||||
lastManualExposure = exposureRaw;
|
||||
|
||||
// Minimum exposure can't be below 1uS cause otherwise it would be 0 and 0 is auto exposure.
|
||||
double minExposure = 1;
|
||||
|
||||
// HACKS!
|
||||
// If we set exposure too low, libcamera crashes or slows down
|
||||
// Very weird and smelly
|
||||
// For now, band-aid this by just not setting it lower than the "it breaks" limit
|
||||
// is different depending on camera.
|
||||
// All units are uS.
|
||||
if (sensorModel == LibCameraJNI.SensorModel.OV9281) {
|
||||
minExposure = 4800;
|
||||
} else if (sensorModel == LibCameraJNI.SensorModel.OV5647) {
|
||||
minExposure = 560;
|
||||
}
|
||||
// 80,000 uS seems like an exposure value that will be greater than ever needed while giving
|
||||
// enough control over exposure.
|
||||
exposure = MathUtils.map(exposure, 0, 100, minExposure, 80000);
|
||||
exposureRaw = MathUtil.clamp(exposureRaw, minExposure, maxExposure);
|
||||
|
||||
var success = LibCameraJNI.setExposure(r_ptr, (int) exposure);
|
||||
var success = LibCameraJNI.setExposure(r_ptr, (int) exposureRaw);
|
||||
if (!success) LibcameraGpuSource.logger.warn("Couldn't set Pi Camera exposure");
|
||||
}
|
||||
|
||||
@@ -231,8 +226,8 @@ public class LibcameraGpuSettables extends VisionSourceSettables {
|
||||
}
|
||||
|
||||
// We don't store last settings on the native side, and when you change video mode these get
|
||||
// reset on MMAL's end
|
||||
setExposure(lastManualExposure);
|
||||
// reset on the native driver's end. Reset em back to be correct
|
||||
setExposureRaw(lastManualExposure);
|
||||
setAutoExposure(lastAutoExposureActive);
|
||||
setBrightness(lastBrightness);
|
||||
setGain(lastGain);
|
||||
@@ -251,4 +246,14 @@ public class LibcameraGpuSettables extends VisionSourceSettables {
|
||||
public LibCameraJNI.SensorModel getModel() {
|
||||
return sensorModel;
|
||||
}
|
||||
|
||||
@Override
|
||||
public double getMinExposureRaw() {
|
||||
return this.minExposure;
|
||||
}
|
||||
|
||||
@Override
|
||||
public double getMaxExposureRaw() {
|
||||
return this.maxExposure;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,6 +58,12 @@ public class LibcameraGpuSource extends VisionSource {
|
||||
return settables;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void remakeSettables() {
|
||||
// Nothing to do, settables for this type of VisionSource should never be remade.
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* On the OV5649 the actual FPS we want to request from the GPU can be higher than the FPS that we
|
||||
* can do after processing. On the IMX219 these FPSes match pretty closely, except for the
|
||||
@@ -88,4 +94,9 @@ public class LibcameraGpuSource extends VisionSource {
|
||||
public boolean isVendorCamera() {
|
||||
return ConfigManager.getInstance().getConfig().getHardwareConfig().hasPresetFOV();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasLEDs() {
|
||||
return (ConfigManager.getInstance().getConfig().getHardwareConfig().ledPins.size() > 0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,29 +28,27 @@ import java.util.Objects;
|
||||
public class QuirkyCamera {
|
||||
private static final List<QuirkyCamera> quirkyCameras =
|
||||
List.of(
|
||||
// Chris's older generic "Logitec HD Webcam"
|
||||
new QuirkyCamera(0x9331, 0x5A3, CameraQuirk.CompletelyBroken),
|
||||
// Logitec C270
|
||||
new QuirkyCamera(0x825, 0x46D, CameraQuirk.CompletelyBroken),
|
||||
// A laptop internal camera someone found broken
|
||||
new QuirkyCamera(0x0bda, 0x5510, CameraQuirk.CompletelyBroken),
|
||||
// SnapCamera on Windows
|
||||
new QuirkyCamera(-1, -1, "Snap Camera", CameraQuirk.CompletelyBroken),
|
||||
// Mac Facetime Camera shared into Windows in Bootcamp
|
||||
new QuirkyCamera(-1, -1, "FaceTime HD Camera", CameraQuirk.CompletelyBroken),
|
||||
// Microsoft Lifecam
|
||||
new QuirkyCamera(-1, -1, "LifeCam HD-3000", CameraQuirk.LifeCamControls),
|
||||
// Microsoft Lifecam
|
||||
new QuirkyCamera(-1, -1, "LifeCam Cinema (TM)", CameraQuirk.LifeCamControls),
|
||||
// PS3Eye
|
||||
new QuirkyCamera(
|
||||
0x9331,
|
||||
0x5A3,
|
||||
CameraQuirk.CompletelyBroken), // Chris's older generic "Logitec HD Webcam"
|
||||
new QuirkyCamera(0x825, 0x46D, CameraQuirk.CompletelyBroken), // Logitec C270
|
||||
new QuirkyCamera(
|
||||
0x0bda,
|
||||
0x5510,
|
||||
CameraQuirk.CompletelyBroken), // A laptop internal camera someone found broken
|
||||
new QuirkyCamera(
|
||||
-1, -1, "Snap Camera", CameraQuirk.CompletelyBroken), // SnapCamera on Windows
|
||||
new QuirkyCamera(
|
||||
-1,
|
||||
-1,
|
||||
"FaceTime HD Camera",
|
||||
CameraQuirk.CompletelyBroken), // Mac Facetime Camera shared into Windows in Bootcamp
|
||||
new QuirkyCamera(0x2000, 0x1415, CameraQuirk.Gain, CameraQuirk.FPSCap100), // PS3Eye
|
||||
new QuirkyCamera(
|
||||
-1, -1, "mmal service 16.1", CameraQuirk.PiCam), // PiCam (via V4L2, not zerocopy)
|
||||
new QuirkyCamera(-1, -1, "unicam", CameraQuirk.PiCam), // PiCam (via V4L2, not zerocopy)
|
||||
new QuirkyCamera(0x85B, 0x46D, CameraQuirk.AdjustableFocus), // Logitech C925-e
|
||||
// Generic arducam. Since OV2311 can't be differentiated at first boot, apply stickyFPS to
|
||||
// the generic case, too
|
||||
0x1415, 0x2000, CameraQuirk.Gain, CameraQuirk.FPSCap100, CameraQuirk.PsEyeControls),
|
||||
// Logitech C925-e
|
||||
new QuirkyCamera(0x85B, 0x46D, CameraQuirk.AdjustableFocus),
|
||||
// Generic arducam. Since OV2311 can't be differentiated
|
||||
// at first boot, apply stickyFPS to the generic case, too
|
||||
new QuirkyCamera(
|
||||
0x0c45,
|
||||
0x6366,
|
||||
@@ -65,7 +63,7 @@ public class QuirkyCamera {
|
||||
"OV2311",
|
||||
"OV2311",
|
||||
CameraQuirk.ArduCamCamera,
|
||||
CameraQuirk.ArduOV2311,
|
||||
CameraQuirk.ArduOV2311Controls,
|
||||
CameraQuirk.StickyFPS),
|
||||
// Arducam OV9281
|
||||
new QuirkyCamera(
|
||||
@@ -74,7 +72,7 @@ public class QuirkyCamera {
|
||||
"OV9281",
|
||||
"OV9281",
|
||||
CameraQuirk.ArduCamCamera,
|
||||
CameraQuirk.ArduOV9281),
|
||||
CameraQuirk.ArduOV9281Controls),
|
||||
// Arducam OV
|
||||
new QuirkyCamera(
|
||||
0x0c45,
|
||||
@@ -82,17 +80,19 @@ public class QuirkyCamera {
|
||||
"OV9782",
|
||||
"OV9782",
|
||||
CameraQuirk.ArduCamCamera,
|
||||
CameraQuirk.ArduOV9782));
|
||||
CameraQuirk.ArduOV9782Controls),
|
||||
// Innomaker OV9281
|
||||
new QuirkyCamera(
|
||||
0x0c45, 0x636d, "USB Camera", "USB Camera", CameraQuirk.InnoOV9281Controls));
|
||||
|
||||
public static final QuirkyCamera DefaultCamera = new QuirkyCamera(0, 0, "");
|
||||
public static final QuirkyCamera ZeroCopyPiCamera =
|
||||
new QuirkyCamera(
|
||||
-1,
|
||||
-1,
|
||||
"mmal service 16.1",
|
||||
CameraQuirk.PiCam,
|
||||
"unicam",
|
||||
CameraQuirk.Gain,
|
||||
CameraQuirk.AWBGain); // PiCam (special zerocopy version)
|
||||
CameraQuirk.AWBGain); // PiCam (using libpicam GPU Driver on raspberry pi)
|
||||
|
||||
@JsonProperty("baseName")
|
||||
public final String baseName;
|
||||
@@ -181,11 +181,22 @@ public class QuirkyCamera {
|
||||
|
||||
public static QuirkyCamera getQuirkyCamera(int usbVid, int usbPid, String baseName) {
|
||||
for (var qc : quirkyCameras) {
|
||||
boolean hasBaseName = !qc.baseName.isEmpty();
|
||||
boolean matchesBaseName = qc.baseName.equals(baseName) || !hasBaseName;
|
||||
// If we have a quirkycamera we need to copy the quirks from our predefined object and create
|
||||
// a quirkycamera object with the baseName.
|
||||
if (qc.usbVid == usbVid && qc.usbPid == usbPid && matchesBaseName) {
|
||||
boolean useBaseNameMatch = !qc.baseName.isEmpty();
|
||||
boolean matchesBaseName = true; // default to matching
|
||||
if (useBaseNameMatch) {
|
||||
matchesBaseName = baseName.endsWith(qc.baseName);
|
||||
}
|
||||
|
||||
boolean usePidVidMatch = (qc.usbVid != -1) && (qc.usbPid != -1);
|
||||
boolean matchesPidVid = true; // default to matching
|
||||
if (usePidVidMatch) {
|
||||
matchesPidVid = (qc.usbVid == usbVid && qc.usbPid == usbPid);
|
||||
}
|
||||
|
||||
if (matchesPidVid && matchesBaseName) {
|
||||
// We have a quirky camera!
|
||||
// Copy the quirks from our predefined object and create
|
||||
// a QuirkyCamera object with the complete properties
|
||||
List<CameraQuirk> quirks = new ArrayList<CameraQuirk>();
|
||||
for (var q : CameraQuirk.values()) {
|
||||
if (qc.hasQuirk(q)) quirks.add(q);
|
||||
|
||||
@@ -34,9 +34,14 @@ public class TestSource extends VisionSource {
|
||||
public TestSource(CameraConfiguration config) {
|
||||
super(config);
|
||||
|
||||
if (getCameraConfiguration().cameraQuirks == null)
|
||||
getCameraConfiguration().cameraQuirks =
|
||||
QuirkyCamera.getQuirkyCamera(config.usbVID, config.usbVID, config.baseName);
|
||||
getCameraConfiguration().cameraQuirks =
|
||||
QuirkyCamera.getQuirkyCamera(config.usbVID, config.usbVID, config.baseName);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void remakeSettables() {
|
||||
// Nothing to do, settables for this type of VisionSource should never be remade.
|
||||
return;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -90,4 +95,9 @@ public class TestSource extends VisionSource {
|
||||
// TODO Auto-generated method stub
|
||||
throw new UnsupportedOperationException("Unimplemented method 'isVendorCamera'");
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasLEDs() {
|
||||
return false; // Assume USB cameras do not have photonvision-controlled LEDs
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,495 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) Photon Vision.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.photonvision.vision.camera;
|
||||
|
||||
import edu.wpi.first.cameraserver.CameraServer;
|
||||
import edu.wpi.first.cscore.CvSink;
|
||||
import edu.wpi.first.cscore.UsbCamera;
|
||||
import edu.wpi.first.cscore.VideoException;
|
||||
import edu.wpi.first.cscore.VideoMode;
|
||||
import edu.wpi.first.cscore.VideoProperty.Kind;
|
||||
import edu.wpi.first.util.PixelFormat;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
import org.photonvision.common.configuration.CameraConfiguration;
|
||||
import org.photonvision.common.configuration.ConfigManager;
|
||||
import org.photonvision.common.logging.LogGroup;
|
||||
import org.photonvision.common.logging.Logger;
|
||||
import org.photonvision.common.util.TestUtils;
|
||||
import org.photonvision.common.util.math.MathUtils;
|
||||
import org.photonvision.vision.frame.FrameProvider;
|
||||
import org.photonvision.vision.frame.provider.FileFrameProvider;
|
||||
import org.photonvision.vision.frame.provider.USBFrameProvider;
|
||||
import org.photonvision.vision.processes.VisionSource;
|
||||
import org.photonvision.vision.processes.VisionSourceSettables;
|
||||
|
||||
public class USBCameraSource extends VisionSource {
|
||||
private final Logger logger;
|
||||
private final UsbCamera camera;
|
||||
private final USBCameraSettables usbCameraSettables;
|
||||
private FrameProvider usbFrameProvider;
|
||||
private final CvSink cvSink;
|
||||
|
||||
public USBCameraSource(CameraConfiguration config) {
|
||||
super(config);
|
||||
|
||||
logger = new Logger(USBCameraSource.class, config.nickname, LogGroup.Camera);
|
||||
// cscore will auto-reconnect to the camera path we give it. v4l does not guarantee that if i
|
||||
// swap cameras around, the same /dev/videoN ID will be assigned to that camera. So instead
|
||||
// default to pinning to a particular USB port, or by "path" (appears to be a global identifier)
|
||||
// on Windows.
|
||||
camera = new UsbCamera(config.nickname, config.getUSBPath().orElse(config.path));
|
||||
cvSink = CameraServer.getVideo(this.camera);
|
||||
|
||||
// set vid/pid if not done already for future matching
|
||||
if (config.usbVID <= 0) config.usbVID = this.camera.getInfo().vendorId;
|
||||
if (config.usbPID <= 0) config.usbPID = this.camera.getInfo().productId;
|
||||
|
||||
if (getCameraConfiguration().cameraQuirks == null)
|
||||
getCameraConfiguration().cameraQuirks =
|
||||
QuirkyCamera.getQuirkyCamera(
|
||||
camera.getInfo().vendorId, camera.getInfo().productId, config.baseName);
|
||||
|
||||
if (getCameraConfiguration().cameraQuirks.hasQuirks()) {
|
||||
logger.info("Quirky camera detected: " + getCameraConfiguration().cameraQuirks.baseName);
|
||||
}
|
||||
|
||||
if (getCameraConfiguration().cameraQuirks.hasQuirk(CameraQuirk.ArduOV9782)) {
|
||||
try {
|
||||
// Set white balance temperature to 3500 for OV9782 camera
|
||||
camera.getProperty("white_balance_temperature").set(3500);
|
||||
} catch (VideoException e) {
|
||||
logger.error("Failed to set white balance temperature for OV9782 camera!", e);
|
||||
}
|
||||
}
|
||||
|
||||
if (getCameraConfiguration().cameraQuirks.hasQuirk(CameraQuirk.CompletelyBroken)) {
|
||||
// set some defaults, as these should never be used.
|
||||
logger.info(
|
||||
"Camera "
|
||||
+ getCameraConfiguration().cameraQuirks.baseName
|
||||
+ " is not supported for PhotonVision");
|
||||
usbCameraSettables = null;
|
||||
usbFrameProvider = null;
|
||||
} else {
|
||||
// Normal init
|
||||
// auto exposure/brightness/gain will be set by the visionmodule later
|
||||
disableAutoFocus();
|
||||
|
||||
usbCameraSettables = new USBCameraSettables(config);
|
||||
if (usbCameraSettables.getAllVideoModes().isEmpty()) {
|
||||
logger.info("Camera " + camera.getPath() + " has no video modes supported by PhotonVision");
|
||||
usbFrameProvider = null;
|
||||
} else {
|
||||
usbFrameProvider = new USBFrameProvider(cvSink, usbCameraSettables);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Mostly just used for unit tests to better simulate a usb camera without a camera being present.
|
||||
*/
|
||||
public USBCameraSource(CameraConfiguration config, int pid, int vid, boolean unitTest) {
|
||||
this(config);
|
||||
|
||||
if (getCameraConfiguration().cameraQuirks == null)
|
||||
getCameraConfiguration().cameraQuirks =
|
||||
QuirkyCamera.getQuirkyCamera(pid, vid, config.baseName);
|
||||
|
||||
if (unitTest)
|
||||
usbFrameProvider =
|
||||
new FileFrameProvider(
|
||||
TestUtils.getWPIImagePath(
|
||||
TestUtils.WPI2019Image.kCargoStraightDark72in_HighRes, false),
|
||||
TestUtils.WPI2019Image.FOV);
|
||||
}
|
||||
|
||||
void disableAutoFocus() {
|
||||
if (getCameraConfiguration().cameraQuirks.hasQuirk(CameraQuirk.AdjustableFocus)) {
|
||||
try {
|
||||
camera.getProperty("focus_auto").set(0);
|
||||
camera.getProperty("focus_absolute").set(0); // Focus into infinity
|
||||
} catch (VideoException e) {
|
||||
logger.error("Unable to disable autofocus!", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public QuirkyCamera getCameraQuirks() {
|
||||
return getCameraConfiguration().cameraQuirks;
|
||||
}
|
||||
|
||||
@Override
|
||||
public FrameProvider getFrameProvider() {
|
||||
return usbFrameProvider;
|
||||
}
|
||||
|
||||
@Override
|
||||
public VisionSourceSettables getSettables() {
|
||||
return this.usbCameraSettables;
|
||||
}
|
||||
|
||||
public class USBCameraSettables extends VisionSourceSettables {
|
||||
// We need to remember the last exposure set when exiting auto exposure mode so we can restore
|
||||
// it
|
||||
private double last_exposure = -1;
|
||||
|
||||
protected USBCameraSettables(CameraConfiguration configuration) {
|
||||
super(configuration);
|
||||
getAllVideoModes();
|
||||
if (!configuration.cameraQuirks.hasQuirk(CameraQuirk.StickyFPS))
|
||||
if (!videoModes.isEmpty()) setVideoMode(videoModes.get(0)); // fixes double FPS set
|
||||
}
|
||||
|
||||
public void setAutoExposure(boolean cameraAutoExposure) {
|
||||
logger.debug("Setting auto exposure to " + cameraAutoExposure);
|
||||
|
||||
if (getCameraConfiguration().cameraQuirks.hasQuirk(CameraQuirk.PiCam)) {
|
||||
// Case, we know this is a picam. Go through v4l2-ctl interface directly
|
||||
|
||||
// Common settings
|
||||
camera
|
||||
.getProperty("image_stabilization")
|
||||
.set(0); // No image stabilization, as this will throw off odometry
|
||||
camera.getProperty("power_line_frequency").set(2); // Assume 60Hz USA
|
||||
camera.getProperty("scene_mode").set(0); // no presets
|
||||
camera.getProperty("exposure_metering_mode").set(0);
|
||||
camera.getProperty("exposure_dynamic_framerate").set(0);
|
||||
|
||||
if (!cameraAutoExposure) {
|
||||
// Pick a bunch of reasonable setting defaults for vision processing retroreflective
|
||||
camera.getProperty("auto_exposure_bias").set(0);
|
||||
camera.getProperty("iso_sensitivity_auto").set(0); // Disable auto ISO adjustment
|
||||
camera.getProperty("iso_sensitivity").set(0); // Manual ISO adjustment
|
||||
camera.getProperty("white_balance_auto_preset").set(2); // Auto white-balance disabled
|
||||
camera.getProperty("auto_exposure").set(1); // auto exposure disabled
|
||||
} else {
|
||||
// Pick a bunch of reasonable setting defaults for driver, fiducials, or otherwise
|
||||
// nice-for-humans
|
||||
camera.getProperty("auto_exposure_bias").set(12);
|
||||
camera.getProperty("iso_sensitivity_auto").set(1);
|
||||
camera.getProperty("iso_sensitivity").set(1); // Manual ISO adjustment by default
|
||||
camera.getProperty("white_balance_auto_preset").set(1); // Auto white-balance enabled
|
||||
camera.getProperty("auto_exposure").set(0); // auto exposure enabled
|
||||
}
|
||||
|
||||
} else {
|
||||
// Case - this is some other USB cam. Default to wpilib's implementation
|
||||
|
||||
var canSetWhiteBalance = !getCameraConfiguration().cameraQuirks.hasQuirk(CameraQuirk.Gain);
|
||||
|
||||
if (!cameraAutoExposure) {
|
||||
// Pick a bunch of reasonable setting defaults for vision processing retroreflective
|
||||
if (canSetWhiteBalance) {
|
||||
// Linux kernel bump changed names -- now called white_balance_automatic and
|
||||
// white_balance_temperature
|
||||
if (camera.getProperty("white_balance_automatic").getKind() != Kind.kNone) {
|
||||
if (getCameraConfiguration().cameraQuirks.hasQuirk(CameraQuirk.ArduOV9782)) {
|
||||
try {
|
||||
// Set white balance temperature to 3500 for OV9782 camera
|
||||
camera.getProperty("white_balance_automatic").set(0);
|
||||
camera.getProperty("white_balance_temperature").set(3500);
|
||||
} catch (VideoException e) {
|
||||
logger.error("Failed to set white balance temperature for OV9782 camera!", e);
|
||||
}
|
||||
} else {
|
||||
// 1=auto, 0=manual
|
||||
camera.getProperty("white_balance_automatic").set(0);
|
||||
camera.getProperty("white_balance_temperature").set(4000);
|
||||
}
|
||||
} else {
|
||||
camera.setWhiteBalanceManual(4000); // Auto white-balance disabled, 4000K preset
|
||||
}
|
||||
|
||||
// Most cameras leave exposure time absolute at the last value from their AE algorithm.
|
||||
// Set it back to the exposure slider value
|
||||
setExposure(this.last_exposure);
|
||||
}
|
||||
} else {
|
||||
// Pick a bunch of reasonable setting defaults for driver, fiducials, or otherwise
|
||||
// nice-for-humans
|
||||
if (canSetWhiteBalance) {
|
||||
// Linux kernel bump changed names -- now called white_balance_automatic
|
||||
if (camera.getProperty("white_balance_automatic").getKind() != Kind.kNone) {
|
||||
// 1=auto, 0=manual
|
||||
camera.getProperty("white_balance_automatic").set(1);
|
||||
} else {
|
||||
camera.setWhiteBalanceAuto(); // Auto white-balance enabled
|
||||
}
|
||||
}
|
||||
|
||||
// Linux kernel bump changed names -- exposure_auto is now called auto_exposure
|
||||
if (camera.getProperty("auto_exposure").getKind() != Kind.kNone) {
|
||||
var prop = camera.getProperty("auto_exposure");
|
||||
// 3=auto-aperature
|
||||
prop.set((int) 3);
|
||||
} else {
|
||||
camera.setExposureAuto(); // auto exposure enabled
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private int timeToPiCamRawExposure(double time_us) {
|
||||
int retVal =
|
||||
(int)
|
||||
Math.round(
|
||||
time_us / 100.0); // Pi Cam's (both v1 and v2) need exposure time in units of
|
||||
// 100us/bit
|
||||
return Math.min(Math.max(retVal, 1), 10000); // Cap to allowable range for parameter
|
||||
}
|
||||
|
||||
private double pctToExposureTimeUs(double pct_in) {
|
||||
// Mirror the photonvision raspicam driver's algorithm for picking an exposure time
|
||||
// from a 0-100% input
|
||||
final double PADDING_LOW_US = 10;
|
||||
final double PADDING_HIGH_US = 10;
|
||||
return PADDING_LOW_US
|
||||
+ (pct_in / 100.0) * ((1e6 / (double) camera.getVideoMode().fps) - PADDING_HIGH_US);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setExposure(double exposure) {
|
||||
if (exposure >= 0.0) {
|
||||
try {
|
||||
int scaledExposure = 1;
|
||||
if (getCameraConfiguration().cameraQuirks.hasQuirk(CameraQuirk.PiCam)) {
|
||||
scaledExposure = Math.round(timeToPiCamRawExposure(pctToExposureTimeUs(exposure)));
|
||||
logger.debug("Setting camera raw exposure to " + scaledExposure);
|
||||
camera.getProperty("raw_exposure_time_absolute").set(scaledExposure);
|
||||
camera.getProperty("raw_exposure_time_absolute").set(scaledExposure);
|
||||
|
||||
// Yay thanks v4l for changing names randomly
|
||||
} else if (camera.getProperty("exposure_time_absolute").getKind() != Kind.kNone
|
||||
&& camera.getProperty("auto_exposure").getKind() != Kind.kNone) {
|
||||
// 1=manual-aperature
|
||||
camera.getProperty("auto_exposure").set(1);
|
||||
|
||||
// Seems like the name changed at some point in v4l? set it ouyrselves too
|
||||
var prop = camera.getProperty("raw_exposure_time_absolute");
|
||||
|
||||
var propMin = prop.getMin();
|
||||
var propMax = prop.getMax();
|
||||
|
||||
if (getCameraConfiguration().cameraQuirks.hasQuirk(CameraQuirk.ArduOV9281)) {
|
||||
propMin = 1;
|
||||
propMax = 75;
|
||||
} else if (getCameraConfiguration().cameraQuirks.hasQuirk(CameraQuirk.ArduOV2311)) {
|
||||
propMin = 1;
|
||||
propMax = 140;
|
||||
} else if (getCameraConfiguration().cameraQuirks.hasQuirk(CameraQuirk.ArduOV9782)) {
|
||||
propMin = 1;
|
||||
propMax = 60;
|
||||
}
|
||||
|
||||
var exposure_manual_val = MathUtils.map(Math.round(exposure), 0, 100, propMin, propMax);
|
||||
logger.debug("Setting camera exposure to " + exposure_manual_val);
|
||||
prop.set((int) exposure_manual_val);
|
||||
} else {
|
||||
scaledExposure = (int) Math.round(exposure);
|
||||
logger.debug("Setting camera exposure to " + scaledExposure);
|
||||
camera.setExposureManual(scaledExposure);
|
||||
camera.setExposureManual(scaledExposure);
|
||||
}
|
||||
} catch (VideoException e) {
|
||||
logger.error("Failed to set camera exposure!", e);
|
||||
}
|
||||
this.last_exposure = exposure;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setBrightness(int brightness) {
|
||||
try {
|
||||
camera.setBrightness(brightness);
|
||||
camera.setBrightness(brightness);
|
||||
} catch (VideoException e) {
|
||||
logger.error("Failed to set camera brightness!", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setGain(int gain) {
|
||||
try {
|
||||
if (getCameraConfiguration().cameraQuirks.hasQuirk(CameraQuirk.Gain)) {
|
||||
camera.getProperty("gain_automatic").set(0);
|
||||
camera.getProperty("gain").set(gain);
|
||||
}
|
||||
} catch (VideoException e) {
|
||||
logger.error("Failed to set camera gain!", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public VideoMode getCurrentVideoMode() {
|
||||
return camera.isConnected() ? camera.getVideoMode() : null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setVideoModeInternal(VideoMode videoMode) {
|
||||
try {
|
||||
if (videoMode == null) {
|
||||
logger.error("Got a null video mode! Doing nothing...");
|
||||
return;
|
||||
}
|
||||
camera.setVideoMode(videoMode);
|
||||
} catch (Exception e) {
|
||||
logger.error("Failed to set video mode!", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public HashMap<Integer, VideoMode> getAllVideoModes() {
|
||||
if (videoModes == null) {
|
||||
videoModes = new HashMap<>();
|
||||
List<VideoMode> videoModesList = new ArrayList<>();
|
||||
try {
|
||||
VideoMode[] modes;
|
||||
if (getCameraConfiguration().cameraQuirks.hasQuirk(CameraQuirk.PiCam)) {
|
||||
modes =
|
||||
new VideoMode[] {
|
||||
new VideoMode(PixelFormat.kBGR, 320, 240, 90),
|
||||
new VideoMode(PixelFormat.kBGR, 320, 240, 30),
|
||||
new VideoMode(PixelFormat.kBGR, 320, 240, 15),
|
||||
new VideoMode(PixelFormat.kBGR, 320, 240, 10),
|
||||
new VideoMode(PixelFormat.kBGR, 640, 480, 90),
|
||||
new VideoMode(PixelFormat.kBGR, 640, 480, 45),
|
||||
new VideoMode(PixelFormat.kBGR, 640, 480, 30),
|
||||
new VideoMode(PixelFormat.kBGR, 640, 480, 15),
|
||||
new VideoMode(PixelFormat.kBGR, 640, 480, 10),
|
||||
new VideoMode(PixelFormat.kBGR, 960, 720, 60),
|
||||
new VideoMode(PixelFormat.kBGR, 960, 720, 10),
|
||||
new VideoMode(PixelFormat.kBGR, 1280, 720, 45),
|
||||
new VideoMode(PixelFormat.kBGR, 1920, 1080, 20),
|
||||
};
|
||||
} else {
|
||||
modes = camera.enumerateVideoModes();
|
||||
}
|
||||
for (VideoMode videoMode : modes) {
|
||||
// Filter grey modes
|
||||
if (videoMode.pixelFormat == PixelFormat.kGray
|
||||
|| videoMode.pixelFormat == PixelFormat.kUnknown) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// On picam, filter non-bgr modes for performance
|
||||
if (getCameraConfiguration().cameraQuirks.hasQuirk(CameraQuirk.PiCam)) {
|
||||
if (videoMode.pixelFormat != PixelFormat.kBGR) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (getCameraConfiguration().cameraQuirks.hasQuirk(CameraQuirk.FPSCap100)) {
|
||||
if (videoMode.fps > 100) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
videoModesList.add(videoMode);
|
||||
|
||||
// TODO - do we want to trim down FPS modes? in cases where the camera has no gain
|
||||
// control,
|
||||
// lower FPS might be needed to ensure total exposure is acceptable.
|
||||
// We look for modes with the same height/width/pixelformat as this mode
|
||||
// and remove all the ones that are slower. This is sorted low to high.
|
||||
// So we remove the last element (the fastest FPS) from the duplicate list,
|
||||
// and remove all remaining elements from the final list
|
||||
// var duplicateModes =
|
||||
// videoModesList.stream()
|
||||
// .filter(
|
||||
// it ->
|
||||
// it.height == videoMode.height
|
||||
// && it.width == videoMode.width
|
||||
// && it.pixelFormat == videoMode.pixelFormat)
|
||||
// .sorted(Comparator.comparingDouble(it -> it.fps))
|
||||
// .collect(Collectors.toList());
|
||||
// duplicateModes.remove(duplicateModes.size() - 1);
|
||||
// videoModesList.removeAll(duplicateModes);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
logger.error("Exception while enumerating video modes!", e);
|
||||
videoModesList = List.of();
|
||||
}
|
||||
|
||||
// Sort by resolution
|
||||
var sortedList =
|
||||
videoModesList.stream()
|
||||
.distinct() // remove redundant video mode entries
|
||||
.sorted(((a, b) -> (b.width + b.height) - (a.width + a.height)))
|
||||
.collect(Collectors.toList());
|
||||
Collections.reverse(sortedList);
|
||||
|
||||
// On vendor cameras, respect blacklisted indices
|
||||
var indexBlacklist =
|
||||
ConfigManager.getInstance().getConfig().getHardwareConfig().blacklistedResIndices;
|
||||
for (int badIdx : indexBlacklist) {
|
||||
sortedList.remove(badIdx);
|
||||
}
|
||||
|
||||
for (VideoMode videoMode : sortedList) {
|
||||
videoModes.put(sortedList.indexOf(videoMode), videoMode);
|
||||
}
|
||||
}
|
||||
return videoModes;
|
||||
}
|
||||
}
|
||||
|
||||
// TODO improve robustness of this detection
|
||||
@Override
|
||||
public boolean isVendorCamera() {
|
||||
return ConfigManager.getInstance().getConfig().getHardwareConfig().hasPresetFOV()
|
||||
&& getCameraConfiguration().cameraQuirks.hasQuirk(CameraQuirk.PiCam);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (this == obj) return true;
|
||||
if (obj == null) return false;
|
||||
if (getClass() != obj.getClass()) return false;
|
||||
USBCameraSource other = (USBCameraSource) obj;
|
||||
if (camera == null) {
|
||||
if (other.camera != null) return false;
|
||||
} else if (!camera.equals(other.camera)) return false;
|
||||
if (usbCameraSettables == null) {
|
||||
if (other.usbCameraSettables != null) return false;
|
||||
} else if (!usbCameraSettables.equals(other.usbCameraSettables)) return false;
|
||||
if (usbFrameProvider == null) {
|
||||
if (other.usbFrameProvider != null) return false;
|
||||
} else if (!usbFrameProvider.equals(other.usbFrameProvider)) return false;
|
||||
if (cvSink == null) {
|
||||
if (other.cvSink != null) return false;
|
||||
} else if (!cvSink.equals(other.cvSink)) return false;
|
||||
if (getCameraConfiguration().cameraQuirks == null) {
|
||||
if (other.getCameraConfiguration().cameraQuirks != null) return false;
|
||||
} else if (!getCameraConfiguration()
|
||||
.cameraQuirks
|
||||
.equals(other.getCameraConfiguration().cameraQuirks)) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(
|
||||
camera,
|
||||
usbCameraSettables,
|
||||
usbFrameProvider,
|
||||
cameraConfiguration,
|
||||
cvSink,
|
||||
getCameraConfiguration().cameraQuirks);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
/*
|
||||
* Copyright (C) Photon Vision.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.photonvision.vision.camera.USBCameras;
|
||||
|
||||
import edu.wpi.first.cscore.UsbCamera;
|
||||
import org.photonvision.common.configuration.CameraConfiguration;
|
||||
|
||||
public class ArduOV2311CameraSettables extends GenericUSBCameraSettables {
|
||||
public ArduOV2311CameraSettables(CameraConfiguration configuration, UsbCamera camera) {
|
||||
super(configuration, camera);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void setUpExposureProperties() {
|
||||
super.setUpExposureProperties();
|
||||
|
||||
// Property limits are incorrect
|
||||
this.minExposure = 1;
|
||||
this.maxExposure = 140;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
/*
|
||||
* Copyright (C) Photon Vision.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.photonvision.vision.camera.USBCameras;
|
||||
|
||||
import edu.wpi.first.cscore.UsbCamera;
|
||||
import org.photonvision.common.configuration.CameraConfiguration;
|
||||
|
||||
public class ArduOV9281CameraSettables extends GenericUSBCameraSettables {
|
||||
public ArduOV9281CameraSettables(CameraConfiguration configuration, UsbCamera camera) {
|
||||
super(configuration, camera);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void setUpExposureProperties() {
|
||||
super.setUpExposureProperties();
|
||||
|
||||
// Property limits are incorrect
|
||||
this.minExposure = 1;
|
||||
this.maxExposure = 75;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
/*
|
||||
* Copyright (C) Photon Vision.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.photonvision.vision.camera.USBCameras;
|
||||
|
||||
import edu.wpi.first.cscore.UsbCamera;
|
||||
import org.photonvision.common.configuration.CameraConfiguration;
|
||||
|
||||
public class ArduOV9782CameraSettables extends GenericUSBCameraSettables {
|
||||
public ArduOV9782CameraSettables(CameraConfiguration configuration, UsbCamera camera) {
|
||||
super(configuration, camera);
|
||||
whiteBalanceTemperature = 3500;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void setUpExposureProperties() {
|
||||
super.setUpExposureProperties();
|
||||
|
||||
// Property limits are incorrect
|
||||
this.minExposure = 1;
|
||||
this.maxExposure = 60;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,306 @@
|
||||
/*
|
||||
* Copyright (C) Photon Vision.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.photonvision.vision.camera.USBCameras;
|
||||
|
||||
import edu.wpi.first.cscore.UsbCamera;
|
||||
import edu.wpi.first.cscore.VideoException;
|
||||
import edu.wpi.first.cscore.VideoMode;
|
||||
import edu.wpi.first.cscore.VideoProperty;
|
||||
import edu.wpi.first.math.MathUtil;
|
||||
import edu.wpi.first.util.PixelFormat;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Collectors;
|
||||
import org.photonvision.common.configuration.CameraConfiguration;
|
||||
import org.photonvision.common.configuration.ConfigManager;
|
||||
import org.photonvision.vision.camera.CameraQuirk;
|
||||
import org.photonvision.vision.processes.VisionSourceSettables;
|
||||
|
||||
public class GenericUSBCameraSettables extends VisionSourceSettables {
|
||||
// We need to remember the last exposure set when exiting
|
||||
// auto exposure mode so we can restore it
|
||||
protected double lastExposureRaw = -1;
|
||||
|
||||
// Some cameras need logic where we re-apply brightness after
|
||||
// changing exposure
|
||||
protected int lastBrightness = -1;
|
||||
|
||||
protected VideoProperty exposureAbsProp = null;
|
||||
protected VideoProperty autoExposureProp = null;
|
||||
|
||||
protected double minExposure = 1;
|
||||
protected double maxExposure = 80000;
|
||||
|
||||
protected static final int PROP_AUTO_EXPOSURE_ENABLED = 3;
|
||||
protected static final int PROP_AUTO_EXPOSURE_DISABLED = 1;
|
||||
|
||||
protected int whiteBalanceTemperature = 4000;
|
||||
|
||||
protected UsbCamera camera;
|
||||
protected CameraConfiguration configuration;
|
||||
|
||||
public GenericUSBCameraSettables(CameraConfiguration configuration, UsbCamera camera) {
|
||||
super(configuration);
|
||||
|
||||
this.configuration = configuration;
|
||||
this.camera = camera;
|
||||
|
||||
getAllVideoModes();
|
||||
|
||||
if (!configuration.cameraQuirks.hasQuirk(CameraQuirk.StickyFPS)) {
|
||||
if (!videoModes.isEmpty()) {
|
||||
setVideoMode(videoModes.get(0)); // fixes double FPS set
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected void setUpExposureProperties() {
|
||||
// Photonvision needs to be able to control absolute exposure. Make sure we can
|
||||
// first.
|
||||
var expProp =
|
||||
findProperty(
|
||||
"raw_exposure_absolute", "raw_exposure_time_absolute", "exposure", "raw_Exposure");
|
||||
|
||||
// Photonvision needs to be able to control auto exposure. Make sure we can
|
||||
// first.
|
||||
var autoExpProp = findProperty("exposure_auto", "auto_exposure");
|
||||
|
||||
exposureAbsProp = expProp.get();
|
||||
autoExposureProp = autoExpProp.get();
|
||||
this.minExposure = exposureAbsProp.getMin();
|
||||
this.maxExposure = exposureAbsProp.getMax();
|
||||
}
|
||||
|
||||
public void setAllCamDefaults() {
|
||||
// Common settings for all cameras to attempt to get their image
|
||||
// as close as possible to what we want for image processing
|
||||
softSet("image_stabilization", 0); // No image stabilization, as this will throw off odometry
|
||||
softSet("power_line_frequency", 2); // Assume 60Hz USA
|
||||
softSet("scene_mode", 0); // no presets
|
||||
softSet("exposure_metering_mode", 0);
|
||||
softSet("exposure_dynamic_framerate", 0);
|
||||
softSet("focus_auto", 0);
|
||||
softSet("focus_absolute", 0); // Focus into infinity
|
||||
softSet("white_balance_temperature", whiteBalanceTemperature);
|
||||
}
|
||||
|
||||
public void setAutoExposure(boolean cameraAutoExposure) {
|
||||
logger.debug("Setting auto exposure to " + cameraAutoExposure);
|
||||
|
||||
if (!cameraAutoExposure) {
|
||||
// Pick a bunch of reasonable setting defaults for vision processing
|
||||
softSet("auto_exposure_bias", 0);
|
||||
softSet("iso_sensitivity_auto", 0); // Disable auto ISO adjustment
|
||||
softSet("iso_sensitivity", 0); // Manual ISO adjustment
|
||||
softSet("white_balance_auto_preset", 2); // Auto white-balance disabled
|
||||
softSet("white_balance_automatic", 0);
|
||||
softSet("white_balance_temperature", whiteBalanceTemperature);
|
||||
autoExposureProp.set(PROP_AUTO_EXPOSURE_DISABLED);
|
||||
|
||||
// Most cameras leave exposure time absolute at the last value from their AE
|
||||
// algorithm.
|
||||
// Set it back to the exposure slider value
|
||||
setExposureRaw(this.lastExposureRaw);
|
||||
|
||||
} else {
|
||||
// Pick a bunch of reasonable setting to make the picture nice-for-humans
|
||||
softSet("auto_exposure_bias", 12);
|
||||
softSet("iso_sensitivity_auto", 1);
|
||||
softSet("iso_sensitivity", 1); // Manual ISO adjustment by default
|
||||
softSet("white_balance_auto_preset", 1); // Auto white-balance enabled
|
||||
softSet("white_balance_automatic", 1);
|
||||
autoExposureProp.set(PROP_AUTO_EXPOSURE_ENABLED);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public double getMinExposureRaw() {
|
||||
return minExposure;
|
||||
}
|
||||
|
||||
@Override
|
||||
public double getMaxExposureRaw() {
|
||||
return maxExposure;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setExposureRaw(double exposureRaw) {
|
||||
if (exposureRaw >= 0.0) {
|
||||
try {
|
||||
autoExposureProp.set(PROP_AUTO_EXPOSURE_DISABLED);
|
||||
|
||||
int propVal = (int) MathUtil.clamp(exposureRaw, minExposure, maxExposure);
|
||||
|
||||
logger.debug(
|
||||
"Setting property "
|
||||
+ exposureAbsProp.getName()
|
||||
+ " to "
|
||||
+ propVal
|
||||
+ " (user requested "
|
||||
+ exposureRaw
|
||||
+ " μs)");
|
||||
|
||||
exposureAbsProp.set(propVal);
|
||||
|
||||
this.lastExposureRaw = exposureRaw;
|
||||
|
||||
} catch (VideoException e) {
|
||||
logger.error("Failed to set camera exposure!", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setBrightness(int brightness) {
|
||||
try {
|
||||
camera.setBrightness(brightness);
|
||||
this.lastBrightness = brightness;
|
||||
} catch (VideoException e) {
|
||||
logger.error("Failed to set camera brightness!", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setGain(int gain) {
|
||||
softSet("gain_automatic", 0);
|
||||
softSet("gain", gain);
|
||||
}
|
||||
|
||||
@Override
|
||||
public VideoMode getCurrentVideoMode() {
|
||||
return camera.isConnected() ? camera.getVideoMode() : null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setVideoModeInternal(VideoMode videoMode) {
|
||||
try {
|
||||
if (videoMode == null) {
|
||||
logger.error("Got a null video mode! Doing nothing...");
|
||||
return;
|
||||
}
|
||||
camera.setVideoMode(videoMode);
|
||||
} catch (Exception e) {
|
||||
logger.error("Failed to set video mode!", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public HashMap<Integer, VideoMode> getAllVideoModes() {
|
||||
if (videoModes == null) {
|
||||
videoModes = new HashMap<>();
|
||||
List<VideoMode> videoModesList = new ArrayList<>();
|
||||
try {
|
||||
VideoMode[] modes;
|
||||
|
||||
modes = camera.enumerateVideoModes();
|
||||
|
||||
for (VideoMode videoMode : modes) {
|
||||
// Filter grey modes
|
||||
if (videoMode.pixelFormat == PixelFormat.kGray
|
||||
|| videoMode.pixelFormat == PixelFormat.kUnknown) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (configuration.cameraQuirks.hasQuirk(CameraQuirk.FPSCap100)) {
|
||||
if (videoMode.fps > 100) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
videoModesList.add(videoMode);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
logger.error("Exception while enumerating video modes!", e);
|
||||
videoModesList = List.of();
|
||||
}
|
||||
|
||||
// Sort by resolution
|
||||
var sortedList =
|
||||
videoModesList.stream()
|
||||
.distinct() // remove redundant video mode entries
|
||||
.sorted(((a, b) -> (b.width + b.height) - (a.width + a.height)))
|
||||
.collect(Collectors.toList());
|
||||
Collections.reverse(sortedList);
|
||||
|
||||
// On vendor cameras, respect blacklisted indices
|
||||
var indexBlacklist =
|
||||
ConfigManager.getInstance().getConfig().getHardwareConfig().blacklistedResIndices;
|
||||
for (int badIdx : indexBlacklist) {
|
||||
sortedList.remove(badIdx);
|
||||
}
|
||||
|
||||
for (VideoMode videoMode : sortedList) {
|
||||
videoModes.put(sortedList.indexOf(videoMode), videoMode);
|
||||
}
|
||||
}
|
||||
return videoModes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Forgiving "set this property" action. Produces a debug message but skips properties if they
|
||||
* aren't supported Errors if the property exists but the set fails.
|
||||
*
|
||||
* @param property
|
||||
* @param value
|
||||
*/
|
||||
protected void softSet(String property, int value) {
|
||||
VideoProperty prop = camera.getProperty(property);
|
||||
if (prop.getKind() == VideoProperty.Kind.kNone) {
|
||||
logger.debug("No property " + property + " for " + camera.getName() + " , skipping.");
|
||||
} else {
|
||||
try {
|
||||
prop.set(value);
|
||||
} catch (VideoException e) {
|
||||
logger.error("Failed to set " + property + " for " + camera.getName() + " !", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the first property with a name in the list. Useful to find gandolf property that goes
|
||||
* by many names in different os/releases/whatever
|
||||
*
|
||||
* @param options
|
||||
* @return
|
||||
*/
|
||||
protected Optional<VideoProperty> findProperty(String... options) {
|
||||
VideoProperty retProp = null;
|
||||
boolean found = false;
|
||||
for (var option : options) {
|
||||
retProp = camera.getProperty(option);
|
||||
if (retProp.getKind() != VideoProperty.Kind.kNone) {
|
||||
// got em
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!found) {
|
||||
logger.warn(
|
||||
"Expected at least one of the following properties to be available: "
|
||||
+ Arrays.toString(options));
|
||||
retProp = null;
|
||||
}
|
||||
|
||||
return Optional.ofNullable(retProp);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
/*
|
||||
* Copyright (C) Photon Vision.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.photonvision.vision.camera.USBCameras;
|
||||
|
||||
import edu.wpi.first.cscore.UsbCamera;
|
||||
import org.photonvision.common.configuration.CameraConfiguration;
|
||||
|
||||
public class InnoOV9281CameraSettables extends GenericUSBCameraSettables {
|
||||
public InnoOV9281CameraSettables(CameraConfiguration configuration, UsbCamera camera) {
|
||||
super(configuration, camera);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void setUpExposureProperties() {
|
||||
super.setUpExposureProperties();
|
||||
|
||||
// Property limits are incorrect
|
||||
this.minExposure = 1;
|
||||
this.maxExposure = 500;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
/*
|
||||
* Copyright (C) Photon Vision.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.photonvision.vision.camera.USBCameras;
|
||||
|
||||
import edu.wpi.first.cscore.UsbCamera;
|
||||
import edu.wpi.first.cscore.VideoException;
|
||||
import edu.wpi.first.math.MathUtil;
|
||||
import org.photonvision.common.configuration.CameraConfiguration;
|
||||
import org.photonvision.common.util.math.MathUtils;
|
||||
|
||||
public class LifeCam3kCameraSettables extends GenericUSBCameraSettables {
|
||||
// Lifecam only allows specific exposures. Pulled this list from
|
||||
// https://github.com/wpilibsuite/allwpilib/blob/main/cscore/src/main/native/linux/UsbCameraImpl.cpp#L129
|
||||
private static int[] allowableExposures = {5, 10, 20, 39, 78, 156, 312, 625};
|
||||
|
||||
public LifeCam3kCameraSettables(CameraConfiguration configuration, UsbCamera camera) {
|
||||
super(configuration, camera);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void setUpExposureProperties() {
|
||||
autoExposureProp = findProperty("exposure_auto", "auto_exposure").get();
|
||||
exposureAbsProp = findProperty("raw_exposure_time_absolute", "raw_exposure_absolute").get();
|
||||
|
||||
this.minExposure = exposureAbsProp.getMin();
|
||||
this.maxExposure = exposureAbsProp.getMax();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setExposureRaw(double exposureRaw) {
|
||||
if (exposureRaw >= 0.0) {
|
||||
try {
|
||||
int propVal = (int) MathUtil.clamp(exposureRaw, minExposure, maxExposure);
|
||||
|
||||
propVal = MathUtils.quantize(propVal, allowableExposures);
|
||||
|
||||
logger.debug(
|
||||
"Setting property "
|
||||
+ autoExposureProp.getName()
|
||||
+ " to "
|
||||
+ propVal
|
||||
+ " (user requested "
|
||||
+ exposureRaw
|
||||
+ " )");
|
||||
|
||||
exposureAbsProp.set(propVal);
|
||||
|
||||
this.lastExposureRaw = exposureRaw;
|
||||
|
||||
// Lifecam requires setting brightness again after exposure
|
||||
// And it requires setting it twice, ensuring the value is different
|
||||
// This camera is very bork.
|
||||
if (lastBrightness >= 0) {
|
||||
setBrightness(lastBrightness - 1);
|
||||
}
|
||||
|
||||
} catch (VideoException e) {
|
||||
logger.error("Failed to set camera exposure!", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setAllCamDefaults() {
|
||||
// Common settings for all cameras to attempt to get their image
|
||||
// as close as possible to what we want for image processing
|
||||
softSet("raw_contrast", 5);
|
||||
softSet("raw_saturation", 85);
|
||||
softSet("raw_sharpness", 25);
|
||||
softSet("white_balance_automatic", 0);
|
||||
softSet("white_balance_temperature", 4000);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
/*
|
||||
* Copyright (C) Photon Vision.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.photonvision.vision.camera.USBCameras;
|
||||
|
||||
import edu.wpi.first.cscore.UsbCamera;
|
||||
import edu.wpi.first.cscore.VideoException;
|
||||
import edu.wpi.first.math.MathUtil;
|
||||
import org.photonvision.common.configuration.CameraConfiguration;
|
||||
|
||||
public class LifeCam3kWindowsCameraSettables extends GenericUSBCameraSettables {
|
||||
public LifeCam3kWindowsCameraSettables(CameraConfiguration configuration, UsbCamera camera) {
|
||||
super(configuration, camera);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void setUpExposureProperties() {
|
||||
autoExposureProp = null; // Not Used
|
||||
exposureAbsProp = null; // Not Used
|
||||
|
||||
// We'll fallback on cscore's implementation for windows lifecam
|
||||
this.minExposure = 0;
|
||||
this.maxExposure = 100;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setExposureRaw(double exposureRaw) {
|
||||
if (exposureRaw >= 0.0) {
|
||||
try {
|
||||
int propVal = (int) MathUtil.clamp(exposureRaw, minExposure, maxExposure);
|
||||
|
||||
// exposureAbsProp.set(propVal);
|
||||
camera.setExposureManual(propVal);
|
||||
|
||||
this.lastExposureRaw = exposureRaw;
|
||||
|
||||
// Lifecam requires setting brightness again after exposure
|
||||
// And it requires setting it twice, ensuring the value is different
|
||||
// This camera is very bork.
|
||||
if (lastBrightness >= 0) {
|
||||
setBrightness(lastBrightness - 1);
|
||||
}
|
||||
|
||||
} catch (VideoException e) {
|
||||
logger.error("Failed to set camera exposure!", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void setAutoExposure(boolean cameraAutoExposure) {
|
||||
logger.debug("Setting auto exposure to " + cameraAutoExposure);
|
||||
|
||||
if (!cameraAutoExposure) {
|
||||
// Most cameras leave exposure time absolute at the last value from their AE
|
||||
// algorithm.
|
||||
// Set it back to the exposure slider value
|
||||
camera.setExposureManual((int) this.lastExposureRaw);
|
||||
} else {
|
||||
camera.setExposureAuto();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setAllCamDefaults() {
|
||||
// Common settings for all cameras to attempt to get their image
|
||||
// as close as possible to what we want for image processing
|
||||
softSet("raw_Contrast", 5);
|
||||
softSet("raw_Saturation", 85);
|
||||
softSet("raw_Sharpness", 25);
|
||||
softSet("WhiteBalance", 4000);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
/*
|
||||
* Copyright (C) Photon Vision.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.photonvision.vision.camera.USBCameras;
|
||||
|
||||
import edu.wpi.first.cscore.UsbCamera;
|
||||
import org.photonvision.common.configuration.CameraConfiguration;
|
||||
|
||||
public class PsEyeCameraSettables extends GenericUSBCameraSettables {
|
||||
public PsEyeCameraSettables(CameraConfiguration configuration, UsbCamera camera) {
|
||||
super(configuration, camera);
|
||||
}
|
||||
|
||||
public void setAutoExposure(boolean cameraAutoExposure) {
|
||||
logger.debug("Setting auto exposure to " + cameraAutoExposure);
|
||||
|
||||
// PS Eye uses inverted 1=Disabled, 0=Enabled for auto exposure
|
||||
if (!cameraAutoExposure) {
|
||||
autoExposureProp.set(1);
|
||||
|
||||
// Most cameras leave exposure time absolute at the last value
|
||||
// from their auto exposure algorithm.
|
||||
// Set it back to the exposure slider value
|
||||
setExposureRaw(this.lastExposureRaw);
|
||||
|
||||
} else {
|
||||
autoExposureProp.set(0);
|
||||
}
|
||||
}
|
||||
|
||||
public void setAllCamDefaults() {
|
||||
// Common settings for all cameras to attempt to get their image
|
||||
// as close as possible to what we want for image processing
|
||||
softSet("raw_hue", 0);
|
||||
softSet("raw_contrast", 32);
|
||||
softSet("raw_saturation", 100);
|
||||
softSet("raw_hue", -10);
|
||||
softSet("raw_sharpness", 0);
|
||||
softSet("white_balance_automatic", 0);
|
||||
softSet("gain_automatic", 0);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,250 @@
|
||||
/*
|
||||
* Copyright (C) Photon Vision.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.photonvision.vision.camera.USBCameras;
|
||||
|
||||
import edu.wpi.first.cameraserver.CameraServer;
|
||||
import edu.wpi.first.cscore.CvSink;
|
||||
import edu.wpi.first.cscore.UsbCamera;
|
||||
import edu.wpi.first.cscore.VideoException;
|
||||
import edu.wpi.first.cscore.VideoProperty;
|
||||
import edu.wpi.first.util.RuntimeDetector;
|
||||
import java.util.*;
|
||||
import org.photonvision.common.configuration.CameraConfiguration;
|
||||
import org.photonvision.common.logging.LogGroup;
|
||||
import org.photonvision.common.logging.Logger;
|
||||
import org.photonvision.vision.camera.CameraQuirk;
|
||||
import org.photonvision.vision.camera.QuirkyCamera;
|
||||
import org.photonvision.vision.frame.FrameProvider;
|
||||
import org.photonvision.vision.frame.provider.USBFrameProvider;
|
||||
import org.photonvision.vision.processes.VisionSource;
|
||||
import org.photonvision.vision.processes.VisionSourceSettables;
|
||||
|
||||
public class USBCameraSource extends VisionSource {
|
||||
private final Logger logger;
|
||||
private final UsbCamera camera;
|
||||
protected GenericUSBCameraSettables settables;
|
||||
protected FrameProvider usbFrameProvider;
|
||||
private final CvSink cvSink;
|
||||
|
||||
public USBCameraSource(CameraConfiguration config) {
|
||||
super(config);
|
||||
|
||||
logger = new Logger(USBCameraSource.class, config.nickname, LogGroup.Camera);
|
||||
// cscore will auto-reconnect to the camera path we give it. v4l does not guarantee that if i
|
||||
// swap cameras around, the same /dev/videoN ID will be assigned to that camera. So instead
|
||||
// default to pinning to a particular USB port, or by "path" (appears to be a global identifier)
|
||||
// on Windows.
|
||||
camera = new UsbCamera(config.nickname, config.getUSBPath().orElse(config.path));
|
||||
cvSink = CameraServer.getVideo(this.camera);
|
||||
|
||||
// set vid/pid if not done already for future matching
|
||||
if (config.usbVID <= 0) config.usbVID = this.camera.getInfo().vendorId;
|
||||
if (config.usbPID <= 0) config.usbPID = this.camera.getInfo().productId;
|
||||
|
||||
if (getCameraConfiguration().cameraQuirks == null) {
|
||||
getCameraConfiguration().cameraQuirks =
|
||||
QuirkyCamera.getQuirkyCamera(
|
||||
camera.getInfo().vendorId, camera.getInfo().productId, config.baseName);
|
||||
}
|
||||
|
||||
if (getCameraConfiguration().cameraQuirks.hasQuirks()) {
|
||||
logger.info("Quirky camera detected: " + getCameraConfiguration().cameraQuirks.baseName);
|
||||
}
|
||||
|
||||
// Aid to the development team - record the properties available for whatever the user plugged
|
||||
// in
|
||||
printCameraProperaties();
|
||||
|
||||
var cameraBroken = getCameraConfiguration().cameraQuirks.hasQuirk(CameraQuirk.CompletelyBroken);
|
||||
|
||||
if (cameraBroken) {
|
||||
// Known issues - Disable this camera
|
||||
logger.info(
|
||||
"Camera "
|
||||
+ getCameraConfiguration().cameraQuirks.baseName
|
||||
+ " is not supported for PhotonVision");
|
||||
// set some defaults, as these should never be used.
|
||||
settables = null;
|
||||
usbFrameProvider = null;
|
||||
|
||||
} else {
|
||||
// Camera is likely to work, set up the Settables
|
||||
settables = createSettables(config, camera);
|
||||
|
||||
if (settables.getAllVideoModes().isEmpty()) {
|
||||
// No video modes produced from settables, disable the camera
|
||||
logger.info("Camera " + camera.getPath() + " has no video modes supported by PhotonVision");
|
||||
usbFrameProvider = null;
|
||||
|
||||
} else {
|
||||
// Functional camera, set up the frame provider and configure defaults
|
||||
usbFrameProvider = new USBFrameProvider(cvSink, settables);
|
||||
settables.setAllCamDefaults();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Factory for making appropriate settables
|
||||
*
|
||||
* @param config
|
||||
* @param camera
|
||||
* @return
|
||||
*/
|
||||
protected GenericUSBCameraSettables createSettables(
|
||||
CameraConfiguration config, UsbCamera camera) {
|
||||
var quirks = getCameraConfiguration().cameraQuirks;
|
||||
|
||||
GenericUSBCameraSettables settables;
|
||||
|
||||
if (quirks.hasQuirk(CameraQuirk.LifeCamControls)) {
|
||||
if (RuntimeDetector.isWindows()) {
|
||||
logger.debug("Using Microsoft Lifecam 3000 Windows-Specific Settables");
|
||||
settables = new LifeCam3kWindowsCameraSettables(config, camera);
|
||||
} else {
|
||||
logger.debug("Using Microsoft Lifecam 3000 Settables");
|
||||
settables = new LifeCam3kCameraSettables(config, camera);
|
||||
}
|
||||
} else if (quirks.hasQuirk(CameraQuirk.PsEyeControls)) {
|
||||
logger.debug("Using PlayStation Eye Camera Settables");
|
||||
settables = new PsEyeCameraSettables(config, camera);
|
||||
} else if (quirks.hasQuirk(CameraQuirk.ArduOV2311Controls)) {
|
||||
logger.debug("Using Arducam OV2311 Settables");
|
||||
settables = new ArduOV2311CameraSettables(config, camera);
|
||||
} else if (quirks.hasQuirk(CameraQuirk.ArduOV9281Controls)) {
|
||||
logger.debug("Using Arducam OV9281 Settables");
|
||||
settables = new InnoOV9281CameraSettables(config, camera);
|
||||
} else if (quirks.hasQuirk(CameraQuirk.ArduOV9782Controls)) {
|
||||
logger.debug("Using Arducam OV9782 Settables");
|
||||
settables = new ArduOV9782CameraSettables(config, camera);
|
||||
} else if (quirks.hasQuirk(CameraQuirk.InnoOV9281Controls)) {
|
||||
settables = new InnoOV9281CameraSettables(config, camera);
|
||||
} else {
|
||||
logger.debug("Using Generic USB Cam Settables");
|
||||
settables = new GenericUSBCameraSettables(config, camera);
|
||||
}
|
||||
|
||||
settables.setUpExposureProperties();
|
||||
|
||||
return settables;
|
||||
}
|
||||
|
||||
/**
|
||||
* Must be called after createSettables Using the current config/camera and modified quirks, make
|
||||
* a new settables
|
||||
*/
|
||||
public void remakeSettables() {
|
||||
var oldConfig = this.cameraConfiguration;
|
||||
var oldCamera = this.camera;
|
||||
|
||||
this.settables = createSettables(oldConfig, oldCamera);
|
||||
}
|
||||
|
||||
private void printCameraProperaties() {
|
||||
VideoProperty[] cameraProperties = null;
|
||||
try {
|
||||
cameraProperties = camera.enumerateProperties();
|
||||
} catch (VideoException e) {
|
||||
logger.error("Failed to list camera properties!", e);
|
||||
}
|
||||
|
||||
if (cameraProperties != null) {
|
||||
String cameraPropertiesStr = "Cam Properties Dump:\n";
|
||||
for (int i = 0; i < cameraProperties.length; i++) {
|
||||
cameraPropertiesStr +=
|
||||
"Name: "
|
||||
+ cameraProperties[i].getName()
|
||||
+ ", Kind: "
|
||||
+ cameraProperties[i].getKind()
|
||||
+ ", Value: "
|
||||
+ cameraProperties[i].getKind().getValue()
|
||||
+ ", Min: "
|
||||
+ cameraProperties[i].getMin()
|
||||
+ ", Max: "
|
||||
+ cameraProperties[i].getMax()
|
||||
+ ", Dflt: "
|
||||
+ cameraProperties[i].getDefault()
|
||||
+ ", Step: "
|
||||
+ cameraProperties[i].getStep()
|
||||
+ "\n";
|
||||
}
|
||||
logger.debug(cameraPropertiesStr);
|
||||
}
|
||||
}
|
||||
|
||||
public QuirkyCamera getCameraQuirks() {
|
||||
return getCameraConfiguration().cameraQuirks;
|
||||
}
|
||||
|
||||
@Override
|
||||
public FrameProvider getFrameProvider() {
|
||||
return usbFrameProvider;
|
||||
}
|
||||
|
||||
@Override
|
||||
public VisionSourceSettables getSettables() {
|
||||
return this.settables;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isVendorCamera() {
|
||||
return false; // Vendors do not supply USB Cameras
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasLEDs() {
|
||||
return false; // Assume USB cameras do not have photonvision-controlled LEDs
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (this == obj) return true;
|
||||
if (obj == null) return false;
|
||||
if (getClass() != obj.getClass()) return false;
|
||||
USBCameraSource other = (USBCameraSource) obj;
|
||||
if (camera == null) {
|
||||
if (other.camera != null) return false;
|
||||
} else if (!camera.equals(other.camera)) return false;
|
||||
if (settables == null) {
|
||||
if (other.settables != null) return false;
|
||||
} else if (!settables.equals(other.settables)) return false;
|
||||
if (usbFrameProvider == null) {
|
||||
if (other.usbFrameProvider != null) return false;
|
||||
} else if (!usbFrameProvider.equals(other.usbFrameProvider)) return false;
|
||||
if (cvSink == null) {
|
||||
if (other.cvSink != null) return false;
|
||||
} else if (!cvSink.equals(other.cvSink)) return false;
|
||||
if (getCameraConfiguration().cameraQuirks == null) {
|
||||
if (other.getCameraConfiguration().cameraQuirks != null) return false;
|
||||
} else if (!getCameraConfiguration()
|
||||
.cameraQuirks
|
||||
.equals(other.getCameraConfiguration().cameraQuirks)) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(
|
||||
camera,
|
||||
settables,
|
||||
usbFrameProvider,
|
||||
cameraConfiguration,
|
||||
cvSink,
|
||||
getCameraConfiguration().cameraQuirks);
|
||||
}
|
||||
}
|
||||
@@ -42,7 +42,7 @@ public class AprilTagPipelineSettings extends AdvancedPipelineSettings {
|
||||
pipelineType = PipelineType.AprilTag;
|
||||
outputShowMultipleTargets = true;
|
||||
targetModel = TargetModel.kAprilTag6p5in_36h11;
|
||||
cameraExposure = 20;
|
||||
cameraExposureRaw = 20;
|
||||
cameraAutoExposure = false;
|
||||
ledMode = false;
|
||||
}
|
||||
|
||||
@@ -47,7 +47,7 @@ public class ArucoPipelineSettings extends AdvancedPipelineSettings {
|
||||
pipelineType = PipelineType.Aruco;
|
||||
outputShowMultipleTargets = true;
|
||||
targetModel = TargetModel.kAprilTag6p5in_36h11;
|
||||
cameraExposure = -1;
|
||||
cameraExposureRaw = -1;
|
||||
cameraAutoExposure = true;
|
||||
ledMode = false;
|
||||
}
|
||||
|
||||
@@ -42,7 +42,9 @@ public class CVPipelineSettings implements Cloneable {
|
||||
public String pipelineNickname = "New Pipeline";
|
||||
public boolean cameraAutoExposure = false;
|
||||
// manual exposure only used if cameraAutoExposure is false
|
||||
public double cameraExposure = 20;
|
||||
public double cameraExposureRaw = 20;
|
||||
public double cameraMinExposureRaw = 1;
|
||||
public double cameraMaxExposureRaw = 100;
|
||||
public int cameraBrightness = 50;
|
||||
// Currently only used by a few cameras (notably the zero-copy Pi Camera driver) with the Gain
|
||||
// quirk
|
||||
@@ -62,7 +64,9 @@ public class CVPipelineSettings implements Cloneable {
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
CVPipelineSettings that = (CVPipelineSettings) o;
|
||||
return pipelineIndex == that.pipelineIndex
|
||||
&& Double.compare(that.cameraExposure, cameraExposure) == 0
|
||||
&& Double.compare(that.cameraExposureRaw, cameraExposureRaw) == 0
|
||||
&& Double.compare(that.cameraMinExposureRaw, cameraMinExposureRaw) == 0
|
||||
&& Double.compare(that.cameraMaxExposureRaw, cameraMaxExposureRaw) == 0
|
||||
&& Double.compare(that.cameraBrightness, cameraBrightness) == 0
|
||||
&& Double.compare(that.cameraGain, cameraGain) == 0
|
||||
&& Double.compare(that.cameraRedGain, cameraRedGain) == 0
|
||||
@@ -84,7 +88,9 @@ public class CVPipelineSettings implements Cloneable {
|
||||
pipelineType,
|
||||
inputImageRotationMode,
|
||||
pipelineNickname,
|
||||
cameraExposure,
|
||||
cameraExposureRaw,
|
||||
cameraMinExposureRaw,
|
||||
cameraMaxExposureRaw,
|
||||
cameraBrightness,
|
||||
cameraGain,
|
||||
cameraRedGain,
|
||||
@@ -118,8 +124,8 @@ public class CVPipelineSettings implements Cloneable {
|
||||
+ ", pipelineNickname='"
|
||||
+ pipelineNickname
|
||||
+ '\''
|
||||
+ ", cameraExposure="
|
||||
+ cameraExposure
|
||||
+ ", cameraExposureRaw="
|
||||
+ cameraExposureRaw
|
||||
+ ", cameraBrightness="
|
||||
+ cameraBrightness
|
||||
+ ", cameraGain="
|
||||
|
||||
@@ -42,7 +42,7 @@ public class ColoredShapePipelineSettings extends AdvancedPipelineSettings {
|
||||
public ColoredShapePipelineSettings() {
|
||||
super();
|
||||
pipelineType = PipelineType.ColoredShape;
|
||||
cameraExposure = 20;
|
||||
cameraExposureRaw = 20;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -54,11 +54,6 @@ public class DriverModePipeline
|
||||
|
||||
resizeImagePipe.setParams(
|
||||
new ResizeImagePipe.ResizeImageParams(settings.streamingFrameDivisor));
|
||||
|
||||
// if (LibCameraJNI.isSupported() && cameraQuirks.hasQuirk(CameraQuirk.PiCam)) {
|
||||
// LibCameraJNI.setRotation(settings.inputImageRotationMode.value);
|
||||
// LibCameraJNI.setShouldCopyColor(true);
|
||||
// }
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -25,7 +25,7 @@ public class ObjectDetectionPipelineSettings extends AdvancedPipelineSettings {
|
||||
super();
|
||||
this.pipelineType = PipelineType.ObjectDetection; // TODO: FIX this
|
||||
this.outputShowMultipleTargets = true;
|
||||
cameraExposure = 20;
|
||||
cameraExposureRaw = 20;
|
||||
cameraAutoExposure = false;
|
||||
ledMode = false;
|
||||
confidence = .9;
|
||||
|
||||
@@ -27,7 +27,7 @@ public class ReflectivePipelineSettings extends AdvancedPipelineSettings {
|
||||
public ReflectivePipelineSettings() {
|
||||
super();
|
||||
pipelineType = PipelineType.Reflective;
|
||||
cameraExposure = 6;
|
||||
cameraExposureRaw = 6;
|
||||
cameraGain = 20;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -411,11 +411,11 @@ public class VisionModule {
|
||||
|
||||
// If manual exposure, force exposure slider to be valid
|
||||
if (!pipelineSettings.cameraAutoExposure) {
|
||||
if (pipelineSettings.cameraExposure < 0)
|
||||
pipelineSettings.cameraExposure = 10; // reasonable default
|
||||
if (pipelineSettings.cameraExposureRaw < 0)
|
||||
pipelineSettings.cameraExposureRaw = 10; // reasonable default
|
||||
}
|
||||
|
||||
visionSource.getSettables().setExposure(pipelineSettings.cameraExposure);
|
||||
visionSource.getSettables().setExposureRaw(pipelineSettings.cameraExposureRaw);
|
||||
try {
|
||||
visionSource.getSettables().setAutoExposure(pipelineSettings.cameraAutoExposure);
|
||||
} catch (VideoException e) {
|
||||
@@ -453,7 +453,7 @@ public class VisionModule {
|
||||
// Heuristic - if the camera has a known FOV or is a piCam, assume it's in use for
|
||||
// vision processing, and should command stuff to the LED's.
|
||||
// TODO: Make LED control a property of the camera itself and controllable in the UI.
|
||||
return isVendorCamera() || cameraQuirks.hasQuirk(CameraQuirk.PiCam);
|
||||
return isVendorCamera();
|
||||
}
|
||||
|
||||
private void setVisionLEDs(boolean on) {
|
||||
@@ -511,6 +511,8 @@ public class VisionModule {
|
||||
ret.currentPipelineIndex = pipelineManager.getRequestedIndex();
|
||||
ret.pipelineNicknames = pipelineManager.getPipelineNicknames();
|
||||
ret.cameraQuirks = visionSource.getSettables().getConfiguration().cameraQuirks;
|
||||
ret.maxExposureRaw = visionSource.getSettables().getMaxExposureRaw();
|
||||
ret.minExposureRaw = visionSource.getSettables().getMinExposureRaw();
|
||||
|
||||
// TODO refactor into helper method
|
||||
var temp = new HashMap<Integer, HashMap<String, Object>>();
|
||||
@@ -541,8 +543,7 @@ public class VisionModule {
|
||||
.collect(Collectors.toList());
|
||||
|
||||
ret.isFovConfigurable =
|
||||
!(ConfigManager.getInstance().getConfig().getHardwareConfig().hasPresetFOV()
|
||||
&& cameraQuirks.hasQuirk(CameraQuirk.PiCam));
|
||||
!(ConfigManager.getInstance().getConfig().getHardwareConfig().hasPresetFOV());
|
||||
|
||||
return ret;
|
||||
}
|
||||
@@ -620,6 +621,7 @@ public class VisionModule {
|
||||
*/
|
||||
public void changeCameraQuirks(HashMap<CameraQuirk, Boolean> quirksToChange) {
|
||||
visionSource.getCameraConfiguration().cameraQuirks.updateQuirks(quirksToChange);
|
||||
visionSource.remakeSettables();
|
||||
saveAndBroadcastAll();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,4 +36,8 @@ public abstract class VisionSource {
|
||||
public abstract VisionSourceSettables getSettables();
|
||||
|
||||
public abstract boolean isVendorCamera();
|
||||
|
||||
public abstract boolean hasLEDs();
|
||||
|
||||
public abstract void remakeSettables();
|
||||
}
|
||||
|
||||
@@ -40,7 +40,7 @@ import org.photonvision.vision.camera.CameraQuirk;
|
||||
import org.photonvision.vision.camera.CameraType;
|
||||
import org.photonvision.vision.camera.LibcameraGpuSource;
|
||||
import org.photonvision.vision.camera.TestSource;
|
||||
import org.photonvision.vision.camera.USBCameraSource;
|
||||
import org.photonvision.vision.camera.USBCameras.USBCameraSource;
|
||||
|
||||
public class VisionSourceManager {
|
||||
private static final Logger logger = new Logger(VisionSourceManager.class, LogGroup.Camera);
|
||||
|
||||
@@ -42,7 +42,11 @@ public abstract class VisionSourceSettables {
|
||||
return configuration;
|
||||
}
|
||||
|
||||
public abstract void setExposure(double exposure);
|
||||
public abstract void setExposureRaw(double exposureRaw);
|
||||
|
||||
public abstract double getMinExposureRaw();
|
||||
|
||||
public abstract double getMaxExposureRaw();
|
||||
|
||||
public abstract void setAutoExposure(boolean cameraAutoExposure);
|
||||
|
||||
|
||||
@@ -29,26 +29,15 @@ public class QuirkyCameraTest {
|
||||
HashMap<CameraQuirk, Boolean> ps3EyeQuirks = new HashMap<>();
|
||||
ps3EyeQuirks.put(CameraQuirk.Gain, true);
|
||||
ps3EyeQuirks.put(CameraQuirk.FPSCap100, true);
|
||||
ps3EyeQuirks.put(CameraQuirk.PsEyeControls, true);
|
||||
for (var q : CameraQuirk.values()) {
|
||||
ps3EyeQuirks.putIfAbsent(q, false);
|
||||
}
|
||||
|
||||
QuirkyCamera psEye = QuirkyCamera.getQuirkyCamera(0x2000, 0x1415);
|
||||
QuirkyCamera psEye = QuirkyCamera.getQuirkyCamera(0x1415, 0x2000);
|
||||
Assertions.assertEquals(psEye.quirks, ps3EyeQuirks);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void picamTest() {
|
||||
HashMap<CameraQuirk, Boolean> picamQuirks = new HashMap<>();
|
||||
picamQuirks.put(CameraQuirk.PiCam, true);
|
||||
for (var q : CameraQuirk.values()) {
|
||||
picamQuirks.putIfAbsent(q, false);
|
||||
}
|
||||
|
||||
QuirkyCamera picam = QuirkyCamera.getQuirkyCamera(-1, -1, "mmal service 16.1");
|
||||
Assertions.assertEquals(picam.quirks, picamQuirks);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void quirklessCameraTest() {
|
||||
HashMap<CameraQuirk, Boolean> noQuirks = new HashMap<>();
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
/*
|
||||
* Copyright (C) Photon Vision.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.photonvision.vision.processes;
|
||||
|
||||
import edu.wpi.first.cscore.UsbCamera;
|
||||
import edu.wpi.first.cscore.VideoMode;
|
||||
import org.photonvision.common.configuration.CameraConfiguration;
|
||||
import org.photonvision.common.util.TestUtils;
|
||||
import org.photonvision.vision.camera.QuirkyCamera;
|
||||
import org.photonvision.vision.camera.USBCameras.GenericUSBCameraSettables;
|
||||
import org.photonvision.vision.camera.USBCameras.USBCameraSource;
|
||||
import org.photonvision.vision.frame.provider.FileFrameProvider;
|
||||
|
||||
public class MockUsbCameraSource extends USBCameraSource {
|
||||
/** Used for unit tests to better simulate a usb camera without a camera being present. */
|
||||
public MockUsbCameraSource(CameraConfiguration config, int pid, int vid) {
|
||||
super(config);
|
||||
|
||||
getCameraConfiguration().cameraQuirks = QuirkyCamera.getQuirkyCamera(pid, vid, config.baseName);
|
||||
|
||||
/** File used as frame provider */
|
||||
usbFrameProvider =
|
||||
new FileFrameProvider(
|
||||
TestUtils.getWPIImagePath(TestUtils.WPI2019Image.kCargoStraightDark72in_HighRes, false),
|
||||
TestUtils.WPI2019Image.FOV);
|
||||
|
||||
this.settables = createSettables(config, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public GenericUSBCameraSettables createSettables(CameraConfiguration config, UsbCamera camera) {
|
||||
return new MockUsbCameraSettables(config, null);
|
||||
}
|
||||
|
||||
private class MockUsbCameraSettables extends GenericUSBCameraSettables {
|
||||
public MockUsbCameraSettables(CameraConfiguration config, UsbCamera camera) {
|
||||
super(config, camera);
|
||||
}
|
||||
|
||||
/** Hardware-specific implementation - do nothing in test */
|
||||
@Override
|
||||
public void setExposureRaw(double exposureRaw) {}
|
||||
|
||||
/** Hardware-specific implementation - do nothing in test */
|
||||
@Override
|
||||
public void setAutoExposure(boolean cameraAutoExposure) {}
|
||||
|
||||
/** Hardware-specific implementation - do nothing in test */
|
||||
@Override
|
||||
public void setBrightness(int brightness) {}
|
||||
|
||||
@Override
|
||||
public void setGain(int gain) {}
|
||||
|
||||
@Override
|
||||
public void setVideoModeInternal(VideoMode videoMode) {}
|
||||
|
||||
@Override
|
||||
public void setUpExposureProperties() {}
|
||||
}
|
||||
}
|
||||
@@ -32,7 +32,7 @@ import org.photonvision.common.configuration.ConfigManager;
|
||||
import org.photonvision.common.dataflow.CVPipelineResultConsumer;
|
||||
import org.photonvision.common.util.TestUtils;
|
||||
import org.photonvision.vision.camera.QuirkyCamera;
|
||||
import org.photonvision.vision.camera.USBCameraSource;
|
||||
import org.photonvision.vision.camera.USBCameras.USBCameraSource;
|
||||
import org.photonvision.vision.frame.FrameProvider;
|
||||
import org.photonvision.vision.frame.FrameStaticProperties;
|
||||
import org.photonvision.vision.frame.provider.FileFrameProvider;
|
||||
@@ -68,6 +68,16 @@ public class VisionModuleManagerTest {
|
||||
public boolean isVendorCamera() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasLEDs() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void remakeSettables() {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
private static class TestSettables extends VisionSourceSettables {
|
||||
@@ -76,7 +86,7 @@ public class VisionModuleManagerTest {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setExposure(double exposure) {}
|
||||
public void setExposureRaw(double exposure) {}
|
||||
|
||||
@Override
|
||||
public void setBrightness(int brightness) {}
|
||||
@@ -103,6 +113,16 @@ public class VisionModuleManagerTest {
|
||||
|
||||
@Override
|
||||
public void setAutoExposure(boolean cameraAutoExposure) {}
|
||||
|
||||
@Override
|
||||
public double getMinExposureRaw() {
|
||||
return 1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public double getMaxExposureRaw() {
|
||||
return 1234;
|
||||
}
|
||||
}
|
||||
|
||||
private static class TestDataConsumer implements CVPipelineResultConsumer {
|
||||
@@ -171,10 +191,10 @@ public class VisionModuleManagerTest {
|
||||
|
||||
// Arducam OV9281 UC844 raspberry pi test.
|
||||
var conf4 = new CameraConfiguration("Left", "dev/video1");
|
||||
USBCameraSource usbSimulation = new USBCameraSource(conf4, 0x6366, 0x0c45, true);
|
||||
USBCameraSource usbSimulation = new MockUsbCameraSource(conf4, 0x6366, 0x0c45);
|
||||
|
||||
var conf5 = new CameraConfiguration("Right", "dev/video2");
|
||||
USBCameraSource usbSimulation2 = new USBCameraSource(conf5, 0x6366, 0x0c45, true);
|
||||
USBCameraSource usbSimulation2 = new MockUsbCameraSource(conf5, 0x6366, 0x0c45);
|
||||
|
||||
var modules =
|
||||
vmm.addSources(
|
||||
|
||||
@@ -68,12 +68,6 @@ remotes {
|
||||
password = 'raspberry'
|
||||
knownHosts = allowAnyHosts
|
||||
}
|
||||
gloworm {
|
||||
host = 'gloworm.local'
|
||||
user = 'pi'
|
||||
password = 'raspberry'
|
||||
knownHosts = allowAnyHosts
|
||||
}
|
||||
}
|
||||
|
||||
task findDeployTarget {
|
||||
|
||||
Reference in New Issue
Block a user