mirror of
https://github.com/PhotonVision/photonvision
synced 2026-07-03 03:01:40 +00:00
Compare commits
36 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ec66645667 | ||
|
|
39aaa34520 | ||
|
|
4a3200d0c0 | ||
|
|
01dc7ea5ce | ||
|
|
2a9502be3d | ||
|
|
39216db143 | ||
|
|
428f926ac2 | ||
|
|
4efeb3d412 | ||
|
|
6a2d83e19b | ||
|
|
1c0d92641f | ||
|
|
9653c46bdb | ||
|
|
3738e7821b | ||
|
|
0eb0a4e3c5 | ||
|
|
7666f152bb | ||
|
|
45a39f6609 | ||
|
|
bc55218739 | ||
|
|
e616d93d59 | ||
|
|
5851509a9e | ||
|
|
ea1b701ba7 | ||
|
|
62112cd2fd | ||
|
|
c7508fea46 | ||
|
|
eca3cea82d | ||
|
|
cbbfbda59d | ||
|
|
a3e1dda3aa | ||
|
|
939283df0e | ||
|
|
43338a4e96 | ||
|
|
bcea6fcc8d | ||
|
|
90773e0e4a | ||
|
|
57f02f31a5 | ||
|
|
580bbb4a4d | ||
|
|
4a0c15b61b | ||
|
|
a1df37e20f | ||
|
|
644c162834 | ||
|
|
5f591a51c4 | ||
|
|
d59be893ae | ||
|
|
f13a507a71 |
8
.github/workflows/build.yml
vendored
8
.github/workflows/build.yml
vendored
@@ -290,7 +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.4/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
|
||||||
|
image_additional_mb: 4096
|
||||||
|
- os: ubuntu-latest
|
||||||
|
artifact-name: LinuxArm64
|
||||||
|
image_suffix: orangepi5plus
|
||||||
|
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
|
||||||
|
|
||||||
|
|||||||
22
.github/workflows/documentation.yml
vendored
22
.github/workflows/documentation.yml
vendored
@@ -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/
|
||||||
|
|||||||
@@ -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.1.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.1.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";
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -311,7 +306,7 @@ const setSelectedVideoFormat = (format: VideoFormat) => {
|
|||||||
/>
|
/>
|
||||||
<pv-number-input
|
<pv-number-input
|
||||||
v-model="patternWidth"
|
v-model="patternWidth"
|
||||||
label="Board Width (in)"
|
label="Board Width (squares)"
|
||||||
tooltip="Width of the board in dots or chessboard squares"
|
tooltip="Width of the board in dots or chessboard squares"
|
||||||
:disabled="isCalibrating"
|
:disabled="isCalibrating"
|
||||||
:rules="[(v) => v >= 4 || 'Width must be at least 4']"
|
:rules="[(v) => v >= 4 || 'Width must be at least 4']"
|
||||||
@@ -319,7 +314,7 @@ const setSelectedVideoFormat = (format: VideoFormat) => {
|
|||||||
/>
|
/>
|
||||||
<pv-number-input
|
<pv-number-input
|
||||||
v-model="patternHeight"
|
v-model="patternHeight"
|
||||||
label="Board Height (in)"
|
label="Board Height (squares)"
|
||||||
tooltip="Height of the board in dots or chessboard squares"
|
tooltip="Height of the board in dots or chessboard squares"
|
||||||
:disabled="isCalibrating"
|
:disabled="isCalibrating"
|
||||||
:rules="[(v) => v >= 4 || 'Height must be at least 4']"
|
:rules="[(v) => v >= 4 || 'Height must be at least 4']"
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ const changeCurrentCameraIndex = (index: number) => {
|
|||||||
break;
|
break;
|
||||||
case PipelineType.ObjectDetection:
|
case PipelineType.ObjectDetection:
|
||||||
pipelineType.value = WebsocketPipelineType.ObjectDetection;
|
pipelineType.value = WebsocketPipelineType.ObjectDetection;
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -124,6 +125,18 @@ const cancelPipelineNameEdit = () => {
|
|||||||
const showPipelineCreationDialog = ref(false);
|
const showPipelineCreationDialog = ref(false);
|
||||||
const newPipelineName = ref("");
|
const newPipelineName = ref("");
|
||||||
const newPipelineType = ref<WebsocketPipelineType>(useCameraSettingsStore().currentWebsocketPipelineType);
|
const newPipelineType = ref<WebsocketPipelineType>(useCameraSettingsStore().currentWebsocketPipelineType);
|
||||||
|
const validNewPipelineTypes = computed(() => {
|
||||||
|
const pipelineTypes = [
|
||||||
|
{ name: "Reflective", value: WebsocketPipelineType.Reflective },
|
||||||
|
{ name: "Colored Shape", value: WebsocketPipelineType.ColoredShape },
|
||||||
|
{ name: "AprilTag", value: WebsocketPipelineType.AprilTag },
|
||||||
|
{ name: "Aruco", value: WebsocketPipelineType.Aruco }
|
||||||
|
];
|
||||||
|
if (useSettingsStore().general.rknnSupported) {
|
||||||
|
pipelineTypes.push({ name: "Object Detection", value: WebsocketPipelineType.ObjectDetection });
|
||||||
|
}
|
||||||
|
return pipelineTypes;
|
||||||
|
});
|
||||||
const showCreatePipelineDialog = () => {
|
const showCreatePipelineDialog = () => {
|
||||||
newPipelineName.value = "";
|
newPipelineName.value = "";
|
||||||
newPipelineType.value = useCameraSettingsStore().currentWebsocketPipelineType;
|
newPipelineType.value = useCameraSettingsStore().currentWebsocketPipelineType;
|
||||||
@@ -359,13 +372,7 @@ useCameraSettingsStore().$subscribe((mutation, state) => {
|
|||||||
:select-cols="12 - 3"
|
:select-cols="12 - 3"
|
||||||
label="Tracking Type"
|
label="Tracking Type"
|
||||||
tooltip="Pipeline type, which changes the type of processing that will happen on input frames"
|
tooltip="Pipeline type, which changes the type of processing that will happen on input frames"
|
||||||
:items="[
|
:items="validNewPipelineTypes"
|
||||||
{ name: 'Reflective', value: WebsocketPipelineType.Reflective },
|
|
||||||
{ name: 'Colored Shape', value: WebsocketPipelineType.ColoredShape },
|
|
||||||
{ name: 'AprilTag', value: WebsocketPipelineType.AprilTag },
|
|
||||||
{ name: 'Aruco', value: WebsocketPipelineType.Aruco },
|
|
||||||
{ name: 'Object Detection', value: WebsocketPipelineType.ObjectDetection }
|
|
||||||
]"
|
|
||||||
/>
|
/>
|
||||||
</v-card-text>
|
</v-card-text>
|
||||||
<v-divider />
|
<v-divider />
|
||||||
|
|||||||
@@ -14,13 +14,12 @@ const currentPipelineSettings = computed<ActivePipelineSettings>(
|
|||||||
() => useCameraSettingsStore().currentPipelineSettings
|
() => useCameraSettingsStore().currentPipelineSettings
|
||||||
);
|
);
|
||||||
|
|
||||||
const interactiveCols = computed(
|
const interactiveCols = computed(() =>
|
||||||
() =>
|
(getCurrentInstance()?.proxy.$vuetify.breakpoint.mdAndDown || false) &&
|
||||||
(getCurrentInstance()?.proxy.$vuetify.breakpoint.mdAndDown || false) &&
|
(!useStateStore().sidebarFolded || useCameraSettingsStore().isDriverMode)
|
||||||
(!useStateStore().sidebarFolded || useCameraSettingsStore().isDriverMode)
|
? 9
|
||||||
)
|
: 8
|
||||||
? 9
|
);
|
||||||
: 8;
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|||||||
@@ -14,13 +14,12 @@ const currentPipelineSettings = computed<ActivePipelineSettings>(
|
|||||||
() => useCameraSettingsStore().currentPipelineSettings
|
() => useCameraSettingsStore().currentPipelineSettings
|
||||||
);
|
);
|
||||||
|
|
||||||
const interactiveCols = computed(
|
const interactiveCols = computed(() =>
|
||||||
() =>
|
(getCurrentInstance()?.proxy.$vuetify.breakpoint.mdAndDown || false) &&
|
||||||
(getCurrentInstance()?.proxy.$vuetify.breakpoint.mdAndDown || false) &&
|
(!useStateStore().sidebarFolded || useCameraSettingsStore().isDriverMode)
|
||||||
(!useStateStore().sidebarFolded || useCameraSettingsStore().isDriverMode)
|
? 9
|
||||||
)
|
: 8
|
||||||
? 9
|
);
|
||||||
: 8;
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|||||||
@@ -49,13 +49,12 @@ const contourRadius = computed<[number, number]>({
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const interactiveCols = computed(
|
const interactiveCols = computed(() =>
|
||||||
() =>
|
(getCurrentInstance()?.proxy.$vuetify.breakpoint.mdAndDown || false) &&
|
||||||
(getCurrentInstance()?.proxy.$vuetify.breakpoint.mdAndDown || false) &&
|
(!useStateStore().sidebarFolded || useCameraSettingsStore().isDriverMode)
|
||||||
(!useStateStore().sidebarFolded || useCameraSettingsStore().isDriverMode)
|
? 9
|
||||||
)
|
: 8
|
||||||
? 9
|
);
|
||||||
: 8;
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|||||||
@@ -63,13 +63,12 @@ const handleStreamResolutionChange = (value: number) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const interactiveCols = computed(
|
const interactiveCols = computed(() =>
|
||||||
() =>
|
(getCurrentInstance()?.proxy.$vuetify.breakpoint.mdAndDown || false) &&
|
||||||
(getCurrentInstance()?.proxy.$vuetify.breakpoint.mdAndDown || false) &&
|
(!useStateStore().sidebarFolded || useCameraSettingsStore().isDriverMode)
|
||||||
(!useStateStore().sidebarFolded || useCameraSettingsStore().isDriverMode)
|
? 9
|
||||||
)
|
: 8
|
||||||
? 9
|
);
|
||||||
: 8;
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|||||||
@@ -1,21 +1,32 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useCameraSettingsStore } from "@/stores/settings/CameraSettingsStore";
|
import { useCameraSettingsStore } from "@/stores/settings/CameraSettingsStore";
|
||||||
import { PipelineType } from "@/types/PipelineTypes";
|
import { type ActivePipelineSettings, PipelineType } from "@/types/PipelineTypes";
|
||||||
import PvSlider from "@/components/common/pv-slider.vue";
|
import PvSlider from "@/components/common/pv-slider.vue";
|
||||||
import { computed, getCurrentInstance } from "vue";
|
import { computed, getCurrentInstance } from "vue";
|
||||||
import { useStateStore } from "@/stores/StateStore";
|
import { useStateStore } from "@/stores/StateStore";
|
||||||
|
|
||||||
// TODO fix pipeline typing in order to fix this, the store settings call should be able to infer that only valid pipeline type settings are exposed based on pre-checks for the entire config section
|
// TODO fix pipeline typing in order to fix this, the store settings call should be able to infer that only valid pipeline type settings are exposed based on pre-checks for the entire config section
|
||||||
// Defer reference to store access method
|
// Defer reference to store access method
|
||||||
const currentPipelineSettings = useCameraSettingsStore().currentPipelineSettings;
|
const currentPipelineSettings = computed<ActivePipelineSettings>(
|
||||||
|
() => useCameraSettingsStore().currentPipelineSettings
|
||||||
|
);
|
||||||
|
|
||||||
const interactiveCols = computed(
|
// TODO fix pv-range-slider so that store access doesn't need to be deferred
|
||||||
() =>
|
const contourArea = computed<[number, number]>({
|
||||||
(getCurrentInstance()?.proxy.$vuetify.breakpoint.mdAndDown || false) &&
|
get: () => Object.values(useCameraSettingsStore().currentPipelineSettings.contourArea) as [number, number],
|
||||||
(!useStateStore().sidebarFolded || useCameraSettingsStore().isDriverMode)
|
set: (v) => (useCameraSettingsStore().currentPipelineSettings.contourArea = v)
|
||||||
)
|
});
|
||||||
? 9
|
const contourRatio = computed<[number, number]>({
|
||||||
: 8;
|
get: () => Object.values(useCameraSettingsStore().currentPipelineSettings.contourRatio) as [number, number],
|
||||||
|
set: (v) => (useCameraSettingsStore().currentPipelineSettings.contourRatio = v)
|
||||||
|
});
|
||||||
|
|
||||||
|
const interactiveCols = computed(() =>
|
||||||
|
(getCurrentInstance()?.proxy.$vuetify.breakpoint.mdAndDown || false) &&
|
||||||
|
(!useStateStore().sidebarFolded || useCameraSettingsStore().isDriverMode)
|
||||||
|
? 9
|
||||||
|
: 8
|
||||||
|
);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -31,5 +42,42 @@ const interactiveCols = computed(
|
|||||||
:step="0.01"
|
:step="0.01"
|
||||||
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ confidence: value }, false)"
|
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ confidence: value }, false)"
|
||||||
/>
|
/>
|
||||||
|
<pv-range-slider
|
||||||
|
v-model="contourArea"
|
||||||
|
label="Area"
|
||||||
|
:min="0"
|
||||||
|
:max="100"
|
||||||
|
:slider-cols="interactiveCols"
|
||||||
|
:step="0.01"
|
||||||
|
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ contourArea: value }, false)"
|
||||||
|
/>
|
||||||
|
<pv-range-slider
|
||||||
|
v-model="contourRatio"
|
||||||
|
label="Ratio (W/H)"
|
||||||
|
tooltip="Min and max ratio between the width and height of a contour's bounding rectangle"
|
||||||
|
:min="0"
|
||||||
|
:max="100"
|
||||||
|
:slider-cols="interactiveCols"
|
||||||
|
:step="0.01"
|
||||||
|
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ contourRatio: value }, false)"
|
||||||
|
/>
|
||||||
|
<pv-select
|
||||||
|
v-model="useCameraSettingsStore().currentPipelineSettings.contourTargetOrientation"
|
||||||
|
label="Target Orientation"
|
||||||
|
tooltip="Used to determine how to calculate target landmarks, as well as aspect ratio"
|
||||||
|
:items="['Portrait', 'Landscape']"
|
||||||
|
:select-cols="interactiveCols"
|
||||||
|
@input="
|
||||||
|
(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ contourTargetOrientation: value }, false)
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
<pv-select
|
||||||
|
v-model="currentPipelineSettings.contourSortMode"
|
||||||
|
label="Target Sort"
|
||||||
|
tooltip="Chooses the sorting mode used to determine the 'best' targets to provide to user code"
|
||||||
|
:select-cols="interactiveCols"
|
||||||
|
:items="['Largest', 'Smallest', 'Highest', 'Lowest', 'Rightmost', 'Leftmost', 'Centermost']"
|
||||||
|
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ contourSortMode: value }, false)"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -46,13 +46,12 @@ const currentPipelineSettings = computed<ActivePipelineSettings>(
|
|||||||
() => useCameraSettingsStore().currentPipelineSettings
|
() => useCameraSettingsStore().currentPipelineSettings
|
||||||
);
|
);
|
||||||
|
|
||||||
const interactiveCols = computed(
|
const interactiveCols = computed(() =>
|
||||||
() =>
|
(getCurrentInstance()?.proxy.$vuetify.breakpoint.mdAndDown || false) &&
|
||||||
(getCurrentInstance()?.proxy.$vuetify.breakpoint.mdAndDown || false) &&
|
(!useStateStore().sidebarFolded || useCameraSettingsStore().isDriverMode)
|
||||||
(!useStateStore().sidebarFolded || useCameraSettingsStore().isDriverMode)
|
? 9
|
||||||
)
|
: 8
|
||||||
? 9
|
);
|
||||||
: 8;
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|||||||
@@ -6,13 +6,12 @@ import PvSlider from "@/components/common/pv-slider.vue";
|
|||||||
import { computed, getCurrentInstance } from "vue";
|
import { computed, getCurrentInstance } from "vue";
|
||||||
import { useStateStore } from "@/stores/StateStore";
|
import { useStateStore } from "@/stores/StateStore";
|
||||||
|
|
||||||
const interactiveCols = computed(
|
const interactiveCols = computed(() =>
|
||||||
() =>
|
(getCurrentInstance()?.proxy.$vuetify.breakpoint.mdAndDown || false) &&
|
||||||
(getCurrentInstance()?.proxy.$vuetify.breakpoint.mdAndDown || false) &&
|
(!useStateStore().sidebarFolded || useCameraSettingsStore().isDriverMode)
|
||||||
(!useStateStore().sidebarFolded || useCameraSettingsStore().isDriverMode)
|
? 9
|
||||||
)
|
: 8
|
||||||
? 9
|
);
|
||||||
: 8;
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|||||||
@@ -124,13 +124,12 @@ onBeforeUnmount(() => {
|
|||||||
cameraStream.removeEventListener("click", handleStreamClick);
|
cameraStream.removeEventListener("click", handleStreamClick);
|
||||||
});
|
});
|
||||||
|
|
||||||
const interactiveCols = computed(
|
const interactiveCols = computed(() =>
|
||||||
() =>
|
(getCurrentInstance()?.proxy.$vuetify.breakpoint.mdAndDown || false) &&
|
||||||
(getCurrentInstance()?.proxy.$vuetify.breakpoint.mdAndDown || false) &&
|
(!useStateStore().sidebarFolded || useCameraSettingsStore().isDriverMode)
|
||||||
(!useStateStore().sidebarFolded || useCameraSettingsStore().isDriverMode)
|
? 9
|
||||||
)
|
: 8
|
||||||
? 9
|
);
|
||||||
: 8;
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|||||||
@@ -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 = () => {
|
||||||
|
|||||||
@@ -5,12 +5,11 @@ import PvInput from "@/components/common/pv-input.vue";
|
|||||||
import PvRadio from "@/components/common/pv-radio.vue";
|
import PvRadio from "@/components/common/pv-radio.vue";
|
||||||
import PvSwitch from "@/components/common/pv-switch.vue";
|
import PvSwitch from "@/components/common/pv-switch.vue";
|
||||||
import PvSelect from "@/components/common/pv-select.vue";
|
import PvSelect from "@/components/common/pv-select.vue";
|
||||||
import { NetworkConnectionType, type NetworkSettings } from "@/types/SettingTypes";
|
import { type ConfigurableNetworkSettings, NetworkConnectionType } from "@/types/SettingTypes";
|
||||||
import { useStateStore } from "@/stores/StateStore";
|
import { useStateStore } from "@/stores/StateStore";
|
||||||
|
|
||||||
// Copy object to remove reference to store
|
// Copy object to remove reference to store
|
||||||
const tempSettingsStruct = ref<NetworkSettings>(Object.assign({}, useSettingsStore().network));
|
const tempSettingsStruct = ref<ConfigurableNetworkSettings>(Object.assign({}, useSettingsStore().network));
|
||||||
|
|
||||||
const resetTempSettingsStruct = () => {
|
const resetTempSettingsStruct = () => {
|
||||||
tempSettingsStruct.value = Object.assign({}, useSettingsStore().network);
|
tempSettingsStruct.value = Object.assign({}, useSettingsStore().network);
|
||||||
};
|
};
|
||||||
@@ -58,10 +57,10 @@ const settingsHaveChanged = (): boolean => {
|
|||||||
a.runNTServer !== b.runNTServer ||
|
a.runNTServer !== b.runNTServer ||
|
||||||
a.shouldManage !== b.shouldManage ||
|
a.shouldManage !== b.shouldManage ||
|
||||||
a.shouldPublishProto !== b.shouldPublishProto ||
|
a.shouldPublishProto !== b.shouldPublishProto ||
|
||||||
a.canManage !== b.canManage ||
|
|
||||||
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
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -79,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
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -91,7 +91,10 @@ const saveGeneralSettings = () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Update the local settings cause the backend checked their validity. Assign is to deref value
|
// Update the local settings cause the backend checked their validity. Assign is to deref value
|
||||||
useSettingsStore().network = Object.assign({}, tempSettingsStruct.value);
|
useSettingsStore().network = {
|
||||||
|
...useSettingsStore().network,
|
||||||
|
...Object.assign({}, tempSettingsStruct.value)
|
||||||
|
};
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
resetTempSettingsStruct();
|
resetTempSettingsStruct();
|
||||||
@@ -136,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">
|
||||||
@@ -162,42 +167,63 @@ watchEffect(() => {
|
|||||||
The NetworkTables Server Address is not set or is invalid. NetworkTables is unable to connect.
|
The NetworkTables Server Address is not set or is invalid. NetworkTables is unable to connect.
|
||||||
</v-banner>
|
</v-banner>
|
||||||
<pv-radio
|
<pv-radio
|
||||||
|
v-show="!useSettingsStore().network.networkingDisabled"
|
||||||
v-model="tempSettingsStruct.connectionType"
|
v-model="tempSettingsStruct.connectionType"
|
||||||
label="IP Assignment Mode"
|
label="IP Assignment Mode"
|
||||||
tooltip="DHCP will make the radio (router) automatically assign an IP address; this may result in an IP address that changes across reboots. Static IP assignment means that you pick the IP address and it won't change."
|
tooltip="DHCP will make the radio (router) automatically assign an IP address; this may result in an IP address that changes across reboots. Static IP assignment means that you pick the IP address and it won't change."
|
||||||
:input-cols="12 - 4"
|
:input-cols="12 - 4"
|
||||||
:list="['DHCP', 'Static']"
|
:list="['DHCP', 'Static']"
|
||||||
:disabled="!(tempSettingsStruct.shouldManage && tempSettingsStruct.canManage)"
|
:disabled="
|
||||||
|
!tempSettingsStruct.shouldManage ||
|
||||||
|
!useSettingsStore().network.canManage ||
|
||||||
|
useSettingsStore().network.networkingDisabled
|
||||||
|
"
|
||||||
/>
|
/>
|
||||||
<pv-input
|
<pv-input
|
||||||
|
v-show="!useSettingsStore().network.networkingDisabled"
|
||||||
v-if="tempSettingsStruct.connectionType === NetworkConnectionType.Static"
|
v-if="tempSettingsStruct.connectionType === NetworkConnectionType.Static"
|
||||||
v-model="tempSettingsStruct.staticIp"
|
v-model="tempSettingsStruct.staticIp"
|
||||||
:input-cols="12 - 4"
|
:input-cols="12 - 4"
|
||||||
label="Static IP"
|
label="Static IP"
|
||||||
:rules="[(v) => isValidIPv4(v) || 'Invalid IPv4 address']"
|
:rules="[(v) => isValidIPv4(v) || 'Invalid IPv4 address']"
|
||||||
:disabled="!(tempSettingsStruct.shouldManage && tempSettingsStruct.canManage)"
|
:disabled="
|
||||||
|
!tempSettingsStruct.shouldManage ||
|
||||||
|
!useSettingsStore().network.canManage ||
|
||||||
|
useSettingsStore().network.networkingDisabled
|
||||||
|
"
|
||||||
/>
|
/>
|
||||||
<pv-input
|
<pv-input
|
||||||
|
v-show="!useSettingsStore().network.networkingDisabled"
|
||||||
v-model="tempSettingsStruct.hostname"
|
v-model="tempSettingsStruct.hostname"
|
||||||
label="Hostname"
|
label="Hostname"
|
||||||
:input-cols="12 - 4"
|
:input-cols="12 - 4"
|
||||||
:rules="[(v) => isValidHostname(v) || 'Invalid hostname']"
|
:rules="[(v) => isValidHostname(v) || 'Invalid hostname']"
|
||||||
:disabled="!(tempSettingsStruct.shouldManage && tempSettingsStruct.canManage)"
|
:disabled="
|
||||||
|
!tempSettingsStruct.shouldManage ||
|
||||||
|
!useSettingsStore().network.canManage ||
|
||||||
|
useSettingsStore().network.networkingDisabled
|
||||||
|
"
|
||||||
/>
|
/>
|
||||||
<v-divider class="pb-3" />
|
<v-divider class="pb-3" />
|
||||||
<span style="font-weight: 700">Advanced Networking</span>
|
<span style="font-weight: 700">Advanced Networking</span>
|
||||||
<pv-switch
|
<pv-switch
|
||||||
|
v-show="!useSettingsStore().network.networkingDisabled"
|
||||||
v-model="tempSettingsStruct.shouldManage"
|
v-model="tempSettingsStruct.shouldManage"
|
||||||
:disabled="!tempSettingsStruct.canManage"
|
:disabled="!useSettingsStore().network.canManage || useSettingsStore().network.networkingDisabled"
|
||||||
label="Manage Device Networking"
|
label="Manage Device Networking"
|
||||||
tooltip="If enabled, Photon will manage device hostname and network settings."
|
tooltip="If enabled, Photon will manage device hostname and network settings."
|
||||||
:label-cols="4"
|
:label-cols="4"
|
||||||
class="pt-2"
|
class="pt-2"
|
||||||
/>
|
/>
|
||||||
<pv-select
|
<pv-select
|
||||||
|
v-show="!useSettingsStore().network.networkingDisabled"
|
||||||
v-model="currentNetworkInterfaceIndex"
|
v-model="currentNetworkInterfaceIndex"
|
||||||
label="NetworkManager interface"
|
label="NetworkManager interface"
|
||||||
:disabled="!(tempSettingsStruct.shouldManage && tempSettingsStruct.canManage)"
|
:disabled="
|
||||||
|
!tempSettingsStruct.shouldManage ||
|
||||||
|
!useSettingsStore().network.canManage ||
|
||||||
|
useSettingsStore().network.networkingDisabled
|
||||||
|
"
|
||||||
:select-cols="12 - 4"
|
:select-cols="12 - 4"
|
||||||
tooltip="Name of the interface PhotonVision should manage the IP address of"
|
tooltip="Name of the interface PhotonVision should manage the IP address of"
|
||||||
:items="useSettingsStore().networkInterfaceNames"
|
:items="useSettingsStore().networkInterfaceNames"
|
||||||
@@ -206,7 +232,8 @@ watchEffect(() => {
|
|||||||
v-show="
|
v-show="
|
||||||
!useSettingsStore().networkInterfaceNames.length &&
|
!useSettingsStore().networkInterfaceNames.length &&
|
||||||
tempSettingsStruct.shouldManage &&
|
tempSettingsStruct.shouldManage &&
|
||||||
tempSettingsStruct.canManage
|
useSettingsStore().network.canManage &&
|
||||||
|
!useSettingsStore().network.networkingDisabled
|
||||||
"
|
"
|
||||||
rounded
|
rounded
|
||||||
color="red"
|
color="red"
|
||||||
@@ -231,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"
|
||||||
@@ -249,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"
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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 {
|
||||||
@@ -45,9 +46,14 @@ export interface NetworkSettings {
|
|||||||
setStaticCommand?: string;
|
setStaticCommand?: string;
|
||||||
setDHCPcommand?: string;
|
setDHCPcommand?: string;
|
||||||
networkInterfaceNames: NetworkInterfaceType[];
|
networkInterfaceNames: NetworkInterfaceType[];
|
||||||
|
networkingDisabled: boolean;
|
||||||
|
matchCamerasOnlyByPath: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ConfigurableNetworkSettings = Omit<NetworkSettings, "canManage" | "networkInterfaceNames">;
|
export type ConfigurableNetworkSettings = Omit<
|
||||||
|
NetworkSettings,
|
||||||
|
"canManage" | "networkInterfaceNames" | "networkingDisabled"
|
||||||
|
>;
|
||||||
|
|
||||||
export interface LightingSettings {
|
export interface LightingSettings {
|
||||||
supported: boolean;
|
supported: boolean;
|
||||||
@@ -134,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 {
|
||||||
@@ -251,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"],
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ export default defineConfig({
|
|||||||
css: {
|
css: {
|
||||||
preprocessorOptions: {
|
preprocessorOptions: {
|
||||||
sass: {
|
sass: {
|
||||||
additionalData: ['@import "@/assets/styles/variables.scss"', ""].join("\n")
|
additionalData: ["@import \"@/assets/styles/variables.scss\"", ""].join("\n")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|
||||||
|
|||||||
@@ -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="
|
||||||
|
|||||||
@@ -50,6 +50,10 @@ public class ConfigManager {
|
|||||||
private final Thread settingsSaveThread;
|
private final Thread settingsSaveThread;
|
||||||
private long saveRequestTimestamp = -1;
|
private long saveRequestTimestamp = -1;
|
||||||
|
|
||||||
|
// special case flag to disable flushing settings to disk at shutdown. Avoids the jvm shutdown
|
||||||
|
// hook overwriting the settings we just uploaded
|
||||||
|
private boolean flushOnShutdown = true;
|
||||||
|
|
||||||
enum ConfigSaveStrategy {
|
enum ConfigSaveStrategy {
|
||||||
SQL,
|
SQL,
|
||||||
LEGACY,
|
LEGACY,
|
||||||
@@ -303,4 +307,19 @@ public class ConfigManager {
|
|||||||
if (!ret.exists()) ret.mkdirs();
|
if (!ret.exists()) ret.mkdirs();
|
||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Disable flushing settings to disk as part of our JVM exit hook. Used to prevent uploading all
|
||||||
|
* settings from getting its new configs overwritten at program exit and before theyre all loaded.
|
||||||
|
*/
|
||||||
|
public void disableFlushOnShutdown() {
|
||||||
|
this.flushOnShutdown = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void onJvmExit() {
|
||||||
|
if (flushOnShutdown) {
|
||||||
|
logger.info("Force-flushing settings...");
|
||||||
|
saveToDisk();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 + "]";
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,12 +25,13 @@ import java.util.Map;
|
|||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
import org.photonvision.PhotonVersion;
|
import org.photonvision.PhotonVersion;
|
||||||
import org.photonvision.common.hardware.Platform;
|
import org.photonvision.common.hardware.Platform;
|
||||||
|
import org.photonvision.common.networking.NetworkManager;
|
||||||
import org.photonvision.common.networking.NetworkUtils;
|
import org.photonvision.common.networking.NetworkUtils;
|
||||||
import org.photonvision.common.util.SerializationUtils;
|
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;
|
||||||
@@ -121,16 +122,10 @@ public class PhotonConfiguration {
|
|||||||
// Hack active interfaces into networkSettings
|
// Hack active interfaces into networkSettings
|
||||||
var netConfigMap = networkConfig.toHashMap();
|
var netConfigMap = networkConfig.toHashMap();
|
||||||
netConfigMap.put("networkInterfaceNames", NetworkUtils.getAllWiredInterfaces());
|
netConfigMap.put("networkInterfaceNames", NetworkUtils.getAllWiredInterfaces());
|
||||||
|
netConfigMap.put("networkingDisabled", NetworkManager.getInstance().networkingIsDisabled);
|
||||||
|
|
||||||
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();
|
||||||
@@ -179,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;
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|
||||||
|
|||||||
@@ -145,8 +145,7 @@ public class HardwareManager {
|
|||||||
logger.info("Shutting down LEDs...");
|
logger.info("Shutting down LEDs...");
|
||||||
if (visionLED != null) visionLED.setState(false);
|
if (visionLED != null) visionLED.setState(false);
|
||||||
|
|
||||||
logger.info("Force-flushing settings...");
|
ConfigManager.getInstance().onJvmExit();
|
||||||
ConfigManager.getInstance().saveToDisk();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean restartDevice() {
|
public boolean restartDevice() {
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ import org.photonvision.common.hardware.metrics.cmds.CmdBase;
|
|||||||
import org.photonvision.common.hardware.metrics.cmds.FileCmds;
|
import org.photonvision.common.hardware.metrics.cmds.FileCmds;
|
||||||
import org.photonvision.common.hardware.metrics.cmds.LinuxCmds;
|
import org.photonvision.common.hardware.metrics.cmds.LinuxCmds;
|
||||||
import org.photonvision.common.hardware.metrics.cmds.PiCmds;
|
import org.photonvision.common.hardware.metrics.cmds.PiCmds;
|
||||||
|
import org.photonvision.common.hardware.metrics.cmds.RK3588Cmds;
|
||||||
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.common.util.ShellExec;
|
import org.photonvision.common.util.ShellExec;
|
||||||
@@ -44,6 +45,8 @@ public class MetricsManager {
|
|||||||
cmds = new FileCmds();
|
cmds = new FileCmds();
|
||||||
} else if (Platform.isRaspberryPi()) {
|
} else if (Platform.isRaspberryPi()) {
|
||||||
cmds = new PiCmds(); // Pi's can use a hardcoded command set
|
cmds = new PiCmds(); // Pi's can use a hardcoded command set
|
||||||
|
} else if (Platform.isRK3588()) {
|
||||||
|
cmds = new RK3588Cmds(); // RK3588 chipset hardcoded command set
|
||||||
} else if (Platform.isLinux()) {
|
} else if (Platform.isLinux()) {
|
||||||
cmds = new LinuxCmds(); // Linux/Unix platforms assume a nominal command set
|
cmds = new LinuxCmds(); // Linux/Unix platforms assume a nominal command set
|
||||||
} else {
|
} else {
|
||||||
@@ -89,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() {
|
||||||
@@ -125,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));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ import org.photonvision.common.configuration.HardwareConfig;
|
|||||||
public class LinuxCmds extends CmdBase {
|
public class LinuxCmds extends CmdBase {
|
||||||
public void initCmds(HardwareConfig config) {
|
public void initCmds(HardwareConfig config) {
|
||||||
// CPU
|
// CPU
|
||||||
cpuMemoryCommand = "free -m | awk 'FNR == 2 {print $3}'";
|
cpuMemoryCommand = "free -m | awk 'FNR == 2 {print $2}'";
|
||||||
|
|
||||||
// TODO: boards have lots of thermal devices. Hard to pick the CPU
|
// TODO: boards have lots of thermal devices. Hard to pick the CPU
|
||||||
|
|
||||||
|
|||||||
@@ -25,7 +25,6 @@ public class PiCmds extends LinuxCmds {
|
|||||||
super.initCmds(config);
|
super.initCmds(config);
|
||||||
|
|
||||||
// CPU
|
// CPU
|
||||||
cpuMemoryCommand = "free -m | awk 'FNR == 2 {print $2}'";
|
|
||||||
cpuTemperatureCommand = "sed 's/.\\{3\\}$/.&/' /sys/class/thermal/thermal_zone0/temp";
|
cpuTemperatureCommand = "sed 's/.\\{3\\}$/.&/' /sys/class/thermal/thermal_zone0/temp";
|
||||||
cpuThrottleReasonCmd =
|
cpuThrottleReasonCmd =
|
||||||
"if (( $(( $(vcgencmd get_throttled | grep -Eo 0x[0-9a-fA-F]*) & 0x01 )) != 0x00 )); then echo \"LOW VOLTAGE\"; "
|
"if (( $(( $(vcgencmd get_throttled | grep -Eo 0x[0-9a-fA-F]*) & 0x01 )) != 0x00 )); then echo \"LOW VOLTAGE\"; "
|
||||||
|
|||||||
@@ -0,0 +1,50 @@
|
|||||||
|
/*
|
||||||
|
* 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.common.hardware.metrics.cmds;
|
||||||
|
|
||||||
|
import org.photonvision.common.configuration.HardwareConfig;
|
||||||
|
|
||||||
|
public class RK3588Cmds extends LinuxCmds {
|
||||||
|
/** Applies pi-specific commands, ignoring any input configuration */
|
||||||
|
public void initCmds(HardwareConfig config) {
|
||||||
|
super.initCmds(config);
|
||||||
|
|
||||||
|
// CPU Temperature
|
||||||
|
/* The RK3588 chip has 7 thermal zones that can be accessed via:
|
||||||
|
* /sys/class/thermal/thermal_zoneX/temp
|
||||||
|
* where X is an interger from 0 to 6.
|
||||||
|
*
|
||||||
|
* || Zone || Location || Comments ||
|
||||||
|
* | 0 | soc | soc thermal (near the center of the chip) |
|
||||||
|
* | 1 | bigcore0 | CPU Big Core A76_0/1 (CPU4 and CPU5) |
|
||||||
|
* | 2 | bigcore1 | CPU Big Core A76_2/3 (CPU6 and CPU7) |
|
||||||
|
* | 3 | littlecore | CPU Small Core A55_0/1/2/3 (CPU0, CPU1, CPU2, and CPU3) |
|
||||||
|
* | 4 | center | also called PD_CENTER |
|
||||||
|
* | 5 | gpu | GPU |
|
||||||
|
* | 6 | npu | NPU |
|
||||||
|
*
|
||||||
|
* Sources:
|
||||||
|
* - http://forum.armsom.org/t/topic/51/3
|
||||||
|
* - https://lore.kernel.org/lkml/7276280.TLKafQO6qx@archbook/
|
||||||
|
*/
|
||||||
|
cpuTemperatureCommand =
|
||||||
|
"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/ *$//'";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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()));
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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()
|
||||||
|
+ "]";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
+ "]";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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'");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
|||||||
@@ -47,6 +47,16 @@ public class Contour implements Releasable {
|
|||||||
this.mat = mat;
|
this.mat = mat;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Contour(Rect2d box) {
|
||||||
|
// no easy way to convert a Rect2d to Mat, diy it. Order is tl tr br bl
|
||||||
|
this.mat =
|
||||||
|
new MatOfPoint(
|
||||||
|
box.tl(),
|
||||||
|
new Point(box.x + box.width, box.y),
|
||||||
|
box.br(),
|
||||||
|
new Point(box.x, box.y + box.height));
|
||||||
|
}
|
||||||
|
|
||||||
public MatOfPoint2f getMat2f() {
|
public MatOfPoint2f getMat2f() {
|
||||||
if (mat2f == null) {
|
if (mat2f == null) {
|
||||||
mat2f = new MatOfPoint2f(mat.toArray());
|
mat2f = new MatOfPoint2f(mat.toArray());
|
||||||
|
|||||||
@@ -25,15 +25,15 @@ public enum ContourSortMode {
|
|||||||
Comparator.comparingDouble(PotentialTarget::getArea)
|
Comparator.comparingDouble(PotentialTarget::getArea)
|
||||||
.reversed()), // reversed so that zero index has the largest size
|
.reversed()), // reversed so that zero index has the largest size
|
||||||
Smallest(Largest.getComparator().reversed()),
|
Smallest(Largest.getComparator().reversed()),
|
||||||
Highest(Comparator.comparingDouble(rect -> rect.getMinAreaRect().center.y)),
|
Highest(Comparator.comparingDouble(tgt -> tgt.getMinAreaRect().center.y)),
|
||||||
Lowest(Highest.getComparator().reversed()),
|
Lowest(Highest.getComparator().reversed()),
|
||||||
Leftmost(Comparator.comparingDouble(target -> target.getMinAreaRect().center.x * -1)),
|
Leftmost(Comparator.comparingDouble(tgt -> tgt.getMinAreaRect().center.x * -1)),
|
||||||
Rightmost(Leftmost.getComparator().reversed()),
|
Rightmost(Leftmost.getComparator().reversed()),
|
||||||
Centermost(
|
Centermost(
|
||||||
Comparator.comparingDouble(
|
Comparator.comparingDouble(
|
||||||
rect ->
|
tgt ->
|
||||||
(Math.pow(rect.getMinAreaRect().center.y, 2)
|
(Math.pow(tgt.getMinAreaRect().center.y, 2)
|
||||||
+ Math.pow(rect.getMinAreaRect().center.x, 2))));
|
+ Math.pow(tgt.getMinAreaRect().center.x, 2))));
|
||||||
|
|
||||||
private final Comparator<PotentialTarget> m_comparator;
|
private final Comparator<PotentialTarget> m_comparator;
|
||||||
|
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import java.util.List;
|
|||||||
import org.apache.commons.lang3.tuple.Pair;
|
import org.apache.commons.lang3.tuple.Pair;
|
||||||
import org.opencv.core.Mat;
|
import org.opencv.core.Mat;
|
||||||
import org.opencv.core.Point;
|
import org.opencv.core.Point;
|
||||||
|
import org.opencv.core.Scalar;
|
||||||
import org.opencv.imgproc.Imgproc;
|
import org.opencv.imgproc.Imgproc;
|
||||||
import org.photonvision.common.util.ColorHelper;
|
import org.photonvision.common.util.ColorHelper;
|
||||||
import org.photonvision.vision.frame.FrameDivisor;
|
import org.photonvision.vision.frame.FrameDivisor;
|
||||||
@@ -31,22 +32,44 @@ import org.photonvision.vision.target.TrackedTarget;
|
|||||||
public class DrawCalibrationPipe
|
public class DrawCalibrationPipe
|
||||||
extends MutatingPipe<
|
extends MutatingPipe<
|
||||||
Pair<Mat, List<TrackedTarget>>, DrawCalibrationPipe.DrawCalibrationPipeParams> {
|
Pair<Mat, List<TrackedTarget>>, DrawCalibrationPipe.DrawCalibrationPipeParams> {
|
||||||
|
Scalar[] chessboardColors =
|
||||||
|
new Scalar[] {
|
||||||
|
ColorHelper.colorToScalar(Color.RED, 0.4),
|
||||||
|
ColorHelper.colorToScalar(Color.ORANGE, 0.4),
|
||||||
|
ColorHelper.colorToScalar(Color.GREEN, 0.4),
|
||||||
|
ColorHelper.colorToScalar(Color.BLUE, 0.4),
|
||||||
|
ColorHelper.colorToScalar(Color.MAGENTA, 0.4),
|
||||||
|
};
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected Void process(Pair<Mat, List<TrackedTarget>> in) {
|
protected Void process(Pair<Mat, List<TrackedTarget>> in) {
|
||||||
var image = in.getLeft();
|
var image = in.getLeft();
|
||||||
|
|
||||||
|
var imgSz = image.size();
|
||||||
|
var diag = Math.hypot(imgSz.width, imgSz.height);
|
||||||
|
|
||||||
|
// heuristic: about 4px at a diagonal of 750px, or .5%, 'looks good'. keep it at least 3px at
|
||||||
|
// worst tho
|
||||||
|
int r = (int) Math.max(diag * 4.0 / 750.0, 3);
|
||||||
|
int thickness = (int) Math.max(diag * 1.0 / 600.0, 1);
|
||||||
|
|
||||||
|
int i = 0;
|
||||||
for (var target : in.getRight()) {
|
for (var target : in.getRight()) {
|
||||||
for (var c : target.getTargetCorners()) {
|
for (var c : target.getTargetCorners()) {
|
||||||
c =
|
c =
|
||||||
new Point(
|
new Point(
|
||||||
c.x / params.divisor.value.doubleValue(), c.y / params.divisor.value.doubleValue());
|
c.x / params.divisor.value.doubleValue(), c.y / params.divisor.value.doubleValue());
|
||||||
var r = 4;
|
|
||||||
var r2 = r / Math.sqrt(2);
|
var r2 = r / Math.sqrt(2);
|
||||||
var color = ColorHelper.colorToScalar(Color.RED, 0.4);
|
var color = chessboardColors[i % chessboardColors.length];
|
||||||
Imgproc.circle(image, c, r, color, 1);
|
Imgproc.circle(image, c, r, color, thickness);
|
||||||
Imgproc.line(image, new Point(c.x - r2, c.y - r2), new Point(c.x + r2, c.y + r2), color);
|
Imgproc.line(
|
||||||
Imgproc.line(image, new Point(c.x + r2, c.y - r2), new Point(c.x - r2, c.y + r2), color);
|
image, new Point(c.x - r2, c.y - r2), new Point(c.x + r2, c.y + r2), color, thickness);
|
||||||
|
Imgproc.line(
|
||||||
|
image, new Point(c.x + r2, c.y - r2), new Point(c.x - r2, c.y + r2), color, thickness);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
i++;
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@@ -0,0 +1,89 @@
|
|||||||
|
/*
|
||||||
|
* 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.pipe.impl;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import org.photonvision.common.util.numbers.DoubleCouple;
|
||||||
|
import org.photonvision.vision.frame.FrameStaticProperties;
|
||||||
|
import org.photonvision.vision.pipe.CVPipe;
|
||||||
|
|
||||||
|
public class FilterObjectDetectionsPipe
|
||||||
|
extends CVPipe<
|
||||||
|
List<NeuralNetworkPipeResult>,
|
||||||
|
List<NeuralNetworkPipeResult>,
|
||||||
|
FilterObjectDetectionsPipe.FilterContoursParams> {
|
||||||
|
List<NeuralNetworkPipeResult> m_filteredContours = new ArrayList<>();
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected List<NeuralNetworkPipeResult> process(List<NeuralNetworkPipeResult> in) {
|
||||||
|
m_filteredContours.clear();
|
||||||
|
for (var contour : in) {
|
||||||
|
filterContour(contour);
|
||||||
|
}
|
||||||
|
|
||||||
|
return m_filteredContours;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void filterContour(NeuralNetworkPipeResult contour) {
|
||||||
|
var boc = contour.box;
|
||||||
|
|
||||||
|
// Area filtering
|
||||||
|
double areaPercentage = boc.area() / params.getFrameStaticProperties().imageArea * 100.0;
|
||||||
|
double minAreaPercentage = params.getArea().getFirst();
|
||||||
|
double maxAreaPercentage = params.getArea().getSecond();
|
||||||
|
if (areaPercentage < minAreaPercentage || areaPercentage > maxAreaPercentage) return;
|
||||||
|
|
||||||
|
// Aspect ratio filtering; much simpler since always axis-aligned
|
||||||
|
double aspectRatio = boc.width / boc.height;
|
||||||
|
if (aspectRatio < params.getRatio().getFirst() || aspectRatio > params.getRatio().getSecond())
|
||||||
|
return;
|
||||||
|
|
||||||
|
m_filteredContours.add(contour);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class FilterContoursParams {
|
||||||
|
private final DoubleCouple m_area;
|
||||||
|
private final DoubleCouple m_ratio;
|
||||||
|
private final FrameStaticProperties m_frameStaticProperties;
|
||||||
|
public final boolean isLandscape;
|
||||||
|
|
||||||
|
public FilterContoursParams(
|
||||||
|
DoubleCouple area,
|
||||||
|
DoubleCouple ratio,
|
||||||
|
FrameStaticProperties camProperties,
|
||||||
|
boolean isLandscape) {
|
||||||
|
this.m_area = area;
|
||||||
|
this.m_ratio = ratio;
|
||||||
|
this.m_frameStaticProperties = camProperties;
|
||||||
|
this.isLandscape = isLandscape;
|
||||||
|
}
|
||||||
|
|
||||||
|
public DoubleCouple getArea() {
|
||||||
|
return m_area;
|
||||||
|
}
|
||||||
|
|
||||||
|
public DoubleCouple getRatio() {
|
||||||
|
return m_ratio;
|
||||||
|
}
|
||||||
|
|
||||||
|
public FrameStaticProperties getFrameStaticProperties() {
|
||||||
|
return m_frameStaticProperties;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ public class SortContoursPipe
|
|||||||
if (params.getSortMode() != ContourSortMode.Centermost) {
|
if (params.getSortMode() != ContourSortMode.Centermost) {
|
||||||
m_sortedContours.sort(params.getSortMode().getComparator());
|
m_sortedContours.sort(params.getSortMode().getComparator());
|
||||||
} else {
|
} else {
|
||||||
|
// we need knowledge of camera properties to calculate this distance -- do it ourselves
|
||||||
m_sortedContours.sort(Comparator.comparingDouble(this::calcSquareCenterDistance));
|
m_sortedContours.sort(Comparator.comparingDouble(this::calcSquareCenterDistance));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -50,10 +51,10 @@ public class SortContoursPipe
|
|||||||
m_sortedContours.subList(0, Math.min(in.size(), params.getMaxTargets())));
|
m_sortedContours.subList(0, Math.min(in.size(), params.getMaxTargets())));
|
||||||
}
|
}
|
||||||
|
|
||||||
private double calcSquareCenterDistance(PotentialTarget rect) {
|
private double calcSquareCenterDistance(PotentialTarget tgt) {
|
||||||
return Math.sqrt(
|
return Math.sqrt(
|
||||||
Math.pow(params.getCamProperties().centerX - rect.getMinAreaRect().center.x, 2)
|
Math.pow(params.getCamProperties().centerX - tgt.getMinAreaRect().center.x, 2)
|
||||||
+ Math.pow(params.getCamProperties().centerY - rect.getMinAreaRect().center.y, 2));
|
+ Math.pow(params.getCamProperties().centerY - tgt.getMinAreaRect().center.y, 2));
|
||||||
}
|
}
|
||||||
|
|
||||||
public static class SortContoursParams {
|
public static class SortContoursParams {
|
||||||
|
|||||||
@@ -26,6 +26,8 @@ import org.photonvision.vision.pipeline.result.CVPipelineResult;
|
|||||||
|
|
||||||
public abstract class CVPipeline<R extends CVPipelineResult, S extends CVPipelineSettings>
|
public abstract class CVPipeline<R extends CVPipelineResult, S extends CVPipelineSettings>
|
||||||
implements Releasable {
|
implements Releasable {
|
||||||
|
static final int MAX_MULTI_TARGET_RESULTS = 10;
|
||||||
|
|
||||||
protected S settings;
|
protected S settings;
|
||||||
protected FrameStaticProperties frameStaticProperties;
|
protected FrameStaticProperties frameStaticProperties;
|
||||||
protected QuirkyCamera cameraQuirks;
|
protected QuirkyCamera cameraQuirks;
|
||||||
|
|||||||
@@ -109,7 +109,7 @@ public class ColoredShapePipeline
|
|||||||
SortContoursPipe.SortContoursParams sortContoursParams =
|
SortContoursPipe.SortContoursParams sortContoursParams =
|
||||||
new SortContoursPipe.SortContoursParams(
|
new SortContoursPipe.SortContoursParams(
|
||||||
settings.contourSortMode,
|
settings.contourSortMode,
|
||||||
settings.outputShowMultipleTargets ? 5 : 1,
|
settings.outputShowMultipleTargets ? MAX_MULTI_TARGET_RESULTS : 1,
|
||||||
frameStaticProperties); // TODO don't hardcode?
|
frameStaticProperties); // TODO don't hardcode?
|
||||||
sortContoursPipe.setParams(sortContoursParams);
|
sortContoursPipe.setParams(sortContoursParams);
|
||||||
|
|
||||||
|
|||||||
@@ -17,21 +17,26 @@
|
|||||||
|
|
||||||
package org.photonvision.vision.pipeline;
|
package org.photonvision.vision.pipeline;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
import org.photonvision.vision.frame.Frame;
|
import org.photonvision.vision.frame.Frame;
|
||||||
import org.photonvision.vision.frame.FrameThresholdType;
|
import org.photonvision.vision.frame.FrameThresholdType;
|
||||||
|
import org.photonvision.vision.opencv.DualOffsetValues;
|
||||||
import org.photonvision.vision.pipe.CVPipe.CVPipeResult;
|
import org.photonvision.vision.pipe.CVPipe.CVPipeResult;
|
||||||
import org.photonvision.vision.pipe.impl.*;
|
import org.photonvision.vision.pipe.impl.*;
|
||||||
import org.photonvision.vision.pipe.impl.RknnDetectionPipe.RknnDetectionPipeParams;
|
import org.photonvision.vision.pipe.impl.RknnDetectionPipe.RknnDetectionPipeParams;
|
||||||
import org.photonvision.vision.pipeline.result.CVPipelineResult;
|
import org.photonvision.vision.pipeline.result.CVPipelineResult;
|
||||||
|
import org.photonvision.vision.target.PotentialTarget;
|
||||||
|
import org.photonvision.vision.target.TargetOrientation;
|
||||||
import org.photonvision.vision.target.TrackedTarget;
|
import org.photonvision.vision.target.TrackedTarget;
|
||||||
import org.photonvision.vision.target.TrackedTarget.TargetCalculationParameters;
|
|
||||||
|
|
||||||
public class ObjectDetectionPipeline
|
public class ObjectDetectionPipeline
|
||||||
extends CVPipeline<CVPipelineResult, ObjectDetectionPipelineSettings> {
|
extends CVPipeline<CVPipelineResult, ObjectDetectionPipelineSettings> {
|
||||||
private final CalculateFPSPipe calculateFPSPipe = new CalculateFPSPipe();
|
private final CalculateFPSPipe calculateFPSPipe = new CalculateFPSPipe();
|
||||||
private final RknnDetectionPipe rknnPipe = new RknnDetectionPipe();
|
private final RknnDetectionPipe rknnPipe = new RknnDetectionPipe();
|
||||||
|
private final SortContoursPipe sortContoursPipe = new SortContoursPipe();
|
||||||
|
private final Collect2dTargetsPipe collect2dTargetsPipe = new Collect2dTargetsPipe();
|
||||||
|
private final FilterObjectDetectionsPipe filterContoursPipe = new FilterObjectDetectionsPipe();
|
||||||
|
|
||||||
private static final FrameThresholdType PROCESSING_TYPE = FrameThresholdType.NONE;
|
private static final FrameThresholdType PROCESSING_TYPE = FrameThresholdType.NONE;
|
||||||
|
|
||||||
@@ -52,6 +57,38 @@ public class ObjectDetectionPipeline
|
|||||||
params.confidence = settings.confidence;
|
params.confidence = settings.confidence;
|
||||||
params.nms = settings.nms;
|
params.nms = settings.nms;
|
||||||
rknnPipe.setParams(params);
|
rknnPipe.setParams(params);
|
||||||
|
|
||||||
|
DualOffsetValues dualOffsetValues =
|
||||||
|
new DualOffsetValues(
|
||||||
|
settings.offsetDualPointA,
|
||||||
|
settings.offsetDualPointAArea,
|
||||||
|
settings.offsetDualPointB,
|
||||||
|
settings.offsetDualPointBArea);
|
||||||
|
|
||||||
|
SortContoursPipe.SortContoursParams sortContoursParams =
|
||||||
|
new SortContoursPipe.SortContoursParams(
|
||||||
|
settings.contourSortMode,
|
||||||
|
settings.outputShowMultipleTargets ? MAX_MULTI_TARGET_RESULTS : 1,
|
||||||
|
frameStaticProperties);
|
||||||
|
sortContoursPipe.setParams(sortContoursParams);
|
||||||
|
|
||||||
|
var filterContoursParams =
|
||||||
|
new FilterObjectDetectionsPipe.FilterContoursParams(
|
||||||
|
settings.contourArea,
|
||||||
|
settings.contourRatio,
|
||||||
|
frameStaticProperties,
|
||||||
|
settings.contourTargetOrientation == TargetOrientation.Landscape);
|
||||||
|
filterContoursPipe.setParams(filterContoursParams);
|
||||||
|
|
||||||
|
Collect2dTargetsPipe.Collect2dTargetsParams collect2dTargetsParams =
|
||||||
|
new Collect2dTargetsPipe.Collect2dTargetsParams(
|
||||||
|
settings.offsetRobotOffsetMode,
|
||||||
|
settings.offsetSinglePoint,
|
||||||
|
dualOffsetValues,
|
||||||
|
settings.contourTargetOffsetPointEdge,
|
||||||
|
settings.contourTargetOrientation,
|
||||||
|
frameStaticProperties);
|
||||||
|
collect2dTargetsPipe.setParams(collect2dTargetsParams);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -60,31 +97,35 @@ public class ObjectDetectionPipeline
|
|||||||
|
|
||||||
// ***************** change based on backend ***********************
|
// ***************** change based on backend ***********************
|
||||||
|
|
||||||
CVPipeResult<List<NeuralNetworkPipeResult>> ret = rknnPipe.run(input_frame.colorImage);
|
CVPipeResult<List<NeuralNetworkPipeResult>> rknnResult = rknnPipe.run(input_frame.colorImage);
|
||||||
sumPipeNanosElapsed += ret.nanosElapsed;
|
sumPipeNanosElapsed += rknnResult.nanosElapsed;
|
||||||
List<NeuralNetworkPipeResult> targetList;
|
List<NeuralNetworkPipeResult> targetList;
|
||||||
|
|
||||||
targetList = ret.output;
|
|
||||||
var names = rknnPipe.getClassNames();
|
var names = rknnPipe.getClassNames();
|
||||||
|
|
||||||
input_frame.colorImage.getMat().copyTo(input_frame.processedImage.getMat());
|
input_frame.colorImage.getMat().copyTo(input_frame.processedImage.getMat());
|
||||||
|
|
||||||
// ***************** change based on backend ***********************
|
// ***************** change based on backend ***********************
|
||||||
|
|
||||||
List<TrackedTarget> targets = new ArrayList<>();
|
var filterContoursResult = filterContoursPipe.run(rknnResult.output);
|
||||||
|
sumPipeNanosElapsed += filterContoursResult.nanosElapsed;
|
||||||
|
|
||||||
for (var t : targetList) {
|
CVPipeResult<List<PotentialTarget>> sortContoursResult =
|
||||||
targets.add(
|
sortContoursPipe.run(
|
||||||
new TrackedTarget(
|
filterContoursResult.output.stream()
|
||||||
t,
|
.map(shape -> new PotentialTarget(shape))
|
||||||
new TargetCalculationParameters(
|
.collect(Collectors.toList()));
|
||||||
false, null, null, null, null, frameStaticProperties)));
|
sumPipeNanosElapsed += sortContoursResult.nanosElapsed;
|
||||||
}
|
|
||||||
|
CVPipeResult<List<TrackedTarget>> collect2dTargetsResult =
|
||||||
|
collect2dTargetsPipe.run(sortContoursResult.output);
|
||||||
|
sumPipeNanosElapsed += collect2dTargetsResult.nanosElapsed;
|
||||||
|
|
||||||
var fpsResult = calculateFPSPipe.run(null);
|
var fpsResult = calculateFPSPipe.run(null);
|
||||||
var fps = fpsResult.output;
|
var fps = fpsResult.output;
|
||||||
|
|
||||||
return new CVPipelineResult(sumPipeNanosElapsed, fps, targets, input_frame, names);
|
return new CVPipelineResult(
|
||||||
|
sumPipeNanosElapsed, fps, collect2dTargetsResult.output, input_frame, names);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@@ -64,29 +64,6 @@ public class ReflectivePipeline extends CVPipeline<CVPipelineResult, ReflectiveP
|
|||||||
settings.offsetDualPointB,
|
settings.offsetDualPointB,
|
||||||
settings.offsetDualPointBArea);
|
settings.offsetDualPointBArea);
|
||||||
|
|
||||||
// var rotateImageParams = new
|
|
||||||
// RotateImagePipe.RotateImageParams(settings.inputImageRotationMode);
|
|
||||||
// rotateImagePipe.setParams(rotateImageParams);
|
|
||||||
|
|
||||||
// if (cameraQuirks.hasQuirk(CameraQuirk.PiCam) && LibCameraJNI.isSupported()) {
|
|
||||||
// LibCameraJNI.setThresholds(
|
|
||||||
// settings.hsvHue.getFirst() / 180d,
|
|
||||||
// settings.hsvSaturation.getFirst() / 255d,
|
|
||||||
// settings.hsvValue.getFirst() / 255d,
|
|
||||||
// settings.hsvHue.getSecond() / 180d,
|
|
||||||
// settings.hsvSaturation.getSecond() / 255d,
|
|
||||||
// settings.hsvValue.getSecond() / 255d);
|
|
||||||
// // LibCameraJNI.setInvertHue(settings.hueInverted);
|
|
||||||
// LibCameraJNI.setRotation(settings.inputImageRotationMode.value);
|
|
||||||
// // LibCameraJNI.setShouldCopyColor(settings.inputShouldShow);
|
|
||||||
// } else {
|
|
||||||
// var hsvParams =
|
|
||||||
// new HSVPipe.HSVParams(
|
|
||||||
// settings.hsvHue, settings.hsvSaturation, settings.hsvValue,
|
|
||||||
// settings.hueInverted);
|
|
||||||
// hsvPipe.setParams(hsvParams);
|
|
||||||
// }
|
|
||||||
|
|
||||||
var findContoursParams = new FindContoursPipe.FindContoursParams();
|
var findContoursParams = new FindContoursPipe.FindContoursParams();
|
||||||
findContoursPipe.setParams(findContoursParams);
|
findContoursPipe.setParams(findContoursParams);
|
||||||
|
|
||||||
@@ -113,7 +90,7 @@ public class ReflectivePipeline extends CVPipeline<CVPipelineResult, ReflectiveP
|
|||||||
var sortContoursParams =
|
var sortContoursParams =
|
||||||
new SortContoursPipe.SortContoursParams(
|
new SortContoursPipe.SortContoursParams(
|
||||||
settings.contourSortMode,
|
settings.contourSortMode,
|
||||||
settings.outputShowMultipleTargets ? 8 : 1, // TODO don't hardcode?
|
settings.outputShowMultipleTargets ? MAX_MULTI_TARGET_RESULTS : 1,
|
||||||
frameStaticProperties);
|
frameStaticProperties);
|
||||||
sortContoursPipe.setParams(sortContoursParams);
|
sortContoursPipe.setParams(sortContoursParams);
|
||||||
|
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -98,8 +98,7 @@ public class VisionRunner {
|
|||||||
var pipelineResult = pipeline.run(frame, cameraQuirks);
|
var pipelineResult = pipeline.run(frame, cameraQuirks);
|
||||||
pipelineResultConsumer.accept(pipelineResult);
|
pipelineResultConsumer.accept(pipelineResult);
|
||||||
} catch (Exception ex) {
|
} catch (Exception ex) {
|
||||||
logger.error("Exception on loop " + loopCount);
|
logger.error("Exception on loop " + loopCount, ex);
|
||||||
ex.printStackTrace();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
loopCount++;
|
loopCount++;
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|
||||||
|
|||||||
@@ -21,7 +21,9 @@ import java.util.List;
|
|||||||
import org.opencv.core.RotatedRect;
|
import org.opencv.core.RotatedRect;
|
||||||
import org.photonvision.vision.opencv.CVShape;
|
import org.photonvision.vision.opencv.CVShape;
|
||||||
import org.photonvision.vision.opencv.Contour;
|
import org.photonvision.vision.opencv.Contour;
|
||||||
|
import org.photonvision.vision.opencv.ContourShape;
|
||||||
import org.photonvision.vision.opencv.Releasable;
|
import org.photonvision.vision.opencv.Releasable;
|
||||||
|
import org.photonvision.vision.pipe.impl.NeuralNetworkPipeResult;
|
||||||
|
|
||||||
public class PotentialTarget implements Releasable {
|
public class PotentialTarget implements Releasable {
|
||||||
|
|
||||||
@@ -29,6 +31,10 @@ public class PotentialTarget implements Releasable {
|
|||||||
public final List<Contour> m_subContours;
|
public final List<Contour> m_subContours;
|
||||||
public final CVShape shape;
|
public final CVShape shape;
|
||||||
|
|
||||||
|
// additional metadata about object detections we need to keep around
|
||||||
|
public final double confidence;
|
||||||
|
public final int clsId;
|
||||||
|
|
||||||
public PotentialTarget(Contour inputContour) {
|
public PotentialTarget(Contour inputContour) {
|
||||||
this(inputContour, List.of());
|
this(inputContour, List.of());
|
||||||
}
|
}
|
||||||
@@ -41,12 +47,26 @@ public class PotentialTarget implements Releasable {
|
|||||||
m_mainContour = inputContour;
|
m_mainContour = inputContour;
|
||||||
m_subContours = new ArrayList<>(subContours);
|
m_subContours = new ArrayList<>(subContours);
|
||||||
this.shape = shape;
|
this.shape = shape;
|
||||||
|
this.clsId = -1;
|
||||||
|
this.confidence = -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
public PotentialTarget(Contour inputContour, CVShape shape) {
|
public PotentialTarget(Contour inputContour, CVShape shape) {
|
||||||
this(inputContour, List.of(), shape);
|
this(inputContour, List.of(), shape);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public PotentialTarget(NeuralNetworkPipeResult det) {
|
||||||
|
this.shape = new CVShape(new Contour(det.box), ContourShape.Quadrilateral);
|
||||||
|
this.m_mainContour = this.shape.getContour();
|
||||||
|
m_subContours = List.of();
|
||||||
|
this.clsId = det.classIdx;
|
||||||
|
this.confidence = det.confidence;
|
||||||
|
}
|
||||||
|
|
||||||
|
public PotentialTarget(CVShape cvShape) {
|
||||||
|
this(cvShape.getContour(), cvShape);
|
||||||
|
}
|
||||||
|
|
||||||
public RotatedRect getMinAreaRect() {
|
public RotatedRect getMinAreaRect() {
|
||||||
return m_mainContour.getMinAreaRect();
|
return m_mainContour.getMinAreaRect();
|
||||||
}
|
}
|
||||||
@@ -61,7 +81,7 @@ public class PotentialTarget implements Releasable {
|
|||||||
for (var sc : m_subContours) {
|
for (var sc : m_subContours) {
|
||||||
sc.release();
|
sc.release();
|
||||||
}
|
}
|
||||||
m_subContours.clear();
|
if (!m_subContours.isEmpty()) m_subContours.clear();
|
||||||
if (shape != null) shape.release();
|
if (shape != null) shape.release();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,7 +27,6 @@ import org.opencv.core.Mat;
|
|||||||
import org.opencv.core.MatOfPoint;
|
import org.opencv.core.MatOfPoint;
|
||||||
import org.opencv.core.MatOfPoint2f;
|
import org.opencv.core.MatOfPoint2f;
|
||||||
import org.opencv.core.Point;
|
import org.opencv.core.Point;
|
||||||
import org.opencv.core.Rect2d;
|
|
||||||
import org.opencv.core.RotatedRect;
|
import org.opencv.core.RotatedRect;
|
||||||
import org.photonvision.common.util.SerializationUtils;
|
import org.photonvision.common.util.SerializationUtils;
|
||||||
import org.photonvision.common.util.math.MathUtils;
|
import org.photonvision.common.util.math.MathUtils;
|
||||||
@@ -39,7 +38,6 @@ import org.photonvision.vision.opencv.CVShape;
|
|||||||
import org.photonvision.vision.opencv.Contour;
|
import org.photonvision.vision.opencv.Contour;
|
||||||
import org.photonvision.vision.opencv.DualOffsetValues;
|
import org.photonvision.vision.opencv.DualOffsetValues;
|
||||||
import org.photonvision.vision.opencv.Releasable;
|
import org.photonvision.vision.opencv.Releasable;
|
||||||
import org.photonvision.vision.pipe.impl.NeuralNetworkPipeResult;
|
|
||||||
|
|
||||||
public class TrackedTarget implements Releasable {
|
public class TrackedTarget implements Releasable {
|
||||||
public final Contour m_mainContour;
|
public final Contour m_mainContour;
|
||||||
@@ -76,6 +74,9 @@ public class TrackedTarget implements Releasable {
|
|||||||
this.m_subContours = origTarget.m_subContours;
|
this.m_subContours = origTarget.m_subContours;
|
||||||
this.m_shape = shape;
|
this.m_shape = shape;
|
||||||
calculateValues(params);
|
calculateValues(params);
|
||||||
|
|
||||||
|
this.m_classId = origTarget.clsId;
|
||||||
|
this.m_confidence = origTarget.confidence;
|
||||||
}
|
}
|
||||||
|
|
||||||
public TrackedTarget(
|
public TrackedTarget(
|
||||||
@@ -159,47 +160,6 @@ public class TrackedTarget implements Releasable {
|
|||||||
m_robotOffsetPoint = new Point();
|
m_robotOffsetPoint = new Point();
|
||||||
}
|
}
|
||||||
|
|
||||||
public TrackedTarget(
|
|
||||||
Rect2d box, int class_id, double confidence, TargetCalculationParameters params) {
|
|
||||||
m_targetOffsetPoint = new Point(box.x + box.width / 2.0, box.y + box.height / 2.0);
|
|
||||||
m_robotOffsetPoint = new Point();
|
|
||||||
|
|
||||||
var yawPitch =
|
|
||||||
TargetCalculations.calculateYawPitch(
|
|
||||||
params.cameraCenterPoint.x,
|
|
||||||
box.x + box.width / 2.0,
|
|
||||||
params.horizontalFocalLength,
|
|
||||||
params.cameraCenterPoint.y,
|
|
||||||
box.y + box.height / 2.0,
|
|
||||||
params.verticalFocalLength);
|
|
||||||
m_yaw = yawPitch.getFirst();
|
|
||||||
m_pitch = yawPitch.getSecond();
|
|
||||||
|
|
||||||
Point[] cornerPoints =
|
|
||||||
new Point[] {
|
|
||||||
// Box.x/y is the top-left corner, not the center
|
|
||||||
new Point(box.x, box.y), // tl
|
|
||||||
new Point(box.x + box.width, box.y), // tr
|
|
||||||
new Point(box.x + box.width, box.y + box.height), // br
|
|
||||||
new Point(box.x, box.y + box.height), // bl
|
|
||||||
};
|
|
||||||
|
|
||||||
m_targetCorners = List.of(cornerPoints);
|
|
||||||
MatOfPoint contourMat = new MatOfPoint(cornerPoints);
|
|
||||||
m_approximateBoundingPolygon = new MatOfPoint2f(cornerPoints);
|
|
||||||
|
|
||||||
m_mainContour = new Contour(contourMat);
|
|
||||||
m_area = m_mainContour.getArea() / params.imageArea * 100;
|
|
||||||
|
|
||||||
m_classId = class_id;
|
|
||||||
m_confidence = confidence;
|
|
||||||
}
|
|
||||||
|
|
||||||
public TrackedTarget(
|
|
||||||
NeuralNetworkPipeResult t, TargetCalculationParameters targetCalculationParameters) {
|
|
||||||
this(t.box, t.classIdx, t.confidence, targetCalculationParameters);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return Returns the confidence of the detection ranging from 0 - 1.
|
* @return Returns the confidence of the detection ranging from 0 - 1.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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(),
|
||||||
|
|||||||
@@ -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());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
26
photon-lib/py/photonlibpy/estimatedRobotPose.py
Normal file
26
photon-lib/py/photonlibpy/estimatedRobotPose.py
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
from dataclasses import dataclass
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
|
from wpimath.geometry import Pose3d
|
||||||
|
|
||||||
|
from .photonTrackedTarget import PhotonTrackedTarget
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from .photonPoseEstimator import PoseStrategy
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class EstimatedRobotPose:
|
||||||
|
"""An estimated pose based on pipeline result"""
|
||||||
|
|
||||||
|
estimatedPose: Pose3d
|
||||||
|
"""The estimated pose"""
|
||||||
|
|
||||||
|
timestampSeconds: float
|
||||||
|
"""The estimated time the frame used to derive the robot pose was taken"""
|
||||||
|
|
||||||
|
targetsUsed: list[PhotonTrackedTarget]
|
||||||
|
"""A list of the targets used to compute this pose"""
|
||||||
|
|
||||||
|
strategy: "PoseStrategy"
|
||||||
|
"""The strategy actually used to produce this pose"""
|
||||||
@@ -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]
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -17,7 +17,6 @@ class PhotonPipelineResult:
|
|||||||
self.latencyMillis = packet.decodeDouble()
|
self.latencyMillis = packet.decodeDouble()
|
||||||
targetCount = packet.decode8()
|
targetCount = packet.decode8()
|
||||||
|
|
||||||
print(f"targetCount = {targetCount}")
|
|
||||||
for _ in range(targetCount):
|
for _ in range(targetCount):
|
||||||
target = PhotonTrackedTarget()
|
target = PhotonTrackedTarget()
|
||||||
target.createFromPacket(packet)
|
target.createFromPacket(packet)
|
||||||
@@ -39,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
|
||||||
|
|||||||
321
photon-lib/py/photonlibpy/photonPoseEstimator.py
Normal file
321
photon-lib/py/photonlibpy/photonPoseEstimator.py
Normal file
@@ -0,0 +1,321 @@
|
|||||||
|
import enum
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
import wpilib
|
||||||
|
from robotpy_apriltag import AprilTagFieldLayout
|
||||||
|
from wpimath.geometry import Transform3d, Pose3d, Pose2d
|
||||||
|
|
||||||
|
from .photonPipelineResult import PhotonPipelineResult
|
||||||
|
from .photonCamera import PhotonCamera
|
||||||
|
from .estimatedRobotPose import EstimatedRobotPose
|
||||||
|
|
||||||
|
|
||||||
|
class PoseStrategy(enum.Enum):
|
||||||
|
"""
|
||||||
|
Position estimation strategies that can be used by the PhotonPoseEstimator class.
|
||||||
|
"""
|
||||||
|
|
||||||
|
LOWEST_AMBIGUITY = enum.auto()
|
||||||
|
"""Choose the Pose with the lowest ambiguity."""
|
||||||
|
|
||||||
|
CLOSEST_TO_CAMERA_HEIGHT = enum.auto()
|
||||||
|
"""Choose the Pose which is closest to the camera height."""
|
||||||
|
|
||||||
|
CLOSEST_TO_REFERENCE_POSE = enum.auto()
|
||||||
|
"""Choose the Pose which is closest to a set Reference position."""
|
||||||
|
|
||||||
|
CLOSEST_TO_LAST_POSE = enum.auto()
|
||||||
|
"""Choose the Pose which is closest to the last pose calculated."""
|
||||||
|
|
||||||
|
AVERAGE_BEST_TARGETS = enum.auto()
|
||||||
|
"""Return the average of the best target poses using ambiguity as weight."""
|
||||||
|
|
||||||
|
MULTI_TAG_PNP_ON_COPROCESSOR = enum.auto()
|
||||||
|
"""
|
||||||
|
Use all visible tags to compute a single pose estimate on coprocessor.
|
||||||
|
This option needs to be enabled on the PhotonVision web UI as well.
|
||||||
|
"""
|
||||||
|
|
||||||
|
MULTI_TAG_PNP_ON_RIO = enum.auto()
|
||||||
|
"""
|
||||||
|
Use all visible tags to compute a single pose estimate.
|
||||||
|
This runs on the RoboRIO, and can take a lot of time.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
class PhotonPoseEstimator:
|
||||||
|
"""
|
||||||
|
The PhotonPoseEstimator class filters or combines readings from all the AprilTags visible at a
|
||||||
|
given timestamp on the field to produce a single robot in field pose, using the strategy set
|
||||||
|
below. Example usage can be found in our apriltagExample example project.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
fieldTags: AprilTagFieldLayout,
|
||||||
|
strategy: PoseStrategy,
|
||||||
|
camera: PhotonCamera,
|
||||||
|
robotToCamera: Transform3d,
|
||||||
|
):
|
||||||
|
"""Create a new PhotonPoseEstimator.
|
||||||
|
|
||||||
|
:param fieldTags: A WPILib AprilTagFieldLayout linking AprilTag IDs to Pose3d objects
|
||||||
|
with respect to the FIRST field using the Field Coordinate System.
|
||||||
|
Note that setting the origin of this layout object will affect the
|
||||||
|
results from this class.
|
||||||
|
:param strategy: The strategy it should use to determine the best pose.
|
||||||
|
:param camera: PhotonCamera
|
||||||
|
:param robotToCamera: Transform3d from the center of the robot to the camera mount position (i.e.,
|
||||||
|
robot ➔ camera) in the Robot Coordinate System.
|
||||||
|
"""
|
||||||
|
self._fieldTags = fieldTags
|
||||||
|
self._primaryStrategy = strategy
|
||||||
|
self._camera = camera
|
||||||
|
self.robotToCamera = robotToCamera
|
||||||
|
|
||||||
|
self._multiTagFallbackStrategy = PoseStrategy.LOWEST_AMBIGUITY
|
||||||
|
self._reportedErrors: set[int] = set()
|
||||||
|
self._poseCacheTimestampSeconds = -1.0
|
||||||
|
self._lastPose: Optional[Pose3d] = None
|
||||||
|
self._referencePose: Optional[Pose3d] = None
|
||||||
|
|
||||||
|
# TODO: Implement HAL reporting
|
||||||
|
|
||||||
|
@property
|
||||||
|
def fieldTags(self) -> AprilTagFieldLayout:
|
||||||
|
"""Get the AprilTagFieldLayout being used by the PositionEstimator.
|
||||||
|
|
||||||
|
Note: Setting the origin of this layout will affect the results from this class.
|
||||||
|
|
||||||
|
:returns: the AprilTagFieldLayout
|
||||||
|
"""
|
||||||
|
return self._fieldTags
|
||||||
|
|
||||||
|
@fieldTags.setter
|
||||||
|
def fieldTags(self, fieldTags: AprilTagFieldLayout):
|
||||||
|
"""Set the AprilTagFieldLayout being used by the PositionEstimator.
|
||||||
|
|
||||||
|
Note: Setting the origin of this layout will affect the results from this class.
|
||||||
|
|
||||||
|
:param fieldTags: the AprilTagFieldLayout
|
||||||
|
"""
|
||||||
|
self._checkUpdate(self._fieldTags, fieldTags)
|
||||||
|
self._fieldTags = fieldTags
|
||||||
|
|
||||||
|
@property
|
||||||
|
def primaryStrategy(self) -> PoseStrategy:
|
||||||
|
"""Get the Position Estimation Strategy being used by the Position Estimator.
|
||||||
|
|
||||||
|
:returns: the strategy
|
||||||
|
"""
|
||||||
|
return self._primaryStrategy
|
||||||
|
|
||||||
|
@primaryStrategy.setter
|
||||||
|
def primaryStrategy(self, strategy: PoseStrategy):
|
||||||
|
"""Set the Position Estimation Strategy used by the Position Estimator.
|
||||||
|
|
||||||
|
:param strategy: the strategy to set
|
||||||
|
"""
|
||||||
|
self._checkUpdate(self._primaryStrategy, strategy)
|
||||||
|
self._primaryStrategy = strategy
|
||||||
|
|
||||||
|
@property
|
||||||
|
def multiTagFallbackStrategy(self) -> PoseStrategy:
|
||||||
|
return self._multiTagFallbackStrategy
|
||||||
|
|
||||||
|
@multiTagFallbackStrategy.setter
|
||||||
|
def multiTagFallbackStrategy(self, strategy: PoseStrategy):
|
||||||
|
"""Set the Position Estimation Strategy used in multi-tag mode when only one tag can be seen. Must
|
||||||
|
NOT be MULTI_TAG_PNP
|
||||||
|
|
||||||
|
:param strategy: the strategy to set
|
||||||
|
"""
|
||||||
|
self._checkUpdate(self._multiTagFallbackStrategy, strategy)
|
||||||
|
if (
|
||||||
|
strategy is PoseStrategy.MULTI_TAG_PNP_ON_COPROCESSOR
|
||||||
|
or strategy is PoseStrategy.MULTI_TAG_PNP_ON_RIO
|
||||||
|
):
|
||||||
|
wpilib.reportWarning(
|
||||||
|
"Fallback cannot be set to MULTI_TAG_PNP! Setting to lowest ambiguity",
|
||||||
|
False,
|
||||||
|
)
|
||||||
|
strategy = PoseStrategy.LOWEST_AMBIGUITY
|
||||||
|
self._multiTagFallbackStrategy = strategy
|
||||||
|
|
||||||
|
@property
|
||||||
|
def referencePose(self) -> Optional[Pose3d]:
|
||||||
|
"""Return the reference position that is being used by the estimator.
|
||||||
|
|
||||||
|
:returns: the referencePose
|
||||||
|
"""
|
||||||
|
return self._referencePose
|
||||||
|
|
||||||
|
@referencePose.setter
|
||||||
|
def referencePose(self, referencePose: Pose3d | Pose2d):
|
||||||
|
"""Update the stored reference pose for use when using the **CLOSEST_TO_REFERENCE_POSE**
|
||||||
|
strategy.
|
||||||
|
|
||||||
|
:param referencePose: the referencePose to set
|
||||||
|
"""
|
||||||
|
if isinstance(referencePose, Pose2d):
|
||||||
|
referencePose = Pose3d(referencePose)
|
||||||
|
self._checkUpdate(self._referencePose, referencePose)
|
||||||
|
self._referencePose = referencePose
|
||||||
|
|
||||||
|
@property
|
||||||
|
def lastPose(self) -> Optional[Pose3d]:
|
||||||
|
return self._lastPose
|
||||||
|
|
||||||
|
@lastPose.setter
|
||||||
|
def lastPose(self, lastPose: Pose3d | Pose2d):
|
||||||
|
"""Update the stored last pose. Useful for setting the initial estimate when using the
|
||||||
|
**CLOSEST_TO_LAST_POSE** strategy.
|
||||||
|
|
||||||
|
:param lastPose: the lastPose to set
|
||||||
|
"""
|
||||||
|
if isinstance(lastPose, Pose2d):
|
||||||
|
lastPose = Pose3d(lastPose)
|
||||||
|
self._checkUpdate(self._lastPose, lastPose)
|
||||||
|
self._lastPose = lastPose
|
||||||
|
|
||||||
|
def _invalidatePoseCache(self) -> None:
|
||||||
|
self._poseCacheTimestampSeconds = -1.0
|
||||||
|
|
||||||
|
def _checkUpdate(self, oldObj, newObj) -> None:
|
||||||
|
if oldObj != newObj and oldObj is not None and oldObj is not newObj:
|
||||||
|
self._invalidatePoseCache()
|
||||||
|
|
||||||
|
def update(
|
||||||
|
self, cameraResult: Optional[PhotonPipelineResult] = None
|
||||||
|
) -> Optional[EstimatedRobotPose]:
|
||||||
|
"""
|
||||||
|
Updates the estimated position of the robot. Returns empty if:
|
||||||
|
|
||||||
|
- The timestamp of the provided pipeline result is the same as in the previous call to
|
||||||
|
``update()``.
|
||||||
|
|
||||||
|
- No targets were found in the pipeline results.
|
||||||
|
|
||||||
|
:param cameraResult: The latest pipeline result from the camera
|
||||||
|
|
||||||
|
:returns: an :class:`EstimatedRobotPose` with an estimated pose, timestamp, and targets used to
|
||||||
|
create the estimate.
|
||||||
|
"""
|
||||||
|
if not cameraResult:
|
||||||
|
if not self._camera:
|
||||||
|
wpilib.reportError("[PhotonPoseEstimator] Missing camera!", False)
|
||||||
|
return None
|
||||||
|
cameraResult = self._camera.getLatestResult()
|
||||||
|
|
||||||
|
if cameraResult.timestampSec < 0:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# If the pose cache timestamp was set, and the result is from the same
|
||||||
|
# timestamp, return an
|
||||||
|
# empty result
|
||||||
|
if (
|
||||||
|
self._poseCacheTimestampSeconds > 0.0
|
||||||
|
and abs(self._poseCacheTimestampSeconds - cameraResult.timestampSec) < 1e-6
|
||||||
|
):
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Remember the timestamp of the current result used
|
||||||
|
self._poseCacheTimestampSeconds = cameraResult.timestampSec
|
||||||
|
|
||||||
|
# If no targets seen, trivial case -- return empty result
|
||||||
|
if not cameraResult.targets:
|
||||||
|
return None
|
||||||
|
|
||||||
|
return self._update(cameraResult, self._primaryStrategy)
|
||||||
|
|
||||||
|
def _update(
|
||||||
|
self, cameraResult: PhotonPipelineResult, strat: PoseStrategy
|
||||||
|
) -> Optional[EstimatedRobotPose]:
|
||||||
|
if strat is PoseStrategy.LOWEST_AMBIGUITY:
|
||||||
|
estimatedPose = self._lowestAmbiguityStrategy(cameraResult)
|
||||||
|
elif strat is PoseStrategy.MULTI_TAG_PNP_ON_COPROCESSOR:
|
||||||
|
estimatedPose = self._multiTagOnCoprocStrategy(cameraResult)
|
||||||
|
else:
|
||||||
|
wpilib.reportError(
|
||||||
|
"[PhotonPoseEstimator] Unknown Position Estimation Strategy!", False
|
||||||
|
)
|
||||||
|
return None
|
||||||
|
|
||||||
|
if not estimatedPose:
|
||||||
|
self._lastPose = None
|
||||||
|
|
||||||
|
return estimatedPose
|
||||||
|
|
||||||
|
def _multiTagOnCoprocStrategy(
|
||||||
|
self, result: PhotonPipelineResult
|
||||||
|
) -> Optional[EstimatedRobotPose]:
|
||||||
|
if result.multiTagResult.estimatedPose.isPresent:
|
||||||
|
best_tf = result.multiTagResult.estimatedPose.best
|
||||||
|
best = (
|
||||||
|
Pose3d()
|
||||||
|
.transformBy(best_tf) # field-to-camera
|
||||||
|
.relativeTo(self._fieldTags.getOrigin())
|
||||||
|
.transformBy(self.robotToCamera.inverse()) # field-to-robot
|
||||||
|
)
|
||||||
|
return EstimatedRobotPose(
|
||||||
|
best,
|
||||||
|
result.timestampSec,
|
||||||
|
result.targets,
|
||||||
|
PoseStrategy.MULTI_TAG_PNP_ON_COPROCESSOR,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
return self._update(result, self._multiTagFallbackStrategy)
|
||||||
|
|
||||||
|
def _lowestAmbiguityStrategy(
|
||||||
|
self, result: PhotonPipelineResult
|
||||||
|
) -> Optional[EstimatedRobotPose]:
|
||||||
|
"""
|
||||||
|
Return the estimated position of the robot with the lowest position ambiguity from a List of
|
||||||
|
pipeline results.
|
||||||
|
|
||||||
|
:param result: pipeline result
|
||||||
|
|
||||||
|
:returns: the estimated position of the robot in the FCS and the estimated timestamp of this
|
||||||
|
estimation.
|
||||||
|
"""
|
||||||
|
lowestAmbiguityTarget = None
|
||||||
|
|
||||||
|
lowestAmbiguityScore = 10.0
|
||||||
|
|
||||||
|
for target in result.targets:
|
||||||
|
targetPoseAmbiguity = target.poseAmbiguity
|
||||||
|
|
||||||
|
# Make sure the target is a Fiducial target.
|
||||||
|
if targetPoseAmbiguity != -1 and targetPoseAmbiguity < lowestAmbiguityScore:
|
||||||
|
lowestAmbiguityScore = targetPoseAmbiguity
|
||||||
|
lowestAmbiguityTarget = target
|
||||||
|
|
||||||
|
# Although there are confirmed to be targets, none of them may be fiducial
|
||||||
|
# targets.
|
||||||
|
if not lowestAmbiguityTarget:
|
||||||
|
return None
|
||||||
|
|
||||||
|
targetFiducialId = lowestAmbiguityTarget.fiducialId
|
||||||
|
|
||||||
|
targetPosition = self._fieldTags.getTagPose(targetFiducialId)
|
||||||
|
|
||||||
|
if not targetPosition:
|
||||||
|
self._reportFiducialPoseError(targetFiducialId)
|
||||||
|
return None
|
||||||
|
|
||||||
|
return EstimatedRobotPose(
|
||||||
|
targetPosition.transformBy(
|
||||||
|
lowestAmbiguityTarget.getBestCameraToTarget().inverse()
|
||||||
|
).transformBy(self.robotToCamera.inverse()),
|
||||||
|
result.timestampSec,
|
||||||
|
result.targets,
|
||||||
|
PoseStrategy.LOWEST_AMBIGUITY,
|
||||||
|
)
|
||||||
|
|
||||||
|
def _reportFiducialPoseError(self, fiducialId: int) -> None:
|
||||||
|
if fiducialId not in self._reportedErrors:
|
||||||
|
wpilib.reportError(
|
||||||
|
f"[PhotonPoseEstimator] Tried to get pose of unknown AprilTag: {fiducialId}",
|
||||||
|
False,
|
||||||
|
)
|
||||||
|
self._reportedErrors.add(fiducialId)
|
||||||
@@ -60,6 +60,7 @@ setup(
|
|||||||
install_requires=[
|
install_requires=[
|
||||||
"wpilib<2025,>=2024.0.0b2",
|
"wpilib<2025,>=2024.0.0b2",
|
||||||
"robotpy-wpimath<2025,>=2024.0.0b2",
|
"robotpy-wpimath<2025,>=2024.0.0b2",
|
||||||
|
"robotpy-apriltag<2025,>=2024.0.0b2",
|
||||||
"pyntcore<2025,>=2024.0.0b2",
|
"pyntcore<2025,>=2024.0.0b2",
|
||||||
],
|
],
|
||||||
description=descriptionStr,
|
description=descriptionStr,
|
||||||
|
|||||||
243
photon-lib/py/test/photonPoseEstimator_test.py
Normal file
243
photon-lib/py/test/photonPoseEstimator_test.py
Normal file
@@ -0,0 +1,243 @@
|
|||||||
|
from photonlibpy.multiTargetPNPResult import MultiTargetPNPResult, PNPResult
|
||||||
|
from photonlibpy.photonPipelineResult import PhotonPipelineResult
|
||||||
|
from photonlibpy.photonPoseEstimator import PhotonPoseEstimator, PoseStrategy
|
||||||
|
from photonlibpy.photonTrackedTarget import PhotonTrackedTarget, TargetCorner
|
||||||
|
from robotpy_apriltag import AprilTag, AprilTagFieldLayout
|
||||||
|
from wpimath.geometry import Pose3d, Rotation3d, Transform3d, Translation3d
|
||||||
|
|
||||||
|
|
||||||
|
class PhotonCameraInjector:
|
||||||
|
result: PhotonPipelineResult
|
||||||
|
|
||||||
|
def getLatestResult(self) -> PhotonPipelineResult:
|
||||||
|
return self.result
|
||||||
|
|
||||||
|
|
||||||
|
def setupCommon() -> AprilTagFieldLayout:
|
||||||
|
tagList = []
|
||||||
|
tagPoses = (
|
||||||
|
Pose3d(3, 3, 3, Rotation3d()),
|
||||||
|
Pose3d(5, 5, 5, Rotation3d()),
|
||||||
|
)
|
||||||
|
for id_, pose in enumerate(tagPoses):
|
||||||
|
aprilTag = AprilTag()
|
||||||
|
aprilTag.ID = id_
|
||||||
|
aprilTag.pose = pose
|
||||||
|
tagList.append(aprilTag)
|
||||||
|
|
||||||
|
fieldLength = 54 / 3.281 # 54 ft -> meters
|
||||||
|
fieldWidth = 27 / 3.281 # 24 ft -> meters
|
||||||
|
|
||||||
|
return AprilTagFieldLayout(tagList, fieldLength, fieldWidth)
|
||||||
|
|
||||||
|
|
||||||
|
def test_lowestAmbiguityStrategy():
|
||||||
|
aprilTags = setupCommon()
|
||||||
|
|
||||||
|
cameraOne = PhotonCameraInjector()
|
||||||
|
cameraOne.result = PhotonPipelineResult(
|
||||||
|
2,
|
||||||
|
11,
|
||||||
|
[
|
||||||
|
PhotonTrackedTarget(
|
||||||
|
3.0,
|
||||||
|
-4.0,
|
||||||
|
9.0,
|
||||||
|
4.0,
|
||||||
|
0,
|
||||||
|
Transform3d(Translation3d(1, 2, 3), Rotation3d(1, 2, 3)),
|
||||||
|
Transform3d(Translation3d(1, 2, 3), Rotation3d(1, 2, 3)),
|
||||||
|
[
|
||||||
|
TargetCorner(1, 2),
|
||||||
|
TargetCorner(3, 4),
|
||||||
|
TargetCorner(5, 6),
|
||||||
|
TargetCorner(7, 8),
|
||||||
|
],
|
||||||
|
[
|
||||||
|
TargetCorner(1, 2),
|
||||||
|
TargetCorner(3, 4),
|
||||||
|
TargetCorner(5, 6),
|
||||||
|
TargetCorner(7, 8),
|
||||||
|
],
|
||||||
|
0.7,
|
||||||
|
),
|
||||||
|
PhotonTrackedTarget(
|
||||||
|
3.0,
|
||||||
|
-4.0,
|
||||||
|
9.1,
|
||||||
|
6.7,
|
||||||
|
1,
|
||||||
|
Transform3d(Translation3d(4, 2, 3), Rotation3d(0, 0, 0)),
|
||||||
|
Transform3d(Translation3d(4, 2, 3), Rotation3d(1, 5, 3)),
|
||||||
|
[
|
||||||
|
TargetCorner(1, 2),
|
||||||
|
TargetCorner(3, 4),
|
||||||
|
TargetCorner(5, 6),
|
||||||
|
TargetCorner(7, 8),
|
||||||
|
],
|
||||||
|
[
|
||||||
|
TargetCorner(1, 2),
|
||||||
|
TargetCorner(3, 4),
|
||||||
|
TargetCorner(5, 6),
|
||||||
|
TargetCorner(7, 8),
|
||||||
|
],
|
||||||
|
0.3,
|
||||||
|
),
|
||||||
|
PhotonTrackedTarget(
|
||||||
|
9.0,
|
||||||
|
-2.0,
|
||||||
|
19.0,
|
||||||
|
3.0,
|
||||||
|
0,
|
||||||
|
Transform3d(Translation3d(1, 2, 3), Rotation3d(1, 2, 3)),
|
||||||
|
Transform3d(Translation3d(1, 2, 3), Rotation3d(1, 2, 3)),
|
||||||
|
[
|
||||||
|
TargetCorner(1, 2),
|
||||||
|
TargetCorner(3, 4),
|
||||||
|
TargetCorner(5, 6),
|
||||||
|
TargetCorner(7, 8),
|
||||||
|
],
|
||||||
|
[
|
||||||
|
TargetCorner(1, 2),
|
||||||
|
TargetCorner(3, 4),
|
||||||
|
TargetCorner(5, 6),
|
||||||
|
TargetCorner(7, 8),
|
||||||
|
],
|
||||||
|
0.4,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
estimator = PhotonPoseEstimator(
|
||||||
|
aprilTags, PoseStrategy.LOWEST_AMBIGUITY, cameraOne, Transform3d()
|
||||||
|
)
|
||||||
|
|
||||||
|
estimatedPose = estimator.update()
|
||||||
|
pose = estimatedPose.estimatedPose
|
||||||
|
|
||||||
|
assertEquals(11, estimatedPose.timestampSeconds)
|
||||||
|
assertEquals(1, pose.x, 0.01)
|
||||||
|
assertEquals(3, pose.y, 0.01)
|
||||||
|
assertEquals(2, pose.z, 0.01)
|
||||||
|
|
||||||
|
|
||||||
|
def test_multiTagOnCoprocStrategy():
|
||||||
|
cameraOne = PhotonCameraInjector()
|
||||||
|
cameraOne.result = PhotonPipelineResult(
|
||||||
|
2,
|
||||||
|
11,
|
||||||
|
# There needs to be at least one target present for pose estimation to work
|
||||||
|
# Doesn't matter which/how many targets for this test
|
||||||
|
[
|
||||||
|
PhotonTrackedTarget(
|
||||||
|
3.0,
|
||||||
|
-4.0,
|
||||||
|
9.0,
|
||||||
|
4.0,
|
||||||
|
0,
|
||||||
|
Transform3d(Translation3d(1, 2, 3), Rotation3d(1, 2, 3)),
|
||||||
|
Transform3d(Translation3d(1, 2, 3), Rotation3d(1, 2, 3)),
|
||||||
|
[
|
||||||
|
TargetCorner(1, 2),
|
||||||
|
TargetCorner(3, 4),
|
||||||
|
TargetCorner(5, 6),
|
||||||
|
TargetCorner(7, 8),
|
||||||
|
],
|
||||||
|
[
|
||||||
|
TargetCorner(1, 2),
|
||||||
|
TargetCorner(3, 4),
|
||||||
|
TargetCorner(5, 6),
|
||||||
|
TargetCorner(7, 8),
|
||||||
|
],
|
||||||
|
0.7,
|
||||||
|
)
|
||||||
|
],
|
||||||
|
multiTagResult=MultiTargetPNPResult(
|
||||||
|
PNPResult(True, Transform3d(1, 3, 2, Rotation3d()))
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
estimator = PhotonPoseEstimator(
|
||||||
|
AprilTagFieldLayout(),
|
||||||
|
PoseStrategy.MULTI_TAG_PNP_ON_COPROCESSOR,
|
||||||
|
cameraOne,
|
||||||
|
Transform3d(),
|
||||||
|
)
|
||||||
|
|
||||||
|
estimatedPose = estimator.update()
|
||||||
|
pose = estimatedPose.estimatedPose
|
||||||
|
|
||||||
|
assertEquals(11, estimatedPose.timestampSeconds)
|
||||||
|
assertEquals(1, pose.x, 0.01)
|
||||||
|
assertEquals(3, pose.y, 0.01)
|
||||||
|
assertEquals(2, pose.z, 0.01)
|
||||||
|
|
||||||
|
|
||||||
|
def test_cacheIsInvalidated():
|
||||||
|
aprilTags = setupCommon()
|
||||||
|
|
||||||
|
cameraOne = PhotonCameraInjector()
|
||||||
|
result = PhotonPipelineResult(
|
||||||
|
2,
|
||||||
|
20,
|
||||||
|
[
|
||||||
|
PhotonTrackedTarget(
|
||||||
|
3.0,
|
||||||
|
-4.0,
|
||||||
|
9.0,
|
||||||
|
4.0,
|
||||||
|
0,
|
||||||
|
Transform3d(Translation3d(1, 2, 3), Rotation3d(1, 2, 3)),
|
||||||
|
Transform3d(Translation3d(1, 2, 3), Rotation3d(1, 2, 3)),
|
||||||
|
[
|
||||||
|
TargetCorner(1, 2),
|
||||||
|
TargetCorner(3, 4),
|
||||||
|
TargetCorner(5, 6),
|
||||||
|
TargetCorner(7, 8),
|
||||||
|
],
|
||||||
|
[
|
||||||
|
TargetCorner(1, 2),
|
||||||
|
TargetCorner(3, 4),
|
||||||
|
TargetCorner(5, 6),
|
||||||
|
TargetCorner(7, 8),
|
||||||
|
],
|
||||||
|
0.7,
|
||||||
|
)
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
estimator = PhotonPoseEstimator(
|
||||||
|
aprilTags, PoseStrategy.LOWEST_AMBIGUITY, cameraOne, Transform3d()
|
||||||
|
)
|
||||||
|
|
||||||
|
# Empty result, expect empty result
|
||||||
|
cameraOne.result = PhotonPipelineResult(timestampSec=1)
|
||||||
|
estimatedPose = estimator.update()
|
||||||
|
assert estimatedPose is None
|
||||||
|
|
||||||
|
# Set actual result
|
||||||
|
cameraOne.result = result
|
||||||
|
estimatedPose = estimator.update()
|
||||||
|
assert estimatedPose is not None
|
||||||
|
assertEquals(20, estimatedPose.timestampSeconds, 0.01)
|
||||||
|
assertEquals(20, estimator._poseCacheTimestampSeconds)
|
||||||
|
|
||||||
|
# And again -- pose cache should mean this is empty
|
||||||
|
cameraOne.result = result
|
||||||
|
estimatedPose = estimator.update()
|
||||||
|
assert estimatedPose is None
|
||||||
|
# Expect the old timestamp to still be here
|
||||||
|
assertEquals(20, estimator._poseCacheTimestampSeconds)
|
||||||
|
|
||||||
|
# Set new field layout -- right after, the pose cache timestamp should be -1
|
||||||
|
estimator.fieldTags = AprilTagFieldLayout([AprilTag()], 0, 0)
|
||||||
|
assertEquals(-1, estimator._poseCacheTimestampSeconds)
|
||||||
|
# Update should cache the current timestamp (20) again
|
||||||
|
cameraOne.result = result
|
||||||
|
estimatedPose = estimator.update()
|
||||||
|
assertEquals(20, estimatedPose.timestampSeconds, 0.01)
|
||||||
|
assertEquals(20, estimator._poseCacheTimestampSeconds)
|
||||||
|
|
||||||
|
|
||||||
|
def assertEquals(expected, actual, epsilon=0.0):
|
||||||
|
assert abs(expected - actual) <= epsilon
|
||||||
@@ -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;
|
||||||
|
|||||||
@@ -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()),
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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});
|
||||||
@@ -435,7 +435,7 @@ class PhotonCameraSim {
|
|||||||
double minTargetAreaPercent;
|
double minTargetAreaPercent;
|
||||||
|
|
||||||
frc::AprilTagFieldLayout tagLayout{
|
frc::AprilTagFieldLayout tagLayout{
|
||||||
frc::LoadAprilTagLayoutField(frc::AprilTagField::k2023ChargedUp)};
|
frc::LoadAprilTagLayoutField(frc::AprilTagField::k2024Crescendo)};
|
||||||
|
|
||||||
cs::CvSource videoSimRaw;
|
cs::CvSource videoSimRaw;
|
||||||
cv::Mat videoSimFrameRaw{};
|
cv::Mat videoSimFrameRaw{};
|
||||||
|
|||||||
@@ -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());
|
||||||
|
|||||||
@@ -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>(),
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -94,6 +97,7 @@ public class RequestHandler {
|
|||||||
ctx.status(200);
|
ctx.status(200);
|
||||||
ctx.result("Successfully saved the uploaded settings zip, rebooting...");
|
ctx.result("Successfully saved the uploaded settings zip, rebooting...");
|
||||||
logger.info("Successfully saved the uploaded settings zip, rebooting...");
|
logger.info("Successfully saved the uploaded settings zip, rebooting...");
|
||||||
|
ConfigManager.getInstance().disableFlushOnShutdown();
|
||||||
restartProgram();
|
restartProgram();
|
||||||
} else {
|
} else {
|
||||||
ctx.status(500);
|
ctx.status(500);
|
||||||
@@ -579,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();
|
||||||
|
|||||||
@@ -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);
|
||||||
@@ -138,7 +140,6 @@ public class Server {
|
|||||||
app.post("/api/calibration/importFromData", RequestHandler::onDataCalibrationImportRequest);
|
app.post("/api/calibration/importFromData", RequestHandler::onDataCalibrationImportRequest);
|
||||||
|
|
||||||
app.start(port);
|
app.start(port);
|
||||||
System.out.println("hi");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -37,12 +37,13 @@ bool PhotonPipelineResult::operator==(const PhotonPipelineResult& other) const {
|
|||||||
|
|
||||||
Packet& operator<<(Packet& packet, const PhotonPipelineResult& result) {
|
Packet& operator<<(Packet& packet, const PhotonPipelineResult& result) {
|
||||||
// Encode latency and number of targets.
|
// Encode latency and number of targets.
|
||||||
packet << result.latency.value() << result.multitagResult
|
packet << result.latency.value()
|
||||||
<< static_cast<int8_t>(result.targets.size());
|
<< static_cast<int8_t>(result.targets.size());
|
||||||
|
|
||||||
// Encode the information of each target.
|
// Encode the information of each target.
|
||||||
for (auto& target : result.targets) packet << target;
|
for (auto& target : result.targets) packet << target;
|
||||||
|
|
||||||
|
packet << result.multitagResult;
|
||||||
// Return the packet
|
// Return the packet
|
||||||
return packet;
|
return packet;
|
||||||
}
|
}
|
||||||
@@ -51,7 +52,7 @@ Packet& operator>>(Packet& packet, PhotonPipelineResult& result) {
|
|||||||
// Decode latency, existence of targets, and number of targets.
|
// Decode latency, existence of targets, and number of targets.
|
||||||
double latencyMillis = 0;
|
double latencyMillis = 0;
|
||||||
int8_t targetCount = 0;
|
int8_t targetCount = 0;
|
||||||
packet >> latencyMillis >> result.multitagResult >> targetCount;
|
packet >> latencyMillis >> targetCount;
|
||||||
result.latency = units::millisecond_t(latencyMillis);
|
result.latency = units::millisecond_t(latencyMillis);
|
||||||
|
|
||||||
result.targets.clear();
|
result.targets.clear();
|
||||||
@@ -62,6 +63,8 @@ Packet& operator>>(Packet& packet, PhotonPipelineResult& result) {
|
|||||||
packet >> target;
|
packet >> target;
|
||||||
result.targets.push_back(target);
|
result.targets.push_back(target);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
packet >> result.multitagResult;
|
||||||
return packet;
|
return packet;
|
||||||
}
|
}
|
||||||
} // namespace photon
|
} // namespace photon
|
||||||
|
|||||||
@@ -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"
|
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"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"
|
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"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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};
|
||||||
|
|
||||||
|
|||||||
@@ -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"
|
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"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
plugins {
|
plugins {
|
||||||
id "com.diffplug.spotless" version "6.1.2"
|
id "com.diffplug.spotless" version "6.1.2"
|
||||||
id "edu.wpi.first.GradleRIO" version "2024.1.1" apply false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
allprojects {
|
allprojects {
|
||||||
|
|||||||
@@ -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"
|
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"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"
|
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.1.1"
|
wpi.versions.wpilibVersion = "2024.3.1"
|
||||||
wpi.versions.wpimathVersion = "2024.1.1"
|
wpi.versions.wpimathVersion = "2024.3.1"
|
||||||
|
|
||||||
apply from: "${rootDir}/../shared/examples_common.gradle"
|
apply from: "${rootDir}/../shared/examples_common.gradle"
|
||||||
|
|
||||||
|
|||||||
@@ -35,48 +35,48 @@
|
|||||||
|
|
||||||
namespace constants {
|
namespace constants {
|
||||||
namespace Vision {
|
namespace Vision {
|
||||||
static constexpr std::string_view kCameraName{"YOUR CAMERA NAME"};
|
inline constexpr std::string_view kCameraName{"YOUR CAMERA NAME"};
|
||||||
static const frc::Transform3d kRobotToCam{
|
inline const frc::Transform3d kRobotToCam{
|
||||||
frc::Translation3d{0.5_m, 0.0_m, 0.5_m},
|
frc::Translation3d{0.5_m, 0.0_m, 0.5_m},
|
||||||
frc::Rotation3d{0_rad, 0_rad, 0_rad}};
|
frc::Rotation3d{0_rad, 0_rad, 0_rad}};
|
||||||
static const frc::AprilTagFieldLayout kTagLayout{
|
inline const frc::AprilTagFieldLayout kTagLayout{
|
||||||
frc::LoadAprilTagLayoutField(frc::AprilTagField::k2023ChargedUp)};
|
frc::LoadAprilTagLayoutField(frc::AprilTagField::k2024Crescendo)};
|
||||||
|
|
||||||
static const Eigen::Matrix<double, 3, 1> kSingleTagStdDevs{4, 4, 8};
|
inline const Eigen::Matrix<double, 3, 1> kSingleTagStdDevs{4, 4, 8};
|
||||||
static const Eigen::Matrix<double, 3, 1> kMultiTagStdDevs{0.5, 0.5, 1};
|
inline const Eigen::Matrix<double, 3, 1> kMultiTagStdDevs{0.5, 0.5, 1};
|
||||||
} // namespace Vision
|
} // namespace Vision
|
||||||
namespace Swerve {
|
namespace Swerve {
|
||||||
|
|
||||||
static constexpr units::meter_t kTrackWidth{18.5_in};
|
inline constexpr units::meter_t kTrackWidth{18.5_in};
|
||||||
static constexpr units::meter_t kTrackLength{18.5_in};
|
inline constexpr units::meter_t kTrackLength{18.5_in};
|
||||||
static constexpr units::meter_t kRobotWidth{25_in + 3.25_in * 2};
|
inline constexpr units::meter_t kRobotWidth{25_in + 3.25_in * 2};
|
||||||
static constexpr units::meter_t kRobotLength{25_in + 3.25_in * 2};
|
inline constexpr units::meter_t kRobotLength{25_in + 3.25_in * 2};
|
||||||
static constexpr units::meters_per_second_t kMaxLinearSpeed{15.5_fps};
|
inline constexpr units::meters_per_second_t kMaxLinearSpeed{15.5_fps};
|
||||||
static constexpr units::radians_per_second_t kMaxAngularSpeed{720_deg_per_s};
|
inline constexpr units::radians_per_second_t kMaxAngularSpeed{720_deg_per_s};
|
||||||
static constexpr units::meter_t kWheelDiameter{4_in};
|
inline constexpr units::meter_t kWheelDiameter{4_in};
|
||||||
static constexpr units::meter_t kWheelCircumference{kWheelDiameter *
|
inline constexpr units::meter_t kWheelCircumference{kWheelDiameter *
|
||||||
std::numbers::pi};
|
std::numbers::pi};
|
||||||
|
|
||||||
static constexpr double kDriveGearRatio = 6.75;
|
inline constexpr double kDriveGearRatio = 6.75;
|
||||||
static constexpr double kSteerGearRatio = 12.8;
|
inline constexpr double kSteerGearRatio = 12.8;
|
||||||
|
|
||||||
static constexpr units::meter_t kDriveDistPerPulse =
|
inline constexpr units::meter_t kDriveDistPerPulse =
|
||||||
kWheelCircumference / 1024.0 / kDriveGearRatio;
|
kWheelCircumference / 1024.0 / kDriveGearRatio;
|
||||||
static constexpr units::radian_t kSteerRadPerPulse =
|
inline constexpr units::radian_t kSteerRadPerPulse =
|
||||||
units::radian_t{2 * std::numbers::pi} / 1024.0;
|
units::radian_t{2 * std::numbers::pi} / 1024.0;
|
||||||
|
|
||||||
static constexpr double kDriveKP = 1.0;
|
inline constexpr double kDriveKP = 1.0;
|
||||||
static constexpr double kDriveKI = 0.0;
|
inline constexpr double kDriveKI = 0.0;
|
||||||
static constexpr double kDriveKD = 0.0;
|
inline constexpr double kDriveKD = 0.0;
|
||||||
|
|
||||||
static constexpr double kSteerKP = 20.0;
|
inline constexpr double kSteerKP = 20.0;
|
||||||
static constexpr double kSteerKI = 0.0;
|
inline constexpr double kSteerKI = 0.0;
|
||||||
static constexpr double kSteerKD = 0.25;
|
inline constexpr double kSteerKD = 0.25;
|
||||||
|
|
||||||
static const frc::SimpleMotorFeedforward<units::meters> kDriveFF{
|
inline const frc::SimpleMotorFeedforward<units::meters> kDriveFF{
|
||||||
0.25_V, 2.5_V / 1_mps, 0.3_V / 1_mps_sq};
|
0.25_V, 2.5_V / 1_mps, 0.3_V / 1_mps_sq};
|
||||||
|
|
||||||
static const frc::SimpleMotorFeedforward<units::radians> kSteerFF{
|
inline const frc::SimpleMotorFeedforward<units::radians> kSteerFF{
|
||||||
0.5_V, 0.25_V / 1_rad_per_s, 0.01_V / 1_rad_per_s_sq};
|
0.5_V, 0.25_V / 1_rad_per_s, 0.01_V / 1_rad_per_s_sq};
|
||||||
|
|
||||||
struct ModuleConstants {
|
struct ModuleConstants {
|
||||||
@@ -106,13 +106,13 @@ struct ModuleConstants {
|
|||||||
centerOffset(frc::Translation2d{xOffset, yOffset}) {}
|
centerOffset(frc::Translation2d{xOffset, yOffset}) {}
|
||||||
};
|
};
|
||||||
|
|
||||||
static const ModuleConstants FL_CONSTANTS{
|
inline const ModuleConstants FL_CONSTANTS{
|
||||||
1, 0, 0, 1, 1, 2, 3, 0, kTrackLength / 2, kTrackWidth / 2};
|
1, 0, 0, 1, 1, 2, 3, 0, kTrackLength / 2, kTrackWidth / 2};
|
||||||
static const ModuleConstants FR_CONSTANTS{
|
inline const ModuleConstants FR_CONSTANTS{
|
||||||
2, 2, 4, 5, 3, 6, 7, 0, kTrackLength / 2, -kTrackWidth / 2};
|
2, 2, 4, 5, 3, 6, 7, 0, kTrackLength / 2, -kTrackWidth / 2};
|
||||||
static const ModuleConstants BL_CONSTANTS{
|
inline const ModuleConstants BL_CONSTANTS{
|
||||||
3, 4, 8, 9, 5, 10, 11, 0, -kTrackLength / 2, kTrackWidth / 2};
|
3, 4, 8, 9, 5, 10, 11, 0, -kTrackLength / 2, kTrackWidth / 2};
|
||||||
static const ModuleConstants BR_CONSTANTS{
|
inline const ModuleConstants BR_CONSTANTS{
|
||||||
4, 6, 12, 13, 7, 14, 15, 0, -kTrackLength / 2, -kTrackWidth / 2};
|
4, 6, 12, 13, 7, 14, 15, 0, -kTrackLength / 2, -kTrackWidth / 2};
|
||||||
} // namespace Swerve
|
} // namespace Swerve
|
||||||
} // namespace constants
|
} // namespace constants
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ class Vision {
|
|||||||
cameraSim = std::make_shared<photon::PhotonCameraSim>(camera.get(),
|
cameraSim = std::make_shared<photon::PhotonCameraSim>(camera.get(),
|
||||||
*cameraProp.get());
|
*cameraProp.get());
|
||||||
|
|
||||||
visionSim->AddCamera(cameraSim.get(), robotToCam);
|
visionSim->AddCamera(cameraSim.get(), constants::Vision::kRobotToCam);
|
||||||
cameraSim->EnableDrawWireframe(true);
|
cameraSim->EnableDrawWireframe(true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -138,12 +138,10 @@ class Vision {
|
|||||||
frc::Field2d& GetSimDebugField() { return visionSim->GetDebugField(); }
|
frc::Field2d& GetSimDebugField() { return visionSim->GetDebugField(); }
|
||||||
|
|
||||||
private:
|
private:
|
||||||
frc::Transform3d robotToCam{frc::Translation3d{0.5_m, 0.5_m, 0.5_m},
|
|
||||||
frc::Rotation3d{}};
|
|
||||||
photon::PhotonPoseEstimator photonEstimator{
|
photon::PhotonPoseEstimator photonEstimator{
|
||||||
LoadAprilTagLayoutField(frc::AprilTagField::k2023ChargedUp),
|
constants::Vision::kTagLayout,
|
||||||
photon::PoseStrategy::MULTI_TAG_PNP_ON_COPROCESSOR,
|
photon::PoseStrategy::MULTI_TAG_PNP_ON_COPROCESSOR,
|
||||||
photon::PhotonCamera{"photonvision"}, robotToCam};
|
photon::PhotonCamera{"photonvision"}, constants::Vision::kRobotToCam};
|
||||||
std::shared_ptr<photon::PhotonCamera> camera{photonEstimator.GetCamera()};
|
std::shared_ptr<photon::PhotonCamera> camera{photonEstimator.GetCamera()};
|
||||||
std::unique_ptr<photon::VisionSystemSim> visionSim;
|
std::unique_ptr<photon::VisionSystemSim> visionSim;
|
||||||
std::unique_ptr<photon::SimCameraProperties> cameraProp;
|
std::unique_ptr<photon::SimCameraProperties> cameraProp;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
plugins {
|
plugins {
|
||||||
id "java"
|
id "java"
|
||||||
id "edu.wpi.first.GradleRIO"
|
id "edu.wpi.first.GradleRIO" version "2024.3.1"
|
||||||
}
|
}
|
||||||
|
|
||||||
sourceCompatibility = JavaVersion.VERSION_11
|
sourceCompatibility = JavaVersion.VERSION_11
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
plugins {
|
plugins {
|
||||||
id "java"
|
id "java"
|
||||||
id "edu.wpi.first.GradleRIO"
|
id "edu.wpi.first.GradleRIO" version "2024.3.1"
|
||||||
}
|
}
|
||||||
|
|
||||||
sourceCompatibility = JavaVersion.VERSION_11
|
sourceCompatibility = JavaVersion.VERSION_11
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
plugins {
|
plugins {
|
||||||
id "com.diffplug.spotless" version "6.1.2"
|
id "com.diffplug.spotless" version "6.1.2"
|
||||||
id "edu.wpi.first.GradleRIO" version "2024.1.1" apply false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "examples.gradle"
|
apply from: "examples.gradle"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
plugins {
|
plugins {
|
||||||
id "java"
|
id "java"
|
||||||
id "edu.wpi.first.GradleRIO"
|
id "edu.wpi.first.GradleRIO" version "2024.3.1"
|
||||||
}
|
}
|
||||||
|
|
||||||
sourceCompatibility = JavaVersion.VERSION_11
|
sourceCompatibility = JavaVersion.VERSION_11
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
plugins {
|
plugins {
|
||||||
id "java"
|
id "java"
|
||||||
id "edu.wpi.first.GradleRIO"
|
id "edu.wpi.first.GradleRIO" version "2024.3.1"
|
||||||
}
|
}
|
||||||
|
|
||||||
sourceCompatibility = JavaVersion.VERSION_11
|
sourceCompatibility = JavaVersion.VERSION_11
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
plugins {
|
plugins {
|
||||||
id "java"
|
id "java"
|
||||||
id "edu.wpi.first.GradleRIO"
|
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.1.1"
|
wpi.versions.wpilibVersion = "2024.3.1"
|
||||||
wpi.versions.wpimathVersion = "2024.1.1"
|
wpi.versions.wpimathVersion = "2024.3.1"
|
||||||
|
|
||||||
|
|
||||||
// Define my targets (RoboRIO) and artifacts (deployable files)
|
// Define my targets (RoboRIO) and artifacts (deployable files)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user