Compare commits

...

36 Commits

Author SHA1 Message Date
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
Chris Gerth
c827afb25f 3d viewer cleanup (#490)
* WIP fiddling with 3js stuff for different viewpoints

* more wip viewer cleanup

* More cleanups - split out minimap
2022-10-09 20:26:49 -07:00
Matt
87e7c3ca74 [Wip] Add auto exposure switch (#488)
* Add auto exposure switch

* Run wpiformat

* Update ZeroCopyPicamSource.java
2022-10-09 21:41:40 -05:00
Chris Gerth
4d5904dd6d Stream content reorg. (#489)
Revised stream and target draw logic to divide the streams by "Raw" and "Processed" and only draw the results on the "Processed" stream.

Should allow for input sterams to be recorded for raw camera input, and output for debug info.
2022-10-09 21:30:16 -04:00
Avery Black
9bf589ebc6 Disable auto focus on USB cameras by default (#487)
* Disable auto focus on USB cameras by default

* Remove extra log

* Implement camera quirk for auto focus

* Spotless apply
2022-10-09 17:49:58 -04:00
Σx
1e4a92c71f Calculate and Report FOV from Calibration Coefficients (#486) 2022-10-08 23:08:57 -04:00
Matt
4ad9d97508 Fix AprilTag rotation reversal bug (#482)
Applies base rotation to apriltags to match solvepnp base rotation
2022-10-08 09:27:27 -04:00
Matt
2c6b0ddac3 Expose pose ambiguity (#483)
* Expose pose ambiguity

* Run spotless

* Add tooltips and expose number of iterations
2022-10-08 09:27:00 -04:00
shueja-personal
dafee954e0 Draw3dTargetsPipe returns immediately if coeffs are null (previously NPE crashlooped) (#485)
* Draw3dTargetsPipe returns immediately if coeffs are null

* fix lint
2022-10-08 09:26:37 -04:00
86 changed files with 2052 additions and 574 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)

View File

@@ -19,8 +19,7 @@
"three-full": "^28.0.2",
"vue": "^2.6.12",
"vue-axios": "^2.1.5",
"vue-native-websocket": "git+https://github.com/PhotonVision/vue-native-websocket.git#5189f29",
"vue-router": "^3.4.3",
"vue-native-websocket": "git+https://github.com/PhotonVision/vue-native-websocket.git#5189f29", "vue-router": "^3.4.3",
"vuetify": "^2.3.10",
"vuex": "^3.5.1"
},
@@ -25271,8 +25270,7 @@
},
"vue-native-websocket": {
"version": "git+ssh://git@github.com/PhotonVision/vue-native-websocket.git#7a327918e03b215b6899b0d648c5130ece1fa912",
"from": "vue-native-websocket@git+https://github.com/PhotonVision/vue-native-websocket.git#7a32791"
},
"from": "vue-native-websocket@git+https://github.com/PhotonVision/vue-native-websocket.git#7a32791" },
"vue-router": {
"version": "3.4.3"
},

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,7 +13,7 @@
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,
@@ -46,18 +46,48 @@
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.stopStream();
} else {
this.wsStream.startStream();
}
}
},
mounted() {
this.reload(); // Force reload image on creation
var wsvs = require('../../plugins/WebsocketVideoStream');
this.wsStream = new wsvs.WebsocketVideoStream(this.id, this.port, window.location.host);
},
unmounted() {
this.wsStream.stopStream();
this.wsStream.ws_close();
},
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

@@ -1,11 +1,14 @@
<template>
<div id="MapContainer" style="flex-grow:1">
<div
id="MapContainer"
style="flex-grow:1"
>
<v-row>
<v-col
align="center"
cols="12"
>
<span class="white--text" >Target Location</span>
<span class="white--text">Target Location</span>
</v-col>
</v-row>
<v-row>
@@ -14,8 +17,31 @@
cols="12"
align-self="stretch"
>
<canvas id="canvasId" style="width:100%;height:100%"/>
<canvas
id="canvasId"
style="width:100%;height:100%"
/>
</v-col>
<v-row>
<v-col>
<v-btn
class="ml-10"
color="secondary"
@click="resetCamFirstPerson"
>
First Person
</v-btn>
</v-col>
<v-col>
<v-btn
class="ml-10"
color="secondary"
@click="resetCamThirdPerson"
>
Third Person
</v-btn>
</v-col>
</v-row>
</v-row>
</div>
</template>
@@ -25,6 +51,7 @@
import {
ArrowHelper,
BoxGeometry,
ConeGeometry,
Mesh,
MeshNormalMaterial,
PerspectiveCamera,
@@ -59,13 +86,102 @@ export default {
}
},
},
mounted() {
const scene = new Scene();
this.scene = scene;
const camera = new PerspectiveCamera(75, 800 / 800, 0.1, 1000);
this.camera = camera;
const canvas = document.getElementById("canvasId"); // getting the canvas element
this.canvas = canvas;
const renderer = new WebGLRenderer({"canvas": canvas});
this.renderer = renderer;
scene.background = new Color(0xa9a9a9)
//Set up resize handlers
this.onWindowResize();
window.addEventListener( 'resize', this.onWindowResize, false );
//Add the reference frame cues
this.refFrameCues = []
// coordinate system
this.refFrameCues.push(new ArrowHelper(new Vector3(1, 0, 0).normalize(), new Vector3(0, 0, 0),
1, // length
0xff0000,
0.1,
0.1,
))
this.refFrameCues.push(new ArrowHelper(new Vector3(0, 1, 0).normalize(), new Vector3(0, 0, 0),
1, // length
0x00ff00,
0.1,
0.1,
))
this.refFrameCues.push(new ArrowHelper(new Vector3(0, 0, 1).normalize(), new Vector3(0, 0, 0),
1, // length
0x0000ff,
0.1,
0.1,
))
//something that looks vaguely like a camera
const camSize = 0.2;
const camBodyGeometry = new BoxGeometry(camSize, camSize, camSize);
const camLensGeometry = new ConeGeometry(camSize*0.4, camSize*0.8, 30);
const camMaterial = new MeshNormalMaterial();
const camBody = new Mesh(camBodyGeometry, camMaterial);
const camLens = new Mesh(camLensGeometry, camMaterial);
camBody.position.set(0,0,0);
camLens.rotateZ(Math.PI / 2);
camLens.position.set(camSize*0.8,0,0);
this.refFrameCues.push(camBody)
this.refFrameCues.push(camLens)
var controls = new TrackballControls(
camera,
renderer.domElement
);
controls.rotateSpeed = 1.0;
controls.zoomSpeed = 1.2;
controls.panSpeed = 0.8;
controls.noZoom = false;
controls.noPan = false;
controls.staticMoving = true;
controls.dynamicDampingFactor = 0.3;
controls.keys = [65, 83, 68];
this.controls = controls;
this.scene.add(...this.refFrameCues)
this.resetCamFirstPerson();
controls.update();
function animate() {
requestAnimationFrame(animate);
controls.update();
renderer.render(scene, camera);
//camera.updateMatrixWorld();
//console.log("================")
//console.log(camera.position);
//console.log(camera.rotation);
//console.log(camera.up);
}
this.drawTargets()
animate();
},
methods: {
drawTargets() {
this.scene.remove(...this.cubes)
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,
@@ -113,6 +229,7 @@ export default {
if(this.cubes.length > 0)
this.scene.add(...this.cubes);
},
onWindowResize() {
var container = document.getElementById("MapContainer")
if(container){
@@ -123,72 +240,24 @@ export default {
this.renderer.setSize( this.canvas.width, this.canvas.height );
}
},
},
mounted() {
const scene = new Scene();
this.scene = scene;
const camera = new PerspectiveCamera(75, 800 / 800, 0.1, 1000);
this.camera = camera;
const canvas = document.getElementById("canvasId"); // getting the canvas element
this.canvas = canvas;
const renderer = new WebGLRenderer({"canvas": canvas});
this.renderer = renderer;
scene.background = new Color(0xa9a9a9)
this.onWindowResize();
window.addEventListener( 'resize', this.onWindowResize, false );
scene.add(new ArrowHelper(new Vector3(1, 0, 0).normalize(), new Vector3(0, 0, 0),
1, // length
0xff0000,
0.5,
0.5,
))
scene.add(new ArrowHelper(new Vector3(0, 1, 0).normalize(), new Vector3(0, 0, 0),
1, // length
0x00ff00,
0.5,
0.5,
))
scene.add(new ArrowHelper(new Vector3(0, 0, 1).normalize(), new Vector3(0, 0, 0),
1, // length
0x0000ff,
0.5,
0.5,
))
var controls = new TrackballControls(
camera,
renderer.domElement
);
controls.rotateSpeed = 1.0;
controls.zoomSpeed = 1.2;
controls.panSpeed = 0.8;
controls.noZoom = false;
controls.noPan = false;
controls.staticMoving = true;
controls.dynamicDampingFactor = 0.3;
controls.keys = [65, 83, 68];
camera.position.set(-0.1,0,0);
camera.rotation.set(-90, 0, 90);
camera.up.set(0,0,1);
controls.update();
function animate() {
requestAnimationFrame(animate);
controls.update();
renderer.render(scene, camera);
}
this.drawTargets()
animate();
resetCamThirdPerson(){
//Sets camera to third person position
this.controls.reset();
this.camera.position.set(-1.39,-1.09,1.17);
this.camera.up.set(0,0,1);
this.controls.target.set(4.0,0.0,0.0);
this.controls.update();
this.scene.add(...this.refFrameCues)
},
resetCamFirstPerson(){
//Sets camera to first person position
this.controls.reset();
this.camera.position.set(-0.1,0,0);
this.camera.up.set(0,0,1);
this.controls.target.set(0.0,0.0,0.0);
this.controls.update();
this.scene.remove(...this.refFrameCues)
},
}

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,148 @@
export class WebsocketVideoStream{
constructor(drawDiv, streamPort, host) {
this.drawDiv = drawDiv;
this.image = document.getElementById(this.drawDiv);
this.streamPort = streamPort;
this.serverAddr = "ws://" + host + "/websocket_cameras";
this.noStream = false;
this.noStreamPrev = false;
this.setNoStream();
this.ws_connect();
this.imgData = null;
this.imgDataTime = -1;
this.imgObjURL = null;
this.frameRxCount = 0;
requestAnimationFrame(()=>this.animationLoop());
}
animationLoop(){
var now = window.performance.now();
if((now - this.imgDataTime) > 2500 && this.imgData != null){
//Handle websocket send timeouts by restarting
this.setNoStream();
this.stopStream();
setTimeout(this.startStream.bind(this), 1000); //restart stream one second later
} else {
if(this.streamPort == null){
this.setNoStream();
} else if (this.imgData != null) {
//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;
this.noStream = false;
} else {
//Nothing, hold previous image while waiting for next frame
}
}
requestAnimationFrame(()=>this.animationLoop());
}
setNoStream() {
this.noStreamPrev = this.noStream;
this.noStream = true;
if(this.noStreamPrev == false && this.noStream == true){
//One-shot background change to preserve animation
this.image.src = require("../assets/loading.gif");
}
}
startStream() {
if(this.serverConnectionActive == true && this.streamPort > 0){
this.ws.send(JSON.stringify({"cmd": "subscribe", "port":this.streamPort}));
this.noStream = false;
}
}
stopStream() {
if(this.serverConnectionActive == true && this.streamPort > 0){
this.ws.send(JSON.stringify({"cmd": "unsubscribe"}));
this.noStream = true;
}
}
setPort(streamPort){
this.stopStream();
this.frameRxCount = 0;
this.streamPort = streamPort;
this.startStream();
}
ws_onOpen() {
// Set the flag allowing general server communication
this.serverConnectionActive = true;
console.log("Connected!");
this.startStream();
}
ws_onClose(e) {
this.setNoStream();
//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 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.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: [
{
@@ -57,6 +57,7 @@ export default new Vuex.Store({
// Settings that apply to all pipeline types
cameraExposure: 1,
cameraBrightness: 2,
cameraAutoExposure: false,
cameraRedGain: 3,
cameraBlueGain: 4,
inputImageRotationMode: 0,
@@ -94,7 +95,8 @@ export default new Vuex.Store({
blur: 0.0,
threads: 1,
debug: false,
refineEdges: true
refineEdges: true,
numIterations: 1,
}
}
],

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
@@ -146,6 +138,24 @@
text="Standard Deviation"
/>
</th>
<th class="text-center">
<tooltipped-label
tooltip="Estimated Horizontal FOV, in degrees"
text="Horizontal FOV"
/>
</th>
<th class="text-center">
<tooltipped-label
tooltip="Estimated Vertical FOV, in degrees"
text="Vertical FOV"
/>
</th>
<th class="text-center">
<tooltipped-label
tooltip="Estimated Diagonal FOV, in degrees"
text="Diagonal FOV"
/>
</th>
</tr>
</thead>
<tbody>
@@ -158,6 +168,9 @@
{{ isCalibrated(value) ? value.mean.toFixed(2) + "px" : "—" }}
</td>
<td> {{ isCalibrated(value) ? value.standardDeviation.toFixed(2) + "px" : "—" }} </td>
<td> {{ isCalibrated(value) ? value.horizontalFOV.toFixed(2) + "°" : "—" }} </td>
<td> {{ isCalibrated(value) ? value.verticalFOV.toFixed(2) + "°" : "—" }} </td>
<td> {{ isCalibrated(value) ? value.diagonalFOV.toFixed(2) + "°" : "—" }} </td>
</tr>
</tbody>
</v-simple-table>
@@ -181,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
@@ -195,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"
@@ -268,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;"
@@ -339,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";
@@ -351,6 +376,7 @@ export default {
CVselect,
CVnumberinput,
CVslider,
CVswitch,
CVimage
},
data() {
@@ -396,6 +422,9 @@ export default {
if (calib != null) {
it['standardDeviation'] = calib.standardDeviation;
it['mean'] = calib.perViewErrors.reduce((a, b) => a + b) / calib.perViewErrors.length;
it['horizontalFOV'] = 2 * Math.atan2(it.width/2,calib.intrinsics[0]) * (180/Math.PI);
it['verticalFOV'] = 2 * Math.atan2(it.height/2,calib.intrinsics[4]) * (180/Math.PI);
it['diagonalFOV'] = 2 * Math.atan2(Math.sqrt(it.width**2 + (it.height/(calib.intrinsics[4]/calib.intrinsics[0]))**2)/2,calib.intrinsics[0]) * (180/Math.PI);
}
filtered.push(it);
}
@@ -404,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;
@@ -419,7 +446,6 @@ export default {
this.$store.commit('cameraSettings', value);
}
},
boardType: {
get() {
return this.calibrationData.boardType
@@ -601,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;
}
@@ -623,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

@@ -36,7 +36,7 @@
<span class="pr-1">{{ Math.round($store.state.pipelineResults.fps) }}&nbsp;FPS &ndash;</span>
<span v-if="!fpsTooLow">{{ Math.min(Math.round($store.state.pipelineResults.latency), 9999) }} ms latency</span>
<span v-else-if="!$store.getters.currentPipelineSettings.inputShouldShow">HSV thresholds are too broad; narrow them for better performance</span>
<span v-else>stop viewing the color stream for better performance</span>
<span v-else>stop viewing the raw stream for better performance</span>
</v-chip>
<v-switch
v-model="driverMode"
@@ -58,16 +58,16 @@
>
<div style="position: relative; width: 100%; height: 100%;">
<cv-image
:id="idx === 0 ? 'normal-stream' : ''"
:id="idx === 0 ? 'raw-stream' : 'processed-stream'"
ref="streams"
:address="$store.getters.streamAddress[idx]"
:idx=idx
:disconnected="!$store.state.backendConnected"
scale="100"
:max-height="$store.getters.isDriverMode ? '40vh' : '300px'"
:max-height-md="$store.getters.isDriverMode ? '50vh' : '380px'"
:max-height-lg="$store.getters.isDriverMode ? '55vh' : '390px'"
:max-height-xl="$store.getters.isDriverMode ? '60vh' : '450px'"
:alt="'Stream' + idx"
:alt="'Stream ' + idx"
:color-picking="$store.state.colorPicking && idx === 0"
@click="onImageClick"
/>
@@ -85,7 +85,7 @@
<v-card
color="primary"
>
<camera-and-pipeline-select @camera-name-changed="reloadStreams" />
<camera-and-pipeline-select />
</v-card>
<v-card
:disabled="$store.getters.isDriverMode || $store.state.colorPicking"
@@ -136,15 +136,15 @@
color="secondary"
class="fill"
>
<v-icon>mdi-palette</v-icon>
<span>Normal</span>
<v-icon>mdi-import</v-icon>
<span>Raw</span>
</v-btn>
<v-btn
color="secondary"
class="fill"
>
<v-icon>mdi-compare</v-icon>
<span>Threshold</span>
<v-icon>mdi-export</v-icon>
<span>Processed</span>
</v-btn>
</v-btn-toggle>
</v-col>
@@ -261,6 +261,7 @@ import ThresholdTab from './PipelineViews/ThresholdTab';
import ContoursTab from './PipelineViews/ContoursTab';
import OutputTab from './PipelineViews/OutputTab';
import TargetsTab from "./PipelineViews/TargetsTab";
import Map3DTab from './PipelineViews/Map3DTab';
import PnPTab from './PipelineViews/PnPTab';
import AprilTagTab from './PipelineViews/AprilTagTab';
@@ -274,6 +275,7 @@ export default {
ContoursTab,
OutputTab,
TargetsTab,
Map3DTab,
PnPTab,
AprilTagTab,
},
@@ -319,12 +321,16 @@ export default {
component: "OutputTab",
},
targets: {
name: "Target Info",
name: "Targets",
component: "TargetsTab",
},
pnp: {
name: "3D",
name: "PnP",
component: "PnPTab",
},
map3d: {
name: "3D",
component: "Map3DTab",
}
};
@@ -341,18 +347,18 @@ export default {
} else if (this.$vuetify.breakpoint.mdAndDown || !this.$store.state.compactMode) {
// Two tab groups, one with "input, threshold, contours, output" and the other with "target info, 3D"
ret[0] = [tabs.input, tabs.threshold, tabs.contours, tabs.apriltag, tabs.output];
ret[1] = [tabs.targets, tabs.pnp];
ret[1] = [tabs.targets, tabs.pnp, tabs.map3d];
} else if (this.$vuetify.breakpoint.lgAndDown) {
// Three tab groups, one with "input", one with "threshold, contours, output", and the other with "target info, 3D"
ret[0] = [tabs.input];
ret[1] = [tabs.threshold, tabs.contours, tabs.apriltag, tabs.output];
ret[2] = [tabs.targets, tabs.pnp];
ret[2] = [tabs.targets, tabs.pnp, tabs.map3d];
} else if (this.$vuetify.breakpoint.xl) {
// Three tab groups, one with "input", one with "threshold, contours", and the other with "output, target info, 3D"
ret[0] = [tabs.input];
ret[1] = [tabs.threshold];
ret[2] = [tabs.contours, tabs.apriltag, tabs.output];
ret[3] = [tabs.targets, tabs.pnp];
ret[3] = [tabs.targets, tabs.pnp, tabs.map3d];
}
for(let i = 0; i < ret.length; i++) {
@@ -360,10 +366,11 @@ export default {
// All the tabs we allow
const filteredGroup = group.filter(it =>
!(!allow3d && it.name === "3D")
&& !(isAprilTag && (it.name === "Threshold"))
&& !(isAprilTag && (it.name === "Contours"))
&& !(!isAprilTag && it.name === "AprilTag")
!(!allow3d && it.name === "3D") //Filter out 3D tab any time 3D isn't calibrated
&& !((!allow3d || isAprilTag) && it.name === "PnP") //Filter out the PnP config tab if 3D isn't available, or we're doing Apriltags
&& !(isAprilTag && (it.name === "Threshold")) //Filter out threshold tab if we're doing apriltags
&& !(isAprilTag && (it.name === "Contours")) //Filter out contours if we're doing Apriltag
&& !(!isAprilTag && it.name === "AprilTag") //Filter out apriltag unless we actually are doing Apriltags
);
ret[i] = filteredGroup;
}

View File

@@ -12,40 +12,55 @@
<CVslider
v-model="decimate"
class="pt-2"
slider-cols="12"
slider-cols="8"
name="Decimate"
min="0"
max="3"
step=".5"
tooltip="Increases FPS at the expense of range by reducing image resolution initially"
@input="handlePipelineData('decimate')"
/>
<CVslider
v-model="blur"
class="pt-2"
slider-cols="12"
slider-cols="8"
name="Blur"
min="0"
max="5"
step=".01"
tooltip="Gaussian blur added to the image, high FPS cost for slightly decreased noise"
@input="handlePipelineData('blur')"
/>
<CVslider
v-model="threads"
class="pt-2"
slider-cols="12"
slider-cols="8"
name="Threads"
min="1"
max="8"
step="1"
tooltip="Number of threads spawned by the AprilTag detector"
@input="handlePipelineData('threads')"
/>
<CVswitch
v-model="refineEdges"
class="pt-2"
slider-cols="12"
slider-cols="8"
name="Refine Edges"
tooltip="Further refines the apriltag corner position initial estimate, suggested left on"
@input="handlePipelineData('refineEdges')"
/>
<CVslider
v-model="numIterations"
class="pt-2 pb-4"
slider-cols="8"
name="Pose Estimation Iterations"
min="0"
max="500"
step="1"
tooltip="Number of iterations the pose estimation algorithm will run, 50-100 is a good starting point"
@input="handlePipelineData('numIterations')"
/>
</div>
</template>
@@ -82,6 +97,14 @@
this.$store.commit("mutatePipeline", {"decimate": val});
}
},
numIterations: {
get() {
return this.$store.getters.currentPipelineSettings.numIterations
},
set(val) {
this.$store.commit("mutatePipeline", {"numIterations": val});
}
},
blur: {
get() {
return this.$store.getters.currentPipelineSettings.blur

View File

@@ -19,12 +19,12 @@
@input="handlePipelineData('contourRatio')"
/>
<CVselect
v-model="contourTargetOrientation"
name="Target Orientation"
tooltip="Used to determine how to calculate target landmarks, as well as aspect ratio"
:list="['Portrait', 'Landscape']"
@input="handlePipelineData('contourTargetOrientation')"
@rollback="e=> rollback('contourTargetOrientation', e)"
v-model="contourTargetOrientation"
name="Target Orientation"
tooltip="Used to determine how to calculate target landmarks, as well as aspect ratio"
:list="['Portrait', 'Landscape']"
@input="handlePipelineData('contourTargetOrientation')"
@rollback="e=> rollback('contourTargetOrientation', e)"
/>
<CVrangeSlider
v-if="currentPipelineType() !== 3"

View File

@@ -1,13 +1,13 @@
<template>
<div>
<CVslider
v-if="cameraExposure !== -1"
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)"
@@ -22,10 +22,28 @@
@input="handlePipelineData('cameraBrightness')"
@rollback="e => rollback('cameraBrightness', e)"
/>
<CVswitch
v-model="cameraAutoExposure"
class="pt-2"
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"
min="0"
max="100"
tooltip="Controls camera gain, similar to brightness"
:slider-cols="largeBox"
@input="handlePipelineData('cameraGain')"
@rollback="e => rollback('cameraGain', e)"
/>
<CVslider
v-if="cameraRedGain !== -1"
v-model="cameraRedGain"
name="Red AWB Gain"
name="Red Balance"
min="0"
max="100"
tooltip="Controls red automatic white balance gain, which affects how the camera captures colors in different conditions"
@@ -36,7 +54,7 @@
<CVslider
v-if="cameraBlueGain !== -1"
v-model="cameraBlueGain"
name="Blue AWB Gain"
name="Blue Balance"
min="0"
max="100"
tooltip="Controls blue automatic white balance gain, which affects how the camera captures colors in different conditions"
@@ -76,6 +94,7 @@
<script>
import CVslider from '../../components/common/cv-slider'
import CVselect from '../../components/common/cv-select'
import CVswitch from '../../components/common/cv-switch'
const unfilteredStreamDivisors = [1, 2, 4, 6];
@@ -84,6 +103,7 @@
components: {
CVslider,
CVselect,
CVswitch,
},
// eslint-disable-next-line vue/require-prop-types
props: ['value'],
@@ -109,6 +129,14 @@
this.$store.commit("mutatePipeline", {"cameraExposure": parseFloat(val)});
}
},
cameraAutoExposure: {
get() {
return this.$store.getters.currentPipelineSettings.cameraAutoExposure;
},
set(val) {
this.$store.commit("mutatePipeline", {"cameraAutoExposure": val});
}
},
cameraBrightness: {
get() {
return parseInt(this.$store.getters.currentPipelineSettings.cameraBrightness)
@@ -117,6 +145,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)

View File

@@ -0,0 +1,53 @@
<template>
<div>
<mini-map
class="miniMapClass"
:targets="targets"
:horizontal-f-o-v="horizontalFOV"
/>
</div>
</template>
<script>
import miniMap from '../../components/pipeline/3D/MiniMap';
export default {
name: "Map3D",
components: {
miniMap
},
data() {
return {
}
},
computed: {
targets: {
get() {
return this.$store.getters.currentPipelineResults.targets;
}
},
horizontalFOV: {
get() {
let index = this.$store.getters.currentPipelineSettings.cameraVideoModeIndex;
let FOV = this.$store.getters.currentCameraSettings.fov;
let resolution = this.$store.getters.videoFormatList[index];
let diagonalView = FOV * (Math.PI / 180);
let diagonalAspect = Math.hypot(resolution.width, resolution.height);
return Math.atan(Math.tan(diagonalView / 2) * (resolution.width / diagonalAspect)) * 2 * (180 / Math.PI)
}
},
},
methods: {
}
}
</script>
<style scoped>
.miniMapClass {
width: 400px !important;
height: 100% !important;
margin-left: auto;
margin-right: auto;
}
</style>

View File

@@ -6,7 +6,6 @@
type="file"
accept=".csv"
style="display: none;"
@change="readFile"
>
@@ -32,11 +31,7 @@
@input="handlePipelineData('cornerDetectionAccuracyPercentage')"
@rollback="e => rollback('cornerDetectionAccuracyPercentage', e)"
/>
<mini-map
class="miniMapClass"
:targets="targets"
:horizontal-f-o-v="horizontalFOV"
/>
<v-snackbar
v-model="snack"
top
@@ -49,14 +44,12 @@
<script>
import Papa from 'papaparse';
import miniMap from '../../components/pipeline/3D/MiniMap';
import CVslider from '../../components/common/cv-slider'
export default {
name: "PnP",
components: {
CVslider,
miniMap
CVslider
},
data() {
return {
@@ -87,21 +80,6 @@
this.$store.commit("mutatePipeline", {"cornerDetectionAccuracyPercentage": val});
}
},
targets: {
get() {
return this.$store.getters.currentPipelineResults.targets;
}
},
horizontalFOV: {
get() {
let index = this.$store.getters.currentPipelineSettings.cameraVideoModeIndex;
let FOV = this.$store.getters.currentCameraSettings.fov;
let resolution = this.$store.getters.videoFormatList[index];
let diagonalView = FOV * (Math.PI / 180);
let diagonalAspect = Math.hypot(resolution.width, resolution.height);
return Math.atan(Math.tan(diagonalView / 2) * (resolution.width / diagonalAspect)) * 2 * (180 / Math.PI)
}
},
},
methods: {
readFile(event) {

View File

@@ -18,7 +18,10 @@
<th class="text-center">
Target
</th>
<th class="text-center" v-if="$store.getters.pipelineType === 4">
<th
v-if="$store.getters.pipelineType === 4"
class="text-center"
>
Fiducial ID
</th>
<template v-if="!$store.getters.currentPipelineSettings.solvePNPEnabled">
@@ -46,6 +49,11 @@
Z Angle,&nbsp;&deg;
</th>
</template>
<template v-if="$store.getters.pipelineType === 4 && $store.getters.currentPipelineSettings.solvePNPEnabled">
<th class="text-center">
Ambiguity
</th>
</template>
</tr>
</thead>
<tbody>
@@ -73,6 +81,11 @@
<td>{{ parseFloat(value.pose.y).toFixed(2) }}&nbsp;m</td>
<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>
</template>
</tr>
</tbody>
</template>

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,9 @@ public class NTDataPublisher implements CVPipelineResultConsumer {
t.getArea(),
t.getSkew(),
t.getFiducialId(),
t.getCameraToTarget3d(),
t.getBestCameraToTarget3d(),
t.getAltCameraToTarget3d(),
t.getPoseAmbiguity(),
cornerList));
}
return ret;

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

@@ -33,7 +33,6 @@ public class TestUtils {
try {
CameraServerCvJNI.forceLoad();
// PicamJNI.forceLoad();
// AprilTagJNI.forceLoad();
} catch (IOException ex) {
// ignored
}
@@ -165,7 +164,8 @@ public class TestUtils {
}
public enum ApriltagTestImages {
kRobots;
kRobots,
kTag1_640_480;
public final Path path;
@@ -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() {
@@ -233,6 +233,10 @@ public class TestUtils {
return getTestImagesPath(testMode).resolve(image.path);
}
public static Path getApriltagImagePath(ApriltagTestImages image, boolean testMode) {
return getTestImagesPath(testMode).resolve(image.path);
}
public static Path getPowercellImagePath(PowercellTestImages image, boolean testMode) {
return getPowercellPath(testMode).resolve(image.path);
}

View File

@@ -19,14 +19,17 @@ package org.photonvision.common.util.math;
import edu.wpi.first.math.MatBuilder;
import edu.wpi.first.math.Nat;
import edu.wpi.first.math.VecBuilder;
import edu.wpi.first.math.geometry.CoordinateSystem;
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.util.Units;
import edu.wpi.first.util.WPIUtilJNI;
import java.util.Arrays;
import java.util.List;
import org.opencv.core.Mat;
public class MathUtils {
MathUtils() {}
@@ -156,28 +159,46 @@ public class MathUtils {
pose.getTranslation().rotateBy(rotationQuat), pose.getRotation().rotateBy(rotationQuat));
}
// TODO: Refactor into new pipe?
public static Pose3d convertOpenCVtoPhotonPose(Transform3d cameraToTarget3d) {
// CameraToTarget _should_ be in opencv-land EDN
/**
* 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));
var pose =
public static Pose3d convertOpenCVtoPhotonPose(Transform3d cameraToTarget3d) {
// TODO: Refactor into new pipe?
// CameraToTarget _should_ be in opencv-land EDN
var nwu =
CoordinateSystem.convert(
new Pose3d(cameraToTarget3d), CoordinateSystem.EDN(), CoordinateSystem.NWU());
return pose;
return new Pose3d(nwu.getTranslation(), WPILIB_BASE_ROTATION.rotateBy(nwu.getRotation()));
}
public static Pose3d convertApriltagtoPhotonPose(Transform3d cameraToTarget3d) {
// CameraToTarget _should_ be in opencv-land EDN
var pose =
CoordinateSystem.convert(
new Pose3d(cameraToTarget3d), CoordinateSystem.EDN(), CoordinateSystem.NWU());
/*
* The AprilTag pose rotation outputs are X left, Y down, Z away from the tag with the tag facing
* the camera upright and the camera facing the target parallel to the floor. But our OpenCV
* solvePNP code would have X left, Y up, Z towards the camera with the target facing the camera
* and both parallel to the floor. So we apply a base rotation to the rotation component of the
* apriltag pose to make it consistent with the EDN system that OpenCV uses, internally a 180
* rotation about the X axis
*/
private static final Rotation3d APRILTAG_BASE_ROTATION =
new Rotation3d(VecBuilder.fill(1, 0, 0), Units.degreesToRadians(180));
// Apply an extra rotation so that at zero pose, X ls left, Y is up, and Z is towards the camera
// to a camera facing along the +X axis of the field parallel with the ground plane
// So we need a 180 flip about X axis
var newRotation = pose.getRotation().rotateBy(new Rotation3d(0, Math.PI, 0));
/**
* Apply a 180 degree rotation about X to the rotation component of a given Apriltag pose. This
* aligns it with the OpenCV poses we use in other places.
*/
public static Transform3d convertApriltagtoOpenCV(Transform3d pose) {
var ocvRotation = APRILTAG_BASE_ROTATION.rotateBy(pose.getRotation());
return new Transform3d(pose.getTranslation(), ocvRotation);
}
return new Pose3d(pose.getTranslation(), newRotation);
public static void rotationToOpencvRvec(Rotation3d rotation, Mat rvecOutput) {
var angle = rotation.getAngle();
var axis = rotation.getAxis().times(angle);
rvecOutput.put(0, 0, axis.getData());
}
}

View File

@@ -19,8 +19,8 @@ package org.photonvision.vision.apriltag;
import edu.wpi.first.math.MatBuilder;
import edu.wpi.first.math.Nat;
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 java.util.Arrays;
@@ -81,11 +81,11 @@ public class DetectionResult {
return error2;
}
public Pose3d getPoseResult1() {
public Transform3d getPoseResult1() {
return poseResult1;
}
public Pose3d getPoseResult2() {
public Transform3d getPoseResult2() {
return poseResult2;
}
@@ -96,9 +96,9 @@ public class DetectionResult {
double centerX, centerY;
double[] corners;
Pose3d poseResult1;
Transform3d poseResult1;
double error1;
Pose3d poseResult2;
Transform3d poseResult2;
double error2;
public DetectionResult(
@@ -125,16 +125,31 @@ public class DetectionResult {
this.error1 = err1;
this.poseResult1 =
new Pose3d(
new Transform3d(
new Translation3d(pose1TransArr[0], pose1TransArr[1], pose1TransArr[2]),
new Rotation3d(new MatBuilder<>(Nat.N3(), Nat.N3()).fill(pose1RotArr)));
this.error2 = err2;
this.poseResult2 =
new Pose3d(
new Transform3d(
new Translation3d(pose2TransArr[0], pose2TransArr[1], pose2TransArr[2]),
new Rotation3d(new MatBuilder<>(Nat.N3(), Nat.N3()).fill(pose2RotArr)));
}
/**
* Get the ratio of pose reprojection errors, called ambiguity. Numbers above 0.2 are likely to be
* ambiguous.
*/
public double getPoseAmbiguity() {
var min = Math.min(error1, error2);
var max = Math.max(error1, error2);
if (max > 0) {
return min / max;
} else {
return -1;
}
}
@Override
public String toString() {
return "DetectionResult [centerX="

View File

@@ -27,5 +27,7 @@ public enum CameraQuirk {
/** Separate red/blue gain controls available */
AWBGain,
/** Will not work with photonvision - Logitec C270 at least */
CompletelyBroken
CompletelyBroken,
/** Has adjustable focus and autofocus switch */
AdjustableFocus,
}

View File

@@ -91,15 +91,14 @@ public class FileVisionSource extends VisionSource {
@Override
public void setExposure(double exposure) {}
public void setAutoExposure(boolean cameraAutoExposure) {}
@Override
public void setBrightness(int brightness) {}
@Override
public void setGain(int gain) {}
@Override
public void setLowExposureOptimization(boolean mode) {}
@Override
public VideoMode getCurrentVideoMode() {
return videoMode;

View File

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

View File

@@ -18,10 +18,7 @@
package org.photonvision.vision.camera;
import edu.wpi.first.cameraserver.CameraServer;
import edu.wpi.first.cscore.CvSink;
import edu.wpi.first.cscore.UsbCamera;
import edu.wpi.first.cscore.VideoException;
import edu.wpi.first.cscore.VideoMode;
import edu.wpi.first.cscore.*;
import java.util.*;
import java.util.stream.Collectors;
import org.photonvision.common.configuration.CameraConfiguration;
@@ -64,60 +61,26 @@ public class USBCameraSource extends VisionSource {
usbFrameProvider = null;
} else {
// Normal init
setLowExposureOptimizationImpl(false);
// auto exposure/brightness/gain will be set by the visionmodule later
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);
}
}
}
void setLowExposureOptimizationImpl(boolean lowExposureMode) {
if (cameraQuirks.hasQuirk(CameraQuirk.PiCam)) {
// Case, we know this is a picam. Go through v4l2-ctl interface directly
// Common settings
camera
.getProperty("image_stabilization")
.set(0); // No image stabilization, as this will throw off odometry
camera.getProperty("power_line_frequency").set(2); // Assume 60Hz USA
camera.getProperty("scene_mode").set(0); // no presets
camera.getProperty("exposure_metering_mode").set(0);
camera.getProperty("exposure_dynamic_framerate").set(0);
if (lowExposureMode) {
// Pick a bunch of reasonable setting defaults for vision processing retroreflective
camera.getProperty("auto_exposure_bias").set(0);
camera.getProperty("iso_sensitivity_auto").set(0); // Disable auto ISO adjustement
camera.getProperty("iso_sensitivity").set(0); // Manual ISO adjustement
camera.getProperty("white_balance_auto_preset").set(2); // Auto white-balance disabled
camera.getProperty("auto_exposure").set(1); // auto exposure disabled
} else {
// Pick a bunch of reasonable setting defaults for driver, fiducials, or otherwise
// nice-for-humans
camera.getProperty("auto_exposure_bias").set(12);
camera.getProperty("iso_sensitivity_auto").set(1);
camera.getProperty("iso_sensitivity").set(1); // Manual ISO adjustement by default
camera.getProperty("white_balance_auto_preset").set(1); // Auto white-balance enabled
camera.getProperty("auto_exposure").set(0); // auto exposure enabled
}
} else {
// Case - this is some other USB cam. Default to wpilib's implementation
var canSetWhiteBalance = !cameraQuirks.hasQuirk(CameraQuirk.Gain);
if (lowExposureMode) {
// Pick a bunch of reasonable setting defaults for vision processing retroreflective
if (canSetWhiteBalance) {
camera.setWhiteBalanceManual(4000); // Auto white-balance disabled, 4000K preset
}
this.getSettables().setExposure(50); // auto exposure disabled, put a sane default
} else {
// Pick a bunch of reasonable setting defaults for driver, fiducials, or otherwise
// nice-for-humans
if (canSetWhiteBalance) {
camera.setWhiteBalanceAuto(); // Auto white-balance enabled
}
camera.setExposureAuto(); // auto exposure enabled
void disableAutoFocus() {
if (cameraQuirks.hasQuirk(CameraQuirk.AdjustableFocus)) {
try {
camera.getProperty("focus_auto").set(0);
camera.getProperty("focus_absolute").set(0); // Focus into infinity
} catch (VideoException e) {
logger.error("Unable to disable autofocus!", e);
}
}
}
@@ -139,6 +102,59 @@ public class USBCameraSource extends VisionSource {
setVideoMode(videoModes.get(0));
}
public void setAutoExposure(boolean cameraAutoExposure) {
logger.debug("Setting auto exposure to " + cameraAutoExposure);
if (cameraQuirks.hasQuirk(CameraQuirk.PiCam)) {
// Case, we know this is a picam. Go through v4l2-ctl interface directly
// Common settings
camera
.getProperty("image_stabilization")
.set(0); // No image stabilization, as this will throw off odometry
camera.getProperty("power_line_frequency").set(2); // Assume 60Hz USA
camera.getProperty("scene_mode").set(0); // no presets
camera.getProperty("exposure_metering_mode").set(0);
camera.getProperty("exposure_dynamic_framerate").set(0);
if (!cameraAutoExposure) {
// Pick a bunch of reasonable setting defaults for vision processing retroreflective
camera.getProperty("auto_exposure_bias").set(0);
camera.getProperty("iso_sensitivity_auto").set(0); // Disable auto ISO adjustement
camera.getProperty("iso_sensitivity").set(0); // Manual ISO adjustement
camera.getProperty("white_balance_auto_preset").set(2); // Auto white-balance disabled
camera.getProperty("auto_exposure").set(1); // auto exposure disabled
} else {
// Pick a bunch of reasonable setting defaults for driver, fiducials, or otherwise
// nice-for-humans
camera.getProperty("auto_exposure_bias").set(12);
camera.getProperty("iso_sensitivity_auto").set(1);
camera.getProperty("iso_sensitivity").set(1); // Manual ISO adjustement by default
camera.getProperty("white_balance_auto_preset").set(1); // Auto white-balance enabled
camera.getProperty("auto_exposure").set(0); // auto exposure enabled
}
} else {
// Case - this is some other USB cam. Default to wpilib's implementation
var canSetWhiteBalance = !cameraQuirks.hasQuirk(CameraQuirk.Gain);
if (!cameraAutoExposure) {
// Pick a bunch of reasonable setting defaults for vision processing retroreflective
if (canSetWhiteBalance) {
camera.setWhiteBalanceManual(4000); // Auto white-balance disabled, 4000K preset
}
} else {
// Pick a bunch of reasonable setting defaults for driver, fiducials, or otherwise
// nice-for-humans
if (canSetWhiteBalance) {
camera.setWhiteBalanceAuto(); // Auto white-balance enabled
}
camera.setExposureAuto(); // auto exposure enabled
}
}
}
private int timeToPiCamRawExposure(double time_us) {
int retVal =
(int)
@@ -157,11 +173,6 @@ public class USBCameraSource extends VisionSource {
+ (pct_in / 100.0) * ((1e6 / (double) camera.getVideoMode().fps) - PADDING_HIGH_US);
}
@Override
public void setLowExposureOptimization(boolean mode) {
setLowExposureOptimizationImpl(mode);
}
@Override
public void setExposure(double exposure) {
if (exposure >= 0.0) {

View File

@@ -45,13 +45,6 @@ public class ZeroCopyPicamSource extends VisionSource {
settables = new PicamSettables(configuration);
frameProvider = new AcceleratedPicamFrameProvider(settables);
setLowExposureOptimizationImpl(false);
}
static void setLowExposureOptimizationImpl(boolean mode) {
// TODO - ZeroCopy does not... yet? ... have the configuration params necessary to make this
// work well.
}
@Override
@@ -94,6 +87,7 @@ public class ZeroCopyPicamSource extends VisionSource {
private FPSRatedVideoMode currentVideoMode;
private double lastExposure = 50;
private int lastBrightness = 50;
private boolean lastExposureMode;
private int lastGain = 50;
private Pair<Integer, Integer> lastAwbGains = new Pair(18, 18);
@@ -150,9 +144,15 @@ public class ZeroCopyPicamSource extends VisionSource {
return getCurrentVideoMode().fovMultiplier * getConfiguration().FOV;
}
@Override
public void setAutoExposure(boolean cameraAutoExposure) {
lastExposureMode = cameraAutoExposure;
// TODO (Matt) -- call PicamJNI's auto exposure function, when that exists
}
@Override
public void setExposure(double exposure) {
// Todo - for now, handle auto exposure by using 100% exposure
// Todo (Chris) - for now, handle auto exposure by using 100% exposure
if (exposure < 0.0) {
exposure = 100.0;
}
@@ -162,11 +162,6 @@ public class ZeroCopyPicamSource extends VisionSource {
if (failure) logger.warn("Couldn't set Pi Camera exposure");
}
@Override
public void setLowExposureOptimization(boolean mode) {
setLowExposureOptimizationImpl(mode);
}
@Override
public void setBrightness(int brightness) {
lastBrightness = brightness;
@@ -218,6 +213,7 @@ public class ZeroCopyPicamSource extends VisionSource {
// We don't store last settings on the native side, and when you change video mode these get
// reset on MMAL's end
setExposure(lastExposure);
setAutoExposure(lastExposureMode);
setBrightness(lastBrightness);
setGain(lastGain);
setAwbGain(lastAwbGains.getFirst(), lastAwbGains.getSecond());

View File

@@ -25,7 +25,9 @@ import org.opencv.calib3d.Calib3d;
import org.opencv.core.Mat;
import org.opencv.core.MatOfPoint;
import org.opencv.core.MatOfPoint2f;
import org.opencv.core.MatOfPoint3f;
import org.opencv.core.Point;
import org.opencv.core.Point3;
import org.opencv.imgproc.Imgproc;
import org.photonvision.common.logging.LogGroup;
import org.photonvision.common.logging.Logger;
@@ -43,6 +45,11 @@ public class Draw3dTargetsPipe
@Override
protected Void process(Pair<Mat, List<TrackedTarget>> in) {
if (!params.shouldDraw) return null;
if (params.cameraCalibrationCoefficients == null
|| params.cameraCalibrationCoefficients.getCameraIntrinsicsMat() == null
|| params.cameraCalibrationCoefficients.getCameraExtrinsicsMat() == null) {
return null;
}
for (var target : in.getRight()) {
// draw convex hull
@@ -111,6 +118,54 @@ public class Draw3dTargetsPipe
ColorHelper.colorToScalar(Color.green),
3);
}
// Draw X, Y and Z axis
MatOfPoint3f pointMat = new MatOfPoint3f();
// Those points are in opencv-land, but we are in NWU
// NWU | EDN
// X: Z
// Y: -X
// Z: -Y
final double AXIS_LEN = 0.2;
var list =
List.of(
new Point3(0, 0, 0),
new Point3(0, 0, AXIS_LEN),
new Point3(AXIS_LEN, 0, 0),
new Point3(0, AXIS_LEN, 0));
pointMat.fromList(list);
Calib3d.projectPoints(
pointMat,
target.getCameraRelativeRvec(),
target.getCameraRelativeTvec(),
params.cameraCalibrationCoefficients.getCameraIntrinsicsMat(),
params.cameraCalibrationCoefficients.getCameraExtrinsicsMat(),
tempMat,
jac);
var axisPoints = tempMat.toList();
dividePointList(axisPoints);
// Red = x, green y, blue z
Imgproc.line(
in.getLeft(),
axisPoints.get(0),
axisPoints.get(2),
ColorHelper.colorToScalar(Color.GREEN),
3);
Imgproc.line(
in.getLeft(),
axisPoints.get(0),
axisPoints.get(3),
ColorHelper.colorToScalar(Color.BLUE),
3);
Imgproc.line(
in.getLeft(),
axisPoints.get(0),
axisPoints.get(1),
ColorHelper.colorToScalar(Color.RED),
3);
for (int i = 0; i < bottomPoints.size(); i++) {
Imgproc.line(
in.getLeft(),
@@ -127,8 +182,10 @@ public class Draw3dTargetsPipe
ColorHelper.colorToScalar(Color.orange),
3);
}
tempMat.release();
jac.release();
pointMat.release();
}
// draw corners

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

@@ -117,9 +117,13 @@ public class AprilTagPipeline extends CVPipeline<CVPipelineResult, AprilTagPipel
sumPipeNanosElapsed += rotateImageResult.nanosElapsed;
}
var inputFrame = new Frame(new CVMat(rawInputMat), frameStaticProperties);
grayscalePipeResult = grayscalePipe.run(rawInputMat);
sumPipeNanosElapsed += grayscalePipeResult.nanosElapsed;
var outputFrame = new Frame(new CVMat(grayscalePipeResult.output), frameStaticProperties);
List<TrackedTarget> targetList;
CVPipeResult<List<DetectionResult>> tagDetectionPipeResult;
@@ -127,7 +131,6 @@ public class AprilTagPipeline extends CVPipeline<CVPipelineResult, AprilTagPipel
aprilTagDetectionPipe.setNativePoseEstimationEnabled(settings.solvePNPEnabled);
tagDetectionPipeResult = aprilTagDetectionPipe.run(grayscalePipeResult.output);
grayscalePipeResult.output.release();
sumPipeNanosElapsed += tagDetectionPipeResult.nanosElapsed;
targetList = new ArrayList<>();
@@ -140,9 +143,13 @@ public class AprilTagPipeline extends CVPipeline<CVPipelineResult, AprilTagPipel
new TargetCalculationParameters(
false, null, null, null, null, frameStaticProperties));
var correctedPose = MathUtils.convertApriltagtoPhotonPose(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);
}
@@ -150,11 +157,6 @@ public class AprilTagPipeline extends CVPipeline<CVPipelineResult, AprilTagPipel
var fpsResult = calculateFPSPipe.run(null);
var fps = fpsResult.output;
var inputFrame = new Frame(new CVMat(rawInputMat), frameStaticProperties);
// empty output frame
var outputFrame =
Frame.emptyFrame(frameStaticProperties.imageWidth, frameStaticProperties.imageHeight);
return new CVPipelineResult(sumPipeNanosElapsed, fps, targetList, outputFrame, inputFrame);
}
}

View File

@@ -40,6 +40,7 @@ public class AprilTagPipelineSettings extends AdvancedPipelineSettings {
outputShowMultipleTargets = true;
targetModel = TargetModel.k200mmAprilTag;
cameraExposure = -1;
cameraAutoExposure = true;
ledMode = false;
}

View File

@@ -40,7 +40,8 @@ public class CVPipelineSettings implements Cloneable {
public ImageFlipMode inputImageFlipMode = ImageFlipMode.NONE;
public ImageRotationMode inputImageRotationMode = ImageRotationMode.DEG_0;
public String pipelineNickname = "New Pipeline";
// Only used if the pipeline type does not enable auto-exposure
public boolean cameraAutoExposure = false;
// manual exposure only used if cameraAutoExposure if false
public double cameraExposure = 100;
public int cameraBrightness = 50;
// Currently only used by a few cameras (notably the zero-copy Pi Camera driver) with the Gain

View File

@@ -120,42 +120,58 @@ public class OutputStreamPipeline {
pipeProfileNanos[2] = 0;
}
// Draw 2D Crosshair on input and output
var draw2dCrosshairResultOnInput = draw2dCrosshairPipe.run(Pair.of(inMat, targetsToDraw));
// Draw 2D Crosshair on output
var draw2dCrosshairResultOnInput = draw2dCrosshairPipe.run(Pair.of(outMat, targetsToDraw));
sumPipeNanosElapsed += pipeProfileNanos[3] = draw2dCrosshairResultOnInput.nanosElapsed;
if (!(settings instanceof AprilTagPipelineSettings)) {
// If we're processing anything other than Apriltags...
var draw2dCrosshairResultOnOutput = draw2dCrosshairPipe.run(Pair.of(outMat, targetsToDraw));
sumPipeNanosElapsed += pipeProfileNanos[4] = draw2dCrosshairResultOnOutput.nanosElapsed;
// Draw 3D Targets on input and output if necessary
if (settings.solvePNPEnabled
|| (settings.solvePNPEnabled
&& settings instanceof ColoredShapePipelineSettings
&& ((ColoredShapePipelineSettings) settings).contourShape == ContourShape.Circle)) {
var drawOnInputResult = draw3dTargetsPipe.run(Pair.of(inMat, targetsToDraw));
sumPipeNanosElapsed += pipeProfileNanos[7] = drawOnInputResult.nanosElapsed;
// Draw 3D Targets on input and output if possible
pipeProfileNanos[5] = 0;
pipeProfileNanos[6] = 0;
pipeProfileNanos[7] = 0;
var drawOnOutputResult = draw3dTargetsPipe.run(Pair.of(outMat, targetsToDraw));
sumPipeNanosElapsed += pipeProfileNanos[8] = drawOnOutputResult.nanosElapsed;
} else {
pipeProfileNanos[7] = 0;
pipeProfileNanos[8] = 0;
var draw2dTargetsOnInput = draw2dTargetsPipe.run(Pair.of(inMat, targetsToDraw));
sumPipeNanosElapsed += pipeProfileNanos[5] = draw2dTargetsOnInput.nanosElapsed;
// Only draw 2d targets
pipeProfileNanos[5] = 0;
var draw2dTargetsOnOutput = draw2dTargetsPipe.run(Pair.of(outMat, targetsToDraw));
sumPipeNanosElapsed += pipeProfileNanos[6] = draw2dTargetsOnOutput.nanosElapsed;
pipeProfileNanos[7] = 0;
pipeProfileNanos[8] = 0;
}
} else {
// If we are doing apriltags...
if (settings.solvePNPEnabled) {
var drawOnInputResult = draw3dAprilTagsPipe.run(Pair.of(inMat, targetsToDraw));
// Draw 3d Apriltag markers (camera is calibrated and running in 3d mode)
pipeProfileNanos[5] = 0;
pipeProfileNanos[6] = 0;
var drawOnInputResult = draw3dAprilTagsPipe.run(Pair.of(outMat, targetsToDraw));
sumPipeNanosElapsed += pipeProfileNanos[7] = drawOnInputResult.nanosElapsed;
pipeProfileNanos[8] = 0;
} else {
var draw2dTargetsOnInput = draw2dAprilTagsPipe.run(Pair.of(inMat, targetsToDraw));
// Draw 2d apriltag markers
var draw2dTargetsOnInput = draw2dAprilTagsPipe.run(Pair.of(outMat, targetsToDraw));
sumPipeNanosElapsed += pipeProfileNanos[5] = draw2dTargetsOnInput.nanosElapsed;
pipeProfileNanos[6] = 0;
pipeProfileNanos[7] = 0;
pipeProfileNanos[8] = 0;
}
}

View File

@@ -181,16 +181,23 @@ public class PipelineManager {
var desiredPipelineSettings = userPipelineSettings.get(currentPipelineIndex);
switch (desiredPipelineSettings.pipelineType) {
case Reflective:
logger.debug("Creatig Reflective pipeline");
currentUserPipeline =
new ReflectivePipeline((ReflectivePipelineSettings) desiredPipelineSettings);
break;
case ColoredShape:
logger.debug("Creatig ColoredShape pipeline");
currentUserPipeline =
new ColoredShapePipeline((ColoredShapePipelineSettings) desiredPipelineSettings);
break;
case AprilTag:
logger.debug("Creatig AprilTag pipeline");
currentUserPipeline =
new AprilTagPipeline((AprilTagPipelineSettings) desiredPipelineSettings);
break;
default:
// Can be calib3d or drivermode, both of which are special cases
break;
}
}
}

View File

@@ -17,7 +17,6 @@
package org.photonvision.vision.processes;
import edu.wpi.first.math.geometry.Rotation2d;
import edu.wpi.first.math.util.Units;
import io.javalin.websocket.WsContext;
import java.util.*;
@@ -41,15 +40,15 @@ 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.PipelineType;
import org.photonvision.vision.pipeline.ReflectivePipelineSettings;
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
@@ -58,32 +57,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(
@@ -131,7 +129,7 @@ public class VisionModule {
createStreams();
recreateFpsLimitedResultConsumers();
recreateStreamResultConsumers();
ntConsumer =
new NTDataPublisher(
@@ -168,49 +166,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 {
@@ -272,12 +254,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);
@@ -305,7 +286,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 + ")");
@@ -361,7 +342,7 @@ public class VisionModule {
settings.cameraBlueGain = -1;
}
settings.cameraExposure = -1;
settings.cameraAutoExposure = true;
setPipeline(PipelineManager.CAL_3D_INDEX);
}
@@ -400,22 +381,14 @@ public class VisionModule {
visionSource.getSettables().setBrightness(pipelineSettings.cameraBrightness);
visionSource.getSettables().setGain(pipelineSettings.cameraGain);
// set to true to change camera gain/exposure settings for low exposure
// false will keep the camera running in a more "nice-for-humans" mode
// Each camera type is allowed to decide what settings that exactly means
boolean lowExposureOptimization =
(pipelineSettings.pipelineType == PipelineType.ColoredShape
|| pipelineSettings.pipelineType == PipelineType.Reflective);
visionSource.getSettables().setLowExposureOptimization(lowExposureOptimization);
if (lowExposureOptimization) {
if (pipelineSettings.cameraExposure == -1)
// If manual exposure, force exposure slider to be valid
if (!pipelineSettings.cameraAutoExposure) {
if (pipelineSettings.cameraExposure < 0)
pipelineSettings.cameraExposure = 10; // reasonable default
} else {
// in human-friendly mode, exposure is automatic
pipelineSettings.cameraExposure = -1;
}
visionSource.getSettables().setExposure(pipelineSettings.cameraExposure);
visionSource.getSettables().setAutoExposure(pipelineSettings.cameraAutoExposure);
if (cameraQuirks.hasQuirk(CameraQuirk.Gain)) {
// If the gain is disabled for some reason, re-enable it
@@ -483,14 +456,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();
@@ -525,8 +498,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) {
@@ -576,7 +549,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
@@ -589,19 +562,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

@@ -44,6 +44,8 @@ public abstract class VisionSourceSettables {
public abstract void setExposure(double exposure);
public abstract void setAutoExposure(boolean cameraAutoExposure);
public abstract void setBrightness(int brightness);
public abstract void setGain(int gain);
@@ -64,8 +66,6 @@ public abstract class VisionSourceSettables {
calculateFrameStaticProps();
}
public abstract void setLowExposureOptimization(boolean mode);
protected abstract void setVideoModeInternal(VideoMode videoMode);
@SuppressWarnings("unused")

View File

@@ -108,7 +108,7 @@ 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));
@JsonIgnore private MatOfPoint3f realWorldTargetCoordinates;
@JsonIgnore private MatOfPoint3f visualizationBoxBottom = new MatOfPoint3f();

View File

@@ -16,7 +16,6 @@
*/
package org.photonvision.vision.target;
import edu.wpi.first.math.geometry.Pose3d;
import edu.wpi.first.math.geometry.Transform3d;
import java.util.HashMap;
import java.util.List;
@@ -26,6 +25,7 @@ import org.opencv.core.MatOfPoint;
import org.opencv.core.MatOfPoint2f;
import org.opencv.core.Point;
import org.opencv.core.RotatedRect;
import org.photonvision.common.util.math.MathUtils;
import org.photonvision.vision.apriltag.DetectionResult;
import org.photonvision.vision.frame.FrameStaticProperties;
import org.photonvision.vision.opencv.*;
@@ -46,11 +46,13 @@ 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;
private int m_fiducialId = -1;
private double m_poseAmbiguity = -1;
private Mat m_cameraRelativeTvec, m_cameraRelativeRvec;
@@ -72,14 +74,21 @@ public class TrackedTarget implements Releasable {
m_yaw =
TargetCalculations.calculateYaw(
result.getCenterX(), params.cameraCenterPoint.x, params.horizontalFocalLength);
Pose3d bestPose = new Pose3d();
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();
}
m_cameraToTarget3d = new Transform3d(new Pose3d(), bestPose);
bestPose = MathUtils.convertApriltagtoOpenCV(bestPose);
altPose = MathUtils.convertApriltagtoOpenCV(altPose);
m_bestCameraToTarget3d = bestPose;
m_altCameraToTarget3d = altPose;
double[] corners = result.getCorners();
Point[] cornerPoints =
@@ -113,10 +122,10 @@ public class TrackedTarget implements Releasable {
// Opencv expects a 3d vector with norm = angle and direction = axis
var rvec = new Mat(3, 1, CvType.CV_64FC1);
var angle = bestPose.getRotation().getAngle();
var axis = bestPose.getRotation().getAxis().times(angle);
rvec.put(0, 0, axis.getData());
MathUtils.rotationToOpencvRvec(bestPose.getRotation(), rvec);
setCameraRelativeRvec(rvec);
m_poseAmbiguity = result.getPoseAmbiguity();
}
public void setFiducialId(int id) {
@@ -127,6 +136,14 @@ public class TrackedTarget implements Releasable {
return m_fiducialId;
}
public void setPoseAmbiguity(double ambiguity) {
m_poseAmbiguity = ambiguity;
}
public double getPoseAmbiguity() {
return m_poseAmbiguity;
}
/**
* Set the approximate bouding polygon.
*
@@ -220,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() {
@@ -260,8 +285,9 @@ public class TrackedTarget implements Releasable {
ret.put("yaw", getYaw());
ret.put("skew", getSkew());
ret.put("area", getArea());
if (getCameraToTarget3d() != null) {
ret.put("pose", transformToMap(getCameraToTarget3d()));
ret.put("ambiguity", getPoseAmbiguity());
if (getBestCameraToTarget3d() != null) {
ret.put("pose", transformToMap(getBestCameraToTarget3d()));
}
ret.put("fiducialId", getFiducialId());
return ret;
@@ -277,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,97 @@
/*
* 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) {
var stream = streams.get(port);
userSubscriptions.put(user, NO_STREAM_PORT);
stream.removeUser();
}
}
// 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

@@ -30,6 +30,7 @@ import org.photonvision.common.logging.LogLevel;
import org.photonvision.common.logging.Logger;
import org.photonvision.common.util.TestUtils;
import org.photonvision.common.util.file.JacksonUtils;
import org.photonvision.vision.pipeline.AprilTagPipelineSettings;
import org.photonvision.vision.pipeline.ColoredShapePipelineSettings;
import org.photonvision.vision.pipeline.ReflectivePipelineSettings;
import org.photonvision.vision.target.TargetModel;
@@ -40,6 +41,7 @@ public class ConfigTest {
new CameraConfiguration("TestCamera", "/dev/video420");
private static ReflectivePipelineSettings REFLECTIVE_PIPELINE_SETTINGS;
private static ColoredShapePipelineSettings COLORED_SHAPE_PIPELINE_SETTINGS;
private static AprilTagPipelineSettings APRIL_TAG_PIPELINE_SETTINGS;
@BeforeAll
public static void init() {
@@ -51,6 +53,7 @@ public class ConfigTest {
REFLECTIVE_PIPELINE_SETTINGS = new ReflectivePipelineSettings();
COLORED_SHAPE_PIPELINE_SETTINGS = new ColoredShapePipelineSettings();
APRIL_TAG_PIPELINE_SETTINGS = new AprilTagPipelineSettings();
REFLECTIVE_PIPELINE_SETTINGS.pipelineNickname = "2019Tape";
REFLECTIVE_PIPELINE_SETTINGS.targetModel = TargetModel.k2019DualTarget;
@@ -58,8 +61,12 @@ public class ConfigTest {
COLORED_SHAPE_PIPELINE_SETTINGS.pipelineNickname = "2019Cargo";
COLORED_SHAPE_PIPELINE_SETTINGS.pipelineIndex = 1;
APRIL_TAG_PIPELINE_SETTINGS.pipelineNickname = "apriltag";
APRIL_TAG_PIPELINE_SETTINGS.pipelineIndex = 2;
cameraConfig.addPipelineSetting(REFLECTIVE_PIPELINE_SETTINGS);
cameraConfig.addPipelineSetting(COLORED_SHAPE_PIPELINE_SETTINGS);
cameraConfig.addPipelineSetting(APRIL_TAG_PIPELINE_SETTINGS);
}
@Test
@@ -90,9 +97,12 @@ public class ConfigTest {
configMgr.getConfig().getCameraConfigurations().get("TestCamera").pipelineSettings.get(0);
var coloredShapePipelineSettings =
configMgr.getConfig().getCameraConfigurations().get("TestCamera").pipelineSettings.get(1);
var apriltagPipelineSettings =
configMgr.getConfig().getCameraConfigurations().get("TestCamera").pipelineSettings.get(2);
Assertions.assertEquals(REFLECTIVE_PIPELINE_SETTINGS, reflectivePipelineSettings);
Assertions.assertEquals(COLORED_SHAPE_PIPELINE_SETTINGS, coloredShapePipelineSettings);
Assertions.assertEquals(APRIL_TAG_PIPELINE_SETTINGS, apriltagPipelineSettings);
Assertions.assertTrue(
reflectivePipelineSettings instanceof ReflectivePipelineSettings,
@@ -100,6 +110,9 @@ public class ConfigTest {
Assertions.assertTrue(
coloredShapePipelineSettings instanceof ColoredShapePipelineSettings,
"Conig loaded pipeline settings for index 1 not of expected type ColoredShapePipelineSettings!");
Assertions.assertTrue(
apriltagPipelineSettings instanceof AprilTagPipelineSettings,
"Conig loaded pipeline settings for index 2 not of expected type AprilTagPipelineSettings!");
}
@AfterAll

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);
}
@@ -150,11 +150,20 @@ public class SolvePNPTest {
CVPipelineResult pipelineResult = pipeline.run(frameProvider.get(), QuirkyCamera.DefaultCamera);
printTestResults(pipelineResult);
// Draw on input
var outputPipe = new OutputStreamPipeline();
outputPipe.process(
pipelineResult.inputFrame,
pipelineResult.outputFrame,
pipeline.getSettings(),
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);
}
@@ -202,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

@@ -78,9 +78,6 @@ public class VisionModuleManagerTest {
@Override
public void setGain(int gain) {}
@Override
public void setLowExposureOptimization(boolean mode) {}
@Override
public VideoMode getCurrentVideoMode() {
return new VideoMode(0, 320, 240, 254);
@@ -97,6 +94,9 @@ public class VisionModuleManagerTest {
ret.put(0, getCurrentVideoMode());
return ret;
}
@Override
public void setAutoExposure(boolean cameraAutoExposure) {}
}
private static class TestDataConsumer implements CVPipelineResultConsumer {

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;
@@ -207,6 +210,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

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

@@ -171,6 +171,8 @@ public class SimVisionSystem {
0.0,
-1, // TODO fiducial ID
new Transform3d(),
new Transform3d(),
0.25,
List.of(
new TargetCorner(0, 0), new TargetCorner(0, 0),
new TargetCorner(0, 0), new TargetCorner(0, 0))));

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();
@@ -99,9 +103,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,22 @@ 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++) {
packet << target.corners[i].first << target.corners[i].second;
@@ -74,17 +83,30 @@ 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;
target.corners.clear();
for (int i = 0; i < 4; i++) {

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

@@ -91,10 +91,28 @@ class PhotonTrackedTarget {
}
/**
* Returns the pose of the target relative to the robot.
* Get the ratio of pose reprojection errors, called ambiguity. Numbers above
* 0.2 are likely to be ambiguous. -1 if invalid.
*/
double GetPoseAmbiguity() const { return poseAmbiguity; }
/**
* 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;
@@ -108,7 +126,9 @@ 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;
};
} // namespace photonlib

View File

@@ -45,6 +45,8 @@ 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),
new TargetCorner(3, 4),
@@ -81,6 +83,8 @@ 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),
new TargetCorner(3, 4),
@@ -93,6 +97,8 @@ 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),
new TargetCorner(3, 4),

View File

@@ -13,6 +13,7 @@ apply from: "${rootDir}/shared/common.gradle"
dependencies {
implementation project(':photon-core')
implementation project(':photon-targeting')
implementation "io.javalin:javalin:4.2.0"
@@ -42,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

Binary file not shown.

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 +1 @@
<p>UI has not been copied!</p>
<!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.3dcf0da8.css rel=preload as=style><link href=/css/chunk-vendors.7e4af884.css rel=preload as=style><link href=/js/app.4e0aa52b.js rel=preload as=script><link href=/js/chunk-vendors.58a768b6.js rel=preload as=script><link href=/css/chunk-vendors.7e4af884.css rel=stylesheet><link href=/css/app.3dcf0da8.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.4e0aa52b.js></script></body></html>

View File

@@ -0,0 +1,103 @@
/*
* 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.pipeline;
import edu.wpi.first.math.geometry.Translation3d;
import java.io.IOException;
import java.util.stream.Collectors;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.photonvision.common.util.TestUtils;
import org.photonvision.vision.apriltag.AprilTagJNI;
import org.photonvision.vision.camera.QuirkyCamera;
import org.photonvision.vision.frame.provider.FileFrameProvider;
import org.photonvision.vision.pipeline.result.CVPipelineResult;
import org.photonvision.vision.target.TargetModel;
import org.photonvision.vision.target.TrackedTarget;
public class AprilTagTest {
@BeforeEach
public void Init() throws IOException {
TestUtils.loadLibraries();
AprilTagJNI.forceLoad();
}
@Test
public void testApriltagFacingCamera() {
var pipeline = new AprilTagPipeline();
pipeline.getSettings().inputShouldShow = true;
pipeline.getSettings().outputShouldDraw = true;
pipeline.getSettings().solvePNPEnabled = true;
pipeline.getSettings().cornerDetectionAccuracyPercentage = 4;
pipeline.getSettings().cornerDetectionUseConvexHulls = true;
pipeline.getSettings().targetModel = TargetModel.k200mmAprilTag;
var frameProvider =
new FileFrameProvider(
TestUtils.getApriltagImagePath(TestUtils.ApriltagTestImages.kTag1_640_480, false),
TestUtils.WPI2020Image.FOV,
TestUtils.get2020LifeCamCoeffs(false));
CVPipelineResult pipelineResult = pipeline.run(frameProvider.get(), QuirkyCamera.DefaultCamera);
printTestResults(pipelineResult);
// Draw on input
var outputPipe = new OutputStreamPipeline();
outputPipe.process(
pipelineResult.inputFrame,
pipelineResult.outputFrame,
pipeline.getSettings(),
pipelineResult.targets);
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).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);
var objX = new Translation3d(1, 0, 0).rotateBy(pose.getRotation()).getY();
var objY = new Translation3d(0, 1, 0).rotateBy(pose.getRotation()).getZ();
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 to be forward, or -X in world space
Assertions.assertEquals(
-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()).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) {
double fps = 1000 / pipelineResult.getLatencyMillis();
System.out.println(
"Pipeline ran in " + pipelineResult.getLatencyMillis() + "ms (" + fps + " " + "fps)");
System.out.println("Found " + pipelineResult.targets.size() + " valid targets");
System.out.println(
"Found targets at "
+ pipelineResult.targets.stream()
.map(TrackedTarget::getBestCameraToTarget3d)
.collect(Collectors.toList()));
}
}

View File

@@ -27,14 +27,16 @@ 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);
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;
public PhotonTrackedTarget() {}
@@ -47,6 +49,8 @@ public class PhotonTrackedTarget {
double skew,
int id,
Transform3d pose,
Transform3d altPose,
double ambiguity,
List<TargetCorner> corners) {
assert corners.size() == 4;
this.yaw = yaw;
@@ -54,8 +58,10 @@ 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;
}
public double getYaw() {
@@ -79,6 +85,14 @@ public class PhotonTrackedTarget {
return fiducialId;
}
/**
* Get the ratio of pose reprojection errors, called ambiguity. Numbers above 0.2 are likely to be
* ambiguous. -1 if invalid.
*/
public double getPoseAmbiguity() {
return poseAmbiguity;
}
/**
* Return a list of the 4 corners in image space (origin top left, x left, y down), in no
* particular order, of the minimum area bounding rectangle of this target
@@ -89,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
@@ -103,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());
}
/**
@@ -125,15 +171,10 @@ 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.bestCameraToTarget = decodeTransform(packet);
this.altCameraToTarget = decodeTransform(packet);
this.poseAmbiguity = packet.decodeDouble();
this.targetCorners = new ArrayList<>(4);
for (int i = 0; i < 4; i++) {
@@ -142,8 +183,6 @@ public class PhotonTrackedTarget {
targetCorners.add(new TargetCorner(cx, cy));
}
this.cameraToTarget = new Transform3d(translation, rotation);
return packet;
}
@@ -159,13 +198,9 @@ 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++) {
packet.encode(targetCorners.get(i).x);
@@ -189,7 +224,7 @@ public class PhotonTrackedTarget {
+ ", fiducialId="
+ fiducialId
+ ", cameraToTarget="
+ cameraToTarget
+ bestCameraToTarget
+ ", targetCorners="
+ targetCorners
+ '}';

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View File

@@ -0,0 +1,223 @@
<!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.serverAddr = "ws://" + host + "/websocket_cameras";
this.noStream = false;
this.noStreamPrev = false;
this.setNoStream();
this.ws_connect();
this.imgData = null;
this.imgDataTime = -1;
this.imgObjURL = null;
this.frameRxCount = 0;
requestAnimationFrame(()=>this.animationLoop());
}
animationLoop(){
var now = window.performance.now();
if((now - this.imgDataTime) > 2500 && this.imgData != null){
//Handle websocket send timeouts by restarting
this.setNoStream();
this.stopStream();
setTimeout(this.startStream.bind(this), 1000); //restart stream one second later
} else {
if(this.streamPort == null){
this.setNoStream();
} else if (this.imgData != null) {
//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;
this.noStream = false;
} else {
//Nothing, hold previous image while waiting for next frame
}
}
requestAnimationFrame(()=>this.animationLoop());
}
setNoStream() {
this.noStreamPrev = this.noStream;
this.noStream = true;
if(this.noStreamPrev == false && this.noStream == true){
//One-shot background change to preserve animation
this.image.src = "loading.gif";
}
}
startStream() {
if(this.serverConnectionActive == true && this.streamPort > 0){
this.ws.send(JSON.stringify({"cmd": "subscribe", "port":this.streamPort}));
this.noStream = false;
}
}
stopStream() {
if(this.serverConnectionActive == true && this.streamPort > 0){
this.ws.send(JSON.stringify({"cmd": "unsubscribe"}));
this.noStream = true;
}
}
setPort(streamPort){
this.stopStream();
this.frameRxCount = 0;
this.streamPort = streamPort;
this.startStream();
}
ws_onOpen() {
// Set the flag allowing general server communication
this.serverConnectionActive = true;
console.log("Connected!");
this.startStream();
}
ws_onClose(e) {
this.setNoStream();
//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.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);
stream.startStream();
} 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

@@ -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
@@ -94,9 +94,15 @@ public class Constants {
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.toPose2d(), targetHeightAboveGround, targetWidth, targetHeight);
}

View File

@@ -28,8 +28,6 @@ import edu.wpi.first.math.Matrix;
import edu.wpi.first.math.VecBuilder;
import edu.wpi.first.math.estimator.DifferentialDrivePoseEstimator;
import edu.wpi.first.math.geometry.Pose2d;
import edu.wpi.first.math.geometry.Transform2d;
import edu.wpi.first.math.geometry.Transform3d;
import edu.wpi.first.math.kinematics.DifferentialDriveWheelSpeeds;
import edu.wpi.first.math.numbers.N1;
import edu.wpi.first.math.numbers.N3;
@@ -86,15 +84,11 @@ public class DrivetrainPoseEstimator {
var res = cam.getLatestResult();
if (res.hasTargets()) {
double imageCaptureTime = Timer.getFPGATimestamp() - res.getLatencyMillis();
Transform3d camToTargetTrans = res.getBestTarget().getCameraToTarget();
var transform =
new Transform2d(
camToTargetTrans.getTranslation().toTranslation2d(),
camToTargetTrans.getRotation().toRotation2d());
Pose2d camPose = Constants.kFarTargetPose.transformBy(transform.inverse());
var imageCaptureTime = Timer.getFPGATimestamp() - res.getLatencyMillis() / 1000.0;
var camToTargetTrans = res.getBestTarget().getBestCameraToTarget();
var camPose = Constants.kFarTargetPose.transformBy(camToTargetTrans.inverse());
m_poseEstimator.addVisionMeasurement(
camPose.transformBy(Constants.kCameraToRobot), imageCaptureTime);
camPose.transformBy(Constants.kCameraToRobot).toPose2d(), imageCaptureTime);
}
}

View File

@@ -90,7 +90,9 @@ public class DrivetrainSim {
Constants.kCamName,
camDiagFOV,
camPitch,
Constants.kCameraToRobot,
new Transform2d(
Constants.kCameraToRobot.getTranslation().toTranslation2d(),
Constants.kCameraToRobot.getRotation().toRotation2d()),
camHeightOffGround,
maxLEDRange,
camResolutionWidth,

View File

@@ -1,11 +1,16 @@
# We need to look for a JAR with the "-raspi" suffix so we don't accidentally bundle the big jar
# Not that it really matters, but it'll save us 50 megs or so
NEW_JAR=$(realpath $(find . -name photonvision\*-raspi.jar))
sudo apt install unzip zip
curl -sk https://api.github.com/repos/photonvision/photon-pi-gen/releases/tags/v2021.1.4 | grep "browser_download_url.*zip" | cut -d : -f 2,3 | tr -d '"' | wget -qi -
FILE_NAME=$(ls | grep image_*.zip)
unzip $FILE_NAME
echo "Using jar: " $NEW_JAR
sudo apt install xz-utils
curl -sk https://api.github.com/repos/photonvision/photon-pi-gen/releases/tags/v2023.1.0-beta-1 | grep "browser_download_url.*xz" | cut -d : -f 2,3 | tr -d '"' | wget -qi -
ls
FILE_NAME=$(ls | grep image_*.xz)
echo "Downloaded " $FILE_NAME
xz -T0 -v --decompress $FILE_NAME
IMAGE_FILE=$(ls | grep *.img)
ls
echo "Unziped image: " $IMAGE_FILE
TMP=$(mktemp -d)
LOOP=$(sudo losetup --show -fP "${IMAGE_FILE}")
sudo mount ${LOOP}p2 $TMP
@@ -16,8 +21,7 @@ sudo cp $NEW_JAR photonvision.jar
popd
sudo umount ${TMP}
sudo rmdir ${TMP}
rm $FILE_NAME
NEW_IMAGE=$(basename "${NEW_JAR/jar/img}")
mv $IMAGE_FILE $NEW_IMAGE
zip -r $(basename "${NEW_JAR/.jar/-image.zip}") $NEW_IMAGE
rm $NEW_IMAGE
xz -T0 -v -z $NEW_IMAGE
mv $NEW_IMAGE.xz $(basename "${NEW_JAR/.jar/-image.xz}")

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB