Add support for removing calib coefficients (#2150)

## Description

Adds the ability to remove old calibrations. This might be helpful if
you have a bad calibration.

closes #1262

## Meta

Merge checklist:
- [x] Pull Request title is [short, imperative
summary](https://cbea.ms/git-commit/) of proposed changes
- [x] The description documents the _what_ and _why_
- [ ] If this PR changes behavior or adds a feature, user documentation
is updated
- [ ] If this PR touches photon-serde, all messages have been
regenerated and hashes have not changed unexpectedly
- [ ] If this PR touches configuration, this is backwards compatible
with settings back to v2025.3.2
- [ ] If this PR touches pipeline settings or anything related to data
exchange, the frontend typing is updated
- [ ] If this PR addresses a bug, a regression test for it is added

---------

Co-authored-by: Gold856 <117957790+Gold856@users.noreply.github.com>
This commit is contained in:
Sam Freund
2025-11-02 15:17:22 -06:00
committed by GitHub
parent 6f2603f0cb
commit dad7f0a82d
6 changed files with 133 additions and 8 deletions

View File

@@ -3,7 +3,7 @@ import type { CameraCalibrationResult, VideoFormat } from "@/types/SettingTypes"
import { useCameraSettingsStore } from "@/stores/settings/CameraSettingsStore";
import { useStateStore } from "@/stores/StateStore";
import { computed, inject, ref } from "vue";
import { getResolutionString, parseJsonFile } from "@/lib/PhotonUtils";
import { axiosPost, getResolutionString, parseJsonFile } from "@/lib/PhotonUtils";
import { useTheme } from "vuetify";
const theme = useTheme();
@@ -12,6 +12,18 @@ const props = defineProps<{
videoFormat: VideoFormat;
}>();
const confirmRemoveDialog = ref({ show: false, vf: {} as VideoFormat });
const removeCalibration = (vf: VideoFormat) => {
axiosPost("/calibration/remove", "delete a camera calibration", {
cameraUniqueName: useCameraSettingsStore().currentCameraSettings.uniqueName,
width: vf.resolution.width,
height: vf.resolution.height
});
confirmRemoveDialog.value.show = false;
};
const exportCalibration = ref();
const openExportCalibrationPrompt = () => {
exportCalibration.value.click();
@@ -93,18 +105,30 @@ const calibrationImageURL = (index: number) =>
</script>
<template>
<v-card color="surface" dark>
<div class="d-flex flex-wrap pt-2 pl-2 pr-2">
<div class="d-flex flex-wrap pt-2 pl-2 pr-2 align-center">
<v-col cols="12" md="6">
<v-card-title class="pa-0"> Calibration Details </v-card-title>
</v-col>
<v-col cols="6" md="3" class="d-flex align-center pt-0 pt-md-3 pl-6 pl-md-3">
<v-col cols="12" md="6" class="d-flex align-center pt-0 pt-md-3">
<v-btn
color="error"
:disabled="!currentCalibrationCoeffs"
class="mr-2"
style="flex: 1"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
@click="() => (confirmRemoveDialog = { show: true, vf: props.videoFormat })"
>
<v-icon start size="large">mdi-delete</v-icon>
<span>Delete</span>
</v-btn>
<v-btn
color="buttonPassive"
style="width: 100%"
class="mr-2"
style="flex: 1"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
@click="openUploadPhotonCalibJsonPrompt"
>
<v-icon start size="large"> mdi-import</v-icon>
<v-icon start size="large">mdi-import</v-icon>
<span>Import</span>
</v-btn>
<input
@@ -114,12 +138,10 @@ const calibrationImageURL = (index: number) =>
style="display: none"
@change="importCalibration"
/>
</v-col>
<v-col cols="6" md="3" class="d-flex align-center pt-0 pt-md-3 pr-6 pr-md-3">
<v-btn
color="buttonPassive"
:disabled="!currentCalibrationCoeffs"
style="width: 100%"
style="flex: 1"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
@click="openExportCalibrationPrompt"
>
@@ -289,6 +311,33 @@ const calibrationImageURL = (index: number) =>
</v-data-table>
</v-card-text>
</v-card>
<v-dialog v-model="confirmRemoveDialog.show" width="600">
<v-card color="surface" dark>
<v-card-title>Delete Calibration</v-card-title>
<v-card-text class="pt-0">
Are you sure you want to delete the calibration for {{ confirmRemoveDialog.vf.resolution.width }}x{{
confirmRemoveDialog.vf.resolution.height
}}? This cannot be undone.
<v-card-actions class="pt-5 pb-0 pr-0" style="justify-content: flex-end">
<v-btn
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
color="primary"
@click="() => (confirmRemoveDialog.show = false)"
>
Cancel
</v-btn>
<v-btn
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
color="error"
@click="removeCalibration(confirmRemoveDialog.vf)"
>
Delete
</v-btn>
</v-card-actions>
</v-card-text>
</v-card>
</v-dialog>
</template>
<style scoped>

View File

@@ -24,6 +24,7 @@ import edu.wpi.first.cscore.UsbCameraInfo;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import org.opencv.core.Size;
import org.photonvision.common.dataflow.websocket.UICameraConfiguration;
import org.photonvision.common.logging.LogGroup;
import org.photonvision.common.logging.Logger;
@@ -189,6 +190,23 @@ public class CameraConfiguration {
calibrations.add(calibration);
}
/**
* Remove a calibration from our list.
*
* @param calibration The calibration to remove
*/
public void removeCalibration(Size unrotatedImageSize) {
logger.info("deleting calibration " + unrotatedImageSize);
calibrations.stream()
.filter(it -> it.unrotatedImageSize.equals(unrotatedImageSize))
.findAny()
.ifPresent(
(it) -> {
it.release();
calibrations.remove(it);
});
}
/**
* 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

View File

@@ -680,6 +680,16 @@ public class VisionModule {
saveAndBroadcastAll();
}
public void removeCalibrationFromConfig(Size unrotatedImageSize) {
if (unrotatedImageSize != null) {
visionSource.getSettables().removeCalibration(unrotatedImageSize);
} else {
logger.error("Got null size?");
}
saveAndBroadcastAll();
}
/**
* Add/remove quirks from the camera we're controlling
*

View File

@@ -19,6 +19,7 @@ package org.photonvision.vision.processes;
import edu.wpi.first.cscore.VideoMode;
import java.util.HashMap;
import org.opencv.core.Size;
import org.photonvision.common.configuration.CameraConfiguration;
import org.photonvision.common.logging.LogGroup;
import org.photonvision.common.logging.Logger;
@@ -120,6 +121,11 @@ public abstract class VisionSourceSettables {
calculateFrameStaticProps();
}
public void removeCalibration(Size unrotatedImageSize) {
configuration.removeCalibration(unrotatedImageSize);
calculateFrameStaticProps();
}
protected void calculateFrameStaticProps() {
var videoMode = getCurrentVideoMode();
this.frameStaticProperties =

View File

@@ -35,6 +35,7 @@ import org.apache.commons.io.FileUtils;
import org.opencv.core.Mat;
import org.opencv.core.MatOfByte;
import org.opencv.core.MatOfInt;
import org.opencv.core.Size;
import org.opencv.imgcodecs.Imgcodecs;
import org.photonvision.common.configuration.ConfigManager;
import org.photonvision.common.configuration.NetworkConfig;
@@ -997,6 +998,46 @@ public class RequestHandler {
ctx.status(204);
}
private record CalibrationRemoveRequest(int width, int height, String cameraUniqueName) {}
public static void onCalibrationRemoveRequest(Context ctx) {
try {
CalibrationRemoveRequest request =
kObjectMapper.readValue(ctx.body(), CalibrationRemoveRequest.class);
logger.info(
"Attempting to remove calibration for camera: "
+ request.cameraUniqueName
+ " with a resolution of "
+ request.width
+ "x"
+ request.height);
VisionSourceManager.getInstance()
.vmm
.getModule(request.cameraUniqueName)
.removeCalibrationFromConfig(new Size(request.width, request.height));
ctx.status(200);
ctx.result(
"Successfully removed calibration for resolution: "
+ request.width
+ "x"
+ request.height);
logger.info(
"Successfully removed calibration for resolution: "
+ request.width
+ "x"
+ request.height);
} catch (JsonProcessingException e) {
ctx.status(400).result("Invalid JSON format");
logger.error("Failed to process calibration removed request", e);
} catch (Exception e) {
ctx.status(500).result("Failed to removed calibration");
logger.error("Unexpected error while attempting to remove calibration", e);
}
}
public static void onCalibrationSnapshotRequest(Context ctx) {
String cameraUniqueName = ctx.queryParam("cameraUniqueName");
var width = Integer.parseInt(ctx.queryParam("width"));

View File

@@ -148,6 +148,7 @@ public class Server {
// Calibration
app.post("/api/calibration/end", RequestHandler::onCalibrationEndRequest);
app.post("/api/calibration/importFromData", RequestHandler::onDataCalibrationImportRequest);
app.post("/api/calibration/remove", RequestHandler::onCalibrationRemoveRequest);
// Object detection
app.post("/api/objectdetection/import", RequestHandler::onImportObjectDetectionModelRequest);