mirror of
https://github.com/PhotonVision/photonvision
synced 2026-06-19 00:41:41 +00:00
Compare commits
55 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5dc70e4d3f | ||
|
|
5597f5acd9 | ||
|
|
fae3116951 | ||
|
|
def37b92ba | ||
|
|
5b878fe3a3 | ||
|
|
d9c2a382f1 | ||
|
|
e125632960 | ||
|
|
d8f82bf9ee | ||
|
|
587ac478f4 | ||
|
|
bad676f67c | ||
|
|
71128d1569 | ||
|
|
7cec141341 | ||
|
|
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 | ||
|
|
628cead2dc | ||
|
|
7b67f6bebf | ||
|
|
e1f550a751 | ||
|
|
a40e4049d4 | ||
|
|
152888f216 | ||
|
|
b729d9e917 | ||
|
|
6917ec8401 |
93
.github/workflows/build.yml
vendored
93
.github/workflows/build.yml
vendored
@@ -260,6 +260,83 @@ jobs:
|
||||
with:
|
||||
name: jar-${{ matrix.artifact-name }}
|
||||
path: photon-server/build/libs
|
||||
|
||||
run-smoketest-native:
|
||||
needs: [build-package]
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- os: ubuntu-latest
|
||||
artifact-name: jar-Linux
|
||||
extraOpts: -Djdk.lang.Process.launchMechanism=vfork
|
||||
- os: windows-latest
|
||||
artifact-name: jar-Win64
|
||||
extraOpts: ""
|
||||
- os: macos-latest
|
||||
artifact-name: jar-macOS
|
||||
architecture: x64
|
||||
|
||||
runs-on: ${{ matrix.os }}
|
||||
|
||||
steps:
|
||||
- name: Install Java 17
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
java-version: 17
|
||||
distribution: temurin
|
||||
- uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: ${{ matrix.artifact-name }}
|
||||
# On linux, install mrcal packages
|
||||
- run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install --yes libcholmod3 liblapack3 libsuitesparseconfig5
|
||||
if: ${{ (matrix.os) == 'ubuntu-latest' }}
|
||||
# and actually run the jar
|
||||
- run: java -jar ${{ matrix.extraOpts }} *.jar --smoketest
|
||||
if: ${{ (matrix.os) != 'windows-latest' }}
|
||||
- run: ls *.jar | %{ Write-Host "Running $($_.Name)"; Start-Process "java" -ArgumentList "-jar `"$($_.FullName)`" --smoketest" -NoNewWindow -Wait; break }
|
||||
if: ${{ (matrix.os) == 'windows-latest' }}
|
||||
|
||||
run-smoketest-chroot:
|
||||
needs: [build-package]
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- os: ubuntu-latest
|
||||
artifact-name: LinuxArm64
|
||||
image_suffix: RaspberryPi
|
||||
image_url: https://github.com/PhotonVision/photon-image-modifier/releases/download/v2024.0.4/photonvision_raspi.img.xz
|
||||
cpu: cortex-a7
|
||||
image_additional_mb: 0
|
||||
extraOpts: -Djdk.lang.Process.launchMechanism=vfork
|
||||
|
||||
runs-on: ${{ matrix.os }}
|
||||
name: smoketest-${{ matrix.image_suffix }}
|
||||
|
||||
steps:
|
||||
- uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: jar-${{ matrix.artifact-name }}
|
||||
|
||||
- uses: pguyot/arm-runner-action@v2
|
||||
name: Run photon smoketest
|
||||
id: generate_image
|
||||
with:
|
||||
base_image: ${{ matrix.image_url }}
|
||||
image_additional_mb: ${{ matrix.image_additional_mb }}
|
||||
optimize_image: yes
|
||||
cpu: ${{ matrix.cpu }}
|
||||
# We do _not_ wanna copy photon into the image. Bind mount instead
|
||||
bind_mount_repository: true
|
||||
# our image better have java installed already
|
||||
commands: |
|
||||
java -jar ${{ matrix.extraOpts }} *.jar --smoketest
|
||||
|
||||
build-image:
|
||||
needs: [build-package]
|
||||
|
||||
@@ -281,10 +358,22 @@ jobs:
|
||||
image_url: https://github.com/PhotonVision/photon-image-modifier/releases/download/v2024.0.4/photonvision_limelight.img.xz
|
||||
cpu: cortex-a7
|
||||
image_additional_mb: 0
|
||||
- os: ubuntu-latest
|
||||
artifact-name: LinuxArm64
|
||||
image_suffix: limelight3
|
||||
image_url: https://github.com/PhotonVision/photon-image-modifier/releases/download/v2024.0.5/photonvision_limelight3.img.xz
|
||||
cpu: cortex-a7
|
||||
image_additional_mb: 0
|
||||
- os: ubuntu-latest
|
||||
artifact-name: LinuxArm64
|
||||
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.10/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.10/photonvision_opi5plus.img.xz
|
||||
cpu: cortex-a8
|
||||
image_additional_mb: 4096
|
||||
|
||||
@@ -307,6 +396,8 @@ jobs:
|
||||
image_additional_mb: ${{ matrix.image_additional_mb }}
|
||||
optimize_image: yes
|
||||
cpu: ${{ matrix.cpu }}
|
||||
# We do _not_ wanna copy photon into the image. Bind mount instead
|
||||
bind_mount_repository: true
|
||||
commands: |
|
||||
chmod +x scripts/armrunner.sh
|
||||
./scripts/armrunner.sh
|
||||
|
||||
22
.github/workflows/documentation.yml
vendored
22
.github/workflows/documentation.yml
vendored
@@ -68,10 +68,6 @@ jobs:
|
||||
release:
|
||||
needs: [build-client, run_docs]
|
||||
|
||||
environment:
|
||||
name: github-pages
|
||||
url: ${{ steps.deployment.outputs.page_url }}
|
||||
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
|
||||
@@ -79,14 +75,12 @@ jobs:
|
||||
- uses: actions/download-artifact@v4
|
||||
|
||||
- run: find .
|
||||
|
||||
- name: Setup Pages
|
||||
uses: actions/configure-pages@v4
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-pages-artifact@v3
|
||||
- name: copy file via ssh password
|
||||
uses: appleboy/scp-action@v0.1.7
|
||||
with:
|
||||
# Upload entire repository
|
||||
path: '.'
|
||||
- name: Deploy to GitHub Pages
|
||||
id: deployment
|
||||
uses: actions/deploy-pages@v4
|
||||
host: ${{ secrets.WEBMASTER_SSH_HOST }}
|
||||
username: ${{ secrets.WEBMASTER_SSH_USERNAME }}
|
||||
password: ${{ secrets.WEBMASTER_SSH_KEY }}
|
||||
port: ${{ secrets.WEBMASTER_SSH_PORT }}
|
||||
source: "*"
|
||||
target: /var/www/html/photonvision-docs/
|
||||
|
||||
@@ -18,6 +18,7 @@ modifiableFileExclude {
|
||||
\.dll$
|
||||
\.webp$
|
||||
\.ico$
|
||||
\.rknn$
|
||||
gradlew
|
||||
}
|
||||
|
||||
|
||||
16
build.gradle
16
build.gradle
@@ -1,8 +1,10 @@
|
||||
import edu.wpi.first.toolchain.*
|
||||
|
||||
plugins {
|
||||
id "com.diffplug.spotless" version "6.22.0"
|
||||
id "com.diffplug.spotless" version "6.24.0"
|
||||
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.GradleRIO" version "2024.1.1"
|
||||
id "edu.wpi.first.GradleRIO" version "2024.3.2"
|
||||
id 'edu.wpi.first.WpilibTools' version '1.3.0'
|
||||
id 'com.google.protobuf' version '0.9.4' apply false
|
||||
}
|
||||
@@ -22,14 +24,16 @@ allprojects {
|
||||
apply from: "versioningHelper.gradle"
|
||||
|
||||
ext {
|
||||
wpilibVersion = "2024.1.1"
|
||||
wpilibVersion = "2024.3.2"
|
||||
wpimathVersion = wpilibVersion
|
||||
openCVversion = "4.8.0-2"
|
||||
joglVersion = "2.4.0-rc-20200307"
|
||||
javalinVersion = "5.6.2"
|
||||
photonGlDriverLibVersion = "dev-v2023.1.0-9-g75fc678"
|
||||
rknnVersion = "dev-v2024.0.0-64-gc0836a6"
|
||||
frcYear = "2024"
|
||||
mrcalVersion = "dev-v2024.0.0-7-gc976aaa";
|
||||
mrcalVersion = "dev-v2024.0.0-18-gb903a09";
|
||||
|
||||
|
||||
pubVersion = versionString
|
||||
isDev = pubVersion.startsWith("dev")
|
||||
@@ -96,3 +100,7 @@ spotless {
|
||||
wrapper {
|
||||
gradleVersion '8.4'
|
||||
}
|
||||
|
||||
ext.getCurrentArch = {
|
||||
return NativePlatforms.desktop
|
||||
}
|
||||
|
||||
@@ -162,9 +162,9 @@ def __convert_cal_to_mrcal_cameramodel(
|
||||
"indices_point_camintrinsics_camextrinsics": None,
|
||||
"lensmodel": model,
|
||||
"imagersizes": np.array([imagersize], dtype=np.int32),
|
||||
"calobject_warp": np.array(cal.calobjectWarp)
|
||||
if len(cal.calobjectWarp) > 0
|
||||
else None,
|
||||
"calobject_warp": (
|
||||
np.array(cal.calobjectWarp) if len(cal.calobjectWarp) > 0 else None
|
||||
),
|
||||
# We always do all the things
|
||||
"do_optimize_intrinsics_core": True,
|
||||
"do_optimize_intrinsics_distortions": True,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<!DOCTYPE html>
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
|
||||
8
photon-client/package-lock.json
generated
8
photon-client/package-lock.json
generated
@@ -31,7 +31,7 @@
|
||||
"eslint": "^8.56.0",
|
||||
"eslint-plugin-vue": "^9.19.2",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"prettier": "^3.1.1",
|
||||
"prettier": "3.2.2",
|
||||
"sass": "~1.32",
|
||||
"sass-loader": "^13.3.2",
|
||||
"terser": "^5.14.2",
|
||||
@@ -3917,9 +3917,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/prettier": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.1.1.tgz",
|
||||
"integrity": "sha512-22UbSzg8luF4UuZtzgiUOfcGM8s4tjBv6dJRT7j275NXsy2jb4aJa4NNveul5x4eqlF1wuhuR2RElK71RvmVaw==",
|
||||
"version": "3.2.2",
|
||||
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.2.2.tgz",
|
||||
"integrity": "sha512-HTByuKZzw7utPiDO523Tt2pLtEyK7OibUD9suEJQrPUCYQqrHr74GGX6VidMrovbf/I50mPqr8j/II6oBAuc5A==",
|
||||
"dev": true,
|
||||
"bin": {
|
||||
"prettier": "bin/prettier.cjs"
|
||||
|
||||
@@ -27,17 +27,17 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@rushstack/eslint-patch": "^1.3.2",
|
||||
"@vue/eslint-config-prettier": "^9.0.0",
|
||||
"@vue/eslint-config-typescript": "^12.0.0",
|
||||
"prettier": "^3.1.1",
|
||||
"@types/node": "^16.11.45",
|
||||
"@types/three": "^0.160.0",
|
||||
"@vitejs/plugin-vue2": "^2.3.1",
|
||||
"@vue/eslint-config-prettier": "^9.0.0",
|
||||
"@vue/eslint-config-typescript": "^12.0.0",
|
||||
"@vue/tsconfig": "^0.5.1",
|
||||
"deepmerge": "^4.3.1",
|
||||
"eslint": "^8.56.0",
|
||||
"eslint-plugin-vue": "^9.19.2",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"prettier": "3.2.2",
|
||||
"sass": "~1.32",
|
||||
"sass-loader": "^13.3.2",
|
||||
"terser": "^5.14.2",
|
||||
|
||||
@@ -25,15 +25,10 @@ const getUniqueVideoFormatsByResolution = (): VideoFormat[] => {
|
||||
|
||||
const calib = useCameraSettingsStore().getCalibrationCoeffs(format.resolution);
|
||||
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
|
||||
format.mean = Math.sqrt(
|
||||
perViewSumSquareReprojectionError.map((it) => Math.pow(it, 2)).reduce((a, b) => a + b, 0) /
|
||||
perViewSumSquareReprojectionError.length
|
||||
);
|
||||
if (calib.meanErrors.length)
|
||||
format.mean = calib.meanErrors.reduce((a, b) => a + b, 0) / calib.meanErrors.length;
|
||||
else format.mean = NaN;
|
||||
|
||||
format.horizontalFOV =
|
||||
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;
|
||||
|
||||
// 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");
|
||||
}
|
||||
}
|
||||
@@ -263,7 +258,7 @@ const setSelectedVideoFormat = (format: VideoFormat) => {
|
||||
>
|
||||
<td>{{ getResolutionString(value.resolution) }}</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>{{ value.horizontalFOV !== undefined ? value.horizontalFOV.toFixed(2) + "°" : "-" }}</td>
|
||||
<td>{{ value.verticalFOV !== undefined ? value.verticalFOV.toFixed(2) + "°" : "-" }}</td>
|
||||
@@ -311,7 +306,7 @@ const setSelectedVideoFormat = (format: VideoFormat) => {
|
||||
/>
|
||||
<pv-number-input
|
||||
v-model="patternWidth"
|
||||
label="Board Width (in)"
|
||||
label="Board Width (squares)"
|
||||
tooltip="Width of the board in dots or chessboard squares"
|
||||
:disabled="isCalibrating"
|
||||
:rules="[(v) => v >= 4 || 'Width must be at least 4']"
|
||||
@@ -319,7 +314,7 @@ const setSelectedVideoFormat = (format: VideoFormat) => {
|
||||
/>
|
||||
<pv-number-input
|
||||
v-model="patternHeight"
|
||||
label="Board Height (in)"
|
||||
label="Board Height (squares)"
|
||||
tooltip="Height of the board in dots or chessboard squares"
|
||||
:disabled="isCalibrating"
|
||||
:rules="[(v) => v >= 4 || 'Height must be at least 4']"
|
||||
|
||||
@@ -1,51 +1,19 @@
|
||||
<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 { useStateStore } from "@/stores/StateStore";
|
||||
import { ref } from "vue";
|
||||
import loadingImage from "@/assets/images/loading.svg";
|
||||
import { computed, inject, ref } from "vue";
|
||||
import { getResolutionString, parseJsonFile } from "@/lib/PhotonUtils";
|
||||
|
||||
const props = defineProps<{
|
||||
videoFormat: VideoFormat;
|
||||
}>();
|
||||
|
||||
const getMeanFromView = (o: BoardObservation) => {
|
||||
// Is this the right formula for RMS error? who knows! not me!
|
||||
const perViewSumSquareReprojectionError = o.reprojectionErrors.flatMap((it2) => [it2.x, it2.y]);
|
||||
|
||||
// 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
|
||||
);
|
||||
const exportCalibration = ref();
|
||||
const openExportCalibrationPrompt = () => {
|
||||
exportCalibration.value.click();
|
||||
};
|
||||
|
||||
// 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 openUploadPhotonCalibJsonPrompt = () => {
|
||||
importCalibrationFromPhotonJson.value.click();
|
||||
@@ -97,19 +65,28 @@ const importCalibration = async () => {
|
||||
};
|
||||
|
||||
interface ObservationDetails {
|
||||
snapshotSrc: any;
|
||||
mean: number;
|
||||
index: number;
|
||||
}
|
||||
|
||||
const currentCalibrationCoeffs = computed<CameraCalibrationResult | undefined>(() =>
|
||||
useCameraSettingsStore().getCalibrationCoeffs(props.videoFormat.resolution)
|
||||
);
|
||||
|
||||
const getObservationDetails = (): ObservationDetails[] | undefined => {
|
||||
return useCameraSettingsStore()
|
||||
.getCalibrationCoeffs(props.videoFormat.resolution)
|
||||
?.observations.map((o, i) => ({
|
||||
index: i,
|
||||
mean: parseFloat(getMeanFromView(o).toFixed(2)),
|
||||
snapshotSrc: o.includeObservationInCalibration ? "data:image/png;base64," + o.snapshotData.data : loadingImage
|
||||
}));
|
||||
const coefficients = currentCalibrationCoeffs.value;
|
||||
|
||||
return coefficients?.meanErrors.map((m, i) => ({
|
||||
index: i,
|
||||
mean: parseFloat(m.toFixed(2))
|
||||
}));
|
||||
};
|
||||
|
||||
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>
|
||||
|
||||
<template>
|
||||
@@ -140,19 +117,22 @@ const getObservationDetails = (): ObservationDetails[] | undefined => {
|
||||
<v-btn
|
||||
color="secondary"
|
||||
class="mt-4"
|
||||
:disabled="useCameraSettingsStore().getCalibrationCoeffs(props.videoFormat.resolution) === undefined"
|
||||
:disabled="!currentCalibrationCoeffs"
|
||||
style="width: 100%"
|
||||
@click="downloadCalibration"
|
||||
@click="openExportCalibrationPrompt"
|
||||
>
|
||||
<v-icon left>mdi-export</v-icon>
|
||||
<span>Export</span>
|
||||
</v-btn>
|
||||
<a
|
||||
ref="exportCalibration"
|
||||
style="color: black; text-decoration: none; display: none"
|
||||
:href="exportCalibrationURL"
|
||||
target="_blank"
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row
|
||||
v-if="useCameraSettingsStore().getCalibrationCoeffs(props.videoFormat.resolution) !== undefined"
|
||||
class="pt-2"
|
||||
>
|
||||
<v-row v-if="currentCalibrationCoeffs" class="pt-2">
|
||||
<v-card-subtitle>Calibration Details</v-card-subtitle>
|
||||
<v-simple-table dense style="width: 100%" class="pl-2 pr-2">
|
||||
<template #default>
|
||||
@@ -231,7 +211,9 @@ const getObservationDetails = (): ObservationDetails[] | undefined => {
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Horizontal FOV</td>
|
||||
<td>{{ videoFormat.horizontalFOV !== undefined ? videoFormat.horizontalFOV.toFixed(2) + "°" : "-" }}</td>
|
||||
<td>
|
||||
{{ videoFormat.horizontalFOV !== undefined ? videoFormat.horizontalFOV.toFixed(2) + "°" : "-" }}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Vertical FOV</td>
|
||||
@@ -242,11 +224,7 @@ const getObservationDetails = (): ObservationDetails[] | undefined => {
|
||||
<td>{{ videoFormat.diagonalFOV !== undefined ? videoFormat.diagonalFOV.toFixed(2) + "°" : "-" }}</td>
|
||||
</tr>
|
||||
<!-- Board warp, only shown for mrcal-calibrated cameras -->
|
||||
<tr
|
||||
v-if="
|
||||
useCameraSettingsStore().getCalibrationCoeffs(props.videoFormat.resolution)?.calobjectWarp?.length === 2
|
||||
"
|
||||
>
|
||||
<tr v-if="currentCalibrationCoeffs?.calobjectWarp?.length === 2">
|
||||
<td>Board warp, X/Y</td>
|
||||
<td>
|
||||
{{
|
||||
@@ -278,7 +256,7 @@ const getObservationDetails = (): ObservationDetails[] | undefined => {
|
||||
<template #expanded-item="{ headers, item }">
|
||||
<td :colspan="headers.length">
|
||||
<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>
|
||||
</td>
|
||||
</template>
|
||||
|
||||
@@ -7,6 +7,7 @@ import { computed, ref } from "vue";
|
||||
import PvIcon from "@/components/common/pv-icon.vue";
|
||||
import PvInput from "@/components/common/pv-input.vue";
|
||||
import { PipelineType } from "@/types/PipelineTypes";
|
||||
import { useSettingsStore } from "@/stores/settings/GeneralSettingsStore";
|
||||
|
||||
const changeCurrentCameraIndex = (index: number) => {
|
||||
useCameraSettingsStore().setCurrentCameraIndex(index, true);
|
||||
@@ -24,6 +25,9 @@ const changeCurrentCameraIndex = (index: number) => {
|
||||
case PipelineType.Aruco:
|
||||
pipelineType.value = WebsocketPipelineType.Aruco;
|
||||
break;
|
||||
case PipelineType.ObjectDetection:
|
||||
pipelineType.value = WebsocketPipelineType.ObjectDetection;
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -121,6 +125,18 @@ const cancelPipelineNameEdit = () => {
|
||||
const showPipelineCreationDialog = ref(false);
|
||||
const newPipelineName = ref("");
|
||||
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 = () => {
|
||||
newPipelineName.value = "";
|
||||
newPipelineType.value = useCameraSettingsStore().currentWebsocketPipelineType;
|
||||
@@ -154,6 +170,9 @@ const pipelineTypesWrapper = computed<{ name: string; value: number }[]>(() => {
|
||||
{ name: "AprilTag", value: WebsocketPipelineType.AprilTag },
|
||||
{ name: "Aruco", value: WebsocketPipelineType.Aruco }
|
||||
];
|
||||
if (useSettingsStore().general.rknnSupported) {
|
||||
pipelineTypes.push({ name: "Object Detection", value: WebsocketPipelineType.ObjectDetection });
|
||||
}
|
||||
|
||||
if (useCameraSettingsStore().isDriverMode) {
|
||||
pipelineTypes.push({ name: "Driver Mode", value: WebsocketPipelineType.DriverMode });
|
||||
@@ -208,6 +227,9 @@ useCameraSettingsStore().$subscribe((mutation, state) => {
|
||||
case PipelineType.Aruco:
|
||||
pipelineType.value = WebsocketPipelineType.Aruco;
|
||||
break;
|
||||
case PipelineType.ObjectDetection:
|
||||
pipelineType.value = WebsocketPipelineType.ObjectDetection;
|
||||
break;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
@@ -350,12 +372,7 @@ useCameraSettingsStore().$subscribe((mutation, state) => {
|
||||
:select-cols="12 - 3"
|
||||
label="Tracking Type"
|
||||
tooltip="Pipeline type, which changes the type of processing that will happen on input frames"
|
||||
:items="[
|
||||
{ name: 'Reflective', value: WebsocketPipelineType.Reflective },
|
||||
{ name: 'Colored Shape', value: WebsocketPipelineType.ColoredShape },
|
||||
{ name: 'AprilTag', value: WebsocketPipelineType.AprilTag },
|
||||
{ name: 'Aruco', value: WebsocketPipelineType.Aruco }
|
||||
]"
|
||||
:items="validNewPipelineTypes"
|
||||
/>
|
||||
</v-card-text>
|
||||
<v-divider />
|
||||
|
||||
@@ -13,11 +13,7 @@ defineProps<{
|
||||
|
||||
const driverMode = computed<boolean>({
|
||||
get: () => useCameraSettingsStore().isDriverMode,
|
||||
set: (v) =>
|
||||
useCameraSettingsStore().changeCurrentPipelineIndex(
|
||||
v ? -1 : useCameraSettingsStore().currentCameraSettings.lastPipelineIndex || 0,
|
||||
true
|
||||
)
|
||||
set: (v) => useCameraSettingsStore().setDriverMode(v)
|
||||
});
|
||||
|
||||
const fpsTooLow = computed<boolean>(() => {
|
||||
|
||||
@@ -8,6 +8,7 @@ import ThresholdTab from "@/components/dashboard/tabs/ThresholdTab.vue";
|
||||
import ContoursTab from "@/components/dashboard/tabs/ContoursTab.vue";
|
||||
import AprilTagTab from "@/components/dashboard/tabs/AprilTagTab.vue";
|
||||
import ArucoTab from "@/components/dashboard/tabs/ArucoTab.vue";
|
||||
import ObjectDetectionTab from "@/components/dashboard/tabs/ObjectDetectionTab.vue";
|
||||
import OutputTab from "@/components/dashboard/tabs/OutputTab.vue";
|
||||
import TargetsTab from "@/components/dashboard/tabs/TargetsTab.vue";
|
||||
import PnPTab from "@/components/dashboard/tabs/PnPTab.vue";
|
||||
@@ -40,6 +41,10 @@ const allTabs = Object.freeze({
|
||||
tabName: "Aruco",
|
||||
component: ArucoTab
|
||||
},
|
||||
objectDetectionTab: {
|
||||
tabName: "Object Detection",
|
||||
component: ObjectDetectionTab
|
||||
},
|
||||
outputTab: {
|
||||
tabName: "Output",
|
||||
component: OutputTab
|
||||
@@ -75,6 +80,7 @@ const getTabGroups = (): ConfigOption[][] => {
|
||||
allTabs.contoursTab,
|
||||
allTabs.apriltagTab,
|
||||
allTabs.arucoTab,
|
||||
allTabs.objectDetectionTab,
|
||||
allTabs.outputTab
|
||||
],
|
||||
[allTabs.targetsTab, allTabs.pnpTab, allTabs.map3dTab]
|
||||
@@ -82,14 +88,21 @@ const getTabGroups = (): ConfigOption[][] => {
|
||||
} else if (lgAndDown) {
|
||||
return [
|
||||
[allTabs.inputTab],
|
||||
[allTabs.thresholdTab, allTabs.contoursTab, allTabs.apriltagTab, allTabs.arucoTab, allTabs.outputTab],
|
||||
[
|
||||
allTabs.thresholdTab,
|
||||
allTabs.contoursTab,
|
||||
allTabs.apriltagTab,
|
||||
allTabs.arucoTab,
|
||||
allTabs.objectDetectionTab,
|
||||
allTabs.outputTab
|
||||
],
|
||||
[allTabs.targetsTab, allTabs.pnpTab, allTabs.map3dTab]
|
||||
];
|
||||
} else if (xl) {
|
||||
return [
|
||||
[allTabs.inputTab],
|
||||
[allTabs.thresholdTab],
|
||||
[allTabs.contoursTab, allTabs.apriltagTab, allTabs.arucoTab, allTabs.outputTab],
|
||||
[allTabs.contoursTab, allTabs.apriltagTab, allTabs.arucoTab, allTabs.objectDetectionTab, allTabs.outputTab],
|
||||
[allTabs.targetsTab, allTabs.pnpTab, allTabs.map3dTab]
|
||||
];
|
||||
}
|
||||
@@ -103,17 +116,20 @@ const tabGroups = computed<ConfigOption[][]>(() => {
|
||||
const allow3d = useCameraSettingsStore().currentPipelineSettings.solvePNPEnabled;
|
||||
const isAprilTag = useCameraSettingsStore().currentWebsocketPipelineType === WebsocketPipelineType.AprilTag;
|
||||
const isAruco = useCameraSettingsStore().currentWebsocketPipelineType === WebsocketPipelineType.Aruco;
|
||||
const isObjectDetection =
|
||||
useCameraSettingsStore().currentWebsocketPipelineType === WebsocketPipelineType.ObjectDetection;
|
||||
|
||||
return getTabGroups()
|
||||
.map((tabGroup) =>
|
||||
tabGroup.filter(
|
||||
(tabConfig) =>
|
||||
!(!allow3d && tabConfig.tabName === "3D") && //Filter out 3D tab any time 3D isn't calibrated
|
||||
!((!allow3d || isAprilTag || isAruco) && tabConfig.tabName === "PnP") && //Filter out the PnP config tab if 3D isn't available, or we're doing AprilTags
|
||||
!((isAprilTag || isAruco) && tabConfig.tabName === "Threshold") && //Filter out threshold tab if we're doing AprilTags
|
||||
!((isAprilTag || isAruco) && tabConfig.tabName === "Contours") && //Filter out contours if we're doing AprilTags
|
||||
!((!allow3d || isAprilTag || isAruco || isObjectDetection) && tabConfig.tabName === "PnP") && //Filter out the PnP config tab if 3D isn't available, or we're doing AprilTags
|
||||
!((isAprilTag || isAruco || isObjectDetection) && tabConfig.tabName === "Threshold") && //Filter out threshold tab if we're doing AprilTags
|
||||
!((isAprilTag || isAruco || isObjectDetection) && tabConfig.tabName === "Contours") && //Filter out contours if we're doing AprilTags
|
||||
!(!isAprilTag && tabConfig.tabName === "AprilTag") && //Filter out apriltag unless we actually are doing AprilTags
|
||||
!(!isAruco && tabConfig.tabName === "Aruco") //Filter out aruco unless we actually are doing Aruco
|
||||
!(!isAruco && tabConfig.tabName === "Aruco") &&
|
||||
!(!isObjectDetection && tabConfig.tabName === "Object Detection") //Filter out aruco unless we actually are doing Aruco
|
||||
)
|
||||
)
|
||||
.filter((it) => it.length); // Remove empty tab groups
|
||||
|
||||
@@ -14,13 +14,12 @@ const currentPipelineSettings = computed<ActivePipelineSettings>(
|
||||
() => useCameraSettingsStore().currentPipelineSettings
|
||||
);
|
||||
|
||||
const interactiveCols = computed(
|
||||
() =>
|
||||
(getCurrentInstance()?.proxy.$vuetify.breakpoint.mdAndDown || false) &&
|
||||
(!useStateStore().sidebarFolded || useCameraSettingsStore().isDriverMode)
|
||||
)
|
||||
? 9
|
||||
: 8;
|
||||
const interactiveCols = computed(() =>
|
||||
(getCurrentInstance()?.proxy.$vuetify.breakpoint.mdAndDown || false) &&
|
||||
(!useStateStore().sidebarFolded || useCameraSettingsStore().isDriverMode)
|
||||
? 9
|
||||
: 8
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
@@ -14,13 +14,12 @@ const currentPipelineSettings = computed<ActivePipelineSettings>(
|
||||
() => useCameraSettingsStore().currentPipelineSettings
|
||||
);
|
||||
|
||||
const interactiveCols = computed(
|
||||
() =>
|
||||
(getCurrentInstance()?.proxy.$vuetify.breakpoint.mdAndDown || false) &&
|
||||
(!useStateStore().sidebarFolded || useCameraSettingsStore().isDriverMode)
|
||||
)
|
||||
? 9
|
||||
: 8;
|
||||
const interactiveCols = computed(() =>
|
||||
(getCurrentInstance()?.proxy.$vuetify.breakpoint.mdAndDown || false) &&
|
||||
(!useStateStore().sidebarFolded || useCameraSettingsStore().isDriverMode)
|
||||
? 9
|
||||
: 8
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
@@ -49,13 +49,12 @@ const contourRadius = computed<[number, number]>({
|
||||
}
|
||||
});
|
||||
|
||||
const interactiveCols = computed(
|
||||
() =>
|
||||
(getCurrentInstance()?.proxy.$vuetify.breakpoint.mdAndDown || false) &&
|
||||
(!useStateStore().sidebarFolded || useCameraSettingsStore().isDriverMode)
|
||||
)
|
||||
? 9
|
||||
: 8;
|
||||
const interactiveCols = computed(() =>
|
||||
(getCurrentInstance()?.proxy.$vuetify.breakpoint.mdAndDown || false) &&
|
||||
(!useStateStore().sidebarFolded || useCameraSettingsStore().isDriverMode)
|
||||
? 9
|
||||
: 8
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
@@ -63,13 +63,12 @@ const handleStreamResolutionChange = (value: number) => {
|
||||
);
|
||||
};
|
||||
|
||||
const interactiveCols = computed(
|
||||
() =>
|
||||
(getCurrentInstance()?.proxy.$vuetify.breakpoint.mdAndDown || false) &&
|
||||
(!useStateStore().sidebarFolded || useCameraSettingsStore().isDriverMode)
|
||||
)
|
||||
? 9
|
||||
: 8;
|
||||
const interactiveCols = computed(() =>
|
||||
(getCurrentInstance()?.proxy.$vuetify.breakpoint.mdAndDown || false) &&
|
||||
(!useStateStore().sidebarFolded || useCameraSettingsStore().isDriverMode)
|
||||
? 9
|
||||
: 8
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -131,12 +130,32 @@ const interactiveCols = computed(
|
||||
tooltip="Controls blue automatic white balance gain, which affects how the camera captures colors in different conditions"
|
||||
@input="(args) => useCameraSettingsStore().changeCurrentPipelineSetting({ cameraBlueGain: args }, false)"
|
||||
/>
|
||||
<!-- Disable camera orientation as stop gap for Issue 1084 until calibration data gets rotated. https://github.com/PhotonVision/photonvision/issues/1084 -->
|
||||
<v-banner
|
||||
v-show="
|
||||
useCameraSettingsStore().isCurrentVideoFormatCalibrated &&
|
||||
useCameraSettingsStore().currentPipelineSettings.inputImageRotationMode != 0
|
||||
"
|
||||
rounded
|
||||
dark
|
||||
color="red"
|
||||
text-color="white"
|
||||
class="mt-3"
|
||||
icon="mdi-alert-circle-outline"
|
||||
>
|
||||
Warning! A known bug affects rotation of calibrated camera. Turn off rotation here and rotate using
|
||||
cameraToRobotTransform in your robot code.
|
||||
</v-banner>
|
||||
<pv-select
|
||||
v-model="useCameraSettingsStore().currentPipelineSettings.inputImageRotationMode"
|
||||
label="Orientation"
|
||||
tooltip="Rotates the camera stream"
|
||||
tooltip="Rotates the camera stream. Rotation not available when camera has been calibrated."
|
||||
:items="cameraRotations"
|
||||
:select-cols="interactiveCols"
|
||||
:disabled="
|
||||
useCameraSettingsStore().isCurrentVideoFormatCalibrated &&
|
||||
useCameraSettingsStore().currentPipelineSettings.inputImageRotationMode == 0
|
||||
"
|
||||
@input="(args) => useCameraSettingsStore().changeCurrentPipelineSetting({ inputImageRotationMode: args }, false)"
|
||||
/>
|
||||
<pv-select
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
<script setup lang="ts">
|
||||
import { useCameraSettingsStore } from "@/stores/settings/CameraSettingsStore";
|
||||
import { type ActivePipelineSettings, PipelineType } from "@/types/PipelineTypes";
|
||||
import PvSlider from "@/components/common/pv-slider.vue";
|
||||
import { computed, getCurrentInstance } from "vue";
|
||||
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
|
||||
// Defer reference to store access method
|
||||
const currentPipelineSettings = computed<ActivePipelineSettings>(
|
||||
() => useCameraSettingsStore().currentPipelineSettings
|
||||
);
|
||||
|
||||
// TODO fix pv-range-slider so that store access doesn't need to be deferred
|
||||
const contourArea = computed<[number, number]>({
|
||||
get: () => Object.values(useCameraSettingsStore().currentPipelineSettings.contourArea) as [number, number],
|
||||
set: (v) => (useCameraSettingsStore().currentPipelineSettings.contourArea = v)
|
||||
});
|
||||
const contourRatio = computed<[number, number]>({
|
||||
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>
|
||||
|
||||
<template>
|
||||
<div v-if="currentPipelineSettings.pipelineType === PipelineType.ObjectDetection">
|
||||
<pv-slider
|
||||
v-model="currentPipelineSettings.confidence"
|
||||
class="pt-2"
|
||||
:slider-cols="interactiveCols"
|
||||
label="Confidence"
|
||||
tooltip="The minimum confidence for a detection to be considered valid. Bigger numbers mean fewer but more probable detections are allowed through."
|
||||
:min="0"
|
||||
:max="1"
|
||||
:step="0.01"
|
||||
@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>
|
||||
</template>
|
||||
@@ -46,13 +46,12 @@ const currentPipelineSettings = computed<ActivePipelineSettings>(
|
||||
() => useCameraSettingsStore().currentPipelineSettings
|
||||
);
|
||||
|
||||
const interactiveCols = computed(
|
||||
() =>
|
||||
(getCurrentInstance()?.proxy.$vuetify.breakpoint.mdAndDown || false) &&
|
||||
(!useStateStore().sidebarFolded || useCameraSettingsStore().isDriverMode)
|
||||
)
|
||||
? 9
|
||||
: 8;
|
||||
const interactiveCols = computed(() =>
|
||||
(getCurrentInstance()?.proxy.$vuetify.breakpoint.mdAndDown || false) &&
|
||||
(!useStateStore().sidebarFolded || useCameraSettingsStore().isDriverMode)
|
||||
? 9
|
||||
: 8
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
@@ -6,13 +6,12 @@ import PvSlider from "@/components/common/pv-slider.vue";
|
||||
import { computed, getCurrentInstance } from "vue";
|
||||
import { useStateStore } from "@/stores/StateStore";
|
||||
|
||||
const interactiveCols = computed(
|
||||
() =>
|
||||
(getCurrentInstance()?.proxy.$vuetify.breakpoint.mdAndDown || false) &&
|
||||
(!useStateStore().sidebarFolded || useCameraSettingsStore().isDriverMode)
|
||||
)
|
||||
? 9
|
||||
: 8;
|
||||
const interactiveCols = computed(() =>
|
||||
(getCurrentInstance()?.proxy.$vuetify.breakpoint.mdAndDown || false) &&
|
||||
(!useStateStore().sidebarFolded || useCameraSettingsStore().isDriverMode)
|
||||
? 9
|
||||
: 8
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
@@ -48,6 +48,10 @@ const resetCurrentBuffer = () => {
|
||||
>
|
||||
Fiducial ID
|
||||
</th>
|
||||
<template v-if="currentPipelineSettings.pipelineType === PipelineType.ObjectDetection">
|
||||
<th class="text-center white--text">Class</th>
|
||||
<th class="text-center white--text">Confidence</th>
|
||||
</template>
|
||||
<template v-if="!useCameraSettingsStore().currentPipelineSettings.solvePNPEnabled">
|
||||
<th class="text-center white--text">Pitch θ°</th>
|
||||
<th class="text-center white--text">Yaw θ°</th>
|
||||
@@ -85,6 +89,18 @@ const resetCurrentBuffer = () => {
|
||||
>
|
||||
{{ target.fiducialId }}
|
||||
</td>
|
||||
<td
|
||||
v-if="currentPipelineSettings.pipelineType === PipelineType.ObjectDetection"
|
||||
class="text-center white--text"
|
||||
>
|
||||
{{ useStateStore().currentPipelineResults?.classNames[target.classId] }}
|
||||
</td>
|
||||
<td
|
||||
v-if="currentPipelineSettings.pipelineType === PipelineType.ObjectDetection"
|
||||
class="text-center white--text"
|
||||
>
|
||||
{{ target.confidence.toFixed(2) }}
|
||||
</td>
|
||||
<template v-if="!useCameraSettingsStore().currentPipelineSettings.solvePNPEnabled">
|
||||
<td class="text-center">{{ target.pitch.toFixed(2) }}°</td>
|
||||
<td class="text-center">{{ target.yaw.toFixed(2) }}°</td>
|
||||
|
||||
@@ -124,13 +124,12 @@ onBeforeUnmount(() => {
|
||||
cameraStream.removeEventListener("click", handleStreamClick);
|
||||
});
|
||||
|
||||
const interactiveCols = computed(
|
||||
() =>
|
||||
(getCurrentInstance()?.proxy.$vuetify.breakpoint.mdAndDown || false) &&
|
||||
(!useStateStore().sidebarFolded || useCameraSettingsStore().isDriverMode)
|
||||
)
|
||||
? 9
|
||||
: 8;
|
||||
const interactiveCols = computed(() =>
|
||||
(getCurrentInstance()?.proxy.$vuetify.breakpoint.mdAndDown || false) &&
|
||||
(!useStateStore().sidebarFolded || useCameraSettingsStore().isDriverMode)
|
||||
? 9
|
||||
: 8
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
@@ -27,42 +27,54 @@ const generalMetrics = computed<MetricItem[]>(() => [
|
||||
value: useSettingsStore().general.gpuAcceleration || "Unknown"
|
||||
}
|
||||
]);
|
||||
const platformMetrics = computed<MetricItem[]>(() => [
|
||||
{
|
||||
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 Memory Usage",
|
||||
value:
|
||||
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
|
||||
? "Unknown"
|
||||
: `${useSettingsStore().metrics.gpuMemUtil}MB of ${useSettingsStore().metrics.gpuMem}MB`
|
||||
},
|
||||
{
|
||||
header: "CPU Throttling",
|
||||
value: useSettingsStore().metrics.cpuThr || "Unknown"
|
||||
},
|
||||
{
|
||||
header: "CPU Uptime",
|
||||
value: useSettingsStore().metrics.cpuUptime || "Unknown"
|
||||
},
|
||||
{
|
||||
header: "Disk Usage",
|
||||
value: useSettingsStore().metrics.diskUtilPct || "Unknown"
|
||||
|
||||
const platformMetrics = computed<MetricItem[]>(() => {
|
||||
const stats = [
|
||||
{
|
||||
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 Memory Usage",
|
||||
value:
|
||||
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
|
||||
? "Unknown"
|
||||
: `${useSettingsStore().metrics.gpuMemUtil}MB of ${useSettingsStore().metrics.gpuMem}MB`
|
||||
},
|
||||
{
|
||||
header: "CPU Throttling",
|
||||
value: useSettingsStore().metrics.cpuThr || "Unknown"
|
||||
},
|
||||
{
|
||||
header: "CPU Uptime",
|
||||
value: useSettingsStore().metrics.cpuUptime || "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 fetchMetrics = () => {
|
||||
|
||||
@@ -5,12 +5,11 @@ import PvInput from "@/components/common/pv-input.vue";
|
||||
import PvRadio from "@/components/common/pv-radio.vue";
|
||||
import PvSwitch from "@/components/common/pv-switch.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";
|
||||
|
||||
// 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 = () => {
|
||||
tempSettingsStruct.value = Object.assign({}, useSettingsStore().network);
|
||||
};
|
||||
@@ -58,10 +57,10 @@ const settingsHaveChanged = (): boolean => {
|
||||
a.runNTServer !== b.runNTServer ||
|
||||
a.shouldManage !== b.shouldManage ||
|
||||
a.shouldPublishProto !== b.shouldPublishProto ||
|
||||
a.canManage !== b.canManage ||
|
||||
a.networkManagerIface !== b.networkManagerIface ||
|
||||
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 || "",
|
||||
shouldManage: tempSettingsStruct.value.shouldManage,
|
||||
shouldPublishProto: tempSettingsStruct.value.shouldPublishProto,
|
||||
matchCamerasOnlyByPath: tempSettingsStruct.value.matchCamerasOnlyByPath,
|
||||
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
|
||||
useSettingsStore().network = Object.assign({}, tempSettingsStruct.value);
|
||||
useSettingsStore().network = {
|
||||
...useSettingsStore().network,
|
||||
...Object.assign({}, tempSettingsStruct.value)
|
||||
};
|
||||
})
|
||||
.catch((error) => {
|
||||
resetTempSettingsStruct();
|
||||
@@ -136,6 +139,8 @@ watchEffect(() => {
|
||||
|
||||
<template>
|
||||
<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>
|
||||
<div class="ml-5">
|
||||
<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.
|
||||
</v-banner>
|
||||
<pv-radio
|
||||
v-show="!useSettingsStore().network.networkingDisabled"
|
||||
v-model="tempSettingsStruct.connectionType"
|
||||
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."
|
||||
:input-cols="12 - 4"
|
||||
:list="['DHCP', 'Static']"
|
||||
:disabled="!(tempSettingsStruct.shouldManage && tempSettingsStruct.canManage)"
|
||||
:disabled="
|
||||
!tempSettingsStruct.shouldManage ||
|
||||
!useSettingsStore().network.canManage ||
|
||||
useSettingsStore().network.networkingDisabled
|
||||
"
|
||||
/>
|
||||
<pv-input
|
||||
v-show="!useSettingsStore().network.networkingDisabled"
|
||||
v-if="tempSettingsStruct.connectionType === NetworkConnectionType.Static"
|
||||
v-model="tempSettingsStruct.staticIp"
|
||||
:input-cols="12 - 4"
|
||||
label="Static IP"
|
||||
:rules="[(v) => isValidIPv4(v) || 'Invalid IPv4 address']"
|
||||
:disabled="!(tempSettingsStruct.shouldManage && tempSettingsStruct.canManage)"
|
||||
:disabled="
|
||||
!tempSettingsStruct.shouldManage ||
|
||||
!useSettingsStore().network.canManage ||
|
||||
useSettingsStore().network.networkingDisabled
|
||||
"
|
||||
/>
|
||||
<pv-input
|
||||
v-show="!useSettingsStore().network.networkingDisabled"
|
||||
v-model="tempSettingsStruct.hostname"
|
||||
label="Hostname"
|
||||
:input-cols="12 - 4"
|
||||
: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" />
|
||||
<span style="font-weight: 700">Advanced Networking</span>
|
||||
<pv-switch
|
||||
v-show="!useSettingsStore().network.networkingDisabled"
|
||||
v-model="tempSettingsStruct.shouldManage"
|
||||
:disabled="!tempSettingsStruct.canManage"
|
||||
:disabled="!useSettingsStore().network.canManage || useSettingsStore().network.networkingDisabled"
|
||||
label="Manage Device Networking"
|
||||
tooltip="If enabled, Photon will manage device hostname and network settings."
|
||||
:label-cols="4"
|
||||
class="pt-2"
|
||||
/>
|
||||
<pv-select
|
||||
v-show="!useSettingsStore().network.networkingDisabled"
|
||||
v-model="currentNetworkInterfaceIndex"
|
||||
label="NetworkManager interface"
|
||||
:disabled="!(tempSettingsStruct.shouldManage && tempSettingsStruct.canManage)"
|
||||
:disabled="
|
||||
!tempSettingsStruct.shouldManage ||
|
||||
!useSettingsStore().network.canManage ||
|
||||
useSettingsStore().network.networkingDisabled
|
||||
"
|
||||
:select-cols="12 - 4"
|
||||
tooltip="Name of the interface PhotonVision should manage the IP address of"
|
||||
:items="useSettingsStore().networkInterfaceNames"
|
||||
@@ -206,7 +232,8 @@ watchEffect(() => {
|
||||
v-show="
|
||||
!useSettingsStore().networkInterfaceNames.length &&
|
||||
tempSettingsStruct.shouldManage &&
|
||||
tempSettingsStruct.canManage
|
||||
useSettingsStore().network.canManage &&
|
||||
!useSettingsStore().network.networkingDisabled
|
||||
"
|
||||
rounded
|
||||
color="red"
|
||||
@@ -231,6 +258,9 @@ watchEffect(() => {
|
||||
>
|
||||
This mode is intended for debugging; it should be off for proper usage. PhotonLib will NOT work!
|
||||
</v-banner>
|
||||
|
||||
<v-divider />
|
||||
<v-card-title>Miscellaneous</v-card-title>
|
||||
<pv-switch
|
||||
v-model="tempSettingsStruct.shouldPublishProto"
|
||||
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.
|
||||
</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-btn
|
||||
color="accent"
|
||||
|
||||
@@ -236,6 +236,13 @@ export const useCameraSettingsStore = defineStore("cameraSettings", {
|
||||
}
|
||||
useStateStore().websocket?.send(payload, true);
|
||||
},
|
||||
setDriverMode(isDriverMode: boolean, cameraIndex: number = useStateStore().currentCameraIndex) {
|
||||
const payload = {
|
||||
driverMode: isDriverMode,
|
||||
cameraIndex: cameraIndex
|
||||
};
|
||||
useStateStore().websocket?.send(payload, true);
|
||||
},
|
||||
/**
|
||||
* Change the currently selected pipeline of the provided camera.
|
||||
*
|
||||
@@ -416,6 +423,23 @@ export const useCameraSettingsStore = defineStore("cameraSettings", {
|
||||
cameraIndex: number = useStateStore().currentCameraIndex
|
||||
): CameraCalibrationResult | undefined {
|
||||
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;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -27,7 +27,8 @@ export const useSettingsStore = defineStore("settings", {
|
||||
gpuAcceleration: undefined,
|
||||
hardwareModel: undefined,
|
||||
hardwarePlatform: undefined,
|
||||
mrCalWorking: true
|
||||
mrCalWorking: true,
|
||||
rknnSupported: false
|
||||
},
|
||||
network: {
|
||||
ntServerAddress: "",
|
||||
@@ -43,7 +44,9 @@ export const useSettingsStore = defineStore("settings", {
|
||||
connName: "Example Wired Connection",
|
||||
devName: "eth0"
|
||||
}
|
||||
]
|
||||
],
|
||||
networkingDisabled: false,
|
||||
matchCamerasOnlyByPath: false
|
||||
},
|
||||
lighting: {
|
||||
supported: true,
|
||||
@@ -58,7 +61,8 @@ export const useSettingsStore = defineStore("settings", {
|
||||
gpuMemUtil: undefined,
|
||||
cpuThr: undefined,
|
||||
cpuUptime: undefined,
|
||||
diskUtilPct: undefined
|
||||
diskUtilPct: undefined,
|
||||
npuUsage: undefined
|
||||
},
|
||||
currentFieldLayout: {
|
||||
field: {
|
||||
@@ -90,7 +94,8 @@ export const useSettingsStore = defineStore("settings", {
|
||||
gpuMemUtil: data.gpuMemUtil || undefined,
|
||||
cpuThr: data.cpuThr || undefined,
|
||||
cpuUptime: data.cpuUptime || undefined,
|
||||
diskUtilPct: data.diskUtilPct || undefined
|
||||
diskUtilPct: data.diskUtilPct || undefined,
|
||||
npuUsage: data.npuUsage || undefined
|
||||
};
|
||||
},
|
||||
updateGeneralSettingsFromWebsocket(data: WebsocketSettingsUpdate) {
|
||||
@@ -99,7 +104,8 @@ export const useSettingsStore = defineStore("settings", {
|
||||
hardwareModel: data.general.hardwareModel || undefined,
|
||||
hardwarePlatform: data.general.hardwarePlatform || undefined,
|
||||
gpuAcceleration: data.general.gpuAcceleration || undefined,
|
||||
mrCalWorking: data.general.mrCalWorking
|
||||
mrCalWorking: data.general.mrCalWorking,
|
||||
rknnSupported: data.general.rknnSupported
|
||||
};
|
||||
this.lighting = data.lighting;
|
||||
this.network = data.networkSettings;
|
||||
|
||||
@@ -54,6 +54,8 @@ export interface PhotonTarget {
|
||||
ambiguity: number;
|
||||
// -1 if not set
|
||||
fiducialId: number;
|
||||
confidence: number;
|
||||
classId: number;
|
||||
// undefined if 3d isn't enabled
|
||||
pose?: Transform3d;
|
||||
}
|
||||
@@ -70,4 +72,6 @@ export interface PipelineResult {
|
||||
targets: PhotonTarget[];
|
||||
// undefined if multitag failed or non-tag pipeline
|
||||
multitagResult?: MultitagResult;
|
||||
// Object detection class names -- empty if not doing object detection
|
||||
classNames: string[];
|
||||
}
|
||||
|
||||
@@ -5,7 +5,8 @@ export enum PipelineType {
|
||||
Reflective = 2,
|
||||
ColoredShape = 3,
|
||||
AprilTag = 4,
|
||||
Aruco = 5
|
||||
Aruco = 5,
|
||||
ObjectDetection = 6
|
||||
}
|
||||
|
||||
export enum AprilTagFamily {
|
||||
@@ -281,14 +282,39 @@ export const DefaultArucoPipelineSettings: ArucoPipelineSettings = {
|
||||
doSingleTargetAlways: false
|
||||
};
|
||||
|
||||
export interface ObjectDetectionPipelineSettings extends PipelineSettings {
|
||||
pipelineType: PipelineType.ObjectDetection;
|
||||
confidence: number;
|
||||
nms: number;
|
||||
box_thresh: number;
|
||||
}
|
||||
export type ConfigurableObjectDetectionPipelineSettings = Partial<
|
||||
Omit<ObjectDetectionPipelineSettings, "pipelineType">
|
||||
> &
|
||||
ConfigurablePipelineSettings;
|
||||
export const DefaultObjectDetectionPipelineSettings: ObjectDetectionPipelineSettings = {
|
||||
...DefaultPipelineSettings,
|
||||
pipelineType: PipelineType.ObjectDetection,
|
||||
cameraGain: 20,
|
||||
targetModel: TargetModel.InfiniteRechargeHighGoalOuter,
|
||||
ledMode: true,
|
||||
outputShowMultipleTargets: false,
|
||||
cameraExposure: 6,
|
||||
confidence: 0.9,
|
||||
nms: 0.45,
|
||||
box_thresh: 0.25
|
||||
};
|
||||
|
||||
export type ActivePipelineSettings =
|
||||
| ReflectivePipelineSettings
|
||||
| ColoredShapePipelineSettings
|
||||
| AprilTagPipelineSettings
|
||||
| ArucoPipelineSettings;
|
||||
| ArucoPipelineSettings
|
||||
| ObjectDetectionPipelineSettings;
|
||||
|
||||
export type ActiveConfigurablePipelineSettings =
|
||||
| ConfigurableReflectivePipelineSettings
|
||||
| ConfigurableColoredShapePipelineSettings
|
||||
| ConfigurableAprilTagPipelineSettings
|
||||
| ConfigurableArucoPipelineSettings;
|
||||
| ConfigurableArucoPipelineSettings
|
||||
| ConfigurableObjectDetectionPipelineSettings;
|
||||
|
||||
@@ -7,6 +7,7 @@ export interface GeneralSettings {
|
||||
hardwareModel?: string;
|
||||
hardwarePlatform?: string;
|
||||
mrCalWorking: boolean;
|
||||
rknnSupported: boolean;
|
||||
}
|
||||
|
||||
export interface MetricData {
|
||||
@@ -19,6 +20,7 @@ export interface MetricData {
|
||||
cpuThr?: string;
|
||||
cpuUptime?: string;
|
||||
diskUtilPct?: string;
|
||||
npuUsage?: string;
|
||||
}
|
||||
|
||||
export enum NetworkConnectionType {
|
||||
@@ -44,9 +46,14 @@ export interface NetworkSettings {
|
||||
setStaticCommand?: string;
|
||||
setDHCPcommand?: string;
|
||||
networkInterfaceNames: NetworkInterfaceType[];
|
||||
networkingDisabled: boolean;
|
||||
matchCamerasOnlyByPath: boolean;
|
||||
}
|
||||
|
||||
export type ConfigurableNetworkSettings = Omit<NetworkSettings, "canManage" | "networkInterfaceNames">;
|
||||
export type ConfigurableNetworkSettings = Omit<
|
||||
NetworkSettings,
|
||||
"canManage" | "networkInterfaceNames" | "networkingDisabled"
|
||||
>;
|
||||
|
||||
export interface LightingSettings {
|
||||
supported: boolean;
|
||||
@@ -133,6 +140,9 @@ export interface CameraCalibrationResult {
|
||||
distCoeffs: JsonMatOfDouble;
|
||||
observations: BoardObservation[];
|
||||
calobjectWarp?: number[];
|
||||
// We might have to omit observations for bandwith, so backend will send us this
|
||||
numSnapshots: number;
|
||||
meanErrors: number[];
|
||||
}
|
||||
|
||||
export enum ValidQuirks {
|
||||
@@ -250,7 +260,9 @@ export const PlaceholderCameraSettings: CameraSettings = {
|
||||
snapshotName: "img0.png",
|
||||
snapshotData: { rows: 480, cols: 640, type: CvType.CV_8U, data: "" }
|
||||
}
|
||||
]
|
||||
],
|
||||
numSnapshots: 1,
|
||||
meanErrors: [123.45]
|
||||
}
|
||||
],
|
||||
pipelineNicknames: ["Placeholder Pipeline"],
|
||||
|
||||
@@ -101,5 +101,6 @@ export enum WebsocketPipelineType {
|
||||
Reflective = 0,
|
||||
ColoredShape = 1,
|
||||
AprilTag = 2,
|
||||
Aruco = 3
|
||||
Aruco = 3,
|
||||
ObjectDetection = 4
|
||||
}
|
||||
|
||||
@@ -10,25 +10,22 @@ export default defineConfig({
|
||||
plugins: [
|
||||
Vue2(),
|
||||
Components({
|
||||
resolvers: [
|
||||
VuetifyResolver()
|
||||
],
|
||||
resolvers: [VuetifyResolver()],
|
||||
dts: true,
|
||||
transformer: "vue2",
|
||||
types: [{
|
||||
from: "vue-router",
|
||||
names: ["RouterLink", "RouterView"]
|
||||
}],
|
||||
types: [
|
||||
{
|
||||
from: "vue-router",
|
||||
names: ["RouterLink", "RouterView"]
|
||||
}
|
||||
],
|
||||
version: 2.7
|
||||
})
|
||||
],
|
||||
css: {
|
||||
preprocessorOptions: {
|
||||
sass: {
|
||||
additionalData: [
|
||||
"@import \"@/assets/styles/variables.scss\"",
|
||||
""
|
||||
].join("\n")
|
||||
additionalData: ["@import \"@/assets/styles/variables.scss\"", ""].join("\n")
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1,7 +1,31 @@
|
||||
plugins {
|
||||
id 'edu.wpi.first.WpilibTools' version '1.3.0'
|
||||
}
|
||||
|
||||
import java.nio.file.Path
|
||||
|
||||
apply from: "${rootDir}/shared/common.gradle"
|
||||
|
||||
wpilibTools.deps.wpilibVersion = wpi.versions.wpilibVersion.get()
|
||||
|
||||
def nativeConfigName = 'wpilibNatives'
|
||||
def nativeConfig = configurations.create(nativeConfigName)
|
||||
|
||||
def nativeTasks = wpilibTools.createExtractionTasks {
|
||||
configurationName = nativeConfigName
|
||||
}
|
||||
|
||||
nativeTasks.addToSourceSetResources(sourceSets.main)
|
||||
|
||||
nativeConfig.dependencies.add wpilibTools.deps.wpilib("wpimath")
|
||||
nativeConfig.dependencies.add wpilibTools.deps.wpilib("wpinet")
|
||||
nativeConfig.dependencies.add wpilibTools.deps.wpilib("wpiutil")
|
||||
nativeConfig.dependencies.add wpilibTools.deps.wpilib("ntcore")
|
||||
nativeConfig.dependencies.add wpilibTools.deps.wpilib("cscore")
|
||||
nativeConfig.dependencies.add wpilibTools.deps.wpilib("apriltag")
|
||||
nativeConfig.dependencies.add wpilibTools.deps.wpilib("hal")
|
||||
nativeConfig.dependencies.add wpilibTools.deps.wpilibOpenCv("frc" + wpi.frcYear.get(), wpi.versions.opencvVersion.get())
|
||||
|
||||
dependencies {
|
||||
// JOGL stuff (currently we only distribute for aarch64, which is Pi 4)
|
||||
implementation "org.jogamp.gluegen:gluegen-rt:$joglVersion"
|
||||
@@ -13,7 +37,8 @@ dependencies {
|
||||
implementation 'org.zeroturnaround:zt-zip:1.14'
|
||||
|
||||
implementation "org.xerial:sqlite-jdbc:3.41.0.0"
|
||||
|
||||
implementation "org.photonvision:rknn_jni-jni:$rknnVersion:linuxarm64"
|
||||
implementation "org.photonvision:rknn_jni-java:$rknnVersion"
|
||||
implementation "org.photonvision:photon-libcamera-gl-driver-jni:$photonGlDriverLibVersion:linuxarm64"
|
||||
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.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import org.photonvision.common.logging.LogGroup;
|
||||
import org.photonvision.common.logging.Logger;
|
||||
import org.photonvision.vision.calibration.CameraCalibrationCoefficients;
|
||||
@@ -51,6 +52,12 @@ public class CameraConfiguration {
|
||||
|
||||
@JsonIgnore public String[] otherPaths = {};
|
||||
|
||||
@JsonProperty("usbVID")
|
||||
public int usbVID = -1;
|
||||
|
||||
@JsonProperty("usbPID")
|
||||
public int usbPID = -1;
|
||||
|
||||
public CameraType cameraType = CameraType.UsbCamera;
|
||||
public double FOV = 70;
|
||||
public final List<CameraCalibrationCoefficients> calibrations;
|
||||
@@ -98,7 +105,9 @@ public class CameraConfiguration {
|
||||
@JsonProperty("cameraType") CameraType cameraType,
|
||||
@JsonProperty("cameraQuirks") QuirkyCamera cameraQuirks,
|
||||
@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.uniqueName = uniqueName;
|
||||
this.nickname = nickname;
|
||||
@@ -108,6 +117,8 @@ public class CameraConfiguration {
|
||||
this.cameraQuirks = cameraQuirks;
|
||||
this.calibrations = calibrations != null ? calibrations : new ArrayList<>();
|
||||
this.currentPipelineIndex = currentPipelineIndex;
|
||||
this.usbPID = usbPID;
|
||||
this.usbVID = usbVID;
|
||||
|
||||
logger.debug(
|
||||
"Creating camera configuration for "
|
||||
@@ -156,6 +167,17 @@ public class CameraConfiguration {
|
||||
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
|
||||
public String toString() {
|
||||
return "CameraConfiguration [baseName="
|
||||
|
||||
@@ -50,6 +50,10 @@ public class ConfigManager {
|
||||
private final Thread settingsSaveThread;
|
||||
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 {
|
||||
SQL,
|
||||
LEGACY,
|
||||
@@ -296,4 +300,26 @@ public class ConfigManager {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Get (and create if not present) the subfolder where ML models are stored */
|
||||
public File getModelsDirectory() {
|
||||
var ret = new File(configDirectoryFile, "models");
|
||||
if (!ret.exists()) ret.mkdirs();
|
||||
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 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
|
||||
public String toString() {
|
||||
return "HardwareSettings [ledBrightnessPercentage=" + ledBrightnessPercentage + "]";
|
||||
|
||||
@@ -39,6 +39,14 @@ public class NetworkConfig {
|
||||
public boolean shouldManage;
|
||||
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_IP_STRING = "${ipaddr}";
|
||||
|
||||
@@ -76,7 +84,8 @@ public class NetworkConfig {
|
||||
@JsonProperty("shouldPublishProto") boolean shouldPublishProto,
|
||||
@JsonProperty("networkManagerIface") String networkManagerIface,
|
||||
@JsonProperty("setStaticCommand") String setStaticCommand,
|
||||
@JsonProperty("setDHCPcommand") String setDHCPcommand) {
|
||||
@JsonProperty("setDHCPcommand") String setDHCPcommand,
|
||||
@JsonProperty("matchCamerasOnlyByPath") boolean matchCamerasOnlyByPath) {
|
||||
this.ntServerAddress = ntServerAddress;
|
||||
this.connectionType = connectionType;
|
||||
this.staticIp = staticIp;
|
||||
@@ -86,6 +95,7 @@ public class NetworkConfig {
|
||||
this.networkManagerIface = networkManagerIface;
|
||||
this.setStaticCommand = setStaticCommand;
|
||||
this.setDHCPcommand = setDHCPcommand;
|
||||
this.matchCamerasOnlyByPath = matchCamerasOnlyByPath;
|
||||
setShouldManage(shouldManage);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
/*
|
||||
* 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.configuration;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.List;
|
||||
import org.photonvision.common.logging.LogGroup;
|
||||
import org.photonvision.common.logging.Logger;
|
||||
import org.photonvision.rknn.RknnJNI;
|
||||
|
||||
public class NeuralNetworkModelManager {
|
||||
private static NeuralNetworkModelManager INSTANCE;
|
||||
private static final Logger logger = new Logger(NeuralNetworkModelManager.class, LogGroup.Config);
|
||||
|
||||
private final String MODEL_NAME = "note-640-640-yolov5s.rknn";
|
||||
private final RknnJNI.ModelVersion modelVersion = RknnJNI.ModelVersion.YOLO_V5;
|
||||
private File defaultModelFile;
|
||||
private List<String> labels;
|
||||
|
||||
public static NeuralNetworkModelManager getInstance() {
|
||||
if (INSTANCE == null) {
|
||||
INSTANCE = new NeuralNetworkModelManager();
|
||||
}
|
||||
return INSTANCE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform initial setup and extract default model from JAR to the filesystem
|
||||
*
|
||||
* @param modelsFolder Where models live
|
||||
*/
|
||||
public void initialize(File modelsFolder) {
|
||||
var modelResourcePath = "/models/" + MODEL_NAME;
|
||||
this.defaultModelFile = new File(modelsFolder, MODEL_NAME);
|
||||
extractResource(modelResourcePath, defaultModelFile);
|
||||
|
||||
File labelsFile = new File(modelsFolder, "labels_v5.txt");
|
||||
var labelResourcePath = "/models/" + labelsFile.getName();
|
||||
extractResource(labelResourcePath, labelsFile);
|
||||
|
||||
try {
|
||||
labels = Files.readAllLines(Paths.get(labelsFile.getPath()));
|
||||
} catch (IOException e) {
|
||||
logger.error("Error reading labels.txt", e);
|
||||
}
|
||||
}
|
||||
|
||||
private void extractResource(String resourcePath, File outputFile) {
|
||||
try (var in = NeuralNetworkModelManager.class.getResourceAsStream(resourcePath)) {
|
||||
if (in == null) {
|
||||
logger.error("Failed to find jar resource at " + resourcePath);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!outputFile.exists()) {
|
||||
try (FileOutputStream fos = new FileOutputStream(outputFile)) {
|
||||
int read = -1;
|
||||
byte[] buffer = new byte[1024];
|
||||
while ((read = in.read(buffer)) != -1) {
|
||||
fos.write(buffer, 0, read);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
logger.error("Error extracting resource to " + outputFile.toPath().toString(), e);
|
||||
}
|
||||
} else {
|
||||
logger.info(
|
||||
"File " + outputFile.toPath().toString() + " already exists. Skipping extraction.");
|
||||
}
|
||||
} catch (IOException e) {
|
||||
logger.error("Error finding jar resource " + resourcePath, e);
|
||||
}
|
||||
}
|
||||
|
||||
public File getDefaultRknnModel() {
|
||||
return defaultModelFile;
|
||||
}
|
||||
|
||||
public List<String> getLabels() {
|
||||
return labels;
|
||||
}
|
||||
|
||||
public RknnJNI.ModelVersion getModelVersion() {
|
||||
return modelVersion;
|
||||
}
|
||||
}
|
||||
@@ -25,11 +25,13 @@ import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
import org.photonvision.PhotonVersion;
|
||||
import org.photonvision.common.hardware.Platform;
|
||||
import org.photonvision.common.networking.NetworkManager;
|
||||
import org.photonvision.common.networking.NetworkUtils;
|
||||
import org.photonvision.common.util.SerializationUtils;
|
||||
import org.photonvision.jni.RknnDetectorJNI;
|
||||
import org.photonvision.mrcal.MrCalJNILoader;
|
||||
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.processes.VisionModule;
|
||||
import org.photonvision.vision.processes.VisionModuleManager;
|
||||
@@ -120,16 +122,10 @@ public class PhotonConfiguration {
|
||||
// Hack active interfaces into networkSettings
|
||||
var netConfigMap = networkConfig.toHashMap();
|
||||
netConfigMap.put("networkInterfaceNames", NetworkUtils.getAllWiredInterfaces());
|
||||
netConfigMap.put("networkingDisabled", NetworkManager.getInstance().networkingIsDisabled);
|
||||
|
||||
settingsSubmap.put("networkSettings", netConfigMap);
|
||||
|
||||
map.put(
|
||||
"cameraSettings",
|
||||
VisionModuleManager.getInstance().getModules().stream()
|
||||
.map(VisionModule::toUICameraConfig)
|
||||
.map(SerializationUtils::objectToHashMap)
|
||||
.collect(Collectors.toList()));
|
||||
|
||||
var lightingConfig = new UILightingConfig();
|
||||
lightingConfig.brightness = hardwareSettings.ledBrightnessPercentage;
|
||||
lightingConfig.supported = !hardwareConfig.ledPins.isEmpty();
|
||||
@@ -142,7 +138,8 @@ public class PhotonConfiguration {
|
||||
LibCameraJNILoader.isSupported()
|
||||
? "Zerocopy Libcamera Working"
|
||||
: ""); // TODO add support for other types of GPU accel
|
||||
generalSubmap.put("mrCalWorking", MrCalJNILoader.isWorking());
|
||||
generalSubmap.put("mrCalWorking", MrCalJNILoader.getInstance().isLoaded());
|
||||
generalSubmap.put("rknnSupported", RknnDetectorJNI.getInstance().isLoaded());
|
||||
generalSubmap.put("hardwareModel", hardwareConfig.deviceName);
|
||||
generalSubmap.put("hardwarePlatform", Platform.getPlatformName());
|
||||
settingsSubmap.put("general", generalSubmap);
|
||||
@@ -177,7 +174,7 @@ public class PhotonConfiguration {
|
||||
public HashMap<Integer, HashMap<String, Object>> videoFormatList;
|
||||
public int outputStreamPort;
|
||||
public int inputStreamPort;
|
||||
public List<CameraCalibrationCoefficients> calibrations;
|
||||
public List<UICameraCalibrationCoefficients> calibrations;
|
||||
public boolean isFovConfigurable = true;
|
||||
public QuirkyCamera cameraQuirks;
|
||||
public boolean isCSICamera;
|
||||
|
||||
@@ -26,6 +26,7 @@ import org.photonvision.common.logging.LogGroup;
|
||||
import org.photonvision.common.logging.Logger;
|
||||
import org.photonvision.common.util.SerializationUtils;
|
||||
import org.photonvision.vision.pipeline.result.CVPipelineResult;
|
||||
import org.photonvision.vision.pipeline.result.CalibrationPipelineResult;
|
||||
|
||||
public class UIDataPublisher implements CVPipelineResultConsumer {
|
||||
private static final Logger logger = new Logger(UIDataPublisher.class, LogGroup.VisionModule);
|
||||
@@ -41,17 +42,24 @@ public class UIDataPublisher implements CVPipelineResultConsumer {
|
||||
public void accept(CVPipelineResult result) {
|
||||
long now = System.currentTimeMillis();
|
||||
|
||||
// only update the UI at 15hz
|
||||
// only update the UI at 10hz
|
||||
if (lastUIResultUpdateTime + 1000.0 / 10.0 > now) return;
|
||||
|
||||
var dataMap = new HashMap<String, Object>();
|
||||
dataMap.put("fps", result.fps);
|
||||
dataMap.put("latency", result.getLatencyMillis());
|
||||
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("classNames", result.objectDetectionClassNames);
|
||||
|
||||
// Only send Multitag Results if they are present, similar to 3d pose
|
||||
if (result.multiTagResult.estimatedPose.isPresent) {
|
||||
|
||||
@@ -145,8 +145,7 @@ public class HardwareManager {
|
||||
logger.info("Shutting down LEDs...");
|
||||
if (visionLED != null) visionLED.setState(false);
|
||||
|
||||
logger.info("Force-flushing settings...");
|
||||
ConfigManager.getInstance().saveToDisk();
|
||||
ConfigManager.getInstance().onJvmExit();
|
||||
}
|
||||
|
||||
public boolean restartDevice() {
|
||||
|
||||
@@ -43,6 +43,7 @@ public enum Platform {
|
||||
true,
|
||||
OSType.LINUX,
|
||||
true), // Raspberry Pi 3/4 with a 64-bit image
|
||||
LINUX_RK3588_64("Linux AARCH 64-bit with RK3588", "linuxarm64", false, OSType.LINUX, true),
|
||||
LINUX_AARCH64(
|
||||
"Linux AARCH64", "linuxarm64", false, OSType.LINUX, true), // Jetson Nano, Jetson TX2
|
||||
|
||||
@@ -94,6 +95,10 @@ public enum Platform {
|
||||
return currentPlatform.osType == OSType.LINUX;
|
||||
}
|
||||
|
||||
public static boolean isRK3588() {
|
||||
return Platform.isOrangePi() || Platform.isCoolPi4b();
|
||||
}
|
||||
|
||||
public static boolean isRaspberryPi() {
|
||||
return currentPlatform.isPi;
|
||||
}
|
||||
@@ -186,7 +191,11 @@ public enum Platform {
|
||||
return LINUX_32;
|
||||
} else if (RuntimeDetector.isArm64()) {
|
||||
// TODO - os detection needed?
|
||||
return LINUX_AARCH64;
|
||||
if (isOrangePi()) {
|
||||
return LINUX_RK3588_64;
|
||||
} else {
|
||||
return LINUX_AARCH64;
|
||||
}
|
||||
} else if (RuntimeDetector.isArm32()) {
|
||||
return LINUX_ARM32;
|
||||
} else {
|
||||
@@ -204,6 +213,14 @@ public enum Platform {
|
||||
return fileHasText("/proc/cpuinfo", "Raspberry Pi");
|
||||
}
|
||||
|
||||
private static boolean isOrangePi() {
|
||||
return fileHasText("/proc/device-tree/model", "Orange Pi 5");
|
||||
}
|
||||
|
||||
private static boolean isCoolPi4b() {
|
||||
return fileHasText("/proc/device-tree/model", "CoolPi 4B");
|
||||
}
|
||||
|
||||
private static boolean isJetsonSBC() {
|
||||
// https://forums.developer.nvidia.com/t/how-to-recognize-jetson-nano-device/146624
|
||||
return fileHasText("/proc/device-tree/model", "NVIDIA Jetson");
|
||||
|
||||
@@ -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.LinuxCmds;
|
||||
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.Logger;
|
||||
import org.photonvision.common.util.ShellExec;
|
||||
@@ -44,6 +45,8 @@ public class MetricsManager {
|
||||
cmds = new FileCmds();
|
||||
} else if (Platform.isRaspberryPi()) {
|
||||
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()) {
|
||||
cmds = new LinuxCmds(); // Linux/Unix platforms assume a nominal command set
|
||||
} else {
|
||||
@@ -89,6 +92,10 @@ public class MetricsManager {
|
||||
return safeExecute(cmds.cpuThrottleReasonCmd);
|
||||
}
|
||||
|
||||
public String getNpuUsage() {
|
||||
return safeExecute(cmds.npuUsageCommand);
|
||||
}
|
||||
|
||||
private String gpuMemSave = null;
|
||||
|
||||
public String getGPUMemorySplit() {
|
||||
@@ -125,6 +132,7 @@ public class MetricsManager {
|
||||
metrics.put("ramUtil", this.getUsedRam());
|
||||
metrics.put("gpuMemUtil", this.getMallocedMemory());
|
||||
metrics.put("diskUtilPct", this.getUsedDiskPct());
|
||||
metrics.put("npuUsage", this.getNpuUsage());
|
||||
|
||||
DataChangeService.getInstance().publishEvent(OutgoingUIEvent.wrappedOf("metrics", metrics));
|
||||
}
|
||||
|
||||
@@ -29,6 +29,8 @@ public class CmdBase {
|
||||
// GPU
|
||||
public String gpuMemoryCommand = "";
|
||||
public String gpuMemUsageCommand = "";
|
||||
// NPU
|
||||
public String npuUsageCommand = "";
|
||||
// RAM
|
||||
public String ramUsageCommand = "";
|
||||
// Disk
|
||||
|
||||
@@ -22,7 +22,7 @@ import org.photonvision.common.configuration.HardwareConfig;
|
||||
public class LinuxCmds extends CmdBase {
|
||||
public void initCmds(HardwareConfig config) {
|
||||
// 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
|
||||
|
||||
|
||||
@@ -25,7 +25,6 @@ public class PiCmds extends LinuxCmds {
|
||||
super.initCmds(config);
|
||||
|
||||
// CPU
|
||||
cpuMemoryCommand = "free -m | awk 'FNR == 2 {print $2}'";
|
||||
cpuTemperatureCommand = "sed 's/.\\{3\\}$/.&/' /sys/class/thermal/thermal_zone0/temp";
|
||||
cpuThrottleReasonCmd =
|
||||
"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/ *$//'";
|
||||
}
|
||||
}
|
||||
@@ -23,5 +23,6 @@ public enum LogGroup {
|
||||
VisionModule,
|
||||
Data,
|
||||
General,
|
||||
Config
|
||||
Config,
|
||||
CSCore,
|
||||
}
|
||||
|
||||
@@ -99,6 +99,7 @@ public class Logger {
|
||||
levelMap.put(LogGroup.Data, LogLevel.INFO);
|
||||
levelMap.put(LogGroup.VisionModule, LogLevel.INFO);
|
||||
levelMap.put(LogGroup.Config, LogLevel.INFO);
|
||||
levelMap.put(LogGroup.CSCore, LogLevel.TRACE);
|
||||
}
|
||||
|
||||
static {
|
||||
@@ -194,7 +195,7 @@ public class Logger {
|
||||
return logLevel.code <= levelMap.get(group).code;
|
||||
}
|
||||
|
||||
private void log(String message, LogLevel level) {
|
||||
void log(String message, LogLevel level) {
|
||||
if (shouldLog(level)) {
|
||||
log(message, level, group, className);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
/*
|
||||
* 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.logging;
|
||||
|
||||
import edu.wpi.first.cscore.CameraServerJNI;
|
||||
import java.nio.file.Path;
|
||||
|
||||
/** Redirect cscore logs to our logger */
|
||||
public class PvCSCoreLogger {
|
||||
private static PvCSCoreLogger INSTANCE;
|
||||
|
||||
public static PvCSCoreLogger getInstance() {
|
||||
if (INSTANCE == null) {
|
||||
INSTANCE = new PvCSCoreLogger();
|
||||
}
|
||||
return INSTANCE;
|
||||
}
|
||||
|
||||
private Logger logger;
|
||||
|
||||
private PvCSCoreLogger() {
|
||||
CameraServerJNI.setLogger(this::logMsg, 7);
|
||||
this.logger = new Logger(getClass(), LogGroup.CSCore);
|
||||
}
|
||||
|
||||
private void logMsg(int level, String file, int line, String msg) {
|
||||
if (level == 20) {
|
||||
logger.info(msg);
|
||||
return;
|
||||
}
|
||||
|
||||
file = Path.of(file).getFileName().toString();
|
||||
|
||||
String levelmsg;
|
||||
LogLevel pvlevel;
|
||||
if (level >= 50) {
|
||||
levelmsg = "CRITICAL";
|
||||
pvlevel = LogLevel.ERROR;
|
||||
} else if (level >= 40) {
|
||||
levelmsg = "ERROR";
|
||||
pvlevel = LogLevel.ERROR;
|
||||
} else if (level >= 30) {
|
||||
levelmsg = "WARNING";
|
||||
pvlevel = LogLevel.WARN;
|
||||
} else if (level >= 20) {
|
||||
levelmsg = "INFO";
|
||||
pvlevel = LogLevel.INFO;
|
||||
} else {
|
||||
levelmsg = "DEBUG";
|
||||
pvlevel = LogLevel.DEBUG;
|
||||
}
|
||||
logger.log(
|
||||
"CS: " + levelmsg + " " + level + ": " + msg + " (" + file + ":" + line + ")", pvlevel);
|
||||
}
|
||||
}
|
||||
@@ -26,12 +26,15 @@ import org.photonvision.common.logging.LogGroup;
|
||||
import org.photonvision.common.logging.Logger;
|
||||
|
||||
public abstract class PhotonJNICommon {
|
||||
static boolean libraryLoaded = false;
|
||||
public abstract boolean isLoaded();
|
||||
|
||||
public abstract void setLoaded(boolean state);
|
||||
|
||||
protected static Logger logger = null;
|
||||
|
||||
protected static synchronized void forceLoad(Class<?> clazz, List<String> libraries)
|
||||
throws IOException {
|
||||
if (libraryLoaded) return;
|
||||
protected static synchronized void forceLoad(
|
||||
PhotonJNICommon instance, Class<?> clazz, List<String> libraries) throws IOException {
|
||||
if (instance.isLoaded()) return;
|
||||
if (logger == null) logger = new Logger(clazz, LogGroup.Camera);
|
||||
|
||||
for (var libraryName : libraries) {
|
||||
@@ -42,7 +45,7 @@ public abstract class PhotonJNICommon {
|
||||
var in = clazz.getResourceAsStream("/nativelibraries/" + arch_name + "/" + nativeLibName);
|
||||
|
||||
if (in == null) {
|
||||
libraryLoaded = false;
|
||||
instance.setLoaded(false);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -69,15 +72,11 @@ public abstract class PhotonJNICommon {
|
||||
break;
|
||||
}
|
||||
}
|
||||
libraryLoaded = true;
|
||||
instance.setLoaded(true);
|
||||
}
|
||||
|
||||
protected static synchronized void forceLoad(Class<?> clazz, String libraryName)
|
||||
throws IOException {
|
||||
forceLoad(clazz, List.of(libraryName));
|
||||
}
|
||||
|
||||
public static boolean isWorking() {
|
||||
return libraryLoaded;
|
||||
protected static synchronized void forceLoad(
|
||||
PhotonJNICommon instance, Class<?> clazz, String libraryName) throws IOException {
|
||||
forceLoad(instance, clazz, List.of(libraryName));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,149 @@
|
||||
/*
|
||||
* 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.jni;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CopyOnWriteArrayList;
|
||||
import java.util.stream.Collectors;
|
||||
import org.opencv.core.Mat;
|
||||
import org.photonvision.common.logging.LogGroup;
|
||||
import org.photonvision.common.logging.Logger;
|
||||
import org.photonvision.common.util.TestUtils;
|
||||
import org.photonvision.rknn.RknnJNI;
|
||||
import org.photonvision.rknn.RknnJNI.RknnResult;
|
||||
import org.photonvision.vision.pipe.impl.NeuralNetworkPipeResult;
|
||||
|
||||
public class RknnDetectorJNI extends PhotonJNICommon {
|
||||
private static final Logger logger = new Logger(RknnDetectorJNI.class, LogGroup.General);
|
||||
private boolean isLoaded;
|
||||
private static RknnDetectorJNI instance = null;
|
||||
|
||||
private RknnDetectorJNI() {
|
||||
isLoaded = false;
|
||||
}
|
||||
|
||||
public static RknnDetectorJNI getInstance() {
|
||||
if (instance == null) instance = new RknnDetectorJNI();
|
||||
|
||||
return instance;
|
||||
}
|
||||
|
||||
public static synchronized void forceLoad() throws IOException {
|
||||
TestUtils.loadLibraries();
|
||||
|
||||
forceLoad(getInstance(), RknnDetectorJNI.class, List.of("rga", "rknnrt", "rknn_jni"));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isLoaded() {
|
||||
return isLoaded;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setLoaded(boolean state) {
|
||||
isLoaded = state;
|
||||
}
|
||||
|
||||
public static class RknnObjectDetector {
|
||||
long objPointer = -1;
|
||||
private List<String> labels;
|
||||
private final Object lock = new Object();
|
||||
private static final CopyOnWriteArrayList<RknnObjectDetector> detectors =
|
||||
new CopyOnWriteArrayList<>();
|
||||
|
||||
static volatile boolean hook = false;
|
||||
|
||||
public RknnObjectDetector(String modelPath, List<String> labels, RknnJNI.ModelVersion version) {
|
||||
synchronized (lock) {
|
||||
objPointer = RknnJNI.create(modelPath, labels.size(), version.ordinal(), -1);
|
||||
detectors.add(this);
|
||||
logger.debug(
|
||||
"Created detector "
|
||||
+ objPointer
|
||||
+ " from path "
|
||||
+ modelPath
|
||||
+ "! Detectors: "
|
||||
+ Arrays.toString(detectors.toArray()));
|
||||
}
|
||||
this.labels = labels;
|
||||
|
||||
// the kernel should probably alredy deal with this for us, but I'm gunna be paranoid anyways.
|
||||
if (!hook) {
|
||||
Runtime.getRuntime()
|
||||
.addShutdownHook(
|
||||
new Thread(
|
||||
() -> {
|
||||
System.err.println("Shutdown hook rknn");
|
||||
for (var d : detectors) {
|
||||
d.release();
|
||||
}
|
||||
}));
|
||||
hook = true;
|
||||
}
|
||||
}
|
||||
|
||||
public List<String> getClasses() {
|
||||
return labels;
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect forwards using this model
|
||||
*
|
||||
* @param in The image to process
|
||||
* @param nmsThresh Non-maximum supression threshold. Probably should not change
|
||||
* @param boxThresh Minimum confidence for a box to be added. Basically just confidence
|
||||
* threshold
|
||||
*/
|
||||
public List<NeuralNetworkPipeResult> detect(Mat in, double nmsThresh, double boxThresh) {
|
||||
RknnResult[] ret;
|
||||
synchronized (lock) {
|
||||
// We can technically be asked to detect and the lock might be acquired _after_ release has
|
||||
// been called. This would mean objPointer would be invalid which would call everything to
|
||||
// explode.
|
||||
if (objPointer > 0) {
|
||||
ret = RknnJNI.detect(objPointer, in.getNativeObjAddr(), nmsThresh, boxThresh);
|
||||
} else {
|
||||
logger.warn("Detect called after destroy -- giving up");
|
||||
return List.of();
|
||||
}
|
||||
}
|
||||
if (ret == null) {
|
||||
return List.of();
|
||||
}
|
||||
return List.of(ret).stream()
|
||||
.map(it -> new NeuralNetworkPipeResult(it.rect, it.class_id, it.conf))
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
public void release() {
|
||||
synchronized (lock) {
|
||||
if (objPointer > 0) {
|
||||
RknnJNI.destroy(objPointer);
|
||||
detectors.remove(this);
|
||||
System.out.println(
|
||||
"Killed " + objPointer + "! Detectors: " + Arrays.toString(detectors.toArray()));
|
||||
objPointer = -1;
|
||||
} else {
|
||||
logger.error("RKNN Detector has already been destroyed!");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -24,6 +24,19 @@ import org.photonvision.common.util.TestUtils;
|
||||
import org.photonvision.jni.PhotonJNICommon;
|
||||
|
||||
public class MrCalJNILoader extends PhotonJNICommon {
|
||||
private boolean isLoaded;
|
||||
private static MrCalJNILoader instance = null;
|
||||
|
||||
private MrCalJNILoader() {
|
||||
isLoaded = false;
|
||||
}
|
||||
|
||||
public static synchronized MrCalJNILoader getInstance() {
|
||||
if (instance == null) instance = new MrCalJNILoader();
|
||||
|
||||
return instance;
|
||||
}
|
||||
|
||||
public static synchronized void forceLoad() throws IOException {
|
||||
// Force load opencv
|
||||
TestUtils.loadLibraries();
|
||||
@@ -32,6 +45,7 @@ public class MrCalJNILoader extends PhotonJNICommon {
|
||||
if (Platform.isWindows()) {
|
||||
// Order is correct to match dependencies of libraries
|
||||
forceLoad(
|
||||
MrCalJNILoader.getInstance(),
|
||||
MrCalJNILoader.class,
|
||||
List.of(
|
||||
"libamd",
|
||||
@@ -39,18 +53,30 @@ public class MrCalJNILoader extends PhotonJNICommon {
|
||||
"libcolamd",
|
||||
"libccolamd",
|
||||
"openblas",
|
||||
"libwinpthread-1",
|
||||
"libgcc_s_seh-1",
|
||||
"libquadmath-0",
|
||||
"libgfortran-5",
|
||||
"liblapack",
|
||||
"libcholmod",
|
||||
"mrcal_jni"));
|
||||
} else {
|
||||
// Nothing else to do on linux
|
||||
forceLoad(MrCalJNILoader.class, List.of("mrcal_jni"));
|
||||
forceLoad(MrCalJNILoader.getInstance(), MrCalJNILoader.class, List.of("mrcal_jni"));
|
||||
}
|
||||
|
||||
if (!MrCalJNILoader.isWorking()) {
|
||||
if (!MrCalJNILoader.getInstance().isLoaded()) {
|
||||
throw new IOException("Unable to load mrcal JNI!");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isLoaded() {
|
||||
return isLoaded;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setLoaded(boolean state) {
|
||||
isLoaded = state;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,16 +23,37 @@ import java.util.Comparator;
|
||||
import org.opencv.core.Mat;
|
||||
import org.opencv.objdetect.ArucoDetector;
|
||||
import org.opencv.objdetect.DetectorParameters;
|
||||
import org.opencv.objdetect.Dictionary;
|
||||
import org.opencv.objdetect.Objdetect;
|
||||
import org.photonvision.common.logging.LogGroup;
|
||||
import org.photonvision.common.logging.Logger;
|
||||
import org.photonvision.vision.opencv.Releasable;
|
||||
|
||||
/** This class wraps an {@link ArucoDetector} for convenience. */
|
||||
public class PhotonArucoDetector {
|
||||
public class PhotonArucoDetector implements Releasable {
|
||||
private static final Logger logger = new Logger(PhotonArucoDetector.class, LogGroup.VisionModule);
|
||||
|
||||
private final ArucoDetector detector =
|
||||
new ArucoDetector(Objdetect.getPredefinedDictionary(Objdetect.DICT_APRILTAG_16h5));
|
||||
private static class ArucoDetectorHack extends ArucoDetector {
|
||||
public ArucoDetectorHack(Dictionary predefinedDictionary) {
|
||||
super(predefinedDictionary);
|
||||
}
|
||||
|
||||
// avoid double-free by keeping track of this ourselves (ew)
|
||||
private boolean freed = false;
|
||||
|
||||
@Override
|
||||
public void finalize() throws Throwable {
|
||||
if (freed) {
|
||||
return;
|
||||
}
|
||||
|
||||
super.finalize();
|
||||
freed = true;
|
||||
}
|
||||
}
|
||||
|
||||
private final ArucoDetectorHack detector =
|
||||
new ArucoDetectorHack(Objdetect.getPredefinedDictionary(Objdetect.DICT_APRILTAG_16h5));
|
||||
|
||||
private final Mat ids = new Mat();
|
||||
private final ArrayList<Mat> cornerMats = new ArrayList<>();
|
||||
@@ -95,4 +116,16 @@ public class PhotonArucoDetector {
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void release() {
|
||||
try {
|
||||
detector.finalize();
|
||||
} catch (Throwable e) {
|
||||
logger.error("Exception destroying PhotonArucoDetector", e);
|
||||
}
|
||||
ids.release();
|
||||
for (var m : cornerMats) m.release();
|
||||
cornerMats.clear();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ import java.util.List;
|
||||
import org.opencv.core.Point;
|
||||
import org.opencv.core.Point3;
|
||||
|
||||
public final class BoardObservation {
|
||||
public final class BoardObservation implements Cloneable {
|
||||
// Expected feature 3d location in the camera frame
|
||||
@JsonProperty("locationInObjectSpace")
|
||||
public List<Point3> locationInObjectSpace;
|
||||
@@ -68,4 +68,33 @@ public final class BoardObservation {
|
||||
this.snapshotName = snapshotName;
|
||||
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
|
||||
+ ", distCoeffs="
|
||||
+ distCoeffs
|
||||
+ ", observations="
|
||||
+ observations
|
||||
+ ", observationslen="
|
||||
+ observations.size()
|
||||
+ ", calobjectWarp="
|
||||
+ Arrays.toString(calobjectWarp)
|
||||
+ ", intrinsicsArr="
|
||||
@@ -201,4 +201,16 @@ public class CameraCalibrationCoefficients implements Releasable {
|
||||
+ 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() {
|
||||
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 Matrix wpilibMat = null;
|
||||
|
||||
private MatOfDouble wrappedMatOfDouble;
|
||||
@JsonIgnore private MatOfDouble wrappedMatOfDouble;
|
||||
|
||||
public JsonMatOfDouble(int rows, int cols, double[] 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 java.util.Arrays;
|
||||
import java.util.Optional;
|
||||
import org.photonvision.common.hardware.Platform;
|
||||
|
||||
public class CameraInfo extends UsbCameraInfo {
|
||||
public final CameraType cameraType;
|
||||
@@ -68,15 +70,54 @@ public class CameraInfo extends UsbCameraInfo {
|
||||
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
|
||||
public boolean equals(Object o) {
|
||||
if (o == this) return true;
|
||||
if (!(o instanceof UsbCameraInfo || o instanceof CameraInfo)) return false;
|
||||
UsbCameraInfo other = (UsbCameraInfo) o;
|
||||
return path.equals(other.path)
|
||||
// && a.dev == b.dev (dev is not constant in Windows)
|
||||
&& name.equals(other.name)
|
||||
&& productId == other.productId
|
||||
&& vendorId == other.vendorId;
|
||||
public boolean equals(Object obj) {
|
||||
if (this == obj) return true;
|
||||
if (obj == null) return false;
|
||||
if (getClass() != obj.getClass()) return false;
|
||||
CameraInfo other = (CameraInfo) obj;
|
||||
|
||||
// Windows device number is not significant. See
|
||||
// https://github.com/wpilibsuite/allwpilib/blob/4b94a64b06057c723d6fcafeb1a45f55a70d179a/cscore/src/main/native/windows/UsbCameraImpl.cpp#L1128
|
||||
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);
|
||||
|
||||
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);
|
||||
|
||||
// set vid/pid if not done already for future matching
|
||||
if (config.usbVID <= 0) config.usbVID = this.camera.getInfo().vendorId;
|
||||
if (config.usbPID <= 0) config.usbPID = this.camera.getInfo().productId;
|
||||
|
||||
if (getCameraConfiguration().cameraQuirks == null)
|
||||
getCameraConfiguration().cameraQuirks =
|
||||
QuirkyCamera.getQuirkyCamera(
|
||||
@@ -262,9 +270,13 @@ public class USBCameraSource extends VisionSource {
|
||||
if (getCameraConfiguration().cameraQuirks.hasQuirk(CameraQuirk.ArduOV9281)) {
|
||||
propMin = 1;
|
||||
propMax = 75;
|
||||
} else if (getCameraConfiguration().cameraQuirks.hasQuirk(CameraQuirk.ArduOV2311)) {
|
||||
propMin = 1;
|
||||
propMax = 140;
|
||||
}
|
||||
|
||||
var exposure_manual_val = MathUtils.map(Math.round(exposure), 0, 100, propMin, propMax);
|
||||
logger.debug("Setting camera exposure to " + exposure_manual_val);
|
||||
prop.set((int) exposure_manual_val);
|
||||
} else {
|
||||
scaledExposure = (int) Math.round(exposure);
|
||||
@@ -395,6 +407,7 @@ public class USBCameraSource extends VisionSource {
|
||||
// Sort by resolution
|
||||
var sortedList =
|
||||
videoModesList.stream()
|
||||
.distinct() // remove redundant video mode entries
|
||||
.sorted(((a, b) -> (b.width + b.height) - (a.width + a.height)))
|
||||
.collect(Collectors.toList());
|
||||
Collections.reverse(sortedList);
|
||||
|
||||
@@ -33,7 +33,7 @@ import org.photonvision.vision.opencv.CVMat;
|
||||
* path}.
|
||||
*/
|
||||
public class FileFrameProvider extends CpuImageProcessor {
|
||||
public static final int MAX_FPS = 5;
|
||||
public static final int MAX_FPS = 10;
|
||||
private static int count = 0;
|
||||
|
||||
private final int thisIndex = count++;
|
||||
|
||||
@@ -47,6 +47,16 @@ public class Contour implements Releasable {
|
||||
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() {
|
||||
if (mat2f == null) {
|
||||
mat2f = new MatOfPoint2f(mat.toArray());
|
||||
|
||||
@@ -25,15 +25,15 @@ public enum ContourSortMode {
|
||||
Comparator.comparingDouble(PotentialTarget::getArea)
|
||||
.reversed()), // reversed so that zero index has the largest size
|
||||
Smallest(Largest.getComparator().reversed()),
|
||||
Highest(Comparator.comparingDouble(rect -> rect.getMinAreaRect().center.y)),
|
||||
Highest(Comparator.comparingDouble(tgt -> tgt.getMinAreaRect().center.y)),
|
||||
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()),
|
||||
Centermost(
|
||||
Comparator.comparingDouble(
|
||||
rect ->
|
||||
(Math.pow(rect.getMinAreaRect().center.y, 2)
|
||||
+ Math.pow(rect.getMinAreaRect().center.x, 2))));
|
||||
tgt ->
|
||||
(Math.pow(tgt.getMinAreaRect().center.y, 2)
|
||||
+ Math.pow(tgt.getMinAreaRect().center.x, 2))));
|
||||
|
||||
private final Comparator<PotentialTarget> m_comparator;
|
||||
|
||||
|
||||
@@ -33,6 +33,10 @@ public abstract class CVPipe<I, O, P> {
|
||||
this.params = params;
|
||||
}
|
||||
|
||||
public P getParams() {
|
||||
return this.params;
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs the process for the pipe.
|
||||
*
|
||||
|
||||
@@ -21,11 +21,13 @@ import edu.wpi.first.apriltag.AprilTagDetection;
|
||||
import edu.wpi.first.apriltag.AprilTagDetector;
|
||||
import java.util.List;
|
||||
import org.photonvision.vision.opencv.CVMat;
|
||||
import org.photonvision.vision.opencv.Releasable;
|
||||
import org.photonvision.vision.pipe.CVPipe;
|
||||
|
||||
public class AprilTagDetectionPipe
|
||||
extends CVPipe<CVMat, List<AprilTagDetection>, AprilTagDetectionPipeParams> {
|
||||
private final AprilTagDetector m_detector = new AprilTagDetector();
|
||||
extends CVPipe<CVMat, List<AprilTagDetection>, AprilTagDetectionPipeParams>
|
||||
implements Releasable {
|
||||
private AprilTagDetector m_detector = new AprilTagDetector();
|
||||
|
||||
public AprilTagDetectionPipe() {
|
||||
super();
|
||||
@@ -40,6 +42,10 @@ public class AprilTagDetectionPipe
|
||||
return List.of();
|
||||
}
|
||||
|
||||
if (m_detector == null) {
|
||||
throw new RuntimeException("Apriltag detector was released!");
|
||||
}
|
||||
|
||||
var ret = m_detector.detect(in.getMat());
|
||||
|
||||
if (ret == null) {
|
||||
@@ -60,4 +66,10 @@ public class AprilTagDetectionPipe
|
||||
|
||||
super.setParams(newParams);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void release() {
|
||||
m_detector.close();
|
||||
m_detector = null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,13 +25,15 @@ import org.opencv.calib3d.Calib3d;
|
||||
import org.opencv.core.MatOfPoint2f;
|
||||
import org.opencv.core.Point;
|
||||
import org.photonvision.vision.calibration.CameraCalibrationCoefficients;
|
||||
import org.photonvision.vision.opencv.Releasable;
|
||||
import org.photonvision.vision.pipe.CVPipe;
|
||||
|
||||
public class AprilTagPoseEstimatorPipe
|
||||
extends CVPipe<
|
||||
AprilTagDetection,
|
||||
AprilTagPoseEstimate,
|
||||
AprilTagPoseEstimatorPipe.AprilTagPoseEstimatorPipeParams> {
|
||||
AprilTagPoseEstimatorPipe.AprilTagPoseEstimatorPipeParams>
|
||||
implements Releasable {
|
||||
private final AprilTagPoseEstimator m_poseEstimator =
|
||||
new AprilTagPoseEstimator(new AprilTagPoseEstimator.Config(0, 0, 0, 0, 0));
|
||||
|
||||
@@ -92,6 +94,11 @@ public class AprilTagPoseEstimatorPipe
|
||||
super.setParams(newParams);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void release() {
|
||||
temp.release();
|
||||
}
|
||||
|
||||
public static class AprilTagPoseEstimatorPipeParams {
|
||||
final AprilTagPoseEstimator.Config config;
|
||||
final CameraCalibrationCoefficients calibration;
|
||||
|
||||
@@ -29,10 +29,12 @@ import org.opencv.objdetect.Objdetect;
|
||||
import org.photonvision.vision.aruco.ArucoDetectionResult;
|
||||
import org.photonvision.vision.aruco.PhotonArucoDetector;
|
||||
import org.photonvision.vision.opencv.CVMat;
|
||||
import org.photonvision.vision.opencv.Releasable;
|
||||
import org.photonvision.vision.pipe.CVPipe;
|
||||
|
||||
public class ArucoDetectionPipe
|
||||
extends CVPipe<CVMat, List<ArucoDetectionResult>, ArucoDetectionPipeParams> {
|
||||
extends CVPipe<CVMat, List<ArucoDetectionResult>, ArucoDetectionPipeParams>
|
||||
implements Releasable {
|
||||
// ArucoDetector wrapper class
|
||||
private final PhotonArucoDetector photonDetector = new PhotonArucoDetector();
|
||||
|
||||
@@ -44,6 +46,13 @@ public class ArucoDetectionPipe
|
||||
@Override
|
||||
protected List<ArucoDetectionResult> process(CVMat in) {
|
||||
var imgMat = in.getMat();
|
||||
|
||||
// Sanity check -- image should not be empty
|
||||
if (imgMat.empty()) {
|
||||
// give up is best we can do here
|
||||
return List.of();
|
||||
}
|
||||
|
||||
var detections = photonDetector.detect(imgMat);
|
||||
// manually do corner refinement ourselves
|
||||
if (params.useCornerRefinement) {
|
||||
@@ -124,4 +133,9 @@ public class ArucoDetectionPipe
|
||||
var pt2 = new Point(corner.x + windowSize, corner.y + windowSize);
|
||||
Imgproc.rectangle(outputMat, pt1, pt2, new Scalar(0, 0, 255), thickness);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void release() {
|
||||
photonDetector.release();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,13 +32,15 @@ import org.opencv.core.MatOfPoint3f;
|
||||
import org.opencv.core.Point3;
|
||||
import org.photonvision.vision.aruco.ArucoDetectionResult;
|
||||
import org.photonvision.vision.calibration.CameraCalibrationCoefficients;
|
||||
import org.photonvision.vision.opencv.Releasable;
|
||||
import org.photonvision.vision.pipe.CVPipe;
|
||||
|
||||
public class ArucoPoseEstimatorPipe
|
||||
extends CVPipe<
|
||||
ArucoDetectionResult,
|
||||
AprilTagPoseEstimate,
|
||||
ArucoPoseEstimatorPipe.ArucoPoseEstimatorPipeParams> {
|
||||
ArucoPoseEstimatorPipe.ArucoPoseEstimatorPipeParams>
|
||||
implements Releasable {
|
||||
// image points of marker corners
|
||||
private final MatOfPoint2f imagePoints = new MatOfPoint2f(Mat.zeros(4, 1, CvType.CV_32FC2));
|
||||
// rvec/tvec estimations from solvepnp
|
||||
@@ -117,6 +119,18 @@ public class ArucoPoseEstimatorPipe
|
||||
super.setParams(newParams);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void release() {
|
||||
imagePoints.release();
|
||||
for (var m : rvecs) m.release();
|
||||
rvecs.clear();
|
||||
for (var m : tvecs) m.release();
|
||||
tvecs.clear();
|
||||
rvec.release();
|
||||
tvec.release();
|
||||
reprojectionErrors.release();
|
||||
}
|
||||
|
||||
public static class ArucoPoseEstimatorPipeParams {
|
||||
final CameraCalibrationCoefficients calibration;
|
||||
final double tagSize;
|
||||
|
||||
@@ -38,13 +38,26 @@ import org.photonvision.vision.calibration.CameraCalibrationCoefficients;
|
||||
import org.photonvision.vision.calibration.CameraLensModel;
|
||||
import org.photonvision.vision.calibration.JsonImageMat;
|
||||
import org.photonvision.vision.calibration.JsonMatOfDouble;
|
||||
import org.photonvision.vision.frame.FrameStaticProperties;
|
||||
import org.photonvision.vision.pipe.CVPipe;
|
||||
import org.photonvision.vision.pipe.impl.FindBoardCornersPipe.FindBoardCornersPipeResult;
|
||||
|
||||
public class Calibrate3dPipe
|
||||
extends CVPipe<
|
||||
List<FindBoardCornersPipe.FindBoardCornersPipeResult>,
|
||||
Calibrate3dPipe.CalibrationInput,
|
||||
CameraCalibrationCoefficients,
|
||||
Calibrate3dPipe.CalibratePipeParams> {
|
||||
public static class CalibrationInput {
|
||||
final List<FindBoardCornersPipe.FindBoardCornersPipeResult> observations;
|
||||
final FrameStaticProperties imageProps;
|
||||
|
||||
public CalibrationInput(
|
||||
List<FindBoardCornersPipeResult> observations, FrameStaticProperties imageProps) {
|
||||
this.observations = observations;
|
||||
this.imageProps = imageProps;
|
||||
}
|
||||
}
|
||||
|
||||
// For logging
|
||||
private static final Logger logger = new Logger(Calibrate3dPipe.class, LogGroup.General);
|
||||
|
||||
@@ -63,10 +76,9 @@ public class Calibrate3dPipe
|
||||
* @return Result of processing.
|
||||
*/
|
||||
@Override
|
||||
protected CameraCalibrationCoefficients process(
|
||||
List<FindBoardCornersPipe.FindBoardCornersPipeResult> in) {
|
||||
in =
|
||||
in.stream()
|
||||
protected CameraCalibrationCoefficients process(CalibrationInput in) {
|
||||
var filteredIn =
|
||||
in.observations.stream()
|
||||
.filter(
|
||||
it ->
|
||||
it != null
|
||||
@@ -77,19 +89,23 @@ public class Calibrate3dPipe
|
||||
|
||||
CameraCalibrationCoefficients ret;
|
||||
var start = System.nanoTime();
|
||||
if (MrCalJNILoader.isWorking() && params.useMrCal) {
|
||||
if (MrCalJNILoader.getInstance().isLoaded() && params.useMrCal) {
|
||||
logger.debug("Calibrating with mrcal!");
|
||||
ret = calibrateMrcal(in);
|
||||
ret =
|
||||
calibrateMrcal(
|
||||
filteredIn, in.imageProps.horizontalFocalLength, in.imageProps.verticalFocalLength);
|
||||
} else {
|
||||
logger.debug("Calibrating with opencv!");
|
||||
ret = calibrateOpenCV(in);
|
||||
ret =
|
||||
calibrateOpenCV(
|
||||
filteredIn, in.imageProps.horizontalFocalLength, in.imageProps.verticalFocalLength);
|
||||
}
|
||||
var dt = System.nanoTime() - start;
|
||||
|
||||
if (ret != null)
|
||||
logger.info(
|
||||
"CALIBRATION SUCCESS for res "
|
||||
+ in.get(0).size
|
||||
+ in.observations.get(0).size
|
||||
+ " in "
|
||||
+ dt / 1e6
|
||||
+ "ms! camMatrix: \n"
|
||||
@@ -103,7 +119,7 @@ public class Calibrate3dPipe
|
||||
}
|
||||
|
||||
protected CameraCalibrationCoefficients calibrateOpenCV(
|
||||
List<FindBoardCornersPipe.FindBoardCornersPipeResult> in) {
|
||||
List<FindBoardCornersPipe.FindBoardCornersPipeResult> in, double fxGuess, double fyGuess) {
|
||||
List<Mat> objPoints = in.stream().map(it -> it.objectPoints).collect(Collectors.toList());
|
||||
List<Mat> imgPts = in.stream().map(it -> it.imagePoints).collect(Collectors.toList());
|
||||
if (objPoints.size() != imgPts.size()) {
|
||||
@@ -111,30 +127,32 @@ public class Calibrate3dPipe
|
||||
return null;
|
||||
}
|
||||
|
||||
Mat cameraMatrix = new Mat();
|
||||
Mat cameraMatrix = new Mat(3, 3, CvType.CV_64F);
|
||||
MatOfDouble distortionCoefficients = new MatOfDouble();
|
||||
List<Mat> rvecs = new ArrayList<>();
|
||||
List<Mat> tvecs = new ArrayList<>();
|
||||
|
||||
// RMS of the calibration
|
||||
double calibrationAccuracy;
|
||||
// initial camera matrix guess
|
||||
double cx = (in.get(0).size.width / 2.0) - 0.5;
|
||||
double cy = (in.get(0).size.width / 2.0) - 0.5;
|
||||
cameraMatrix.put(0, 0, new double[] {fxGuess, 0, cx, 0, fyGuess, cy, 0, 0, 1});
|
||||
|
||||
try {
|
||||
// FindBoardCorners pipe outputs all the image points, object points, and frames to calculate
|
||||
// imageSize from, other parameters are output Mats
|
||||
|
||||
calibrationAccuracy =
|
||||
Calib3d.calibrateCameraExtended(
|
||||
objPoints,
|
||||
imgPts,
|
||||
new Size(in.get(0).size.width, in.get(0).size.height),
|
||||
cameraMatrix,
|
||||
distortionCoefficients,
|
||||
rvecs,
|
||||
tvecs,
|
||||
stdDeviationsIntrinsics,
|
||||
stdDeviationsExtrinsics,
|
||||
perViewErrors);
|
||||
Calib3d.calibrateCameraExtended(
|
||||
objPoints,
|
||||
imgPts,
|
||||
new Size(in.get(0).size.width, in.get(0).size.height),
|
||||
cameraMatrix,
|
||||
distortionCoefficients,
|
||||
rvecs,
|
||||
tvecs,
|
||||
stdDeviationsIntrinsics,
|
||||
stdDeviationsExtrinsics,
|
||||
perViewErrors,
|
||||
Calib3d.CALIB_USE_LU + Calib3d.CALIB_USE_INTRINSIC_GUESS);
|
||||
} catch (Exception e) {
|
||||
logger.error("Calibration failed!", e);
|
||||
e.printStackTrace();
|
||||
@@ -164,13 +182,12 @@ public class Calibrate3dPipe
|
||||
}
|
||||
|
||||
protected CameraCalibrationCoefficients calibrateMrcal(
|
||||
List<FindBoardCornersPipe.FindBoardCornersPipeResult> in) {
|
||||
List<FindBoardCornersPipe.FindBoardCornersPipeResult> in, double fxGuess, double fyGuess) {
|
||||
List<MatOfPoint2f> corner_locations =
|
||||
in.stream().map(it -> it.imagePoints).map(MatOfPoint2f::new).collect(Collectors.toList());
|
||||
|
||||
int imageWidth = (int) in.get(0).size.width;
|
||||
int imageHeight = (int) in.get(0).size.height;
|
||||
final double FOCAL_LENGTH_GUESS = 1200;
|
||||
|
||||
MrCalResult result =
|
||||
MrCalJNI.calibrateCamera(
|
||||
@@ -180,7 +197,7 @@ public class Calibrate3dPipe
|
||||
params.squareSize,
|
||||
imageWidth,
|
||||
imageHeight,
|
||||
FOCAL_LENGTH_GUESS);
|
||||
(fxGuess + fyGuess) / 2.0);
|
||||
|
||||
// intrinsics are fx fy cx cy from mrcal
|
||||
JsonMatOfDouble cameraMatrixMat =
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.photonvision.vision.pipeline;
|
||||
package org.photonvision.vision.pipe.impl;
|
||||
|
||||
import edu.wpi.first.math.util.Units;
|
||||
import java.util.ArrayList;
|
||||
@@ -36,10 +36,11 @@ import org.photonvision.vision.frame.FrameThresholdType;
|
||||
import org.photonvision.vision.opencv.CVMat;
|
||||
import org.photonvision.vision.opencv.ImageRotationMode;
|
||||
import org.photonvision.vision.pipe.CVPipe.CVPipeResult;
|
||||
import org.photonvision.vision.pipe.impl.CalculateFPSPipe;
|
||||
import org.photonvision.vision.pipe.impl.Calibrate3dPipe;
|
||||
import org.photonvision.vision.pipe.impl.FindBoardCornersPipe;
|
||||
import org.photonvision.vision.pipe.impl.Calibrate3dPipe.CalibrationInput;
|
||||
import org.photonvision.vision.pipe.impl.FindBoardCornersPipe.FindBoardCornersPipeResult;
|
||||
import org.photonvision.vision.pipeline.CVPipeline;
|
||||
import org.photonvision.vision.pipeline.Calibration3dPipelineSettings;
|
||||
import org.photonvision.vision.pipeline.UICalibrationData;
|
||||
import org.photonvision.vision.pipeline.result.CVPipelineResult;
|
||||
import org.photonvision.vision.pipeline.result.CalibrationPipelineResult;
|
||||
|
||||
@@ -176,7 +177,8 @@ public class Calibrate3dPipeline
|
||||
|
||||
/*Pass the board corners to the pipe, which will check again to see if all boards are valid
|
||||
and returns the corresponding image and object points*/
|
||||
calibrationOutput = calibrate3dPipe.run(foundCornersList);
|
||||
calibrationOutput =
|
||||
calibrate3dPipe.run(new CalibrationInput(foundCornersList, frameStaticProperties));
|
||||
|
||||
this.calibrating = false;
|
||||
|
||||
@@ -229,4 +231,9 @@ public class Calibrate3dPipeline
|
||||
public CameraCalibrationCoefficients cameraCalibrationCoefficients() {
|
||||
return calibrationOutput.output;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void release() {
|
||||
// we never actually need to give resources up since pipelinemanager only makes one of us
|
||||
}
|
||||
}
|
||||
@@ -22,6 +22,7 @@ import java.util.List;
|
||||
import org.apache.commons.lang3.tuple.Pair;
|
||||
import org.opencv.core.Mat;
|
||||
import org.opencv.core.Point;
|
||||
import org.opencv.core.Scalar;
|
||||
import org.opencv.imgproc.Imgproc;
|
||||
import org.photonvision.common.util.ColorHelper;
|
||||
import org.photonvision.vision.frame.FrameDivisor;
|
||||
@@ -31,22 +32,44 @@ import org.photonvision.vision.target.TrackedTarget;
|
||||
public class DrawCalibrationPipe
|
||||
extends MutatingPipe<
|
||||
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
|
||||
protected Void process(Pair<Mat, List<TrackedTarget>> in) {
|
||||
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 c : target.getTargetCorners()) {
|
||||
c =
|
||||
new Point(
|
||||
c.x / params.divisor.value.doubleValue(), c.y / params.divisor.value.doubleValue());
|
||||
var r = 4;
|
||||
|
||||
var r2 = r / Math.sqrt(2);
|
||||
var color = ColorHelper.colorToScalar(Color.RED, 0.4);
|
||||
Imgproc.circle(image, c, r, color, 1);
|
||||
Imgproc.line(image, new Point(c.x - r2, c.y - r2), new Point(c.x + r2, c.y + r2), color);
|
||||
Imgproc.line(image, new Point(c.x + r2, c.y - r2), new Point(c.x - r2, c.y + r2), color);
|
||||
var color = chessboardColors[i % chessboardColors.length];
|
||||
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, 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;
|
||||
|
||||
@@ -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.bbox;
|
||||
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
/*
|
||||
* 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 org.opencv.core.Rect2d;
|
||||
|
||||
public class NeuralNetworkPipeResult {
|
||||
public NeuralNetworkPipeResult(Rect2d boundingBox, int classIdx, double confidence) {
|
||||
bbox = boundingBox;
|
||||
this.classIdx = classIdx;
|
||||
this.confidence = confidence;
|
||||
}
|
||||
|
||||
public final int classIdx;
|
||||
public final Rect2d bbox;
|
||||
public final double confidence;
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
/*
|
||||
* 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.awt.Color;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import org.opencv.core.Core;
|
||||
import org.opencv.core.Mat;
|
||||
import org.opencv.core.Rect2d;
|
||||
import org.opencv.core.Scalar;
|
||||
import org.opencv.core.Size;
|
||||
import org.opencv.imgproc.Imgproc;
|
||||
import org.photonvision.common.configuration.NeuralNetworkModelManager;
|
||||
import org.photonvision.common.util.ColorHelper;
|
||||
import org.photonvision.jni.RknnDetectorJNI.RknnObjectDetector;
|
||||
import org.photonvision.vision.opencv.CVMat;
|
||||
import org.photonvision.vision.opencv.Releasable;
|
||||
import org.photonvision.vision.pipe.CVPipe;
|
||||
|
||||
public class RknnDetectionPipe
|
||||
extends CVPipe<CVMat, List<NeuralNetworkPipeResult>, RknnDetectionPipe.RknnDetectionPipeParams>
|
||||
implements Releasable {
|
||||
private RknnObjectDetector detector;
|
||||
|
||||
public RknnDetectionPipe() {
|
||||
// For now this is hard-coded to defaults. Should be refactored into set pipe
|
||||
// params, though.
|
||||
// And ideally a little wrapper helper for only changing native stuff on content
|
||||
// change created.
|
||||
this.detector =
|
||||
new RknnObjectDetector(
|
||||
NeuralNetworkModelManager.getInstance().getDefaultRknnModel().getAbsolutePath(),
|
||||
NeuralNetworkModelManager.getInstance().getLabels(),
|
||||
NeuralNetworkModelManager.getInstance().getModelVersion());
|
||||
}
|
||||
|
||||
private static class Letterbox {
|
||||
double dx;
|
||||
double dy;
|
||||
double scale;
|
||||
|
||||
public Letterbox(double dx, double dy, double scale) {
|
||||
this.dx = dx;
|
||||
this.dy = dy;
|
||||
this.scale = scale;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected List<NeuralNetworkPipeResult> process(CVMat in) {
|
||||
var frame = in.getMat();
|
||||
|
||||
// Make sure we don't get a weird empty frame
|
||||
if (frame.empty()) {
|
||||
return List.of();
|
||||
}
|
||||
|
||||
// letterbox
|
||||
var letterboxed = new Mat();
|
||||
var scale =
|
||||
letterbox(frame, letterboxed, new Size(640, 640), ColorHelper.colorToScalar(Color.GRAY));
|
||||
|
||||
if (letterboxed.width() != 640 || letterboxed.height() != 640) {
|
||||
// huh whack give up lol
|
||||
throw new RuntimeException("RGA bugged but still wrong size");
|
||||
}
|
||||
var ret = detector.detect(letterboxed, params.nms, params.confidence);
|
||||
|
||||
return resizeDetections(ret, scale);
|
||||
}
|
||||
|
||||
private List<NeuralNetworkPipeResult> resizeDetections(
|
||||
List<NeuralNetworkPipeResult> unscaled, Letterbox letterbox) {
|
||||
var ret = new ArrayList<NeuralNetworkPipeResult>();
|
||||
|
||||
for (var t : unscaled) {
|
||||
var scale = 1.0 / letterbox.scale;
|
||||
var boundingBox = t.bbox;
|
||||
double x = (boundingBox.x - letterbox.dx) * scale;
|
||||
double y = (boundingBox.y - letterbox.dy) * scale;
|
||||
double width = boundingBox.width * scale;
|
||||
double height = boundingBox.height * scale;
|
||||
|
||||
ret.add(
|
||||
new NeuralNetworkPipeResult(new Rect2d(x, y, width, height), t.classIdx, t.confidence));
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
private static Letterbox letterbox(Mat frame, Mat letterboxed, Size newShape, Scalar color) {
|
||||
// from https://github.com/ultralytics/yolov5/issues/8427#issuecomment-1172469631
|
||||
var frameSize = frame.size();
|
||||
var r = Math.min(newShape.height / frameSize.height, newShape.width / frameSize.width);
|
||||
|
||||
var newUnpad = new Size(Math.round(frameSize.width * r), Math.round(frameSize.height * r));
|
||||
|
||||
if (!(frameSize.equals(newUnpad))) {
|
||||
Imgproc.resize(frame, letterboxed, newUnpad, Imgproc.INTER_LINEAR);
|
||||
} else {
|
||||
frame.copyTo(letterboxed);
|
||||
}
|
||||
|
||||
var dw = newShape.width - newUnpad.width;
|
||||
var dh = newShape.height - newUnpad.height;
|
||||
|
||||
dw /= 2;
|
||||
dh /= 2;
|
||||
|
||||
int top = (int) (Math.round(dh - 0.1f));
|
||||
int bottom = (int) (Math.round(dh + 0.1f));
|
||||
int left = (int) (Math.round(dw - 0.1f));
|
||||
int right = (int) (Math.round(dw + 0.1f));
|
||||
Core.copyMakeBorder(
|
||||
letterboxed, letterboxed, top, bottom, left, right, Core.BORDER_CONSTANT, color);
|
||||
|
||||
return new Letterbox(dw, dh, r);
|
||||
}
|
||||
|
||||
public static class RknnDetectionPipeParams {
|
||||
public double confidence;
|
||||
public double nms;
|
||||
public int max_detections;
|
||||
|
||||
public RknnDetectionPipeParams() {}
|
||||
}
|
||||
|
||||
public List<String> getClassNames() {
|
||||
return detector.getClasses();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void release() {
|
||||
detector.release();
|
||||
}
|
||||
}
|
||||
@@ -42,6 +42,7 @@ public class SortContoursPipe
|
||||
if (params.getSortMode() != ContourSortMode.Centermost) {
|
||||
m_sortedContours.sort(params.getSortMode().getComparator());
|
||||
} else {
|
||||
// we need knowledge of camera properties to calculate this distance -- do it ourselves
|
||||
m_sortedContours.sort(Comparator.comparingDouble(this::calcSquareCenterDistance));
|
||||
}
|
||||
}
|
||||
@@ -50,10 +51,10 @@ public class SortContoursPipe
|
||||
m_sortedContours.subList(0, Math.min(in.size(), params.getMaxTargets())));
|
||||
}
|
||||
|
||||
private double calcSquareCenterDistance(PotentialTarget rect) {
|
||||
private double calcSquareCenterDistance(PotentialTarget tgt) {
|
||||
return Math.sqrt(
|
||||
Math.pow(params.getCamProperties().centerX - rect.getMinAreaRect().center.x, 2)
|
||||
+ Math.pow(params.getCamProperties().centerY - rect.getMinAreaRect().center.y, 2));
|
||||
Math.pow(params.getCamProperties().centerX - tgt.getMinAreaRect().center.x, 2)
|
||||
+ Math.pow(params.getCamProperties().centerY - tgt.getMinAreaRect().center.y, 2));
|
||||
}
|
||||
|
||||
public static class SortContoursParams {
|
||||
|
||||
@@ -221,4 +221,11 @@ public class AprilTagPipeline extends CVPipeline<CVPipelineResult, AprilTagPipel
|
||||
|
||||
return new CVPipelineResult(sumPipeNanosElapsed, fps, targetList, multiTagResult, frame);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void release() {
|
||||
aprilTagDetectionPipe.release();
|
||||
singleTagPoseEstimatorPipe.release();
|
||||
super.release();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,8 +60,8 @@ import org.photonvision.vision.target.TrackedTarget;
|
||||
import org.photonvision.vision.target.TrackedTarget.TargetCalculationParameters;
|
||||
|
||||
public class ArucoPipeline extends CVPipeline<CVPipelineResult, ArucoPipelineSettings> {
|
||||
private final ArucoDetectionPipe arucoDetectionPipe = new ArucoDetectionPipe();
|
||||
private final ArucoPoseEstimatorPipe singleTagPoseEstimatorPipe = new ArucoPoseEstimatorPipe();
|
||||
private ArucoDetectionPipe arucoDetectionPipe = new ArucoDetectionPipe();
|
||||
private ArucoPoseEstimatorPipe singleTagPoseEstimatorPipe = new ArucoPoseEstimatorPipe();
|
||||
private final MultiTargetPNPPipe multiTagPNPPipe = new MultiTargetPNPPipe();
|
||||
private final CalculateFPSPipe calculateFPSPipe = new CalculateFPSPipe();
|
||||
|
||||
@@ -250,4 +250,13 @@ public class ArucoPipeline extends CVPipeline<CVPipelineResult, ArucoPipelineSet
|
||||
windowSize,
|
||||
constant);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void release() {
|
||||
arucoDetectionPipe.release();
|
||||
singleTagPoseEstimatorPipe.release();
|
||||
arucoDetectionPipe = null;
|
||||
singleTagPoseEstimatorPipe = null;
|
||||
super.release();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,15 +21,22 @@ import org.photonvision.vision.camera.QuirkyCamera;
|
||||
import org.photonvision.vision.frame.Frame;
|
||||
import org.photonvision.vision.frame.FrameStaticProperties;
|
||||
import org.photonvision.vision.frame.FrameThresholdType;
|
||||
import org.photonvision.vision.opencv.Releasable;
|
||||
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 {
|
||||
static final int MAX_MULTI_TARGET_RESULTS = 10;
|
||||
|
||||
protected S settings;
|
||||
protected FrameStaticProperties frameStaticProperties;
|
||||
protected QuirkyCamera cameraQuirks;
|
||||
|
||||
private final FrameThresholdType thresholdType;
|
||||
|
||||
// So releaseable doesn't keep track of if we double-free something. so (ew) remember that here
|
||||
protected volatile boolean released = false;
|
||||
|
||||
public CVPipeline(FrameThresholdType thresholdType) {
|
||||
this.thresholdType = thresholdType;
|
||||
}
|
||||
@@ -60,6 +67,9 @@ public abstract class CVPipeline<R extends CVPipelineResult, S extends CVPipelin
|
||||
}
|
||||
|
||||
public R run(Frame frame, QuirkyCamera cameraQuirks) {
|
||||
if (released) {
|
||||
throw new RuntimeException("Pipeline use-after-free!");
|
||||
}
|
||||
if (settings == null) {
|
||||
throw new RuntimeException("No settings provided for pipeline!");
|
||||
}
|
||||
@@ -75,4 +85,13 @@ public abstract class CVPipeline<R extends CVPipelineResult, S extends CVPipelin
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Release any native memory associated with this pipeline. Called by pipelinemanager at pipeline
|
||||
* switch. Stubbed out, but override if needed.
|
||||
*/
|
||||
@Override
|
||||
public void release() {
|
||||
released = true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,7 +32,8 @@ import org.photonvision.vision.opencv.ImageRotationMode;
|
||||
@JsonSubTypes.Type(value = ReflectivePipelineSettings.class),
|
||||
@JsonSubTypes.Type(value = DriverModePipelineSettings.class),
|
||||
@JsonSubTypes.Type(value = AprilTagPipelineSettings.class),
|
||||
@JsonSubTypes.Type(value = ArucoPipelineSettings.class)
|
||||
@JsonSubTypes.Type(value = ArucoPipelineSettings.class),
|
||||
@JsonSubTypes.Type(value = ObjectDetectionPipelineSettings.class)
|
||||
})
|
||||
public class CVPipelineSettings implements Cloneable {
|
||||
public int pipelineIndex = 0;
|
||||
|
||||
@@ -109,7 +109,7 @@ public class ColoredShapePipeline
|
||||
SortContoursPipe.SortContoursParams sortContoursParams =
|
||||
new SortContoursPipe.SortContoursParams(
|
||||
settings.contourSortMode,
|
||||
settings.outputShowMultipleTargets ? 5 : 1,
|
||||
settings.outputShowMultipleTargets ? MAX_MULTI_TARGET_RESULTS : 1,
|
||||
frameStaticProperties); // TODO don't hardcode?
|
||||
sortContoursPipe.setParams(sortContoursParams);
|
||||
|
||||
|
||||
@@ -88,4 +88,9 @@ public class DriverModePipeline
|
||||
fps,
|
||||
new Frame(frame.processedImage, frame.colorImage, frame.type, frame.frameStaticProperties));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void release() {
|
||||
// we never actually need to give resources up since pipelinemanager only makes one of us
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,136 @@
|
||||
/*
|
||||
* 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.pipeline;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
import org.photonvision.vision.frame.Frame;
|
||||
import org.photonvision.vision.frame.FrameThresholdType;
|
||||
import org.photonvision.vision.opencv.DualOffsetValues;
|
||||
import org.photonvision.vision.pipe.CVPipe.CVPipeResult;
|
||||
import org.photonvision.vision.pipe.impl.*;
|
||||
import org.photonvision.vision.pipe.impl.RknnDetectionPipe.RknnDetectionPipeParams;
|
||||
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;
|
||||
|
||||
public class ObjectDetectionPipeline
|
||||
extends CVPipeline<CVPipelineResult, ObjectDetectionPipelineSettings> {
|
||||
private final CalculateFPSPipe calculateFPSPipe = new CalculateFPSPipe();
|
||||
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;
|
||||
|
||||
public ObjectDetectionPipeline() {
|
||||
super(PROCESSING_TYPE);
|
||||
settings = new ObjectDetectionPipelineSettings();
|
||||
}
|
||||
|
||||
public ObjectDetectionPipeline(ObjectDetectionPipelineSettings settings) {
|
||||
super(PROCESSING_TYPE);
|
||||
this.settings = settings;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void setPipeParamsImpl() {
|
||||
// this needs to be based off of the current backend selected!!
|
||||
var params = new RknnDetectionPipeParams();
|
||||
params.confidence = settings.confidence;
|
||||
params.nms = settings.nms;
|
||||
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
|
||||
protected CVPipelineResult process(Frame input_frame, ObjectDetectionPipelineSettings settings) {
|
||||
long sumPipeNanosElapsed = 0;
|
||||
|
||||
// ***************** change based on backend ***********************
|
||||
|
||||
CVPipeResult<List<NeuralNetworkPipeResult>> rknnResult = rknnPipe.run(input_frame.colorImage);
|
||||
sumPipeNanosElapsed += rknnResult.nanosElapsed;
|
||||
List<NeuralNetworkPipeResult> targetList;
|
||||
|
||||
var names = rknnPipe.getClassNames();
|
||||
|
||||
input_frame.colorImage.getMat().copyTo(input_frame.processedImage.getMat());
|
||||
|
||||
// ***************** change based on backend ***********************
|
||||
|
||||
var filterContoursResult = filterContoursPipe.run(rknnResult.output);
|
||||
sumPipeNanosElapsed += filterContoursResult.nanosElapsed;
|
||||
|
||||
CVPipeResult<List<PotentialTarget>> sortContoursResult =
|
||||
sortContoursPipe.run(
|
||||
filterContoursResult.output.stream()
|
||||
.map(shape -> new PotentialTarget(shape))
|
||||
.collect(Collectors.toList()));
|
||||
sumPipeNanosElapsed += sortContoursResult.nanosElapsed;
|
||||
|
||||
CVPipeResult<List<TrackedTarget>> collect2dTargetsResult =
|
||||
collect2dTargetsPipe.run(sortContoursResult.output);
|
||||
sumPipeNanosElapsed += collect2dTargetsResult.nanosElapsed;
|
||||
|
||||
var fpsResult = calculateFPSPipe.run(null);
|
||||
var fps = fpsResult.output;
|
||||
|
||||
return new CVPipelineResult(
|
||||
sumPipeNanosElapsed, fps, collect2dTargetsResult.output, input_frame, names);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void release() {
|
||||
rknnPipe.release();
|
||||
super.release();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
/*
|
||||
* 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.pipeline;
|
||||
|
||||
public class ObjectDetectionPipelineSettings extends AdvancedPipelineSettings {
|
||||
public double confidence;
|
||||
public double nms; // non maximal suppression
|
||||
|
||||
public ObjectDetectionPipelineSettings() {
|
||||
super();
|
||||
this.pipelineType = PipelineType.ObjectDetection; // TODO: FIX this
|
||||
this.outputShowMultipleTargets = true;
|
||||
cameraExposure = 20;
|
||||
cameraAutoExposure = false;
|
||||
ledMode = false;
|
||||
confidence = .9;
|
||||
nms = .45;
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,8 @@
|
||||
|
||||
package org.photonvision.vision.pipeline;
|
||||
|
||||
import org.photonvision.vision.pipe.impl.Calibrate3dPipeline;
|
||||
|
||||
@SuppressWarnings("rawtypes")
|
||||
public enum PipelineType {
|
||||
Calib3d(-2, Calibrate3dPipeline.class),
|
||||
@@ -24,7 +26,8 @@ public enum PipelineType {
|
||||
Reflective(0, ReflectivePipeline.class),
|
||||
ColoredShape(1, ColoredShapePipeline.class),
|
||||
AprilTag(2, AprilTagPipeline.class),
|
||||
Aruco(3, ArucoPipeline.class);
|
||||
Aruco(3, ArucoPipeline.class),
|
||||
ObjectDetection(4, ObjectDetectionPipeline.class);
|
||||
|
||||
public final int baseIndex;
|
||||
public final Class clazz;
|
||||
|
||||
@@ -64,29 +64,6 @@ public class ReflectivePipeline extends CVPipeline<CVPipelineResult, ReflectiveP
|
||||
settings.offsetDualPointB,
|
||||
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();
|
||||
findContoursPipe.setParams(findContoursParams);
|
||||
|
||||
@@ -113,7 +90,7 @@ public class ReflectivePipeline extends CVPipeline<CVPipelineResult, ReflectiveP
|
||||
var sortContoursParams =
|
||||
new SortContoursPipe.SortContoursParams(
|
||||
settings.contourSortMode,
|
||||
settings.outputShowMultipleTargets ? 8 : 1, // TODO don't hardcode?
|
||||
settings.outputShowMultipleTargets ? MAX_MULTI_TARGET_RESULTS : 1,
|
||||
frameStaticProperties);
|
||||
sortContoursPipe.setParams(sortContoursParams);
|
||||
|
||||
|
||||
@@ -32,10 +32,20 @@ public class CVPipelineResult implements Releasable {
|
||||
public final List<TrackedTarget> targets;
|
||||
public final Frame inputAndOutputFrame;
|
||||
public MultiTargetPNPResult multiTagResult;
|
||||
public final List<String> objectDetectionClassNames;
|
||||
|
||||
public CVPipelineResult(
|
||||
double processingNanos, double fps, List<TrackedTarget> targets, Frame inputFrame) {
|
||||
this(processingNanos, fps, targets, new MultiTargetPNPResult(), inputFrame);
|
||||
this(processingNanos, fps, targets, new MultiTargetPNPResult(), inputFrame, List.of());
|
||||
}
|
||||
|
||||
public CVPipelineResult(
|
||||
double processingNanos,
|
||||
double fps,
|
||||
List<TrackedTarget> targets,
|
||||
Frame inputFrame,
|
||||
List<String> classNames) {
|
||||
this(processingNanos, fps, targets, new MultiTargetPNPResult(), inputFrame, classNames);
|
||||
}
|
||||
|
||||
public CVPipelineResult(
|
||||
@@ -44,10 +54,21 @@ public class CVPipelineResult implements Releasable {
|
||||
List<TrackedTarget> targets,
|
||||
MultiTargetPNPResult multiTagResult,
|
||||
Frame inputFrame) {
|
||||
this(processingNanos, fps, targets, multiTagResult, inputFrame, List.of());
|
||||
}
|
||||
|
||||
public CVPipelineResult(
|
||||
double processingNanos,
|
||||
double fps,
|
||||
List<TrackedTarget> targets,
|
||||
MultiTargetPNPResult multiTagResult,
|
||||
Frame inputFrame,
|
||||
List<String> classNames) {
|
||||
this.processingNanos = processingNanos;
|
||||
this.fps = fps;
|
||||
this.targets = targets != null ? targets : Collections.emptyList();
|
||||
this.multiTagResult = multiTagResult;
|
||||
this.objectDetectionClassNames = classNames;
|
||||
|
||||
this.inputAndOutputFrame = inputFrame;
|
||||
}
|
||||
@@ -57,7 +78,7 @@ public class CVPipelineResult implements Releasable {
|
||||
double fps,
|
||||
List<TrackedTarget> targets,
|
||||
MultiTargetPNPResult multiTagResult) {
|
||||
this(processingNanos, fps, targets, multiTagResult, null);
|
||||
this(processingNanos, fps, targets, multiTagResult, null, List.of());
|
||||
}
|
||||
|
||||
public boolean hasTargets() {
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
/*
|
||||
* Copyright (C) Photon Vision.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.photonvision.vision.processes;
|
||||
|
||||
import java.util.List;
|
||||
import org.photonvision.vision.camera.CameraType;
|
||||
|
||||
public class CameraMatchingOptions {
|
||||
public CameraMatchingOptions(
|
||||
boolean checkUSBPath,
|
||||
boolean checkVidPid,
|
||||
boolean checkBaseName,
|
||||
boolean checkPath,
|
||||
CameraType... allowedTypes) {
|
||||
this.checkUSBPath = checkUSBPath;
|
||||
this.checkVidPid = checkVidPid;
|
||||
this.checkBaseName = checkBaseName;
|
||||
this.checkPath = checkPath;
|
||||
this.allowedTypes = List.of(allowedTypes);
|
||||
}
|
||||
|
||||
public final boolean checkUSBPath;
|
||||
public final boolean checkVidPid;
|
||||
public final boolean checkBaseName;
|
||||
public final boolean checkPath;
|
||||
public final List<CameraType> allowedTypes;
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "CameraMatchingOptions [checkUSBPath="
|
||||
+ checkUSBPath
|
||||
+ ", checkVidPid="
|
||||
+ checkVidPid
|
||||
+ ", checkBaseName="
|
||||
+ checkBaseName
|
||||
+ ", checkPath="
|
||||
+ checkPath
|
||||
+ ", allowedTypes="
|
||||
+ allowedTypes
|
||||
+ "]";
|
||||
}
|
||||
}
|
||||
@@ -27,6 +27,7 @@ import org.photonvision.common.dataflow.DataChangeService;
|
||||
import org.photonvision.common.dataflow.events.OutgoingUIEvent;
|
||||
import org.photonvision.common.logging.LogGroup;
|
||||
import org.photonvision.common.logging.Logger;
|
||||
import org.photonvision.vision.pipe.impl.Calibrate3dPipeline;
|
||||
import org.photonvision.vision.pipeline.*;
|
||||
|
||||
@SuppressWarnings({"rawtypes", "unused"})
|
||||
@@ -41,7 +42,7 @@ public class PipelineManager {
|
||||
protected final DriverModePipeline driverModePipeline = new DriverModePipeline();
|
||||
|
||||
/** Index of the currently active pipeline. Defaults to 0. */
|
||||
private int currentPipelineIndex = 0;
|
||||
private int currentPipelineIndex = DRIVERMODE_INDEX;
|
||||
|
||||
/** The currently active pipeline. */
|
||||
private CVPipeline currentUserPipeline = driverModePipeline;
|
||||
@@ -60,7 +61,8 @@ public class PipelineManager {
|
||||
PipelineManager(
|
||||
DriverModePipelineSettings driverSettings,
|
||||
List<CVPipelineSettings> userPipelines,
|
||||
String uniqueName) {
|
||||
String uniqueName,
|
||||
int defaultIndex) {
|
||||
this.userPipelineSettings = new ArrayList<>(userPipelines);
|
||||
// This is to respect the default res idx for vendor cameras
|
||||
|
||||
@@ -69,10 +71,19 @@ public class PipelineManager {
|
||||
if (userPipelines.isEmpty()) addPipeline(PipelineType.Reflective);
|
||||
|
||||
calibration3dPipeline = new Calibrate3dPipeline(uniqueName);
|
||||
|
||||
// We know that at this stage, VisionRunner hasn't yet started so we're good to do this from
|
||||
// this thread
|
||||
this.setIndex(defaultIndex);
|
||||
updatePipelineFromRequested();
|
||||
}
|
||||
|
||||
public PipelineManager(CameraConfiguration config) {
|
||||
this(config.driveModeSettings, config.pipelineSettings, config.uniqueName);
|
||||
this(
|
||||
config.driveModeSettings,
|
||||
config.pipelineSettings,
|
||||
config.uniqueName,
|
||||
config.currentPipelineIndex);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -147,6 +158,7 @@ public class PipelineManager {
|
||||
* @return The currently active pipeline.
|
||||
*/
|
||||
public CVPipeline getCurrentPipeline() {
|
||||
updatePipelineFromRequested();
|
||||
if (currentPipelineIndex < 0) {
|
||||
switch (currentPipelineIndex) {
|
||||
case CAL_3D_INDEX:
|
||||
@@ -169,6 +181,16 @@ public class PipelineManager {
|
||||
return getPipelineSettings(currentPipelineIndex);
|
||||
}
|
||||
|
||||
private volatile int requestedIndex = 0;
|
||||
|
||||
/**
|
||||
* Grab the currently requested pipeline index. The VisionRunner may not have changed over to this
|
||||
* pipeline yet.
|
||||
*/
|
||||
public int getRequestedIndex() {
|
||||
return requestedIndex;
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal method for setting the active pipeline. <br>
|
||||
* <br>
|
||||
@@ -178,6 +200,22 @@ public class PipelineManager {
|
||||
* @param newIndex Index of pipeline to be active
|
||||
*/
|
||||
private void setPipelineInternal(int newIndex) {
|
||||
requestedIndex = newIndex;
|
||||
}
|
||||
|
||||
/**
|
||||
* Based on a requested pipeline index, create/destroy pipelines as necessary. We do this as a
|
||||
* side effect of the main thread that calls getCurrentPipeline to avoid race conditions between
|
||||
* server threads and the VisionRunner TODO: this should be refactored. Shame Java doesn't have
|
||||
* RAII
|
||||
*/
|
||||
private void updatePipelineFromRequested() {
|
||||
int newIndex = requestedIndex;
|
||||
if (newIndex == currentPipelineIndex) {
|
||||
// nothing to do, probably no change -- give up
|
||||
return;
|
||||
}
|
||||
|
||||
if (newIndex < 0 && currentPipelineIndex >= 0) {
|
||||
// Transitioning to a built-in pipe, save off the current user one
|
||||
lastUserPipelineIdx = currentPipelineIndex;
|
||||
@@ -188,6 +226,11 @@ public class PipelineManager {
|
||||
return;
|
||||
}
|
||||
|
||||
// Cleanup potential old native resources before swapping over for user pipelines
|
||||
if (currentUserPipeline != null && !(newIndex < 0)) {
|
||||
currentUserPipeline.release();
|
||||
}
|
||||
|
||||
currentPipelineIndex = newIndex;
|
||||
if (newIndex >= 0) {
|
||||
var desiredPipelineSettings = userPipelineSettings.get(currentPipelineIndex);
|
||||
@@ -212,6 +255,11 @@ public class PipelineManager {
|
||||
logger.debug("Creating Aruco Pipeline");
|
||||
currentUserPipeline = new ArucoPipeline((ArucoPipelineSettings) desiredPipelineSettings);
|
||||
break;
|
||||
case ObjectDetection:
|
||||
logger.debug("Creating ObjectDetection Pipeline");
|
||||
currentUserPipeline =
|
||||
new ObjectDetectionPipeline(
|
||||
(ObjectDetectionPipelineSettings) desiredPipelineSettings);
|
||||
default:
|
||||
// Can be calib3d or drivermode, both of which are special cases
|
||||
break;
|
||||
@@ -313,6 +361,12 @@ public class PipelineManager {
|
||||
added.pipelineNickname = nickname;
|
||||
return added;
|
||||
}
|
||||
case ObjectDetection:
|
||||
{
|
||||
var added = new ObjectDetectionPipelineSettings();
|
||||
added.pipelineNickname = nickname;
|
||||
return added;
|
||||
}
|
||||
default:
|
||||
{
|
||||
logger.error("Got invalid pipeline type: " + type);
|
||||
|
||||
@@ -26,6 +26,7 @@ import java.util.HashMap;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.function.BiConsumer;
|
||||
import java.util.stream.Collectors;
|
||||
import org.opencv.core.Size;
|
||||
import org.photonvision.common.configuration.CameraConfiguration;
|
||||
import org.photonvision.common.configuration.ConfigManager;
|
||||
@@ -142,7 +143,7 @@ public class VisionModule {
|
||||
ntConsumer =
|
||||
new NTDataPublisher(
|
||||
visionSource.getSettables().getConfiguration().nickname,
|
||||
pipelineManager::getCurrentPipelineIndex,
|
||||
pipelineManager::getRequestedIndex,
|
||||
this::setPipeline,
|
||||
pipelineManager::getDriverMode,
|
||||
this::setDriverMode);
|
||||
@@ -155,6 +156,7 @@ public class VisionModule {
|
||||
(result) ->
|
||||
lastPipelineResultBestTarget = result.hasTargets() ? result.targets.get(0) : null);
|
||||
|
||||
// Sync VisionModule state with the first pipeline index
|
||||
setPipeline(visionSource.getSettables().getConfiguration().currentPipelineIndex);
|
||||
|
||||
// Set vendor FOV
|
||||
@@ -320,7 +322,7 @@ public class VisionModule {
|
||||
|
||||
void changePipelineType(int newType) {
|
||||
pipelineManager.changePipelineType(newType);
|
||||
setPipeline(pipelineManager.getCurrentPipelineIndex());
|
||||
setPipeline(pipelineManager.getRequestedIndex());
|
||||
saveAndBroadcastAll();
|
||||
}
|
||||
|
||||
@@ -328,9 +330,7 @@ public class VisionModule {
|
||||
pipelineManager.setDriverMode(isDriverMode);
|
||||
setVisionLEDs(!isDriverMode);
|
||||
setPipeline(
|
||||
isDriverMode
|
||||
? PipelineManager.DRIVERMODE_INDEX
|
||||
: pipelineManager.getCurrentPipelineIndex());
|
||||
isDriverMode ? PipelineManager.DRIVERMODE_INDEX : pipelineManager.getRequestedIndex());
|
||||
saveAndBroadcastAll();
|
||||
}
|
||||
|
||||
@@ -384,7 +384,7 @@ public class VisionModule {
|
||||
var ret = pipelineManager.calibration3dPipeline.tryCalibration();
|
||||
pipelineManager.setCalibrationMode(false);
|
||||
|
||||
setPipeline(pipelineManager.getCurrentPipelineIndex());
|
||||
setPipeline(pipelineManager.getRequestedIndex());
|
||||
|
||||
if (ret != null) {
|
||||
logger.debug("Saving calibration...");
|
||||
@@ -446,7 +446,7 @@ public class VisionModule {
|
||||
setVisionLEDs(pipelineSettings.ledMode);
|
||||
|
||||
visionSource.getSettables().getConfiguration().currentPipelineIndex =
|
||||
pipelineManager.getCurrentPipelineIndex();
|
||||
pipelineManager.getRequestedIndex();
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -510,7 +510,7 @@ public class VisionModule {
|
||||
ret.uniqueName = visionSource.getSettables().getConfiguration().uniqueName;
|
||||
ret.currentPipelineSettings =
|
||||
SerializationUtils.objectToHashMap(pipelineManager.getCurrentPipelineSettings());
|
||||
ret.currentPipelineIndex = pipelineManager.getCurrentPipelineIndex();
|
||||
ret.currentPipelineIndex = pipelineManager.getRequestedIndex();
|
||||
ret.pipelineNicknames = pipelineManager.getPipelineNicknames();
|
||||
ret.cameraQuirks = visionSource.getSettables().getConfiguration().cameraQuirks;
|
||||
|
||||
@@ -536,7 +536,10 @@ public class VisionModule {
|
||||
ret.outputStreamPort = this.outputStreamPort;
|
||||
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 =
|
||||
!(ConfigManager.getInstance().getConfig().getHardwareConfig().hasPresetFOV()
|
||||
@@ -549,7 +552,7 @@ public class VisionModule {
|
||||
var config = visionSource.getSettables().getConfiguration();
|
||||
config.setPipelineSettings(pipelineManager.userPipelineSettings);
|
||||
config.driveModeSettings = pipelineManager.driverModePipeline.getSettings();
|
||||
config.currentPipelineIndex = Math.max(pipelineManager.getCurrentPipelineIndex(), -1);
|
||||
config.currentPipelineIndex = Math.max(pipelineManager.getRequestedIndex(), -1);
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
@@ -88,7 +88,7 @@ public class VisionModuleChangeSubscriber extends DataChangeSubscriber {
|
||||
parentModule.saveAndBroadcastAll();
|
||||
return;
|
||||
case "deleteCurrPipeline":
|
||||
var indexToDelete = parentModule.pipelineManager.getCurrentPipelineIndex();
|
||||
var indexToDelete = parentModule.pipelineManager.getRequestedIndex();
|
||||
logger.info("Deleting current pipe at index " + indexToDelete);
|
||||
int newIndex = parentModule.pipelineManager.removePipeline(indexToDelete);
|
||||
parentModule.setPipeline(newIndex);
|
||||
@@ -96,7 +96,7 @@ public class VisionModuleChangeSubscriber extends DataChangeSubscriber {
|
||||
return;
|
||||
case "changePipeline": // change active pipeline
|
||||
var index = (Integer) newPropValue;
|
||||
if (index == parentModule.pipelineManager.getCurrentPipelineIndex()) {
|
||||
if (index == parentModule.pipelineManager.getRequestedIndex()) {
|
||||
logger.debug("Skipping pipeline change, index " + index + " already active");
|
||||
return;
|
||||
}
|
||||
@@ -181,6 +181,9 @@ public class VisionModuleChangeSubscriber extends DataChangeSubscriber {
|
||||
parentModule.changePipelineType((Integer) newPropValue);
|
||||
parentModule.saveAndBroadcastAll();
|
||||
return;
|
||||
case "isDriverMode":
|
||||
parentModule.setDriverMode((Boolean) newPropValue);
|
||||
return;
|
||||
}
|
||||
|
||||
// special case for camera settables
|
||||
|
||||
@@ -98,8 +98,7 @@ public class VisionRunner {
|
||||
var pipelineResult = pipeline.run(frame, cameraQuirks);
|
||||
pipelineResultConsumer.accept(pipelineResult);
|
||||
} catch (Exception ex) {
|
||||
logger.error("Exception on loop " + loopCount);
|
||||
ex.printStackTrace();
|
||||
logger.error("Exception on loop " + loopCount, ex);
|
||||
}
|
||||
|
||||
loopCount++;
|
||||
|
||||
@@ -23,6 +23,7 @@ import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CopyOnWriteArrayList;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.stream.Collectors;
|
||||
import org.photonvision.common.configuration.CameraConfiguration;
|
||||
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.CameraType;
|
||||
import org.photonvision.vision.camera.LibcameraGpuSource;
|
||||
import org.photonvision.vision.camera.TestSource;
|
||||
import org.photonvision.vision.camera.USBCameraSource;
|
||||
|
||||
public class VisionSourceManager {
|
||||
@@ -145,8 +147,8 @@ public class VisionSourceManager {
|
||||
}
|
||||
|
||||
// Return no new sources because there are no new sources
|
||||
if (connectedCameras.isEmpty() && !cameraInfos.isEmpty()) {
|
||||
if (hasWarnedNoCameras) {
|
||||
if (connectedCameras.isEmpty()) {
|
||||
if (!hasWarnedNoCameras) {
|
||||
logger.warn(
|
||||
"No cameras were detected! Check that all cameras are connected, and that the path is correct.");
|
||||
hasWarnedNoCameras = true;
|
||||
@@ -164,7 +166,7 @@ public class VisionSourceManager {
|
||||
|
||||
// Debug prints
|
||||
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())
|
||||
@@ -185,7 +187,7 @@ public class VisionSourceManager {
|
||||
"Unloaded configs: "
|
||||
+ unmatchedLoadedConfigs.stream()
|
||||
.map(it -> it.nickname)
|
||||
.collect(Collectors.joining()));
|
||||
.collect(Collectors.joining(", ")));
|
||||
hasWarned = true;
|
||||
}
|
||||
|
||||
@@ -194,13 +196,8 @@ public class VisionSourceManager {
|
||||
|
||||
if (matchedCameras.isEmpty()) return null;
|
||||
|
||||
// for unit tests only!
|
||||
if (!createSources) {
|
||||
return List.of();
|
||||
}
|
||||
|
||||
// Turn these camera configs into vision sources
|
||||
var sources = loadVisionSourcesFromCamConfigs(matchedCameras);
|
||||
var sources = loadVisionSourcesFromCamConfigs(matchedCameras, createSources);
|
||||
|
||||
// Print info about each vision source
|
||||
for (var src : sources) {
|
||||
@@ -216,6 +213,54 @@ public class VisionSourceManager {
|
||||
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));
|
||||
}
|
||||
|
||||
matches &= (physicalCamera.cameraType == savedConfig.cameraType);
|
||||
|
||||
return matches;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Create {@link CameraConfiguration}s based on a list of detected USB cameras and the configs on
|
||||
* disk.
|
||||
@@ -226,158 +271,212 @@ public class VisionSourceManager {
|
||||
*/
|
||||
public List<CameraConfiguration> matchCameras(
|
||||
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);
|
||||
ArrayList<CameraConfiguration> cameraConfigurations = new ArrayList<CameraConfiguration>();
|
||||
ArrayList<CameraConfiguration> unloadedConfigs =
|
||||
new ArrayList<CameraConfiguration>(loadedCamConfigs);
|
||||
|
||||
if (detectedCameraList.size() > 0 || unloadedConfigs.size() > 0)
|
||||
cameraConfigurations.addAll(matchByPathByID(detectedCameraList, unloadedConfigs));
|
||||
else logger.debug("Skipping matchByPath no configs or cameras left to match");
|
||||
logger.info("Matching CSI cameras by port & base name...");
|
||||
cameraConfigurations.addAll(
|
||||
matchCamerasByStrategy(
|
||||
detectedCameraList,
|
||||
unloadedConfigs,
|
||||
new CameraMatchingOptions(false, false, true, true, CameraType.ZeroCopyPicam)));
|
||||
|
||||
if (detectedCameraList.size() > 0 || unloadedConfigs.size() > 0)
|
||||
cameraConfigurations.addAll(matchByPath(detectedCameraList, unloadedConfigs));
|
||||
else logger.debug("Skipping matchByPath no configs or cameras left to match");
|
||||
logger.info("Matching USB cameras by usb port & name & USB VID/PID...");
|
||||
cameraConfigurations.addAll(
|
||||
matchCamerasByStrategy(
|
||||
detectedCameraList,
|
||||
unloadedConfigs,
|
||||
new CameraMatchingOptions(true, true, true, false, CameraType.UsbCamera)));
|
||||
|
||||
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)
|
||||
// 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) {
|
||||
logger.info("Matching USB cameras by windows-path & USB VID/PID only...");
|
||||
cameraConfigurations.addAll(
|
||||
createConfigsForCameras(detectedCameraList, unloadedConfigs, cameraConfigurations));
|
||||
matchCamerasByStrategy(
|
||||
detectedCameraList,
|
||||
unloadedConfigs,
|
||||
new CameraMatchingOptions(false, true, true, true, CameraType.UsbCamera)));
|
||||
}
|
||||
|
||||
logger.info("Matching USB cameras by usb port & USB VID/PID...");
|
||||
cameraConfigurations.addAll(
|
||||
matchCamerasByStrategy(
|
||||
detectedCameraList,
|
||||
unloadedConfigs,
|
||||
new CameraMatchingOptions(true, true, false, false, CameraType.UsbCamera)));
|
||||
|
||||
// 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
|
||||
logger.info("Matching USB cameras by base-name & usb port...");
|
||||
cameraConfigurations.addAll(
|
||||
matchCamerasByStrategy(
|
||||
detectedCameraList,
|
||||
unloadedConfigs,
|
||||
new CameraMatchingOptions(true, false, true, false, CameraType.UsbCamera)));
|
||||
|
||||
// handle disabling only-by-base-name matching
|
||||
if (!matchCamerasOnlyByPath) {
|
||||
logger.info("Matching USB cameras by base-name & USB VID/PID only...");
|
||||
cameraConfigurations.addAll(
|
||||
matchCamerasByStrategy(
|
||||
detectedCameraList,
|
||||
unloadedConfigs,
|
||||
new CameraMatchingOptions(false, true, true, false, CameraType.UsbCamera)));
|
||||
|
||||
// Legacy migration for if no USB VID/PID set
|
||||
logger.info("Matching USB cameras by base-name only...");
|
||||
cameraConfigurations.addAll(
|
||||
matchCamerasByStrategy(
|
||||
detectedCameraList,
|
||||
unloadedConfigs,
|
||||
new CameraMatchingOptions(false, false, true, false, CameraType.UsbCamera)));
|
||||
} 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!");
|
||||
return cameraConfigurations;
|
||||
}
|
||||
|
||||
// loop over all the configs loaded from disk, attempting to match each camera
|
||||
// to a config by path-by-id on linux
|
||||
private List<CameraConfiguration> matchByPathByID(
|
||||
List<CameraInfo> detectedCamInfos, List<CameraConfiguration> unloadedConfigs) {
|
||||
/**
|
||||
* Abstractly match cameras
|
||||
*
|
||||
* @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,
|
||||
CameraMatchingOptions matchingOptions) {
|
||||
List<CameraConfiguration> ret = new ArrayList<CameraConfiguration>();
|
||||
List<CameraConfiguration> unloadedConfigsCopy =
|
||||
new ArrayList<CameraConfiguration>(unloadedConfigs);
|
||||
|
||||
for (CameraConfiguration config : unloadedConfigsCopy) {
|
||||
// Only run match path by id if the camera is not a CSI camera.
|
||||
if (config.cameraType != CameraType.ZeroCopyPicam) {
|
||||
CameraInfo cameraInfo;
|
||||
if (config.otherPaths.length == 0) {
|
||||
logger.debug("No valid path-by-id found for config with name " + config.baseName);
|
||||
} else {
|
||||
// attempt matching by path and basename
|
||||
logger.debug(
|
||||
"Trying to find a match for loaded camera "
|
||||
+ config.baseName
|
||||
+ " with path-by-id "
|
||||
+ 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 (unloadedConfigsCopy.isEmpty()) return List.of();
|
||||
|
||||
// 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);
|
||||
}
|
||||
logger.debug("Matching with options " + matchingOptions.toString());
|
||||
|
||||
for (CameraConfiguration config : unloadedConfigsCopy) {
|
||||
// Only run match path by id if the camera type is allowed. This allows us to specify matching
|
||||
// behavior per-camera-type
|
||||
if (matchingOptions.allowedTypes.contains(config.cameraType)) {
|
||||
logger.debug(
|
||||
String.format(
|
||||
"Trying to find a match for loaded camera %s (%s) with camera config: %s",
|
||||
config.baseName, config.uniqueName, camCfgToString(config)));
|
||||
|
||||
// Get matcher and filter against it, picking out the first match
|
||||
Predicate<CameraInfo> matches =
|
||||
getCameraMatcher(
|
||||
config,
|
||||
matchingOptions.checkUSBPath,
|
||||
matchingOptions.checkVidPid,
|
||||
matchingOptions.checkBaseName,
|
||||
matchingOptions.checkPath);
|
||||
var cameraInfo = detectedCamInfos.stream().filter(matches).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.uniqueName
|
||||
+ " to the physical camera config above!");
|
||||
ret.add(mergeInfoIntoConfig(config, cameraInfo));
|
||||
detectedCamInfos.remove(cameraInfo);
|
||||
unloadedConfigs.remove(config);
|
||||
} else {
|
||||
logger.debug("No camera found for the config " + config.uniqueName);
|
||||
}
|
||||
}
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
private List<CameraConfiguration> matchByPath(
|
||||
List<CameraInfo> detectedCamInfos, List<CameraConfiguration> unloadedConfigs) {
|
||||
List<CameraConfiguration> ret = new ArrayList<CameraConfiguration>();
|
||||
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.
|
||||
/**
|
||||
* Create new {@link CameraConfiguration}s for unmatched cameras, and assign them a unique name
|
||||
* (unique in the set of (loaded configs, unloaded configs, loaded vision modules) at least)
|
||||
*/
|
||||
private List<CameraConfiguration> createConfigsForCameras(
|
||||
List<CameraInfo> detectedCameraList,
|
||||
List<CameraConfiguration> loadedCamConfigs,
|
||||
List<CameraConfiguration> unloadedCamConfigs,
|
||||
List<CameraConfiguration> loadedConfigs) {
|
||||
List<CameraConfiguration> ret = new ArrayList<CameraConfiguration>();
|
||||
logger.debug(
|
||||
"After matching loaded configs " + detectedCameraList.size() + " cameras were unmatched.");
|
||||
"After matching loaded configs, these configs remained unmatched: "
|
||||
+ detectedCameraList.stream()
|
||||
.map(n -> String.valueOf(n))
|
||||
.collect(Collectors.joining("-", "{", "}")));
|
||||
for (CameraInfo info : detectedCameraList) {
|
||||
// create new camera config for all new cameras
|
||||
String baseName = info.getBaseName();
|
||||
String uniqueName = info.getHumanReadableName();
|
||||
|
||||
int suffix = 0;
|
||||
while (containsName(loadedConfigs, uniqueName) || containsName(uniqueName)) {
|
||||
while (containsName(loadedConfigs, uniqueName)
|
||||
|| containsName(uniqueName)
|
||||
|| containsName(unloadedCamConfigs, uniqueName)
|
||||
|| containsName(ret, uniqueName)) {
|
||||
suffix++;
|
||||
uniqueName = String.format("%s (%d)", uniqueName, suffix);
|
||||
}
|
||||
@@ -457,10 +556,16 @@ public class VisionSourceManager {
|
||||
}
|
||||
|
||||
private static List<VisionSource> loadVisionSourcesFromCamConfigs(
|
||||
List<CameraConfiguration> camConfigs) {
|
||||
List<CameraConfiguration> camConfigs, boolean createSources) {
|
||||
var cameraSources = new ArrayList<VisionSource>();
|
||||
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();
|
||||
|
||||
|
||||
@@ -21,7 +21,9 @@ import java.util.List;
|
||||
import org.opencv.core.RotatedRect;
|
||||
import org.photonvision.vision.opencv.CVShape;
|
||||
import org.photonvision.vision.opencv.Contour;
|
||||
import org.photonvision.vision.opencv.ContourShape;
|
||||
import org.photonvision.vision.opencv.Releasable;
|
||||
import org.photonvision.vision.pipe.impl.NeuralNetworkPipeResult;
|
||||
|
||||
public class PotentialTarget implements Releasable {
|
||||
|
||||
@@ -29,6 +31,10 @@ public class PotentialTarget implements Releasable {
|
||||
public final List<Contour> m_subContours;
|
||||
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) {
|
||||
this(inputContour, List.of());
|
||||
}
|
||||
@@ -41,12 +47,26 @@ public class PotentialTarget implements Releasable {
|
||||
m_mainContour = inputContour;
|
||||
m_subContours = new ArrayList<>(subContours);
|
||||
this.shape = shape;
|
||||
this.clsId = -1;
|
||||
this.confidence = -1;
|
||||
}
|
||||
|
||||
public PotentialTarget(Contour inputContour, CVShape shape) {
|
||||
this(inputContour, List.of(), shape);
|
||||
}
|
||||
|
||||
public PotentialTarget(NeuralNetworkPipeResult det) {
|
||||
this.shape = new CVShape(new Contour(det.bbox), 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() {
|
||||
return m_mainContour.getMinAreaRect();
|
||||
}
|
||||
@@ -61,7 +81,7 @@ public class PotentialTarget implements Releasable {
|
||||
for (var sc : m_subContours) {
|
||||
sc.release();
|
||||
}
|
||||
m_subContours.clear();
|
||||
if (!m_subContours.isEmpty()) m_subContours.clear();
|
||||
if (shape != null) shape.release();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,12 +65,18 @@ public class TrackedTarget implements Releasable {
|
||||
|
||||
private Mat m_cameraRelativeTvec, m_cameraRelativeRvec;
|
||||
|
||||
private int m_classId = -1;
|
||||
private double m_confidence = -1;
|
||||
|
||||
public TrackedTarget(
|
||||
PotentialTarget origTarget, TargetCalculationParameters params, CVShape shape) {
|
||||
this.m_mainContour = origTarget.m_mainContour;
|
||||
this.m_subContours = origTarget.m_subContours;
|
||||
this.m_shape = shape;
|
||||
calculateValues(params);
|
||||
|
||||
this.m_classId = origTarget.clsId;
|
||||
this.m_confidence = origTarget.confidence;
|
||||
}
|
||||
|
||||
public TrackedTarget(
|
||||
@@ -154,6 +160,20 @@ public class TrackedTarget implements Releasable {
|
||||
m_robotOffsetPoint = new Point();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Returns the confidence of the detection ranging from 0 - 1.
|
||||
*/
|
||||
public double getConfidence() {
|
||||
return m_confidence;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return O-indexed class index for the detected object.
|
||||
*/
|
||||
public double getClassID() {
|
||||
return m_classId;
|
||||
}
|
||||
|
||||
public TrackedTarget(
|
||||
ArucoDetectionResult result,
|
||||
AprilTagPoseEstimate tagPose,
|
||||
@@ -388,6 +408,8 @@ public class TrackedTarget implements Releasable {
|
||||
ret.put("skew", getSkew());
|
||||
ret.put("area", getArea());
|
||||
ret.put("ambiguity", getPoseAmbiguity());
|
||||
ret.put("confidence", m_confidence);
|
||||
ret.put("classId", m_classId);
|
||||
|
||||
var bestCameraToTarget3d = getBestCameraToTarget3d();
|
||||
if (bestCameraToTarget3d != null) {
|
||||
|
||||
@@ -139,8 +139,31 @@ public class ConfigTest {
|
||||
writer.write(str);
|
||||
writer.flush();
|
||||
writer.close();
|
||||
Assertions.assertDoesNotThrow(
|
||||
() -> JacksonUtils.deserialize(tempFile.toPath(), CameraConfiguration.class));
|
||||
CameraConfiguration result =
|
||||
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();
|
||||
}
|
||||
|
||||
@@ -84,7 +84,9 @@ public class SQLConfigTest {
|
||||
CameraType.UsbCamera,
|
||||
QuirkyCamera.getQuirkyCamera(-1, -1),
|
||||
List.of(),
|
||||
0);
|
||||
0,
|
||||
-1,
|
||||
-1);
|
||||
testcamcfg.pipelineSettings =
|
||||
List.of(
|
||||
new ReflectivePipelineSettings(),
|
||||
|
||||
@@ -44,6 +44,7 @@ import org.photonvision.vision.frame.FrameDivisor;
|
||||
import org.photonvision.vision.frame.FrameStaticProperties;
|
||||
import org.photonvision.vision.frame.FrameThresholdType;
|
||||
import org.photonvision.vision.opencv.CVMat;
|
||||
import org.photonvision.vision.pipe.impl.Calibrate3dPipeline;
|
||||
|
||||
public class Calibrate3dPipeTest {
|
||||
@BeforeAll
|
||||
|
||||
@@ -30,7 +30,7 @@ public class PipelineManagerTest {
|
||||
public void testUniqueName() {
|
||||
TestUtils.loadLibraries();
|
||||
PipelineManager manager =
|
||||
new PipelineManager(new DriverModePipelineSettings(), List.of(), "meme_name");
|
||||
new PipelineManager(new DriverModePipelineSettings(), List.of(), "meme_name", -1);
|
||||
manager.addPipeline(PipelineType.Reflective, "Another");
|
||||
|
||||
// We now have ["New Pipeline", "Another"]
|
||||
|
||||
@@ -21,17 +21,24 @@ import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.photonvision.common.configuration.CameraConfiguration;
|
||||
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.CameraType;
|
||||
|
||||
public class VisionSourceManagerTest {
|
||||
@Test
|
||||
public void visionSourceTest() {
|
||||
Logger.setLevel(LogGroup.Camera, LogLevel.DEBUG);
|
||||
|
||||
var inst = new VisionSourceManager();
|
||||
var cameraInfos = new ArrayList<CameraInfo>();
|
||||
ConfigManager.getInstance().clearConfig();
|
||||
ConfigManager.getInstance().load();
|
||||
|
||||
inst.tryMatchCamImpl(cameraInfos);
|
||||
@@ -43,6 +50,8 @@ public class VisionSourceManagerTest {
|
||||
"thirdTestVideo",
|
||||
"dev/video1",
|
||||
new String[] {"by-id/123"});
|
||||
config3.usbVID = 3;
|
||||
config3.usbPID = 4;
|
||||
var config4 =
|
||||
new CameraConfiguration(
|
||||
"fourthTestVideo",
|
||||
@@ -50,6 +59,8 @@ public class VisionSourceManagerTest {
|
||||
"fourthTestVideo",
|
||||
"dev/video2",
|
||||
new String[] {"by-id/321"});
|
||||
config4.usbVID = 5;
|
||||
config4.usbPID = 6;
|
||||
|
||||
CameraInfo info1 = new CameraInfo(0, "dev/video0", "testVideo", new String[0], 1, 2);
|
||||
|
||||
@@ -261,4 +272,335 @@ public class VisionSourceManagerTest {
|
||||
assertEquals(10, inst.knownCameras.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 testCSICameraMatching() {
|
||||
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;
|
||||
|
||||
CameraInfo info1 =
|
||||
new CameraInfo(
|
||||
-1,
|
||||
"/base/soc/i2c0mux/i2c@0/ov9281@60",
|
||||
"OV9281", // Typically rp1-cfe for unit test changed to CSICAM-DEV
|
||||
new String[] {},
|
||||
-1,
|
||||
-1,
|
||||
CameraType.ZeroCopyPicam);
|
||||
|
||||
CameraInfo info2 =
|
||||
new CameraInfo(
|
||||
-1,
|
||||
"/base/soc/i2c0mux/i2c@1/ov9281@60",
|
||||
"OV9281", // Typically rp1-cfe for unit test changed to CSICAM-DEV
|
||||
new String[] {},
|
||||
-1,
|
||||
-1,
|
||||
CameraType.ZeroCopyPicam);
|
||||
|
||||
var camera1_saved_config =
|
||||
new CameraConfiguration(
|
||||
"OV9281", "OV9281", "test-1", "/base/soc/i2c0mux/i2c@0/ov9281@60", new String[0]);
|
||||
camera1_saved_config.cameraType = CameraType.ZeroCopyPicam;
|
||||
camera1_saved_config.usbVID = -1;
|
||||
camera1_saved_config.usbPID = -1;
|
||||
|
||||
var camera2_saved_config =
|
||||
new CameraConfiguration(
|
||||
"OV9281", "OV9281 (1)", "test-2", "/base/soc/i2c0mux/i2c@1/ov9281@60", new String[0]);
|
||||
camera2_saved_config.usbVID = -1;
|
||||
camera2_saved_config.usbPID = -1;
|
||||
camera2_saved_config.cameraType = CameraType.ZeroCopyPicam;
|
||||
|
||||
cameraInfos.add(info1);
|
||||
cameraInfos.add(info2);
|
||||
|
||||
// Try matching with both cameras being "known"
|
||||
inst.registerLoadedConfigs(camera1_saved_config, camera2_saved_config);
|
||||
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).path;
|
||||
assertEquals(
|
||||
1, ret1.stream().filter(it -> testPath.equals(it.cameraConfiguration.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());
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user