Compare commits

...

72 Commits

Author SHA1 Message Date
Sriman Achanta
15fbe29d34 Remove redundant if check in OutputStreamPipeline (#660)
Co-authored-by: Matt <matthew.morley.ca@gmail.com>
2022-12-30 02:00:14 -05:00
Jack
550194152a Add RobotPoseEstimator (#571)
RobotPoseEstimator can pick the most likely pose for the robot given a number of possible poses, using a number of different strategies. Examples are still WIP.
2022-12-30 01:40:13 -05:00
Matt
3a10f49b54 Only run Apriltag pose estimation when enabled (#657)
Co-authored-by: Chris Gerth <gerth2@users.noreply.github.com>
2022-12-28 13:32:38 -08:00
Matt
7ff630dc44 Replace MMAL driver with Libcamera (#491)
Co-authored-by: Chris Gerth <gerth2@users.noreply.github.com>
2022-12-28 14:21:41 -05:00
Matt
4088a394f3 Fix typo in main.yml (#659) 2022-12-28 13:41:29 -05:00
Mohammad Durrani
78ab5e7c1d Upgrade to jdk 17 (#653)
Still targets Java 11 language support

Co-authored-by: Chris Gerth <gerth2@users.noreply.github.com>
Co-authored-by: Matt <matthew.morley.ca@gmail.com>
2022-12-27 15:56:21 -05:00
Matt
14d263a567 Fix dev release artifact path (#654) 2022-12-27 15:55:59 -05:00
Chris Gerth
cf1a45d35b Set a compression level to better optimize the latency of the stream by reducing the bytes sent (#656) 2022-12-27 15:55:43 -05:00
Matt
2ebc27aa3b Use refactored Apriltag API in WPILib (#644)
Bumps to a wpilib dev version, until they cut a new release. Should help address the random NPEs from the old JNI.

Co-authored-by: Chris Gerth <gerth2@users.noreply.github.com>
2022-12-27 13:47:20 -05:00
Matt
95c55f08cf Switch to native WebSocket in UI (#649)
Greatly improves Firefox performance
2022-12-27 10:23:55 -05:00
Paul Rensing
4382b8ea3f Cpu performance mode & BIG.little tweaks (#633)
* Add nice value to service file, and give example CPU selection for those who need it.

* Use cpufrequtils package to set CPUs into performance mode

* Add comment about nice value

* Need to say "yes" to installing cpufrequtils, and safer to do so for all installs.

Co-authored-by: Matt <matthew.morley.ca@gmail.com>
2022-12-26 23:04:46 -05:00
Chris Gerth
b1905954bc Fix isRaspian() to properly detect Buster image (#637)
* Revised isRaspian() call to look in multiple spots to check if we're a Pi or not

* wpiformat

* linefeed fixup

* whoops

* WIP updating platform

* More platform fixups WIP

* Condensed metrics classes, but expanded the configuration to default to file, but fall back on hardcoded commands for certain platforms

* Migrate unixSupported to isLinux

* applied spotless

* wpiformat

* Linux metrics (#641)

* Move generic commands from PiCmds to LinuxCmds; have PiCmds inherit from LinuxCmds

* Better names for variables to save the total memory values

* Remove "Bionic" from the architecture; that is not actually determined.

* Trigger PhotonVision CI

* Dummy change to trigger CI

* Run format

Update index.html

Co-authored-by: Mohammad Durrani <46766905+mdurrani808@users.noreply.github.com>
Co-authored-by: Paul Rensing <prensing@gmail.com>
Co-authored-by: Matt <matthew.morley.ca@gmail.com>
2022-12-26 22:51:34 -05:00
Mohammad Durrani
548f52e117 Allow JAR update/restart on all linux platforms (#651)
* Added linux support

* Changed to just check linux
2022-12-26 21:40:36 -05:00
Chris Gerth
1971744589 Revert to mjpeg (#645)
* Reverted to front end using MJPEG streams. Added FPS limiting to the stream.

* formatting

* fixup - got handlers getting called on error to reload

* revised architecture to let a click open a new tab

Co-authored-by: Matt <matthew.morley.ca@gmail.com>
2022-12-25 06:55:12 -08:00
Matt
6c51d8ab51 Update license on orthogonalizeRotationMatrix (#615)
Co-authored-by: Chris Gerth <gerth2@users.noreply.github.com>
Co-authored-by: Mohammad Durrani <46766905+mdurrani808@users.noreply.github.com>
2022-12-25 00:08:09 -05:00
Lavi Arzi
8330bf9d92 Expose camera name in PhotonCamera (#523)
Co-authored-by: Matt <matthew.morley.ca@gmail.com>
2022-12-25 00:01:57 -05:00
amquake
e1b39a1723 Prepare for config.json renaming cameraExtrinsics to distCoeffs (#612)
Doesn't actually rename in the JSON file yet, but fixes the name everywhere else
2022-12-25 00:01:24 -05:00
Mohammad Durrani
915f784d9d Add distance, yaw, and robot pose methods to photonlib (#642) 2022-12-25 00:00:00 -05:00
Chris Gerth
96006fc501 Fix misplaced crosshair when rotated +-90 degrees (#646) 2022-12-24 19:57:26 -05:00
Matt
4fd7533456 Manage network on all Linux platforms (#630) 2022-12-17 22:29:27 -06:00
shueja-personal
bb63af601d Update to wpilib 2023 beta 7 (#607)
We now need platform specific jars -- reworks actions to support that. Currently only generates 32 bit pi images.
2022-12-16 20:05:23 -05:00
Mohammad Durrani
da1aabae3a Add avahi daemon to install script (#625) 2022-12-08 19:23:02 -05:00
Drew Williams
643db9c435 Add check for packet of incorrect length (#629)
Co-authored-by: Matt <matthew.morley.ca@gmail.com>
2022-12-08 19:22:31 -05:00
sarah-e-c
ec7bef7a4b Logging for NTDataPublisher (#560)
* logging for NTDataPublisher

* logging name along with index

* formatting lol

* resolution logging

* Removed pipelineManager object from data publisher
2022-12-01 18:35:43 -06:00
Mohammad Durrani
b72f4ca2a9 Update heap size for install script (#622) 2022-12-01 18:25:29 -06:00
Mohammad Durrani
ffd741ec0a Add curl (#618)
* Add curl

* goofy wording

Co-authored-by: shueja-personal <32416547+shueja-personal@users.noreply.github.com>
2022-12-01 18:24:57 -06:00
Chris Gerth
678961e4f2 Websocket Stream Stats & Robustness Improvements (#605)
* wip support for a stats overlay

* WIP adding stream stats. But.... eeeh. Corporate.

* kbits over mbytes

* A ton more tweaks:

- Increased thread priority for streaming to reduce "stutter/slow" issues
- revised client side URL creation order to prevent the possibility of repeat-identical URL's
- Improved overlay to only be visible on mouseover, and fully centered in the screen

* wpiformat on js
2022-12-01 12:42:21 -06:00
amquake
4c004fc780 README link fix (#598) 2022-11-14 20:27:29 -05:00
Jack
41a00bc90f Fix mismatched doc building python version that prevents package install (#596) 2022-11-13 23:33:35 -05:00
Matt
dcad7f34a2 Fix thinclient address in dev builds and move thinclient (#586)
* Fix thinclient address in dev builds and move thinclient

Update USBFrameProvider.java

Create index.html

Update index.html

Null check stream to prevent spam

* Update main.yml

Co-authored-by: Chris Gerth <gerth2@users.noreply.github.com>
2022-11-13 15:51:02 -06:00
Matt
72d8f49145 Add orthogonalizeRotationMatrix (#587)
* Add orthogonalizeRotationMatrix

* Update docstring

Co-authored-by: Chris Gerth <gerth2@users.noreply.github.com>
2022-11-13 14:49:59 -06:00
Matt
df852410b0 Disable 3d if new resolution is uncalibrated (#591)
Closes #590

Co-authored-by: Chris Gerth <gerth2@users.noreply.github.com>
2022-11-13 14:32:49 -06:00
Matt
3c7165bb0d Force photonlib JSON to regenerate every build (#589) 2022-11-13 14:07:15 -06:00
Ethan Frank
f193a2331a Fix sim versionEntry NT table path (#569)
* Fix sim versionEntry NT table path

* Fix compile issue that mainTable is not accessible form SimPhotonCamera

* Fix format

Co-authored-by: Matt <matthew.morley.ca@gmail.com>
Co-authored-by: Chris Gerth <gerth2@users.noreply.github.com>
2022-11-13 13:18:55 -06:00
Matt
c7aa84ca41 Add tag16h5 support (#584)
* Fix target dropdown

* Add hamming and decision margin to UI, pipeline

* Run spotless

* Update index.html

* Update index.html

* Implement second apriltag size
2022-11-10 18:48:41 -08:00
Matt
209cdbf45f Re-license apriltag code (#585)
Relicense under wpilib BSD license
2022-11-09 23:22:47 -05:00
Chris Gerth
e03ec862a8 disallowed non-integer decimation (#573) 2022-11-09 22:01:58 -06:00
Chris Gerth
8169da5ad4 wip getting stream divisor to update properly (#574) 2022-11-09 23:01:11 -05:00
shueja-personal
916431b4ff JSONify the bundled 3D geometry (#578)
Co-authored-by: Matt M <matthew.morley.ca@gmail.com>
2022-11-09 14:00:46 -05:00
Noah
7dd1719fbd Expose NT entry change time in PhotonLib (#562)
Adds target change timestamp to PhotonPipelineResult

Co-authored-by: Matt <matthew.morley.ca@gmail.com>
2022-11-07 13:09:55 -05:00
Chris Gerth
b408a58e9e Sim Updates for 2023 (#512)
* WIP updating sim stuff for 2023 and pose3d's

* vision system build fixups, but test not yet passing.

* WIP Sim fixups and working on testcases

* Still doesn't work, but closer

* tests pass

* removed C++ sim support

* formatting update

* adjusted target height above ground per review

* Turns out its unused

* missed example removal
2022-11-03 15:05:37 -05:00
Chris Gerth
a64697e714 Added proper state machine to websocket video stream to control connect/disconnect sequence better. (#561) 2022-11-03 15:05:17 -05:00
Jack
e971db2f52 Fix pipeline setting update not being sent if only autoexposure fails (#565) 2022-11-03 13:12:21 -04:00
Matt
7b6afd545b Pull thinclient into built JAR (#558) 2022-10-31 16:18:02 -04:00
Matt
0f99044468 Update pi image generation zip/xz confusion (#555)
* Add prints to image generation

* Make xz multithreaded

* More rename copypasta
2022-10-31 11:27:57 -04:00
sarah-e-c
1412155c50 Replace jcenter with MavenCentral (#554) 2022-10-31 08:32:49 -04:00
Andrew Gasser
b1280e49d5 Ignore cameras with no supported VideoModes (#550) 2022-10-30 22:58:22 -04:00
Chris Gerth
aaac6a4fbb Add Websocket Camera Streaming (#529)
* WIP adding second websocket handling for cameras

* just more WIP

* even more wip. Most java-side framework completed, but not yet debugged

* IT LIVES. Still needs lots of cleanup. But we're transferring and displaying data!

* moved down an architecture layer. Improved multiple-camera handling

* Additional WIP to help improve smoothness and performance, though not yet tested

* bugfixes galore

* tweak compression

* spotless

* more tweaks for handling slow/intermittent streams

* wpilibformat maybe?

* clang-format maybe?

* WIP - adding thinclient. I don't like it yet, it should be more auto-generated than it is.

* thinclient formatting fixups

* Reduced amount of empty send data by limiting to only one stream per client (which is all we really need). Framerate is up slightly, overhead is down.

* bugfixes, faster streaming, better mjpeg compression settings, thinclient working

* spotless and formatting

* cmon wpiformat....

* re-added mjpg streams

* added a loading GIF to imporve the feeling of responsiveness

* formatting

* urlparams and built-in thinclient

* wpiformat

* prevent wpiformat complaints

* Removed uint8 array and base64 conversion from client side

* Synced up js implementations for ws streaming

* formatting/spotless
2022-10-30 13:16:17 -05:00
laviRZ
b68b0ca5f6 Rename artifact to jars (#534) 2022-10-30 14:14:14 -04:00
Chris Gerth
45d99f1f6b Added camera quirek to account for Facetime HD Cameras, and fix logging message (#551) 2022-10-30 14:13:55 -04:00
Jack
a42fef67f2 Fix Camera Calibration Frontend (#542)
* Fix Start Calibration button requiring a page refresh

* Fix camera resolution selection

* Fix camera resolution selection so it works with the default selection
2022-10-29 06:57:32 -04:00
Jack
bd4d74c192 Fix missing and incorrectly bound snackbar (#539)
* Fix missing and incorrectly bound snackbar

* Add 5 second timeout
2022-10-29 06:52:59 -04:00
Chris Gerth
c4500ce12b Added throttling reasons and cpu uptime (#507)
* Added throttling reasons and cpu uptime

* spotless

* adding tooltips for the acronyms used

* Added icon for suggesting folks should attempt a hover-over for tooltip

* wip making the implementaiton more platform independent

* spotless

* wpiformat

* wpilibformat pt 2
2022-10-29 06:50:51 -04:00
Jack
81d19672d2 Change order of drawing to better show axes (#541) 2022-10-28 17:54:57 -05:00
Andrew Gasser
04bde1b230 Update sim pose estimator example to use 3d (#524) 2022-10-25 21:11:41 -04:00
Avery Black
4f355f2749 Fix photon-build-action versioning (#535)
* Describe tags (Do Not Merge)

* Try fetch depth 0

* Remove fetch tags

* Remove describe action

* Apparently more is broken than I thought (oops)
2022-10-24 15:56:49 -04:00
Avery Black
5e604cf98d Remove 90 degree offset from UI (#533)
Removes offset originally added to offset broken backend code
2022-10-24 15:18:46 -04:00
Matt
2d7a88e231 Expose both pose solutions (#521)
* Half-add second pose

* add c++

* run wpiformat

* Fix c++
2022-10-22 06:42:45 -05:00
amquake
27198a3e32 Don't spam log on client connection retry (#530)
* dont spam log on connection retry

* Move print into ntTick

Update NetworkTablesManager.java

Co-authored-by: Matt <matthew.morley.ca@gmail.com>
2022-10-21 23:37:22 -04:00
Chris Gerth
fbf6fb304e Add Auto-Exposure Switch to Calibration Window (#526) 2022-10-21 22:12:11 -04:00
Avery Black
d24a8d4188 Ci update (#518)
Update action versions so that github actions stop complaining about Node and set/get-ouput commands.
2022-10-21 20:56:08 -04:00
Matt
def40484e3 Add delay to version check (#466)
Rate limits version check spam print

Co-authored-by: Chris Gerth <gerth2@users.noreply.github.com>
2022-10-21 20:53:28 -04:00
Chris Gerth
aff163fc6a Pull latest pi image and updates for .xz (previously .zip) (#506) 2022-10-21 20:50:45 -04:00
Chris Gerth
c392d5fa4d Exclude more broken cameras (#527)
* Adding new broken cameras

* Fixed up snapcamera enumeration to actually detect snapcamera
2022-10-21 19:39:30 -04:00
Chris Gerth
8dbd428359 Temporarily remove RIO finder from UI (#525) 2022-10-21 19:36:30 -04:00
Chris Gerth
ccd3a512d6 Add additional try/catch to prevent pigpio communication issues from crashing the main thread (#511) 2022-10-21 18:10:32 -04:00
Matt
bfc5e45cd0 Restart NT client every 5 seconds if not connected (#467)
Fun hack to get around photonvision not connecting if it boots before robot code starts

Co-authored-by: shueja-personal <32416547+shueja-personal@users.noreply.github.com>
2022-10-18 23:52:13 -04:00
Jack
a1b09100e0 Remove pitch camera configuration (#492)
* Remove pitch configuration from camera view

* Remove pitch config from backend; fix 'this' binding bug

* Stylistic choice to remove excessive whitespace br

* Spotless apply

* Spotless apply 2
2022-10-17 12:41:57 -04:00
Avery Black
2bf7a77885 Update aarch64 apriltag build from CI (#497) 2022-10-17 07:12:29 -04:00
Andrew Gasser
d1bfb86ab4 Correct image capture time (#501)
* Correct image capture time

`Timer.getFPGATimesptamp()` returns the current time in _seconds_, but `res.getLatencyMillis()` is in _miliseconds_.

* Correct image capture time (correctly)

* Change double literal to not use suffix

Co-authored-by: shueja-personal <32416547+shueja-personal@users.noreply.github.com>
2022-10-16 20:51:48 -07:00
Matt
07904589df Rotate all solvePNP-ed poses to be 180 about Z facing camera (#500)
* Rotate all solvePNP-ed poses to be 180 about Z facing camera

* Run spotless

* Fix test coordinate systems
2022-10-16 17:48:30 -07:00
Jack
5540bbf115 [UI] Fix camera gain slider Vue errors (#493) 2022-10-12 15:51:53 -04:00
320 changed files with 13032 additions and 7814 deletions

View File

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

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

View File

@@ -13,6 +13,7 @@ modifiableFileExclude {
\.jpg$
\.jpeg$
\.png$
\.gif$
\.so$
\.dll$
}

View 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.

View File

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

View File

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

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

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

View File

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

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

View File

@@ -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();
}

View File

@@ -26,7 +26,7 @@
</v-row>
</div>
</template>
s
<script>
import TooltippedLabel from "./cv-tooltipped-label";

View File

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

View File

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

View File

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

View File

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

View 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}

View File

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

View File

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

View File

@@ -33,10 +33,10 @@
:color="fpsTooLow ? 'error' : 'transparent'"
:text-color="fpsTooLow ? 'white' : 'grey'"
>
<span class="pr-1">{{ Math.round($store.state.pipelineResults.fps) }}&nbsp;FPS &ndash;</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) }}&nbsp;FPS &ndash;</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"

View File

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

View File

@@ -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);
}
},

View File

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

View File

@@ -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) }}&deg;</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>

View File

@@ -247,7 +247,7 @@ export default {
'cameraIndex': this.$store.state.currentCameraIndex
}
});
this.$socket.send(msg);
this.$store.state.websocket.send(msg);
this.$emit('update');
}
},

View File

@@ -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) }}&deg;&nbsp;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>

View File

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

View File

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

View File

@@ -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
+ "]";
}
}

View File

@@ -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);
}
}
}

View File

@@ -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 != "";
}
}

View File

@@ -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();
}
}

View File

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

View File

@@ -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);
}
}

View File

@@ -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));
}

View File

@@ -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...");
}
}
}

View File

@@ -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();
}
}

View File

@@ -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 "";
}
}

View File

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

View File

@@ -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());

View File

@@ -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);
}
}

View File

@@ -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 "";
}
}
}

View File

@@ -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 "";
}
}
}

View File

@@ -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();
}
}

View File

@@ -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);
}
}

View File

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

View File

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

View File

@@ -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";
}
}

View File

@@ -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]+'";
}
}

View File

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

View File

@@ -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());

View File

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

View File

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

View File

@@ -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));
}
}

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -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);
}
}

View File

@@ -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
+ '}';
}
}

View File

@@ -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));
}
}

View File

@@ -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
+ "]";
}
}

View File

@@ -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();
}
}

View File

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

View File

@@ -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();
}
}

View File

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

View File

@@ -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);
}
}
}

View File

@@ -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();
}
}

View File

@@ -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();
}
}

View File

@@ -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);
}

View File

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

View File

@@ -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();
}
}
}

View File

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

View File

@@ -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());
}
}

View File

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

View File

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

View File

@@ -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);
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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();

View File

@@ -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);
}
}

View File

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

View File

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

View File

@@ -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
+ '}';
}
}

View File

@@ -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() {

View File

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

View File

@@ -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);
}
}

View File

@@ -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));
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

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

View File

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

View File

@@ -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);
}
}

View File

@@ -148,9 +148,13 @@ public class VisionModuleChangeSubscriber extends DataChangeSubscriber {
curAdvSettings.offsetDualPointB = newPoint;
curAdvSettings.offsetDualPointBArea = latestTarget.getArea();
break;
default:
break;
}
}
break;
default:
break;
}
}
}

View File

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