Compare commits

...

42 Commits

Author SHA1 Message Date
Matt M
e613e75db6 Backport maven changes 2024-08-02 08:18:44 -07: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
Programmers3539
628cead2dc Add LL3 image from photon-image-modifier (#1166)
* LL3

* Update build.yml
2024-01-15 22:50:44 -05:00
Mohammad Durrani
7b67f6bebf Add RKNN / Object Detection Pipeline (#1144)
Tested on Orange Pi 5 and Cool Pi 4B. Merge with parts of the OpenCV DNN PR. 

Adds support for YOLOv5s models for Rockchip CPUs with a NPU. Right now hard coded to a note model from alex_idk. Very much still incubating and largely untested.
2024-01-15 22:28:34 -05:00
Matt
e1f550a751 Load libquadmath on Windows (#1163)
Nobody reads these, right? This probably won't make things worse. Surely.
2024-01-15 18:44:58 -05:00
Ryan Blue
a40e4049d4 Update spotless to fix exception spam (#1162) 2024-01-15 15:44:43 -05:00
Matt
152888f216 Bind-mount repo in image builder (#1157)
Reduces built image size by not accidentally copying source in
2024-01-14 13:31:12 -05:00
ArchB1W
b729d9e917 [photon-lib java] Implement ProtobufSerializable (#1156)
* [photon-lib java] Fix classes with protobuf support not "announcing it"

Since they didn't implement `ProtobufSerializable` this meant that most other software didn't even know protobufs were even implemented.
In AdvantageKit for example this would cause it to not work it all and crash.

* Run `spotlessJavaApply`
2024-01-13 22:35:57 -05:00
Thad House
6917ec8401 Fix vendordep including all wpilib libraries (#1155)
* Fix vendordep including all wpilib libraries. Without this fix, users were consuming a massively oversized .jar
2024-01-13 18:57:56 -06:00
Judson James
a8aa32fab5 Fix build.yml (#1153) 2024-01-12 21:39:48 -08:00
Sriman Achanta
e40761aaba Publish photonlib json to releases (#1141) 2024-01-12 23:55:52 -05:00
Judson James
354dd15620 Rewrite ARM builds to use arm-runner-action to resolve OrangePi5 images (#1143) 2024-01-12 20:52:39 -08:00
Matt
07b299a076 Update vendor JSON url (#1150) 2024-01-12 10:07:19 -05:00
Matt
0cec1eef9f [python] Only add maturity/suffix to version if groups matched (#1146)
* Only add maturity/suffix if groups matched

---------

Co-authored-by: Chris Gerth <gerth2@users.noreply.github.com>
2024-01-10 23:23:40 -06:00
Matt
68d8a943f7 Add 2024 test mode pictures (#1136) 2024-01-08 14:09:15 -05:00
Drew Williams
9f0aebe4ce Updates workflow to publish photon-targeting for docker hosts (#1138) 2024-01-08 14:08:48 -05:00
Matt
6444ae884d Restart server on general settings change (#1137) 2024-01-08 13:02:31 -05:00
Matt
02df8aa925 Bound check sliders in web UI (#1134)
* Bound check sliders

* Update pv-range-slider.vue

---------

Co-authored-by: Sriman Achanta <68172138+srimanachanta@users.noreply.github.com>
2024-01-08 09:27:54 -05:00
Sriman Achanta
4d458198c1 Fix bug with saving general settings not using tempSettingsStruct and using store values instead (#1131)
residual from #1075
closes #1129
2024-01-08 08:32:56 -05:00
Craig Schardt
5cbb507c87 Fix OV9281 typo in UI (#1130) 2024-01-07 21:15:35 -05:00
Matt
e71ce899d6 Add mrcal packges to install script (#1128)
Closes #1124

Also bumps opencv to 4.6 to match our libcamera driver
2024-01-07 20:18:48 -05:00
Matt
60220f38e6 Add cameralensmodel enum (#1126)
Preparing for future lens models like splined stereo
2024-01-06 23:17:55 -07:00
Matt
bf5e8dc81b Bump wpilib to 2024.1.1 (#1127)
Does not yet include test mode
2024-01-07 00:44:28 -05:00
Craig Schardt
b8a6a5d56a Install NetworkManager on Ubuntu distributions (fixes #1052) (#1070)
Add the following args to the install script:

Syntax: sudo ./install.sh [-h|m|n|q]
  options:
  -h        Display this help message.
  -m        Install and configure NetworkManager (Ubuntu only).
  -n        Disable networking. This will also prevent installation of NetworkManager.
  -q        Silent install, automatically accepts all defaults. For non-interactive use.
2024-01-06 09:45:56 -05:00
Sriman Achanta
bf156f544e Update HTTP based settings when new fullsettings are emited (#1122)
Previously, if someone were changing network or camera settings while the backend sent an update request, the frontend wouldn't update the UI until the HTTP request was sent, likely leading to an error or confusion, now, values will be reset whenever new settings are sent. It also checks that settings were changed before allowing the user to click the save button.
2024-01-06 09:43:29 -05:00
Chris Gerth
851f2e4e68 Update Python rawBytes parsing (#1119)
*  data updates to capture multiple rawBytes packets associated with serde updates from late this past month

---------

Co-authored-by: Matt <matthew.morley.ca@gmail.com>
2024-01-06 06:17:06 -06:00
Matt
4068025572 Check all new prop names not just exposure time (#1080)
Fixes v4l renaming prop names and OV9281 exposure min/max being wrong by introducing new UI control
2024-01-05 23:40:06 -05:00
Craig Schardt
f37a0d0300 Add database migrations (fixes #1046) (#1065)
Add database version pragma to SQL database to automatically migrate between versions
2024-01-05 21:02:47 -05:00
156 changed files with 3255 additions and 1350 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
@@ -184,149 +75,7 @@ jobs:
- name: Publish
run: |
chmod +x gradlew
./gradlew photon-lib:publish
./gradlew photon-lib:publish photon-targeting:publish
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
- 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
- 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
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 }}
# TODO- replace with the arm-runner action and run this inside of the chroot. but this works for now.
- name: Generate image
run: |
chmod +x scripts/generatePiImage.sh
./scripts/generatePiImage.sh ${{ matrix.image_url }} ${{ matrix.image_suffix }}
- 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
if: github.event_name == 'push'
# Upload all jars and xz archives
- uses: softprops/action-gh-release@v1
with:
files: |
**/*.xz
**/*.jar
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

@@ -18,6 +18,7 @@ modifiableFileExclude {
\.dll$
\.webp$
\.ico$
\.rknn$
gradlew
}

View File

@@ -1,8 +1,10 @@
import edu.wpi.first.toolchain.*
plugins {
id "com.diffplug.spotless" version "6.22.0"
id "com.diffplug.spotless" version "6.24.0"
id "edu.wpi.first.NativeUtils" version "2024.6.1" apply false
id "edu.wpi.first.wpilib.repositories.WPILibRepositoriesPlugin" version "2020.2"
id "edu.wpi.first.GradleRIO" version "2024.1.1-beta-4"
id "edu.wpi.first.GradleRIO" version "2024.2.1"
id 'edu.wpi.first.WpilibTools' version '1.3.0'
id 'com.google.protobuf' version '0.9.4' apply false
}
@@ -11,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)
@@ -22,15 +25,17 @@ allprojects {
apply from: "versioningHelper.gradle"
ext {
wpilibVersion = "2024.1.1-beta-4-35-g141241d"
wpilibVersion = "2024.2.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"
frcYear = "2024"
mrcalVersion = "dev-v2024.0.0-7-gc976aaa";
pubVersion = versionString
isDev = pubVersion.startsWith("dev")
@@ -46,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 {
@@ -96,3 +105,7 @@ spotless {
wrapper {
gradleVersion '8.4'
}
ext.getCurrentArch = {
return NativePlatforms.desktop
}

View File

@@ -1,4 +1,4 @@
<!DOCTYPE html>
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />

View File

@@ -31,7 +31,7 @@
"eslint": "^8.56.0",
"eslint-plugin-vue": "^9.19.2",
"npm-run-all": "^4.1.5",
"prettier": "^3.1.1",
"prettier": "3.2.2",
"sass": "~1.32",
"sass-loader": "^13.3.2",
"terser": "^5.14.2",
@@ -3917,9 +3917,9 @@
}
},
"node_modules/prettier": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.1.1.tgz",
"integrity": "sha512-22UbSzg8luF4UuZtzgiUOfcGM8s4tjBv6dJRT7j275NXsy2jb4aJa4NNveul5x4eqlF1wuhuR2RElK71RvmVaw==",
"version": "3.2.2",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.2.2.tgz",
"integrity": "sha512-HTByuKZzw7utPiDO523Tt2pLtEyK7OibUD9suEJQrPUCYQqrHr74GGX6VidMrovbf/I50mPqr8j/II6oBAuc5A==",
"dev": true,
"bin": {
"prettier": "bin/prettier.cjs"

View File

@@ -27,17 +27,17 @@
},
"devDependencies": {
"@rushstack/eslint-patch": "^1.3.2",
"@vue/eslint-config-prettier": "^9.0.0",
"@vue/eslint-config-typescript": "^12.0.0",
"prettier": "^3.1.1",
"@types/node": "^16.11.45",
"@types/three": "^0.160.0",
"@vitejs/plugin-vue2": "^2.3.1",
"@vue/eslint-config-prettier": "^9.0.0",
"@vue/eslint-config-typescript": "^12.0.0",
"@vue/tsconfig": "^0.5.1",
"deepmerge": "^4.3.1",
"eslint": "^8.56.0",
"eslint-plugin-vue": "^9.19.2",
"npm-run-all": "^4.1.5",
"prettier": "3.2.2",
"sass": "~1.32",
"sass-loader": "^13.3.2",
"terser": "^5.14.2",

View File

@@ -311,7 +311,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 +319,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

@@ -3,22 +3,79 @@ import PvSelect from "@/components/common/pv-select.vue";
import PvNumberInput from "@/components/common/pv-number-input.vue";
import { useCameraSettingsStore } from "@/stores/settings/CameraSettingsStore";
import { useStateStore } from "@/stores/StateStore";
import { ref, watchEffect } from "vue";
import { computed, ref, watchEffect } from "vue";
import { type CameraSettingsChangeRequest, ValidQuirks } from "@/types/SettingTypes";
const currentFov = ref();
const tempSettingsStruct = ref<CameraSettingsChangeRequest>({
fov: useCameraSettingsStore().currentCameraSettings.fov.value,
quirksToChange: Object.assign({}, useCameraSettingsStore().currentCameraSettings.cameraQuirks.quirks)
});
const arducamSelectWrapper = computed<number>({
get: () => {
if (tempSettingsStruct.value.quirksToChange.ArduOV9281) return 1;
else if (tempSettingsStruct.value.quirksToChange.ArduOV2311) return 2;
else return 0;
},
set: (v) => {
switch (v) {
case 1:
tempSettingsStruct.value.quirksToChange.ArduOV9281 = true;
tempSettingsStruct.value.quirksToChange.ArduOV2311 = false;
break;
case 2:
tempSettingsStruct.value.quirksToChange.ArduOV9281 = false;
tempSettingsStruct.value.quirksToChange.ArduOV2311 = true;
break;
default:
tempSettingsStruct.value.quirksToChange.ArduOV9281 = false;
tempSettingsStruct.value.quirksToChange.ArduOV2311 = false;
break;
}
}
});
const currentCameraIsArducam = computed<boolean>(
() => useCameraSettingsStore().currentCameraSettings.cameraQuirks.quirks.ArduCamCamera
);
const settingsHaveChanged = (): boolean => {
const a = tempSettingsStruct.value;
const b = useCameraSettingsStore().currentCameraSettings;
for (const q in ValidQuirks) {
if (a.quirksToChange[q] != b.cameraQuirks.quirks[q]) return true;
}
return a.fov != b.fov.value;
};
const resetTempSettingsStruct = () => {
tempSettingsStruct.value.fov = useCameraSettingsStore().currentCameraSettings.fov.value;
tempSettingsStruct.value.quirksToChange = Object.assign(
{},
useCameraSettingsStore().currentCameraSettings.cameraQuirks.quirks
);
};
const saveCameraSettings = () => {
useCameraSettingsStore()
.updateCameraSettings({ fov: currentFov.value }, false)
.updateCameraSettings(tempSettingsStruct.value)
.then((response) => {
useCameraSettingsStore().currentCameraSettings.fov.value = currentFov.value;
useStateStore().showSnackbarMessage({
color: "success",
message: response.data.text || response.data
});
// Update the local settings cause the backend checked their validity. Assign is to deref value
useCameraSettingsStore().currentCameraSettings.fov.value = tempSettingsStruct.value.fov;
useCameraSettingsStore().currentCameraSettings.cameraQuirks.quirks = Object.assign(
{},
tempSettingsStruct.value.quirksToChange
);
})
.catch((error) => {
currentFov.value = useCameraSettingsStore().currentCameraSettings.fov.value;
resetTempSettingsStruct();
if (error.response) {
useStateStore().showSnackbarMessage({
color: "error",
@@ -39,7 +96,8 @@ const saveCameraSettings = () => {
};
watchEffect(() => {
currentFov.value = useCameraSettingsStore().currentCameraSettings.fov.value;
// Reset temp settings on remote camera settings change
resetTempSettingsStruct();
});
</script>
@@ -52,15 +110,9 @@ watchEffect(() => {
label="Camera"
:items="useCameraSettingsStore().cameraNames"
:select-cols="8"
@input="
(args) => {
currentFov = useCameraSettingsStore().cameras[args].fov.value;
useCameraSettingsStore().setCurrentCameraIndex(args);
}
"
/>
<pv-number-input
v-model="currentFov"
v-model="tempSettingsStruct.fov"
:tooltip="
!useCameraSettingsStore().currentCameraSettings.fov.managedByVendor
? 'Field of view (in degrees) of the camera measured across the diagonal of the frame, in a video mode which covers the whole sensor area.'
@@ -70,12 +122,24 @@ watchEffect(() => {
:disabled="useCameraSettingsStore().currentCameraSettings.fov.managedByVendor"
:label-cols="4"
/>
<pv-select
v-show="currentCameraIsArducam"
v-model="arducamSelectWrapper"
label="Arducam Model"
:items="[
{ name: 'None', value: 0, disabled: true },
{ name: 'OV9281', value: 1 },
{ name: 'OV2311', value: 2 }
]"
:select-cols="8"
/>
<br />
<v-btn
style="margin-top: 10px"
class="mt-2 mb-3"
style="width: 100%"
small
color="secondary"
:disabled="currentFov === useCameraSettingsStore().currentCameraSettings.fov.value"
:disabled="!settingsHaveChanged()"
@click="saveCameraSettings"
>
<v-icon left> mdi-content-save </v-icon>

View File

@@ -40,12 +40,21 @@ const localValue = computed<[number, number]>({
}
});
const changeFromSlot = (v: number, i: number) => {
const changeFromSlot = (v: string, i: number) => {
// v comes in as a string, not a number, for some reason
// if v is undefined, take a guess and set it to 0
const val = Math.max(props.min, Math.min(parseFloat(v) || 0, props.max));
// localValue.value must be replaced for a reactive change to take place
const temp = localValue.value;
temp[i] = v;
temp[i] = val;
localValue.value = temp;
};
const checkNumberRange = (v: string): boolean => {
const val: number = parseFloat(v);
return isFinite(val) && val >= props.min && val <= props.max;
};
</script>
<template>
@@ -79,6 +88,7 @@ const changeFromSlot = (v: number, i: number) => {
:max="max"
:min="min"
:step="step"
:rules="[checkNumberRange]"
type="number"
style="width: 60px"
@input="(v) => changeFromSlot(v, 0)"
@@ -95,6 +105,7 @@ const changeFromSlot = (v: number, i: number) => {
:max="max"
:min="min"
:step="step"
:rules="[checkNumberRange]"
type="number"
style="width: 60px"
@input="(v) => changeFromSlot(v, 1)"

View File

@@ -7,6 +7,7 @@ import { computed, ref } from "vue";
import PvIcon from "@/components/common/pv-icon.vue";
import PvInput from "@/components/common/pv-input.vue";
import { PipelineType } from "@/types/PipelineTypes";
import { useSettingsStore } from "@/stores/settings/GeneralSettingsStore";
const changeCurrentCameraIndex = (index: number) => {
useCameraSettingsStore().setCurrentCameraIndex(index, true);
@@ -24,6 +25,9 @@ const changeCurrentCameraIndex = (index: number) => {
case PipelineType.Aruco:
pipelineType.value = WebsocketPipelineType.Aruco;
break;
case PipelineType.ObjectDetection:
pipelineType.value = WebsocketPipelineType.ObjectDetection;
break;
}
};
@@ -121,6 +125,18 @@ const cancelPipelineNameEdit = () => {
const showPipelineCreationDialog = ref(false);
const newPipelineName = ref("");
const newPipelineType = ref<WebsocketPipelineType>(useCameraSettingsStore().currentWebsocketPipelineType);
const validNewPipelineTypes = computed(() => {
const pipelineTypes = [
{ name: "Reflective", value: WebsocketPipelineType.Reflective },
{ name: "Colored Shape", value: WebsocketPipelineType.ColoredShape },
{ name: "AprilTag", value: WebsocketPipelineType.AprilTag },
{ name: "Aruco", value: WebsocketPipelineType.Aruco }
];
if (useSettingsStore().general.rknnSupported) {
pipelineTypes.push({ name: "Object Detection", value: WebsocketPipelineType.ObjectDetection });
}
return pipelineTypes;
});
const showCreatePipelineDialog = () => {
newPipelineName.value = "";
newPipelineType.value = useCameraSettingsStore().currentWebsocketPipelineType;
@@ -154,6 +170,9 @@ const pipelineTypesWrapper = computed<{ name: string; value: number }[]>(() => {
{ name: "AprilTag", value: WebsocketPipelineType.AprilTag },
{ name: "Aruco", value: WebsocketPipelineType.Aruco }
];
if (useSettingsStore().general.rknnSupported) {
pipelineTypes.push({ name: "Object Detection", value: WebsocketPipelineType.ObjectDetection });
}
if (useCameraSettingsStore().isDriverMode) {
pipelineTypes.push({ name: "Driver Mode", value: WebsocketPipelineType.DriverMode });
@@ -208,6 +227,9 @@ useCameraSettingsStore().$subscribe((mutation, state) => {
case PipelineType.Aruco:
pipelineType.value = WebsocketPipelineType.Aruco;
break;
case PipelineType.ObjectDetection:
pipelineType.value = WebsocketPipelineType.ObjectDetection;
break;
}
});
</script>
@@ -350,12 +372,7 @@ useCameraSettingsStore().$subscribe((mutation, state) => {
:select-cols="12 - 3"
label="Tracking Type"
tooltip="Pipeline type, which changes the type of processing that will happen on input frames"
:items="[
{ name: 'Reflective', value: WebsocketPipelineType.Reflective },
{ name: 'Colored Shape', value: WebsocketPipelineType.ColoredShape },
{ name: 'AprilTag', value: WebsocketPipelineType.AprilTag },
{ name: 'Aruco', value: WebsocketPipelineType.Aruco }
]"
:items="validNewPipelineTypes"
/>
</v-card-text>
<v-divider />

View File

@@ -8,6 +8,7 @@ import ThresholdTab from "@/components/dashboard/tabs/ThresholdTab.vue";
import ContoursTab from "@/components/dashboard/tabs/ContoursTab.vue";
import AprilTagTab from "@/components/dashboard/tabs/AprilTagTab.vue";
import ArucoTab from "@/components/dashboard/tabs/ArucoTab.vue";
import ObjectDetectionTab from "@/components/dashboard/tabs/ObjectDetectionTab.vue";
import OutputTab from "@/components/dashboard/tabs/OutputTab.vue";
import TargetsTab from "@/components/dashboard/tabs/TargetsTab.vue";
import PnPTab from "@/components/dashboard/tabs/PnPTab.vue";
@@ -40,6 +41,10 @@ const allTabs = Object.freeze({
tabName: "Aruco",
component: ArucoTab
},
objectDetectionTab: {
tabName: "Object Detection",
component: ObjectDetectionTab
},
outputTab: {
tabName: "Output",
component: OutputTab
@@ -75,6 +80,7 @@ const getTabGroups = (): ConfigOption[][] => {
allTabs.contoursTab,
allTabs.apriltagTab,
allTabs.arucoTab,
allTabs.objectDetectionTab,
allTabs.outputTab
],
[allTabs.targetsTab, allTabs.pnpTab, allTabs.map3dTab]
@@ -82,14 +88,21 @@ const getTabGroups = (): ConfigOption[][] => {
} else if (lgAndDown) {
return [
[allTabs.inputTab],
[allTabs.thresholdTab, allTabs.contoursTab, allTabs.apriltagTab, allTabs.arucoTab, allTabs.outputTab],
[
allTabs.thresholdTab,
allTabs.contoursTab,
allTabs.apriltagTab,
allTabs.arucoTab,
allTabs.objectDetectionTab,
allTabs.outputTab
],
[allTabs.targetsTab, allTabs.pnpTab, allTabs.map3dTab]
];
} else if (xl) {
return [
[allTabs.inputTab],
[allTabs.thresholdTab],
[allTabs.contoursTab, allTabs.apriltagTab, allTabs.arucoTab, allTabs.outputTab],
[allTabs.contoursTab, allTabs.apriltagTab, allTabs.arucoTab, allTabs.objectDetectionTab, allTabs.outputTab],
[allTabs.targetsTab, allTabs.pnpTab, allTabs.map3dTab]
];
}
@@ -103,17 +116,20 @@ const tabGroups = computed<ConfigOption[][]>(() => {
const allow3d = useCameraSettingsStore().currentPipelineSettings.solvePNPEnabled;
const isAprilTag = useCameraSettingsStore().currentWebsocketPipelineType === WebsocketPipelineType.AprilTag;
const isAruco = useCameraSettingsStore().currentWebsocketPipelineType === WebsocketPipelineType.Aruco;
const isObjectDetection =
useCameraSettingsStore().currentWebsocketPipelineType === WebsocketPipelineType.ObjectDetection;
return getTabGroups()
.map((tabGroup) =>
tabGroup.filter(
(tabConfig) =>
!(!allow3d && tabConfig.tabName === "3D") && //Filter out 3D tab any time 3D isn't calibrated
!((!allow3d || isAprilTag || isAruco) && tabConfig.tabName === "PnP") && //Filter out the PnP config tab if 3D isn't available, or we're doing AprilTags
!((isAprilTag || isAruco) && tabConfig.tabName === "Threshold") && //Filter out threshold tab if we're doing AprilTags
!((isAprilTag || isAruco) && tabConfig.tabName === "Contours") && //Filter out contours if we're doing AprilTags
!((!allow3d || isAprilTag || isAruco || isObjectDetection) && tabConfig.tabName === "PnP") && //Filter out the PnP config tab if 3D isn't available, or we're doing AprilTags
!((isAprilTag || isAruco || isObjectDetection) && tabConfig.tabName === "Threshold") && //Filter out threshold tab if we're doing AprilTags
!((isAprilTag || isAruco || isObjectDetection) && tabConfig.tabName === "Contours") && //Filter out contours if we're doing AprilTags
!(!isAprilTag && tabConfig.tabName === "AprilTag") && //Filter out apriltag unless we actually are doing AprilTags
!(!isAruco && tabConfig.tabName === "Aruco") //Filter out aruco unless we actually are doing Aruco
!(!isAruco && tabConfig.tabName === "Aruco") &&
!(!isObjectDetection && tabConfig.tabName === "Object Detection") //Filter out aruco unless we actually are doing Aruco
)
)
.filter((it) => it.length); // Remove empty tab groups

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

@@ -0,0 +1,83 @@
<script setup lang="ts">
import { useCameraSettingsStore } from "@/stores/settings/CameraSettingsStore";
import { type ActivePipelineSettings, PipelineType } from "@/types/PipelineTypes";
import PvSlider from "@/components/common/pv-slider.vue";
import { computed, getCurrentInstance } from "vue";
import { useStateStore } from "@/stores/StateStore";
// TODO fix pipeline typing in order to fix this, the store settings call should be able to infer that only valid pipeline type settings are exposed based on pre-checks for the entire config section
// Defer reference to store access method
const currentPipelineSettings = computed<ActivePipelineSettings>(
() => useCameraSettingsStore().currentPipelineSettings
);
// TODO fix pv-range-slider so that store access doesn't need to be deferred
const contourArea = computed<[number, number]>({
get: () => Object.values(useCameraSettingsStore().currentPipelineSettings.contourArea) as [number, number],
set: (v) => (useCameraSettingsStore().currentPipelineSettings.contourArea = v)
});
const contourRatio = computed<[number, number]>({
get: () => Object.values(useCameraSettingsStore().currentPipelineSettings.contourRatio) as [number, number],
set: (v) => (useCameraSettingsStore().currentPipelineSettings.contourRatio = v)
});
const interactiveCols = computed(() =>
(getCurrentInstance()?.proxy.$vuetify.breakpoint.mdAndDown || false) &&
(!useStateStore().sidebarFolded || useCameraSettingsStore().isDriverMode)
? 9
: 8
);
</script>
<template>
<div v-if="currentPipelineSettings.pipelineType === PipelineType.ObjectDetection">
<pv-slider
v-model="currentPipelineSettings.confidence"
class="pt-2"
:slider-cols="interactiveCols"
label="Confidence"
tooltip="The minimum confidence for a detection to be considered valid. Bigger numbers mean fewer but more probable detections are allowed through."
:min="0"
:max="1"
:step="0.01"
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ confidence: value }, false)"
/>
<pv-range-slider
v-model="contourArea"
label="Area"
:min="0"
:max="100"
:slider-cols="interactiveCols"
:step="0.01"
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ contourArea: value }, false)"
/>
<pv-range-slider
v-model="contourRatio"
label="Ratio (W/H)"
tooltip="Min and max ratio between the width and height of a contour's bounding rectangle"
:min="0"
:max="100"
:slider-cols="interactiveCols"
:step="0.01"
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ contourRatio: value }, false)"
/>
<pv-select
v-model="useCameraSettingsStore().currentPipelineSettings.contourTargetOrientation"
label="Target Orientation"
tooltip="Used to determine how to calculate target landmarks, as well as aspect ratio"
:items="['Portrait', 'Landscape']"
:select-cols="interactiveCols"
@input="
(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ contourTargetOrientation: value }, false)
"
/>
<pv-select
v-model="currentPipelineSettings.contourSortMode"
label="Target Sort"
tooltip="Chooses the sorting mode used to determine the 'best' targets to provide to user code"
:select-cols="interactiveCols"
:items="['Largest', 'Smallest', 'Highest', 'Lowest', 'Rightmost', 'Leftmost', 'Centermost']"
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ contourSortMode: value }, false)"
/>
</div>
</template>

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

@@ -48,6 +48,10 @@ const resetCurrentBuffer = () => {
>
Fiducial ID
</th>
<template v-if="currentPipelineSettings.pipelineType === PipelineType.ObjectDetection">
<th class="text-center white--text">Class</th>
<th class="text-center white--text">Confidence</th>
</template>
<template v-if="!useCameraSettingsStore().currentPipelineSettings.solvePNPEnabled">
<th class="text-center white--text">Pitch &theta;&deg;</th>
<th class="text-center white--text">Yaw &theta;&deg;</th>
@@ -85,6 +89,18 @@ const resetCurrentBuffer = () => {
>
{{ target.fiducialId }}
</td>
<td
v-if="currentPipelineSettings.pipelineType === PipelineType.ObjectDetection"
class="text-center white--text"
>
{{ useStateStore().currentPipelineResults?.classNames[target.classId] }}
</td>
<td
v-if="currentPipelineSettings.pipelineType === PipelineType.ObjectDetection"
class="text-center white--text"
>
{{ target.confidence.toFixed(2) }}
</td>
<template v-if="!useCameraSettingsStore().currentPipelineSettings.solvePNPEnabled">
<td class="text-center">{{ target.pitch.toFixed(2) }}&deg;</td>
<td class="text-center">{{ target.yaw.toFixed(2) }}&deg;</td>

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

@@ -1,16 +1,21 @@
<script setup lang="ts">
import { useSettingsStore } from "@/stores/settings/GeneralSettingsStore";
import { computed, ref } from "vue";
import { computed, ref, watchEffect } from "vue";
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";
const settingsValid = ref(true);
// 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);
};
const settingsValid = ref(true);
const isValidNetworkTablesIP = (v: string | undefined): boolean => {
// Check if it is a valid team number between 1-9999
const teamNumberRegex = /^[1-9][0-9]{0,3}$/;
@@ -52,7 +57,6 @@ 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
@@ -62,18 +66,36 @@ const settingsHaveChanged = (): boolean => {
const saveGeneralSettings = () => {
const changingStaticIp = useSettingsStore().network.connectionType === NetworkConnectionType.Static;
// Update with new values
Object.assign(useSettingsStore().network, tempSettingsStruct.value);
// replace undefined members with empty strings for backend
const payload = {
connectionType: tempSettingsStruct.value.connectionType,
hostname: tempSettingsStruct.value.hostname,
networkManagerIface: tempSettingsStruct.value.networkManagerIface || "",
ntServerAddress: tempSettingsStruct.value.ntServerAddress,
runNTServer: tempSettingsStruct.value.runNTServer,
setDHCPcommand: tempSettingsStruct.value.setDHCPcommand || "",
setStaticCommand: tempSettingsStruct.value.setStaticCommand || "",
shouldManage: tempSettingsStruct.value.shouldManage,
shouldPublishProto: tempSettingsStruct.value.shouldPublishProto,
staticIp: tempSettingsStruct.value.staticIp
};
useSettingsStore()
.saveGeneralSettings()
.updateGeneralSettings(payload)
.then((response) => {
useStateStore().showSnackbarMessage({
message: response.data.text || response.data,
color: "success"
});
// Update the local settings cause the backend checked their validity. Assign is to deref value
useSettingsStore().network = {
...useSettingsStore().network,
...Object.assign({}, tempSettingsStruct.value)
};
})
.catch((error) => {
resetTempSettingsStruct();
if (error.response) {
if (error.status === 504 || changingStaticIp) {
useStateStore().showSnackbarMessage({
@@ -106,6 +128,11 @@ const currentNetworkInterfaceIndex = computed<number>({
get: () => useSettingsStore().networkInterfaceNames.indexOf(useSettingsStore().network.networkManagerIface || ""),
set: (v) => (tempSettingsStruct.value.networkManagerIface = useSettingsStore().networkInterfaceNames[v])
});
watchEffect(() => {
// Reset temp settings on remote network settings change
resetTempSettingsStruct();
});
</script>
<template>
@@ -136,42 +163,63 @@ const currentNetworkInterfaceIndex = computed<number>({
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"
@@ -180,7 +228,8 @@ const currentNetworkInterfaceIndex = computed<number>({
v-show="
!useSettingsStore().networkInterfaceNames.length &&
tempSettingsStruct.shouldManage &&
tempSettingsStruct.canManage
useSettingsStore().network.canManage &&
!useSettingsStore().network.networkingDisabled
"
rounded
color="red"

View File

@@ -3,7 +3,7 @@ import type {
CalibrationBoardTypes,
CameraCalibrationResult,
CameraSettings,
ConfigurableCameraSettings,
CameraSettingsChangeRequest,
Resolution,
RobotOffsetType,
VideoFormat
@@ -103,21 +103,17 @@ export const useCameraSettingsStore = defineStore("cameraSettings", {
isCSICamera: d.isCSICamera,
pipelineNicknames: d.pipelineNicknames,
currentPipelineIndex: d.currentPipelineIndex,
pipelineSettings: d.currentPipelineSettings
pipelineSettings: d.currentPipelineSettings,
cameraQuirks: d.cameraQuirks
}));
},
/**
* Update the configurable camera settings.
*
* @param data camera settings to save.
* @param updateStore whether or not to update the store. This is useful if the input field already models the store reference.
* @param cameraIndex the index of the camera.
*/
updateCameraSettings(
data: ConfigurableCameraSettings,
updateStore = true,
cameraIndex: number = useStateStore().currentCameraIndex
) {
updateCameraSettings(data: CameraSettingsChangeRequest, cameraIndex: number = useStateStore().currentCameraIndex) {
// The camera settings endpoint doesn't actually require all data, instead, it needs key data such as the FOV
const payload = {
settings: {
@@ -125,9 +121,6 @@ export const useCameraSettingsStore = defineStore("cameraSettings", {
},
index: cameraIndex
};
if (updateStore) {
this.currentCameraSettings.fov.value = data.fov;
}
return axios.post("/settings/camera", payload);
},
/**

View File

@@ -27,7 +27,8 @@ export const useSettingsStore = defineStore("settings", {
gpuAcceleration: undefined,
hardwareModel: undefined,
hardwarePlatform: undefined,
mrCalWorking: true
mrCalWorking: true,
rknnSupported: false
},
network: {
ntServerAddress: "",
@@ -99,25 +100,14 @@ export const useSettingsStore = defineStore("settings", {
hardwareModel: data.general.hardwareModel || undefined,
hardwarePlatform: data.general.hardwarePlatform || undefined,
gpuAcceleration: data.general.gpuAcceleration || undefined,
mrCalWorking: data.general.mrCalWorking
mrCalWorking: data.general.mrCalWorking,
rknnSupported: data.general.rknnSupported
};
this.lighting = data.lighting;
this.network = data.networkSettings;
this.currentFieldLayout = data.atfl;
},
saveGeneralSettings() {
const payload: Required<ConfigurableNetworkSettings> = {
connectionType: this.network.connectionType,
hostname: this.network.hostname,
networkManagerIface: this.network.networkManagerIface || "",
ntServerAddress: this.network.ntServerAddress,
runNTServer: this.network.runNTServer,
setDHCPcommand: this.network.setDHCPcommand || "",
setStaticCommand: this.network.setStaticCommand || "",
shouldManage: this.network.shouldManage,
shouldPublishProto: this.network.shouldPublishProto,
staticIp: this.network.staticIp
};
updateGeneralSettings(payload: Required<ConfigurableNetworkSettings>) {
return axios.post("/settings/general", payload);
},
/**

View File

@@ -54,6 +54,8 @@ export interface PhotonTarget {
ambiguity: number;
// -1 if not set
fiducialId: number;
confidence: number;
classId: number;
// undefined if 3d isn't enabled
pose?: Transform3d;
}
@@ -70,4 +72,6 @@ export interface PipelineResult {
targets: PhotonTarget[];
// undefined if multitag failed or non-tag pipeline
multitagResult?: MultitagResult;
// Object detection class names -- empty if not doing object detection
classNames: string[];
}

View File

@@ -5,7 +5,8 @@ export enum PipelineType {
Reflective = 2,
ColoredShape = 3,
AprilTag = 4,
Aruco = 5
Aruco = 5,
ObjectDetection = 6
}
export enum AprilTagFamily {
@@ -281,14 +282,39 @@ export const DefaultArucoPipelineSettings: ArucoPipelineSettings = {
doSingleTargetAlways: false
};
export interface ObjectDetectionPipelineSettings extends PipelineSettings {
pipelineType: PipelineType.ObjectDetection;
confidence: number;
nms: number;
box_thresh: number;
}
export type ConfigurableObjectDetectionPipelineSettings = Partial<
Omit<ObjectDetectionPipelineSettings, "pipelineType">
> &
ConfigurablePipelineSettings;
export const DefaultObjectDetectionPipelineSettings: ObjectDetectionPipelineSettings = {
...DefaultPipelineSettings,
pipelineType: PipelineType.ObjectDetection,
cameraGain: 20,
targetModel: TargetModel.InfiniteRechargeHighGoalOuter,
ledMode: true,
outputShowMultipleTargets: false,
cameraExposure: 6,
confidence: 0.9,
nms: 0.45,
box_thresh: 0.25
};
export type ActivePipelineSettings =
| ReflectivePipelineSettings
| ColoredShapePipelineSettings
| AprilTagPipelineSettings
| ArucoPipelineSettings;
| ArucoPipelineSettings
| ObjectDetectionPipelineSettings;
export type ActiveConfigurablePipelineSettings =
| ConfigurableReflectivePipelineSettings
| ConfigurableColoredShapePipelineSettings
| ConfigurableAprilTagPipelineSettings
| ConfigurableArucoPipelineSettings;
| ConfigurableArucoPipelineSettings
| ConfigurableObjectDetectionPipelineSettings;

View File

@@ -7,6 +7,7 @@ export interface GeneralSettings {
hardwareModel?: string;
hardwarePlatform?: string;
mrCalWorking: boolean;
rknnSupported: boolean;
}
export interface MetricData {
@@ -44,9 +45,13 @@ export interface NetworkSettings {
setStaticCommand?: string;
setDHCPcommand?: string;
networkInterfaceNames: NetworkInterfaceType[];
networkingDisabled: boolean;
}
export type ConfigurableNetworkSettings = Omit<NetworkSettings, "canManage" | "networkInterfaceNames">;
export type ConfigurableNetworkSettings = Omit<
NetworkSettings,
"canManage" | "networkInterfaceNames" | "networkingDisabled"
>;
export interface LightingSettings {
supported: boolean;
@@ -135,8 +140,25 @@ export interface CameraCalibrationResult {
calobjectWarp?: number[];
}
export interface ConfigurableCameraSettings {
fov: number;
export enum ValidQuirks {
AWBGain = "AWBGain",
AdjustableFocus = "AdjustableFocus",
ArduOV9281 = "ArduOV9281",
ArduOV2311 = "ArduOV2311",
ArduCamCamera = "ArduCamCamera",
CompletelyBroken = "CompletelyBroken",
FPSCap100 = "FPSCap100",
Gain = "Gain",
PiCam = "PiCam",
StickyFPS = "StickyFPS"
}
export interface QuirkyCamera {
baseName: string;
usbVid: number;
usbPid: number;
displayName: string;
quirks: Record<ValidQuirks, boolean>;
}
export interface CameraSettings {
@@ -159,15 +181,22 @@ export interface CameraSettings {
currentPipelineIndex: number;
pipelineNicknames: string[];
pipelineSettings: ActivePipelineSettings;
cameraQuirks: QuirkyCamera;
isCSICamera: boolean;
}
export interface CameraSettingsChangeRequest {
fov: number;
quirksToChange: Record<ValidQuirks, boolean>;
}
export const PlaceholderCameraSettings: CameraSettings = {
nickname: "Placeholder Camera",
uniqueName: "Placeholder Name",
fov: {
value: 70,
managedByVendor: true
managedByVendor: false
},
stream: {
inputPort: 0,
@@ -233,6 +262,24 @@ export const PlaceholderCameraSettings: CameraSettings = {
lastPipelineIndex: 0,
currentPipelineIndex: 0,
pipelineSettings: DefaultAprilTagPipelineSettings,
cameraQuirks: {
displayName: "Blank 1",
baseName: "Blank 2",
usbVid: -1,
usbPid: -1,
quirks: {
AWBGain: false,
AdjustableFocus: false,
ArduOV9281: false,
ArduOV2311: false,
ArduCamCamera: false,
CompletelyBroken: false,
FPSCap100: false,
Gain: false,
PiCam: false,
StickyFPS: false
}
},
isCSICamera: false
};

View File

@@ -4,7 +4,8 @@ import type {
LightingSettings,
LogLevel,
MetricData,
NetworkSettings
NetworkSettings,
QuirkyCamera
} from "@/types/SettingTypes";
import type { ActivePipelineSettings } from "@/types/PipelineTypes";
import type { AprilTagFieldLayout, PipelineResult } from "@/types/PhotonTrackingTypes";
@@ -56,6 +57,7 @@ export interface WebsocketCameraSettingsUpdate {
outputStreamPort: number;
pipelineNicknames: string[];
videoFormatList: WebsocketVideoFormat;
cameraQuirks: QuirkyCamera;
}
export interface WebsocketNTUpdate {
connected: boolean;
@@ -99,5 +101,6 @@ export enum WebsocketPipelineType {
Reflective = 0,
ColoredShape = 1,
AprilTag = 2,
Aruco = 3
Aruco = 3,
ObjectDetection = 4
}

View File

@@ -10,25 +10,22 @@ export default defineConfig({
plugins: [
Vue2(),
Components({
resolvers: [
VuetifyResolver()
],
resolvers: [VuetifyResolver()],
dts: true,
transformer: "vue2",
types: [{
from: "vue-router",
names: ["RouterLink", "RouterView"]
}],
types: [
{
from: "vue-router",
names: ["RouterLink", "RouterView"]
}
],
version: 2.7
})
],
css: {
preprocessorOptions: {
sass: {
additionalData: [
"@import \"@/assets/styles/variables.scss\"",
""
].join("\n")
additionalData: ["@import \"@/assets/styles/variables.scss\"", ""].join("\n")
}
}
},

View File

@@ -1,7 +1,31 @@
plugins {
id 'edu.wpi.first.WpilibTools' version '1.3.0'
}
import java.nio.file.Path
apply from: "${rootDir}/shared/common.gradle"
wpilibTools.deps.wpilibVersion = wpi.versions.wpilibVersion.get()
def nativeConfigName = 'wpilibNatives'
def nativeConfig = configurations.create(nativeConfigName)
def nativeTasks = wpilibTools.createExtractionTasks {
configurationName = nativeConfigName
}
nativeTasks.addToSourceSetResources(sourceSets.main)
nativeConfig.dependencies.add wpilibTools.deps.wpilib("wpimath")
nativeConfig.dependencies.add wpilibTools.deps.wpilib("wpinet")
nativeConfig.dependencies.add wpilibTools.deps.wpilib("wpiutil")
nativeConfig.dependencies.add wpilibTools.deps.wpilib("ntcore")
nativeConfig.dependencies.add wpilibTools.deps.wpilib("cscore")
nativeConfig.dependencies.add wpilibTools.deps.wpilib("apriltag")
nativeConfig.dependencies.add wpilibTools.deps.wpilib("hal")
nativeConfig.dependencies.add wpilibTools.deps.wpilibOpenCv("frc" + wpi.frcYear.get(), wpi.versions.opencvVersion.get())
dependencies {
// JOGL stuff (currently we only distribute for aarch64, which is Pi 4)
implementation "org.jogamp.gluegen:gluegen-rt:$joglVersion"
@@ -13,7 +37,9 @@ 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:photon-libcamera-gl-driver-jni:$photonGlDriverLibVersion:linuxarm64"
implementation "org.photonvision:photon-libcamera-gl-driver-java:$photonGlDriverLibVersion"

View File

@@ -27,6 +27,7 @@ import org.photonvision.common.logging.LogGroup;
import org.photonvision.common.logging.Logger;
import org.photonvision.vision.calibration.CameraCalibrationCoefficients;
import org.photonvision.vision.camera.CameraType;
import org.photonvision.vision.camera.QuirkyCamera;
import org.photonvision.vision.pipeline.CVPipelineSettings;
import org.photonvision.vision.pipeline.DriverModePipelineSettings;
import org.photonvision.vision.processes.PipelineManager;
@@ -46,6 +47,8 @@ public class CameraConfiguration {
/** Can be either path (ex /dev/videoX) or index (ex 1). */
public String path = "";
public QuirkyCamera cameraQuirks;
@JsonIgnore public String[] otherPaths = {};
public CameraType cameraType = CameraType.UsbCamera;
@@ -93,6 +96,7 @@ public class CameraConfiguration {
@JsonProperty("FOV") double FOV,
@JsonProperty("path") String path,
@JsonProperty("cameraType") CameraType cameraType,
@JsonProperty("cameraQuirks") QuirkyCamera cameraQuirks,
@JsonProperty("calibration") List<CameraCalibrationCoefficients> calibrations,
@JsonProperty("currentPipelineIndex") int currentPipelineIndex) {
this.baseName = baseName;
@@ -101,6 +105,7 @@ public class CameraConfiguration {
this.FOV = FOV;
this.path = path;
this.cameraType = cameraType;
this.cameraQuirks = cameraQuirks;
this.calibrations = calibrations != null ? calibrations : new ArrayList<>();
this.currentPipelineIndex = currentPipelineIndex;
@@ -165,6 +170,8 @@ public class CameraConfiguration {
+ Arrays.toString(otherPaths)
+ ", cameraType="
+ cameraType
+ ", cameraQuirks="
+ cameraQuirks
+ ", FOV="
+ FOV
+ ", calibrations="

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,
@@ -296,4 +300,26 @@ public class ConfigManager {
}
}
}
/** Get (and create if not present) the subfolder where ML models are stored */
public File getModelsDirectory() {
var ret = new File(configDirectoryFile, "models");
if (!ret.exists()) ret.mkdirs();
return ret;
}
/**
* Disable flushing settings to disk as part of our JVM exit hook. Used to prevent uploading all
* settings from getting its new configs overwritten at program exit and before theyre all loaded.
*/
public void disableFlushOnShutdown() {
this.flushOnShutdown = false;
}
public void onJvmExit() {
if (flushOnShutdown) {
logger.info("Force-flushing settings...");
saveToDisk();
}
}
}

View File

@@ -0,0 +1,64 @@
/*
* Copyright (C) Photon Vision.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.photonvision.common.configuration;
/**
* Add migrations by adding the SQL commands for each migration sequentially to this array. DO NOT
* edit or delete existing SQL commands. That will lead to producing an icompatible database.
*
* <p>You can use multiple SQL statements in one migration step as long as you separate them with a
* semicolon (;).
*/
public final class DatabaseSchema {
public static final String[] migrations = {
// #1 - initial schema
"CREATE TABLE IF NOT EXISTS global (\n"
+ " filename TINYTEXT PRIMARY KEY,\n"
+ " contents mediumtext NOT NULL\n"
+ ");"
+ "CREATE TABLE IF NOT EXISTS cameras (\n"
+ " unique_name TINYTEXT PRIMARY KEY,\n"
+ " config_json text NOT NULL,\n"
+ " drivermode_json text NOT NULL,\n"
+ " pipeline_jsons mediumtext NOT NULL\n"
+ ");",
// #2 - add column otherpaths_json
"ALTER TABLE cameras ADD COLUMN otherpaths_json TEXT NOT NULL DEFAULT '[]';",
// add future migrations here
};
// Constants for the tables and column to help prevent typos in SQL queries
// Update these tables to keep them constant with the current schema
public final class Tables {
// These constants should match the current SQL name of each table
public static final String GLOBAL = "global";
public static final String CAMERAS = "cameras";
}
public final class Columns {
// These constants should match the current SQL name of each column
static final String GLB_FILENAME = "filename";
static final String GLB_CONTENTS = "contents";
static final String CAM_UNIQUE_NAME = "unique_name";
static final String CAM_CONFIG_JSON = "config_json";
static final String CAM_DRIVERMODE_JSON = "drivermode_json";
static final String CAM_PIPELINE_JSONS = "pipeline_jsons";
static final String CAM_OTHERPATHS_JSON = "otherpaths_json";
}
}

View File

@@ -196,7 +196,7 @@ class LegacyConfigProvider extends ConfigProvider {
}
}
if (atfl == null) {
logger.info("Loading default apriltags for 2023 field...");
logger.info("Loading default apriltags for 2024 field...");
try {
atfl = AprilTagFields.kDefaultField.loadAprilTagLayoutField();
} catch (UncheckedIOException e) {

View File

@@ -0,0 +1,98 @@
/*
* Copyright (C) Photon Vision.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.photonvision.common.configuration;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.List;
import org.photonvision.common.logging.LogGroup;
import org.photonvision.common.logging.Logger;
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 File defaultModelFile;
private List<String> labels;
public static NeuralNetworkModelManager getInstance() {
if (INSTANCE == null) {
INSTANCE = new NeuralNetworkModelManager();
}
return INSTANCE;
}
/**
* Perform initial setup and extract default model from JAR to the filesystem
*
* @param modelsFolder Where models live
*/
public void initialize(File modelsFolder) {
var modelResourcePath = "/models/" + MODEL_NAME;
this.defaultModelFile = new File(modelsFolder, MODEL_NAME);
extractResource(modelResourcePath, defaultModelFile);
File labelsFile = new File(modelsFolder, "labels.txt");
var labelResourcePath = "/models/" + labelsFile.getName();
extractResource(labelResourcePath, labelsFile);
try {
labels = Files.readAllLines(Paths.get(labelsFile.getPath()));
} catch (IOException e) {
logger.error("Error reading labels.txt", e);
}
}
private void extractResource(String resourcePath, File outputFile) {
try (var in = NeuralNetworkModelManager.class.getResourceAsStream(resourcePath)) {
if (in == null) {
logger.error("Failed to find jar resource at " + resourcePath);
return;
}
if (!outputFile.exists()) {
try (FileOutputStream fos = new FileOutputStream(outputFile)) {
int read = -1;
byte[] buffer = new byte[1024];
while ((read = in.read(buffer)) != -1) {
fos.write(buffer, 0, read);
}
} catch (IOException e) {
logger.error("Error extracting resource to " + outputFile.toPath().toString(), e);
}
} else {
logger.info(
"File " + outputFile.toPath().toString() + " already exists. Skipping extraction.");
}
} catch (IOException e) {
logger.error("Error finding jar resource " + resourcePath, e);
}
}
public File getDefaultRknnModel() {
return defaultModelFile;
}
public List<String> getLabels() {
return labels;
}
}

View File

@@ -25,11 +25,14 @@ 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.camera.QuirkyCamera;
import org.photonvision.vision.processes.VisionModule;
import org.photonvision.vision.processes.VisionModuleManager;
import org.photonvision.vision.processes.VisionSource;
@@ -119,6 +122,7 @@ 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);
@@ -141,7 +145,8 @@ public class PhotonConfiguration {
LibCameraJNILoader.isSupported()
? "Zerocopy Libcamera Working"
: ""); // TODO add support for other types of GPU accel
generalSubmap.put("mrCalWorking", MrCalJNILoader.isWorking());
generalSubmap.put("mrCalWorking", MrCalJNILoader.getInstance().isLoaded());
generalSubmap.put("rknnSupported", RknnDetectorJNI.getInstance().isLoaded());
generalSubmap.put("hardwareModel", hardwareConfig.deviceName);
generalSubmap.put("hardwarePlatform", Platform.getPlatformName());
settingsSubmap.put("general", generalSubmap);
@@ -178,6 +183,7 @@ public class PhotonConfiguration {
public int inputStreamPort;
public List<CameraCalibrationCoefficients> calibrations;
public boolean isFovConfigurable = true;
public QuirkyCamera cameraQuirks;
public boolean isCSICamera;
}
}

View File

@@ -30,6 +30,8 @@ import java.util.HashMap;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
import org.photonvision.common.configuration.DatabaseSchema.Columns;
import org.photonvision.common.configuration.DatabaseSchema.Tables;
import org.photonvision.common.logging.LogGroup;
import org.photonvision.common.logging.Logger;
import org.photonvision.common.util.file.JacksonUtils;
@@ -47,13 +49,7 @@ import org.photonvision.vision.pipeline.DriverModePipelineSettings;
public class SqlConfigProvider extends ConfigProvider {
private static final Logger logger = new Logger(SqlConfigProvider.class, LogGroup.Config);
static class TableKeys {
static final String CAM_UNIQUE_NAME = "unique_name";
static final String CONFIG_JSON = "config_json";
static final String DRIVERMODE_JSON = "drivermode_json";
static final String OTHERPATHS_JSON = "otherpaths_json";
static final String PIPELINE_JSONS = "pipeline_jsons";
static class GlobalKeys {
static final String NETWORK_CONFIG = "networkConfig";
static final String HARDWARE_CONFIG = "hardwareConfig";
static final String HARDWARE_SETTINGS = "hardwareSettings";
@@ -61,14 +57,24 @@ public class SqlConfigProvider extends ConfigProvider {
}
private static final String dbName = "photon.sqlite";
// private final File rootFolder;
private final String dbPath;
private final String url;
private final Object m_mutex = new Object();
private final File rootFolder;
public SqlConfigProvider(Path rootFolder) {
this.rootFolder = rootFolder.toFile();
public SqlConfigProvider(Path rootPath) {
File rootFolder = rootPath.toFile();
// Make sure root dir exists
if (!rootFolder.exists()) {
if (rootFolder.mkdirs()) {
logger.debug("Root config folder did not exist. Created!");
} else {
logger.error("Failed to create root config folder!");
}
}
dbPath = Path.of(rootFolder.toString(), dbName).toAbsolutePath().toString();
url = "jdbc:sqlite:" + dbPath;
logger.debug("Using database " + dbPath);
initDatabase();
}
@@ -80,91 +86,136 @@ public class SqlConfigProvider extends ConfigProvider {
return config;
}
private Connection createConn() {
String url = "jdbc:sqlite:" + dbPath;
private Connection createConn(boolean autoCommit) {
Connection conn = null;
try {
var conn = DriverManager.getConnection(url);
conn.setAutoCommit(false);
return conn;
conn = DriverManager.getConnection(url);
conn.setAutoCommit(autoCommit);
} catch (SQLException e) {
logger.error("Error creating connection", e);
return null;
}
return conn;
}
private Connection createConn() {
return createConn(false);
}
private void tryCommit(Connection conn) {
try {
conn.commit();
} catch (SQLException e) {
logger.error("Err committing changes: ", e);
} catch (SQLException e1) {
logger.error("Err committing changes: ", e1);
try {
conn.rollback();
} catch (SQLException e1) {
logger.error("Err rolling back changes: ", e);
} catch (SQLException e2) {
logger.error("Err rolling back changes: ", e2);
}
}
}
private void initDatabase() {
// Make sure root dir exists
private int getIntPragma(String pragma) {
int retval = 0;
try (Connection conn = createConn(true);
Statement stmt = conn.createStatement()) {
ResultSet rs = stmt.executeQuery("PRAGMA " + pragma + ";");
retval = rs.getInt(1);
} catch (SQLException e) {
logger.error("Error querying " + pragma, e);
}
return retval;
}
if (!rootFolder.exists()) {
if (rootFolder.mkdirs()) {
logger.debug("Root config folder did not exist. Created!");
} else {
logger.error("Failed to create root config folder!");
private int getSchemaVersion() {
return getIntPragma("schema_version");
}
public int getUserVersion() {
return getIntPragma("user_version");
}
private void setUserVersion(Connection conn, int value) {
try (Statement stmt = conn.createStatement()) {
stmt.execute("PRAGMA user_version = " + value + ";");
} catch (SQLException e) {
logger.error("Error setting user_version to ", e);
}
}
private void doMigration(int index) throws SQLException {
logger.debug("Running migration step " + index);
try (Connection conn = createConn();
Statement stmt = conn.createStatement()) {
for (String sql : DatabaseSchema.migrations[index].split(";")) {
stmt.addBatch(sql);
}
stmt.executeBatch();
setUserVersion(conn, index + 1);
tryCommit(conn);
} catch (SQLException e) {
logger.error("Error with migration step " + index, e);
throw e;
}
}
private void initDatabase() {
int userVersion = getUserVersion();
int expectedVersion = DatabaseSchema.migrations.length;
if (userVersion < expectedVersion) {
// older database, run migrations
// first, check to see if this is one of the ones from 2024 beta that need special handling
if (userVersion == 0 && getSchemaVersion() > 0) {
String sql =
"SELECT COUNT(*) AS CNTREC FROM pragma_table_info('cameras') WHERE name='otherpaths_json';";
try (Connection conn = createConn(true);
Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery(sql); ) {
if (rs.getInt("CNTREC") == 0) {
// need to add otherpaths_json
userVersion = 1;
} else {
// already there, no need to add the column
userVersion = 2;
}
setUserVersion(conn, userVersion);
} catch (SQLException e) {
logger.error(
"Could not determine the version of the database. Try deleting "
+ dbName
+ "and restart photonvision.",
e);
}
}
logger.debug("Older database version. Migrating ... ");
try {
for (int index = userVersion; index < expectedVersion; index++) {
doMigration(index);
}
logger.debug("Database migration complete");
} catch (SQLException e) {
logger.error("Error with database migration", e);
}
}
Connection conn = null;
Statement createGlobalTableStatement = null, createCameraTableStatement = null;
try {
conn = createConn();
if (conn == null) {
logger.error("No connection, cannot init db");
return;
}
// Create global settings table. Just a dumb table with list of jsons and their
// name
try {
createGlobalTableStatement = conn.createStatement();
String sql =
"CREATE TABLE IF NOT EXISTS global (\n"
+ " filename TINYTEXT PRIMARY KEY,\n"
+ " contents mediumtext NOT NULL\n"
+ ");";
createGlobalTableStatement.execute(sql);
} catch (SQLException e) {
logger.error("Err creating global table", e);
}
// Create cameras table, key is the camera unique name
try {
createCameraTableStatement = conn.createStatement();
var sql =
"CREATE TABLE IF NOT EXISTS cameras (\n"
+ " unique_name TINYTEXT PRIMARY KEY,\n"
+ " config_json text NOT NULL,\n"
+ " drivermode_json text NOT NULL,\n"
+ " otherpaths_json text NOT NULL,\n"
+ " pipeline_jsons mediumtext NOT NULL\n"
+ ");";
createCameraTableStatement.execute(sql);
} catch (SQLException e) {
logger.error("Err creating cameras table", e);
}
this.tryCommit(conn);
} finally {
try {
if (createGlobalTableStatement != null) createGlobalTableStatement.close();
if (createCameraTableStatement != null) createCameraTableStatement.close();
if (conn != null) conn.close();
} catch (SQLException e) {
e.printStackTrace();
}
// Warn if the database still isn't at the correct version
userVersion = getUserVersion();
if (userVersion > expectedVersion) {
// database must be from a newer version, so warn
logger.warn(
"This database is from a newer version of PhotonVision. Check that you are running the right version of PhotonVision.");
} else if (userVersion < expectedVersion) {
// migration didn't work, so warn
logger.warn(
"This database migration failed. Expected version: "
+ expectedVersion
+ ", got version: "
+ userVersion);
} else {
// migration worked
logger.info("Using correct database version: " + userVersion);
}
}
@@ -212,7 +263,7 @@ public class SqlConfigProvider extends ConfigProvider {
try {
hardwareConfig =
JacksonUtils.deserialize(
getOneConfigFile(conn, TableKeys.HARDWARE_CONFIG), HardwareConfig.class);
getOneConfigFile(conn, GlobalKeys.HARDWARE_CONFIG), HardwareConfig.class);
} catch (IOException e) {
logger.error("Could not deserialize hardware config! Loading defaults");
hardwareConfig = new HardwareConfig();
@@ -221,7 +272,7 @@ public class SqlConfigProvider extends ConfigProvider {
try {
hardwareSettings =
JacksonUtils.deserialize(
getOneConfigFile(conn, TableKeys.HARDWARE_SETTINGS), HardwareSettings.class);
getOneConfigFile(conn, GlobalKeys.HARDWARE_SETTINGS), HardwareSettings.class);
} catch (IOException e) {
logger.error("Could not deserialize hardware settings! Loading defaults");
hardwareSettings = new HardwareSettings();
@@ -230,7 +281,7 @@ public class SqlConfigProvider extends ConfigProvider {
try {
networkConfig =
JacksonUtils.deserialize(
getOneConfigFile(conn, TableKeys.NETWORK_CONFIG), NetworkConfig.class);
getOneConfigFile(conn, GlobalKeys.NETWORK_CONFIG), NetworkConfig.class);
} catch (IOException e) {
logger.error("Could not deserialize network config! Loading defaults");
networkConfig = new NetworkConfig();
@@ -239,7 +290,7 @@ public class SqlConfigProvider extends ConfigProvider {
try {
atfl =
JacksonUtils.deserialize(
getOneConfigFile(conn, TableKeys.ATFL_CONFIG_FILE), AprilTagFieldLayout.class);
getOneConfigFile(conn, GlobalKeys.ATFL_CONFIG_FILE), AprilTagFieldLayout.class);
} catch (IOException e) {
logger.error("Could not deserialize apriltag layout! Loading defaults");
try {
@@ -273,12 +324,15 @@ public class SqlConfigProvider extends ConfigProvider {
PreparedStatement query = null;
try {
query =
conn.prepareStatement("SELECT contents FROM global where filename=\"" + filename + "\"");
conn.prepareStatement(
String.format(
"SELECT %s FROM %s WHERE %s = \"%s\"",
Columns.GLB_CONTENTS, Tables.GLOBAL, Columns.GLB_FILENAME, filename));
var result = query.executeQuery();
while (result.next()) {
return result.getString("contents");
return result.getString(Columns.GLB_CONTENTS);
}
} catch (SQLException e) {
logger.error("SQL Err getting file " + filename, e);
@@ -297,8 +351,14 @@ public class SqlConfigProvider extends ConfigProvider {
try {
// Replace this camera's row with the new settings
var sqlString =
"REPLACE INTO cameras (unique_name, config_json, drivermode_json, otherpaths_json, pipeline_jsons) VALUES "
+ "(?,?,?,?,?);";
String.format(
"REPLACE INTO %s (%s, %s, %s, %s, %s) VALUES (?,?,?,?,?);",
Tables.CAMERAS,
Columns.CAM_UNIQUE_NAME,
Columns.CAM_CONFIG_JSON,
Columns.CAM_DRIVERMODE_JSON,
Columns.CAM_OTHERPATHS_JSON,
Columns.CAM_PIPELINE_JSONS);
for (var c : config.getCameraConfigurations().entrySet()) {
PreparedStatement statement = conn.prepareStatement(sqlString);
@@ -372,13 +432,16 @@ public class SqlConfigProvider extends ConfigProvider {
PreparedStatement statement3 = null;
try {
// Replace this camera's row with the new settings
var sqlString = "REPLACE INTO global (filename, contents) VALUES " + "(?,?);";
var sqlString =
String.format(
"REPLACE INTO %s (%s, %s) VALUES (?,?);",
Tables.GLOBAL, Columns.GLB_FILENAME, Columns.GLB_CONTENTS);
if (!skipSavingHWSet) {
statement1 = conn.prepareStatement(sqlString);
addFile(
statement1,
TableKeys.HARDWARE_SETTINGS,
GlobalKeys.HARDWARE_SETTINGS,
JacksonUtils.serializeToString(config.getHardwareSettings()));
statement1.executeUpdate();
}
@@ -387,7 +450,7 @@ public class SqlConfigProvider extends ConfigProvider {
statement2 = conn.prepareStatement(sqlString);
addFile(
statement2,
TableKeys.NETWORK_CONFIG,
GlobalKeys.NETWORK_CONFIG,
JacksonUtils.serializeToString(config.getNetworkConfig()));
statement2.executeUpdate();
statement2.close();
@@ -397,7 +460,7 @@ public class SqlConfigProvider extends ConfigProvider {
statement3 = conn.prepareStatement(sqlString);
addFile(
statement3,
TableKeys.HARDWARE_CONFIG,
GlobalKeys.HARDWARE_CONFIG,
JacksonUtils.serializeToString(config.getHardwareConfig()));
statement3.executeUpdate();
statement3.close();
@@ -432,7 +495,10 @@ public class SqlConfigProvider extends ConfigProvider {
}
// Replace this camera's row with the new settings
var sqlString = "REPLACE INTO global (filename, contents) VALUES " + "(?,?);";
var sqlString =
String.format(
"REPLACE INTO %s (%s, %s) VALUES (?,?);",
Tables.GLOBAL, Columns.GLB_FILENAME, Columns.GLB_CONTENTS);
statement1 = conn.prepareStatement(sqlString);
addFile(statement1, fname, Files.readString(path));
@@ -461,25 +527,25 @@ public class SqlConfigProvider extends ConfigProvider {
@Override
public boolean saveUploadedHardwareConfig(Path uploadPath) {
skipSavingHWCfg = true;
return saveOneFile(TableKeys.HARDWARE_CONFIG, uploadPath);
return saveOneFile(GlobalKeys.HARDWARE_CONFIG, uploadPath);
}
@Override
public boolean saveUploadedHardwareSettings(Path uploadPath) {
skipSavingHWSet = true;
return saveOneFile(TableKeys.HARDWARE_SETTINGS, uploadPath);
return saveOneFile(GlobalKeys.HARDWARE_SETTINGS, uploadPath);
}
@Override
public boolean saveUploadedNetworkConfig(Path uploadPath) {
skipSavingNWCfg = true;
return saveOneFile(TableKeys.NETWORK_CONFIG, uploadPath);
return saveOneFile(GlobalKeys.NETWORK_CONFIG, uploadPath);
}
@Override
public boolean saveUploadedAprilTagFieldLayout(Path uploadPath) {
skipSavingAPRTG = true;
return saveOneFile(TableKeys.ATFL_CONFIG_FILE, uploadPath);
return saveOneFile(GlobalKeys.ATFL_CONFIG_FILE, uploadPath);
}
private HashMap<String, CameraConfiguration> loadCameraConfigs(Connection conn) {
@@ -491,12 +557,13 @@ public class SqlConfigProvider extends ConfigProvider {
query =
conn.prepareStatement(
String.format(
"SELECT %s, %s, %s, %s, %s FROM cameras",
TableKeys.CAM_UNIQUE_NAME,
TableKeys.CONFIG_JSON,
TableKeys.DRIVERMODE_JSON,
TableKeys.OTHERPATHS_JSON,
TableKeys.PIPELINE_JSONS));
"SELECT %s, %s, %s, %s, %s FROM %s",
Columns.CAM_UNIQUE_NAME,
Columns.CAM_CONFIG_JSON,
Columns.CAM_DRIVERMODE_JSON,
Columns.CAM_OTHERPATHS_JSON,
Columns.CAM_PIPELINE_JSONS,
Tables.CAMERAS));
var result = query.executeQuery();
@@ -504,18 +571,18 @@ public class SqlConfigProvider extends ConfigProvider {
while (result.next()) {
List<String> dummyList = new ArrayList<>();
var uniqueName = result.getString(TableKeys.CAM_UNIQUE_NAME);
var uniqueName = result.getString(Columns.CAM_UNIQUE_NAME);
var config =
JacksonUtils.deserialize(
result.getString(TableKeys.CONFIG_JSON), CameraConfiguration.class);
result.getString(Columns.CAM_CONFIG_JSON), CameraConfiguration.class);
var driverMode =
JacksonUtils.deserialize(
result.getString(TableKeys.DRIVERMODE_JSON), DriverModePipelineSettings.class);
result.getString(Columns.CAM_DRIVERMODE_JSON), DriverModePipelineSettings.class);
var otherPaths =
JacksonUtils.deserialize(result.getString(TableKeys.OTHERPATHS_JSON), String[].class);
JacksonUtils.deserialize(result.getString(Columns.CAM_OTHERPATHS_JSON), String[].class);
List<?> pipelineSettings =
JacksonUtils.deserialize(
result.getString(TableKeys.PIPELINE_JSONS), dummyList.getClass());
result.getString(Columns.CAM_PIPELINE_JSONS), dummyList.getClass());
List<CVPipelineSettings> loadedSettings = new ArrayList<>();
for (var str : pipelineSettings) {

View File

@@ -26,6 +26,7 @@ public enum DataChangeDestination {
DCD_ACTIVEPIPELINESETTINGS,
DCD_GENSETTINGS,
DCD_UI,
DCD_WEBSERVER,
DCD_OTHER;
public static final List<DataChangeDestination> AllDestinations =

View File

@@ -52,6 +52,7 @@ public class UIDataPublisher implements CVPipelineResultConsumer {
uiTargets.add(t.toHashMap());
}
dataMap.put("targets", uiTargets);
dataMap.put("classNames", result.objectDetectionClassNames);
// Only send Multitag Results if they are present, similar to 3d pose
if (result.multiTagResult.estimatedPose.isPresent) {

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

@@ -43,6 +43,7 @@ public enum Platform {
true,
OSType.LINUX,
true), // Raspberry Pi 3/4 with a 64-bit image
LINUX_RK3588_64("Linux AARCH 64-bit with RK3588", "linuxarm64", false, OSType.LINUX, true),
LINUX_AARCH64(
"Linux AARCH64", "linuxarm64", false, OSType.LINUX, true), // Jetson Nano, Jetson TX2
@@ -94,6 +95,10 @@ public enum Platform {
return currentPlatform.osType == OSType.LINUX;
}
public static boolean isRK3588() {
return Platform.isOrangePi() || Platform.isCoolPi4b();
}
public static boolean isRaspberryPi() {
return currentPlatform.isPi;
}
@@ -186,7 +191,11 @@ public enum Platform {
return LINUX_32;
} else if (RuntimeDetector.isArm64()) {
// TODO - os detection needed?
return LINUX_AARCH64;
if (isOrangePi()) {
return LINUX_RK3588_64;
} else {
return LINUX_AARCH64;
}
} else if (RuntimeDetector.isArm32()) {
return LINUX_ARM32;
} else {
@@ -204,6 +213,14 @@ public enum Platform {
return fileHasText("/proc/cpuinfo", "Raspberry Pi");
}
private static boolean isOrangePi() {
return fileHasText("/proc/device-tree/model", "Orange Pi 5");
}
private static boolean isCoolPi4b() {
return fileHasText("/proc/device-tree/model", "CoolPi 4B");
}
private static boolean isJetsonSBC() {
// https://forums.developer.nvidia.com/t/how-to-recognize-jetson-nano-device/146624
return fileHasText("/proc/device-tree/model", "NVIDIA Jetson");

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 {

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,48 @@
/*
* 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}'";
}
}

View File

@@ -19,6 +19,10 @@ package org.photonvision.common.networking;
import org.photonvision.common.configuration.ConfigManager;
import org.photonvision.common.configuration.NetworkConfig;
import org.photonvision.common.dataflow.DataChangeDestination;
import org.photonvision.common.dataflow.DataChangeService;
import org.photonvision.common.dataflow.DataChangeSource;
import org.photonvision.common.dataflow.events.DataChangeEvent;
import org.photonvision.common.hardware.Platform;
import org.photonvision.common.logging.LogGroup;
import org.photonvision.common.logging.Logger;
@@ -43,6 +47,7 @@ public class NetworkManager {
public void initialize(boolean shouldManage) {
isManaged = shouldManage && !networkingIsDisabled;
if (!isManaged) {
logger.info("Network management is disabled.");
return;
}
@@ -147,5 +152,13 @@ public class NetworkManager {
public void reinitialize() {
initialize(ConfigManager.getInstance().getConfig().getNetworkConfig().shouldManage());
DataChangeService.getInstance()
.publishEvent(
new DataChangeEvent<Boolean>(
DataChangeSource.DCS_OTHER,
DataChangeDestination.DCD_WEBSERVER,
"restartServer",
true));
}
}

View File

@@ -144,6 +144,24 @@ public class TestUtils {
}
}
public enum WPI2024Images {
kBackAmpZone_117in,
kSpeakerCenter_143in;
public static double FOV = 68.5;
public final Path path;
Path getPath() {
var filename = this.toString().substring(1);
return Path.of("2024", filename + ".jpg");
}
WPI2024Images() {
this.path = getPath();
}
}
public enum WPI2023Apriltags {
k162_36_Angle,
k162_36_Straight,

View File

@@ -26,12 +26,15 @@ import org.photonvision.common.logging.LogGroup;
import org.photonvision.common.logging.Logger;
public abstract class PhotonJNICommon {
static boolean libraryLoaded = false;
public abstract boolean isLoaded();
public abstract void setLoaded(boolean state);
protected static Logger logger = null;
protected static synchronized void forceLoad(Class<?> clazz, List<String> libraries)
throws IOException {
if (libraryLoaded) return;
protected static synchronized void forceLoad(
PhotonJNICommon instance, Class<?> clazz, List<String> libraries) throws IOException {
if (instance.isLoaded()) return;
if (logger == null) logger = new Logger(clazz, LogGroup.Camera);
for (var libraryName : libraries) {
@@ -42,7 +45,7 @@ public abstract class PhotonJNICommon {
var in = clazz.getResourceAsStream("/nativelibraries/" + arch_name + "/" + nativeLibName);
if (in == null) {
libraryLoaded = false;
instance.setLoaded(false);
return;
}
@@ -69,15 +72,11 @@ public abstract class PhotonJNICommon {
break;
}
}
libraryLoaded = true;
instance.setLoaded(true);
}
protected static synchronized void forceLoad(Class<?> clazz, String libraryName)
throws IOException {
forceLoad(clazz, List.of(libraryName));
}
public static boolean isWorking() {
return libraryLoaded;
protected static synchronized void forceLoad(
PhotonJNICommon instance, Class<?> clazz, String libraryName) throws IOException {
forceLoad(instance, clazz, List.of(libraryName));
}
}

View File

@@ -0,0 +1,138 @@
/*
* Copyright (C) Photon Vision.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.photonvision.jni;
import java.io.IOException;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.stream.Collectors;
import org.photonvision.common.logging.LogGroup;
import org.photonvision.common.logging.Logger;
import org.photonvision.common.util.TestUtils;
import org.photonvision.rknn.RknnJNI;
import org.photonvision.rknn.RknnJNI.RknnResult;
import org.photonvision.vision.opencv.CVMat;
import org.photonvision.vision.pipe.impl.NeuralNetworkPipeResult;
public class RknnDetectorJNI extends PhotonJNICommon {
private static final Logger logger = new Logger(RknnDetectorJNI.class, LogGroup.General);
private boolean isLoaded;
private static RknnDetectorJNI instance = null;
private RknnDetectorJNI() {
isLoaded = false;
}
public static RknnDetectorJNI getInstance() {
if (instance == null) instance = new RknnDetectorJNI();
return instance;
}
public static synchronized void forceLoad() throws IOException {
TestUtils.loadLibraries();
forceLoad(getInstance(), RknnDetectorJNI.class, List.of("rga", "rknnrt", "rknn_jni"));
}
@Override
public boolean isLoaded() {
return isLoaded;
}
@Override
public void setLoaded(boolean state) {
isLoaded = state;
}
public static class RknnObjectDetector {
long objPointer = -1;
private List<String> labels;
private final Object lock = new Object();
private static final CopyOnWriteArrayList<Long> detectors = new CopyOnWriteArrayList<>();
public RknnObjectDetector(String modelPath, List<String> labels) {
synchronized (lock) {
objPointer = RknnJNI.create(modelPath, labels.size());
detectors.add(objPointer);
System.out.println(
"Created " + objPointer + "! Detectors: " + Arrays.toString(detectors.toArray()));
}
this.labels = labels;
}
public List<String> getClasses() {
return labels;
}
/**
* Detect forwards using this model
*
* @param in The image to process
* @param nmsThresh Non-maximum supression threshold. Probably should not change
* @param boxThresh Minimum confidence for a box to be added. Basically just confidence
* threshold
*/
public List<NeuralNetworkPipeResult> detect(CVMat in, double nmsThresh, double boxThresh) {
RknnResult[] ret;
synchronized (lock) {
// We can technically be asked to detect and the lock might be acquired _after_ release has
// been called. This would mean objPointer would be invalid which would call everything to
// explode.
if (objPointer > 0) {
ret = RknnJNI.detect(objPointer, in.getMat().getNativeObjAddr(), nmsThresh, boxThresh);
} else {
logger.warn("Detect called after destroy -- giving up");
return List.of();
}
}
if (ret == null) {
return List.of();
}
return List.of(ret).stream()
.map(it -> new NeuralNetworkPipeResult(it.rect, it.class_id, it.conf))
.collect(Collectors.toList());
}
public void release() {
synchronized (lock) {
if (objPointer > 0) {
RknnJNI.destroy(objPointer);
detectors.remove(objPointer);
System.out.println(
"Killed " + objPointer + "! Detectors: " + Arrays.toString(detectors.toArray()));
objPointer = -1;
} else {
logger.error("RKNN Detector has already been destroyed!");
}
}
}
}
// public static void createRknnDetector() {
// objPointer =
// RknnJNI.create(
// NeuralNetworkModelManager.getInstance()
// .getDefaultRknnModel()
// .getAbsolutePath()
// .toString(),
// NeuralNetworkModelManager.getInstance().getLabels().size());
// }
}

View File

@@ -24,6 +24,19 @@ import org.photonvision.common.util.TestUtils;
import org.photonvision.jni.PhotonJNICommon;
public class MrCalJNILoader extends PhotonJNICommon {
private boolean isLoaded;
private static MrCalJNILoader instance = null;
private MrCalJNILoader() {
isLoaded = false;
}
public static synchronized MrCalJNILoader getInstance() {
if (instance == null) instance = new MrCalJNILoader();
return instance;
}
public static synchronized void forceLoad() throws IOException {
// Force load opencv
TestUtils.loadLibraries();
@@ -32,6 +45,7 @@ public class MrCalJNILoader extends PhotonJNICommon {
if (Platform.isWindows()) {
// Order is correct to match dependencies of libraries
forceLoad(
MrCalJNILoader.getInstance(),
MrCalJNILoader.class,
List.of(
"libamd",
@@ -40,17 +54,28 @@ public class MrCalJNILoader extends PhotonJNICommon {
"libccolamd",
"openblas",
"libgcc_s_seh-1",
"libquadmath-0",
"libgfortran-5",
"liblapack",
"libcholmod",
"mrcal_jni"));
} else {
// Nothing else to do on linux
forceLoad(MrCalJNILoader.class, List.of("mrcal_jni"));
forceLoad(MrCalJNILoader.getInstance(), MrCalJNILoader.class, List.of("mrcal_jni"));
}
if (!MrCalJNILoader.isWorking()) {
if (!MrCalJNILoader.getInstance().isLoaded()) {
throw new IOException("Unable to load mrcal JNI!");
}
}
@Override
public boolean isLoaded() {
return isLoaded;
}
@Override
public void setLoaded(boolean state) {
isLoaded = state;
}
}

View File

@@ -54,6 +54,9 @@ public class CameraCalibrationCoefficients implements Releasable {
@JsonProperty("calobjectSpacing")
public final double calobjectSpacing;
@JsonProperty("lensmodel")
public final CameraLensModel lensmodel;
@JsonIgnore private final double[] intrinsicsArr = new double[9];
@JsonIgnore private final double[] distCoeffsArr = new double[5];
@@ -83,13 +86,15 @@ public class CameraCalibrationCoefficients implements Releasable {
@JsonProperty("calobjectWarp") double[] calobjectWarp,
@JsonProperty("observations") List<BoardObservation> observations,
@JsonProperty("calobjectSize") Size calobjectSize,
@JsonProperty("calobjectSpacing") double calobjectSpacing) {
@JsonProperty("calobjectSpacing") double calobjectSpacing,
@JsonProperty("lensmodel") CameraLensModel lensmodel) {
this.resolution = resolution;
this.cameraIntrinsics = cameraIntrinsics;
this.distCoeffs = distCoeffs;
this.calobjectWarp = calobjectWarp;
this.calobjectSize = calobjectSize;
this.calobjectSpacing = calobjectSpacing;
this.lensmodel = lensmodel;
// Legacy migration just to make sure that observations is at worst empty and never null
if (observations == null) {
@@ -174,7 +179,8 @@ public class CameraCalibrationCoefficients implements Releasable {
new double[0],
List.of(),
new Size(0, 0),
0);
0,
CameraLensModel.LENSMODEL_OPENCV);
}
@Override

View File

@@ -0,0 +1,34 @@
/*
* Copyright (C) Photon Vision.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.photonvision.vision.calibration;
/**
* What kind of camera lens model our intrinsics are modeling. For more info see:
* https://docs.opencv.org/4.x/dc/dbb/tutorial_py_calibration.html
* https://mrcal.secretsauce.net/lensmodels.html#org4e95788
*/
public enum CameraLensModel {
/** OpenCV[4,5,8,12]-based model */
LENSMODEL_OPENCV,
/** Mrcal steriographic lens model. See LENSMODEL_STEREOGRAPHIC in the mrcal docs */
LENSMODEL_STERIOGRAPHIC,
/**
* Mrcal splined-steriographic lens model. See LENSMODEL_SPLINED_STEREOGRAPHIC_ in the mrcal docs
*/
LENSMODEL_SPLINED_STERIOGRAPHIC
}

View File

@@ -32,4 +32,13 @@ public enum CameraQuirk {
AdjustableFocus,
/** Changing FPS repeatedly with small delay does not work correctly */
StickyFPS,
/** Camera is an arducam. This means it shares VID/PID with other arducams (ew) */
ArduCamCamera,
/**
* Camera is an arducam ov9281 which has a funky exposure issue where it is defined in v4l as
* 1-5000 instead of 1-75
*/
ArduOV9281,
/** Dummy quirk to tell OV2311 from OV9281 */
ArduOV2311,
}

View File

@@ -17,6 +17,8 @@
package org.photonvision.vision.camera;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
@@ -47,8 +49,32 @@ public class QuirkyCamera {
-1, -1, "mmal service 16.1", CameraQuirk.PiCam), // PiCam (via V4L2, not zerocopy)
new QuirkyCamera(-1, -1, "unicam", CameraQuirk.PiCam), // PiCam (via V4L2, not zerocopy)
new QuirkyCamera(0x85B, 0x46D, CameraQuirk.AdjustableFocus), // Logitech C925-e
new QuirkyCamera(0x6366, 0x0c45, CameraQuirk.StickyFPS) // Arducam OV2311
);
// Generic arducam. Since OV2311 can't be differentiated at first boot, apply stickyFPS to
// the generic case, too
new QuirkyCamera(
0x0c45,
0x6366,
"",
"Arducam Generic",
CameraQuirk.ArduCamCamera,
CameraQuirk.StickyFPS),
// Arducam OV2311
new QuirkyCamera(
0x0c45,
0x6366,
"OV2311",
"OV2311",
CameraQuirk.ArduCamCamera,
CameraQuirk.ArduOV2311,
CameraQuirk.StickyFPS),
// Arducam OV9281
new QuirkyCamera(
0x0c45,
0x6366,
"OV9281",
"OV9281",
CameraQuirk.ArduCamCamera,
CameraQuirk.ArduOV9281));
public static final QuirkyCamera DefaultCamera = new QuirkyCamera(0, 0, "");
public static final QuirkyCamera ZeroCopyPiCamera =
@@ -60,9 +86,19 @@ public class QuirkyCamera {
CameraQuirk.Gain,
CameraQuirk.AWBGain); // PiCam (special zerocopy version)
@JsonProperty("baseName")
public final String baseName;
@JsonProperty("usbVid")
public final int usbVid;
@JsonProperty("usbPid")
public final int usbPid;
@JsonProperty("displayName")
public final String displayName;
@JsonProperty("quirks")
public final HashMap<CameraQuirk, Boolean> quirks;
/**
@@ -85,9 +121,24 @@ public class QuirkyCamera {
* @param quirks Camera quirks
*/
private QuirkyCamera(int usbVid, int usbPid, String baseName, CameraQuirk... quirks) {
this(usbVid, usbPid, baseName, "", quirks);
}
/**
* Creates a QuirkyCamera that matches by USB VID/PID and name
*
* @param usbVid USB VID of camera
* @param usbPid USB PID of camera
* @param baseName CSCore name of camera
* @param displayName Human-friendly quicky camera name
* @param quirks Camera quirks
*/
private QuirkyCamera(
int usbVid, int usbPid, String baseName, String displayName, CameraQuirk... quirks) {
this.usbVid = usbVid;
this.usbPid = usbPid;
this.baseName = baseName;
this.displayName = displayName;
this.quirks = new HashMap<>();
for (var q : quirks) {
@@ -98,6 +149,20 @@ public class QuirkyCamera {
}
}
@JsonCreator
public QuirkyCamera(
@JsonProperty("baseName") String baseName,
@JsonProperty("usbVid") int usbVid,
@JsonProperty("usbPid") int usbPid,
@JsonProperty("displayName") String displayName,
@JsonProperty("quirks") HashMap<CameraQuirk, Boolean> quirks) {
this.baseName = baseName;
this.usbPid = usbPid;
this.usbVid = usbVid;
this.quirks = quirks;
this.displayName = displayName;
}
public boolean hasQuirk(CameraQuirk quirk) {
return quirks.get(quirk);
}
@@ -144,8 +209,39 @@ public class QuirkyCamera {
&& Objects.equals(quirks, that.quirks);
}
@Override
public String toString() {
String ret =
"QuirkyCamera [baseName="
+ baseName
+ ", displayName="
+ displayName
+ ", usbVid="
+ usbVid
+ ", usbPid="
+ usbPid
+ ", quirks="
+ quirks.toString()
+ "]";
return ret;
}
@Override
public int hashCode() {
return Objects.hash(usbVid, usbPid, baseName, quirks);
}
/**
* Add/remove quirks from the camera we're controlling
*
* @param quirksToChange map of true/false for quirks we should change
*/
public void updateQuirks(HashMap<CameraQuirk, Boolean> quirksToChange) {
for (var q : quirksToChange.entrySet()) {
var quirk = q.getKey();
var hasQuirk = q.getValue();
this.quirks.put(quirk, hasQuirk);
}
}
}

View File

@@ -45,8 +45,6 @@ public class USBCameraSource extends VisionSource {
private FrameProvider usbFrameProvider;
private final CvSink cvSink;
private QuirkyCamera cameraQuirks;
public USBCameraSource(CameraConfiguration config) {
super(config);
@@ -54,17 +52,21 @@ public class USBCameraSource extends VisionSource {
camera = new UsbCamera(config.nickname, config.path);
cvSink = CameraServer.getVideo(this.camera);
cameraQuirks =
QuirkyCamera.getQuirkyCamera(
camera.getInfo().productId, camera.getInfo().vendorId, config.baseName);
if (getCameraConfiguration().cameraQuirks == null)
getCameraConfiguration().cameraQuirks =
QuirkyCamera.getQuirkyCamera(
camera.getInfo().vendorId, camera.getInfo().productId, config.baseName);
if (cameraQuirks.hasQuirks()) {
logger.info("Quirky camera detected: " + cameraQuirks.baseName);
if (getCameraConfiguration().cameraQuirks.hasQuirks()) {
logger.info("Quirky camera detected: " + getCameraConfiguration().cameraQuirks.baseName);
}
if (cameraQuirks.hasQuirk(CameraQuirk.CompletelyBroken)) {
if (getCameraConfiguration().cameraQuirks.hasQuirk(CameraQuirk.CompletelyBroken)) {
// set some defaults, as these should never be used.
logger.info("Camera " + cameraQuirks.baseName + " is not supported for PhotonVision");
logger.info(
"Camera "
+ getCameraConfiguration().cameraQuirks.baseName
+ " is not supported for PhotonVision");
usbCameraSettables = null;
usbFrameProvider = null;
} else {
@@ -88,7 +90,9 @@ public class USBCameraSource extends VisionSource {
public USBCameraSource(CameraConfiguration config, int pid, int vid, boolean unitTest) {
this(config);
cameraQuirks = QuirkyCamera.getQuirkyCamera(pid, vid, config.baseName);
if (getCameraConfiguration().cameraQuirks == null)
getCameraConfiguration().cameraQuirks =
QuirkyCamera.getQuirkyCamera(pid, vid, config.baseName);
if (unitTest)
usbFrameProvider =
@@ -99,7 +103,7 @@ public class USBCameraSource extends VisionSource {
}
void disableAutoFocus() {
if (cameraQuirks.hasQuirk(CameraQuirk.AdjustableFocus)) {
if (getCameraConfiguration().cameraQuirks.hasQuirk(CameraQuirk.AdjustableFocus)) {
try {
camera.getProperty("focus_auto").set(0);
camera.getProperty("focus_absolute").set(0); // Focus into infinity
@@ -110,7 +114,7 @@ public class USBCameraSource extends VisionSource {
}
public QuirkyCamera getCameraQuirks() {
return this.cameraQuirks;
return getCameraConfiguration().cameraQuirks;
}
@Override
@@ -124,17 +128,21 @@ public class USBCameraSource extends VisionSource {
}
public class USBCameraSettables extends VisionSourceSettables {
// We need to remember the last exposure set when exiting auto exposure mode so we can restore
// it
private double last_exposure = -1;
protected USBCameraSettables(CameraConfiguration configuration) {
super(configuration);
getAllVideoModes();
if (!cameraQuirks.hasQuirk(CameraQuirk.StickyFPS))
if (!configuration.cameraQuirks.hasQuirk(CameraQuirk.StickyFPS))
if (!videoModes.isEmpty()) setVideoMode(videoModes.get(0)); // fixes double FPS set
}
public void setAutoExposure(boolean cameraAutoExposure) {
logger.debug("Setting auto exposure to " + cameraAutoExposure);
if (cameraQuirks.hasQuirk(CameraQuirk.PiCam)) {
if (getCameraConfiguration().cameraQuirks.hasQuirk(CameraQuirk.PiCam)) {
// Case, we know this is a picam. Go through v4l2-ctl interface directly
// Common settings
@@ -166,20 +174,46 @@ public class USBCameraSource extends VisionSource {
} else {
// Case - this is some other USB cam. Default to wpilib's implementation
var canSetWhiteBalance = !cameraQuirks.hasQuirk(CameraQuirk.Gain);
var canSetWhiteBalance = !getCameraConfiguration().cameraQuirks.hasQuirk(CameraQuirk.Gain);
if (!cameraAutoExposure) {
// Pick a bunch of reasonable setting defaults for vision processing retroreflective
if (canSetWhiteBalance) {
camera.setWhiteBalanceManual(4000); // Auto white-balance disabled, 4000K preset
// Linux kernel bump changed names -- now called white_balance_automatic and
// white_balance_temperature
if (camera.getProperty("white_balance_automatic").getKind() != Kind.kNone) {
// 1=auto, 0=manual
camera.getProperty("white_balance_automatic").set(0);
camera.getProperty("white_balance_temperature").set(4000);
} else {
camera.setWhiteBalanceManual(4000); // Auto white-balance disabled, 4000K preset
}
// Most cameras leave exposure time absolute at the last value from their AE algorithm.
// Set it back to the exposure slider value
setExposure(this.last_exposure);
}
} else {
// Pick a bunch of reasonable setting defaults for driver, fiducials, or otherwise
// nice-for-humans
if (canSetWhiteBalance) {
camera.setWhiteBalanceAuto(); // Auto white-balance enabled
// Linux kernel bump changed names -- now called white_balance_automatic
if (camera.getProperty("white_balance_automatic").getKind() != Kind.kNone) {
// 1=auto, 0=manual
camera.getProperty("white_balance_automatic").set(1);
} else {
camera.setWhiteBalanceAuto(); // Auto white-balance enabled
}
}
// Linux kernel bump changed names -- exposure_auto is now called auto_exposure
if (camera.getProperty("auto_exposure").getKind() != Kind.kNone) {
var prop = camera.getProperty("auto_exposure");
// 3=auto-aperature
prop.set((int) 3);
} else {
camera.setExposureAuto(); // auto exposure enabled
}
camera.setExposureAuto(); // auto exposure enabled
}
}
}
@@ -207,17 +241,30 @@ public class USBCameraSource extends VisionSource {
if (exposure >= 0.0) {
try {
int scaledExposure = 1;
if (cameraQuirks.hasQuirk(CameraQuirk.PiCam)) {
if (getCameraConfiguration().cameraQuirks.hasQuirk(CameraQuirk.PiCam)) {
scaledExposure = Math.round(timeToPiCamRawExposure(pctToExposureTimeUs(exposure)));
logger.debug("Setting camera raw exposure to " + scaledExposure);
camera.getProperty("raw_exposure_time_absolute").set(scaledExposure);
camera.getProperty("raw_exposure_time_absolute").set(scaledExposure);
} else if (camera.getProperty("exposure_time_absolute").getKind() != Kind.kNone) {
// Seems like the name changed at some point in v4l? set it instead
var prop = camera.getProperty("exposure_time_absolute");
var exposure_manual_val =
MathUtils.map(Math.round(exposure), 0, 100, prop.getMin(), prop.getMax());
// Yay thanks v4l for changing names randomly
} else if (camera.getProperty("exposure_time_absolute").getKind() != Kind.kNone
&& camera.getProperty("auto_exposure").getKind() != Kind.kNone) {
// 1=manual-aperature
camera.getProperty("auto_exposure").set(1);
// Seems like the name changed at some point in v4l? set it ouyrselves too
var prop = camera.getProperty("raw_exposure_time_absolute");
var propMin = prop.getMin();
var propMax = prop.getMax();
if (getCameraConfiguration().cameraQuirks.hasQuirk(CameraQuirk.ArduOV9281)) {
propMin = 1;
propMax = 75;
}
var exposure_manual_val = MathUtils.map(Math.round(exposure), 0, 100, propMin, propMax);
prop.set((int) exposure_manual_val);
} else {
scaledExposure = (int) Math.round(exposure);
@@ -228,6 +275,7 @@ public class USBCameraSource extends VisionSource {
} catch (VideoException e) {
logger.error("Failed to set camera exposure!", e);
}
this.last_exposure = exposure;
}
}
@@ -244,7 +292,7 @@ public class USBCameraSource extends VisionSource {
@Override
public void setGain(int gain) {
try {
if (cameraQuirks.hasQuirk(CameraQuirk.Gain)) {
if (getCameraConfiguration().cameraQuirks.hasQuirk(CameraQuirk.Gain)) {
camera.getProperty("gain_automatic").set(0);
camera.getProperty("gain").set(gain);
}
@@ -278,7 +326,7 @@ public class USBCameraSource extends VisionSource {
List<VideoMode> videoModesList = new ArrayList<>();
try {
VideoMode[] modes;
if (cameraQuirks.hasQuirk(CameraQuirk.PiCam)) {
if (getCameraConfiguration().cameraQuirks.hasQuirk(CameraQuirk.PiCam)) {
modes =
new VideoMode[] {
new VideoMode(PixelFormat.kBGR, 320, 240, 90),
@@ -306,13 +354,13 @@ public class USBCameraSource extends VisionSource {
}
// On picam, filter non-bgr modes for performance
if (cameraQuirks.hasQuirk(CameraQuirk.PiCam)) {
if (getCameraConfiguration().cameraQuirks.hasQuirk(CameraQuirk.PiCam)) {
if (videoMode.pixelFormat != PixelFormat.kBGR) {
continue;
}
}
if (cameraQuirks.hasQuirk(CameraQuirk.FPSCap100)) {
if (getCameraConfiguration().cameraQuirks.hasQuirk(CameraQuirk.FPSCap100)) {
if (videoMode.fps > 100) {
continue;
}
@@ -370,7 +418,7 @@ public class USBCameraSource extends VisionSource {
@Override
public boolean isVendorCamera() {
return ConfigManager.getInstance().getConfig().getHardwareConfig().hasPresetFOV()
&& cameraQuirks.hasQuirk(CameraQuirk.PiCam);
&& getCameraConfiguration().cameraQuirks.hasQuirk(CameraQuirk.PiCam);
}
@Override
@@ -391,15 +439,22 @@ public class USBCameraSource extends VisionSource {
if (cvSink == null) {
if (other.cvSink != null) return false;
} else if (!cvSink.equals(other.cvSink)) return false;
if (cameraQuirks == null) {
if (other.cameraQuirks != null) return false;
} else if (!cameraQuirks.equals(other.cameraQuirks)) return false;
if (getCameraConfiguration().cameraQuirks == null) {
if (other.getCameraConfiguration().cameraQuirks != null) return false;
} else if (!getCameraConfiguration()
.cameraQuirks
.equals(other.getCameraConfiguration().cameraQuirks)) return false;
return true;
}
@Override
public int hashCode() {
return Objects.hash(
camera, usbCameraSettables, usbFrameProvider, cameraConfiguration, cvSink, cameraQuirks);
camera,
usbCameraSettables,
usbFrameProvider,
cameraConfiguration,
cvSink,
getCameraConfiguration().cameraQuirks);
}
}

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

@@ -33,6 +33,10 @@ public abstract class CVPipe<I, O, P> {
this.params = params;
}
public P getParams() {
return this.params;
}
/**
* Runs the process for the pipe.
*

View File

@@ -44,6 +44,13 @@ public class ArucoDetectionPipe
@Override
protected List<ArucoDetectionResult> process(CVMat in) {
var imgMat = in.getMat();
// Sanity check -- image should not be empty
if (imgMat.empty()) {
// give up is best we can do here
return List.of();
}
var detections = photonDetector.detect(imgMat);
// manually do corner refinement ourselves
if (params.useCornerRefinement) {

View File

@@ -35,6 +35,7 @@ import org.photonvision.mrcal.MrCalJNI.MrCalResult;
import org.photonvision.mrcal.MrCalJNILoader;
import org.photonvision.vision.calibration.BoardObservation;
import org.photonvision.vision.calibration.CameraCalibrationCoefficients;
import org.photonvision.vision.calibration.CameraLensModel;
import org.photonvision.vision.calibration.JsonImageMat;
import org.photonvision.vision.calibration.JsonMatOfDouble;
import org.photonvision.vision.pipe.CVPipe;
@@ -76,7 +77,7 @@ public class Calibrate3dPipe
CameraCalibrationCoefficients ret;
var start = System.nanoTime();
if (MrCalJNILoader.isWorking() && params.useMrCal) {
if (MrCalJNILoader.getInstance().isLoaded() && params.useMrCal) {
logger.debug("Calibrating with mrcal!");
ret = calibrateMrcal(in);
} else {
@@ -158,7 +159,8 @@ public class Calibrate3dPipe
new double[0],
observations,
new Size(params.boardWidth, params.boardHeight),
params.squareSize);
params.squareSize,
CameraLensModel.LENSMODEL_OPENCV);
}
protected CameraCalibrationCoefficients calibrateMrcal(
@@ -240,7 +242,8 @@ public class Calibrate3dPipe
new double[] {result.warp_x, result.warp_y},
observations,
new Size(params.boardWidth, params.boardHeight),
params.squareSize);
params.squareSize,
CameraLensModel.LENSMODEL_OPENCV);
}
private List<BoardObservation> createObservations(

View File

@@ -15,7 +15,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.photonvision.vision.pipeline;
package org.photonvision.vision.pipe.impl;
import edu.wpi.first.math.util.Units;
import java.util.ArrayList;
@@ -36,10 +36,10 @@ import org.photonvision.vision.frame.FrameThresholdType;
import org.photonvision.vision.opencv.CVMat;
import org.photonvision.vision.opencv.ImageRotationMode;
import org.photonvision.vision.pipe.CVPipe.CVPipeResult;
import org.photonvision.vision.pipe.impl.CalculateFPSPipe;
import org.photonvision.vision.pipe.impl.Calibrate3dPipe;
import org.photonvision.vision.pipe.impl.FindBoardCornersPipe;
import org.photonvision.vision.pipe.impl.FindBoardCornersPipe.FindBoardCornersPipeResult;
import org.photonvision.vision.pipeline.CVPipeline;
import org.photonvision.vision.pipeline.Calibration3dPipelineSettings;
import org.photonvision.vision.pipeline.UICalibrationData;
import org.photonvision.vision.pipeline.result.CVPipelineResult;
import org.photonvision.vision.pipeline.result.CalibrationPipelineResult;

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

@@ -0,0 +1,32 @@
/*
* Copyright (C) Photon Vision.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.photonvision.vision.pipe.impl;
import org.opencv.core.Rect2d;
public class NeuralNetworkPipeResult {
public NeuralNetworkPipeResult(Rect2d box2, Integer classIdx, Float confidence) {
box = box2;
this.classIdx = classIdx;
this.confidence = confidence;
}
public final int classIdx;
public final Rect2d box;
public final double confidence;
}

View File

@@ -0,0 +1,69 @@
/*
* 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.List;
import org.photonvision.common.configuration.NeuralNetworkModelManager;
import org.photonvision.jni.RknnDetectorJNI.RknnObjectDetector;
import org.photonvision.vision.opencv.CVMat;
import org.photonvision.vision.opencv.Releasable;
import org.photonvision.vision.pipe.CVPipe;
public class RknnDetectionPipe
extends CVPipe<CVMat, List<NeuralNetworkPipeResult>, RknnDetectionPipe.RknnDetectionPipeParams>
implements Releasable {
private RknnObjectDetector detector;
public RknnDetectionPipe() {
// For now this is hard-coded to defaults. Should be refactored into set pipe params, though.
// And ideally a little wrapper helper for only changing native stuff on content change created.
this.detector =
new RknnObjectDetector(
NeuralNetworkModelManager.getInstance().getDefaultRknnModel().getAbsolutePath(),
NeuralNetworkModelManager.getInstance().getLabels());
}
@Override
protected List<NeuralNetworkPipeResult> process(CVMat in) {
var frame = in.getMat();
// Make sure we don't get a weird empty frame
if (frame.empty()) {
return List.of();
}
return detector.detect(in, params.nms, params.confidence);
}
public static class RknnDetectionPipeParams {
public double confidence;
public double nms;
public int max_detections;
public RknnDetectionPipeParams() {}
}
public List<String> getClassNames() {
return detector.getClasses();
}
@Override
public void release() {
detector.release();
}
}

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

@@ -21,9 +21,13 @@ import org.photonvision.vision.camera.QuirkyCamera;
import org.photonvision.vision.frame.Frame;
import org.photonvision.vision.frame.FrameStaticProperties;
import org.photonvision.vision.frame.FrameThresholdType;
import org.photonvision.vision.opencv.Releasable;
import org.photonvision.vision.pipeline.result.CVPipelineResult;
public abstract class CVPipeline<R extends CVPipelineResult, S extends CVPipelineSettings> {
public abstract class CVPipeline<R extends CVPipelineResult, S extends CVPipelineSettings>
implements Releasable {
static final int MAX_MULTI_TARGET_RESULTS = 10;
protected S settings;
protected FrameStaticProperties frameStaticProperties;
protected QuirkyCamera cameraQuirks;
@@ -75,4 +79,11 @@ public abstract class CVPipeline<R extends CVPipelineResult, S extends CVPipelin
return result;
}
/**
* Release any native memory associated with this pipeline. Called by pipelinemanager at pipeline
* switch. Stubbed out, but override if needed.
*/
@Override
public void release() {}
}

View File

@@ -32,7 +32,8 @@ import org.photonvision.vision.opencv.ImageRotationMode;
@JsonSubTypes.Type(value = ReflectivePipelineSettings.class),
@JsonSubTypes.Type(value = DriverModePipelineSettings.class),
@JsonSubTypes.Type(value = AprilTagPipelineSettings.class),
@JsonSubTypes.Type(value = ArucoPipelineSettings.class)
@JsonSubTypes.Type(value = ArucoPipelineSettings.class),
@JsonSubTypes.Type(value = ObjectDetectionPipelineSettings.class)
})
public class CVPipelineSettings implements Cloneable {
public int pipelineIndex = 0;

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

@@ -0,0 +1,135 @@
/*
* Copyright (C) Photon Vision.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.photonvision.vision.pipeline;
import java.util.List;
import java.util.stream.Collectors;
import org.photonvision.vision.frame.Frame;
import org.photonvision.vision.frame.FrameThresholdType;
import org.photonvision.vision.opencv.DualOffsetValues;
import org.photonvision.vision.pipe.CVPipe.CVPipeResult;
import org.photonvision.vision.pipe.impl.*;
import org.photonvision.vision.pipe.impl.RknnDetectionPipe.RknnDetectionPipeParams;
import org.photonvision.vision.pipeline.result.CVPipelineResult;
import org.photonvision.vision.target.PotentialTarget;
import org.photonvision.vision.target.TargetOrientation;
import org.photonvision.vision.target.TrackedTarget;
public class ObjectDetectionPipeline
extends CVPipeline<CVPipelineResult, ObjectDetectionPipelineSettings> {
private final CalculateFPSPipe calculateFPSPipe = new CalculateFPSPipe();
private final RknnDetectionPipe rknnPipe = new RknnDetectionPipe();
private final SortContoursPipe sortContoursPipe = new SortContoursPipe();
private final Collect2dTargetsPipe collect2dTargetsPipe = new Collect2dTargetsPipe();
private final FilterObjectDetectionsPipe filterContoursPipe = new FilterObjectDetectionsPipe();
private static final FrameThresholdType PROCESSING_TYPE = FrameThresholdType.NONE;
public ObjectDetectionPipeline() {
super(PROCESSING_TYPE);
settings = new ObjectDetectionPipelineSettings();
}
public ObjectDetectionPipeline(ObjectDetectionPipelineSettings settings) {
super(PROCESSING_TYPE);
this.settings = settings;
}
@Override
protected void setPipeParamsImpl() {
// this needs to be based off of the current backend selected!!
var params = new RknnDetectionPipeParams();
params.confidence = settings.confidence;
params.nms = settings.nms;
rknnPipe.setParams(params);
DualOffsetValues dualOffsetValues =
new DualOffsetValues(
settings.offsetDualPointA,
settings.offsetDualPointAArea,
settings.offsetDualPointB,
settings.offsetDualPointBArea);
SortContoursPipe.SortContoursParams sortContoursParams =
new SortContoursPipe.SortContoursParams(
settings.contourSortMode,
settings.outputShowMultipleTargets ? MAX_MULTI_TARGET_RESULTS : 1,
frameStaticProperties);
sortContoursPipe.setParams(sortContoursParams);
var filterContoursParams =
new FilterObjectDetectionsPipe.FilterContoursParams(
settings.contourArea,
settings.contourRatio,
frameStaticProperties,
settings.contourTargetOrientation == TargetOrientation.Landscape);
filterContoursPipe.setParams(filterContoursParams);
Collect2dTargetsPipe.Collect2dTargetsParams collect2dTargetsParams =
new Collect2dTargetsPipe.Collect2dTargetsParams(
settings.offsetRobotOffsetMode,
settings.offsetSinglePoint,
dualOffsetValues,
settings.contourTargetOffsetPointEdge,
settings.contourTargetOrientation,
frameStaticProperties);
collect2dTargetsPipe.setParams(collect2dTargetsParams);
}
@Override
protected CVPipelineResult process(Frame input_frame, ObjectDetectionPipelineSettings settings) {
long sumPipeNanosElapsed = 0;
// ***************** change based on backend ***********************
CVPipeResult<List<NeuralNetworkPipeResult>> rknnResult = rknnPipe.run(input_frame.colorImage);
sumPipeNanosElapsed += rknnResult.nanosElapsed;
List<NeuralNetworkPipeResult> targetList;
var names = rknnPipe.getClassNames();
input_frame.colorImage.getMat().copyTo(input_frame.processedImage.getMat());
// ***************** change based on backend ***********************
var filterContoursResult = filterContoursPipe.run(rknnResult.output);
sumPipeNanosElapsed += filterContoursResult.nanosElapsed;
CVPipeResult<List<PotentialTarget>> sortContoursResult =
sortContoursPipe.run(
filterContoursResult.output.stream()
.map(shape -> new PotentialTarget(shape))
.collect(Collectors.toList()));
sumPipeNanosElapsed += sortContoursResult.nanosElapsed;
CVPipeResult<List<TrackedTarget>> collect2dTargetsResult =
collect2dTargetsPipe.run(sortContoursResult.output);
sumPipeNanosElapsed += collect2dTargetsResult.nanosElapsed;
var fpsResult = calculateFPSPipe.run(null);
var fps = fpsResult.output;
return new CVPipelineResult(
sumPipeNanosElapsed, fps, collect2dTargetsResult.output, input_frame, names);
}
@Override
public void release() {
rknnPipe.release();
}
}

View File

@@ -0,0 +1,34 @@
/*
* Copyright (C) Photon Vision.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.photonvision.vision.pipeline;
public class ObjectDetectionPipelineSettings extends AdvancedPipelineSettings {
public double confidence;
public double nms; // non maximal suppression
public ObjectDetectionPipelineSettings() {
super();
this.pipelineType = PipelineType.ObjectDetection; // TODO: FIX this
this.outputShowMultipleTargets = true;
cameraExposure = 20;
cameraAutoExposure = false;
ledMode = false;
confidence = .9;
nms = .45;
}
}

View File

@@ -17,6 +17,8 @@
package org.photonvision.vision.pipeline;
import org.photonvision.vision.pipe.impl.Calibrate3dPipeline;
@SuppressWarnings("rawtypes")
public enum PipelineType {
Calib3d(-2, Calibrate3dPipeline.class),
@@ -24,7 +26,8 @@ public enum PipelineType {
Reflective(0, ReflectivePipeline.class),
ColoredShape(1, ColoredShapePipeline.class),
AprilTag(2, AprilTagPipeline.class),
Aruco(3, ArucoPipeline.class);
Aruco(3, ArucoPipeline.class),
ObjectDetection(4, ObjectDetectionPipeline.class);
public final int baseIndex;
public final Class clazz;

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

@@ -32,10 +32,20 @@ public class CVPipelineResult implements Releasable {
public final List<TrackedTarget> targets;
public final Frame inputAndOutputFrame;
public MultiTargetPNPResult multiTagResult;
public final List<String> objectDetectionClassNames;
public CVPipelineResult(
double processingNanos, double fps, List<TrackedTarget> targets, Frame inputFrame) {
this(processingNanos, fps, targets, new MultiTargetPNPResult(), inputFrame);
this(processingNanos, fps, targets, new MultiTargetPNPResult(), inputFrame, List.of());
}
public CVPipelineResult(
double processingNanos,
double fps,
List<TrackedTarget> targets,
Frame inputFrame,
List<String> classNames) {
this(processingNanos, fps, targets, new MultiTargetPNPResult(), inputFrame, classNames);
}
public CVPipelineResult(
@@ -44,10 +54,21 @@ public class CVPipelineResult implements Releasable {
List<TrackedTarget> targets,
MultiTargetPNPResult multiTagResult,
Frame inputFrame) {
this(processingNanos, fps, targets, multiTagResult, inputFrame, List.of());
}
public CVPipelineResult(
double processingNanos,
double fps,
List<TrackedTarget> targets,
MultiTargetPNPResult multiTagResult,
Frame inputFrame,
List<String> classNames) {
this.processingNanos = processingNanos;
this.fps = fps;
this.targets = targets != null ? targets : Collections.emptyList();
this.multiTagResult = multiTagResult;
this.objectDetectionClassNames = classNames;
this.inputAndOutputFrame = inputFrame;
}
@@ -57,7 +78,7 @@ public class CVPipelineResult implements Releasable {
double fps,
List<TrackedTarget> targets,
MultiTargetPNPResult multiTagResult) {
this(processingNanos, fps, targets, multiTagResult, null);
this(processingNanos, fps, targets, multiTagResult, null, List.of());
}
public boolean hasTargets() {

View File

@@ -27,6 +27,7 @@ import org.photonvision.common.dataflow.DataChangeService;
import org.photonvision.common.dataflow.events.OutgoingUIEvent;
import org.photonvision.common.logging.LogGroup;
import org.photonvision.common.logging.Logger;
import org.photonvision.vision.pipe.impl.Calibrate3dPipeline;
import org.photonvision.vision.pipeline.*;
@SuppressWarnings({"rawtypes", "unused"})
@@ -41,7 +42,7 @@ public class PipelineManager {
protected final DriverModePipeline driverModePipeline = new DriverModePipeline();
/** Index of the currently active pipeline. Defaults to 0. */
private int currentPipelineIndex = 0;
private int currentPipelineIndex = DRIVERMODE_INDEX;
/** The currently active pipeline. */
private CVPipeline currentUserPipeline = driverModePipeline;
@@ -188,6 +189,11 @@ public class PipelineManager {
return;
}
// Cleanup potential old native resources before swapping over
if (currentUserPipeline != null) {
currentUserPipeline.release();
}
currentPipelineIndex = newIndex;
if (newIndex >= 0) {
var desiredPipelineSettings = userPipelineSettings.get(currentPipelineIndex);
@@ -212,6 +218,11 @@ public class PipelineManager {
logger.debug("Creating Aruco Pipeline");
currentUserPipeline = new ArucoPipeline((ArucoPipelineSettings) desiredPipelineSettings);
break;
case ObjectDetection:
logger.debug("Creating ObjectDetection Pipeline");
currentUserPipeline =
new ObjectDetectionPipeline(
(ObjectDetectionPipelineSettings) desiredPipelineSettings);
default:
// Can be calib3d or drivermode, both of which are special cases
break;
@@ -313,6 +324,12 @@ public class PipelineManager {
added.pipelineNickname = nickname;
return added;
}
case ObjectDetection:
{
var added = new ObjectDetectionPipelineSettings();
added.pipelineNickname = nickname;
return added;
}
default:
{
logger.error("Got invalid pipeline type: " + type);

View File

@@ -512,6 +512,7 @@ public class VisionModule {
SerializationUtils.objectToHashMap(pipelineManager.getCurrentPipelineSettings());
ret.currentPipelineIndex = pipelineManager.getCurrentPipelineIndex();
ret.pipelineNicknames = pipelineManager.getPipelineNicknames();
ret.cameraQuirks = visionSource.getSettables().getConfiguration().cameraQuirks;
// TODO refactor into helper method
var temp = new HashMap<Integer, HashMap<String, Object>>();
@@ -609,4 +610,14 @@ public class VisionModule {
saveAndBroadcastAll();
}
/**
* Add/remove quirks from the camera we're controlling
*
* @param quirksToChange map of true/false for quirks we should change
*/
public void changeCameraQuirks(HashMap<CameraQuirk, Boolean> quirksToChange) {
visionSource.getCameraConfiguration().cameraQuirks.updateQuirks(quirksToChange);
saveAndBroadcastAll();
}
}

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

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

@@ -65,12 +65,18 @@ public class TrackedTarget implements Releasable {
private Mat m_cameraRelativeTvec, m_cameraRelativeRvec;
private int m_classId = -1;
private double m_confidence = -1;
public TrackedTarget(
PotentialTarget origTarget, TargetCalculationParameters params, CVShape shape) {
this.m_mainContour = origTarget.m_mainContour;
this.m_subContours = origTarget.m_subContours;
this.m_shape = shape;
calculateValues(params);
this.m_classId = origTarget.clsId;
this.m_confidence = origTarget.confidence;
}
public TrackedTarget(
@@ -154,6 +160,20 @@ public class TrackedTarget implements Releasable {
m_robotOffsetPoint = new Point();
}
/**
* @return Returns the confidence of the detection ranging from 0 - 1.
*/
public double getConfidence() {
return m_confidence;
}
/**
* @return O-indexed class index for the detected object.
*/
public double getClassID() {
return m_classId;
}
public TrackedTarget(
ArucoDetectionResult result,
AprilTagPoseEstimate tagPose,
@@ -388,6 +408,8 @@ public class TrackedTarget implements Releasable {
ret.put("skew", getSkew());
ret.put("area", getArea());
ret.put("ambiguity", getPoseAmbiguity());
ret.put("confidence", m_confidence);
ret.put("classId", m_classId);
var bestCameraToTarget3d = getBestCameraToTarget3d();
if (bestCameraToTarget3d != null) {

View File

@@ -19,25 +19,58 @@ package org.photonvision.common.configuration;
import static org.junit.jupiter.api.Assertions.assertEquals;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Comparator;
import java.util.List;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Order;
import org.junit.jupiter.api.Test;
import org.photonvision.common.util.TestUtils;
import org.photonvision.vision.camera.CameraType;
import org.photonvision.vision.camera.QuirkyCamera;
import org.photonvision.vision.pipeline.AprilTagPipelineSettings;
import org.photonvision.vision.pipeline.ColoredShapePipelineSettings;
import org.photonvision.vision.pipeline.ReflectivePipelineSettings;
public class SQLConfigTest {
private static Path tmpDir;
@BeforeAll
public static void init() {
TestUtils.loadLibraries();
try {
tmpDir = Files.createTempDirectory("SQLConfigTest");
} catch (IOException e) {
System.out.println("Couldn't create temporary directory, using current directory");
tmpDir = Path.of("jdbc_test", "temp");
}
}
@AfterAll
public static void cleanUp() throws IOException {
Files.walk(tmpDir).sorted(Comparator.reverseOrder()).map(Path::toFile).forEach(File::delete);
}
@Test
@Order(1)
public void testMigration() {
SqlConfigProvider cfgLoader = new SqlConfigProvider(tmpDir);
cfgLoader.load();
assertEquals(
DatabaseSchema.migrations.length,
cfgLoader.getUserVersion(),
"Database isn't at the correct version");
}
@Test
@Order(2)
public void testLoad() {
var cfgLoader = new SqlConfigProvider(Path.of("jdbc_test"));
var cfgLoader = new SqlConfigProvider(tmpDir);
cfgLoader.load();
@@ -49,6 +82,7 @@ public class SQLConfigTest {
69,
"a/path/idk",
CameraType.UsbCamera,
QuirkyCamera.getQuirkyCamera(-1, -1),
List.of(),
0);
testcamcfg.pipelineSettings =

View File

@@ -44,6 +44,7 @@ import org.photonvision.vision.frame.FrameDivisor;
import org.photonvision.vision.frame.FrameStaticProperties;
import org.photonvision.vision.frame.FrameThresholdType;
import org.photonvision.vision.opencv.CVMat;
import org.photonvision.vision.pipe.impl.Calibrate3dPipeline;
public class Calibrate3dPipeTest {
@BeforeAll

View File

@@ -1,9 +1,3 @@
plugins {
id 'edu.wpi.first.WpilibTools' version '1.3.0'
}
apply plugin: "edu.wpi.first.NativeUtils"
import java.nio.file.Path
ext {
@@ -13,9 +7,125 @@ ext {
generatedHeaders = "src/generate/native/include"
}
apply from: "${rootDir}/shared/javacpp/setupBuild.gradle"
apply plugin: 'cpp'
apply plugin: 'google-test-test-suite'
apply plugin: 'edu.wpi.first.NativeUtils'
apply from: "${rootDir}/shared/config.gradle"
apply from: "${rootDir}/shared/javacommon.gradle"
apply from: "${rootDir}/versioningHelper.gradle"
nativeUtils {
exportsConfigs {
"${nativeName}" {}
}
}
model {
components {
"${nativeName}"(NativeLibrarySpec) {
sources {
cpp {
source {
srcDirs 'src/main/native/cpp', "$buildDir/generated/source/proto/main/cpp"
include '**/*.cpp', '**/*.cc'
}
exportedHeaders {
srcDirs 'src/main/native/include', "$buildDir/generated/source/proto/main/cpp"
if (project.hasProperty('generatedHeaders')) {
srcDir generatedHeaders
}
include "**/*.h"
}
}
}
binaries.all {
it.tasks.withType(CppCompile) {
it.dependsOn generateProto
}
if(project.hasProperty('includePhotonTargeting')) {
lib project: ':photon-targeting', library: 'photontargeting', linkage: 'shared'
}
}
nativeUtils.useRequiredLibrary(it, "wpilib_shared")
nativeUtils.useRequiredLibrary(it, "apriltag_shared")
nativeUtils.useRequiredLibrary(it, "opencv_shared")
}
}
testSuites {
"${nativeName}Test"(GoogleTestTestSuiteSpec) {
for(NativeComponentSpec c : $.components) {
if (c.name == nativeName) {
testing c
break
}
}
sources {
cpp {
source {
srcDirs 'src/test/native/cpp'
include '**/*.cpp'
}
exportedHeaders {
srcDirs 'src/test/native/include', "$buildDir/generated/source/proto/main/cpp"
}
}
}
binaries.all {
it.tasks.withType(CppCompile) {
it.dependsOn generateProto
}
if(project.hasProperty('includePhotonTargeting')) {
lib project: ':photon-targeting', library: 'photontargeting', linkage: 'shared'
}
}
nativeUtils.useRequiredLibrary(it, "cscore_shared")
nativeUtils.useRequiredLibrary(it, "cameraserver_shared")
nativeUtils.useRequiredLibrary(it, "wpilib_executable_shared")
nativeUtils.useRequiredLibrary(it, "googletest_static")
nativeUtils.useRequiredLibrary(it, "apriltag_shared")
nativeUtils.useRequiredLibrary(it, "opencv_shared")
}
}
tasks {
def c = $.testSuites
project.tasks.create('runCpp', Exec) {
description = "Run the photon-lib executable"
def found = false
def systemArch = getCurrentArch()
c.each {
if (it in GoogleTestTestSuiteSpec && it.name == "${nativeName}Test") {
it.binaries.each {
if (!found) {
def arch = it.targetPlatform.name
if (arch == systemArch) {
dependsOn it.tasks.install
commandLine it.tasks.install.runScriptFile.get().asFile.toString()
def filePath = it.tasks.install.installDirectory.get().toString() + File.separatorChar + 'lib'
test.dependsOn it.tasks.install
test.systemProperty 'java.library.path', filePath
test.environment 'LD_LIBRARY_PATH', filePath
test.environment 'DYLD_LIBRARY_PATH', filePath
test.workingDir filePath
found = true
}
}
}
}
}
}
}
}
apply from: "${rootDir}/shared/javacpp/publish.gradle"
// Include the version file in the distributed sources
cppHeadersZip {
from('src/generate/native/include') {

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: [PhotonTrackedTarget]
"""A list of the targets used to compute this pose"""
strategy: "PoseStrategy"
"""The strategy actually used to produce this pose"""

View File

@@ -17,6 +17,10 @@ class PNPResult:
def createFromPacket(self, packet: Packet) -> Packet:
self.isPresent = packet.decodeBoolean()
if not self.isPresent:
return packet
self.best = packet.decodeTransform()
self.alt = packet.decodeTransform()
self.bestReprojError = packet.decodeDouble()

View File

@@ -14,7 +14,7 @@ class VisionLEDMode(Enum):
kBlink = 2
lastVersionTimeCheck = 0.0
_lastVersionTimeCheck = 0.0
_VERSION_CHECK_ENABLED = True
@@ -26,41 +26,41 @@ def setVersionCheckEnabled(enabled: bool):
class PhotonCamera:
def __init__(self, cameraName: str):
instance = ntcore.NetworkTableInstance.getDefault()
self.name = cameraName
self._name = cameraName
self._tableName = "photonvision"
photonvision_root_table = instance.getTable(self._tableName)
self.cameraTable = photonvision_root_table.getSubTable(cameraName)
self.path = self.cameraTable.getPath()
self.rawBytesEntry = self.cameraTable.getRawTopic("rawBytes").subscribe(
self._cameraTable = photonvision_root_table.getSubTable(cameraName)
self._path = self._cameraTable.getPath()
self._rawBytesEntry = self._cameraTable.getRawTopic("rawBytes").subscribe(
"rawBytes", bytes([]), ntcore.PubSubOptions(periodic=0.01, sendAll=True)
)
self.driverModePublisher = self.cameraTable.getBooleanTopic(
self._driverModePublisher = self._cameraTable.getBooleanTopic(
"driverModeRequest"
).publish()
self.driverModeSubscriber = self.cameraTable.getBooleanTopic(
self._driverModeSubscriber = self._cameraTable.getBooleanTopic(
"driverMode"
).subscribe(False)
self.inputSaveImgEntry = self.cameraTable.getIntegerTopic(
self._inputSaveImgEntry = self._cameraTable.getIntegerTopic(
"inputSaveImgCmd"
).getEntry(0)
self.outputSaveImgEntry = self.cameraTable.getIntegerTopic(
self._outputSaveImgEntry = self._cameraTable.getIntegerTopic(
"outputSaveImgCmd"
).getEntry(0)
self.pipelineIndexRequest = self.cameraTable.getIntegerTopic(
self._pipelineIndexRequest = self._cameraTable.getIntegerTopic(
"pipelineIndexRequest"
).publish()
self.pipelineIndexState = self.cameraTable.getIntegerTopic(
self._pipelineIndexState = self._cameraTable.getIntegerTopic(
"pipelineIndexState"
).subscribe(0)
self.heartbeatEntry = self.cameraTable.getIntegerTopic("heartbeat").subscribe(
self._heartbeatEntry = self._cameraTable.getIntegerTopic("heartbeat").subscribe(
-1
)
self.ledModeRequest = photonvision_root_table.getIntegerTopic(
self._ledModeRequest = photonvision_root_table.getIntegerTopic(
"ledModeRequest"
).publish()
self.ledModeState = photonvision_root_table.getIntegerTopic(
self._ledModeState = photonvision_root_table.getIntegerTopic(
"ledModeState"
).subscribe(-1)
self.versionEntry = photonvision_root_table.getStringTopic("version").subscribe(
@@ -72,14 +72,14 @@ class PhotonCamera:
instance, ["/photonvision/"], ntcore.PubSubOptions(topicsOnly=True)
)
self.prevHeartbeat = 0
self.prevHeartbeatChangeTime = Timer.getFPGATimestamp()
self._prevHeartbeat = 0
self._prevHeartbeatChangeTime = Timer.getFPGATimestamp()
def getLatestResult(self) -> PhotonPipelineResult:
self._versionCheck()
retVal = PhotonPipelineResult()
packetWithTimestamp = self.rawBytesEntry.getAtomic()
packetWithTimestamp = self._rawBytesEntry.getAtomic()
byteList = packetWithTimestamp.value
timestamp = packetWithTimestamp.time
@@ -94,57 +94,57 @@ class PhotonCamera:
return retVal
def getDriverMode(self) -> bool:
return self.driverModeSubscriber.get()
return self._driverModeSubscriber.get()
def setDriverMode(self, driverMode: bool) -> None:
self.driverModePublisher.set(driverMode)
self._driverModePublisher.set(driverMode)
def takeInputSnapshot(self) -> None:
self.inputSaveImgEntry.set(self.inputSaveImgEntry.get() + 1)
self._inputSaveImgEntry.set(self._inputSaveImgEntry.get() + 1)
def takeOutputSnapshot(self) -> None:
self.outputSaveImgEntry.set(self.outputSaveImgEntry.get() + 1)
self._outputSaveImgEntry.set(self._outputSaveImgEntry.get() + 1)
def getPipelineIndex(self) -> int:
return self.pipelineIndexState.get(0)
return self._pipelineIndexState.get(0)
def setPipelineIndex(self, index: int) -> None:
self.pipelineIndexRequest.set(index)
self._pipelineIndexRequest.set(index)
def getLEDMode(self) -> VisionLEDMode:
mode = self.ledModeState.get()
mode = self._ledModeState.get()
return VisionLEDMode(mode)
def setLEDMode(self, led: VisionLEDMode) -> None:
self.ledModeRequest.set(led.value)
self._ledModeRequest.set(led.value)
def getName(self) -> str:
return self.name
return self._name
def isConnected(self) -> bool:
curHeartbeat = self.heartbeatEntry.get()
curHeartbeat = self._heartbeatEntry.get()
now = Timer.getFPGATimestamp()
if curHeartbeat != self.prevHeartbeat:
self.prevHeartbeat = curHeartbeat
self.prevHeartbeatChangeTime = now
if curHeartbeat != self._prevHeartbeat:
self._prevHeartbeat = curHeartbeat
self._prevHeartbeatChangeTime = now
return (now - self.prevHeartbeatChangeTime) < 0.5
return (now - self._prevHeartbeatChangeTime) < 0.5
def _versionCheck(self) -> None:
global lastVersionTimeCheck
global _lastVersionTimeCheck
if not _VERSION_CHECK_ENABLED:
return
if (Timer.getFPGATimestamp() - lastVersionTimeCheck) < 5.0:
if (Timer.getFPGATimestamp() - _lastVersionTimeCheck) < 5.0:
return
lastVersionTimeCheck = Timer.getFPGATimestamp()
_lastVersionTimeCheck = Timer.getFPGATimestamp()
if not self.heartbeatEntry.exists():
if not self._heartbeatEntry.exists():
cameraNames = (
self.cameraTable.getInstance().getTable(self._tableName).getSubTables()
self._cameraTable.getInstance().getTable(self._tableName).getSubTables()
)
if len(cameraNames) == 0:
wpilib.reportError(
@@ -153,13 +153,13 @@ class PhotonCamera:
)
else:
wpilib.reportError(
f"PhotonVision coprocessor at path {self.path} not found in Network Tables. Double check that your camera names match! Only the following camera names were found: { ''.join(cameraNames)}",
f"PhotonVision coprocessor at path {self._path} not found in Network Tables. Double check that your camera names match! Only the following camera names were found: { ''.join(cameraNames)}",
True,
)
elif not self.isConnected():
wpilib.reportWarning(
f"PhotonVision coprocessor at path {self.path} is not sending new data.",
f"PhotonVision coprocessor at path {self._path} is not sending new data.",
True,
)

View File

@@ -15,14 +15,16 @@ class PhotonPipelineResult:
def populateFromPacket(self, packet: Packet) -> Packet:
self.targets = []
self.latencyMillis = packet.decodeDouble()
self.multiTagResult = MultiTargetPNPResult()
self.multiTagResult.createFromPacket(packet)
targetCount = packet.decode8()
for _ in range(targetCount):
target = PhotonTrackedTarget()
target.createFromPacket(packet)
self.targets.append(target)
self.multiTagResult = MultiTargetPNPResult()
self.multiTagResult.createFromPacket(packet)
return packet
def setTimestampSeconds(self, timestampSec: float) -> None:

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
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) -> 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) -> 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):
self._poseCacheTimestampSeconds = -1
def _checkUpdate(self, oldObj, newObj):
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
cameraResult = self._camera.getLatestResult()
if cameraResult.timestampSec < 0:
return
# If the pose cache timestamp was set, and the result is from the same
# timestamp, return an
# empty result
if (
self._poseCacheTimestampSeconds > 0
and abs(self._poseCacheTimestampSeconds - cameraResult.timestampSec) < 1e-6
):
return
# 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
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
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
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
targetFiducialId = lowestAmbiguityTarget.fiducialId
targetPosition = self._fieldTags.getTagPose(targetFiducialId)
if not targetPosition:
self._reportFiducialPoseError(targetFiducialId)
return
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

@@ -16,10 +16,24 @@ m = re.search(
# which should be PEP440 compliant
if m:
versionString = m.group(0)
prefix = m.group(1)
maturity = m.group(2)
suffix = m.group(3).replace(".", "")
versionString = f"{prefix}.{maturity}.{suffix}"
# Hack -- for strings like v2024.1.1, do NOT add matruity/suffix
if len(m.group(2)) > 0:
print("using beta group matcher")
prefix = m.group(1)
maturity = m.group(2)
suffix = m.group(3).replace(".", "")
versionString = f"{prefix}.{maturity}.{suffix}"
else:
split = gitDescribeResult.split("-")
if len(split) == 3:
year, commits, sha = split
# Chop off leading v from "v2024.1.2", and use "post" for commits to master since
versionString = f"{year[1:]}post{commits}"
print("using dev release " + versionString)
else:
year = gitDescribeResult
versionString = year[1:]
print("using full release " + versionString)
else:
@@ -46,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,

File diff suppressed because one or more lines are too long

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

@@ -6,6 +6,9 @@ from data import rawBytes3
from data import rawBytes4
from data import rawBytes5
from data import rawBytes6
from data import rawBytes7
from data import rawBytes8
from data import rawBytes9
def setupCommon(bytesIn):
@@ -28,7 +31,7 @@ def test_byteParse2():
def test_byteParse3():
res = setupCommon(rawBytes3)
assert len(res.getTargets()) == 0
assert len(res.getTargets()) >= 4
def test_byteParse4():
@@ -38,9 +41,24 @@ def test_byteParse4():
def test_byteParse5():
res = setupCommon(rawBytes5)
assert len(res.getTargets()) == 1
assert len(res.getTargets()) == 2
def test_byteParse6():
res = setupCommon(rawBytes6)
assert len(res.getTargets()) > 6
# assert len(res.getTargets()) >= 0
def test_byteParse7():
res = setupCommon(rawBytes7)
# assert len(res.getTargets()) >= 0
def test_byteParse8():
res = setupCommon(rawBytes8)
# assert len(res.getTargets()) >= 0
def test_byteParse9():
res = setupCommon(rawBytes9)
# assert len(res.getTargets()) >= 0

View File

@@ -8,7 +8,7 @@
"https://maven.photonvision.org/repository/internal",
"https://maven.photonvision.org/repository/snapshots"
],
"jsonUrl": "https://maven.photonvision.org/repository/internal/org/photonvision/PhotonLib-json/1.0/PhotonLib-json-1.0.json",
"jsonUrl": "https://maven.photonvision.org/repository/internal/org/photonvision/photonlib-json/1.0/photonlib-json-1.0.json",
"jniDependencies": [],
"cppDependencies": [
{

View File

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

@@ -26,31 +26,23 @@ package org.photonvision;
import static org.junit.jupiter.api.Assertions.*;
import edu.wpi.first.apriltag.jni.AprilTagJNI;
import edu.wpi.first.cscore.CameraServerCvJNI;
import edu.wpi.first.cscore.CameraServerJNI;
import edu.wpi.first.hal.JNIWrapper;
import edu.wpi.first.math.MathUtil;
import edu.wpi.first.math.geometry.Pose3d;
import edu.wpi.first.math.geometry.Rotation2d;
import edu.wpi.first.math.geometry.Rotation3d;
import edu.wpi.first.math.geometry.Transform3d;
import edu.wpi.first.math.geometry.Translation3d;
import edu.wpi.first.net.WPINetJNI;
import edu.wpi.first.networktables.NetworkTableInstance;
import edu.wpi.first.networktables.NetworkTablesJNI;
import edu.wpi.first.util.CombinedRuntimeLoader;
import edu.wpi.first.util.WPIUtilJNI;
import java.io.IOException;
import java.util.List;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.opencv.core.Core;
import org.photonvision.estimation.CameraTargetRelation;
import org.photonvision.estimation.OpenCVHelp;
import org.photonvision.estimation.RotTrlTransform3d;
import org.photonvision.estimation.TargetModel;
import org.photonvision.simulation.SimCameraProperties;
import org.photonvision.simulation.VisionSystemSim;
import org.photonvision.simulation.VisionTargetSim;
public class OpenCVTest {
@@ -84,28 +76,8 @@ public class OpenCVTest {
private static final SimCameraProperties prop = new SimCameraProperties();
@BeforeAll
public static void setUp() {
JNIWrapper.Helper.setExtractOnStaticLoad(false);
WPIUtilJNI.Helper.setExtractOnStaticLoad(false);
NetworkTablesJNI.Helper.setExtractOnStaticLoad(false);
WPINetJNI.Helper.setExtractOnStaticLoad(false);
CameraServerJNI.Helper.setExtractOnStaticLoad(false);
CameraServerCvJNI.Helper.setExtractOnStaticLoad(false);
AprilTagJNI.Helper.setExtractOnStaticLoad(false);
try {
CombinedRuntimeLoader.loadLibraries(
VisionSystemSim.class,
"wpiutiljni",
"ntcorejni",
"wpinetjni",
"wpiHaljni",
Core.NATIVE_LIBRARY_NAME,
"cscorejni",
"apriltagjni");
} catch (Exception e) {
e.printStackTrace();
}
public static void setUp() throws IOException {
CameraServerCvJNI.forceLoad();
// NT live for debug purposes
NetworkTableInstance.getDefault().startServer();

View File

@@ -30,17 +30,11 @@ import static org.junit.jupiter.api.Assertions.assertTrue;
import edu.wpi.first.apriltag.AprilTag;
import edu.wpi.first.apriltag.AprilTagFieldLayout;
import edu.wpi.first.hal.JNIWrapper;
import edu.wpi.first.math.geometry.Pose3d;
import edu.wpi.first.math.geometry.Rotation3d;
import edu.wpi.first.math.geometry.Transform3d;
import edu.wpi.first.math.geometry.Translation3d;
import edu.wpi.first.math.util.Units;
import edu.wpi.first.net.WPINetJNI;
import edu.wpi.first.networktables.NetworkTablesJNI;
import edu.wpi.first.util.CombinedRuntimeLoader;
import edu.wpi.first.util.WPIUtilJNI;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
@@ -56,19 +50,6 @@ class PhotonPoseEstimatorTest {
@BeforeAll
public static void init() {
JNIWrapper.Helper.setExtractOnStaticLoad(false);
WPIUtilJNI.Helper.setExtractOnStaticLoad(false);
NetworkTablesJNI.Helper.setExtractOnStaticLoad(false);
WPINetJNI.Helper.setExtractOnStaticLoad(false);
try {
CombinedRuntimeLoader.loadLibraries(
PhotonPoseEstimatorTest.class, "wpiutiljni", "ntcorejni", "wpinetjni", "wpiHaljni");
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
List<AprilTag> tagList = new ArrayList<>(2);
tagList.add(new AprilTag(0, new Pose3d(3, 3, 3, new Rotation3d())));
tagList.add(new AprilTag(1, new Pose3d(5, 5, 5, new Rotation3d())));

View File

@@ -30,10 +30,6 @@ import static org.junit.jupiter.api.Assertions.assertTrue;
import edu.wpi.first.apriltag.AprilTag;
import edu.wpi.first.apriltag.AprilTagFieldLayout;
import edu.wpi.first.apriltag.jni.AprilTagJNI;
import edu.wpi.first.cscore.CameraServerCvJNI;
import edu.wpi.first.cscore.CameraServerJNI;
import edu.wpi.first.hal.JNIWrapper;
import edu.wpi.first.math.geometry.Pose2d;
import edu.wpi.first.math.geometry.Pose3d;
import edu.wpi.first.math.geometry.Rotation2d;
@@ -42,11 +38,7 @@ import edu.wpi.first.math.geometry.Transform3d;
import edu.wpi.first.math.geometry.Translation2d;
import edu.wpi.first.math.geometry.Translation3d;
import edu.wpi.first.math.util.Units;
import edu.wpi.first.net.WPINetJNI;
import edu.wpi.first.networktables.NetworkTableInstance;
import edu.wpi.first.networktables.NetworkTablesJNI;
import edu.wpi.first.util.CombinedRuntimeLoader;
import edu.wpi.first.util.WPIUtilJNI;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Stream;
@@ -58,7 +50,6 @@ import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import org.junit.jupiter.params.provider.ValueSource;
import org.opencv.core.Core;
import org.photonvision.estimation.TargetModel;
import org.photonvision.estimation.VisionEstimation;
import org.photonvision.simulation.PhotonCameraSim;
@@ -85,28 +76,6 @@ class VisionSystemSimTest {
@BeforeAll
public static void setUp() {
JNIWrapper.Helper.setExtractOnStaticLoad(false);
WPIUtilJNI.Helper.setExtractOnStaticLoad(false);
NetworkTablesJNI.Helper.setExtractOnStaticLoad(false);
WPINetJNI.Helper.setExtractOnStaticLoad(false);
CameraServerJNI.Helper.setExtractOnStaticLoad(false);
CameraServerCvJNI.Helper.setExtractOnStaticLoad(false);
AprilTagJNI.Helper.setExtractOnStaticLoad(false);
try {
CombinedRuntimeLoader.loadLibraries(
VisionSystemSim.class,
"wpiutiljni",
"ntcorejni",
"wpinetjni",
"wpiHaljni",
Core.NATIVE_LIBRARY_NAME,
"cscorejni",
"apriltagjni");
} catch (Exception e) {
e.printStackTrace();
}
// NT live for debug purposes
NetworkTableInstance.getDefault().startServer();

View File

@@ -26,14 +26,8 @@ package org.photonvision.estimation;
import edu.wpi.first.apriltag.AprilTagFieldLayout;
import edu.wpi.first.apriltag.AprilTagFields;
import edu.wpi.first.cscore.CameraServerCvJNI;
import edu.wpi.first.hal.JNIWrapper;
import edu.wpi.first.math.geometry.Transform3d;
import edu.wpi.first.net.WPINetJNI;
import edu.wpi.first.networktables.NetworkTableInstance;
import edu.wpi.first.networktables.NetworkTablesJNI;
import edu.wpi.first.util.CombinedRuntimeLoader;
import edu.wpi.first.util.WPIUtilJNI;
import edu.wpi.first.wpilibj.smartdashboard.Field2d;
import edu.wpi.first.wpilibj.smartdashboard.SmartDashboard;
import java.io.IOException;
@@ -44,25 +38,6 @@ import org.photonvision.PhotonPoseEstimator;
public class ApriltagWorkbenchTest {
@BeforeAll
public static void setUp() {
JNIWrapper.Helper.setExtractOnStaticLoad(false);
WPIUtilJNI.Helper.setExtractOnStaticLoad(false);
NetworkTablesJNI.Helper.setExtractOnStaticLoad(false);
WPINetJNI.Helper.setExtractOnStaticLoad(false);
CameraServerCvJNI.Helper.setExtractOnStaticLoad(false);
try {
CombinedRuntimeLoader.loadLibraries(
ApriltagWorkbenchTest.class,
"wpiutiljni",
"ntcorejni",
"wpinetjni",
"wpiHaljni",
"cscorejnicvstatic");
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
// No version check for testing
PhotonCamera.setVersionCheckEnabled(false);
}

View File

@@ -27,6 +27,7 @@ import java.util.stream.Collectors;
import org.apache.commons.cli.*;
import org.photonvision.common.configuration.CameraConfiguration;
import org.photonvision.common.configuration.ConfigManager;
import org.photonvision.common.configuration.NeuralNetworkModelManager;
import org.photonvision.common.dataflow.networktables.NetworkTablesManager;
import org.photonvision.common.hardware.HardwareManager;
import org.photonvision.common.hardware.PiVersion;
@@ -37,9 +38,11 @@ import org.photonvision.common.logging.Logger;
import org.photonvision.common.networking.NetworkManager;
import org.photonvision.common.util.TestUtils;
import org.photonvision.common.util.numbers.IntegerCouple;
import org.photonvision.jni.RknnDetectorJNI;
import org.photonvision.mrcal.MrCalJNILoader;
import org.photonvision.raspi.LibCameraJNILoader;
import org.photonvision.server.Server;
import org.photonvision.vision.apriltag.AprilTagFamily;
import org.photonvision.vision.camera.FileVisionSource;
import org.photonvision.vision.opencv.CVMat;
import org.photonvision.vision.opencv.ContourGroupingMode;
@@ -261,6 +264,34 @@ public class Main {
camConf2023.pipelineSettings = psList2023;
}
CameraConfiguration camConf2024 =
ConfigManager.getInstance().getConfig().getCameraConfigurations().get("WPI2024");
if (camConf2024 == null || true) {
camConf2024 =
new CameraConfiguration(
"WPI2024",
TestUtils.getResourcesFolderPath(true)
.resolve("testimages")
.resolve(TestUtils.WPI2024Images.kSpeakerCenter_143in.path)
.toString());
camConf2024.FOV = TestUtils.WPI2024Images.FOV;
// same camera as 2023
camConf2024.calibrations.add(TestUtils.get2023LifeCamCoeffs(true));
var pipeline2024 = new AprilTagPipelineSettings();
var path_split = Path.of(camConf2024.path).getFileName().toString();
pipeline2024.pipelineNickname = path_split.replace(".jpg", "");
pipeline2024.targetModel = TargetModel.kAprilTag6p5in_36h11;
pipeline2024.tagFamily = AprilTagFamily.kTag36h11;
pipeline2024.inputShouldShow = true;
pipeline2024.solvePNPEnabled = true;
var psList2024 = new ArrayList<CVPipelineSettings>();
psList2024.add(pipeline2024);
camConf2024.pipelineSettings = psList2024;
}
// Colored shape testing
var camConfShape =
ConfigManager.getInstance().getConfig().getCameraConfigurations().get("Shape");
@@ -290,12 +321,14 @@ public class Main {
var fvs2020 = new FileVisionSource(camConf2020);
var fvs2022 = new FileVisionSource(camConf2022);
var fvs2023 = new FileVisionSource(camConf2023);
var fvs2024 = new FileVisionSource(camConf2024);
collectedSources.add(fvs2023);
collectedSources.add(fvs2022);
collectedSources.add(fvsShape);
collectedSources.add(fvs2020);
collectedSources.add(fvs2019);
collectedSources.add(fvs2024);
// collectedSources.add(fvs2023);
// collectedSources.add(fvs2022);
// collectedSources.add(fvsShape);
// collectedSources.add(fvs2020);
// collectedSources.add(fvs2019);
ConfigManager.getInstance().unloadCameraConfigs();
VisionModuleManager.getInstance().addSources(collectedSources).forEach(VisionModule::start);
@@ -317,7 +350,15 @@ public class Main {
} catch (IOException e) {
logger.error("Failed to load libcamera-JNI!", e);
}
try {
if (Platform.isRK3588()) {
RknnDetectorJNI.forceLoad();
} else {
logger.error("Platform does not support RKNN based machine learning!");
}
} catch (IOException e) {
logger.error("Failed to load rknn-JNI!", e);
}
try {
MrCalJNILoader.forceLoad();
} catch (IOException e) {
@@ -333,7 +374,6 @@ public class Main {
} catch (ParseException e) {
logger.error("Failed to parse command-line options!", e);
}
CVMat.enablePrint(false);
PipelineProfiler.enablePrint(false);
@@ -368,6 +408,10 @@ public class Main {
NetworkTablesManager.getInstance()
.setConfig(ConfigManager.getInstance().getConfig().getNetworkConfig());
logger.info("Loading ML models");
NeuralNetworkModelManager.getInstance()
.initialize(ConfigManager.getInstance().getModelsDirectory());
if (!isTestMode) {
logger.debug("Loading VisionSourceManager...");
VisionSourceManager.getInstance()
@@ -385,6 +429,6 @@ public class Main {
logger.info("Starting server...");
HardwareManager.getInstance().setRunning(true);
Server.start(DEFAULT_WEBPORT);
Server.initialize(DEFAULT_WEBPORT);
}
}

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