Compare commits

...

38 Commits

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

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

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

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

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

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

---------

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

* type hints fixed up

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

* Filter objdet results by ratio and area

* Address code review

---------

Co-authored-by: Mohammad Durrani <46766905+mdurrani808@users.noreply.github.com>
2024-01-23 14:10:31 -05:00
Craig Schardt
cbbfbda59d clean up debugging println (#1193) 2024-01-22 22:59:42 -05:00
Drew Williams
a3e1dda3aa Fixed cpp sim apriltag layout and cleaned up cpp sim example (#1190)
* Fixed cpp sim apriltag layout and cleaned up cpp sim example

* changed layout for photoncamerasim

---------

Co-authored-by: Drew Williams <DrewW@iARx.com>
2024-01-22 15:38:25 -05:00
Aiden Lambert
939283df0e Fix positioning of multitarget struct in pipelineresult unpack (#1181)
fixed the unpacking order to match the current pipelineresult data layout.

* fix positioning of multitarget struct in pipelineresult unpack

* fix encode order in PhotonPipelineResult.cpp
2024-01-22 13:05:30 -05:00
Craig Schardt
43338a4e96 Temperature monitoring for RK3588 (#1186) 2024-01-22 07:59:40 -05:00
Craig Schardt
bcea6fcc8d Bump WPILib to 2024.2.1 (#1188) 2024-01-21 20:06:47 -05:00
Ethan Wall
90773e0e4a [photonlib-py] Begin implementing PhotonPoseEstimator in Python (#1178)
* [photonlib-py] Initial impl of PhotonPoseEstimator

---------

Co-authored-by: Matt <matthew.morley.ca@gmail.com>
2024-01-21 06:57:32 -06:00
Matt
57f02f31a5 Dont flush settings on exit after import (#1179)
Fixes bug when importing settings zip that would have the new settings be over written, and would not actually update
2024-01-20 20:49:51 -05:00
Matt
580bbb4a4d Draw calibration rainbow and scale thickness based on image size (#1174) 2024-01-20 20:04:15 -05:00
Craig Schardt
4a0c15b61b Disable the network controls when networkingIsDisabled is true (#1118)
* commented controls that should depend on networkingIsDisabled

* add the thing

* fix Manage Device Networking showing disabled

* commented controls that should depend on networkingIsDisabled

* add the thing

* fix Manage Device Networking showing disabled

* Hide the settings that aren't available when networking is disabled

* Update NetworkingCard.vue

* Update NetworkingCard.vue

---------

Co-authored-by: Sriman Achanta <68172138+srimanachanta@users.noreply.github.com>
2024-01-20 19:46:47 -05:00
Programmers3539
a1df37e20f Add Orange Pi 5 Plus image (#1170)
And bumps both opi images to kill snapd
2024-01-20 19:45:58 -05:00
DeltaDizzy
644c162834 Make java examples independent by adding GradleRIO version (#1158) 2024-01-20 19:45:29 -05:00
Max Worrall
5f591a51c4 [photonlib-py] Remove print statement (#1171) 2024-01-18 11:21:42 -05:00
Sriman Achanta
d59be893ae Fix UI bugs from RKNN PR (#1169)
* fix interactiveCols

* fix deferred store bug

* Fix bug where ObjectDetection pipeline could be made on invalid platforms

* Update vite.config.ts
2024-01-16 22:23:05 -05:00
Ryan Blue
f13a507a71 Fix total ram reporting (#1161) 2024-01-15 23:03:52 -05:00
103 changed files with 2409 additions and 1071 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,173 +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
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.4/photonvision_opi5.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,92 +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]
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
runs-on: ubuntu-22.04
steps:
# Download literally every single artifact.
- uses: actions/download-artifact@v4
- run: find .
- name: Setup Pages
uses: actions/configure-pages@v4
- name: Upload artifact
uses: actions/upload-pages-artifact@v3
with:
# Upload entire repository
path: '.'
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v4

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.1.1"
id "edu.wpi.first.GradleRIO" version "2024.3.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,13 +25,13 @@ allprojects {
apply from: "versioningHelper.gradle"
ext {
wpilibVersion = "2024.1.1"
wpilibVersion = "2024.3.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-30-g001b5ec"
rknnVersion = "dev-v2024.0.0-64-gc0836a6"
frcYear = "2024"
mrcalVersion = "dev-v2024.0.0-7-gc976aaa";
@@ -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,15 +25,10 @@ const getUniqueVideoFormatsByResolution = (): VideoFormat[] => {
const calib = useCameraSettingsStore().getCalibrationCoeffs(format.resolution);
if (calib !== undefined) {
// Is this the right formula for RMS error? who knows! not me!
const perViewSumSquareReprojectionError = calib.observations.flatMap((it) =>
it.reprojectionErrors.flatMap((it2) => [it2.x, it2.y])
);
// For each error, square it, sum the squares, and divide by total points N
format.mean = Math.sqrt(
perViewSumSquareReprojectionError.map((it) => Math.pow(it, 2)).reduce((a, b) => a + b, 0) /
perViewSumSquareReprojectionError.length
);
if (calib.meanErrors.length)
format.mean = calib.meanErrors.reduce((a, b) => a + b, 0) / calib.meanErrors.length;
else format.mean = NaN;
format.horizontalFOV =
2 * Math.atan2(format.resolution.width / 2, calib.cameraIntrinsics.data[0]) * (180 / Math.PI);
@@ -109,7 +104,7 @@ const downloadCalibBoard = () => {
const yPos = chessboardStartY + squareY * squareSizeIn.value;
// Only draw the odd squares to create the chessboard pattern
if ((xPos + yPos + 0.25) % 2 === 0) {
if (squareY % 2 != squareX % 2) {
doc.rect(xPos, yPos, squareSizeIn.value, squareSizeIn.value, "F");
}
}
@@ -263,7 +258,7 @@ const setSelectedVideoFormat = (format: VideoFormat) => {
>
<td>{{ getResolutionString(value.resolution) }}</td>
<td>
{{ value.mean !== undefined ? (isNaN(value.mean) ? "NaN" : value.mean.toFixed(2) + "px") : "-" }}
{{ value.mean !== undefined ? (isNaN(value.mean) ? "Unknown" : value.mean.toFixed(2) + "px") : "-" }}
</td>
<td>{{ value.horizontalFOV !== undefined ? value.horizontalFOV.toFixed(2) + "°" : "-" }}</td>
<td>{{ value.verticalFOV !== undefined ? value.verticalFOV.toFixed(2) + "°" : "-" }}</td>
@@ -311,7 +306,7 @@ const setSelectedVideoFormat = (format: VideoFormat) => {
/>
<pv-number-input
v-model="patternWidth"
label="Board Width (in)"
label="Board Width (squares)"
tooltip="Width of the board in dots or chessboard squares"
:disabled="isCalibrating"
:rules="[(v) => v >= 4 || 'Width must be at least 4']"
@@ -319,7 +314,7 @@ const setSelectedVideoFormat = (format: VideoFormat) => {
/>
<pv-number-input
v-model="patternHeight"
label="Board Height (in)"
label="Board Height (squares)"
tooltip="Height of the board in dots or chessboard squares"
:disabled="isCalibrating"
:rules="[(v) => v >= 4 || 'Height must be at least 4']"

View File

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

View File

@@ -27,6 +27,7 @@ const changeCurrentCameraIndex = (index: number) => {
break;
case PipelineType.ObjectDetection:
pipelineType.value = WebsocketPipelineType.ObjectDetection;
break;
}
};
@@ -124,6 +125,18 @@ const cancelPipelineNameEdit = () => {
const showPipelineCreationDialog = ref(false);
const newPipelineName = ref("");
const newPipelineType = ref<WebsocketPipelineType>(useCameraSettingsStore().currentWebsocketPipelineType);
const validNewPipelineTypes = computed(() => {
const pipelineTypes = [
{ name: "Reflective", value: WebsocketPipelineType.Reflective },
{ name: "Colored Shape", value: WebsocketPipelineType.ColoredShape },
{ name: "AprilTag", value: WebsocketPipelineType.AprilTag },
{ name: "Aruco", value: WebsocketPipelineType.Aruco }
];
if (useSettingsStore().general.rknnSupported) {
pipelineTypes.push({ name: "Object Detection", value: WebsocketPipelineType.ObjectDetection });
}
return pipelineTypes;
});
const showCreatePipelineDialog = () => {
newPipelineName.value = "";
newPipelineType.value = useCameraSettingsStore().currentWebsocketPipelineType;
@@ -359,13 +372,7 @@ useCameraSettingsStore().$subscribe((mutation, state) => {
:select-cols="12 - 3"
label="Tracking Type"
tooltip="Pipeline type, which changes the type of processing that will happen on input frames"
:items="[
{ name: 'Reflective', value: WebsocketPipelineType.Reflective },
{ name: 'Colored Shape', value: WebsocketPipelineType.ColoredShape },
{ name: 'AprilTag', value: WebsocketPipelineType.AprilTag },
{ name: 'Aruco', value: WebsocketPipelineType.Aruco },
{ name: 'Object Detection', value: WebsocketPipelineType.ObjectDetection }
]"
:items="validNewPipelineTypes"
/>
</v-card-text>
<v-divider />

View File

@@ -14,13 +14,12 @@ const currentPipelineSettings = computed<ActivePipelineSettings>(
() => useCameraSettingsStore().currentPipelineSettings
);
const interactiveCols = computed(
() =>
(getCurrentInstance()?.proxy.$vuetify.breakpoint.mdAndDown || false) &&
(!useStateStore().sidebarFolded || useCameraSettingsStore().isDriverMode)
)
? 9
: 8;
const interactiveCols = computed(() =>
(getCurrentInstance()?.proxy.$vuetify.breakpoint.mdAndDown || false) &&
(!useStateStore().sidebarFolded || useCameraSettingsStore().isDriverMode)
? 9
: 8
);
</script>
<template>

View File

@@ -14,13 +14,12 @@ const currentPipelineSettings = computed<ActivePipelineSettings>(
() => useCameraSettingsStore().currentPipelineSettings
);
const interactiveCols = computed(
() =>
(getCurrentInstance()?.proxy.$vuetify.breakpoint.mdAndDown || false) &&
(!useStateStore().sidebarFolded || useCameraSettingsStore().isDriverMode)
)
? 9
: 8;
const interactiveCols = computed(() =>
(getCurrentInstance()?.proxy.$vuetify.breakpoint.mdAndDown || false) &&
(!useStateStore().sidebarFolded || useCameraSettingsStore().isDriverMode)
? 9
: 8
);
</script>
<template>

View File

@@ -49,13 +49,12 @@ const contourRadius = computed<[number, number]>({
}
});
const interactiveCols = computed(
() =>
(getCurrentInstance()?.proxy.$vuetify.breakpoint.mdAndDown || false) &&
(!useStateStore().sidebarFolded || useCameraSettingsStore().isDriverMode)
)
? 9
: 8;
const interactiveCols = computed(() =>
(getCurrentInstance()?.proxy.$vuetify.breakpoint.mdAndDown || false) &&
(!useStateStore().sidebarFolded || useCameraSettingsStore().isDriverMode)
? 9
: 8
);
</script>
<template>

View File

@@ -63,13 +63,12 @@ const handleStreamResolutionChange = (value: number) => {
);
};
const interactiveCols = computed(
() =>
(getCurrentInstance()?.proxy.$vuetify.breakpoint.mdAndDown || false) &&
(!useStateStore().sidebarFolded || useCameraSettingsStore().isDriverMode)
)
? 9
: 8;
const interactiveCols = computed(() =>
(getCurrentInstance()?.proxy.$vuetify.breakpoint.mdAndDown || false) &&
(!useStateStore().sidebarFolded || useCameraSettingsStore().isDriverMode)
? 9
: 8
);
</script>
<template>

View File

@@ -1,21 +1,32 @@
<script setup lang="ts">
import { useCameraSettingsStore } from "@/stores/settings/CameraSettingsStore";
import { PipelineType } from "@/types/PipelineTypes";
import { type ActivePipelineSettings, PipelineType } from "@/types/PipelineTypes";
import PvSlider from "@/components/common/pv-slider.vue";
import { 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 = useCameraSettingsStore().currentPipelineSettings;
const currentPipelineSettings = computed<ActivePipelineSettings>(
() => useCameraSettingsStore().currentPipelineSettings
);
const interactiveCols = computed(
() =>
(getCurrentInstance()?.proxy.$vuetify.breakpoint.mdAndDown || false) &&
(!useStateStore().sidebarFolded || useCameraSettingsStore().isDriverMode)
)
? 9
: 8;
// TODO fix pv-range-slider so that store access doesn't need to be deferred
const contourArea = computed<[number, number]>({
get: () => Object.values(useCameraSettingsStore().currentPipelineSettings.contourArea) as [number, number],
set: (v) => (useCameraSettingsStore().currentPipelineSettings.contourArea = v)
});
const contourRatio = computed<[number, number]>({
get: () => Object.values(useCameraSettingsStore().currentPipelineSettings.contourRatio) as [number, number],
set: (v) => (useCameraSettingsStore().currentPipelineSettings.contourRatio = v)
});
const interactiveCols = computed(() =>
(getCurrentInstance()?.proxy.$vuetify.breakpoint.mdAndDown || false) &&
(!useStateStore().sidebarFolded || useCameraSettingsStore().isDriverMode)
? 9
: 8
);
</script>
<template>
@@ -31,5 +42,42 @@ 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,13 +46,12 @@ const currentPipelineSettings = computed<ActivePipelineSettings>(
() => useCameraSettingsStore().currentPipelineSettings
);
const interactiveCols = computed(
() =>
(getCurrentInstance()?.proxy.$vuetify.breakpoint.mdAndDown || false) &&
(!useStateStore().sidebarFolded || useCameraSettingsStore().isDriverMode)
)
? 9
: 8;
const interactiveCols = computed(() =>
(getCurrentInstance()?.proxy.$vuetify.breakpoint.mdAndDown || false) &&
(!useStateStore().sidebarFolded || useCameraSettingsStore().isDriverMode)
? 9
: 8
);
</script>
<template>

View File

@@ -6,13 +6,12 @@ import PvSlider from "@/components/common/pv-slider.vue";
import { computed, getCurrentInstance } from "vue";
import { useStateStore } from "@/stores/StateStore";
const interactiveCols = computed(
() =>
(getCurrentInstance()?.proxy.$vuetify.breakpoint.mdAndDown || false) &&
(!useStateStore().sidebarFolded || useCameraSettingsStore().isDriverMode)
)
? 9
: 8;
const interactiveCols = computed(() =>
(getCurrentInstance()?.proxy.$vuetify.breakpoint.mdAndDown || false) &&
(!useStateStore().sidebarFolded || useCameraSettingsStore().isDriverMode)
? 9
: 8
);
</script>
<template>

View File

@@ -124,13 +124,12 @@ onBeforeUnmount(() => {
cameraStream.removeEventListener("click", handleStreamClick);
});
const interactiveCols = computed(
() =>
(getCurrentInstance()?.proxy.$vuetify.breakpoint.mdAndDown || false) &&
(!useStateStore().sidebarFolded || useCameraSettingsStore().isDriverMode)
)
? 9
: 8;
const interactiveCols = computed(() =>
(getCurrentInstance()?.proxy.$vuetify.breakpoint.mdAndDown || false) &&
(!useStateStore().sidebarFolded || useCameraSettingsStore().isDriverMode)
? 9
: 8
);
</script>
<template>

View File

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

View File

@@ -5,12 +5,11 @@ import PvInput from "@/components/common/pv-input.vue";
import PvRadio from "@/components/common/pv-radio.vue";
import PvSwitch from "@/components/common/pv-switch.vue";
import PvSelect from "@/components/common/pv-select.vue";
import { NetworkConnectionType, type NetworkSettings } from "@/types/SettingTypes";
import { type ConfigurableNetworkSettings, NetworkConnectionType } from "@/types/SettingTypes";
import { useStateStore } from "@/stores/StateStore";
// Copy object to remove reference to store
const tempSettingsStruct = ref<NetworkSettings>(Object.assign({}, useSettingsStore().network));
const tempSettingsStruct = ref<ConfigurableNetworkSettings>(Object.assign({}, useSettingsStore().network));
const resetTempSettingsStruct = () => {
tempSettingsStruct.value = Object.assign({}, useSettingsStore().network);
};
@@ -58,10 +57,10 @@ const settingsHaveChanged = (): boolean => {
a.runNTServer !== b.runNTServer ||
a.shouldManage !== b.shouldManage ||
a.shouldPublishProto !== b.shouldPublishProto ||
a.canManage !== b.canManage ||
a.networkManagerIface !== b.networkManagerIface ||
a.setStaticCommand !== b.setStaticCommand ||
a.setDHCPcommand !== b.setDHCPcommand
a.setDHCPcommand !== b.setDHCPcommand ||
a.matchCamerasOnlyByPath !== b.matchCamerasOnlyByPath
);
};
@@ -79,6 +78,7 @@ const saveGeneralSettings = () => {
setStaticCommand: tempSettingsStruct.value.setStaticCommand || "",
shouldManage: tempSettingsStruct.value.shouldManage,
shouldPublishProto: tempSettingsStruct.value.shouldPublishProto,
matchCamerasOnlyByPath: tempSettingsStruct.value.matchCamerasOnlyByPath,
staticIp: tempSettingsStruct.value.staticIp
};
@@ -91,7 +91,10 @@ const saveGeneralSettings = () => {
});
// Update the local settings cause the backend checked their validity. Assign is to deref value
useSettingsStore().network = Object.assign({}, tempSettingsStruct.value);
useSettingsStore().network = {
...useSettingsStore().network,
...Object.assign({}, tempSettingsStruct.value)
};
})
.catch((error) => {
resetTempSettingsStruct();
@@ -136,6 +139,8 @@ watchEffect(() => {
<template>
<v-card dark class="mb-3 pr-6 pb-3" style="background-color: #006492">
<v-card-title>Global Settings</v-card-title>
<v-divider />
<v-card-title>Networking</v-card-title>
<div class="ml-5">
<v-form ref="form" v-model="settingsValid">
@@ -162,42 +167,63 @@ watchEffect(() => {
The NetworkTables Server Address is not set or is invalid. NetworkTables is unable to connect.
</v-banner>
<pv-radio
v-show="!useSettingsStore().network.networkingDisabled"
v-model="tempSettingsStruct.connectionType"
label="IP Assignment Mode"
tooltip="DHCP will make the radio (router) automatically assign an IP address; this may result in an IP address that changes across reboots. Static IP assignment means that you pick the IP address and it won't change."
:input-cols="12 - 4"
:list="['DHCP', 'Static']"
:disabled="!(tempSettingsStruct.shouldManage && tempSettingsStruct.canManage)"
:disabled="
!tempSettingsStruct.shouldManage ||
!useSettingsStore().network.canManage ||
useSettingsStore().network.networkingDisabled
"
/>
<pv-input
v-show="!useSettingsStore().network.networkingDisabled"
v-if="tempSettingsStruct.connectionType === NetworkConnectionType.Static"
v-model="tempSettingsStruct.staticIp"
:input-cols="12 - 4"
label="Static IP"
:rules="[(v) => isValidIPv4(v) || 'Invalid IPv4 address']"
:disabled="!(tempSettingsStruct.shouldManage && tempSettingsStruct.canManage)"
:disabled="
!tempSettingsStruct.shouldManage ||
!useSettingsStore().network.canManage ||
useSettingsStore().network.networkingDisabled
"
/>
<pv-input
v-show="!useSettingsStore().network.networkingDisabled"
v-model="tempSettingsStruct.hostname"
label="Hostname"
:input-cols="12 - 4"
:rules="[(v) => isValidHostname(v) || 'Invalid hostname']"
:disabled="!(tempSettingsStruct.shouldManage && tempSettingsStruct.canManage)"
:disabled="
!tempSettingsStruct.shouldManage ||
!useSettingsStore().network.canManage ||
useSettingsStore().network.networkingDisabled
"
/>
<v-divider class="pb-3" />
<span style="font-weight: 700">Advanced Networking</span>
<pv-switch
v-show="!useSettingsStore().network.networkingDisabled"
v-model="tempSettingsStruct.shouldManage"
:disabled="!tempSettingsStruct.canManage"
:disabled="!useSettingsStore().network.canManage || useSettingsStore().network.networkingDisabled"
label="Manage Device Networking"
tooltip="If enabled, Photon will manage device hostname and network settings."
:label-cols="4"
class="pt-2"
/>
<pv-select
v-show="!useSettingsStore().network.networkingDisabled"
v-model="currentNetworkInterfaceIndex"
label="NetworkManager interface"
:disabled="!(tempSettingsStruct.shouldManage && tempSettingsStruct.canManage)"
:disabled="
!tempSettingsStruct.shouldManage ||
!useSettingsStore().network.canManage ||
useSettingsStore().network.networkingDisabled
"
:select-cols="12 - 4"
tooltip="Name of the interface PhotonVision should manage the IP address of"
:items="useSettingsStore().networkInterfaceNames"
@@ -206,7 +232,8 @@ watchEffect(() => {
v-show="
!useSettingsStore().networkInterfaceNames.length &&
tempSettingsStruct.shouldManage &&
tempSettingsStruct.canManage
useSettingsStore().network.canManage &&
!useSettingsStore().network.networkingDisabled
"
rounded
color="red"
@@ -231,6 +258,9 @@ watchEffect(() => {
>
This mode is intended for debugging; it should be off for proper usage. PhotonLib will NOT work!
</v-banner>
<v-divider />
<v-card-title>Miscellaneous</v-card-title>
<pv-switch
v-model="tempSettingsStruct.shouldPublishProto"
label="Also Publish Protobuf"
@@ -249,6 +279,32 @@ watchEffect(() => {
This mode is intended for debugging; it should be off for field use. You may notice a performance hit by using
this mode.
</v-banner>
<pv-switch
v-model="tempSettingsStruct.matchCamerasOnlyByPath"
label="Strictly match ONLY known cameras"
tooltip="ONLY match cameras by the USB port they're plugged into + (basename or USB VID/PID), and never only by the device product string. Also disables automatic detection of new cameras."
class="mt-3 mb-2"
:label-cols="4"
/>
<v-banner
v-show="tempSettingsStruct.matchCamerasOnlyByPath"
rounded
color="red"
class="mb-3"
text-color="white"
icon="mdi-information-outline"
>
Physical cameras will be strictly matched to camera configurations using physical USB port they are plugged
into, in addition to device name and other USB metadata. Additionally, no new cameras are allowed to be added.
This setting is useful for guaranteeing that an already known and configured camera can never be matched as an
"unknown"/"new" camera, which resets pipelines and calibration data.
<p />
Cameras will NOT be matched if they change USB ports, and new cameras plugged into this coprocessor will NOT
be automatically recognized or configured for vision processing.
<p />
To add a new camera to this coprocessor, disable this setting, connect the camera, and re-enable.
</v-banner>
<v-divider class="mb-3" />
</v-form>
<v-btn
color="accent"

View File

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

View File

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

View File

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

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,9 +37,8 @@ dependencies {
implementation 'org.zeroturnaround:zt-zip:1.14'
implementation "org.xerial:sqlite-jdbc:3.41.0.0"
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:rknn_jni-jni:$rknnVersion:linuxarm64"
implementation "org.photonvision:rknn_jni-java:$rknnVersion"
implementation "org.photonvision:photon-libcamera-gl-driver-jni:$photonGlDriverLibVersion:linuxarm64"
implementation "org.photonvision:photon-libcamera-gl-driver-java:$photonGlDriverLibVersion"

View File

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

View File

@@ -50,6 +50,10 @@ public class ConfigManager {
private final Thread settingsSaveThread;
private long saveRequestTimestamp = -1;
// special case flag to disable flushing settings to disk at shutdown. Avoids the jvm shutdown
// hook overwriting the settings we just uploaded
private boolean flushOnShutdown = true;
enum ConfigSaveStrategy {
SQL,
LEGACY,
@@ -303,4 +307,19 @@ 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,6 +20,24 @@ package org.photonvision.common.configuration;
public class HardwareSettings {
public int ledBrightnessPercentage = 100;
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ledBrightnessPercentage;
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (obj == null) return false;
if (getClass() != obj.getClass()) return false;
HardwareSettings other = (HardwareSettings) obj;
if (ledBrightnessPercentage != other.ledBrightnessPercentage) return false;
return true;
}
@Override
public String toString() {
return "HardwareSettings [ledBrightnessPercentage=" + ledBrightnessPercentage + "]";

View File

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

View File

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

View File

@@ -25,12 +25,13 @@ import java.util.Map;
import java.util.stream.Collectors;
import org.photonvision.PhotonVersion;
import org.photonvision.common.hardware.Platform;
import org.photonvision.common.networking.NetworkManager;
import org.photonvision.common.networking.NetworkUtils;
import org.photonvision.common.util.SerializationUtils;
import org.photonvision.jni.RknnDetectorJNI;
import org.photonvision.mrcal.MrCalJNILoader;
import org.photonvision.raspi.LibCameraJNILoader;
import org.photonvision.vision.calibration.CameraCalibrationCoefficients;
import org.photonvision.vision.calibration.UICameraCalibrationCoefficients;
import org.photonvision.vision.camera.QuirkyCamera;
import org.photonvision.vision.processes.VisionModule;
import org.photonvision.vision.processes.VisionModuleManager;
@@ -121,16 +122,10 @@ public class PhotonConfiguration {
// Hack active interfaces into networkSettings
var netConfigMap = networkConfig.toHashMap();
netConfigMap.put("networkInterfaceNames", NetworkUtils.getAllWiredInterfaces());
netConfigMap.put("networkingDisabled", NetworkManager.getInstance().networkingIsDisabled);
settingsSubmap.put("networkSettings", netConfigMap);
map.put(
"cameraSettings",
VisionModuleManager.getInstance().getModules().stream()
.map(VisionModule::toUICameraConfig)
.map(SerializationUtils::objectToHashMap)
.collect(Collectors.toList()));
var lightingConfig = new UILightingConfig();
lightingConfig.brightness = hardwareSettings.ledBrightnessPercentage;
lightingConfig.supported = !hardwareConfig.ledPins.isEmpty();
@@ -179,7 +174,7 @@ public class PhotonConfiguration {
public HashMap<Integer, HashMap<String, Object>> videoFormatList;
public int outputStreamPort;
public int inputStreamPort;
public List<CameraCalibrationCoefficients> calibrations;
public List<UICameraCalibrationCoefficients> calibrations;
public boolean isFovConfigurable = true;
public QuirkyCamera cameraQuirks;
public boolean isCSICamera;

View File

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

View File

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

View File

@@ -28,6 +28,7 @@ import org.photonvision.common.hardware.metrics.cmds.CmdBase;
import org.photonvision.common.hardware.metrics.cmds.FileCmds;
import org.photonvision.common.hardware.metrics.cmds.LinuxCmds;
import org.photonvision.common.hardware.metrics.cmds.PiCmds;
import org.photonvision.common.hardware.metrics.cmds.RK3588Cmds;
import org.photonvision.common.logging.LogGroup;
import org.photonvision.common.logging.Logger;
import org.photonvision.common.util.ShellExec;
@@ -44,6 +45,8 @@ public class MetricsManager {
cmds = new FileCmds();
} else if (Platform.isRaspberryPi()) {
cmds = new PiCmds(); // Pi's can use a hardcoded command set
} else if (Platform.isRK3588()) {
cmds = new RK3588Cmds(); // RK3588 chipset hardcoded command set
} else if (Platform.isLinux()) {
cmds = new LinuxCmds(); // Linux/Unix platforms assume a nominal command set
} else {
@@ -89,6 +92,10 @@ public class MetricsManager {
return safeExecute(cmds.cpuThrottleReasonCmd);
}
public String getNpuUsage() {
return safeExecute(cmds.npuUsageCommand);
}
private String gpuMemSave = null;
public String getGPUMemorySplit() {
@@ -125,6 +132,7 @@ public class MetricsManager {
metrics.put("ramUtil", this.getUsedRam());
metrics.put("gpuMemUtil", this.getMallocedMemory());
metrics.put("diskUtilPct", this.getUsedDiskPct());
metrics.put("npuUsage", this.getNpuUsage());
DataChangeService.getInstance().publishEvent(OutgoingUIEvent.wrappedOf("metrics", metrics));
}

View File

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

View File

@@ -25,7 +25,6 @@ public class PiCmds extends LinuxCmds {
super.initCmds(config);
// CPU
cpuMemoryCommand = "free -m | awk 'FNR == 2 {print $2}'";
cpuTemperatureCommand = "sed 's/.\\{3\\}$/.&/' /sys/class/thermal/thermal_zone0/temp";
cpuThrottleReasonCmd =
"if (( $(( $(vcgencmd get_throttled | grep -Eo 0x[0-9a-fA-F]*) & 0x01 )) != 0x00 )); then echo \"LOW VOLTAGE\"; "

View File

@@ -0,0 +1,50 @@
/*
* Copyright (C) Photon Vision.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.photonvision.common.hardware.metrics.cmds;
import org.photonvision.common.configuration.HardwareConfig;
public class RK3588Cmds extends LinuxCmds {
/** Applies pi-specific commands, ignoring any input configuration */
public void initCmds(HardwareConfig config) {
super.initCmds(config);
// CPU Temperature
/* The RK3588 chip has 7 thermal zones that can be accessed via:
* /sys/class/thermal/thermal_zoneX/temp
* where X is an interger from 0 to 6.
*
* || Zone || Location || Comments ||
* | 0 | soc | soc thermal (near the center of the chip) |
* | 1 | bigcore0 | CPU Big Core A76_0/1 (CPU4 and CPU5) |
* | 2 | bigcore1 | CPU Big Core A76_2/3 (CPU6 and CPU7) |
* | 3 | littlecore | CPU Small Core A55_0/1/2/3 (CPU0, CPU1, CPU2, and CPU3) |
* | 4 | center | also called PD_CENTER |
* | 5 | gpu | GPU |
* | 6 | npu | NPU |
*
* Sources:
* - http://forum.armsom.org/t/topic/51/3
* - https://lore.kernel.org/lkml/7276280.TLKafQO6qx@archbook/
*/
cpuTemperatureCommand =
"cat /sys/class/thermal/thermal_zone1/temp | awk '{printf \"%.1f\", $1/1000}'";
npuUsageCommand = "cat /sys/kernel/debug/rknpu/load | sed 's/NPU load://; s/^ *//; s/ *$//'";
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -49,9 +49,17 @@ public class USBCameraSource extends VisionSource {
super(config);
logger = new Logger(USBCameraSource.class, config.nickname, LogGroup.Camera);
camera = new UsbCamera(config.nickname, config.path);
// cscore will auto-reconnect to the camera path we give it. v4l does not guarantee that if i
// swap cameras around, the same /dev/videoN ID will be assigned to that camera. So instead
// default to pinning to a particular USB port, or by "path" (appears to be a global identifier)
// on Windows.
camera = new UsbCamera(config.nickname, config.getUSBPath().orElse(config.path));
cvSink = CameraServer.getVideo(this.camera);
// set vid/pid if not done already for future matching
if (config.usbVID <= 0) config.usbVID = this.camera.getInfo().vendorId;
if (config.usbPID <= 0) config.usbPID = this.camera.getInfo().productId;
if (getCameraConfiguration().cameraQuirks == null)
getCameraConfiguration().cameraQuirks =
QuirkyCamera.getQuirkyCamera(
@@ -395,6 +403,7 @@ public class USBCameraSource extends VisionSource {
// Sort by resolution
var sortedList =
videoModesList.stream()
.distinct() // remove redundant video mode entries
.sorted(((a, b) -> (b.width + b.height) - (a.width + a.height)))
.collect(Collectors.toList());
Collections.reverse(sortedList);

View File

@@ -47,6 +47,16 @@ public class Contour implements Releasable {
this.mat = mat;
}
public Contour(Rect2d box) {
// no easy way to convert a Rect2d to Mat, diy it. Order is tl tr br bl
this.mat =
new MatOfPoint(
box.tl(),
new Point(box.x + box.width, box.y),
box.br(),
new Point(box.x, box.y + box.height));
}
public MatOfPoint2f getMat2f() {
if (mat2f == null) {
mat2f = new MatOfPoint2f(mat.toArray());

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

View File

@@ -22,6 +22,7 @@ import java.util.List;
import org.apache.commons.lang3.tuple.Pair;
import org.opencv.core.Mat;
import org.opencv.core.Point;
import org.opencv.core.Scalar;
import org.opencv.imgproc.Imgproc;
import org.photonvision.common.util.ColorHelper;
import org.photonvision.vision.frame.FrameDivisor;
@@ -31,22 +32,44 @@ import org.photonvision.vision.target.TrackedTarget;
public class DrawCalibrationPipe
extends MutatingPipe<
Pair<Mat, List<TrackedTarget>>, DrawCalibrationPipe.DrawCalibrationPipeParams> {
Scalar[] chessboardColors =
new Scalar[] {
ColorHelper.colorToScalar(Color.RED, 0.4),
ColorHelper.colorToScalar(Color.ORANGE, 0.4),
ColorHelper.colorToScalar(Color.GREEN, 0.4),
ColorHelper.colorToScalar(Color.BLUE, 0.4),
ColorHelper.colorToScalar(Color.MAGENTA, 0.4),
};
@Override
protected Void process(Pair<Mat, List<TrackedTarget>> in) {
var image = in.getLeft();
var imgSz = image.size();
var diag = Math.hypot(imgSz.width, imgSz.height);
// heuristic: about 4px at a diagonal of 750px, or .5%, 'looks good'. keep it at least 3px at
// worst tho
int r = (int) Math.max(diag * 4.0 / 750.0, 3);
int thickness = (int) Math.max(diag * 1.0 / 600.0, 1);
int i = 0;
for (var target : in.getRight()) {
for (var c : target.getTargetCorners()) {
c =
new Point(
c.x / params.divisor.value.doubleValue(), c.y / params.divisor.value.doubleValue());
var r = 4;
var r2 = r / Math.sqrt(2);
var color = ColorHelper.colorToScalar(Color.RED, 0.4);
Imgproc.circle(image, c, r, color, 1);
Imgproc.line(image, new Point(c.x - r2, c.y - r2), new Point(c.x + r2, c.y + r2), color);
Imgproc.line(image, new Point(c.x + r2, c.y - r2), new Point(c.x - r2, c.y + r2), color);
var color = chessboardColors[i % chessboardColors.length];
Imgproc.circle(image, c, r, color, thickness);
Imgproc.line(
image, new Point(c.x - r2, c.y - r2), new Point(c.x + r2, c.y + r2), color, thickness);
Imgproc.line(
image, new Point(c.x + r2, c.y - r2), new Point(c.x - r2, c.y + r2), color, thickness);
}
i++;
}
return null;

View File

@@ -0,0 +1,89 @@
/*
* Copyright (C) Photon Vision.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.photonvision.vision.pipe.impl;
import java.util.ArrayList;
import java.util.List;
import org.photonvision.common.util.numbers.DoubleCouple;
import org.photonvision.vision.frame.FrameStaticProperties;
import org.photonvision.vision.pipe.CVPipe;
public class FilterObjectDetectionsPipe
extends CVPipe<
List<NeuralNetworkPipeResult>,
List<NeuralNetworkPipeResult>,
FilterObjectDetectionsPipe.FilterContoursParams> {
List<NeuralNetworkPipeResult> m_filteredContours = new ArrayList<>();
@Override
protected List<NeuralNetworkPipeResult> process(List<NeuralNetworkPipeResult> in) {
m_filteredContours.clear();
for (var contour : in) {
filterContour(contour);
}
return m_filteredContours;
}
private void filterContour(NeuralNetworkPipeResult contour) {
var boc = contour.box;
// Area filtering
double areaPercentage = boc.area() / params.getFrameStaticProperties().imageArea * 100.0;
double minAreaPercentage = params.getArea().getFirst();
double maxAreaPercentage = params.getArea().getSecond();
if (areaPercentage < minAreaPercentage || areaPercentage > maxAreaPercentage) return;
// Aspect ratio filtering; much simpler since always axis-aligned
double aspectRatio = boc.width / boc.height;
if (aspectRatio < params.getRatio().getFirst() || aspectRatio > params.getRatio().getSecond())
return;
m_filteredContours.add(contour);
}
public static class FilterContoursParams {
private final DoubleCouple m_area;
private final DoubleCouple m_ratio;
private final FrameStaticProperties m_frameStaticProperties;
public final boolean isLandscape;
public FilterContoursParams(
DoubleCouple area,
DoubleCouple ratio,
FrameStaticProperties camProperties,
boolean isLandscape) {
this.m_area = area;
this.m_ratio = ratio;
this.m_frameStaticProperties = camProperties;
this.isLandscape = isLandscape;
}
public DoubleCouple getArea() {
return m_area;
}
public DoubleCouple getRatio() {
return m_ratio;
}
public FrameStaticProperties getFrameStaticProperties() {
return m_frameStaticProperties;
}
}
}

View File

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

View File

@@ -42,6 +42,7 @@ public class SortContoursPipe
if (params.getSortMode() != ContourSortMode.Centermost) {
m_sortedContours.sort(params.getSortMode().getComparator());
} else {
// we need knowledge of camera properties to calculate this distance -- do it ourselves
m_sortedContours.sort(Comparator.comparingDouble(this::calcSquareCenterDistance));
}
}
@@ -50,10 +51,10 @@ public class SortContoursPipe
m_sortedContours.subList(0, Math.min(in.size(), params.getMaxTargets())));
}
private double calcSquareCenterDistance(PotentialTarget rect) {
private double calcSquareCenterDistance(PotentialTarget tgt) {
return Math.sqrt(
Math.pow(params.getCamProperties().centerX - rect.getMinAreaRect().center.x, 2)
+ Math.pow(params.getCamProperties().centerY - rect.getMinAreaRect().center.y, 2));
Math.pow(params.getCamProperties().centerX - tgt.getMinAreaRect().center.x, 2)
+ Math.pow(params.getCamProperties().centerY - tgt.getMinAreaRect().center.y, 2));
}
public static class SortContoursParams {

View File

@@ -26,6 +26,8 @@ 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;

View File

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

View File

@@ -17,21 +17,26 @@
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;
@@ -52,6 +57,38 @@ 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
@@ -60,31 +97,35 @@ public class ObjectDetectionPipeline
// ***************** change based on backend ***********************
CVPipeResult<List<NeuralNetworkPipeResult>> ret = rknnPipe.run(input_frame.colorImage);
sumPipeNanosElapsed += ret.nanosElapsed;
CVPipeResult<List<NeuralNetworkPipeResult>> rknnResult = rknnPipe.run(input_frame.colorImage);
sumPipeNanosElapsed += rknnResult.nanosElapsed;
List<NeuralNetworkPipeResult> targetList;
targetList = ret.output;
var names = rknnPipe.getClassNames();
input_frame.colorImage.getMat().copyTo(input_frame.processedImage.getMat());
// ***************** change based on backend ***********************
List<TrackedTarget> targets = new ArrayList<>();
var filterContoursResult = filterContoursPipe.run(rknnResult.output);
sumPipeNanosElapsed += filterContoursResult.nanosElapsed;
for (var t : targetList) {
targets.add(
new TrackedTarget(
t,
new TargetCalculationParameters(
false, null, null, null, null, frameStaticProperties)));
}
CVPipeResult<List<PotentialTarget>> sortContoursResult =
sortContoursPipe.run(
filterContoursResult.output.stream()
.map(shape -> new PotentialTarget(shape))
.collect(Collectors.toList()));
sumPipeNanosElapsed += sortContoursResult.nanosElapsed;
CVPipeResult<List<TrackedTarget>> collect2dTargetsResult =
collect2dTargetsPipe.run(sortContoursResult.output);
sumPipeNanosElapsed += collect2dTargetsResult.nanosElapsed;
var fpsResult = calculateFPSPipe.run(null);
var fps = fpsResult.output;
return new CVPipelineResult(sumPipeNanosElapsed, fps, targets, input_frame, names);
return new CVPipelineResult(
sumPipeNanosElapsed, fps, collect2dTargetsResult.output, input_frame, names);
}
@Override

View File

@@ -64,29 +64,6 @@ public class ReflectivePipeline extends CVPipeline<CVPipelineResult, ReflectiveP
settings.offsetDualPointB,
settings.offsetDualPointBArea);
// var rotateImageParams = new
// RotateImagePipe.RotateImageParams(settings.inputImageRotationMode);
// rotateImagePipe.setParams(rotateImageParams);
// if (cameraQuirks.hasQuirk(CameraQuirk.PiCam) && LibCameraJNI.isSupported()) {
// LibCameraJNI.setThresholds(
// settings.hsvHue.getFirst() / 180d,
// settings.hsvSaturation.getFirst() / 255d,
// settings.hsvValue.getFirst() / 255d,
// settings.hsvHue.getSecond() / 180d,
// settings.hsvSaturation.getSecond() / 255d,
// settings.hsvValue.getSecond() / 255d);
// // LibCameraJNI.setInvertHue(settings.hueInverted);
// LibCameraJNI.setRotation(settings.inputImageRotationMode.value);
// // LibCameraJNI.setShouldCopyColor(settings.inputShouldShow);
// } else {
// var hsvParams =
// new HSVPipe.HSVParams(
// settings.hsvHue, settings.hsvSaturation, settings.hsvValue,
// settings.hueInverted);
// hsvPipe.setParams(hsvParams);
// }
var findContoursParams = new FindContoursPipe.FindContoursParams();
findContoursPipe.setParams(findContoursParams);
@@ -113,7 +90,7 @@ public class ReflectivePipeline extends CVPipeline<CVPipelineResult, ReflectiveP
var sortContoursParams =
new SortContoursPipe.SortContoursParams(
settings.contourSortMode,
settings.outputShowMultipleTargets ? 8 : 1, // TODO don't hardcode?
settings.outputShowMultipleTargets ? MAX_MULTI_TARGET_RESULTS : 1,
frameStaticProperties);
sortContoursPipe.setParams(sortContoursParams);

View File

@@ -0,0 +1,57 @@
/*
* Copyright (C) Photon Vision.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.photonvision.vision.processes;
import java.util.List;
import org.photonvision.vision.camera.CameraType;
public class CameraMatchingOptions {
public CameraMatchingOptions(
boolean checkUSBPath,
boolean checkVidPid,
boolean checkBaseName,
boolean checkPath,
CameraType... allowedTypes) {
this.checkUSBPath = checkUSBPath;
this.checkVidPid = checkVidPid;
this.checkBaseName = checkBaseName;
this.checkPath = checkPath;
this.allowedTypes = List.of(allowedTypes);
}
public final boolean checkUSBPath;
public final boolean checkVidPid;
public final boolean checkBaseName;
public final boolean checkPath;
public final List<CameraType> allowedTypes;
@Override
public String toString() {
return "CameraMatchingOptions [checkUSBPath="
+ checkUSBPath
+ ", checkVidPid="
+ checkVidPid
+ ", checkBaseName="
+ checkBaseName
+ ", checkPath="
+ checkPath
+ ", allowedTypes="
+ allowedTypes
+ "]";
}
}

View File

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

View File

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

View File

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

View File

@@ -21,7 +21,9 @@ import java.util.List;
import org.opencv.core.RotatedRect;
import org.photonvision.vision.opencv.CVShape;
import org.photonvision.vision.opencv.Contour;
import org.photonvision.vision.opencv.ContourShape;
import org.photonvision.vision.opencv.Releasable;
import org.photonvision.vision.pipe.impl.NeuralNetworkPipeResult;
public class PotentialTarget implements Releasable {
@@ -29,6 +31,10 @@ public class PotentialTarget implements Releasable {
public final List<Contour> m_subContours;
public final CVShape shape;
// additional metadata about object detections we need to keep around
public final double confidence;
public final int clsId;
public PotentialTarget(Contour inputContour) {
this(inputContour, List.of());
}
@@ -41,12 +47,26 @@ public class PotentialTarget implements Releasable {
m_mainContour = inputContour;
m_subContours = new ArrayList<>(subContours);
this.shape = shape;
this.clsId = -1;
this.confidence = -1;
}
public PotentialTarget(Contour inputContour, CVShape shape) {
this(inputContour, List.of(), shape);
}
public PotentialTarget(NeuralNetworkPipeResult det) {
this.shape = new CVShape(new Contour(det.box), ContourShape.Quadrilateral);
this.m_mainContour = this.shape.getContour();
m_subContours = List.of();
this.clsId = det.classIdx;
this.confidence = det.confidence;
}
public PotentialTarget(CVShape cvShape) {
this(cvShape.getContour(), cvShape);
}
public RotatedRect getMinAreaRect() {
return m_mainContour.getMinAreaRect();
}
@@ -61,7 +81,7 @@ public class PotentialTarget implements Releasable {
for (var sc : m_subContours) {
sc.release();
}
m_subContours.clear();
if (!m_subContours.isEmpty()) m_subContours.clear();
if (shape != null) shape.release();
}
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -4,7 +4,7 @@ import wpilib
class Packet:
def __init__(self, data: list[int]):
def __init__(self, data: bytes):
"""
* Constructs an empty packet.
*
@@ -30,7 +30,7 @@ class Packet:
matches the version of photonlib running in the robot code.
"""
def _getNextByte(self) -> int:
def _getNextByteAsInt(self) -> int:
retVal = 0x00
if not self.outOfBytes:
@@ -43,7 +43,7 @@ class Packet:
return retVal
def getData(self) -> list[int]:
def getData(self) -> bytes:
"""
* Returns the packet data.
*
@@ -51,7 +51,7 @@ class Packet:
"""
return self.packetData
def setData(self, data: list[int]):
def setData(self, data: bytes):
"""
* 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._getNextByte())
intList.append(self._getNextByteAsInt())
# 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
from photonlibpy.version import PHOTONVISION_VERSION, PHOTONLIB_VERSION # type: ignore[import-untyped]
class VisionLEDMode(Enum):
@@ -86,10 +86,11 @@ class PhotonCamera:
if len(byteList) < 1:
return retVal
else:
retVal.populateFromPacket(Packet(byteList))
pkt = Packet(byteList)
retVal.populateFromPacket(pkt)
# NT4 allows us to correct the timestamp based on when the message was sent
retVal.setTimestampSeconds(
timestamp / 1e-6 - retVal.getLatencyMillis() / 1e-3
timestamp / 1e6 - retVal.getLatencyMillis() / 1e3
)
return retVal

View File

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

View File

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

View File

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

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

View File

@@ -408,8 +408,8 @@ public class PhotonPoseEstimator {
return Optional.empty();
}
if (estimatedPose.isEmpty()) {
lastPose = null;
if (estimatedPose.isPresent()) {
lastPose = estimatedPose.get().estimatedPose;
}
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,6 +186,9 @@ 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::k2023ChargedUp)};
frc::LoadAprilTagLayoutField(frc::AprilTagField::k2024Crescendo)};
cs::CvSource videoSimRaw;
cv::Mat videoSimFrameRaw{};

View File

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

View File

@@ -220,8 +220,9 @@ TEST_P(VisionSystemSimTestWithParamsTest, YawAngles) {
visionSysSim.AddVisionTargets({photon::VisionTargetSim{
targetPose, photon::TargetModel{0.5_m, 0.5_m}, 3}});
robotPose = frc::Pose2d{frc::Translation2d{10_m, 0_m},
frc::Rotation2d{-1 * GetParam()}};
// 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()}};
visionSysSim.Update(robotPose);
ASSERT_TRUE(camera.GetLatestResult().HasTargets());
ASSERT_NEAR(GetParam().to<double>(),

View File

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

View File

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

View File

@@ -130,6 +130,8 @@ public class Server {
app.post("/api/utils/restartDevice", RequestHandler::onDeviceRestartRequest);
app.post("/api/utils/publishMetrics", RequestHandler::onMetricsPublishRequest);
app.get("/api/utils/getImageSnapshots", RequestHandler::onImageSnapshotsRequest);
app.get("/api/utils/getCalSnapshot", RequestHandler::onCalibrationSnapshotRequest);
app.get("/api/utils/getCalibrationJSON", RequestHandler::onCalibrationExportRequest);
// Calibration
app.post("/api/calibration/end", RequestHandler::onCalibrationEndRequest);
@@ -138,7 +140,6 @@ public class Server {
app.post("/api/calibration/importFromData", RequestHandler::onDataCalibrationImportRequest);
app.start(port);
System.out.println("hi");
}
/**

View File

@@ -37,12 +37,13 @@ bool PhotonPipelineResult::operator==(const PhotonPipelineResult& other) const {
Packet& operator<<(Packet& packet, const PhotonPipelineResult& result) {
// Encode latency and number of targets.
packet << result.latency.value() << result.multitagResult
packet << result.latency.value()
<< static_cast<int8_t>(result.targets.size());
// Encode the information of each target.
for (auto& target : result.targets) packet << target;
packet << result.multitagResult;
// Return the packet
return packet;
}
@@ -51,7 +52,7 @@ Packet& operator>>(Packet& packet, PhotonPipelineResult& result) {
// Decode latency, existence of targets, and number of targets.
double latencyMillis = 0;
int8_t targetCount = 0;
packet >> latencyMillis >> result.multitagResult >> targetCount;
packet >> latencyMillis >> targetCount;
result.latency = units::millisecond_t(latencyMillis);
result.targets.clear();
@@ -62,6 +63,8 @@ Packet& operator>>(Packet& packet, PhotonPipelineResult& result) {
packet >> target;
result.targets.push_back(target);
}
packet >> result.multitagResult;
return packet;
}
} // namespace photon

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,13 +1,12 @@
plugins {
id "com.diffplug.spotless" version "6.1.2"
id "edu.wpi.first.GradleRIO" version "2024.1.1" apply false
}
allprojects {
repositories {
mavenCentral()
mavenLocal()
maven { url = "https://maven.photonvision.org/repository/internal/" }
maven { url = "https://maven.photonvision.org/releases" }
}
}

View File

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

View File

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

View File

@@ -35,48 +35,48 @@
namespace constants {
namespace Vision {
static constexpr std::string_view kCameraName{"YOUR CAMERA NAME"};
static const frc::Transform3d kRobotToCam{
inline constexpr std::string_view kCameraName{"YOUR CAMERA NAME"};
inline const frc::Transform3d kRobotToCam{
frc::Translation3d{0.5_m, 0.0_m, 0.5_m},
frc::Rotation3d{0_rad, 0_rad, 0_rad}};
static const frc::AprilTagFieldLayout kTagLayout{
frc::LoadAprilTagLayoutField(frc::AprilTagField::k2023ChargedUp)};
inline const frc::AprilTagFieldLayout kTagLayout{
frc::LoadAprilTagLayoutField(frc::AprilTagField::k2024Crescendo)};
static const Eigen::Matrix<double, 3, 1> kSingleTagStdDevs{4, 4, 8};
static const Eigen::Matrix<double, 3, 1> kMultiTagStdDevs{0.5, 0.5, 1};
inline const Eigen::Matrix<double, 3, 1> kSingleTagStdDevs{4, 4, 8};
inline const Eigen::Matrix<double, 3, 1> kMultiTagStdDevs{0.5, 0.5, 1};
} // namespace Vision
namespace Swerve {
static constexpr units::meter_t kTrackWidth{18.5_in};
static constexpr units::meter_t kTrackLength{18.5_in};
static constexpr units::meter_t kRobotWidth{25_in + 3.25_in * 2};
static constexpr units::meter_t kRobotLength{25_in + 3.25_in * 2};
static constexpr units::meters_per_second_t kMaxLinearSpeed{15.5_fps};
static constexpr units::radians_per_second_t kMaxAngularSpeed{720_deg_per_s};
static constexpr units::meter_t kWheelDiameter{4_in};
static constexpr units::meter_t kWheelCircumference{kWheelDiameter *
inline constexpr units::meter_t kTrackWidth{18.5_in};
inline constexpr units::meter_t kTrackLength{18.5_in};
inline constexpr units::meter_t kRobotWidth{25_in + 3.25_in * 2};
inline constexpr units::meter_t kRobotLength{25_in + 3.25_in * 2};
inline constexpr units::meters_per_second_t kMaxLinearSpeed{15.5_fps};
inline constexpr units::radians_per_second_t kMaxAngularSpeed{720_deg_per_s};
inline constexpr units::meter_t kWheelDiameter{4_in};
inline constexpr units::meter_t kWheelCircumference{kWheelDiameter *
std::numbers::pi};
static constexpr double kDriveGearRatio = 6.75;
static constexpr double kSteerGearRatio = 12.8;
inline constexpr double kDriveGearRatio = 6.75;
inline constexpr double kSteerGearRatio = 12.8;
static constexpr units::meter_t kDriveDistPerPulse =
inline constexpr units::meter_t kDriveDistPerPulse =
kWheelCircumference / 1024.0 / kDriveGearRatio;
static constexpr units::radian_t kSteerRadPerPulse =
inline constexpr units::radian_t kSteerRadPerPulse =
units::radian_t{2 * std::numbers::pi} / 1024.0;
static constexpr double kDriveKP = 1.0;
static constexpr double kDriveKI = 0.0;
static constexpr double kDriveKD = 0.0;
inline constexpr double kDriveKP = 1.0;
inline constexpr double kDriveKI = 0.0;
inline constexpr double kDriveKD = 0.0;
static constexpr double kSteerKP = 20.0;
static constexpr double kSteerKI = 0.0;
static constexpr double kSteerKD = 0.25;
inline constexpr double kSteerKP = 20.0;
inline constexpr double kSteerKI = 0.0;
inline constexpr double kSteerKD = 0.25;
static const frc::SimpleMotorFeedforward<units::meters> kDriveFF{
inline const frc::SimpleMotorFeedforward<units::meters> kDriveFF{
0.25_V, 2.5_V / 1_mps, 0.3_V / 1_mps_sq};
static const frc::SimpleMotorFeedforward<units::radians> kSteerFF{
inline const frc::SimpleMotorFeedforward<units::radians> kSteerFF{
0.5_V, 0.25_V / 1_rad_per_s, 0.01_V / 1_rad_per_s_sq};
struct ModuleConstants {
@@ -106,13 +106,13 @@ struct ModuleConstants {
centerOffset(frc::Translation2d{xOffset, yOffset}) {}
};
static const ModuleConstants FL_CONSTANTS{
inline const ModuleConstants FL_CONSTANTS{
1, 0, 0, 1, 1, 2, 3, 0, kTrackLength / 2, kTrackWidth / 2};
static const ModuleConstants FR_CONSTANTS{
inline const ModuleConstants FR_CONSTANTS{
2, 2, 4, 5, 3, 6, 7, 0, kTrackLength / 2, -kTrackWidth / 2};
static const ModuleConstants BL_CONSTANTS{
inline const ModuleConstants BL_CONSTANTS{
3, 4, 8, 9, 5, 10, 11, 0, -kTrackLength / 2, kTrackWidth / 2};
static const ModuleConstants BR_CONSTANTS{
inline const ModuleConstants BR_CONSTANTS{
4, 6, 12, 13, 7, 14, 15, 0, -kTrackLength / 2, -kTrackWidth / 2};
} // namespace Swerve
} // namespace constants

View File

@@ -61,7 +61,7 @@ class Vision {
cameraSim = std::make_shared<photon::PhotonCameraSim>(camera.get(),
*cameraProp.get());
visionSim->AddCamera(cameraSim.get(), robotToCam);
visionSim->AddCamera(cameraSim.get(), constants::Vision::kRobotToCam);
cameraSim->EnableDrawWireframe(true);
}
}
@@ -138,12 +138,10 @@ class Vision {
frc::Field2d& GetSimDebugField() { return visionSim->GetDebugField(); }
private:
frc::Transform3d robotToCam{frc::Translation3d{0.5_m, 0.5_m, 0.5_m},
frc::Rotation3d{}};
photon::PhotonPoseEstimator photonEstimator{
LoadAprilTagLayoutField(frc::AprilTagField::k2023ChargedUp),
constants::Vision::kTagLayout,
photon::PoseStrategy::MULTI_TAG_PNP_ON_COPROCESSOR,
photon::PhotonCamera{"photonvision"}, robotToCam};
photon::PhotonCamera{"photonvision"}, constants::Vision::kRobotToCam};
std::shared_ptr<photon::PhotonCamera> camera{photonEstimator.GetCamera()};
std::unique_ptr<photon::VisionSystemSim> visionSim;
std::unique_ptr<photon::SimCameraProperties> cameraProp;

View File

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

View File

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

View File

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

View File

@@ -1,6 +1,5 @@
plugins {
id "com.diffplug.spotless" version "6.1.2"
id "edu.wpi.first.GradleRIO" version "2024.1.1" apply false
}
apply from: "examples.gradle"
@@ -9,7 +8,7 @@ allprojects {
repositories {
mavenCentral()
mavenLocal()
maven { url = "https://maven.photonvision.org/repository/internal/" }
maven { url = "https://maven.photonvision.org/releases" }
}
}

View File

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

View File

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

View File

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

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