Compare commits

...

39 Commits

Author SHA1 Message Date
Matt
e58c27caa2 Bump LL image to fix NetworkManager (#780) 2023-01-31 06:57:45 -06:00
Matt
f6e3c9b3ee Fix desync between web UI and NT (#778)
Actually calls VisionModule::setPipeline when changing pipelines (needed to change video modes)
2023-01-29 23:30:34 -05:00
Matt
88ed2ebf51 Add PhotonVersion to sources/headers zip (#777)
* Add PhotonVersion to sources/headers zip

* Update publish.gradle
2023-01-29 23:30:22 -05:00
David Vo
5f39123bde photon-lib: Fix C++ sources publish classifier (#765)
The canonical classifier is sources, not source.
2023-01-27 10:52:14 -05:00
Matt
37a7d378fd Fix publish type in photoncamera (#760) 2023-01-22 10:56:41 -05:00
Matt
811fef1212 Bump pi image versions (#747) 2023-01-18 16:31:42 -05:00
Matt
d0162b0ed0 Switch network management to networkmanager on Linux (#738)
* Switch network management to networkmanager

* Run style

* Fix command formatting

* Add curst Pi 5 second sleep

* Run formatter

* Also bring up/down on other linux

* Switch to nmcli down/up

* Remove sleep in nmcli down/up

* Address review
2023-01-18 16:31:14 -05:00
Matt
9d6997180d Add calibdb upload button (#735)
* Add calibdb upload

* Fix distortion coefficients size
2023-01-18 16:29:58 -05:00
smoser-frc
a985c6cf3a Fix #748 - add libopencv-core4.5 for aarch64 systems. (#749)
* Add and use a function in install.sh to determine if package is installed.

Move the "is a package installed" code into a function.

* Install libopencv-core4.5 on aarch64, which is likely raspberry pi.

The libphotonvision.so on Raspberry pi depends on libopencv-core4.5.
The code here installs that package on all aarch64 systems, as
there was not an obvious way to install on only Raspberry pi systems.

Fixes #748.

Co-authored-by: Scott Moser <smoser@brickies.net>
2023-01-18 09:25:10 -05:00
Sriman Achanta
167a4661ca [NFC] Update RobotPoseEstimator documentation (#740)
* update documentation

* add suggested changes

* rename April Tag to AprilTag

* Update RobotPoseEstimator.java

Co-authored-by: Mohammad Durrani <46766905+mdurrani808@users.noreply.github.com>
2023-01-17 20:34:21 -05:00
Matt
a16ac4af57 Bump to wpilib 2023.2.1 (#741) 2023-01-15 10:12:25 -05:00
Matt
d9f99f9c9b Add calibration decimate dropdown (#739)
* Increase resized size to 640

* Add calibration decimation dropdown

* Update Calibrate3dPipeTest.java

* Only allow decimation down to >=320x240

* Update CamerasView.vue
2023-01-14 19:23:14 -06:00
Andrew Gasser
357d8a518a Return named type from PhotonPoseEstimator (#734)
Adds PhotonPoseEstimator class, and deprecates RobotPoseEstimator
2023-01-14 10:06:15 -05:00
Matt
073714f0bc [AprilTags] Reduce default iterations to 40 (#726)
Co-authored-by: Mohammad Durrani <46766905+mdurrani808@users.noreply.github.com>
2023-01-11 16:32:31 -05:00
Nick Hadley
39f6ab8805 Add method to clear sim targets (#733)
Closes #731
2023-01-11 12:33:19 -05:00
Mohammad Durrani
5c66785095 Delete EigenCore.h (#732) 2023-01-10 21:37:22 -08:00
Matt
53c67a07e4 [photonlib] Only link to apriltag_shared (#730) 2023-01-10 10:09:24 -05:00
Matt
7c985e3a84 Remove force istestmode in Main (#723) 2023-01-10 09:13:59 -05:00
Jack
80e16ece87 Add hostname to camerapublisher mjpeg stream (#722)
Closes #721
2023-01-09 13:11:49 -05:00
Matt
86b9d4b037 Add 2023 pics to test mode (#720) 2023-01-07 20:48:21 -05:00
Chris Gerth
e12f360a29 Update cv-select.vue (#719) 2023-01-07 10:54:54 -05:00
Declan
d0641d0cb6 Fix the reflective mode color picker (#715)
Co-authored-by: Mohammad Durrani <46766905+mdurrani808@users.noreply.github.com>
2023-01-07 09:51:01 -05:00
Declan
871aa8b44b Clean up AprilTag tab visuals and code a little (#717)
Co-authored-by: Mohammad Durrani <46766905+mdurrani808@users.noreply.github.com>
Co-authored-by: Chris Gerth <gerth2@users.noreply.github.com>
2023-01-07 09:50:51 -05:00
Declan
beaee9f6c0 Don't give ArUco as an option in the UI, for now (#713)
Seems to be broken on things going through the libcamera path. Odd.
Hopefully we can re-enable this later on.
2023-01-07 08:17:58 -06:00
Declan
11f5069148 Hide or disable unsuitable items in output tab in tag mode (#714) 2023-01-07 08:17:31 -06:00
Declan
6716d41a62 Filter out rotation modes that are broken in libcamera driver (#716) 2023-01-07 08:16:01 -06:00
amquake
63b3cfe7e1 Remove distortion logs (#712)
* remove distortion logs

* spotless

* Run spotless

💀

Co-authored-by: Matt <matthew.morley.ca@gmail.com>
2023-01-06 23:09:58 -05:00
Matt
967be84b4b Expose detected tag corners (#702)
Removes GetCorners, replaces with getMinAreaRectCorners and getDetectedCorners
2023-01-06 22:20:27 -05:00
Mohammad Durrani
16ca2671f0 Update to osxuniversal (#711)
Probably closes #710
2023-01-06 21:10:48 -05:00
Matt
5e977445ee Improve websocket reconnect robustness (#706)
Replace with stripped down NT4 client 

Co-authored-by: Mohammad Durrani <46766905+mdurrani808@users.noreply.github.com>
2023-01-06 17:25:11 -08:00
Matt
8117b5814b Bump to wpilib 2023.1.1 (#694) 2023-01-06 17:53:39 -05:00
Matt
087429dab9 [Workaround] Publish rawBytes as periodic (#707)
Closes #704 -- addressed by upstream PR https://github.com/wpilibsuite/allwpilib/pull/4903 (not released yet)
2023-01-06 17:53:19 -05:00
Nick Hadley
dbe7464ea9 Change pose estimator to take robotToCamera (#698)
Co-authored-by: Matt <matthew.morley.ca@gmail.com>
2023-01-06 17:41:47 -05:00
Mohammad Durrani
ebef19af3d Add aprilTagExample to Java example list (#709)
Co-authored-by: Matt <matthew.morley.ca@gmail.com>
2023-01-06 11:33:47 -05:00
Mohammad Durrani
bde023c025 Apriltag example from gerth2 (#701)
* apriltag example

* vendor dep update

* Run formatters

* Update Drivetrain.java

Co-authored-by: Matt <matthew.morley.ca@gmail.com>
2023-01-05 20:48:06 -05:00
Nick Hadley
0f427bb52b Update PhotonCamera error messages to be more specific (#697)
Closes #692
2023-01-05 19:28:32 -05:00
Mohammad Durrani
05198ef294 Aruco Support for AprilTag Detection (Experimental) (#636)
Uses OpenCV's aruco module for AprilTag detection.
2023-01-05 13:25:44 -05:00
Matt
b263fe19cc Undistort corners in umich pose estimation (#699)
* Undistort corners in umich pose estimation

Add tag corner unit test

Delete hellooo.jpg

Update Draw3dTargetsPipe.java

Update FileFrameProvider.java

* Update AprilTagTest.java
2023-01-05 12:08:25 -06:00
Matt
e68e6f3181 Update SimPhotonCamera.java (#703) 2023-01-05 06:43:23 -06:00
134 changed files with 5473 additions and 1071 deletions

View File

@@ -393,11 +393,11 @@ jobs:
- os: ubuntu-latest
artifact-name: LinuxArm64
image_suffix: RaspberryPi
image_url: https://api.github.com/repos/photonvision/photon-pi-gen/releases/tags/v2023.1.0-beta-4_arm64
image_url: https://api.github.com/repos/photonvision/photon-pi-gen/releases/tags/v2023.1.1_arm64
- os: ubuntu-latest
artifact-name: LinuxArm64
image_suffix: limelight
image_url: https://api.github.com/repos/photonvision/photon-pi-gen/releases/tags/v2023.1.0-beta-5_limelight-arm64
image_url: https://api.github.com/repos/photonvision/photon-pi-gen/releases/tags/v2023.2.2_limelight-arm64
runs-on: ${{ matrix.os }}
name: "Build image - ${{ matrix.image_url }}"

View File

@@ -26,7 +26,7 @@ allprojects {
apply from: "versioningHelper.gradle"
ext {
wpilibVersion = "2023.1.1-beta-7-15-g1e7fcd5"
wpilibVersion = "2023.2.1"
opencvVersion = "4.6.0-4"
joglVersion = "2.4.0-rc-20200307"
pubVersion = versionString

View File

@@ -134,7 +134,7 @@
<script>
import Logs from "./views/LogsView"
// import {mapState} from "vuex";
import { ReconnectingWebsocket } from "./plugins/ReconnectingWebsocket.js"
export default {
name: 'App',
@@ -145,7 +145,8 @@ export default {
// Used so that we can switch back to the previously selected pipeline after camera calibration
previouslySelectedIndices: [],
timer: undefined,
teamNumberDialog: true
teamNumberDialog: true,
websocket: null,
}),
computed: {
needsTeamNumberSet: {
@@ -190,15 +191,12 @@ export default {
}
});
this.recreateWebsocket();
},
methods: {
recreateWebsocket() {
const wsDataURL = 'ws://' + this.$address + '/websocket_data';
let socket = new WebSocket(wsDataURL);
socket.binaryType = "arraybuffer";
const wsDataURL = 'ws://' + this.$address + '/websocket_data';
this.websocket = new ReconnectingWebsocket(
wsDataURL,
socket.onmessage = (event) => {
// On data in
(event) => {
try {
let message = this.$msgPack.decode(event.data);
for (let prop in message) {
@@ -210,31 +208,21 @@ export default {
console.log(event)
console.error('error: ' + JSON.stringify(event.data) + " , " + error);
}
};
},
socket.onerror = () => {
socket.close();
this.$store.commit("backendConnected", false)
};
// on connect
(event) => {
event; this.$store.commit("backendConnected", true);
this.$store.state.connectedCallbacks.forEach(it => it());
},
// on disconnect
(event) => { event; this.$store.commit("backendConnected", false) }
);
socket.onopen = () => {
clearInterval(this.timerId);
socket.onclose = () => {
this.$store.commit("backendConnected", false)
this.timerId = setInterval(() => {
this.recreateWebsocket();
}, 1000);
};
this.$store.commit("backendConnected", true)
this.$store.state.connectedCallbacks.forEach(it => it())
};
this.$store.commit("websocket", socket);
},
this.$store.commit("websocket", this.websocket);
},
methods: {
handleMessage(key, value) {
if (key === "logMessage") {
this.logMessage(value["logMessage"], value["logLevel"]);

View File

@@ -4,7 +4,7 @@
crossOrigin="anonymous"
:style="styleObject"
:src="src"
alt=""
:alt="alt"
@click="clickHandler"
@error="loadErrHandler"
/>
@@ -14,7 +14,7 @@
export default {
name: "CvImage",
// eslint-disable-next-line vue/require-prop-types
props: ['address', 'scale', 'maxHeight', 'maxHeightMd', 'maxHeightLg', 'maxHeightXl', 'colorPicking', 'id', 'disconnected'],
props: ['address', 'scale', 'maxHeight', 'maxHeightMd', 'maxHeightLg', 'maxHeightXl', 'colorPicking', 'id', 'disconnected', 'alt'],
data() {
return {
seed: 1.0,

View File

@@ -37,7 +37,7 @@ import TooltippedLabel from "./cv-tooltipped-label";
TooltippedLabel,
},
// eslint-disable-next-line vue/require-prop-types
props: ['list', 'name', 'value', 'disabled', 'selectCols', 'rules', 'tooltip'],
props: ['list', 'name', 'value', 'disabled', 'filteredIndices', 'selectCols', 'rules', 'tooltip'],
computed: {
localValue: {
get() {
@@ -50,6 +50,7 @@ import TooltippedLabel from "./cv-tooltipped-label";
indexList() {
let list = [];
for (let i = 0; i < this.list.length; i++) {
if (this.filteredIndices instanceof Set && this.filteredIndices.has(i)) continue;
list.push({
name: this.list[i],
index: i

View File

@@ -2,14 +2,14 @@ export const dataHandleMixin = {
methods: {
handleInput(key, value) {
let msg = this.$msgPack.encode({[key]: value});
this.$store.state.websocket.send(msg);
this.$store.state.websocket.ws.send(msg);
},
handleInputWithIndex(key, value, cameraIndex = this.$store.getters.currentCameraIndex) {
let msg = this.$msgPack.encode({
[key]: value,
["cameraIndex"]: cameraIndex,
});
this.$store.state.websocket.send(msg);
this.$store.state.websocket.ws.send(msg);
},
handleData(val) {
this.handleInput(val, this[val]);
@@ -22,7 +22,7 @@ export const dataHandleMixin = {
["cameraIndex"]: this.$store.getters.currentCameraIndex
}
});
this.$store.state.websocket.send(msg);
this.$store.state.websocket.ws.send(msg);
this.$emit('update')
},
handlePipelineUpdate(key, val) {
@@ -32,7 +32,7 @@ export const dataHandleMixin = {
["cameraIndex"]: this.$store.getters.currentCameraIndex
}
});
this.$store.state.websocket.send(msg);
this.$store.state.websocket.ws.send(msg);
this.$emit('update')
},
handleTruthyPipelineData(val) {
@@ -42,7 +42,7 @@ export const dataHandleMixin = {
["cameraIndex"]: this.$store.getters.currentCameraIndex
}
});
this.$store.state.websocket.send(msg);
this.$store.state.websocket.ws.send(msg);
this.$emit('update')
},
rollback(val, e) {

View File

@@ -0,0 +1,74 @@
/**
* Auto-reconnecting Websocket, a stripped down version of the NT4 client from
* https://raw.githubusercontent.com/wpilibsuite/NetworkTablesClients/2f8d378ac08d5ca703d590cfb019fc4af062db89/nt4/js/src/nt4.js
*/
export class ReconnectingWebsocket {
constructor(serverAddr,
onDataIn_in,
onConnect_in,
onDisconnect_in) {
this.onDataIn = onDataIn_in;
this.onConnect = onConnect_in;
this.onDisconnect = onDisconnect_in;
// WS Connection State (with defaults)
this.serverAddr = serverAddr;
this.serverConnectionActive = false;
//Trigger the websocket to connect automatically
this.ws_connect();
}
//////////////////////////////////////////////////////////////
// Websocket connection Maintenance
ws_onOpen() {
// Set the flag allowing general server communication
this.serverConnectionActive = true;
console.log("[WebSocket] Connected!");
// User connection-opened hook
this.onConnect();
}
ws_onClose(e) {
//Clear flags to stop server communication
this.ws = null;
this.serverConnectionActive = false;
// User connection-closed hook
this.onDisconnect();
console.log('[WebSocket] 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) {
console.log("[WebSocket] Websocket error - " + e.toString());
this.ws.close();
}
ws_onMessage(e) {
this.onDataIn(e);
}
ws_connect() {
this.ws = new WebSocket(this.serverAddr);
this.ws.binaryType = "arraybuffer";
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("[WebSocket] Starting...");
}
}
export default { ReconnectingWebsocket }

View File

@@ -1,121 +0,0 @@
//https://gomakethings.com/getting-the-differences-between-two-objects-with-vanilla-js/
export const diff = function (obj1, obj2) {
// Make sure an object to compare is provided
if (!obj2 || Object.prototype.toString.call(obj2) !== '[object Object]') {
return obj1;
}
//
// Variables
//
let diffs = {};
let key;
//
// Methods
//
/**
* Check if two arrays are equal
* @param {Array} arr1 The first array
* @param {Array} arr2 The second array
* @return {Boolean} If true, both arrays are equal
*/
const arraysMatch = function (arr1, arr2) {
// Check if the arrays are the same length
if (arr1.length !== arr2.length) return false;
// Check if all items exist and are in the same order
for (let i = 0; i < arr1.length; i++) {
if (arr1[i] !== arr2[i]) return false;
}
// Otherwise, return true
return true;
};
/**
* Compare two items and push non-matches to object
* @param {*} item1 The first item
* @param {*} item2 The second item
* @param {String} key The key in our object
*/
const compare = function (item1, item2, key) {
// Get the object type
let type1 = Object.prototype.toString.call(item1);
let type2 = Object.prototype.toString.call(item2);
// If type2 is undefined it has been removed
if (type2 === '[object Undefined]') {
diffs[key] = null;
return;
}
// If items are different types
if (type1 !== type2) {
diffs[key] = item2;
return;
}
// If an object, compare recursively
if (type1 === '[object Object]') {
let objDiff = diff(item1, item2);
if (Object.keys(objDiff).length > 1) {
diffs[key] = objDiff;
}
return;
}
// If an array, compare
if (type1 === '[object Array]') {
if (!arraysMatch(item1, item2)) {
diffs[key] = item2;
}
return;
}
// Else if it's a function, convert to a string and compare
// Otherwise, just compare
if (type1 === '[object Function]') {
if (item1.toString() !== item2.toString()) {
diffs[key] = item2;
}
} else {
if (item1 !== item2) {
diffs[key] = item2;
}
}
};
//
// Compare our objects
//
// Loop through the first object
for (key in obj1) {
if (obj1.hasOwnProperty(key)) {
compare(obj1[key], obj2[key], key);
}
}
// Loop through the second object and find missing items
for (key in obj2) {
if (obj2.hasOwnProperty(key)) {
if (!obj1[key] && obj1[key] !== obj2[key] ) {
diffs[key] = obj2[key];
}
}
}
// Return the object of differences
return diffs;
};

View File

@@ -52,7 +52,7 @@ export default new Vuex.Store({
isFovConfigurable: true,
calibrated: false,
currentPipelineSettings: {
pipelineType: 4, // One of "calib", "driver", "reflective", "shape", "AprilTag"
pipelineType: 5, // One of "calib", "driver", "reflective", "shape", "AprilTag"
// 2 is reflective
// Settings that apply to all pipeline types
@@ -91,7 +91,7 @@ export default new Vuex.Store({
cornerDetectionAccuracyPercentage: 10,
// Settings that apply to AprilTag
tagFamily: 0,
tagFamily: 1,
decimate: 1.0,
blur: 0.0,
threads: 1,

View File

@@ -72,6 +72,14 @@
:disabled="isCalibrating"
tooltip="Resolution to calibrate at (you will have to calibrate every resolution you use 3D mode on)"
/>
<CVselect
v-model="streamingFrameDivisor"
name="Decimation"
tooltip="Resolution to which camera frames are downscaled for detection. Calibration still uses full-res"
:list="calibrationDivisors"
select-cols="7"
@rollback="e => rollback('streamingFrameDivisor', e)"
/>
<CVselect
v-model="boardType"
name="Board Type"
@@ -291,6 +299,19 @@
Download Target
</v-btn>
</v-col>
<v-col>
<v-btn
color="secondary"
small
style="width: 100%;"
@click="$refs.importCalibrationFromCalibdb.click()"
>
<v-icon left>
mdi-upload
</v-icon>
Import From CalibDB
</v-btn>
</v-col>
</v-row>
</div>
</v-card>
@@ -367,6 +388,20 @@
</template>
</v-col>
</v-row>
<!-- Special hidden upload input that gets 'clicked' when the user imports settings -->
<input
ref="importCalibrationFromCalibdb"
type="file"
accept=".json"
style="display: none;"
@change="readImportedCalibration"
/>
<v-snackbar v-model="uploadSnack" top :color="uploadSnackData.color" timeout="-1">
<span>{{ uploadSnackData.text }}</span>
</v-snackbar>
</div>
</template>
@@ -397,6 +432,12 @@ export default {
calibrationFailed: false,
filteredVideomodeIndex: 0,
settingsValid: true,
unfilteredStreamDivisors: [1, 2, 4],
uploadSnackData: {
color: "success",
text: "",
},
uploadSnack: false,
}
},
computed: {
@@ -430,6 +471,22 @@ export default {
}
},
calibrationDivisors: {
get() {
return this.unfilteredStreamDivisors.filter(item => {
var res = this.stringResolutionList[this.selectedFilteredResIndex].split(" X ").map(it => parseInt(it));
console.log(res);
console.log(item);
// Realistically, we need more than 320x240, but lower than this is
// basically unusable. For now, don't allow decimations that take us
// below that
const ret = ((res[0] / item) >= 300 && (res[1] / item) >= 220) || (item === 1);
console.log(ret);
return ret;
})
}
},
// Makes sure there's only one entry per resolution
filteredResolutionList: {
get() {
@@ -466,6 +523,17 @@ export default {
this.$store.commit('cameraSettings', value);
}
},
streamingFrameDivisor: {
get() {
return this.$store.getters.currentPipelineSettings.streamingFrameDivisor;
},
set(val) {
this.$store.commit("mutatePipeline", {"streamingFrameDivisor": val});
this.handlePipelineUpdate("streamingFrameDivisor", val);
}
},
boardType: {
get() {
return this.calibrationData.boardType
@@ -535,6 +603,57 @@ export default {
},
},
methods: {
readImportedCalibration(event) {
// let formData = new FormData();
// formData.append("zipData", event.target.files[0]);
const filename = event.target.files[0].name;
event.target.files[0].text().then(fileText => {
const data = {
"cameraIndex": this.$store.getters.currentCameraIndex,
"payload": fileText,
"filename": filename,
};
this.axios
.post("http://" + this.$address + "/api/calibration/import", data, {
headers: { "Content-Type": "text/plain" },
})
.then(() => {
this.uploadSnackData = {
color: "success",
text:
"Calibration imported successfully! PhotonVision will restart in the background...",
};
this.uploadSnack = true;
})
.catch((err) => {
if (err.response) {
this.uploadSnackData = {
color: "error",
text:
"Error while uploading calibration file! Could not process provided file.",
};
} else if (err.request) {
this.uploadSnackData = {
color: "error",
text:
"Error while uploading calibration file! No respond to upload attempt.",
};
} else {
this.uploadSnackData = {
color: "error",
text: "Error while uploading calibration file!",
};
}
this.uploadSnack = true;
});
})
},
closeDialog() {
this.snack = false;
this.calibrationInProgress = false;
@@ -676,7 +795,7 @@ export default {
console.log("starting calibration with index " + calData.videoModeIndex);
}
this.$store.commit('currentPipelineIndex', -2);
this.$store.state.websocket.send(this.$msgPack.encode(data));
this.$store.state.websocket.ws.send(this.$msgPack.encode(data));
},
sendCalibrationFinish() {
console.log("finishing calibration for index " + this.$store.getters.currentCameraIndex);

View File

@@ -1,75 +1,75 @@
<template>
<div>
<v-container
class="pa-3"
fluid
class="pa-3"
fluid
>
<v-row
no-gutters
align="center"
justify="center"
no-gutters
align="center"
justify="center"
>
<v-col
cols="12"
:class="['pb-3 ', 'pr-lg-3']"
lg="8"
align-self="stretch"
cols="12"
:class="['pb-3 ', 'pr-lg-3']"
lg="8"
align-self="stretch"
>
<v-card
color="primary"
height="100%"
style="display: flex; flex-direction: column"
dark
color="primary"
height="100%"
style="display: flex; flex-direction: column"
dark
>
<v-card-title
class="pb-0 mb-0 pl-4 pt-1"
style="height: 15%; min-height: 50px;"
class="pb-0 mb-0 pl-4 pt-1"
style="height: 15%; min-height: 50px;"
>
Cameras
<v-chip
:class="fpsTooLow ? 'ml-2 mt-1' : 'mt-2'"
x-small
label
:color="fpsTooLow ? 'error' : 'transparent'"
:text-color="fpsTooLow ? 'white' : 'grey'"
:class="fpsTooLow ? 'ml-2 mt-1' : 'mt-2'"
x-small
label
:color="fpsTooLow ? 'error' : 'transparent'"
:text-color="fpsTooLow ? 'white' : 'grey'"
>
<span class="pr-1">Processing @ {{ Math.round($store.state.pipelineResults.fps) }}&nbsp;FPS &ndash;</span>
<span v-if="fpsTooLow && !$store.getters.currentPipelineSettings.inputShouldShow && $store.getters.pipelineType == 2">HSV thresholds are too broad; narrow them for better performance</span>
<span v-else-if="$fpsTooLow && getters.currentCameraSettings.inputShouldShow">stop viewing the raw stream for better performance</span>
<span v-else-if="fpsTooLow && getters.currentCameraSettings.inputShouldShow">stop viewing the raw stream for better performance</span>
<span v-else>{{ Math.min(Math.round($store.state.pipelineResults.latency), 9999) }} ms latency</span>
</v-chip>
<v-switch
v-model="driverMode"
label="Driver Mode"
style="margin-left: auto;"
color="accent"
v-model="driverMode"
label="Driver Mode"
style="margin-left: auto;"
color="accent"
/>
</v-card-title>
<v-row
align="center"
align="center"
>
<v-col
v-for="idx in (selectedOutputs instanceof Array ? selectedOutputs : [selectedOutputs])"
:key="idx"
cols="12"
:md="selectedOutputs.length === 1 ? 12 : Math.floor(12 / selectedOutputs.length)"
class="pb-0 pt-0"
style="height: 100%;"
v-for="idx in (selectedOutputs instanceof Array ? selectedOutputs : [selectedOutputs])"
:key="idx"
cols="12"
:md="selectedOutputs.length === 1 ? 12 : Math.floor(12 / selectedOutputs.length)"
class="pb-0 pt-0"
style="height: 100%;"
>
<div style="position: relative; width: 100%; height: 100%;">
<cv-image
:id="idx === 0 ? 'raw-stream' : 'processed-stream'"
ref="streams"
: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"
:color-picking="$store.state.colorPicking && idx === 0"
@click="onImageClick"
:id="idx === 0 ? 'raw-stream' : 'processed-stream'"
ref="streams"
: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="idx === 0 ? 'Raw stream' : 'Processed stream'"
:color-picking="$store.state.colorPicking && idx === 0"
@click="onImageClick"
/>
</div>
</v-col>
@@ -77,44 +77,44 @@
</v-card>
</v-col>
<v-col
cols="12"
class="pb-3"
lg="4"
align-self="stretch"
cols="12"
class="pb-3"
lg="4"
align-self="stretch"
>
<v-card
color="primary"
color="primary"
>
<camera-and-pipeline-select />
</v-card>
<v-card
:disabled="$store.getters.isDriverMode || $store.state.colorPicking"
class="mt-3"
color="primary"
:disabled="$store.getters.isDriverMode || $store.state.colorPicking"
class="mt-3"
color="primary"
>
<v-row
align="center"
class="pl-3 pr-3"
align="center"
class="pl-3 pr-3"
>
<v-col lg="12">
<p style="color: white;">
Processing mode:
</p>
<v-btn-toggle
v-model="processingMode"
mandatory
dark
class="fill"
v-model="processingMode"
mandatory
dark
class="fill"
>
<v-btn
color="secondary"
color="secondary"
>
<v-icon>mdi-crop-square</v-icon>
<span>2D</span>
</v-btn>
<v-btn
color="secondary"
@click="on3DClick"
color="secondary"
@click="on3DClick"
>
<v-icon>mdi-cube-outline</v-icon>
<span>3D</span>
@@ -126,22 +126,22 @@
Stream display:
</p>
<v-btn-toggle
v-model="selectedOutputs"
:multiple="$vuetify.breakpoint.mdAndUp"
mandatory
dark
class="fill"
v-model="selectedOutputs"
:multiple="$vuetify.breakpoint.mdAndUp"
mandatory
dark
class="fill"
>
<v-btn
color="secondary"
class="fill"
color="secondary"
class="fill"
>
<v-icon>mdi-import</v-icon>
<span>Raw</span>
</v-btn>
<v-btn
color="secondary"
class="fill"
color="secondary"
class="fill"
>
<v-icon>mdi-export</v-icon>
<span>Processed</span>
@@ -154,29 +154,29 @@
</v-row>
<v-row no-gutters>
<v-col
v-for="(tabs, idx) in tabGroups"
:key="idx"
:cols="Math.floor(12 / tabGroups.length)"
:class="idx !== tabGroups.length - 1 ? 'pr-3' : ''"
align-self="stretch"
v-for="(tabs, idx) in tabGroups"
:key="idx"
:cols="Math.floor(12 / tabGroups.length)"
:class="idx !== tabGroups.length - 1 ? 'pr-3' : ''"
align-self="stretch"
>
<v-card
color="primary"
height="100%"
class="pr-4 pl-4"
color="primary"
height="100%"
class="pr-4 pl-4"
>
<v-tabs
v-if="!$store.getters.isDriverMode"
v-model="selectedTabs[idx]"
grow
background-color="primary"
dark
height="48"
slider-color="accent"
v-if="!$store.getters.isDriverMode"
v-model="selectedTabs[idx]"
grow
background-color="primary"
dark
height="48"
slider-color="accent"
>
<v-tab
v-for="(tab, i) in tabs"
:key="i"
v-for="(tab, i) in tabs"
:key="i"
>
{{ tab.name }}
</v-tab>
@@ -184,10 +184,10 @@
<div class="pl-4 pr-4 pt-2">
<keep-alive>
<component
:is="(tabs[selectedTabs[idx]] || tabs[0]).component"
:ref="(tabs[selectedTabs[idx]] || tabs[0]).name"
v-model="$store.getters.pipeline"
@update="$emit('save')"
:is="(tabs[selectedTabs[idx]] || tabs[0]).component"
:ref="(tabs[selectedTabs[idx]] || tabs[0]).name"
v-model="$store.getters.pipeline"
@update="$emit('save')"
/>
</keep-alive>
</div>
@@ -197,18 +197,18 @@
</v-container>
<v-snackbar
v-model="showNTWarning"
color="error"
timeout="-1"
top
v-model="showNTWarning"
color="error"
timeout="-1"
top
>
{{ $store.state.settings.networkSettings.runNTServer ?
"NetworkTables server enabled! PhotonLib may not work." :
"NetworkTables not connected! Are you on a network with a robot?" }}
<template v-slot:action>
<v-btn
text
@click="hideNTWarning = true"
text
@click="hideNTWarning = true"
>
Hide
</v-btn>
@@ -216,12 +216,12 @@
</v-snackbar>
<v-dialog
v-model="dialog"
width="500"
v-model="dialog"
width="500"
>
<v-card
color="primary"
dark
color="primary"
dark
>
<v-card-title>
Current resolution not calibrated
@@ -230,9 +230,9 @@
<v-card-text>
Because the current resolution {{ this.$store.getters.currentVideoFormat.width }} x {{ this.$store.getters.currentVideoFormat.height }} is not yet calibrated, 3D mode cannot be enabled. Please
<a
href="/#/cameras"
class="white--text"
@click="$emit('switch-to-cameras')"
href="/#/cameras"
class="white--text"
@click="$emit('switch-to-cameras')"
> visit the Cameras tab</a> to calibrate this resolution. For now, SolvePNP will do nothing.
</v-card-text>
@@ -241,9 +241,9 @@
<v-card-actions>
<v-spacer />
<v-btn
color="white"
text
@click="closeUncalibratedDialog"
color="white"
text
@click="closeUncalibratedDialog"
>
OK
</v-btn>
@@ -264,252 +264,264 @@ import TargetsTab from "./PipelineViews/TargetsTab";
import Map3DTab from './PipelineViews/Map3DTab';
import PnPTab from './PipelineViews/PnPTab';
import AprilTagTab from './PipelineViews/AprilTagTab';
import ArucoTab from './PipelineViews/ArucoTab';
export default {
name: 'Pipeline',
components: {
CameraAndPipelineSelect,
cvImage,
InputTab,
ThresholdTab,
ContoursTab,
OutputTab,
TargetsTab,
Map3DTab,
PnPTab,
AprilTagTab,
},
data() {
return {
selectedTabsData: [0, 0, 0, 0],
counterData: 0,
dialog: false,
processingModeOverride: false,
hideNTWarning: false,
}
},
computed: {
selectedTabs: {
get() {
return this.$store.getters.isDriverMode ? [0] : this.selectedTabsData;
},
set(value) {
this.selectedTabsData = value;
}
},
tabGroups: {
get() {
let tabs = {
input: {
name: "Input",
component: "InputTab",
},
threshold: {
name: "Threshold",
component: "ThresholdTab",
},
contours: {
name: "Contours",
component: "ContoursTab",
},
apriltag: {
name: "AprilTag",
component: "AprilTagTab",
},
output: {
name: "Output",
component: "OutputTab",
},
targets: {
name: "Targets",
component: "TargetsTab",
},
pnp: {
name: "PnP",
component: "PnPTab",
},
map3d: {
name: "3D",
component: "Map3DTab",
}
};
// If not in 3d, name "3D" is illegal
const allow3d = this.$store.getters.currentPipelineSettings.solvePNPEnabled;
// If in apriltag, "Threshold" and "Contours" are illegal -- otherwise "AprilTag" is
const isAprilTag = (this.$store.getters.currentPipelineSettings.pipelineType - 2) === 2;
// 2D array of tab names and component names; each sub-array is a separate tab group
let ret = [];
if (this.$vuetify.breakpoint.smAndDown || this.$store.getters.isDriverMode || (this.$vuetify.breakpoint.mdAndDown && !this.$store.state.compactMode)) {
// One big tab group with all the tabs
ret[0] = Object.values(tabs);
} else if (this.$vuetify.breakpoint.mdAndDown || !this.$store.state.compactMode) {
// Two tab groups, one with "input, threshold, contours, output" and the other with "target info, 3D"
ret[0] = [tabs.input, tabs.threshold, tabs.contours, tabs.apriltag, tabs.output];
ret[1] = [tabs.targets, tabs.pnp, tabs.map3d];
} else if (this.$vuetify.breakpoint.lgAndDown) {
// Three tab groups, one with "input", one with "threshold, contours, output", and the other with "target info, 3D"
ret[0] = [tabs.input];
ret[1] = [tabs.threshold, tabs.contours, tabs.apriltag, tabs.output];
ret[2] = [tabs.targets, tabs.pnp, tabs.map3d];
} else if (this.$vuetify.breakpoint.xl) {
// Three tab groups, one with "input", one with "threshold, contours", and the other with "output, target info, 3D"
ret[0] = [tabs.input];
ret[1] = [tabs.threshold];
ret[2] = [tabs.contours, tabs.apriltag, tabs.output];
ret[3] = [tabs.targets, tabs.pnp, tabs.map3d];
}
for(let i = 0; i < ret.length; i++) {
const group = ret[i];
// All the tabs we allow
const filteredGroup = group.filter(it =>
!(!allow3d && it.name === "3D") //Filter out 3D tab any time 3D isn't calibrated
&& !((!allow3d || isAprilTag) && it.name === "PnP") //Filter out the PnP config tab if 3D isn't available, or we're doing Apriltags
&& !(isAprilTag && (it.name === "Threshold")) //Filter out threshold tab if we're doing apriltags
&& !(isAprilTag && (it.name === "Contours")) //Filter out contours if we're doing Apriltag
&& !(!isAprilTag && it.name === "AprilTag") //Filter out apriltag unless we actually are doing Apriltags
);
ret[i] = filteredGroup;
}
// One last filter to remove empty lists
return ret.filter(it => it !== undefined && it.length > 0);
}
},
processingMode: {
get() {
return (this.$store.getters.currentPipelineSettings.solvePNPEnabled || this.processingModeOverride) ? 1 : 0;
},
set(value) {
if (this.$store.getters.isCalibrated) {
this.$store.getters.currentPipelineSettings.solvePNPEnabled = value === 1;
this.handlePipelineUpdate("solvePNPEnabled", value === 1);
}
}
},
driverMode: {
get() {
return this.$store.getters.isDriverMode;
},
set(value) {
this.$store.getters.currentCameraSettings.currentPipelineIndex = value ? -1 : 0;
this.handleInputWithIndex('currentPipeline', value ? -1 : 0);
}
},
selectedOutputs: {
// All this logic exists to deal with the reality that the output select buttons sometimes need an array and sometimes need a number (depending on whether or not they're exclusive)
get() {
// We switch the selector to single-select only on sm-and-down size devices, so we have to return a Number instead of an Array in that state
let ret = [];
if (this.$store.state.colorPicking) {
ret = [0]; // We want the input stream only while color picking
} else if (this.$store.getters.isDriverMode) {
ret = [1]; // We want only the output stream in driver mode
} else {
if (this.$store.getters.currentPipelineSettings.inputShouldShow) ret = ret.concat([0]);
if (this.$store.getters.currentPipelineSettings.outputShouldShow) ret = ret.concat([1]);
if (!ret.length) ret = [0];
}
if (this.$vuetify.breakpoint.mdAndUp) {
return ret;
} else {
return ret[0] || 0;
}
},
set(value) {
let valToCommit = [0];
if (value instanceof Array) {
// Value is already an array, we don't need to do anything
valToCommit = value;
} else if (value) {
// Value is assumed to be a number, so we wrap it into an array
valToCommit = [value];
}
this.$store.commit("mutatePipeline", {"inputShouldShow": valToCommit.includes(0)});
this.$store.commit("mutatePipeline", {"outputShouldShow": valToCommit.includes(1)});
this.handlePipelineUpdate("inputShouldShow", valToCommit.includes(0));
}
},
fpsTooLow: {
get() {
// For now we only show the FPS is too low warning when GPU acceleration is enabled, because we don't really trust the presented video modes otherwise
return this.$store.state.pipelineResults.fps - this.$store.getters.currentVideoFormat.fps < -5 && this.$store.state.pipelineResults.fps !== 0 && !this.$store.getters.isDriverMode && this.$store.state.settings.general.gpuAcceleration;
}
},
latency: {
get() {
return this.$store.getters.currentPipelineResults.latency;
}
},
isCalibrated: {
get() {
const resolution = this.$store.getters.videoFormatList[this.$store.getters.currentPipelineSettings.cameraVideoModeIndex];
return this.$store.getters.currentCameraSettings.calibrations
.some(e => e.width === resolution.width && e.height === resolution.height)
}
},
isRobotConnected: {
get() {
// return this.$store.state.ntConnectionInfo.connected && this.$store.state.backendConnected;
return true;
}
},
showNTWarning: {
get() {
return (!this.$store.state.ntConnectionInfo.connected || this.$store.state.settings.networkSettings.runNTServer) && this.$store.state.settings.networkSettings.teamNumber > 0 && this.$store.state.backendConnected && !this.hideNTWarning;
}
},
},
created() {
this.$store.state.connectedCallbacks.push(this.reloadStreams)
},
methods: {
reloadStreams() {
// Reload the streams as we technically close and reopen them
this.$refs.streams.forEach(it => it.reload())
},
onImageClick(event) {
// Only run on the input stream
if (event.target.alt !== "Stream0") return;
// Get a reference to the threshold tab (if it is shown) and call its "onClick" method
let ref = this.$refs["Threshold"];
if (ref && ref[0])
ref[0].onClick(event)
},
on3DClick() {
if (!this.$store.getters.isCalibrated) {
this.dialog = true;
this.processingModeOverride = true;
}
},
closeUncalibratedDialog() {
this.dialog = false;
this.processingModeOverride = false;
// this.$store.getters.currentPipelineSettings.solvePNPEnabled = false;
this.handlePipelineUpdate("solvePNPEnabled", false);
}
name: 'Pipeline',
components: {
CameraAndPipelineSelect,
cvImage,
InputTab,
ThresholdTab,
ContoursTab,
OutputTab,
TargetsTab,
Map3DTab,
PnPTab,
AprilTagTab,
ArucoTab,
},
data() {
return {
selectedTabsData: [0, 0, 0, 0],
counterData: 0,
dialog: false,
processingModeOverride: false,
hideNTWarning: false,
}
},
computed: {
selectedTabs: {
get() {
return this.$store.getters.isDriverMode ? [0] : this.selectedTabsData;
},
set(value) {
this.selectedTabsData = value;
}
},
tabGroups: {
get() {
let tabs = {
input: {
name: "Input",
component: "InputTab",
},
threshold: {
name: "Threshold",
component: "ThresholdTab",
},
contours: {
name: "Contours",
component: "ContoursTab",
},
apriltag: {
name: "AprilTag",
component: "AprilTagTab",
},
aruco: {
name: "Aruco",
component: "ArucoTab",
},
output: {
name: "Output",
component: "OutputTab",
},
targets: {
name: "Targets",
component: "TargetsTab",
},
pnp: {
name: "PnP",
component: "PnPTab",
},
map3d: {
name: "3D",
component: "Map3DTab",
}
};
// If not in 3d, name "3D" is illegal
const allow3d = this.$store.getters.currentPipelineSettings.solvePNPEnabled;
// If in apriltag, "Threshold" and "Contours" are illegal -- otherwise "AprilTag" is
const isAprilTag = (this.$store.getters.currentPipelineSettings.pipelineType - 2) === 2;
const isAruco = (this.$store.getters.currentPipelineSettings.pipelineType - 2) === 3;
// 2D array of tab names and component names; each sub-array is a separate tab group
let ret = [];
if (this.$vuetify.breakpoint.smAndDown || this.$store.getters.isDriverMode || (this.$vuetify.breakpoint.mdAndDown && !this.$store.state.compactMode)) {
// One big tab group with all the tabs
ret[0] = Object.values(tabs);
} else if (this.$vuetify.breakpoint.mdAndDown || !this.$store.state.compactMode) {
// Two tab groups, one with "input, threshold, contours, output" and the other with "target info, 3D"
ret[0] = [tabs.input, tabs.threshold, tabs.contours, tabs.apriltag, tabs.aruco, tabs.output];
ret[1] = [tabs.targets, tabs.pnp, tabs.map3d];
} else if (this.$vuetify.breakpoint.lgAndDown) {
// Three tab groups, one with "input", one with "threshold, contours, output", and the other with "target info, 3D"
ret[0] = [tabs.input];
ret[1] = [tabs.threshold, tabs.contours, tabs.apriltag,tabs.aruco, tabs.output];
ret[2] = [tabs.targets, tabs.pnp, tabs.map3d];
} else if (this.$vuetify.breakpoint.xl) {
// Three tab groups, one with "input", one with "threshold, contours", and the other with "output, target info, 3D"
ret[0] = [tabs.input];
ret[1] = [tabs.threshold];
ret[2] = [tabs.contours, tabs.apriltag, tabs.aruco,tabs.output];
ret[3] = [tabs.targets, tabs.pnp, tabs.map3d];
}
for(let i = 0; i < ret.length; i++) {
const group = ret[i];
// All the tabs we allow
const filteredGroup = group.filter(it =>
!(!allow3d && it.name === "3D") //Filter out 3D tab any time 3D isn't calibrated
&& !((!allow3d || isAprilTag || isAruco) && it.name === "PnP") //Filter out the PnP config tab if 3D isn't available, or we're doing Apriltags
&& !((isAprilTag || isAruco) && (it.name === "Threshold")) //Filter out threshold tab if we're doing apriltags
&& !((isAprilTag || isAruco)&& (it.name === "Contours")) //Filter out contours if we're doing Apriltag
&& !(!isAprilTag && it.name === "AprilTag") //Filter out apriltag unless we actually are doing Apriltags
&& !(!isAruco && it.name === "Aruco")
);
ret[i] = filteredGroup;
}
// One last filter to remove empty lists
return ret.filter(it => it !== undefined && it.length > 0);
}
},
processingMode: {
get() {
return (this.$store.getters.currentPipelineSettings.solvePNPEnabled || this.processingModeOverride) ? 1 : 0;
},
set(value) {
if (this.$store.getters.isCalibrated) {
this.$store.getters.currentPipelineSettings.solvePNPEnabled = value === 1;
this.handlePipelineUpdate("solvePNPEnabled", value === 1);
}
}
},
driverMode: {
get() {
return this.$store.getters.isDriverMode;
},
set(value) {
this.$store.getters.currentCameraSettings.currentPipelineIndex = value ? -1 : 0;
this.handleInputWithIndex('currentPipeline', value ? -1 : 0);
}
},
selectedOutputs: {
// All this logic exists to deal with the reality that the output select buttons sometimes need an array and sometimes need a number (depending on whether or not they're exclusive)
get() {
// We switch the selector to single-select only on sm-and-down size devices, so we have to return a Number instead of an Array in that state
let ret = [];
if (this.$store.state.colorPicking) {
ret = [0]; // We want the input stream only while color picking
} else if (this.$store.getters.isDriverMode) {
ret = [1]; // We want only the output stream in driver mode
} else {
if (this.$store.getters.currentPipelineSettings.inputShouldShow) ret = ret.concat([0]);
if (this.$store.getters.currentPipelineSettings.outputShouldShow) ret = ret.concat([1]);
if (!ret.length) ret = [0];
}
if (this.$vuetify.breakpoint.mdAndUp) {
return ret;
} else {
return ret[0] || 0;
}
},
set(value) {
let valToCommit = [0];
if (value instanceof Array) {
// Value is already an array, we don't need to do anything
valToCommit = value;
} else if (value) {
// Value is assumed to be a number, so we wrap it into an array
valToCommit = [value];
}
this.$store.commit("mutatePipeline", {"inputShouldShow": valToCommit.includes(0)});
this.$store.commit("mutatePipeline", {"outputShouldShow": valToCommit.includes(1)});
this.handlePipelineUpdate("inputShouldShow", valToCommit.includes(0));
}
},
fpsTooLow: {
get() {
// For now we only show the FPS is too low warning when GPU acceleration is enabled, because we don't really trust the presented video modes otherwise
const currFPS = this.$store.state.pipelineResults.fps;
const targetFPS = this.$store.getters.currentVideoFormat.fps;
const driverMode = this.$store.getters.isDriverMode;
const gpuAccel = this.$store.state.settings.general.gpuAcceleration === true;
const isReflective = this.$store.getters.pipelineType === 2;
return (currFPS - targetFPS) < -5 && this.$store.state.pipelineResults.fps !== 0 && !driverMode && gpuAccel && isReflective;
}
},
latency: {
get() {
return this.$store.getters.currentPipelineResults.latency;
}
},
isCalibrated: {
get() {
const resolution = this.$store.getters.videoFormatList[this.$store.getters.currentPipelineSettings.cameraVideoModeIndex];
return this.$store.getters.currentCameraSettings.calibrations
.some(e => e.width === resolution.width && e.height === resolution.height)
}
},
isRobotConnected: {
get() {
// return this.$store.state.ntConnectionInfo.connected && this.$store.state.backendConnected;
return true;
}
},
showNTWarning: {
get() {
return (!this.$store.state.ntConnectionInfo.connected || this.$store.state.settings.networkSettings.runNTServer) && this.$store.state.settings.networkSettings.teamNumber > 0 && this.$store.state.backendConnected && !this.hideNTWarning;
}
},
},
created() {
this.$store.state.connectedCallbacks.push(this.reloadStreams)
},
methods: {
reloadStreams() {
// Reload the streams as we technically close and reopen them
this.$refs.streams.forEach(it => it.reload())
},
onImageClick(event) {
// Get a reference to the threshold tab (if it is shown) and call its "onClick" method
let ref = this.$refs["Threshold"];
if (ref && ref[0])
ref[0].onClick(event)
},
on3DClick() {
if (!this.$store.getters.isCalibrated) {
this.dialog = true;
this.processingModeOverride = true;
}
},
closeUncalibratedDialog() {
this.dialog = false;
this.processingModeOverride = false;
// this.$store.getters.currentPipelineSettings.solvePNPEnabled = false;
this.handlePipelineUpdate("solvePNPEnabled", false);
}
}
}
</script>
<style scoped>
.v-btn-toggle.fill {
width: 100%;
height: 100%;
width: 100%;
height: 100%;
}
.v-btn-toggle.fill > .v-btn {
width: 50%;
height: 100%;
width: 50%;
height: 100%;
}
th {
width: 80px;
text-align: center;
width: 80px;
text-align: center;
}
</style>

View File

@@ -1,13 +1,11 @@
<template>
<div>
<v-select
<CVselect
v-model="selectedFamily"
dark
color="accent"
item-color="secondary"
label="Select target family"
:items="familyList"
@input="handlePipelineUpdate('tagFamily', familyList.indexOf(selectedFamily))"
name="Target family"
:list="['AprilTag family 36h11', 'AprilTag family 25h9', 'AprilTag family 16h5']"
select-cols="8"
@input="handlePipelineUpdate('tagFamily', selectedFamily)"
/>
<CVslider
v-model="decimate"
@@ -78,37 +76,27 @@
<script>
import CVslider from '../../components/common/cv-slider'
import CVswitch from '../../components/common/cv-switch'
import CVselect from '../../components/common/cv-select'
export default {
name: "AprilTag",
components: {
CVslider,
CVswitch,
CVselect,
},
data() {
return {
familyList: ["tag36h11", "tag25h9", "tag16h5"],
// Selected model is offset (ew) by 6 from the photon ordinal, as we only wanna show the 36h11 and 16h5 options
targetList: ['6in (16h5) AprilTag'], //Keep in sync with TargetModel.java
familyList: ["AprilTag family 36h11", "AprilTag family 25h9", "AprilTag family 16h5"],
}
},
computed: {
selectedModel: {
get() {
let ret = this.$store.getters.currentPipelineSettings.targetModel - 6
return this.targetList[ret];
},
set(val) {
this.$store.commit("mutatePipeline", {"targetModel": this.targetList.indexOf(val) + 6})
}
},
selectedFamily: {
get() {
let ret = this.$store.getters.currentPipelineSettings.tagFamily
return this.familyList[ret];
return this.$store.getters.currentPipelineSettings.tagFamily
},
set(val) {
this.$store.commit("mutatePipeline", {"tagFamily": this.familyList.indexOf(val)})
this.$store.commit("mutatePipeline", {"tagFamily": val})
}
},
decimate: {

View File

@@ -0,0 +1,76 @@
<template>
<div>
<CVslider
v-model="decimate"
class="pt-2"
slider-cols="8"
name="Decimate"
min="1"
max="8"
step=".5"
tooltip="Increases FPS at the expense of range by reducing image resolution initially"
@input="handlePipelineData('decimate')"
/>
<CVslider
v-model="numIterations"
class="pt-2"
slider-cols="8"
name="Corner Iterations"
min="30"
max="1000"
step="5"
tooltip="How many iterations are going to be used in order to refine corners. Higher values are lead to more accuracy at the cost of performance"
@input="handlePipelineData('numIterations')"
/>
<CVslider
v-model="cornerAccuracy"
class="pt-2"
slider-cols="8"
name="Corner Accuracy"
min=".01"
max="100"
step=".01"
tooltip="Minimum accuracy for the corners, lower is better but more performance intensive "
@input="handlePipelineData('cornerAccuracy')"
/>
</div>
</template>
<script>
import CVslider from '../../components/common/cv-slider'
export default {
name: "Aruco",
components: {
CVslider
},
computed: {
decimate: {
get() {
return this.$store.getters.currentPipelineSettings.decimate
},
set(val) {
this.$store.commit("mutatePipeline", {"decimate": val});
},
},
numIterations: {
get() {
return this.$store.getters.currentPipelineSettings.numIterations
},
set(val) {
this.$store.commit("mutatePipeline", {"numIterations": val});
},
},
cornerAccuracy: {
get() {
return this.$store.getters.currentPipelineSettings.cornerAccuracy
},
set(val) {
this.$store.commit("mutatePipeline", {"cornerAccuracy": val});
},
},
},
methods: {
}
}
</script>

View File

@@ -62,11 +62,13 @@
@input="handlePipelineData('cameraBlueGain')"
@rollback="e => rollback('cameraBlueGain', e)"
/>
<!-- TODO: stop filtering out the 90 degree rotation modes when we fix those in libcamera -->
<CVselect
v-model="inputImageRotationMode"
name="Orientation"
tooltip="Rotates the camera stream"
:list="['Normal','90° CW','180°','90° CCW']"
:filtered-indices="this.$store.state.settings.general.gpuAcceleration ? new Set([1, 3]) : undefined"
:select-cols="largeBox"
@input="handlePipelineData('inputImageRotationMode')"
@rollback="e => rollback('inputImageRotationMode',e)"

View File

@@ -10,6 +10,7 @@
/>
<CVselect
v-if="!isTagPipeline"
v-model="contourTargetOrientation"
name="Target Orientation"
tooltip="Used to determine how to calculate target landmarks (e.g. the top, left, or bottom of the target)"
@@ -21,7 +22,8 @@
<CVswitch
v-model="outputShowMultipleTargets"
name="Show Multiple Targets"
tooltip="If enabled, up to five targets will be displayed and sent to user code"
tooltip="If enabled, up to five targets will be displayed and sent to user code, instead of just one"
:disabled="isTagPipeline"
class="mb-4"
text-cols="3"
@input="handlePipelineData('outputShowMultipleTargets')"
@@ -137,6 +139,11 @@
get() {
return undefined; // TODO fix
}
},
isTagPipeline: {
get() {
return this.$store.getters.currentPipelineSettings.pipelineType > 3;
}
}
},
methods: {

View File

@@ -19,7 +19,7 @@
Target
</th>
<th
v-if="$store.getters.pipelineType === 4"
v-if="$store.getters.pipelineType === 4 || (($store.getters.pipelineType - 2) === 3)"
class="text-center"
>
Fiducial ID
@@ -62,7 +62,7 @@
:key="index"
>
<td>{{ index }}</td>
<td v-if="$store.getters.pipelineType === 4">
<td v-if="$store.getters.pipelineType === 4 || (($store.getters.pipelineType - 2) === 3)">
{{ parseInt(value.fiducialId) }}
</td>
<template v-if="!$store.getters.currentPipelineSettings.solvePNPEnabled">

View File

@@ -247,7 +247,7 @@ export default {
'cameraIndex': this.$store.state.currentCameraIndex
}
});
this.$store.state.websocket.send(msg);
this.$store.state.websocket.ws.send(msg);
this.$emit('update');
}
},

View File

@@ -21,7 +21,7 @@
Team number is unset or invalid. NetworkTables will not be able to connect.
</v-banner>
<CVradio
v-show="$store.state.settings.networkSettings.supported"
v-show="$store.state.settings.networkSettings.shouldManage"
v-model="connectionType"
:input-cols="inputCols"
name="IP Assignment Mode"
@@ -74,7 +74,33 @@
>
<span>{{ snackbar.text }}</span>
</v-snackbar>
<v-divider class="mt-4 mb-4" />
<template v-if="$store.state.settings.networkSettings.shouldManage && false">
<!-- Advanced controls for changing DHCP settings and stuff -->
<v-divider class="mt-4 mb-4" />
<v-title> Advanced </v-title>
<CVinput
:input-cols="inputCols"
name="Set DHCP command"
/>
<CVinput
:input-cols="inputCols"
name="Set static command"
/>
<CVinput
:input-cols="inputCols"
name="NetworkManager interface"
/>
<CVinput
:input-cols="inputCols"
name="Physical interface"
/>
</template>
<!-- TEMP - RIO finder is not currently enabled
<v-row>
<v-col
@@ -246,6 +272,14 @@ export default {
return true;
},
sendGeneralSettings() {
const changingStaticIp = !this.isDHCP;
this.snackbar = {
color: "secondary",
text: "Updating settings..."
};
this.snack = true;
this.axios.post("http://" + this.$address + "/api/settings/general", this.settings).then(
response => {
if (response.status === 200) {
@@ -257,10 +291,17 @@ export default {
}
},
error => {
if (error.status === 504 || changingStaticIp) {
this.snackbar = {
color: "error",
text: (error.response || {data: `Connection lost! Try the new static IP at ${this.staticIp}:5800 or ${this.hostname}:5800 ?`}).data
};
} else {
this.snackbar = {
color: "error",
text: (error.response || {data: "Couldn't save settings"}).data
};
}
this.snack = true;
}
)

View File

@@ -19,12 +19,15 @@ package org.photonvision.common.configuration;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonGetter;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonSetter;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.util.HashMap;
import java.util.Map;
import org.photonvision.common.hardware.Platform;
import org.photonvision.common.networking.NetworkMode;
import org.photonvision.common.util.file.JacksonUtils;
public class NetworkConfig {
public int teamNumber = 0;
@@ -33,6 +36,16 @@ public class NetworkConfig {
public String hostname = "photonvision";
public boolean runNTServer = false;
@JsonIgnore public static final String NM_IFACE_STRING = "${interface}";
@JsonIgnore public static final String NM_IP_STRING = "${ipaddr}";
public String networkManagerIface = "Wired\\ connection\\ 1";
public String physicalInterface = "eth0";
public String setStaticCommand =
"nmcli con mod ${interface} ipv4.addresses ${ipaddr}/8 ipv4.method \"manual\" ipv6.method \"disabled\"";
public String setDHCPcommand =
"nmcli con mod ${interface} ipv4.method \"auto\" ipv6.method \"disabled\"";
private boolean shouldManage;
public NetworkConfig() {
@@ -46,37 +59,39 @@ public class NetworkConfig {
@JsonProperty("staticIp") String staticIp,
@JsonProperty("hostname") String hostname,
@JsonProperty("runNTServer") boolean runNTServer,
@JsonProperty("shouldManage") boolean shouldManage) {
@JsonProperty("shouldManage") boolean shouldManage,
@JsonProperty("networkManagerIface") String networkManagerIface,
@JsonProperty("physicalInterface") String physicalInterface,
@JsonProperty("setStaticCommand") String setStaticCommand,
@JsonProperty("setDHCPcommand") String setDHCPcommand) {
this.teamNumber = teamNumber;
this.connectionType = connectionType;
this.staticIp = staticIp;
this.hostname = hostname;
this.runNTServer = runNTServer;
this.networkManagerIface = networkManagerIface;
this.physicalInterface = physicalInterface;
this.setStaticCommand = setStaticCommand;
this.setDHCPcommand = setDHCPcommand;
setShouldManage(shouldManage);
}
public static NetworkConfig fromHashMap(Map<String, Object> map) {
// teamNumber (int), supported (bool), connectionType (int),
// staticIp (str), netmask (str), hostname (str)
var ret = new NetworkConfig();
ret.teamNumber = Integer.parseInt(map.get("teamNumber").toString());
ret.connectionType = NetworkMode.values()[(Integer) map.get("connectionType")];
ret.staticIp = (String) map.get("staticIp");
ret.hostname = (String) map.get("hostname");
ret.runNTServer = (Boolean) map.get("runNTServer");
ret.setShouldManage((Boolean) map.get("supported"));
return ret;
try {
return new ObjectMapper().convertValue(map, NetworkConfig.class);
} catch (Exception e) {
e.printStackTrace();
return new NetworkConfig();
}
}
public HashMap<String, Object> toHashMap() {
HashMap<String, Object> tmp = new HashMap<>();
tmp.put("teamNumber", teamNumber);
tmp.put("supported", shouldManage());
tmp.put("connectionType", connectionType.ordinal());
tmp.put("staticIp", staticIp);
tmp.put("hostname", hostname);
tmp.put("runNTServer", runNTServer);
return tmp;
public Map<String, Object> toHashMap() {
try {
return new ObjectMapper().convertValue(this, JacksonUtils.UIMap.class);
} catch (Exception e) {
e.printStackTrace();
return new HashMap<>();
}
}
@JsonGetter("shouldManage")

View File

@@ -142,8 +142,8 @@ public class NTDataPublisher implements CVPipelineResultConsumer {
ts.rawBytesEntry.set(packet.getData());
ts.pipelineIndexPublisher.setDefault(pipelineIndexSupplier.get());
ts.driverModePublisher.setDefault(driverModeSupplier.getAsBoolean());
ts.pipelineIndexPublisher.set(pipelineIndexSupplier.get());
ts.driverModePublisher.set(driverModeSupplier.getAsBoolean());
ts.latencyMillisEntry.set(result.getLatencyMillis());
ts.hasTargetEntry.set(result.hasTargets());
@@ -189,11 +189,21 @@ public class NTDataPublisher implements CVPipelineResultConsumer {
public static List<PhotonTrackedTarget> simpleFromTrackedTargets(List<TrackedTarget> targets) {
var ret = new ArrayList<PhotonTrackedTarget>();
for (var t : targets) {
var points = new Point[4];
t.getMinAreaRect().points(points);
var cornerList = new ArrayList<TargetCorner>();
for (int i = 0; i < 4; i++) cornerList.add(new TargetCorner(points[i].x, points[i].y));
var minAreaRectCorners = new ArrayList<TargetCorner>();
var detectedCorners = new ArrayList<TargetCorner>();
{
var points = new Point[4];
t.getMinAreaRect().points(points);
for (int i = 0; i < 4; i++) {
minAreaRectCorners.add(new TargetCorner(points[i].x, points[i].y));
}
}
{
var points = t.getTargetCorners();
for (int i = 0; i < points.size(); i++) {
detectedCorners.add(new TargetCorner(points.get(i).x, points.get(i).y));
}
}
ret.add(
new PhotonTrackedTarget(
@@ -205,7 +215,8 @@ public class NTDataPublisher implements CVPipelineResultConsumer {
t.getBestCameraToTarget3d(),
t.getAltCameraToTarget3d(),
t.getPoseAmbiguity(),
cornerList));
minAreaRectCorners,
detectedCorners));
}
return ret;
}

View File

@@ -18,6 +18,7 @@
package org.photonvision.common.networking;
import org.photonvision.common.configuration.ConfigManager;
import org.photonvision.common.configuration.NetworkConfig;
import org.photonvision.common.hardware.Platform;
import org.photonvision.common.logging.LogGroup;
import org.photonvision.common.logging.Logger;
@@ -48,8 +49,7 @@ public class NetworkManager {
logger.info("Setting " + config.connectionType + " with team team " + config.teamNumber);
if (Platform.isLinux()) {
if (!Platform.isRoot()) {
logger.error("Cannot manage network without root!");
return;
logger.error("Cannot manage hostname without root!");
}
// always set hostname
@@ -96,10 +96,11 @@ public class NetworkManager {
if (config.connectionType == NetworkMode.DHCP) {
var shell = new ShellExec();
try {
if (!config.staticIp.equals("")) {
shell.executeBashCommand("ip addr del " + config.staticIp + "/8 dev eth0");
}
shell.executeBashCommand("dhclient eth0", false);
// set nmcli back to DHCP, and re-run dhclient -- this ought to grab a new IP address
shell.executeBashCommand(
config.setDHCPcommand.replace(
NetworkConfig.NM_IFACE_STRING, config.networkManagerIface));
shell.executeBashCommand("dhclient " + config.physicalInterface, false);
} catch (Exception e) {
logger.error("Exception while setting DHCP!");
}
@@ -107,7 +108,30 @@ public class NetworkManager {
var shell = new ShellExec();
if (config.staticIp.length() > 0) {
try {
shell.executeBashCommand("ip addr add " + config.staticIp + "/8" + " dev eth0");
shell.executeBashCommand(
config
.setStaticCommand
.replace(NetworkConfig.NM_IFACE_STRING, config.networkManagerIface)
.replace(NetworkConfig.NM_IP_STRING, config.staticIp));
if (Platform.isRaspberryPi()) {
// Pi's need to manually have their interface adjusted?? and the 5 second sleep is
// integral in my testing (Matt)
shell.executeBashCommand(
"sh -c 'nmcli con down "
+ config.networkManagerIface
+ "; nmcli con up "
+ config.networkManagerIface
+ "'");
} else {
// for now just bring down /up -- more testing needed on beelink et al
shell.executeBashCommand(
"sh -c 'nmcli con down "
+ config.networkManagerIface
+ "; nmcli con up "
+ config.networkManagerIface
+ "'");
}
} catch (Exception e) {
logger.error("Error while setting static IP!", e);
}

View File

@@ -17,7 +17,14 @@
package org.photonvision.common.networking;
import com.fasterxml.jackson.annotation.JsonValue;
public enum NetworkMode {
DHCP,
STATIC
STATIC;
@JsonValue
public int toValue() {
return ordinal();
}
}

View File

@@ -22,6 +22,7 @@ import edu.wpi.first.apriltag.jni.AprilTagJNI;
import edu.wpi.first.cscore.CameraServerCvJNI;
import edu.wpi.first.cscore.CameraServerJNI;
import edu.wpi.first.hal.JNIWrapper;
import edu.wpi.first.math.geometry.Translation2d;
import edu.wpi.first.math.util.Units;
import edu.wpi.first.net.WPINetJNI;
import edu.wpi.first.networktables.NetworkTablesJNI;
@@ -139,6 +140,34 @@ public class TestUtils {
}
}
public enum WPI2023Apriltags {
k162_36_Angle,
k162_36_Straight,
k383_60_Angle2;
public static double FOV = 68.5;
public final Translation2d approxPose;
public final Path path;
Path getPath() {
var filename = this.toString().substring(1);
return Path.of("2023", "AprilTags", filename + ".png");
}
Translation2d getPose() {
var names = this.toString().substring(1).split("_");
var x = Units.inchesToMeters(Integer.parseInt(names[0]));
var y = Units.inchesToMeters(Integer.parseInt(names[1]));
return new Translation2d(x, y);
}
WPI2023Apriltags() {
this.approxPose = getPose();
this.path = getPath();
}
}
public enum WPI2022Image {
kTerminal12ft6in(Units.feetToMeters(12.5)),
kTerminal22ft6in(Units.feetToMeters(22.5));
@@ -196,14 +225,18 @@ public class TestUtils {
public enum ApriltagTestImages {
kRobots,
kTag1_640_480;
kTag1_640_480,
kTag1_16h5_1280,
kTag_corner_1280;
public final Path path;
Path getPath() {
// Strip leading k
var filename = this.toString().substring(1).toLowerCase();
return Path.of("apriltag", filename + ".jpg");
var extension = ".jpg";
if (filename.equals("tag1_16h5_1280")) extension = ".png";
return Path.of("apriltag", filename + extension);
}
ApriltagTestImages() {
@@ -211,7 +244,7 @@ public class TestUtils {
}
}
private static Path getResourcesFolderPath(boolean testMode) {
public static Path getResourcesFolderPath(boolean testMode) {
System.out.println("CWD: " + Path.of("").toAbsolutePath().toString());
// VSCode likes to make this path relative to the wrong root directory, so a fun hack to tell
@@ -302,6 +335,8 @@ public class TestUtils {
private static final String LIFECAM_240P_CAL_FILE = "lifecam240p.json";
private static final String LIFECAM_480P_CAL_FILE = "lifecam480p.json";
public static final String LIFECAM_1280P_CAL_FILE = "lifecam_1280.json";
public static final String LIMELIGHT_480P_CAL_FILE = "limelight_1280_720.json";
public static CameraCalibrationCoefficients getCoeffs(String filename, boolean testMode) {
try {
@@ -350,4 +385,14 @@ public class TestUtils {
public static void showImage(Mat frame) {
showImage(frame, DefaultTimeoutMillis);
}
public static Path getTestMode2023ImagePath() {
return getResourcesFolderPath(true)
.resolve("testimages")
.resolve(WPI2022Image.kTerminal22ft6in.path);
}
public static CameraCalibrationCoefficients get2023LifeCamCoeffs(boolean testMode) {
return getCoeffs(LIFECAM_1280P_CAL_FILE, testMode);
}
}

View File

@@ -31,8 +31,11 @@ import java.io.FileDescriptor;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.file.Path;
import java.util.HashMap;
public class JacksonUtils {
public static class UIMap extends HashMap<String, Object> {}
public static <T> void serialize(Path path, T object) throws IOException {
serialize(path, object, true);
}

View File

@@ -172,6 +172,17 @@ public class MathUtils {
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 Transform3d convertOpenCVtoPhotonTransform(Transform3d cameraToTarget3d) {
// TODO: Refactor into new pipe?
// CameraToTarget _should_ be in opencv-land EDN
var nwu =
CoordinateSystem.convert(
new Pose3d().transformBy(cameraToTarget3d),
CoordinateSystem.EDN(),
CoordinateSystem.NWU());
return new Transform3d(nwu.getTranslation(), WPILIB_BASE_ROTATION.rotateBy(nwu.getRotation()));
}
public static Pose3d convertOpenCVtoPhotonPose(Transform3d cameraToTarget3d) {
// TODO: Refactor into new pipe?
// CameraToTarget _should_ be in opencv-land EDN
@@ -208,6 +219,14 @@ public class MathUtils {
return new Transform3d(pose.getTranslation(), ocvRotation);
}
public static Pose3d convertArucotoOpenCV(Transform3d pose) {
var ocvRotation =
APRILTAG_BASE_ROTATION.rotateBy(
new Rotation3d(VecBuilder.fill(0, 0, 1), Units.degreesToRadians(180))
.rotateBy(pose.getRotation()));
return new Pose3d(pose.getTranslation(), ocvRotation);
}
public static void rotationToOpencvRvec(Rotation3d rotation, Mat rvecOutput) {
var angle = rotation.getAngle();
var axis = rotation.getAxis().times(angle);

View File

@@ -0,0 +1,83 @@
/*
* 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.aruco;
import java.util.Arrays;
import org.photonvision.common.logging.LogGroup;
import org.photonvision.common.logging.Logger;
public class ArucoDetectionResult {
private static final Logger logger =
new Logger(ArucoDetectionResult.class, LogGroup.VisionModule);
double[] xCorners;
double[] yCorners;
int id;
double[] tvec, rvec;
public ArucoDetectionResult(
double[] xCorners, double[] yCorners, int id, double[] tvec, double[] rvec) {
this.xCorners = xCorners;
this.yCorners = yCorners;
this.id = id;
this.tvec = tvec;
this.rvec = rvec;
// logger.debug("Creating a new detection result: " + this.toString());
}
public double[] getTvec() {
return tvec;
}
public double[] getRvec() {
return rvec;
}
public double[] getxCorners() {
return xCorners;
}
public double[] getyCorners() {
return yCorners;
}
public int getId() {
return id;
}
public double getCenterX() {
return (xCorners[0] + xCorners[1] + xCorners[2] + xCorners[3]) * .25;
}
public double getCenterY() {
return (yCorners[0] + yCorners[1] + yCorners[2] + yCorners[3]) * .25;
}
@Override
public String toString() {
return "ArucoDetectionResult{"
+ "xCorners="
+ Arrays.toString(xCorners)
+ ", yCorners="
+ Arrays.toString(yCorners)
+ ", id="
+ id
+ '}';
}
}

View File

@@ -0,0 +1,76 @@
/*
* 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.aruco;
import org.opencv.aruco.Aruco;
import org.opencv.aruco.ArucoDetector;
import org.opencv.aruco.DetectorParameters;
import org.opencv.aruco.Dictionary;
import org.photonvision.common.logging.LogGroup;
import org.photonvision.common.logging.Logger;
public class ArucoDetectorParams {
private static final Logger logger = new Logger(PhotonArucoDetector.class, LogGroup.VisionModule);
private float m_decimate = -1;
private int m_iterations = -1;
private double m_accuracy = -1;
DetectorParameters parameters = DetectorParameters.create();
ArucoDetector detector;
public ArucoDetectorParams() {
setDecimation(1);
setCornerAccuracy(25);
setCornerRefinementMaxIterations(100);
detector = new ArucoDetector(Dictionary.get(Aruco.DICT_APRILTAG_16h5), parameters);
}
public void setDecimation(float decimate) {
if (decimate == m_decimate) return;
logger.info("Setting decimation from " + m_decimate + " to " + decimate);
// We only need to mutate the parameters -- the detector keeps a poitner to the parameters
// object internally, so it should automatically update
parameters.set_aprilTagQuadDecimate((float) decimate);
m_decimate = decimate;
}
public void setCornerRefinementMaxIterations(int iters) {
if (iters == m_iterations || iters <= 0) return;
parameters.set_cornerRefinementMethod(Aruco.CORNER_REFINE_SUBPIX);
parameters.set_cornerRefinementMaxIterations(iters); // 200
m_iterations = iters;
}
public void setCornerAccuracy(double accuracy) {
if (accuracy == m_accuracy || accuracy <= 0) return;
parameters.set_cornerRefinementMinAccuracy(
accuracy / 1000.0); // divides by 1000 because the UI multiplies it by 1000
m_accuracy = accuracy;
}
public ArucoDetector getDetector() {
return detector;
}
}

View File

@@ -0,0 +1,132 @@
/*
* 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.aruco;
import edu.wpi.first.math.VecBuilder;
import edu.wpi.first.math.geometry.Pose3d;
import edu.wpi.first.math.geometry.Rotation3d;
import edu.wpi.first.math.geometry.Translation3d;
import edu.wpi.first.math.util.Units;
import java.util.ArrayList;
import org.opencv.aruco.Aruco;
import org.opencv.aruco.ArucoDetector;
import org.opencv.core.Mat;
import org.photonvision.common.logging.LogGroup;
import org.photonvision.common.logging.Logger;
import org.photonvision.vision.calibration.CameraCalibrationCoefficients;
public class PhotonArucoDetector {
private static final Logger logger = new Logger(PhotonArucoDetector.class, LogGroup.VisionModule);
private static final Rotation3d ARUCO_BASE_ROTATION =
new Rotation3d(VecBuilder.fill(0, 0, 1), Units.degreesToRadians(180));
Mat ids;
Mat tvecs;
Mat rvecs;
ArrayList<Mat> corners;
Mat cornerMat;
Translation3d translation;
Rotation3d rotation;
double timeStartDetect;
double timeEndDetect;
Pose3d tagPose;
double timeStartProcess;
double timeEndProcess;
double[] xCorners = new double[4];
double[] yCorners = new double[4];
public PhotonArucoDetector() {
logger.debug("New Aruco Detector");
ids = new Mat();
tvecs = new Mat();
rvecs = new Mat();
corners = new ArrayList<Mat>();
tagPose = new Pose3d();
translation = new Translation3d();
rotation = new Rotation3d();
}
public ArucoDetectionResult[] detect(
Mat grayscaleImg,
float tagSize,
CameraCalibrationCoefficients coeffs,
ArucoDetector detector) {
detector.detectMarkers(grayscaleImg, corners, ids);
if (coeffs != null) {
Aruco.estimatePoseSingleMarkers(
corners,
tagSize,
coeffs.getCameraIntrinsicsMat(),
coeffs.getDistCoeffsMat(),
rvecs,
tvecs);
}
ArucoDetectionResult[] toReturn = new ArucoDetectionResult[corners.size()];
timeStartProcess = System.currentTimeMillis();
for (int i = 0; i < corners.size(); i++) {
cornerMat = corners.get(i);
// logger.debug(cornerMat.dump());
xCorners =
new double[] {
cornerMat.get(0, 0)[0],
cornerMat.get(0, 1)[0],
cornerMat.get(0, 2)[0],
cornerMat.get(0, 3)[0]
};
yCorners =
new double[] {
cornerMat.get(0, 0)[1],
cornerMat.get(0, 1)[1],
cornerMat.get(0, 2)[1],
cornerMat.get(0, 3)[1]
};
cornerMat.release();
double[] tvec;
double[] rvec;
if (coeffs != null) {
// Need to apply a 180 rotation about Z
var origRvec = rvecs.get(i, 0);
var axisangle = VecBuilder.fill(origRvec[0], origRvec[1], origRvec[2]);
Rotation3d rotation = new Rotation3d(axisangle, axisangle.normF());
var ocvRotation = ARUCO_BASE_ROTATION.rotateBy(rotation);
var angle = ocvRotation.getAngle();
var finalAxisAngle = ocvRotation.getAxis().times(angle);
tvec = tvecs.get(i, 0);
rvec = finalAxisAngle.getData();
} else {
tvec = new double[] {0, 0, 0};
rvec = new double[] {0, 0, 0};
}
toReturn[i] =
new ArucoDetectionResult(xCorners, yCorners, (int) ids.get(i, 0)[0], tvec, rvec);
}
rvecs.release();
tvecs.release();
ids.release();
return toReturn;
}
}

View File

@@ -21,6 +21,7 @@ import com.fasterxml.jackson.annotation.JsonAlias;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.JsonNode;
import org.opencv.core.Mat;
import org.opencv.core.MatOfDouble;
import org.opencv.core.Size;
@@ -82,4 +83,43 @@ public class CameraCalibrationCoefficients implements Releasable {
cameraIntrinsics.release();
distCoeffs.release();
}
public static CameraCalibrationCoefficients parseFromCalibdbJson(JsonNode json) {
// camera_matrix is a row major, array of arrays
var cam_matrix = json.get("camera_matrix");
double[] cam_arr =
new double[] {
cam_matrix.get(0).get(0).doubleValue(),
cam_matrix.get(0).get(1).doubleValue(),
cam_matrix.get(0).get(2).doubleValue(),
cam_matrix.get(1).get(0).doubleValue(),
cam_matrix.get(1).get(1).doubleValue(),
cam_matrix.get(1).get(2).doubleValue(),
cam_matrix.get(2).get(0).doubleValue(),
cam_matrix.get(2).get(1).doubleValue(),
cam_matrix.get(2).get(2).doubleValue()
};
var dist_coefs = json.get("distortion_coefficients");
double[] dist_array =
new double[] {
dist_coefs.get(0).doubleValue(),
dist_coefs.get(1).doubleValue(),
dist_coefs.get(2).doubleValue(),
dist_coefs.get(3).doubleValue(),
dist_coefs.get(4).doubleValue(),
};
var cam_jsonmat = new JsonMat(3, 3, cam_arr);
var distortion_jsonmat = new JsonMat(1, 5, dist_array);
var error = json.get("avg_reprojection_error").asDouble();
var width = json.get("img_size").get(0).doubleValue();
var height = json.get("img_size").get(1).doubleValue();
return new CameraCalibrationCoefficients(
new Size(width, height), cam_jsonmat, distortion_jsonmat, new double[] {error}, 0);
}
}

View File

@@ -21,6 +21,10 @@ import edu.wpi.first.apriltag.AprilTagDetection;
import edu.wpi.first.apriltag.AprilTagPoseEstimate;
import edu.wpi.first.apriltag.AprilTagPoseEstimator;
import edu.wpi.first.apriltag.AprilTagPoseEstimator.Config;
import org.opencv.calib3d.Calib3d;
import org.opencv.core.MatOfPoint2f;
import org.opencv.core.Point;
import org.photonvision.vision.calibration.CameraCalibrationCoefficients;
import org.photonvision.vision.pipe.CVPipe;
public class AprilTagPoseEstimatorPipe
@@ -37,9 +41,48 @@ public class AprilTagPoseEstimatorPipe
super();
}
MatOfPoint2f temp = new MatOfPoint2f();
@Override
protected AprilTagPoseEstimate process(AprilTagDetection in) {
return m_poseEstimator.estimateOrthogonalIteration(in, params.nIters);
// Save the corner points of our detection to an array
Point corners[] = new Point[4];
for (int i = 0; i < 4; i++) {
corners[i] = new Point(in.getCornerX(i), in.getCornerY(i));
}
// And shove into our matofpoints
temp.fromArray(corners);
// Probably overwrites what was in temp before. I hope
Calib3d.undistortImagePoints(
temp,
temp,
params.calibration.getCameraIntrinsicsMat(),
params.calibration.getDistCoeffsMat());
// Save out undistorted corners
corners = temp.toArray();
// Apriltagdetection expects an array in form [x1 y1 x2 y2 ...]
var fixedCorners = new double[8];
for (int i = 0; i < 4; i++) {
fixedCorners[i * 2] = corners[i].x;
fixedCorners[i * 2 + 1] = corners[i].y;
}
// Create a new Detection with the fixed corners
var corrected =
new AprilTagDetection(
in.getFamily(),
in.getId(),
in.getHamming(),
in.getDecisionMargin(),
in.getHomography(),
in.getCenterX(),
in.getCenterY(),
fixedCorners);
return m_poseEstimator.estimateOrthogonalIteration(corrected, params.nIters);
}
@Override
@@ -57,11 +100,14 @@ public class AprilTagPoseEstimatorPipe
public static class AprilTagPoseEstimatorPipeParams {
final AprilTagPoseEstimator.Config config;
final CameraCalibrationCoefficients calibration;
final int nIters;
public AprilTagPoseEstimatorPipeParams(Config config, int nIters) {
public AprilTagPoseEstimatorPipeParams(
Config config, CameraCalibrationCoefficients cal, int nIters) {
this.config = config;
this.nIters = nIters;
this.calibration = cal;
}
@Override

View File

@@ -0,0 +1,50 @@
/*
* 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.pipe.impl;
import edu.wpi.first.math.util.Units;
import java.util.List;
import org.opencv.aruco.DetectorParameters;
import org.opencv.core.Mat;
import org.photonvision.vision.aruco.ArucoDetectionResult;
import org.photonvision.vision.aruco.PhotonArucoDetector;
import org.photonvision.vision.pipe.CVPipe;
public class ArucoDetectionPipe
extends CVPipe<Mat, List<ArucoDetectionResult>, ArucoDetectionPipeParams> {
PhotonArucoDetector detector = new PhotonArucoDetector();
@Override
protected List<ArucoDetectionResult> process(Mat in) {
return List.of(
detector.detect(
in,
(float) Units.inchesToMeters(6),
params.cameraCalibrationCoefficients,
params.detectorParams));
}
@Override
public void setParams(ArucoDetectionPipeParams params) {
super.setParams(params);
}
public DetectorParameters getParameters() {
return params == null ? null : params.detectorParams.get_params();
}
}

View File

@@ -0,0 +1,57 @@
/*
* 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.pipe.impl;
import java.util.Objects;
import org.opencv.aruco.ArucoDetector;
import org.photonvision.vision.calibration.CameraCalibrationCoefficients;
public class ArucoDetectionPipeParams {
public ArucoDetector detectorParams;
public final CameraCalibrationCoefficients cameraCalibrationCoefficients;
public ArucoDetectionPipeParams(
ArucoDetector detector, CameraCalibrationCoefficients cameraCalibrationCoefficients) {
this.detectorParams = detector;
this.cameraCalibrationCoefficients = cameraCalibrationCoefficients;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
ArucoDetectionPipeParams that = (ArucoDetectionPipeParams) o;
return Objects.equals(detectorParams, that.detectorParams)
&& Objects.equals(cameraCalibrationCoefficients, that.cameraCalibrationCoefficients);
}
@Override
public int hashCode() {
return Objects.hash(detectorParams, cameraCalibrationCoefficients);
}
@Override
public String toString() {
return "ArucoDetectionPipeParams{"
+ "detectorParams="
+ detectorParams
+ ", cameraCalibrationCoefficients="
+ cameraCalibrationCoefficients
+ '}';
}
}

View File

@@ -47,7 +47,7 @@ public class CornerDetectionPipe
{
var targetCorners =
detectExtremeCornersByApproxPolyDp(target, params.calculateConvexHulls);
target.setCorners(targetCorners);
target.setTargetCorners(targetCorners);
break;
}
default:

View File

@@ -0,0 +1,34 @@
/*
* 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.pipe.impl;
import java.awt.*;
import org.photonvision.vision.frame.FrameDivisor;
public class Draw2dArucoPipe extends Draw2dTargetsPipe {
public static class Draw2dArucoParams extends Draw2dTargetsPipe.Draw2dTargetsParams {
public Draw2dArucoParams(
boolean shouldDraw, boolean showMultipleTargets, FrameDivisor divisor) {
super(shouldDraw, showMultipleTargets, divisor);
// We want to show the polygon, not the rotated box
this.showRotatedBox = false;
this.showMaximumBox = false;
this.rotatedBoxColor = Color.RED;
}
}
}

View File

@@ -30,6 +30,7 @@ public class Draw3dAprilTagsPipe extends Draw3dTargetsPipe {
FrameDivisor divisor) {
super(shouldDraw, cameraCalibrationCoefficients, targetModel, divisor);
this.shouldDrawHull = false;
this.redistortPoints = true;
}
}
}

View File

@@ -0,0 +1,35 @@
/*
* 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.pipe.impl;
import org.photonvision.vision.calibration.CameraCalibrationCoefficients;
import org.photonvision.vision.frame.FrameDivisor;
import org.photonvision.vision.target.TargetModel;
public class Draw3dArucoPipe extends Draw3dTargetsPipe {
public static class Draw3dArucoParams extends Draw3dContoursParams {
public Draw3dArucoParams(
boolean shouldDraw,
CameraCalibrationCoefficients cameraCalibrationCoefficients,
TargetModel targetModel,
FrameDivisor divisor) {
super(shouldDraw, cameraCalibrationCoefficients, targetModel, divisor);
this.shouldDrawHull = false;
}
}
}

View File

@@ -93,7 +93,12 @@ public class Draw3dTargetsPipe
params.cameraCalibrationCoefficients.getDistCoeffsMat(),
tempMat,
jac);
// Distort the points so they match the image they're being overlaid on
if (params.redistortPoints) {
// Distort the points so they match the image they're being overlaid on
distortPoints(tempMat, tempMat);
}
var bottomPoints = tempMat.toList();
Calib3d.projectPoints(
@@ -104,6 +109,11 @@ public class Draw3dTargetsPipe
params.cameraCalibrationCoefficients.getDistCoeffsMat(),
tempMat,
jac);
if (params.redistortPoints) {
// Distort the points so they match the image they're being overlaid on
distortPoints(tempMat, tempMat);
}
var topPoints = tempMat.toList();
dividePointList(bottomPoints);
@@ -290,6 +300,8 @@ public class Draw3dTargetsPipe
public final CameraCalibrationCoefficients cameraCalibrationCoefficients;
public final FrameDivisor divisor;
public boolean redistortPoints = false;
public Draw3dContoursParams(
boolean shouldDraw,
CameraCalibrationCoefficients cameraCalibrationCoefficients,

View File

@@ -17,7 +17,6 @@
package org.photonvision.vision.pipe.impl;
import java.util.Objects;
import org.apache.commons.lang3.tuple.Pair;
import org.apache.commons.lang3.tuple.Triple;
import org.opencv.calib3d.Calib3d;
@@ -25,6 +24,7 @@ import org.opencv.core.*;
import org.opencv.imgproc.Imgproc;
import org.photonvision.common.logging.LogGroup;
import org.photonvision.common.logging.Logger;
import org.photonvision.vision.frame.FrameDivisor;
import org.photonvision.vision.pipe.CVPipe;
import org.photonvision.vision.pipeline.UICalibrationData;
@@ -39,10 +39,6 @@ public class FindBoardCornersPipe
Size imageSize;
Size patternSize;
// Tune to taste for a reasonable tradeoff between making
// the findCorners portion work hard, versus the subpixel refinement work hard.
final int FIND_CORNERS_WIDTH_PX = 320;
// Configure the optimizations used while using openCV's find corners algorithm
// Since we return results in real-time, we want ensure it goes as fast as possible
// and fails as fast as possible.
@@ -125,17 +121,13 @@ public class FindBoardCornersPipe
/**
* Figures out how much a frame or point cloud must be scaled down by to match the desired size at
* which to run FindCorners
* which to run FindCorners. Should usually be > 1.
*
* @param inFrame
* @return
*/
private double getFindCornersScaleFactor(Mat inFrame) {
if (inFrame.width() > FIND_CORNERS_WIDTH_PX) {
return ((double) FIND_CORNERS_WIDTH_PX) / inFrame.width();
} else {
return 1.0;
}
return 1.0 / params.divisor.value;
}
/**
@@ -174,9 +166,10 @@ public class FindBoardCornersPipe
* findBoardCorners
* @return the size to scale the input mat to
*/
private Size getFindCornersImgSize(Mat inFrame) {
var findcorners_height = Math.round(inFrame.height() * getFindCornersScaleFactor(inFrame));
return new Size(FIND_CORNERS_WIDTH_PX, findcorners_height);
private Size getFindCornersImgSize(Mat in) {
int width = in.cols() / params.divisor.value;
int height = in.rows() / params.divisor.value;
return new Size(width, height);
}
/**
@@ -295,29 +288,48 @@ public class FindBoardCornersPipe
private final int boardWidth;
private final UICalibrationData.BoardType type;
private final double gridSize;
private final FrameDivisor divisor;
public FindCornersPipeParams(
int boardHeight, int boardWidth, UICalibrationData.BoardType type, double gridSize) {
int boardHeight,
int boardWidth,
UICalibrationData.BoardType type,
double gridSize,
FrameDivisor divisor) {
this.boardHeight = boardHeight;
this.boardWidth = boardWidth;
this.type = type;
this.gridSize = gridSize; // mm
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
FindCornersPipeParams that = (FindCornersPipeParams) o;
return boardHeight == that.boardHeight
&& boardWidth == that.boardWidth
&& Double.compare(that.gridSize, gridSize) == 0
&& type == that.type;
this.divisor = divisor;
}
@Override
public int hashCode() {
return Objects.hash(boardHeight, boardWidth, type, gridSize);
final int prime = 31;
int result = 1;
result = prime * result + boardHeight;
result = prime * result + boardWidth;
result = prime * result + ((type == null) ? 0 : type.hashCode());
long temp;
temp = Double.doubleToLongBits(gridSize);
result = prime * result + (int) (temp ^ (temp >>> 32));
result = prime * result + ((divisor == null) ? 0 : divisor.hashCode());
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (obj == null) return false;
if (getClass() != obj.getClass()) return false;
FindCornersPipeParams other = (FindCornersPipeParams) obj;
if (boardHeight != other.boardHeight) return false;
if (boardWidth != other.boardWidth) return false;
if (type != other.type) return false;
if (Double.doubleToLongBits(gridSize) != Double.doubleToLongBits(other.gridSize))
return false;
if (divisor != other.divisor) return false;
return true;
}
}
}

View File

@@ -97,7 +97,9 @@ public class AprilTagPipeline extends CVPipeline<CVPipelineResult, AprilTagPipel
poseEstimatorPipe.setParams(
new AprilTagPoseEstimatorPipeParams(
new Config(tagWidth, fx, fy, cx, cy), settings.numIterations));
new Config(tagWidth, fx, fy, cx, cy),
frameStaticProperties.cameraCalibration,
settings.numIterations));
}
}
}

View File

@@ -30,7 +30,7 @@ public class AprilTagPipelineSettings extends AdvancedPipelineSettings {
public int threads = 4; // Multiple threads seems to be better performance on most platforms
public boolean debug = false;
public boolean refineEdges = true;
public int numIterations = 200;
public int numIterations = 40;
public int hammingDist = 0;
public int decisionMargin = 35;

View File

@@ -0,0 +1,128 @@
/*
* Copyright (C) Photon Vision.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
/*
* Copyright (C) Photon Vision.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.photonvision.vision.pipeline;
import edu.wpi.first.math.geometry.Transform3d;
import java.util.ArrayList;
import java.util.List;
import org.opencv.core.Mat;
import org.photonvision.vision.aruco.ArucoDetectionResult;
import org.photonvision.vision.aruco.ArucoDetectorParams;
import org.photonvision.vision.frame.Frame;
import org.photonvision.vision.frame.FrameThresholdType;
import org.photonvision.vision.pipe.CVPipe.CVPipeResult;
import org.photonvision.vision.pipe.impl.*;
import org.photonvision.vision.pipeline.result.CVPipelineResult;
import org.photonvision.vision.target.TrackedTarget;
import org.photonvision.vision.target.TrackedTarget.TargetCalculationParameters;
@SuppressWarnings("DuplicatedCode")
public class ArucoPipeline extends CVPipeline<CVPipelineResult, ArucoPipelineSettings> {
private final RotateImagePipe rotateImagePipe = new RotateImagePipe();
private final GrayscalePipe grayscalePipe = new GrayscalePipe();
private final ArucoDetectionPipe arucoDetectionPipe = new ArucoDetectionPipe();
private final CalculateFPSPipe calculateFPSPipe = new CalculateFPSPipe();
ArucoDetectorParams m_arucoDetectorParams = new ArucoDetectorParams();
public ArucoPipeline() {
super(FrameThresholdType.GREYSCALE);
settings = new ArucoPipelineSettings();
}
public ArucoPipeline(ArucoPipelineSettings settings) {
super(FrameThresholdType.GREYSCALE);
this.settings = settings;
}
@Override
protected void setPipeParamsImpl() {
// Sanitize thread count - not supported to have fewer than 1 threads
settings.threads = Math.max(1, settings.threads);
RotateImagePipe.RotateImageParams rotateImageParams =
new RotateImagePipe.RotateImageParams(settings.inputImageRotationMode);
rotateImagePipe.setParams(rotateImageParams);
m_arucoDetectorParams.setDecimation((float) settings.decimate);
m_arucoDetectorParams.setCornerRefinementMaxIterations(settings.numIterations);
m_arucoDetectorParams.setCornerAccuracy(settings.cornerAccuracy);
arucoDetectionPipe.setParams(
new ArucoDetectionPipeParams(
m_arucoDetectorParams.getDetector(), frameStaticProperties.cameraCalibration));
}
@Override
protected CVPipelineResult process(Frame frame, ArucoPipelineSettings settings) {
long sumPipeNanosElapsed = 0L;
Mat rawInputMat;
rawInputMat = frame.colorImage.getMat();
List<TrackedTarget> targetList;
CVPipeResult<List<ArucoDetectionResult>> tagDetectionPipeResult;
if (rawInputMat.empty()) {
return new CVPipelineResult(sumPipeNanosElapsed, 0, List.of(), frame);
}
tagDetectionPipeResult = arucoDetectionPipe.run(rawInputMat);
targetList = new ArrayList<>();
for (ArucoDetectionResult detection : tagDetectionPipeResult.output) {
// TODO this should be in a pipe, not in the top level here (Matt)
// populate the target list
// Challenge here is that TrackedTarget functions with OpenCV Contour
TrackedTarget target =
new TrackedTarget(
detection,
new TargetCalculationParameters(
false, null, null, null, null, frameStaticProperties));
var correctedBestPose = target.getBestCameraToTarget3d();
target.setBestCameraToTarget3d(
new Transform3d(correctedBestPose.getTranslation(), correctedBestPose.getRotation()));
targetList.add(target);
}
var fpsResult = calculateFPSPipe.run(null);
var fps = fpsResult.output;
return new CVPipelineResult(sumPipeNanosElapsed, fps, targetList, frame);
}
}

View File

@@ -0,0 +1,42 @@
/*
* Copyright (C) Photon Vision.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.photonvision.vision.pipeline;
import com.fasterxml.jackson.annotation.JsonTypeName;
import org.photonvision.vision.target.TargetModel;
@JsonTypeName("ArucoPipelineSettings")
public class ArucoPipelineSettings extends AdvancedPipelineSettings {
public double decimate = 1;
public int threads = 2;
public int numIterations = 100;
public double cornerAccuracy = 25.0;
public boolean useAruco3 = true;
// 3d settings
public ArucoPipelineSettings() {
super();
pipelineType = PipelineType.Aruco;
outputShowMultipleTargets = true;
targetModel = TargetModel.kAruco6in_16h5;
cameraExposure = -1;
cameraAutoExposure = true;
ledMode = false;
}
}

View File

@@ -31,7 +31,8 @@ import org.photonvision.vision.opencv.ImageRotationMode;
@JsonSubTypes.Type(value = ColoredShapePipelineSettings.class),
@JsonSubTypes.Type(value = ReflectivePipelineSettings.class),
@JsonSubTypes.Type(value = DriverModePipelineSettings.class),
@JsonSubTypes.Type(value = AprilTagPipelineSettings.class)
@JsonSubTypes.Type(value = AprilTagPipelineSettings.class),
@JsonSubTypes.Type(value = ArucoPipelineSettings.class)
})
public class CVPipelineSettings implements Cloneable {
public int pipelineIndex = 0;

View File

@@ -87,7 +87,11 @@ public class Calibrate3dPipeline
protected void setPipeParamsImpl() {
FindBoardCornersPipe.FindCornersPipeParams findCornersPipeParams =
new FindBoardCornersPipe.FindCornersPipeParams(
settings.boardHeight, settings.boardWidth, settings.boardType, settings.gridSize);
settings.boardHeight,
settings.boardWidth,
settings.boardType,
settings.gridSize,
settings.streamingFrameDivisor);
findBoardCornersPipe.setParams(findCornersPipeParams);
Calibrate3dPipe.CalibratePipeParams calibratePipeParams =
@@ -105,7 +109,7 @@ public class Calibrate3dPipeline
protected CVPipelineResult process(Frame frame, Calibration3dPipelineSettings settings) {
Mat inputColorMat = frame.colorImage.getMat();
if (this.calibrating) {
if (this.calibrating || inputColorMat.empty()) {
return new CVPipelineResult(0, 0, null, frame);
}

View File

@@ -19,6 +19,7 @@ package org.photonvision.vision.pipeline;
import edu.wpi.first.math.util.Units;
import org.opencv.core.Size;
import org.photonvision.vision.frame.FrameDivisor;
public class Calibration3dPipelineSettings extends AdvancedPipelineSettings {
public int boardHeight = 8;
@@ -33,5 +34,6 @@ public class Calibration3dPipelineSettings extends AdvancedPipelineSettings {
this.cameraAutoExposure = true;
this.inputShouldShow = true;
this.outputShouldShow = true;
this.streamingFrameDivisor = FrameDivisor.HALF;
}
}

View File

@@ -223,7 +223,7 @@ public class ColoredShapePipeline
collect2dTargetsResult.output.forEach(
shape -> {
shape.getMinAreaRect().points(rectPoints);
shape.setCorners(Arrays.asList(rectPoints));
shape.setTargetCorners(Arrays.asList(rectPoints));
});
sumPipeNanosElapsed += cornerDetectionResult.nanosElapsed;

View File

@@ -24,6 +24,7 @@ import org.photonvision.vision.frame.FrameStaticProperties;
import org.photonvision.vision.opencv.DualOffsetValues;
import org.photonvision.vision.pipe.impl.*;
import org.photonvision.vision.pipeline.result.CVPipelineResult;
import org.photonvision.vision.target.TargetModel;
import org.photonvision.vision.target.TrackedTarget;
/**
@@ -37,6 +38,9 @@ public class OutputStreamPipeline {
private final Draw3dTargetsPipe draw3dTargetsPipe = new Draw3dTargetsPipe();
private final Draw2dAprilTagsPipe draw2dAprilTagsPipe = new Draw2dAprilTagsPipe();
private final Draw3dAprilTagsPipe draw3dAprilTagsPipe = new Draw3dAprilTagsPipe();
private final Draw2dArucoPipe draw2dArucoPipe = new Draw2dArucoPipe();
private final Draw3dArucoPipe draw3dArucoPipe = new Draw3dArucoPipe();
private final CalculateFPSPipe calculateFPSPipe = new CalculateFPSPipe();
private final ResizeImagePipe resizeImagePipe = new ResizeImagePipe();
@@ -65,6 +69,13 @@ public class OutputStreamPipeline {
settings.streamingFrameDivisor);
draw2dAprilTagsPipe.setParams(draw2DAprilTagsParams);
var draw2DArucoParams =
new Draw2dArucoPipe.Draw2dArucoParams(
settings.outputShouldDraw,
settings.outputShowMultipleTargets,
settings.streamingFrameDivisor);
draw2dArucoPipe.setParams(draw2DArucoParams);
var draw2dCrosshairParams =
new Draw2dCrosshairPipe.Draw2dCrosshairParams(
settings.outputShouldDraw,
@@ -92,6 +103,14 @@ public class OutputStreamPipeline {
settings.streamingFrameDivisor);
draw3dAprilTagsPipe.setParams(draw3dAprilTagsParams);
var draw3dArucoParams =
new Draw3dArucoPipe.Draw3dArucoParams(
settings.outputShouldDraw,
frameStaticProperties.cameraCalibration,
TargetModel.k6in_16h5,
settings.streamingFrameDivisor);
draw3dArucoPipe.setParams(draw3dArucoParams);
resizeImagePipe.setParams(
new ResizeImagePipe.ResizeImageParams(settings.streamingFrameDivisor));
}
@@ -126,12 +145,12 @@ public class OutputStreamPipeline {
}
// Draw 2D Crosshair on output
var draw2dCrosshairResultOnInput = draw2dCrosshairPipe.run(Pair.of(outMat, targetsToDraw));
var draw2dCrosshairResultOnInput = draw2dCrosshairPipe.run(Pair.of(inMat, targetsToDraw));
sumPipeNanosElapsed += pipeProfileNanos[3] = draw2dCrosshairResultOnInput.nanosElapsed;
if (!(settings instanceof AprilTagPipelineSettings)) {
// If we're processing anything other than Apriltags...
if (!(settings instanceof AprilTagPipelineSettings)
&& !(settings instanceof ArucoPipelineSettings)) {
// If we're processing anything other than Apriltags..
var draw2dCrosshairResultOnOutput = draw2dCrosshairPipe.run(Pair.of(outMat, targetsToDraw));
sumPipeNanosElapsed += pipeProfileNanos[4] = draw2dCrosshairResultOnOutput.nanosElapsed;
@@ -154,7 +173,7 @@ public class OutputStreamPipeline {
pipeProfileNanos[8] = 0;
}
} else {
} else if (settings instanceof AprilTagPipelineSettings) {
// If we are doing apriltags...
if (settings.solvePNPEnabled) {
// Draw 3d Apriltag markers (camera is calibrated and running in 3d mode)
@@ -171,6 +190,26 @@ public class OutputStreamPipeline {
var draw2dTargetsOnInput = draw2dAprilTagsPipe.run(Pair.of(outMat, targetsToDraw));
sumPipeNanosElapsed += pipeProfileNanos[5] = draw2dTargetsOnInput.nanosElapsed;
pipeProfileNanos[6] = 0;
pipeProfileNanos[7] = 0;
pipeProfileNanos[8] = 0;
}
} else if (settings instanceof ArucoPipelineSettings) {
if (settings.solvePNPEnabled) {
// Draw 3d Apriltag markers (camera is calibrated and running in 3d mode)
pipeProfileNanos[5] = 0;
pipeProfileNanos[6] = 0;
var drawOnInputResult = draw3dArucoPipe.run(Pair.of(outMat, targetsToDraw));
sumPipeNanosElapsed += pipeProfileNanos[7] = drawOnInputResult.nanosElapsed;
pipeProfileNanos[8] = 0;
} else {
// Draw 2d apriltag markers
var draw2dTargetsOnInput = draw2dArucoPipe.run(Pair.of(outMat, targetsToDraw));
sumPipeNanosElapsed += pipeProfileNanos[5] = draw2dTargetsOnInput.nanosElapsed;
pipeProfileNanos[6] = 0;
pipeProfileNanos[7] = 0;
pipeProfileNanos[8] = 0;

View File

@@ -23,7 +23,8 @@ public enum PipelineType {
DriverMode(-1, DriverModePipeline.class),
Reflective(0, ReflectivePipeline.class),
ColoredShape(1, ColoredShapePipeline.class),
AprilTag(2, AprilTagPipeline.class);
AprilTag(2, AprilTagPipeline.class),
Aruco(3, ArucoPipeline.class);
public final int baseIndex;
public final Class clazz;

View File

@@ -21,6 +21,7 @@ import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
import org.opencv.aruco.Aruco;
import org.photonvision.common.configuration.CameraConfiguration;
import org.photonvision.common.configuration.ConfigManager;
import org.photonvision.common.dataflow.DataChangeService;
@@ -205,6 +206,11 @@ public class PipelineManager {
currentUserPipeline =
new AprilTagPipeline((AprilTagPipelineSettings) desiredPipelineSettings);
break;
case Aruco:
logger.debug("Creating Aruco Pipeline");
currentUserPipeline = new ArucoPipeline((ArucoPipelineSettings) desiredPipelineSettings);
break;
default:
// Can be calib3d or drivermode, both of which are special cases
break;
@@ -300,6 +306,12 @@ public class PipelineManager {
added.pipelineNickname = nickname;
return added;
}
case Aruco:
{
var added = new ArucoPipelineSettings();
added.pipelineNickname = nickname;
return added;
}
default:
{
logger.error("Got invalid pipeline type: " + type.toString());

View File

@@ -136,7 +136,7 @@ public class VisionModule {
new NTDataPublisher(
visionSource.getSettables().getConfiguration().nickname,
pipelineManager::getCurrentPipelineIndex,
pipelineManager::setIndex,
this::setPipeline,
pipelineManager::getDriverMode,
this::setDriverMode);
uiDataConsumer = new UIDataPublisher(index);
@@ -586,4 +586,15 @@ public class VisionModule {
logger.error("Cannot set target model of non-reflective pipe! Ignoring...");
}
}
public void addCalibrationToConfig(CameraCalibrationCoefficients newCalibration) {
if (newCalibration != null) {
logger.info("Got new calibration for " + newCalibration.resolution);
visionSource.getSettables().getConfiguration().addCalibration(newCalibration);
} else {
logger.error("Got null calibration?");
}
saveAndBroadcastAll();
}
}

View File

@@ -28,6 +28,7 @@ import org.photonvision.common.logging.LogGroup;
import org.photonvision.common.logging.Logger;
import org.photonvision.common.util.numbers.DoubleCouple;
import org.photonvision.common.util.numbers.IntegerCouple;
import org.photonvision.vision.calibration.CameraCalibrationCoefficients;
import org.photonvision.vision.pipeline.AdvancedPipelineSettings;
import org.photonvision.vision.pipeline.PipelineType;
import org.photonvision.vision.pipeline.UICalibrationData;
@@ -114,6 +115,10 @@ public class VisionModuleChangeSubscriber extends DataChangeSubscriber {
parentModule.setPipeline(idx);
parentModule.saveAndBroadcastAll();
return;
case "calibrationUploaded":
if (newPropValue instanceof CameraCalibrationCoefficients)
parentModule.addCalibrationToConfig((CameraCalibrationCoefficients) newPropValue);
return;
case "robotOffsetPoint":
if (currentSettings instanceof AdvancedPipelineSettings) {
var curAdvSettings = (AdvancedPipelineSettings) currentSettings;

View File

@@ -109,6 +109,14 @@ public enum TargetModel implements Releasable {
new Point3(Units.inchesToMeters(3.25), -Units.inchesToMeters(3.25), 0),
new Point3(-Units.inchesToMeters(3.25), -Units.inchesToMeters(3.25), 0)),
Units.inchesToMeters(3.25 * 2)),
kAruco6in_16h5( // Nominal edge length of 200 mm includes the white border, but solvePNP corners
// do not
List.of(
new Point3(Units.inchesToMeters(3), Units.inchesToMeters(3), 0),
new Point3(Units.inchesToMeters(3), -Units.inchesToMeters(3), 0),
new Point3(-Units.inchesToMeters(3), -Units.inchesToMeters(3), 0),
new Point3(Units.inchesToMeters(3), -Units.inchesToMeters(3), 0)),
Units.inchesToMeters(3 * 2)),
k6in_16h5( // Nominal edge length of 200 mm includes the white border, but solvePNP corners
// do not
List.of(

View File

@@ -18,7 +18,10 @@ package org.photonvision.vision.target;
import edu.wpi.first.apriltag.AprilTagDetection;
import edu.wpi.first.apriltag.AprilTagPoseEstimate;
import edu.wpi.first.math.VecBuilder;
import edu.wpi.first.math.geometry.Rotation3d;
import edu.wpi.first.math.geometry.Transform3d;
import edu.wpi.first.math.geometry.Translation3d;
import java.util.HashMap;
import java.util.List;
import org.opencv.core.CvType;
@@ -28,6 +31,7 @@ import org.opencv.core.MatOfPoint2f;
import org.opencv.core.Point;
import org.opencv.core.RotatedRect;
import org.photonvision.common.util.math.MathUtils;
import org.photonvision.vision.aruco.ArucoDetectionResult;
import org.photonvision.vision.frame.FrameStaticProperties;
import org.photonvision.vision.opencv.*;
@@ -37,7 +41,7 @@ public class TrackedTarget implements Releasable {
private MatOfPoint2f m_approximateBoundingPolygon;
private List<Point> m_targetCorners;
private List<Point> m_targetCorners = List.of();
private Point m_targetOffsetPoint;
private Point m_robotOffsetPoint;
@@ -135,6 +139,59 @@ public class TrackedTarget implements Releasable {
setCameraRelativeRvec(rvec);
}
public TrackedTarget(ArucoDetectionResult result, TargetCalculationParameters params) {
m_targetOffsetPoint = new Point(result.getCenterX(), result.getCenterY());
m_robotOffsetPoint = new Point();
m_pitch =
TargetCalculations.calculatePitch(
result.getCenterY(), params.cameraCenterPoint.y, params.verticalFocalLength);
m_yaw =
TargetCalculations.calculateYaw(
result.getCenterX(), params.cameraCenterPoint.x, params.horizontalFocalLength);
double[] xCorners = result.getxCorners();
double[] yCorners = result.getyCorners();
Point[] cornerPoints =
new Point[] {
new Point(xCorners[0], yCorners[0]),
new Point(xCorners[1], yCorners[1]),
new Point(xCorners[2], yCorners[2]),
new Point(xCorners[3], yCorners[3])
};
m_targetCorners = List.of(cornerPoints);
MatOfPoint contourMat = new MatOfPoint(cornerPoints);
m_approximateBoundingPolygon = new MatOfPoint2f(cornerPoints);
m_mainContour = new Contour(contourMat);
m_area = m_mainContour.getArea() / params.imageArea * 100;
m_fiducialId = result.getId();
m_shape = null;
// TODO implement skew? or just yeet
var tvec = new Mat(3, 1, CvType.CV_64FC1);
tvec.put(0, 0, result.getTvec());
setCameraRelativeTvec(tvec);
var rvec = new Mat(3, 1, CvType.CV_64FC1);
rvec.put(0, 0, result.getRvec());
setCameraRelativeRvec(rvec);
{
Translation3d translation =
// new Translation3d(tVec.get(0, 0)[0], tVec.get(1, 0)[0], tVec.get(2, 0)[0]);
new Translation3d(result.getTvec()[0], result.getTvec()[1], result.getTvec()[2]);
var axisangle =
VecBuilder.fill(result.getRvec()[0], result.getRvec()[1], result.getRvec()[2]);
Rotation3d rotation = new Rotation3d(axisangle, axisangle.normF());
Transform3d targetPose =
MathUtils.convertOpenCVtoPhotonTransform(new Transform3d(translation, rotation));
m_bestCameraToTarget3d = targetPose;
}
}
public void setFiducialId(int id) {
m_fiducialId = id;
}
@@ -232,7 +289,7 @@ public class TrackedTarget implements Releasable {
if (m_cameraRelativeRvec != null) m_cameraRelativeRvec.release();
}
public void setCorners(List<Point> targetCorners) {
public void setTargetCorners(List<Point> targetCorners) {
this.m_targetCorners = targetCorners;
}

View File

@@ -17,6 +17,7 @@
package org.photonvision.vision.videoStream;
import edu.wpi.first.cscore.CameraServerJNI;
import java.nio.ByteBuffer;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
@@ -51,7 +52,9 @@ public class SocketVideoStream implements Consumer<CVMat> {
public SocketVideoStream(int portID) {
this.portID = portID;
oldSchoolServer =
new MJPGFrameConsumer("Port_" + Integer.toString(portID) + "_MJPEG_Server", portID);
new MJPGFrameConsumer(
CameraServerJNI.getHostname() + "_Port_" + Integer.toString(portID) + "_MJPEG_Server",
portID);
}
@Override

View File

@@ -18,7 +18,6 @@
package org.photonvision.vision.pipeline;
import edu.wpi.first.math.geometry.Translation3d;
import java.io.IOException;
import java.util.stream.Collectors;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
@@ -33,7 +32,7 @@ import org.photonvision.vision.target.TrackedTarget;
public class AprilTagTest {
@BeforeEach
public void Init() throws IOException {
public void Init() {
TestUtils.loadLibraries();
}
@@ -67,22 +66,21 @@ public class AprilTagTest {
// Draw on input
var outputPipe = new OutputStreamPipeline();
outputPipe.process(
pipelineResult.inputAndOutputFrame, pipeline.getSettings(), pipelineResult.targets);
var ret =
outputPipe.process(
pipelineResult.inputAndOutputFrame, pipeline.getSettings(), pipelineResult.targets);
TestUtils.showImage(
pipelineResult.inputAndOutputFrame.colorImage.getMat(), "Pipeline output", 999999);
TestUtils.showImage(ret.inputAndOutputFrame.processedImage.getMat(), "Pipeline output", 999999);
// these numbers are not *accurate*, but they are known and expected
var pose = pipelineResult.targets.get(0).getBestCameraToTarget3d();
Assertions.assertEquals(2, pose.getTranslation().getX(), 0.2);
Assertions.assertEquals(0.0, pose.getTranslation().getY(), 0.2);
Assertions.assertEquals(0.0, pose.getTranslation().getY(), 0.2);
Assertions.assertEquals(0.0, pose.getTranslation().getZ(), 0.2);
var objX = new Translation3d(1, 0, 0).rotateBy(pose.getRotation()).getY();
var objY = new Translation3d(0, 1, 0).rotateBy(pose.getRotation()).getZ();
var objZ = new Translation3d(0, 0, 1).rotateBy(pose.getRotation()).getX();
System.out.printf("Object x %.2f y %.2f z %.2f\n", objX, objY, objZ);
// We expect the object X to be forward, or -X in world space
Assertions.assertEquals(
@@ -94,6 +92,49 @@ public class AprilTagTest {
Assertions.assertEquals(1, new Translation3d(0, 0, 1).rotateBy(pose.getRotation()).getZ(), 0.1);
}
@Test
public void testApriltagDistorted() {
var pipeline = new AprilTagPipeline();
pipeline.getSettings().inputShouldShow = true;
pipeline.getSettings().outputShouldDraw = true;
pipeline.getSettings().solvePNPEnabled = true;
pipeline.getSettings().cornerDetectionAccuracyPercentage = 4;
pipeline.getSettings().cornerDetectionUseConvexHulls = true;
pipeline.getSettings().targetModel = TargetModel.k200mmAprilTag;
pipeline.getSettings().tagFamily = AprilTagFamily.kTag16h5;
var frameProvider =
new FileFrameProvider(
TestUtils.getApriltagImagePath(TestUtils.ApriltagTestImages.kTag_corner_1280, false),
TestUtils.WPI2020Image.FOV,
TestUtils.getCoeffs(TestUtils.LIMELIGHT_480P_CAL_FILE, false));
frameProvider.requestFrameThresholdType(pipeline.getThresholdType());
CVPipelineResult pipelineResult;
try {
pipelineResult = pipeline.run(frameProvider.get(), QuirkyCamera.DefaultCamera);
printTestResults(pipelineResult);
} catch (RuntimeException e) {
// For now, will throw coz rotation3d ctor
return;
}
// Draw on input
var outputPipe = new OutputStreamPipeline();
var ret =
outputPipe.process(
pipelineResult.inputAndOutputFrame, pipeline.getSettings(), pipelineResult.targets);
TestUtils.showImage(ret.inputAndOutputFrame.processedImage.getMat(), "Pipeline output", 999999);
// these numbers are not *accurate*, but they are known and expected
var pose = pipelineResult.targets.get(0).getBestCameraToTarget3d();
Assertions.assertEquals(4.14, pose.getTranslation().getX(), 0.2);
Assertions.assertEquals(2, pose.getTranslation().getY(), 0.2);
Assertions.assertEquals(0.0, pose.getTranslation().getZ(), 0.2);
}
private static void printTestResults(CVPipelineResult pipelineResult) {
double fps = 1000 / pipelineResult.getLatencyMillis();
System.out.println(

View File

@@ -0,0 +1,86 @@
/*
* Copyright (C) Photon Vision.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.photonvision.vision.pipeline;
import java.io.IOException;
import java.util.stream.Collectors;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.photonvision.common.util.TestUtils;
import org.photonvision.vision.camera.QuirkyCamera;
import org.photonvision.vision.frame.provider.FileFrameProvider;
import org.photonvision.vision.pipeline.result.CVPipelineResult;
import org.photonvision.vision.target.TargetModel;
import org.photonvision.vision.target.TrackedTarget;
public class ArucoPipelineTest {
@BeforeEach
public void Init() throws IOException {
TestUtils.loadLibraries();
}
@Test
public void testApriltagFacingCameraAruco() {
var pipeline = new ArucoPipeline();
pipeline.getSettings().inputShouldShow = true;
pipeline.getSettings().outputShouldDraw = true;
pipeline.getSettings().solvePNPEnabled = true;
pipeline.getSettings().cornerDetectionAccuracyPercentage = 4;
pipeline.getSettings().cornerDetectionUseConvexHulls = true;
pipeline.getSettings().targetModel = TargetModel.k200mmAprilTag;
// pipeline.getSettings().tagFamily = AprilTagFamily.kTag36h11;
var frameProvider =
new FileFrameProvider(
TestUtils.getApriltagImagePath(TestUtils.ApriltagTestImages.kTag1_16h5_1280, false),
106,
TestUtils.getCoeffs("laptop_1280.json", false));
frameProvider.requestFrameThresholdType(pipeline.getThresholdType());
CVPipelineResult pipelineResult;
try {
pipelineResult = pipeline.run(frameProvider.get(), QuirkyCamera.DefaultCamera);
printTestResults(pipelineResult);
} catch (RuntimeException e) {
// For now, will throw coz rotation3d ctor
return;
}
// Draw on input
var outputPipe = new OutputStreamPipeline();
outputPipe.process(
pipelineResult.inputAndOutputFrame, pipeline.getSettings(), pipelineResult.targets);
TestUtils.showImage(
pipelineResult.inputAndOutputFrame.processedImage.getMat(), "Pipeline output", 999999);
}
private static void printTestResults(CVPipelineResult pipelineResult) {
double fps = 1000 / pipelineResult.getLatencyMillis();
System.out.println(
"Pipeline ran in " + pipelineResult.getLatencyMillis() + "ms (" + fps + " " + "fps)");
System.out.println("Found " + pipelineResult.targets.size() + " valid targets");
System.out.println(
"Found targets at "
+ pipelineResult.targets.stream()
.map(TrackedTarget::getBestCameraToTarget3d)
.collect(Collectors.toList()));
}
}

View File

@@ -37,6 +37,7 @@ import org.photonvision.common.util.TestUtils;
import org.photonvision.vision.calibration.CameraCalibrationCoefficients;
import org.photonvision.vision.camera.QuirkyCamera;
import org.photonvision.vision.frame.Frame;
import org.photonvision.vision.frame.FrameDivisor;
import org.photonvision.vision.frame.FrameStaticProperties;
import org.photonvision.vision.frame.FrameThresholdType;
import org.photonvision.vision.opencv.CVMat;
@@ -62,7 +63,7 @@ public class Calibrate3dPipeTest {
FindBoardCornersPipe findBoardCornersPipe = new FindBoardCornersPipe();
findBoardCornersPipe.setParams(
new FindBoardCornersPipe.FindCornersPipeParams(
11, 4, UICalibrationData.BoardType.DOTBOARD, 15));
11, 4, UICalibrationData.BoardType.DOTBOARD, 15, FrameDivisor.NONE));
List<Triple<Size, Mat, Mat>> foundCornersList = new ArrayList<>();
@@ -264,6 +265,7 @@ public class Calibrate3dPipeTest {
calibration3dPipeline.getSettings().boardHeight = (int) Math.round(boardDim.height);
calibration3dPipeline.getSettings().boardWidth = (int) Math.round(boardDim.width);
calibration3dPipeline.getSettings().gridSize = boardGridSize_m;
calibration3dPipeline.getSettings().streamingFrameDivisor = FrameDivisor.NONE;
for (var file : directoryListing) {
if (file.isFile()) {

View File

@@ -65,7 +65,7 @@ model {
}
}
nativeUtils.useRequiredLibrary(it, "wpilib_shared")
nativeUtils.useRequiredLibrary(it, "vision_shared")
nativeUtils.useRequiredLibrary(it, "apriltag_shared")
}
}
testSuites {
@@ -80,7 +80,7 @@ model {
}
nativeUtils.useRequiredLibrary(it, "wpilib_executable_shared")
nativeUtils.useRequiredLibrary(it, "vision_shared")
nativeUtils.useRequiredLibrary(it, "apriltag_shared")
nativeUtils.useRequiredLibrary(it, "googletest_static")
}
}
@@ -123,6 +123,10 @@ task writeCurrentVersion {
build.dependsOn writeCurrentVersion
tasks.withType(Javadoc) {
options.encoding = 'UTF-8'
}
apply from: "publish.gradle"
def testNativeConfigName = 'wpilibTestNatives'

View File

@@ -1,6 +1,7 @@
apply plugin: 'maven-publish'
ext.licenseFile = files("$rootDir/LICENSE.txt")
ext.licenseFile = files("$rootDir/LICENSE")
ext.photonVersionFile = files("$projectDir/src/generate/native/include")
def outputsFolder = file("$buildDir/outputs")
def allOutputsFolder = file("$buildDir/allOutputs")
@@ -54,15 +55,19 @@ task cppHeadersZip(type: Zip) {
into '/'
}
from(photonVersionFile) {
into '/'
}
from('src/main/native/include/') {
into '/'
}
}
task cppSourceZip(type: Zip) {
task cppSourcesZip(type: Zip) {
destinationDirectory = outputsFolder
archiveBaseName = zipBaseName
classifier = "source"
classifier = "sources"
from(licenseFile) {
into '/'
@@ -75,8 +80,8 @@ task cppSourceZip(type: Zip) {
build.dependsOn cppHeadersZip
addTaskToCopyAllOutputs(cppHeadersZip)
build.dependsOn cppSourceZip
addTaskToCopyAllOutputs(cppSourceZip)
build.dependsOn cppSourcesZip
addTaskToCopyAllOutputs(cppSourcesZip)
task sourcesJar(type: Jar, dependsOn: classes) {
classifier = 'sources'
@@ -162,7 +167,7 @@ model {
artifact it
}
artifact cppHeadersZip
artifact cppSourceZip
artifact cppSourcesZip
artifactId = "${baseArtifactId}-cpp"
groupId artifactGroupId

View File

@@ -22,7 +22,7 @@
"windowsx86-64",
"linuxathena",
"linuxx86-64",
"osxx86-64"
"osxuniversal"
]
}
],

View File

@@ -0,0 +1,47 @@
/*
* MIT License
*
* Copyright (c) 2022 PhotonVision
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package org.photonvision;
import edu.wpi.first.math.geometry.Pose3d;
/** An estimated pose based on pipeline result */
public class EstimatedRobotPose {
/** The estimated pose */
public final Pose3d estimatedPose;
/** The estimated time the frame used to derive the robot pose was taken */
public final double timestampSeconds;
/**
* Constructs an EstimatedRobotPose
*
* @param estimatedPose estimated pose
* @param timestampSeconds timestamp of the estimate
*/
public EstimatedRobotPose(Pose3d estimatedPose, double timestampSeconds) {
this.estimatedPose = estimatedPose;
this.timestampSeconds = timestampSeconds;
}
}

View File

@@ -24,28 +24,31 @@
package org.photonvision;
import edu.wpi.first.networktables.BooleanEntry;
import edu.wpi.first.networktables.BooleanPublisher;
import edu.wpi.first.networktables.BooleanSubscriber;
import edu.wpi.first.networktables.DoubleArrayPublisher;
import edu.wpi.first.networktables.DoublePublisher;
import edu.wpi.first.networktables.IntegerEntry;
import edu.wpi.first.networktables.IntegerSubscriber;
import edu.wpi.first.networktables.MultiSubscriber;
import edu.wpi.first.networktables.NetworkTable;
import edu.wpi.first.networktables.NetworkTableInstance;
import edu.wpi.first.networktables.PubSubOption;
import edu.wpi.first.networktables.RawSubscriber;
import edu.wpi.first.networktables.StringSubscriber;
import edu.wpi.first.wpilibj.DriverStation;
import edu.wpi.first.wpilibj.Timer;
import java.util.Set;
import org.photonvision.common.dataflow.structures.Packet;
import org.photonvision.common.hardware.VisionLEDMode;
import org.photonvision.targeting.PhotonPipelineResult;
/** Represents a camera that is connected to PhotonVision. */
public class PhotonCamera {
static final String kTableName = "photonvision";
protected final NetworkTable rootTable;
RawSubscriber rawBytesEntry;
BooleanEntry driverModeEntry;
BooleanPublisher driverModePublisher;
BooleanSubscriber driverModeSubscriber;
DoublePublisher latencyMillisEntry;
@@ -62,7 +65,6 @@ public class PhotonCamera {
public void close() {
rawBytesEntry.close();
driverModeEntry.close();
driverModePublisher.close();
driverModeSubscriber.close();
latencyMillisEntry.close();
@@ -96,6 +98,8 @@ public class PhotonCamera {
Packet packet = new Packet(1);
private final MultiSubscriber m_topicNameSubscriber;
/**
* Constructs a PhotonCamera from a root table.
*
@@ -106,17 +110,28 @@ public class PhotonCamera {
*/
public PhotonCamera(NetworkTableInstance instance, String cameraName) {
name = cameraName;
var mainTable = instance.getTable("photonvision");
var mainTable = instance.getTable(kTableName);
this.rootTable = mainTable.getSubTable(cameraName);
path = rootTable.getPath();
rawBytesEntry = rootTable.getRawTopic("rawBytes").subscribe("rawBytes", new byte[] {});
driverModeEntry = rootTable.getBooleanTopic("driverMode").getEntry(false);
rawBytesEntry =
rootTable
.getRawTopic("rawBytes")
.subscribe(
"rawBytes", new byte[] {}, PubSubOption.periodic(0.01), PubSubOption.sendAll(true));
driverModePublisher = rootTable.getBooleanTopic("driverMode").publish();
driverModeSubscriber = rootTable.getBooleanTopic("driverModeRequest").subscribe(false);
inputSaveImgEntry = rootTable.getIntegerTopic("inputSaveImgCmd").getEntry(0);
outputSaveImgEntry = rootTable.getIntegerTopic("outputSaveImgCmd").getEntry(0);
pipelineIndexEntry = rootTable.getIntegerTopic("pipelineIndex").getEntry(0);
heartbeatEntry = rootTable.getIntegerTopic("heartbeat").subscribe(-1);
ledModeEntry = mainTable.getIntegerTopic("ledMode").getEntry(-1);
versionEntry = mainTable.getStringTopic("version").subscribe("");
m_topicNameSubscriber =
new MultiSubscriber(
instance,
new String[] {"/photonvision/"},
new PubSubOption[] {PubSubOption.topicsOnly(true)});
}
/**
@@ -162,7 +177,7 @@ public class PhotonCamera {
* @return Whether the camera is in driver mode.
*/
public boolean getDriverMode() {
return driverModeEntry.get(false);
return driverModeSubscriber.get();
}
/**
@@ -171,7 +186,7 @@ public class PhotonCamera {
* @param driverMode Whether to set driver mode.
*/
public void setDriverMode(boolean driverMode) {
driverModeEntry.set(driverMode);
driverModePublisher.set(driverMode);
}
/**
@@ -280,7 +295,7 @@ public class PhotonCamera {
prevHeartbeatValue = curHeartbeat;
}
return ((now - prevHeartbeatChangeTime) < HEARBEAT_DEBOUNCE_SEC);
return (now - prevHeartbeatChangeTime) < HEARBEAT_DEBOUNCE_SEC;
}
private void verifyVersion() {
@@ -292,12 +307,25 @@ public class PhotonCamera {
// Heartbeat entry is assumed to always be present. If it's not present, we
// assume that a camera with that name was never connected in the first place.
if (!heartbeatEntry.exists()) {
DriverStation.reportError(
"PhotonVision coprocessor at path " + path + " not found on NetworkTables!", true);
Set<String> cameraNames = rootTable.getInstance().getTable(kTableName).getSubTables();
if (cameraNames.isEmpty()) {
DriverStation.reportError(
"Could not find any PhotonVision coprocessors on NetworkTables. Double check that PhotonVision is running, and that your camera is connected!",
false);
} else {
DriverStation.reportError(
"PhotonVision coprocessor at path "
+ path
+ " not found on NetworkTables. Double check that your camera names match!",
true);
DriverStation.reportError(
"Found the following PhotonVision cameras on NetworkTables:\n"
+ String.join("\n", cameraNames),
false);
}
}
// Check for connection status. Warn if disconnected.
if (!isConnected()) {
else if (!isConnected()) {
DriverStation.reportWarning(
"PhotonVision coprocessor at path " + path + " is not sending new data.", true);
}

View File

@@ -0,0 +1,496 @@
/*
* MIT License
*
* Copyright (c) 2022 PhotonVision
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package org.photonvision;
import edu.wpi.first.apriltag.AprilTagFieldLayout;
import edu.wpi.first.math.Pair;
import edu.wpi.first.math.geometry.Pose2d;
import edu.wpi.first.math.geometry.Pose3d;
import edu.wpi.first.math.geometry.Rotation3d;
import edu.wpi.first.math.geometry.Transform3d;
import edu.wpi.first.math.geometry.Translation3d;
import edu.wpi.first.wpilibj.DriverStation;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import org.photonvision.targeting.PhotonPipelineResult;
import org.photonvision.targeting.PhotonTrackedTarget;
/**
* The PhotonPoseEstimator class filters or combines readings from all the AprilTags visible at a
* given timestamp on the field to produce a single robot in field pose, using the strategy set
* below. Example usage can be found in our apriltagExample example project.
*/
public class PhotonPoseEstimator {
/** Position estimation strategies that can be used by the {@link PhotonPoseEstimator} class. */
public enum PoseStrategy {
/** Choose the Pose with the lowest ambiguity. */
LOWEST_AMBIGUITY,
/** Choose the Pose which is closest to the camera height. */
CLOSEST_TO_CAMERA_HEIGHT,
/** Choose the Pose which is closest to a set Reference position. */
CLOSEST_TO_REFERENCE_POSE,
/** Choose the Pose which is closest to the last pose calculated */
CLOSEST_TO_LAST_POSE,
/** Choose the Pose with the lowest ambiguity. */
AVERAGE_BEST_TARGETS
}
private AprilTagFieldLayout fieldTags;
private PoseStrategy strategy;
private final PhotonCamera camera;
private final Transform3d robotToCamera;
private Pose3d lastPose;
private Pose3d referencePose;
private final Set<Integer> reportedErrors = new HashSet<>();
/**
* Create a new PhotonPoseEstimator.
*
* @param fieldTags A WPILib {@link AprilTagFieldLayout} linking AprilTag IDs to Pose3d objects
* with respect to the FIRST field using the <a
* href="https://docs.wpilib.org/en/stable/docs/software/advanced-controls/geometry/coordinate-systems.html#field-coordinate-system">Field
* Coordinate System</a>.
* @param strategy The strategy it should use to determine the best pose.
* @param camera PhotonCameras and
* @param robotToCamera Transform3d from the center of the robot to the camera mount positions
* (ie, robot ➔ camera) in the <a
* href="https://docs.wpilib.org/en/stable/docs/software/advanced-controls/geometry/coordinate-systems.html#robot-coordinate-system">Robot
* Coordinate System</a>.
*/
public PhotonPoseEstimator(
AprilTagFieldLayout fieldTags,
PoseStrategy strategy,
PhotonCamera camera,
Transform3d robotToCamera) {
this.fieldTags = fieldTags;
this.strategy = strategy;
this.camera = camera;
this.robotToCamera = robotToCamera;
}
/**
* Get the AprilTagFieldLayout being used by the PositionEstimator.
*
* @return the AprilTagFieldLayout
*/
public AprilTagFieldLayout getFieldTags() {
return fieldTags;
}
/**
* Set the AprilTagFieldLayout being used by the PositionEstimator.
*
* @param fieldTags the AprilTagFieldLayout
*/
public void setFieldTags(AprilTagFieldLayout fieldTags) {
this.fieldTags = fieldTags;
}
/**
* Get the Position Estimation Strategy being used by the Position Estimator.
*
* @return the strategy
*/
public PoseStrategy getStrategy() {
return strategy;
}
/**
* Set the Position Estimation Strategy used by the Position Estimator.
*
* @param strategy the strategy to set
*/
public void setStrategy(PoseStrategy strategy) {
this.strategy = strategy;
}
/**
* Return the reference position that is being used by the estimator.
*
* @return the referencePose
*/
public Pose3d getReferencePose() {
return referencePose;
}
/**
* Update the stored reference pose for use when using the <b>CLOSEST_TO_REFERENCE_POSE</b>
* strategy.
*
* @param referencePose the referencePose to set
*/
public void setReferencePose(Pose3d referencePose) {
this.referencePose = referencePose;
}
/**
* Update the stored reference pose for use when using the <b>CLOSEST_TO_REFERENCE_POSE</b>
* strategy.
*
* @param referencePose the referencePose to set
*/
public void setReferencePose(Pose2d referencePose) {
this.referencePose = new Pose3d(referencePose);
}
/**
* Update the stored last pose. Useful for setting the initial estimate when using the
* <b>CLOSEST_TO_LAST_POSE</b> strategy.
*
* @param lastPose the lastPose to set
*/
public void setLastPose(Pose3d lastPose) {
this.lastPose = lastPose;
}
/**
* Update the stored last pose. Useful for setting the initial estimate when using the
* <b>CLOSEST_TO_LAST_POSE</b> strategy.
*
* @param lastPose the lastPose to set
*/
public void setLastPose(Pose2d lastPose) {
setLastPose(new Pose3d(lastPose));
}
/**
* Poll data from the configured cameras and update the estimated position of the robot. Returns
* empty if there are no cameras set or no targets were found from the cameras.
*
* @return an EstimatedRobotPose with an estimated pose, and information about the camera(s) and
* pipeline results used to create the estimate
*/
public Optional<EstimatedRobotPose> update() {
if (camera == null) {
DriverStation.reportError("[PhotonPoseEstimator] Missing camera!", false);
return Optional.empty();
}
PhotonPipelineResult cameraResult = camera.getLatestResult();
if (!cameraResult.hasTargets()) {
return Optional.empty();
}
Optional<EstimatedRobotPose> estimatedPose;
switch (strategy) {
case LOWEST_AMBIGUITY:
estimatedPose = lowestAmbiguityStrategy(cameraResult);
break;
case CLOSEST_TO_CAMERA_HEIGHT:
estimatedPose = closestToCameraHeightStrategy(cameraResult);
break;
case CLOSEST_TO_LAST_POSE:
setReferencePose(lastPose);
case CLOSEST_TO_REFERENCE_POSE:
estimatedPose = closestToReferencePoseStrategy(cameraResult, referencePose);
break;
case AVERAGE_BEST_TARGETS:
estimatedPose = averageBestTargetsStrategy(cameraResult);
break;
default:
DriverStation.reportError(
"[PhotonPoseEstimator] Unknown Position Estimation Strategy!", false);
return Optional.empty();
}
if (estimatedPose.isEmpty()) {
lastPose = null;
}
return estimatedPose;
}
/**
* Return the estimated position of the robot with the lowest position ambiguity from a List of
* pipeline results.
*
* @param result pipeline result
* @return the estimated position of the robot in the FCS and the estimated timestamp of this
* estimation.
*/
private Optional<EstimatedRobotPose> lowestAmbiguityStrategy(PhotonPipelineResult result) {
PhotonTrackedTarget lowestAmbiguityTarget = null;
double lowestAmbiguityScore = 10;
for (PhotonTrackedTarget target : result.targets) {
double targetPoseAmbiguity = target.getPoseAmbiguity();
// Make sure the target is a Fiducial target.
if (targetPoseAmbiguity != -1 && targetPoseAmbiguity < lowestAmbiguityScore) {
lowestAmbiguityScore = targetPoseAmbiguity;
lowestAmbiguityTarget = target;
}
}
// Although there are confirmed to be targets, none of them may be fiducial
// targets.
if (lowestAmbiguityTarget == null) return Optional.empty();
int targetFiducialId = lowestAmbiguityTarget.getFiducialId();
Optional<Pose3d> targetPosition = fieldTags.getTagPose(targetFiducialId);
if (targetPosition.isEmpty()) {
reportFiducialPoseError(targetFiducialId);
return Optional.empty();
}
return Optional.of(
new EstimatedRobotPose(
targetPosition
.get()
.transformBy(lowestAmbiguityTarget.getBestCameraToTarget().inverse())
.transformBy(robotToCamera.inverse()),
result.getTimestampSeconds()));
}
/**
* Return the estimated position of the robot using the target with the lowest delta height
* difference between the estimated and actual height of the camera.
*
* @param result pipeline result
* @return the estimated position of the robot in the FCS and the estimated timestamp of this
* estimation.
*/
private Optional<EstimatedRobotPose> closestToCameraHeightStrategy(PhotonPipelineResult result) {
double smallestHeightDifference = 10e9;
EstimatedRobotPose closestHeightTarget = null;
for (PhotonTrackedTarget target : result.targets) {
int targetFiducialId = target.getFiducialId();
// Don't report errors for non-fiducial targets. This could also be resolved by
// adding -1 to
// the initial HashSet.
if (targetFiducialId == -1) continue;
Optional<Pose3d> targetPosition = fieldTags.getTagPose(target.getFiducialId());
if (targetPosition.isEmpty()) {
reportFiducialPoseError(target.getFiducialId());
continue;
}
double alternateTransformDelta =
Math.abs(
robotToCamera.getZ()
- targetPosition
.get()
.transformBy(target.getAlternateCameraToTarget().inverse())
.getZ());
double bestTransformDelta =
Math.abs(
robotToCamera.getZ()
- targetPosition
.get()
.transformBy(target.getBestCameraToTarget().inverse())
.getZ());
if (alternateTransformDelta < smallestHeightDifference) {
smallestHeightDifference = alternateTransformDelta;
closestHeightTarget =
new EstimatedRobotPose(
targetPosition
.get()
.transformBy(target.getAlternateCameraToTarget().inverse())
.transformBy(robotToCamera.inverse()),
result.getTimestampSeconds());
}
if (bestTransformDelta < smallestHeightDifference) {
smallestHeightDifference = bestTransformDelta;
closestHeightTarget =
new EstimatedRobotPose(
targetPosition
.get()
.transformBy(target.getBestCameraToTarget().inverse())
.transformBy(robotToCamera.inverse()),
result.getTimestampSeconds());
}
}
// Need to null check here in case none of the provided targets are fiducial.
return Optional.ofNullable(closestHeightTarget);
}
/**
* Return the estimated position of the robot using the target with the lowest delta in the vector
* magnitude between it and the reference pose.
*
* @param result pipeline result
* @param referencePose reference pose to check vector magnitude difference against.
* @return the estimated position of the robot in the FCS and the estimated timestamp of this
* estimation.
*/
private Optional<EstimatedRobotPose> closestToReferencePoseStrategy(
PhotonPipelineResult result, Pose3d referencePose) {
if (referencePose == null) {
DriverStation.reportError(
"[PhotonPoseEstimator] Tried to use reference pose strategy without setting the reference!",
false);
return Optional.empty();
}
double smallestPoseDelta = 10e9;
EstimatedRobotPose lowestDeltaPose = null;
for (PhotonTrackedTarget target : result.targets) {
int targetFiducialId = target.getFiducialId();
// Don't report errors for non-fiducial targets. This could also be resolved by
// adding -1 to
// the initial HashSet.
if (targetFiducialId == -1) continue;
Optional<Pose3d> targetPosition = fieldTags.getTagPose(target.getFiducialId());
if (targetPosition.isEmpty()) {
reportFiducialPoseError(targetFiducialId);
continue;
}
Pose3d altTransformPosition =
targetPosition
.get()
.transformBy(target.getAlternateCameraToTarget().inverse())
.transformBy(robotToCamera.inverse());
Pose3d bestTransformPosition =
targetPosition
.get()
.transformBy(target.getBestCameraToTarget().inverse())
.transformBy(robotToCamera.inverse());
double altDifference = Math.abs(calculateDifference(referencePose, altTransformPosition));
double bestDifference = Math.abs(calculateDifference(referencePose, bestTransformPosition));
if (altDifference < smallestPoseDelta) {
smallestPoseDelta = altDifference;
lowestDeltaPose =
new EstimatedRobotPose(altTransformPosition, result.getTimestampSeconds());
}
if (bestDifference < smallestPoseDelta) {
smallestPoseDelta = bestDifference;
lowestDeltaPose =
new EstimatedRobotPose(bestTransformPosition, result.getTimestampSeconds());
}
}
return Optional.ofNullable(lowestDeltaPose);
}
/**
* Return the average of the best target poses using ambiguity as weight.
*
* @param result pipeline result
* @return the estimated position of the robot in the FCS and the estimated timestamp of this
* estimation.
*/
private Optional<EstimatedRobotPose> averageBestTargetsStrategy(PhotonPipelineResult result) {
List<Pair<PhotonTrackedTarget, Pose3d>> estimatedRobotPoses = new ArrayList<>();
double totalAmbiguity = 0;
for (PhotonTrackedTarget target : result.targets) {
int targetFiducialId = target.getFiducialId();
// Don't report errors for non-fiducial targets. This could also be resolved by
// adding -1 to
// the initial HashSet.
if (targetFiducialId == -1) continue;
Optional<Pose3d> targetPosition = fieldTags.getTagPose(target.getFiducialId());
if (targetPosition.isEmpty()) {
reportFiducialPoseError(targetFiducialId);
continue;
}
double targetPoseAmbiguity = target.getPoseAmbiguity();
// Pose ambiguity is 0, use that pose
if (targetPoseAmbiguity == 0) {
return Optional.of(
new EstimatedRobotPose(
targetPosition
.get()
.transformBy(target.getBestCameraToTarget().inverse())
.transformBy(robotToCamera.inverse()),
result.getTimestampSeconds()));
}
totalAmbiguity += 1.0 / target.getPoseAmbiguity();
estimatedRobotPoses.add(
new Pair<>(
target,
targetPosition
.get()
.transformBy(target.getBestCameraToTarget().inverse())
.transformBy(robotToCamera.inverse())));
}
// Take the average
Translation3d transform = new Translation3d();
Rotation3d rotation = new Rotation3d();
if (estimatedRobotPoses.isEmpty()) return Optional.empty();
for (Pair<PhotonTrackedTarget, Pose3d> pair : estimatedRobotPoses) {
// Total ambiguity is non-zero confirmed because if it was zero, that pose was
// returned.
double weight = (1.0 / pair.getFirst().getPoseAmbiguity()) / totalAmbiguity;
Pose3d estimatedPose = pair.getSecond();
transform = transform.plus(estimatedPose.getTranslation().times(weight));
rotation = rotation.plus(estimatedPose.getRotation().times(weight));
}
return Optional.of(
new EstimatedRobotPose(new Pose3d(transform, rotation), result.getTimestampSeconds()));
}
/**
* Difference is defined as the vector magnitude between the two poses
*
* @return The absolute "difference" (>=0) between two Pose3ds.
*/
private double calculateDifference(Pose3d x, Pose3d y) {
return x.getTranslation().getDistance(y.getTranslation());
}
private void reportFiducialPoseError(int fiducialId) {
if (!reportedErrors.contains(fiducialId)) {
DriverStation.reportError(
"[PhotonPoseEstimator] Tried to get pose of unknown AprilTag: " + fiducialId, false);
reportedErrors.add(fiducialId);
}
}
}

View File

@@ -40,6 +40,8 @@ import java.util.Set;
import org.photonvision.targeting.PhotonPipelineResult;
import org.photonvision.targeting.PhotonTrackedTarget;
/** @deprecated Use {@link PhotonPoseEstimator} */
@Deprecated
public class RobotPoseEstimator {
/**
*
@@ -73,14 +75,11 @@ public class RobotPoseEstimator {
/**
* Create a new RobotPoseEstimator.
*
* <p>Example: {@code <code> <p> Map<Integer, Pose3d> map = new HashMap<>(); <p> map.put(1, new
* Pose3d(1.0, 2.0, 3.0, new Rotation3d())); // Tag ID 1 is at (1.0,2.0,3.0) </code> }
*
* @param aprilTags A AprilTagFieldLayout linking AprilTag IDs to Pose3ds with respect to the
* FIRST field.
* @param aprilTags A WPILib {@link AprilTagFieldLayout} linking AprilTag IDs to Pose3ds with
* respect to the FIRST field.
* @param strategy The strategy it should use to determine the best pose.
* @param cameras An ArrayList of Pairs of PhotonCameras and their respective Transform3ds from
* the center of the robot to the cameras.
* the center of the robot to the camera mount positions (ie, robot ➔ camera).
*/
public RobotPoseEstimator(
AprilTagFieldLayout aprilTags,
@@ -96,7 +95,7 @@ public class RobotPoseEstimator {
/**
* Update the estimated pose using the selected strategy.
*
* @return The updated estimated pose and the latency in milliseconds Estimated pose may be null
* @return The updated estimated pose and the latency in milliseconds. Estimated pose may be null
* if no targets were seen
*/
public Optional<Pair<Pose3d, Double>> update() {
@@ -390,7 +389,7 @@ public class RobotPoseEstimator {
}
/**
* UPdate the stored last pose. Useful for setting the initial estimate with CLOSEST_TO_LAST_POSE
* Update the stored last pose. Useful for setting the initial estimate with CLOSEST_TO_LAST_POSE
*
* @param lastPose the lastPose to set
*/
@@ -401,7 +400,7 @@ public class RobotPoseEstimator {
private void reportFiducialPoseError(int fiducialId) {
if (!reportedErrors.contains(fiducialId)) {
DriverStation.reportError(
"[RobotPoseEstimator] Tried to get pose of unknown April Tag: " + fiducialId, false);
"[RobotPoseEstimator] Tried to get pose of unknown AprilTag: " + fiducialId, false);
reportedErrors.add(fiducialId);
}
}

View File

@@ -36,6 +36,8 @@ import org.photonvision.targeting.PhotonTrackedTarget;
public class SimPhotonCamera {
NTTopicSet ts = new NTTopicSet();
PhotonPipelineResult latestResult;
private long heartbeatCounter = 0;
/**
* Constructs a Simulated PhotonCamera from a root table.
*
@@ -46,7 +48,7 @@ public class SimPhotonCamera {
*/
public SimPhotonCamera(NetworkTableInstance instance, String cameraName) {
ts.removeEntries();
ts.subTable = instance.getTable("/photonvision").getSubTable(cameraName);
ts.subTable = instance.getTable(PhotonCamera.kTableName).getSubTable(cameraName);
ts.updateEntries();
}
@@ -134,6 +136,8 @@ public class SimPhotonCamera {
ts.targetPoseEntry.set(poseData);
}
ts.heartbeatPublisher.set(heartbeatCounter++);
latestResult = newResult;
}

View File

@@ -24,6 +24,8 @@
package org.photonvision;
import edu.wpi.first.apriltag.AprilTag;
import edu.wpi.first.apriltag.AprilTagFieldLayout;
import edu.wpi.first.math.geometry.Pose2d;
import edu.wpi.first.math.geometry.Pose3d;
import edu.wpi.first.math.geometry.Rotation2d;
@@ -49,7 +51,7 @@ public class SimVisionSystem {
int cameraResWidth;
int cameraResHeight;
double minTargetArea;
Transform3d cameraToRobot;
Transform3d robotToCamera;
Field2d dbgField;
FieldObject2d dbgRobot;
@@ -67,7 +69,8 @@ public class SimVisionSystem {
* @param camDiagFOVDegrees Diagonal Field of View of the camera used. Align it with the
* manufacturer specifications, and/or whatever is configured in the PhotonVision Setting
* page.
* @param cameraToRobot Transform to move from the camera's mount position to the robot's position
* @param robotToCamera Transform to move from the center of the robot to the camera's mount
* position
* @param maxLEDRangeMeters Maximum distance at which your camera can illuminate the target and
* make it visible. Set to 9000 or more if your vision system does not rely on LED's.
* @param cameraResWidth Width of your camera's image sensor in pixels
@@ -79,12 +82,12 @@ public class SimVisionSystem {
public SimVisionSystem(
String camName,
double camDiagFOVDegrees,
Transform3d cameraToRobot,
Transform3d robotToCamera,
double maxLEDRangeMeters,
int cameraResWidth,
int cameraResHeight,
double minTargetArea) {
this.cameraToRobot = cameraToRobot;
this.robotToCamera = robotToCamera;
this.maxLEDRangeMeters = maxLEDRangeMeters;
this.cameraResWidth = cameraResWidth;
this.cameraResHeight = cameraResHeight;
@@ -116,17 +119,42 @@ public class SimVisionSystem {
dbgField
.getObject("Target " + Integer.toString(target.targetID))
.setPose(target.targetPose.toPose2d());
;
}
/**
* Adds all apriltags from the provided {@link AprilTagFieldLayout} as sim vision targets. The
* poses added will preserve the tag layout's alliance origin at the time of calling this method.
*
* @param tagLayout The field tag layout to get Apriltag poses and IDs from
*/
public void addVisionTargets(AprilTagFieldLayout tagLayout) {
for (AprilTag tag : tagLayout.getTags()) {
addSimVisionTarget(
new SimVisionTarget(
tagLayout.getTagPose(tag.ID).get(), // preserve alliance rotation
Units.inchesToMeters(6),
Units.inchesToMeters(6),
tag.ID));
}
}
/**
* Clears all sim vision targets. This is useful for switching alliances and needing to repopulate
* the sim targets. NOTE: Old targets will still show on the Field2d unless overwritten by new
* targets with the same ID
*/
public void clearVisionTargets() {
tgtList.clear();
}
/**
* Adjust the camera position relative to the robot. Use this if your camera is on a gimbal or
* turret or some other mobile platform.
*
* @param newCameraToRobot New Transform from the robot to the camera
* @param newRobotToCamera New Transform from the robot to the camera
*/
public void moveCamera(Transform3d newCameraToRobot) {
this.cameraToRobot = newCameraToRobot;
public void moveCamera(Transform3d newRobotToCamera) {
this.robotToCamera = newRobotToCamera;
}
/**
@@ -150,7 +178,7 @@ public class SimVisionSystem {
* PhotonVision parameters.
*/
public void processFrame(Pose3d robotPoseMeters) {
Pose3d cameraPose = robotPoseMeters.transformBy(cameraToRobot.inverse());
Pose3d cameraPose = robotPoseMeters.transformBy(robotToCamera);
dbgRobot.setPose(robotPoseMeters.toPose2d());
dbgCamera.setPose(cameraPose.toPose2d());
@@ -211,6 +239,9 @@ public class SimVisionSystem {
camToTargetTrans,
camToTargetTransAlt,
0.0, // TODO - simulate ambiguity when straight on?
List.of(
new TargetCorner(0, 0), new TargetCorner(0, 0),
new TargetCorner(0, 0), new TargetCorner(0, 0)),
List.of(
new TargetCorner(0, 0), new TargetCorner(0, 0),
new TargetCorner(0, 0), new TargetCorner(0, 0))));

View File

@@ -33,13 +33,15 @@
namespace photonlib {
constexpr const units::second_t VERSION_CHECK_INTERVAL = 5_s;
static const std::vector<std::string_view> PHOTON_PREFIX = {"/photonvision/"};
PhotonCamera::PhotonCamera(std::shared_ptr<nt::NetworkTableInstance> instance,
PhotonCamera::PhotonCamera(nt::NetworkTableInstance instance,
const std::string_view cameraName)
: mainTable(instance->GetTable("photonvision")),
: mainTable(instance.GetTable("photonvision")),
rootTable(mainTable->GetSubTable(cameraName)),
rawBytesEntry(rootTable->GetRawTopic("rawBytes").Subscribe("raw", {})),
driverModeEntry(rootTable->GetBooleanTopic("driverMode").Publish()),
rawBytesEntry(
rootTable->GetRawTopic("rawBytes")
.Subscribe("rawBytes", {}, {.periodic = 0.01, .sendAll = true})),
inputSaveImgEntry(
rootTable->GetIntegerTopic("inputSaveImgCmd").Publish()),
inputSaveImgSubscriber(
@@ -53,16 +55,17 @@ PhotonCamera::PhotonCamera(std::shared_ptr<nt::NetworkTableInstance> instance,
versionEntry(mainTable->GetStringTopic("version").Subscribe("")),
driverModeSubscriber(
rootTable->GetBooleanTopic("driverMode").Subscribe(false)),
driverModePublisher(
rootTable->GetBooleanTopic("driverModeRequest").Publish()),
pipelineIndexSubscriber(
rootTable->GetIntegerTopic("pipelineIndex").Subscribe(-1)),
ledModeSubscriber(mainTable->GetIntegerTopic("ledMode").Subscribe(0)),
m_topicNameSubscriber(instance, PHOTON_PREFIX, {.topicsOnly = true}),
path(rootTable->GetPath()),
m_cameraName(cameraName) {}
PhotonCamera::PhotonCamera(const std::string_view cameraName)
: PhotonCamera(std::make_shared<nt::NetworkTableInstance>(
nt::NetworkTableInstance::GetDefault()),
cameraName) {}
: PhotonCamera(nt::NetworkTableInstance::GetDefault(), cameraName) {}
PhotonPipelineResult PhotonCamera::GetLatestResult() {
if (test) return testResult;
@@ -90,7 +93,7 @@ PhotonPipelineResult PhotonCamera::GetLatestResult() {
}
void PhotonCamera::SetDriverMode(bool driverMode) {
driverModeEntry.Set(driverMode);
driverModePublisher.Set(driverMode);
}
void PhotonCamera::TakeInputSnapshot() {
@@ -134,10 +137,29 @@ void PhotonCamera::VerifyVersion() {
const std::string& versionString = versionEntry.Get("");
if (versionString.empty()) {
std::string path_ = path;
FRC_ReportError(
frc::warn::Warning,
"PhotonVision coprocessor at path {} not found on NetworkTables!",
path_);
std::vector<std::string> cameraNames =
rootTable->GetInstance().GetTable("photonvision")->GetSubTables();
if (cameraNames.empty()) {
FRC_ReportError(frc::warn::Warning,
"Could not find any PhotonVision coprocessors on "
"NetworkTables. Double check that PhotonVision is "
"running, and that your camera is connected!");
} else {
FRC_ReportError(
frc::warn::Warning,
"PhotonVision coprocessor at path {} not found on NetworkTables. "
"Double check that your camera names match!",
path_);
std::string cameraNameOutString;
for (unsigned int i = 0; i < cameraNames.size(); i++) {
cameraNameOutString += "\n" + cameraNames[i];
}
FRC_ReportError(
frc::warn::Warning,
"Found the following PhotonVision cameras on NetworkTables:{}",
cameraNameOutString);
}
} else if (!VersionMatches(versionString)) {
FRC_ReportError(frc::warn::Warning,
"Photon version {} does not match coprocessor version {}!",

View File

@@ -0,0 +1,270 @@
/*
* MIT License
*
* Copyright (c) 2022 PhotonVision
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
#include "photonlib/PhotonPoseEstimator.h"
#include <iostream>
#include <limits>
#include <map>
#include <span>
#include <string>
#include <utility>
#include <vector>
#include <frc/Errors.h>
#include <frc/geometry/Pose3d.h>
#include <frc/geometry/Rotation3d.h>
#include <frc/geometry/Transform3d.h>
#include <units/time.h>
#include "photonlib/PhotonCamera.h"
#include "photonlib/PhotonPipelineResult.h"
#include "photonlib/PhotonTrackedTarget.h"
namespace photonlib {
PhotonPoseEstimator::PhotonPoseEstimator(frc::AprilTagFieldLayout tags,
PoseStrategy strat, PhotonCamera&& cam,
frc::Transform3d robotToCamera)
: aprilTags(tags),
strategy(strat),
camera(std::move(cam)),
m_robotToCamera(robotToCamera),
lastPose(frc::Pose3d()),
referencePose(frc::Pose3d()) {}
std::optional<EstimatedRobotPose> PhotonPoseEstimator::Update() {
auto result = camera.GetLatestResult();
if (!result.HasTargets()) {
return std::nullopt;
}
std::optional<EstimatedRobotPose> ret = std::nullopt;
switch (strategy) {
case LOWEST_AMBIGUITY:
ret = LowestAmbiguityStrategy(result);
break;
case CLOSEST_TO_CAMERA_HEIGHT:
ret = ClosestToCameraHeightStrategy(result);
break;
case CLOSEST_TO_REFERENCE_POSE:
ret = ClosestToReferencePoseStrategy(result);
break;
case CLOSEST_TO_LAST_POSE:
SetReferencePose(lastPose);
ret = ClosestToReferencePoseStrategy(result);
break;
case AVERAGE_BEST_TARGETS:
ret = AverageBestTargetsStrategy(result);
break;
default:
FRC_ReportError(frc::warn::Warning, "Invalid Pose Strategy selected!",
"");
return std::nullopt;
}
if (!ret) {
// TODO
}
return ret;
}
std::optional<EstimatedRobotPose> PhotonPoseEstimator::LowestAmbiguityStrategy(
PhotonPipelineResult result) {
int lowestAJ = -1;
double lowestAmbiguityScore = std::numeric_limits<double>::infinity();
auto targets = result.GetTargets();
for (PhotonPoseEstimator::size_type j = 0; j < targets.size(); ++j) {
if (targets[j].GetPoseAmbiguity() < lowestAmbiguityScore) {
lowestAJ = j;
lowestAmbiguityScore = targets[j].GetPoseAmbiguity();
}
}
if (lowestAJ == -1) {
return std::nullopt;
}
PhotonTrackedTarget bestTarget = targets[lowestAJ];
std::optional<frc::Pose3d> fiducialPose =
aprilTags.GetTagPose(bestTarget.GetFiducialId());
if (!fiducialPose) {
FRC_ReportError(frc::warn::Warning,
"Tried to get pose of unknown April Tag: {}",
bestTarget.GetFiducialId());
return std::nullopt;
}
return EstimatedRobotPose{
fiducialPose.value()
.TransformBy(bestTarget.GetBestCameraToTarget().Inverse())
.TransformBy(m_robotToCamera.Inverse()),
result.GetTimestamp()};
}
std::optional<EstimatedRobotPose>
PhotonPoseEstimator::ClosestToCameraHeightStrategy(
PhotonPipelineResult result) {
units::meter_t smallestHeightDifference =
units::meter_t(std::numeric_limits<double>::infinity());
std::optional<EstimatedRobotPose> pose = std::nullopt;
for (auto& target : result.GetTargets()) {
std::optional<frc::Pose3d> fiducialPose =
aprilTags.GetTagPose(target.GetFiducialId());
if (!fiducialPose) {
FRC_ReportError(frc::warn::Warning,
"Tried to get pose of unknown April Tag: {}",
target.GetFiducialId());
continue;
}
frc::Pose3d targetPose = fiducialPose.value();
units::meter_t alternativeDifference = units::math::abs(
m_robotToCamera.Z() -
targetPose.TransformBy(target.GetAlternateCameraToTarget().Inverse())
.Z());
units::meter_t bestDifference = units::math::abs(
m_robotToCamera.Z() -
targetPose.TransformBy(target.GetBestCameraToTarget().Inverse()).Z());
if (alternativeDifference < smallestHeightDifference) {
smallestHeightDifference = alternativeDifference;
pose = EstimatedRobotPose{
targetPose.TransformBy(target.GetAlternateCameraToTarget().Inverse())
.TransformBy(m_robotToCamera.Inverse()),
result.GetTimestamp()};
}
if (bestDifference < smallestHeightDifference) {
smallestHeightDifference = bestDifference;
pose = EstimatedRobotPose{
targetPose.TransformBy(target.GetBestCameraToTarget().Inverse())
.TransformBy(m_robotToCamera.Inverse()),
result.GetTimestamp()};
}
}
return pose;
}
std::optional<EstimatedRobotPose>
PhotonPoseEstimator::ClosestToReferencePoseStrategy(
PhotonPipelineResult result) {
units::meter_t smallestDifference =
units::meter_t(std::numeric_limits<double>::infinity());
units::second_t stateTimestamp = units::second_t(0);
frc::Pose3d pose = lastPose;
auto targets = result.GetTargets();
for (PhotonPoseEstimator::size_type j = 0; j < targets.size(); ++j) {
PhotonTrackedTarget target = targets[j];
std::optional<frc::Pose3d> fiducialPose =
aprilTags.GetTagPose(target.GetFiducialId());
if (!fiducialPose) {
FRC_ReportError(frc::warn::Warning,
"Tried to get pose of unknown April Tag: {}",
target.GetFiducialId());
continue;
}
frc::Pose3d targetPose = fiducialPose.value();
const auto altPose =
targetPose.TransformBy(target.GetAlternateCameraToTarget().Inverse())
.TransformBy(m_robotToCamera.Inverse());
const auto bestPose =
targetPose.TransformBy(target.GetBestCameraToTarget().Inverse())
.TransformBy(m_robotToCamera.Inverse());
units::meter_t alternativeDifference = units::math::abs(
referencePose.Translation().Distance(altPose.Translation()));
units::meter_t bestDifference = units::math::abs(
referencePose.Translation().Distance(bestPose.Translation()));
if (alternativeDifference < smallestDifference) {
smallestDifference = alternativeDifference;
pose = altPose;
stateTimestamp = result.GetTimestamp();
}
if (bestDifference < smallestDifference) {
smallestDifference = bestDifference;
pose = bestPose;
stateTimestamp = result.GetTimestamp();
}
}
return EstimatedRobotPose{pose, stateTimestamp};
}
std::optional<EstimatedRobotPose>
PhotonPoseEstimator::AverageBestTargetsStrategy(PhotonPipelineResult result) {
std::vector<std::pair<frc::Pose3d, std::pair<double, units::second_t>>>
tempPoses;
double totalAmbiguity = 0;
auto targets = result.GetTargets();
for (PhotonPoseEstimator::size_type j = 0; j < targets.size(); ++j) {
PhotonTrackedTarget target = targets[j];
std::optional<frc::Pose3d> fiducialPose =
aprilTags.GetTagPose(target.GetFiducialId());
if (!fiducialPose) {
FRC_ReportError(frc::warn::Warning,
"Tried to get pose of unknown April Tag: {}",
target.GetFiducialId());
continue;
}
frc::Pose3d targetPose = fiducialPose.value();
// Ambiguity = 0, use that pose
if (target.GetPoseAmbiguity() == 0) {
return EstimatedRobotPose{
targetPose.TransformBy(target.GetBestCameraToTarget().Inverse())
.TransformBy(m_robotToCamera.Inverse()),
result.GetLatency()};
}
totalAmbiguity += 1. / target.GetPoseAmbiguity();
tempPoses.push_back(std::make_pair(
targetPose.TransformBy(target.GetBestCameraToTarget().Inverse()),
std::make_pair(target.GetPoseAmbiguity(), result.GetTimestamp())));
}
frc::Translation3d transform = frc::Translation3d();
frc::Rotation3d rotation = frc::Rotation3d();
for (std::pair<frc::Pose3d, std::pair<double, units::second_t>>& pair :
tempPoses) {
double weight = (1. / pair.second.first) / totalAmbiguity;
transform = transform + pair.first.Translation() * weight;
rotation = rotation + pair.first.Rotation() * weight;
}
return EstimatedRobotPose{frc::Pose3d(transform, rotation),
result.GetTimestamp()};
}
} // namespace photonlib

View File

@@ -30,13 +30,16 @@
#include <frc/geometry/Translation2d.h>
#include <wpi/SmallVector.h>
static constexpr const uint8_t MAX_CORNERS = 8;
namespace photonlib {
PhotonTrackedTarget::PhotonTrackedTarget(
double yaw, double pitch, double area, double skew, int id,
const frc::Transform3d& pose, const frc::Transform3d& alternatePose,
double ambiguity,
const wpi::SmallVector<std::pair<double, double>, 4> corners)
const wpi::SmallVector<std::pair<double, double>, 4> minAreaRectCorners,
const std::vector<std::pair<double, double>> detectedCorners)
: yaw(yaw),
pitch(pitch),
area(area),
@@ -45,12 +48,13 @@ PhotonTrackedTarget::PhotonTrackedTarget(
bestCameraToTarget(pose),
altCameraToTarget(alternatePose),
poseAmbiguity(ambiguity),
corners(corners) {}
minAreaRectCorners(minAreaRectCorners),
detectedCorners(detectedCorners) {}
bool PhotonTrackedTarget::operator==(const PhotonTrackedTarget& other) const {
return other.yaw == yaw && other.pitch == pitch && other.area == area &&
other.skew == skew && other.bestCameraToTarget == bestCameraToTarget &&
other.corners == corners;
other.minAreaRectCorners == minAreaRectCorners;
}
bool PhotonTrackedTarget::operator!=(const PhotonTrackedTarget& other) const {
@@ -77,7 +81,16 @@ Packet& operator<<(Packet& packet, const PhotonTrackedTarget& target) {
<< target.poseAmbiguity;
for (int i = 0; i < 4; i++) {
packet << target.corners[i].first << target.corners[i].second;
packet << target.minAreaRectCorners[i].first
<< target.minAreaRectCorners[i].second;
}
uint8_t num_corners =
std::min<uint8_t>(target.detectedCorners.size(), MAX_CORNERS);
packet << num_corners;
for (size_t i = 0; i < target.detectedCorners.size(); i++) {
packet << target.detectedCorners[i].first
<< target.detectedCorners[i].second;
}
return packet;
@@ -111,12 +124,21 @@ Packet& operator>>(Packet& packet, PhotonTrackedTarget& target) {
packet >> target.poseAmbiguity;
target.corners.clear();
target.minAreaRectCorners.clear();
double first = 0;
double second = 0;
for (int i = 0; i < 4; i++) {
double first = 0;
double second = 0;
packet >> first >> second;
target.corners.emplace_back(first, second);
target.minAreaRectCorners.emplace_back(first, second);
}
uint8_t numCorners = 0;
packet >> numCorners;
target.detectedCorners.clear();
target.detectedCorners.reserve(numCorners);
for (size_t i = 0; i < numCorners; i++) {
packet >> first >> second;
target.detectedCorners.emplace_back(first, second);
}
return packet;

View File

@@ -54,11 +54,12 @@ RobotPoseEstimator::RobotPoseEstimator(
lastPose(frc::Pose3d()),
referencePose(frc::Pose3d()) {}
std::pair<frc::Pose3d, units::millisecond_t> RobotPoseEstimator::Update() {
std::pair<frc::Pose3d, units::second_t> RobotPoseEstimator::Update() {
if (cameras.empty()) {
return std::make_pair(lastPose, units::millisecond_t(0));
return std::make_pair(lastPose, units::second_t(0));
}
std::pair<frc::Pose3d, units::millisecond_t> pair;
std::pair<frc::Pose3d, units::second_t> pair;
switch (strategy) {
case LOWEST_AMBIGUITY:
pair = LowestAmbiguityStrategy();
@@ -73,7 +74,7 @@ std::pair<frc::Pose3d, units::millisecond_t> RobotPoseEstimator::Update() {
lastPose = pair.first;
return pair;
case CLOSEST_TO_LAST_POSE:
referencePose = lastPose;
SetReferencePose(lastPose);
pair = ClosestToReferencePoseStrategy();
lastPose = pair.first;
return pair;
@@ -85,10 +86,11 @@ std::pair<frc::Pose3d, units::millisecond_t> RobotPoseEstimator::Update() {
FRC_ReportError(frc::warn::Warning, "Invalid Pose Strategy selected!",
"");
}
return std::make_pair(lastPose, units::millisecond_t(0));
return std::make_pair(lastPose, units::second_t(0));
}
std::pair<frc::Pose3d, units::millisecond_t>
std::pair<frc::Pose3d, units::second_t>
RobotPoseEstimator::LowestAmbiguityStrategy() {
int lowestAI = -1;
int lowestAJ = -1;
@@ -107,7 +109,7 @@ RobotPoseEstimator::LowestAmbiguityStrategy() {
}
if (lowestAI == -1 || lowestAJ == -1) {
return std::make_pair(lastPose, units::millisecond_t(0));
return std::make_pair(lastPose, units::second_t(0));
}
PhotonTrackedTarget bestTarget =
@@ -119,20 +121,21 @@ RobotPoseEstimator::LowestAmbiguityStrategy() {
FRC_ReportError(frc::warn::Warning,
"Tried to get pose of unknown April Tag: {}",
bestTarget.GetFiducialId());
return std::make_pair(lastPose, units::millisecond_t(0));
return std::make_pair(lastPose, units::second_t(0));
}
return std::make_pair(
fiducialPose.value()
.TransformBy(bestTarget.GetBestCameraToTarget().Inverse())
.TransformBy(cameras[lowestAI].second.Inverse()),
cameras[lowestAI].first->GetLatestResult().GetLatency() / 1000.);
cameras[lowestAI].first->GetLatestResult().GetTimestamp());
}
std::pair<frc::Pose3d, units::millisecond_t>
std::pair<frc::Pose3d, units::second_t>
RobotPoseEstimator::ClosestToCameraHeightStrategy() {
units::meter_t smallestHeightDifference =
units::meter_t(std::numeric_limits<double>::infinity());
units::millisecond_t milli = units::millisecond_t(0);
units::second_t stateTimestamp = units::second_t(0);
frc::Pose3d pose = lastPose;
for (RobotPoseEstimator::size_type i = 0; i < cameras.size(); ++i) {
@@ -161,22 +164,23 @@ RobotPoseEstimator::ClosestToCameraHeightStrategy() {
smallestHeightDifference = alternativeDifference;
pose = targetPose.TransformBy(
target.GetAlternateCameraToTarget().Inverse());
milli = p.first->GetLatestResult().GetLatency() / 1000.;
stateTimestamp = p.first->GetLatestResult().GetTimestamp();
}
if (bestDifference < smallestHeightDifference) {
smallestHeightDifference = bestDifference;
pose = targetPose.TransformBy(target.GetBestCameraToTarget().Inverse());
milli = p.first->GetLatestResult().GetLatency() / 1000.;
stateTimestamp = p.first->GetLatestResult().GetTimestamp();
}
}
}
return std::make_pair(pose, milli);
return std::make_pair(pose, stateTimestamp);
}
std::pair<frc::Pose3d, units::millisecond_t>
std::pair<frc::Pose3d, units::second_t>
RobotPoseEstimator::ClosestToReferencePoseStrategy() {
units::meter_t smallestDifference =
units::meter_t(std::numeric_limits<double>::infinity());
units::millisecond_t milli = units::millisecond_t(0);
units::second_t stateTimestamp = units::second_t(0);
frc::Pose3d pose = lastPose;
for (RobotPoseEstimator::size_type i = 0; i < cameras.size(); ++i) {
@@ -207,29 +211,32 @@ RobotPoseEstimator::ClosestToReferencePoseStrategy() {
smallestDifference = alternativeDifference;
pose = targetPose.TransformBy(
target.GetAlternateCameraToTarget().Inverse());
milli = p.first->GetLatestResult().GetLatency() / 1000.;
stateTimestamp = p.first->GetLatestResult().GetTimestamp();
}
if (bestDifference < smallestDifference) {
smallestDifference = bestDifference;
pose = targetPose.TransformBy(target.GetBestCameraToTarget().Inverse());
milli = p.first->GetLatestResult().GetLatency() / 1000.;
stateTimestamp = p.first->GetLatestResult().GetTimestamp();
}
}
}
return std::make_pair(pose, milli);
return std::make_pair(pose, stateTimestamp);
}
std::pair<frc::Pose3d, units::millisecond_t>
std::pair<frc::Pose3d, units::second_t>
RobotPoseEstimator::AverageBestTargetsStrategy() {
std::vector<std::pair<frc::Pose3d, std::pair<double, units::millisecond_t>>>
std::vector<std::pair<frc::Pose3d, std::pair<double, units::second_t>>>
tempPoses;
double totalAmbiguity = 0;
units::second_t timstampSum = units::second_t(0);
for (RobotPoseEstimator::size_type i = 0; i < cameras.size(); ++i) {
std::pair<std::shared_ptr<PhotonCamera>, frc::Transform3d> p = cameras[i];
std::span<const PhotonTrackedTarget> targets =
p.first->GetLatestResult().GetTargets();
timstampSum += p.first->GetLatestResult().GetTimestamp();
for (RobotPoseEstimator::size_type j = 0; j < targets.size(); ++j) {
PhotonTrackedTarget target = targets[j];
std::optional<frc::Pose3d> fiducialPose =
@@ -255,20 +262,21 @@ RobotPoseEstimator::AverageBestTargetsStrategy() {
tempPoses.push_back(std::make_pair(
targetPose.TransformBy(target.GetBestCameraToTarget().Inverse()),
std::make_pair(target.GetPoseAmbiguity(),
p.first->GetLatestResult().GetLatency() / 1000.)));
p.first->GetLatestResult().GetTimestamp())));
}
}
frc::Translation3d transform = frc::Translation3d();
frc::Rotation3d rotation = frc::Rotation3d();
units::millisecond_t latency = units::millisecond_t(0);
for (std::pair<frc::Pose3d, std::pair<double, units::millisecond_t>>& pair :
for (std::pair<frc::Pose3d, std::pair<double, units::second_t>>& pair :
tempPoses) {
double weight = (1. / pair.second.first) / totalAmbiguity;
transform = transform + pair.first.Translation() * weight;
rotation = rotation + pair.first.Rotation() * weight;
latency += pair.second.second * weight;
}
return std::make_pair(frc::Pose3d(transform, rotation), latency);
return std::make_pair(frc::Pose3d(transform, rotation),
timstampSum / cameras.size());
}
} // namespace photonlib

View File

@@ -30,6 +30,7 @@
#include <networktables/BooleanTopic.h>
#include <networktables/DoubleTopic.h>
#include <networktables/IntegerTopic.h>
#include <networktables/MultiSubscriber.h>
#include <networktables/NetworkTable.h>
#include <networktables/NetworkTableInstance.h>
#include <networktables/RawTopic.h>
@@ -57,7 +58,7 @@ class PhotonCamera {
* @param cameraName The name of the camera, as seen in the UI.
* over.
*/
explicit PhotonCamera(std::shared_ptr<nt::NetworkTableInstance> instance,
explicit PhotonCamera(nt::NetworkTableInstance instance,
const std::string_view cameraName);
/**
@@ -67,6 +68,8 @@ class PhotonCamera {
*/
explicit PhotonCamera(const std::string_view cameraName);
PhotonCamera(PhotonCamera&&) = default;
virtual ~PhotonCamera() = default;
/**
@@ -165,7 +168,6 @@ class PhotonCamera {
std::shared_ptr<nt::NetworkTable> mainTable;
std::shared_ptr<nt::NetworkTable> rootTable;
nt::RawSubscriber rawBytesEntry;
nt::BooleanPublisher driverModeEntry;
nt::IntegerPublisher inputSaveImgEntry;
nt::IntegerSubscriber inputSaveImgSubscriber;
nt::IntegerPublisher outputSaveImgEntry;
@@ -175,9 +177,12 @@ class PhotonCamera {
nt::StringSubscriber versionEntry;
nt::BooleanSubscriber driverModeSubscriber;
nt::BooleanPublisher driverModePublisher;
nt::IntegerSubscriber pipelineIndexSubscriber;
nt::IntegerSubscriber ledModeSubscriber;
nt::MultiSubscriber m_topicNameSubscriber;
std::string path;
std::string m_cameraName;

View File

@@ -0,0 +1,196 @@
/*
* MIT License
*
* Copyright (c) 2022 PhotonVision
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
#pragma once
#include <map>
#include <memory>
#include <utility>
#include <vector>
#include <frc/apriltag/AprilTagFieldLayout.h>
#include <frc/geometry/Pose3d.h>
#include <frc/geometry/Transform3d.h>
#include "photonlib/PhotonCamera.h"
namespace photonlib {
enum PoseStrategy : int {
LOWEST_AMBIGUITY,
CLOSEST_TO_CAMERA_HEIGHT,
CLOSEST_TO_REFERENCE_POSE,
CLOSEST_TO_LAST_POSE,
AVERAGE_BEST_TARGETS
};
struct EstimatedRobotPose {
/** The estimated pose */
frc::Pose3d estimatedPose;
/** The estimated time the frame used to derive the robot pose was taken, in
* the same timebase as the RoboRIO FPGA Timestamp */
units::second_t timestamp;
EstimatedRobotPose(frc::Pose3d pose_, units::second_t time_)
: estimatedPose(pose_), timestamp(time_) {}
};
/**
* The PhotonPoseEstimator class filters or combines readings from all the
* fiducials visible at a given timestamp on the field to produce a single robot
* in field pose, using the strategy set below. Example usage can be found in
* our apriltagExample example project.
*/
class PhotonPoseEstimator {
public:
using map_value_type =
std::pair<std::shared_ptr<PhotonCamera>, frc::Transform3d>;
using size_type = std::vector<map_value_type>::size_type;
/**
* Create a new PhotonPoseEstimator.
*
* <p>Example: {@code <code> <p> Map<Integer, Pose3d> map = new HashMap<>();
* <p> map.put(1, new Pose3d(1.0, 2.0, 3.0, new Rotation3d())); // Tag ID 1 is
* at (1.0,2.0,3.0) </code> }
*
* @param aprilTags A AprilTagFieldLayout linking AprilTag IDs to Pose3ds with
* respect to the FIRST field.
* @param strategy The strategy it should use to determine the best pose.
* @param camera PhotonCameras and
* @param robotToCamera Transform3d from the center of the robot to the camera
* mount positions (ie, robot ➔ camera).
*/
explicit PhotonPoseEstimator(frc::AprilTagFieldLayout aprilTags,
PoseStrategy strategy, PhotonCamera&& camera,
frc::Transform3d robotToCamera);
/**
* Get the AprilTagFieldLayout being used by the PositionEstimator.
*
* @return the AprilTagFieldLayout
*/
frc::AprilTagFieldLayout GetFieldLayout() const { return aprilTags; }
/**
* Get the Position Estimation Strategy being used by the Position Estimator.
*
* @return the strategy
*/
PoseStrategy GetPoseStrategy() const { return strategy; }
/**
* Set the Position Estimation Strategy used by the Position Estimator.
*
* @param strategy the strategy to set
*/
inline void SetPoseStrategy(PoseStrategy strat) { strategy = strat; }
/**
* Return the reference position that is being used by the estimator.
*
* @return the referencePose
*/
frc::Pose3d GetReferencePose() const { return referencePose; }
/**
* Update the stored reference pose for use when using the
* CLOSEST_TO_REFERENCE_POSE strategy.
*
* @param referencePose the referencePose to set
*/
inline void SetReferencePose(frc::Pose3d referencePose) {
this->referencePose = referencePose;
}
/**
* Update the stored last pose. Useful for setting the initial estimate when
* using the CLOSEST_TO_LAST_POSE strategy.
*
* @param lastPose the lastPose to set
*/
inline void SetLastPose(frc::Pose3d lastPose) { this->lastPose = lastPose; }
/**
* Update the pose estimator. Internally grabs a new PhotonPipelineResult from
* the camera and process it.
*/
std::optional<EstimatedRobotPose> Update();
inline PhotonCamera& GetCamera() { return camera; }
private:
frc::AprilTagFieldLayout aprilTags;
PoseStrategy strategy;
PhotonCamera camera;
frc::Transform3d m_robotToCamera;
frc::Pose3d lastPose;
frc::Pose3d referencePose;
/**
* Return the estimated position of the robot with the lowest position
* ambiguity from a List of pipeline results.
*
* @return the estimated position of the robot in the FCS and the estimated
* timestamp of this estimation.
*/
std::optional<EstimatedRobotPose> LowestAmbiguityStrategy(
PhotonPipelineResult result);
/**
* Return the estimated position of the robot using the target with the lowest
* delta height difference between the estimated and actual height of the
* camera.
*
* @return the estimated position of the robot in the FCS and the estimated
* timestamp of this estimation.
*/
std::optional<EstimatedRobotPose> ClosestToCameraHeightStrategy(
PhotonPipelineResult result);
/**
* Return the estimated position of the robot using the target with the lowest
* delta in the vector magnitude between it and the reference pose.
*
* @param referencePose reference pose to check vector magnitude difference
* against.
* @return the estimated position of the robot in the FCS and the estimated
* timestamp of this estimation.
*/
std::optional<EstimatedRobotPose> ClosestToReferencePoseStrategy(
PhotonPipelineResult result);
/**
* Return the average of the best target poses using ambiguity as weight.
* @return the estimated position of the robot in the FCS and the estimated
timestamp of this
* estimation.
*/
std::optional<EstimatedRobotPose> AverageBestTargetsStrategy(
PhotonPipelineResult result);
};
} // namespace photonlib

View File

@@ -59,7 +59,8 @@ class PhotonTrackedTarget {
double yaw, double pitch, double area, double skew, int fiducialID,
const frc::Transform3d& pose, const frc::Transform3d& alternatePose,
double ambiguity,
const wpi::SmallVector<std::pair<double, double>, 4> corners);
const wpi::SmallVector<std::pair<double, double>, 4> corners,
const std::vector<std::pair<double, double>> detectedCorners);
/**
* Returns the target yaw (positive-left).
@@ -92,10 +93,27 @@ class PhotonTrackedTarget {
int GetFiducialId() const { return fiducialId; }
/**
* Returns the corners of the minimum area rectangle bounding this target.
* Return a list of the 4 corners in image space (origin top left, x right, y
* down), in no particular order, of the minimum area bounding rectangle of
* this target
*/
wpi::SmallVector<std::pair<double, double>, 4> GetCorners() const {
return corners;
wpi::SmallVector<std::pair<double, double>, 4> GetMinAreaRectCorners() const {
return minAreaRectCorners;
}
/**
* Return a list of the n corners in image space (origin top left, x right, y
* down), in no particular order, detected for this target.
* For fiducials, the order is known and is always counter-clock wise around
* the tag, like so
*
* -> +X 3 ----- 2
* | | |
* V + Y | |
* 0 ----- 1
*/
std::vector<std::pair<double, double>> GetDetectedCorners() {
return detectedCorners;
}
/**
@@ -137,6 +155,7 @@ class PhotonTrackedTarget {
frc::Transform3d bestCameraToTarget;
frc::Transform3d altCameraToTarget;
double poseAmbiguity;
wpi::SmallVector<std::pair<double, double>, 4> corners;
wpi::SmallVector<std::pair<double, double>, 4> minAreaRectCorners;
std::vector<std::pair<double, double>> detectedCorners;
};
} // namespace photonlib

View File

@@ -48,7 +48,10 @@ enum PoseStrategy : int {
};
/**
* A managing class to determine how an estimated pose should be chosen.
* The RobotPoseEstimator class filters or combines readings from all the
* fiducials visible at a given timestamp on the field to produce a single robot
* in field pose, using the strategy set below. Example usage can be found in
* our apriltagExample example project.
*/
class RobotPoseEstimator {
public:
@@ -56,32 +59,84 @@ class RobotPoseEstimator {
std::pair<std::shared_ptr<PhotonCamera>, frc::Transform3d>;
using size_type = std::vector<map_value_type>::size_type;
/**
* Create a new RobotPoseEstimator.
*
* <p>Example: {@code <code> <p> Map<Integer, Pose3d> map = new HashMap<>();
* <p> map.put(1, new Pose3d(1.0, 2.0, 3.0, new Rotation3d())); // Tag ID 1 is
* at (1.0,2.0,3.0) </code> }
*
* @param aprilTags A AprilTagFieldLayout linking AprilTag IDs to Pose3ds with
* respect to the FIRST field.
* @param strategy The strategy it should use to determine the best pose.
* @param cameras An ArrayList of Pairs of PhotonCameras and their respective
* Transform3ds from the center of the robot to the cameras.
*/
explicit RobotPoseEstimator(
std::shared_ptr<frc::AprilTagFieldLayout> aprilTags,
PoseStrategy strategy, std::vector<map_value_type> cameras);
std::pair<frc::Pose3d, units::millisecond_t> Update();
inline void SetPoseStrategy(PoseStrategy strat) { strategy = strat; }
inline void SetReferencePose(frc::Pose3d referencePose) {
this->referencePose = referencePose;
/**
* Get the AprilTagFieldLayout being used by the PositionEstimator.
*
* @return the AprilTagFieldLayout
*/
std::shared_ptr<frc::AprilTagFieldLayout> getFieldLayout() const {
return aprilTags;
}
inline void SetLastPose(frc::Pose3d lastPose) { this->lastPose = lastPose; }
/**
* Set the cameras to be used by the PoseEstimator.
*
* @param cameras cameras to set.
*/
inline void SetCameras(
const std::vector<std::pair<std::shared_ptr<PhotonCamera>,
frc::Transform3d>>& cameras) {
this->cameras = cameras;
}
/**
* Get the Position Estimation Strategy being used by the Position Estimator.
*
* @return the strategy
*/
PoseStrategy GetPoseStrategy() const { return strategy; }
frc::Pose3d GetLastPose() const { return lastPose; }
/**
* Set the Position Estimation Strategy used by the Position Estimator.
*
* @param strategy the strategy to set
*/
inline void SetPoseStrategy(PoseStrategy strat) { strategy = strat; }
/**
* Return the reference position that is being used by the estimator.
*
* @return the referencePose
*/
frc::Pose3d GetReferencePose() const { return referencePose; }
/**
* Update the stored reference pose for use when using the
* CLOSEST_TO_REFERENCE_POSE strategy.
*
* @param referencePose the referencePose to set
*/
inline void SetReferencePose(frc::Pose3d referencePose) {
this->referencePose = referencePose;
}
/**
* Update the stored last pose. Useful for setting the initial estimate when
* using the CLOSEST_TO_LAST_POSE strategy.
*
* @param lastPose the lastPose to set
*/
inline void SetLastPose(frc::Pose3d lastPose) { this->lastPose = lastPose; }
std::pair<frc::Pose3d, units::second_t> Update();
private:
std::shared_ptr<frc::AprilTagFieldLayout> aprilTags;
PoseStrategy strategy;
@@ -89,13 +144,44 @@ class RobotPoseEstimator {
frc::Pose3d lastPose;
frc::Pose3d referencePose;
std::pair<frc::Pose3d, units::millisecond_t> LowestAmbiguityStrategy();
/**
* Return the estimated position of the robot with the lowest position
* ambiguity from a List of pipeline results.
*
* @return the estimated position of the robot in the FCS and the estimated
* timestamp of this estimation.
*/
std::pair<frc::Pose3d, units::second_t> LowestAmbiguityStrategy();
std::pair<frc::Pose3d, units::millisecond_t> ClosestToCameraHeightStrategy();
/**
* Return the estimated position of the robot using the target with the lowest
* delta height difference between the estimated and actual height of the
* camera.
*
* @return the estimated position of the robot in the FCS and the estimated
* timestamp of this estimation.
*/
std::pair<frc::Pose3d, units::second_t> ClosestToCameraHeightStrategy();
std::pair<frc::Pose3d, units::millisecond_t> ClosestToReferencePoseStrategy();
/**
* Return the estimated position of the robot using the target with the lowest
* delta in the vector magnitude between it and the reference pose.
*
* @param referencePose reference pose to check vector magnitude difference
* against.
* @return the estimated position of the robot in the FCS and the estimated
* timestamp of this estimation.
*/
std::pair<frc::Pose3d, units::second_t> ClosestToReferencePoseStrategy();
std::pair<frc::Pose3d, units::millisecond_t> AverageBestTargetsStrategy();
/**
* Return the average of the best target poses using ambiguity as weight.
* @return the estimated position of the robot in the FCS and the estimated
timestamp of this
* estimation.
*/
std::pair<frc::Pose3d, units::second_t> AverageBestTargetsStrategy();
};
} // namespace photonlib

View File

@@ -37,7 +37,7 @@
namespace photonlib {
class SimPhotonCamera : public PhotonCamera {
public:
SimPhotonCamera(std::shared_ptr<nt::NetworkTableInstance> instance,
SimPhotonCamera(nt::NetworkTableInstance instance,
const std::string& cameraName)
: PhotonCamera(instance, cameraName) {
latencyMillisEntry = rootTable->GetEntry("latencyMillis");
@@ -48,14 +48,12 @@ class SimPhotonCamera : public PhotonCamera {
targetSkewEntry = rootTable->GetEntry("targetSkewEntry");
targetPoseEntry = rootTable->GetEntry("targetPoseEntry");
rawBytesPublisher = rootTable->GetRawTopic("rawBytes").Publish("raw");
versionEntry = instance->GetTable("photonvision")->GetEntry("version");
versionEntry = instance.GetTable("photonvision")->GetEntry("version");
// versionEntry.SetString(PhotonVersion.versionString);
}
explicit SimPhotonCamera(const std::string& cameraName)
: SimPhotonCamera(std::make_shared<nt::NetworkTableInstance>(
nt::NetworkTableInstance::GetDefault()),
cameraName) {}
: SimPhotonCamera(nt::NetworkTableInstance::GetDefault(), cameraName) {}
virtual ~SimPhotonCamera() = default;

View File

@@ -108,6 +108,14 @@ class SimVisionSystem {
->SetPose(target.targetPose.ToPose2d());
}
/**
* Clears all sim vision targets.
* This is useful for switching alliances and needing to repopulate the sim
* targets. NOTE: Old targets will still show on the Field2d unless
* overwritten by new targets with the same ID
*/
void ClearVisionTargets() { targetList.clear(); }
/**
* Adjust the camera position relative to the robot. Use this if your camera
* is on a gimbal or turret or some other mobile platform.
@@ -185,6 +193,7 @@ class SimVisionSystem {
camToTargetTransform,
// TODO ambiguity
0.0,
{{0, 0}, {0, 0}, {0, 0}, {0, 0}},
{{0, 0}, {0, 0}, {0, 0}, {0, 0}}});
}

View File

@@ -47,6 +47,11 @@ class PacketTest {
new Transform3d(new Translation3d(), new Rotation3d()),
new Transform3d(new Translation3d(), new Rotation3d()),
0.25,
List.of(
new TargetCorner(1, 2),
new TargetCorner(3, 4),
new TargetCorner(5, 6),
new TargetCorner(7, 8)),
List.of(
new TargetCorner(1, 2),
new TargetCorner(3, 4),
@@ -85,6 +90,11 @@ class PacketTest {
new Transform3d(new Translation3d(1, 2, 3), new Rotation3d(1, 2, 3)),
new Transform3d(new Translation3d(1, 2, 3), new Rotation3d(1, 2, 3)),
0.25,
List.of(
new TargetCorner(1, 2),
new TargetCorner(3, 4),
new TargetCorner(5, 6),
new TargetCorner(7, 8)),
List.of(
new TargetCorner(1, 2),
new TargetCorner(3, 4),
@@ -99,6 +109,11 @@ class PacketTest {
new Transform3d(new Translation3d(4, 2, 3), new Rotation3d(1, 5, 3)),
new Transform3d(new Translation3d(4, 2, 3), new Rotation3d(1, 5, 3)),
0.25,
List.of(
new TargetCorner(1, 2),
new TargetCorner(3, 4),
new TargetCorner(5, 6),
new TargetCorner(7, 8)),
List.of(
new TargetCorner(1, 2),
new TargetCorner(3, 4),

View File

@@ -45,12 +45,12 @@ import java.util.List;
import java.util.Optional;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.photonvision.RobotPoseEstimator.PoseStrategy;
import org.photonvision.PhotonPoseEstimator.PoseStrategy;
import org.photonvision.targeting.PhotonPipelineResult;
import org.photonvision.targeting.PhotonTrackedTarget;
import org.photonvision.targeting.TargetCorner;
class RobotPoseEstimatorTest {
class PhotonPoseEstimatorTest {
static AprilTagFieldLayout aprilTags;
@BeforeAll
@@ -62,24 +62,22 @@ class RobotPoseEstimatorTest {
try {
CombinedRuntimeLoader.loadLibraries(
RobotPoseEstimatorTest.class, "wpiutiljni", "ntcorejni", "wpinetjni", "wpiHaljni");
PhotonPoseEstimatorTest.class, "wpiutiljni", "ntcorejni", "wpinetjni", "wpiHaljni");
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
List<AprilTag> atList = new ArrayList<AprilTag>(2);
atList.add(new AprilTag(0, new Pose3d(3, 3, 3, new Rotation3d())));
atList.add(new AprilTag(1, new Pose3d(5, 5, 5, new Rotation3d())));
var fl = Units.feetToMeters(54.0);
var fw = Units.feetToMeters(27.0);
aprilTags = new AprilTagFieldLayout(atList, fl, fw);
List<AprilTag> tagList = new ArrayList<AprilTag>(2);
tagList.add(new AprilTag(0, new Pose3d(3, 3, 3, new Rotation3d())));
tagList.add(new AprilTag(1, new Pose3d(5, 5, 5, new Rotation3d())));
double fieldLength = Units.feetToMeters(54.0);
double fieldWidth = Units.feetToMeters(27.0);
aprilTags = new AprilTagFieldLayout(tagList, fieldLength, fieldWidth);
}
@Test
void testLowestAmbiguityStrategy() {
ArrayList<Pair<PhotonCamera, Transform3d>> cameras = new ArrayList<>();
PhotonCameraInjector cameraOne = new PhotonCameraInjector();
cameraOne.result =
new PhotonPipelineResult(
@@ -94,6 +92,11 @@ class RobotPoseEstimatorTest {
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.7,
List.of(
new TargetCorner(1, 2),
new TargetCorner(3, 4),
new TargetCorner(5, 6),
new TargetCorner(7, 8)),
List.of(
new TargetCorner(1, 2),
new TargetCorner(3, 4),
@@ -112,12 +115,12 @@ class RobotPoseEstimatorTest {
new TargetCorner(1, 2),
new TargetCorner(3, 4),
new TargetCorner(5, 6),
new TargetCorner(7, 8)))));
PhotonCameraInjector cameraTwo = new PhotonCameraInjector();
cameraTwo.result =
new PhotonPipelineResult(
4,
List.of(
new TargetCorner(7, 8)),
List.of(
new TargetCorner(1, 2),
new TargetCorner(3, 4),
new TargetCorner(5, 6),
new TargetCorner(7, 8))),
new PhotonTrackedTarget(
9.0,
-2.0,
@@ -127,22 +130,26 @@ class RobotPoseEstimatorTest {
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.4,
List.of(
new TargetCorner(1, 2),
new TargetCorner(3, 4),
new TargetCorner(5, 6),
new TargetCorner(7, 8)),
List.of(
new TargetCorner(1, 2),
new TargetCorner(3, 4),
new TargetCorner(5, 6),
new TargetCorner(7, 8)))));
cameraOne.result.setTimestampSeconds(11);
cameras.add(Pair.of(cameraOne, new Transform3d()));
cameras.add(Pair.of(cameraTwo, new Transform3d()));
PhotonPoseEstimator estimator =
new PhotonPoseEstimator(
aprilTags, PoseStrategy.LOWEST_AMBIGUITY, cameraOne, new Transform3d());
RobotPoseEstimator estimator =
new RobotPoseEstimator(aprilTags, PoseStrategy.LOWEST_AMBIGUITY, cameras);
Optional<EstimatedRobotPose> estimatedPose = estimator.update();
Pose3d pose = estimatedPose.get().estimatedPose;
Optional<Pair<Pose3d, Double>> estimatedPose = estimator.update();
Pose3d pose = estimatedPose.get().getFirst();
assertEquals(2, estimatedPose.get().getSecond());
assertEquals(11, estimatedPose.get().timestampSeconds);
assertEquals(1, pose.getX(), .01);
assertEquals(3, pose.getY(), .01);
assertEquals(2, pose.getZ(), .01);
@@ -166,6 +173,11 @@ class RobotPoseEstimatorTest {
new Transform3d(new Translation3d(0, 0, 0), new Rotation3d()),
new Transform3d(new Translation3d(1, 1, 1), new Rotation3d()),
0.7,
List.of(
new TargetCorner(1, 2),
new TargetCorner(3, 4),
new TargetCorner(5, 6),
new TargetCorner(7, 8)),
List.of(
new TargetCorner(1, 2),
new TargetCorner(3, 4),
@@ -184,12 +196,12 @@ class RobotPoseEstimatorTest {
new TargetCorner(1, 2),
new TargetCorner(3, 4),
new TargetCorner(5, 6),
new TargetCorner(7, 8)))));
PhotonCameraInjector cameraTwo = new PhotonCameraInjector();
cameraTwo.result =
new PhotonPipelineResult(
4,
List.of(
new TargetCorner(7, 8)),
List.of(
new TargetCorner(1, 2),
new TargetCorner(3, 4),
new TargetCorner(5, 6),
new TargetCorner(7, 8))),
new PhotonTrackedTarget(
9.0,
-2.0,
@@ -199,22 +211,30 @@ class RobotPoseEstimatorTest {
new Transform3d(new Translation3d(4, 4, 4), new Rotation3d()),
new Transform3d(new Translation3d(5, 5, 5), new Rotation3d()),
0.4,
List.of(
new TargetCorner(1, 2),
new TargetCorner(3, 4),
new TargetCorner(5, 6),
new TargetCorner(7, 8)),
List.of(
new TargetCorner(1, 2),
new TargetCorner(3, 4),
new TargetCorner(5, 6),
new TargetCorner(7, 8)))));
cameras.add(Pair.of(cameraOne, new Transform3d(new Translation3d(0, 0, 4), new Rotation3d())));
cameras.add(Pair.of(cameraTwo, new Transform3d(new Translation3d(0, 0, 2), new Rotation3d())));
cameraOne.result.setTimestampSeconds(4);
RobotPoseEstimator estimator =
new RobotPoseEstimator(aprilTags, PoseStrategy.CLOSEST_TO_CAMERA_HEIGHT, cameras);
PhotonPoseEstimator estimator =
new PhotonPoseEstimator(
aprilTags,
PoseStrategy.CLOSEST_TO_CAMERA_HEIGHT,
cameraOne,
new Transform3d(new Translation3d(0, 0, 4), new Rotation3d()));
Optional<Pair<Pose3d, Double>> estimatedPose = estimator.update();
Pose3d pose = estimatedPose.get().getFirst();
Optional<EstimatedRobotPose> estimatedPose = estimator.update();
Pose3d pose = estimatedPose.get().estimatedPose;
assertEquals(2, estimatedPose.get().getSecond());
assertEquals(4, estimatedPose.get().timestampSeconds);
assertEquals(4, pose.getX(), .01);
assertEquals(4, pose.getY(), .01);
assertEquals(0, pose.getZ(), .01);
@@ -222,8 +242,6 @@ class RobotPoseEstimatorTest {
@Test
void closestToReferencePoseStrategy() {
ArrayList<Pair<PhotonCamera, Transform3d>> cameras = new ArrayList<>();
PhotonCameraInjector cameraOne = new PhotonCameraInjector();
cameraOne.result =
new PhotonPipelineResult(
@@ -238,6 +256,11 @@ class RobotPoseEstimatorTest {
new Transform3d(new Translation3d(0, 0, 0), new Rotation3d()),
new Transform3d(new Translation3d(1, 1, 1), new Rotation3d()),
0.7,
List.of(
new TargetCorner(1, 2),
new TargetCorner(3, 4),
new TargetCorner(5, 6),
new TargetCorner(7, 8)),
List.of(
new TargetCorner(1, 2),
new TargetCorner(3, 4),
@@ -256,12 +279,12 @@ class RobotPoseEstimatorTest {
new TargetCorner(1, 2),
new TargetCorner(3, 4),
new TargetCorner(5, 6),
new TargetCorner(7, 8)))));
PhotonCameraInjector cameraTwo = new PhotonCameraInjector();
cameraTwo.result =
new PhotonPipelineResult(
4,
List.of(
new TargetCorner(7, 8)),
List.of(
new TargetCorner(1, 2),
new TargetCorner(3, 4),
new TargetCorner(5, 6),
new TargetCorner(7, 8))),
new PhotonTrackedTarget(
9.0,
-2.0,
@@ -271,23 +294,30 @@ class RobotPoseEstimatorTest {
new Transform3d(new Translation3d(2.2, 2.2, 2.2), new Rotation3d()),
new Transform3d(new Translation3d(2, 1.9, 2.1), new Rotation3d()),
0.4,
List.of(
new TargetCorner(1, 2),
new TargetCorner(3, 4),
new TargetCorner(5, 6),
new TargetCorner(7, 8)),
List.of(
new TargetCorner(1, 2),
new TargetCorner(3, 4),
new TargetCorner(5, 6),
new TargetCorner(7, 8)))));
cameraOne.result.setTimestampSeconds(17);
cameras.add(Pair.of(cameraOne, new Transform3d(new Translation3d(0, 0, 0), new Rotation3d())));
cameras.add(Pair.of(cameraTwo, new Transform3d(new Translation3d(0, 0, 0), new Rotation3d())));
RobotPoseEstimator estimator =
new RobotPoseEstimator(aprilTags, PoseStrategy.CLOSEST_TO_REFERENCE_POSE, cameras);
PhotonPoseEstimator estimator =
new PhotonPoseEstimator(
aprilTags,
PoseStrategy.CLOSEST_TO_REFERENCE_POSE,
cameraOne,
new Transform3d(new Translation3d(0, 0, 0), new Rotation3d()));
estimator.setReferencePose(new Pose3d(1, 1, 1, new Rotation3d()));
Optional<Pair<Pose3d, Double>> estimatedPose = estimator.update();
Pose3d pose = estimatedPose.get().getFirst();
Optional<EstimatedRobotPose> estimatedPose = estimator.update();
Pose3d pose = estimatedPose.get().estimatedPose;
assertEquals(4, estimatedPose.get().getSecond());
assertEquals(17, estimatedPose.get().timestampSeconds);
assertEquals(1, pose.getX(), .01);
assertEquals(1.1, pose.getY(), .01);
assertEquals(.9, pose.getZ(), .01);
@@ -295,8 +325,6 @@ class RobotPoseEstimatorTest {
@Test
void closestToLastPose() {
ArrayList<Pair<PhotonCamera, Transform3d>> cameras = new ArrayList<>();
PhotonCameraInjector cameraOne = new PhotonCameraInjector();
cameraOne.result =
new PhotonPipelineResult(
@@ -311,6 +339,11 @@ class RobotPoseEstimatorTest {
new Transform3d(new Translation3d(0, 0, 0), new Rotation3d()),
new Transform3d(new Translation3d(1, 1, 1), new Rotation3d()),
0.7,
List.of(
new TargetCorner(1, 2),
new TargetCorner(3, 4),
new TargetCorner(5, 6),
new TargetCorner(7, 8)),
List.of(
new TargetCorner(1, 2),
new TargetCorner(3, 4),
@@ -329,12 +362,12 @@ class RobotPoseEstimatorTest {
new TargetCorner(1, 2),
new TargetCorner(3, 4),
new TargetCorner(5, 6),
new TargetCorner(7, 8)))));
PhotonCameraInjector cameraTwo = new PhotonCameraInjector();
cameraTwo.result =
new PhotonPipelineResult(
4,
List.of(
new TargetCorner(7, 8)),
List.of(
new TargetCorner(1, 2),
new TargetCorner(3, 4),
new TargetCorner(5, 6),
new TargetCorner(7, 8))),
new PhotonTrackedTarget(
9.0,
-2.0,
@@ -344,22 +377,28 @@ class RobotPoseEstimatorTest {
new Transform3d(new Translation3d(2.2, 2.2, 2.2), new Rotation3d()),
new Transform3d(new Translation3d(2, 1.9, 2.1), new Rotation3d()),
0.4,
List.of(
new TargetCorner(1, 2),
new TargetCorner(3, 4),
new TargetCorner(5, 6),
new TargetCorner(7, 8)),
List.of(
new TargetCorner(1, 2),
new TargetCorner(3, 4),
new TargetCorner(5, 6),
new TargetCorner(7, 8)))));
cameras.add(Pair.of(cameraOne, new Transform3d(new Translation3d(0, 0, 0), new Rotation3d())));
cameras.add(Pair.of(cameraTwo, new Transform3d(new Translation3d(0, 0, 0), new Rotation3d())));
RobotPoseEstimator estimator =
new RobotPoseEstimator(aprilTags, PoseStrategy.CLOSEST_TO_LAST_POSE, cameras);
PhotonPoseEstimator estimator =
new PhotonPoseEstimator(
aprilTags,
PoseStrategy.CLOSEST_TO_LAST_POSE,
cameraOne,
new Transform3d(new Translation3d(0, 0, 0), new Rotation3d()));
estimator.setLastPose(new Pose3d(1, 1, 1, new Rotation3d()));
Optional<Pair<Pose3d, Double>> estimatedPose = estimator.update();
Pose3d pose = estimatedPose.get().getFirst();
Optional<EstimatedRobotPose> estimatedPose = estimator.update();
Pose3d pose = estimatedPose.get().estimatedPose;
cameraOne.result =
new PhotonPipelineResult(
@@ -374,6 +413,11 @@ class RobotPoseEstimatorTest {
new Transform3d(new Translation3d(0, 0, 0), new Rotation3d()),
new Transform3d(new Translation3d(1, 1, 1), new Rotation3d()),
0.7,
List.of(
new TargetCorner(1, 2),
new TargetCorner(3, 4),
new TargetCorner(5, 6),
new TargetCorner(7, 8)),
List.of(
new TargetCorner(1, 2),
new TargetCorner(3, 4),
@@ -392,11 +436,12 @@ class RobotPoseEstimatorTest {
new TargetCorner(1, 2),
new TargetCorner(3, 4),
new TargetCorner(5, 6),
new TargetCorner(7, 8)))));
cameraTwo.result =
new PhotonPipelineResult(
4,
List.of(
new TargetCorner(7, 8)),
List.of(
new TargetCorner(1, 2),
new TargetCorner(3, 4),
new TargetCorner(5, 6),
new TargetCorner(7, 8))),
new PhotonTrackedTarget(
9.0,
-2.0,
@@ -406,16 +451,22 @@ class RobotPoseEstimatorTest {
new Transform3d(new Translation3d(2.4, 2.4, 2.2), new Rotation3d()),
new Transform3d(new Translation3d(2, 1, 2), new Rotation3d()),
0.4,
List.of(
new TargetCorner(1, 2),
new TargetCorner(3, 4),
new TargetCorner(5, 6),
new TargetCorner(7, 8)),
List.of(
new TargetCorner(1, 2),
new TargetCorner(3, 4),
new TargetCorner(5, 6),
new TargetCorner(7, 8)))));
cameraOne.result.setTimestampSeconds(7);
estimatedPose = estimator.update();
pose = estimatedPose.get().getFirst();
pose = estimatedPose.get().estimatedPose;
assertEquals(2, estimatedPose.get().getSecond());
assertEquals(7, estimatedPose.get().timestampSeconds);
assertEquals(.9, pose.getX(), .01);
assertEquals(1.1, pose.getY(), .01);
assertEquals(1, pose.getZ(), .01);
@@ -439,6 +490,11 @@ class RobotPoseEstimatorTest {
new Transform3d(new Translation3d(2, 2, 2), new Rotation3d()),
new Transform3d(new Translation3d(1, 1, 1), new Rotation3d()),
0.7,
List.of(
new TargetCorner(1, 2),
new TargetCorner(3, 4),
new TargetCorner(5, 6),
new TargetCorner(7, 8)),
List.of(
new TargetCorner(1, 2),
new TargetCorner(3, 4),
@@ -457,12 +513,12 @@ class RobotPoseEstimatorTest {
new TargetCorner(1, 2),
new TargetCorner(3, 4),
new TargetCorner(5, 6),
new TargetCorner(7, 8))))); // 2 2 2 ambig .3
PhotonCameraInjector cameraTwo = new PhotonCameraInjector();
cameraTwo.result =
new PhotonPipelineResult(
4,
List.of(
new TargetCorner(7, 8)),
List.of(
new TargetCorner(1, 2),
new TargetCorner(3, 4),
new TargetCorner(5, 6),
new TargetCorner(7, 8))), // 2 2 2 ambig .3
new PhotonTrackedTarget(
9.0,
-2.0,
@@ -472,21 +528,29 @@ class RobotPoseEstimatorTest {
new Transform3d(new Translation3d(0, 0, 0), new Rotation3d()),
new Transform3d(new Translation3d(2, 1.9, 2.1), new Rotation3d()),
0.4,
List.of(
new TargetCorner(1, 2),
new TargetCorner(3, 4),
new TargetCorner(5, 6),
new TargetCorner(7, 8)),
List.of(
new TargetCorner(1, 2),
new TargetCorner(3, 4),
new TargetCorner(5, 6),
new TargetCorner(7, 8))))); // 3 3 3 ambig .4
cameraOne.result.setTimestampSeconds(20);
cameras.add(Pair.of(cameraOne, new Transform3d(new Translation3d(0, 0, 0), new Rotation3d())));
cameras.add(Pair.of(cameraTwo, new Transform3d(new Translation3d(0, 0, 0), new Rotation3d())));
PhotonPoseEstimator estimator =
new PhotonPoseEstimator(
aprilTags,
PoseStrategy.AVERAGE_BEST_TARGETS,
cameraOne,
new Transform3d(new Translation3d(0, 0, 0), new Rotation3d()));
RobotPoseEstimator estimator =
new RobotPoseEstimator(aprilTags, PoseStrategy.AVERAGE_BEST_TARGETS, cameras);
Optional<EstimatedRobotPose> estimatedPose = estimator.update();
Pose3d pose = estimatedPose.get().estimatedPose;
Optional<Pair<Pose3d, Double>> estimatedPose = estimator.update();
Pose3d pose = estimatedPose.get().getFirst();
assertEquals(2.6885245901639347, estimatedPose.get().getSecond(), .01);
assertEquals(20, estimatedPose.get().timestampSeconds, .01);
assertEquals(2.15, pose.getX(), .01);
assertEquals(2.15, pose.getY(), .01);
assertEquals(2.15, pose.getZ(), .01);

View File

@@ -182,8 +182,7 @@ class SimVisionSystemTest {
new Pose3d(new Translation3d(15.98, 0, 2), new Rotation3d(0, 0, Math.PI));
var robotToCamera =
new Transform3d(new Translation3d(0, 0, 1), new Rotation3d(0, Math.PI / 4, 0));
var sysUnderTest =
new SimVisionSystem("Test", 80.0, robotToCamera.inverse(), 99999, 1234, 1234, 0);
var sysUnderTest = new SimVisionSystem("Test", 80.0, robotToCamera, 99999, 1234, 1234, 0);
sysUnderTest.addSimVisionTarget(new SimVisionTarget(targetPose, 3.0, 0.5, 1736));
var robotPose = new Pose2d(new Translation2d(14.98, 0), Rotation2d.fromDegrees(5));
@@ -247,14 +246,15 @@ class SimVisionSystemTest {
@ParameterizedTest
@ValueSource(doubles = {-10, -5, -0, -1, -2, 5, 7, 10.23, 20.21, -19.999})
public void testCameraPitch(double testPitch) {
testPitch = testPitch * -1;
final var targetPose =
new Pose3d(new Translation3d(15.98, 0, 0), new Rotation3d(0, 0, 3 * Math.PI / 4));
final var robotPose = new Pose2d(new Translation2d(10, 0), new Rotation2d(0));
var sysUnderTest = new SimVisionSystem("Test", 120.0, new Transform3d(), 99999, 640, 480, 0);
sysUnderTest.addSimVisionTarget(new SimVisionTarget(targetPose, 0.5, 0.5, 23));
// Here, passing in a positive testPitch points the camera downward (since moveCamera takes the
// camera->robot transform)
// Transform is now robot -> camera
sysUnderTest.moveCamera(
new Transform3d(
new Translation3d(), new Rotation3d(0, Units.degreesToRadians(testPitch), 0)));
@@ -263,10 +263,11 @@ class SimVisionSystemTest {
assertTrue(res.hasTargets());
var tgt = res.getBestTarget();
// Since the camera is level with the target, a downward point will mean the target is in the
// upper half of the image
// which should produce positive pitch.
assertEquals(testPitch, tgt.getPitch(), 0.0001);
// Since the camera is level with the target, a positive-upward point will mean the target is in
// the
// lower half of the image
// which should produce negative pitch.
assertEquals(testPitch * -1, tgt.getPitch(), 0.0001);
}
private static Stream<Arguments> distCalCParamProvider() {
@@ -309,7 +310,7 @@ class SimVisionSystemTest {
new SimVisionSystem(
"absurdlylongnamewhichshouldneveractuallyhappenbuteehwelltestitanywaysohowsyourdaygoingihopegoodhaveagreatrestofyourlife!",
160.0,
robotToCamera.inverse(),
robotToCamera,
99999,
640,
480,

View File

@@ -42,6 +42,7 @@ TEST(PacketTest, PhotonTrackedTarget) {
frc::Transform3d(frc::Translation3d(1_m, 2_m, 3_m),
frc::Rotation3d(1_rad, 2_rad, 3_rad)),
-1,
{std::pair{1, 2}, std::pair{3, 4}, std::pair{5, 6}, std::pair{7, 8}},
{std::pair{1, 2}, std::pair{3, 4}, std::pair{5, 6}, std::pair{7, 8}}};
photonlib::Packet p;
@@ -79,6 +80,7 @@ TEST(PacketTest, PhotonPipelineResult) {
frc::Transform3d(frc::Translation3d(1_m, 2_m, 3_m),
frc::Rotation3d(1_rad, 2_rad, 3_rad)),
-1,
{std::pair{1, 2}, std::pair{3, 4}, std::pair{5, 6}, std::pair{7, 8}},
{std::pair{1, 2}, std::pair{3, 4}, std::pair{5, 6}, std::pair{7, 8}}},
photonlib::PhotonTrackedTarget{
3.0,
@@ -91,6 +93,7 @@ TEST(PacketTest, PhotonPipelineResult) {
frc::Transform3d(frc::Translation3d(1_m, 2_m, 3_m),
frc::Rotation3d(1_rad, 2_rad, 3_rad)),
-1,
{std::pair{1, 2}, std::pair{3, 4}, std::pair{5, 6}, std::pair{7, 8}},
{std::pair{1, 2}, std::pair{3, 4}, std::pair{5, 6},
std::pair{7, 8}}}};

View File

@@ -0,0 +1,312 @@
/*
* MIT License
*
* Copyright (c) 2022 PhotonVision
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
#include <map>
#include <utility>
#include <vector>
#include <frc/apriltag/AprilTagFieldLayout.h>
#include <frc/geometry/Pose3d.h>
#include <frc/geometry/Rotation3d.h>
#include <frc/geometry/Transform3d.h>
#include <units/angle.h>
#include <units/length.h>
#include <wpi/SmallVector.h>
#include "gtest/gtest.h"
#include "photonlib/PhotonCamera.h"
#include "photonlib/PhotonPipelineResult.h"
#include "photonlib/PhotonPoseEstimator.h"
#include "photonlib/PhotonTrackedTarget.h"
static std::vector<frc::AprilTag> tags = {
{0, frc::Pose3d(units::meter_t(3), units::meter_t(3), units::meter_t(3),
frc::Rotation3d())},
{1, frc::Pose3d(units::meter_t(5), units::meter_t(5), units::meter_t(5),
frc::Rotation3d())}};
static frc::AprilTagFieldLayout aprilTags{tags, 54_ft, 27_ft};
static wpi::SmallVector<std::pair<double, double>, 4> corners{
std::pair{1, 2}, std::pair{3, 4}, std::pair{5, 6}, std::pair{7, 8}};
static std::vector<std::pair<double, double>> detectedCorners{
std::pair{1, 2}, std::pair{3, 4}, std::pair{5, 6}, std::pair{7, 8}};
TEST(PhotonPoseEstimatorTest, LowestAmbiguityStrategy) {
photonlib::PhotonCamera cameraOne = photonlib::PhotonCamera("test");
wpi::SmallVector<photonlib::PhotonTrackedTarget, 3> targets{
photonlib::PhotonTrackedTarget{
3.0, -4.0, 9.0, 4.0, 0,
frc::Transform3d(frc::Translation3d(1_m, 2_m, 3_m),
frc::Rotation3d(1_rad, 2_rad, 3_rad)),
frc::Transform3d(frc::Translation3d(1_m, 2_m, 3_m),
frc::Rotation3d(1_rad, 2_rad, 3_rad)),
0.7, corners, detectedCorners},
photonlib::PhotonTrackedTarget{
3.0, -4.0, 9.1, 6.7, 1,
frc::Transform3d(frc::Translation3d(4_m, 2_m, 3_m),
frc::Rotation3d(0_rad, 0_rad, 0_rad)),
frc::Transform3d(frc::Translation3d(4_m, 2_m, 3_m),
frc::Rotation3d(0_rad, 0_rad, 0_rad)),
0.3, corners, detectedCorners},
photonlib::PhotonTrackedTarget{
9.0, -2.0, 19.0, 3.0, 0,
frc::Transform3d(frc::Translation3d(1_m, 2_m, 3_m),
frc::Rotation3d(1_rad, 2_rad, 3_rad)),
frc::Transform3d(frc::Translation3d(1_m, 2_m, 3_m),
frc::Rotation3d(1_rad, 2_rad, 3_rad)),
0.4, corners, detectedCorners}};
cameraOne.test = true;
cameraOne.testResult = {2_ms, targets};
cameraOne.testResult.SetTimestamp(units::second_t(11));
photonlib::PhotonPoseEstimator estimator(
aprilTags, photonlib::LOWEST_AMBIGUITY, std::move(cameraOne), {});
auto estimatedPose = estimator.Update();
frc::Pose3d pose = estimatedPose.value().estimatedPose;
EXPECT_NEAR(11, units::unit_cast<double>(estimatedPose.value().timestamp),
.02);
EXPECT_NEAR(1, units::unit_cast<double>(pose.X()), .01);
EXPECT_NEAR(3, units::unit_cast<double>(pose.Y()), .01);
EXPECT_NEAR(2, units::unit_cast<double>(pose.Z()), .01);
}
TEST(PhotonPoseEstimatorTest, ClosestToCameraHeightStrategy) {
std::vector<frc::AprilTag> tags = {
{0, frc::Pose3d(units::meter_t(3), units::meter_t(3), units::meter_t(3),
frc::Rotation3d())},
{1, frc::Pose3d(units::meter_t(5), units::meter_t(5), units::meter_t(5),
frc::Rotation3d())},
};
auto aprilTags = frc::AprilTagFieldLayout(tags, 54_ft, 27_ft);
std::vector<std::pair<photonlib::PhotonCamera, frc::Transform3d>> cameras;
photonlib::PhotonCamera cameraOne = photonlib::PhotonCamera("test");
// ID 0 at 3,3,3
// ID 1 at 5,5,5
wpi::SmallVector<photonlib::PhotonTrackedTarget, 3> targets{
photonlib::PhotonTrackedTarget{
3.0, -4.0, 9.0, 4.0, 1,
frc::Transform3d(frc::Translation3d(0_m, 0_m, 0_m),
frc::Rotation3d(0_rad, 0_rad, 0_rad)),
frc::Transform3d(frc::Translation3d(1_m, 1_m, 1_m),
frc::Rotation3d(0_rad, 0_rad, 0_rad)),
0.7, corners, detectedCorners},
photonlib::PhotonTrackedTarget{
3.0, -4.0, 9.1, 6.7, 1,
frc::Transform3d(frc::Translation3d(2_m, 2_m, 2_m),
frc::Rotation3d(0_rad, 0_rad, 0_rad)),
frc::Transform3d(frc::Translation3d(3_m, 3_m, 3_m),
frc::Rotation3d(0_rad, 0_rad, 0_rad)),
0.3, corners, detectedCorners},
photonlib::PhotonTrackedTarget{
9.0, -2.0, 19.0, 3.0, 0,
frc::Transform3d(frc::Translation3d(4_m, 4_m, 4_m),
frc::Rotation3d(0_rad, 0_rad, 0_rad)),
frc::Transform3d(frc::Translation3d(5_m, 5_m, 5_m),
frc::Rotation3d(0_rad, 0_rad, 0_rad)),
0.4, corners, detectedCorners}};
cameraOne.test = true;
cameraOne.testResult = {2_ms, targets};
cameraOne.testResult.SetTimestamp(17_s);
photonlib::PhotonPoseEstimator estimator(
aprilTags, photonlib::CLOSEST_TO_CAMERA_HEIGHT, std::move(cameraOne),
{{0_m, 0_m, 4_m}, {}});
auto estimatedPose = estimator.Update();
frc::Pose3d pose = estimatedPose.value().estimatedPose;
EXPECT_NEAR(17, units::unit_cast<double>(estimatedPose.value().timestamp),
.02);
EXPECT_NEAR(4, units::unit_cast<double>(pose.X()), .01);
EXPECT_NEAR(4, units::unit_cast<double>(pose.Y()), .01);
EXPECT_NEAR(0, units::unit_cast<double>(pose.Z()), .01);
}
TEST(PhotonPoseEstimatorTest, ClosestToReferencePoseStrategy) {
photonlib::PhotonCamera cameraOne = photonlib::PhotonCamera("test");
wpi::SmallVector<photonlib::PhotonTrackedTarget, 3> targets{
photonlib::PhotonTrackedTarget{
3.0, -4.0, 9.0, 4.0, 1,
frc::Transform3d(frc::Translation3d(0_m, 0_m, 0_m),
frc::Rotation3d(0_rad, 0_rad, 0_rad)),
frc::Transform3d(frc::Translation3d(1_m, 1_m, 1_m),
frc::Rotation3d(0_rad, 0_rad, 0_rad)),
0.7, corners, detectedCorners},
photonlib::PhotonTrackedTarget{
3.0, -4.0, 9.1, 6.7, 1,
frc::Transform3d(frc::Translation3d(2_m, 2_m, 2_m),
frc::Rotation3d(0_rad, 0_rad, 0_rad)),
frc::Transform3d(frc::Translation3d(3_m, 3_m, 3_m),
frc::Rotation3d(0_rad, 0_rad, 0_rad)),
0.3, corners, detectedCorners},
photonlib::PhotonTrackedTarget{
9.0, -2.0, 19.0, 3.0, 0,
frc::Transform3d(frc::Translation3d(2.2_m, 2.2_m, 2.2_m),
frc::Rotation3d(0_rad, 0_rad, 0_rad)),
frc::Transform3d(frc::Translation3d(2_m, 1.9_m, 2.1_m),
frc::Rotation3d(0_rad, 0_rad, 0_rad)),
0.4, corners, detectedCorners}};
cameraOne.test = true;
cameraOne.testResult = {2_ms, targets};
cameraOne.testResult.SetTimestamp(units::second_t(17));
photonlib::PhotonPoseEstimator estimator(aprilTags,
photonlib::CLOSEST_TO_REFERENCE_POSE,
std::move(cameraOne), {});
estimator.SetReferencePose(
frc::Pose3d(1_m, 1_m, 1_m, frc::Rotation3d(0_rad, 0_rad, 0_rad)));
auto estimatedPose = estimator.Update();
frc::Pose3d pose = estimatedPose.value().estimatedPose;
EXPECT_NEAR(17, units::unit_cast<double>(estimatedPose.value().timestamp),
.01);
EXPECT_NEAR(1, units::unit_cast<double>(pose.X()), .01);
EXPECT_NEAR(1.1, units::unit_cast<double>(pose.Y()), .01);
EXPECT_NEAR(.9, units::unit_cast<double>(pose.Z()), .01);
}
TEST(PhotonPoseEstimatorTest, ClosestToLastPose) {
photonlib::PhotonCamera cameraOne = photonlib::PhotonCamera("test");
wpi::SmallVector<photonlib::PhotonTrackedTarget, 3> targets{
photonlib::PhotonTrackedTarget{
3.0, -4.0, 9.0, 4.0, 1,
frc::Transform3d(frc::Translation3d(0_m, 0_m, 0_m),
frc::Rotation3d(0_rad, 0_rad, 0_rad)),
frc::Transform3d(frc::Translation3d(1_m, 1_m, 1_m),
frc::Rotation3d(0_rad, 0_rad, 0_rad)),
0.7, corners, detectedCorners},
photonlib::PhotonTrackedTarget{
3.0, -4.0, 9.1, 6.7, 1,
frc::Transform3d(frc::Translation3d(2_m, 2_m, 2_m),
frc::Rotation3d(0_rad, 0_rad, 0_rad)),
frc::Transform3d(frc::Translation3d(3_m, 3_m, 3_m),
frc::Rotation3d(0_rad, 0_rad, 0_rad)),
0.3, corners, detectedCorners},
photonlib::PhotonTrackedTarget{
9.0, -2.0, 19.0, 3.0, 0,
frc::Transform3d(frc::Translation3d(2.2_m, 2.2_m, 2.2_m),
frc::Rotation3d(0_rad, 0_rad, 0_rad)),
frc::Transform3d(frc::Translation3d(2_m, 1.9_m, 2.1_m),
frc::Rotation3d(0_rad, 0_rad, 0_rad)),
0.4, corners, detectedCorners}};
cameraOne.test = true;
cameraOne.testResult = {2_ms, targets};
cameraOne.testResult.SetTimestamp(units::second_t(17));
photonlib::PhotonPoseEstimator estimator(
aprilTags, photonlib::CLOSEST_TO_LAST_POSE, std::move(cameraOne), {});
estimator.SetLastPose(
frc::Pose3d(1_m, 1_m, 1_m, frc::Rotation3d(0_rad, 0_rad, 0_rad)));
auto estimatedPose = estimator.Update();
frc::Pose3d pose = estimatedPose.value().estimatedPose;
wpi::SmallVector<photonlib::PhotonTrackedTarget, 3> targetsThree{
photonlib::PhotonTrackedTarget{
3.0, -4.0, 9.0, 4.0, 1,
frc::Transform3d(frc::Translation3d(0_m, 0_m, 0_m),
frc::Rotation3d(0_rad, 0_rad, 0_rad)),
frc::Transform3d(frc::Translation3d(1_m, 1_m, 1_m),
frc::Rotation3d(0_rad, 0_rad, 0_rad)),
0.7, corners, detectedCorners},
photonlib::PhotonTrackedTarget{
3.0, -4.0, 9.1, 6.7, 0,
frc::Transform3d(frc::Translation3d(2.1_m, 1.9_m, 2_m),
frc::Rotation3d(0_rad, 0_rad, 0_rad)),
frc::Transform3d(frc::Translation3d(3_m, 3_m, 3_m),
frc::Rotation3d(0_rad, 0_rad, 0_rad)),
0.3, corners, detectedCorners},
photonlib::PhotonTrackedTarget{
9.0, -2.0, 19.0, 3.0, 0,
frc::Transform3d(frc::Translation3d(2.4_m, 2.4_m, 2.2_m),
frc::Rotation3d(0_rad, 0_rad, 0_rad)),
frc::Transform3d(frc::Translation3d(2_m, 1_m, 2_m),
frc::Rotation3d(0_rad, 0_rad, 0_rad)),
0.4, corners, detectedCorners}};
estimator.GetCamera().testResult = {2_ms, targetsThree};
estimator.GetCamera().testResult.SetTimestamp(units::second_t(7));
estimatedPose = estimator.Update();
pose = estimatedPose.value().estimatedPose;
EXPECT_NEAR(7.0, units::unit_cast<double>(estimatedPose.value().timestamp),
.01);
EXPECT_NEAR(.9, units::unit_cast<double>(pose.X()), .01);
EXPECT_NEAR(1.1, units::unit_cast<double>(pose.Y()), .01);
EXPECT_NEAR(1, units::unit_cast<double>(pose.Z()), .01);
}
TEST(PhotonPoseEstimatorTest, AverageBestPoses) {
photonlib::PhotonCamera cameraOne = photonlib::PhotonCamera("test");
wpi::SmallVector<photonlib::PhotonTrackedTarget, 3> targets{
photonlib::PhotonTrackedTarget{
3.0, -4.0, 9.0, 4.0, 0,
frc::Transform3d(frc::Translation3d(2_m, 2_m, 2_m),
frc::Rotation3d(0_rad, 0_rad, 0_rad)),
frc::Transform3d(frc::Translation3d(1_m, 1_m, 1_m),
frc::Rotation3d(0_rad, 0_rad, 0_rad)),
0.7, corners, detectedCorners},
photonlib::PhotonTrackedTarget{
3.0, -4.0, 9.1, 6.7, 1,
frc::Transform3d(frc::Translation3d(3_m, 3_m, 3_m),
frc::Rotation3d(0_rad, 0_rad, 0_rad)),
frc::Transform3d(frc::Translation3d(3_m, 3_m, 3_m),
frc::Rotation3d(0_rad, 0_rad, 0_rad)),
0.3, corners, detectedCorners},
photonlib::PhotonTrackedTarget{
9.0, -2.0, 19.0, 3.0, 0,
frc::Transform3d(frc::Translation3d(0_m, 0_m, 0_m),
frc::Rotation3d(0_rad, 0_rad, 0_rad)),
frc::Transform3d(frc::Translation3d(2_m, 1.9_m, 2.1_m),
frc::Rotation3d(0_rad, 0_rad, 0_rad)),
0.4, corners, detectedCorners}};
cameraOne.test = true;
cameraOne.testResult = {2_ms, targets};
cameraOne.testResult.SetTimestamp(units::second_t(15));
photonlib::PhotonPoseEstimator estimator(
aprilTags, photonlib::AVERAGE_BEST_TARGETS, std::move(cameraOne), {});
auto estimatedPose = estimator.Update();
frc::Pose3d pose = estimatedPose.value().estimatedPose;
EXPECT_NEAR(15.0, units::unit_cast<double>(estimatedPose.value().timestamp),
.01);
EXPECT_NEAR(2.15, units::unit_cast<double>(pose.X()), .01);
EXPECT_NEAR(2.15, units::unit_cast<double>(pose.Y()), .01);
EXPECT_NEAR(2.15, units::unit_cast<double>(pose.Z()), .01);
}

View File

@@ -40,6 +40,11 @@
#include "photonlib/PhotonTrackedTarget.h"
#include "photonlib/RobotPoseEstimator.h"
static wpi::SmallVector<std::pair<double, double>, 4> corners{
std::pair{1, 2}, std::pair{3, 4}, std::pair{5, 6}, std::pair{7, 8}};
static std::vector<std::pair<double, double>> detectedCorners{
std::pair{1, 2}, std::pair{3, 4}, std::pair{5, 6}, std::pair{7, 8}};
TEST(RobotPoseEstimatorTest, LowestAmbiguityStrategy) {
std::vector<frc::AprilTag> tags = {
{0, frc::Pose3d(units::meter_t(3), units::meter_t(3), units::meter_t(3),
@@ -61,51 +66,36 @@ TEST(RobotPoseEstimatorTest, LowestAmbiguityStrategy) {
wpi::SmallVector<photonlib::PhotonTrackedTarget, 2> targets{
photonlib::PhotonTrackedTarget{
3.0,
-4.0,
9.0,
4.0,
0,
3.0, -4.0, 9.0, 4.0, 0,
frc::Transform3d(frc::Translation3d(1_m, 2_m, 3_m),
frc::Rotation3d(1_rad, 2_rad, 3_rad)),
frc::Transform3d(frc::Translation3d(1_m, 2_m, 3_m),
frc::Rotation3d(1_rad, 2_rad, 3_rad)),
0.7,
{std::pair{1, 2}, std::pair{3, 4}, std::pair{5, 6}, std::pair{7, 8}}},
0.7, corners, detectedCorners},
photonlib::PhotonTrackedTarget{
3.0,
-4.0,
9.1,
6.7,
1,
3.0, -4.0, 9.1, 6.7, 1,
frc::Transform3d(frc::Translation3d(4_m, 2_m, 3_m),
frc::Rotation3d(0_rad, 0_rad, 0_rad)),
frc::Transform3d(frc::Translation3d(4_m, 2_m, 3_m),
frc::Rotation3d(0_rad, 0_rad, 0_rad)),
0.3,
{std::pair{1, 2}, std::pair{3, 4}, std::pair{5, 6},
std::pair{7, 8}}}};
0.3, corners, detectedCorners}};
cameraOne->test = true;
cameraOne->testResult = {2_s, targets};
cameraOne->testResult.SetTimestamp(units::second_t(11));
wpi::SmallVector<photonlib::PhotonTrackedTarget, 1> targetsTwo{
photonlib::PhotonTrackedTarget{
9.0,
-2.0,
19.0,
3.0,
0,
9.0, -2.0, 19.0, 3.0, 0,
frc::Transform3d(frc::Translation3d(1_m, 2_m, 3_m),
frc::Rotation3d(1_rad, 2_rad, 3_rad)),
frc::Transform3d(frc::Translation3d(1_m, 2_m, 3_m),
frc::Rotation3d(1_rad, 2_rad, 3_rad)),
0.4,
{std::pair{1, 2}, std::pair{3, 4}, std::pair{5, 6},
std::pair{7, 8}}}};
0.4, corners, detectedCorners}};
cameraTwo->test = true;
cameraTwo->testResult = {4_s, targetsTwo};
cameraTwo->testResult.SetTimestamp(units::second_t(units::second_t(16)));
cameras.push_back(std::make_pair(
cameraOne, frc::Transform3d(frc::Translation3d(0_m, 0_m, 0_m),
@@ -119,7 +109,7 @@ TEST(RobotPoseEstimatorTest, LowestAmbiguityStrategy) {
estimator.Update();
frc::Pose3d pose = estimatedPose.first;
EXPECT_NEAR(2, units::unit_cast<double>(estimatedPose.second), .01);
EXPECT_NEAR(11, units::unit_cast<double>(estimatedPose.second) / 1000.0, .01);
EXPECT_NEAR(1, units::unit_cast<double>(pose.X()), .01);
EXPECT_NEAR(3, units::unit_cast<double>(pose.Y()), .01);
EXPECT_NEAR(2, units::unit_cast<double>(pose.Z()), .01);
@@ -146,51 +136,36 @@ TEST(RobotPoseEstimatorTest, ClosestToCameraHeightStrategy) {
wpi::SmallVector<photonlib::PhotonTrackedTarget, 2> targets{
photonlib::PhotonTrackedTarget{
3.0,
-4.0,
9.0,
4.0,
1,
3.0, -4.0, 9.0, 4.0, 1,
frc::Transform3d(frc::Translation3d(0_m, 0_m, 0_m),
frc::Rotation3d(0_rad, 0_rad, 0_rad)),
frc::Transform3d(frc::Translation3d(1_m, 1_m, 1_m),
frc::Rotation3d(0_rad, 0_rad, 0_rad)),
0.7,
{std::pair{1, 2}, std::pair{3, 4}, std::pair{5, 6}, std::pair{7, 8}}},
0.7, corners, detectedCorners},
photonlib::PhotonTrackedTarget{
3.0,
-4.0,
9.1,
6.7,
1,
3.0, -4.0, 9.1, 6.7, 1,
frc::Transform3d(frc::Translation3d(2_m, 2_m, 2_m),
frc::Rotation3d(0_rad, 0_rad, 0_rad)),
frc::Transform3d(frc::Translation3d(3_m, 3_m, 3_m),
frc::Rotation3d(0_rad, 0_rad, 0_rad)),
0.3,
{std::pair{1, 2}, std::pair{3, 4}, std::pair{5, 6},
std::pair{7, 8}}}};
0.3, corners, detectedCorners}};
cameraOne->test = true;
cameraOne->testResult = {2_s, targets};
cameraOne->testResult.SetTimestamp(units::second_t(4));
wpi::SmallVector<photonlib::PhotonTrackedTarget, 1> targetsTwo{
photonlib::PhotonTrackedTarget{
9.0,
-2.0,
19.0,
3.0,
0,
9.0, -2.0, 19.0, 3.0, 0,
frc::Transform3d(frc::Translation3d(4_m, 4_m, 4_m),
frc::Rotation3d(0_rad, 0_rad, 0_rad)),
frc::Transform3d(frc::Translation3d(5_m, 5_m, 5_m),
frc::Rotation3d(0_rad, 0_rad, 0_rad)),
0.4,
{std::pair{1, 2}, std::pair{3, 4}, std::pair{5, 6},
std::pair{7, 8}}}};
0.4, corners, detectedCorners}};
cameraTwo->test = true;
cameraTwo->testResult = {4_s, targetsTwo};
cameraOne->testResult.SetTimestamp(units::second_t(12));
cameras.push_back(std::make_pair(
cameraOne, frc::Transform3d(frc::Translation3d(0_m, 0_m, 4_m),
@@ -204,7 +179,7 @@ TEST(RobotPoseEstimatorTest, ClosestToCameraHeightStrategy) {
estimator.Update();
frc::Pose3d pose = estimatedPose.first;
EXPECT_NEAR(2, units::unit_cast<double>(estimatedPose.second), .01);
EXPECT_NEAR(12, units::unit_cast<double>(estimatedPose.second) / 1000.0, .01);
EXPECT_NEAR(4, units::unit_cast<double>(pose.X()), .01);
EXPECT_NEAR(4, units::unit_cast<double>(pose.Y()), .01);
EXPECT_NEAR(4, units::unit_cast<double>(pose.Z()), .01);
@@ -231,51 +206,36 @@ TEST(RobotPoseEstimatorTest, ClosestToReferencePoseStrategy) {
wpi::SmallVector<photonlib::PhotonTrackedTarget, 2> targets{
photonlib::PhotonTrackedTarget{
3.0,
-4.0,
9.0,
4.0,
1,
3.0, -4.0, 9.0, 4.0, 1,
frc::Transform3d(frc::Translation3d(0_m, 0_m, 0_m),
frc::Rotation3d(0_rad, 0_rad, 0_rad)),
frc::Transform3d(frc::Translation3d(1_m, 1_m, 1_m),
frc::Rotation3d(0_rad, 0_rad, 0_rad)),
0.7,
{std::pair{1, 2}, std::pair{3, 4}, std::pair{5, 6}, std::pair{7, 8}}},
0.7, corners, detectedCorners},
photonlib::PhotonTrackedTarget{
3.0,
-4.0,
9.1,
6.7,
1,
3.0, -4.0, 9.1, 6.7, 1,
frc::Transform3d(frc::Translation3d(2_m, 2_m, 2_m),
frc::Rotation3d(0_rad, 0_rad, 0_rad)),
frc::Transform3d(frc::Translation3d(3_m, 3_m, 3_m),
frc::Rotation3d(0_rad, 0_rad, 0_rad)),
0.3,
{std::pair{1, 2}, std::pair{3, 4}, std::pair{5, 6},
std::pair{7, 8}}}};
0.3, corners, detectedCorners}};
cameraOne->test = true;
cameraOne->testResult = {2_s, targets};
cameraOne->testResult.SetTimestamp(units::second_t(4));
wpi::SmallVector<photonlib::PhotonTrackedTarget, 1> targetsTwo{
photonlib::PhotonTrackedTarget{
9.0,
-2.0,
19.0,
3.0,
0,
9.0, -2.0, 19.0, 3.0, 0,
frc::Transform3d(frc::Translation3d(2.2_m, 2.2_m, 2.2_m),
frc::Rotation3d(0_rad, 0_rad, 0_rad)),
frc::Transform3d(frc::Translation3d(2_m, 1.9_m, 2.1_m),
frc::Rotation3d(0_rad, 0_rad, 0_rad)),
0.4,
{std::pair{1, 2}, std::pair{3, 4}, std::pair{5, 6},
std::pair{7, 8}}}};
0.4, corners, detectedCorners}};
cameraTwo->test = true;
cameraTwo->testResult = {4_s, targetsTwo};
cameraTwo->testResult.SetTimestamp(units::second_t(17));
cameras.push_back(std::make_pair(
cameraOne, frc::Transform3d(frc::Translation3d(0_m, 0_m, 0_m),
@@ -291,7 +251,7 @@ TEST(RobotPoseEstimatorTest, ClosestToReferencePoseStrategy) {
estimator.Update();
frc::Pose3d pose = estimatedPose.first;
EXPECT_NEAR(4, units::unit_cast<double>(estimatedPose.second), .01);
EXPECT_NEAR(17, units::unit_cast<double>(estimatedPose.second) / 1000.0, .01);
EXPECT_NEAR(1, units::unit_cast<double>(pose.X()), .01);
EXPECT_NEAR(1.1, units::unit_cast<double>(pose.Y()), .01);
EXPECT_NEAR(.9, units::unit_cast<double>(pose.Z()), .01);
@@ -317,48 +277,31 @@ TEST(RobotPoseEstimatorTest, ClosestToLastPose) {
wpi::SmallVector<photonlib::PhotonTrackedTarget, 2> targets{
photonlib::PhotonTrackedTarget{
3.0,
-4.0,
9.0,
4.0,
1,
3.0, -4.0, 9.0, 4.0, 1,
frc::Transform3d(frc::Translation3d(0_m, 0_m, 0_m),
frc::Rotation3d(0_rad, 0_rad, 0_rad)),
frc::Transform3d(frc::Translation3d(1_m, 1_m, 1_m),
frc::Rotation3d(0_rad, 0_rad, 0_rad)),
0.7,
{std::pair{1, 2}, std::pair{3, 4}, std::pair{5, 6}, std::pair{7, 8}}},
0.7, corners, detectedCorners},
photonlib::PhotonTrackedTarget{
3.0,
-4.0,
9.1,
6.7,
1,
3.0, -4.0, 9.1, 6.7, 1,
frc::Transform3d(frc::Translation3d(2_m, 2_m, 2_m),
frc::Rotation3d(0_rad, 0_rad, 0_rad)),
frc::Transform3d(frc::Translation3d(3_m, 3_m, 3_m),
frc::Rotation3d(0_rad, 0_rad, 0_rad)),
0.3,
{std::pair{1, 2}, std::pair{3, 4}, std::pair{5, 6},
std::pair{7, 8}}}};
0.3, corners, detectedCorners}};
cameraOne->test = true;
cameraOne->testResult = {2_s, targets};
wpi::SmallVector<photonlib::PhotonTrackedTarget, 1> targetsTwo{
photonlib::PhotonTrackedTarget{
9.0,
-2.0,
19.0,
3.0,
0,
9.0, -2.0, 19.0, 3.0, 0,
frc::Transform3d(frc::Translation3d(2.2_m, 2.2_m, 2.2_m),
frc::Rotation3d(0_rad, 0_rad, 0_rad)),
frc::Transform3d(frc::Translation3d(2_m, 1.9_m, 2.1_m),
frc::Rotation3d(0_rad, 0_rad, 0_rad)),
0.4,
{std::pair{1, 2}, std::pair{3, 4}, std::pair{5, 6},
std::pair{7, 8}}}};
0.4, corners, detectedCorners}};
cameraTwo->test = true;
cameraTwo->testResult = {4_s, targetsTwo};
@@ -379,49 +322,34 @@ TEST(RobotPoseEstimatorTest, ClosestToLastPose) {
wpi::SmallVector<photonlib::PhotonTrackedTarget, 2> targetsThree{
photonlib::PhotonTrackedTarget{
3.0,
-4.0,
9.0,
4.0,
1,
3.0, -4.0, 9.0, 4.0, 1,
frc::Transform3d(frc::Translation3d(0_m, 0_m, 0_m),
frc::Rotation3d(0_rad, 0_rad, 0_rad)),
frc::Transform3d(frc::Translation3d(1_m, 1_m, 1_m),
frc::Rotation3d(0_rad, 0_rad, 0_rad)),
0.7,
{std::pair{1, 2}, std::pair{3, 4}, std::pair{5, 6}, std::pair{7, 8}}},
0.7, corners, detectedCorners},
photonlib::PhotonTrackedTarget{
3.0,
-4.0,
9.1,
6.7,
0,
3.0, -4.0, 9.1, 6.7, 0,
frc::Transform3d(frc::Translation3d(2.1_m, 1.9_m, 2_m),
frc::Rotation3d(0_rad, 0_rad, 0_rad)),
frc::Transform3d(frc::Translation3d(3_m, 3_m, 3_m),
frc::Rotation3d(0_rad, 0_rad, 0_rad)),
0.3,
{std::pair{1, 2}, std::pair{3, 4}, std::pair{5, 6},
std::pair{7, 8}}}};
0.3, corners, detectedCorners}};
cameraOne->testResult = {2_s, targetsThree};
cameraOne->testResult.SetTimestamp(units::second_t(7));
wpi::SmallVector<photonlib::PhotonTrackedTarget, 1> targetsFour{
photonlib::PhotonTrackedTarget{
9.0,
-2.0,
19.0,
3.0,
0,
9.0, -2.0, 19.0, 3.0, 0,
frc::Transform3d(frc::Translation3d(2.4_m, 2.4_m, 2.2_m),
frc::Rotation3d(0_rad, 0_rad, 0_rad)),
frc::Transform3d(frc::Translation3d(2_m, 1_m, 2_m),
frc::Rotation3d(0_rad, 0_rad, 0_rad)),
0.4,
{std::pair{1, 2}, std::pair{3, 4}, std::pair{5, 6},
std::pair{7, 8}}}};
0.4, corners, detectedCorners}};
cameraTwo->testResult = {4_s, targetsFour};
cameraTwo->testResult.SetTimestamp(units::second_t(13));
std::vector<
std::pair<std::shared_ptr<photonlib::PhotonCamera>, frc::Transform3d>>
@@ -436,7 +364,8 @@ TEST(RobotPoseEstimatorTest, ClosestToLastPose) {
estimatedPose = estimator.Update();
pose = estimatedPose.first;
EXPECT_NEAR(2, units::unit_cast<double>(estimatedPose.second), .01);
EXPECT_NEAR(7.0, units::unit_cast<double>(estimatedPose.second) / 1000.0,
.01);
EXPECT_NEAR(.9, units::unit_cast<double>(pose.X()), .01);
EXPECT_NEAR(1.1, units::unit_cast<double>(pose.Y()), .01);
EXPECT_NEAR(1, units::unit_cast<double>(pose.Z()), .01);
@@ -462,51 +391,36 @@ TEST(RobotPoseEstimatorTest, AverageBestPoses) {
wpi::SmallVector<photonlib::PhotonTrackedTarget, 2> targets{
photonlib::PhotonTrackedTarget{
3.0,
-4.0,
9.0,
4.0,
0,
3.0, -4.0, 9.0, 4.0, 0,
frc::Transform3d(frc::Translation3d(2_m, 2_m, 2_m),
frc::Rotation3d(0_rad, 0_rad, 0_rad)),
frc::Transform3d(frc::Translation3d(1_m, 1_m, 1_m),
frc::Rotation3d(0_rad, 0_rad, 0_rad)),
0.7,
{std::pair{1, 2}, std::pair{3, 4}, std::pair{5, 6}, std::pair{7, 8}}},
0.7, corners, detectedCorners},
photonlib::PhotonTrackedTarget{
3.0,
-4.0,
9.1,
6.7,
1,
3.0, -4.0, 9.1, 6.7, 1,
frc::Transform3d(frc::Translation3d(3_m, 3_m, 3_m),
frc::Rotation3d(0_rad, 0_rad, 0_rad)),
frc::Transform3d(frc::Translation3d(3_m, 3_m, 3_m),
frc::Rotation3d(0_rad, 0_rad, 0_rad)),
0.3,
{std::pair{1, 2}, std::pair{3, 4}, std::pair{5, 6},
std::pair{7, 8}}}};
0.3, corners, detectedCorners}};
cameraOne->test = true;
cameraOne->testResult = {2_s, targets};
cameraOne->testResult.SetTimestamp(units::second_t(10));
wpi::SmallVector<photonlib::PhotonTrackedTarget, 1> targetsTwo{
photonlib::PhotonTrackedTarget{
9.0,
-2.0,
19.0,
3.0,
0,
9.0, -2.0, 19.0, 3.0, 0,
frc::Transform3d(frc::Translation3d(0_m, 0_m, 0_m),
frc::Rotation3d(0_rad, 0_rad, 0_rad)),
frc::Transform3d(frc::Translation3d(2_m, 1.9_m, 2.1_m),
frc::Rotation3d(0_rad, 0_rad, 0_rad)),
0.4,
{std::pair{1, 2}, std::pair{3, 4}, std::pair{5, 6},
std::pair{7, 8}}}};
0.4, corners, detectedCorners}};
cameraTwo->test = true;
cameraTwo->testResult = {4_s, targetsTwo};
cameraTwo->testResult.SetTimestamp(units::second_t(20));
cameras.push_back(std::make_pair(
cameraOne, frc::Transform3d(frc::Translation3d(0_m, 0_m, 0_m),
@@ -520,8 +434,8 @@ TEST(RobotPoseEstimatorTest, AverageBestPoses) {
estimator.Update();
frc::Pose3d pose = estimatedPose.first;
EXPECT_NEAR(2.6885245901639347,
units::unit_cast<double>(estimatedPose.second), .01);
EXPECT_NEAR(15.0, units::unit_cast<double>(estimatedPose.second) / 1000.0,
.01);
EXPECT_NEAR(2.15, units::unit_cast<double>(pose.X()), .01);
EXPECT_NEAR(2.15, units::unit_cast<double>(pose.Y()), .01);
EXPECT_NEAR(2.15, units::unit_cast<double>(pose.Z()), .01);

Binary file not shown.

View File

@@ -169,24 +169,6 @@ public class Main {
private static void addTestModeSources() {
ConfigManager.getInstance().load();
var camConfApril =
ConfigManager.getInstance().getConfig().getCameraConfigurations().get("Apriltag");
if (camConfApril == null) {
camConfApril =
new CameraConfiguration("Apriltag", TestUtils.getTestModeApriltagPath().toString());
camConfApril.FOV = TestUtils.WPI2019Image.FOV;
camConfApril.calibrations.add(TestUtils.get2019LifeCamCoeffs(true));
var pipeline2019 = new AprilTagPipelineSettings();
pipeline2019.pipelineNickname = "Robots";
pipeline2019.outputShowMultipleTargets = true;
pipeline2019.inputShouldShow = true;
var psList2019 = new ArrayList<CVPipelineSettings>();
psList2019.add(pipeline2019);
camConfApril.pipelineSettings = psList2019;
}
var camConf2019 =
ConfigManager.getInstance().getConfig().getCameraConfigurations().get("WPI2019");
if (camConf2019 == null) {
@@ -245,6 +227,32 @@ public class Main {
camConf2022.pipelineSettings = psList2022;
}
CameraConfiguration camConf2023 =
ConfigManager.getInstance().getConfig().getCameraConfigurations().get("WPI2023");
if (camConf2023 == null) {
camConf2023 =
new CameraConfiguration(
"WPI2023",
TestUtils.getResourcesFolderPath(true)
.resolve("testimages")
.resolve(TestUtils.WPI2023Apriltags.k383_60_Angle2.path)
.toString());
camConf2023.FOV = TestUtils.WPI2023Apriltags.FOV;
camConf2023.calibrations.add(TestUtils.get2023LifeCamCoeffs(true));
var pipeline2023 = new AprilTagPipelineSettings();
var path_split = Path.of(camConf2023.path).getFileName().toString();
pipeline2023.pipelineNickname = path_split.replace(".png", "");
pipeline2023.targetModel = TargetModel.k6in_16h5;
pipeline2023.inputShouldShow = true;
pipeline2023.solvePNPEnabled = true;
var psList2023 = new ArrayList<CVPipelineSettings>();
psList2023.add(pipeline2023);
camConf2023.pipelineSettings = psList2023;
}
// Colored shape testing
var camConfShape =
ConfigManager.getInstance().getConfig().getCameraConfigurations().get("Shape");
@@ -269,13 +277,13 @@ public class Main {
var collectedSources = new ArrayList<VisionSource>();
var fvsApril = new FileVisionSource(camConfApril);
var fvsShape = new FileVisionSource(camConfShape);
var fvs2019 = new FileVisionSource(camConf2019);
var fvs2020 = new FileVisionSource(camConf2020);
var fvs2022 = new FileVisionSource(camConf2022);
var fvs2023 = new FileVisionSource(camConf2023);
collectedSources.add(fvsApril);
collectedSources.add(fvs2023);
collectedSources.add(fvs2022);
collectedSources.add(fvsShape);
collectedSources.add(fvs2020);

View File

@@ -18,6 +18,7 @@
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.http.Context;
import java.io.File;
@@ -33,6 +34,9 @@ import java.util.Map;
import org.apache.commons.io.FileUtils;
import org.photonvision.common.configuration.ConfigManager;
import org.photonvision.common.configuration.NetworkConfig;
import org.photonvision.common.dataflow.DataChangeDestination;
import org.photonvision.common.dataflow.DataChangeService;
import org.photonvision.common.dataflow.events.IncomingWebSocketEvent;
import org.photonvision.common.dataflow.networktables.NetworkTablesManager;
import org.photonvision.common.hardware.HardwareManager;
import org.photonvision.common.hardware.Platform;
@@ -42,6 +46,7 @@ import org.photonvision.common.networking.NetworkManager;
import org.photonvision.common.util.ShellExec;
import org.photonvision.common.util.TimedTaskManager;
import org.photonvision.common.util.file.ProgramDirectoryUtilities;
import org.photonvision.vision.calibration.CameraCalibrationCoefficients;
import org.photonvision.vision.processes.VisionModuleManager;
import org.photonvision.vision.target.TargetModel;
@@ -274,6 +279,44 @@ public class RequestHandler {
}
}
public static void importCalibrationFromCalibdb(Context ctx) {
var file = ctx.body();
if (file != null) {
// check if it's a JSON file
// Load using Jackson
try {
ObjectMapper mapper = new ObjectMapper();
JsonNode actualObj = mapper.readTree(file);
int cameraIndex = actualObj.get("cameraIndex").asInt();
String filename = actualObj.get("filename").asText();
var payload = mapper.readTree(actualObj.get("payload").asText());
var coeffs = CameraCalibrationCoefficients.parseFromCalibdbJson(payload);
var uploadCalibrationEvent =
new IncomingWebSocketEvent<CameraCalibrationCoefficients>(
DataChangeDestination.DCD_ACTIVEMODULE,
"calibrationUploaded",
coeffs,
(Integer) cameraIndex,
null);
DataChangeService.getInstance().publishEvent(uploadCalibrationEvent);
ctx.status(200);
logger.info("Calibration added!");
} catch (Exception e) {
logger.warn("Could not parse cal metaJSON!");
e.printStackTrace();
return;
}
} else {
ctx.status(500);
return;
}
}
public static void setCameraNickname(Context ctx) {
try {
var data = kObjectMapper.readValue(ctx.body(), HashMap.class);
@@ -304,7 +347,8 @@ public class RequestHandler {
public static void sendMetrics(Context ctx) {
HardwareManager.getInstance().publishMetrics();
// TimedTaskManager.getInstance().addOneShotTask(() -> RoborioFinder.getInstance().findRios(),
// TimedTaskManager.getInstance().addOneShotTask(() ->
// RoborioFinder.getInstance().findRios(),
// 0);
ctx.status(200);
}

View File

@@ -93,6 +93,7 @@ public class Server {
app.post("api/vision/pnpModel", RequestHandler::uploadPnpModel);
app.post("api/sendMetrics", RequestHandler::sendMetrics);
app.post("api/setCameraNickname", RequestHandler::setCameraNickname);
app.post("api/calibration/import", RequestHandler::importCalibrationFromCalibdb);
app.start(port);
}

View File

@@ -26,6 +26,7 @@ import edu.wpi.first.networktables.IntegerPublisher;
import edu.wpi.first.networktables.IntegerSubscriber;
import edu.wpi.first.networktables.IntegerTopic;
import edu.wpi.first.networktables.NetworkTable;
import edu.wpi.first.networktables.PubSubOption;
import edu.wpi.first.networktables.RawPublisher;
/**
@@ -65,15 +66,20 @@ public class NTTopicSet {
public IntegerPublisher heartbeatPublisher;
public void updateEntries() {
rawBytesEntry = subTable.getRawTopic("rawBytes").publish("rawBytes");
rawBytesEntry =
subTable
.getRawTopic("rawBytes")
.publish("rawBytes", PubSubOption.periodic(0.01), PubSubOption.sendAll(true));
pipelineIndexTopic = subTable.getIntegerTopic("pipelineIndex");
pipelineIndexPublisher = pipelineIndexTopic.publish();
pipelineIndexSubscriber = pipelineIndexTopic.subscribe(0);
driverModeEntry = subTable.getBooleanTopic("driverMode");
driverModePublisher = driverModeEntry.publish();
driverModeSubscriber = driverModeEntry.subscribe(false);
driverModePublisher = subTable.getBooleanTopic("driverMode").publish();
driverModeSubscriber = subTable.getBooleanTopic("driverModeRequest").subscribe(false);
// Fun little hack to make the request show up
driverModeSubscriber.getTopic().publish().setDefault(false);
latencyMillisEntry = subTable.getDoubleTopic("latencyMillis").publish();
hasTargetEntry = subTable.getBooleanTopic("hasTarget").publish();

View File

@@ -23,11 +23,12 @@ import edu.wpi.first.math.geometry.Transform3d;
import edu.wpi.first.math.geometry.Translation3d;
import java.util.ArrayList;
import java.util.List;
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 + 7);
private static final int MAX_CORNERS = 8;
public static final int PACK_SIZE_BYTES =
Double.BYTES * (5 + 7 + 2 * 4 + 1 + 7 + 2 * MAX_CORNERS);
private double yaw;
private double pitch;
@@ -37,7 +38,12 @@ public class PhotonTrackedTarget {
private Transform3d bestCameraToTarget = new Transform3d();
private Transform3d altCameraToTarget = new Transform3d();
private double poseAmbiguity;
private List<TargetCorner> targetCorners;
// Corners from the min-area rectangle bounding the target
private List<TargetCorner> minAreaRectCorners;
// Corners from whatever corner detection method was used
private List<TargetCorner> detectedCorners;
public PhotonTrackedTarget() {}
@@ -51,8 +57,14 @@ public class PhotonTrackedTarget {
Transform3d pose,
Transform3d altPose,
double ambiguity,
List<TargetCorner> corners) {
assert corners.size() == 4;
List<TargetCorner> minAreaRectCorners,
List<TargetCorner> detectedCorners) {
assert minAreaRectCorners.size() == 4;
if (detectedCorners.size() > MAX_CORNERS) {
detectedCorners = detectedCorners.subList(0, MAX_CORNERS);
}
this.yaw = yaw;
this.pitch = pitch;
this.area = area;
@@ -60,7 +72,8 @@ public class PhotonTrackedTarget {
this.fiducialId = id;
this.bestCameraToTarget = pose;
this.altCameraToTarget = altPose;
this.targetCorners = corners;
this.minAreaRectCorners = minAreaRectCorners;
this.detectedCorners = detectedCorners;
this.poseAmbiguity = ambiguity;
}
@@ -94,11 +107,28 @@ public class PhotonTrackedTarget {
}
/**
* Return a list of the 4 corners in image space (origin top left, x left, y down), in no
* Return a list of the 4 corners in image space (origin top left, x right, y down), in no
* particular order, of the minimum area bounding rectangle of this target
*/
public List<TargetCorner> getCorners() {
return targetCorners;
public List<TargetCorner> getMinAreaRectCorners() {
return minAreaRectCorners;
}
/**
* Return a list of the n corners in image space (origin top left, x right, y down), in no
* particular order, detected for this target.
*
* <p>For fiducials, the order is known and is always counter-clock wise around the tag, like so
*
* <p>spotless:off
* -> +X 3 ----- 2
* | | |
* V | |
* +Y 0 ----- 1
* spotless:on
*/
public List<TargetCorner> getDetectedCorners() {
return detectedCorners;
}
/**
@@ -118,21 +148,54 @@ public class PhotonTrackedTarget {
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
PhotonTrackedTarget that = (PhotonTrackedTarget) o;
return Double.compare(that.yaw, yaw) == 0
&& Double.compare(that.pitch, pitch) == 0
&& Double.compare(that.area, area) == 0
&& Objects.equals(bestCameraToTarget, that.bestCameraToTarget)
&& Objects.equals(altCameraToTarget, that.altCameraToTarget)
&& Objects.equals(targetCorners, that.targetCorners);
public int hashCode() {
final int prime = 31;
int result = 1;
long temp;
temp = Double.doubleToLongBits(yaw);
result = prime * result + (int) (temp ^ (temp >>> 32));
temp = Double.doubleToLongBits(pitch);
result = prime * result + (int) (temp ^ (temp >>> 32));
temp = Double.doubleToLongBits(area);
result = prime * result + (int) (temp ^ (temp >>> 32));
temp = Double.doubleToLongBits(skew);
result = prime * result + (int) (temp ^ (temp >>> 32));
result = prime * result + fiducialId;
result = prime * result + ((bestCameraToTarget == null) ? 0 : bestCameraToTarget.hashCode());
result = prime * result + ((altCameraToTarget == null) ? 0 : altCameraToTarget.hashCode());
temp = Double.doubleToLongBits(poseAmbiguity);
result = prime * result + (int) (temp ^ (temp >>> 32));
result = prime * result + ((minAreaRectCorners == null) ? 0 : minAreaRectCorners.hashCode());
result = prime * result + ((detectedCorners == null) ? 0 : detectedCorners.hashCode());
return result;
}
@Override
public int hashCode() {
return Objects.hash(yaw, pitch, area, bestCameraToTarget, altCameraToTarget);
public boolean equals(Object obj) {
if (this == obj) return true;
if (obj == null) return false;
if (getClass() != obj.getClass()) return false;
PhotonTrackedTarget other = (PhotonTrackedTarget) obj;
if (Double.doubleToLongBits(yaw) != Double.doubleToLongBits(other.yaw)) return false;
if (Double.doubleToLongBits(pitch) != Double.doubleToLongBits(other.pitch)) return false;
if (Double.doubleToLongBits(area) != Double.doubleToLongBits(other.area)) return false;
if (Double.doubleToLongBits(skew) != Double.doubleToLongBits(other.skew)) return false;
if (fiducialId != other.fiducialId) return false;
if (bestCameraToTarget == null) {
if (other.bestCameraToTarget != null) return false;
} else if (!bestCameraToTarget.equals(other.bestCameraToTarget)) return false;
if (altCameraToTarget == null) {
if (other.altCameraToTarget != null) return false;
} else if (!altCameraToTarget.equals(other.altCameraToTarget)) return false;
if (Double.doubleToLongBits(poseAmbiguity) != Double.doubleToLongBits(other.poseAmbiguity))
return false;
if (minAreaRectCorners == null) {
if (other.minAreaRectCorners != null) return false;
} else if (!minAreaRectCorners.equals(other.minAreaRectCorners)) return false;
if (detectedCorners == null) {
if (other.detectedCorners != null) return false;
} else if (!detectedCorners.equals(other.detectedCorners)) return false;
return true;
}
private static Transform3d decodeTransform(Packet packet) {
@@ -158,6 +221,25 @@ public class PhotonTrackedTarget {
packet.encode(transform.getRotation().getQuaternion().getZ());
}
private static void encodeList(Packet packet, List<TargetCorner> list) {
packet.encode((byte) Math.min(list.size(), Byte.MAX_VALUE));
for (int i = 0; i < list.size(); i++) {
packet.encode(list.get(i).x);
packet.encode(list.get(i).y);
}
}
private static List<TargetCorner> decodeList(Packet p) {
byte len = p.decodeByte();
var ret = new ArrayList<TargetCorner>();
for (int i = 0; i < len; i++) {
double cx = p.decodeDouble();
double cy = p.decodeDouble();
ret.add(new TargetCorner(cx, cy));
}
return ret;
}
/**
* Populates the fields of this class with information from the incoming packet.
*
@@ -176,13 +258,15 @@ public class PhotonTrackedTarget {
this.poseAmbiguity = packet.decodeDouble();
this.targetCorners = new ArrayList<>(4);
this.minAreaRectCorners = new ArrayList<>(4);
for (int i = 0; i < 4; i++) {
double cx = packet.decodeDouble();
double cy = packet.decodeDouble();
targetCorners.add(new TargetCorner(cx, cy));
minAreaRectCorners.add(new TargetCorner(cx, cy));
}
detectedCorners = decodeList(packet);
return packet;
}
@@ -203,10 +287,12 @@ public class PhotonTrackedTarget {
packet.encode(poseAmbiguity);
for (int i = 0; i < 4; i++) {
packet.encode(targetCorners.get(i).x);
packet.encode(targetCorners.get(i).y);
packet.encode(minAreaRectCorners.get(i).x);
packet.encode(minAreaRectCorners.get(i).y);
}
encodeList(packet, detectedCorners);
return packet;
}
@@ -226,7 +312,7 @@ public class PhotonTrackedTarget {
+ ", cameraToTarget="
+ bestCameraToTarget
+ ", targetCorners="
+ targetCorners
+ minAreaRectCorners
+ '}';
}
}

View File

@@ -1,6 +1,6 @@
{
"enableCppIntellisense": true,
"currentLanguage": "cpp",
"projectYear": "2023Beta",
"projectYear": "2023",
"teamNumber": 5
}

View File

@@ -1,7 +1,7 @@
plugins {
id "cpp"
id "google-test-test-suite"
id "edu.wpi.first.GradleRIO" version "2023.1.1-beta-7"
id "edu.wpi.first.GradleRIO" version "2023.2.1"
id "com.dorongold.task-tree" version "2.1.0"
}

View File

@@ -1,6 +1,6 @@
{
"enableCppIntellisense": true,
"currentLanguage": "cpp",
"projectYear": "2023Beta",
"projectYear": "2023",
"teamNumber": 5
}

View File

@@ -1,7 +1,7 @@
plugins {
id "cpp"
id "google-test-test-suite"
id "edu.wpi.first.GradleRIO" version "2023.1.1-beta-7"
id "edu.wpi.first.GradleRIO" version "2023.2.1"
id "com.dorongold.task-tree" version "2.1.0"
}

View File

@@ -1,6 +1,6 @@
{
"enableCppIntellisense": true,
"currentLanguage": "cpp",
"projectYear": "2023Beta",
"projectYear": "2023",
"teamNumber": 5
}

View File

@@ -1,7 +1,7 @@
plugins {
id "cpp"
id "google-test-test-suite"
id "edu.wpi.first.GradleRIO" version "2023.1.1-beta-7"
id "edu.wpi.first.GradleRIO" version "2023.2.1"
id "com.dorongold.task-tree" version "2.1.0"
}

View File

@@ -1,6 +1,6 @@
{
"enableCppIntellisense": false,
"currentLanguage": "java",
"projectYear": "2023Beta",
"projectYear": "2023",
"teamNumber": 6995
}

View File

@@ -1,6 +1,6 @@
plugins {
id "java"
id "edu.wpi.first.GradleRIO" version "2023.1.1-beta-7"
id "edu.wpi.first.GradleRIO" version "2023.2.1"
}
sourceCompatibility = JavaVersion.VERSION_11

View File

@@ -1,6 +1,6 @@
{
"enableCppIntellisense": false,
"currentLanguage": "java",
"projectYear": "2023Beta",
"projectYear": "2023",
"teamNumber": 6995
}

View File

@@ -1,6 +1,6 @@
plugins {
id "java"
id "edu.wpi.first.GradleRIO" version "2023.1.1-beta-7"
id "edu.wpi.first.GradleRIO" version "2023.2.1"
}
sourceCompatibility = JavaVersion.VERSION_11

View File

@@ -0,0 +1,162 @@
# This gitignore has been specially created by the WPILib team.
# If you remove items from this file, intellisense might break.
### C++ ###
# Prerequisites
*.d
# Compiled Object files
*.slo
*.lo
*.o
*.obj
# Precompiled Headers
*.gch
*.pch
# Compiled Dynamic libraries
*.so
*.dylib
*.dll
# Fortran module files
*.mod
*.smod
# Compiled Static libraries
*.lai
*.la
*.a
*.lib
# Executables
*.exe
*.out
*.app
### Java ###
# Compiled class file
*.class
# Log file
*.log
# BlueJ files
*.ctxt
# Mobile Tools for Java (J2ME)
.mtj.tmp/
# Package Files #
*.jar
*.war
*.nar
*.ear
*.zip
*.tar.gz
*.rar
# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
hs_err_pid*
### Linux ###
*~
# temporary files which can be created if a process still has a handle open of a deleted file
.fuse_hidden*
# KDE directory preferences
.directory
# Linux trash folder which might appear on any partition or disk
.Trash-*
# .nfs files are created when an open file is removed but is still being accessed
.nfs*
### macOS ###
# General
.DS_Store
.AppleDouble
.LSOverride
# Icon must end with two \r
Icon
# Thumbnails
._*
# Files that might appear in the root of a volume
.DocumentRevisions-V100
.fseventsd
.Spotlight-V100
.TemporaryItems
.Trashes
.VolumeIcon.icns
.com.apple.timemachine.donotpresent
# Directories potentially created on remote AFP share
.AppleDB
.AppleDesktop
Network Trash Folder
Temporary Items
.apdisk
### VisualStudioCode ###
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
### Windows ###
# Windows thumbnail cache files
Thumbs.db
ehthumbs.db
ehthumbs_vista.db
# Dump file
*.stackdump
# Folder config file
[Dd]esktop.ini
# Recycle Bin used on file shares
$RECYCLE.BIN/
# Windows Installer files
*.cab
*.msi
*.msix
*.msm
*.msp
# Windows shortcuts
*.lnk
### Gradle ###
.gradle
/build/
# Ignore Gradle GUI config
gradle-app.setting
# Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored)
!gradle-wrapper.jar
# Cache of project
.gradletasknamecache
# # Work around https://youtrack.jetbrains.com/issue/IDEA-116898
# gradle/wrapper/gradle-wrapper.properties
# # VS Code Specific Java Settings
# DO NOT REMOVE .classpath and .project
.classpath
.project
.settings/
bin/
# Simulation GUI and other tools window save file
*-window.json

View File

@@ -0,0 +1,6 @@
{
"enableCppIntellisense": false,
"currentLanguage": "java",
"projectYear": "2023Beta",
"teamNumber": 1736
}

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