mirror of
https://github.com/PhotonVision/photonvision
synced 2026-06-22 01:11:40 +00:00
Compare commits
101 Commits
v2025.0.0-
...
v2025.2.1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9589967808 | ||
|
|
311846dc26 | ||
|
|
533f8c97fd | ||
|
|
75c289f526 | ||
|
|
e97865166d | ||
|
|
53144bfcf1 | ||
|
|
ee97a1b62e | ||
|
|
01a3d31734 | ||
|
|
a546ff0819 | ||
|
|
67bc032d26 | ||
|
|
8f816cf1af | ||
|
|
e2b028abdc | ||
|
|
00fb4bdf07 | ||
|
|
7067c75525 | ||
|
|
ef82328d74 | ||
|
|
be59e3a958 | ||
|
|
99427d888a | ||
|
|
8ec2da952f | ||
|
|
a5d007e258 | ||
|
|
78b82e3a96 | ||
|
|
1303a0eaae | ||
|
|
ab41d2d1ed | ||
|
|
d78f2b8650 | ||
|
|
5e5df483e2 | ||
|
|
009ec9e1d4 | ||
|
|
a08dc374c8 | ||
|
|
5b0ec742c4 | ||
|
|
04e28bc2d8 | ||
|
|
966b9e8c61 | ||
|
|
5f75619063 | ||
|
|
03ff9357d7 | ||
|
|
d05032963d | ||
|
|
7d9f9a627c | ||
|
|
2d19908119 | ||
|
|
d487e1c466 | ||
|
|
159b848234 | ||
|
|
53e6890f20 | ||
|
|
83c124f7fc | ||
|
|
05348f3981 | ||
|
|
e40c8fbca0 | ||
|
|
27684eef60 | ||
|
|
cc740c92c9 | ||
|
|
e673304221 | ||
|
|
974fcec21e | ||
|
|
36e3a1f72e | ||
|
|
5993e79e00 | ||
|
|
484e8d4298 | ||
|
|
fa2034d30b | ||
|
|
a84d681782 | ||
|
|
ab844a77b8 | ||
|
|
6c7a174424 | ||
|
|
474e4f07f8 | ||
|
|
7c254ec5dc | ||
|
|
09a741f540 | ||
|
|
a540d2dd3f | ||
|
|
3f8c406898 | ||
|
|
6065a3d70c | ||
|
|
34e9d5084c | ||
|
|
a56101197a | ||
|
|
2a82393c8e | ||
|
|
418eada0b5 | ||
|
|
b2e70a7257 | ||
|
|
b7a2636e97 | ||
|
|
d0e5e169cc | ||
|
|
7e18424d11 | ||
|
|
00d307439c | ||
|
|
5df189d306 | ||
|
|
bd1c5c0330 | ||
|
|
77e75b9975 | ||
|
|
e506ac6b28 | ||
|
|
88bc63cf82 | ||
|
|
a05542c06c | ||
|
|
ffc4e06ac6 | ||
|
|
81076375b8 | ||
|
|
66f369f3a9 | ||
|
|
7cba7b432d | ||
|
|
dd98d96d7e | ||
|
|
8ede892c14 | ||
|
|
08c62ab8cd | ||
|
|
e8efef476b | ||
|
|
c6403a65d2 | ||
|
|
6a8d638853 | ||
|
|
782929b006 | ||
|
|
4997ad9115 | ||
|
|
857a30d980 | ||
|
|
a40e69cca0 | ||
|
|
e069a79a32 | ||
|
|
d9dfe15bfe | ||
|
|
1dbd2e5990 | ||
|
|
7e9da4133d | ||
|
|
163b5c9c81 | ||
|
|
c6a3638a2f | ||
|
|
44f78cb03e | ||
|
|
61552ad6ca | ||
|
|
fa66ed866c | ||
|
|
08b4bd1f03 | ||
|
|
c536a1c312 | ||
|
|
adb18fe711 | ||
|
|
7d1e748b0e | ||
|
|
3a9d22c76b | ||
|
|
417e1a65b6 |
3
.gitattributes
vendored
3
.gitattributes
vendored
@@ -35,3 +35,6 @@
|
||||
*.so binary
|
||||
*.dll binary
|
||||
*.webp binary
|
||||
|
||||
# autogenerated constrained solve pnp code
|
||||
photon-targeting/src/main/native/cpp/photon/constrained_solvepnp/generate/**/* linguist-generated
|
||||
|
||||
4
.github/CODEOWNERS
vendored
4
.github/CODEOWNERS
vendored
@@ -1,2 +1,6 @@
|
||||
# These owners will be the default owners for everything in the repo.
|
||||
* @PhotonVision/program-devs
|
||||
docs/* @PhotonVision/doc-maintainers
|
||||
photonlib-java-examples/* @PhotonVision/doc-maintainers
|
||||
photonlib-cpp-examples/* @PhotonVision/doc-maintainers
|
||||
photonlib-python-examples/* @PhotonVision/doc-maintainers
|
||||
|
||||
17
.github/pull_request_template.md
vendored
Normal file
17
.github/pull_request_template.md
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
## Description
|
||||
|
||||
<!-- What changed? Why? (the code + comments should speak for itself on the "how") -->
|
||||
|
||||
<!-- Fun screenshots or a cool video or something are super helpful as well. If this touches platform-specific behavior, this is where test evidence should be collected. -->
|
||||
|
||||
<!-- Any issues this pull request closes or pull requests this supersedes should be linked with `Closes #issuenumber`. -->
|
||||
|
||||
## Meta
|
||||
|
||||
Merge checklist:
|
||||
- [ ] Pull Request title is [short, imperative summary](https://cbea.ms/git-commit/) of proposed changes
|
||||
- [ ] The description documents the _what_ and _why_
|
||||
- [ ] If this PR changes behavior or adds a feature, user documentation is updated
|
||||
- [ ] If this PR touches photon-serde, all messages have been regenerated and hashes have not changed unexpectedly
|
||||
- [ ] If this PR touches configuration, this is backwards compatible with settings back to v2024.3.1
|
||||
- [ ] If this PR addresses a bug, a regression test for it is added
|
||||
55
.github/workflows/build.yml
vendored
55
.github/workflows/build.yml
vendored
@@ -1,21 +1,18 @@
|
||||
name: Build
|
||||
|
||||
on:
|
||||
# Run on pushes to master and pushed tags, and on pull requests against master, but ignore the docs folder
|
||||
# Run on pushes to main and pushed tags, and on pull requests against main, but ignore the docs folder
|
||||
push:
|
||||
branches: [ master ]
|
||||
branches: [ main ]
|
||||
tags:
|
||||
- 'v*'
|
||||
paths:
|
||||
- '**'
|
||||
- '!docs/**'
|
||||
- '.github/**'
|
||||
pull_request:
|
||||
branches: [ master ]
|
||||
paths:
|
||||
- '**'
|
||||
- '!docs/**'
|
||||
- '.github/**'
|
||||
branches: [ main ]
|
||||
merge_group:
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.head_ref || github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
build-client:
|
||||
@@ -212,7 +209,7 @@ jobs:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- container: wpilib/roborio-cross-ubuntu:2024-22.04
|
||||
- container: wpilib/roborio-cross-ubuntu:2025-24.04
|
||||
artifact-name: Athena
|
||||
build-options: "-Ponlylinuxathena"
|
||||
- container: wpilib/raspbian-cross-ubuntu:bullseye-22.04
|
||||
@@ -394,7 +391,7 @@ jobs:
|
||||
- os: ubuntu-22.04
|
||||
artifact-name: LinuxArm64
|
||||
image_suffix: RaspberryPi
|
||||
image_url: https://github.com/PhotonVision/photon-image-modifier/releases/download/v2025.0.0-beta-4/photonvision_raspi.img.xz
|
||||
image_url: https://github.com/PhotonVision/photon-image-modifier/releases/download/v2025.0.3/photonvision_raspi.img.xz
|
||||
cpu: cortex-a7
|
||||
image_additional_mb: 0
|
||||
extraOpts: -Djdk.lang.Process.launchMechanism=vfork
|
||||
@@ -433,49 +430,61 @@ jobs:
|
||||
- os: ubuntu-22.04
|
||||
artifact-name: LinuxArm64
|
||||
image_suffix: RaspberryPi
|
||||
image_url: https://github.com/PhotonVision/photon-image-modifier/releases/download/v2025.0.0-beta-6/photonvision_raspi.img.xz
|
||||
image_url: https://github.com/PhotonVision/photon-image-modifier/releases/download/v2025.0.3/photonvision_raspi.img.xz
|
||||
cpu: cortex-a7
|
||||
image_additional_mb: 0
|
||||
- os: ubuntu-22.04
|
||||
artifact-name: LinuxArm64
|
||||
image_suffix: limelight2
|
||||
image_url: https://github.com/PhotonVision/photon-image-modifier/releases/download/v2025.0.0-beta-6/photonvision_limelight.img.xz
|
||||
image_url: https://github.com/PhotonVision/photon-image-modifier/releases/download/v2025.0.3/photonvision_limelight.img.xz
|
||||
cpu: cortex-a7
|
||||
image_additional_mb: 0
|
||||
- os: ubuntu-22.04
|
||||
artifact-name: LinuxArm64
|
||||
image_suffix: limelight3
|
||||
image_url: https://github.com/PhotonVision/photon-image-modifier/releases/download/v2025.0.0-beta-6/photonvision_limelight3.img.xz
|
||||
image_url: https://github.com/PhotonVision/photon-image-modifier/releases/download/v2025.0.3/photonvision_limelight3.img.xz
|
||||
cpu: cortex-a7
|
||||
image_additional_mb: 0
|
||||
- os: ubuntu-22.04
|
||||
artifact-name: LinuxArm64
|
||||
image_suffix: limelight3G
|
||||
image_url: https://github.com/PhotonVision/photon-image-modifier/releases/download/v2025.0.3/photonvision_limelight3g.img.xz
|
||||
cpu: cortex-a7
|
||||
image_additional_mb: 0
|
||||
- os: ubuntu-22.04
|
||||
artifact-name: LinuxArm64
|
||||
image_suffix: orangepi5
|
||||
image_url: https://github.com/PhotonVision/photon-image-modifier/releases/download/v2025.0.0-beta-6/photonvision_opi5.img.xz
|
||||
image_url: https://github.com/PhotonVision/photon-image-modifier/releases/download/v2025.0.3/photonvision_opi5.img.xz
|
||||
cpu: cortex-a8
|
||||
image_additional_mb: 1024
|
||||
- os: ubuntu-22.04
|
||||
artifact-name: LinuxArm64
|
||||
image_suffix: orangepi5b
|
||||
image_url: https://github.com/PhotonVision/photon-image-modifier/releases/download/v2025.0.0-beta-6/photonvision_opi5b.img.xz
|
||||
image_url: https://github.com/PhotonVision/photon-image-modifier/releases/download/v2025.0.3/photonvision_opi5b.img.xz
|
||||
cpu: cortex-a8
|
||||
image_additional_mb: 1024
|
||||
- os: ubuntu-22.04
|
||||
artifact-name: LinuxArm64
|
||||
image_suffix: orangepi5plus
|
||||
image_url: https://github.com/PhotonVision/photon-image-modifier/releases/download/v2025.0.0-beta-6/photonvision_opi5plus.img.xz
|
||||
image_url: https://github.com/PhotonVision/photon-image-modifier/releases/download/v2025.0.3/photonvision_opi5plus.img.xz
|
||||
cpu: cortex-a8
|
||||
image_additional_mb: 1024
|
||||
- os: ubuntu-22.04
|
||||
artifact-name: LinuxArm64
|
||||
image_suffix: orangepi5pro
|
||||
image_url: https://github.com/PhotonVision/photon-image-modifier/releases/download/v2025.0.0-beta-6/photonvision_opi5pro.img.xz
|
||||
image_url: https://github.com/PhotonVision/photon-image-modifier/releases/download/v2025.0.3/photonvision_opi5pro.img.xz
|
||||
cpu: cortex-a8
|
||||
image_additional_mb: 1024
|
||||
- os: ubuntu-22.04
|
||||
artifact-name: LinuxArm64
|
||||
image_suffix: orangepi5max
|
||||
image_url: https://github.com/PhotonVision/photon-image-modifier/releases/download/v2025.0.0-beta-6/photonvision_opi5max.img.xz
|
||||
image_url: https://github.com/PhotonVision/photon-image-modifier/releases/download/v2025.0.3/photonvision_opi5max.img.xz
|
||||
cpu: cortex-a8
|
||||
image_additional_mb: 1024
|
||||
- os: ubuntu-22.04
|
||||
artifact-name: LinuxArm64
|
||||
image_suffix: rock5c
|
||||
image_url: https://github.com/PhotonVision/photon-image-modifier/releases/download/v2025.0.3/photonvision_rock5c.img.xz
|
||||
cpu: cortex-a8
|
||||
image_additional_mb: 1024
|
||||
|
||||
@@ -558,14 +567,14 @@ jobs:
|
||||
- uses: softprops/action-gh-release@v2.0.9
|
||||
with:
|
||||
files: |
|
||||
**/*orangepi5*.xz
|
||||
**/@(*orangepi5*|*rock5*).xz
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
- uses: softprops/action-gh-release@v2.0.9
|
||||
with:
|
||||
files: |
|
||||
**/!(*orangepi5*).xz
|
||||
**/!(*orangepi5*|*rock5*).xz
|
||||
**/*.jar
|
||||
**/photonlib*.json
|
||||
**/photonlib*.zip
|
||||
|
||||
17
.github/workflows/cut-new-tag.yml
vendored
Normal file
17
.github/workflows/cut-new-tag.yml
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
name: Cut a new tag
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
tag_name:
|
||||
type: string
|
||||
description: The full name of the new tag to push to the latest commit to main
|
||||
|
||||
jobs:
|
||||
push_tag:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- run: git tag ${{ github.event.inputs.tag_name }}
|
||||
- run: git push origin ${{ github.event.inputs.tag_name }}
|
||||
21
.github/workflows/lint-format.yml
vendored
21
.github/workflows/lint-format.yml
vendored
@@ -1,21 +1,14 @@
|
||||
name: Lint and Format
|
||||
|
||||
on:
|
||||
# Run on pushes to master and pushed tags, and on pull requests against master, but ignore the docs folder
|
||||
# Run on pushes to main and pushed tags, and on pull requests against main, but ignore the docs folder
|
||||
push:
|
||||
branches: [ master ]
|
||||
branches: [ main ]
|
||||
tags:
|
||||
- 'v*'
|
||||
paths:
|
||||
- '**'
|
||||
- '!docs/**'
|
||||
- '.github/**'
|
||||
pull_request:
|
||||
branches: [ master ]
|
||||
paths:
|
||||
- '**'
|
||||
- '!docs/**'
|
||||
- '.github/**'
|
||||
branches: [ main ]
|
||||
merge_group:
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.head_ref || github.ref }}
|
||||
@@ -31,7 +24,7 @@ jobs:
|
||||
run: |
|
||||
git fetch --prune --unshallow
|
||||
git checkout -b pr
|
||||
git branch -f master origin/master
|
||||
git branch -f main origin/main
|
||||
- name: Set up Python 3.8
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
@@ -91,6 +84,6 @@ jobs:
|
||||
run: |
|
||||
git fetch --prune --unshallow
|
||||
git checkout -b pr
|
||||
git branch -f master origin/master
|
||||
git branch -f main origin/main
|
||||
- name: Check index.html not changed
|
||||
run: git --no-pager diff --exit-code origin/master photon-server/src/main/resources/web/index.html
|
||||
run: git --no-pager diff --exit-code origin/main photon-server/src/main/resources/web/index.html
|
||||
|
||||
25
.github/workflows/photon-code-docs.yml
vendored
25
.github/workflows/photon-code-docs.yml
vendored
@@ -1,21 +1,18 @@
|
||||
name: Photon Code Documentation
|
||||
|
||||
on:
|
||||
# Run on pushes to master and pushed tags, and on pull requests against master, but ignore the docs folder
|
||||
# Run on pushes to main and pushed tags, and on pull requests against main, but ignore the docs folder
|
||||
push:
|
||||
branches: [ master ]
|
||||
branches: [ main ]
|
||||
tags:
|
||||
- 'v*'
|
||||
paths:
|
||||
- '**'
|
||||
- '!docs/**'
|
||||
- '.github/**'
|
||||
pull_request:
|
||||
branches: [ master ]
|
||||
paths:
|
||||
- '**'
|
||||
- '!docs/**'
|
||||
- '.github/**'
|
||||
branches: [ main ]
|
||||
merge_group:
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.head_ref || github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages
|
||||
permissions:
|
||||
@@ -23,10 +20,6 @@ permissions:
|
||||
pages: write
|
||||
id-token: write
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
build-client:
|
||||
name: "PhotonClient Build"
|
||||
@@ -85,7 +78,7 @@ jobs:
|
||||
|
||||
- run: find .
|
||||
- name: copy file via ssh password
|
||||
if: github.ref == 'refs/heads/master'
|
||||
if: github.ref == 'refs/heads/main'
|
||||
uses: appleboy/scp-action@v0.1.7
|
||||
with:
|
||||
host: ${{ secrets.WEBMASTER_SSH_HOST }}
|
||||
|
||||
15
.github/workflows/photonvision-docs.yml
vendored
15
.github/workflows/photonvision-docs.yml
vendored
@@ -2,15 +2,14 @@ name: PhotonVision Sphinx Documentation Checks
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ master ]
|
||||
paths:
|
||||
- 'docs/**'
|
||||
- '.github/**'
|
||||
branches: [ main ]
|
||||
pull_request:
|
||||
branches: [ master ]
|
||||
paths:
|
||||
- 'docs/**'
|
||||
- '.github/**'
|
||||
branches: [ main ]
|
||||
merge_group:
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.head_ref || github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
17
.github/workflows/python.yml
vendored
17
.github/workflows/python.yml
vendored
@@ -5,19 +5,16 @@ permissions:
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ master ]
|
||||
branches: [ main ]
|
||||
tags:
|
||||
- 'v*'
|
||||
paths:
|
||||
- '**'
|
||||
- '!docs/**'
|
||||
- '.github/**'
|
||||
pull_request:
|
||||
branches: [ master ]
|
||||
paths:
|
||||
- '**'
|
||||
- '!docs/**'
|
||||
- '.github/**'
|
||||
branches: [ main ]
|
||||
merge_group:
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.head_ref || github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
buildAndDeploy:
|
||||
|
||||
@@ -20,6 +20,9 @@ modifiableFileExclude {
|
||||
\.ico$
|
||||
\.rknn$
|
||||
gradlew
|
||||
photon-lib/py/photonlibpy/generated/
|
||||
photon-targeting/src/main/native/cpp/photon/constrained_solvepnp/generate/
|
||||
photon-targeting/src/generated/
|
||||
}
|
||||
|
||||
includeProject {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# PhotonVision
|
||||
|
||||
[](https://github.com/PhotonVision/photonvision/actions?query=workflow%3ACI) [](https://codecov.io/gh/PhotonVision/photonvision) [](https://discord.gg/wYxTwym)
|
||||
[](https://discord.gg/wYxTwym)
|
||||
|
||||
PhotonVision is the free, fast, and easy-to-use computer vision solution for the *FIRST* Robotics Competition. You can read an overview of our features [on our website](https://photonvision.org). You can find our comprehensive documentation [here](https://docs.photonvision.org).
|
||||
|
||||
@@ -17,7 +17,7 @@ If you are interested in contributing code or documentation to the project, plea
|
||||
## Documentation
|
||||
|
||||
- Our main documentation page: [docs.photonvision.org](https://docs.photonvision.org)
|
||||
- Photon UI demo: [demo.photonvision.org](https://demo.photonvision.org) (or [manual link](https://photonvision.github.io/photonvision/built-client/))
|
||||
- Photon UI demo: [http://photonvision.global/](http://photonvision.global/) (or [manual link](https://photonvision.github.io/photonvision/built-client/))
|
||||
- Javadocs: [javadocs.photonvision.org](https://javadocs.photonvision.org) (or [manual link](https://photonvision.github.io/photonvision/built-docs/javadoc/))
|
||||
- C++ Doxygen [cppdocs.photonvision.org](https://cppdocs.photonvision.org) (or [manual link](https://photonvision.github.io/photonvision/built-docs/doxygen/html/))
|
||||
|
||||
@@ -67,7 +67,7 @@ sudo apt install libcholmod3 liblapack3 libsuitesparseconfig5
|
||||
|
||||
PhotonVision was forked from [Chameleon Vision](https://github.com/Chameleon-Vision/chameleon-vision/). Thank you to everyone who worked on the original project.
|
||||
|
||||
* [WPILib](https://github.com/wpilibsuite) - Specifically [cscore](https://github.com/wpilibsuite/allwpilib/tree/master/cscore), [CameraServer](https://github.com/wpilibsuite/allwpilib/tree/master/cameraserver), [NTCore](https://github.com/wpilibsuite/allwpilib/tree/master/ntcore), and [OpenCV](https://github.com/wpilibsuite/thirdparty-opencv).
|
||||
* [WPILib](https://github.com/wpilibsuite) - Specifically [cscore](https://github.com/wpilibsuite/allwpilib/tree/main/cscore), [CameraServer](https://github.com/wpilibsuite/allwpilib/tree/main/cameraserver), [NTCore](https://github.com/wpilibsuite/allwpilib/tree/main/ntcore), and [OpenCV](https://github.com/wpilibsuite/thirdparty-opencv).
|
||||
|
||||
* [Apache Commons](https://commons.apache.org/) - Specifically [Commons Math](https://commons.apache.org/proper/commons-math/), and [Commons Lang](https://commons.apache.org/proper/commons-lang/)
|
||||
|
||||
|
||||
31
build.gradle
31
build.gradle
@@ -1,14 +1,17 @@
|
||||
import edu.wpi.first.toolchain.*
|
||||
|
||||
plugins {
|
||||
id "java"
|
||||
id "cpp"
|
||||
id "com.diffplug.spotless" version "6.24.0"
|
||||
id "edu.wpi.first.wpilib.repositories.WPILibRepositoriesPlugin" version "2020.2"
|
||||
id "edu.wpi.first.GradleRIO" version "2025.1.1-beta-1"
|
||||
id "edu.wpi.first.GradleRIO" version "2025.3.1"
|
||||
id 'edu.wpi.first.WpilibTools' version '1.3.0'
|
||||
id 'com.google.protobuf' version '0.9.3' apply false
|
||||
id 'edu.wpi.first.GradleJni' version '1.1.0'
|
||||
id "org.ysb33r.doxygen" version "1.0.4" apply false
|
||||
id 'com.gradleup.shadow' version '8.3.4' apply false
|
||||
id "com.github.node-gradle.node" version "7.0.1" apply false
|
||||
id "org.hidetake.ssh" version "2.11.2" apply false
|
||||
}
|
||||
|
||||
allprojects {
|
||||
@@ -30,16 +33,16 @@ ext.allOutputsFolder = file("$project.buildDir/outputs")
|
||||
apply from: "versioningHelper.gradle"
|
||||
|
||||
ext {
|
||||
wpilibVersion = "2025.1.1-beta-1"
|
||||
wpilibVersion = "2025.3.1"
|
||||
wpimathVersion = wpilibVersion
|
||||
openCVYear = "2024"
|
||||
openCVversion = "4.8.0-4"
|
||||
openCVYear = "2025"
|
||||
openCVversion = "4.10.0-3"
|
||||
joglVersion = "2.4.0"
|
||||
javalinVersion = "5.6.2"
|
||||
libcameraDriverVersion = "dev-v2023.1.0-15-gc8988b3"
|
||||
rknnVersion = "dev-v2024.0.1-4-g0db16ac"
|
||||
libcameraDriverVersion = "v2025.0.3"
|
||||
rknnVersion = "dev-v2025.0.0-1-g33b6263"
|
||||
frcYear = "2025"
|
||||
mrcalVersion = "dev-v2024.0.0-27-g41d7868";
|
||||
mrcalVersion = "v2025.0.0";
|
||||
|
||||
|
||||
pubVersion = versionString
|
||||
@@ -87,16 +90,6 @@ spotless {
|
||||
trimTrailingWhitespace()
|
||||
endWithNewline()
|
||||
}
|
||||
format 'xml', {
|
||||
target fileTree('.') {
|
||||
include '**/*.xml'
|
||||
exclude '**/build/**', '**/build-*/**', "**/.idea/**"
|
||||
}
|
||||
eclipseWtp('xml')
|
||||
trimTrailingWhitespace()
|
||||
indentWithSpaces(2)
|
||||
endWithNewline()
|
||||
}
|
||||
format 'misc', {
|
||||
target fileTree('.') {
|
||||
include '**/*.md', '**/.gitignore'
|
||||
@@ -109,7 +102,7 @@ spotless {
|
||||
}
|
||||
|
||||
wrapper {
|
||||
gradleVersion '8.4'
|
||||
gradleVersion '8.11'
|
||||
}
|
||||
|
||||
ext.getCurrentArch = {
|
||||
|
||||
@@ -12,5 +12,6 @@ modifiableFileExclude {
|
||||
\.ico$
|
||||
\.rknn$
|
||||
\.svg$
|
||||
\.woff2$
|
||||
gradlew
|
||||
}
|
||||
|
||||
@@ -6,4 +6,4 @@ PhotonVision is a free open-source vision processing software for FRC teams.
|
||||
|
||||
This repository is the source code for our ReadTheDocs documentation, which can be found [here](https://docs.photonvision.org).
|
||||
|
||||
[Contribution and formatting guidelines for this project](https://docs.photonvision.org/en/latest/docs/contributing/photonvision-docs/index.html)
|
||||
[Contribution and formatting guidelines for this project](https://docs.photonvision.org/en/latest/docs/contributing/index.html)
|
||||
|
||||
Binary file not shown.
Binary file not shown.
BIN
docs/source/_static/assets/poseest_demo.mp4
Normal file
BIN
docs/source/_static/assets/poseest_demo.mp4
Normal file
Binary file not shown.
@@ -1,16 +1,3 @@
|
||||
/*!
|
||||
* Font Awesome 4.7.0 by @davegandy - http://fontawesome.io - @fontawesome
|
||||
* License - http://fontawesome.io/license (Font: SIL OFL 1.1, CSS: MIT License)
|
||||
*/
|
||||
|
||||
@font-face {
|
||||
font-family: FontAwesome;
|
||||
src: url(fonts/fontawesome-webfont.eot?674f50d287a8c48dc19ba404d20fe713);
|
||||
src: url(fonts/fontawesome-webfont.eot?674f50d287a8c48dc19ba404d20fe713?#iefix&v=4.7.0) format("embedded-opentype"), url(fonts/fontawesome-webfont.woff2?af7ae505a9eed503f8b8e6982036873e) format("woff2"), url(fonts/fontawesome-webfont.woff?fee66e712a8a08eef5805a46892932ad) format("woff"), url(fonts/fontawesome-webfont.ttf?b06871f281fee6b241d60582ae9369b9) format("truetype"), url(fonts/fontawesome-webfont.svg?912ec66d7572ff821749319396470bde#fontawesomeregular) format("svg");
|
||||
font-weight: 400;
|
||||
font-style:normal
|
||||
}
|
||||
|
||||
.code-block-caption>.headerlink, dl dt>.headerlink, h1>.headerlink, h2>.headerlink, h3>.headerlink, h4>.headerlink, h5>.headerlink, h6>.headerlink, p.caption>.headerlink, table>caption>.headerlink {
|
||||
font-family: FontAwesome;
|
||||
font-size: 0.75em;
|
||||
|
||||
6
docs/source/_static/css/v4-font-face.min.css
vendored
Normal file
6
docs/source/_static/css/v4-font-face.min.css
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
/*!
|
||||
* Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com
|
||||
* License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License)
|
||||
* Copyright 2024 Fonticons, Inc.
|
||||
*/
|
||||
@font-face{font-family:"FontAwesome";font-display:block;src:url(../webfonts/fa-solid-900.woff2) format("woff2"),url(../webfonts/fa-solid-900.ttf) format("truetype")}@font-face{font-family:"FontAwesome";font-display:block;src:url(../webfonts/fa-brands-400.woff2) format("woff2"),url(../webfonts/fa-brands-400.ttf) format("truetype")}@font-face{font-family:"FontAwesome";font-display:block;src:url(../webfonts/fa-regular-400.woff2) format("woff2"),url(../webfonts/fa-regular-400.ttf) format("truetype");unicode-range:u+f003,u+f006,u+f014,u+f016-f017,u+f01a-f01b,u+f01d,u+f022,u+f03e,u+f044,u+f046,u+f05c-f05d,u+f06e,u+f070,u+f087-f088,u+f08a,u+f094,u+f096-f097,u+f09d,u+f0a0,u+f0a2,u+f0a4-f0a7,u+f0c5,u+f0c7,u+f0e5-f0e6,u+f0eb,u+f0f6-f0f8,u+f10c,u+f114-f115,u+f118-f11a,u+f11c-f11d,u+f133,u+f147,u+f14e,u+f150-f152,u+f185-f186,u+f18e,u+f190-f192,u+f196,u+f1c1-f1c9,u+f1d9,u+f1db,u+f1e3,u+f1ea,u+f1f7,u+f1f9,u+f20a,u+f247-f248,u+f24a,u+f24d,u+f255-f25b,u+f25d,u+f271-f274,u+f278,u+f27b,u+f28c,u+f28e,u+f29c,u+f2b5,u+f2b7,u+f2ba,u+f2bc,u+f2be,u+f2c0-f2c1,u+f2c3,u+f2d0,u+f2d2,u+f2d4,u+f2dc}@font-face{font-family:"FontAwesome";font-display:block;src:url(../webfonts/fa-v4compatibility.woff2) format("woff2"),url(../webfonts/fa-v4compatibility.ttf) format("truetype");unicode-range:u+f041,u+f047,u+f065-f066,u+f07d-f07e,u+f080,u+f08b,u+f08e,u+f090,u+f09a,u+f0ac,u+f0ae,u+f0b2,u+f0d0,u+f0d6,u+f0e4,u+f0ec,u+f10a-f10b,u+f123,u+f13e,u+f148-f149,u+f14c,u+f156,u+f15e,u+f160-f161,u+f163,u+f175-f178,u+f195,u+f1f8,u+f219,u+f27a}
|
||||
BIN
docs/source/_static/webfonts/fa-brands-400.woff2
Normal file
BIN
docs/source/_static/webfonts/fa-brands-400.woff2
Normal file
Binary file not shown.
BIN
docs/source/_static/webfonts/fa-regular-400.woff2
Normal file
BIN
docs/source/_static/webfonts/fa-regular-400.woff2
Normal file
Binary file not shown.
BIN
docs/source/_static/webfonts/fa-solid-900.woff2
Normal file
BIN
docs/source/_static/webfonts/fa-solid-900.woff2
Normal file
Binary file not shown.
BIN
docs/source/_static/webfonts/fa-v4compatibility.woff2
Normal file
BIN
docs/source/_static/webfonts/fa-v4compatibility.woff2
Normal file
Binary file not shown.
@@ -45,7 +45,7 @@ extensions = [
|
||||
|
||||
ogp_site_url = "https://docs.photonvision.org/en/latest/"
|
||||
ogp_site_name = "PhotonVision Documentation"
|
||||
ogp_image = "https://raw.githubusercontent.com/PhotonVision/photonvision-docs/master/source/assets/RectLogo.png"
|
||||
ogp_image = "https://raw.githubusercontent.com/PhotonVision/photonvision-docs/main/source/assets/RectLogo.png"
|
||||
|
||||
# Add any paths that contain templates here, relative to this directory.
|
||||
templates_path = ["_templates"]
|
||||
@@ -79,6 +79,7 @@ source_suffix = [".rst", ".md"]
|
||||
|
||||
|
||||
def setup(app):
|
||||
app.add_css_file("css/v4-font-face.min.css")
|
||||
app.add_css_file("css/pv-icons.css")
|
||||
|
||||
|
||||
|
||||
@@ -3,27 +3,31 @@
|
||||
## Before Competition
|
||||
|
||||
- Ensure you have spares of the relevant electronics if you can afford it (switch, coprocessor, cameras, etc.).
|
||||
- Download the latest release .jar onto your computer and update your Pi if necessary (only update if the release is labeled "critical" or similar, we do not recommend updating right before an event in case there are unforeseen bugs).
|
||||
- Test out PhotonVision at your home setup.
|
||||
- Ensure that you have set up SmartDashboard / Shuffleboard to view your camera streams during matches.
|
||||
- Follow all the recommendations under the Networking section in installation (network switch and static IP).
|
||||
- Use high quality ethernet cables that have been rigorously tested.
|
||||
- Set up port forwarding using the guide in the Networking section in installation.
|
||||
- Stay on the latest version of PhotonVision until you have tested your full robot system to be functional.
|
||||
- Some time before the competition, lock down the version you are using and do not upgrade unless you encounter a critical bug.
|
||||
- Have a copy of the installation image for the version you are using on your programming laptop, in case re-imaging (without internet) is needed.
|
||||
- Extensively test at your home setup. Practice tuning from scratch under different lighting conditions.
|
||||
- Use SmartDashboard / Shuffleboard to view your camera streams during practice.
|
||||
- Confirm you have followed all the recommendations under the Networking section in installation (network switch and static IP).
|
||||
- Only use high quality ethernet cables that have been rigorously tested.
|
||||
- Set up RIO USB port forwarding using the guide in the Networking section in installation.
|
||||
|
||||
## During the Competition
|
||||
|
||||
- Make sure you take advantage of the field calibration time given at the start of the event:
|
||||
- Bring your robot to the field at the allotted time.
|
||||
- Turn on your robot and pull up the dashboard on your driver station.
|
||||
- Point your robot at the AprilTags(s) and ensure you get a consistent tracking (you hold one AprilTag consistently, the ceiling lights aren't detected, etc.).
|
||||
- If you have problems with your pipeline, go to the pipeline tuning section and retune the pipeline using the guide there.
|
||||
- Move the robot close, far, angled, and around the field to ensure no extra AprilTags are found.
|
||||
- Go to a practice match to ensure everything is working correctly.
|
||||
- Use the field calibration time given at the start of the event:
|
||||
- Bring your robot to the field at the allotted time.
|
||||
- Make sure the field has match-accurate lighting conditions active.
|
||||
- Turn on your robot and pull up the dashboard on your driver station.
|
||||
- Point your robot at the targets and ensure you get a consistent tracking (you hold one targets consistently, the ceiling lights aren't detected, etc.).
|
||||
- If you have problems with your pipeline, go to the pipeline tuning section and retune the pipeline using the guide there.
|
||||
- Move the robot close, far, angled, and around the field to ensure no extra targets are found.
|
||||
- Monitor camera feeds during a practice match to ensure everything is working correctly.
|
||||
- After field calibration, use the "Export Settings" button in the "Settings" page to create a backup.
|
||||
- Do this for each coprocessor on your robot that runs PhotonVision, and name your exports with meaningful names.
|
||||
- This will contain camera information/calibration, pipeline information, network settings, etc.
|
||||
- In the event of software/hardware failures (IE lost SD Card, broken device), you can then use the "Import Settings" button and select "All Settings" to restore your settings.
|
||||
- This effectively works as a snapshot of your PhotonVision data that can be restored at any point.
|
||||
- Before every match, check the ethernet connection going into your coprocessor and that it is seated fully.
|
||||
- Ensure that exposure is as low as possible and that you don't have the dashboard up when you don't need it to reduce bandwidth.
|
||||
- Do this for each coprocessor on your robot that runs PhotonVision, and name your exports with meaningful names.
|
||||
- This will contain camera information/calibration, pipeline information, network settings, etc.
|
||||
- In the event of software/hardware failures (IE lost SD Card, broken device), you can then use the "Import Settings" button and select "All Settings" to restore your settings.
|
||||
- This effectively works as a snapshot of your PhotonVision data that can be restored at any point.
|
||||
- Before every match:
|
||||
- Check the ethernet and USB connectors are seated fully.
|
||||
- Close streaming dashboards when you don't need them to reduce bandwidth.
|
||||
- Stream at as low of a resolution as possible while still detecting AprilTags to stay within field bandwidth limits.
|
||||
|
||||
@@ -48,3 +48,6 @@ A variety of files can be imported back into PhotonVision:
|
||||
- {code}`hardwareSettings.json`
|
||||
- {code}`networkSettings.json`
|
||||
- Useful for simple hardware or network configuration tasks without overwriting all settings.
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
# Installing Pre-Release Versions
|
||||
|
||||
Pre-release/development version of PhotonVision can be tested by installing/downloading artifacts from Github Actions (see below), which are built automatically on commits to open pull requests and to PhotonVision's `master` branch, or by {ref}`compiling PhotonVision locally <docs/contributing/building-photon:Build Instructions>`.
|
||||
Pre-release/development version of PhotonVision can be tested by installing/downloading artifacts from Github Actions (see below), which are built automatically on commits to open pull requests and to PhotonVision's `main` branch, or by {ref}`compiling PhotonVision locally <docs/contributing/building-photon:Build Instructions>`.
|
||||
|
||||
:::{warning}
|
||||
If testing a pre-release version of PhotonVision with a robot, PhotonLib must be updated to match the version downloaded! If not, packet schema definitions may not match and unexpected things will occur. To update PhotonLib, refer to {ref}`installing specific version of PhotonLib<docs/programming/photonlib/adding-vendordep:Install Specific Version - Java/C++>`.
|
||||
:::
|
||||
|
||||
GitHub Actions builds pre-release version of PhotonVision automatically on PRs and on each commit merged to master. To test a particular commit to master, navigate to the [PhotonVision commit list](https://github.com/PhotonVision/photonvision/commits/master/) and click on the check mark (below). Scroll to "Build / Build fat JAR - PLATFORM", click details, and then summary. From here, JAR and image files can be downloaded to be flashed or uploaded using "Offline Update".
|
||||
GitHub Actions builds pre-release version of PhotonVision automatically on PRs and on each commit merged to main. To test a particular commit to main, navigate to the [PhotonVision commit list](https://github.com/PhotonVision/photonvision/commits/main/) and click on the check mark (below). Scroll to "Build / Build fat JAR - PLATFORM", click details, and then summary. From here, JAR and image files can be downloaded to be flashed or uploaded using "Offline Update".
|
||||
|
||||
```{image} images/gh_actions_1.png
|
||||
:alt: Github Actions Badge
|
||||
|
||||
@@ -8,14 +8,14 @@ You do not need to install PhotonVision on a Windows PC in order to access the w
|
||||
|
||||
## Installing Java
|
||||
|
||||
PhotonVision requires a JDK installed and on the system path. JDK 11 is needed (different versions will not work). If you don't have JDK 11 already, run the following to install it:
|
||||
PhotonVision requires a JDK installed and on the system path. JDK 17 is needed (different versions will not work). If you don't have JDK 17 already, run the following to install it:
|
||||
|
||||
```
|
||||
$ sudo apt-get install openjdk-11-jdk
|
||||
$ sudo apt-get install openjdk-17-jdk
|
||||
```
|
||||
|
||||
:::{warning}
|
||||
Using a JDK other than JDK11 will cause issues when running PhotonVision and is not supported.
|
||||
Using a JDK other than JDK17 will cause issues when running PhotonVision and is not supported.
|
||||
:::
|
||||
|
||||
## Downloading the Latest Stable Release of PhotonVision
|
||||
|
||||
@@ -5,17 +5,17 @@ Due to current [cscore](https://github.com/wpilibsuite/allwpilib/tree/main/cscor
|
||||
:::
|
||||
|
||||
:::{note}
|
||||
You do not need to install PhotonVision on a Windows PC in order to access the webdashboard (assuming you are using an external coprocessor like a Raspberry Pi).
|
||||
You do not need to install PhotonVision on a Mac in order to access the webdashboard (assuming you are using an external coprocessor like a Raspberry Pi).
|
||||
:::
|
||||
|
||||
VERY Limited macOS support is available.
|
||||
|
||||
## Installing Java
|
||||
|
||||
PhotonVision requires a JDK installed and on the system path. JDK 11 is needed (different versions will not work). You may already have this if you have installed WPILib. If not, [download and install it from here](https://adoptium.net/temurin/releases?version=11).
|
||||
PhotonVision requires a JDK installed and on the system path. JDK 17 is needed (different versions will not work). You may already have this if you have installed WPILib 2025+. If not, [download and install it from here](https://adoptium.net/temurin/releases?version=17).
|
||||
|
||||
:::{warning}
|
||||
Using a JDK other than JDK11 will cause issues when running PhotonVision and is not supported.
|
||||
Using a JDK other than JDK17 will cause issues when running PhotonVision and is not supported.
|
||||
:::
|
||||
|
||||
## Downloading the Latest Stable Release of PhotonVision
|
||||
|
||||
@@ -15,12 +15,12 @@ SSH into the Raspberry Pi (using Windows command line, or a tool like [Putty](ht
|
||||
:::{attention}
|
||||
The version of WPILibPi for the Romi is 2023.2.1, which is not compatible with the current version of PhotonVision. **If you are using WPILibPi 2023.2.1 on your Romi, you must install PhotonVision v2023.4.2 or earlier!**
|
||||
|
||||
To install a compatible version of PhotonVision, enter these commands in the SSH terminal connected to the Raspberry Pi. This will download and run the install script, which will intall PhotonVision on your Raspberry Pi and configure it to run at startup.
|
||||
To install a compatible version of PhotonVision, enter these commands in the SSH terminal connected to the Raspberry Pi. This will download and run the install script, which will install PhotonVision on your Raspberry Pi and configure it to run at startup.
|
||||
|
||||
```bash
|
||||
$ wget https://git.io/JJrEP -O install.sh
|
||||
$ sudo chmod +x install.sh
|
||||
$ sudo ./install.sh -v 2023.4.2
|
||||
$ sudo ./install.sh -v v2023.4.2
|
||||
```
|
||||
The install script requires an internet connection, so connecting the Raspberry Pi to an internet-connected router via an Ethernet cable will be the easiest solution. The pi must remain writable while you are following these steps!
|
||||
:::
|
||||
@@ -32,7 +32,7 @@ Next, from the SSH terminal, run `sudo nano /home/pi/runCamera` then arrow down
|
||||
|
||||
```
|
||||
|
||||
After the Romi reboots, you should be able to open the PhotonVision UI at: [`http://10.0.0.2:5800/`](http://10.0.0.2:5800/). From here, you can adjust {ref}`Settings <docs/settings:Settings>` and configure {ref}`Pipelines <docs/pipelines/index:Pipelines>`.
|
||||
After the Romi reboots, you should be able to open the PhotonVision UI at: [`http://10.0.0.2:5800/`](http://10.0.0.2:5800/). From here, you can adjust settings and configure {ref}`Pipelines <docs/pipelines/index:Pipelines>`.
|
||||
|
||||
:::{warning}
|
||||
In order for settings, logs, etc. to be saved / take effect, ensure that PhotonVision is in writable mode.
|
||||
|
||||
@@ -12,10 +12,14 @@ Bonjour provides more stable networking when using Windows PCs. Install [Bonjour
|
||||
|
||||
## Installing Java
|
||||
|
||||
PhotonVision requires a JDK installed and on the system path. **JDK 11 is needed** (different versions will not work). You may already have this if you have installed WPILib, but ensure that running `java -version` shows JDK 11. If not, [download and install it from here](https://adoptium.net/temurin/releases?version=11) and ensure that the new JDK is being used.
|
||||
PhotonVision requires a JDK installed and on the system path. **JDK 17 is needed. Windows Users must use the JDK that ships with WPILib.** [Download and install it from here.](https://github.com/wpilibsuite/allwpilib/releases/tag/v2025.3.1) Either ensure the only Java on your PATH is the WPILIB Java or specify it to gradle with `-Dorg.gradle.java.home=C:\Users\Public\wpilib\2025\jdk`:
|
||||
|
||||
```
|
||||
> ./gradlew run "-Dorg.gradle.java.home=C:\Users\Public\wpilib\2025\jdk"
|
||||
```
|
||||
|
||||
:::{warning}
|
||||
Using a JDK other than JDK11 will cause issues when running PhotonVision and is not supported.
|
||||
Using a JDK other than WPILIB's JDK17 will cause issues when running PhotonVision and is not supported.
|
||||
:::
|
||||
|
||||
## Downloading the Latest Stable Release of PhotonVision
|
||||
|
||||
@@ -21,7 +21,9 @@ AprilTag pipelines come with reasonable defaults to get you up and running with
|
||||
|
||||
### Target Family
|
||||
|
||||
Target families are defined by two numbers (before and after the h). The first number is the number of bits the tag is able to encode (which means more tags are available in the respective family) and the second is the hamming distance. Hamming distance describes the ability for error correction while identifying tag ids. A high hamming distance generally means that it will be easier for a tag to be identified even if there are errors. However, as hamming distance increases, the number of available tags decreases. The 2024 FRC game will be using 36h11 tags, which can be found [here](https://github.com/AprilRobotics/apriltag-imgs/tree/master/tag36h11).
|
||||
Target families are defined by two numbers (before and after the h). The first number is the number of bits the tag is able to encode (which means more tags are available in the respective family) and the second is the hamming distance. Hamming distance describes the ability for error correction while identifying tag ids. A high hamming distance generally means that it will be easier for a tag to be identified even if there are errors. However, as hamming distance increases, the number of available tags decreases.
|
||||
|
||||
The 2025 FRC game will be using 36h11 tags, which can be found [here](https://github.com/AprilRobotics/apriltag-imgs/tree/main/tag36h11).
|
||||
|
||||
### Decimate
|
||||
|
||||
|
||||
@@ -10,5 +10,5 @@ AprilTags are a common type of visual fiducial marker. Visual fiducial markers a
|
||||
A more technical explanation can be found in the [WPILib documentation](https://docs.wpilib.org/en/latest/docs/software/vision-processing/apriltag/apriltag-intro.html).
|
||||
|
||||
:::{note}
|
||||
You can get FIRST's [official PDF of the targets used in 2024 here](https://firstfrc.blob.core.windows.net/frc2024/FieldAssets/Apriltag_Images_and_User_Guide.pdf).
|
||||
You can get FIRST's [official PDF of the targets used in 2025 here](https://firstfrc.blob.core.windows.net/frc2025/FieldAssets/Apriltag_Images_and_User_Guide.pdf).
|
||||
:::
|
||||
|
||||
@@ -12,4 +12,4 @@ The AprilTag pipeline type is based on the [AprilTag](https://april.eecs.umich.e
|
||||
|
||||
## AruCo
|
||||
|
||||
The AruCo pipeline is based on the [AruCo](https://docs.opencv.org/4.8.0/d9/d6a/group__aruco.html) library implementation from OpenCV. It is ~2x higher fps and ~2x lower latency than the AprilTag pipeline type, but is less accurate. We recommend this pipeline type for teams that need to run at a higher framerate or have a lower powered device. This pipeline type is new for the 2024 season and is not as well tested as AprilTag.
|
||||
The AruCo pipeline is based on the [AruCo](https://docs.opencv.org/4.8.0/d9/d6a/group__aruco.html) library implementation from OpenCV. It is ~2x higher fps and ~2x lower latency than the AprilTag pipeline type, but is less accurate. We recommend this pipeline type for teams that need to run at a higher framerate or have a lower powered device. This pipeline type was new for the 2024 season.
|
||||
|
||||
@@ -6,6 +6,10 @@ PhotonVision can combine AprilTag detections from multiple simultaneously observ
|
||||
MultiTag requires an accurate field layout JSON to be uploaded! Differences between this layout and the tags' physical location will drive error in the estimated pose output.
|
||||
:::
|
||||
|
||||
:::{warning}
|
||||
For the 2025 Reefscape Season, there are two different field layouts. The first is the [welded field layout](https://github.com/wpilibsuite/allwpilib/blob/main/apriltag/src/main/native/resources/edu/wpi/first/apriltag/2025-reefscape-welded.json), which photonvision ships with. The second is the [Andymark field layout](https://github.com/wpilibsuite/allwpilib/blob/main/apriltag/src/main/native/resources/edu/wpi/first/apriltag/2025-reefscape-andymark.json). It is very important to ensure that you use the correct field layout, both in the [PhotonPoseEstimator](https://docs.photonvision.org/en/latest/docs/programming/photonlib/robot-pose-estimator.html#apriltags-and-photonposeestimator) and on the [coprocessor](https://docs.photonvision.org/en/latest/docs/apriltag-pipelines/multitag.html#updating-the-field-layout).
|
||||
:::
|
||||
|
||||
## Enabling MultiTag
|
||||
|
||||
Ensure that your camera is calibrated and 3D mode is enabled. Navigate to the Output tab and enable "Do Multi-Target Estimation". This enables MultiTag to use the uploaded field layout JSON to calculate your camera's pose in the field. This 3D transform will be shown as an additional table in the "targets" tab, along with the IDs of AprilTags used to compute this transform.
|
||||
@@ -51,7 +55,7 @@ The returned field to camera transform is a transform from the fixed field origi
|
||||
|
||||
## Updating the Field Layout
|
||||
|
||||
PhotonVision ships by default with the [2024 field layout JSON](https://github.com/wpilibsuite/allwpilib/blob/main/apriltag/src/main/native/resources/edu/wpi/first/apriltag/2024-crescendo.json). The layout can be inspected by navigating to the settings tab and scrolling down to the "AprilTag Field Layout" card, as shown below.
|
||||
PhotonVision ships by default with the [2025 welded field layout JSON](https://github.com/wpilibsuite/allwpilib/blob/main/apriltag/src/main/native/resources/edu/wpi/first/apriltag/2025-reefscape-welded.json). The layout can be inspected by navigating to the settings tab and scrolling down to the "AprilTag Field Layout" card, as shown below.
|
||||
|
||||
```{image} images/field-layout.png
|
||||
:alt: The currently saved field layout in the Photon UI
|
||||
|
||||
@@ -51,6 +51,10 @@ We'll next select a resolution to calibrate and populate our pattern spacing, ma
|
||||
:::{warning} Old OpenCV Pattern selector. This should be used in the case that the calibration image is generated from a version of OpenCV before version 4.6.0. This would include targets created by calib.io. If this selector is not set correctly the calibration will be completely invalid. For more info view [this GitHub issue](https://github.com/opencv/opencv_contrib/issues/3291).
|
||||
:::
|
||||
|
||||
:::{note}
|
||||
If you have a [calib.io](https://calib.io/) CharuCo Target you will have to enter the paramaters of your target. For example if your taget says "9x12 | Chceker Size: 30 mm | Marker Size: 22 mm | Dictionary: AruCo DICT 5x5", you would have to set the board type to Dict_5x5_1000, the pattern spacing to 1.1811 in (30 mm converted to inches), the marker size 0.866142 in (22 mm converted to inches), the board width to 12 and the board height to 9. If you chose the wrong tag family the baord wont be detected duting calibration. If you swap the width and height your calibration will have a very high error.
|
||||
:::
|
||||
|
||||
### 4. Take at calibration images from various angles.
|
||||
|
||||
Now, we'll capture images of our board from various angles. It's important to check that the board overlay matches the board in your image. The further the overdrawn points are from the true position of the chessboard corners, the less accurate the final calibration will be. We'll want to capture enough images to cover the whole camera's FOV (with a minimum of 12). Once we've got our images, we'll click "Finish calibration" and wait for the calibration process to complete. If all goes well, the mean error and FOVs will be shown in the table on the right. The FOV should be close to the camera's specified FOV (usually found in a datasheet) usually within + or - 10 degrees. The mean error should also be low, usually less than 1 pixel.
|
||||
@@ -83,7 +87,7 @@ More info on what these parameters mean can be found in [OpenCV's docs](https://
|
||||
|
||||
Below these outputs are the snapshots collected for calibration, along with a per-snapshot mean reprojection error. A snapshot with a larger reprojection error might indicate a bad snapshot, due to effects such as motion blur or misidentified chessboard corners.
|
||||
|
||||
Calibration images can also be extracted from the downloaded JSON file using [this Python script](https://raw.githubusercontent.com/PhotonVision/photonvision/master/devTools/calibrationUtils.py). This script will unpack calibration images, and also generate a VNL file for use [with mrcal](https://mrcal.secretsauce.net/).
|
||||
Calibration images can also be extracted from the downloaded JSON file using [this Python script](https://raw.githubusercontent.com/PhotonVision/photonvision/main/devTools/calibrationUtils.py). This script will unpack calibration images, and also generate a VNL file for use [with mrcal](https://mrcal.secretsauce.net/).
|
||||
|
||||
```
|
||||
python3 /path/to/calibrationUtils.py path/to/photon_calibration.json /path/to/output/folder
|
||||
|
||||
BIN
docs/source/docs/contributing/assets/vscode-gradle-args.png
Normal file
BIN
docs/source/docs/contributing/assets/vscode-gradle-args.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 87 KiB |
BIN
docs/source/docs/contributing/assets/vscode-gradle-tests.png
Normal file
BIN
docs/source/docs/contributing/assets/vscode-gradle-tests.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 42 KiB |
BIN
docs/source/docs/contributing/assets/vscode-runner-tests.png
Normal file
BIN
docs/source/docs/contributing/assets/vscode-runner-tests.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 334 KiB |
@@ -167,30 +167,31 @@ repositories {
|
||||
}
|
||||
```
|
||||
|
||||
### VSCode Test Runner Extension
|
||||
|
||||
With the VSCode [Extension Pack for Java](https://marketplace.visualstudio.com/items?itemName=vscjava.vscode-java-pack), you can get the Test Runner for Java and Gradle for Java extensions. This lets you easily run specific tests through the IDE:
|
||||
|
||||
```{image} assets/vscode-runner-tests.png
|
||||
:alt: An image showing how unit tests can be ran in VSCode through the Test Runner for Java extension.
|
||||
```
|
||||
|
||||
To correctly run PhotonVision tests this way, you must [delegate the tests to Gradle](https://code.visualstudio.com/docs/java/java-build#_delegate-tests-to-gradle). Debugging tests like this will [**not** currently](https://github.com/microsoft/build-server-for-gradle/issues/119) collect outputs.
|
||||
|
||||
### Debugging PhotonVision Running Locally
|
||||
|
||||
One way is by running the program using gradle with the {code}`--debug-jvm` flag. Run the program with {code}`./gradlew run --debug-jvm`, and attach to it with VSCode by adding the following to {code}`launch.json`. Note args can be passed with {code}`--args="foobar"`.
|
||||
Unit tests can instead be debugged through the ``test`` Gradle task for a specific subproject in VSCode, found in the Gradle tab:
|
||||
|
||||
```
|
||||
{
|
||||
// Use IntelliSense to learn about possible attributes.
|
||||
// Hover to view descriptions of existing attributes.
|
||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"type": "java",
|
||||
"name": "Attach to Remote Program",
|
||||
"request": "attach",
|
||||
"hostName": "localhost",
|
||||
"port": "5005",
|
||||
"projectName": "photon-core",
|
||||
}
|
||||
]
|
||||
}
|
||||
```{image} assets/vscode-gradle-tests.png
|
||||
:alt: An image showing how unit tests can be debugged in VSCode through the Gradle for Java extension.
|
||||
```
|
||||
|
||||
PhotonVision can also be run using the gradle tasks plugin with {code}`"args": "--debug-jvm"` added to launch.json.
|
||||
However, this will run all tests in a subproject.
|
||||
|
||||
Similarly, a local instance of PhotonVision can be debugged in the same way using the Gradle ``run`` task. In both cases, additional arguments can be specified:
|
||||
|
||||
```{image} assets/vscode-gradle-args.png
|
||||
:alt: An image showing how VSCode gradle tasks can specify additional arguments.
|
||||
```
|
||||
|
||||
### Debugging PhotonVision Running on a CoProcessor
|
||||
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
# Camera Matching
|
||||
|
||||
Diagrams generated by the [PlantUML UML editor](https://www.plantuml.com/plantuml/). Copy the image URLs below and decode in the editor to make changes.
|
||||
|
||||
## Initial Setup
|
||||
|
||||
When PhotonVision first starts, settings are loaded from disk and [VisionSources](https://javadocs.photonvision.org/org/photonvision/vision/processes/VisionSource.html) are created for every serialized & active [Camera Configuration](https://javadocs.photonvision.org/org/photonvision/common/configuration/CameraConfiguration.html)
|
||||
|
||||

|
||||
|
||||
## UI Workflow
|
||||
|
||||
A [background thread](https://javadocs.photonvision.org/org/photonvision/common/util/TimedTaskManager.html) will periodically query CSCore and Libcamera for what cameras we currently see connected. This list is provided to the web UI for display.
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
This UI allows users to "Activate" a camera that's never been seen before, or activate a CameraConfiguration we've seen before but was disabled. Allowing camera configurations to be saved but not loaded by default lets us support temporarily disabling/unplugging a camera without flooding log files.
|
||||
|
||||
Since our backend logic intentionally does not protect users from plugging camera B into the port that camera A was active on, the UI shall show a warning but vision processing will (attempt to) continue like normal.
|
||||
|
||||
### Activate New Camera
|
||||
|
||||
When a new camera (ie, one we can't match by-path to a deserialized CameraConfiguration) is activated, we'll create a spin up a new Vision Module for it
|
||||
|
||||

|
||||
|
||||
### Deactivate Camera
|
||||
|
||||
Deactivating a camera will release the native resources it owns, and return the CameraConfiguration to the pool of currently disabled cameras we can re-enable later.
|
||||
|
||||

|
||||
|
||||
### Reactivate a CameraConfig
|
||||
|
||||
When a new camera (ie, one we can't match by-path to a deserialized CameraConfiguration) is activated, we'll create and spin up a new Vision Module for it.
|
||||
|
||||

|
||||
|
||||
# Camera Matching Requirements
|
||||
|
||||
## Definitions
|
||||
- VALID USB PATH: a path in the form `/dev/v4l/by-path/[UUID]`
|
||||
- VIDEO DEVICE PATH: a CSCore-provided identifier derived from the V4L path `/dev/video[N]` on Linux, or an opaque string on Windows
|
||||
- UNIQUE NAME: an identifier that is unique within the set of all deserialized CameraConfigurations and unmatched USB cameras
|
||||
- I don't love this, it means that a USB camera matched to a VisionModule will share a UNIQUE NAME, right?
|
||||
- DESERIALIZED CAMERA CONFIGURATIONS: The set of camera configurations loaded from disk and provided to the VisionSourceManager. This configuration data structure includes the UNIQUE NAME
|
||||
- CURRENTLY ACTIVE CAMERAS: The set of VisionModules currently active and processing vision data, and associated metadata
|
||||
|
||||
## Startup:
|
||||
|
||||
- GIVEN An empty set of deserialized Camera Configurations
|
||||
<br>WHEN PhotonVision starts
|
||||
<br>THEN no VisionModules will be started
|
||||
|
||||
- GIVEN A valid set of deserialized Camera Configurations
|
||||
<br>WHEN PhotonVision starts
|
||||
<br>THEN VisionModules will be started FOR EACH un-DISABLED config
|
||||
|
||||
- GIVEN A valid set of deserialized Camera Configurations
|
||||
<br>WHEN PhotonVision starts
|
||||
<br>THEN VisionModules will NOT be started FOR EACH DISABLED config
|
||||
|
||||
- GIVEN A CameraConfiguration with a VALID USB PATH
|
||||
<br>WHEN a VisionModule is created
|
||||
<br>THEN The VisionModule shall open the camera using the USB path
|
||||
|
||||
- GIVEN A CameraConfiguration without a valid USB path
|
||||
<br>WHEN a VisionModule is created
|
||||
<br>THEN The VisionModule shall open the camera using the VIDEO DEVICE PATH
|
||||
|
||||
## Camera (re)enumeration:
|
||||
|
||||
- GIVEN a NEW USB CAMERA is available for enumeration
|
||||
<br>WHEN a USB camera is discovered by VisionSourceManager
|
||||
<br>AND the USB camera's VIDEO DEVICE PATH is not in the set of DESERIALIZED CAMERA CONFIGURATIONS
|
||||
<br>THEN a UNIQUE NAME will be assigned to the camera info
|
||||
|
||||
- GIVEN a NEW USB CAMERA is available for enumeration
|
||||
<br>WHEN a USB camera is discovered by VisionSourceManager
|
||||
<br>AND the USB camera's VIDEO DEVICE PATH is in the set of DESERIALIZED CAMERA CONFIGURATIONS
|
||||
<br>THEN a UNIQUE NAME equal to the matching DESERIALIZED CAMERA CONFIGURATION will be assigned to the camera info
|
||||
- This is a weird case. How -should- we handle this? see above
|
||||
|
||||
## Creating from a new camera
|
||||
|
||||
- Given: A UNIQUE NAME from a NEW USB CAMERA
|
||||
<br>WHEN I request a new VisionModule is created for this NEW USB CAMERA
|
||||
<br>AND the camera has a VALID USB PATH
|
||||
<br>AND the camera's VALID USB PATH is not in use by any CURRENTLY ACTIVE CAMERAS
|
||||
<br>THEN a NEW VisionModule will be started for the NEW USB CAMERA using the VALID USB PATH
|
||||
|
||||
- Given: A UNIQUE NAME from a NEW USB CAMERA
|
||||
<br>WHEN I request a new VisionModule is created for this NEW USB CAMERA
|
||||
<br>AND the camera does not have a VALID USB PATH
|
||||
<br>AND the camera's VIDEO DEVICE PATH is not in use by any CURRENTLY ACTIVE CAMERAS
|
||||
<br>THEN a NEW VisionModule will be started for the NEW USB CAMERA using the VIDEO DEVICE PATH
|
||||
|
||||
## Deactivate
|
||||
|
||||
- Given: A UNIQUE NAME from a CURRENTLY ACTIVE CAMERA
|
||||
<br>WHEN I request the VisionModule be DEACTIVATED
|
||||
<br>THEN the VisionModule will be stopped for the given CURRENTLY ACTIVE CAMERA
|
||||
<br>AND the CameraConfiguration DISABLED flag will be set to TRUE
|
||||
|
||||
## Reactivate
|
||||
|
||||
- Given: A UNIQUE NAME from a DESERIALIZED CAMERA CONFIGURATIONS
|
||||
<br>WHEN I request the VisionModule be ACTIVATED
|
||||
<br>AND the CameraConfiguration's DISABLED flag is TRUE
|
||||
<br>THEN a VisionModule will be created and started for the camera
|
||||
136
docs/source/docs/contributing/design-descriptions/e2e-latency.md
Normal file
136
docs/source/docs/contributing/design-descriptions/e2e-latency.md
Normal file
@@ -0,0 +1,136 @@
|
||||
# Latency Characterization
|
||||
|
||||
|
||||
## A primer on time
|
||||
|
||||
Especially starting around 2022 with AprilTags making localization easier, providing a way to know when a camera image was captured at became more important for localization.
|
||||
Since the [creation of USBFrameProvider](https://github.com/PhotonVision/photonvision/commit/f92bf670ded52b59a00352a4a49c277f01bae305), we used the time [provided by CSCore](https://github.wpilib.org/allwpilib/docs/release/java/edu/wpi/first/cscore/CvSink.html#grabFrame(org.opencv.core.Mat)) to tell when a camera image was captured at, but just keeping track of "CSCore told us frame N was captured 104.21s after the Raspberry Pi turned on" isn't very helpful. We can decompose this into asking:
|
||||
|
||||
- At what time was a particular image captured at, in the coprocessor's timebase?
|
||||
- How do I convert a time in a coprocessor's timebase into the RoboRIO's timebase, so I can integrate the measurement with my other sensor measurements (like encoders)?
|
||||
|
||||
The first one seems easy - CSCore tells us the time, so just keep track of that? Should be easy. For the second, translating this time, as measured by the coprocessor's clock, into a timebase also used by user code on the RoboRIO, is actually a [fairly hard problem](time-sync.md) that involved reinventing [PTP](https://en.wikipedia.org/wiki/PTP).
|
||||
|
||||
And on latency vs timestamps - PhotonVision has exposed a magic "latency" number since forever, but latency (as in, the time from image capture to acting on data) can be useful for benchmarking code, but robots actually want to answer "what time was this image from, relative to "?
|
||||
|
||||
|
||||
## CSCore's Frame Time
|
||||
|
||||
WPILib's CSCore is a platform-agnostic wrapper around Windows, Linux, and MacOS camera APIs. On Linux, CSCore uses [Video4Linux](https://en.wikipedia.org/wiki/Video4Linux) to access USB Video Class (UVC) devices like webcams, as well as CSI cameras on some platforms. At a high level, CSCore's [Linux USB Camera driver](https://github.com/wpilibsuite/allwpilib/blob/17a03514bad6de195639634b3d57d5ac411d601e/cscore/src/main/native/linux/UsbCameraImpl.cpp) works by:
|
||||
|
||||
- Opening a camera with `open`
|
||||
- Creating and `mmap`ing a handful of buffers V4L will fill with frame data into program memory
|
||||
- Asking V4L to start streaming
|
||||
- While the camera is running:
|
||||
- Wait for new frames
|
||||
- Dequeue one buffer
|
||||
- Call `SourceImpl::PutFrame`, which will copy the image out and convert as needed
|
||||
- Return the buffer to V4L to fill again
|
||||
|
||||
Prior to https://github.com/wpilibsuite/allwpilib/pull/7609, CSCore used the [time it dequeued the buffer at](https://github.com/wpilibsuite/allwpilib/blob/17a03514bad6de195639634b3d57d5ac411d601e/cscore/src/main/native/linux/UsbCameraImpl.cpp#L559) as the image capture time. But this doesn't account for exposure time or latency introduced by the camera + USB stack + Linux itself.
|
||||
|
||||
V4L does expose (with some [very heavy caveats](https://github.com/torvalds/linux/blob/fc033cf25e612e840e545f8d5ad2edd6ba613ed5/drivers/media/usb/uvc/uvc_video.c#L600) for some troublesome cameras) its best guess at the time an image was captured at via [buffer flags](https://www.kernel.org/doc/html/v4.9/media/uapi/v4l/buffer.html#buffer-flags). In my testing, all my cameras were able to provide timestamps with both these flags set:
|
||||
- `V4L2_BUF_FLAG_TIMESTAMP_MONOTONIC`: The buffer timestamp has been taken from the CLOCK_MONOTONIC clock [...] accessible via `clock_gettime()`.
|
||||
- `V4L2_BUF_FLAG_TSTAMP_SRC_SOE`: Start Of Exposure. The buffer timestamp has been taken when the exposure of the frame has begun.
|
||||
|
||||
I'm sure that we'll find a camera that doesn't play nice, because we can't have nice things :). But until then, using this timestamp gets us a free accuracy bump.
|
||||
|
||||
Other things to note: This gets us an estimate at when the camera *started* collecting photons. The camera's sensor will remain collecting light for up to the total integration time, plus readout time for rolling shutter cameras.
|
||||
|
||||
## Latency Testing
|
||||
|
||||
Here, I've got a RoboRIO with an LED, an Orange Pi 5, and a network switch on a test bench. The LED is assumed to turn on basically instantly once we apply current, and based on DMA testing, the total time to switch a digital output on is on the order of 10uS. The RoboRIO is running a TimeSync Server, and the Orange Pi is running a TimeSync Client.
|
||||
|
||||
### Test Setup
|
||||
|
||||
<details>
|
||||
<summary>Show RoboRIO Test Code</summary>
|
||||
|
||||
```java
|
||||
package frc.robot;
|
||||
|
||||
import org.photonvision.PhotonCamera;
|
||||
|
||||
import edu.wpi.first.wpilibj.DigitalOutput;
|
||||
import edu.wpi.first.wpilibj.TimedRobot;
|
||||
import edu.wpi.first.wpilibj.Timer;
|
||||
import edu.wpi.first.wpilibj.smartdashboard.SmartDashboard;
|
||||
|
||||
public class Robot extends TimedRobot {
|
||||
PhotonCamera camera;
|
||||
DigitalOutput light;
|
||||
|
||||
@Override
|
||||
public void robotInit() {
|
||||
camera = new PhotonCamera("Arducam_OV9782_USB_Camera");
|
||||
|
||||
light = new DigitalOutput(0);
|
||||
light.set(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void robotPeriodic() {
|
||||
super.robotPeriodic();
|
||||
|
||||
try {
|
||||
light.set(false);
|
||||
for (int i = 0; i < 50; i++) {
|
||||
Thread.sleep(20);
|
||||
camera.getAllUnreadResults();
|
||||
}
|
||||
|
||||
var t1 = Timer.getFPGATimestamp();
|
||||
light.set(true);
|
||||
var t2 = Timer.getFPGATimestamp();
|
||||
|
||||
|
||||
for (int i = 0; i < 100; i++) {
|
||||
for (var result : camera.getAllUnreadResults()) {
|
||||
if (result.hasTargets()) {
|
||||
var t3 = result.getTimestampSeconds();
|
||||
var t1p5 = (t1 + t2) / 2;
|
||||
var error = t3-t1p5;
|
||||
SmartDashboard.putNumber("blink_error_ms", error * 1000);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
Thread.sleep(20);
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
</details>
|
||||
|
||||
I've decreased camera exposure as much as possible (so we know with reasonable confidence that the image was collected right at the start of the exposure time reported by V4L), but we only get back new images at 60fps. So we don't know when between frame N and N+1 the LED turned on - just that sometime between now and 1/60th of a second a go, the LED turned on.
|
||||
|
||||
The test coprocessor was an Orange Pi 5 running a PhotonVision 2025 (Ubuntu 24.04 based) image, with an ArduCam OV9782 at 1280x800, 60fps, MJPG running a reflective pipeline.
|
||||
|
||||
|
||||
### Test Results
|
||||
|
||||
The videos above show the difference between when the RoboRIO turned the LED on and when PhotonVision first seeing a camera frame with the LED on, what I've called error and plotted in yellow with units of seconds. This error decreases when I use the frame time reported by V4L from a mean delta of 26 ms to a mean delta of 11 ms (below the maximum temporal resolution of my camera).
|
||||
|
||||
Old CSCore:
|
||||
```{raw} html
|
||||
<video width="85%" controls>
|
||||
<source src="../../../_static/assets/latency-tests/ov9782_1280x720x60xMJPG_old.mp4" type="video/mp4">
|
||||
Your browser does not support the video tag.
|
||||
</video>
|
||||
```
|
||||
CSCore using V4L frame time:
|
||||
```{raw} html
|
||||
<video width="85%" controls>
|
||||
<source src="../../../_static/assets/latency-tests/ov9782_1280x720x60xMJPG_new.mp4" type="video/mp4">
|
||||
Your browser does not support the video tag.
|
||||
</video>
|
||||
```
|
||||
|
||||
With the camera capturing at 60fps, the time between successive frames is only ~16.7 ms, so I don't expect to be able to resolve anything smaller. Given sufficient time and with perfect latency compensation, and with more noise in the robot program to make sure we vary LED toggle times, I'd expect the error to converge to ~half the interval between frames - so being within this frame interval with CSCore updates is a very good sign.
|
||||
|
||||
### Future Work
|
||||
|
||||
This test also makes no effort to isolate error from time synchronization from error introduced by frame time measurement - we're just interested in overall error. Future work could investigate the latency contribution
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 215 KiB |
@@ -4,4 +4,6 @@
|
||||
:maxdepth: 1
|
||||
image-rotation
|
||||
time-sync
|
||||
camera-matching
|
||||
e2e-latency
|
||||
```
|
||||
|
||||
@@ -76,7 +76,7 @@ Communication between server and clients shall occur over the User Datagram Prot
|
||||
|
||||
## Message Format
|
||||
|
||||
The message format forgoes CRCs (as these are provided by the Ethernet physical layer) or packet delimination (as our packetsa are assumed be under the network MTU). **TSP Ping** and **TSP Pong** messages shall be encoded in a manor compatible with a WPILib packed struct with respect to byte alignment and endienness.
|
||||
The message format forgoes CRCs (as these are provided by the Ethernet physical layer) or packet delineation (as our packets are assumed be under the network MTU). **TSP Ping** and **TSP Pong** messages shall be encoded in a manor compatible with a WPILib packed struct with respect to byte alignment and endianness.
|
||||
|
||||
### TSP Ping
|
||||
|
||||
@@ -98,7 +98,7 @@ The message format forgoes CRCs (as these are provided by the Ethernet physical
|
||||
|
||||
## Optional Protocol Extensions
|
||||
|
||||
Clients may publish statistics to NetworkTables. If they do, they shall publish to a key that is globally unique per participant in the Time Synronization network. If a client implements this, it shall provide the following publishers:
|
||||
Clients may publish statistics to NetworkTables. If they do, they shall publish to a key that is globally unique per participant in the Time Synchronization network. If a client implements this, it shall provide the following publishers:
|
||||
|
||||
| Key | Type | Notes |
|
||||
| ------ | ------ | ---- |
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
## Description
|
||||
|
||||
PhotonVision is a free, fast, and easy-to-use vision processing solution for the _FIRST_ Robotics Competition. PhotonVision is designed to get vision working on your robot _quickly_, without the significant cost of other similar solutions.
|
||||
PhotonVision is a free, fast, and easy-to-use vision processing solution for the _FIRST_ Robotics Competition. PhotonVision is designed to get vision working on your robot _quickly_, but with lower cost than other solutions.
|
||||
Using PhotonVision, teams can go from setting up a camera and coprocessor to detecting and tracking AprilTags and other targets by simply tuning sliders. With an easy to use interface, comprehensive documentation, and a feature rich vendor dependency, no experience is necessary to use PhotonVision. No matter your resources, using PhotonVision is easy compared to its alternatives.
|
||||
|
||||
## Advantages
|
||||
@@ -19,19 +19,15 @@ The PhotonVision user interface is simple and modular, making things easier for
|
||||
|
||||
### PhotonLib Vendor Dependency
|
||||
|
||||
The PhotonLib vendor dependency allows you to easily get necessary target data (without having to work directly with NetworkTables) while also providing utility methods to get distance and position on the field. This helps your team focus less on getting data and more on using it to do cool things. This also has the benefit of having a structure that ensures all data is from the same timestamp, which is helpful for latency compensation.
|
||||
The PhotonLib vendor dependency allows you to easily get necessary target data (without having to work directly with NetworkTables) while also providing utility methods to get distance and position on the field. A serialization strategy is used to guarantees data coherency, which is helpful for latency compensation. This helps your team focus less on getting data and more on using it to do cool things.
|
||||
|
||||
### User Calibration
|
||||
|
||||
Using PhotonVision allows the user to calibrate for their specific camera, which will get you the best tracking results. This is extremely important as every camera (even if it is the same model) will have it's own quirks and user calibration allows for those to be accounted for.
|
||||
|
||||
### High FPS Processing
|
||||
### Low Latency, High FPS Processing
|
||||
|
||||
Compared to alternative solutions, PhotonVision boasts higher frames per second which allows for a smoother video stream and detection of targets to ensure you aren't losing out on any performance.
|
||||
|
||||
### Low Latency
|
||||
|
||||
PhotonVision provides low latency processing to make sure you get vision measurements as fast as possible, which makes complex vision tasks easier. We guarantee that all measurements are sent from the same timestamp, making life easier for your programmers.
|
||||
PhotonVision exposes specialized hardware on select coprocessors to maximize processing speed. This allows for lower-latency detection of targets to ensure you aren't losing out on any performance.
|
||||
|
||||
### Fully Open Source and Active Developer Community
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Combining Aiming and Getting in Range
|
||||
|
||||
The following example is from the PhotonLib example repository ([Java](https://github.com/PhotonVision/photonvision/tree/master/photonlib-java-examples/aimandrange)/[C++](https://github.com/PhotonVision/photonvision/tree/master/photonlib-cpp-examples/aimandrange)).
|
||||
The following example is from the PhotonLib example repository ([Java](https://github.com/PhotonVision/photonvision/tree/main/photonlib-java-examples/aimandrange)/[C++](https://github.com/PhotonVision/photonvision/tree/main/photonlib-cpp-examples/aimandrange)).
|
||||
|
||||
## Knowledge and Equipment Needed
|
||||
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
# Aiming at a Target
|
||||
|
||||
The following example is from the PhotonLib example repository ([Java](https://github.com/PhotonVision/photonvision/tree/master/photonlib-java-examples/aimattarget)).
|
||||
The following example is from the PhotonLib example repository ([Java](https://github.com/PhotonVision/photonvision/tree/main/photonlib-java-examples/aimattarget)).
|
||||
|
||||
## Knowledge and Equipment Needed
|
||||
|
||||
- A Robot
|
||||
- A camera mounted rigidly to the robot's frame, cenetered and pointed forward.
|
||||
- A coprocessor running PhotonVision with an AprilTag or Aurco 2D Pipeline.
|
||||
- [A printout of AprilTag 7](https://firstfrc.blob.core.windows.net/frc2024/FieldAssets/Apriltag_Images_and_User_Guide.pdf), mounted on a rigid and flat surface.
|
||||
- A camera mounted rigidly to the robot's frame, centered and pointed forward.
|
||||
- A coprocessor running PhotonVision with an AprilTag or Aruco 2D Pipeline.
|
||||
- [A printout of AprilTag 7](https://firstfrc.blob.core.windows.net/frc2025/FieldAssets/Apriltag_Images_and_User_Guide.pdf), mounted on a rigid and flat surface.
|
||||
|
||||
## Code
|
||||
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 24 MiB |
@@ -1,6 +1,6 @@
|
||||
# Using WPILib Pose Estimation, Simulation, and PhotonVision Together
|
||||
|
||||
The following example comes from the PhotonLib example repository ([Java](https://github.com/PhotonVision/photonvision/tree/master/photonlib-java-examples/poseest)/[C++](https://github.com/PhotonVision/photonvision/tree/master/photonlib-cpp-examples/poseest)/[Python](https://github.com/PhotonVision/photonvision/tree/master/photonlib-python-examples/poseest)). Full code is available at that links.
|
||||
The following example comes from the PhotonLib example repository ([Java](https://github.com/PhotonVision/photonvision/tree/main/photonlib-java-examples/poseest)/[C++](https://github.com/PhotonVision/photonvision/tree/main/photonlib-cpp-examples/poseest)/[Python](https://github.com/PhotonVision/photonvision/tree/main/photonlib-python-examples/poseest)). Full code is available at that links.
|
||||
|
||||
## Knowledge and Equipment Needed
|
||||
|
||||
@@ -205,7 +205,9 @@ During simulation, we periodically update the simulated vision system.
|
||||
|
||||
The rest is done behind the scenes.
|
||||
|
||||
```{image} images/poseest_demo.gif
|
||||
:alt: Simulated swerve drive and vision system working together in teleoperated mode.
|
||||
:width: 1200
|
||||
```{raw} html
|
||||
<video width="85%" controls>
|
||||
<source src="../../_static/assets/poseest_demo.mp4" type="video/mp4">
|
||||
Your browser does not support the video tag.
|
||||
</video>
|
||||
```
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# Pi Camera Configuration
|
||||
|
||||
This page covers specifics about the _Raspberry Pi_ CSI camera configuration.
|
||||
|
||||
## Background
|
||||
|
||||
The Raspberry Pi CSI Camera port is routed through and processed by the GPU. Since the GPU boots before the CPU, it must be configured properly for the attached camera. Additionally, this configuration cannot be changed without rebooting.
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
# Selecting Hardware
|
||||
|
||||
In order to use PhotonVision, you need a coprocessor and a camera. Other than the recommended hardware found in the {ref}`quick start guide<docs/quick-start/common-setups:Common Hardware Setups>`, this page will help you select hardware that should work for photonvision even though it is not supported/recommended.
|
||||
:::{note}
|
||||
See the {ref}`quick start guide<docs/quick-start/common-setups:Common Hardware Setups>`, for latest, specific recommendations on hardware to use for PhotonVision.
|
||||
:::
|
||||
|
||||
In order to use PhotonVision, you need a coprocessor and a camera. This page discusses the specifics of why that hardware is recommended.
|
||||
|
||||
## Choosing a Coprocessor
|
||||
|
||||
@@ -17,41 +21,76 @@ In order to use PhotonVision, you need a coprocessor and a camera. Other than th
|
||||
- Note that we only support using the Raspberry Pi's MIPI-CSI port, other MIPI-CSI ports from other coprocessors will probably not work.
|
||||
- Ethernet port for networking
|
||||
|
||||
Note these are bare minimums. Most high-performance vision processing will require higher specs.
|
||||
|
||||
### Coprocessor Recommendations
|
||||
|
||||
When selecting a coprocessor, it is important to consider various factors, particularly when it comes to AprilTag detection. Opting for a coprocessor with a more powerful CPU can generally result in higher FPS AprilTag detection, leading to more accurate pose estimation. However, it is important to note that there is a point of diminishing returns, where the benefits of a more powerful CPU may not outweigh the additional cost. Other coprocessors can be used but may require some extra work / command line usage in order to get it working properly.
|
||||
Vision processing on one camera stream is usually a CPU-bound operation. Some operations are able to be done in parallel, but not all. USB bandwidth and network data transfer also cause a fixed overhead.
|
||||
|
||||
Faster CPU's generally result in lower latency, but eventually with diminishing returns. More cores allow for some improvement, especially if multiple camera streams are being processed.
|
||||
|
||||
PhotonVision is most commonly tested around Raspbian (Debian-based) operating systems.
|
||||
|
||||
Other coprocessors can be used but may require some extra work / command line usage in order to get it working properly.
|
||||
|
||||
### Power Supply
|
||||
|
||||
Coprocessors need a steady, regulated power supply. Under-volting the processor will result in CPU throttling, low performance, unexpected reboots, and sometimes electrical damage. Many coprocessors draw 5-10 amps of current.
|
||||
|
||||
Be sure to select a power supply which regulate's the robot's variable battery voltage into something steady that the robot can use.
|
||||
|
||||
### Storage Media
|
||||
|
||||
Most single-board computer coprocessors use micro SD cards as their storage media.
|
||||
|
||||
Three important considerations include total storage space, read/write speed, and robustness.
|
||||
|
||||
PhotonVision is not usually disk-bound, other than during coprocessor boot-up and initial startup. Some disk writing is done at runtime for logging, settings, and saving camera images on command.
|
||||
|
||||
Better storage space and read/write speed mostly matter if image capture is used frequently on the field.
|
||||
|
||||
Industrial-grade SD cards are recommended for their stability under shock, vibration, variable voltage, and power-off. Raspberry Pi and Orange Pi coprocessors are generally robust against robot power interruptions, teams have anecdotally reported that Sandisk industrial SD cards reduce the chances of an unexpected settings or log file corruption on shutdown.
|
||||
|
||||
|
||||
## Choosing a Camera
|
||||
|
||||
PhotonVision works with Pi Cameras and most USB Cameras. Other cameras such as webcams, virtual cameras, etc. are not officially supported and may not work. It is important to note that fisheye cameras should only be used as a driver camera / gamepeice detection and not for detecting targets / AprilTags.
|
||||
PhotonVision relies on [CSCore](https://github.com/wpilibsuite/allwpilib/tree/main/cscore) to detect and process cameras, so camera support is determined based off compatibility with CScore along with native support for the camera within your OS (ex. [V4L compatibility](https://en.wikipedia.org/wiki/Video4Linux)).
|
||||
|
||||
PhotonVision relies on [CSCore](https://github.com/wpilibsuite/allwpilib/tree/main/cscore) to detect and process cameras, so camera support is determined based off compatibility with CScore along with native support for the camera within your OS (ex. [V4L compatibility](https://en.wikipedia.org/wiki/Video4Linux) if using a Linux machine like a Raspberry Pi).
|
||||
PhotonVision attempts to support most USB Cameras. Exceptions include:
|
||||
|
||||
:::{note}
|
||||
Logitech Cameras and integrated laptop cameras will not work with PhotonVision due to oddities with their drivers. We recommend using a different camera.
|
||||
:::
|
||||
- All Logitech brand cameras
|
||||
- Logitech uses a non-standard driver which is not currently supported
|
||||
- Built-in webcams
|
||||
- Driver support is too varied. Some may happen to work, but most have been found to be non-functional
|
||||
- virtual cameras (OBS, Snapchat camera, etc.)
|
||||
- PhotonVision assumes the camera has real physical hardware to control - these do not expose the minimum number of controls.
|
||||
|
||||
Use caution when using multiple identical cameras, as only the physical USB port they are plugged into can differentiate them. PhotonVision provides a "strict matching" setting which can reduce errors related to identical cameras. Arducam has a [tool that allows for identical cameras to be renamed](https://docs.arducam.com/UVC-Camera/Serial-Number-Tool-Guide/) by their physical location or purpose.
|
||||
|
||||
:::{note}
|
||||
We do not currently support the usage of two of the same camera on the same coprocessor. You can only use two or more cameras if they are of different models or they are from Arducam, which has a [tool that allows for cameras to be renamed](https://docs.arducam.com/UVC-Camera/Serial-Number-Tool-Guide/).
|
||||
:::
|
||||
|
||||
### Cameras Attributes
|
||||
|
||||
For colored shape detection, any non-fisheye camera supported by PhotonVision will work. We recommend a high fps USB camera.
|
||||
For colored shape detection, any non-fisheye camera supported by PhotonVision will work.
|
||||
|
||||
For driver camera, we recommend a USB camera with a fisheye lens, so your driver can see more of the field.
|
||||
For driver camera, we recommend a USB camera with a fisheye lens, so your driver can see more of the field. Use the minimum acceptable resolution to help keep latency low.
|
||||
|
||||
For AprilTag detection, we recommend you use a global shutter camera that has ~100 degree diagonal FOV. This will allow you to see more AprilTags in frame, and will allow for more accurate pose estimation. You also want a camera that supports high FPS, as this will allow you to update your pose estimator at a higher frequency.
|
||||
For AprilTag detection, we recommend you use a camera that has ~100 degree diagonal FOV. This will allow you to see more AprilTags in frame, and will allow for more accurate pose estimation. You also want a camera that supports high FPS, as this will allow you to update your pose estimator at a higher frequency.
|
||||
|
||||
Another cause of image distortion is 'rolling shutter.' This occurs when the camera captures pixels sequentially from top to bottom, which can also lead to distortion if the camera or object is moving.
|
||||
For object detection, we recommend a USB camera. Some fisheye lenses may be ok, but very wide angle cameras may distort the gamepiece beyond recognition.
|
||||
|
||||
Global shutter cameras are recommended in all cases, to reduce rolling-shutter image sheer while the robot is moving.
|
||||
|
||||
```{image} images/rollingshutter.gif
|
||||
:align: center
|
||||
```
|
||||
|
||||
Cameras capable of capturing a good image with very short exposures will also help reduce image blur. Usually, high-FPS-capable cameras designed for computer vision are better at this than "consumer-grade" USB webcams.
|
||||
|
||||
### Using Multiple Cameras
|
||||
|
||||
Using multiple cameras on your robot will help you detect more AprilTags at once and improve your pose estimation as a result. In order to use multiple cameras, you will need to create multiple PhotonPoseEstimators and add all of their measurements to a single drivetrain pose estimator. Please note that the accuracy of your robot to camera transform is especially important when using multiple cameras as any error in the transform will cause your pose estimations to "fight" each other. For more information, see {ref}`the programming reference. <docs/programming/index:programming reference>`.
|
||||
Keeping the target(s) in view of the robot often requires more than one camera. PhotonVision has no hardcoded limit on the number of cameras supported. The limit is usually dependant on CPU (can all frames be processed fast enough?) and USB bandwidth (Can all cameras send their images without overwhelming the bus?).
|
||||
|
||||
Note that cameras are not synchronized together. Frames are captured and processed asynchronously. Robot Code must fuse estimates together. For more information, see {ref}`the programming reference. <docs/programming/index:programming reference>`.
|
||||
|
||||
## Performance Matrix
|
||||
|
||||
|
||||
@@ -2,22 +2,18 @@
|
||||
|
||||
## How does it work?
|
||||
|
||||
PhotonVision supports object detection using neural network accelerator hardware built into Orange Pi 5/5+ coprocessors. The Neural Processing Unit, or NPU, is [used by PhotonVision](https://github.com/PhotonVision/rknn_jni/tree/main) to massively accelerate certain math operations like those needed for running ML-based object detection.
|
||||
PhotonVision supports object detection using neural network accelerator hardware built into Orange Pi 5/5+ coprocessors. Please note that the Orange Pi 5/5+ are the only coprocessors that are currently supported. The Neural Processing Unit, or NPU, is [used by PhotonVision](https://github.com/PhotonVision/rknn_jni/tree/main) to massively accelerate certain math operations like those needed for running ML-based object detection.
|
||||
|
||||
For the 2024 season, PhotonVision ships with a **pre-trained NOTE detector** (shown above), as well as a mechanism for swapping in custom models. Future development will focus on enabling lower friction management of multiple custom models.
|
||||
|
||||
```{image} images/notes-ui.png
|
||||
|
||||
```
|
||||
For the 2025 season, PhotonVision ships with a pretrained ALGAE model. A model to detect coral is not currently stable, and interested teams should ask in the Photonvision discord.
|
||||
|
||||
## Tracking Objects
|
||||
|
||||
Before you get started with object detection, ensure that you have followed the previous sections on installation, wiring, and networking. Next, open the Web UI, go to the top right card, and switch to the “Object Detection” type. You should see a screen similar to the image above.
|
||||
|
||||
PhotonVision currently ships with a NOTE detector based on a [YOLOv5 model](https://docs.ultralytics.com/yolov5/). This model is trained to detect one or more object "classes" (such as cars, stoplights, or in our case, NOTES) in an input image. For each detected object, the model outputs a bounding box around where in the image the object is located, what class the object belongs to, and a unitless confidence between 0 and 1.
|
||||
Models are trained to detect one or more object "classes" (such as cars, stoplights) in an input image. For each detected object, the model outputs a bounding box around where in the image the object is located, what class the object belongs to, and a unitless confidence between 0 and 1.
|
||||
|
||||
:::{note}
|
||||
This model output means that while its fairly easy to say that "this rectangle probably contains a NOTE", we don't have any information about the NOTE's orientation or location. Further math in user code would be required to make estimates about where an object is physically located relative to the camera.
|
||||
This model output means that while its fairly easy to say that "this rectangle probably contains an object", we don't have any information about the object's orientation or location. Further math in user code would be required to make estimates about where an object is physically located relative to the camera.
|
||||
:::
|
||||
|
||||
## Tuning and Filtering
|
||||
@@ -39,14 +35,21 @@ Photonvision will letterbox your camera frame to 640x640. This means that if you
|
||||
|
||||
## Training Custom Models
|
||||
|
||||
Coming soon!
|
||||
:::{warning}
|
||||
Power users only. This requires some setup, such as obtaining your own dataset and installing various tools. It's additionally advised to have a general knowledge of ML before attempting to train your own model. Additionally, this is not officially supported by Photonvision, and any problems that may arise are not attributable to Photonvision.
|
||||
:::
|
||||
|
||||
Before beginning, it is necessary to install the [rknn-toolkit2](https://github.com/airockchip/rknn-toolkit2). Then, install the relevant [Ultralytics repository](https://github.com/airockchip?tab=repositories&q=yolo&type=&language=&sort=) from this list. After training your model, export it to `rknn`. This will give you an `onnx` file, formatted for conversion. Copy this file to the relevant folder in [rknn_model_zoo](https://github.com/airockchip/rknn_model_zoo), and use the conversion script located there to convert it. If necessary, modify the script to provide the path to your training database for quantization.
|
||||
|
||||
## Uploading Custom Models
|
||||
|
||||
:::{warning}
|
||||
PhotonVision currently ONLY supports YOLOv5 models trained and converted to `.rknn` format for RK3588 CPUs! Other models require different post-processing code and will NOT work. The model conversion process is also highly particular. Proceed with care.
|
||||
PhotonVision currently ONLY supports 640x640 Ultralytics YOLOv5, YOLOv8, and YOLO11 models trained and converted to `.rknn` format for RK3588 CPUs! Other models require different post-processing code and will NOT work. The model conversion process is also highly particular. Proceed with care.
|
||||
:::
|
||||
|
||||
Our [pre-trained NOTE model](https://github.com/PhotonVision/photonvision/blob/master/photon-server/src/main/resources/models/note-640-640-yolov5s.rknn) is automatically extracted from the JAR when PhotonVision starts, only if a file named “note-640-640-yolov5s.rknn” and "labels.txt" does not exist in the folder `photonvision_config/models/`. This technically allows power users to replace the model and label files with new ones without rebuilding Photon from source and uploading a new JAR.
|
||||
|
||||
Use a program like WinSCP or FileZilla to access your coprocessor's filesystem, and copy the new `.rknn` model file into /home/pi. Next, SSH into the coprocessor and `sudo mv /path/to/new/model.rknn /opt/photonvision/photonvision_config/models/note-640-640-yolov5s.rknn`. Repeat this process with the labels file, which should contain one line per label the model outputs with no training newline. Next, restart PhotonVision via the web UI.
|
||||
In the settings, under `Device Control`, there's an option to upload a new object detection model. Naming convention
|
||||
should be `name-verticalResolution-horizontalResolution-modelType`. The
|
||||
`name` should only include alphanumeric characters, periods, and underscores. Additionally, the labels
|
||||
file ought to have the same name as the RKNN file, with `-labels` appended to the end. For
|
||||
example, if the RKNN file is named `Algae_1.03.2025-640-640-yolov5s.rknn`, the labels file should be
|
||||
named `Algae_1.03.2025-640-640-yolov5s-labels.txt`.
|
||||
|
||||
@@ -60,7 +60,7 @@ Use the `getLatestResult()`/`GetLatestResult()` (Java and C++ respectively) to o
|
||||
```
|
||||
|
||||
:::{note}
|
||||
Unlike other vision software solutions, using the latest result guarantees that all information is from the same timestamp. This is achievable because the PhotonVision backend sends a byte-packed string of data which is then deserialized by PhotonLib to get target data. For more information, check out the [PhotonLib source code](https://github.com/PhotonVision/photonvision/tree/master/photon-lib).
|
||||
Unlike other vision software solutions, using the latest result guarantees that all information is from the same timestamp. This is achievable because the PhotonVision backend sends a byte-packed string of data which is then deserialized by PhotonLib to get target data. For more information, check out the [PhotonLib source code](https://github.com/PhotonVision/photonvision/tree/main/photon-lib).
|
||||
:::
|
||||
|
||||
## Checking for Existence of Targets
|
||||
|
||||
@@ -17,12 +17,12 @@ The API documentation can be found in here: [Java](https://github.wpilib.org/all
|
||||
.. code-block:: Java
|
||||
|
||||
// The field from AprilTagFields will be different depending on the game.
|
||||
AprilTagFieldLayout aprilTagFieldLayout = AprilTagFields.k2024Crescendo.loadAprilTagLayoutField();
|
||||
AprilTagFieldLayout aprilTagFieldLayout = AprilTagFieldLayout.loadField(AprilTagFields.kDefaultField);
|
||||
|
||||
.. code-block:: C++
|
||||
|
||||
// The parameter for LoadAPrilTagLayoutField will be different depending on the game.
|
||||
frc::AprilTagFieldLayout aprilTagFieldLayout = frc::LoadAprilTagLayoutField(frc::AprilTagField::k2024Crescendo);
|
||||
frc::AprilTagFieldLayout aprilTagFieldLayout = frc::LoadAprilTagLayoutField(frc::AprilTagField::kDefaultField);
|
||||
|
||||
.. code-block:: Python
|
||||
|
||||
@@ -47,6 +47,19 @@ The PhotonPoseEstimator has a constructor that takes an `AprilTagFieldLayout` (s
|
||||
- Choose the Pose which is closest to the last pose calculated.
|
||||
- AVERAGE_BEST_TARGETS
|
||||
- Choose the Pose which is the average of all the poses from each tag.
|
||||
- MULTI_TAG_PNP_ON_RIO
|
||||
- A slower, older version of MULTI_TAG_PNP_ON_COPROCESSOR, not recommended for use.
|
||||
- PNP_DISTANCE_TRIG_SOLVE
|
||||
- Use distance data from best visible tag to compute a Pose. This runs on the RoboRIO in order
|
||||
to access the robot's yaw heading, and MUST have addHeadingData called every frame so heading
|
||||
data is up-to-date. Based on a reference implementation by [FRC Team 6328 Mechanical Advantage](https://www.chiefdelphi.com/t/frc-6328-mechanical-advantage-2025-build-thread/477314/98).
|
||||
- CONSTRAINED_SOLVEPNP
|
||||
- Solve a constrained version of the Perspective-n-Point problem with the robot's drivebase
|
||||
flat on the floor. This computation takes place on the RoboRIO, and should not take more than 2ms.
|
||||
This also requires addHeadingData to be called every frame so heading data is up to date.
|
||||
If Multi-Tag PNP is enabled on the coprocessor, it will be used to provide an initial seed to
|
||||
the optimization algorithm -- otherwise, the multi-tag fallback strategy will be used as the
|
||||
seed.
|
||||
|
||||
```{eval-rst}
|
||||
.. tab-set-code::
|
||||
@@ -91,7 +104,7 @@ The PhotonPoseEstimator has a constructor that takes an `AprilTagFieldLayout` (s
|
||||
self.cam = PhotonCamera("YOUR CAMERA NAME")
|
||||
|
||||
self.camPoseEst = PhotonPoseEstimator(
|
||||
loadAprilTagLayoutField(AprilTagField.k2024Crescendo),
|
||||
loadAprilTagLayoutField(AprilTagField.kDefaultField),
|
||||
PoseStrategy.CLOSEST_TO_REFERENCE_POSE,
|
||||
self.cam,
|
||||
kRobotToCam
|
||||
|
||||
@@ -106,7 +106,7 @@ You can get a [translation](https://docs.wpilib.org/en/latest/docs/software/adva
|
||||
.. code-block:: C++
|
||||
|
||||
// Calculate a translation from the camera to the target.
|
||||
frc::Translation2d translation = photonlib::PhotonUtils::EstimateCameraToTargetTranslationn(
|
||||
frc::Translation2d translation = photonlib::PhotonUtils::EstimateCameraToTargetTranslation(
|
||||
distance, frc::Rotation2d(units::degree_t(-target.GetYaw())));
|
||||
|
||||
.. code-block:: Python
|
||||
|
||||
@@ -17,6 +17,6 @@ Arducam cameras are supported for setups with multiple devices. This is possible
|
||||
3. **Save Settings**: Ensure that you save the settings after selecting the appropriate camera model for each device.
|
||||
|
||||
```{image} images/setArducamModel.png
|
||||
:alt: The camera model can be selected from the Arudcam model selector in the cameras tab
|
||||
:alt: The camera model can be selected from the Arducam model selector in the cameras tab
|
||||
:align: center
|
||||
```
|
||||
|
||||
@@ -8,7 +8,7 @@ If you’re not using cameras in 3D mode, calibration is optional, but it can st
|
||||
|
||||
## Print the Calibration Target
|
||||
|
||||
- Downloaded from our [demo site](https://demo.photonvision.org/#/cameras), or directly from your coprocessors cameras tab.
|
||||
- Downloaded from our [demo site](http://photonvision.global/#/cameras), or directly from your coprocessors cameras tab.
|
||||
- Use the Charuco calibration board:
|
||||
- Board Type: Charuco
|
||||
- Tag Family: 4x4
|
||||
|
||||
@@ -1,44 +1,46 @@
|
||||
# Common Hardware Setups
|
||||
|
||||
PhotonVision requires dedicated hardware, above and beyond a roboRIO. This page lists hardware that is frequently used with PhotonVision.
|
||||
|
||||
## Coprocessors
|
||||
|
||||
- Orange Pi 5 4GB
|
||||
- Supports up to 2 object detection streams, along with 2 AprilTag streams at 1280x800 (30fps).
|
||||
- Raspberry Pi 5 2GB
|
||||
- Supports up to 2 AprilTag streams at 1280x800 (30fps).
|
||||
|
||||
:::{note}
|
||||
The Orange Pi 5 is the only currently supported device for object detection.
|
||||
:::
|
||||
|
||||
- Orange Pi 5 4GB
|
||||
- Able to process two object detection streams at once while also processing 1 to 2 AprilTag streams at 1280x800 (30fps).
|
||||
- Raspberry Pi 5 2GB
|
||||
- A good cheaper option. Doesn't support object detection. Able to process 2 AprilTag streams at 1280x800 (30fps).
|
||||
|
||||
## SD Cards
|
||||
|
||||
- 8GB or larger micro SD card
|
||||
- Many teams have found that an industrial micro sd card are much more stable in competition. One example is the SanDisk industrial 16GB micro SD card.
|
||||
|
||||
:::{important}
|
||||
Industrial grade SD cards from major manufacturers are recommended for robotics applications. For example: Sandisk SDSDQAF3-016G-I .
|
||||
:::
|
||||
|
||||
## Cameras
|
||||
|
||||
- AprilTag
|
||||
Innomaker and Arducam are common manufacturers of hardware designed specifically for vision processing.
|
||||
|
||||
- Innomaker or Arducam OV9281 UVC USB cameras.
|
||||
- AprilTag Detection
|
||||
- OV9281
|
||||
|
||||
- Object Detection
|
||||
|
||||
- Arducam OV9782 works well with its global shutter.
|
||||
- Most other fixed-focus color UVC USB webcams.
|
||||
- OV9782
|
||||
|
||||
- Driver Camera
|
||||
- OV9281
|
||||
- OV9782
|
||||
- Pi Camera Module V1 {ref}`(More setup info)<docs/hardware/picamconfig:Pi Camera Configuration>`
|
||||
- Most other fixed-focus UVC USB webcams
|
||||
|
||||
Feel free to get started with any color webcam you have sitting around.
|
||||
|
||||
## Power
|
||||
|
||||
- Pololu S13V30F5 Regulator
|
||||
|
||||
- Wide power range input. Recommended by many teams.
|
||||
|
||||
- Redux Robotics Zinc-V Regulator
|
||||
|
||||
- Recently released for the 2025 season, offering reliable and easy integration.
|
||||
See {ref}`(Selecting Hardware)<docs/hardware/selecting-hardware:Selecting Hardware>` for info on why these are recommended.
|
||||
|
||||
@@ -11,7 +11,7 @@ When using PhotonVision off robot, you _MUST_ plug the coprocessor into a physic
|
||||
:::{tab-item} New Radio (2025 - present)
|
||||
|
||||
```{danger}
|
||||
Ensure that DIP switches 1 and 2 are turned off; otherwise, the radio PoE feature will fry your coprocessor. [More info.](https://frc-radio.vivid-hosting.net/getting-started/passive-power-over-ethernet-poe-for-downstream-devices)
|
||||
Ensure that the radio's DIP switches 1 and 2 are turned off; otherwise, the radio PoE feature may electrically destroy your coprocessor. [More info.](https://frc-radio.vivid-hosting.net/getting-started/passive-power-over-ethernet-poe-for-downstream-devices)
|
||||
```
|
||||
|
||||
```{image} images/networking-diagram-vividhosting.png
|
||||
@@ -33,13 +33,13 @@ PhotonVision _STRONGLY_ recommends the usage of a network switch on your robot.
|
||||
|
||||
## Network Hostname
|
||||
|
||||
Rename each device from the default "Photonvision" to a unique hostname (e.g., "Photon-OrangePi-Left" or "Photon-RPi5-Back"). This helps differentiate multiple coprocessors on your network, making it easier to manage them. Navigate to the settings page and scroll down to the network section. You will find the hostname is set to "photonvision" by default, this can only contain letters (A-Z), numeric characters (0-9), and the minus sign (-).
|
||||
Rename each device from the default "photonvision" to a unique hostname (e.g., "Photon-OrangePi-Left" or "Photon-RPi5-Back"). This helps differentiate multiple coprocessors on your network, making it easier to manage them. Navigate to the settings page and scroll down to the network section. You will find the hostname is set to "photonvision" by default, this can only contain letters (A-Z), numeric characters (0-9), and the minus sign (-).
|
||||
|
||||
```{image} images/editHostname.png
|
||||
:alt: The hostname can be edited in the settings page under the network section.
|
||||
```
|
||||
|
||||
## Digital Networking
|
||||
## Robot Networking
|
||||
|
||||
PhotonVision _STRONGLY_ recommends the usage of Static IPs as it increases reliability on the field and when using PhotonVision in general. To properly set up your static IP, follow the steps below:
|
||||
|
||||
@@ -61,6 +61,8 @@ Power-cycle your robot and then you will now be access the PhotonVision dashboar
|
||||
```{image} images/static.png
|
||||
:alt: Correctly set static IP
|
||||
```
|
||||
The "team number" field will accept (in addition to a team number) an IP address or hostname. This is useful for testing PhotonVision on the same computer as a simulated robot program;
|
||||
you can set the team number to "localhost", and PhotonVision will send data to the network tables in the simulated robot.
|
||||
|
||||
## Port Forwarding
|
||||
|
||||
@@ -88,4 +90,4 @@ The address in the code above (`photonvision.local`) is the hostname of the copr
|
||||
|
||||
## Camera Stream Ports
|
||||
|
||||
The camera streams start at they begin at 1181 with two ports for each camera (ex. 1181 and 1182 for camera one, 1183 and 1184 for camera two, etc.). The easiest way to identify the port of the camera that you want is by double clicking on the stream, which opens it in a separate page. The port will be listed below the stream.
|
||||
The camera streams start at 1181 with two ports for each camera (ex. 1181 and 1182 for camera one, 1183 and 1184 for camera two, etc.). The easiest way to identify the port of the camera that you want is by double clicking on the stream, which opens it in a separate page. The port will be listed below the stream.
|
||||
|
||||
@@ -8,7 +8,7 @@ In order for photonvision to connect to the roborio it needs to know your team n
|
||||
|
||||
### Camera Nickname
|
||||
|
||||
You **must** nickname your cameras in photonvision to ensure that every camera has a unique name. This is how we will identify cameras in robot code. The camera can be nickname using the edit button next to the camera name in the upper right of the Dashboard tab.
|
||||
You **must** nickname your cameras in PhotonVision to ensure that every camera has a unique name. This is how you will identify cameras in robot code. The camera can be nicknamed using the edit button next to the camera name in the upper right of the Dashboard tab.
|
||||
|
||||
```{image} images/editCameraName.png
|
||||
:align: center
|
||||
@@ -38,7 +38,7 @@ When detecting AprilTags, it's important to minimize 'motion blur' as much as po
|
||||
- Fixes
|
||||
- Lower your exposure as low as possible. Using gain and brightness to account for lack of brightness.
|
||||
- Other Options:
|
||||
- Don't use/rely vision measurements while moving.
|
||||
- Don't use/rely on vision measurements while moving.
|
||||
|
||||
```{image} images/motionblur.png
|
||||
:align: center
|
||||
@@ -51,7 +51,7 @@ When using an Orange Pi 5 with an OV9782 teams will usually change the following
|
||||
- Resolution:
|
||||
- Resolutions higher than 640x640 may not result in any higher detection accuracy and may lower {ref}`performance<docs/objectDetection/about-object-detection:Letterboxing>`.
|
||||
- Confidence:
|
||||
- 0.75 - 0.95 Lower values are fpr detecting warn game pieces or less ideal game pieces. Higher for less warn, more ideal game pieces.
|
||||
- 0.75 - 0.95 Lower values are for detecting worn game pieces or less ideal game pieces. Higher for less worn, more ideal game pieces.
|
||||
- White Balance Temperature:
|
||||
- Adjust this to achieve better color accuracy. This may be needed to increase confidence.
|
||||
- Set arducam specific camera type selector to OV9782
|
||||
|
||||
@@ -29,7 +29,7 @@ Limelights have a different installation processes. Simply connect the limelight
|
||||
Unless otherwise noted in release notes or if updating from the prior years version, to update PhotonVision after the initial installation, use the offline update option in the settings page with the downloaded jar file from the latest release.
|
||||
|
||||
:::{note}
|
||||
Limelight 2, 2+, and 3 will need a [custom hardware config file](https://github.com/PhotonVision/photonvision/tree/master/docs/source/docs/advanced-installation/sw_install/files) for lighting to work. Currently only limelight 2 and 2+ files are available.
|
||||
Limelight 2, 2+, and 3 will need a [custom hardware config file](https://github.com/PhotonVision/photonvision/tree/main/docs/source/docs/advanced-installation/sw_install/files) for lighting to work. Currently only limelight 2 and 2+ files are available.
|
||||
:::
|
||||
|
||||
:::{note}
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
# Settings
|
||||
|
||||
```{image} assets/settings.png
|
||||
```
|
||||
|
||||
## General
|
||||
|
||||
Here, you can view general data on your system, including version, hardware, your platform, and performance statistics. You can also export/import the settings in a .zip file or restart PhotonVision/your coprocessor.
|
||||
|
||||
## Networking
|
||||
|
||||
Here, you can set your team number, switch your IP between DHCP and static, and specify your host name. For more information about on-robot networking, click [here.](https://docs.wpilib.org/en/latest/docs/networking/networking-introduction/networking-basics.html)
|
||||
|
||||
The "team number" field will accept (in addition to a team number) an IP address or hostname. This is useful for testing PhotonVision on the same computer as a simulated robot program;
|
||||
you can set the team number to "localhost", and PhotonVision will send data to the network tables in the simulated robot.
|
||||
|
||||
:::{note}
|
||||
Something must be entered into the team number field if using PhotonVision on a robot. Using a team number is recommended (as opposed to an IP address or hostname).
|
||||
:::
|
||||
|
||||
## LEDs
|
||||
|
||||
If your coprocessor electronics support hardware-controlled LED's and has the proper hardware configuration set up, here you can adjust the brightness of your LEDs.
|
||||
@@ -103,7 +103,7 @@ For convenience, an `AprilTagFieldLayout` can also be added to automatically cre
|
||||
.. code-block:: Java
|
||||
|
||||
// The layout of AprilTags which we want to add to the vision system
|
||||
AprilTagFieldLayout tagLayout = AprilTagFieldLayout.loadFromResource(AprilTagFields.k2024Crescendo.m_resourceFile);
|
||||
AprilTagFieldLayout tagLayout = AprilTagFieldLayout.loadFromResource(AprilTagFields.kDefaultField.m_resourceFile);
|
||||
|
||||
visionSim.addAprilTags(tagLayout);
|
||||
```
|
||||
|
||||
@@ -10,16 +10,11 @@ A few issues make up the majority of support requests. Run through this checklis
|
||||
- Ethernet straight from a laptop to a coprocessor will not work (most likely), due to the unreliability of link-local connections.
|
||||
- Even if there's a switch between your laptop and coprocessor, you'll still want a radio or router in the loop somehow.
|
||||
- The FRC radio is the _only_ router we will officially support due to the innumerable variations between routers.
|
||||
- (Raspberry Pi, Orange Pi & Limelight only) have you flashed the correct image, and is it up to date?
|
||||
- Limelights 2/2+ should be flashed using the Limelight 2 image (eg, `photonvision-v2024.2.8-linuxarm64_limelight2.img.xz`).
|
||||
- Limelights 3 should be flashed using the Limelight 3 image (eg, `photonvision-v2024.2.8-linuxarm64_limelight3.img.xz`).
|
||||
- Raspberry Pi devices (including Pi 3, Pi 4, CM3 and CM4) should be flashed using the Raspberry Pi image (eg, `photonvision-v2024.2.8-linuxarm64_RaspberryPi.img.xz`).
|
||||
- Orange Pi 5 devices should be flashed using the Orange Pi 5 image (eg, `photonvision-v2024.2.8-linuxarm64_orangepi5.img.xz`).
|
||||
- Orange Pi 5+ devices should be flashed using the Orange Pi 5+ image (eg, `photonvision-v2024.2.8-linuxarm64_orangepi5plus.img.xz`).
|
||||
- Is your robot code using a **2024** version of WPILib, and is your coprocessor using the most up to date **2024** release?
|
||||
- 2022, 2023 and 2024 versions of either cannot be mix-and-matched!
|
||||
- Your PhotonVision version can be checked on the {ref}`settings tab<docs/settings:settings>`.
|
||||
- Is your team number correctly set on the {ref}`settings tab<docs/settings:settings>`?
|
||||
- (Raspberry Pi, Orange Pi & Limelight only) have you flashed the correct image, and is it [up to date](https://github.com/PhotonVision/photonvision/releases/latest)?
|
||||
- Is your robot code using a **2025** version of WPILib, and is your coprocessor using the most up to date **2025** release?
|
||||
- 2022, 2023, 2024, and 2025 versions of either cannot be mix-and-matched!
|
||||
- Your PhotonVision version can be checked on the settings tab.
|
||||
- Is your team number correctly set on the settings tab?
|
||||
|
||||
### photonvision.local Not Found
|
||||
|
||||
@@ -35,7 +30,7 @@ Please check that:
|
||||
1\. You don't have the NetworkTables Server on (toggleable in the settings tab). Turn this off when doing work on a robot.
|
||||
2\. You have your team number set properly in the settings tab.
|
||||
3\. Your camera name in the `PhotonCamera` constructor matches the name in the UI.
|
||||
4\. You are using the 2024 version of WPILib and RoboRIO image.
|
||||
4\. You are using the 2025 version of WPILib and RoboRIO image.
|
||||
5\. Your robot is on.
|
||||
|
||||
If all of the above are met and you still have issues, feel free to {ref}`contact us <index:contact us>` and provide the following information:
|
||||
|
||||
@@ -71,13 +71,12 @@ Welcome to the official documentation of PhotonVision! PhotonVision is the free,
|
||||
The source code for all PhotonVision projects is available through our [GitHub organization](https://github.com/PhotonVision).
|
||||
|
||||
- [PhotonVision](https://github.com/PhotonVision/photonvision)
|
||||
- [PhotonVision ReadTheDocs](https://github.com/PhotonVision/photonvision-docs/)
|
||||
|
||||
# Contact Us
|
||||
|
||||
To report a bug or submit a feature request in PhotonVision, please [submit an issue on the PhotonVision GitHub](https://github.com/PhotonVision/photonvision) or [contact the developers on Discord](https://discord.com/invite/KS76FrX).
|
||||
|
||||
If you find a problem in this documentation, please submit an issue on the [PhotonVision Documentation GitHub](https://github.com/PhotonVision/photonvision-docs).
|
||||
If you find a problem in this documentation, please submit an issue on the [PhotonVision Documentation GitHub](https://github.com/PhotonVision/photonvision/tree/main/docs).
|
||||
|
||||
# License
|
||||
|
||||
@@ -92,7 +91,6 @@ docs/description
|
||||
docs/quick-start/index
|
||||
docs/hardware/index
|
||||
docs/advanced-installation/index
|
||||
docs/settings
|
||||
```
|
||||
|
||||
```{toctree}
|
||||
|
||||
@@ -6,3 +6,4 @@ org.gradle.jvmargs= \
|
||||
--add-exports jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED \
|
||||
--add-exports jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED \
|
||||
--add-exports jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED
|
||||
org.ysb33r.gradle.doxygen.download.url=https://frcmaven.wpi.edu/artifactory/generic-release-mirror/doxygen
|
||||
|
||||
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
Binary file not shown.
2
gradle/wrapper/gradle-wrapper.properties
vendored
2
gradle/wrapper/gradle-wrapper.properties
vendored
@@ -1,6 +1,6 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=permwrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-bin.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.11-bin.zip
|
||||
networkTimeout=10000
|
||||
validateDistributionUrl=true
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
|
||||
7
gradlew
vendored
7
gradlew
vendored
@@ -15,6 +15,8 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
#
|
||||
|
||||
##############################################################################
|
||||
#
|
||||
@@ -55,7 +57,7 @@
|
||||
# Darwin, MinGW, and NonStop.
|
||||
#
|
||||
# (3) This script is generated from the Groovy template
|
||||
# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
||||
# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
||||
# within the Gradle project.
|
||||
#
|
||||
# You can find Gradle at https://github.com/gradle/gradle/.
|
||||
@@ -84,7 +86,8 @@ done
|
||||
# shellcheck disable=SC2034
|
||||
APP_BASE_NAME=${0##*/}
|
||||
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
|
||||
APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit
|
||||
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s
|
||||
' "$PWD" ) || exit
|
||||
|
||||
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||
MAX_FD=maximum
|
||||
|
||||
22
gradlew.bat
vendored
22
gradlew.bat
vendored
@@ -13,6 +13,8 @@
|
||||
@rem See the License for the specific language governing permissions and
|
||||
@rem limitations under the License.
|
||||
@rem
|
||||
@rem SPDX-License-Identifier: Apache-2.0
|
||||
@rem
|
||||
|
||||
@if "%DEBUG%"=="" @echo off
|
||||
@rem ##########################################################################
|
||||
@@ -43,11 +45,11 @@ set JAVA_EXE=java.exe
|
||||
%JAVA_EXE% -version >NUL 2>&1
|
||||
if %ERRORLEVEL% equ 0 goto execute
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
echo.
|
||||
echo Please set the JAVA_HOME variable in your environment to match the
|
||||
echo location of your Java installation.
|
||||
echo. 1>&2
|
||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
|
||||
echo. 1>&2
|
||||
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||
echo location of your Java installation. 1>&2
|
||||
|
||||
goto fail
|
||||
|
||||
@@ -57,11 +59,11 @@ set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||
|
||||
if exist "%JAVA_EXE%" goto execute
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
|
||||
echo.
|
||||
echo Please set the JAVA_HOME variable in your environment to match the
|
||||
echo location of your Java installation.
|
||||
echo. 1>&2
|
||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
|
||||
echo. 1>&2
|
||||
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||
echo location of your Java installation. 1>&2
|
||||
|
||||
goto fail
|
||||
|
||||
|
||||
24
photon-client/package-lock.json
generated
24
photon-client/package-lock.json
generated
@@ -13,11 +13,13 @@
|
||||
"@msgpack/msgpack": "^3.0.0-beta2",
|
||||
"axios": "^1.6.3",
|
||||
"jspdf": "^2.5.1",
|
||||
"lodash": "^4.17.21",
|
||||
"pinia": "^2.1.4",
|
||||
"three": "^0.160.0",
|
||||
"vue": "^2.7.14",
|
||||
"vue-router": "^3.6.5",
|
||||
"vue-virtual-scroll-list": "^2.3.5",
|
||||
"vue2-helpers": "^2.1.1",
|
||||
"vuetify": "^2.7.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -3482,8 +3484,7 @@
|
||||
"node_modules/lodash": {
|
||||
"version": "4.17.21",
|
||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
|
||||
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
|
||||
"dev": true
|
||||
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
|
||||
},
|
||||
"node_modules/lodash.merge": {
|
||||
"version": "4.6.2",
|
||||
@@ -5347,6 +5348,25 @@
|
||||
"resolved": "https://registry.npmjs.org/vue-virtual-scroll-list/-/vue-virtual-scroll-list-2.3.5.tgz",
|
||||
"integrity": "sha512-YFK6u5yltqtAOfTBcij/KGAS2SoZvzbNIAf9qTULauPObEp53xj22tDuohrrM2vNkgoD5kejXICIUBt2Q4ZDqQ=="
|
||||
},
|
||||
"node_modules/vue2-helpers": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/vue2-helpers/-/vue2-helpers-2.1.1.tgz",
|
||||
"integrity": "sha512-ujYiQ5xfO8qKP3ly8hMqtNA/QGoJCTmKdYErsL3Oxr3nURJ5axah3IV4ztC/Y3zR6qsST0yVwuG1nEneQ9jXQQ==",
|
||||
"license": "Apache-2.0",
|
||||
"peerDependencies": {
|
||||
"vue": "~2.7.0",
|
||||
"vue-router": "^3",
|
||||
"vuex": "^3"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"vue-router": {
|
||||
"optional": true
|
||||
},
|
||||
"vuex": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/vuetify": {
|
||||
"version": "2.7.1",
|
||||
"resolved": "https://registry.npmjs.org/vuetify/-/vuetify-2.7.1.tgz",
|
||||
|
||||
@@ -20,11 +20,13 @@
|
||||
"@msgpack/msgpack": "^3.0.0-beta2",
|
||||
"axios": "^1.6.3",
|
||||
"jspdf": "^2.5.1",
|
||||
"lodash": "^4.17.21",
|
||||
"pinia": "^2.1.4",
|
||||
"three": "^0.160.0",
|
||||
"vue": "^2.7.14",
|
||||
"vue-router": "^3.6.5",
|
||||
"vue-virtual-scroll-list": "^2.3.5",
|
||||
"vue2-helpers": "^2.1.1",
|
||||
"vuetify": "^2.7.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -34,12 +34,15 @@ if (!is_demo) {
|
||||
if (data.updatePipelineResult !== undefined) {
|
||||
useStateStore().updateBackendResultsFromWebsocket(data.updatePipelineResult);
|
||||
}
|
||||
if (data.mutatePipelineSettings !== undefined && data.cameraIndex !== undefined) {
|
||||
useCameraSettingsStore().changePipelineSettingsInStore(data.mutatePipelineSettings, data.cameraIndex);
|
||||
if (data.mutatePipelineSettings !== undefined && data.cameraUniqueName !== undefined) {
|
||||
useCameraSettingsStore().changePipelineSettingsInStore(data.mutatePipelineSettings, data.cameraUniqueName);
|
||||
}
|
||||
if (data.calibrationData !== undefined) {
|
||||
useStateStore().updateCalibrationStateValuesFromWebsocket(data.calibrationData);
|
||||
}
|
||||
if (data.visionSourceManager !== undefined) {
|
||||
useStateStore().updateDiscoveredCameras(data.visionSourceManager);
|
||||
}
|
||||
},
|
||||
() => {
|
||||
useStateStore().$patch({ backendConnected: false });
|
||||
|
||||
57
photon-client/src/assets/images/loading-transparent.svg
Normal file
57
photon-client/src/assets/images/loading-transparent.svg
Normal file
@@ -0,0 +1,57 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" preserveAspectRatio="xMidYMid" width="200" height="200" style="shape-rendering: auto; display: block; background: rgb(0, 100, 146);" xmlns:xlink="http://www.w3.org/1999/xlink"><g><g transform="translate(80,50)">
|
||||
<g transform="rotate(0)">
|
||||
<circle fill-opacity="1" fill="#ffd943" r="6" cy="0" cx="0">
|
||||
<animateTransform repeatCount="indefinite" dur="0.9345794392523364s" keyTimes="0;1" values="1.5 1.5;1 1" begin="-0.8177570093457943s" type="scale" attributeName="transform"></animateTransform>
|
||||
<animate begin="-0.8177570093457943s" values="1;0" repeatCount="indefinite" dur="0.9345794392523364s" keyTimes="0;1" attributeName="fill-opacity"></animate>
|
||||
</circle>
|
||||
</g>
|
||||
</g><g transform="translate(71.21320343559643,71.21320343559643)">
|
||||
<g transform="rotate(45)">
|
||||
<circle fill-opacity="0.875" fill="#ffd943" r="6" cy="0" cx="0">
|
||||
<animateTransform repeatCount="indefinite" dur="0.9345794392523364s" keyTimes="0;1" values="1.5 1.5;1 1" begin="-0.7009345794392523s" type="scale" attributeName="transform"></animateTransform>
|
||||
<animate begin="-0.7009345794392523s" values="1;0" repeatCount="indefinite" dur="0.9345794392523364s" keyTimes="0;1" attributeName="fill-opacity"></animate>
|
||||
</circle>
|
||||
</g>
|
||||
</g><g transform="translate(50,80)">
|
||||
<g transform="rotate(90)">
|
||||
<circle fill-opacity="0.75" fill="#ffd943" r="6" cy="0" cx="0">
|
||||
<animateTransform repeatCount="indefinite" dur="0.9345794392523364s" keyTimes="0;1" values="1.5 1.5;1 1" begin="-0.5841121495327103s" type="scale" attributeName="transform"></animateTransform>
|
||||
<animate begin="-0.5841121495327103s" values="1;0" repeatCount="indefinite" dur="0.9345794392523364s" keyTimes="0;1" attributeName="fill-opacity"></animate>
|
||||
</circle>
|
||||
</g>
|
||||
</g><g transform="translate(28.786796564403577,71.21320343559643)">
|
||||
<g transform="rotate(135)">
|
||||
<circle fill-opacity="0.625" fill="#ffd943" r="6" cy="0" cx="0">
|
||||
<animateTransform repeatCount="indefinite" dur="0.9345794392523364s" keyTimes="0;1" values="1.5 1.5;1 1" begin="-0.4672897196261682s" type="scale" attributeName="transform"></animateTransform>
|
||||
<animate begin="-0.4672897196261682s" values="1;0" repeatCount="indefinite" dur="0.9345794392523364s" keyTimes="0;1" attributeName="fill-opacity"></animate>
|
||||
</circle>
|
||||
</g>
|
||||
</g><g transform="translate(20,50.00000000000001)">
|
||||
<g transform="rotate(180)">
|
||||
<circle fill-opacity="0.5" fill="#ffd943" r="6" cy="0" cx="0">
|
||||
<animateTransform repeatCount="indefinite" dur="0.9345794392523364s" keyTimes="0;1" values="1.5 1.5;1 1" begin="-0.35046728971962615s" type="scale" attributeName="transform"></animateTransform>
|
||||
<animate begin="-0.35046728971962615s" values="1;0" repeatCount="indefinite" dur="0.9345794392523364s" keyTimes="0;1" attributeName="fill-opacity"></animate>
|
||||
</circle>
|
||||
</g>
|
||||
</g><g transform="translate(28.78679656440357,28.786796564403577)">
|
||||
<g transform="rotate(225)">
|
||||
<circle fill-opacity="0.375" fill="#ffd943" r="6" cy="0" cx="0">
|
||||
<animateTransform repeatCount="indefinite" dur="0.9345794392523364s" keyTimes="0;1" values="1.5 1.5;1 1" begin="-0.2336448598130841s" type="scale" attributeName="transform"></animateTransform>
|
||||
<animate begin="-0.2336448598130841s" values="1;0" repeatCount="indefinite" dur="0.9345794392523364s" keyTimes="0;1" attributeName="fill-opacity"></animate>
|
||||
</circle>
|
||||
</g>
|
||||
</g><g transform="translate(49.99999999999999,20)">
|
||||
<g transform="rotate(270)">
|
||||
<circle fill-opacity="0.25" fill="#ffd943" r="6" cy="0" cx="0">
|
||||
<animateTransform repeatCount="indefinite" dur="0.9345794392523364s" keyTimes="0;1" values="1.5 1.5;1 1" begin="-0.11682242990654206s" type="scale" attributeName="transform"></animateTransform>
|
||||
<animate begin="-0.11682242990654206s" values="1;0" repeatCount="indefinite" dur="0.9345794392523364s" keyTimes="0;1" attributeName="fill-opacity"></animate>
|
||||
</circle>
|
||||
</g>
|
||||
</g><g transform="translate(71.21320343559643,28.78679656440357)">
|
||||
<g transform="rotate(315)">
|
||||
<circle fill-opacity="0.125" fill="#ffd943" r="6" cy="0" cx="0">
|
||||
<animateTransform repeatCount="indefinite" dur="0.9345794392523364s" keyTimes="0;1" values="1.5 1.5;1 1" begin="0s" type="scale" attributeName="transform"></animateTransform>
|
||||
<animate begin="0s" values="1;0" repeatCount="indefinite" dur="0.9345794392523364s" keyTimes="0;1" attributeName="fill-opacity"></animate>
|
||||
</circle>
|
||||
</g>
|
||||
</g><g></g></g><!-- [ldio] generated by https://loading.io --></svg>
|
||||
|
After Width: | Height: | Size: 4.3 KiB |
@@ -3,6 +3,11 @@
|
||||
$default-font: "Prompt", sans-serif !default;
|
||||
$body-font-family: $default-font;
|
||||
$heading-font-family: $default-font;
|
||||
$body-background: #282c34;
|
||||
|
||||
body {
|
||||
background: $body-background;
|
||||
}
|
||||
|
||||
.v-application {
|
||||
font-family: $default-font !important;
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, inject, ref, onBeforeUnmount } from "vue";
|
||||
import { useCameraSettingsStore } from "@/stores/settings/CameraSettingsStore";
|
||||
import { useStateStore } from "@/stores/StateStore";
|
||||
import loadingImage from "@/assets/images/loading.svg";
|
||||
import loadingImage from "@/assets/images/loading-transparent.svg";
|
||||
import type { StyleValue } from "vue/types/jsx";
|
||||
import PvIcon from "@/components/common/pv-icon.vue";
|
||||
import type { UiCameraConfiguration } from "@/types/SettingTypes";
|
||||
|
||||
const props = defineProps<{
|
||||
streamType: "Raw" | "Processed";
|
||||
id: string;
|
||||
cameraSettings: UiCameraConfiguration;
|
||||
}>();
|
||||
|
||||
const emptyStreamSrc = "//:0";
|
||||
const streamSrc = computed<string>(() => {
|
||||
const port =
|
||||
useCameraSettingsStore().currentCameraSettings.stream[props.streamType === "Raw" ? "inputPort" : "outputPort"];
|
||||
const port = props.cameraSettings.stream[props.streamType === "Raw" ? "inputPort" : "outputPort"];
|
||||
|
||||
if (!useStateStore().backendConnected || port === 0) {
|
||||
return emptyStreamSrc;
|
||||
@@ -32,8 +32,12 @@ const streamStyle = computed<StyleValue>(() => {
|
||||
});
|
||||
|
||||
const containerStyle = computed<StyleValue>(() => {
|
||||
const resolution = useCameraSettingsStore().currentVideoFormat.resolution;
|
||||
const rotation = useCameraSettingsStore().currentPipelineSettings.inputImageRotationMode;
|
||||
if (props.cameraSettings.validVideoFormats.length === 0) {
|
||||
return { aspectRatio: "1/1" };
|
||||
}
|
||||
const resolution =
|
||||
props.cameraSettings.validVideoFormats[props.cameraSettings.pipelineSettings.cameraVideoModeIndex].resolution;
|
||||
const rotation = props.cameraSettings.pipelineSettings.inputImageRotationMode;
|
||||
if (rotation === 1 || rotation === 3) {
|
||||
return {
|
||||
aspectRatio: `${resolution.height}/${resolution.width}`
|
||||
@@ -54,9 +58,9 @@ const overlayStyle = computed<StyleValue>(() => {
|
||||
|
||||
const handleCaptureClick = () => {
|
||||
if (props.streamType === "Raw") {
|
||||
useCameraSettingsStore().saveInputSnapshot();
|
||||
props.cameraSettings.pipelineSettings[props.cameraSettings.currentPipelineIndex].saveInputSnapshot();
|
||||
} else {
|
||||
useCameraSettingsStore().saveOutputSnapshot();
|
||||
props.cameraSettings.pipelineSettings[props.cameraSettings.currentPipelineIndex].saveOutputSnapshot();
|
||||
}
|
||||
};
|
||||
const handlePopoutClick = () => {
|
||||
@@ -69,6 +73,16 @@ const handleFullscreenRequest = () => {
|
||||
};
|
||||
|
||||
const mjpgStream: any = ref(null);
|
||||
|
||||
const handleStreamError = () => {
|
||||
if (streamSrc.value && streamSrc.value !== emptyStreamSrc) {
|
||||
console.error("Error loading stream:", streamSrc.value, " Trying again.");
|
||||
setTimeout(() => {
|
||||
mjpgStream.value.src = streamSrc.value;
|
||||
}, 100);
|
||||
}
|
||||
};
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
if (!mjpgStream.value) return;
|
||||
mjpgStream.value["src"] = emptyStreamSrc;
|
||||
@@ -79,7 +93,6 @@ onBeforeUnmount(() => {
|
||||
<div class="stream-container" :style="containerStyle">
|
||||
<img :src="loadingImage" class="stream-loading" />
|
||||
<img
|
||||
v-show="streamSrc !== emptyStreamSrc"
|
||||
:id="id"
|
||||
ref="mjpgStream"
|
||||
class="stream-video"
|
||||
@@ -87,6 +100,7 @@ onBeforeUnmount(() => {
|
||||
:src="streamSrc"
|
||||
:alt="streamDesc"
|
||||
:style="streamStyle"
|
||||
@error="handleStreamError"
|
||||
/>
|
||||
<div class="stream-overlay" :style="overlayStyle">
|
||||
<pv-icon
|
||||
@@ -116,18 +130,25 @@ onBeforeUnmount(() => {
|
||||
display: flex;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.stream-loading {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
width: 25%;
|
||||
height: 25%;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.stream-video {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.stream-overlay {
|
||||
|
||||
@@ -2,6 +2,9 @@
|
||||
import { computed, getCurrentInstance } from "vue";
|
||||
import { useSettingsStore } from "@/stores/settings/GeneralSettingsStore";
|
||||
import { useStateStore } from "@/stores/StateStore";
|
||||
import { useCameraSettingsStore } from "@/stores/settings/CameraSettingsStore";
|
||||
import { PlaceholderCameraSettings } from "@/types/SettingTypes";
|
||||
import { useRoute } from "vue2-helpers/vue-router";
|
||||
|
||||
const compact = computed<boolean>({
|
||||
get: () => {
|
||||
@@ -14,6 +17,13 @@ const compact = computed<boolean>({
|
||||
|
||||
// Vuetify2 doesn't yet support the useDisplay API so this is required to access the prop when using the Composition API
|
||||
const mdAndUp = computed<boolean>(() => getCurrentInstance()?.proxy.$vuetify.breakpoint.mdAndUp || false);
|
||||
|
||||
const needsCamerasConfigured = computed<boolean>(() => {
|
||||
return (
|
||||
Object.values(useCameraSettingsStore().cameras).length === 0 ||
|
||||
useCameraSettingsStore().cameras["PlaceHolder Name"] === PlaceholderCameraSettings
|
||||
);
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -35,14 +45,6 @@ const mdAndUp = computed<boolean>(() => getCurrentInstance()?.proxy.$vuetify.bre
|
||||
<v-list-item-title>Dashboard</v-list-item-title>
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
<v-list-item ref="camerasTabOpener" link to="/cameras">
|
||||
<v-list-item-icon>
|
||||
<v-icon>mdi-camera</v-icon>
|
||||
</v-list-item-icon>
|
||||
<v-list-item-content>
|
||||
<v-list-item-title>Cameras</v-list-item-title>
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
<v-list-item link to="/settings">
|
||||
<v-list-item-icon>
|
||||
<v-icon>mdi-cog</v-icon>
|
||||
@@ -51,6 +53,26 @@ const mdAndUp = computed<boolean>(() => getCurrentInstance()?.proxy.$vuetify.bre
|
||||
<v-list-item-title>Settings</v-list-item-title>
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
<v-list-item ref="camerasTabOpener" link to="/cameras">
|
||||
<v-list-item-icon>
|
||||
<v-icon>mdi-camera</v-icon>
|
||||
</v-list-item-icon>
|
||||
<v-list-item-content>
|
||||
<v-list-item-title>Camera</v-list-item-title>
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
<v-list-item
|
||||
link
|
||||
to="/cameraConfigs"
|
||||
:class="{ cameraicon: needsCamerasConfigured && useRoute().path !== '/cameraConfigs' }"
|
||||
>
|
||||
<v-list-item-icon>
|
||||
<v-icon :class="{ 'red--text': needsCamerasConfigured }">mdi-swap-horizontal-bold</v-icon>
|
||||
</v-list-item-icon>
|
||||
<v-list-item-content>
|
||||
<v-list-item-title :class="{ 'red--text': needsCamerasConfigured }">Camera Matching</v-list-item-title>
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
<v-list-item link to="/docs">
|
||||
<v-list-item-icon>
|
||||
<v-icon>mdi-bookshelf</v-icon>
|
||||
@@ -59,17 +81,17 @@ const mdAndUp = computed<boolean>(() => getCurrentInstance()?.proxy.$vuetify.bre
|
||||
<v-list-item-title>Documentation</v-list-item-title>
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
<v-list-item v-if="mdAndUp" link @click="() => (compact = !compact)">
|
||||
<v-list-item-icon>
|
||||
<v-icon v-if="compact || !mdAndUp"> mdi-chevron-right </v-icon>
|
||||
<v-icon v-else> mdi-chevron-left </v-icon>
|
||||
</v-list-item-icon>
|
||||
<v-list-item-content>
|
||||
<v-list-item-title>Compact Mode</v-list-item-title>
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
|
||||
<div style="position: absolute; bottom: 0; left: 0">
|
||||
<v-list-item v-if="mdAndUp" link @click="() => (compact = !compact)">
|
||||
<v-list-item-icon>
|
||||
<v-icon v-if="compact || !mdAndUp"> mdi-chevron-right </v-icon>
|
||||
<v-icon v-else> mdi-chevron-left </v-icon>
|
||||
</v-list-item-icon>
|
||||
<v-list-item-content>
|
||||
<v-list-item-title>Compact Mode</v-list-item-title>
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
<v-list-item>
|
||||
<v-list-item-icon>
|
||||
<v-icon v-if="useSettingsStore().network.runNTServer"> mdi-server </v-icon>
|
||||
@@ -119,4 +141,18 @@ const mdAndUp = computed<boolean>(() => getCurrentInstance()?.proxy.$vuetify.bre
|
||||
height: 70px;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.cameraicon {
|
||||
animation: pulse 2s infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%,
|
||||
100% {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
50% {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -20,6 +20,7 @@ const settingsValid = ref(true);
|
||||
|
||||
const getUniqueVideoFormatsByResolution = (): VideoFormat[] => {
|
||||
const uniqueResolutions: VideoFormat[] = [];
|
||||
if (useCameraSettingsStore().currentCameraSettings.validVideoFormats.length === 0) return uniqueResolutions;
|
||||
useCameraSettingsStore().currentCameraSettings.validVideoFormats.forEach((format) => {
|
||||
const index = uniqueResolutions.findIndex((v) => resolutionsAreEqual(v.resolution, format.resolution));
|
||||
const contains = index != -1;
|
||||
@@ -157,7 +158,10 @@ const downloadCalibBoard = () => {
|
||||
doc.save(`calibrationTarget-${CalibrationBoardTypes[boardType.value]}.pdf`);
|
||||
};
|
||||
|
||||
const isCalibrating = ref(false);
|
||||
const isCalibrating = computed(
|
||||
() => useCameraSettingsStore().currentCameraSettings.currentPipelineIndex === WebsocketPipelineType.Calib3d
|
||||
);
|
||||
|
||||
const startCalibration = () => {
|
||||
useCameraSettingsStore().startPnPCalibration({
|
||||
squareSizeIn: squareSizeIn.value,
|
||||
@@ -170,13 +174,15 @@ const startCalibration = () => {
|
||||
});
|
||||
// The Start PnP method already handles updating the backend so only a store update is required
|
||||
useCameraSettingsStore().currentCameraSettings.currentPipelineIndex = WebsocketPipelineType.Calib3d;
|
||||
isCalibrating.value = true;
|
||||
// isCalibrating.value = true;
|
||||
calibCanceled.value = false;
|
||||
};
|
||||
const showCalibEndDialog = ref(false);
|
||||
const calibCanceled = ref(false);
|
||||
const calibSuccess = ref<boolean | undefined>(undefined);
|
||||
const endCalibration = () => {
|
||||
calibSuccess.value = undefined;
|
||||
|
||||
if (!useStateStore().calibrationData.hasEnoughImages) {
|
||||
calibCanceled.value = true;
|
||||
}
|
||||
@@ -192,7 +198,8 @@ const endCalibration = () => {
|
||||
calibSuccess.value = false;
|
||||
})
|
||||
.finally(() => {
|
||||
isCalibrating.value = false;
|
||||
// isCalibrating.value = false;
|
||||
// backend deals with this for us
|
||||
});
|
||||
};
|
||||
|
||||
@@ -208,277 +215,256 @@ const setSelectedVideoFormat = (format: VideoFormat) => {
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<v-card class="mb-3 pr-6 pb-3" color="primary" dark>
|
||||
<v-card-title>Camera Calibration</v-card-title>
|
||||
<div class="ml-5">
|
||||
<v-row v-show="!isCalibrating" class="pb-12">
|
||||
<v-card-subtitle class="pb-0 mb-0 pl-3">Complete Calibrations</v-card-subtitle>
|
||||
<v-simple-table fixed-header height="100%" dense class="mt-2">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Resolution</th>
|
||||
<th>Mean Error</th>
|
||||
<th>Horizontal FOV</th>
|
||||
<th>Vertical FOV</th>
|
||||
<th>Diagonal FOV</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr
|
||||
v-for="(value, index) in getUniqueVideoFormatsByResolution()"
|
||||
:key="index"
|
||||
title="Click to get calibration specific information"
|
||||
@click="setSelectedVideoFormat(value)"
|
||||
>
|
||||
<td>{{ getResolutionString(value.resolution) }}</td>
|
||||
<td>
|
||||
{{ value.mean !== undefined ? (isNaN(value.mean) ? "Unknown" : value.mean.toFixed(2) + "px") : "-" }}
|
||||
</td>
|
||||
<td>{{ value.horizontalFOV !== undefined ? value.horizontalFOV.toFixed(2) + "°" : "-" }}</td>
|
||||
<td>{{ value.verticalFOV !== undefined ? value.verticalFOV.toFixed(2) + "°" : "-" }}</td>
|
||||
<td>{{ value.diagonalFOV !== undefined ? value.diagonalFOV.toFixed(2) + "°" : "-" }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</v-simple-table>
|
||||
</v-row>
|
||||
<v-divider />
|
||||
<v-row style="display: flex; flex-direction: column" class="mt-4">
|
||||
<v-card-subtitle v-show="!isCalibrating" class="pl-3 pa-0 ma-0"> Configure New Calibration</v-card-subtitle>
|
||||
<v-form ref="form" v-model="settingsValid" class="pl-4 mb-10 pr-5">
|
||||
<pv-select
|
||||
v-model="useStateStore().calibrationData.videoFormatIndex"
|
||||
label="Resolution"
|
||||
:select-cols="8"
|
||||
:disabled="isCalibrating"
|
||||
tooltip="Resolution to calibrate at (you will have to calibrate every resolution you use 3D mode on)"
|
||||
:items="getUniqueVideoResolutionStrings()"
|
||||
/>
|
||||
<pv-select
|
||||
v-show="isCalibrating && boardType != CalibrationBoardTypes.Charuco"
|
||||
v-model="useCameraSettingsStore().currentPipelineSettings.streamingFrameDivisor"
|
||||
label="Decimation"
|
||||
tooltip="Resolution to which camera frames are downscaled for detection. Calibration still uses full-res"
|
||||
:items="calibrationDivisors"
|
||||
:select-cols="8"
|
||||
@input="(v) => useCameraSettingsStore().changeCurrentPipelineSetting({ streamingFrameDivisor: v }, false)"
|
||||
/>
|
||||
<pv-select
|
||||
v-model="boardType"
|
||||
label="Board Type"
|
||||
tooltip="Calibration board pattern to use"
|
||||
:select-cols="8"
|
||||
:items="['Chessboard', 'Charuco']"
|
||||
:disabled="isCalibrating"
|
||||
/>
|
||||
<pv-select
|
||||
v-show="boardType == CalibrationBoardTypes.Charuco"
|
||||
v-model="tagFamily"
|
||||
label="Tag Family"
|
||||
tooltip="Dictionary of aruco markers on the charuco board"
|
||||
:select-cols="8"
|
||||
:items="['Dict_4X4_1000', 'Dict_5X5_1000', 'Dict_6X6_1000', 'Dict_7X7_1000']"
|
||||
:disabled="isCalibrating"
|
||||
/>
|
||||
<pv-number-input
|
||||
v-model="squareSizeIn"
|
||||
label="Pattern Spacing (in)"
|
||||
tooltip="Spacing between pattern features in inches"
|
||||
:disabled="isCalibrating"
|
||||
:rules="[(v) => v > 0 || 'Size must be positive']"
|
||||
:label-cols="4"
|
||||
/>
|
||||
<pv-number-input
|
||||
v-show="boardType == CalibrationBoardTypes.Charuco"
|
||||
v-model="markerSizeIn"
|
||||
label="Marker Size (in)"
|
||||
tooltip="Size of the tag markers in inches must be smaller than pattern spacing"
|
||||
:disabled="isCalibrating"
|
||||
:rules="[(v) => v > 0 || 'Size must be positive']"
|
||||
:label-cols="4"
|
||||
/>
|
||||
<pv-number-input
|
||||
v-model="patternWidth"
|
||||
label="Board Width (squares)"
|
||||
tooltip="Width of the board in dots or chessboard squares"
|
||||
:disabled="isCalibrating"
|
||||
:rules="[(v) => v >= 4 || 'Width must be at least 4']"
|
||||
:label-cols="4"
|
||||
/>
|
||||
<pv-number-input
|
||||
v-model="patternHeight"
|
||||
label="Board Height (squares)"
|
||||
tooltip="Height of the board in dots or chessboard squares"
|
||||
:disabled="isCalibrating"
|
||||
:rules="[(v) => v >= 4 || 'Height must be at least 4']"
|
||||
:label-cols="4"
|
||||
/>
|
||||
<pv-switch
|
||||
v-show="boardType == CalibrationBoardTypes.Charuco"
|
||||
v-model="useOldPattern"
|
||||
label="Old OpenCV Pattern"
|
||||
:disabled="isCalibrating"
|
||||
tooltip="If enabled, Photon will use the old OpenCV pattern for calibration."
|
||||
:label-cols="4"
|
||||
/>
|
||||
<v-banner
|
||||
v-show="useSettingsStore().general.mrCalWorking"
|
||||
rounded
|
||||
color="secondary"
|
||||
text-color="white"
|
||||
class="mt-3"
|
||||
icon="mdi-alert-circle-outline"
|
||||
>
|
||||
Mrcal was successfully loaded, and will be used!
|
||||
</v-banner>
|
||||
<v-banner
|
||||
v-show="!useSettingsStore().general.mrCalWorking"
|
||||
rounded
|
||||
color="red"
|
||||
text-color="white"
|
||||
class="mt-3"
|
||||
icon="mdi-alert-circle-outline"
|
||||
>
|
||||
MrCal JNI could not be loaded! Consult journalctl logs for additional details.
|
||||
</v-banner>
|
||||
</v-form>
|
||||
<v-row justify="center">
|
||||
<v-chip
|
||||
v-show="isCalibrating"
|
||||
label
|
||||
:color="useStateStore().calibrationData.hasEnoughImages ? 'secondary' : 'gray'"
|
||||
class="mb-6"
|
||||
>
|
||||
Snapshots: {{ useStateStore().calibrationData.imageCount }} of at least
|
||||
{{ useStateStore().calibrationData.minimumImageCount }}
|
||||
</v-chip>
|
||||
</v-row>
|
||||
</v-row>
|
||||
<v-row v-if="isCalibrating">
|
||||
<v-col cols="12" class="pt-0">
|
||||
<pv-slider
|
||||
v-model="useCameraSettingsStore().currentPipelineSettings.cameraExposureRaw"
|
||||
:disabled="useCameraSettingsStore().currentCameraSettings.pipelineSettings.cameraAutoExposure"
|
||||
label="Exposure"
|
||||
tooltip="Directly controls how long the camera shutter remains open. Units are dependant on the underlying driver."
|
||||
:min="useCameraSettingsStore().minExposureRaw"
|
||||
:max="useCameraSettingsStore().maxExposureRaw"
|
||||
:slider-cols="8"
|
||||
:step="1"
|
||||
@input="
|
||||
(args) => useCameraSettingsStore().changeCurrentPipelineSetting({ cameraExposureRaw: args }, false)
|
||||
"
|
||||
/>
|
||||
<pv-slider
|
||||
v-model="useCameraSettingsStore().currentPipelineSettings.cameraBrightness"
|
||||
label="Brightness"
|
||||
:min="0"
|
||||
:max="100"
|
||||
:slider-cols="8"
|
||||
@input="
|
||||
(args) => useCameraSettingsStore().changeCurrentPipelineSetting({ cameraBrightness: args }, false)
|
||||
"
|
||||
/>
|
||||
<pv-switch
|
||||
v-model="useCameraSettingsStore().currentPipelineSettings.cameraAutoExposure"
|
||||
class="pt-2"
|
||||
label="Auto Exposure"
|
||||
:label-cols="4"
|
||||
tooltip="Enables or Disables camera automatic adjustment for current lighting conditions"
|
||||
@input="
|
||||
(args) => useCameraSettingsStore().changeCurrentPipelineSetting({ cameraAutoExposure: args }, false)
|
||||
"
|
||||
/>
|
||||
<pv-slider
|
||||
v-if="useCameraSettingsStore().currentPipelineSettings.cameraGain >= 0"
|
||||
v-model="useCameraSettingsStore().currentPipelineSettings.cameraGain"
|
||||
label="Camera Gain"
|
||||
tooltip="Controls camera gain, similar to brightness"
|
||||
:min="0"
|
||||
:max="100"
|
||||
@input="(args) => useCameraSettingsStore().changeCurrentPipelineSetting({ cameraGain: args }, false)"
|
||||
/>
|
||||
<pv-slider
|
||||
v-if="useCameraSettingsStore().currentPipelineSettings.cameraRedGain !== -1"
|
||||
v-model="useCameraSettingsStore().currentPipelineSettings.cameraRedGain"
|
||||
label="Red AWB Gain"
|
||||
:min="0"
|
||||
:max="100"
|
||||
tooltip="Controls red automatic white balance gain, which affects how the camera captures colors in different conditions"
|
||||
@input="(args) => useCameraSettingsStore().changeCurrentPipelineSetting({ cameraRedGain: args }, false)"
|
||||
/>
|
||||
<pv-slider
|
||||
v-if="useCameraSettingsStore().currentPipelineSettings.cameraBlueGain !== -1"
|
||||
v-model="useCameraSettingsStore().currentPipelineSettings.cameraBlueGain"
|
||||
label="Blue AWB Gain"
|
||||
:min="0"
|
||||
:max="100"
|
||||
tooltip="Controls blue automatic white balance gain, which affects how the camera captures colors in different conditions"
|
||||
@input="(args) => useCameraSettingsStore().changeCurrentPipelineSetting({ cameraBlueGain: args }, false)"
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col v-if="tooManyPoints" :cols="12">
|
||||
<v-banner rounded color="red" text-color="white" class="mt-3" icon="mdi-alert-circle-outline">
|
||||
Too many corners - finish calibration now!
|
||||
</v-banner>
|
||||
</v-col>
|
||||
<v-col :cols="6">
|
||||
<v-btn
|
||||
small
|
||||
color="secondary"
|
||||
style="width: 100%"
|
||||
:disabled="!settingsValid || tooManyPoints"
|
||||
@click="isCalibrating ? useCameraSettingsStore().takeCalibrationSnapshot() : startCalibration()"
|
||||
>
|
||||
<v-icon left class="calib-btn-icon"> {{ isCalibrating ? "mdi-camera" : "mdi-flag-outline" }} </v-icon>
|
||||
<span class="calib-btn-label">{{ isCalibrating ? "Take Snapshot" : "Start Calibration" }}</span>
|
||||
</v-btn>
|
||||
</v-col>
|
||||
<v-col :cols="6">
|
||||
<v-btn
|
||||
small
|
||||
:color="useStateStore().calibrationData.hasEnoughImages ? 'accent' : 'red'"
|
||||
:class="useStateStore().calibrationData.hasEnoughImages ? 'black--text' : 'white---text'"
|
||||
style="width: 100%"
|
||||
:disabled="!isCalibrating || !settingsValid"
|
||||
@click="endCalibration"
|
||||
>
|
||||
<v-icon left class="calib-btn-icon">
|
||||
{{ useStateStore().calibrationData.hasEnoughImages ? "mdi-flag-checkered" : "mdi-flag-off-outline" }}
|
||||
</v-icon>
|
||||
<span class="calib-btn-label">{{
|
||||
useStateStore().calibrationData.hasEnoughImages ? "Finish Calibration" : "Cancel Calibration"
|
||||
}}</span>
|
||||
</v-btn>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row justify="center">
|
||||
<v-col cols="12">
|
||||
<v-btn
|
||||
color="accent"
|
||||
small
|
||||
outlined
|
||||
style="width: 100%"
|
||||
:disabled="!settingsValid"
|
||||
@click="downloadCalibBoard"
|
||||
>
|
||||
<v-icon left class="calib-btn-icon"> mdi-download </v-icon>
|
||||
<span class="calib-btn-label">Generate Board</span>
|
||||
</v-btn>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row v-if="isCalibrating" style="display: flex; flex-direction: column">
|
||||
<pv-switch
|
||||
v-model="drawAllSnapshots"
|
||||
class="pt-2"
|
||||
label="Draw Collected Corners"
|
||||
:switch-cols="8"
|
||||
tooltip="Draw all snapshots"
|
||||
@input="(args) => useCameraSettingsStore().changeCurrentPipelineSetting({ drawAllSnapshots: args }, false)"
|
||||
<v-card class="mb-3" color="primary" dark>
|
||||
<v-card-title class="pa-6 pb-3">Camera Calibration</v-card-title>
|
||||
<v-card-text v-show="!isCalibrating">
|
||||
<v-card-subtitle class="pt-3 pl-2 pb-4 white--text">Current Calibration</v-card-subtitle>
|
||||
<v-simple-table fixed-header height="100%" dense>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Resolution</th>
|
||||
<th>Mean Error</th>
|
||||
<th>Horizontal FOV</th>
|
||||
<th>Vertical FOV</th>
|
||||
<th>Diagonal FOV</th>
|
||||
<th>Info</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody style="cursor: pointer">
|
||||
<tr v-for="(value, index) in getUniqueVideoFormatsByResolution()" :key="index">
|
||||
<td>{{ getResolutionString(value.resolution) }}</td>
|
||||
<td>
|
||||
{{ value.mean !== undefined ? (isNaN(value.mean) ? "Unknown" : value.mean.toFixed(2) + "px") : "-" }}
|
||||
</td>
|
||||
<td>{{ value.horizontalFOV !== undefined ? value.horizontalFOV.toFixed(2) + "°" : "-" }}</td>
|
||||
<td>{{ value.verticalFOV !== undefined ? value.verticalFOV.toFixed(2) + "°" : "-" }}</td>
|
||||
<td>{{ value.diagonalFOV !== undefined ? value.diagonalFOV.toFixed(2) + "°" : "-" }}</td>
|
||||
<v-tooltip bottom>
|
||||
<template #activator="{ on, attrs }">
|
||||
<td v-bind="attrs" v-on="on" @click="setSelectedVideoFormat(value)">
|
||||
<v-icon small class="mr-2">mdi-information</v-icon>
|
||||
</td>
|
||||
</template>
|
||||
<span>Click for more info on this calibration.</span>
|
||||
</v-tooltip>
|
||||
</tr>
|
||||
</tbody>
|
||||
</v-simple-table>
|
||||
</v-card-text>
|
||||
<v-card-text v-if="useCameraSettingsStore().isConnected" class="d-flex flex-column pa-6 pt-0">
|
||||
<v-card-subtitle v-show="!isCalibrating" class="pl-0 pb-3 pt-3 white--text"
|
||||
>Configure New Calibration</v-card-subtitle
|
||||
>
|
||||
<v-form ref="form" v-model="settingsValid">
|
||||
<!-- TODO: the default videoFormatIndex is 0, but the list of unique video mode indexes might not include 0. getUniqueVideoResolutionStrings indexing is also different from the normal video mode indexing -->
|
||||
<pv-select
|
||||
v-model="useStateStore().calibrationData.videoFormatIndex"
|
||||
label="Resolution"
|
||||
:select-cols="8"
|
||||
:disabled="isCalibrating"
|
||||
tooltip="Resolution to calibrate at (you will have to calibrate every resolution you use 3D mode on)"
|
||||
:items="getUniqueVideoResolutionStrings()"
|
||||
/>
|
||||
</v-row>
|
||||
</div>
|
||||
<pv-select
|
||||
v-show="isCalibrating && boardType != CalibrationBoardTypes.Charuco"
|
||||
v-model="useCameraSettingsStore().currentPipelineSettings.streamingFrameDivisor"
|
||||
label="Decimation"
|
||||
tooltip="Resolution to which camera frames are downscaled for detection. Calibration still uses full-res"
|
||||
:items="calibrationDivisors"
|
||||
:select-cols="8"
|
||||
@input="(v) => useCameraSettingsStore().changeCurrentPipelineSetting({ streamingFrameDivisor: +v }, false)"
|
||||
/>
|
||||
<pv-select
|
||||
v-model="boardType"
|
||||
label="Board Type"
|
||||
tooltip="Calibration board pattern to use"
|
||||
:select-cols="8"
|
||||
:items="['Chessboard', 'Charuco']"
|
||||
:disabled="isCalibrating"
|
||||
/>
|
||||
<pv-select
|
||||
v-show="boardType == CalibrationBoardTypes.Charuco"
|
||||
v-model="tagFamily"
|
||||
label="Tag Family"
|
||||
tooltip="Dictionary of aruco markers on the charuco board"
|
||||
:select-cols="8"
|
||||
:items="['Dict_4X4_1000', 'Dict_5X5_1000', 'Dict_6X6_1000', 'Dict_7X7_1000']"
|
||||
:disabled="isCalibrating"
|
||||
/>
|
||||
<pv-number-input
|
||||
v-model="squareSizeIn"
|
||||
label="Pattern Spacing (in)"
|
||||
tooltip="Spacing between pattern features in inches"
|
||||
:disabled="isCalibrating"
|
||||
:rules="[(v) => v > 0 || 'Size must be positive']"
|
||||
:label-cols="4"
|
||||
/>
|
||||
<pv-number-input
|
||||
v-show="boardType == CalibrationBoardTypes.Charuco"
|
||||
v-model="markerSizeIn"
|
||||
label="Marker Size (in)"
|
||||
tooltip="Size of the tag markers in inches must be smaller than pattern spacing"
|
||||
:disabled="isCalibrating"
|
||||
:rules="[(v) => v > 0 || 'Size must be positive']"
|
||||
:label-cols="4"
|
||||
/>
|
||||
<pv-number-input
|
||||
v-model="patternWidth"
|
||||
label="Board Width (squares)"
|
||||
tooltip="Width of the board in dots or chessboard squares"
|
||||
:disabled="isCalibrating"
|
||||
:rules="[(v) => v >= 4 || 'Width must be at least 4']"
|
||||
:label-cols="4"
|
||||
/>
|
||||
<pv-number-input
|
||||
v-model="patternHeight"
|
||||
label="Board Height (squares)"
|
||||
tooltip="Height of the board in dots or chessboard squares"
|
||||
:disabled="isCalibrating"
|
||||
:rules="[(v) => v >= 4 || 'Height must be at least 4']"
|
||||
:label-cols="4"
|
||||
/>
|
||||
<pv-switch
|
||||
v-show="boardType == CalibrationBoardTypes.Charuco"
|
||||
v-model="useOldPattern"
|
||||
label="Old OpenCV Pattern"
|
||||
:disabled="isCalibrating"
|
||||
tooltip="If enabled, Photon will use the old OpenCV pattern for calibration."
|
||||
:label-cols="4"
|
||||
/>
|
||||
<v-banner
|
||||
v-if="useSettingsStore().general.mrCalWorking"
|
||||
rounded
|
||||
color="secondary"
|
||||
text-color="white"
|
||||
class="mt-3"
|
||||
icon="mdi-alert-circle-outline"
|
||||
>
|
||||
Mrcal was successfully loaded and will be used!
|
||||
</v-banner>
|
||||
<v-banner v-else rounded color="error" text-color="white" class="mt-3" icon="mdi-alert-circle-outline">
|
||||
MrCal JNI could not be loaded! Consult journalctl logs for additional details.
|
||||
</v-banner>
|
||||
</v-form>
|
||||
</v-card-text>
|
||||
<v-card-text v-if="isCalibrating" class="pa-6 pt-0">
|
||||
<pv-switch
|
||||
v-model="drawAllSnapshots"
|
||||
label="Draw Collected Corners"
|
||||
:switch-cols="8"
|
||||
tooltip="Draw all snapshots"
|
||||
@input="(args) => useCameraSettingsStore().changeCurrentPipelineSetting({ drawAllSnapshots: args }, false)"
|
||||
/>
|
||||
<pv-switch
|
||||
v-model="useCameraSettingsStore().currentPipelineSettings.cameraAutoExposure"
|
||||
label="Auto Exposure"
|
||||
:label-cols="4"
|
||||
tooltip="Enables or Disables camera automatic adjustment for current lighting conditions"
|
||||
@input="(args) => useCameraSettingsStore().changeCurrentPipelineSetting({ cameraAutoExposure: args }, false)"
|
||||
/>
|
||||
<pv-slider
|
||||
v-model="useCameraSettingsStore().currentPipelineSettings.cameraExposureRaw"
|
||||
:disabled="useCameraSettingsStore().currentCameraSettings.pipelineSettings.cameraAutoExposure"
|
||||
label="Exposure"
|
||||
tooltip="Directly controls how long the camera shutter remains open. Units are dependant on the underlying driver."
|
||||
:min="useCameraSettingsStore().minExposureRaw"
|
||||
:max="useCameraSettingsStore().maxExposureRaw"
|
||||
:slider-cols="7"
|
||||
:step="1"
|
||||
@input="(args) => useCameraSettingsStore().changeCurrentPipelineSetting({ cameraExposureRaw: args }, false)"
|
||||
/>
|
||||
<pv-slider
|
||||
v-model="useCameraSettingsStore().currentPipelineSettings.cameraBrightness"
|
||||
label="Brightness"
|
||||
:min="0"
|
||||
:max="100"
|
||||
:slider-cols="7"
|
||||
@input="(args) => useCameraSettingsStore().changeCurrentPipelineSetting({ cameraBrightness: args }, false)"
|
||||
/>
|
||||
<pv-slider
|
||||
v-if="useCameraSettingsStore().currentPipelineSettings.cameraGain >= 0"
|
||||
v-model="useCameraSettingsStore().currentPipelineSettings.cameraGain"
|
||||
label="Camera Gain"
|
||||
tooltip="Controls camera gain, similar to brightness"
|
||||
:min="0"
|
||||
:max="100"
|
||||
:slider-cols="7"
|
||||
@input="(args) => useCameraSettingsStore().changeCurrentPipelineSetting({ cameraGain: args }, false)"
|
||||
/>
|
||||
<pv-slider
|
||||
v-if="useCameraSettingsStore().currentPipelineSettings.cameraRedGain !== -1"
|
||||
v-model="useCameraSettingsStore().currentPipelineSettings.cameraRedGain"
|
||||
label="Red AWB Gain"
|
||||
:min="0"
|
||||
:max="100"
|
||||
:slider-cols="7"
|
||||
tooltip="Controls red automatic white balance gain, which affects how the camera captures colors in different conditions"
|
||||
@input="(args) => useCameraSettingsStore().changeCurrentPipelineSetting({ cameraRedGain: args }, false)"
|
||||
/>
|
||||
<pv-slider
|
||||
v-if="useCameraSettingsStore().currentPipelineSettings.cameraBlueGain !== -1"
|
||||
v-model="useCameraSettingsStore().currentPipelineSettings.cameraBlueGain"
|
||||
label="Blue AWB Gain"
|
||||
:min="0"
|
||||
:max="100"
|
||||
:slider-cols="7"
|
||||
tooltip="Controls blue automatic white balance gain, which affects how the camera captures colors in different conditions"
|
||||
@input="(args) => useCameraSettingsStore().changeCurrentPipelineSetting({ cameraBlueGain: args }, false)"
|
||||
/>
|
||||
<v-banner
|
||||
v-if="tooManyPoints"
|
||||
rounded
|
||||
class="mt-3"
|
||||
color="error"
|
||||
text-color="white"
|
||||
icon="mdi-alert-circle-outline"
|
||||
>
|
||||
Too many corners. Finish calibration now!
|
||||
</v-banner>
|
||||
</v-card-text>
|
||||
<v-card-text v-if="isCalibrating" class="d-flex justify-center align-center pa-6 pt-0">
|
||||
<v-chip label :color="useStateStore().calibrationData.hasEnoughImages ? 'secondary' : 'gray'">
|
||||
Snapshots: {{ useStateStore().calibrationData.imageCount }} of at least
|
||||
{{ useStateStore().calibrationData.minimumImageCount }}
|
||||
</v-chip>
|
||||
</v-card-text>
|
||||
<v-card-text class="d-flex pa-6 pt-0">
|
||||
<v-col cols="6" class="pa-0 pr-2">
|
||||
<v-btn
|
||||
small
|
||||
block
|
||||
color="secondary"
|
||||
:disabled="!settingsValid || tooManyPoints"
|
||||
@click="isCalibrating ? useCameraSettingsStore().takeCalibrationSnapshot() : startCalibration()"
|
||||
>
|
||||
<v-icon left class="calib-btn-icon"> {{ isCalibrating ? "mdi-camera" : "mdi-flag-outline" }} </v-icon>
|
||||
<span class="calib-btn-label">{{ isCalibrating ? "Take Snapshot" : "Start Calibration" }}</span>
|
||||
</v-btn>
|
||||
</v-col>
|
||||
<v-col cols="6" class="pa-0 pl-2">
|
||||
<v-btn
|
||||
small
|
||||
block
|
||||
:color="useStateStore().calibrationData.hasEnoughImages ? 'accent' : 'error'"
|
||||
:class="useStateStore().calibrationData.hasEnoughImages ? 'black--text' : 'white---text'"
|
||||
:disabled="!isCalibrating || !settingsValid"
|
||||
@click="endCalibration"
|
||||
>
|
||||
<v-icon left class="calib-btn-icon">
|
||||
{{ useStateStore().calibrationData.hasEnoughImages ? "mdi-flag-checkered" : "mdi-flag-off-outline" }}
|
||||
</v-icon>
|
||||
<span class="calib-btn-label">{{
|
||||
useStateStore().calibrationData.hasEnoughImages ? "Finish Calibration" : "Cancel Calibration"
|
||||
}}</span>
|
||||
</v-btn>
|
||||
</v-col>
|
||||
</v-card-text>
|
||||
<v-card-text class="pa-6 pt-0">
|
||||
<v-btn color="accent" small block outlined :disabled="!settingsValid" @click="downloadCalibBoard">
|
||||
<v-icon left class="calib-btn-icon"> mdi-download </v-icon>
|
||||
<span class="calib-btn-label">Generate Board</span>
|
||||
</v-btn>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
<v-dialog v-model="showCalibEndDialog" width="500px" :persistent="true">
|
||||
<v-card color="primary" dark>
|
||||
@@ -492,10 +478,12 @@ const setSelectedVideoFormat = (format: VideoFormat) => {
|
||||
process.</v-card-text
|
||||
>
|
||||
</template>
|
||||
<template v-else-if="isCalibrating">
|
||||
<!-- No result reported yet -->
|
||||
<template v-else-if="calibSuccess === undefined">
|
||||
<v-progress-circular indeterminate :size="70" :width="8" color="accent" />
|
||||
<v-card-text>Camera is being calibrated. This process may take several minutes...</v-card-text>
|
||||
</template>
|
||||
<!-- Got positive result -->
|
||||
<template v-else-if="calibSuccess">
|
||||
<v-icon color="green" size="70"> mdi-check-bold </v-icon>
|
||||
<v-card-text>
|
||||
|
||||
@@ -90,18 +90,13 @@ const calibrationImageURL = (index: number) =>
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<v-card color="primary" class="pa-6" dark>
|
||||
<v-row>
|
||||
<v-col cols="12" md="5">
|
||||
<v-card-title class="pl-0 ml-0"
|
||||
><span class="text-no-wrap" style="white-space: pre !important">Calibration Details: </span
|
||||
><span class="text-no-wrap"
|
||||
>{{ useCameraSettingsStore().currentCameraName }}@{{ getResolutionString(videoFormat.resolution) }}</span
|
||||
></v-card-title
|
||||
>
|
||||
<v-card color="primary" dark>
|
||||
<div class="d-flex flex-wrap pr-md-3">
|
||||
<v-col cols="12" md="6">
|
||||
<v-card-title class="pl-3 pb-0 pb-md-4"> Calibration Details </v-card-title>
|
||||
</v-col>
|
||||
<v-col>
|
||||
<v-btn color="secondary" class="mt-4" style="width: 100%" @click="openUploadPhotonCalibJsonPrompt">
|
||||
<v-col cols="6" md="3" class="d-flex align-center pt-0 pt-md-3 pl-6 pl-md-3">
|
||||
<v-btn color="secondary" style="width: 100%" @click="openUploadPhotonCalibJsonPrompt">
|
||||
<v-icon left> mdi-import</v-icon>
|
||||
<span>Import</span>
|
||||
</v-btn>
|
||||
@@ -113,10 +108,9 @@ const calibrationImageURL = (index: number) =>
|
||||
@change="importCalibration"
|
||||
/>
|
||||
</v-col>
|
||||
<v-col>
|
||||
<v-col cols="6" md="3" class="d-flex align-center pt-0 pt-md-3 pr-6 pr-md-3">
|
||||
<v-btn
|
||||
color="secondary"
|
||||
class="mt-4"
|
||||
:disabled="!currentCalibrationCoeffs"
|
||||
style="width: 100%"
|
||||
@click="openExportCalibrationPrompt"
|
||||
@@ -131,10 +125,17 @@ const calibrationImageURL = (index: number) =>
|
||||
target="_blank"
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row v-if="currentCalibrationCoeffs" class="pt-2">
|
||||
<v-card-subtitle>Calibration Details</v-card-subtitle>
|
||||
<v-simple-table dense style="width: 100%" class="pl-2 pr-2">
|
||||
</div>
|
||||
<v-card-title class="pt-0 pb-3"
|
||||
>{{ useCameraSettingsStore().currentCameraName }}@{{ getResolutionString(videoFormat.resolution) }}</v-card-title
|
||||
>
|
||||
<v-card-text v-if="!currentCalibrationCoeffs">
|
||||
<v-banner rounded color="secondary" text-color="white" class="mt-3" icon="mdi-alert-circle-outline">
|
||||
The selected video format has not been calibrated.
|
||||
</v-banner>
|
||||
</v-card-text>
|
||||
<v-card-text>
|
||||
<v-simple-table dense style="width: 100%">
|
||||
<template #default>
|
||||
<thead>
|
||||
<tr>
|
||||
@@ -238,15 +239,16 @@ const calibrationImageURL = (index: number) =>
|
||||
</tbody>
|
||||
</template>
|
||||
</v-simple-table>
|
||||
<hr style="width: 100%" class="ma-6" />
|
||||
<v-card-subtitle>Per Observation Details</v-card-subtitle>
|
||||
</v-card-text>
|
||||
<v-card-title v-if="currentCalibrationCoeffs" class="pt-0">Individual Observations</v-card-title>
|
||||
<v-card-text v-if="currentCalibrationCoeffs">
|
||||
<v-data-table
|
||||
dense
|
||||
style="width: 100%"
|
||||
class="pl-2 pr-2"
|
||||
:headers="[
|
||||
{ text: 'Observation Id', value: 'index' },
|
||||
{ text: 'Mean Reprojection Error', value: 'mean' }
|
||||
{ text: 'Mean Reprojection Error', value: 'mean' },
|
||||
{ text: '', value: 'data-table-expand' }
|
||||
]"
|
||||
:items="getObservationDetails()"
|
||||
item-key="index"
|
||||
@@ -261,10 +263,7 @@ const calibrationImageURL = (index: number) =>
|
||||
</td>
|
||||
</template>
|
||||
</v-data-table>
|
||||
</v-row>
|
||||
<v-row v-else class="pt-2 mb-0 pb-0">
|
||||
The selected video format doesn't have any additional information as it has yet to be calibrated.
|
||||
</v-row>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
@@ -275,7 +274,6 @@ const calibrationImageURL = (index: number) =>
|
||||
.snapshot-preview {
|
||||
max-width: 55%;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 512px) {
|
||||
.snapshot-preview {
|
||||
max-width: 100%;
|
||||
|
||||
@@ -91,16 +91,14 @@ const expanded = ref([]);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<v-card dark class="pr-6 pb-3" style="background-color: #006492">
|
||||
<v-card dark style="background-color: #006492">
|
||||
<v-card-title>Camera Control</v-card-title>
|
||||
<v-row class="pl-6">
|
||||
<v-col>
|
||||
<v-btn color="secondary" @click="fetchSnapshots">
|
||||
<v-icon left class="open-icon"> mdi-folder </v-icon>
|
||||
<span class="open-label">Show Saved Snapshots</span>
|
||||
</v-btn>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-card-text>
|
||||
<v-btn color="secondary" @click="fetchSnapshots">
|
||||
<v-icon left class="open-icon"> mdi-folder </v-icon>
|
||||
<span class="open-label">Show Saved Snapshots</span>
|
||||
</v-btn>
|
||||
</v-card-text>
|
||||
<v-dialog v-model="showSnapshotViewerDialog">
|
||||
<v-card dark class="pt-3 pl-5 pr-5" color="primary" flat>
|
||||
<v-card-title> View Saved Frame Snapshots </v-card-title>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import PvSelect from "@/components/common/pv-select.vue";
|
||||
import PvSelect, { type SelectItem } from "@/components/common/pv-select.vue";
|
||||
import PvNumberInput from "@/components/common/pv-number-input.vue";
|
||||
import { useCameraSettingsStore } from "@/stores/settings/CameraSettingsStore";
|
||||
import { useStateStore } from "@/stores/StateStore";
|
||||
@@ -119,9 +119,13 @@ const openExportSettingsPrompt = () => {
|
||||
};
|
||||
|
||||
const yesDeleteMySettingsText = ref("");
|
||||
const deletingCamera = ref(false);
|
||||
const deleteThisCamera = () => {
|
||||
if (deletingCamera.value) return;
|
||||
deletingCamera.value = true;
|
||||
|
||||
const payload = {
|
||||
cameraUniqueName: useCameraSettingsStore().cameraUniqueNames[useStateStore().currentCameraIndex]
|
||||
cameraUniqueName: useStateStore().currentCameraUniqueName
|
||||
};
|
||||
|
||||
axios
|
||||
@@ -149,19 +153,28 @@ const deleteThisCamera = () => {
|
||||
color: "error"
|
||||
});
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
deletingCamera.value = false;
|
||||
showDeleteCamera.value = false;
|
||||
});
|
||||
showDeleteCamera.value = false;
|
||||
};
|
||||
const wrappedCameras = computed<SelectItem[]>(() =>
|
||||
Object.keys(useCameraSettingsStore().cameras).map((cameraUniqueName) => ({
|
||||
name: useCameraSettingsStore().cameras[cameraUniqueName].nickname,
|
||||
value: cameraUniqueName
|
||||
}))
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<v-card class="mb-3 pr-6 pb-3" color="primary" dark>
|
||||
<v-card-title>Camera Settings</v-card-title>
|
||||
<div class="ml-5">
|
||||
<v-card class="mb-3" color="primary" dark>
|
||||
<v-card-title class="pa-6 pb-0">Camera Settings</v-card-title>
|
||||
<v-card-text class="pa-6 pt-3">
|
||||
<pv-select
|
||||
v-model="useStateStore().currentCameraIndex"
|
||||
v-model="useStateStore().currentCameraUniqueName"
|
||||
label="Camera"
|
||||
:items="useCameraSettingsStore().cameraNames"
|
||||
:items="wrappedCameras"
|
||||
:select-cols="8"
|
||||
/>
|
||||
<pv-number-input
|
||||
@@ -187,76 +200,67 @@ const deleteThisCamera = () => {
|
||||
]"
|
||||
:select-cols="8"
|
||||
/>
|
||||
<br />
|
||||
<v-row>
|
||||
<v-col cols="6">
|
||||
</v-card-text>
|
||||
<v-card-text class="d-flex pa-6 pt-0">
|
||||
<v-col cols="6" class="pa-0 pr-2">
|
||||
<v-btn block small color="secondary" :disabled="!settingsHaveChanged()" @click="saveCameraSettings">
|
||||
<v-icon left> mdi-content-save </v-icon>
|
||||
Save Changes
|
||||
</v-btn>
|
||||
</v-col>
|
||||
<v-col cols="6" class="pa-0 pl-2">
|
||||
<v-btn block small color="error" @click="() => (showDeleteCamera = true)">
|
||||
<v-icon left> mdi-trash-can-outline </v-icon>
|
||||
Delete Camera
|
||||
</v-btn>
|
||||
</v-col>
|
||||
</v-card-text>
|
||||
|
||||
<v-dialog v-model="showDeleteCamera" dark width="800">
|
||||
<v-card dark class="dialog-container pa-3 pb-2" color="primary" flat>
|
||||
<v-card-title> Delete {{ useCameraSettingsStore().currentCameraSettings.nickname }}? </v-card-title>
|
||||
<v-card-text>
|
||||
<v-row class="align-center pt-6">
|
||||
<v-col cols="12" md="6">
|
||||
<span class="white--text"> This will delete ALL OF YOUR SETTINGS and restart PhotonVision. </span>
|
||||
</v-col>
|
||||
<v-col cols="12" md="6">
|
||||
<v-btn color="secondary" block @click="openExportSettingsPrompt">
|
||||
<v-icon left class="open-icon"> mdi-export </v-icon>
|
||||
<span class="open-label">Backup Settings</span>
|
||||
<a
|
||||
ref="exportSettings"
|
||||
style="color: black; text-decoration: none; display: none"
|
||||
:href="`http://${address}/api/settings/photonvision_config.zip`"
|
||||
download="photonvision-settings.zip"
|
||||
target="_blank"
|
||||
/>
|
||||
</v-btn>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card-text>
|
||||
<v-card-text>
|
||||
<pv-input
|
||||
v-model="yesDeleteMySettingsText"
|
||||
:label="'Type "' + useCameraSettingsStore().currentCameraName + '":'"
|
||||
:label-cols="6"
|
||||
:input-cols="6"
|
||||
/>
|
||||
</v-card-text>
|
||||
<v-card-text>
|
||||
<v-btn
|
||||
class="mt-2 mb-3"
|
||||
style="width: 100%"
|
||||
small
|
||||
color="secondary"
|
||||
:disabled="!settingsHaveChanged()"
|
||||
@click="saveCameraSettings"
|
||||
>
|
||||
<v-icon left> mdi-content-save </v-icon>
|
||||
Save Changes
|
||||
</v-btn>
|
||||
</v-col>
|
||||
<v-col cols="6">
|
||||
<v-btn class="mt-2 mb-3" style="width: 100%" small color="red" @click="() => (showDeleteCamera = true)">
|
||||
<v-icon left> mdi-bomb </v-icon>
|
||||
Delete Camera
|
||||
</v-btn>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</div>
|
||||
|
||||
<v-dialog v-model="showDeleteCamera" dark width="1500">
|
||||
<v-card dark class="dialog-container pa-6" color="primary" flat>
|
||||
<v-card-title
|
||||
>Delete camera "{{ useCameraSettingsStore().cameraNames[useStateStore().currentCameraIndex] }}"</v-card-title
|
||||
>
|
||||
<v-row class="pl-3 align-center pa-6">
|
||||
<v-col cols="12" md="6">
|
||||
<span class="mt-3"> This will delete ALL OF YOUR SETTINGS and restart PhotonVision. </span>
|
||||
</v-col>
|
||||
<v-col cols="12" md="6">
|
||||
<v-btn color="secondary" style="float: right" @click="openExportSettingsPrompt">
|
||||
<v-icon left class="open-icon"> mdi-export </v-icon>
|
||||
<span class="open-label">Backup Settings</span>
|
||||
<a
|
||||
ref="exportSettings"
|
||||
style="color: black; text-decoration: none; display: none"
|
||||
:href="`http://${address}/api/settings/photonvision_config.zip`"
|
||||
download="photonvision-settings.zip"
|
||||
target="_blank"
|
||||
/>
|
||||
</v-btn>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<v-divider class="mt-4 mb-4" />
|
||||
<v-row class="pl-3 align-center pa-6">
|
||||
<v-col>
|
||||
<pv-input
|
||||
v-model="yesDeleteMySettingsText"
|
||||
:label="'Type "' + useCameraSettingsStore().currentCameraName + '":'"
|
||||
:label-cols="12"
|
||||
:input-cols="12"
|
||||
/>
|
||||
</v-col>
|
||||
|
||||
<v-btn
|
||||
color="red"
|
||||
block
|
||||
color="error"
|
||||
:disabled="
|
||||
yesDeleteMySettingsText.toLowerCase() !== useCameraSettingsStore().currentCameraName.toLowerCase()
|
||||
"
|
||||
:loading="deletingCamera"
|
||||
@click="deleteThisCamera"
|
||||
>
|
||||
<v-icon left class="open-icon"> mdi-skull </v-icon>
|
||||
<v-icon left class="open-icon"> mdi-trash-can-outline </v-icon>
|
||||
<span class="open-label">DELETE (UNRECOVERABLE)</span>
|
||||
</v-btn>
|
||||
</v-row>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</v-card>
|
||||
|
||||
@@ -41,22 +41,15 @@ const fpsTooLow = computed<boolean>(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<v-card
|
||||
id="camera-settings-camera-view-card"
|
||||
class="camera-settings-camera-view-card mb-3 pb-3 pa-4"
|
||||
color="primary"
|
||||
dark
|
||||
>
|
||||
<v-card-title
|
||||
class="pb-0 mb-2 pl-4 pt-1"
|
||||
style="min-height: 50px; justify-content: space-between; align-content: center"
|
||||
>
|
||||
<div style="display: flex; flex-wrap: wrap">
|
||||
<v-card id="camera-settings-camera-view-card" class="camera-settings-camera-view-card" color="primary" dark>
|
||||
<v-card-title class="justify-space-between align-content-center pa-0 pl-6 pr-6">
|
||||
<div class="d-flex flex-wrap pt-4 pb-4">
|
||||
<div>
|
||||
<span class="mr-4" style="white-space: nowrap"> Cameras </span>
|
||||
</div>
|
||||
<div>
|
||||
<v-chip
|
||||
v-if="useCameraSettingsStore().currentCameraSettings.isConnected"
|
||||
label
|
||||
:color="fpsTooLow ? 'error' : 'transparent'"
|
||||
:text-color="fpsTooLow ? '#C7EA46' : '#ff4d00'"
|
||||
@@ -67,25 +60,29 @@ const fpsTooLow = computed<boolean>(() => {
|
||||
{{ Math.min(Math.round(useStateStore().currentPipelineResults?.latency || 0), 9999) }} ms latency
|
||||
</span>
|
||||
</v-chip>
|
||||
<v-chip v-else label color="transparent" text-color="red" style="font-size: 1rem; padding: 0; margin: 0">
|
||||
<span class="pr-1">Camera not connected</span>
|
||||
</v-chip>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="d-flex align-center">
|
||||
<v-switch
|
||||
v-model="driverMode"
|
||||
:disabled="useCameraSettingsStore().isCalibrationMode || useCameraSettingsStore().pipelineNames.length === 0"
|
||||
label="Driver Mode"
|
||||
style="margin-left: auto"
|
||||
color="accent"
|
||||
class="pt-2"
|
||||
class="pt-2 pb-2"
|
||||
hide-details="auto"
|
||||
/>
|
||||
</div>
|
||||
</v-card-title>
|
||||
<div class="stream-container pb-4">
|
||||
<v-card-text class="stream-container">
|
||||
<div class="stream">
|
||||
<photon-camera-stream
|
||||
v-if="value.includes(0)"
|
||||
id="input-camera-stream"
|
||||
:camera-settings="useCameraSettingsStore().currentCameraSettings"
|
||||
stream-type="Raw"
|
||||
style="max-width: 100%"
|
||||
/>
|
||||
@@ -94,14 +91,13 @@ const fpsTooLow = computed<boolean>(() => {
|
||||
<photon-camera-stream
|
||||
v-if="value.includes(1)"
|
||||
id="output-camera-stream"
|
||||
:camera-settings="useCameraSettingsStore().currentCameraSettings"
|
||||
stream-type="Processed"
|
||||
style="max-width: 100%"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<v-divider />
|
||||
<div class="pt-4">
|
||||
<p style="color: white">Stream Display</p>
|
||||
</v-card-text>
|
||||
<v-card-text class="pt-0">
|
||||
<v-btn-toggle v-model="localValue" :multiple="true" mandatory dark class="fill" style="width: 100%">
|
||||
<v-btn
|
||||
color="secondary"
|
||||
@@ -120,7 +116,7 @@ const fpsTooLow = computed<boolean>(() => {
|
||||
<span class="mode-btn-label">Processed</span>
|
||||
</v-btn>
|
||||
</v-btn-toggle>
|
||||
</div>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
@@ -138,6 +134,10 @@ th {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.v-input--switch {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.stream-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
|
||||
71
photon-client/src/components/common/pv-camera-info-card.vue
Normal file
71
photon-client/src/components/common/pv-camera-info-card.vue
Normal file
@@ -0,0 +1,71 @@
|
||||
<script setup lang="ts">
|
||||
import { PVCameraInfo } from "@/types/SettingTypes";
|
||||
|
||||
const { camera } = defineProps({
|
||||
camera: {
|
||||
type: PVCameraInfo,
|
||||
required: true
|
||||
}
|
||||
});
|
||||
|
||||
const cameraInfoFor: any = (camera: PVCameraInfo) => {
|
||||
if (camera.PVUsbCameraInfo) {
|
||||
return camera.PVUsbCameraInfo;
|
||||
}
|
||||
if (camera.PVCSICameraInfo) {
|
||||
return camera.PVCSICameraInfo;
|
||||
}
|
||||
if (camera.PVFileCameraInfo) {
|
||||
return camera.PVFileCameraInfo;
|
||||
}
|
||||
return {};
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<v-simple-table dense :style="{ backgroundColor: 'var(--v-primary-base)' }">
|
||||
<tbody>
|
||||
<tr v-if="cameraInfoFor(camera).dev !== undefined && cameraInfoFor(camera).dev !== null">
|
||||
<td>Device Number:</td>
|
||||
<td>{{ cameraInfoFor(camera).dev }}</td>
|
||||
</tr>
|
||||
<tr v-if="cameraInfoFor(camera).name !== undefined && cameraInfoFor(camera).name !== null">
|
||||
<td>Name:</td>
|
||||
<td>{{ cameraInfoFor(camera).name }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Type:</td>
|
||||
<td v-if="camera.PVUsbCameraInfo" class="mb-3">USB Camera</td>
|
||||
<td v-else-if="camera.PVCSICameraInfo" class="mb-3">CSI Camera</td>
|
||||
<td v-else-if="camera.PVFileCameraInfo" class="mb-3">File Camera</td>
|
||||
<td v-else>Unidentified Camera Type</td>
|
||||
</tr>
|
||||
<tr v-if="cameraInfoFor(camera).baseName !== undefined && cameraInfoFor(camera).baseName !== null">
|
||||
<td>Base Name:</td>
|
||||
<td>{{ cameraInfoFor(camera).baseName }}</td>
|
||||
</tr>
|
||||
<tr v-if="cameraInfoFor(camera).vendorId !== undefined && cameraInfoFor(camera).vendorId !== null">
|
||||
<td>Vendor ID:</td>
|
||||
<td>{{ cameraInfoFor(camera).vendorId }}</td>
|
||||
</tr>
|
||||
<tr v-if="cameraInfoFor(camera).productId !== undefined && cameraInfoFor(camera).productId !== null">
|
||||
<td>Product ID:</td>
|
||||
<td>{{ cameraInfoFor(camera).productId }}</td>
|
||||
</tr>
|
||||
<tr v-if="cameraInfoFor(camera).path !== undefined && cameraInfoFor(camera).path !== null">
|
||||
<td>Path:</td>
|
||||
<td style="word-break: break-all">{{ cameraInfoFor(camera).path }}</td>
|
||||
</tr>
|
||||
<tr v-if="cameraInfoFor(camera).uniquePath !== undefined && cameraInfoFor(camera).uniquePath !== null">
|
||||
<td>Unique Path:</td>
|
||||
<td style="word-break: break-all">{{ cameraInfoFor(camera).uniquePath }}</td>
|
||||
</tr>
|
||||
<tr v-if="cameraInfoFor(camera).otherPaths !== undefined && cameraInfoFor(camera).otherPaths !== null">
|
||||
<td>Other Paths:</td>
|
||||
<td>{{ cameraInfoFor(camera).otherPaths }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</v-simple-table>
|
||||
</div>
|
||||
</template>
|
||||
123
photon-client/src/components/common/pv-camera-match-card.vue
Normal file
123
photon-client/src/components/common/pv-camera-match-card.vue
Normal file
@@ -0,0 +1,123 @@
|
||||
<script setup lang="ts">
|
||||
import { PVCameraInfo } from "@/types/SettingTypes";
|
||||
import _ from "lodash";
|
||||
|
||||
const { saved, current } = defineProps({
|
||||
saved: {
|
||||
type: PVCameraInfo,
|
||||
required: true
|
||||
},
|
||||
current: {
|
||||
type: PVCameraInfo,
|
||||
required: true
|
||||
}
|
||||
});
|
||||
|
||||
const cameraInfoFor = (camera: PVCameraInfo): any => {
|
||||
if (camera.PVUsbCameraInfo) {
|
||||
return camera.PVUsbCameraInfo;
|
||||
}
|
||||
if (camera.PVCSICameraInfo) {
|
||||
return camera.PVCSICameraInfo;
|
||||
}
|
||||
if (camera.PVFileCameraInfo) {
|
||||
return camera.PVFileCameraInfo;
|
||||
}
|
||||
return {};
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<v-simple-table dense :style="{ backgroundColor: 'var(--v-primary-base)' }">
|
||||
<tbody>
|
||||
<tr>
|
||||
<th></th>
|
||||
<th>Saved</th>
|
||||
<th>Current</th>
|
||||
</tr>
|
||||
<tr
|
||||
v-if="cameraInfoFor(saved).dev !== undefined && cameraInfoFor(saved).dev !== null"
|
||||
:class="cameraInfoFor(saved).dev !== cameraInfoFor(current).dev ? 'mismatch' : ''"
|
||||
>
|
||||
<td>Device Number:</td>
|
||||
<td>{{ cameraInfoFor(saved).dev }}</td>
|
||||
<td>{{ cameraInfoFor(current).dev }}</td>
|
||||
</tr>
|
||||
<tr
|
||||
v-if="cameraInfoFor(saved).name !== undefined && cameraInfoFor(saved).name !== null"
|
||||
:class="cameraInfoFor(saved).name !== cameraInfoFor(current).name ? 'mismatch' : ''"
|
||||
>
|
||||
<td>Name:</td>
|
||||
<td>{{ cameraInfoFor(saved).name }}</td>
|
||||
<td>{{ cameraInfoFor(current).name }}</td>
|
||||
</tr>
|
||||
<tr
|
||||
v-if="cameraInfoFor(saved).baseName !== undefined && cameraInfoFor(saved).baseName !== null"
|
||||
:class="cameraInfoFor(saved).baseName !== cameraInfoFor(current).baseName ? 'mismatch' : ''"
|
||||
>
|
||||
<td>Base Name:</td>
|
||||
<td>{{ cameraInfoFor(saved).baseName }}</td>
|
||||
<td>{{ cameraInfoFor(current).baseName }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Type:</td>
|
||||
<td v-if="saved.PVUsbCameraInfo" class="mb-3">USB Camera</td>
|
||||
<td v-else-if="saved.PVCSICameraInfo" class="mb-3">CSI Camera</td>
|
||||
<td v-else-if="saved.PVFileCameraInfo" class="mb-3">File Camera</td>
|
||||
<td v-else>Unidentified Camera Type</td>
|
||||
<td v-if="current.PVUsbCameraInfo" class="mb-3">USB Camera</td>
|
||||
<td v-else-if="current.PVCSICameraInfo" class="mb-3">CSI Camera</td>
|
||||
<td v-else-if="current.PVFileCameraInfo" class="mb-3">File Camera</td>
|
||||
<td v-else>Unidentified Camera Type</td>
|
||||
</tr>
|
||||
<tr
|
||||
v-if="cameraInfoFor(saved).vendorId !== undefined && cameraInfoFor(saved).vendorId !== null"
|
||||
:class="cameraInfoFor(saved).vendorId !== cameraInfoFor(current).vendorId ? 'mismatch' : ''"
|
||||
>
|
||||
<td>Vendor ID:</td>
|
||||
<td>{{ cameraInfoFor(saved).vendorId }}</td>
|
||||
<td>{{ cameraInfoFor(current).vendorId }}</td>
|
||||
</tr>
|
||||
<tr
|
||||
v-if="cameraInfoFor(saved).productId !== undefined && cameraInfoFor(saved).productId !== null"
|
||||
:class="cameraInfoFor(saved).productId !== cameraInfoFor(current).productId ? 'mismatch' : ''"
|
||||
>
|
||||
<td>Product ID:</td>
|
||||
<td>{{ cameraInfoFor(saved).productId }}</td>
|
||||
<td>{{ cameraInfoFor(current).productId }}</td>
|
||||
</tr>
|
||||
<tr
|
||||
v-if="cameraInfoFor(saved).path !== undefined && cameraInfoFor(saved).path !== null"
|
||||
:class="cameraInfoFor(saved).path !== cameraInfoFor(current).path ? 'mismatch' : ''"
|
||||
>
|
||||
<td>Path:</td>
|
||||
<td style="word-break: break-all">{{ cameraInfoFor(saved).path }}</td>
|
||||
<td style="word-break: break-all">{{ cameraInfoFor(current).path }}</td>
|
||||
</tr>
|
||||
<tr
|
||||
v-if="cameraInfoFor(saved).uniquePath !== undefined && cameraInfoFor(saved).uniquePath !== null"
|
||||
:class="cameraInfoFor(saved).uniquePath !== cameraInfoFor(current).uniquePath ? 'mismatch' : ''"
|
||||
>
|
||||
<td>Unique Path:</td>
|
||||
<td style="word-break: break-all">{{ cameraInfoFor(saved).uniquePath }}</td>
|
||||
<td style="word-break: break-all">{{ cameraInfoFor(current).uniquePath }}</td>
|
||||
</tr>
|
||||
<tr
|
||||
v-if="cameraInfoFor(saved).otherPaths !== undefined && cameraInfoFor(saved).otherPaths !== null"
|
||||
:class="!_.isEqual(cameraInfoFor(saved).otherPaths, cameraInfoFor(current).otherPaths) ? 'mismatch' : ''"
|
||||
>
|
||||
<td>Other Paths:</td>
|
||||
<td>{{ cameraInfoFor(saved).otherPaths }}</td>
|
||||
<td>{{ cameraInfoFor(current).otherPaths }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</v-simple-table>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.mismatch {
|
||||
background: #39a4d546 !important;
|
||||
}
|
||||
</style>
|
||||
@@ -48,26 +48,36 @@ const handleKeydown = ({ key }) => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<v-row dense align="center">
|
||||
<v-col :cols="labelCols || 12 - inputCols">
|
||||
<tooltipped-label :tooltip="tooltip" :label="label" />
|
||||
</v-col>
|
||||
<div class="d-flex">
|
||||
<v-col :cols="labelCols || 12 - inputCols" class="d-flex align-center pl-0">
|
||||
<tooltipped-label :tooltip="tooltip" :label="label" />
|
||||
</v-col>
|
||||
|
||||
<v-col :cols="inputCols">
|
||||
<v-text-field
|
||||
v-model="localValue"
|
||||
dark
|
||||
dense
|
||||
color="accent"
|
||||
:placeholder="placeholder"
|
||||
:disabled="disabled"
|
||||
:error-messages="errorMessage"
|
||||
:rules="rules"
|
||||
class="mt-1 pt-2"
|
||||
@keydown="handleKeydown"
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-col :cols="inputCols" class="d-flex align-center pr-0">
|
||||
<v-text-field
|
||||
v-model="localValue"
|
||||
dark
|
||||
dense
|
||||
color="accent"
|
||||
:placeholder="placeholder"
|
||||
:disabled="disabled"
|
||||
:error-messages="errorMessage"
|
||||
:rules="rules"
|
||||
hide-details="auto"
|
||||
class="light-error"
|
||||
@keydown="handleKeydown"
|
||||
/>
|
||||
</v-col>
|
||||
</div>
|
||||
</template>
|
||||
<style scoped>
|
||||
.v-text-field {
|
||||
margin-top: 0px;
|
||||
}
|
||||
</style>
|
||||
<style>
|
||||
.light-error .error--text {
|
||||
color: red !important;
|
||||
caret-color: red !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -31,26 +31,24 @@ const localValue = computed({
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<v-row dense align="center">
|
||||
<v-col :cols="labelCols">
|
||||
<tooltipped-label :tooltip="tooltip" :label="label" />
|
||||
</v-col>
|
||||
<v-col>
|
||||
<v-text-field
|
||||
v-model="localValue"
|
||||
dark
|
||||
class="mt-0 pt-0"
|
||||
hide-details
|
||||
single-line
|
||||
color="accent"
|
||||
type="number"
|
||||
style="width: 70px"
|
||||
:step="step"
|
||||
:disabled="disabled"
|
||||
:rules="rules"
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<div class="d-flex">
|
||||
<v-col :cols="labelCols" class="d-flex pl-0 align-center">
|
||||
<tooltipped-label :tooltip="tooltip" :label="label" />
|
||||
</v-col>
|
||||
<v-col class="pr-0">
|
||||
<v-text-field
|
||||
v-model="localValue"
|
||||
dark
|
||||
class="mt-0 pt-0"
|
||||
hide-details
|
||||
single-line
|
||||
color="accent"
|
||||
type="number"
|
||||
style="width: 70px"
|
||||
:step="step"
|
||||
:disabled="disabled"
|
||||
:rules="rules"
|
||||
/>
|
||||
</v-col>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -29,23 +29,27 @@ const localValue = computed({
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<v-row dense align="center">
|
||||
<v-col :cols="12 - inputCols">
|
||||
<tooltipped-label :tooltip="tooltip" :label="label" />
|
||||
</v-col>
|
||||
<v-col :cols="inputCols">
|
||||
<v-radio-group v-model="localValue" row dark :mandatory="true">
|
||||
<v-radio
|
||||
v-for="(radioName, index) in list"
|
||||
:key="index"
|
||||
color="#ffd843"
|
||||
:label="radioName"
|
||||
:value="index"
|
||||
:disabled="disabled"
|
||||
/>
|
||||
</v-radio-group>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<div class="d-flex">
|
||||
<v-col :cols="12 - inputCols" class="d-flex align-center pl-0">
|
||||
<tooltipped-label :tooltip="tooltip" :label="label" />
|
||||
</v-col>
|
||||
<v-col :cols="inputCols" class="d-flex align-center pr-0">
|
||||
<v-radio-group v-model="localValue" row dark :mandatory="true" hide-details="auto">
|
||||
<v-radio
|
||||
v-for="(radioName, index) in list"
|
||||
:key="index"
|
||||
color="#ffd843"
|
||||
:label="radioName"
|
||||
:value="index"
|
||||
:disabled="disabled"
|
||||
/>
|
||||
</v-radio-group>
|
||||
</v-col>
|
||||
</div>
|
||||
</template>
|
||||
<style scoped>
|
||||
.v-input--radio-group {
|
||||
padding-top: 0;
|
||||
margin-top: 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -58,61 +58,59 @@ const checkNumberRange = (v: string): boolean => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<v-row dense align="center">
|
||||
<v-col :cols="12 - sliderCols">
|
||||
<tooltipped-label :tooltip="tooltip" :label="label" />
|
||||
</v-col>
|
||||
<v-col :cols="sliderCols">
|
||||
<v-range-slider
|
||||
v-model="localValue"
|
||||
:max="max"
|
||||
:min="min"
|
||||
:disabled="disabled"
|
||||
hide-details
|
||||
class="align-center"
|
||||
dark
|
||||
:color="inverted ? 'rgba(255, 255, 255, 0.2)' : 'accent'"
|
||||
:track-color="inverted ? 'accent' : undefined"
|
||||
thumb-color="accent"
|
||||
:step="step"
|
||||
>
|
||||
<template #prepend>
|
||||
<v-text-field
|
||||
:value="localValue[0]"
|
||||
dark
|
||||
color="accent"
|
||||
class="mt-0 pt-0"
|
||||
hide-details
|
||||
single-line
|
||||
:max="max"
|
||||
:min="min"
|
||||
:step="step"
|
||||
:rules="[checkNumberRange]"
|
||||
type="number"
|
||||
style="width: 60px"
|
||||
@input="(v) => changeFromSlot(v, 0)"
|
||||
/>
|
||||
</template>
|
||||
<template #append>
|
||||
<v-text-field
|
||||
:value="localValue[1]"
|
||||
dark
|
||||
color="accent"
|
||||
class="mt-0 pt-0"
|
||||
hide-details
|
||||
single-line
|
||||
:max="max"
|
||||
:min="min"
|
||||
:step="step"
|
||||
:rules="[checkNumberRange]"
|
||||
type="number"
|
||||
style="width: 60px"
|
||||
@input="(v) => changeFromSlot(v, 1)"
|
||||
/>
|
||||
</template>
|
||||
</v-range-slider>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<div class="d-flex">
|
||||
<v-col :cols="12 - sliderCols" class="d-flex align-center pl-0">
|
||||
<tooltipped-label :tooltip="tooltip" :label="label" />
|
||||
</v-col>
|
||||
<v-col :cols="sliderCols" class="pr-0">
|
||||
<v-range-slider
|
||||
v-model="localValue"
|
||||
:max="max"
|
||||
:min="min"
|
||||
:disabled="disabled"
|
||||
hide-details
|
||||
class="align-center"
|
||||
dark
|
||||
:color="inverted ? 'rgba(255, 255, 255, 0.2)' : 'accent'"
|
||||
:track-color="inverted ? 'accent' : undefined"
|
||||
thumb-color="accent"
|
||||
:step="step"
|
||||
>
|
||||
<template #prepend>
|
||||
<v-text-field
|
||||
:value="localValue[0]"
|
||||
dark
|
||||
color="accent"
|
||||
class="mt-0 pt-0"
|
||||
hide-details
|
||||
single-line
|
||||
:max="max"
|
||||
:min="min"
|
||||
:step="step"
|
||||
:rules="[checkNumberRange]"
|
||||
type="number"
|
||||
style="width: 60px"
|
||||
@input="(v) => changeFromSlot(v, 0)"
|
||||
/>
|
||||
</template>
|
||||
<template #append>
|
||||
<v-text-field
|
||||
:value="localValue[1]"
|
||||
dark
|
||||
color="accent"
|
||||
class="mt-0 pt-0"
|
||||
hide-details
|
||||
single-line
|
||||
:max="max"
|
||||
:min="min"
|
||||
:step="step"
|
||||
:rules="[checkNumberRange]"
|
||||
type="number"
|
||||
style="width: 60px"
|
||||
@input="(v) => changeFromSlot(v, 1)"
|
||||
/>
|
||||
</template>
|
||||
</v-range-slider>
|
||||
</v-col>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import { computed } from "vue";
|
||||
import TooltippedLabel from "@/components/common/pv-tooltipped-label.vue";
|
||||
|
||||
interface SelectItem {
|
||||
export interface SelectItem {
|
||||
name: string | number;
|
||||
value: string | number;
|
||||
disabled?: boolean;
|
||||
@@ -14,7 +14,7 @@ const props = withDefaults(
|
||||
tooltip?: string;
|
||||
selectCols?: number;
|
||||
// TODO fully update v-model usage in custom components on Vue3 update
|
||||
value: number;
|
||||
value: any;
|
||||
disabled?: boolean;
|
||||
items: string[] | number[] | SelectItem[];
|
||||
}>(),
|
||||
@@ -25,7 +25,7 @@ const props = withDefaults(
|
||||
);
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "input", value: number): void;
|
||||
(e: "input", value: string): void;
|
||||
}>();
|
||||
|
||||
const localValue = computed({
|
||||
@@ -49,24 +49,29 @@ const items = computed<SelectItem[]>(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<v-row dense align="center">
|
||||
<v-col :cols="12 - selectCols">
|
||||
<tooltipped-label :tooltip="tooltip" :label="label" />
|
||||
</v-col>
|
||||
<v-col :cols="selectCols">
|
||||
<v-select
|
||||
v-model="localValue"
|
||||
:items="items"
|
||||
item-text="name"
|
||||
item-value="value"
|
||||
item-disabled="disabled"
|
||||
dark
|
||||
color="accent"
|
||||
item-color="secondary"
|
||||
:disabled="disabled"
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<div class="d-flex">
|
||||
<v-col :cols="12 - selectCols" class="d-flex align-center pl-0">
|
||||
<tooltipped-label :tooltip="tooltip" :label="label" />
|
||||
</v-col>
|
||||
<v-col :cols="selectCols" class="d-flex align-center pr-0">
|
||||
<v-select
|
||||
v-model="localValue"
|
||||
:items="items"
|
||||
item-text="name"
|
||||
item-value="value"
|
||||
item-disabled="disabled"
|
||||
dark
|
||||
color="accent"
|
||||
item-color="secondary"
|
||||
:disabled="disabled"
|
||||
hide-details="auto"
|
||||
/>
|
||||
</v-col>
|
||||
</div>
|
||||
</template>
|
||||
<style scoped>
|
||||
.v-select {
|
||||
padding-top: 0px;
|
||||
margin-top: 0px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -45,41 +45,45 @@ const localValue = computed({
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<v-row dense align="center">
|
||||
<v-col :cols="12 - sliderCols">
|
||||
<tooltipped-label :tooltip="tooltip" :label="label" />
|
||||
</v-col>
|
||||
<v-col :cols="sliderCols">
|
||||
<v-slider
|
||||
v-model="localValue"
|
||||
dark
|
||||
class="align-center"
|
||||
:max="max"
|
||||
:min="min"
|
||||
hide-details
|
||||
color="accent"
|
||||
:disabled="disabled"
|
||||
:step="step"
|
||||
>
|
||||
<template #append>
|
||||
<v-text-field
|
||||
v-model="localValue"
|
||||
dark
|
||||
color="accent"
|
||||
:max="max"
|
||||
:min="min"
|
||||
:disabled="disabled"
|
||||
class="mt-0 pt-0"
|
||||
hide-details
|
||||
single-line
|
||||
type="number"
|
||||
style="width: 50px"
|
||||
:step="step"
|
||||
/>
|
||||
</template>
|
||||
</v-slider>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<div class="d-flex">
|
||||
<v-col :cols="12 - sliderCols" class="pl-0 d-flex align-center">
|
||||
<tooltipped-label :tooltip="tooltip" :label="label" />
|
||||
</v-col>
|
||||
<v-col :cols="sliderCols - 1">
|
||||
<v-slider
|
||||
v-model="localValue"
|
||||
dark
|
||||
class="align-center"
|
||||
:max="max"
|
||||
:min="min"
|
||||
hide-details
|
||||
color="accent"
|
||||
:disabled="disabled"
|
||||
:step="step"
|
||||
append-icon="mdi-menu-right"
|
||||
prepend-icon="mdi-menu-left"
|
||||
@click:append="localValue += step"
|
||||
@click:prepend="localValue -= step"
|
||||
/>
|
||||
</v-col>
|
||||
<v-col :cols="1" class="pr-0">
|
||||
<v-text-field
|
||||
:value="localValue"
|
||||
dark
|
||||
color="accent"
|
||||
:max="max"
|
||||
:min="min"
|
||||
:disabled="disabled"
|
||||
class="mt-0 pt-0"
|
||||
hide-details
|
||||
single-line
|
||||
type="number"
|
||||
style="width: 100%"
|
||||
:step="step"
|
||||
:hide-spin-buttons="true"
|
||||
@keyup.enter="localValue = $event.target.value"
|
||||
@blur="localValue = $event.target.value"
|
||||
/>
|
||||
</v-col>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -11,10 +11,13 @@ const props = withDefaults(
|
||||
disabled?: boolean;
|
||||
labelCols?: number;
|
||||
switchCols?: number;
|
||||
dense?: boolean;
|
||||
}>(),
|
||||
{
|
||||
disabled: false,
|
||||
labelCols: 2
|
||||
labelCols: 2,
|
||||
switchCols: 8,
|
||||
dense: false
|
||||
}
|
||||
);
|
||||
|
||||
@@ -29,14 +32,17 @@ const localValue = computed({
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<v-row dense align="center">
|
||||
<v-col :cols="12 - switchCols || labelCols">
|
||||
<tooltipped-label :tooltip="tooltip" :label="label" />
|
||||
</v-col>
|
||||
<v-col :cols="switchCols || 12 - labelCols">
|
||||
<v-switch v-model="localValue" dark :disabled="disabled" color="#ffd843" />
|
||||
</v-col>
|
||||
</v-row>
|
||||
<div class="d-flex">
|
||||
<v-col :cols="12 - switchCols || labelCols" class="d-flex align-center pl-0">
|
||||
<tooltipped-label :tooltip="tooltip" :label="label" />
|
||||
</v-col>
|
||||
<v-col :cols="switchCols || 12 - labelCols" class="d-flex align-center pr-0">
|
||||
<v-switch v-model="localValue" dark :disabled="disabled" color="#ffd843" hide-details="auto" class="pb-1" />
|
||||
</v-col>
|
||||
</div>
|
||||
</template>
|
||||
<style scoped>
|
||||
.v-input--selection-controls {
|
||||
margin-top: 0px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import PvSelect from "@/components/common/pv-select.vue";
|
||||
import PvSelect, { type SelectItem } from "@/components/common/pv-select.vue";
|
||||
import { useStateStore } from "@/stores/StateStore";
|
||||
import { useCameraSettingsStore } from "@/stores/settings/CameraSettingsStore";
|
||||
import { WebsocketPipelineType } from "@/types/WebsocketDataTypes";
|
||||
@@ -9,10 +9,10 @@ import PvInput from "@/components/common/pv-input.vue";
|
||||
import { PipelineType } from "@/types/PipelineTypes";
|
||||
import { useSettingsStore } from "@/stores/settings/GeneralSettingsStore";
|
||||
|
||||
const changeCurrentCameraIndex = (index: number) => {
|
||||
useCameraSettingsStore().setCurrentCameraIndex(index, true);
|
||||
const changeCurrentCameraUniqueName = (cameraUniqueName: string) => {
|
||||
useCameraSettingsStore().setCurrentCameraUniqueName(cameraUniqueName, true);
|
||||
|
||||
switch (useCameraSettingsStore().cameras[index].pipelineSettings.pipelineType) {
|
||||
switch (useCameraSettingsStore().cameras[cameraUniqueName].pipelineSettings.pipelineType) {
|
||||
case PipelineType.Reflective:
|
||||
pipelineType.value = WebsocketPipelineType.Reflective;
|
||||
break;
|
||||
@@ -86,7 +86,7 @@ const cancelCameraNameEdit = () => {
|
||||
};
|
||||
|
||||
// Pipeline Name Edit
|
||||
const pipelineNamesWrapper = computed<{ name: string; value: number }[]>(() => {
|
||||
const pipelineNamesWrapper = computed<SelectItem[]>(() => {
|
||||
const pipelineNames = useCameraSettingsStore().pipelineNames.map((name, index) => ({ name: name, value: index }));
|
||||
|
||||
if (useCameraSettingsStore().isDriverMode) {
|
||||
@@ -212,7 +212,7 @@ const duplicateCurrentPipeline = () => {
|
||||
|
||||
// Change Props whenever the pipeline settings are changed
|
||||
useCameraSettingsStore().$subscribe((mutation, state) => {
|
||||
const currentCameraSettings = state.cameras[useStateStore().currentCameraIndex];
|
||||
const currentCameraSettings = state.cameras[useStateStore().currentCameraUniqueName];
|
||||
|
||||
switch (currentCameraSettings.pipelineSettings.pipelineType) {
|
||||
case PipelineType.Reflective:
|
||||
@@ -232,18 +232,24 @@ useCameraSettingsStore().$subscribe((mutation, state) => {
|
||||
break;
|
||||
}
|
||||
});
|
||||
const wrappedCameras = computed<SelectItem[]>(() =>
|
||||
Object.keys(useCameraSettingsStore().cameras).map((cameraUniqueName) => ({
|
||||
name: useCameraSettingsStore().cameras[cameraUniqueName].nickname,
|
||||
value: cameraUniqueName
|
||||
}))
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<v-card color="primary">
|
||||
<v-row style="padding: 12px 12px 0 24px">
|
||||
<v-row style="padding: 20px 12px 0 30px">
|
||||
<v-col cols="10" class="pa-0">
|
||||
<pv-select
|
||||
v-if="!isCameraNameEdit"
|
||||
v-model="useStateStore().currentCameraIndex"
|
||||
v-model="useStateStore().currentCameraUniqueName"
|
||||
label="Camera"
|
||||
:items="useCameraSettingsStore().cameraNames"
|
||||
@input="changeCurrentCameraIndex"
|
||||
:items="wrappedCameras"
|
||||
@input="changeCurrentCameraUniqueName"
|
||||
/>
|
||||
<pv-input
|
||||
v-else
|
||||
@@ -275,14 +281,18 @@ useCameraSettingsStore().$subscribe((mutation, state) => {
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row style="padding: 0 12px 0 24px">
|
||||
<v-row style="padding: 0 12px 0 30px">
|
||||
<v-col cols="10" class="pa-0">
|
||||
<pv-select
|
||||
v-if="!isPipelineNameEdit"
|
||||
:value="useCameraSettingsStore().currentCameraSettings.currentPipelineIndex"
|
||||
label="Pipeline"
|
||||
tooltip="Each pipeline runs on a camera output and stores a unique set of processing settings"
|
||||
:disabled="useCameraSettingsStore().isDriverMode || useCameraSettingsStore().isCalibrationMode"
|
||||
:disabled="
|
||||
useCameraSettingsStore().isDriverMode ||
|
||||
useCameraSettingsStore().isCalibrationMode ||
|
||||
!useCameraSettingsStore().hasConnected
|
||||
"
|
||||
:items="pipelineNamesWrapper"
|
||||
@input="(args) => useCameraSettingsStore().changeCurrentPipelineIndex(args, true)"
|
||||
/>
|
||||
@@ -343,13 +353,17 @@ useCameraSettingsStore().$subscribe((mutation, state) => {
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row style="padding: 0 12px 12px 24px">
|
||||
<v-row style="padding: 0 12px 24px 30px">
|
||||
<v-col cols="10" class="pa-0">
|
||||
<pv-select
|
||||
v-model="currentPipelineType"
|
||||
label="Type"
|
||||
tooltip="Changes the pipeline type, which changes the type of processing that will happen on input frames"
|
||||
:disabled="useCameraSettingsStore().isDriverMode || useCameraSettingsStore().isCalibrationMode"
|
||||
:disabled="
|
||||
useCameraSettingsStore().isDriverMode ||
|
||||
useCameraSettingsStore().isCalibrationMode ||
|
||||
!useCameraSettingsStore().hasConnected
|
||||
"
|
||||
:items="pipelineTypesWrapper"
|
||||
@input="showPipelineTypeChangeDialog = true"
|
||||
/>
|
||||
@@ -378,7 +392,12 @@ useCameraSettingsStore().$subscribe((mutation, state) => {
|
||||
<v-divider />
|
||||
<v-card-actions>
|
||||
<v-spacer />
|
||||
<v-btn color="#ffd843" :disabled="checkPipelineName(newPipelineName) !== true" @click="createNewPipeline">
|
||||
<v-btn
|
||||
color="#ffd843"
|
||||
class="black--text"
|
||||
:disabled="checkPipelineName(newPipelineName) !== true"
|
||||
@click="createNewPipeline"
|
||||
>
|
||||
Save
|
||||
</v-btn>
|
||||
<v-btn color="error" @click="cancelPipelineCreation"> Cancel </v-btn>
|
||||
@@ -399,7 +418,9 @@ useCameraSettingsStore().$subscribe((mutation, state) => {
|
||||
<v-card-actions>
|
||||
<v-spacer />
|
||||
<v-btn color="error" @click="confirmDeleteCurrentPipeline"> Yes, I'm sure </v-btn>
|
||||
<v-btn color="#ffd843" @click="showPipelineDeletionConfirmationDialog = false"> No, take me back </v-btn>
|
||||
<v-btn color="#ffd843" class="black--text" @click="showPipelineDeletionConfirmationDialog = false">
|
||||
No, take me back
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
@@ -415,7 +436,7 @@ useCameraSettingsStore().$subscribe((mutation, state) => {
|
||||
<v-card-actions>
|
||||
<v-spacer />
|
||||
<v-btn color="error" @click="confirmChangePipelineType"> Yes, I'm sure </v-btn>
|
||||
<v-btn color="#ffd843" @click="cancelChangePipelineType"> No, take me back </v-btn>
|
||||
<v-btn color="#ffd843" class="black--text" @click="cancelChangePipelineType"> No, take me back </v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
|
||||
@@ -42,56 +42,57 @@ const performanceRecommendation = computed<string>(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<v-card color="primary" height="100%" style="display: flex; flex-direction: column" dark>
|
||||
<v-row>
|
||||
<v-col class="align-self-center text-no-wrap">
|
||||
<v-card-title>Cameras</v-card-title>
|
||||
</v-col>
|
||||
<v-col class="align-self-center" style="text-align: right; margin-right: 12px; padding-left: 24px">
|
||||
<v-chip
|
||||
label
|
||||
:color="fpsTooLow ? 'error' : 'transparent'"
|
||||
:text-color="fpsTooLow ? '#C7EA46' : '#ff4d00'"
|
||||
style="font-size: 1rem; padding: 0; margin: 0"
|
||||
>
|
||||
<span class="pr-1"
|
||||
>Processing @ {{ Math.round(useStateStore().currentPipelineResults?.fps || 0) }} FPS –</span
|
||||
><span>{{ performanceRecommendation }}</span>
|
||||
</v-chip>
|
||||
</v-col>
|
||||
<v-col
|
||||
class="align-self-center"
|
||||
style="
|
||||
width: min-content;
|
||||
flex-grow: 0;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-right: 24px;
|
||||
padding: 0;
|
||||
"
|
||||
<v-card color="primary" height="100%" class="d-flex flex-column" dark>
|
||||
<v-card-title class="justify-space-between align-center pt-3 pb-3">
|
||||
<span>Cameras</span>
|
||||
<v-chip
|
||||
v-if="useCameraSettingsStore().currentCameraSettings.isConnected"
|
||||
label
|
||||
:color="fpsTooLow ? 'error' : 'transparent'"
|
||||
:text-color="fpsTooLow ? '#C7EA46' : '#ff4d00'"
|
||||
style="font-size: 1rem; padding: 0; margin: 0"
|
||||
>
|
||||
<v-switch
|
||||
v-model="driverMode"
|
||||
:disabled="useCameraSettingsStore().isCalibrationMode || useCameraSettingsStore().pipelineNames.length === 0"
|
||||
label="Driver Mode"
|
||||
style="margin: 0; padding: 0; padding-left: 18px; margin-top: 14px"
|
||||
color="accent"
|
||||
<span class="pr-1"
|
||||
>Processing @ {{ Math.round(useStateStore().currentPipelineResults?.fps || 0) }} FPS –</span
|
||||
><span>{{ performanceRecommendation }}</span>
|
||||
</v-chip>
|
||||
<v-chip v-else label color="transparent" text-color="red" style="font-size: 1rem; padding: 0; margin: 0">
|
||||
<span class="pr-1"> Camera not connected </span>
|
||||
</v-chip>
|
||||
<v-switch
|
||||
v-model="driverMode"
|
||||
:disabled="useCameraSettingsStore().isCalibrationMode || useCameraSettingsStore().pipelineNames.length === 0"
|
||||
label="Driver Mode"
|
||||
color="accent"
|
||||
hide-details="auto"
|
||||
/>
|
||||
</v-card-title>
|
||||
<v-divider class="ml-3 mr-3" />
|
||||
<v-row class="stream-viewer-container pa-3 align-center">
|
||||
<v-col v-if="value.includes(0)" class="stream-view">
|
||||
<photon-camera-stream
|
||||
id="input-camera-stream"
|
||||
:camera-settings="useCameraSettingsStore().currentCameraSettings"
|
||||
stream-type="Raw"
|
||||
style="width: 100%; height: auto"
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-divider style="border-color: white" />
|
||||
<v-row class="stream-viewer-container pa-3">
|
||||
<v-col v-if="value.includes(0)" class="stream-view">
|
||||
<photon-camera-stream id="input-camera-stream" stream-type="Raw" style="width: 100%; height: auto" />
|
||||
</v-col>
|
||||
<v-col v-if="value.includes(1)" class="stream-view">
|
||||
<photon-camera-stream id="output-camera-stream" stream-type="Processed" style="width: 100%; height: auto" />
|
||||
<photon-camera-stream
|
||||
id="output-camera-stream"
|
||||
:camera-settings="useCameraSettingsStore().currentCameraSettings"
|
||||
stream-type="Processed"
|
||||
style="width: 100%; height: auto"
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.v-input--switch {
|
||||
margin-top: 0;
|
||||
}
|
||||
.stream-viewer-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
|
||||
@@ -145,31 +145,42 @@ onBeforeUpdate(() => {
|
||||
|
||||
<template>
|
||||
<v-row no-gutters class="tabGroups">
|
||||
<v-col
|
||||
v-for="(tabGroupData, tabGroupIndex) in tabGroups"
|
||||
:key="tabGroupIndex"
|
||||
:class="tabGroupIndex !== tabGroups.length - 1 && 'pr-3'"
|
||||
>
|
||||
<v-card color="primary" height="100%" class="pr-4 pl-4">
|
||||
<v-tabs
|
||||
v-model="selectedTabs[tabGroupIndex]"
|
||||
grow
|
||||
background-color="primary"
|
||||
dark
|
||||
height="48"
|
||||
slider-color="accent"
|
||||
>
|
||||
<v-tab v-for="(tabConfig, index) in tabGroupData" :key="index">
|
||||
{{ tabConfig.tabName }}
|
||||
</v-tab>
|
||||
</v-tabs>
|
||||
<div class="pl-4 pr-4 pt-4 pb-2">
|
||||
<KeepAlive>
|
||||
<Component :is="tabGroupData[selectedTabs[tabGroupIndex]].component" />
|
||||
</KeepAlive>
|
||||
</div>
|
||||
</v-card>
|
||||
</v-col>
|
||||
<template v-if="!useCameraSettingsStore().hasConnected">
|
||||
<v-col cols="12">
|
||||
<v-card color="error">
|
||||
<v-card-title class="white--text">
|
||||
Camera has not connected. Please check your connection and try again.
|
||||
</v-card-title>
|
||||
</v-card>
|
||||
</v-col>
|
||||
</template>
|
||||
<template v-else>
|
||||
<v-col
|
||||
v-for="(tabGroupData, tabGroupIndex) in tabGroups"
|
||||
:key="tabGroupIndex"
|
||||
:class="tabGroupIndex !== tabGroups.length - 1 && 'pr-3'"
|
||||
>
|
||||
<v-card color="primary" height="100%" class="pr-4 pl-4">
|
||||
<v-tabs
|
||||
v-model="selectedTabs[tabGroupIndex]"
|
||||
grow
|
||||
background-color="primary"
|
||||
dark
|
||||
height="48"
|
||||
slider-color="accent"
|
||||
>
|
||||
<v-tab v-for="(tabConfig, index) in tabGroupData" :key="index">
|
||||
{{ tabConfig.tabName }}
|
||||
</v-tab>
|
||||
</v-tabs>
|
||||
<div class="pl-2 pr-2 pt-3 pb-3">
|
||||
<KeepAlive>
|
||||
<Component :is="tabGroupData[selectedTabs[tabGroupIndex]].component" />
|
||||
</KeepAlive>
|
||||
</div>
|
||||
</v-card>
|
||||
</v-col>
|
||||
</template>
|
||||
</v-row>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -34,23 +34,28 @@ const processingMode = computed<number>({
|
||||
color="primary"
|
||||
style="height: 100%; display: flex; flex-direction: column"
|
||||
>
|
||||
<v-row align="center" class="pa-3 pb-0">
|
||||
<v-col>
|
||||
<v-row class="pa-3 pb-0 align-center">
|
||||
<v-col class="pa-4">
|
||||
<p style="color: white">Processing Mode</p>
|
||||
<v-btn-toggle v-model="processingMode" mandatory dark class="fill">
|
||||
<v-btn color="secondary">
|
||||
<v-btn color="secondary" :disabled="!useCameraSettingsStore().hasConnected">
|
||||
<v-icon left>mdi-square-outline</v-icon>
|
||||
<span>2D</span>
|
||||
</v-btn>
|
||||
<v-btn color="secondary" :disabled="!useCameraSettingsStore().isCurrentVideoFormatCalibrated">
|
||||
<v-btn
|
||||
color="secondary"
|
||||
:disabled="
|
||||
!useCameraSettingsStore().hasConnected || !useCameraSettingsStore().isCurrentVideoFormatCalibrated
|
||||
"
|
||||
>
|
||||
<v-icon left>mdi-cube-outline</v-icon>
|
||||
<span>3D</span>
|
||||
</v-btn>
|
||||
</v-btn-toggle>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row align="center" class="pa-3 pt-0">
|
||||
<v-col>
|
||||
<v-row class="pa-3 pt-0 align-center">
|
||||
<v-col class="pa-4 pt-0">
|
||||
<p style="color: white">Stream Display</p>
|
||||
<v-btn-toggle v-model="localValue" :multiple="true" mandatory dark class="fill">
|
||||
<v-btn color="secondary" class="fill">
|
||||
|
||||
@@ -17,8 +17,8 @@ const currentPipelineSettings = computed<ActivePipelineSettings>(
|
||||
const interactiveCols = computed(() =>
|
||||
(getCurrentInstance()?.proxy.$vuetify.breakpoint.mdAndDown || false) &&
|
||||
(!useStateStore().sidebarFolded || useCameraSettingsStore().isDriverMode)
|
||||
? 9
|
||||
: 8
|
||||
? 8
|
||||
: 7
|
||||
);
|
||||
</script>
|
||||
|
||||
@@ -27,13 +27,12 @@ const interactiveCols = computed(() =>
|
||||
<pv-select
|
||||
v-model="currentPipelineSettings.tagFamily"
|
||||
label="Target family"
|
||||
:items="['AprilTag 36h11 (6.5in)', 'AprilTag 25h9 (6in)', 'AprilTag 16h5 (6in)']"
|
||||
:items="['AprilTag 36h11 (6.5in)', 'AprilTag 16h5 (6in)']"
|
||||
:select-cols="interactiveCols"
|
||||
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ tagFamily: value }, false)"
|
||||
/>
|
||||
<pv-slider
|
||||
v-model="currentPipelineSettings.decimate"
|
||||
class="pt-2"
|
||||
:slider-cols="interactiveCols"
|
||||
label="Decimate"
|
||||
tooltip="Increases FPS at the expense of range by reducing image resolution initially"
|
||||
@@ -43,7 +42,6 @@ const interactiveCols = computed(() =>
|
||||
/>
|
||||
<pv-slider
|
||||
v-model="currentPipelineSettings.blur"
|
||||
class="pt-2"
|
||||
:slider-cols="interactiveCols"
|
||||
label="Blur"
|
||||
tooltip="Gaussian blur added to the image, high FPS cost for slightly decreased noise"
|
||||
@@ -54,7 +52,6 @@ const interactiveCols = computed(() =>
|
||||
/>
|
||||
<pv-slider
|
||||
v-model="currentPipelineSettings.threads"
|
||||
class="pt-2"
|
||||
:slider-cols="interactiveCols"
|
||||
label="Threads"
|
||||
tooltip="Number of threads spawned by the AprilTag detector"
|
||||
@@ -62,16 +59,8 @@ const interactiveCols = computed(() =>
|
||||
:max="8"
|
||||
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ threads: value }, false)"
|
||||
/>
|
||||
<pv-switch
|
||||
v-model="currentPipelineSettings.refineEdges"
|
||||
class="pt-2"
|
||||
label="Refine Edges"
|
||||
tooltip="Further refines the AprilTag corner position initial estimate, suggested left on"
|
||||
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ refineEdges: value }, false)"
|
||||
/>
|
||||
<pv-slider
|
||||
v-model="currentPipelineSettings.decisionMargin"
|
||||
class="pt-2 pb-4"
|
||||
:slider-cols="interactiveCols"
|
||||
label="Decision Margin Cutoff"
|
||||
tooltip="Tags with a 'margin' (decoding quality score) less than this wil be rejected. Increase this to reduce the number of false positive detections"
|
||||
@@ -81,7 +70,6 @@ const interactiveCols = computed(() =>
|
||||
/>
|
||||
<pv-slider
|
||||
v-model="currentPipelineSettings.numIterations"
|
||||
class="pt-2 pb-4"
|
||||
:slider-cols="interactiveCols"
|
||||
label="Pose Estimation Iterations"
|
||||
tooltip="Number of iterations the pose estimation algorithm will run, 50-100 is a good starting point"
|
||||
@@ -89,5 +77,12 @@ const interactiveCols = computed(() =>
|
||||
:max="500"
|
||||
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ numIterations: value }, false)"
|
||||
/>
|
||||
<pv-switch
|
||||
v-model="currentPipelineSettings.refineEdges"
|
||||
:switch-cols="interactiveCols"
|
||||
label="Refine Edges"
|
||||
tooltip="Further refines the AprilTag corner position initial estimate, suggested left on"
|
||||
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ refineEdges: value }, false)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -17,8 +17,8 @@ const currentPipelineSettings = computed<ActivePipelineSettings>(
|
||||
const interactiveCols = computed(() =>
|
||||
(getCurrentInstance()?.proxy.$vuetify.breakpoint.mdAndDown || false) &&
|
||||
(!useStateStore().sidebarFolded || useCameraSettingsStore().isDriverMode)
|
||||
? 9
|
||||
: 8
|
||||
? 8
|
||||
: 7
|
||||
);
|
||||
</script>
|
||||
|
||||
@@ -27,18 +27,10 @@ const interactiveCols = computed(() =>
|
||||
<pv-select
|
||||
v-model="currentPipelineSettings.tagFamily"
|
||||
label="Target family"
|
||||
:items="['AprilTag Family 36h11', 'AprilTag Family 25h9', 'AprilTag Family 16h5']"
|
||||
:items="['AprilTag Family 36h11', 'AprilTag Family 16h5']"
|
||||
:select-cols="interactiveCols"
|
||||
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ tagFamily: value }, false)"
|
||||
/>
|
||||
<pv-switch
|
||||
v-model="currentPipelineSettings.useCornerRefinement"
|
||||
class="pt-2"
|
||||
label="Refine Corners"
|
||||
tooltip="Further refine the initial corners with subpixel accuracy."
|
||||
:switch-cols="interactiveCols"
|
||||
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ useCornerRefinement: value }, false)"
|
||||
/>
|
||||
<pv-range-slider
|
||||
v-model="currentPipelineSettings.threshWinSizes"
|
||||
label="Thresh Min/Max Size"
|
||||
@@ -51,7 +43,6 @@ const interactiveCols = computed(() =>
|
||||
/>
|
||||
<pv-slider
|
||||
v-model="currentPipelineSettings.threshStepSize"
|
||||
class="pt-2"
|
||||
:slider-cols="interactiveCols"
|
||||
label="Thresh Step Size"
|
||||
tooltip="Smaller values will cause more steps between the min/max sizes. More, varied steps can improve detection robustness to lighting, but may decrease performance."
|
||||
@@ -62,7 +53,6 @@ const interactiveCols = computed(() =>
|
||||
/>
|
||||
<pv-slider
|
||||
v-model="currentPipelineSettings.threshConstant"
|
||||
class="pt-2"
|
||||
:slider-cols="interactiveCols"
|
||||
label="Thresh Constant"
|
||||
tooltip="Affects the threshold window mean value cutoff for all steps. Higher values can improve performance, but may harm detection rate."
|
||||
@@ -71,9 +61,15 @@ const interactiveCols = computed(() =>
|
||||
:step="1"
|
||||
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ threshConstant: value }, false)"
|
||||
/>
|
||||
<pv-switch
|
||||
v-model="currentPipelineSettings.useCornerRefinement"
|
||||
label="Refine Corners"
|
||||
tooltip="Further refine the initial corners with subpixel accuracy."
|
||||
:switch-cols="interactiveCols"
|
||||
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ useCornerRefinement: value }, false)"
|
||||
/>
|
||||
<pv-switch
|
||||
v-model="currentPipelineSettings.debugThreshold"
|
||||
class="pt-2"
|
||||
label="Debug Threshold"
|
||||
tooltip="Display the first threshold step to the color stream."
|
||||
:switch-cols="interactiveCols"
|
||||
|
||||
@@ -52,13 +52,31 @@ const contourRadius = computed<[number, number]>({
|
||||
const interactiveCols = computed(() =>
|
||||
(getCurrentInstance()?.proxy.$vuetify.breakpoint.mdAndDown || false) &&
|
||||
(!useStateStore().sidebarFolded || useCameraSettingsStore().isDriverMode)
|
||||
? 9
|
||||
: 8
|
||||
? 8
|
||||
: 7
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<pv-select
|
||||
v-model="useCameraSettingsStore().currentPipelineSettings.contourTargetOrientation"
|
||||
label="Target Orientation"
|
||||
tooltip="Used to determine how to calculate target landmarks, as well as aspect ratio"
|
||||
:items="['Portrait', 'Landscape']"
|
||||
:select-cols="interactiveCols"
|
||||
@input="
|
||||
(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ contourTargetOrientation: value }, false)
|
||||
"
|
||||
/>
|
||||
<pv-select
|
||||
v-model="useCameraSettingsStore().currentPipelineSettings.contourSortMode"
|
||||
label="Target Sort"
|
||||
tooltip="Chooses the sorting mode used to determine the 'best' targets to provide to user code"
|
||||
:select-cols="interactiveCols"
|
||||
:items="['Largest', 'Smallest', 'Highest', 'Lowest', 'Rightmost', 'Leftmost', 'Centermost']"
|
||||
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ contourSortMode: value }, false)"
|
||||
/>
|
||||
<pv-range-slider
|
||||
v-model="contourArea"
|
||||
label="Area"
|
||||
@@ -79,16 +97,6 @@ const interactiveCols = computed(() =>
|
||||
:step="0.1"
|
||||
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ contourRatio: value }, false)"
|
||||
/>
|
||||
<pv-select
|
||||
v-model="useCameraSettingsStore().currentPipelineSettings.contourTargetOrientation"
|
||||
label="Target Orientation"
|
||||
tooltip="Used to determine how to calculate target landmarks, as well as aspect ratio"
|
||||
:items="['Portrait', 'Landscape']"
|
||||
:select-cols="interactiveCols"
|
||||
@input="
|
||||
(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ contourTargetOrientation: value }, false)
|
||||
"
|
||||
/>
|
||||
<pv-range-slider
|
||||
v-if="useCameraSettingsStore().currentPipelineType === PipelineType.ColoredShape"
|
||||
v-model="contourFullness"
|
||||
@@ -160,7 +168,6 @@ const interactiveCols = computed(() =>
|
||||
/>
|
||||
</template>
|
||||
<template v-else-if="currentPipelineSettings.pipelineType === PipelineType.ColoredShape">
|
||||
<v-divider class="mt-3" />
|
||||
<pv-select
|
||||
v-model="currentPipelineSettings.contourShape"
|
||||
label="Target Shape"
|
||||
@@ -170,6 +177,7 @@ const interactiveCols = computed(() =>
|
||||
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ contourShape: value }, false)"
|
||||
/>
|
||||
<pv-slider
|
||||
v-if="currentPipelineSettings.contourShape >= 1"
|
||||
v-model="currentPipelineSettings.accuracyPercentage"
|
||||
:disabled="currentPipelineSettings.contourShape < 1"
|
||||
label="Shape Simplification"
|
||||
@@ -180,6 +188,7 @@ const interactiveCols = computed(() =>
|
||||
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ accuracyPercentage: value }, false)"
|
||||
/>
|
||||
<pv-slider
|
||||
v-if="currentPipelineSettings.contourShape === 0"
|
||||
v-model="currentPipelineSettings.circleDetectThreshold"
|
||||
:disabled="currentPipelineSettings.contourShape !== 0"
|
||||
label="Circle match distance"
|
||||
@@ -191,16 +200,8 @@ const interactiveCols = computed(() =>
|
||||
(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ circleDetectThreshold: value }, false)
|
||||
"
|
||||
/>
|
||||
<pv-range-slider
|
||||
v-model="contourRadius"
|
||||
:disabled="currentPipelineSettings.contourShape !== 0"
|
||||
label="Radius"
|
||||
:min="0"
|
||||
:max="100"
|
||||
:slider-cols="interactiveCols"
|
||||
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ contourRadius: value }, false)"
|
||||
/>
|
||||
<pv-slider
|
||||
v-if="currentPipelineSettings.contourShape === 0"
|
||||
v-model="currentPipelineSettings.maxCannyThresh"
|
||||
:disabled="currentPipelineSettings.contourShape !== 0"
|
||||
label="Max Canny Threshold"
|
||||
@@ -210,6 +211,7 @@ const interactiveCols = computed(() =>
|
||||
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ maxCannyThresh: value }, false)"
|
||||
/>
|
||||
<pv-slider
|
||||
v-if="currentPipelineSettings.contourShape === 0"
|
||||
v-model="currentPipelineSettings.circleAccuracy"
|
||||
:disabled="currentPipelineSettings.contourShape !== 0"
|
||||
label="Circle Accuracy"
|
||||
@@ -218,15 +220,16 @@ const interactiveCols = computed(() =>
|
||||
:slider-cols="interactiveCols"
|
||||
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ circleAccuracy: value }, false)"
|
||||
/>
|
||||
<v-divider class="mt-3" />
|
||||
<pv-range-slider
|
||||
v-if="currentPipelineSettings.contourShape === 0"
|
||||
v-model="contourRadius"
|
||||
:disabled="currentPipelineSettings.contourShape !== 0"
|
||||
label="Radius"
|
||||
:min="0"
|
||||
:max="100"
|
||||
:slider-cols="interactiveCols"
|
||||
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ contourRadius: value }, false)"
|
||||
/>
|
||||
</template>
|
||||
<pv-select
|
||||
v-model="useCameraSettingsStore().currentPipelineSettings.contourSortMode"
|
||||
label="Target Sort"
|
||||
tooltip="Chooses the sorting mode used to determine the 'best' targets to provide to user code"
|
||||
:select-cols="interactiveCols"
|
||||
:items="['Largest', 'Smallest', 'Highest', 'Lowest', 'Rightmost', 'Leftmost', 'Centermost']"
|
||||
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ contourSortMode: value }, false)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -66,13 +66,21 @@ const handleStreamResolutionChange = (value: number) => {
|
||||
const interactiveCols = computed(() =>
|
||||
(getCurrentInstance()?.proxy.$vuetify.breakpoint.mdAndDown || false) &&
|
||||
(!useStateStore().sidebarFolded || useCameraSettingsStore().isDriverMode)
|
||||
? 9
|
||||
: 8
|
||||
? 8
|
||||
: 7
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<pv-switch
|
||||
v-model="useCameraSettingsStore().currentPipelineSettings.cameraAutoExposure"
|
||||
class="pt-2"
|
||||
label="Auto Exposure"
|
||||
:switch-cols="interactiveCols"
|
||||
tooltip="Enables or Disables camera automatic adjustment for current lighting conditions"
|
||||
@input="(args) => useCameraSettingsStore().changeCurrentPipelineSetting({ cameraAutoExposure: args }, false)"
|
||||
/>
|
||||
<pv-slider
|
||||
v-model="useCameraSettingsStore().currentPipelineSettings.cameraExposureRaw"
|
||||
:disabled="useCameraSettingsStore().currentCameraSettings.pipelineSettings.cameraAutoExposure"
|
||||
@@ -92,14 +100,6 @@ const interactiveCols = computed(() =>
|
||||
:slider-cols="interactiveCols"
|
||||
@input="(args) => useCameraSettingsStore().changeCurrentPipelineSetting({ cameraBrightness: args }, false)"
|
||||
/>
|
||||
<pv-switch
|
||||
v-model="useCameraSettingsStore().currentPipelineSettings.cameraAutoExposure"
|
||||
class="pt-2"
|
||||
label="Auto Exposure"
|
||||
:switch-cols="interactiveCols"
|
||||
tooltip="Enables or Disables camera automatic adjustment for current lighting conditions"
|
||||
@input="(args) => useCameraSettingsStore().changeCurrentPipelineSetting({ cameraAutoExposure: args }, false)"
|
||||
/>
|
||||
<pv-slider
|
||||
v-if="useCameraSettingsStore().currentPipelineSettings.cameraGain >= 0"
|
||||
v-model="useCameraSettingsStore().currentPipelineSettings.cameraGain"
|
||||
@@ -130,25 +130,22 @@ const interactiveCols = computed(() =>
|
||||
tooltip="Controls blue automatic white balance gain, which affects how the camera captures colors in different conditions"
|
||||
@input="(args) => useCameraSettingsStore().changeCurrentPipelineSetting({ cameraBlueGain: args }, false)"
|
||||
/>
|
||||
|
||||
<pv-switch
|
||||
v-model="useCameraSettingsStore().currentPipelineSettings.cameraAutoWhiteBalance"
|
||||
class="pt-2"
|
||||
label="Auto White Balance"
|
||||
:switch-cols="interactiveCols"
|
||||
tooltip="Enables or Disables camera automatic adjustment for current lighting conditions"
|
||||
@input="(args) => useCameraSettingsStore().changeCurrentPipelineSetting({ cameraAutoWhiteBalance: args }, false)"
|
||||
/>
|
||||
<pv-slider
|
||||
v-if="!useCameraSettingsStore().currentPipelineSettings.cameraAutoWhiteBalance"
|
||||
v-model="useCameraSettingsStore().currentPipelineSettings.cameraWhiteBalanceTemp"
|
||||
:disabled="useCameraSettingsStore().currentPipelineSettings.cameraAutoWhiteBalance"
|
||||
label="White Balance Temperature"
|
||||
:min="useCameraSettingsStore().minWhiteBalanceTemp"
|
||||
:max="useCameraSettingsStore().maxWhiteBalanceTemp"
|
||||
:slider-cols="interactiveCols"
|
||||
@input="(args) => useCameraSettingsStore().changeCurrentPipelineSetting({ cameraWhiteBalanceTemp: args }, false)"
|
||||
/>
|
||||
|
||||
<pv-select
|
||||
v-model="useCameraSettingsStore().currentPipelineSettings.inputImageRotationMode"
|
||||
label="Orientation"
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user