mirror of
https://github.com/PhotonVision/photonvision
synced 2026-06-25 01:41:40 +00:00
Compare commits
39 Commits
v2025.0.0-
...
v2025.0.0-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
81076375b8 | ||
|
|
66f369f3a9 | ||
|
|
7cba7b432d | ||
|
|
dd98d96d7e | ||
|
|
8ede892c14 | ||
|
|
08c62ab8cd | ||
|
|
e8efef476b | ||
|
|
c6403a65d2 | ||
|
|
6a8d638853 | ||
|
|
782929b006 | ||
|
|
4997ad9115 | ||
|
|
857a30d980 | ||
|
|
a40e69cca0 | ||
|
|
e069a79a32 | ||
|
|
d9dfe15bfe | ||
|
|
1dbd2e5990 | ||
|
|
7e9da4133d | ||
|
|
163b5c9c81 | ||
|
|
c6a3638a2f | ||
|
|
44f78cb03e | ||
|
|
61552ad6ca | ||
|
|
fa66ed866c | ||
|
|
08b4bd1f03 | ||
|
|
c536a1c312 | ||
|
|
adb18fe711 | ||
|
|
7d1e748b0e | ||
|
|
3a9d22c76b | ||
|
|
417e1a65b6 | ||
|
|
5762167186 | ||
|
|
faa9eb0093 | ||
|
|
005363c5cd | ||
|
|
478723ca2c | ||
|
|
05dcfa2a13 | ||
|
|
eff95c09f1 | ||
|
|
097e641789 | ||
|
|
f107c94d05 | ||
|
|
93242edc86 | ||
|
|
eb395414ab | ||
|
|
04191efc51 |
57
.github/workflows/build.yml
vendored
57
.github/workflows/build.yml
vendored
@@ -6,16 +6,9 @@ on:
|
||||
branches: [ master ]
|
||||
tags:
|
||||
- 'v*'
|
||||
paths:
|
||||
- '**'
|
||||
- '!docs/**'
|
||||
- '.github/**'
|
||||
pull_request:
|
||||
branches: [ master ]
|
||||
paths:
|
||||
- '**'
|
||||
- '!docs/**'
|
||||
- '.github/**'
|
||||
merge_group:
|
||||
|
||||
jobs:
|
||||
build-client:
|
||||
@@ -68,23 +61,14 @@ jobs:
|
||||
- name: Install RoboRIO Toolchain
|
||||
run: ./gradlew installRoboRioToolchain
|
||||
# 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 photon-targeting:publishtomavenlocal photon-lib:publishtomavenlocal -x check
|
||||
run: ./gradlew photon-targeting:publishtomavenlocal photon-lib:publishtomavenlocal -x check
|
||||
- name: Build Java examples
|
||||
working-directory: photonlib-java-examples
|
||||
run: |
|
||||
chmod +x gradlew
|
||||
./gradlew copyPhotonlib -x check
|
||||
./gradlew build
|
||||
run: ./gradlew build
|
||||
- name: Build C++ examples
|
||||
working-directory: photonlib-cpp-examples
|
||||
run: |
|
||||
chmod +x gradlew
|
||||
./gradlew copyPhotonlib -x check
|
||||
./gradlew build
|
||||
run: ./gradlew build
|
||||
build-gradle:
|
||||
name: "Gradle Build"
|
||||
runs-on: ubuntu-22.04
|
||||
@@ -104,9 +88,7 @@ jobs:
|
||||
- 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 photon-targeting:build photon-core:build photon-server:build -x check
|
||||
run: ./gradlew photon-targeting:build photon-core:build photon-server:build -x check
|
||||
- name: Gradle Tests
|
||||
run: ./gradlew testHeadless -i --stacktrace
|
||||
- name: Gradle Coverage
|
||||
@@ -165,7 +147,6 @@ jobs:
|
||||
|
||||
# Generate the JSON and give it the ""standard""" name maven gives it
|
||||
- run: |
|
||||
chmod +x gradlew
|
||||
./gradlew photon-lib:generateVendorJson
|
||||
export VERSION=$(git describe --tags --match=v*)
|
||||
mv photon-lib/build/generated/vendordeps/photonlib.json photon-lib/build/generated/vendordeps/photonlib-$(git describe --tags --match=v*).json
|
||||
@@ -205,9 +186,7 @@ jobs:
|
||||
distribution: temurin
|
||||
architecture: ${{ matrix.architecture }}
|
||||
- run: git fetch --tags --force
|
||||
- run: |
|
||||
chmod +x gradlew
|
||||
./gradlew photon-targeting:build photon-lib:build -i
|
||||
- run: ./gradlew photon-targeting:build photon-lib:build -i
|
||||
name: Build with Gradle
|
||||
- run: ./gradlew photon-lib:publish photon-targeting:publish
|
||||
name: Publish
|
||||
@@ -248,13 +227,9 @@ jobs:
|
||||
git config --global --add safe.directory /__w/photonvision/photonvision
|
||||
- name: Build PhotonLib
|
||||
# We don't need to run tests, since we specify only non-native platforms
|
||||
run: |
|
||||
chmod +x gradlew
|
||||
./gradlew photon-targeting:build photon-lib:build ${{ matrix.build-options }} -i -x test
|
||||
run: ./gradlew photon-targeting:build photon-lib:build ${{ matrix.build-options }} -i -x test
|
||||
- name: Publish
|
||||
run: |
|
||||
chmod +x gradlew
|
||||
./gradlew photon-lib:publish photon-targeting:publish ${{ matrix.build-options }}
|
||||
run: ./gradlew photon-lib:publish photon-targeting:publish ${{ matrix.build-options }}
|
||||
env:
|
||||
ARTIFACTORY_API_KEY: ${{ secrets.ARTIFACTORY_API_KEY }}
|
||||
if: github.event_name == 'push' && github.repository_owner == 'photonvision'
|
||||
@@ -350,13 +325,9 @@ jobs:
|
||||
with:
|
||||
name: built-docs
|
||||
path: photon-server/src/main/resources/web/docs
|
||||
- run: |
|
||||
chmod +x gradlew
|
||||
./gradlew photon-targeting:jar photon-server:shadowJar -PArchOverride=${{ matrix.arch-override }}
|
||||
- run: ./gradlew photon-targeting:jar photon-server:shadowJar -PArchOverride=${{ matrix.arch-override }}
|
||||
if: ${{ (matrix.arch-override != 'none') }}
|
||||
- run: |
|
||||
chmod +x gradlew
|
||||
./gradlew photon-server:shadowJar
|
||||
- run: ./gradlew photon-server:shadowJar
|
||||
if: ${{ (matrix.arch-override == 'none') }}
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
@@ -500,6 +471,12 @@ jobs:
|
||||
image_url: https://github.com/PhotonVision/photon-image-modifier/releases/download/v2025.0.0-beta-6/photonvision_opi5max.img.xz
|
||||
cpu: cortex-a8
|
||||
image_additional_mb: 1024
|
||||
- os: ubuntu-22.04
|
||||
artifact-name: LinuxArm64
|
||||
image_suffix: rock5c
|
||||
image_url: https://github.com/PhotonVision/photon-image-modifier/releases/download/v2025.0.0-beta-7/photonvision_rock5c.img.xz
|
||||
cpu: cortex-a8
|
||||
image_additional_mb: 1024
|
||||
|
||||
runs-on: ${{ matrix.os }}
|
||||
name: "Build image - ${{ matrix.image_url }}"
|
||||
@@ -581,6 +558,7 @@ jobs:
|
||||
with:
|
||||
files: |
|
||||
**/*orangepi5*.xz
|
||||
**/*rock5*.xz
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
@@ -588,6 +566,7 @@ jobs:
|
||||
with:
|
||||
files: |
|
||||
**/!(*orangepi5*).xz
|
||||
**/!(*rock5*).xz
|
||||
**/*.jar
|
||||
**/photonlib*.json
|
||||
**/photonlib*.zip
|
||||
|
||||
13
.github/workflows/lint-format.yml
vendored
13
.github/workflows/lint-format.yml
vendored
@@ -6,16 +6,9 @@ on:
|
||||
branches: [ master ]
|
||||
tags:
|
||||
- 'v*'
|
||||
paths:
|
||||
- '**'
|
||||
- '!docs/**'
|
||||
- '.github/**'
|
||||
pull_request:
|
||||
branches: [ master ]
|
||||
paths:
|
||||
- '**'
|
||||
- '!docs/**'
|
||||
- '.github/**'
|
||||
merge_group:
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.head_ref || github.ref }}
|
||||
@@ -61,9 +54,7 @@ jobs:
|
||||
with:
|
||||
java-version: 17
|
||||
distribution: temurin
|
||||
- run: |
|
||||
chmod +x gradlew
|
||||
./gradlew spotlessCheck
|
||||
- run: ./gradlew spotlessCheck
|
||||
name: Run spotless
|
||||
|
||||
client-lint-format:
|
||||
|
||||
9
.github/workflows/photon-code-docs.yml
vendored
9
.github/workflows/photon-code-docs.yml
vendored
@@ -6,16 +6,9 @@ on:
|
||||
branches: [ master ]
|
||||
tags:
|
||||
- 'v*'
|
||||
paths:
|
||||
- '**'
|
||||
- '!docs/**'
|
||||
- '.github/**'
|
||||
pull_request:
|
||||
branches: [ master ]
|
||||
paths:
|
||||
- '**'
|
||||
- '!docs/**'
|
||||
- '.github/**'
|
||||
merge_group:
|
||||
|
||||
# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages
|
||||
permissions:
|
||||
|
||||
10
.github/workflows/photonvision-docs.yml
vendored
10
.github/workflows/photonvision-docs.yml
vendored
@@ -3,14 +3,12 @@ name: PhotonVision Sphinx Documentation Checks
|
||||
on:
|
||||
push:
|
||||
branches: [ master ]
|
||||
paths:
|
||||
- 'docs/**'
|
||||
- '.github/**'
|
||||
pull_request:
|
||||
branches: [ master ]
|
||||
paths:
|
||||
- 'docs/**'
|
||||
- '.github/**'
|
||||
merge_group:
|
||||
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
jobs:
|
||||
build:
|
||||
|
||||
9
.github/workflows/python.yml
vendored
9
.github/workflows/python.yml
vendored
@@ -8,16 +8,9 @@ on:
|
||||
branches: [ master ]
|
||||
tags:
|
||||
- 'v*'
|
||||
paths:
|
||||
- '**'
|
||||
- '!docs/**'
|
||||
- '.github/**'
|
||||
pull_request:
|
||||
branches: [ master ]
|
||||
paths:
|
||||
- '**'
|
||||
- '!docs/**'
|
||||
- '.github/**'
|
||||
merge_group:
|
||||
|
||||
jobs:
|
||||
buildAndDeploy:
|
||||
|
||||
24
.gitignore
vendored
24
.gitignore
vendored
@@ -131,27 +131,12 @@ New client/photon-client/*
|
||||
*.jfr
|
||||
.DS_Store
|
||||
# *.iml
|
||||
photon-server/build
|
||||
photon-server/photon-vision
|
||||
photon-server/src/main/resources/web
|
||||
photon-server/src/main/java/org/photonvision/PhotonVersion.java
|
||||
photon-server/src/main/generated/native/include/org_photonvision_raspi_PicamJNI.h
|
||||
*.bin
|
||||
.gradle
|
||||
.gradle/*
|
||||
photonvision_config
|
||||
build/spotlessJava
|
||||
build/*
|
||||
build
|
||||
photon-lib/src/main/java/org/photonvision/PhotonVersion.java
|
||||
photon-lib/bin/main/images/*
|
||||
/photonlib-java-examples/bin/
|
||||
photon-lib/src/generate/native/include/PhotonVersion.h
|
||||
.gitattributes
|
||||
lib/*
|
||||
photon-server/lib/libapriltag.so
|
||||
photon-server/bin/main/nativelibraries/apriltag/*
|
||||
photon-server/src/main/resources/nativelibraries/apriltag/*
|
||||
bin*/
|
||||
build*/
|
||||
|
||||
photonlib-java-examples/*/vendordeps/*
|
||||
photonlib-cpp-examples/*/vendordeps/*
|
||||
@@ -161,10 +146,7 @@ photonlib-cpp-examples/*/vendordeps/*
|
||||
photonlib-cpp-examples/*/networktables.json.bck
|
||||
photonlib-java-examples/*/networktables.json.bck
|
||||
*.sqlite
|
||||
photon-server/src/main/resources/web/index.html
|
||||
photon-lib/src/generate/native/cpp/PhotonVersion.cpp
|
||||
|
||||
photon-server/src/main/resources/web/*
|
||||
venv
|
||||
|
||||
.venv/*
|
||||
.venv
|
||||
|
||||
@@ -20,6 +20,8 @@ modifiableFileExclude {
|
||||
\.ico$
|
||||
\.rknn$
|
||||
gradlew
|
||||
photon-lib/py/photonlibpy/generated/
|
||||
photon-targeting/src/generated/
|
||||
}
|
||||
|
||||
includeProject {
|
||||
|
||||
@@ -17,7 +17,7 @@ If you are interested in contributing code or documentation to the project, plea
|
||||
## Documentation
|
||||
|
||||
- Our main documentation page: [docs.photonvision.org](https://docs.photonvision.org)
|
||||
- Photon UI demo: [demo.photonvision.org](https://demo.photonvision.org) (or [manual link](https://photonvision.github.io/photonvision/built-client/))
|
||||
- Photon UI demo: [http://photonvision.global/](http://photonvision.global/) (or [manual link](https://photonvision.github.io/photonvision/built-client/))
|
||||
- Javadocs: [javadocs.photonvision.org](https://javadocs.photonvision.org) (or [manual link](https://photonvision.github.io/photonvision/built-docs/javadoc/))
|
||||
- C++ Doxygen [cppdocs.photonvision.org](https://cppdocs.photonvision.org) (or [manual link](https://photonvision.github.io/photonvision/built-docs/doxygen/html/))
|
||||
|
||||
|
||||
18
build.gradle
18
build.gradle
@@ -5,7 +5,7 @@ plugins {
|
||||
id "cpp"
|
||||
id "com.diffplug.spotless" version "6.24.0"
|
||||
id "edu.wpi.first.wpilib.repositories.WPILibRepositoriesPlugin" version "2020.2"
|
||||
id "edu.wpi.first.GradleRIO" version "2025.1.1-beta-1"
|
||||
id "edu.wpi.first.GradleRIO" version "2025.1.1-beta-3"
|
||||
id 'edu.wpi.first.WpilibTools' version '1.3.0'
|
||||
id 'com.google.protobuf' version '0.9.3' apply false
|
||||
id 'edu.wpi.first.GradleJni' version '1.1.0'
|
||||
@@ -30,16 +30,16 @@ ext.allOutputsFolder = file("$project.buildDir/outputs")
|
||||
apply from: "versioningHelper.gradle"
|
||||
|
||||
ext {
|
||||
wpilibVersion = "2025.1.1-beta-1"
|
||||
wpilibVersion = "2025.1.1-beta-3"
|
||||
wpimathVersion = wpilibVersion
|
||||
openCVYear = "2024"
|
||||
openCVversion = "4.8.0-4"
|
||||
openCVYear = "2025"
|
||||
openCVversion = "4.10.0-3"
|
||||
joglVersion = "2.4.0"
|
||||
javalinVersion = "5.6.2"
|
||||
libcameraDriverVersion = "dev-v2023.1.0-15-gc8988b3"
|
||||
rknnVersion = "dev-v2024.0.1-4-g0db16ac"
|
||||
libcameraDriverVersion = "v2025.0.0"
|
||||
rknnVersion = "v2025.0.0"
|
||||
frcYear = "2025"
|
||||
mrcalVersion = "dev-v2024.0.0-24-gc1efcf0";
|
||||
mrcalVersion = "v2025.0.0";
|
||||
|
||||
|
||||
pubVersion = versionString
|
||||
@@ -67,7 +67,7 @@ spotless {
|
||||
java {
|
||||
target fileTree('.') {
|
||||
include '**/*.java'
|
||||
exclude '**/build/**', '**/build-*/**', "photon-core\\src\\main\\java\\org\\photonvision\\PhotonVersion.java", "photon-lib\\src\\main\\java\\org\\photonvision\\PhotonVersion.java", "**/src/generated/**"
|
||||
exclude '**/build/**', '**/build-*/**', '**/src/generated/**'
|
||||
}
|
||||
toggleOffOn()
|
||||
googleJavaFormat()
|
||||
@@ -109,7 +109,7 @@ spotless {
|
||||
}
|
||||
|
||||
wrapper {
|
||||
gradleVersion '8.4'
|
||||
gradleVersion '8.11'
|
||||
}
|
||||
|
||||
ext.getCurrentArch = {
|
||||
|
||||
@@ -10,7 +10,8 @@
|
||||
# add these directories to sys.path here. If the directory is relative to the
|
||||
# documentation root, use os.path.abspath to make it absolute, like shown here.
|
||||
#
|
||||
# import os
|
||||
import os
|
||||
|
||||
# import sys
|
||||
# sys.path.insert(0, os.path.abspath('.'))
|
||||
|
||||
@@ -138,9 +139,15 @@ suppress_warnings = ["epub.unknown_project_files"]
|
||||
|
||||
sphinx_tabs_valid_builders = ["epub", "linkcheck"]
|
||||
|
||||
# -- Options for linkcheck -------------------------------------------------
|
||||
|
||||
# Excluded links for linkcheck
|
||||
# These should be periodically checked by hand to ensure that they are still functional
|
||||
linkcheck_ignore = ["https://www.raspberrypi.com/software/"]
|
||||
linkcheck_ignore = [R"https://www.raspberrypi.com/software/", R"http://10\..+"]
|
||||
|
||||
token = os.environ.get("GITHUB_TOKEN", None)
|
||||
if token:
|
||||
linkcheck_auth = [(R"https://github.com/.+", token)]
|
||||
|
||||
# MyST configuration (https://myst-parser.readthedocs.io/en/latest/configuration.html)
|
||||
myst_enable_extensions = ["colon_fence"]
|
||||
|
||||
@@ -8,14 +8,14 @@ You do not need to install PhotonVision on a Windows PC in order to access the w
|
||||
|
||||
## Installing Java
|
||||
|
||||
PhotonVision requires a JDK installed and on the system path. JDK 11 is needed (different versions will not work). If you don't have JDK 11 already, run the following to install it:
|
||||
PhotonVision requires a JDK installed and on the system path. JDK 17 is needed (different versions will not work). If you don't have JDK 17 already, run the following to install it:
|
||||
|
||||
```
|
||||
$ sudo apt-get install openjdk-11-jdk
|
||||
$ sudo apt-get install openjdk-17-jdk
|
||||
```
|
||||
|
||||
:::{warning}
|
||||
Using a JDK other than JDK11 will cause issues when running PhotonVision and is not supported.
|
||||
Using a JDK other than JDK17 will cause issues when running PhotonVision and is not supported.
|
||||
:::
|
||||
|
||||
## Downloading the Latest Stable Release of PhotonVision
|
||||
|
||||
@@ -5,17 +5,17 @@ Due to current [cscore](https://github.com/wpilibsuite/allwpilib/tree/main/cscor
|
||||
:::
|
||||
|
||||
:::{note}
|
||||
You do not need to install PhotonVision on a Windows PC in order to access the webdashboard (assuming you are using an external coprocessor like a Raspberry Pi).
|
||||
You do not need to install PhotonVision on a Mac in order to access the webdashboard (assuming you are using an external coprocessor like a Raspberry Pi).
|
||||
:::
|
||||
|
||||
VERY Limited macOS support is available.
|
||||
|
||||
## Installing Java
|
||||
|
||||
PhotonVision requires a JDK installed and on the system path. JDK 11 is needed (different versions will not work). You may already have this if you have installed WPILib. If not, [download and install it from here](https://adoptium.net/temurin/releases?version=11).
|
||||
PhotonVision requires a JDK installed and on the system path. JDK 17 is needed (different versions will not work). You may already have this if you have installed WPILib 2025+. If not, [download and install it from here](https://adoptium.net/temurin/releases?version=17).
|
||||
|
||||
:::{warning}
|
||||
Using a JDK other than JDK11 will cause issues when running PhotonVision and is not supported.
|
||||
Using a JDK other than JDK17 will cause issues when running PhotonVision and is not supported.
|
||||
:::
|
||||
|
||||
## Downloading the Latest Stable Release of PhotonVision
|
||||
|
||||
@@ -8,16 +8,36 @@ The WPILibPi image includes FRCVision, which reserves USB cameras; to use Photon
|
||||
|
||||
SSH into the Raspberry Pi (using Windows command line, or a tool like [Putty](https://www.chiark.greenend.org.uk/~sgtatham/putty/) ) at the Romi's default address `10.0.0.2`. The default user is `pi`, and the password is `raspberry`.
|
||||
|
||||
Follow the process for installing PhotonVision on {ref}`"Other Debian-Based Co-Processor Installation" <docs/advanced-installation/sw_install/other-coprocessors:Other Debian-Based Co-Processor Installation>`. As it mentions this will require an internet connection so plugging into the ethernet jack on the Raspberry Pi will be the easiest solution. The pi must remain writable!
|
||||
:::.. The following paragraph can be restored when WPILibPi becomes compatible with the current version of PhotonVision.
|
||||
:::.. Follow the process for installing PhotonVision on {ref}`"Other Debian-Based Co-Processor Installation" <docs/advanced-installation/sw_install/other-coprocessors:Other Debian-Based Co-Processor Installation>`. As it mentions, this will require an internet connection so connecting the Raspberry Pi to an internet-connected router via an Ethernet cable will be the easiest solution. The pi must remain writable while you are following these steps!
|
||||
|
||||
Next, from the SSH terminal, run `sudo nano /home/pi/runCamera` then arrow down to the start of the exec line and press "Enter" to add a new line. Then add `#` before the exec command to comment it out. Then, arrow up to the new line and type `sleep 10000`. Hit "Ctrl + O" and then "Enter" to save the file. Finally press "Ctrl + X" to exit nano. Now, reboot the Romi by typing `sudo reboot`.
|
||||
:::..Temporary instructions explaining how to install the older version of PhotonVision on a Romi. Remove when no longer needed.
|
||||
:::{attention}
|
||||
The version of WPILibPi for the Romi is 2023.2.1, which is not compatible with the current version of PhotonVision. **If you are using WPILibPi 2023.2.1 on your Romi, you must install PhotonVision v2023.4.2 or earlier!**
|
||||
|
||||
To install a compatible version of PhotonVision, enter these commands in the SSH terminal connected to the Raspberry Pi. This will download and run the install script, which will intall PhotonVision on your Raspberry Pi and configure it to run at startup.
|
||||
|
||||
```bash
|
||||
$ wget https://git.io/JJrEP -O install.sh
|
||||
$ sudo chmod +x install.sh
|
||||
$ sudo ./install.sh -v 2023.4.2
|
||||
```
|
||||
The install script requires an internet connection, so connecting the Raspberry Pi to an internet-connected router via an Ethernet cable will be the easiest solution. The pi must remain writable while you are following these steps!
|
||||
:::
|
||||
:::..End of temporary instructions.
|
||||
|
||||
Next, from the SSH terminal, run `sudo nano /home/pi/runCamera` then arrow down to the start of the exec line and press "Enter" to add a new line. Then add `#` before the exec command to comment it out. Then, arrow up to the new line and type `sleep 10000`. Hit "Ctrl + O" and then "Enter" to save the file. Finally press "Ctrl + X" to exit nano. Now, reboot the Romi by typing `sudo reboot now`.
|
||||
|
||||
```{image} images/nano.png
|
||||
|
||||
```
|
||||
|
||||
After it reboots, you should be able to [locate the PhotonVision UI](https://photonvision.github.io/gloworm-docs/docs/quickstart/#finding-gloworm) at: `http://10.0.0.2:5800/`.
|
||||
After the Romi reboots, you should be able to open the PhotonVision UI at: [`http://10.0.0.2:5800/`](http://10.0.0.2:5800/). From here, you can adjust {ref}`Settings <docs/settings:Settings>` and configure {ref}`Pipelines <docs/pipelines/index:Pipelines>`.
|
||||
|
||||
:::{warning}
|
||||
In order for settings, logs, etc. to be saved / take effect, ensure that PhotonVision is in writable mode.
|
||||
:::
|
||||
|
||||
:::{attention}
|
||||
When using an older version of PhotonVision, the user interface and features may be different than what appears in the online documentation. The [Documentation](http://10.0.0.2:5800/#/docs) link in the User Interface will open a bundled version of the documentation that matches the PhotonVision version running on your coprocessor.
|
||||
:::
|
||||
|
||||
@@ -12,10 +12,14 @@ Bonjour provides more stable networking when using Windows PCs. Install [Bonjour
|
||||
|
||||
## Installing Java
|
||||
|
||||
PhotonVision requires a JDK installed and on the system path. **JDK 11 is needed** (different versions will not work). You may already have this if you have installed WPILib, but ensure that running `java -version` shows JDK 11. If not, [download and install it from here](https://adoptium.net/temurin/releases?version=11) and ensure that the new JDK is being used.
|
||||
PhotonVision requires a JDK installed and on the system path. **JDK 17 is needed. Windows Users must use the JDK that ships with WPILib.** [Download and install it from here.](https://github.com/wpilibsuite/allwpilib/releases/tag/v2025.1.1-beta-3) Either ensure the only Java on your PATH is the WPILIB Java or specify it to gradle with `-Dorg.gradle.java.home=C:\Users\Public\wpilib\2025\jdk`:
|
||||
|
||||
```
|
||||
> ./gradlew run "-Dorg.gradle.java.home=C:\Users\Public\wpilib\2025\jdk"
|
||||
```
|
||||
|
||||
:::{warning}
|
||||
Using a JDK other than JDK11 will cause issues when running PhotonVision and is not supported.
|
||||
Using a JDK other than WPILIB's JDK17 will cause issues when running PhotonVision and is not supported.
|
||||
:::
|
||||
|
||||
## Downloading the Latest Stable Release of PhotonVision
|
||||
|
||||
@@ -139,25 +139,7 @@ The `deploy` command is tested against Raspberry Pi coprocessors. Other similar
|
||||
|
||||
### Using PhotonLib Builds
|
||||
|
||||
The build process includes the following task:
|
||||
|
||||
```{eval-rst}
|
||||
.. tab-set::
|
||||
|
||||
.. tab-item:: Linux
|
||||
|
||||
``./gradlew generateVendorJson``
|
||||
|
||||
.. tab-item:: macOS
|
||||
|
||||
``./gradlew generateVendorJson``
|
||||
|
||||
.. tab-item:: Windows (cmd)
|
||||
|
||||
``gradlew generateVendorJson``
|
||||
```
|
||||
|
||||
This generates a vendordep JSON of your local build at `photon-lib/build/generated/vendordeps/photonlib.json`.
|
||||
The build process automatically generates a vendordep JSON of your local build at `photon-lib/build/generated/vendordeps/photonlib.json`.
|
||||
|
||||
The photonlib source can be published to your local maven repository after building:
|
||||
|
||||
@@ -247,17 +229,15 @@ You can run one of the many built in examples straight from the command line, to
|
||||
|
||||
#### Running C++/Java
|
||||
|
||||
PhotonLib must first be published to your local maven repository, then the copy PhotonLib task will copy the generated vendordep json file into each example. After that, the simulateJava/simulateNative task can be used like a normal robot project. Robot simulation with attached debugger is technically possible by using simulateExternalJava and modifying the launch script it exports, though not yet supported.
|
||||
PhotonLib must first be published to your local maven repository. This will also copy the generated vendordep json file into each example. After that, the simulateJava/simulateNative task can be used like a normal robot project. Robot simulation with attached debugger is technically possible by using simulateExternalJava and modifying the launch script it exports, though not yet supported.
|
||||
|
||||
```
|
||||
~/photonvision$ ./gradlew publishToMavenLocal
|
||||
|
||||
~/photonvision$ cd photonlib-java-examples
|
||||
~/photonvision/photonlib-java-examples$ ./gradlew copyPhotonlib
|
||||
~/photonvision/photonlib-java-examples$ ./gradlew <example-name>:simulateJava
|
||||
|
||||
~/photonvision$ cd photonlib-cpp-examples
|
||||
~/photonvision/photonlib-cpp-examples$ ./gradlew copyPhotonlib
|
||||
~/photonvision/photonlib-cpp-examples$ ./gradlew <example-name>:simulateNative
|
||||
```
|
||||
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
# Selecting Hardware
|
||||
|
||||
:::{note}
|
||||
It is highly recommended that you read the {ref}`quick start guide<docs/quick-start/common-setups:Common Hardware Setups>`, and use the hardware recommended there that
|
||||
is not touched on here.
|
||||
:::
|
||||
|
||||
In order to use PhotonVision, you need a coprocessor and a camera. Other than the recommended hardware found in the {ref}`quick start guide<docs/quick-start/common-setups:Common Hardware Setups>`, this page will help you select hardware that should work for photonvision even though it is not supported/recommended.
|
||||
|
||||
## Choosing a Coprocessor
|
||||
|
||||
@@ -8,7 +8,7 @@ If you’re not using cameras in 3D mode, calibration is optional, but it can st
|
||||
|
||||
## Print the Calibration Target
|
||||
|
||||
- Downloaded from our [demo site](https://demo.photonvision.org/#/cameras), or directly from your coprocessors cameras tab.
|
||||
- Downloaded from our [demo site](http://photonvision.global/#/cameras), or directly from your coprocessors cameras tab.
|
||||
- Use the Charuco calibration board:
|
||||
- Board Type: Charuco
|
||||
- Tag Family: 4x4
|
||||
|
||||
@@ -13,6 +13,11 @@ The Orange Pi 5 is the only currently supported device for object detection.
|
||||
|
||||
## SD Cards
|
||||
|
||||
:::{important}
|
||||
It is highly recommended that you use an industrial micro SD card, as they offer far greater protection against corruption from improper shutdowns, like most cards
|
||||
face every time the robot is turned off.
|
||||
:::
|
||||
|
||||
- 8GB or larger micro SD card
|
||||
- Many teams have found that an industrial micro sd card are much more stable in competition. One example is the SanDisk industrial 16GB micro SD card.
|
||||
|
||||
|
||||
@@ -88,4 +88,4 @@ The address in the code above (`photonvision.local`) is the hostname of the copr
|
||||
|
||||
## Camera Stream Ports
|
||||
|
||||
The camera streams start at they begin at 1181 with two ports for each camera (ex. 1181 and 1182 for camera one, 1183 and 1184 for camera two, etc.). The easiest way to identify the port of the camera that you want is by double clicking on the stream, which opens it in a separate page. The port will be listed below the stream.
|
||||
The camera streams start at 1181 with two ports for each camera (ex. 1181 and 1182 for camera one, 1183 and 1184 for camera two, etc.). The easiest way to identify the port of the camera that you want is by double clicking on the stream, which opens it in a separate page. The port will be listed below the stream.
|
||||
|
||||
@@ -8,7 +8,7 @@ In order for photonvision to connect to the roborio it needs to know your team n
|
||||
|
||||
### Camera Nickname
|
||||
|
||||
You **must** nickname your cameras in photonvision to ensure that every camera has a unique name. This is how we will identify cameras in robot code. The camera can be nickname using the edit button next to the camera name in the upper right of the Dashboard tab.
|
||||
You **must** nickname your cameras in PhotonVision to ensure that every camera has a unique name. This is how you will identify cameras in robot code. The camera can be nicknamed using the edit button next to the camera name in the upper right of the Dashboard tab.
|
||||
|
||||
```{image} images/editCameraName.png
|
||||
:align: center
|
||||
@@ -38,7 +38,7 @@ When detecting AprilTags, it's important to minimize 'motion blur' as much as po
|
||||
- Fixes
|
||||
- Lower your exposure as low as possible. Using gain and brightness to account for lack of brightness.
|
||||
- Other Options:
|
||||
- Don't use/rely vision measurements while moving.
|
||||
- Don't use/rely on vision measurements while moving.
|
||||
|
||||
```{image} images/motionblur.png
|
||||
:align: center
|
||||
@@ -51,7 +51,7 @@ When using an Orange Pi 5 with an OV9782 teams will usually change the following
|
||||
- Resolution:
|
||||
- Resolutions higher than 640x640 may not result in any higher detection accuracy and may lower {ref}`performance<docs/objectDetection/about-object-detection:Letterboxing>`.
|
||||
- Confidence:
|
||||
- 0.75 - 0.95 Lower values are fpr detecting warn game pieces or less ideal game pieces. Higher for less warn, more ideal game pieces.
|
||||
- 0.75 - 0.95 Lower values are for detecting worn game pieces or less ideal game pieces. Higher for less worn, more ideal game pieces.
|
||||
- White Balance Temperature:
|
||||
- Adjust this to achieve better color accuracy. This may be needed to increase confidence.
|
||||
- Set arducam specific camera type selector to OV9782
|
||||
|
||||
@@ -6,3 +6,4 @@ org.gradle.jvmargs= \
|
||||
--add-exports jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED \
|
||||
--add-exports jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED \
|
||||
--add-exports jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED
|
||||
org.ysb33r.gradle.doxygen.download.url=https://frcmaven.wpi.edu/artifactory/generic-release-mirror/doxygen
|
||||
|
||||
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
Binary file not shown.
2
gradle/wrapper/gradle-wrapper.properties
vendored
2
gradle/wrapper/gradle-wrapper.properties
vendored
@@ -1,6 +1,6 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=permwrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-bin.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.11-bin.zip
|
||||
networkTimeout=10000
|
||||
validateDistributionUrl=true
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
|
||||
7
gradlew
vendored
7
gradlew
vendored
@@ -15,6 +15,8 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
#
|
||||
|
||||
##############################################################################
|
||||
#
|
||||
@@ -55,7 +57,7 @@
|
||||
# Darwin, MinGW, and NonStop.
|
||||
#
|
||||
# (3) This script is generated from the Groovy template
|
||||
# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
||||
# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
||||
# within the Gradle project.
|
||||
#
|
||||
# You can find Gradle at https://github.com/gradle/gradle/.
|
||||
@@ -84,7 +86,8 @@ done
|
||||
# shellcheck disable=SC2034
|
||||
APP_BASE_NAME=${0##*/}
|
||||
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
|
||||
APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit
|
||||
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s
|
||||
' "$PWD" ) || exit
|
||||
|
||||
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||
MAX_FD=maximum
|
||||
|
||||
22
gradlew.bat
vendored
22
gradlew.bat
vendored
@@ -13,6 +13,8 @@
|
||||
@rem See the License for the specific language governing permissions and
|
||||
@rem limitations under the License.
|
||||
@rem
|
||||
@rem SPDX-License-Identifier: Apache-2.0
|
||||
@rem
|
||||
|
||||
@if "%DEBUG%"=="" @echo off
|
||||
@rem ##########################################################################
|
||||
@@ -43,11 +45,11 @@ set JAVA_EXE=java.exe
|
||||
%JAVA_EXE% -version >NUL 2>&1
|
||||
if %ERRORLEVEL% equ 0 goto execute
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
echo.
|
||||
echo Please set the JAVA_HOME variable in your environment to match the
|
||||
echo location of your Java installation.
|
||||
echo. 1>&2
|
||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
|
||||
echo. 1>&2
|
||||
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||
echo location of your Java installation. 1>&2
|
||||
|
||||
goto fail
|
||||
|
||||
@@ -57,11 +59,11 @@ set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||
|
||||
if exist "%JAVA_EXE%" goto execute
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
|
||||
echo.
|
||||
echo Please set the JAVA_HOME variable in your environment to match the
|
||||
echo location of your Java installation.
|
||||
echo. 1>&2
|
||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
|
||||
echo. 1>&2
|
||||
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||
echo location of your Java installation. 1>&2
|
||||
|
||||
goto fail
|
||||
|
||||
|
||||
@@ -82,6 +82,11 @@ const boardType = ref<CalibrationBoardTypes>(CalibrationBoardTypes.Charuco);
|
||||
const useOldPattern = ref(false);
|
||||
const tagFamily = ref<CalibrationTagFamilies>(CalibrationTagFamilies.Dict_4X4_1000);
|
||||
|
||||
// Emperical testing - with stack size limit of 1MB, we can handle at -least- 700k points
|
||||
const tooManyPoints = computed(
|
||||
() => useStateStore().calibrationData.imageCount * patternWidth.value * patternHeight.value > 700000
|
||||
);
|
||||
|
||||
const downloadCalibBoard = () => {
|
||||
const doc = new JsPDF({ unit: "in", format: "letter" });
|
||||
|
||||
@@ -152,7 +157,10 @@ const downloadCalibBoard = () => {
|
||||
doc.save(`calibrationTarget-${CalibrationBoardTypes[boardType.value]}.pdf`);
|
||||
};
|
||||
|
||||
const isCalibrating = ref(false);
|
||||
const isCalibrating = computed(
|
||||
() => useCameraSettingsStore().currentCameraSettings.currentPipelineIndex === WebsocketPipelineType.Calib3d
|
||||
);
|
||||
|
||||
const startCalibration = () => {
|
||||
useCameraSettingsStore().startPnPCalibration({
|
||||
squareSizeIn: squareSizeIn.value,
|
||||
@@ -165,13 +173,15 @@ const startCalibration = () => {
|
||||
});
|
||||
// The Start PnP method already handles updating the backend so only a store update is required
|
||||
useCameraSettingsStore().currentCameraSettings.currentPipelineIndex = WebsocketPipelineType.Calib3d;
|
||||
isCalibrating.value = true;
|
||||
// isCalibrating.value = true;
|
||||
calibCanceled.value = false;
|
||||
};
|
||||
const showCalibEndDialog = ref(false);
|
||||
const calibCanceled = ref(false);
|
||||
const calibSuccess = ref<boolean | undefined>(undefined);
|
||||
const endCalibration = () => {
|
||||
calibSuccess.value = undefined;
|
||||
|
||||
if (!useStateStore().calibrationData.hasEnoughImages) {
|
||||
calibCanceled.value = true;
|
||||
}
|
||||
@@ -187,7 +197,8 @@ const endCalibration = () => {
|
||||
calibSuccess.value = false;
|
||||
})
|
||||
.finally(() => {
|
||||
isCalibrating.value = false;
|
||||
// isCalibrating.value = false;
|
||||
// backend deals with this for us
|
||||
});
|
||||
};
|
||||
|
||||
@@ -240,6 +251,7 @@ const setSelectedVideoFormat = (format: VideoFormat) => {
|
||||
<v-row style="display: flex; flex-direction: column" class="mt-4">
|
||||
<v-card-subtitle v-show="!isCalibrating" class="pl-3 pa-0 ma-0"> Configure New Calibration</v-card-subtitle>
|
||||
<v-form ref="form" v-model="settingsValid" class="pl-4 mb-10 pr-5">
|
||||
<!-- TODO: the default videoFormatIndex is 0, but the list of unique video mode indexes might not include 0. getUniqueVideoResolutionStrings indexing is also different from the normal video mode indexing -->
|
||||
<pv-select
|
||||
v-model="useStateStore().calibrationData.videoFormatIndex"
|
||||
label="Resolution"
|
||||
@@ -413,12 +425,17 @@ const setSelectedVideoFormat = (format: VideoFormat) => {
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col v-if="tooManyPoints" :cols="12">
|
||||
<v-banner rounded color="red" text-color="white" class="mt-3" icon="mdi-alert-circle-outline">
|
||||
Too many corners - finish calibration now!
|
||||
</v-banner>
|
||||
</v-col>
|
||||
<v-col :cols="6">
|
||||
<v-btn
|
||||
small
|
||||
color="secondary"
|
||||
style="width: 100%"
|
||||
:disabled="!settingsValid"
|
||||
:disabled="!settingsValid || tooManyPoints"
|
||||
@click="isCalibrating ? useCameraSettingsStore().takeCalibrationSnapshot() : startCalibration()"
|
||||
>
|
||||
<v-icon left class="calib-btn-icon"> {{ isCalibrating ? "mdi-camera" : "mdi-flag-outline" }} </v-icon>
|
||||
@@ -482,10 +499,12 @@ const setSelectedVideoFormat = (format: VideoFormat) => {
|
||||
process.</v-card-text
|
||||
>
|
||||
</template>
|
||||
<template v-else-if="isCalibrating">
|
||||
<!-- No result reported yet -->
|
||||
<template v-else-if="calibSuccess === undefined">
|
||||
<v-progress-circular indeterminate :size="70" :width="8" color="accent" />
|
||||
<v-card-text>Camera is being calibrated. This process may take several minutes...</v-card-text>
|
||||
</template>
|
||||
<!-- Got positive result -->
|
||||
<template v-else-if="calibSuccess">
|
||||
<v-icon color="green" size="70"> mdi-check-bold </v-icon>
|
||||
<v-card-text>
|
||||
|
||||
2
photon-core/.gitignore
vendored
2
photon-core/.gitignore
vendored
@@ -11,5 +11,3 @@ photonvision/*
|
||||
photonvision_config/*
|
||||
photon-server/lib/*
|
||||
photon-server/package-lock.json
|
||||
|
||||
src/main/java/org/photonvision/PhotonVersion.java
|
||||
|
||||
@@ -65,9 +65,13 @@ dependencies {
|
||||
}
|
||||
|
||||
task writeCurrentVersion {
|
||||
def versionFileIn = file("${rootDir}/shared/PhotonVersion.java.in")
|
||||
writePhotonVersionFile(versionFileIn, Path.of("$projectDir", "src", "main", "java", "org", "photonvision", "PhotonVersion.java"),
|
||||
versionString)
|
||||
doLast {
|
||||
def versionFileIn = file("${rootDir}/shared/PhotonVersion.java.in")
|
||||
writePhotonVersionFile(versionFileIn, Path.of("$buildDir", "generated", "java", "org", "photonvision", "PhotonVersion.java"),
|
||||
versionString)
|
||||
}
|
||||
}
|
||||
// https://github.com/wpilibsuite/allwpilib/blob/main/wpilibj/build.gradle#L52
|
||||
sourceSets.main.java.srcDir "${buildDir}/generated/java/"
|
||||
|
||||
build.dependsOn writeCurrentVersion
|
||||
compileJava.dependsOn writeCurrentVersion
|
||||
|
||||
@@ -70,16 +70,12 @@ public class ConfigManager {
|
||||
if (INSTANCE == null) {
|
||||
Path rootFolder = PathManager.getInstance().getRootFolder();
|
||||
switch (m_saveStrat) {
|
||||
case SQL:
|
||||
INSTANCE = new ConfigManager(rootFolder, new SqlConfigProvider(rootFolder));
|
||||
break;
|
||||
case LEGACY:
|
||||
INSTANCE = new ConfigManager(rootFolder, new LegacyConfigProvider(rootFolder));
|
||||
break;
|
||||
case ATOMIC_ZIP:
|
||||
// not yet done, fall through
|
||||
default:
|
||||
break;
|
||||
case SQL -> INSTANCE = new ConfigManager(rootFolder, new SqlConfigProvider(rootFolder));
|
||||
case LEGACY ->
|
||||
INSTANCE = new ConfigManager(rootFolder, new LegacyConfigProvider(rootFolder));
|
||||
case ATOMIC_ZIP -> {
|
||||
// TODO: Not done yet
|
||||
}
|
||||
}
|
||||
}
|
||||
return INSTANCE;
|
||||
|
||||
@@ -21,12 +21,8 @@ import com.fasterxml.jackson.annotation.JsonAlias;
|
||||
import com.fasterxml.jackson.annotation.JsonCreator;
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import org.photonvision.common.hardware.Platform;
|
||||
import org.photonvision.common.networking.NetworkMode;
|
||||
import org.photonvision.common.util.file.JacksonUtils;
|
||||
|
||||
public class NetworkConfig {
|
||||
// Can be an integer team number, or an IP address
|
||||
@@ -89,15 +85,19 @@ public class NetworkConfig {
|
||||
setShouldManage(shouldManage);
|
||||
}
|
||||
|
||||
public Map<String, Object> toHashMap() {
|
||||
try {
|
||||
var ret = new ObjectMapper().convertValue(this, JacksonUtils.UIMap.class);
|
||||
ret.put("canManage", this.deviceCanManageNetwork());
|
||||
return ret;
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
return new HashMap<>();
|
||||
}
|
||||
public NetworkConfig(NetworkConfig config) {
|
||||
this(
|
||||
config.ntServerAddress,
|
||||
config.connectionType,
|
||||
config.staticIp,
|
||||
config.hostname,
|
||||
config.runNTServer,
|
||||
config.shouldManage,
|
||||
config.shouldPublishProto,
|
||||
config.networkManagerIface,
|
||||
config.setStaticCommand,
|
||||
config.setDHCPcommand,
|
||||
config.matchCamerasOnlyByPath);
|
||||
}
|
||||
|
||||
@JsonIgnore
|
||||
@@ -110,18 +110,12 @@ public class NetworkConfig {
|
||||
return "\"" + networkManagerIface + "\"";
|
||||
}
|
||||
|
||||
@JsonIgnore
|
||||
public boolean shouldManage() {
|
||||
return this.shouldManage;
|
||||
}
|
||||
|
||||
@JsonIgnore
|
||||
public void setShouldManage(boolean shouldManage) {
|
||||
this.shouldManage = shouldManage && this.deviceCanManageNetwork();
|
||||
}
|
||||
|
||||
@JsonIgnore
|
||||
private boolean deviceCanManageNetwork() {
|
||||
protected boolean deviceCanManageNetwork() {
|
||||
return Platform.isLinux();
|
||||
}
|
||||
|
||||
|
||||
@@ -205,13 +205,11 @@ public class NeuralNetworkModelManager {
|
||||
|
||||
try {
|
||||
switch (backend.get()) {
|
||||
case RKNN:
|
||||
case RKNN -> {
|
||||
models.get(backend.get()).add(new RknnModel(model, labels));
|
||||
logger.info(
|
||||
"Loaded model " + model.getName() + " for backend " + backend.get().toString());
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
} catch (IllegalArgumentException e) {
|
||||
logger.error("Failed to load model " + model.getName(), e);
|
||||
|
||||
@@ -21,19 +21,6 @@ import edu.wpi.first.apriltag.AprilTagFieldLayout;
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
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.mrcal.MrCalJNILoader;
|
||||
import org.photonvision.raspi.LibCameraJNILoader;
|
||||
import org.photonvision.vision.calibration.UICameraCalibrationCoefficients;
|
||||
import org.photonvision.vision.camera.QuirkyCamera;
|
||||
import org.photonvision.vision.processes.VisionModule;
|
||||
import org.photonvision.vision.processes.VisionModuleManager;
|
||||
import org.photonvision.vision.processes.VisionSource;
|
||||
|
||||
public class PhotonConfiguration {
|
||||
@@ -124,81 +111,6 @@ public class PhotonConfiguration {
|
||||
return cameraConfigurations.remove(name) != null;
|
||||
}
|
||||
|
||||
public Map<String, Object> toHashMap() {
|
||||
Map<String, Object> map = new HashMap<>();
|
||||
var settingsSubmap = new HashMap<String, Object>();
|
||||
|
||||
// Hack active interfaces into networkSettings
|
||||
var netConfigMap = networkConfig.toHashMap();
|
||||
netConfigMap.put("networkInterfaceNames", NetworkUtils.getAllActiveWiredInterfaces());
|
||||
netConfigMap.put("networkingDisabled", NetworkManager.getInstance().networkingIsDisabled);
|
||||
|
||||
settingsSubmap.put("networkSettings", netConfigMap);
|
||||
|
||||
var lightingConfig = new UILightingConfig();
|
||||
lightingConfig.brightness = hardwareSettings.ledBrightnessPercentage;
|
||||
lightingConfig.supported = !hardwareConfig.ledPins.isEmpty();
|
||||
settingsSubmap.put("lighting", SerializationUtils.objectToHashMap(lightingConfig));
|
||||
// General Settings
|
||||
var generalSubmap = new HashMap<String, Object>();
|
||||
generalSubmap.put("version", PhotonVersion.versionString);
|
||||
generalSubmap.put(
|
||||
"gpuAcceleration",
|
||||
LibCameraJNILoader.isSupported()
|
||||
? "Zerocopy Libcamera Working"
|
||||
: ""); // TODO add support for other types of GPU accel
|
||||
generalSubmap.put("mrCalWorking", MrCalJNILoader.getInstance().isLoaded());
|
||||
generalSubmap.put("availableModels", NeuralNetworkModelManager.getInstance().getModels());
|
||||
generalSubmap.put(
|
||||
"supportedBackends", NeuralNetworkModelManager.getInstance().getSupportedBackends());
|
||||
generalSubmap.put(
|
||||
"hardwareModel",
|
||||
hardwareConfig.deviceName.isEmpty()
|
||||
? Platform.getHardwareModel()
|
||||
: hardwareConfig.deviceName);
|
||||
generalSubmap.put("hardwarePlatform", Platform.getPlatformName());
|
||||
settingsSubmap.put("general", generalSubmap);
|
||||
// AprilTagFieldLayout
|
||||
settingsSubmap.put("atfl", this.atfl);
|
||||
|
||||
map.put(
|
||||
"cameraSettings",
|
||||
VisionModuleManager.getInstance().getModules().stream()
|
||||
.map(VisionModule::toUICameraConfig)
|
||||
.map(SerializationUtils::objectToHashMap)
|
||||
.collect(Collectors.toList()));
|
||||
map.put("settings", settingsSubmap);
|
||||
|
||||
return map;
|
||||
}
|
||||
|
||||
public static class UILightingConfig {
|
||||
public int brightness = 0;
|
||||
public boolean supported = true;
|
||||
}
|
||||
|
||||
public static class UICameraConfiguration {
|
||||
@SuppressWarnings("unused")
|
||||
public double fov;
|
||||
|
||||
public String nickname;
|
||||
public String uniqueName;
|
||||
public HashMap<String, Object> currentPipelineSettings;
|
||||
public int currentPipelineIndex;
|
||||
public List<String> pipelineNicknames;
|
||||
public HashMap<Integer, HashMap<String, Object>> videoFormatList;
|
||||
public int outputStreamPort;
|
||||
public int inputStreamPort;
|
||||
public List<UICameraCalibrationCoefficients> calibrations;
|
||||
public boolean isFovConfigurable = true;
|
||||
public QuirkyCamera cameraQuirks;
|
||||
public boolean isCSICamera;
|
||||
public double minExposureRaw;
|
||||
public double maxExposureRaw;
|
||||
public double minWhiteBalanceTemp;
|
||||
public double maxWhiteBalanceTemp;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "PhotonConfiguration [\n hardwareConfig="
|
||||
|
||||
@@ -599,9 +599,9 @@ public class SqlConfigProvider extends ConfigProvider {
|
||||
result.getString(Columns.CAM_PIPELINE_JSONS), dummyList.getClass());
|
||||
|
||||
List<CVPipelineSettings> loadedSettings = new ArrayList<>();
|
||||
for (var str : pipelineSettings) {
|
||||
if (str instanceof String) {
|
||||
loadedSettings.add(JacksonUtils.deserialize((String) str, CVPipelineSettings.class));
|
||||
for (var setting : pipelineSettings) {
|
||||
if (setting instanceof String str) {
|
||||
loadedSettings.add(JacksonUtils.deserialize(str, CVPipelineSettings.class));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -32,6 +32,7 @@ import org.photonvision.common.configuration.ConfigManager;
|
||||
import org.photonvision.common.configuration.NetworkConfig;
|
||||
import org.photonvision.common.dataflow.DataChangeService;
|
||||
import org.photonvision.common.dataflow.events.OutgoingUIEvent;
|
||||
import org.photonvision.common.dataflow.websocket.UIPhotonConfiguration;
|
||||
import org.photonvision.common.hardware.HardwareManager;
|
||||
import org.photonvision.common.logging.LogGroup;
|
||||
import org.photonvision.common.logging.LogLevel;
|
||||
@@ -165,7 +166,8 @@ public class NetworkTablesManager {
|
||||
DataChangeService.getInstance()
|
||||
.publishEvent(
|
||||
new OutgoingUIEvent<>(
|
||||
"fullsettings", ConfigManager.getInstance().getConfig().toHashMap()));
|
||||
"fullsettings",
|
||||
UIPhotonConfiguration.programStateToUi(ConfigManager.getInstance().getConfig())));
|
||||
} catch (IOException e) {
|
||||
logger.error("Error deserializing atfl!");
|
||||
logger.error(atfl_json);
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
/*
|
||||
* 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.dataflow.websocket;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import org.photonvision.vision.calibration.UICameraCalibrationCoefficients;
|
||||
import org.photonvision.vision.camera.QuirkyCamera;
|
||||
|
||||
public class UICameraConfiguration {
|
||||
@SuppressWarnings("unused")
|
||||
public double fov;
|
||||
|
||||
public String nickname;
|
||||
public String uniqueName;
|
||||
public HashMap<String, Object> currentPipelineSettings;
|
||||
public int currentPipelineIndex;
|
||||
public List<String> pipelineNicknames;
|
||||
public HashMap<Integer, HashMap<String, Object>> videoFormatList;
|
||||
public int outputStreamPort;
|
||||
public int inputStreamPort;
|
||||
public List<UICameraCalibrationCoefficients> calibrations;
|
||||
public boolean isFovConfigurable = true;
|
||||
public QuirkyCamera cameraQuirks;
|
||||
public boolean isCSICamera;
|
||||
public double minExposureRaw;
|
||||
public double maxExposureRaw;
|
||||
public double minWhiteBalanceTemp;
|
||||
public double maxWhiteBalanceTemp;
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
/*
|
||||
* 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.dataflow.websocket;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public class UIGeneralSettings {
|
||||
public UIGeneralSettings(
|
||||
String version,
|
||||
String gpuAcceleration,
|
||||
boolean mrCalWorking,
|
||||
Map<String, ArrayList<String>> availableModels,
|
||||
List<String> supportedBackends,
|
||||
String hardwareModel,
|
||||
String hardwarePlatform) {
|
||||
this.version = version;
|
||||
this.gpuAcceleration = gpuAcceleration;
|
||||
this.mrCalWorking = mrCalWorking;
|
||||
this.availableModels = availableModels;
|
||||
this.supportedBackends = supportedBackends;
|
||||
this.hardwareModel = hardwareModel;
|
||||
this.hardwarePlatform = hardwarePlatform;
|
||||
}
|
||||
|
||||
public String version;
|
||||
public String gpuAcceleration;
|
||||
public boolean mrCalWorking;
|
||||
public Map<String, ArrayList<String>> availableModels;
|
||||
public List<String> supportedBackends;
|
||||
public String hardwareModel;
|
||||
public String hardwarePlatform;
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
/*
|
||||
* 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.dataflow.websocket;
|
||||
|
||||
public class UILightingConfig {
|
||||
public UILightingConfig(int brightness, boolean supported) {
|
||||
this.brightness = brightness;
|
||||
this.supported = supported;
|
||||
}
|
||||
|
||||
public int brightness = 0;
|
||||
public boolean supported = true;
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
/*
|
||||
* 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.dataflow.websocket;
|
||||
|
||||
import java.util.List;
|
||||
import org.photonvision.common.configuration.NetworkConfig;
|
||||
import org.photonvision.common.networking.NetworkUtils.NMDeviceInfo;
|
||||
|
||||
public class UINetConfig extends NetworkConfig {
|
||||
public UINetConfig(
|
||||
NetworkConfig config, List<NMDeviceInfo> networkInterfaceNames, boolean networkingDisabled) {
|
||||
super(config);
|
||||
this.networkInterfaceNames = networkInterfaceNames;
|
||||
this.networkingDisabled = networkingDisabled;
|
||||
this.canManage = this.deviceCanManageNetwork();
|
||||
}
|
||||
|
||||
public List<NMDeviceInfo> networkInterfaceNames;
|
||||
public boolean networkingDisabled;
|
||||
public boolean canManage;
|
||||
}
|
||||
@@ -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.common.dataflow.websocket;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
import org.photonvision.PhotonVersion;
|
||||
import org.photonvision.common.configuration.NeuralNetworkModelManager;
|
||||
import org.photonvision.common.configuration.PhotonConfiguration;
|
||||
import org.photonvision.common.hardware.Platform;
|
||||
import org.photonvision.common.networking.NetworkManager;
|
||||
import org.photonvision.common.networking.NetworkUtils;
|
||||
import org.photonvision.mrcal.MrCalJNILoader;
|
||||
import org.photonvision.raspi.LibCameraJNILoader;
|
||||
import org.photonvision.vision.processes.VisionModule;
|
||||
import org.photonvision.vision.processes.VisionModuleManager;
|
||||
|
||||
public class UIPhotonConfiguration {
|
||||
public List<UICameraConfiguration> cameraSettings;
|
||||
public UIProgramSettings settings;
|
||||
|
||||
public UIPhotonConfiguration(
|
||||
UIProgramSettings settings, List<UICameraConfiguration> cameraSettings) {
|
||||
this.cameraSettings = cameraSettings;
|
||||
this.settings = settings;
|
||||
}
|
||||
|
||||
public static UIPhotonConfiguration programStateToUi(PhotonConfiguration c) {
|
||||
return new UIPhotonConfiguration(
|
||||
new UIProgramSettings(
|
||||
new UINetConfig(
|
||||
c.getNetworkConfig(),
|
||||
NetworkUtils.getAllActiveWiredInterfaces(),
|
||||
NetworkManager.getInstance().networkingIsDisabled),
|
||||
new UILightingConfig(
|
||||
c.getHardwareSettings().ledBrightnessPercentage,
|
||||
!c.getHardwareConfig().ledPins.isEmpty()),
|
||||
new UIGeneralSettings(
|
||||
PhotonVersion.versionString,
|
||||
// TODO add support for other types of GPU accel
|
||||
LibCameraJNILoader.isSupported() ? "Zerocopy Libcamera Working" : "",
|
||||
MrCalJNILoader.getInstance().isLoaded(),
|
||||
NeuralNetworkModelManager.getInstance().getModels(),
|
||||
NeuralNetworkModelManager.getInstance().getSupportedBackends(),
|
||||
c.getHardwareConfig().deviceName.isEmpty()
|
||||
? Platform.getHardwareModel()
|
||||
: c.getHardwareConfig().deviceName,
|
||||
Platform.getPlatformName()),
|
||||
c.getApriltagFieldLayout()),
|
||||
VisionModuleManager.getInstance().getModules().stream()
|
||||
.map(VisionModule::toUICameraConfig)
|
||||
.collect(Collectors.toList()));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
/*
|
||||
* 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.dataflow.websocket;
|
||||
|
||||
import edu.wpi.first.apriltag.AprilTagFieldLayout;
|
||||
|
||||
public class UIProgramSettings {
|
||||
public UIProgramSettings(
|
||||
UINetConfig networkSettings,
|
||||
UILightingConfig lighting,
|
||||
UIGeneralSettings general,
|
||||
AprilTagFieldLayout atfl) {
|
||||
this.networkSettings = networkSettings;
|
||||
this.lighting = lighting;
|
||||
this.general = general;
|
||||
this.atfl = atfl;
|
||||
}
|
||||
|
||||
public UINetConfig networkSettings;
|
||||
public UILightingConfig lighting;
|
||||
public UIGeneralSettings general;
|
||||
public AprilTagFieldLayout atfl;
|
||||
}
|
||||
@@ -242,22 +242,7 @@ public class PigpioSocket {
|
||||
waveSendOnce(waveformId);
|
||||
}
|
||||
} else {
|
||||
String error = "";
|
||||
switch (waveformId) {
|
||||
case PI_EMPTY_WAVEFORM:
|
||||
error = "Waveform empty";
|
||||
break;
|
||||
case PI_TOO_MANY_CBS:
|
||||
error = "Too many CBS";
|
||||
break;
|
||||
case PI_TOO_MANY_OOL:
|
||||
error = "Too many OOL";
|
||||
break;
|
||||
case PI_NO_WAVEFORM_ID:
|
||||
error = "No waveform ID";
|
||||
break;
|
||||
}
|
||||
logger.error("Failed to send wave: " + error);
|
||||
logger.error("Failed to send wave: " + getMessageForError(waveformId));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
/*
|
||||
* 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;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Optional;
|
||||
import org.photonvision.common.logging.LogGroup;
|
||||
import org.photonvision.common.logging.Logger;
|
||||
|
||||
/**
|
||||
* Our blessed images inject the current version via this build workflow:
|
||||
* https://github.com/PhotonVision/photon-image-modifier/blob/2e5ddb6b599df0be921c12c8dbe7b939ecd7f615/.github/workflows/main.yml#L67
|
||||
*
|
||||
* <p>This class provides a convienent abstraction around this
|
||||
*/
|
||||
public class OsImageVersion {
|
||||
private static final Logger logger = new Logger(OsImageVersion.class, LogGroup.General);
|
||||
|
||||
private static Path imageVersionFile = Path.of("/opt/photonvision/image-version");
|
||||
|
||||
public static final Optional<String> IMAGE_VERSION = getImageVersion();
|
||||
|
||||
private static Optional<String> getImageVersion() {
|
||||
if (!imageVersionFile.toFile().exists()) {
|
||||
logger.warn(
|
||||
"Photon cannot locate base OS image version metadata at " + imageVersionFile.toString());
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
try {
|
||||
return Optional.of(Files.readString(imageVersionFile).strip());
|
||||
} catch (IOException e) {
|
||||
logger.error("Couldn't read image-version file", e);
|
||||
}
|
||||
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
@@ -134,25 +134,17 @@ public class VisionLED {
|
||||
var newLedModeRaw = (int) entryNotification.valueData.value.getInteger();
|
||||
logger.debug("Got LED mode " + newLedModeRaw);
|
||||
if (newLedModeRaw != currentLedMode.value) {
|
||||
VisionLEDMode newLedMode;
|
||||
switch (newLedModeRaw) {
|
||||
case -1:
|
||||
newLedMode = VisionLEDMode.kDefault;
|
||||
break;
|
||||
case 0:
|
||||
newLedMode = VisionLEDMode.kOff;
|
||||
break;
|
||||
case 1:
|
||||
newLedMode = VisionLEDMode.kOn;
|
||||
break;
|
||||
case 2:
|
||||
newLedMode = VisionLEDMode.kBlink;
|
||||
break;
|
||||
default:
|
||||
logger.warn("User supplied invalid LED mode, falling back to Default");
|
||||
newLedMode = VisionLEDMode.kDefault;
|
||||
break;
|
||||
}
|
||||
VisionLEDMode newLedMode =
|
||||
switch (newLedModeRaw) {
|
||||
case -1 -> newLedMode = VisionLEDMode.kDefault;
|
||||
case 0 -> newLedMode = VisionLEDMode.kOff;
|
||||
case 1 -> newLedMode = VisionLEDMode.kOn;
|
||||
case 2 -> newLedMode = VisionLEDMode.kBlink;
|
||||
default -> {
|
||||
logger.warn("User supplied invalid LED mode, falling back to Default");
|
||||
yield VisionLEDMode.kDefault;
|
||||
}
|
||||
};
|
||||
setInternal(newLedMode, true);
|
||||
|
||||
if (modeConsumer != null) modeConsumer.accept(newLedMode.value);
|
||||
@@ -164,18 +156,10 @@ public class VisionLED {
|
||||
|
||||
if (fromNT) {
|
||||
switch (newLedMode) {
|
||||
case kDefault:
|
||||
setStateImpl(pipelineModeSupplier.getAsBoolean());
|
||||
break;
|
||||
case kOff:
|
||||
setStateImpl(false);
|
||||
break;
|
||||
case kOn:
|
||||
setStateImpl(true);
|
||||
break;
|
||||
case kBlink:
|
||||
blinkImpl(85, -1);
|
||||
break;
|
||||
case kDefault -> setStateImpl(pipelineModeSupplier.getAsBoolean());
|
||||
case kOff -> setStateImpl(false);
|
||||
case kOn -> setStateImpl(true);
|
||||
case kBlink -> blinkImpl(85, -1);
|
||||
}
|
||||
currentLedMode = newLedMode;
|
||||
logger.info(
|
||||
@@ -183,18 +167,10 @@ public class VisionLED {
|
||||
} else {
|
||||
if (currentLedMode == VisionLEDMode.kDefault) {
|
||||
switch (newLedMode) {
|
||||
case kDefault:
|
||||
setStateImpl(pipelineModeSupplier.getAsBoolean());
|
||||
break;
|
||||
case kOff:
|
||||
setStateImpl(false);
|
||||
break;
|
||||
case kOn:
|
||||
setStateImpl(true);
|
||||
break;
|
||||
case kBlink:
|
||||
blinkImpl(85, -1);
|
||||
break;
|
||||
case kDefault -> setStateImpl(pipelineModeSupplier.getAsBoolean());
|
||||
case kOff -> setStateImpl(false);
|
||||
case kOn -> setStateImpl(true);
|
||||
case kBlink -> blinkImpl(85, -1);
|
||||
}
|
||||
}
|
||||
logger.info("Changing LED internal state to " + newLedMode.toString());
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
|
||||
package org.photonvision.common.logging;
|
||||
|
||||
import edu.wpi.first.util.RuntimeDetector;
|
||||
import org.photonvision.common.hardware.Platform;
|
||||
import org.photonvision.common.util.TimedTaskManager;
|
||||
import org.photonvision.jni.QueuedFileLogger;
|
||||
|
||||
@@ -39,7 +39,7 @@ public class KernelLogLogger {
|
||||
Logger logger = new Logger(KernelLogLogger.class, LogGroup.General);
|
||||
|
||||
public KernelLogLogger() {
|
||||
if (RuntimeDetector.isLinux()) {
|
||||
if (Platform.isLinux()) {
|
||||
listener = new QueuedFileLogger("/var/log/kern.log");
|
||||
} else {
|
||||
System.out.println("NOT for klogs");
|
||||
|
||||
@@ -113,7 +113,7 @@ public class NetworkManager {
|
||||
}
|
||||
|
||||
public void reinitialize() {
|
||||
initialize(ConfigManager.getInstance().getConfig().getNetworkConfig().shouldManage());
|
||||
initialize(ConfigManager.getInstance().getConfig().getNetworkConfig().shouldManage);
|
||||
|
||||
DataChangeService.getInstance()
|
||||
.publishEvent(
|
||||
|
||||
@@ -51,16 +51,9 @@ public abstract class NumberCouple<T extends Number> {
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (!(obj instanceof NumberCouple)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
var couple = (NumberCouple) obj;
|
||||
if (!couple.first.equals(first)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return couple.second.equals(second);
|
||||
return obj instanceof NumberCouple<?> couple
|
||||
&& couple.first.equals(first)
|
||||
&& couple.second.equals(second);
|
||||
}
|
||||
|
||||
@JsonIgnore
|
||||
|
||||
@@ -69,7 +69,8 @@ public abstract class PhotonJNICommon {
|
||||
logger.error("Couldn't load shared object " + libraryName, e);
|
||||
e.printStackTrace();
|
||||
// logger.error(System.getProperty("java.library.path"));
|
||||
break;
|
||||
instance.setLoaded(false);
|
||||
return;
|
||||
}
|
||||
}
|
||||
instance.setLoaded(true);
|
||||
|
||||
@@ -95,9 +95,11 @@ public class GenericUSBCameraSettables extends VisionSourceSettables {
|
||||
// first.
|
||||
var autoExpProp = findProperty("exposure_auto", "auto_exposure");
|
||||
|
||||
exposureAbsProp = expProp.get();
|
||||
this.minExposure = exposureAbsProp.getMin();
|
||||
this.maxExposure = exposureAbsProp.getMax();
|
||||
if (expProp.isPresent()) {
|
||||
exposureAbsProp = expProp.get();
|
||||
this.minExposure = exposureAbsProp.getMin();
|
||||
this.maxExposure = exposureAbsProp.getMax();
|
||||
}
|
||||
|
||||
if (autoExpProp.isPresent()) {
|
||||
autoExposureProp = autoExpProp.get();
|
||||
@@ -184,7 +186,7 @@ public class GenericUSBCameraSettables extends VisionSourceSettables {
|
||||
softSet("auto_exposure_bias", 12);
|
||||
softSet("iso_sensitivity_auto", 1);
|
||||
softSet("iso_sensitivity", 1); // Manual ISO adjustment by default
|
||||
autoExposureProp.set(PROP_AUTO_EXPOSURE_ENABLED);
|
||||
if (autoExposureProp != null) autoExposureProp.set(PROP_AUTO_EXPOSURE_ENABLED);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -22,9 +22,9 @@ import edu.wpi.first.cscore.CvSink;
|
||||
import edu.wpi.first.cscore.UsbCamera;
|
||||
import edu.wpi.first.cscore.VideoException;
|
||||
import edu.wpi.first.cscore.VideoProperty;
|
||||
import edu.wpi.first.util.RuntimeDetector;
|
||||
import java.util.*;
|
||||
import org.photonvision.common.configuration.CameraConfiguration;
|
||||
import org.photonvision.common.hardware.Platform;
|
||||
import org.photonvision.common.logging.LogGroup;
|
||||
import org.photonvision.common.logging.Logger;
|
||||
import org.photonvision.vision.camera.CameraQuirk;
|
||||
@@ -113,7 +113,7 @@ public class USBCameraSource extends VisionSource {
|
||||
GenericUSBCameraSettables settables;
|
||||
|
||||
if (quirks.hasQuirk(CameraQuirk.LifeCamControls)) {
|
||||
if (RuntimeDetector.isWindows()) {
|
||||
if (Platform.isWindows()) {
|
||||
logger.debug("Using Microsoft Lifecam 3000 Windows-Specific Settables");
|
||||
settables = new LifeCam3kWindowsCameraSettables(config, camera);
|
||||
} else {
|
||||
@@ -124,7 +124,7 @@ public class USBCameraSource extends VisionSource {
|
||||
logger.debug("Using PlayStation Eye Camera Settables");
|
||||
settables = new PsEyeCameraSettables(config, camera);
|
||||
} else if (quirks.hasQuirk(CameraQuirk.ArduOV2311Controls)) {
|
||||
if (RuntimeDetector.isWindows()) {
|
||||
if (Platform.isWindows()) {
|
||||
logger.debug("Using Arducam OV2311 Windows-Specific Settables");
|
||||
settables = new ArduOV2311WindowsCameraSettables(config, camera);
|
||||
} else {
|
||||
|
||||
@@ -135,20 +135,14 @@ public class MJPGFrameConsumer implements AutoCloseable {
|
||||
}
|
||||
|
||||
private static String pixelFormatToString(PixelFormat pixelFormat) {
|
||||
switch (pixelFormat) {
|
||||
case kMJPEG:
|
||||
return "MJPEG";
|
||||
case kYUYV:
|
||||
return "YUYV";
|
||||
case kRGB565:
|
||||
return "RGB565";
|
||||
case kBGR:
|
||||
return "BGR";
|
||||
case kGray:
|
||||
return "Gray";
|
||||
default:
|
||||
return "Unknown";
|
||||
}
|
||||
return switch (pixelFormat) {
|
||||
case kMJPEG -> "MJPEG";
|
||||
case kYUYV -> "YUYV";
|
||||
case kRGB565 -> "RGB565";
|
||||
case kBGR -> "BGR";
|
||||
case kGray -> "Gray";
|
||||
case kUYVY, kUnknown, kY16, kBGRA -> "Unknown";
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -158,18 +158,19 @@ public class Contour implements Releasable {
|
||||
double massX = (x0A + x0B) / 2;
|
||||
double massY = (y0A + y0B) / 2;
|
||||
switch (intersectionDirection) {
|
||||
case Up:
|
||||
case None -> {}
|
||||
case Up -> {
|
||||
if (intersectionY < massY) isIntersecting = true;
|
||||
break;
|
||||
case Down:
|
||||
}
|
||||
case Down -> {
|
||||
if (intersectionY > massY) isIntersecting = true;
|
||||
break;
|
||||
case Left:
|
||||
}
|
||||
case Left -> {
|
||||
if (intersectionX < massX) isIntersecting = true;
|
||||
break;
|
||||
case Right:
|
||||
}
|
||||
case Right -> {
|
||||
if (intersectionX > massX) isIntersecting = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
intersectMatA.release();
|
||||
intersectMatB.release();
|
||||
|
||||
@@ -17,9 +17,6 @@
|
||||
|
||||
package org.photonvision.vision.opencv;
|
||||
|
||||
import java.util.EnumSet;
|
||||
import java.util.HashMap;
|
||||
|
||||
public enum ContourShape {
|
||||
Circle(0),
|
||||
Custom(-1),
|
||||
@@ -32,15 +29,12 @@ public enum ContourShape {
|
||||
this.sides = sides;
|
||||
}
|
||||
|
||||
private static final HashMap<Integer, ContourShape> sidesToValueMap = new HashMap<>();
|
||||
|
||||
static {
|
||||
for (var value : EnumSet.allOf(ContourShape.class)) {
|
||||
sidesToValueMap.put(value.sides, value);
|
||||
}
|
||||
}
|
||||
|
||||
public static ContourShape fromSides(int sides) {
|
||||
return sidesToValueMap.get(sides);
|
||||
return switch (sides) {
|
||||
case 0 -> Circle;
|
||||
case 3 -> Triangle;
|
||||
case 4 -> Quadrilateral;
|
||||
default -> Custom;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,13 +48,14 @@ public class Draw2dCrosshairPipe
|
||||
double scale = params.frameStaticProperties.imageWidth / (double) params.divisor.value / 32.0;
|
||||
|
||||
switch (params.robotOffsetPointMode) {
|
||||
case Single:
|
||||
case None -> {}
|
||||
case Single -> {
|
||||
if (params.singleOffsetPoint.x != 0 && params.singleOffsetPoint.y != 0) {
|
||||
x = params.singleOffsetPoint.x;
|
||||
y = params.singleOffsetPoint.y;
|
||||
}
|
||||
break;
|
||||
case Dual:
|
||||
}
|
||||
case Dual -> {
|
||||
if (!in.getRight().isEmpty()) {
|
||||
var target = in.getRight().get(0);
|
||||
if (target != null) {
|
||||
@@ -65,7 +66,7 @@ public class Draw2dCrosshairPipe
|
||||
y = offsetCrosshair.y;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
x /= (double) params.divisor.value;
|
||||
|
||||
@@ -50,23 +50,7 @@ public class FindPolygonPipe
|
||||
|
||||
private CVShape getShape(Contour in) {
|
||||
int corners = getCorners(in);
|
||||
|
||||
/*The contourShape enum has predefined shapes for Circles, Triangles, and Quads
|
||||
meaning any shape not fitting in those predefined shapes must be a custom shape.
|
||||
*/
|
||||
if (ContourShape.fromSides(corners) == null) {
|
||||
return new CVShape(in, ContourShape.Custom);
|
||||
}
|
||||
switch (ContourShape.fromSides(corners)) {
|
||||
case Circle:
|
||||
return new CVShape(in, ContourShape.Circle);
|
||||
case Triangle:
|
||||
return new CVShape(in, ContourShape.Triangle);
|
||||
case Quadrilateral:
|
||||
return new CVShape(in, ContourShape.Quadrilateral);
|
||||
}
|
||||
|
||||
return new CVShape(in, ContourShape.Custom);
|
||||
return new CVShape(in, ContourShape.fromSides(corners));
|
||||
}
|
||||
|
||||
private int getCorners(Contour contour) {
|
||||
|
||||
@@ -85,19 +85,29 @@ public class ArucoPipeline extends CVPipeline<CVPipelineResult, ArucoPipelineSet
|
||||
// 2023/other: best guess is 6in
|
||||
double tagWidth = Units.inchesToMeters(6);
|
||||
TargetModel tagModel = TargetModel.kAprilTag16h5;
|
||||
switch (settings.tagFamily) {
|
||||
case kTag36h11:
|
||||
// 2024 tag, 6.5in
|
||||
params.tagFamily = Objdetect.DICT_APRILTAG_36h11;
|
||||
tagWidth = Units.inchesToMeters(6.5);
|
||||
tagModel = TargetModel.kAprilTag36h11;
|
||||
break;
|
||||
case kTag25h9:
|
||||
params.tagFamily = Objdetect.DICT_APRILTAG_25h9;
|
||||
break;
|
||||
default:
|
||||
params.tagFamily = Objdetect.DICT_APRILTAG_16h5;
|
||||
}
|
||||
|
||||
params.tagFamily =
|
||||
switch (settings.tagFamily) {
|
||||
case kTag36h11 -> {
|
||||
// 2024 tag, 6.5in
|
||||
tagWidth = Units.inchesToMeters(6.5);
|
||||
tagModel = TargetModel.kAprilTag36h11;
|
||||
yield Objdetect.DICT_APRILTAG_36h11;
|
||||
}
|
||||
case kTag25h9 -> Objdetect.DICT_APRILTAG_25h9;
|
||||
// TODO: explicitly drop support for these
|
||||
case kTag16h5,
|
||||
kTagCircle21h7,
|
||||
kTagCircle49h12,
|
||||
kTagCustom48h11,
|
||||
kTagStandard41h12,
|
||||
kTagStandard52h13 -> {
|
||||
// 2024 tag, 6.5in
|
||||
tagWidth = Units.inchesToMeters(6.5);
|
||||
tagModel = TargetModel.kAprilTag36h11;
|
||||
yield Objdetect.DICT_APRILTAG_36h11;
|
||||
}
|
||||
};
|
||||
|
||||
int threshMinSize = Math.max(3, settings.threshWinSizes.getFirst());
|
||||
settings.threshWinSizes.setFirst(threshMinSize);
|
||||
|
||||
@@ -26,6 +26,7 @@ import org.photonvision.common.configuration.CameraConfiguration;
|
||||
import org.photonvision.common.configuration.ConfigManager;
|
||||
import org.photonvision.common.dataflow.DataChangeService;
|
||||
import org.photonvision.common.dataflow.events.OutgoingUIEvent;
|
||||
import org.photonvision.common.dataflow.websocket.UIPhotonConfiguration;
|
||||
import org.photonvision.common.logging.LogGroup;
|
||||
import org.photonvision.common.logging.Logger;
|
||||
import org.photonvision.vision.pipeline.*;
|
||||
@@ -231,7 +232,8 @@ public class PipelineManager {
|
||||
DataChangeService.getInstance()
|
||||
.publishEvent(
|
||||
new OutgoingUIEvent<>(
|
||||
"fullsettings", ConfigManager.getInstance().getConfig().toHashMap()));
|
||||
"fullsettings",
|
||||
UIPhotonConfiguration.programStateToUi(ConfigManager.getInstance().getConfig())));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -239,41 +241,38 @@ public class PipelineManager {
|
||||
* recreation after changing pipeline type
|
||||
*/
|
||||
private void recreateUserPipeline() {
|
||||
// Cleanup potential old native resources before swapping over from a user
|
||||
// pipeline
|
||||
// Cleanup potential old native resources before swapping over from a user pipeline
|
||||
if (currentUserPipeline != null && !(currentPipelineIndex < 0)) {
|
||||
currentUserPipeline.release();
|
||||
}
|
||||
|
||||
var desiredPipelineSettings = userPipelineSettings.get(currentPipelineIndex);
|
||||
switch (desiredPipelineSettings.pipelineType) {
|
||||
case Reflective:
|
||||
case Reflective -> {
|
||||
logger.debug("Creating Reflective pipeline");
|
||||
currentUserPipeline =
|
||||
new ReflectivePipeline((ReflectivePipelineSettings) desiredPipelineSettings);
|
||||
break;
|
||||
case ColoredShape:
|
||||
}
|
||||
case ColoredShape -> {
|
||||
logger.debug("Creating ColoredShape pipeline");
|
||||
currentUserPipeline =
|
||||
new ColoredShapePipeline((ColoredShapePipelineSettings) desiredPipelineSettings);
|
||||
break;
|
||||
case AprilTag:
|
||||
}
|
||||
case AprilTag -> {
|
||||
logger.debug("Creating AprilTag pipeline");
|
||||
currentUserPipeline =
|
||||
new AprilTagPipeline((AprilTagPipelineSettings) desiredPipelineSettings);
|
||||
break;
|
||||
|
||||
case Aruco:
|
||||
}
|
||||
case Aruco -> {
|
||||
logger.debug("Creating Aruco Pipeline");
|
||||
currentUserPipeline = new ArucoPipeline((ArucoPipelineSettings) desiredPipelineSettings);
|
||||
break;
|
||||
case ObjectDetection:
|
||||
}
|
||||
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;
|
||||
}
|
||||
case Calib3d, DriverMode -> {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -340,44 +339,40 @@ public class PipelineManager {
|
||||
}
|
||||
|
||||
private CVPipelineSettings createSettingsForType(PipelineType type, String nickname) {
|
||||
CVPipelineSettings newSettings;
|
||||
switch (type) {
|
||||
case Reflective:
|
||||
{
|
||||
var added = new ReflectivePipelineSettings();
|
||||
added.pipelineNickname = nickname;
|
||||
return added;
|
||||
}
|
||||
case ColoredShape:
|
||||
{
|
||||
var added = new ColoredShapePipelineSettings();
|
||||
added.pipelineNickname = nickname;
|
||||
return added;
|
||||
}
|
||||
case AprilTag:
|
||||
{
|
||||
var added = new AprilTagPipelineSettings();
|
||||
added.pipelineNickname = nickname;
|
||||
return added;
|
||||
}
|
||||
case Aruco:
|
||||
{
|
||||
var added = new ArucoPipelineSettings();
|
||||
added.pipelineNickname = nickname;
|
||||
return added;
|
||||
}
|
||||
case ObjectDetection:
|
||||
{
|
||||
var added = new ObjectDetectionPipelineSettings();
|
||||
added.pipelineNickname = nickname;
|
||||
return added;
|
||||
}
|
||||
default:
|
||||
{
|
||||
logger.error("Got invalid pipeline type: " + type);
|
||||
return null;
|
||||
}
|
||||
case Reflective -> {
|
||||
var added = new ReflectivePipelineSettings();
|
||||
added.pipelineNickname = nickname;
|
||||
return added;
|
||||
}
|
||||
case ColoredShape -> {
|
||||
var added = new ColoredShapePipelineSettings();
|
||||
added.pipelineNickname = nickname;
|
||||
return added;
|
||||
}
|
||||
case AprilTag -> {
|
||||
var added = new AprilTagPipelineSettings();
|
||||
added.pipelineNickname = nickname;
|
||||
return added;
|
||||
}
|
||||
case Aruco -> {
|
||||
var added = new ArucoPipelineSettings();
|
||||
added.pipelineNickname = nickname;
|
||||
return added;
|
||||
}
|
||||
case ObjectDetection -> {
|
||||
var added = new ObjectDetectionPipelineSettings();
|
||||
added.pipelineNickname = nickname;
|
||||
return added;
|
||||
}
|
||||
case Calib3d, DriverMode -> {
|
||||
logger.error("Got invalid pipeline type: " + type);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// This can never happen, this is here to satisfy the compiler.
|
||||
throw new IllegalStateException("Got impossible pipeline type: " + type);
|
||||
}
|
||||
|
||||
private void addPipelineInternal(CVPipelineSettings settings) {
|
||||
|
||||
@@ -30,13 +30,14 @@ import java.util.stream.Collectors;
|
||||
import org.opencv.core.Size;
|
||||
import org.photonvision.common.configuration.CameraConfiguration;
|
||||
import org.photonvision.common.configuration.ConfigManager;
|
||||
import org.photonvision.common.configuration.PhotonConfiguration;
|
||||
import org.photonvision.common.dataflow.CVPipelineResultConsumer;
|
||||
import org.photonvision.common.dataflow.DataChangeService;
|
||||
import org.photonvision.common.dataflow.events.OutgoingUIEvent;
|
||||
import org.photonvision.common.dataflow.networktables.NTDataPublisher;
|
||||
import org.photonvision.common.dataflow.statusLEDs.StatusLEDConsumer;
|
||||
import org.photonvision.common.dataflow.websocket.UICameraConfiguration;
|
||||
import org.photonvision.common.dataflow.websocket.UIDataPublisher;
|
||||
import org.photonvision.common.dataflow.websocket.UIPhotonConfiguration;
|
||||
import org.photonvision.common.hardware.HardwareManager;
|
||||
import org.photonvision.common.logging.LogGroup;
|
||||
import org.photonvision.common.logging.Logger;
|
||||
@@ -489,7 +490,8 @@ public class VisionModule {
|
||||
DataChangeService.getInstance()
|
||||
.publishEvent(
|
||||
new OutgoingUIEvent<>(
|
||||
"fullsettings", ConfigManager.getInstance().getConfig().toHashMap()));
|
||||
"fullsettings",
|
||||
UIPhotonConfiguration.programStateToUi(ConfigManager.getInstance().getConfig())));
|
||||
}
|
||||
|
||||
void saveAndBroadcastSelective(WsContext originContext, String propertyName, Object value) {
|
||||
@@ -516,8 +518,8 @@ public class VisionModule {
|
||||
saveAndBroadcastAll();
|
||||
}
|
||||
|
||||
public PhotonConfiguration.UICameraConfiguration toUICameraConfig() {
|
||||
var ret = new PhotonConfiguration.UICameraConfiguration();
|
||||
public UICameraConfiguration toUICameraConfig() {
|
||||
var ret = new UICameraConfiguration();
|
||||
|
||||
ret.fov = visionSource.getSettables().getFOV();
|
||||
ret.isCSICamera = visionSource.getCameraConfiguration().cameraType == CameraType.ZeroCopyPicam;
|
||||
@@ -585,11 +587,9 @@ public class VisionModule {
|
||||
|
||||
// Pipelines like DriverMode and Calibrate3dPipeline have null output frames
|
||||
if (result.inputAndOutputFrame != null
|
||||
&& (pipelineManager.getCurrentPipelineSettings() instanceof AdvancedPipelineSettings)) {
|
||||
streamRunnable.updateData(
|
||||
result.inputAndOutputFrame,
|
||||
(AdvancedPipelineSettings) pipelineManager.getCurrentPipelineSettings(),
|
||||
result.targets);
|
||||
&& (pipelineManager.getCurrentPipelineSettings()
|
||||
instanceof AdvancedPipelineSettings settings)) {
|
||||
streamRunnable.updateData(result.inputAndOutputFrame, settings, result.targets);
|
||||
// The streamRunnable manages releasing in this case
|
||||
} else {
|
||||
consumeResults(result.inputAndOutputFrame, result.targets);
|
||||
@@ -613,9 +613,9 @@ public class VisionModule {
|
||||
}
|
||||
|
||||
public void setTargetModel(TargetModel targetModel) {
|
||||
var settings = pipelineManager.getCurrentPipeline().getSettings();
|
||||
if (settings instanceof ReflectivePipelineSettings) {
|
||||
((ReflectivePipelineSettings) settings).targetModel = targetModel;
|
||||
var pipelineSettings = pipelineManager.getCurrentPipeline().getSettings();
|
||||
if (pipelineSettings instanceof ReflectivePipelineSettings settings) {
|
||||
settings.targetModel = targetModel;
|
||||
saveAndBroadcastAll();
|
||||
} else {
|
||||
logger.error("Cannot set target model of non-reflective pipe! Ignoring...");
|
||||
|
||||
@@ -55,11 +55,8 @@ public class VisionModuleChangeSubscriber extends DataChangeSubscriber {
|
||||
|
||||
@Override
|
||||
public void onDataChangeEvent(DataChangeEvent<?> event) {
|
||||
if (event instanceof IncomingWebSocketEvent) {
|
||||
var wsEvent = (IncomingWebSocketEvent<?>) event;
|
||||
|
||||
// Camera index -1 means a "multicast event" (i.e. the event is received by all
|
||||
// cameras)
|
||||
if (event instanceof IncomingWebSocketEvent wsEvent) {
|
||||
// Camera index -1 means a "multicast event" (i.e. the event is received by all cameras)
|
||||
if (wsEvent.cameraIndex != null
|
||||
&& (wsEvent.cameraIndex == parentModule.moduleIndex || wsEvent.cameraIndex == -1)) {
|
||||
logger.trace("Got PSC event - propName: " + wsEvent.propertyName);
|
||||
@@ -93,120 +90,32 @@ public class VisionModuleChangeSubscriber extends DataChangeSubscriber {
|
||||
var currentSettings = change.getCurrentSettings();
|
||||
var originContext = change.getOriginContext();
|
||||
switch (propName) {
|
||||
case "pipelineName": // rename current pipeline
|
||||
logger.info("Changing nick to " + newPropValue);
|
||||
parentModule.pipelineManager.getCurrentPipelineSettings().pipelineNickname =
|
||||
(String) newPropValue;
|
||||
parentModule.saveAndBroadcastAll();
|
||||
continue;
|
||||
case "newPipelineInfo": // add new pipeline
|
||||
var typeName = (Pair<String, PipelineType>) newPropValue;
|
||||
var type = typeName.getRight();
|
||||
var name = typeName.getLeft();
|
||||
|
||||
logger.info("Adding a " + type + " pipeline with name " + name);
|
||||
|
||||
var addedSettings = parentModule.pipelineManager.addPipeline(type);
|
||||
addedSettings.pipelineNickname = name;
|
||||
parentModule.saveAndBroadcastAll();
|
||||
continue;
|
||||
case "deleteCurrPipeline":
|
||||
var indexToDelete = parentModule.pipelineManager.getRequestedIndex();
|
||||
logger.info("Deleting current pipe at index " + indexToDelete);
|
||||
int newIndex = parentModule.pipelineManager.removePipeline(indexToDelete);
|
||||
parentModule.setPipeline(newIndex);
|
||||
parentModule.saveAndBroadcastAll();
|
||||
continue;
|
||||
case "changePipeline": // change active pipeline
|
||||
var index = (Integer) newPropValue;
|
||||
if (index == parentModule.pipelineManager.getRequestedIndex()) {
|
||||
logger.debug("Skipping pipeline change, index " + index + " already active");
|
||||
continue;
|
||||
case "pipelineName" -> newPipelineNickname((String) newPropValue);
|
||||
case "newPipelineInfo" -> newPipelineInfo((Pair<String, PipelineType>) newPropValue);
|
||||
case "deleteCurrPipeline" -> deleteCurrPipeline();
|
||||
case "changePipeline" -> changePipeline((Integer) newPropValue);
|
||||
case "startCalibration" -> startCalibration((Map<String, Object>) newPropValue);
|
||||
case "saveInputSnapshot" -> parentModule.saveInputSnapshot();
|
||||
case "saveOutputSnapshot" -> parentModule.saveOutputSnapshot();
|
||||
case "takeCalSnapshot" -> parentModule.takeCalibrationSnapshot();
|
||||
case "duplicatePipeline" -> duplicatePipeline((Integer) newPropValue);
|
||||
case "calibrationUploaded" -> {
|
||||
if (newPropValue instanceof CameraCalibrationCoefficients newCal) {
|
||||
parentModule.addCalibrationToConfig(newCal);
|
||||
} else {
|
||||
logger.warn("Received invalid calibration data");
|
||||
}
|
||||
parentModule.setPipeline(index);
|
||||
parentModule.saveAndBroadcastAll();
|
||||
continue;
|
||||
case "startCalibration":
|
||||
try {
|
||||
var data =
|
||||
JacksonUtils.deserialize(
|
||||
(Map<String, Object>) newPropValue, UICalibrationData.class);
|
||||
parentModule.startCalibration(data);
|
||||
parentModule.saveAndBroadcastAll();
|
||||
} catch (Exception e) {
|
||||
logger.error("Error deserailizing start-cal request", e);
|
||||
}
|
||||
case "robotOffsetPoint" -> {
|
||||
if (currentSettings instanceof AdvancedPipelineSettings curAdvSettings) {
|
||||
robotOffsetPoint(curAdvSettings, (Integer) newPropValue);
|
||||
}
|
||||
continue;
|
||||
case "saveInputSnapshot":
|
||||
parentModule.saveInputSnapshot();
|
||||
continue;
|
||||
case "saveOutputSnapshot":
|
||||
parentModule.saveOutputSnapshot();
|
||||
continue;
|
||||
case "takeCalSnapshot":
|
||||
parentModule.takeCalibrationSnapshot();
|
||||
continue;
|
||||
case "duplicatePipeline":
|
||||
int idx = parentModule.pipelineManager.duplicatePipeline((Integer) newPropValue);
|
||||
parentModule.setPipeline(idx);
|
||||
parentModule.saveAndBroadcastAll();
|
||||
continue;
|
||||
case "calibrationUploaded":
|
||||
if (newPropValue instanceof CameraCalibrationCoefficients)
|
||||
parentModule.addCalibrationToConfig((CameraCalibrationCoefficients) newPropValue);
|
||||
continue;
|
||||
case "robotOffsetPoint":
|
||||
if (currentSettings instanceof AdvancedPipelineSettings) {
|
||||
var curAdvSettings = (AdvancedPipelineSettings) currentSettings;
|
||||
var offsetOperation = RobotOffsetPointOperation.fromIndex((int) newPropValue);
|
||||
var latestTarget = parentModule.lastPipelineResultBestTarget;
|
||||
|
||||
if (latestTarget != null) {
|
||||
var newPoint = latestTarget.getTargetOffsetPoint();
|
||||
|
||||
switch (curAdvSettings.offsetRobotOffsetMode) {
|
||||
case Single:
|
||||
if (offsetOperation == RobotOffsetPointOperation.ROPO_CLEAR) {
|
||||
curAdvSettings.offsetSinglePoint = new Point();
|
||||
} else if (offsetOperation == RobotOffsetPointOperation.ROPO_TAKESINGLE) {
|
||||
curAdvSettings.offsetSinglePoint = newPoint;
|
||||
}
|
||||
break;
|
||||
case Dual:
|
||||
if (offsetOperation == RobotOffsetPointOperation.ROPO_CLEAR) {
|
||||
curAdvSettings.offsetDualPointA = new Point();
|
||||
curAdvSettings.offsetDualPointAArea = 0;
|
||||
curAdvSettings.offsetDualPointB = new Point();
|
||||
curAdvSettings.offsetDualPointBArea = 0;
|
||||
} else {
|
||||
// update point and area
|
||||
switch (offsetOperation) {
|
||||
case ROPO_TAKEFIRSTDUAL:
|
||||
curAdvSettings.offsetDualPointA = newPoint;
|
||||
curAdvSettings.offsetDualPointAArea = latestTarget.getArea();
|
||||
break;
|
||||
case ROPO_TAKESECONDDUAL:
|
||||
curAdvSettings.offsetDualPointB = newPoint;
|
||||
curAdvSettings.offsetDualPointBArea = latestTarget.getArea();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
continue;
|
||||
case "changePipelineType":
|
||||
}
|
||||
case "changePipelineType" -> {
|
||||
parentModule.changePipelineType((Integer) newPropValue);
|
||||
parentModule.saveAndBroadcastAll();
|
||||
continue;
|
||||
case "isDriverMode":
|
||||
parentModule.setDriverMode((Boolean) newPropValue);
|
||||
continue;
|
||||
}
|
||||
case "isDriverMode" -> parentModule.setDriverMode((Boolean) newPropValue);
|
||||
}
|
||||
|
||||
// special case for camera settables
|
||||
@@ -249,6 +158,104 @@ public class VisionModuleChangeSubscriber extends DataChangeSubscriber {
|
||||
}
|
||||
}
|
||||
|
||||
public void newPipelineNickname(String newNickname) {
|
||||
logger.info("Changing pipeline nickname to " + newNickname);
|
||||
parentModule.pipelineManager.getCurrentPipelineSettings().pipelineNickname = newNickname;
|
||||
parentModule.saveAndBroadcastAll();
|
||||
}
|
||||
|
||||
public void newPipelineInfo(Pair<String, PipelineType> typeName) {
|
||||
var type = typeName.getRight();
|
||||
var name = typeName.getLeft();
|
||||
|
||||
logger.info("Adding a " + type + " pipeline with name " + name);
|
||||
|
||||
var addedSettings = parentModule.pipelineManager.addPipeline(type);
|
||||
addedSettings.pipelineNickname = name;
|
||||
parentModule.saveAndBroadcastAll();
|
||||
}
|
||||
|
||||
public void deleteCurrPipeline() {
|
||||
var indexToDelete = parentModule.pipelineManager.getRequestedIndex();
|
||||
logger.info("Deleting current pipe at index " + indexToDelete);
|
||||
int newIndex = parentModule.pipelineManager.removePipeline(indexToDelete);
|
||||
parentModule.setPipeline(newIndex);
|
||||
parentModule.saveAndBroadcastAll();
|
||||
}
|
||||
|
||||
public void changePipeline(int index) {
|
||||
if (index == parentModule.pipelineManager.getRequestedIndex()) {
|
||||
logger.debug("Skipping pipeline change, index " + index + " already active");
|
||||
return;
|
||||
}
|
||||
parentModule.setPipeline(index);
|
||||
parentModule.saveAndBroadcastAll();
|
||||
}
|
||||
|
||||
public void startCalibration(Map<String, Object> data) {
|
||||
try {
|
||||
var deserialized = JacksonUtils.deserialize(data, UICalibrationData.class);
|
||||
parentModule.startCalibration(deserialized);
|
||||
parentModule.saveAndBroadcastAll();
|
||||
} catch (Exception e) {
|
||||
logger.error("Error deserailizing start-calibration request", e);
|
||||
}
|
||||
}
|
||||
|
||||
public void duplicatePipeline(int index) {
|
||||
var newIndex = parentModule.pipelineManager.duplicatePipeline(index);
|
||||
parentModule.setPipeline(newIndex);
|
||||
parentModule.saveAndBroadcastAll();
|
||||
}
|
||||
|
||||
public void robotOffsetPoint(AdvancedPipelineSettings curAdvSettings, int offsetIndex) {
|
||||
RobotOffsetPointOperation offsetOperation = RobotOffsetPointOperation.fromIndex(offsetIndex);
|
||||
|
||||
var latestTarget = parentModule.lastPipelineResultBestTarget;
|
||||
if (latestTarget == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
var newPoint = latestTarget.getTargetOffsetPoint();
|
||||
switch (curAdvSettings.offsetRobotOffsetMode) {
|
||||
case Single -> {
|
||||
switch (offsetOperation) {
|
||||
case CLEAR -> curAdvSettings.offsetSinglePoint = new Point();
|
||||
case TAKE_SINGLE -> curAdvSettings.offsetSinglePoint = newPoint;
|
||||
case TAKE_FIRST_DUAL, TAKE_SECOND_DUAL -> {
|
||||
logger.warn("Dual point operation in single point mode");
|
||||
}
|
||||
}
|
||||
}
|
||||
case Dual -> {
|
||||
switch (offsetOperation) {
|
||||
case CLEAR -> {
|
||||
curAdvSettings.offsetDualPointA = new Point();
|
||||
curAdvSettings.offsetDualPointAArea = 0;
|
||||
curAdvSettings.offsetDualPointB = new Point();
|
||||
curAdvSettings.offsetDualPointBArea = 0;
|
||||
}
|
||||
case TAKE_FIRST_DUAL -> {
|
||||
// update point and area
|
||||
curAdvSettings.offsetDualPointA = newPoint;
|
||||
curAdvSettings.offsetDualPointAArea = latestTarget.getArea();
|
||||
}
|
||||
case TAKE_SECOND_DUAL -> {
|
||||
// update point and area
|
||||
curAdvSettings.offsetDualPointB = newPoint;
|
||||
curAdvSettings.offsetDualPointBArea = latestTarget.getArea();
|
||||
}
|
||||
case TAKE_SINGLE -> {
|
||||
logger.warn("Single point operation in dual point mode");
|
||||
}
|
||||
}
|
||||
}
|
||||
case None -> {
|
||||
logger.warn("Robot offset point operation requested, but no offset mode set");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the value of a property in the given object using reflection. This method should not be
|
||||
* used generally and is only known to be correct in the context of `onDataChangeEvent`.
|
||||
@@ -281,8 +288,8 @@ public class VisionModuleChangeSubscriber extends DataChangeSubscriber {
|
||||
} else if (propType.equals(Integer.TYPE)) {
|
||||
propField.setInt(currentSettings, (Integer) newPropValue);
|
||||
} else if (propType.equals(Boolean.TYPE)) {
|
||||
if (newPropValue instanceof Integer) {
|
||||
propField.setBoolean(currentSettings, (Integer) newPropValue != 0);
|
||||
if (newPropValue instanceof Integer intValue) {
|
||||
propField.setBoolean(currentSettings, intValue != 0);
|
||||
} else {
|
||||
propField.setBoolean(currentSettings, (Boolean) newPropValue);
|
||||
}
|
||||
|
||||
@@ -84,8 +84,7 @@ public class VisionRunner {
|
||||
|
||||
frameSupplier.requestFrameThresholdType(wantedProcessType);
|
||||
var settings = pipeline.getSettings();
|
||||
if (settings instanceof AdvancedPipelineSettings) {
|
||||
var advanced = (AdvancedPipelineSettings) settings;
|
||||
if (settings instanceof AdvancedPipelineSettings advanced) {
|
||||
var hsvParams =
|
||||
new HSVPipe.HSVParams(
|
||||
advanced.hsvHue, advanced.hsvSaturation, advanced.hsvValue, advanced.hueInverted);
|
||||
|
||||
@@ -29,6 +29,7 @@ import org.photonvision.common.configuration.CameraConfiguration;
|
||||
import org.photonvision.common.configuration.ConfigManager;
|
||||
import org.photonvision.common.dataflow.DataChangeService;
|
||||
import org.photonvision.common.dataflow.events.OutgoingUIEvent;
|
||||
import org.photonvision.common.dataflow.websocket.UIPhotonConfiguration;
|
||||
import org.photonvision.common.hardware.Platform;
|
||||
import org.photonvision.common.hardware.Platform.OSType;
|
||||
import org.photonvision.common.logging.LogGroup;
|
||||
@@ -122,7 +123,8 @@ public class VisionSourceManager {
|
||||
DataChangeService.getInstance()
|
||||
.publishEvent(
|
||||
new OutgoingUIEvent<>(
|
||||
"fullsettings", ConfigManager.getInstance().getConfig().toHashMap()));
|
||||
"fullsettings",
|
||||
UIPhotonConfiguration.programStateToUi(ConfigManager.getInstance().getConfig())));
|
||||
}
|
||||
|
||||
protected List<VisionSource> tryMatchCamImpl() {
|
||||
|
||||
@@ -17,10 +17,10 @@
|
||||
package org.photonvision.vision.target;
|
||||
|
||||
public enum RobotOffsetPointOperation {
|
||||
ROPO_CLEAR(0),
|
||||
ROPO_TAKESINGLE(1),
|
||||
ROPO_TAKEFIRSTDUAL(2),
|
||||
ROPO_TAKESECONDDUAL(3);
|
||||
CLEAR(0),
|
||||
TAKE_SINGLE(1),
|
||||
TAKE_FIRST_DUAL(2),
|
||||
TAKE_SECOND_DUAL(3);
|
||||
|
||||
public final int index;
|
||||
|
||||
@@ -29,17 +29,12 @@ public enum RobotOffsetPointOperation {
|
||||
}
|
||||
|
||||
public static RobotOffsetPointOperation fromIndex(int index) {
|
||||
switch (index) {
|
||||
case 0:
|
||||
return ROPO_CLEAR;
|
||||
case 1:
|
||||
return ROPO_TAKESINGLE;
|
||||
case 2:
|
||||
return ROPO_TAKEFIRSTDUAL;
|
||||
case 3:
|
||||
return ROPO_TAKESECONDDUAL;
|
||||
default:
|
||||
return ROPO_CLEAR;
|
||||
}
|
||||
return switch (index) {
|
||||
case 0 -> CLEAR;
|
||||
case 1 -> TAKE_SINGLE;
|
||||
case 2 -> TAKE_FIRST_DUAL;
|
||||
case 3 -> TAKE_SECOND_DUAL;
|
||||
default -> CLEAR;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
plugins {
|
||||
id 'java'
|
||||
id "org.ysb33r.doxygen" version "0.7.0"
|
||||
id "org.ysb33r.doxygen" version "1.0.4"
|
||||
}
|
||||
|
||||
|
||||
@@ -36,15 +36,16 @@ doxygen {
|
||||
String arch = System.getProperty("os.arch");
|
||||
if (arch.equals("x86_64") || arch.equals("amd64")) {
|
||||
executables {
|
||||
doxygen version : '1.9.4',
|
||||
baseURI : 'https://frcmaven.wpi.edu/artifactory/generic-release-mirror/doxygen'
|
||||
doxygen {
|
||||
executableByVersion('1.12.0')
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
doxygen {
|
||||
generate_html true
|
||||
html_extra_stylesheet 'theme.css'
|
||||
option 'generate_html', true
|
||||
option 'html_extra_stylesheet', 'theme.css'
|
||||
|
||||
cppProjectZips.each {
|
||||
dependsOn it
|
||||
@@ -53,126 +54,37 @@ doxygen {
|
||||
cppIncludeRoots.add(it.absolutePath)
|
||||
}
|
||||
}
|
||||
cppIncludeRoots << '../ntcore/build/generated/main/native/include/'
|
||||
|
||||
if (project.hasProperty('docWarningsAsErrors')) {
|
||||
// Eigen
|
||||
exclude 'Eigen/**'
|
||||
exclude 'unsupported/**'
|
||||
|
||||
// LLVM
|
||||
exclude 'wpi/AlignOf.h'
|
||||
exclude 'wpi/Casting.h'
|
||||
exclude 'wpi/Chrono.h'
|
||||
exclude 'wpi/Compiler.h'
|
||||
exclude 'wpi/ConvertUTF.h'
|
||||
exclude 'wpi/DenseMap.h'
|
||||
exclude 'wpi/DenseMapInfo.h'
|
||||
exclude 'wpi/Endian.h'
|
||||
exclude 'wpi/EpochTracker.h'
|
||||
exclude 'wpi/Errc.h'
|
||||
exclude 'wpi/Errno.h'
|
||||
exclude 'wpi/ErrorHandling.h'
|
||||
exclude 'wpi/bit.h'
|
||||
exclude 'wpi/fs.h'
|
||||
exclude 'wpi/FunctionExtras.h'
|
||||
exclude 'wpi/function_ref.h'
|
||||
exclude 'wpi/Hashing.h'
|
||||
exclude 'wpi/iterator.h'
|
||||
exclude 'wpi/iterator_range.h'
|
||||
exclude 'wpi/ManagedStatic.h'
|
||||
exclude 'wpi/MapVector.h'
|
||||
exclude 'wpi/MathExtras.h'
|
||||
exclude 'wpi/MemAlloc.h'
|
||||
exclude 'wpi/PointerIntPair.h'
|
||||
exclude 'wpi/PointerLikeTypeTraits.h'
|
||||
exclude 'wpi/PointerUnion.h'
|
||||
exclude 'wpi/raw_os_ostream.h'
|
||||
exclude 'wpi/raw_ostream.h'
|
||||
exclude 'wpi/SmallPtrSet.h'
|
||||
exclude 'wpi/SmallSet.h'
|
||||
exclude 'wpi/SmallString.h'
|
||||
exclude 'wpi/SmallVector.h'
|
||||
exclude 'wpi/StringExtras.h'
|
||||
exclude 'wpi/StringMap.h'
|
||||
exclude 'wpi/SwapByteOrder.h'
|
||||
exclude 'wpi/type_traits.h'
|
||||
exclude 'wpi/VersionTuple.h'
|
||||
exclude 'wpi/WindowsError.h'
|
||||
|
||||
// fmtlib
|
||||
exclude 'fmt/**'
|
||||
|
||||
// libuv
|
||||
exclude 'uv.h'
|
||||
exclude 'uv/**'
|
||||
exclude 'wpinet/uv/**'
|
||||
|
||||
// json
|
||||
exclude 'wpi/adl_serializer.h'
|
||||
exclude 'wpi/byte_container_with_subtype.h'
|
||||
exclude 'wpi/detail/**'
|
||||
exclude 'wpi/json.h'
|
||||
exclude 'wpi/json_fwd.h'
|
||||
exclude 'wpi/ordered_map.h'
|
||||
exclude 'wpi/thirdparty/**'
|
||||
|
||||
// memory
|
||||
exclude 'wpi/memory/**'
|
||||
|
||||
// mpack
|
||||
exclude 'wpi/mpack.h'
|
||||
|
||||
// units
|
||||
exclude 'units/**'
|
||||
}
|
||||
|
||||
//TODO: building memory docs causes search to break
|
||||
exclude 'wpi/memory/**'
|
||||
|
||||
exclude '*.pb.h'
|
||||
|
||||
// Save space by excluding protobuf and eigen
|
||||
exclude 'Eigen/**'
|
||||
exclude 'google/protobuf/**'
|
||||
|
||||
aliases 'effects=\\par <i>Effects:</i>^^',
|
||||
'notes=\\par <i>Notes:</i>^^',
|
||||
'requires=\\par <i>Requires:</i>^^',
|
||||
'requiredbe=\\par <i>Required Behavior:</i>^^',
|
||||
'concept{2}=<a href=\"md_doc_concepts.html#\1\">\2</a>',
|
||||
'defaultbe=\\par <i>Default Behavior:</i>^^'
|
||||
case_sense_names false
|
||||
extension_mapping 'inc=C++', 'no_extension=C++'
|
||||
extract_all true
|
||||
extract_static true
|
||||
file_patterns '*'
|
||||
full_path_names true
|
||||
generate_html true
|
||||
generate_latex false
|
||||
generate_treeview true
|
||||
html_extra_stylesheet 'theme.css'
|
||||
html_timestamp true
|
||||
javadoc_autobrief true
|
||||
project_name 'PhotonVision C++'
|
||||
project_logo '../photon-client/src/assets/images/logoSmall.svg'
|
||||
project_number pubVersion
|
||||
quiet true
|
||||
recursive true
|
||||
strip_code_comments false
|
||||
strip_from_inc_path cppIncludeRoots as String[]
|
||||
strip_from_path cppIncludeRoots as String[]
|
||||
use_mathjax true
|
||||
warnings false
|
||||
warn_if_incomplete_doc true
|
||||
warn_if_undocumented false
|
||||
warn_no_paramdoc true
|
||||
option 'case_sense_names', false
|
||||
option 'extension_mapping', 'inc=C++ no_extension=C++'
|
||||
option 'extract_all', true
|
||||
option 'extract_static', true
|
||||
option 'file_patterns', '*'
|
||||
option 'full_path_names', true
|
||||
option 'generate_html', true
|
||||
option 'generate_latex', false
|
||||
option 'generate_treeview', true
|
||||
option 'html_extra_stylesheet', 'theme.css'
|
||||
option 'html_timestamp', true
|
||||
option 'javadoc_autobrief', true
|
||||
option 'project_name', 'PhotonVision C++'
|
||||
option 'project_logo', '../docs/source/assets/RoundLogo.png'
|
||||
option 'project_number', pubVersion
|
||||
option 'quiet', true
|
||||
option 'recursive', true
|
||||
option 'strip_code_comments', false
|
||||
option 'strip_from_inc_path', cppIncludeRoots
|
||||
option 'strip_from_path', cppIncludeRoots
|
||||
option 'use_mathjax', true
|
||||
option 'warnings', false
|
||||
option 'warn_if_incomplete_doc', true
|
||||
option 'warn_if_undocumented', false
|
||||
option 'warn_no_paramdoc', true
|
||||
|
||||
//enable doxygen preprocessor expansion of WPI_DEPRECATED to fix MotorController docs
|
||||
enable_preprocessing true
|
||||
macro_expansion true
|
||||
expand_only_predef true
|
||||
predefined "WPI_DEPRECATED(x)=[[deprecated(x)]]\"\\\n" +
|
||||
option 'enable_preprocessing', true
|
||||
option 'macro_expansion', true
|
||||
option 'expand_only_predef', true
|
||||
option 'predefined', "WPI_DEPRECATED(x)=[[deprecated(x)]]\"\\\n" +
|
||||
"\"__cplusplus\"\\\n" +
|
||||
"\"HAL_ENUM(name)=enum name : int32_t"
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -33,7 +33,7 @@ model {
|
||||
sources {
|
||||
cpp {
|
||||
source {
|
||||
srcDirs 'src/main/native/cpp', "$buildDir/generated/source/proto/main/cpp", "src/generate/native/cpp"
|
||||
srcDirs 'src/main/native/cpp', "$buildDir/generated/source/proto/main/cpp", "$buildDir/generated/native/cpp"
|
||||
include '**/*.cpp', '**/*.cc'
|
||||
}
|
||||
exportedHeaders {
|
||||
@@ -161,14 +161,12 @@ task generateVendorJson() {
|
||||
def read = photonlibFileInput.text
|
||||
.replace('${photon_version}', pubVersion)
|
||||
.replace('${frc_year}', frcYear)
|
||||
.replace('${wpilib_version}', wpilibVersion)
|
||||
photonlibFileOutput.text = read
|
||||
|
||||
outputs.upToDateWhen { false }
|
||||
}
|
||||
|
||||
build.mustRunAfter generateVendorJson
|
||||
publish.mustRunAfter generateVendorJson
|
||||
build.dependsOn generateVendorJson
|
||||
|
||||
task publishVendorJsonToLocalOutputs(type: Copy) {
|
||||
from photonlibFileOutput
|
||||
@@ -182,17 +180,69 @@ task publishVendorJsonToLocalOutputs(type: Copy) {
|
||||
publish.dependsOn it
|
||||
}
|
||||
|
||||
task writeCurrentVersion {
|
||||
def versionFileIn = file("${rootDir}/shared/PhotonVersion.java.in")
|
||||
writePhotonVersionFile(versionFileIn, Path.of("$projectDir", "src", "main", "java", "org", "photonvision", "PhotonVersion.java"),
|
||||
versionString)
|
||||
versionFileIn = file("${rootDir}/shared/PhotonVersion.cpp.in")
|
||||
writePhotonVersionFile(versionFileIn, Path.of("$projectDir", "src", "generate", "native", "cpp", "PhotonVersion.cpp"),
|
||||
versionString)
|
||||
task copyVendorJsonToExamples {
|
||||
outputs.upToDateWhen { false }
|
||||
jar.finalizedBy it
|
||||
}
|
||||
|
||||
build.mustRunAfter writeCurrentVersion
|
||||
cppHeadersZip.dependsOn writeCurrentVersion
|
||||
[
|
||||
"photonlib-cpp-examples",
|
||||
"photonlib-java-examples"
|
||||
].each { exampleFolder ->
|
||||
file("${rootDir}/${exampleFolder}")
|
||||
.listFiles()
|
||||
.findAll {
|
||||
return (it.isDirectory()
|
||||
&& !it.isHidden()
|
||||
&& !it.name.startsWith(".")
|
||||
&& it.toPath().resolve("build.gradle").toFile().exists())
|
||||
}
|
||||
.collect { it.name }
|
||||
.each { exampleVendordepFolder ->
|
||||
task "copyVendorJsonTo${exampleFolder}-${exampleVendordepFolder}"(type: Copy) {
|
||||
from photonlibFileOutput
|
||||
|
||||
into "${rootDir}/${exampleFolder}/${exampleVendordepFolder}/vendordeps/"
|
||||
outputs.upToDateWhen { false }
|
||||
copyVendorJsonToExamples.dependsOn it
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
clean {
|
||||
[
|
||||
"photonlib-cpp-examples",
|
||||
"photonlib-java-examples"
|
||||
].each { exampleFolder ->
|
||||
file("${rootDir}/${exampleFolder}")
|
||||
.listFiles()
|
||||
.findAll {
|
||||
return (it.isDirectory()
|
||||
&& !it.isHidden()
|
||||
&& !it.name.startsWith(".")
|
||||
&& it.toPath().resolve("build.gradle").toFile().exists())
|
||||
}
|
||||
.collect { it.name }
|
||||
.each { exampleVendordepFolder ->
|
||||
delete "${rootDir}/${exampleFolder}/${exampleVendordepFolder}/vendordeps/"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
task writeCurrentVersion {
|
||||
doLast {
|
||||
def versionFileIn = file("${rootDir}/shared/PhotonVersion.java.in")
|
||||
writePhotonVersionFile(versionFileIn, Path.of("$buildDir", "generated", "java", "org", "photonvision", "PhotonVersion.java"),
|
||||
versionString)
|
||||
versionFileIn = file("${rootDir}/shared/PhotonVersion.cpp.in")
|
||||
writePhotonVersionFile(versionFileIn, Path.of("$buildDir", "generated", "native", "cpp", "PhotonVersion.cpp"),
|
||||
versionString)
|
||||
}
|
||||
}
|
||||
|
||||
// https://github.com/wpilibsuite/allwpilib/blob/main/wpilibj/build.gradle#L52
|
||||
sourceSets.main.java.srcDir "${buildDir}/generated/java/"
|
||||
compileJava.dependsOn writeCurrentVersion
|
||||
|
||||
// Building photon-lib requires photon-targeting to generate its proto files. This technically shouldn't be required but is needed for it to build.
|
||||
model {
|
||||
@@ -206,6 +256,7 @@ model {
|
||||
}
|
||||
it.binaries.all {
|
||||
it.tasks.withType(CppCompile) {
|
||||
it.dependsOn writeCurrentVersion
|
||||
it.dependsOn ":photon-targeting:generateProto"
|
||||
}
|
||||
}
|
||||
@@ -243,7 +294,7 @@ if (!project.hasProperty('copyOfflineArtifacts')) {
|
||||
tasks.named('cppSourcesZip') {
|
||||
dependsOn writeCurrentVersion
|
||||
|
||||
from("$projectDir/src/generate/native/cpp") {
|
||||
from("$buildDir/generated/native/cpp") {
|
||||
into '/'
|
||||
}
|
||||
}
|
||||
@@ -252,7 +303,6 @@ tasks.named('cppSourcesZip') {
|
||||
def zipBaseNameCombined = '_GROUP_org.photonvision_combinedcpp_ID_photonvision-combinedcpp_CLS'
|
||||
task combinedCppSourcesZip(type: Zip) {
|
||||
dependsOn(':photon-lib:cppSourcesZip', ':photon-targeting:cppSourcesZip')
|
||||
mustRunAfter(':photon-lib:cppHeadersZip', ':photon-targeting:cppHeadersZip')
|
||||
|
||||
destinationDirectory = file("$buildDir/outputs")
|
||||
archiveBaseName = zipBaseNameCombined
|
||||
@@ -270,7 +320,6 @@ task combinedCppSourcesZip(type: Zip) {
|
||||
}
|
||||
task combinedHeadersZip(type: Zip) {
|
||||
dependsOn(':photon-lib:cppHeadersZip', ':photon-targeting:cppHeadersZip')
|
||||
mustRunAfter(':photon-lib:cppHeadersZip', ':photon-targeting:cppHeadersZip')
|
||||
|
||||
destinationDirectory = file("$buildDir/outputs")
|
||||
archiveBaseName = zipBaseNameCombined
|
||||
@@ -316,7 +365,6 @@ def nativeTasks = wpilibTools.createExtractionTasks {
|
||||
|
||||
nativeTasks.addToSourceSetResources(sourceSets.test)
|
||||
|
||||
nativeConfig.dependencies.add wpilibTools.deps.wpilib("wpilibc")
|
||||
nativeConfig.dependencies.add wpilibTools.deps.wpilib("wpimath")
|
||||
nativeConfig.dependencies.add wpilibTools.deps.wpilib("wpinet")
|
||||
nativeConfig.dependencies.add wpilibTools.deps.wpilib("wpiutil")
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import logging
|
||||
import math
|
||||
from typing import Any
|
||||
|
||||
@@ -11,6 +12,8 @@ from .rotTrlTransform3d import RotTrlTransform3d
|
||||
NWU_TO_EDN = Rotation3d(np.array([[0, -1, 0], [0, 0, -1], [1, 0, 0]]))
|
||||
EDN_TO_NWU = Rotation3d(np.array([[0, 0, 1], [-1, 0, 0], [0, -1, 0]]))
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class OpenCVHelp:
|
||||
@staticmethod
|
||||
@@ -27,6 +30,12 @@ class OpenCVHelp:
|
||||
|
||||
@staticmethod
|
||||
def translationToTVec(translations: list[Translation3d]) -> np.ndarray:
|
||||
"""Creates a new :class:`np.array` with these 3d translations. The opencv tvec is a vector with
|
||||
three elements representing {x, y, z} in the EDN coordinate system.
|
||||
|
||||
:param translations: The translations to convert into a np.array
|
||||
"""
|
||||
|
||||
retVal: list[list] = []
|
||||
for translation in translations:
|
||||
trl = OpenCVHelp.translationNWUtoEDN(translation)
|
||||
@@ -38,6 +47,13 @@ class OpenCVHelp:
|
||||
|
||||
@staticmethod
|
||||
def rotationToRVec(rotation: Rotation3d) -> np.ndarray:
|
||||
"""Creates a new :class:`.np.array` with this 3d rotation. The opencv rvec Mat is a vector with
|
||||
three elements representing the axis scaled by the angle in the EDN coordinate system. (angle =
|
||||
norm, and axis = rvec / norm)
|
||||
|
||||
:param rotation: The rotation to convert into a np.array
|
||||
"""
|
||||
|
||||
retVal: list[np.ndarray] = []
|
||||
rot = OpenCVHelp.rotationNWUtoEDN(rotation)
|
||||
rotVec = rot.getQuaternion().toRotationVector()
|
||||
@@ -88,6 +104,25 @@ class OpenCVHelp:
|
||||
def reorderCircular(
|
||||
elements: list[Any] | np.ndarray, backwards: bool, shiftStart: int
|
||||
) -> list[Any]:
|
||||
"""Reorders the list, optionally indexing backwards and wrapping around to the last element after
|
||||
the first, and shifting all indices in the direction of indexing.
|
||||
|
||||
e.g.
|
||||
|
||||
({1,2,3}, false, 1) == {2,3,1}
|
||||
|
||||
({1,2,3}, true, 0) == {1,3,2}
|
||||
|
||||
({1,2,3}, true, 1) == {3,2,1}
|
||||
|
||||
:param elements: list elements
|
||||
:param backwards: If indexing should happen in reverse (0, size-1, size-2, ...)
|
||||
:param shiftStart: How much the initial index should be shifted (instead of starting at index 0,
|
||||
start at shiftStart, negated if backwards)
|
||||
|
||||
:returns: Reordered list
|
||||
"""
|
||||
|
||||
size = len(elements)
|
||||
reordered = []
|
||||
dir = -1 if backwards else 1
|
||||
@@ -100,18 +135,39 @@ class OpenCVHelp:
|
||||
|
||||
@staticmethod
|
||||
def translationEDNToNWU(trl: Translation3d) -> Translation3d:
|
||||
"""Convert a rotation delta from EDN to NWU. For example, if you have a rotation X,Y,Z {1, 0, 0}
|
||||
in EDN, this would be {0, -1, 0} in NWU.
|
||||
"""
|
||||
|
||||
return trl.rotateBy(EDN_TO_NWU)
|
||||
|
||||
@staticmethod
|
||||
def rotationEDNToNWU(rot: Rotation3d) -> Rotation3d:
|
||||
"""Convert a rotation delta from NWU to EDN. For example, if you have a rotation X,Y,Z {1, 0, 0}
|
||||
in NWU, this would be {0, 0, 1} in EDN.
|
||||
"""
|
||||
|
||||
return -EDN_TO_NWU + (rot + EDN_TO_NWU)
|
||||
|
||||
@staticmethod
|
||||
def tVecToTranslation(tvecInput: np.ndarray) -> Translation3d:
|
||||
"""Returns a new 3d translation from this :class:`.Mat`. The opencv tvec is a vector with three
|
||||
elements representing {x, y, z} in the EDN coordinate system.
|
||||
|
||||
:param tvecInput: The tvec to create a Translation3d from
|
||||
"""
|
||||
|
||||
return OpenCVHelp.translationEDNToNWU(Translation3d(tvecInput))
|
||||
|
||||
@staticmethod
|
||||
def rVecToRotation(rvecInput: np.ndarray) -> Rotation3d:
|
||||
"""Returns a 3d rotation from this :class:`.Mat`. The opencv rvec Mat is a vector with three
|
||||
elements representing the axis scaled by the angle in the EDN coordinate system. (angle = norm,
|
||||
and axis = rvec / norm)
|
||||
|
||||
:param rvecInput: The rvec to create a Rotation3d from
|
||||
"""
|
||||
|
||||
return OpenCVHelp.rotationEDNToNWU(Rotation3d(rvecInput))
|
||||
|
||||
@staticmethod
|
||||
@@ -121,6 +177,33 @@ class OpenCVHelp:
|
||||
modelTrls: list[Translation3d],
|
||||
imagePoints: np.ndarray,
|
||||
) -> PnpResult | None:
|
||||
"""Finds the transformation(s) that map the camera's pose to the target's pose. The camera's pose
|
||||
relative to the target is determined by the supplied 3d points of the target's model and their
|
||||
associated 2d points imaged by the camera. The supplied model translations must be relative to
|
||||
the target's pose.
|
||||
|
||||
For planar targets, there may be an alternate solution which is plausible given the 2d image
|
||||
points. This has an associated "ambiguity" which describes the ratio of reprojection error
|
||||
between the "best" and "alternate" solution.
|
||||
|
||||
This method is intended for use with individual AprilTags, and will not work unless 4 points
|
||||
are provided.
|
||||
|
||||
:param cameraMatrix: The camera intrinsics matrix in standard opencv form
|
||||
:param distCoeffs: The camera distortion matrix in standard opencv form
|
||||
:param modelTrls: The translations of the object corners. These should have the object pose as
|
||||
their origin. These must come in a specific, pose-relative order (in NWU):
|
||||
|
||||
- Point 0: [0, -squareLength / 2, squareLength / 2]
|
||||
- Point 1: [0, squareLength / 2, squareLength / 2]
|
||||
- Point 2: [0, squareLength / 2, -squareLength / 2]
|
||||
- Point 3: [0, -squareLength / 2, -squareLength / 2]
|
||||
:param imagePoints: The projection of these 3d object points into the 2d camera image. The order
|
||||
should match the given object point translations.
|
||||
|
||||
:returns: The resulting transformation that maps the camera pose to the target pose and the
|
||||
ambiguity if an alternate solution is available.
|
||||
"""
|
||||
modelTrls = OpenCVHelp.reorderCircular(modelTrls, True, -1)
|
||||
imagePoints = np.array(OpenCVHelp.reorderCircular(imagePoints, True, -1))
|
||||
objectMat = np.array(OpenCVHelp.translationToTVec(modelTrls))
|
||||
@@ -130,6 +213,7 @@ class OpenCVHelp:
|
||||
best: Transform3d = Transform3d()
|
||||
|
||||
for tries in range(2):
|
||||
# calc rvecs/tvecs and associated reprojection error from image points
|
||||
retval, rvecs, tvecs, reprojectionError = cv.solvePnPGeneric(
|
||||
objectMat,
|
||||
imagePoints,
|
||||
@@ -138,6 +222,7 @@ class OpenCVHelp:
|
||||
flags=cv.SOLVEPNP_IPPE_SQUARE,
|
||||
)
|
||||
|
||||
# convert to wpilib coordinates
|
||||
best = Transform3d(
|
||||
OpenCVHelp.tVecToTranslation(tvecs[0]),
|
||||
OpenCVHelp.rVecToRotation(rvecs[0]),
|
||||
@@ -148,6 +233,7 @@ class OpenCVHelp:
|
||||
OpenCVHelp.rVecToRotation(rvecs[1]),
|
||||
)
|
||||
|
||||
# check if we got a NaN result
|
||||
if reprojectionError is not None and not math.isnan(
|
||||
reprojectionError[0, 0]
|
||||
):
|
||||
@@ -158,8 +244,9 @@ class OpenCVHelp:
|
||||
pt[0, 1] -= 0.001
|
||||
imagePoints[0] = pt
|
||||
|
||||
# solvePnP failed
|
||||
if reprojectionError is None or math.isnan(reprojectionError[0, 0]):
|
||||
print("SolvePNP_Square failed!")
|
||||
logger.error("SolvePNP_Square failed!")
|
||||
return None
|
||||
|
||||
if alt:
|
||||
@@ -186,6 +273,27 @@ class OpenCVHelp:
|
||||
modelTrls: list[Translation3d],
|
||||
imagePoints: np.ndarray,
|
||||
) -> PnpResult | None:
|
||||
"""Finds the transformation that maps the camera's pose to the origin of the supplied object. An
|
||||
"object" is simply a set of known 3d translations that correspond to the given 2d points. If,
|
||||
for example, the object translations are given relative to close-right corner of the blue
|
||||
alliance(the default origin), a camera-to-origin transformation is returned. If the
|
||||
translations are relative to a target's pose, a camera-to-target transformation is returned.
|
||||
|
||||
There must be at least 3 points to use this method. This does not return an alternate
|
||||
solution-- if you are intending to use solvePNP on a single AprilTag, see {@link
|
||||
#solvePNP_SQUARE} instead.
|
||||
|
||||
:param cameraMatrix: The camera intrinsics matrix in standard opencv form
|
||||
:param distCoeffs: The camera distortion matrix in standard opencv form
|
||||
:param objectTrls: The translations of the object corners, relative to the field.
|
||||
:param imagePoints: The projection of these 3d object points into the 2d camera image. The order
|
||||
should match the given object point translations.
|
||||
|
||||
:returns: The resulting transformation that maps the camera pose to the target pose. If the 3d
|
||||
model points are supplied relative to the origin, this transformation brings the camera to
|
||||
the origin.
|
||||
"""
|
||||
|
||||
objectMat = np.array(OpenCVHelp.translationToTVec(modelTrls))
|
||||
|
||||
retval, rvecs, tvecs, reprojectionError = cv.solvePnPGeneric(
|
||||
@@ -198,6 +306,7 @@ class OpenCVHelp:
|
||||
)
|
||||
|
||||
if math.isnan(error):
|
||||
logger.error("SolvePNP_SQPNP failed!")
|
||||
return None
|
||||
|
||||
# We have no alternative so set it to best as well
|
||||
|
||||
@@ -4,24 +4,38 @@ from wpimath.geometry import Pose3d, Rotation3d, Transform3d, Translation3d
|
||||
|
||||
|
||||
class RotTrlTransform3d:
|
||||
"""Represents a transformation that first rotates a pose around the origin, and then translates it."""
|
||||
|
||||
def __init__(
|
||||
self, rot: Rotation3d = Rotation3d(), trl: Translation3d = Translation3d()
|
||||
):
|
||||
"""A rotation-translation transformation.
|
||||
|
||||
Applying this RotTrlTransform3d to poses will preserve their current origin-to-pose
|
||||
transform as if the origin was transformed by these components instead.
|
||||
|
||||
:param rot: The rotation component
|
||||
:param trl: The translation component
|
||||
"""
|
||||
self.rot = rot
|
||||
self.trl = trl
|
||||
|
||||
def inverse(self) -> Self:
|
||||
"""The inverse of this transformation. Applying the inverse will "undo" this transformation."""
|
||||
invRot = -self.rot
|
||||
invTrl = -(self.trl.rotateBy(invRot))
|
||||
return type(self)(invRot, invTrl)
|
||||
|
||||
def getTransform(self) -> Transform3d:
|
||||
"""This transformation as a Transform3d (as if of the origin)"""
|
||||
return Transform3d(self.trl, self.rot)
|
||||
|
||||
def getTranslation(self) -> Translation3d:
|
||||
"""The translation component of this transformation"""
|
||||
return self.trl
|
||||
|
||||
def getRotation(self) -> Rotation3d:
|
||||
"""The rotation component of this transformation"""
|
||||
return self.rot
|
||||
|
||||
def applyTranslation(self, trlToApply: Translation3d) -> Translation3d:
|
||||
@@ -44,6 +58,11 @@ class RotTrlTransform3d:
|
||||
|
||||
@classmethod
|
||||
def makeRelativeTo(cls, pose: Pose3d) -> Self:
|
||||
"""The rotation-translation transformation that makes poses in the world consider this pose as the
|
||||
new origin, or change the basis to this pose.
|
||||
|
||||
:param pose: The new origin
|
||||
"""
|
||||
return cls(pose.rotation(), pose.translation()).inverse()
|
||||
|
||||
@classmethod
|
||||
|
||||
@@ -8,14 +8,27 @@ from . import RotTrlTransform3d
|
||||
|
||||
|
||||
class TargetModel:
|
||||
"""Describes the 3d model of a target."""
|
||||
|
||||
def __init__(self):
|
||||
"""Default constructor for initialising internal class members. DO NOT USE THIS!!! USE THE createPlanar,
|
||||
createCuboid, createSpheroid or create Arbitrary
|
||||
"""
|
||||
self.vertices: List[Translation3d] = []
|
||||
self.isPlanar = False
|
||||
self.isSpherical = False
|
||||
|
||||
@classmethod
|
||||
def createPlanar(cls, width: meters, height: meters) -> Self:
|
||||
"""Creates a rectangular, planar target model given the width and height. The model has four
|
||||
vertices:
|
||||
|
||||
- Point 0: [0, -width/2, -height/2]
|
||||
- Point 1: [0, width/2, -height/2]
|
||||
- Point 2: [0, width/2, height/2]
|
||||
- Point 3: [0, -width/2, height/2]
|
||||
"""
|
||||
|
||||
tm = cls()
|
||||
|
||||
tm.isPlanar = True
|
||||
@@ -30,6 +43,18 @@ class TargetModel:
|
||||
|
||||
@classmethod
|
||||
def createCuboid(cls, length: meters, width: meters, height: meters) -> Self:
|
||||
"""Creates a cuboid target model given the length, width, height. The model has eight vertices:
|
||||
|
||||
- Point 0: [length/2, -width/2, -height/2]
|
||||
- Point 1: [length/2, width/2, -height/2]
|
||||
- Point 2: [length/2, width/2, height/2]
|
||||
- Point 3: [length/2, -width/2, height/2]
|
||||
- Point 4: [-length/2, -width/2, height/2]
|
||||
- Point 5: [-length/2, width/2, height/2]
|
||||
- Point 6: [-length/2, width/2, -height/2]
|
||||
- Point 7: [-length/2, -width/2, -height/2]
|
||||
"""
|
||||
|
||||
tm = cls()
|
||||
verts = [
|
||||
Translation3d(length / 2.0, -width / 2.0, -height / 2.0),
|
||||
@@ -48,6 +73,20 @@ class TargetModel:
|
||||
|
||||
@classmethod
|
||||
def createSpheroid(cls, diameter: meters) -> Self:
|
||||
"""Creates a spherical target model which has similar dimensions regardless of its rotation. This
|
||||
model has four vertices:
|
||||
|
||||
- Point 0: [0, -radius, 0]
|
||||
- Point 1: [0, 0, -radius]
|
||||
- Point 2: [0, radius, 0]
|
||||
- Point 3: [0, 0, radius]
|
||||
|
||||
*Q: Why these vertices?* A: This target should be oriented to the camera every frame, much
|
||||
like a sprite/decal, and these vertices represent the ellipse vertices (maxima). These vertices
|
||||
are used for drawing the image of this sphere, but do not match the corners that will be
|
||||
published by photonvision.
|
||||
"""
|
||||
|
||||
tm = cls()
|
||||
|
||||
tm.isPlanar = False
|
||||
@@ -63,6 +102,14 @@ class TargetModel:
|
||||
|
||||
@classmethod
|
||||
def createArbitrary(cls, verts: List[Translation3d]) -> Self:
|
||||
"""Creates a target model from arbitrary 3d vertices. Automatically determines if the given
|
||||
vertices are planar(x == 0). More than 2 vertices must be given. If this is a planar model, the
|
||||
vertices should define a non-intersecting contour.
|
||||
|
||||
:param vertices: Translations representing the vertices of this target model relative to its
|
||||
pose.
|
||||
"""
|
||||
|
||||
tm = cls()
|
||||
tm._common_construction(verts)
|
||||
|
||||
@@ -83,6 +130,12 @@ class TargetModel:
|
||||
self.vertices = verts
|
||||
|
||||
def getFieldVertices(self, targetPose: Pose3d) -> List[Translation3d]:
|
||||
"""This target's vertices offset from its field pose.
|
||||
|
||||
Note: If this target is spherical, use {@link #getOrientedPose(Translation3d,
|
||||
Translation3d)} with this method.
|
||||
"""
|
||||
|
||||
basisChange = RotTrlTransform3d(targetPose.rotation(), targetPose.translation())
|
||||
|
||||
retVal = []
|
||||
@@ -94,6 +147,16 @@ class TargetModel:
|
||||
|
||||
@classmethod
|
||||
def getOrientedPose(cls, tgtTrl: Translation3d, cameraTrl: Translation3d):
|
||||
"""Returns a Pose3d with the given target translation oriented (with its relative x-axis aligned)
|
||||
to the camera translation. This is used for spherical targets which should not have their
|
||||
projection change regardless of their own rotation.
|
||||
|
||||
:param tgtTrl: This target's translation
|
||||
:param cameraTrl: Camera's translation
|
||||
|
||||
:returns: This target's pose oriented to the camera
|
||||
"""
|
||||
|
||||
relCam = cameraTrl - tgtTrl
|
||||
orientToCam = Rotation3d(
|
||||
0.0,
|
||||
|
||||
@@ -11,6 +11,7 @@ class VisionEstimation:
|
||||
def getVisibleLayoutTags(
|
||||
visTags: list[PhotonTrackedTarget], layout: AprilTagFieldLayout
|
||||
) -> list[AprilTag]:
|
||||
"""Get the visible :class:`.AprilTag`s which are in the tag layout using the visible tag IDs."""
|
||||
retVal: list[AprilTag] = []
|
||||
for tag in visTags:
|
||||
id = tag.getFiducialId()
|
||||
@@ -30,12 +31,31 @@ class VisionEstimation:
|
||||
layout: AprilTagFieldLayout,
|
||||
tagModel: TargetModel,
|
||||
) -> PnpResult | None:
|
||||
"""Performs solvePNP using 3d-2d point correspondences of visible AprilTags to estimate the
|
||||
field-to-camera transformation. If only one tag is visible, the result may have an alternate
|
||||
solution.
|
||||
|
||||
**Note:** The returned transformation is from the field origin to the camera pose!
|
||||
|
||||
With only one tag: {@link OpenCVHelp#solvePNP_SQUARE}
|
||||
|
||||
With multiple tags: {@link OpenCVHelp#solvePNP_SQPNP}
|
||||
|
||||
:param cameraMatrix: The camera intrinsics matrix in standard opencv form
|
||||
:param distCoeffs: The camera distortion matrix in standard opencv form
|
||||
:param visTags: The visible tags reported by PV. Non-tag targets are automatically excluded.
|
||||
:param tagLayout: The known tag layout on the field
|
||||
|
||||
:returns: The transformation that maps the field origin to the camera pose. Ensure the {@link
|
||||
PnpResult} are present before utilizing them.
|
||||
"""
|
||||
if len(visTags) == 0:
|
||||
return None
|
||||
|
||||
corners: list[TargetCorner] = []
|
||||
knownTags: list[AprilTag] = []
|
||||
|
||||
# ensure these are AprilTags in our layout
|
||||
for tgt in visTags:
|
||||
id = tgt.getFiducialId()
|
||||
maybePose = layout.getTagPose(id)
|
||||
@@ -53,6 +73,7 @@ class VisionEstimation:
|
||||
|
||||
points = OpenCVHelp.cornersToPoints(corners)
|
||||
|
||||
# single-tag pnp
|
||||
if len(knownTags) == 1:
|
||||
camToTag = OpenCVHelp.solvePNP_Square(
|
||||
cameraMatrix, distCoeffs, tagModel.getVertices(), points
|
||||
@@ -74,6 +95,7 @@ class VisionEstimation:
|
||||
altReprojErr=camToTag.altReprojErr,
|
||||
)
|
||||
return result
|
||||
# multi-tag pnp
|
||||
else:
|
||||
objectTrls: list[Translation3d] = []
|
||||
for tag in knownTags:
|
||||
|
||||
@@ -42,9 +42,7 @@ class PhotonPipelineResultSerde:
|
||||
ret = Packet()
|
||||
|
||||
# metadata is of non-intrinsic type PhotonPipelineMetadata
|
||||
ret.encodeBytes(
|
||||
PhotonPipelineMetadata.photonStruct.pack(value.metadata).getData()
|
||||
)
|
||||
ret.encodeBytes(PhotonPipelineMetadata.photonStruct.pack(value.metadata).getData())
|
||||
|
||||
# targets is a custom VLA!
|
||||
ret.encodeList(value.targets, PhotonTrackedTarget.photonStruct)
|
||||
|
||||
@@ -9,11 +9,19 @@ PhotonPipelineResult_TYPE_STRING = (
|
||||
|
||||
|
||||
class NTTopicSet:
|
||||
"""This class is a wrapper around all per-pipeline NT topics that PhotonVision should be publishing
|
||||
It's split here so the sim and real-camera implementations can share a common implementation of
|
||||
the naming and registration of the NT content.
|
||||
|
||||
def __init__(self, tableName: str, cameraName: str) -> None:
|
||||
instance = nt.NetworkTableInstance.getDefault()
|
||||
photonvision_root_table = instance.getTable(tableName)
|
||||
self.subTable = photonvision_root_table.getSubTable(cameraName)
|
||||
However, we do expect that the actual logic which fills out values in the entries will be
|
||||
different for sim vs. real camera
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
ntSubTable: nt.NetworkTable,
|
||||
) -> None:
|
||||
self.subTable = ntSubTable
|
||||
|
||||
def updateEntries(self) -> None:
|
||||
options = nt.PubSubOptions()
|
||||
|
||||
@@ -48,6 +48,10 @@ def setVersionCheckEnabled(enabled: bool):
|
||||
|
||||
class PhotonCamera:
|
||||
def __init__(self, cameraName: str):
|
||||
"""Constructs a PhotonCamera from the name of the camera.
|
||||
|
||||
:param cameraName: The nickname of the camera (found in the PhotonVision UI).
|
||||
"""
|
||||
instance = ntcore.NetworkTableInstance.getDefault()
|
||||
self._name = cameraName
|
||||
self._tableName = "photonvision"
|
||||
@@ -132,6 +136,14 @@ class PhotonCamera:
|
||||
return ret
|
||||
|
||||
def getLatestResult(self) -> PhotonPipelineResult:
|
||||
"""Returns the latest pipeline result. This is simply the most recent result Received via NT.
|
||||
Calling this multiple times will always return the most recent result.
|
||||
|
||||
Replaced by :meth:`.getAllUnreadResults` over getLatestResult, as this function can miss
|
||||
results, or provide duplicate ones!
|
||||
TODO implement the thing that will take this ones place...
|
||||
"""
|
||||
|
||||
self._versionCheck()
|
||||
|
||||
now = RobotController.getFPGATime()
|
||||
@@ -149,34 +161,85 @@ class PhotonCamera:
|
||||
return retVal
|
||||
|
||||
def getDriverMode(self) -> bool:
|
||||
"""Returns whether the camera is in driver mode.
|
||||
|
||||
:returns: Whether the camera is in driver mode.
|
||||
"""
|
||||
|
||||
return self._driverModeSubscriber.get()
|
||||
|
||||
def setDriverMode(self, driverMode: bool) -> None:
|
||||
"""Toggles driver mode.
|
||||
|
||||
:param driverMode: Whether to set driver mode.
|
||||
"""
|
||||
|
||||
self._driverModePublisher.set(driverMode)
|
||||
|
||||
def takeInputSnapshot(self) -> None:
|
||||
"""Request the camera to save a new image file from the input camera stream with overlays. Images
|
||||
take up space in the filesystem of the PhotonCamera. Calling it frequently will fill up disk
|
||||
space and eventually cause the system to stop working. Clear out images in
|
||||
/opt/photonvision/photonvision_config/imgSaves frequently to prevent issues.
|
||||
"""
|
||||
|
||||
self._inputSaveImgEntry.set(self._inputSaveImgEntry.get() + 1)
|
||||
|
||||
def takeOutputSnapshot(self) -> None:
|
||||
"""Request the camera to save a new image file from the output stream with overlays. Images take
|
||||
up space in the filesystem of the PhotonCamera. Calling it frequently will fill up disk space
|
||||
and eventually cause the system to stop working. Clear out images in
|
||||
/opt/photonvision/photonvision_config/imgSaves frequently to prevent issues.
|
||||
"""
|
||||
self._outputSaveImgEntry.set(self._outputSaveImgEntry.get() + 1)
|
||||
|
||||
def getPipelineIndex(self) -> int:
|
||||
"""Returns the active pipeline index.
|
||||
|
||||
:returns: The active pipeline index.
|
||||
"""
|
||||
|
||||
return self._pipelineIndexState.get(0)
|
||||
|
||||
def setPipelineIndex(self, index: int) -> None:
|
||||
"""Allows the user to select the active pipeline index.
|
||||
|
||||
:param index: The active pipeline index.
|
||||
"""
|
||||
self._pipelineIndexRequest.set(index)
|
||||
|
||||
def getLEDMode(self) -> VisionLEDMode:
|
||||
"""Returns the current LED mode.
|
||||
|
||||
:returns: The current LED mode.
|
||||
"""
|
||||
|
||||
mode = self._ledModeState.get()
|
||||
return VisionLEDMode(mode)
|
||||
|
||||
def setLEDMode(self, led: VisionLEDMode) -> None:
|
||||
"""Sets the LED mode.
|
||||
|
||||
:param led: The mode to set to.
|
||||
"""
|
||||
|
||||
self._ledModeRequest.set(led.value)
|
||||
|
||||
def getName(self) -> str:
|
||||
"""Returns the name of the camera. This will return the same value that was given to the
|
||||
constructor as cameraName.
|
||||
|
||||
:returns: The name of the camera.
|
||||
"""
|
||||
return self._name
|
||||
|
||||
def isConnected(self) -> bool:
|
||||
"""Returns whether the camera is connected and actively returning new data. Connection status is
|
||||
debounced.
|
||||
|
||||
:returns: True if the camera is actively sending frame data, false otherwise.
|
||||
"""
|
||||
|
||||
curHeartbeat = self._heartbeatEntry.get()
|
||||
now = Timer.getFPGATimestamp()
|
||||
|
||||
@@ -197,6 +260,8 @@ class PhotonCamera:
|
||||
|
||||
_lastVersionTimeCheck = Timer.getFPGATimestamp()
|
||||
|
||||
# Heartbeat entry is assumed to always be present. If it's not present, we
|
||||
# assume that a camera with that name was never connected in the first place.
|
||||
if not self._heartbeatEntry.exists():
|
||||
cameraNames = (
|
||||
self._cameraTable.getInstance().getTable(self._tableName).getSubTables()
|
||||
@@ -222,6 +287,7 @@ class PhotonCamera:
|
||||
True,
|
||||
)
|
||||
|
||||
# Check for connection status. Warn if disconnected.
|
||||
elif not self.isConnected():
|
||||
wpilib.reportWarning(
|
||||
f"PhotonVision coprocessor at path {self._path} is not sending new data.",
|
||||
@@ -229,45 +295,45 @@ class PhotonCamera:
|
||||
)
|
||||
|
||||
versionString = self.versionEntry.get(defaultValue="")
|
||||
|
||||
# Check mdef UUID
|
||||
localUUID = PhotonPipelineResult.photonStruct.MESSAGE_VERSION
|
||||
remoteUUID = self._rawBytesEntry.getTopic().getProperty("message_uuid")
|
||||
|
||||
remoteUUID = str(self._rawBytesEntry.getTopic().getProperty("message_uuid"))
|
||||
|
||||
if not remoteUUID:
|
||||
if remoteUUID is None:
|
||||
wpilib.reportWarning(
|
||||
f"PhotonVision coprocessor at path {self._path} has not reported a message interface UUID - is your coprocessor's camera started?",
|
||||
True,
|
||||
)
|
||||
else:
|
||||
# ntcore hands us a JSON string with leading/trailing quotes - remove those
|
||||
remoteUUID = str(remoteUUID).replace('"', "")
|
||||
|
||||
assert isinstance(remoteUUID, str)
|
||||
# ntcore hands us a JSON string with leading/trailing quotes - remove those
|
||||
remoteUUID = remoteUUID.replace('"', "")
|
||||
if localUUID != remoteUUID:
|
||||
# Verified version mismatch
|
||||
|
||||
if localUUID != remoteUUID:
|
||||
# Verified version mismatch
|
||||
bfw = """
|
||||
\n\n\n
|
||||
>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
|
||||
>>> !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||
>>>
|
||||
>>> You are running an incompatible version
|
||||
>>> of PhotonVision on your coprocessor!
|
||||
>>>
|
||||
>>> This is neither tested nor supported.
|
||||
>>> You MUST update PhotonVision,
|
||||
>>> PhotonLib, or both.
|
||||
>>>
|
||||
>>> Your code will now crash.
|
||||
>>> We hope your day gets better.
|
||||
>>>
|
||||
>>> !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||
>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
|
||||
\n\n
|
||||
"""
|
||||
|
||||
bfw = """
|
||||
\n\n\n
|
||||
>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
|
||||
>>> !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||
>>>
|
||||
>>> You are running an incompatible version
|
||||
>>> of PhotonVision on your coprocessor!
|
||||
>>>
|
||||
>>> This is neither tested nor supported.
|
||||
>>> You MUST update PhotonVision,
|
||||
>>> PhotonLib, or both.
|
||||
>>>
|
||||
>>> Your code will now crash.
|
||||
>>> We hope your day gets better.
|
||||
>>>
|
||||
>>> !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||
>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
|
||||
\n\n
|
||||
"""
|
||||
wpilib.reportWarning(bfw)
|
||||
|
||||
wpilib.reportWarning(bfw)
|
||||
|
||||
errText = f"Photonlibpy version {PHOTONLIB_VERSION} (With message UUID {localUUID}) does not match coprocessor version {versionString} (with message UUID {remoteUUID}). Please install photonlibpy version {versionString}, or update your coprocessor to {PHOTONLIB_VERSION}."
|
||||
wpilib.reportError(errText, True)
|
||||
raise Exception(errText)
|
||||
errText = f"Photonlibpy version {PHOTONLIB_VERSION} (With message UUID {localUUID}) does not match coprocessor version {versionString} (with message UUID {remoteUUID}). Please install photonlibpy version {versionString}, or update your coprocessor to {PHOTONLIB_VERSION}."
|
||||
wpilib.reportError(errText, True)
|
||||
raise Exception(errText)
|
||||
|
||||
1
photon-lib/py/photonlibpy/py.typed
Normal file
1
photon-lib/py/photonlibpy/py.typed
Normal file
@@ -0,0 +1 @@
|
||||
|
||||
@@ -26,6 +26,10 @@ from .visionTargetSim import VisionTargetSim
|
||||
|
||||
|
||||
class PhotonCameraSim:
|
||||
"""A handle for simulating :class:`.PhotonCamera` values. Processing simulated targets through this
|
||||
class will change the associated PhotonCamera's results.
|
||||
"""
|
||||
|
||||
kDefaultMinAreaPx: float = 100
|
||||
|
||||
def __init__(
|
||||
@@ -35,17 +39,31 @@ class PhotonCameraSim:
|
||||
minTargetAreaPercent: float | None = None,
|
||||
maxSightRange: meters | None = None,
|
||||
):
|
||||
"""Constructs a handle for simulating :class:`.PhotonCamera` values. Processing simulated targets
|
||||
through this class will change the associated PhotonCamera's results.
|
||||
|
||||
By default, this constructor's camera has a 90 deg FOV with no simulated lag if props!
|
||||
By default, the minimum target area is 100 pixels and there is no maximum sight range unless both params are passed to override.
|
||||
|
||||
|
||||
:param camera: The camera to be simulated
|
||||
:param prop: Properties of this camera such as FOV and FPS
|
||||
:param minTargetAreaPercent: The minimum percentage(0 - 100) a detected target must take up of
|
||||
the camera's image to be processed. Match this with your contour filtering settings in the
|
||||
PhotonVision GUI.
|
||||
:param maxSightRangeMeters: Maximum distance at which the target is illuminated to your camera.
|
||||
Note that minimum target area of the image is separate from this.
|
||||
"""
|
||||
|
||||
self.minTargetAreaPercent: float = 0.0
|
||||
self.maxSightRange: float = 1.0e99
|
||||
self.videoSimRawEnabled: bool = False
|
||||
self.videoSimWireframeEnabled: bool = False
|
||||
self.videoSimWireframeResolution: float = 0.1
|
||||
self.videoSimProcEnabled: bool = (
|
||||
False # TODO switch this back to default True when the functionality is enabled
|
||||
)
|
||||
# TODO switch this back to default True when the functionality is enabled
|
||||
self.videoSimProcEnabled: bool = False
|
||||
self.heartbeatCounter: int = 0
|
||||
self.nextNtEntryTime = int(wpilib.Timer.getFPGATimestamp() * 1e6)
|
||||
self.nextNtEntryTime = wpilib.Timer.getFPGATimestamp()
|
||||
self.tagLayout = AprilTagFieldLayout.loadField(AprilTagField.k2024Crescendo)
|
||||
|
||||
self.cam = camera
|
||||
@@ -76,7 +94,7 @@ class PhotonCameraSim:
|
||||
(self.prop.getResWidth(), self.prop.getResHeight())
|
||||
)
|
||||
|
||||
self.ts = NTTopicSet("photonvision", self.cam.getName())
|
||||
self.ts = NTTopicSet(self.cam._cameraTable)
|
||||
self.ts.updateEntries()
|
||||
|
||||
# Handle this last explicitly for this function signature because the other constructor is called in the initialiser list
|
||||
@@ -103,22 +121,39 @@ class PhotonCameraSim:
|
||||
return self.videoSimFrameRaw
|
||||
|
||||
def canSeeTargetPose(self, camPose: Pose3d, target: VisionTargetSim) -> bool:
|
||||
"""Determines if this target's pose should be visible to the camera without considering its
|
||||
projected image points. Does not account for image area.
|
||||
|
||||
:param camPose: Camera's 3d pose
|
||||
:param target: Vision target containing pose and shape
|
||||
|
||||
:returns: If this vision target can be seen before image projection.
|
||||
"""
|
||||
|
||||
rel = CameraTargetRelation(camPose, target.getPose())
|
||||
return (
|
||||
(
|
||||
# target translation is outside of camera's FOV
|
||||
abs(rel.camToTargYaw.degrees())
|
||||
< self.prop.getHorizFOV().degrees() / 2.0
|
||||
and abs(rel.camToTargPitch.degrees())
|
||||
< self.prop.getVertFOV().degrees() / 2.0
|
||||
)
|
||||
and (
|
||||
# camera is behind planar target and it should be occluded
|
||||
not target.getModel().getIsPlanar()
|
||||
or abs(rel.targtoCamAngle.degrees()) < 90
|
||||
)
|
||||
# target is too far
|
||||
and rel.camToTarg.translation().norm() <= self.maxSightRange
|
||||
)
|
||||
|
||||
def canSeeCorner(self, points: np.ndarray) -> bool:
|
||||
"""Determines if all target points are inside the camera's image.
|
||||
|
||||
:param points: The target's 2d image points
|
||||
"""
|
||||
|
||||
assert points.shape[1] == 1
|
||||
assert points.shape[2] == 2
|
||||
for pt in points:
|
||||
@@ -130,51 +165,88 @@ class PhotonCameraSim:
|
||||
or y < 0
|
||||
or y > self.prop.getResHeight()
|
||||
):
|
||||
return False
|
||||
return False # point is outside of resolution
|
||||
|
||||
return True
|
||||
|
||||
def consumeNextEntryTime(self) -> float | None:
|
||||
now = int(wpilib.Timer.getFPGATimestamp() * 1e6)
|
||||
timestamp = 0
|
||||
"""Determine if this camera should process a new frame based on performance metrics and the time
|
||||
since the last update. This returns an Optional which is either empty if no update should occur
|
||||
or a float of the timestamp in seconds of when the frame which should be received by NT. If
|
||||
a timestamp is returned, the last frame update time becomes that timestamp.
|
||||
|
||||
:returns: Optional float which is empty while blocked or the NT entry timestamp in seconds if
|
||||
ready
|
||||
"""
|
||||
# check if this camera is ready for another frame update
|
||||
now = wpilib.Timer.getFPGATimestamp()
|
||||
timestamp = 0.0
|
||||
iter = 0
|
||||
# prepare next latest update
|
||||
while now >= self.nextNtEntryTime:
|
||||
timestamp = int(self.nextNtEntryTime)
|
||||
frameTime = int(self.prop.estSecUntilNextFrame() * 1e6)
|
||||
timestamp = self.nextNtEntryTime
|
||||
frameTime = self.prop.estSecUntilNextFrame()
|
||||
self.nextNtEntryTime += frameTime
|
||||
|
||||
# if frame time is very small, avoid blocking
|
||||
iter += 1
|
||||
if iter > 50:
|
||||
timestamp = now
|
||||
self.nextNtEntryTime = now + frameTime
|
||||
break
|
||||
|
||||
# return the timestamp of the latest update
|
||||
if timestamp != 0:
|
||||
return timestamp
|
||||
|
||||
# or this camera isn't ready to process yet
|
||||
return None
|
||||
|
||||
def setMinTargetAreaPercent(self, areaPercent: float) -> None:
|
||||
"""The minimum percentage(0 - 100) a detected target must take up of the camera's image to be
|
||||
processed.
|
||||
"""
|
||||
self.minTargetAreaPercent = areaPercent
|
||||
|
||||
def setMinTargetAreaPixels(self, areaPx: float) -> None:
|
||||
"""The minimum number of pixels a detected target must take up in the camera's image to be
|
||||
processed.
|
||||
"""
|
||||
self.minTargetAreaPercent = areaPx / self.prop.getResArea() * 100.0
|
||||
|
||||
def setMaxSightRange(self, range: meters) -> None:
|
||||
"""Maximum distance at which the target is illuminated to your camera. Note that minimum target
|
||||
area of the image is separate from this.
|
||||
"""
|
||||
self.maxSightRange = range
|
||||
|
||||
def enableRawStream(self, enabled: bool) -> None:
|
||||
"""Sets whether the raw video stream simulation is enabled.
|
||||
|
||||
Note: This may increase loop times.
|
||||
"""
|
||||
self.videoSimRawEnabled = enabled
|
||||
raise Exception("Raw stream not implemented")
|
||||
|
||||
def enableDrawWireframe(self, enabled: bool) -> None:
|
||||
"""Sets whether a wireframe of the field is drawn to the raw video stream.
|
||||
|
||||
Note: This will dramatically increase loop times.
|
||||
"""
|
||||
self.videoSimWireframeEnabled = enabled
|
||||
raise Exception("Wireframe not implemented")
|
||||
|
||||
def setWireframeResolution(self, resolution: float) -> None:
|
||||
"""Sets the resolution of the drawn wireframe if enabled. Drawn line segments will be subdivided
|
||||
into smaller segments based on a threshold set by the resolution.
|
||||
|
||||
:param resolution: Resolution as a fraction(0 - 1) of the video frame's diagonal length in
|
||||
pixels
|
||||
"""
|
||||
self.videoSimWireframeResolution = resolution
|
||||
|
||||
def enableProcessedStream(self, enabled: bool) -> None:
|
||||
"""Sets whether the processed video stream simulation is enabled."""
|
||||
self.videoSimProcEnabled = enabled
|
||||
raise Exception("Processed stream not implemented")
|
||||
|
||||
@@ -187,25 +259,32 @@ class PhotonCameraSim:
|
||||
|
||||
targets.sort(key=distance, reverse=True)
|
||||
|
||||
# all targets visible before noise
|
||||
visibleTgts: list[typing.Tuple[VisionTargetSim, np.ndarray]] = []
|
||||
# all targets actually detected by camera (after noise)
|
||||
detectableTgts: list[PhotonTrackedTarget] = []
|
||||
|
||||
# basis change from world coordinates to camera coordinates
|
||||
camRt = RotTrlTransform3d.makeRelativeTo(cameraPose)
|
||||
|
||||
for tgt in targets:
|
||||
# pose isn't visible, skip to next
|
||||
if not self.canSeeTargetPose(cameraPose, tgt):
|
||||
continue
|
||||
|
||||
# find target's 3d corner points
|
||||
fieldCorners = tgt.getFieldVertices()
|
||||
isSpherical = tgt.getModel().getIsSpherical()
|
||||
if isSpherical:
|
||||
if isSpherical: # target is spherical
|
||||
model = tgt.getModel()
|
||||
# orient the model to the camera (like a sprite/decal) so it appears similar regardless of view
|
||||
fieldCorners = model.getFieldVertices(
|
||||
TargetModel.getOrientedPose(
|
||||
tgt.getPose().translation(), cameraPose.translation()
|
||||
)
|
||||
)
|
||||
|
||||
# project 3d target points into 2d image points
|
||||
imagePoints = OpenCVHelp.projectPoints(
|
||||
self.prop.getIntrinsics(),
|
||||
self.prop.getDistCoeffs(),
|
||||
@@ -213,9 +292,11 @@ class PhotonCameraSim:
|
||||
fieldCorners,
|
||||
)
|
||||
|
||||
# spherical targets need a rotated rectangle of their midpoints for visualization
|
||||
if isSpherical:
|
||||
center = OpenCVHelp.avgPoint(imagePoints)
|
||||
l: int = 0
|
||||
# reference point (left side midpoint)
|
||||
for i in range(4):
|
||||
if imagePoints[i, 0, 0] < imagePoints[l, 0, 0].x:
|
||||
l = i
|
||||
@@ -239,6 +320,7 @@ class PhotonCameraSim:
|
||||
for i in range(4):
|
||||
if i != t and i != l and i != b:
|
||||
r = i
|
||||
# create RotatedRect from midpoints
|
||||
rect = cv.RotatedRect(
|
||||
(center[0, 0], center[0, 1]),
|
||||
(
|
||||
@@ -247,16 +329,23 @@ class PhotonCameraSim:
|
||||
),
|
||||
-angles[r],
|
||||
)
|
||||
# set target corners to rect corners
|
||||
imagePoints = np.array([[p[0], p[1], p[2]] for p in rect.points()])
|
||||
|
||||
# save visible targets for raw video stream simulation
|
||||
visibleTgts.append((tgt, imagePoints))
|
||||
# estimate pixel noise
|
||||
noisyTargetCorners = self.prop.estPixelNoise(imagePoints)
|
||||
# find the minimum area rectangle of target corners
|
||||
minAreaRect = OpenCVHelp.getMinAreaRect(noisyTargetCorners)
|
||||
minAreaRectPts = minAreaRect.points()
|
||||
# find the (naive) 2d yaw/pitch
|
||||
centerPt = minAreaRect.center
|
||||
centerRot = self.prop.getPixelRot(centerPt)
|
||||
# find contour area
|
||||
areaPercent = self.prop.getContourAreaPercent(noisyTargetCorners)
|
||||
|
||||
# projected target can't be detected, skip to next
|
||||
if (
|
||||
not self.canSeeCorner(noisyTargetCorners)
|
||||
or not areaPercent >= self.minTargetAreaPercent
|
||||
@@ -265,6 +354,7 @@ class PhotonCameraSim:
|
||||
|
||||
pnpSim: PnpResult | None = None
|
||||
if tgt.fiducialId >= 0 and len(tgt.getFieldVertices()) == 4:
|
||||
# single AprilTag solvePNP
|
||||
pnpSim = OpenCVHelp.solvePNP_Square(
|
||||
self.prop.getIntrinsics(),
|
||||
self.prop.getDistCoeffs(),
|
||||
@@ -295,6 +385,7 @@ class PhotonCameraSim:
|
||||
|
||||
# Video streams disabled for now
|
||||
if self.videoSimRawEnabled:
|
||||
# TODO Video streams disabled for now port and uncomment when implemented
|
||||
# VideoSimUtil::UpdateVideoProp(videoSimRaw, prop);
|
||||
# cv::Size videoFrameSize{prop.GetResWidth(), prop.GetResHeight()};
|
||||
# cv::Mat blankFrame = cv::Mat::zeros(videoFrameSize, CV_8UC1);
|
||||
@@ -312,6 +403,7 @@ class PhotonCameraSim:
|
||||
|
||||
if len(visibleLayoutTags) > 1:
|
||||
usedIds = [tag.ID for tag in visibleLayoutTags]
|
||||
# sort target order sorts in ascending order by default
|
||||
usedIds.sort()
|
||||
pnpResult = VisionEstimation.estimateCamPosePNP(
|
||||
self.prop.getIntrinsics(),
|
||||
@@ -323,56 +415,72 @@ class PhotonCameraSim:
|
||||
if pnpResult is not None:
|
||||
multiTagResults = MultiTargetPNPResult(pnpResult, usedIds)
|
||||
|
||||
# put this simulated data to NT
|
||||
self.heartbeatCounter += 1
|
||||
now_micros = wpilib.Timer.getFPGATimestamp() * 1e6
|
||||
return PhotonPipelineResult(
|
||||
metadata=PhotonPipelineMetadata(
|
||||
self.heartbeatCounter, int(latency * 1e6), 1000000
|
||||
self.heartbeatCounter,
|
||||
int(now_micros - latency * 1e6),
|
||||
int(now_micros),
|
||||
# Pretend like we heard a pong recently
|
||||
int(np.random.uniform(950, 1050)),
|
||||
),
|
||||
targets=detectableTgts,
|
||||
multitagResult=multiTagResults,
|
||||
)
|
||||
|
||||
def submitProcessedFrame(
|
||||
self, result: PhotonPipelineResult, receiveTimestamp: float | None
|
||||
self,
|
||||
result: PhotonPipelineResult,
|
||||
receiveTimestamp_us: float | None = None,
|
||||
):
|
||||
if receiveTimestamp is None:
|
||||
receiveTimestamp = wpilib.Timer.getFPGATimestamp() * 1e6
|
||||
receiveTimestamp = int(receiveTimestamp)
|
||||
"""Simulate one processed frame of vision data, putting one result to NT. Image capture timestamp
|
||||
overrides :meth:`.PhotonPipelineResult.getTimestampSeconds` for more
|
||||
precise latency simulation.
|
||||
|
||||
self.ts.latencyMillisEntry.set(result.getLatencyMillis(), receiveTimestamp)
|
||||
:param result: The pipeline result to submit
|
||||
:param receiveTimestamp: The (sim) timestamp when this result was read by NT in microseconds. If not passed image capture time is assumed be (current time - latency)
|
||||
"""
|
||||
if receiveTimestamp_us is None:
|
||||
receiveTimestamp_us = wpilib.Timer.getFPGATimestamp() * 1e6
|
||||
receiveTimestamp_us = int(receiveTimestamp_us)
|
||||
|
||||
self.ts.latencyMillisEntry.set(result.getLatencyMillis(), receiveTimestamp_us)
|
||||
|
||||
newPacket = PhotonPipelineResult.photonStruct.pack(result)
|
||||
self.ts.rawBytesEntry.set(newPacket.getData(), receiveTimestamp)
|
||||
self.ts.rawBytesEntry.set(newPacket.getData(), receiveTimestamp_us)
|
||||
|
||||
hasTargets = result.hasTargets()
|
||||
self.ts.hasTargetEntry.set(hasTargets, receiveTimestamp)
|
||||
self.ts.hasTargetEntry.set(hasTargets, receiveTimestamp_us)
|
||||
if not hasTargets:
|
||||
self.ts.targetPitchEntry.set(0.0, receiveTimestamp)
|
||||
self.ts.targetYawEntry.set(0.0, receiveTimestamp)
|
||||
self.ts.targetAreaEntry.set(0.0, receiveTimestamp)
|
||||
self.ts.targetPoseEntry.set(Transform3d(), receiveTimestamp)
|
||||
self.ts.targetSkewEntry.set(0.0, receiveTimestamp)
|
||||
self.ts.targetPitchEntry.set(0.0, receiveTimestamp_us)
|
||||
self.ts.targetYawEntry.set(0.0, receiveTimestamp_us)
|
||||
self.ts.targetAreaEntry.set(0.0, receiveTimestamp_us)
|
||||
self.ts.targetPoseEntry.set(Transform3d(), receiveTimestamp_us)
|
||||
self.ts.targetSkewEntry.set(0.0, receiveTimestamp_us)
|
||||
else:
|
||||
bestTarget = result.getBestTarget()
|
||||
assert bestTarget
|
||||
|
||||
self.ts.targetPitchEntry.set(bestTarget.getPitch(), receiveTimestamp)
|
||||
self.ts.targetYawEntry.set(bestTarget.getYaw(), receiveTimestamp)
|
||||
self.ts.targetAreaEntry.set(bestTarget.getArea(), receiveTimestamp)
|
||||
self.ts.targetSkewEntry.set(bestTarget.getSkew(), receiveTimestamp)
|
||||
self.ts.targetPitchEntry.set(bestTarget.getPitch(), receiveTimestamp_us)
|
||||
self.ts.targetYawEntry.set(bestTarget.getYaw(), receiveTimestamp_us)
|
||||
self.ts.targetAreaEntry.set(bestTarget.getArea(), receiveTimestamp_us)
|
||||
self.ts.targetSkewEntry.set(bestTarget.getSkew(), receiveTimestamp_us)
|
||||
|
||||
self.ts.targetPoseEntry.set(
|
||||
bestTarget.getBestCameraToTarget(), receiveTimestamp
|
||||
bestTarget.getBestCameraToTarget(), receiveTimestamp_us
|
||||
)
|
||||
|
||||
intrinsics = self.prop.getIntrinsics()
|
||||
intrinsicsView = intrinsics.flatten().tolist()
|
||||
self.ts.cameraIntrinsicsPublisher.set(intrinsicsView, receiveTimestamp)
|
||||
intrinsics = self.prop.getIntrinsics()
|
||||
intrinsicsView = intrinsics.flatten().tolist()
|
||||
self.ts.cameraIntrinsicsPublisher.set(intrinsicsView, receiveTimestamp_us)
|
||||
|
||||
distortion = self.prop.getDistCoeffs()
|
||||
distortionView = distortion.flatten().tolist()
|
||||
self.ts.cameraDistortionPublisher.set(distortionView, receiveTimestamp)
|
||||
distortion = self.prop.getDistCoeffs()
|
||||
distortionView = distortion.flatten().tolist()
|
||||
self.ts.cameraDistortionPublisher.set(distortionView, receiveTimestamp_us)
|
||||
|
||||
self.ts.heartbeatPublisher.set(self.heartbeatCounter, receiveTimestamp)
|
||||
self.ts.heartbeatPublisher.set(self.heartbeatCounter, receiveTimestamp_us)
|
||||
self.heartbeatCounter += 1
|
||||
|
||||
self.ts.subTable.getInstance().flush()
|
||||
self.ts.subTable.getInstance().flush()
|
||||
|
||||
@@ -4,18 +4,36 @@ import typing
|
||||
|
||||
import cv2 as cv
|
||||
import numpy as np
|
||||
import numpy.typing as npt
|
||||
from wpimath.geometry import Rotation2d, Rotation3d, Translation3d
|
||||
from wpimath.units import hertz, seconds
|
||||
|
||||
from ..estimation import RotTrlTransform3d
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class SimCameraProperties:
|
||||
"""Calibration and performance values for this camera.
|
||||
|
||||
The resolution will affect the accuracy of projected(3d to 2d) target corners and similarly
|
||||
the severity of image noise on estimation(2d to 3d).
|
||||
|
||||
The camera intrinsics and distortion coefficients describe the results of calibration, and how
|
||||
to map between 3d field points and 2d image points.
|
||||
|
||||
The performance values (framerate/exposure time, latency) determine how often results should
|
||||
be updated and with how much latency in simulation. High exposure time causes motion blur which
|
||||
can inhibit target detection while moving. Note that latency estimation does not account for
|
||||
network latency and the latency reported will always be perfect.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
"""Default constructor which is the same as {@link #PERFECT_90DEG}"""
|
||||
self.resWidth: int = -1
|
||||
self.resHeight: int = -1
|
||||
self.camIntrinsics: np.ndarray = np.zeros((3, 3)) # [3,3]
|
||||
self.distCoeffs: np.ndarray = np.zeros((8, 1)) # [8,1]
|
||||
self.camIntrinsics: npt.NDArray[np.floating] = np.zeros((3, 3)) # [3,3]
|
||||
self.distCoeffs: npt.NDArray[np.floating] = np.zeros((8, 1)) # [8,1]
|
||||
self.avgErrorPx: float = 0.0
|
||||
self.errorStdDevPx: float = 0.0
|
||||
self.frameSpeed: seconds = 0.0
|
||||
@@ -31,21 +49,25 @@ class SimCameraProperties:
|
||||
) -> None:
|
||||
if fovDiag.degrees() < 1.0 or fovDiag.degrees() > 179.0:
|
||||
fovDiag = Rotation2d.fromDegrees(max(min(fovDiag.degrees(), 179.0), 1.0))
|
||||
logging.error("Requested invalid FOV! Clamping between (1, 179) degrees...")
|
||||
logger.error("Requested invalid FOV! Clamping between (1, 179) degrees...")
|
||||
|
||||
resDiag = math.sqrt(width * width + height * height)
|
||||
diagRatio = math.tan(fovDiag.radians() / 2.0)
|
||||
fovWidth = Rotation2d(math.atan((diagRatio * (width / resDiag)) * 2))
|
||||
fovHeight = Rotation2d(math.atan(diagRatio * (height / resDiag)) * 2)
|
||||
|
||||
# assume no distortion
|
||||
newDistCoeffs = np.zeros((8, 1))
|
||||
|
||||
# assume centered principal point (pixels)
|
||||
cx = width / 2.0 - 0.5
|
||||
cy = height / 2.0 - 0.5
|
||||
|
||||
# use given fov to determine focal point (pixels)
|
||||
fx = cx / math.tan(fovWidth.radians() / 2.0)
|
||||
fy = cy / math.tan(fovHeight.radians() / 2.0)
|
||||
|
||||
# create camera intrinsics matrix
|
||||
newCamIntrinsics = np.array([[fx, 0.0, cx], [0.0, fy, cy], [0.0, 0.0, 1.0]])
|
||||
|
||||
self.setCalibrationFromIntrinsics(
|
||||
@@ -59,12 +81,12 @@ class SimCameraProperties:
|
||||
newCamIntrinsics: np.ndarray,
|
||||
newDistCoeffs: np.ndarray,
|
||||
) -> None:
|
||||
|
||||
self.resWidth = width
|
||||
self.resHeight = height
|
||||
self.camIntrinsics = newCamIntrinsics
|
||||
self.distCoeffs = newDistCoeffs
|
||||
|
||||
# left, right, up, and down view planes
|
||||
p = [
|
||||
Translation3d(
|
||||
1.0,
|
||||
@@ -110,16 +132,33 @@ class SimCameraProperties:
|
||||
self.errorStdDevPx = newErrorStdDevPx
|
||||
|
||||
def setFPS(self, fps: hertz):
|
||||
"""
|
||||
:param fps: The average frames per second the camera should process at. :strong:`Exposure time limits
|
||||
FPS if set!`
|
||||
"""
|
||||
|
||||
self.frameSpeed = max(1.0 / fps, self.exposureTime)
|
||||
|
||||
def setExposureTime(self, newExposureTime: seconds):
|
||||
"""
|
||||
:param newExposureTime: The amount of time the "shutter" is open for one frame. Affects motion
|
||||
blur. **Frame speed(from FPS) is limited to this!**
|
||||
"""
|
||||
|
||||
self.exposureTime = newExposureTime
|
||||
self.frameSpeed = max(self.frameSpeed, self.exposureTime)
|
||||
|
||||
def setAvgLatency(self, newAvgLatency: seconds):
|
||||
"""
|
||||
:param newAvgLatency: The average latency (from image capture to data published) in milliseconds
|
||||
a frame should have
|
||||
"""
|
||||
self.vgLatency = newAvgLatency
|
||||
|
||||
def setLatencyStdDev(self, newLatencyStdDev: seconds):
|
||||
"""
|
||||
:param latencyStdDevMs: The standard deviation in milliseconds of the latency
|
||||
"""
|
||||
self.latencyStdDev = newLatencyStdDev
|
||||
|
||||
def getResWidth(self) -> int:
|
||||
@@ -134,10 +173,10 @@ class SimCameraProperties:
|
||||
def getAspectRatio(self) -> float:
|
||||
return 1.0 * self.resWidth / self.resHeight
|
||||
|
||||
def getIntrinsics(self) -> np.ndarray:
|
||||
def getIntrinsics(self) -> npt.NDArray[np.floating]:
|
||||
return self.camIntrinsics
|
||||
|
||||
def getDistCoeffs(self) -> np.ndarray:
|
||||
def getDistCoeffs(self) -> npt.NDArray[np.floating]:
|
||||
return self.distCoeffs
|
||||
|
||||
def getFPS(self) -> hertz:
|
||||
@@ -156,21 +195,43 @@ class SimCameraProperties:
|
||||
return self.latencyStdDev
|
||||
|
||||
def getContourAreaPercent(self, points: np.ndarray) -> float:
|
||||
"""The percentage(0 - 100) of this camera's resolution the contour takes up in pixels of the
|
||||
image.
|
||||
|
||||
:param points: Points of the contour
|
||||
"""
|
||||
|
||||
return cv.contourArea(cv.convexHull(points)) / self.getResArea() * 100.0
|
||||
|
||||
def getPixelYaw(self, pixelX: float) -> Rotation2d:
|
||||
"""The yaw from the principal point of this camera to the pixel x value. Positive values left."""
|
||||
fx = self.camIntrinsics[0, 0]
|
||||
# account for principal point not being centered
|
||||
cx = self.camIntrinsics[0, 2]
|
||||
xOffset = cx - pixelX
|
||||
return Rotation2d(fx, xOffset)
|
||||
|
||||
def getPixelPitch(self, pixelY: float) -> Rotation2d:
|
||||
"""The pitch from the principal point of this camera to the pixel y value. Pitch is positive down.
|
||||
|
||||
Note that this angle is naively computed and may be incorrect. See {@link
|
||||
#getCorrectedPixelRot(Point)}.
|
||||
"""
|
||||
|
||||
fy = self.camIntrinsics[1, 1]
|
||||
# account for principal point not being centered
|
||||
cy = self.camIntrinsics[1, 2]
|
||||
yOffset = cy - pixelY
|
||||
return Rotation2d(fy, -yOffset)
|
||||
|
||||
def getPixelRot(self, point: cv.typing.Point2f) -> Rotation3d:
|
||||
"""Finds the yaw and pitch to the given image point. Yaw is positive left, and pitch is positive
|
||||
down.
|
||||
|
||||
Note that pitch is naively computed and may be incorrect. See {@link
|
||||
#getCorrectedPixelRot(Point)}.
|
||||
"""
|
||||
|
||||
return Rotation3d(
|
||||
0.0,
|
||||
self.getPixelPitch(point[1]).radians(),
|
||||
@@ -178,6 +239,27 @@ class SimCameraProperties:
|
||||
)
|
||||
|
||||
def getCorrectedPixelRot(self, point: cv.typing.Point2f) -> Rotation3d:
|
||||
"""Gives the yaw and pitch of the line intersecting the camera lens and the given pixel
|
||||
coordinates on the sensor. Yaw is positive left, and pitch positive down.
|
||||
|
||||
The pitch traditionally calculated from pixel offsets do not correctly account for non-zero
|
||||
values of yaw because of perspective distortion (not to be confused with lens distortion)-- for
|
||||
example, the pitch angle is naively calculated as:
|
||||
|
||||
<pre>pitch = arctan(pixel y offset / focal length y)</pre>
|
||||
|
||||
However, using focal length as a side of the associated right triangle is not correct when the
|
||||
pixel x value is not 0, because the distance from this pixel (projected on the x-axis) to the
|
||||
camera lens increases. Projecting a line back out of the camera with these naive angles will
|
||||
not intersect the 3d point that was originally projected into this 2d pixel. Instead, this
|
||||
length should be:
|
||||
|
||||
<pre>focal length y ⟶ (focal length y / cos(arctan(pixel x offset / focal length x)))</pre>
|
||||
|
||||
:returns: Rotation3d with yaw and pitch of the line projected out of the camera from the given
|
||||
pixel (roll is zero).
|
||||
"""
|
||||
|
||||
fx = self.camIntrinsics[0, 0]
|
||||
cx = self.camIntrinsics[0, 2]
|
||||
xOffset = cx - point[0]
|
||||
@@ -191,11 +273,13 @@ class SimCameraProperties:
|
||||
return Rotation3d(0.0, pitch.radians(), yaw.radians())
|
||||
|
||||
def getHorizFOV(self) -> Rotation2d:
|
||||
# sum of FOV left and right principal point
|
||||
left = self.getPixelYaw(0)
|
||||
right = self.getPixelYaw(self.resWidth)
|
||||
return left - right
|
||||
|
||||
def getVertFOV(self) -> Rotation2d:
|
||||
# sum of FOV above and below principal point
|
||||
above = self.getPixelPitch(0)
|
||||
below = self.getPixelPitch(self.resHeight)
|
||||
return below - above
|
||||
@@ -208,9 +292,34 @@ class SimCameraProperties:
|
||||
def getVisibleLine(
|
||||
self, camRt: RotTrlTransform3d, a: Translation3d, b: Translation3d
|
||||
) -> typing.Tuple[float | None, float | None]:
|
||||
"""Determines where the line segment defined by the two given translations intersects the camera's
|
||||
frustum/field-of-vision, if at all.
|
||||
|
||||
The line is parametrized so any of its points <code>p = t * (b - a) + a</code>. This method
|
||||
returns these values of t, minimum first, defining the region of the line segment which is
|
||||
visible in the frustum. If both ends of the line segment are visible, this simply returns {0,
|
||||
1}. If, for example, point b is visible while a is not, and half of the line segment is inside
|
||||
the camera frustum, {0.5, 1} would be returned.
|
||||
|
||||
:param camRt: The change in basis from world coordinates to camera coordinates. See {@link
|
||||
RotTrlTransform3d#makeRelativeTo(Pose3d)}.
|
||||
:param a: The initial translation of the line
|
||||
:param b: The final translation of the line
|
||||
|
||||
:returns: A Pair of Doubles. The values may be null:
|
||||
|
||||
- {Double, Double} : Two parametrized values(t), minimum first, representing which
|
||||
segment of the line is visible in the camera frustum.
|
||||
- {Double, null} : One value(t) representing a single intersection point. For example,
|
||||
the line only intersects the intersection of two adjacent viewplanes.
|
||||
- {null, null} : No values. The line segment is not visible in the camera frustum.
|
||||
"""
|
||||
|
||||
# translations relative to the camera
|
||||
relA = camRt.applyTranslation(a)
|
||||
relB = camRt.applyTranslation(b)
|
||||
|
||||
# check if both ends are behind camera
|
||||
if relA.X() <= 0.0 and relB.X() <= 0.0:
|
||||
return (None, None)
|
||||
|
||||
@@ -221,6 +330,7 @@ class SimCameraProperties:
|
||||
aVisible = True
|
||||
bVisible = True
|
||||
|
||||
# check if the ends of the line segment are visible
|
||||
for normal in self.viewplanes:
|
||||
aVisibility = av.dot(normal)
|
||||
if aVisibility < 0:
|
||||
@@ -229,38 +339,53 @@ class SimCameraProperties:
|
||||
bVisibility = bv.dot(normal)
|
||||
if bVisibility < 0:
|
||||
bVisible = False
|
||||
# both ends are outside at least one of the same viewplane
|
||||
if aVisibility <= 0 and bVisibility <= 0:
|
||||
return (None, None)
|
||||
|
||||
# both ends are inside frustum
|
||||
if aVisible and bVisible:
|
||||
return (0.0, 1.0)
|
||||
|
||||
# parametrized (t=0 at a, t=1 at b) intersections with viewplanes
|
||||
intersections = [float("nan"), float("nan"), float("nan"), float("nan")]
|
||||
|
||||
# Optionally 3x1 vector
|
||||
ipts: typing.List[np.ndarray | None] = [None, None, None, None]
|
||||
|
||||
# find intersections
|
||||
for i, normal in enumerate(self.viewplanes):
|
||||
# // we want to know the value of t when the line intercepts this plane
|
||||
# // parametrized: v = t * ab + a, where v lies on the plane
|
||||
# // we can find the projection of a onto the plane normal
|
||||
# // a_projn = normal.times(av.dot(normal) / normal.dot(normal));
|
||||
a_projn = (av.dot(normal) / normal.dot(normal)) * normal
|
||||
|
||||
# // this projection lets us determine the scalar multiple t of ab where
|
||||
# // (t * ab + a) is a vector which lies on the plane
|
||||
if abs(abv.dot(normal)) < 1.0e-5:
|
||||
continue
|
||||
intersections[i] = a_projn.dot(a_projn) / -(abv.dot(a_projn))
|
||||
|
||||
# // vector a to the viewplane
|
||||
apv = intersections[i] * abv
|
||||
# av + apv = intersection point
|
||||
intersectpt = av + apv
|
||||
ipts[i] = intersectpt
|
||||
|
||||
# // discard intersections outside the camera frustum
|
||||
for j in range(1, len(self.viewplanes)):
|
||||
if j == 0:
|
||||
continue
|
||||
oi = (i + j) % len(self.viewplanes)
|
||||
onormal = self.viewplanes[oi]
|
||||
# if the dot of the intersection point with any plane normal is negative, it is outside
|
||||
if intersectpt.dot(onormal) < 0:
|
||||
intersections[i] = float("nan")
|
||||
ipts[i] = None
|
||||
break
|
||||
|
||||
# // discard duplicate intersections
|
||||
if ipts[i] is None:
|
||||
continue
|
||||
|
||||
@@ -275,6 +400,7 @@ class SimCameraProperties:
|
||||
ipts[i] = None
|
||||
break
|
||||
|
||||
# determine visible segment (minimum and maximum t)
|
||||
inter1 = float("nan")
|
||||
inter2 = float("nan")
|
||||
for inter in intersections:
|
||||
@@ -284,6 +410,7 @@ class SimCameraProperties:
|
||||
else:
|
||||
inter2 = inter
|
||||
|
||||
# // two viewplane intersections
|
||||
if not math.isnan(inter2):
|
||||
max_ = max(inter1, inter2)
|
||||
min_ = min(inter1, inter2)
|
||||
@@ -292,16 +419,19 @@ class SimCameraProperties:
|
||||
if bVisible:
|
||||
max_ = 1
|
||||
return (min_, max_)
|
||||
# // one viewplane intersection
|
||||
elif not math.isnan(inter1):
|
||||
if aVisible:
|
||||
return (0, inter1)
|
||||
if bVisible:
|
||||
return (inter1, 1)
|
||||
return (inter1, None)
|
||||
# no intersections
|
||||
else:
|
||||
return (None, None)
|
||||
|
||||
def estPixelNoise(self, points: np.ndarray) -> np.ndarray:
|
||||
"""Returns these points after applying this camera's estimated noise."""
|
||||
assert points.shape[1] == 1, points.shape
|
||||
assert points.shape[2] == 2, points.shape
|
||||
if self.avgErrorPx == 0 and self.errorStdDevPx == 0:
|
||||
@@ -309,6 +439,7 @@ class SimCameraProperties:
|
||||
|
||||
noisyPts: list[list] = []
|
||||
for p in points:
|
||||
# // error pixels in random direction
|
||||
error = np.random.normal(self.avgErrorPx, self.errorStdDevPx, 1)[0]
|
||||
errorAngle = np.random.uniform(-math.pi, math.pi)
|
||||
noisyPts.append(
|
||||
@@ -324,16 +455,25 @@ class SimCameraProperties:
|
||||
return retval
|
||||
|
||||
def estLatency(self) -> seconds:
|
||||
"""
|
||||
:returns: Noisy estimation of a frame's processing latency
|
||||
"""
|
||||
|
||||
return max(
|
||||
float(np.random.normal(self.avgLatency, self.latencyStdDev, 1)[0]),
|
||||
0.0,
|
||||
)
|
||||
|
||||
def estSecUntilNextFrame(self) -> seconds:
|
||||
"""
|
||||
:returns: Estimate how long until the next frame should be processed in seconds
|
||||
"""
|
||||
# // exceptional processing latency blocks the next frame
|
||||
return self.frameSpeed + max(0.0, self.estLatency() - self.frameSpeed)
|
||||
|
||||
@classmethod
|
||||
def PERFECT_90DEG(cls) -> typing.Self:
|
||||
"""960x720 resolution, 90 degree FOV, "perfect" lagless camera"""
|
||||
return cls()
|
||||
|
||||
@classmethod
|
||||
|
||||
@@ -15,7 +15,22 @@ from .visionTargetSim import VisionTargetSim
|
||||
|
||||
|
||||
class VisionSystemSim:
|
||||
"""A simulated vision system involving a camera(s) and coprocessor(s) mounted on a mobile robot
|
||||
running PhotonVision, detecting targets placed on the field. :class:`.VisionTargetSim`s added to
|
||||
this class will be detected by the :class:`.PhotonCameraSim`s added to this class. This class
|
||||
should be updated periodically with the robot's current pose in order to publish the simulated
|
||||
camera target info.
|
||||
"""
|
||||
|
||||
def __init__(self, visionSystemName: str):
|
||||
"""A simulated vision system involving a camera(s) and coprocessor(s) mounted on a mobile robot
|
||||
running PhotonVision, detecting targets placed on the field. :class:`.VisionTargetSim`s added to
|
||||
this class will be detected by the :class:`.PhotonCameraSim`s added to this class. This class
|
||||
should be updated periodically with the robot's current pose in order to publish the simulated
|
||||
camera target info.
|
||||
|
||||
:param visionSystemName: The specific identifier for this vision system in NetworkTables.
|
||||
"""
|
||||
self.dbgField: Field2d = Field2d()
|
||||
self.bufferLength: seconds = 1.5
|
||||
|
||||
@@ -32,12 +47,21 @@ class VisionSystemSim:
|
||||
wpilib.SmartDashboard.putData(self.tableName + "/Sim Field", self.dbgField)
|
||||
|
||||
def getCameraSim(self, name: str) -> PhotonCameraSim | None:
|
||||
"""Get one of the simulated cameras."""
|
||||
return self.camSimMap.get(name, None)
|
||||
|
||||
def getCameraSims(self) -> list[PhotonCameraSim]:
|
||||
"""Get all the simulated cameras."""
|
||||
return [*self.camSimMap.values()]
|
||||
|
||||
def addCamera(self, cameraSim: PhotonCameraSim, robotToCamera: Transform3d) -> None:
|
||||
"""Adds a simulated camera to this vision system with a specified robot-to-camera transformation.
|
||||
The vision targets registered with this vision system simulation will be observed by the
|
||||
simulated :class:`.PhotonCamera`.
|
||||
|
||||
:param cameraSim: The camera simulation
|
||||
:param robotToCamera: The transform from the robot pose to the camera pose
|
||||
"""
|
||||
name = cameraSim.getCamera().getName()
|
||||
if name not in self.camSimMap:
|
||||
self.camSimMap[name] = cameraSim
|
||||
@@ -49,10 +73,15 @@ class VisionSystemSim:
|
||||
)
|
||||
|
||||
def clearCameras(self) -> None:
|
||||
"""Remove all simulated cameras from this vision system."""
|
||||
self.camSimMap.clear()
|
||||
self.camTrfMap.clear()
|
||||
|
||||
def removeCamera(self, cameraSim: PhotonCameraSim) -> bool:
|
||||
"""Remove a simulated camera from this vision system.
|
||||
|
||||
:returns: If the camera was present and removed
|
||||
"""
|
||||
name = cameraSim.getCamera().getName()
|
||||
if name in self.camSimMap:
|
||||
del self.camSimMap[name]
|
||||
@@ -65,6 +94,14 @@ class VisionSystemSim:
|
||||
cameraSim: PhotonCameraSim,
|
||||
time: seconds = wpilib.Timer.getFPGATimestamp(),
|
||||
) -> Transform3d | None:
|
||||
"""Get a simulated camera's position relative to the robot. If the requested camera is invalid, an
|
||||
empty optional is returned.
|
||||
|
||||
:param cameraSim: The specific camera to get the robot-to-camera transform of
|
||||
:param timeSeconds: Timestamp in seconds of when the transform should be observed
|
||||
|
||||
:returns: The transform of this camera, or an empty optional if it is invalid
|
||||
"""
|
||||
if cameraSim in self.camTrfMap:
|
||||
trfBuffer = self.camTrfMap[cameraSim]
|
||||
sample = trfBuffer.sample(time)
|
||||
@@ -80,6 +117,13 @@ class VisionSystemSim:
|
||||
cameraSim: PhotonCameraSim,
|
||||
time: seconds = wpilib.Timer.getFPGATimestamp(),
|
||||
) -> Pose3d | None:
|
||||
"""Get a simulated camera's position on the field. If the requested camera is invalid, an empty
|
||||
optional is returned.
|
||||
|
||||
:param cameraSim: The specific camera to get the field pose of
|
||||
|
||||
:returns: The pose of this camera, or an empty optional if it is invalid
|
||||
"""
|
||||
robotToCamera = self.getRobotToCamera(cameraSim, time)
|
||||
if robotToCamera is None:
|
||||
return None
|
||||
@@ -93,6 +137,14 @@ class VisionSystemSim:
|
||||
def adjustCamera(
|
||||
self, cameraSim: PhotonCameraSim, robotToCamera: Transform3d
|
||||
) -> bool:
|
||||
"""Adjust a camera's position relative to the robot. Use this if your camera is on a gimbal or
|
||||
turret or some other mobile platform.
|
||||
|
||||
:param cameraSim: The simulated camera to change the relative position of
|
||||
:param robotToCamera: New transform from the robot to the camera
|
||||
|
||||
:returns: If the cameraSim was valid and transform was adjusted
|
||||
"""
|
||||
if cameraSim in self.camTrfMap:
|
||||
self.camTrfMap[cameraSim].addSample(
|
||||
wpilib.Timer.getFPGATimestamp(), Pose3d() + robotToCamera
|
||||
@@ -102,6 +154,7 @@ class VisionSystemSim:
|
||||
return False
|
||||
|
||||
def resetCameraTransforms(self, cameraSim: PhotonCameraSim | None = None) -> None:
|
||||
"""Reset the transform history for this camera to just the current transform."""
|
||||
now = wpilib.Timer.getFPGATimestamp()
|
||||
|
||||
def resetSingleCamera(self, cameraSim: PhotonCameraSim) -> bool:
|
||||
@@ -133,12 +186,30 @@ class VisionSystemSim:
|
||||
def addVisionTargets(
|
||||
self, targets: list[VisionTargetSim], targetType: str = "targets"
|
||||
) -> None:
|
||||
"""Adds targets on the field which your vision system is designed to detect. The {@link
|
||||
PhotonCamera}s simulated from this system will report the location of the camera relative to
|
||||
the subset of these targets which are visible from the given camera position.
|
||||
|
||||
:param targets: Targets to add to the simulated field
|
||||
:param type: Type of target (e.g. "cargo").
|
||||
"""
|
||||
|
||||
if targetType not in self.targetSets:
|
||||
self.targetSets[targetType] = targets
|
||||
else:
|
||||
self.targetSets[targetType] += targets
|
||||
|
||||
def addAprilTags(self, layout: AprilTagFieldLayout) -> None:
|
||||
"""Adds targets on the field which your vision system is designed to detect. The {@link
|
||||
PhotonCamera}s simulated from this system will report the location of the camera relative to
|
||||
the subset of these targets which are visible from the given camera position.
|
||||
|
||||
The AprilTags from this layout will be added as vision targets under the type "apriltag".
|
||||
The poses added preserve the tag layout's current alliance origin. If the tag layout's alliance
|
||||
origin is changed, these added tags will have to be cleared and re-added.
|
||||
|
||||
:param tagLayout: The field tag layout to get Apriltag poses and IDs from
|
||||
"""
|
||||
targets: list[VisionTargetSim] = []
|
||||
for tag in layout.getTags():
|
||||
tag_pose = layout.getTagPose(tag.ID)
|
||||
@@ -172,9 +243,15 @@ class VisionSystemSim:
|
||||
def getRobotPose(
|
||||
self, timestamp: seconds = wpilib.Timer.getFPGATimestamp()
|
||||
) -> Pose3d | None:
|
||||
"""Get the robot pose in meters saved by the vision system at this timestamp.
|
||||
|
||||
:param timestamp: Timestamp of the desired robot pose
|
||||
"""
|
||||
|
||||
return self.robotPoseBuffer.sample(timestamp)
|
||||
|
||||
def resetRobotPose(self, robotPose: Pose2d | Pose3d) -> None:
|
||||
"""Clears all previous robot poses and sets robotPose at current time."""
|
||||
if type(robotPose) is Pose2d:
|
||||
robotPose = Pose3d(robotPose)
|
||||
assert type(robotPose) is Pose3d
|
||||
@@ -186,16 +263,23 @@ class VisionSystemSim:
|
||||
return self.dbgField
|
||||
|
||||
def update(self, robotPose: Pose2d | Pose3d) -> None:
|
||||
"""Periodic update. Ensure this is called repeatedly-- camera performance is used to automatically
|
||||
determine if a new frame should be submitted.
|
||||
|
||||
:param robotPoseMeters: The simulated robot pose in meters
|
||||
"""
|
||||
if type(robotPose) is Pose2d:
|
||||
robotPose = Pose3d(robotPose)
|
||||
assert type(robotPose) is Pose3d
|
||||
|
||||
# update vision targets on field
|
||||
for targetType, targets in self.targetSets.items():
|
||||
posesToAdd: list[Pose2d] = []
|
||||
for target in targets:
|
||||
posesToAdd.append(target.getPose().toPose2d())
|
||||
self.dbgField.getObject(targetType).setPoses(posesToAdd)
|
||||
|
||||
# save "real" robot poses over time
|
||||
now = wpilib.Timer.getFPGATimestamp()
|
||||
self.robotPoseBuffer.addSample(now, robotPose)
|
||||
self.dbgField.setRobotPose(robotPose.toPose2d())
|
||||
@@ -208,17 +292,22 @@ class VisionSystemSim:
|
||||
visTgtPoses2d: list[Pose2d] = []
|
||||
cameraPoses2d: list[Pose2d] = []
|
||||
processed = False
|
||||
# process each camera
|
||||
for camSim in self.camSimMap.values():
|
||||
# check if this camera is ready to process and get latency
|
||||
optTimestamp = camSim.consumeNextEntryTime()
|
||||
if optTimestamp is None:
|
||||
continue
|
||||
else:
|
||||
processed = True
|
||||
|
||||
# when this result "was" read by NT
|
||||
timestampNt = optTimestamp
|
||||
latency = camSim.prop.estLatency()
|
||||
timestampCapture = timestampNt * 1.0e-6 - latency
|
||||
# the image capture timestamp in seconds of this result
|
||||
timestampCapture = timestampNt - latency
|
||||
|
||||
# use camera pose from the image capture timestamp
|
||||
lateRobotPose = self.getRobotPose(timestampCapture)
|
||||
robotToCamera = self.getRobotToCamera(camSim, timestampCapture)
|
||||
if lateRobotPose is None or robotToCamera is None:
|
||||
@@ -226,8 +315,12 @@ class VisionSystemSim:
|
||||
lateCameraPose = lateRobotPose + robotToCamera
|
||||
cameraPoses2d.append(lateCameraPose.toPose2d())
|
||||
|
||||
# process a PhotonPipelineResult with visible targets
|
||||
camResult = camSim.process(latency, lateCameraPose, allTargets)
|
||||
camSim.submitProcessedFrame(camResult, timestampNt)
|
||||
# publish this info to NT at estimated timestamp of receive
|
||||
# needs a timestamp in microseconds
|
||||
camSim.submitProcessedFrame(camResult, timestampNt * 1.0e6)
|
||||
# display debug results
|
||||
for tgt in camResult.getTargets():
|
||||
trf = tgt.getBestCameraToTarget()
|
||||
if trf == Transform3d():
|
||||
|
||||
@@ -6,7 +6,16 @@ from ..estimation.targetModel import TargetModel
|
||||
|
||||
|
||||
class VisionTargetSim:
|
||||
"""Describes a vision target located somewhere on the field that your vision system can detect."""
|
||||
|
||||
def __init__(self, pose: Pose3d, model: TargetModel, id: int = -1):
|
||||
"""Describes a fiducial tag located somewhere on the field that your vision system can detect.
|
||||
|
||||
:param pose: Pose3d of the tag in field-relative coordinates
|
||||
:param model: TargetModel which describes the shape of the target(tag)
|
||||
:param id: The ID of this fiducial tag
|
||||
"""
|
||||
|
||||
self.pose: Pose3d = pose
|
||||
self.model: TargetModel = model
|
||||
self.fiducialId: int = id
|
||||
@@ -47,4 +56,5 @@ class VisionTargetSim:
|
||||
return self.model
|
||||
|
||||
def getFieldVertices(self) -> list[Translation3d]:
|
||||
"""This target's vertices offset from its field pose."""
|
||||
return self.model.getFieldVertices(self.pose)
|
||||
|
||||
@@ -55,6 +55,7 @@ descriptionStr = f"Pure-python implementation of PhotonLib for interfacing with
|
||||
setup(
|
||||
name="photonlibpy",
|
||||
packages=find_packages(),
|
||||
package_data={"photonlibpy": ["py.typed"]},
|
||||
version=versionString,
|
||||
install_requires=[
|
||||
"numpy~=2.1",
|
||||
@@ -63,7 +64,6 @@ setup(
|
||||
"robotpy-apriltag<2026,>=2025.0.0b1",
|
||||
"robotpy-cscore<2026,>=2025.0.0b1",
|
||||
"pyntcore<2026,>=2025.0.0b1",
|
||||
"robotpy-opencv;platform_machine=='roborio'",
|
||||
"opencv-python;platform_machine!='roborio'",
|
||||
],
|
||||
description=descriptionStr,
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import math
|
||||
|
||||
import ntcore as nt
|
||||
import pytest
|
||||
from photonlibpy.estimation import TargetModel, VisionEstimation
|
||||
from photonlibpy.photonCamera import PhotonCamera, setVersionCheckEnabled
|
||||
from photonlibpy.photonCamera import PhotonCamera
|
||||
from photonlibpy.simulation import PhotonCameraSim, VisionSystemSim, VisionTargetSim
|
||||
from robotpy_apriltag import AprilTag, AprilTagFieldLayout
|
||||
from wpimath.geometry import (
|
||||
@@ -18,12 +17,6 @@ from wpimath.geometry import (
|
||||
from wpimath.units import feetToMeters, meters
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def setupCommon() -> None:
|
||||
nt.NetworkTableInstance.getDefault().startServer()
|
||||
setVersionCheckEnabled(False)
|
||||
|
||||
|
||||
def test_VisibilityCupidShuffle() -> None:
|
||||
targetPose = Pose3d(Translation3d(15.98, 0.0, 2.0), Rotation3d(0, 0, math.pi))
|
||||
|
||||
@@ -32,6 +25,8 @@ def test_VisibilityCupidShuffle() -> None:
|
||||
cameraSim = PhotonCameraSim(camera)
|
||||
visionSysSim.addCamera(cameraSim, Transform3d())
|
||||
|
||||
# Set massive FPS so timing isn't an issue
|
||||
cameraSim.prop.setFPS(1e6)
|
||||
cameraSim.prop.setCalibrationFromFOV(640, 480, fovDiag=Rotation2d.fromDegrees(80.0))
|
||||
|
||||
visionSysSim.addVisionTargets(
|
||||
@@ -93,6 +88,8 @@ def test_NotVisibleVert1() -> None:
|
||||
cameraSim = PhotonCameraSim(camera)
|
||||
visionSysSim.addCamera(cameraSim, Transform3d())
|
||||
|
||||
# Set massive FPS so timing isn't an issue
|
||||
cameraSim.prop.setFPS(1e6)
|
||||
cameraSim.prop.setCalibrationFromFOV(640, 480, fovDiag=Rotation2d.fromDegrees(80.0))
|
||||
|
||||
visionSysSim.addVisionTargets(
|
||||
@@ -128,6 +125,8 @@ def test_NotVisibleVert2() -> None:
|
||||
cameraSim = PhotonCameraSim(camera)
|
||||
visionSysSim.addCamera(cameraSim, robotToCamera)
|
||||
|
||||
# Set massive FPS so timing isn't an issue
|
||||
cameraSim.prop.setFPS(1e6)
|
||||
cameraSim.prop.setCalibrationFromFOV(
|
||||
4774, 4774, fovDiag=Rotation2d.fromDegrees(80.0)
|
||||
)
|
||||
@@ -156,6 +155,8 @@ def test_NotVisibleTargetSize() -> None:
|
||||
cameraSim = PhotonCameraSim(camera)
|
||||
visionSysSim.addCamera(cameraSim, Transform3d())
|
||||
|
||||
# Set massive FPS so timing isn't an issue
|
||||
cameraSim.prop.setFPS(1e6)
|
||||
cameraSim.prop.setCalibrationFromFOV(640, 480, fovDiag=Rotation2d.fromDegrees(80.0))
|
||||
cameraSim.setMinTargetAreaPixels(20.0)
|
||||
visionSysSim.addVisionTargets(
|
||||
@@ -183,6 +184,8 @@ def test_NotVisibleTooFarLeds() -> None:
|
||||
cameraSim = PhotonCameraSim(camera)
|
||||
visionSysSim.addCamera(cameraSim, Transform3d())
|
||||
|
||||
# Set massive FPS so timing isn't an issue
|
||||
cameraSim.prop.setFPS(1e6)
|
||||
cameraSim.prop.setCalibrationFromFOV(640, 480, fovDiag=Rotation2d.fromDegrees(80.0))
|
||||
cameraSim.setMinTargetAreaPixels(1.0)
|
||||
cameraSim.setMaxSightRange(10.0)
|
||||
@@ -216,6 +219,9 @@ def test_YawAngles(expected_yaw) -> None:
|
||||
cameraSim = PhotonCameraSim(camera)
|
||||
|
||||
visionSysSim.addCamera(cameraSim, Transform3d())
|
||||
|
||||
# Set massive FPS so timing isn't an issue
|
||||
cameraSim.prop.setFPS(1e6)
|
||||
cameraSim.prop.setCalibrationFromFOV(640, 480, fovDiag=Rotation2d.fromDegrees(80.0))
|
||||
cameraSim.setMinTargetAreaPixels(0.0)
|
||||
visionSysSim.addVisionTargets(
|
||||
@@ -250,6 +256,9 @@ def test_PitchAngles(expected_pitch) -> None:
|
||||
camera = PhotonCamera("camera")
|
||||
cameraSim = PhotonCameraSim(camera)
|
||||
visionSysSim.addCamera(cameraSim, Transform3d())
|
||||
|
||||
# Set massive FPS so timing isn't an issue
|
||||
cameraSim.prop.setFPS(1e6)
|
||||
cameraSim.prop.setCalibrationFromFOV(
|
||||
640, 480, fovDiag=Rotation2d.fromDegrees(120.0)
|
||||
)
|
||||
@@ -316,8 +325,10 @@ def test_distanceCalc(distParam, pitchParam, heightParam) -> None:
|
||||
)
|
||||
camera = PhotonCamera("camera")
|
||||
cameraSim = PhotonCameraSim(camera)
|
||||
|
||||
visionSysSim.addCamera(cameraSim, Transform3d())
|
||||
|
||||
# Set massive FPS so timing isn't an issue
|
||||
cameraSim.prop.setFPS(1e6)
|
||||
cameraSim.prop.setCalibrationFromFOV(
|
||||
640, 480, fovDiag=Rotation2d.fromDegrees(160.0)
|
||||
)
|
||||
@@ -354,6 +365,9 @@ def test_MultipleTargets() -> None:
|
||||
camera = PhotonCamera("camera")
|
||||
cameraSim = PhotonCameraSim(camera)
|
||||
visionSysSim.addCamera(cameraSim, Transform3d())
|
||||
|
||||
# Set massive FPS so timing isn't an issue
|
||||
cameraSim.prop.setFPS(1e6)
|
||||
cameraSim.prop.setCalibrationFromFOV(640, 480, fovDiag=Rotation2d.fromDegrees(80.0))
|
||||
cameraSim.setMinTargetAreaPixels(20.0)
|
||||
|
||||
@@ -451,6 +465,9 @@ def test_PoseEstimation() -> None:
|
||||
camera = PhotonCamera("camera")
|
||||
cameraSim = PhotonCameraSim(camera)
|
||||
visionSysSim.addCamera(cameraSim, Transform3d())
|
||||
|
||||
# Set massive FPS so timing isn't an issue
|
||||
cameraSim.prop.setFPS(1e6)
|
||||
cameraSim.prop.setCalibrationFromFOV(640, 480, fovDiag=Rotation2d.fromDegrees(90.0))
|
||||
cameraSim.setMinTargetAreaPixels(20.0)
|
||||
|
||||
@@ -525,6 +542,9 @@ def test_PoseEstimationRotated() -> None:
|
||||
camera = PhotonCamera("camera")
|
||||
cameraSim = PhotonCameraSim(camera)
|
||||
visionSysSim.addCamera(cameraSim, robotToCamera)
|
||||
|
||||
# Set massive FPS so timing isn't an issue
|
||||
cameraSim.prop.setFPS(1e6)
|
||||
cameraSim.prop.setCalibrationFromFOV(640, 480, fovDiag=Rotation2d.fromDegrees(90.0))
|
||||
cameraSim.setMinTargetAreaPixels(20.0)
|
||||
|
||||
|
||||
@@ -298,17 +298,12 @@ public class PhotonCamera implements AutoCloseable {
|
||||
*/
|
||||
public VisionLEDMode getLEDMode() {
|
||||
int value = (int) ledModeState.get(-1);
|
||||
switch (value) {
|
||||
case 0:
|
||||
return VisionLEDMode.kOff;
|
||||
case 1:
|
||||
return VisionLEDMode.kOn;
|
||||
case 2:
|
||||
return VisionLEDMode.kBlink;
|
||||
case -1:
|
||||
default:
|
||||
return VisionLEDMode.kDefault;
|
||||
}
|
||||
return switch (value) {
|
||||
case 0 -> VisionLEDMode.kOff;
|
||||
case 1 -> VisionLEDMode.kOn;
|
||||
case 2 -> VisionLEDMode.kBlink;
|
||||
default -> VisionLEDMode.kDefault;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -345,46 +345,35 @@ public class PhotonPoseEstimator {
|
||||
PhotonPipelineResult cameraResult,
|
||||
Optional<Matrix<N3, N3>> cameraMatrix,
|
||||
Optional<Matrix<N8, N1>> distCoeffs,
|
||||
PoseStrategy strat) {
|
||||
Optional<EstimatedRobotPose> estimatedPose = Optional.empty();
|
||||
switch (strat) {
|
||||
case LOWEST_AMBIGUITY:
|
||||
estimatedPose = lowestAmbiguityStrategy(cameraResult);
|
||||
break;
|
||||
case CLOSEST_TO_CAMERA_HEIGHT:
|
||||
estimatedPose = closestToCameraHeightStrategy(cameraResult);
|
||||
break;
|
||||
case CLOSEST_TO_REFERENCE_POSE:
|
||||
estimatedPose = closestToReferencePoseStrategy(cameraResult, referencePose);
|
||||
break;
|
||||
case CLOSEST_TO_LAST_POSE:
|
||||
setReferencePose(lastPose);
|
||||
estimatedPose = closestToReferencePoseStrategy(cameraResult, referencePose);
|
||||
break;
|
||||
case AVERAGE_BEST_TARGETS:
|
||||
estimatedPose = averageBestTargetsStrategy(cameraResult);
|
||||
break;
|
||||
case MULTI_TAG_PNP_ON_RIO:
|
||||
if (cameraMatrix.isEmpty()) {
|
||||
DriverStation.reportWarning(
|
||||
"Camera matrix is empty for multi-tag-on-rio",
|
||||
Thread.currentThread().getStackTrace());
|
||||
} else if (distCoeffs.isEmpty()) {
|
||||
DriverStation.reportWarning(
|
||||
"Camera matrix is empty for multi-tag-on-rio",
|
||||
Thread.currentThread().getStackTrace());
|
||||
} else {
|
||||
estimatedPose = multiTagOnRioStrategy(cameraResult, cameraMatrix, distCoeffs);
|
||||
}
|
||||
break;
|
||||
case MULTI_TAG_PNP_ON_COPROCESSOR:
|
||||
estimatedPose = multiTagOnCoprocStrategy(cameraResult);
|
||||
break;
|
||||
default:
|
||||
DriverStation.reportError(
|
||||
"[PhotonPoseEstimator] Unknown Position Estimation Strategy!", false);
|
||||
return Optional.empty();
|
||||
}
|
||||
PoseStrategy strategy) {
|
||||
Optional<EstimatedRobotPose> estimatedPose =
|
||||
switch (strategy) {
|
||||
case LOWEST_AMBIGUITY -> lowestAmbiguityStrategy(cameraResult);
|
||||
case CLOSEST_TO_CAMERA_HEIGHT -> closestToCameraHeightStrategy(cameraResult);
|
||||
case CLOSEST_TO_REFERENCE_POSE ->
|
||||
closestToReferencePoseStrategy(cameraResult, referencePose);
|
||||
case CLOSEST_TO_LAST_POSE -> {
|
||||
setReferencePose(lastPose);
|
||||
yield closestToReferencePoseStrategy(cameraResult, referencePose);
|
||||
}
|
||||
case AVERAGE_BEST_TARGETS -> averageBestTargetsStrategy(cameraResult);
|
||||
case MULTI_TAG_PNP_ON_RIO -> {
|
||||
if (cameraMatrix.isEmpty()) {
|
||||
DriverStation.reportWarning(
|
||||
"Camera matrix is empty for multi-tag-on-rio",
|
||||
Thread.currentThread().getStackTrace());
|
||||
yield Optional.empty();
|
||||
} else if (distCoeffs.isEmpty()) {
|
||||
DriverStation.reportWarning(
|
||||
"Camera matrix is empty for multi-tag-on-rio",
|
||||
Thread.currentThread().getStackTrace());
|
||||
yield Optional.empty();
|
||||
} else {
|
||||
yield multiTagOnRioStrategy(cameraResult, cameraMatrix, distCoeffs);
|
||||
}
|
||||
}
|
||||
case MULTI_TAG_PNP_ON_COPROCESSOR -> multiTagOnCoprocStrategy(cameraResult);
|
||||
};
|
||||
|
||||
if (estimatedPose.isPresent()) {
|
||||
lastPose = estimatedPose.get().estimatedPose;
|
||||
|
||||
@@ -84,11 +84,9 @@ public class VisionTargetSim {
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (this == obj) return true;
|
||||
if (obj instanceof VisionTargetSim) {
|
||||
var o = (VisionTargetSim) obj;
|
||||
return pose.equals(o.pose) && model.equals(o.model);
|
||||
}
|
||||
return false;
|
||||
return this == obj
|
||||
&& obj instanceof VisionTargetSim o
|
||||
&& pose.equals(o.pose)
|
||||
&& model.equals(o.model);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -305,6 +305,7 @@ void PhotonCamera::VerifyVersion() {
|
||||
FRC_ReportError(frc::warn::Warning,
|
||||
"Cannot find property message_uuid for PhotonCamera {}",
|
||||
path);
|
||||
return;
|
||||
}
|
||||
std::string remote_uuid{remote_uuid_json};
|
||||
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
package org.photonvision;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
|
||||
import static org.junit.jupiter.api.Assumptions.assumeTrue;
|
||||
import static org.photonvision.UnitTestUtils.waitForCondition;
|
||||
import static org.photonvision.UnitTestUtils.waitForSequenceNumber;
|
||||
|
||||
@@ -54,6 +55,9 @@ class PhotonCameraTest {
|
||||
@BeforeAll
|
||||
public static void load_wpilib() {
|
||||
WpilibLoader.loadLibraries();
|
||||
|
||||
// See #1574 - test flakey, disabled until we address this
|
||||
assumeTrue(false);
|
||||
}
|
||||
|
||||
@BeforeEach
|
||||
|
||||
@@ -28,6 +28,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.junit.jupiter.api.Assertions.fail;
|
||||
import static org.junit.jupiter.api.Assumptions.assumeTrue;
|
||||
import static org.photonvision.UnitTestUtils.waitForSequenceNumber;
|
||||
|
||||
import edu.wpi.first.apriltag.AprilTag;
|
||||
@@ -66,29 +67,27 @@ import org.photonvision.simulation.VisionSystemSim;
|
||||
import org.photonvision.simulation.VisionTargetSim;
|
||||
import org.photonvision.targeting.PhotonTrackedTarget;
|
||||
|
||||
// See #1574 - flakey on windows and also linux, so commenting out until we bump wpilib
|
||||
class VisionSystemSimTest {
|
||||
private static final double kTrlDelta = 0.005;
|
||||
private static final double kRotDeltaDeg = 0.25;
|
||||
|
||||
NetworkTableInstance inst;
|
||||
|
||||
@BeforeAll
|
||||
public static void setUp() {
|
||||
WpilibLoader.loadLibraries();
|
||||
assertTrue(WpilibLoader.loadLibraries());
|
||||
|
||||
try {
|
||||
if (!PhotonTargetingJniLoader.load()) fail();
|
||||
assertTrue(PhotonTargetingJniLoader.load());
|
||||
} catch (UnsatisfiedLinkError | IOException e) {
|
||||
e.printStackTrace();
|
||||
fail(e);
|
||||
}
|
||||
|
||||
OpenCVHelp.forceLoadOpenCV();
|
||||
}
|
||||
|
||||
@BeforeEach
|
||||
public void init() {
|
||||
// // No version check for testing
|
||||
// PhotonCamera.setVersionCheckEnabled(false);
|
||||
// See #1574 - test flakey, disabled until we address this
|
||||
assumeTrue(false);
|
||||
}
|
||||
|
||||
@BeforeEach
|
||||
|
||||
@@ -6,6 +6,8 @@ bool:
|
||||
cpp_type: bool
|
||||
java_decode_method: decodeBoolean
|
||||
java_encode_shim: encodeBoolean
|
||||
python_decode_shim: decodeBoolean
|
||||
python_encode_shim: encodeBoolean
|
||||
int16:
|
||||
len: 2
|
||||
java_type: short
|
||||
@@ -13,27 +15,37 @@ int16:
|
||||
java_decode_method: decodeShort
|
||||
java_list_decode_method: decodeShortList
|
||||
java_encode_shim: encodeShort
|
||||
python_decode_shim: decodeShort
|
||||
python_encode_shim: encodeShort
|
||||
int32:
|
||||
len: 4
|
||||
java_type: int
|
||||
cpp_type: int32_t
|
||||
java_decode_method: decodeInt
|
||||
java_encode_shim: encodeInt
|
||||
python_decode_shim: decodeInt
|
||||
python_encode_shim: encodeInt
|
||||
int64:
|
||||
len: 8
|
||||
java_type: long
|
||||
cpp_type: int64_t
|
||||
java_decode_method: decodeLong
|
||||
java_encode_shim: encodeLong
|
||||
python_decode_shim: decodeLong
|
||||
python_encode_shim: encodeLong
|
||||
float32:
|
||||
len: 4
|
||||
java_type: float
|
||||
cpp_type: float
|
||||
java_decode_method: decodeFloat
|
||||
java_encode_shim: encodeFloat
|
||||
python_decode_shim: decodeFloat
|
||||
python_encode_shim: encodeFloat
|
||||
float64:
|
||||
len: 8
|
||||
java_type: double
|
||||
cpp_type: double
|
||||
java_decode_method: decodeDouble
|
||||
java_encode_shim: encodeDouble
|
||||
python_decode_shim: decodeDouble
|
||||
python_encode_shim: encodeDouble
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
java_encode_shim: PacketUtils.packTransform3d
|
||||
cpp_type: frc::Transform3d
|
||||
cpp_include: "<frc/geometry/Transform3d.h>"
|
||||
python_decode_shim: packet.decodeTransform
|
||||
python_decode_shim: decodeTransform
|
||||
python_encode_shim: encodeTransform
|
||||
java_import: edu.wpi.first.math.geometry.Transform3d
|
||||
# shim since we expect fields to at least exist
|
||||
|
||||
@@ -54,26 +54,26 @@ public class {{ name }}Serde implements PacketSerde<{{name}}> {
|
||||
@Override
|
||||
public void pack(Packet packet, {{ name }} value) {
|
||||
{%- for field in fields -%}
|
||||
{%- if field.type | is_shimmed %}
|
||||
{%- if field.type | is_shimmed %}
|
||||
{{ get_message_by_name(field.type).java_encode_shim }}(packet, value.{{ field.name }});
|
||||
{%- elif field.optional == True %}
|
||||
{%- elif field.optional == True %}
|
||||
// {{ field.name }} is optional! it better not be a VLA too
|
||||
packet.encodeOptional(value.{{ field.name }});
|
||||
{%- elif field.vla == True and field.type | is_intrinsic %}
|
||||
{%- elif field.vla == True and field.type | is_intrinsic %}
|
||||
// {{ field.name }} is a intrinsic VLA!
|
||||
packet.encode(value.{{ field.name }});
|
||||
{%- elif field.vla == True %}
|
||||
{%- elif field.vla == True %}
|
||||
// {{ field.name }} is a custom VLA!
|
||||
packet.encodeList(value.{{ field.name }});
|
||||
{%- elif field.type | is_intrinsic %}
|
||||
{%- elif field.type | is_intrinsic %}
|
||||
// field {{ field.name }} is of intrinsic type {{ field.type }}
|
||||
packet.encode(({{ type_map[field.type].java_type }}) value.{{ field.name }});
|
||||
{%- else %}
|
||||
{%- else %}
|
||||
// field {{ field.name }} is of non-intrinsic type {{ field.type }}
|
||||
{{ field.type }}.photonStruct.pack(packet, value.{{ field.name }});
|
||||
{%- endif %}
|
||||
{%- if not loop.last %}
|
||||
{% endif -%}
|
||||
{%- endif %}
|
||||
{%- if not loop.last %}
|
||||
{% endif -%}
|
||||
{% endfor%}
|
||||
}
|
||||
|
||||
@@ -81,26 +81,26 @@ public class {{ name }}Serde implements PacketSerde<{{name}}> {
|
||||
public {{ name }} unpack(Packet packet) {
|
||||
var ret = new {{ name }}();
|
||||
{% for field in fields -%}
|
||||
{%- if field.type | is_shimmed %}
|
||||
{%- if field.type | is_shimmed %}
|
||||
ret.{{ field.name }} = {{ get_message_by_name(field.type).java_decode_shim }}(packet);
|
||||
{%- elif field.optional == True %}
|
||||
{%- elif field.optional == True %}
|
||||
// {{ field.name }} is optional! it better not be a VLA too
|
||||
ret.{{ field.name }} = packet.decodeOptional({{ field.type }}.photonStruct);
|
||||
{%- elif field.vla == True and not field.type | is_intrinsic %}
|
||||
{%- elif field.vla == True and not field.type | is_intrinsic %}
|
||||
// {{ field.name }} is a custom VLA!
|
||||
ret.{{ field.name }} = packet.decodeList({{ field.type }}.photonStruct);
|
||||
{%- elif field.vla == True and field.type | is_intrinsic %}
|
||||
{%- elif field.vla == True and field.type | is_intrinsic %}
|
||||
// {{ field.name }} is a custom VLA!
|
||||
ret.{{ field.name }} = packet.decode{{ type_map[field.type].java_type.title() }}List();
|
||||
{%- elif field.type | is_intrinsic %}
|
||||
{%- elif field.type | is_intrinsic %}
|
||||
// {{ field.name }} is of intrinsic type {{ field.type }}
|
||||
ret.{{field.name}} = packet.{{ type_map[field.type].java_decode_method }}();
|
||||
{%- else %}
|
||||
{%- else %}
|
||||
// {{ field.name }} is of non-intrinsic type {{ field.type }}
|
||||
ret.{{field.name}} = {{ field.type }}.photonStruct.unpack(packet);
|
||||
{%- endif %}
|
||||
{%- if not loop.last %}
|
||||
{% endif -%}
|
||||
{%- endif %}
|
||||
{%- if not loop.last %}
|
||||
{% endif -%}
|
||||
{% endfor%}
|
||||
|
||||
return ret;
|
||||
@@ -125,4 +125,4 @@ public class {{ name }}Serde implements PacketSerde<{{name}}> {
|
||||
{%- endfor%}
|
||||
};
|
||||
}
|
||||
}
|
||||
}{{'\n'}}
|
||||
|
||||
@@ -24,21 +24,21 @@ namespace photon {
|
||||
using StructType = SerdeType<{{ name }}>;
|
||||
|
||||
void StructType::Pack(Packet& packet, const {{ name }}& value) {
|
||||
{% for field in fields -%}
|
||||
packet.Pack<{{ field | get_qualified_name }}>(value.{{ field.name }});
|
||||
{%- if not loop.last %}
|
||||
{% endif -%}
|
||||
{% endfor %}
|
||||
{% for field in fields -%}
|
||||
packet.Pack<{{ field | get_qualified_name }}>(value.{{ field.name }});
|
||||
{%- if not loop.last %}
|
||||
{% endif -%}
|
||||
{% endfor %}
|
||||
}
|
||||
|
||||
{{ name }} StructType::Unpack(Packet& packet) {
|
||||
return {{ name }}{ {{ name }}_PhotonStruct{
|
||||
{% for field in fields -%}
|
||||
.{{ field.name}} = packet.Unpack<{{ field | get_qualified_name }}>(),
|
||||
{%- if not loop.last %}
|
||||
{% endif -%}
|
||||
{% endfor %}
|
||||
}};
|
||||
return {{ name }}{ {{ name }}_PhotonStruct{
|
||||
{% for field in fields -%}
|
||||
.{{ field.name}} = packet.Unpack<{{ field | get_qualified_name }}>(),
|
||||
{%- if not loop.last %}
|
||||
{% endif -%}
|
||||
{% endfor %}
|
||||
}};
|
||||
}
|
||||
|
||||
} // namespace photon
|
||||
} // namespace photon{{'\n'}}
|
||||
|
||||
@@ -48,4 +48,4 @@ struct WPILIB_DLLEXPORT SerdeType<{{ name }}> {
|
||||
|
||||
static_assert(photon::PhotonStructSerializable<photon::{{ name }}>);
|
||||
|
||||
} // namespace photon
|
||||
} // namespace photon{{'\n'}}
|
||||
|
||||
@@ -44,7 +44,7 @@ class {{ name }}Serde:
|
||||
MESSAGE_FORMAT = "{{ message_fmt }}"
|
||||
|
||||
@staticmethod
|
||||
def pack(value: '{{ name }}' ) -> 'Packet':
|
||||
def pack(value: "{{ name }}") -> "Packet":
|
||||
ret = Packet()
|
||||
{% for field in fields -%}
|
||||
{%- if field.type | is_shimmed %}
|
||||
@@ -60,7 +60,7 @@ class {{ name }}Serde:
|
||||
ret.encode{{ type_map[field.type].java_type.title() }}List(value.{{ field.name }})
|
||||
{%- elif field.type | is_intrinsic %}
|
||||
# {{ field.name }} is of intrinsic type {{ field.type }}
|
||||
ret.{{ type_map[field.type].java_encode_shim }}(value.{{field.name}})
|
||||
ret.{{ type_map[field.type].python_encode_shim }}(value.{{field.name}})
|
||||
{%- else %}
|
||||
# {{ field.name }} is of non-intrinsic type {{ field.type }}
|
||||
ret.encodeBytes({{ field.type }}.photonStruct.pack(value.{{field.name}}).getData())
|
||||
@@ -70,13 +70,12 @@ class {{ name }}Serde:
|
||||
{% endfor%}
|
||||
return ret
|
||||
|
||||
|
||||
@staticmethod
|
||||
def unpack(packet: 'Packet') -> '{{ name }}':
|
||||
def unpack(packet: "Packet") -> "{{ name }}":
|
||||
ret = {{ name }}()
|
||||
{% for field in fields -%}
|
||||
{%- if field.type | is_shimmed %}
|
||||
ret.{{ field.name }} = {{ get_message_by_name(field.type).python_decode_shim }}()
|
||||
ret.{{ field.name }} = packet.{{ get_message_by_name(field.type).python_decode_shim }}()
|
||||
{%- elif field.optional == True %}
|
||||
# {{ field.name }} is optional! it better not be a VLA too
|
||||
ret.{{ field.name }} = packet.decodeOptional({{ field.type }}.photonStruct)
|
||||
@@ -88,7 +87,7 @@ class {{ name }}Serde:
|
||||
ret.{{ field.name }} = packet.decode{{ type_map[field.type].java_type.title() }}List()
|
||||
{%- elif field.type | is_intrinsic %}
|
||||
# {{ field.name }} is of intrinsic type {{ field.type }}
|
||||
ret.{{field.name}} = packet.{{ type_map[field.type].java_decode_method }}()
|
||||
ret.{{field.name}} = packet.{{ type_map[field.type].python_decode_shim }}()
|
||||
{%- else %}
|
||||
# {{ field.name }} is of non-intrinsic type {{ field.type }}
|
||||
ret.{{field.name}} = {{ field.type }}.photonStruct.unpack(packet)
|
||||
@@ -101,4 +100,4 @@ class {{ name }}Serde:
|
||||
|
||||
|
||||
# Hack ourselves into the base class
|
||||
{{ name }}.photonStruct = {{ name }}Serde()
|
||||
{{ name }}.photonStruct = {{ name }}Serde(){{'\n'}}
|
||||
|
||||
@@ -36,4 +36,4 @@ struct {{ name }}_PhotonStruct {
|
||||
friend bool operator==({{ name }}_PhotonStruct const&, {{ name }}_PhotonStruct const&) = default;
|
||||
};
|
||||
|
||||
} // namespace photon
|
||||
} // namespace photon{{'\n'}}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
plugins {
|
||||
id "application"
|
||||
id 'com.github.johnrengelman.shadow' version '8.1.1'
|
||||
id 'com.gradleup.shadow' version '8.3.4'
|
||||
id "com.github.node-gradle.node" version "7.0.1"
|
||||
id "org.hidetake.ssh" version "2.11.2"
|
||||
id 'edu.wpi.first.WpilibTools' version '1.3.0'
|
||||
|
||||
@@ -31,6 +31,7 @@ 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.OsImageVersion;
|
||||
import org.photonvision.common.hardware.PiVersion;
|
||||
import org.photonvision.common.hardware.Platform;
|
||||
import org.photonvision.common.logging.KernelLogLogger;
|
||||
@@ -353,10 +354,14 @@ public class Main {
|
||||
logger.info(
|
||||
"Starting PhotonVision version "
|
||||
+ PhotonVersion.versionString
|
||||
+ " on "
|
||||
+ " on platform "
|
||||
+ Platform.getPlatformName()
|
||||
+ (Platform.isRaspberryPi() ? (" (Pi " + PiVersion.getPiVersion() + ")") : ""));
|
||||
|
||||
if (OsImageVersion.IMAGE_VERSION.isPresent()) {
|
||||
logger.info("PhotonVision image version: " + OsImageVersion.IMAGE_VERSION.get());
|
||||
}
|
||||
|
||||
try {
|
||||
if (!handleArgs(args)) {
|
||||
System.exit(1);
|
||||
|
||||
@@ -99,10 +99,9 @@ public class DataSocketHandler {
|
||||
objectMapper.readValue(context.data(), new TypeReference<>() {});
|
||||
|
||||
// Special case the current camera index
|
||||
var camIndexValue = deserializedData.get("cameraIndex");
|
||||
Integer cameraIndex = null;
|
||||
if (camIndexValue instanceof Integer) {
|
||||
cameraIndex = (Integer) camIndexValue;
|
||||
if (deserializedData.get("cameraIndex") instanceof Integer camIndexValue) {
|
||||
cameraIndex = camIndexValue;
|
||||
deserializedData.remove("cameraIndex");
|
||||
}
|
||||
|
||||
@@ -128,216 +127,182 @@ public class DataSocketHandler {
|
||||
}
|
||||
|
||||
switch (socketMessageType) {
|
||||
case SMT_DRIVERMODE:
|
||||
{
|
||||
// TODO: what is this event?
|
||||
var data = (Boolean) entryValue;
|
||||
var dmIsDriverEvent =
|
||||
new IncomingWebSocketEvent<Boolean>(
|
||||
DataChangeDestination.DCD_ACTIVEMODULE,
|
||||
"isDriverMode",
|
||||
data,
|
||||
cameraIndex,
|
||||
context);
|
||||
case SMT_DRIVERMODE -> {
|
||||
// TODO: what is this event?
|
||||
var data = (Boolean) entryValue;
|
||||
var dmIsDriverEvent =
|
||||
new IncomingWebSocketEvent<Boolean>(
|
||||
DataChangeDestination.DCD_ACTIVEMODULE,
|
||||
"isDriverMode",
|
||||
data,
|
||||
cameraIndex,
|
||||
context);
|
||||
|
||||
dcService.publishEvents(dmIsDriverEvent);
|
||||
break;
|
||||
}
|
||||
case SMT_CHANGECAMERANAME:
|
||||
{
|
||||
var ccnEvent =
|
||||
new IncomingWebSocketEvent<>(
|
||||
DataChangeDestination.DCD_ACTIVEMODULE,
|
||||
"cameraNickname",
|
||||
(String) entryValue,
|
||||
cameraIndex,
|
||||
context);
|
||||
dcService.publishEvent(ccnEvent);
|
||||
break;
|
||||
}
|
||||
case SMT_CHANGEPIPELINENAME:
|
||||
{
|
||||
var cpnEvent =
|
||||
new IncomingWebSocketEvent<>(
|
||||
DataChangeDestination.DCD_ACTIVEMODULE,
|
||||
"pipelineName",
|
||||
(String) entryValue,
|
||||
cameraIndex,
|
||||
context);
|
||||
dcService.publishEvent(cpnEvent);
|
||||
break;
|
||||
}
|
||||
case SMT_ADDNEWPIPELINE:
|
||||
{
|
||||
// HashMap<String, Object> data = (HashMap<String,
|
||||
// Object>) entryValue;
|
||||
// var type = (PipelineType)
|
||||
// data.get("pipelineType");
|
||||
// var name = (String) data.get("pipelineName");
|
||||
var arr = (ArrayList<Object>) entryValue;
|
||||
var name = (String) arr.get(0);
|
||||
var type = PipelineType.values()[(Integer) arr.get(1) + 2];
|
||||
dcService.publishEvents(dmIsDriverEvent);
|
||||
}
|
||||
case SMT_CHANGECAMERANAME -> {
|
||||
var ccnEvent =
|
||||
new IncomingWebSocketEvent<>(
|
||||
DataChangeDestination.DCD_ACTIVEMODULE,
|
||||
"cameraNickname",
|
||||
(String) entryValue,
|
||||
cameraIndex,
|
||||
context);
|
||||
dcService.publishEvent(ccnEvent);
|
||||
}
|
||||
case SMT_CHANGEPIPELINENAME -> {
|
||||
var cpnEvent =
|
||||
new IncomingWebSocketEvent<>(
|
||||
DataChangeDestination.DCD_ACTIVEMODULE,
|
||||
"pipelineName",
|
||||
(String) entryValue,
|
||||
cameraIndex,
|
||||
context);
|
||||
dcService.publishEvent(cpnEvent);
|
||||
}
|
||||
case SMT_ADDNEWPIPELINE -> {
|
||||
// HashMap<String, Object> data = (HashMap<String, Object>) entryValue;
|
||||
// var type = (PipelineType) data.get("pipelineType");
|
||||
// var name = (String) data.get("pipelineName");
|
||||
var arr = (ArrayList<Object>) entryValue;
|
||||
var name = (String) arr.get(0);
|
||||
var type = PipelineType.values()[(Integer) arr.get(1) + 2];
|
||||
|
||||
var newPipelineEvent =
|
||||
new IncomingWebSocketEvent<>(
|
||||
DataChangeDestination.DCD_ACTIVEMODULE,
|
||||
"newPipelineInfo",
|
||||
Pair.of(name, type),
|
||||
cameraIndex,
|
||||
context);
|
||||
dcService.publishEvent(newPipelineEvent);
|
||||
break;
|
||||
}
|
||||
case SMT_CHANGEBRIGHTNESS:
|
||||
{
|
||||
HardwareManager.getInstance()
|
||||
.setBrightnessPercent(Integer.parseInt(entryValue.toString()));
|
||||
break;
|
||||
}
|
||||
case SMT_DUPLICATEPIPELINE:
|
||||
{
|
||||
var pipeIndex = (Integer) entryValue;
|
||||
var newPipelineEvent =
|
||||
new IncomingWebSocketEvent<>(
|
||||
DataChangeDestination.DCD_ACTIVEMODULE,
|
||||
"newPipelineInfo",
|
||||
Pair.of(name, type),
|
||||
cameraIndex,
|
||||
context);
|
||||
dcService.publishEvent(newPipelineEvent);
|
||||
}
|
||||
case SMT_CHANGEBRIGHTNESS -> {
|
||||
HardwareManager.getInstance()
|
||||
.setBrightnessPercent(Integer.parseInt(entryValue.toString()));
|
||||
}
|
||||
case SMT_DUPLICATEPIPELINE -> {
|
||||
var pipeIndex = (Integer) entryValue;
|
||||
|
||||
logger.info("Duplicating pipe@index" + pipeIndex + " for camera " + cameraIndex);
|
||||
logger.info("Duplicating pipe@index" + pipeIndex + " for camera " + cameraIndex);
|
||||
|
||||
var newPipelineEvent =
|
||||
new IncomingWebSocketEvent<>(
|
||||
DataChangeDestination.DCD_ACTIVEMODULE,
|
||||
"duplicatePipeline",
|
||||
pipeIndex,
|
||||
cameraIndex,
|
||||
context);
|
||||
dcService.publishEvent(newPipelineEvent);
|
||||
break;
|
||||
}
|
||||
case SMT_DELETECURRENTPIPELINE:
|
||||
{
|
||||
var deleteCurrentPipelineEvent =
|
||||
new IncomingWebSocketEvent<>(
|
||||
DataChangeDestination.DCD_ACTIVEMODULE,
|
||||
"deleteCurrPipeline",
|
||||
0,
|
||||
cameraIndex,
|
||||
context);
|
||||
dcService.publishEvent(deleteCurrentPipelineEvent);
|
||||
break;
|
||||
}
|
||||
case SMT_ROBOTOFFSETPOINT:
|
||||
{
|
||||
var robotOffsetPointEvent =
|
||||
new IncomingWebSocketEvent<>(
|
||||
DataChangeDestination.DCD_ACTIVEMODULE,
|
||||
"robotOffsetPoint",
|
||||
(Integer) entryValue,
|
||||
cameraIndex,
|
||||
null);
|
||||
dcService.publishEvent(robotOffsetPointEvent);
|
||||
break;
|
||||
}
|
||||
case SMT_CURRENTCAMERA:
|
||||
{
|
||||
var changeCurrentCameraEvent =
|
||||
new IncomingWebSocketEvent<>(
|
||||
DataChangeDestination.DCD_OTHER, "changeUICamera", (Integer) entryValue);
|
||||
dcService.publishEvent(changeCurrentCameraEvent);
|
||||
break;
|
||||
}
|
||||
case SMT_CURRENTPIPELINE:
|
||||
{
|
||||
var changePipelineEvent =
|
||||
new IncomingWebSocketEvent<>(
|
||||
DataChangeDestination.DCD_ACTIVEMODULE,
|
||||
"changePipeline",
|
||||
(Integer) entryValue,
|
||||
cameraIndex,
|
||||
context);
|
||||
dcService.publishEvent(changePipelineEvent);
|
||||
break;
|
||||
}
|
||||
case SMT_STARTPNPCALIBRATION:
|
||||
{
|
||||
var changePipelineEvent =
|
||||
new IncomingWebSocketEvent<>(
|
||||
DataChangeDestination.DCD_ACTIVEMODULE,
|
||||
"startCalibration",
|
||||
(Map) entryValue,
|
||||
cameraIndex,
|
||||
context);
|
||||
dcService.publishEvent(changePipelineEvent);
|
||||
break;
|
||||
}
|
||||
case SMT_SAVEINPUTSNAPSHOT:
|
||||
{
|
||||
var takeInputSnapshotEvent =
|
||||
new IncomingWebSocketEvent<>(
|
||||
DataChangeDestination.DCD_ACTIVEMODULE,
|
||||
"saveInputSnapshot",
|
||||
0,
|
||||
cameraIndex,
|
||||
context);
|
||||
dcService.publishEvent(takeInputSnapshotEvent);
|
||||
break;
|
||||
}
|
||||
case SMT_SAVEOUTPUTSNAPSHOT:
|
||||
{
|
||||
var takeOutputSnapshotEvent =
|
||||
new IncomingWebSocketEvent<>(
|
||||
DataChangeDestination.DCD_ACTIVEMODULE,
|
||||
"saveOutputSnapshot",
|
||||
0,
|
||||
cameraIndex,
|
||||
context);
|
||||
dcService.publishEvent(takeOutputSnapshotEvent);
|
||||
break;
|
||||
}
|
||||
case SMT_TAKECALIBRATIONSNAPSHOT:
|
||||
{
|
||||
var takeCalSnapshotEvent =
|
||||
new IncomingWebSocketEvent<>(
|
||||
DataChangeDestination.DCD_ACTIVEMODULE,
|
||||
"takeCalSnapshot",
|
||||
0,
|
||||
cameraIndex,
|
||||
context);
|
||||
dcService.publishEvent(takeCalSnapshotEvent);
|
||||
break;
|
||||
}
|
||||
case SMT_PIPELINESETTINGCHANGE:
|
||||
{
|
||||
HashMap<String, Object> data = (HashMap<String, Object>) entryValue;
|
||||
var newPipelineEvent =
|
||||
new IncomingWebSocketEvent<>(
|
||||
DataChangeDestination.DCD_ACTIVEMODULE,
|
||||
"duplicatePipeline",
|
||||
pipeIndex,
|
||||
cameraIndex,
|
||||
context);
|
||||
dcService.publishEvent(newPipelineEvent);
|
||||
}
|
||||
case SMT_DELETECURRENTPIPELINE -> {
|
||||
var deleteCurrentPipelineEvent =
|
||||
new IncomingWebSocketEvent<>(
|
||||
DataChangeDestination.DCD_ACTIVEMODULE,
|
||||
"deleteCurrPipeline",
|
||||
0,
|
||||
cameraIndex,
|
||||
context);
|
||||
dcService.publishEvent(deleteCurrentPipelineEvent);
|
||||
}
|
||||
case SMT_ROBOTOFFSETPOINT -> {
|
||||
var robotOffsetPointEvent =
|
||||
new IncomingWebSocketEvent<>(
|
||||
DataChangeDestination.DCD_ACTIVEMODULE,
|
||||
"robotOffsetPoint",
|
||||
(Integer) entryValue,
|
||||
cameraIndex,
|
||||
null);
|
||||
dcService.publishEvent(robotOffsetPointEvent);
|
||||
}
|
||||
case SMT_CURRENTCAMERA -> {
|
||||
var changeCurrentCameraEvent =
|
||||
new IncomingWebSocketEvent<>(
|
||||
DataChangeDestination.DCD_OTHER, "changeUICamera", (Integer) entryValue);
|
||||
dcService.publishEvent(changeCurrentCameraEvent);
|
||||
}
|
||||
case SMT_CURRENTPIPELINE -> {
|
||||
var changePipelineEvent =
|
||||
new IncomingWebSocketEvent<>(
|
||||
DataChangeDestination.DCD_ACTIVEMODULE,
|
||||
"changePipeline",
|
||||
(Integer) entryValue,
|
||||
cameraIndex,
|
||||
context);
|
||||
dcService.publishEvent(changePipelineEvent);
|
||||
}
|
||||
case SMT_STARTPNPCALIBRATION -> {
|
||||
var changePipelineEvent =
|
||||
new IncomingWebSocketEvent<>(
|
||||
DataChangeDestination.DCD_ACTIVEMODULE,
|
||||
"startCalibration",
|
||||
(Map) entryValue,
|
||||
cameraIndex,
|
||||
context);
|
||||
dcService.publishEvent(changePipelineEvent);
|
||||
}
|
||||
case SMT_SAVEINPUTSNAPSHOT -> {
|
||||
var takeInputSnapshotEvent =
|
||||
new IncomingWebSocketEvent<>(
|
||||
DataChangeDestination.DCD_ACTIVEMODULE,
|
||||
"saveInputSnapshot",
|
||||
0,
|
||||
cameraIndex,
|
||||
context);
|
||||
dcService.publishEvent(takeInputSnapshotEvent);
|
||||
}
|
||||
case SMT_SAVEOUTPUTSNAPSHOT -> {
|
||||
var takeOutputSnapshotEvent =
|
||||
new IncomingWebSocketEvent<>(
|
||||
DataChangeDestination.DCD_ACTIVEMODULE,
|
||||
"saveOutputSnapshot",
|
||||
0,
|
||||
cameraIndex,
|
||||
context);
|
||||
dcService.publishEvent(takeOutputSnapshotEvent);
|
||||
}
|
||||
case SMT_TAKECALIBRATIONSNAPSHOT -> {
|
||||
var takeCalSnapshotEvent =
|
||||
new IncomingWebSocketEvent<>(
|
||||
DataChangeDestination.DCD_ACTIVEMODULE,
|
||||
"takeCalSnapshot",
|
||||
0,
|
||||
cameraIndex,
|
||||
context);
|
||||
dcService.publishEvent(takeCalSnapshotEvent);
|
||||
}
|
||||
case SMT_PIPELINESETTINGCHANGE -> {
|
||||
HashMap<String, Object> data = (HashMap<String, Object>) entryValue;
|
||||
|
||||
if (data.size() >= 2) {
|
||||
var cameraIndex2 = (int) data.get("cameraIndex");
|
||||
for (var dataEntry : data.entrySet()) {
|
||||
if (dataEntry.getKey().equals("cameraIndex")) {
|
||||
continue;
|
||||
}
|
||||
var pipelineSettingChangeEvent =
|
||||
new IncomingWebSocketEvent(
|
||||
DataChangeDestination.DCD_ACTIVEPIPELINESETTINGS,
|
||||
dataEntry.getKey(),
|
||||
dataEntry.getValue(),
|
||||
cameraIndex2,
|
||||
context);
|
||||
dcService.publishEvent(pipelineSettingChangeEvent);
|
||||
if (data.size() >= 2) {
|
||||
var cameraIndex2 = (int) data.get("cameraIndex");
|
||||
for (var dataEntry : data.entrySet()) {
|
||||
if (dataEntry.getKey().equals("cameraIndex")) {
|
||||
continue;
|
||||
}
|
||||
} else {
|
||||
logger.warn("Unknown message for PSC: " + data.keySet().iterator().next());
|
||||
var pipelineSettingChangeEvent =
|
||||
new IncomingWebSocketEvent(
|
||||
DataChangeDestination.DCD_ACTIVEPIPELINESETTINGS,
|
||||
dataEntry.getKey(),
|
||||
dataEntry.getValue(),
|
||||
cameraIndex2,
|
||||
context);
|
||||
dcService.publishEvent(pipelineSettingChangeEvent);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case SMT_CHANGEPIPELINETYPE:
|
||||
{
|
||||
var changePipelineEvent =
|
||||
new IncomingWebSocketEvent<>(
|
||||
DataChangeDestination.DCD_ACTIVEMODULE,
|
||||
"changePipelineType",
|
||||
(Integer) entryValue,
|
||||
cameraIndex,
|
||||
context);
|
||||
dcService.publishEvent(changePipelineEvent);
|
||||
break;
|
||||
} else {
|
||||
logger.warn("Unknown message for PSC: " + data.keySet().iterator().next());
|
||||
}
|
||||
}
|
||||
case SMT_CHANGEPIPELINETYPE -> {
|
||||
var changePipelineEvent =
|
||||
new IncomingWebSocketEvent<>(
|
||||
DataChangeDestination.DCD_ACTIVEMODULE,
|
||||
"changePipelineType",
|
||||
(Integer) entryValue,
|
||||
cameraIndex,
|
||||
context);
|
||||
dcService.publishEvent(changePipelineEvent);
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
logger.error("Failed to parse message!", e);
|
||||
|
||||
@@ -27,6 +27,7 @@ import org.photonvision.common.dataflow.events.DataChangeEvent;
|
||||
import org.photonvision.common.dataflow.events.IncomingWebSocketEvent;
|
||||
import org.photonvision.common.dataflow.events.OutgoingUIEvent;
|
||||
import org.photonvision.common.dataflow.networktables.NetworkTablesManager;
|
||||
import org.photonvision.common.dataflow.websocket.UIPhotonConfiguration;
|
||||
import org.photonvision.common.logging.Logger;
|
||||
|
||||
public class UIInboundSubscriber extends DataChangeSubscriber {
|
||||
@@ -38,12 +39,12 @@ public class UIInboundSubscriber extends DataChangeSubscriber {
|
||||
|
||||
@Override
|
||||
public void onDataChangeEvent(DataChangeEvent<?> event) {
|
||||
if (event instanceof IncomingWebSocketEvent) {
|
||||
var incomingWSEvent = (IncomingWebSocketEvent<?>) event;
|
||||
if (event instanceof IncomingWebSocketEvent incomingWSEvent) {
|
||||
if (incomingWSEvent.propertyName.equals("userConnected")
|
||||
|| incomingWSEvent.propertyName.equals("sendFullSettings")) {
|
||||
// Send full settings
|
||||
var settings = ConfigManager.getInstance().getConfig().toHashMap();
|
||||
var settings =
|
||||
UIPhotonConfiguration.programStateToUi(ConfigManager.getInstance().getConfig());
|
||||
var message =
|
||||
new OutgoingUIEvent<>("fullsettings", settings, incomingWSEvent.originContext);
|
||||
DataChangeService.getInstance().publishEvent(message);
|
||||
|
||||
@@ -44,11 +44,9 @@ class UIOutboundSubscriber extends DataChangeSubscriber {
|
||||
|
||||
@Override
|
||||
public void onDataChangeEvent(DataChangeEvent event) {
|
||||
if (event instanceof OutgoingUIEvent) {
|
||||
var thisEvent = (OutgoingUIEvent) event;
|
||||
if (event instanceof OutgoingUIEvent thisEvent) {
|
||||
try {
|
||||
if (event.data instanceof HashMap) {
|
||||
var data = (HashMap) event.data;
|
||||
if (event.data instanceof HashMap data) {
|
||||
socketHandler.broadcastMessage(data, thisEvent.originContext);
|
||||
} else {
|
||||
socketHandler.broadcastMessage(event.data, thisEvent.originContext);
|
||||
|
||||
2
photon-targeting/.gitignore
vendored
2
photon-targeting/.gitignore
vendored
@@ -9,5 +9,3 @@ build
|
||||
build/*
|
||||
photonvision/*
|
||||
photonvision_config/*
|
||||
|
||||
src/main/java/org/photonvision/PhotonVersion.java
|
||||
|
||||
@@ -25,13 +25,7 @@ import edu.wpi.first.apriltag.jni.AprilTagJNI;
|
||||
import edu.wpi.first.cscore.CameraServerJNI;
|
||||
import edu.wpi.first.cscore.OpenCvLoader;
|
||||
import edu.wpi.first.hal.JNIWrapper;
|
||||
import edu.wpi.first.math.jni.ArmFeedforwardJNI;
|
||||
import edu.wpi.first.math.jni.DAREJNI;
|
||||
import edu.wpi.first.math.jni.EigenJNI;
|
||||
import edu.wpi.first.math.jni.Ellipse2dJNI;
|
||||
import edu.wpi.first.math.jni.Pose3dJNI;
|
||||
import edu.wpi.first.math.jni.StateSpaceUtilJNI;
|
||||
import edu.wpi.first.math.jni.TrajectoryUtilJNI;
|
||||
import edu.wpi.first.math.jni.WPIMathJNI;
|
||||
import edu.wpi.first.net.WPINetJNI;
|
||||
import edu.wpi.first.networktables.NetworkTablesJNI;
|
||||
import edu.wpi.first.util.CombinedRuntimeLoader;
|
||||
@@ -48,18 +42,8 @@ public class WpilibLoader {
|
||||
OpenCvLoader.Helper.setExtractOnStaticLoad(false);
|
||||
JNIWrapper.Helper.setExtractOnStaticLoad(false);
|
||||
WPINetJNI.Helper.setExtractOnStaticLoad(false);
|
||||
WPIMathJNI.Helper.setExtractOnStaticLoad(false);
|
||||
AprilTagJNI.Helper.setExtractOnStaticLoad(false);
|
||||
|
||||
// wpimathjni is a bit odd, it's all in the wpimathjni shared lib, but the java side stuff has
|
||||
// been split.
|
||||
ArmFeedforwardJNI.Helper.setExtractOnStaticLoad(false);
|
||||
DAREJNI.Helper.setExtractOnStaticLoad(false);
|
||||
EigenJNI.Helper.setExtractOnStaticLoad(false);
|
||||
Ellipse2dJNI.Helper.setExtractOnStaticLoad(false);
|
||||
Pose3dJNI.Helper.setExtractOnStaticLoad(false);
|
||||
StateSpaceUtilJNI.Helper.setExtractOnStaticLoad(false);
|
||||
TrajectoryUtilJNI.Helper.setExtractOnStaticLoad(false);
|
||||
|
||||
try {
|
||||
CombinedRuntimeLoader.loadLibraries(
|
||||
WpilibLoader.class,
|
||||
@@ -68,7 +52,6 @@ public class WpilibLoader {
|
||||
"ntcorejni",
|
||||
"wpinetjni",
|
||||
"wpiHaljni",
|
||||
"wpi",
|
||||
"cscorejni",
|
||||
"apriltagjni");
|
||||
|
||||
|
||||
@@ -34,6 +34,7 @@ import edu.wpi.first.util.struct.Struct;
|
||||
* Auto-generated serialization/deserialization helper for MultiTargetPNPResult
|
||||
*/
|
||||
public class MultiTargetPNPResultSerde implements PacketSerde<MultiTargetPNPResult> {
|
||||
|
||||
@Override
|
||||
public final String getInterfaceUUID() { return "541096947e9f3ca2d3f425ff7b04aa7b"; }
|
||||
@Override
|
||||
@@ -79,6 +80,7 @@ public class MultiTargetPNPResultSerde implements PacketSerde<MultiTargetPNPResu
|
||||
@Override
|
||||
public Struct<?>[] getNestedWpilibMessages() {
|
||||
return new Struct<?>[] {
|
||||
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,6 +34,7 @@ import edu.wpi.first.util.struct.Struct;
|
||||
* Auto-generated serialization/deserialization helper for PhotonPipelineMetadata
|
||||
*/
|
||||
public class PhotonPipelineMetadataSerde implements PacketSerde<PhotonPipelineMetadata> {
|
||||
|
||||
@Override
|
||||
public final String getInterfaceUUID() { return "ac0a45f686457856fb30af77699ea356"; }
|
||||
@Override
|
||||
@@ -84,12 +85,14 @@ public class PhotonPipelineMetadataSerde implements PacketSerde<PhotonPipelineMe
|
||||
@Override
|
||||
public PacketSerde<?>[] getNestedPhotonMessages() {
|
||||
return new PacketSerde<?>[] {
|
||||
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public Struct<?>[] getNestedWpilibMessages() {
|
||||
return new Struct<?>[] {
|
||||
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user