Make the latency/fps setting per camera instead of global (#2260)

There's a new `low latency mode` switch on the input tab.

This replaces use_new_cscore_frametime and makes it per pipeline.

<img width="684" height="535" alt="image"
src="https://github.com/user-attachments/assets/a7ba8bc0-69b6-44f3-83e3-9b88d8219dfa"
/>

The default behavior is still to block for new frames (ie, preserve old
behavior)

---------

Co-authored-by: Matt Morley <matthew.morley.ca@gmail.com>
This commit is contained in:
Vasista Vovveti
2026-01-11 21:21:50 -08:00
committed by GitHub
parent 12a8b88b4a
commit 224ce46f14
11 changed files with 43 additions and 14 deletions

View File

@@ -158,6 +158,16 @@ const interactiveCols = computed(() =>
(args) => useCameraSettingsStore().changeCurrentPipelineSetting({ cameraWhiteBalanceTemp: args }, false)
"
/>
<pv-switch
v-model="useCameraSettingsStore().currentPipelineSettings.blockForFrames"
:disabled="!useCameraSettingsStore().currentCameraSettings.matchedCameraInfo.PVUsbCameraInfo"
label="Low Latency Mode"
:switch-cols="interactiveCols"
tooltip="When enabled, USB cameras wait for the next camera frame for lowest latency. When disabled, uses the most recent available frame for higher FPS."
@update:modelValue="
(args) => useCameraSettingsStore().changeCurrentPipelineSetting({ blockForFrames: args }, false)
"
/>
<pv-select
v-model="useCameraSettingsStore().currentPipelineSettings.inputImageRotationMode"
label="Orientation"

View File

@@ -84,6 +84,8 @@ export interface PipelineSettings {
cameraAutoWhiteBalance: boolean;
cameraWhiteBalanceTemp: number;
blockForFrames: boolean;
}
export type ConfigurablePipelineSettings = Partial<
Omit<
@@ -148,7 +150,8 @@ export const DefaultPipelineSettings: Omit<
cameraAutoWhiteBalance: false,
cameraWhiteBalanceTemp: 4000,
cameraMinExposureRaw: 1,
cameraMaxExposureRaw: 2
cameraMaxExposureRaw: 2,
blockForFrames: true
};
export interface ReflectivePipelineSettings extends PipelineSettings {

View File

@@ -191,9 +191,10 @@ public class CameraConfiguration {
}
/**
* Remove a calibration from our list.
* Remove a calibration from our list. If found, the calibration will be "released". If not found,
* no-op.
*
* @param calibration The calibration to remove
* @param unrotatedImageSize The resolution to remove.
*/
public void removeCalibration(Size unrotatedImageSize) {
logger.info("deleting calibration " + unrotatedImageSize);

View File

@@ -81,6 +81,12 @@ public class TestSource extends VisionSource {
throw new UnsupportedOperationException("Unimplemented method 'requestHsvSettings'");
}
@Override
public void requestBlockForFrames(boolean blockForFrames) {
// TODO Auto-generated method stub
throw new UnsupportedOperationException("Unimplemented method 'requestBlockForFrames'");
}
@Override
public void release() {
// TODO Auto-generated method stub

View File

@@ -59,4 +59,7 @@ public abstract class FrameProvider implements Supplier<Frame>, Releasable {
/** Ask the camera to rotate frames it outputs */
public abstract void requestHsvSettings(HSVPipe.HSVParams params);
/** Ask the camera to block for new frames (true) or use latest available (false) */
public abstract void requestBlockForFrames(boolean blockForFrames);
}

View File

@@ -47,6 +47,7 @@ public abstract class CpuImageProcessor extends FrameProvider {
private final RotateImagePipe m_rImagePipe = new RotateImagePipe();
private final GrayscalePipe m_grayPipe = new GrayscalePipe();
FrameThresholdType m_processType;
boolean m_blockForFrames = true;
private final Object m_mutex = new Object();
@@ -129,4 +130,11 @@ public abstract class CpuImageProcessor extends FrameProvider {
public void requestFrameCopies(boolean copyInput, boolean copyOutput) {
// We don't actually do zero-copy, so this method is a no-op
}
@Override
public void requestBlockForFrames(boolean blockForFrames) {
synchronized (m_mutex) {
this.m_blockForFrames = blockForFrames;
}
}
}

View File

@@ -135,6 +135,11 @@ public class LibcameraGpuFrameProvider extends FrameProvider {
LibCameraJNI.setFramesToCopy(settables.r_ptr, copyInput, copyOutput);
}
@Override
public void requestBlockForFrames(boolean blockForFrames) {
// GPU provider always blocks for frames, so this is a no-op
}
@Override
public void release() {
synchronized (settables.CAMERA_LOCK) {

View File

@@ -20,11 +20,9 @@ package org.photonvision.vision.frame.provider;
import edu.wpi.first.cameraserver.CameraServer;
import edu.wpi.first.cscore.CvSink;
import edu.wpi.first.cscore.UsbCamera;
import edu.wpi.first.networktables.BooleanSubscriber;
import edu.wpi.first.util.PixelFormat;
import edu.wpi.first.util.RawFrame;
import org.opencv.core.Mat;
import org.photonvision.common.dataflow.networktables.NetworkTablesManager;
import org.photonvision.common.logging.LogGroup;
import org.photonvision.common.logging.Logger;
import org.photonvision.jni.CscoreExtras;
@@ -44,9 +42,6 @@ public class USBFrameProvider extends CpuImageProcessor {
private long lastTime = 0;
// subscribers are lightweight, and I'm lazy
private final BooleanSubscriber useNewBehaviorSub;
@SuppressWarnings("SpellCheckingInspection")
public USBFrameProvider(
UsbCamera camera, VisionSourceSettables visionSettables, Runnable connectedCallback) {
@@ -59,10 +54,6 @@ public class USBFrameProvider extends CpuImageProcessor {
this.settables = visionSettables;
var useNewBehaviorTopic =
NetworkTablesManager.getInstance().kRootTable.getBooleanTopic("use_new_cscore_frametime");
useNewBehaviorSub = useNewBehaviorTopic.subscribe(false);
this.connectedCallback = connectedCallback;
}
@@ -86,7 +77,7 @@ public class USBFrameProvider extends CpuImageProcessor {
onCameraConnected();
}
if (!useNewBehaviorSub.get()) {
if (m_blockForFrames) {
// We allocate memory so we don't fill a Mat in use by another thread (memory model is easier)
var mat = new CVMat();
// This is from wpi::Now, or WPIUtilJNI.now(). The epoch from grabFrame is uS since

View File

@@ -61,6 +61,8 @@ public class CVPipelineSettings implements Cloneable {
public boolean cameraAutoWhiteBalance = false;
public double cameraWhiteBalanceTemp = 4000;
public boolean blockForFrames = true;
@Override
public boolean equals(Object o) {
if (this == o) return true;

View File

@@ -181,6 +181,7 @@ public class VisionRunner {
}
frameSupplier.requestFrameRotation(settings.inputImageRotationMode);
frameSupplier.requestFrameCopies(settings.inputShouldShow, settings.outputShouldShow);
frameSupplier.requestBlockForFrames(settings.blockForFrames);
// Grab the new camera frame
var frame = frameSupplier.get();

View File

@@ -49,7 +49,6 @@ public class EstimatedRobotPose {
* @param estimatedPose estimated pose
* @param timestampSeconds timestamp of the estimate
* @param targetsUsed list of targets used
* @param strategy pose strategy
*/
public EstimatedRobotPose(
Pose3d estimatedPose, double timestampSeconds, List<PhotonTrackedTarget> targetsUsed) {