Compare commits

...

21 Commits

Author SHA1 Message Date
Matt
ec66645667 Update build.yml (#1249) 2024-02-20 16:28:50 -05:00
Vasista Vovveti
39aaa34520 update wpilib to 2024.3.1 (#1246) 2024-02-20 15:08:52 -05:00
Vasista Vovveti
4a3200d0c0 Run apt update and install sqlite3 (#1247) 2024-02-19 21:37:55 -05:00
Matt
01dc7ea5ce Properly check camera info equality and handle zero cameras (#1245)
- Fix CameraInfo equality check (which prevents the same camera on a new usb port from being enumerated by us)
- Fix warning prints
- Make matchCamerasOnlyByPath apply to Windows
- Add unit tests
2024-02-19 12:34:57 -05:00
Matt
2a9502be3d Add matching by base-name only (fused off by only by path) (#1238) 2024-02-18 21:00:14 -05:00
amquake
39216db143 [photonlib] Invert simulated target yaw (#1243) 2024-02-18 20:59:54 -05:00
Matt
428f926ac2 Actually properly match cameras by name fr this time (#1237)
Our current code matches cameras in this order (which I think is objectively wrong and stupid)

- by-id (/dev/v4l/by-id/product-string)
- by path (/dev/videoN)
- product string/name, but ascii only
- asks cscore to reconnect to cameras using `path`, which on linux is actually /dev/videoN. This isn't guaranteed to stick to a camera if you replug them weirdly at runtime.

This is silly and does not consider the actual physical usb port. I propose instead, in this order:

- By physical usb port path and base name
- by physical usb port path and USB VID/PID
- By base name only (with a toggle switch to disable this, and create a new VisionModule instead)
- Give cscore /dev/video/by-path on Linux systems, pinning Photon USBCameras to a particular usb port once created.

This changes lots of things so stay paranoid!
2024-02-16 16:05:47 -05:00
Matt
4efeb3d412 Load libwinpthread-1.dll before libgcc_s_seh-1 (#1228) 2024-02-16 16:05:16 -05:00
Matt
6a2d83e19b Upload docs to VPS via SFTP (#1235)
Still in testing, might break our docs for now
2024-02-12 19:57:23 -05:00
Matt
1c0d92641f Check empty mean errors in calibration card (#1229)
Fixes calibration card disappearing if calibdb calibration was used
2024-02-12 15:55:31 -05:00
DeltaDizzy
9653c46bdb fix cpp and java photoncamera names (#1230) 2024-02-11 04:27:25 -05:00
Chris Gerth
3738e7821b fix latency calculation (#1227) 2024-02-09 18:45:38 -06:00
Tim Winters
0eb0a4e3c5 Store the last pose on update (#1207)
* Store the last pose on update

* Don't clear lastPose if pose isn't calculated

---------

Co-authored-by: Mohammad Durrani <46766905+mdurrani808@users.noreply.github.com>
2024-02-05 09:50:36 -05:00
Chris Gerth
7666f152bb Fix chessboard gen for unique square sizes (#1217) 2024-02-05 09:48:39 -05:00
Craig Schardt
45a39f6609 Remove duplicate video modes (#1221)
(Fixes #1219)
2024-02-04 22:42:01 -05:00
Matt
bc55218739 Add NPU usage to metrics on supported platforms (#1215) 2024-02-03 12:31:31 -05:00
Matt
e616d93d59 Update CameraCalibrationInfoCard.vue (#1214) 2024-02-02 21:53:47 -05:00
Chris Gerth
5851509a9e Python tweaks (#1211)
* Increasing api parity with java/cpp by adding hasTargets

* type hints fixed up

* wpiFormat
2024-02-02 14:17:53 -06:00
james20902
ea1b701ba7 Add support for different RKNN YOLO models in the backend (#1205) 2024-02-01 23:48:02 -05:00
Matt
62112cd2fd Reduce initial connection bandwidth (#1200)
Reduces bandwidth requirements by being much lazier about how much calibration data is sent to the UI.
2024-02-01 21:42:54 -05:00
Gautam
c7508fea46 Add v4l-utils to install script (#1201)
adds about 2kb to our image
2024-01-27 09:46:50 -05:00
64 changed files with 1190 additions and 347 deletions

View File

@@ -290,13 +290,13 @@ jobs:
- os: ubuntu-latest - os: ubuntu-latest
artifact-name: LinuxArm64 artifact-name: LinuxArm64
image_suffix: orangepi5 image_suffix: orangepi5
image_url: https://github.com/PhotonVision/photon-image-modifier/releases/download/v2024.0.6/photonvision_opi5.img.xz image_url: https://github.com/PhotonVision/photon-image-modifier/releases/download/v2024.0.9/photonvision_opi5.img.xz
cpu: cortex-a8 cpu: cortex-a8
image_additional_mb: 4096 image_additional_mb: 4096
- os: ubuntu-latest - os: ubuntu-latest
artifact-name: LinuxArm64 artifact-name: LinuxArm64
image_suffix: orangepi5plus image_suffix: orangepi5plus
image_url: https://github.com/PhotonVision/photon-image-modifier/releases/download/v2024.0.6/photonvision_opi5plus.img.xz image_url: https://github.com/PhotonVision/photon-image-modifier/releases/download/v2024.0.9/photonvision_opi5plus.img.xz
cpu: cortex-a8 cpu: cortex-a8
image_additional_mb: 4096 image_additional_mb: 4096

View File

@@ -68,10 +68,6 @@ jobs:
release: release:
needs: [build-client, run_docs] needs: [build-client, run_docs]
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
runs-on: ubuntu-22.04 runs-on: ubuntu-22.04
steps: steps:
@@ -79,14 +75,12 @@ jobs:
- uses: actions/download-artifact@v4 - uses: actions/download-artifact@v4
- run: find . - run: find .
- name: copy file via ssh password
- name: Setup Pages uses: appleboy/scp-action@v0.1.7
uses: actions/configure-pages@v4
- name: Upload artifact
uses: actions/upload-pages-artifact@v3
with: with:
# Upload entire repository host: ${{ secrets.WEBMASTER_SSH_HOST }}
path: '.' username: ${{ secrets.WEBMASTER_SSH_USERNAME }}
- name: Deploy to GitHub Pages password: ${{ secrets.WEBMASTER_SSH_KEY }}
id: deployment port: ${{ secrets.WEBMASTER_SSH_PORT }}
uses: actions/deploy-pages@v4 source: "*"
target: /var/www/html/photonvision-docs/

View File

@@ -4,7 +4,7 @@ plugins {
id "com.diffplug.spotless" version "6.24.0" id "com.diffplug.spotless" version "6.24.0"
id "edu.wpi.first.NativeUtils" version "2024.6.1" apply false id "edu.wpi.first.NativeUtils" version "2024.6.1" apply false
id "edu.wpi.first.wpilib.repositories.WPILibRepositoriesPlugin" version "2020.2" id "edu.wpi.first.wpilib.repositories.WPILibRepositoriesPlugin" version "2020.2"
id "edu.wpi.first.GradleRIO" version "2024.2.1" id "edu.wpi.first.GradleRIO" version "2024.3.1"
id 'edu.wpi.first.WpilibTools' version '1.3.0' id 'edu.wpi.first.WpilibTools' version '1.3.0'
id 'com.google.protobuf' version '0.9.4' apply false id 'com.google.protobuf' version '0.9.4' apply false
} }
@@ -24,13 +24,13 @@ allprojects {
apply from: "versioningHelper.gradle" apply from: "versioningHelper.gradle"
ext { ext {
wpilibVersion = "2024.2.1" wpilibVersion = "2024.3.1"
wpimathVersion = wpilibVersion wpimathVersion = wpilibVersion
openCVversion = "4.8.0-2" openCVversion = "4.8.0-2"
joglVersion = "2.4.0-rc-20200307" joglVersion = "2.4.0-rc-20200307"
javalinVersion = "5.6.2" javalinVersion = "5.6.2"
photonGlDriverLibVersion = "dev-v2023.1.0-9-g75fc678" photonGlDriverLibVersion = "dev-v2023.1.0-9-g75fc678"
rknnVersion = "dev-v2024.0.0-30-g001b5ec" rknnVersion = "dev-v2024.0.0-64-gc0836a6"
frcYear = "2024" frcYear = "2024"
mrcalVersion = "dev-v2024.0.0-7-gc976aaa"; mrcalVersion = "dev-v2024.0.0-7-gc976aaa";

View File

@@ -25,15 +25,10 @@ const getUniqueVideoFormatsByResolution = (): VideoFormat[] => {
const calib = useCameraSettingsStore().getCalibrationCoeffs(format.resolution); const calib = useCameraSettingsStore().getCalibrationCoeffs(format.resolution);
if (calib !== undefined) { if (calib !== undefined) {
// Is this the right formula for RMS error? who knows! not me!
const perViewSumSquareReprojectionError = calib.observations.flatMap((it) =>
it.reprojectionErrors.flatMap((it2) => [it2.x, it2.y])
);
// For each error, square it, sum the squares, and divide by total points N // For each error, square it, sum the squares, and divide by total points N
format.mean = Math.sqrt( if (calib.meanErrors.length)
perViewSumSquareReprojectionError.map((it) => Math.pow(it, 2)).reduce((a, b) => a + b, 0) / format.mean = calib.meanErrors.reduce((a, b) => a + b, 0) / calib.meanErrors.length;
perViewSumSquareReprojectionError.length else format.mean = NaN;
);
format.horizontalFOV = format.horizontalFOV =
2 * Math.atan2(format.resolution.width / 2, calib.cameraIntrinsics.data[0]) * (180 / Math.PI); 2 * Math.atan2(format.resolution.width / 2, calib.cameraIntrinsics.data[0]) * (180 / Math.PI);
@@ -109,7 +104,7 @@ const downloadCalibBoard = () => {
const yPos = chessboardStartY + squareY * squareSizeIn.value; const yPos = chessboardStartY + squareY * squareSizeIn.value;
// Only draw the odd squares to create the chessboard pattern // Only draw the odd squares to create the chessboard pattern
if ((xPos + yPos + 0.25) % 2 === 0) { if (squareY % 2 != squareX % 2) {
doc.rect(xPos, yPos, squareSizeIn.value, squareSizeIn.value, "F"); doc.rect(xPos, yPos, squareSizeIn.value, squareSizeIn.value, "F");
} }
} }
@@ -263,7 +258,7 @@ const setSelectedVideoFormat = (format: VideoFormat) => {
> >
<td>{{ getResolutionString(value.resolution) }}</td> <td>{{ getResolutionString(value.resolution) }}</td>
<td> <td>
{{ value.mean !== undefined ? (isNaN(value.mean) ? "NaN" : value.mean.toFixed(2) + "px") : "-" }} {{ value.mean !== undefined ? (isNaN(value.mean) ? "Unknown" : value.mean.toFixed(2) + "px") : "-" }}
</td> </td>
<td>{{ value.horizontalFOV !== undefined ? value.horizontalFOV.toFixed(2) + "°" : "-" }}</td> <td>{{ value.horizontalFOV !== undefined ? value.horizontalFOV.toFixed(2) + "°" : "-" }}</td>
<td>{{ value.verticalFOV !== undefined ? value.verticalFOV.toFixed(2) + "°" : "-" }}</td> <td>{{ value.verticalFOV !== undefined ? value.verticalFOV.toFixed(2) + "°" : "-" }}</td>

View File

@@ -1,51 +1,19 @@
<script setup lang="ts"> <script setup lang="ts">
import type { BoardObservation, CameraCalibrationResult, VideoFormat } from "@/types/SettingTypes"; import type { CameraCalibrationResult, VideoFormat } from "@/types/SettingTypes";
import { useCameraSettingsStore } from "@/stores/settings/CameraSettingsStore"; import { useCameraSettingsStore } from "@/stores/settings/CameraSettingsStore";
import { useStateStore } from "@/stores/StateStore"; import { useStateStore } from "@/stores/StateStore";
import { ref } from "vue"; import { computed, inject, ref } from "vue";
import loadingImage from "@/assets/images/loading.svg";
import { getResolutionString, parseJsonFile } from "@/lib/PhotonUtils"; import { getResolutionString, parseJsonFile } from "@/lib/PhotonUtils";
const props = defineProps<{ const props = defineProps<{
videoFormat: VideoFormat; videoFormat: VideoFormat;
}>(); }>();
const getMeanFromView = (o: BoardObservation) => { const exportCalibration = ref();
// Is this the right formula for RMS error? who knows! not me! const openExportCalibrationPrompt = () => {
const perViewSumSquareReprojectionError = o.reprojectionErrors.flatMap((it2) => [it2.x, it2.y]); exportCalibration.value.click();
// For each error, square it, sum the squares, and divide by total points N
return Math.sqrt(
perViewSumSquareReprojectionError.map((it) => Math.pow(it, 2)).reduce((a, b) => a + b, 0) /
perViewSumSquareReprojectionError.length
);
}; };
// Import and export functions
const downloadCalibration = () => {
const calibData = useCameraSettingsStore().getCalibrationCoeffs(props.videoFormat.resolution);
if (calibData === undefined) {
useStateStore().showSnackbarMessage({
color: "error",
message:
"Calibration data isn't available for the requested resolution, please calibrate the requested resolution first"
});
return;
}
const camUniqueName = useCameraSettingsStore().currentCameraSettings.uniqueName;
const filename = `photon_calibration_${camUniqueName}_${calibData.resolution.width}x${calibData.resolution.height}.json`;
const fileData = JSON.stringify(calibData);
const element = document.createElement("a");
element.style.display = "none";
element.setAttribute("href", "data:text/plain;charset=utf-8," + encodeURIComponent(fileData));
element.setAttribute("download", filename);
document.body.appendChild(element);
element.click();
document.body.removeChild(element);
};
const importCalibrationFromPhotonJson = ref(); const importCalibrationFromPhotonJson = ref();
const openUploadPhotonCalibJsonPrompt = () => { const openUploadPhotonCalibJsonPrompt = () => {
importCalibrationFromPhotonJson.value.click(); importCalibrationFromPhotonJson.value.click();
@@ -97,19 +65,28 @@ const importCalibration = async () => {
}; };
interface ObservationDetails { interface ObservationDetails {
snapshotSrc: any;
mean: number; mean: number;
index: number; index: number;
} }
const currentCalibrationCoeffs = computed<CameraCalibrationResult | undefined>(() =>
useCameraSettingsStore().getCalibrationCoeffs(props.videoFormat.resolution)
);
const getObservationDetails = (): ObservationDetails[] | undefined => { const getObservationDetails = (): ObservationDetails[] | undefined => {
return useCameraSettingsStore() const coefficients = currentCalibrationCoeffs.value;
.getCalibrationCoeffs(props.videoFormat.resolution)
?.observations.map((o, i) => ({ return coefficients?.meanErrors.map((m, i) => ({
index: i, index: i,
mean: parseFloat(getMeanFromView(o).toFixed(2)), mean: parseFloat(m.toFixed(2))
snapshotSrc: o.includeObservationInCalibration ? "data:image/png;base64," + o.snapshotData.data : loadingImage }));
}));
}; };
const exportCalibrationURL = computed<string>(() =>
useCameraSettingsStore().getCalJSONUrl(inject("backendHost") as string, props.videoFormat.resolution)
);
const calibrationImageURL = (index: number) =>
useCameraSettingsStore().getCalImageUrl(inject<string>("backendHost") as string, props.videoFormat.resolution, index);
</script> </script>
<template> <template>
@@ -140,19 +117,22 @@ const getObservationDetails = (): ObservationDetails[] | undefined => {
<v-btn <v-btn
color="secondary" color="secondary"
class="mt-4" class="mt-4"
:disabled="useCameraSettingsStore().getCalibrationCoeffs(props.videoFormat.resolution) === undefined" :disabled="!currentCalibrationCoeffs"
style="width: 100%" style="width: 100%"
@click="downloadCalibration" @click="openExportCalibrationPrompt"
> >
<v-icon left>mdi-export</v-icon> <v-icon left>mdi-export</v-icon>
<span>Export</span> <span>Export</span>
</v-btn> </v-btn>
<a
ref="exportCalibration"
style="color: black; text-decoration: none; display: none"
:href="exportCalibrationURL"
target="_blank"
/>
</v-col> </v-col>
</v-row> </v-row>
<v-row <v-row v-if="currentCalibrationCoeffs" class="pt-2">
v-if="useCameraSettingsStore().getCalibrationCoeffs(props.videoFormat.resolution) !== undefined"
class="pt-2"
>
<v-card-subtitle>Calibration Details</v-card-subtitle> <v-card-subtitle>Calibration Details</v-card-subtitle>
<v-simple-table dense style="width: 100%" class="pl-2 pr-2"> <v-simple-table dense style="width: 100%" class="pl-2 pr-2">
<template #default> <template #default>
@@ -231,7 +211,9 @@ const getObservationDetails = (): ObservationDetails[] | undefined => {
</tr> </tr>
<tr> <tr>
<td>Horizontal FOV</td> <td>Horizontal FOV</td>
<td>{{ videoFormat.horizontalFOV !== undefined ? videoFormat.horizontalFOV.toFixed(2) + "°" : "-" }}</td> <td>
{{ videoFormat.horizontalFOV !== undefined ? videoFormat.horizontalFOV.toFixed(2) + "°" : "-" }}
</td>
</tr> </tr>
<tr> <tr>
<td>Vertical FOV</td> <td>Vertical FOV</td>
@@ -242,11 +224,7 @@ const getObservationDetails = (): ObservationDetails[] | undefined => {
<td>{{ videoFormat.diagonalFOV !== undefined ? videoFormat.diagonalFOV.toFixed(2) + "°" : "-" }}</td> <td>{{ videoFormat.diagonalFOV !== undefined ? videoFormat.diagonalFOV.toFixed(2) + "°" : "-" }}</td>
</tr> </tr>
<!-- Board warp, only shown for mrcal-calibrated cameras --> <!-- Board warp, only shown for mrcal-calibrated cameras -->
<tr <tr v-if="currentCalibrationCoeffs?.calobjectWarp?.length === 2">
v-if="
useCameraSettingsStore().getCalibrationCoeffs(props.videoFormat.resolution)?.calobjectWarp?.length === 2
"
>
<td>Board warp, X/Y</td> <td>Board warp, X/Y</td>
<td> <td>
{{ {{
@@ -278,7 +256,7 @@ const getObservationDetails = (): ObservationDetails[] | undefined => {
<template #expanded-item="{ headers, item }"> <template #expanded-item="{ headers, item }">
<td :colspan="headers.length"> <td :colspan="headers.length">
<div style="display: flex; justify-content: center; width: 100%"> <div style="display: flex; justify-content: center; width: 100%">
<img :src="item.snapshotSrc" alt="observation image" class="snapshot-preview pt-2 pb-2" /> <img :src="calibrationImageURL(item.index)" alt="observation image" class="snapshot-preview pt-2 pb-2" />
</div> </div>
</td> </td>
</template> </template>

View File

@@ -27,42 +27,54 @@ const generalMetrics = computed<MetricItem[]>(() => [
value: useSettingsStore().general.gpuAcceleration || "Unknown" value: useSettingsStore().general.gpuAcceleration || "Unknown"
} }
]); ]);
const platformMetrics = computed<MetricItem[]>(() => [
{ const platformMetrics = computed<MetricItem[]>(() => {
header: "CPU Temp", const stats = [
value: useSettingsStore().metrics.cpuTemp === undefined ? "Unknown" : `${useSettingsStore().metrics.cpuTemp}°C` {
}, header: "CPU Temp",
{ value: useSettingsStore().metrics.cpuTemp === undefined ? "Unknown" : `${useSettingsStore().metrics.cpuTemp}°C`
header: "CPU Usage", },
value: useSettingsStore().metrics.cpuUtil === undefined ? "Unknown" : `${useSettingsStore().metrics.cpuUtil}%` {
}, header: "CPU Usage",
{ value: useSettingsStore().metrics.cpuUtil === undefined ? "Unknown" : `${useSettingsStore().metrics.cpuUtil}%`
header: "CPU Memory Usage", },
value: {
useSettingsStore().metrics.ramUtil === undefined || useSettingsStore().metrics.cpuMem === undefined header: "CPU Memory Usage",
? "Unknown" value:
: `${useSettingsStore().metrics.ramUtil || "Unknown"}MB of ${useSettingsStore().metrics.cpuMem}MB` useSettingsStore().metrics.ramUtil === undefined || useSettingsStore().metrics.cpuMem === undefined
}, ? "Unknown"
{ : `${useSettingsStore().metrics.ramUtil || "Unknown"}MB of ${useSettingsStore().metrics.cpuMem}MB`
header: "GPU Memory Usage", },
value: {
useSettingsStore().metrics.gpuMemUtil === undefined || useSettingsStore().metrics.gpuMem === undefined header: "GPU Memory Usage",
? "Unknown" value:
: `${useSettingsStore().metrics.gpuMemUtil}MB of ${useSettingsStore().metrics.gpuMem}MB` useSettingsStore().metrics.gpuMemUtil === undefined || useSettingsStore().metrics.gpuMem === undefined
}, ? "Unknown"
{ : `${useSettingsStore().metrics.gpuMemUtil}MB of ${useSettingsStore().metrics.gpuMem}MB`
header: "CPU Throttling", },
value: useSettingsStore().metrics.cpuThr || "Unknown" {
}, header: "CPU Throttling",
{ value: useSettingsStore().metrics.cpuThr || "Unknown"
header: "CPU Uptime", },
value: useSettingsStore().metrics.cpuUptime || "Unknown" {
}, header: "CPU Uptime",
{ value: useSettingsStore().metrics.cpuUptime || "Unknown"
header: "Disk Usage", },
value: useSettingsStore().metrics.diskUtilPct || "Unknown" {
header: "Disk Usage",
value: useSettingsStore().metrics.diskUtilPct || "Unknown"
}
];
if (useSettingsStore().metrics.npuUsage) {
stats.push({
header: "NPU Usage",
value: useSettingsStore().metrics.npuUsage || "Unknown"
});
} }
]);
return stats;
});
const metricsLastFetched = ref("Never"); const metricsLastFetched = ref("Never");
const fetchMetrics = () => { const fetchMetrics = () => {

View File

@@ -59,7 +59,8 @@ const settingsHaveChanged = (): boolean => {
a.shouldPublishProto !== b.shouldPublishProto || a.shouldPublishProto !== b.shouldPublishProto ||
a.networkManagerIface !== b.networkManagerIface || a.networkManagerIface !== b.networkManagerIface ||
a.setStaticCommand !== b.setStaticCommand || a.setStaticCommand !== b.setStaticCommand ||
a.setDHCPcommand !== b.setDHCPcommand a.setDHCPcommand !== b.setDHCPcommand ||
a.matchCamerasOnlyByPath !== b.matchCamerasOnlyByPath
); );
}; };
@@ -77,6 +78,7 @@ const saveGeneralSettings = () => {
setStaticCommand: tempSettingsStruct.value.setStaticCommand || "", setStaticCommand: tempSettingsStruct.value.setStaticCommand || "",
shouldManage: tempSettingsStruct.value.shouldManage, shouldManage: tempSettingsStruct.value.shouldManage,
shouldPublishProto: tempSettingsStruct.value.shouldPublishProto, shouldPublishProto: tempSettingsStruct.value.shouldPublishProto,
matchCamerasOnlyByPath: tempSettingsStruct.value.matchCamerasOnlyByPath,
staticIp: tempSettingsStruct.value.staticIp staticIp: tempSettingsStruct.value.staticIp
}; };
@@ -137,6 +139,8 @@ watchEffect(() => {
<template> <template>
<v-card dark class="mb-3 pr-6 pb-3" style="background-color: #006492"> <v-card dark class="mb-3 pr-6 pb-3" style="background-color: #006492">
<v-card-title>Global Settings</v-card-title>
<v-divider />
<v-card-title>Networking</v-card-title> <v-card-title>Networking</v-card-title>
<div class="ml-5"> <div class="ml-5">
<v-form ref="form" v-model="settingsValid"> <v-form ref="form" v-model="settingsValid">
@@ -254,6 +258,9 @@ watchEffect(() => {
> >
This mode is intended for debugging; it should be off for proper usage. PhotonLib will NOT work! This mode is intended for debugging; it should be off for proper usage. PhotonLib will NOT work!
</v-banner> </v-banner>
<v-divider />
<v-card-title>Miscellaneous</v-card-title>
<pv-switch <pv-switch
v-model="tempSettingsStruct.shouldPublishProto" v-model="tempSettingsStruct.shouldPublishProto"
label="Also Publish Protobuf" label="Also Publish Protobuf"
@@ -272,6 +279,32 @@ watchEffect(() => {
This mode is intended for debugging; it should be off for field use. You may notice a performance hit by using This mode is intended for debugging; it should be off for field use. You may notice a performance hit by using
this mode. this mode.
</v-banner> </v-banner>
<pv-switch
v-model="tempSettingsStruct.matchCamerasOnlyByPath"
label="Strictly match ONLY known cameras"
tooltip="ONLY match cameras by the USB port they're plugged into + (basename or USB VID/PID), and never only by the device product string. Also disables automatic detection of new cameras."
class="mt-3 mb-2"
:label-cols="4"
/>
<v-banner
v-show="tempSettingsStruct.matchCamerasOnlyByPath"
rounded
color="red"
class="mb-3"
text-color="white"
icon="mdi-information-outline"
>
Physical cameras will be strictly matched to camera configurations using physical USB port they are plugged
into, in addition to device name and other USB metadata. Additionally, no new cameras are allowed to be added.
This setting is useful for guaranteeing that an already known and configured camera can never be matched as an
"unknown"/"new" camera, which resets pipelines and calibration data.
<p />
Cameras will NOT be matched if they change USB ports, and new cameras plugged into this coprocessor will NOT
be automatically recognized or configured for vision processing.
<p />
To add a new camera to this coprocessor, disable this setting, connect the camera, and re-enable.
</v-banner>
<v-divider class="mb-3" />
</v-form> </v-form>
<v-btn <v-btn
color="accent" color="accent"

View File

@@ -416,6 +416,23 @@ export const useCameraSettingsStore = defineStore("cameraSettings", {
cameraIndex: number = useStateStore().currentCameraIndex cameraIndex: number = useStateStore().currentCameraIndex
): CameraCalibrationResult | undefined { ): CameraCalibrationResult | undefined {
return this.cameras[cameraIndex].completeCalibrations.find((v) => resolutionsAreEqual(v.resolution, resolution)); return this.cameras[cameraIndex].completeCalibrations.find((v) => resolutionsAreEqual(v.resolution, resolution));
},
getCalImageUrl(host: string, resolution: Resolution, idx: number, cameraIdx = useStateStore().currentCameraIndex) {
const url = new URL(`http://${host}/api/utils/getCalSnapshot`);
url.searchParams.set("width", Math.round(resolution.width).toFixed(0));
url.searchParams.set("height", Math.round(resolution.height).toFixed(0));
url.searchParams.set("snapshotIdx", Math.round(idx).toFixed(0));
url.searchParams.set("cameraIdx", Math.round(cameraIdx).toFixed(0));
return url.href;
},
getCalJSONUrl(host: string, resolution: Resolution, cameraIdx = useStateStore().currentCameraIndex) {
const url = new URL(`http://${host}/api/utils/getCalibrationJSON`);
url.searchParams.set("width", Math.round(resolution.width).toFixed(0));
url.searchParams.set("height", Math.round(resolution.height).toFixed(0));
url.searchParams.set("cameraIdx", Math.round(cameraIdx).toFixed(0));
return url.href;
} }
} }
}); });

View File

@@ -44,7 +44,9 @@ export const useSettingsStore = defineStore("settings", {
connName: "Example Wired Connection", connName: "Example Wired Connection",
devName: "eth0" devName: "eth0"
} }
] ],
networkingDisabled: false,
matchCamerasOnlyByPath: false
}, },
lighting: { lighting: {
supported: true, supported: true,
@@ -59,7 +61,8 @@ export const useSettingsStore = defineStore("settings", {
gpuMemUtil: undefined, gpuMemUtil: undefined,
cpuThr: undefined, cpuThr: undefined,
cpuUptime: undefined, cpuUptime: undefined,
diskUtilPct: undefined diskUtilPct: undefined,
npuUsage: undefined
}, },
currentFieldLayout: { currentFieldLayout: {
field: { field: {
@@ -91,7 +94,8 @@ export const useSettingsStore = defineStore("settings", {
gpuMemUtil: data.gpuMemUtil || undefined, gpuMemUtil: data.gpuMemUtil || undefined,
cpuThr: data.cpuThr || undefined, cpuThr: data.cpuThr || undefined,
cpuUptime: data.cpuUptime || undefined, cpuUptime: data.cpuUptime || undefined,
diskUtilPct: data.diskUtilPct || undefined diskUtilPct: data.diskUtilPct || undefined,
npuUsage: data.npuUsage || undefined
}; };
}, },
updateGeneralSettingsFromWebsocket(data: WebsocketSettingsUpdate) { updateGeneralSettingsFromWebsocket(data: WebsocketSettingsUpdate) {

View File

@@ -20,6 +20,7 @@ export interface MetricData {
cpuThr?: string; cpuThr?: string;
cpuUptime?: string; cpuUptime?: string;
diskUtilPct?: string; diskUtilPct?: string;
npuUsage?: string;
} }
export enum NetworkConnectionType { export enum NetworkConnectionType {
@@ -46,6 +47,7 @@ export interface NetworkSettings {
setDHCPcommand?: string; setDHCPcommand?: string;
networkInterfaceNames: NetworkInterfaceType[]; networkInterfaceNames: NetworkInterfaceType[];
networkingDisabled: boolean; networkingDisabled: boolean;
matchCamerasOnlyByPath: boolean;
} }
export type ConfigurableNetworkSettings = Omit< export type ConfigurableNetworkSettings = Omit<
@@ -138,6 +140,9 @@ export interface CameraCalibrationResult {
distCoeffs: JsonMatOfDouble; distCoeffs: JsonMatOfDouble;
observations: BoardObservation[]; observations: BoardObservation[];
calobjectWarp?: number[]; calobjectWarp?: number[];
// We might have to omit observations for bandwith, so backend will send us this
numSnapshots: number;
meanErrors: number[];
} }
export enum ValidQuirks { export enum ValidQuirks {
@@ -255,7 +260,9 @@ export const PlaceholderCameraSettings: CameraSettings = {
snapshotName: "img0.png", snapshotName: "img0.png",
snapshotData: { rows: 480, cols: 640, type: CvType.CV_8U, data: "" } snapshotData: { rows: 480, cols: 640, type: CvType.CV_8U, data: "" }
} }
] ],
numSnapshots: 1,
meanErrors: [123.45]
} }
], ],
pipelineNicknames: ["Placeholder Pipeline"], pipelineNicknames: ["Placeholder Pipeline"],

View File

@@ -37,9 +37,8 @@ dependencies {
implementation 'org.zeroturnaround:zt-zip:1.14' implementation 'org.zeroturnaround:zt-zip:1.14'
implementation "org.xerial:sqlite-jdbc:3.41.0.0" implementation "org.xerial:sqlite-jdbc:3.41.0.0"
def rknnjniversion = "dev-v2024.0.0-44-g8022c40" implementation "org.photonvision:rknn_jni-jni:$rknnVersion:linuxarm64"
implementation "org.photonvision:rknn_jni-jni:$rknnjniversion:linuxarm64" implementation "org.photonvision:rknn_jni-java:$rknnVersion"
implementation "org.photonvision:rknn_jni-java:$rknnjniversion"
implementation "org.photonvision:photon-libcamera-gl-driver-jni:$photonGlDriverLibVersion:linuxarm64" implementation "org.photonvision:photon-libcamera-gl-driver-jni:$photonGlDriverLibVersion:linuxarm64"
implementation "org.photonvision:photon-libcamera-gl-driver-java:$photonGlDriverLibVersion" implementation "org.photonvision:photon-libcamera-gl-driver-java:$photonGlDriverLibVersion"

View File

@@ -23,6 +23,7 @@ import com.fasterxml.jackson.annotation.JsonProperty;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.List; import java.util.List;
import java.util.Optional;
import org.photonvision.common.logging.LogGroup; import org.photonvision.common.logging.LogGroup;
import org.photonvision.common.logging.Logger; import org.photonvision.common.logging.Logger;
import org.photonvision.vision.calibration.CameraCalibrationCoefficients; import org.photonvision.vision.calibration.CameraCalibrationCoefficients;
@@ -51,6 +52,12 @@ public class CameraConfiguration {
@JsonIgnore public String[] otherPaths = {}; @JsonIgnore public String[] otherPaths = {};
@JsonProperty("usbVID")
public int usbVID = -1;
@JsonProperty("usbPID")
public int usbPID = -1;
public CameraType cameraType = CameraType.UsbCamera; public CameraType cameraType = CameraType.UsbCamera;
public double FOV = 70; public double FOV = 70;
public final List<CameraCalibrationCoefficients> calibrations; public final List<CameraCalibrationCoefficients> calibrations;
@@ -98,7 +105,9 @@ public class CameraConfiguration {
@JsonProperty("cameraType") CameraType cameraType, @JsonProperty("cameraType") CameraType cameraType,
@JsonProperty("cameraQuirks") QuirkyCamera cameraQuirks, @JsonProperty("cameraQuirks") QuirkyCamera cameraQuirks,
@JsonProperty("calibration") List<CameraCalibrationCoefficients> calibrations, @JsonProperty("calibration") List<CameraCalibrationCoefficients> calibrations,
@JsonProperty("currentPipelineIndex") int currentPipelineIndex) { @JsonProperty("currentPipelineIndex") int currentPipelineIndex,
@JsonProperty("usbVID") int usbVID,
@JsonProperty("usbPID") int usbPID) {
this.baseName = baseName; this.baseName = baseName;
this.uniqueName = uniqueName; this.uniqueName = uniqueName;
this.nickname = nickname; this.nickname = nickname;
@@ -108,6 +117,8 @@ public class CameraConfiguration {
this.cameraQuirks = cameraQuirks; this.cameraQuirks = cameraQuirks;
this.calibrations = calibrations != null ? calibrations : new ArrayList<>(); this.calibrations = calibrations != null ? calibrations : new ArrayList<>();
this.currentPipelineIndex = currentPipelineIndex; this.currentPipelineIndex = currentPipelineIndex;
this.usbPID = usbPID;
this.usbVID = usbVID;
logger.debug( logger.debug(
"Creating camera configuration for " "Creating camera configuration for "
@@ -156,6 +167,17 @@ public class CameraConfiguration {
calibrations.add(calibration); calibrations.add(calibration);
} }
/**
* Get a unique descriptor of the USB port this camera is attached to. EG
* "/dev/v4l/by-path/platform-fc800000.usb-usb-0:1.3:1.0-video-index0"
*
* @return
*/
@JsonIgnore
public Optional<String> getUSBPath() {
return Arrays.stream(otherPaths).filter(path -> path.contains("/by-path/")).findFirst();
}
@Override @Override
public String toString() { public String toString() {
return "CameraConfiguration [baseName=" return "CameraConfiguration [baseName="

View File

@@ -20,6 +20,24 @@ package org.photonvision.common.configuration;
public class HardwareSettings { public class HardwareSettings {
public int ledBrightnessPercentage = 100; public int ledBrightnessPercentage = 100;
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ledBrightnessPercentage;
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (obj == null) return false;
if (getClass() != obj.getClass()) return false;
HardwareSettings other = (HardwareSettings) obj;
if (ledBrightnessPercentage != other.ledBrightnessPercentage) return false;
return true;
}
@Override @Override
public String toString() { public String toString() {
return "HardwareSettings [ledBrightnessPercentage=" + ledBrightnessPercentage + "]"; return "HardwareSettings [ledBrightnessPercentage=" + ledBrightnessPercentage + "]";

View File

@@ -39,6 +39,14 @@ public class NetworkConfig {
public boolean shouldManage; public boolean shouldManage;
public boolean shouldPublishProto = false; public boolean shouldPublishProto = false;
/**
* If we should ONLY match cameras by path, and NEVER only by base-name. For now default to false
* to preserve old matching logic.
*
* <p>This also disables creating new CameraConfigurations for detected "new" cameras.
*/
public boolean matchCamerasOnlyByPath = false;
@JsonIgnore public static final String NM_IFACE_STRING = "${interface}"; @JsonIgnore public static final String NM_IFACE_STRING = "${interface}";
@JsonIgnore public static final String NM_IP_STRING = "${ipaddr}"; @JsonIgnore public static final String NM_IP_STRING = "${ipaddr}";
@@ -76,7 +84,8 @@ public class NetworkConfig {
@JsonProperty("shouldPublishProto") boolean shouldPublishProto, @JsonProperty("shouldPublishProto") boolean shouldPublishProto,
@JsonProperty("networkManagerIface") String networkManagerIface, @JsonProperty("networkManagerIface") String networkManagerIface,
@JsonProperty("setStaticCommand") String setStaticCommand, @JsonProperty("setStaticCommand") String setStaticCommand,
@JsonProperty("setDHCPcommand") String setDHCPcommand) { @JsonProperty("setDHCPcommand") String setDHCPcommand,
@JsonProperty("matchCamerasOnlyByPath") boolean matchCamerasOnlyByPath) {
this.ntServerAddress = ntServerAddress; this.ntServerAddress = ntServerAddress;
this.connectionType = connectionType; this.connectionType = connectionType;
this.staticIp = staticIp; this.staticIp = staticIp;
@@ -86,6 +95,7 @@ public class NetworkConfig {
this.networkManagerIface = networkManagerIface; this.networkManagerIface = networkManagerIface;
this.setStaticCommand = setStaticCommand; this.setStaticCommand = setStaticCommand;
this.setDHCPcommand = setDHCPcommand; this.setDHCPcommand = setDHCPcommand;
this.matchCamerasOnlyByPath = matchCamerasOnlyByPath;
setShouldManage(shouldManage); setShouldManage(shouldManage);
} }

View File

@@ -25,12 +25,14 @@ import java.nio.file.Paths;
import java.util.List; import java.util.List;
import org.photonvision.common.logging.LogGroup; import org.photonvision.common.logging.LogGroup;
import org.photonvision.common.logging.Logger; import org.photonvision.common.logging.Logger;
import org.photonvision.rknn.RknnJNI;
public class NeuralNetworkModelManager { public class NeuralNetworkModelManager {
private static NeuralNetworkModelManager INSTANCE; private static NeuralNetworkModelManager INSTANCE;
private static final Logger logger = new Logger(NeuralNetworkModelManager.class, LogGroup.Config); private static final Logger logger = new Logger(NeuralNetworkModelManager.class, LogGroup.Config);
private final String MODEL_NAME = "note-640-640-yolov5s.rknn"; private final String MODEL_NAME = "note-640-640-yolov5s.rknn";
private final RknnJNI.ModelVersion modelVersion = RknnJNI.ModelVersion.YOLO_V5;
private File defaultModelFile; private File defaultModelFile;
private List<String> labels; private List<String> labels;
@@ -51,7 +53,7 @@ public class NeuralNetworkModelManager {
this.defaultModelFile = new File(modelsFolder, MODEL_NAME); this.defaultModelFile = new File(modelsFolder, MODEL_NAME);
extractResource(modelResourcePath, defaultModelFile); extractResource(modelResourcePath, defaultModelFile);
File labelsFile = new File(modelsFolder, "labels.txt"); File labelsFile = new File(modelsFolder, "labels_v5.txt");
var labelResourcePath = "/models/" + labelsFile.getName(); var labelResourcePath = "/models/" + labelsFile.getName();
extractResource(labelResourcePath, labelsFile); extractResource(labelResourcePath, labelsFile);
@@ -95,4 +97,8 @@ public class NeuralNetworkModelManager {
public List<String> getLabels() { public List<String> getLabels() {
return labels; return labels;
} }
public RknnJNI.ModelVersion getModelVersion() {
return modelVersion;
}
} }

View File

@@ -31,7 +31,7 @@ import org.photonvision.common.util.SerializationUtils;
import org.photonvision.jni.RknnDetectorJNI; import org.photonvision.jni.RknnDetectorJNI;
import org.photonvision.mrcal.MrCalJNILoader; import org.photonvision.mrcal.MrCalJNILoader;
import org.photonvision.raspi.LibCameraJNILoader; import org.photonvision.raspi.LibCameraJNILoader;
import org.photonvision.vision.calibration.CameraCalibrationCoefficients; import org.photonvision.vision.calibration.UICameraCalibrationCoefficients;
import org.photonvision.vision.camera.QuirkyCamera; import org.photonvision.vision.camera.QuirkyCamera;
import org.photonvision.vision.processes.VisionModule; import org.photonvision.vision.processes.VisionModule;
import org.photonvision.vision.processes.VisionModuleManager; import org.photonvision.vision.processes.VisionModuleManager;
@@ -126,13 +126,6 @@ public class PhotonConfiguration {
settingsSubmap.put("networkSettings", netConfigMap); settingsSubmap.put("networkSettings", netConfigMap);
map.put(
"cameraSettings",
VisionModuleManager.getInstance().getModules().stream()
.map(VisionModule::toUICameraConfig)
.map(SerializationUtils::objectToHashMap)
.collect(Collectors.toList()));
var lightingConfig = new UILightingConfig(); var lightingConfig = new UILightingConfig();
lightingConfig.brightness = hardwareSettings.ledBrightnessPercentage; lightingConfig.brightness = hardwareSettings.ledBrightnessPercentage;
lightingConfig.supported = !hardwareConfig.ledPins.isEmpty(); lightingConfig.supported = !hardwareConfig.ledPins.isEmpty();
@@ -181,7 +174,7 @@ public class PhotonConfiguration {
public HashMap<Integer, HashMap<String, Object>> videoFormatList; public HashMap<Integer, HashMap<String, Object>> videoFormatList;
public int outputStreamPort; public int outputStreamPort;
public int inputStreamPort; public int inputStreamPort;
public List<CameraCalibrationCoefficients> calibrations; public List<UICameraCalibrationCoefficients> calibrations;
public boolean isFovConfigurable = true; public boolean isFovConfigurable = true;
public QuirkyCamera cameraQuirks; public QuirkyCamera cameraQuirks;
public boolean isCSICamera; public boolean isCSICamera;

View File

@@ -26,6 +26,7 @@ import org.photonvision.common.logging.LogGroup;
import org.photonvision.common.logging.Logger; import org.photonvision.common.logging.Logger;
import org.photonvision.common.util.SerializationUtils; import org.photonvision.common.util.SerializationUtils;
import org.photonvision.vision.pipeline.result.CVPipelineResult; import org.photonvision.vision.pipeline.result.CVPipelineResult;
import org.photonvision.vision.pipeline.result.CalibrationPipelineResult;
public class UIDataPublisher implements CVPipelineResultConsumer { public class UIDataPublisher implements CVPipelineResultConsumer {
private static final Logger logger = new Logger(UIDataPublisher.class, LogGroup.VisionModule); private static final Logger logger = new Logger(UIDataPublisher.class, LogGroup.VisionModule);
@@ -41,16 +42,22 @@ public class UIDataPublisher implements CVPipelineResultConsumer {
public void accept(CVPipelineResult result) { public void accept(CVPipelineResult result) {
long now = System.currentTimeMillis(); long now = System.currentTimeMillis();
// only update the UI at 15hz // only update the UI at 10hz
if (lastUIResultUpdateTime + 1000.0 / 10.0 > now) return; if (lastUIResultUpdateTime + 1000.0 / 10.0 > now) return;
var dataMap = new HashMap<String, Object>(); var dataMap = new HashMap<String, Object>();
dataMap.put("fps", result.fps); dataMap.put("fps", result.fps);
dataMap.put("latency", result.getLatencyMillis()); dataMap.put("latency", result.getLatencyMillis());
var uiTargets = new ArrayList<HashMap<String, Object>>(result.targets.size()); var uiTargets = new ArrayList<HashMap<String, Object>>(result.targets.size());
for (var t : result.targets) {
uiTargets.add(t.toHashMap()); // We don't actually need to send targets during calibration and it can take up a lot (up to
// 1.2Mbps for 60 snapshots) of target results with no pitch/yaw/etc set
if (!(result instanceof CalibrationPipelineResult)) {
for (var t : result.targets) {
uiTargets.add(t.toHashMap());
}
} }
dataMap.put("targets", uiTargets); dataMap.put("targets", uiTargets);
dataMap.put("classNames", result.objectDetectionClassNames); dataMap.put("classNames", result.objectDetectionClassNames);

View File

@@ -92,6 +92,10 @@ public class MetricsManager {
return safeExecute(cmds.cpuThrottleReasonCmd); return safeExecute(cmds.cpuThrottleReasonCmd);
} }
public String getNpuUsage() {
return safeExecute(cmds.npuUsageCommand);
}
private String gpuMemSave = null; private String gpuMemSave = null;
public String getGPUMemorySplit() { public String getGPUMemorySplit() {
@@ -128,6 +132,7 @@ public class MetricsManager {
metrics.put("ramUtil", this.getUsedRam()); metrics.put("ramUtil", this.getUsedRam());
metrics.put("gpuMemUtil", this.getMallocedMemory()); metrics.put("gpuMemUtil", this.getMallocedMemory());
metrics.put("diskUtilPct", this.getUsedDiskPct()); metrics.put("diskUtilPct", this.getUsedDiskPct());
metrics.put("npuUsage", this.getNpuUsage());
DataChangeService.getInstance().publishEvent(OutgoingUIEvent.wrappedOf("metrics", metrics)); DataChangeService.getInstance().publishEvent(OutgoingUIEvent.wrappedOf("metrics", metrics));
} }

View File

@@ -29,6 +29,8 @@ public class CmdBase {
// GPU // GPU
public String gpuMemoryCommand = ""; public String gpuMemoryCommand = "";
public String gpuMemUsageCommand = ""; public String gpuMemUsageCommand = "";
// NPU
public String npuUsageCommand = "";
// RAM // RAM
public String ramUsageCommand = ""; public String ramUsageCommand = "";
// Disk // Disk

View File

@@ -44,5 +44,7 @@ public class RK3588Cmds extends LinuxCmds {
*/ */
cpuTemperatureCommand = cpuTemperatureCommand =
"cat /sys/class/thermal/thermal_zone1/temp | awk '{printf \"%.1f\", $1/1000}'"; "cat /sys/class/thermal/thermal_zone1/temp | awk '{printf \"%.1f\", $1/1000}'";
npuUsageCommand = "cat /sys/kernel/debug/rknpu/load | sed 's/NPU load://; s/^ *//; s/ *$//'";
} }
} }

View File

@@ -65,12 +65,11 @@ public class RknnDetectorJNI extends PhotonJNICommon {
long objPointer = -1; long objPointer = -1;
private List<String> labels; private List<String> labels;
private final Object lock = new Object(); private final Object lock = new Object();
private static final CopyOnWriteArrayList<Long> detectors = new CopyOnWriteArrayList<>(); private static final CopyOnWriteArrayList<Long> detectors = new CopyOnWriteArrayList<>();
public RknnObjectDetector(String modelPath, List<String> labels) { public RknnObjectDetector(String modelPath, List<String> labels, RknnJNI.ModelVersion version) {
synchronized (lock) { synchronized (lock) {
objPointer = RknnJNI.create(modelPath, labels.size()); objPointer = RknnJNI.create(modelPath, labels.size(), version.ordinal(), -1);
detectors.add(objPointer); detectors.add(objPointer);
System.out.println( System.out.println(
"Created " + objPointer + "! Detectors: " + Arrays.toString(detectors.toArray())); "Created " + objPointer + "! Detectors: " + Arrays.toString(detectors.toArray()));

View File

@@ -53,6 +53,7 @@ public class MrCalJNILoader extends PhotonJNICommon {
"libcolamd", "libcolamd",
"libccolamd", "libccolamd",
"openblas", "openblas",
"libwinpthread-1",
"libgcc_s_seh-1", "libgcc_s_seh-1",
"libquadmath-0", "libquadmath-0",
"libgfortran-5", "libgfortran-5",

View File

@@ -24,7 +24,7 @@ import java.util.List;
import org.opencv.core.Point; import org.opencv.core.Point;
import org.opencv.core.Point3; import org.opencv.core.Point3;
public final class BoardObservation { public final class BoardObservation implements Cloneable {
// Expected feature 3d location in the camera frame // Expected feature 3d location in the camera frame
@JsonProperty("locationInObjectSpace") @JsonProperty("locationInObjectSpace")
public List<Point3> locationInObjectSpace; public List<Point3> locationInObjectSpace;
@@ -68,4 +68,33 @@ public final class BoardObservation {
this.snapshotName = snapshotName; this.snapshotName = snapshotName;
this.snapshotData = snapshotData; this.snapshotData = snapshotData;
} }
@Override
public String toString() {
return "BoardObservation [locationInObjectSpace="
+ locationInObjectSpace
+ ", locationInImageSpace="
+ locationInImageSpace
+ ", reprojectionErrors="
+ reprojectionErrors
+ ", optimisedCameraToObject="
+ optimisedCameraToObject
+ ", includeObservationInCalibration="
+ includeObservationInCalibration
+ ", snapshotName="
+ snapshotName
+ ", snapshotData="
+ snapshotData
+ "]";
}
@Override
public BoardObservation clone() {
try {
return (BoardObservation) super.clone();
} catch (CloneNotSupportedException e) {
System.err.println("Guhhh clone buh");
return null;
}
}
} }

View File

@@ -191,8 +191,8 @@ public class CameraCalibrationCoefficients implements Releasable {
+ cameraIntrinsics + cameraIntrinsics
+ ", distCoeffs=" + ", distCoeffs="
+ distCoeffs + distCoeffs
+ ", observations=" + ", observationslen="
+ observations + observations.size()
+ ", calobjectWarp=" + ", calobjectWarp="
+ Arrays.toString(calobjectWarp) + Arrays.toString(calobjectWarp)
+ ", intrinsicsArr=" + ", intrinsicsArr="
@@ -201,4 +201,16 @@ public class CameraCalibrationCoefficients implements Releasable {
+ Arrays.toString(distCoeffsArr) + Arrays.toString(distCoeffsArr)
+ "]"; + "]";
} }
public UICameraCalibrationCoefficients cloneWithoutObservations() {
return new UICameraCalibrationCoefficients(
resolution,
cameraIntrinsics,
distCoeffs,
calobjectWarp,
observations,
calobjectSize,
calobjectSpacing,
lensmodel);
}
} }

View File

@@ -76,4 +76,17 @@ public class JsonImageMat implements Releasable {
public void release() { public void release() {
if (wrappedMat != null) wrappedMat.release(); if (wrappedMat != null) wrappedMat.release();
} }
@Override
public String toString() {
return "JsonImageMat [rows="
+ rows
+ ", cols="
+ cols
+ ", type="
+ type
+ ", datalen="
+ data.length()
+ "]";
}
} }

View File

@@ -40,7 +40,7 @@ public class JsonMatOfDouble implements Releasable {
@JsonIgnore private Mat wrappedMat = null; @JsonIgnore private Mat wrappedMat = null;
@JsonIgnore private Matrix wpilibMat = null; @JsonIgnore private Matrix wpilibMat = null;
private MatOfDouble wrappedMatOfDouble; @JsonIgnore private MatOfDouble wrappedMatOfDouble;
public JsonMatOfDouble(int rows, int cols, double[] data) { public JsonMatOfDouble(int rows, int cols, double[] data) {
this(rows, cols, CvType.CV_64FC1, data); this(rows, cols, CvType.CV_64FC1, data);

View File

@@ -0,0 +1,59 @@
/*
* 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.calibration;
import java.util.List;
import java.util.stream.Collectors;
import org.opencv.core.Size;
public class UICameraCalibrationCoefficients extends CameraCalibrationCoefficients {
public int numSnapshots;
public List<Double> meanErrors;
public UICameraCalibrationCoefficients(
Size resolution,
JsonMatOfDouble cameraIntrinsics,
JsonMatOfDouble distCoeffs,
double[] calobjectWarp,
List<BoardObservation> observations,
Size calobjectSize,
double calobjectSpacing,
CameraLensModel lensmodel) {
// yeet observations, keep all else
super(
resolution,
cameraIntrinsics,
distCoeffs,
calobjectWarp,
List.of(),
calobjectSize,
calobjectSpacing,
lensmodel);
this.numSnapshots = observations.size();
this.meanErrors =
observations.stream()
.map(
it2 ->
it2.reprojectionErrors.stream()
.mapToDouble(it -> Math.sqrt(it.x * it.x + it.y * it.y))
.average()
.orElse(0))
.collect(Collectors.toList());
}
}

View File

@@ -19,6 +19,8 @@ package org.photonvision.vision.camera;
import edu.wpi.first.cscore.UsbCameraInfo; import edu.wpi.first.cscore.UsbCameraInfo;
import java.util.Arrays; import java.util.Arrays;
import java.util.Optional;
import org.photonvision.common.hardware.Platform;
public class CameraInfo extends UsbCameraInfo { public class CameraInfo extends UsbCameraInfo {
public final CameraType cameraType; public final CameraType cameraType;
@@ -68,15 +70,54 @@ public class CameraInfo extends UsbCameraInfo {
return getBaseName().replaceAll(" ", "_"); return getBaseName().replaceAll(" ", "_");
} }
/**
* Get a unique descriptor of the USB port this camera is attached to. EG
* "/dev/v4l/by-path/platform-fc800000.usb-usb-0:1.3:1.0-video-index0"
*
* @return
*/
public Optional<String> getUSBPath() {
return Arrays.stream(otherPaths).filter(path -> path.contains("/by-path/")).findFirst();
}
@Override @Override
public boolean equals(Object o) { public boolean equals(Object obj) {
if (o == this) return true; if (this == obj) return true;
if (!(o instanceof UsbCameraInfo || o instanceof CameraInfo)) return false; if (obj == null) return false;
UsbCameraInfo other = (UsbCameraInfo) o; if (getClass() != obj.getClass()) return false;
return path.equals(other.path) CameraInfo other = (CameraInfo) obj;
// && a.dev == b.dev (dev is not constant in Windows)
&& name.equals(other.name) // Windows device number is not significant. See
&& productId == other.productId // https://github.com/wpilibsuite/allwpilib/blob/4b94a64b06057c723d6fcafeb1a45f55a70d179a/cscore/src/main/native/windows/UsbCameraImpl.cpp#L1128
&& vendorId == other.vendorId; if (!Platform.isWindows()) {
if (dev != other.dev) return false;
}
if (!path.equals(other.path)) return false;
if (!name.equals(other.name)) return false;
if (!Arrays.asList(this.otherPaths).containsAll(Arrays.asList(other.otherPaths))) return false;
if (vendorId != other.vendorId) return false;
if (productId != other.productId) return false;
// Don't trust super.equals, as it compares references. Should PR this to allwpilib at some
// point
return true;
}
@Override
public String toString() {
return "CameraInfo [cameraType="
+ cameraType
+ "baseName="
+ getBaseName()
+ ", vid="
+ vendorId
+ ", pid="
+ productId
+ ", path="
+ path
+ ", otherPaths="
+ Arrays.toString(otherPaths)
+ "]";
} }
} }

View File

@@ -0,0 +1,93 @@
/*
* 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 java.util.*;
import org.photonvision.common.configuration.CameraConfiguration;
import org.photonvision.vision.frame.Frame;
import org.photonvision.vision.frame.FrameProvider;
import org.photonvision.vision.frame.FrameThresholdType;
import org.photonvision.vision.opencv.ImageRotationMode;
import org.photonvision.vision.pipe.impl.HSVPipe.HSVParams;
import org.photonvision.vision.processes.VisionSource;
import org.photonvision.vision.processes.VisionSourceSettables;
/** Dummy class for unit testing the vision source manager */
public class TestSource extends VisionSource {
private FrameProvider usbFrameProvider;
public TestSource(CameraConfiguration config) {
super(config);
if (getCameraConfiguration().cameraQuirks == null)
getCameraConfiguration().cameraQuirks =
QuirkyCamera.getQuirkyCamera(config.usbVID, config.usbVID, config.baseName);
}
@Override
public FrameProvider getFrameProvider() {
return new FrameProvider() {
@Override
public Frame get() {
// TODO Auto-generated method stub
throw new UnsupportedOperationException("Unimplemented method 'get'");
}
@Override
public String getName() {
return cameraConfiguration.uniqueName;
}
@Override
public void requestFrameThresholdType(FrameThresholdType type) {
// TODO Auto-generated method stub
throw new UnsupportedOperationException("Unimplemented method 'requestFrameThresholdType'");
}
@Override
public void requestFrameRotation(ImageRotationMode rotationMode) {
// TODO Auto-generated method stub
throw new UnsupportedOperationException("Unimplemented method 'requestFrameRotation'");
}
@Override
public void requestFrameCopies(boolean copyInput, boolean copyOutput) {
// TODO Auto-generated method stub
throw new UnsupportedOperationException("Unimplemented method 'requestFrameCopies'");
}
@Override
public void requestHsvSettings(HSVParams params) {
// TODO Auto-generated method stub
throw new UnsupportedOperationException("Unimplemented method 'requestHsvSettings'");
}
};
}
@Override
public VisionSourceSettables getSettables() {
// TODO Auto-generated method stub
throw new UnsupportedOperationException("Unimplemented method 'getSettables'");
}
@Override
public boolean isVendorCamera() {
// TODO Auto-generated method stub
throw new UnsupportedOperationException("Unimplemented method 'isVendorCamera'");
}
}

View File

@@ -49,9 +49,17 @@ public class USBCameraSource extends VisionSource {
super(config); super(config);
logger = new Logger(USBCameraSource.class, config.nickname, LogGroup.Camera); logger = new Logger(USBCameraSource.class, config.nickname, LogGroup.Camera);
camera = new UsbCamera(config.nickname, config.path); // 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); 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) if (getCameraConfiguration().cameraQuirks == null)
getCameraConfiguration().cameraQuirks = getCameraConfiguration().cameraQuirks =
QuirkyCamera.getQuirkyCamera( QuirkyCamera.getQuirkyCamera(
@@ -395,6 +403,7 @@ public class USBCameraSource extends VisionSource {
// Sort by resolution // Sort by resolution
var sortedList = var sortedList =
videoModesList.stream() videoModesList.stream()
.distinct() // remove redundant video mode entries
.sorted(((a, b) -> (b.width + b.height) - (a.width + a.height))) .sorted(((a, b) -> (b.width + b.height) - (a.width + a.height)))
.collect(Collectors.toList()); .collect(Collectors.toList());
Collections.reverse(sortedList); Collections.reverse(sortedList);

View File

@@ -35,7 +35,8 @@ public class RknnDetectionPipe
this.detector = this.detector =
new RknnObjectDetector( new RknnObjectDetector(
NeuralNetworkModelManager.getInstance().getDefaultRknnModel().getAbsolutePath(), NeuralNetworkModelManager.getInstance().getDefaultRknnModel().getAbsolutePath(),
NeuralNetworkModelManager.getInstance().getLabels()); NeuralNetworkModelManager.getInstance().getLabels(),
NeuralNetworkModelManager.getInstance().getModelVersion());
} }
@Override @Override

View File

@@ -26,6 +26,7 @@ import java.util.HashMap;
import java.util.LinkedList; import java.util.LinkedList;
import java.util.List; import java.util.List;
import java.util.function.BiConsumer; import java.util.function.BiConsumer;
import java.util.stream.Collectors;
import org.opencv.core.Size; import org.opencv.core.Size;
import org.photonvision.common.configuration.CameraConfiguration; import org.photonvision.common.configuration.CameraConfiguration;
import org.photonvision.common.configuration.ConfigManager; import org.photonvision.common.configuration.ConfigManager;
@@ -536,7 +537,10 @@ public class VisionModule {
ret.outputStreamPort = this.outputStreamPort; ret.outputStreamPort = this.outputStreamPort;
ret.inputStreamPort = this.inputStreamPort; ret.inputStreamPort = this.inputStreamPort;
ret.calibrations = visionSource.getSettables().getConfiguration().calibrations; ret.calibrations =
visionSource.getSettables().getConfiguration().calibrations.stream()
.map(CameraCalibrationCoefficients::cloneWithoutObservations)
.collect(Collectors.toList());
ret.isFovConfigurable = ret.isFovConfigurable =
!(ConfigManager.getInstance().getConfig().getHardwareConfig().hasPresetFOV() !(ConfigManager.getInstance().getConfig().getHardwareConfig().hasPresetFOV()

View File

@@ -23,6 +23,7 @@ import java.util.Arrays;
import java.util.Collection; import java.util.Collection;
import java.util.List; import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.CopyOnWriteArrayList;
import java.util.function.Predicate;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import org.photonvision.common.configuration.CameraConfiguration; import org.photonvision.common.configuration.CameraConfiguration;
import org.photonvision.common.configuration.ConfigManager; import org.photonvision.common.configuration.ConfigManager;
@@ -38,6 +39,7 @@ import org.photonvision.vision.camera.CameraInfo;
import org.photonvision.vision.camera.CameraQuirk; import org.photonvision.vision.camera.CameraQuirk;
import org.photonvision.vision.camera.CameraType; import org.photonvision.vision.camera.CameraType;
import org.photonvision.vision.camera.LibcameraGpuSource; import org.photonvision.vision.camera.LibcameraGpuSource;
import org.photonvision.vision.camera.TestSource;
import org.photonvision.vision.camera.USBCameraSource; import org.photonvision.vision.camera.USBCameraSource;
public class VisionSourceManager { public class VisionSourceManager {
@@ -145,8 +147,8 @@ public class VisionSourceManager {
} }
// Return no new sources because there are no new sources // Return no new sources because there are no new sources
if (connectedCameras.isEmpty() && !cameraInfos.isEmpty()) { if (connectedCameras.isEmpty()) {
if (hasWarnedNoCameras) { if (!hasWarnedNoCameras) {
logger.warn( logger.warn(
"No cameras were detected! Check that all cameras are connected, and that the path is correct."); "No cameras were detected! Check that all cameras are connected, and that the path is correct.");
hasWarnedNoCameras = true; hasWarnedNoCameras = true;
@@ -164,7 +166,7 @@ public class VisionSourceManager {
// Debug prints // Debug prints
for (var info : connectedCameras) { for (var info : connectedCameras) {
logger.info("Adding local video device - \"" + info.name + "\" at \"" + info.path + "\""); logger.info("Detected unmatched physical camera: " + info.toString());
} }
if (!unmatchedLoadedConfigs.isEmpty()) if (!unmatchedLoadedConfigs.isEmpty())
@@ -185,7 +187,7 @@ public class VisionSourceManager {
"Unloaded configs: " "Unloaded configs: "
+ unmatchedLoadedConfigs.stream() + unmatchedLoadedConfigs.stream()
.map(it -> it.nickname) .map(it -> it.nickname)
.collect(Collectors.joining())); .collect(Collectors.joining(", ")));
hasWarned = true; hasWarned = true;
} }
@@ -194,13 +196,8 @@ public class VisionSourceManager {
if (matchedCameras.isEmpty()) return null; if (matchedCameras.isEmpty()) return null;
// for unit tests only!
if (!createSources) {
return List.of();
}
// Turn these camera configs into vision sources // Turn these camera configs into vision sources
var sources = loadVisionSourcesFromCamConfigs(matchedCameras); var sources = loadVisionSourcesFromCamConfigs(matchedCameras, createSources);
// Print info about each vision source // Print info about each vision source
for (var src : sources) { for (var src : sources) {
@@ -216,6 +213,52 @@ public class VisionSourceManager {
return sources; return sources;
} }
/**
* Get a predicate for checking cameras against a saved config.
*
* @param savedConfig The saved camera configuration to match against
* @param checkUSBPath If we should compare the USB port/bus IDs
* @param checkVidPid If we should compare USB VID and PID
* @param checkBaseName If we should compare {@link CameraInfo#getBaseName}
* @param checkPath If we should check {@link CameraInfo::path} (eg /dev/videoN on Linux, or
* ?/usb#vid_05c8&pid_03df&mi_00#7&fa76035&0&0000#{e5323777-f976-4f5b-9b55-b94699c46e44}\global
* on Windows)
*/
private final Predicate<CameraInfo> getCameraMatcher(
final CameraConfiguration savedConfig,
boolean checkUSBPath,
boolean checkVidPid,
boolean checkBaseName,
boolean checkPath) {
if (checkUSBPath && savedConfig.getUSBPath().isEmpty()) {
logger.debug(
"WARN: Camera has empty USB path, but asked to match by name: "
+ camCfgToString(savedConfig));
}
return (CameraInfo physicalCamera) -> {
var matches = true;
if (checkUSBPath) {
var savedPath = savedConfig.getUSBPath();
matches &= (savedPath.isPresent() && physicalCamera.getUSBPath().equals(savedPath));
}
if (checkBaseName) {
matches &= physicalCamera.getBaseName().equals(savedConfig.baseName);
}
if (checkVidPid) {
matches &=
(physicalCamera.vendorId == savedConfig.usbVID
&& physicalCamera.productId == savedConfig.usbPID);
}
if (checkPath) {
matches &= (physicalCamera.path.equals(savedConfig.path));
}
return matches;
};
}
/** /**
* Create {@link CameraConfiguration}s based on a list of detected USB cameras and the configs on * Create {@link CameraConfiguration}s based on a list of detected USB cameras and the configs on
* disk. * disk.
@@ -226,35 +269,133 @@ public class VisionSourceManager {
*/ */
public List<CameraConfiguration> matchCameras( public List<CameraConfiguration> matchCameras(
List<CameraInfo> detectedCamInfos, List<CameraConfiguration> loadedCamConfigs) { List<CameraInfo> detectedCamInfos, List<CameraConfiguration> loadedCamConfigs) {
return matchCameras(
detectedCamInfos,
loadedCamConfigs,
ConfigManager.getInstance().getConfig().getNetworkConfig().matchCamerasOnlyByPath);
}
private static final String camCfgToString(CameraConfiguration c) {
return new StringBuilder()
.append("[baseName=")
.append(c.baseName)
.append(", uniqueName=")
.append(c.uniqueName)
.append(", otherPaths=")
.append(Arrays.toString(c.otherPaths))
.append(", vid=")
.append(c.usbVID)
.append(", pid=")
.append(c.usbPID)
.append("]")
.toString();
}
/**
* Create {@link CameraConfiguration}s based on a list of detected USB cameras and the configs on
* disk.
*
* @param detectedCamInfos Information about currently connected USB cameras.
* @param loadedCamConfigs The USB {@link CameraConfiguration}s loaded from disk.
* @param matchCamerasOnlyByPath If we should never try to match only by (base name, vid, pid)
* @return the matched configurations.
*/
public List<CameraConfiguration> matchCameras(
List<CameraInfo> detectedCamInfos,
List<CameraConfiguration> loadedCamConfigs,
boolean matchCamerasOnlyByPath) {
var detectedCameraList = new ArrayList<>(detectedCamInfos); var detectedCameraList = new ArrayList<>(detectedCamInfos);
ArrayList<CameraConfiguration> cameraConfigurations = new ArrayList<CameraConfiguration>(); ArrayList<CameraConfiguration> cameraConfigurations = new ArrayList<CameraConfiguration>();
ArrayList<CameraConfiguration> unloadedConfigs = ArrayList<CameraConfiguration> unloadedConfigs =
new ArrayList<CameraConfiguration>(loadedCamConfigs); new ArrayList<CameraConfiguration>(loadedCamConfigs);
if (detectedCameraList.size() > 0 || unloadedConfigs.size() > 0) if (detectedCameraList.size() > 0 || unloadedConfigs.size() > 0) {
cameraConfigurations.addAll(matchByPathByID(detectedCameraList, unloadedConfigs)); logger.info("Matching by usb port & name & USB VID/PID...");
else logger.debug("Skipping matchByPath no configs or cameras left to match");
if (detectedCameraList.size() > 0 || unloadedConfigs.size() > 0)
cameraConfigurations.addAll(matchByPath(detectedCameraList, unloadedConfigs));
else logger.debug("Skipping matchByPath no configs or cameras left to match");
if (detectedCameraList.size() > 0 || unloadedConfigs.size() > 0)
cameraConfigurations.addAll(matchByName(detectedCameraList, unloadedConfigs));
else logger.debug("Skipping matchByName no configs or cameras left to match");
if (detectedCameraList.size() > 0)
cameraConfigurations.addAll( cameraConfigurations.addAll(
createConfigsForCameras(detectedCameraList, unloadedConfigs, cameraConfigurations)); matchCamerasByStrategy(detectedCameraList, unloadedConfigs, true, true, true, false));
}
// On windows, the v4l path is actually useful and tells us the port the camera is physically
// connected to which is neat
if (Platform.isWindows() && !matchCamerasOnlyByPath) {
if (detectedCameraList.size() > 0 || unloadedConfigs.size() > 0) {
logger.info("Matching by windows-path & USB VID/PID only...");
cameraConfigurations.addAll(
matchCamerasByStrategy(detectedCameraList, unloadedConfigs, false, true, true, true));
}
}
if (detectedCameraList.size() > 0 || unloadedConfigs.size() > 0) {
logger.info("Matching by usb port & USB VID/PID...");
cameraConfigurations.addAll(
matchCamerasByStrategy(detectedCameraList, unloadedConfigs, true, true, false, false));
}
// Legacy migration -- VID/PID will be unset, so we have to try with our most relaxed strategy
// at least once. We _should_ still have a valid USB path (assuming cameras have not moved), so
// try that first, then fallback to base name only beloow
if (detectedCameraList.size() > 0 || unloadedConfigs.size() > 0) {
logger.info("Matching by base-name & usb port...");
cameraConfigurations.addAll(
matchCamerasByStrategy(detectedCameraList, unloadedConfigs, true, false, true, false));
}
// handle disabling only-by-base-name matching
if (!matchCamerasOnlyByPath) {
if (detectedCameraList.size() > 0 || unloadedConfigs.size() > 0) {
logger.info("Matching by base-name & USB VID/PID only...");
cameraConfigurations.addAll(
matchCamerasByStrategy(detectedCameraList, unloadedConfigs, false, true, true, false));
}
// Legacy migration for if no USB VID/PID set
if (detectedCameraList.size() > 0 || unloadedConfigs.size() > 0) {
logger.info("Matching by base-name only...");
cameraConfigurations.addAll(
matchCamerasByStrategy(detectedCameraList, unloadedConfigs, false, false, true, false));
}
} else logger.info("Skipping match by filepath/vid/pid, disabled by user");
if (detectedCameraList.size() > 0) {
// handle disabling only-by-base-name matching
if (!matchCamerasOnlyByPath) {
cameraConfigurations.addAll(
createConfigsForCameras(detectedCameraList, unloadedConfigs, cameraConfigurations));
} else {
logger.warn(
"Not creating 'new' Photon CameraConfigurations for ["
+ detectedCamInfos.stream()
.map(CameraInfo::toString)
.collect(Collectors.joining(";"))
+ "], disabled by user");
}
}
logger.debug("Matched or created " + cameraConfigurations.size() + " camera configs!"); logger.debug("Matched or created " + cameraConfigurations.size() + " camera configs!");
return cameraConfigurations; return cameraConfigurations;
} }
// loop over all the configs loaded from disk, attempting to match each camera /**
// to a config by path-by-id on linux * Abstractly match cameras
private List<CameraConfiguration> matchByPathByID( *
List<CameraInfo> detectedCamInfos, List<CameraConfiguration> unloadedConfigs) { * @param detectedCamInfos Physical cameras unmatched and attached to the device
* @param unloadedConfigs {@link CameraConfiguration}
* @param checkUSBPath If we should compare the USB port/bus IDs
* @param checkVidPid If we should compare USB VID and PID
* @param checkBaseName If we should check {@link CameraInfo::getBaseName}
* @param checkPath If we should check {@link CameraInfo::path} (eg /dev/videoN on Linux, or
* usb#vid_05c8&pid_03df&mi_00#7&fa76035&0&0000#{e5323777-f976-4f5b-9b55-b94699c46e44}\global
* on Windows). Note that path may change based on order cameras are plugged in/unplugged on
* Linux, and should not be trusted to remain the same.
* @return All matched or created new configs
*/
private List<CameraConfiguration> matchCamerasByStrategy(
List<CameraInfo> detectedCamInfos,
List<CameraConfiguration> unloadedConfigs,
boolean checkUSBPath,
boolean checkVidPid,
boolean checkBaseName,
boolean checkPath) {
List<CameraConfiguration> ret = new ArrayList<CameraConfiguration>(); List<CameraConfiguration> ret = new ArrayList<CameraConfiguration>();
List<CameraConfiguration> unloadedConfigsCopy = List<CameraConfiguration> unloadedConfigsCopy =
new ArrayList<CameraConfiguration>(unloadedConfigs); new ArrayList<CameraConfiguration>(unloadedConfigs);
@@ -262,111 +403,43 @@ public class VisionSourceManager {
for (CameraConfiguration config : unloadedConfigsCopy) { for (CameraConfiguration config : unloadedConfigsCopy) {
// Only run match path by id if the camera is not a CSI camera. // Only run match path by id if the camera is not a CSI camera.
if (config.cameraType != CameraType.ZeroCopyPicam) { if (config.cameraType != CameraType.ZeroCopyPicam) {
CameraInfo cameraInfo; logger.debug(
if (config.otherPaths.length == 0) { String.format(
logger.debug("No valid path-by-id found for config with name " + config.baseName); "Trying to find a match for loaded camera %s by strategy (path %s vid/pid %s basename %s path %s) with camera config: %s",
} else { config.baseName,
// attempt matching by path and basename checkUSBPath,
logger.debug( checkVidPid,
"Trying to find a match for loaded camera " checkBaseName,
+ config.baseName checkPath,
+ " with path-by-id " camCfgToString(config)));
+ config.otherPaths[0]);
cameraInfo =
detectedCamInfos.stream()
.filter(
usbCameraInfo ->
usbCameraInfo.otherPaths.length != 0
&& usbCameraInfo.otherPaths[0].equals(config.otherPaths[0])
&& usbCameraInfo.getBaseName().equals(config.baseName))
.findFirst()
.orElse(null);
// If we actually matched a camera to a config, remove that camera from the list // Get matcher and filter against it, picking out the first match
// and add it to the output Predicate<CameraInfo> matches =
if (cameraInfo != null) { getCameraMatcher(config, checkUSBPath, checkVidPid, checkBaseName, checkPath);
logger.debug("Matched the config for " + config.baseName + " to a physical camera!"); var cameraInfo = detectedCamInfos.stream().filter(matches).findFirst().orElse(null);
ret.add(mergeInfoIntoConfig(config, cameraInfo));
detectedCamInfos.remove(cameraInfo); // If we actually matched a camera to a config, remove that camera from the list
unloadedConfigs.remove(config); // and add it to the output
} if (cameraInfo != null) {
logger.debug("Matched the config for " + config.baseName + " to a physical camera!");
ret.add(mergeInfoIntoConfig(config, cameraInfo));
detectedCamInfos.remove(cameraInfo);
unloadedConfigs.remove(config);
} else {
logger.debug("No camera found for the config " + config.baseName);
} }
} }
} }
return ret; return ret;
} }
private List<CameraConfiguration> matchByPath( /**
List<CameraInfo> detectedCamInfos, List<CameraConfiguration> unloadedConfigs) { * Create new {@link CameraConfiguration}s for unmatched cameras, and assign them a unique name
List<CameraConfiguration> ret = new ArrayList<CameraConfiguration>(); * (unique in the set of (loaded configs, unloaded configs, loaded vision modules) at least)
List<CameraConfiguration> unloadedConfigsCopy = */
new ArrayList<CameraConfiguration>(unloadedConfigs);
// now attempt to match the cameras and configs remaining by normal path
for (CameraConfiguration config : unloadedConfigsCopy) {
CameraInfo cameraInfo;
// attempt matching by path and basename
logger.debug(
"Trying to find a match for loaded camera "
+ config.baseName
+ " with path "
+ config.path);
cameraInfo =
detectedCamInfos.stream()
.filter(
usbCameraInfo ->
usbCameraInfo.path.equals(config.path)
&& usbCameraInfo.getBaseName().equals(config.baseName))
.findFirst()
.orElse(null);
// If we actually matched a camera to a config, remove that camera from the list
// and add it to the output
if (cameraInfo != null) {
logger.debug("Matched the config for " + config.baseName + " to a physical camera!");
ret.add(mergeInfoIntoConfig(config, cameraInfo));
detectedCamInfos.remove(cameraInfo);
unloadedConfigs.remove(config);
}
}
return ret;
}
// Try matching cameras to configs by name.
private List<CameraConfiguration> matchByName(
List<CameraInfo> detectedCamInfos, List<CameraConfiguration> unloadedConfigs) {
List<CameraConfiguration> ret = new ArrayList<CameraConfiguration>();
List<CameraConfiguration> unloadedConfigsCopy =
new ArrayList<CameraConfiguration>(unloadedConfigs);
// if both path and ID based matching fails, attempt basename only match
for (CameraConfiguration config : unloadedConfigsCopy) {
CameraInfo cameraInfo;
logger.debug("Trying to find a match for loaded camera with name " + config.baseName);
cameraInfo =
detectedCamInfos.stream()
.filter(CameraInfo -> CameraInfo.getBaseName().equals(config.baseName))
.findFirst()
.orElse(null);
// If we actually matched a camera to a config, remove that camera from the list
// and add it to the output
if (cameraInfo != null) {
logger.debug("Matched the config for " + config.baseName + " to a physical camera!");
ret.add(mergeInfoIntoConfig(config, cameraInfo));
detectedCamInfos.remove(cameraInfo);
unloadedConfigs.remove(config);
}
}
return ret;
}
// If we have any unmatched cameras left, create a new CameraConfiguration for
// them here.
private List<CameraConfiguration> createConfigsForCameras( private List<CameraConfiguration> createConfigsForCameras(
List<CameraInfo> detectedCameraList, List<CameraInfo> detectedCameraList,
List<CameraConfiguration> loadedCamConfigs, List<CameraConfiguration> unloadedCamConfigs,
List<CameraConfiguration> loadedConfigs) { List<CameraConfiguration> loadedConfigs) {
List<CameraConfiguration> ret = new ArrayList<CameraConfiguration>(); List<CameraConfiguration> ret = new ArrayList<CameraConfiguration>();
logger.debug( logger.debug(
@@ -377,7 +450,10 @@ public class VisionSourceManager {
String uniqueName = info.getHumanReadableName(); String uniqueName = info.getHumanReadableName();
int suffix = 0; int suffix = 0;
while (containsName(loadedConfigs, uniqueName) || containsName(uniqueName)) { while (containsName(loadedConfigs, uniqueName)
|| containsName(uniqueName)
|| containsName(unloadedCamConfigs, uniqueName)
|| containsName(ret, uniqueName)) {
suffix++; suffix++;
uniqueName = String.format("%s (%d)", uniqueName, suffix); uniqueName = String.format("%s (%d)", uniqueName, suffix);
} }
@@ -457,10 +533,16 @@ public class VisionSourceManager {
} }
private static List<VisionSource> loadVisionSourcesFromCamConfigs( private static List<VisionSource> loadVisionSourcesFromCamConfigs(
List<CameraConfiguration> camConfigs) { List<CameraConfiguration> camConfigs, boolean createSources) {
var cameraSources = new ArrayList<VisionSource>(); var cameraSources = new ArrayList<VisionSource>();
for (var configuration : camConfigs) { for (var configuration : camConfigs) {
logger.debug("Creating VisionSource for " + configuration); logger.debug("Creating VisionSource for " + camCfgToString(configuration));
// In unit tests, create dummy
if (!createSources) {
cameraSources.add(new TestSource(configuration));
continue;
}
boolean is_pi = Platform.isRaspberryPi(); boolean is_pi = Platform.isRaspberryPi();

View File

@@ -139,8 +139,31 @@ public class ConfigTest {
writer.write(str); writer.write(str);
writer.flush(); writer.flush();
writer.close(); writer.close();
Assertions.assertDoesNotThrow( CameraConfiguration result =
() -> JacksonUtils.deserialize(tempFile.toPath(), CameraConfiguration.class)); JacksonUtils.deserialize(tempFile.toPath(), CameraConfiguration.class);
tempFile.delete();
}
@Test
public void testJacksonAddUSBVIDPID() throws IOException {
var str =
"{\"baseName\":\"aaaaaa\",\"uniqueName\":\"aaaaaa\",\"nickname\":\"aaaaaa\",\"FOV\":70.0,\"path\":\"dev/vid\",\"cameraType\":\"UsbCamera\",\"currentPipelineIndex\":0,\"camPitch\":{\"radians\":0.0},\"calibrations\":[], \"usbVID\":3, \"usbPID\":4, \"cameraLEDs\":[]}";
File tempFile = File.createTempFile("test", ".json");
tempFile.deleteOnExit();
var writer = new FileWriter(tempFile);
writer.write(str);
writer.flush();
writer.close();
try {
CameraConfiguration result =
JacksonUtils.deserialize(tempFile.toPath(), CameraConfiguration.class);
String ser = JacksonUtils.serializeToString(result);
System.out.println(ser);
} catch (Exception e) {
e.printStackTrace();
}
tempFile.delete(); tempFile.delete();
} }

View File

@@ -84,7 +84,9 @@ public class SQLConfigTest {
CameraType.UsbCamera, CameraType.UsbCamera,
QuirkyCamera.getQuirkyCamera(-1, -1), QuirkyCamera.getQuirkyCamera(-1, -1),
List.of(), List.of(),
0); 0,
-1,
-1);
testcamcfg.pipelineSettings = testcamcfg.pipelineSettings =
List.of( List.of(
new ReflectivePipelineSettings(), new ReflectivePipelineSettings(),

View File

@@ -21,17 +21,24 @@ import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.assertTrue;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.photonvision.common.configuration.CameraConfiguration; import org.photonvision.common.configuration.CameraConfiguration;
import org.photonvision.common.configuration.ConfigManager; import org.photonvision.common.configuration.ConfigManager;
import org.photonvision.common.logging.LogGroup;
import org.photonvision.common.logging.LogLevel;
import org.photonvision.common.logging.Logger;
import org.photonvision.vision.camera.CameraInfo; import org.photonvision.vision.camera.CameraInfo;
import org.photonvision.vision.camera.CameraType; import org.photonvision.vision.camera.CameraType;
public class VisionSourceManagerTest { public class VisionSourceManagerTest {
@Test @Test
public void visionSourceTest() { public void visionSourceTest() {
Logger.setLevel(LogGroup.Camera, LogLevel.DEBUG);
var inst = new VisionSourceManager(); var inst = new VisionSourceManager();
var cameraInfos = new ArrayList<CameraInfo>(); var cameraInfos = new ArrayList<CameraInfo>();
ConfigManager.getInstance().clearConfig();
ConfigManager.getInstance().load(); ConfigManager.getInstance().load();
inst.tryMatchCamImpl(cameraInfos); inst.tryMatchCamImpl(cameraInfos);
@@ -43,6 +50,8 @@ public class VisionSourceManagerTest {
"thirdTestVideo", "thirdTestVideo",
"dev/video1", "dev/video1",
new String[] {"by-id/123"}); new String[] {"by-id/123"});
config3.usbVID = 3;
config3.usbPID = 4;
var config4 = var config4 =
new CameraConfiguration( new CameraConfiguration(
"fourthTestVideo", "fourthTestVideo",
@@ -50,6 +59,8 @@ public class VisionSourceManagerTest {
"fourthTestVideo", "fourthTestVideo",
"dev/video2", "dev/video2",
new String[] {"by-id/321"}); new String[] {"by-id/321"});
config4.usbVID = 5;
config4.usbPID = 6;
CameraInfo info1 = new CameraInfo(0, "dev/video0", "testVideo", new String[0], 1, 2); CameraInfo info1 = new CameraInfo(0, "dev/video0", "testVideo", new String[0], 1, 2);
@@ -261,4 +272,268 @@ public class VisionSourceManagerTest {
assertEquals(10, inst.knownCameras.size()); assertEquals(10, inst.knownCameras.size());
assertEquals(0, inst.unmatchedLoadedConfigs.size()); assertEquals(0, inst.unmatchedLoadedConfigs.size());
} }
@Test
public void testDisableInhibitPathChangeIdenticalCams() {
Logger.setLevel(LogGroup.Camera, LogLevel.DEBUG);
var inst = new VisionSourceManager();
ConfigManager.getInstance().clearConfig();
ConfigManager.getInstance().load();
ConfigManager.getInstance().getConfig().getNetworkConfig().matchCamerasOnlyByPath = false;
var CAM2_OLD_PATH =
new String[] {"/dev/v4l/by-path/platform-fc880000.usb-usb-0:1:1.0-video-index0"};
var CAM2_NEW_PATH =
new String[] {"/dev/v4l/by-path/platform-fc880080.usb-usb-0:1:1.3-video-index0"};
var CAM1_OLD_PATHS =
new String[] {
"/dev/v4l/by-id/usb-Arducam_Technology_Co.__Ltd._Arducam_OV2311_USB_Camera_UC621-video-index0",
"/dev/v4l/by-path/platform-fc800000.usb-usb-0:1:1.0-video-index0"
};
var camera1_saved_config =
new CameraConfiguration(
"Arducam OV2311 USB Camera",
"Arducam OV2311 USB Camera",
"fromt-left",
"/dev/video0",
CAM1_OLD_PATHS);
camera1_saved_config.usbVID = 3141;
camera1_saved_config.usbPID = 25446;
var camera2_saved_config =
new CameraConfiguration(
"Arducam OV2311 USB Camera",
"Arducam OV2311 USB Camera (1)",
"fromt-left",
"/dev/video2",
CAM2_OLD_PATH);
camera2_saved_config.usbVID = 3141;
camera2_saved_config.usbPID = 25446;
// And load our "old" configs
inst.registerLoadedConfigs(camera1_saved_config, camera2_saved_config);
// Camera attached to new port, but strict matching disabled
{
CameraInfo info1 =
new CameraInfo(
0, "/dev/video11", "Arducam OV2311 USB Camera", CAM1_OLD_PATHS, 3141, 25446);
CameraInfo info2 =
new CameraInfo(
0, "/dev/video12", "Arducam OV2311 USB Camera", CAM2_NEW_PATH, 3141, 25446);
var cameraInfos = new ArrayList<CameraInfo>();
cameraInfos.add(info1);
cameraInfos.add(info2);
List<VisionSource> ret1 = inst.tryMatchCamImpl(cameraInfos);
// and check the new one got matched got matched
assertEquals(2, ret1.size());
assertEquals(
1, ret1.stream().filter(it -> it.cameraConfiguration.path.equals(info1.path)).count());
assertEquals(
1, ret1.stream().filter(it -> it.cameraConfiguration.path.equals(info2.path)).count());
}
}
@Test
public void testInhibitPathChangeIdenticalCams() {
Logger.setLevel(LogGroup.Camera, LogLevel.DEBUG);
var inst = new VisionSourceManager();
ConfigManager.getInstance().clearConfig();
ConfigManager.getInstance().load();
ConfigManager.getInstance().getConfig().getNetworkConfig().matchCamerasOnlyByPath = true;
var CAM2_OLD_PATH =
new String[] {"/dev/v4l/by-path/platform-fc880000.usb-usb-0:1:1.0-video-index0"};
var CAM2_NEW_PATH =
new String[] {"/dev/v4l/by-path/platform-fc880080.usb-usb-0:1:1.3-video-index0"};
var CAM1_OLD_PATHS =
new String[] {
"/dev/v4l/by-id/usb-Arducam_Technology_Co.__Ltd._Arducam_OV2311_USB_Camera_UC621-video-index0",
"/dev/v4l/by-path/platform-fc800000.usb-usb-0:1:1.0-video-index0"
};
var camera1_saved_config =
new CameraConfiguration(
"Arducam OV2311 USB Camera",
"Arducam OV2311 USB Camera (1)",
"fromt-left",
"/dev/video0",
CAM1_OLD_PATHS);
camera1_saved_config.usbVID = 3141;
camera1_saved_config.usbPID = 25446;
var camera2_saved_config =
new CameraConfiguration(
"Arducam OV2311 USB Camera",
"Arducam OV2311 USB Camera (1)",
"fromt-left",
"/dev/video2",
CAM2_OLD_PATH);
camera2_saved_config.usbVID = 3141;
camera2_saved_config.usbPID = 25446;
// And load our "old" configs
inst.registerLoadedConfigs(camera1_saved_config, camera2_saved_config);
// initial pass with camera in the wrong spot
{
// Give our cameras new "paths" to fake the windows logic out. this should not
// affect strict matching
CameraInfo info1 =
new CameraInfo(
0, "/dev/video11", "Arducam OV2311 USB Camera", CAM1_OLD_PATHS, 3141, 25446);
CameraInfo info2 =
new CameraInfo(
0, "/dev/video12", "Arducam OV2311 USB Camera", CAM2_NEW_PATH, 3141, 25446);
var cameraInfos = new ArrayList<CameraInfo>();
cameraInfos.add(info1);
cameraInfos.add(info2);
List<VisionSource> ret1 = inst.tryMatchCamImpl(cameraInfos);
// Our cameras should be "known"
assertTrue(inst.knownCameras.contains(info1));
assertTrue(inst.knownCameras.contains(info2));
assertEquals(2, inst.knownCameras.size());
// And we should have matched one camera
assertEquals(1, ret1.size());
// and only matched camera1, not 2
assertEquals(
1, ret1.stream().filter(it -> it.cameraConfiguration.path.equals(info1.path)).count());
assertEquals(
0, ret1.stream().filter(it -> it.cameraConfiguration.path.equals(info2.path)).count());
}
// Now move our camera back
{
CameraInfo info1 =
new CameraInfo(
0, "/dev/video11", "Arducam OV2311 USB Camera", CAM1_OLD_PATHS, 3141, 25446);
CameraInfo info2 =
new CameraInfo(
0, "/dev/video12", "Arducam OV2311 USB Camera", CAM2_OLD_PATH, 3141, 25446);
var cameraInfos = new ArrayList<CameraInfo>();
cameraInfos.add(info1);
cameraInfos.add(info2);
List<VisionSource> ret1 = inst.tryMatchCamImpl(cameraInfos);
// and check the new one got matched got matched
assertEquals(1, ret1.size());
assertEquals(
0, ret1.stream().filter(it -> it.cameraConfiguration.path.equals(info1.path)).count());
assertEquals(
1, ret1.stream().filter(it -> it.cameraConfiguration.path.equals(info2.path)).count());
}
}
@Test
public void testIdenticalCameras() {
Logger.setLevel(LogGroup.Camera, LogLevel.DEBUG);
// List of known cameras
var cameraInfos = new ArrayList<CameraInfo>();
var inst = new VisionSourceManager();
ConfigManager.getInstance().clearConfig();
ConfigManager.getInstance().load();
ConfigManager.getInstance().getConfig().getNetworkConfig().matchCamerasOnlyByPath = false;
// Match empty camera infos
inst.tryMatchCamImpl(cameraInfos);
CameraInfo info1 =
new CameraInfo(
0,
"/dev/video0",
"Arducam OV2311 USB Camera",
new String[] {
"/dev/v4l/by-id/usb-Arducam_Technology_Co.__Ltd._Arducam_OV2311_USB_Camera_UC621-video-index0",
"/dev/v4l/by-path/platform-fc800000.usb-usb-0:1:1.0-video-index0"
},
3141,
25446);
CameraInfo info2 =
new CameraInfo(
0,
"/dev/video2",
"Arducam OV2311 USB Camera",
new String[] {
"/dev/v4l/by-id/usb-Arducam_Technology_Co.__Ltd._Arducam_OV2311_USB_Camera_UC621-video-index0",
"/dev/v4l/by-path/platform-fc880000.usb-usb-0:1:1.0-video-index0"
},
3141,
25446);
cameraInfos.add(info1);
cameraInfos.add(info2);
// Match two "new" cameras
var ret1 = inst.tryMatchCamImpl(cameraInfos);
// Our cameras should be "known"
assertTrue(inst.knownCameras.contains(info1));
assertTrue(inst.knownCameras.contains(info2));
assertEquals(2, inst.knownCameras.size());
assertEquals(2, ret1.size());
// Exactly one camera should have the path we put in
for (int i = 0; i < cameraInfos.size(); i++) {
var testPath = cameraInfos.get(i).getUSBPath().get();
assertEquals(
1,
ret1.stream()
.filter(it -> testPath.equals(it.cameraConfiguration.getUSBPath().get()))
.count());
}
// and the names should be unique
for (int i = 0; i < ret1.size(); i++) {
var thisName = ret1.get(i).cameraConfiguration.uniqueName;
assertEquals(
1,
ret1.stream().filter(it -> thisName.equals(it.cameraConfiguration.uniqueName)).count());
}
// duplciate cameras, same info, new ref
var duplicateCameraInfos = new ArrayList<CameraInfo>();
CameraInfo info1_dup =
new CameraInfo(
0,
"/dev/video0",
"Arducam OV2311 USB Camera",
new String[] {
"/dev/v4l/by-id/usb-Arducam_Technology_Co.__Ltd._Arducam_OV2311_USB_Camera_UC621-video-index0",
"/dev/v4l/by-path/platform-fc800000.usb-usb-0:1:1.0-video-index0"
},
3141,
25446);
CameraInfo info2_dup =
new CameraInfo(
0,
"/dev/video2",
"Arducam OV2311 USB Camera",
new String[] {
"/dev/v4l/by-id/usb-Arducam_Technology_Co.__Ltd._Arducam_OV2311_USB_Camera_UC621-video-index0",
"/dev/v4l/by-path/platform-fc880000.usb-usb-0:1:1.0-video-index0"
},
3141,
25446);
duplicateCameraInfos.add(info1_dup);
duplicateCameraInfos.add(info2_dup);
inst.tryMatchCamImpl(duplicateCameraInfos);
// Our cameras should be "known", and we should only "know" two cameras still
assertTrue(inst.knownCameras.contains(info1_dup));
assertTrue(inst.knownCameras.contains(info2_dup));
assertEquals(2, inst.knownCameras.size());
}
} }

View File

@@ -19,7 +19,7 @@ class EstimatedRobotPose:
timestampSeconds: float timestampSeconds: float
"""The estimated time the frame used to derive the robot pose was taken""" """The estimated time the frame used to derive the robot pose was taken"""
targetsUsed: [PhotonTrackedTarget] targetsUsed: list[PhotonTrackedTarget]
"""A list of the targets used to compute this pose""" """A list of the targets used to compute this pose"""
strategy: "PoseStrategy" strategy: "PoseStrategy"

View File

@@ -4,7 +4,7 @@ import wpilib
class Packet: class Packet:
def __init__(self, data: list[int]): def __init__(self, data: bytes):
""" """
* Constructs an empty packet. * Constructs an empty packet.
* *
@@ -30,7 +30,7 @@ class Packet:
matches the version of photonlib running in the robot code. matches the version of photonlib running in the robot code.
""" """
def _getNextByte(self) -> int: def _getNextByteAsInt(self) -> int:
retVal = 0x00 retVal = 0x00
if not self.outOfBytes: if not self.outOfBytes:
@@ -43,7 +43,7 @@ class Packet:
return retVal return retVal
def getData(self) -> list[int]: def getData(self) -> bytes:
""" """
* Returns the packet data. * Returns the packet data.
* *
@@ -51,7 +51,7 @@ class Packet:
""" """
return self.packetData return self.packetData
def setData(self, data: list[int]): def setData(self, data: bytes):
""" """
* Sets the packet data. * Sets the packet data.
* *
@@ -65,7 +65,7 @@ class Packet:
# Read ints in from the data buffer # Read ints in from the data buffer
intList = [] intList = []
for _ in range(numBytes): for _ in range(numBytes):
intList.append(self._getNextByte()) intList.append(self._getNextByteAsInt())
# Interpret the bytes as a floating point number # Interpret the bytes as a floating point number
value = struct.unpack(unpackFormat, bytes(intList))[0] value = struct.unpack(unpackFormat, bytes(intList))[0]

View File

@@ -4,7 +4,7 @@ from wpilib import Timer
import wpilib import wpilib
from photonlibpy.packet import Packet from photonlibpy.packet import Packet
from photonlibpy.photonPipelineResult import PhotonPipelineResult from photonlibpy.photonPipelineResult import PhotonPipelineResult
from photonlibpy.version import PHOTONVISION_VERSION, PHOTONLIB_VERSION from photonlibpy.version import PHOTONVISION_VERSION, PHOTONLIB_VERSION # type: ignore[import-untyped]
class VisionLEDMode(Enum): class VisionLEDMode(Enum):
@@ -86,10 +86,11 @@ class PhotonCamera:
if len(byteList) < 1: if len(byteList) < 1:
return retVal return retVal
else: else:
retVal.populateFromPacket(Packet(byteList)) pkt = Packet(byteList)
retVal.populateFromPacket(pkt)
# NT4 allows us to correct the timestamp based on when the message was sent # NT4 allows us to correct the timestamp based on when the message was sent
retVal.setTimestampSeconds( retVal.setTimestampSeconds(
timestamp / 1e-6 - retVal.getLatencyMillis() / 1e-3 timestamp / 1e6 - retVal.getLatencyMillis() / 1e3
) )
return retVal return retVal

View File

@@ -38,3 +38,6 @@ class PhotonPipelineResult:
def getTargets(self) -> list[PhotonTrackedTarget]: def getTargets(self) -> list[PhotonTrackedTarget]:
return self.targets return self.targets
def hasTargets(self) -> bool:
return len(self.targets) > 0

View File

@@ -75,7 +75,7 @@ class PhotonPoseEstimator:
self._multiTagFallbackStrategy = PoseStrategy.LOWEST_AMBIGUITY self._multiTagFallbackStrategy = PoseStrategy.LOWEST_AMBIGUITY
self._reportedErrors: set[int] = set() self._reportedErrors: set[int] = set()
self._poseCacheTimestampSeconds = -1 self._poseCacheTimestampSeconds = -1.0
self._lastPose: Optional[Pose3d] = None self._lastPose: Optional[Pose3d] = None
self._referencePose: Optional[Pose3d] = None self._referencePose: Optional[Pose3d] = None
@@ -143,7 +143,7 @@ class PhotonPoseEstimator:
self._multiTagFallbackStrategy = strategy self._multiTagFallbackStrategy = strategy
@property @property
def referencePose(self) -> Pose3d: def referencePose(self) -> Optional[Pose3d]:
"""Return the reference position that is being used by the estimator. """Return the reference position that is being used by the estimator.
:returns: the referencePose :returns: the referencePose
@@ -163,7 +163,7 @@ class PhotonPoseEstimator:
self._referencePose = referencePose self._referencePose = referencePose
@property @property
def lastPose(self) -> Pose3d: def lastPose(self) -> Optional[Pose3d]:
return self._lastPose return self._lastPose
@lastPose.setter @lastPose.setter
@@ -178,10 +178,10 @@ class PhotonPoseEstimator:
self._checkUpdate(self._lastPose, lastPose) self._checkUpdate(self._lastPose, lastPose)
self._lastPose = lastPose self._lastPose = lastPose
def _invalidatePoseCache(self): def _invalidatePoseCache(self) -> None:
self._poseCacheTimestampSeconds = -1 self._poseCacheTimestampSeconds = -1.0
def _checkUpdate(self, oldObj, newObj): def _checkUpdate(self, oldObj, newObj) -> None:
if oldObj != newObj and oldObj is not None and oldObj is not newObj: if oldObj != newObj and oldObj is not None and oldObj is not newObj:
self._invalidatePoseCache() self._invalidatePoseCache()
@@ -204,27 +204,27 @@ class PhotonPoseEstimator:
if not cameraResult: if not cameraResult:
if not self._camera: if not self._camera:
wpilib.reportError("[PhotonPoseEstimator] Missing camera!", False) wpilib.reportError("[PhotonPoseEstimator] Missing camera!", False)
return return None
cameraResult = self._camera.getLatestResult() cameraResult = self._camera.getLatestResult()
if cameraResult.timestampSec < 0: if cameraResult.timestampSec < 0:
return return None
# If the pose cache timestamp was set, and the result is from the same # If the pose cache timestamp was set, and the result is from the same
# timestamp, return an # timestamp, return an
# empty result # empty result
if ( if (
self._poseCacheTimestampSeconds > 0 self._poseCacheTimestampSeconds > 0.0
and abs(self._poseCacheTimestampSeconds - cameraResult.timestampSec) < 1e-6 and abs(self._poseCacheTimestampSeconds - cameraResult.timestampSec) < 1e-6
): ):
return return None
# Remember the timestamp of the current result used # Remember the timestamp of the current result used
self._poseCacheTimestampSeconds = cameraResult.timestampSec self._poseCacheTimestampSeconds = cameraResult.timestampSec
# If no targets seen, trivial case -- return empty result # If no targets seen, trivial case -- return empty result
if not cameraResult.targets: if not cameraResult.targets:
return return None
return self._update(cameraResult, self._primaryStrategy) return self._update(cameraResult, self._primaryStrategy)
@@ -239,7 +239,7 @@ class PhotonPoseEstimator:
wpilib.reportError( wpilib.reportError(
"[PhotonPoseEstimator] Unknown Position Estimation Strategy!", False "[PhotonPoseEstimator] Unknown Position Estimation Strategy!", False
) )
return return None
if not estimatedPose: if not estimatedPose:
self._lastPose = None self._lastPose = None
@@ -280,7 +280,7 @@ class PhotonPoseEstimator:
""" """
lowestAmbiguityTarget = None lowestAmbiguityTarget = None
lowestAmbiguityScore = 10 lowestAmbiguityScore = 10.0
for target in result.targets: for target in result.targets:
targetPoseAmbiguity = target.poseAmbiguity targetPoseAmbiguity = target.poseAmbiguity
@@ -293,7 +293,7 @@ class PhotonPoseEstimator:
# Although there are confirmed to be targets, none of them may be fiducial # Although there are confirmed to be targets, none of them may be fiducial
# targets. # targets.
if not lowestAmbiguityTarget: if not lowestAmbiguityTarget:
return return None
targetFiducialId = lowestAmbiguityTarget.fiducialId targetFiducialId = lowestAmbiguityTarget.fiducialId
@@ -301,7 +301,7 @@ class PhotonPoseEstimator:
if not targetPosition: if not targetPosition:
self._reportFiducialPoseError(targetFiducialId) self._reportFiducialPoseError(targetFiducialId)
return return None
return EstimatedRobotPose( return EstimatedRobotPose(
targetPosition.transformBy( targetPosition.transformBy(

View File

@@ -408,8 +408,8 @@ public class PhotonPoseEstimator {
return Optional.empty(); return Optional.empty();
} }
if (estimatedPose.isEmpty()) { if (estimatedPose.isPresent()) {
lastPose = null; lastPose = estimatedPose.get().estimatedPose;
} }
return estimatedPose; return estimatedPose;

View File

@@ -431,7 +431,7 @@ public class PhotonCameraSim implements AutoCloseable {
detectableTgts.add( detectableTgts.add(
new PhotonTrackedTarget( new PhotonTrackedTarget(
Math.toDegrees(centerRot.getZ()), -Math.toDegrees(centerRot.getZ()),
-Math.toDegrees(centerRot.getY()), -Math.toDegrees(centerRot.getY()),
areaPercent, areaPercent,
Math.toDegrees(centerRot.getX()), Math.toDegrees(centerRot.getX()),

View File

@@ -186,6 +186,9 @@ std::optional<EstimatedRobotPose> PhotonPoseEstimator::Update(
ret = std::nullopt; ret = std::nullopt;
} }
if (ret) {
lastPose = ret.value().estimatedPose;
}
return ret; return ret;
} }

View File

@@ -260,7 +260,7 @@ class PhotonCameraSim {
std::vector<std::pair<double, double>> cornersDouble{cornersFloat.begin(), std::vector<std::pair<double, double>> cornersDouble{cornersFloat.begin(),
cornersFloat.end()}; cornersFloat.end()};
detectableTgts.emplace_back(PhotonTrackedTarget{ detectableTgts.emplace_back(PhotonTrackedTarget{
centerRot.Z().convert<units::degrees>().to<double>(), -centerRot.Z().convert<units::degrees>().to<double>(),
-centerRot.Y().convert<units::degrees>().to<double>(), areaPercent, -centerRot.Y().convert<units::degrees>().to<double>(), areaPercent,
centerRot.X().convert<units::degrees>().to<double>(), tgt.fiducialId, centerRot.X().convert<units::degrees>().to<double>(), tgt.fiducialId,
pnpSim.best, pnpSim.alt, pnpSim.ambiguity, smallVec, cornersDouble}); pnpSim.best, pnpSim.alt, pnpSim.ambiguity, smallVec, cornersDouble});

View File

@@ -256,7 +256,8 @@ class VisionSystemSimTest {
cameraSim.setMinTargetAreaPixels(0.0); cameraSim.setMinTargetAreaPixels(0.0);
visionSysSim.addVisionTargets(new VisionTargetSim(targetPose, new TargetModel(0.5, 0.5), 3)); visionSysSim.addVisionTargets(new VisionTargetSim(targetPose, new TargetModel(0.5, 0.5), 3));
var robotPose = new Pose2d(new Translation2d(10, 0), Rotation2d.fromDegrees(-1.0 * testYaw)); // If the robot is rotated x deg (CCW+), the target yaw should be x deg (CW+)
var robotPose = new Pose2d(new Translation2d(10, 0), Rotation2d.fromDegrees(testYaw));
visionSysSim.update(robotPose); visionSysSim.update(robotPose);
var res = camera.getLatestResult(); var res = camera.getLatestResult();
assertTrue(res.hasTargets()); assertTrue(res.hasTargets());

View File

@@ -220,8 +220,9 @@ TEST_P(VisionSystemSimTestWithParamsTest, YawAngles) {
visionSysSim.AddVisionTargets({photon::VisionTargetSim{ visionSysSim.AddVisionTargets({photon::VisionTargetSim{
targetPose, photon::TargetModel{0.5_m, 0.5_m}, 3}}); targetPose, photon::TargetModel{0.5_m, 0.5_m}, 3}});
robotPose = frc::Pose2d{frc::Translation2d{10_m, 0_m}, // If the robot is rotated x deg (CCW+), the target yaw should be x deg (CW+)
frc::Rotation2d{-1 * GetParam()}}; robotPose =
frc::Pose2d{frc::Translation2d{10_m, 0_m}, frc::Rotation2d{GetParam()}};
visionSysSim.Update(robotPose); visionSysSim.Update(robotPose);
ASSERT_TRUE(camera.GetLatestResult().HasTargets()); ASSERT_TRUE(camera.GetLatestResult().HasTargets());
ASSERT_NEAR(GetParam().to<double>(), ASSERT_NEAR(GetParam().to<double>(),

View File

@@ -350,8 +350,7 @@ public class DataSocketHandler {
} }
} }
private void sendMessage(Object message, WsContext user) throws JsonProcessingException { private void sendMessage(ByteBuffer b, WsContext user) throws JsonProcessingException {
ByteBuffer b = ByteBuffer.wrap(objectMapper.writeValueAsBytes(message));
if (user.session.isOpen()) { if (user.session.isOpen()) {
user.send(b); user.send(b);
} }
@@ -359,16 +358,18 @@ public class DataSocketHandler {
public void broadcastMessage(Object message, WsContext userToSkip) public void broadcastMessage(Object message, WsContext userToSkip)
throws JsonProcessingException { throws JsonProcessingException {
ByteBuffer b = ByteBuffer.wrap(objectMapper.writeValueAsBytes(message));
if (userToSkip == null) { if (userToSkip == null) {
for (WsContext user : users) { for (WsContext user : users) {
sendMessage(message, user); sendMessage(b, user);
} }
} else { } else {
var skipUserPort = ((InetSocketAddress) userToSkip.session.getRemoteAddress()).getPort(); var skipUserPort = ((InetSocketAddress) userToSkip.session.getRemoteAddress()).getPort();
for (WsContext user : users) { for (WsContext user : users) {
var userPort = ((InetSocketAddress) user.session.getRemoteAddress()).getPort(); var userPort = ((InetSocketAddress) user.session.getRemoteAddress()).getPort();
if (userPort != skipUserPort) { if (userPort != skipUserPort) {
sendMessage(message, user); sendMessage(b, user);
} }
} }
} }

View File

@@ -31,6 +31,9 @@ import java.util.HashMap;
import java.util.Optional; import java.util.Optional;
import javax.imageio.ImageIO; import javax.imageio.ImageIO;
import org.apache.commons.io.FileUtils; import org.apache.commons.io.FileUtils;
import org.opencv.core.MatOfByte;
import org.opencv.core.MatOfInt;
import org.opencv.imgcodecs.Imgcodecs;
import org.photonvision.common.configuration.ConfigManager; import org.photonvision.common.configuration.ConfigManager;
import org.photonvision.common.configuration.NetworkConfig; import org.photonvision.common.configuration.NetworkConfig;
import org.photonvision.common.dataflow.DataChangeDestination; import org.photonvision.common.dataflow.DataChangeDestination;
@@ -580,6 +583,77 @@ public class RequestHandler {
ctx.status(204); ctx.status(204);
} }
public static void onCalibrationSnapshotRequest(Context ctx) {
logger.info(ctx.queryString().toString());
int idx = Integer.parseInt(ctx.queryParam("cameraIdx"));
var width = Integer.parseInt(ctx.queryParam("width"));
var height = Integer.parseInt(ctx.queryParam("height"));
var observationIdx = Integer.parseInt(ctx.queryParam("snapshotIdx"));
CameraCalibrationCoefficients calList =
VisionModuleManager.getInstance()
.getModule(idx)
.getStateAsCameraConfig()
.calibrations
.stream()
.filter(
it ->
Math.abs(it.resolution.width - width) < 1e-4
&& Math.abs(it.resolution.height - height) < 1e-4)
.findFirst()
.orElse(null);
if (calList == null || calList.observations.size() < observationIdx) {
ctx.status(404);
return;
}
// encode as jpeg to save even more space. reduces size of a 1280p image from 300k to 25k
var jpegBytes = new MatOfByte();
Imgcodecs.imencode(
".jpg",
calList.observations.get(observationIdx).snapshotData.getAsMat(),
jpegBytes,
new MatOfInt(Imgcodecs.IMWRITE_JPEG_QUALITY, 60));
ctx.result(jpegBytes.toArray());
jpegBytes.release();
ctx.status(200);
}
public static void onCalibrationExportRequest(Context ctx) {
logger.info(ctx.queryString().toString());
int idx = Integer.parseInt(ctx.queryParam("cameraIdx"));
var width = Integer.parseInt(ctx.queryParam("width"));
var height = Integer.parseInt(ctx.queryParam("height"));
var cc = VisionModuleManager.getInstance().getModule(idx).getStateAsCameraConfig();
CameraCalibrationCoefficients calList =
cc.calibrations.stream()
.filter(
it ->
Math.abs(it.resolution.width - width) < 1e-4
&& Math.abs(it.resolution.height - height) < 1e-4)
.findFirst()
.orElse(null);
if (calList == null) {
ctx.status(404);
return;
}
var filename = "photon_calibration_" + cc.uniqueName + "_" + width + "x" + height + ".json";
ctx.contentType("application/zip");
ctx.header("Content-Disposition", "attachment; filename=\"" + filename + "\"");
ctx.json(calList);
ctx.status(200);
}
public static void onImageSnapshotsRequest(Context ctx) { public static void onImageSnapshotsRequest(Context ctx) {
var snapshots = new ArrayList<HashMap<String, Object>>(); var snapshots = new ArrayList<HashMap<String, Object>>();
var cameraDirs = ConfigManager.getInstance().getImageSavePath().toFile().listFiles(); var cameraDirs = ConfigManager.getInstance().getImageSavePath().toFile().listFiles();

View File

@@ -130,6 +130,8 @@ public class Server {
app.post("/api/utils/restartDevice", RequestHandler::onDeviceRestartRequest); app.post("/api/utils/restartDevice", RequestHandler::onDeviceRestartRequest);
app.post("/api/utils/publishMetrics", RequestHandler::onMetricsPublishRequest); app.post("/api/utils/publishMetrics", RequestHandler::onMetricsPublishRequest);
app.get("/api/utils/getImageSnapshots", RequestHandler::onImageSnapshotsRequest); app.get("/api/utils/getImageSnapshots", RequestHandler::onImageSnapshotsRequest);
app.get("/api/utils/getCalSnapshot", RequestHandler::onCalibrationSnapshotRequest);
app.get("/api/utils/getCalibrationJSON", RequestHandler::onCalibrationExportRequest);
// Calibration // Calibration
app.post("/api/calibration/end", RequestHandler::onCalibrationEndRequest); app.post("/api/calibration/end", RequestHandler::onCalibrationEndRequest);

View File

@@ -1,7 +1,7 @@
plugins { plugins {
id "cpp" id "cpp"
id "google-test-test-suite" id "google-test-test-suite"
id "edu.wpi.first.GradleRIO" version "2024.2.1" id "edu.wpi.first.GradleRIO" version "2024.3.1"
id "com.dorongold.task-tree" version "2.1.0" id "com.dorongold.task-tree" version "2.1.0"
} }

View File

@@ -1,7 +1,7 @@
plugins { plugins {
id "cpp" id "cpp"
id "google-test-test-suite" id "google-test-test-suite"
id "edu.wpi.first.GradleRIO" version "2024.2.1" id "edu.wpi.first.GradleRIO" version "2024.3.1"
id "com.dorongold.task-tree" version "2.1.0" id "com.dorongold.task-tree" version "2.1.0"
} }

View File

@@ -39,8 +39,8 @@ class Robot : public frc::TimedRobot {
void TeleopPeriodic() override; void TeleopPeriodic() override;
private: private:
// Change this to match the name of your camera // Change this to match the name of your camera as shown in the web UI
photon::PhotonCamera camera{"photonvision"}; photon::PhotonCamera camera{"YOUR_CAMERA_NAME_HERE"};
// PID constants should be tuned per robot // PID constants should be tuned per robot
frc::PIDController controller{.1, 0, 0}; frc::PIDController controller{.1, 0, 0};

View File

@@ -1,7 +1,7 @@
plugins { plugins {
id "cpp" id "cpp"
id "google-test-test-suite" id "google-test-test-suite"
id "edu.wpi.first.GradleRIO" version "2024.2.1" id "edu.wpi.first.GradleRIO" version "2024.3.1"
id "com.dorongold.task-tree" version "2.1.0" id "com.dorongold.task-tree" version "2.1.0"
} }

View File

@@ -1,7 +1,7 @@
plugins { plugins {
id "cpp" id "cpp"
id "google-test-test-suite" id "google-test-test-suite"
id "edu.wpi.first.GradleRIO" version "2024.2.1" id "edu.wpi.first.GradleRIO" version "2024.3.1"
id "com.dorongold.task-tree" version "2.1.0" id "com.dorongold.task-tree" version "2.1.0"
} }

View File

@@ -1,7 +1,7 @@
plugins { plugins {
id "cpp" id "cpp"
id "google-test-test-suite" id "google-test-test-suite"
id "edu.wpi.first.GradleRIO" version "2024.2.1" id "edu.wpi.first.GradleRIO" version "2024.3.1"
id "com.dorongold.task-tree" version "2.1.0" id "com.dorongold.task-tree" version "2.1.0"
} }
@@ -12,8 +12,8 @@ repositories {
} }
wpi.maven.useDevelopment = true wpi.maven.useDevelopment = true
wpi.versions.wpilibVersion = "2024.2.1" wpi.versions.wpilibVersion = "2024.3.1"
wpi.versions.wpimathVersion = "2024.2.1" wpi.versions.wpimathVersion = "2024.3.1"
apply from: "${rootDir}/../shared/examples_common.gradle" apply from: "${rootDir}/../shared/examples_common.gradle"

View File

@@ -1,6 +1,6 @@
plugins { plugins {
id "java" id "java"
id "edu.wpi.first.GradleRIO" version "2024.2.1" id "edu.wpi.first.GradleRIO" version "2024.3.1"
} }
sourceCompatibility = JavaVersion.VERSION_11 sourceCompatibility = JavaVersion.VERSION_11

View File

@@ -1,6 +1,6 @@
plugins { plugins {
id "java" id "java"
id "edu.wpi.first.GradleRIO" version "2024.2.1" id "edu.wpi.first.GradleRIO" version "2024.3.1"
} }
sourceCompatibility = JavaVersion.VERSION_11 sourceCompatibility = JavaVersion.VERSION_11

View File

@@ -48,8 +48,8 @@ public class Robot extends TimedRobot {
// How far from the target we want to be // How far from the target we want to be
final double GOAL_RANGE_METERS = Units.feetToMeters(3); final double GOAL_RANGE_METERS = Units.feetToMeters(3);
// Change this to match the name of your camera // Change this to match the name of your camera as shown in the web UI
PhotonCamera camera = new PhotonCamera("photonvision"); PhotonCamera camera = new PhotonCamera("YOUR_CAMERA_NAME_HERE");
// PID constants should be tuned per robot // PID constants should be tuned per robot
final double LINEAR_P = 0.1; final double LINEAR_P = 0.1;

View File

@@ -1,6 +1,6 @@
plugins { plugins {
id "java" id "java"
id "edu.wpi.first.GradleRIO" version "2024.2.1" id "edu.wpi.first.GradleRIO" version "2024.3.1"
} }
sourceCompatibility = JavaVersion.VERSION_11 sourceCompatibility = JavaVersion.VERSION_11

View File

@@ -1,6 +1,6 @@
plugins { plugins {
id "java" id "java"
id "edu.wpi.first.GradleRIO" version "2024.2.1" id "edu.wpi.first.GradleRIO" version "2024.3.1"
} }
sourceCompatibility = JavaVersion.VERSION_11 sourceCompatibility = JavaVersion.VERSION_11

View File

@@ -1,6 +1,6 @@
plugins { plugins {
id "java" id "java"
id "edu.wpi.first.GradleRIO" version "2024.2.1" id "edu.wpi.first.GradleRIO" version "2024.3.1"
} }
sourceCompatibility = JavaVersion.VERSION_11 sourceCompatibility = JavaVersion.VERSION_11
@@ -11,8 +11,8 @@ apply from: "${rootDir}/../shared/examples_common.gradle"
def ROBOT_MAIN_CLASS = "frc.robot.Main" def ROBOT_MAIN_CLASS = "frc.robot.Main"
wpi.maven.useDevelopment = true wpi.maven.useDevelopment = true
wpi.versions.wpilibVersion = "2024.2.1" wpi.versions.wpilibVersion = "2024.3.1"
wpi.versions.wpimathVersion = "2024.2.1" wpi.versions.wpimathVersion = "2024.3.1"
// Define my targets (RoboRIO) and artifacts (deployable files) // Define my targets (RoboRIO) and artifacts (deployable files)

View File

@@ -80,6 +80,9 @@ if [[ "$DISTRO" = "Ubuntu" && "$INSTALL_NETWORK_MANAGER" != "true" && -z "$QUIET
fi fi
fi fi
echo "Update package list"
apt-get update
echo "Installing curl..." echo "Installing curl..."
apt-get install --yes curl apt-get install --yes curl
echo "curl installation complete." echo "curl installation complete."
@@ -132,6 +135,13 @@ fi
echo "Installing additional math packages" echo "Installing additional math packages"
apt-get install --yes libcholmod3 liblapack3 libsuitesparseconfig5 apt-get install --yes libcholmod3 liblapack3 libsuitesparseconfig5
echo "Installing v4l-utils..."
apt-get install --yes v4l-utils
echo "v4l-utils installation complete."
echo "Installing sqlite3"
apt-get install --yes sqlite3
echo "Downloading latest stable release of PhotonVision..." echo "Downloading latest stable release of PhotonVision..."
mkdir -p /opt/photonvision mkdir -p /opt/photonvision
cd /opt/photonvision cd /opt/photonvision