Compare commits

...

43 Commits

Author SHA1 Message Date
Matt
dcad7f34a2 Fix thinclient address in dev builds and move thinclient (#586)
* Fix thinclient address in dev builds and move thinclient

Update USBFrameProvider.java

Create index.html

Update index.html

Null check stream to prevent spam

* Update main.yml

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

* Update docstring

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

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

* Fix compile issue that mainTable is not accessible form SimPhotonCamera

* Fix format

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

* Add hamming and decision margin to UI, pipeline

* Run spotless

* Update index.html

* Update index.html

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

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

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

* WIP Sim fixups and working on testcases

* Still doesn't work, but closer

* tests pass

* removed C++ sim support

* formatting update

* adjusted target height above ground per review

* Turns out its unused

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

* Make xz multithreaded

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

* just more WIP

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

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

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

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

* bugfixes galore

* tweak compression

* spotless

* more tweaks for handling slow/intermittent streams

* wpilibformat maybe?

* clang-format maybe?

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

* thinclient formatting fixups

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

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

* spotless and formatting

* cmon wpiformat....

* re-added mjpg streams

* added a loading GIF to imporve the feeling of responsiveness

* formatting

* urlparams and built-in thinclient

* wpiformat

* prevent wpiformat complaints

* Removed uint8 array and base64 conversion from client side

* Synced up js implementations for ws streaming

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

* Fix camera resolution selection

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

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

* spotless

* adding tooltips for the acronyms used

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

* wip making the implementaiton more platform independent

* spotless

* wpiformat

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

* Try fetch depth 0

* Remove fetch tags

* Remove describe action

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

* add c++

* run wpiformat

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

* Move print into ntTick

Update NetworkTablesManager.java

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

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

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

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

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

* Stylistic choice to remove excessive whitespace br

* Spotless apply

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

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

* Correct image capture time (correctly)

* Change double literal to not use suffix

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

* Run spotless

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

View File

@@ -24,25 +24,20 @@ jobs:
# The type of runner that the job will run on.
runs-on: ubuntu-latest
# Grab the docker container.
container:
image: docker://node:10
steps:
# Checkout code.
- uses: actions/checkout@v1
- uses: actions/checkout@v3
# Setup Node.js
- name: Setup Node.js
uses: actions/setup-node@v3.4.1
uses: actions/setup-node@v3
with:
node-version: 14
node-version: 16
# Run npm
- run: |
npm install -g npm
npm ci
npm run build --if-present
- run: npm update -g npm
- run: npm ci
- run: npm run build --if-present
# Upload client artifact.
- uses: actions/upload-artifact@master
@@ -57,7 +52,9 @@ jobs:
steps:
# Checkout code.
- name: Checkout code
uses: actions/checkout@v1
uses: actions/checkout@v3
with:
fetch-depth: 0
# Fetch tags.
- name: Fetch tags
@@ -65,9 +62,10 @@ jobs:
# Install Java 11.
- name: Install Java 11
uses: actions/setup-java@v1
uses: actions/setup-java@v3
with:
java-version: 11
distribution: temurin
# Run Gradle build.
- name: Gradle Build
@@ -85,12 +83,12 @@ jobs:
# Publish Coverage Report.
- name: Publish Server Coverage Report
uses: codecov/codecov-action@v1
uses: codecov/codecov-action@v3
with:
file: ./photon-server/build/reports/jacoco/test/jacocoTestReport.xml
- name: Publish Core Coverage Report
uses: codecov/codecov-action@v1
uses: codecov/codecov-action@v3
with:
file: ./photon-core/build/reports/jacoco/test/jacocoTestReport.xml
@@ -99,13 +97,13 @@ jobs:
steps:
# Checkout docs.
- uses: actions/checkout@v2
- uses: actions/checkout@v3
with:
repository: 'PhotonVision/photonvision-docs.git'
ref: master
# Install Python.
- uses: actions/setup-python@v2
- uses: actions/setup-python@v4
with:
python-version: '3.6'
@@ -136,12 +134,15 @@ jobs:
steps:
# Checkout code.
- uses: actions/checkout@v1
- uses: actions/checkout@v3
with:
fetch-depth: 0
# Install Java 11.
- uses: actions/setup-java@v1
- uses: actions/setup-java@v3
with:
java-version: 11
distribution: temurin
# Check server code with Spotless.
- run: |
@@ -168,12 +169,13 @@ jobs:
runs-on: ${{ matrix.os }}
name: "Photonlib - Build - ${{ matrix.artifact-name }}"
steps:
- uses: actions/checkout@v2.3.4
- uses: actions/checkout@v3
with:
fetch-depth: 0
- uses: actions/setup-java@v1
- uses: actions/setup-java@v3
with:
java-version: 11
distribution: temurin
- run: git fetch --tags --force
- run: |
chmod +x gradlew
@@ -200,12 +202,13 @@ jobs:
container: ${{ matrix.container }}
name: "Photonlib - Build - ${{ matrix.artifact-name }}"
steps:
- uses: actions/checkout@v2.3.4
- uses: actions/checkout@v3
with:
fetch-depth: 0
- uses: actions/setup-java@v1
- uses: actions/setup-java@v3
with:
java-version: 11
distribution: temurin
- run: |
chmod +x gradlew
./gradlew photon-lib:build --max-workers 1
@@ -220,14 +223,14 @@ jobs:
name: "wpiformat"
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v3
- name: Fetch all history and metadata
run: |
git fetch --prune --unshallow
git checkout -b pr
git branch -f master origin/master
- name: Set up Python 3.8
uses: actions/setup-python@v2
uses: actions/setup-python@v4
with:
python-version: 3.8
- name: Install clang-format
@@ -244,7 +247,7 @@ 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
@@ -258,12 +261,15 @@ jobs:
steps:
# Checkout code.
- uses: actions/checkout@v1
- uses: actions/checkout@v3
with:
fetch-depth: 0
# Install Java 11.
- uses: actions/setup-java@v1
- uses: actions/setup-java@v3
with:
java-version: 11
distribution: temurin
# Clear any existing web resources.
- run: |
@@ -271,13 +277,13 @@ jobs:
mkdir -p photon-server/src/main/resources/web/docs
# Download client artifact to resources folder.
- uses: actions/download-artifact@v2
- uses: actions/download-artifact@v3
with:
name: built-client
path: photon-server/src/main/resources/web/
# Download docs artifact to resources folder.
- uses: actions/download-artifact@v2
- uses: actions/download-artifact@v3
with:
name: built-docs
path: photon-server/src/main/resources/web/docs
@@ -296,14 +302,15 @@ jobs:
./scripts/generatePiImage.sh
# Upload final fat jar as artifact.
- uses: actions/upload-artifact@master
- uses: actions/upload-artifact@v3
with:
name: jar
name: jars
path: photon-server/build/libs
- uses: actions/upload-artifact@master
- uses: actions/upload-artifact@v3
if: github.event_name != 'pull_request'
with:
name: image
path: photonvision*.zip
path: photonvision*.xz
- uses: pyTooling/Actions/releaser@r0
with:
@@ -312,7 +319,7 @@ jobs:
rm: true
files: |
photon-server/build/libs/*.jar
photonvision*.zip
photonvision*.xz
if: github.event_name == 'push'
photon-release:
@@ -323,7 +330,7 @@ jobs:
# This *should* pull in fat and pi-only jars
- uses: actions/download-artifact@v2
with:
name: jar
name: jars
# And the image we made previously
- uses: actions/download-artifact@v2

1
.gitignore vendored
View File

@@ -30,6 +30,7 @@ backend/settings/
*.nar
*.ear
*.zip
*.xz
*.tar.gz
*.rar

View File

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

View File

@@ -11,7 +11,7 @@ plugins {
allprojects {
repositories {
jcenter()
mavenCentral()
maven { url = "https://maven.photonvision.org/repository/internal/" }
}
wpilibRepositories.addAllReleaseRepositories(it)

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View File

@@ -0,0 +1,317 @@
<!DOCTYPE html>
<html>
<head>
<title>ThinClient</title>
<style>
* {
margin: 0;
padding: 0;
}
.imgbox {
display: grid;
height: 100%;
width: 100%;
}
.center-fit {
width: 90vw;
margin: auto;
}
</style>
</head>
<body>
<hr>
<div class="imgbox">
<img id="streamImg" class="center-fit" src=''>
</div>
<hr>
<form id="frm1">
Host <input type="text" id="host" value="photonvision.local"><br>
Port <input type="text" id="port" value="1181"><br>
</form>
<button>Start Stream</button>
<script type="module">
class WebsocketVideoStream{
constructor(drawDiv, streamPort, host) {
this.drawDiv = drawDiv;
this.image = document.getElementById(this.drawDiv);
this.streamPort = streamPort;
this.newStreamPortReq = null;
this.serverAddr = "ws://" + host + "/websocket_cameras";
this.dispNoStream();
this.ws_connect();
this.imgData = null;
this.imgDataTime = -1;
this.imgObjURL = null;
this.frameRxCount = 0;
//Display state machine
this.DSM_DISCONNECTED = "DISCONNECTED";
this.DSM_WAIT_FOR_VALID_PORT = "WAIT_FOR_VALID_PORT";
this.DSM_SUBSCRIBE = "SUBSCRIBE";
this.DSM_WAIT_FOR_FIRST_FRAME = "WAIT_FOR_FIRST_FRAME";
this.DSM_SHOWING = "SHOWING";
this.DSM_RESTART_UNSUBSCRIBE = "UNSUBSCRIBE";
this.DSM_RESTART_WAIT = "WAIT_BEFORE_SUBSCRIBE";
this.dsm_cur_state = this.DSM_DISCONNECTED;
this.dsm_prev_state = this.DSM_DISCONNECTED;
this.dsm_restart_start_time = window.performance.now();
requestAnimationFrame(()=>this.animationLoop());
}
dispImageData(){
//From https://stackoverflow.com/questions/67507616/set-image-src-from-image-blob/67507685#67507685
if(this.imgObjURL != null){
URL.revokeObjectURL(this.imgObjURL)
}
this.imgObjURL = URL.createObjectURL(this.imgData);
//Update the image with the new mimetype and image
this.image.src = this.imgObjURL;
}
dispNoStream() {
this.image.src = "loading.gif";
}
animationLoop(){
// Update time metrics
var now = window.performance.now();
var timeInState = now - this.dsm_restart_start_time;
// Save previous state
this.dsm_prev_state = this.dsm_cur_state;
// Evaluate state transitions
if(this.serverConnectionActive == false){
//Any state - if the server connection goes false, always transition to disconnected
this.dsm_cur_state = this.DSM_DISCONNECTED;
} else {
//Conditional transitions
switch(this.dsm_cur_state) {
case this.DSM_DISCONNECTED:
//Immediately transition to waiting for the first frame
this.dsm_cur_state = this.DSM_WAIT_FOR_VALID_PORT;
break;
case this.DSM_WAIT_FOR_VALID_PORT:
// Wait until the user has configured a valid port
if(this.streamPort > 0){
this.dsm_cur_state = this.DSM_SUBSCRIBE;
} else {
this.dsm_cur_state = this.DSM_WAIT_FOR_VALID_PORT;
}
break;
case this.DSM_SUBSCRIBE:
// Immediately transition after subscriptions is sent
this.dsm_cur_state = this.DSM_WAIT_FOR_FIRST_FRAME;
break;
case this.DSM_WAIT_FOR_FIRST_FRAME:
if(this.imgData != null){
//we got some image data, start showing it
this.dsm_cur_state = this.DSM_SHOWING;
} else if (this.newStreamPortReq != null){
//Stream port requested changed, unsubscribe and restart
this.dsm_cur_state = this.DSM_RESTART_UNSUBSCRIBE;
} else {
this.dsm_cur_state = this.DSM_WAIT_FOR_FIRST_FRAME;
}
break;
case this.DSM_SHOWING:
if((now - this.imgDataTime) > 2500){
//timeout, begin the restart sequence
this.dsm_cur_state = this.DSM_RESTART_UNSUBSCRIBE;
} else if (this.newStreamPortReq != null){
//Stream port requested changed, unsubscribe and restart
this.dsm_cur_state = this.DSM_RESTART_UNSUBSCRIBE;
} else {
//stay in this state.
this.dsm_cur_state = this.DSM_SHOWING;
}
break;
case this.DSM_RESTART_UNSUBSCRIBE:
//Only should spend one loop in Unsubscribe, immediately transition
this.dsm_cur_state = this.DSM_RESTART_WAIT;
break;
case this.DSM_RESTART_WAIT:
if (timeInState > 250) {
//we've waited long enough, go to try to re-subscribe
this.dsm_cur_state = this.DSM_WAIT_FOR_VALID_PORT;
} else {
//stay in this state.
this.dsm_cur_state = this.DSM_RESTART_WAIT;
}
break;
default:
// Shouldn't get here, default back to init
this.dsm_cur_state = this.DSM_DISCONNECTED;
}
}
//take current-state or state-transition actions
if(this.dsm_cur_state != this.dsm_prev_state){
//Any state transition
console.log("State Change: " + this.dsm_prev_state + " -> " + this.dsm_cur_state);
}
if(this.dsm_cur_state == this.DSM_SHOWING){
// Currently in SHOWING
this.dispImageData();
}
if(this.dsm_cur_state != this.DSM_SHOWING && this.dsm_prev_state == this.DSM_SHOWING ){
//Any transition out of showing - no stream
this.dispNoStream();
}
if(this.dsm_cur_state == this.DSM_RESTART_UNSUBSCRIBE){
// Currently in UNSUBSCRIBE, do the unsubscribe actions
this.stopStream();
this.dsm_restart_start_time = now;
}
if(this.dsm_cur_state == this.DSM_SUBSCRIBE){
// Currently in SUBSCRIBE, do the subscribe actions
this.startStream();
this.dsm_restart_start_time = now;
}
if(this.dsm_cur_state == this.DSM_WAIT_FOR_VALID_PORT){
// Currently waiting for a vaild port to be requested
if(this.newStreamPortReq != null){
this.streamPort = this.newStreamPortReq;
this.newStreamPortReq = null;
}
}
requestAnimationFrame(()=>this.animationLoop());
}
startStream() {
console.log("Subscribing to port " + this.streamPort);
this.imgData = null;
this.ws.send(JSON.stringify({"cmd": "subscribe", "port":this.streamPort}));
}
stopStream() {
console.log("Unsubscribing");
this.ws.send(JSON.stringify({"cmd": "unsubscribe"}));
this.imgData = null;
}
setPort(streamPort){
console.log("Port set to " + streamPort);
this.newStreamPortReq = streamPort;
}
ws_onOpen() {
// Set the flag allowing general server communication
this.serverConnectionActive = true;
console.log("Connected!");
}
ws_onClose(e) {
//Clear flags to stop server communication
this.ws = null;
this.serverConnectionActive = false;
console.log('Camera Socket is closed. Reconnect will be attempted in 0.5 second.', e.reason);
setTimeout(this.ws_connect.bind(this), 500);
if(!e.wasClean){
console.error('Socket encountered error!');
}
}
ws_onError(e){
e; //prevent unused failure
this.ws.close();
}
ws_onMessage(e){
if(typeof e.data === 'string'){
//string data from host
//TODO - anything to recieve info here? Maybe "avaialble streams?"
} else {
if(e.data.size > 0){
//binary data - a frame
this.imgData = e.data;
this.imgDataTime = window.performance.now();
this.frameRxCount++;
} else {
//TODO - server is sending empty frames?
}
}
}
ws_connect() {
this.serverConnectionActive = false;
this.ws = new WebSocket(this.serverAddr);
this.ws.binaryType = "blob";
this.ws.onopen = this.ws_onOpen.bind(this);
this.ws.onmessage = this.ws_onMessage.bind(this);
this.ws.onclose = this.ws_onClose.bind(this);
this.ws.onerror = this.ws_onError.bind(this);
console.log("Connecting to server " + this.serverAddr);
}
ws_close(){
this.ws.close();
}
}
var stream = null;
function streamStartRequest() {
var host = document.getElementById("host").value + ":5800";
var port = document.getElementById("port").value;
if(stream == null){
stream = new WebsocketVideoStream("streamImg",port,host);
} else {
stream.setPort(port);
}
}
// Attach listener
document.querySelector('button').addEventListener('click', streamStartRequest);
// Deal with URLParams, validating inputs
const queryString = window.location.search;
const urlParams = new URLSearchParams(queryString);
const port_in = urlParams.get('port')
const host_in = urlParams.get('host')
if(port_in != ""){
document.getElementById("port").value = port_in;
}
if(host_in != ""){
document.getElementById("host").value = host_in;
}
if(port_in != "" & host_in != ""){
streamStartRequest(); //we got valid inputs, auto-start the stream
}
</script>
</body>
</html>

View File

@@ -308,11 +308,7 @@ export default {
} else if (this.$store.state.settings.hasOwnProperty(key)) {
this.$store.commit('mutateSettings', {[key]: value});
} else {
switch (key) {
default: {
console.error("Unknown message from backend: " + value);
}
}
console.error("Unknown message from backend: " + value);
}
},
toggleCompactMode() {

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

After

Width:  |  Height:  |  Size: 76 KiB

View File

@@ -5,7 +5,7 @@
:style="styleObject"
:src="src"
alt=""
@click="e => $emit('click', e)"
@click="e => {this.openThinclientStream(e)}"
>
</template>
@@ -13,10 +13,11 @@
export default {
name: "CvImage",
// eslint-disable-next-line vue/require-prop-types
props: ['address', 'scale', 'maxHeight', 'maxHeightMd', 'maxHeightLg', 'maxHeightXl', 'colorPicking', 'id', 'disconnected'],
props: ['idx', 'scale', 'maxHeight', 'maxHeightMd', 'maxHeightLg', 'maxHeightXl', 'colorPicking', 'id', 'disconnected'],
data() {
return {
seed: 1.0,
src: ""
}
},
computed: {
@@ -46,18 +47,47 @@
return ret;
}
},
src: {
port: {
get() {
return this.disconnected ? require("../../assets/noStream.jpg") : this.address + "?" + this.seed // This prevents caching
},
},
if(this.idx == 0){
return this.$store.state.cameraSettings[this.$store.state.currentCameraIndex].inputStreamPort;
} else {
return this.$store.state.cameraSettings[this.$store.state.currentCameraIndex].outputStreamPort;
}
}
}
},
watch : {
port(newPort, oldPort){
newPort;
oldPort;
this.reload();
},
disconnected(newVal, oldVal){
oldVal;
if(newVal){
this.wsStream.setPort(0);
} else {
this.wsStream.setPort(this.port);
}
}
},
mounted() {
this.reload(); // Force reload image on creation
var wsvs = require('../../plugins/WebsocketVideoStream');
this.wsStream = new wsvs.WebsocketVideoStream(this.id, this.port, this.$address);
},
unmounted() {
this.wsStream.setPort(0);
},
methods: {
reload() {
this.seed = new Date().getTime();
console.log("Reloading " + this.id + " with port " + String(this.port));
this.wsStream.setPort(this.port);
},
openThinclientStream(e){
e;
var URL = "/thinclient.html?port=" + String(this.port) + "&host=" + window.location.hostname;
window.open(URL, '_blank');
}
},
}

View File

@@ -181,7 +181,7 @@ export default {
this.cubes = []
for (const target of this.targets) {
const geometry = new BoxGeometry(0.2, 0.2, 0.3 / 5);
const geometry = new BoxGeometry(0.3 / 5, 0.2, 0.2);
const material = new MeshNormalMaterial();
let quat = (new Quaternion(
target.pose.qx,

View File

@@ -15,11 +15,11 @@ if (process.env.NODE_ENV === "production") {
Vue.prototype.$address = location.hostname + ":5800";
}
const wsURL = '//' + Vue.prototype.$address + '/websocket';
const wsDataURL = '//' + Vue.prototype.$address + '/websocket_data';
import VueNativeSock from 'vue-native-websocket';
Vue.use(VueNativeSock, wsURL, {
Vue.use(VueNativeSock, wsDataURL, {
reconnection: true,
reconnectionDelay: 100,
connectManually: true,

View File

@@ -5,7 +5,7 @@ function initColorPicker() {
if (!canvas)
canvas = document.createElement('canvas');
image = document.querySelector('#normal-stream');
image = document.querySelector('#raw-stream');
if (image !== null) {
canvas.width = image.width;
canvas.height = image.height;

View File

@@ -0,0 +1,244 @@
export class WebsocketVideoStream{
constructor(drawDiv, streamPort, host) {
console.log("host " + host + " port " + streamPort)
this.drawDiv = drawDiv;
this.image = document.getElementById(this.drawDiv);
this.streamPort = streamPort;
this.newStreamPortReq = null;
this.serverAddr = "ws://" + host + "/websocket_cameras";
this.dispNoStream();
this.ws_connect();
this.imgData = null;
this.imgDataTime = -1;
this.imgObjURL = null;
this.frameRxCount = 0;
//Display state machine
this.DSM_DISCONNECTED = "DISCONNECTED";
this.DSM_WAIT_FOR_VALID_PORT = "WAIT_FOR_VALID_PORT";
this.DSM_SUBSCRIBE = "SUBSCRIBE";
this.DSM_WAIT_FOR_FIRST_FRAME = "WAIT_FOR_FIRST_FRAME";
this.DSM_SHOWING = "SHOWING";
this.DSM_RESTART_UNSUBSCRIBE = "UNSUBSCRIBE";
this.DSM_RESTART_WAIT = "WAIT_BEFORE_SUBSCRIBE";
this.dsm_cur_state = this.DSM_DISCONNECTED;
this.dsm_prev_state = this.DSM_DISCONNECTED;
this.dsm_restart_start_time = window.performance.now();
requestAnimationFrame(()=>this.animationLoop());
}
dispImageData(){
//From https://stackoverflow.com/questions/67507616/set-image-src-from-image-blob/67507685#67507685
if(this.imgObjURL != null){
URL.revokeObjectURL(this.imgObjURL)
}
this.imgObjURL = URL.createObjectURL(this.imgData);
//Update the image with the new mimetype and image
this.image.src = this.imgObjURL;
}
dispNoStream() {
this.image.src = require("../assets/loading.gif");
}
animationLoop(){
// Update time metrics
var now = window.performance.now();
var timeInState = now - this.dsm_restart_start_time;
// Save previous state
this.dsm_prev_state = this.dsm_cur_state;
// Evaluate state transitions
if(this.serverConnectionActive == false){
//Any state - if the server connection goes false, always transition to disconnected
this.dsm_cur_state = this.DSM_DISCONNECTED;
} else {
//Conditional transitions
switch(this.dsm_cur_state) {
case this.DSM_DISCONNECTED:
//Immediately transition to waiting for the first frame
this.dsm_cur_state = this.DSM_WAIT_FOR_VALID_PORT;
break;
case this.DSM_WAIT_FOR_VALID_PORT:
// Wait until the user has configured a valid port
if(this.streamPort > 0){
this.dsm_cur_state = this.DSM_SUBSCRIBE;
} else {
this.dsm_cur_state = this.DSM_WAIT_FOR_VALID_PORT;
}
break;
case this.DSM_SUBSCRIBE:
// Immediately transition after subscriptions is sent
this.dsm_cur_state = this.DSM_WAIT_FOR_FIRST_FRAME;
break;
case this.DSM_WAIT_FOR_FIRST_FRAME:
if(this.imgData != null){
//we got some image data, start showing it
this.dsm_cur_state = this.DSM_SHOWING;
} else if (this.newStreamPortReq != null){
//Stream port requested changed, unsubscribe and restart
this.dsm_cur_state = this.DSM_RESTART_UNSUBSCRIBE;
} else {
this.dsm_cur_state = this.DSM_WAIT_FOR_FIRST_FRAME;
}
break;
case this.DSM_SHOWING:
if((now - this.imgDataTime) > 2500){
//timeout, begin the restart sequence
this.dsm_cur_state = this.DSM_RESTART_UNSUBSCRIBE;
} else if (this.newStreamPortReq != null){
//Stream port requested changed, unsubscribe and restart
this.dsm_cur_state = this.DSM_RESTART_UNSUBSCRIBE;
} else {
//stay in this state.
this.dsm_cur_state = this.DSM_SHOWING;
}
break;
case this.DSM_RESTART_UNSUBSCRIBE:
//Only should spend one loop in Unsubscribe, immediately transition
this.dsm_cur_state = this.DSM_RESTART_WAIT;
break;
case this.DSM_RESTART_WAIT:
if (timeInState > 250) {
//we've waited long enough, go to try to re-subscribe
this.dsm_cur_state = this.DSM_WAIT_FOR_VALID_PORT;
} else {
//stay in this state.
this.dsm_cur_state = this.DSM_RESTART_WAIT;
}
break;
default:
// Shouldn't get here, default back to init
this.dsm_cur_state = this.DSM_DISCONNECTED;
}
}
//take current-state or state-transition actions
if(this.dsm_cur_state != this.dsm_prev_state){
//Any state transition
console.log("State Change: " + this.dsm_prev_state + " -> " + this.dsm_cur_state);
}
if(this.dsm_cur_state == this.DSM_SHOWING){
// Currently in SHOWING
this.dispImageData();
}
if(this.dsm_cur_state != this.DSM_SHOWING && this.dsm_prev_state == this.DSM_SHOWING ){
//Any transition out of showing - no stream
this.dispNoStream();
}
if(this.dsm_cur_state == this.DSM_RESTART_UNSUBSCRIBE){
// Currently in UNSUBSCRIBE, do the unsubscribe actions
this.stopStream();
this.dsm_restart_start_time = now;
}
if(this.dsm_cur_state == this.DSM_SUBSCRIBE){
// Currently in SUBSCRIBE, do the subscribe actions
this.startStream();
this.dsm_restart_start_time = now;
}
if(this.dsm_cur_state == this.DSM_WAIT_FOR_VALID_PORT){
// Currently waiting for a vaild port to be requested
if(this.newStreamPortReq != null){
this.streamPort = this.newStreamPortReq;
this.newStreamPortReq = null;
}
}
requestAnimationFrame(()=>this.animationLoop());
}
startStream() {
console.log("Subscribing to port " + this.streamPort);
this.imgData = null;
this.ws.send(JSON.stringify({"cmd": "subscribe", "port":this.streamPort}));
}
stopStream() {
console.log("Unsubscribing");
this.ws.send(JSON.stringify({"cmd": "unsubscribe"}));
this.imgData = null;
}
setPort(streamPort){
console.log("Port set to " + streamPort);
this.newStreamPortReq = streamPort;
}
ws_onOpen() {
// Set the flag allowing general server communication
this.serverConnectionActive = true;
console.log("Camera Websockets Connected!");
}
ws_onClose(e) {
//Clear flags to stop server communication
this.ws = null;
this.serverConnectionActive = false;
console.log('Camera Socket is closed. Reconnect will be attempted in 0.5 second.', e.reason);
setTimeout(this.ws_connect.bind(this), 500);
if(!e.wasClean){
console.error('Socket encountered error!');
}
}
ws_onError(e){
e; //prevent unused failure
this.ws.close();
}
ws_onMessage(e){
console.log("Got message from " + this.serverAddr)
if(typeof e.data === 'string'){
//string data from host
//TODO - anything to receive info here? Maybe "available streams?"
} else {
if(e.data.size > 0){
//binary data - a frame
this.imgData = e.data;
this.imgDataTime = window.performance.now();
this.frameRxCount++;
} else {
//TODO - server is sending empty frames?
}
}
}
ws_connect() {
this.serverConnectionActive = false;
this.ws = new WebSocket(this.serverAddr);
this.ws.binaryType = "blob";
this.ws.onopen = this.ws_onOpen.bind(this);
this.ws.onmessage = this.ws_onMessage.bind(this);
this.ws.onclose = this.ws_onClose.bind(this);
this.ws.onerror = this.ws_onError.bind(this);
console.log("Connecting to server " + this.serverAddr);
}
ws_close(){
this.ws.close();
}
}
export default {WebsocketVideoStream}

View File

@@ -35,8 +35,8 @@ export default new Vuex.Store({
tiltDegrees: 0.0,
currentPipelineIndex: 0,
pipelineNicknames: ["Unknown"],
outputStreamPort: 1181,
inputStreamPort: 1182,
outputStreamPort: 0,
inputStreamPort: 0,
nickname: "Unknown",
videoFormatList: [
{
@@ -97,6 +97,8 @@ export default new Vuex.Store({
debug: false,
refineEdges: true,
numIterations: 1,
decisionMargin: 0,
hammingDist: 0,
}
}
],

View File

@@ -31,14 +31,6 @@
:label-cols="$vuetify.breakpoint.mdAndUp ? undefined : 7"
/>
<br>
<CVnumberinput
v-model="cameraSettings.tiltDegrees"
name="Camera pitch"
tooltip="How many degrees above the horizontal the physical camera is tilted"
:step="0.01"
:label-cols="$vuetify.breakpoint.mdAndUp ? undefined : 7"
/>
<br>
<v-btn
style="margin-top:10px"
small
@@ -202,10 +194,13 @@
>
<CVslider
v-model="$store.getters.currentPipelineSettings.cameraExposure"
:disabled="$store.getters.currentPipelineSettings.cameraAutoExposure"
name="Exposure"
:min="0"
:max="100"
slider-cols="8"
step="0.1"
tooltip="Directly controls how much light is allowed to fall onto the sensor, which affects apparent brightness"
@input="e => handlePipelineUpdate('cameraExposure', e)"
/>
<CVslider
@@ -216,6 +211,13 @@
slider-cols="8"
@input="e => handlePipelineUpdate('cameraBrightness', e)"
/>
<CVswitch
v-model="$store.getters.currentPipelineSettings.cameraAutoExposure"
class="pt-2"
name="Auto Exposure"
tooltip="Enables or Disables camera automatic adjustment for current lighting conditions"
@input="e => handlePipelineUpdate('cameraAutoExposure', e)"
/>
<CVslider
v-if="$store.getters.currentPipelineSettings.cameraRedGain !== -1"
v-model="$store.getters.currentPipelineSettings.cameraRedGain"
@@ -289,7 +291,8 @@
>
<template>
<CVimage
:address="$store.getters.streamAddress[1]"
:id="cameras-cal"
:idx=1
:disconnected="!$store.state.backendConnected"
scale="100"
style="border-radius: 5px;"
@@ -360,6 +363,7 @@
import CVselect from '../components/common/cv-select';
import CVnumberinput from '../components/common/cv-number-input';
import CVslider from '../components/common/cv-slider';
import CVswitch from '../components/common/cv-switch';
import CVimage from "../components/common/cv-image";
import TooltippedLabel from "../components/common/cv-tooltipped-label";
import jsPDF from "jspdf";
@@ -372,6 +376,7 @@ export default {
CVselect,
CVnumberinput,
CVslider,
CVswitch,
CVimage
},
data() {
@@ -428,13 +433,11 @@ export default {
return filtered
}
},
stringResolutionList: {
get() {
return this.filteredResolutionList.map(res => `${res['width']} X ${res['height']}`);
}
},
cameraSettings: {
get() {
return this.$store.getters.currentCameraSettings;
@@ -443,7 +446,6 @@ export default {
this.$store.commit('cameraSettings', value);
}
},
boardType: {
get() {
return this.calibrationData.boardType
@@ -625,8 +627,7 @@ export default {
this.axios.post("http://" + this.$address + "/api/settings/camera", {
"settings": this.cameraSettings,
"index": this.$store.state.currentCameraIndex
}).then(
function (response) {
}).then(response => {
if (response.status === 200) {
this.$store.state.saveBar = true;
}
@@ -647,13 +648,14 @@ export default {
if (this.isCalibrating === true) {
data['takeCalibrationSnapshot'] = true
} else {
// This store prevents an edge case of a user not selecting a different resolution, which causes the set logic to not be called
this.$store.commit('mutateCalibrationState', {['videoModeIndex']: this.filteredResolutionList[this.selectedFilteredResIndex].index});
const calData = this.calibrationData;
calData.isCalibrating = true;
data['startPnpCalibration'] = calData;
console.log("starting calibration with index " + calData.videoModeIndex);
}
this.$store.commit('currentPipelineIndex', -2);
this.$socket.send(this.$msgPack.encode(data));
},
sendCalibrationFinish() {

View File

@@ -58,16 +58,16 @@
>
<div style="position: relative; width: 100%; height: 100%;">
<cv-image
:id="idx === 0 ? 'normal-stream' : ''"
:id="idx === 0 ? 'raw-stream' : 'processed-stream'"
ref="streams"
:address="$store.getters.streamAddress[idx]"
:idx=idx
:disconnected="!$store.state.backendConnected"
scale="100"
:max-height="$store.getters.isDriverMode ? '40vh' : '300px'"
:max-height-md="$store.getters.isDriverMode ? '50vh' : '380px'"
:max-height-lg="$store.getters.isDriverMode ? '55vh' : '390px'"
:max-height-xl="$store.getters.isDriverMode ? '60vh' : '450px'"
:alt="'Stream' + idx"
:alt="'Stream ' + idx"
:color-picking="$store.state.colorPicking && idx === 0"
@click="onImageClick"
/>
@@ -85,7 +85,7 @@
<v-card
color="primary"
>
<camera-and-pipeline-select @camera-name-changed="reloadStreams" />
<camera-and-pipeline-select />
</v-card>
<v-card
:disabled="$store.getters.isDriverMode || $store.state.colorPicking"

View File

@@ -7,16 +7,27 @@
item-color="secondary"
label="Select target family"
:items="familyList"
@input="handlePipelineUpdate('tagFamily', targetList.indexOf(selectedModel))"
@input="handlePipelineUpdate('tagFamily', familyList.indexOf(selectedFamily))"
/>
<v-select
v-model="selectedModel"
dark
color="accent"
item-color="secondary"
label="Select a target model"
:items="targetList"
item-text="name"
item-value="data"
@input="handlePipelineUpdate('targetModel', targetList.indexOf(selectedModel) + 6)"
/>
<CVslider
v-model="decimate"
class="pt-2"
slider-cols="8"
name="Decimate"
min="0"
max="3"
step=".5"
min="1"
max="8"
step="1.0"
tooltip="Increases FPS at the expense of range by reducing image resolution initially"
@input="handlePipelineData('decimate')"
/>
@@ -50,6 +61,28 @@
tooltip="Further refines the apriltag corner position initial estimate, suggested left on"
@input="handlePipelineData('refineEdges')"
/>
<CVslider
v-model="hammingDist"
class="pt-2 pb-4"
slider-cols="8"
name="Max error bits"
min="0"
max="10"
step="1"
tooltip="Maximum number of error bits to correct; potential tags with more will be thrown out. For smaller tags (like 16h5), set this as low as possible."
@input="handlePipelineData('hammingDist')"
/>
<CVslider
v-model="decisionMargin"
class="pt-2 pb-4"
slider-cols="8"
name="Decision Margin Cutoff"
min="0"
max="250"
step="1"
tooltip="Tags with a 'margin' (decoding quality score) less than this wil be rejected. Increase this to reduce the number of false positive detections"
@input="handlePipelineData('decisionMargin')"
/>
<CVslider
v-model="numIterations"
class="pt-2 pb-4"
@@ -76,10 +109,21 @@
},
data() {
return {
familyList: ["tag36h11"],
familyList: ["tag36h11", "tag25h9", "tag16h5"],
// Selected model is offset (ew) by 6 from the photon ordinal, as we only wanna show the 36h11 and 16h5 options
targetList: ['6.5in (36h11) AprilTag', '6in (16h5) AprilTag'], //Keep in sync with TargetModel.java
}
},
computed: {
selectedModel: {
get() {
let ret = this.$store.getters.currentPipelineSettings.targetModel - 6
return this.targetList[ret];
},
set(val) {
this.$store.commit("mutatePipeline", {"targetModel": this.targetList.indexOf(val) + 6})
}
},
selectedFamily: {
get() {
let ret = this.$store.getters.currentPipelineSettings.tagFamily
@@ -97,6 +141,22 @@
this.$store.commit("mutatePipeline", {"decimate": val});
}
},
hammingDist: {
get() {
return this.$store.getters.currentPipelineSettings.hammingDist
},
set(val) {
this.$store.commit("mutatePipeline", {"hammingDist": val});
}
},
decisionMargin: {
get() {
return this.$store.getters.currentPipelineSettings.decisionMargin
},
set(val) {
this.$store.commit("mutatePipeline", {"decisionMargin": val});
}
},
numIterations: {
get() {
return this.$store.getters.currentPipelineSettings.numIterations

View File

@@ -1,13 +1,13 @@
<template>
<div>
<CVslider
:disabled="cameraAutoExposure"
v-model="cameraExposure"
:disabled="cameraAutoExposure"
name="Exposure"
min="0"
max="100"
step="0.1"
tooltip="Directly controls how much light is allowed to fall onto the sensor, which affects brightness"
tooltip="Directly controls how much light is allowed to fall onto the sensor, which affects apparent brightness"
:slider-cols="largeBox"
@input="handlePipelineData('cameraExposure')"
@rollback="e => rollback('cameraExposure', e)"
@@ -25,19 +25,20 @@
<CVswitch
v-model="cameraAutoExposure"
class="pt-2"
name="Auto exposure"
name="Auto Exposure"
tooltip="Enables or Disables camera automatic adjustment for current lighting conditions"
@input="handlePipelineData('cameraAutoExposure')"
/>
<CVslider
v-if="cameraGain >= 0"
v-model="cameraGain"
name="Camera gain"
name="Camera Gain"
min="0"
max="100"
tooltip="Controls camera gain, similar to brightness"
:slider-cols="largeBox"
@input="handlePipelineData('cameraRedGain')"
@rollback="e => rollback('cameraRedGain', e)"
@input="handlePipelineData('cameraGain')"
@rollback="e => rollback('cameraGain', e)"
/>
<CVslider
v-if="cameraRedGain !== -1"
@@ -106,11 +107,6 @@
},
// eslint-disable-next-line vue/require-prop-types
props: ['value'],
data() {
return {
rawStreamDivisorIndex: 0,
}
},
computed: {
largeBox: {
get() {
@@ -144,6 +140,14 @@
this.$store.commit("mutatePipeline", {"cameraBrightness": parseInt(val)});
}
},
cameraGain: {
get() {
return parseInt(this.$store.getters.currentPipelineSettings.cameraGain)
},
set(val) {
this.$store.commit("mutatePipeline", {"cameraGain": parseInt(val)});
}
},
cameraRedGain: {
get() {
return parseInt(this.$store.getters.currentPipelineSettings.cameraRedGain)
@@ -176,15 +180,22 @@
this.$store.commit("mutatePipeline", {"cameraVideoModeIndex": val});
this.handlePipelineUpdate("streamingFrameDivisor", this.getNumSkippedStreamDivisors());
this.rawStreamDivisorIndex = 0;
this.$store.commit("mutatePipeline", {"streamingFrameDivisor": 0});
// If we don't have 3d mode calibrated at the new resolution either, we should disable it here
// (TODO) This probably belongs in the backend (Matt)
if (!this.$store.getters.isCalibrated) {
this.handlePipelineUpdate("solvePNPEnabled", false);
this.$store.commit("mutatePipeline", {"solvePNPEnabled": false});
}
}
},
streamingFrameDivisor: {
get() {
return this.rawStreamDivisorIndex;
return this.$store.getters.currentPipelineSettings.streamingFrameDivisor;
},
set(val) {
this.rawStreamDivisorIndex = val;
this.$store.commit("mutatePipeline", {"streamingFrameDivisor": val});
this.handlePipelineUpdate("streamingFrameDivisor", this.getNumSkippedStreamDivisors() + val);
}
},

View File

@@ -53,7 +53,7 @@
},
data() {
return {
targetList: ['2020 High Goal Outer', '2020 High Goal Inner', '2019 Dual Target', '2020 Power Cell (7in)','2022 Cargo Ball (9.5in)', '2016 High Goal'], //Keep in sync with TargetModel.java
targetList: ['2020 High Goal Outer', '2020 High Goal Inner', '2019 Dual Target', '2020 Power Cell (7in)','2022 Cargo Ball (9.5in)', '2016 High Goal', '6.5in (36h11) AprilTag', '6in (16h5) AprilTag'], //Keep in sync with TargetModel.java
snackbar: {
color: "Success",
text: ""
@@ -65,7 +65,6 @@
selectedModel: {
get() {
let ret = this.$store.getters.currentPipelineSettings.targetModel
console.log(ret)
return this.targetList[ret];
},
set(val) {

View File

@@ -50,7 +50,7 @@
</th>
</template>
<template v-if="$store.getters.pipelineType === 4 && $store.getters.currentPipelineSettings.solvePNPEnabled">
<th class="text-center" >
<th class="text-center">
Ambiguity
</th>
</template>
@@ -82,9 +82,9 @@
<td>{{ (parseFloat(value.pose.angle_z) * 180 / Math.PI).toFixed(2) }}&deg;</td>
</template>
<template v-if="$store.getters.pipelineType === 4 && $store.getters.currentPipelineSettings.solvePNPEnabled">
<td>
{{ parseFloat(value.ambiguity).toFixed(2) }}
</td>
<td>
{{ parseFloat(value.ambiguity).toFixed(2) }}
</td>
</template>
</tr>
</tbody>

View File

@@ -49,22 +49,46 @@
<th class="infoElem">
Disk Usage
</th>
<th class="infoElem">
<v-tooltip top>
<template v-slot:activator="{ on, attrs }">
<span
v-bind="attrs"
v-on="on"
>
CPU Throttling
</span>
</template>
<span>
Current or Previous Reason for the cpu being held back from maximum performance.
</span>
</v-tooltip>
</th>
<th class="infoElem">
CPU Uptime
</th>
</tr>
<tr v-if="metrics.cpuUtil !== 'N/A'">
<td class="infoElem">
{{ metrics.cpuUtil.replace(" ", "") }}%
{{ metrics.cpuUtil }}%
</td>
<td class="infoElem">
{{ parseInt(metrics.cpuTemp) }}&deg;&nbsp;C
</td>
<td class="infoElem">
{{ metrics.ramUtil.replace(" ", "") }}MB of {{ metrics.cpuMem }}MB
{{ metrics.ramUtil }}MB of {{ metrics.cpuMem }}MB
</td>
<td class="infoElem">
{{ metrics.gpuMemUtil.replace(" ", "") }}MB of {{ metrics.gpuMem }}MB
{{ metrics.gpuMemUtil }}MB of {{ metrics.gpuMem }}MB
</td>
<td class="infoElem">
{{ metrics.diskUtilPct.replace(" ", "") }}
{{ metrics.diskUtilPct }}
</td>
<td class="infoElem">
{{ metrics.cpuThr }}
</td>
<td class="infoElem">
{{ metrics.cpuUptime }}
</td>
</tr>
<tr v-if="metrics.cpuUtil === 'N/A'">
@@ -83,6 +107,12 @@
<td class="infoElem">
---
</td>
<td class="infoElem">
---
</td>
<td class="infoElem">
---
</td>
</tr>
</table>
</v-row>

View File

@@ -66,7 +66,16 @@
>
Save
</v-btn>
<v-snackbar
v-model="snack"
top
:color="snackbar.color"
timeout="5000"
>
<span>{{ snackbar.text }}</span>
</v-snackbar>
<v-divider class="mt-4 mb-4" />
<!-- TEMP - RIO finder is not currently enabled
<v-row>
<v-col
cols="12"
@@ -125,6 +134,7 @@
</v-simple-table>
</v-col>
</v-row>
-->
</div>
</template>
@@ -237,7 +247,7 @@ export default {
},
sendGeneralSettings() {
this.axios.post("http://" + this.$address + "/api/settings/general", this.settings).then(
function (response) {
response => {
if (response.status === 200) {
this.snackbar = {
color: "success",
@@ -246,7 +256,7 @@ export default {
this.snack = true;
}
},
function (error) {
error => {
this.snackbar = {
color: "error",
text: (error.response || {data: "Couldn't save settings"}).data

View File

@@ -72,6 +72,7 @@ public class CameraConfiguration {
logger.debug(
"Creating USB camera configuration for "
+ cameraType
+ " "
+ baseName
+ " (AKA "
+ nickname
@@ -101,6 +102,7 @@ public class CameraConfiguration {
logger.debug(
"Creating camera configuration for "
+ cameraType
+ " "
+ baseName
+ " (AKA "
+ nickname

View File

@@ -438,7 +438,7 @@ public class ConfigManager {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
logger.error("Exception waiting for settings semaphor", e);
logger.error("Exception waiting for settings semaphore", e);
}
}
}

View File

@@ -41,6 +41,8 @@ public class HardwareConfig {
public final String cpuTempCommand;
public final String cpuMemoryCommand;
public final String cpuUtilCommand;
public final String cpuThrottleReasonCmd;
public final String cpuUptimeCommand;
public final String gpuMemoryCommand;
public final String ramUtilCommand;
public final String gpuMemUsageCommand;
@@ -65,6 +67,8 @@ public class HardwareConfig {
cpuTempCommand = "";
cpuMemoryCommand = "";
cpuUtilCommand = "";
cpuThrottleReasonCmd = "";
cpuUptimeCommand = "";
gpuMemoryCommand = "";
ramUtilCommand = "";
ledBlinkCommand = "";
@@ -91,6 +95,8 @@ public class HardwareConfig {
String cpuTempCommand,
String cpuMemoryCommand,
String cpuUtilCommand,
String cpuThrottleReasonCmd,
String cpuUptimeCommand,
String gpuMemoryCommand,
String ramUtilCommand,
String gpuMemUsageCommand,
@@ -111,6 +117,8 @@ public class HardwareConfig {
this.cpuTempCommand = cpuTempCommand;
this.cpuMemoryCommand = cpuMemoryCommand;
this.cpuUtilCommand = cpuUtilCommand;
this.cpuThrottleReasonCmd = cpuThrottleReasonCmd;
this.cpuUptimeCommand = cpuUptimeCommand;
this.gpuMemoryCommand = gpuMemoryCommand;
this.ramUtilCommand = ramUtilCommand;
this.gpuMemUsageCommand = gpuMemUsageCommand;

View File

@@ -189,7 +189,7 @@ public class NTDataPublisher implements CVPipelineResultConsumer {
targetAreaEntry.forceSetDouble(bestTarget.getArea());
targetSkewEntry.forceSetDouble(bestTarget.getSkew());
var pose = bestTarget.getCameraToTarget3d();
var pose = bestTarget.getBestCameraToTarget3d();
targetPoseEntry.forceSetDoubleArray(
new double[] {
pose.getTranslation().getX(),
@@ -232,7 +232,8 @@ public class NTDataPublisher implements CVPipelineResultConsumer {
t.getArea(),
t.getSkew(),
t.getFiducialId(),
t.getCameraToTarget3d(),
t.getBestCameraToTarget3d(),
t.getAltCameraToTarget3d(),
t.getPoseAmbiguity(),
cornerList));
}

View File

@@ -23,6 +23,7 @@ import edu.wpi.first.networktables.NetworkTableInstance;
import java.util.HashMap;
import java.util.function.Consumer;
import org.photonvision.PhotonVersion;
import org.photonvision.common.configuration.ConfigManager;
import org.photonvision.common.configuration.NetworkConfig;
import org.photonvision.common.dataflow.DataChangeService;
import org.photonvision.common.dataflow.events.OutgoingUIEvent;
@@ -37,8 +38,11 @@ public class NetworkTablesManager {
private final String kRootTableName = "/photonvision";
public final NetworkTable kRootTable = ntInstance.getTable(kRootTableName);
private boolean isRetryingConnection = false;
private NetworkTablesManager() {
ntInstance.addLogger(new NTLogger(), 0, 255); // to hide error messages
TimedTaskManager.getInstance().addTask("NTManager", this::ntTick, 5000);
}
private static NetworkTablesManager INSTANCE;
@@ -109,17 +113,11 @@ public class NetworkTablesManager {
}
private void setClientMode(int teamNumber) {
logger.info("Starting NT Client");
if (!isRetryingConnection) logger.info("Starting NT Client");
ntInstance.stopServer();
ntInstance.startClientTeam(teamNumber);
ntInstance.startDSClient();
if (ntInstance.isConnected()) {
logger.info("[NetworkTablesManager] Connected to the robot!");
} else {
logger.error(
"[NetworkTablesManager] Could not connect to the robot! Will retry in the background...");
}
broadcastVersion();
}
@@ -129,4 +127,22 @@ public class NetworkTablesManager {
ntInstance.startServer();
broadcastVersion();
}
// So it seems like if Photon starts before the robot NT server does, and both aren't static IP,
// it'll never connect. This hack works around it by restarting the client/server while the nt
// instance
// isn't connected, same as clicking the save button in the settings menu (or restarting the
// service)
private void ntTick() {
if (!ntInstance.isConnected()
&& !ConfigManager.getInstance().getConfig().getNetworkConfig().runNTServer) {
setConfig(ConfigManager.getInstance().getConfig().getNetworkConfig());
}
if (!ntInstance.isConnected() && !isRetryingConnection) {
isRetryingConnection = true;
logger.error(
"[NetworkTablesManager] Could not connect to the robot! Will retry in the background...");
}
}
}

View File

@@ -85,6 +85,8 @@ public class VisionLED {
pigpioSocket.generateAndSendWaveform(pulseLengthMillis, blinkCount, ledPins);
} catch (PigpioException e) {
logger.error("Failed to blink!", e);
} catch (NullPointerException e) {
logger.error("Failed to blink, pigpio internal issue!", e);
}
} else {
for (GPIOBase led : visionLEDs) {
@@ -100,13 +102,19 @@ public class VisionLED {
pigpioSocket.waveTxStop();
} catch (PigpioException e) {
logger.error("Failed to stop blink!", e);
} catch (NullPointerException e) {
logger.error("Failed to blink, pigpio internal issue!", e);
}
}
// if the user has set an LED brightness other than 100%, use that instead
if (mappedBrightnessPercentage == 100 || !state) {
visionLEDs.forEach((led) -> led.setState(state));
} else {
visionLEDs.forEach((led) -> led.setBrightness(mappedBrightnessPercentage));
try {
// if the user has set an LED brightness other than 100%, use that instead
if (mappedBrightnessPercentage == 100 || !state) {
visionLEDs.forEach((led) -> led.setState(state));
} else {
visionLEDs.forEach((led) -> led.setBrightness(mappedBrightnessPercentage));
}
} catch (NullPointerException e) {
logger.error("Failed to blink, pigpio internal issue!", e);
}
}

View File

@@ -40,4 +40,12 @@ public class CPUMetrics extends MetricsBase {
public String getUtilization() {
return execute(cpuUtilizationCommand);
}
public String getUptime() {
return execute(cpuUptimeCommand);
}
public String getThrottleReason() {
return execute(cpuThrottleReasonCmd);
}
}

View File

@@ -26,7 +26,7 @@ import org.photonvision.common.logging.Logger;
import org.photonvision.common.util.ShellExec;
public abstract class MetricsBase {
private static final Logger logger = new Logger(MetricsBase.class, LogGroup.General);
static final Logger logger = new Logger(MetricsBase.class, LogGroup.General);
// CPU
public static String cpuMemoryCommand = "vcgencmd get_mem arm | grep -Eo '[0-9]+'";
public static String cpuTemperatureCommand =
@@ -34,6 +34,15 @@ public abstract class MetricsBase {
public static String cpuUtilizationCommand =
"top -bn1 | grep \"Cpu(s)\" | sed \"s/.*, *\\([0-9.]*\\)%* id.*/\\1/\" | awk '{print 100 - $1}'";
public static String cpuThrottleReasonCmd =
"if (( $(( $(vcgencmd get_throttled | grep -Eo 0x[0-9a-fA-F]*) & 0x01 )) != 0x00 )); then echo \"LOW VOLTAGE\"; "
+ "elif (( $(( $(vcgencmd get_throttled | grep -Eo 0x[0-9a-fA-F]*) & 0x08 )) != 0x00 )); then echo \"HIGH TEMP\"; "
+ "elif (( $(( $(vcgencmd get_throttled | grep -Eo 0x[0-9a-fA-F]*) & 0x10000 )) != 0x00 )); then echo \"Prev. Low Voltage\"; "
+ "elif (( $(( $(vcgencmd get_throttled | grep -Eo 0x[0-9a-fA-F]*) & 0x80000 )) != 0x00 )); then echo \"Prev. High Temp\"; "
+ " else echo \"None\"; fi";
public static String cpuUptimeCommand = "uptime -p | cut -c 4-";
// GPU
public static String gpuMemoryCommand = "vcgencmd get_mem gpu | grep -Eo '[0-9]+'";
public static String gpuMemUsageCommand = "vcgencmd get_mem malloc | grep -Eo '[0-9]+'";
@@ -51,6 +60,8 @@ public abstract class MetricsBase {
cpuMemoryCommand = config.cpuMemoryCommand;
cpuTemperatureCommand = config.cpuTempCommand;
cpuUtilizationCommand = config.cpuUtilCommand;
cpuThrottleReasonCmd = config.cpuThrottleReasonCmd;
cpuUptimeCommand = config.cpuUptimeCommand;
gpuMemoryCommand = config.gpuMemoryCommand;
gpuMemUsageCommand = config.gpuMemUsageCommand;

View File

@@ -60,6 +60,8 @@ public class MetricsPublisher {
metrics.put("cpuTemp", cpuMetrics.getTemp());
metrics.put("cpuUtil", cpuMetrics.getUtilization());
metrics.put("cpuMem", cpuMetrics.getMemory());
metrics.put("cpuThr", cpuMetrics.getThrottleReason());
metrics.put("cpuUptime", cpuMetrics.getUptime());
metrics.put("gpuMem", gpuMetrics.getGPUMemorySplit());
metrics.put("ramUtil", ramMetrics.getUsedRam());
metrics.put("gpuMemUtil", gpuMetrics.getMallocedMemory());

View File

@@ -194,7 +194,7 @@ public class TestUtils {
public static Path getTestMode2020ImagePath() {
return getResourcesFolderPath(true)
.resolve("testimages")
.resolve(WPI2020Image.kBlueGoal_108in_Center.path);
.resolve(WPI2020Image.kBlueGoal_156in_Left.path);
}
public static Path getTestMode2022ImagePath() {

View File

@@ -18,6 +18,7 @@
package org.photonvision.common.util.math;
import edu.wpi.first.math.MatBuilder;
import edu.wpi.first.math.Matrix;
import edu.wpi.first.math.Nat;
import edu.wpi.first.math.VecBuilder;
import edu.wpi.first.math.geometry.CoordinateSystem;
@@ -25,10 +26,14 @@ import edu.wpi.first.math.geometry.Pose3d;
import edu.wpi.first.math.geometry.Quaternion;
import edu.wpi.first.math.geometry.Rotation3d;
import edu.wpi.first.math.geometry.Transform3d;
import edu.wpi.first.math.numbers.N3;
import edu.wpi.first.math.util.Units;
import edu.wpi.first.util.WPIUtilJNI;
import java.util.Arrays;
import java.util.List;
import org.ejml.data.DMatrixRMaj;
import org.ejml.dense.row.factory.DecompositionFactory_DDRM;
import org.ejml.simple.SimpleMatrix;
import org.opencv.core.Mat;
public class MathUtils {
@@ -159,11 +164,21 @@ public class MathUtils {
pose.getTranslation().rotateBy(rotationQuat), pose.getRotation().rotateBy(rotationQuat));
}
// TODO: Refactor into new pipe?
/**
* All our solvepnp code returns a tag with X left, Y up, and Z out of the tag To better match
* wpilib, we want to apply another rotation so that we get Z up, X out of the tag, and Y to the
* right. We apply the following change of basis: X -> Y Y -> Z Z -> X
*/
private static final Rotation3d WPILIB_BASE_ROTATION =
new Rotation3d(new MatBuilder<>(Nat.N3(), Nat.N3()).fill(0, 1, 0, 0, 0, 1, 1, 0, 0));
public static Pose3d convertOpenCVtoPhotonPose(Transform3d cameraToTarget3d) {
// TODO: Refactor into new pipe?
// CameraToTarget _should_ be in opencv-land EDN
return CoordinateSystem.convert(
new Pose3d(cameraToTarget3d), CoordinateSystem.EDN(), CoordinateSystem.NWU());
var nwu =
CoordinateSystem.convert(
new Pose3d(cameraToTarget3d), CoordinateSystem.EDN(), CoordinateSystem.NWU());
return new Pose3d(nwu.getTranslation(), WPILIB_BASE_ROTATION.rotateBy(nwu.getRotation()));
}
/*
@@ -191,4 +206,35 @@ public class MathUtils {
var axis = rotation.getAxis().times(angle);
rvecOutput.put(0, 0, axis.getData());
}
/**
* Orthogonalize an input matrix using a QR decomposition. QR decompositions decompose a
* rectangular matrix 'A' such that 'A=QR', where Q is the closest orthogonal matrix to the input,
* and R is an upper triangular matrix.
*/
public static Matrix<N3, N3> orthogonalizeRotationMatrix(Matrix<N3, N3> input) {
var a = DecompositionFactory_DDRM.qr(3, 3);
if (!a.decompose(input.getStorage().getDDRM())) {
// best we can do is return the input
return input;
}
// Grab results (thanks for this _great_ api, EJML)
var Q = new DMatrixRMaj(3, 3);
var R = new DMatrixRMaj(3, 3);
a.getQ(Q, false);
a.getR(R, false);
// Fix signs in R if they're < 0 so it's close to an identity matrix
// (our QR decomposition implementation sometimes flips the signs of columns)
for (int colR = 0; colR < 3; ++colR) {
if (R.get(colR, colR) < 0) {
for (int rowQ = 0; rowQ < 3; ++rowQ) {
Q.set(rowQ, colR, -Q.get(rowQ, colR));
}
}
}
return new Matrix<>(new SimpleMatrix(Q));
}
}

View File

@@ -0,0 +1,25 @@
/*
Copyright (c) {year} Photon Vision. All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
* Neither the name of FIRST, WPILib, nor the names of other WPILib
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY FIRST AND OTHER WPILIB CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY NONINFRINGEMENT AND FITNESS FOR A PARTICULAR
PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL FIRST OR CONTRIBUTORS BE LIABLE FOR
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/

View File

@@ -1,19 +1,28 @@
/*
* Copyright (C) Photon Vision.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
Copyright (c) 2022 Photon Vision. All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
* Neither the name of FIRST, WPILib, nor the names of other WPILib
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY FIRST AND OTHER WPILIB CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY NONINFRINGEMENT AND FITNESS FOR A PARTICULAR
PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL FIRST OR CONTRIBUTORS BE LIABLE FOR
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package org.photonvision.vision.apriltag;

View File

@@ -1,19 +1,28 @@
/*
* Copyright (C) Photon Vision.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
Copyright (c) 2022 Photon Vision. All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
* Neither the name of FIRST, WPILib, nor the names of other WPILib
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY FIRST AND OTHER WPILIB CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY NONINFRINGEMENT AND FITNESS FOR A PARTICULAR
PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL FIRST OR CONTRIBUTORS BE LIABLE FOR
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package org.photonvision.vision.apriltag;

View File

@@ -1,19 +1,28 @@
/*
* Copyright (C) Photon Vision.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
Copyright (c) 2022 Photon Vision. All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
* Neither the name of FIRST, WPILib, nor the names of other WPILib
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY FIRST AND OTHER WPILIB CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY NONINFRINGEMENT AND FITNESS FOR A PARTICULAR
PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL FIRST OR CONTRIBUTORS BE LIABLE FOR
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package org.photonvision.vision.apriltag;

View File

@@ -1,19 +1,28 @@
/*
* Copyright (C) Photon Vision.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
Copyright (c) 2022 Photon Vision. All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
* Neither the name of FIRST, WPILib, nor the names of other WPILib
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY FIRST AND OTHER WPILIB CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY NONINFRINGEMENT AND FITNESS FOR A PARTICULAR
PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL FIRST OR CONTRIBUTORS BE LIABLE FOR
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package org.photonvision.vision.apriltag;

View File

@@ -1,19 +1,28 @@
/*
* Copyright (C) Photon Vision.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
Copyright (c) 2022 Photon Vision. All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
* Neither the name of FIRST, WPILib, nor the names of other WPILib
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY FIRST AND OTHER WPILIB CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY NONINFRINGEMENT AND FITNESS FOR A PARTICULAR
PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL FIRST OR CONTRIBUTORS BE LIABLE FOR
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package org.photonvision.vision.apriltag;
@@ -24,6 +33,8 @@ import edu.wpi.first.math.geometry.Transform3d;
import edu.wpi.first.math.geometry.Translation3d;
import java.util.Arrays;
import org.photonvision.common.util.math.MathUtils;
public class DetectionResult {
public int getId() {
return id;
@@ -127,12 +138,12 @@ public class DetectionResult {
this.poseResult1 =
new Transform3d(
new Translation3d(pose1TransArr[0], pose1TransArr[1], pose1TransArr[2]),
new Rotation3d(new MatBuilder<>(Nat.N3(), Nat.N3()).fill(pose1RotArr)));
new Rotation3d(MathUtils.orthogonalizeRotationMatrix(new MatBuilder<>(Nat.N3(), Nat.N3()).fill(pose1RotArr))));
this.error2 = err2;
this.poseResult2 =
new Transform3d(
new Translation3d(pose2TransArr[0], pose2TransArr[1], pose2TransArr[2]),
new Rotation3d(new MatBuilder<>(Nat.N3(), Nat.N3()).fill(pose2RotArr)));
new Rotation3d(MathUtils.orthogonalizeRotationMatrix(new MatBuilder<>(Nat.N3(), Nat.N3()).fill(pose2RotArr))));
}
/**

View File

@@ -29,6 +29,17 @@ public class QuirkyCamera {
0x5A3,
CameraQuirk.CompletelyBroken), // Chris's older generic "Logitec HD Webcam"
new QuirkyCamera(0x825, 0x46D, CameraQuirk.CompletelyBroken), // Logitec C270
new QuirkyCamera(
0x0bda,
0x5510,
CameraQuirk.CompletelyBroken), // A laptop internal camera someone found broken
new QuirkyCamera(
-1, -1, "Snap Camera", CameraQuirk.CompletelyBroken), // SnapCamera on Windows
new QuirkyCamera(
-1,
-1,
"FaceTime HD Camera",
CameraQuirk.CompletelyBroken), // Mac Facetime Camera shared into Windows in Bootcamp
new QuirkyCamera(0x2000, 0x1415, CameraQuirk.Gain, CameraQuirk.FPSCap100), // PS3Eye
new QuirkyCamera(
-1, -1, "mmal service 16.1", CameraQuirk.PiCam), // PiCam (via V4L2, not zerocopy)

View File

@@ -65,7 +65,12 @@ public class USBCameraSource extends VisionSource {
disableAutoFocus();
usbCameraSettables = new USBCameraSettables(config);
usbFrameProvider = new USBFrameProvider(cvSink, usbCameraSettables);
if (usbCameraSettables.getAllVideoModes().isEmpty()) {
logger.info("Camera " + camera.getPath() + " has no video modes supported by PhotonVision");
usbFrameProvider = null;
} else {
usbFrameProvider = new USBFrameProvider(cvSink, usbCameraSettables);
}
}
}

View File

@@ -118,31 +118,21 @@ public class Draw3dTargetsPipe
ColorHelper.colorToScalar(Color.green),
3);
}
for (int i = 0; i < bottomPoints.size(); i++) {
Imgproc.line(
in.getLeft(),
bottomPoints.get(i),
topPoints.get(i),
ColorHelper.colorToScalar(Color.blue),
3);
}
for (int i = 0; i < topPoints.size(); i++) {
Imgproc.line(
in.getLeft(),
topPoints.get(i),
topPoints.get((i + 1) % (bottomPoints.size())),
ColorHelper.colorToScalar(Color.orange),
3);
}
// Draw X, Y and Z axis
MatOfPoint3f pointMat = new MatOfPoint3f();
// Those points are in opencv-land, but we are in NWU
// NWU | EDN
// X: Z
// Y: -X
// Z: -Y
final double AXIS_LEN = 0.2;
var list =
List.of(
new Point3(0, 0, 0),
new Point3(0.2, 0, 0),
new Point3(0, 0.2, 0),
new Point3(0, 0, 0.2));
new Point3(0, 0, AXIS_LEN),
new Point3(AXIS_LEN, 0, 0),
new Point3(0, AXIS_LEN, 0));
pointMat.fromList(list);
Calib3d.projectPoints(
@@ -157,12 +147,6 @@ public class Draw3dTargetsPipe
dividePointList(axisPoints);
// Red = x, green y, blue z
Imgproc.line(
in.getLeft(),
axisPoints.get(0),
axisPoints.get(1),
ColorHelper.colorToScalar(Color.RED),
3);
Imgproc.line(
in.getLeft(),
axisPoints.get(0),
@@ -175,6 +159,29 @@ public class Draw3dTargetsPipe
axisPoints.get(3),
ColorHelper.colorToScalar(Color.BLUE),
3);
Imgproc.line(
in.getLeft(),
axisPoints.get(0),
axisPoints.get(1),
ColorHelper.colorToScalar(Color.RED),
3);
for (int i = 0; i < bottomPoints.size(); i++) {
Imgproc.line(
in.getLeft(),
bottomPoints.get(i),
topPoints.get(i),
ColorHelper.colorToScalar(Color.blue),
3);
}
for (int i = 0; i < topPoints.size(); i++) {
Imgproc.line(
in.getLeft(),
topPoints.get(i),
topPoints.get((i + 1) % (bottomPoints.size())),
ColorHelper.colorToScalar(Color.orange),
3);
}
tempMat.release();
jac.release();

View File

@@ -100,8 +100,9 @@ public class SolvePNPPipe
Core.norm(rVec));
Pose3d targetPose = MathUtils.convertOpenCVtoPhotonPose(new Transform3d(translation, rotation));
target.setCameraToTarget3d(
target.setBestCameraToTarget3d(
new Transform3d(targetPose.getTranslation(), targetPose.getRotation()));
target.setAltCameraToTarget3d(new Transform3d());
}
Mat rotationMatrix = new Mat();

View File

@@ -79,15 +79,25 @@ public class AprilTagPipeline extends CVPipeline<CVPipelineResult, AprilTagPipel
// target model for the draw 3d targets pipeline to work...
// for now, hard code tag width based on enum value
double tagWidth = 0.16; // guess at 200mm??
double tagWidth;
// This needs
switch (settings.targetModel) {
case k200mmAprilTag:
{
tagWidth = Units.inchesToMeters(3.25 * 2);
break;
}
case k6in_16h5:
{
tagWidth = Units.inchesToMeters(3 * 2);
break;
}
default:
{
// guess at 200mm?? If it's zero everything breaks, but it should _never_ be zero. Unless
// users select the wrong model...
tagWidth = 0.16;
break;
}
}
@@ -135,6 +145,10 @@ public class AprilTagPipeline extends CVPipeline<CVPipelineResult, AprilTagPipel
targetList = new ArrayList<>();
for (DetectionResult detection : tagDetectionPipeResult.output) {
// TODO this should be in a pipe, not in the top level here (Matt)
if (detection.getDecisionMargin() < settings.decisionMargin) continue;
if (detection.getHamming() > settings.hammingDist) continue;
// populate the target list
// Challenge here is that TrackedTarget functions with OpenCV Contour
TrackedTarget target =
@@ -143,9 +157,13 @@ public class AprilTagPipeline extends CVPipeline<CVPipelineResult, AprilTagPipel
new TargetCalculationParameters(
false, null, null, null, null, frameStaticProperties));
var correctedPose = MathUtils.convertOpenCVtoPhotonPose(target.getCameraToTarget3d());
target.setCameraToTarget3d(
new Transform3d(correctedPose.getTranslation(), correctedPose.getRotation()));
var correctedBestPose = MathUtils.convertOpenCVtoPhotonPose(target.getBestCameraToTarget3d());
var correctedAltPose = MathUtils.convertOpenCVtoPhotonPose(target.getAltCameraToTarget3d());
target.setBestCameraToTarget3d(
new Transform3d(correctedBestPose.getTranslation(), correctedBestPose.getRotation()));
target.setAltCameraToTarget3d(
new Transform3d(correctedAltPose.getTranslation(), correctedAltPose.getRotation()));
targetList.add(target);
}

View File

@@ -32,6 +32,10 @@ public class AprilTagPipelineSettings extends AdvancedPipelineSettings {
public boolean refineEdges = true;
public int numIterations = 200;
// TODO is this a legit, reasonable default?
public int hammingDist = 1;
public int decisionMargin = 30;
// 3d settings
public AprilTagPipelineSettings() {

View File

@@ -181,17 +181,17 @@ public class PipelineManager {
var desiredPipelineSettings = userPipelineSettings.get(currentPipelineIndex);
switch (desiredPipelineSettings.pipelineType) {
case Reflective:
logger.debug("Creatig Reflective pipeline");
logger.debug("Creating Reflective pipeline");
currentUserPipeline =
new ReflectivePipeline((ReflectivePipelineSettings) desiredPipelineSettings);
break;
case ColoredShape:
logger.debug("Creatig ColoredShape pipeline");
logger.debug("Creating ColoredShape pipeline");
currentUserPipeline =
new ColoredShapePipeline((ColoredShapePipelineSettings) desiredPipelineSettings);
break;
case AprilTag:
logger.debug("Creatig AprilTag pipeline");
logger.debug("Creating AprilTag pipeline");
currentUserPipeline =
new AprilTagPipeline((AprilTagPipelineSettings) desiredPipelineSettings);
break;

View File

@@ -17,7 +17,7 @@
package org.photonvision.vision.processes;
import edu.wpi.first.math.geometry.Rotation2d;
import edu.wpi.first.cscore.VideoException;
import edu.wpi.first.math.util.Units;
import io.javalin.websocket.WsContext;
import java.util.*;
@@ -41,7 +41,6 @@ import org.photonvision.vision.camera.USBCameraSource;
import org.photonvision.vision.camera.ZeroCopyPicamSource;
import org.photonvision.vision.frame.Frame;
import org.photonvision.vision.frame.consumer.FileSaveFrameConsumer;
import org.photonvision.vision.frame.consumer.MJPGFrameConsumer;
import org.photonvision.vision.pipeline.AdvancedPipelineSettings;
import org.photonvision.vision.pipeline.OutputStreamPipeline;
import org.photonvision.vision.pipeline.ReflectivePipelineSettings;
@@ -49,6 +48,8 @@ import org.photonvision.vision.pipeline.UICalibrationData;
import org.photonvision.vision.pipeline.result.CVPipelineResult;
import org.photonvision.vision.target.TargetModel;
import org.photonvision.vision.target.TrackedTarget;
import org.photonvision.vision.videoStream.SocketVideoStream;
import org.photonvision.vision.videoStream.SocketVideoStreamManager;
/**
* This is the God Class
@@ -57,32 +58,31 @@ import org.photonvision.vision.target.TrackedTarget;
* provide info on settings changes. VisionModuleManager holds a list of all current vision modules.
*/
public class VisionModule {
private static final int streamFPSCap = 30;
private final Logger logger;
protected final PipelineManager pipelineManager;
protected final VisionSource visionSource;
private final VisionRunner visionRunner;
private final StreamRunnable streamRunnable;
private final LinkedList<CVPipelineResultConsumer> resultConsumers = new LinkedList<>();
private final LinkedList<CVPipelineResultConsumer> fpsLimitedResultConsumers = new LinkedList<>();
// Raw result consumers run before any drawing has been done by the OutputStreamPipeline
private final LinkedList<TriConsumer<Frame, Frame, List<TrackedTarget>>> rawResultConsumers =
private final LinkedList<TriConsumer<Frame, Frame, List<TrackedTarget>>> streamResultConsumers =
new LinkedList<>();
private final NTDataPublisher ntConsumer;
private final UIDataPublisher uiDataConsumer;
protected final int moduleIndex;
protected final QuirkyCamera cameraQuirks;
private long lastFrameConsumeMillis;
protected TrackedTarget lastPipelineResultBestTarget;
MJPGFrameConsumer dashboardInputStreamer;
MJPGFrameConsumer dashboardOutputStreamer;
private int inputStreamPort = -1;
private int outputStreamPort = -1;
FileSaveFrameConsumer inputFrameSaver;
FileSaveFrameConsumer outputFrameSaver;
SocketVideoStream inputVideoStreamer;
SocketVideoStream outputVideoStreamer;
public VisionModule(PipelineManager pipelineManager, VisionSource visionSource, int index) {
logger =
new Logger(
@@ -130,7 +130,7 @@ public class VisionModule {
createStreams();
recreateFpsLimitedResultConsumers();
recreateStreamResultConsumers();
ntConsumer =
new NTDataPublisher(
@@ -167,49 +167,33 @@ public class VisionModule {
}
private void destroyStreams() {
dashboardInputStreamer.close();
dashboardOutputStreamer.close();
SocketVideoStreamManager.getInstance().removeStream(inputVideoStreamer);
SocketVideoStreamManager.getInstance().removeStream(outputVideoStreamer);
}
private void createStreams() {
var camStreamIdx = visionSource.getSettables().getConfiguration().streamIndex;
// If idx = 0, we want (1181, 1182)
var inputStreamPort = 1181 + (camStreamIdx * 2);
var outputStreamPort = 1181 + (camStreamIdx * 2) + 1;
dashboardOutputStreamer =
new MJPGFrameConsumer(
visionSource.getSettables().getConfiguration().nickname + "-output", outputStreamPort);
dashboardInputStreamer =
new MJPGFrameConsumer(
visionSource.getSettables().getConfiguration().uniqueName + "-input", inputStreamPort);
this.inputStreamPort = 1181 + (camStreamIdx * 2);
this.outputStreamPort = 1181 + (camStreamIdx * 2) + 1;
inputFrameSaver =
new FileSaveFrameConsumer(visionSource.getSettables().getConfiguration().nickname, "input");
outputFrameSaver =
new FileSaveFrameConsumer(
visionSource.getSettables().getConfiguration().nickname, "output");
inputVideoStreamer = new SocketVideoStream(this.inputStreamPort);
outputVideoStreamer = new SocketVideoStream(this.outputStreamPort);
SocketVideoStreamManager.getInstance().addStream(inputVideoStreamer);
SocketVideoStreamManager.getInstance().addStream(outputVideoStreamer);
}
private void recreateFpsLimitedResultConsumers() {
// Important! These must come before the stream result consumers because the stream result
// consumers release the frame
rawResultConsumers.add((in, out, tgts) -> inputFrameSaver.accept(in));
fpsLimitedResultConsumers.add(result -> outputFrameSaver.accept(result.outputFrame));
fpsLimitedResultConsumers.add(
result -> {
if (this.pipelineManager.getCurrentPipelineSettings().inputShouldShow)
dashboardInputStreamer.accept(result.inputFrame);
else dashboardInputStreamer.disabledTick();
});
fpsLimitedResultConsumers.add(
result -> {
if (this.pipelineManager.getCurrentPipelineSettings().outputShouldShow)
dashboardOutputStreamer.accept(result.outputFrame);
else dashboardInputStreamer.disabledTick();
;
});
private void recreateStreamResultConsumers() {
streamResultConsumers.add((in, out, tgts) -> inputFrameSaver.accept(in));
streamResultConsumers.add((in, out, tgts) -> outputFrameSaver.accept(out));
streamResultConsumers.add((in, out, tgts) -> inputVideoStreamer.accept(in));
streamResultConsumers.add((in, out, tgts) -> outputVideoStreamer.accept(out));
}
private class StreamRunnable extends Thread {
@@ -271,12 +255,11 @@ public class VisionModule {
this.shouldRun = false;
}
if (shouldRun) {
consumeRawResults(inputFrame, outputFrame, targets);
try {
CVPipelineResult osr =
outputStreamPipeline.process(inputFrame, outputFrame, settings, targets);
consumeResults(inputFrame, osr.outputFrame, targets);
consumeFpsLimitedResult(osr);
} catch (Exception e) {
// Never die
logger.error("Exception while running stream runnable!", e);
@@ -304,7 +287,7 @@ public class VisionModule {
streamRunnable.start();
}
public void setFovAndPitch(double fov, Rotation2d pitch) {
public void setFov(double fov) {
var settables = visionSource.getSettables();
logger.trace(() -> "Setting " + settables.getConfiguration().nickname + ") FOV (" + fov + ")");
@@ -406,8 +389,12 @@ public class VisionModule {
}
visionSource.getSettables().setExposure(pipelineSettings.cameraExposure);
visionSource.getSettables().setAutoExposure(pipelineSettings.cameraAutoExposure);
try {
visionSource.getSettables().setAutoExposure(pipelineSettings.cameraAutoExposure);
} catch (VideoException e) {
logger.error("Unable to set camera auto exposure!");
logger.error(e.toString());
}
if (cameraQuirks.hasQuirk(CameraQuirk.Gain)) {
// If the gain is disabled for some reason, re-enable it
if (pipelineSettings.cameraGain == -1) pipelineSettings.cameraGain = 20;
@@ -474,14 +461,14 @@ public class VisionModule {
outputFrameSaver.updateCameraNickname(newName);
// Rename streams
fpsLimitedResultConsumers.clear();
streamResultConsumers.clear();
// Teardown and recreate streams
destroyStreams();
createStreams();
// Rebuild streamers
recreateFpsLimitedResultConsumers();
recreateStreamResultConsumers();
// Push new data to the UI
saveAndBroadcastAll();
@@ -516,8 +503,8 @@ public class VisionModule {
temp.put(k, internalMap);
}
ret.videoFormatList = temp;
ret.outputStreamPort = dashboardOutputStreamer.getCurrentStreamPort();
ret.inputStreamPort = dashboardInputStreamer.getCurrentStreamPort();
ret.outputStreamPort = this.outputStreamPort;
ret.inputStreamPort = this.inputStreamPort;
var calList = new ArrayList<HashMap<String, Object>>();
for (var c : visionSource.getSettables().getConfiguration().calibrations) {
@@ -567,7 +554,7 @@ public class VisionModule {
result.targets);
// The streamRunnable manages releasing in this case
} else {
consumeFpsLimitedResult(result);
consumeResults(result.inputFrame, result.outputFrame, result.targets);
result.release();
// In this case we don't bother with a separate streaming thread and we release
@@ -580,19 +567,9 @@ public class VisionModule {
}
}
private void consumeFpsLimitedResult(CVPipelineResult result) {
long dt = System.currentTimeMillis() - lastFrameConsumeMillis;
if (dt > 1000 / streamFPSCap) {
for (var c : fpsLimitedResultConsumers) {
c.accept(result);
}
lastFrameConsumeMillis = System.currentTimeMillis();
}
}
/** Consume results prior to drawing on them. */
private void consumeRawResults(Frame inputFrame, Frame outputFrame, List<TrackedTarget> targets) {
for (var c : rawResultConsumers) {
/** Consume stream/target results, no rate limiting applied */
private void consumeResults(Frame inputFrame, Frame outputFrame, List<TrackedTarget> targets) {
for (var c : streamResultConsumers) {
c.accept(inputFrame, outputFrame, targets);
}
}

View File

@@ -318,7 +318,8 @@ public class VisionSourceManager {
var newCam = new USBCameraSource(configuration);
if (!newCam.cameraQuirks.hasQuirk(CameraQuirk.CompletelyBroken)) {
if (!newCam.cameraQuirks.hasQuirk(CameraQuirk.CompletelyBroken)
&& !newCam.getSettables().videoModes.isEmpty()) {
cameraSources.add(newCam);
}
}

View File

@@ -108,7 +108,15 @@ public enum TargetModel implements Releasable {
new Point3(Units.inchesToMeters(3.25), Units.inchesToMeters(3.25), 0),
new Point3(Units.inchesToMeters(3.25), -Units.inchesToMeters(3.25), 0),
new Point3(-Units.inchesToMeters(3.25), -Units.inchesToMeters(3.25), 0)),
Units.inchesToMeters(3.25 * 2));
Units.inchesToMeters(3.25 * 2)),
k6in_16h5( // Nominal edge length of 200 mm includes the white border, but solvePNP corners
// do not
List.of(
new Point3(-Units.inchesToMeters(3), Units.inchesToMeters(3), 0),
new Point3(Units.inchesToMeters(3), Units.inchesToMeters(3), 0),
new Point3(Units.inchesToMeters(3), -Units.inchesToMeters(3), 0),
new Point3(-Units.inchesToMeters(3), -Units.inchesToMeters(3), 0)),
Units.inchesToMeters(3 * 2));
@JsonIgnore private MatOfPoint3f realWorldTargetCoordinates;
@JsonIgnore private MatOfPoint3f visualizationBoxBottom = new MatOfPoint3f();

View File

@@ -46,7 +46,8 @@ public class TrackedTarget implements Releasable {
private double m_area;
private double m_skew;
private Transform3d m_cameraToTarget3d = new Transform3d();
private Transform3d m_bestCameraToTarget3d = new Transform3d();
private Transform3d m_altCameraToTarget3d = new Transform3d();
private CVShape m_shape;
@@ -74,15 +75,20 @@ public class TrackedTarget implements Releasable {
TargetCalculations.calculateYaw(
result.getCenterX(), params.cameraCenterPoint.x, params.horizontalFocalLength);
var bestPose = new Transform3d();
var altPose = new Transform3d();
if (result.getError1() <= result.getError2()) {
bestPose = result.getPoseResult1();
altPose = result.getPoseResult2();
} else {
bestPose = result.getPoseResult2();
altPose = result.getPoseResult1();
}
bestPose = MathUtils.convertApriltagtoOpenCV(bestPose);
altPose = MathUtils.convertApriltagtoOpenCV(altPose);
m_cameraToTarget3d = bestPose;
m_bestCameraToTarget3d = bestPose;
m_altCameraToTarget3d = altPose;
double[] corners = result.getCorners();
Point[] cornerPoints =
@@ -231,12 +237,20 @@ public class TrackedTarget implements Releasable {
return !m_subContours.isEmpty();
}
public Transform3d getCameraToTarget3d() {
return m_cameraToTarget3d;
public Transform3d getBestCameraToTarget3d() {
return m_bestCameraToTarget3d;
}
public void setCameraToTarget3d(Transform3d pose) {
this.m_cameraToTarget3d = pose;
public Transform3d getAltCameraToTarget3d() {
return m_altCameraToTarget3d;
}
public void setBestCameraToTarget3d(Transform3d pose) {
this.m_bestCameraToTarget3d = pose;
}
public void setAltCameraToTarget3d(Transform3d pose) {
this.m_altCameraToTarget3d = pose;
}
public Mat getCameraRelativeTvec() {
@@ -272,8 +286,8 @@ public class TrackedTarget implements Releasable {
ret.put("skew", getSkew());
ret.put("area", getArea());
ret.put("ambiguity", getPoseAmbiguity());
if (getCameraToTarget3d() != null) {
ret.put("pose", transformToMap(getCameraToTarget3d()));
if (getBestCameraToTarget3d() != null) {
ret.put("pose", transformToMap(getBestCameraToTarget3d()));
}
ret.put("fiducialId", getFiducialId());
return ret;
@@ -289,7 +303,7 @@ public class TrackedTarget implements Releasable {
ret.put("qy", transform.getRotation().getQuaternion().getY());
ret.put("qz", transform.getRotation().getQuaternion().getZ());
ret.put("angle_z", transform.getRotation().getZ() + Math.PI / 2.0);
ret.put("angle_z", transform.getRotation().getZ());
return ret;
}

View File

@@ -0,0 +1,116 @@
/*
* Copyright (C) Photon Vision.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.photonvision.vision.videoStream;
import java.nio.ByteBuffer;
import java.util.Base64;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.function.Consumer;
import org.opencv.core.MatOfByte;
import org.opencv.core.MatOfInt;
import org.opencv.imgcodecs.Imgcodecs;
import org.photonvision.vision.frame.Frame;
import org.photonvision.vision.frame.consumer.MJPGFrameConsumer;
public class SocketVideoStream implements Consumer<Frame> {
int portID = 0; // Align with cscore's port for unique identification of stream
MatOfByte jpegBytes = null;
// Gets set to true when another class reads out valid jpeg bytes at least once
// Set back to false when another frame is freshly converted
// Should eliminate synchronization issues of differeing rates of putting frames in
// and taking them back out
boolean frameWasConsumed = false;
// Synclock around manipulating the jpeg bytes from multiple threads
Lock jpegBytesLock = new ReentrantLock();
MJPGFrameConsumer oldSchoolServer;
private int userCount = 0;
public SocketVideoStream(int portID) {
this.portID = portID;
oldSchoolServer =
new MJPGFrameConsumer("Port_" + Integer.toString(portID) + "_MJPEG_Server", portID);
}
@Override
public void accept(Frame frame) {
if (userCount > 0) {
if (jpegBytesLock
.tryLock()) { // we assume frames are coming in frequently. Just skip this frame if we're
// locked doing something else.
try {
// Does a single-shot frame recieve and convert to JPEG for efficency
// Will not capture/convert again until convertNextFrame() is called
if (frame != null && !frame.image.getMat().empty() && jpegBytes == null) {
frameWasConsumed = false;
jpegBytes = new MatOfByte();
Imgcodecs.imencode(
".jpg",
frame.image.getMat(),
jpegBytes,
new MatOfInt(Imgcodecs.IMWRITE_JPEG_QUALITY, 75));
}
} finally {
jpegBytesLock.unlock();
}
}
}
oldSchoolServer.accept(frame);
}
public String getJPEGBase64EncodedStr() {
String sendStr = null;
jpegBytesLock.lock();
if (jpegBytes != null) {
sendStr = Base64.getEncoder().encodeToString(jpegBytes.toArray());
}
jpegBytesLock.unlock();
return sendStr;
}
public ByteBuffer getJPEGByteBuffer() {
ByteBuffer sendStr = null;
jpegBytesLock.lock();
if (jpegBytes != null) {
sendStr = ByteBuffer.wrap(jpegBytes.toArray());
}
jpegBytesLock.unlock();
return sendStr;
}
public void convertNextFrame() {
jpegBytesLock.lock();
if (jpegBytes != null) {
jpegBytes.release();
jpegBytes = null;
}
jpegBytesLock.unlock();
}
public void addUser() {
userCount++;
}
public void removeUser() {
userCount--;
}
}

View File

@@ -0,0 +1,102 @@
/*
* Copyright (C) Photon Vision.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.photonvision.vision.videoStream;
import io.javalin.websocket.WsContext;
import java.nio.ByteBuffer;
import java.util.Hashtable;
import java.util.Map;
import org.photonvision.common.logging.LogGroup;
import org.photonvision.common.logging.Logger;
public class SocketVideoStreamManager {
private static final int NO_STREAM_PORT = -1;
private final Logger logger = new Logger(SocketVideoStreamManager.class, LogGroup.Camera);
private Map<Integer, SocketVideoStream> streams = new Hashtable<Integer, SocketVideoStream>();
private Map<WsContext, Integer> userSubscriptions = new Hashtable<WsContext, Integer>();
private static class ThreadSafeSingleton {
private static final SocketVideoStreamManager INSTANCE = new SocketVideoStreamManager();
}
public static SocketVideoStreamManager getInstance() {
return ThreadSafeSingleton.INSTANCE;
}
private SocketVideoStreamManager() {}
// Register a new available camera stream
public void addStream(SocketVideoStream newStream) {
streams.put(newStream.portID, newStream);
logger.debug("Added new stream for port " + Integer.toString(newStream.portID));
}
// Remove a previously-added camera stream, and unsubscribe all users
public void removeStream(SocketVideoStream oldStream) {
streams.remove(oldStream.portID);
logger.debug("Removed stream for port " + Integer.toString(oldStream.portID));
}
// Indicate a user would like to subscribe to a camera stream and get frames from it periodically
public void addSubscription(WsContext user, int streamPortID) {
var stream = streams.get(streamPortID);
if (stream != null) {
userSubscriptions.put(user, streamPortID);
stream.addUser();
} else {
logger.error(
"User attempted to subscribe to non-existent port " + Integer.toString(streamPortID));
}
}
// Indicate a user would like to stop receiving one camera stream
public void removeSubscription(WsContext user) {
var port = userSubscriptions.get(user);
if (port != null && port != NO_STREAM_PORT) {
var stream = streams.get(port);
userSubscriptions.put(user, NO_STREAM_PORT);
if (stream != null) {
stream.removeUser();
}
} else {
logger.error(
"User attempted to unsubscribe, but had not yet previously subscribed successfully.");
}
}
// For a given user, return the jpeg bytes (or null) for the most recent frame
public ByteBuffer getSendFrame(WsContext user) {
var port = userSubscriptions.get(user);
if (port != null && port != NO_STREAM_PORT) {
var stream = streams.get(port);
return stream.getJPEGByteBuffer();
} else {
return null;
}
}
// Causes all streams to "re-trigger" and recieve and convert their next mjpeg frame
// Only invoke this after all returned jpeg Strings have been used.
public void allStreamConvertNextFrame() {
for (SocketVideoStream stream : streams.values()) {
stream.convertNextFrame();
}
}
}

View File

@@ -161,7 +161,7 @@ public class CirclePNPTest {
System.out.println(
"Found targets at "
+ pipelineResult.targets.stream()
.map(TrackedTarget::getCameraToTarget3d)
.map(TrackedTarget::getBestCameraToTarget3d)
.collect(Collectors.toList()));
}
}

View File

@@ -111,19 +111,19 @@ public class SolvePNPTest {
printTestResults(pipelineResult);
// these numbers are not *accurate*, but they are known and expected
var pose = pipelineResult.targets.get(0).getCameraToTarget3d();
var pose = pipelineResult.targets.get(0).getBestCameraToTarget3d();
Assertions.assertEquals(1.1, pose.getTranslation().getX(), 0.05);
Assertions.assertEquals(0.0, pose.getTranslation().getY(), 0.05);
// We expect the object X axis to be to the right, or negative-Y in world space
// We expect the object X to be forward, or -X in world space
Assertions.assertEquals(
-1, new Translation3d(1, 0, 0).rotateBy(pose.getRotation()).getY(), 0.05);
// We expect the object Y axis to be up, or +Z in world space
-1, new Translation3d(1, 0, 0).rotateBy(pose.getRotation()).getX(), 0.05);
// We expect the object Y axis to be right, or negative-Y in world space
Assertions.assertEquals(
1, new Translation3d(0, 1, 0).rotateBy(pose.getRotation()).getZ(), 0.05);
// We expect the object Z axis to towards the camera, or negative-X in world space
-1, new Translation3d(0, 1, 0).rotateBy(pose.getRotation()).getY(), 0.05);
// We expect the object Z axis to be up, or +Z in world space
Assertions.assertEquals(
-1, new Translation3d(0, 0, 1).rotateBy(pose.getRotation()).getX(), 0.05);
1, new Translation3d(0, 0, 1).rotateBy(pose.getRotation()).getZ(), 0.05);
TestUtils.showImage(pipelineResult.outputFrame.image.getMat(), "Pipeline output", 999999);
}
@@ -159,10 +159,11 @@ public class SolvePNPTest {
pipelineResult.targets);
// these numbers are not *accurate*, but they are known and expected
var pose = pipelineResult.targets.get(0).getCameraToTarget3d();
var pose = pipelineResult.targets.get(0).getBestCameraToTarget3d();
Assertions.assertEquals(Units.inchesToMeters(240.26), pose.getTranslation().getX(), 0.05);
Assertions.assertEquals(Units.inchesToMeters(35), pose.getTranslation().getY(), 0.05);
Assertions.assertEquals(Units.degreesToRadians(-42), pose.getRotation().getZ(), 1);
// Z rotation should be mostly facing us
Assertions.assertEquals(Units.degreesToRadians(-140), pose.getRotation().getZ(), 1);
TestUtils.showImage(pipelineResult.inputFrame.image.getMat(), "Pipeline output", 999999);
}
@@ -210,7 +211,7 @@ public class SolvePNPTest {
System.out.println(
"Found targets at "
+ pipelineResult.targets.stream()
.map(TrackedTarget::getCameraToTarget3d)
.map(TrackedTarget::getBestCameraToTarget3d)
.collect(Collectors.toList()));
}
}

View File

@@ -103,6 +103,8 @@ task generateVendorJson() {
def read = photonlibFileInput.text.replace('${photon_version}', pubVersion)
photonlibFileOutput.write(read)
}
outputs.upToDateWhen { false }
}
build.dependsOn generateVendorJson

View File

@@ -28,6 +28,7 @@ import edu.wpi.first.networktables.NetworkTable;
import edu.wpi.first.networktables.NetworkTableEntry;
import edu.wpi.first.networktables.NetworkTableInstance;
import edu.wpi.first.wpilibj.DriverStation;
import edu.wpi.first.wpilibj.Timer;
import org.photonvision.common.dataflow.structures.Packet;
import org.photonvision.common.hardware.VisionLEDMode;
import org.photonvision.targeting.PhotonPipelineResult;
@@ -46,6 +47,8 @@ public class PhotonCamera {
private final String path;
private static boolean VERSION_CHECK_ENABLED = true;
private static long VERSION_CHECK_INTERVAL = 1;
private double lastVersionCheckTime = 0;
public static void setVersionCheckEnabled(boolean enabled) {
VERSION_CHECK_ENABLED = enabled;
@@ -99,9 +102,14 @@ public class PhotonCamera {
// Populate packet and create result.
packet.setData(rawBytesEntry.getRaw(new byte[] {}));
if (packet.getSize() < 1) return ret;
ret.createFromPacket(packet);
// Set the timestamp of the result.
// getLatestChange returns in microseconds so we divide by 1e6 to convert to seconds.
ret.setTimestampSeconds((rawBytesEntry.getLastChange() / 1e6) - ret.getLatencyMillis() / 1e3);
// Return result.
return ret;
}
@@ -207,6 +215,9 @@ public class PhotonCamera {
private void verifyVersion() {
if (!VERSION_CHECK_ENABLED) return;
if ((Timer.getFPGATimestamp() - lastVersionCheckTime) < VERSION_CHECK_INTERVAL) return;
lastVersionCheckTime = Timer.getFPGATimestamp();
String versionString = versionEntry.getString("");
if (versionString.equals("")) {
DriverStation.reportError(

View File

@@ -61,7 +61,9 @@ public class SimPhotonCamera extends PhotonCamera {
targetAreaEntry = rootTable.getEntry("targetAreaEntry");
targetSkewEntry = rootTable.getEntry("targetSkewEntry");
targetPoseEntry = rootTable.getEntry("targetPoseEntry");
versionEntry = rootTable.getEntry("versionEntry");
// The versionEntry is stored under the main table of <instance>/photonvision
versionEntry = instance.getTable("photonvision").getEntry("version");
// Sets the version string so that it will always match the current version
versionEntry.setString(PhotonVersion.versionString);
@@ -144,7 +146,7 @@ public class SimPhotonCamera extends PhotonCamera {
targetAreaEntry.setDouble(bestTarget.getArea());
targetSkewEntry.setDouble(bestTarget.getSkew());
var transform = bestTarget.getCameraToTarget();
var transform = bestTarget.getBestCameraToTarget();
double[] poseData = {
transform.getX(), transform.getY(), transform.getRotation().toRotation2d().getDegrees()
};

View File

@@ -25,9 +25,15 @@
package org.photonvision;
import edu.wpi.first.math.geometry.Pose2d;
import edu.wpi.first.math.geometry.Pose3d;
import edu.wpi.first.math.geometry.Rotation3d;
import edu.wpi.first.math.geometry.Transform2d;
import edu.wpi.first.math.geometry.Transform3d;
import edu.wpi.first.math.geometry.Translation3d;
import edu.wpi.first.math.util.Units;
import edu.wpi.first.wpilibj.smartdashboard.Field2d;
import edu.wpi.first.wpilibj.smartdashboard.FieldObject2d;
import edu.wpi.first.wpilibj.smartdashboard.SmartDashboard;
import java.util.ArrayList;
import java.util.List;
import org.photonvision.targeting.PhotonTrackedTarget;
@@ -40,11 +46,14 @@ public class SimVisionSystem {
double camVertFOVDegrees;
double cameraHeightOffGroundMeters;
double maxLEDRangeMeters;
double camPitchDegrees;
int cameraResWidth;
int cameraResHeight;
double minTargetArea;
Transform2d cameraToRobot;
Transform3d cameraToRobot;
Field2d dbgField;
FieldObject2d dbgRobot;
FieldObject2d dbgCamera;
ArrayList<SimVisionTarget> tgtList;
@@ -58,11 +67,7 @@ public class SimVisionSystem {
* @param camDiagFOVDegrees Diagonal Field of View of the camera used. Align it with the
* manufacturer specifications, and/or whatever is configured in the PhotonVision Setting
* page.
* @param camPitchDegrees pitch of the camera's view axis back from horizontal. Make this the same
* as whatever is configured in the PhotonVision Setting page.
* @param cameraToRobot Pose Transform to move from the camera's mount position to the robot's
* position
* @param cameraHeightOffGroundMeters Height of the camera off the ground in meters
* @param cameraToRobot Transform to move from the camera's mount position to the robot's position
* @param maxLEDRangeMeters Maximum distance at which your camera can illuminate the target and
* make it visible. Set to 9000 or more if your vision system does not rely on LED's.
* @param cameraResWidth Width of your camera's image sensor in pixels
@@ -74,16 +79,12 @@ public class SimVisionSystem {
public SimVisionSystem(
String camName,
double camDiagFOVDegrees,
double camPitchDegrees,
Transform2d cameraToRobot,
double cameraHeightOffGroundMeters,
Transform3d cameraToRobot,
double maxLEDRangeMeters,
int cameraResWidth,
int cameraResHeight,
double minTargetArea) {
this.camPitchDegrees = camPitchDegrees;
this.cameraToRobot = cameraToRobot;
this.cameraHeightOffGroundMeters = cameraHeightOffGroundMeters;
this.maxLEDRangeMeters = maxLEDRangeMeters;
this.cameraResWidth = cameraResWidth;
this.cameraResHeight = cameraResHeight;
@@ -96,6 +97,11 @@ public class SimVisionSystem {
cam = new SimPhotonCamera(camName);
tgtList = new ArrayList<>();
dbgField = new Field2d();
dbgRobot = dbgField.getRobotObject();
dbgCamera = dbgField.getObject(camName + " Camera");
SmartDashboard.putData(camName + " Sim Field", dbgField);
}
/**
@@ -107,6 +113,10 @@ public class SimVisionSystem {
*/
public void addSimVisionTarget(SimVisionTarget target) {
tgtList.add(target);
dbgField
.getObject("Target " + Integer.toString(target.targetID))
.setPose(target.targetPose.toPose2d());
;
}
/**
@@ -114,14 +124,9 @@ public class SimVisionSystem {
* turret or some other mobile platform.
*
* @param newCameraToRobot New Transform from the robot to the camera
* @param newCamHeightMeters New height of the camera off the floor
* @param newCamPitchDegrees New pitch of the camera axis back from horizontal
*/
public void moveCamera(
Transform2d newCameraToRobot, double newCamHeightMeters, double newCamPitchDegrees) {
public void moveCamera(Transform3d newCameraToRobot) {
this.cameraToRobot = newCameraToRobot;
this.cameraHeightOffGroundMeters = newCamHeightMeters;
this.camPitchDegrees = newCamPitchDegrees;
}
/**
@@ -133,45 +138,76 @@ public class SimVisionSystem {
* PhotonVision parameters.
*/
public void processFrame(Pose2d robotPoseMeters) {
Pose2d cameraPos = robotPoseMeters.transformBy(cameraToRobot.inverse());
var robotPose3d =
new Pose3d(
robotPoseMeters.getX(),
robotPoseMeters.getY(),
0.0,
new Rotation3d(0, 0, robotPoseMeters.getRotation().getRadians()));
processFrame(robotPose3d);
}
/**
* Periodic update. Call this once per frame of image data you wish to process and send to
* NetworkTables
*
* @param robotPoseMeters current pose of the robot in space. Will be used to calculate which
* targets are actually in view, where they are at relative to the robot, and relevant
* PhotonVision parameters.
*/
public void processFrame(Pose3d robotPoseMeters) {
Pose3d cameraPose = robotPoseMeters.transformBy(cameraToRobot.inverse());
dbgRobot.setPose(robotPoseMeters.toPose2d());
dbgCamera.setPose(cameraPose.toPose2d());
ArrayList<PhotonTrackedTarget> visibleTgtList = new ArrayList<>(tgtList.size());
tgtList.forEach(
(tgt) -> {
var camToTargetTrans = new Transform2d(cameraPos, tgt.targetPos);
var camToTargetTrans = new Transform3d(cameraPose, tgt.targetPose);
var t = camToTargetTrans.getTranslation();
double distAlongGroundMeters = camToTargetTrans.getTranslation().getNorm();
double distVerticalMeters =
tgt.targetHeightAboveGroundMeters - this.cameraHeightOffGroundMeters;
double distMeters = Math.hypot(distAlongGroundMeters, distVerticalMeters);
// Rough approximation of the alternate solution, which is (so far) always incorrect.
var altTrans =
new Translation3d(
t.getX(),
-1.0 * t.getY(),
t.getZ()); // mirrored across camera axis in Y direction
var altRot = camToTargetTrans.getRotation().times(-1.0); // flipped
var camToTargetTransAlt = new Transform3d(altTrans, altRot);
double area = tgt.tgtAreaMeters2 / getM2PerPx(distAlongGroundMeters);
double distMeters = t.getNorm();
double area_px = tgt.tgtAreaMeters2 / getM2PerPx(distMeters);
double yawDegrees = Units.radiansToDegrees(Math.atan2(t.getY(), t.getX()));
double camHeightAboveGround = cameraPose.getZ();
double tgtHeightAboveGround = tgt.targetPose.getZ();
double camPitchDegrees = Units.radiansToDegrees(cameraPose.getRotation().getY());
var transformAlongGround =
new Transform2d(cameraPose.toPose2d(), tgt.targetPose.toPose2d());
double distAlongGround = transformAlongGround.getTranslation().getNorm();
// 2D yaw mode considers the target as a point, and should ignore target rotation.
// Photon reports it in the correct robot reference frame.
// IE: targets to the left of the image should report negative yaw.
double yawDegrees =
-1.0
* Units.radiansToDegrees(
Math.atan2(
camToTargetTrans.getTranslation().getY(),
camToTargetTrans.getTranslation().getX()));
double pitchDegrees =
Units.radiansToDegrees(Math.atan2(distVerticalMeters, distAlongGroundMeters))
- this.camPitchDegrees;
Units.radiansToDegrees(
Math.atan2((tgtHeightAboveGround - camHeightAboveGround), distAlongGround))
- camPitchDegrees;
if (camCanSeeTarget(distMeters, yawDegrees, pitchDegrees, area)) {
if (camCanSeeTarget(distMeters, yawDegrees, pitchDegrees, area_px)) {
// TODO simulate target corners
visibleTgtList.add(
new PhotonTrackedTarget(
yawDegrees,
pitchDegrees,
area,
area_px,
0.0,
-1, // TODO fiducial ID
new Transform3d(),
0.25,
tgt.targetID,
camToTargetTrans,
camToTargetTransAlt,
0.0, // TODO - simulate ambiguity when straight on?
List.of(
new TargetCorner(0, 0), new TargetCorner(0, 0),
new TargetCorner(0, 0), new TargetCorner(0, 0))));

View File

@@ -24,34 +24,28 @@
package org.photonvision;
import edu.wpi.first.math.geometry.Pose2d;
import edu.wpi.first.math.geometry.Pose3d;
public class SimVisionTarget {
Pose2d targetPos;
Pose3d targetPose;
double targetWidthMeters;
double targetHeightMeters;
double targetHeightAboveGroundMeters;
double tgtAreaMeters2;
int targetID;
/**
* Describes a vision target located somewhere on the field that your SimVisionSystem can detect.
*
* @param targetPos Pose2d of the target on the field. Define it such that, if you are standing on
* the middle of the field facing the target, the Y axis points to your left, and the X axis
* points away from you.
* @param targetHeightAboveGroundMeters Height of the target above the field plane, in meters.
* @param targetPos Pose3d of the target in field-relative coordinates
* @param targetWidthMeters Width of the outer bounding box of the target in meters.
* @param targetHeightMeters Pair Height of the outer bounding box of the target in meters.
*/
public SimVisionTarget(
Pose2d targetPos,
double targetHeightAboveGroundMeters,
double targetWidthMeters,
double targetHeightMeters) {
this.targetPos = targetPos;
this.targetHeightAboveGroundMeters = targetHeightAboveGroundMeters;
Pose3d targetPos, double targetWidthMeters, double targetHeightMeters, int targetID) {
this.targetPose = targetPos;
this.targetWidthMeters = targetWidthMeters;
this.targetHeightMeters = targetHeightMeters;
this.tgtAreaMeters2 = targetWidthMeters * targetHeightMeters;
this.targetID = targetID;
}
}

View File

@@ -25,11 +25,15 @@
#include "photonlib/PhotonCamera.h"
#include <frc/Errors.h>
#include <frc/Timer.h>
#include "PhotonVersion.h"
#include "photonlib/Packet.h"
namespace photonlib {
constexpr const units::second_t VERSION_CHECK_INTERVAL = 5_s;
PhotonCamera::PhotonCamera(std::shared_ptr<nt::NetworkTableInstance> instance,
const std::string& cameraName)
: mainTable(instance->GetTable("photonvision")),
@@ -48,7 +52,7 @@ PhotonCamera::PhotonCamera(const std::string& cameraName)
nt::NetworkTableInstance::GetDefault()),
cameraName) {}
PhotonPipelineResult PhotonCamera::GetLatestResult() const {
PhotonPipelineResult PhotonCamera::GetLatestResult() {
// Prints warning if not connected
VerifyVersion();
@@ -68,6 +72,10 @@ PhotonPipelineResult PhotonCamera::GetLatestResult() const {
photonlib::Packet packet{bytes};
packet >> result;
result.SetTimestamp(units::microsecond_t(rawBytesEntry.GetLastChange()) -
result.GetLatency());
return result;
}
@@ -99,9 +107,14 @@ void PhotonCamera::SetLEDMode(LEDMode mode) {
ledModeEntry.SetDouble(static_cast<double>(static_cast<int>(mode)));
}
void PhotonCamera::VerifyVersion() const {
void PhotonCamera::VerifyVersion() {
if (!PhotonCamera::VERSION_CHECK_ENABLED) return;
if ((frc::Timer::GetFPGATimestamp() - lastVersionCheckTime) <
VERSION_CHECK_INTERVAL)
return;
this->lastVersionCheckTime = frc::Timer::GetFPGATimestamp();
const std::string& versionString = versionEntry.GetString("");
if (versionString.empty()) {
std::string path_ = path;

View File

@@ -41,12 +41,12 @@ PhotonTrackedTarget::PhotonTrackedTarget(
area(area),
skew(skew),
fiducialId(id),
cameraToTarget(pose),
bestCameraToTarget(pose),
corners(corners) {}
bool PhotonTrackedTarget::operator==(const PhotonTrackedTarget& other) const {
return other.yaw == yaw && other.pitch == pitch && other.area == area &&
other.skew == skew && other.cameraToTarget == cameraToTarget &&
other.skew == skew && other.bestCameraToTarget == bestCameraToTarget &&
other.corners == corners;
}
@@ -56,13 +56,21 @@ bool PhotonTrackedTarget::operator!=(const PhotonTrackedTarget& other) const {
Packet& operator<<(Packet& packet, const PhotonTrackedTarget& target) {
packet << target.yaw << target.pitch << target.area << target.skew
<< target.fiducialId << target.cameraToTarget.Translation().X().value()
<< target.cameraToTarget.Translation().Y().value()
<< target.cameraToTarget.Translation().Z().value()
<< target.cameraToTarget.Rotation().GetQuaternion().W()
<< target.cameraToTarget.Rotation().GetQuaternion().X()
<< target.cameraToTarget.Rotation().GetQuaternion().Y()
<< target.cameraToTarget.Rotation().GetQuaternion().Z()
<< target.fiducialId
<< target.bestCameraToTarget.Translation().X().value()
<< target.bestCameraToTarget.Translation().Y().value()
<< target.bestCameraToTarget.Translation().Z().value()
<< target.bestCameraToTarget.Rotation().GetQuaternion().W()
<< target.bestCameraToTarget.Rotation().GetQuaternion().X()
<< target.bestCameraToTarget.Rotation().GetQuaternion().Y()
<< target.bestCameraToTarget.Rotation().GetQuaternion().Z()
<< target.altCameraToTarget.Translation().X().value()
<< target.altCameraToTarget.Translation().Y().value()
<< target.altCameraToTarget.Translation().Z().value()
<< target.altCameraToTarget.Rotation().GetQuaternion().W()
<< target.altCameraToTarget.Rotation().GetQuaternion().X()
<< target.altCameraToTarget.Rotation().GetQuaternion().Y()
<< target.altCameraToTarget.Rotation().GetQuaternion().Z()
<< target.poseAmbiguity;
for (int i = 0; i < 4; i++) {
@@ -75,17 +83,28 @@ Packet& operator<<(Packet& packet, const PhotonTrackedTarget& target) {
Packet& operator>>(Packet& packet, PhotonTrackedTarget& target) {
packet >> target.yaw >> target.pitch >> target.area >> target.skew >>
target.fiducialId;
// We use these for best and alt transforms below
double x = 0;
double y = 0;
double z = 0;
double w = 0;
// First transform is the "best" pose
packet >> x >> y >> z;
const auto translation = frc::Translation3d(
const auto bestTranslation = frc::Translation3d(
units::meter_t(x), units::meter_t(y), units::meter_t(z));
packet >> w >> x >> y >> z;
const auto rotation = frc::Rotation3d(frc::Quaternion(w, x, y, z));
const auto bestRotation = frc::Rotation3d(frc::Quaternion(w, x, y, z));
target.bestCameraToTarget = frc::Transform3d(bestTranslation, bestRotation);
target.cameraToTarget = frc::Transform3d(translation, rotation);
// Second transform is the "alternate" pose
packet >> x >> y >> z;
const auto altTranslation = frc::Translation3d(
units::meter_t(x), units::meter_t(y), units::meter_t(z));
packet >> w >> x >> y >> z;
const auto altRotation = frc::Rotation3d(frc::Quaternion(w, x, y, z));
target.altCameraToTarget = frc::Transform3d(altTranslation, altRotation);
packet >> target.poseAmbiguity;

View File

@@ -1,51 +0,0 @@
/*
* MIT License
*
* Copyright (c) 2022 PhotonVision
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
#include "photonlib/SimPhotonCamera.h"
namespace photonlib {
SimPhotonCamera::SimPhotonCamera(
std::shared_ptr<nt::NetworkTableInstance> instance,
const std::string& cameraName)
: PhotonCamera(instance, cameraName) {}
SimPhotonCamera::SimPhotonCamera(const std::string& cameraName)
: PhotonCamera(cameraName) {}
void SimPhotonCamera::SubmitProcessedFrame(
units::second_t latency, wpi::span<const PhotonTrackedTarget> tgtList) {
if (!GetDriverMode()) {
// Clear the current packet.
simPacket.Clear();
// Create the new result and pump it into the packet
simPacket << PhotonPipelineResult(latency, tgtList);
rawBytesEntry.SetRaw(std::string_view{simPacket.GetData().data(),
simPacket.GetData().size()});
}
}
} // namespace photonlib

View File

@@ -1,127 +0,0 @@
/*
* MIT License
*
* Copyright (c) 2022 PhotonVision
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
#include "photonlib/SimVisionSystem.h"
#include <cmath>
#include <units/angle.h>
#include <units/length.h>
#include <wpi/span.h>
namespace photonlib {
SimVisionSystem::SimVisionSystem(const std::string& name,
units::degree_t camDiagFOV,
frc::Transform2d cameraToRobot,
units::meter_t cameraHeightOffGround,
units::meter_t maxLEDRange, int cameraResWidth,
int cameraResHeight, double minTargetArea)
: cameraToRobot(cameraToRobot),
cameraHeightOffGround(cameraHeightOffGround),
maxLEDRange(maxLEDRange),
cameraResWidth(cameraResWidth),
cameraResHeight(cameraResHeight),
minTargetArea(minTargetArea) {
double hypotPixels = std::hypot(cameraResWidth, cameraResHeight);
camHorizFOV = camDiagFOV * cameraResWidth / hypotPixels;
camVertFOV = camDiagFOV * cameraResHeight / hypotPixels;
cam = SimPhotonCamera(name);
tgtList.clear();
}
void SimVisionSystem::AddSimVisionTarget(SimVisionTarget tgt) {
tgtList.push_back(tgt);
}
void SimVisionSystem::MoveCamera(frc::Transform2d newCameraToRobot,
units::meter_t newCamHeight) {
cameraToRobot = newCameraToRobot;
cameraHeightOffGround = newCamHeight;
}
void SimVisionSystem::ProcessFrame(frc::Pose2d robotPose) {
frc::Pose2d cameraPos = robotPose.TransformBy(cameraToRobot.Inverse());
std::vector<PhotonTrackedTarget> visibleTgtList = {};
for (auto&& tgt : tgtList) {
frc::Transform2d camToTargetTrans =
frc::Transform2d(cameraPos, tgt.targetPos);
units::meter_t distAlongGround = camToTargetTrans.Translation().Norm();
units::meter_t distVertical =
tgt.targetHeightAboveGround - cameraHeightOffGround;
units::meter_t distHypot =
units::math::hypot(distAlongGround, distVertical);
double area = tgt.tgtArea.value() / GetM2PerPx(distAlongGround);
// 2D yaw mode considers the target as a point, and should ignore target
// rotation.
// Photon reports it in the correct robot reference frame.
// IE: targets to the left of the image should report negative yaw.
units::degree_t yawAngle = -units::math::atan2(
camToTargetTrans.Translation().Y(), camToTargetTrans.Translation().X());
units::degree_t pitchAngle =
units::math::atan2(distVertical, distAlongGround);
auto translation = frc::Translation3d(camToTargetTrans.Translation().X(),
camToTargetTrans.Translation().Y(),
units::meter_t(0)); // TODO z height
auto rotation = frc::Rotation3d(units::radian_t(0), pitchAngle, -yawAngle);
frc::Transform3d camToTarget3d{translation, rotation};
if (CamCanSeeTarget(distHypot, yawAngle, pitchAngle, area)) {
PhotonTrackedTarget newTgt = PhotonTrackedTarget(
yawAngle.value(), pitchAngle.value(), area, 0.0, -1, camToTarget3d,
{std::pair{1, 2}, std::pair{3, 4}, std::pair{5, 6}, std::pair{7, 8}});
visibleTgtList.push_back(newTgt);
}
}
units::second_t procDelay(0.0); // Future - tie this to something meaningful
cam.SubmitProcessedFrame(procDelay,
wpi::span<PhotonTrackedTarget>(visibleTgtList));
}
double SimVisionSystem::GetM2PerPx(units::meter_t dist) {
double heightMPerPx =
2 * dist.value() * units::math::tan(camVertFOV / 2) / cameraResHeight;
double widthMPerPx =
2 * dist.value() * units::math::tan(camHorizFOV / 2) / cameraResWidth;
return widthMPerPx * heightMPerPx;
}
bool SimVisionSystem::CamCanSeeTarget(units::meter_t distHypot,
units::degree_t yaw,
units::degree_t pitch, double area) {
bool inRange = (distHypot < maxLEDRange);
bool inHorizAngle = units::math::abs(yaw) < (camHorizFOV / 2);
bool inVertAngle = units::math::abs(pitch) < (camVertFOV / 2);
bool targetBigEnough = area > minTargetArea;
return (inRange && inHorizAngle && inVertAngle && targetBigEnough);
}
} // namespace photonlib

View File

@@ -1,40 +0,0 @@
/*
* MIT License
*
* Copyright (c) 2022 PhotonVision
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
#include "photonlib/SimVisionTarget.h"
namespace photonlib {
SimVisionTarget::SimVisionTarget(frc::Pose2d& targetPos,
units::meter_t targetHeightAboveGround,
units::meter_t targetWidth,
units::meter_t targetHeight)
: targetPos(targetPos),
targetHeightAboveGround(targetHeightAboveGround),
targetWidth(targetWidth),
targetHeight(targetHeight) {
tgtArea = targetWidth * targetHeight;
}
} // namespace photonlib

View File

@@ -30,6 +30,7 @@
#include <networktables/NetworkTable.h>
#include <networktables/NetworkTableEntry.h>
#include <networktables/NetworkTableInstance.h>
#include <units/time.h>
#include <wpi/deprecated.h>
#include "photonlib/PhotonPipelineResult.h"
@@ -66,7 +67,7 @@ class PhotonCamera {
* Returns the latest pipeline result.
* @return The latest pipeline result.
*/
PhotonPipelineResult GetLatestResult() const;
PhotonPipelineResult GetLatestResult();
/**
* Toggles driver mode.
@@ -136,7 +137,7 @@ class PhotonCamera {
*/
WPI_DEPRECATED(
"This method should be replaced with PhotonPipelineResult::HasTargets()")
bool HasTargets() const { return GetLatestResult().HasTargets(); }
bool HasTargets() { return GetLatestResult().HasTargets(); }
inline static void SetVersionCheckEnabled(bool enabled) {
PhotonCamera::VERSION_CHECK_ENABLED = enabled;
@@ -158,9 +159,10 @@ class PhotonCamera {
mutable Packet packet;
private:
units::second_t lastVersionCheckTime = 0_s;
inline static bool VERSION_CHECK_ENABLED = true;
void VerifyVersion() const;
void VerifyVersion();
};
} // namespace photonlib

View File

@@ -79,6 +79,22 @@ class PhotonPipelineResult {
*/
units::second_t GetLatency() const { return latency; }
/**
* Returns the estimated time the frame was taken,
* This is much more accurate than using GetLatency()
* @return The timestamp in seconds or -1 if this result was not initiated
* with a timestamp.
*/
units::second_t GetTimestamp() const { return timestamp; }
/**
* Sets the timestamp in seconds
* @param timestamp The timestamp in seconds
*/
void SetTimestamp(const units::second_t timestamp) {
this->timestamp = timestamp;
}
/**
* Returns whether the pipeline has targets.
* @return Whether the pipeline has targets.
@@ -101,6 +117,7 @@ class PhotonPipelineResult {
private:
units::second_t latency = 0_s;
units::second_t timestamp = -1_s;
wpi::SmallVector<PhotonTrackedTarget, 10> targets;
inline static bool HAS_WARNED = false;
};

View File

@@ -97,10 +97,22 @@ class PhotonTrackedTarget {
double GetPoseAmbiguity() const { return poseAmbiguity; }
/**
* Returns the pose of the target relative to the robot.
* Get the transform that maps camera space (X = forward, Y = left, Z = up) to
* object/fiducial tag space (X forward, Y left, Z up) with the lowest
* reprojection error. The ratio between this and the alternate target's
* reprojection error is the ambiguity, which is between 0 and 1.
* @return The pose of the target relative to the robot.
*/
frc::Transform3d GetCameraToTarget() const { return cameraToTarget; }
frc::Transform3d GetBestCameraToTarget() const { return bestCameraToTarget; }
/**
* Get the transform that maps camera space (X = forward, Y = left, Z = up) to
* object/fiducial tag space (X forward, Y left, Z up) with the highest
* reprojection error
*/
frc::Transform3d GetAlternateCameraToTarget() const {
return altCameraToTarget;
}
bool operator==(const PhotonTrackedTarget& other) const;
bool operator!=(const PhotonTrackedTarget& other) const;
@@ -114,7 +126,8 @@ class PhotonTrackedTarget {
double area = 0;
double skew = 0;
int fiducialId;
frc::Transform3d cameraToTarget;
frc::Transform3d bestCameraToTarget;
frc::Transform3d altCameraToTarget;
double poseAmbiguity;
wpi::SmallVector<std::pair<double, double>, 4> corners;
};

View File

@@ -1,75 +0,0 @@
/*
* MIT License
*
* Copyright (c) 2022 PhotonVision
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
#pragma once
#include <memory>
#include <string>
#include <units/time.h>
#include <wpi/SmallVector.h>
#include <wpi/span.h>
#include "photonlib/Packet.h"
#include "photonlib/PhotonCamera.h"
namespace photonlib {
/**
* Represents a camera that is connected to PhotonVision.ß
*/
class SimPhotonCamera : public PhotonCamera {
public:
/**
* Constructs a Simulated PhotonCamera from a root table.
*
* @param instance The NetworkTableInstance to pull data from. This can be a
* custom instance in simulation, but should *usually* be the default
* NTInstance from {@link NetworkTableInstance::getDefault}
* @param cameraName The name of the camera, as seen in the UI.
*/
explicit SimPhotonCamera(std::shared_ptr<nt::NetworkTableInstance> instance,
const std::string& cameraName);
/**
* Constructs a Simulated PhotonCamera from the name of the camera.
*
* @param cameraName The nickname of the camera (found in the PhotonVision
* UI).
*/
explicit SimPhotonCamera(const std::string& cameraName);
/**
* Simulate one processed frame of vision data, putting one result to NT.
* @param latency Latency of frame processing
* @param tgtList Set of targets detected
*/
void SubmitProcessedFrame(units::second_t latency,
wpi::span<const PhotonTrackedTarget> tgtList);
private:
mutable Packet simPacket;
};
} // namespace photonlib

View File

@@ -1,77 +0,0 @@
/*
* MIT License
*
* Copyright (c) 2022 PhotonVision
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
#pragma once
#include <string>
#include <vector>
#include <frc/geometry/Translation2d.h>
#include <units/angle.h>
#include <units/area.h>
#include <units/length.h>
#include <units/time.h>
#include <wpi/SmallVector.h>
#include "photonlib/SimPhotonCamera.h"
#include "photonlib/SimVisionTarget.h"
namespace photonlib {
/**
* Represents a camera that is connected to PhotonVision.
*/
class SimVisionSystem {
public:
explicit SimVisionSystem(const std::string& name, units::degree_t camDiagFOV,
frc::Transform2d cameraToRobot,
units::meter_t cameraHeightOffGround,
units::meter_t maxLEDRange, int cameraResWidth,
int cameraResHeight, double minTargetArea);
void AddSimVisionTarget(SimVisionTarget tgt);
void MoveCamera(frc::Transform2d newcameraToRobot,
units::meter_t newCamHeight);
void ProcessFrame(frc::Pose2d robotPose);
private:
frc::Transform2d cameraToRobot;
units::meter_t cameraHeightOffGround;
units::meter_t maxLEDRange;
int cameraResWidth;
int cameraResHeight;
double minTargetArea;
units::degree_t camHorizFOV;
units::degree_t camVertFOV;
std::vector<SimVisionTarget> tgtList = {};
double GetM2PerPx(units::meter_t dist);
bool CamCanSeeTarget(units::meter_t distHypot, units::degree_t yaw,
units::degree_t pitch, double area);
public:
SimPhotonCamera cam = photonlib::SimPhotonCamera("Default");
};
} // namespace photonlib

View File

@@ -1,51 +0,0 @@
/*
* MIT License
*
* Copyright (c) 2022 PhotonVision
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
#pragma once
#include <frc/geometry/Pose2d.h>
#include <units/area.h>
#include <units/length.h>
namespace photonlib {
/**
* Represents a target on the field which the vision processing system could
* detect.
*/
class SimVisionTarget {
public:
explicit SimVisionTarget(frc::Pose2d& targetPos,
units::meter_t targetHeightAboveGround,
units::meter_t targetWidth,
units::meter_t targetHeight);
frc::Pose2d targetPos;
units::meter_t targetHeightAboveGround;
units::meter_t targetWidth;
units::meter_t targetHeight;
units::square_meter_t tgtArea;
};
} // namespace photonlib

View File

@@ -45,6 +45,7 @@ class PacketTest {
-5.0,
-1,
new Transform3d(new Translation3d(), new Rotation3d()),
new Transform3d(new Translation3d(), new Rotation3d()),
0.25,
List.of(
new TargetCorner(1, 2),
@@ -82,6 +83,7 @@ class PacketTest {
4.0,
2,
new Transform3d(new Translation3d(1, 2, 3), new Rotation3d(1, 2, 3)),
new Transform3d(new Translation3d(1, 2, 3), new Rotation3d(1, 2, 3)),
0.25,
List.of(
new TargetCorner(1, 2),
@@ -95,6 +97,7 @@ class PacketTest {
6.7,
3,
new Transform3d(new Translation3d(4, 2, 3), new Rotation3d(1, 5, 3)),
new Transform3d(new Translation3d(4, 2, 3), new Rotation3d(1, 5, 3)),
0.25,
List.of(
new TargetCorner(1, 2),

View File

@@ -29,13 +29,19 @@ import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
import edu.wpi.first.math.geometry.Pose2d;
import edu.wpi.first.math.geometry.Pose3d;
import edu.wpi.first.math.geometry.Rotation2d;
import edu.wpi.first.math.geometry.Transform2d;
import edu.wpi.first.math.geometry.Rotation3d;
import edu.wpi.first.math.geometry.Transform3d;
import edu.wpi.first.math.geometry.Translation2d;
import edu.wpi.first.math.geometry.Translation3d;
import edu.wpi.first.math.util.Units;
import edu.wpi.first.networktables.NetworkTableInstance;
import java.util.List;
import java.util.stream.Stream;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
@@ -49,18 +55,30 @@ class SimVisionSystemTest {
Assertions.assertDoesNotThrow(
() -> {
var sysUnderTest =
new SimVisionSystem("Test", 80.0, 0.0, new Transform2d(), 1, 99999, 320, 240, 0);
sysUnderTest.addSimVisionTarget(new SimVisionTarget(new Pose2d(), 0.0, 1.0, 1.0));
new SimVisionSystem("Test", 80.0, new Transform3d(), 99999, 320, 240, 0);
sysUnderTest.addSimVisionTarget(new SimVisionTarget(new Pose3d(), 1.0, 1.0, 42));
for (int loopIdx = 0; loopIdx < 100; loopIdx++) {
sysUnderTest.processFrame(new Pose2d());
}
});
}
@BeforeAll
public static void setUp() {
// NT live for debug purposes
NetworkTableInstance.getDefault().startServer();
// No version check for testing
PhotonCamera.setVersionCheckEnabled(false);
}
@AfterAll
public static void shutDown() {}
// @ParameterizedTest
// @ValueSource(doubles = {5, 10, 15, 20, 25, 30})
// public void testDistanceAligned(double dist) {
// final var targetPose = new Pose2d(new Translation2d(35, 0), new Rotation2d());
// final var targetPose = new Pose2d(new Translation2d(15.98, 0), new Rotation2d());
// var sysUnderTest =
// new SimVisionSystem("Test", 80.0, 0.0, new Transform2d(), 1, 99999, 320, 240, 0);
// sysUnderTest.addSimVisionTarget(new SimVisionTarget(targetPose, 0.0, 1.0, 1.0));
@@ -76,10 +94,10 @@ class SimVisionSystemTest {
@Test
public void testVisibilityCupidShuffle() {
final var targetPose = new Pose2d(new Translation2d(35, 0), new Rotation2d());
var sysUnderTest =
new SimVisionSystem("Test", 80.0, 0.0, new Transform2d(), 1, 99999, 640, 480, 0);
sysUnderTest.addSimVisionTarget(new SimVisionTarget(targetPose, 1.0, 3.0, 3.0));
final var targetPose =
new Pose3d(new Translation3d(15.98, 0, 2), new Rotation3d(0, 0, Math.PI));
var sysUnderTest = new SimVisionSystem("Test", 80.0, new Transform3d(), 99999, 640, 480, 0);
sysUnderTest.addSimVisionTarget(new SimVisionTarget(targetPose, 1.0, 3.0, 3));
// To the right, to the right
var robotPose = new Pose2d(new Translation2d(5, 0), Rotation2d.fromDegrees(-70));
@@ -117,37 +135,40 @@ class SimVisionSystemTest {
assertFalse(sysUnderTest.cam.getLatestResult().hasTargets());
// now walk it by yourself
sysUnderTest.moveCamera(
new Transform2d(new Translation2d(), Rotation2d.fromDegrees(180)), 0, 1.0);
sysUnderTest.moveCamera(new Transform3d(new Translation3d(), new Rotation3d(0, 0, Math.PI)));
sysUnderTest.processFrame(robotPose);
assertTrue(sysUnderTest.cam.getLatestResult().hasTargets());
}
@Test
public void testNotVisibleVert1() {
final var targetPose = new Pose2d(new Translation2d(35, 0), new Rotation2d());
var sysUnderTest =
new SimVisionSystem("Test", 80.0, 0.0, new Transform2d(), 1, 99999, 640, 480, 0);
sysUnderTest.addSimVisionTarget(new SimVisionTarget(targetPose, 1.0, 3.0, 3.0));
final var targetPose =
new Pose3d(new Translation3d(15.98, 0, 1), new Rotation3d(0, 0, Math.PI));
var sysUnderTest = new SimVisionSystem("Test", 80.0, new Transform3d(), 99999, 640, 480, 0);
sysUnderTest.addSimVisionTarget(new SimVisionTarget(targetPose, 1.0, 3.0, 3));
PhotonCamera.setVersionCheckEnabled(false);
var robotPose = new Pose2d(new Translation2d(5, 0), Rotation2d.fromDegrees(5));
sysUnderTest.processFrame(robotPose);
assertTrue(sysUnderTest.cam.getLatestResult().hasTargets());
sysUnderTest.moveCamera(new Transform2d(), 5000, 1.0); // vooop selfie stick
sysUnderTest.moveCamera(
new Transform3d(
new Translation3d(0, 0, 5000), new Rotation3d(0, 0, Math.PI))); // vooop selfie stick
sysUnderTest.processFrame(robotPose);
assertFalse(sysUnderTest.cam.getLatestResult().hasTargets());
}
@Test
public void testNotVisibleVert2() {
final var targetPose = new Pose2d(new Translation2d(35, 0), new Rotation2d());
final var targetPose =
new Pose3d(new Translation3d(15.98, 0, 2), new Rotation3d(0, 0, Math.PI));
var robotToCamera =
new Transform3d(new Translation3d(0, 0, 1), new Rotation3d(0, Math.PI / 4, 0));
var sysUnderTest =
new SimVisionSystem("Test", 80.0, 45.0, new Transform2d(), 1, 99999, 1234, 1234, 0);
sysUnderTest.addSimVisionTarget(new SimVisionTarget(targetPose, 3.0, 0.5, 0.5));
new SimVisionSystem("Test", 80.0, robotToCamera.inverse(), 99999, 1234, 1234, 0);
sysUnderTest.addSimVisionTarget(new SimVisionTarget(targetPose, 3.0, 0.5, 1736));
var robotPose = new Pose2d(new Translation2d(32, 0), Rotation2d.fromDegrees(5));
var robotPose = new Pose2d(new Translation2d(14.98, 0), Rotation2d.fromDegrees(5));
sysUnderTest.processFrame(robotPose);
assertTrue(sysUnderTest.cam.getLatestResult().hasTargets());
@@ -159,12 +180,12 @@ class SimVisionSystemTest {
@Test
public void testNotVisibleTgtSize() {
final var targetPose = new Pose2d(new Translation2d(35, 0), new Rotation2d());
var sysUnderTest =
new SimVisionSystem("Test", 80.0, 0.0, new Transform2d(), 1, 99999, 640, 480, 20.0);
sysUnderTest.addSimVisionTarget(new SimVisionTarget(targetPose, 1.0, 0.25, 0.1));
final var targetPose =
new Pose3d(new Translation3d(15.98, 0, 1), new Rotation3d(0, 0, Math.PI));
var sysUnderTest = new SimVisionSystem("Test", 80.0, new Transform3d(), 99999, 640, 480, 20.0);
sysUnderTest.addSimVisionTarget(new SimVisionTarget(targetPose, 0.1, 0.025, 24));
var robotPose = new Pose2d(new Translation2d(32, 0), Rotation2d.fromDegrees(5));
var robotPose = new Pose2d(new Translation2d(12, 0), Rotation2d.fromDegrees(5));
sysUnderTest.processFrame(robotPose);
assertTrue(sysUnderTest.cam.getLatestResult().hasTargets());
@@ -175,12 +196,12 @@ class SimVisionSystemTest {
@Test
public void testNotVisibleTooFarForLEDs() {
final var targetPose = new Pose2d(new Translation2d(35, 0), new Rotation2d());
var sysUnderTest =
new SimVisionSystem("Test", 80.0, 0.0, new Transform2d(), 1, 10, 640, 480, 1.0);
sysUnderTest.addSimVisionTarget(new SimVisionTarget(targetPose, 1.0, 0.25, 0.1));
final var targetPose =
new Pose3d(new Translation3d(15.98, 0, 1), new Rotation3d(0, 0, Math.PI));
var sysUnderTest = new SimVisionSystem("Test", 80.0, new Transform3d(), 10, 640, 480, 1.0);
sysUnderTest.addSimVisionTarget(new SimVisionTarget(targetPose, 1.0, 0.25, 78));
var robotPose = new Pose2d(new Translation2d(28, 0), Rotation2d.fromDegrees(5));
var robotPose = new Pose2d(new Translation2d(10, 0), Rotation2d.fromDegrees(5));
sysUnderTest.processFrame(robotPose);
assertTrue(sysUnderTest.cam.getLatestResult().hasTargets());
@@ -192,12 +213,12 @@ class SimVisionSystemTest {
@ParameterizedTest
@ValueSource(doubles = {-10, -5, -0, -1, -2, 5, 7, 10.23})
public void testYawAngles(double testYaw) {
final var targetPose = new Pose2d(new Translation2d(35, 0), new Rotation2d(Math.PI / 4));
var sysUnderTest =
new SimVisionSystem("Test", 80.0, 0.0, new Transform2d(), 1, 99999, 640, 480, 0);
sysUnderTest.addSimVisionTarget(new SimVisionTarget(targetPose, 0.0, 0.5, 0.5));
final var targetPose =
new Pose3d(new Translation3d(15.98, 0, 1), new Rotation3d(0, 0, 3 * Math.PI / 4));
var sysUnderTest = new SimVisionSystem("Test", 80.0, new Transform3d(), 99999, 640, 480, 0);
sysUnderTest.addSimVisionTarget(new SimVisionTarget(targetPose, 0.5, 0.5, 3));
var robotPose = new Pose2d(new Translation2d(32, 0), Rotation2d.fromDegrees(testYaw));
var robotPose = new Pose2d(new Translation2d(10, 0), Rotation2d.fromDegrees(-1.0 * testYaw));
sysUnderTest.processFrame(robotPose);
var res = sysUnderTest.cam.getLatestResult();
assertTrue(res.hasTargets());
@@ -208,45 +229,48 @@ class SimVisionSystemTest {
@ParameterizedTest
@ValueSource(doubles = {-10, -5, -0, -1, -2, 5, 7, 10.23, 20.21, -19.999})
public void testCameraPitch(double testPitch) {
final var targetPose = new Pose2d(new Translation2d(35, 0), new Rotation2d(Math.PI / 4));
final var robotPose = new Pose2d(new Translation2d(30, 0), new Rotation2d(0));
var sysUnderTest =
new SimVisionSystem("Test", 80.0, 0.0, new Transform2d(), 0.0, 99999, 640, 480, 0);
sysUnderTest.addSimVisionTarget(new SimVisionTarget(targetPose, 0.0, 0.5, 0.5));
final var targetPose =
new Pose3d(new Translation3d(15.98, 0, 0), new Rotation3d(0, 0, 3 * Math.PI / 4));
final var robotPose = new Pose2d(new Translation2d(10, 0), new Rotation2d(0));
var sysUnderTest = new SimVisionSystem("Test", 120.0, new Transform3d(), 99999, 640, 480, 0);
sysUnderTest.addSimVisionTarget(new SimVisionTarget(targetPose, 0.5, 0.5, 23));
sysUnderTest.moveCamera(new Transform2d(), 0.0, testPitch);
// Here, passing in a positive testPitch points the camera downward (since moveCamera takes the
// camera->robot transform)
sysUnderTest.moveCamera(
new Transform3d(
new Translation3d(), new Rotation3d(0, Units.degreesToRadians(testPitch), 0)));
sysUnderTest.processFrame(robotPose);
var res = sysUnderTest.cam.getLatestResult();
assertTrue(res.hasTargets());
var tgt = res.getBestTarget();
// If the camera is pitched down by 10 degrees, the target should appear
// in the upper part of the image (ie, pitch positive). Therefor,
// pass/fail involves -1.0.
assertEquals(tgt.getPitch(), -testPitch, 0.0001);
// Since the camera is level with the target, a downward point will mean the target is in the
// upper half of the image
// which should produce positive pitch.
assertEquals(testPitch, tgt.getPitch(), 0.0001);
}
private static Stream<Arguments> distCalCParamProvider() {
// Arbitrary and fairly random assortment of distances, camera pitches, and heights
return Stream.of(
Arguments.of(5, 35, 0),
Arguments.of(6, 35, 1),
Arguments.of(10, 35, 0),
Arguments.of(15, 35, 2),
Arguments.of(19.95, 35, 0),
Arguments.of(20, 35, 0),
Arguments.of(5, 15.98, 0),
Arguments.of(6, 15.98, 1),
Arguments.of(10, 15.98, 0),
Arguments.of(15, 15.98, 2),
Arguments.of(19.95, 15.98, 0),
Arguments.of(20, 15.98, 0),
Arguments.of(5, 42, 1),
Arguments.of(6, 42, 0),
Arguments.of(10, 42, 2),
Arguments.of(15, 42, 0.5),
Arguments.of(19.42, 35, 0),
Arguments.of(19.42, 15.98, 0),
Arguments.of(20, 42, 0),
Arguments.of(5, 55, 2),
Arguments.of(6, 55, 0),
Arguments.of(10, 54, 2.2),
Arguments.of(15, 53, 0),
Arguments.of(19.52, 35, 1.1),
Arguments.of(20, 51, 2.87),
Arguments.of(20, 55, 3));
Arguments.of(19.52, 15.98, 1.1));
}
@ParameterizedTest
@@ -254,20 +278,25 @@ class SimVisionSystemTest {
public void testDistanceCalc(double testDist, double testPitch, double testHeight) {
// Assume dist along ground and tgt height the same. Iterate over other parameters.
final var targetPose = new Pose2d(new Translation2d(35, 0), new Rotation2d(Math.PI / 42));
final var robotPose = new Pose2d(new Translation2d(35 - testDist, 0), new Rotation2d(0));
final var targetPose =
new Pose3d(new Translation3d(15.98, 0, 1), new Rotation3d(0, 0, Math.PI * 0.98));
final var robotPose =
new Pose3d(new Translation3d(15.98 - Units.feetToMeters(testDist), 0, 0), new Rotation3d());
final var robotToCamera =
new Transform3d(
new Translation3d(0, 0, Units.feetToMeters(testHeight)),
new Rotation3d(0, Units.degreesToRadians(testPitch), 0));
var sysUnderTest =
new SimVisionSystem(
"absurdlylongnamewhichshouldneveractuallyhappenbuteehwelltestitanywaysohowsyourdaygoingihopegoodhaveagreatrestofyourlife!",
160.0,
testPitch,
new Transform2d(),
testHeight,
robotToCamera.inverse(),
99999,
640,
480,
0);
sysUnderTest.addSimVisionTarget(new SimVisionTarget(targetPose, testDist, 0.5, 0.5));
sysUnderTest.addSimVisionTarget(new SimVisionTarget(targetPose, 0.5, 0.5, 0));
sysUnderTest.processFrame(robotPose);
var res = sysUnderTest.cam.getLatestResult();
@@ -276,34 +305,102 @@ class SimVisionSystemTest {
assertEquals(tgt.getYaw(), 0.0, 0.0001);
double distMeas =
PhotonUtils.calculateDistanceToTargetMeters(
testHeight,
testDist,
robotToCamera.getZ(),
targetPose.getZ(),
Units.degreesToRadians(testPitch),
Units.degreesToRadians(tgt.getPitch()));
assertEquals(distMeas, testDist, 0.001);
assertEquals(Units.feetToMeters(testDist), distMeas, 0.001);
}
@Test
public void testMultipleTargets() {
final var targetPoseL = new Pose2d(new Translation2d(35, 2), new Rotation2d());
final var targetPoseC = new Pose2d(new Translation2d(35, 0), new Rotation2d());
final var targetPoseR = new Pose2d(new Translation2d(35, -2), new Rotation2d());
var sysUnderTest =
new SimVisionSystem("Test", 160.0, 0.0, new Transform2d(), 5.0, 99999, 640, 480, 20.0);
final var targetPoseL =
new Pose3d(new Translation3d(15.98, 2, 0), new Rotation3d(0, 0, Math.PI));
final var targetPoseC =
new Pose3d(new Translation3d(15.98, 0, 0), new Rotation3d(0, 0, Math.PI));
final var targetPoseR =
new Pose3d(new Translation3d(15.98, -2, 0), new Rotation3d(0, 0, Math.PI));
var sysUnderTest = new SimVisionSystem("Test", 160.0, new Transform3d(), 99999, 640, 480, 20.0);
sysUnderTest.addSimVisionTarget(new SimVisionTarget(targetPoseL, 0.0, 0.25, 0.1));
sysUnderTest.addSimVisionTarget(new SimVisionTarget(targetPoseC, 1.0, 0.25, 0.1));
sysUnderTest.addSimVisionTarget(new SimVisionTarget(targetPoseR, 2.0, 0.25, 0.1));
sysUnderTest.addSimVisionTarget(new SimVisionTarget(targetPoseL, 3.0, 0.25, 0.1));
sysUnderTest.addSimVisionTarget(new SimVisionTarget(targetPoseC, 4.0, 0.25, 0.1));
sysUnderTest.addSimVisionTarget(new SimVisionTarget(targetPoseR, 5.0, 0.25, 0.1));
sysUnderTest.addSimVisionTarget(new SimVisionTarget(targetPoseL, 6.0, 0.25, 0.1));
sysUnderTest.addSimVisionTarget(new SimVisionTarget(targetPoseC, 7.0, 0.25, 0.1));
sysUnderTest.addSimVisionTarget(new SimVisionTarget(targetPoseL, 8.0, 0.25, 0.1));
sysUnderTest.addSimVisionTarget(new SimVisionTarget(targetPoseR, 9.0, 0.25, 0.1));
sysUnderTest.addSimVisionTarget(new SimVisionTarget(targetPoseL, 10.0, 0.25, 0.1));
sysUnderTest.addSimVisionTarget(
new SimVisionTarget(
targetPoseL.transformBy(
new Transform3d(new Translation3d(0, 0, 0.00), new Rotation3d())),
0.25,
0.25,
1));
sysUnderTest.addSimVisionTarget(
new SimVisionTarget(
targetPoseC.transformBy(
new Transform3d(new Translation3d(0, 0, 0.00), new Rotation3d())),
0.25,
0.25,
2));
sysUnderTest.addSimVisionTarget(
new SimVisionTarget(
targetPoseR.transformBy(
new Transform3d(new Translation3d(0, 0, 0.00), new Rotation3d())),
0.25,
0.25,
3));
sysUnderTest.addSimVisionTarget(
new SimVisionTarget(
targetPoseL.transformBy(
new Transform3d(new Translation3d(0, 0, 1.00), new Rotation3d())),
0.25,
0.25,
4));
sysUnderTest.addSimVisionTarget(
new SimVisionTarget(
targetPoseC.transformBy(
new Transform3d(new Translation3d(0, 0, 1.00), new Rotation3d())),
0.25,
0.25,
5));
sysUnderTest.addSimVisionTarget(
new SimVisionTarget(
targetPoseR.transformBy(
new Transform3d(new Translation3d(0, 0, 1.00), new Rotation3d())),
0.25,
0.25,
6));
sysUnderTest.addSimVisionTarget(
new SimVisionTarget(
targetPoseL.transformBy(
new Transform3d(new Translation3d(0, 0, 0.50), new Rotation3d())),
0.25,
0.25,
7));
sysUnderTest.addSimVisionTarget(
new SimVisionTarget(
targetPoseC.transformBy(
new Transform3d(new Translation3d(0, 0, 0.50), new Rotation3d())),
0.25,
0.25,
8));
sysUnderTest.addSimVisionTarget(
new SimVisionTarget(
targetPoseL.transformBy(
new Transform3d(new Translation3d(0, 0, 0.75), new Rotation3d())),
0.25,
0.25,
9));
sysUnderTest.addSimVisionTarget(
new SimVisionTarget(
targetPoseR.transformBy(
new Transform3d(new Translation3d(0, 0, 0.75), new Rotation3d())),
0.25,
0.25,
10));
sysUnderTest.addSimVisionTarget(
new SimVisionTarget(
targetPoseL.transformBy(
new Transform3d(new Translation3d(0, 0, 0.25), new Rotation3d())),
0.25,
0.25,
11));
var robotPose = new Pose2d(new Translation2d(30, 0), Rotation2d.fromDegrees(5));
var robotPose = new Pose2d(new Translation2d(6.0, 0), Rotation2d.fromDegrees(0.25));
sysUnderTest.processFrame(robotPose);
var res = sysUnderTest.cam.getLatestResult();
assertTrue(res.hasTargets());

View File

@@ -1,416 +0,0 @@
/*
* MIT License
*
* Copyright (c) 2022 PhotonVision
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
// So for now, with the new apriltag stuff, this is totally broken
// For now, commented out
/*
#include <networktables/NetworkTable.h>
#include <networktables/NetworkTableEntry.h>
#include <networktables/NetworkTableInstance.h>
#include <units/angle.h>
#include <units/length.h>
#include "gtest/gtest.h"
#include "photonlib/PhotonCamera.h"
#include "photonlib/PhotonUtils.h"
#include "photonlib/SimVisionSystem.h"
TEST(SimVisionSystemTest, Empty) {
photonlib::SimVisionSystem sysUnderTest("Test", 80.0_deg,
frc::Transform2d(), 1.0_m, 99999.0_m,
320, 240, 0.0);
for (int loopIdx = 0; loopIdx < 100; loopIdx++) {
sysUnderTest.ProcessFrame(frc::Pose2d());
}
}
class SimVisionSystemDistParamTest : public testing::TestWithParam<double> {};
INSTANTIATE_TEST_SUITE_P(SimVisionSystemDistParamTests,
SimVisionSystemDistParamTest,
testing::Values(5, 10, 15, 20, 25, 30));
TEST_P(SimVisionSystemDistParamTest, DistanceAligned) {
double dist = GetParam();
auto targetPose =
frc::Pose2d(frc::Translation2d(35_m, 0_m), frc::Rotation2d());
photonlib::SimVisionSystem sysUnderTest("Test", 80.0_deg,
frc::Transform2d(), 1.0_m, 99999.0_m,
320, 240, 0.0);
sysUnderTest.AddSimVisionTarget(
photonlib::SimVisionTarget(targetPose, 0.0_m, 1.0_m, 1.0_m));
auto robotPose = frc::Pose2d(
frc::Translation2d(units::meter_t(35.0 - dist), 0_m), frc::Rotation2d());
sysUnderTest.ProcessFrame(robotPose);
auto result = sysUnderTest.cam.GetLatestResult();
ASSERT_TRUE(result.HasTargets());
std::cout << "Best target pitch " <<
result.GetBestTarget().GetCameraToTarget().Translation().X().value();
ASSERT_EQ(result.GetBestTarget()
.GetCameraToTarget()
.Translation()
.Norm()
.value(),
dist);
}
TEST(SimVisionSystemTest, VisibilityCupidShuffle) {
auto targetPose =
frc::Pose2d(frc::Translation2d(35_m, 0_m), frc::Rotation2d());
photonlib::SimVisionSystem sysUnderTest("Test", 80.0_deg,
frc::Transform2d(), 1.0_m, 99999.0_m,
320, 240, 0.0);
sysUnderTest.AddSimVisionTarget(
photonlib::SimVisionTarget(targetPose, 0.0_m, 3.0_m, 3.0_m));
// To the right, to the right
auto robotPose =
frc::Pose2d(frc::Translation2d(5.0_m, 0.0_m), frc::Rotation2d(-70.0_deg));
sysUnderTest.ProcessFrame(robotPose);
auto result = sysUnderTest.cam.GetLatestResult();
EXPECT_FALSE(result.HasTargets());
// To the right, to the right
robotPose =
frc::Pose2d(frc::Translation2d(5.0_m, 0.0_m), frc::Rotation2d(-95.0_deg));
sysUnderTest.ProcessFrame(robotPose);
result = sysUnderTest.cam.GetLatestResult();
EXPECT_FALSE(result.HasTargets());
// To the left, to the left
robotPose =
frc::Pose2d(frc::Translation2d(5.0_m, 0.0_m), frc::Rotation2d(90.0_deg));
sysUnderTest.ProcessFrame(robotPose);
result = sysUnderTest.cam.GetLatestResult();
EXPECT_FALSE(result.HasTargets());
// To the left, to the left
robotPose =
frc::Pose2d(frc::Translation2d(5.0_m, 0.0_m), frc::Rotation2d(65.0_deg));
sysUnderTest.ProcessFrame(robotPose);
result = sysUnderTest.cam.GetLatestResult();
EXPECT_FALSE(result.HasTargets());
// now kick, now kick
robotPose =
frc::Pose2d(frc::Translation2d(2.0_m, 0.0_m), frc::Rotation2d(5.0_deg));
sysUnderTest.ProcessFrame(robotPose);
result = sysUnderTest.cam.GetLatestResult();
EXPECT_TRUE(result.HasTargets());
// now kick, now kick
robotPose =
frc::Pose2d(frc::Translation2d(2.0_m, 0.0_m), frc::Rotation2d(-5.0_deg));
sysUnderTest.ProcessFrame(robotPose);
result = sysUnderTest.cam.GetLatestResult();
EXPECT_TRUE(result.HasTargets());
// now walk it by yourself
robotPose = frc::Pose2d(frc::Translation2d(2.0_m, 0.0_m),
frc::Rotation2d(-179.0_deg));
sysUnderTest.ProcessFrame(robotPose);
result = sysUnderTest.cam.GetLatestResult();
EXPECT_FALSE(result.HasTargets());
// now walk it by yourself
sysUnderTest.MoveCamera(
frc::Transform2d(frc::Translation2d(), frc::Rotation2d(180_deg)), 0.0_m);
sysUnderTest.ProcessFrame(robotPose);
result = sysUnderTest.cam.GetLatestResult();
EXPECT_TRUE(result.HasTargets());
}
TEST(SimVisionSystemTest, NotVisibleVert1) {
auto targetPose =
frc::Pose2d(frc::Translation2d(35_m, 0_m), frc::Rotation2d());
photonlib::SimVisionSystem sysUnderTest("Test", 80.0_deg,
frc::Transform2d(), 1.0_m, 99999.0_m,
640, 480, 0.0);
sysUnderTest.AddSimVisionTarget(
photonlib::SimVisionTarget(targetPose, 1.0_m, 3.0_m, 2.0_m));
auto robotPose =
frc::Pose2d(frc::Translation2d(5.0_m, 0.0_m), frc::Rotation2d(5.0_deg));
sysUnderTest.ProcessFrame(robotPose);
auto result = sysUnderTest.cam.GetLatestResult();
ASSERT_TRUE(result.HasTargets());
sysUnderTest.MoveCamera(
frc::Transform2d(frc::Translation2d(), frc::Rotation2d(0_deg)), 5000.0_m);
sysUnderTest.ProcessFrame(robotPose);
result = sysUnderTest.cam.GetLatestResult();
EXPECT_FALSE(result.HasTargets());
}
TEST(SimVisionSystemTest, NotVisibleVert2) {
auto targetPose =
frc::Pose2d(frc::Translation2d(35_m, 0_m), frc::Rotation2d());
photonlib::SimVisionSystem sysUnderTest("Test", 80.0_deg,
frc::Transform2d(), 1.0_m, 99999.0_m,
1234, 1234, 0.5);
sysUnderTest.AddSimVisionTarget(
photonlib::SimVisionTarget(targetPose, 3.0_m, 0.5_m, 0.5_m));
auto robotPose =
frc::Pose2d(frc::Translation2d(32.0_m, 0.0_m), frc::Rotation2d(5.0_deg));
sysUnderTest.ProcessFrame(robotPose);
auto result = sysUnderTest.cam.GetLatestResult();
EXPECT_TRUE(result.HasTargets());
robotPose =
frc::Pose2d(frc::Translation2d(0.0_m, 0.0_m), frc::Rotation2d(5.0_deg));
sysUnderTest.ProcessFrame(robotPose);
result = sysUnderTest.cam.GetLatestResult();
EXPECT_FALSE(result.HasTargets());
}
TEST(SimVisionSystemTest, NotVisibleTgtSize) {
auto targetPose =
frc::Pose2d(frc::Translation2d(35_m, 0_m), frc::Rotation2d());
photonlib::SimVisionSystem sysUnderTest("Test", 80.0_deg,
frc::Transform2d(), 1.0_m, 99999.0_m,
640, 480, 20.0);
sysUnderTest.AddSimVisionTarget(
photonlib::SimVisionTarget(targetPose, 1.10_m, 0.25_m, 0.1_m));
auto robotPose =
frc::Pose2d(frc::Translation2d(32.0_m, 0.0_m), frc::Rotation2d(5.0_deg));
sysUnderTest.ProcessFrame(robotPose);
auto result = sysUnderTest.cam.GetLatestResult();
EXPECT_TRUE(result.HasTargets());
robotPose =
frc::Pose2d(frc::Translation2d(0.0_m, 0.0_m), frc::Rotation2d(5.0_deg));
sysUnderTest.ProcessFrame(robotPose);
result = sysUnderTest.cam.GetLatestResult();
EXPECT_FALSE(result.HasTargets());
}
TEST(SimVisionSystemTest, NotVisibleTooFarForLEDs) {
auto targetPose =
frc::Pose2d(frc::Translation2d(35_m, 0_m), frc::Rotation2d());
photonlib::SimVisionSystem sysUnderTest("Test", 80.0_deg,
frc::Transform2d(), 1.0_m, 10.0_m,
640, 480, 1.0);
sysUnderTest.AddSimVisionTarget(
photonlib::SimVisionTarget(targetPose, 1.10_m, 0.25_m, 0.1_m));
auto robotPose =
frc::Pose2d(frc::Translation2d(28.0_m, 0.0_m), frc::Rotation2d(5.0_deg));
sysUnderTest.ProcessFrame(robotPose);
auto result = sysUnderTest.cam.GetLatestResult();
EXPECT_TRUE(result.HasTargets());
robotPose =
frc::Pose2d(frc::Translation2d(0.0_m, 0.0_m), frc::Rotation2d(5.0_deg));
sysUnderTest.ProcessFrame(robotPose);
result = sysUnderTest.cam.GetLatestResult();
EXPECT_FALSE(result.HasTargets());
}
class SimVisionSystemYawParamTest : public testing::TestWithParam<double> {};
INSTANTIATE_TEST_SUITE_P(SimVisionSystemYawParamTests,
SimVisionSystemYawParamTest,
testing::Values(-10, -5, -0, -1, -2, 5, 7, 10.23));
TEST_P(SimVisionSystemYawParamTest, YawAngles) {
double testYaw = GetParam(); // Nope, Chuck testYaw
auto targetPose =
frc::Pose2d(frc::Translation2d(35_m, 0_m), frc::Rotation2d(45_deg));
photonlib::SimVisionSystem sysUnderTest("Test", 80.0_deg,
frc::Transform2d(), 1.0_m, 99999.0_m,
640, 480, 0.0);
sysUnderTest.AddSimVisionTarget(
photonlib::SimVisionTarget(targetPose, 0.0_m, 0.5_m, 0.5_m));
auto robotPose = frc::Pose2d(frc::Translation2d(32_m, 0.0_m),
frc::Rotation2d(units::degree_t(testYaw)));
sysUnderTest.ProcessFrame(robotPose);
auto result = sysUnderTest.cam.GetLatestResult();
ASSERT_TRUE(result.HasTargets());
auto tgt = result.GetBestTarget();
EXPECT_DOUBLE_EQ(tgt.GetYaw(), testYaw);
}
// class SimVisionSystemCameraPitchParamTest
// : public testing::TestWithParam<double> {};
// INSTANTIATE_TEST_SUITE_P(SimVisionSystemCameraPitchParamTests,
// SimVisionSystemCameraPitchParamTest,
// testing::Values(-10, -5, -0, -1, -2, 5, 7, 10.23,
// 20.21, -19.999));
// TEST_P(SimVisionSystemCameraPitchParamTest, CameraPitch) {
// double testPitch = GetParam();
// auto targetPose =
// frc::Pose2d(frc::Translation2d(35_m, 0_m), frc::Rotation2d(45_deg));
// auto robotPose =
// frc::Pose2d(frc::Translation2d(30_m, 0.0_m), frc::Rotation2d(0.0_deg));
// photonlib::SimVisionSystem sysUnderTest("Test", 80.0_deg,
// frc::Transform2d(), 1.0_m,
99999.0_m,
// 640, 480, 0.0);
// sysUnderTest.AddSimVisionTarget(
// photonlib::SimVisionTarget(targetPose, 0.0_m, 0.5_m, 0.5_m));
// sysUnderTest.MoveCamera(
// frc::Transform2d(frc::Translation2d(), frc::Rotation2d()), 0.0_m));
// photonlib::PhotonCamera::SetVersionCheckEnabled(false);
// sysUnderTest.ProcessFrame(robotPose);
// auto result = sysUnderTest.cam.GetLatestResult();
// ASSERT_TRUE(result.HasTargets());
// auto tgt = result.GetBestTarget();
// // If the camera is pitched down by 10 degrees, the target should appear
// // in the upper part of the image (ie, pitch positive). Therefor,
// // pass/fail involves -1.0.
// EXPECT_DOUBLE_EQ(tgt.GetPitch(), -testPitch);
// }
class SimVisionSystemDistCalcParamTest
: public testing::TestWithParam<std::tuple<double, double, double>> {};
INSTANTIATE_TEST_SUITE_P(
SimVisionSystemDistCalcParamTests, SimVisionSystemDistCalcParamTest,
testing::Values(std::tuple<double, double, double>(5, 35, 0),
std::tuple<double, double, double>(6, 35, 1),
std::tuple<double, double, double>(10, 35, 0),
std::tuple<double, double, double>(15, 35, 2),
std::tuple<double, double, double>(19.95, 35, 0),
std::tuple<double, double, double>(20, 35, 0),
std::tuple<double, double, double>(5, 42, 1),
std::tuple<double, double, double>(6, 42, 0),
std::tuple<double, double, double>(10, 42, 2),
std::tuple<double, double, double>(15, 42, 0.5),
std::tuple<double, double, double>(19.42, 35, 0),
std::tuple<double, double, double>(20, 42, 0),
std::tuple<double, double, double>(5, 55, 2),
std::tuple<double, double, double>(6, 55, 0),
std::tuple<double, double, double>(10, 54, 2.2),
std::tuple<double, double, double>(15, 53, 0),
std::tuple<double, double, double>(19.52, 35, 1.1),
std::tuple<double, double, double>(20, 51, 2.87),
std::tuple<double, double, double>(20, 55, 3)));
// TEST_P(SimVisionSystemDistCalcParamTest, DistanceCalc) {
// std::tuple<double, double, double> testArgs = GetParam();
// double testDist = std::get<0>(testArgs);
// double testPitch = std::get<1>(testArgs);
// double testHeight = std::get<2>(testArgs);
// auto targetPose = frc::Pose2d(frc::Translation2d(35_m, 0_m),
// frc::Rotation2d(units::radian_t(3.14159 /
// 42)));
// auto robotPose =
// frc::Pose2d(frc::Translation2d(units::meter_t(35 - testDist), 0.0_m),
// frc::Rotation2d(0.0_deg));
// photonlib::SimVisionSystem sysUnderTest(
// "absurdlylongnamewhichshouldneveractuallyhappenbuteehwelltestitanywaysoho"
// "wsyourdaygoingihopegoodhaveagreatrestofyourlife",
// 160.0_deg, units::degree_t(testPitch), frc::Transform2d(),
// units::meter_t(testHeight), 99999.0_m, 640, 480, 0.0);
// sysUnderTest.AddSimVisionTarget(photonlib::SimVisionTarget(
// targetPose, units::meter_t(testDist), 0.5_m, 0.5_m));
// sysUnderTest.ProcessFrame(robotPose);
// auto result = sysUnderTest.cam.GetLatestResult();
// ASSERT_TRUE(result.HasTargets());
// auto tgt = result.GetBestTarget();
// EXPECT_DOUBLE_EQ(tgt.GetYaw(), 0.0);
// units::meter_t distMeas =
// photonlib::PhotonUtils::CalculateDistanceToTarget(
// units::meter_t(testHeight), units::meter_t(testDist),
// units::degree_t(testPitch), units::degree_t(tgt.GetPitch()));
// EXPECT_DOUBLE_EQ(distMeas.value(), testDist);
// }
TEST(SimVisionSystemTest, MultipleTargets) {
auto targetPoseL =
frc::Pose2d(frc::Translation2d(35_m, 2_m), frc::Rotation2d());
auto targetPoseC =
frc::Pose2d(frc::Translation2d(35_m, 0_m), frc::Rotation2d());
auto targetPoseR =
frc::Pose2d(frc::Translation2d(35_m, -2_m), frc::Rotation2d());
photonlib::SimVisionSystem sysUnderTest("test", 160.0_deg,
frc::Transform2d(), 5.0_m, 99999.0_m,
640, 480, 20.0);
sysUnderTest.AddSimVisionTarget(
photonlib::SimVisionTarget(targetPoseL, 0.0_m, 0.25_m, 0.1_m));
sysUnderTest.AddSimVisionTarget(
photonlib::SimVisionTarget(targetPoseC, 1.0_m, 0.25_m, 0.1_m));
sysUnderTest.AddSimVisionTarget(
photonlib::SimVisionTarget(targetPoseR, 2.0_m, 0.25_m, 0.1_m));
sysUnderTest.AddSimVisionTarget(
photonlib::SimVisionTarget(targetPoseL, 3.0_m, 0.25_m, 0.1_m));
sysUnderTest.AddSimVisionTarget(
photonlib::SimVisionTarget(targetPoseC, 4.0_m, 0.25_m, 0.1_m));
sysUnderTest.AddSimVisionTarget(
photonlib::SimVisionTarget(targetPoseR, 5.0_m, 0.25_m, 0.1_m));
sysUnderTest.AddSimVisionTarget(
photonlib::SimVisionTarget(targetPoseL, 6.0_m, 0.25_m, 0.1_m));
sysUnderTest.AddSimVisionTarget(
photonlib::SimVisionTarget(targetPoseC, 7.0_m, 0.25_m, 0.1_m));
sysUnderTest.AddSimVisionTarget(
photonlib::SimVisionTarget(targetPoseL, 8.0_m, 0.25_m, 0.1_m));
sysUnderTest.AddSimVisionTarget(
photonlib::SimVisionTarget(targetPoseR, 9.0_m, 0.25_m, 0.1_m));
sysUnderTest.AddSimVisionTarget(
photonlib::SimVisionTarget(targetPoseL, 10.0_m, 0.25_m, 0.1_m));
auto robotPose =
frc::Pose2d(frc::Translation2d(30_m, 0.0_m), frc::Rotation2d(5.0_deg));
sysUnderTest.ProcessFrame(robotPose);
auto result = sysUnderTest.cam.GetLatestResult();
ASSERT_TRUE(result.HasTargets());
auto tgtList = result.GetTargets();
EXPECT_EQ(11ul, tgtList.size());
}
*/

View File

@@ -43,9 +43,16 @@ task copyClientUIToResources(type: Copy) {
into "${projectDir}/src/main/resources/web/"
}
task copyThinclientToResources(type: Copy) {
from "${projectDir}/../photon-thinclient/"
into "${projectDir}/src/main/resources/web/"
}
task buildAndCopyUI {}
buildAndCopyUI.dependsOn copyClientUIToResources
buildAndCopyUI.dependsOn copyThinclientToResources
copyClientUIToResources.dependsOn runNpmOnClient
copyClientUIToResources.shouldRunAfter runNpmOnClient

View File

@@ -0,0 +1,140 @@
/*
* Copyright (C) Photon Vision.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.photonvision.server;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.javalin.websocket.WsBinaryMessageContext;
import io.javalin.websocket.WsCloseContext;
import io.javalin.websocket.WsConnectContext;
import io.javalin.websocket.WsContext;
import io.javalin.websocket.WsMessageContext;
import java.util.HashMap;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
import org.photonvision.common.logging.LogGroup;
import org.photonvision.common.logging.Logger;
import org.photonvision.vision.videoStream.SocketVideoStreamManager;
public class CameraSocketHandler {
private final Logger logger = new Logger(CameraSocketHandler.class, LogGroup.WebServer);
private final List<WsContext> users = new CopyOnWriteArrayList<>();
private final SocketVideoStreamManager svsManager = SocketVideoStreamManager.getInstance();
private Thread cameraBroadcastThread;
public static class UIMap extends HashMap<String, Object> {}
private static class ThreadSafeSingleton {
private static final CameraSocketHandler INSTANCE = new CameraSocketHandler();
}
public static CameraSocketHandler getInstance() {
return CameraSocketHandler.ThreadSafeSingleton.INSTANCE;
}
private CameraSocketHandler() {
cameraBroadcastThread = new Thread(this::broadcastFramesTask);
cameraBroadcastThread.setPriority(2); // fairly low priority
cameraBroadcastThread.start();
}
public void onConnect(WsConnectContext context) {
context.session.setIdleTimeout(Long.MAX_VALUE); // TODO: determine better value
var insa = context.session.getRemote().getInetSocketAddress();
var host = insa.getAddress().toString() + ":" + insa.getPort();
logger.info("New camera websocket connection from " + host);
users.add(context);
}
protected void onClose(WsCloseContext context) {
var insa = context.session.getRemote().getInetSocketAddress();
var host = insa.getAddress().toString() + ":" + insa.getPort();
var reason = context.reason() != null ? context.reason() : "Connection closed by client";
logger.info("Closing camera websocket connection from " + host + " for reason: " + reason);
svsManager.removeSubscription(context);
users.remove(context);
}
@SuppressWarnings({"unchecked"})
public void onMessage(WsMessageContext context) {
var messageStr = context.message();
ObjectMapper mapper = new ObjectMapper();
try {
JsonNode actualObj = mapper.readTree(messageStr);
try {
var entryCmd = actualObj.get("cmd").asText();
var socketMessageType = CameraSocketMessageType.fromEntryKey(entryCmd);
logger.trace(() -> "Got Camera WS message: [" + socketMessageType + "]");
if (socketMessageType == null) {
logger.warn("Got unknown socket message command: " + entryCmd);
}
switch (socketMessageType) {
case CSMT_SUBSCRIBE:
{
int portId = actualObj.get("port").asInt();
svsManager.addSubscription(context, portId);
break;
}
case CSMT_UNSUBSCRIBE:
{
svsManager.removeSubscription(context);
break;
}
}
} catch (Exception e) {
logger.error("Failed to parse message!", e);
}
} catch (JsonProcessingException e) {
logger.warn("Could not parse message \"" + messageStr + "\"");
e.printStackTrace();
return;
}
}
@SuppressWarnings({"unchecked"})
public void onBinaryMessage(WsBinaryMessageContext context) {
return; // ignoring binary messages for now
}
private void broadcastFramesTask() {
// Background camera image broadcasting thread
while (!Thread.currentThread().isInterrupted()) {
svsManager.allStreamConvertNextFrame();
try {
Thread.sleep(1);
} catch (InterruptedException e) {
logger.error("Exception waiting for camera stream broadcast semaphore", e);
}
for (var user : users) {
var sendBytes = svsManager.getSendFrame(user);
if (sendBytes != null) {
user.send(sendBytes);
}
}
}
}
}

View File

@@ -0,0 +1,46 @@
/*
* Copyright (C) Photon Vision.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.photonvision.server;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.Map;
@SuppressWarnings("unused")
public enum CameraSocketMessageType {
CSMT_SUBSCRIBE("subscribe"),
CSMT_UNSUBSCRIBE("unsubscribe");
public final String entryKey;
CameraSocketMessageType(String entryKey) {
this.entryKey = entryKey;
}
private static final Map<String, CameraSocketMessageType> entryKeyToValueMap = new HashMap<>();
static {
for (var value : EnumSet.allOf(CameraSocketMessageType.class)) {
entryKeyToValueMap.put(value.entryKey, value);
}
}
public static CameraSocketMessageType fromEntryKey(String entryKey) {
return entryKeyToValueMap.get(entryKey);
}
}

View File

@@ -42,8 +42,8 @@ import org.photonvision.common.logging.Logger;
import org.photonvision.vision.pipeline.PipelineType;
@SuppressWarnings("rawtypes")
public class SocketHandler {
private final Logger logger = new Logger(SocketHandler.class, LogGroup.WebServer);
public class DataSocketHandler {
private final Logger logger = new Logger(DataSocketHandler.class, LogGroup.WebServer);
private final List<WsContext> users = new CopyOnWriteArrayList<>();
private final ObjectMapper objectMapper = new ObjectMapper(new MessagePackFactory());
private final DataChangeService dcService = DataChangeService.getInstance();
@@ -54,14 +54,14 @@ public class SocketHandler {
public static class UIMap extends HashMap<String, Object> {}
private static class ThreadSafeSingleton {
private static final SocketHandler INSTANCE = new SocketHandler();
private static final DataSocketHandler INSTANCE = new DataSocketHandler();
}
public static SocketHandler getInstance() {
return SocketHandler.ThreadSafeSingleton.INSTANCE;
public static DataSocketHandler getInstance() {
return DataSocketHandler.ThreadSafeSingleton.INSTANCE;
}
private SocketHandler() {
private DataSocketHandler() {
dcService.addSubscribers(
uiOutboundSubscriber,
new UIInboundSubscriber()); // Subscribe outgoing messages to the data change service
@@ -84,19 +84,6 @@ public class SocketHandler {
var reason = context.reason() != null ? context.reason() : "Connection closed by client";
logger.info("Closing websocket connection from " + host + " for reason: " + reason);
users.remove(context);
if (users.size() == 0) {
logger.info("All websocket connections are closed. Setting inputShouldShow to false.");
// cameraIndex -1 means the event is received by all cameras
dcService.publishEvent(
new IncomingWebSocketEvent<>(
DataChangeDestination.DCD_ACTIVEPIPELINESETTINGS,
"inputShouldShow",
false,
-1,
null));
}
}
@SuppressWarnings({"unchecked"})
@@ -117,7 +104,7 @@ public class SocketHandler {
try {
var entryKey = entry.getKey();
var entryValue = entry.getValue();
var socketMessageType = SocketMessageType.fromEntryKey(entryKey);
var socketMessageType = DataSocketMessageType.fromEntryKey(entryKey);
logger.trace(
() ->

View File

@@ -22,7 +22,7 @@ import java.util.HashMap;
import java.util.Map;
@SuppressWarnings("unused")
public enum SocketMessageType {
public enum DataSocketMessageType {
SMT_DRIVERMODE("driverMode"),
SMT_CHANGECAMERANAME("changeCameraName"),
SMT_CHANGEPIPELINENAME("changePipelineName"),
@@ -40,19 +40,19 @@ public enum SocketMessageType {
public final String entryKey;
SocketMessageType(String entryKey) {
DataSocketMessageType(String entryKey) {
this.entryKey = entryKey;
}
private static final Map<String, SocketMessageType> entryKeyToValueMap = new HashMap<>();
private static final Map<String, DataSocketMessageType> entryKeyToValueMap = new HashMap<>();
static {
for (var value : EnumSet.allOf(SocketMessageType.class)) {
for (var value : EnumSet.allOf(DataSocketMessageType.class)) {
entryKeyToValueMap.put(value.entryKey, value);
}
}
public static SocketMessageType fromEntryKey(String entryKey) {
public static DataSocketMessageType fromEntryKey(String entryKey) {
return entryKeyToValueMap.get(entryKey);
}
}

View File

@@ -19,7 +19,6 @@ package org.photonvision.server;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import edu.wpi.first.math.geometry.Rotation2d;
import io.javalin.http.Context;
import java.io.File;
import java.io.FileInputStream;
@@ -173,16 +172,12 @@ public class RequestHandler {
var settings = (HashMap<String, Object>) settingsAndIndex.get("settings");
int index = (Integer) settingsAndIndex.get("index");
// The only settings we actually care about are FOV and pitch
// The only settings we actually care about are FOV
var fov = Double.parseDouble(settings.get("fov").toString());
var pitch =
Rotation2d.fromDegrees(Double.parseDouble(settings.get("tiltDegrees").toString()));
logger.info(
String.format(
"Setting camera %s's fov to %s w/pitch %s", index, fov, pitch.getDegrees()));
logger.info(String.format("Setting camera %s's fov to %s", index, fov));
var module = VisionModuleManager.getInstance().getModule(index);
module.setFovAndPitch(fov, pitch);
module.setFov(fov);
module.saveModule();
} catch (JsonProcessingException e) {
logger.error("Got invalid camera setting JSON from frontend!");

View File

@@ -61,15 +61,24 @@ public class Server {
})));
});
var socketHandler = SocketHandler.getInstance();
/*Web Socket Events */
/*Web Socket Events for Data Exchage */
var dsHandler = DataSocketHandler.getInstance();
app.ws(
"/websocket",
"/websocket_data",
ws -> {
ws.onConnect(socketHandler::onConnect);
ws.onClose(socketHandler::onClose);
ws.onBinaryMessage(socketHandler::onBinaryMessage);
ws.onConnect(dsHandler::onConnect);
ws.onClose(dsHandler::onClose);
ws.onBinaryMessage(dsHandler::onBinaryMessage);
});
/*Web Socket Events for Camera Streaming */
var camDsHandler = CameraSocketHandler.getInstance();
app.ws(
"/websocket_cameras",
ws -> {
ws.onConnect(camDsHandler::onConnect);
ws.onClose(camDsHandler::onClose);
ws.onBinaryMessage(camDsHandler::onBinaryMessage);
ws.onMessage(camDsHandler::onMessage);
});
/*API Events*/
app.post("/api/settings/import", RequestHandler::onSettingUpload);

View File

@@ -35,9 +35,9 @@ import org.photonvision.common.logging.Logger;
class UIOutboundSubscriber extends DataChangeSubscriber {
Logger logger = new Logger(UIOutboundSubscriber.class, LogGroup.WebServer);
private final SocketHandler socketHandler;
private final DataSocketHandler socketHandler;
public UIOutboundSubscriber(SocketHandler socketHandler) {
public UIOutboundSubscriber(DataSocketHandler socketHandler) {
super(DataChangeSource.AllSources, Collections.singletonList(DataChangeDestination.DCD_UI));
this.socketHandler = socketHandler;
}

View File

@@ -1 +0,0 @@
<p>Docs have not been copied!</p>

View File

@@ -1 +1 @@
<!DOCTYPE html><html lang=en><head><meta charset=utf-8><meta http-equiv=X-UA-Compatible content="IE=edge"><meta name=viewport content="width=device-width,initial-scale=1"><link rel=icon href=/favicon.png><title>PhotonVision</title><link href=/js/chunk-2d216214.4302abb0.js rel=prefetch><link href=/js/chunk-2d216257.0de4b8cc.js rel=prefetch><link href=/js/chunk-ad0384ec.7dafafc8.js rel=prefetch><link href=/css/app.a6eec7e2.css rel=preload as=style><link href=/css/chunk-vendors.db45c662.css rel=preload as=style><link href=/js/app.612f1f56.js rel=preload as=script><link href=/js/chunk-vendors.58a768b6.js rel=preload as=script><link href=/css/chunk-vendors.db45c662.css rel=stylesheet><link href=/css/app.a6eec7e2.css rel=stylesheet></head><body><noscript><strong>We're sorry but PhotonVision doesn't work properly without JavaScript enabled. Please enable it to continue.</strong></noscript><div id=app></div><script src=/js/chunk-vendors.58a768b6.js></script><script src=/js/app.612f1f56.js></script></body></html>
<p>UI has not been copied!</p>

View File

@@ -69,7 +69,7 @@ public class AprilTagTest {
TestUtils.showImage(pipelineResult.inputFrame.image.getMat(), "Pipeline output", 999999);
// these numbers are not *accurate*, but they are known and expected
var pose = pipelineResult.targets.get(0).getCameraToTarget3d();
var pose = pipelineResult.targets.get(0).getBestCameraToTarget3d();
Assertions.assertEquals(2, pose.getTranslation().getX(), 0.2);
Assertions.assertEquals(0.0, pose.getTranslation().getY(), 0.2);
Assertions.assertEquals(0.0, pose.getTranslation().getY(), 0.2);
@@ -79,15 +79,14 @@ public class AprilTagTest {
var objZ = new Translation3d(0, 0, 1).rotateBy(pose.getRotation()).getX();
System.out.printf("Object x %.2f y %.2f z %.2f\n", objX, objY, objZ);
// We expect the object X axis to be to the right, or negative-Y in world space
// We expect the object X to be forward, or -X in world space
Assertions.assertEquals(
-1, new Translation3d(1, 0, 0).rotateBy(pose.getRotation()).getY(), 0.08);
// We expect the object Y axis to be up, or +Z in world space
-1, new Translation3d(1, 0, 0).rotateBy(pose.getRotation()).getX(), 0.1);
// We expect the object Y axis to be right, or negative-Y in world space
Assertions.assertEquals(
1, new Translation3d(0, 1, 0).rotateBy(pose.getRotation()).getZ(), 0.08);
// We expect the object Z axis to towards the camera, or negative-X in world space
Assertions.assertEquals(
-1, new Translation3d(0, 0, 1).rotateBy(pose.getRotation()).getX(), 0.08);
-1, new Translation3d(0, 1, 0).rotateBy(pose.getRotation()).getY(), 0.1);
// We expect the object Z axis to be up, or +Z in world space
Assertions.assertEquals(1, new Translation3d(0, 0, 1).rotateBy(pose.getRotation()).getZ(), 0.1);
}
private static void printTestResults(CVPipelineResult pipelineResult) {
@@ -98,7 +97,7 @@ public class AprilTagTest {
System.out.println(
"Found targets at "
+ pipelineResult.targets.stream()
.map(TrackedTarget::getCameraToTarget3d)
.map(TrackedTarget::getBestCameraToTarget3d)
.collect(Collectors.toList()));
}
}

View File

@@ -1,5 +1,7 @@
apply plugin: "java"
apply from: "${rootDir}/shared/common.gradle"
sourceCompatibility = 11
targetCompatibility = 11

View File

@@ -17,6 +17,10 @@
package edu.wpi.first.math.geometry;
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import edu.wpi.first.math.MatBuilder;
import edu.wpi.first.math.Matrix;
import edu.wpi.first.math.Nat;
@@ -27,6 +31,8 @@ import edu.wpi.first.math.numbers.N3;
import java.util.Objects;
/** Represents a 3D pose containing translational and rotational elements. */
@JsonIgnoreProperties(ignoreUnknown = true)
@JsonAutoDetect(getterVisibility = JsonAutoDetect.Visibility.NONE)
public class Pose3d implements Interpolatable<Pose3d> {
private final Translation3d m_translation;
private final Rotation3d m_rotation;
@@ -43,11 +49,13 @@ public class Pose3d implements Interpolatable<Pose3d> {
* @param translation The translational component of the pose.
* @param rotation The rotational component of the pose.
*/
public Pose3d(Translation3d translation, Rotation3d rotation) {
@JsonCreator
public Pose3d(
@JsonProperty(required = true, value = "translation") Translation3d translation,
@JsonProperty(required = true, value = "rotation") Rotation3d rotation) {
m_translation = translation;
m_rotation = rotation;
}
/**
* Constructs a pose with x, y, and z translations instead of a separate Translation3d.
*
@@ -91,6 +99,7 @@ public class Pose3d implements Interpolatable<Pose3d> {
*
* @return The translational component of the pose.
*/
@JsonProperty
public Translation3d getTranslation() {
return m_translation;
}
@@ -127,6 +136,7 @@ public class Pose3d implements Interpolatable<Pose3d> {
*
* @return The rotational component of the pose.
*/
@JsonProperty
public Rotation3d getRotation() {
return m_rotation;
}

View File

@@ -17,22 +17,25 @@
package edu.wpi.first.math.geometry;
import edu.wpi.first.math.MatBuilder;
import edu.wpi.first.math.Matrix;
import edu.wpi.first.math.Nat;
import edu.wpi.first.math.numbers.N1;
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import edu.wpi.first.math.VecBuilder;
import edu.wpi.first.math.Vector;
import edu.wpi.first.math.numbers.N3;
import java.util.Arrays;
import java.util.Objects;
@JsonIgnoreProperties(ignoreUnknown = true)
@JsonAutoDetect(getterVisibility = JsonAutoDetect.Visibility.NONE)
public class Quaternion {
private final double m_r;
private final Matrix<N3, N1> m_v;
private final Vector<N3> m_v;
/** Constructs a quaternion with a default angle of 0 degrees. */
public Quaternion() {
m_r = 1.0;
m_v = new MatBuilder<>(Nat.N3(), Nat.N1()).fill(0.0, 0.0, 0.0);
m_v = VecBuilder.fill(0.0, 0.0, 0.0);
}
/**
@@ -43,9 +46,14 @@ public class Quaternion {
* @param y Y component of the quaternion.
* @param z Z component of the quaternion.
*/
public Quaternion(double w, double x, double y, double z) {
@JsonCreator
public Quaternion(
@JsonProperty(required = true, value = "W") double w,
@JsonProperty(required = true, value = "X") double x,
@JsonProperty(required = true, value = "Y") double y,
@JsonProperty(required = true, value = "Z") double z) {
m_r = w;
m_v = new MatBuilder<>(Nat.N3(), Nat.N1()).fill(x, y, z);
m_v = VecBuilder.fill(x, y, z);
}
/**
@@ -61,21 +69,23 @@ public class Quaternion {
final var r2 = other.m_r;
final var v2 = other.m_v;
final var v1x = v1.get(0, 0);
final var v1y = v1.get(1, 0);
final var v1z = v1.get(2, 0);
final var v2x = v2.get(0, 0);
final var v2y = v2.get(1, 0);
final var v2z = v2.get(2, 0);
// v₁ x v₂
var cross =
new MatBuilder<>(Nat.N3(), Nat.N1())
.fill(v1y * v2z - v2y * v1z, v2x * v1z - v1x * v2z, v1x * v2y - v2x * v1y);
double dot = v1x * v2x + v1y * v2y + v1z * v2z;
VecBuilder.fill(
v1.get(1, 0) * v2.get(2, 0) - v2.get(1, 0) * v1.get(2, 0),
v2.get(0, 0) * v1.get(2, 0) - v1.get(0, 0) * v2.get(2, 0),
v1.get(0, 0) * v2.get(1, 0) - v2.get(0, 0) * v1.get(1, 0));
// v = r₁v₂ + r₂v₁ + v₁ x v₂
final var v = v2.times(r1).plus(v1.times(r2)).plus(cross);
return new Quaternion(r1 * r2 - dot, v.get(0, 0), v.get(1, 0), v.get(2, 0));
return new Quaternion(r1 * r2 - v1.dot(v2), v.get(0, 0), v.get(1, 0), v.get(2, 0));
}
@Override
public String toString() {
return String.format(
"Quaternion(%s, %s, %s, %s)", m_r, m_v.get(0, 0), m_v.get(1, 0), m_v.get(2, 0));
}
/**
@@ -89,20 +99,7 @@ public class Quaternion {
if (obj instanceof Quaternion) {
var other = (Quaternion) obj;
final var r1 = m_r;
final var v1 = m_v;
final var r2 = other.m_r;
final var v2 = other.m_v;
final var v1x = v1.get(0, 0);
final var v1y = v1.get(1, 0);
final var v1z = v1.get(2, 0);
final var v2x = v2.get(0, 0);
final var v2y = v2.get(1, 0);
final var v2z = v2.get(2, 0);
return Math.abs(r1 * r2 + v1x * v2x + v1y * v2y + v1z * v2z) > 1.0 - 1E-9;
return Math.abs(m_r * other.m_r + m_v.dot(other.m_v)) > 1.0 - 1E-9;
}
return false;
}
@@ -140,6 +137,7 @@ public class Quaternion {
*
* @return W component of the quaternion.
*/
@JsonProperty(value = "W")
public double getW() {
return m_r;
}
@@ -149,6 +147,7 @@ public class Quaternion {
*
* @return X component of the quaternion.
*/
@JsonProperty(value = "X")
public double getX() {
return m_v.get(0, 0);
}
@@ -158,6 +157,7 @@ public class Quaternion {
*
* @return Y component of the quaternion.
*/
@JsonProperty(value = "Y")
public double getY() {
return m_v.get(1, 0);
}
@@ -167,12 +167,33 @@ public class Quaternion {
*
* @return Z component of the quaternion.
*/
@JsonProperty(value = "Z")
public double getZ() {
return m_v.get(2, 0);
}
@Override
public String toString() {
return "Quaternion{" + "m_r=" + m_r + ", m_v=" + Arrays.toString(m_v.getData()) + '}';
/**
* Returns the rotation vector representation of this quaternion.
*
* <p>This is also the log operator of SO(3).
*
* @return The rotation vector representation of this quaternion.
*/
public Vector<N3> toRotationVector() {
// See equation (31) in "Integrating Generic Sensor Fusion Algorithms with
// Sound State Representation through Encapsulation of Manifolds"
//
// https://arxiv.org/pdf/1107.1119.pdf
double norm = m_v.norm();
if (norm < 1e-9) {
return m_v.times(2.0 / getW() - 2.0 / 3.0 * norm * norm / (getW() * getW() * getW()));
} else {
if (getW() < 0.0) {
return m_v.times(2.0 * Math.atan2(-norm, -getW()) / norm);
} else {
return m_v.times(2.0 * Math.atan2(norm, getW()) / norm);
}
}
}
}

View File

@@ -17,6 +17,10 @@
package edu.wpi.first.math.geometry;
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import edu.wpi.first.math.MathUtil;
import edu.wpi.first.math.Matrix;
import edu.wpi.first.math.VecBuilder;
@@ -25,7 +29,9 @@ import edu.wpi.first.math.interpolation.Interpolatable;
import edu.wpi.first.math.numbers.N3;
import java.util.Objects;
/** A rotation in a 3D coordinate. */
/** A rotation in a 3D coordinate frame represented by a quaternion. */
@JsonIgnoreProperties(ignoreUnknown = true)
@JsonAutoDetect(getterVisibility = JsonAutoDetect.Visibility.NONE)
public class Rotation3d implements Interpolatable<Rotation3d> {
private Quaternion m_q = new Quaternion();
@@ -37,7 +43,8 @@ public class Rotation3d implements Interpolatable<Rotation3d> {
*
* @param q The quaternion.
*/
public Rotation3d(Quaternion q) {
@JsonCreator
public Rotation3d(@JsonProperty(required = true, value = "quaternion") Quaternion q) {
m_q = q.normalize();
}
@@ -174,6 +181,7 @@ public class Rotation3d implements Interpolatable<Rotation3d> {
*
* @return The quaternion representation of the Rotation3d.
*/
@JsonProperty(value = "quaternion")
public Quaternion getQuaternion() {
return m_q;
}

View File

@@ -17,6 +17,10 @@
package edu.wpi.first.math.geometry;
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import edu.wpi.first.math.MathUtil;
import edu.wpi.first.math.interpolation.Interpolatable;
import java.util.Objects;
@@ -28,7 +32,8 @@ import java.util.Objects;
* origin facing in the positive X direction, forward is positive X, left is positive Y, and up is
* positive Z.
*/
@SuppressWarnings({"ParameterName", "MemberName"})
@JsonIgnoreProperties(ignoreUnknown = true)
@JsonAutoDetect(getterVisibility = JsonAutoDetect.Visibility.NONE)
public class Translation3d implements Interpolatable<Translation3d> {
private final double m_x;
private final double m_y;
@@ -46,7 +51,11 @@ public class Translation3d implements Interpolatable<Translation3d> {
* @param y The y component of the translation.
* @param z The z component of the translation.
*/
public Translation3d(double x, double y, double z) {
@JsonCreator
public Translation3d(
@JsonProperty(required = true, value = "x") double x,
@JsonProperty(required = true, value = "y") double y,
@JsonProperty(required = true, value = "z") double z) {
m_x = x;
m_y = y;
m_z = z;
@@ -84,6 +93,7 @@ public class Translation3d implements Interpolatable<Translation3d> {
*
* @return The X component of the translation.
*/
@JsonProperty
public double getX() {
return m_x;
}
@@ -93,6 +103,7 @@ public class Translation3d implements Interpolatable<Translation3d> {
*
* @return The Y component of the translation.
*/
@JsonProperty
public double getY() {
return m_y;
}
@@ -102,6 +113,7 @@ public class Translation3d implements Interpolatable<Translation3d> {
*
* @return The Z component of the translation.
*/
@JsonProperty
public double getZ() {
return m_z;
}

View File

@@ -32,6 +32,9 @@ public class PhotonPipelineResult {
// Latency in milliseconds.
private double latencyMillis;
// Timestamp in milliseconds.
private double timestampSeconds = -1;
/** Constructs an empty pipeline result. */
public PhotonPipelineResult() {}
@@ -83,6 +86,25 @@ public class PhotonPipelineResult {
return latencyMillis;
}
/**
* Returns the estimated time the frame was taken, This is more accurate than using <code>
* getLatencyMillis()</code>
*
* @return The timestamp in seconds, or -1 if this result has no timestamp set.
*/
public double getTimestampSeconds() {
return timestampSeconds;
}
/**
* Sets the FPGA timestamp of this result in seconds.
*
* @param timestampSeconds The timestamp in seconds.
*/
public void setTimestampSeconds(double timestampSeconds) {
this.timestampSeconds = timestampSeconds;
}
/**
* Returns whether the pipeline has targets.
*

View File

@@ -27,14 +27,15 @@ import java.util.Objects;
import org.photonvision.common.dataflow.structures.Packet;
public class PhotonTrackedTarget {
public static final int PACK_SIZE_BYTES = Double.BYTES * (5 + 7 + 2 * 4 + 1);
public static final int PACK_SIZE_BYTES = Double.BYTES * (5 + 7 + 2 * 4 + 1 + 7);
private double yaw;
private double pitch;
private double area;
private double skew;
private int fiducialId;
private Transform3d cameraToTarget = new Transform3d();
private Transform3d bestCameraToTarget = new Transform3d();
private Transform3d altCameraToTarget = new Transform3d();
private double poseAmbiguity;
private List<TargetCorner> targetCorners;
@@ -48,6 +49,7 @@ public class PhotonTrackedTarget {
double skew,
int id,
Transform3d pose,
Transform3d altPose,
double ambiguity,
List<TargetCorner> corners) {
assert corners.size() == 4;
@@ -56,7 +58,8 @@ public class PhotonTrackedTarget {
this.area = area;
this.skew = skew;
this.fiducialId = id;
this.cameraToTarget = pose;
this.bestCameraToTarget = pose;
this.altCameraToTarget = altPose;
this.targetCorners = corners;
this.poseAmbiguity = ambiguity;
}
@@ -100,10 +103,18 @@ public class PhotonTrackedTarget {
/**
* Get the transform that maps camera space (X = forward, Y = left, Z = up) to object/fiducial tag
* space (X right, Y up, Z towards the camera/out of the wall)
* space (X forward, Y left, Z up) with the lowest reprojection error
*/
public Transform3d getCameraToTarget() {
return cameraToTarget;
public Transform3d getBestCameraToTarget() {
return bestCameraToTarget;
}
/**
* Get the transform that maps camera space (X = forward, Y = left, Z = up) to object/fiducial tag
* space (X forward, Y left, Z up) with the highest reprojection error
*/
public Transform3d getAlternateCameraToTarget() {
return altCameraToTarget;
}
@Override
@@ -114,13 +125,37 @@ public class PhotonTrackedTarget {
return Double.compare(that.yaw, yaw) == 0
&& Double.compare(that.pitch, pitch) == 0
&& Double.compare(that.area, area) == 0
&& Objects.equals(cameraToTarget, that.cameraToTarget)
&& Objects.equals(bestCameraToTarget, that.bestCameraToTarget)
&& Objects.equals(altCameraToTarget, that.altCameraToTarget)
&& Objects.equals(targetCorners, that.targetCorners);
}
@Override
public int hashCode() {
return Objects.hash(yaw, pitch, area, cameraToTarget);
return Objects.hash(yaw, pitch, area, bestCameraToTarget, altCameraToTarget);
}
private static Transform3d decodeTransform(Packet packet) {
double x = packet.decodeDouble();
double y = packet.decodeDouble();
double z = packet.decodeDouble();
var translation = new Translation3d(x, y, z);
double w = packet.decodeDouble();
x = packet.decodeDouble();
y = packet.decodeDouble();
z = packet.decodeDouble();
var rotation = new Rotation3d(new Quaternion(w, x, y, z));
return new Transform3d(translation, rotation);
}
private static void encodeTransform(Packet packet, Transform3d transform) {
packet.encode(transform.getTranslation().getX());
packet.encode(transform.getTranslation().getY());
packet.encode(transform.getTranslation().getZ());
packet.encode(transform.getRotation().getQuaternion().getW());
packet.encode(transform.getRotation().getQuaternion().getX());
packet.encode(transform.getRotation().getQuaternion().getY());
packet.encode(transform.getRotation().getQuaternion().getZ());
}
/**
@@ -136,16 +171,9 @@ public class PhotonTrackedTarget {
this.skew = packet.decodeDouble();
this.fiducialId = packet.decodeInt();
double x = packet.decodeDouble();
double y = packet.decodeDouble();
double z = packet.decodeDouble();
var translation = new Translation3d(x, y, z);
double w = packet.decodeDouble();
x = packet.decodeDouble();
y = packet.decodeDouble();
z = packet.decodeDouble();
var rotation = new Rotation3d(new Quaternion(w, x, y, z));
this.cameraToTarget = new Transform3d(translation, rotation);
this.bestCameraToTarget = decodeTransform(packet);
this.altCameraToTarget = decodeTransform(packet);
this.poseAmbiguity = packet.decodeDouble();
this.targetCorners = new ArrayList<>(4);
@@ -170,13 +198,8 @@ public class PhotonTrackedTarget {
packet.encode(area);
packet.encode(skew);
packet.encode(fiducialId);
packet.encode(cameraToTarget.getTranslation().getX());
packet.encode(cameraToTarget.getTranslation().getY());
packet.encode(cameraToTarget.getTranslation().getZ());
packet.encode(cameraToTarget.getRotation().getQuaternion().getW());
packet.encode(cameraToTarget.getRotation().getQuaternion().getX());
packet.encode(cameraToTarget.getRotation().getQuaternion().getY());
packet.encode(cameraToTarget.getRotation().getQuaternion().getZ());
encodeTransform(packet, bestCameraToTarget);
encodeTransform(packet, altCameraToTarget);
packet.encode(poseAmbiguity);
for (int i = 0; i < 4; i++) {
@@ -201,7 +224,7 @@ public class PhotonTrackedTarget {
+ ", fiducialId="
+ fiducialId
+ ", cameraToTarget="
+ cameraToTarget
+ bestCameraToTarget
+ ", targetCorners="
+ targetCorners
+ '}';

View File

@@ -35,16 +35,4 @@
"dependencies": [],
"foldername": "aimandrange"
}
{
"name": "SimAimAndRange",
"description": "Adding Simulation Support to the Aim And Range example",
"tags": [],
"gradlebase": "cpp",
"language": "cpp",
"commandversion": 1,
"mainclass": "Main",
"packagetoreplace": null,
"dependencies": [],
"foldername": "simaimandrange"
}
]

View File

@@ -1,73 +0,0 @@
/*
* MIT License
*
* Copyright (c) 2022 PhotonVision
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
#include "Robot.h"
#include <photonlib/PhotonUtils.h>
void Robot::TeleopPeriodic() {
double forwardSpeed;
double rotationSpeed;
if (xboxController.GetAButton()) {
// Vision-alignment mode
// Query the latest result from PhotonVision
const auto& result = camera.GetLatestResult();
if (result.HasTargets()) {
// First calculate range
units::meter_t range = photonlib::PhotonUtils::CalculateDistanceToTarget(
CAMERA_HEIGHT, TARGET_HEIGHT, CAMERA_PITCH,
units::degree_t{result.GetBestTarget().GetPitch()});
// Use this range as the measurement we give to the PID controller.
// -1.0 required to ensure positive PID controller effort _increases_
// range
forwardSpeed = -forwardController.Calculate(range.value(),
GOAL_RANGE_METERS.value());
// Also calculate angular power
// -1.0 required to ensure positive PID controller effort _increases_ yaw
rotationSpeed =
-turnController.Calculate(result.GetBestTarget().GetYaw(), 0);
} else {
// If we have no targets, stay still.
forwardSpeed = 0;
rotationSpeed = 0;
}
} else {
// Manual Driver Mode
forwardSpeed = -xboxController.GetRightY();
rotationSpeed = xboxController.GetLeftX();
}
// Use our forward/turn speeds to control the drivetrain
drive.ArcadeDrive(forwardSpeed, rotationSpeed);
}
void Robot::SimulationPeriodic() { dtSim.update(); }
#ifndef RUNNING_FRC_TESTS
int main() { return frc::StartRobot<Robot>(); }
#endif

View File

@@ -1,50 +0,0 @@
/*
* MIT License
*
* Copyright (c) 2022 PhotonVision
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
#include "DrivetrainSim.h"
/**
* Perform all periodic drivetrain simulation related tasks to advance our
* simulation of robot physics forward by a single 20ms step.
*/
void DrivetrainSim::update() {
double leftMotorCmd = 0;
double rightMotorCmd = 0;
if (frc::DriverStation::IsEnabled() &&
!frc::RobotController::IsBrownedOut()) {
leftMotorCmd = leftLeader.GetSpeed();
rightMotorCmd = rightLeader.GetSpeed();
}
m_drivetrainSimulator.SetInputs(
units::volt_t(leftMotorCmd * frc::RobotController::GetInputVoltage()),
units::volt_t(-rightMotorCmd * frc::RobotController::GetInputVoltage()));
m_drivetrainSimulator.Update(20_ms);
// Update PhotonVision based on our new robot position.
simVision.ProcessFrame(m_drivetrainSimulator.GetPose());
field.SetRobotPose(m_drivetrainSimulator.GetPose());
}

View File

@@ -1,111 +0,0 @@
/*
* MIT License
*
* Copyright (c) 2022 PhotonVision
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
#include <photonlib/SimVisionSystem.h>
#include <frc/DriverStation.h>
#include <frc/RobotController.h>
#include <frc/geometry/Pose2d.h>
#include <frc/geometry/Rotation2d.h>
#include <frc/geometry/Translation2d.h>
#include <frc/simulation/DifferentialDrivetrainSim.h>
#include <frc/simulation/PWMSim.h>
#include <frc/smartdashboard/Field2d.h>
#include <frc/smartdashboard/SmartDashboard.h>
#include <frc/system/plant/LinearSystemId.h>
#include <units/angle.h>
#include <units/length.h>
#include <units/time.h>
#pragma once
class DrivetrainSim {
public:
DrivetrainSim() {
simVision.AddSimVisionTarget(photonlib::SimVisionTarget(
farTargetPose, 81.91_in, targetWidth, targetHeight));
frc::SmartDashboard::PutData("Field", &field);
}
void update();
private:
// Simulated Motor Controllers
frc::sim::PWMSim leftLeader{0};
frc::sim::PWMSim rightLeader{1};
// Simulation Physics
// Configure these to match your drivetrain's physical dimensions
// and characterization results.
static constexpr decltype(1_V / 1_mps) kv = 1.98 * 1_V * 1_s / 1_m;
static constexpr decltype(1_V / 1_mps_sq) ka = 0.2 * 1_V * 1_s * 1_s / 1_m;
static constexpr decltype(1_V / 1_rad_per_s) kvAngular =
1.5 * 1_V * 1_s / 1_rad;
static constexpr decltype(1_V / 1_rad_per_s_sq) kaAngular =
0.3 * 1_V * 1_s * 1_s / 1_rad;
static constexpr units::meter_t kTrackWidth = 1_m;
const frc::LinearSystem<2, 2, 2> kDrivetrainPlant =
frc::LinearSystemId::IdentifyDrivetrainSystem(kv, ka, kvAngular,
kaAngular, kTrackWidth);
frc::sim::DifferentialDrivetrainSim m_drivetrainSimulator{
kDrivetrainPlant, 2.0_ft,
frc::DCMotor::CIM(2), 8.0,
6.0_in / 2, {0.001, 0.001, 0.0001, 0.1, 0.1, 0.005, 0.005}};
// Simulated Vision System.
// Configure these to match your PhotonVision Camera,
// pipeline, and LED setup.
units::degree_t camDiagFOV = 170.0_deg; // assume wide-angle camera
units::meter_t camHeightOffGround = 24_in;
units::meter_t maxLEDRange = 20_m;
int camResolutionWidth = 640; // pixels
int camResolutionHeight = 480; // pixels
double minTargetArea = 10; // square pixels
photonlib::SimVisionSystem simVision{"photonvision", camDiagFOV,
frc::Transform2d{}, camHeightOffGround,
maxLEDRange, camResolutionWidth,
camResolutionHeight, minTargetArea};
// See
// https://firstfrc.blob.core.windows.net/frc2020/PlayingField/2020FieldDrawing-SeasonSpecific.pdf
// page 208
const units::meter_t targetWidth = 41.30_in - 6.70_in;
// See
// https://firstfrc.blob.core.windows.net/frc2020/PlayingField/2020FieldDrawing-SeasonSpecific.pdf
// page 197
const units::meter_t targetHeight = 98.19_in - 81.19_in; // meters
// See
// https://firstfrc.blob.core.windows.net/frc2020/PlayingField/LayoutandMarkingDiagram.pdf
// pages 4 and 5
const units::meter_t tgtXPos = 54_ft;
const units::meter_t tgtYPos = (27.0_ft / 2) - 43.75_in - (48.0_in / 2.0);
const frc::Translation2d targetTrans{tgtXPos, tgtYPos};
const frc::Rotation2d targetRot{0.0_deg};
frc::Pose2d farTargetPose{targetTrans, targetRot};
frc::Field2d field{};
};

View File

@@ -1,77 +0,0 @@
/*
* MIT License
*
* Copyright (c) 2022 PhotonVision
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
#pragma once
#include <photonlib/PhotonCamera.h>
#include <frc/TimedRobot.h>
#include <frc/XboxController.h>
#include <frc/controller/PIDController.h>
#include <frc/drive/DifferentialDrive.h>
#include <frc/motorcontrol/PWMVictorSPX.h>
#include <units/angle.h>
#include <units/length.h>
#include "DrivetrainSim.h"
class Robot : public frc::TimedRobot {
public:
void TeleopPeriodic() override;
void SimulationPeriodic() override;
private:
// Constants such as camera and target height stored. Change per robot and
// goal!
const units::meter_t CAMERA_HEIGHT = 24_in;
const units::meter_t TARGET_HEIGHT = 81.19_in;
// Angle between horizontal and the camera.
const units::radian_t CAMERA_PITCH = 15_deg;
// How far from the target we want to be
const units::meter_t GOAL_RANGE_METERS = 10_ft;
// PID constants should be tuned per robot
const double LINEAR_P = 2.0;
const double LINEAR_D = 0.0;
frc2::PIDController forwardController{LINEAR_P, 0.0, LINEAR_D};
const double ANGULAR_P = 0.03;
const double ANGULAR_D = 0.003;
frc2::PIDController turnController{ANGULAR_P, 0.0, ANGULAR_D};
// Change this to match the name of your camera
photonlib::PhotonCamera camera{"photonvision"};
frc::XboxController xboxController{0};
// Drive motors
frc::PWMVictorSPX leftMotor{0};
frc::PWMVictorSPX rightMotor{1};
frc::DifferentialDrive drive{leftMotor, rightMotor};
// Simulation Support
DrivetrainSim dtSim{};
};

View File

@@ -25,9 +25,10 @@
package org.photonlib.examples.simaimandrange.sim;
import edu.wpi.first.math.geometry.Pose2d;
import edu.wpi.first.math.geometry.Rotation2d;
import edu.wpi.first.math.geometry.Transform2d;
import edu.wpi.first.math.geometry.Translation2d;
import edu.wpi.first.math.geometry.Pose3d;
import edu.wpi.first.math.geometry.Rotation3d;
import edu.wpi.first.math.geometry.Transform3d;
import edu.wpi.first.math.geometry.Translation3d;
import edu.wpi.first.math.numbers.N2;
import edu.wpi.first.math.system.LinearSystem;
import edu.wpi.first.math.system.plant.DCMotor;
@@ -73,7 +74,7 @@ public class DrivetrainSim {
// Configure these to match your PhotonVision Camera,
// pipeline, and LED setup.
double camDiagFOV = 170.0; // degrees - assume wide-angle camera
double camPitch = Units.radiansToDegrees(Robot.CAMERA_PITCH_RADIANS); // degrees
double camPitch = Robot.CAMERA_PITCH_RADIANS; // degrees
double camHeightOffGround = Robot.CAMERA_HEIGHT_METERS; // meters
double maxLEDRange = 20; // meters
int camResolutionWidth = 640; // pixels
@@ -84,9 +85,8 @@ public class DrivetrainSim {
new SimVisionSystem(
"photonvision",
camDiagFOV,
camPitch,
new Transform2d(),
camHeightOffGround,
new Transform3d(
new Translation3d(0, 0, camHeightOffGround), new Rotation3d(0, camPitch, 0)),
maxLEDRange,
camResolutionWidth,
camResolutionHeight,
@@ -105,13 +105,15 @@ public class DrivetrainSim {
double tgtXPos = Units.feetToMeters(54);
double tgtYPos =
Units.feetToMeters(27 / 2) - Units.inchesToMeters(43.75) - Units.inchesToMeters(48.0 / 2.0);
Pose2d farTargetPose = new Pose2d(new Translation2d(tgtXPos, tgtYPos), new Rotation2d(0.0));
Pose3d farTargetPose =
new Pose3d(
new Translation3d(tgtXPos, tgtYPos, Robot.TARGET_HEIGHT_METERS),
new Rotation3d(0.0, 0.0, 0.0));
Field2d field = new Field2d();
public DrivetrainSim() {
simVision.addSimVisionTarget(
new SimVisionTarget(farTargetPose, Robot.TARGET_HEIGHT_METERS, targetWidth, targetHeight));
simVision.addSimVisionTarget(new SimVisionTarget(farTargetPose, targetWidth, targetHeight, -1));
SmartDashboard.putData("Field", field);
}

View File

@@ -24,10 +24,10 @@
package org.photonlib.examples.simposeest.robot;
import edu.wpi.first.math.geometry.Pose2d;
import edu.wpi.first.math.geometry.Rotation2d;
import edu.wpi.first.math.geometry.Transform2d;
import edu.wpi.first.math.geometry.Translation2d;
import edu.wpi.first.math.geometry.Pose3d;
import edu.wpi.first.math.geometry.Rotation3d;
import edu.wpi.first.math.geometry.Transform3d;
import edu.wpi.first.math.geometry.Translation3d;
import edu.wpi.first.math.util.Units;
import org.photonvision.SimVisionTarget;
@@ -71,10 +71,10 @@ public class Constants {
// Physical location of the camera on the robot, relative to the center of the
// robot.
public static final Transform2d kCameraToRobot =
new Transform2d(
new Translation2d(-0.25, 0), // in meters
new Rotation2d());
public static final Transform3d kCameraToRobot =
new Transform3d(
new Translation3d(-0.25, 0, -.25), // in meters
new Rotation3d());
// See
// https://firstfrc.blob.core.windows.net/frc2020/PlayingField/2020FieldDrawing-SeasonSpecific.pdf
@@ -87,16 +87,20 @@ public class Constants {
// page 197
public static final double targetHeight =
Units.inchesToMeters(98.19) - Units.inchesToMeters(81.19); // meters
public static final double targetHeightAboveGround = Units.inchesToMeters(81.19); // meters
// See https://firstfrc.blob.core.windows.net/frc2020/PlayingField/LayoutandMarkingDiagram.pdf
// pages 4 and 5
public static final double kFarTgtXPos = Units.feetToMeters(54);
public static final double kFarTgtYPos =
Units.feetToMeters(27 / 2) - Units.inchesToMeters(43.75) - Units.inchesToMeters(48.0 / 2.0);
public static final Pose2d kFarTargetPose =
new Pose2d(new Translation2d(kFarTgtXPos, kFarTgtYPos), new Rotation2d(0.0));
public static final double kFarTgtZPos =
(Units.inchesToMeters(98.19) - targetHeight) / 2 + targetHeight;
public static final Pose3d kFarTargetPose =
new Pose3d(
new Translation3d(kFarTgtXPos, kFarTgtYPos, kFarTgtZPos),
new Rotation3d(0.0, 0.0, Units.degreesToRadians(180)));
public static final SimVisionTarget kFarTarget =
new SimVisionTarget(kFarTargetPose, targetHeightAboveGround, targetWidth, targetHeight);
new SimVisionTarget(kFarTargetPose, targetWidth, targetHeight, 42);
}

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