mirror of
https://github.com/PhotonVision/photonvision
synced 2026-06-27 02:01:40 +00:00
Compare commits
72 Commits
v2023.1.1-
...
v2023.1.1-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
15fbe29d34 | ||
|
|
550194152a | ||
|
|
3a10f49b54 | ||
|
|
7ff630dc44 | ||
|
|
4088a394f3 | ||
|
|
78ab5e7c1d | ||
|
|
14d263a567 | ||
|
|
cf1a45d35b | ||
|
|
2ebc27aa3b | ||
|
|
95c55f08cf | ||
|
|
4382b8ea3f | ||
|
|
b1905954bc | ||
|
|
548f52e117 | ||
|
|
1971744589 | ||
|
|
6c51d8ab51 | ||
|
|
8330bf9d92 | ||
|
|
e1b39a1723 | ||
|
|
915f784d9d | ||
|
|
96006fc501 | ||
|
|
4fd7533456 | ||
|
|
bb63af601d | ||
|
|
da1aabae3a | ||
|
|
643db9c435 | ||
|
|
ec7bef7a4b | ||
|
|
b72f4ca2a9 | ||
|
|
ffd741ec0a | ||
|
|
678961e4f2 | ||
|
|
4c004fc780 | ||
|
|
41a00bc90f | ||
|
|
dcad7f34a2 | ||
|
|
72d8f49145 | ||
|
|
df852410b0 | ||
|
|
3c7165bb0d | ||
|
|
f193a2331a | ||
|
|
c7aa84ca41 | ||
|
|
209cdbf45f | ||
|
|
e03ec862a8 | ||
|
|
8169da5ad4 | ||
|
|
916431b4ff | ||
|
|
7dd1719fbd | ||
|
|
b408a58e9e | ||
|
|
a64697e714 | ||
|
|
e971db2f52 | ||
|
|
7b6afd545b | ||
|
|
0f99044468 | ||
|
|
1412155c50 | ||
|
|
b1280e49d5 | ||
|
|
aaac6a4fbb | ||
|
|
b68b0ca5f6 | ||
|
|
45d99f1f6b | ||
|
|
a42fef67f2 | ||
|
|
bd4d74c192 | ||
|
|
c4500ce12b | ||
|
|
81d19672d2 | ||
|
|
04bde1b230 | ||
|
|
4f355f2749 | ||
|
|
5e604cf98d | ||
|
|
2d7a88e231 | ||
|
|
27198a3e32 | ||
|
|
fbf6fb304e | ||
|
|
d24a8d4188 | ||
|
|
def40484e3 | ||
|
|
aff163fc6a | ||
|
|
c392d5fa4d | ||
|
|
8dbd428359 | ||
|
|
ccd3a512d6 | ||
|
|
bfc5e45cd0 | ||
|
|
a1b09100e0 | ||
|
|
2bf7a77885 | ||
|
|
d1bfb86ab4 | ||
|
|
07904589df | ||
|
|
5540bbf115 |
285
.github/workflows/main.yml
vendored
285
.github/workflows/main.yml
vendored
@@ -22,27 +22,22 @@ jobs:
|
||||
working-directory: photon-client
|
||||
|
||||
# The type of runner that the job will run on.
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
# Grab the docker container.
|
||||
container:
|
||||
image: docker://node:10
|
||||
runs-on: ubuntu-22.04
|
||||
|
||||
steps:
|
||||
# Checkout code.
|
||||
- uses: actions/checkout@v1
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
# Setup Node.js
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v3.4.1
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 14
|
||||
node-version: 16
|
||||
|
||||
# Run npm
|
||||
- run: |
|
||||
npm install -g npm
|
||||
npm ci
|
||||
npm run build --if-present
|
||||
- run: npm update -g npm
|
||||
- run: npm ci
|
||||
- run: npm run build --if-present
|
||||
|
||||
# Upload client artifact.
|
||||
- uses: actions/upload-artifact@master
|
||||
@@ -50,34 +45,80 @@ jobs:
|
||||
name: built-client
|
||||
path: photon-client/dist/
|
||||
|
||||
photon-build-all:
|
||||
# The type of runner that the job will run on.
|
||||
runs-on: ubuntu-latest
|
||||
photon-build-examples:
|
||||
runs-on: ubuntu-22.04
|
||||
name: "Build Examples"
|
||||
|
||||
steps:
|
||||
# Checkout code.
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v1
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
# Fetch tags.
|
||||
- name: Fetch tags
|
||||
run: git fetch --tags --force
|
||||
|
||||
# Install Java 11.
|
||||
- name: Install Java 11
|
||||
uses: actions/setup-java@v1
|
||||
# Install Java 17.
|
||||
- name: Install Java 17
|
||||
uses: actions/setup-java@v3
|
||||
with:
|
||||
java-version: 11
|
||||
java-version: 17
|
||||
distribution: temurin
|
||||
|
||||
# Run Gradle build.
|
||||
# Need to publish to maven local first, so that C++ sim can pick it up
|
||||
# Still haven't figure out how to make the vendordep file be copied before trying to build examples
|
||||
- name: Publish photonlib to maven local
|
||||
run: |
|
||||
chmod +x gradlew
|
||||
./gradlew publishtomavenlocal -x check
|
||||
|
||||
- name: Build Java examples
|
||||
working-directory: photonlib-java-examples
|
||||
run: |
|
||||
chmod +x gradlew
|
||||
./gradlew copyPhotonlib -x check
|
||||
./gradlew buildAllExamples -x check --max-workers 2
|
||||
|
||||
- name: Build C++ examples
|
||||
working-directory: photonlib-cpp-examples
|
||||
run: |
|
||||
chmod +x gradlew
|
||||
./gradlew copyPhotonlib -x check
|
||||
./gradlew buildAllExamples -x check --max-workers 2
|
||||
|
||||
photon-build-all:
|
||||
# The type of runner that the job will run on.
|
||||
runs-on: ubuntu-22.04
|
||||
|
||||
steps:
|
||||
# Checkout code.
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
# Fetch tags.
|
||||
- name: Fetch tags
|
||||
run: git fetch --tags --force
|
||||
|
||||
# Install Java 17.
|
||||
- name: Install Java 17
|
||||
uses: actions/setup-java@v3
|
||||
with:
|
||||
java-version: 17
|
||||
distribution: temurin
|
||||
|
||||
# Run only build tasks, no checks??
|
||||
- name: Gradle Build
|
||||
run: |
|
||||
chmod +x gradlew
|
||||
./gradlew build -x check --max-workers 1
|
||||
./gradlew photon-server:build photon-lib:build -x check --max-workers 2
|
||||
|
||||
# Run Gradle Tests.
|
||||
- name: Gradle Tests
|
||||
run: ./gradlew testHeadless -i --max-workers 1
|
||||
run: ./gradlew testHeadless -i --max-workers 1 --stacktrace
|
||||
|
||||
# Generate Coverage Report.
|
||||
- name: Gradle Coverage
|
||||
@@ -85,29 +126,29 @@ jobs:
|
||||
|
||||
# Publish Coverage Report.
|
||||
- name: Publish Server Coverage Report
|
||||
uses: codecov/codecov-action@v1
|
||||
uses: codecov/codecov-action@v3
|
||||
with:
|
||||
file: ./photon-server/build/reports/jacoco/test/jacocoTestReport.xml
|
||||
|
||||
- name: Publish Core Coverage Report
|
||||
uses: codecov/codecov-action@v1
|
||||
uses: codecov/codecov-action@v3
|
||||
with:
|
||||
file: ./photon-core/build/reports/jacoco/test/jacocoTestReport.xml
|
||||
|
||||
photonserver-build-offline-docs:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-22.04
|
||||
|
||||
steps:
|
||||
# Checkout docs.
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
repository: 'PhotonVision/photonvision-docs.git'
|
||||
ref: master
|
||||
|
||||
# Install Python.
|
||||
- uses: actions/setup-python@v2
|
||||
- uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: '3.6'
|
||||
python-version: '3.9'
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
@@ -132,24 +173,25 @@ jobs:
|
||||
|
||||
photonserver-check-lint:
|
||||
# The type of runner that the job will run on.
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-22.04
|
||||
|
||||
steps:
|
||||
# Checkout code.
|
||||
- uses: actions/checkout@v1
|
||||
|
||||
# Install Java 11.
|
||||
- uses: actions/setup-java@v1
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
java-version: 11
|
||||
fetch-depth: 0
|
||||
|
||||
# Install Java 17.
|
||||
- uses: actions/setup-java@v3
|
||||
with:
|
||||
java-version: 17
|
||||
distribution: temurin
|
||||
|
||||
# Check server code with Spotless.
|
||||
- run: |
|
||||
chmod +x gradlew
|
||||
./gradlew spotlessCheck
|
||||
|
||||
|
||||
|
||||
# Building photonlib
|
||||
photonlib-build-host:
|
||||
env:
|
||||
@@ -158,22 +200,23 @@ jobs:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- os: windows-latest
|
||||
- os: windows-2022
|
||||
artifact-name: Win64
|
||||
- os: macos-latest
|
||||
- os: macos-11
|
||||
artifact-name: macOS
|
||||
- os: ubuntu-latest
|
||||
- os: ubuntu-22.04
|
||||
artifact-name: Linux
|
||||
|
||||
runs-on: ${{ matrix.os }}
|
||||
name: "Photonlib - Build - ${{ matrix.artifact-name }}"
|
||||
name: "Photonlib - Build Host - ${{ matrix.artifact-name }}"
|
||||
steps:
|
||||
- uses: actions/checkout@v2.3.4
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- uses: actions/setup-java@v1
|
||||
- uses: actions/setup-java@v3
|
||||
with:
|
||||
java-version: 11
|
||||
java-version: 17
|
||||
distribution: temurin
|
||||
- run: git fetch --tags --force
|
||||
- run: |
|
||||
chmod +x gradlew
|
||||
@@ -189,27 +232,29 @@ jobs:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- container: wpilib/roborio-cross-ubuntu:2022-18.04
|
||||
- container: wpilib/roborio-cross-ubuntu:2023-22.04
|
||||
artifact-name: Athena
|
||||
- container: wpilib/raspbian-cross-ubuntu:10-18.04
|
||||
- container: wpilib/raspbian-cross-ubuntu:bullseye-22.04
|
||||
artifact-name: Raspbian
|
||||
- container: wpilib/aarch64-cross-ubuntu:bionic-18.04
|
||||
- container: wpilib/aarch64-cross-ubuntu:bullseye-22.04
|
||||
artifact-name: Aarch64
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-22.04
|
||||
container: ${{ matrix.container }}
|
||||
name: "Photonlib - Build - ${{ matrix.artifact-name }}"
|
||||
name: "Photonlib - Build Docker - ${{ matrix.artifact-name }}"
|
||||
steps:
|
||||
- uses: actions/checkout@v2.3.4
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- uses: actions/setup-java@v1
|
||||
with:
|
||||
java-version: 11
|
||||
- run: |
|
||||
- name: Config Git
|
||||
run: |
|
||||
git config --global --add safe.directory /__w/photonvision/photonvision
|
||||
- name: Build PhotonLib
|
||||
run: |
|
||||
chmod +x gradlew
|
||||
./gradlew photon-lib:build --max-workers 1
|
||||
- run: |
|
||||
- name: Publish
|
||||
run: |
|
||||
chmod +x gradlew
|
||||
./gradlew photon-lib:publish
|
||||
env:
|
||||
@@ -218,16 +263,16 @@ jobs:
|
||||
|
||||
photonlib-wpiformat:
|
||||
name: "wpiformat"
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v3
|
||||
- name: Fetch all history and metadata
|
||||
run: |
|
||||
git fetch --prune --unshallow
|
||||
git checkout -b pr
|
||||
git branch -f master origin/master
|
||||
- name: Set up Python 3.8
|
||||
uses: actions/setup-python@v2
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: 3.8
|
||||
- name: Install clang-format
|
||||
@@ -244,40 +289,78 @@ jobs:
|
||||
- name: Generate diff
|
||||
run: git diff HEAD > wpiformat-fixes.patch
|
||||
if: ${{ failure() }}
|
||||
- uses: actions/upload-artifact@v2
|
||||
- uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: wpiformat fixes
|
||||
path: wpiformat-fixes.patch
|
||||
if: ${{ failure() }}
|
||||
|
||||
photon-build-package:
|
||||
needs: [photonclient-build, photon-build-all, photonserver-build-offline-docs, photonlib-build-host, photonlib-build-docker]
|
||||
needs: [photonclient-build, photon-build-all, photonserver-build-offline-docs]
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- os: windows-latest
|
||||
artifact-name: Win64
|
||||
architecture: x64
|
||||
arch-override: none
|
||||
- os: macos-latest
|
||||
artifact-name: macOS
|
||||
architecture: x64
|
||||
arch-override: none
|
||||
- os: ubuntu-latest
|
||||
artifact-name: Linux
|
||||
architecture: x64
|
||||
arch-override: none
|
||||
- os: macos-latest
|
||||
artifact-name: macOSArm
|
||||
architecture: x64
|
||||
arch-override: macarm64
|
||||
- os: ubuntu-latest
|
||||
artifact-name: LinuxArm32
|
||||
architecture: x64
|
||||
arch-override: linuxarm32
|
||||
- os: ubuntu-latest
|
||||
artifact-name: LinuxArm64
|
||||
architecture: x64
|
||||
arch-override: linuxarm64
|
||||
|
||||
# The type of runner that the job will run on.
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ${{ matrix.os }}
|
||||
name: "Build fat JAR - ${{ matrix.artifact-name }}"
|
||||
|
||||
steps:
|
||||
# Checkout code.
|
||||
- uses: actions/checkout@v1
|
||||
|
||||
# Install Java 11.
|
||||
- uses: actions/setup-java@v1
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
java-version: 11
|
||||
fetch-depth: 0
|
||||
|
||||
# Install Java 17.
|
||||
- uses: actions/setup-java@v3
|
||||
with:
|
||||
java-version: 17
|
||||
distribution: temurin
|
||||
|
||||
# Clear any existing web resources.
|
||||
- run: |
|
||||
rm -rf photon-server/src/main/resources/web/*
|
||||
mkdir -p photon-server/src/main/resources/web/docs
|
||||
if: ${{ (matrix.os) != 'windows-latest' }}
|
||||
- run: |
|
||||
del photon-server\src\main\resources\web\*.*
|
||||
mkdir photon-server\src\main\resources\web\docs
|
||||
if: ${{ (matrix.os) == 'windows-latest' }}
|
||||
|
||||
# Download client artifact to resources folder.
|
||||
- uses: actions/download-artifact@v2
|
||||
- uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: built-client
|
||||
path: photon-server/src/main/resources/web/
|
||||
|
||||
# Download docs artifact to resources folder.
|
||||
- uses: actions/download-artifact@v2
|
||||
- uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: built-docs
|
||||
path: photon-server/src/main/resources/web/docs
|
||||
@@ -285,54 +368,62 @@ jobs:
|
||||
# Build fat jar for both pi and everything
|
||||
- run: |
|
||||
chmod +x gradlew
|
||||
./gradlew photon-server:shadowJar --max-workers 1
|
||||
./gradlew photon-server:shadowJar --max-workers 1 -Ppionly
|
||||
./gradlew photon-server:shadowJar --max-workers 2 -PArchOverride=${{ matrix.arch-override }}
|
||||
if: ${{ (matrix.arch-override != 'none') }}
|
||||
- run: |
|
||||
chmod +x gradlew
|
||||
./gradlew photon-server:shadowJar --max-workers 2
|
||||
if: ${{ (matrix.arch-override == 'none') }}
|
||||
|
||||
# The image will only pull the Pi JAR in
|
||||
# The image will only pull the Pi32 JAR in
|
||||
- name: Generate image
|
||||
if: github.event_name != 'pull_request'
|
||||
if: ${{ github.event_name != 'pull_request' && (matrix.artifact-name) == 'LinuxArm32' }}
|
||||
run: |
|
||||
chmod +x scripts/generatePiImage.sh
|
||||
./scripts/generatePiImage.sh
|
||||
|
||||
# Upload final fat jar as artifact.
|
||||
- uses: actions/upload-artifact@master
|
||||
- uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: jar
|
||||
name: jar-${{ matrix.artifact-name }}
|
||||
path: photon-server/build/libs
|
||||
- uses: actions/upload-artifact@master
|
||||
with:
|
||||
name: image
|
||||
path: photonvision*.zip
|
||||
|
||||
# Upload image as well
|
||||
|
||||
- uses: actions/upload-artifact@v3
|
||||
if: ${{ github.event_name != 'pull_request' && (matrix.artifact-name) == 'LinuxArm32' }}
|
||||
with:
|
||||
name: image-${{ matrix.artifact-name }}
|
||||
path: photonvision*.xz
|
||||
|
||||
|
||||
photon-release:
|
||||
needs: [photon-build-package]
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
# Download literally every single artifact. This also downloads client and docs,
|
||||
# but the filtering below won't pick these up (I hope)
|
||||
- uses: actions/download-artifact@v2
|
||||
|
||||
- run: find
|
||||
|
||||
# Push to dev release
|
||||
- uses: pyTooling/Actions/releaser@r0
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
tag: 'Dev'
|
||||
rm: true
|
||||
files: |
|
||||
photon-server/build/libs/*.jar
|
||||
photonvision*.zip
|
||||
**/*.xz
|
||||
**/*.jar
|
||||
if: github.event_name == 'push'
|
||||
|
||||
photon-release:
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
needs: [photon-build-package]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
# This *should* pull in fat and pi-only jars
|
||||
- uses: actions/download-artifact@v2
|
||||
with:
|
||||
name: jar
|
||||
|
||||
# And the image we made previously
|
||||
- uses: actions/download-artifact@v2
|
||||
with:
|
||||
name: image
|
||||
|
||||
# All we've downloaded (ideally) is the fat jar, pi jar, and image. So just upload it all
|
||||
# Upload all jars and xz archives
|
||||
- uses: softprops/action-gh-release@v1
|
||||
with:
|
||||
files: '**/*'
|
||||
files: |
|
||||
**/*.xz
|
||||
**/*.jar
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -30,6 +30,7 @@ backend/settings/
|
||||
*.nar
|
||||
*.ear
|
||||
*.zip
|
||||
*.xz
|
||||
*.tar.gz
|
||||
*.rar
|
||||
|
||||
@@ -149,3 +150,6 @@ lib/*
|
||||
photon-server/lib/libapriltag.so
|
||||
photon-server/bin/main/nativelibraries/apriltag/*
|
||||
photon-server/src/main/resources/nativelibraries/apriltag/*
|
||||
|
||||
photonlib-java-examples/*/vendordeps/*
|
||||
photonlib-cpp-examples/*/vendordeps/*
|
||||
|
||||
@@ -13,6 +13,7 @@ modifiableFileExclude {
|
||||
\.jpg$
|
||||
\.jpeg$
|
||||
\.png$
|
||||
\.gif$
|
||||
\.so$
|
||||
\.dll$
|
||||
}
|
||||
|
||||
23
LICENSE_MathUtils_orthogonalizeRotationMatrix.txt
Normal file
23
LICENSE_MathUtils_orthogonalizeRotationMatrix.txt
Normal file
@@ -0,0 +1,23 @@
|
||||
Copyright (c) 2022 Photon Vision. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
* Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
* Redistributions in binary form must reproduce the above copyright
|
||||
notice, this list of conditions and the following disclaimer in the
|
||||
documentation and/or other materials provided with the distribution.
|
||||
* Neither the name of FIRST, WPILib, nor the names of other WPILib
|
||||
contributors may be used to endorse or promote products derived from
|
||||
this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY FIRST AND OTHER WPILIB CONTRIBUTORS "AS IS" AND
|
||||
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
WARRANTIES OF MERCHANTABILITY NONINFRINGEMENT AND FITNESS FOR A PARTICULAR
|
||||
PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL FIRST OR CONTRIBUTORS BE LIABLE FOR
|
||||
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
|
||||
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
36
README.md
36
README.md
@@ -6,7 +6,7 @@ PhotonVision is the free, fast, and easy-to-use computer vision solution for the
|
||||
|
||||
A copy of the latest Raspberry Pi image is available [here](https://github.com/PhotonVision/photon-pi-gen/releases). A copy of the latest standalone JAR is available [here](https://github.com/PhotonVision/photonvision/releases). If you are a Gloworm user you can find the latest Gloworm image [here](https://github.com/gloworm-vision/pi-gen/releases).
|
||||
|
||||
If you are interested in contributing code or documentation to the project, please [read our getting started page for contributors](https://docs.photonvision.org/en/latest/docs/other/contributing/index.html) and **[join the Discord](https://discord.gg/wYxTwym) to introduce yourself!** We hope to provide a welcoming community to anyone who is interested in helping.
|
||||
If you are interested in contributing code or documentation to the project, please [read our getting started page for contributors](https://docs.photonvision.org/en/latest/docs/contributing/index.html) and **[join the Discord](https://discord.gg/wYxTwym) to introduce yourself!** We hope to provide a welcoming community to anyone who is interested in helping.
|
||||
|
||||
## Authors
|
||||
|
||||
@@ -18,10 +18,42 @@ If you are interested in contributing code or documentation to the project, plea
|
||||
|
||||
Note that these are case sensitive!
|
||||
|
||||
* `-Ppionly`: only builds for `linuxraspbian`, which reduces JAR size. The JAR name will have "-raspi" appended.
|
||||
* `-PArchOverride=foobar`: builds for a target system other than your current architecture. Valid overrides are winx32, winx64,
|
||||
macx64, macarm64, linuxx64, linuxarm64, linuxarm32, and linuxathena.
|
||||
- `-PtgtIp`: deploys (builds and copies the JAR) to the coprocessor at the specified IP
|
||||
- `-Pprofile`: enables JVM profiling
|
||||
|
||||
## Building
|
||||
|
||||
Gradle is used for all C++ and Java code, and NPM is used for the web UI. Instructions to compile PhotonVision yourself can be found [in our docs](https://docs.photonvision.org/en/latest/docs/contributing/photonvision/build-instructions.html?highlight=npm%20install#compiling-instructions).
|
||||
|
||||
You can run one of the many built in examples straight from the command line, too! They contain a fully featured robot project, and some include simulation support. The projects can be found inside the `photonlib-java-examples` and `photonlib-cpp-examples` subdirectories, respectively. The projects currently available include:
|
||||
|
||||
- photonlib-java-examples:
|
||||
- aimandrange:simulateJava
|
||||
- aimattarget:simulateJava
|
||||
- getinrange:simulateJava
|
||||
- simaimandrange:simulateJava
|
||||
- simposeest:simulateJava
|
||||
- photonlib-cpp-examples:
|
||||
- aimandrange:simulateNative
|
||||
- getinrange:simulateNative
|
||||
|
||||
To run them, use the commands listed below. Photonlib must first be published to your local maven repository, then the `copyPhotonlib` 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 unsupported.
|
||||
|
||||
```
|
||||
~/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
|
||||
```
|
||||
|
||||
|
||||
## Acknowledgments
|
||||
PhotonVision was forked from [Chameleon Vision](https://github.com/Chameleon-Vision/chameleon-vision/). Thank you to everyone who worked on the original project.
|
||||
|
||||
|
||||
22
build.gradle
22
build.gradle
@@ -4,14 +4,18 @@ plugins {
|
||||
id "com.github.node-gradle.node" version "3.1.1" apply false
|
||||
id "edu.wpi.first.GradleJni" version "1.0.0"
|
||||
id "edu.wpi.first.GradleVsCode" version "1.1.0"
|
||||
id "edu.wpi.first.NativeUtils" version "2022.8.1" apply false
|
||||
id "edu.wpi.first.NativeUtils" version "2023.10.0" apply false
|
||||
id "edu.wpi.first.wpilib.repositories.WPILibRepositoriesPlugin" version "2020.2"
|
||||
id "org.hidetake.ssh" version "2.10.1"
|
||||
id 'edu.wpi.first.WpilibTools' version '1.0.0'
|
||||
}
|
||||
|
||||
import org.gradle.api.internal.artifacts.dependencies.DefaultExternalModuleDependency;
|
||||
|
||||
allprojects {
|
||||
repositories {
|
||||
jcenter()
|
||||
mavenCentral()
|
||||
mavenLocal()
|
||||
maven { url = "https://maven.photonvision.org/repository/internal/" }
|
||||
}
|
||||
wpilibRepositories.addAllReleaseRepositories(it)
|
||||
@@ -22,24 +26,28 @@ allprojects {
|
||||
apply from: "versioningHelper.gradle"
|
||||
|
||||
ext {
|
||||
wpilibVersion = "2022.1.1"
|
||||
opencvVersion = "4.5.2-1"
|
||||
wpilibVersion = "2023.1.1-beta-7-15-g1e7fcd5"
|
||||
opencvVersion = "4.6.0-4"
|
||||
joglVersion = "2.4.0-rc-20200307"
|
||||
pubVersion = versionString
|
||||
isDev = pubVersion.startsWith("dev")
|
||||
|
||||
if(project.hasProperty('pionly')) {
|
||||
jniPlatforms = ['linuxraspbian']
|
||||
jniPlatforms = ['linuxarm32']
|
||||
} else if(project.hasProperty('winonly')) {
|
||||
jniPlatforms = ['windowsx86-64']
|
||||
} else if(project.hasProperty('aarch64only')) {
|
||||
jniPlatforms = ['linuxaarch64bionic']
|
||||
} else {
|
||||
jniPlatforms = ['linuxaarch64bionic', 'linuxraspbian', 'linuxx86-64', 'osxx86-64', 'windowsx86-64']
|
||||
|
||||
jniPlatforms = ['linuxarm64', 'linuxarm32', 'linuxx86-64', 'osxuniversal', 'windowsx86-64']
|
||||
}
|
||||
|
||||
println("Building for archs " + jniPlatforms)
|
||||
|
||||
}
|
||||
|
||||
wpilibTools.deps.wpilibVersion = wpilibVersion
|
||||
|
||||
spotless {
|
||||
java {
|
||||
toggleOffOn()
|
||||
|
||||
25
photon-client/package-lock.json
generated
25
photon-client/package-lock.json
generated
@@ -5,7 +5,6 @@
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "photon-client",
|
||||
"version": "3.0.0",
|
||||
"dependencies": {
|
||||
"@femessage/log-viewer": "^1.4.2",
|
||||
@@ -19,7 +18,8 @@
|
||||
"three-full": "^28.0.2",
|
||||
"vue": "^2.6.12",
|
||||
"vue-axios": "^2.1.5",
|
||||
"vue-native-websocket": "git+https://github.com/PhotonVision/vue-native-websocket.git#5189f29", "vue-router": "^3.4.3",
|
||||
"vue-native-websocket": "git+https://git@github.com/PhotonVision/vue-native-websocket.git#5189f29",
|
||||
"vue-router": "^3.4.3",
|
||||
"vuetify": "^2.3.10",
|
||||
"vuex": "^3.5.1"
|
||||
},
|
||||
@@ -2682,6 +2682,7 @@
|
||||
"thread-loader": "^2.1.3",
|
||||
"url-loader": "^2.2.0",
|
||||
"vue-loader": "^15.9.2",
|
||||
"vue-loader-v16": "npm:vue-loader@^16.0.0-beta.3",
|
||||
"vue-style-loader": "^4.1.2",
|
||||
"webpack": "^4.0.0",
|
||||
"webpack-bundle-analyzer": "^3.8.0",
|
||||
@@ -3115,6 +3116,7 @@
|
||||
"merge-source-map": "^1.1.0",
|
||||
"postcss": "^7.0.14",
|
||||
"postcss-selector-parser": "^6.0.2",
|
||||
"prettier": "^1.18.2",
|
||||
"source-map": "~0.6.1",
|
||||
"vue-template-es2015-compiler": "^1.9.0"
|
||||
},
|
||||
@@ -4701,6 +4703,7 @@
|
||||
"dependencies": {
|
||||
"anymatch": "~3.1.1",
|
||||
"braces": "~3.0.2",
|
||||
"fsevents": "~2.1.2",
|
||||
"glob-parent": "~5.1.0",
|
||||
"is-binary-path": "~2.1.0",
|
||||
"is-glob": "~4.0.1",
|
||||
@@ -9164,6 +9167,9 @@
|
||||
"version": "4.0.0",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"graceful-fs": "^4.1.6"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"graceful-fs": "^4.1.6"
|
||||
}
|
||||
@@ -9175,7 +9181,11 @@
|
||||
"@babel/runtime": "^7.14.0",
|
||||
"atob": "^2.1.2",
|
||||
"btoa": "^1.2.1",
|
||||
"fflate": "^0.4.8"
|
||||
"canvg": "^3.0.6",
|
||||
"core-js": "^3.6.0",
|
||||
"dompurify": "^2.2.0",
|
||||
"fflate": "^0.4.8",
|
||||
"html2canvas": "^1.0.0-rc.5"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"canvg": "^3.0.6",
|
||||
@@ -14190,8 +14200,10 @@
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"chokidar": "^3.4.0",
|
||||
"graceful-fs": "^4.1.2",
|
||||
"neo-async": "^2.5.0"
|
||||
"neo-async": "^2.5.0",
|
||||
"watchpack-chokidar2": "^2.0.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"chokidar": "^3.4.0",
|
||||
@@ -14300,6 +14312,7 @@
|
||||
"anymatch": "^2.0.0",
|
||||
"async-each": "^1.0.1",
|
||||
"braces": "^2.3.2",
|
||||
"fsevents": "^1.2.7",
|
||||
"glob-parent": "^3.1.0",
|
||||
"inherits": "^2.0.3",
|
||||
"is-binary-path": "^1.0.0",
|
||||
@@ -14540,6 +14553,7 @@
|
||||
"anymatch": "^2.0.0",
|
||||
"async-each": "^1.0.1",
|
||||
"braces": "^2.3.2",
|
||||
"fsevents": "^1.2.7",
|
||||
"glob-parent": "^3.1.0",
|
||||
"inherits": "^2.0.3",
|
||||
"is-binary-path": "^1.0.0",
|
||||
@@ -25270,7 +25284,8 @@
|
||||
},
|
||||
"vue-native-websocket": {
|
||||
"version": "git+ssh://git@github.com/PhotonVision/vue-native-websocket.git#7a327918e03b215b6899b0d648c5130ece1fa912",
|
||||
"from": "vue-native-websocket@git+https://github.com/PhotonVision/vue-native-websocket.git#7a32791" },
|
||||
"from": "vue-native-websocket@git+https://git@github.com/PhotonVision/vue-native-websocket.git#5189f29"
|
||||
},
|
||||
"vue-router": {
|
||||
"version": "3.4.3"
|
||||
},
|
||||
|
||||
BIN
photon-client/public/loading.gif
Normal file
BIN
photon-client/public/loading.gif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 12 KiB |
317
photon-client/public/thinclient.html
Normal file
317
photon-client/public/thinclient.html
Normal file
@@ -0,0 +1,317 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<title>ThinClient</title>
|
||||
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
.imgbox {
|
||||
display: grid;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
.center-fit {
|
||||
|
||||
width: 90vw;
|
||||
margin: auto;
|
||||
}
|
||||
</style>
|
||||
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<hr>
|
||||
<div class="imgbox">
|
||||
<img id="streamImg" class="center-fit" src=''>
|
||||
</div>
|
||||
<hr>
|
||||
|
||||
<form id="frm1">
|
||||
Host <input type="text" id="host" value="photonvision.local"><br>
|
||||
Port <input type="text" id="port" value="1181"><br>
|
||||
</form>
|
||||
|
||||
<button>Start Stream</button>
|
||||
|
||||
<script type="module">
|
||||
class WebsocketVideoStream{
|
||||
|
||||
constructor(drawDiv, streamPort, host) {
|
||||
|
||||
this.drawDiv = drawDiv;
|
||||
this.image = document.getElementById(this.drawDiv);
|
||||
this.streamPort = streamPort;
|
||||
this.newStreamPortReq = null;
|
||||
this.serverAddr = "ws://" + host + "/websocket_cameras";
|
||||
this.dispNoStream();
|
||||
this.ws_connect();
|
||||
this.imgData = null;
|
||||
this.imgDataTime = -1;
|
||||
this.imgObjURL = null;
|
||||
this.frameRxCount = 0;
|
||||
|
||||
//Display state machine
|
||||
this.DSM_DISCONNECTED = "DISCONNECTED";
|
||||
this.DSM_WAIT_FOR_VALID_PORT = "WAIT_FOR_VALID_PORT";
|
||||
this.DSM_SUBSCRIBE = "SUBSCRIBE";
|
||||
this.DSM_WAIT_FOR_FIRST_FRAME = "WAIT_FOR_FIRST_FRAME";
|
||||
this.DSM_SHOWING = "SHOWING";
|
||||
this.DSM_RESTART_UNSUBSCRIBE = "UNSUBSCRIBE";
|
||||
this.DSM_RESTART_WAIT = "WAIT_BEFORE_SUBSCRIBE";
|
||||
|
||||
this.dsm_cur_state = this.DSM_DISCONNECTED;
|
||||
this.dsm_prev_state = this.DSM_DISCONNECTED;
|
||||
this.dsm_restart_start_time = window.performance.now();
|
||||
|
||||
requestAnimationFrame(()=>this.animationLoop());
|
||||
}
|
||||
|
||||
dispImageData(){
|
||||
//From https://stackoverflow.com/questions/67507616/set-image-src-from-image-blob/67507685#67507685
|
||||
if(this.imgObjURL != null){
|
||||
URL.revokeObjectURL(this.imgObjURL)
|
||||
}
|
||||
this.imgObjURL = URL.createObjectURL(this.imgData);
|
||||
|
||||
//Update the image with the new mimetype and image
|
||||
this.image.src = this.imgObjURL;
|
||||
}
|
||||
|
||||
dispNoStream() {
|
||||
this.image.src = "loading.gif";
|
||||
}
|
||||
|
||||
animationLoop(){
|
||||
// Update time metrics
|
||||
var now = window.performance.now();
|
||||
var timeInState = now - this.dsm_restart_start_time;
|
||||
|
||||
// Save previous state
|
||||
this.dsm_prev_state = this.dsm_cur_state;
|
||||
|
||||
// Evaluate state transitions
|
||||
if(this.serverConnectionActive == false){
|
||||
//Any state - if the server connection goes false, always transition to disconnected
|
||||
this.dsm_cur_state = this.DSM_DISCONNECTED;
|
||||
} else {
|
||||
//Conditional transitions
|
||||
switch(this.dsm_cur_state) {
|
||||
case this.DSM_DISCONNECTED:
|
||||
//Immediately transition to waiting for the first frame
|
||||
this.dsm_cur_state = this.DSM_WAIT_FOR_VALID_PORT;
|
||||
break;
|
||||
case this.DSM_WAIT_FOR_VALID_PORT:
|
||||
// Wait until the user has configured a valid port
|
||||
if(this.streamPort > 0){
|
||||
this.dsm_cur_state = this.DSM_SUBSCRIBE;
|
||||
} else {
|
||||
this.dsm_cur_state = this.DSM_WAIT_FOR_VALID_PORT;
|
||||
}
|
||||
break;
|
||||
case this.DSM_SUBSCRIBE:
|
||||
// Immediately transition after subscriptions is sent
|
||||
this.dsm_cur_state = this.DSM_WAIT_FOR_FIRST_FRAME;
|
||||
break;
|
||||
case this.DSM_WAIT_FOR_FIRST_FRAME:
|
||||
if(this.imgData != null){
|
||||
//we got some image data, start showing it
|
||||
this.dsm_cur_state = this.DSM_SHOWING;
|
||||
} else if (this.newStreamPortReq != null){
|
||||
//Stream port requested changed, unsubscribe and restart
|
||||
this.dsm_cur_state = this.DSM_RESTART_UNSUBSCRIBE;
|
||||
} else {
|
||||
this.dsm_cur_state = this.DSM_WAIT_FOR_FIRST_FRAME;
|
||||
}
|
||||
break;
|
||||
case this.DSM_SHOWING:
|
||||
if((now - this.imgDataTime) > 2500){
|
||||
//timeout, begin the restart sequence
|
||||
this.dsm_cur_state = this.DSM_RESTART_UNSUBSCRIBE;
|
||||
} else if (this.newStreamPortReq != null){
|
||||
//Stream port requested changed, unsubscribe and restart
|
||||
this.dsm_cur_state = this.DSM_RESTART_UNSUBSCRIBE;
|
||||
} else {
|
||||
//stay in this state.
|
||||
this.dsm_cur_state = this.DSM_SHOWING;
|
||||
}
|
||||
break;
|
||||
case this.DSM_RESTART_UNSUBSCRIBE:
|
||||
//Only should spend one loop in Unsubscribe, immediately transition
|
||||
this.dsm_cur_state = this.DSM_RESTART_WAIT;
|
||||
break;
|
||||
case this.DSM_RESTART_WAIT:
|
||||
if (timeInState > 250) {
|
||||
//we've waited long enough, go to try to re-subscribe
|
||||
this.dsm_cur_state = this.DSM_WAIT_FOR_VALID_PORT;
|
||||
} else {
|
||||
//stay in this state.
|
||||
this.dsm_cur_state = this.DSM_RESTART_WAIT;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
// Shouldn't get here, default back to init
|
||||
this.dsm_cur_state = this.DSM_DISCONNECTED;
|
||||
}
|
||||
}
|
||||
|
||||
//take current-state or state-transition actions
|
||||
|
||||
if(this.dsm_cur_state != this.dsm_prev_state){
|
||||
//Any state transition
|
||||
console.log("State Change: " + this.dsm_prev_state + " -> " + this.dsm_cur_state);
|
||||
}
|
||||
|
||||
if(this.dsm_cur_state == this.DSM_SHOWING){
|
||||
// Currently in SHOWING
|
||||
this.dispImageData();
|
||||
}
|
||||
|
||||
if(this.dsm_cur_state != this.DSM_SHOWING && this.dsm_prev_state == this.DSM_SHOWING ){
|
||||
//Any transition out of showing - no stream
|
||||
this.dispNoStream();
|
||||
}
|
||||
|
||||
if(this.dsm_cur_state == this.DSM_RESTART_UNSUBSCRIBE){
|
||||
// Currently in UNSUBSCRIBE, do the unsubscribe actions
|
||||
this.stopStream();
|
||||
this.dsm_restart_start_time = now;
|
||||
}
|
||||
|
||||
if(this.dsm_cur_state == this.DSM_SUBSCRIBE){
|
||||
// Currently in SUBSCRIBE, do the subscribe actions
|
||||
this.startStream();
|
||||
this.dsm_restart_start_time = now;
|
||||
}
|
||||
|
||||
if(this.dsm_cur_state == this.DSM_WAIT_FOR_VALID_PORT){
|
||||
// Currently waiting for a vaild port to be requested
|
||||
if(this.newStreamPortReq != null){
|
||||
this.streamPort = this.newStreamPortReq;
|
||||
this.newStreamPortReq = null;
|
||||
}
|
||||
}
|
||||
|
||||
requestAnimationFrame(()=>this.animationLoop());
|
||||
}
|
||||
|
||||
startStream() {
|
||||
console.log("Subscribing to port " + this.streamPort);
|
||||
this.imgData = null;
|
||||
this.ws.send(JSON.stringify({"cmd": "subscribe", "port":this.streamPort}));
|
||||
}
|
||||
|
||||
stopStream() {
|
||||
console.log("Unsubscribing");
|
||||
this.ws.send(JSON.stringify({"cmd": "unsubscribe"}));
|
||||
this.imgData = null;
|
||||
}
|
||||
|
||||
setPort(streamPort){
|
||||
console.log("Port set to " + streamPort);
|
||||
this.newStreamPortReq = streamPort;
|
||||
}
|
||||
|
||||
ws_onOpen() {
|
||||
// Set the flag allowing general server communication
|
||||
this.serverConnectionActive = true;
|
||||
console.log("Connected!");
|
||||
}
|
||||
|
||||
ws_onClose(e) {
|
||||
//Clear flags to stop server communication
|
||||
this.ws = null;
|
||||
this.serverConnectionActive = false;
|
||||
|
||||
console.log('Camera Socket is closed. Reconnect will be attempted in 0.5 second.', e.reason);
|
||||
setTimeout(this.ws_connect.bind(this), 500);
|
||||
|
||||
if(!e.wasClean){
|
||||
console.error('Socket encountered error!');
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
ws_onError(e){
|
||||
e; //prevent unused failure
|
||||
this.ws.close();
|
||||
}
|
||||
|
||||
ws_onMessage(e){
|
||||
if(typeof e.data === 'string'){
|
||||
//string data from host
|
||||
//TODO - anything to recieve info here? Maybe "avaialble streams?"
|
||||
} else {
|
||||
if(e.data.size > 0){
|
||||
//binary data - a frame
|
||||
this.imgData = e.data;
|
||||
this.imgDataTime = window.performance.now();
|
||||
this.frameRxCount++;
|
||||
} else {
|
||||
//TODO - server is sending empty frames?
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
ws_connect() {
|
||||
this.serverConnectionActive = false;
|
||||
this.ws = new WebSocket(this.serverAddr);
|
||||
this.ws.binaryType = "blob";
|
||||
this.ws.onopen = this.ws_onOpen.bind(this);
|
||||
this.ws.onmessage = this.ws_onMessage.bind(this);
|
||||
this.ws.onclose = this.ws_onClose.bind(this);
|
||||
this.ws.onerror = this.ws_onError.bind(this);
|
||||
console.log("Connecting to server " + this.serverAddr);
|
||||
}
|
||||
|
||||
ws_close(){
|
||||
this.ws.close();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
var stream = null;
|
||||
|
||||
function streamStartRequest() {
|
||||
var host = document.getElementById("host").value + ":5800";
|
||||
var port = document.getElementById("port").value;
|
||||
if(stream == null){
|
||||
stream = new WebsocketVideoStream("streamImg",port,host);
|
||||
} else {
|
||||
stream.setPort(port);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Attach listener
|
||||
document.querySelector('button').addEventListener('click', streamStartRequest);
|
||||
|
||||
// Deal with URLParams, validating inputs
|
||||
const queryString = window.location.search;
|
||||
const urlParams = new URLSearchParams(queryString);
|
||||
const port_in = urlParams.get('port')
|
||||
const host_in = urlParams.get('host')
|
||||
if(port_in != ""){
|
||||
document.getElementById("port").value = port_in;
|
||||
}
|
||||
|
||||
if(host_in != ""){
|
||||
document.getElementById("host").value = host_in;
|
||||
}
|
||||
|
||||
if(port_in != "" & host_in != ""){
|
||||
streamStartRequest(); //we got valid inputs, auto-start the stream
|
||||
}
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
</body>
|
||||
|
||||
|
||||
</html>
|
||||
@@ -1,35 +1,17 @@
|
||||
<template>
|
||||
<v-app>
|
||||
<!-- Although most of the app runs with the "light" theme, the navigation drawer needs to have white text and icons so it uses the dark theme-->
|
||||
<v-navigation-drawer
|
||||
dark
|
||||
app
|
||||
permanent
|
||||
:mini-variant="compact"
|
||||
color="primary"
|
||||
>
|
||||
<v-navigation-drawer dark app permanent :mini-variant="compact" color="primary">
|
||||
<v-list>
|
||||
<!-- List item for the heading; note that there are some tricks in setting padding and image width make things look right -->
|
||||
<v-list-item :class="compact ? 'pr-0 pl-0' : ''">
|
||||
<v-list-item-icon class="mr-0">
|
||||
<img
|
||||
v-if="!compact"
|
||||
class="logo"
|
||||
src="./assets/logoLarge.png"
|
||||
>
|
||||
<img
|
||||
v-else
|
||||
class="logo"
|
||||
src="./assets/logoSmall.png"
|
||||
>
|
||||
<img v-if="!compact" class="logo" src="./assets/logoLarge.png">
|
||||
<img v-else class="logo" src="./assets/logoSmall.png">
|
||||
</v-list-item-icon>
|
||||
</v-list-item>
|
||||
|
||||
<v-list-item
|
||||
link
|
||||
to="dashboard"
|
||||
@click="rollbackPipelineIndex()"
|
||||
>
|
||||
<v-list-item link to="dashboard" @click="rollbackPipelineIndex()">
|
||||
<v-list-item-icon>
|
||||
<v-icon>mdi-view-dashboard</v-icon>
|
||||
</v-list-item-icon>
|
||||
@@ -37,12 +19,7 @@
|
||||
<v-list-item-title>Dashboard</v-list-item-title>
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
<v-list-item
|
||||
ref="camerasTabOpener"
|
||||
link
|
||||
to="cameras"
|
||||
@click="switchToDriverMode()"
|
||||
>
|
||||
<v-list-item ref="camerasTabOpener" link to="cameras" @click="switchToDriverMode()">
|
||||
<v-list-item-icon>
|
||||
<v-icon>mdi-camera</v-icon>
|
||||
</v-list-item-icon>
|
||||
@@ -50,11 +27,7 @@
|
||||
<v-list-item-title>Cameras</v-list-item-title>
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
<v-list-item
|
||||
link
|
||||
to="settings"
|
||||
@click="switchToSettingsTab()"
|
||||
>
|
||||
<v-list-item link to="settings" @click="switchToSettingsTab()">
|
||||
<v-list-item-icon>
|
||||
<v-icon>mdi-settings</v-icon>
|
||||
</v-list-item-icon>
|
||||
@@ -62,10 +35,7 @@
|
||||
<v-list-item-title>Settings</v-list-item-title>
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
<v-list-item
|
||||
link
|
||||
to="docs"
|
||||
>
|
||||
<v-list-item link to="docs">
|
||||
<v-list-item-icon>
|
||||
<v-icon>mdi-bookshelf</v-icon>
|
||||
</v-list-item-icon>
|
||||
@@ -73,11 +43,7 @@
|
||||
<v-list-item-title>Documentation</v-list-item-title>
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
<v-list-item
|
||||
v-if="this.$vuetify.breakpoint.mdAndUp"
|
||||
link
|
||||
@click.stop="toggleCompactMode"
|
||||
>
|
||||
<v-list-item v-if="this.$vuetify.breakpoint.mdAndUp" link @click.stop="toggleCompactMode">
|
||||
<v-list-item-icon>
|
||||
<v-icon v-if="compact">
|
||||
mdi-chevron-right
|
||||
@@ -97,44 +63,24 @@
|
||||
<v-icon v-if="$store.state.settings.networkSettings.runNTServer">
|
||||
mdi-server
|
||||
</v-icon>
|
||||
<img
|
||||
v-else-if="$store.state.ntConnectionInfo.connected"
|
||||
src="@/assets/robot.svg"
|
||||
alt=""
|
||||
>
|
||||
<img
|
||||
v-else
|
||||
class="pulse"
|
||||
style="border-radius: 100%"
|
||||
src="@/assets/robot-off.svg"
|
||||
alt=""
|
||||
>
|
||||
<img v-else-if="$store.state.ntConnectionInfo.connected" src="@/assets/robot.svg" alt="">
|
||||
<img v-else class="pulse" style="border-radius: 100%" src="@/assets/robot-off.svg" alt="">
|
||||
</v-list-item-icon>
|
||||
<v-list-item-content>
|
||||
<v-list-item-title
|
||||
v-if="$store.state.settings.networkSettings.runNTServer"
|
||||
class="text-wrap"
|
||||
>
|
||||
NetworkTables server running for {{ $store.state.ntConnectionInfo.clients ? $store.state.ntConnectionInfo.clients : 'zero' }} clients!
|
||||
<v-list-item-title v-if="$store.state.settings.networkSettings.runNTServer" class="text-wrap">
|
||||
NetworkTables server running for {{ $store.state.ntConnectionInfo.clients ?
|
||||
$store.state.ntConnectionInfo.clients : 'zero'
|
||||
}} clients!
|
||||
</v-list-item-title>
|
||||
<v-list-item-title
|
||||
v-else-if="$store.state.ntConnectionInfo.connected && $store.state.backendConnected"
|
||||
class="text-wrap"
|
||||
>
|
||||
<v-list-item-title v-else-if="$store.state.ntConnectionInfo.connected && $store.state.backendConnected"
|
||||
class="text-wrap">
|
||||
Robot connected! {{ $store.state.ntConnectionInfo.address }}
|
||||
</v-list-item-title>
|
||||
<v-list-item-title
|
||||
v-else
|
||||
class="text-wrap"
|
||||
>
|
||||
<v-list-item-title v-else class="text-wrap">
|
||||
Not connected to robot!
|
||||
</v-list-item-title>
|
||||
<router-link
|
||||
v-if="!$store.state.settings.networkSettings.runNTServer"
|
||||
to="settings"
|
||||
class="accent--text"
|
||||
@click="switchToSettingsTab"
|
||||
>
|
||||
<router-link v-if="!$store.state.settings.networkSettings.runNTServer" to="settings" class="accent--text"
|
||||
@click="switchToSettingsTab">
|
||||
Team number is {{ $store.state.settings.networkSettings.teamNumber }}
|
||||
</router-link>
|
||||
</v-list-item-content>
|
||||
@@ -145,11 +91,7 @@
|
||||
<v-icon v-if="$store.state.backendConnected">
|
||||
mdi-wifi
|
||||
</v-icon>
|
||||
<v-icon
|
||||
v-else
|
||||
class="pulse"
|
||||
style="border-radius: 100%;"
|
||||
>
|
||||
<v-icon v-else class="pulse" style="border-radius: 100%;">
|
||||
mdi-wifi-off
|
||||
</v-icon>
|
||||
</v-list-item-icon>
|
||||
@@ -163,10 +105,7 @@
|
||||
</v-list>
|
||||
</v-navigation-drawer>
|
||||
<v-main>
|
||||
<v-container
|
||||
fluid
|
||||
fill-height
|
||||
>
|
||||
<v-container fluid fill-height>
|
||||
<v-layout>
|
||||
<v-flex>
|
||||
<router-view @switch-to-cameras="switchToDriverMode" />
|
||||
@@ -175,32 +114,15 @@
|
||||
</v-container>
|
||||
</v-main>
|
||||
|
||||
<v-dialog
|
||||
v-model="$store.state.logsOverlay"
|
||||
width="1500"
|
||||
dark
|
||||
>
|
||||
<v-dialog v-model="$store.state.logsOverlay" width="1500" dark>
|
||||
<logs />
|
||||
</v-dialog>
|
||||
<v-dialog
|
||||
v-model="needsTeamNumberSet"
|
||||
width="500"
|
||||
dark
|
||||
persistent
|
||||
>
|
||||
<v-card
|
||||
dark
|
||||
color="primary"
|
||||
flat
|
||||
>
|
||||
<v-dialog v-model="needsTeamNumberSet" width="500" dark persistent>
|
||||
<v-card dark color="primary" flat>
|
||||
<v-card-title>No team number set!</v-card-title>
|
||||
<v-card-text>
|
||||
PhotonVision cannot connect to your robot! Please
|
||||
<router-link
|
||||
to="settings"
|
||||
class="accent--text"
|
||||
@click="switchToSettingsTab"
|
||||
>
|
||||
<router-link to="settings" class="accent--text" @click="switchToSettingsTab">
|
||||
visit the settings tab
|
||||
</router-link>
|
||||
and set your team number.
|
||||
@@ -215,138 +137,152 @@ import Logs from "./views/LogsView"
|
||||
// import {mapState} from "vuex";
|
||||
|
||||
export default {
|
||||
name: 'App',
|
||||
components: {
|
||||
Logs
|
||||
},
|
||||
data: () => ({
|
||||
// Used so that we can switch back to the previously selected pipeline after camera calibration
|
||||
previouslySelectedIndices: [],
|
||||
timer: undefined,
|
||||
teamNumberDialog: true
|
||||
}),
|
||||
computed: {
|
||||
needsTeamNumberSet: {
|
||||
get() {
|
||||
return this.$store.state.settings.networkSettings.teamNumber < 1
|
||||
&& this.teamNumberDialog && this.$store.state.backendConnected
|
||||
&& !this.$route.name.toLowerCase().includes("settings");
|
||||
}
|
||||
},
|
||||
compact: {
|
||||
get() {
|
||||
if (this.$store.state.compactMode === undefined) {
|
||||
return this.$vuetify.breakpoint.smAndDown;
|
||||
} else {
|
||||
return this.$store.state.compactMode || this.$vuetify.breakpoint.smAndDown;
|
||||
}
|
||||
},
|
||||
set(value) {
|
||||
// compactMode is the user's preference for compact mode; it overrides screen size
|
||||
this.$store.commit("compactMode", value);
|
||||
localStorage.setItem("compactMode", value);
|
||||
},
|
||||
},
|
||||
},
|
||||
created() {
|
||||
document.addEventListener("keydown", e => {
|
||||
switch (e.key) {
|
||||
case "`":
|
||||
this.$store.state.logsOverlay = !this.$store.state.logsOverlay;
|
||||
break;
|
||||
case "z":
|
||||
if (e.ctrlKey && this.$store.getters.canUndo) {
|
||||
this.$store.dispatch('undo', {vm: this});
|
||||
}
|
||||
break;
|
||||
case "y":
|
||||
if (e.ctrlKey && this.$store.getters.canRedo) {
|
||||
this.$store.dispatch('redo', {vm: this});
|
||||
}
|
||||
break;
|
||||
|
||||
}
|
||||
});
|
||||
|
||||
this.$options.sockets.onmessage = (data) => {
|
||||
try {
|
||||
let message = this.$msgPack.decode(data.data);
|
||||
for (let prop in message) {
|
||||
if (message.hasOwnProperty(prop)) {
|
||||
this.handleMessage(prop, message[prop]);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('error: ' + JSON.stringify(data.data) + " , " + error);
|
||||
}
|
||||
};
|
||||
this.$options.sockets.onopen = () => {
|
||||
this.$store.commit("backendConnected", true)
|
||||
this.$store.state.connectedCallbacks.forEach(it => it())
|
||||
};
|
||||
|
||||
let closed = () => {
|
||||
this.$store.commit("backendConnected", false)
|
||||
};
|
||||
this.$options.sockets.onclose = closed;
|
||||
this.$options.sockets.onerror = closed;
|
||||
|
||||
this.$connect();
|
||||
},
|
||||
methods: {
|
||||
handleMessage(key, value) {
|
||||
if (key === "logMessage") {
|
||||
this.logMessage(value["logMessage"], value["logLevel"]);
|
||||
} else if(key === "log"){
|
||||
this.logMessage(value["logMessage"]["logMessage"], value["logMessage"]["logLevel"]);
|
||||
} else if (key === "updatePipelineResult") {
|
||||
this.$store.commit('mutatePipelineResults', value)
|
||||
} else if (this.$store.state.hasOwnProperty(key)) {
|
||||
this.$store.commit(key, value);
|
||||
} else if (this.$store.getters.currentPipelineSettings.hasOwnProperty(key)) {
|
||||
this.$store.commit('mutatePipeline', {[key]: value});
|
||||
} else if (this.$store.state.settings.hasOwnProperty(key)) {
|
||||
this.$store.commit('mutateSettings', {[key]: value});
|
||||
} else {
|
||||
switch (key) {
|
||||
default: {
|
||||
console.error("Unknown message from backend: " + value);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
toggleCompactMode() {
|
||||
this.compact = !this.compact;
|
||||
},
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
logMessage(message, levelInt) {
|
||||
this.$store.commit('logString', {
|
||||
['level']: levelInt,
|
||||
['message']: message
|
||||
})
|
||||
},
|
||||
switchToDriverMode() {
|
||||
if (!this.previouslySelectedIndices) this.previouslySelectedIndices = [];
|
||||
|
||||
for (const [i, cameraSettings] of this.$store.state.cameraSettings.entries()) {
|
||||
this.previouslySelectedIndices[i] = cameraSettings.currentPipelineIndex;
|
||||
this.handleInputWithIndex('currentPipeline', -1, i);
|
||||
}
|
||||
},
|
||||
rollbackPipelineIndex()
|
||||
{
|
||||
if (this.previouslySelectedIndices !== null) {
|
||||
for (const [i] of this.$store.state.cameraSettings.entries()) {
|
||||
this.handleInputWithIndex('currentPipeline', this.previouslySelectedIndices[i] || 0, i);
|
||||
}
|
||||
}
|
||||
this.previouslySelectedIndices = null;
|
||||
},
|
||||
switchToSettingsTab() {
|
||||
this.axios.post('http://' + this.$address + '/api/sendMetrics', {})
|
||||
}
|
||||
name: 'App',
|
||||
components: {
|
||||
Logs
|
||||
},
|
||||
data: () => ({
|
||||
// Used so that we can switch back to the previously selected pipeline after camera calibration
|
||||
previouslySelectedIndices: [],
|
||||
timer: undefined,
|
||||
teamNumberDialog: true
|
||||
}),
|
||||
computed: {
|
||||
needsTeamNumberSet: {
|
||||
get() {
|
||||
return this.$store.state.settings.networkSettings.teamNumber < 1
|
||||
&& this.teamNumberDialog && this.$store.state.backendConnected
|
||||
&& !this.$route.name.toLowerCase().includes("settings");
|
||||
}
|
||||
},
|
||||
compact: {
|
||||
get() {
|
||||
if (this.$store.state.compactMode === undefined) {
|
||||
return this.$vuetify.breakpoint.smAndDown;
|
||||
} else {
|
||||
return this.$store.state.compactMode || this.$vuetify.breakpoint.smAndDown;
|
||||
}
|
||||
};
|
||||
},
|
||||
set(value) {
|
||||
// compactMode is the user's preference for compact mode; it overrides screen size
|
||||
this.$store.commit("compactMode", value);
|
||||
localStorage.setItem("compactMode", value);
|
||||
},
|
||||
},
|
||||
},
|
||||
created() {
|
||||
document.addEventListener("keydown", e => {
|
||||
switch (e.key) {
|
||||
case "`":
|
||||
this.$store.state.logsOverlay = !this.$store.state.logsOverlay;
|
||||
break;
|
||||
case "z":
|
||||
if (e.ctrlKey && this.$store.getters.canUndo) {
|
||||
this.$store.dispatch('undo', { vm: this });
|
||||
}
|
||||
break;
|
||||
case "y":
|
||||
if (e.ctrlKey && this.$store.getters.canRedo) {
|
||||
this.$store.dispatch('redo', { vm: this });
|
||||
}
|
||||
break;
|
||||
|
||||
}
|
||||
});
|
||||
|
||||
this.recreateWebsocket();
|
||||
},
|
||||
methods: {
|
||||
recreateWebsocket() {
|
||||
const wsDataURL = 'ws://' + this.$address + '/websocket_data';
|
||||
let socket = new WebSocket(wsDataURL);
|
||||
socket.binaryType = "arraybuffer";
|
||||
|
||||
socket.onmessage = (event) => {
|
||||
try {
|
||||
let message = this.$msgPack.decode(event.data);
|
||||
for (let prop in message) {
|
||||
if (message.hasOwnProperty(prop)) {
|
||||
this.handleMessage(prop, message[prop]);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(event)
|
||||
console.error('error: ' + JSON.stringify(event.data) + " , " + error);
|
||||
}
|
||||
};
|
||||
|
||||
socket.onerror = () => {
|
||||
socket.close();
|
||||
this.$store.commit("backendConnected", false)
|
||||
};
|
||||
|
||||
|
||||
socket.onopen = () => {
|
||||
clearInterval(this.timerId);
|
||||
|
||||
socket.onclose = () => {
|
||||
this.$store.commit("backendConnected", false)
|
||||
this.timerId = setInterval(() => {
|
||||
this.recreateWebsocket();
|
||||
}, 1000);
|
||||
};
|
||||
|
||||
|
||||
this.$store.commit("backendConnected", true)
|
||||
this.$store.state.connectedCallbacks.forEach(it => it())
|
||||
};
|
||||
|
||||
this.$store.commit("websocket", socket);
|
||||
},
|
||||
handleMessage(key, value) {
|
||||
if (key === "logMessage") {
|
||||
this.logMessage(value["logMessage"], value["logLevel"]);
|
||||
} else if (key === "log") {
|
||||
this.logMessage(value["logMessage"]["logMessage"], value["logMessage"]["logLevel"]);
|
||||
} else if (key === "updatePipelineResult") {
|
||||
this.$store.commit('mutatePipelineResults', value)
|
||||
} else if (this.$store.state.hasOwnProperty(key)) {
|
||||
this.$store.commit(key, value);
|
||||
} else if (this.$store.getters.currentPipelineSettings.hasOwnProperty(key)) {
|
||||
this.$store.commit('mutatePipeline', { [key]: value });
|
||||
} else if (this.$store.state.settings.hasOwnProperty(key)) {
|
||||
this.$store.commit('mutateSettings', { [key]: value });
|
||||
} else {
|
||||
console.error("Unknown message from backend: " + value);
|
||||
}
|
||||
},
|
||||
toggleCompactMode() {
|
||||
this.compact = !this.compact;
|
||||
},
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
logMessage(message, levelInt) {
|
||||
this.$store.commit('logString', {
|
||||
['level']: levelInt,
|
||||
['message']: message
|
||||
})
|
||||
},
|
||||
switchToDriverMode() {
|
||||
if (!this.previouslySelectedIndices) this.previouslySelectedIndices = [];
|
||||
|
||||
for (const [i, cameraSettings] of this.$store.state.cameraSettings.entries()) {
|
||||
this.previouslySelectedIndices[i] = cameraSettings.currentPipelineIndex;
|
||||
this.handleInputWithIndex('currentPipeline', -1, i);
|
||||
}
|
||||
},
|
||||
rollbackPipelineIndex() {
|
||||
if (this.previouslySelectedIndices !== null) {
|
||||
for (const [i] of this.$store.state.cameraSettings.entries()) {
|
||||
this.handleInputWithIndex('currentPipeline', this.previouslySelectedIndices[i] || 0, i);
|
||||
}
|
||||
}
|
||||
this.previouslySelectedIndices = null;
|
||||
},
|
||||
switchToSettingsTab() {
|
||||
this.axios.post('http://' + this.$address + '/api/sendMetrics', {})
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="sass">
|
||||
@@ -354,76 +290,77 @@ export default {
|
||||
</style>
|
||||
|
||||
<style>
|
||||
.pulse {
|
||||
animation: pulse-animation 2s infinite;
|
||||
}
|
||||
.pulse {
|
||||
animation: pulse-animation 2s infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse-animation {
|
||||
0% {
|
||||
box-shadow: 0 0 0 0px rgba(0, 0, 0, 0.2);
|
||||
background-color: rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
100% {
|
||||
box-shadow: 0 0 0 20px rgba(0, 0, 0, 0);
|
||||
background-color: rgba(0, 0, 0, 0);
|
||||
}
|
||||
}
|
||||
@keyframes pulse-animation {
|
||||
0% {
|
||||
box-shadow: 0 0 0 0px rgba(0, 0, 0, 0.2);
|
||||
background-color: rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.logo {
|
||||
width: 100%;
|
||||
height: 70px;
|
||||
object-fit: contain;
|
||||
}
|
||||
100% {
|
||||
box-shadow: 0 0 0 20px rgba(0, 0, 0, 0);
|
||||
background-color: rgba(0, 0, 0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
::-webkit-scrollbar {
|
||||
width: 0.5em;
|
||||
border-radius: 5px;
|
||||
}
|
||||
.logo {
|
||||
width: 100%;
|
||||
height: 70px;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
-webkit-box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.3);
|
||||
border-radius: 10px;
|
||||
}
|
||||
::-webkit-scrollbar {
|
||||
width: 0.5em;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
-webkit-box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.3);
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background-color: #ffd843;
|
||||
border-radius: 10px;
|
||||
}
|
||||
::-webkit-scrollbar-thumb {
|
||||
background-color: #ffd843;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.container {
|
||||
background-color: #232c37;
|
||||
padding: 0 !important;
|
||||
}
|
||||
.container {
|
||||
background-color: #232c37;
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
#title {
|
||||
color: #ffd843;
|
||||
}
|
||||
#title {
|
||||
color: #ffd843;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style>
|
||||
/* Hacks */
|
||||
/* Hacks */
|
||||
|
||||
.v-divider {
|
||||
border-color: white !important;
|
||||
}
|
||||
.v-divider {
|
||||
border-color: white !important;
|
||||
}
|
||||
|
||||
.v-input {
|
||||
font-size: 1rem !important;
|
||||
}
|
||||
.v-input {
|
||||
font-size: 1rem !important;
|
||||
}
|
||||
|
||||
/* This is unfortunately the only way to override table background color */
|
||||
.theme--dark.v-data-table > .v-data-table__wrapper > table > tbody > tr:hover:not(.v-data-table__expanded__content):not(.v-data-table__empty-wrapper) {
|
||||
background: #005281 !important;
|
||||
}
|
||||
/* This is unfortunately the only way to override table background color */
|
||||
.theme--dark.v-data-table>.v-data-table__wrapper>table>tbody>tr:hover:not(.v-data-table__expanded__content):not(.v-data-table__empty-wrapper) {
|
||||
background: #005281 !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss">
|
||||
@import '~vuetify/src/styles/settings/_variables';
|
||||
@import '~vuetify/src/styles/settings/_variables';
|
||||
|
||||
@media #{map-get($display-breakpoints, 'md-and-down')} {
|
||||
html {
|
||||
font-size: 14px !important;
|
||||
}
|
||||
}
|
||||
@media #{map-get($display-breakpoints, 'md-and-down')} {
|
||||
html {
|
||||
font-size: 14px !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
BIN
photon-client/src/assets/loading.gif
Normal file
BIN
photon-client/src/assets/loading.gif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 12 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 48 KiB After Width: | Height: | Size: 76 KiB |
@@ -5,8 +5,9 @@
|
||||
:style="styleObject"
|
||||
:src="src"
|
||||
alt=""
|
||||
@click="e => $emit('click', e)"
|
||||
>
|
||||
@click="clickHandler"
|
||||
@error="loadErrHandler"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
@@ -26,13 +27,14 @@
|
||||
"border-radius": "3px",
|
||||
"display": "block",
|
||||
"object-fit": "contain",
|
||||
"background-size:": "contain",
|
||||
"object-position": "50% 50%",
|
||||
"max-width": "100%",
|
||||
"margin-left": "auto",
|
||||
"margin-right": "auto",
|
||||
"max-height": this.maxHeight,
|
||||
height: `${this.scale}%`,
|
||||
cursor: (this.colorPicking ? `url(${require("../../assets/eyedropper.svg")}),` : "") + "default",
|
||||
cursor: (this.colorPicking ? `url(${require("../../assets/eyedropper.svg")}),` : "pointer") + "default",
|
||||
};
|
||||
|
||||
if (this.$vuetify.breakpoint.xl) {
|
||||
@@ -48,7 +50,14 @@
|
||||
},
|
||||
src: {
|
||||
get() {
|
||||
return this.disconnected ? require("../../assets/noStream.jpg") : this.address + "?" + this.seed // This prevents caching
|
||||
var port = this.getCurPort();
|
||||
if(port <= 0){
|
||||
//Invalid port, keep it spinny
|
||||
return require("../../assets/loading.gif");
|
||||
} else {
|
||||
//Valid port, connect
|
||||
return this.getSrcURLFromPort(port);
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -56,6 +65,43 @@
|
||||
this.reload(); // Force reload image on creation
|
||||
},
|
||||
methods: {
|
||||
getCurPort(){
|
||||
var port = -1;
|
||||
if(this.disconnected){
|
||||
//Disconnected, port is unknown.
|
||||
port = -1;
|
||||
} else {
|
||||
//Connected - get the port
|
||||
if(this.id == 'raw-stream'){
|
||||
port = this.$store.state.cameraSettings[this.$store.state.currentCameraIndex].inputStreamPort
|
||||
} else {
|
||||
port = this.$store.state.cameraSettings[this.$store.state.currentCameraIndex].outputStreamPort
|
||||
}
|
||||
}
|
||||
return port;
|
||||
},
|
||||
getSrcURLFromPort(port){
|
||||
return "http://" + location.hostname + ":" + port + "/stream.mjpg" + "?" + this.seed;
|
||||
},
|
||||
loadErrHandler(event) {
|
||||
console.log(event);
|
||||
console.log("Error loading image, attempting to do it again...");
|
||||
this.reload();
|
||||
},
|
||||
clickHandler(event) {
|
||||
if(this.colorPicking){
|
||||
this.$emit('click', event);
|
||||
} else {
|
||||
var port = this.getCurPort();
|
||||
if(port <= 0){
|
||||
console.log("No valid port, ignoring click.");
|
||||
} else {
|
||||
//Valid port, connect
|
||||
window.open(this.getSrcURLFromPort(port), '_blank');
|
||||
}
|
||||
}
|
||||
|
||||
},
|
||||
reload() {
|
||||
this.seed = new Date().getTime();
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
</v-row>
|
||||
</div>
|
||||
</template>
|
||||
s
|
||||
|
||||
<script>
|
||||
import TooltippedLabel from "./cv-tooltipped-label";
|
||||
|
||||
|
||||
@@ -181,7 +181,7 @@ export default {
|
||||
this.cubes = []
|
||||
|
||||
for (const target of this.targets) {
|
||||
const geometry = new BoxGeometry(0.2, 0.2, 0.3 / 5);
|
||||
const geometry = new BoxGeometry(0.3 / 5, 0.2, 0.2);
|
||||
const material = new MeshNormalMaterial();
|
||||
let quat = (new Quaternion(
|
||||
target.pose.qx,
|
||||
|
||||
@@ -15,16 +15,15 @@ if (process.env.NODE_ENV === "production") {
|
||||
Vue.prototype.$address = location.hostname + ":5800";
|
||||
}
|
||||
|
||||
const wsURL = '//' + Vue.prototype.$address + '/websocket';
|
||||
// const wsDataURL = '//' + Vue.prototype.$address + '/websocket_data';
|
||||
// import VueNativeSock from 'vue-native-websocket';
|
||||
// Vue.use(VueNativeSock, wsDataURL, {
|
||||
// reconnection: true,
|
||||
// reconnectionDelay: 100,
|
||||
// connectManually: true,
|
||||
// format: "arraybuffer",
|
||||
// });
|
||||
|
||||
import VueNativeSock from 'vue-native-websocket';
|
||||
|
||||
Vue.use(VueNativeSock, wsURL, {
|
||||
reconnection: true,
|
||||
reconnectionDelay: 100,
|
||||
connectManually: true,
|
||||
format: "arraybuffer",
|
||||
});
|
||||
Vue.use(VueAxios, axios);
|
||||
Vue.prototype.$msgPack = msgPack(true);
|
||||
|
||||
|
||||
@@ -2,14 +2,14 @@ export const dataHandleMixin = {
|
||||
methods: {
|
||||
handleInput(key, value) {
|
||||
let msg = this.$msgPack.encode({[key]: value});
|
||||
this.$socket.send(msg);
|
||||
this.$store.state.websocket.send(msg);
|
||||
},
|
||||
handleInputWithIndex(key, value, cameraIndex = this.$store.getters.currentCameraIndex) {
|
||||
let msg = this.$msgPack.encode({
|
||||
[key]: value,
|
||||
["cameraIndex"]: cameraIndex,
|
||||
});
|
||||
this.$socket.send(msg);
|
||||
this.$store.state.websocket.send(msg);
|
||||
},
|
||||
handleData(val) {
|
||||
this.handleInput(val, this[val]);
|
||||
@@ -22,7 +22,7 @@ export const dataHandleMixin = {
|
||||
["cameraIndex"]: this.$store.getters.currentCameraIndex
|
||||
}
|
||||
});
|
||||
this.$socket.send(msg);
|
||||
this.$store.state.websocket.send(msg);
|
||||
this.$emit('update')
|
||||
},
|
||||
handlePipelineUpdate(key, val) {
|
||||
@@ -32,7 +32,7 @@ export const dataHandleMixin = {
|
||||
["cameraIndex"]: this.$store.getters.currentCameraIndex
|
||||
}
|
||||
});
|
||||
this.$socket.send(msg);
|
||||
this.$store.state.websocket.send(msg);
|
||||
this.$emit('update')
|
||||
},
|
||||
handleTruthyPipelineData(val) {
|
||||
@@ -42,7 +42,7 @@ export const dataHandleMixin = {
|
||||
["cameraIndex"]: this.$store.getters.currentCameraIndex
|
||||
}
|
||||
});
|
||||
this.$socket.send(msg);
|
||||
this.$store.state.websocket.send(msg);
|
||||
this.$emit('update')
|
||||
},
|
||||
rollback(val, e) {
|
||||
|
||||
@@ -5,7 +5,7 @@ function initColorPicker() {
|
||||
if (!canvas)
|
||||
canvas = document.createElement('canvas');
|
||||
|
||||
image = document.querySelector('#normal-stream');
|
||||
image = document.querySelector('#raw-stream');
|
||||
if (image !== null) {
|
||||
canvas.width = image.width;
|
||||
canvas.height = image.height;
|
||||
|
||||
359
photon-client/src/plugins/WebsocketVideoStream.js
Normal file
359
photon-client/src/plugins/WebsocketVideoStream.js
Normal file
@@ -0,0 +1,359 @@
|
||||
// Circular buffer storage. Externally-apparent 'length' increases indefinitely
|
||||
// while any items with indexes below length-n will be forgotten (undefined
|
||||
// will be returned if you try to get them, trying to set is an exception).
|
||||
// n represents the initial length of the array, not a maximum
|
||||
class StatsHistoryBuffer{
|
||||
constructor (){
|
||||
this.windowLen = 10;
|
||||
this._array= new Array(this.windowLen);
|
||||
this.headPtr = 0;
|
||||
this.frameCount = 0;
|
||||
this.bitAvgAccum = 0;
|
||||
|
||||
//calculated vals
|
||||
this.bitRate_Mbps = 0;
|
||||
this.framerate_fps = 0;
|
||||
}
|
||||
|
||||
putAndPop(v){
|
||||
this.headPtr++;
|
||||
var idx = (this.headPtr)%this._array.length;
|
||||
var poppedVal = this._array[idx];
|
||||
this._array[idx] = v;
|
||||
return poppedVal;
|
||||
}
|
||||
|
||||
addSample(time, frameSize_bits, dispFrame_count) {
|
||||
var oldVal = this.putAndPop([time, frameSize_bits, dispFrame_count]);
|
||||
|
||||
this.bitAvgAccum += frameSize_bits;
|
||||
|
||||
if(oldVal !=null){
|
||||
var oldTime = oldVal[0];
|
||||
var oldFrameSize = oldVal[1];
|
||||
var oldFrameCount = oldVal[2];
|
||||
|
||||
var deltaTime_s = (time - oldTime);
|
||||
|
||||
this.bitAvgAccum -= oldFrameSize;
|
||||
|
||||
//bitrate - total bits transferred over the time period, divided by the period length
|
||||
// converted to mbps
|
||||
this.bitRate_Mbps = ( this.bitAvgAccum / deltaTime_s ) * (1.0/1048576.0);
|
||||
|
||||
//framerate - total frames displayed over the time period, divided by the period length
|
||||
this.framerate_fps = (dispFrame_count - oldFrameCount) / deltaTime_s;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
getText(){
|
||||
return "Streaming @ " + this.framerate_fps.toFixed(1) + "FPS " + this.bitRate_Mbps.toFixed(1) + "Mbps";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
export class WebsocketVideoStream{
|
||||
|
||||
constructor(drawDiv, streamPort, host) {
|
||||
console.log("host " + host + " port " + streamPort)
|
||||
|
||||
this.drawDiv = drawDiv;
|
||||
this.image = document.getElementById(this.drawDiv);
|
||||
this.streamPort = streamPort;
|
||||
this.newStreamPortReq = null;
|
||||
this.serverAddr = "ws://" + host + "/websocket_cameras";
|
||||
this.imgData = null;
|
||||
this.imgDataTime = -1;
|
||||
this.prevImgDataTime = -1;
|
||||
this.imgObjURL = null;
|
||||
this.frameRxCount = 0;
|
||||
this.dispFrameCount = 0;
|
||||
this.stats = null;
|
||||
|
||||
//Set up div for stream stats info provided for users
|
||||
this.statsTextDiv = this.image.parentNode.appendChild(document.createElement("div"));
|
||||
|
||||
//Centered over the image
|
||||
this.statsTextDiv.style.position = "absolute";
|
||||
this.statsTextDiv.style.left = "50%";
|
||||
this.statsTextDiv.style.top = "50%";
|
||||
this.statsTextDiv.style.transform = "translate(-50%, -50%)";
|
||||
|
||||
// Big enough for a line or two of text, with centered text
|
||||
this.statsTextDiv.style.padding = "0.5em"
|
||||
this.statsTextDiv.style.overflow = "hidden";
|
||||
this.statsTextDiv.style.textAlign = "center";
|
||||
this.statsTextDiv.style.verticalAlign = "middle";
|
||||
|
||||
// Styled to be black with grey text
|
||||
this.statsTextDiv.style.backgroundColor = "black";
|
||||
this.statsTextDiv.style.color = "#9E9E9E";
|
||||
this.statsTextDiv.style.borderRadius = "3px";
|
||||
|
||||
//Default no text
|
||||
this.statsTextDiv.innerHTML = "";
|
||||
|
||||
// Only show on mouseover, with opacity fade-in/fade-out
|
||||
this.statsTextDiv.style.opacity = "0.0";
|
||||
this.statsTextDiv.style.transition = "opacity 0.25s ease 0.25s";
|
||||
this.statsTextDiv.style.transitionDelay = "opacity 0.5s";
|
||||
this.image.addEventListener('mouseover', () => {this.statsTextDiv.style.opacity = "0.6";});
|
||||
this.statsTextDiv.addEventListener('mouseover', () => {this.statsTextDiv.style.opacity = "0.6";});
|
||||
this.image.addEventListener('mouseout', () => {this.statsTextDiv.style.opacity = "0.0";});
|
||||
|
||||
//Display state machine descriptions
|
||||
this.DSM_DISCONNECTED = "Disconnected";
|
||||
this.DSM_WAIT_FOR_VALID_PORT = "Waiting for valid port ID";
|
||||
this.DSM_SUBSCRIBE = "Subscribing";
|
||||
this.DSM_WAIT_FOR_FIRST_FRAME = "Waiting for frame data";
|
||||
this.DSM_SHOWING = "Showing Frames";
|
||||
this.DSM_RESTART_UNSUBSCRIBE = "Unsubscribing";
|
||||
this.DSM_RESTART_WAIT = "Waiting before resubscribe";
|
||||
|
||||
this.dsm_cur_state = this.DSM_DISCONNECTED;
|
||||
this.dsm_prev_state = this.DSM_DISCONNECTED;
|
||||
this.dsm_restart_start_time = window.performance.now();
|
||||
|
||||
this.dispNoStream();
|
||||
this.ws_connect();
|
||||
|
||||
requestAnimationFrame(()=>this.animationLoop());
|
||||
|
||||
}
|
||||
|
||||
dispImageData(){
|
||||
if(this.prevImgDataTime != this.imgDataTime){
|
||||
//From https://stackoverflow.com/questions/67507616/set-image-src-from-image-blob/67507685#67507685
|
||||
//Ensure uniqueness by making the new one before revoking the old one.
|
||||
var oldURL = this.imgObjURL
|
||||
this.imgObjURL = URL.createObjectURL(this.imgData);
|
||||
if(oldURL != null){
|
||||
URL.revokeObjectURL(oldURL)
|
||||
}
|
||||
|
||||
//Update the image with the new mimetype and image
|
||||
this.image.src = this.imgObjURL;
|
||||
|
||||
this.dispFrameCount++;
|
||||
this.prevImgDataTime = this.imgDataTime;
|
||||
} // else no new image, don't update anything
|
||||
}
|
||||
|
||||
dispNoStream() {
|
||||
this.image.src = require("../assets/loading.gif");
|
||||
}
|
||||
|
||||
animationLoop(){
|
||||
// Update time metrics
|
||||
var curTime_s = window.performance.now() / 1000.0;
|
||||
var timeInState = curTime_s - this.dsm_restart_start_time;
|
||||
|
||||
// Save previous state
|
||||
this.dsm_prev_state = this.dsm_cur_state;
|
||||
|
||||
// Evaluate state transitions
|
||||
if(this.serverConnectionActive == false){
|
||||
//Any state - if the server connection goes false, always transition to disconnected
|
||||
this.dsm_cur_state = this.DSM_DISCONNECTED;
|
||||
} else {
|
||||
//Conditional transitions
|
||||
switch(this.dsm_cur_state) {
|
||||
case this.DSM_DISCONNECTED:
|
||||
//Immediately transition to waiting for the first frame
|
||||
this.dsm_cur_state = this.DSM_WAIT_FOR_VALID_PORT;
|
||||
break;
|
||||
case this.DSM_WAIT_FOR_VALID_PORT:
|
||||
// Wait until the user has configured a valid port
|
||||
if(this.streamPort > 0){
|
||||
this.dsm_cur_state = this.DSM_SUBSCRIBE;
|
||||
} else {
|
||||
this.dsm_cur_state = this.DSM_WAIT_FOR_VALID_PORT;
|
||||
}
|
||||
break;
|
||||
case this.DSM_SUBSCRIBE:
|
||||
// Immediately transition after subscriptions is sent
|
||||
this.dsm_cur_state = this.DSM_WAIT_FOR_FIRST_FRAME;
|
||||
break;
|
||||
case this.DSM_WAIT_FOR_FIRST_FRAME:
|
||||
if(this.imgData != null){
|
||||
//we got some image data, start showing it
|
||||
this.dsm_cur_state = this.DSM_SHOWING;
|
||||
} else if (this.newStreamPortReq != null){
|
||||
//Stream port requested changed, unsubscribe and restart
|
||||
this.dsm_cur_state = this.DSM_RESTART_UNSUBSCRIBE;
|
||||
} else {
|
||||
this.dsm_cur_state = this.DSM_WAIT_FOR_FIRST_FRAME;
|
||||
}
|
||||
break;
|
||||
case this.DSM_SHOWING:
|
||||
if((curTime_s - this.imgDataTime) > 2.5){
|
||||
//timeout, begin the restart sequence
|
||||
this.dsm_cur_state = this.DSM_RESTART_UNSUBSCRIBE;
|
||||
} else if (this.newStreamPortReq != null){
|
||||
//Stream port requested changed, unsubscribe and restart
|
||||
this.dsm_cur_state = this.DSM_RESTART_UNSUBSCRIBE;
|
||||
} else {
|
||||
//stay in this state.
|
||||
this.dsm_cur_state = this.DSM_SHOWING;
|
||||
}
|
||||
break;
|
||||
case this.DSM_RESTART_UNSUBSCRIBE:
|
||||
//Only should spend one loop in Unsubscribe, immediately transition
|
||||
this.dsm_cur_state = this.DSM_RESTART_WAIT;
|
||||
break;
|
||||
case this.DSM_RESTART_WAIT:
|
||||
if (timeInState > 0.25) {
|
||||
//we've waited long enough, go to try to re-subscribe
|
||||
this.dsm_cur_state = this.DSM_WAIT_FOR_VALID_PORT;
|
||||
} else {
|
||||
//stay in this state.
|
||||
this.dsm_cur_state = this.DSM_RESTART_WAIT;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
// Shouldn't get here, default back to init
|
||||
this.dsm_cur_state = this.DSM_DISCONNECTED;
|
||||
}
|
||||
}
|
||||
|
||||
//take current-state or state-transition actions
|
||||
|
||||
if(this.dsm_cur_state != this.dsm_prev_state){
|
||||
//Any state transition
|
||||
console.log("State Change: " + this.dsm_prev_state + " -> " + this.dsm_cur_state);
|
||||
}
|
||||
|
||||
if(this.dsm_cur_state == this.DSM_SHOWING){
|
||||
// Currently in SHOWING
|
||||
// Show image and update status text
|
||||
this.dispImageData();
|
||||
this.statsTextDiv.innerHTML = this.stats.getText();
|
||||
} else {
|
||||
//Just show the state for debug
|
||||
this.statsTextDiv.innerHTML = this.dsm_cur_state;
|
||||
}
|
||||
|
||||
if(this.dsm_cur_state != this.DSM_SHOWING && this.dsm_prev_state == this.DSM_SHOWING ){
|
||||
//Any transition out of showing - no stream
|
||||
this.dispNoStream();
|
||||
}
|
||||
|
||||
if(this.dsm_cur_state == this.DSM_RESTART_UNSUBSCRIBE){
|
||||
// Currently in UNSUBSCRIBE, do the unsubscribe actions
|
||||
this.stopStream();
|
||||
this.dsm_restart_start_time = curTime_s;
|
||||
}
|
||||
|
||||
if(this.dsm_cur_state == this.DSM_SUBSCRIBE){
|
||||
// Currently in SUBSCRIBE, do the subscribe actions
|
||||
this.startStream();
|
||||
this.dsm_restart_start_time = curTime_s;
|
||||
}
|
||||
|
||||
if(this.dsm_cur_state == this.DSM_WAIT_FOR_VALID_PORT){
|
||||
// Currently waiting for a vaild port to be requested
|
||||
if(this.newStreamPortReq != null){
|
||||
this.streamPort = this.newStreamPortReq;
|
||||
this.newStreamPortReq = null;
|
||||
}
|
||||
}
|
||||
|
||||
//Update status text
|
||||
|
||||
requestAnimationFrame(()=>this.animationLoop());
|
||||
}
|
||||
|
||||
startStream() {
|
||||
console.log("Subscribing to port " + this.streamPort);
|
||||
this.imgData = null;
|
||||
this.ws.send(JSON.stringify({"cmd": "subscribe", "port":this.streamPort}));
|
||||
}
|
||||
|
||||
stopStream() {
|
||||
console.log("Unsubscribing");
|
||||
this.ws.send(JSON.stringify({"cmd": "unsubscribe"}));
|
||||
this.imgData = null;
|
||||
}
|
||||
|
||||
setPort(streamPort){
|
||||
console.log("Port set to " + streamPort);
|
||||
this.newStreamPortReq = streamPort;
|
||||
}
|
||||
|
||||
ws_onOpen() {
|
||||
// Set the flag allowing general server communication
|
||||
this.serverConnectionActive = true;
|
||||
console.log("Camera Websockets Connected!");
|
||||
|
||||
// New websocket connection, reset stats
|
||||
this.frameRxCount = 0;
|
||||
this.dispFrameCount = 0;
|
||||
this.stats = new StatsHistoryBuffer();
|
||||
}
|
||||
|
||||
ws_onClose(e) {
|
||||
//Clear flags to stop server communication
|
||||
this.ws = null;
|
||||
this.serverConnectionActive = false;
|
||||
|
||||
console.log('Camera Socket is closed. Reconnect will be attempted in 0.5 second.', e.reason);
|
||||
setTimeout(this.ws_connect.bind(this), 500);
|
||||
|
||||
if(!e.wasClean){
|
||||
console.error('Socket encountered error!');
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
ws_onError(e){
|
||||
e; //prevent unused failure
|
||||
this.ws.close();
|
||||
}
|
||||
|
||||
ws_onMessage(e){
|
||||
//console.log("Got message from " + this.serverAddr)
|
||||
var msgTime_s = window.performance.now() / 1000.0;
|
||||
if(typeof e.data === 'string'){
|
||||
//string data from host
|
||||
//TODO - anything to receive info here? Maybe "available streams?"
|
||||
} else {
|
||||
if(e.data.size > 0){
|
||||
//binary data - a frame!
|
||||
//Save frame data for display in the next animation thread
|
||||
this.imgData = e.data;
|
||||
this.imgDataTime = msgTime_s;
|
||||
|
||||
//Count the incoming frame
|
||||
this.frameRxCount++;
|
||||
|
||||
//keep the stats up to date
|
||||
this.stats.addSample(msgTime_s,this.imgData.size * 8,this.dispFrameCount);
|
||||
} else {
|
||||
console.log("WS Stream Error: Server sent empty frame!");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
ws_connect() {
|
||||
this.serverConnectionActive = false;
|
||||
this.ws = new WebSocket(this.serverAddr);
|
||||
this.ws.binaryType = "blob";
|
||||
this.ws.onopen = this.ws_onOpen.bind(this);
|
||||
this.ws.onmessage = this.ws_onMessage.bind(this);
|
||||
this.ws.onclose = this.ws_onClose.bind(this);
|
||||
this.ws.onerror = this.ws_onError.bind(this);
|
||||
console.log("Connecting to server " + this.serverAddr);
|
||||
}
|
||||
|
||||
ws_close(){
|
||||
this.ws.close();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
export default {WebsocketVideoStream}
|
||||
@@ -15,6 +15,7 @@ export default new Vuex.Store({
|
||||
},
|
||||
state: {
|
||||
backendConnected: false,
|
||||
websocket: null,
|
||||
ntConnectionInfo: {
|
||||
connected: false,
|
||||
address: "",
|
||||
@@ -35,8 +36,8 @@ export default new Vuex.Store({
|
||||
tiltDegrees: 0.0,
|
||||
currentPipelineIndex: 0,
|
||||
pipelineNicknames: ["Unknown"],
|
||||
outputStreamPort: 1181,
|
||||
inputStreamPort: 1182,
|
||||
outputStreamPort: 0,
|
||||
inputStreamPort: 0,
|
||||
nickname: "Unknown",
|
||||
videoFormatList: [
|
||||
{
|
||||
@@ -97,6 +98,8 @@ export default new Vuex.Store({
|
||||
debug: false,
|
||||
refineEdges: true,
|
||||
numIterations: 1,
|
||||
decisionMargin: 0,
|
||||
hammingDist: 0,
|
||||
}
|
||||
}
|
||||
],
|
||||
@@ -167,6 +170,7 @@ export default new Vuex.Store({
|
||||
},
|
||||
mutations: {
|
||||
compactMode: set('compactMode'),
|
||||
websocket: set('websocket'),
|
||||
cameraSettings: set('cameraSettings'),
|
||||
currentCameraIndex: set('currentCameraIndex'),
|
||||
selectedOutputs: set('selectedOutputs'),
|
||||
|
||||
@@ -31,14 +31,6 @@
|
||||
:label-cols="$vuetify.breakpoint.mdAndUp ? undefined : 7"
|
||||
/>
|
||||
<br>
|
||||
<CVnumberinput
|
||||
v-model="cameraSettings.tiltDegrees"
|
||||
name="Camera pitch"
|
||||
tooltip="How many degrees above the horizontal the physical camera is tilted"
|
||||
:step="0.01"
|
||||
:label-cols="$vuetify.breakpoint.mdAndUp ? undefined : 7"
|
||||
/>
|
||||
<br>
|
||||
<v-btn
|
||||
style="margin-top:10px"
|
||||
small
|
||||
@@ -202,10 +194,13 @@
|
||||
>
|
||||
<CVslider
|
||||
v-model="$store.getters.currentPipelineSettings.cameraExposure"
|
||||
:disabled="$store.getters.currentPipelineSettings.cameraAutoExposure"
|
||||
name="Exposure"
|
||||
:min="0"
|
||||
:max="100"
|
||||
slider-cols="8"
|
||||
step="0.1"
|
||||
tooltip="Directly controls how much light is allowed to fall onto the sensor, which affects apparent brightness"
|
||||
@input="e => handlePipelineUpdate('cameraExposure', e)"
|
||||
/>
|
||||
<CVslider
|
||||
@@ -216,6 +211,24 @@
|
||||
slider-cols="8"
|
||||
@input="e => handlePipelineUpdate('cameraBrightness', e)"
|
||||
/>
|
||||
<CVswitch
|
||||
v-model="$store.getters.currentPipelineSettings.cameraAutoExposure"
|
||||
class="pt-2"
|
||||
name="Auto Exposure"
|
||||
tooltip="Enables or Disables camera automatic adjustment for current lighting conditions"
|
||||
@input="e => handlePipelineUpdate('cameraAutoExposure', e)"
|
||||
/>
|
||||
<CVslider
|
||||
v-if="cameraGain >= 0"
|
||||
v-model="cameraGain"
|
||||
name="Camera Gain"
|
||||
min="0"
|
||||
max="100"
|
||||
tooltip="Controls camera gain, similar to brightness"
|
||||
:slider-cols="largeBox"
|
||||
@input="handlePipelineData('cameraGain')"
|
||||
@rollback="e => rollback('cameraGain', e)"
|
||||
/>
|
||||
<CVslider
|
||||
v-if="$store.getters.currentPipelineSettings.cameraRedGain !== -1"
|
||||
v-model="$store.getters.currentPipelineSettings.cameraRedGain"
|
||||
@@ -289,7 +302,8 @@
|
||||
>
|
||||
<template>
|
||||
<CVimage
|
||||
:address="$store.getters.streamAddress[1]"
|
||||
:id="cameras-cal"
|
||||
:idx=1
|
||||
:disconnected="!$store.state.backendConnected"
|
||||
scale="100"
|
||||
style="border-radius: 5px;"
|
||||
@@ -360,6 +374,7 @@
|
||||
import CVselect from '../components/common/cv-select';
|
||||
import CVnumberinput from '../components/common/cv-number-input';
|
||||
import CVslider from '../components/common/cv-slider';
|
||||
import CVswitch from '../components/common/cv-switch';
|
||||
import CVimage from "../components/common/cv-image";
|
||||
import TooltippedLabel from "../components/common/cv-tooltipped-label";
|
||||
import jsPDF from "jspdf";
|
||||
@@ -372,6 +387,7 @@ export default {
|
||||
CVselect,
|
||||
CVnumberinput,
|
||||
CVslider,
|
||||
CVswitch,
|
||||
CVimage
|
||||
},
|
||||
data() {
|
||||
@@ -405,6 +421,15 @@ export default {
|
||||
}
|
||||
},
|
||||
|
||||
cameraGain: {
|
||||
get() {
|
||||
return parseInt(this.$store.getters.currentPipelineSettings.cameraGain)
|
||||
},
|
||||
set(val) {
|
||||
this.$store.commit("mutatePipeline", {"cameraGain": parseInt(val)});
|
||||
}
|
||||
},
|
||||
|
||||
// Makes sure there's only one entry per resolution
|
||||
filteredResolutionList: {
|
||||
get() {
|
||||
@@ -428,13 +453,11 @@ export default {
|
||||
return filtered
|
||||
}
|
||||
},
|
||||
|
||||
stringResolutionList: {
|
||||
get() {
|
||||
return this.filteredResolutionList.map(res => `${res['width']} X ${res['height']}`);
|
||||
}
|
||||
},
|
||||
|
||||
cameraSettings: {
|
||||
get() {
|
||||
return this.$store.getters.currentCameraSettings;
|
||||
@@ -443,7 +466,6 @@ export default {
|
||||
this.$store.commit('cameraSettings', value);
|
||||
}
|
||||
},
|
||||
|
||||
boardType: {
|
||||
get() {
|
||||
return this.calibrationData.boardType
|
||||
@@ -625,8 +647,7 @@ export default {
|
||||
this.axios.post("http://" + this.$address + "/api/settings/camera", {
|
||||
"settings": this.cameraSettings,
|
||||
"index": this.$store.state.currentCameraIndex
|
||||
}).then(
|
||||
function (response) {
|
||||
}).then(response => {
|
||||
if (response.status === 200) {
|
||||
this.$store.state.saveBar = true;
|
||||
}
|
||||
@@ -647,14 +668,15 @@ export default {
|
||||
if (this.isCalibrating === true) {
|
||||
data['takeCalibrationSnapshot'] = true
|
||||
} else {
|
||||
// This store prevents an edge case of a user not selecting a different resolution, which causes the set logic to not be called
|
||||
this.$store.commit('mutateCalibrationState', {['videoModeIndex']: this.filteredResolutionList[this.selectedFilteredResIndex].index});
|
||||
const calData = this.calibrationData;
|
||||
calData.isCalibrating = true;
|
||||
data['startPnpCalibration'] = calData;
|
||||
|
||||
console.log("starting calibration with index " + calData.videoModeIndex);
|
||||
}
|
||||
|
||||
this.$socket.send(this.$msgPack.encode(data));
|
||||
this.$store.commit('currentPipelineIndex', -2);
|
||||
this.$store.state.websocket.send(this.$msgPack.encode(data));
|
||||
},
|
||||
sendCalibrationFinish() {
|
||||
console.log("finishing calibration for index " + this.$store.getters.currentCameraIndex);
|
||||
|
||||
@@ -33,10 +33,10 @@
|
||||
:color="fpsTooLow ? 'error' : 'transparent'"
|
||||
:text-color="fpsTooLow ? 'white' : 'grey'"
|
||||
>
|
||||
<span class="pr-1">{{ Math.round($store.state.pipelineResults.fps) }} FPS –</span>
|
||||
<span v-if="!fpsTooLow">{{ Math.min(Math.round($store.state.pipelineResults.latency), 9999) }} ms latency</span>
|
||||
<span v-else-if="!$store.getters.currentPipelineSettings.inputShouldShow">HSV thresholds are too broad; narrow them for better performance</span>
|
||||
<span v-else>stop viewing the raw stream for better performance</span>
|
||||
<span class="pr-1">Processing @ {{ Math.round($store.state.pipelineResults.fps) }} FPS –</span>
|
||||
<span v-if="fpsTooLow && !$store.getters.currentPipelineSettings.inputShouldShow && $store.getters.pipelineType == 2">HSV thresholds are too broad; narrow them for better performance</span>
|
||||
<span v-else-if="$fpsTooLow && getters.currentCameraSettings.inputShouldShow">stop viewing the raw stream for better performance</span>
|
||||
<span v-else>{{ Math.min(Math.round($store.state.pipelineResults.latency), 9999) }} ms latency</span>
|
||||
</v-chip>
|
||||
<v-switch
|
||||
v-model="driverMode"
|
||||
@@ -58,16 +58,16 @@
|
||||
>
|
||||
<div style="position: relative; width: 100%; height: 100%;">
|
||||
<cv-image
|
||||
:id="idx === 0 ? 'normal-stream' : ''"
|
||||
:id="idx === 0 ? 'raw-stream' : 'processed-stream'"
|
||||
ref="streams"
|
||||
:address="$store.getters.streamAddress[idx]"
|
||||
:idx=idx
|
||||
:disconnected="!$store.state.backendConnected"
|
||||
scale="100"
|
||||
:max-height="$store.getters.isDriverMode ? '40vh' : '300px'"
|
||||
:max-height-md="$store.getters.isDriverMode ? '50vh' : '380px'"
|
||||
:max-height-lg="$store.getters.isDriverMode ? '55vh' : '390px'"
|
||||
:max-height-xl="$store.getters.isDriverMode ? '60vh' : '450px'"
|
||||
:alt="'Stream' + idx"
|
||||
:alt="'Stream ' + idx"
|
||||
:color-picking="$store.state.colorPicking && idx === 0"
|
||||
@click="onImageClick"
|
||||
/>
|
||||
@@ -85,7 +85,7 @@
|
||||
<v-card
|
||||
color="primary"
|
||||
>
|
||||
<camera-and-pipeline-select @camera-name-changed="reloadStreams" />
|
||||
<camera-and-pipeline-select />
|
||||
</v-card>
|
||||
<v-card
|
||||
:disabled="$store.getters.isDriverMode || $store.state.colorPicking"
|
||||
|
||||
@@ -7,16 +7,27 @@
|
||||
item-color="secondary"
|
||||
label="Select target family"
|
||||
:items="familyList"
|
||||
@input="handlePipelineUpdate('tagFamily', targetList.indexOf(selectedModel))"
|
||||
@input="handlePipelineUpdate('tagFamily', familyList.indexOf(selectedFamily))"
|
||||
/>
|
||||
<v-select
|
||||
v-model="selectedModel"
|
||||
dark
|
||||
color="accent"
|
||||
item-color="secondary"
|
||||
label="Select a target model"
|
||||
:items="targetList"
|
||||
item-text="name"
|
||||
item-value="data"
|
||||
@input="handlePipelineUpdate('targetModel', targetList.indexOf(selectedModel) + 6)"
|
||||
/>
|
||||
<CVslider
|
||||
v-model="decimate"
|
||||
class="pt-2"
|
||||
slider-cols="8"
|
||||
name="Decimate"
|
||||
min="0"
|
||||
max="3"
|
||||
step=".5"
|
||||
min="1"
|
||||
max="8"
|
||||
step="1.0"
|
||||
tooltip="Increases FPS at the expense of range by reducing image resolution initially"
|
||||
@input="handlePipelineData('decimate')"
|
||||
/>
|
||||
@@ -50,6 +61,28 @@
|
||||
tooltip="Further refines the apriltag corner position initial estimate, suggested left on"
|
||||
@input="handlePipelineData('refineEdges')"
|
||||
/>
|
||||
<CVslider
|
||||
v-model="hammingDist"
|
||||
class="pt-2 pb-4"
|
||||
slider-cols="8"
|
||||
name="Max error bits"
|
||||
min="0"
|
||||
max="10"
|
||||
step="1"
|
||||
tooltip="Maximum number of error bits to correct; potential tags with more will be thrown out. For smaller tags (like 16h5), set this as low as possible."
|
||||
@input="handlePipelineData('hammingDist')"
|
||||
/>
|
||||
<CVslider
|
||||
v-model="decisionMargin"
|
||||
class="pt-2 pb-4"
|
||||
slider-cols="8"
|
||||
name="Decision Margin Cutoff"
|
||||
min="0"
|
||||
max="250"
|
||||
step="1"
|
||||
tooltip="Tags with a 'margin' (decoding quality score) less than this wil be rejected. Increase this to reduce the number of false positive detections"
|
||||
@input="handlePipelineData('decisionMargin')"
|
||||
/>
|
||||
<CVslider
|
||||
v-model="numIterations"
|
||||
class="pt-2 pb-4"
|
||||
@@ -76,10 +109,21 @@
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
familyList: ["tag36h11"],
|
||||
familyList: ["tag36h11", "tag25h9", "tag16h5"],
|
||||
// Selected model is offset (ew) by 6 from the photon ordinal, as we only wanna show the 36h11 and 16h5 options
|
||||
targetList: ['6.5in (36h11) AprilTag', '6in (16h5) AprilTag'], //Keep in sync with TargetModel.java
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
selectedModel: {
|
||||
get() {
|
||||
let ret = this.$store.getters.currentPipelineSettings.targetModel - 6
|
||||
return this.targetList[ret];
|
||||
},
|
||||
set(val) {
|
||||
this.$store.commit("mutatePipeline", {"targetModel": this.targetList.indexOf(val) + 6})
|
||||
}
|
||||
},
|
||||
selectedFamily: {
|
||||
get() {
|
||||
let ret = this.$store.getters.currentPipelineSettings.tagFamily
|
||||
@@ -97,6 +141,22 @@
|
||||
this.$store.commit("mutatePipeline", {"decimate": val});
|
||||
}
|
||||
},
|
||||
hammingDist: {
|
||||
get() {
|
||||
return this.$store.getters.currentPipelineSettings.hammingDist
|
||||
},
|
||||
set(val) {
|
||||
this.$store.commit("mutatePipeline", {"hammingDist": val});
|
||||
}
|
||||
},
|
||||
decisionMargin: {
|
||||
get() {
|
||||
return this.$store.getters.currentPipelineSettings.decisionMargin
|
||||
},
|
||||
set(val) {
|
||||
this.$store.commit("mutatePipeline", {"decisionMargin": val});
|
||||
}
|
||||
},
|
||||
numIterations: {
|
||||
get() {
|
||||
return this.$store.getters.currentPipelineSettings.numIterations
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
<template>
|
||||
<div>
|
||||
<CVslider
|
||||
:disabled="cameraAutoExposure"
|
||||
v-model="cameraExposure"
|
||||
:disabled="cameraAutoExposure"
|
||||
name="Exposure"
|
||||
min="0"
|
||||
max="100"
|
||||
step="0.1"
|
||||
tooltip="Directly controls how much light is allowed to fall onto the sensor, which affects brightness"
|
||||
tooltip="Directly controls how much light is allowed to fall onto the sensor, which affects apparent brightness"
|
||||
:slider-cols="largeBox"
|
||||
@input="handlePipelineData('cameraExposure')"
|
||||
@rollback="e => rollback('cameraExposure', e)"
|
||||
@@ -25,19 +25,20 @@
|
||||
<CVswitch
|
||||
v-model="cameraAutoExposure"
|
||||
class="pt-2"
|
||||
name="Auto exposure"
|
||||
name="Auto Exposure"
|
||||
tooltip="Enables or Disables camera automatic adjustment for current lighting conditions"
|
||||
@input="handlePipelineData('cameraAutoExposure')"
|
||||
/>
|
||||
<CVslider
|
||||
v-if="cameraGain >= 0"
|
||||
v-model="cameraGain"
|
||||
name="Camera gain"
|
||||
name="Camera Gain"
|
||||
min="0"
|
||||
max="100"
|
||||
tooltip="Controls camera gain, similar to brightness"
|
||||
:slider-cols="largeBox"
|
||||
@input="handlePipelineData('cameraRedGain')"
|
||||
@rollback="e => rollback('cameraRedGain', e)"
|
||||
@input="handlePipelineData('cameraGain')"
|
||||
@rollback="e => rollback('cameraGain', e)"
|
||||
/>
|
||||
<CVslider
|
||||
v-if="cameraRedGain !== -1"
|
||||
@@ -106,11 +107,6 @@
|
||||
},
|
||||
// eslint-disable-next-line vue/require-prop-types
|
||||
props: ['value'],
|
||||
data() {
|
||||
return {
|
||||
rawStreamDivisorIndex: 0,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
largeBox: {
|
||||
get() {
|
||||
@@ -144,6 +140,14 @@
|
||||
this.$store.commit("mutatePipeline", {"cameraBrightness": parseInt(val)});
|
||||
}
|
||||
},
|
||||
cameraGain: {
|
||||
get() {
|
||||
return parseInt(this.$store.getters.currentPipelineSettings.cameraGain)
|
||||
},
|
||||
set(val) {
|
||||
this.$store.commit("mutatePipeline", {"cameraGain": parseInt(val)});
|
||||
}
|
||||
},
|
||||
cameraRedGain: {
|
||||
get() {
|
||||
return parseInt(this.$store.getters.currentPipelineSettings.cameraRedGain)
|
||||
@@ -176,15 +180,22 @@
|
||||
this.$store.commit("mutatePipeline", {"cameraVideoModeIndex": val});
|
||||
|
||||
this.handlePipelineUpdate("streamingFrameDivisor", this.getNumSkippedStreamDivisors());
|
||||
this.rawStreamDivisorIndex = 0;
|
||||
this.$store.commit("mutatePipeline", {"streamingFrameDivisor": 0});
|
||||
|
||||
// If we don't have 3d mode calibrated at the new resolution either, we should disable it here
|
||||
// (TODO) This probably belongs in the backend (Matt)
|
||||
if (!this.$store.getters.isCalibrated) {
|
||||
this.handlePipelineUpdate("solvePNPEnabled", false);
|
||||
this.$store.commit("mutatePipeline", {"solvePNPEnabled": false});
|
||||
}
|
||||
}
|
||||
},
|
||||
streamingFrameDivisor: {
|
||||
get() {
|
||||
return this.rawStreamDivisorIndex;
|
||||
return this.$store.getters.currentPipelineSettings.streamingFrameDivisor;
|
||||
},
|
||||
set(val) {
|
||||
this.rawStreamDivisorIndex = val;
|
||||
this.$store.commit("mutatePipeline", {"streamingFrameDivisor": val});
|
||||
this.handlePipelineUpdate("streamingFrameDivisor", this.getNumSkippedStreamDivisors() + val);
|
||||
}
|
||||
},
|
||||
|
||||
@@ -53,7 +53,7 @@
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
targetList: ['2020 High Goal Outer', '2020 High Goal Inner', '2019 Dual Target', '2020 Power Cell (7in)','2022 Cargo Ball (9.5in)', '2016 High Goal'], //Keep in sync with TargetModel.java
|
||||
targetList: ['2020 High Goal Outer', '2020 High Goal Inner', '2019 Dual Target', '2020 Power Cell (7in)','2022 Cargo Ball (9.5in)', '2016 High Goal', '6.5in (36h11) AprilTag', '6in (16h5) AprilTag'], //Keep in sync with TargetModel.java
|
||||
snackbar: {
|
||||
color: "Success",
|
||||
text: ""
|
||||
@@ -65,7 +65,6 @@
|
||||
selectedModel: {
|
||||
get() {
|
||||
let ret = this.$store.getters.currentPipelineSettings.targetModel
|
||||
console.log(ret)
|
||||
return this.targetList[ret];
|
||||
},
|
||||
set(val) {
|
||||
|
||||
@@ -50,7 +50,7 @@
|
||||
</th>
|
||||
</template>
|
||||
<template v-if="$store.getters.pipelineType === 4 && $store.getters.currentPipelineSettings.solvePNPEnabled">
|
||||
<th class="text-center" >
|
||||
<th class="text-center">
|
||||
Ambiguity
|
||||
</th>
|
||||
</template>
|
||||
@@ -82,9 +82,9 @@
|
||||
<td>{{ (parseFloat(value.pose.angle_z) * 180 / Math.PI).toFixed(2) }}°</td>
|
||||
</template>
|
||||
<template v-if="$store.getters.pipelineType === 4 && $store.getters.currentPipelineSettings.solvePNPEnabled">
|
||||
<td>
|
||||
{{ parseFloat(value.ambiguity).toFixed(2) }}
|
||||
</td>
|
||||
<td>
|
||||
{{ parseFloat(value.ambiguity).toFixed(2) }}
|
||||
</td>
|
||||
</template>
|
||||
</tr>
|
||||
</tbody>
|
||||
|
||||
@@ -247,7 +247,7 @@ export default {
|
||||
'cameraIndex': this.$store.state.currentCameraIndex
|
||||
}
|
||||
});
|
||||
this.$socket.send(msg);
|
||||
this.$store.state.websocket.send(msg);
|
||||
this.$emit('update');
|
||||
}
|
||||
},
|
||||
|
||||
@@ -49,22 +49,46 @@
|
||||
<th class="infoElem">
|
||||
Disk Usage
|
||||
</th>
|
||||
<th class="infoElem">
|
||||
<v-tooltip top>
|
||||
<template v-slot:activator="{ on, attrs }">
|
||||
<span
|
||||
v-bind="attrs"
|
||||
v-on="on"
|
||||
>
|
||||
ⓘ CPU Throttling
|
||||
</span>
|
||||
</template>
|
||||
<span>
|
||||
Current or Previous Reason for the cpu being held back from maximum performance.
|
||||
</span>
|
||||
</v-tooltip>
|
||||
</th>
|
||||
<th class="infoElem">
|
||||
CPU Uptime
|
||||
</th>
|
||||
</tr>
|
||||
<tr v-if="metrics.cpuUtil !== 'N/A'">
|
||||
<td class="infoElem">
|
||||
{{ metrics.cpuUtil.replace(" ", "") }}%
|
||||
{{ metrics.cpuUtil }}%
|
||||
</td>
|
||||
<td class="infoElem">
|
||||
{{ parseInt(metrics.cpuTemp) }}° C
|
||||
</td>
|
||||
<td class="infoElem">
|
||||
{{ metrics.ramUtil.replace(" ", "") }}MB of {{ metrics.cpuMem }}MB
|
||||
{{ metrics.ramUtil }}MB of {{ metrics.cpuMem }}MB
|
||||
</td>
|
||||
<td class="infoElem">
|
||||
{{ metrics.gpuMemUtil.replace(" ", "") }}MB of {{ metrics.gpuMem }}MB
|
||||
{{ metrics.gpuMemUtil }}MB of {{ metrics.gpuMem }}MB
|
||||
</td>
|
||||
<td class="infoElem">
|
||||
{{ metrics.diskUtilPct.replace(" ", "") }}
|
||||
{{ metrics.diskUtilPct }}
|
||||
</td>
|
||||
<td class="infoElem">
|
||||
{{ metrics.cpuThr }}
|
||||
</td>
|
||||
<td class="infoElem">
|
||||
{{ metrics.cpuUptime }}
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="metrics.cpuUtil === 'N/A'">
|
||||
@@ -83,6 +107,12 @@
|
||||
<td class="infoElem">
|
||||
---
|
||||
</td>
|
||||
<td class="infoElem">
|
||||
---
|
||||
</td>
|
||||
<td class="infoElem">
|
||||
---
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</v-row>
|
||||
|
||||
@@ -66,7 +66,16 @@
|
||||
>
|
||||
Save
|
||||
</v-btn>
|
||||
<v-snackbar
|
||||
v-model="snack"
|
||||
top
|
||||
:color="snackbar.color"
|
||||
timeout="5000"
|
||||
>
|
||||
<span>{{ snackbar.text }}</span>
|
||||
</v-snackbar>
|
||||
<v-divider class="mt-4 mb-4" />
|
||||
<!-- TEMP - RIO finder is not currently enabled
|
||||
<v-row>
|
||||
<v-col
|
||||
cols="12"
|
||||
@@ -125,6 +134,7 @@
|
||||
</v-simple-table>
|
||||
</v-col>
|
||||
</v-row>
|
||||
-->
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -237,7 +247,7 @@ export default {
|
||||
},
|
||||
sendGeneralSettings() {
|
||||
this.axios.post("http://" + this.$address + "/api/settings/general", this.settings).then(
|
||||
function (response) {
|
||||
response => {
|
||||
if (response.status === 200) {
|
||||
this.snackbar = {
|
||||
color: "success",
|
||||
@@ -246,7 +256,7 @@ export default {
|
||||
this.snack = true;
|
||||
}
|
||||
},
|
||||
function (error) {
|
||||
error => {
|
||||
this.snackbar = {
|
||||
color: "error",
|
||||
text: (error.response || {data: "Couldn't save settings"}).data
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
plugins {
|
||||
id 'edu.wpi.first.WpilibTools' version '1.0.0'
|
||||
}
|
||||
|
||||
import java.nio.file.Path
|
||||
|
||||
apply from: "${rootDir}/shared/common.gradle"
|
||||
@@ -10,9 +14,6 @@ dependencies {
|
||||
implementation 'org.msgpack:msgpack-core:0.9.0'
|
||||
implementation 'org.msgpack:jackson-dataformat-msgpack:0.9.0'
|
||||
|
||||
// wpiutil
|
||||
jniPlatforms.each { implementation "edu.wpi.first.wpiutil:wpiutil-jni:$wpilibVersion:$it" }
|
||||
|
||||
// JOGL stuff (currently we only distribute for aarch64, which is Pi 4)
|
||||
implementation "org.jogamp.gluegen:gluegen-rt:$joglVersion"
|
||||
implementation "org.jogamp.jogl:jogl-all:$joglVersion"
|
||||
@@ -22,6 +23,8 @@ dependencies {
|
||||
|
||||
// Zip
|
||||
implementation 'org.zeroturnaround:zt-zip:1.14'
|
||||
|
||||
implementation wpilibTools.deps.wpilibJava("apriltag")
|
||||
}
|
||||
|
||||
task writeCurrentVersionJava {
|
||||
@@ -31,3 +34,24 @@ task writeCurrentVersionJava {
|
||||
}
|
||||
|
||||
build.dependsOn writeCurrentVersionJava
|
||||
|
||||
def testNativeConfigName = 'wpilibTestNative'
|
||||
def testNativeConfig = configurations.create(testNativeConfigName)
|
||||
|
||||
def folder = project.layout.buildDirectory.dir('NativeTest')
|
||||
|
||||
def testNativeTasks = wpilibTools.createExtractionTasks {
|
||||
taskPostfix = "Test"
|
||||
configurationName = testNativeConfigName
|
||||
rootTaskFolder.set(folder)
|
||||
}
|
||||
|
||||
testNativeTasks.addToSourceSetResources(sourceSets.test)
|
||||
|
||||
testNativeConfig.dependencies.add wpilibTools.deps.cscore()
|
||||
testNativeConfig.dependencies.add wpilibTools.deps.wpilib("ntcore")
|
||||
testNativeConfig.dependencies.add wpilibTools.deps.wpilib("wpinet")
|
||||
testNativeConfig.dependencies.add wpilibTools.deps.wpilib("hal")
|
||||
testNativeConfig.dependencies.add wpilibTools.deps.wpilib("wpiutil")
|
||||
testNativeConfig.dependencies.add wpilibTools.deps.wpilib("apriltag")
|
||||
testNativeConfig.dependencies.add wpilibTools.deps.wpilib("wpimath")
|
||||
|
||||
@@ -21,6 +21,7 @@ import com.fasterxml.jackson.annotation.JsonCreator;
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import org.photonvision.common.logging.LogGroup;
|
||||
import org.photonvision.common.logging.Logger;
|
||||
@@ -45,6 +46,8 @@ public class CameraConfiguration {
|
||||
/** Can be either path (ex /dev/videoX) or index (ex 1). */
|
||||
public String path = "";
|
||||
|
||||
@JsonIgnore public String[] otherPaths = {};
|
||||
|
||||
public CameraType cameraType = CameraType.UsbCamera;
|
||||
public double FOV = 70;
|
||||
public final List<CameraCalibrationCoefficients> calibrations;
|
||||
@@ -59,19 +62,22 @@ public class CameraConfiguration {
|
||||
public DriverModePipelineSettings driveModeSettings = new DriverModePipelineSettings();
|
||||
|
||||
public CameraConfiguration(String baseName, String path) {
|
||||
this(baseName, baseName, baseName, path);
|
||||
this(baseName, baseName, baseName, path, new String[0]);
|
||||
}
|
||||
|
||||
public CameraConfiguration(String baseName, String uniqueName, String nickname, String path) {
|
||||
public CameraConfiguration(
|
||||
String baseName, String uniqueName, String nickname, String path, String[] alternates) {
|
||||
this.baseName = baseName;
|
||||
this.uniqueName = uniqueName;
|
||||
this.nickname = nickname;
|
||||
this.path = path;
|
||||
this.calibrations = new ArrayList<>();
|
||||
this.otherPaths = alternates;
|
||||
|
||||
logger.debug(
|
||||
"Creating USB camera configuration for "
|
||||
+ cameraType
|
||||
+ " "
|
||||
+ baseName
|
||||
+ " (AKA "
|
||||
+ nickname
|
||||
@@ -101,6 +107,7 @@ public class CameraConfiguration {
|
||||
logger.debug(
|
||||
"Creating camera configuration for "
|
||||
+ cameraType
|
||||
+ " "
|
||||
+ baseName
|
||||
+ " (AKA "
|
||||
+ nickname
|
||||
@@ -143,4 +150,33 @@ public class CameraConfiguration {
|
||||
.ifPresent(calibrations::remove);
|
||||
calibrations.add(calibration);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "CameraConfiguration [baseName="
|
||||
+ baseName
|
||||
+ ", uniqueName="
|
||||
+ uniqueName
|
||||
+ ", nickname="
|
||||
+ nickname
|
||||
+ ", path="
|
||||
+ path
|
||||
+ ", otherPaths="
|
||||
+ Arrays.toString(otherPaths)
|
||||
+ ", cameraType="
|
||||
+ cameraType
|
||||
+ ", FOV="
|
||||
+ FOV
|
||||
+ ", calibrations="
|
||||
+ calibrations
|
||||
+ ", currentPipelineIndex="
|
||||
+ currentPipelineIndex
|
||||
+ ", streamIndex="
|
||||
+ streamIndex
|
||||
+ ", pipelineSettings="
|
||||
+ pipelineSettings
|
||||
+ ", driveModeSettings="
|
||||
+ driveModeSettings
|
||||
+ "]";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -438,7 +438,7 @@ public class ConfigManager {
|
||||
try {
|
||||
Thread.sleep(1000);
|
||||
} catch (InterruptedException e) {
|
||||
logger.error("Exception waiting for settings semaphor", e);
|
||||
logger.error("Exception waiting for settings semaphore", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,6 +41,8 @@ public class HardwareConfig {
|
||||
public final String cpuTempCommand;
|
||||
public final String cpuMemoryCommand;
|
||||
public final String cpuUtilCommand;
|
||||
public final String cpuThrottleReasonCmd;
|
||||
public final String cpuUptimeCommand;
|
||||
public final String gpuMemoryCommand;
|
||||
public final String ramUtilCommand;
|
||||
public final String gpuMemUsageCommand;
|
||||
@@ -65,6 +67,8 @@ public class HardwareConfig {
|
||||
cpuTempCommand = "";
|
||||
cpuMemoryCommand = "";
|
||||
cpuUtilCommand = "";
|
||||
cpuThrottleReasonCmd = "";
|
||||
cpuUptimeCommand = "";
|
||||
gpuMemoryCommand = "";
|
||||
ramUtilCommand = "";
|
||||
ledBlinkCommand = "";
|
||||
@@ -91,6 +95,8 @@ public class HardwareConfig {
|
||||
String cpuTempCommand,
|
||||
String cpuMemoryCommand,
|
||||
String cpuUtilCommand,
|
||||
String cpuThrottleReasonCmd,
|
||||
String cpuUptimeCommand,
|
||||
String gpuMemoryCommand,
|
||||
String ramUtilCommand,
|
||||
String gpuMemUsageCommand,
|
||||
@@ -111,6 +117,8 @@ public class HardwareConfig {
|
||||
this.cpuTempCommand = cpuTempCommand;
|
||||
this.cpuMemoryCommand = cpuMemoryCommand;
|
||||
this.cpuUtilCommand = cpuUtilCommand;
|
||||
this.cpuThrottleReasonCmd = cpuThrottleReasonCmd;
|
||||
this.cpuUptimeCommand = cpuUptimeCommand;
|
||||
this.gpuMemoryCommand = gpuMemoryCommand;
|
||||
this.ramUtilCommand = ramUtilCommand;
|
||||
this.gpuMemUsageCommand = gpuMemUsageCommand;
|
||||
@@ -120,7 +128,22 @@ public class HardwareConfig {
|
||||
this.blacklistedResIndices = blacklistedResIndices;
|
||||
}
|
||||
|
||||
/** @return True if the FOV has been preset to a sane value, false otherwise */
|
||||
public final boolean hasPresetFOV() {
|
||||
return vendorFOV > 0;
|
||||
}
|
||||
|
||||
/** @return True if any command has been configured to a non-default empty, false otherwise */
|
||||
public final boolean hasCommandsConfigured() {
|
||||
return cpuTempCommand != ""
|
||||
|| cpuMemoryCommand != ""
|
||||
|| cpuUtilCommand != ""
|
||||
|| cpuThrottleReasonCmd != ""
|
||||
|| cpuUptimeCommand != ""
|
||||
|| gpuMemoryCommand != ""
|
||||
|| ramUtilCommand != ""
|
||||
|| ledBlinkCommand != ""
|
||||
|| gpuMemUsageCommand != ""
|
||||
|| diskUsageCommand != "";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -81,11 +81,11 @@ public class NetworkConfig {
|
||||
|
||||
@JsonGetter("shouldManage")
|
||||
public boolean shouldManage() {
|
||||
return this.shouldManage || Platform.isRaspberryPi();
|
||||
return this.shouldManage || Platform.isLinux();
|
||||
}
|
||||
|
||||
@JsonSetter("shouldManage")
|
||||
public void setShouldManage(boolean shouldManage) {
|
||||
this.shouldManage = shouldManage || Platform.isRaspberryPi();
|
||||
this.shouldManage = shouldManage || Platform.isLinux();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@ import java.util.stream.Collectors;
|
||||
import org.photonvision.PhotonVersion;
|
||||
import org.photonvision.common.hardware.Platform;
|
||||
import org.photonvision.common.util.SerializationUtils;
|
||||
import org.photonvision.raspi.PicamJNI;
|
||||
import org.photonvision.raspi.LibCameraJNI;
|
||||
import org.photonvision.vision.processes.VisionModule;
|
||||
import org.photonvision.vision.processes.VisionModuleManager;
|
||||
import org.photonvision.vision.processes.VisionSource;
|
||||
@@ -110,11 +110,11 @@ public class PhotonConfiguration {
|
||||
generalSubmap.put("version", PhotonVersion.versionString);
|
||||
generalSubmap.put(
|
||||
"gpuAcceleration",
|
||||
PicamJNI.isSupported()
|
||||
? "Zerocopy MMAL on " + PicamJNI.getSensorModel().getFriendlyName()
|
||||
LibCameraJNI.isSupported()
|
||||
? "Zerocopy Libcamera on " + LibCameraJNI.getSensorModel().getFriendlyName()
|
||||
: ""); // TODO add support for other types of GPU accel
|
||||
generalSubmap.put("hardwareModel", hardwareConfig.deviceName);
|
||||
generalSubmap.put("hardwarePlatform", Platform.getCurrentPlatform().toString());
|
||||
generalSubmap.put("hardwarePlatform", Platform.getPlatformName());
|
||||
settingsSubmap.put("general", generalSubmap);
|
||||
|
||||
map.put("settings", settingsSubmap);
|
||||
|
||||
@@ -17,22 +17,29 @@
|
||||
|
||||
package org.photonvision.common.dataflow.networktables;
|
||||
|
||||
import edu.wpi.first.networktables.EntryListenerFlags;
|
||||
import edu.wpi.first.networktables.EntryNotification;
|
||||
import edu.wpi.first.networktables.NetworkTableEntry;
|
||||
import edu.wpi.first.networktables.NetworkTableEvent;
|
||||
import edu.wpi.first.networktables.NetworkTableInstance;
|
||||
import edu.wpi.first.networktables.Subscriber;
|
||||
import java.util.EnumSet;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
public class NTDataChangeListener {
|
||||
private final NetworkTableEntry watchedEntry;
|
||||
private final NetworkTableInstance instance;
|
||||
private final Subscriber watchedEntry;
|
||||
private final int listenerID;
|
||||
|
||||
public NTDataChangeListener(
|
||||
NetworkTableEntry watchedEntry, Consumer<EntryNotification> dataChangeConsumer) {
|
||||
this.watchedEntry = watchedEntry;
|
||||
listenerID = watchedEntry.addListener(dataChangeConsumer, EntryListenerFlags.kUpdate);
|
||||
NetworkTableInstance instance,
|
||||
Subscriber watchedSubscriber,
|
||||
Consumer<NetworkTableEvent> dataChangeConsumer) {
|
||||
this.watchedEntry = watchedSubscriber;
|
||||
this.instance = instance;
|
||||
listenerID =
|
||||
this.instance.addListener(
|
||||
watchedEntry, EnumSet.of(NetworkTableEvent.Kind.kValueAll), dataChangeConsumer);
|
||||
}
|
||||
|
||||
public void remove() {
|
||||
watchedEntry.removeListener(listenerID);
|
||||
this.instance.removeListener(listenerID);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,9 +17,8 @@
|
||||
|
||||
package org.photonvision.common.dataflow.networktables;
|
||||
|
||||
import edu.wpi.first.networktables.EntryNotification;
|
||||
import edu.wpi.first.networktables.NetworkTable;
|
||||
import edu.wpi.first.networktables.NetworkTableEntry;
|
||||
import edu.wpi.first.networktables.NetworkTableEvent;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.function.BooleanSupplier;
|
||||
@@ -28,6 +27,9 @@ import java.util.function.Supplier;
|
||||
import org.opencv.core.Point;
|
||||
import org.photonvision.common.dataflow.CVPipelineResultConsumer;
|
||||
import org.photonvision.common.dataflow.structures.Packet;
|
||||
import org.photonvision.common.logging.LogGroup;
|
||||
import org.photonvision.common.logging.Logger;
|
||||
import org.photonvision.common.networktables.NTTopicSet;
|
||||
import org.photonvision.targeting.PhotonPipelineResult;
|
||||
import org.photonvision.targeting.PhotonTrackedTarget;
|
||||
import org.photonvision.targeting.TargetCorner;
|
||||
@@ -35,31 +37,21 @@ import org.photonvision.vision.pipeline.result.CVPipelineResult;
|
||||
import org.photonvision.vision.target.TrackedTarget;
|
||||
|
||||
public class NTDataPublisher implements CVPipelineResultConsumer {
|
||||
private final Logger logger = new Logger(NTDataPublisher.class, LogGroup.General);
|
||||
|
||||
private final NetworkTable rootTable = NetworkTablesManager.getInstance().kRootTable;
|
||||
private NetworkTable subTable;
|
||||
private NetworkTableEntry rawBytesEntry;
|
||||
|
||||
private NetworkTableEntry pipelineIndexEntry;
|
||||
private final Consumer<Integer> pipelineIndexConsumer;
|
||||
private NTDataChangeListener pipelineIndexListener;
|
||||
private NetworkTableEntry driverModeEntry;
|
||||
private final Consumer<Boolean> driverModeConsumer;
|
||||
private NTDataChangeListener driverModeListener;
|
||||
|
||||
private NetworkTableEntry latencyMillisEntry;
|
||||
private NetworkTableEntry hasTargetEntry;
|
||||
private NetworkTableEntry targetPitchEntry;
|
||||
private NetworkTableEntry targetYawEntry;
|
||||
private NetworkTableEntry targetAreaEntry;
|
||||
private NetworkTableEntry targetPoseEntry;
|
||||
private NetworkTableEntry targetSkewEntry;
|
||||
|
||||
// The raw position of the best target, in pixels.
|
||||
private NetworkTableEntry bestTargetPosX;
|
||||
private NetworkTableEntry bestTargetPosY;
|
||||
private NTTopicSet ts = new NTTopicSet();
|
||||
|
||||
NTDataChangeListener pipelineIndexListener;
|
||||
private final Supplier<Integer> pipelineIndexSupplier;
|
||||
private final Consumer<Integer> pipelineIndexConsumer;
|
||||
|
||||
NTDataChangeListener driverModeListener;
|
||||
private final BooleanSupplier driverModeSupplier;
|
||||
private final Consumer<Boolean> driverModeConsumer;
|
||||
|
||||
private long heartbeatCounter = 0;
|
||||
|
||||
public NTDataPublisher(
|
||||
String cameraNickname,
|
||||
@@ -76,93 +68,67 @@ public class NTDataPublisher implements CVPipelineResultConsumer {
|
||||
updateEntries();
|
||||
}
|
||||
|
||||
private void onPipelineIndexChange(EntryNotification entryNotification) {
|
||||
var newIndex = (int) entryNotification.value.getDouble();
|
||||
private void onPipelineIndexChange(NetworkTableEvent entryNotification) {
|
||||
var newIndex = (int) entryNotification.valueData.value.getInteger();
|
||||
var originalIndex = pipelineIndexSupplier.get();
|
||||
|
||||
// ignore indexes below 0
|
||||
if (newIndex < 0) {
|
||||
pipelineIndexEntry.forceSetNumber(originalIndex);
|
||||
ts.pipelineIndexPublisher.set(originalIndex);
|
||||
return;
|
||||
}
|
||||
|
||||
if (newIndex == originalIndex) {
|
||||
// TODO: Log
|
||||
logger.debug("Pipeline index is already " + newIndex);
|
||||
return;
|
||||
}
|
||||
|
||||
pipelineIndexConsumer.accept(newIndex);
|
||||
var setIndex = pipelineIndexSupplier.get();
|
||||
if (newIndex != setIndex) { // set failed
|
||||
pipelineIndexEntry.forceSetNumber(setIndex);
|
||||
ts.pipelineIndexPublisher.set(setIndex);
|
||||
// TODO: Log
|
||||
}
|
||||
// TODO: Log
|
||||
logger.debug("Successfully set pipeline index to " + newIndex);
|
||||
}
|
||||
|
||||
private void onDriverModeChange(EntryNotification entryNotification) {
|
||||
var newDriverMode = entryNotification.value.getBoolean();
|
||||
private void onDriverModeChange(NetworkTableEvent entryNotification) {
|
||||
var newDriverMode = entryNotification.valueData.value.getBoolean();
|
||||
var originalDriverMode = driverModeSupplier.getAsBoolean();
|
||||
|
||||
if (newDriverMode == originalDriverMode) {
|
||||
// TODO: Log
|
||||
logger.debug("Driver mode is already " + newDriverMode);
|
||||
return;
|
||||
}
|
||||
|
||||
driverModeConsumer.accept(newDriverMode);
|
||||
// TODO: Log
|
||||
logger.debug("Successfully set driver mode to " + newDriverMode);
|
||||
}
|
||||
|
||||
@SuppressWarnings("DuplicatedCode")
|
||||
private void removeEntries() {
|
||||
if (rawBytesEntry != null) rawBytesEntry.delete();
|
||||
if (pipelineIndexListener != null) pipelineIndexListener.remove();
|
||||
if (pipelineIndexEntry != null) pipelineIndexEntry.delete();
|
||||
if (driverModeListener != null) driverModeListener.remove();
|
||||
if (driverModeEntry != null) driverModeEntry.delete();
|
||||
if (latencyMillisEntry != null) latencyMillisEntry.delete();
|
||||
if (hasTargetEntry != null) hasTargetEntry.delete();
|
||||
if (targetPitchEntry != null) targetPitchEntry.delete();
|
||||
if (targetAreaEntry != null) targetAreaEntry.delete();
|
||||
if (targetYawEntry != null) targetYawEntry.delete();
|
||||
if (targetPoseEntry != null) targetPoseEntry.delete();
|
||||
if (targetSkewEntry != null) targetSkewEntry.delete();
|
||||
if (bestTargetPosX != null) bestTargetPosX.delete();
|
||||
if (bestTargetPosY != null) bestTargetPosY.delete();
|
||||
ts.removeEntries();
|
||||
}
|
||||
|
||||
private void updateEntries() {
|
||||
rawBytesEntry = subTable.getEntry("rawBytes");
|
||||
if (pipelineIndexListener != null) pipelineIndexListener.remove();
|
||||
if (driverModeListener != null) driverModeListener.remove();
|
||||
|
||||
ts.updateEntries();
|
||||
|
||||
if (pipelineIndexListener != null) {
|
||||
pipelineIndexListener.remove();
|
||||
}
|
||||
pipelineIndexEntry = subTable.getEntry("pipelineIndex");
|
||||
pipelineIndexListener =
|
||||
new NTDataChangeListener(pipelineIndexEntry, this::onPipelineIndexChange);
|
||||
new NTDataChangeListener(
|
||||
ts.subTable.getInstance(), ts.pipelineIndexSubscriber, this::onPipelineIndexChange);
|
||||
|
||||
if (driverModeListener != null) {
|
||||
driverModeListener.remove();
|
||||
}
|
||||
driverModeEntry = subTable.getEntry("driverMode");
|
||||
driverModeListener = new NTDataChangeListener(driverModeEntry, this::onDriverModeChange);
|
||||
|
||||
latencyMillisEntry = subTable.getEntry("latencyMillis");
|
||||
hasTargetEntry = subTable.getEntry("hasTarget");
|
||||
|
||||
targetPitchEntry = subTable.getEntry("targetPitch");
|
||||
targetAreaEntry = subTable.getEntry("targetArea");
|
||||
targetYawEntry = subTable.getEntry("targetYaw");
|
||||
targetPoseEntry = subTable.getEntry("targetPose");
|
||||
targetSkewEntry = subTable.getEntry("targetSkew");
|
||||
|
||||
bestTargetPosX = subTable.getEntry("targetPixelsX");
|
||||
bestTargetPosY = subTable.getEntry("targetPixelsY");
|
||||
driverModeListener =
|
||||
new NTDataChangeListener(
|
||||
ts.subTable.getInstance(), ts.driverModeSubscriber, this::onDriverModeChange);
|
||||
}
|
||||
|
||||
public void updateCameraNickname(String newCameraNickname) {
|
||||
removeEntries();
|
||||
subTable = rootTable.getSubTable(newCameraNickname);
|
||||
ts.subTable = rootTable.getSubTable(newCameraNickname);
|
||||
updateEntries();
|
||||
}
|
||||
|
||||
@@ -174,23 +140,23 @@ public class NTDataPublisher implements CVPipelineResultConsumer {
|
||||
Packet packet = new Packet(simplified.getPacketSize());
|
||||
simplified.populatePacket(packet);
|
||||
|
||||
rawBytesEntry.forceSetRaw(packet.getData());
|
||||
ts.rawBytesEntry.set(packet.getData());
|
||||
|
||||
pipelineIndexEntry.forceSetNumber(pipelineIndexSupplier.get());
|
||||
driverModeEntry.forceSetBoolean(driverModeSupplier.getAsBoolean());
|
||||
latencyMillisEntry.forceSetDouble(result.getLatencyMillis());
|
||||
hasTargetEntry.forceSetBoolean(result.hasTargets());
|
||||
ts.pipelineIndexPublisher.set(pipelineIndexSupplier.get());
|
||||
ts.driverModePublisher.set(driverModeSupplier.getAsBoolean());
|
||||
ts.latencyMillisEntry.set(result.getLatencyMillis());
|
||||
ts.hasTargetEntry.set(result.hasTargets());
|
||||
|
||||
if (result.hasTargets()) {
|
||||
var bestTarget = result.targets.get(0);
|
||||
|
||||
targetPitchEntry.forceSetDouble(bestTarget.getPitch());
|
||||
targetYawEntry.forceSetDouble(bestTarget.getYaw());
|
||||
targetAreaEntry.forceSetDouble(bestTarget.getArea());
|
||||
targetSkewEntry.forceSetDouble(bestTarget.getSkew());
|
||||
ts.targetPitchEntry.set(bestTarget.getPitch());
|
||||
ts.targetYawEntry.set(bestTarget.getYaw());
|
||||
ts.targetAreaEntry.set(bestTarget.getArea());
|
||||
ts.targetSkewEntry.set(bestTarget.getSkew());
|
||||
|
||||
var pose = bestTarget.getCameraToTarget3d();
|
||||
targetPoseEntry.forceSetDoubleArray(
|
||||
var pose = bestTarget.getBestCameraToTarget3d();
|
||||
ts.targetPoseEntry.set(
|
||||
new double[] {
|
||||
pose.getTranslation().getX(),
|
||||
pose.getTranslation().getY(),
|
||||
@@ -202,17 +168,21 @@ public class NTDataPublisher implements CVPipelineResultConsumer {
|
||||
});
|
||||
|
||||
var targetOffsetPoint = bestTarget.getTargetOffsetPoint();
|
||||
bestTargetPosX.forceSetDouble(targetOffsetPoint.x);
|
||||
bestTargetPosY.forceSetDouble(targetOffsetPoint.y);
|
||||
ts.bestTargetPosX.set(targetOffsetPoint.x);
|
||||
ts.bestTargetPosY.set(targetOffsetPoint.y);
|
||||
} else {
|
||||
targetPitchEntry.forceSetDouble(0);
|
||||
targetYawEntry.forceSetDouble(0);
|
||||
targetAreaEntry.forceSetDouble(0);
|
||||
targetSkewEntry.forceSetDouble(0);
|
||||
targetPoseEntry.forceSetDoubleArray(new double[] {0, 0, 0});
|
||||
bestTargetPosX.forceSetDouble(0);
|
||||
bestTargetPosY.forceSetDouble(0);
|
||||
ts.targetPitchEntry.set(0);
|
||||
ts.targetYawEntry.set(0);
|
||||
ts.targetAreaEntry.set(0);
|
||||
ts.targetSkewEntry.set(0);
|
||||
ts.targetPoseEntry.set(new double[] {0, 0, 0});
|
||||
ts.bestTargetPosX.set(0);
|
||||
ts.bestTargetPosY.set(0);
|
||||
}
|
||||
|
||||
ts.heartbeatPublisher.set(heartbeatCounter++);
|
||||
|
||||
// TODO...nt4... is this needed?
|
||||
rootTable.getInstance().flush();
|
||||
}
|
||||
|
||||
@@ -232,7 +202,8 @@ public class NTDataPublisher implements CVPipelineResultConsumer {
|
||||
t.getArea(),
|
||||
t.getSkew(),
|
||||
t.getFiducialId(),
|
||||
t.getCameraToTarget3d(),
|
||||
t.getBestCameraToTarget3d(),
|
||||
t.getAltCameraToTarget3d(),
|
||||
t.getPoseAmbiguity(),
|
||||
cornerList));
|
||||
}
|
||||
|
||||
@@ -17,12 +17,13 @@
|
||||
|
||||
package org.photonvision.common.dataflow.networktables;
|
||||
|
||||
import edu.wpi.first.networktables.LogMessage;
|
||||
import edu.wpi.first.networktables.NetworkTable;
|
||||
import edu.wpi.first.networktables.NetworkTableEvent;
|
||||
import edu.wpi.first.networktables.NetworkTableInstance;
|
||||
import java.util.HashMap;
|
||||
import java.util.function.Consumer;
|
||||
import org.photonvision.PhotonVersion;
|
||||
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;
|
||||
@@ -37,8 +38,11 @@ public class NetworkTablesManager {
|
||||
private final String kRootTableName = "/photonvision";
|
||||
public final NetworkTable kRootTable = ntInstance.getTable(kRootTableName);
|
||||
|
||||
private boolean isRetryingConnection = false;
|
||||
|
||||
private NetworkTablesManager() {
|
||||
ntInstance.addLogger(new NTLogger(), 0, 255); // to hide error messages
|
||||
ntInstance.addLogger(0, 255, new NTLogger()); // to hide error messages
|
||||
TimedTaskManager.getInstance().addTask("NTManager", this::ntTick, 5000);
|
||||
}
|
||||
|
||||
private static NetworkTablesManager INSTANCE;
|
||||
@@ -50,17 +54,17 @@ public class NetworkTablesManager {
|
||||
|
||||
private static final Logger logger = new Logger(NetworkTablesManager.class, LogGroup.General);
|
||||
|
||||
private static class NTLogger implements Consumer<LogMessage> {
|
||||
private static class NTLogger implements Consumer<NetworkTableEvent> {
|
||||
private boolean hasReportedConnectionFailure = false;
|
||||
private long lastConnectMessageMillis = 0;
|
||||
|
||||
@Override
|
||||
public void accept(LogMessage logMessage) {
|
||||
if (!hasReportedConnectionFailure && logMessage.message.contains("timed out")) {
|
||||
public void accept(NetworkTableEvent event) {
|
||||
if (!hasReportedConnectionFailure && event.logMessage.message.contains("timed out")) {
|
||||
logger.error("NT Connection has failed! Will retry in background.");
|
||||
hasReportedConnectionFailure = true;
|
||||
getInstance().broadcastConnectedStatus();
|
||||
} else if (logMessage.message.contains("connected")
|
||||
} else if (event.logMessage.message.contains("connected")
|
||||
&& System.currentTimeMillis() - lastConnectMessageMillis > 125) {
|
||||
logger.info("NT Connected!");
|
||||
hasReportedConnectionFailure = false;
|
||||
@@ -109,17 +113,11 @@ public class NetworkTablesManager {
|
||||
}
|
||||
|
||||
private void setClientMode(int teamNumber) {
|
||||
logger.info("Starting NT Client");
|
||||
if (!isRetryingConnection) logger.info("Starting NT Client");
|
||||
ntInstance.stopServer();
|
||||
|
||||
ntInstance.startClientTeam(teamNumber);
|
||||
ntInstance.startClient4("photonvision");
|
||||
ntInstance.setServerTeam(teamNumber);
|
||||
ntInstance.startDSClient();
|
||||
if (ntInstance.isConnected()) {
|
||||
logger.info("[NetworkTablesManager] Connected to the robot!");
|
||||
} else {
|
||||
logger.error(
|
||||
"[NetworkTablesManager] Could not connect to the robot! Will retry in the background...");
|
||||
}
|
||||
broadcastVersion();
|
||||
}
|
||||
|
||||
@@ -129,4 +127,22 @@ public class NetworkTablesManager {
|
||||
ntInstance.startServer();
|
||||
broadcastVersion();
|
||||
}
|
||||
|
||||
// So it seems like if Photon starts before the robot NT server does, and both aren't static IP,
|
||||
// it'll never connect. This hack works around it by restarting the client/server while the nt
|
||||
// instance
|
||||
// isn't connected, same as clicking the save button in the settings menu (or restarting the
|
||||
// service)
|
||||
private void ntTick() {
|
||||
if (!ntInstance.isConnected()
|
||||
&& !ConfigManager.getInstance().getConfig().getNetworkConfig().runNTServer) {
|
||||
setConfig(ConfigManager.getInstance().getConfig().getNetworkConfig());
|
||||
}
|
||||
|
||||
if (!ntInstance.isConnected() && !isRetryingConnection) {
|
||||
isRetryingConnection = true;
|
||||
logger.error(
|
||||
"[NetworkTablesManager] Could not connect to the robot! Will retry in the background...");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
|
||||
package org.photonvision.common.hardware;
|
||||
|
||||
import edu.wpi.first.networktables.NetworkTableEntry;
|
||||
import edu.wpi.first.networktables.IntegerEntry;
|
||||
import java.io.IOException;
|
||||
import org.photonvision.common.ProgramStatus;
|
||||
import org.photonvision.common.configuration.ConfigManager;
|
||||
@@ -27,7 +27,7 @@ import org.photonvision.common.dataflow.networktables.NTDataChangeListener;
|
||||
import org.photonvision.common.dataflow.networktables.NetworkTablesManager;
|
||||
import org.photonvision.common.hardware.GPIO.CustomGPIO;
|
||||
import org.photonvision.common.hardware.GPIO.pi.PigpioSocket;
|
||||
import org.photonvision.common.hardware.metrics.MetricsBase;
|
||||
import org.photonvision.common.hardware.metrics.MetricsManager;
|
||||
import org.photonvision.common.logging.LogGroup;
|
||||
import org.photonvision.common.logging.Logger;
|
||||
import org.photonvision.common.util.ShellExec;
|
||||
@@ -41,11 +41,13 @@ public class HardwareManager {
|
||||
private final HardwareConfig hardwareConfig;
|
||||
private final HardwareSettings hardwareSettings;
|
||||
|
||||
private final MetricsManager metricsManager;
|
||||
|
||||
@SuppressWarnings({"FieldCanBeLocal", "unused"})
|
||||
private final StatusLED statusLED;
|
||||
|
||||
@SuppressWarnings("FieldCanBeLocal")
|
||||
private final NetworkTableEntry ledModeEntry;
|
||||
private final IntegerEntry ledModeEntry;
|
||||
|
||||
@SuppressWarnings({"FieldCanBeLocal", "unused"})
|
||||
private final NTDataChangeListener ledModeListener;
|
||||
@@ -65,8 +67,11 @@ public class HardwareManager {
|
||||
private HardwareManager(HardwareConfig hardwareConfig, HardwareSettings hardwareSettings) {
|
||||
this.hardwareConfig = hardwareConfig;
|
||||
this.hardwareSettings = hardwareSettings;
|
||||
|
||||
this.metricsManager = new MetricsManager();
|
||||
this.metricsManager.setConfig(hardwareConfig);
|
||||
|
||||
CustomGPIO.setConfig(hardwareConfig);
|
||||
MetricsBase.setConfig(hardwareConfig);
|
||||
|
||||
if (Platform.isRaspberryPi()) {
|
||||
pigpioSocket = new PigpioSocket();
|
||||
@@ -89,12 +94,16 @@ public class HardwareManager {
|
||||
hasBrightnessRange ? hardwareConfig.ledBrightnessRange.get(1) : 100,
|
||||
pigpioSocket);
|
||||
|
||||
ledModeEntry = NetworkTablesManager.getInstance().kRootTable.getEntry("ledMode");
|
||||
ledModeEntry.setNumber(VisionLEDMode.kDefault.value);
|
||||
ledModeEntry =
|
||||
NetworkTablesManager.getInstance().kRootTable.getIntegerTopic("ledMode").getEntry(0);
|
||||
ledModeEntry.set(VisionLEDMode.kDefault.value);
|
||||
ledModeListener =
|
||||
visionLED == null
|
||||
? null
|
||||
: new NTDataChangeListener(ledModeEntry, visionLED::onLedModeChange);
|
||||
: new NTDataChangeListener(
|
||||
NetworkTablesManager.getInstance().kRootTable.getInstance(),
|
||||
ledModeEntry,
|
||||
visionLED::onLedModeChange);
|
||||
|
||||
Runtime.getRuntime().addShutdownHook(new Thread(this::onJvmExit));
|
||||
|
||||
@@ -122,7 +131,7 @@ public class HardwareManager {
|
||||
}
|
||||
|
||||
public boolean restartDevice() {
|
||||
if (Platform.isRaspberryPi()) {
|
||||
if (Platform.isLinux()) {
|
||||
try {
|
||||
return shellExec.executeBashCommand("reboot now") == 0;
|
||||
} catch (IOException e) {
|
||||
@@ -158,4 +167,8 @@ public class HardwareManager {
|
||||
public HardwareConfig getConfig() {
|
||||
return hardwareConfig;
|
||||
}
|
||||
|
||||
public void publishMetrics() {
|
||||
metricsManager.publishMetrics();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,6 +17,9 @@
|
||||
|
||||
package org.photonvision.common.hardware;
|
||||
|
||||
import java.io.IOException;
|
||||
import org.photonvision.common.util.ShellExec;
|
||||
|
||||
public enum PiVersion {
|
||||
PI_B("Pi Model B"),
|
||||
COMPUTE_MODULE("Compute Module Rev"),
|
||||
@@ -28,17 +31,41 @@ public enum PiVersion {
|
||||
UNKNOWN("UNKNOWN");
|
||||
|
||||
private final String identifier;
|
||||
private static final ShellExec shell = new ShellExec(true, false);
|
||||
private static final PiVersion currentPiVersion = calcPiVersion();
|
||||
|
||||
PiVersion(String s) {
|
||||
private PiVersion(String s) {
|
||||
this.identifier = s.toLowerCase();
|
||||
}
|
||||
|
||||
public static PiVersion getPiVersion() {
|
||||
return currentPiVersion;
|
||||
}
|
||||
|
||||
private static PiVersion calcPiVersion() {
|
||||
if (!Platform.isRaspberryPi()) return PiVersion.UNKNOWN;
|
||||
String piString = Platform.currentPiVersionStr;
|
||||
String piString = getPiVersionString();
|
||||
for (PiVersion p : PiVersion.values()) {
|
||||
if (piString.toLowerCase().contains(p.identifier)) return p;
|
||||
}
|
||||
return UNKNOWN;
|
||||
}
|
||||
|
||||
// Query /proc/device-tree/model. This should return the model of the pi
|
||||
// Versions here:
|
||||
// https://github.com/raspberrypi/linux/blob/rpi-5.10.y/arch/arm/boot/dts/bcm2710-rpi-cm3.dts
|
||||
private static String getPiVersionString() {
|
||||
if (!Platform.isRaspberryPi()) return "";
|
||||
try {
|
||||
shell.executeBashCommand("cat /proc/device-tree/model");
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
if (shell.getExitCode() == 0) {
|
||||
// We expect it to be in the format "raspberry pi X model X"
|
||||
return shell.getOutput();
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,58 +17,91 @@
|
||||
|
||||
package org.photonvision.common.hardware;
|
||||
|
||||
import com.jogamp.common.os.Platform.OSType;
|
||||
import edu.wpi.first.util.RuntimeDetector;
|
||||
import java.io.BufferedReader;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Paths;
|
||||
import org.photonvision.common.util.ShellExec;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public enum Platform {
|
||||
// WPILib Supported (JNI)
|
||||
WINDOWS_32("Windows x32"),
|
||||
WINDOWS_64("Windows x64"),
|
||||
LINUX_64("Linux x64"),
|
||||
LINUX_RASPBIAN("Linux Raspbian"), // Raspberry Pi 3/4
|
||||
LINUX_AARCH64BIONIC("Linux AARCH64 Bionic"), // Jetson Nano, Jetson TX2
|
||||
// PhotonVision Supported (Manual install)
|
||||
LINUX_ARM32("Linux ARM32"), // ODROID XU4, C1+
|
||||
LINUX_ARM64("Linux ARM64"), // ODROID C2, N2
|
||||
WINDOWS_64("Windows x64", false, OSType.WINDOWS, true),
|
||||
LINUX_32("Linux x86", false, OSType.LINUX, true),
|
||||
LINUX_64("Linux x64", false, OSType.LINUX, true),
|
||||
LINUX_RASPBIAN32(
|
||||
"Linux Raspbian 32-bit", true, OSType.LINUX, true), // Raspberry Pi 3/4 with a 32-bit image
|
||||
LINUX_RASPBIAN64(
|
||||
"Linux Raspbian 64-bit", true, OSType.LINUX, true), // Raspberry Pi 3/4 with a 64-bit image
|
||||
LINUX_AARCH64("Linux AARCH64", false, OSType.LINUX, true), // Jetson Nano, Jetson TX2
|
||||
|
||||
// PhotonVision Supported (Manual build/install)
|
||||
LINUX_ARM32("Linux ARM32", false, OSType.LINUX, true), // ODROID XU4, C1+
|
||||
LINUX_ARM64("Linux ARM64", false, OSType.LINUX, true), // ODROID C2, N2
|
||||
|
||||
// Completely unsupported
|
||||
UNSUPPORTED("Unsupported Platform");
|
||||
WINDOWS_32("Windows x86", false, OSType.WINDOWS, false),
|
||||
MACOS("Mac OS", false, OSType.MACOS, false),
|
||||
UNKNOWN("Unsupported Platform", false, OSType.UNKNOWN, false);
|
||||
|
||||
private enum OSType {
|
||||
WINDOWS,
|
||||
LINUX,
|
||||
MACOS,
|
||||
UNKNOWN
|
||||
}
|
||||
|
||||
private static final ShellExec shell = new ShellExec(true, false);
|
||||
public final String value;
|
||||
public static final boolean isRoot = checkForRoot();
|
||||
public final String description;
|
||||
public final boolean isPi;
|
||||
public final OSType osType;
|
||||
public final boolean isSupported;
|
||||
|
||||
Platform(String value) {
|
||||
this.value = value;
|
||||
// Set once at init, shouldn't be needed after.
|
||||
private static final Platform currentPlatform = getCurrentPlatform();
|
||||
private static final boolean isRoot = checkForRoot();
|
||||
|
||||
Platform(String description, boolean isPi, OSType osType, boolean isSupported) {
|
||||
this.description = description;
|
||||
this.isPi = isPi;
|
||||
this.osType = osType;
|
||||
this.isSupported = isSupported;
|
||||
}
|
||||
|
||||
private static final String OS_NAME = System.getProperty("os.name");
|
||||
private static final String OS_ARCH = System.getProperty("os.arch");
|
||||
|
||||
// These are queried on init and should never change after
|
||||
public static final Platform currentPlatform = getCurrentPlatform();
|
||||
static final String currentPiVersionStr = getPiVersionString();
|
||||
public static final PiVersion currentPiVersion = PiVersion.getPiVersion();
|
||||
|
||||
private static final String UnknownPlatformString =
|
||||
String.format("Unknown Platform. OS: %s, Architecture: %s", OS_NAME, OS_ARCH);
|
||||
|
||||
public static boolean isWindows() {
|
||||
return currentPlatform == WINDOWS_64 || currentPlatform == WINDOWS_32;
|
||||
}
|
||||
//////////////////////////////////////////////////////
|
||||
// Public API
|
||||
|
||||
// Checks specifically if unix shell and API are supported
|
||||
public static boolean isLinux() {
|
||||
return currentPlatform == LINUX_64
|
||||
|| currentPlatform == LINUX_RASPBIAN
|
||||
|| currentPlatform == LINUX_ARM64;
|
||||
return currentPlatform.osType == OSType.LINUX;
|
||||
}
|
||||
|
||||
public static boolean isRaspberryPi() {
|
||||
return currentPlatform.equals(LINUX_RASPBIAN);
|
||||
return currentPlatform.isPi;
|
||||
}
|
||||
|
||||
public static String getPlatformName() {
|
||||
if (currentPlatform.equals(UNKNOWN)) {
|
||||
return UnknownPlatformString;
|
||||
} else {
|
||||
return currentPlatform.description;
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean isRoot() {
|
||||
return isRoot;
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////
|
||||
|
||||
// Debug info related to unknown platforms for debug help
|
||||
private static final String OS_NAME = System.getProperty("os.name");
|
||||
private static final String OS_ARCH = System.getProperty("os.arch");
|
||||
private static final String UnknownPlatformString =
|
||||
String.format("Unknown Platform. OS: %s, Architecture: %s", OS_NAME, OS_ARCH);
|
||||
|
||||
@SuppressWarnings("StatementWithEmptyBody")
|
||||
private static boolean checkForRoot() {
|
||||
if (isLinux()) {
|
||||
@@ -92,49 +125,92 @@ public enum Platform {
|
||||
return false;
|
||||
}
|
||||
|
||||
public static Platform getCurrentPlatform() {
|
||||
private static Platform getCurrentPlatform() {
|
||||
if (RuntimeDetector.isWindows()) {
|
||||
if (RuntimeDetector.is32BitIntel()) return WINDOWS_32;
|
||||
if (RuntimeDetector.is64BitIntel()) return WINDOWS_64;
|
||||
if (RuntimeDetector.is32BitIntel()) {
|
||||
return WINDOWS_32;
|
||||
} else if (RuntimeDetector.is64BitIntel()) {
|
||||
return WINDOWS_64;
|
||||
} else {
|
||||
// please don't try this
|
||||
return UNKNOWN;
|
||||
}
|
||||
}
|
||||
|
||||
if (RuntimeDetector.isMac()) {
|
||||
if (RuntimeDetector.is32BitIntel()) return UNSUPPORTED;
|
||||
// TODO - once we have real support, this might have to be more granular
|
||||
return MACOS;
|
||||
}
|
||||
|
||||
if (RuntimeDetector.isLinux()) {
|
||||
if (RuntimeDetector.is32BitIntel()) return UNSUPPORTED;
|
||||
if (RuntimeDetector.is64BitIntel()) return LINUX_64;
|
||||
if (RuntimeDetector.isRaspbian()) return LINUX_RASPBIAN;
|
||||
if (isPiSBC()) {
|
||||
if (RuntimeDetector.isArm32()) {
|
||||
return LINUX_RASPBIAN32;
|
||||
} else if (RuntimeDetector.isArm64()) {
|
||||
return LINUX_RASPBIAN64;
|
||||
} else {
|
||||
// Unknown/exotic installation
|
||||
return UNKNOWN;
|
||||
}
|
||||
} else if (isJetsonSBC()) {
|
||||
if (RuntimeDetector.isArm64()) {
|
||||
// TODO - do we need to check OS version?
|
||||
return LINUX_AARCH64;
|
||||
} else {
|
||||
// Unknown/exotic installation
|
||||
return UNKNOWN;
|
||||
}
|
||||
} else if (RuntimeDetector.is64BitIntel()) {
|
||||
return LINUX_64;
|
||||
} else if (RuntimeDetector.is32BitIntel()) {
|
||||
return LINUX_32;
|
||||
} else if (RuntimeDetector.isArm64()) {
|
||||
// TODO - os detection needed?
|
||||
return LINUX_AARCH64;
|
||||
} else {
|
||||
// Unknown or otherwise unsupported platform
|
||||
return Platform.UNKNOWN;
|
||||
}
|
||||
}
|
||||
|
||||
System.out.println(UnknownPlatformString);
|
||||
return Platform.UNSUPPORTED;
|
||||
// If we fall through all the way to here,
|
||||
return Platform.UNKNOWN;
|
||||
}
|
||||
|
||||
public String toString() {
|
||||
if (this.equals(UNSUPPORTED)) {
|
||||
return UnknownPlatformString;
|
||||
} else {
|
||||
return this.value;
|
||||
}
|
||||
// Check for various known SBC types
|
||||
private static boolean isPiSBC() {
|
||||
return fileHasText("/proc/cpuinfo", "Raspberry Pi");
|
||||
}
|
||||
|
||||
// Querry /proc/device-tree/model. This should return the model of the pi
|
||||
// Versions here:
|
||||
// https://github.com/raspberrypi/linux/blob/rpi-5.10.y/arch/arm/boot/dts/bcm2710-rpi-cm3.dts
|
||||
private static String getPiVersionString() {
|
||||
if (!isRaspberryPi()) return "";
|
||||
try {
|
||||
shell.executeBashCommand("cat /proc/device-tree/model");
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
if (shell.getExitCode() == 0) {
|
||||
// We expect it to be in the format "raspberry pi X model X"
|
||||
return shell.getOutput();
|
||||
}
|
||||
private static boolean isJetsonSBC() {
|
||||
// https://forums.developer.nvidia.com/t/how-to-recognize-jetson-nano-device/146624
|
||||
return fileHasText("/proc/device-tree/model", "NVIDIA Jetson");
|
||||
}
|
||||
|
||||
return "";
|
||||
// Checks for various names of linux OS
|
||||
private static boolean isStretch() {
|
||||
// TODO - this is a total guess
|
||||
return fileHasText("/etc/os-release", "Stretch");
|
||||
}
|
||||
|
||||
private static boolean isBuster() {
|
||||
// TODO - this is a total guess
|
||||
return fileHasText("/etc/os-release", "Buster");
|
||||
}
|
||||
|
||||
private static boolean fileHasText(String filename, String text) {
|
||||
try (BufferedReader reader = Files.newBufferedReader(Paths.get(filename))) {
|
||||
while (true) {
|
||||
String value = reader.readLine();
|
||||
if (value == null) {
|
||||
return false;
|
||||
|
||||
} else if (value.contains(text)) {
|
||||
return true;
|
||||
} // else, next line
|
||||
}
|
||||
} catch (IOException ex) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
|
||||
package org.photonvision.common.hardware;
|
||||
|
||||
import edu.wpi.first.networktables.EntryNotification;
|
||||
import edu.wpi.first.networktables.NetworkTableEvent;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.function.BooleanSupplier;
|
||||
@@ -85,6 +85,8 @@ public class VisionLED {
|
||||
pigpioSocket.generateAndSendWaveform(pulseLengthMillis, blinkCount, ledPins);
|
||||
} catch (PigpioException e) {
|
||||
logger.error("Failed to blink!", e);
|
||||
} catch (NullPointerException e) {
|
||||
logger.error("Failed to blink, pigpio internal issue!", e);
|
||||
}
|
||||
} else {
|
||||
for (GPIOBase led : visionLEDs) {
|
||||
@@ -100,13 +102,19 @@ public class VisionLED {
|
||||
pigpioSocket.waveTxStop();
|
||||
} catch (PigpioException e) {
|
||||
logger.error("Failed to stop blink!", e);
|
||||
} catch (NullPointerException e) {
|
||||
logger.error("Failed to blink, pigpio internal issue!", e);
|
||||
}
|
||||
}
|
||||
// if the user has set an LED brightness other than 100%, use that instead
|
||||
if (mappedBrightnessPercentage == 100 || !state) {
|
||||
visionLEDs.forEach((led) -> led.setState(state));
|
||||
} else {
|
||||
visionLEDs.forEach((led) -> led.setBrightness(mappedBrightnessPercentage));
|
||||
try {
|
||||
// if the user has set an LED brightness other than 100%, use that instead
|
||||
if (mappedBrightnessPercentage == 100 || !state) {
|
||||
visionLEDs.forEach((led) -> led.setState(state));
|
||||
} else {
|
||||
visionLEDs.forEach((led) -> led.setBrightness(mappedBrightnessPercentage));
|
||||
}
|
||||
} catch (NullPointerException e) {
|
||||
logger.error("Failed to blink, pigpio internal issue!", e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -114,8 +122,8 @@ public class VisionLED {
|
||||
setInternal(on ? VisionLEDMode.kOn : VisionLEDMode.kOff, false);
|
||||
}
|
||||
|
||||
void onLedModeChange(EntryNotification entryNotification) {
|
||||
var newLedModeRaw = (int) entryNotification.value.getDouble();
|
||||
void onLedModeChange(NetworkTableEvent entryNotification) {
|
||||
var newLedModeRaw = (int) entryNotification.valueData.value.getDouble();
|
||||
if (newLedModeRaw != currentLedMode.value) {
|
||||
VisionLEDMode newLedMode;
|
||||
switch (newLedModeRaw) {
|
||||
@@ -177,6 +185,9 @@ public class VisionLED {
|
||||
case kOn:
|
||||
setStateImpl(true);
|
||||
break;
|
||||
case kBlink:
|
||||
blinkImpl(85, -1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
logger.info("Changing LED internal state to " + newLedMode.toString());
|
||||
|
||||
@@ -1,43 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) Photon Vision.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.photonvision.common.hardware.metrics;
|
||||
|
||||
public class CPUMetrics extends MetricsBase {
|
||||
private String cpuMemSplit = null;
|
||||
|
||||
public String getMemory() {
|
||||
if (cpuMemoryCommand.isEmpty()) return "";
|
||||
if (cpuMemSplit == null) {
|
||||
cpuMemSplit = execute(cpuMemoryCommand);
|
||||
}
|
||||
return cpuMemSplit;
|
||||
}
|
||||
|
||||
public String getTemp() {
|
||||
if (cpuTemperatureCommand.isEmpty()) return "";
|
||||
try {
|
||||
return execute(cpuTemperatureCommand);
|
||||
} catch (Exception e) {
|
||||
return "N/A";
|
||||
}
|
||||
}
|
||||
|
||||
public String getUtilization() {
|
||||
return execute(cpuUtilizationCommand);
|
||||
}
|
||||
}
|
||||
@@ -1,92 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) Photon Vision.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.photonvision.common.hardware.metrics;
|
||||
|
||||
import java.io.PrintWriter;
|
||||
import java.io.StringWriter;
|
||||
import org.photonvision.common.configuration.HardwareConfig;
|
||||
import org.photonvision.common.hardware.Platform;
|
||||
import org.photonvision.common.logging.LogGroup;
|
||||
import org.photonvision.common.logging.Logger;
|
||||
import org.photonvision.common.util.ShellExec;
|
||||
|
||||
public abstract class MetricsBase {
|
||||
private static final Logger logger = new Logger(MetricsBase.class, LogGroup.General);
|
||||
// CPU
|
||||
public static String cpuMemoryCommand = "vcgencmd get_mem arm | grep -Eo '[0-9]+'";
|
||||
public static String cpuTemperatureCommand =
|
||||
"sed 's/.\\{3\\}$/.&/' <<< cat /sys/class/thermal/thermal_zone0/temp";
|
||||
public static String cpuUtilizationCommand =
|
||||
"top -bn1 | grep \"Cpu(s)\" | sed \"s/.*, *\\([0-9.]*\\)%* id.*/\\1/\" | awk '{print 100 - $1}'";
|
||||
|
||||
// GPU
|
||||
public static String gpuMemoryCommand = "vcgencmd get_mem gpu | grep -Eo '[0-9]+'";
|
||||
public static String gpuMemUsageCommand = "vcgencmd get_mem malloc | grep -Eo '[0-9]+'";
|
||||
|
||||
// RAM
|
||||
public static String ramUsageCommand = "free --mega | awk -v i=2 -v j=3 'FNR == i {print $j}'";
|
||||
|
||||
// Disk
|
||||
public static String diskUsageCommand = "df ./ --output=pcent | tail -n +2";
|
||||
|
||||
private static ShellExec runCommand = new ShellExec(true, true);
|
||||
|
||||
public static void setConfig(HardwareConfig config) {
|
||||
if (Platform.isRaspberryPi()) return;
|
||||
cpuMemoryCommand = config.cpuMemoryCommand;
|
||||
cpuTemperatureCommand = config.cpuTempCommand;
|
||||
cpuUtilizationCommand = config.cpuUtilCommand;
|
||||
|
||||
gpuMemoryCommand = config.gpuMemoryCommand;
|
||||
gpuMemUsageCommand = config.gpuMemUsageCommand;
|
||||
|
||||
diskUsageCommand = config.diskUsageCommand;
|
||||
|
||||
ramUsageCommand = config.ramUtilCommand;
|
||||
}
|
||||
|
||||
public static synchronized String execute(String command) {
|
||||
try {
|
||||
runCommand.executeBashCommand(command);
|
||||
return runCommand.getOutput();
|
||||
} catch (Exception e) {
|
||||
StringWriter sw = new StringWriter();
|
||||
PrintWriter pw = new PrintWriter(sw);
|
||||
e.printStackTrace(pw);
|
||||
|
||||
logger.error(
|
||||
"Command: \""
|
||||
+ command
|
||||
+ "\" returned an error!"
|
||||
+ "\nOutput Received: "
|
||||
+ runCommand.getOutput()
|
||||
+ "\nStandard Error: "
|
||||
+ runCommand.getError()
|
||||
+ "\nCommand completed: "
|
||||
+ runCommand.isOutputCompleted()
|
||||
+ "\nError completed: "
|
||||
+ runCommand.isErrorCompleted()
|
||||
+ "\nExit code: "
|
||||
+ runCommand.getExitCode()
|
||||
+ "\n Exception: "
|
||||
+ e.toString()
|
||||
+ sw.toString());
|
||||
return "";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,161 @@
|
||||
/*
|
||||
* Copyright (C) Photon Vision.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.photonvision.common.hardware.metrics;
|
||||
|
||||
import java.io.PrintWriter;
|
||||
import java.io.StringWriter;
|
||||
import java.util.HashMap;
|
||||
import org.photonvision.common.configuration.HardwareConfig;
|
||||
import org.photonvision.common.dataflow.DataChangeService;
|
||||
import org.photonvision.common.dataflow.events.OutgoingUIEvent;
|
||||
import org.photonvision.common.hardware.Platform;
|
||||
import org.photonvision.common.hardware.metrics.cmds.CmdBase;
|
||||
import org.photonvision.common.hardware.metrics.cmds.FileCmds;
|
||||
import org.photonvision.common.hardware.metrics.cmds.LinuxCmds;
|
||||
import org.photonvision.common.hardware.metrics.cmds.PiCmds;
|
||||
import org.photonvision.common.logging.LogGroup;
|
||||
import org.photonvision.common.logging.Logger;
|
||||
import org.photonvision.common.util.ShellExec;
|
||||
|
||||
public class MetricsManager {
|
||||
final Logger logger = new Logger(MetricsManager.class, LogGroup.General);
|
||||
|
||||
CmdBase cmds;
|
||||
|
||||
private ShellExec runCommand = new ShellExec(true, true);
|
||||
|
||||
public void setConfig(HardwareConfig config) {
|
||||
if (config.hasCommandsConfigured()) {
|
||||
cmds = new FileCmds();
|
||||
} else if (Platform.isRaspberryPi()) {
|
||||
cmds = new PiCmds(); // Pi's can use a hardcoded command set
|
||||
} else if (Platform.isLinux()) {
|
||||
cmds = new LinuxCmds(); // Linux/Unix platforms assume a nominal command set
|
||||
} else {
|
||||
cmds = new CmdBase(); // default - base has no commands
|
||||
}
|
||||
|
||||
cmds.initCmds(config);
|
||||
}
|
||||
|
||||
public String safeExecute(String str) {
|
||||
if (str.isEmpty()) return "";
|
||||
try {
|
||||
return execute(str);
|
||||
} catch (Exception e) {
|
||||
return "****";
|
||||
}
|
||||
}
|
||||
|
||||
private String cpuMemSave = null;
|
||||
|
||||
public String getMemory() {
|
||||
if (cmds.cpuMemoryCommand.isEmpty()) return "";
|
||||
if (cpuMemSave == null) {
|
||||
// save the value and only run it once
|
||||
cpuMemSave = execute(cmds.cpuMemoryCommand);
|
||||
}
|
||||
return cpuMemSave;
|
||||
}
|
||||
|
||||
public String getTemp() {
|
||||
return safeExecute(cmds.cpuTemperatureCommand);
|
||||
}
|
||||
|
||||
public String getUtilization() {
|
||||
return safeExecute(cmds.cpuUtilizationCommand);
|
||||
}
|
||||
|
||||
public String getUptime() {
|
||||
return safeExecute(cmds.cpuUptimeCommand);
|
||||
}
|
||||
|
||||
public String getThrottleReason() {
|
||||
return safeExecute(cmds.cpuThrottleReasonCmd);
|
||||
}
|
||||
|
||||
private String gpuMemSave = null;
|
||||
|
||||
public String getGPUMemorySplit() {
|
||||
if (gpuMemSave == null) {
|
||||
// only needs to run once
|
||||
gpuMemSave = safeExecute(cmds.gpuMemoryCommand);
|
||||
}
|
||||
return gpuMemSave;
|
||||
}
|
||||
|
||||
public String getMallocedMemory() {
|
||||
return safeExecute(cmds.gpuMemUsageCommand);
|
||||
}
|
||||
|
||||
public String getUsedDiskPct() {
|
||||
return safeExecute(cmds.diskUsageCommand);
|
||||
}
|
||||
|
||||
// TODO: Output in MBs for consistency
|
||||
public String getUsedRam() {
|
||||
return safeExecute(cmds.ramUsageCommand);
|
||||
}
|
||||
|
||||
public void publishMetrics() {
|
||||
logger.debug("Publishing Metrics...");
|
||||
final var metrics = new HashMap<String, String>();
|
||||
|
||||
metrics.put("cpuTemp", this.getTemp());
|
||||
metrics.put("cpuUtil", this.getUtilization());
|
||||
metrics.put("cpuMem", this.getMemory());
|
||||
metrics.put("cpuThr", this.getThrottleReason());
|
||||
metrics.put("cpuUptime", this.getUptime());
|
||||
metrics.put("gpuMem", this.getGPUMemorySplit());
|
||||
metrics.put("ramUtil", this.getUsedRam());
|
||||
metrics.put("gpuMemUtil", this.getMallocedMemory());
|
||||
metrics.put("diskUtilPct", this.getUsedDiskPct());
|
||||
|
||||
DataChangeService.getInstance().publishEvent(OutgoingUIEvent.wrappedOf("metrics", metrics));
|
||||
}
|
||||
|
||||
public synchronized String execute(String command) {
|
||||
try {
|
||||
runCommand.executeBashCommand(command);
|
||||
return runCommand.getOutput();
|
||||
} catch (Exception e) {
|
||||
StringWriter sw = new StringWriter();
|
||||
PrintWriter pw = new PrintWriter(sw);
|
||||
e.printStackTrace(pw);
|
||||
|
||||
logger.error(
|
||||
"Command: \""
|
||||
+ command
|
||||
+ "\" returned an error!"
|
||||
+ "\nOutput Received: "
|
||||
+ runCommand.getOutput()
|
||||
+ "\nStandard Error: "
|
||||
+ runCommand.getError()
|
||||
+ "\nCommand completed: "
|
||||
+ runCommand.isOutputCompleted()
|
||||
+ "\nError completed: "
|
||||
+ runCommand.isErrorCompleted()
|
||||
+ "\nExit code: "
|
||||
+ runCommand.getExitCode()
|
||||
+ "\n Exception: "
|
||||
+ e.toString()
|
||||
+ sw.toString());
|
||||
return "";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,74 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) Photon Vision.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.photonvision.common.hardware.metrics;
|
||||
|
||||
import java.util.HashMap;
|
||||
import org.photonvision.common.dataflow.DataChangeService;
|
||||
import org.photonvision.common.dataflow.events.OutgoingUIEvent;
|
||||
import org.photonvision.common.hardware.Platform;
|
||||
import org.photonvision.common.logging.LogGroup;
|
||||
import org.photonvision.common.logging.Logger;
|
||||
import org.photonvision.common.util.TimedTaskManager;
|
||||
|
||||
public class MetricsPublisher {
|
||||
private static final Logger logger = new Logger(MetricsPublisher.class, LogGroup.General);
|
||||
private static CPUMetrics cpuMetrics;
|
||||
private static GPUMetrics gpuMetrics;
|
||||
private static RAMMetrics ramMetrics;
|
||||
private static DiskMetrics diskMetrics;
|
||||
|
||||
public static MetricsPublisher getInstance() {
|
||||
return Singleton.INSTANCE;
|
||||
}
|
||||
|
||||
private MetricsPublisher() {
|
||||
cpuMetrics = new CPUMetrics();
|
||||
gpuMetrics = new GPUMetrics();
|
||||
ramMetrics = new RAMMetrics();
|
||||
diskMetrics = new DiskMetrics();
|
||||
}
|
||||
|
||||
public void stopTask() {
|
||||
TimedTaskManager.getInstance().cancelTask("Metrics");
|
||||
logger.info("This device does not support running bash commands. Stopped metrics thread.");
|
||||
}
|
||||
|
||||
public void publish() {
|
||||
if (!Platform.isRaspberryPi()) {
|
||||
logger.debug("Ignoring metrics on non-Pi devices");
|
||||
return;
|
||||
}
|
||||
|
||||
logger.debug("Publishing Metrics...");
|
||||
final var metrics = new HashMap<String, String>();
|
||||
|
||||
metrics.put("cpuTemp", cpuMetrics.getTemp());
|
||||
metrics.put("cpuUtil", cpuMetrics.getUtilization());
|
||||
metrics.put("cpuMem", cpuMetrics.getMemory());
|
||||
metrics.put("gpuMem", gpuMetrics.getGPUMemorySplit());
|
||||
metrics.put("ramUtil", ramMetrics.getUsedRam());
|
||||
metrics.put("gpuMemUtil", gpuMetrics.getMallocedMemory());
|
||||
metrics.put("diskUtilPct", diskMetrics.getUsedDiskPct());
|
||||
|
||||
DataChangeService.getInstance().publishEvent(OutgoingUIEvent.wrappedOf("metrics", metrics));
|
||||
}
|
||||
|
||||
private static class Singleton {
|
||||
public static final MetricsPublisher INSTANCE = new MetricsPublisher();
|
||||
}
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) Photon Vision.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.photonvision.common.hardware.metrics;
|
||||
|
||||
public class RAMMetrics extends MetricsBase {
|
||||
// TODO: Output in MBs for consistency
|
||||
public String getUsedRam() {
|
||||
if (ramUsageCommand.isEmpty()) return "";
|
||||
return execute(ramUsageCommand);
|
||||
}
|
||||
}
|
||||
@@ -15,19 +15,26 @@
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.photonvision.common.hardware.metrics;
|
||||
package org.photonvision.common.hardware.metrics.cmds;
|
||||
|
||||
public class GPUMetrics extends MetricsBase {
|
||||
private String gpuMemSplit = null;
|
||||
import org.photonvision.common.configuration.HardwareConfig;
|
||||
|
||||
public String getGPUMemorySplit() {
|
||||
if (gpuMemSplit == null) {
|
||||
gpuMemSplit = execute(gpuMemoryCommand);
|
||||
}
|
||||
return gpuMemSplit;
|
||||
}
|
||||
public class CmdBase {
|
||||
// CPU
|
||||
public String cpuMemoryCommand = "";
|
||||
public String cpuTemperatureCommand = "";
|
||||
public String cpuUtilizationCommand = "";
|
||||
public String cpuThrottleReasonCmd = "";
|
||||
public String cpuUptimeCommand = "";
|
||||
// GPU
|
||||
public String gpuMemoryCommand = "";
|
||||
public String gpuMemUsageCommand = "";
|
||||
// RAM
|
||||
public String ramUsageCommand = "";
|
||||
// Disk
|
||||
public String diskUsageCommand = "";
|
||||
|
||||
public String getMallocedMemory() {
|
||||
return execute(gpuMemUsageCommand);
|
||||
public void initCmds(HardwareConfig config) {
|
||||
return; // default - do nothing
|
||||
}
|
||||
}
|
||||
@@ -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.hardware.metrics.cmds;
|
||||
|
||||
import org.photonvision.common.configuration.HardwareConfig;
|
||||
|
||||
public class FileCmds extends CmdBase {
|
||||
@Override
|
||||
public void initCmds(HardwareConfig config) {
|
||||
cpuMemoryCommand = config.cpuMemoryCommand;
|
||||
cpuTemperatureCommand = config.cpuTempCommand;
|
||||
cpuUtilizationCommand = config.cpuUtilCommand;
|
||||
cpuThrottleReasonCmd = config.cpuThrottleReasonCmd;
|
||||
cpuUptimeCommand = config.cpuUptimeCommand;
|
||||
|
||||
gpuMemoryCommand = config.gpuMemoryCommand;
|
||||
gpuMemUsageCommand = config.gpuMemUsageCommand;
|
||||
|
||||
diskUsageCommand = config.diskUsageCommand;
|
||||
|
||||
ramUsageCommand = config.ramUtilCommand;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
/*
|
||||
* Copyright (C) Photon Vision.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.photonvision.common.hardware.metrics.cmds;
|
||||
|
||||
import org.photonvision.common.configuration.HardwareConfig;
|
||||
|
||||
public class LinuxCmds extends CmdBase {
|
||||
public void initCmds(HardwareConfig config) {
|
||||
// CPU
|
||||
cpuMemoryCommand = "awk '/MemTotal:/ {print int($2 / 1000);}' /proc/meminfo";
|
||||
|
||||
// TODO: boards have lots of thermal devices. Hard to pick the CPU
|
||||
|
||||
cpuUtilizationCommand =
|
||||
"top -bn1 | grep \"Cpu(s)\" | sed \"s/.*, *\\([0-9.]*\\)%* id.*/\\1/\" | awk '{print 100 - $1}'";
|
||||
|
||||
cpuUptimeCommand = "uptime -p | cut -c 4-";
|
||||
|
||||
// RAM
|
||||
ramUsageCommand = "awk '/MemFree:/ {print int($2 / 1000);}' /proc/meminfo";
|
||||
|
||||
// Disk
|
||||
diskUsageCommand = "df ./ --output=pcent | tail -n +2";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
/*
|
||||
* Copyright (C) Photon Vision.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.photonvision.common.hardware.metrics.cmds;
|
||||
|
||||
import org.photonvision.common.configuration.HardwareConfig;
|
||||
|
||||
public class PiCmds extends LinuxCmds {
|
||||
/** Applies pi-specific commands, ignoring any input configuration */
|
||||
public void initCmds(HardwareConfig config) {
|
||||
super.initCmds(config);
|
||||
|
||||
// CPU
|
||||
cpuMemoryCommand = "vcgencmd get_mem arm | grep -Eo '[0-9]+'";
|
||||
cpuTemperatureCommand = "sed 's/.\\{3\\}$/.&/' /sys/class/thermal/thermal_zone0/temp";
|
||||
cpuThrottleReasonCmd =
|
||||
"if (( $(( $(vcgencmd get_throttled | grep -Eo 0x[0-9a-fA-F]*) & 0x01 )) != 0x00 )); then echo \"LOW VOLTAGE\"; "
|
||||
+ " elif (( $(( $(vcgencmd get_throttled | grep -Eo 0x[0-9a-fA-F]*) & 0x08 )) != 0x00 )); then echo \"HIGH TEMP\"; "
|
||||
+ " elif (( $(( $(vcgencmd get_throttled | grep -Eo 0x[0-9a-fA-F]*) & 0x10000 )) != 0x00 )); then echo \"Prev. Low Voltage\"; "
|
||||
+ " elif (( $(( $(vcgencmd get_throttled | grep -Eo 0x[0-9a-fA-F]*) & 0x80000 )) != 0x00 )); then echo \"Prev. High Temp\"; "
|
||||
+ " else echo \"None\"; fi";
|
||||
|
||||
// GPU
|
||||
gpuMemoryCommand = "vcgencmd get_mem gpu | grep -Eo '[0-9]+'";
|
||||
gpuMemUsageCommand = "vcgencmd get_mem malloc | grep -Eo '[0-9]+'";
|
||||
}
|
||||
}
|
||||
@@ -46,8 +46,8 @@ public class NetworkManager {
|
||||
|
||||
var config = ConfigManager.getInstance().getConfig().getNetworkConfig();
|
||||
logger.info("Setting " + config.connectionType + " with team team " + config.teamNumber);
|
||||
if (Platform.isRaspberryPi()) {
|
||||
if (!Platform.isRoot) {
|
||||
if (Platform.isLinux()) {
|
||||
if (!Platform.isRoot()) {
|
||||
logger.error("Cannot manage network without root!");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -128,7 +128,7 @@ public class ScriptManager {
|
||||
}
|
||||
|
||||
public static void queueEvent(ScriptEventType eventType) {
|
||||
if (!Platform.currentPlatform.isWindows()) {
|
||||
if (Platform.isLinux()) {
|
||||
try {
|
||||
queuedEvents.putLast(eventType);
|
||||
logger.info("Queued event: " + eventType.name());
|
||||
|
||||
@@ -18,23 +18,54 @@
|
||||
package org.photonvision.common.util;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import edu.wpi.first.apriltag.jni.AprilTagJNI;
|
||||
import edu.wpi.first.cscore.CameraServerCvJNI;
|
||||
import edu.wpi.first.cscore.CameraServerJNI;
|
||||
import edu.wpi.first.hal.JNIWrapper;
|
||||
import edu.wpi.first.math.util.Units;
|
||||
import edu.wpi.first.net.WPINetJNI;
|
||||
import edu.wpi.first.networktables.NetworkTablesJNI;
|
||||
import edu.wpi.first.util.CombinedRuntimeLoader;
|
||||
import edu.wpi.first.util.RuntimeLoader;
|
||||
import edu.wpi.first.util.WPIUtilJNI;
|
||||
import java.awt.*;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Path;
|
||||
import org.opencv.core.Core;
|
||||
import org.opencv.core.Mat;
|
||||
import org.opencv.highgui.HighGui;
|
||||
import org.photonvision.vision.calibration.CameraCalibrationCoefficients;
|
||||
|
||||
public class TestUtils {
|
||||
public static void loadLibraries() {
|
||||
public static boolean loadLibraries() {
|
||||
JNIWrapper.Helper.setExtractOnStaticLoad(false);
|
||||
WPIUtilJNI.Helper.setExtractOnStaticLoad(false);
|
||||
NetworkTablesJNI.Helper.setExtractOnStaticLoad(false);
|
||||
WPINetJNI.Helper.setExtractOnStaticLoad(false);
|
||||
CameraServerJNI.Helper.setExtractOnStaticLoad(false);
|
||||
CameraServerCvJNI.Helper.setExtractOnStaticLoad(false);
|
||||
AprilTagJNI.Helper.setExtractOnStaticLoad(false);
|
||||
|
||||
try {
|
||||
CameraServerCvJNI.forceLoad();
|
||||
// PicamJNI.forceLoad();
|
||||
} catch (IOException ex) {
|
||||
// ignored
|
||||
var loader =
|
||||
new RuntimeLoader<>(
|
||||
Core.NATIVE_LIBRARY_NAME, RuntimeLoader.getDefaultExtractionRoot(), Core.class);
|
||||
loader.loadLibrary();
|
||||
|
||||
CombinedRuntimeLoader.loadLibraries(
|
||||
TestUtils.class,
|
||||
"wpiutiljni",
|
||||
"ntcorejni",
|
||||
"wpinetjni",
|
||||
"wpiHaljni",
|
||||
"cscorejni",
|
||||
"cscorejnicvstatic",
|
||||
"apriltagjni");
|
||||
return true;
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -182,7 +213,20 @@ public class TestUtils {
|
||||
|
||||
private static Path getResourcesFolderPath(boolean testMode) {
|
||||
System.out.println("CWD: " + Path.of("").toAbsolutePath().toString());
|
||||
return Path.of("test-resources").toAbsolutePath();
|
||||
|
||||
// VSCode likes to make this path relative to the wrong root directory, so a fun hack to tell
|
||||
// if it's wrong
|
||||
Path ret = Path.of("test-resources").toAbsolutePath();
|
||||
if (Path.of("test-resources")
|
||||
.toAbsolutePath()
|
||||
.toString()
|
||||
.replace("/", "")
|
||||
.replace("\\", "")
|
||||
.toLowerCase()
|
||||
.matches(".*photon-[a-z]*test-resources")) {
|
||||
ret = Path.of("../test-resources").toAbsolutePath();
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
public static Path getTestMode2019ImagePath() {
|
||||
@@ -194,7 +238,7 @@ public class TestUtils {
|
||||
public static Path getTestMode2020ImagePath() {
|
||||
return getResourcesFolderPath(true)
|
||||
.resolve("testimages")
|
||||
.resolve(WPI2020Image.kBlueGoal_108in_Center.path);
|
||||
.resolve(WPI2020Image.kBlueGoal_156in_Left.path);
|
||||
}
|
||||
|
||||
public static Path getTestMode2022ImagePath() {
|
||||
@@ -286,6 +330,7 @@ public class TestUtils {
|
||||
private static int DefaultTimeoutMillis = 5000;
|
||||
|
||||
public static void showImage(Mat frame, String title, int timeoutMs) {
|
||||
if (frame.empty()) return;
|
||||
try {
|
||||
HighGui.imshow(title, frame);
|
||||
HighGui.waitKey(timeoutMs);
|
||||
|
||||
@@ -78,7 +78,7 @@ public class FileUtils {
|
||||
}
|
||||
|
||||
public static void setFilePerms(Path path) throws IOException {
|
||||
if (!Platform.currentPlatform.isWindows()) {
|
||||
if (Platform.isLinux()) {
|
||||
File thisFile = path.toFile();
|
||||
Set<PosixFilePermission> perms =
|
||||
Files.readAttributes(path, PosixFileAttributes.class).permissions();
|
||||
@@ -96,7 +96,7 @@ public class FileUtils {
|
||||
}
|
||||
|
||||
public static void setAllPerms(Path path) {
|
||||
if (!Platform.currentPlatform.isWindows()) {
|
||||
if (Platform.isLinux()) {
|
||||
String command = String.format("chmod 777 -R %s", path.toString());
|
||||
try {
|
||||
Process p = Runtime.getRuntime().exec(command);
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
package org.photonvision.common.util.math;
|
||||
|
||||
import edu.wpi.first.math.MatBuilder;
|
||||
import edu.wpi.first.math.Matrix;
|
||||
import edu.wpi.first.math.Nat;
|
||||
import edu.wpi.first.math.VecBuilder;
|
||||
import edu.wpi.first.math.geometry.CoordinateSystem;
|
||||
@@ -25,10 +26,14 @@ import edu.wpi.first.math.geometry.Pose3d;
|
||||
import edu.wpi.first.math.geometry.Quaternion;
|
||||
import edu.wpi.first.math.geometry.Rotation3d;
|
||||
import edu.wpi.first.math.geometry.Transform3d;
|
||||
import edu.wpi.first.math.numbers.N3;
|
||||
import edu.wpi.first.math.util.Units;
|
||||
import edu.wpi.first.util.WPIUtilJNI;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import org.ejml.data.DMatrixRMaj;
|
||||
import org.ejml.dense.row.factory.DecompositionFactory_DDRM;
|
||||
import org.ejml.simple.SimpleMatrix;
|
||||
import org.opencv.core.Mat;
|
||||
|
||||
public class MathUtils {
|
||||
@@ -99,7 +104,7 @@ public class MathUtils {
|
||||
return list.get(0); // always return single value for n = 1
|
||||
}
|
||||
|
||||
// Sort array. We avoid a third copy here by just creating the
|
||||
// Sort array. We avoid a third copy here by just creating the
|
||||
// list directly.
|
||||
double[] sorted = new double[list.size()];
|
||||
for (int i = 0; i < list.size(); i++) {
|
||||
@@ -159,19 +164,36 @@ public class MathUtils {
|
||||
pose.getTranslation().rotateBy(rotationQuat), pose.getRotation().rotateBy(rotationQuat));
|
||||
}
|
||||
|
||||
// TODO: Refactor into new pipe?
|
||||
/**
|
||||
* All our solvepnp code returns a tag with X left, Y up, and Z out of the tag To better match
|
||||
* wpilib, we want to apply another rotation so that we get Z up, X out of the tag, and Y to the
|
||||
* right. We apply the following change of basis: X -> Y Y -> Z Z -> X
|
||||
*/
|
||||
private static final Rotation3d WPILIB_BASE_ROTATION =
|
||||
new Rotation3d(new MatBuilder<>(Nat.N3(), Nat.N3()).fill(0, 1, 0, 0, 0, 1, 1, 0, 0));
|
||||
|
||||
public static Pose3d convertOpenCVtoPhotonPose(Transform3d cameraToTarget3d) {
|
||||
// TODO: Refactor into new pipe?
|
||||
// CameraToTarget _should_ be in opencv-land EDN
|
||||
return CoordinateSystem.convert(
|
||||
new Pose3d(cameraToTarget3d), CoordinateSystem.EDN(), CoordinateSystem.NWU());
|
||||
var nwu =
|
||||
CoordinateSystem.convert(
|
||||
new Pose3d().transformBy(cameraToTarget3d),
|
||||
CoordinateSystem.EDN(),
|
||||
CoordinateSystem.NWU());
|
||||
return new Pose3d(nwu.getTranslation(), WPILIB_BASE_ROTATION.rotateBy(nwu.getRotation()));
|
||||
}
|
||||
|
||||
/*
|
||||
* The AprilTag pose rotation outputs are X left, Y down, Z away from the tag with the tag facing
|
||||
* the camera upright and the camera facing the target parallel to the floor. But our OpenCV
|
||||
* solvePNP code would have X left, Y up, Z towards the camera with the target facing the camera
|
||||
* and both parallel to the floor. So we apply a base rotation to the rotation component of the
|
||||
* apriltag pose to make it consistent with the EDN system that OpenCV uses, internally a 180
|
||||
* The AprilTag pose rotation outputs are X left, Y down, Z away from the tag
|
||||
* with the tag facing
|
||||
* the camera upright and the camera facing the target parallel to the floor.
|
||||
* But our OpenCV
|
||||
* solvePNP code would have X left, Y up, Z towards the camera with the target
|
||||
* facing the camera
|
||||
* and both parallel to the floor. So we apply a base rotation to the rotation
|
||||
* component of the
|
||||
* apriltag pose to make it consistent with the EDN system that OpenCV uses,
|
||||
* internally a 180
|
||||
* rotation about the X axis
|
||||
*/
|
||||
private static final Rotation3d APRILTAG_BASE_ROTATION =
|
||||
@@ -191,4 +213,38 @@ public class MathUtils {
|
||||
var axis = rotation.getAxis().times(angle);
|
||||
rvecOutput.put(0, 0, axis.getData());
|
||||
}
|
||||
|
||||
/**
|
||||
* Orthogonalize an input matrix using a QR decomposition. QR decompositions decompose a
|
||||
* rectangular matrix 'A' such that 'A=QR', where Q is the closest orthogonal matrix to the input,
|
||||
* and R is an upper triangular matrix.
|
||||
*
|
||||
* <p>The following function is released under the BSD license avaliable in
|
||||
* LICENSE_MathUtils_orthogonalizeRotationMatrix.txt.
|
||||
*/
|
||||
public static Matrix<N3, N3> orthogonalizeRotationMatrix(Matrix<N3, N3> input) {
|
||||
var a = DecompositionFactory_DDRM.qr(3, 3);
|
||||
if (!a.decompose(input.getStorage().getDDRM())) {
|
||||
// best we can do is return the input
|
||||
return input;
|
||||
}
|
||||
|
||||
// Grab results (thanks for this _great_ api, EJML)
|
||||
var Q = new DMatrixRMaj(3, 3);
|
||||
var R = new DMatrixRMaj(3, 3);
|
||||
a.getQ(Q, false);
|
||||
a.getR(R, false);
|
||||
|
||||
// Fix signs in R if they're < 0 so it's close to an identity matrix
|
||||
// (our QR decomposition implementation sometimes flips the signs of columns)
|
||||
for (int colR = 0; colR < 3; ++colR) {
|
||||
if (R.get(colR, colR) < 0) {
|
||||
for (int rowQ = 0; rowQ < 3; ++rowQ) {
|
||||
Q.set(rowQ, colR, -Q.get(rowQ, colR));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return new Matrix<>(new SimpleMatrix(Q));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,191 @@
|
||||
/*
|
||||
* 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.raspi;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.net.URL;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import org.photonvision.common.logging.LogGroup;
|
||||
import org.photonvision.common.logging.Logger;
|
||||
|
||||
public class LibCameraJNI {
|
||||
private static boolean libraryLoaded = false;
|
||||
private static Logger logger = new Logger(LibCameraJNI.class, LogGroup.Camera);
|
||||
|
||||
public static final Object CAMERA_LOCK = new Object();
|
||||
|
||||
public static synchronized void forceLoad() throws IOException {
|
||||
if (libraryLoaded) return;
|
||||
|
||||
try {
|
||||
File libDirectory = Path.of("lib/").toFile();
|
||||
if (!libDirectory.exists()) {
|
||||
Files.createDirectory(libDirectory.toPath()).toFile();
|
||||
}
|
||||
|
||||
// We always extract the shared object (we could hash each so, but that's a lot of work)
|
||||
URL resourceURL = LibCameraJNI.class.getResource("/nativelibraries/libphotonlibcamera.so");
|
||||
File libFile = Path.of("lib/libphotonlibcamera.so").toFile();
|
||||
try (InputStream in = resourceURL.openStream()) {
|
||||
if (libFile.exists()) Files.delete(libFile.toPath());
|
||||
Files.copy(in, libFile.toPath());
|
||||
} catch (Exception e) {
|
||||
logger.error("Could not extract the native library!");
|
||||
}
|
||||
System.load(libFile.getAbsolutePath());
|
||||
|
||||
libraryLoaded = true;
|
||||
logger.info("Successfully loaded libpicam shared object");
|
||||
} catch (UnsatisfiedLinkError e) {
|
||||
logger.error("Couldn't load libpicam shared object");
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
public enum SensorModel {
|
||||
Disconnected,
|
||||
OV5647, // Picam v1
|
||||
IMX219, // Picam v2
|
||||
IMX477, // Picam HQ
|
||||
OV9281,
|
||||
OV7251,
|
||||
Unknown;
|
||||
|
||||
public String getFriendlyName() {
|
||||
switch (this) {
|
||||
case Disconnected:
|
||||
return "Disconnected Camera";
|
||||
case OV5647:
|
||||
return "Camera Module v1";
|
||||
case IMX219:
|
||||
return "Camera Module v2";
|
||||
case IMX477:
|
||||
return "HQ Camera";
|
||||
case OV9281:
|
||||
return "OV9281";
|
||||
case OV7251:
|
||||
return "OV7251";
|
||||
case Unknown:
|
||||
default:
|
||||
return "Unknown Camera";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static SensorModel getSensorModel() {
|
||||
int model = getSensorModelRaw();
|
||||
return SensorModel.values()[model];
|
||||
}
|
||||
|
||||
public static boolean isSupported() {
|
||||
return libraryLoaded
|
||||
// && getSensorModel() != PicamJNI.SensorModel.Disconnected
|
||||
// && Platform.isRaspberryPi()
|
||||
&& isLibraryWorking();
|
||||
}
|
||||
|
||||
private static native boolean isLibraryWorking();
|
||||
|
||||
public static native int getSensorModelRaw();
|
||||
|
||||
// ======================================================== //
|
||||
|
||||
/**
|
||||
* Creates a new runner with a given width/height/fps
|
||||
*
|
||||
* @param width Camera video mode width in pixels
|
||||
* @param height Camera video mode height in pixels
|
||||
* @param fps Camera video mode FPS
|
||||
* @return success of creating a camera object
|
||||
*/
|
||||
public static native boolean createCamera(int width, int height, int rotation);
|
||||
|
||||
/**
|
||||
* Starts the camera thresholder and display threads running. Make sure that this function is
|
||||
* called syncronously with stopCamera and returnFrame!
|
||||
*/
|
||||
public static native boolean startCamera();
|
||||
|
||||
/** Stops the camera runner. Make sure to call prior to destroying the camera! */
|
||||
public static native boolean stopCamera();
|
||||
|
||||
// Destroy all native resources associated with a camera. Ensure stop is called prior!
|
||||
public static native boolean destroyCamera();
|
||||
|
||||
// ======================================================== //
|
||||
|
||||
// Set thresholds on [0..1]
|
||||
public static native boolean setThresholds(
|
||||
double hl, double sl, double vl, double hu, double su, double vu, boolean hueInverted);
|
||||
|
||||
public static native boolean setAutoExposure(boolean doAutoExposure);
|
||||
|
||||
// Exposure time, in microseconds
|
||||
public static native boolean setExposure(int exposureUs);
|
||||
|
||||
// Set brighness on [-1, 1]
|
||||
public static native boolean setBrightness(double brightness);
|
||||
|
||||
// Unknown ranges for red and blue AWB gain
|
||||
public static native boolean setAwbGain(double red, double blue);
|
||||
|
||||
/**
|
||||
* Get the time when the first pixel exposure was started, in the same timebase as libcamera gives
|
||||
* the frame capture time. Units are nanoseconds.
|
||||
*/
|
||||
public static native long getFrameCaptureTime();
|
||||
|
||||
/**
|
||||
* Get the current time, in the same timebase as libcamera gives the frame capture time. Units are
|
||||
* nanoseconds.
|
||||
*/
|
||||
public static native long getLibcameraTimestamp();
|
||||
|
||||
public static native long setFramesToCopy(boolean copyIn, boolean copyOut);
|
||||
|
||||
// Analog gain multiplier to apply to all color channels, on [1, Big Number]
|
||||
public static native boolean setAnalogGain(double analog);
|
||||
|
||||
/** Block until a new frame is avaliable from native code. */
|
||||
public static native boolean awaitNewFrame();
|
||||
|
||||
/**
|
||||
* Get a pointer to the most recent color mat generated. Call this immediatly after awaitNewFrame,
|
||||
* and call onlly once per new frame!
|
||||
*/
|
||||
public static native long takeColorFrame();
|
||||
|
||||
/**
|
||||
* Get a pointer to the most recent processed mat generated. Call this immediatly after
|
||||
* awaitNewFrame, and call onlly once per new frame!
|
||||
*/
|
||||
public static native long takeProcessedFrame();
|
||||
|
||||
/**
|
||||
* Set the GPU processing type we should do. Enum of [none, HSV, greyscale, adaptive threshold].
|
||||
*/
|
||||
public static native boolean setGpuProcessType(int type);
|
||||
|
||||
public static native int getGpuProcessType();
|
||||
|
||||
// /** Release a frame pointer back to the libcamera driver code to be filled again */
|
||||
// public static native long returnFrame(long frame);
|
||||
}
|
||||
@@ -1,159 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) Photon Vision.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.photonvision.raspi;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.net.URL;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import org.photonvision.common.hardware.PiVersion;
|
||||
import org.photonvision.common.hardware.Platform;
|
||||
import org.photonvision.common.logging.LogGroup;
|
||||
import org.photonvision.common.logging.Logger;
|
||||
|
||||
public class PicamJNI {
|
||||
private static boolean libraryLoaded = false;
|
||||
private static boolean enabled =
|
||||
false; // TODO once we've sorted out what apriltags needs to be doing, we can bring this back?
|
||||
private static Logger logger = new Logger(PicamJNI.class, LogGroup.Camera);
|
||||
|
||||
public enum SensorModel {
|
||||
Disconnected,
|
||||
OV5647, // Picam v1
|
||||
IMX219, // Picam v2
|
||||
IMX477, // Picam HQ
|
||||
Unknown;
|
||||
|
||||
public String getFriendlyName() {
|
||||
switch (this) {
|
||||
case Disconnected:
|
||||
return "Disconnected Camera";
|
||||
case OV5647:
|
||||
return "Camera Module v1";
|
||||
case IMX219:
|
||||
return "Camera Module v2";
|
||||
case IMX477:
|
||||
return "HQ Camera";
|
||||
case Unknown:
|
||||
default:
|
||||
return "Unknown Camera";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static synchronized void forceLoad() throws IOException {
|
||||
if (libraryLoaded || !Platform.isRaspberryPi()) return;
|
||||
|
||||
try {
|
||||
File libDirectory = Path.of("lib/").toFile();
|
||||
if (!libDirectory.exists()) {
|
||||
Files.createDirectory(libDirectory.toPath()).toFile();
|
||||
}
|
||||
|
||||
// We always extract the shared object (we could hash each so, but that's a lot of work)
|
||||
URL resourceURL = PicamJNI.class.getResource("/nativelibraries/libpicam.so");
|
||||
File libFile = Path.of("lib/libpicam.so").toFile();
|
||||
try (InputStream in = resourceURL.openStream()) {
|
||||
if (libFile.exists()) Files.delete(libFile.toPath());
|
||||
Files.copy(in, libFile.toPath());
|
||||
} catch (Exception e) {
|
||||
logger.error("Could not extract the native library!");
|
||||
}
|
||||
System.load(libFile.getAbsolutePath());
|
||||
|
||||
libraryLoaded = true;
|
||||
logger.info("Successfully loaded libpicam shared object");
|
||||
} catch (UnsatisfiedLinkError e) {
|
||||
logger.error("Couldn't load libpicam shared object");
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean isSupported() {
|
||||
return libraryLoaded
|
||||
&& enabled
|
||||
&& isVCSMSupported()
|
||||
&& getSensorModel() != SensorModel.Disconnected
|
||||
&& Platform.isRaspberryPi()
|
||||
&& (Platform.currentPiVersion == PiVersion.PI_3
|
||||
|| Platform.currentPiVersion == PiVersion.COMPUTE_MODULE_3
|
||||
|| Platform.currentPiVersion == PiVersion.ZERO_2_W);
|
||||
}
|
||||
|
||||
public static SensorModel getSensorModel() {
|
||||
switch (getSensorModelRaw().toLowerCase()) {
|
||||
case "":
|
||||
return SensorModel.Disconnected;
|
||||
case "ov5647":
|
||||
return SensorModel.OV5647;
|
||||
case "imx219":
|
||||
return SensorModel.IMX219;
|
||||
case "imx477":
|
||||
return SensorModel.IMX477;
|
||||
default:
|
||||
return SensorModel.Unknown;
|
||||
}
|
||||
}
|
||||
|
||||
private static native String getSensorModelRaw();
|
||||
|
||||
// This is the main thing we need that isn't supported on Pi 4s, which makes it a good check
|
||||
private static native boolean isVCSMSupported();
|
||||
|
||||
// Everything here is static because multiple picams are unsupported at the hardware level
|
||||
|
||||
/**
|
||||
* Called once for each video mode change. Starts a native thread running MMAL that stays alive
|
||||
* until destroyCamera is called.
|
||||
*
|
||||
* @return true on error.
|
||||
*/
|
||||
public static native boolean createCamera(int width, int height, int fps);
|
||||
|
||||
/**
|
||||
* Destroys MMAL and EGL contexts. Called once for each video mode change *before* createCamera.
|
||||
*
|
||||
* @return true on error.
|
||||
*/
|
||||
public static native boolean destroyCamera();
|
||||
|
||||
public static native void setThresholds(
|
||||
double hL, double sL, double vL, double hU, double sU, double vU);
|
||||
|
||||
public static native void setInvertHue(boolean shouldInvert);
|
||||
|
||||
public static native boolean setExposure(int exposure);
|
||||
|
||||
public static native boolean setBrightness(int brightness);
|
||||
|
||||
// This adjusts the analog gain (normalized to 0-100); ignores the digital gain
|
||||
public static native boolean setGain(int gain);
|
||||
|
||||
// Adjusts the auto white balance gains, which are normalized 0-100 in the native code
|
||||
public static native boolean setAwbGain(int red, int blue);
|
||||
|
||||
public static native boolean setRotation(int rotation);
|
||||
|
||||
public static native void setShouldCopyColor(boolean shouldCopyColor);
|
||||
|
||||
public static native long getFrameLatency();
|
||||
|
||||
public static native long grabFrame(boolean shouldReturnColor);
|
||||
}
|
||||
@@ -1,92 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) Photon Vision.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.photonvision.vision.apriltag;
|
||||
|
||||
import org.opencv.core.Mat;
|
||||
import org.photonvision.common.logging.LogGroup;
|
||||
import org.photonvision.common.logging.Logger;
|
||||
import org.photonvision.vision.calibration.CameraCalibrationCoefficients;
|
||||
|
||||
public class AprilTagDetector {
|
||||
private static final Logger logger = new Logger(AprilTagDetector.class, LogGroup.VisionModule);
|
||||
private long m_detectorPtr = 0;
|
||||
private AprilTagDetectorParams m_detectorParams = AprilTagDetectorParams.DEFAULT_36H11;
|
||||
|
||||
public AprilTagDetector() {
|
||||
updateDetector();
|
||||
}
|
||||
|
||||
private void updateDetector() {
|
||||
if (m_detectorPtr != 0) {
|
||||
// TODO: in JNI
|
||||
AprilTagJNI.AprilTag_Destroy(m_detectorPtr);
|
||||
m_detectorPtr = 0;
|
||||
}
|
||||
|
||||
logger.debug("Creating detector with params " + m_detectorParams);
|
||||
m_detectorPtr =
|
||||
AprilTagJNI.AprilTag_Create(
|
||||
m_detectorParams.tagFamily.getNativeName(),
|
||||
m_detectorParams.decimate,
|
||||
m_detectorParams.blur,
|
||||
m_detectorParams.threads,
|
||||
m_detectorParams.debug,
|
||||
m_detectorParams.refineEdges);
|
||||
}
|
||||
|
||||
public void updateParams(AprilTagDetectorParams newParams) {
|
||||
if (!m_detectorParams.equals(newParams)) {
|
||||
m_detectorParams = newParams;
|
||||
updateDetector();
|
||||
}
|
||||
}
|
||||
|
||||
public DetectionResult[] detect(
|
||||
Mat grayscaleImg,
|
||||
CameraCalibrationCoefficients coeffs,
|
||||
boolean useNativePoseEst,
|
||||
int numIterations,
|
||||
double tagWidthMeters) {
|
||||
if (m_detectorPtr == 0) {
|
||||
// Detector not set up (JNI issue? or similar?)
|
||||
// No detection is possible.
|
||||
return new DetectionResult[] {};
|
||||
}
|
||||
|
||||
var cx = 0.0;
|
||||
var cy = 0.0;
|
||||
var fx = 0.0;
|
||||
var fy = 0.0;
|
||||
var doPoseEst = false;
|
||||
|
||||
if (coeffs != null && useNativePoseEst) {
|
||||
final Mat cameraMatrix = coeffs.getCameraIntrinsicsMat();
|
||||
if (cameraMatrix != null) {
|
||||
// Camera calibration has been done, we should be able to do pose estimation
|
||||
cx = cameraMatrix.get(0, 2)[0];
|
||||
cy = cameraMatrix.get(1, 2)[0];
|
||||
fx = cameraMatrix.get(0, 0)[0];
|
||||
fy = cameraMatrix.get(1, 1)[0];
|
||||
doPoseEst = true;
|
||||
}
|
||||
}
|
||||
|
||||
return AprilTagJNI.AprilTag_Detect(
|
||||
m_detectorPtr, grayscaleImg, doPoseEst, tagWidthMeters, fx, fy, cx, cy, numIterations);
|
||||
}
|
||||
}
|
||||
@@ -1,78 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) Photon Vision.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.photonvision.vision.apriltag;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
public class AprilTagDetectorParams {
|
||||
public static AprilTagDetectorParams DEFAULT_36H11 =
|
||||
new AprilTagDetectorParams(AprilTagFamily.kTag36h11, 1.0, 0.0, 4, false, false);
|
||||
|
||||
public final AprilTagFamily tagFamily;
|
||||
public final double decimate;
|
||||
public final double blur;
|
||||
public final int threads;
|
||||
public final boolean debug;
|
||||
public final boolean refineEdges;
|
||||
|
||||
public AprilTagDetectorParams(
|
||||
AprilTagFamily tagFamily,
|
||||
double decimate,
|
||||
double blur,
|
||||
int threads,
|
||||
boolean debug,
|
||||
boolean refineEdges) {
|
||||
this.tagFamily = tagFamily;
|
||||
this.decimate = decimate;
|
||||
this.blur = blur;
|
||||
this.threads = threads;
|
||||
this.debug = debug;
|
||||
this.refineEdges = refineEdges;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
AprilTagDetectorParams that = (AprilTagDetectorParams) o;
|
||||
return Objects.equals(tagFamily, that.tagFamily)
|
||||
&& Double.compare(decimate, that.decimate) == 0
|
||||
&& Double.compare(blur, that.blur) == 0
|
||||
&& threads == that.threads
|
||||
&& debug == that.debug
|
||||
&& refineEdges == that.refineEdges;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "AprilTagDetectorParams{"
|
||||
+ "tagFamily="
|
||||
+ tagFamily.getNativeName()
|
||||
+ ", decimate="
|
||||
+ decimate
|
||||
+ ", blur="
|
||||
+ blur
|
||||
+ ", threads="
|
||||
+ threads
|
||||
+ ", debug="
|
||||
+ debug
|
||||
+ ", refineEdges="
|
||||
+ refineEdges
|
||||
+ '}';
|
||||
}
|
||||
}
|
||||
@@ -1,182 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) Photon Vision.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.photonvision.vision.apriltag;
|
||||
|
||||
import edu.wpi.first.util.RuntimeDetector;
|
||||
import edu.wpi.first.util.RuntimeLoader;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.net.URL;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import org.opencv.core.Mat;
|
||||
import org.photonvision.common.logging.LogGroup;
|
||||
import org.photonvision.common.logging.Logger;
|
||||
|
||||
public class AprilTagJNI {
|
||||
static final boolean USE_DEBUG =
|
||||
false; // Development flag - should be false on release, but flip to True to read in a debug
|
||||
// version of the library
|
||||
static final String NATIVE_DEBUG_LIBRARY_NAME = "apriltagd";
|
||||
static final String NATIVE_RELEASE_LIBRARY_NAME = "apriltag";
|
||||
|
||||
static boolean s_libraryLoaded = false;
|
||||
static RuntimeLoader<AprilTagJNI> s_loader = null;
|
||||
private static Logger logger = new Logger(AprilTagJNI.class, LogGroup.VisionModule);
|
||||
|
||||
public static synchronized void forceLoad() throws IOException {
|
||||
if (s_libraryLoaded) return;
|
||||
|
||||
try {
|
||||
// Ensure the lib directory has been created to receive the unpacked shared object
|
||||
File libDirectory = Path.of("lib/").toFile();
|
||||
if (!libDirectory.exists()) {
|
||||
Files.createDirectory(libDirectory.toPath()).toFile();
|
||||
}
|
||||
|
||||
// Pick the proper library based on development flags
|
||||
String libBaseName = USE_DEBUG ? NATIVE_DEBUG_LIBRARY_NAME : NATIVE_RELEASE_LIBRARY_NAME;
|
||||
String libFileName = System.mapLibraryName(libBaseName);
|
||||
File libFile = Path.of("lib/" + libFileName).toFile();
|
||||
|
||||
// Always extract the library fresh
|
||||
// Yes, technically, a hashing strategy should speed this up, but it's only a
|
||||
// one-time, at-startup time hit. And not very big.
|
||||
URL resourceURL;
|
||||
|
||||
String subfolder;
|
||||
// TODO 64-bit Pi support
|
||||
if (RuntimeDetector.isAthena()) {
|
||||
subfolder = "athena";
|
||||
} else if (RuntimeDetector.isAarch64()) {
|
||||
subfolder = "aarch64";
|
||||
} else if (RuntimeDetector.isRaspbian()) {
|
||||
subfolder = "raspbian";
|
||||
} else if (RuntimeDetector.isWindows()) {
|
||||
subfolder = "win64";
|
||||
} else if (RuntimeDetector.isLinux()) {
|
||||
subfolder = "linux64";
|
||||
} else if (RuntimeDetector.isMac()) {
|
||||
subfolder = "mac";
|
||||
} // NOT m1, afaict, lol
|
||||
else {
|
||||
logger.error("Could not determine platform! Cannot load Apriltag JNI");
|
||||
return;
|
||||
}
|
||||
|
||||
resourceURL =
|
||||
AprilTagJNI.class.getResource(
|
||||
"/nativelibraries/apriltag/" + subfolder + "/" + libFileName);
|
||||
|
||||
try (InputStream in = resourceURL.openStream()) {
|
||||
// Remove the file if it already exists
|
||||
if (libFile.exists()) Files.delete(libFile.toPath());
|
||||
// Copy in a fresh resource
|
||||
Files.copy(in, libFile.toPath());
|
||||
}
|
||||
|
||||
// Actually load the library
|
||||
System.load(libFile.getAbsolutePath());
|
||||
|
||||
s_libraryLoaded = true;
|
||||
|
||||
} catch (UnsatisfiedLinkError e) {
|
||||
logger.error("Couldn't load apriltag shared object");
|
||||
e.printStackTrace();
|
||||
} catch (IOException ioe) {
|
||||
logger.error("IO exception copying apriltag shared object");
|
||||
ioe.printStackTrace();
|
||||
}
|
||||
|
||||
if (!s_libraryLoaded) {
|
||||
logger.error("Failed to load AprilTag Native Library!");
|
||||
} else {
|
||||
logger.info("AprilTag Native Library loaded successfully");
|
||||
}
|
||||
}
|
||||
|
||||
// Returns a pointer to a apriltag_detector_t
|
||||
public static native long AprilTag_Create(
|
||||
String fam, double decimate, double blur, int threads, boolean debug, boolean refine_edges);
|
||||
|
||||
// Destroy and free a previously created detector.
|
||||
public static native long AprilTag_Destroy(long detector);
|
||||
|
||||
private static native Object[] AprilTag_Detect(
|
||||
long detector,
|
||||
long imgAddr,
|
||||
int rows,
|
||||
int cols,
|
||||
boolean doPoseEstimation,
|
||||
double tagWidth,
|
||||
double fx,
|
||||
double fy,
|
||||
double cx,
|
||||
double cy,
|
||||
int nIters);
|
||||
|
||||
// Detect targets given a GRAY frame. Returns a pointer toa zarray
|
||||
public static DetectionResult[] AprilTag_Detect(
|
||||
long detector,
|
||||
Mat img,
|
||||
boolean doPoseEstimation,
|
||||
double tagWidth,
|
||||
double fx,
|
||||
double fy,
|
||||
double cx,
|
||||
double cy,
|
||||
int nIters) {
|
||||
return (DetectionResult[])
|
||||
AprilTag_Detect(
|
||||
detector,
|
||||
img.dataAddr(),
|
||||
img.rows(),
|
||||
img.cols(),
|
||||
doPoseEstimation,
|
||||
tagWidth,
|
||||
fx,
|
||||
fy,
|
||||
cx,
|
||||
cy,
|
||||
nIters);
|
||||
}
|
||||
|
||||
public static void main(String[] args) {
|
||||
// System.loadLibrary("apriltag");
|
||||
|
||||
long detector = AprilTag_Create("tag36h11", 2, 2, 1, false, true);
|
||||
|
||||
// var buff = ByteBuffer.allocateDirect(1280 * 720);
|
||||
|
||||
// // try {
|
||||
// // CameraServerCvJNI.forceLoad();
|
||||
// // } catch (IOException e) {
|
||||
// // // TODO Auto-generated catch block
|
||||
// // e.printStackTrace();
|
||||
// // }
|
||||
// // PicamJNI.forceLoad();
|
||||
// // TestUtils.loadLibraries();
|
||||
// var img = Imgcodecs.imread("~/Downloads/TagFams.jpg");
|
||||
|
||||
// var ret = AprilTag_Detect(detector, 0, 720, 1280);
|
||||
// System.out.println(detector);
|
||||
// System.out.println(ret);
|
||||
// System.out.println(List.of(ret));
|
||||
}
|
||||
}
|
||||
@@ -1,179 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) Photon Vision.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.photonvision.vision.apriltag;
|
||||
|
||||
import edu.wpi.first.math.MatBuilder;
|
||||
import edu.wpi.first.math.Nat;
|
||||
import edu.wpi.first.math.geometry.Rotation3d;
|
||||
import edu.wpi.first.math.geometry.Transform3d;
|
||||
import edu.wpi.first.math.geometry.Translation3d;
|
||||
import java.util.Arrays;
|
||||
|
||||
public class DetectionResult {
|
||||
public int getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public int getHamming() {
|
||||
return hamming;
|
||||
}
|
||||
|
||||
public float getDecisionMargin() {
|
||||
return decision_margin;
|
||||
}
|
||||
|
||||
public void setDecisionMargin(float decision_margin) {
|
||||
this.decision_margin = decision_margin;
|
||||
}
|
||||
|
||||
public double[] getHomography() {
|
||||
return homography;
|
||||
}
|
||||
|
||||
public void setHomography(double[] homography) {
|
||||
this.homography = homography;
|
||||
}
|
||||
|
||||
public double getCenterX() {
|
||||
return centerX;
|
||||
}
|
||||
|
||||
public void setCenterX(double centerX) {
|
||||
this.centerX = centerX;
|
||||
}
|
||||
|
||||
public double getCenterY() {
|
||||
return centerY;
|
||||
}
|
||||
|
||||
public void setCenterY(double centerY) {
|
||||
this.centerY = centerY;
|
||||
}
|
||||
|
||||
public double[] getCorners() {
|
||||
return corners;
|
||||
}
|
||||
|
||||
public void setCorners(double[] corners) {
|
||||
this.corners = corners;
|
||||
}
|
||||
|
||||
public double getError1() {
|
||||
return error1;
|
||||
}
|
||||
|
||||
public double getError2() {
|
||||
return error2;
|
||||
}
|
||||
|
||||
public Transform3d getPoseResult1() {
|
||||
return poseResult1;
|
||||
}
|
||||
|
||||
public Transform3d getPoseResult2() {
|
||||
return poseResult2;
|
||||
}
|
||||
|
||||
int id;
|
||||
int hamming;
|
||||
float decision_margin;
|
||||
double[] homography;
|
||||
double centerX, centerY;
|
||||
double[] corners;
|
||||
|
||||
Transform3d poseResult1;
|
||||
double error1;
|
||||
Transform3d poseResult2;
|
||||
double error2;
|
||||
|
||||
public DetectionResult(
|
||||
int id,
|
||||
int hamming,
|
||||
float decision_margin,
|
||||
double[] homography,
|
||||
double centerX,
|
||||
double centerY,
|
||||
double[] corners,
|
||||
double[] pose1TransArr,
|
||||
double[] pose1RotArr,
|
||||
double err1,
|
||||
double[] pose2TransArr,
|
||||
double[] pose2RotArr,
|
||||
double err2) {
|
||||
this.id = id;
|
||||
this.hamming = hamming;
|
||||
this.decision_margin = decision_margin;
|
||||
this.homography = homography;
|
||||
this.centerX = centerX;
|
||||
this.centerY = centerY;
|
||||
this.corners = corners;
|
||||
|
||||
this.error1 = err1;
|
||||
this.poseResult1 =
|
||||
new Transform3d(
|
||||
new Translation3d(pose1TransArr[0], pose1TransArr[1], pose1TransArr[2]),
|
||||
new Rotation3d(new MatBuilder<>(Nat.N3(), Nat.N3()).fill(pose1RotArr)));
|
||||
this.error2 = err2;
|
||||
this.poseResult2 =
|
||||
new Transform3d(
|
||||
new Translation3d(pose2TransArr[0], pose2TransArr[1], pose2TransArr[2]),
|
||||
new Rotation3d(new MatBuilder<>(Nat.N3(), Nat.N3()).fill(pose2RotArr)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the ratio of pose reprojection errors, called ambiguity. Numbers above 0.2 are likely to be
|
||||
* ambiguous.
|
||||
*/
|
||||
public double getPoseAmbiguity() {
|
||||
var min = Math.min(error1, error2);
|
||||
var max = Math.max(error1, error2);
|
||||
|
||||
if (max > 0) {
|
||||
return min / max;
|
||||
} else {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "DetectionResult [centerX="
|
||||
+ centerX
|
||||
+ ", centerY="
|
||||
+ centerY
|
||||
+ ", corners="
|
||||
+ Arrays.toString(corners)
|
||||
+ ", decision_margin="
|
||||
+ decision_margin
|
||||
+ ", error1="
|
||||
+ error1
|
||||
+ ", error2="
|
||||
+ error2
|
||||
+ ", hamming="
|
||||
+ hamming
|
||||
+ ", homography="
|
||||
+ Arrays.toString(homography)
|
||||
+ ", id="
|
||||
+ id
|
||||
+ ", poseResult1="
|
||||
+ poseResult1
|
||||
+ ", poseResult2="
|
||||
+ poseResult2
|
||||
+ "]";
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,7 @@
|
||||
|
||||
package org.photonvision.vision.calibration;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonAlias;
|
||||
import com.fasterxml.jackson.annotation.JsonCreator;
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
@@ -33,7 +34,8 @@ public class CameraCalibrationCoefficients implements Releasable {
|
||||
public final JsonMat cameraIntrinsics;
|
||||
|
||||
@JsonProperty("cameraExtrinsics")
|
||||
public final JsonMat cameraExtrinsics;
|
||||
@JsonAlias({"cameraExtrinsics", "distCoeffs"})
|
||||
public final JsonMat distCoeffs;
|
||||
|
||||
@JsonProperty("perViewErrors")
|
||||
public final double[] perViewErrors;
|
||||
@@ -45,12 +47,12 @@ public class CameraCalibrationCoefficients implements Releasable {
|
||||
public CameraCalibrationCoefficients(
|
||||
@JsonProperty("resolution") Size resolution,
|
||||
@JsonProperty("cameraIntrinsics") JsonMat cameraIntrinsics,
|
||||
@JsonProperty("cameraExtrinsics") JsonMat cameraExtrinsics,
|
||||
@JsonProperty("cameraExtrinsics") JsonMat distCoeffs,
|
||||
@JsonProperty("perViewErrors") double[] perViewErrors,
|
||||
@JsonProperty("standardDeviation") double standardDeviation) {
|
||||
this.resolution = resolution;
|
||||
this.cameraIntrinsics = cameraIntrinsics;
|
||||
this.cameraExtrinsics = cameraExtrinsics;
|
||||
this.distCoeffs = distCoeffs;
|
||||
this.perViewErrors = perViewErrors;
|
||||
this.standardDeviation = standardDeviation;
|
||||
}
|
||||
@@ -61,8 +63,8 @@ public class CameraCalibrationCoefficients implements Releasable {
|
||||
}
|
||||
|
||||
@JsonIgnore
|
||||
public MatOfDouble getCameraExtrinsicsMat() {
|
||||
return cameraExtrinsics.getAsMatOfDouble();
|
||||
public MatOfDouble getDistCoeffsMat() {
|
||||
return distCoeffs.getAsMatOfDouble();
|
||||
}
|
||||
|
||||
@JsonIgnore
|
||||
@@ -78,6 +80,6 @@ public class CameraCalibrationCoefficients implements Releasable {
|
||||
@Override
|
||||
public void release() {
|
||||
cameraIntrinsics.release();
|
||||
cameraExtrinsics.release();
|
||||
distCoeffs.release();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,220 @@
|
||||
/*
|
||||
* Copyright (C) Photon Vision.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.photonvision.vision.camera;
|
||||
|
||||
import edu.wpi.first.cscore.VideoMode;
|
||||
import edu.wpi.first.math.Pair;
|
||||
import java.util.HashMap;
|
||||
import org.photonvision.common.configuration.CameraConfiguration;
|
||||
import org.photonvision.common.util.math.MathUtils;
|
||||
import org.photonvision.raspi.LibCameraJNI;
|
||||
import org.photonvision.vision.camera.LibcameraGpuSource.FPSRatedVideoMode;
|
||||
import org.photonvision.vision.opencv.ImageRotationMode;
|
||||
import org.photonvision.vision.processes.VisionSourceSettables;
|
||||
|
||||
public class LibcameraGpuSettables extends VisionSourceSettables {
|
||||
private FPSRatedVideoMode currentVideoMode;
|
||||
private double lastExposure = 50;
|
||||
private int lastBrightness = 50;
|
||||
private boolean lastExposureMode;
|
||||
private int lastGain = 50;
|
||||
private Pair<Integer, Integer> lastAwbGains = new Pair<>(18, 18);
|
||||
private boolean m_initialized = false;
|
||||
|
||||
private ImageRotationMode m_rotationMode;
|
||||
|
||||
public void setRotation(ImageRotationMode rotationMode) {
|
||||
if (rotationMode != m_rotationMode) {
|
||||
m_rotationMode = rotationMode;
|
||||
|
||||
setVideoModeInternal(getCurrentVideoMode());
|
||||
}
|
||||
}
|
||||
|
||||
public LibcameraGpuSettables(CameraConfiguration configuration) {
|
||||
super(configuration);
|
||||
|
||||
videoModes = new HashMap<>();
|
||||
|
||||
LibCameraJNI.SensorModel sensorModel = LibCameraJNI.getSensorModel();
|
||||
|
||||
if (sensorModel == LibCameraJNI.SensorModel.IMX219) {
|
||||
// Settings for the IMX219 sensor, which is used on the Pi Camera Module v2
|
||||
videoModes.put(
|
||||
0, new FPSRatedVideoMode(VideoMode.PixelFormat.kUnknown, 320, 240, 120, 120, .39));
|
||||
videoModes.put(
|
||||
1, new FPSRatedVideoMode(VideoMode.PixelFormat.kUnknown, 320, 240, 30, 30, .39));
|
||||
videoModes.put(
|
||||
2, new FPSRatedVideoMode(VideoMode.PixelFormat.kUnknown, 640, 480, 65, 90, .39));
|
||||
videoModes.put(
|
||||
3, new FPSRatedVideoMode(VideoMode.PixelFormat.kUnknown, 640, 480, 30, 30, .39));
|
||||
// TODO: fix 1280x720 in the native code and re-add it
|
||||
videoModes.put(
|
||||
4, new FPSRatedVideoMode(VideoMode.PixelFormat.kUnknown, 1920, 1080, 15, 20, .53));
|
||||
videoModes.put(
|
||||
5, new FPSRatedVideoMode(VideoMode.PixelFormat.kUnknown, 3280 / 2, 2464 / 2, 15, 20, 1));
|
||||
videoModes.put(
|
||||
6, new FPSRatedVideoMode(VideoMode.PixelFormat.kUnknown, 3280 / 4, 2464 / 4, 15, 20, 1));
|
||||
} else if (sensorModel == LibCameraJNI.SensorModel.OV9281) {
|
||||
videoModes.put(
|
||||
0, new FPSRatedVideoMode(VideoMode.PixelFormat.kUnknown, 1280, 800, 60, 60, 1));
|
||||
videoModes.put(
|
||||
1, new FPSRatedVideoMode(VideoMode.PixelFormat.kUnknown, 1280 / 2, 800 / 2, 60, 60, 1));
|
||||
videoModes.put(
|
||||
2, new FPSRatedVideoMode(VideoMode.PixelFormat.kUnknown, 320, 240, 30, 30, .39));
|
||||
videoModes.put(
|
||||
3, new FPSRatedVideoMode(VideoMode.PixelFormat.kUnknown, 640, 480, 65, 90, .39));
|
||||
} else {
|
||||
if (sensorModel == LibCameraJNI.SensorModel.IMX477) {
|
||||
LibcameraGpuSource.logger.warn(
|
||||
"It appears you are using a Pi HQ Camera. This camera is not officially supported. You will have to set your camera FOV differently based on resolution.");
|
||||
} else if (sensorModel == LibCameraJNI.SensorModel.Unknown) {
|
||||
LibcameraGpuSource.logger.warn(
|
||||
"You have an unknown sensor connected to your Pi over CSI! This is likely a bug. If it is not, then you will have to set your camera FOV differently based on resolution.");
|
||||
}
|
||||
|
||||
// Settings for the OV5647 sensor, which is used by the Pi Camera Module v1
|
||||
videoModes.put(0, new FPSRatedVideoMode(VideoMode.PixelFormat.kUnknown, 320, 240, 90, 90, 1));
|
||||
videoModes.put(1, new FPSRatedVideoMode(VideoMode.PixelFormat.kUnknown, 640, 480, 85, 90, 1));
|
||||
videoModes.put(
|
||||
2, new FPSRatedVideoMode(VideoMode.PixelFormat.kUnknown, 960, 720, 45, 49, 0.74));
|
||||
// Half the size of the active areas on the OV5647
|
||||
videoModes.put(
|
||||
3, new FPSRatedVideoMode(VideoMode.PixelFormat.kUnknown, 2592 / 2, 1944 / 2, 20, 20, 1));
|
||||
videoModes.put(
|
||||
4, new FPSRatedVideoMode(VideoMode.PixelFormat.kUnknown, 1280, 720, 30, 45, 0.91));
|
||||
videoModes.put(
|
||||
5, new FPSRatedVideoMode(VideoMode.PixelFormat.kUnknown, 1920, 1080, 15, 20, 0.72));
|
||||
}
|
||||
|
||||
// TODO need to add more video modes for new sensors here
|
||||
|
||||
currentVideoMode = (FPSRatedVideoMode) videoModes.get(0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public double getFOV() {
|
||||
return getCurrentVideoMode().fovMultiplier * getConfiguration().FOV;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setAutoExposure(boolean cameraAutoExposure) {
|
||||
lastExposureMode = cameraAutoExposure;
|
||||
// TODO (Matt) -- call LibCameraJNI's auto exposure function, when that exists
|
||||
LibCameraJNI.setAutoExposure(cameraAutoExposure);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setExposure(double exposure) {
|
||||
// Todo (Chris) - for now, handle auto exposure by using -1
|
||||
if (exposure < 0.0) {
|
||||
exposure = -1;
|
||||
}
|
||||
|
||||
// TODO convert to uS
|
||||
lastExposure = exposure;
|
||||
var success = LibCameraJNI.setExposure((int) Math.round(exposure) * 800);
|
||||
if (!success) LibcameraGpuSource.logger.warn("Couldn't set Pi Camera exposure");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setBrightness(int brightness) {
|
||||
lastBrightness = brightness;
|
||||
double realBrightness = MathUtils.map(brightness, 0.0, 100.0, -1.0, 1.0);
|
||||
var success = LibCameraJNI.setBrightness(realBrightness);
|
||||
if (!success) LibcameraGpuSource.logger.warn("Couldn't set Pi Camera brightness");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setGain(int gain) {
|
||||
lastGain = gain;
|
||||
// TODO units here seem odd -- 5ish seems legit? So divide by 10
|
||||
var success = LibCameraJNI.setAnalogGain(gain / 10.0);
|
||||
if (!success) LibcameraGpuSource.logger.warn("Couldn't set Pi Camera gain");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setRedGain(int red) {
|
||||
lastAwbGains = Pair.of(red, lastAwbGains.getSecond());
|
||||
setAwbGain(lastAwbGains.getFirst(), lastAwbGains.getSecond());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setBlueGain(int blue) {
|
||||
lastAwbGains = Pair.of(lastAwbGains.getFirst(), blue);
|
||||
setAwbGain(lastAwbGains.getFirst(), lastAwbGains.getSecond());
|
||||
}
|
||||
|
||||
public void setAwbGain(int red, int blue) {
|
||||
var success = LibCameraJNI.setAwbGain(red / 10.0, blue / 10.0);
|
||||
if (!success) LibcameraGpuSource.logger.warn("Couldn't set Pi Camera AWB gains");
|
||||
}
|
||||
|
||||
@Override
|
||||
public FPSRatedVideoMode getCurrentVideoMode() {
|
||||
return currentVideoMode;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void setVideoModeInternal(VideoMode videoMode) {
|
||||
var mode = (FPSRatedVideoMode) videoMode;
|
||||
|
||||
// We need to make sure that other threads don't try to do anything funny while we're recreating
|
||||
// the camera
|
||||
synchronized (LibCameraJNI.CAMERA_LOCK) {
|
||||
boolean success = false;
|
||||
if (m_initialized) {
|
||||
success |= LibCameraJNI.stopCamera();
|
||||
success |= LibCameraJNI.destroyCamera();
|
||||
}
|
||||
|
||||
// if (!success) {
|
||||
// throw new RuntimeException(
|
||||
// "Couldn't destroy a zero copy Pi Camera while switching video modes");
|
||||
// }
|
||||
|
||||
System.out.println("Starting camera");
|
||||
success |=
|
||||
LibCameraJNI.createCamera(
|
||||
mode.width, mode.height, (m_rotationMode == ImageRotationMode.DEG_180 ? 180 : 0));
|
||||
success |= LibCameraJNI.startCamera();
|
||||
if (!success) {
|
||||
throw new RuntimeException(
|
||||
"Couldn't create a zero copy Pi Camera while switching video modes");
|
||||
}
|
||||
m_initialized = true;
|
||||
}
|
||||
|
||||
// We don't store last settings on the native side, and when you change video mode these get
|
||||
// reset on MMAL's end
|
||||
setExposure(lastExposure);
|
||||
setAutoExposure(lastExposureMode);
|
||||
setBrightness(lastBrightness);
|
||||
setGain(lastGain);
|
||||
setAwbGain(lastAwbGains.getFirst(), lastAwbGains.getSecond());
|
||||
|
||||
LibCameraJNI.setFramesToCopy(true, true);
|
||||
|
||||
currentVideoMode = mode;
|
||||
}
|
||||
|
||||
@Override
|
||||
public HashMap<Integer, VideoMode> getAllVideoModes() {
|
||||
return videoModes;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
/*
|
||||
* Copyright (C) Photon Vision.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.photonvision.vision.camera;
|
||||
|
||||
import edu.wpi.first.cscore.VideoMode;
|
||||
import org.photonvision.common.configuration.CameraConfiguration;
|
||||
import org.photonvision.common.configuration.ConfigManager;
|
||||
import org.photonvision.common.logging.LogGroup;
|
||||
import org.photonvision.common.logging.Logger;
|
||||
import org.photonvision.vision.frame.FrameProvider;
|
||||
import org.photonvision.vision.frame.provider.LibcameraGpuFrameProvider;
|
||||
import org.photonvision.vision.processes.VisionSource;
|
||||
import org.photonvision.vision.processes.VisionSourceSettables;
|
||||
|
||||
public class LibcameraGpuSource extends VisionSource {
|
||||
static final Logger logger = new Logger(LibcameraGpuSource.class, LogGroup.Camera);
|
||||
|
||||
private final LibcameraGpuSettables settables;
|
||||
private final LibcameraGpuFrameProvider frameProvider;
|
||||
|
||||
public LibcameraGpuSource(CameraConfiguration configuration) {
|
||||
super(configuration);
|
||||
if (configuration.cameraType != CameraType.ZeroCopyPicam) {
|
||||
throw new IllegalArgumentException(
|
||||
"GPUAcceleratedPicamSource only accepts CameraConfigurations with type Picam");
|
||||
}
|
||||
|
||||
settables = new LibcameraGpuSettables(configuration);
|
||||
frameProvider = new LibcameraGpuFrameProvider(settables);
|
||||
}
|
||||
|
||||
@Override
|
||||
public FrameProvider getFrameProvider() {
|
||||
return frameProvider;
|
||||
}
|
||||
|
||||
@Override
|
||||
public VisionSourceSettables getSettables() {
|
||||
return settables;
|
||||
}
|
||||
|
||||
/**
|
||||
* On the OV5649 the actual FPS we want to request from the GPU can be higher than the FPS that we
|
||||
* can do after processing. On the IMX219 these FPSes match pretty closely, except for the
|
||||
* 1280x720 mode. We use this to present a rated FPS to the user that's lower than the actual FPS
|
||||
* we request from the GPU. This is important for setting user expectations, and is also used by
|
||||
* the frontend to detect and explain FPS drops. This class should ONLY be used by Picam video
|
||||
* modes! This is to make sure it shows up nice in the frontend
|
||||
*/
|
||||
public static class FPSRatedVideoMode extends VideoMode {
|
||||
public final int fpsActual;
|
||||
public final double fovMultiplier;
|
||||
|
||||
public FPSRatedVideoMode(
|
||||
PixelFormat pixelFormat,
|
||||
int width,
|
||||
int height,
|
||||
int ratedFPS,
|
||||
int actualFPS,
|
||||
double fovMultiplier) {
|
||||
super(pixelFormat, width, height, ratedFPS);
|
||||
|
||||
this.fpsActual = actualFPS;
|
||||
this.fovMultiplier = fovMultiplier;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isVendorCamera() {
|
||||
return ConfigManager.getInstance().getConfig().getHardwareConfig().hasPresetFOV();
|
||||
}
|
||||
}
|
||||
@@ -29,9 +29,21 @@ public class QuirkyCamera {
|
||||
0x5A3,
|
||||
CameraQuirk.CompletelyBroken), // Chris's older generic "Logitec HD Webcam"
|
||||
new QuirkyCamera(0x825, 0x46D, CameraQuirk.CompletelyBroken), // Logitec C270
|
||||
new QuirkyCamera(
|
||||
0x0bda,
|
||||
0x5510,
|
||||
CameraQuirk.CompletelyBroken), // A laptop internal camera someone found broken
|
||||
new QuirkyCamera(
|
||||
-1, -1, "Snap Camera", CameraQuirk.CompletelyBroken), // SnapCamera on Windows
|
||||
new QuirkyCamera(
|
||||
-1,
|
||||
-1,
|
||||
"FaceTime HD Camera",
|
||||
CameraQuirk.CompletelyBroken), // Mac Facetime Camera shared into Windows in Bootcamp
|
||||
new QuirkyCamera(0x2000, 0x1415, CameraQuirk.Gain, CameraQuirk.FPSCap100), // PS3Eye
|
||||
new QuirkyCamera(
|
||||
-1, -1, "mmal service 16.1", CameraQuirk.PiCam), // PiCam (via V4L2, not zerocopy)
|
||||
new QuirkyCamera(-1, -1, "unicam", CameraQuirk.PiCam), // PiCam (via V4L2, not zerocopy)
|
||||
new QuirkyCamera(0x85B, 0x46D, CameraQuirk.AdjustableFocus) // Logitech C925-e
|
||||
);
|
||||
|
||||
|
||||
@@ -44,7 +44,7 @@ public class USBCameraSource extends VisionSource {
|
||||
|
||||
logger = new Logger(USBCameraSource.class, config.nickname, LogGroup.Camera);
|
||||
camera = new UsbCamera(config.nickname, config.path);
|
||||
cvSink = CameraServer.getInstance().getVideo(this.camera);
|
||||
cvSink = CameraServer.getVideo(this.camera);
|
||||
|
||||
cameraQuirks =
|
||||
QuirkyCamera.getQuirkyCamera(
|
||||
@@ -65,7 +65,12 @@ public class USBCameraSource extends VisionSource {
|
||||
disableAutoFocus();
|
||||
|
||||
usbCameraSettables = new USBCameraSettables(config);
|
||||
usbFrameProvider = new USBFrameProvider(cvSink, usbCameraSettables);
|
||||
if (usbCameraSettables.getAllVideoModes().isEmpty()) {
|
||||
logger.info("Camera " + camera.getPath() + " has no video modes supported by PhotonVision");
|
||||
usbFrameProvider = null;
|
||||
} else {
|
||||
usbFrameProvider = new USBFrameProvider(cvSink, usbCameraSettables);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,234 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) Photon Vision.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.photonvision.vision.camera;
|
||||
|
||||
import edu.wpi.first.cscore.VideoMode;
|
||||
import edu.wpi.first.math.Pair;
|
||||
import java.util.HashMap;
|
||||
import org.photonvision.common.configuration.CameraConfiguration;
|
||||
import org.photonvision.common.configuration.ConfigManager;
|
||||
import org.photonvision.common.logging.LogGroup;
|
||||
import org.photonvision.common.logging.Logger;
|
||||
import org.photonvision.raspi.PicamJNI;
|
||||
import org.photonvision.vision.frame.FrameProvider;
|
||||
import org.photonvision.vision.frame.provider.AcceleratedPicamFrameProvider;
|
||||
import org.photonvision.vision.processes.VisionSource;
|
||||
import org.photonvision.vision.processes.VisionSourceSettables;
|
||||
|
||||
public class ZeroCopyPicamSource extends VisionSource {
|
||||
private static final Logger logger = new Logger(ZeroCopyPicamSource.class, LogGroup.Camera);
|
||||
|
||||
private final VisionSourceSettables settables;
|
||||
private final AcceleratedPicamFrameProvider frameProvider;
|
||||
|
||||
public ZeroCopyPicamSource(CameraConfiguration configuration) {
|
||||
super(configuration);
|
||||
if (configuration.cameraType != CameraType.ZeroCopyPicam) {
|
||||
throw new IllegalArgumentException(
|
||||
"GPUAcceleratedPicamSource only accepts CameraConfigurations with type Picam");
|
||||
}
|
||||
|
||||
settables = new PicamSettables(configuration);
|
||||
frameProvider = new AcceleratedPicamFrameProvider(settables);
|
||||
}
|
||||
|
||||
@Override
|
||||
public FrameProvider getFrameProvider() {
|
||||
return frameProvider;
|
||||
}
|
||||
|
||||
@Override
|
||||
public VisionSourceSettables getSettables() {
|
||||
return settables;
|
||||
}
|
||||
|
||||
/**
|
||||
* On the OV5649 the actual FPS we want to request from the GPU can be higher than the FPS that we
|
||||
* can do after processing. On the IMX219 these FPSes match pretty closely, except for the
|
||||
* 1280x720 mode. We use this to present a rated FPS to the user that's lower than the actual FPS
|
||||
* we request from the GPU. This is important for setting user expectations, and is also used by
|
||||
* the frontend to detect and explain FPS drops. This class should ONLY be used by Picam video
|
||||
* modes! This is to make sure it shows up nice in the frontend
|
||||
*/
|
||||
public static class FPSRatedVideoMode extends VideoMode {
|
||||
public final int fpsActual;
|
||||
public final double fovMultiplier;
|
||||
|
||||
public FPSRatedVideoMode(
|
||||
PixelFormat pixelFormat,
|
||||
int width,
|
||||
int height,
|
||||
int ratedFPS,
|
||||
int actualFPS,
|
||||
double fovMultiplier) {
|
||||
super(pixelFormat, width, height, ratedFPS);
|
||||
|
||||
this.fpsActual = actualFPS;
|
||||
this.fovMultiplier = fovMultiplier;
|
||||
}
|
||||
}
|
||||
|
||||
public static class PicamSettables extends VisionSourceSettables {
|
||||
private FPSRatedVideoMode currentVideoMode;
|
||||
private double lastExposure = 50;
|
||||
private int lastBrightness = 50;
|
||||
private boolean lastExposureMode;
|
||||
private int lastGain = 50;
|
||||
private Pair<Integer, Integer> lastAwbGains = new Pair(18, 18);
|
||||
|
||||
public PicamSettables(CameraConfiguration configuration) {
|
||||
super(configuration);
|
||||
|
||||
videoModes = new HashMap<>();
|
||||
PicamJNI.SensorModel sensorModel = PicamJNI.getSensorModel();
|
||||
|
||||
if (sensorModel == PicamJNI.SensorModel.IMX219) {
|
||||
// Settings for the IMX219 sensor, which is used on the Pi Camera Module v2
|
||||
videoModes.put(
|
||||
0, new FPSRatedVideoMode(VideoMode.PixelFormat.kUnknown, 320, 240, 120, 120, .39));
|
||||
videoModes.put(
|
||||
1, new FPSRatedVideoMode(VideoMode.PixelFormat.kUnknown, 320, 240, 30, 30, .39));
|
||||
videoModes.put(
|
||||
2, new FPSRatedVideoMode(VideoMode.PixelFormat.kUnknown, 640, 480, 65, 90, .39));
|
||||
videoModes.put(
|
||||
3, new FPSRatedVideoMode(VideoMode.PixelFormat.kUnknown, 640, 480, 30, 30, .39));
|
||||
// TODO: fix 1280x720 in the native code and re-add it
|
||||
videoModes.put(
|
||||
4, new FPSRatedVideoMode(VideoMode.PixelFormat.kUnknown, 1920, 1080, 15, 20, .53));
|
||||
} else {
|
||||
if (sensorModel == PicamJNI.SensorModel.IMX477) {
|
||||
logger.warn(
|
||||
"It appears you are using a Pi HQ Camera. This camera is not officially supported. You will have to set your camera FOV differently based on resolution.");
|
||||
} else if (sensorModel == PicamJNI.SensorModel.Unknown) {
|
||||
logger.warn(
|
||||
"You have an unknown sensor connected to your Pi over CSI! This is likely a bug. If it is not, then you will have to set your camera FOV differently based on resolution.");
|
||||
}
|
||||
|
||||
// Settings for the OV5647 sensor, which is used by the Pi Camera Module v1
|
||||
videoModes.put(
|
||||
0, new FPSRatedVideoMode(VideoMode.PixelFormat.kUnknown, 320, 240, 90, 90, 1));
|
||||
videoModes.put(
|
||||
1, new FPSRatedVideoMode(VideoMode.PixelFormat.kUnknown, 320, 240, 30, 30, 1));
|
||||
videoModes.put(
|
||||
2, new FPSRatedVideoMode(VideoMode.PixelFormat.kUnknown, 640, 480, 85, 90, 1));
|
||||
videoModes.put(
|
||||
3, new FPSRatedVideoMode(VideoMode.PixelFormat.kUnknown, 640, 480, 30, 30, 1));
|
||||
videoModes.put(
|
||||
4, new FPSRatedVideoMode(VideoMode.PixelFormat.kUnknown, 960, 720, 45, 49, 0.74));
|
||||
videoModes.put(
|
||||
5, new FPSRatedVideoMode(VideoMode.PixelFormat.kUnknown, 1280, 720, 30, 45, 0.91));
|
||||
videoModes.put(
|
||||
6, new FPSRatedVideoMode(VideoMode.PixelFormat.kUnknown, 1920, 1080, 15, 20, 0.72));
|
||||
}
|
||||
|
||||
currentVideoMode = (FPSRatedVideoMode) videoModes.get(0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public double getFOV() {
|
||||
return getCurrentVideoMode().fovMultiplier * getConfiguration().FOV;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setAutoExposure(boolean cameraAutoExposure) {
|
||||
lastExposureMode = cameraAutoExposure;
|
||||
// TODO (Matt) -- call PicamJNI's auto exposure function, when that exists
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setExposure(double exposure) {
|
||||
// Todo (Chris) - for now, handle auto exposure by using 100% exposure
|
||||
if (exposure < 0.0) {
|
||||
exposure = 100.0;
|
||||
}
|
||||
|
||||
lastExposure = exposure;
|
||||
var failure = PicamJNI.setExposure((int) Math.round(exposure));
|
||||
if (failure) logger.warn("Couldn't set Pi Camera exposure");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setBrightness(int brightness) {
|
||||
lastBrightness = brightness;
|
||||
var failure = PicamJNI.setBrightness(brightness);
|
||||
if (failure) logger.warn("Couldn't set Pi Camera brightness");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setGain(int gain) {
|
||||
lastGain = gain;
|
||||
var failure = PicamJNI.setGain(gain);
|
||||
if (failure) logger.warn("Couldn't set Pi Camera gain");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setRedGain(int red) {
|
||||
lastAwbGains = Pair.of(red, lastAwbGains.getSecond());
|
||||
setAwbGain(lastAwbGains.getFirst(), lastAwbGains.getSecond());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setBlueGain(int blue) {
|
||||
lastAwbGains = Pair.of(lastAwbGains.getFirst(), blue);
|
||||
setAwbGain(lastAwbGains.getFirst(), lastAwbGains.getSecond());
|
||||
}
|
||||
|
||||
public void setAwbGain(int red, int blue) {
|
||||
var failure = PicamJNI.setAwbGain(red, blue);
|
||||
if (failure) logger.warn("Couldn't set Pi Camera AWB gains");
|
||||
}
|
||||
|
||||
@Override
|
||||
public FPSRatedVideoMode getCurrentVideoMode() {
|
||||
return currentVideoMode;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void setVideoModeInternal(VideoMode videoMode) {
|
||||
var mode = (FPSRatedVideoMode) videoMode;
|
||||
var failure = PicamJNI.destroyCamera();
|
||||
if (failure)
|
||||
throw new RuntimeException(
|
||||
"Couldn't destroy a zero copy Pi Camera while switching video modes");
|
||||
failure = PicamJNI.createCamera(mode.width, mode.height, mode.fpsActual);
|
||||
if (failure)
|
||||
throw new RuntimeException(
|
||||
"Couldn't create a zero copy Pi Camera while switching video modes");
|
||||
|
||||
// We don't store last settings on the native side, and when you change video mode these get
|
||||
// reset on MMAL's end
|
||||
setExposure(lastExposure);
|
||||
setAutoExposure(lastExposureMode);
|
||||
setBrightness(lastBrightness);
|
||||
setGain(lastGain);
|
||||
setAwbGain(lastAwbGains.getFirst(), lastAwbGains.getSecond());
|
||||
|
||||
currentVideoMode = mode;
|
||||
}
|
||||
|
||||
@Override
|
||||
public HashMap<Integer, VideoMode> getAllVideoModes() {
|
||||
return videoModes;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isVendorCamera() {
|
||||
return ConfigManager.getInstance().getConfig().getHardwareConfig().hasPresetFOV();
|
||||
}
|
||||
}
|
||||
@@ -17,52 +17,58 @@
|
||||
|
||||
package org.photonvision.vision.frame;
|
||||
|
||||
import org.opencv.core.CvType;
|
||||
import org.opencv.core.Mat;
|
||||
import org.opencv.core.Size;
|
||||
import org.photonvision.common.util.math.MathUtils;
|
||||
import org.photonvision.vision.opencv.CVMat;
|
||||
import org.photonvision.vision.opencv.Releasable;
|
||||
|
||||
public class Frame implements Releasable {
|
||||
public final long timestampNanos;
|
||||
public final CVMat image;
|
||||
|
||||
// Frame should at _least_ contain the thresholded frame, and sometimes the color image
|
||||
public final CVMat colorImage;
|
||||
public final CVMat processedImage;
|
||||
public final FrameThresholdType type;
|
||||
|
||||
public final FrameStaticProperties frameStaticProperties;
|
||||
|
||||
public Frame(CVMat image, long timestampNanos, FrameStaticProperties frameStaticProperties) {
|
||||
this.image = image;
|
||||
public Frame(
|
||||
CVMat color,
|
||||
CVMat processed,
|
||||
FrameThresholdType type,
|
||||
long timestampNanos,
|
||||
FrameStaticProperties frameStaticProperties) {
|
||||
this.colorImage = color;
|
||||
this.processedImage = processed;
|
||||
this.type = type;
|
||||
this.timestampNanos = timestampNanos;
|
||||
this.frameStaticProperties = frameStaticProperties;
|
||||
}
|
||||
|
||||
public Frame(CVMat image, FrameStaticProperties frameStaticProperties) {
|
||||
this(image, MathUtils.wpiNanoTime(), frameStaticProperties);
|
||||
public Frame(
|
||||
CVMat color,
|
||||
CVMat processed,
|
||||
FrameThresholdType processType,
|
||||
FrameStaticProperties frameStaticProperties) {
|
||||
this(color, processed, processType, MathUtils.wpiNanoTime(), frameStaticProperties);
|
||||
}
|
||||
|
||||
public Frame() {
|
||||
this(new CVMat(), MathUtils.wpiNanoTime(), new FrameStaticProperties(0, 0, 0, null));
|
||||
}
|
||||
|
||||
public static Frame emptyFrame(int width, int height) {
|
||||
return new Frame(
|
||||
new CVMat(Mat.zeros(new Size(width, height), CvType.CV_8UC3)),
|
||||
this(
|
||||
new CVMat(),
|
||||
new CVMat(),
|
||||
FrameThresholdType.NONE,
|
||||
MathUtils.wpiNanoTime(),
|
||||
new FrameStaticProperties(width, height, 0, null));
|
||||
new FrameStaticProperties(0, 0, 0, null));
|
||||
}
|
||||
|
||||
public void copyTo(Frame destFrame) {
|
||||
image.getMat().copyTo(destFrame.image.getMat());
|
||||
}
|
||||
|
||||
public static Frame copyFromAndRelease(Frame frame) {
|
||||
var mat = new CVMat();
|
||||
frame.image.copyTo(mat);
|
||||
frame.release();
|
||||
return new Frame(mat, frame.timestampNanos, frame.frameStaticProperties);
|
||||
colorImage.getMat().copyTo(destFrame.colorImage.getMat());
|
||||
processedImage.getMat().copyTo(destFrame.processedImage.getMat());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void release() {
|
||||
image.release();
|
||||
colorImage.release();
|
||||
processedImage.release();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,7 +18,21 @@
|
||||
package org.photonvision.vision.frame;
|
||||
|
||||
import java.util.function.Supplier;
|
||||
import org.photonvision.vision.opencv.ImageRotationMode;
|
||||
import org.photonvision.vision.pipe.impl.HSVPipe;
|
||||
|
||||
public interface FrameProvider extends Supplier<Frame> {
|
||||
String getName();
|
||||
|
||||
/** Ask the camera to produce a certain kind of processed image (eg HSV or greyscale) */
|
||||
public void requestFrameThresholdType(FrameThresholdType type);
|
||||
|
||||
/** Ask the camera to rotate frames it outputs */
|
||||
public void requestFrameRotation(ImageRotationMode rotationMode);
|
||||
|
||||
/** Ask the camera to provide either the input, output, or both frames. */
|
||||
public void requestFrameCopies(boolean copyInput, boolean copyOutput);
|
||||
|
||||
/** Ask the camera to rotate frames it outputs */
|
||||
public void requestHsvSettings(HSVPipe.HSVParams params);
|
||||
}
|
||||
|
||||
@@ -15,11 +15,10 @@
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.photonvision.common.hardware.metrics;
|
||||
package org.photonvision.vision.frame;
|
||||
|
||||
public class DiskMetrics extends MetricsBase {
|
||||
public String getUsedDiskPct() {
|
||||
if (diskUsageCommand.isEmpty()) return "";
|
||||
return execute(diskUsageCommand);
|
||||
}
|
||||
public enum FrameThresholdType {
|
||||
NONE,
|
||||
HSV,
|
||||
GREYSCALE,
|
||||
}
|
||||
@@ -17,8 +17,8 @@
|
||||
|
||||
package org.photonvision.vision.frame.consumer;
|
||||
|
||||
import edu.wpi.first.networktables.BooleanEntry;
|
||||
import edu.wpi.first.networktables.NetworkTable;
|
||||
import edu.wpi.first.networktables.NetworkTableEntry;
|
||||
import java.io.File;
|
||||
import java.text.DateFormat;
|
||||
import java.text.SimpleDateFormat;
|
||||
@@ -31,9 +31,9 @@ import org.photonvision.common.dataflow.networktables.NetworkTablesManager;
|
||||
import org.photonvision.common.logging.LogGroup;
|
||||
import org.photonvision.common.logging.Logger;
|
||||
import org.photonvision.common.util.TimedTaskManager;
|
||||
import org.photonvision.vision.frame.Frame;
|
||||
import org.photonvision.vision.opencv.CVMat;
|
||||
|
||||
public class FileSaveFrameConsumer implements Consumer<Frame> {
|
||||
public class FileSaveFrameConsumer implements Consumer<CVMat> {
|
||||
// Formatters to generate unique, timestamped file names
|
||||
private static String FILE_PATH = ConfigManager.getInstance().getImageSavePath().toString();
|
||||
private static String FILE_EXTENSION = ".jpg";
|
||||
@@ -48,7 +48,7 @@ public class FileSaveFrameConsumer implements Consumer<Frame> {
|
||||
private String camNickname;
|
||||
private String fnamePrefix;
|
||||
private final long CMD_RESET_TIME_MS = 500;
|
||||
private final NetworkTableEntry entry;
|
||||
private final BooleanEntry entry;
|
||||
// Helps prevent race conditions between user set & auto-reset logic
|
||||
private ReentrantLock lock;
|
||||
|
||||
@@ -58,15 +58,14 @@ public class FileSaveFrameConsumer implements Consumer<Frame> {
|
||||
this.ntEntryName = streamPrefix + NT_SUFFIX;
|
||||
this.rootTable = NetworkTablesManager.getInstance().kRootTable;
|
||||
updateCameraNickname(camNickname);
|
||||
entry = subTable.getEntry(ntEntryName);
|
||||
entry.forceSetBoolean(false);
|
||||
entry = subTable.getBooleanTopic(ntEntryName).getEntry(false);
|
||||
this.logger = new Logger(FileSaveFrameConsumer.class, this.camNickname, LogGroup.General);
|
||||
}
|
||||
|
||||
public void accept(Frame frame) {
|
||||
if (frame != null && !frame.image.getMat().empty()) {
|
||||
public void accept(CVMat image) {
|
||||
if (image != null && image.getMat() != null && !image.getMat().empty()) {
|
||||
if (lock.tryLock()) {
|
||||
boolean curCommand = entry.getBoolean(false);
|
||||
boolean curCommand = entry.get(false);
|
||||
if (curCommand && !prevCommand) {
|
||||
Date now = new Date();
|
||||
String savefile =
|
||||
@@ -79,7 +78,7 @@ public class FileSaveFrameConsumer implements Consumer<Frame> {
|
||||
+ tf.format(now)
|
||||
+ FILE_EXTENSION;
|
||||
|
||||
Imgcodecs.imwrite(savefile, frame.image.getMat());
|
||||
Imgcodecs.imwrite(savefile, image.getMat());
|
||||
|
||||
// Help the user a bit - set the NT entry back to false after 500ms
|
||||
TimedTaskManager.getInstance().addOneShotTask(this::resetCommand, CMD_RESET_TIME_MS);
|
||||
@@ -88,7 +87,7 @@ public class FileSaveFrameConsumer implements Consumer<Frame> {
|
||||
} else if (!curCommand) {
|
||||
// If the entry is currently false, set it again. This will make sure it shows up on the
|
||||
// dashboard.
|
||||
entry.forceSetBoolean(false);
|
||||
entry.set(false);
|
||||
}
|
||||
|
||||
prevCommand = curCommand;
|
||||
@@ -106,7 +105,7 @@ public class FileSaveFrameConsumer implements Consumer<Frame> {
|
||||
private void removeEntries() {
|
||||
if (this.subTable != null) {
|
||||
if (this.subTable.containsKey(ntEntryName)) {
|
||||
this.subTable.delete(ntEntryName);
|
||||
this.subTable.getEntry(ntEntryName).close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ import org.opencv.core.Point;
|
||||
import org.opencv.core.Rect;
|
||||
import org.opencv.imgproc.Imgproc;
|
||||
import org.photonvision.common.util.ColorHelper;
|
||||
import org.photonvision.vision.frame.Frame;
|
||||
import org.photonvision.vision.opencv.CVMat;
|
||||
|
||||
public class MJPGFrameConsumer {
|
||||
public static final Mat EMPTY_MAT = new Mat(60, 15 * 7, CvType.CV_8UC3);
|
||||
@@ -119,6 +119,7 @@ public class MJPGFrameConsumer {
|
||||
|
||||
this.mjpegServer = new MjpegServer("serve_" + cvSource.getName(), port);
|
||||
mjpegServer.setSource(cvSource);
|
||||
mjpegServer.setCompression(75);
|
||||
|
||||
listener =
|
||||
new VideoListener(
|
||||
@@ -166,9 +167,9 @@ public class MJPGFrameConsumer {
|
||||
this(name, 320, 240, port);
|
||||
}
|
||||
|
||||
public void accept(Frame frame) {
|
||||
if (frame != null && !frame.image.getMat().empty()) {
|
||||
cvSource.putFrame(frame.image.getMat());
|
||||
public void accept(CVMat image) {
|
||||
if (image != null && !image.getMat().empty()) {
|
||||
cvSource.putFrame(image.getMat());
|
||||
|
||||
// Make sure our disabled framerate limiting doesn't get confused
|
||||
isDisabled = false;
|
||||
|
||||
@@ -1,60 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) Photon Vision.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.photonvision.vision.frame.provider;
|
||||
|
||||
import org.opencv.core.Mat;
|
||||
import org.photonvision.common.util.math.MathUtils;
|
||||
import org.photonvision.raspi.PicamJNI;
|
||||
import org.photonvision.vision.frame.Frame;
|
||||
import org.photonvision.vision.frame.FrameProvider;
|
||||
import org.photonvision.vision.opencv.CVMat;
|
||||
import org.photonvision.vision.processes.VisionSourceSettables;
|
||||
|
||||
public class AcceleratedPicamFrameProvider implements FrameProvider {
|
||||
private final VisionSourceSettables settables;
|
||||
|
||||
private CVMat mat;
|
||||
|
||||
public AcceleratedPicamFrameProvider(VisionSourceSettables visionSettables) {
|
||||
this.settables = visionSettables;
|
||||
|
||||
var vidMode = settables.getCurrentVideoMode();
|
||||
var failure = PicamJNI.createCamera(vidMode.width, vidMode.height, vidMode.fps);
|
||||
if (failure) {
|
||||
failure = PicamJNI.destroyCamera();
|
||||
if (failure) throw new RuntimeException("Couldn't destroy Pi camera after init failure!");
|
||||
throw new RuntimeException(
|
||||
"Couldn't initialize zero copy Pi camera; check stdout for native code logs");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "AcceleratedPicamFrameProvider";
|
||||
}
|
||||
|
||||
@Override
|
||||
public Frame get() {
|
||||
long matHandle = PicamJNI.grabFrame(false);
|
||||
mat = new CVMat(new Mat(matHandle));
|
||||
return new Frame(
|
||||
mat,
|
||||
MathUtils.wpiNanoTime() - PicamJNI.getFrameLatency(),
|
||||
settables.getFrameStaticProperties());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
/*
|
||||
* Copyright (C) Photon Vision.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.photonvision.vision.frame.provider;
|
||||
|
||||
import org.photonvision.common.util.numbers.IntegerCouple;
|
||||
import org.photonvision.vision.frame.Frame;
|
||||
import org.photonvision.vision.frame.FrameProvider;
|
||||
import org.photonvision.vision.frame.FrameStaticProperties;
|
||||
import org.photonvision.vision.frame.FrameThresholdType;
|
||||
import org.photonvision.vision.opencv.CVMat;
|
||||
import org.photonvision.vision.opencv.ImageRotationMode;
|
||||
import org.photonvision.vision.pipe.CVPipe.CVPipeResult;
|
||||
import org.photonvision.vision.pipe.impl.GrayscalePipe;
|
||||
import org.photonvision.vision.pipe.impl.HSVPipe;
|
||||
import org.photonvision.vision.pipe.impl.RotateImagePipe;
|
||||
|
||||
public abstract class CpuImageProcessor implements FrameProvider {
|
||||
protected class CapturedFrame {
|
||||
CVMat colorImage;
|
||||
FrameStaticProperties staticProps;
|
||||
long captureTimestamp;
|
||||
|
||||
public CapturedFrame(
|
||||
CVMat colorImage, FrameStaticProperties staticProps, long captureTimestampNanos) {
|
||||
this.colorImage = colorImage;
|
||||
this.staticProps = staticProps;
|
||||
this.captureTimestamp = captureTimestampNanos;
|
||||
}
|
||||
}
|
||||
|
||||
private final HSVPipe m_hsvPipe = new HSVPipe();
|
||||
private final RotateImagePipe m_rImagePipe = new RotateImagePipe();
|
||||
private final GrayscalePipe m_grayPipe = new GrayscalePipe();
|
||||
FrameThresholdType m_processType;
|
||||
|
||||
private final Object m_mutex = new Object();
|
||||
|
||||
abstract CapturedFrame getInputMat();
|
||||
|
||||
public CpuImageProcessor() {
|
||||
m_hsvPipe.setParams(
|
||||
new HSVPipe.HSVParams(
|
||||
new IntegerCouple(0, 180),
|
||||
new IntegerCouple(0, 255),
|
||||
new IntegerCouple(0, 255),
|
||||
false));
|
||||
}
|
||||
|
||||
@Override
|
||||
public final Frame get() {
|
||||
// TODO Auto-generated method stub
|
||||
var input = getInputMat();
|
||||
|
||||
CVMat outputMat = null;
|
||||
long sumNanos = 0;
|
||||
|
||||
{
|
||||
CVPipeResult<Void> out = m_rImagePipe.run(input.colorImage.getMat());
|
||||
sumNanos += out.nanosElapsed;
|
||||
}
|
||||
|
||||
if (!input.colorImage.getMat().empty()) {
|
||||
if (m_processType == FrameThresholdType.HSV) {
|
||||
var hsvResult = m_hsvPipe.run(input.colorImage.getMat());
|
||||
outputMat = new CVMat(hsvResult.output);
|
||||
sumNanos += hsvResult.nanosElapsed;
|
||||
} else if (m_processType == FrameThresholdType.GREYSCALE) {
|
||||
var result = m_grayPipe.run(input.colorImage.getMat());
|
||||
outputMat = new CVMat(result.output);
|
||||
sumNanos += result.nanosElapsed;
|
||||
} else {
|
||||
outputMat = new CVMat();
|
||||
}
|
||||
} else {
|
||||
System.out.println("Input was empty!");
|
||||
outputMat = new CVMat();
|
||||
}
|
||||
|
||||
return new Frame(
|
||||
input.colorImage, outputMat, m_processType, input.captureTimestamp, input.staticProps);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void requestFrameThresholdType(FrameThresholdType type) {
|
||||
synchronized (m_mutex) {
|
||||
this.m_processType = type;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void requestFrameRotation(ImageRotationMode rotationMode) {
|
||||
synchronized (m_mutex) {
|
||||
m_rImagePipe.setParams(new RotateImagePipe.RotateImageParams(rotationMode));
|
||||
}
|
||||
}
|
||||
|
||||
/** Ask the camera to rotate frames it outputs */
|
||||
public void requestHsvSettings(HSVPipe.HSVParams params) {
|
||||
synchronized (m_mutex) {
|
||||
m_hsvPipe.setParams(params);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void requestFrameCopies(boolean copyInput, boolean copyOutput) {
|
||||
// We don't actually do zero-copy, so this method is a no-op
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -22,8 +22,8 @@ import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import org.opencv.core.Mat;
|
||||
import org.opencv.imgcodecs.Imgcodecs;
|
||||
import org.photonvision.common.util.math.MathUtils;
|
||||
import org.photonvision.vision.calibration.CameraCalibrationCoefficients;
|
||||
import org.photonvision.vision.frame.Frame;
|
||||
import org.photonvision.vision.frame.FrameProvider;
|
||||
import org.photonvision.vision.frame.FrameStaticProperties;
|
||||
import org.photonvision.vision.opencv.CVMat;
|
||||
@@ -32,14 +32,14 @@ import org.photonvision.vision.opencv.CVMat;
|
||||
* A {@link FrameProvider} that will read and provide an image from a {@link java.nio.file.Path
|
||||
* path}.
|
||||
*/
|
||||
public class FileFrameProvider implements FrameProvider {
|
||||
public class FileFrameProvider extends CpuImageProcessor {
|
||||
public static final int MAX_FPS = 5;
|
||||
private static int count = 0;
|
||||
|
||||
private final int thisIndex = count++;
|
||||
private final Path path;
|
||||
private final int millisDelay;
|
||||
private final Frame originalFrame;
|
||||
private final CVMat originalFrame;
|
||||
|
||||
private final FrameStaticProperties properties;
|
||||
|
||||
@@ -70,7 +70,7 @@ public class FileFrameProvider implements FrameProvider {
|
||||
Mat rawImage = Imgcodecs.imread(path.toString());
|
||||
if (rawImage.cols() > 0 && rawImage.rows() > 0) {
|
||||
properties = new FrameStaticProperties(rawImage.width(), rawImage.height(), fov, calibration);
|
||||
originalFrame = new Frame(new CVMat(rawImage), properties);
|
||||
originalFrame = new CVMat(rawImage);
|
||||
} else {
|
||||
throw new RuntimeException("Image loading failed!");
|
||||
}
|
||||
@@ -97,9 +97,9 @@ public class FileFrameProvider implements FrameProvider {
|
||||
}
|
||||
|
||||
@Override
|
||||
public Frame get() {
|
||||
Frame outputFrame = new Frame(new CVMat(), properties);
|
||||
originalFrame.copyTo(outputFrame);
|
||||
public CapturedFrame getInputMat() {
|
||||
var out = new CVMat();
|
||||
out.copyTo(originalFrame);
|
||||
|
||||
// block to keep FPS at a defined rate
|
||||
if (System.currentTimeMillis() - lastGetMillis < millisDelay) {
|
||||
@@ -111,7 +111,7 @@ public class FileFrameProvider implements FrameProvider {
|
||||
}
|
||||
|
||||
lastGetMillis = System.currentTimeMillis();
|
||||
return outputFrame;
|
||||
return new CapturedFrame(out, properties, MathUtils.wpiNanoTime());
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
/*
|
||||
* Copyright (C) Photon Vision.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.photonvision.vision.frame.provider;
|
||||
|
||||
import org.opencv.core.Mat;
|
||||
import org.photonvision.common.util.math.MathUtils;
|
||||
import org.photonvision.raspi.LibCameraJNI;
|
||||
import org.photonvision.vision.camera.LibcameraGpuSettables;
|
||||
import org.photonvision.vision.frame.Frame;
|
||||
import org.photonvision.vision.frame.FrameProvider;
|
||||
import org.photonvision.vision.frame.FrameThresholdType;
|
||||
import org.photonvision.vision.opencv.CVMat;
|
||||
import org.photonvision.vision.opencv.ImageRotationMode;
|
||||
import org.photonvision.vision.pipe.impl.HSVPipe.HSVParams;
|
||||
|
||||
public class LibcameraGpuFrameProvider implements FrameProvider {
|
||||
private final LibcameraGpuSettables settables;
|
||||
|
||||
public LibcameraGpuFrameProvider(LibcameraGpuSettables visionSettables) {
|
||||
this.settables = visionSettables;
|
||||
|
||||
var vidMode = settables.getCurrentVideoMode();
|
||||
settables.setVideoMode(vidMode);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "AcceleratedPicamFrameProvider";
|
||||
}
|
||||
|
||||
int i = 0;
|
||||
|
||||
@Override
|
||||
public Frame get() {
|
||||
// We need to make sure that other threads don't try to change video modes while we're waiting
|
||||
// for a frame
|
||||
// System.out.println("GET!");
|
||||
synchronized (LibCameraJNI.CAMERA_LOCK) {
|
||||
var success = LibCameraJNI.awaitNewFrame();
|
||||
|
||||
if (!success) {
|
||||
System.out.println("No new frame");
|
||||
return new Frame();
|
||||
}
|
||||
|
||||
var colorMat = new CVMat(new Mat(LibCameraJNI.takeColorFrame()));
|
||||
var processedMat = new CVMat(new Mat(LibCameraJNI.takeProcessedFrame()));
|
||||
|
||||
// System.out.println("Color mat: " + colorMat.getMat().size());
|
||||
|
||||
// Imgcodecs.imwrite("color" + i + ".jpg", colorMat.getMat());
|
||||
// Imgcodecs.imwrite("processed" + (i) + ".jpg", processedMat.getMat());
|
||||
|
||||
int itype = LibCameraJNI.getGpuProcessType();
|
||||
FrameThresholdType type = FrameThresholdType.NONE;
|
||||
if (itype < FrameThresholdType.values().length && itype >= 0) {
|
||||
type = FrameThresholdType.values()[itype];
|
||||
}
|
||||
|
||||
var now = LibCameraJNI.getLibcameraTimestamp();
|
||||
var capture = LibCameraJNI.getFrameCaptureTime();
|
||||
var latency = (now - capture);
|
||||
|
||||
return new Frame(
|
||||
colorMat,
|
||||
processedMat,
|
||||
type,
|
||||
MathUtils.wpiNanoTime() - latency,
|
||||
settables.getFrameStaticProperties());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void requestFrameThresholdType(FrameThresholdType type) {
|
||||
LibCameraJNI.setGpuProcessType(type.ordinal());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void requestFrameRotation(ImageRotationMode rotationMode) {
|
||||
this.settables.setRotation(rotationMode);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void requestHsvSettings(HSVParams params) {
|
||||
LibCameraJNI.setThresholds(
|
||||
params.getHsvLower().val[0] / 180.0,
|
||||
params.getHsvLower().val[1] / 255.0,
|
||||
params.getHsvLower().val[2] / 255.0,
|
||||
params.getHsvUpper().val[0] / 180.0,
|
||||
params.getHsvUpper().val[1] / 255.0,
|
||||
params.getHsvUpper().val[2] / 255.0,
|
||||
params.getHueInverted());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void requestFrameCopies(boolean copyInput, boolean copyOutput) {
|
||||
LibCameraJNI.setFramesToCopy(copyInput, copyOutput);
|
||||
}
|
||||
}
|
||||
@@ -19,12 +19,10 @@ package org.photonvision.vision.frame.provider;
|
||||
|
||||
import edu.wpi.first.cscore.CvSink;
|
||||
import org.photonvision.common.util.math.MathUtils;
|
||||
import org.photonvision.vision.frame.Frame;
|
||||
import org.photonvision.vision.frame.FrameProvider;
|
||||
import org.photonvision.vision.opencv.CVMat;
|
||||
import org.photonvision.vision.processes.VisionSourceSettables;
|
||||
|
||||
public class USBFrameProvider implements FrameProvider {
|
||||
public class USBFrameProvider extends CpuImageProcessor {
|
||||
private final CvSink cvSink;
|
||||
|
||||
@SuppressWarnings("SpellCheckingInspection")
|
||||
@@ -38,18 +36,19 @@ public class USBFrameProvider implements FrameProvider {
|
||||
}
|
||||
|
||||
@Override
|
||||
public Frame get() {
|
||||
public CapturedFrame getInputMat() {
|
||||
var mat = new CVMat(); // We do this so that we don't fill a Mat in use by another thread
|
||||
// This is from wpi::Now, or WPIUtilJNI.now()
|
||||
long time =
|
||||
cvSink.grabFrame(
|
||||
mat.getMat()); // Units are microseconds, epoch is the same as the Unix epoch
|
||||
cvSink.grabFrame(mat.getMat())
|
||||
* 1000; // Units are microseconds, epoch is the same as the Unix epoch
|
||||
|
||||
// Sometimes CSCore gives us a zero frametime.
|
||||
if (time <= 1e-6) {
|
||||
time = MathUtils.wpiNanoTime();
|
||||
}
|
||||
return new Frame(mat, MathUtils.microsToNanos(time), settables.getFrameStaticProperties());
|
||||
|
||||
return new CapturedFrame(mat, settables.getFrameStaticProperties(), time);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -17,33 +17,50 @@
|
||||
|
||||
package org.photonvision.vision.pipe.impl;
|
||||
|
||||
import edu.wpi.first.apriltag.AprilTagDetection;
|
||||
import edu.wpi.first.apriltag.AprilTagDetector;
|
||||
import java.util.List;
|
||||
import org.opencv.core.Mat;
|
||||
import org.photonvision.vision.apriltag.AprilTagDetector;
|
||||
import org.photonvision.vision.apriltag.DetectionResult;
|
||||
import org.photonvision.vision.opencv.CVMat;
|
||||
import org.photonvision.vision.pipe.CVPipe;
|
||||
|
||||
public class AprilTagDetectionPipe
|
||||
extends CVPipe<Mat, List<DetectionResult>, AprilTagDetectionPipeParams> {
|
||||
extends CVPipe<CVMat, List<AprilTagDetection>, AprilTagDetectionPipeParams> {
|
||||
private final AprilTagDetector m_detector = new AprilTagDetector();
|
||||
|
||||
boolean useNativePoseEst;
|
||||
|
||||
@Override
|
||||
protected List<DetectionResult> process(Mat in) {
|
||||
return List.of(
|
||||
m_detector.detect(
|
||||
in,
|
||||
params.cameraCalibrationCoefficients,
|
||||
useNativePoseEst,
|
||||
params.numIterations,
|
||||
params.tagWidthMeters));
|
||||
public AprilTagDetectionPipe() {
|
||||
super();
|
||||
|
||||
m_detector.addFamily("tag16h5");
|
||||
m_detector.addFamily("tag36h11");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setParams(AprilTagDetectionPipeParams params) {
|
||||
super.setParams(params);
|
||||
m_detector.updateParams(params.detectorParams);
|
||||
protected List<AprilTagDetection> process(CVMat in) {
|
||||
if (in.getMat().empty()) {
|
||||
return List.of();
|
||||
}
|
||||
|
||||
var ret = m_detector.detect(in.getMat());
|
||||
|
||||
if (ret == null) {
|
||||
return List.of();
|
||||
}
|
||||
|
||||
return List.of(ret);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setParams(AprilTagDetectionPipeParams newParams) {
|
||||
if (this.params == null || !this.params.equals(newParams)) {
|
||||
m_detector.setConfig(newParams.detectorParams);
|
||||
|
||||
m_detector.clearFamilies();
|
||||
m_detector.addFamily(newParams.family.getNativeName());
|
||||
}
|
||||
|
||||
super.setParams(newParams);
|
||||
}
|
||||
|
||||
public void setNativePoseEstimationEnabled(boolean enabled) {
|
||||
|
||||
@@ -17,61 +17,37 @@
|
||||
|
||||
package org.photonvision.vision.pipe.impl;
|
||||
|
||||
import java.util.Objects;
|
||||
import org.photonvision.vision.apriltag.AprilTagDetectorParams;
|
||||
import edu.wpi.first.apriltag.AprilTagDetector;
|
||||
import org.photonvision.vision.apriltag.AprilTagFamily;
|
||||
import org.photonvision.vision.calibration.CameraCalibrationCoefficients;
|
||||
|
||||
public class AprilTagDetectionPipeParams {
|
||||
public final AprilTagDetectorParams detectorParams;
|
||||
public final CameraCalibrationCoefficients cameraCalibrationCoefficients;
|
||||
public final int numIterations;
|
||||
public final double tagWidthMeters;
|
||||
public final AprilTagFamily family;
|
||||
public final AprilTagDetector.Config detectorParams;
|
||||
|
||||
public AprilTagDetectionPipeParams(
|
||||
AprilTagFamily tagFamily,
|
||||
double decimate,
|
||||
double blur,
|
||||
int threads,
|
||||
boolean debug,
|
||||
boolean refineEdges,
|
||||
int numIters,
|
||||
double tagWidthMeters,
|
||||
CameraCalibrationCoefficients cameraCalibrationCoefficients) {
|
||||
detectorParams =
|
||||
new AprilTagDetectorParams(tagFamily, decimate, blur, threads, debug, refineEdges);
|
||||
this.cameraCalibrationCoefficients = cameraCalibrationCoefficients;
|
||||
this.numIterations = numIters;
|
||||
this.tagWidthMeters = tagWidthMeters;
|
||||
}
|
||||
|
||||
public AprilTagDetectionPipeParams(
|
||||
AprilTagDetectorParams detectorParams,
|
||||
CameraCalibrationCoefficients cameraCalibrationCoefficients,
|
||||
int numIters,
|
||||
double tagWidthMeters) {
|
||||
this.detectorParams = detectorParams;
|
||||
this.cameraCalibrationCoefficients = cameraCalibrationCoefficients;
|
||||
this.numIterations = numIters;
|
||||
this.tagWidthMeters = tagWidthMeters;
|
||||
public AprilTagDetectionPipeParams(AprilTagFamily tagFamily, AprilTagDetector.Config config) {
|
||||
this.family = tagFamily;
|
||||
this.detectorParams = config;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
AprilTagDetectionPipeParams that = (AprilTagDetectionPipeParams) o;
|
||||
return Objects.equals(detectorParams, that.detectorParams)
|
||||
&& Objects.equals(cameraCalibrationCoefficients, that.cameraCalibrationCoefficients);
|
||||
public int hashCode() {
|
||||
final int prime = 31;
|
||||
int result = 1;
|
||||
result = prime * result + ((family == null) ? 0 : family.hashCode());
|
||||
result = prime * result + ((detectorParams == null) ? 0 : detectorParams.hashCode());
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "AprilTagDetectionPipeParams{"
|
||||
+ "detectorParams="
|
||||
+ detectorParams
|
||||
+ ", cameraCalibrationCoefficients="
|
||||
+ cameraCalibrationCoefficients
|
||||
+ '}';
|
||||
public boolean equals(Object obj) {
|
||||
if (this == obj) return true;
|
||||
if (obj == null) return false;
|
||||
if (getClass() != obj.getClass()) return false;
|
||||
AprilTagDetectionPipeParams other = (AprilTagDetectionPipeParams) obj;
|
||||
if (family != other.family) return false;
|
||||
if (detectorParams == null) {
|
||||
if (other.detectorParams != null) return false;
|
||||
} else if (!detectorParams.equals(other.detectorParams)) return false;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
/*
|
||||
* Copyright (C) Photon Vision.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.photonvision.vision.pipe.impl;
|
||||
|
||||
import edu.wpi.first.apriltag.AprilTagDetection;
|
||||
import edu.wpi.first.apriltag.AprilTagPoseEstimate;
|
||||
import edu.wpi.first.apriltag.AprilTagPoseEstimator;
|
||||
import edu.wpi.first.apriltag.AprilTagPoseEstimator.Config;
|
||||
import org.photonvision.vision.pipe.CVPipe;
|
||||
|
||||
public class AprilTagPoseEstimatorPipe
|
||||
extends CVPipe<
|
||||
AprilTagDetection,
|
||||
AprilTagPoseEstimate,
|
||||
AprilTagPoseEstimatorPipe.AprilTagPoseEstimatorPipeParams> {
|
||||
private final AprilTagPoseEstimator m_poseEstimator =
|
||||
new AprilTagPoseEstimator(new AprilTagPoseEstimator.Config(0, 0, 0, 0, 0));
|
||||
|
||||
boolean useNativePoseEst;
|
||||
|
||||
public AprilTagPoseEstimatorPipe() {
|
||||
super();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected AprilTagPoseEstimate process(AprilTagDetection in) {
|
||||
return m_poseEstimator.estimateOrthogonalIteration(in, params.nIters);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setParams(AprilTagPoseEstimatorPipe.AprilTagPoseEstimatorPipeParams newParams) {
|
||||
if (this.params == null || !this.params.equals(newParams)) {
|
||||
m_poseEstimator.setConfig(newParams.config);
|
||||
}
|
||||
|
||||
super.setParams(newParams);
|
||||
}
|
||||
|
||||
public void setNativePoseEstimationEnabled(boolean enabled) {
|
||||
this.useNativePoseEst = enabled;
|
||||
}
|
||||
|
||||
public static class AprilTagPoseEstimatorPipeParams {
|
||||
final AprilTagPoseEstimator.Config config;
|
||||
final int nIters;
|
||||
|
||||
public AprilTagPoseEstimatorPipeParams(Config config, int nIters) {
|
||||
this.config = config;
|
||||
this.nIters = nIters;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
final int prime = 31;
|
||||
int result = 1;
|
||||
result = prime * result + ((config == null) ? 0 : config.hashCode());
|
||||
result = prime * result + nIters;
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (this == obj) return true;
|
||||
if (obj == null) return false;
|
||||
if (getClass() != obj.getClass()) return false;
|
||||
AprilTagPoseEstimatorPipeParams other = (AprilTagPoseEstimatorPipeParams) obj;
|
||||
if (config == null) {
|
||||
if (other.config != null) return false;
|
||||
} else if (!config.equals(other.config)) return false;
|
||||
if (nIters != other.nIters) return false;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -27,6 +27,7 @@ import org.photonvision.common.util.ColorHelper;
|
||||
import org.photonvision.vision.frame.FrameDivisor;
|
||||
import org.photonvision.vision.frame.FrameStaticProperties;
|
||||
import org.photonvision.vision.opencv.DualOffsetValues;
|
||||
import org.photonvision.vision.opencv.ImageRotationMode;
|
||||
import org.photonvision.vision.pipe.MutatingPipe;
|
||||
import org.photonvision.vision.target.RobotOffsetPointMode;
|
||||
import org.photonvision.vision.target.TargetCalculations;
|
||||
@@ -46,6 +47,13 @@ public class Draw2dCrosshairPipe
|
||||
double y = params.frameStaticProperties.centerY;
|
||||
double scale = params.frameStaticProperties.imageWidth / (double) params.divisor.value / 32.0;
|
||||
|
||||
if (this.params.rotMode == ImageRotationMode.DEG_270
|
||||
|| this.params.rotMode == ImageRotationMode.DEG_90) {
|
||||
var tmp = x;
|
||||
x = y;
|
||||
y = tmp;
|
||||
}
|
||||
|
||||
switch (params.robotOffsetPointMode) {
|
||||
case Single:
|
||||
if (params.singleOffsetPoint.x != 0 && params.singleOffsetPoint.y != 0) {
|
||||
@@ -87,15 +95,19 @@ public class Draw2dCrosshairPipe
|
||||
|
||||
public final boolean shouldDraw;
|
||||
public final FrameStaticProperties frameStaticProperties;
|
||||
public final ImageRotationMode rotMode;
|
||||
public final RobotOffsetPointMode robotOffsetPointMode;
|
||||
public final Point singleOffsetPoint;
|
||||
public final DualOffsetValues dualOffsetValues;
|
||||
private final FrameDivisor divisor;
|
||||
|
||||
public Draw2dCrosshairParams(
|
||||
FrameStaticProperties frameStaticProperties, FrameDivisor divisor) {
|
||||
FrameStaticProperties frameStaticProperties,
|
||||
FrameDivisor divisor,
|
||||
ImageRotationMode rotMode) {
|
||||
shouldDraw = true;
|
||||
this.frameStaticProperties = frameStaticProperties;
|
||||
this.rotMode = rotMode;
|
||||
robotOffsetPointMode = RobotOffsetPointMode.None;
|
||||
singleOffsetPoint = new Point();
|
||||
dualOffsetValues = new DualOffsetValues();
|
||||
@@ -108,13 +120,15 @@ public class Draw2dCrosshairPipe
|
||||
Point singleOffsetPoint,
|
||||
DualOffsetValues dualOffsetValues,
|
||||
FrameStaticProperties frameStaticProperties,
|
||||
FrameDivisor divisor) {
|
||||
FrameDivisor divisor,
|
||||
ImageRotationMode rotMode) {
|
||||
this.shouldDraw = shouldDraw;
|
||||
this.frameStaticProperties = frameStaticProperties;
|
||||
this.robotOffsetPointMode = robotOffsetPointMode;
|
||||
this.singleOffsetPoint = singleOffsetPoint;
|
||||
this.dualOffsetValues = dualOffsetValues;
|
||||
this.divisor = divisor;
|
||||
this.rotMode = rotMode;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,7 +47,7 @@ public class Draw3dTargetsPipe
|
||||
if (!params.shouldDraw) return null;
|
||||
if (params.cameraCalibrationCoefficients == null
|
||||
|| params.cameraCalibrationCoefficients.getCameraIntrinsicsMat() == null
|
||||
|| params.cameraCalibrationCoefficients.getCameraExtrinsicsMat() == null) {
|
||||
|| params.cameraCalibrationCoefficients.getDistCoeffsMat() == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -90,7 +90,7 @@ public class Draw3dTargetsPipe
|
||||
target.getCameraRelativeRvec(),
|
||||
target.getCameraRelativeTvec(),
|
||||
params.cameraCalibrationCoefficients.getCameraIntrinsicsMat(),
|
||||
params.cameraCalibrationCoefficients.getCameraExtrinsicsMat(),
|
||||
params.cameraCalibrationCoefficients.getDistCoeffsMat(),
|
||||
tempMat,
|
||||
jac);
|
||||
// Distort the points so they match the image they're being overlaid on
|
||||
@@ -101,7 +101,7 @@ public class Draw3dTargetsPipe
|
||||
target.getCameraRelativeRvec(),
|
||||
target.getCameraRelativeTvec(),
|
||||
params.cameraCalibrationCoefficients.getCameraIntrinsicsMat(),
|
||||
params.cameraCalibrationCoefficients.getCameraExtrinsicsMat(),
|
||||
params.cameraCalibrationCoefficients.getDistCoeffsMat(),
|
||||
tempMat,
|
||||
jac);
|
||||
var topPoints = tempMat.toList();
|
||||
@@ -118,6 +118,54 @@ public class Draw3dTargetsPipe
|
||||
ColorHelper.colorToScalar(Color.green),
|
||||
3);
|
||||
}
|
||||
|
||||
// Draw X, Y and Z axis
|
||||
MatOfPoint3f pointMat = new MatOfPoint3f();
|
||||
// Those points are in opencv-land, but we are in NWU
|
||||
// NWU | EDN
|
||||
// X: Z
|
||||
// Y: -X
|
||||
// Z: -Y
|
||||
final double AXIS_LEN = 0.2;
|
||||
var list =
|
||||
List.of(
|
||||
new Point3(0, 0, 0),
|
||||
new Point3(0, 0, AXIS_LEN),
|
||||
new Point3(AXIS_LEN, 0, 0),
|
||||
new Point3(0, AXIS_LEN, 0));
|
||||
pointMat.fromList(list);
|
||||
|
||||
Calib3d.projectPoints(
|
||||
pointMat,
|
||||
target.getCameraRelativeRvec(),
|
||||
target.getCameraRelativeTvec(),
|
||||
params.cameraCalibrationCoefficients.getCameraIntrinsicsMat(),
|
||||
params.cameraCalibrationCoefficients.getDistCoeffsMat(),
|
||||
tempMat,
|
||||
jac);
|
||||
var axisPoints = tempMat.toList();
|
||||
dividePointList(axisPoints);
|
||||
|
||||
// Red = x, green y, blue z
|
||||
Imgproc.line(
|
||||
in.getLeft(),
|
||||
axisPoints.get(0),
|
||||
axisPoints.get(2),
|
||||
ColorHelper.colorToScalar(Color.GREEN),
|
||||
3);
|
||||
Imgproc.line(
|
||||
in.getLeft(),
|
||||
axisPoints.get(0),
|
||||
axisPoints.get(3),
|
||||
ColorHelper.colorToScalar(Color.BLUE),
|
||||
3);
|
||||
Imgproc.line(
|
||||
in.getLeft(),
|
||||
axisPoints.get(0),
|
||||
axisPoints.get(1),
|
||||
ColorHelper.colorToScalar(Color.RED),
|
||||
3);
|
||||
|
||||
for (int i = 0; i < bottomPoints.size(); i++) {
|
||||
Imgproc.line(
|
||||
in.getLeft(),
|
||||
@@ -135,47 +183,6 @@ public class Draw3dTargetsPipe
|
||||
3);
|
||||
}
|
||||
|
||||
// Draw X, Y and Z axis
|
||||
MatOfPoint3f pointMat = new MatOfPoint3f();
|
||||
var list =
|
||||
List.of(
|
||||
new Point3(0, 0, 0),
|
||||
new Point3(0.2, 0, 0),
|
||||
new Point3(0, 0.2, 0),
|
||||
new Point3(0, 0, 0.2));
|
||||
pointMat.fromList(list);
|
||||
|
||||
Calib3d.projectPoints(
|
||||
pointMat,
|
||||
target.getCameraRelativeRvec(),
|
||||
target.getCameraRelativeTvec(),
|
||||
params.cameraCalibrationCoefficients.getCameraIntrinsicsMat(),
|
||||
params.cameraCalibrationCoefficients.getCameraExtrinsicsMat(),
|
||||
tempMat,
|
||||
jac);
|
||||
var axisPoints = tempMat.toList();
|
||||
dividePointList(axisPoints);
|
||||
|
||||
// Red = x, green y, blue z
|
||||
Imgproc.line(
|
||||
in.getLeft(),
|
||||
axisPoints.get(0),
|
||||
axisPoints.get(1),
|
||||
ColorHelper.colorToScalar(Color.RED),
|
||||
3);
|
||||
Imgproc.line(
|
||||
in.getLeft(),
|
||||
axisPoints.get(0),
|
||||
axisPoints.get(2),
|
||||
ColorHelper.colorToScalar(Color.GREEN),
|
||||
3);
|
||||
Imgproc.line(
|
||||
in.getLeft(),
|
||||
axisPoints.get(0),
|
||||
axisPoints.get(3),
|
||||
ColorHelper.colorToScalar(Color.BLUE),
|
||||
3);
|
||||
|
||||
tempMat.release();
|
||||
jac.release();
|
||||
pointMat.release();
|
||||
@@ -206,7 +213,7 @@ public class Draw3dTargetsPipe
|
||||
var dstList = new ArrayList<Point>();
|
||||
final Mat cameraMatrix = params.cameraCalibrationCoefficients.getCameraIntrinsicsMat();
|
||||
// k1, k2, p1, p2, k3
|
||||
final Mat distCoeffs = params.cameraCalibrationCoefficients.getCameraExtrinsicsMat();
|
||||
final Mat distCoeffs = params.cameraCalibrationCoefficients.getDistCoeffsMat();
|
||||
var cx = cameraMatrix.get(0, 2)[0];
|
||||
var cy = cameraMatrix.get(1, 2)[0];
|
||||
var fx = cameraMatrix.get(0, 0)[0];
|
||||
|
||||
@@ -107,8 +107,8 @@ public class FilterContoursPipe
|
||||
|
||||
// Fullness Filtering.
|
||||
double contourArea = contour.getArea();
|
||||
double minFullness = params.getFullness().getFirst() * minAreaRect.size.area() / 100;
|
||||
double maxFullness = params.getFullness().getSecond() * minAreaRect.size.area() / 100;
|
||||
double minFullness = params.getFullness().getFirst() * minAreaRect.size.area() / 100.0;
|
||||
double maxFullness = params.getFullness().getSecond() * minAreaRect.size.area() / 100.0;
|
||||
if (contourArea <= minFullness || contourArea >= maxFullness) return;
|
||||
|
||||
// Aspect Ratio Filtering.
|
||||
|
||||
@@ -48,7 +48,7 @@ public class SolvePNPPipe
|
||||
protected List<TrackedTarget> process(List<TrackedTarget> targetList) {
|
||||
if (params.cameraCoefficients == null
|
||||
|| params.cameraCoefficients.getCameraIntrinsicsMat() == null
|
||||
|| params.cameraCoefficients.getCameraExtrinsicsMat() == null) {
|
||||
|| params.cameraCoefficients.getDistCoeffsMat() == null) {
|
||||
if (!hasWarned) {
|
||||
logger.warn(
|
||||
"Cannot perform solvePNP an uncalibrated camera! Please calibrate this resolution...");
|
||||
@@ -69,7 +69,7 @@ public class SolvePNPPipe
|
||||
|| corners.isEmpty()
|
||||
|| params.cameraCoefficients == null
|
||||
|| params.cameraCoefficients.getCameraIntrinsicsMat() == null
|
||||
|| params.cameraCoefficients.getCameraExtrinsicsMat() == null) {
|
||||
|| params.cameraCoefficients.getDistCoeffsMat() == null) {
|
||||
return;
|
||||
}
|
||||
this.imagePoints.fromList(corners);
|
||||
@@ -81,7 +81,7 @@ public class SolvePNPPipe
|
||||
params.targetModel.getRealWorldTargetCoordinates(),
|
||||
imagePoints,
|
||||
params.cameraCoefficients.getCameraIntrinsicsMat(),
|
||||
params.cameraCoefficients.getCameraExtrinsicsMat(),
|
||||
params.cameraCoefficients.getDistCoeffsMat(),
|
||||
rVec,
|
||||
tVec);
|
||||
} catch (Exception e) {
|
||||
@@ -100,8 +100,9 @@ public class SolvePNPPipe
|
||||
Core.norm(rVec));
|
||||
|
||||
Pose3d targetPose = MathUtils.convertOpenCVtoPhotonPose(new Transform3d(translation, rotation));
|
||||
target.setCameraToTarget3d(
|
||||
target.setBestCameraToTarget3d(
|
||||
new Transform3d(targetPose.getTranslation(), targetPose.getRotation()));
|
||||
target.setAltCameraToTarget3d(new Transform3d());
|
||||
}
|
||||
|
||||
Mat rotationMatrix = new Mat();
|
||||
|
||||
@@ -17,36 +17,39 @@
|
||||
|
||||
package org.photonvision.vision.pipeline;
|
||||
|
||||
import edu.wpi.first.apriltag.AprilTagDetection;
|
||||
import edu.wpi.first.apriltag.AprilTagDetector;
|
||||
import edu.wpi.first.apriltag.AprilTagPoseEstimate;
|
||||
import edu.wpi.first.apriltag.AprilTagPoseEstimator.Config;
|
||||
import edu.wpi.first.math.geometry.Transform3d;
|
||||
import edu.wpi.first.math.util.Units;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import org.opencv.core.Mat;
|
||||
import org.photonvision.common.util.math.MathUtils;
|
||||
import org.photonvision.raspi.PicamJNI;
|
||||
import org.photonvision.vision.apriltag.AprilTagDetectorParams;
|
||||
import org.photonvision.vision.apriltag.DetectionResult;
|
||||
import org.photonvision.vision.camera.CameraQuirk;
|
||||
import org.photonvision.vision.frame.Frame;
|
||||
import org.photonvision.vision.opencv.CVMat;
|
||||
import org.photonvision.vision.frame.FrameThresholdType;
|
||||
import org.photonvision.vision.pipe.CVPipe.CVPipeResult;
|
||||
import org.photonvision.vision.pipe.impl.*;
|
||||
import org.photonvision.vision.pipe.impl.AprilTagPoseEstimatorPipe.AprilTagPoseEstimatorPipeParams;
|
||||
import org.photonvision.vision.pipeline.result.CVPipelineResult;
|
||||
import org.photonvision.vision.target.TrackedTarget;
|
||||
import org.photonvision.vision.target.TrackedTarget.TargetCalculationParameters;
|
||||
|
||||
@SuppressWarnings("DuplicatedCode")
|
||||
public class AprilTagPipeline extends CVPipeline<CVPipelineResult, AprilTagPipelineSettings> {
|
||||
private final RotateImagePipe rotateImagePipe = new RotateImagePipe();
|
||||
private final GrayscalePipe grayscalePipe = new GrayscalePipe();
|
||||
private final AprilTagDetectionPipe aprilTagDetectionPipe = new AprilTagDetectionPipe();
|
||||
private final AprilTagPoseEstimatorPipe poseEstimatorPipe = new AprilTagPoseEstimatorPipe();
|
||||
private final CalculateFPSPipe calculateFPSPipe = new CalculateFPSPipe();
|
||||
|
||||
private static final FrameThresholdType PROCESSING_TYPE = FrameThresholdType.GREYSCALE;
|
||||
|
||||
public AprilTagPipeline() {
|
||||
super(PROCESSING_TYPE);
|
||||
settings = new AprilTagPipelineSettings();
|
||||
}
|
||||
|
||||
public AprilTagPipeline(AprilTagPipelineSettings settings) {
|
||||
super(PROCESSING_TYPE);
|
||||
this.settings = settings;
|
||||
}
|
||||
|
||||
@@ -55,97 +58,118 @@ public class AprilTagPipeline extends CVPipeline<CVPipelineResult, AprilTagPipel
|
||||
// Sanitize thread count - not supported to have fewer than 1 threads
|
||||
settings.threads = Math.max(1, settings.threads);
|
||||
|
||||
RotateImagePipe.RotateImageParams rotateImageParams =
|
||||
new RotateImagePipe.RotateImageParams(settings.inputImageRotationMode);
|
||||
rotateImagePipe.setParams(rotateImageParams);
|
||||
|
||||
if (cameraQuirks.hasQuirk(CameraQuirk.PiCam) && PicamJNI.isSupported()) {
|
||||
// TODO: Picam grayscale
|
||||
PicamJNI.setRotation(settings.inputImageRotationMode.value);
|
||||
PicamJNI.setShouldCopyColor(true); // need the color image to grayscale
|
||||
}
|
||||
|
||||
AprilTagDetectorParams aprilTagDetectionParams =
|
||||
new AprilTagDetectorParams(
|
||||
settings.tagFamily,
|
||||
settings.decimate,
|
||||
settings.blur,
|
||||
settings.threads,
|
||||
settings.debug,
|
||||
settings.refineEdges);
|
||||
// if (cameraQuirks.hasQuirk(CameraQuirk.PiCam) && LibCameraJNI.isSupported()) {
|
||||
// // TODO: Picam grayscale
|
||||
// LibCameraJNI.setRotation(settings.inputImageRotationMode.value);
|
||||
// // LibCameraJNI.setShouldCopyColor(true); // need the color image to grayscale
|
||||
// }
|
||||
|
||||
// TODO (HACK): tag width is Fun because it really belongs in the "target model"
|
||||
// We need the tag width for the JNI to figure out target pose, but we need a
|
||||
// target model for the draw 3d targets pipeline to work...
|
||||
|
||||
// for now, hard code tag width based on enum value
|
||||
double tagWidth = 0.16; // guess at 200mm??
|
||||
double tagWidth;
|
||||
|
||||
// This needs
|
||||
switch (settings.targetModel) {
|
||||
case k200mmAprilTag:
|
||||
{
|
||||
tagWidth = Units.inchesToMeters(3.25 * 2);
|
||||
break;
|
||||
}
|
||||
case k6in_16h5:
|
||||
{
|
||||
tagWidth = Units.inchesToMeters(3 * 2);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
{
|
||||
// guess at 200mm?? If it's zero everything breaks, but it should _never_ be zero. Unless
|
||||
// users select the wrong model...
|
||||
tagWidth = 0.16;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
aprilTagDetectionPipe.setParams(
|
||||
new AprilTagDetectionPipeParams(
|
||||
aprilTagDetectionParams,
|
||||
frameStaticProperties.cameraCalibration,
|
||||
settings.numIterations,
|
||||
tagWidth));
|
||||
// AprilTagDetectorParams aprilTagDetectionParams =
|
||||
// new AprilTagDetectorParams(
|
||||
// settings.tagFamily,
|
||||
// settings.decimate,
|
||||
// settings.blur,
|
||||
// settings.threads,
|
||||
// settings.debug,
|
||||
// settings.refineEdges);
|
||||
|
||||
var config = new AprilTagDetector.Config();
|
||||
config.numThreads = settings.threads;
|
||||
config.refineEdges = settings.refineEdges;
|
||||
config.quadSigma = (float) settings.blur;
|
||||
config.quadDecimate = settings.decimate;
|
||||
aprilTagDetectionPipe.setParams(new AprilTagDetectionPipeParams(settings.tagFamily, config));
|
||||
|
||||
if (frameStaticProperties.cameraCalibration != null) {
|
||||
var cameraMatrix = frameStaticProperties.cameraCalibration.getCameraIntrinsicsMat();
|
||||
if (cameraMatrix != null) {
|
||||
var cx = cameraMatrix.get(0, 2)[0];
|
||||
var cy = cameraMatrix.get(1, 2)[0];
|
||||
var fx = cameraMatrix.get(0, 0)[0];
|
||||
var fy = cameraMatrix.get(1, 1)[0];
|
||||
|
||||
poseEstimatorPipe.setParams(
|
||||
new AprilTagPoseEstimatorPipeParams(
|
||||
new Config(tagWidth, fx, fy, cx, cy), settings.numIterations));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected CVPipelineResult process(Frame frame, AprilTagPipelineSettings settings) {
|
||||
long sumPipeNanosElapsed = 0L;
|
||||
|
||||
CVPipeResult<Mat> grayscalePipeResult;
|
||||
Mat rawInputMat;
|
||||
boolean inputSingleChannel = frame.image.getMat().channels() == 1;
|
||||
|
||||
if (inputSingleChannel) {
|
||||
rawInputMat = new Mat(PicamJNI.grabFrame(true));
|
||||
frame.image.getMat().release(); // release the 8bit frame ASAP.
|
||||
} else {
|
||||
rawInputMat = frame.image.getMat();
|
||||
var rotateImageResult = rotateImagePipe.run(rawInputMat);
|
||||
sumPipeNanosElapsed += rotateImageResult.nanosElapsed;
|
||||
}
|
||||
|
||||
var inputFrame = new Frame(new CVMat(rawInputMat), frameStaticProperties);
|
||||
|
||||
grayscalePipeResult = grayscalePipe.run(rawInputMat);
|
||||
sumPipeNanosElapsed += grayscalePipeResult.nanosElapsed;
|
||||
|
||||
var outputFrame = new Frame(new CVMat(grayscalePipeResult.output), frameStaticProperties);
|
||||
|
||||
List<TrackedTarget> targetList;
|
||||
CVPipeResult<List<DetectionResult>> tagDetectionPipeResult;
|
||||
|
||||
// Use the solvePNP Enabled flag to enable native pose estimation
|
||||
aprilTagDetectionPipe.setNativePoseEstimationEnabled(settings.solvePNPEnabled);
|
||||
|
||||
tagDetectionPipeResult = aprilTagDetectionPipe.run(grayscalePipeResult.output);
|
||||
if (frame.type != FrameThresholdType.GREYSCALE) {
|
||||
// TODO so all cameras should give us ADAPTIVE_THRESH -- how should we handle if not?
|
||||
return new CVPipelineResult(0, 0, List.of());
|
||||
}
|
||||
|
||||
CVPipeResult<List<AprilTagDetection>> tagDetectionPipeResult;
|
||||
tagDetectionPipeResult = aprilTagDetectionPipe.run(frame.processedImage);
|
||||
sumPipeNanosElapsed += tagDetectionPipeResult.nanosElapsed;
|
||||
|
||||
targetList = new ArrayList<>();
|
||||
for (DetectionResult detection : tagDetectionPipeResult.output) {
|
||||
for (AprilTagDetection detection : tagDetectionPipeResult.output) {
|
||||
// TODO this should be in a pipe, not in the top level here (Matt)
|
||||
if (detection.getDecisionMargin() < settings.decisionMargin) continue;
|
||||
if (detection.getHamming() > settings.hammingDist) continue;
|
||||
|
||||
AprilTagPoseEstimate tagPoseEstimate = null;
|
||||
if (settings.solvePNPEnabled) {
|
||||
var poseResult = poseEstimatorPipe.run(detection);
|
||||
sumPipeNanosElapsed += poseResult.nanosElapsed;
|
||||
tagPoseEstimate = poseResult.output;
|
||||
}
|
||||
|
||||
// populate the target list
|
||||
// Challenge here is that TrackedTarget functions with OpenCV Contour
|
||||
TrackedTarget target =
|
||||
new TrackedTarget(
|
||||
detection,
|
||||
tagPoseEstimate,
|
||||
new TargetCalculationParameters(
|
||||
false, null, null, null, null, frameStaticProperties));
|
||||
|
||||
var correctedPose = MathUtils.convertOpenCVtoPhotonPose(target.getCameraToTarget3d());
|
||||
target.setCameraToTarget3d(
|
||||
new Transform3d(correctedPose.getTranslation(), correctedPose.getRotation()));
|
||||
var correctedBestPose = MathUtils.convertOpenCVtoPhotonPose(target.getBestCameraToTarget3d());
|
||||
var correctedAltPose = MathUtils.convertOpenCVtoPhotonPose(target.getAltCameraToTarget3d());
|
||||
|
||||
target.setBestCameraToTarget3d(
|
||||
new Transform3d(correctedBestPose.getTranslation(), correctedBestPose.getRotation()));
|
||||
target.setAltCameraToTarget3d(
|
||||
new Transform3d(correctedAltPose.getTranslation(), correctedAltPose.getRotation()));
|
||||
|
||||
targetList.add(target);
|
||||
}
|
||||
@@ -153,6 +177,6 @@ public class AprilTagPipeline extends CVPipeline<CVPipelineResult, AprilTagPipel
|
||||
var fpsResult = calculateFPSPipe.run(null);
|
||||
var fps = fpsResult.output;
|
||||
|
||||
return new CVPipelineResult(sumPipeNanosElapsed, fps, targetList, outputFrame, inputFrame);
|
||||
return new CVPipelineResult(sumPipeNanosElapsed, fps, targetList, frame);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,13 +25,17 @@ import org.photonvision.vision.target.TargetModel;
|
||||
@JsonTypeName("AprilTagPipelineSettings")
|
||||
public class AprilTagPipelineSettings extends AdvancedPipelineSettings {
|
||||
public AprilTagFamily tagFamily = AprilTagFamily.kTag36h11;
|
||||
public double decimate = 1.0;
|
||||
public int decimate = 1;
|
||||
public double blur = 0;
|
||||
public int threads = 1;
|
||||
public boolean debug = false;
|
||||
public boolean refineEdges = true;
|
||||
public int numIterations = 200;
|
||||
|
||||
// TODO is this a legit, reasonable default?
|
||||
public int hammingDist = 1;
|
||||
public int decisionMargin = 30;
|
||||
|
||||
// 3d settings
|
||||
|
||||
public AprilTagPipelineSettings() {
|
||||
@@ -39,8 +43,6 @@ public class AprilTagPipelineSettings extends AdvancedPipelineSettings {
|
||||
pipelineType = PipelineType.AprilTag;
|
||||
outputShowMultipleTargets = true;
|
||||
targetModel = TargetModel.k200mmAprilTag;
|
||||
cameraExposure = -1;
|
||||
cameraAutoExposure = true;
|
||||
ledMode = false;
|
||||
}
|
||||
|
||||
|
||||
@@ -17,10 +17,10 @@
|
||||
|
||||
package org.photonvision.vision.pipeline;
|
||||
|
||||
import java.util.List;
|
||||
import org.photonvision.vision.camera.QuirkyCamera;
|
||||
import org.photonvision.vision.frame.Frame;
|
||||
import org.photonvision.vision.frame.FrameStaticProperties;
|
||||
import org.photonvision.vision.frame.FrameThresholdType;
|
||||
import org.photonvision.vision.pipeline.result.CVPipelineResult;
|
||||
|
||||
public abstract class CVPipeline<R extends CVPipelineResult, S extends CVPipelineSettings> {
|
||||
@@ -28,6 +28,16 @@ public abstract class CVPipeline<R extends CVPipelineResult, S extends CVPipelin
|
||||
protected FrameStaticProperties frameStaticProperties;
|
||||
protected QuirkyCamera cameraQuirks;
|
||||
|
||||
private final FrameThresholdType thresholdType;
|
||||
|
||||
public CVPipeline(FrameThresholdType thresholdType) {
|
||||
this.thresholdType = thresholdType;
|
||||
}
|
||||
|
||||
public FrameThresholdType getThresholdType() {
|
||||
return thresholdType;
|
||||
}
|
||||
|
||||
protected void setPipeParams(
|
||||
FrameStaticProperties frameStaticProperties, S settings, QuirkyCamera cameraQuirks) {
|
||||
this.settings = settings;
|
||||
@@ -55,10 +65,10 @@ public abstract class CVPipeline<R extends CVPipelineResult, S extends CVPipelin
|
||||
}
|
||||
setPipeParams(frame.frameStaticProperties, settings, cameraQuirks);
|
||||
|
||||
if (frame.image.getMat().empty()) {
|
||||
//noinspection unchecked
|
||||
return (R) new CVPipelineResult(0, 0, List.of(), frame);
|
||||
}
|
||||
// if (frame.image.getMat().empty()) {
|
||||
// //noinspection unchecked
|
||||
// return (R) new CVPipelineResult(0, 0, List.of(), frame);
|
||||
// }
|
||||
R result = process(frame, settings);
|
||||
|
||||
result.setImageCaptureTimestampNanos(frame.timestampNanos);
|
||||
|
||||
@@ -21,7 +21,6 @@ import com.fasterxml.jackson.annotation.JsonSubTypes;
|
||||
import com.fasterxml.jackson.annotation.JsonTypeInfo;
|
||||
import java.util.Objects;
|
||||
import org.photonvision.vision.frame.FrameDivisor;
|
||||
import org.photonvision.vision.opencv.ImageFlipMode;
|
||||
import org.photonvision.vision.opencv.ImageRotationMode;
|
||||
|
||||
@JsonTypeInfo(
|
||||
@@ -37,7 +36,6 @@ import org.photonvision.vision.opencv.ImageRotationMode;
|
||||
public class CVPipelineSettings implements Cloneable {
|
||||
public int pipelineIndex = 0;
|
||||
public PipelineType pipelineType = PipelineType.DriverMode;
|
||||
public ImageFlipMode inputImageFlipMode = ImageFlipMode.NONE;
|
||||
public ImageRotationMode inputImageRotationMode = ImageRotationMode.DEG_0;
|
||||
public String pipelineNickname = "New Pipeline";
|
||||
public boolean cameraAutoExposure = false;
|
||||
@@ -70,7 +68,6 @@ public class CVPipelineSettings implements Cloneable {
|
||||
&& cameraVideoModeIndex == that.cameraVideoModeIndex
|
||||
&& ledMode == that.ledMode
|
||||
&& pipelineType == that.pipelineType
|
||||
&& inputImageFlipMode == that.inputImageFlipMode
|
||||
&& inputImageRotationMode == that.inputImageRotationMode
|
||||
&& pipelineNickname.equals(that.pipelineNickname)
|
||||
&& streamingFrameDivisor == that.streamingFrameDivisor
|
||||
@@ -83,7 +80,6 @@ public class CVPipelineSettings implements Cloneable {
|
||||
return Objects.hash(
|
||||
pipelineIndex,
|
||||
pipelineType,
|
||||
inputImageFlipMode,
|
||||
inputImageRotationMode,
|
||||
pipelineNickname,
|
||||
cameraExposure,
|
||||
@@ -107,4 +103,39 @@ public class CVPipelineSettings implements Cloneable {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "CVPipelineSettings{"
|
||||
+ "pipelineIndex="
|
||||
+ pipelineIndex
|
||||
+ ", pipelineType="
|
||||
+ pipelineType
|
||||
+ ", inputImageRotationMode="
|
||||
+ inputImageRotationMode
|
||||
+ ", pipelineNickname='"
|
||||
+ pipelineNickname
|
||||
+ '\''
|
||||
+ ", cameraExposure="
|
||||
+ cameraExposure
|
||||
+ ", cameraBrightness="
|
||||
+ cameraBrightness
|
||||
+ ", cameraGain="
|
||||
+ cameraGain
|
||||
+ ", cameraRedGain="
|
||||
+ cameraRedGain
|
||||
+ ", cameraBlueGain="
|
||||
+ cameraBlueGain
|
||||
+ ", cameraVideoModeIndex="
|
||||
+ cameraVideoModeIndex
|
||||
+ ", streamingFrameDivisor="
|
||||
+ streamingFrameDivisor
|
||||
+ ", ledMode="
|
||||
+ ledMode
|
||||
+ ", inputShouldShow="
|
||||
+ inputShouldShow
|
||||
+ ", outputShouldShow="
|
||||
+ outputShouldShow
|
||||
+ '}';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,10 +34,9 @@ import org.photonvision.common.logging.LogGroup;
|
||||
import org.photonvision.common.logging.Logger;
|
||||
import org.photonvision.common.util.SerializationUtils;
|
||||
import org.photonvision.common.util.file.FileUtils;
|
||||
import org.photonvision.raspi.PicamJNI;
|
||||
import org.photonvision.vision.calibration.CameraCalibrationCoefficients;
|
||||
import org.photonvision.vision.camera.CameraQuirk;
|
||||
import org.photonvision.vision.frame.Frame;
|
||||
import org.photonvision.vision.frame.FrameThresholdType;
|
||||
import org.photonvision.vision.opencv.CVMat;
|
||||
import org.photonvision.vision.pipe.CVPipe.CVPipeResult;
|
||||
import org.photonvision.vision.pipe.impl.CalculateFPSPipe;
|
||||
@@ -71,11 +70,14 @@ public class Calibrate3dPipeline
|
||||
// Path to save images
|
||||
private final Path imageDir = ConfigManager.getInstance().getCalibDir();
|
||||
|
||||
private static final FrameThresholdType PROCESSING_TYPE = FrameThresholdType.NONE;
|
||||
|
||||
public Calibrate3dPipeline() {
|
||||
this(12);
|
||||
}
|
||||
|
||||
public Calibrate3dPipeline(int minSnapshots) {
|
||||
super(PROCESSING_TYPE);
|
||||
this.settings = new Calibration3dPipelineSettings();
|
||||
this.foundCornersList = new ArrayList<>();
|
||||
this.minSnapshots = minSnapshots;
|
||||
@@ -93,26 +95,18 @@ public class Calibrate3dPipeline
|
||||
new Size(frameStaticProperties.imageWidth, frameStaticProperties.imageHeight));
|
||||
calibrate3dPipe.setParams(calibratePipeParams);
|
||||
|
||||
if (cameraQuirks.hasQuirk(CameraQuirk.PiCam) && PicamJNI.isSupported()) {
|
||||
PicamJNI.setRotation(settings.inputImageRotationMode.value);
|
||||
PicamJNI.setShouldCopyColor(true);
|
||||
}
|
||||
// if (cameraQuirks.hasQuirk(CameraQuirk.PiCam) && LibCameraJNI.isSupported()) {
|
||||
// LibCameraJNI.setRotation(settings.inputImageRotationMode.value);
|
||||
// // LibCameraJNI.setShouldCopyColor(true);
|
||||
// }
|
||||
}
|
||||
|
||||
@Override
|
||||
protected CVPipelineResult process(Frame frame, Calibration3dPipelineSettings settings) {
|
||||
Mat inputColorMat = frame.image.getMat();
|
||||
if (inputColorMat.channels() == 1
|
||||
&& cameraQuirks.hasQuirk(CameraQuirk.PiCam)
|
||||
&& PicamJNI.isSupported()) {
|
||||
long colorMatPtr = PicamJNI.grabFrame(true);
|
||||
if (colorMatPtr == 0) throw new RuntimeException("Got null Mat from GPU Picam driver");
|
||||
inputColorMat = new Mat(colorMatPtr);
|
||||
}
|
||||
Mat inputColorMat = frame.colorImage.getMat();
|
||||
|
||||
if (this.calibrating) {
|
||||
return new CVPipelineResult(
|
||||
0, 0, null, new Frame(new CVMat(inputColorMat), frame.frameStaticProperties));
|
||||
return new CVPipelineResult(0, 0, null, frame);
|
||||
}
|
||||
|
||||
long sumPipeNanosElapsed = 0L;
|
||||
@@ -141,14 +135,15 @@ public class Calibrate3dPipeline
|
||||
}
|
||||
}
|
||||
|
||||
frame.image.release();
|
||||
frame.release();
|
||||
|
||||
// Return the drawn chessboard if corners are found, if not, then return the input image.
|
||||
return new CVPipelineResult(
|
||||
sumPipeNanosElapsed,
|
||||
fps, // Unused but here in case
|
||||
Collections.emptyList(),
|
||||
new Frame(outputColorCVMat, frame.frameStaticProperties));
|
||||
new Frame(
|
||||
new CVMat(), outputColorCVMat, FrameThresholdType.NONE, frame.frameStaticProperties));
|
||||
}
|
||||
|
||||
public void deleteSavedImages() {
|
||||
|
||||
@@ -27,4 +27,11 @@ public class Calibration3dPipelineSettings extends AdvancedPipelineSettings {
|
||||
public double gridSize = Units.inchesToMeters(1.0);
|
||||
|
||||
public Size resolution = new Size(640, 480);
|
||||
|
||||
public Calibration3dPipelineSettings() {
|
||||
super();
|
||||
|
||||
this.inputShouldShow = true;
|
||||
this.outputShouldShow = true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,12 +21,9 @@ import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
import org.apache.commons.lang3.tuple.Pair;
|
||||
import org.opencv.core.Mat;
|
||||
import org.opencv.core.Point;
|
||||
import org.photonvision.common.util.math.MathUtils;
|
||||
import org.photonvision.raspi.PicamJNI;
|
||||
import org.photonvision.vision.camera.CameraQuirk;
|
||||
import org.photonvision.vision.frame.Frame;
|
||||
import org.photonvision.vision.frame.FrameThresholdType;
|
||||
import org.photonvision.vision.opencv.*;
|
||||
import org.photonvision.vision.pipe.CVPipe.CVPipeResult;
|
||||
import org.photonvision.vision.pipe.impl.*;
|
||||
@@ -37,9 +34,7 @@ import org.photonvision.vision.target.TrackedTarget;
|
||||
@SuppressWarnings({"DuplicatedCode"})
|
||||
public class ColoredShapePipeline
|
||||
extends CVPipeline<CVPipelineResult, ColoredShapePipelineSettings> {
|
||||
private final RotateImagePipe rotateImagePipe = new RotateImagePipe();
|
||||
private final ErodeDilatePipe erodeDilatePipe = new ErodeDilatePipe();
|
||||
private final HSVPipe hsvPipe = new HSVPipe();
|
||||
private final SpeckleRejectPipe speckleRejectPipe = new SpeckleRejectPipe();
|
||||
private final FindContoursPipe findContoursPipe = new FindContoursPipe();
|
||||
private final FindPolygonPipe findPolygonPipe = new FindPolygonPipe();
|
||||
@@ -56,11 +51,15 @@ public class ColoredShapePipeline
|
||||
|
||||
private final Point[] rectPoints = new Point[4];
|
||||
|
||||
private static final FrameThresholdType PROCESSING_TYPE = FrameThresholdType.HSV;
|
||||
|
||||
public ColoredShapePipeline() {
|
||||
super(PROCESSING_TYPE);
|
||||
settings = new ColoredShapePipelineSettings();
|
||||
}
|
||||
|
||||
public ColoredShapePipeline(ColoredShapePipelineSettings settings) {
|
||||
super(PROCESSING_TYPE);
|
||||
this.settings = settings;
|
||||
}
|
||||
|
||||
@@ -73,29 +72,6 @@ public class ColoredShapePipeline
|
||||
settings.offsetDualPointB,
|
||||
settings.offsetDualPointBArea);
|
||||
|
||||
RotateImagePipe.RotateImageParams rotateImageParams =
|
||||
new RotateImagePipe.RotateImageParams(settings.inputImageRotationMode);
|
||||
rotateImagePipe.setParams(rotateImageParams);
|
||||
|
||||
if (cameraQuirks.hasQuirk(CameraQuirk.PiCam) && PicamJNI.isSupported()) {
|
||||
PicamJNI.setThresholds(
|
||||
settings.hsvHue.getFirst() / 180d,
|
||||
settings.hsvSaturation.getFirst() / 255d,
|
||||
settings.hsvValue.getFirst() / 255d,
|
||||
settings.hsvHue.getSecond() / 180d,
|
||||
settings.hsvSaturation.getSecond() / 255d,
|
||||
settings.hsvValue.getSecond() / 255d);
|
||||
PicamJNI.setInvertHue(settings.hueInverted);
|
||||
|
||||
PicamJNI.setRotation(settings.inputImageRotationMode.value);
|
||||
PicamJNI.setShouldCopyColor(settings.inputShouldShow);
|
||||
} else {
|
||||
var hsvParams =
|
||||
new HSVPipe.HSVParams(
|
||||
settings.hsvHue, settings.hsvSaturation, settings.hsvValue, settings.hueInverted);
|
||||
hsvPipe.setParams(hsvParams);
|
||||
}
|
||||
|
||||
ErodeDilatePipe.ErodeDilateParams erodeDilateParams =
|
||||
new ErodeDilatePipe.ErodeDilateParams(settings.erode, settings.dilate, 5);
|
||||
// TODO: add kernel size to pipeline settings
|
||||
@@ -182,7 +158,8 @@ public class ColoredShapePipeline
|
||||
settings.offsetSinglePoint,
|
||||
dualOffsetValues,
|
||||
frameStaticProperties,
|
||||
settings.streamingFrameDivisor);
|
||||
settings.streamingFrameDivisor,
|
||||
settings.inputImageRotationMode);
|
||||
draw2dCrosshairPipe.setParams(draw2dCrosshairParams);
|
||||
|
||||
var draw3dTargetsParams =
|
||||
@@ -198,45 +175,14 @@ public class ColoredShapePipeline
|
||||
protected CVPipelineResult process(Frame frame, ColoredShapePipelineSettings settings) {
|
||||
long sumPipeNanosElapsed = 0L;
|
||||
|
||||
CVPipeResult<Mat> hsvPipeResult;
|
||||
Mat rawInputMat;
|
||||
if (frame.image.getMat().channels() != 1) {
|
||||
var rotateImageResult = rotateImagePipe.run(frame.image.getMat());
|
||||
sumPipeNanosElapsed = rotateImageResult.nanosElapsed;
|
||||
|
||||
rawInputMat = frame.image.getMat();
|
||||
|
||||
hsvPipeResult = hsvPipe.run(rawInputMat);
|
||||
sumPipeNanosElapsed += hsvPipeResult.nanosElapsed;
|
||||
} else {
|
||||
// Try to copy the color frame.
|
||||
long inputMatPtr = PicamJNI.grabFrame(true);
|
||||
if (inputMatPtr != 0) {
|
||||
// If we grabbed it (in color copy mode), make a new Mat of it
|
||||
rawInputMat = new Mat(inputMatPtr);
|
||||
} else {
|
||||
// // Otherwise, use a blank/empty mat as placeholder
|
||||
// rawInputMat = new Mat();
|
||||
// Otherwise, the input mat is frame we got from the camera
|
||||
rawInputMat = frame.image.getMat();
|
||||
}
|
||||
|
||||
// We can skip a few steps if the image is single channel because we've already done them on
|
||||
// the GPU
|
||||
hsvPipeResult = new CVPipeResult<>();
|
||||
hsvPipeResult.output = frame.image.getMat();
|
||||
hsvPipeResult.nanosElapsed = MathUtils.wpiNanoTime() - frame.timestampNanos;
|
||||
|
||||
sumPipeNanosElapsed += hsvPipeResult.nanosElapsed;
|
||||
}
|
||||
|
||||
// var erodeDilateResult = erodeDilatePipe.run(rawInputMat);
|
||||
// sumPipeNanosElapsed += erodeDilateResult.nanosElapsed;
|
||||
//
|
||||
// CVPipeResult<Mat> hsvPipeResult = hsvPipe.run(rawInputMat);
|
||||
// sumPipeNanosElapsed += hsvPipeResult.nanosElapsed;
|
||||
|
||||
CVPipeResult<List<Contour>> findContoursResult = findContoursPipe.run(hsvPipeResult.output);
|
||||
CVPipeResult<List<Contour>> findContoursResult =
|
||||
findContoursPipe.run(frame.processedImage.getMat());
|
||||
sumPipeNanosElapsed += findContoursResult.nanosElapsed;
|
||||
|
||||
CVPipeResult<List<Contour>> speckleRejectResult =
|
||||
@@ -246,7 +192,7 @@ public class ColoredShapePipeline
|
||||
List<CVShape> shapes = null;
|
||||
if (settings.contourShape == ContourShape.Circle) {
|
||||
CVPipeResult<List<CVShape>> findCirclesResult =
|
||||
findCirclesPipe.run(Pair.of(hsvPipeResult.output, speckleRejectResult.output));
|
||||
findCirclesPipe.run(Pair.of(frame.processedImage.getMat(), speckleRejectResult.output));
|
||||
sumPipeNanosElapsed += findCirclesResult.nanosElapsed;
|
||||
shapes = findCirclesResult.output;
|
||||
} else {
|
||||
@@ -292,11 +238,6 @@ public class ColoredShapePipeline
|
||||
var fpsResult = calculateFPSPipe.run(null);
|
||||
var fps = fpsResult.output;
|
||||
|
||||
return new CVPipelineResult(
|
||||
sumPipeNanosElapsed,
|
||||
fps,
|
||||
targetList,
|
||||
new Frame(new CVMat(hsvPipeResult.output), frame.frameStaticProperties),
|
||||
new Frame(new CVMat(rawInputMat), frame.frameStaticProperties));
|
||||
return new CVPipelineResult(sumPipeNanosElapsed, fps, targetList, frame);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,12 +19,11 @@ package org.photonvision.vision.pipeline;
|
||||
|
||||
import java.util.List;
|
||||
import org.apache.commons.lang3.tuple.Pair;
|
||||
import org.opencv.core.Mat;
|
||||
import org.photonvision.common.util.math.MathUtils;
|
||||
import org.photonvision.raspi.PicamJNI;
|
||||
import org.photonvision.raspi.LibCameraJNI;
|
||||
import org.photonvision.vision.camera.CameraQuirk;
|
||||
import org.photonvision.vision.frame.Frame;
|
||||
import org.photonvision.vision.opencv.CVMat;
|
||||
import org.photonvision.vision.frame.FrameThresholdType;
|
||||
import org.photonvision.vision.pipe.impl.CalculateFPSPipe;
|
||||
import org.photonvision.vision.pipe.impl.Draw2dCrosshairPipe;
|
||||
import org.photonvision.vision.pipe.impl.ResizeImagePipe;
|
||||
@@ -38,10 +37,18 @@ public class DriverModePipeline
|
||||
private final CalculateFPSPipe calculateFPSPipe = new CalculateFPSPipe();
|
||||
private final ResizeImagePipe resizeImagePipe = new ResizeImagePipe();
|
||||
|
||||
private static final FrameThresholdType PROCESSING_TYPE = FrameThresholdType.NONE;
|
||||
|
||||
public DriverModePipeline() {
|
||||
super(PROCESSING_TYPE);
|
||||
settings = new DriverModePipelineSettings();
|
||||
}
|
||||
|
||||
public DriverModePipeline(DriverModePipelineSettings settings) {
|
||||
super(PROCESSING_TYPE);
|
||||
this.settings = settings;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void setPipeParamsImpl() {
|
||||
RotateImagePipe.RotateImageParams rotateImageParams =
|
||||
@@ -50,31 +57,25 @@ public class DriverModePipeline
|
||||
|
||||
Draw2dCrosshairPipe.Draw2dCrosshairParams draw2dCrosshairParams =
|
||||
new Draw2dCrosshairPipe.Draw2dCrosshairParams(
|
||||
frameStaticProperties, settings.streamingFrameDivisor);
|
||||
frameStaticProperties, settings.streamingFrameDivisor, settings.inputImageRotationMode);
|
||||
draw2dCrosshairPipe.setParams(draw2dCrosshairParams);
|
||||
|
||||
resizeImagePipe.setParams(
|
||||
new ResizeImagePipe.ResizeImageParams(settings.streamingFrameDivisor));
|
||||
|
||||
if (PicamJNI.isSupported() && cameraQuirks.hasQuirk(CameraQuirk.PiCam)) {
|
||||
PicamJNI.setRotation(settings.inputImageRotationMode.value);
|
||||
PicamJNI.setShouldCopyColor(true);
|
||||
}
|
||||
// if (LibCameraJNI.isSupported() && cameraQuirks.hasQuirk(CameraQuirk.PiCam)) {
|
||||
// LibCameraJNI.setRotation(settings.inputImageRotationMode.value);
|
||||
// LibCameraJNI.setShouldCopyColor(true);
|
||||
// }
|
||||
}
|
||||
|
||||
@Override
|
||||
public DriverModePipelineResult process(Frame frame, DriverModePipelineSettings settings) {
|
||||
long totalNanos = 0;
|
||||
boolean accelerated = PicamJNI.isSupported() && cameraQuirks.hasQuirk(CameraQuirk.PiCam);
|
||||
boolean accelerated = LibCameraJNI.isSupported() && cameraQuirks.hasQuirk(CameraQuirk.PiCam);
|
||||
|
||||
// apply pipes
|
||||
var inputMat = frame.image.getMat();
|
||||
if (inputMat.channels() == 1 && accelerated) {
|
||||
long colorMatPtr = PicamJNI.grabFrame(true);
|
||||
if (colorMatPtr == 0) throw new RuntimeException("Got null Mat from GPU Picam driver");
|
||||
frame.image.release();
|
||||
inputMat = new Mat(colorMatPtr);
|
||||
}
|
||||
var inputMat = frame.colorImage.getMat();
|
||||
|
||||
totalNanos += resizeImagePipe.run(inputMat).nanosElapsed;
|
||||
|
||||
@@ -91,9 +92,10 @@ public class DriverModePipeline
|
||||
var fpsResult = calculateFPSPipe.run(null);
|
||||
var fps = fpsResult.output;
|
||||
|
||||
// Flip-flop input and output in the Frame
|
||||
return new DriverModePipelineResult(
|
||||
MathUtils.nanosToMillis(totalNanos),
|
||||
fps,
|
||||
new Frame(new CVMat(inputMat), frame.frameStaticProperties));
|
||||
new Frame(frame.processedImage, frame.colorImage, frame.type, frame.frameStaticProperties));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,8 +21,6 @@ import java.util.List;
|
||||
import org.apache.commons.lang3.tuple.Pair;
|
||||
import org.photonvision.vision.frame.Frame;
|
||||
import org.photonvision.vision.frame.FrameStaticProperties;
|
||||
import org.photonvision.vision.opencv.CVMat;
|
||||
import org.photonvision.vision.opencv.ContourShape;
|
||||
import org.photonvision.vision.opencv.DualOffsetValues;
|
||||
import org.photonvision.vision.pipe.impl.*;
|
||||
import org.photonvision.vision.pipeline.result.CVPipelineResult;
|
||||
@@ -74,7 +72,8 @@ public class OutputStreamPipeline {
|
||||
settings.offsetSinglePoint,
|
||||
dualOffsetValues,
|
||||
frameStaticProperties,
|
||||
settings.streamingFrameDivisor);
|
||||
settings.streamingFrameDivisor,
|
||||
settings.inputImageRotationMode);
|
||||
draw2dCrosshairPipe.setParams(draw2dCrosshairParams);
|
||||
|
||||
var draw3dTargetsParams =
|
||||
@@ -98,18 +97,19 @@ public class OutputStreamPipeline {
|
||||
}
|
||||
|
||||
public CVPipelineResult process(
|
||||
Frame inputFrame,
|
||||
Frame outputFrame,
|
||||
Frame inputAndOutputFrame,
|
||||
AdvancedPipelineSettings settings,
|
||||
List<TrackedTarget> targetsToDraw) {
|
||||
setPipeParams(inputFrame.frameStaticProperties, settings);
|
||||
var inMat = inputFrame.image.getMat();
|
||||
var outMat = outputFrame.image.getMat();
|
||||
setPipeParams(inputAndOutputFrame.frameStaticProperties, settings);
|
||||
var inMat = inputAndOutputFrame.colorImage.getMat();
|
||||
var outMat = inputAndOutputFrame.processedImage.getMat();
|
||||
|
||||
long sumPipeNanosElapsed = 0L;
|
||||
|
||||
// Resize both in place before doing any conversion
|
||||
sumPipeNanosElapsed += pipeProfileNanos[0] = resizeImagePipe.run(inMat).nanosElapsed;
|
||||
boolean inEmpty = inMat.empty();
|
||||
if (!inEmpty)
|
||||
sumPipeNanosElapsed += pipeProfileNanos[0] = resizeImagePipe.run(inMat).nanosElapsed;
|
||||
sumPipeNanosElapsed += pipeProfileNanos[1] = resizeImagePipe.run(outMat).nanosElapsed;
|
||||
|
||||
// Convert single-channel HSV output mat to 3-channel BGR in preparation for streaming
|
||||
@@ -130,10 +130,7 @@ public class OutputStreamPipeline {
|
||||
var draw2dCrosshairResultOnOutput = draw2dCrosshairPipe.run(Pair.of(outMat, targetsToDraw));
|
||||
sumPipeNanosElapsed += pipeProfileNanos[4] = draw2dCrosshairResultOnOutput.nanosElapsed;
|
||||
|
||||
if (settings.solvePNPEnabled
|
||||
|| (settings.solvePNPEnabled
|
||||
&& settings instanceof ColoredShapePipelineSettings
|
||||
&& ((ColoredShapePipelineSettings) settings).contourShape == ContourShape.Circle)) {
|
||||
if (settings.solvePNPEnabled) {
|
||||
// Draw 3D Targets on input and output if possible
|
||||
pipeProfileNanos[5] = 0;
|
||||
pipeProfileNanos[6] = 0;
|
||||
@@ -182,7 +179,6 @@ public class OutputStreamPipeline {
|
||||
sumPipeNanosElapsed,
|
||||
fps, // Unused but here just in case
|
||||
targetsToDraw,
|
||||
new Frame(new CVMat(outMat), outputFrame.frameStaticProperties),
|
||||
new Frame(new CVMat(inMat), inputFrame.frameStaticProperties));
|
||||
inputAndOutputFrame);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,12 +18,8 @@
|
||||
package org.photonvision.vision.pipeline;
|
||||
|
||||
import java.util.List;
|
||||
import org.opencv.core.Mat;
|
||||
import org.photonvision.common.util.math.MathUtils;
|
||||
import org.photonvision.raspi.PicamJNI;
|
||||
import org.photonvision.vision.camera.CameraQuirk;
|
||||
import org.photonvision.vision.frame.Frame;
|
||||
import org.photonvision.vision.opencv.CVMat;
|
||||
import org.photonvision.vision.frame.FrameThresholdType;
|
||||
import org.photonvision.vision.opencv.Contour;
|
||||
import org.photonvision.vision.opencv.DualOffsetValues;
|
||||
import org.photonvision.vision.pipe.CVPipe.CVPipeResult;
|
||||
@@ -36,8 +32,6 @@ import org.photonvision.vision.target.TrackedTarget;
|
||||
/** Represents a pipeline for tracking retro-reflective targets. */
|
||||
@SuppressWarnings({"DuplicatedCode"})
|
||||
public class ReflectivePipeline extends CVPipeline<CVPipelineResult, ReflectivePipelineSettings> {
|
||||
private final RotateImagePipe rotateImagePipe = new RotateImagePipe();
|
||||
private final HSVPipe hsvPipe = new HSVPipe();
|
||||
private final FindContoursPipe findContoursPipe = new FindContoursPipe();
|
||||
private final SpeckleRejectPipe speckleRejectPipe = new SpeckleRejectPipe();
|
||||
private final FilterContoursPipe filterContoursPipe = new FilterContoursPipe();
|
||||
@@ -50,11 +44,15 @@ public class ReflectivePipeline extends CVPipeline<CVPipelineResult, ReflectiveP
|
||||
|
||||
private final long[] pipeProfileNanos = new long[PipelineProfiler.ReflectivePipeCount];
|
||||
|
||||
private static final FrameThresholdType PROCESSING_TYPE = FrameThresholdType.HSV;
|
||||
|
||||
public ReflectivePipeline() {
|
||||
super(PROCESSING_TYPE);
|
||||
settings = new ReflectivePipelineSettings();
|
||||
}
|
||||
|
||||
public ReflectivePipeline(ReflectivePipelineSettings settings) {
|
||||
super(PROCESSING_TYPE);
|
||||
this.settings = settings;
|
||||
}
|
||||
|
||||
@@ -67,27 +65,28 @@ public class ReflectivePipeline extends CVPipeline<CVPipelineResult, ReflectiveP
|
||||
settings.offsetDualPointB,
|
||||
settings.offsetDualPointBArea);
|
||||
|
||||
var rotateImageParams = new RotateImagePipe.RotateImageParams(settings.inputImageRotationMode);
|
||||
rotateImagePipe.setParams(rotateImageParams);
|
||||
// var rotateImageParams = new
|
||||
// RotateImagePipe.RotateImageParams(settings.inputImageRotationMode);
|
||||
// rotateImagePipe.setParams(rotateImageParams);
|
||||
|
||||
if (cameraQuirks.hasQuirk(CameraQuirk.PiCam) && PicamJNI.isSupported()) {
|
||||
PicamJNI.setThresholds(
|
||||
settings.hsvHue.getFirst() / 180d,
|
||||
settings.hsvSaturation.getFirst() / 255d,
|
||||
settings.hsvValue.getFirst() / 255d,
|
||||
settings.hsvHue.getSecond() / 180d,
|
||||
settings.hsvSaturation.getSecond() / 255d,
|
||||
settings.hsvValue.getSecond() / 255d);
|
||||
PicamJNI.setInvertHue(settings.hueInverted);
|
||||
|
||||
PicamJNI.setRotation(settings.inputImageRotationMode.value);
|
||||
PicamJNI.setShouldCopyColor(settings.inputShouldShow);
|
||||
} else {
|
||||
var hsvParams =
|
||||
new HSVPipe.HSVParams(
|
||||
settings.hsvHue, settings.hsvSaturation, settings.hsvValue, settings.hueInverted);
|
||||
hsvPipe.setParams(hsvParams);
|
||||
}
|
||||
// if (cameraQuirks.hasQuirk(CameraQuirk.PiCam) && LibCameraJNI.isSupported()) {
|
||||
// LibCameraJNI.setThresholds(
|
||||
// settings.hsvHue.getFirst() / 180d,
|
||||
// settings.hsvSaturation.getFirst() / 255d,
|
||||
// settings.hsvValue.getFirst() / 255d,
|
||||
// settings.hsvHue.getSecond() / 180d,
|
||||
// settings.hsvSaturation.getSecond() / 255d,
|
||||
// settings.hsvValue.getSecond() / 255d);
|
||||
// // LibCameraJNI.setInvertHue(settings.hueInverted);
|
||||
// LibCameraJNI.setRotation(settings.inputImageRotationMode.value);
|
||||
// // LibCameraJNI.setShouldCopyColor(settings.inputShouldShow);
|
||||
// } else {
|
||||
// var hsvParams =
|
||||
// new HSVPipe.HSVParams(
|
||||
// settings.hsvHue, settings.hsvSaturation, settings.hsvValue,
|
||||
// settings.hueInverted);
|
||||
// hsvPipe.setParams(hsvParams);
|
||||
// }
|
||||
|
||||
var findContoursParams = new FindContoursPipe.FindContoursParams();
|
||||
findContoursPipe.setParams(findContoursParams);
|
||||
@@ -148,40 +147,8 @@ public class ReflectivePipeline extends CVPipeline<CVPipelineResult, ReflectiveP
|
||||
public CVPipelineResult process(Frame frame, ReflectivePipelineSettings settings) {
|
||||
long sumPipeNanosElapsed = 0L;
|
||||
|
||||
CVPipeResult<Mat> hsvPipeResult;
|
||||
Mat rawInputMat;
|
||||
if (frame.image.getMat().channels() != 1) {
|
||||
var rotateImageResult = rotateImagePipe.run(frame.image.getMat());
|
||||
sumPipeNanosElapsed += pipeProfileNanos[0] = rotateImageResult.nanosElapsed;
|
||||
|
||||
rawInputMat = frame.image.getMat();
|
||||
|
||||
hsvPipeResult = hsvPipe.run(rawInputMat);
|
||||
sumPipeNanosElapsed += hsvPipeResult.nanosElapsed;
|
||||
pipeProfileNanos[1] = pipeProfileNanos[1] = hsvPipeResult.nanosElapsed;
|
||||
} else {
|
||||
// Try to copy the color frame.
|
||||
long inputMatPtr = PicamJNI.grabFrame(true);
|
||||
if (inputMatPtr != 0) {
|
||||
// If we grabbed it (in color copy mode), make a new Mat of it
|
||||
rawInputMat = new Mat(inputMatPtr);
|
||||
} else {
|
||||
// Otherwise, the input mat is frame we got from the camera
|
||||
rawInputMat = frame.image.getMat();
|
||||
// // Otherwise, use a blank/empty mat as placeholder
|
||||
// rawInputMat = new Mat();
|
||||
}
|
||||
|
||||
// We can skip a few steps if the image is single channel because we've already done them on
|
||||
// the GPU
|
||||
hsvPipeResult = new CVPipeResult<>();
|
||||
hsvPipeResult.output = frame.image.getMat();
|
||||
hsvPipeResult.nanosElapsed = MathUtils.wpiNanoTime() - frame.timestampNanos;
|
||||
|
||||
sumPipeNanosElapsed = pipeProfileNanos[1] = hsvPipeResult.nanosElapsed;
|
||||
}
|
||||
|
||||
CVPipeResult<List<Contour>> findContoursResult = findContoursPipe.run(hsvPipeResult.output);
|
||||
CVPipeResult<List<Contour>> findContoursResult =
|
||||
findContoursPipe.run(frame.processedImage.getMat());
|
||||
sumPipeNanosElapsed += pipeProfileNanos[2] = findContoursResult.nanosElapsed;
|
||||
|
||||
CVPipeResult<List<Contour>> speckleRejectResult =
|
||||
@@ -226,11 +193,6 @@ public class ReflectivePipeline extends CVPipeline<CVPipelineResult, ReflectiveP
|
||||
|
||||
PipelineProfiler.printReflectiveProfile(pipeProfileNanos);
|
||||
|
||||
return new CVPipelineResult(
|
||||
sumPipeNanosElapsed,
|
||||
fps,
|
||||
targetList,
|
||||
new Frame(new CVMat(hsvPipeResult.output), frame.frameStaticProperties),
|
||||
new Frame(new CVMat(rawInputMat), frame.frameStaticProperties));
|
||||
return new CVPipelineResult(sumPipeNanosElapsed, fps, targetList, frame);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,26 +29,19 @@ public class CVPipelineResult implements Releasable {
|
||||
public final double processingNanos;
|
||||
public final double fps;
|
||||
public final List<TrackedTarget> targets;
|
||||
public final Frame outputFrame;
|
||||
public final Frame inputFrame;
|
||||
public final Frame inputAndOutputFrame;
|
||||
|
||||
public CVPipelineResult(
|
||||
double processingNanos,
|
||||
double fps,
|
||||
List<TrackedTarget> targets,
|
||||
Frame outputFrame,
|
||||
Frame inputFrame) {
|
||||
double processingNanos, double fps, List<TrackedTarget> targets, Frame inputFrame) {
|
||||
this.processingNanos = processingNanos;
|
||||
this.fps = fps;
|
||||
this.targets = targets != null ? targets : Collections.emptyList();
|
||||
|
||||
this.outputFrame = outputFrame;
|
||||
this.inputFrame = inputFrame;
|
||||
this.inputAndOutputFrame = inputFrame;
|
||||
}
|
||||
|
||||
public CVPipelineResult(
|
||||
double processingNanos, double fps, List<TrackedTarget> targets, Frame outputFrame) {
|
||||
this(processingNanos, fps, targets, outputFrame, null);
|
||||
public CVPipelineResult(double processingNanos, double fps, List<TrackedTarget> targets) {
|
||||
this(processingNanos, fps, targets, null);
|
||||
}
|
||||
|
||||
public boolean hasTargets() {
|
||||
@@ -59,8 +52,7 @@ public class CVPipelineResult implements Releasable {
|
||||
for (TrackedTarget tt : targets) {
|
||||
tt.release();
|
||||
}
|
||||
outputFrame.release();
|
||||
if (inputFrame != null) inputFrame.release();
|
||||
if (inputAndOutputFrame != null) inputAndOutputFrame.release();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -68,6 +60,7 @@ public class CVPipelineResult implements Releasable {
|
||||
* the latency is relative to the time at which this method is called. Waiting to call this method
|
||||
* will change the latency this method returns.
|
||||
*/
|
||||
@Deprecated
|
||||
public double getLatencyMillis() {
|
||||
var now = MathUtils.wpiNanoTime();
|
||||
return MathUtils.nanosToMillis(now - imageCaptureTimestampNanos);
|
||||
|
||||
@@ -92,6 +92,28 @@ public class PipelineManager {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the settings for a pipeline by index.
|
||||
*
|
||||
* @param index Index of pipeline whose nickname needs getting.
|
||||
* @return the nickname of the pipeline whose index was provided.
|
||||
*/
|
||||
public String getPipelineNickname(int index) {
|
||||
if (index < 0) {
|
||||
switch (index) {
|
||||
case DRIVERMODE_INDEX:
|
||||
return driverModePipeline.getSettings().pipelineNickname;
|
||||
case CAL_3D_INDEX:
|
||||
return calibration3dPipeline.getSettings().pipelineNickname;
|
||||
}
|
||||
}
|
||||
|
||||
for (var setting : userPipelineSettings) {
|
||||
if (setting.pipelineIndex == index) return setting.pipelineNickname;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a list of nicknames for all user pipelines
|
||||
*
|
||||
@@ -181,17 +203,17 @@ public class PipelineManager {
|
||||
var desiredPipelineSettings = userPipelineSettings.get(currentPipelineIndex);
|
||||
switch (desiredPipelineSettings.pipelineType) {
|
||||
case Reflective:
|
||||
logger.debug("Creatig Reflective pipeline");
|
||||
logger.debug("Creating Reflective pipeline");
|
||||
currentUserPipeline =
|
||||
new ReflectivePipeline((ReflectivePipelineSettings) desiredPipelineSettings);
|
||||
break;
|
||||
case ColoredShape:
|
||||
logger.debug("Creatig ColoredShape pipeline");
|
||||
logger.debug("Creating ColoredShape pipeline");
|
||||
currentUserPipeline =
|
||||
new ColoredShapePipeline((ColoredShapePipelineSettings) desiredPipelineSettings);
|
||||
break;
|
||||
case AprilTag:
|
||||
logger.debug("Creatig AprilTag pipeline");
|
||||
logger.debug("Creating AprilTag pipeline");
|
||||
currentUserPipeline =
|
||||
new AprilTagPipeline((AprilTagPipelineSettings) desiredPipelineSettings);
|
||||
break;
|
||||
|
||||
@@ -17,10 +17,11 @@
|
||||
|
||||
package org.photonvision.vision.processes;
|
||||
|
||||
import edu.wpi.first.math.geometry.Rotation2d;
|
||||
import edu.wpi.first.cscore.VideoException;
|
||||
import edu.wpi.first.math.util.Units;
|
||||
import io.javalin.websocket.WsContext;
|
||||
import java.util.*;
|
||||
import java.util.function.BiConsumer;
|
||||
import org.photonvision.common.configuration.CameraConfiguration;
|
||||
import org.photonvision.common.configuration.ConfigManager;
|
||||
import org.photonvision.common.configuration.PhotonConfiguration;
|
||||
@@ -33,15 +34,13 @@ import org.photonvision.common.hardware.HardwareManager;
|
||||
import org.photonvision.common.logging.LogGroup;
|
||||
import org.photonvision.common.logging.Logger;
|
||||
import org.photonvision.common.util.SerializationUtils;
|
||||
import org.photonvision.common.util.java.TriConsumer;
|
||||
import org.photonvision.vision.calibration.CameraCalibrationCoefficients;
|
||||
import org.photonvision.vision.camera.CameraQuirk;
|
||||
import org.photonvision.vision.camera.LibcameraGpuSource;
|
||||
import org.photonvision.vision.camera.QuirkyCamera;
|
||||
import org.photonvision.vision.camera.USBCameraSource;
|
||||
import org.photonvision.vision.camera.ZeroCopyPicamSource;
|
||||
import org.photonvision.vision.frame.Frame;
|
||||
import org.photonvision.vision.frame.consumer.FileSaveFrameConsumer;
|
||||
import org.photonvision.vision.frame.consumer.MJPGFrameConsumer;
|
||||
import org.photonvision.vision.pipeline.AdvancedPipelineSettings;
|
||||
import org.photonvision.vision.pipeline.OutputStreamPipeline;
|
||||
import org.photonvision.vision.pipeline.ReflectivePipelineSettings;
|
||||
@@ -49,6 +48,8 @@ import org.photonvision.vision.pipeline.UICalibrationData;
|
||||
import org.photonvision.vision.pipeline.result.CVPipelineResult;
|
||||
import org.photonvision.vision.target.TargetModel;
|
||||
import org.photonvision.vision.target.TrackedTarget;
|
||||
import org.photonvision.vision.videoStream.SocketVideoStream;
|
||||
import org.photonvision.vision.videoStream.SocketVideoStreamManager;
|
||||
|
||||
/**
|
||||
* This is the God Class
|
||||
@@ -57,32 +58,31 @@ import org.photonvision.vision.target.TrackedTarget;
|
||||
* provide info on settings changes. VisionModuleManager holds a list of all current vision modules.
|
||||
*/
|
||||
public class VisionModule {
|
||||
private static final int streamFPSCap = 30;
|
||||
|
||||
private final Logger logger;
|
||||
protected final PipelineManager pipelineManager;
|
||||
protected final VisionSource visionSource;
|
||||
private final VisionRunner visionRunner;
|
||||
private final StreamRunnable streamRunnable;
|
||||
private final LinkedList<CVPipelineResultConsumer> resultConsumers = new LinkedList<>();
|
||||
private final LinkedList<CVPipelineResultConsumer> fpsLimitedResultConsumers = new LinkedList<>();
|
||||
// Raw result consumers run before any drawing has been done by the OutputStreamPipeline
|
||||
private final LinkedList<TriConsumer<Frame, Frame, List<TrackedTarget>>> rawResultConsumers =
|
||||
private final LinkedList<BiConsumer<Frame, List<TrackedTarget>>> streamResultConsumers =
|
||||
new LinkedList<>();
|
||||
private final NTDataPublisher ntConsumer;
|
||||
private final UIDataPublisher uiDataConsumer;
|
||||
protected final int moduleIndex;
|
||||
protected final QuirkyCamera cameraQuirks;
|
||||
|
||||
private long lastFrameConsumeMillis;
|
||||
protected TrackedTarget lastPipelineResultBestTarget;
|
||||
|
||||
MJPGFrameConsumer dashboardInputStreamer;
|
||||
MJPGFrameConsumer dashboardOutputStreamer;
|
||||
private int inputStreamPort = -1;
|
||||
private int outputStreamPort = -1;
|
||||
|
||||
FileSaveFrameConsumer inputFrameSaver;
|
||||
FileSaveFrameConsumer outputFrameSaver;
|
||||
|
||||
SocketVideoStream inputVideoStreamer;
|
||||
SocketVideoStream outputVideoStreamer;
|
||||
|
||||
public VisionModule(PipelineManager pipelineManager, VisionSource visionSource, int index) {
|
||||
logger =
|
||||
new Logger(
|
||||
@@ -93,7 +93,7 @@ public class VisionModule {
|
||||
// Find quirks for the current camera
|
||||
if (visionSource instanceof USBCameraSource) {
|
||||
cameraQuirks = ((USBCameraSource) visionSource).cameraQuirks;
|
||||
} else if (visionSource instanceof ZeroCopyPicamSource) {
|
||||
} else if (visionSource instanceof LibcameraGpuSource) {
|
||||
cameraQuirks = QuirkyCamera.ZeroCopyPiCamera;
|
||||
} else {
|
||||
cameraQuirks = QuirkyCamera.DefaultCamera;
|
||||
@@ -130,7 +130,7 @@ public class VisionModule {
|
||||
|
||||
createStreams();
|
||||
|
||||
recreateFpsLimitedResultConsumers();
|
||||
recreateStreamResultConsumers();
|
||||
|
||||
ntConsumer =
|
||||
new NTDataPublisher(
|
||||
@@ -167,48 +167,44 @@ public class VisionModule {
|
||||
}
|
||||
|
||||
private void destroyStreams() {
|
||||
dashboardInputStreamer.close();
|
||||
dashboardOutputStreamer.close();
|
||||
SocketVideoStreamManager.getInstance().removeStream(inputVideoStreamer);
|
||||
SocketVideoStreamManager.getInstance().removeStream(outputVideoStreamer);
|
||||
}
|
||||
|
||||
private void createStreams() {
|
||||
var camStreamIdx = visionSource.getSettables().getConfiguration().streamIndex;
|
||||
// If idx = 0, we want (1181, 1182)
|
||||
var inputStreamPort = 1181 + (camStreamIdx * 2);
|
||||
var outputStreamPort = 1181 + (camStreamIdx * 2) + 1;
|
||||
|
||||
dashboardOutputStreamer =
|
||||
new MJPGFrameConsumer(
|
||||
visionSource.getSettables().getConfiguration().nickname + "-output", outputStreamPort);
|
||||
dashboardInputStreamer =
|
||||
new MJPGFrameConsumer(
|
||||
visionSource.getSettables().getConfiguration().uniqueName + "-input", inputStreamPort);
|
||||
this.inputStreamPort = 1181 + (camStreamIdx * 2);
|
||||
this.outputStreamPort = 1181 + (camStreamIdx * 2) + 1;
|
||||
|
||||
inputFrameSaver =
|
||||
new FileSaveFrameConsumer(visionSource.getSettables().getConfiguration().nickname, "input");
|
||||
outputFrameSaver =
|
||||
new FileSaveFrameConsumer(
|
||||
visionSource.getSettables().getConfiguration().nickname, "output");
|
||||
|
||||
inputVideoStreamer = new SocketVideoStream(this.inputStreamPort);
|
||||
outputVideoStreamer = new SocketVideoStream(this.outputStreamPort);
|
||||
SocketVideoStreamManager.getInstance().addStream(inputVideoStreamer);
|
||||
SocketVideoStreamManager.getInstance().addStream(outputVideoStreamer);
|
||||
}
|
||||
|
||||
private void recreateFpsLimitedResultConsumers() {
|
||||
// Important! These must come before the stream result consumers because the stream result
|
||||
// consumers release the frame
|
||||
rawResultConsumers.add((in, out, tgts) -> inputFrameSaver.accept(in));
|
||||
fpsLimitedResultConsumers.add(result -> outputFrameSaver.accept(result.outputFrame));
|
||||
|
||||
fpsLimitedResultConsumers.add(
|
||||
result -> {
|
||||
if (this.pipelineManager.getCurrentPipelineSettings().inputShouldShow)
|
||||
dashboardInputStreamer.accept(result.inputFrame);
|
||||
else dashboardInputStreamer.disabledTick();
|
||||
private void recreateStreamResultConsumers() {
|
||||
streamResultConsumers.add(
|
||||
(frame, tgts) -> {
|
||||
if (frame != null) inputFrameSaver.accept(frame.colorImage);
|
||||
});
|
||||
fpsLimitedResultConsumers.add(
|
||||
result -> {
|
||||
if (this.pipelineManager.getCurrentPipelineSettings().outputShouldShow)
|
||||
dashboardOutputStreamer.accept(result.outputFrame);
|
||||
else dashboardInputStreamer.disabledTick();
|
||||
;
|
||||
streamResultConsumers.add(
|
||||
(frame, tgts) -> {
|
||||
if (frame != null) outputFrameSaver.accept(frame.processedImage);
|
||||
});
|
||||
streamResultConsumers.add(
|
||||
(frame, tgts) -> {
|
||||
if (frame != null) inputVideoStreamer.accept(frame.colorImage);
|
||||
});
|
||||
streamResultConsumers.add(
|
||||
(frame, tgts) -> {
|
||||
if (frame != null) outputVideoStreamer.accept(frame.processedImage);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -216,7 +212,7 @@ public class VisionModule {
|
||||
private final OutputStreamPipeline outputStreamPipeline;
|
||||
|
||||
private final Object frameLock = new Object();
|
||||
private Frame inputFrame, outputFrame;
|
||||
private Frame latestFrame;
|
||||
private AdvancedPipelineSettings settings = new AdvancedPipelineSettings();
|
||||
private List<TrackedTarget> targets = new ArrayList<>();
|
||||
|
||||
@@ -227,42 +223,35 @@ public class VisionModule {
|
||||
}
|
||||
|
||||
public void updateData(
|
||||
Frame inputFrame,
|
||||
Frame outputFrame,
|
||||
AdvancedPipelineSettings settings,
|
||||
List<TrackedTarget> targets) {
|
||||
Frame inputOutputFrame, AdvancedPipelineSettings settings, List<TrackedTarget> targets) {
|
||||
synchronized (frameLock) {
|
||||
if (shouldRun && this.inputFrame != null && this.outputFrame != null) {
|
||||
if (shouldRun && this.latestFrame != null) {
|
||||
logger.trace("Fell behind; releasing last unused Mats");
|
||||
this.inputFrame.release();
|
||||
this.outputFrame.release();
|
||||
this.latestFrame.release();
|
||||
}
|
||||
|
||||
this.inputFrame = inputFrame;
|
||||
this.outputFrame = outputFrame;
|
||||
this.latestFrame = inputOutputFrame;
|
||||
this.settings = settings;
|
||||
this.targets = targets;
|
||||
|
||||
shouldRun =
|
||||
inputFrame != null
|
||||
&& !inputFrame.image.getMat().empty()
|
||||
&& outputFrame != null
|
||||
&& !outputFrame.image.getMat().empty();
|
||||
shouldRun = inputOutputFrame != null;
|
||||
// && inputOutputFrame.colorImage != null
|
||||
// && !inputOutputFrame.colorImage.getMat().empty()
|
||||
// && inputOutputFrame.processedImage != null
|
||||
// && !inputOutputFrame.processedImage.getMat().empty();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
while (true) {
|
||||
final Frame inputFrame, outputFrame;
|
||||
final Frame m_frame;
|
||||
final AdvancedPipelineSettings settings;
|
||||
final List<TrackedTarget> targets;
|
||||
final boolean shouldRun;
|
||||
synchronized (frameLock) {
|
||||
inputFrame = this.inputFrame;
|
||||
outputFrame = this.outputFrame;
|
||||
this.inputFrame = null;
|
||||
this.outputFrame = null;
|
||||
m_frame = this.latestFrame;
|
||||
this.latestFrame = null;
|
||||
|
||||
settings = this.settings;
|
||||
targets = this.targets;
|
||||
@@ -271,19 +260,16 @@ public class VisionModule {
|
||||
this.shouldRun = false;
|
||||
}
|
||||
if (shouldRun) {
|
||||
consumeRawResults(inputFrame, outputFrame, targets);
|
||||
try {
|
||||
CVPipelineResult osr =
|
||||
outputStreamPipeline.process(inputFrame, outputFrame, settings, targets);
|
||||
CVPipelineResult osr = outputStreamPipeline.process(m_frame, settings, targets);
|
||||
consumeResults(m_frame, targets);
|
||||
|
||||
consumeFpsLimitedResult(osr);
|
||||
} catch (Exception e) {
|
||||
// Never die
|
||||
logger.error("Exception while running stream runnable!", e);
|
||||
}
|
||||
try {
|
||||
inputFrame.release();
|
||||
outputFrame.release();
|
||||
m_frame.release();
|
||||
} catch (Exception e) {
|
||||
logger.error("Exception freeing frames", e);
|
||||
}
|
||||
@@ -304,7 +290,7 @@ public class VisionModule {
|
||||
streamRunnable.start();
|
||||
}
|
||||
|
||||
public void setFovAndPitch(double fov, Rotation2d pitch) {
|
||||
public void setFov(double fov) {
|
||||
var settables = visionSource.getSettables();
|
||||
logger.trace(() -> "Setting " + settables.getConfiguration().nickname + ") FOV (" + fov + ")");
|
||||
|
||||
@@ -387,6 +373,7 @@ public class VisionModule {
|
||||
|
||||
void setPipeline(int index) {
|
||||
logger.info("Setting pipeline to " + index);
|
||||
logger.info("Pipeline name: " + pipelineManager.getPipelineNickname(index));
|
||||
pipelineManager.setIndex(index);
|
||||
var pipelineSettings = pipelineManager.getPipelineSettings(index);
|
||||
|
||||
@@ -406,8 +393,12 @@ public class VisionModule {
|
||||
}
|
||||
|
||||
visionSource.getSettables().setExposure(pipelineSettings.cameraExposure);
|
||||
visionSource.getSettables().setAutoExposure(pipelineSettings.cameraAutoExposure);
|
||||
|
||||
try {
|
||||
visionSource.getSettables().setAutoExposure(pipelineSettings.cameraAutoExposure);
|
||||
} catch (VideoException e) {
|
||||
logger.error("Unable to set camera auto exposure!");
|
||||
logger.error(e.toString());
|
||||
}
|
||||
if (cameraQuirks.hasQuirk(CameraQuirk.Gain)) {
|
||||
// If the gain is disabled for some reason, re-enable it
|
||||
if (pipelineSettings.cameraGain == -1) pipelineSettings.cameraGain = 20;
|
||||
@@ -474,14 +465,14 @@ public class VisionModule {
|
||||
outputFrameSaver.updateCameraNickname(newName);
|
||||
|
||||
// Rename streams
|
||||
fpsLimitedResultConsumers.clear();
|
||||
streamResultConsumers.clear();
|
||||
|
||||
// Teardown and recreate streams
|
||||
destroyStreams();
|
||||
createStreams();
|
||||
|
||||
// Rebuild streamers
|
||||
recreateFpsLimitedResultConsumers();
|
||||
recreateStreamResultConsumers();
|
||||
|
||||
// Push new data to the UI
|
||||
saveAndBroadcastAll();
|
||||
@@ -508,7 +499,7 @@ public class VisionModule {
|
||||
internalMap.put("fps", videoModes.get(k).fps);
|
||||
internalMap.put(
|
||||
"pixelFormat",
|
||||
((videoModes.get(k) instanceof ZeroCopyPicamSource.FPSRatedVideoMode)
|
||||
((videoModes.get(k) instanceof LibcameraGpuSource.FPSRatedVideoMode)
|
||||
? "kPicam"
|
||||
: videoModes.get(k).pixelFormat.toString())
|
||||
.substring(1)); // Remove the k prefix
|
||||
@@ -516,8 +507,8 @@ public class VisionModule {
|
||||
temp.put(k, internalMap);
|
||||
}
|
||||
ret.videoFormatList = temp;
|
||||
ret.outputStreamPort = dashboardOutputStreamer.getCurrentStreamPort();
|
||||
ret.inputStreamPort = dashboardInputStreamer.getCurrentStreamPort();
|
||||
ret.outputStreamPort = this.outputStreamPort;
|
||||
ret.inputStreamPort = this.inputStreamPort;
|
||||
|
||||
var calList = new ArrayList<HashMap<String, Object>>();
|
||||
for (var c : visionSource.getSettables().getConfiguration().calibrations) {
|
||||
@@ -528,7 +519,7 @@ public class VisionModule {
|
||||
internalMap.put("width", c.resolution.width);
|
||||
internalMap.put("height", c.resolution.height);
|
||||
internalMap.put("intrinsics", c.cameraIntrinsics.data);
|
||||
internalMap.put("extrinsics", c.cameraExtrinsics.data);
|
||||
internalMap.put("distCoeffs", c.distCoeffs.data);
|
||||
|
||||
calList.add(internalMap);
|
||||
}
|
||||
@@ -558,16 +549,15 @@ public class VisionModule {
|
||||
consumePipelineResult(result);
|
||||
|
||||
// Pipelines like DriverMode and Calibrate3dPipeline have null output frames
|
||||
if (result.inputFrame != null
|
||||
if (result.inputAndOutputFrame != null
|
||||
&& (pipelineManager.getCurrentPipelineSettings() instanceof AdvancedPipelineSettings)) {
|
||||
streamRunnable.updateData(
|
||||
result.inputFrame,
|
||||
result.outputFrame,
|
||||
result.inputAndOutputFrame,
|
||||
(AdvancedPipelineSettings) pipelineManager.getCurrentPipelineSettings(),
|
||||
result.targets);
|
||||
// The streamRunnable manages releasing in this case
|
||||
} else {
|
||||
consumeFpsLimitedResult(result);
|
||||
consumeResults(result.inputAndOutputFrame, result.targets);
|
||||
|
||||
result.release();
|
||||
// In this case we don't bother with a separate streaming thread and we release
|
||||
@@ -580,20 +570,10 @@ public class VisionModule {
|
||||
}
|
||||
}
|
||||
|
||||
private void consumeFpsLimitedResult(CVPipelineResult result) {
|
||||
long dt = System.currentTimeMillis() - lastFrameConsumeMillis;
|
||||
if (dt > 1000 / streamFPSCap) {
|
||||
for (var c : fpsLimitedResultConsumers) {
|
||||
c.accept(result);
|
||||
}
|
||||
lastFrameConsumeMillis = System.currentTimeMillis();
|
||||
}
|
||||
}
|
||||
|
||||
/** Consume results prior to drawing on them. */
|
||||
private void consumeRawResults(Frame inputFrame, Frame outputFrame, List<TrackedTarget> targets) {
|
||||
for (var c : rawResultConsumers) {
|
||||
c.accept(inputFrame, outputFrame, targets);
|
||||
/** Consume stream/target results, no rate limiting applied */
|
||||
private void consumeResults(Frame frame, List<TrackedTarget> targets) {
|
||||
for (var c : streamResultConsumers) {
|
||||
c.accept(frame, targets);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -148,9 +148,13 @@ public class VisionModuleChangeSubscriber extends DataChangeSubscriber {
|
||||
curAdvSettings.offsetDualPointB = newPoint;
|
||||
curAdvSettings.offsetDualPointBArea = latestTarget.getArea();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,8 +22,9 @@ import java.util.function.Supplier;
|
||||
import org.photonvision.common.logging.LogGroup;
|
||||
import org.photonvision.common.logging.Logger;
|
||||
import org.photonvision.vision.camera.QuirkyCamera;
|
||||
import org.photonvision.vision.frame.Frame;
|
||||
import org.photonvision.vision.frame.FrameProvider;
|
||||
import org.photonvision.vision.pipe.impl.HSVPipe;
|
||||
import org.photonvision.vision.pipeline.AdvancedPipelineSettings;
|
||||
import org.photonvision.vision.pipeline.CVPipeline;
|
||||
import org.photonvision.vision.pipeline.result.CVPipelineResult;
|
||||
|
||||
@@ -32,7 +33,7 @@ import org.photonvision.vision.pipeline.result.CVPipelineResult;
|
||||
public class VisionRunner {
|
||||
private final Logger logger;
|
||||
private final Thread visionProcessThread;
|
||||
private final Supplier<Frame> frameSupplier;
|
||||
private final FrameProvider frameSupplier;
|
||||
private final Supplier<CVPipeline> pipelineSupplier;
|
||||
private final Consumer<CVPipelineResult> pipelineResultConsumer;
|
||||
private final QuirkyCamera cameraQuirks;
|
||||
@@ -69,8 +70,29 @@ public class VisionRunner {
|
||||
private void update() {
|
||||
while (!Thread.interrupted()) {
|
||||
var pipeline = pipelineSupplier.get();
|
||||
|
||||
// Tell our camera implementation here what kind of pre-processing we need it to be doing
|
||||
// (pipeline-dependent). I kinda hate how much leak this has...
|
||||
// TODO would a callback object be a better fit?
|
||||
var wantedProcessType = pipeline.getThresholdType();
|
||||
frameSupplier.requestFrameThresholdType(wantedProcessType);
|
||||
var settings = pipeline.getSettings();
|
||||
if (settings instanceof AdvancedPipelineSettings) {
|
||||
var advanced = (AdvancedPipelineSettings) settings;
|
||||
var hsvParams =
|
||||
new HSVPipe.HSVParams(
|
||||
advanced.hsvHue, advanced.hsvSaturation, advanced.hsvValue, advanced.hueInverted);
|
||||
// TODO who should deal with preventing this from happening _every single loop_?
|
||||
frameSupplier.requestHsvSettings(hsvParams);
|
||||
}
|
||||
frameSupplier.requestFrameRotation(settings.inputImageRotationMode);
|
||||
frameSupplier.requestFrameCopies(settings.inputShouldShow, settings.outputShouldShow);
|
||||
|
||||
// Grab the new camera frame
|
||||
var frame = frameSupplier.get();
|
||||
|
||||
// There's no guarantee the processing type change will occur this tick, so pipelines should
|
||||
// check themselves
|
||||
try {
|
||||
var pipelineResult = pipeline.run(frame, cameraQuirks);
|
||||
pipelineResultConsumer.accept(pipelineResult);
|
||||
@@ -78,6 +100,7 @@ public class VisionRunner {
|
||||
logger.error("Exception on loop " + loopCount);
|
||||
ex.printStackTrace();
|
||||
}
|
||||
|
||||
loopCount++;
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user