Compare commits

...

50 Commits

Author SHA1 Message Date
Matt
a5cc0808c4 Make photon targeting respect snapshot repo (#309) 2021-11-06 20:12:50 -04:00
Banks T
08fafe2607 Fix file delete, possible null on perms set (#303) 2021-11-06 19:58:07 -04:00
Matt
a2af7d9273 Add red warning text with server mode, team number not set (#308) 2021-11-03 09:12:29 -04:00
Matt
a731c7a8db Revert part of #288 (#306)
Fixes picam streaming/hangs
2021-10-30 12:27:41 -04:00
Matt
7e74da5cff Make photonlib complain if versions don't match (#302)
Adds messages if Photon isn't detected or major versions don't match
2021-10-18 22:31:18 -04:00
Matt
0977fd0dff Update PacketTest.java (#301)
Adds unit test to make sure the packet structure doesn't change
2021-10-16 09:42:21 -04:00
Matt
3241ef7b1b Update dev tag matcher (#300) 2021-10-07 11:13:11 -04:00
Matt
f922466d41 Fix contour grouping (#298)
Fixes bug where n+1 contours were needed to match a target of size n
2021-10-05 12:16:50 -04:00
Matt
243f06da2d Fix vision module stream index logic (#295)
Previous logic could cause stream index with multiple cameras to run away
2021-10-03 22:43:39 -04:00
Matt
44e91a184d Publish photon-targeting (#292)
Pushes photon-targeting to Maven
2021-10-03 10:58:35 -04:00
Matt
e6b0f398b6 Fix versioning picked up in CI (#291)
Fixes ignore blob in versioningHelper
2021-09-24 18:51:40 -04:00
Chris Gerth
5a475e1071 Fix crash in logging when log files are not writable (#286)
* Addresses null pointer crash in logging when log files are not writable

* One of these days, I'll be able to type code without spotless complaining.

But today is not that day.
2021-09-23 22:38:50 -04:00
Tyler Veness
f8def88e4d Rename tests to appease wpiformat (#290) 2021-09-23 21:13:04 -04:00
Chris Gerth
db09e5209f Colored shape fix (#288)
* Move test images out of resources folder

* Limit workers in CI

* Fix image area filtering bug in colored shape

* Add missing picam settings

* Swap to make blank/empty Mat when a picam doesn't supply a color image.

Co-authored-by: Matt <matthew.morley.ca@gmail.com>
2021-09-23 18:48:18 -04:00
Matt
9fdd945a52 Fix compilation with gradlew build (#284)
* Fix everything but test mode

* Update TestUtils.java

* Jank testutils fix

* Limit workers in CI
2021-09-07 06:49:07 -07:00
Matt
00b8e7d1c5 Add colored shape to the UI (#258)
* Fixup colored shape backend code

* More colored shape stuff

* Start adding shape change to drawing

* Mostly works!

* Add powercell image for shape test mode

* Make super-duper-sure to release stuff

Fixes colored shape leak

* Move approx poly dp into Contour

* Adjust epsilon threshold

* Add dialog to change pipeline type

* Run spotless

* Make yes red :>

* Move "no" button

* Fix duplication/deletion name logic and switching

* Fix compilation errors from rebase

* Update VisionSourceManager.java

* Update type dialog, remove duplicate popup

The dropdown still switches even if the user says "no" tho

Co-authored-by: Banks Troutman <btrout.dhrs@gmail.com>
2021-09-03 22:20:55 -04:00
Tyler Veness
798b8e398a Remove hasTargets member variable and fix docs warnings (#283)
hasTargets is redundant because the same information can be obtained by
checking the size of the targets array.
2021-09-03 22:19:38 -04:00
Tyler Veness
affb27038b Fix uninitialized variables in PhotonPipelineResult (#282)
If the default constructor is used, some member variables weren't properly initialized.
2021-09-02 20:48:05 -07:00
Tyler Veness
6767781a41 Update photonlib vendordep JSON URL (#281) 2021-09-01 05:11:03 -07:00
Matt
6007cc752d Add libpicam with gain slider bugfix (#278)
* Add libpicam with gain slider bugfix

* Patches to get zero-copy working with Pi3.

-- Success/Failure mistmatch assumptions - lots of functions in the JNI return true on failure (not true on success)
-- isVSCMSupported() is currently implemented to be "isVSCMNotSupported()"

Likely, we'll want at least some .so changes

Co-authored-by: Chris Gerth <chrisgerth010592@gmail.com>
2021-08-31 23:48:11 -04:00
Banks T
9cf5c77d69 CI test fix (#280)
Addresses OOM in CI
2021-08-31 20:27:51 -07:00
Matt
9dc5481d1c Push dev tag last 2021-04-02 17:04:22 -04:00
Matt
3948650e6c [photonlib] Fix C++ compilation errors (#266)
Properly builds linux athena artifacts and fixes vendor JSON bug
2021-03-28 14:36:03 -07:00
Chris Gerth
49fcdb64ed Update Sim Example Unit Conversions (#265)
Updated Java examples to fix radians/degrees mismatch.

Inspected c++ examples - they create a units::degree_t from the NT value, which should be handled properly inside the PhotonUtils methods.
2021-03-23 10:57:32 -07:00
Banks T
1e715ce4bb Sim target sorting and "easy" NT entries (#262)
* Typo fixes, add target sorting

* Add easy NT entries

* spotttttttttttttttttlessssssssssssss

* Run wpiformat

* Fix return on no targets

* formatting hell 2 electric boogaloo

Co-authored-by: Matt <matthew.morley.ca@gmail.com>
2021-03-23 12:47:26 -04:00
Vasista Vovveti
f9fd7a0b45 Handle GetLatestResult segfault (#259)
* Handle GetLatestResult segfault

* Update PhotonCamera.cpp

* Update PhotonCamera.cpp
2021-03-21 16:29:29 -04:00
Matt
e9a3c2d1b8 Pull tags in photon CI (#263) 2021-03-15 16:13:45 -07:00
Banks T
129575dd23 Fix Test Mode (#246)
* Fix test mode path and args

* spoooootless

* Fix unit test resources
2021-03-07 21:39:02 -05:00
Declan Freeman-Gleason
ba8d2691fc Add more GPU accel support detection and more libpicam error checks (#257) 2021-03-02 17:49:29 -05:00
Matt
f3d3a59ca0 Use snapshot repo for dev releases (#241) 2021-02-23 15:07:43 -05:00
Matt
b653fc7db1 deprecate hasTargets (#252)
hasTargets has the potential to allow users to inadvertantly create NPEs.
2021-02-23 15:07:31 -05:00
MarkGhebrial
71ee03a531 Draw 2D contours on top (#254)
Closes #102
2021-02-23 15:07:03 -05:00
Banks T
4a2493ff2e Remove spamy CSCore prints in unit tests (#255) 2021-02-23 11:37:01 -08:00
Banks T
0b20111824 Add update script (#248) 2021-02-01 11:00:29 -08:00
Banks T
449977e63b Update NetworkTablesManager.java (#250)
Allows NT connection on Romi
2021-01-29 12:20:16 -08:00
Banks T
b0504cbef5 Add VisionModule order determinism (#245)
This makes sure that VisionModules always appear in the same order.
2021-01-29 10:57:18 -08:00
Matt
fccb395564 Fix CVMat weirdness in calibration unit test (#240)
* Remove failing assertion

Cannot reproduce locally.

* comment out other test

* Update Calibrate3dPipeTest.java
2021-01-29 07:08:09 -05:00
Chris Gerth
b510417541 Fixup support for log messages in the client web interface (#247) 2021-01-26 23:48:14 -05:00
Chris Gerth
0330467874 Add Basic Sim Example (#237)
* WIP adding sim pose example

* WIP making examples buildable like WPI. Not quite there yet....

* Make examples runnable

* remove lock

* add lock

* WIP Adding a simpler example for simulation

* Spotless Apply

* Added simulation-supporting aim and range example

* Spotless, revised hand usage to be consistent across examples, and propagated required -1.0's to non-sim examples.

Co-authored-by: Prateek Machiraju <prateek.machiraju@gmail.com>
2021-01-26 23:26:15 -05:00
Banks T
4c15a46cda Move CameraConfiguration to VisionSource (#244) 2021-01-22 22:10:20 -08:00
Matt
d59f8d1227 Cleanup photonlib deps (#243)
Removes unnecessary photonlib dependencies and removes commons-math.
2021-01-22 22:05:39 -08:00
Matt
bbc8a3137b Fix photon-lib spelling in CI (#239) 2021-01-19 05:48:15 -08:00
Prateek Machiraju
951c038f19 Clean up buildscripts (#238)
Cleans up gradle buildscripts.
2021-01-18 19:12:57 -08:00
Prateek Machiraju
1b0c5af86e Make examples runnable, start work on examples (#235)
Co-authored-by: Chris <chrisgerth010592@gmail.com>
Co-authored-by: Matt <matthew.morley.ca@gmail.com>
2021-01-18 10:56:04 -05:00
Banks T
a113bd4192 Fix processing timings in CVPipelineResult (#236) 2021-01-17 23:28:00 -05:00
Matt
d3c23345da Use wpi::Now for image capture timestamp (#232)
This uses the same time source as CSCore does for image captures, and will make latency measurements more accurate.

Co-authored-by: Banks T <btrout.dhrs@gmail.com>
2021-01-17 14:57:21 -08:00
Matt
2e1b3d0f83 Add Photonlib (#231)
Merges Photonlib into Photonvision, along with the Photonlib code examples. Also creates a new PhotonTargeting library teams can depend on.
2021-01-16 20:41:47 -08:00
Banks T
58b39f47aa Split photon-server and photon-core (#211)
Uses multiple Gradle projects to support the split.
2021-01-14 18:45:26 -08:00
Declan Freeman-Gleason
dc0f8cf296 Fetch tags before building server so that the version string is correct (#230) 2021-01-12 12:37:41 -08:00
Declan Freeman-Gleason
3d34d1ca40 Remove 1280x720 on the Pi Camera v1 for now (#228) 2021-01-11 00:35:04 -05:00
638 changed files with 9810 additions and 2548 deletions

View File

@@ -14,7 +14,7 @@ on:
jobs:
# This job builds the client (web view).
build-client:
photonclient-build:
# Let all steps run within the photon-client dir.
defaults:
@@ -49,12 +49,7 @@ jobs:
name: built-client
path: photon-client/dist/
build-server:
# Let all steps run within the photon-server dir.
defaults:
run:
working-directory: photon-server
photon-build-all:
# The type of runner that the job will run on.
runs-on: ubuntu-latest
@@ -62,6 +57,10 @@ jobs:
# Checkout code.
- name: Checkout code
uses: actions/checkout@v1
# Fetch tags.
- name: Fetch tags
run: git fetch --tags --force
# Install Java 11.
- name: Install Java 11
@@ -73,19 +72,28 @@ jobs:
- name: Gradle Build
run: |
chmod +x gradlew
./gradlew build -x check
./gradlew build -x check --max-workers 1
# Run Tests Generate Coverage Report.
- name: Gradle Test and Coverage
run: ./gradlew jacocoTestReport
# Run Gradle Tests.
- name: Gradle Tests
run: ./gradlew testHeadless -i --max-workers 1
# Generate Coverage Report.
- name: Gradle Coverage
run: ./gradlew jacocoTestReport --max-workers 1
# Publish Coverage Report.
- name: Publish Coverage Report
- name: Publish Server Coverage Report
uses: codecov/codecov-action@v1
with:
file: ./photon-server/build/reports/jacoco/test/jacocoTestReport.xml
build-offline-docs:
- name: Publish Core Coverage Report
uses: codecov/codecov-action@v1
with:
file: ./photon-core/build/reports/jacoco/test/jacocoTestReport.xml
photonserver-build-offline-docs:
runs-on: ubuntu-latest
steps:
@@ -121,75 +129,7 @@ jobs:
name: built-docs
path: build/html
build-package:
needs: [build-client, build-server, build-offline-docs]
# Let all steps run within the photon-server dir.
defaults:
run:
working-directory: photon-server
# The type of runner that the job will run on.
runs-on: ubuntu-latest
steps:
# Checkout code.
- uses: actions/checkout@v1
# Install Java 11.
- uses: actions/setup-java@v1
with:
java-version: 11
# Clear any existing web resources.
- run: |
rm -rf src/main/resources/web/*
mkdir -p src/main/resources/web/docs
# Download client artifact to resources folder.
- uses: actions/download-artifact@v2
with:
name: built-client
path: photon-server/src/main/resources/web/
# Download docs artifact to resources folder.
- uses: actions/download-artifact@v2
with:
name: built-docs
path: photon-server/src/main/resources/web/docs
# Print folder contents.
- run: ls
working-directory: photon-server/src/main/resources/web/
# Build fat jar.
- run: |
chmod +x gradlew
./gradlew shadowJar
working-directory: photon-server
# Upload final fat jar as artifact.
- uses: actions/upload-artifact@master
with:
name: jar
path: photon-server/build/libs
- uses: eine/tip@master
with:
token: ${{ secrets.GITHUB_TOKEN }}
tag: 'Dev'
rm: true
files: |
photon-server/build/libs/*.jar
if: github.event_name == 'push'
check-lint:
# Let all steps run within the photon-server dir.
defaults:
run:
working-directory: photon-server
photonserver-check-lint:
# The type of runner that the job will run on.
runs-on: ubuntu-latest
@@ -207,9 +147,10 @@ jobs:
chmod +x gradlew
./gradlew spotlessCheck
release:
photon-release:
if: startsWith(github.ref, 'refs/tags/v')
needs: [build-package]
needs: [photon-build-package]
runs-on: ubuntu-latest
steps:
- uses: actions/download-artifact@v2
@@ -220,3 +161,151 @@ jobs:
files: '**/*'
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# Building photonlib
photonlib-build-host:
strategy:
fail-fast: false
matrix:
include:
- os: windows-latest
artifact-name: Win64
- os: macos-latest
artifact-name: macOS
- os: ubuntu-latest
artifact-name: Linux
runs-on: ${{ matrix.os }}
name: "Photonlib - Build - ${{ matrix.artifact-name }}"
steps:
- uses: actions/checkout@v2.3.4
with:
fetch-depth: 0
- uses: actions/setup-java@v1
with:
java-version: 11
- run: git fetch --tags --force
- run: |
chmod +x gradlew
./gradlew photon-lib:build --max-workers 1
- run: ./gradlew photon-lib:publish photon-targeting:publish
name: Publish
env:
ARTIFACTORY_API_KEY: ${{ secrets.ARTIFACTORY_API_KEY }}
if: github.event_name == 'push'
photonlib-build-docker:
strategy:
fail-fast: false
matrix:
include:
- container: wpilib/roborio-cross-ubuntu:2021-18.04
artifact-name: Athena
- container: wpilib/raspbian-cross-ubuntu:10-18.04
artifact-name: Raspbian
- container: wpilib/aarch64-cross-ubuntu:bionic-18.04
artifact-name: Aarch64
runs-on: ubuntu-latest
container: ${{ matrix.container }}
name: "Photonlib - Build - ${{ matrix.artifact-name }}"
steps:
- uses: actions/checkout@v2.3.4
with:
fetch-depth: 0
- uses: actions/setup-java@v1
with:
java-version: 11
- run: |
chmod +x gradlew
./gradlew photon-lib:build --max-workers 1
- run: |
chmod +x gradlew
./gradlew photon-lib:publish
env:
ARTIFACTORY_API_KEY: ${{ secrets.ARTIFACTORY_API_KEY }}
if: github.event_name == 'push'
photonlib-wpiformat:
name: "wpiformat"
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- 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
with:
python-version: 3.8
- name: Install clang-format
run: sudo apt-get update -q && sudo apt-get install clang-format-10
- name: Install wpiformat
run: pip3 install wpiformat
- name: Run
run: wpiformat -clang 10 -f photon-lib
- name: Check Output
run: git --no-pager diff --exit-code HEAD
- name: Generate diff
run: git diff HEAD > wpiformat-fixes.patch
if: ${{ failure() }}
- uses: actions/upload-artifact@v2
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]
# The type of runner that the job will run on.
runs-on: ubuntu-latest
steps:
# Checkout code.
- uses: actions/checkout@v1
# Install Java 11.
- uses: actions/setup-java@v1
with:
java-version: 11
# Clear any existing web resources.
- run: |
rm -rf photon-server/src/main/resources/web/*
mkdir -p photon-server/src/main/resources/web/docs
# Download client artifact to resources folder.
- uses: actions/download-artifact@v2
with:
name: built-client
path: photon-server/src/main/resources/web/
# Download docs artifact to resources folder.
- uses: actions/download-artifact@v2
with:
name: built-docs
path: photon-server/src/main/resources/web/docs
# Build fat jar.
- run: |
chmod +x gradlew
./gradlew photon-server:shadowJar --max-workers 1
# Upload final fat jar as artifact.
- uses: actions/upload-artifact@master
with:
name: jar
path: photon-server/build/libs
- uses: eine/tip@master
with:
token: ${{ secrets.GITHUB_TOKEN }}
tag: 'Dev'
rm: true
files: |
photon-server/build/libs/*.jar
if: github.event_name == 'push'

27
.gitignore vendored
View File

@@ -104,15 +104,15 @@ fabric.properties
# Android studio 3.1+ serialized cache file
.idea/caches/build_file_checksums.ser
photon-server/.gradle
photon-server/target
photon-server/src/main/java/META-INF
photon-server/.settings
photon-server/.classpath
photon-server/.project
photon-server/settings
photon-server/dependency-reduced-pom.xml
# Temporary build files
**/.gradle
**/target
**/src/main/java/META-INF
**/.settings
**/.classpath
**/.project
**/settings
**/dependency-reduced-pom.xml
# photon-server/photon-vision.iml
New client/photon-client/*
@@ -126,3 +126,12 @@ photon-server/photon-vision
photon-server/src/main/resources/web
photon-server/src/main/java/org/photonvision/PhotonVersion.java
photon-server/src/main/generated/native/include/org_photonvision_raspi_PicamJNI.h
*.bin
.gradle
.gradle/*
photonvision_config
build/spotlessJava
build/*
build
photon-lib/src/main/java/org/photonvision/PhotonVersion.java
/photonlib-java-examples/bin/

46
build.gradle Normal file
View File

@@ -0,0 +1,46 @@
plugins {
id "com.diffplug.gradle.spotless" version "3.28.0"
id "com.github.johnrengelman.shadow" version "5.2.0"
id "com.github.node-gradle.node" version "2.2.4" apply false
id "edu.wpi.first.GradleJni" version "0.10.1"
id "edu.wpi.first.GradleVsCode" version "0.12.0"
id "edu.wpi.first.NativeUtils" version "2021.1.1" apply false
id "edu.wpi.first.wpilib.repositories.WPILibRepositoriesPlugin" version "2020.2"
id "org.hidetake.ssh" version "2.10.1"
}
allprojects {
repositories {
jcenter()
maven { url = "https://maven.photonvision.org/repository/internal/" }
}
wpilibRepositories.addAllReleaseRepositories(it)
wpilibRepositories.addAllDevelopmentRepositories(it)
}
// Configure the version number.
apply from: "versioningHelper.gradle"
ext {
wpilibVersion = "2021.3.1"
opencvVersion = "3.4.7-5"
joglVersion = "2.4.0-rc-20200307"
pubVersion = versionString
isDev = pubVersion.startsWith("dev")
}
spotless {
java {
googleJavaFormat()
paddedCell()
indentWithTabs(2)
indentWithSpaces(4)
removeUnusedImports()
}
java {
target "**/*.java"
licenseHeaderFile "$rootDir/LicenseHeader.txt"
targetExclude("photon-core/src/main/java/org/photonvision/PhotonVersion.java")
targetExclude("photon-lib/src/main/java/org/photonvision/PhotonVersion.java")
}
}

View File

@@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-6.0-bin.zip
distributionSha256Sum=5a3578b9f0bb162f5e08cf119f447dfb8fa950cedebb4d2a977e912a11a74b91
distributionUrl=https\://services.gradle.org/distributions/gradle-6.7.1-bin.zip
distributionSha256Sum=3239b5ed86c3838a37d983ac100573f64c1f3fd8e1eb6c89fa5f9529b5ec091d
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

View File

View File

@@ -213,6 +213,8 @@ import Logs from "./views/LogsView"
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)) {

View File

@@ -15,6 +15,7 @@
:value="localValue"
:max="max"
:min="min"
:disabled="disabled"
hide-details
class="align-center"
dark
@@ -34,7 +35,7 @@
hide-details
single-line
type="number"
style="width: 50px"
style="width: 60px"
:step="step"
@input="handleChange"
@focus="prependFocused = true"
@@ -53,7 +54,7 @@
hide-details
single-line
type="number"
style="width: 50px"
style="width: 60px"
:step="step"
@input="handleChange"
@focus="appendFocused = true"
@@ -75,7 +76,7 @@ export default {
TooltippedLabel,
},
// eslint-disable-next-line vue/require-prop-types
props: ["name", "min", "max", "value", "step", "tooltip"],
props: ["name", "min", "max", "value", "step", "tooltip", "disabled"],
data() {
return {
prependFocused: false,

View File

@@ -88,7 +88,11 @@
menu
</v-icon>
</template>
<v-list dense>
<v-list
dark
dense
color="primary"
>
<v-list-item @click="toPipelineNameChange">
<v-list-item-title>
<CVicon
@@ -119,7 +123,7 @@
/>
</v-list-item-title>
</v-list-item>
<v-list-item @click="openDuplicateDialog">
<v-list-item @click="duplicatePipeline">
<v-list-item-title>
<CVicon
color="#c5c5c5"
@@ -132,66 +136,51 @@
</v-list>
</v-menu>
</v-col>
<v-col
v-if="currentPipelineType >= 0"
cols="10"
md="5"
lg="10"
class="pt-0 pb-0 pl-6 ml-16"
>
<CVselect
v-model="currentPipelineType"
name="Type"
:list="['Reflective', 'Shape']"
@input="e => showTypeDialog(e)"
/>
</v-col>
</v-row>
<!--pipeline duplicate dialog-->
<v-dialog
v-model="duplicateDialog"
dark
width="500"
height="357"
>
<v-card dark>
<v-card-title
class="headline"
primary-title
>
Duplicate Pipeline
</v-card-title>
<v-card-text>
<CVselect
v-model="pipeIndexToDuplicate"
name="Pipeline"
:list="$store.getters.pipelineList"
/>
</v-card-text>
<v-divider />
<v-card-actions>
<v-spacer />
<v-btn
color="#ffd843"
@click="duplicatePipeline"
>
Duplicate
</v-btn>
<v-btn
color="error"
@click="closeDuplicateDialog"
>
Cancel
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
<!--pipeline naming dialog-->
<v-dialog
v-model="namingDialog"
dark
persistent
width="500"
height="357"
>
<v-card dark>
<v-card
dark
color="primary"
>
<v-card-title
class="headline"
primary-title
>
Pipeline Name
{{ isPipelineNameEdit ? "Edit Pipeline Name" : "Create Pipeline" }}
</v-card-title>
<v-card-text>
<CVinput
v-model="newPipelineName"
name="Pipeline"
:error-message="checkPipelineName"
@Enter="savePipelineNameChange"
/>
<CVselect
v-model="newPipelineType"
name="Pipeline Type"
:list="['Reflective', 'Shape']"
:disabled="isPipelineNameEdit"
/>
</v-card-text>
<v-divider />
@@ -213,161 +202,205 @@
</v-card-actions>
</v-card>
</v-dialog>
<v-dialog
v-model="showPipeTypeDialog"
width="600"
>
<v-card
color="primary"
dark
>
<v-card-title>Change Pipeline Type</v-card-title>
<v-card-text>
Changing the type of this pipeline will erase the current pipeline's settings and replace it with a new {{ ['Reflective', 'Shape'][proposedPipelineType] }} pipeline. <b class="red--text format_bold">You will lose all settings for the pipeline
"{{ ($store.getters.isDriverMode ? ['Driver Mode'] : []).concat($store.getters.pipelineList)[currentPipelineIndex] }}."</b> Are you sure you want to do this?
<v-row
class="mt-6"
style="display: flex; align-items: center; justify-content: center"
align="center"
>
<v-btn
class="mr-3"
color="red"
width="250"
@click="e => changePipeType(true)"
>
Yes, replace this pipeline
</v-btn>
<v-btn
class="ml-10"
color="secondary"
width="250"
@click="e => changePipeType(false)"
>
No, take me back
</v-btn>
</v-row>
</v-card-text>
</v-card>
</v-dialog>
</div>
</template>
<script>
import CVicon from '../common/cv-icon'
import CVselect from '../common/cv-select'
import CVinput from '../common/cv-input'
export default {
name: "CameraAndPipelineSelect",
components: {
CVicon,
CVselect,
CVinput
},
data: () => {
return {
re: RegExp("^[A-Za-z0-9 \\-)(]*[A-Za-z0-9][A-Za-z0-9 \\-)(.]*$"),
isCameraNameEdit: false,
newCameraName: "",
cameraNameError: "",
isPipelineNameEdit: false,
namingDialog: false,
newPipelineName: "",
duplicateDialog: false,
pipeIndexToDuplicate: undefined
}
},
computed: {
checkCameraName() {
if (this.newCameraName !== this.$store.getters.cameraList[this.currentCameraIndex]) {
if (this.re.test(this.newCameraName)) {
for (let cam in this.cameraList) {
if (this.cameraList.hasOwnProperty(cam)) {
if (this.newCameraName === this.cameraList[cam]) {
return "A camera by that name already Exists"
}
}
}
} else {
return "A camera name can only contain letters, numbers and spaces"
}
}
return "";
},
checkPipelineName() {
if (this.newPipelineName !== this.$store.getters.pipelineList[this.currentPipelineIndex - 1] || this.isPipelineNameEdit === false) {
if (this.re.test(this.newPipelineName)) {
for (let pipe in this.$store.getters.pipelineList) {
if (this.$store.getters.pipelineList.hasOwnProperty(pipe)) {
if (this.newPipelineName === this.$store.getters.pipelineList[pipe]) {
return "A pipeline with this name already exists"
}
}
}
} else {
return "A pipeline name can only contain letters, numbers, and spaces"
}
}
return ""
},
currentCameraIndex: {
get() {
return this.$store.getters.currentCameraIndex;
},
set(value) {
this.$store.commit('currentCameraIndex', value);
}
},
currentPipelineIndex: {
get() {
return this.$store.getters.currentPipelineIndex + (this.$store.getters.isDriverMode ? 1 : 0);
},
set(value) {
this.$store.commit('currentPipelineIndex', value - (this.$store.getters.isDriverMode ? 1 : 0));
}
}
},
methods: {
changeCameraName() {
this.newCameraName = this.$store.getters.cameraList[this.currentCameraIndex];
this.isCameraNameEdit = true;
},
saveCameraNameChange() {
if (this.checkCameraName === "") {
// this.handleInputWithIndex("changeCameraName", this.newCameraName);
this.axios.post('http://' + this.$address + '/api/setCameraNickname',
{name: this.newCameraName, cameraIndex: this.$store.getters.currentCameraIndex})
// eslint-disable-next-line
.then(r => {
this.$emit('camera-name-changed')
})
.catch(e => {
console.log("HTTP error while changing camera name " + e);
this.$emit('camera-name-changed')
})
this.discardCameraNameChange();
}
},
discardCameraNameChange() {
this.isCameraNameEdit = false;
this.newCameraName = "";
},
toPipelineNameChange() {
this.newPipelineName = this.$store.getters.pipelineList[this.currentPipelineIndex - 1];
this.isPipelineNameEdit = true;
this.namingDialog = true;
},
toCreatePipeline() {
this.newPipelineName = "New Pipeline";
this.isPipelineNameEdit = false;
this.namingDialog = true;
},
openDuplicateDialog() {
this.pipeIndexToDuplicate = this.currentPipelineIndex - 1;
this.duplicateDialog = true;
},
deleteCurrentPipeline() {
if (this.$store.getters.pipelineList.length > 1) {
this.handleInputWithIndex('deleteCurrentPipeline', {});
} else {
this.snackbar = true;
}
},
savePipelineNameChange() {
if (this.checkPipelineName === "") {
if (this.isPipelineNameEdit) {
this.handleInputWithIndex("changePipelineName", this.newPipelineName);
} else {
this.handleInputWithIndex("addNewPipeline", this.newPipelineName);
}
this.discardPipelineNameChange();
}
},
duplicatePipeline() {
// if (!this.anotherCamera) {
// this.pipelineDuplicate.camera = -1
// }
this.handleInputWithIndex("duplicatePipeline", this.pipeIndexToDuplicate);
// this.axios.post("http://" + this.$address + "/api/vision/duplicate", this.pipeIndexToDuplicate);
this.closeDuplicateDialog();
},
closeDuplicateDialog() {
this.duplicateDialog = false;
this.pipeIndexToDuplicate = undefined;
},
discardPipelineNameChange() {
this.namingDialog = false;
this.isPipelineNameEdit = false;
this.newPipelineName = "";
},
}
import CVicon from '../common/cv-icon'
import CVselect from '../common/cv-select'
import CVinput from '../common/cv-input'
export default {
name: "CameraAndPipelineSelect",
components: {
CVicon,
CVselect,
CVinput
},
data: () => {
return {
re: RegExp("^[A-Za-z0-9 \\-)(]*[A-Za-z0-9][A-Za-z0-9 \\-)(.]*$"),
isCameraNameEdit: false,
newCameraName: "",
cameraNameError: "",
isPipelineNameEdit: false,
namingDialog: false,
newPipelineName: "",
newPipelineType: 0,
duplicateDialog: false,
showPipeTypeDialog: false,
proposedPipelineType : 0,
pipeIndexToDuplicate: undefined
}
},
computed: {
checkCameraName() {
if (this.newCameraName !== this.$store.getters.cameraList[this.currentCameraIndex]) {
if (this.re.test(this.newCameraName)) {
for (let cam in this.cameraList) {
if (this.cameraList.hasOwnProperty(cam)) {
if (this.newCameraName === this.cameraList[cam]) {
return "A camera by that name already Exists"
}
}
}
} else {
return "A camera name can only contain letters, numbers and spaces"
}
}
return "";
},
checkPipelineName() {
if (this.newPipelineName !== this.$store.getters.pipelineList[this.currentPipelineIndex - 1] || this.isPipelineNameEdit === false) {
if (this.re.test(this.newPipelineName)) {
for (let pipe in this.$store.getters.pipelineList) {
if (this.$store.getters.pipelineList.hasOwnProperty(pipe)) {
if (this.newPipelineName === this.$store.getters.pipelineList[pipe]) {
return "A pipeline with this name already exists"
}
}
}
} else {
return "A pipeline name can only contain letters, numbers, and spaces"
}
}
return ""
},
currentCameraIndex: {
get() {
return this.$store.getters.currentCameraIndex;
},
set(value) {
this.$store.commit('currentCameraIndex', value);
}
},
currentPipelineIndex: {
get() {
return this.$store.getters.currentPipelineIndex + (this.$store.getters.isDriverMode ? 1 : 0);
},
set(value) {
this.$store.commit('currentPipelineIndex', value - (this.$store.getters.isDriverMode ? 1 : 0));
}
},
currentPipelineType: {
get() {
return this.$store.getters.currentPipelineSettings.pipelineType - 2;
},
set(value) {
value; // nop, since we have the dialog for this
}
}
},
methods: {
showTypeDialog(idx) {
// Only show the dialog if it's a new type
this.showPipeTypeDialog = idx !== this.currentPipelineType;
this.proposedPipelineType = idx;
},
changePipeType(actuallyChange) {
const newIdx = actuallyChange ? this.proposedPipelineType : this.currentPipelineType
this.handleInputWithIndex('pipelineType', newIdx);
this.showPipeTypeDialog = false;
},
changeCameraName() {
this.newCameraName = this.$store.getters.cameraList[this.currentCameraIndex];
this.isCameraNameEdit = true;
},
saveCameraNameChange() {
if (this.checkCameraName === "") {
// this.handleInputWithIndex("changeCameraName", this.newCameraName);
this.axios.post('http://' + this.$address + '/api/setCameraNickname',
{name: this.newCameraName, cameraIndex: this.$store.getters.currentCameraIndex})
// eslint-disable-next-line
.then(r => {
this.$emit('camera-name-changed')
})
.catch(e => {
console.log("HTTP error while changing camera name " + e);
this.$emit('camera-name-changed')
})
this.discardCameraNameChange();
}
},
discardCameraNameChange() {
this.isCameraNameEdit = false;
this.newCameraName = "";
},
toPipelineNameChange() {
this.newPipelineName = this.$store.getters.pipelineList[this.currentPipelineIndex - 1];
this.isPipelineNameEdit = true;
this.namingDialog = true;
},
toCreatePipeline() {
this.newPipelineName = "New Pipeline";
this.isPipelineNameEdit = false;
this.namingDialog = true;
},
deleteCurrentPipeline() {
if (this.$store.getters.pipelineList.length > 1) {
this.handleInputWithIndex('deleteCurrentPipeline', {});
} else {
this.snackbar = true;
}
},
savePipelineNameChange() {
if (this.checkPipelineName === "") {
if (this.isPipelineNameEdit) {
this.handleInputWithIndex("changePipelineName", this.newPipelineName);
} else {
this.handleInputWithIndex("addNewPipeline", [this.newPipelineName, this.newPipelineType]); // 0 for reflective, 1 for colored shpae
}
this.discardPipelineNameChange();
}
},
duplicatePipeline() {
this.handleInputWithIndex("duplicatePipeline", this.currentPipelineIndex);
},
discardPipelineNameChange() {
this.namingDialog = false;
this.isPipelineNameEdit = false;
this.newPipelineName = "";
},
}
}
</script>
<style scoped>

View File

@@ -29,9 +29,11 @@ Vue.use(VueAxios, axios);
Vue.prototype.$msgPack = msgPack(true);
import {dataHandleMixin} from './mixins/global/dataHandleMixin'
Vue.mixin(dataHandleMixin);
import {stateMixin} from './mixins/global/stateMixin'
Vue.mixin(stateMixin);
new Vue({
router,
store,

View File

@@ -0,0 +1,10 @@
export const stateMixin = {
methods: {
currentPipelineType() {
return this.$store.getters.pipelineType
},
currentPipelineSettings() {
return this.$store.getters.currentPipelineSettings
},
}
};

View File

@@ -42,7 +42,7 @@ export default new Vuex.Store({
isFovConfigurable: true,
calibrated: false,
currentPipelineSettings: {
pipelineType: 2, // One of "driver", "reflective", "shape"
pipelineType: 2, // One of "calib", "driver", "reflective", "shape"
// 2 is reflective
// Settings that apply to all pipeline types
@@ -245,5 +245,6 @@ export default new Vuex.Store({
},
pipelineList: state => state.cameraSettings[state.currentCameraIndex].pipelineNicknames,
calibrationList: state => state.cameraSettings[state.currentCameraIndex].calibrations,
pipelineType: state => state.cameraSettings[state.currentCameraIndex].currentPipelineSettings.pipelineType
}
})

View File

@@ -34,7 +34,7 @@
:text-color="fpsTooLow ? 'white' : 'grey'"
>
<span class="pr-1">{{ Math.round($store.state.pipelineResults.fps) }}&nbsp;FPS &ndash;</span>
<span v-if="!fpsTooLow">{{ Math.round($store.state.pipelineResults.latency) }} ms latency</span>
<span v-if="!fpsTooLow">{{ Math.min(Math.round($store.state.pipelineResults.latency), 100) }} 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 color stream for better performance</span>
</v-chip>

View File

@@ -1,160 +1,286 @@
<template>
<div>
<CVrangeSlider
v-model="contourArea"
name="Area"
min="0"
max="100"
step="0.1"
@input="handlePipelineData('contourArea')"
@rollback="e=> rollback('contourArea',e)"
v-model="contourArea"
name="Area"
min="0"
max="100"
step="0.1"
@input="handlePipelineData('contourArea')"
/>
<CVrangeSlider
v-model="contourRatio"
name="Ratio (W/H)"
tooltip="Min and max ratio between the width and height of a contour's bounding rectangle"
min="0"
max="100"
step="0.1"
@input="handlePipelineData('contourRatio')"
@rollback="e=> rollback('contourRatio',e)"
v-model="contourRatio"
v-if="currentPipelineType() !== 3"
name="Ratio (W/H)"
tooltip="Min and max ratio between the width and height of a contour's bounding rectangle"
min="0"
max="100"
step="0.1"
@input="handlePipelineData('contourRatio')"
/>
<CVrangeSlider
v-model="contourFullness"
name="Fullness"
tooltip="Min and max ratio between a contour's area and its bounding rectangle"
min="0"
max="100"
@input="handlePipelineData('contourFullness')"
@rollback="e=> rollback('contourFullness',e)"
v-model="contourFullness"
v-if="currentPipelineType() !== 3"
name="Fullness"
tooltip="Min and max ratio between a contour's area and its bounding rectangle"
min="0"
max="100"
@input="handlePipelineData('contourFullness')"
/>
<CVrangeSlider
v-model="contourPerimeter"
v-if="currentPipelineType() === 3"
name="Perimeter"
tooltip="Min and max perimeter of the shape, in pixels"
min="0"
max="4000"
@input="handlePipelineData('contourPerimeter')"
/>
<CVslider
v-model="contourSpecklePercentage"
name="Speckle Rejection"
tooltip="Rejects contours whose average area is less than the given percentage of the average area of all the other contours"
min="0"
max="100"
:slider-cols="largeBox"
@input="handlePipelineData('contourSpecklePercentage')"
@rollback="e=> rollback('contourSpecklePercentage',e)"
v-model="contourSpecklePercentage"
name="Speckle Rejection"
tooltip="Rejects contours whose average area is less than the given percentage of the average area of all the other contours"
min="0"
max="100"
:slider-cols="largeBox"
@input="handlePipelineData('contourSpecklePercentage')"
/>
<template v-if="currentPipelineType() !== 3">
<CVselect
v-model="contourGroupingMode"
name="Target Grouping"
tooltip="Whether or not every two targets are paired with each other (good for e.g. 2019 targets)"
:select-cols="largeBox"
:list="['Single','Dual']"
@input="handlePipelineData('contourGroupingMode')"
/>
<CVselect
v-model="contourIntersection"
name="Target Intersection"
tooltip="If target grouping is in dual mode it will use this dropdown to decide how targets are grouped with adjacent targets"
:select-cols="largeBox"
:list="['None','Up','Down','Left','Right']"
:disabled="contourGroupingMode === 0"
@input="handlePipelineData('contourIntersection')"
/>
</template>
<!-- If we arent not a shape, we are a shape-->
<template v-else>
<v-divider class="mt-3"/>
<CVselect
v-model="contourShape"
name="Target Shape"
tooltip="The shape of targets to look for"
:select-cols="largeBox"
:list="['Circle', 'Polygon', 'Triangle', 'Quadrilateral']"
@input="handlePipelineData('contourShape')"
/>
<!-- Accuracy % is only for polygons-->
<CVslider
v-model="accuracyPercentage"
:disabled="currentPipelineSettings().contourShape < 1"
name="Shape Simplification"
tooltip="How much we should simply the input contour before checking how many sides it has"
min="0"
max="100"
:slider-cols="largeBox"
@input="handlePipelineData('accuracyPercentage')"
/>
<!-- Similarly, the threshold is only for circles -->
<CVslider
v-model="circleDetectThreshold"
:disabled="currentPipelineSettings().contourShape !== 0"
name="Circle match distance"
tooltip="How close the centroid of a contour must be to the center of a circle in order for them to be matched"
min="1"
max="100"
:slider-cols="largeBox"
@input="handlePipelineData('circleDetectThreshold')"
/>
<CVrangeSlider
v-model="contourRadius"
:disabled="currentPipelineSettings().contourShape !== 0"
name="Radius"
min="0"
max="100"
step="1"
@input="handlePipelineData('contourRadius')"
/>
<CVslider
v-model="maxCannyThresh"
:disabled="currentPipelineSettings().contourShape !== 0"
name="Max Canny Threshold"
min="1"
max="100"
:slider-cols="largeBox"
@input="handlePipelineData('maxCannyThresh')"
/>
<CVslider
v-model="circleAccuracy"
:disabled="currentPipelineSettings().contourShape !== 0"
name="Circle Accuracy"
min="0"
max="100"
:slider-cols="largeBox"
@input="handlePipelineData('circleAccuracy')"
/>
<v-divider class="mt-3"/>
</template>
<CVselect
v-model="contourGroupingMode"
name="Target Grouping"
tooltip="Whether or not every two targets are paired with each other (good for e.g. 2019 targets)"
:select-cols="largeBox"
:list="['Single','Dual']"
@input="handlePipelineData('contourGroupingMode')"
@rollback="e=> rollback('contourGroupingMode',e)"
/>
<CVselect
v-model="contourIntersection"
name="Target Intersection"
tooltip="If target grouping is in dual mode it will use this dropdown to decide how targets are grouped with adjacent targets"
:select-cols="largeBox"
:list="['None','Up','Down','Left','Right']"
:disabled="contourGroupingMode === 0"
@input="handlePipelineData('contourIntersection')"
@rollback="e=> rollback('contourIntersection',e)"
/>
<CVselect
v-model="contourSortMode"
name="Target Sort"
tooltip="Chooses the sorting mode used to determine the 'best' targets to provide to user code"
:select-cols="largeBox"
:list="['Largest','Smallest','Highest','Lowest','Rightmost','Leftmost','Centermost']"
@input="handlePipelineData('contourSortMode')"
@rollback="e => rollback('contourSortMode', e)"
v-model="contourSortMode"
name="Target Sort"
tooltip="Chooses the sorting mode used to determine the 'best' targets to provide to user code"
:select-cols="largeBox"
:list="['Largest','Smallest','Highest','Lowest','Rightmost','Leftmost','Centermost']"
@input="handlePipelineData('contourSortMode')"
@rollback="e => rollback('contourSortMode', e)"
/>
</div>
</template>
<script>
import CVrangeSlider from '../../components/common/cv-range-slider'
import CVselect from '../../components/common/cv-select'
import CVslider from '../../components/common/cv-slider'
import CVrangeSlider from '../../components/common/cv-range-slider'
import CVselect from '../../components/common/cv-select'
import CVslider from '../../components/common/cv-slider'
export default {
name: 'Contours',
components: {
CVrangeSlider,
CVselect,
CVslider
},
// eslint-disable-next-line vue/require-prop-types
props: ['value'],
export default {
name: 'Contours',
components: {
CVrangeSlider,
CVselect,
CVslider
},
// eslint-disable-next-line vue/require-prop-types
props: ['value'],
data() {
return {}
},
computed: {
largeBox: {
get() {
// Sliders and selectors should be fuller width if we're on screen size medium and
// up and either not in compact mode (because the tab will be 100% screen width),
// or in driver mode (where the card will also be 100% screen width).
return this.$vuetify.breakpoint.mdAndUp && (!this.$store.state.compactMode || this.$store.getters.isDriverMode) ? 10 : 8;
}
},
contourArea: {
get() {
return this.$store.getters.currentPipelineSettings.contourArea
},
set(val) {
this.$store.commit("mutatePipeline", {"contourArea": val});
}
},
contourRatio: {
get() {
return this.$store.getters.currentPipelineSettings.contourRatio
},
set(val) {
this.$store.commit("mutatePipeline", {"contourRatio": val});
}
},
contourFullness: {
get() {
return this.$store.getters.currentPipelineSettings.contourFullness
},
set(val) {
this.$store.commit("mutatePipeline", {"contourFullness": val});
}
},
contourSpecklePercentage: {
get() {
return this.$store.getters.currentPipelineSettings.contourSpecklePercentage
},
set(val) {
this.$store.commit("mutatePipeline", {"contourSpecklePercentage": val});
}
},
contourGroupingMode: {
get() {
return this.$store.getters.currentPipelineSettings.contourGroupingMode
},
set(val) {
this.$store.commit("mutatePipeline", {"contourGroupingMode": val});
}
},
contourSortMode: {
get() {
return this.$store.getters.currentPipelineSettings.contourSortMode
},
set(val) {
this.$store.commit("mutatePipeline", {"contourSortMode": val});
}
},
contourIntersection: {
get() {
return this.$store.getters.currentPipelineSettings.contourIntersection
},
set(val) {
this.$store.commit("mutatePipeline", {"contourIntersection": val});
}
}
},
methods: {},
}
data() {
return {}
},
computed: {
largeBox: {
get() {
// Sliders and selectors should be fuller width if we're on screen size medium and
// up and either not in compact mode (because the tab will be 100% screen width),
// or in driver mode (where the card will also be 100% screen width).
return this.$vuetify.breakpoint.mdAndUp && (!this.$store.state.compactMode || this.$store.getters.isDriverMode) ? 10 : 8;
}
},
contourArea: {
get() {
return this.$store.getters.currentPipelineSettings.contourArea
},
set(val) {
this.$store.commit("mutatePipeline", {"contourArea": val});
}
},
contourRatio: {
get() {
return this.$store.getters.currentPipelineSettings.contourRatio
},
set(val) {
this.$store.commit("mutatePipeline", {"contourRatio": val});
}
},
contourFullness: {
get() {
return this.$store.getters.currentPipelineSettings.contourFullness
},
set(val) {
this.$store.commit("mutatePipeline", {"contourFullness": val});
}
},
contourPerimeter: {
get() {
return this.$store.getters.currentPipelineSettings.contourPerimeter
},
set(val) {
this.$store.commit("mutatePipeline", {"contourPerimeter": val});
}
},
contourSpecklePercentage: {
get() {
return this.$store.getters.currentPipelineSettings.contourSpecklePercentage
},
set(val) {
this.$store.commit("mutatePipeline", {"contourSpecklePercentage": val});
}
},
contourGroupingMode: {
get() {
return this.$store.getters.currentPipelineSettings.contourGroupingMode
},
set(val) {
this.$store.commit("mutatePipeline", {"contourGroupingMode": val});
}
},
contourSortMode: {
get() {
return this.$store.getters.currentPipelineSettings.contourSortMode
},
set(val) {
this.$store.commit("mutatePipeline", {"contourSortMode": val});
}
},
contourShape: {
get() {
return this.$store.getters.currentPipelineSettings.contourShape
},
set(val) {
this.$store.commit("mutatePipeline", {"contourShape": val});
}
},
contourIntersection: {
get() {
return this.$store.getters.currentPipelineSettings.contourIntersection
},
set(val) {
this.$store.commit("mutatePipeline", {"contourIntersection": val});
}
},
accuracyPercentage: {
get() {
return this.$store.getters.currentPipelineSettings.accuracyPercentage
},
set(val) {
this.$store.commit("mutatePipeline", {"accuracyPercentage": val});
}
},
contourRadius: {
get() {
return this.$store.getters.currentPipelineSettings.contourRadius
},
set(val) {
this.$store.commit("mutatePipeline", {"contourRadius": val});
}
},
circleDetectThreshold: {
get() {
return this.$store.getters.currentPipelineSettings.circleDetectThreshold
},
set(val) {
this.$store.commit("mutatePipeline", {"circleDetectThreshold": val});
}
},
maxCannyThresh: {
get() {
return this.$store.getters.currentPipelineSettings.maxCannyThresh
},
set(val) {
this.$store.commit("mutatePipeline", {"maxCannyThresh": val});
}
},
circleAccuracy: {
get() {
return this.$store.getters.currentPipelineSettings.circleAccuracy
},
set(val) {
this.$store.commit("mutatePipeline", {"circleAccuracy": val});
}
},
},
methods: {},
}
</script>
<style lang="" scoped>

View File

@@ -1,48 +1,66 @@
<template>
<div>
<CVrangeSlider
v-model="hsvHue"
name="Hue"
tooltip="Describes color"
:min="0"
:max="180"
@input="handlePipelineData('hsvHue')"
@rollback="e => rollback('hue',e)"
v-model="hsvHue"
name="Hue"
tooltip="Describes color"
:min="0"
:max="180"
@input="handlePipelineData('hsvHue')"
@rollback="e => rollback('hue',e)"
/>
<CVrangeSlider
v-model="hsvSaturation"
name="Saturation"
tooltip="Describes colorfulness; the smaller this value the 'whiter' the color becomes"
:min="0"
:max="255"
@input="handlePipelineData('hsvSaturation')"
@rollback="e => rollback('saturation',e)"
v-model="hsvSaturation"
name="Saturation"
tooltip="Describes colorfulness; the smaller this value the 'whiter' the color becomes"
:min="0"
:max="255"
@input="handlePipelineData('hsvSaturation')"
@rollback="e => rollback('saturation',e)"
/>
<CVrangeSlider
v-model="hsvValue"
name="Value"
tooltip="Describes lightness; the smaller this value the 'blacker' the color becomes"
:min="0"
:max="255"
@input="handlePipelineData('hsvValue')"
@rollback="e => rollback('value',e)"
v-model="hsvValue"
name="Value"
tooltip="Describes lightness; the smaller this value the 'blacker' the color becomes"
:min="0"
:max="255"
@input="handlePipelineData('hsvValue')"
@rollback="e => rollback('value',e)"
/>
<template v-if="this.currentPipelineType() === 3">
<CVSwitch
v-model="erode"
name="Erode"
tooltip="Removes pixels around the edges of white areas in the thresholded image"
@input="handlePipelineData('erode')"
@rollback="e => rollback('erode',e)"
/>
<CVSwitch
v-model="dilate"
class="mb-0"
name="Dilate"
tooltip="Adds pixels around the edges of white areas in the thresholded image"
@input="handlePipelineData('dilate')"
@rollback="e => rollback('dilate',e)"
/>
</template>
<v-divider class="mb-3 mt-3"/>
<div class="pt-3 white--text">
Color Picker
</div>
<v-divider
class="mt-3"
class="mt-3"
/>
<v-row
justify="center"
class="mt-3 mb-3"
justify="center"
class="mt-3 mb-3"
>
<template v-if="!$store.state.colorPicking">
<v-btn
color="accent"
class="ma-2 black--text"
small
@click="setFunction(3)"
color="accent"
class="ma-2 black--text"
small
@click="setFunction(3)"
>
<v-icon left>
mdi-minus
@@ -50,10 +68,10 @@
Shrink Range
</v-btn>
<v-btn
color="accent"
class="ma-2 black--text"
small
@click="setFunction(1)"
color="accent"
class="ma-2 black--text"
small
@click="setFunction(1)"
>
<v-icon left>
mdi-plus-minus
@@ -61,10 +79,10 @@
Set To Average
</v-btn>
<v-btn
color="accent"
class="ma-2 black--text"
small
@click="setFunction(2)"
color="accent"
class="ma-2 black--text"
small
@click="setFunction(2)"
>
<v-icon left>
mdi-plus
@@ -74,11 +92,11 @@
</template>
<template v-else>
<v-btn
color="accent"
class="ma-2 black--text"
style="width: 30%;"
small
@click="setFunction(0)"
color="accent"
class="ma-2 black--text"
style="width: 30%;"
small
@click="setFunction(0)"
>
Cancel
</v-btn>
@@ -88,112 +106,130 @@
</template>
<script>
import CVrangeSlider from '../../components/common/cv-range-slider'
export default {
name: 'Threshold',
components: {
CVrangeSlider
},
// eslint-disable-next-line vue/require-prop-types
props: ['value'],
data() {
return {
currentFunction: undefined,
colorPicker: undefined,
showThresholdState: 0
}
},
computed: {
hsvHue: {
get() {
return this.$store.getters.currentPipelineSettings.hsvHue
},
set(val) {
this.$store.commit("mutatePipeline", {"hsvHue": val})
}
},
hsvSaturation: {
get() {
return this.$store.getters.currentPipelineSettings.hsvSaturation
},
set(val) {
this.$store.commit("mutatePipeline", {"hsvSaturation": val})
}
},
hsvValue: {
get() {
return this.$store.getters.currentPipelineSettings.hsvValue
},
set(val) {
this.$store.commit("mutatePipeline", {"hsvValue": val})
}
}
},
mounted: function () {
const self = this;
this.colorPicker = require('../../plugins/ColorPicker').default;
this.$nextTick(() => {
self.colorPicker.initColorPicker();
});
},
methods: {
onClick(event) {
if (this.currentFunction !== undefined) {
this.colorPicker.initColorPicker();
import CVrangeSlider from '../../components/common/cv-range-slider'
import CVSwitch from "@/components/common/cv-switch";
let s = this.$store.getters.currentPipelineSettings;
let hsvArray = this.colorPicker.colorPickerClick(event, this.currentFunction,
[
[s.hsvHue[0], s.hsvSaturation[0], s.hsvValue[0]],
[s.hsvHue[1], s.hsvSaturation[1], s.hsvValue[1]]
].map(hsv => hsv.map(it => it || 0)));
// That `map` calls are to make sure that we don't let any undefined/null values slip in
this.currentFunction = undefined;
this.$store.state.colorPicking = false;
this.handlePipelineUpdate("outputShouldDraw", true);
s.hsvHue = [hsvArray[0][0], hsvArray[1][0]];
s.hsvSaturation = [hsvArray[0][1], hsvArray[1][1]];
s.hsvValue = [hsvArray[0][2], hsvArray[1][2]];
let msg = this.$msgPack.encode({
"changePipelineSetting": {
'hsvHue': s.hsvHue,
'hsvSaturation': s.hsvSaturation,
'hsvValue': s.hsvValue,
'cameraIndex': this.$store.state.currentCameraIndex
}
});
this.$socket.send(msg);
this.$emit('update');
}
},
setFunction(index) {
switch (index) {
case 0:
this.currentFunction = undefined;
this.$store.state.colorPicking = false;
this.handlePipelineUpdate("outputShouldDraw", true);
return;
case 1:
this.currentFunction = this.colorPicker.eyeDrop;
break;
case 2:
this.currentFunction = this.colorPicker.expand;
break;
case 3:
this.currentFunction = this.colorPicker.shrink;
break;
}
this.$store.state.colorPicking = true;
this.handlePipelineUpdate("outputShouldDraw", false);
this.$store.commit("mutatePipeline", {"inputShouldShow": true});
this.handlePipelineUpdate("inputShouldShow", true);
}
}
export default {
name: 'Threshold',
components: {
CVSwitch,
CVrangeSlider
},
// eslint-disable-next-line vue/require-prop-types
props: ['value'],
data() {
return {
currentFunction: undefined,
colorPicker: undefined,
showThresholdState: 0
}
},
computed: {
hsvHue: {
get() {
return this.$store.getters.currentPipelineSettings.hsvHue
},
set(val) {
this.$store.commit("mutatePipeline", {"hsvHue": val})
}
},
hsvSaturation: {
get() {
return this.$store.getters.currentPipelineSettings.hsvSaturation
},
set(val) {
this.$store.commit("mutatePipeline", {"hsvSaturation": val})
}
},
hsvValue: {
get() {
return this.$store.getters.currentPipelineSettings.hsvValue
},
set(val) {
this.$store.commit("mutatePipeline", {"hsvValue": val})
}
},
erode: {
get() {
return this.$store.getters.currentPipelineSettings.erode
},
set(val) {
this.$store.commit("mutatePipeline", {"erode": val});
}
},
dilate: {
get() {
return this.$store.getters.currentPipelineSettings.dilate
},
set(val) {
this.$store.commit("mutatePipeline", {"dilate": val});
}
},
},
mounted: function () {
const self = this;
this.colorPicker = require('../../plugins/ColorPicker').default;
this.$nextTick(() => {
self.colorPicker.initColorPicker();
});
},
methods: {
onClick(event) {
if (this.currentFunction !== undefined) {
this.colorPicker.initColorPicker();
let s = this.$store.getters.currentPipelineSettings;
let hsvArray = this.colorPicker.colorPickerClick(event, this.currentFunction,
[
[s.hsvHue[0], s.hsvSaturation[0], s.hsvValue[0]],
[s.hsvHue[1], s.hsvSaturation[1], s.hsvValue[1]]
].map(hsv => hsv.map(it => it || 0)));
// That `map` calls are to make sure that we don't let any undefined/null values slip in
this.currentFunction = undefined;
this.$store.state.colorPicking = false;
this.handlePipelineUpdate("outputShouldDraw", true);
s.hsvHue = [hsvArray[0][0], hsvArray[1][0]];
s.hsvSaturation = [hsvArray[0][1], hsvArray[1][1]];
s.hsvValue = [hsvArray[0][2], hsvArray[1][2]];
let msg = this.$msgPack.encode({
"changePipelineSetting": {
'hsvHue': s.hsvHue,
'hsvSaturation': s.hsvSaturation,
'hsvValue': s.hsvValue,
'cameraIndex': this.$store.state.currentCameraIndex
}
});
this.$socket.send(msg);
this.$emit('update');
}
},
setFunction(index) {
switch (index) {
case 0:
this.currentFunction = undefined;
this.$store.state.colorPicking = false;
this.handlePipelineUpdate("outputShouldDraw", true);
return;
case 1:
this.currentFunction = this.colorPicker.eyeDrop;
break;
case 2:
this.currentFunction = this.colorPicker.expand;
break;
case 3:
this.currentFunction = this.colorPicker.shrink;
break;
}
this.$store.state.colorPicking = true;
this.handlePipelineUpdate("outputShouldDraw", false);
this.$store.commit("mutatePipeline", {"inputShouldShow": true});
this.handlePipelineUpdate("inputShouldShow", true);
}
}
}
</script>

View File

@@ -4,11 +4,6 @@
ref="form"
v-model="valid"
>
<CVSwitch
v-model="runNTServer"
name="Run NetworkTables Server"
tooltip="If enabled, this device will create a NT server. This is useful for home debugging, but should be disabled on-robot."
/>
<CVnumberinput
v-model="teamNumber"
:disabled="settings.runNTServer"
@@ -16,6 +11,7 @@
:rules="[v => (v > 0) || 'Team number must be greater than zero', v => (v < 10000) || 'Team number must have fewer than five digits']"
class="mb-4"
/>
<span v-if="parseInt(teamNumber) < 1 && !runNTServer" class="red font-weight-bold">Team number not set! NetworkTables cannot connect.</span>
<CVradio
v-model="connectionType"
:list="['DHCP','Static']"
@@ -35,9 +31,18 @@
:rules="[v => isHostname(v) || 'Invalid hostname']"
name="Hostname"
/>
Advanced
<v-divider/>
<CVSwitch
v-model="runNTServer"
name="Run NetworkTables Server (Debugging Only!)"
tooltip="If enabled, this device will create a NT server. This is useful for home debugging, but should be disabled on-robot."
/>
<span v-if="runNTServer" class="red font-weight-bold">Disable this switch if you're on a robot! Photonlib will NOT work.</span>
</v-form>
<v-btn
color="accent"
:class="runNTServer ? 'mt-3' : ''"
style="color: black; width: 100%;"
:disabled="!valid && !runNTServer"
@click="sendGeneralSettings()"

13
photon-core/.gitignore vendored Normal file
View File

@@ -0,0 +1,13 @@
bin/*
.settings/*
.project
.classpath
*.prefs
.gradle
.gradle/*
build
build/*
photonvision/*
photonvision_config/*
src/main/java/org/photonvision/PhotonVersion.java

36
photon-core/build.gradle Normal file
View File

@@ -0,0 +1,36 @@
import java.nio.file.Path
apply from: "${rootDir}/shared/common.gradle"
dependencies {
implementation project(':photon-targeting')
implementation 'io.javalin:javalin:3.7.0'
implementation 'org.msgpack:msgpack-core:0.8.20'
implementation 'org.msgpack:jackson-dataformat-msgpack:0.8.20'
// wpiutil
compile "edu.wpi.first.wpiutil:wpiutil-jni:$wpilibVersion:linuxaarch64bionic"
compile "edu.wpi.first.wpiutil:wpiutil-jni:$wpilibVersion:linuxraspbian"
compile "edu.wpi.first.wpiutil:wpiutil-jni:$wpilibVersion:linuxx86-64"
compile "edu.wpi.first.wpiutil:wpiutil-jni:$wpilibVersion:osxx86-64"
compile "edu.wpi.first.wpiutil:wpiutil-jni:$wpilibVersion:windowsx86-64"
// 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"
implementation "org.jogamp.gluegen:gluegen-rt:$joglVersion:natives-linux-aarch64"
implementation "org.jogamp.jogl:jogl-all:$joglVersion:natives-linux-aarch64"
// Zip
compile 'org.zeroturnaround:zt-zip:1.14'
}
task writeCurrentVersionJava {
writePhotonVersionFile(Path.of("$projectDir", "src", "main", "java", "org", "photonvision", "PhotonVersion.java"),
versionString)
}
build.dependsOn writeCurrentVersionJava

View File

@@ -0,0 +1,2 @@
rootProject.name = 'photon-core'

View File

@@ -332,8 +332,8 @@ public class ConfigManager {
return loadedConfigurations;
}
public void addCameraConfigurations(HashMap<VisionSource, CameraConfiguration> sources) {
getConfig().addCameraConfigs(sources.values());
public void addCameraConfigurations(List<VisionSource> sources) {
getConfig().addCameraConfigs(sources);
requestSave();
}

View File

@@ -28,6 +28,7 @@ import org.photonvision.common.util.SerializationUtils;
import org.photonvision.raspi.PicamJNI;
import org.photonvision.vision.processes.VisionModule;
import org.photonvision.vision.processes.VisionModuleManager;
import org.photonvision.vision.processes.VisionSource;
// TODO rename this class
public class PhotonConfiguration {
@@ -75,9 +76,9 @@ public class PhotonConfiguration {
return cameraConfigurations;
}
public void addCameraConfigs(Collection<CameraConfiguration> config) {
for (var c : config) {
addCameraConfig(c);
public void addCameraConfigs(Collection<VisionSource> sources) {
for (var s : sources) {
addCameraConfig(s.getCameraConfiguration());
}
}

View File

@@ -34,6 +34,13 @@ public class OutgoingUIEvent<T> extends DataChangeEvent<T> {
this.originContext = originContext;
}
public static OutgoingUIEvent<HashMap<String, Object>> wrappedOf(
String commandName, Object value) {
HashMap<String, Object> data = new HashMap<>();
data.put(commandName, value);
return new OutgoingUIEvent<>(commandName, data);
}
public static OutgoingUIEvent<HashMap<String, Object>> wrappedOf(
String commandName, String propertyName, Object value, WsContext originContext) {
HashMap<String, Object> data = new HashMap<>();

View File

@@ -20,13 +20,17 @@ 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 java.util.ArrayList;
import java.util.List;
import java.util.function.BooleanSupplier;
import java.util.function.Consumer;
import java.util.function.Supplier;
import org.photonvision.common.dataflow.CVPipelineResultConsumer;
import org.photonvision.common.dataflow.structures.Packet;
import org.photonvision.targeting.PhotonPipelineResult;
import org.photonvision.targeting.PhotonTrackedTarget;
import org.photonvision.vision.pipeline.result.CVPipelineResult;
import org.photonvision.vision.pipeline.result.SimplePipelineResult;
import org.photonvision.vision.target.TrackedTarget;
public class NTDataPublisher implements CVPipelineResultConsumer {
@@ -163,7 +167,9 @@ public class NTDataPublisher implements CVPipelineResultConsumer {
@Override
public void accept(CVPipelineResult result) {
var simplified = new SimplePipelineResult(result);
var simplified =
new PhotonPipelineResult(
result.getLatencyMillis(), simpleFromTrackedTargets(result.targets));
Packet packet = new Packet(simplified.getPacketSize());
simplified.populatePacket(packet);
@@ -201,4 +207,14 @@ public class NTDataPublisher implements CVPipelineResultConsumer {
}
rootTable.getInstance().flush();
}
public static List<PhotonTrackedTarget> simpleFromTrackedTargets(List<TrackedTarget> targets) {
var ret = new ArrayList<PhotonTrackedTarget>();
for (var t : targets) {
ret.add(
new PhotonTrackedTarget(
t.getYaw(), t.getPitch(), t.getArea(), t.getSkew(), t.getCameraToTarget()));
}
return ret;
}
}

View File

@@ -21,6 +21,7 @@ import edu.wpi.first.networktables.LogMessage;
import edu.wpi.first.networktables.NetworkTable;
import edu.wpi.first.networktables.NetworkTableInstance;
import java.util.function.Consumer;
import org.photonvision.PhotonVersion;
import org.photonvision.common.configuration.NetworkConfig;
import org.photonvision.common.logging.LogGroup;
import org.photonvision.common.logging.Logger;
@@ -62,16 +63,23 @@ public class NetworkTablesManager {
hasReportedConnectionFailure = false;
lastConnectMessageMillis = System.currentTimeMillis();
ScriptManager.queueEvent(ScriptEventType.kNTConnected);
getInstance().broadcastVersion();
}
}
}
private void broadcastVersion() {
kRootTable.getEntry("version").setString(PhotonVersion.versionString);
kRootTable.getEntry("buildDate").setString(PhotonVersion.buildDate);
}
public void setConfig(NetworkConfig config) {
if (config.runNTServer) {
setServerMode();
} else {
setClientMode(config.teamNumber);
}
broadcastVersion();
}
private void setClientMode(int teamNumber) {
@@ -79,17 +87,20 @@ public class NetworkTablesManager {
ntInstance.stopServer();
ntInstance.startClientTeam(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();
}
private void setServerMode() {
logger.info("Starting NT Server");
ntInstance.stopClient();
ntInstance.startServer();
broadcastVersion();
}
}

View File

@@ -17,14 +17,13 @@
package org.photonvision.common.dataflow.websocket;
import com.fasterxml.jackson.core.JsonProcessingException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import org.photonvision.common.dataflow.CVPipelineResultConsumer;
import org.photonvision.common.dataflow.DataChangeService;
import org.photonvision.common.dataflow.events.OutgoingUIEvent;
import org.photonvision.common.logging.LogGroup;
import org.photonvision.common.logging.Logger;
import org.photonvision.server.SocketHandler;
import org.photonvision.vision.pipeline.result.CVPipelineResult;
public class UIDataPublisher implements CVPipelineResultConsumer {
@@ -39,16 +38,17 @@ public class UIDataPublisher implements CVPipelineResultConsumer {
@Override
public void accept(CVPipelineResult result) {
var now = System.currentTimeMillis();
long now = System.currentTimeMillis();
var dataMap = new HashMap<String, Object>();
dataMap.put("latency", result.getLatencyMillis());
// only update the UI at 15hz
if (lastUIResultUpdateTime + 1000.0 / 10.0 > now) return;
var uiMap = new HashMap<Integer, HashMap<String, Object>>();
var dataMap = new HashMap<String, Object>();
dataMap.put("fps", result.fps);
dataMap.put("latency", result.getLatencyMillis());
var targets = result.targets;
@@ -57,18 +57,10 @@ public class UIDataPublisher implements CVPipelineResultConsumer {
uiTargets.add(t.toHashMap());
}
dataMap.put("targets", uiTargets);
uiMap.put(index, dataMap);
var retMap = new HashMap<String, Object>();
retMap.put("updatePipelineResult", uiMap);
try {
SocketHandler.getInstance().broadcastMessage(retMap, null);
} catch (JsonProcessingException e) {
logger.error(e.getMessage());
logger.error(Arrays.toString(e.getStackTrace()));
}
DataChangeService.getInstance()
.publishEvent(OutgoingUIEvent.wrappedOf("updatePipelineResult", uiMap));
lastUIResultUpdateTime = now;
}
}

View File

@@ -27,7 +27,6 @@ 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.VisionLED.VisionLEDMode;
import org.photonvision.common.hardware.metrics.MetricsBase;
import org.photonvision.common.logging.LogGroup;
import org.photonvision.common.logging.Logger;
@@ -91,7 +90,7 @@ public class HardwareManager {
pigpioSocket);
ledModeEntry = NetworkTablesManager.getInstance().kRootTable.getEntry("ledMode");
ledModeEntry.setNumber(VisionLEDMode.VLM_DEFAULT.value);
ledModeEntry.setNumber(VisionLEDMode.kDefault.value);
ledModeListener =
visionLED == null
? null

View File

@@ -40,7 +40,7 @@ public class VisionLED {
private final int brightnessMax;
private final PigpioSocket pigpioSocket;
private VisionLEDMode currentLedMode = VisionLEDMode.VLM_DEFAULT;
private VisionLEDMode currentLedMode = VisionLEDMode.kDefault;
private BooleanSupplier pipelineModeSupplier;
private int mappedBrightnessPercentage;
@@ -111,7 +111,7 @@ public class VisionLED {
}
public void setState(boolean on) {
setInternal(on ? VisionLEDMode.VLM_ON : VisionLEDMode.VLM_OFF, false);
setInternal(on ? VisionLEDMode.kOn : VisionLEDMode.kOff, false);
}
void onLedModeChange(EntryNotification entryNotification) {
@@ -120,20 +120,20 @@ public class VisionLED {
VisionLEDMode newLedMode;
switch (newLedModeRaw) {
case -1:
newLedMode = VisionLEDMode.VLM_DEFAULT;
newLedMode = VisionLEDMode.kDefault;
break;
case 0:
newLedMode = VisionLEDMode.VLM_OFF;
newLedMode = VisionLEDMode.kOff;
break;
case 1:
newLedMode = VisionLEDMode.VLM_ON;
newLedMode = VisionLEDMode.kOn;
break;
case 2:
newLedMode = VisionLEDMode.VLM_BLINK;
newLedMode = VisionLEDMode.kBlink;
break;
default:
logger.warn("User supplied invalid LED mode, falling back to Default");
newLedMode = VisionLEDMode.VLM_DEFAULT;
newLedMode = VisionLEDMode.kDefault;
break;
}
setInternal(newLedMode, true);
@@ -145,16 +145,16 @@ public class VisionLED {
if (fromNT) {
switch (newLedMode) {
case VLM_DEFAULT:
case kDefault:
setStateImpl(pipelineModeSupplier.getAsBoolean());
break;
case VLM_OFF:
case kOff:
setStateImpl(false);
break;
case VLM_ON:
case kOn:
setStateImpl(true);
break;
case VLM_BLINK:
case kBlink:
blinkImpl(85, -1);
break;
}
@@ -166,15 +166,15 @@ public class VisionLED {
+ newLedMode.toString()
+ "\"");
} else {
if (currentLedMode == VisionLEDMode.VLM_DEFAULT) {
if (currentLedMode == VisionLEDMode.kDefault) {
switch (newLedMode) {
case VLM_DEFAULT:
case kDefault:
setStateImpl(pipelineModeSupplier.getAsBoolean());
break;
case VLM_OFF:
case kOff:
setStateImpl(false);
break;
case VLM_ON:
case kOn:
setStateImpl(true);
break;
}
@@ -182,32 +182,4 @@ public class VisionLED {
logger.info("Changing LED internal state to " + newLedMode.toString());
}
}
public enum VisionLEDMode {
VLM_DEFAULT(-1),
VLM_OFF(0),
VLM_ON(1),
VLM_BLINK(2);
public final int value;
VisionLEDMode(int value) {
this.value = value;
}
@Override
public String toString() {
switch (this) {
case VLM_DEFAULT:
return "Default";
case VLM_OFF:
return "Off";
case VLM_ON:
return "On";
case VLM_BLINK:
return "Blink";
}
return "";
}
}
}

View File

@@ -17,13 +17,13 @@
package org.photonvision.common.hardware.metrics;
import com.fasterxml.jackson.core.JsonProcessingException;
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;
import org.photonvision.server.SocketHandler;
public class MetricsPublisher {
private static final Logger logger = new Logger(MetricsPublisher.class, LogGroup.General);
@@ -65,14 +65,7 @@ public class MetricsPublisher {
metrics.put("gpuMemUtil", gpuMetrics.getMallocedMemory());
metrics.put("diskUtilPct", diskMetrics.getUsedDiskPct());
var retMap = new HashMap<String, Object>();
retMap.put("metrics", metrics);
try {
SocketHandler.getInstance().broadcastMessage(retMap, null);
} catch (JsonProcessingException e) {
logger.error("Exception while sending metrics!", e);
}
DataChangeService.getInstance().publishEvent(OutgoingUIEvent.wrappedOf("metrics", metrics));
}
private static class Singleton {

View File

@@ -33,7 +33,6 @@ import org.photonvision.common.configuration.ConfigManager;
import org.photonvision.common.dataflow.DataChangeService;
import org.photonvision.common.dataflow.events.OutgoingUIEvent;
import org.photonvision.common.util.TimedTaskManager;
import org.photonvision.server.SocketHandler;
@SuppressWarnings("unused")
public class Logger {
@@ -302,12 +301,12 @@ public class Logger {
private static class UILogAppender implements LogAppender {
@Override
public void log(String message, LogLevel level) {
var messageMap = new SocketHandler.UIMap();
var messageMap = new HashMap<String, Object>();
messageMap.put("logMessage", message);
messageMap.put("logLevel", level.code);
var superMap = new SocketHandler.UIMap();
var superMap = new HashMap<String, Object>();
superMap.put("logMessage", messageMap);
DataChangeService.getInstance().publishEvent(new OutgoingUIEvent<>("log", superMap));
DataChangeService.getInstance().publishEvent(OutgoingUIEvent.wrappedOf("log", superMap));
}
}
@@ -345,6 +344,8 @@ public class Logger {
wantsFlush = true;
} catch (IOException e) {
e.printStackTrace();
} catch (NullPointerException e) {
// Nothing to do - no stream available for writing
}
}
}

View File

@@ -135,7 +135,7 @@ public class TestUtils {
}
private static Path getResourcesFolderPath(boolean testMode) {
return Path.of("src", (testMode ? "main" : "test"), "resources").toAbsolutePath();
return Path.of(testMode ? "src/main/resources" : "../test-resources").toAbsolutePath();
}
public static Path getTestMode2019ImagePath() {

View File

@@ -18,6 +18,7 @@
package org.photonvision.common.util.file;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
@@ -35,7 +36,7 @@ public class FileUtils {
private FileUtils() {}
private static Logger logger = new Logger(FileUtils.class, LogGroup.General);
private static final Logger logger = new Logger(FileUtils.class, LogGroup.General);
private static final Set<PosixFilePermission> allReadWriteExecutePerms =
new HashSet<>(Arrays.asList(PosixFilePermission.values()));
@@ -49,7 +50,7 @@ public class FileUtils {
.sorted(Comparator.reverseOrder())
.map(Path::toFile)
.filter(File::isFile)
.forEach(File::delete);
.forEach((var file) -> deleteFile(file.toPath()));
// close the stream
files.close();
@@ -61,8 +62,10 @@ public class FileUtils {
public static void deleteFile(Path path) {
try {
Files.delete(path);
} catch (FileNotFoundException fe) {
logger.warn("Tried to delete file \"" + path + "\" but it did not exist");
} catch (IOException e) {
logger.error("Exception deleting file " + path + "!", e);
logger.error("Exception deleting file \"" + path + "\"!", e);
}
}
@@ -80,10 +83,11 @@ public class FileUtils {
Set<PosixFilePermission> perms =
Files.readAttributes(path, PosixFileAttributes.class).permissions();
if (!perms.equals(allReadWriteExecutePerms)) {
logger.info("Setting perms on" + path.toString());
logger.info("Setting perms on" + path);
Files.setPosixFilePermissions(path, perms);
if (thisFile.isDirectory()) {
for (File subfile : thisFile.listFiles()) {
var theseFiles = thisFile.listFiles();
if (thisFile.isDirectory() && theseFiles != null) {
for (File subfile : theseFiles) {
setFilePerms(subfile.toPath());
}
}

View File

@@ -17,13 +17,13 @@
package org.photonvision.common.util.math;
import org.apache.commons.math3.util.FastMath;
import edu.wpi.first.wpiutil.WPIUtilJNI;
public class MathUtils {
MathUtils() {}
public static double toSlope(Number angle) {
return FastMath.atan(FastMath.toRadians(angle.doubleValue() - 90));
return Math.atan(Math.toRadians(angle.doubleValue() - 90));
}
public static int safeDivide(int quotient, int divisor) {
@@ -43,6 +43,10 @@ public class MathUtils {
return nanos / 1000000.0;
}
public static double nanosToMillis(double nanos) {
return nanos / 1000000.0;
}
public static long millisToNanos(long millis) {
return millis * 1000000;
}
@@ -59,4 +63,21 @@ public class MathUtils {
public static int map(int value, int inMin, int inMax, int outMin, int outMax) {
return (int) Math.floor(map((double) value, inMin, inMax, outMin, outMax) + 0.5);
}
/**
* Linearly interpolates between two values.
*
* @param startValue The start value.
* @param endValue The end value.
* @param t The fraction for interpolation.
* @return The interpolated value.
*/
@SuppressWarnings("ParameterName")
public static double lerp(double startValue, double endValue, double t) {
return startValue + (endValue - startValue) * t;
}
public static long wpiNanoTime() {
return microsToNanos(WPIUtilJNI.now());
}
}

View File

@@ -85,7 +85,7 @@ public class PicamJNI {
}
public static boolean isSupported() {
return libraryLoaded && getSensorModel() != SensorModel.Disconnected;
return libraryLoaded && !isVCSMSupported() && getSensorModel() != SensorModel.Disconnected;
}
public static SensorModel getSensorModel() {
@@ -105,6 +105,9 @@ public class PicamJNI {
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
/**

View File

@@ -28,27 +28,30 @@ import org.photonvision.vision.frame.provider.FileFrameProvider;
import org.photonvision.vision.processes.VisionSource;
import org.photonvision.vision.processes.VisionSourceSettables;
public class FileVisionSource implements VisionSource {
public class FileVisionSource extends VisionSource {
private final CameraConfiguration cameraConfiguration;
private final FileFrameProvider frameProvider;
private final FileSourceSettables settables;
public FileVisionSource(CameraConfiguration cameraConfiguration) {
this.cameraConfiguration = cameraConfiguration;
super(cameraConfiguration);
var calibration =
cameraConfiguration.calibrations.size() > 0
? cameraConfiguration.calibrations.get(0)
: null;
frameProvider =
new FileFrameProvider(
Path.of(cameraConfiguration.path),
cameraConfiguration.FOV,
FileFrameProvider.MAX_FPS,
cameraConfiguration.camPitch,
cameraConfiguration.calibrations.get(0));
calibration);
settables =
new FileSourceSettables(cameraConfiguration, frameProvider.get().frameStaticProperties);
}
public FileVisionSource(String name, String imagePath, double fov) {
cameraConfiguration = new CameraConfiguration(name, imagePath);
super(new CameraConfiguration(name, imagePath));
frameProvider = new FileFrameProvider(imagePath, fov);
settables =
new FileSourceSettables(cameraConfiguration, frameProvider.get().frameStaticProperties);

View File

@@ -33,19 +33,18 @@ import org.photonvision.vision.frame.provider.USBFrameProvider;
import org.photonvision.vision.processes.VisionSource;
import org.photonvision.vision.processes.VisionSourceSettables;
public class USBCameraSource implements VisionSource {
public class USBCameraSource extends VisionSource {
private final Logger logger;
private final UsbCamera camera;
private final USBCameraSettables usbCameraSettables;
private final USBFrameProvider usbFrameProvider;
public final CameraConfiguration configuration;
private final CvSink cvSink;
public final QuirkyCamera cameraQuirks;
public USBCameraSource(CameraConfiguration config) {
super(config);
logger = new Logger(USBCameraSource.class, config.nickname, LogGroup.Camera);
configuration = config;
camera = new UsbCamera(config.nickname, config.path);
cvSink = CameraServer.getInstance().getVideo(this.camera);
@@ -88,7 +87,6 @@ public class USBCameraSource implements VisionSource {
super(configuration);
getAllVideoModes();
setVideoMode(videoModes.get(0));
calculateFrameStaticProps();
}
private int timeToPiCamV2RawExposure(double time_us) {
@@ -281,6 +279,6 @@ public class USBCameraSource implements VisionSource {
@Override
public int hashCode() {
return Objects.hash(
camera, usbCameraSettables, usbFrameProvider, configuration, cvSink, cameraQuirks);
camera, usbCameraSettables, usbFrameProvider, cameraConfiguration, cvSink, cameraQuirks);
}
}

View File

@@ -29,13 +29,14 @@ import org.photonvision.vision.frame.provider.AcceleratedPicamFrameProvider;
import org.photonvision.vision.processes.VisionSource;
import org.photonvision.vision.processes.VisionSourceSettables;
public class ZeroCopyPicamSource implements VisionSource {
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");
@@ -99,8 +100,7 @@ public class ZeroCopyPicamSource implements VisionSource {
0, new FPSRatedVideoMode(VideoMode.PixelFormat.kUnknown, 320, 240, 120, 120, .39));
videoModes.put(
1, new FPSRatedVideoMode(VideoMode.PixelFormat.kUnknown, 640, 480, 65, 90, .39));
videoModes.put(
2, new FPSRatedVideoMode(VideoMode.PixelFormat.kUnknown, 1280, 720, 40, 90, .72));
// TODO: fix 1280x720 in the native code and re-add it
videoModes.put(
3, new FPSRatedVideoMode(VideoMode.PixelFormat.kUnknown, 1920, 1080, 15, 20, .53));
} else {
@@ -136,19 +136,22 @@ public class ZeroCopyPicamSource implements VisionSource {
@Override
public void setExposure(double exposure) {
lastExposure = exposure;
PicamJNI.setExposure((int) Math.round(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;
PicamJNI.setBrightness(brightness);
var failure = PicamJNI.setBrightness(brightness);
if (failure) logger.warn("Couldn't set Pi camera brightness");
}
@Override
public void setGain(int gain) {
lastGain = gain;
PicamJNI.setGain(gain);
var failure = PicamJNI.setGain(gain);
if (failure) logger.warn("Couldn't set Pi camera gain");
}
@Override
@@ -159,8 +162,14 @@ public class ZeroCopyPicamSource implements VisionSource {
@Override
protected void setVideoModeInternal(VideoMode videoMode) {
var mode = (FPSRatedVideoMode) videoMode;
PicamJNI.destroyCamera();
PicamJNI.createCamera(mode.width, mode.height, mode.fpsActual);
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

View File

@@ -18,6 +18,7 @@
package org.photonvision.vision.frame;
import edu.wpi.first.wpilibj.geometry.Rotation2d;
import org.photonvision.common.util.math.MathUtils;
import org.photonvision.vision.opencv.CVMat;
import org.photonvision.vision.opencv.Releasable;
@@ -33,13 +34,14 @@ public class Frame implements Releasable {
}
public Frame(CVMat image, FrameStaticProperties frameStaticProperties) {
this(image, System.nanoTime(), frameStaticProperties);
this(image, MathUtils.wpiNanoTime(), frameStaticProperties);
}
public Frame() {
timestampNanos = 0;
image = new CVMat();
frameStaticProperties = new FrameStaticProperties(0, 0, 0, new Rotation2d(), null);
this(
new CVMat(),
MathUtils.wpiNanoTime(),
new FrameStaticProperties(0, 0, 0, new Rotation2d(), null));
}
public void copyTo(Frame destFrame) {

View File

@@ -19,8 +19,6 @@ package org.photonvision.vision.frame;
import edu.wpi.cscore.VideoMode;
import edu.wpi.first.wpilibj.geometry.Rotation2d;
import org.apache.commons.math3.fraction.Fraction;
import org.apache.commons.math3.util.FastMath;
import org.opencv.core.Point;
import org.photonvision.common.util.numbers.DoubleCouple;
import org.photonvision.vision.calibration.CameraCalibrationCoefficients;
@@ -79,23 +77,20 @@ public class FrameStaticProperties {
DoubleCouple horizVertViews =
calculateHorizontalVerticalFoV(this.fov, this.imageWidth, this.imageHeight);
horizontalFocalLength = this.imageWidth / (2 * FastMath.tan(horizVertViews.getFirst() / 2));
verticalFocalLength = this.imageHeight / (2 * FastMath.tan(horizVertViews.getSecond() / 2));
horizontalFocalLength = this.imageWidth / (2 * Math.tan(horizVertViews.getFirst() / 2));
verticalFocalLength = this.imageHeight / (2 * Math.tan(horizVertViews.getSecond() / 2));
}
public static DoubleCouple calculateHorizontalVerticalFoV(
double diagonalFoV, int imageWidth, int imageHeight) {
double diagonalView = FastMath.toRadians(diagonalFoV);
Fraction aspectFraction = new Fraction(imageWidth, imageHeight);
int horizontalRatio = aspectFraction.getNumerator();
int verticalRatio = aspectFraction.getDenominator();
double diagonalView = Math.toRadians(diagonalFoV);
double diagonalAspect = Math.hypot(imageWidth, imageHeight);
double diagonalAspect = FastMath.hypot(horizontalRatio, verticalRatio);
double horizontalView =
FastMath.atan(FastMath.tan(diagonalView / 2) * (horizontalRatio / diagonalAspect)) * 2;
Math.atan(Math.tan(diagonalView / 2) * (imageWidth / diagonalAspect)) * 2;
double verticalView =
FastMath.atan(FastMath.tan(diagonalView / 2) * (verticalRatio / diagonalAspect)) * 2;
Math.atan(Math.tan(diagonalView / 2) * (imageHeight / diagonalAspect)) * 2;
return new DoubleCouple(horizontalView, verticalView);
}

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