3d, camera calibration, backend settings hookup (#80)

* Implement new UI backend stuff

* Kinda partially add resolution accuracy list

* camera calibration go brrrrrrrr

* ayyyy calibration works

* Maybe fix grouping

* Reorganize camera view

* Fix settings not getting sent

* Make pretty (#4)

* Reorganize camera view

* Apply some cosmetic layout changes to the cameras page

* Fix pipeline rollback bug when starting on non-dashboard pages

Co-authored-by: Matt <matthew.morley.ca@gmail.com>

* Fix naming mismatch

* Mostly make stuff work

* rename robot-relative pose to camera-relative pose

* SolvePNP memes, fix isFovConfigurable

* Change config path to photonvision_config

* netmask go poof, fix zip download?

* Update index.js

* Fix multi cam stuff?

* Use LinearFilter instead

* Fix multicam

* aaa

* start adding restart device and restart program, fix square size bug

* Add some debug stuff

* oop

* Start fixing tests

* Fix tests

* Make target box proportinal

* run spotless

* Make crosshair h o t p i n k

* Address review comments

* Address review 2 electric booaloo

* Possibly implement vendor FOV?

* Make centroid crosshair gren

* actually use FOV

* Fix tests

* actually fix tests

Co-authored-by: Declan Freeman-Gleason <declanfreemangleason@gmail.com>
This commit is contained in:
Matt
2020-08-14 12:39:21 -07:00
committed by GitHub
parent 86ea661ed9
commit b3436765e1
86 changed files with 2106 additions and 1173 deletions

View File

@@ -104,7 +104,9 @@
</v-icon>
</v-list-item-icon>
<v-list-item-content>
<v-list-item-title>{{ $store.state.backendConnected ? "Connected" : "Trying to connect..." }}</v-list-item-title>
<v-list-item-title>
{{ $store.state.backendConnected ? "Connected" : "Trying to connect..." }}
</v-list-item-title>
</v-list-item-content>
</v-list-item>
</v-list>
@@ -152,7 +154,7 @@
},
data: () => ({
// Used so that we can switch back to the previously selected pipeline after camera calibration
previouslySelectedIndex: null,
previouslySelectedIndex: undefined,
timer: undefined,
isLogger: false,
log: "",
@@ -169,9 +171,9 @@
compact: {
get() {
if (this.$store.state.compactMode === undefined) {
return this.$vuetify.breakpoint.smAndDown;
return this.$vuetify.breakpoint.smAndDown;
} else {
return this.$store.state.compactMode || this.$vuetify.breakpoint.smAndDown;
return this.$store.state.compactMode || this.$vuetify.breakpoint.smAndDown;
}
},
set(value) {
@@ -179,7 +181,7 @@
this.$store.commit("compactMode", value);
localStorage.setItem("compactMode", value);
},
}
},
},
created() {
document.addEventListener("keydown", e => {
@@ -210,16 +212,16 @@
}
}
} catch (error) {
console.error('error: ' + data.data + " , " + error);
console.error('error: ' + JSON.stringify(data.data) + " , " + error);
}
};
this.$options.sockets.onopen = () => {
this.$store.state.backendConnected = true;
this.$store.state.backendConnected = true;
};
let closed = () => {
this.$store.state.backendConnected = false;
}
this.$store.state.backendConnected = false;
};
this.$options.sockets.onclose = closed;
this.$options.sockets.onerror = closed;
@@ -228,14 +230,15 @@
methods: {
handleMessage(key, value) {
if (key === "logMessage") {
console.log("[FROM BACKEND]" + value);
this.logMessage(value, 0);
this.logMessage(value["logMessage"], value["logLevel"]);
} else if (key === "updatePipelineResult") {
this.$store.commit('mutatePipelineResults', value)
} else if (this.$store.state.hasOwnProperty(key)) {
this.$store.commit(key, value);
} else if (this.$store.getters.currentPipelineSettings.hasOwnProperty(key)) {
this.$store.commit('mutatePipeline', {[key]: value});
} else if (this.$store.state.settings.hasOwnProperty(key)) {
this.$store.commit('mutateSettings', {[key]: value});
} else {
switch (key) {
default: {
@@ -259,9 +262,10 @@
this.timer = setInterval(this.saveSettings, 4000);
},
logMessage(message, level) {
const colors = ["\u001b[31m", "\u001b[32m", "\u001b[33m", "\u001b[34m"]
const colors = ["\u001B[30m", "\u001B[31m", "\u001B[33m", "\u001B[32m", "\u001B[37m", "\u001B[36m"]
const reset = "\u001b[0m"
this.log += `${colors[level]}${message}${reset}\n`
console.log(message)
},
switchToDriverMode() {
this.previouslySelectedIndex = this.$store.getters.currentPipelineIndex;
@@ -269,7 +273,7 @@
},
rollbackPipelineIndex() {
if (this.previouslySelectedIndex !== null) {
this.handleInputWithIndex('currentPipeline', this.previouslySelectedIndex)
this.handleInputWithIndex('currentPipeline', this.previouslySelectedIndex || 0);
}
this.previouslySelectedIndex = null;
}
@@ -278,7 +282,7 @@
</script>
<style lang="sass">
@import "./scss/variables.scss"
@import "./scss/variables.scss"
</style>
<style>
@@ -288,12 +292,12 @@
@keyframes pulse-animation {
0% {
box-shadow: 0 0 0 0px rgba(0, 0, 0, 0.2);
background-color: rgba(0, 0, 0, 0.2);
box-shadow: 0 0 0 0px rgba(0, 0, 0, 0.2);
background-color: rgba(0, 0, 0, 0.2);
}
100% {
box-shadow: 0 0 0 20px rgba(0, 0, 0, 0);
background-color: rgba(0, 0, 0, 0);
box-shadow: 0 0 0 20px rgba(0, 0, 0, 0);
background-color: rgba(0, 0, 0, 0);
}
}
@@ -349,17 +353,17 @@
border-color: white !important;
}
.v-input {
font-size: 1rem !important;
}
.v-input {
font-size: 1rem !important;
}
</style>
<style lang="scss">
@import '~vuetify/src/styles/settings/_variables';
@import '~vuetify/src/styles/settings/_variables';
@media #{map-get($display-breakpoints, 'md-and-down')} {
html {
font-size: 14px !important;
@media #{map-get($display-breakpoints, 'md-and-down')} {
html {
font-size: 14px !important;
}
}
}
</style>

View File

@@ -21,6 +21,7 @@
type="number"
style="width: 70px"
:step="step"
:disabled="disabled"
:rules="rules"
/>
</v-col>
@@ -37,7 +38,7 @@
TooltippedLabel,
},
// eslint-disable-next-line vue/require-prop-types
props: ['name', 'value', 'step', 'labelCols', 'rules', 'tooltip'],
props: ['name', 'value', 'step', 'labelCols', 'rules', 'tooltip', 'disabled'],
computed: {
localValue: {
get() {

View File

@@ -148,22 +148,10 @@
</v-card-title>
<v-card-text>
<CVselect
v-model="pipelineDuplicate.pipeline"
v-model="pipeIndexToDuplicate"
name="Pipeline"
:list="$store.getters.pipelineList"
/>
<v-checkbox
v-if="$store.getters.cameraList.length > 1"
v-model="anotherCamera"
dark
:label="'To another camera'"
/>
<CVselect
v-if="anotherCamera === true"
v-model="pipelineDuplicate.camera"
name="Camera"
:list="$store.getters.cameraList"
/>
</v-card-text>
<v-divider />
<v-card-actions>
@@ -249,11 +237,7 @@
namingDialog: false,
newPipelineName: "",
duplicateDialog: false,
anotherCamera: false,
pipelineDuplicate: {
pipeline: undefined,
camera: -1
},
pipeIndexToDuplicate: undefined
}
},
computed: {
@@ -299,10 +283,10 @@
},
currentPipelineIndex: {
get() {
return this.$store.getters.currentPipelineIndex + this.$store.getters.isDriverMode ? 1 : 0;
return this.$store.getters.currentPipelineIndex + (this.$store.getters.isDriverMode ? 1 : 0);
},
set(value) {
this.$store.commit('currentPipelineIndex', value - this.$store.getters.isDriverMode ? 1 : 0);
this.$store.commit('currentPipelineIndex', value - (this.$store.getters.isDriverMode ? 1 : 0));
}
}
},
@@ -332,10 +316,7 @@
this.namingDialog = true;
},
openDuplicateDialog() {
this.pipelineDuplicate = {
pipeline: this.currentPipelineIndex - 1,
camera: -1
};
this.pipeIndexToDuplicate = this.currentPipelineIndex - 1;
this.duplicateDialog = true;
},
deleteCurrentPipeline() {
@@ -356,19 +337,17 @@
}
},
duplicatePipeline() {
if (!this.anotherCamera) {
this.pipelineDuplicate.camera = -1
}
// this.handleInput("duplicatePipeline", this.pipelineDuplicate);
this.axios.post("http://" + this.$address + "/api/vision/duplicate", this.pipelineDuplicate);
// if (!this.anotherCamera) {
// this.pipelineDuplicate.camera = -1
// }
this.handleInputWithIndex("duplicatePipeline", this.pipeIndexToDuplicate);
// this.axios.post("http://" + this.$address + "/api/vision/duplicate", this.pipeIndexToDuplicate);
this.closeDuplicateDialog();
},
closeDuplicateDialog() {
this.duplicateDialog = false;
this.pipelineDuplicate = {
pipeline: undefined,
camera: -1
}
this.pipeIndexToDuplicate = undefined;
},
discardPipelineNameChange() {
this.namingDialog = false;

View File

@@ -1,7 +1,6 @@
import Vue from 'vue'
import Vuex from 'vuex'
import networkSettings from "./modules/networkSettings"
import undoRedo from "./modules/undoRedo";
Vue.use(Vuex);
@@ -17,7 +16,6 @@ export default new Vuex.Store({
currentResolutionIndex: 0,
},
},
networkSettings: networkSettings,
undoRedo: undoRedo
},
state: {
@@ -43,6 +41,7 @@ export default new Vuex.Store({
"pixelFormat": "BGR"
}
],
calibrations: [ ],
fov: 70.0,
isFovConfigurable: true,
calibrated: false,
@@ -76,14 +75,14 @@ export default new Vuex.Store({
solvePNPEnabled: false,
targetRegion: 0,
contourTargetOrientation: 1,
is3D: false,
cornerDetectionAccuracyPercentage: 10,
// Settings that apply to shape
}
}
],
pipelineResults: [
{
pipelineResults: {
fps: 0,
latency: 0,
targets: [{
@@ -95,8 +94,7 @@ export default new Vuex.Store({
// 3D only
pose: {x: 0, y: 0, rotation: 0},
}]
}
],
},
settings: {
general: {
version: "Unknown",
@@ -106,7 +104,7 @@ export default new Vuex.Store({
hardwareModel: "Unknown",
hardwarePlatform: "Unknown",
},
networking: {
networkSettings: {
teamNumber: 0,
supported: true,
@@ -120,19 +118,29 @@ export default new Vuex.Store({
supported: true,
brightness: 0.0,
},
}
},
calibrationData: {
count: 0,
videoModeIndex: 0,
minCount: 25,
hasEnough: false,
squareSizeIn: 1.0,
patternWidth: 7,
patternHeight: 7,
boardType: 0, // Chessboard, dotboard
},
},
mutations: {
saveBar: set('saveBar'),
compactMode: set('compactMode'),
cameraSettings: set('cameraSettings'),
currentCameraIndex: set('currentCameraIndex'),
pipelineResults: set('pipelineResults'),
networkSettings: set('networkSettings'),
selectedOutputs: set('selectedOutputs'),
settings: set('settings'),
calibrationData: set('calibrationData'),
is3D: (state, val) => {
state.cameraSettings[state.currentCameraIndex].currentPipelineSettings.is3D = val;
solvePNPEnabled: (state, val) => {
state.cameraSettings[state.currentCameraIndex].currentPipelineSettings.solvePNPEnabled = val;
},
currentPipelineIndex: (state, val) => {
@@ -152,27 +160,50 @@ export default new Vuex.Store({
}
},
mutateSettings: (state, payload) => {
for (let key in payload) {
if (!payload.hasOwnProperty(key)) continue;
const value = payload[key];
const settings = state.settings;
if (settings.hasOwnProperty(key)) {
Vue.set(settings, key, value);
}
}
},
mutatePipelineResults(state, payload) {
// Key: index, value: result
let newResultArray = [];
for (let key in payload) {
if (!payload.hasOwnProperty(key)) continue;
const index = parseInt(key);
newResultArray[index] = payload[key];
if(index === state.currentCameraIndex) {
Vue.set(state, 'pipelineResults', payload[key])
}
}
Vue.set(state, 'pipelineResults', newResultArray)
}
},
mutateCalibrationState: (state, payload) => {
for (let key in payload) {
if (!payload.hasOwnProperty(key)) continue;
const value = payload[key];
const calibration = state.calibrationData;
if (calibration.hasOwnProperty(key)) {
calibration[key] = value
}
Vue.set(state, 'calibrationData', calibration)
}
},
},
getters: {
isDriverMode: state => state.cameraSettings[state.currentCameraIndex].currentPipelineIndex === -1,
pipelineSettings: state => state.pipelineSettings,
streamAddress: state =>
["http://" + location.hostname + ":" + state.cameraSettings[state.currentCameraIndex].inputStreamPort + "/stream.mjpg",
"http://" + location.hostname + ":" + state.cameraSettings[state.currentCameraIndex].outputStreamPort + "/stream.mjpg"],
targets: state => state.pipelineResults.length,
currentPipelineResults: state =>
state.pipelineResults[state.cameraSettings[state.currentCameraIndex].currentPipelineIndex],
currentPipelineResults: state => {
return state.pipelineResults;
},
cameraList: state => state.cameraSettings.map(it => it.nickname),
currentCameraSettings: state => state.cameraSettings[state.currentCameraIndex],
currentCameraIndex: state => state.currentCameraIndex,
@@ -182,6 +213,6 @@ export default new Vuex.Store({
return Object.values(state.cameraSettings[state.currentCameraIndex].videoFormatList); // convert to a list
},
pipelineList: state => state.cameraSettings[state.currentCameraIndex].pipelineNicknames,
currentCameraFPS: state => state.pipelineResults[state.currentCameraIndex].fps
calibrationList: state => state.cameraSettings[state.currentCameraIndex].calibrations,
}
})

View File

@@ -1,17 +0,0 @@
export default {
state: {
netmask: "",
ip: "",
teamNumber: "",
connectionType: "",
gateway: ""
},
mutations: {
},
actions: {},
getters: {
pipeline: state => {
return state
}
}
};

View File

@@ -1,45 +0,0 @@
import Vue from 'vue'
export default {
state: {
exposure: 0,
brightness: 0,
gain: 0,
rotationMode: 0,
hue: [0, 15],
saturation: [0, 15],
value: [0, 25],
erode: false,
dilate: false,
area: [0, 12],
ratio: [0, 12],
fullness: [0, 12],
speckle: 5,
targetGrouping: 0,
targetIntersection: 0,
sortMode: 0,
multiple: false,
isBinary: 0,
calibrationMode: 0,
videoModeIndex: 0,
streamDivisor: 0,
is3D: false,
targetRegion: 0,
targetOrientation: 1
},
mutations: {
isBinary: (state, value) => {
state.isBinary = value
},
mutatePipeline: (state, {key, value}) => {
Vue.set(state, key, value)
}
},
actions: {},
getters: {
pipeline: state => {
return state
}
}
};

View File

@@ -8,6 +8,7 @@
cols="12"
md="7"
>
<!-- Camera card -->
<v-card
class="mb-3 pr-6 pb-3"
color="primary"
@@ -23,14 +24,14 @@
@input="handleInput('currentCamera',currentCameraIndex)"
/>
<CVnumberinput
v-if="cameraSettings.isFovConfigurable"
v-model="cameraSettings.fov"
tooltip="Field of view (in degrees) of the camera measured across the diagonal of the frame"
:tooltip="cameraSettings.isFovConfigurable ? 'Field of view (in degrees) of the camera measured across the diagonal of the frame' : 'This setting is managed by a vendor'"
name="Diagonal FOV"
:disabled="!cameraSettings.isFovConfigurable"
/>
<br>
<CVnumberinput
v-model="cameraSettings.tilt"
v-model="cameraSettings.tiltDegrees"
name="Camera pitch"
tooltip="How many degrees above the horizontal the physical camera is tilted"
:step="0.01"
@@ -49,52 +50,175 @@
</v-btn>
</div>
</v-card>
<!-- Calibration card -->
<v-card
class="pr-6 pb-3"
color="primary"
dark
>
<v-card-title>Camera Calibration</v-card-title>
<div class="ml-5">
<v-row>
<v-col cols="8">
<!-- Calibration input -->
<v-col
cols="12"
md="6"
>
<CVselect
v-model="resolutionIndex"
v-model="selectedFilteredResIndex"
name="Resolution"
select-cols="7"
:list="stringResolutionList"
:disabled="isCalibrating"
tooltip="Resolution to calibrate at (you will have to calibrate every resolution you use 3D mode on)"
/>
<CVselect
v-model="boardType"
name="Board Type"
select-cols="7"
:list="['Chessboard', 'Dot Grid']"
:disabled="isCalibrating"
tooltip="Calibration board pattern to use"
/>
<CVnumberinput
v-model="squareSizeIn"
name="Pattern Spacing (in)"
label-cols="5"
tooltip="Spacing between pattern features in inches"
:disabled="isCalibrating"
/>
<CVnumberinput
v-model="boardWidth"
name="Board width"
label-cols="5"
tooltip="Width of the board in dots or corners; with the standard chessboard, this is usually 7"
:disabled="isCalibrating"
/>
<CVnumberinput
v-model="boardHeight"
name="Board height"
label-cols="5"
tooltip="Height of the board in dots or corners; with the standard chessboard, this is usually 7"
:disabled="isCalibrating"
/>
</v-col>
<!-- Calibrated table -->
<v-col
cols="4"
align-self="center"
cols="12"
md="6"
>
<CVnumberinput
v-model="squareSize"
name="Square Size (in)"
tooltip="Length of one side of the checkerboard's square in inches"
label-cols="unset"
<v-row
align="start"
class="pb-4"
>
<v-simple-table
fixed-header
height="100%"
dense
>
<thead style="font-size: 1.25rem;">
<tr>
<th class="text-center">
<tooltipped-label text="Resolution" />
</th>
<th class="text-center">
<tooltipped-label
tooltip="Average reprojection error of the calibration, in pixels"
text="Mean Error"
/>
</th>
<th class="text-center">
<tooltipped-label
tooltip="Standard deviation of the mean error, in pixels"
text="Standard Deviation"
/>
</th>
</tr>
</thead>
<tbody>
<tr
v-for="(value, index) in filteredResolutionList"
:key="index"
>
<td> {{ value.width }} X {{ value.height }} </td>
<td>
{{ isCalibrated(value) ? value.mean.toFixed(2) + "px" : "—" }}
</td>
<td> {{ isCalibrated(value) ? value.standardDeviation.toFixed(2) + "px" : "—" }} </td>
</tr>
</tbody>
</v-simple-table>
</v-row>
<v-row justify="center">
<v-chip
v-show="isCalibrating"
label
:color="snapshotAmount < 25 ? 'grey' : 'secondary'"
>
Snapshots: {{ snapshotAmount }} of at least {{ minSnapshots }}
</v-chip>
</v-row>
</v-col>
</v-row>
<v-row v-if="isCalibrating">
<v-col
cols="12"
class="pt-0"
>
<CVslider
v-model="$store.getters.currentPipelineSettings.cameraExposure"
name="Exposure"
:min="0"
:max="100"
slider-cols="8"
@input="e => handlePipelineUpdate('cameraExposure', e)"
/>
<CVslider
v-model="this.$store.getters.currentPipelineSettings.cameraBrightness"
name="Brightness"
:min="0"
:max="100"
slider-cols="8"
@input="e => handlePipelineUpdate('cameraBrightness', e)"
/>
<CVslider
v-if="$store.getters.currentPipelineSettings.cameraGain !== -1"
v-model="$store.getters.currentPipelineSettings.cameraGain"
name="Gain"
:min="0"
:max="100"
slider-cols="8"
@input="e => handlePipelineUpdate('cameraGain', e)"
/>
</v-col>
</v-row>
<v-row>
<v-col>
<v-col align-self="center">
<v-btn
small
color="secondary"
:disabled="checkResolution"
style="width: 100%;"
:disabled="disallowCalibration"
@click="sendCalibrationMode"
>
{{ calibrationModeButton.text }}
{{ isCalibrating ? "Take Snapshot" : "Start Calibration" }}
</v-btn>
</v-col>
<v-col>
<v-col align-self="center">
<v-btn
small
color="red"
:color="hasEnough ? 'accent' : 'red'"
:class="hasEnough ? 'black--text' : 'white---text'"
style="width: 100%;"
:disabled="checkCancellation"
@click="sendCalibrationFinish"
>
{{ cancellationModeButton.text }}
{{ hasEnough ? "End Calibration" : "Cancel Calibration" }}
</v-btn>
</v-col>
<v-col>
@@ -102,6 +226,7 @@
color="accent"
small
outlined
style="width: 100%;"
@click="downloadBoard"
>
<v-icon left>
@@ -113,52 +238,10 @@
ref="calibrationFile"
style="color: black; text-decoration: none; display: none"
:href="require('../assets/chessboard.png')"
download="Calibration Board.png"
download="chessboard.png"
/>
</v-col>
</v-row>
<v-row v-if="isCalibrating">
<v-col>
<span>Snapshot Amount: {{ snapshotAmount }}</span>
</v-col>
</v-row>
<div v-if="isCalibrating">
<v-checkbox
v-model="isAdvanced"
label="Advanced Menu"
dark
/>
<div v-if="isAdvanced">
<CVslider
v-model="$store.getters.pipeline.exposure"
name="Exposure"
:min="0"
:max="100"
@input="e=> handleInput('exposure', e)"
/>
<CVslider
v-model="$store.getters.pipeline.brightness"
name="Brightness"
:min="0"
:max="100"
@input="e=> handleInput('brightness', e)"
/>
<CVslider
v-if="$store.getters.pipeline.gain !== -1"
v-model="$store.getters.pipeline.gain"
name="Gain"
:min="0"
:max="100"
@input="e=> handleInput('gain', e)"
/>
<CVselect
v-model="$store.getters.pipeline.videoModeIndex"
name="FPS"
:list="stringFpsList"
@input="changeFps"
/>
</div>
</div>
</div>
</v-card>
</v-col>
@@ -186,207 +269,265 @@
</template>
<script>
import CVselect from '../components/common/cv-select';
import CVnumberinput from '../components/common/cv-number-input';
import CVslider from '../components/common/cv-slider';
import CVimage from "../components/common/cv-image";
import CVselect from '../components/common/cv-select';
import CVnumberinput from '../components/common/cv-number-input';
import CVslider from '../components/common/cv-slider';
import CVimage from "../components/common/cv-image";
import TooltippedLabel from "../components/common/cv-tooltipped-label";
export default {
name: 'Cameras',
components: {
CVselect,
CVnumberinput,
CVslider,
CVimage
export default {
name: 'Cameras',
components: {
TooltippedLabel,
CVselect,
CVnumberinput,
CVslider,
CVimage
},
data() {
return {
snackbar: {
color: "success",
text: ""
},
snack: false,
filteredVideomodeIndex: 0
}
},
computed: {
disallowCalibration() {
return !(this.calibrationData.boardType === 0 || this.calibrationData.boardType === 1);
},
data() {
return {
isCalibrating: false,
resolutionIndex: undefined,
calibrationModeButton: {
text: "Start Calibration",
color: "green"
},
cancellationModeButton: {
text: "Cancel Calibration",
color: "red"
},
snackbar: {
color: "success",
text: ""
},
squareSize: 1.0,
snapshotAmount: 0,
hasEnough: false,
snack: false,
isAdvanced: false
checkCancellation() {
if (this.isCalibrating) {
return false
} else if (this.disallowCalibration) {
return true;
} else {
return true
}
},
computed: {
checkResolution() {
return this.resolutionIndex === undefined;
currentCameraIndex: {
get() {
return this.$store.state.currentCameraIndex;
},
checkCancellation() {
if (this.isCalibrating) {
return false
} else if (this.checkResolution) {
return true;
} else {
return true
}
},
currentCameraIndex: {
get() {
return this.$store.state.currentCameraIndex;
},
set(value) {
this.$store.commit('currentCameraIndex', value);
}
},
filteredResolutionList: {
get() {
let tmp_list = [];
for (let i in this.$store.state.resolutionList) {
if (this.$store.state.resolutionList.hasOwnProperty(i)) {
let res = JSON.parse(JSON.stringify(this.$store.state.resolutionList[i]));
if (!tmp_list.some(e => e.width === res.width && e.height === res.height)) {
res['actualIndex'] = parseInt(i);
tmp_list.push(res);
}
}
}
return tmp_list;
}
},
filteredFpsList() {
let selectedRes = this.$store.state.resolutionList[this.resolutionIndex];
let tmpList = [];
for (let i in this.$store.state.resolutionList) {
if (this.$store.state.resolutionList.hasOwnProperty(i)) {
let res = JSON.parse(JSON.stringify(this.$store.state.resolutionList[i]));
if (!tmpList.some(e => e['fps'] === res['fps'])) {
if (res.width === selectedRes.width && res.height === selectedRes.height) {
res['actualIndex'] = parseInt(i);
tmpList.push(res);
}
}
}
}
return tmpList;
},
stringFpsList() {
let tmp = [];
for (let i of this.filteredFpsList) {
tmp.push(i['fps']);
}
return tmp;
},
stringResolutionList: {
get() {
let tmp = [];
for (let i of this.filteredResolutionList) {
tmp.push(`${i['width']} X ${i['height']}`)
}
return tmp
}
},
cameraSettings: {
get() {
return this.$store.getters.currentCameraSettings;
},
set(value) {
this.$store.commit('cameraSettings', value);
}
set(value) {
this.$store.commit('currentCameraIndex', value);
}
},
methods: {
downloadBoard() {
this.axios.get("http://" + this.$address + require('../assets/chessboard.png'), {responseType: 'blob'}).then((response) => {
require('downloadjs')(response.data, "Calibration Board", "image/png")
})
},
changeFps() {
this.handleInput('videoModeIndex', this.filteredFpsList[this.$store.getters.pipeline['videoModeIndex']]['actualIndex']);
},
sendCameraSettings() {
const self = this;
this.axios.post("http://" + this.$address + "/api/settings/camera", this.cameraSettings).then(
function (response) {
if (response.status === 200) {
self.$store.state.saveBar = true;
// Makes sure there's only one entry per resolution
filteredResolutionList: {
get() {
let list = this.$store.getters.videoFormatList;
let filtered = [];
list.forEach((it, i) => {
if (!filtered.some(e => e.width === it.width && e.height === it.height)) {
it['index'] = i;
const calib = this.getCalibrationCoeffs(it);
if(calib != null) {
it['standardDeviation'] = calib.standardDeviation;
it['mean'] = calib.perViewErrors.reduce((a, b) => a + b) / calib.perViewErrors.length;
}
filtered.push(it);
}
)
},
sendCalibrationMode() {
const self = this;
let data = {};
let connection_string = "/api/settings/";
if (self.isCalibrating === true) {
connection_string += "snapshot"
} else {
connection_string += "startCalibration";
data['resolution'] = this.filteredResolutionList[this.resolutionIndex].actualIndex;
data['squareSize'] = this.squareSize;
self.hasEnough = false;
}
this.axios.post("http://" + this.$address + connection_string, data).then(
function (response) {
if (response.status === 200) {
if (self.isCalibrating) {
self.snapshotAmount = response.data['snapshotCount'];
self.hasEnough = response.data['hasEnough'];
if (self.hasEnough === true) {
self.cancellationModeButton.text = "Finish Calibration";
self.cancellationModeButton.color = "green";
}
} else {
self.calibrationModeButton.text = "Take Snapshot";
self.isCalibrating = true;
}
}
}
);
},
sendCalibrationFinish() {
const self = this;
let connection_string = "/api/settings/endCalibration";
let data = {};
data['squareSize'] = this.squareSize;
self.axios.post("http://" + this.$address + connection_string, data).then((response) => {
if (response.status === 200) {
self.snackbar = {
color: "success",
text: "calibration successful. \n" +
"accuracy: " + response.data['accuracy'].toFixed(5)
};
self.snack = true;
}
self.isCalibrating = false;
self.hasEnough = false;
self.snapshotAmount = 0;
self.calibrationModeButton.text = "Start Calibration";
self.cancellationModeButton.text = "Cancel Calibration";
self.cancellationModeButton.color = "red";
}
).catch(() => {
self.snackbar = {
color: "error",
text: "calibration failed"
};
self.snack = true;
self.isCalibrating = false;
self.hasEnough = false;
self.snapshotAmount = 0;
self.calibrationModeButton.text = "Start Calibration";
self.cancellationModeButton.text = "Cancel Calibration";
self.cancellationModeButton.color = "red";
});
filtered.sort((a, b) => (b.width + b.height) - (a.width + a.height));
return filtered
}
},
stringResolutionList: {
get() {
return this.filteredResolutionList.map(res => `${res['width']} X ${res['height']}`);
}
},
cameraSettings: {
get() {
return this.$store.getters.currentCameraSettings;
},
set(value) {
this.$store.commit('cameraSettings', value);
}
},
boardType: {
get() {
return this.calibrationData.boardType
},
set(value) {
this.$store.commit('mutateCalibrationState', {['boardType']: value});
}
},
snapshotAmount: {
get() {
return this.calibrationData.count
}
},
minSnapshots: {
get() {
return this.calibrationData.minCount
}
},
hasEnough: {
get() {
return this.calibrationData.hasEnough
}
},
boardWidth: {
get() {
return this.calibrationData.patternWidth
},
set(value) {
this.$store.commit('mutateCalibrationState', {['patternWidth']: value})
}
},
boardHeight: {
get() {
return this.calibrationData.patternHeight
},
set(value) {
this.$store.commit('mutateCalibrationState', {['patternHeight']: value})
}
},
squareSizeIn: {
get() {
return this.calibrationData.squareSizeIn
},
set(value) {
this.$store.commit('mutateCalibrationState', {['squareSizeIn']: value})
}
},
calibrationData: {
get() {
return this.$store.state.calibrationData
}
},
isCalibrating: {
get() {
return this.$store.getters.currentPipelineIndex === -2;
}
},
selectedFilteredResIndex: {
get() {
return this.filteredVideomodeIndex
},
set(i) {
console.log(`Setting filtered index to ${i}`)
this.filteredVideomodeIndex = i
this.$store.commit('mutateCalibrationState', {['videoModeIndex']: this.filteredResolutionList[i].index});
}
},
},
methods: {
getCalibrationCoeffs(resolution) {
const calList = this.$store.getters.calibrationList;
let ret = null;
calList.forEach(cal => {
if(cal.width === resolution.width && cal.height === resolution.height) {
ret = cal
}
})
return ret;
},
downloadBoard() {
this.axios.get("http://" + this.$address + require('../assets/chessboard.png'), {responseType: 'blob'}).then((response) => {
require('downloadjs')(response.data, "Calibration Board", "image/png")
})
},
sendCameraSettings() {
this.axios.post("http://" + this.$address + "/api/settings/camera", {
"settings": this.cameraSettings,
"index": this.$store.state.currentCameraIndex
}).then(
function (response) {
if (response.status === 200) {
this.$store.state.saveBar = true;
}
}
)
},
isCalibrated(resolution) {
return this.$store.getters.currentCameraSettings.calibrations
.some(e => e.width === resolution.width && e.height === resolution.height)
},
sendCalibrationMode() {
let data = {
['cameraIndex']: this.$store.state.currentCameraIndex
};
if (this.isCalibrating === true) {
data['takeCalibrationSnapshot'] = true
} else {
const calData = this.calibrationData
calData.isCalibrating = true
data['startPnpCalibration'] = calData
console.log("starting calibration with index " + calData.videoModeIndex)
}
this.$socket.send(this.$msgPack.encode(data));
},
sendCalibrationFinish() {
console.log("finishing calibration for index " + this.$store.getters.currentCameraIndex)
this.snackbar.text = "Calibrating...";
this.snackbar.color = "secondary"
this.snack = true;
this.axios.post("http://" + this.$address + "/api/settings/endCalibration", this.$store.getters.currentCameraIndex)
.then((response) => {
if (response.status === 200) {
this.snackbar = {
color: "success",
text: "Calibration successful! \n" +
"Standard deviation: " + response.data.toFixed(5)
};
this.snack = true;
}
else {
this.snackbar = {
color: "error",
text: "Calibration Failed!"
};
this.snack = true;
}
}
).catch(() => {
this.snackbar = {
color: "error",
text: "Calibration Failed!"
};
this.snack = true;
});
}
}
}
</script>
<style lang="" scoped>
<style scoped>
.v-data-table {
text-align: center;
background-color: transparent !important;
width: 100%;
height: 100%;
overflow-y: auto;
}
.v-data-table th {
background-color: #006492 !important;
}
.v-data-table th,td {
font-size: 1rem !important;
}
/** This is unfortunately the only way to override table background color **/
.theme--dark.v-data-table tbody tr:hover:not(.v-data-table__expanded__content):not(.v-data-table__empty-wrapper) {
background: #005281;
}
</style>

View File

@@ -25,12 +25,7 @@
class="pb-0 mb-0 pl-4 pt-1"
style="height: 15%; min-height: 50px;"
>
<div>
Cameras <span
class="pl-2 caption grey--text text--lighten-2"
style="line-height: 220%; display: inline-block; vertical-align: bottom;"
>{{ parseFloat(fps).toFixed(2) }} FPS</span>
</div>
Cameras
<v-switch
v-model="driverMode"
label="Driver Mode"
@@ -62,7 +57,6 @@
:color-picking="$store.state.colorPicking && idx == 0"
@click="onImageClick"
/>
<!-- <span class="fps-indicator">{{ parseFloat(fps).toFixed(2) }}</span>-->
</div>
</v-col>
</v-row>
@@ -166,7 +160,7 @@
slider-color="accent"
>
<v-tab
v-for="(tab, i) in tabs.filter(it => it.name !== '3D' || $store.getters.currentPipelineSettings.is3D)"
v-for="(tab, i) in tabs.filter(it => it.name !== '3D' || $store.getters.currentPipelineSettings.solvePNPEnabled)"
:key="i"
>
{{ tab.name }}
@@ -299,11 +293,11 @@
},
processingMode: {
get() {
return this.$store.getters.currentPipelineSettings.is3D ? 1 : 0;
return this.$store.getters.currentPipelineSettings.solvePNPEnabled ? 1 : 0;
},
set(value) {
this.$store.getters.currentPipelineSettings.is3D = value === 1;
this.handlePipelineUpdate("is3D", value === 1);
this.$store.getters.currentPipelineSettings.solvePNPEnabled = value === 1;
this.handlePipelineUpdate("solvePNPEnabled", value === 1);
}
},
driverMode: {
@@ -349,11 +343,6 @@
// this.handlePipelineUpdate('selectedOutputs', valToCommit);
}
},
fps: {
get() {
return this.$store.getters.currentCameraFPS;
}
},
latency: {
get() {
return this.$store.getters.currentPipelineResults.latency;
@@ -384,14 +373,6 @@
height: 100%;
}
.fps-indicator {
position: absolute;
top: 2%;
left: 2%;
font-size: 1.75rem;
text-shadow: 1px 1px 5px rgba(1, 1, 1, 0.65);
}
th {
width: 80px;
text-align: center;

View File

@@ -22,15 +22,15 @@
@change="onModelSelect"
/>
<CVslider
v-model="value.accuracy"
v-model="cornerDetectionAccuracyPercentage"
class="pt-2"
slider-cols="12"
name="Contour simplification amount"
:disabled="selectedModel === null"
min="0"
max="100"
@input="handleData('accuracy')"
@rollback="e => rollback('accuracy', e)"
@input="handlePipelineData('cornerDetectionAccuracyPercentage')"
@rollback="e => rollback('cornerDetectionAccuracyPercentage', e)"
/>
<mini-map
class="miniMapClass"
@@ -59,20 +59,25 @@
CVslider,
miniMap
},
// eslint-disable-next-line vue/require-prop-types
props: ['value'],
data() {
return {
selectedModel: null,
FRCtargets: null,
snackbar: {
color: "success",
color: "Success",
text: ""
},
snack: false
}
},
computed: {
cornerDetectionAccuracyPercentage: {
get() {
return this.$store.getters.currentPipelineSettings.cornerDetectionAccuracyPercentage
},
set(val) {
this.$store.commit("mutatePipeline", {"cornerDetectionAccuracyPercentage": val});
}
},
targets: {
get() {
return this.$store.getters.currentPipelineResults.targets;

View File

@@ -18,7 +18,7 @@
<th class="text-center">
Target
</th>
<template v-if="!$store.getters.currentPipelineSettings.is3D">
<template v-if="!$store.getters.currentPipelineSettings.solvePNPEnabled">
<th class="text-center">
Pitch
</th>
@@ -32,7 +32,7 @@
<th class="text-center">
Area
</th>
<template v-if="$store.getters.currentPipelineSettings.is3D">
<template v-if="$store.getters.currentPipelineSettings.solvePNPEnabled">
<th class="text-center">
X
</th>
@@ -51,17 +51,17 @@
:key="index"
>
<td>{{ index }}</td>
<template v-if="!$store.getters.currentPipelineSettings.is3D">
<template v-if="!$store.getters.currentPipelineSettings.solvePNPEnabled">
<td>{{ parseFloat(value.pitch).toFixed(2) }}</td>
<td>{{ parseFloat(value.yaw).toFixed(2) }}</td>
<td>{{ parseFloat(value.skew).toFixed(2) }}</td>
</template>
<td>{{ parseFloat(value.area).toFixed(2) }}</td>
<template v-if="$store.getters.currentPipelineSettings.is3D">
<template v-if="$store.getters.currentPipelineSettings.solvePNPEnabled">
<!-- TODO: Make sure that units are correct -->
<td>{{ parseFloat(value.pose.x).toFixed(2) }}&nbsp;m</td>
<td>{{ parseFloat(value.pose.y).toFixed(2) }}&nbsp;m</td>
<td>{{ parseFloat(value.pose.rotation).toFixed(2) }}&deg;</td>
<td>{{ parseFloat(value.pose.rot).toFixed(2) }}&deg;</td>
</template>
</tr>
</tbody>

View File

@@ -52,7 +52,6 @@
import cvImage from '../components/common/cv-image'
import General from "./SettingsViews/General";
export default {
name: 'SettingsTab',
components: {
@@ -89,23 +88,22 @@
},
methods: {
sendGeneralSettings() {
const self = this;
this.axios.post("http://" + this.$address + "/api/settings/general", this.settings).then(
function (response) {
if (response.status === 200) {
self.snackbar = {
this.snackbar = {
color: "success",
text: "Settings updated successfully"
};
self.snack = true;
this.snack = true;
}
},
function (error) {
self.snackbar = {
this.snackbar = {
color: "error",
text: (error.response || {data: "Couldn't save settings"}).data
};
self.snack = true;
this.snack = true;
}
)
},

View File

@@ -11,7 +11,7 @@
<v-col
cols="12"
sm="6"
lg="4"
lg="3"
>
<v-btn
color="secondary"
@@ -25,7 +25,7 @@
<v-col
cols="12"
sm="6"
lg="4"
lg="3"
>
<v-btn
color="secondary"
@@ -38,11 +38,24 @@
</v-col>
<v-col
cols="12"
lg="4"
lg="3"
>
<v-btn
color="red"
@click="restartDevice"
@click="axios.post('http://' + this.$address + '/api/restartProgram')"
>
<v-icon left>
mdi-restart
</v-icon> Restart Photon
</v-btn>
</v-col>
<v-col
cols="12"
lg="3"
>
<v-btn
color="red"
@click="axios.post('http://' + this.$address + '/api/restartDevice')"
>
<v-icon left>
mdi-restart
@@ -71,56 +84,53 @@
<a
ref="exportSettings"
style="color: black; text-decoration: none; display: none"
href="/api/settings/export"
:href="'http://' + this.$address + '/api/settings/photonvision_config.zip'"
download="photonvision-settings.zip"
/>
</div>
</template>
<script>
export default {
name: 'General',
data() {
return {
export default {
name: 'General',
data() {
return {
snack: false,
snackbar: {
color: "success",
text: ""
color: "success",
text: ""
},
}
},
computed: {
settings() {
}
},
computed: {
settings() {
return this.$store.state.settings.general;
}
},
methods: {
readImportedSettings(event) {
}
},
methods: {
readImportedSettings(event) {
let formData = new FormData();
formData.append("zipData", event.target.files[0]);
this.axios.post("http://" + this.$address + "/api/settings/import", formData,
{headers: {"Content-Type": "multipart/form-data"}}).then(() => {
this.snackbar = {
color: "success",
text: "Settings imported successfully",
};
{headers: {"Content-Type": "multipart/form-data"}}).then(() => {
this.snackbar = {
color: "success",
text: "Settings imported successfully",
};
}).catch(() => {
this.snackbar = {
color: "error",
text: "Couldn't import settings",
}
this.snackbar = {
color: "error",
text: "Couldn't import settings",
}
});
this.snack = true;
},
restartDevice() {
this.axios.post("http://" + this.$address + "/api/restart");
}
}
},
}
}
</script>
<style lang="css" scoped>
.v-btn {
width: 100%;
}
.v-btn {
width: 100%;
}
</style>

View File

@@ -5,7 +5,7 @@
name="Team Number"
:rules="[v => (v > 0) || 'Team number must be greater than zero', v => (v < 10000) || 'Team number must have fewer than five digits']"
/>
<template v-if="$store.state.settings.networking.supported">
<template v-if="$store.state.settings.networkSettings.supported">
<CVradio
v-model="settings.connectionType"
:list="['DHCP','Static']"
@@ -17,12 +17,6 @@
:rules="[v => isIPv4(v) || 'Invalid IPv4 address']"
name="IP"
/>
<CVinput
v-model="settings.netmask"
:input-cols="inputCols"
:rules="[v => isSubnetMask(v) || 'Invalid subnet mask']"
name="Subnet Mask"
/>
</template>
</template>
<CVinput
@@ -70,7 +64,7 @@
return this.settings.connectionType === 0;
},
settings() {
return this.$store.state.settings.networking;
return this.$store.state.settings.networkSettings;
}
},
methods: {