mirror of
https://github.com/PhotonVision/photonvision
synced 2026-06-27 02:01:40 +00:00
Compare commits
28 Commits
v2023.1.1-
...
v2023.1.1-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0f99044468 | ||
|
|
1412155c50 | ||
|
|
b1280e49d5 | ||
|
|
aaac6a4fbb | ||
|
|
b68b0ca5f6 | ||
|
|
45d99f1f6b | ||
|
|
a42fef67f2 | ||
|
|
bd4d74c192 | ||
|
|
c4500ce12b | ||
|
|
81d19672d2 | ||
|
|
04bde1b230 | ||
|
|
4f355f2749 | ||
|
|
5e604cf98d | ||
|
|
2d7a88e231 | ||
|
|
27198a3e32 | ||
|
|
fbf6fb304e | ||
|
|
d24a8d4188 | ||
|
|
def40484e3 | ||
|
|
aff163fc6a | ||
|
|
c392d5fa4d | ||
|
|
8dbd428359 | ||
|
|
ccd3a512d6 | ||
|
|
bfc5e45cd0 | ||
|
|
a1b09100e0 | ||
|
|
2bf7a77885 | ||
|
|
d1bfb86ab4 | ||
|
|
07904589df | ||
|
|
5540bbf115 |
79
.github/workflows/main.yml
vendored
79
.github/workflows/main.yml
vendored
@@ -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
1
.gitignore
vendored
@@ -30,6 +30,7 @@ backend/settings/
|
||||
*.nar
|
||||
*.ear
|
||||
*.zip
|
||||
*.xz
|
||||
*.tar.gz
|
||||
*.rar
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ modifiableFileExclude {
|
||||
\.jpg$
|
||||
\.jpeg$
|
||||
\.png$
|
||||
\.gif$
|
||||
\.so$
|
||||
\.dll$
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ plugins {
|
||||
|
||||
allprojects {
|
||||
repositories {
|
||||
jcenter()
|
||||
mavenCentral()
|
||||
maven { url = "https://maven.photonvision.org/repository/internal/" }
|
||||
}
|
||||
wpilibRepositories.addAllReleaseRepositories(it)
|
||||
|
||||
BIN
photon-client/src/assets/loading.gif
Normal file
BIN
photon-client/src/assets/loading.gif
Normal file
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 |
@@ -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');
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
@@ -181,7 +181,7 @@ export default {
|
||||
this.cubes = []
|
||||
|
||||
for (const target of this.targets) {
|
||||
const geometry = new BoxGeometry(0.2, 0.2, 0.3 / 5);
|
||||
const geometry = new BoxGeometry(0.3 / 5, 0.2, 0.2);
|
||||
const material = new MeshNormalMaterial();
|
||||
let quat = (new Quaternion(
|
||||
target.pose.qx,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
148
photon-client/src/plugins/WebsocketVideoStream.js
Normal file
148
photon-client/src/plugins/WebsocketVideoStream.js
Normal 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}
|
||||
@@ -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: [
|
||||
{
|
||||
|
||||
@@ -31,14 +31,6 @@
|
||||
:label-cols="$vuetify.breakpoint.mdAndUp ? undefined : 7"
|
||||
/>
|
||||
<br>
|
||||
<CVnumberinput
|
||||
v-model="cameraSettings.tiltDegrees"
|
||||
name="Camera pitch"
|
||||
tooltip="How many degrees above the horizontal the physical camera is tilted"
|
||||
:step="0.01"
|
||||
:label-cols="$vuetify.breakpoint.mdAndUp ? undefined : 7"
|
||||
/>
|
||||
<br>
|
||||
<v-btn
|
||||
style="margin-top:10px"
|
||||
small
|
||||
@@ -202,10 +194,13 @@
|
||||
>
|
||||
<CVslider
|
||||
v-model="$store.getters.currentPipelineSettings.cameraExposure"
|
||||
:disabled="$store.getters.currentPipelineSettings.cameraAutoExposure"
|
||||
name="Exposure"
|
||||
:min="0"
|
||||
:max="100"
|
||||
slider-cols="8"
|
||||
step="0.1"
|
||||
tooltip="Directly controls how much light is allowed to fall onto the sensor, which affects apparent brightness"
|
||||
@input="e => handlePipelineUpdate('cameraExposure', e)"
|
||||
/>
|
||||
<CVslider
|
||||
@@ -216,6 +211,13 @@
|
||||
slider-cols="8"
|
||||
@input="e => handlePipelineUpdate('cameraBrightness', e)"
|
||||
/>
|
||||
<CVswitch
|
||||
v-model="$store.getters.currentPipelineSettings.cameraAutoExposure"
|
||||
class="pt-2"
|
||||
name="Auto Exposure"
|
||||
tooltip="Enables or Disables camera automatic adjustment for current lighting conditions"
|
||||
@input="e => handlePipelineUpdate('cameraAutoExposure', e)"
|
||||
/>
|
||||
<CVslider
|
||||
v-if="$store.getters.currentPipelineSettings.cameraRedGain !== -1"
|
||||
v-model="$store.getters.currentPipelineSettings.cameraRedGain"
|
||||
@@ -289,7 +291,8 @@
|
||||
>
|
||||
<template>
|
||||
<CVimage
|
||||
:address="$store.getters.streamAddress[1]"
|
||||
:id="cameras-cal"
|
||||
:idx=1
|
||||
:disconnected="!$store.state.backendConnected"
|
||||
scale="100"
|
||||
style="border-radius: 5px;"
|
||||
@@ -360,6 +363,7 @@
|
||||
import CVselect from '../components/common/cv-select';
|
||||
import CVnumberinput from '../components/common/cv-number-input';
|
||||
import CVslider from '../components/common/cv-slider';
|
||||
import CVswitch from '../components/common/cv-switch';
|
||||
import CVimage from "../components/common/cv-image";
|
||||
import TooltippedLabel from "../components/common/cv-tooltipped-label";
|
||||
import jsPDF from "jspdf";
|
||||
@@ -372,6 +376,7 @@ export default {
|
||||
CVselect,
|
||||
CVnumberinput,
|
||||
CVslider,
|
||||
CVswitch,
|
||||
CVimage
|
||||
},
|
||||
data() {
|
||||
@@ -428,13 +433,11 @@ export default {
|
||||
return filtered
|
||||
}
|
||||
},
|
||||
|
||||
stringResolutionList: {
|
||||
get() {
|
||||
return this.filteredResolutionList.map(res => `${res['width']} X ${res['height']}`);
|
||||
}
|
||||
},
|
||||
|
||||
cameraSettings: {
|
||||
get() {
|
||||
return this.$store.getters.currentCameraSettings;
|
||||
@@ -443,7 +446,6 @@ export default {
|
||||
this.$store.commit('cameraSettings', value);
|
||||
}
|
||||
},
|
||||
|
||||
boardType: {
|
||||
get() {
|
||||
return this.calibrationData.boardType
|
||||
@@ -625,8 +627,7 @@ export default {
|
||||
this.axios.post("http://" + this.$address + "/api/settings/camera", {
|
||||
"settings": this.cameraSettings,
|
||||
"index": this.$store.state.currentCameraIndex
|
||||
}).then(
|
||||
function (response) {
|
||||
}).then(response => {
|
||||
if (response.status === 200) {
|
||||
this.$store.state.saveBar = true;
|
||||
}
|
||||
@@ -647,13 +648,14 @@ export default {
|
||||
if (this.isCalibrating === true) {
|
||||
data['takeCalibrationSnapshot'] = true
|
||||
} else {
|
||||
// This store prevents an edge case of a user not selecting a different resolution, which causes the set logic to not be called
|
||||
this.$store.commit('mutateCalibrationState', {['videoModeIndex']: this.filteredResolutionList[this.selectedFilteredResIndex].index});
|
||||
const calData = this.calibrationData;
|
||||
calData.isCalibrating = true;
|
||||
data['startPnpCalibration'] = calData;
|
||||
|
||||
console.log("starting calibration with index " + calData.videoModeIndex);
|
||||
}
|
||||
|
||||
this.$store.commit('currentPipelineIndex', -2);
|
||||
this.$socket.send(this.$msgPack.encode(data));
|
||||
},
|
||||
sendCalibrationFinish() {
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
<template>
|
||||
<div>
|
||||
<CVslider
|
||||
:disabled="cameraAutoExposure"
|
||||
v-model="cameraExposure"
|
||||
:disabled="cameraAutoExposure"
|
||||
name="Exposure"
|
||||
min="0"
|
||||
max="100"
|
||||
step="0.1"
|
||||
tooltip="Directly controls how much light is allowed to fall onto the sensor, which affects brightness"
|
||||
tooltip="Directly controls how much light is allowed to fall onto the sensor, which affects apparent brightness"
|
||||
:slider-cols="largeBox"
|
||||
@input="handlePipelineData('cameraExposure')"
|
||||
@rollback="e => rollback('cameraExposure', e)"
|
||||
@@ -25,19 +25,20 @@
|
||||
<CVswitch
|
||||
v-model="cameraAutoExposure"
|
||||
class="pt-2"
|
||||
name="Auto exposure"
|
||||
name="Auto Exposure"
|
||||
tooltip="Enables or Disables camera automatic adjustment for current lighting conditions"
|
||||
@input="handlePipelineData('cameraAutoExposure')"
|
||||
/>
|
||||
<CVslider
|
||||
v-if="cameraGain >= 0"
|
||||
v-model="cameraGain"
|
||||
name="Camera gain"
|
||||
name="Camera Gain"
|
||||
min="0"
|
||||
max="100"
|
||||
tooltip="Controls camera gain, similar to brightness"
|
||||
:slider-cols="largeBox"
|
||||
@input="handlePipelineData('cameraRedGain')"
|
||||
@rollback="e => rollback('cameraRedGain', e)"
|
||||
@input="handlePipelineData('cameraGain')"
|
||||
@rollback="e => rollback('cameraGain', e)"
|
||||
/>
|
||||
<CVslider
|
||||
v-if="cameraRedGain !== -1"
|
||||
@@ -144,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)
|
||||
|
||||
@@ -50,7 +50,7 @@
|
||||
</th>
|
||||
</template>
|
||||
<template v-if="$store.getters.pipelineType === 4 && $store.getters.currentPipelineSettings.solvePNPEnabled">
|
||||
<th class="text-center" >
|
||||
<th class="text-center">
|
||||
Ambiguity
|
||||
</th>
|
||||
</template>
|
||||
@@ -82,9 +82,9 @@
|
||||
<td>{{ (parseFloat(value.pose.angle_z) * 180 / Math.PI).toFixed(2) }}°</td>
|
||||
</template>
|
||||
<template v-if="$store.getters.pipelineType === 4 && $store.getters.currentPipelineSettings.solvePNPEnabled">
|
||||
<td>
|
||||
{{ parseFloat(value.ambiguity).toFixed(2) }}
|
||||
</td>
|
||||
<td>
|
||||
{{ parseFloat(value.ambiguity).toFixed(2) }}
|
||||
</td>
|
||||
</template>
|
||||
</tr>
|
||||
</tbody>
|
||||
|
||||
@@ -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) }}° 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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -189,7 +189,7 @@ public class NTDataPublisher implements CVPipelineResultConsumer {
|
||||
targetAreaEntry.forceSetDouble(bestTarget.getArea());
|
||||
targetSkewEntry.forceSetDouble(bestTarget.getSkew());
|
||||
|
||||
var pose = bestTarget.getCameraToTarget3d();
|
||||
var pose = bestTarget.getBestCameraToTarget3d();
|
||||
targetPoseEntry.forceSetDoubleArray(
|
||||
new double[] {
|
||||
pose.getTranslation().getX(),
|
||||
@@ -232,7 +232,8 @@ public class NTDataPublisher implements CVPipelineResultConsumer {
|
||||
t.getArea(),
|
||||
t.getSkew(),
|
||||
t.getFiducialId(),
|
||||
t.getCameraToTarget3d(),
|
||||
t.getBestCameraToTarget3d(),
|
||||
t.getAltCameraToTarget3d(),
|
||||
t.getPoseAmbiguity(),
|
||||
cornerList));
|
||||
}
|
||||
|
||||
@@ -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...");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -159,11 +159,21 @@ public class MathUtils {
|
||||
pose.getTranslation().rotateBy(rotationQuat), pose.getRotation().rotateBy(rotationQuat));
|
||||
}
|
||||
|
||||
// TODO: Refactor into new pipe?
|
||||
/**
|
||||
* All our solvepnp code returns a tag with X left, Y up, and Z out of the tag To better match
|
||||
* wpilib, we want to apply another rotation so that we get Z up, X out of the tag, and Y to the
|
||||
* right. We apply the following change of basis: X -> Y Y -> Z Z -> X
|
||||
*/
|
||||
private static final Rotation3d WPILIB_BASE_ROTATION =
|
||||
new Rotation3d(new MatBuilder<>(Nat.N3(), Nat.N3()).fill(0, 1, 0, 0, 0, 1, 1, 0, 0));
|
||||
|
||||
public static Pose3d convertOpenCVtoPhotonPose(Transform3d cameraToTarget3d) {
|
||||
// TODO: Refactor into new pipe?
|
||||
// CameraToTarget _should_ be in opencv-land EDN
|
||||
return CoordinateSystem.convert(
|
||||
new Pose3d(cameraToTarget3d), CoordinateSystem.EDN(), CoordinateSystem.NWU());
|
||||
var nwu =
|
||||
CoordinateSystem.convert(
|
||||
new Pose3d(cameraToTarget3d), CoordinateSystem.EDN(), CoordinateSystem.NWU());
|
||||
return new Pose3d(nwu.getTranslation(), WPILIB_BASE_ROTATION.rotateBy(nwu.getRotation()));
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
@@ -29,6 +29,17 @@ public class QuirkyCamera {
|
||||
0x5A3,
|
||||
CameraQuirk.CompletelyBroken), // Chris's older generic "Logitec HD Webcam"
|
||||
new QuirkyCamera(0x825, 0x46D, CameraQuirk.CompletelyBroken), // Logitec C270
|
||||
new QuirkyCamera(
|
||||
0x0bda,
|
||||
0x5510,
|
||||
CameraQuirk.CompletelyBroken), // A laptop internal camera someone found broken
|
||||
new QuirkyCamera(
|
||||
-1, -1, "Snap Camera", CameraQuirk.CompletelyBroken), // SnapCamera on Windows
|
||||
new QuirkyCamera(
|
||||
-1,
|
||||
-1,
|
||||
"FaceTime HD Camera",
|
||||
CameraQuirk.CompletelyBroken), // Mac Facetime Camera shared into Windows in Bootcamp
|
||||
new QuirkyCamera(0x2000, 0x1415, CameraQuirk.Gain, CameraQuirk.FPSCap100), // PS3Eye
|
||||
new QuirkyCamera(
|
||||
-1, -1, "mmal service 16.1", CameraQuirk.PiCam), // PiCam (via V4L2, not zerocopy)
|
||||
|
||||
@@ -65,7 +65,12 @@ public class USBCameraSource extends VisionSource {
|
||||
disableAutoFocus();
|
||||
|
||||
usbCameraSettables = new USBCameraSettables(config);
|
||||
usbFrameProvider = new USBFrameProvider(cvSink, usbCameraSettables);
|
||||
if (usbCameraSettables.getAllVideoModes().isEmpty()) {
|
||||
logger.info("Camera " + camera.getPath() + " has no video modes supported by PhotonVision");
|
||||
usbFrameProvider = null;
|
||||
} else {
|
||||
usbFrameProvider = new USBFrameProvider(cvSink, usbCameraSettables);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -118,31 +118,21 @@ public class Draw3dTargetsPipe
|
||||
ColorHelper.colorToScalar(Color.green),
|
||||
3);
|
||||
}
|
||||
for (int i = 0; i < bottomPoints.size(); i++) {
|
||||
Imgproc.line(
|
||||
in.getLeft(),
|
||||
bottomPoints.get(i),
|
||||
topPoints.get(i),
|
||||
ColorHelper.colorToScalar(Color.blue),
|
||||
3);
|
||||
}
|
||||
for (int i = 0; i < topPoints.size(); i++) {
|
||||
Imgproc.line(
|
||||
in.getLeft(),
|
||||
topPoints.get(i),
|
||||
topPoints.get((i + 1) % (bottomPoints.size())),
|
||||
ColorHelper.colorToScalar(Color.orange),
|
||||
3);
|
||||
}
|
||||
|
||||
// Draw X, Y and Z axis
|
||||
MatOfPoint3f pointMat = new MatOfPoint3f();
|
||||
// Those points are in opencv-land, but we are in NWU
|
||||
// NWU | EDN
|
||||
// X: Z
|
||||
// Y: -X
|
||||
// Z: -Y
|
||||
final double AXIS_LEN = 0.2;
|
||||
var list =
|
||||
List.of(
|
||||
new Point3(0, 0, 0),
|
||||
new Point3(0.2, 0, 0),
|
||||
new Point3(0, 0.2, 0),
|
||||
new Point3(0, 0, 0.2));
|
||||
new Point3(0, 0, AXIS_LEN),
|
||||
new Point3(AXIS_LEN, 0, 0),
|
||||
new Point3(0, AXIS_LEN, 0));
|
||||
pointMat.fromList(list);
|
||||
|
||||
Calib3d.projectPoints(
|
||||
@@ -157,12 +147,6 @@ public class Draw3dTargetsPipe
|
||||
dividePointList(axisPoints);
|
||||
|
||||
// Red = x, green y, blue z
|
||||
Imgproc.line(
|
||||
in.getLeft(),
|
||||
axisPoints.get(0),
|
||||
axisPoints.get(1),
|
||||
ColorHelper.colorToScalar(Color.RED),
|
||||
3);
|
||||
Imgproc.line(
|
||||
in.getLeft(),
|
||||
axisPoints.get(0),
|
||||
@@ -175,6 +159,29 @@ public class Draw3dTargetsPipe
|
||||
axisPoints.get(3),
|
||||
ColorHelper.colorToScalar(Color.BLUE),
|
||||
3);
|
||||
Imgproc.line(
|
||||
in.getLeft(),
|
||||
axisPoints.get(0),
|
||||
axisPoints.get(1),
|
||||
ColorHelper.colorToScalar(Color.RED),
|
||||
3);
|
||||
|
||||
for (int i = 0; i < bottomPoints.size(); i++) {
|
||||
Imgproc.line(
|
||||
in.getLeft(),
|
||||
bottomPoints.get(i),
|
||||
topPoints.get(i),
|
||||
ColorHelper.colorToScalar(Color.blue),
|
||||
3);
|
||||
}
|
||||
for (int i = 0; i < topPoints.size(); i++) {
|
||||
Imgproc.line(
|
||||
in.getLeft(),
|
||||
topPoints.get(i),
|
||||
topPoints.get((i + 1) % (bottomPoints.size())),
|
||||
ColorHelper.colorToScalar(Color.orange),
|
||||
3);
|
||||
}
|
||||
|
||||
tempMat.release();
|
||||
jac.release();
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -143,9 +143,13 @@ public class AprilTagPipeline extends CVPipeline<CVPipelineResult, AprilTagPipel
|
||||
new TargetCalculationParameters(
|
||||
false, null, null, null, null, frameStaticProperties));
|
||||
|
||||
var correctedPose = MathUtils.convertOpenCVtoPhotonPose(target.getCameraToTarget3d());
|
||||
target.setCameraToTarget3d(
|
||||
new Transform3d(correctedPose.getTranslation(), correctedPose.getRotation()));
|
||||
var correctedBestPose = MathUtils.convertOpenCVtoPhotonPose(target.getBestCameraToTarget3d());
|
||||
var correctedAltPose = MathUtils.convertOpenCVtoPhotonPose(target.getAltCameraToTarget3d());
|
||||
|
||||
target.setBestCameraToTarget3d(
|
||||
new Transform3d(correctedBestPose.getTranslation(), correctedBestPose.getRotation()));
|
||||
target.setAltCameraToTarget3d(
|
||||
new Transform3d(correctedAltPose.getTranslation(), correctedAltPose.getRotation()));
|
||||
|
||||
targetList.add(target);
|
||||
}
|
||||
|
||||
@@ -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,7 +40,6 @@ import org.photonvision.vision.camera.USBCameraSource;
|
||||
import org.photonvision.vision.camera.ZeroCopyPicamSource;
|
||||
import org.photonvision.vision.frame.Frame;
|
||||
import org.photonvision.vision.frame.consumer.FileSaveFrameConsumer;
|
||||
import org.photonvision.vision.frame.consumer.MJPGFrameConsumer;
|
||||
import org.photonvision.vision.pipeline.AdvancedPipelineSettings;
|
||||
import org.photonvision.vision.pipeline.OutputStreamPipeline;
|
||||
import org.photonvision.vision.pipeline.ReflectivePipelineSettings;
|
||||
@@ -49,6 +47,8 @@ import org.photonvision.vision.pipeline.UICalibrationData;
|
||||
import org.photonvision.vision.pipeline.result.CVPipelineResult;
|
||||
import org.photonvision.vision.target.TargetModel;
|
||||
import org.photonvision.vision.target.TrackedTarget;
|
||||
import org.photonvision.vision.videoStream.SocketVideoStream;
|
||||
import org.photonvision.vision.videoStream.SocketVideoStreamManager;
|
||||
|
||||
/**
|
||||
* This is the God Class
|
||||
@@ -57,32 +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(
|
||||
@@ -130,7 +129,7 @@ public class VisionModule {
|
||||
|
||||
createStreams();
|
||||
|
||||
recreateFpsLimitedResultConsumers();
|
||||
recreateStreamResultConsumers();
|
||||
|
||||
ntConsumer =
|
||||
new NTDataPublisher(
|
||||
@@ -167,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 {
|
||||
@@ -271,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);
|
||||
@@ -304,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 + ")");
|
||||
|
||||
@@ -474,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();
|
||||
@@ -516,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) {
|
||||
@@ -567,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
|
||||
@@ -580,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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,7 +46,8 @@ public class TrackedTarget implements Releasable {
|
||||
private double m_area;
|
||||
private double m_skew;
|
||||
|
||||
private Transform3d m_cameraToTarget3d = new Transform3d();
|
||||
private Transform3d m_bestCameraToTarget3d = new Transform3d();
|
||||
private Transform3d m_altCameraToTarget3d = new Transform3d();
|
||||
|
||||
private CVShape m_shape;
|
||||
|
||||
@@ -74,15 +75,20 @@ public class TrackedTarget implements Releasable {
|
||||
TargetCalculations.calculateYaw(
|
||||
result.getCenterX(), params.cameraCenterPoint.x, params.horizontalFocalLength);
|
||||
var bestPose = new Transform3d();
|
||||
var altPose = new Transform3d();
|
||||
if (result.getError1() <= result.getError2()) {
|
||||
bestPose = result.getPoseResult1();
|
||||
altPose = result.getPoseResult2();
|
||||
} else {
|
||||
bestPose = result.getPoseResult2();
|
||||
altPose = result.getPoseResult1();
|
||||
}
|
||||
|
||||
bestPose = MathUtils.convertApriltagtoOpenCV(bestPose);
|
||||
altPose = MathUtils.convertApriltagtoOpenCV(altPose);
|
||||
|
||||
m_cameraToTarget3d = bestPose;
|
||||
m_bestCameraToTarget3d = bestPose;
|
||||
m_altCameraToTarget3d = altPose;
|
||||
|
||||
double[] corners = result.getCorners();
|
||||
Point[] cornerPoints =
|
||||
@@ -231,12 +237,20 @@ public class TrackedTarget implements Releasable {
|
||||
return !m_subContours.isEmpty();
|
||||
}
|
||||
|
||||
public Transform3d getCameraToTarget3d() {
|
||||
return m_cameraToTarget3d;
|
||||
public Transform3d getBestCameraToTarget3d() {
|
||||
return m_bestCameraToTarget3d;
|
||||
}
|
||||
|
||||
public void setCameraToTarget3d(Transform3d pose) {
|
||||
this.m_cameraToTarget3d = pose;
|
||||
public Transform3d getAltCameraToTarget3d() {
|
||||
return m_altCameraToTarget3d;
|
||||
}
|
||||
|
||||
public void setBestCameraToTarget3d(Transform3d pose) {
|
||||
this.m_bestCameraToTarget3d = pose;
|
||||
}
|
||||
|
||||
public void setAltCameraToTarget3d(Transform3d pose) {
|
||||
this.m_altCameraToTarget3d = pose;
|
||||
}
|
||||
|
||||
public Mat getCameraRelativeTvec() {
|
||||
@@ -272,8 +286,8 @@ public class TrackedTarget implements Releasable {
|
||||
ret.put("skew", getSkew());
|
||||
ret.put("area", getArea());
|
||||
ret.put("ambiguity", getPoseAmbiguity());
|
||||
if (getCameraToTarget3d() != null) {
|
||||
ret.put("pose", transformToMap(getCameraToTarget3d()));
|
||||
if (getBestCameraToTarget3d() != null) {
|
||||
ret.put("pose", transformToMap(getBestCameraToTarget3d()));
|
||||
}
|
||||
ret.put("fiducialId", getFiducialId());
|
||||
return ret;
|
||||
@@ -289,7 +303,7 @@ public class TrackedTarget implements Releasable {
|
||||
ret.put("qy", transform.getRotation().getQuaternion().getY());
|
||||
ret.put("qz", transform.getRotation().getQuaternion().getZ());
|
||||
|
||||
ret.put("angle_z", transform.getRotation().getZ() + Math.PI / 2.0);
|
||||
ret.put("angle_z", transform.getRotation().getZ());
|
||||
return ret;
|
||||
}
|
||||
|
||||
|
||||
@@ -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--;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -111,19 +111,19 @@ public class SolvePNPTest {
|
||||
printTestResults(pipelineResult);
|
||||
|
||||
// these numbers are not *accurate*, but they are known and expected
|
||||
var pose = pipelineResult.targets.get(0).getCameraToTarget3d();
|
||||
var pose = pipelineResult.targets.get(0).getBestCameraToTarget3d();
|
||||
Assertions.assertEquals(1.1, pose.getTranslation().getX(), 0.05);
|
||||
Assertions.assertEquals(0.0, pose.getTranslation().getY(), 0.05);
|
||||
|
||||
// We expect the object X axis to be to the right, or negative-Y in world space
|
||||
// We expect the object X to be forward, or -X in world space
|
||||
Assertions.assertEquals(
|
||||
-1, new Translation3d(1, 0, 0).rotateBy(pose.getRotation()).getY(), 0.05);
|
||||
// We expect the object Y axis to be up, or +Z in world space
|
||||
-1, new Translation3d(1, 0, 0).rotateBy(pose.getRotation()).getX(), 0.05);
|
||||
// We expect the object Y axis to be right, or negative-Y in world space
|
||||
Assertions.assertEquals(
|
||||
1, new Translation3d(0, 1, 0).rotateBy(pose.getRotation()).getZ(), 0.05);
|
||||
// We expect the object Z axis to towards the camera, or negative-X in world space
|
||||
-1, new Translation3d(0, 1, 0).rotateBy(pose.getRotation()).getY(), 0.05);
|
||||
// We expect the object Z axis to be up, or +Z in world space
|
||||
Assertions.assertEquals(
|
||||
-1, new Translation3d(0, 0, 1).rotateBy(pose.getRotation()).getX(), 0.05);
|
||||
1, new Translation3d(0, 0, 1).rotateBy(pose.getRotation()).getZ(), 0.05);
|
||||
|
||||
TestUtils.showImage(pipelineResult.outputFrame.image.getMat(), "Pipeline output", 999999);
|
||||
}
|
||||
@@ -159,10 +159,11 @@ public class SolvePNPTest {
|
||||
pipelineResult.targets);
|
||||
|
||||
// these numbers are not *accurate*, but they are known and expected
|
||||
var pose = pipelineResult.targets.get(0).getCameraToTarget3d();
|
||||
var pose = pipelineResult.targets.get(0).getBestCameraToTarget3d();
|
||||
Assertions.assertEquals(Units.inchesToMeters(240.26), pose.getTranslation().getX(), 0.05);
|
||||
Assertions.assertEquals(Units.inchesToMeters(35), pose.getTranslation().getY(), 0.05);
|
||||
Assertions.assertEquals(Units.degreesToRadians(-42), pose.getRotation().getZ(), 1);
|
||||
// Z rotation should be mostly facing us
|
||||
Assertions.assertEquals(Units.degreesToRadians(-140), pose.getRotation().getZ(), 1);
|
||||
|
||||
TestUtils.showImage(pipelineResult.inputFrame.image.getMat(), "Pipeline output", 999999);
|
||||
}
|
||||
@@ -210,7 +211,7 @@ public class SolvePNPTest {
|
||||
System.out.println(
|
||||
"Found targets at "
|
||||
+ pipelineResult.targets.stream()
|
||||
.map(TrackedTarget::getCameraToTarget3d)
|
||||
.map(TrackedTarget::getBestCameraToTarget3d)
|
||||
.collect(Collectors.toList()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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()
|
||||
};
|
||||
|
||||
@@ -171,6 +171,7 @@ 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),
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -41,12 +41,12 @@ PhotonTrackedTarget::PhotonTrackedTarget(
|
||||
area(area),
|
||||
skew(skew),
|
||||
fiducialId(id),
|
||||
cameraToTarget(pose),
|
||||
bestCameraToTarget(pose),
|
||||
corners(corners) {}
|
||||
|
||||
bool PhotonTrackedTarget::operator==(const PhotonTrackedTarget& other) const {
|
||||
return other.yaw == yaw && other.pitch == pitch && other.area == area &&
|
||||
other.skew == skew && other.cameraToTarget == cameraToTarget &&
|
||||
other.skew == skew && other.bestCameraToTarget == bestCameraToTarget &&
|
||||
other.corners == corners;
|
||||
}
|
||||
|
||||
@@ -56,13 +56,21 @@ bool PhotonTrackedTarget::operator!=(const PhotonTrackedTarget& other) const {
|
||||
|
||||
Packet& operator<<(Packet& packet, const PhotonTrackedTarget& target) {
|
||||
packet << target.yaw << target.pitch << target.area << target.skew
|
||||
<< target.fiducialId << target.cameraToTarget.Translation().X().value()
|
||||
<< target.cameraToTarget.Translation().Y().value()
|
||||
<< target.cameraToTarget.Translation().Z().value()
|
||||
<< target.cameraToTarget.Rotation().GetQuaternion().W()
|
||||
<< target.cameraToTarget.Rotation().GetQuaternion().X()
|
||||
<< target.cameraToTarget.Rotation().GetQuaternion().Y()
|
||||
<< target.cameraToTarget.Rotation().GetQuaternion().Z()
|
||||
<< target.fiducialId
|
||||
<< target.bestCameraToTarget.Translation().X().value()
|
||||
<< target.bestCameraToTarget.Translation().Y().value()
|
||||
<< target.bestCameraToTarget.Translation().Z().value()
|
||||
<< target.bestCameraToTarget.Rotation().GetQuaternion().W()
|
||||
<< target.bestCameraToTarget.Rotation().GetQuaternion().X()
|
||||
<< target.bestCameraToTarget.Rotation().GetQuaternion().Y()
|
||||
<< target.bestCameraToTarget.Rotation().GetQuaternion().Z()
|
||||
<< target.altCameraToTarget.Translation().X().value()
|
||||
<< target.altCameraToTarget.Translation().Y().value()
|
||||
<< target.altCameraToTarget.Translation().Z().value()
|
||||
<< target.altCameraToTarget.Rotation().GetQuaternion().W()
|
||||
<< target.altCameraToTarget.Rotation().GetQuaternion().X()
|
||||
<< target.altCameraToTarget.Rotation().GetQuaternion().Y()
|
||||
<< target.altCameraToTarget.Rotation().GetQuaternion().Z()
|
||||
<< target.poseAmbiguity;
|
||||
|
||||
for (int i = 0; i < 4; i++) {
|
||||
@@ -75,17 +83,28 @@ Packet& operator<<(Packet& packet, const PhotonTrackedTarget& target) {
|
||||
Packet& operator>>(Packet& packet, PhotonTrackedTarget& target) {
|
||||
packet >> target.yaw >> target.pitch >> target.area >> target.skew >>
|
||||
target.fiducialId;
|
||||
|
||||
// We use these for best and alt transforms below
|
||||
double x = 0;
|
||||
double y = 0;
|
||||
double z = 0;
|
||||
double w = 0;
|
||||
|
||||
// First transform is the "best" pose
|
||||
packet >> x >> y >> z;
|
||||
const auto translation = frc::Translation3d(
|
||||
const auto bestTranslation = frc::Translation3d(
|
||||
units::meter_t(x), units::meter_t(y), units::meter_t(z));
|
||||
packet >> w >> x >> y >> z;
|
||||
const auto rotation = frc::Rotation3d(frc::Quaternion(w, x, y, z));
|
||||
const auto bestRotation = frc::Rotation3d(frc::Quaternion(w, x, y, z));
|
||||
target.bestCameraToTarget = frc::Transform3d(bestTranslation, bestRotation);
|
||||
|
||||
target.cameraToTarget = frc::Transform3d(translation, rotation);
|
||||
// Second transform is the "alternate" pose
|
||||
packet >> x >> y >> z;
|
||||
const auto altTranslation = frc::Translation3d(
|
||||
units::meter_t(x), units::meter_t(y), units::meter_t(z));
|
||||
packet >> w >> x >> y >> z;
|
||||
const auto altRotation = frc::Rotation3d(frc::Quaternion(w, x, y, z));
|
||||
target.altCameraToTarget = frc::Transform3d(altTranslation, altRotation);
|
||||
|
||||
packet >> target.poseAmbiguity;
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -97,10 +97,22 @@ class PhotonTrackedTarget {
|
||||
double GetPoseAmbiguity() const { return poseAmbiguity; }
|
||||
|
||||
/**
|
||||
* Returns the pose of the target relative to the robot.
|
||||
* Get the transform that maps camera space (X = forward, Y = left, Z = up) to
|
||||
* object/fiducial tag space (X forward, Y left, Z up) with the lowest
|
||||
* reprojection error. The ratio between this and the alternate target's
|
||||
* reprojection error is the ambiguity, which is between 0 and 1.
|
||||
* @return The pose of the target relative to the robot.
|
||||
*/
|
||||
frc::Transform3d GetCameraToTarget() const { return cameraToTarget; }
|
||||
frc::Transform3d GetBestCameraToTarget() const { return bestCameraToTarget; }
|
||||
|
||||
/**
|
||||
* Get the transform that maps camera space (X = forward, Y = left, Z = up) to
|
||||
* object/fiducial tag space (X forward, Y left, Z up) with the highest
|
||||
* reprojection error
|
||||
*/
|
||||
frc::Transform3d GetAlternateCameraToTarget() const {
|
||||
return altCameraToTarget;
|
||||
}
|
||||
|
||||
bool operator==(const PhotonTrackedTarget& other) const;
|
||||
bool operator!=(const PhotonTrackedTarget& other) const;
|
||||
@@ -114,7 +126,8 @@ class PhotonTrackedTarget {
|
||||
double area = 0;
|
||||
double skew = 0;
|
||||
int fiducialId;
|
||||
frc::Transform3d cameraToTarget;
|
||||
frc::Transform3d bestCameraToTarget;
|
||||
frc::Transform3d altCameraToTarget;
|
||||
double poseAmbiguity;
|
||||
wpi::SmallVector<std::pair<double, double>, 4> corners;
|
||||
};
|
||||
|
||||
@@ -45,6 +45,7 @@ class PacketTest {
|
||||
-5.0,
|
||||
-1,
|
||||
new Transform3d(new Translation3d(), new Rotation3d()),
|
||||
new Transform3d(new Translation3d(), new Rotation3d()),
|
||||
0.25,
|
||||
List.of(
|
||||
new TargetCorner(1, 2),
|
||||
@@ -82,6 +83,7 @@ class PacketTest {
|
||||
4.0,
|
||||
2,
|
||||
new Transform3d(new Translation3d(1, 2, 3), new Rotation3d(1, 2, 3)),
|
||||
new Transform3d(new Translation3d(1, 2, 3), new Rotation3d(1, 2, 3)),
|
||||
0.25,
|
||||
List.of(
|
||||
new TargetCorner(1, 2),
|
||||
@@ -95,6 +97,7 @@ class PacketTest {
|
||||
6.7,
|
||||
3,
|
||||
new Transform3d(new Translation3d(4, 2, 3), new Rotation3d(1, 5, 3)),
|
||||
new Transform3d(new Translation3d(4, 2, 3), new Rotation3d(1, 5, 3)),
|
||||
0.25,
|
||||
List.of(
|
||||
new TargetCorner(1, 2),
|
||||
|
||||
@@ -43,9 +43,16 @@ task copyClientUIToResources(type: Copy) {
|
||||
into "${projectDir}/src/main/resources/web/"
|
||||
}
|
||||
|
||||
task copyThinclientToResources(type: Copy) {
|
||||
from "${projectDir}/../photon-thinclient/"
|
||||
into "${projectDir}/src/main/resources/web/"
|
||||
}
|
||||
|
||||
|
||||
task buildAndCopyUI {}
|
||||
|
||||
buildAndCopyUI.dependsOn copyClientUIToResources
|
||||
buildAndCopyUI.dependsOn copyThinclientToResources
|
||||
copyClientUIToResources.dependsOn runNpmOnClient
|
||||
copyClientUIToResources.shouldRunAfter runNpmOnClient
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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(
|
||||
() ->
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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!");
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Binary file not shown.
@@ -1 +1 @@
|
||||
<!DOCTYPE html><html lang=en><head><meta charset=utf-8><meta http-equiv=X-UA-Compatible content="IE=edge"><meta name=viewport content="width=device-width,initial-scale=1"><link rel=icon href=/favicon.png><title>PhotonVision</title><link href=/js/chunk-2d216214.4302abb0.js rel=prefetch><link href=/js/chunk-2d216257.0de4b8cc.js rel=prefetch><link href=/js/chunk-ad0384ec.7dafafc8.js rel=prefetch><link href=/css/app.a6eec7e2.css rel=preload as=style><link href=/css/chunk-vendors.db45c662.css rel=preload as=style><link href=/js/app.612f1f56.js rel=preload as=script><link href=/js/chunk-vendors.58a768b6.js rel=preload as=script><link href=/css/chunk-vendors.db45c662.css rel=stylesheet><link href=/css/app.a6eec7e2.css rel=stylesheet></head><body><noscript><strong>We're sorry but PhotonVision doesn't work properly without JavaScript enabled. Please enable it to continue.</strong></noscript><div id=app></div><script src=/js/chunk-vendors.58a768b6.js></script><script src=/js/app.612f1f56.js></script></body></html>
|
||||
<!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>
|
||||
@@ -69,7 +69,7 @@ public class AprilTagTest {
|
||||
TestUtils.showImage(pipelineResult.inputFrame.image.getMat(), "Pipeline output", 999999);
|
||||
|
||||
// these numbers are not *accurate*, but they are known and expected
|
||||
var pose = pipelineResult.targets.get(0).getCameraToTarget3d();
|
||||
var pose = pipelineResult.targets.get(0).getBestCameraToTarget3d();
|
||||
Assertions.assertEquals(2, pose.getTranslation().getX(), 0.2);
|
||||
Assertions.assertEquals(0.0, pose.getTranslation().getY(), 0.2);
|
||||
Assertions.assertEquals(0.0, pose.getTranslation().getY(), 0.2);
|
||||
@@ -79,15 +79,14 @@ public class AprilTagTest {
|
||||
var objZ = new Translation3d(0, 0, 1).rotateBy(pose.getRotation()).getX();
|
||||
System.out.printf("Object x %.2f y %.2f z %.2f\n", objX, objY, objZ);
|
||||
|
||||
// We expect the object X axis to be to the right, or negative-Y in world space
|
||||
// We expect the object X to be forward, or -X in world space
|
||||
Assertions.assertEquals(
|
||||
-1, new Translation3d(1, 0, 0).rotateBy(pose.getRotation()).getY(), 0.08);
|
||||
// We expect the object Y axis to be up, or +Z in world space
|
||||
-1, new Translation3d(1, 0, 0).rotateBy(pose.getRotation()).getX(), 0.1);
|
||||
// We expect the object Y axis to be right, or negative-Y in world space
|
||||
Assertions.assertEquals(
|
||||
1, new Translation3d(0, 1, 0).rotateBy(pose.getRotation()).getZ(), 0.08);
|
||||
// We expect the object Z axis to towards the camera, or negative-X in world space
|
||||
Assertions.assertEquals(
|
||||
-1, new Translation3d(0, 0, 1).rotateBy(pose.getRotation()).getX(), 0.08);
|
||||
-1, new Translation3d(0, 1, 0).rotateBy(pose.getRotation()).getY(), 0.1);
|
||||
// We expect the object Z axis to be up, or +Z in world space
|
||||
Assertions.assertEquals(1, new Translation3d(0, 0, 1).rotateBy(pose.getRotation()).getZ(), 0.1);
|
||||
}
|
||||
|
||||
private static void printTestResults(CVPipelineResult pipelineResult) {
|
||||
@@ -98,7 +97,7 @@ public class AprilTagTest {
|
||||
System.out.println(
|
||||
"Found targets at "
|
||||
+ pipelineResult.targets.stream()
|
||||
.map(TrackedTarget::getCameraToTarget3d)
|
||||
.map(TrackedTarget::getBestCameraToTarget3d)
|
||||
.collect(Collectors.toList()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,14 +27,15 @@ import java.util.Objects;
|
||||
import org.photonvision.common.dataflow.structures.Packet;
|
||||
|
||||
public class PhotonTrackedTarget {
|
||||
public static final int PACK_SIZE_BYTES = Double.BYTES * (5 + 7 + 2 * 4 + 1);
|
||||
public static final int PACK_SIZE_BYTES = Double.BYTES * (5 + 7 + 2 * 4 + 1 + 7);
|
||||
|
||||
private double yaw;
|
||||
private double pitch;
|
||||
private double area;
|
||||
private double skew;
|
||||
private int fiducialId;
|
||||
private Transform3d cameraToTarget = new Transform3d();
|
||||
private Transform3d bestCameraToTarget = new Transform3d();
|
||||
private Transform3d altCameraToTarget = new Transform3d();
|
||||
private double poseAmbiguity;
|
||||
private List<TargetCorner> targetCorners;
|
||||
|
||||
@@ -48,6 +49,7 @@ public class PhotonTrackedTarget {
|
||||
double skew,
|
||||
int id,
|
||||
Transform3d pose,
|
||||
Transform3d altPose,
|
||||
double ambiguity,
|
||||
List<TargetCorner> corners) {
|
||||
assert corners.size() == 4;
|
||||
@@ -56,7 +58,8 @@ public class PhotonTrackedTarget {
|
||||
this.area = area;
|
||||
this.skew = skew;
|
||||
this.fiducialId = id;
|
||||
this.cameraToTarget = pose;
|
||||
this.bestCameraToTarget = pose;
|
||||
this.altCameraToTarget = altPose;
|
||||
this.targetCorners = corners;
|
||||
this.poseAmbiguity = ambiguity;
|
||||
}
|
||||
@@ -100,10 +103,18 @@ public class PhotonTrackedTarget {
|
||||
|
||||
/**
|
||||
* Get the transform that maps camera space (X = forward, Y = left, Z = up) to object/fiducial tag
|
||||
* space (X right, Y up, Z towards the camera/out of the wall)
|
||||
* space (X forward, Y left, Z up) with the lowest reprojection error
|
||||
*/
|
||||
public Transform3d getCameraToTarget() {
|
||||
return cameraToTarget;
|
||||
public Transform3d getBestCameraToTarget() {
|
||||
return bestCameraToTarget;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the transform that maps camera space (X = forward, Y = left, Z = up) to object/fiducial tag
|
||||
* space (X forward, Y left, Z up) with the highest reprojection error
|
||||
*/
|
||||
public Transform3d getAlternateCameraToTarget() {
|
||||
return altCameraToTarget;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -114,13 +125,37 @@ public class PhotonTrackedTarget {
|
||||
return Double.compare(that.yaw, yaw) == 0
|
||||
&& Double.compare(that.pitch, pitch) == 0
|
||||
&& Double.compare(that.area, area) == 0
|
||||
&& Objects.equals(cameraToTarget, that.cameraToTarget)
|
||||
&& Objects.equals(bestCameraToTarget, that.bestCameraToTarget)
|
||||
&& Objects.equals(altCameraToTarget, that.altCameraToTarget)
|
||||
&& Objects.equals(targetCorners, that.targetCorners);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(yaw, pitch, area, cameraToTarget);
|
||||
return Objects.hash(yaw, pitch, area, bestCameraToTarget, altCameraToTarget);
|
||||
}
|
||||
|
||||
private static Transform3d decodeTransform(Packet packet) {
|
||||
double x = packet.decodeDouble();
|
||||
double y = packet.decodeDouble();
|
||||
double z = packet.decodeDouble();
|
||||
var translation = new Translation3d(x, y, z);
|
||||
double w = packet.decodeDouble();
|
||||
x = packet.decodeDouble();
|
||||
y = packet.decodeDouble();
|
||||
z = packet.decodeDouble();
|
||||
var rotation = new Rotation3d(new Quaternion(w, x, y, z));
|
||||
return new Transform3d(translation, rotation);
|
||||
}
|
||||
|
||||
private static void encodeTransform(Packet packet, Transform3d transform) {
|
||||
packet.encode(transform.getTranslation().getX());
|
||||
packet.encode(transform.getTranslation().getY());
|
||||
packet.encode(transform.getTranslation().getZ());
|
||||
packet.encode(transform.getRotation().getQuaternion().getW());
|
||||
packet.encode(transform.getRotation().getQuaternion().getX());
|
||||
packet.encode(transform.getRotation().getQuaternion().getY());
|
||||
packet.encode(transform.getRotation().getQuaternion().getZ());
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -136,16 +171,9 @@ public class PhotonTrackedTarget {
|
||||
this.skew = packet.decodeDouble();
|
||||
this.fiducialId = packet.decodeInt();
|
||||
|
||||
double x = packet.decodeDouble();
|
||||
double y = packet.decodeDouble();
|
||||
double z = packet.decodeDouble();
|
||||
var translation = new Translation3d(x, y, z);
|
||||
double w = packet.decodeDouble();
|
||||
x = packet.decodeDouble();
|
||||
y = packet.decodeDouble();
|
||||
z = packet.decodeDouble();
|
||||
var rotation = new Rotation3d(new Quaternion(w, x, y, z));
|
||||
this.cameraToTarget = new Transform3d(translation, rotation);
|
||||
this.bestCameraToTarget = decodeTransform(packet);
|
||||
this.altCameraToTarget = decodeTransform(packet);
|
||||
|
||||
this.poseAmbiguity = packet.decodeDouble();
|
||||
|
||||
this.targetCorners = new ArrayList<>(4);
|
||||
@@ -170,13 +198,8 @@ public class PhotonTrackedTarget {
|
||||
packet.encode(area);
|
||||
packet.encode(skew);
|
||||
packet.encode(fiducialId);
|
||||
packet.encode(cameraToTarget.getTranslation().getX());
|
||||
packet.encode(cameraToTarget.getTranslation().getY());
|
||||
packet.encode(cameraToTarget.getTranslation().getZ());
|
||||
packet.encode(cameraToTarget.getRotation().getQuaternion().getW());
|
||||
packet.encode(cameraToTarget.getRotation().getQuaternion().getX());
|
||||
packet.encode(cameraToTarget.getRotation().getQuaternion().getY());
|
||||
packet.encode(cameraToTarget.getRotation().getQuaternion().getZ());
|
||||
encodeTransform(packet, bestCameraToTarget);
|
||||
encodeTransform(packet, altCameraToTarget);
|
||||
packet.encode(poseAmbiguity);
|
||||
|
||||
for (int i = 0; i < 4; i++) {
|
||||
@@ -201,7 +224,7 @@ public class PhotonTrackedTarget {
|
||||
+ ", fiducialId="
|
||||
+ fiducialId
|
||||
+ ", cameraToTarget="
|
||||
+ cameraToTarget
|
||||
+ bestCameraToTarget
|
||||
+ ", targetCorners="
|
||||
+ targetCorners
|
||||
+ '}';
|
||||
|
||||
BIN
photon-thinclient/loading.gif
Normal file
BIN
photon-thinclient/loading.gif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 12 KiB |
223
photon-thinclient/thinclient.html
Normal file
223
photon-thinclient/thinclient.html
Normal 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>
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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}")
|
||||
|
||||
Reference in New Issue
Block a user