Compare commits

..

1 Commits

Author SHA1 Message Date
Matt M
826053e15a Backport maven changes 2024-08-02 08:20:12 -07:00
123 changed files with 656 additions and 3480 deletions

View File

@@ -2,123 +2,14 @@ name: Build
on:
push:
branches: [ master ]
branches:
- master
tags:
- 'v*'
pull_request:
branches: [ master ]
jobs:
build-client:
name: "PhotonClient Build"
defaults:
run:
working-directory: photon-client
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 18
- name: Install Dependencies
run: npm ci
- name: Build Production Client
run: npm run build
- uses: actions/upload-artifact@v4
with:
name: built-client
path: photon-client/dist/
build-examples:
name: "Build Examples"
runs-on: ubuntu-22.04
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Fetch tags
run: git fetch --tags --force
- name: Install Java 17
uses: actions/setup-java@v4
with:
java-version: 17
distribution: temurin
# Need to publish to maven local first, so that C++ sim can pick it up
# Still haven't figured out how to make the vendordep file be copied before trying to build examples
- name: Publish photonlib to maven local
run: |
chmod +x gradlew
./gradlew publishtomavenlocal -x check
- name: Build Java examples
working-directory: photonlib-java-examples
run: |
chmod +x gradlew
./gradlew copyPhotonlib -x check
./gradlew build -x check --max-workers 2
- name: Build C++ examples
working-directory: photonlib-cpp-examples
run: |
chmod +x gradlew
./gradlew copyPhotonlib -x check
./gradlew build -x check --max-workers 2
build-gradle:
name: "Gradle Build"
runs-on: ubuntu-22.04
steps:
# Checkout code.
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Fetch tags
run: git fetch --tags --force
- name: Install Java 17
uses: actions/setup-java@v3
with:
java-version: 17
distribution: temurin
- name: Install mrcal deps
run: sudo apt-get update && sudo apt-get install -y libcholmod3 liblapack3 libsuitesparseconfig5
- name: Gradle Build
run: |
chmod +x gradlew
./gradlew build -x check --max-workers 2
- name: Gradle Tests
run: ./gradlew testHeadless -i --max-workers 1 --stacktrace
- name: Gradle Coverage
run: ./gradlew jacocoTestReport --max-workers 1
- name: Publish Coverage Report
uses: codecov/codecov-action@v3
with:
file: ./photon-server/build/reports/jacoco/test/jacocoTestReport.xml
- name: Publish Core Coverage Report
uses: codecov/codecov-action@v3
with:
file: ./photon-core/build/reports/jacoco/test/jacocoTestReport.xml
build-offline-docs:
name: "Build Offline Docs"
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v4
with:
repository: 'PhotonVision/photonvision-docs.git'
ref: master
- uses: actions/setup-python@v5
with:
python-version: '3.9'
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install sphinx sphinx_rtd_theme sphinx-tabs sphinxext-opengraph doc8
pip install -r requirements.txt
- name: Build the docs
run: |
make html
- uses: actions/upload-artifact@master
with:
name: built-docs
path: build/html
build-photonlib-host:
env:
MACOSX_DEPLOYMENT_TARGET: 12
@@ -188,256 +79,3 @@ jobs:
env:
ARTIFACTORY_API_KEY: ${{ secrets.ARTIFACTORY_API_KEY }}
if: github.event_name == 'push'
build-package:
needs: [build-client, build-gradle, build-offline-docs]
strategy:
fail-fast: false
matrix:
include:
- os: windows-latest
artifact-name: Win64
architecture: x64
arch-override: none
- os: macos-latest
artifact-name: macOS
architecture: x64
arch-override: none
- os: ubuntu-latest
artifact-name: Linux
architecture: x64
arch-override: none
- os: macos-latest
artifact-name: macOSArm
architecture: x64
arch-override: macarm64
- os: ubuntu-latest
artifact-name: LinuxArm32
architecture: x64
arch-override: linuxarm32
- os: ubuntu-latest
artifact-name: LinuxArm64
architecture: x64
arch-override: linuxarm64
runs-on: ${{ matrix.os }}
name: "Build fat JAR - ${{ matrix.artifact-name }}"
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Install Java 17
uses: actions/setup-java@v4
with:
java-version: 17
distribution: temurin
- run: |
rm -rf photon-server/src/main/resources/web/*
mkdir -p photon-server/src/main/resources/web/docs
if: ${{ (matrix.os) != 'windows-latest' }}
- run: |
del photon-server\src\main\resources\web\*.*
mkdir photon-server\src\main\resources\web\docs
if: ${{ (matrix.os) == 'windows-latest' }}
- uses: actions/download-artifact@v4
with:
name: built-client
path: photon-server/src/main/resources/web/
- uses: actions/download-artifact@v4
with:
name: built-docs
path: photon-server/src/main/resources/web/docs
- run: |
chmod +x gradlew
./gradlew photon-server:shadowJar --max-workers 2 -PArchOverride=${{ matrix.arch-override }}
if: ${{ (matrix.arch-override != 'none') }}
- run: |
chmod +x gradlew
./gradlew photon-server:shadowJar --max-workers 2
if: ${{ (matrix.arch-override == 'none') }}
- uses: actions/upload-artifact@v4
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]
if: ${{ github.event_name != 'pull_request' }}
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
- os: ubuntu-latest
artifact-name: LinuxArm64
image_suffix: limelight2
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.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
runs-on: ${{ matrix.os }}
name: "Build image - ${{ matrix.image_url }}"
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: actions/download-artifact@v4
with:
name: jar-${{ matrix.artifact-name }}
- uses: pguyot/arm-runner-action@v2
name: Generate image
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
commands: |
chmod +x scripts/armrunner.sh
./scripts/armrunner.sh
- name: Compress image
run: |
new_jar=$(realpath $(find . -name photonvision\*-linuxarm64.jar))
new_image_name=$(basename "${new_jar/.jar/_${{ matrix.image_suffix }}.img}")
mv ${{ steps.generate_image.outputs.image }} $new_image_name
sudo xz -T 0 -v $new_image_name
- uses: actions/upload-artifact@v4
name: Upload image
with:
name: image-${{ matrix.image_suffix }}
path: photonvision*.xz
release:
needs: [build-package, build-image]
runs-on: ubuntu-22.04
steps:
# Download literally every single artifact. This also downloads client and docs,
# but the filtering below won't pick these up (I hope)
- uses: actions/download-artifact@v4
- run: find
# Push to dev release
- uses: pyTooling/Actions/releaser@r0
with:
token: ${{ secrets.GITHUB_TOKEN }}
tag: 'Dev'
rm: true
files: |
**/*.xz
**/*.jar
**/photonlib*.json
if: github.event_name == 'push'
# Upload all jars and xz archives
- uses: softprops/action-gh-release@v1
with:
files: |
**/*.xz
**/*.jar
**/photonlib*.json
if: startsWith(github.ref, 'refs/tags/v')
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -1,86 +0,0 @@
name: Documentation
on:
push:
# For now, run on all commits to master
branches: [ master ]
# and also all tags starting with v
tags:
- 'v*'
# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages
permissions:
contents: read
pages: write
id-token: write
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
build-client:
name: "PhotonClient Build"
defaults:
run:
working-directory: photon-client
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 18
- name: Install Dependencies
run: npm ci
- name: Build Production Client
run: npm run build-demo
- uses: actions/upload-artifact@v4
with:
name: built-client
path: photon-client/dist/
run_docs:
runs-on: "ubuntu-22.04"
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Fetch tags
run: git fetch --tags --force
- name: Install Java 17
uses: actions/setup-java@v3
with:
java-version: 17
distribution: temurin
- name: Build javadocs/doxygen
run: |
chmod +x gradlew
./gradlew docs:generateJavaDocs docs:doxygen
- uses: actions/upload-artifact@v4
with:
name: built-docs
path: docs/build/docs
release:
needs: [build-client, run_docs]
runs-on: ubuntu-22.04
steps:
# Download literally every single artifact.
- uses: actions/download-artifact@v4
- run: find .
- name: copy file via ssh password
uses: appleboy/scp-action@v0.1.7
with:
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/

View File

@@ -1,88 +0,0 @@
name: Lint and Format
on:
push:
branches: [ master ]
tags:
- 'v*'
pull_request:
branches: [ master ]
concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.ref }}
cancel-in-progress: true
jobs:
wpiformat:
name: "wpiformat"
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v3
- name: Fetch all history and metadata
run: |
git fetch --prune --unshallow
git checkout -b pr
git branch -f master origin/master
- name: Set up Python 3.8
uses: actions/setup-python@v4
with:
python-version: 3.8
- name: Install wpiformat
run: pip3 install wpiformat
- name: Run
run: wpiformat
- name: Check output
run: git --no-pager diff --exit-code HEAD
- name: Generate diff
run: git diff HEAD > wpiformat-fixes.patch
if: ${{ failure() }}
- uses: actions/upload-artifact@v3
with:
name: wpiformat fixes
path: wpiformat-fixes.patch
if: ${{ failure() }}
javaformat:
name: "Java Formatting"
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0
- uses: actions/setup-java@v3
with:
java-version: 17
distribution: temurin
- run: |
chmod +x gradlew
./gradlew spotlessCheck
client-lint-format:
name: "PhotonClient Lint and Formatting"
defaults:
run:
working-directory: photon-client
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: 18
- name: Install Dependencies
run: npm ci
- name: Check Linting
run: npm run lint-ci
- name: Check Formatting
run: npm run format-ci
server-index:
name: "Check server index.html not changed"
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v3
- name: Fetch all history and metadata
run: |
git fetch --prune --unshallow
git checkout -b pr
git branch -f master origin/master
- name: Check index.html not changed
run: git --no-pager diff --exit-code origin/master photon-server/src/main/resources/web/index.html

View File

@@ -1,60 +0,0 @@
name: Build and Distribute PhotonLibPy
permissions:
id-token: write # IMPORTANT: this permission is mandatory for trusted publishing
on:
push:
branches: [ master ]
tags:
- 'v*'
pull_request:
branches: [ master ]
jobs:
buildAndDeploy:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: 3.11
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install setuptools wheel pytest
- name: Build wheel
working-directory: ./photon-lib/py
run: |
python setup.py sdist bdist_wheel
- name: Run Unit Tests
working-directory: ./photon-lib/py
run: |
pip install --no-cache-dir dist/*.whl
pytest
- name: Upload artifacts
uses: actions/upload-artifact@master
with:
name: dist
path: ./photon-lib/py/dist/
- name: Publish package distributions to TestPyPI
# Only upload on tags
if: startsWith(github.ref, 'refs/tags/v')
uses: pypa/gh-action-pypi-publish@release/v1
with:
packages_dir: ./photon-lib/py/dist/
permissions:
id-token: write # IMPORTANT: this permission is mandatory for trusted publishing

View File

@@ -4,7 +4,7 @@ plugins {
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.3.2"
id "edu.wpi.first.GradleRIO" version "2024.1.1"
id 'edu.wpi.first.WpilibTools' version '1.3.0'
id 'com.google.protobuf' version '0.9.4' apply false
}
@@ -13,8 +13,9 @@ allprojects {
repositories {
mavenCentral()
mavenLocal()
maven { url = "https://maven.photonvision.org/repository/internal/" }
maven { url = "https://maven.photonvision.org/repository/snapshots/" }
maven { url = "https://maven.photonvision.org/releases" }
maven { url = "https://maven.photonvision.org/snapshots" }
maven { url = "https://jogamp.org/deployment/maven/" }
}
wpilibRepositories.addAllReleaseRepositories(it)
wpilibRepositories.addAllDevelopmentRepositories(it)
@@ -24,15 +25,15 @@ allprojects {
apply from: "versioningHelper.gradle"
ext {
wpilibVersion = "2024.3.2"
wpilibVersion = "2024.1.1"
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"
rknnVersion = "dev-v2024.0.0-30-g001b5ec"
frcYear = "2024"
mrcalVersion = "dev-v2024.0.0-18-gb903a09";
mrcalVersion = "dev-v2024.0.0-7-gc976aaa";
pubVersion = versionString
@@ -50,6 +51,10 @@ ext {
println("Building for platform " + jniPlatform + " wpilib: " + wpilibNativeName)
println("Using Wpilib: " + wpilibVersion)
println("Using OpenCV: " + openCVversion)
photonMavenURL = 'https://maven.photonvision.org/' + (isDev ? 'snapshots' : 'releases');
println("Publishing Photonlib to " + photonMavenURL)
}
spotless {

View File

@@ -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,

View File

@@ -25,10 +25,15 @@ 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
if (calib.meanErrors.length)
format.mean = calib.meanErrors.reduce((a, b) => a + b, 0) / calib.meanErrors.length;
else format.mean = NaN;
format.mean = Math.sqrt(
perViewSumSquareReprojectionError.map((it) => Math.pow(it, 2)).reduce((a, b) => a + b, 0) /
perViewSumSquareReprojectionError.length
);
format.horizontalFOV =
2 * Math.atan2(format.resolution.width / 2, calib.cameraIntrinsics.data[0]) * (180 / Math.PI);
@@ -104,7 +109,7 @@ const downloadCalibBoard = () => {
const yPos = chessboardStartY + squareY * squareSizeIn.value;
// Only draw the odd squares to create the chessboard pattern
if (squareY % 2 != squareX % 2) {
if ((xPos + yPos + 0.25) % 2 === 0) {
doc.rect(xPos, yPos, squareSizeIn.value, squareSizeIn.value, "F");
}
}
@@ -258,7 +263,7 @@ const setSelectedVideoFormat = (format: VideoFormat) => {
>
<td>{{ getResolutionString(value.resolution) }}</td>
<td>
{{ value.mean !== undefined ? (isNaN(value.mean) ? "Unknown" : value.mean.toFixed(2) + "px") : "-" }}
{{ value.mean !== undefined ? (isNaN(value.mean) ? "NaN" : value.mean.toFixed(2) + "px") : "-" }}
</td>
<td>{{ value.horizontalFOV !== undefined ? value.horizontalFOV.toFixed(2) + "°" : "-" }}</td>
<td>{{ value.verticalFOV !== undefined ? value.verticalFOV.toFixed(2) + "°" : "-" }}</td>
@@ -306,7 +311,7 @@ const setSelectedVideoFormat = (format: VideoFormat) => {
/>
<pv-number-input
v-model="patternWidth"
label="Board Width (squares)"
label="Board Width (in)"
tooltip="Width of the board in dots or chessboard squares"
:disabled="isCalibrating"
:rules="[(v) => v >= 4 || 'Width must be at least 4']"
@@ -314,7 +319,7 @@ const setSelectedVideoFormat = (format: VideoFormat) => {
/>
<pv-number-input
v-model="patternHeight"
label="Board Height (squares)"
label="Board Height (in)"
tooltip="Height of the board in dots or chessboard squares"
:disabled="isCalibrating"
:rules="[(v) => v >= 4 || 'Height must be at least 4']"

View File

@@ -1,19 +1,51 @@
<script setup lang="ts">
import type { CameraCalibrationResult, VideoFormat } from "@/types/SettingTypes";
import type { BoardObservation, CameraCalibrationResult, VideoFormat } from "@/types/SettingTypes";
import { useCameraSettingsStore } from "@/stores/settings/CameraSettingsStore";
import { useStateStore } from "@/stores/StateStore";
import { computed, inject, ref } from "vue";
import { ref } from "vue";
import loadingImage from "@/assets/images/loading.svg";
import { getResolutionString, parseJsonFile } from "@/lib/PhotonUtils";
const props = defineProps<{
videoFormat: VideoFormat;
}>();
const exportCalibration = ref();
const openExportCalibrationPrompt = () => {
exportCalibration.value.click();
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
);
};
// 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();
@@ -65,28 +97,19 @@ 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 => {
const coefficients = currentCalibrationCoeffs.value;
return coefficients?.meanErrors.map((m, i) => ({
index: i,
mean: parseFloat(m.toFixed(2))
}));
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 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>
@@ -117,22 +140,19 @@ const calibrationImageURL = (index: number) =>
<v-btn
color="secondary"
class="mt-4"
:disabled="!currentCalibrationCoeffs"
:disabled="useCameraSettingsStore().getCalibrationCoeffs(props.videoFormat.resolution) === undefined"
style="width: 100%"
@click="openExportCalibrationPrompt"
@click="downloadCalibration"
>
<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="currentCalibrationCoeffs" class="pt-2">
<v-row
v-if="useCameraSettingsStore().getCalibrationCoeffs(props.videoFormat.resolution) !== undefined"
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>
@@ -211,9 +231,7 @@ const calibrationImageURL = (index: number) =>
</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>
@@ -224,7 +242,11 @@ const calibrationImageURL = (index: number) =>
<td>{{ videoFormat.diagonalFOV !== undefined ? videoFormat.diagonalFOV.toFixed(2) + "°" : "-" }}</td>
</tr>
<!-- Board warp, only shown for mrcal-calibrated cameras -->
<tr v-if="currentCalibrationCoeffs?.calobjectWarp?.length === 2">
<tr
v-if="
useCameraSettingsStore().getCalibrationCoeffs(props.videoFormat.resolution)?.calobjectWarp?.length === 2
"
>
<td>Board warp, X/Y</td>
<td>
{{
@@ -256,7 +278,7 @@ const calibrationImageURL = (index: number) =>
<template #expanded-item="{ headers, item }">
<td :colspan="headers.length">
<div style="display: flex; justify-content: center; width: 100%">
<img :src="calibrationImageURL(item.index)" alt="observation image" class="snapshot-preview pt-2 pb-2" />
<img :src="item.snapshotSrc" alt="observation image" class="snapshot-preview pt-2 pb-2" />
</div>
</td>
</template>

View File

@@ -27,7 +27,6 @@ const changeCurrentCameraIndex = (index: number) => {
break;
case PipelineType.ObjectDetection:
pipelineType.value = WebsocketPipelineType.ObjectDetection;
break;
}
};
@@ -125,18 +124,6 @@ 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;
@@ -372,7 +359,13 @@ 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="validNewPipelineTypes"
:items="[
{ name: 'Reflective', value: WebsocketPipelineType.Reflective },
{ name: 'Colored Shape', value: WebsocketPipelineType.ColoredShape },
{ name: 'AprilTag', value: WebsocketPipelineType.AprilTag },
{ name: 'Aruco', value: WebsocketPipelineType.Aruco },
{ name: 'Object Detection', value: WebsocketPipelineType.ObjectDetection }
]"
/>
</v-card-text>
<v-divider />

View File

@@ -13,7 +13,11 @@ defineProps<{
const driverMode = computed<boolean>({
get: () => useCameraSettingsStore().isDriverMode,
set: (v) => useCameraSettingsStore().setDriverMode(v)
set: (v) =>
useCameraSettingsStore().changeCurrentPipelineIndex(
v ? -1 : useCameraSettingsStore().currentCameraSettings.lastPipelineIndex || 0,
true
)
});
const fpsTooLow = computed<boolean>(() => {

View File

@@ -14,12 +14,13 @@ 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>

View File

@@ -14,12 +14,13 @@ 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>

View File

@@ -49,12 +49,13 @@ 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>

View File

@@ -63,12 +63,13 @@ 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>
@@ -130,32 +131,12 @@ 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. Rotation not available when camera has been calibrated."
tooltip="Rotates the camera stream"
:items="cameraRotations"
:select-cols="interactiveCols"
:disabled="
useCameraSettingsStore().isCurrentVideoFormatCalibrated &&
useCameraSettingsStore().currentPipelineSettings.inputImageRotationMode == 0
"
@input="(args) => useCameraSettingsStore().changeCurrentPipelineSetting({ inputImageRotationMode: args }, false)"
/>
<pv-select

View File

@@ -1,32 +1,21 @@
<script setup lang="ts">
import { useCameraSettingsStore } from "@/stores/settings/CameraSettingsStore";
import { type ActivePipelineSettings, PipelineType } from "@/types/PipelineTypes";
import { 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
);
const currentPipelineSettings = 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
);
const interactiveCols = computed(
() =>
(getCurrentInstance()?.proxy.$vuetify.breakpoint.mdAndDown || false) &&
(!useStateStore().sidebarFolded || useCameraSettingsStore().isDriverMode)
)
? 9
: 8;
</script>
<template>
@@ -42,42 +31,5 @@ const interactiveCols = computed(() =>
: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>

View File

@@ -46,12 +46,13 @@ 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>

View File

@@ -6,12 +6,13 @@ 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>

View File

@@ -124,12 +124,13 @@ 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>

View File

@@ -27,54 +27,42 @@ const generalMetrics = computed<MetricItem[]>(() => [
value: useSettingsStore().general.gpuAcceleration || "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"
});
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"
}
return stats;
});
]);
const metricsLastFetched = ref("Never");
const fetchMetrics = () => {

View File

@@ -5,11 +5,12 @@ 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 { type ConfigurableNetworkSettings, NetworkConnectionType } from "@/types/SettingTypes";
import { NetworkConnectionType, type NetworkSettings } from "@/types/SettingTypes";
import { useStateStore } from "@/stores/StateStore";
// Copy object to remove reference to store
const tempSettingsStruct = ref<ConfigurableNetworkSettings>(Object.assign({}, useSettingsStore().network));
const tempSettingsStruct = ref<NetworkSettings>(Object.assign({}, useSettingsStore().network));
const resetTempSettingsStruct = () => {
tempSettingsStruct.value = Object.assign({}, useSettingsStore().network);
};
@@ -57,10 +58,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.matchCamerasOnlyByPath !== b.matchCamerasOnlyByPath
a.setDHCPcommand !== b.setDHCPcommand
);
};
@@ -78,7 +79,6 @@ const saveGeneralSettings = () => {
setStaticCommand: tempSettingsStruct.value.setStaticCommand || "",
shouldManage: tempSettingsStruct.value.shouldManage,
shouldPublishProto: tempSettingsStruct.value.shouldPublishProto,
matchCamerasOnlyByPath: tempSettingsStruct.value.matchCamerasOnlyByPath,
staticIp: tempSettingsStruct.value.staticIp
};
@@ -91,10 +91,7 @@ const saveGeneralSettings = () => {
});
// Update the local settings cause the backend checked their validity. Assign is to deref value
useSettingsStore().network = {
...useSettingsStore().network,
...Object.assign({}, tempSettingsStruct.value)
};
useSettingsStore().network = Object.assign({}, tempSettingsStruct.value);
})
.catch((error) => {
resetTempSettingsStruct();
@@ -139,8 +136,6 @@ 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">
@@ -167,63 +162,42 @@ 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 ||
!useSettingsStore().network.canManage ||
useSettingsStore().network.networkingDisabled
"
:disabled="!(tempSettingsStruct.shouldManage && tempSettingsStruct.canManage)"
/>
<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 ||
!useSettingsStore().network.canManage ||
useSettingsStore().network.networkingDisabled
"
:disabled="!(tempSettingsStruct.shouldManage && tempSettingsStruct.canManage)"
/>
<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 ||
!useSettingsStore().network.canManage ||
useSettingsStore().network.networkingDisabled
"
:disabled="!(tempSettingsStruct.shouldManage && tempSettingsStruct.canManage)"
/>
<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="!useSettingsStore().network.canManage || useSettingsStore().network.networkingDisabled"
:disabled="!tempSettingsStruct.canManage"
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 ||
!useSettingsStore().network.canManage ||
useSettingsStore().network.networkingDisabled
"
:disabled="!(tempSettingsStruct.shouldManage && tempSettingsStruct.canManage)"
:select-cols="12 - 4"
tooltip="Name of the interface PhotonVision should manage the IP address of"
:items="useSettingsStore().networkInterfaceNames"
@@ -232,8 +206,7 @@ watchEffect(() => {
v-show="
!useSettingsStore().networkInterfaceNames.length &&
tempSettingsStruct.shouldManage &&
useSettingsStore().network.canManage &&
!useSettingsStore().network.networkingDisabled
tempSettingsStruct.canManage
"
rounded
color="red"
@@ -258,9 +231,6 @@ 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"
@@ -279,32 +249,6 @@ 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"

View File

@@ -236,13 +236,6 @@ 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.
*
@@ -423,23 +416,6 @@ 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;
}
}
});

View File

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

View File

@@ -20,7 +20,6 @@ export interface MetricData {
cpuThr?: string;
cpuUptime?: string;
diskUtilPct?: string;
npuUsage?: string;
}
export enum NetworkConnectionType {
@@ -46,14 +45,9 @@ export interface NetworkSettings {
setStaticCommand?: string;
setDHCPcommand?: string;
networkInterfaceNames: NetworkInterfaceType[];
networkingDisabled: boolean;
matchCamerasOnlyByPath: boolean;
}
export type ConfigurableNetworkSettings = Omit<
NetworkSettings,
"canManage" | "networkInterfaceNames" | "networkingDisabled"
>;
export type ConfigurableNetworkSettings = Omit<NetworkSettings, "canManage" | "networkInterfaceNames">;
export interface LightingSettings {
supported: boolean;
@@ -140,9 +134,6 @@ 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 {
@@ -260,9 +251,7 @@ 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"],

View File

@@ -25,7 +25,7 @@ export default defineConfig({
css: {
preprocessorOptions: {
sass: {
additionalData: ["@import \"@/assets/styles/variables.scss\"", ""].join("\n")
additionalData: ['@import "@/assets/styles/variables.scss"', ""].join("\n")
}
}
},

View File

@@ -37,8 +37,9 @@ 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"
def rknnjniversion = "dev-v2024.0.0-44-g8022c40"
implementation "org.photonvision:rknn_jni-jni:$rknnjniversion:linuxarm64"
implementation "org.photonvision:rknn_jni-java:$rknnjniversion"
implementation "org.photonvision:photon-libcamera-gl-driver-jni:$photonGlDriverLibVersion:linuxarm64"
implementation "org.photonvision:photon-libcamera-gl-driver-java:$photonGlDriverLibVersion"

View File

@@ -23,7 +23,6 @@ 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;
@@ -52,12 +51,6 @@ 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;
@@ -105,9 +98,7 @@ public class CameraConfiguration {
@JsonProperty("cameraType") CameraType cameraType,
@JsonProperty("cameraQuirks") QuirkyCamera cameraQuirks,
@JsonProperty("calibration") List<CameraCalibrationCoefficients> calibrations,
@JsonProperty("currentPipelineIndex") int currentPipelineIndex,
@JsonProperty("usbVID") int usbVID,
@JsonProperty("usbPID") int usbPID) {
@JsonProperty("currentPipelineIndex") int currentPipelineIndex) {
this.baseName = baseName;
this.uniqueName = uniqueName;
this.nickname = nickname;
@@ -117,8 +108,6 @@ 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 "
@@ -167,17 +156,6 @@ 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="

View File

@@ -50,10 +50,6 @@ 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,
@@ -307,19 +303,4 @@ public class ConfigManager {
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();
}
}
}

View File

@@ -20,24 +20,6 @@ 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 + "]";

View File

@@ -39,14 +39,6 @@ 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}";
@@ -84,8 +76,7 @@ public class NetworkConfig {
@JsonProperty("shouldPublishProto") boolean shouldPublishProto,
@JsonProperty("networkManagerIface") String networkManagerIface,
@JsonProperty("setStaticCommand") String setStaticCommand,
@JsonProperty("setDHCPcommand") String setDHCPcommand,
@JsonProperty("matchCamerasOnlyByPath") boolean matchCamerasOnlyByPath) {
@JsonProperty("setDHCPcommand") String setDHCPcommand) {
this.ntServerAddress = ntServerAddress;
this.connectionType = connectionType;
this.staticIp = staticIp;
@@ -95,7 +86,6 @@ public class NetworkConfig {
this.networkManagerIface = networkManagerIface;
this.setStaticCommand = setStaticCommand;
this.setDHCPcommand = setDHCPcommand;
this.matchCamerasOnlyByPath = matchCamerasOnlyByPath;
setShouldManage(shouldManage);
}

View File

@@ -25,14 +25,12 @@ 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;
@@ -53,7 +51,7 @@ public class NeuralNetworkModelManager {
this.defaultModelFile = new File(modelsFolder, MODEL_NAME);
extractResource(modelResourcePath, defaultModelFile);
File labelsFile = new File(modelsFolder, "labels_v5.txt");
File labelsFile = new File(modelsFolder, "labels.txt");
var labelResourcePath = "/models/" + labelsFile.getName();
extractResource(labelResourcePath, labelsFile);
@@ -97,8 +95,4 @@ public class NeuralNetworkModelManager {
public List<String> getLabels() {
return labels;
}
public RknnJNI.ModelVersion getModelVersion() {
return modelVersion;
}
}

View File

@@ -25,13 +25,12 @@ 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.UICameraCalibrationCoefficients;
import org.photonvision.vision.calibration.CameraCalibrationCoefficients;
import org.photonvision.vision.camera.QuirkyCamera;
import org.photonvision.vision.processes.VisionModule;
import org.photonvision.vision.processes.VisionModuleManager;
@@ -122,10 +121,16 @@ 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();
@@ -174,7 +179,7 @@ public class PhotonConfiguration {
public HashMap<Integer, HashMap<String, Object>> videoFormatList;
public int outputStreamPort;
public int inputStreamPort;
public List<UICameraCalibrationCoefficients> calibrations;
public List<CameraCalibrationCoefficients> calibrations;
public boolean isFovConfigurable = true;
public QuirkyCamera cameraQuirks;
public boolean isCSICamera;

View File

@@ -26,7 +26,6 @@ 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);
@@ -42,22 +41,16 @@ public class UIDataPublisher implements CVPipelineResultConsumer {
public void accept(CVPipelineResult result) {
long now = System.currentTimeMillis();
// only update the UI at 10hz
// only update the UI at 15hz
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());
// 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());
}
for (var t : result.targets) {
uiTargets.add(t.toHashMap());
}
dataMap.put("targets", uiTargets);
dataMap.put("classNames", result.objectDetectionClassNames);

View File

@@ -145,7 +145,8 @@ public class HardwareManager {
logger.info("Shutting down LEDs...");
if (visionLED != null) visionLED.setState(false);
ConfigManager.getInstance().onJvmExit();
logger.info("Force-flushing settings...");
ConfigManager.getInstance().saveToDisk();
}
public boolean restartDevice() {

View File

@@ -28,7 +28,6 @@ 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;
@@ -45,8 +44,6 @@ 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 {
@@ -92,10 +89,6 @@ public class MetricsManager {
return safeExecute(cmds.cpuThrottleReasonCmd);
}
public String getNpuUsage() {
return safeExecute(cmds.npuUsageCommand);
}
private String gpuMemSave = null;
public String getGPUMemorySplit() {
@@ -132,7 +125,6 @@ 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));
}

View File

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

View File

@@ -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 $2}'";
cpuMemoryCommand = "free -m | awk 'FNR == 2 {print $3}'";
// TODO: boards have lots of thermal devices. Hard to pick the CPU

View File

@@ -25,6 +25,7 @@ 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\"; "

View File

@@ -1,50 +0,0 @@
/*
* 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/ *$//'";
}
}

View File

@@ -23,6 +23,5 @@ public enum LogGroup {
VisionModule,
Data,
General,
Config,
CSCore,
Config
}

View File

@@ -99,7 +99,6 @@ 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 {
@@ -195,7 +194,7 @@ public class Logger {
return logLevel.code <= levelMap.get(group).code;
}
void log(String message, LogLevel level) {
private void log(String message, LogLevel level) {
if (shouldLog(level)) {
log(message, level, group, className);
}

View File

@@ -1,70 +0,0 @@
/*
* 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);
}
}

View File

@@ -22,12 +22,12 @@ 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.opencv.CVMat;
import org.photonvision.vision.pipe.impl.NeuralNetworkPipeResult;
public class RknnDetectorJNI extends PhotonJNICommon {
@@ -65,38 +65,17 @@ public class RknnDetectorJNI extends PhotonJNICommon {
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;
private static final CopyOnWriteArrayList<Long> detectors = new CopyOnWriteArrayList<>();
public RknnObjectDetector(String modelPath, List<String> labels, RknnJNI.ModelVersion version) {
public RknnObjectDetector(String modelPath, List<String> labels) {
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()));
objPointer = RknnJNI.create(modelPath, labels.size());
detectors.add(objPointer);
System.out.println(
"Created " + objPointer + "! 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() {
@@ -111,14 +90,14 @@ public class RknnDetectorJNI extends PhotonJNICommon {
* @param boxThresh Minimum confidence for a box to be added. Basically just confidence
* threshold
*/
public List<NeuralNetworkPipeResult> detect(Mat in, double nmsThresh, double boxThresh) {
public List<NeuralNetworkPipeResult> detect(CVMat 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);
ret = RknnJNI.detect(objPointer, in.getMat().getNativeObjAddr(), nmsThresh, boxThresh);
} else {
logger.warn("Detect called after destroy -- giving up");
return List.of();
@@ -136,7 +115,7 @@ public class RknnDetectorJNI extends PhotonJNICommon {
synchronized (lock) {
if (objPointer > 0) {
RknnJNI.destroy(objPointer);
detectors.remove(this);
detectors.remove(objPointer);
System.out.println(
"Killed " + objPointer + "! Detectors: " + Arrays.toString(detectors.toArray()));
objPointer = -1;
@@ -146,4 +125,14 @@ public class RknnDetectorJNI extends PhotonJNICommon {
}
}
}
// public static void createRknnDetector() {
// objPointer =
// RknnJNI.create(
// NeuralNetworkModelManager.getInstance()
// .getDefaultRknnModel()
// .getAbsolutePath()
// .toString(),
// NeuralNetworkModelManager.getInstance().getLabels().size());
// }
}

View File

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

View File

@@ -23,37 +23,16 @@ 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 implements Releasable {
public class PhotonArucoDetector {
private static final Logger logger = new Logger(PhotonArucoDetector.class, LogGroup.VisionModule);
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 ArucoDetector detector =
new ArucoDetector(Objdetect.getPredefinedDictionary(Objdetect.DICT_APRILTAG_16h5));
private final Mat ids = new Mat();
private final ArrayList<Mat> cornerMats = new ArrayList<>();
@@ -116,16 +95,4 @@ public class PhotonArucoDetector implements Releasable {
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();
}
}

View File

@@ -24,7 +24,7 @@ import java.util.List;
import org.opencv.core.Point;
import org.opencv.core.Point3;
public final class BoardObservation implements Cloneable {
public final class BoardObservation {
// Expected feature 3d location in the camera frame
@JsonProperty("locationInObjectSpace")
public List<Point3> locationInObjectSpace;
@@ -68,33 +68,4 @@ public final class BoardObservation implements Cloneable {
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;
}
}
}

View File

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

View File

@@ -76,17 +76,4 @@ 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()
+ "]";
}
}

View File

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

View File

@@ -1,59 +0,0 @@
/*
* Copyright (C) Photon Vision.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.photonvision.vision.calibration;
import java.util.List;
import java.util.stream.Collectors;
import org.opencv.core.Size;
public class UICameraCalibrationCoefficients extends CameraCalibrationCoefficients {
public int numSnapshots;
public List<Double> meanErrors;
public UICameraCalibrationCoefficients(
Size resolution,
JsonMatOfDouble cameraIntrinsics,
JsonMatOfDouble distCoeffs,
double[] calobjectWarp,
List<BoardObservation> observations,
Size calobjectSize,
double calobjectSpacing,
CameraLensModel lensmodel) {
// yeet observations, keep all else
super(
resolution,
cameraIntrinsics,
distCoeffs,
calobjectWarp,
List.of(),
calobjectSize,
calobjectSpacing,
lensmodel);
this.numSnapshots = observations.size();
this.meanErrors =
observations.stream()
.map(
it2 ->
it2.reprojectionErrors.stream()
.mapToDouble(it -> Math.sqrt(it.x * it.x + it.y * it.y))
.average()
.orElse(0))
.collect(Collectors.toList());
}
}

View File

@@ -19,8 +19,6 @@ 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;
@@ -70,54 +68,15 @@ 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 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)
+ "]";
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;
}
}

View File

@@ -1,93 +0,0 @@
/*
* Copyright (C) Photon Vision.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.photonvision.vision.camera;
import java.util.*;
import org.photonvision.common.configuration.CameraConfiguration;
import org.photonvision.vision.frame.Frame;
import org.photonvision.vision.frame.FrameProvider;
import org.photonvision.vision.frame.FrameThresholdType;
import org.photonvision.vision.opencv.ImageRotationMode;
import org.photonvision.vision.pipe.impl.HSVPipe.HSVParams;
import org.photonvision.vision.processes.VisionSource;
import org.photonvision.vision.processes.VisionSourceSettables;
/** Dummy class for unit testing the vision source manager */
public class TestSource extends VisionSource {
private FrameProvider usbFrameProvider;
public TestSource(CameraConfiguration config) {
super(config);
if (getCameraConfiguration().cameraQuirks == null)
getCameraConfiguration().cameraQuirks =
QuirkyCamera.getQuirkyCamera(config.usbVID, config.usbVID, config.baseName);
}
@Override
public FrameProvider getFrameProvider() {
return new FrameProvider() {
@Override
public Frame get() {
// TODO Auto-generated method stub
throw new UnsupportedOperationException("Unimplemented method 'get'");
}
@Override
public String getName() {
return cameraConfiguration.uniqueName;
}
@Override
public void requestFrameThresholdType(FrameThresholdType type) {
// TODO Auto-generated method stub
throw new UnsupportedOperationException("Unimplemented method 'requestFrameThresholdType'");
}
@Override
public void requestFrameRotation(ImageRotationMode rotationMode) {
// TODO Auto-generated method stub
throw new UnsupportedOperationException("Unimplemented method 'requestFrameRotation'");
}
@Override
public void requestFrameCopies(boolean copyInput, boolean copyOutput) {
// TODO Auto-generated method stub
throw new UnsupportedOperationException("Unimplemented method 'requestFrameCopies'");
}
@Override
public void requestHsvSettings(HSVParams params) {
// TODO Auto-generated method stub
throw new UnsupportedOperationException("Unimplemented method 'requestHsvSettings'");
}
};
}
@Override
public VisionSourceSettables getSettables() {
// TODO Auto-generated method stub
throw new UnsupportedOperationException("Unimplemented method 'getSettables'");
}
@Override
public boolean isVendorCamera() {
// TODO Auto-generated method stub
throw new UnsupportedOperationException("Unimplemented method 'isVendorCamera'");
}
}

View File

@@ -49,17 +49,9 @@ public class USBCameraSource extends VisionSource {
super(config);
logger = new Logger(USBCameraSource.class, config.nickname, LogGroup.Camera);
// 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));
camera = new UsbCamera(config.nickname, 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(
@@ -270,13 +262,9 @@ 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);
@@ -407,7 +395,6 @@ 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);

View File

@@ -33,7 +33,7 @@ import org.photonvision.vision.opencv.CVMat;
* path}.
*/
public class FileFrameProvider extends CpuImageProcessor {
public static final int MAX_FPS = 10;
public static final int MAX_FPS = 5;
private static int count = 0;
private final int thisIndex = count++;

View File

@@ -47,16 +47,6 @@ 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());

View File

@@ -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(tgt -> tgt.getMinAreaRect().center.y)),
Highest(Comparator.comparingDouble(rect -> rect.getMinAreaRect().center.y)),
Lowest(Highest.getComparator().reversed()),
Leftmost(Comparator.comparingDouble(tgt -> tgt.getMinAreaRect().center.x * -1)),
Leftmost(Comparator.comparingDouble(target -> target.getMinAreaRect().center.x * -1)),
Rightmost(Leftmost.getComparator().reversed()),
Centermost(
Comparator.comparingDouble(
tgt ->
(Math.pow(tgt.getMinAreaRect().center.y, 2)
+ Math.pow(tgt.getMinAreaRect().center.x, 2))));
rect ->
(Math.pow(rect.getMinAreaRect().center.y, 2)
+ Math.pow(rect.getMinAreaRect().center.x, 2))));
private final Comparator<PotentialTarget> m_comparator;

View File

@@ -21,13 +21,11 @@ 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>
implements Releasable {
private AprilTagDetector m_detector = new AprilTagDetector();
extends CVPipe<CVMat, List<AprilTagDetection>, AprilTagDetectionPipeParams> {
private final AprilTagDetector m_detector = new AprilTagDetector();
public AprilTagDetectionPipe() {
super();
@@ -42,10 +40,6 @@ 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) {
@@ -66,10 +60,4 @@ public class AprilTagDetectionPipe
super.setParams(newParams);
}
@Override
public void release() {
m_detector.close();
m_detector = null;
}
}

View File

@@ -25,15 +25,13 @@ 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>
implements Releasable {
AprilTagPoseEstimatorPipe.AprilTagPoseEstimatorPipeParams> {
private final AprilTagPoseEstimator m_poseEstimator =
new AprilTagPoseEstimator(new AprilTagPoseEstimator.Config(0, 0, 0, 0, 0));
@@ -94,11 +92,6 @@ public class AprilTagPoseEstimatorPipe
super.setParams(newParams);
}
@Override
public void release() {
temp.release();
}
public static class AprilTagPoseEstimatorPipeParams {
final AprilTagPoseEstimator.Config config;
final CameraCalibrationCoefficients calibration;

View File

@@ -29,12 +29,10 @@ 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>
implements Releasable {
extends CVPipe<CVMat, List<ArucoDetectionResult>, ArucoDetectionPipeParams> {
// ArucoDetector wrapper class
private final PhotonArucoDetector photonDetector = new PhotonArucoDetector();
@@ -133,9 +131,4 @@ 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();
}
}

View File

@@ -32,15 +32,13 @@ 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>
implements Releasable {
ArucoPoseEstimatorPipe.ArucoPoseEstimatorPipeParams> {
// image points of marker corners
private final MatOfPoint2f imagePoints = new MatOfPoint2f(Mat.zeros(4, 1, CvType.CV_32FC2));
// rvec/tvec estimations from solvepnp
@@ -119,18 +117,6 @@ 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;

View File

@@ -38,26 +38,13 @@ 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<
Calibrate3dPipe.CalibrationInput,
List<FindBoardCornersPipe.FindBoardCornersPipeResult>,
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);
@@ -76,9 +63,10 @@ public class Calibrate3dPipe
* @return Result of processing.
*/
@Override
protected CameraCalibrationCoefficients process(CalibrationInput in) {
var filteredIn =
in.observations.stream()
protected CameraCalibrationCoefficients process(
List<FindBoardCornersPipe.FindBoardCornersPipeResult> in) {
in =
in.stream()
.filter(
it ->
it != null
@@ -91,21 +79,17 @@ public class Calibrate3dPipe
var start = System.nanoTime();
if (MrCalJNILoader.getInstance().isLoaded() && params.useMrCal) {
logger.debug("Calibrating with mrcal!");
ret =
calibrateMrcal(
filteredIn, in.imageProps.horizontalFocalLength, in.imageProps.verticalFocalLength);
ret = calibrateMrcal(in);
} else {
logger.debug("Calibrating with opencv!");
ret =
calibrateOpenCV(
filteredIn, in.imageProps.horizontalFocalLength, in.imageProps.verticalFocalLength);
ret = calibrateOpenCV(in);
}
var dt = System.nanoTime() - start;
if (ret != null)
logger.info(
"CALIBRATION SUCCESS for res "
+ in.observations.get(0).size
+ in.get(0).size
+ " in "
+ dt / 1e6
+ "ms! camMatrix: \n"
@@ -119,7 +103,7 @@ public class Calibrate3dPipe
}
protected CameraCalibrationCoefficients calibrateOpenCV(
List<FindBoardCornersPipe.FindBoardCornersPipeResult> in, double fxGuess, double fyGuess) {
List<FindBoardCornersPipe.FindBoardCornersPipeResult> in) {
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()) {
@@ -127,32 +111,30 @@ public class Calibrate3dPipe
return null;
}
Mat cameraMatrix = new Mat(3, 3, CvType.CV_64F);
Mat cameraMatrix = new Mat();
MatOfDouble distortionCoefficients = new MatOfDouble();
List<Mat> rvecs = new ArrayList<>();
List<Mat> tvecs = new ArrayList<>();
// 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});
// RMS of the calibration
double calibrationAccuracy;
try {
// FindBoardCorners pipe outputs all the image points, object points, and frames to calculate
// imageSize from, other parameters are output Mats
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);
calibrationAccuracy =
Calib3d.calibrateCameraExtended(
objPoints,
imgPts,
new Size(in.get(0).size.width, in.get(0).size.height),
cameraMatrix,
distortionCoefficients,
rvecs,
tvecs,
stdDeviationsIntrinsics,
stdDeviationsExtrinsics,
perViewErrors);
} catch (Exception e) {
logger.error("Calibration failed!", e);
e.printStackTrace();
@@ -182,12 +164,13 @@ public class Calibrate3dPipe
}
protected CameraCalibrationCoefficients calibrateMrcal(
List<FindBoardCornersPipe.FindBoardCornersPipeResult> in, double fxGuess, double fyGuess) {
List<FindBoardCornersPipe.FindBoardCornersPipeResult> in) {
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(
@@ -197,7 +180,7 @@ public class Calibrate3dPipe
params.squareSize,
imageWidth,
imageHeight,
(fxGuess + fyGuess) / 2.0);
FOCAL_LENGTH_GUESS);
// intrinsics are fx fy cx cy from mrcal
JsonMatOfDouble cameraMatrixMat =

View File

@@ -36,7 +36,6 @@ 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.Calibrate3dPipe.CalibrationInput;
import org.photonvision.vision.pipe.impl.FindBoardCornersPipe.FindBoardCornersPipeResult;
import org.photonvision.vision.pipeline.CVPipeline;
import org.photonvision.vision.pipeline.Calibration3dPipelineSettings;
@@ -177,8 +176,7 @@ 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(new CalibrationInput(foundCornersList, frameStaticProperties));
calibrationOutput = calibrate3dPipe.run(foundCornersList);
this.calibrating = false;
@@ -231,9 +229,4 @@ 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
}
}

View File

@@ -22,7 +22,6 @@ 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;
@@ -32,44 +31,22 @@ 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 = 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);
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);
}
i++;
}
return null;

View File

@@ -1,89 +0,0 @@
/*
* 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;
}
}
}

View File

@@ -20,13 +20,13 @@ package org.photonvision.vision.pipe.impl;
import org.opencv.core.Rect2d;
public class NeuralNetworkPipeResult {
public NeuralNetworkPipeResult(Rect2d boundingBox, int classIdx, double confidence) {
bbox = boundingBox;
public NeuralNetworkPipeResult(Rect2d box2, Integer classIdx, Float confidence) {
box = box2;
this.classIdx = classIdx;
this.confidence = confidence;
}
public final int classIdx;
public final Rect2d bbox;
public final Rect2d box;
public final double confidence;
}

View File

@@ -17,17 +17,8 @@
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;
@@ -39,27 +30,12 @@ public class RknnDetectionPipe
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.
// 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;
}
NeuralNetworkModelManager.getInstance().getLabels());
}
@Override
@@ -71,66 +47,7 @@ public class RknnDetectionPipe
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);
return detector.detect(in, params.nms, params.confidence);
}
public static class RknnDetectionPipeParams {

View File

@@ -42,7 +42,6 @@ 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));
}
}
@@ -51,10 +50,10 @@ public class SortContoursPipe
m_sortedContours.subList(0, Math.min(in.size(), params.getMaxTargets())));
}
private double calcSquareCenterDistance(PotentialTarget tgt) {
private double calcSquareCenterDistance(PotentialTarget rect) {
return Math.sqrt(
Math.pow(params.getCamProperties().centerX - tgt.getMinAreaRect().center.x, 2)
+ Math.pow(params.getCamProperties().centerY - tgt.getMinAreaRect().center.y, 2));
Math.pow(params.getCamProperties().centerX - rect.getMinAreaRect().center.x, 2)
+ Math.pow(params.getCamProperties().centerY - rect.getMinAreaRect().center.y, 2));
}
public static class SortContoursParams {

View File

@@ -221,11 +221,4 @@ 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();
}
}

View File

@@ -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 ArucoDetectionPipe arucoDetectionPipe = new ArucoDetectionPipe();
private ArucoPoseEstimatorPipe singleTagPoseEstimatorPipe = new ArucoPoseEstimatorPipe();
private final ArucoDetectionPipe arucoDetectionPipe = new ArucoDetectionPipe();
private final ArucoPoseEstimatorPipe singleTagPoseEstimatorPipe = new ArucoPoseEstimatorPipe();
private final MultiTargetPNPPipe multiTagPNPPipe = new MultiTargetPNPPipe();
private final CalculateFPSPipe calculateFPSPipe = new CalculateFPSPipe();
@@ -250,13 +250,4 @@ public class ArucoPipeline extends CVPipeline<CVPipelineResult, ArucoPipelineSet
windowSize,
constant);
}
@Override
public void release() {
arucoDetectionPipe.release();
singleTagPoseEstimatorPipe.release();
arucoDetectionPipe = null;
singleTagPoseEstimatorPipe = null;
super.release();
}
}

View File

@@ -26,17 +26,12 @@ import org.photonvision.vision.pipeline.result.CVPipelineResult;
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;
}
@@ -67,9 +62,6 @@ 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!");
}
@@ -91,7 +83,5 @@ public abstract class CVPipeline<R extends CVPipelineResult, S extends CVPipelin
* switch. Stubbed out, but override if needed.
*/
@Override
public void release() {
released = true;
}
public void release() {}
}

View File

@@ -109,7 +109,7 @@ public class ColoredShapePipeline
SortContoursPipe.SortContoursParams sortContoursParams =
new SortContoursPipe.SortContoursParams(
settings.contourSortMode,
settings.outputShowMultipleTargets ? MAX_MULTI_TARGET_RESULTS : 1,
settings.outputShowMultipleTargets ? 5 : 1,
frameStaticProperties); // TODO don't hardcode?
sortContoursPipe.setParams(sortContoursParams);

View File

@@ -88,9 +88,4 @@ 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
}
}

View File

@@ -17,26 +17,21 @@
package org.photonvision.vision.pipeline;
import java.util.ArrayList;
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;
import org.photonvision.vision.target.TrackedTarget.TargetCalculationParameters;
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;
@@ -57,38 +52,6 @@ public class ObjectDetectionPipeline
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
@@ -97,40 +60,35 @@ public class ObjectDetectionPipeline
// ***************** change based on backend ***********************
CVPipeResult<List<NeuralNetworkPipeResult>> rknnResult = rknnPipe.run(input_frame.colorImage);
sumPipeNanosElapsed += rknnResult.nanosElapsed;
CVPipeResult<List<NeuralNetworkPipeResult>> ret = rknnPipe.run(input_frame.colorImage);
sumPipeNanosElapsed += ret.nanosElapsed;
List<NeuralNetworkPipeResult> targetList;
targetList = ret.output;
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;
List<TrackedTarget> targets = new ArrayList<>();
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;
for (var t : targetList) {
targets.add(
new TrackedTarget(
t,
new TargetCalculationParameters(
false, null, null, null, null, frameStaticProperties)));
}
var fpsResult = calculateFPSPipe.run(null);
var fps = fpsResult.output;
return new CVPipelineResult(
sumPipeNanosElapsed, fps, collect2dTargetsResult.output, input_frame, names);
return new CVPipelineResult(sumPipeNanosElapsed, fps, targets, input_frame, names);
}
@Override
public void release() {
rknnPipe.release();
super.release();
}
}

View File

@@ -64,6 +64,29 @@ 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);
@@ -90,7 +113,7 @@ public class ReflectivePipeline extends CVPipeline<CVPipelineResult, ReflectiveP
var sortContoursParams =
new SortContoursPipe.SortContoursParams(
settings.contourSortMode,
settings.outputShowMultipleTargets ? MAX_MULTI_TARGET_RESULTS : 1,
settings.outputShowMultipleTargets ? 8 : 1, // TODO don't hardcode?
frameStaticProperties);
sortContoursPipe.setParams(sortContoursParams);

View File

@@ -1,57 +0,0 @@
/*
* 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
+ "]";
}
}

View File

@@ -61,8 +61,7 @@ public class PipelineManager {
PipelineManager(
DriverModePipelineSettings driverSettings,
List<CVPipelineSettings> userPipelines,
String uniqueName,
int defaultIndex) {
String uniqueName) {
this.userPipelineSettings = new ArrayList<>(userPipelines);
// This is to respect the default res idx for vendor cameras
@@ -71,19 +70,10 @@ 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,
config.currentPipelineIndex);
this(config.driveModeSettings, config.pipelineSettings, config.uniqueName);
}
/**
@@ -158,7 +148,6 @@ public class PipelineManager {
* @return The currently active pipeline.
*/
public CVPipeline getCurrentPipeline() {
updatePipelineFromRequested();
if (currentPipelineIndex < 0) {
switch (currentPipelineIndex) {
case CAL_3D_INDEX:
@@ -181,16 +170,6 @@ 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>
@@ -200,22 +179,6 @@ 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;
@@ -226,8 +189,8 @@ public class PipelineManager {
return;
}
// Cleanup potential old native resources before swapping over for user pipelines
if (currentUserPipeline != null && !(newIndex < 0)) {
// Cleanup potential old native resources before swapping over
if (currentUserPipeline != null) {
currentUserPipeline.release();
}

View File

@@ -26,7 +26,6 @@ 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;
@@ -143,7 +142,7 @@ public class VisionModule {
ntConsumer =
new NTDataPublisher(
visionSource.getSettables().getConfiguration().nickname,
pipelineManager::getRequestedIndex,
pipelineManager::getCurrentPipelineIndex,
this::setPipeline,
pipelineManager::getDriverMode,
this::setDriverMode);
@@ -156,7 +155,6 @@ 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
@@ -322,7 +320,7 @@ public class VisionModule {
void changePipelineType(int newType) {
pipelineManager.changePipelineType(newType);
setPipeline(pipelineManager.getRequestedIndex());
setPipeline(pipelineManager.getCurrentPipelineIndex());
saveAndBroadcastAll();
}
@@ -330,7 +328,9 @@ public class VisionModule {
pipelineManager.setDriverMode(isDriverMode);
setVisionLEDs(!isDriverMode);
setPipeline(
isDriverMode ? PipelineManager.DRIVERMODE_INDEX : pipelineManager.getRequestedIndex());
isDriverMode
? PipelineManager.DRIVERMODE_INDEX
: pipelineManager.getCurrentPipelineIndex());
saveAndBroadcastAll();
}
@@ -384,7 +384,7 @@ public class VisionModule {
var ret = pipelineManager.calibration3dPipeline.tryCalibration();
pipelineManager.setCalibrationMode(false);
setPipeline(pipelineManager.getRequestedIndex());
setPipeline(pipelineManager.getCurrentPipelineIndex());
if (ret != null) {
logger.debug("Saving calibration...");
@@ -446,7 +446,7 @@ public class VisionModule {
setVisionLEDs(pipelineSettings.ledMode);
visionSource.getSettables().getConfiguration().currentPipelineIndex =
pipelineManager.getRequestedIndex();
pipelineManager.getCurrentPipelineIndex();
return true;
}
@@ -510,7 +510,7 @@ public class VisionModule {
ret.uniqueName = visionSource.getSettables().getConfiguration().uniqueName;
ret.currentPipelineSettings =
SerializationUtils.objectToHashMap(pipelineManager.getCurrentPipelineSettings());
ret.currentPipelineIndex = pipelineManager.getRequestedIndex();
ret.currentPipelineIndex = pipelineManager.getCurrentPipelineIndex();
ret.pipelineNicknames = pipelineManager.getPipelineNicknames();
ret.cameraQuirks = visionSource.getSettables().getConfiguration().cameraQuirks;
@@ -536,10 +536,7 @@ public class VisionModule {
ret.outputStreamPort = this.outputStreamPort;
ret.inputStreamPort = this.inputStreamPort;
ret.calibrations =
visionSource.getSettables().getConfiguration().calibrations.stream()
.map(CameraCalibrationCoefficients::cloneWithoutObservations)
.collect(Collectors.toList());
ret.calibrations = visionSource.getSettables().getConfiguration().calibrations;
ret.isFovConfigurable =
!(ConfigManager.getInstance().getConfig().getHardwareConfig().hasPresetFOV()
@@ -552,7 +549,7 @@ public class VisionModule {
var config = visionSource.getSettables().getConfiguration();
config.setPipelineSettings(pipelineManager.userPipelineSettings);
config.driveModeSettings = pipelineManager.driverModePipeline.getSettings();
config.currentPipelineIndex = Math.max(pipelineManager.getRequestedIndex(), -1);
config.currentPipelineIndex = Math.max(pipelineManager.getCurrentPipelineIndex(), -1);
return config;
}

View File

@@ -88,7 +88,7 @@ public class VisionModuleChangeSubscriber extends DataChangeSubscriber {
parentModule.saveAndBroadcastAll();
return;
case "deleteCurrPipeline":
var indexToDelete = parentModule.pipelineManager.getRequestedIndex();
var indexToDelete = parentModule.pipelineManager.getCurrentPipelineIndex();
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.getRequestedIndex()) {
if (index == parentModule.pipelineManager.getCurrentPipelineIndex()) {
logger.debug("Skipping pipeline change, index " + index + " already active");
return;
}
@@ -181,9 +181,6 @@ public class VisionModuleChangeSubscriber extends DataChangeSubscriber {
parentModule.changePipelineType((Integer) newPropValue);
parentModule.saveAndBroadcastAll();
return;
case "isDriverMode":
parentModule.setDriverMode((Boolean) newPropValue);
return;
}
// special case for camera settables

View File

@@ -98,7 +98,8 @@ public class VisionRunner {
var pipelineResult = pipeline.run(frame, cameraQuirks);
pipelineResultConsumer.accept(pipelineResult);
} catch (Exception ex) {
logger.error("Exception on loop " + loopCount, ex);
logger.error("Exception on loop " + loopCount);
ex.printStackTrace();
}
loopCount++;

View File

@@ -23,7 +23,6 @@ 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;
@@ -39,7 +38,6 @@ 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 {
@@ -147,8 +145,8 @@ public class VisionSourceManager {
}
// Return no new sources because there are no new sources
if (connectedCameras.isEmpty()) {
if (!hasWarnedNoCameras) {
if (connectedCameras.isEmpty() && !cameraInfos.isEmpty()) {
if (hasWarnedNoCameras) {
logger.warn(
"No cameras were detected! Check that all cameras are connected, and that the path is correct.");
hasWarnedNoCameras = true;
@@ -166,7 +164,7 @@ public class VisionSourceManager {
// Debug prints
for (var info : connectedCameras) {
logger.info("Detected unmatched physical camera: " + info.toString());
logger.info("Adding local video device - \"" + info.name + "\" at \"" + info.path + "\"");
}
if (!unmatchedLoadedConfigs.isEmpty())
@@ -187,7 +185,7 @@ public class VisionSourceManager {
"Unloaded configs: "
+ unmatchedLoadedConfigs.stream()
.map(it -> it.nickname)
.collect(Collectors.joining(", ")));
.collect(Collectors.joining()));
hasWarned = true;
}
@@ -196,8 +194,13 @@ 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, createSources);
var sources = loadVisionSourcesFromCamConfigs(matchedCameras);
// Print info about each vision source
for (var src : sources) {
@@ -213,54 +216,6 @@ 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.
@@ -271,212 +226,158 @@ 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);
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(matchByPathByID(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(matchByPath(detectedCameraList, unloadedConfigs));
else logger.debug("Skipping matchByPath no configs or cameras left to match");
// 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...");
if (detectedCameraList.size() > 0 || unloadedConfigs.size() > 0)
cameraConfigurations.addAll(matchByName(detectedCameraList, unloadedConfigs));
else logger.debug("Skipping matchByName no configs or cameras left to match");
if (detectedCameraList.size() > 0)
cameraConfigurations.addAll(
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");
}
}
createConfigsForCameras(detectedCameraList, unloadedConfigs, cameraConfigurations));
logger.debug("Matched or created " + cameraConfigurations.size() + " camera configs!");
return cameraConfigurations;
}
/**
* 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) {
// 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) {
List<CameraConfiguration> ret = new ArrayList<CameraConfiguration>();
List<CameraConfiguration> unloadedConfigsCopy =
new ArrayList<CameraConfiguration>(unloadedConfigs);
if (unloadedConfigsCopy.isEmpty()) return List.of();
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);
// 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 {
logger.debug("No camera found for the config " + config.uniqueName);
// 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 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;
}
/**
* 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> 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.
private List<CameraConfiguration> createConfigsForCameras(
List<CameraInfo> detectedCameraList,
List<CameraConfiguration> unloadedCamConfigs,
List<CameraConfiguration> loadedCamConfigs,
List<CameraConfiguration> loadedConfigs) {
List<CameraConfiguration> ret = new ArrayList<CameraConfiguration>();
logger.debug(
"After matching loaded configs, these configs remained unmatched: "
+ detectedCameraList.stream()
.map(n -> String.valueOf(n))
.collect(Collectors.joining("-", "{", "}")));
"After matching loaded configs " + detectedCameraList.size() + " cameras were unmatched.");
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)
|| containsName(unloadedCamConfigs, uniqueName)
|| containsName(ret, uniqueName)) {
while (containsName(loadedConfigs, uniqueName) || containsName(uniqueName)) {
suffix++;
uniqueName = String.format("%s (%d)", uniqueName, suffix);
}
@@ -556,16 +457,10 @@ public class VisionSourceManager {
}
private static List<VisionSource> loadVisionSourcesFromCamConfigs(
List<CameraConfiguration> camConfigs, boolean createSources) {
List<CameraConfiguration> camConfigs) {
var cameraSources = new ArrayList<VisionSource>();
for (var configuration : camConfigs) {
logger.debug("Creating VisionSource for " + camCfgToString(configuration));
// In unit tests, create dummy
if (!createSources) {
cameraSources.add(new TestSource(configuration));
continue;
}
logger.debug("Creating VisionSource for " + configuration);
boolean is_pi = Platform.isRaspberryPi();

View File

@@ -21,9 +21,7 @@ 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 {
@@ -31,10 +29,6 @@ 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());
}
@@ -47,26 +41,12 @@ 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();
}
@@ -81,7 +61,7 @@ public class PotentialTarget implements Releasable {
for (var sc : m_subContours) {
sc.release();
}
if (!m_subContours.isEmpty()) m_subContours.clear();
m_subContours.clear();
if (shape != null) shape.release();
}
}

View File

@@ -27,6 +27,7 @@ import org.opencv.core.Mat;
import org.opencv.core.MatOfPoint;
import org.opencv.core.MatOfPoint2f;
import org.opencv.core.Point;
import org.opencv.core.Rect2d;
import org.opencv.core.RotatedRect;
import org.photonvision.common.util.SerializationUtils;
import org.photonvision.common.util.math.MathUtils;
@@ -38,6 +39,7 @@ import org.photonvision.vision.opencv.CVShape;
import org.photonvision.vision.opencv.Contour;
import org.photonvision.vision.opencv.DualOffsetValues;
import org.photonvision.vision.opencv.Releasable;
import org.photonvision.vision.pipe.impl.NeuralNetworkPipeResult;
public class TrackedTarget implements Releasable {
public final Contour m_mainContour;
@@ -74,9 +76,6 @@ public class TrackedTarget implements Releasable {
this.m_subContours = origTarget.m_subContours;
this.m_shape = shape;
calculateValues(params);
this.m_classId = origTarget.clsId;
this.m_confidence = origTarget.confidence;
}
public TrackedTarget(
@@ -160,6 +159,47 @@ public class TrackedTarget implements Releasable {
m_robotOffsetPoint = new Point();
}
public TrackedTarget(
Rect2d box, int class_id, double confidence, TargetCalculationParameters params) {
m_targetOffsetPoint = new Point(box.x + box.width / 2.0, box.y + box.height / 2.0);
m_robotOffsetPoint = new Point();
var yawPitch =
TargetCalculations.calculateYawPitch(
params.cameraCenterPoint.x,
box.x + box.width / 2.0,
params.horizontalFocalLength,
params.cameraCenterPoint.y,
box.y + box.height / 2.0,
params.verticalFocalLength);
m_yaw = yawPitch.getFirst();
m_pitch = yawPitch.getSecond();
Point[] cornerPoints =
new Point[] {
// Box.x/y is the top-left corner, not the center
new Point(box.x, box.y), // tl
new Point(box.x + box.width, box.y), // tr
new Point(box.x + box.width, box.y + box.height), // br
new Point(box.x, box.y + box.height), // bl
};
m_targetCorners = List.of(cornerPoints);
MatOfPoint contourMat = new MatOfPoint(cornerPoints);
m_approximateBoundingPolygon = new MatOfPoint2f(cornerPoints);
m_mainContour = new Contour(contourMat);
m_area = m_mainContour.getArea() / params.imageArea * 100;
m_classId = class_id;
m_confidence = confidence;
}
public TrackedTarget(
NeuralNetworkPipeResult t, TargetCalculationParameters targetCalculationParameters) {
this(t.box, t.classIdx, t.confidence, targetCalculationParameters);
}
/**
* @return Returns the confidence of the detection ranging from 0 - 1.
*/

View File

@@ -139,31 +139,8 @@ public class ConfigTest {
writer.write(str);
writer.flush();
writer.close();
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();
}
Assertions.assertDoesNotThrow(
() -> JacksonUtils.deserialize(tempFile.toPath(), CameraConfiguration.class));
tempFile.delete();
}

View File

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

View File

@@ -30,7 +30,7 @@ public class PipelineManagerTest {
public void testUniqueName() {
TestUtils.loadLibraries();
PipelineManager manager =
new PipelineManager(new DriverModePipelineSettings(), List.of(), "meme_name", -1);
new PipelineManager(new DriverModePipelineSettings(), List.of(), "meme_name");
manager.addPipeline(PipelineType.Reflective, "Another");
// We now have ["New Pipeline", "Another"]

View File

@@ -21,24 +21,17 @@ 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);
@@ -50,8 +43,6 @@ public class VisionSourceManagerTest {
"thirdTestVideo",
"dev/video1",
new String[] {"by-id/123"});
config3.usbVID = 3;
config3.usbPID = 4;
var config4 =
new CameraConfiguration(
"fourthTestVideo",
@@ -59,8 +50,6 @@ 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);
@@ -272,335 +261,4 @@ 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());
}
}

View File

@@ -1,26 +0,0 @@
from dataclasses import dataclass
from typing import TYPE_CHECKING
from wpimath.geometry import Pose3d
from .photonTrackedTarget import PhotonTrackedTarget
if TYPE_CHECKING:
from .photonPoseEstimator import PoseStrategy
@dataclass
class EstimatedRobotPose:
"""An estimated pose based on pipeline result"""
estimatedPose: Pose3d
"""The estimated pose"""
timestampSeconds: float
"""The estimated time the frame used to derive the robot pose was taken"""
targetsUsed: list[PhotonTrackedTarget]
"""A list of the targets used to compute this pose"""
strategy: "PoseStrategy"
"""The strategy actually used to produce this pose"""

View File

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

View File

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

View File

@@ -17,6 +17,7 @@ class PhotonPipelineResult:
self.latencyMillis = packet.decodeDouble()
targetCount = packet.decode8()
print(f"targetCount = {targetCount}")
for _ in range(targetCount):
target = PhotonTrackedTarget()
target.createFromPacket(packet)
@@ -38,6 +39,3 @@ class PhotonPipelineResult:
def getTargets(self) -> list[PhotonTrackedTarget]:
return self.targets
def hasTargets(self) -> bool:
return len(self.targets) > 0

View File

@@ -1,321 +0,0 @@
import enum
from typing import Optional
import wpilib
from robotpy_apriltag import AprilTagFieldLayout
from wpimath.geometry import Transform3d, Pose3d, Pose2d
from .photonPipelineResult import PhotonPipelineResult
from .photonCamera import PhotonCamera
from .estimatedRobotPose import EstimatedRobotPose
class PoseStrategy(enum.Enum):
"""
Position estimation strategies that can be used by the PhotonPoseEstimator class.
"""
LOWEST_AMBIGUITY = enum.auto()
"""Choose the Pose with the lowest ambiguity."""
CLOSEST_TO_CAMERA_HEIGHT = enum.auto()
"""Choose the Pose which is closest to the camera height."""
CLOSEST_TO_REFERENCE_POSE = enum.auto()
"""Choose the Pose which is closest to a set Reference position."""
CLOSEST_TO_LAST_POSE = enum.auto()
"""Choose the Pose which is closest to the last pose calculated."""
AVERAGE_BEST_TARGETS = enum.auto()
"""Return the average of the best target poses using ambiguity as weight."""
MULTI_TAG_PNP_ON_COPROCESSOR = enum.auto()
"""
Use all visible tags to compute a single pose estimate on coprocessor.
This option needs to be enabled on the PhotonVision web UI as well.
"""
MULTI_TAG_PNP_ON_RIO = enum.auto()
"""
Use all visible tags to compute a single pose estimate.
This runs on the RoboRIO, and can take a lot of time.
"""
class PhotonPoseEstimator:
"""
The PhotonPoseEstimator class filters or combines readings from all the AprilTags visible at a
given timestamp on the field to produce a single robot in field pose, using the strategy set
below. Example usage can be found in our apriltagExample example project.
"""
def __init__(
self,
fieldTags: AprilTagFieldLayout,
strategy: PoseStrategy,
camera: PhotonCamera,
robotToCamera: Transform3d,
):
"""Create a new PhotonPoseEstimator.
:param fieldTags: A WPILib AprilTagFieldLayout linking AprilTag IDs to Pose3d objects
with respect to the FIRST field using the Field Coordinate System.
Note that setting the origin of this layout object will affect the
results from this class.
:param strategy: The strategy it should use to determine the best pose.
:param camera: PhotonCamera
:param robotToCamera: Transform3d from the center of the robot to the camera mount position (i.e.,
robot ➔ camera) in the Robot Coordinate System.
"""
self._fieldTags = fieldTags
self._primaryStrategy = strategy
self._camera = camera
self.robotToCamera = robotToCamera
self._multiTagFallbackStrategy = PoseStrategy.LOWEST_AMBIGUITY
self._reportedErrors: set[int] = set()
self._poseCacheTimestampSeconds = -1.0
self._lastPose: Optional[Pose3d] = None
self._referencePose: Optional[Pose3d] = None
# TODO: Implement HAL reporting
@property
def fieldTags(self) -> AprilTagFieldLayout:
"""Get the AprilTagFieldLayout being used by the PositionEstimator.
Note: Setting the origin of this layout will affect the results from this class.
:returns: the AprilTagFieldLayout
"""
return self._fieldTags
@fieldTags.setter
def fieldTags(self, fieldTags: AprilTagFieldLayout):
"""Set the AprilTagFieldLayout being used by the PositionEstimator.
Note: Setting the origin of this layout will affect the results from this class.
:param fieldTags: the AprilTagFieldLayout
"""
self._checkUpdate(self._fieldTags, fieldTags)
self._fieldTags = fieldTags
@property
def primaryStrategy(self) -> PoseStrategy:
"""Get the Position Estimation Strategy being used by the Position Estimator.
:returns: the strategy
"""
return self._primaryStrategy
@primaryStrategy.setter
def primaryStrategy(self, strategy: PoseStrategy):
"""Set the Position Estimation Strategy used by the Position Estimator.
:param strategy: the strategy to set
"""
self._checkUpdate(self._primaryStrategy, strategy)
self._primaryStrategy = strategy
@property
def multiTagFallbackStrategy(self) -> PoseStrategy:
return self._multiTagFallbackStrategy
@multiTagFallbackStrategy.setter
def multiTagFallbackStrategy(self, strategy: PoseStrategy):
"""Set the Position Estimation Strategy used in multi-tag mode when only one tag can be seen. Must
NOT be MULTI_TAG_PNP
:param strategy: the strategy to set
"""
self._checkUpdate(self._multiTagFallbackStrategy, strategy)
if (
strategy is PoseStrategy.MULTI_TAG_PNP_ON_COPROCESSOR
or strategy is PoseStrategy.MULTI_TAG_PNP_ON_RIO
):
wpilib.reportWarning(
"Fallback cannot be set to MULTI_TAG_PNP! Setting to lowest ambiguity",
False,
)
strategy = PoseStrategy.LOWEST_AMBIGUITY
self._multiTagFallbackStrategy = strategy
@property
def referencePose(self) -> Optional[Pose3d]:
"""Return the reference position that is being used by the estimator.
:returns: the referencePose
"""
return self._referencePose
@referencePose.setter
def referencePose(self, referencePose: Pose3d | Pose2d):
"""Update the stored reference pose for use when using the **CLOSEST_TO_REFERENCE_POSE**
strategy.
:param referencePose: the referencePose to set
"""
if isinstance(referencePose, Pose2d):
referencePose = Pose3d(referencePose)
self._checkUpdate(self._referencePose, referencePose)
self._referencePose = referencePose
@property
def lastPose(self) -> Optional[Pose3d]:
return self._lastPose
@lastPose.setter
def lastPose(self, lastPose: Pose3d | Pose2d):
"""Update the stored last pose. Useful for setting the initial estimate when using the
**CLOSEST_TO_LAST_POSE** strategy.
:param lastPose: the lastPose to set
"""
if isinstance(lastPose, Pose2d):
lastPose = Pose3d(lastPose)
self._checkUpdate(self._lastPose, lastPose)
self._lastPose = lastPose
def _invalidatePoseCache(self) -> None:
self._poseCacheTimestampSeconds = -1.0
def _checkUpdate(self, oldObj, newObj) -> None:
if oldObj != newObj and oldObj is not None and oldObj is not newObj:
self._invalidatePoseCache()
def update(
self, cameraResult: Optional[PhotonPipelineResult] = None
) -> Optional[EstimatedRobotPose]:
"""
Updates the estimated position of the robot. Returns empty if:
- The timestamp of the provided pipeline result is the same as in the previous call to
``update()``.
- No targets were found in the pipeline results.
:param cameraResult: The latest pipeline result from the camera
:returns: an :class:`EstimatedRobotPose` with an estimated pose, timestamp, and targets used to
create the estimate.
"""
if not cameraResult:
if not self._camera:
wpilib.reportError("[PhotonPoseEstimator] Missing camera!", False)
return None
cameraResult = self._camera.getLatestResult()
if cameraResult.timestampSec < 0:
return None
# If the pose cache timestamp was set, and the result is from the same
# timestamp, return an
# empty result
if (
self._poseCacheTimestampSeconds > 0.0
and abs(self._poseCacheTimestampSeconds - cameraResult.timestampSec) < 1e-6
):
return None
# Remember the timestamp of the current result used
self._poseCacheTimestampSeconds = cameraResult.timestampSec
# If no targets seen, trivial case -- return empty result
if not cameraResult.targets:
return None
return self._update(cameraResult, self._primaryStrategy)
def _update(
self, cameraResult: PhotonPipelineResult, strat: PoseStrategy
) -> Optional[EstimatedRobotPose]:
if strat is PoseStrategy.LOWEST_AMBIGUITY:
estimatedPose = self._lowestAmbiguityStrategy(cameraResult)
elif strat is PoseStrategy.MULTI_TAG_PNP_ON_COPROCESSOR:
estimatedPose = self._multiTagOnCoprocStrategy(cameraResult)
else:
wpilib.reportError(
"[PhotonPoseEstimator] Unknown Position Estimation Strategy!", False
)
return None
if not estimatedPose:
self._lastPose = None
return estimatedPose
def _multiTagOnCoprocStrategy(
self, result: PhotonPipelineResult
) -> Optional[EstimatedRobotPose]:
if result.multiTagResult.estimatedPose.isPresent:
best_tf = result.multiTagResult.estimatedPose.best
best = (
Pose3d()
.transformBy(best_tf) # field-to-camera
.relativeTo(self._fieldTags.getOrigin())
.transformBy(self.robotToCamera.inverse()) # field-to-robot
)
return EstimatedRobotPose(
best,
result.timestampSec,
result.targets,
PoseStrategy.MULTI_TAG_PNP_ON_COPROCESSOR,
)
else:
return self._update(result, self._multiTagFallbackStrategy)
def _lowestAmbiguityStrategy(
self, result: PhotonPipelineResult
) -> Optional[EstimatedRobotPose]:
"""
Return the estimated position of the robot with the lowest position ambiguity from a List of
pipeline results.
:param result: pipeline result
:returns: the estimated position of the robot in the FCS and the estimated timestamp of this
estimation.
"""
lowestAmbiguityTarget = None
lowestAmbiguityScore = 10.0
for target in result.targets:
targetPoseAmbiguity = target.poseAmbiguity
# Make sure the target is a Fiducial target.
if targetPoseAmbiguity != -1 and targetPoseAmbiguity < lowestAmbiguityScore:
lowestAmbiguityScore = targetPoseAmbiguity
lowestAmbiguityTarget = target
# Although there are confirmed to be targets, none of them may be fiducial
# targets.
if not lowestAmbiguityTarget:
return None
targetFiducialId = lowestAmbiguityTarget.fiducialId
targetPosition = self._fieldTags.getTagPose(targetFiducialId)
if not targetPosition:
self._reportFiducialPoseError(targetFiducialId)
return None
return EstimatedRobotPose(
targetPosition.transformBy(
lowestAmbiguityTarget.getBestCameraToTarget().inverse()
).transformBy(self.robotToCamera.inverse()),
result.timestampSec,
result.targets,
PoseStrategy.LOWEST_AMBIGUITY,
)
def _reportFiducialPoseError(self, fiducialId: int) -> None:
if fiducialId not in self._reportedErrors:
wpilib.reportError(
f"[PhotonPoseEstimator] Tried to get pose of unknown AprilTag: {fiducialId}",
False,
)
self._reportedErrors.add(fiducialId)

View File

@@ -60,7 +60,6 @@ setup(
install_requires=[
"wpilib<2025,>=2024.0.0b2",
"robotpy-wpimath<2025,>=2024.0.0b2",
"robotpy-apriltag<2025,>=2024.0.0b2",
"pyntcore<2025,>=2024.0.0b2",
],
description=descriptionStr,

View File

@@ -1,243 +0,0 @@
from photonlibpy.multiTargetPNPResult import MultiTargetPNPResult, PNPResult
from photonlibpy.photonPipelineResult import PhotonPipelineResult
from photonlibpy.photonPoseEstimator import PhotonPoseEstimator, PoseStrategy
from photonlibpy.photonTrackedTarget import PhotonTrackedTarget, TargetCorner
from robotpy_apriltag import AprilTag, AprilTagFieldLayout
from wpimath.geometry import Pose3d, Rotation3d, Transform3d, Translation3d
class PhotonCameraInjector:
result: PhotonPipelineResult
def getLatestResult(self) -> PhotonPipelineResult:
return self.result
def setupCommon() -> AprilTagFieldLayout:
tagList = []
tagPoses = (
Pose3d(3, 3, 3, Rotation3d()),
Pose3d(5, 5, 5, Rotation3d()),
)
for id_, pose in enumerate(tagPoses):
aprilTag = AprilTag()
aprilTag.ID = id_
aprilTag.pose = pose
tagList.append(aprilTag)
fieldLength = 54 / 3.281 # 54 ft -> meters
fieldWidth = 27 / 3.281 # 24 ft -> meters
return AprilTagFieldLayout(tagList, fieldLength, fieldWidth)
def test_lowestAmbiguityStrategy():
aprilTags = setupCommon()
cameraOne = PhotonCameraInjector()
cameraOne.result = PhotonPipelineResult(
2,
11,
[
PhotonTrackedTarget(
3.0,
-4.0,
9.0,
4.0,
0,
Transform3d(Translation3d(1, 2, 3), Rotation3d(1, 2, 3)),
Transform3d(Translation3d(1, 2, 3), Rotation3d(1, 2, 3)),
[
TargetCorner(1, 2),
TargetCorner(3, 4),
TargetCorner(5, 6),
TargetCorner(7, 8),
],
[
TargetCorner(1, 2),
TargetCorner(3, 4),
TargetCorner(5, 6),
TargetCorner(7, 8),
],
0.7,
),
PhotonTrackedTarget(
3.0,
-4.0,
9.1,
6.7,
1,
Transform3d(Translation3d(4, 2, 3), Rotation3d(0, 0, 0)),
Transform3d(Translation3d(4, 2, 3), Rotation3d(1, 5, 3)),
[
TargetCorner(1, 2),
TargetCorner(3, 4),
TargetCorner(5, 6),
TargetCorner(7, 8),
],
[
TargetCorner(1, 2),
TargetCorner(3, 4),
TargetCorner(5, 6),
TargetCorner(7, 8),
],
0.3,
),
PhotonTrackedTarget(
9.0,
-2.0,
19.0,
3.0,
0,
Transform3d(Translation3d(1, 2, 3), Rotation3d(1, 2, 3)),
Transform3d(Translation3d(1, 2, 3), Rotation3d(1, 2, 3)),
[
TargetCorner(1, 2),
TargetCorner(3, 4),
TargetCorner(5, 6),
TargetCorner(7, 8),
],
[
TargetCorner(1, 2),
TargetCorner(3, 4),
TargetCorner(5, 6),
TargetCorner(7, 8),
],
0.4,
),
],
)
estimator = PhotonPoseEstimator(
aprilTags, PoseStrategy.LOWEST_AMBIGUITY, cameraOne, Transform3d()
)
estimatedPose = estimator.update()
pose = estimatedPose.estimatedPose
assertEquals(11, estimatedPose.timestampSeconds)
assertEquals(1, pose.x, 0.01)
assertEquals(3, pose.y, 0.01)
assertEquals(2, pose.z, 0.01)
def test_multiTagOnCoprocStrategy():
cameraOne = PhotonCameraInjector()
cameraOne.result = PhotonPipelineResult(
2,
11,
# There needs to be at least one target present for pose estimation to work
# Doesn't matter which/how many targets for this test
[
PhotonTrackedTarget(
3.0,
-4.0,
9.0,
4.0,
0,
Transform3d(Translation3d(1, 2, 3), Rotation3d(1, 2, 3)),
Transform3d(Translation3d(1, 2, 3), Rotation3d(1, 2, 3)),
[
TargetCorner(1, 2),
TargetCorner(3, 4),
TargetCorner(5, 6),
TargetCorner(7, 8),
],
[
TargetCorner(1, 2),
TargetCorner(3, 4),
TargetCorner(5, 6),
TargetCorner(7, 8),
],
0.7,
)
],
multiTagResult=MultiTargetPNPResult(
PNPResult(True, Transform3d(1, 3, 2, Rotation3d()))
),
)
estimator = PhotonPoseEstimator(
AprilTagFieldLayout(),
PoseStrategy.MULTI_TAG_PNP_ON_COPROCESSOR,
cameraOne,
Transform3d(),
)
estimatedPose = estimator.update()
pose = estimatedPose.estimatedPose
assertEquals(11, estimatedPose.timestampSeconds)
assertEquals(1, pose.x, 0.01)
assertEquals(3, pose.y, 0.01)
assertEquals(2, pose.z, 0.01)
def test_cacheIsInvalidated():
aprilTags = setupCommon()
cameraOne = PhotonCameraInjector()
result = PhotonPipelineResult(
2,
20,
[
PhotonTrackedTarget(
3.0,
-4.0,
9.0,
4.0,
0,
Transform3d(Translation3d(1, 2, 3), Rotation3d(1, 2, 3)),
Transform3d(Translation3d(1, 2, 3), Rotation3d(1, 2, 3)),
[
TargetCorner(1, 2),
TargetCorner(3, 4),
TargetCorner(5, 6),
TargetCorner(7, 8),
],
[
TargetCorner(1, 2),
TargetCorner(3, 4),
TargetCorner(5, 6),
TargetCorner(7, 8),
],
0.7,
)
],
)
estimator = PhotonPoseEstimator(
aprilTags, PoseStrategy.LOWEST_AMBIGUITY, cameraOne, Transform3d()
)
# Empty result, expect empty result
cameraOne.result = PhotonPipelineResult(timestampSec=1)
estimatedPose = estimator.update()
assert estimatedPose is None
# Set actual result
cameraOne.result = result
estimatedPose = estimator.update()
assert estimatedPose is not None
assertEquals(20, estimatedPose.timestampSeconds, 0.01)
assertEquals(20, estimator._poseCacheTimestampSeconds)
# And again -- pose cache should mean this is empty
cameraOne.result = result
estimatedPose = estimator.update()
assert estimatedPose is None
# Expect the old timestamp to still be here
assertEquals(20, estimator._poseCacheTimestampSeconds)
# Set new field layout -- right after, the pose cache timestamp should be -1
estimator.fieldTags = AprilTagFieldLayout([AprilTag()], 0, 0)
assertEquals(-1, estimator._poseCacheTimestampSeconds)
# Update should cache the current timestamp (20) again
cameraOne.result = result
estimatedPose = estimator.update()
assertEquals(20, estimatedPose.timestampSeconds, 0.01)
assertEquals(20, estimator._poseCacheTimestampSeconds)
def assertEquals(expected, actual, epsilon=0.0):
assert abs(expected - actual) <= epsilon

View File

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

View File

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

View File

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

View File

@@ -260,7 +260,7 @@ class PhotonCameraSim {
std::vector<std::pair<double, double>> cornersDouble{cornersFloat.begin(),
cornersFloat.end()};
detectableTgts.emplace_back(PhotonTrackedTarget{
-centerRot.Z().convert<units::degrees>().to<double>(),
centerRot.Z().convert<units::degrees>().to<double>(),
-centerRot.Y().convert<units::degrees>().to<double>(), areaPercent,
centerRot.X().convert<units::degrees>().to<double>(), tgt.fiducialId,
pnpSim.best, pnpSim.alt, pnpSim.ambiguity, smallVec, cornersDouble});
@@ -435,7 +435,7 @@ class PhotonCameraSim {
double minTargetAreaPercent;
frc::AprilTagFieldLayout tagLayout{
frc::LoadAprilTagLayoutField(frc::AprilTagField::k2024Crescendo)};
frc::LoadAprilTagLayoutField(frc::AprilTagField::k2023ChargedUp)};
cs::CvSource videoSimRaw;
cv::Mat videoSimFrameRaw{};

View File

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

View File

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

View File

@@ -35,7 +35,6 @@ import org.photonvision.common.hardware.Platform;
import org.photonvision.common.logging.LogGroup;
import org.photonvision.common.logging.LogLevel;
import org.photonvision.common.logging.Logger;
import org.photonvision.common.logging.PvCSCoreLogger;
import org.photonvision.common.networking.NetworkManager;
import org.photonvision.common.util.TestUtils;
import org.photonvision.common.util.numbers.IntegerCouple;
@@ -66,7 +65,6 @@ public class Main {
private static final boolean isRelease = PhotonVersion.isRelease;
private static boolean isTestMode = false;
private static boolean isSmoketest = false;
private static Path testModeFolder = null;
private static boolean printDebugLogs;
@@ -92,11 +90,6 @@ public class Main {
"clear-config",
false,
"Clears PhotonVision pipeline and networking settings. Preserves log files");
options.addOption(
"s",
"smoketest",
false,
"Exit Photon after loading native libraries and camera configs, but before starting up camera runners");
CommandLineParser parser = new DefaultParser();
CommandLine cmd = parser.parse(options, args);
@@ -134,10 +127,6 @@ public class Main {
if (cmd.hasOption("clear-config")) {
ConfigManager.getInstance().clearConfig();
}
if (cmd.hasOption("smoketest")) {
isSmoketest = true;
}
}
return true;
}
@@ -277,7 +266,7 @@ public class Main {
CameraConfiguration camConf2024 =
ConfigManager.getInstance().getConfig().getCameraConfigurations().get("WPI2024");
if (camConf2024 == null) {
if (camConf2024 == null || true) {
camConf2024 =
new CameraConfiguration(
"WPI2024",
@@ -348,17 +337,11 @@ public class Main {
public static void main(String[] args) {
try {
boolean success = TestUtils.loadLibraries();
if (!success) {
logger.error("Failed to load native libraries! Giving up :(");
System.exit(1);
}
TestUtils.loadLibraries();
logger.info("Native libraries loaded.");
} catch (Exception e) {
logger.error("Failed to load native libraries!", e);
System.exit(1);
}
logger.info("Native libraries loaded.");
try {
if (Platform.isRaspberryPi()) {
@@ -410,8 +393,6 @@ public class Main {
+ Platform.getPlatformName()
+ (Platform.isRaspberryPi() ? (" (Pi " + PiVersion.getPiVersion() + ")") : ""));
PvCSCoreLogger.getInstance();
logger.debug("Loading ConfigManager...");
ConfigManager.getInstance().load(); // init config manager
ConfigManager.getInstance().requestSave();
@@ -431,11 +412,6 @@ public class Main {
NeuralNetworkModelManager.getInstance()
.initialize(ConfigManager.getInstance().getModelsDirectory());
if (isSmoketest) {
logger.info("PhotonVision base functionality loaded -- smoketest complete");
System.exit(0);
}
if (!isTestMode) {
logger.debug("Loading VisionSourceManager...");
VisionSourceManager.getInstance()

View File

@@ -131,16 +131,18 @@ public class DataSocketHandler {
case SMT_DRIVERMODE:
{
// TODO: what is this event?
var data = (Boolean) entryValue;
var data = (HashMap<String, Object>) entryValue;
var dmExpEvent =
new IncomingWebSocketEvent<Integer>(
DataChangeDestination.DCD_ACTIVEMODULE, "driverExposure", data);
var dmBrightEvent =
new IncomingWebSocketEvent<Integer>(
DataChangeDestination.DCD_ACTIVEMODULE, "driverBrightness", data);
var dmIsDriverEvent =
new IncomingWebSocketEvent<Boolean>(
DataChangeDestination.DCD_ACTIVEMODULE,
"isDriverMode",
data,
cameraIndex,
context);
DataChangeDestination.DCD_ACTIVEMODULE, "isDriver", data);
dcService.publishEvents(dmIsDriverEvent);
dcService.publishEvents(dmExpEvent, dmBrightEvent, dmIsDriverEvent);
break;
}
case SMT_CHANGECAMERANAME:
@@ -348,7 +350,8 @@ public class DataSocketHandler {
}
}
private void sendMessage(ByteBuffer b, WsContext user) throws JsonProcessingException {
private void sendMessage(Object message, WsContext user) throws JsonProcessingException {
ByteBuffer b = ByteBuffer.wrap(objectMapper.writeValueAsBytes(message));
if (user.session.isOpen()) {
user.send(b);
}
@@ -356,18 +359,16 @@ public class DataSocketHandler {
public void broadcastMessage(Object message, WsContext userToSkip)
throws JsonProcessingException {
ByteBuffer b = ByteBuffer.wrap(objectMapper.writeValueAsBytes(message));
if (userToSkip == null) {
for (WsContext user : users) {
sendMessage(b, user);
sendMessage(message, user);
}
} else {
var skipUserPort = ((InetSocketAddress) userToSkip.session.getRemoteAddress()).getPort();
for (WsContext user : users) {
var userPort = ((InetSocketAddress) user.session.getRemoteAddress()).getPort();
if (userPort != skipUserPort) {
sendMessage(b, user);
sendMessage(message, user);
}
}
}

Some files were not shown because too many files have changed in this diff Show More