mirror of
https://github.com/wpilibsuite/allwpilib
synced 2026-06-24 01:31:46 +00:00
Compare commits
420 Commits
v2023.0.0-
...
v2023.4.2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4b0eecaee0 | ||
|
|
edf4ded412 | ||
|
|
4c46b6aff9 | ||
|
|
490ca4a68a | ||
|
|
cbb5b0b802 | ||
|
|
bb7053d9ee | ||
|
|
9efed9a533 | ||
|
|
dbbfe1aed2 | ||
|
|
de65a135c3 | ||
|
|
3e9788cdff | ||
|
|
ecb072724d | ||
|
|
0d462a4561 | ||
|
|
ba37986561 | ||
|
|
25ab9cda92 | ||
|
|
2f6251d4a6 | ||
|
|
e9a7bed988 | ||
|
|
9cc14bbb43 | ||
|
|
8068369542 | ||
|
|
805c837a42 | ||
|
|
fd18577ba0 | ||
|
|
74dea9f05e | ||
|
|
9eef79d638 | ||
|
|
843574a810 | ||
|
|
226ef35212 | ||
|
|
b30664d630 | ||
|
|
804e5ce236 | ||
|
|
49af88f2bb | ||
|
|
d56314f866 | ||
|
|
43975ac7cc | ||
|
|
5483464158 | ||
|
|
785e7dd85c | ||
|
|
e57ded8c39 | ||
|
|
01f0394419 | ||
|
|
59be120982 | ||
|
|
37f065032f | ||
|
|
22a170bee7 | ||
|
|
2f310a748c | ||
|
|
b43ec87f57 | ||
|
|
19267bef0c | ||
|
|
84cbd48d84 | ||
|
|
1f35750865 | ||
|
|
8230fc631d | ||
|
|
b879a6f8c6 | ||
|
|
49459d3e45 | ||
|
|
4079eabe9b | ||
|
|
fe5d226a19 | ||
|
|
b7535252c2 | ||
|
|
b61ac6db33 | ||
|
|
7b828ce84f | ||
|
|
08a536291b | ||
|
|
193a10d020 | ||
|
|
7867bbde0e | ||
|
|
fa7c01b598 | ||
|
|
2b81610248 | ||
|
|
a4a369b8da | ||
|
|
d991f6e435 | ||
|
|
a27a047ae8 | ||
|
|
2f96cae31a | ||
|
|
83ef8f9658 | ||
|
|
4054893669 | ||
|
|
f75acd11ce | ||
|
|
8bf67b1b33 | ||
|
|
49bb1358d8 | ||
|
|
9c4c07c0f9 | ||
|
|
1a47cc2e86 | ||
|
|
7cd30cffbc | ||
|
|
92aecab2ef | ||
|
|
8785bba080 | ||
|
|
9e5b7b8040 | ||
|
|
917906530a | ||
|
|
00aa66e4fd | ||
|
|
893320544a | ||
|
|
b95d0e060d | ||
|
|
008232b43c | ||
|
|
522be348f4 | ||
|
|
d48a83dee2 | ||
|
|
504fa22143 | ||
|
|
b2b25bf09f | ||
|
|
ce3dc4eb3b | ||
|
|
1ea48caa7d | ||
|
|
fb101925a7 | ||
|
|
657951f6dd | ||
|
|
a60ca9d71c | ||
|
|
f8a45f1558 | ||
|
|
ecba8b99a8 | ||
|
|
e95e88fdf9 | ||
|
|
371d15dec3 | ||
|
|
cb9b8938af | ||
|
|
3b084ecbe0 | ||
|
|
27ba096ea1 | ||
|
|
42c997a3c4 | ||
|
|
5f1a025f27 | ||
|
|
0ebf79b54c | ||
|
|
a8c465f3fb | ||
|
|
a7b1ab683d | ||
|
|
bd6479dc29 | ||
|
|
5cb0340a8c | ||
|
|
ab0e8c37a7 | ||
|
|
b74ac1c645 | ||
|
|
cf1a411acf | ||
|
|
1e05b21ab5 | ||
|
|
e5a6197633 | ||
|
|
039edcc23f | ||
|
|
f7f19207e0 | ||
|
|
befd12911c | ||
|
|
34519de60a | ||
|
|
dc4355c031 | ||
|
|
53d8d33bca | ||
|
|
530ae40614 | ||
|
|
79f565191e | ||
|
|
2cd9be413f | ||
|
|
babb0c1fcf | ||
|
|
330ba45f9c | ||
|
|
51272ef6b3 | ||
|
|
0d105ab771 | ||
|
|
cf4235ea36 | ||
|
|
2d4b7b9147 | ||
|
|
aec6f3d506 | ||
|
|
bfe346c76a | ||
|
|
83f1860047 | ||
|
|
9872e676d8 | ||
|
|
25db20e49d | ||
|
|
b0c6724eed | ||
|
|
f0fa8205ac | ||
|
|
42fc4cb6bc | ||
|
|
cc166c98d2 | ||
|
|
3f51f10ad3 | ||
|
|
1562eae74a | ||
|
|
b632b288a3 | ||
|
|
c11bd2720f | ||
|
|
f1151d375f | ||
|
|
fe1b62647f | ||
|
|
c49a45abbd | ||
|
|
bc3d01a721 | ||
|
|
bc473240ae | ||
|
|
2121bd5fb8 | ||
|
|
835f8470d6 | ||
|
|
6cfe5de00d | ||
|
|
2ac41f3edc | ||
|
|
26bdbf3d41 | ||
|
|
92149efa11 | ||
|
|
176fddeb4c | ||
|
|
87a34af367 | ||
|
|
4534e75787 | ||
|
|
1cbebaa2f7 | ||
|
|
6efb9ee405 | ||
|
|
1e7fcd5637 | ||
|
|
1f940e2b60 | ||
|
|
a6d127aedf | ||
|
|
b893b3d6d3 | ||
|
|
1696a490fa | ||
|
|
40a22d69bc | ||
|
|
e84dbfede0 | ||
|
|
8aa9dbfa90 | ||
|
|
eda2fa8a17 | ||
|
|
d20594db0d | ||
|
|
dd8ecfdd54 | ||
|
|
17ceebfff4 | ||
|
|
8b74ab389d | ||
|
|
1aad3489c2 | ||
|
|
2744991771 | ||
|
|
ffbf6a1fa2 | ||
|
|
fbabd0ef15 | ||
|
|
7713f68772 | ||
|
|
701995d6cc | ||
|
|
bf7068ac27 | ||
|
|
aae0f52ca6 | ||
|
|
ee02fb7ba7 | ||
|
|
518916ba02 | ||
|
|
3997c6635b | ||
|
|
cc8675a4e5 | ||
|
|
fb2c170b6e | ||
|
|
7ba8a9ee1f | ||
|
|
c569d8e523 | ||
|
|
2a5e89fa97 | ||
|
|
cc003c6c38 | ||
|
|
5522916123 | ||
|
|
967b30de3a | ||
|
|
3270d4fc86 | ||
|
|
be39678447 | ||
|
|
61c75deb2a | ||
|
|
a865f48e96 | ||
|
|
f66a667321 | ||
|
|
f8d4e9866e | ||
|
|
7e84ea891f | ||
|
|
da3ec1be10 | ||
|
|
944dd7265d | ||
|
|
6948cea67a | ||
|
|
a31459bce6 | ||
|
|
4a0ad6b48c | ||
|
|
e6552d272e | ||
|
|
bde383f763 | ||
|
|
5a52b51443 | ||
|
|
69a66ec5ec | ||
|
|
989c9fb29a | ||
|
|
0f5b08ec69 | ||
|
|
fba191099c | ||
|
|
b390cad095 | ||
|
|
b9772214d9 | ||
|
|
342c375a71 | ||
|
|
b0e4053087 | ||
|
|
f3e666b7bb | ||
|
|
b300518bd1 | ||
|
|
be27171236 | ||
|
|
4bbdbdfb48 | ||
|
|
f18fd41ac3 | ||
|
|
2d0faecf4f | ||
|
|
348bd107fc | ||
|
|
3149dc64b8 | ||
|
|
8618dd4160 | ||
|
|
72e21a1ed1 | ||
|
|
eab0d929e6 | ||
|
|
6789869663 | ||
|
|
c9dea2968d | ||
|
|
8f402645f5 | ||
|
|
f24ad1d715 | ||
|
|
ff88756864 | ||
|
|
f58873db8e | ||
|
|
37e969b41a | ||
|
|
13cdc29382 | ||
|
|
6e23985ae6 | ||
|
|
66bb0ffb2c | ||
|
|
74cc86c4c5 | ||
|
|
e22d8cc343 | ||
|
|
68dba92630 | ||
|
|
23bfc2d9ab | ||
|
|
1f1461e254 | ||
|
|
eae68fc165 | ||
|
|
4c4545fb4b | ||
|
|
16ffaa754d | ||
|
|
5e74ff26d8 | ||
|
|
53875419a1 | ||
|
|
aa6499e920 | ||
|
|
df70351107 | ||
|
|
e9bd50ff9b | ||
|
|
9b319fd56b | ||
|
|
18d28ec5e3 | ||
|
|
bdfb625211 | ||
|
|
21003e34eb | ||
|
|
70080457d5 | ||
|
|
e82cd5147b | ||
|
|
ec124bb662 | ||
|
|
2b2aa8eef7 | ||
|
|
cb38bacfe8 | ||
|
|
15561338d5 | ||
|
|
ca35a2e097 | ||
|
|
20dbae0cee | ||
|
|
1a59737f40 | ||
|
|
42b6d4e3f7 | ||
|
|
135c13958f | ||
|
|
ffbfc61532 | ||
|
|
8958b2a4da | ||
|
|
e4ac09077c | ||
|
|
f40de0c120 | ||
|
|
51fa3e851f | ||
|
|
1da84b2255 | ||
|
|
e43e2fbc84 | ||
|
|
5804d8fa84 | ||
|
|
169ef5fabf | ||
|
|
148759ef54 | ||
|
|
58ed112b51 | ||
|
|
dd1da77d20 | ||
|
|
7cda85df20 | ||
|
|
7ed9b13277 | ||
|
|
6b4f26225d | ||
|
|
b2d2924b72 | ||
|
|
34ec89c041 | ||
|
|
e15200068d | ||
|
|
d5200db6cd | ||
|
|
2ee3d86de4 | ||
|
|
9f0a8b930f | ||
|
|
2bca43779e | ||
|
|
4307d0ee8b | ||
|
|
3fe8d355a1 | ||
|
|
b44034dadc | ||
|
|
52d2c53888 | ||
|
|
76e918f71e | ||
|
|
0bee875aff | ||
|
|
98e922313b | ||
|
|
9a36373b8f | ||
|
|
cf8faa9e67 | ||
|
|
5ec067c1f8 | ||
|
|
e962fd2916 | ||
|
|
88bd67e7de | ||
|
|
902e8686d3 | ||
|
|
e2d49181da | ||
|
|
149bac55b1 | ||
|
|
88f7a3ccb9 | ||
|
|
8acce443f0 | ||
|
|
295a1f8f3b | ||
|
|
388e7a4265 | ||
|
|
13aceea8dc | ||
|
|
c203f3f0a9 | ||
|
|
f54d495c90 | ||
|
|
e6392a1570 | ||
|
|
53904e7cf4 | ||
|
|
2e88a496c2 | ||
|
|
ce4c45df13 | ||
|
|
0401597d3b | ||
|
|
2e5f9e45bb | ||
|
|
e4b5795fc7 | ||
|
|
03d0ea188c | ||
|
|
3082bd236b | ||
|
|
b7ca860417 | ||
|
|
64838e6367 | ||
|
|
1269d2b901 | ||
|
|
14d8506b72 | ||
|
|
d1d458db2b | ||
|
|
f656e99245 | ||
|
|
6dd937cef7 | ||
|
|
49047c85b9 | ||
|
|
d07267fed1 | ||
|
|
b53ce1d3f0 | ||
|
|
5a320c326b | ||
|
|
c4e526d315 | ||
|
|
d122e4254f | ||
|
|
5a1e7ea036 | ||
|
|
179f569113 | ||
|
|
b0f6dc199d | ||
|
|
7836f661cd | ||
|
|
dbcc1de37f | ||
|
|
93890c528b | ||
|
|
3d8d5936f9 | ||
|
|
2b04159dec | ||
|
|
2764004fad | ||
|
|
85f1bb8f2b | ||
|
|
231ae2c353 | ||
|
|
e92b6dd5f9 | ||
|
|
2a8e0e1cc8 | ||
|
|
7d06e517e9 | ||
|
|
323524fed6 | ||
|
|
d426873ed1 | ||
|
|
5be5869b2f | ||
|
|
b1b4c1e9e7 | ||
|
|
a4054d702f | ||
|
|
0190301e09 | ||
|
|
9d1ce6a6d9 | ||
|
|
5005e2ca04 | ||
|
|
fa44a07938 | ||
|
|
4ba16db645 | ||
|
|
837415abfd | ||
|
|
2c20fd0d09 | ||
|
|
64a7136e08 | ||
|
|
b2b473b24a | ||
|
|
7aab8fa93a | ||
|
|
12c2851856 | ||
|
|
0da169dd84 | ||
|
|
2416827c25 | ||
|
|
1177a3522e | ||
|
|
102344e27a | ||
|
|
1831ef3e19 | ||
|
|
a9606ce870 | ||
|
|
6c80d5eab3 | ||
|
|
b114006543 | ||
|
|
32fbfb7da6 | ||
|
|
02465920fb | ||
|
|
3a5a376465 | ||
|
|
1c3c86e9f1 | ||
|
|
dcda09f90a | ||
|
|
66157397c1 | ||
|
|
9e22ffbebf | ||
|
|
648ab6115c | ||
|
|
8bc3b04f5b | ||
|
|
cfb84a6083 | ||
|
|
02c47726e1 | ||
|
|
b2a0093294 | ||
|
|
2a98d6b5d7 | ||
|
|
9f36301dc8 | ||
|
|
901fc555f4 | ||
|
|
4170ec6107 | ||
|
|
fe400f68c5 | ||
|
|
794669b346 | ||
|
|
dcfa85a5d5 | ||
|
|
15ad855f1d | ||
|
|
11244a49d9 | ||
|
|
1d2e8eb153 | ||
|
|
ad53fb19b4 | ||
|
|
ba850bac3b | ||
|
|
023a5989f8 | ||
|
|
c970011ccc | ||
|
|
07a43c3d9a | ||
|
|
a05b212b04 | ||
|
|
09faf31b67 | ||
|
|
9e1f9c1133 | ||
|
|
f19d2b9b84 | ||
|
|
a28f93863c | ||
|
|
c9f61669b8 | ||
|
|
dcce5ad3b3 | ||
|
|
6836e5923d | ||
|
|
335188c652 | ||
|
|
60a29dcb99 | ||
|
|
b55d5b3034 | ||
|
|
10ed4b3969 | ||
|
|
4a401b89d7 | ||
|
|
c195b4fc46 | ||
|
|
8f2e34c6a3 | ||
|
|
150d692df7 | ||
|
|
3e5bfff1b5 | ||
|
|
9c7e66a27d | ||
|
|
0ca274866b | ||
|
|
dc037f8d41 | ||
|
|
16cdc741cf | ||
|
|
9d5055176d | ||
|
|
d1e66e1296 | ||
|
|
1fc098e696 | ||
|
|
878cc8defb | ||
|
|
8153911160 | ||
|
|
fbdc810887 | ||
|
|
396143004c | ||
|
|
1f45732700 | ||
|
|
574cb41c18 | ||
|
|
d9d6c425e7 | ||
|
|
58b6484dbe | ||
|
|
ca43fe2798 | ||
|
|
87a64ccedc | ||
|
|
89a3d00297 | ||
|
|
1497665f96 | ||
|
|
27b173374e | ||
|
|
2a13dba8ac | ||
|
|
77301b126c |
@@ -1,5 +1,4 @@
|
||||
---
|
||||
Language: Cpp
|
||||
BasedOnStyle: Google
|
||||
AccessModifierOffset: -1
|
||||
AlignAfterOpenBracket: Align
|
||||
@@ -157,7 +156,7 @@ SpacesInCStyleCastParentheses: false
|
||||
SpacesInParentheses: false
|
||||
SpacesInSquareBrackets: false
|
||||
SpaceBeforeSquareBrackets: false
|
||||
Standard: c++17
|
||||
Standard: c++20
|
||||
StatementMacros:
|
||||
- Q_UNUSED
|
||||
- QT_REQUIRE_VERSION
|
||||
|
||||
13
.github/workflows/cmake.yml
vendored
13
.github/workflows/cmake.yml
vendored
@@ -12,9 +12,9 @@ jobs:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- os: ubuntu-latest
|
||||
- os: ubuntu-22.04
|
||||
name: Linux
|
||||
container: wpilib/roborio-cross-ubuntu:2022-20.04
|
||||
container: wpilib/roborio-cross-ubuntu:2023-22.04
|
||||
flags: ""
|
||||
- os: macOS-11
|
||||
name: macOS
|
||||
@@ -27,12 +27,17 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Install dependencies (Linux)
|
||||
if: runner.os == 'Linux'
|
||||
run: sudo apt-get update && sudo apt-get install -y libopencv-dev libopencv4.5-java python-is-python3
|
||||
|
||||
- name: Install opencv (macOS)
|
||||
run: brew install opencv
|
||||
if: runner.os == 'macOS'
|
||||
|
||||
- name: Set up Python 3.8
|
||||
uses: actions/setup-python@v2
|
||||
- name: Set up Python 3.8 (macOS)
|
||||
if: runner.os == 'macOS'
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: 3.8
|
||||
|
||||
|
||||
17
.github/workflows/comment-command.yml
vendored
17
.github/workflows/comment-command.yml
vendored
@@ -4,9 +4,9 @@ on:
|
||||
types: [ created ]
|
||||
|
||||
jobs:
|
||||
wpiformat:
|
||||
if: github.event.issue.pull_request && startsWith(github.event.comment.body, '/wpiformat')
|
||||
runs-on: ubuntu-latest
|
||||
format:
|
||||
if: github.event.issue.pull_request && startsWith(github.event.comment.body, '/format')
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- name: React Rocket
|
||||
uses: actions/github-script@v4
|
||||
@@ -37,21 +37,28 @@ jobs:
|
||||
uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: 3.8
|
||||
- name: Setup Java
|
||||
uses: actions/setup-java@v3
|
||||
with:
|
||||
distribution: 'zulu'
|
||||
java-version: 11
|
||||
- name: Install clang-format
|
||||
run: |
|
||||
wget -O - https://apt.llvm.org/llvm-snapshot.gpg.key | sudo apt-key add -
|
||||
sudo sh -c "echo 'deb http://apt.llvm.org/focal/ llvm-toolchain-focal-14 main' >> /etc/apt/sources.list.d/proposed-repositories.list"
|
||||
sudo sh -c "echo 'deb http://apt.llvm.org/jammy/ llvm-toolchain-jammy-14 main' >> /etc/apt/sources.list.d/proposed-repositories.list"
|
||||
sudo apt-get update -q
|
||||
sudo apt-get install -y clang-format-14
|
||||
- name: Install wpiformat
|
||||
run: pip3 install wpiformat
|
||||
- name: Run wpiformat
|
||||
run: wpiformat -clang 14
|
||||
- name: Run spotlessApply
|
||||
run: ./gradlew spotlessApply
|
||||
- name: Commit
|
||||
run: |
|
||||
# Set credentials
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
||||
# Commit
|
||||
git commit -am "wpiformat"
|
||||
git commit -am "Formatting fixes"
|
||||
git push
|
||||
|
||||
9
.github/workflows/documentation.yml
vendored
9
.github/workflows/documentation.yml
vendored
@@ -12,7 +12,7 @@ env:
|
||||
jobs:
|
||||
publish:
|
||||
name: "Documentation - Publish"
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-22.04
|
||||
if: github.repository_owner == 'wpilibsuite' && (github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v'))
|
||||
concurrency: ci-docs-publish
|
||||
steps:
|
||||
@@ -20,11 +20,10 @@ jobs:
|
||||
with:
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
- uses: actions/setup-java@v1
|
||||
- uses: actions/setup-java@v3
|
||||
with:
|
||||
distribution: 'zulu'
|
||||
java-version: 13
|
||||
- name: Install libclang-9
|
||||
run: sudo apt update && sudo apt install -y libclang-cpp9 libclang1-9
|
||||
- name: Set environment variables (Development)
|
||||
run: |
|
||||
echo "TARGET_FOLDER=$BASE_PATH/development" >> $GITHUB_ENV
|
||||
@@ -42,7 +41,7 @@ jobs:
|
||||
- name: Build with Gradle
|
||||
run: ./gradlew docs:generateJavaDocs docs:doxygen -PbuildServer ${{ env.EXTRA_GRADLE_ARGS }}
|
||||
- name: Install SSH Client 🔑
|
||||
uses: webfactory/ssh-agent@v0.4.1
|
||||
uses: webfactory/ssh-agent@v0.7.0
|
||||
with:
|
||||
ssh-private-key: ${{ secrets.GH_DEPLOY_KEY }}
|
||||
- name: Deploy Java 🚀
|
||||
|
||||
19
.github/workflows/gazebo.yml
vendored
19
.github/workflows/gazebo.yml
vendored
@@ -1,19 +0,0 @@
|
||||
name: Gazebo
|
||||
|
||||
on: [pull_request, push]
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.head_ref || github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: "Build"
|
||||
runs-on: ubuntu-latest
|
||||
container: wpilib/gazebo-ubuntu:20.04
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Build with Gradle
|
||||
run: ./gradlew simulation:frc_gazebo_plugins:build simulation:halsim_gazebo:build -PbuildServer -PforceGazebo
|
||||
59
.github/workflows/gradle.yml
vendored
59
.github/workflows/gradle.yml
vendored
@@ -12,20 +12,20 @@ jobs:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- container: wpilib/roborio-cross-ubuntu:2022-20.04
|
||||
- container: wpilib/roborio-cross-ubuntu:2023-22.04
|
||||
artifact-name: Athena
|
||||
build-options: "-Ponlylinuxathena"
|
||||
- container: wpilib/raspbian-cross-ubuntu:10-20.04
|
||||
- container: wpilib/raspbian-cross-ubuntu:bullseye-22.04
|
||||
artifact-name: Arm32
|
||||
build-options: "-Ponlylinuxarm32"
|
||||
- container: wpilib/aarch64-cross-ubuntu:bionic-20.04
|
||||
- container: wpilib/aarch64-cross-ubuntu:bullseye-22.04
|
||||
artifact-name: Arm64
|
||||
build-options: "-Ponlylinuxarm64"
|
||||
- container: wpilib/ubuntu-base:20.04
|
||||
- container: wpilib/ubuntu-base:22.04
|
||||
artifact-name: Linux
|
||||
build-options: "-Ponlylinuxx86-64"
|
||||
name: "Build - ${{ matrix.artifact-name }}"
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-22.04
|
||||
container: ${{ matrix.container }}
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
@@ -39,42 +39,50 @@ jobs:
|
||||
env:
|
||||
ARTIFACTORY_PUBLISH_USERNAME: ${{ secrets.ARTIFACTORY_USERNAME }}
|
||||
ARTIFACTORY_PUBLISH_PASSWORD: ${{ secrets.ARTIFACTORY_PASSWORD }}
|
||||
- uses: actions/upload-artifact@v2
|
||||
- uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: ${{ matrix.artifact-name }}
|
||||
path: build/allOutputs
|
||||
|
||||
build-host:
|
||||
env:
|
||||
MACOSX_DEPLOYMENT_TARGET: 10.14
|
||||
MACOSX_DEPLOYMENT_TARGET: 10.15
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- os: windows-2019
|
||||
- os: windows-2022
|
||||
artifact-name: Win64Debug
|
||||
architecture: x64
|
||||
task: "build"
|
||||
build-options: "-PciDebugOnly --max-workers 1"
|
||||
- os: windows-2019
|
||||
outputs: "build/allOutputs"
|
||||
- os: windows-2022
|
||||
artifact-name: Win64Release
|
||||
architecture: x64
|
||||
build-options: "-PciReleaseOnly --max-workers 1"
|
||||
task: "copyAllOutputs"
|
||||
outputs: "build/allOutputs"
|
||||
- os: macOS-11
|
||||
artifact-name: macOS
|
||||
architecture: x64
|
||||
build-options: "-Pbuildalldesktop"
|
||||
task: "build"
|
||||
outputs: "build/allOutputs"
|
||||
- os: windows-2022
|
||||
artifact-name: Win32
|
||||
architecture: x86
|
||||
task: ":ntcoreffi:build"
|
||||
outputs: "ntcoreffi/build/outputs"
|
||||
name: "Build - ${{ matrix.artifact-name }}"
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- uses: actions/setup-java@v1
|
||||
- uses: actions/setup-java@v3
|
||||
with:
|
||||
java-version: 11
|
||||
distribution: 'zulu'
|
||||
java-version: 17
|
||||
architecture: ${{ matrix.architecture }}
|
||||
- name: Import Developer ID Certificate
|
||||
uses: wpilibsuite/import-signing-certificate@v1
|
||||
@@ -94,33 +102,35 @@ jobs:
|
||||
run: echo "EXTRA_GRADLE_ARGS=-PreleaseMode" >> $GITHUB_ENV
|
||||
shell: bash
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
- name: Set Java Heap Size
|
||||
run: sed -i 's/-Xmx2g/-Xmx1g/g' gradle.properties
|
||||
if: matrix.artifact-name == 'Win32'
|
||||
- name: Build with Gradle
|
||||
run: ./gradlew ${{ matrix.task }} --build-cache -PbuildServer -PskipJavaFormat ${{ matrix.build-options }} ${{ env.EXTRA_GRADLE_ARGS }}
|
||||
env:
|
||||
ARTIFACTORY_PUBLISH_USERNAME: ${{ secrets.ARTIFACTORY_USERNAME }}
|
||||
ARTIFACTORY_PUBLISH_PASSWORD: ${{ secrets.ARTIFACTORY_PASSWORD }}
|
||||
- name: Sign Libraries with Developer ID
|
||||
run: ./gradlew build -PbuildServer -PskipJavaFormat -PdeveloperID=${{ secrets.APPLE_DEVELOPER_ID }} ${{ matrix.build-options }} ${{ env.EXTRA_GRADLE_ARGS }}
|
||||
run: ./gradlew copyAllOutputs --build-cache -PbuildServer -PskipJavaFormat -PdeveloperID=${{ secrets.APPLE_DEVELOPER_ID }} ${{ matrix.build-options }} ${{ env.EXTRA_GRADLE_ARGS }}
|
||||
if: |
|
||||
matrix.artifact-name == 'macOS' && (github.repository_owner == 'wpilibsuite' &&
|
||||
(github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v')))
|
||||
- uses: actions/upload-artifact@v2
|
||||
- uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: ${{ matrix.artifact-name }}
|
||||
path: build/allOutputs
|
||||
path: ${{ matrix.outputs }}
|
||||
|
||||
build-documentation:
|
||||
name: "Build - Documentation"
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- uses: actions/setup-java@v1
|
||||
- uses: actions/setup-java@v3
|
||||
with:
|
||||
distribution: 'zulu'
|
||||
java-version: 13
|
||||
- name: Install libclang-9
|
||||
run: sudo apt update && sudo apt install -y libclang-cpp9 libclang1-9
|
||||
- name: Set release environment variable
|
||||
run: echo "EXTRA_GRADLE_ARGS=-PreleaseMode" >> $GITHUB_ENV
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
@@ -129,7 +139,7 @@ jobs:
|
||||
env:
|
||||
ARTIFACTORY_PUBLISH_USERNAME: ${{ secrets.ARTIFACTORY_USERNAME }}
|
||||
ARTIFACTORY_PUBLISH_PASSWORD: ${{ secrets.ARTIFACTORY_PASSWORD }}
|
||||
- uses: actions/upload-artifact@v2
|
||||
- uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: Documentation
|
||||
path: docs/build/outputs
|
||||
@@ -137,12 +147,12 @@ jobs:
|
||||
combine:
|
||||
name: Combine
|
||||
needs: [build-docker, build-host, build-documentation]
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
repository: wpilibsuite/build-tools
|
||||
- uses: actions/download-artifact@v2
|
||||
- uses: actions/download-artifact@v3
|
||||
with:
|
||||
path: combiner/products/build/allOutputs
|
||||
- name: Flatten Artifacts
|
||||
@@ -151,8 +161,9 @@ jobs:
|
||||
run: |
|
||||
cat combiner/products/build/allOutputs/version.txt
|
||||
test -s combiner/products/build/allOutputs/version.txt
|
||||
- uses: actions/setup-java@v1
|
||||
- uses: actions/setup-java@v3
|
||||
with:
|
||||
distribution: 'zulu'
|
||||
java-version: 11
|
||||
- name: Combine
|
||||
if: |
|
||||
@@ -177,7 +188,7 @@ jobs:
|
||||
RUN_AZURE_ARTIFACTORY_RELEASE: "TRUE"
|
||||
ARTIFACTORY_PUBLISH_USERNAME: ${{ secrets.ARTIFACTORY_USERNAME }}
|
||||
ARTIFACTORY_PUBLISH_PASSWORD: ${{ secrets.ARTIFACTORY_PASSWORD }}
|
||||
- uses: actions/upload-artifact@v2
|
||||
- uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: Maven
|
||||
path: ~/releases
|
||||
|
||||
35
.github/workflows/lint-format.yml
vendored
35
.github/workflows/lint-format.yml
vendored
@@ -13,7 +13,7 @@ concurrency:
|
||||
jobs:
|
||||
wpiformat:
|
||||
name: "wpiformat"
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Fetch all history and metadata
|
||||
@@ -23,13 +23,13 @@ jobs:
|
||||
git checkout -b pr
|
||||
git branch -f main origin/main
|
||||
- name: Set up Python 3.8
|
||||
uses: actions/setup-python@v2
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: 3.8
|
||||
- name: Install clang-format
|
||||
run: |
|
||||
wget -O - https://apt.llvm.org/llvm-snapshot.gpg.key | sudo apt-key add -
|
||||
sudo sh -c "echo 'deb http://apt.llvm.org/focal/ llvm-toolchain-focal-14 main' >> /etc/apt/sources.list.d/proposed-repositories.list"
|
||||
sudo sh -c "echo 'deb http://apt.llvm.org/jammy/ llvm-toolchain-jammy-14 main' >> /etc/apt/sources.list.d/proposed-repositories.list"
|
||||
sudo apt-get update -q
|
||||
sudo apt-get install -y clang-format-14
|
||||
- name: Install wpiformat
|
||||
@@ -41,15 +41,23 @@ jobs:
|
||||
- name: Generate diff
|
||||
run: git diff HEAD > wpiformat-fixes.patch
|
||||
if: ${{ failure() }}
|
||||
- uses: actions/upload-artifact@v2
|
||||
- uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: wpiformat fixes
|
||||
path: wpiformat-fixes.patch
|
||||
if: ${{ failure() }}
|
||||
- name: Write to job summary
|
||||
run: |
|
||||
echo '```diff' >> $GITHUB_STEP_SUMMARY
|
||||
cat wpiformat-fixes.patch >> $GITHUB_STEP_SUMMARY
|
||||
echo '' >> $GITHUB_STEP_SUMMARY
|
||||
echo '```' >> $GITHUB_STEP_SUMMARY
|
||||
if: ${{ failure() }}
|
||||
|
||||
tidy:
|
||||
name: "clang-tidy"
|
||||
runs-on: ubuntu-latest
|
||||
container: wpilib/roborio-cross-ubuntu:2022-20.04
|
||||
runs-on: ubuntu-22.04
|
||||
container: wpilib/roborio-cross-ubuntu:2023-22.04
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Fetch all history and metadata
|
||||
@@ -59,13 +67,13 @@ jobs:
|
||||
git checkout -b pr
|
||||
git branch -f main origin/main
|
||||
- name: Set up Python 3.8
|
||||
uses: actions/setup-python@v2
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: 3.8
|
||||
- name: Install clang-tidy
|
||||
run: |
|
||||
wget -O - https://apt.llvm.org/llvm-snapshot.gpg.key | sudo apt-key add -
|
||||
sudo sh -c "echo 'deb http://apt.llvm.org/focal/ llvm-toolchain-focal-14 main' >> /etc/apt/sources.list.d/proposed-repositories.list"
|
||||
sudo sh -c "echo 'deb http://apt.llvm.org/jammy/ llvm-toolchain-jammy-14 main' >> /etc/apt/sources.list.d/proposed-repositories.list"
|
||||
sudo apt-get update -q
|
||||
sudo apt-get install -y clang-tidy-14 clang-format-14
|
||||
- name: Install wpiformat
|
||||
@@ -78,8 +86,8 @@ jobs:
|
||||
run: wpiformat -clang 14 -no-format -tidy-changed -compile-commands=build/compile_commands/linuxx86-64 -vv
|
||||
javaformat:
|
||||
name: "Java format"
|
||||
runs-on: ubuntu-latest
|
||||
container: wpilib/ubuntu-base:20.04
|
||||
runs-on: ubuntu-22.04
|
||||
container: wpilib/ubuntu-base:22.04
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Fetch all history and metadata
|
||||
@@ -97,15 +105,14 @@ jobs:
|
||||
if: ${{ failure() }}
|
||||
documentation:
|
||||
name: "Documentation"
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- uses: actions/setup-java@v1
|
||||
- uses: actions/setup-java@v3
|
||||
with:
|
||||
distribution: 'zulu'
|
||||
java-version: 13
|
||||
- name: Install libclang-9
|
||||
run: sudo apt update && sudo apt install -y libclang-cpp9 libclang1-9
|
||||
- name: Build with Gradle
|
||||
run: ./gradlew docs:zipDocs -PbuildServer -PdocWarningsAsErrors ${{ env.EXTRA_GRADLE_ARGS }}
|
||||
|
||||
23
.github/workflows/sanitizers.yml
vendored
23
.github/workflows/sanitizers.yml
vendored
@@ -15,40 +15,29 @@ jobs:
|
||||
- name: asan
|
||||
cmake-flags: "-DCMAKE_BUILD_TYPE=Asan"
|
||||
ctest-env: ""
|
||||
ctest-flags: "-E 'ntcore|wpilibc'"
|
||||
ctest-flags: "-E 'wpilibc'"
|
||||
- name: tsan
|
||||
cmake-flags: "-DCMAKE_BUILD_TYPE=Tsan"
|
||||
ctest-env: "TSAN_OPTIONS=second_deadlock_stack=1"
|
||||
ctest-flags: "-E 'ntcore|cscore|cameraserver|wpilibc|wpilibNewCommands'"
|
||||
ctest-flags: "-E 'cscore|cameraserver|wpilibc|wpilibNewCommands'"
|
||||
- name: ubsan
|
||||
cmake-flags: "-DCMAKE_BUILD_TYPE=Ubsan"
|
||||
ctest-env: ""
|
||||
ctest-flags: ""
|
||||
name: "${{ matrix.name }}"
|
||||
runs-on: ubuntu-latest
|
||||
container: wpilib/roborio-cross-ubuntu:2022-20.04
|
||||
runs-on: ubuntu-22.04
|
||||
container: wpilib/roborio-cross-ubuntu:2023-22.04
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Install Dependencies
|
||||
run: |
|
||||
sudo add-apt-repository ppa:ubuntu-toolchain-r/test
|
||||
sudo apt install -y gcc-11 g++-11
|
||||
sudo update-alternatives \
|
||||
--install /usr/bin/gcc gcc /usr/bin/gcc-11 11 \
|
||||
--slave /usr/bin/g++ g++ /usr/bin/g++-11
|
||||
sudo update-alternatives --set gcc /usr/bin/gcc-11
|
||||
|
||||
- name: Set up Python 3.8
|
||||
uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: 3.8
|
||||
run: sudo apt-get update && sudo apt-get install -y libopencv-dev libopencv4.5-java python-is-python3 clang-14
|
||||
|
||||
- name: Install jinja
|
||||
run: python -m pip install jinja2
|
||||
|
||||
- name: configure
|
||||
run: mkdir build && cd build && cmake ${{ matrix.cmake-flags }} ..
|
||||
run: mkdir build && cd build && cmake -DCMAKE_C_COMPILER:FILEPATH=/usr/bin/clang-14 -DCMAKE_CXX_COMPILER:FILEPATH=/usr/bin/clang++-14 ${{ matrix.cmake-flags }} ..
|
||||
|
||||
- name: build
|
||||
working-directory: build
|
||||
|
||||
8
.github/workflows/upstream-utils.yml
vendored
8
.github/workflows/upstream-utils.yml
vendored
@@ -13,7 +13,7 @@ concurrency:
|
||||
jobs:
|
||||
update:
|
||||
name: "Update"
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Fetch all history and metadata
|
||||
@@ -22,7 +22,7 @@ jobs:
|
||||
git checkout -b pr
|
||||
git branch -f main origin/main
|
||||
- name: Set up Python 3.9
|
||||
uses: actions/setup-python@v2
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: 3.9
|
||||
- name: Configure committer identity
|
||||
@@ -49,6 +49,10 @@ jobs:
|
||||
run: |
|
||||
cd upstream_utils
|
||||
./update_llvm.py
|
||||
- name: Run update_mpack.py
|
||||
run: |
|
||||
cd upstream_utils
|
||||
./update_mpack.py
|
||||
- name: Run update_stack_walker.py
|
||||
run: |
|
||||
cd upstream_utils
|
||||
|
||||
9
.gitignore
vendored
9
.gitignore
vendored
@@ -5,6 +5,12 @@ doxygen.log
|
||||
build*/
|
||||
!buildSrc/
|
||||
|
||||
simgui-ds.json
|
||||
simgui-window.json
|
||||
simgui.json
|
||||
|
||||
networktables.json
|
||||
|
||||
# Created by the jenkins test script
|
||||
test-reports
|
||||
|
||||
@@ -15,6 +21,9 @@ test-reports
|
||||
.idea/
|
||||
out/
|
||||
|
||||
# Fleet
|
||||
.fleet
|
||||
|
||||
# Created by http://www.gitignore.io
|
||||
|
||||
### Linux ###
|
||||
|
||||
@@ -18,6 +18,7 @@ generatedFileExclude {
|
||||
FRCNetComm\.java$
|
||||
simulation/gz_msgs/src/include/simulation/gz_msgs/msgs\.h$
|
||||
fieldImages/src/main/native/resources/
|
||||
apriltag/src/test/resources/
|
||||
}
|
||||
|
||||
repoRootNameOverride {
|
||||
|
||||
@@ -36,22 +36,23 @@ SET(CMAKE_SKIP_BUILD_RPATH FALSE)
|
||||
# (but later on when installing)
|
||||
SET(CMAKE_BUILD_WITH_INSTALL_RPATH FALSE)
|
||||
|
||||
SET(CMAKE_INSTALL_RPATH "${CMAKE_INSTALL_PREFIX}/wpilib/lib")
|
||||
SET(CMAKE_INSTALL_RPATH "${CMAKE_INSTALL_PREFIX}/lib")
|
||||
|
||||
# add the automatically determined parts of the RPATH
|
||||
# which point to directories outside the build tree to the install RPATH
|
||||
SET(CMAKE_INSTALL_RPATH_USE_LINK_PATH TRUE)
|
||||
|
||||
# the RPATH to be used when installing, but only if it's not a system directory
|
||||
LIST(FIND CMAKE_PLATFORM_IMPLICIT_LINK_DIRECTORIES "${CMAKE_INSTALL_PREFIX}/wpilib/lib" isSystemDir)
|
||||
LIST(FIND CMAKE_PLATFORM_IMPLICIT_LINK_DIRECTORIES "${CMAKE_INSTALL_PREFIX}/lib" isSystemDir)
|
||||
IF("${isSystemDir}" STREQUAL "-1")
|
||||
SET(CMAKE_INSTALL_RPATH "${CMAKE_INSTALL_PREFIX}/wpilib/lib")
|
||||
SET(CMAKE_INSTALL_RPATH "${CMAKE_INSTALL_PREFIX}/lib")
|
||||
ENDIF("${isSystemDir}" STREQUAL "-1")
|
||||
|
||||
# Options for building certain parts of the repo. Everything is built by default.
|
||||
option(BUILD_SHARED_LIBS "Build with shared libs (needed for JNI)" ON)
|
||||
option(WITH_JAVA "Include java and JNI in the build" ON)
|
||||
option(WITH_CSCORE "Build cscore (needs OpenCV)" ON)
|
||||
option(WITH_NTCORE "Build ntcore" ON)
|
||||
option(WITH_WPIMATH "Build wpimath" ON)
|
||||
option(WITH_WPILIB "Build hal, wpilibc/j, and myRobot (needs OpenCV)" ON)
|
||||
option(WITH_EXAMPLES "Build examples" OFF)
|
||||
@@ -63,10 +64,10 @@ option(WITH_SIMULATION_MODULES "Build simulation modules" ON)
|
||||
option(WITH_EXTERNAL_HAL "Use a separately built HAL" OFF)
|
||||
set(EXTERNAL_HAL_FILE "" CACHE FILEPATH "Location to look for an external HAL CMake File")
|
||||
|
||||
# Options for using a package manager (vcpkg) for certain dependencies.
|
||||
option(USE_VCPKG_FMTLIB "Use vcpkg fmtlib" OFF)
|
||||
option(USE_VCPKG_LIBUV "Use vcpkg libuv" OFF)
|
||||
option(USE_VCPKG_EIGEN "Use vcpkg eigen" OFF)
|
||||
# Options for using a package manager (e.g., vcpkg) for certain dependencies.
|
||||
option(USE_SYSTEM_FMTLIB "Use system fmtlib" OFF)
|
||||
option(USE_SYSTEM_LIBUV "Use system libuv" OFF)
|
||||
option(USE_SYSTEM_EIGEN "Use system eigen" OFF)
|
||||
|
||||
# Options for installation.
|
||||
option(WITH_FLAT_INSTALL "Use a flat install directory" OFF)
|
||||
@@ -117,6 +118,34 @@ FATAL: Cannot build simulation modules with wpilib disabled.
|
||||
")
|
||||
endif()
|
||||
|
||||
if (NOT WITH_NTCORE AND WITH_CSCORE)
|
||||
message(FATAL_ERROR "
|
||||
FATAL: Cannot build cameraserver without ntcore.
|
||||
Enable ntcore by setting WITH_NTCORE=ON
|
||||
")
|
||||
endif()
|
||||
|
||||
if (NOT WITH_NTCORE AND WITH_GUI)
|
||||
message(FATAL_ERROR "
|
||||
FATAL: Cannot build GUI modules without ntcore.
|
||||
Enable ntcore by setting WITH_NTCORE=ON
|
||||
")
|
||||
endif()
|
||||
|
||||
if (NOT WITH_NTCORE AND WITH_SIMULATION_MODULES)
|
||||
message(FATAL_ERROR "
|
||||
FATAL: Cannot build simulation modules without ntcore.
|
||||
Enable ntcore by setting WITH_NTCORE=ON
|
||||
")
|
||||
endif()
|
||||
|
||||
if (NOT WITH_NTCORE AND WITH_WPILIB)
|
||||
message(FATAL_ERROR "
|
||||
FATAL: Cannot build wpilib without ntcore.
|
||||
Enable ntcore by setting WITH_NTCORE=ON
|
||||
")
|
||||
endif()
|
||||
|
||||
if (NOT WITH_WPIMATH AND WITH_WPILIB)
|
||||
message(FATAL_ERROR "
|
||||
FATAL: Cannot build wpilib without wpimath.
|
||||
@@ -124,11 +153,11 @@ FATAL: Cannot build wpilib without wpimath.
|
||||
")
|
||||
endif()
|
||||
|
||||
set( wpilib_dest wpilib)
|
||||
set( include_dest wpilib/include )
|
||||
set( main_lib_dest wpilib/lib )
|
||||
set( java_lib_dest wpilib/java )
|
||||
set( jni_lib_dest wpilib/jni )
|
||||
set( wpilib_dest "")
|
||||
set( include_dest include )
|
||||
set( main_lib_dest lib )
|
||||
set( java_lib_dest java )
|
||||
set( jni_lib_dest jni )
|
||||
|
||||
if (WITH_FLAT_INSTALL)
|
||||
set (wpilib_config_dir ${wpilib_dest})
|
||||
@@ -136,12 +165,15 @@ else()
|
||||
set (wpilib_config_dir share/wpilib)
|
||||
endif()
|
||||
|
||||
if (USE_VCPKG_LIBUV)
|
||||
set (LIBUV_VCPKG_REPLACE "find_package(unofficial-libuv CONFIG)")
|
||||
if (USE_SYSTEM_LIBUV)
|
||||
set (LIBUV_SYSTEM_REPLACE "
|
||||
find_package(PkgConfig REQUIRED)
|
||||
pkg_check_modules(libuv REQUIRED IMPORTED_TARGET libuv)
|
||||
")
|
||||
endif()
|
||||
|
||||
if (USE_VCPKG_EIGEN)
|
||||
set (EIGEN_VCPKG_REPLACE "find_package(Eigen3 CONFIG)")
|
||||
if (USE_SYSTEM_EIGEN)
|
||||
set (EIGEN_SYSTEM_REPLACE "find_package(Eigen3 CONFIG)")
|
||||
endif()
|
||||
|
||||
find_package(LIBSSH 0.7.1)
|
||||
@@ -247,8 +279,11 @@ if (WITH_TESTS)
|
||||
endif()
|
||||
|
||||
add_subdirectory(wpiutil)
|
||||
add_subdirectory(wpinet)
|
||||
add_subdirectory(ntcore)
|
||||
|
||||
if (WITH_NTCORE)
|
||||
add_subdirectory(wpinet)
|
||||
add_subdirectory(ntcore)
|
||||
endif()
|
||||
|
||||
if (WITH_WPIMATH)
|
||||
add_subdirectory(wpimath)
|
||||
@@ -280,6 +315,7 @@ endif()
|
||||
|
||||
if (WITH_WPILIB)
|
||||
set(WPILIBC_DEP_REPLACE ${WPILIBC_DEP_REPLACE_IMPL})
|
||||
add_subdirectory(apriltag)
|
||||
add_subdirectory(wpilibj)
|
||||
add_subdirectory(wpilibc)
|
||||
add_subdirectory(wpilibNewCommands)
|
||||
|
||||
@@ -37,12 +37,42 @@ So you want to contribute your changes back to WPILib. Great! We have a few cont
|
||||
|
||||
## Coding Guidelines
|
||||
|
||||
WPILib uses modified Google style guides for both C++ and Java, which can be found in the [styleguide repository](https://github.com/wpilibsuite/styleguide). Autoformatters are available for many popular editors at https://github.com/google/styleguide. Running wpiformat is required for all contributions and is enforced by our continuous integration system. We currently use clang-format 14.0 with wpiformat.
|
||||
|
||||
WPILib uses modified Google style guides for both C++ and Java, which can be found in the [styleguide repository](https://github.com/wpilibsuite/styleguide). Autoformatters are available for many popular editors at https://github.com/google/styleguide. Running wpiformat is required for all contributions and is enforced by our continuous integration system.
|
||||
While the library should be fully formatted according to the styles, additional elements of the style guide were not followed when the library was initially created. All new code should follow the guidelines. If you are looking for some easy ramp-up tasks, finding areas that don't follow the style guide and fixing them is very welcome.
|
||||
|
||||
### Math documentation
|
||||
|
||||
When writing math expressions in documentation, use https://www.unicodeit.net/ to convert LaTeX to a Unicode equivalent that's easier to read. Not all expressions will translate (e.g., superscripts of superscripts) so focus on making it readable by someone who isn't familiar with LaTeX. If content on multiple lines needs to be aligned in Doxygen/Javadoc comments (e.g., integration/summation limits, matrices packed with square brackets and superscripts for them), put them in @verbatim/@endverbatim blocks in Doxygen or `<pre>` tags in Javadoc so they render with monospace font.
|
||||
|
||||
The LaTeX to Unicode conversions can also be done locally via the unicodeit Python package. To install it, execute:
|
||||
```bash
|
||||
pip install --user unicodeit
|
||||
```
|
||||
|
||||
Here's example usage:
|
||||
```bash
|
||||
$ python -m unicodeit.cli 'x_{k+1} = Ax_k + Bu_k'
|
||||
xₖ₊₁ = Axₖ + Buₖ
|
||||
```
|
||||
|
||||
On Linux, this process can be streamlined further by adding the following Bash function to your .bashrc (requires `wl-clipboard` on Wayland or `xclip` on X11):
|
||||
```bash
|
||||
# Converts LaTeX to Unicode, prints the result, and copies it to the clipboard
|
||||
uc() {
|
||||
if [ $WAYLAND_DISPLAY ]; then
|
||||
python -m unicodeit.cli $@ | tee >(wl-copy -n)
|
||||
else
|
||||
python -m unicodeit.cli $@ | tee >(xclip -sel)
|
||||
fi
|
||||
}
|
||||
```
|
||||
|
||||
Here's example usage:
|
||||
```bash
|
||||
$ uc 'x_{k+1} = Ax_k + Bu_k'
|
||||
xₖ₊₁ = Axₖ + Buₖ
|
||||
```
|
||||
|
||||
## Submitting Changes
|
||||
|
||||
### Pull Request Format
|
||||
|
||||
@@ -8,7 +8,7 @@ This article contains instructions on building projects using a development buil
|
||||
|
||||
Development builds are the per-commit build hosted every time a commit is pushed to the [allwpilib](https://github.com/wpilibsuite/allwpilib/) repository. These builds are then hosted on [artifactory](https://frcmaven.wpi.edu/artifactory/webapp/#/home).
|
||||
|
||||
In order to build a project using a development build, find the build.gradle file and open it. Then, add the following code below the plugin section and replace YEAR with the year of the development version. It is also necessary to use a 2023 GradleRIO version, ie `2023.0.0-alpha-1`
|
||||
To build a project using a development build, find the build.gradle file and open it. Then, add the following code below the plugin section and replace YEAR with the year of the development version. It is also necessary to use a 2023 GradleRIO version, ie `2023.0.0-alpha-1`
|
||||
|
||||
```groovy
|
||||
wpi.maven.useLocal = false
|
||||
@@ -46,6 +46,11 @@ wpi.versions.wpilibVersion = '2023.+'
|
||||
wpi.versions.wpimathVersion = '2023.+'
|
||||
```
|
||||
|
||||
### Development Build Documentation
|
||||
|
||||
* C++: https://github.wpilib.org/allwpilib/docs/development/cpp/
|
||||
* Java: https://github.wpilib.org/allwpilib/docs/development/java/
|
||||
|
||||
## Local Build
|
||||
|
||||
Building with a local build is very similar to building with a development build. Ensure you have built and published WPILib by following the instructions attached [here](https://github.com/wpilibsuite/allwpilib#building-wpilib). Next, find the ``build.gradle`` file in your robot project and open it. Then, add the following code below the plugin section and replace ``YEAR`` with the year of the local version.
|
||||
@@ -90,4 +95,4 @@ The following 3 tasks can be used for deployment:
|
||||
|
||||
Deploying any of these to the roboRIO will disable the current startup project until it is redeployed.
|
||||
|
||||
From here, ssh into the roboRIO using the `admin` account (`lvuser` will fail to run in many cases). In the admin home directory, a file for each deploy type will exist (`myRobotCpp`, `myRobotCppStatic` and `myRobotJavaRun`). These can be run to start up the corresponding project.
|
||||
From here, ssh into the roboRIO using the `lvuser` account and run `frcRunRobot.sh` (It's in path).
|
||||
@@ -72,12 +72,16 @@ All artifacts are based at `edu.wpi.first.artifactname` in the repository.
|
||||
* wpigui
|
||||
* imgui
|
||||
|
||||
* ntcore
|
||||
* wpiutil
|
||||
|
||||
* wpimath
|
||||
* wpiutil
|
||||
|
||||
* wpinet
|
||||
* wpiutil
|
||||
|
||||
* ntcore
|
||||
* wpiutil
|
||||
* wpinet
|
||||
|
||||
* glass/libglass
|
||||
* wpiutil
|
||||
* wpimath
|
||||
@@ -85,6 +89,7 @@ All artifacts are based at `edu.wpi.first.artifactname` in the repository.
|
||||
|
||||
* glass/libglassnt
|
||||
* wpiutil
|
||||
* wpinet
|
||||
* ntcore
|
||||
* wpimath
|
||||
* wpigui
|
||||
@@ -94,6 +99,7 @@ All artifacts are based at `edu.wpi.first.artifactname` in the repository.
|
||||
|
||||
* halsim
|
||||
* wpiutil
|
||||
* wpinet
|
||||
* ntcore
|
||||
* wpimath
|
||||
* wpigui
|
||||
@@ -102,12 +108,14 @@ All artifacts are based at `edu.wpi.first.artifactname` in the repository.
|
||||
|
||||
* cscore
|
||||
* opencv
|
||||
* wpinet
|
||||
* wpiutil
|
||||
|
||||
* cameraserver
|
||||
* ntcore
|
||||
* cscore
|
||||
* opencv
|
||||
* wpinet
|
||||
* wpiutil
|
||||
|
||||
* wpilibj
|
||||
@@ -115,6 +123,7 @@ All artifacts are based at `edu.wpi.first.artifactname` in the repository.
|
||||
* cameraserver
|
||||
* ntcore
|
||||
* cscore
|
||||
* wpinet
|
||||
* wpiutil
|
||||
|
||||
* wpilibc
|
||||
@@ -123,6 +132,7 @@ All artifacts are based at `edu.wpi.first.artifactname` in the repository.
|
||||
* ntcore
|
||||
* cscore
|
||||
* wpimath
|
||||
* wpinet
|
||||
* wpiutil
|
||||
|
||||
* wpilibNewCommands
|
||||
@@ -132,6 +142,7 @@ All artifacts are based at `edu.wpi.first.artifactname` in the repository.
|
||||
* ntcore
|
||||
* cscore
|
||||
* wpimath
|
||||
* wpinet
|
||||
* wpiutil
|
||||
|
||||
|
||||
|
||||
@@ -31,14 +31,20 @@ The following build options are available:
|
||||
* This option will enable Java and JNI builds. If this is on, `WITH_SHARED_LIBS` must be on. Otherwise CMake will error.
|
||||
* `WITH_SHARED_LIBS` (ON Default)
|
||||
* This option will cause cmake to build static libraries instead of shared libraries. If this is off, `WITH_JAVA` must be off. Otherwise CMake will error.
|
||||
* `WITH_TESTS` (ON Default)
|
||||
* This option will build C++ unit tests. These can be run via `make test`.
|
||||
* `WITH_CSCORE` (ON Default)
|
||||
* This option will cause cscore to be built. Turning this off will implicitly disable cameraserver, the hal and wpilib as well, irrespective of their specific options. If this is off, the OpenCV build requirement is removed.
|
||||
* `WITH_NTCORE` (ON Default)
|
||||
* This option will cause ntcore to be built. Turning this off will implicitly disable wpinet and wpilib as well, irrespective of their specific options.
|
||||
* `WITH_WPIMATH` (ON Default)
|
||||
* This option will build the wpimath library. This option must be on to build wpilib.
|
||||
* `WITH_WPILIB` (ON Default)
|
||||
* This option will build the hal and wpilibc/j during the build. The HAL is the simulator hal, unless the external hal options are used. The cmake build has no capability to build for the RoboRIO.
|
||||
* `WITH_EXAMPLES` (ON Default)
|
||||
* This option will build C++ examples.
|
||||
* `WITH_TESTS` (ON Default)
|
||||
* This option will build C++ unit tests. These can be run via `make test`.
|
||||
* `WITH_GUI` (ON Default)
|
||||
* This option will build GUI items.
|
||||
* `WITH_SIMULATION_MODULES` (ON Default)
|
||||
* This option will build simulation modules, including wpigui and the HALSim plugins.
|
||||
* `WITH_EXTERNAL_HAL` (OFF Default)
|
||||
|
||||
48
README.md
48
README.md
@@ -1,8 +1,8 @@
|
||||
# WPILib Project
|
||||
|
||||

|
||||
[](https://first.wpi.edu/wpilib/allwpilib/docs/development/cpp/)
|
||||
[](https://first.wpi.edu/wpilib/allwpilib/docs/development/java/)
|
||||
[](https://github.com/wpilibsuite/allwpilib/actions/workflows/gradle.yml)
|
||||
[](https://github.wpilib.org/allwpilib/docs/development/cpp/)
|
||||
[](https://github.wpilib.org/allwpilib/docs/development/java/)
|
||||
|
||||
Welcome to the WPILib project. This repository contains the HAL, WPILibJ, and WPILibC projects. These are the core libraries for creating robot programs for the roboRIO.
|
||||
|
||||
@@ -15,7 +15,6 @@ Welcome to the WPILib project. This repository contains the HAL, WPILibJ, and WP
|
||||
- [Faster builds](#faster-builds)
|
||||
- [Using Development Builds](#using-development-builds)
|
||||
- [Custom toolchain location](#custom-toolchain-location)
|
||||
- [Gazebo simulation](#gazebo-simulation)
|
||||
- [Formatting/Linting](#formattinglinting)
|
||||
- [CMake](#cmake)
|
||||
- [Publishing](#publishing)
|
||||
@@ -33,7 +32,7 @@ Below is a list of instructions that guide you through cloning, building, publis
|
||||
1. Clone the repository with `git clone https://github.com/wpilibsuite/allwpilib.git`
|
||||
2. Build the repository with `./gradlew build` or `./gradlew build --build-cache` if you have an internet connection
|
||||
3. Publish the artifacts locally by running `./gradlew publish`
|
||||
4. [Update your](OtherVersions.md) `build.gradle` [to use the artifacts](OtherVersions.md)
|
||||
4. [Update your](DevelopmentBuilds.md) `build.gradle` [to use the artifacts](DevelopmentBuilds.md)
|
||||
|
||||
# Building WPILib
|
||||
|
||||
@@ -41,15 +40,15 @@ Using Gradle makes building WPILib very straightforward. It only has a few depen
|
||||
|
||||
## Requirements
|
||||
|
||||
- [JDK 11](https://adoptopenjdk.net/)
|
||||
- [JDK 11](https://adoptium.net/temurin/releases/?version=11)
|
||||
- Note that the JRE is insufficient; the full JDK is required
|
||||
- On Ubuntu, run `sudo apt install openjdk-11-jdk`
|
||||
- On Windows, install the JDK 11 .msi from the link above
|
||||
- On macOS, install the JDK 11 .pkg from the link above
|
||||
- C++ compiler
|
||||
- On Linux, install GCC 8 or greater
|
||||
- On Windows, install [Visual Studio Community 2022 or 2019](https://visualstudio.microsoft.com/vs/community/) and select the C++ programming language during installation (Gradle can't use the build tools for Visual Studio)
|
||||
- On macOS, install the Xcode command-line build tools via `xcode-select --install`
|
||||
- On Linux, install GCC 11 or greater
|
||||
- On Windows, install [Visual Studio Community 2022](https://visualstudio.microsoft.com/vs/community/) and select the C++ programming language during installation (Gradle can't use the build tools for Visual Studio)
|
||||
- On macOS, install the Xcode command-line build tools via `xcode-select --install`. Xcode 13 or later is required.
|
||||
- ARM compiler toolchain
|
||||
- Run `./gradlew installRoboRioToolchain` after cloning this repository
|
||||
- If the WPILib installer was used, this toolchain is already installed
|
||||
@@ -62,7 +61,7 @@ On macOS ARM, run `softwareupdate --install-rosetta`. This is necessary to be ab
|
||||
|
||||
Clone the WPILib repository and follow the instructions above for installing any required tooling.
|
||||
|
||||
See the [styleguide README](https://github.com/wpilibsuite/styleguide/blob/main/README.md) for wpiformat setup instructions.
|
||||
See the [styleguide README](https://github.com/wpilibsuite/styleguide/blob/main/README.md) for wpiformat setup instructions. We use clang-format 14.
|
||||
|
||||
## Building
|
||||
|
||||
@@ -104,7 +103,7 @@ Run with `--build-cache` on the command-line to use the shared [build cache](htt
|
||||
|
||||
### Using Development Builds
|
||||
|
||||
Please read the documentation available [here](OtherVersions.md)
|
||||
Please read the documentation available [here](DevelopmentBuilds.md)
|
||||
|
||||
### Custom toolchain location
|
||||
|
||||
@@ -114,30 +113,13 @@ If you have installed the FRC Toolchain to a directory other than the default, o
|
||||
./gradlew build -PtoolChainPath=some/path/to/frc/toolchain/bin
|
||||
```
|
||||
|
||||
### Gazebo simulation
|
||||
|
||||
If you also want to force building Gazebo simulation support, add -PforceGazebo. This requires gazebo_transport. We have tested on 14.04 and 15.05, but any correct install of Gazebo should work, even on Windows if you build Gazebo from source. Correct means CMake needs to be able to find gazebo-config.cmake. See [The Gazebo website](https://gazebosim.org/) for installation instructions.
|
||||
|
||||
```bash
|
||||
./gradlew build -PforceGazebo
|
||||
```
|
||||
|
||||
If you prefer to use CMake directly, the you can still do so.
|
||||
The common CMake tasks are wpilibcSim, frc_gazebo_plugins, and gz_msgs
|
||||
|
||||
```bash
|
||||
mkdir build #run this in the root of allwpilib
|
||||
cd build
|
||||
cmake ..
|
||||
make
|
||||
```
|
||||
|
||||
|
||||
### Formatting/linting
|
||||
|
||||
Once a PR has been submitted, formatting can be run in CI by commenting `/format` on the PR. A new commit will be pushed with the formatting changes.
|
||||
|
||||
#### wpiformat
|
||||
|
||||
wpiformat can be executed anywhere in the repository via `py -3 -m wpiformat` on Windows or `python3 -m wpiformat` on other platforms.
|
||||
wpiformat can be executed anywhere in the repository via `py -3 -m wpiformat -clang 14` on Windows or `python3 -m wpiformat -clang 14` on other platforms.
|
||||
|
||||
#### Java Code Quality Tools
|
||||
|
||||
@@ -162,9 +144,7 @@ The maven artifacts are described in [MavenArtifacts.md](MavenArtifacts.md)
|
||||
|
||||
## Structure and Organization
|
||||
|
||||
The main WPILib code you're probably looking for is in WPILibJ and WPILibC. Those directories are split into shared, sim, and athena. Athena contains the WPILib code meant to run on your roboRIO. Sim is WPILib code meant to run on your computer with Gazebo, and shared is code shared between the two. Shared code must be platform-independent, since it will be compiled with both the ARM cross-compiler and whatever desktop compiler you are using (g++, msvc, etc...).
|
||||
|
||||
The Simulation directory contains extra simulation tools and libraries, such as gz_msgs and JavaGazebo. See sub-directories for more information.
|
||||
The main WPILib code you're probably looking for is in WPILibJ and WPILibC. Those directories are split into shared, sim, and athena. Athena contains the WPILib code meant to run on your roboRIO. Sim is WPILib code meant to run on your computer, and shared is code shared between the two. Shared code must be platform-independent, since it will be compiled with both the ARM cross-compiler and whatever desktop compiler you are using (g++, msvc, etc...).
|
||||
|
||||
The integration test directories for C++ and Java contain test code that runs on our test-system. When you submit code for review, it is tested by those programs. If you add new functionality you should make sure to write tests for it so we don't break it in the future.
|
||||
|
||||
|
||||
@@ -34,8 +34,6 @@ popper.js wpinet/src/main/native/resources/popper-*
|
||||
units wpimath/src/main/native/include/units/
|
||||
Eigen wpimath/src/main/native/thirdparty/eigen/include/
|
||||
StackWalker wpiutil/src/main/native/windows/StackWalker.*
|
||||
TCB span wpiutil/src/main/native/thirdparty/include/wpi/span.h
|
||||
wpiutil/src/test/native/cpp/span/
|
||||
GHC filesystem wpiutil/src/main/native/thirdparty/include/wpi/ghc/
|
||||
Team 254 Library wpilibj/src/main/java/edu/wpi/first/wpilibj/spline/SplineParameterizer.java
|
||||
wpilibj/src/main/java/edu/wpi/first/wpilibj/trajectory/TrajectoryParameterizer.java
|
||||
@@ -47,6 +45,7 @@ Drake wpimath/src/main/native/thirdparty/drake/
|
||||
wpimath/src/test/native/cpp/drake/
|
||||
wpimath/src/test/native/include/drake/
|
||||
V8 export-template wpiutil/src/main/native/include/wpi/SymbolExports.h
|
||||
GCEM wpimath/src/main/native/thirdparty/gcem/include/
|
||||
|
||||
==============================================================================
|
||||
Google Test License
|
||||
@@ -1226,3 +1225,20 @@ DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
============
|
||||
GCEM License
|
||||
============
|
||||
Copyright 2022 - ktholer (https://github.com/kthohr/gcem)
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
||||
118
apriltag/CMakeLists.txt
Normal file
118
apriltag/CMakeLists.txt
Normal file
@@ -0,0 +1,118 @@
|
||||
project(apriltag)
|
||||
|
||||
include(CompileWarnings)
|
||||
include(GenResources)
|
||||
include(FetchContent)
|
||||
|
||||
FetchContent_Declare(
|
||||
apriltaglib
|
||||
GIT_REPOSITORY https://github.com/wpilibsuite/apriltag.git
|
||||
GIT_TAG ad31e33d20f9782b7239cb15cde57c56c91383ad
|
||||
)
|
||||
|
||||
# Don't use apriltag's CMakeLists.txt due to conflicting naming and JNI
|
||||
FetchContent_GetProperties(apriltaglib)
|
||||
if(NOT apriltaglib_POPULATED)
|
||||
FetchContent_Populate(apriltaglib)
|
||||
endif()
|
||||
|
||||
aux_source_directory(${apriltaglib_SOURCE_DIR}/common APRILTAGLIB_COMMON_SRC)
|
||||
file(GLOB TAG_FILES ${apriltaglib_SOURCE_DIR}/tag*.c)
|
||||
set(APRILTAGLIB_SRCS ${apriltaglib_SOURCE_DIR}/apriltag.c ${apriltaglib_SOURCE_DIR}/apriltag_pose.c ${apriltaglib_SOURCE_DIR}/apriltag_quad_thresh.c)
|
||||
|
||||
file(GLOB apriltag_jni_src src/main/native/cpp/jni/AprilTagJNI.cpp)
|
||||
|
||||
if (WITH_JAVA)
|
||||
find_package(Java REQUIRED)
|
||||
find_package(JNI REQUIRED)
|
||||
include(UseJava)
|
||||
set(CMAKE_JAVA_COMPILE_FLAGS "-encoding" "UTF8" "-Xlint:unchecked")
|
||||
|
||||
set(CMAKE_JNI_TARGET true)
|
||||
|
||||
file(GLOB EJML_JARS "${WPILIB_BINARY_DIR}/wpimath/thirdparty/ejml/*.jar")
|
||||
file(GLOB JACKSON_JARS "${WPILIB_BINARY_DIR}/wpiutil/thirdparty/jackson/*.jar")
|
||||
find_file(OPENCV_JAR_FILE
|
||||
NAMES opencv-${OpenCV_VERSION_MAJOR}${OpenCV_VERSION_MINOR}${OpenCV_VERSION_PATCH}.jar
|
||||
PATHS ${OPENCV_JAVA_INSTALL_DIR} ${OpenCV_INSTALL_PATH}/bin ${OpenCV_INSTALL_PATH}/share/java
|
||||
NO_DEFAULT_PATH)
|
||||
|
||||
set(CMAKE_JAVA_INCLUDE_PATH apriltag.jar ${EJML_JARS} ${JACKSON_JARS})
|
||||
|
||||
file(GLOB_RECURSE JAVA_SOURCES src/main/java/*.java)
|
||||
file(GLOB_RECURSE JAVA_RESOURCES RELATIVE ${CMAKE_CURRENT_SOURCE_DIR} src/main/native/resources/*.json)
|
||||
add_jar(apriltag_jar
|
||||
SOURCES ${JAVA_SOURCES}
|
||||
RESOURCES NAMESPACE "edu/wpi/first/apriltag" ${JAVA_RESOURCES}
|
||||
INCLUDE_JARS wpimath_jar ${EJML_JARS} wpiutil_jar ${OPENCV_JAR_FILE}
|
||||
OUTPUT_NAME apriltag
|
||||
GENERATE_NATIVE_HEADERS apriltag_jni_headers)
|
||||
|
||||
get_property(APRILTAG_JAR_FILE TARGET apriltag_jar PROPERTY JAR_FILE)
|
||||
install(FILES ${APRILTAG_JAR_FILE} DESTINATION "${java_lib_dest}")
|
||||
|
||||
set_property(TARGET apriltag_jar PROPERTY FOLDER "java")
|
||||
|
||||
add_library(apriltagjni ${apriltag_jni_src})
|
||||
wpilib_target_warnings(apriltagjni)
|
||||
target_link_libraries(apriltagjni PUBLIC apriltag)
|
||||
|
||||
set_property(TARGET apriltagjni PROPERTY FOLDER "libraries")
|
||||
|
||||
target_link_libraries(apriltagjni PRIVATE apriltag_jni_headers)
|
||||
add_dependencies(apriltagjni apriltag_jar)
|
||||
|
||||
if (MSVC)
|
||||
install(TARGETS apriltagjni RUNTIME DESTINATION "${jni_lib_dest}" COMPONENT Runtime)
|
||||
endif()
|
||||
|
||||
install(TARGETS apriltagjni EXPORT apriltagjni DESTINATION "${main_lib_dest}")
|
||||
|
||||
endif()
|
||||
|
||||
GENERATE_RESOURCES(src/main/native/resources/edu/wpi/first/apriltag generated/main/cpp APRILTAG frc apriltag_resources_src)
|
||||
|
||||
file(GLOB apriltag_native_src src/main/native/cpp/*.cpp)
|
||||
|
||||
add_library(apriltag ${apriltag_native_src} ${apriltag_resources_src} ${APRILTAGLIB_SRCS} ${APRILTAGLIB_COMMON_SRC} ${TAG_FILES})
|
||||
set_target_properties(apriltag PROPERTIES DEBUG_POSTFIX "d")
|
||||
|
||||
set_property(TARGET apriltag PROPERTY FOLDER "libraries")
|
||||
target_compile_features(apriltag PUBLIC cxx_std_20)
|
||||
wpilib_target_warnings(apriltag)
|
||||
# disable warnings that apriltaglib can't handle
|
||||
if (MSVC)
|
||||
target_compile_options(apriltag PRIVATE /wd4018)
|
||||
else()
|
||||
target_compile_options(apriltag PRIVATE -Wno-sign-compare -Wno-gnu-zero-variadic-macro-arguments)
|
||||
endif()
|
||||
|
||||
target_link_libraries(apriltag wpimath)
|
||||
|
||||
target_include_directories(apriltag PUBLIC
|
||||
$<BUILD_INTERFACE:${apriltaglib_SOURCE_DIR}>
|
||||
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/src/main/native/include>
|
||||
$<INSTALL_INTERFACE:${include_dest}/apriltag>)
|
||||
|
||||
install(TARGETS apriltag EXPORT apriltag DESTINATION "${main_lib_dest}")
|
||||
install(DIRECTORY src/main/native/include/ DESTINATION "${include_dest}/apriltag")
|
||||
|
||||
if (WITH_JAVA AND MSVC)
|
||||
install(TARGETS apriltag RUNTIME DESTINATION "${jni_lib_dest}" COMPONENT Runtime)
|
||||
endif()
|
||||
|
||||
if (WITH_FLAT_INSTALL)
|
||||
set (apriltag_config_dir ${wpilib_dest})
|
||||
else()
|
||||
set (apriltag_config_dir share/apriltag)
|
||||
endif()
|
||||
|
||||
configure_file(apriltag-config.cmake.in ${WPILIB_BINARY_DIR}/apriltag-config.cmake )
|
||||
install(FILES ${WPILIB_BINARY_DIR}/apriltag-config.cmake DESTINATION ${apriltag_config_dir})
|
||||
install(EXPORT apriltag DESTINATION ${apriltag_config_dir})
|
||||
|
||||
if (WITH_TESTS)
|
||||
wpilib_add_test(apriltag src/test/native/cpp)
|
||||
target_include_directories(apriltag_test PRIVATE src/test/native/include)
|
||||
target_link_libraries(apriltag_test apriltag gmock_main)
|
||||
endif()
|
||||
7
apriltag/apriltag-config.cmake.in
Normal file
7
apriltag/apriltag-config.cmake.in
Normal file
@@ -0,0 +1,7 @@
|
||||
include(CMakeFindDependencyMacro)
|
||||
@FILENAME_DEP_REPLACE@
|
||||
@WPIMATH_DEP_REPLACE@
|
||||
@WPIUTIL_DEP_REPLACE@
|
||||
|
||||
@FILENAME_DEP_REPLACE@
|
||||
include(${SELF_DIR}/apriltag.cmake)
|
||||
88
apriltag/build.gradle
Normal file
88
apriltag/build.gradle
Normal file
@@ -0,0 +1,88 @@
|
||||
apply from: "${rootDir}/shared/resources.gradle"
|
||||
|
||||
ext {
|
||||
nativeName = 'apriltag'
|
||||
devMain = 'edu.wpi.first.apriltag.DevMain'
|
||||
useJava = true
|
||||
useCpp = true
|
||||
sharedCvConfigs = [
|
||||
apriltagDev : [],
|
||||
apriltagTest: []]
|
||||
staticCvConfigs = []
|
||||
|
||||
def generateTask = createGenerateResourcesTask('main', 'APRILTAG', 'frc', project)
|
||||
|
||||
tasks.withType(CppCompile) {
|
||||
dependsOn generateTask
|
||||
}
|
||||
splitSetup = {
|
||||
it.sources {
|
||||
resourcesCpp(CppSourceSet) {
|
||||
source {
|
||||
srcDirs "$buildDir/generated/main/cpp", "$rootDir/shared/singlelib"
|
||||
include '*.cpp'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
evaluationDependsOn(':wpimath')
|
||||
|
||||
apply from: "${rootDir}/shared/jni/setupBuild.gradle"
|
||||
apply from: "${rootDir}/shared/apriltaglib.gradle"
|
||||
apply from: "${rootDir}/shared/opencv.gradle"
|
||||
|
||||
dependencies {
|
||||
implementation project(':wpimath')
|
||||
devImplementation project(':wpimath')
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
main {
|
||||
resources {
|
||||
srcDirs 'src/main/native/resources'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
model {
|
||||
components {}
|
||||
binaries {
|
||||
all {
|
||||
if (!it.buildable || !(it instanceof NativeBinarySpec)) {
|
||||
return
|
||||
}
|
||||
it.cppCompiler.define 'WPILIB_EXPORTS'
|
||||
|
||||
if (it.component.name == "${nativeName}JNI") {
|
||||
lib project: ':wpimath', library: 'wpimath', linkage: 'static'
|
||||
lib project: ':wpiutil', library: 'wpiutil', linkage: 'static'
|
||||
} else {
|
||||
lib project: ':wpimath', library: 'wpimath', linkage: 'shared'
|
||||
lib project: ':wpiutil', library: 'wpiutil', linkage: 'shared'
|
||||
}
|
||||
|
||||
nativeUtils.useRequiredLibrary(it, 'apriltaglib')
|
||||
}
|
||||
}
|
||||
tasks {
|
||||
def c = $.components
|
||||
def found = false
|
||||
def systemArch = getCurrentArch()
|
||||
c.each {
|
||||
if (it in NativeExecutableSpec && it.name == "${nativeName}Dev") {
|
||||
it.binaries.each {
|
||||
if (!found) {
|
||||
def arch = it.targetPlatform.name
|
||||
if (arch == systemArch) {
|
||||
def filePath = it.tasks.install.installDirectory.get().toString() + File.separatorChar + 'lib'
|
||||
|
||||
found = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
20
apriltag/src/dev/java/edu/wpi/first/apriltag/DevMain.java
Normal file
20
apriltag/src/dev/java/edu/wpi/first/apriltag/DevMain.java
Normal file
@@ -0,0 +1,20 @@
|
||||
// Copyright (c) FIRST and other WPILib contributors.
|
||||
// Open Source Software; you can modify and/or share it under the terms of
|
||||
// the WPILib BSD license file in the root directory of this project.
|
||||
|
||||
package edu.wpi.first.apriltag;
|
||||
|
||||
public final class DevMain {
|
||||
/** Main entry point. */
|
||||
public static void main(String[] args) {
|
||||
System.out.println("Hello World!");
|
||||
AprilTagDetector detector = new AprilTagDetector();
|
||||
detector.addFamily("tag16h5");
|
||||
AprilTagDetector.Config config = new AprilTagDetector.Config();
|
||||
config.refineEdges = false;
|
||||
detector.setConfig(config);
|
||||
detector.close();
|
||||
}
|
||||
|
||||
private DevMain() {}
|
||||
}
|
||||
@@ -2,12 +2,10 @@
|
||||
// Open Source Software; you can modify and/or share it under the terms of
|
||||
// the WPILib BSD license file in the root directory of this project.
|
||||
|
||||
#include "networktables/RpcCall.h"
|
||||
#include "frc/apriltag/AprilTagDetector.h"
|
||||
|
||||
#include "networktables/NetworkTableEntry.h"
|
||||
|
||||
using namespace nt;
|
||||
|
||||
NetworkTableEntry RpcCall::GetEntry() const {
|
||||
return NetworkTableEntry{m_entry};
|
||||
int main() {
|
||||
frc::AprilTagDetector detector;
|
||||
detector.AddFamily("tag16h5");
|
||||
detector.SetConfig({.refineEdges = false});
|
||||
}
|
||||
47
apriltag/src/main/java/edu/wpi/first/apriltag/AprilTag.java
Normal file
47
apriltag/src/main/java/edu/wpi/first/apriltag/AprilTag.java
Normal file
@@ -0,0 +1,47 @@
|
||||
// Copyright (c) FIRST and other WPILib contributors.
|
||||
// Open Source Software; you can modify and/or share it under the terms of
|
||||
// the WPILib BSD license file in the root directory of this project.
|
||||
|
||||
package edu.wpi.first.apriltag;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonCreator;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import edu.wpi.first.math.geometry.Pose3d;
|
||||
import java.util.Objects;
|
||||
|
||||
@SuppressWarnings("MemberName")
|
||||
public class AprilTag {
|
||||
@JsonProperty(value = "ID")
|
||||
public int ID;
|
||||
|
||||
@JsonProperty(value = "pose")
|
||||
public Pose3d pose;
|
||||
|
||||
@SuppressWarnings("ParameterName")
|
||||
@JsonCreator
|
||||
public AprilTag(
|
||||
@JsonProperty(required = true, value = "ID") int ID,
|
||||
@JsonProperty(required = true, value = "pose") Pose3d pose) {
|
||||
this.ID = ID;
|
||||
this.pose = pose;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (obj instanceof AprilTag) {
|
||||
var other = (AprilTag) obj;
|
||||
return ID == other.ID && pose.equals(other.pose);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(ID, pose);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "AprilTag(ID: " + ID + ", pose: " + pose + ")";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,190 @@
|
||||
// Copyright (c) FIRST and other WPILib contributors.
|
||||
// Open Source Software; you can modify and/or share it under the terms of
|
||||
// the WPILib BSD license file in the root directory of this project.
|
||||
|
||||
package edu.wpi.first.apriltag;
|
||||
|
||||
import edu.wpi.first.math.MatBuilder;
|
||||
import edu.wpi.first.math.Matrix;
|
||||
import edu.wpi.first.math.Nat;
|
||||
import edu.wpi.first.math.numbers.N3;
|
||||
import java.util.Arrays;
|
||||
|
||||
/** A detection of an AprilTag tag. */
|
||||
public class AprilTagDetection {
|
||||
/**
|
||||
* Gets the decoded tag's family name.
|
||||
*
|
||||
* @return Decoded family name
|
||||
*/
|
||||
public String getFamily() {
|
||||
return m_family;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the decoded ID of the tag.
|
||||
*
|
||||
* @return Decoded ID
|
||||
*/
|
||||
public int getId() {
|
||||
return m_id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets how many error bits were corrected. Note: accepting large numbers of corrected errors
|
||||
* leads to greatly increased false positive rates. NOTE: As of this implementation, the detector
|
||||
* cannot detect tags with a hamming distance greater than 2.
|
||||
*
|
||||
* @return Hamming distance (number of corrected error bits)
|
||||
*/
|
||||
public int getHamming() {
|
||||
return m_hamming;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a measure of the quality of the binary decoding process: the average difference between
|
||||
* the intensity of a data bit versus the decision threshold. Higher numbers roughly indicate
|
||||
* better decodes. This is a reasonable measure of detection accuracy only for very small tags--
|
||||
* not effective for larger tags (where we could have sampled anywhere within a bit cell and still
|
||||
* gotten a good detection.)
|
||||
*
|
||||
* @return Decision margin
|
||||
*/
|
||||
public float getDecisionMargin() {
|
||||
return m_decisionMargin;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the 3x3 homography matrix describing the projection from an "ideal" tag (with corners at
|
||||
* (-1,1), (1,1), (1,-1), and (-1, -1)) to pixels in the image.
|
||||
*
|
||||
* @return Homography matrix data
|
||||
*/
|
||||
@SuppressWarnings("PMD.MethodReturnsInternalArray")
|
||||
public double[] getHomography() {
|
||||
return m_homography;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the 3x3 homography matrix describing the projection from an "ideal" tag (with corners at
|
||||
* (-1,1), (1,1), (1,-1), and (-1, -1)) to pixels in the image.
|
||||
*
|
||||
* @return Homography matrix
|
||||
*/
|
||||
public Matrix<N3, N3> getHomographyMatrix() {
|
||||
return new MatBuilder<>(Nat.N3(), Nat.N3()).fill(m_homography);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the center of the detection in image pixel coordinates.
|
||||
*
|
||||
* @return Center point X coordinate
|
||||
*/
|
||||
public double getCenterX() {
|
||||
return m_centerX;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the center of the detection in image pixel coordinates.
|
||||
*
|
||||
* @return Center point Y coordinate
|
||||
*/
|
||||
public double getCenterY() {
|
||||
return m_centerY;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a corner of the tag in image pixel coordinates. These always wrap counter-clock wise
|
||||
* around the tag.
|
||||
*
|
||||
* @param ndx Corner index (range is 0-3, inclusive)
|
||||
* @return Corner point X coordinate
|
||||
*/
|
||||
public double getCornerX(int ndx) {
|
||||
return m_corners[ndx * 2];
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a corner of the tag in image pixel coordinates. These always wrap counter-clock wise
|
||||
* around the tag.
|
||||
*
|
||||
* @param ndx Corner index (range is 0-3, inclusive)
|
||||
* @return Corner point Y coordinate
|
||||
*/
|
||||
public double getCornerY(int ndx) {
|
||||
return m_corners[ndx * 2 + 1];
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the corners of the tag in image pixel coordinates. These always wrap counter-clock wise
|
||||
* around the tag.
|
||||
*
|
||||
* @return Corner point array (X and Y for each corner in order)
|
||||
*/
|
||||
@SuppressWarnings("PMD.MethodReturnsInternalArray")
|
||||
public double[] getCorners() {
|
||||
return m_corners;
|
||||
}
|
||||
|
||||
private final String m_family;
|
||||
private final int m_id;
|
||||
private final int m_hamming;
|
||||
private final float m_decisionMargin;
|
||||
private final double[] m_homography;
|
||||
private final double m_centerX;
|
||||
private final double m_centerY;
|
||||
private final double[] m_corners;
|
||||
|
||||
/**
|
||||
* Constructs a new detection result. Used from JNI.
|
||||
*
|
||||
* @param family family
|
||||
* @param id id
|
||||
* @param hamming hamming
|
||||
* @param decisionMargin dm
|
||||
* @param homography homography
|
||||
* @param centerX centerX
|
||||
* @param centerY centerY
|
||||
* @param corners corners
|
||||
*/
|
||||
@SuppressWarnings("PMD.ArrayIsStoredDirectly")
|
||||
public AprilTagDetection(
|
||||
String family,
|
||||
int id,
|
||||
int hamming,
|
||||
float decisionMargin,
|
||||
double[] homography,
|
||||
double centerX,
|
||||
double centerY,
|
||||
double[] corners) {
|
||||
m_family = family;
|
||||
m_id = id;
|
||||
m_hamming = hamming;
|
||||
m_decisionMargin = decisionMargin;
|
||||
m_homography = homography;
|
||||
m_centerX = centerX;
|
||||
m_centerY = centerY;
|
||||
m_corners = corners;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "DetectionResult [centerX="
|
||||
+ m_centerX
|
||||
+ ", centerY="
|
||||
+ m_centerY
|
||||
+ ", corners="
|
||||
+ Arrays.toString(m_corners)
|
||||
+ ", decisionMargin="
|
||||
+ m_decisionMargin
|
||||
+ ", hamming="
|
||||
+ m_hamming
|
||||
+ ", homography="
|
||||
+ Arrays.toString(m_homography)
|
||||
+ ", family="
|
||||
+ m_family
|
||||
+ ", id="
|
||||
+ m_id
|
||||
+ "]";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,281 @@
|
||||
// Copyright (c) FIRST and other WPILib contributors.
|
||||
// Open Source Software; you can modify and/or share it under the terms of
|
||||
// the WPILib BSD license file in the root directory of this project.
|
||||
|
||||
package edu.wpi.first.apriltag;
|
||||
|
||||
import edu.wpi.first.apriltag.jni.AprilTagJNI;
|
||||
import org.opencv.core.Mat;
|
||||
|
||||
/**
|
||||
* An AprilTag detector engine. This is expensive to set up and tear down, so most use cases should
|
||||
* only create one of these, add a family to it, set up any other configuration, and repeatedly call
|
||||
* Detect().
|
||||
*/
|
||||
public class AprilTagDetector implements AutoCloseable {
|
||||
/** Detector configuration. */
|
||||
@SuppressWarnings("MemberName")
|
||||
public static class Config {
|
||||
/**
|
||||
* How many threads should be used for computation. Default is single-threaded operation (1
|
||||
* thread).
|
||||
*/
|
||||
public int numThreads = 1;
|
||||
|
||||
/**
|
||||
* Quad decimation. Detection of quads can be done on a lower-resolution image, improving speed
|
||||
* at a cost of pose accuracy and a slight decrease in detection rate. Decoding the binary
|
||||
* payload is still done at full resolution. Default is 2.0.
|
||||
*/
|
||||
public float quadDecimate = 2.0f;
|
||||
|
||||
/**
|
||||
* What Gaussian blur should be applied to the segmented image (used for quad detection). Very
|
||||
* noisy images benefit from non-zero values (e.g. 0.8). Default is 0.0.
|
||||
*/
|
||||
public float quadSigma;
|
||||
|
||||
/**
|
||||
* When true, the edges of the each quad are adjusted to "snap to" strong gradients nearby. This
|
||||
* is useful when decimation is employed, as it can increase the quality of the initial quad
|
||||
* estimate substantially. Generally recommended to be on (true). Default is true.
|
||||
*
|
||||
* <p>Very computationally inexpensive. Option is ignored if quad_decimate = 1.
|
||||
*/
|
||||
public boolean refineEdges = true;
|
||||
|
||||
/**
|
||||
* How much sharpening should be done to decoded images. This can help decode small tags but may
|
||||
* or may not help in odd lighting conditions or low light conditions. Default is 0.25.
|
||||
*/
|
||||
public double decodeSharpening = 0.25;
|
||||
|
||||
/**
|
||||
* Debug mode. When true, the decoder writes a variety of debugging images to the current
|
||||
* working directory at various stages through the detection process. This is slow and should
|
||||
* *not* be used on space-limited systems such as the RoboRIO. Default is disabled (false).
|
||||
*/
|
||||
public boolean debug;
|
||||
|
||||
public Config() {}
|
||||
|
||||
Config(
|
||||
int numThreads,
|
||||
float quadDecimate,
|
||||
float quadSigma,
|
||||
boolean refineEdges,
|
||||
double decodeSharpening,
|
||||
boolean debug) {
|
||||
this.numThreads = numThreads;
|
||||
this.quadDecimate = quadDecimate;
|
||||
this.quadSigma = quadSigma;
|
||||
this.refineEdges = refineEdges;
|
||||
this.decodeSharpening = decodeSharpening;
|
||||
this.debug = debug;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return numThreads
|
||||
+ Float.hashCode(quadDecimate)
|
||||
+ Float.hashCode(quadSigma)
|
||||
+ Boolean.hashCode(refineEdges)
|
||||
+ Double.hashCode(decodeSharpening)
|
||||
+ Boolean.hashCode(debug);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (!(obj instanceof Config)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
Config other = (Config) obj;
|
||||
return numThreads == other.numThreads
|
||||
&& quadDecimate == other.quadDecimate
|
||||
&& quadSigma == other.quadSigma
|
||||
&& refineEdges == other.refineEdges
|
||||
&& decodeSharpening == other.decodeSharpening
|
||||
&& debug == other.debug;
|
||||
}
|
||||
}
|
||||
|
||||
/** Quad threshold parameters. */
|
||||
@SuppressWarnings("MemberName")
|
||||
public static class QuadThresholdParameters {
|
||||
/** Threshold used to reject quads containing too few pixels. Default is 5 pixels. */
|
||||
public int minClusterPixels = 5;
|
||||
|
||||
/**
|
||||
* How many corner candidates to consider when segmenting a group of pixels into a quad. Default
|
||||
* is 10.
|
||||
*/
|
||||
public int maxNumMaxima = 10;
|
||||
|
||||
/**
|
||||
* Critical angle, in radians. The detector will reject quads where pairs of edges have angles
|
||||
* that are close to straight or close to 180 degrees. Zero means that no quads are rejected.
|
||||
* Default is 10 degrees.
|
||||
*/
|
||||
public double criticalAngle = 10 * Math.PI / 180.0;
|
||||
|
||||
/**
|
||||
* When fitting lines to the contours, the maximum mean squared error allowed. This is useful in
|
||||
* rejecting contours that are far from being quad shaped; rejecting these quads "early" saves
|
||||
* expensive decoding processing. Default is 10.0.
|
||||
*/
|
||||
public float maxLineFitMSE = 10.0f;
|
||||
|
||||
/**
|
||||
* Minimum brightness offset. When we build our model of black & white pixels, we add an
|
||||
* extra check that the white model must be (overall) brighter than the black model. How much
|
||||
* brighter? (in pixel values, [0,255]). Default is 5.
|
||||
*/
|
||||
public int minWhiteBlackDiff = 5;
|
||||
|
||||
/**
|
||||
* Whether the thresholded image be should be deglitched. Only useful for very noisy images.
|
||||
* Default is disabled (false).
|
||||
*/
|
||||
public boolean deglitch;
|
||||
|
||||
public QuadThresholdParameters() {}
|
||||
|
||||
QuadThresholdParameters(
|
||||
int minClusterPixels,
|
||||
int maxNumMaxima,
|
||||
double criticalAngle,
|
||||
float maxLineFitMSE,
|
||||
int minWhiteBlackDiff,
|
||||
boolean deglitch) {
|
||||
this.minClusterPixels = minClusterPixels;
|
||||
this.maxNumMaxima = maxNumMaxima;
|
||||
this.criticalAngle = criticalAngle;
|
||||
this.maxLineFitMSE = maxLineFitMSE;
|
||||
this.minWhiteBlackDiff = minWhiteBlackDiff;
|
||||
this.deglitch = deglitch;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return minClusterPixels
|
||||
+ maxNumMaxima
|
||||
+ Double.hashCode(criticalAngle)
|
||||
+ Float.hashCode(maxLineFitMSE)
|
||||
+ minWhiteBlackDiff
|
||||
+ Boolean.hashCode(deglitch);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (!(obj instanceof QuadThresholdParameters)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
QuadThresholdParameters other = (QuadThresholdParameters) obj;
|
||||
return minClusterPixels == other.minClusterPixels
|
||||
&& maxNumMaxima == other.maxNumMaxima
|
||||
&& criticalAngle == other.criticalAngle
|
||||
&& maxLineFitMSE == other.maxLineFitMSE
|
||||
&& minWhiteBlackDiff == other.minWhiteBlackDiff
|
||||
&& deglitch == other.deglitch;
|
||||
}
|
||||
}
|
||||
|
||||
public AprilTagDetector() {
|
||||
m_native = AprilTagJNI.createDetector();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
if (m_native != 0) {
|
||||
AprilTagJNI.destroyDetector(m_native);
|
||||
}
|
||||
m_native = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets detector configuration.
|
||||
*
|
||||
* @param config Configuration
|
||||
*/
|
||||
public void setConfig(Config config) {
|
||||
AprilTagJNI.setDetectorConfig(m_native, config);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets detector configuration.
|
||||
*
|
||||
* @return Configuration
|
||||
*/
|
||||
public Config getConfig() {
|
||||
return AprilTagJNI.getDetectorConfig(m_native);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets quad threshold parameters.
|
||||
*
|
||||
* @param params Parameters
|
||||
*/
|
||||
public void setQuadThresholdParameters(QuadThresholdParameters params) {
|
||||
AprilTagJNI.setDetectorQTP(m_native, params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets quad threshold parameters.
|
||||
*
|
||||
* @return Parameters
|
||||
*/
|
||||
public QuadThresholdParameters getQuadThresholdParameters() {
|
||||
return AprilTagJNI.getDetectorQTP(m_native);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a family of tags to be detected.
|
||||
*
|
||||
* @param fam Family name, e.g. "tag16h5"
|
||||
* @throws IllegalArgumentException if family name not recognized
|
||||
*/
|
||||
public void addFamily(String fam) {
|
||||
addFamily(fam, 2);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a family of tags to be detected.
|
||||
*
|
||||
* @param fam Family name, e.g. "tag16h5"
|
||||
* @param bitsCorrected maximum number of bits to correct
|
||||
* @throws IllegalArgumentException if family name not recognized
|
||||
*/
|
||||
public void addFamily(String fam, int bitsCorrected) {
|
||||
if (!AprilTagJNI.addFamily(m_native, fam, bitsCorrected)) {
|
||||
throw new IllegalArgumentException("unknown family name '" + fam + "'");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a family of tags from the detector.
|
||||
*
|
||||
* @param fam Family name, e.g. "tag16h5"
|
||||
*/
|
||||
public void removeFamily(String fam) {
|
||||
AprilTagJNI.removeFamily(m_native, fam);
|
||||
}
|
||||
|
||||
/** Unregister all families. */
|
||||
public void clearFamilies() {
|
||||
AprilTagJNI.clearFamilies(m_native);
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect tags from an 8-bit image.
|
||||
*
|
||||
* @param img 8-bit OpenCV Mat image
|
||||
* @return Results (array of AprilTagDetection)
|
||||
*/
|
||||
public AprilTagDetection[] detect(Mat img) {
|
||||
return AprilTagJNI.detect(m_native, img.cols(), img.rows(), img.cols(), img.dataAddr());
|
||||
}
|
||||
|
||||
private long m_native;
|
||||
}
|
||||
@@ -0,0 +1,241 @@
|
||||
// Copyright (c) FIRST and other WPILib contributors.
|
||||
// Open Source Software; you can modify and/or share it under the terms of
|
||||
// the WPILib BSD license file in the root directory of this project.
|
||||
|
||||
package edu.wpi.first.apriltag;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonAutoDetect;
|
||||
import com.fasterxml.jackson.annotation.JsonCreator;
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import edu.wpi.first.math.geometry.Pose3d;
|
||||
import edu.wpi.first.math.geometry.Rotation3d;
|
||||
import edu.wpi.first.math.geometry.Translation3d;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* Class for representing a layout of AprilTags on a field and reading them from a JSON format.
|
||||
*
|
||||
* <p>The JSON format contains two top-level objects, "tags" and "field". The "tags" object is a
|
||||
* list of all AprilTags contained within a layout. Each AprilTag serializes to a JSON object
|
||||
* containing an ID and a Pose3d. The "field" object is a descriptor of the size of the field in
|
||||
* meters with "width" and "length" values. This is to account for arbitrary field sizes when
|
||||
* transforming the poses.
|
||||
*
|
||||
* <p>Pose3ds in the JSON are measured using the normal FRC coordinate system, NWU with the origin
|
||||
* at the bottom-right corner of the blue alliance wall. {@link #setOrigin(OriginPosition)} can be
|
||||
* used to change the poses returned from {@link AprilTagFieldLayout#getTagPose(int)} to be from the
|
||||
* perspective of a specific alliance.
|
||||
*
|
||||
* <p>Tag poses represent the center of the tag, with a zero rotation representing a tag that is
|
||||
* upright and facing away from the (blue) alliance wall (that is, towards the opposing alliance).
|
||||
*/
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
@JsonAutoDetect(getterVisibility = JsonAutoDetect.Visibility.NONE)
|
||||
public class AprilTagFieldLayout {
|
||||
public enum OriginPosition {
|
||||
kBlueAllianceWallRightSide,
|
||||
kRedAllianceWallRightSide,
|
||||
}
|
||||
|
||||
private final Map<Integer, AprilTag> m_apriltags = new HashMap<>();
|
||||
|
||||
@JsonProperty(value = "field")
|
||||
private FieldDimensions m_fieldDimensions;
|
||||
|
||||
private Pose3d m_origin;
|
||||
|
||||
/**
|
||||
* Construct a new AprilTagFieldLayout with values imported from a JSON file.
|
||||
*
|
||||
* @param path Path of the JSON file to import from.
|
||||
* @throws IOException If reading from the file fails.
|
||||
*/
|
||||
public AprilTagFieldLayout(String path) throws IOException {
|
||||
this(Path.of(path));
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct a new AprilTagFieldLayout with values imported from a JSON file.
|
||||
*
|
||||
* @param path Path of the JSON file to import from.
|
||||
* @throws IOException If reading from the file fails.
|
||||
*/
|
||||
public AprilTagFieldLayout(Path path) throws IOException {
|
||||
AprilTagFieldLayout layout =
|
||||
new ObjectMapper().readValue(path.toFile(), AprilTagFieldLayout.class);
|
||||
m_apriltags.putAll(layout.m_apriltags);
|
||||
m_fieldDimensions = layout.m_fieldDimensions;
|
||||
setOrigin(OriginPosition.kBlueAllianceWallRightSide);
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct a new AprilTagFieldLayout from a list of {@link AprilTag} objects.
|
||||
*
|
||||
* @param apriltags List of {@link AprilTag}.
|
||||
* @param fieldLength Length of the field the layout is representing in meters.
|
||||
* @param fieldWidth Width of the field the layout is representing in meters.
|
||||
*/
|
||||
public AprilTagFieldLayout(List<AprilTag> apriltags, double fieldLength, double fieldWidth) {
|
||||
this(apriltags, new FieldDimensions(fieldLength, fieldWidth));
|
||||
}
|
||||
|
||||
@JsonCreator
|
||||
private AprilTagFieldLayout(
|
||||
@JsonProperty(required = true, value = "tags") List<AprilTag> apriltags,
|
||||
@JsonProperty(required = true, value = "field") FieldDimensions fieldDimensions) {
|
||||
// To ensure the underlying semantics don't change with what kind of list is passed in
|
||||
for (AprilTag tag : apriltags) {
|
||||
m_apriltags.put(tag.ID, tag);
|
||||
}
|
||||
m_fieldDimensions = fieldDimensions;
|
||||
setOrigin(OriginPosition.kBlueAllianceWallRightSide);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a List of the {@link AprilTag AprilTags} used in this layout.
|
||||
*
|
||||
* @return The {@link AprilTag AprilTags} used in this layout.
|
||||
*/
|
||||
@JsonProperty("tags")
|
||||
public List<AprilTag> getTags() {
|
||||
return new ArrayList<>(m_apriltags.values());
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the origin based on a predefined enumeration of coordinate frame origins. The origins are
|
||||
* calculated from the field dimensions.
|
||||
*
|
||||
* <p>This transforms the Pose3d objects returned by {@link #getTagPose(int)} to return the
|
||||
* correct pose relative to a predefined coordinate frame.
|
||||
*
|
||||
* @param origin The predefined origin
|
||||
*/
|
||||
@JsonIgnore
|
||||
public void setOrigin(OriginPosition origin) {
|
||||
switch (origin) {
|
||||
case kBlueAllianceWallRightSide:
|
||||
setOrigin(new Pose3d());
|
||||
break;
|
||||
case kRedAllianceWallRightSide:
|
||||
setOrigin(
|
||||
new Pose3d(
|
||||
new Translation3d(m_fieldDimensions.fieldLength, m_fieldDimensions.fieldWidth, 0),
|
||||
new Rotation3d(0, 0, Math.PI)));
|
||||
break;
|
||||
default:
|
||||
throw new IllegalArgumentException("Unsupported enum value");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the origin for tag pose transformation.
|
||||
*
|
||||
* <p>This transforms the Pose3d objects returned by {@link #getTagPose(int)} to return the
|
||||
* correct pose relative to the provided origin.
|
||||
*
|
||||
* @param origin The new origin for tag transformations
|
||||
*/
|
||||
@JsonIgnore
|
||||
public void setOrigin(Pose3d origin) {
|
||||
m_origin = origin;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets an AprilTag pose by its ID.
|
||||
*
|
||||
* @param ID The ID of the tag.
|
||||
* @return The pose corresponding to the ID passed in or an empty optional if a tag with that ID
|
||||
* was not found.
|
||||
*/
|
||||
@SuppressWarnings("ParameterName")
|
||||
public Optional<Pose3d> getTagPose(int ID) {
|
||||
AprilTag tag = m_apriltags.get(ID);
|
||||
if (tag == null) {
|
||||
return Optional.empty();
|
||||
}
|
||||
return Optional.of(tag.pose.relativeTo(m_origin));
|
||||
}
|
||||
|
||||
/**
|
||||
* Serializes a AprilTagFieldLayout to a JSON file.
|
||||
*
|
||||
* @param path The path to write to.
|
||||
* @throws IOException If writing to the file fails.
|
||||
*/
|
||||
public void serialize(String path) throws IOException {
|
||||
serialize(Path.of(path));
|
||||
}
|
||||
|
||||
/**
|
||||
* Serializes a AprilTagFieldLayout to a JSON file.
|
||||
*
|
||||
* @param path The path to write to.
|
||||
* @throws IOException If writing to the file fails.
|
||||
*/
|
||||
public void serialize(Path path) throws IOException {
|
||||
new ObjectMapper().writeValue(path.toFile(), this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deserializes a field layout from a resource within a internal jar file.
|
||||
*
|
||||
* <p>Users should use {@link AprilTagFields#loadAprilTagLayoutField()} to load official layouts
|
||||
* and {@link #AprilTagFieldLayout(String)} for custom layouts.
|
||||
*
|
||||
* @param resourcePath The absolute path of the resource
|
||||
* @return The deserialized layout
|
||||
* @throws IOException If the resource could not be loaded
|
||||
*/
|
||||
public static AprilTagFieldLayout loadFromResource(String resourcePath) throws IOException {
|
||||
try (InputStream stream = AprilTagFieldLayout.class.getResourceAsStream(resourcePath);
|
||||
InputStreamReader reader = new InputStreamReader(stream)) {
|
||||
return new ObjectMapper().readerFor(AprilTagFieldLayout.class).readValue(reader);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (obj instanceof AprilTagFieldLayout) {
|
||||
var other = (AprilTagFieldLayout) obj;
|
||||
return m_apriltags.equals(other.m_apriltags) && m_origin.equals(other.m_origin);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(m_apriltags, m_origin);
|
||||
}
|
||||
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
@JsonAutoDetect(getterVisibility = JsonAutoDetect.Visibility.NONE)
|
||||
private static class FieldDimensions {
|
||||
@SuppressWarnings("MemberName")
|
||||
@JsonProperty(value = "length")
|
||||
public double fieldLength;
|
||||
|
||||
@SuppressWarnings("MemberName")
|
||||
@JsonProperty(value = "width")
|
||||
public double fieldWidth;
|
||||
|
||||
@JsonCreator()
|
||||
FieldDimensions(
|
||||
@JsonProperty(required = true, value = "length") double fieldLength,
|
||||
@JsonProperty(required = true, value = "width") double fieldWidth) {
|
||||
this.fieldLength = fieldLength;
|
||||
this.fieldWidth = fieldWidth;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
// Copyright (c) FIRST and other WPILib contributors.
|
||||
// Open Source Software; you can modify and/or share it under the terms of
|
||||
// the WPILib BSD license file in the root directory of this project.
|
||||
|
||||
package edu.wpi.first.apriltag;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
public enum AprilTagFields {
|
||||
k2022RapidReact("2022-rapidreact.json"),
|
||||
k2023ChargedUp("2023-chargedup.json");
|
||||
|
||||
public static final String kBaseResourceDir = "/edu/wpi/first/apriltag/";
|
||||
|
||||
/** Alias to the current game. */
|
||||
public static final AprilTagFields kDefaultField = k2023ChargedUp;
|
||||
|
||||
public final String m_resourceFile;
|
||||
|
||||
AprilTagFields(String resourceFile) {
|
||||
m_resourceFile = kBaseResourceDir + resourceFile;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a {@link AprilTagFieldLayout} from the resource JSON.
|
||||
*
|
||||
* @return AprilTagFieldLayout of the field
|
||||
* @throws IOException If the layout does not exist
|
||||
*/
|
||||
public AprilTagFieldLayout loadAprilTagLayoutField() throws IOException {
|
||||
return AprilTagFieldLayout.loadFromResource(m_resourceFile);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
// Copyright (c) FIRST and other WPILib contributors.
|
||||
// Open Source Software; you can modify and/or share it under the terms of
|
||||
// the WPILib BSD license file in the root directory of this project.
|
||||
|
||||
package edu.wpi.first.apriltag;
|
||||
|
||||
import edu.wpi.first.math.geometry.Transform3d;
|
||||
|
||||
/** A pair of AprilTag pose estimates. */
|
||||
@SuppressWarnings("MemberName")
|
||||
public class AprilTagPoseEstimate {
|
||||
/**
|
||||
* Constructs a pose estimate.
|
||||
*
|
||||
* @param pose1 first pose
|
||||
* @param pose2 second pose
|
||||
* @param error1 error of first pose
|
||||
* @param error2 error of second pose
|
||||
*/
|
||||
public AprilTagPoseEstimate(Transform3d pose1, Transform3d pose2, double error1, double error2) {
|
||||
this.pose1 = pose1;
|
||||
this.pose2 = pose2;
|
||||
this.error1 = error1;
|
||||
this.error2 = error2;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the ratio of pose reprojection errors, called ambiguity. Numbers above 0.2 are likely to be
|
||||
* ambiguous.
|
||||
*
|
||||
* @return The ratio of pose reprojection errors.
|
||||
*/
|
||||
public double getAmbiguity() {
|
||||
double min = Math.min(error1, error2);
|
||||
double max = Math.max(error1, error2);
|
||||
|
||||
if (max > 0) {
|
||||
return min / max;
|
||||
} else {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
/** Pose 1. */
|
||||
public final Transform3d pose1;
|
||||
|
||||
/** Pose 2. */
|
||||
public final Transform3d pose2;
|
||||
|
||||
/** Object-space error of pose 1. */
|
||||
public final double error1;
|
||||
|
||||
/** Object-space error of pose 2. */
|
||||
public final double error2;
|
||||
}
|
||||
@@ -0,0 +1,190 @@
|
||||
// Copyright (c) FIRST and other WPILib contributors.
|
||||
// Open Source Software; you can modify and/or share it under the terms of
|
||||
// the WPILib BSD license file in the root directory of this project.
|
||||
|
||||
package edu.wpi.first.apriltag;
|
||||
|
||||
import edu.wpi.first.apriltag.jni.AprilTagJNI;
|
||||
import edu.wpi.first.math.geometry.Transform3d;
|
||||
|
||||
/** Pose estimators for AprilTag tags. */
|
||||
public class AprilTagPoseEstimator {
|
||||
/** Configuration for the pose estimator. */
|
||||
@SuppressWarnings("MemberName")
|
||||
public static class Config {
|
||||
/**
|
||||
* Creates a pose estimator configuration.
|
||||
*
|
||||
* @param tagSize tag size, in meters
|
||||
* @param fx camera horizontal focal length, in pixels
|
||||
* @param fy camera vertical focal length, in pixels
|
||||
* @param cx camera horizontal focal center, in pixels
|
||||
* @param cy camera vertical focal center, in pixels
|
||||
*/
|
||||
public Config(double tagSize, double fx, double fy, double cx, double cy) {
|
||||
this.tagSize = tagSize;
|
||||
this.fx = fx;
|
||||
this.fy = fy;
|
||||
this.cx = cx;
|
||||
this.cy = cy;
|
||||
}
|
||||
|
||||
public double tagSize;
|
||||
public double fx;
|
||||
public double fy;
|
||||
public double cx;
|
||||
public double cy;
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Double.hashCode(tagSize)
|
||||
+ Double.hashCode(fx)
|
||||
+ Double.hashCode(fy)
|
||||
+ Double.hashCode(cx)
|
||||
+ Double.hashCode(cy);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (!(obj instanceof Config)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
Config other = (Config) obj;
|
||||
return tagSize == other.tagSize
|
||||
&& fx == other.fx
|
||||
&& fy == other.fy
|
||||
&& cx == other.cx
|
||||
&& cy == other.cy;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates estimator.
|
||||
*
|
||||
* @param config Configuration
|
||||
*/
|
||||
public AprilTagPoseEstimator(Config config) {
|
||||
m_config = new Config(config.tagSize, config.fx, config.fy, config.cx, config.cy);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets estimator configuration.
|
||||
*
|
||||
* @param config Configuration
|
||||
*/
|
||||
public void setConfig(Config config) {
|
||||
m_config.tagSize = config.tagSize;
|
||||
m_config.fx = config.fx;
|
||||
m_config.fy = config.fy;
|
||||
m_config.cx = config.cx;
|
||||
m_config.cy = config.cy;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets estimator configuration.
|
||||
*
|
||||
* @return Configuration
|
||||
*/
|
||||
public Config getConfig() {
|
||||
return new Config(m_config.tagSize, m_config.fx, m_config.fy, m_config.cx, m_config.cy);
|
||||
}
|
||||
|
||||
/**
|
||||
* Estimates the pose of the tag using the homography method described in [1].
|
||||
*
|
||||
* @param detection Tag detection
|
||||
* @return Pose estimate
|
||||
*/
|
||||
public Transform3d estimateHomography(AprilTagDetection detection) {
|
||||
return estimateHomography(detection.getHomography());
|
||||
}
|
||||
|
||||
/**
|
||||
* Estimates the pose of the tag using the homography method described in [1].
|
||||
*
|
||||
* @param homography Homography 3x3 matrix data
|
||||
* @return Pose estimate
|
||||
*/
|
||||
public Transform3d estimateHomography(double[] homography) {
|
||||
return AprilTagJNI.estimatePoseHomography(
|
||||
homography, m_config.tagSize, m_config.fx, m_config.fy, m_config.cx, m_config.cy);
|
||||
}
|
||||
|
||||
/**
|
||||
* Estimates the pose of the tag. This returns one or two possible poses for the tag, along with
|
||||
* the object-space error of each.
|
||||
*
|
||||
* <p>This uses the homography method described in [1] for the initial estimate. Then Orthogonal
|
||||
* Iteration [2] is used to refine this estimate. Then [3] is used to find a potential second
|
||||
* local minima and Orthogonal Iteration is used to refine this second estimate.
|
||||
*
|
||||
* <p>[1]: E. Olson, “Apriltag: A robust and flexible visual fiducial system,” in 2011 IEEE
|
||||
* International Conference on Robotics and Automation, May 2011, pp. 3400–3407.
|
||||
*
|
||||
* <p>[2]: Lu, G. D. Hager and E. Mjolsness, "Fast and globally convergent pose estimation from
|
||||
* video images," in IEEE Transactions on Pattern Analysis and Machine Intelligence, vol. 22, no.
|
||||
* 6, pp. 610-622, June 2000. doi: 10.1109/34.862199
|
||||
*
|
||||
* <p>[3]: Schweighofer and A. Pinz, "Robust Pose Estimation from a Planar Target," in IEEE
|
||||
* Transactions on Pattern Analysis and Machine Intelligence, vol. 28, no. 12, pp. 2024-2030, Dec.
|
||||
* 2006. doi: 10.1109/TPAMI.2006.252
|
||||
*
|
||||
* @param detection Tag detection
|
||||
* @param nIters Number of iterations
|
||||
* @return Initial and (possibly) second pose estimates
|
||||
*/
|
||||
public AprilTagPoseEstimate estimateOrthogonalIteration(AprilTagDetection detection, int nIters) {
|
||||
return estimateOrthogonalIteration(detection.getHomography(), detection.getCorners(), nIters);
|
||||
}
|
||||
|
||||
/**
|
||||
* Estimates the pose of the tag. This returns one or two possible poses for the tag, along with
|
||||
* the object-space error of each.
|
||||
*
|
||||
* @param homography Homography 3x3 matrix data
|
||||
* @param corners Corner point array (X and Y for each corner in order)
|
||||
* @param nIters Number of iterations
|
||||
* @return Initial and (possibly) second pose estimates
|
||||
*/
|
||||
public AprilTagPoseEstimate estimateOrthogonalIteration(
|
||||
double[] homography, double[] corners, int nIters) {
|
||||
return AprilTagJNI.estimatePoseOrthogonalIteration(
|
||||
homography,
|
||||
corners,
|
||||
m_config.tagSize,
|
||||
m_config.fx,
|
||||
m_config.fy,
|
||||
m_config.cx,
|
||||
m_config.cy,
|
||||
nIters);
|
||||
}
|
||||
|
||||
/**
|
||||
* Estimates tag pose. This method is an easier to use interface to
|
||||
* EstimatePoseOrthogonalIteration(), running 50 iterations and returning the pose with the lower
|
||||
* object-space error.
|
||||
*
|
||||
* @param detection Tag detection
|
||||
* @return Pose estimate
|
||||
*/
|
||||
public Transform3d estimate(AprilTagDetection detection) {
|
||||
return estimate(detection.getHomography(), detection.getCorners());
|
||||
}
|
||||
|
||||
/**
|
||||
* Estimates tag pose. This method is an easier to use interface to
|
||||
* EstimatePoseOrthogonalIteration(), running 50 iterations and returning the pose with the lower
|
||||
* object-space error.
|
||||
*
|
||||
* @param homography Homography 3x3 matrix data
|
||||
* @param corners Corner point array (X and Y for each corner in order)
|
||||
* @return Pose estimate
|
||||
*/
|
||||
public Transform3d estimate(double[] homography, double[] corners) {
|
||||
return AprilTagJNI.estimatePose(
|
||||
homography, corners, m_config.tagSize, m_config.fx, m_config.fy, m_config.cx, m_config.cy);
|
||||
}
|
||||
|
||||
private final Config m_config;
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
// Copyright (c) FIRST and other WPILib contributors.
|
||||
// Open Source Software; you can modify and/or share it under the terms of
|
||||
// the WPILib BSD license file in the root directory of this project.
|
||||
|
||||
package edu.wpi.first.apriltag.jni;
|
||||
|
||||
import edu.wpi.first.apriltag.AprilTagDetection;
|
||||
import edu.wpi.first.apriltag.AprilTagDetector;
|
||||
import edu.wpi.first.apriltag.AprilTagPoseEstimate;
|
||||
import edu.wpi.first.math.geometry.Transform3d;
|
||||
import edu.wpi.first.util.RuntimeLoader;
|
||||
import java.io.IOException;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
public class AprilTagJNI {
|
||||
static boolean libraryLoaded = false;
|
||||
|
||||
static RuntimeLoader<AprilTagJNI> loader = null;
|
||||
|
||||
public static class Helper {
|
||||
private static AtomicBoolean extractOnStaticLoad = new AtomicBoolean(true);
|
||||
|
||||
public static boolean getExtractOnStaticLoad() {
|
||||
return extractOnStaticLoad.get();
|
||||
}
|
||||
|
||||
public static void setExtractOnStaticLoad(boolean load) {
|
||||
extractOnStaticLoad.set(load);
|
||||
}
|
||||
}
|
||||
|
||||
static {
|
||||
if (Helper.getExtractOnStaticLoad()) {
|
||||
try {
|
||||
loader =
|
||||
new RuntimeLoader<>(
|
||||
"apriltagjni", RuntimeLoader.getDefaultExtractionRoot(), AprilTagJNI.class);
|
||||
loader.loadLibrary();
|
||||
} catch (IOException ex) {
|
||||
ex.printStackTrace();
|
||||
System.exit(1);
|
||||
}
|
||||
libraryLoaded = true;
|
||||
}
|
||||
}
|
||||
|
||||
public static native long createDetector();
|
||||
|
||||
public static native void destroyDetector(long det);
|
||||
|
||||
public static native void setDetectorConfig(long det, AprilTagDetector.Config config);
|
||||
|
||||
public static native AprilTagDetector.Config getDetectorConfig(long det);
|
||||
|
||||
public static native void setDetectorQTP(
|
||||
long det, AprilTagDetector.QuadThresholdParameters params);
|
||||
|
||||
public static native AprilTagDetector.QuadThresholdParameters getDetectorQTP(long det);
|
||||
|
||||
public static native boolean addFamily(long det, String fam, int bitsCorrected);
|
||||
|
||||
public static native void removeFamily(long det, String fam);
|
||||
|
||||
public static native void clearFamilies(long det);
|
||||
|
||||
public static native AprilTagDetection[] detect(
|
||||
long det, int width, int height, int stride, long bufAddr);
|
||||
|
||||
public static native Transform3d estimatePoseHomography(
|
||||
double[] homography, double tagSize, double fx, double fy, double cx, double cy);
|
||||
|
||||
public static native AprilTagPoseEstimate estimatePoseOrthogonalIteration(
|
||||
double[] homography,
|
||||
double[] corners,
|
||||
double tagSize,
|
||||
double fx,
|
||||
double fy,
|
||||
double cx,
|
||||
double cy,
|
||||
int nIters);
|
||||
|
||||
public static native Transform3d estimatePose(
|
||||
double[] homography,
|
||||
double[] corners,
|
||||
double tagSize,
|
||||
double fx,
|
||||
double fy,
|
||||
double cx,
|
||||
double cy);
|
||||
}
|
||||
18
apriltag/src/main/native/cpp/AprilTag.cpp
Normal file
18
apriltag/src/main/native/cpp/AprilTag.cpp
Normal file
@@ -0,0 +1,18 @@
|
||||
// Copyright (c) FIRST and other WPILib contributors.
|
||||
// Open Source Software; you can modify and/or share it under the terms of
|
||||
// the WPILib BSD license file in the root directory of this project.
|
||||
|
||||
#include "frc/apriltag/AprilTag.h"
|
||||
|
||||
#include <wpi/json.h>
|
||||
|
||||
using namespace frc;
|
||||
|
||||
void frc::to_json(wpi::json& json, const AprilTag& apriltag) {
|
||||
json = wpi::json{{"ID", apriltag.ID}, {"pose", apriltag.pose}};
|
||||
}
|
||||
|
||||
void frc::from_json(const wpi::json& json, AprilTag& apriltag) {
|
||||
apriltag.ID = json.at("ID").get<int>();
|
||||
apriltag.pose = json.at("pose").get<Pose3d>();
|
||||
}
|
||||
37
apriltag/src/main/native/cpp/AprilTagDetection.cpp
Normal file
37
apriltag/src/main/native/cpp/AprilTagDetection.cpp
Normal file
@@ -0,0 +1,37 @@
|
||||
// Copyright (c) FIRST and other WPILib contributors.
|
||||
// Open Source Software; you can modify and/or share it under the terms of
|
||||
// the WPILib BSD license file in the root directory of this project.
|
||||
|
||||
#include "frc/apriltag/AprilTagDetection.h"
|
||||
|
||||
#include <type_traits>
|
||||
|
||||
#ifdef _WIN32
|
||||
#pragma warning(disable : 4200)
|
||||
#elif defined(__clang__)
|
||||
#pragma clang diagnostic ignored "-Wc99-extensions"
|
||||
#elif defined(__GNUC__)
|
||||
#pragma GCC diagnostic ignored "-Wpedantic"
|
||||
#endif
|
||||
|
||||
#include "apriltag.h"
|
||||
|
||||
using namespace frc;
|
||||
|
||||
static_assert(sizeof(AprilTagDetection) == sizeof(apriltag_detection_t),
|
||||
"structure sizes don't match");
|
||||
static_assert(std::is_standard_layout_v<AprilTagDetection>,
|
||||
"AprilTagDetection is not standard layout?");
|
||||
|
||||
std::string_view AprilTagDetection::GetFamily() const {
|
||||
return static_cast<const apriltag_family_t*>(family)->name;
|
||||
}
|
||||
|
||||
std::span<const double, 9> AprilTagDetection::GetHomography() const {
|
||||
return std::span<const double, 9>{static_cast<matd_t*>(H)->data, 9};
|
||||
}
|
||||
|
||||
Eigen::Matrix3d AprilTagDetection::GetHomographyMatrix() const {
|
||||
return Eigen::Map<Eigen::Matrix<double, 3, 3, Eigen::RowMajor>>{
|
||||
static_cast<matd_t*>(H)->data};
|
||||
}
|
||||
200
apriltag/src/main/native/cpp/AprilTagDetector.cpp
Normal file
200
apriltag/src/main/native/cpp/AprilTagDetector.cpp
Normal file
@@ -0,0 +1,200 @@
|
||||
// Copyright (c) FIRST and other WPILib contributors.
|
||||
// Open Source Software; you can modify and/or share it under the terms of
|
||||
// the WPILib BSD license file in the root directory of this project.
|
||||
|
||||
#include "frc/apriltag/AprilTagDetector.h"
|
||||
|
||||
#include <cmath>
|
||||
#include <numbers>
|
||||
|
||||
#ifdef _WIN32
|
||||
#pragma warning(disable : 4200)
|
||||
#elif defined(__clang__)
|
||||
#pragma clang diagnostic ignored "-Wc99-extensions"
|
||||
#elif defined(__GNUC__)
|
||||
#pragma GCC diagnostic ignored "-Wpedantic"
|
||||
#endif
|
||||
|
||||
#include "apriltag.h"
|
||||
#include "tag16h5.h"
|
||||
#include "tag25h9.h"
|
||||
#include "tag36h11.h"
|
||||
#include "tagCircle21h7.h"
|
||||
#include "tagCircle49h12.h"
|
||||
#include "tagCustom48h12.h"
|
||||
#include "tagStandard41h12.h"
|
||||
#include "tagStandard52h13.h"
|
||||
|
||||
using namespace frc;
|
||||
|
||||
AprilTagDetector::Results::Results(void* impl, const private_init&)
|
||||
: span{reinterpret_cast<AprilTagDetection**>(
|
||||
static_cast<zarray_t*>(impl)->data),
|
||||
static_cast<size_t>(static_cast<zarray_t*>(impl)->size)},
|
||||
m_impl{impl} {}
|
||||
|
||||
AprilTagDetector::Results& AprilTagDetector::Results::operator=(Results&& rhs) {
|
||||
Destroy();
|
||||
m_impl = rhs.m_impl;
|
||||
rhs.m_impl = nullptr;
|
||||
return *this;
|
||||
}
|
||||
|
||||
void AprilTagDetector::Results::Destroy() {
|
||||
if (m_impl) {
|
||||
apriltag_detections_destroy(static_cast<zarray_t*>(m_impl));
|
||||
}
|
||||
}
|
||||
|
||||
AprilTagDetector::AprilTagDetector() : m_impl{apriltag_detector_create()} {}
|
||||
|
||||
AprilTagDetector& AprilTagDetector::operator=(AprilTagDetector&& rhs) {
|
||||
Destroy();
|
||||
m_impl = rhs.m_impl;
|
||||
rhs.m_impl = nullptr;
|
||||
m_families = std::move(rhs.m_families);
|
||||
rhs.m_families.clear();
|
||||
m_qtpCriticalAngle = rhs.m_qtpCriticalAngle;
|
||||
return *this;
|
||||
}
|
||||
|
||||
void AprilTagDetector::SetConfig(const Config& config) {
|
||||
auto& impl = *static_cast<apriltag_detector_t*>(m_impl);
|
||||
impl.nthreads = config.numThreads;
|
||||
impl.quad_decimate = config.quadDecimate;
|
||||
impl.quad_sigma = config.quadSigma;
|
||||
impl.refine_edges = config.refineEdges;
|
||||
impl.decode_sharpening = config.decodeSharpening;
|
||||
impl.debug = config.debug;
|
||||
}
|
||||
|
||||
AprilTagDetector::Config AprilTagDetector::GetConfig() const {
|
||||
auto& impl = *static_cast<apriltag_detector_t*>(m_impl);
|
||||
return {
|
||||
.numThreads = impl.nthreads,
|
||||
.quadDecimate = impl.quad_decimate,
|
||||
.quadSigma = impl.quad_sigma,
|
||||
.refineEdges = impl.refine_edges,
|
||||
.decodeSharpening = impl.decode_sharpening,
|
||||
.debug = impl.debug,
|
||||
};
|
||||
}
|
||||
|
||||
void AprilTagDetector::SetQuadThresholdParameters(
|
||||
const QuadThresholdParameters& params) {
|
||||
auto& qtp = static_cast<apriltag_detector_t*>(m_impl)->qtp;
|
||||
qtp.min_cluster_pixels = params.minClusterPixels;
|
||||
qtp.max_nmaxima = params.maxNumMaxima;
|
||||
qtp.critical_rad = params.criticalAngle.value();
|
||||
qtp.cos_critical_rad = std::cos(params.criticalAngle.value());
|
||||
qtp.max_line_fit_mse = params.maxLineFitMSE;
|
||||
qtp.min_white_black_diff = params.minWhiteBlackDiff;
|
||||
qtp.deglitch = params.deglitch;
|
||||
|
||||
m_qtpCriticalAngle = params.criticalAngle;
|
||||
}
|
||||
|
||||
AprilTagDetector::QuadThresholdParameters
|
||||
AprilTagDetector::GetQuadThresholdParameters() const {
|
||||
auto& qtp = static_cast<apriltag_detector_t*>(m_impl)->qtp;
|
||||
return {
|
||||
.minClusterPixels = qtp.min_cluster_pixels,
|
||||
.maxNumMaxima = qtp.max_nmaxima,
|
||||
.criticalAngle = m_qtpCriticalAngle,
|
||||
.maxLineFitMSE = qtp.max_line_fit_mse,
|
||||
.minWhiteBlackDiff = qtp.min_white_black_diff,
|
||||
.deglitch = qtp.deglitch != 0,
|
||||
};
|
||||
}
|
||||
|
||||
bool AprilTagDetector::AddFamily(std::string_view fam, int bitsCorrected) {
|
||||
auto& data = m_families[fam];
|
||||
if (data) {
|
||||
return true; // already detecting
|
||||
}
|
||||
// create the family
|
||||
if (fam == "tag16h5") {
|
||||
data = tag16h5_create();
|
||||
} else if (fam == "tag25h9") {
|
||||
data = tag25h9_create();
|
||||
} else if (fam == "tag36h11") {
|
||||
data = tag36h11_create();
|
||||
} else if (fam == "tagCircle21h7") {
|
||||
data = tagCircle21h7_create();
|
||||
} else if (fam == "tagCircle49h12") {
|
||||
data = tagCircle49h12_create();
|
||||
} else if (fam == "tagStandard41h12") {
|
||||
data = tagStandard41h12_create();
|
||||
} else if (fam == "tagStandard52h13") {
|
||||
data = tagStandard52h13_create();
|
||||
} else if (fam == "tagCustom48h12") {
|
||||
data = tagCustom48h12_create();
|
||||
}
|
||||
if (!data) {
|
||||
m_families.erase(fam); // don't keep null value
|
||||
return false; // can't add
|
||||
}
|
||||
apriltag_detector_add_family_bits(static_cast<apriltag_detector_t*>(m_impl),
|
||||
static_cast<apriltag_family_t*>(data),
|
||||
bitsCorrected);
|
||||
return true;
|
||||
}
|
||||
|
||||
void AprilTagDetector::RemoveFamily(std::string_view fam) {
|
||||
auto it = m_families.find(fam);
|
||||
if (it != m_families.end()) {
|
||||
apriltag_detector_remove_family(
|
||||
static_cast<apriltag_detector_t*>(m_impl),
|
||||
static_cast<apriltag_family_t*>(it->second));
|
||||
DestroyFamily(it->getKey(), it->second);
|
||||
m_families.erase(it);
|
||||
}
|
||||
}
|
||||
|
||||
void AprilTagDetector::ClearFamilies() {
|
||||
apriltag_detector_clear_families(static_cast<apriltag_detector_t*>(m_impl));
|
||||
DestroyFamilies();
|
||||
m_families.clear();
|
||||
}
|
||||
|
||||
AprilTagDetector::Results AprilTagDetector::Detect(int width, int height,
|
||||
int stride, uint8_t* buf) {
|
||||
image_u8_t img{width, height, stride, buf};
|
||||
return {
|
||||
apriltag_detector_detect(static_cast<apriltag_detector_t*>(m_impl), &img),
|
||||
Results::private_init{}};
|
||||
}
|
||||
|
||||
void AprilTagDetector::Destroy() {
|
||||
if (m_impl) {
|
||||
apriltag_detector_destroy(static_cast<apriltag_detector_t*>(m_impl));
|
||||
}
|
||||
DestroyFamilies();
|
||||
}
|
||||
|
||||
void AprilTagDetector::DestroyFamilies() {
|
||||
for (auto&& entry : m_families) {
|
||||
DestroyFamily(entry.getKey(), entry.second);
|
||||
}
|
||||
}
|
||||
|
||||
void AprilTagDetector::DestroyFamily(std::string_view name, void* data) {
|
||||
auto fam = static_cast<apriltag_family_t*>(data);
|
||||
if (name == "tag16h5") {
|
||||
tag16h5_destroy(fam);
|
||||
} else if (name == "tag25h9") {
|
||||
tag25h9_destroy(fam);
|
||||
} else if (name == "tag36h11") {
|
||||
tag36h11_destroy(fam);
|
||||
} else if (name == "tagCircle21h7") {
|
||||
tagCircle21h7_destroy(fam);
|
||||
} else if (name == "tagCircle49h12") {
|
||||
tagCircle49h12_destroy(fam);
|
||||
} else if (name == "tagStandard41h12") {
|
||||
tagStandard41h12_destroy(fam);
|
||||
} else if (name == "tagStandard52h13") {
|
||||
tagStandard52h13_destroy(fam);
|
||||
} else if (name == "tagCustom48h12") {
|
||||
tagCustom48h12_destroy(fam);
|
||||
}
|
||||
}
|
||||
107
apriltag/src/main/native/cpp/AprilTagFieldLayout.cpp
Normal file
107
apriltag/src/main/native/cpp/AprilTagFieldLayout.cpp
Normal file
@@ -0,0 +1,107 @@
|
||||
// Copyright (c) FIRST and other WPILib contributors.
|
||||
// Open Source Software; you can modify and/or share it under the terms of
|
||||
// the WPILib BSD license file in the root directory of this project.
|
||||
|
||||
#include "frc/apriltag/AprilTagFieldLayout.h"
|
||||
|
||||
#include <system_error>
|
||||
|
||||
#include <units/angle.h>
|
||||
#include <units/length.h>
|
||||
#include <wpi/json.h>
|
||||
#include <wpi/raw_istream.h>
|
||||
#include <wpi/raw_ostream.h>
|
||||
|
||||
using namespace frc;
|
||||
|
||||
AprilTagFieldLayout::AprilTagFieldLayout(std::string_view path) {
|
||||
std::error_code error_code;
|
||||
|
||||
wpi::raw_fd_istream input{path, error_code};
|
||||
if (error_code) {
|
||||
throw std::runtime_error(fmt::format("Cannot open file: {}", path));
|
||||
}
|
||||
|
||||
wpi::json json;
|
||||
input >> json;
|
||||
|
||||
for (const auto& tag : json.at("tags").get<std::vector<AprilTag>>()) {
|
||||
m_apriltags[tag.ID] = tag;
|
||||
}
|
||||
m_fieldWidth = units::meter_t{json.at("field").at("width").get<double>()};
|
||||
m_fieldLength = units::meter_t{json.at("field").at("length").get<double>()};
|
||||
}
|
||||
|
||||
AprilTagFieldLayout::AprilTagFieldLayout(std::vector<AprilTag> apriltags,
|
||||
units::meter_t fieldLength,
|
||||
units::meter_t fieldWidth)
|
||||
: m_fieldLength(std::move(fieldLength)),
|
||||
m_fieldWidth(std::move(fieldWidth)) {
|
||||
for (const auto& tag : apriltags) {
|
||||
m_apriltags[tag.ID] = tag;
|
||||
}
|
||||
}
|
||||
|
||||
void AprilTagFieldLayout::SetOrigin(OriginPosition origin) {
|
||||
switch (origin) {
|
||||
case OriginPosition::kBlueAllianceWallRightSide:
|
||||
SetOrigin(Pose3d{});
|
||||
break;
|
||||
case OriginPosition::kRedAllianceWallRightSide:
|
||||
SetOrigin(Pose3d{Translation3d{m_fieldLength, m_fieldWidth, 0_m},
|
||||
Rotation3d{0_deg, 0_deg, 180_deg}});
|
||||
break;
|
||||
default:
|
||||
throw std::invalid_argument("Invalid origin");
|
||||
}
|
||||
}
|
||||
|
||||
void AprilTagFieldLayout::SetOrigin(const Pose3d& origin) {
|
||||
m_origin = origin;
|
||||
}
|
||||
|
||||
std::optional<frc::Pose3d> AprilTagFieldLayout::GetTagPose(int ID) const {
|
||||
const auto& it = m_apriltags.find(ID);
|
||||
if (it == m_apriltags.end()) {
|
||||
return std::nullopt;
|
||||
}
|
||||
return it->second.pose.RelativeTo(m_origin);
|
||||
}
|
||||
|
||||
void AprilTagFieldLayout::Serialize(std::string_view path) {
|
||||
std::error_code error_code;
|
||||
|
||||
wpi::raw_fd_ostream output{path, error_code};
|
||||
if (error_code) {
|
||||
throw std::runtime_error(fmt::format("Cannot open file: {}", path));
|
||||
}
|
||||
|
||||
wpi::json json = *this;
|
||||
output << json;
|
||||
output.flush();
|
||||
}
|
||||
|
||||
void frc::to_json(wpi::json& json, const AprilTagFieldLayout& layout) {
|
||||
std::vector<AprilTag> tagVector;
|
||||
tagVector.reserve(layout.m_apriltags.size());
|
||||
for (const auto& pair : layout.m_apriltags) {
|
||||
tagVector.push_back(pair.second);
|
||||
}
|
||||
|
||||
json = wpi::json{{"field",
|
||||
{{"length", layout.m_fieldLength.value()},
|
||||
{"width", layout.m_fieldWidth.value()}}},
|
||||
{"tags", tagVector}};
|
||||
}
|
||||
|
||||
void frc::from_json(const wpi::json& json, AprilTagFieldLayout& layout) {
|
||||
layout.m_apriltags.clear();
|
||||
for (const auto& tag : json.at("tags").get<std::vector<AprilTag>>()) {
|
||||
layout.m_apriltags[tag.ID] = tag;
|
||||
}
|
||||
|
||||
layout.m_fieldLength =
|
||||
units::meter_t{json.at("field").at("length").get<double>()};
|
||||
layout.m_fieldWidth =
|
||||
units::meter_t{json.at("field").at("width").get<double>()};
|
||||
}
|
||||
32
apriltag/src/main/native/cpp/AprilTagFields.cpp
Normal file
32
apriltag/src/main/native/cpp/AprilTagFields.cpp
Normal file
@@ -0,0 +1,32 @@
|
||||
// Copyright (c) FIRST and other WPILib contributors.
|
||||
// Open Source Software; you can modify and/or share it under the terms of
|
||||
// the WPILib BSD license file in the root directory of this project.
|
||||
|
||||
#include "frc/apriltag/AprilTagFields.h"
|
||||
|
||||
#include <wpi/json.h>
|
||||
|
||||
namespace frc {
|
||||
|
||||
// C++ generated from resource files
|
||||
std::string_view GetResource_2022_rapidreact_json();
|
||||
std::string_view GetResource_2023_chargedup_json();
|
||||
|
||||
AprilTagFieldLayout LoadAprilTagLayoutField(AprilTagField field) {
|
||||
std::string_view fieldString;
|
||||
switch (field) {
|
||||
case AprilTagField::k2022RapidReact:
|
||||
fieldString = GetResource_2022_rapidreact_json();
|
||||
break;
|
||||
case AprilTagField::k2023ChargedUp:
|
||||
fieldString = GetResource_2023_chargedup_json();
|
||||
break;
|
||||
case AprilTagField::kNumFields:
|
||||
throw std::invalid_argument("Invalid Field");
|
||||
}
|
||||
|
||||
wpi::json json = wpi::json::parse(fieldString);
|
||||
return json.get<AprilTagFieldLayout>();
|
||||
}
|
||||
|
||||
} // namespace frc
|
||||
20
apriltag/src/main/native/cpp/AprilTagPoseEstimate.cpp
Normal file
20
apriltag/src/main/native/cpp/AprilTagPoseEstimate.cpp
Normal file
@@ -0,0 +1,20 @@
|
||||
// Copyright (c) FIRST and other WPILib contributors.
|
||||
// Open Source Software; you can modify and/or share it under the terms of
|
||||
// the WPILib BSD license file in the root directory of this project.
|
||||
|
||||
#include "frc/apriltag/AprilTagPoseEstimate.h"
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
using namespace frc;
|
||||
|
||||
double AprilTagPoseEstimate::GetAmbiguity() const {
|
||||
auto min = (std::min)(error1, error2);
|
||||
auto max = (std::max)(error1, error2);
|
||||
|
||||
if (max > 0) {
|
||||
return min / max;
|
||||
} else {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
154
apriltag/src/main/native/cpp/AprilTagPoseEstimator.cpp
Normal file
154
apriltag/src/main/native/cpp/AprilTagPoseEstimator.cpp
Normal file
@@ -0,0 +1,154 @@
|
||||
// Copyright (c) FIRST and other WPILib contributors.
|
||||
// Open Source Software; you can modify and/or share it under the terms of
|
||||
// the WPILib BSD license file in the root directory of this project.
|
||||
|
||||
#include "frc/apriltag/AprilTagPoseEstimator.h"
|
||||
|
||||
#include <Eigen/QR>
|
||||
|
||||
#include "frc/apriltag/AprilTagDetection.h"
|
||||
|
||||
#ifdef _WIN32
|
||||
#pragma warning(disable : 4200)
|
||||
#elif defined(__clang__)
|
||||
#pragma clang diagnostic ignored "-Wc99-extensions"
|
||||
#elif defined(__GNUC__)
|
||||
#pragma GCC diagnostic ignored "-Wpedantic"
|
||||
#endif
|
||||
|
||||
#include "apriltag.h"
|
||||
#include "apriltag_pose.h"
|
||||
|
||||
using namespace frc;
|
||||
|
||||
static Eigen::Matrix3d OrthogonalizeRotationMatrix(
|
||||
const Eigen::Matrix3d& input) {
|
||||
Eigen::HouseholderQR<Eigen::Matrix3d> qr{input};
|
||||
|
||||
Eigen::Matrix3d Q = qr.householderQ();
|
||||
Eigen::Matrix3d R = qr.matrixQR().triangularView<Eigen::Upper>();
|
||||
|
||||
// Fix signs in R if they're < 0 so it's close to an identity matrix
|
||||
// (our QR decomposition implementation sometimes flips the signs of
|
||||
// columns)
|
||||
for (int colR = 0; colR < 3; ++colR) {
|
||||
if (R(colR, colR) < 0) {
|
||||
for (int rowQ = 0; rowQ < 3; ++rowQ) {
|
||||
Q(rowQ, colR) = -Q(rowQ, colR);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Q;
|
||||
}
|
||||
|
||||
static Transform3d MakePose(const apriltag_pose_t& pose) {
|
||||
if (!pose.R || !pose.t) {
|
||||
return {};
|
||||
}
|
||||
return {Translation3d{units::meter_t{pose.t->data[0]},
|
||||
units::meter_t{pose.t->data[1]},
|
||||
units::meter_t{pose.t->data[2]}},
|
||||
Rotation3d{OrthogonalizeRotationMatrix(
|
||||
Eigen::Map<Eigen::Matrix<double, 3, 3, Eigen::RowMajor>>{
|
||||
pose.R->data})}};
|
||||
}
|
||||
|
||||
static apriltag_detection_info_t MakeDetectionInfo(
|
||||
const apriltag_detection_t* det,
|
||||
const AprilTagPoseEstimator::Config& config) {
|
||||
return {const_cast<apriltag_detection_t*>(det),
|
||||
config.tagSize.value(),
|
||||
config.fx,
|
||||
config.fy,
|
||||
config.cx,
|
||||
config.cy};
|
||||
}
|
||||
|
||||
static apriltag_detection_t MakeBasicDet(
|
||||
std::span<const double, 9> homography,
|
||||
const std::span<const double, 8>* corners) {
|
||||
apriltag_detection_t detection;
|
||||
detection.H = matd_create(3, 3);
|
||||
std::memcpy(detection.H->data, homography.data(), 9 * sizeof(double));
|
||||
if (corners) {
|
||||
for (int i = 0; i < 4; i++) {
|
||||
detection.p[i][0] = (*corners)[i * 2];
|
||||
detection.p[i][1] = (*corners)[i * 2 + 1];
|
||||
}
|
||||
}
|
||||
return detection;
|
||||
}
|
||||
|
||||
static Transform3d DoEstimateHomography(
|
||||
const apriltag_detection_t* detection,
|
||||
const AprilTagPoseEstimator::Config& config) {
|
||||
auto info = MakeDetectionInfo(detection, config);
|
||||
apriltag_pose_t pose;
|
||||
estimate_pose_for_tag_homography(&info, &pose);
|
||||
return MakePose(pose);
|
||||
}
|
||||
|
||||
Transform3d AprilTagPoseEstimator::EstimateHomography(
|
||||
const AprilTagDetection& detection) const {
|
||||
return DoEstimateHomography(
|
||||
reinterpret_cast<const apriltag_detection_t*>(&detection), m_config);
|
||||
}
|
||||
|
||||
Transform3d AprilTagPoseEstimator::EstimateHomography(
|
||||
std::span<const double, 9> homography) const {
|
||||
auto detection = MakeBasicDet(homography, nullptr);
|
||||
auto rv = DoEstimateHomography(&detection, m_config);
|
||||
matd_destroy(detection.H);
|
||||
return rv;
|
||||
}
|
||||
|
||||
static AprilTagPoseEstimate DoEstimateOrthogonalIteration(
|
||||
const apriltag_detection_t* detection,
|
||||
const AprilTagPoseEstimator::Config& config, int nIters) {
|
||||
auto info = MakeDetectionInfo(detection, config);
|
||||
apriltag_pose_t pose1, pose2;
|
||||
double err1, err2;
|
||||
estimate_tag_pose_orthogonal_iteration(&info, &err1, &pose1, &err2, &pose2,
|
||||
nIters);
|
||||
return {MakePose(pose1), MakePose(pose2), err1, err2};
|
||||
}
|
||||
|
||||
AprilTagPoseEstimate AprilTagPoseEstimator::EstimateOrthogonalIteration(
|
||||
const AprilTagDetection& detection, int nIters) const {
|
||||
return DoEstimateOrthogonalIteration(
|
||||
reinterpret_cast<const apriltag_detection_t*>(&detection), m_config,
|
||||
nIters);
|
||||
}
|
||||
|
||||
AprilTagPoseEstimate AprilTagPoseEstimator::EstimateOrthogonalIteration(
|
||||
std::span<const double, 9> homography, std::span<const double, 8> corners,
|
||||
int nIters) const {
|
||||
auto detection = MakeBasicDet(homography, &corners);
|
||||
auto rv = DoEstimateOrthogonalIteration(&detection, m_config, nIters);
|
||||
matd_destroy(detection.H);
|
||||
return rv;
|
||||
}
|
||||
|
||||
static Transform3d DoEstimate(const apriltag_detection_t* detection,
|
||||
const AprilTagPoseEstimator::Config& config) {
|
||||
auto info = MakeDetectionInfo(detection, config);
|
||||
apriltag_pose_t pose;
|
||||
estimate_tag_pose(&info, &pose);
|
||||
return MakePose(pose);
|
||||
}
|
||||
|
||||
Transform3d AprilTagPoseEstimator::Estimate(
|
||||
const AprilTagDetection& detection) const {
|
||||
return DoEstimate(reinterpret_cast<const apriltag_detection_t*>(&detection),
|
||||
m_config);
|
||||
}
|
||||
|
||||
Transform3d AprilTagPoseEstimator::Estimate(
|
||||
std::span<const double, 9> homography,
|
||||
std::span<const double, 8> corners) const {
|
||||
auto detection = MakeBasicDet(homography, &corners);
|
||||
auto rv = DoEstimate(&detection, m_config);
|
||||
matd_destroy(detection.H);
|
||||
return rv;
|
||||
}
|
||||
595
apriltag/src/main/native/cpp/jni/AprilTagJNI.cpp
Normal file
595
apriltag/src/main/native/cpp/jni/AprilTagJNI.cpp
Normal file
@@ -0,0 +1,595 @@
|
||||
// Copyright (c) FIRST and other WPILib contributors.
|
||||
// Open Source Software; you can modify and/or share it under the terms of
|
||||
// the WPILib BSD license file in the root directory of this project.
|
||||
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
|
||||
#include <wpi/jni_util.h>
|
||||
|
||||
#include "edu_wpi_first_apriltag_jni_AprilTagJNI.h"
|
||||
#include "frc/apriltag/AprilTagDetector.h"
|
||||
#include "frc/apriltag/AprilTagPoseEstimator.h"
|
||||
|
||||
using namespace frc;
|
||||
using namespace wpi::java;
|
||||
|
||||
static JavaVM* jvm = nullptr;
|
||||
|
||||
static JClass detectionCls;
|
||||
static JClass detectorConfigCls;
|
||||
static JClass detectorQTPCls;
|
||||
static JClass poseEstimateCls;
|
||||
static JClass quaternionCls;
|
||||
static JClass rotation3dCls;
|
||||
static JClass transform3dCls;
|
||||
static JClass translation3dCls;
|
||||
static JException illegalArgEx;
|
||||
static JException nullPointerEx;
|
||||
|
||||
static const JClassInit classes[] = {
|
||||
{"edu/wpi/first/apriltag/AprilTagDetection", &detectionCls},
|
||||
{"edu/wpi/first/apriltag/AprilTagDetector$Config", &detectorConfigCls},
|
||||
{"edu/wpi/first/apriltag/AprilTagDetector$QuadThresholdParameters",
|
||||
&detectorQTPCls},
|
||||
{"edu/wpi/first/apriltag/AprilTagPoseEstimate", &poseEstimateCls},
|
||||
{"edu/wpi/first/math/geometry/Quaternion", &quaternionCls},
|
||||
{"edu/wpi/first/math/geometry/Rotation3d", &rotation3dCls},
|
||||
{"edu/wpi/first/math/geometry/Transform3d", &transform3dCls},
|
||||
{"edu/wpi/first/math/geometry/Translation3d", &translation3dCls}};
|
||||
|
||||
static const JExceptionInit exceptions[] = {
|
||||
{"java/lang/IllegalArgumentException", &illegalArgEx},
|
||||
{"java/lang/NullPointerException", &nullPointerEx}};
|
||||
|
||||
extern "C" {
|
||||
|
||||
JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void* reserved) {
|
||||
jvm = vm;
|
||||
|
||||
JNIEnv* env;
|
||||
if (vm->GetEnv(reinterpret_cast<void**>(&env), JNI_VERSION_1_6) != JNI_OK) {
|
||||
return JNI_ERR;
|
||||
}
|
||||
|
||||
// Cache references to classes
|
||||
for (auto& c : classes) {
|
||||
*c.cls = JClass(env, c.name);
|
||||
if (!*c.cls) {
|
||||
std::fprintf(stderr, "could not load class %s\n", c.name);
|
||||
return JNI_ERR;
|
||||
}
|
||||
}
|
||||
|
||||
for (auto& c : exceptions) {
|
||||
*c.cls = JException(env, c.name);
|
||||
if (!*c.cls) {
|
||||
std::fprintf(stderr, "could not load exception %s\n", c.name);
|
||||
return JNI_ERR;
|
||||
}
|
||||
}
|
||||
|
||||
return JNI_VERSION_1_6;
|
||||
}
|
||||
|
||||
JNIEXPORT void JNICALL JNI_OnUnload(JavaVM* vm, void* reserved) {
|
||||
JNIEnv* env;
|
||||
if (vm->GetEnv(reinterpret_cast<void**>(&env), JNI_VERSION_1_6) != JNI_OK) {
|
||||
return;
|
||||
}
|
||||
// Delete global references
|
||||
for (auto& c : classes) {
|
||||
c.cls->free(env);
|
||||
}
|
||||
for (auto& c : exceptions) {
|
||||
c.cls->free(env);
|
||||
}
|
||||
jvm = nullptr;
|
||||
}
|
||||
|
||||
} // extern "C"
|
||||
|
||||
//
|
||||
// Conversions from Java to C++ objects
|
||||
//
|
||||
|
||||
static AprilTagDetector::Config FromJavaDetectorConfig(JNIEnv* env,
|
||||
jobject jconfig) {
|
||||
if (!jconfig) {
|
||||
return {};
|
||||
}
|
||||
#define FIELD(name, sig) \
|
||||
static jfieldID name##Field = nullptr; \
|
||||
if (!name##Field) { \
|
||||
name##Field = env->GetFieldID(detectorConfigCls, #name, sig); \
|
||||
}
|
||||
|
||||
FIELD(numThreads, "I");
|
||||
FIELD(quadDecimate, "F");
|
||||
FIELD(quadSigma, "F");
|
||||
FIELD(refineEdges, "Z");
|
||||
FIELD(decodeSharpening, "D");
|
||||
FIELD(debug, "Z");
|
||||
|
||||
#undef FIELD
|
||||
|
||||
#define FIELD(ctype, jtype, name) \
|
||||
.name = static_cast<ctype>(env->Get##jtype##Field(jconfig, name##Field))
|
||||
|
||||
return {
|
||||
FIELD(int, Int, numThreads),
|
||||
FIELD(float, Float, quadDecimate),
|
||||
FIELD(float, Float, quadSigma),
|
||||
FIELD(bool, Boolean, refineEdges),
|
||||
FIELD(double, Double, decodeSharpening),
|
||||
FIELD(bool, Boolean, debug),
|
||||
};
|
||||
|
||||
#undef GET
|
||||
#undef FIELD
|
||||
}
|
||||
|
||||
static AprilTagDetector::QuadThresholdParameters FromJavaDetectorQTP(
|
||||
JNIEnv* env, jobject jparams) {
|
||||
if (!jparams) {
|
||||
return {};
|
||||
}
|
||||
#define FIELD(name, sig) \
|
||||
static jfieldID name##Field = nullptr; \
|
||||
if (!name##Field) { \
|
||||
name##Field = env->GetFieldID(detectorQTPCls, #name, sig); \
|
||||
}
|
||||
|
||||
FIELD(minClusterPixels, "I");
|
||||
FIELD(maxNumMaxima, "I");
|
||||
FIELD(criticalAngle, "D");
|
||||
FIELD(maxLineFitMSE, "F");
|
||||
FIELD(minWhiteBlackDiff, "I");
|
||||
FIELD(deglitch, "Z");
|
||||
|
||||
#undef FIELD
|
||||
|
||||
#define FIELD(ctype, jtype, name) \
|
||||
.name = static_cast<ctype>(env->Get##jtype##Field(jparams, name##Field))
|
||||
|
||||
return {
|
||||
FIELD(int, Int, minClusterPixels),
|
||||
FIELD(int, Int, maxNumMaxima),
|
||||
.criticalAngle = units::radian_t{static_cast<double>(
|
||||
env->GetDoubleField(jparams, criticalAngleField))},
|
||||
FIELD(float, Float, maxLineFitMSE),
|
||||
FIELD(int, Int, minWhiteBlackDiff),
|
||||
FIELD(bool, Boolean, deglitch),
|
||||
};
|
||||
|
||||
#undef GET
|
||||
#undef FIELD
|
||||
}
|
||||
|
||||
//
|
||||
// Conversions from C++ to Java objects
|
||||
//
|
||||
|
||||
static jobject MakeJObject(JNIEnv* env, const AprilTagDetection& detect) {
|
||||
static jmethodID constructor = env->GetMethodID(
|
||||
detectionCls, "<init>", "(Ljava/lang/String;IIF[DDD[D)V");
|
||||
if (!constructor) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
JLocal<jstring> fam{env, MakeJString(env, detect.GetFamily())};
|
||||
|
||||
auto homography = detect.GetHomography();
|
||||
JLocal<jdoubleArray> harr{
|
||||
env, MakeJDoubleArray(
|
||||
env, {reinterpret_cast<const jdouble*>(homography.data()),
|
||||
homography.size()})};
|
||||
|
||||
double cornersBuf[8];
|
||||
auto corners = detect.GetCorners(cornersBuf);
|
||||
JLocal<jdoubleArray> carr{
|
||||
env,
|
||||
MakeJDoubleArray(env, {reinterpret_cast<const jdouble*>(corners.data()),
|
||||
corners.size()})};
|
||||
|
||||
auto center = detect.GetCenter();
|
||||
|
||||
return env->NewObject(detectionCls, constructor, fam.obj(),
|
||||
static_cast<jint>(detect.GetId()),
|
||||
static_cast<jint>(detect.GetHamming()),
|
||||
static_cast<jfloat>(detect.GetDecisionMargin()),
|
||||
harr.obj(), static_cast<jdouble>(center.x),
|
||||
static_cast<jdouble>(center.y), carr.obj());
|
||||
}
|
||||
|
||||
static jobjectArray MakeJObject(JNIEnv* env,
|
||||
std::span<const AprilTagDetection* const> arr) {
|
||||
jobjectArray jarr = env->NewObjectArray(arr.size(), detectionCls, nullptr);
|
||||
if (!jarr) {
|
||||
return nullptr;
|
||||
}
|
||||
for (size_t i = 0; i < arr.size(); ++i) {
|
||||
JLocal<jobject> elem{env, MakeJObject(env, *arr[i])};
|
||||
env->SetObjectArrayElement(jarr, i, elem.obj());
|
||||
}
|
||||
return jarr;
|
||||
}
|
||||
|
||||
static jobject MakeJObject(JNIEnv* env,
|
||||
const AprilTagDetector::Config& config) {
|
||||
static jmethodID constructor =
|
||||
env->GetMethodID(detectorConfigCls, "<init>", "(IFFZDZ)V");
|
||||
if (!constructor) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
return env->NewObject(detectorConfigCls, constructor,
|
||||
static_cast<jint>(config.numThreads),
|
||||
static_cast<jfloat>(config.quadDecimate),
|
||||
static_cast<jfloat>(config.quadSigma),
|
||||
static_cast<jboolean>(config.refineEdges),
|
||||
static_cast<jdouble>(config.decodeSharpening),
|
||||
static_cast<jboolean>(config.debug));
|
||||
}
|
||||
|
||||
static jobject MakeJObject(
|
||||
JNIEnv* env, const AprilTagDetector::QuadThresholdParameters& params) {
|
||||
static jmethodID constructor =
|
||||
env->GetMethodID(detectorQTPCls, "<init>", "(IIDFIZ)V");
|
||||
if (!constructor) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
return env->NewObject(detectorQTPCls, constructor,
|
||||
static_cast<jint>(params.minClusterPixels),
|
||||
static_cast<jint>(params.maxNumMaxima),
|
||||
static_cast<jdouble>(params.criticalAngle),
|
||||
static_cast<jfloat>(params.maxLineFitMSE),
|
||||
static_cast<jint>(params.minWhiteBlackDiff),
|
||||
static_cast<jboolean>(params.deglitch));
|
||||
}
|
||||
|
||||
static jobject MakeJObject(JNIEnv* env, const Translation3d& xlate) {
|
||||
static jmethodID constructor =
|
||||
env->GetMethodID(translation3dCls, "<init>", "(DDD)V");
|
||||
if (!constructor) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
return env->NewObject(
|
||||
translation3dCls, constructor, static_cast<jdouble>(xlate.X()),
|
||||
static_cast<jdouble>(xlate.Y()), static_cast<jdouble>(xlate.Z()));
|
||||
}
|
||||
|
||||
static jobject MakeJObject(JNIEnv* env, const Quaternion& q) {
|
||||
static jmethodID constructor =
|
||||
env->GetMethodID(quaternionCls, "<init>", "(DDDD)V");
|
||||
if (!constructor) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
return env->NewObject(quaternionCls, constructor, static_cast<jdouble>(q.W()),
|
||||
static_cast<jdouble>(q.X()),
|
||||
static_cast<jdouble>(q.Y()),
|
||||
static_cast<jdouble>(q.Z()));
|
||||
}
|
||||
|
||||
static jobject MakeJObject(JNIEnv* env, const Rotation3d& rot) {
|
||||
static jmethodID constructor = env->GetMethodID(
|
||||
rotation3dCls, "<init>", "(Ledu/wpi/first/math/geometry/Quaternion;)V");
|
||||
if (!constructor) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
JLocal<jobject> q{env, MakeJObject(env, rot.GetQuaternion())};
|
||||
return env->NewObject(rotation3dCls, constructor, q.obj());
|
||||
}
|
||||
|
||||
static jobject MakeJObject(JNIEnv* env, const Transform3d& xform) {
|
||||
static jmethodID constructor =
|
||||
env->GetMethodID(transform3dCls, "<init>",
|
||||
"(Ledu/wpi/first/math/geometry/Translation3d;"
|
||||
"Ledu/wpi/first/math/geometry/Rotation3d;)V");
|
||||
if (!constructor) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
JLocal<jobject> xlate{env, MakeJObject(env, xform.Translation())};
|
||||
JLocal<jobject> rot{env, MakeJObject(env, xform.Rotation())};
|
||||
return env->NewObject(transform3dCls, constructor, xlate.obj(), rot.obj());
|
||||
}
|
||||
|
||||
static jobject MakeJObject(JNIEnv* env, const AprilTagPoseEstimate& est) {
|
||||
static jmethodID constructor =
|
||||
env->GetMethodID(poseEstimateCls, "<init>",
|
||||
"(Ledu/wpi/first/math/geometry/Transform3d;"
|
||||
"Ledu/wpi/first/math/geometry/Transform3d;DD)V");
|
||||
if (!constructor) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
JLocal<jobject> pose1{env, MakeJObject(env, est.pose1)};
|
||||
JLocal<jobject> pose2{env, MakeJObject(env, est.pose2)};
|
||||
return env->NewObject(poseEstimateCls, constructor, pose1.obj(), pose2.obj(),
|
||||
static_cast<jdouble>(est.error1),
|
||||
static_cast<jdouble>(est.error2));
|
||||
}
|
||||
|
||||
extern "C" {
|
||||
|
||||
/*
|
||||
* Class: edu_wpi_first_apriltag_jni_AprilTagJNI
|
||||
* Method: createDetector
|
||||
* Signature: ()J
|
||||
*/
|
||||
JNIEXPORT jlong JNICALL
|
||||
Java_edu_wpi_first_apriltag_jni_AprilTagJNI_createDetector
|
||||
(JNIEnv* env, jclass)
|
||||
{
|
||||
return reinterpret_cast<jlong>(new AprilTagDetector);
|
||||
}
|
||||
|
||||
/*
|
||||
* Class: edu_wpi_first_apriltag_jni_AprilTagJNI
|
||||
* Method: destroyDetector
|
||||
* Signature: (J)V
|
||||
*/
|
||||
JNIEXPORT void JNICALL
|
||||
Java_edu_wpi_first_apriltag_jni_AprilTagJNI_destroyDetector
|
||||
(JNIEnv* env, jclass, jlong det)
|
||||
{
|
||||
delete reinterpret_cast<AprilTagDetector*>(det);
|
||||
}
|
||||
|
||||
/*
|
||||
* Class: edu_wpi_first_apriltag_jni_AprilTagJNI
|
||||
* Method: setDetectorConfig
|
||||
* Signature: (JLjava/lang/Object;)V
|
||||
*/
|
||||
JNIEXPORT void JNICALL
|
||||
Java_edu_wpi_first_apriltag_jni_AprilTagJNI_setDetectorConfig
|
||||
(JNIEnv* env, jclass, jlong det, jobject config)
|
||||
{
|
||||
if (det == 0) {
|
||||
nullPointerEx.Throw(env, "det cannot be null");
|
||||
return;
|
||||
}
|
||||
reinterpret_cast<AprilTagDetector*>(det)->SetConfig(
|
||||
FromJavaDetectorConfig(env, config));
|
||||
}
|
||||
|
||||
/*
|
||||
* Class: edu_wpi_first_apriltag_jni_AprilTagJNI
|
||||
* Method: getDetectorConfig
|
||||
* Signature: (J)Ljava/lang/Object;
|
||||
*/
|
||||
JNIEXPORT jobject JNICALL
|
||||
Java_edu_wpi_first_apriltag_jni_AprilTagJNI_getDetectorConfig
|
||||
(JNIEnv* env, jclass, jlong det)
|
||||
{
|
||||
if (det == 0) {
|
||||
nullPointerEx.Throw(env, "det cannot be null");
|
||||
return nullptr;
|
||||
}
|
||||
return MakeJObject(env,
|
||||
reinterpret_cast<AprilTagDetector*>(det)->GetConfig());
|
||||
}
|
||||
|
||||
/*
|
||||
* Class: edu_wpi_first_apriltag_jni_AprilTagJNI
|
||||
* Method: setDetectorQTP
|
||||
* Signature: (JLjava/lang/Object;)V
|
||||
*/
|
||||
JNIEXPORT void JNICALL
|
||||
Java_edu_wpi_first_apriltag_jni_AprilTagJNI_setDetectorQTP
|
||||
(JNIEnv* env, jclass, jlong det, jobject params)
|
||||
{
|
||||
if (det == 0) {
|
||||
nullPointerEx.Throw(env, "det cannot be null");
|
||||
return;
|
||||
}
|
||||
reinterpret_cast<AprilTagDetector*>(det)->SetQuadThresholdParameters(
|
||||
FromJavaDetectorQTP(env, params));
|
||||
}
|
||||
|
||||
/*
|
||||
* Class: edu_wpi_first_apriltag_jni_AprilTagJNI
|
||||
* Method: getDetectorQTP
|
||||
* Signature: (J)Ljava/lang/Object;
|
||||
*/
|
||||
JNIEXPORT jobject JNICALL
|
||||
Java_edu_wpi_first_apriltag_jni_AprilTagJNI_getDetectorQTP
|
||||
(JNIEnv* env, jclass, jlong det)
|
||||
{
|
||||
if (det == 0) {
|
||||
nullPointerEx.Throw(env, "det cannot be null");
|
||||
return nullptr;
|
||||
}
|
||||
return MakeJObject(
|
||||
env,
|
||||
reinterpret_cast<AprilTagDetector*>(det)->GetQuadThresholdParameters());
|
||||
}
|
||||
|
||||
/*
|
||||
* Class: edu_wpi_first_apriltag_jni_AprilTagJNI
|
||||
* Method: addFamily
|
||||
* Signature: (JLjava/lang/String;I)Z
|
||||
*/
|
||||
JNIEXPORT jboolean JNICALL
|
||||
Java_edu_wpi_first_apriltag_jni_AprilTagJNI_addFamily
|
||||
(JNIEnv* env, jclass, jlong det, jstring fam, jint bitsCorrected)
|
||||
{
|
||||
if (det == 0) {
|
||||
nullPointerEx.Throw(env, "det cannot be null");
|
||||
return false;
|
||||
}
|
||||
if (!fam) {
|
||||
nullPointerEx.Throw(env, "fam cannot be null");
|
||||
return false;
|
||||
}
|
||||
return reinterpret_cast<AprilTagDetector*>(det)->AddFamily(
|
||||
JStringRef{env, fam}, bitsCorrected);
|
||||
}
|
||||
|
||||
/*
|
||||
* Class: edu_wpi_first_apriltag_jni_AprilTagJNI
|
||||
* Method: removeFamily
|
||||
* Signature: (JLjava/lang/String;)V
|
||||
*/
|
||||
JNIEXPORT void JNICALL
|
||||
Java_edu_wpi_first_apriltag_jni_AprilTagJNI_removeFamily
|
||||
(JNIEnv* env, jclass, jlong det, jstring fam)
|
||||
{
|
||||
if (det == 0) {
|
||||
nullPointerEx.Throw(env, "det cannot be null");
|
||||
return;
|
||||
}
|
||||
if (!fam) {
|
||||
nullPointerEx.Throw(env, "fam cannot be null");
|
||||
return;
|
||||
}
|
||||
reinterpret_cast<AprilTagDetector*>(det)->RemoveFamily(JStringRef{env, fam});
|
||||
}
|
||||
|
||||
/*
|
||||
* Class: edu_wpi_first_apriltag_jni_AprilTagJNI
|
||||
* Method: clearFamilies
|
||||
* Signature: (J)V
|
||||
*/
|
||||
JNIEXPORT void JNICALL
|
||||
Java_edu_wpi_first_apriltag_jni_AprilTagJNI_clearFamilies
|
||||
(JNIEnv* env, jclass, jlong det)
|
||||
{
|
||||
if (det == 0) {
|
||||
nullPointerEx.Throw(env, "det cannot be null");
|
||||
return;
|
||||
}
|
||||
reinterpret_cast<AprilTagDetector*>(det)->ClearFamilies();
|
||||
}
|
||||
|
||||
/*
|
||||
* Class: edu_wpi_first_apriltag_jni_AprilTagJNI
|
||||
* Method: detect
|
||||
* Signature: (JIIIJ)[Ljava/lang/Object;
|
||||
*/
|
||||
JNIEXPORT jobjectArray JNICALL
|
||||
Java_edu_wpi_first_apriltag_jni_AprilTagJNI_detect
|
||||
(JNIEnv* env, jclass, jlong det, jint width, jint height, jint stride,
|
||||
jlong bufAddr)
|
||||
{
|
||||
if (det == 0) {
|
||||
nullPointerEx.Throw(env, "det cannot be null");
|
||||
return nullptr;
|
||||
}
|
||||
if (bufAddr == 0) {
|
||||
nullPointerEx.Throw(env, "bufAddr cannot be null");
|
||||
return nullptr;
|
||||
}
|
||||
return MakeJObject(
|
||||
env, reinterpret_cast<AprilTagDetector*>(det)->Detect(
|
||||
width, height, stride, reinterpret_cast<uint8_t*>(bufAddr)));
|
||||
}
|
||||
|
||||
/*
|
||||
* Class: edu_wpi_first_apriltag_jni_AprilTagJNI
|
||||
* Method: estimatePoseHomography
|
||||
* Signature: ([DDDDDD)Ljava/lang/Object;
|
||||
*/
|
||||
JNIEXPORT jobject JNICALL
|
||||
Java_edu_wpi_first_apriltag_jni_AprilTagJNI_estimatePoseHomography
|
||||
(JNIEnv* env, jclass, jdoubleArray homography, jdouble tagSize, jdouble fx,
|
||||
jdouble fy, jdouble cx, jdouble cy)
|
||||
{
|
||||
if (!homography) {
|
||||
nullPointerEx.Throw(env, "homography cannot be null");
|
||||
return nullptr;
|
||||
}
|
||||
JDoubleArrayRef harr{env, homography};
|
||||
if (harr.size() != 9) {
|
||||
illegalArgEx.Throw(env, "homography array must be size 9");
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
AprilTagPoseEstimator estimator({units::meter_t{tagSize}, fx, fy, cx, cy});
|
||||
return MakeJObject(env, estimator.EstimateHomography(
|
||||
std::span<const double, 9>{harr.array()}));
|
||||
}
|
||||
|
||||
/*
|
||||
* Class: edu_wpi_first_apriltag_jni_AprilTagJNI
|
||||
* Method: estimatePoseOrthogonalIteration
|
||||
* Signature: ([D[DDDDDDI)Ljava/lang/Object;
|
||||
*/
|
||||
JNIEXPORT jobject JNICALL
|
||||
Java_edu_wpi_first_apriltag_jni_AprilTagJNI_estimatePoseOrthogonalIteration
|
||||
(JNIEnv* env, jclass, jdoubleArray homography, jdoubleArray corners,
|
||||
jdouble tagSize, jdouble fx, jdouble fy, jdouble cx, jdouble cy, jint nIters)
|
||||
{
|
||||
// homography
|
||||
if (!homography) {
|
||||
nullPointerEx.Throw(env, "homography cannot be null");
|
||||
return nullptr;
|
||||
}
|
||||
JDoubleArrayRef harr{env, homography};
|
||||
if (harr.size() != 9) {
|
||||
illegalArgEx.Throw(env, "homography array must be size 9");
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// corners
|
||||
if (!corners) {
|
||||
nullPointerEx.Throw(env, "corners cannot be null");
|
||||
return nullptr;
|
||||
}
|
||||
JDoubleArrayRef carr{env, corners};
|
||||
if (carr.size() != 8) {
|
||||
illegalArgEx.Throw(env, "corners array must be size 8");
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
AprilTagPoseEstimator estimator({units::meter_t{tagSize}, fx, fy, cx, cy});
|
||||
return MakeJObject(env,
|
||||
estimator.EstimateOrthogonalIteration(
|
||||
std::span<const double, 9>{harr.array()},
|
||||
std::span<const double, 8>{carr.array()}, nIters));
|
||||
}
|
||||
|
||||
/*
|
||||
* Class: edu_wpi_first_apriltag_jni_AprilTagJNI
|
||||
* Method: estimatePose
|
||||
* Signature: ([D[DDDDDD)Ljava/lang/Object;
|
||||
*/
|
||||
JNIEXPORT jobject JNICALL
|
||||
Java_edu_wpi_first_apriltag_jni_AprilTagJNI_estimatePose
|
||||
(JNIEnv* env, jclass, jdoubleArray homography, jdoubleArray corners,
|
||||
jdouble tagSize, jdouble fx, jdouble fy, jdouble cx, jdouble cy)
|
||||
{
|
||||
// homography
|
||||
if (!homography) {
|
||||
nullPointerEx.Throw(env, "homography cannot be null");
|
||||
return nullptr;
|
||||
}
|
||||
JDoubleArrayRef harr{env, homography};
|
||||
if (harr.size() != 9) {
|
||||
illegalArgEx.Throw(env, "homography array must be size 9");
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// corners
|
||||
if (!corners) {
|
||||
nullPointerEx.Throw(env, "corners cannot be null");
|
||||
return nullptr;
|
||||
}
|
||||
JDoubleArrayRef carr{env, corners};
|
||||
if (carr.size() != 8) {
|
||||
illegalArgEx.Throw(env, "corners array must be size 8");
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
AprilTagPoseEstimator estimator({units::meter_t{tagSize}, fx, fy, cx, cy});
|
||||
return MakeJObject(
|
||||
env, estimator.Estimate(std::span<const double, 9>{harr.array()},
|
||||
std::span<const double, 8>{carr.array()}));
|
||||
}
|
||||
|
||||
} // extern "C"
|
||||
34
apriltag/src/main/native/include/frc/apriltag/AprilTag.h
Normal file
34
apriltag/src/main/native/include/frc/apriltag/AprilTag.h
Normal file
@@ -0,0 +1,34 @@
|
||||
// Copyright (c) FIRST and other WPILib contributors.
|
||||
// Open Source Software; you can modify and/or share it under the terms of
|
||||
// the WPILib BSD license file in the root directory of this project.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <wpi/SymbolExports.h>
|
||||
|
||||
#include "frc/geometry/Pose3d.h"
|
||||
|
||||
namespace wpi {
|
||||
class json;
|
||||
} // namespace wpi
|
||||
|
||||
namespace frc {
|
||||
|
||||
struct WPILIB_DLLEXPORT AprilTag {
|
||||
int ID;
|
||||
|
||||
Pose3d pose;
|
||||
|
||||
/**
|
||||
* Checks equality between this AprilTag and another object.
|
||||
*/
|
||||
bool operator==(const AprilTag&) const = default;
|
||||
};
|
||||
|
||||
WPILIB_DLLEXPORT
|
||||
void to_json(wpi::json& json, const AprilTag& apriltag);
|
||||
|
||||
WPILIB_DLLEXPORT
|
||||
void from_json(const wpi::json& json, AprilTag& apriltag);
|
||||
|
||||
} // namespace frc
|
||||
@@ -0,0 +1,160 @@
|
||||
// Copyright (c) FIRST and other WPILib contributors.
|
||||
// Open Source Software; you can modify and/or share it under the terms of
|
||||
// the WPILib BSD license file in the root directory of this project.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#include <span>
|
||||
#include <string_view>
|
||||
|
||||
#include <wpi/SymbolExports.h>
|
||||
|
||||
#include "frc/EigenCore.h"
|
||||
|
||||
namespace frc {
|
||||
|
||||
/**
|
||||
* A detection of an AprilTag tag.
|
||||
*/
|
||||
class WPILIB_DLLEXPORT AprilTagDetection final {
|
||||
public:
|
||||
AprilTagDetection() = delete;
|
||||
AprilTagDetection(const AprilTagDetection&) = delete;
|
||||
AprilTagDetection& operator=(const AprilTagDetection&) = delete;
|
||||
|
||||
/** A point. Used for center and corner points. */
|
||||
struct Point {
|
||||
double x;
|
||||
double y;
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets the decoded tag's family name.
|
||||
*
|
||||
* @return Decoded family name
|
||||
*/
|
||||
std::string_view GetFamily() const;
|
||||
|
||||
/**
|
||||
* Gets the decoded ID of the tag.
|
||||
*
|
||||
* @return Decoded ID
|
||||
*/
|
||||
int GetId() const { return id; }
|
||||
|
||||
/**
|
||||
* Gets how many error bits were corrected. Note: accepting large numbers of
|
||||
* corrected errors leads to greatly increased false positive rates.
|
||||
* NOTE: As of this implementation, the detector cannot detect tags with
|
||||
* a hamming distance greater than 2.
|
||||
*
|
||||
* @return Hamming distance (number of corrected error bits)
|
||||
*/
|
||||
int GetHamming() const { return hamming; }
|
||||
|
||||
/**
|
||||
* Gets a measure of the quality of the binary decoding process: the
|
||||
* average difference between the intensity of a data bit versus
|
||||
* the decision threshold. Higher numbers roughly indicate better
|
||||
* decodes. This is a reasonable measure of detection accuracy
|
||||
* only for very small tags-- not effective for larger tags (where
|
||||
* we could have sampled anywhere within a bit cell and still
|
||||
* gotten a good detection.)
|
||||
*
|
||||
* @return Decision margin
|
||||
*/
|
||||
float GetDecisionMargin() const { return decision_margin; }
|
||||
|
||||
/**
|
||||
* Gets the 3x3 homography matrix describing the projection from an
|
||||
* "ideal" tag (with corners at (-1,1), (1,1), (1,-1), and (-1,
|
||||
* -1)) to pixels in the image.
|
||||
*
|
||||
* @return Homography matrix data
|
||||
*/
|
||||
std::span<const double, 9> GetHomography() const;
|
||||
|
||||
/**
|
||||
* Gets the 3x3 homography matrix describing the projection from an
|
||||
* "ideal" tag (with corners at (-1,1), (1,1), (1,-1), and (-1,
|
||||
* -1)) to pixels in the image.
|
||||
*
|
||||
* @return Homography matrix
|
||||
*/
|
||||
Eigen::Matrix3d GetHomographyMatrix() const;
|
||||
|
||||
/**
|
||||
* Gets the center of the detection in image pixel coordinates.
|
||||
*
|
||||
* @return Center point
|
||||
*/
|
||||
const Point& GetCenter() const { return *reinterpret_cast<const Point*>(c); }
|
||||
|
||||
/**
|
||||
* Gets a corner of the tag in image pixel coordinates. These always
|
||||
* wrap counter-clock wise around the tag.
|
||||
*
|
||||
* @param ndx Corner index (range is 0-3, inclusive)
|
||||
* @return Corner point
|
||||
*/
|
||||
const Point& GetCorner(int ndx) const {
|
||||
return *reinterpret_cast<const Point*>(p[ndx]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the corners of the tag in image pixel coordinates. These always
|
||||
* wrap counter-clock wise around the tag.
|
||||
*
|
||||
* @param cornersBuf Corner point array (X and Y for each corner in order)
|
||||
* @return Corner point array (copy of cornersBuf span)
|
||||
*/
|
||||
std::span<double, 8> GetCorners(std::span<double, 8> cornersBuf) const {
|
||||
for (int i = 0; i < 4; i++) {
|
||||
cornersBuf[i * 2] = p[i][0];
|
||||
cornersBuf[i * 2 + 1] = p[i][1];
|
||||
}
|
||||
return cornersBuf;
|
||||
}
|
||||
|
||||
private:
|
||||
// This class *must* be standard-layout-compatible with apriltag_detection
|
||||
// as we use reinterpret_cast from that structure. This means the below
|
||||
// members must exactly match the contents of the apriltag_detection struct.
|
||||
|
||||
// The tag family.
|
||||
void* family;
|
||||
|
||||
// The decoded ID of the tag.
|
||||
int id;
|
||||
|
||||
// How many error bits were corrected? Note: accepting large numbers of
|
||||
// corrected errors leads to greatly increased false positive rates.
|
||||
// NOTE: As of this implementation, the detector cannot detect tags with
|
||||
// a hamming distance greater than 2.
|
||||
int hamming;
|
||||
|
||||
// A measure of the quality of the binary decoding process: the
|
||||
// average difference between the intensity of a data bit versus
|
||||
// the decision threshold. Higher numbers roughly indicate better
|
||||
// decodes. This is a reasonable measure of detection accuracy
|
||||
// only for very small tags-- not effective for larger tags (where
|
||||
// we could have sampled anywhere within a bit cell and still
|
||||
// gotten a good detection.)
|
||||
float decision_margin;
|
||||
|
||||
// The 3x3 homography matrix describing the projection from an
|
||||
// "ideal" tag (with corners at (-1,1), (1,1), (1,-1), and (-1,
|
||||
// -1)) to pixels in the image.
|
||||
void* H;
|
||||
|
||||
// The center of the detection in image pixel coordinates.
|
||||
double c[2];
|
||||
|
||||
// The corners of the tag in image pixel coordinates. These always
|
||||
// wrap counter-clock wise around the tag.
|
||||
double p[4][2];
|
||||
};
|
||||
|
||||
} // namespace frc
|
||||
260
apriltag/src/main/native/include/frc/apriltag/AprilTagDetector.h
Normal file
260
apriltag/src/main/native/include/frc/apriltag/AprilTagDetector.h
Normal file
@@ -0,0 +1,260 @@
|
||||
// Copyright (c) FIRST and other WPILib contributors.
|
||||
// Open Source Software; you can modify and/or share it under the terms of
|
||||
// the WPILib BSD license file in the root directory of this project.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#include <memory>
|
||||
#include <span>
|
||||
#include <string_view>
|
||||
#include <utility>
|
||||
|
||||
#include <units/angle.h>
|
||||
#include <wpi/StringMap.h>
|
||||
#include <wpi/SymbolExports.h>
|
||||
|
||||
#include "frc/apriltag/AprilTagDetection.h"
|
||||
|
||||
namespace frc {
|
||||
|
||||
/**
|
||||
* An AprilTag detector engine. This is expensive to set up and tear down, so
|
||||
* most use cases should only create one of these, add a family to it, set up
|
||||
* any other configuration, and repeatedly call Detect().
|
||||
*/
|
||||
class WPILIB_DLLEXPORT AprilTagDetector {
|
||||
public:
|
||||
/** Detector configuration. */
|
||||
struct Config {
|
||||
bool operator==(const Config&) const = default;
|
||||
|
||||
/**
|
||||
* How many threads should be used for computation. Default is
|
||||
* single-threaded operation (1 thread).
|
||||
*/
|
||||
int numThreads = 1;
|
||||
|
||||
/**
|
||||
* Quad decimation. Detection of quads can be done on a lower-resolution
|
||||
* image, improving speed at a cost of pose accuracy and a slight decrease
|
||||
* in detection rate. Decoding the binary payload is still done at full
|
||||
* resolution. Default is 2.0.
|
||||
*/
|
||||
float quadDecimate = 2.0f;
|
||||
|
||||
/**
|
||||
* What Gaussian blur should be applied to the segmented image (used for
|
||||
* quad detection). Very noisy images benefit from non-zero values (e.g.
|
||||
* 0.8). Default is 0.0.
|
||||
*/
|
||||
float quadSigma = 0.0f;
|
||||
|
||||
/**
|
||||
* When true, the edges of the each quad are adjusted to "snap to" strong
|
||||
* gradients nearby. This is useful when decimation is employed, as it can
|
||||
* increase the quality of the initial quad estimate substantially.
|
||||
* Generally recommended to be on (true). Default is true.
|
||||
*
|
||||
* Very computationally inexpensive. Option is ignored if
|
||||
* quad_decimate = 1.
|
||||
*/
|
||||
bool refineEdges = true;
|
||||
|
||||
/**
|
||||
* How much sharpening should be done to decoded images. This can help
|
||||
* decode small tags but may or may not help in odd lighting conditions or
|
||||
* low light conditions. Default is 0.25.
|
||||
*/
|
||||
double decodeSharpening = 0.25;
|
||||
|
||||
/**
|
||||
* Debug mode. When true, the decoder writes a variety of debugging images
|
||||
* to the current working directory at various stages through the detection
|
||||
* process. This is slow and should *not* be used on space-limited systems
|
||||
* such as the RoboRIO. Default is disabled (false).
|
||||
*/
|
||||
bool debug = false;
|
||||
};
|
||||
|
||||
/** Quad threshold parameters. */
|
||||
struct QuadThresholdParameters {
|
||||
bool operator==(const QuadThresholdParameters&) const = default;
|
||||
|
||||
/**
|
||||
* Threshold used to reject quads containing too few pixels. Default is 5
|
||||
* pixels.
|
||||
*/
|
||||
int minClusterPixels = 5;
|
||||
|
||||
/**
|
||||
* How many corner candidates to consider when segmenting a group of pixels
|
||||
* into a quad. Default is 10.
|
||||
*/
|
||||
int maxNumMaxima = 10;
|
||||
|
||||
/**
|
||||
* Critical angle. The detector will reject quads where pairs of edges have
|
||||
* angles that are close to straight or close to 180 degrees. Zero means
|
||||
* that no quads are rejected. Default is 10 degrees.
|
||||
*/
|
||||
units::radian_t criticalAngle = 10_deg;
|
||||
|
||||
/**
|
||||
* When fitting lines to the contours, the maximum mean squared error
|
||||
* allowed. This is useful in rejecting contours that are far from being
|
||||
* quad shaped; rejecting these quads "early" saves expensive decoding
|
||||
* processing. Default is 10.0.
|
||||
*/
|
||||
float maxLineFitMSE = 10.0f;
|
||||
|
||||
/**
|
||||
* Minimum brightness offset. When we build our model of black & white
|
||||
* pixels, we add an extra check that the white model must be (overall)
|
||||
* brighter than the black model. How much brighter? (in pixel values,
|
||||
* [0,255]). Default is 5.
|
||||
*/
|
||||
int minWhiteBlackDiff = 5;
|
||||
|
||||
/**
|
||||
* Whether the thresholded image be should be deglitched. Only useful for
|
||||
* very noisy images. Default is disabled (false).
|
||||
*/
|
||||
bool deglitch = false;
|
||||
};
|
||||
|
||||
/**
|
||||
* Array of detection results. Each array element is a pointer to an
|
||||
* AprilTagDetection.
|
||||
*/
|
||||
class WPILIB_DLLEXPORT Results
|
||||
: public std::span<AprilTagDetection const* const> {
|
||||
struct private_init {};
|
||||
friend class AprilTagDetector;
|
||||
|
||||
public:
|
||||
Results() = default;
|
||||
Results(void* impl, const private_init&);
|
||||
~Results() { Destroy(); }
|
||||
Results(const Results&) = delete;
|
||||
Results& operator=(const Results&) = delete;
|
||||
Results(Results&& rhs) : span{std::move(rhs)}, m_impl{rhs.m_impl} {
|
||||
rhs.m_impl = nullptr;
|
||||
}
|
||||
Results& operator=(Results&& rhs);
|
||||
|
||||
private:
|
||||
void Destroy();
|
||||
void* m_impl = nullptr;
|
||||
};
|
||||
|
||||
AprilTagDetector();
|
||||
~AprilTagDetector() { Destroy(); }
|
||||
AprilTagDetector(const AprilTagDetector&) = delete;
|
||||
AprilTagDetector& operator=(const AprilTagDetector&) = delete;
|
||||
AprilTagDetector(AprilTagDetector&& rhs)
|
||||
: m_impl{rhs.m_impl},
|
||||
m_families{std::move(rhs.m_families)},
|
||||
m_qtpCriticalAngle{rhs.m_qtpCriticalAngle} {
|
||||
rhs.m_impl = nullptr;
|
||||
}
|
||||
AprilTagDetector& operator=(AprilTagDetector&& rhs);
|
||||
|
||||
/**
|
||||
* @{
|
||||
* @name Configuration functions
|
||||
*/
|
||||
|
||||
/**
|
||||
* Sets detector configuration.
|
||||
*
|
||||
* @param config Configuration
|
||||
*/
|
||||
void SetConfig(const Config& config);
|
||||
|
||||
/**
|
||||
* Gets detector configuration.
|
||||
*
|
||||
* @return Configuration
|
||||
*/
|
||||
Config GetConfig() const;
|
||||
|
||||
/**
|
||||
* Sets quad threshold parameters.
|
||||
*
|
||||
* @param params Parameters
|
||||
*/
|
||||
void SetQuadThresholdParameters(const QuadThresholdParameters& params);
|
||||
|
||||
/**
|
||||
* Gets quad threshold parameters.
|
||||
*
|
||||
* @return Parameters
|
||||
*/
|
||||
QuadThresholdParameters GetQuadThresholdParameters() const;
|
||||
|
||||
/** @} */
|
||||
|
||||
/**
|
||||
* @{
|
||||
* @name Tag family functions
|
||||
*/
|
||||
|
||||
/**
|
||||
* Adds a family of tags to be detected.
|
||||
*
|
||||
* @param fam Family name, e.g. "tag16h5"
|
||||
* @param bitsCorrected
|
||||
* @return False if family can't be found
|
||||
*/
|
||||
bool AddFamily(std::string_view fam, int bitsCorrected = 2);
|
||||
|
||||
/**
|
||||
* Removes a family of tags from the detector.
|
||||
*
|
||||
* @param fam Family name, e.g. "tag16h5"
|
||||
*/
|
||||
void RemoveFamily(std::string_view fam);
|
||||
|
||||
/**
|
||||
* Unregister all families.
|
||||
*/
|
||||
void ClearFamilies();
|
||||
|
||||
/** @} */
|
||||
|
||||
/**
|
||||
* Detect tags from an 8-bit image.
|
||||
*
|
||||
* @param width width of the image
|
||||
* @param height height of the image
|
||||
* @param stride number of bytes between image rows (often the same as width)
|
||||
* @param buf image buffer
|
||||
* @return Results (array of AprilTagDetection pointers)
|
||||
*/
|
||||
Results Detect(int width, int height, int stride, uint8_t* buf);
|
||||
|
||||
/**
|
||||
* Detect tags from an 8-bit image.
|
||||
*
|
||||
* @param width width of the image
|
||||
* @param height height of the image
|
||||
* @param buf image buffer
|
||||
* @return Results (array of AprilTagDetection pointers)
|
||||
*/
|
||||
Results Detect(int width, int height, uint8_t* buf) {
|
||||
return Detect(width, height, width, buf);
|
||||
}
|
||||
|
||||
private:
|
||||
void Destroy();
|
||||
void DestroyFamilies();
|
||||
void DestroyFamily(std::string_view name, void* data);
|
||||
|
||||
void* m_impl;
|
||||
wpi::StringMap<void*> m_families;
|
||||
units::radian_t m_qtpCriticalAngle = 10_deg;
|
||||
};
|
||||
|
||||
} // namespace frc
|
||||
@@ -0,0 +1,18 @@
|
||||
// Copyright (c) FIRST and other WPILib contributors.
|
||||
// Open Source Software; you can modify and/or share it under the terms of
|
||||
// the WPILib BSD license file in the root directory of this project.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <opencv2/core/mat.hpp>
|
||||
|
||||
#include "frc/apriltag/AprilTagDetector.h"
|
||||
|
||||
namespace frc {
|
||||
|
||||
inline AprilTagDetector::Results AprilTagDetect(AprilTagDetector& detector,
|
||||
cv::Mat& image) {
|
||||
return detector.Detect(image.cols, image.rows, image.data);
|
||||
}
|
||||
|
||||
} // namespace frc
|
||||
@@ -0,0 +1,129 @@
|
||||
// Copyright (c) FIRST and other WPILib contributors.
|
||||
// Open Source Software; you can modify and/or share it under the terms of
|
||||
// the WPILib BSD license file in the root directory of this project.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <optional>
|
||||
#include <string_view>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
#include <units/length.h>
|
||||
#include <wpi/SymbolExports.h>
|
||||
|
||||
#include "frc/apriltag/AprilTag.h"
|
||||
#include "frc/geometry/Pose3d.h"
|
||||
|
||||
namespace wpi {
|
||||
class json;
|
||||
} // namespace wpi
|
||||
|
||||
namespace frc {
|
||||
/**
|
||||
* Class for representing a layout of AprilTags on a field and reading them from
|
||||
* a JSON format.
|
||||
*
|
||||
* The JSON format contains two top-level objects, "tags" and "field".
|
||||
* The "tags" object is a list of all AprilTags contained within a layout. Each
|
||||
* AprilTag serializes to a JSON object containing an ID and a Pose3d. The
|
||||
* "field" object is a descriptor of the size of the field in meters with
|
||||
* "width" and "length" values. This is to account for arbitrary field sizes
|
||||
* when transforming the poses.
|
||||
*
|
||||
* Pose3ds in the JSON are measured using the normal FRC coordinate system, NWU
|
||||
* with the origin at the bottom-right corner of the blue alliance wall.
|
||||
* SetOrigin(OriginPosition) can be used to change the poses returned from
|
||||
* GetTagPose(int) to be from the perspective of a specific alliance.
|
||||
*
|
||||
* Tag poses represent the center of the tag, with a zero rotation representing
|
||||
* a tag that is upright and facing away from the (blue) alliance wall (that is,
|
||||
* towards the opposing alliance). */
|
||||
class WPILIB_DLLEXPORT AprilTagFieldLayout {
|
||||
public:
|
||||
enum class OriginPosition {
|
||||
kBlueAllianceWallRightSide,
|
||||
kRedAllianceWallRightSide,
|
||||
};
|
||||
|
||||
AprilTagFieldLayout() = default;
|
||||
|
||||
/**
|
||||
* Construct a new AprilTagFieldLayout with values imported from a JSON file.
|
||||
*
|
||||
* @param path Path of the JSON file to import from.
|
||||
*/
|
||||
explicit AprilTagFieldLayout(std::string_view path);
|
||||
|
||||
/**
|
||||
* Construct a new AprilTagFieldLayout from a vector of AprilTag objects.
|
||||
*
|
||||
* @param apriltags Vector of AprilTags.
|
||||
* @param fieldLength Length of field the layout is representing.
|
||||
* @param fieldWidth Width of field the layout is representing.
|
||||
*/
|
||||
AprilTagFieldLayout(std::vector<AprilTag> apriltags,
|
||||
units::meter_t fieldLength, units::meter_t fieldWidth);
|
||||
|
||||
/**
|
||||
* Sets the origin based on a predefined enumeration of coordinate frame
|
||||
* origins. The origins are calculated from the field dimensions.
|
||||
*
|
||||
* This transforms the Pose3ds returned by GetTagPose(int) to return the
|
||||
* correct pose relative to a predefined coordinate frame.
|
||||
*
|
||||
* @param origin The predefined origin
|
||||
*/
|
||||
void SetOrigin(OriginPosition origin);
|
||||
|
||||
/**
|
||||
* Sets the origin for tag pose transformation.
|
||||
*
|
||||
* This tranforms the Pose3ds returned by GetTagPose(int) to return the
|
||||
* correct pose relative to the provided origin.
|
||||
*
|
||||
* @param origin The new origin for tag transformations
|
||||
*/
|
||||
void SetOrigin(const Pose3d& origin);
|
||||
|
||||
/**
|
||||
* Gets an AprilTag pose by its ID.
|
||||
*
|
||||
* @param ID The ID of the tag.
|
||||
* @return The pose corresponding to the ID that was passed in or an empty
|
||||
* optional if a tag with that ID is not found.
|
||||
*/
|
||||
std::optional<Pose3d> GetTagPose(int ID) const;
|
||||
|
||||
/**
|
||||
* Serializes an AprilTagFieldLayout to a JSON file.
|
||||
*
|
||||
* @param path The path to write the JSON file to.
|
||||
*/
|
||||
void Serialize(std::string_view path);
|
||||
|
||||
/*
|
||||
* Checks equality between this AprilTagFieldLayout and another object.
|
||||
*/
|
||||
bool operator==(const AprilTagFieldLayout&) const = default;
|
||||
|
||||
private:
|
||||
std::unordered_map<int, AprilTag> m_apriltags;
|
||||
units::meter_t m_fieldLength;
|
||||
units::meter_t m_fieldWidth;
|
||||
Pose3d m_origin;
|
||||
|
||||
friend WPILIB_DLLEXPORT void to_json(wpi::json& json,
|
||||
const AprilTagFieldLayout& layout);
|
||||
|
||||
friend WPILIB_DLLEXPORT void from_json(const wpi::json& json,
|
||||
AprilTagFieldLayout& layout);
|
||||
};
|
||||
|
||||
WPILIB_DLLEXPORT
|
||||
void to_json(wpi::json& json, const AprilTagFieldLayout& layout);
|
||||
|
||||
WPILIB_DLLEXPORT
|
||||
void from_json(const wpi::json& json, AprilTagFieldLayout& layout);
|
||||
|
||||
} // namespace frc
|
||||
@@ -0,0 +1,32 @@
|
||||
// Copyright (c) FIRST and other WPILib contributors.
|
||||
// Open Source Software; you can modify and/or share it under the terms of
|
||||
// the WPILib BSD license file in the root directory of this project.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <string_view>
|
||||
|
||||
#include <wpi/SymbolExports.h>
|
||||
|
||||
#include "frc/apriltag/AprilTagFieldLayout.h"
|
||||
|
||||
namespace frc {
|
||||
|
||||
enum class AprilTagField {
|
||||
k2022RapidReact,
|
||||
k2023ChargedUp,
|
||||
|
||||
// This is a placeholder for denoting the last supported field. This should
|
||||
// always be the last entry in the enum and should not be used by users
|
||||
kNumFields,
|
||||
};
|
||||
|
||||
/**
|
||||
* Loads an AprilTagFieldLayout from a predefined field
|
||||
*
|
||||
* @param field The predefined field
|
||||
*/
|
||||
WPILIB_DLLEXPORT AprilTagFieldLayout
|
||||
LoadAprilTagLayoutField(AprilTagField field);
|
||||
|
||||
} // namespace frc
|
||||
@@ -0,0 +1,36 @@
|
||||
// Copyright (c) FIRST and other WPILib contributors.
|
||||
// Open Source Software; you can modify and/or share it under the terms of
|
||||
// the WPILib BSD license file in the root directory of this project.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <wpi/SymbolExports.h>
|
||||
|
||||
#include "frc/geometry/Transform3d.h"
|
||||
|
||||
namespace frc {
|
||||
|
||||
/** A pair of AprilTag pose estimates. */
|
||||
struct WPILIB_DLLEXPORT AprilTagPoseEstimate {
|
||||
/** Pose 1. */
|
||||
Transform3d pose1;
|
||||
|
||||
/** Pose 2. */
|
||||
Transform3d pose2;
|
||||
|
||||
/** Object-space error of pose 1. */
|
||||
double error1;
|
||||
|
||||
/** Object-space error of pose 2. */
|
||||
double error2;
|
||||
|
||||
/**
|
||||
* Gets the ratio of pose reprojection errors, called ambiguity. Numbers
|
||||
* above 0.2 are likely to be ambiguous.
|
||||
*
|
||||
* @return The ratio of pose reprojection errors.
|
||||
*/
|
||||
double GetAmbiguity() const;
|
||||
};
|
||||
|
||||
} // namespace frc
|
||||
@@ -0,0 +1,145 @@
|
||||
// Copyright (c) FIRST and other WPILib contributors.
|
||||
// Open Source Software; you can modify and/or share it under the terms of
|
||||
// the WPILib BSD license file in the root directory of this project.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <span>
|
||||
|
||||
#include <units/length.h>
|
||||
#include <wpi/SymbolExports.h>
|
||||
|
||||
#include "frc/apriltag/AprilTagPoseEstimate.h"
|
||||
#include "frc/geometry/Transform3d.h"
|
||||
|
||||
namespace frc {
|
||||
|
||||
class AprilTagDetection;
|
||||
|
||||
/** Pose estimators for AprilTag tags. */
|
||||
class WPILIB_DLLEXPORT AprilTagPoseEstimator {
|
||||
public:
|
||||
/** Configuration for the pose estimator. */
|
||||
struct Config {
|
||||
bool operator==(const Config&) const = default;
|
||||
|
||||
/** The tag size. */
|
||||
units::meter_t tagSize;
|
||||
|
||||
/** Camera horizontal focal length, in pixels. */
|
||||
double fx;
|
||||
|
||||
/** Camera vertical focal length, in pixels. */
|
||||
double fy;
|
||||
|
||||
/** Camera horizontal focal center, in pixels. */
|
||||
double cx;
|
||||
|
||||
/** Camera vertical focal center, in pixels. */
|
||||
double cy;
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates estimator.
|
||||
*
|
||||
* @param config Configuration
|
||||
*/
|
||||
explicit AprilTagPoseEstimator(const Config& config) : m_config{config} {}
|
||||
|
||||
/**
|
||||
* Sets estimator configuration.
|
||||
*
|
||||
* @param config Configuration
|
||||
*/
|
||||
void SetConfig(const Config& config) { m_config = config; }
|
||||
|
||||
/**
|
||||
* Gets estimator configuration.
|
||||
*
|
||||
* @return Configuration
|
||||
*/
|
||||
const Config& GetConfig() const { return m_config; }
|
||||
|
||||
/**
|
||||
* Estimates the pose of the tag using the homography method described in [1].
|
||||
*
|
||||
* @param detection Tag detection
|
||||
* @return Pose estimate
|
||||
*/
|
||||
Transform3d EstimateHomography(const AprilTagDetection& detection) const;
|
||||
|
||||
/**
|
||||
* Estimates the pose of the tag using the homography method described in [1].
|
||||
*
|
||||
* @param homography Homography 3x3 matrix data
|
||||
* @return Pose estimate
|
||||
*/
|
||||
Transform3d EstimateHomography(std::span<const double, 9> homography) const;
|
||||
|
||||
/**
|
||||
* Estimates the pose of the tag. This returns one or two possible poses for
|
||||
* the tag, along with the object-space error of each.
|
||||
*
|
||||
* This uses the homography method described in [1] for the initial estimate.
|
||||
* Then Orthogonal Iteration [2] is used to refine this estimate. Then [3] is
|
||||
* used to find a potential second local minima and Orthogonal Iteration is
|
||||
* used to refine this second estimate.
|
||||
*
|
||||
* [1]: E. Olson, “Apriltag: A robust and flexible visual fiducial system,” in
|
||||
* 2011 IEEE International Conference on Robotics and Automation,
|
||||
* May 2011, pp. 3400–3407.
|
||||
* [2]: Lu, G. D. Hager and E. Mjolsness, "Fast and globally convergent pose
|
||||
* estimation from video images," in IEEE Transactions on Pattern
|
||||
* Analysis and Machine Intelligence, vol. 22, no. 6, pp. 610-622, June 2000.
|
||||
* doi: 10.1109/34.862199
|
||||
* [3]: Schweighofer and A. Pinz, "Robust Pose Estimation from a Planar
|
||||
* Target," in IEEE Transactions on Pattern Analysis and Machine Intelligence,
|
||||
* vol. 28, no. 12, pp. 2024-2030, Dec. 2006. doi: 10.1109/TPAMI.2006.252
|
||||
*
|
||||
* @param detection Tag detection
|
||||
* @param nIters Number of iterations
|
||||
* @return Initial and (possibly) second pose estimates
|
||||
*/
|
||||
AprilTagPoseEstimate EstimateOrthogonalIteration(
|
||||
const AprilTagDetection& detection, int nIters) const;
|
||||
|
||||
/**
|
||||
* Estimates the pose of the tag. This returns one or two possible poses for
|
||||
* the tag, along with the object-space error of each.
|
||||
*
|
||||
* @param homography Homography 3x3 matrix data
|
||||
* @param corners Corner point array (X and Y for each corner in order)
|
||||
* @param nIters Number of iterations
|
||||
* @return Initial and (possibly) second pose estimates
|
||||
*/
|
||||
AprilTagPoseEstimate EstimateOrthogonalIteration(
|
||||
std::span<const double, 9> homography, std::span<const double, 8> corners,
|
||||
int nIters) const;
|
||||
|
||||
/**
|
||||
* Estimates tag pose. This method is an easier to use interface to
|
||||
* EstimatePoseOrthogonalIteration(), running 50 iterations and returning the
|
||||
* pose with the lower object-space error.
|
||||
*
|
||||
* @param detection Tag detection
|
||||
* @return Pose estimate
|
||||
*/
|
||||
Transform3d Estimate(const AprilTagDetection& detection) const;
|
||||
|
||||
/**
|
||||
* Estimates tag pose. This method is an easier to use interface to
|
||||
* EstimatePoseOrthogonalIteration(), running 50 iterations and returning the
|
||||
* pose with the lower object-space error.
|
||||
*
|
||||
* @param homography Homography 3x3 matrix data
|
||||
* @param corners Corner point array (X and Y for each corner in order)
|
||||
* @return Pose estimate
|
||||
*/
|
||||
Transform3d Estimate(std::span<const double, 9> homography,
|
||||
std::span<const double, 8> corners) const;
|
||||
|
||||
private:
|
||||
Config m_config;
|
||||
};
|
||||
|
||||
} // namespace frc
|
||||
@@ -0,0 +1,440 @@
|
||||
{
|
||||
"tags": [
|
||||
{
|
||||
"ID": 0,
|
||||
"pose": {
|
||||
"translation": {
|
||||
"x": -0.0035306,
|
||||
"y": 7.578928199999999,
|
||||
"z": 0.8858503999999999
|
||||
},
|
||||
"rotation": {
|
||||
"quaternion": {
|
||||
"W": 1.0,
|
||||
"X": 0.0,
|
||||
"Y": 0.0,
|
||||
"Z": 0.0
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": 1,
|
||||
"pose": {
|
||||
"translation": {
|
||||
"x": 3.2327088,
|
||||
"y": 5.486654,
|
||||
"z": 1.7254728
|
||||
},
|
||||
"rotation": {
|
||||
"quaternion": {
|
||||
"W": 1.0,
|
||||
"X": 0.0,
|
||||
"Y": 0.0,
|
||||
"Z": 0.0
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": 2,
|
||||
"pose": {
|
||||
"translation": {
|
||||
"x": 3.067812,
|
||||
"y": 5.3305202,
|
||||
"z": 1.3762228
|
||||
},
|
||||
"rotation": {
|
||||
"quaternion": {
|
||||
"W": 0.7071067811865476,
|
||||
"X": 0.0,
|
||||
"Y": 0.0,
|
||||
"Z": -0.7071067811865475
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": 3,
|
||||
"pose": {
|
||||
"translation": {
|
||||
"x": 0.0039878,
|
||||
"y": 5.058536999999999,
|
||||
"z": 0.80645
|
||||
},
|
||||
"rotation": {
|
||||
"quaternion": {
|
||||
"W": 1.0,
|
||||
"X": 0.0,
|
||||
"Y": 0.0,
|
||||
"Z": 0.0
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": 4,
|
||||
"pose": {
|
||||
"translation": {
|
||||
"x": 0.0039878,
|
||||
"y": 3.5124898,
|
||||
"z": 0.80645
|
||||
},
|
||||
"rotation": {
|
||||
"quaternion": {
|
||||
"W": 1.0,
|
||||
"X": 0.0,
|
||||
"Y": 0.0,
|
||||
"Z": 0.0
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": 5,
|
||||
"pose": {
|
||||
"translation": {
|
||||
"x": 0.12110719999999998,
|
||||
"y": 1.7178274,
|
||||
"z": 0.8906002000000001
|
||||
},
|
||||
"rotation": {
|
||||
"quaternion": {
|
||||
"W": 0.9196502204050923,
|
||||
"X": 0.0,
|
||||
"Y": 0.0,
|
||||
"Z": 0.39273842708457407
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": 6,
|
||||
"pose": {
|
||||
"translation": {
|
||||
"x": 0.8733027999999999,
|
||||
"y": 0.9412985999999999,
|
||||
"z": 0.8906002000000001
|
||||
},
|
||||
"rotation": {
|
||||
"quaternion": {
|
||||
"W": 0.9196502204050923,
|
||||
"X": 0.0,
|
||||
"Y": 0.0,
|
||||
"Z": 0.39273842708457407
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": 7,
|
||||
"pose": {
|
||||
"translation": {
|
||||
"x": 1.6150844,
|
||||
"y": 0.15725139999999999,
|
||||
"z": 0.8906002000000001
|
||||
},
|
||||
"rotation": {
|
||||
"quaternion": {
|
||||
"W": 0.9196502204050923,
|
||||
"X": 0.0,
|
||||
"Y": 0.0,
|
||||
"Z": 0.39273842708457407
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": 10,
|
||||
"pose": {
|
||||
"translation": {
|
||||
"x": 16.4627306,
|
||||
"y": 0.6506718,
|
||||
"z": 0.8858503999999999
|
||||
},
|
||||
"rotation": {
|
||||
"quaternion": {
|
||||
"W": 6.123233995736766E-17,
|
||||
"X": 0.0,
|
||||
"Y": 0.0,
|
||||
"Z": 1.0
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": 11,
|
||||
"pose": {
|
||||
"translation": {
|
||||
"x": 13.2350002,
|
||||
"y": 2.743454,
|
||||
"z": 1.7254728
|
||||
},
|
||||
"rotation": {
|
||||
"quaternion": {
|
||||
"W": 6.123233995736766E-17,
|
||||
"X": 0.0,
|
||||
"Y": 0.0,
|
||||
"Z": 1.0
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": 12,
|
||||
"pose": {
|
||||
"translation": {
|
||||
"x": 13.391388000000001,
|
||||
"y": 2.8998418,
|
||||
"z": 1.3762228
|
||||
},
|
||||
"rotation": {
|
||||
"quaternion": {
|
||||
"W": 0.7071067811865476,
|
||||
"X": 0.0,
|
||||
"Y": 0.0,
|
||||
"Z": 0.7071067811865475
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": 13,
|
||||
"pose": {
|
||||
"translation": {
|
||||
"x": 16.4552122,
|
||||
"y": 3.1755079999999998,
|
||||
"z": 0.80645
|
||||
},
|
||||
"rotation": {
|
||||
"quaternion": {
|
||||
"W": 6.123233995736766E-17,
|
||||
"X": 0.0,
|
||||
"Y": 0.0,
|
||||
"Z": 1.0
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": 14,
|
||||
"pose": {
|
||||
"translation": {
|
||||
"x": 16.4552122,
|
||||
"y": 4.7171356,
|
||||
"z": 0.80645
|
||||
},
|
||||
"rotation": {
|
||||
"quaternion": {
|
||||
"W": 6.123233995736766E-17,
|
||||
"X": 0.0,
|
||||
"Y": 0.0,
|
||||
"Z": 1.0
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": 15,
|
||||
"pose": {
|
||||
"translation": {
|
||||
"x": 16.3350194,
|
||||
"y": 6.5149729999999995,
|
||||
"z": 0.8937752
|
||||
},
|
||||
"rotation": {
|
||||
"quaternion": {
|
||||
"W": -0.37298778257580906,
|
||||
"X": -0.0,
|
||||
"Y": 0.0,
|
||||
"Z": 0.9278362538989199
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": 16,
|
||||
"pose": {
|
||||
"translation": {
|
||||
"x": 15.5904946,
|
||||
"y": 7.292695599999999,
|
||||
"z": 0.8906002000000001
|
||||
},
|
||||
"rotation": {
|
||||
"quaternion": {
|
||||
"W": -0.37298778257580906,
|
||||
"X": -0.0,
|
||||
"Y": 0.0,
|
||||
"Z": 0.9278362538989199
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": 17,
|
||||
"pose": {
|
||||
"translation": {
|
||||
"x": 14.847188999999998,
|
||||
"y": 8.0691228,
|
||||
"z": 0.8906002000000001
|
||||
},
|
||||
"rotation": {
|
||||
"quaternion": {
|
||||
"W": -0.37298778257580906,
|
||||
"X": -0.0,
|
||||
"Y": 0.0,
|
||||
"Z": 0.9278362538989199
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": 40,
|
||||
"pose": {
|
||||
"translation": {
|
||||
"x": 7.874127,
|
||||
"y": 4.9131728,
|
||||
"z": 0.7032752
|
||||
},
|
||||
"rotation": {
|
||||
"quaternion": {
|
||||
"W": 0.5446390350150271,
|
||||
"X": 0.0,
|
||||
"Y": 0.0,
|
||||
"Z": 0.838670567945424
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": 41,
|
||||
"pose": {
|
||||
"translation": {
|
||||
"x": 7.4312271999999995,
|
||||
"y": 3.759327,
|
||||
"z": 0.7032752
|
||||
},
|
||||
"rotation": {
|
||||
"quaternion": {
|
||||
"W": -0.20791169081775934,
|
||||
"X": -0.0,
|
||||
"Y": 0.0,
|
||||
"Z": 0.9781476007338057
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": 42,
|
||||
"pose": {
|
||||
"translation": {
|
||||
"x": 8.585073,
|
||||
"y": 3.3164272,
|
||||
"z": 0.7032752
|
||||
},
|
||||
"rotation": {
|
||||
"quaternion": {
|
||||
"W": 0.838670567945424,
|
||||
"X": 0.0,
|
||||
"Y": 0.0,
|
||||
"Z": -0.5446390350150271
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": 43,
|
||||
"pose": {
|
||||
"translation": {
|
||||
"x": 9.0279728,
|
||||
"y": 4.470273,
|
||||
"z": 0.7032752
|
||||
},
|
||||
"rotation": {
|
||||
"quaternion": {
|
||||
"W": 0.9781476007338057,
|
||||
"X": 0.0,
|
||||
"Y": 0.0,
|
||||
"Z": 0.20791169081775934
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": 50,
|
||||
"pose": {
|
||||
"translation": {
|
||||
"x": 7.6790296,
|
||||
"y": 4.3261534,
|
||||
"z": 2.4177244
|
||||
},
|
||||
"rotation": {
|
||||
"quaternion": {
|
||||
"W": 0.17729273396782605,
|
||||
"X": -0.22744989571511945,
|
||||
"Y": 0.04215534644161733,
|
||||
"Z": 0.9565859910053995
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": 51,
|
||||
"pose": {
|
||||
"translation": {
|
||||
"x": 8.0182466,
|
||||
"y": 3.5642296,
|
||||
"z": 2.4177244
|
||||
},
|
||||
"rotation": {
|
||||
"quaternion": {
|
||||
"W": -0.5510435465842192,
|
||||
"X": -0.19063969497246985,
|
||||
"Y": -0.13102303230819815,
|
||||
"Z": 0.8017733354717242
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": 52,
|
||||
"pose": {
|
||||
"translation": {
|
||||
"x": 8.7801704,
|
||||
"y": 3.9034466,
|
||||
"z": 2.4177244
|
||||
},
|
||||
"rotation": {
|
||||
"quaternion": {
|
||||
"W": -0.9565859910053994,
|
||||
"X": -0.04215534644161739,
|
||||
"Y": -0.22744989571511942,
|
||||
"Z": 0.17729273396782633
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": 53,
|
||||
"pose": {
|
||||
"translation": {
|
||||
"x": 8.4409534,
|
||||
"y": 4.6653704,
|
||||
"z": 2.4177244
|
||||
},
|
||||
"rotation": {
|
||||
"quaternion": {
|
||||
"W": 0.8017733354717241,
|
||||
"X": -0.1310230323081982,
|
||||
"Y": 0.19063969497246983,
|
||||
"Z": 0.5510435465842194
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"field": {
|
||||
"length": 16.4592,
|
||||
"width": 8.2296
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
{
|
||||
"tags": [
|
||||
{
|
||||
"ID": 1,
|
||||
"pose": {
|
||||
"translation": {
|
||||
"x": 15.513558,
|
||||
"y": 1.071626,
|
||||
"z": 0.462788
|
||||
},
|
||||
"rotation": {
|
||||
"quaternion": {
|
||||
"W": 0.0,
|
||||
"X": 0.0,
|
||||
"Y": 0.0,
|
||||
"Z": 1.0
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": 2,
|
||||
"pose": {
|
||||
"translation": {
|
||||
"x": 15.513558,
|
||||
"y": 2.748026,
|
||||
"z": 0.462788
|
||||
},
|
||||
"rotation": {
|
||||
"quaternion": {
|
||||
"W": 0.0,
|
||||
"X": 0.0,
|
||||
"Y": 0.0,
|
||||
"Z": 1.0
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": 3,
|
||||
"pose": {
|
||||
"translation": {
|
||||
"x": 15.513558,
|
||||
"y": 4.424426,
|
||||
"z": 0.462788
|
||||
},
|
||||
"rotation": {
|
||||
"quaternion": {
|
||||
"W": 0.0,
|
||||
"X": 0.0,
|
||||
"Y": 0.0,
|
||||
"Z": 1.0
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": 4,
|
||||
"pose": {
|
||||
"translation": {
|
||||
"x": 16.178784,
|
||||
"y": 6.749796,
|
||||
"z": 0.695452
|
||||
},
|
||||
"rotation": {
|
||||
"quaternion": {
|
||||
"W": 0.0,
|
||||
"X": 0.0,
|
||||
"Y": 0.0,
|
||||
"Z": 1.0
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": 5,
|
||||
"pose": {
|
||||
"translation": {
|
||||
"x": 0.36195,
|
||||
"y": 6.749796,
|
||||
"z": 0.695452
|
||||
},
|
||||
"rotation": {
|
||||
"quaternion": {
|
||||
"W": 1.0,
|
||||
"X": 0.0,
|
||||
"Y": 0.0,
|
||||
"Z": 0.0
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": 6,
|
||||
"pose": {
|
||||
"translation": {
|
||||
"x": 1.02743,
|
||||
"y": 4.424426,
|
||||
"z": 0.462788
|
||||
},
|
||||
"rotation": {
|
||||
"quaternion": {
|
||||
"W": 1.0,
|
||||
"X": 0.0,
|
||||
"Y": 0.0,
|
||||
"Z": 0.0
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": 7,
|
||||
"pose": {
|
||||
"translation": {
|
||||
"x": 1.02743,
|
||||
"y": 2.748026,
|
||||
"z": 0.462788
|
||||
},
|
||||
"rotation": {
|
||||
"quaternion": {
|
||||
"W": 1.0,
|
||||
"X": 0.0,
|
||||
"Y": 0.0,
|
||||
"Z": 0.0
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": 8,
|
||||
"pose": {
|
||||
"translation": {
|
||||
"x": 1.02743,
|
||||
"y": 1.071626,
|
||||
"z": 0.462788
|
||||
},
|
||||
"rotation": {
|
||||
"quaternion": {
|
||||
"W": 1.0,
|
||||
"X": 0.0,
|
||||
"Y": 0.0,
|
||||
"Z": 0.0
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"field": {
|
||||
"length": 16.54175,
|
||||
"width": 8.0137
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,264 @@
|
||||
// Copyright (c) FIRST and other WPILib contributors.
|
||||
// Open Source Software; you can modify and/or share it under the terms of
|
||||
// the WPILib BSD license file in the root directory of this project.
|
||||
|
||||
package edu.wpi.first.apriltag;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.fail;
|
||||
|
||||
import edu.wpi.first.math.geometry.Transform3d;
|
||||
import edu.wpi.first.math.util.Units;
|
||||
import edu.wpi.first.util.RuntimeLoader;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.opencv.core.Core;
|
||||
import org.opencv.core.CvType;
|
||||
import org.opencv.core.Mat;
|
||||
import org.opencv.imgcodecs.Imgcodecs;
|
||||
import org.opencv.imgproc.Imgproc;
|
||||
|
||||
@SuppressWarnings("PMD.MutableStaticState")
|
||||
class AprilTagDetectorTest {
|
||||
@SuppressWarnings("MemberName")
|
||||
AprilTagDetector detector;
|
||||
|
||||
static RuntimeLoader<Core> loader;
|
||||
|
||||
@BeforeAll
|
||||
static void beforeAll() {
|
||||
try {
|
||||
loader =
|
||||
new RuntimeLoader<>(
|
||||
Core.NATIVE_LIBRARY_NAME, RuntimeLoader.getDefaultExtractionRoot(), Core.class);
|
||||
loader.loadLibrary();
|
||||
} catch (IOException ex) {
|
||||
fail(ex);
|
||||
}
|
||||
}
|
||||
|
||||
@BeforeEach
|
||||
void beforeEach() {
|
||||
detector = new AprilTagDetector();
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
void afterEach() {
|
||||
detector.close();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testConfigDefaults() {
|
||||
var config = detector.getConfig();
|
||||
assertEquals(new AprilTagDetector.Config(), config);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testQtpDefaults() {
|
||||
var params = detector.getQuadThresholdParameters();
|
||||
assertEquals(new AprilTagDetector.QuadThresholdParameters(), params);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSetConfigNumThreads() {
|
||||
var newConfig = new AprilTagDetector.Config();
|
||||
newConfig.numThreads = 2;
|
||||
detector.setConfig(newConfig);
|
||||
var config = detector.getConfig();
|
||||
assertEquals(2, config.numThreads);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testQtpMinClusterPixels() {
|
||||
var newParams = new AprilTagDetector.QuadThresholdParameters();
|
||||
newParams.minClusterPixels = 8;
|
||||
detector.setQuadThresholdParameters(newParams);
|
||||
var params = detector.getQuadThresholdParameters();
|
||||
assertEquals(8, params.minClusterPixels);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testAdd16h5() {
|
||||
assertDoesNotThrow(() -> detector.addFamily("tag16h5"));
|
||||
// duplicate addition is also okay
|
||||
assertDoesNotThrow(() -> detector.addFamily("tag16h5"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testAdd25h9() {
|
||||
assertDoesNotThrow(() -> detector.addFamily("tag25h9"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testAdd36h11() {
|
||||
assertDoesNotThrow(() -> detector.addFamily("tag36h11"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testAddMultiple() {
|
||||
assertDoesNotThrow(() -> detector.addFamily("tag16h5"));
|
||||
assertDoesNotThrow(() -> detector.addFamily("tag36h11"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testRemoveFamily() {
|
||||
// okay to remove non-existent family
|
||||
detector.removeFamily("tag16h5");
|
||||
|
||||
// add and remove
|
||||
detector.addFamily("tag16h5");
|
||||
detector.removeFamily("tag16h5");
|
||||
}
|
||||
|
||||
@SuppressWarnings("PMD.AssignmentInOperand")
|
||||
public Mat loadImage(String resource) throws IOException {
|
||||
Mat encoded;
|
||||
try (InputStream is = getClass().getResource(resource).openStream()) {
|
||||
try (ByteArrayOutputStream os = new ByteArrayOutputStream(is.available())) {
|
||||
byte[] buffer = new byte[4096];
|
||||
int bytesRead;
|
||||
while ((bytesRead = is.read(buffer)) != -1) {
|
||||
os.write(buffer, 0, bytesRead);
|
||||
}
|
||||
encoded = new Mat(1, os.size(), CvType.CV_8U);
|
||||
encoded.put(0, 0, os.toByteArray());
|
||||
}
|
||||
}
|
||||
Mat image = Imgcodecs.imdecode(encoded, Imgcodecs.IMREAD_COLOR);
|
||||
encoded.release();
|
||||
Imgproc.cvtColor(image, image, Imgproc.COLOR_BGR2GRAY);
|
||||
return image;
|
||||
}
|
||||
|
||||
@Test
|
||||
void testDecodeAndPose() {
|
||||
detector.addFamily("tag16h5");
|
||||
detector.addFamily("tag36h11");
|
||||
|
||||
Mat image;
|
||||
try {
|
||||
image = loadImage("tag1_640_480.jpg");
|
||||
} catch (IOException ex) {
|
||||
fail(ex);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
AprilTagDetection[] results = detector.detect(image);
|
||||
assertEquals(1, results.length);
|
||||
assertEquals("tag36h11", results[0].getFamily());
|
||||
assertEquals(1, results[0].getId());
|
||||
assertEquals(0, results[0].getHamming());
|
||||
|
||||
var estimator =
|
||||
new AprilTagPoseEstimator(new AprilTagPoseEstimator.Config(0.2, 500, 500, 320, 240));
|
||||
AprilTagPoseEstimate est = estimator.estimateOrthogonalIteration(results[0], 50);
|
||||
assertEquals(new Transform3d(), est.pose2);
|
||||
Transform3d pose = estimator.estimate(results[0]);
|
||||
assertEquals(est.pose1, pose);
|
||||
} finally {
|
||||
image.release();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This tag is rotated such that the top is closer to the camera than the bottom. In the camera
|
||||
* frame, with +x to the right, this is a rotation about +X by 45 degrees.
|
||||
*/
|
||||
@Test
|
||||
void testPoseRotatedX() {
|
||||
detector.addFamily("tag16h5");
|
||||
|
||||
Mat image;
|
||||
try {
|
||||
image = loadImage("tag2_45deg_X.png");
|
||||
} catch (IOException ex) {
|
||||
fail(ex);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
AprilTagDetection[] results = detector.detect(image);
|
||||
assertEquals(1, results.length);
|
||||
|
||||
var estimator =
|
||||
new AprilTagPoseEstimator(
|
||||
new AprilTagPoseEstimator.Config(
|
||||
0.2, 500, 500, image.cols() / 2.0, image.rows() / 2.0));
|
||||
AprilTagPoseEstimate est = estimator.estimateOrthogonalIteration(results[0], 50);
|
||||
|
||||
assertEquals(Units.degreesToRadians(45), est.pose1.getRotation().getX(), 0.1);
|
||||
assertEquals(Units.degreesToRadians(0), est.pose1.getRotation().getY(), 0.1);
|
||||
assertEquals(Units.degreesToRadians(0), est.pose1.getRotation().getZ(), 0.1);
|
||||
} finally {
|
||||
image.release();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This tag is rotated such that the right is closer to the camera than the left. In the camera
|
||||
* frame, with +y down, this is a rotation of 45 degrees about +y.
|
||||
*/
|
||||
@Test
|
||||
void testPoseRotatedY() {
|
||||
detector.addFamily("tag16h5");
|
||||
|
||||
Mat image;
|
||||
try {
|
||||
image = loadImage("tag2_45deg_y.png");
|
||||
} catch (IOException ex) {
|
||||
fail(ex);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
AprilTagDetection[] results = detector.detect(image);
|
||||
assertEquals(1, results.length);
|
||||
|
||||
var estimator =
|
||||
new AprilTagPoseEstimator(
|
||||
new AprilTagPoseEstimator.Config(
|
||||
0.2, 500, 500, image.cols() / 2.0, image.rows() / 2.0));
|
||||
AprilTagPoseEstimate est = estimator.estimateOrthogonalIteration(results[0], 50);
|
||||
|
||||
assertEquals(Units.degreesToRadians(0), est.pose1.getRotation().getX(), 0.1);
|
||||
assertEquals(Units.degreesToRadians(45), est.pose1.getRotation().getY(), 0.1);
|
||||
assertEquals(Units.degreesToRadians(0), est.pose1.getRotation().getZ(), 0.1);
|
||||
} finally {
|
||||
image.release();
|
||||
}
|
||||
}
|
||||
|
||||
/** This tag is facing right at the camera -- no rotation should be observed. */
|
||||
@Test
|
||||
void testPoseStraightOn() {
|
||||
detector.addFamily("tag16h5");
|
||||
|
||||
Mat image;
|
||||
try {
|
||||
image = loadImage("tag2_16h5_straight.png");
|
||||
} catch (IOException ex) {
|
||||
fail(ex);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
AprilTagDetection[] results = detector.detect(image);
|
||||
assertEquals(1, results.length);
|
||||
|
||||
var estimator =
|
||||
new AprilTagPoseEstimator(
|
||||
new AprilTagPoseEstimator.Config(
|
||||
0.2, 500, 500, image.cols() / 2.0, image.rows() / 2.0));
|
||||
AprilTagPoseEstimate est = estimator.estimateOrthogonalIteration(results[0], 50);
|
||||
|
||||
assertEquals(Units.degreesToRadians(0), est.pose1.getRotation().getX(), 0.1);
|
||||
assertEquals(Units.degreesToRadians(0), est.pose1.getRotation().getY(), 0.1);
|
||||
assertEquals(Units.degreesToRadians(0), est.pose1.getRotation().getZ(), 0.1);
|
||||
} finally {
|
||||
image.release();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
// Copyright (c) FIRST and other WPILib contributors.
|
||||
// Open Source Software; you can modify and/or share it under the terms of
|
||||
// the WPILib BSD license file in the root directory of this project.
|
||||
|
||||
package edu.wpi.first.apriltag;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
import edu.wpi.first.math.geometry.Pose3d;
|
||||
import edu.wpi.first.math.geometry.Rotation3d;
|
||||
import edu.wpi.first.math.geometry.Translation3d;
|
||||
import edu.wpi.first.math.util.Units;
|
||||
import java.util.List;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
class AprilTagPoseSetOriginTest {
|
||||
@Test
|
||||
void transformationMatches() {
|
||||
var layout =
|
||||
new AprilTagFieldLayout(
|
||||
List.of(
|
||||
new AprilTag(1, new Pose3d(new Translation3d(0, 0, 0), new Rotation3d(0, 0, 0))),
|
||||
new AprilTag(
|
||||
2,
|
||||
new Pose3d(
|
||||
new Translation3d(
|
||||
Units.feetToMeters(4.0), Units.feetToMeters(4), Units.feetToMeters(4)),
|
||||
new Rotation3d(0, 0, Units.degreesToRadians(180))))),
|
||||
Units.feetToMeters(54.0),
|
||||
Units.feetToMeters(27.0));
|
||||
layout.setOrigin(AprilTagFieldLayout.OriginPosition.kRedAllianceWallRightSide);
|
||||
|
||||
assertEquals(
|
||||
new Pose3d(
|
||||
new Translation3d(Units.feetToMeters(54.0), Units.feetToMeters(27.0), 0.0),
|
||||
new Rotation3d(0.0, 0.0, Units.degreesToRadians(180.0))),
|
||||
layout.getTagPose(1).orElse(null));
|
||||
|
||||
assertEquals(
|
||||
new Pose3d(
|
||||
new Translation3d(
|
||||
Units.feetToMeters(50.0), Units.feetToMeters(23.0), Units.feetToMeters(4)),
|
||||
new Rotation3d(0.0, 0.0, 0)),
|
||||
layout.getTagPose(2).orElse(null));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
// Copyright (c) FIRST and other WPILib contributors.
|
||||
// Open Source Software; you can modify and/or share it under the terms of
|
||||
// the WPILib BSD license file in the root directory of this project.
|
||||
|
||||
package edu.wpi.first.apriltag;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import edu.wpi.first.math.geometry.Pose3d;
|
||||
import edu.wpi.first.math.geometry.Rotation3d;
|
||||
import edu.wpi.first.math.util.Units;
|
||||
import java.util.List;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
class AprilTagSerializationTest {
|
||||
@Test
|
||||
void deserializeMatches() {
|
||||
var layout =
|
||||
new AprilTagFieldLayout(
|
||||
List.of(
|
||||
new AprilTag(1, new Pose3d(0, 0, 0, new Rotation3d(0, 0, 0))),
|
||||
new AprilTag(3, new Pose3d(0, 1, 0, new Rotation3d(0, 0, 0)))),
|
||||
Units.feetToMeters(54.0),
|
||||
Units.feetToMeters(27.0));
|
||||
|
||||
var objectMapper = new ObjectMapper();
|
||||
|
||||
var deserialized =
|
||||
assertDoesNotThrow(
|
||||
() ->
|
||||
objectMapper.readValue(
|
||||
objectMapper.writeValueAsString(layout), AprilTagFieldLayout.class));
|
||||
|
||||
assertEquals(layout, deserialized);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
// Copyright (c) FIRST and other WPILib contributors.
|
||||
// Open Source Software; you can modify and/or share it under the terms of
|
||||
// the WPILib BSD license file in the root directory of this project.
|
||||
|
||||
package edu.wpi.first.apriltag;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
import edu.wpi.first.math.geometry.Pose3d;
|
||||
import edu.wpi.first.math.geometry.Rotation3d;
|
||||
import edu.wpi.first.math.util.Units;
|
||||
import java.io.IOException;
|
||||
import java.util.Optional;
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.EnumSource;
|
||||
|
||||
class LoadConfigTest {
|
||||
@ParameterizedTest
|
||||
@EnumSource(AprilTagFields.class)
|
||||
void testLoad(AprilTagFields field) {
|
||||
AprilTagFieldLayout layout = Assertions.assertDoesNotThrow(field::loadAprilTagLayoutField);
|
||||
assertNotNull(layout);
|
||||
}
|
||||
|
||||
@Test
|
||||
void test2022RapidReact() throws IOException {
|
||||
AprilTagFieldLayout layout = AprilTagFields.k2022RapidReact.loadAprilTagLayoutField();
|
||||
|
||||
// Blue Hangar Truss - Hub
|
||||
Pose3d expectedPose =
|
||||
new Pose3d(
|
||||
Units.inchesToMeters(127.272),
|
||||
Units.inchesToMeters(216.01),
|
||||
Units.inchesToMeters(67.932),
|
||||
new Rotation3d(0, 0, 0));
|
||||
Optional<Pose3d> maybePose = layout.getTagPose(1);
|
||||
assertTrue(maybePose.isPresent());
|
||||
assertEquals(expectedPose, maybePose.get());
|
||||
|
||||
// Blue Terminal Near Station
|
||||
expectedPose =
|
||||
new Pose3d(
|
||||
Units.inchesToMeters(4.768),
|
||||
Units.inchesToMeters(67.631),
|
||||
Units.inchesToMeters(35.063),
|
||||
new Rotation3d(0, 0, Math.toRadians(46.25)));
|
||||
maybePose = layout.getTagPose(5);
|
||||
assertTrue(maybePose.isPresent());
|
||||
assertEquals(expectedPose, maybePose.get());
|
||||
|
||||
// Upper Hub Blue-Near
|
||||
expectedPose =
|
||||
new Pose3d(
|
||||
Units.inchesToMeters(332.321),
|
||||
Units.inchesToMeters(183.676),
|
||||
Units.inchesToMeters(95.186),
|
||||
new Rotation3d(0, Math.toRadians(26.75), Math.toRadians(69)));
|
||||
maybePose = layout.getTagPose(53);
|
||||
assertTrue(maybePose.isPresent());
|
||||
assertEquals(expectedPose, maybePose.get());
|
||||
|
||||
// Doesn't exist
|
||||
maybePose = layout.getTagPose(54);
|
||||
assertFalse(maybePose.isPresent());
|
||||
}
|
||||
}
|
||||
67
apriltag/src/test/native/cpp/AprilTagDetectorTest.cpp
Normal file
67
apriltag/src/test/native/cpp/AprilTagDetectorTest.cpp
Normal file
@@ -0,0 +1,67 @@
|
||||
// Copyright (c) FIRST and other WPILib contributors.
|
||||
// Open Source Software; you can modify and/or share it under the terms of
|
||||
// the WPILib BSD license file in the root directory of this project.
|
||||
|
||||
#include "frc/apriltag/AprilTagDetector.h"
|
||||
#include "gtest/gtest.h"
|
||||
|
||||
using namespace frc;
|
||||
|
||||
TEST(AprilTagDetectorTest, ConfigDefaults) {
|
||||
AprilTagDetector detector;
|
||||
auto config = detector.GetConfig();
|
||||
ASSERT_EQ(config, AprilTagDetector::Config{});
|
||||
}
|
||||
|
||||
TEST(AprilTagDetectorTest, QtpDefaults) {
|
||||
AprilTagDetector detector;
|
||||
auto params = detector.GetQuadThresholdParameters();
|
||||
ASSERT_EQ(params, AprilTagDetector::QuadThresholdParameters{});
|
||||
}
|
||||
|
||||
TEST(AprilTagDetectorTest, SetConfigNumThreads) {
|
||||
AprilTagDetector detector;
|
||||
detector.SetConfig({.numThreads = 2});
|
||||
auto config = detector.GetConfig();
|
||||
ASSERT_EQ(config.numThreads, 2);
|
||||
}
|
||||
|
||||
TEST(AprilTagDetectorTest, QtpMinClusterPixels) {
|
||||
AprilTagDetector detector;
|
||||
detector.SetQuadThresholdParameters({.minClusterPixels = 8});
|
||||
auto params = detector.GetQuadThresholdParameters();
|
||||
ASSERT_EQ(params.minClusterPixels, 8);
|
||||
}
|
||||
|
||||
TEST(AprilTagDetectorTest, Add16h5) {
|
||||
AprilTagDetector detector;
|
||||
ASSERT_TRUE(detector.AddFamily("tag16h5"));
|
||||
// duplicate addition is also okay
|
||||
ASSERT_TRUE(detector.AddFamily("tag16h5"));
|
||||
}
|
||||
|
||||
TEST(AprilTagDetectorTest, Add25h9) {
|
||||
AprilTagDetector detector;
|
||||
ASSERT_TRUE(detector.AddFamily("tag25h9"));
|
||||
}
|
||||
|
||||
TEST(AprilTagDetectorTest, Add36h11) {
|
||||
AprilTagDetector detector;
|
||||
ASSERT_TRUE(detector.AddFamily("tag36h11"));
|
||||
}
|
||||
|
||||
TEST(AprilTagDetectorTest, AddMultiple) {
|
||||
AprilTagDetector detector;
|
||||
ASSERT_TRUE(detector.AddFamily("tag16h5"));
|
||||
ASSERT_TRUE(detector.AddFamily("tag36h11"));
|
||||
}
|
||||
|
||||
TEST(AprilTagDetectorTest, RemoveFamily) {
|
||||
AprilTagDetector detector;
|
||||
// okay to remove non-existent family
|
||||
detector.RemoveFamily("tag16h5");
|
||||
|
||||
// add and remove
|
||||
detector.AddFamily("tag16h5");
|
||||
detector.RemoveFamily("tag16h5");
|
||||
}
|
||||
27
apriltag/src/test/native/cpp/AprilTagJsonTest.cpp
Normal file
27
apriltag/src/test/native/cpp/AprilTagJsonTest.cpp
Normal file
@@ -0,0 +1,27 @@
|
||||
// Copyright (c) FIRST and other WPILib contributors.
|
||||
// Open Source Software; you can modify and/or share it under the terms of
|
||||
// the WPILib BSD license file in the root directory of this project.
|
||||
|
||||
#include <vector>
|
||||
|
||||
#include <wpi/json.h>
|
||||
|
||||
#include "frc/apriltag/AprilTag.h"
|
||||
#include "frc/apriltag/AprilTagFieldLayout.h"
|
||||
#include "frc/geometry/Pose3d.h"
|
||||
#include "gtest/gtest.h"
|
||||
|
||||
using namespace frc;
|
||||
|
||||
TEST(AprilTagJsonTest, DeserializeMatches) {
|
||||
auto layout = AprilTagFieldLayout{
|
||||
std::vector{
|
||||
AprilTag{1, Pose3d{}},
|
||||
AprilTag{3, Pose3d{0_m, 1_m, 0_m, Rotation3d{0_deg, 0_deg, 0_deg}}}},
|
||||
54_ft, 27_ft};
|
||||
|
||||
AprilTagFieldLayout deserialized;
|
||||
wpi::json json = layout;
|
||||
EXPECT_NO_THROW(deserialized = json.get<AprilTagFieldLayout>());
|
||||
EXPECT_EQ(layout, deserialized);
|
||||
}
|
||||
33
apriltag/src/test/native/cpp/AprilTagPoseSetOriginTest.cpp
Normal file
33
apriltag/src/test/native/cpp/AprilTagPoseSetOriginTest.cpp
Normal file
@@ -0,0 +1,33 @@
|
||||
// Copyright (c) FIRST and other WPILib contributors.
|
||||
// Open Source Software; you can modify and/or share it under the terms of
|
||||
// the WPILib BSD license file in the root directory of this project.
|
||||
|
||||
#include <vector>
|
||||
|
||||
#include <wpi/json.h>
|
||||
|
||||
#include "frc/apriltag/AprilTag.h"
|
||||
#include "frc/apriltag/AprilTagFieldLayout.h"
|
||||
#include "frc/geometry/Pose3d.h"
|
||||
#include "gtest/gtest.h"
|
||||
|
||||
using namespace frc;
|
||||
|
||||
TEST(AprilTagPoseSetOriginTest, TransformationMatches) {
|
||||
auto layout = AprilTagFieldLayout{
|
||||
std::vector<AprilTag>{
|
||||
AprilTag{1,
|
||||
Pose3d{0_ft, 0_ft, 0_ft, Rotation3d{0_deg, 0_deg, 0_deg}}},
|
||||
AprilTag{
|
||||
2, Pose3d{4_ft, 4_ft, 4_ft, Rotation3d{0_deg, 0_deg, 180_deg}}}},
|
||||
54_ft, 27_ft};
|
||||
|
||||
layout.SetOrigin(
|
||||
AprilTagFieldLayout::OriginPosition::kRedAllianceWallRightSide);
|
||||
|
||||
auto mirrorPose =
|
||||
Pose3d{54_ft, 27_ft, 0_ft, Rotation3d{0_deg, 0_deg, 180_deg}};
|
||||
EXPECT_EQ(mirrorPose, *layout.GetTagPose(1));
|
||||
mirrorPose = Pose3d{50_ft, 23_ft, 4_ft, Rotation3d{0_deg, 0_deg, 0_deg}};
|
||||
EXPECT_EQ(mirrorPose, *layout.GetTagPose(2));
|
||||
}
|
||||
61
apriltag/src/test/native/cpp/LoadConfigTest.cpp
Normal file
61
apriltag/src/test/native/cpp/LoadConfigTest.cpp
Normal file
@@ -0,0 +1,61 @@
|
||||
// Copyright (c) FIRST and other WPILib contributors.
|
||||
// Open Source Software; you can modify and/or share it under the terms of
|
||||
// the WPILib BSD license file in the root directory of this project.
|
||||
|
||||
#include "frc/apriltag/AprilTagFields.h"
|
||||
#include "gtest/gtest.h"
|
||||
|
||||
namespace frc {
|
||||
|
||||
std::vector<AprilTagField> GetAllFields() {
|
||||
std::vector<AprilTagField> output;
|
||||
|
||||
for (int i = 0; i < static_cast<int>(AprilTagField::kNumFields); ++i) {
|
||||
output.push_back(static_cast<AprilTagField>(i));
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
TEST(AprilTagFieldsTest, TestLoad2022RapidReact) {
|
||||
AprilTagFieldLayout layout =
|
||||
LoadAprilTagLayoutField(AprilTagField::k2022RapidReact);
|
||||
|
||||
// Blue Hangar Truss - Hub
|
||||
auto expectedPose =
|
||||
Pose3d{127.272_in, 216.01_in, 67.932_in, Rotation3d{0_deg, 0_deg, 0_deg}};
|
||||
auto maybePose = layout.GetTagPose(1);
|
||||
EXPECT_TRUE(maybePose);
|
||||
EXPECT_EQ(expectedPose, *maybePose);
|
||||
|
||||
// Blue Terminal Near Station
|
||||
expectedPose = Pose3d{4.768_in, 67.631_in, 35.063_in,
|
||||
Rotation3d{0_deg, 0_deg, 46.25_deg}};
|
||||
maybePose = layout.GetTagPose(5);
|
||||
EXPECT_TRUE(maybePose);
|
||||
EXPECT_EQ(expectedPose, *maybePose);
|
||||
|
||||
// Upper Hub Blue-Near
|
||||
expectedPose = Pose3d{332.321_in, 183.676_in, 95.186_in,
|
||||
Rotation3d{0_deg, 26.75_deg, 69_deg}};
|
||||
maybePose = layout.GetTagPose(53);
|
||||
EXPECT_TRUE(maybePose);
|
||||
EXPECT_EQ(expectedPose, *maybePose);
|
||||
|
||||
// Doesn't exist
|
||||
maybePose = layout.GetTagPose(54);
|
||||
EXPECT_FALSE(maybePose);
|
||||
}
|
||||
|
||||
// Test all of the fields in the enum
|
||||
class AllFieldsFixtureTest : public ::testing::TestWithParam<AprilTagField> {};
|
||||
|
||||
TEST_P(AllFieldsFixtureTest, CheckEntireEnum) {
|
||||
AprilTagField field = GetParam();
|
||||
EXPECT_NO_THROW(LoadAprilTagLayoutField(field));
|
||||
}
|
||||
|
||||
INSTANTIATE_TEST_SUITE_P(ValuesEnumTestInstTests, AllFieldsFixtureTest,
|
||||
::testing::ValuesIn(GetAllFields()));
|
||||
|
||||
} // namespace frc
|
||||
11
apriltag/src/test/native/cpp/main.cpp
Normal file
11
apriltag/src/test/native/cpp/main.cpp
Normal file
@@ -0,0 +1,11 @@
|
||||
// Copyright (c) FIRST and other WPILib contributors.
|
||||
// Open Source Software; you can modify and/or share it under the terms of
|
||||
// the WPILib BSD license file in the root directory of this project.
|
||||
|
||||
#include "gtest/gtest.h"
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
::testing::InitGoogleTest(&argc, argv);
|
||||
int ret = RUN_ALL_TESTS();
|
||||
return ret;
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 9.8 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 12 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 11 KiB |
@@ -15,7 +15,7 @@ stages:
|
||||
vmImage: 'ubuntu-latest'
|
||||
|
||||
container:
|
||||
image: wpilib/roborio-cross-ubuntu:2022-18.04
|
||||
image: wpilib/roborio-cross-ubuntu:2023-22.04
|
||||
|
||||
timeoutInMinutes: 0
|
||||
|
||||
@@ -38,6 +38,7 @@ stages:
|
||||
|
||||
- stage: TestBench
|
||||
displayName: Test Bench
|
||||
condition: false
|
||||
jobs:
|
||||
- job: Cpp
|
||||
displayName: C++
|
||||
|
||||
21
build.gradle
21
build.gradle
@@ -13,16 +13,16 @@ buildscript {
|
||||
|
||||
plugins {
|
||||
id 'base'
|
||||
id 'edu.wpi.first.wpilib.versioning.WPILibVersioningPlugin' version '4.1.0'
|
||||
id 'edu.wpi.first.wpilib.versioning.WPILibVersioningPlugin' version '2023.0.1'
|
||||
id 'edu.wpi.first.wpilib.repositories.WPILibRepositoriesPlugin' version '2020.2'
|
||||
id 'edu.wpi.first.NativeUtils' apply false
|
||||
id 'edu.wpi.first.GradleJni' version '1.0.0'
|
||||
id 'edu.wpi.first.GradleJni' version '1.1.0'
|
||||
id 'edu.wpi.first.GradleVsCode'
|
||||
id 'idea'
|
||||
id 'visual-studio'
|
||||
id 'net.ltgt.errorprone' version '2.0.2' apply false
|
||||
id 'com.github.johnrengelman.shadow' version '7.1.2' apply false
|
||||
id 'com.diffplug.spotless' version '6.4.2' apply false
|
||||
id 'com.diffplug.spotless' version '6.12.0' apply false
|
||||
id 'com.github.spotbugs' version '5.0.8' apply false
|
||||
}
|
||||
|
||||
@@ -108,6 +108,11 @@ subprojects {
|
||||
subproj.apply plugin: MultiBuilds
|
||||
}
|
||||
|
||||
plugins.withType(JavaPlugin) {
|
||||
sourceCompatibility = 11
|
||||
targetCompatibility = 11
|
||||
}
|
||||
|
||||
apply from: "${rootDir}/shared/java/javastyle.gradle"
|
||||
|
||||
// Disables doclint in java 8.
|
||||
@@ -119,6 +124,10 @@ subprojects {
|
||||
}
|
||||
}
|
||||
|
||||
tasks.withType(JavaCompile) {
|
||||
options.compilerArgs.add '-XDstringConcat=inline'
|
||||
}
|
||||
|
||||
// Enables UTF-8 support in Javadoc
|
||||
tasks.withType(Javadoc) {
|
||||
options.addStringOption("charset", "utf-8")
|
||||
@@ -127,8 +136,10 @@ subprojects {
|
||||
}
|
||||
|
||||
// Sign outputs with Developer ID
|
||||
if (project.hasProperty("developerID")) {
|
||||
tasks.withType(AbstractLinkTask) { task ->
|
||||
tasks.withType(AbstractLinkTask) { task ->
|
||||
task.inputs.property "HasDeveloperId", project.hasProperty("developerID")
|
||||
|
||||
if (project.hasProperty("developerID")) {
|
||||
// Don't sign any executables because codesign complains
|
||||
// about relative rpath.
|
||||
if (!(task instanceof LinkExecutable)) {
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
repositories {
|
||||
maven {
|
||||
url "https://plugins.gradle.org/m2/"
|
||||
mavenLocal()
|
||||
maven {
|
||||
url = 'https://frcmaven.wpi.edu/artifactory/ex-gradle'
|
||||
}
|
||||
mavenCentral()
|
||||
url "https://plugins.gradle.org/m2/"
|
||||
}
|
||||
}
|
||||
dependencies {
|
||||
implementation "edu.wpi.first:native-utils:2023.1.0"
|
||||
implementation "edu.wpi.first:native-utils:2023.11.1"
|
||||
}
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import groovy.transform.CompileStatic
|
||||
import javax.inject.Inject
|
||||
import edu.wpi.first.deployutils.deploy.artifact.MavenArtifact
|
||||
import edu.wpi.first.deployutils.deploy.context.DeployContext
|
||||
import org.gradle.api.Project
|
||||
import edu.wpi.first.deployutils.ActionWrapper
|
||||
import edu.wpi.first.deployutils.deploy.target.RemoteTarget
|
||||
import edu.wpi.first.deployutils.PredicateWrapper
|
||||
import groovy.transform.CompileStatic;
|
||||
import javax.inject.Inject;
|
||||
|
||||
import java.util.function.Function
|
||||
import org.gradle.api.Project;
|
||||
|
||||
import edu.wpi.first.deployutils.deploy.CommandDeployResult;
|
||||
import edu.wpi.first.deployutils.deploy.artifact.MavenArtifact;
|
||||
import edu.wpi.first.deployutils.deploy.context.DeployContext;
|
||||
import edu.wpi.first.deployutils.deploy.target.RemoteTarget;
|
||||
import edu.wpi.first.deployutils.PredicateWrapper;
|
||||
import edu.wpi.first.deployutils.ActionWrapper;
|
||||
|
||||
@CompileStatic
|
||||
public class WPIJREArtifact extends MavenArtifact {
|
||||
@@ -17,6 +18,18 @@ public class WPIJREArtifact extends MavenArtifact {
|
||||
return configName;
|
||||
}
|
||||
|
||||
public boolean isCheckJreVersion() {
|
||||
return checkJreVersion;
|
||||
}
|
||||
|
||||
public void setCheckJreVersion(boolean checkJreVersion) {
|
||||
this.checkJreVersion = checkJreVersion;
|
||||
}
|
||||
|
||||
private boolean checkJreVersion = true;
|
||||
|
||||
private final String artifactLocation = "edu.wpi.first.jdk:roborio-2023:17.0.5u7-1"
|
||||
|
||||
@Inject
|
||||
public WPIJREArtifact(String name, RemoteTarget target) {
|
||||
super(name, target);
|
||||
@@ -24,10 +37,10 @@ public class WPIJREArtifact extends MavenArtifact {
|
||||
this.configName = configName;
|
||||
Project project = target.getProject();
|
||||
getConfiguration().set(project.getConfigurations().create(configName));
|
||||
getDependency().set(project.getDependencies().add(configName, "edu.wpi.first.jdk:roborio-2022:11.0.12u5-1"));
|
||||
getDependency().set(project.getDependencies().add(configName, artifactLocation));
|
||||
|
||||
setOnlyIf(new PredicateWrapper({ DeployContext ctx ->
|
||||
return jreMissing(ctx) || project.hasProperty("force-redeploy-jre");
|
||||
return jreMissing(ctx) || jreOutOfDate(ctx) || project.hasProperty("force-redeploy-jre");
|
||||
}));
|
||||
|
||||
getDirectory().set("/tmp");
|
||||
@@ -35,7 +48,7 @@ public class WPIJREArtifact extends MavenArtifact {
|
||||
|
||||
getPostdeploy().add(new ActionWrapper({ DeployContext ctx ->
|
||||
ctx.getLogger().log("Installing JRE...");
|
||||
ctx.execute("opkg remove frc2022-openjdk*; opkg install /tmp/frcjre.ipk; rm /tmp/frcjre.ipk");
|
||||
ctx.execute("opkg remove frc*-openjdk*; opkg install /tmp/frcjre.ipk; rm /tmp/frcjre.ipk");
|
||||
ctx.getLogger().log("JRE Deployed!");
|
||||
}));
|
||||
}
|
||||
@@ -44,5 +57,21 @@ public class WPIJREArtifact extends MavenArtifact {
|
||||
return ctx.execute("if [[ -f \"/usr/local/frc/JRE/bin/java\" ]]; then echo OK; else echo MISSING; fi").getResult().contains("MISSING");
|
||||
}
|
||||
|
||||
|
||||
private boolean jreOutOfDate(DeployContext ctx) {
|
||||
if (!checkJreVersion) {
|
||||
return false;
|
||||
}
|
||||
String version = getDependency().get().getVersion();
|
||||
CommandDeployResult cmdResult = ctx.execute("opkg list-installed | grep openjdk");
|
||||
if (cmdResult.getExitCode() != 0) {
|
||||
ctx.getLogger().log("JRE not found");
|
||||
return false;
|
||||
}
|
||||
String result = cmdResult.getResult().trim();
|
||||
ctx.getLogger().log("Searching for JRE " + version);
|
||||
ctx.getLogger().log("Found JRE " + result);
|
||||
boolean matches = result.contains(version);
|
||||
ctx.getLogger().log(matches ? "JRE Is Correct Version" : "JRE is mismatched. Reinstalling");
|
||||
return !matches;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,7 +57,7 @@ model {
|
||||
if (!it.buildable || !(it instanceof NativeBinarySpec)) {
|
||||
return
|
||||
}
|
||||
lib project: ':ntcore', library: 'ntcore', linkage: 'shared'
|
||||
project(':ntcore').addNtcoreDependency(it, 'shared')
|
||||
lib project: ':cscore', library: 'cscore', linkage: 'shared'
|
||||
lib project: ':wpinet', library: 'wpinet', linkage: 'shared'
|
||||
lib project: ':wpiutil', library: 'wpiutil', linkage: 'shared'
|
||||
|
||||
@@ -55,7 +55,7 @@ model {
|
||||
}
|
||||
binaries.all { binary ->
|
||||
lib project: ':cameraserver', library: 'cameraserver', linkage: 'static'
|
||||
lib project: ':ntcore', library: 'ntcore', linkage: 'static'
|
||||
project(':ntcore').addNtcoreDependency(binary, 'static')
|
||||
lib project: ':cscore', library: 'cscore', linkage: 'static'
|
||||
lib project: ':wpinet', library: 'wpinet', linkage: 'static'
|
||||
lib project: ':wpiutil', library: 'wpiutil', linkage: 'static'
|
||||
|
||||
@@ -175,7 +175,8 @@ public final class Main {
|
||||
ntinst.startServer();
|
||||
} else {
|
||||
System.out.println("Setting up NetworkTables client for team " + team);
|
||||
ntinst.startClientTeam(team);
|
||||
ntinst.setServerTeam(team);
|
||||
ntinst.startClient4("multicameraserver");
|
||||
}
|
||||
|
||||
// start cameras
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
#include <cstdio>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
|
||||
#include <networktables/NetworkTableInstance.h>
|
||||
@@ -182,7 +183,8 @@ int main(int argc, char* argv[]) {
|
||||
ntinst.StartServer();
|
||||
} else {
|
||||
fmt::print("Setting up NetworkTables client for team {}\n", team);
|
||||
ntinst.StartClientTeam(team);
|
||||
ntinst.StartClient4("multicameraserver");
|
||||
ntinst.SetServerTeam(team);
|
||||
}
|
||||
|
||||
// start cameras
|
||||
|
||||
@@ -15,13 +15,18 @@ import edu.wpi.first.cscore.VideoException;
|
||||
import edu.wpi.first.cscore.VideoListener;
|
||||
import edu.wpi.first.cscore.VideoMode;
|
||||
import edu.wpi.first.cscore.VideoMode.PixelFormat;
|
||||
import edu.wpi.first.cscore.VideoProperty;
|
||||
import edu.wpi.first.cscore.VideoSink;
|
||||
import edu.wpi.first.cscore.VideoSource;
|
||||
import edu.wpi.first.networktables.EntryListenerFlags;
|
||||
import edu.wpi.first.networktables.BooleanEntry;
|
||||
import edu.wpi.first.networktables.BooleanPublisher;
|
||||
import edu.wpi.first.networktables.IntegerEntry;
|
||||
import edu.wpi.first.networktables.IntegerPublisher;
|
||||
import edu.wpi.first.networktables.NetworkTable;
|
||||
import edu.wpi.first.networktables.NetworkTableEntry;
|
||||
import edu.wpi.first.networktables.NetworkTableInstance;
|
||||
import edu.wpi.first.networktables.StringArrayPublisher;
|
||||
import edu.wpi.first.networktables.StringArrayTopic;
|
||||
import edu.wpi.first.networktables.StringEntry;
|
||||
import edu.wpi.first.networktables.StringPublisher;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
@@ -38,11 +43,167 @@ public final class CameraServer {
|
||||
|
||||
private static final String kPublishName = "/CameraPublisher";
|
||||
|
||||
private static final class PropertyPublisher implements AutoCloseable {
|
||||
@SuppressWarnings({"PMD.MissingBreakInSwitch", "PMD.ImplicitSwitchFallThrough", "fallthrough"})
|
||||
PropertyPublisher(NetworkTable table, VideoEvent event) {
|
||||
String name;
|
||||
String infoName;
|
||||
if (event.name.startsWith("raw_")) {
|
||||
name = "RawProperty/" + event.name;
|
||||
infoName = "RawPropertyInfo/" + event.name;
|
||||
} else {
|
||||
name = "Property/" + event.name;
|
||||
infoName = "PropertyInfo/" + event.name;
|
||||
}
|
||||
|
||||
try {
|
||||
switch (event.propertyKind) {
|
||||
case kBoolean:
|
||||
m_booleanValueEntry = table.getBooleanTopic(name).getEntry(false);
|
||||
m_booleanValueEntry.setDefault(event.value != 0);
|
||||
break;
|
||||
case kEnum:
|
||||
m_choicesTopic = table.getStringArrayTopic(infoName + "/choices");
|
||||
// fall through
|
||||
case kInteger:
|
||||
m_integerValueEntry = table.getIntegerTopic(name).getEntry(0);
|
||||
m_minPublisher = table.getIntegerTopic(infoName + "/min").publish();
|
||||
m_maxPublisher = table.getIntegerTopic(infoName + "/max").publish();
|
||||
m_stepPublisher = table.getIntegerTopic(infoName + "/step").publish();
|
||||
m_defaultPublisher = table.getIntegerTopic(infoName + "/default").publish();
|
||||
|
||||
m_integerValueEntry.setDefault(event.value);
|
||||
m_minPublisher.set(CameraServerJNI.getPropertyMin(event.propertyHandle));
|
||||
m_maxPublisher.set(CameraServerJNI.getPropertyMax(event.propertyHandle));
|
||||
m_stepPublisher.set(CameraServerJNI.getPropertyStep(event.propertyHandle));
|
||||
m_defaultPublisher.set(CameraServerJNI.getPropertyDefault(event.propertyHandle));
|
||||
break;
|
||||
case kString:
|
||||
m_stringValueEntry = table.getStringTopic(name).getEntry("");
|
||||
m_stringValueEntry.setDefault(event.valueStr);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
} catch (VideoException ignored) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
void update(VideoEvent event) {
|
||||
switch (event.propertyKind) {
|
||||
case kBoolean:
|
||||
if (m_booleanValueEntry != null) {
|
||||
m_booleanValueEntry.set(event.value != 0);
|
||||
}
|
||||
break;
|
||||
case kInteger:
|
||||
case kEnum:
|
||||
if (m_integerValueEntry != null) {
|
||||
m_integerValueEntry.set(event.value);
|
||||
}
|
||||
break;
|
||||
case kString:
|
||||
if (m_stringValueEntry != null) {
|
||||
m_stringValueEntry.set(event.valueStr);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws Exception {
|
||||
if (m_booleanValueEntry != null) {
|
||||
m_booleanValueEntry.close();
|
||||
}
|
||||
if (m_integerValueEntry != null) {
|
||||
m_integerValueEntry.close();
|
||||
}
|
||||
if (m_stringValueEntry != null) {
|
||||
m_stringValueEntry.close();
|
||||
}
|
||||
if (m_minPublisher != null) {
|
||||
m_minPublisher.close();
|
||||
}
|
||||
if (m_maxPublisher != null) {
|
||||
m_maxPublisher.close();
|
||||
}
|
||||
if (m_stepPublisher != null) {
|
||||
m_stepPublisher.close();
|
||||
}
|
||||
if (m_defaultPublisher != null) {
|
||||
m_defaultPublisher.close();
|
||||
}
|
||||
if (m_choicesPublisher != null) {
|
||||
m_choicesPublisher.close();
|
||||
}
|
||||
}
|
||||
|
||||
BooleanEntry m_booleanValueEntry;
|
||||
IntegerEntry m_integerValueEntry;
|
||||
StringEntry m_stringValueEntry;
|
||||
IntegerPublisher m_minPublisher;
|
||||
IntegerPublisher m_maxPublisher;
|
||||
IntegerPublisher m_stepPublisher;
|
||||
IntegerPublisher m_defaultPublisher;
|
||||
StringArrayTopic m_choicesTopic;
|
||||
StringArrayPublisher m_choicesPublisher;
|
||||
}
|
||||
|
||||
private static final class SourcePublisher implements AutoCloseable {
|
||||
SourcePublisher(NetworkTable table, int sourceHandle) {
|
||||
this.m_table = table;
|
||||
m_sourcePublisher = table.getStringTopic("source").publish();
|
||||
m_descriptionPublisher = table.getStringTopic("description").publish();
|
||||
m_connectedPublisher = table.getBooleanTopic("connected").publish();
|
||||
m_streamsPublisher = table.getStringArrayTopic("streams").publish();
|
||||
m_modeEntry = table.getStringTopic("mode").getEntry("");
|
||||
m_modesPublisher = table.getStringArrayTopic("modes").publish();
|
||||
|
||||
m_sourcePublisher.set(makeSourceValue(sourceHandle));
|
||||
m_descriptionPublisher.set(CameraServerJNI.getSourceDescription(sourceHandle));
|
||||
m_connectedPublisher.set(CameraServerJNI.isSourceConnected(sourceHandle));
|
||||
m_streamsPublisher.set(getSourceStreamValues(sourceHandle));
|
||||
|
||||
try {
|
||||
VideoMode mode = CameraServerJNI.getSourceVideoMode(sourceHandle);
|
||||
m_modeEntry.setDefault(videoModeToString(mode));
|
||||
m_modesPublisher.set(getSourceModeValues(sourceHandle));
|
||||
} catch (VideoException ignored) {
|
||||
// Do nothing. Let the other event handlers update this if there is an error.
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws Exception {
|
||||
m_sourcePublisher.close();
|
||||
m_descriptionPublisher.close();
|
||||
m_connectedPublisher.close();
|
||||
m_streamsPublisher.close();
|
||||
m_modeEntry.close();
|
||||
m_modesPublisher.close();
|
||||
for (PropertyPublisher pp : m_properties.values()) {
|
||||
pp.close();
|
||||
}
|
||||
}
|
||||
|
||||
final NetworkTable m_table;
|
||||
final StringPublisher m_sourcePublisher;
|
||||
final StringPublisher m_descriptionPublisher;
|
||||
final BooleanPublisher m_connectedPublisher;
|
||||
final StringArrayPublisher m_streamsPublisher;
|
||||
final StringEntry m_modeEntry;
|
||||
final StringArrayPublisher m_modesPublisher;
|
||||
final Map<Integer, PropertyPublisher> m_properties = new HashMap<>();
|
||||
}
|
||||
|
||||
private static final AtomicInteger m_defaultUsbDevice = new AtomicInteger();
|
||||
private static String m_primarySourceName;
|
||||
private static final Map<String, VideoSource> m_sources = new HashMap<>();
|
||||
private static final Map<String, VideoSink> m_sinks = new HashMap<>();
|
||||
private static final Map<Integer, NetworkTable> m_tables =
|
||||
private static final Map<Integer, SourcePublisher> m_publishers =
|
||||
new HashMap<>(); // indexed by source handle
|
||||
// source handle indexed by sink handle
|
||||
private static final Map<Integer, Integer> m_fixedSources = new HashMap<>();
|
||||
@@ -61,189 +222,124 @@ public final class CameraServer {
|
||||
// - "PropertyInfo/{Property}" - Property supporting information
|
||||
|
||||
// Listener for video events
|
||||
@SuppressWarnings("PMD.UnusedPrivateField")
|
||||
@SuppressWarnings({"PMD.UnusedPrivateField", "PMD.AvoidCatchingGenericException"})
|
||||
private static final VideoListener m_videoListener =
|
||||
new VideoListener(
|
||||
event -> {
|
||||
switch (event.kind) {
|
||||
case kSourceCreated:
|
||||
{
|
||||
// Create subtable for the camera
|
||||
NetworkTable table = m_publishTable.getSubTable(event.name);
|
||||
m_tables.put(event.sourceHandle, table);
|
||||
table.getEntry("source").setString(makeSourceValue(event.sourceHandle));
|
||||
table
|
||||
.getEntry("description")
|
||||
.setString(CameraServerJNI.getSourceDescription(event.sourceHandle));
|
||||
table
|
||||
.getEntry("connected")
|
||||
.setBoolean(CameraServerJNI.isSourceConnected(event.sourceHandle));
|
||||
table
|
||||
.getEntry("streams")
|
||||
.setStringArray(getSourceStreamValues(event.sourceHandle));
|
||||
try {
|
||||
VideoMode mode = CameraServerJNI.getSourceVideoMode(event.sourceHandle);
|
||||
table.getEntry("mode").setDefaultString(videoModeToString(mode));
|
||||
table.getEntry("modes").setStringArray(getSourceModeValues(event.sourceHandle));
|
||||
} catch (VideoException ignored) {
|
||||
// Do nothing. Let the other event handlers update this if there is an error.
|
||||
synchronized (CameraServer.class) {
|
||||
switch (event.kind) {
|
||||
case kSourceCreated:
|
||||
{
|
||||
// Create subtable for the camera
|
||||
NetworkTable table = m_publishTable.getSubTable(event.name);
|
||||
m_publishers.put(
|
||||
event.sourceHandle, new SourcePublisher(table, event.sourceHandle));
|
||||
break;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case kSourceDestroyed:
|
||||
{
|
||||
NetworkTable table = m_tables.get(event.sourceHandle);
|
||||
if (table != null) {
|
||||
table.getEntry("source").setString("");
|
||||
table.getEntry("streams").setStringArray(new String[0]);
|
||||
table.getEntry("modes").setStringArray(new String[0]);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case kSourceConnected:
|
||||
{
|
||||
NetworkTable table = m_tables.get(event.sourceHandle);
|
||||
if (table != null) {
|
||||
// update the description too (as it may have changed)
|
||||
table
|
||||
.getEntry("description")
|
||||
.setString(CameraServerJNI.getSourceDescription(event.sourceHandle));
|
||||
table.getEntry("connected").setBoolean(true);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case kSourceDisconnected:
|
||||
{
|
||||
NetworkTable table = m_tables.get(event.sourceHandle);
|
||||
if (table != null) {
|
||||
table.getEntry("connected").setBoolean(false);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case kSourceVideoModesUpdated:
|
||||
{
|
||||
NetworkTable table = m_tables.get(event.sourceHandle);
|
||||
if (table != null) {
|
||||
table.getEntry("modes").setStringArray(getSourceModeValues(event.sourceHandle));
|
||||
}
|
||||
break;
|
||||
}
|
||||
case kSourceVideoModeChanged:
|
||||
{
|
||||
NetworkTable table = m_tables.get(event.sourceHandle);
|
||||
if (table != null) {
|
||||
table.getEntry("mode").setString(videoModeToString(event.mode));
|
||||
}
|
||||
break;
|
||||
}
|
||||
case kSourcePropertyCreated:
|
||||
{
|
||||
NetworkTable table = m_tables.get(event.sourceHandle);
|
||||
if (table != null) {
|
||||
putSourcePropertyValue(table, event, true);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case kSourcePropertyValueUpdated:
|
||||
{
|
||||
NetworkTable table = m_tables.get(event.sourceHandle);
|
||||
if (table != null) {
|
||||
putSourcePropertyValue(table, event, false);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case kSourcePropertyChoicesUpdated:
|
||||
{
|
||||
NetworkTable table = m_tables.get(event.sourceHandle);
|
||||
if (table != null) {
|
||||
try {
|
||||
String[] choices =
|
||||
CameraServerJNI.getEnumPropertyChoices(event.propertyHandle);
|
||||
table
|
||||
.getEntry("PropertyInfo/" + event.name + "/choices")
|
||||
.setStringArray(choices);
|
||||
} catch (VideoException ignored) {
|
||||
// ignore
|
||||
case kSourceDestroyed:
|
||||
{
|
||||
SourcePublisher publisher = m_publishers.remove(event.sourceHandle);
|
||||
if (publisher != null) {
|
||||
try {
|
||||
publisher.close();
|
||||
} catch (Exception e) {
|
||||
// ignore (nothing we can do about it)
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case kSourceConnected:
|
||||
{
|
||||
SourcePublisher publisher = m_publishers.get(event.sourceHandle);
|
||||
if (publisher != null) {
|
||||
// update the description too (as it may have changed)
|
||||
publisher.m_descriptionPublisher.set(
|
||||
CameraServerJNI.getSourceDescription(event.sourceHandle));
|
||||
publisher.m_connectedPublisher.set(true);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case kSourceDisconnected:
|
||||
{
|
||||
SourcePublisher publisher = m_publishers.get(event.sourceHandle);
|
||||
if (publisher != null) {
|
||||
publisher.m_connectedPublisher.set(false);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case kSourceVideoModesUpdated:
|
||||
{
|
||||
SourcePublisher publisher = m_publishers.get(event.sourceHandle);
|
||||
if (publisher != null) {
|
||||
publisher.m_modesPublisher.set(getSourceModeValues(event.sourceHandle));
|
||||
}
|
||||
break;
|
||||
}
|
||||
case kSourceVideoModeChanged:
|
||||
{
|
||||
SourcePublisher publisher = m_publishers.get(event.sourceHandle);
|
||||
if (publisher != null) {
|
||||
publisher.m_modeEntry.set(videoModeToString(event.mode));
|
||||
}
|
||||
break;
|
||||
}
|
||||
case kSourcePropertyCreated:
|
||||
{
|
||||
SourcePublisher publisher = m_publishers.get(event.sourceHandle);
|
||||
if (publisher != null) {
|
||||
publisher.m_properties.put(
|
||||
event.propertyHandle, new PropertyPublisher(publisher.m_table, event));
|
||||
}
|
||||
break;
|
||||
}
|
||||
case kSourcePropertyValueUpdated:
|
||||
{
|
||||
SourcePublisher publisher = m_publishers.get(event.sourceHandle);
|
||||
if (publisher != null) {
|
||||
PropertyPublisher pp = publisher.m_properties.get(event.propertyHandle);
|
||||
if (pp != null) {
|
||||
pp.update(event);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case kSourcePropertyChoicesUpdated:
|
||||
{
|
||||
SourcePublisher publisher = m_publishers.get(event.sourceHandle);
|
||||
if (publisher != null) {
|
||||
PropertyPublisher pp = publisher.m_properties.get(event.propertyHandle);
|
||||
if (pp != null && pp.m_choicesTopic != null) {
|
||||
try {
|
||||
String[] choices =
|
||||
CameraServerJNI.getEnumPropertyChoices(event.propertyHandle);
|
||||
if (pp.m_choicesPublisher == null) {
|
||||
pp.m_choicesPublisher = pp.m_choicesTopic.publish();
|
||||
}
|
||||
pp.m_choicesPublisher.set(choices);
|
||||
} catch (VideoException ignored) {
|
||||
// ignore (just don't publish choices if we can't get them)
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case kSinkSourceChanged:
|
||||
case kSinkCreated:
|
||||
case kSinkDestroyed:
|
||||
case kNetworkInterfacesChanged:
|
||||
{
|
||||
m_addresses = CameraServerJNI.getNetworkInterfaces();
|
||||
updateStreamValues();
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
case kSinkSourceChanged:
|
||||
case kSinkCreated:
|
||||
case kSinkDestroyed:
|
||||
case kNetworkInterfacesChanged:
|
||||
{
|
||||
m_addresses = CameraServerJNI.getNetworkInterfaces();
|
||||
updateStreamValues();
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
},
|
||||
0x4fff,
|
||||
true);
|
||||
|
||||
@SuppressWarnings("PMD.UnusedPrivateField")
|
||||
private static final int m_tableListener =
|
||||
NetworkTableInstance.getDefault()
|
||||
.addEntryListener(
|
||||
kPublishName + "/",
|
||||
event -> {
|
||||
String relativeKey = event.name.substring(kPublishName.length() + 1);
|
||||
|
||||
// get source (sourceName/...)
|
||||
int subKeyIndex = relativeKey.indexOf('/');
|
||||
if (subKeyIndex == -1) {
|
||||
return;
|
||||
}
|
||||
String sourceName = relativeKey.substring(0, subKeyIndex);
|
||||
VideoSource source = m_sources.get(sourceName);
|
||||
if (source == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// get subkey
|
||||
relativeKey = relativeKey.substring(subKeyIndex + 1);
|
||||
|
||||
// handle standard names
|
||||
String propName;
|
||||
if ("mode".equals(relativeKey)) {
|
||||
// reset to current mode
|
||||
event.getEntry().setString(videoModeToString(source.getVideoMode()));
|
||||
return;
|
||||
} else if (relativeKey.startsWith("Property/")) {
|
||||
propName = relativeKey.substring(9);
|
||||
} else if (relativeKey.startsWith("RawProperty/")) {
|
||||
propName = relativeKey.substring(12);
|
||||
} else {
|
||||
return; // ignore
|
||||
}
|
||||
|
||||
// everything else is a property
|
||||
VideoProperty property = source.getProperty(propName);
|
||||
switch (property.getKind()) {
|
||||
case kNone:
|
||||
return;
|
||||
case kBoolean:
|
||||
// reset to current setting
|
||||
event.getEntry().setBoolean(property.get() != 0);
|
||||
return;
|
||||
case kInteger:
|
||||
case kEnum:
|
||||
// reset to current setting
|
||||
event.getEntry().setDouble(property.get());
|
||||
return;
|
||||
case kString:
|
||||
// reset to current setting
|
||||
event.getEntry().setString(property.getString());
|
||||
return;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
},
|
||||
EntryListenerFlags.kImmediate | EntryListenerFlags.kUpdate);
|
||||
|
||||
private static int m_nextPort = kBasePort;
|
||||
private static String[] m_addresses = new String[0];
|
||||
|
||||
@@ -369,8 +465,8 @@ public final class CameraServer {
|
||||
if (source == 0) {
|
||||
continue;
|
||||
}
|
||||
NetworkTable table = m_tables.get(source);
|
||||
if (table != null) {
|
||||
SourcePublisher publisher = m_publishers.get(source);
|
||||
if (publisher != null) {
|
||||
// Don't set stream values if this is a HttpCamera passthrough
|
||||
if (VideoSource.getKindFromInt(CameraServerJNI.getSourceKind(source))
|
||||
== VideoSource.Kind.kHttp) {
|
||||
@@ -380,7 +476,7 @@ public final class CameraServer {
|
||||
// Set table value
|
||||
String[] values = getSinkStreamValues(sink);
|
||||
if (values.length > 0) {
|
||||
table.getEntry("streams").setStringArray(values);
|
||||
publisher.m_streamsPublisher.set(values);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -390,12 +486,12 @@ public final class CameraServer {
|
||||
int source = i.getHandle();
|
||||
|
||||
// Get the source's subtable (if none exists, we're done)
|
||||
NetworkTable table = m_tables.get(source);
|
||||
if (table != null) {
|
||||
SourcePublisher publisher = m_publishers.get(source);
|
||||
if (publisher != null) {
|
||||
// Set table value
|
||||
String[] values = getSourceStreamValues(source);
|
||||
if (values.length > 0) {
|
||||
table.getEntry("streams").setStringArray(values);
|
||||
publisher.m_streamsPublisher.set(values);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -449,69 +545,6 @@ public final class CameraServer {
|
||||
return modeStrings;
|
||||
}
|
||||
|
||||
/**
|
||||
* Publish a source property value to NetworkTables.
|
||||
*
|
||||
* @param table NetworkTable to which to push value.
|
||||
* @param event Video event.
|
||||
* @param isNew Whether the property value hasn't been pushed to NetworkTables before.
|
||||
*/
|
||||
private static void putSourcePropertyValue(NetworkTable table, VideoEvent event, boolean isNew) {
|
||||
String name;
|
||||
String infoName;
|
||||
if (event.name.startsWith("raw_")) {
|
||||
name = "RawProperty/" + event.name;
|
||||
infoName = "RawPropertyInfo/" + event.name;
|
||||
} else {
|
||||
name = "Property/" + event.name;
|
||||
infoName = "PropertyInfo/" + event.name;
|
||||
}
|
||||
|
||||
NetworkTableEntry entry = table.getEntry(name);
|
||||
try {
|
||||
switch (event.propertyKind) {
|
||||
case kBoolean:
|
||||
if (isNew) {
|
||||
entry.setDefaultBoolean(event.value != 0);
|
||||
} else {
|
||||
entry.setBoolean(event.value != 0);
|
||||
}
|
||||
break;
|
||||
case kInteger:
|
||||
case kEnum:
|
||||
if (isNew) {
|
||||
entry.setDefaultDouble(event.value);
|
||||
table
|
||||
.getEntry(infoName + "/min")
|
||||
.setDouble(CameraServerJNI.getPropertyMin(event.propertyHandle));
|
||||
table
|
||||
.getEntry(infoName + "/max")
|
||||
.setDouble(CameraServerJNI.getPropertyMax(event.propertyHandle));
|
||||
table
|
||||
.getEntry(infoName + "/step")
|
||||
.setDouble(CameraServerJNI.getPropertyStep(event.propertyHandle));
|
||||
table
|
||||
.getEntry(infoName + "/default")
|
||||
.setDouble(CameraServerJNI.getPropertyDefault(event.propertyHandle));
|
||||
} else {
|
||||
entry.setDouble(event.value);
|
||||
}
|
||||
break;
|
||||
case kString:
|
||||
if (isNew) {
|
||||
entry.setDefaultString(event.valueStr);
|
||||
} else {
|
||||
entry.setString(event.valueStr);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
} catch (VideoException ignored) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
private CameraServer() {}
|
||||
|
||||
/**
|
||||
|
||||
@@ -8,8 +8,12 @@
|
||||
#include <vector>
|
||||
|
||||
#include <fmt/format.h>
|
||||
#include <networktables/BooleanTopic.h>
|
||||
#include <networktables/IntegerTopic.h>
|
||||
#include <networktables/NetworkTable.h>
|
||||
#include <networktables/NetworkTableInstance.h>
|
||||
#include <networktables/StringArrayTopic.h>
|
||||
#include <networktables/StringTopic.h>
|
||||
#include <wpi/DenseMap.h>
|
||||
#include <wpi/SmallString.h>
|
||||
#include <wpi/StringExtras.h>
|
||||
@@ -24,9 +28,42 @@ using namespace frc;
|
||||
static constexpr char const* kPublishName = "/CameraPublisher";
|
||||
|
||||
namespace {
|
||||
|
||||
struct Instance;
|
||||
|
||||
struct PropertyPublisher {
|
||||
PropertyPublisher(nt::NetworkTable& table, const cs::VideoEvent& event);
|
||||
|
||||
void Update(const cs::VideoEvent& event);
|
||||
|
||||
nt::BooleanEntry booleanValueEntry;
|
||||
nt::IntegerEntry integerValueEntry;
|
||||
nt::StringEntry stringValueEntry;
|
||||
nt::IntegerPublisher minPublisher;
|
||||
nt::IntegerPublisher maxPublisher;
|
||||
nt::IntegerPublisher stepPublisher;
|
||||
nt::IntegerPublisher defaultPublisher;
|
||||
nt::StringArrayTopic choicesTopic;
|
||||
nt::StringArrayPublisher choicesPublisher;
|
||||
};
|
||||
|
||||
struct SourcePublisher {
|
||||
SourcePublisher(Instance& inst, std::shared_ptr<nt::NetworkTable> table,
|
||||
CS_Source source);
|
||||
|
||||
std::shared_ptr<nt::NetworkTable> table;
|
||||
nt::StringPublisher sourcePublisher;
|
||||
nt::StringPublisher descriptionPublisher;
|
||||
nt::BooleanPublisher connectedPublisher;
|
||||
nt::StringArrayPublisher streamsPublisher;
|
||||
nt::StringEntry modeEntry;
|
||||
nt::StringArrayPublisher modesPublisher;
|
||||
wpi::DenseMap<CS_Property, PropertyPublisher> properties;
|
||||
};
|
||||
|
||||
struct Instance {
|
||||
Instance();
|
||||
std::shared_ptr<nt::NetworkTable> GetSourceTable(CS_Source source);
|
||||
SourcePublisher* GetPublisher(CS_Source source);
|
||||
std::vector<std::string> GetSinkStreamValues(CS_Sink sink);
|
||||
std::vector<std::string> GetSourceStreamValues(CS_Source source);
|
||||
void UpdateStreamValues();
|
||||
@@ -37,7 +74,7 @@ struct Instance {
|
||||
wpi::StringMap<cs::VideoSource> m_sources;
|
||||
wpi::StringMap<cs::VideoSink> m_sinks;
|
||||
wpi::DenseMap<CS_Sink, CS_Source> m_fixedSources;
|
||||
wpi::DenseMap<CS_Source, std::shared_ptr<nt::NetworkTable>> m_tables;
|
||||
wpi::DenseMap<CS_Source, SourcePublisher> m_publishers;
|
||||
std::shared_ptr<nt::NetworkTable> m_publishTable{
|
||||
nt::NetworkTableInstance::GetDefault().GetTable(kPublishName)};
|
||||
cs::VideoListener m_videoListener;
|
||||
@@ -45,6 +82,7 @@ struct Instance {
|
||||
int m_nextPort{CameraServer::kBasePort};
|
||||
std::vector<std::string> m_addresses;
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
static Instance& GetInstance() {
|
||||
@@ -86,9 +124,13 @@ static std::string MakeStreamValue(std::string_view address, int port) {
|
||||
return fmt::format("mjpg:http://{}:{}/?action=stream", address, port);
|
||||
}
|
||||
|
||||
std::shared_ptr<nt::NetworkTable> Instance::GetSourceTable(CS_Source source) {
|
||||
std::scoped_lock lock(m_mutex);
|
||||
return m_tables.lookup(source);
|
||||
SourcePublisher* Instance::GetPublisher(CS_Source source) {
|
||||
auto it = m_publishers.find(source);
|
||||
if (it != m_publishers.end()) {
|
||||
return &it->second;
|
||||
} else {
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<std::string> Instance::GetSinkStreamValues(CS_Sink sink) {
|
||||
@@ -158,7 +200,6 @@ std::vector<std::string> Instance::GetSourceStreamValues(CS_Source source) {
|
||||
}
|
||||
|
||||
void Instance::UpdateStreamValues() {
|
||||
std::scoped_lock lock(m_mutex);
|
||||
// Over all the sinks...
|
||||
for (const auto& i : m_sinks) {
|
||||
CS_Status status = 0;
|
||||
@@ -172,8 +213,7 @@ void Instance::UpdateStreamValues() {
|
||||
if (source == 0) {
|
||||
continue;
|
||||
}
|
||||
auto table = m_tables.lookup(source);
|
||||
if (table) {
|
||||
if (auto publisher = GetPublisher(source)) {
|
||||
// Don't set stream values if this is a HttpCamera passthrough
|
||||
if (cs::GetSourceKind(source, &status) == CS_SOURCE_HTTP) {
|
||||
continue;
|
||||
@@ -182,7 +222,7 @@ void Instance::UpdateStreamValues() {
|
||||
// Set table value
|
||||
auto values = GetSinkStreamValues(sink);
|
||||
if (!values.empty()) {
|
||||
table->GetEntry("streams").SetStringArray(values);
|
||||
publisher->streamsPublisher.Set(values);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -192,12 +232,11 @@ void Instance::UpdateStreamValues() {
|
||||
CS_Source source = i.second.GetHandle();
|
||||
|
||||
// Get the source's subtable (if none exists, we're done)
|
||||
auto table = m_tables.lookup(source);
|
||||
if (table) {
|
||||
if (auto publisher = GetPublisher(source)) {
|
||||
// Set table value
|
||||
auto values = GetSourceStreamValues(source);
|
||||
if (!values.empty()) {
|
||||
table->GetEntry("streams").SetStringArray(values);
|
||||
publisher->streamsPublisher.Set(values);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -234,51 +273,71 @@ static std::vector<std::string> GetSourceModeValues(int source) {
|
||||
return rv;
|
||||
}
|
||||
|
||||
static void PutSourcePropertyValue(nt::NetworkTable* table,
|
||||
const cs::VideoEvent& event, bool isNew) {
|
||||
std::string_view namePrefix;
|
||||
std::string_view infoPrefix;
|
||||
PropertyPublisher::PropertyPublisher(nt::NetworkTable& table,
|
||||
const cs::VideoEvent& event) {
|
||||
std::string name;
|
||||
std::string infoName;
|
||||
if (wpi::starts_with(event.name, "raw_")) {
|
||||
namePrefix = "RawProperty";
|
||||
infoPrefix = "RawPropertyInfo";
|
||||
name = fmt::format("RawProperty/{}", event.name);
|
||||
infoName = fmt::format("RawPropertyInfo/{}", event.name);
|
||||
} else {
|
||||
namePrefix = "Property";
|
||||
infoPrefix = "PropertyInfo";
|
||||
name = fmt::format("Property/{}", event.name);
|
||||
infoName = fmt::format("PropertyInfo/{}", event.name);
|
||||
}
|
||||
|
||||
wpi::SmallString<64> buf;
|
||||
CS_Status status = 0;
|
||||
nt::NetworkTableEntry entry =
|
||||
table->GetEntry(fmt::format("{}/{}", namePrefix, event.name));
|
||||
switch (event.propertyKind) {
|
||||
case CS_PROP_BOOLEAN:
|
||||
if (isNew) {
|
||||
entry.SetDefaultBoolean(event.value != 0);
|
||||
} else {
|
||||
entry.SetBoolean(event.value != 0);
|
||||
booleanValueEntry = table.GetBooleanTopic(name).GetEntry(false);
|
||||
booleanValueEntry.SetDefault(event.value != 0);
|
||||
break;
|
||||
case CS_PROP_ENUM:
|
||||
choicesTopic =
|
||||
table.GetStringArrayTopic(fmt::format("{}/choices", infoName));
|
||||
[[fallthrough]];
|
||||
case CS_PROP_INTEGER:
|
||||
integerValueEntry = table.GetIntegerTopic(name).GetEntry(0);
|
||||
minPublisher =
|
||||
table.GetIntegerTopic(fmt::format("{}/min", infoName)).Publish();
|
||||
maxPublisher =
|
||||
table.GetIntegerTopic(fmt::format("{}/max", infoName)).Publish();
|
||||
stepPublisher =
|
||||
table.GetIntegerTopic(fmt::format("{}/step", infoName)).Publish();
|
||||
defaultPublisher =
|
||||
table.GetIntegerTopic(fmt::format("{}/default", infoName)).Publish();
|
||||
|
||||
integerValueEntry.SetDefault(event.value);
|
||||
minPublisher.Set(cs::GetPropertyMin(event.propertyHandle, &status));
|
||||
maxPublisher.Set(cs::GetPropertyMax(event.propertyHandle, &status));
|
||||
stepPublisher.Set(cs::GetPropertyStep(event.propertyHandle, &status));
|
||||
defaultPublisher.Set(
|
||||
cs::GetPropertyDefault(event.propertyHandle, &status));
|
||||
break;
|
||||
case CS_PROP_STRING:
|
||||
stringValueEntry = table.GetStringTopic(name).GetEntry("");
|
||||
stringValueEntry.SetDefault(event.valueStr);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void PropertyPublisher::Update(const cs::VideoEvent& event) {
|
||||
switch (event.propertyKind) {
|
||||
case CS_PROP_BOOLEAN:
|
||||
if (booleanValueEntry) {
|
||||
booleanValueEntry.Set(event.value != 0);
|
||||
}
|
||||
break;
|
||||
case CS_PROP_INTEGER:
|
||||
case CS_PROP_ENUM:
|
||||
if (isNew) {
|
||||
entry.SetDefaultDouble(event.value);
|
||||
table->GetEntry(fmt::format("{}/{}/min", infoPrefix, event.name))
|
||||
.SetDouble(cs::GetPropertyMin(event.propertyHandle, &status));
|
||||
table->GetEntry(fmt::format("{}/{}/max", infoPrefix, event.name))
|
||||
.SetDouble(cs::GetPropertyMax(event.propertyHandle, &status));
|
||||
table->GetEntry(fmt::format("{}/{}/step", infoPrefix, event.name))
|
||||
.SetDouble(cs::GetPropertyStep(event.propertyHandle, &status));
|
||||
table->GetEntry(fmt::format("{}/{}/default", infoPrefix, event.name))
|
||||
.SetDouble(cs::GetPropertyDefault(event.propertyHandle, &status));
|
||||
} else {
|
||||
entry.SetDouble(event.value);
|
||||
if (integerValueEntry) {
|
||||
integerValueEntry.Set(event.value);
|
||||
}
|
||||
break;
|
||||
case CS_PROP_STRING:
|
||||
if (isNew) {
|
||||
entry.SetDefaultString(event.valueStr);
|
||||
} else {
|
||||
entry.SetString(event.valueStr);
|
||||
if (stringValueEntry) {
|
||||
stringValueEntry.Set(event.valueStr);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
@@ -286,6 +345,28 @@ static void PutSourcePropertyValue(nt::NetworkTable* table,
|
||||
}
|
||||
}
|
||||
|
||||
SourcePublisher::SourcePublisher(Instance& inst,
|
||||
std::shared_ptr<nt::NetworkTable> table,
|
||||
CS_Source source)
|
||||
: table{table},
|
||||
sourcePublisher{table->GetStringTopic("source").Publish()},
|
||||
descriptionPublisher{table->GetStringTopic("description").Publish()},
|
||||
connectedPublisher{table->GetBooleanTopic("connected").Publish()},
|
||||
streamsPublisher{table->GetStringArrayTopic("streams").Publish()},
|
||||
modeEntry{table->GetStringTopic("mode").GetEntry("")},
|
||||
modesPublisher{table->GetStringArrayTopic("modes").Publish()} {
|
||||
CS_Status status = 0;
|
||||
wpi::SmallString<64> buf;
|
||||
sourcePublisher.Set(MakeSourceValue(source, buf));
|
||||
wpi::SmallString<64> descBuf;
|
||||
descriptionPublisher.Set(cs::GetSourceDescription(source, descBuf, &status));
|
||||
connectedPublisher.Set(cs::IsSourceConnected(source, &status));
|
||||
streamsPublisher.Set(inst.GetSourceStreamValues(source));
|
||||
auto mode = cs::GetSourceVideoMode(source, &status);
|
||||
modeEntry.SetDefault(VideoModeToString(mode));
|
||||
modesPublisher.Set(GetSourceModeValues(source));
|
||||
}
|
||||
|
||||
Instance::Instance() {
|
||||
// We publish sources to NetworkTables using the following structure:
|
||||
// "/CameraPublisher/{Source.Name}/" - root
|
||||
@@ -300,177 +381,88 @@ Instance::Instance() {
|
||||
|
||||
// Listener for video events
|
||||
m_videoListener = cs::VideoListener{
|
||||
[=](const cs::VideoEvent& event) {
|
||||
[=, this](const cs::VideoEvent& event) {
|
||||
std::scoped_lock lock(m_mutex);
|
||||
CS_Status status = 0;
|
||||
switch (event.kind) {
|
||||
case cs::VideoEvent::kSourceCreated: {
|
||||
// Create subtable for the camera
|
||||
auto table = m_publishTable->GetSubTable(event.name);
|
||||
{
|
||||
std::scoped_lock lock(m_mutex);
|
||||
m_tables.insert(std::make_pair(event.sourceHandle, table));
|
||||
}
|
||||
wpi::SmallString<64> buf;
|
||||
table->GetEntry("source").SetString(
|
||||
MakeSourceValue(event.sourceHandle, buf));
|
||||
wpi::SmallString<64> descBuf;
|
||||
table->GetEntry("description")
|
||||
.SetString(cs::GetSourceDescription(event.sourceHandle, descBuf,
|
||||
&status));
|
||||
table->GetEntry("connected")
|
||||
.SetBoolean(cs::IsSourceConnected(event.sourceHandle, &status));
|
||||
table->GetEntry("streams").SetStringArray(
|
||||
GetSourceStreamValues(event.sourceHandle));
|
||||
auto mode = cs::GetSourceVideoMode(event.sourceHandle, &status);
|
||||
table->GetEntry("mode").SetDefaultString(VideoModeToString(mode));
|
||||
table->GetEntry("modes").SetStringArray(
|
||||
GetSourceModeValues(event.sourceHandle));
|
||||
m_publishers.insert(
|
||||
{event.sourceHandle,
|
||||
SourcePublisher{*this, table, event.sourceHandle}});
|
||||
break;
|
||||
}
|
||||
case cs::VideoEvent::kSourceDestroyed: {
|
||||
auto table = GetSourceTable(event.sourceHandle);
|
||||
if (table) {
|
||||
table->GetEntry("source").SetString("");
|
||||
table->GetEntry("streams").SetStringArray(
|
||||
std::vector<std::string>{});
|
||||
table->GetEntry("modes").SetStringArray(
|
||||
std::vector<std::string>{});
|
||||
}
|
||||
case cs::VideoEvent::kSourceDestroyed:
|
||||
m_publishers.erase(event.sourceHandle);
|
||||
break;
|
||||
}
|
||||
case cs::VideoEvent::kSourceConnected: {
|
||||
auto table = GetSourceTable(event.sourceHandle);
|
||||
if (table) {
|
||||
case cs::VideoEvent::kSourceConnected:
|
||||
if (auto publisher = GetPublisher(event.sourceHandle)) {
|
||||
// update the description too (as it may have changed)
|
||||
wpi::SmallString<64> descBuf;
|
||||
table->GetEntry("description")
|
||||
.SetString(cs::GetSourceDescription(event.sourceHandle,
|
||||
descBuf, &status));
|
||||
table->GetEntry("connected").SetBoolean(true);
|
||||
publisher->descriptionPublisher.Set(cs::GetSourceDescription(
|
||||
event.sourceHandle, descBuf, &status));
|
||||
publisher->connectedPublisher.Set(true);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case cs::VideoEvent::kSourceDisconnected: {
|
||||
auto table = GetSourceTable(event.sourceHandle);
|
||||
if (table) {
|
||||
table->GetEntry("connected").SetBoolean(false);
|
||||
case cs::VideoEvent::kSourceDisconnected:
|
||||
if (auto publisher = GetPublisher(event.sourceHandle)) {
|
||||
publisher->connectedPublisher.Set(false);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case cs::VideoEvent::kSourceVideoModesUpdated: {
|
||||
auto table = GetSourceTable(event.sourceHandle);
|
||||
if (table) {
|
||||
table->GetEntry("modes").SetStringArray(
|
||||
case cs::VideoEvent::kSourceVideoModesUpdated:
|
||||
if (auto publisher = GetPublisher(event.sourceHandle)) {
|
||||
publisher->modesPublisher.Set(
|
||||
GetSourceModeValues(event.sourceHandle));
|
||||
}
|
||||
break;
|
||||
}
|
||||
case cs::VideoEvent::kSourceVideoModeChanged: {
|
||||
auto table = GetSourceTable(event.sourceHandle);
|
||||
if (table) {
|
||||
table->GetEntry("mode").SetString(VideoModeToString(event.mode));
|
||||
case cs::VideoEvent::kSourceVideoModeChanged:
|
||||
if (auto publisher = GetPublisher(event.sourceHandle)) {
|
||||
publisher->modeEntry.Set(VideoModeToString(event.mode));
|
||||
}
|
||||
break;
|
||||
}
|
||||
case cs::VideoEvent::kSourcePropertyCreated: {
|
||||
auto table = GetSourceTable(event.sourceHandle);
|
||||
if (table) {
|
||||
PutSourcePropertyValue(table.get(), event, true);
|
||||
case cs::VideoEvent::kSourcePropertyCreated:
|
||||
if (auto publisher = GetPublisher(event.sourceHandle)) {
|
||||
publisher->properties.insert(
|
||||
{event.propertyHandle,
|
||||
PropertyPublisher{*publisher->table, event}});
|
||||
}
|
||||
break;
|
||||
}
|
||||
case cs::VideoEvent::kSourcePropertyValueUpdated: {
|
||||
auto table = GetSourceTable(event.sourceHandle);
|
||||
if (table) {
|
||||
PutSourcePropertyValue(table.get(), event, false);
|
||||
case cs::VideoEvent::kSourcePropertyValueUpdated:
|
||||
if (auto publisher = GetPublisher(event.sourceHandle)) {
|
||||
auto ppIt = publisher->properties.find(event.propertyHandle);
|
||||
if (ppIt != publisher->properties.end()) {
|
||||
ppIt->second.Update(event);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case cs::VideoEvent::kSourcePropertyChoicesUpdated: {
|
||||
auto table = GetSourceTable(event.sourceHandle);
|
||||
if (table) {
|
||||
auto choices =
|
||||
cs::GetEnumPropertyChoices(event.propertyHandle, &status);
|
||||
table
|
||||
->GetEntry(fmt::format("PropertyInfo/{}/choices", event.name))
|
||||
.SetStringArray(choices);
|
||||
case cs::VideoEvent::kSourcePropertyChoicesUpdated:
|
||||
if (auto publisher = GetPublisher(event.sourceHandle)) {
|
||||
auto ppIt = publisher->properties.find(event.propertyHandle);
|
||||
if (ppIt != publisher->properties.end() &&
|
||||
ppIt->second.choicesTopic) {
|
||||
auto choices =
|
||||
cs::GetEnumPropertyChoices(event.propertyHandle, &status);
|
||||
if (!ppIt->second.choicesPublisher) {
|
||||
ppIt->second.choicesPublisher =
|
||||
ppIt->second.choicesTopic.Publish();
|
||||
}
|
||||
ppIt->second.choicesPublisher.Set(choices);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case cs::VideoEvent::kSinkSourceChanged:
|
||||
case cs::VideoEvent::kSinkCreated:
|
||||
case cs::VideoEvent::kSinkDestroyed:
|
||||
case cs::VideoEvent::kNetworkInterfacesChanged: {
|
||||
case cs::VideoEvent::kNetworkInterfacesChanged:
|
||||
m_addresses = cs::GetNetworkInterfaces();
|
||||
UpdateStreamValues();
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
},
|
||||
0x4fff, true};
|
||||
|
||||
// Listener for NetworkTable events
|
||||
// We don't currently support changing settings via NT due to
|
||||
// synchronization issues, so just update to current setting if someone
|
||||
// else tries to change it.
|
||||
wpi::SmallString<64> buf;
|
||||
m_tableListener = nt::NetworkTableInstance::GetDefault().AddEntryListener(
|
||||
fmt::format("{}/", kPublishName),
|
||||
[=](const nt::EntryNotification& event) {
|
||||
auto relativeKey = wpi::drop_front(
|
||||
event.name, std::string_view{kPublishName}.size() + 1);
|
||||
|
||||
// get source (sourceName/...)
|
||||
auto subKeyIndex = relativeKey.find('/');
|
||||
if (subKeyIndex == std::string_view::npos) {
|
||||
return;
|
||||
}
|
||||
auto sourceName = wpi::slice(relativeKey, 0, subKeyIndex);
|
||||
auto sourceIt = m_sources.find(sourceName);
|
||||
if (sourceIt == m_sources.end()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// get subkey
|
||||
relativeKey.remove_prefix(subKeyIndex + 1);
|
||||
|
||||
// handle standard names
|
||||
std::string_view propName;
|
||||
nt::NetworkTableEntry entry{event.entry};
|
||||
if (relativeKey == "mode") {
|
||||
// reset to current mode
|
||||
entry.SetString(VideoModeToString(sourceIt->second.GetVideoMode()));
|
||||
return;
|
||||
} else if (wpi::starts_with(relativeKey, "Property/")) {
|
||||
propName = wpi::substr(relativeKey, 9);
|
||||
} else if (wpi::starts_with(relativeKey, "RawProperty/")) {
|
||||
propName = wpi::substr(relativeKey, 12);
|
||||
} else {
|
||||
return; // ignore
|
||||
}
|
||||
|
||||
// everything else is a property
|
||||
auto property = sourceIt->second.GetProperty(propName);
|
||||
switch (property.GetKind()) {
|
||||
case cs::VideoProperty::kNone:
|
||||
return;
|
||||
case cs::VideoProperty::kBoolean:
|
||||
entry.SetBoolean(property.Get() != 0);
|
||||
return;
|
||||
case cs::VideoProperty::kInteger:
|
||||
case cs::VideoProperty::kEnum:
|
||||
entry.SetDouble(property.Get());
|
||||
return;
|
||||
case cs::VideoProperty::kString:
|
||||
entry.SetString(property.GetString());
|
||||
return;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
},
|
||||
NT_NOTIFY_IMMEDIATE | NT_NOTIFY_UPDATE);
|
||||
}
|
||||
|
||||
cs::UsbCamera CameraServer::StartAutomaticCapture() {
|
||||
@@ -482,6 +474,7 @@ cs::UsbCamera CameraServer::StartAutomaticCapture() {
|
||||
}
|
||||
|
||||
cs::UsbCamera CameraServer::StartAutomaticCapture(int dev) {
|
||||
::GetInstance();
|
||||
cs::UsbCamera camera{fmt::format("USB Camera {}", dev), dev};
|
||||
StartAutomaticCapture(camera);
|
||||
auto csShared = GetCameraServerShared();
|
||||
@@ -491,6 +484,7 @@ cs::UsbCamera CameraServer::StartAutomaticCapture(int dev) {
|
||||
|
||||
cs::UsbCamera CameraServer::StartAutomaticCapture(std::string_view name,
|
||||
int dev) {
|
||||
::GetInstance();
|
||||
cs::UsbCamera camera{name, dev};
|
||||
StartAutomaticCapture(camera);
|
||||
auto csShared = GetCameraServerShared();
|
||||
@@ -500,6 +494,7 @@ cs::UsbCamera CameraServer::StartAutomaticCapture(std::string_view name,
|
||||
|
||||
cs::UsbCamera CameraServer::StartAutomaticCapture(std::string_view name,
|
||||
std::string_view path) {
|
||||
::GetInstance();
|
||||
cs::UsbCamera camera{name, path};
|
||||
StartAutomaticCapture(camera);
|
||||
auto csShared = GetCameraServerShared();
|
||||
@@ -519,12 +514,13 @@ cs::AxisCamera CameraServer::AddAxisCamera(const std::string& host) {
|
||||
return AddAxisCamera("Axis Camera", host);
|
||||
}
|
||||
|
||||
cs::AxisCamera CameraServer::AddAxisCamera(wpi::span<const std::string> hosts) {
|
||||
cs::AxisCamera CameraServer::AddAxisCamera(std::span<const std::string> hosts) {
|
||||
return AddAxisCamera("Axis Camera", hosts);
|
||||
}
|
||||
|
||||
cs::AxisCamera CameraServer::AddAxisCamera(std::string_view name,
|
||||
std::string_view host) {
|
||||
::GetInstance();
|
||||
cs::AxisCamera camera{name, host};
|
||||
StartAutomaticCapture(camera);
|
||||
auto csShared = GetCameraServerShared();
|
||||
@@ -534,6 +530,7 @@ cs::AxisCamera CameraServer::AddAxisCamera(std::string_view name,
|
||||
|
||||
cs::AxisCamera CameraServer::AddAxisCamera(std::string_view name,
|
||||
const char* host) {
|
||||
::GetInstance();
|
||||
cs::AxisCamera camera{name, host};
|
||||
StartAutomaticCapture(camera);
|
||||
auto csShared = GetCameraServerShared();
|
||||
@@ -543,6 +540,7 @@ cs::AxisCamera CameraServer::AddAxisCamera(std::string_view name,
|
||||
|
||||
cs::AxisCamera CameraServer::AddAxisCamera(std::string_view name,
|
||||
const std::string& host) {
|
||||
::GetInstance();
|
||||
cs::AxisCamera camera{name, host};
|
||||
StartAutomaticCapture(camera);
|
||||
auto csShared = GetCameraServerShared();
|
||||
@@ -551,7 +549,8 @@ cs::AxisCamera CameraServer::AddAxisCamera(std::string_view name,
|
||||
}
|
||||
|
||||
cs::AxisCamera CameraServer::AddAxisCamera(std::string_view name,
|
||||
wpi::span<const std::string> hosts) {
|
||||
std::span<const std::string> hosts) {
|
||||
::GetInstance();
|
||||
cs::AxisCamera camera{name, hosts};
|
||||
StartAutomaticCapture(camera);
|
||||
auto csShared = GetCameraServerShared();
|
||||
@@ -560,10 +559,11 @@ cs::AxisCamera CameraServer::AddAxisCamera(std::string_view name,
|
||||
}
|
||||
|
||||
cs::MjpegServer CameraServer::AddSwitchedCamera(std::string_view name) {
|
||||
auto& inst = ::GetInstance();
|
||||
// create a dummy CvSource
|
||||
cs::CvSource source{name, cs::VideoMode::PixelFormat::kMJPEG, 160, 120, 30};
|
||||
cs::MjpegServer server = StartAutomaticCapture(source);
|
||||
::GetInstance().m_fixedSources[server.GetHandle()] = source.GetHandle();
|
||||
inst.m_fixedSources[server.GetHandle()] = source.GetHandle();
|
||||
|
||||
return server;
|
||||
}
|
||||
@@ -640,6 +640,7 @@ cs::CvSink CameraServer::GetVideo(std::string_view name) {
|
||||
|
||||
cs::CvSource CameraServer::PutVideo(std::string_view name, int width,
|
||||
int height) {
|
||||
::GetInstance();
|
||||
cs::CvSource source{name, cs::VideoMode::kMJPEG, width, height, 30};
|
||||
StartAutomaticCapture(source);
|
||||
return source;
|
||||
@@ -656,6 +657,7 @@ cs::MjpegServer CameraServer::AddServer(std::string_view name) {
|
||||
}
|
||||
|
||||
cs::MjpegServer CameraServer::AddServer(std::string_view name, int port) {
|
||||
::GetInstance();
|
||||
cs::MjpegServer server{name, port};
|
||||
AddServer(server);
|
||||
return server;
|
||||
|
||||
@@ -6,11 +6,10 @@
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#include <span>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
|
||||
#include <wpi/span.h>
|
||||
|
||||
#include "cscore.h"
|
||||
#include "cscore_cv.h"
|
||||
|
||||
@@ -110,7 +109,7 @@ class CameraServer {
|
||||
*
|
||||
* @param hosts Array of Camera host IPs/DNS names
|
||||
*/
|
||||
static cs::AxisCamera AddAxisCamera(wpi::span<const std::string> hosts);
|
||||
static cs::AxisCamera AddAxisCamera(std::span<const std::string> hosts);
|
||||
|
||||
/**
|
||||
* Adds an Axis IP camera.
|
||||
@@ -155,7 +154,7 @@ class CameraServer {
|
||||
* @param hosts Array of Camera host IPs/DNS names
|
||||
*/
|
||||
static cs::AxisCamera AddAxisCamera(std::string_view name,
|
||||
wpi::span<const std::string> hosts);
|
||||
std::span<const std::string> hosts);
|
||||
|
||||
/**
|
||||
* Adds an Axis IP camera.
|
||||
|
||||
@@ -49,6 +49,7 @@ class TestEnvironment : public testing::Environment {
|
||||
HAL_GetControlWord(&controlWord);
|
||||
return controlWord.enabled && controlWord.dsAttached;
|
||||
};
|
||||
HAL_RefreshDSData();
|
||||
while (!checkEnabled()) {
|
||||
if (enableCounter > 50) {
|
||||
// Robot did not enable properly after 5 seconds.
|
||||
@@ -60,6 +61,7 @@ class TestEnvironment : public testing::Environment {
|
||||
std::this_thread::sleep_for(100ms);
|
||||
|
||||
fmt::print("Waiting for enable: {}\n", enableCounter++);
|
||||
HAL_RefreshDSData();
|
||||
}
|
||||
std::this_thread::sleep_for(500ms);
|
||||
}
|
||||
|
||||
@@ -13,6 +13,10 @@ cppSrcFileInclude {
|
||||
\.cpp$
|
||||
}
|
||||
|
||||
modifiableFileExclude {
|
||||
objcpp
|
||||
}
|
||||
|
||||
licenseUpdateExclude {
|
||||
src/main/native/cpp/default_init_allocator\.h$
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ file(GLOB
|
||||
cscore_native_src src/main/native/cpp/*.cpp)
|
||||
file(GLOB cscore_linux_src src/main/native/linux/*.cpp)
|
||||
file(GLOB cscore_osx_src src/main/native/osx/*.cpp)
|
||||
file(GLOB cscore_osx_objc_src src/main/native/objcpp/*.mm)
|
||||
file(GLOB cscore_windows_src src/main/native/windows/*.cpp)
|
||||
|
||||
add_library(cscore ${cscore_native_src})
|
||||
@@ -18,7 +19,9 @@ set_target_properties(cscore PROPERTIES DEBUG_POSTFIX "d")
|
||||
|
||||
if(NOT MSVC)
|
||||
if (APPLE)
|
||||
target_sources(cscore PRIVATE ${cscore_osx_src})
|
||||
target_sources(cscore PRIVATE ${cscore_osx_src} ${cscore_osx_objc_src})
|
||||
target_compile_options(cscore PRIVATE "-fobjc-arc")
|
||||
set_target_properties(cscore PROPERTIES LINK_FLAGS "-framework CoreFoundation -framework AVFoundation -framework Foundation -framework CoreMedia -framework CoreVideo")
|
||||
else()
|
||||
target_sources(cscore PRIVATE ${cscore_linux_src})
|
||||
endif()
|
||||
|
||||
@@ -2,16 +2,16 @@ import org.gradle.internal.os.OperatingSystem
|
||||
|
||||
ext {
|
||||
nativeName = 'cscore'
|
||||
devMain = 'edu.wpi.cscore.DevMain'
|
||||
devMain = 'edu.wpi.first.cscore.DevMain'
|
||||
}
|
||||
|
||||
// Removed because having the objective-cpp plugin added breaks
|
||||
// embedded tools and its toolchain check. It causes an obj-cpp
|
||||
// source set to be added to all binaries, even cross binaries
|
||||
// with no support.
|
||||
// if (OperatingSystem.current().isMacOsX()) {
|
||||
// apply plugin: 'objective-cpp'
|
||||
// }
|
||||
if (OperatingSystem.current().isMacOsX()) {
|
||||
apply plugin: 'objective-cpp'
|
||||
}
|
||||
|
||||
apply from: "${rootDir}/shared/jni/setupBuild.gradle"
|
||||
|
||||
@@ -61,8 +61,13 @@ model {
|
||||
if (!it.buildable || !(it instanceof NativeBinarySpec)) {
|
||||
return
|
||||
}
|
||||
lib project: ':wpinet', library: 'wpinet', linkage: 'shared'
|
||||
lib project: ':wpiutil', library: 'wpiutil', linkage: 'shared'
|
||||
if (it.component.name == "${nativeName}JNI") {
|
||||
lib project: ':wpinet', library: 'wpinet', linkage: 'static'
|
||||
lib project: ':wpiutil', library: 'wpiutil', linkage: 'static'
|
||||
} else {
|
||||
lib project: ':wpinet', library: 'wpinet', linkage: 'shared'
|
||||
lib project: ':wpiutil', library: 'wpiutil', linkage: 'shared'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -82,16 +87,16 @@ ext {
|
||||
splitSetup = {
|
||||
if (it.targetPlatform.operatingSystem.isMacOsX()) {
|
||||
it.sources {
|
||||
// macObjCpp(ObjectiveCppSourceSet) {
|
||||
// source {
|
||||
// srcDirs = ['src/main/native/objcpp']
|
||||
// include '**/*.mm'
|
||||
// }
|
||||
// exportedHeaders {
|
||||
// srcDirs 'src/main/native/include'
|
||||
// include '**/*.h'
|
||||
// }
|
||||
// }
|
||||
macObjCpp(ObjectiveCppSourceSet) {
|
||||
source {
|
||||
srcDirs = ['src/main/native/objcpp']
|
||||
include '**/*.mm'
|
||||
}
|
||||
exportedHeaders {
|
||||
srcDirs 'src/main/native/include', 'src/main/native/cpp'
|
||||
include '**/*.h'
|
||||
}
|
||||
}
|
||||
cscoreMacCpp(CppSourceSet) {
|
||||
source {
|
||||
srcDirs 'src/main/native/osx'
|
||||
@@ -152,6 +157,12 @@ Action<List<String>> symbolFilter = { symbols ->
|
||||
symbols.removeIf({ !it.startsWith('CS_') })
|
||||
} as Action<List<String>>;
|
||||
|
||||
run {
|
||||
if (OperatingSystem.current().isMacOsX()) {
|
||||
jvmArgs("-XstartOnFirstThread");
|
||||
}
|
||||
}
|
||||
|
||||
nativeUtils.exportsConfigs {
|
||||
cscore {
|
||||
x64ExcludeSymbols = [
|
||||
@@ -176,11 +187,13 @@ nativeUtils.exportsConfigs {
|
||||
}
|
||||
}
|
||||
|
||||
apply from: "${rootDir}/shared/imgui.gradle"
|
||||
|
||||
model {
|
||||
components {
|
||||
examplesMap.each { key, value ->
|
||||
if (key == "usbviewer") {
|
||||
if (!project.hasProperty('onlylinuxathena') && !project.hasProperty('onlylinuxarm32') && !project.hasProperty('onlylinuxarm64')) {
|
||||
if (!project.hasProperty('onlylinuxathena')) {
|
||||
"${key}"(NativeExecutableSpec) {
|
||||
targetBuildTypes 'debug'
|
||||
binaries.all {
|
||||
@@ -188,8 +201,8 @@ model {
|
||||
lib project: ':wpinet', library: 'wpinet', linkage: 'shared'
|
||||
lib project: ':wpigui', library: 'wpigui', linkage: 'static'
|
||||
lib library: 'cscore', linkage: 'shared'
|
||||
nativeUtils.useRequiredLibrary(it, 'imgui_static')
|
||||
if (it.targetPlatform.name == nativeUtils.wpi.platforms.roborio || it.targetPlatform.name == nativeUtils.wpi.platforms.linuxarm32 || it.targetPlatform.name == nativeUtils.wpi.platforms.linuxarm64) {
|
||||
nativeUtils.useRequiredLibrary(it, 'imgui')
|
||||
if (it.targetPlatform.name == nativeUtils.wpi.platforms.roborio) {
|
||||
it.buildable = false
|
||||
return
|
||||
}
|
||||
@@ -199,6 +212,9 @@ model {
|
||||
it.linker.args << '-framework' << 'Metal' << '-framework' << 'MetalKit' << '-framework' << 'Cocoa' << '-framework' << 'IOKit' << '-framework' << 'CoreFoundation' << '-framework' << 'CoreVideo' << '-framework' << 'QuartzCore'
|
||||
} else {
|
||||
it.linker.args << '-lX11'
|
||||
if (it.targetPlatform.name.startsWith('linuxarm')) {
|
||||
it.linker.args << '-lGL'
|
||||
}
|
||||
}
|
||||
}
|
||||
sources {
|
||||
|
||||
@@ -390,4 +390,10 @@ public class CameraServerJNI {
|
||||
public static native long allocateRawFrame();
|
||||
|
||||
public static native void freeRawFrame(long frame);
|
||||
|
||||
public static native void runMainRunLoop();
|
||||
|
||||
public static native int runMainRunLoopTimeout(double timeoutSeconds);
|
||||
|
||||
public static native void stopMainRunLoop();
|
||||
}
|
||||
|
||||
@@ -13,7 +13,9 @@ public class VideoMode {
|
||||
kYUYV(2),
|
||||
kRGB565(3),
|
||||
kBGR(4),
|
||||
kGray(5);
|
||||
kGray(5),
|
||||
kY16(6),
|
||||
kUYVY(7);
|
||||
|
||||
private final int value;
|
||||
|
||||
|
||||
@@ -88,7 +88,7 @@ int ConfigurableSourceImpl::CreateProperty(
|
||||
}
|
||||
|
||||
void ConfigurableSourceImpl::SetEnumPropertyChoices(
|
||||
int property, wpi::span<const std::string> choices, CS_Status* status) {
|
||||
int property, std::span<const std::string> choices, CS_Status* status) {
|
||||
std::scoped_lock lock(m_mutex);
|
||||
auto prop = GetProperty(property);
|
||||
if (!prop) {
|
||||
|
||||
@@ -8,12 +8,11 @@
|
||||
#include <atomic>
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <span>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
#include <wpi/span.h>
|
||||
|
||||
#include "SourceImpl.h"
|
||||
|
||||
namespace cs {
|
||||
@@ -42,7 +41,7 @@ class ConfigurableSourceImpl : public SourceImpl {
|
||||
int maximum, int step, int defaultValue, int value,
|
||||
std::function<void(CS_Property property)> onChange);
|
||||
void SetEnumPropertyChoices(int property,
|
||||
wpi::span<const std::string> choices,
|
||||
std::span<const std::string> choices,
|
||||
CS_Status* status);
|
||||
|
||||
private:
|
||||
|
||||
@@ -110,7 +110,7 @@ void CvSinkImpl::ThreadMain() {
|
||||
std::this_thread::sleep_for(std::chrono::seconds(1));
|
||||
continue;
|
||||
}
|
||||
SDEBUG4("{}", "waiting for frame");
|
||||
SDEBUG4("waiting for frame");
|
||||
Frame frame = source->GetNextFrame(); // blocks
|
||||
if (!m_active) {
|
||||
break;
|
||||
|
||||
@@ -139,7 +139,7 @@ CS_Property CreateSourcePropertyCallback(
|
||||
}
|
||||
|
||||
void SetSourceEnumPropertyChoices(CS_Source source, CS_Property property,
|
||||
wpi::span<const std::string> choices,
|
||||
std::span<const std::string> choices,
|
||||
CS_Status* status) {
|
||||
auto data = Instance::GetInstance().GetSource(source);
|
||||
if (!data || (data->kind & SourceMask) == 0) {
|
||||
|
||||
@@ -217,7 +217,7 @@ Image* Frame::ConvertImpl(Image* image, VideoMode::PixelFormat pixelFormat,
|
||||
// Color convert
|
||||
switch (pixelFormat) {
|
||||
case VideoMode::kRGB565:
|
||||
// If source is YUYV or Gray, need to convert to BGR first
|
||||
// If source is YUYV, UYVY, Gray, or Y16, need to convert to BGR first
|
||||
if (cur->pixelFormat == VideoMode::kYUYV) {
|
||||
// Check to see if BGR version already exists...
|
||||
if (Image* newImage =
|
||||
@@ -226,6 +226,14 @@ Image* Frame::ConvertImpl(Image* image, VideoMode::PixelFormat pixelFormat,
|
||||
} else {
|
||||
cur = ConvertYUYVToBGR(cur);
|
||||
}
|
||||
} else if (cur->pixelFormat == VideoMode::kUYVY) {
|
||||
// Check to see if BGR version already exists...
|
||||
if (Image* newImage =
|
||||
GetExistingImage(cur->width, cur->height, VideoMode::kBGR)) {
|
||||
cur = newImage;
|
||||
} else {
|
||||
cur = ConvertUYVYToBGR(cur);
|
||||
}
|
||||
} else if (cur->pixelFormat == VideoMode::kGray) {
|
||||
// Check to see if BGR version already exists...
|
||||
if (Image* newImage =
|
||||
@@ -234,18 +242,35 @@ Image* Frame::ConvertImpl(Image* image, VideoMode::PixelFormat pixelFormat,
|
||||
} else {
|
||||
cur = ConvertGrayToBGR(cur);
|
||||
}
|
||||
}
|
||||
return ConvertBGRToRGB565(cur);
|
||||
case VideoMode::kGray:
|
||||
// If source is YUYV or RGB565, need to convert to BGR first
|
||||
if (cur->pixelFormat == VideoMode::kYUYV) {
|
||||
} else if (cur->pixelFormat == VideoMode::kY16) {
|
||||
// Check to see if BGR version already exists...
|
||||
if (Image* newImage =
|
||||
GetExistingImage(cur->width, cur->height, VideoMode::kBGR)) {
|
||||
cur = newImage;
|
||||
} else if (Image* newImage = GetExistingImage(cur->width, cur->height,
|
||||
VideoMode::kGray)) {
|
||||
cur = ConvertGrayToBGR(newImage);
|
||||
} else {
|
||||
cur = ConvertYUYVToBGR(cur);
|
||||
cur = ConvertGrayToBGR(ConvertY16ToGray(cur));
|
||||
}
|
||||
}
|
||||
return ConvertBGRToRGB565(cur);
|
||||
case VideoMode::kGray:
|
||||
case VideoMode::kY16:
|
||||
// If source is also grayscale, convert directly
|
||||
if (pixelFormat == VideoMode::kGray &&
|
||||
cur->pixelFormat == VideoMode::kY16) {
|
||||
return ConvertY16ToGray(cur);
|
||||
} else if (pixelFormat == VideoMode::kY16 &&
|
||||
cur->pixelFormat == VideoMode::kGray) {
|
||||
return ConvertGrayToY16(cur);
|
||||
}
|
||||
// If source is YUYV, UYVY, convert directly to Gray
|
||||
// If RGB565, need to convert to BGR first
|
||||
if (cur->pixelFormat == VideoMode::kYUYV) {
|
||||
cur = ConvertYUYVToGray(cur);
|
||||
} else if (cur->pixelFormat == VideoMode::kUYVY) {
|
||||
cur = ConvertUYVYToGray(cur);
|
||||
} else if (cur->pixelFormat == VideoMode::kRGB565) {
|
||||
// Check to see if BGR version already exists...
|
||||
if (Image* newImage =
|
||||
@@ -254,12 +279,18 @@ Image* Frame::ConvertImpl(Image* image, VideoMode::PixelFormat pixelFormat,
|
||||
} else {
|
||||
cur = ConvertRGB565ToBGR(cur);
|
||||
}
|
||||
cur = ConvertBGRToGray(cur);
|
||||
}
|
||||
return ConvertBGRToGray(cur);
|
||||
if (pixelFormat == VideoMode::kY16) {
|
||||
cur = ConvertGrayToY16(cur);
|
||||
}
|
||||
return cur;
|
||||
case VideoMode::kBGR:
|
||||
case VideoMode::kMJPEG:
|
||||
if (cur->pixelFormat == VideoMode::kYUYV) {
|
||||
cur = ConvertYUYVToBGR(cur);
|
||||
} else if (cur->pixelFormat == VideoMode::kUYVY) {
|
||||
cur = ConvertUYVYToBGR(cur);
|
||||
} else if (cur->pixelFormat == VideoMode::kRGB565) {
|
||||
cur = ConvertRGB565ToBGR(cur);
|
||||
} else if (cur->pixelFormat == VideoMode::kGray) {
|
||||
@@ -268,9 +299,23 @@ Image* Frame::ConvertImpl(Image* image, VideoMode::PixelFormat pixelFormat,
|
||||
} else {
|
||||
return ConvertGrayToMJPEG(cur, defaultJpegQuality);
|
||||
}
|
||||
} else if (cur->pixelFormat == VideoMode::kY16) {
|
||||
// Check to see if Gray version already exists...
|
||||
if (Image* newImage =
|
||||
GetExistingImage(cur->width, cur->height, VideoMode::kGray)) {
|
||||
cur = newImage;
|
||||
} else {
|
||||
cur = ConvertY16ToGray(cur);
|
||||
}
|
||||
if (pixelFormat == VideoMode::kBGR) {
|
||||
return ConvertGrayToBGR(cur);
|
||||
} else {
|
||||
return ConvertGrayToMJPEG(cur, defaultJpegQuality);
|
||||
}
|
||||
}
|
||||
break;
|
||||
case VideoMode::kYUYV:
|
||||
case VideoMode::kUYVY:
|
||||
default:
|
||||
return nullptr; // Unsupported
|
||||
}
|
||||
@@ -351,6 +396,72 @@ Image* Frame::ConvertYUYVToBGR(Image* image) {
|
||||
return rv;
|
||||
}
|
||||
|
||||
Image* Frame::ConvertYUYVToGray(Image* image) {
|
||||
if (!image || image->pixelFormat != VideoMode::kYUYV) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// Allocate a grayscale image
|
||||
auto newImage =
|
||||
m_impl->source.AllocImage(VideoMode::kGray, image->width, image->height,
|
||||
image->width * image->height);
|
||||
|
||||
// Convert
|
||||
cv::cvtColor(image->AsMat(), newImage->AsMat(), cv::COLOR_YUV2GRAY_YUYV);
|
||||
|
||||
// Save the result
|
||||
Image* rv = newImage.release();
|
||||
if (m_impl) {
|
||||
std::scoped_lock lock(m_impl->mutex);
|
||||
m_impl->images.push_back(rv);
|
||||
}
|
||||
return rv;
|
||||
}
|
||||
|
||||
Image* Frame::ConvertUYVYToBGR(Image* image) {
|
||||
if (!image || image->pixelFormat != VideoMode::kUYVY) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// Allocate a BGR image
|
||||
auto newImage =
|
||||
m_impl->source.AllocImage(VideoMode::kBGR, image->width, image->height,
|
||||
image->width * image->height * 3);
|
||||
|
||||
// Convert
|
||||
cv::cvtColor(image->AsMat(), newImage->AsMat(), cv::COLOR_YUV2BGR_UYVY);
|
||||
|
||||
// Save the result
|
||||
Image* rv = newImage.release();
|
||||
if (m_impl) {
|
||||
std::scoped_lock lock(m_impl->mutex);
|
||||
m_impl->images.push_back(rv);
|
||||
}
|
||||
return rv;
|
||||
}
|
||||
|
||||
Image* Frame::ConvertUYVYToGray(Image* image) {
|
||||
if (!image || image->pixelFormat != VideoMode::kUYVY) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// Allocate a grayscale image
|
||||
auto newImage =
|
||||
m_impl->source.AllocImage(VideoMode::kGray, image->width, image->height,
|
||||
image->width * image->height);
|
||||
|
||||
// Convert
|
||||
cv::cvtColor(image->AsMat(), newImage->AsMat(), cv::COLOR_YUV2GRAY_UYVY);
|
||||
|
||||
// Save the result
|
||||
Image* rv = newImage.release();
|
||||
if (m_impl) {
|
||||
std::scoped_lock lock(m_impl->mutex);
|
||||
m_impl->images.push_back(rv);
|
||||
}
|
||||
return rv;
|
||||
}
|
||||
|
||||
Image* Frame::ConvertBGRToRGB565(Image* image) {
|
||||
if (!image || image->pixelFormat != VideoMode::kBGR) {
|
||||
return nullptr;
|
||||
@@ -509,6 +620,50 @@ Image* Frame::ConvertGrayToMJPEG(Image* image, int quality) {
|
||||
return rv;
|
||||
}
|
||||
|
||||
Image* Frame::ConvertGrayToY16(Image* image) {
|
||||
if (!image || image->pixelFormat != VideoMode::kGray) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// Allocate a Y16 image
|
||||
auto newImage =
|
||||
m_impl->source.AllocImage(VideoMode::kY16, image->width, image->height,
|
||||
image->width * image->height * 2);
|
||||
|
||||
// Convert with linear scaling
|
||||
image->AsMat().convertTo(newImage->AsMat(), CV_16U, 256);
|
||||
|
||||
// Save the result
|
||||
Image* rv = newImage.release();
|
||||
if (m_impl) {
|
||||
std::scoped_lock lock(m_impl->mutex);
|
||||
m_impl->images.push_back(rv);
|
||||
}
|
||||
return rv;
|
||||
}
|
||||
|
||||
Image* Frame::ConvertY16ToGray(Image* image) {
|
||||
if (!image || image->pixelFormat != VideoMode::kY16) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// Allocate a Grayscale image
|
||||
auto newImage =
|
||||
m_impl->source.AllocImage(VideoMode::kGray, image->width, image->height,
|
||||
image->width * image->height);
|
||||
|
||||
// Scale min to 0 and max to 255
|
||||
cv::normalize(image->AsMat(), newImage->AsMat(), 255, 0, cv::NORM_MINMAX);
|
||||
|
||||
// Save the result
|
||||
Image* rv = newImage.release();
|
||||
if (m_impl) {
|
||||
std::scoped_lock lock(m_impl->mutex);
|
||||
m_impl->images.push_back(rv);
|
||||
}
|
||||
return rv;
|
||||
}
|
||||
|
||||
Image* Frame::GetImageImpl(int width, int height,
|
||||
VideoMode::PixelFormat pixelFormat,
|
||||
int requiredJpegQuality, int defaultJpegQuality) {
|
||||
|
||||
@@ -195,12 +195,17 @@ class Frame {
|
||||
Image* ConvertMJPEGToBGR(Image* image);
|
||||
Image* ConvertMJPEGToGray(Image* image);
|
||||
Image* ConvertYUYVToBGR(Image* image);
|
||||
Image* ConvertYUYVToGray(Image* image);
|
||||
Image* ConvertUYVYToBGR(Image* image);
|
||||
Image* ConvertUYVYToGray(Image* image);
|
||||
Image* ConvertBGRToRGB565(Image* image);
|
||||
Image* ConvertRGB565ToBGR(Image* image);
|
||||
Image* ConvertBGRToGray(Image* image);
|
||||
Image* ConvertGrayToBGR(Image* image);
|
||||
Image* ConvertBGRToMJPEG(Image* image, int quality);
|
||||
Image* ConvertGrayToMJPEG(Image* image, int quality);
|
||||
Image* ConvertGrayToY16(Image* image);
|
||||
Image* ConvertY16ToGray(Image* image);
|
||||
|
||||
Image* GetImage(int width, int height, VideoMode::PixelFormat pixelFormat) {
|
||||
if (pixelFormat == VideoMode::kMJPEG) {
|
||||
|
||||
@@ -75,7 +75,7 @@ void HttpCameraImpl::MonitorThreadMain() {
|
||||
std::unique_lock lock(m_mutex);
|
||||
// sleep for 1 second between checks
|
||||
m_monitorCond.wait_for(lock, std::chrono::seconds(1),
|
||||
[=] { return !m_active; });
|
||||
[=, this] { return !m_active; });
|
||||
|
||||
if (!m_active) {
|
||||
break;
|
||||
@@ -85,7 +85,7 @@ void HttpCameraImpl::MonitorThreadMain() {
|
||||
// (this will result in an error at the read point, and ultimately
|
||||
// a reconnect attempt)
|
||||
if (m_streamConn && m_frameCount == 0) {
|
||||
SWARNING("{}", "Monitor detected stream hung, disconnecting");
|
||||
SWARNING("Monitor detected stream hung, disconnecting");
|
||||
m_streamConn->stream->close();
|
||||
}
|
||||
|
||||
@@ -93,7 +93,7 @@ void HttpCameraImpl::MonitorThreadMain() {
|
||||
m_frameCount = 0;
|
||||
}
|
||||
|
||||
SDEBUG("{}", "Monitor Thread exiting");
|
||||
SDEBUG("Monitor Thread exiting");
|
||||
}
|
||||
|
||||
void HttpCameraImpl::StreamThreadMain() {
|
||||
@@ -110,7 +110,8 @@ void HttpCameraImpl::StreamThreadMain() {
|
||||
m_streamConn->stream->close();
|
||||
}
|
||||
// Wait for enable
|
||||
m_sinkEnabledCond.wait(lock, [=] { return !m_active || IsEnabled(); });
|
||||
m_sinkEnabledCond.wait(lock,
|
||||
[=, this] { return !m_active || IsEnabled(); });
|
||||
if (!m_active) {
|
||||
return;
|
||||
}
|
||||
@@ -140,7 +141,7 @@ void HttpCameraImpl::StreamThreadMain() {
|
||||
}
|
||||
}
|
||||
|
||||
SDEBUG("{}", "Camera Thread exiting");
|
||||
SDEBUG("Camera Thread exiting");
|
||||
SetConnected(false);
|
||||
}
|
||||
|
||||
@@ -151,7 +152,7 @@ wpi::HttpConnection* HttpCameraImpl::DeviceStreamConnect(
|
||||
{
|
||||
std::scoped_lock lock(m_mutex);
|
||||
if (m_locations.empty()) {
|
||||
SERROR("{}", "locations array is empty!?");
|
||||
SERROR("locations array is empty!?");
|
||||
std::this_thread::sleep_for(std::chrono::seconds(1));
|
||||
return nullptr;
|
||||
}
|
||||
@@ -272,7 +273,7 @@ bool HttpCameraImpl::DeviceStreamFrame(wpi::raw_istream& is,
|
||||
wpi::SmallString<64> contentTypeBuf;
|
||||
wpi::SmallString<64> contentLengthBuf;
|
||||
if (!ParseHttpHeaders(is, &contentTypeBuf, &contentLengthBuf)) {
|
||||
SWARNING("{}", "disconnected during headers");
|
||||
SWARNING("disconnected during headers");
|
||||
PutError("disconnected during headers", wpi::Now());
|
||||
return false;
|
||||
}
|
||||
@@ -294,7 +295,7 @@ bool HttpCameraImpl::DeviceStreamFrame(wpi::raw_istream& is,
|
||||
// Ugh, no Content-Length? Read the blocks of the JPEG file.
|
||||
int width, height;
|
||||
if (!ReadJpeg(is, imageBuf, &width, &height)) {
|
||||
SWARNING("{}", "did not receive a JPEG image");
|
||||
SWARNING("did not receive a JPEG image");
|
||||
PutError("did not receive a JPEG image", wpi::Now());
|
||||
return false;
|
||||
}
|
||||
@@ -313,7 +314,7 @@ bool HttpCameraImpl::DeviceStreamFrame(wpi::raw_istream& is,
|
||||
}
|
||||
int width, height;
|
||||
if (!GetJpegSize(image->str(), &width, &height)) {
|
||||
SWARNING("{}", "did not receive a JPEG image");
|
||||
SWARNING("did not receive a JPEG image");
|
||||
PutError("did not receive a JPEG image", wpi::Now());
|
||||
return false;
|
||||
}
|
||||
@@ -329,7 +330,7 @@ void HttpCameraImpl::SettingsThreadMain() {
|
||||
wpi::HttpRequest req;
|
||||
{
|
||||
std::unique_lock lock(m_mutex);
|
||||
m_settingsCond.wait(lock, [=] {
|
||||
m_settingsCond.wait(lock, [=, this] {
|
||||
return !m_active || (m_prefLocation != -1 && !m_settings.empty());
|
||||
});
|
||||
if (!m_active) {
|
||||
@@ -343,7 +344,7 @@ void HttpCameraImpl::SettingsThreadMain() {
|
||||
DeviceSendSettings(req);
|
||||
}
|
||||
|
||||
SDEBUG("{}", "Settings Thread exiting");
|
||||
SDEBUG("Settings Thread exiting");
|
||||
}
|
||||
|
||||
void HttpCameraImpl::DeviceSendSettings(wpi::HttpRequest& req) {
|
||||
@@ -378,7 +379,7 @@ CS_HttpCameraKind HttpCameraImpl::GetKind() const {
|
||||
return m_kind;
|
||||
}
|
||||
|
||||
bool HttpCameraImpl::SetUrls(wpi::span<const std::string> urls,
|
||||
bool HttpCameraImpl::SetUrls(std::span<const std::string> urls,
|
||||
CS_Status* status) {
|
||||
std::vector<wpi::HttpLocation> locations;
|
||||
for (const auto& url : urls) {
|
||||
@@ -572,14 +573,14 @@ CS_Source CreateHttpCamera(std::string_view name, std::string_view url,
|
||||
break;
|
||||
}
|
||||
std::string urlStr{url};
|
||||
if (!source->SetUrls(wpi::span{&urlStr, 1}, status)) {
|
||||
if (!source->SetUrls(std::span{&urlStr, 1}, status)) {
|
||||
return 0;
|
||||
}
|
||||
return inst.CreateSource(CS_SOURCE_HTTP, source);
|
||||
}
|
||||
|
||||
CS_Source CreateHttpCamera(std::string_view name,
|
||||
wpi::span<const std::string> urls,
|
||||
std::span<const std::string> urls,
|
||||
CS_HttpCameraKind kind, CS_Status* status) {
|
||||
auto& inst = Instance::GetInstance();
|
||||
if (urls.empty()) {
|
||||
@@ -603,7 +604,7 @@ CS_HttpCameraKind GetHttpCameraKind(CS_Source source, CS_Status* status) {
|
||||
return static_cast<HttpCameraImpl&>(*data->source).GetKind();
|
||||
}
|
||||
|
||||
void SetHttpCameraUrls(CS_Source source, wpi::span<const std::string> urls,
|
||||
void SetHttpCameraUrls(CS_Source source, std::span<const std::string> urls,
|
||||
CS_Status* status) {
|
||||
if (urls.empty()) {
|
||||
*status = CS_EMPTY_VALUE;
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
#include <functional>
|
||||
#include <initializer_list>
|
||||
#include <memory>
|
||||
#include <span>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <thread>
|
||||
@@ -18,7 +19,6 @@
|
||||
#include <wpi/StringMap.h>
|
||||
#include <wpi/condition_variable.h>
|
||||
#include <wpi/raw_istream.h>
|
||||
#include <wpi/span.h>
|
||||
#include <wpinet/HttpUtil.h>
|
||||
|
||||
#include "SourceImpl.h"
|
||||
@@ -55,7 +55,7 @@ class HttpCameraImpl : public SourceImpl {
|
||||
void NumSinksEnabledChanged() override;
|
||||
|
||||
CS_HttpCameraKind GetKind() const;
|
||||
bool SetUrls(wpi::span<const std::string> urls, CS_Status* status);
|
||||
bool SetUrls(std::span<const std::string> urls, CS_Status* status);
|
||||
std::vector<std::string> GetUrls() const;
|
||||
|
||||
// Property data
|
||||
|
||||
@@ -74,6 +74,8 @@ class Image {
|
||||
switch (pixelFormat) {
|
||||
case VideoMode::kYUYV:
|
||||
case VideoMode::kRGB565:
|
||||
case VideoMode::kY16:
|
||||
case VideoMode::kUYVY:
|
||||
type = CV_8UC2;
|
||||
break;
|
||||
case VideoMode::kBGR:
|
||||
|
||||
@@ -86,16 +86,16 @@ class Instance {
|
||||
void DestroySource(CS_Source handle);
|
||||
void DestroySink(CS_Sink handle);
|
||||
|
||||
wpi::span<CS_Source> EnumerateSourceHandles(
|
||||
std::span<CS_Source> EnumerateSourceHandles(
|
||||
wpi::SmallVectorImpl<CS_Source>& vec) {
|
||||
return m_sources.GetAll(vec);
|
||||
}
|
||||
|
||||
wpi::span<CS_Sink> EnumerateSinkHandles(wpi::SmallVectorImpl<CS_Sink>& vec) {
|
||||
std::span<CS_Sink> EnumerateSinkHandles(wpi::SmallVectorImpl<CS_Sink>& vec) {
|
||||
return m_sinks.GetAll(vec);
|
||||
}
|
||||
|
||||
wpi::span<CS_Sink> EnumerateSourceSinks(CS_Source source,
|
||||
std::span<CS_Sink> EnumerateSourceSinks(CS_Source source,
|
||||
wpi::SmallVectorImpl<CS_Sink>& vec) {
|
||||
vec.clear();
|
||||
m_sinks.ForEach([&](CS_Sink sinkHandle, const SinkData& data) {
|
||||
|
||||
@@ -27,26 +27,37 @@ inline void NamedLog(wpi::Logger& logger, unsigned int level, const char* file,
|
||||
|
||||
} // namespace cs
|
||||
|
||||
#define LOG(level, format, ...) WPI_LOG(m_logger, level, format, __VA_ARGS__)
|
||||
#define LOG(level, format, ...) \
|
||||
WPI_LOG(m_logger, level, format __VA_OPT__(, ) __VA_ARGS__)
|
||||
|
||||
#undef ERROR
|
||||
#define ERROR(format, ...) WPI_ERROR(m_logger, format, __VA_ARGS__)
|
||||
#define WARNING(format, ...) WPI_WARNING(m_logger, format, __VA_ARGS__)
|
||||
#define INFO(format, ...) WPI_INFO(m_logger, format, __VA_ARGS__)
|
||||
#define ERROR(format, ...) \
|
||||
WPI_ERROR(m_logger, format __VA_OPT__(, ) __VA_ARGS__)
|
||||
#define WARNING(format, ...) \
|
||||
WPI_WARNING(m_logger, format __VA_OPT__(, ) __VA_ARGS__)
|
||||
#define INFO(format, ...) WPI_INFO(m_logger, format __VA_OPT__(, ) __VA_ARGS__)
|
||||
|
||||
#define DEBUG0(format, ...) WPI_DEBUG(m_logger, format, __VA_ARGS__)
|
||||
#define DEBUG1(format, ...) WPI_DEBUG1(m_logger, format, __VA_ARGS__)
|
||||
#define DEBUG2(format, ...) WPI_DEBUG2(m_logger, format, __VA_ARGS__)
|
||||
#define DEBUG3(format, ...) WPI_DEBUG3(m_logger, format, __VA_ARGS__)
|
||||
#define DEBUG4(format, ...) WPI_DEBUG4(m_logger, format, __VA_ARGS__)
|
||||
#define DEBUG0(format, ...) \
|
||||
WPI_DEBUG(m_logger, format __VA_OPT__(, ) __VA_ARGS__)
|
||||
#define DEBUG1(format, ...) \
|
||||
WPI_DEBUG1(m_logger, format __VA_OPT__(, ) __VA_ARGS__)
|
||||
#define DEBUG2(format, ...) \
|
||||
WPI_DEBUG2(m_logger, format __VA_OPT__(, ) __VA_ARGS__)
|
||||
#define DEBUG3(format, ...) \
|
||||
WPI_DEBUG3(m_logger, format __VA_OPT__(, ) __VA_ARGS__)
|
||||
#define DEBUG4(format, ...) \
|
||||
WPI_DEBUG4(m_logger, format __VA_OPT__(, ) __VA_ARGS__)
|
||||
|
||||
#define SLOG(level, format, ...) \
|
||||
NamedLog(m_logger, level, __FILE__, __LINE__, GetName(), FMT_STRING(format), \
|
||||
__VA_ARGS__)
|
||||
#define SLOG(level, format, ...) \
|
||||
NamedLog(m_logger, level, __FILE__, __LINE__, GetName(), \
|
||||
FMT_STRING(format) __VA_OPT__(, ) __VA_ARGS__)
|
||||
|
||||
#define SERROR(format, ...) SLOG(::wpi::WPI_LOG_ERROR, format, __VA_ARGS__)
|
||||
#define SWARNING(format, ...) SLOG(::wpi::WPI_LOG_WARNING, format, __VA_ARGS__)
|
||||
#define SINFO(format, ...) SLOG(::wpi::WPI_LOG_INFO, format, __VA_ARGS__)
|
||||
#define SERROR(format, ...) \
|
||||
SLOG(::wpi::WPI_LOG_ERROR, format __VA_OPT__(, ) __VA_ARGS__)
|
||||
#define SWARNING(format, ...) \
|
||||
SLOG(::wpi::WPI_LOG_WARNING, format __VA_OPT__(, ) __VA_ARGS__)
|
||||
#define SINFO(format, ...) \
|
||||
SLOG(::wpi::WPI_LOG_INFO, format __VA_OPT__(, ) __VA_ARGS__)
|
||||
|
||||
#ifdef NDEBUG
|
||||
#define SDEBUG(format, ...) \
|
||||
@@ -65,11 +76,16 @@ inline void NamedLog(wpi::Logger& logger, unsigned int level, const char* file,
|
||||
do { \
|
||||
} while (0)
|
||||
#else
|
||||
#define SDEBUG(format, ...) SLOG(::wpi::WPI_LOG_DEBUG, format, __VA_ARGS__)
|
||||
#define SDEBUG1(format, ...) SLOG(::wpi::WPI_LOG_DEBUG1, format, __VA_ARGS__)
|
||||
#define SDEBUG2(format, ...) SLOG(::wpi::WPI_LOG_DEBUG2, format, __VA_ARGS__)
|
||||
#define SDEBUG3(format, ...) SLOG(::wpi::WPI_LOG_DEBUG3, format, __VA_ARGS__)
|
||||
#define SDEBUG4(format, ...) SLOG(::wpi::WPI_LOG_DEBUG4, format, __VA_ARGS__)
|
||||
#define SDEBUG(format, ...) \
|
||||
SLOG(::wpi::WPI_LOG_DEBUG, format __VA_OPT__(, ) __VA_ARGS__)
|
||||
#define SDEBUG1(format, ...) \
|
||||
SLOG(::wpi::WPI_LOG_DEBUG1, format __VA_OPT__(, ) __VA_ARGS__)
|
||||
#define SDEBUG2(format, ...) \
|
||||
SLOG(::wpi::WPI_LOG_DEBUG2, format __VA_OPT__(, ) __VA_ARGS__)
|
||||
#define SDEBUG3(format, ...) \
|
||||
SLOG(::wpi::WPI_LOG_DEBUG3, format __VA_OPT__(, ) __VA_ARGS__)
|
||||
#define SDEBUG4(format, ...) \
|
||||
SLOG(::wpi::WPI_LOG_DEBUG4, format __VA_OPT__(, ) __VA_ARGS__)
|
||||
#endif
|
||||
|
||||
#endif // CSCORE_LOG_H_
|
||||
|
||||
@@ -460,6 +460,12 @@ void MjpegServerImpl::ConnThread::SendHTML(wpi::raw_ostream& os,
|
||||
case VideoMode::kGray:
|
||||
os << "gray";
|
||||
break;
|
||||
case VideoMode::kY16:
|
||||
os << "Y16";
|
||||
break;
|
||||
case VideoMode::kUYVY:
|
||||
os << "UYVY";
|
||||
break;
|
||||
default:
|
||||
os << "unknown";
|
||||
break;
|
||||
@@ -569,6 +575,12 @@ void MjpegServerImpl::ConnThread::SendJSON(wpi::raw_ostream& os,
|
||||
case VideoMode::kGray:
|
||||
os << "gray";
|
||||
break;
|
||||
case VideoMode::kY16:
|
||||
os << "Y16";
|
||||
break;
|
||||
case VideoMode::kUYVY:
|
||||
os << "UYVY";
|
||||
break;
|
||||
default:
|
||||
os << "unknown";
|
||||
break;
|
||||
@@ -650,7 +662,7 @@ void MjpegServerImpl::Stop() {
|
||||
// Send HTTP response and a stream of JPG-frames
|
||||
void MjpegServerImpl::ConnThread::SendStream(wpi::raw_socket_ostream& os) {
|
||||
if (m_noStreaming) {
|
||||
SERROR("{}", "Too many simultaneous client streams");
|
||||
SERROR("Too many simultaneous client streams");
|
||||
SendError(os, 503, "Too many simultaneous streams");
|
||||
return;
|
||||
}
|
||||
@@ -663,7 +675,7 @@ void MjpegServerImpl::ConnThread::SendStream(wpi::raw_socket_ostream& os) {
|
||||
SendHeader(oss, 200, "OK", "multipart/x-mixed-replace;boundary=" BOUNDARY);
|
||||
os << oss.str();
|
||||
|
||||
SDEBUG("{}", "Headers send, sending stream now");
|
||||
SDEBUG("Headers send, sending stream now");
|
||||
|
||||
Frame::Time lastFrameTime = 0;
|
||||
Frame::Time timePerFrame = 0;
|
||||
@@ -685,7 +697,7 @@ void MjpegServerImpl::ConnThread::SendStream(wpi::raw_socket_ostream& os) {
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(200));
|
||||
continue;
|
||||
}
|
||||
SDEBUG4("{}", "waiting for frame");
|
||||
SDEBUG4("waiting for frame");
|
||||
Frame frame = source->GetNextFrame(0.225); // blocks
|
||||
if (!m_active) {
|
||||
break;
|
||||
@@ -740,8 +752,10 @@ void MjpegServerImpl::ConnThread::SendStream(wpi::raw_socket_ostream& os) {
|
||||
// for adding it if required.
|
||||
addDHT = JpegNeedsDHT(data, &size, &locSOF);
|
||||
break;
|
||||
case VideoMode::kYUYV:
|
||||
case VideoMode::kUYVY:
|
||||
case VideoMode::kRGB565:
|
||||
case VideoMode::kYUYV:
|
||||
case VideoMode::kY16:
|
||||
default:
|
||||
// Bad frame; sleep for 10 ms so we don't consume all processor time.
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(10));
|
||||
@@ -783,7 +797,7 @@ void MjpegServerImpl::ConnThread::ProcessRequest() {
|
||||
wpi::SmallString<128> reqBuf;
|
||||
std::string_view req = is.getline(reqBuf, 4096);
|
||||
if (is.has_error()) {
|
||||
SDEBUG("{}", "error getting request string");
|
||||
SDEBUG("error getting request string");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -824,7 +838,7 @@ void MjpegServerImpl::ConnThread::ProcessRequest() {
|
||||
} else if (req.find("GET / ") != std::string_view::npos || req == "GET /\n") {
|
||||
kind = kRootPage;
|
||||
} else {
|
||||
SDEBUG("{}", "HTTP request resource not found");
|
||||
SDEBUG("HTTP request resource not found");
|
||||
SendError(os, 404, "Resource not found");
|
||||
return;
|
||||
}
|
||||
@@ -866,11 +880,11 @@ void MjpegServerImpl::ConnThread::ProcessRequest() {
|
||||
SendHeader(os, 200, "OK", "text/plain");
|
||||
os << "Ignored due to no connected source."
|
||||
<< "\r\n";
|
||||
SDEBUG("{}", "Ignored due to no connected source.");
|
||||
SDEBUG("Ignored due to no connected source.");
|
||||
}
|
||||
break;
|
||||
case kGetSettings:
|
||||
SDEBUG("{}", "request for JSON file");
|
||||
SDEBUG("request for JSON file");
|
||||
if (auto source = GetSource()) {
|
||||
SendJSON(os, *source, true);
|
||||
} else {
|
||||
@@ -878,7 +892,7 @@ void MjpegServerImpl::ConnThread::ProcessRequest() {
|
||||
}
|
||||
break;
|
||||
case kGetSourceConfig:
|
||||
SDEBUG("{}", "request for JSON file");
|
||||
SDEBUG("request for JSON file");
|
||||
if (auto source = GetSource()) {
|
||||
SendHeader(os, 200, "OK", "application/json");
|
||||
CS_Status status = CS_OK;
|
||||
@@ -889,7 +903,7 @@ void MjpegServerImpl::ConnThread::ProcessRequest() {
|
||||
}
|
||||
break;
|
||||
case kRootPage:
|
||||
SDEBUG("{}", "request for root page");
|
||||
SDEBUG("request for root page");
|
||||
SendHeader(os, 200, "OK", "text/html");
|
||||
if (auto source = GetSource()) {
|
||||
SendHTML(os, *source, false);
|
||||
@@ -900,7 +914,7 @@ void MjpegServerImpl::ConnThread::ProcessRequest() {
|
||||
break;
|
||||
}
|
||||
|
||||
SDEBUG("{}", "leaving HTTP client thread");
|
||||
SDEBUG("leaving HTTP client thread");
|
||||
}
|
||||
|
||||
// worker thread for clients that connected to this server
|
||||
@@ -927,7 +941,7 @@ void MjpegServerImpl::ServerThreadMain() {
|
||||
return;
|
||||
}
|
||||
|
||||
SDEBUG("{}", "waiting for clients to connect");
|
||||
SDEBUG("waiting for clients to connect");
|
||||
while (m_active) {
|
||||
auto stream = m_acceptor->accept();
|
||||
if (!stream) {
|
||||
@@ -977,7 +991,7 @@ void MjpegServerImpl::ServerThreadMain() {
|
||||
thr->m_cond.notify_one();
|
||||
}
|
||||
|
||||
SDEBUG("{}", "leaving server thread");
|
||||
SDEBUG("leaving server thread");
|
||||
}
|
||||
|
||||
void MjpegServerImpl::SetSourceImpl(std::shared_ptr<SourceImpl> source) {
|
||||
|
||||
@@ -27,7 +27,7 @@ int PropertyContainer::GetPropertyIndex(std::string_view name) const {
|
||||
return ndx;
|
||||
}
|
||||
|
||||
wpi::span<int> PropertyContainer::EnumerateProperties(
|
||||
std::span<int> PropertyContainer::EnumerateProperties(
|
||||
wpi::SmallVectorImpl<int>& vec, CS_Status* status) const {
|
||||
if (!m_properties_cached && !CacheProperties(status)) {
|
||||
return {};
|
||||
|
||||
@@ -8,13 +8,13 @@
|
||||
#include <atomic>
|
||||
#include <cstddef>
|
||||
#include <memory>
|
||||
#include <span>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
#include <wpi/StringMap.h>
|
||||
#include <wpi/mutex.h>
|
||||
#include <wpi/span.h>
|
||||
|
||||
#include "PropertyImpl.h"
|
||||
#include "cscore_cpp.h"
|
||||
@@ -33,7 +33,7 @@ class PropertyContainer {
|
||||
virtual ~PropertyContainer() = default;
|
||||
|
||||
int GetPropertyIndex(std::string_view name) const;
|
||||
wpi::span<int> EnumerateProperties(wpi::SmallVectorImpl<int>& vec,
|
||||
std::span<int> EnumerateProperties(wpi::SmallVectorImpl<int>& vec,
|
||||
CS_Status* status) const;
|
||||
CS_PropertyKind GetPropertyKind(int property) const;
|
||||
std::string_view GetPropertyName(int property,
|
||||
|
||||
@@ -127,7 +127,7 @@ void RawSinkImpl::ThreadMain() {
|
||||
std::this_thread::sleep_for(std::chrono::seconds(1));
|
||||
continue;
|
||||
}
|
||||
SDEBUG4("{}", "waiting for frame");
|
||||
SDEBUG4("waiting for frame");
|
||||
Frame frame = source->GetNextFrame(); // blocks
|
||||
if (!m_active) {
|
||||
break;
|
||||
|
||||
@@ -26,6 +26,8 @@ void RawSourceImpl::PutFrame(const CS_RawFrame& image) {
|
||||
switch (image.pixelFormat) {
|
||||
case VideoMode::kYUYV:
|
||||
case VideoMode::kRGB565:
|
||||
case VideoMode::kY16:
|
||||
case VideoMode::kUYVY:
|
||||
type = CV_8UC2;
|
||||
break;
|
||||
case VideoMode::kBGR:
|
||||
|
||||
@@ -76,7 +76,7 @@ Frame SourceImpl::GetCurFrame() {
|
||||
Frame SourceImpl::GetNextFrame() {
|
||||
std::unique_lock lock{m_frameMutex};
|
||||
auto oldTime = m_frame.GetTime();
|
||||
m_frameCv.wait(lock, [=] { return m_frame.GetTime() != oldTime; });
|
||||
m_frameCv.wait(lock, [=, this] { return m_frame.GetTime() != oldTime; });
|
||||
return m_frame;
|
||||
}
|
||||
|
||||
@@ -85,7 +85,7 @@ Frame SourceImpl::GetNextFrame(double timeout) {
|
||||
auto oldTime = m_frame.GetTime();
|
||||
if (!m_frameCv.wait_for(
|
||||
lock, std::chrono::milliseconds(static_cast<int>(timeout * 1000)),
|
||||
[=] { return m_frame.GetTime() != oldTime; })) {
|
||||
[=, this] { return m_frame.GetTime() != oldTime; })) {
|
||||
m_frame = Frame{*this, "timed out getting frame", wpi::Now()};
|
||||
}
|
||||
return m_frame;
|
||||
@@ -198,6 +198,10 @@ bool SourceImpl::SetConfigJson(const wpi::json& config, CS_Status* status) {
|
||||
mode.pixelFormat = cs::VideoMode::kBGR;
|
||||
} else if (wpi::equals_lower(str, "gray")) {
|
||||
mode.pixelFormat = cs::VideoMode::kGray;
|
||||
} else if (wpi::equals_lower(str, "y16")) {
|
||||
mode.pixelFormat = cs::VideoMode::kY16;
|
||||
} else if (wpi::equals_lower(str, "uyvy")) {
|
||||
mode.pixelFormat = cs::VideoMode::kUYVY;
|
||||
} else {
|
||||
SWARNING("SetConfigJson: could not understand pixel format value '{}'",
|
||||
str);
|
||||
@@ -360,6 +364,12 @@ wpi::json SourceImpl::GetConfigJsonObject(CS_Status* status) {
|
||||
case VideoMode::kGray:
|
||||
pixelFormat = "gray";
|
||||
break;
|
||||
case VideoMode::kY16:
|
||||
pixelFormat = "y16";
|
||||
break;
|
||||
case VideoMode::kUYVY:
|
||||
pixelFormat = "uyvy";
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -50,6 +50,7 @@ class SourceImpl : public PropertyContainer {
|
||||
|
||||
void SetConnectionStrategy(CS_ConnectionStrategy strategy) {
|
||||
m_strategy = static_cast<int>(strategy);
|
||||
NumSinksChanged();
|
||||
}
|
||||
bool IsEnabled() const {
|
||||
return m_strategy == CS_CONNECTION_KEEP_OPEN ||
|
||||
|
||||
@@ -6,12 +6,12 @@
|
||||
#define CSCORE_UNLIMITEDHANDLERESOURCE_H_
|
||||
|
||||
#include <memory>
|
||||
#include <span>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include <wpi/SmallVector.h>
|
||||
#include <wpi/mutex.h>
|
||||
#include <wpi/span.h>
|
||||
|
||||
namespace cs {
|
||||
|
||||
@@ -50,7 +50,7 @@ class UnlimitedHandleResource {
|
||||
std::shared_ptr<TStruct> Free(THandle handle);
|
||||
|
||||
template <typename T>
|
||||
wpi::span<T> GetAll(wpi::SmallVectorImpl<T>& vec);
|
||||
std::span<T> GetAll(wpi::SmallVectorImpl<T>& vec);
|
||||
|
||||
std::vector<std::shared_ptr<TStruct>> FreeAll();
|
||||
|
||||
@@ -151,10 +151,10 @@ UnlimitedHandleResource<THandle, TStruct, typeValue, TMutex>::Free(
|
||||
|
||||
template <typename THandle, typename TStruct, int typeValue, typename TMutex>
|
||||
template <typename T>
|
||||
inline wpi::span<T>
|
||||
inline std::span<T>
|
||||
UnlimitedHandleResource<THandle, TStruct, typeValue, TMutex>::GetAll(
|
||||
wpi::SmallVectorImpl<T>& vec) {
|
||||
ForEach([&](THandle handle, const TStruct& data) { vec.push_back(handle); });
|
||||
ForEach([&](THandle handle, const TStruct&) { vec.push_back(handle); });
|
||||
return vec;
|
||||
}
|
||||
|
||||
|
||||
@@ -286,7 +286,7 @@ CS_Property GetSourceProperty(CS_Source source, std::string_view name,
|
||||
return Handle{source, property, Handle::kProperty};
|
||||
}
|
||||
|
||||
wpi::span<CS_Property> EnumerateSourceProperties(
|
||||
std::span<CS_Property> EnumerateSourceProperties(
|
||||
CS_Source source, wpi::SmallVectorImpl<CS_Property>& vec,
|
||||
CS_Status* status) {
|
||||
auto data = Instance::GetInstance().GetSource(source);
|
||||
@@ -398,7 +398,7 @@ std::vector<VideoMode> EnumerateSourceVideoModes(CS_Source source,
|
||||
return data->source->EnumerateVideoModes(status);
|
||||
}
|
||||
|
||||
wpi::span<CS_Sink> EnumerateSourceSinks(CS_Source source,
|
||||
std::span<CS_Sink> EnumerateSourceSinks(CS_Source source,
|
||||
wpi::SmallVectorImpl<CS_Sink>& vec,
|
||||
CS_Status* status) {
|
||||
auto& inst = Instance::GetInstance();
|
||||
@@ -583,7 +583,7 @@ CS_Property GetSinkProperty(CS_Sink sink, std::string_view name,
|
||||
return Handle{sink, property, Handle::kSinkProperty};
|
||||
}
|
||||
|
||||
wpi::span<CS_Property> EnumerateSinkProperties(
|
||||
std::span<CS_Property> EnumerateSinkProperties(
|
||||
CS_Sink sink, wpi::SmallVectorImpl<CS_Property>& vec, CS_Status* status) {
|
||||
auto data = Instance::GetInstance().GetSink(sink);
|
||||
if (!data) {
|
||||
@@ -865,12 +865,12 @@ void Shutdown() {
|
||||
// Utility Functions
|
||||
//
|
||||
|
||||
wpi::span<CS_Source> EnumerateSourceHandles(
|
||||
std::span<CS_Source> EnumerateSourceHandles(
|
||||
wpi::SmallVectorImpl<CS_Source>& vec, CS_Status* status) {
|
||||
return Instance::GetInstance().EnumerateSourceHandles(vec);
|
||||
}
|
||||
|
||||
wpi::span<CS_Sink> EnumerateSinkHandles(wpi::SmallVectorImpl<CS_Sink>& vec,
|
||||
std::span<CS_Sink> EnumerateSinkHandles(wpi::SmallVectorImpl<CS_Sink>& vec,
|
||||
CS_Status* status) {
|
||||
return Instance::GetInstance().EnumerateSinkHandles(vec);
|
||||
}
|
||||
|
||||
@@ -3,16 +3,17 @@
|
||||
// the WPILib BSD license file in the root directory of this project.
|
||||
|
||||
#include <exception>
|
||||
#include <span>
|
||||
|
||||
#include <fmt/format.h>
|
||||
#include <opencv2/core/core.hpp>
|
||||
#include <wpi/SmallString.h>
|
||||
#include <wpi/jni_util.h>
|
||||
#include <wpi/span.h>
|
||||
|
||||
#include "cscore_cpp.h"
|
||||
#include "cscore_cv.h"
|
||||
#include "cscore_raw.h"
|
||||
#include "cscore_runloop.h"
|
||||
#include "edu_wpi_first_cscore_CameraServerJNI.h"
|
||||
|
||||
namespace cv {
|
||||
@@ -296,7 +297,7 @@ static jobject MakeJObject(JNIEnv* env, const cs::RawEvent& event) {
|
||||
}
|
||||
|
||||
static jobjectArray MakeJObject(JNIEnv* env,
|
||||
wpi::span<const cs::RawEvent> arr) {
|
||||
std::span<const cs::RawEvent> arr) {
|
||||
jobjectArray jarr = env->NewObjectArray(arr.size(), videoEventCls, nullptr);
|
||||
if (!jarr) {
|
||||
return nullptr;
|
||||
@@ -2226,4 +2227,40 @@ Java_edu_wpi_first_cscore_CameraServerJNI_freeRawFrame
|
||||
delete ptr;
|
||||
}
|
||||
|
||||
/*
|
||||
* Class: edu_wpi_first_cscore_CameraServerJNI
|
||||
* Method: runMainRunLoop
|
||||
* Signature: ()V
|
||||
*/
|
||||
JNIEXPORT void JNICALL
|
||||
Java_edu_wpi_first_cscore_CameraServerJNI_runMainRunLoop
|
||||
(JNIEnv*, jclass)
|
||||
{
|
||||
cs::RunMainRunLoop();
|
||||
}
|
||||
|
||||
/*
|
||||
* Class: edu_wpi_first_cscore_CameraServerJNI
|
||||
* Method: runMainRunLoopTimeout
|
||||
* Signature: (D)I
|
||||
*/
|
||||
JNIEXPORT jint JNICALL
|
||||
Java_edu_wpi_first_cscore_CameraServerJNI_runMainRunLoopTimeout
|
||||
(JNIEnv*, jclass, jdouble timeoutSeconds)
|
||||
{
|
||||
return cs::RunMainRunLoopTimeout(timeoutSeconds);
|
||||
}
|
||||
|
||||
/*
|
||||
* Class: edu_wpi_first_cscore_CameraServerJNI
|
||||
* Method: stopMainRunLoop
|
||||
* Signature: ()V
|
||||
*/
|
||||
JNIEXPORT void JNICALL
|
||||
Java_edu_wpi_first_cscore_CameraServerJNI_stopMainRunLoop
|
||||
(JNIEnv*, jclass)
|
||||
{
|
||||
return cs::StopMainRunLoop();
|
||||
}
|
||||
|
||||
} // extern "C"
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user