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:
Chris Gerth
2024-08-17 10:02:59 -05:00
committed by GitHub
parent dbe566cb55
commit f1d1d325e0
40 changed files with 1275 additions and 646 deletions

View File

@@ -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"

View File

@@ -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;
}
}

View File

@@ -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"

View File

@@ -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,

View File

@@ -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

View File

@@ -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 {

View File

@@ -58,6 +58,8 @@ export interface WebsocketCameraSettingsUpdate {
pipelineNicknames: string[];
videoFormatList: WebsocketVideoFormat;
cameraQuirks: QuirkyCamera;
minExposureRaw: number;
maxExposureRaw: number;
}
export interface WebsocketNTUpdate {
connected: boolean;

View File

@@ -178,5 +178,7 @@ public class PhotonConfiguration {
public boolean isFovConfigurable = true;
public QuirkyCamera cameraQuirks;
public boolean isCSICamera;
public double minExposureRaw;
public double maxExposureRaw;
}
}

View File

@@ -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;

View File

@@ -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,
}

View File

@@ -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;
}
}
}

View File

@@ -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;
}
}

View File

@@ -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);
}
}

View File

@@ -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);

View File

@@ -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
}
}

View File

@@ -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);
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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);
}
}

View File

@@ -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;
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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="

View File

@@ -42,7 +42,7 @@ public class ColoredShapePipelineSettings extends AdvancedPipelineSettings {
public ColoredShapePipelineSettings() {
super();
pipelineType = PipelineType.ColoredShape;
cameraExposure = 20;
cameraExposureRaw = 20;
}
@Override

View File

@@ -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

View File

@@ -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;

View File

@@ -27,7 +27,7 @@ public class ReflectivePipelineSettings extends AdvancedPipelineSettings {
public ReflectivePipelineSettings() {
super();
pipelineType = PipelineType.Reflective;
cameraExposure = 6;
cameraExposureRaw = 6;
cameraGain = 20;
}
}

View File

@@ -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();
}
}

View File

@@ -36,4 +36,8 @@ public abstract class VisionSource {
public abstract VisionSourceSettables getSettables();
public abstract boolean isVendorCamera();
public abstract boolean hasLEDs();
public abstract void remakeSettables();
}

View File

@@ -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);

View File

@@ -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);

View File

@@ -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<>();

View File

@@ -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() {}
}
}

View File

@@ -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(

View File

@@ -68,12 +68,6 @@ remotes {
password = 'raspberry'
knownHosts = allowAnyHosts
}
gloworm {
host = 'gloworm.local'
user = 'pi'
password = 'raspberry'
knownHosts = allowAnyHosts
}
}
task findDeployTarget {