mirror of
https://github.com/PhotonVision/photonvision
synced 2026-07-02 02:51:40 +00:00
UI Redesign (#22)
* Rework UI into a new, responsive layout * Send two streams (only one is currently downscaled)
This commit is contained in:
committed by
GitHub
parent
aed92e7132
commit
8b46ad1cab
@@ -1,6 +1,16 @@
|
||||
<template>
|
||||
<div style="overflow:hidden;height:100%;width:100%" height="100%" width="100%">
|
||||
<iframe src="docs/index.html" frameborder="0" style="overflow:hidden;height:100%;width:100%" height="100%" width="100%"></iframe>
|
||||
<div
|
||||
style="overflow:hidden;height:100%;width:100%"
|
||||
height="100%"
|
||||
width="100%"
|
||||
>
|
||||
<iframe
|
||||
src="docs/index.html"
|
||||
frameborder="0"
|
||||
style="overflow:hidden;height:100%;width:100%"
|
||||
height="100%"
|
||||
width="100%"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -1,128 +1,173 @@
|
||||
<template>
|
||||
<div>
|
||||
<camera-and-pipeline-select />
|
||||
<v-row no-gutters>
|
||||
<!-- vision tabs -->
|
||||
<v-col
|
||||
cols="12"
|
||||
sm="12"
|
||||
md="7"
|
||||
xl="6"
|
||||
class="colsClass pr-8"
|
||||
<v-container
|
||||
class="pa-3"
|
||||
fluid
|
||||
>
|
||||
<v-row
|
||||
no-gutters
|
||||
align="center"
|
||||
justify="center"
|
||||
>
|
||||
<v-tabs
|
||||
v-if="($store.getters.currentPipelineIndex + 1) !== 0"
|
||||
v-model="selectedTab"
|
||||
fixed-tabs
|
||||
background-color="#232c37"
|
||||
dark
|
||||
height="48"
|
||||
slider-color="#ffd843"
|
||||
<v-col
|
||||
cols="12"
|
||||
:class="['pb-3 ', $store.getters.isDriverMode ? '' : 'pr-lg-3']"
|
||||
:lg="$store.getters.isDriverMode ? 12 : 8"
|
||||
align-self="stretch"
|
||||
>
|
||||
<v-tab>Input</v-tab>
|
||||
<v-tab>Threshold</v-tab>
|
||||
<v-tab>Contours</v-tab>
|
||||
<v-tab>Output</v-tab>
|
||||
<v-tab>3D</v-tab>
|
||||
</v-tabs>
|
||||
<div
|
||||
v-else
|
||||
style="height: 48px"
|
||||
/>
|
||||
<div style="padding-left:30px">
|
||||
<keep-alive>
|
||||
<!-- vision component -->
|
||||
<component
|
||||
:is="selectedComponent"
|
||||
ref="component"
|
||||
v-model="$store.getters.pipeline"
|
||||
@update="$emit('save')"
|
||||
/>
|
||||
</keep-alive>
|
||||
</div>
|
||||
</v-col>
|
||||
<v-col
|
||||
cols="12"
|
||||
sm="12"
|
||||
md="5"
|
||||
xl="6"
|
||||
class="colsClass"
|
||||
>
|
||||
<div>
|
||||
<!-- camera image tabs -->
|
||||
<v-tabs
|
||||
v-if="($store.getters.currentPipelineIndex + 1) !== 0"
|
||||
v-model="outputShowThresholded"
|
||||
background-color="#232c37"
|
||||
<v-card
|
||||
color="primary"
|
||||
height="100%"
|
||||
dark
|
||||
height="48"
|
||||
slider-color="#ffd843"
|
||||
centered
|
||||
style="padding-bottom:10px"
|
||||
@change="handleTruthyPipelineData('outputShowThresholded')"
|
||||
>
|
||||
<v-tab>Normal</v-tab>
|
||||
<v-tab>Threshold</v-tab>
|
||||
</v-tabs>
|
||||
<div
|
||||
v-else
|
||||
style="height: 58px"
|
||||
/>
|
||||
<!-- camera image stream -->
|
||||
<div class="videoClass">
|
||||
<v-row align="center">
|
||||
<div style="position: relative; width: 100%; height: 100%;">
|
||||
<cvImage
|
||||
:address="$store.getters.streamAddress"
|
||||
:scale="75"
|
||||
@click="onImageClick"
|
||||
/>
|
||||
<span style=" position: absolute; top: 0.2%; left: 13%;">FPS: {{ parseFloat(fps).toFixed(2) }}</span>
|
||||
<span style=" position: absolute; top: 0.2%; right: 13%;">Latency: {{ parseFloat(latency).toFixed(2) }}ms</span>
|
||||
</div>
|
||||
</v-row>
|
||||
|
||||
<v-row align="center">
|
||||
<v-simple-table
|
||||
style="text-align: center;background-color: transparent; display: block;margin: auto"
|
||||
dense
|
||||
dark
|
||||
<v-card-title
|
||||
class="pb-0 mb-0 pl-4 pt-1"
|
||||
style="height: 10%;"
|
||||
>
|
||||
Cameras
|
||||
</v-card-title>
|
||||
<v-row
|
||||
align="center"
|
||||
style="height: 90%;"
|
||||
>
|
||||
<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)"
|
||||
>
|
||||
<template v-slot:default>
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="text-center">
|
||||
Target
|
||||
</th>
|
||||
<th class="text-center">
|
||||
Pitch
|
||||
</th>
|
||||
<th class="text-center">
|
||||
Yaw
|
||||
</th>
|
||||
<th class="text-center">
|
||||
Area
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr
|
||||
v-for="(value, index) in $store.getters.currentPipelineResults.targets"
|
||||
:key="index"
|
||||
>
|
||||
<td>{{ index }}</td>
|
||||
<td>{{ parseFloat(value['pitch']).toFixed(2) }}</td>
|
||||
<td>{{ parseFloat(value['yaw']).toFixed(2) }}</td>
|
||||
<td>{{ parseFloat(value['area']).toFixed(2) }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</template>
|
||||
</v-simple-table>
|
||||
<div style="position: relative; width: 100%; height: 100%;">
|
||||
<cvImage
|
||||
:address="$store.getters.streamAddress[idx]"
|
||||
scale="100"
|
||||
max-height="300px"
|
||||
max-height-md="320px"
|
||||
max-height-xl="450px"
|
||||
:alt="'Stream' + idx"
|
||||
@click="onImageClick"
|
||||
/>
|
||||
<span style="position: absolute; top: 2%; left: 2%; font-size: 28px; -webkit-text-stroke: 1px black;">{{ parseFloat(fps).toFixed(2) }}</span>
|
||||
</div>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</div>
|
||||
</div>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card>
|
||||
</v-col>
|
||||
<v-col
|
||||
cols="12"
|
||||
class="pb-3"
|
||||
:lg="$store.getters.isDriverMode ? 12 : 4"
|
||||
align-self="stretch"
|
||||
>
|
||||
<v-card
|
||||
color="primary"
|
||||
>
|
||||
<camera-and-pipeline-select />
|
||||
</v-card>
|
||||
<v-card
|
||||
v-if="!$store.getters.isDriverMode"
|
||||
class="mt-3"
|
||||
color="primary"
|
||||
>
|
||||
<v-row
|
||||
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-btn color="secondary">
|
||||
<v-icon>mdi-cube-outline</v-icon>
|
||||
<span>3D</span>
|
||||
</v-btn>
|
||||
<v-btn color="secondary">
|
||||
<v-icon>mdi-crop-square</v-icon>
|
||||
<span>2D</span>
|
||||
</v-btn>
|
||||
</v-btn-toggle>
|
||||
</v-col>
|
||||
<v-col lg="12">
|
||||
<p style="color: white;">
|
||||
Stream display:
|
||||
</p>
|
||||
<v-btn-toggle
|
||||
v-model="selectedOutputs"
|
||||
:multiple="$vuetify.breakpoint.mdAndUp"
|
||||
mandatory
|
||||
dark
|
||||
class="fill"
|
||||
>
|
||||
<v-btn
|
||||
color="secondary"
|
||||
class="fill"
|
||||
>
|
||||
<v-icon>mdi-palette</v-icon>
|
||||
<span>Normal</span>
|
||||
</v-btn>
|
||||
<v-btn
|
||||
color="secondary"
|
||||
class="fill"
|
||||
>
|
||||
<v-icon>mdi-compare</v-icon>
|
||||
<span>Threshold</span>
|
||||
</v-btn>
|
||||
</v-btn-toggle>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card>
|
||||
</v-col>
|
||||
</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-card
|
||||
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-tab
|
||||
v-for="(tab, i) in tabs.filter(it => it.name !== '3D' || is3D)"
|
||||
:key="i"
|
||||
>
|
||||
{{ tab.name }}
|
||||
</v-tab>
|
||||
</v-tabs>
|
||||
<div class="pl-4 pr-4 pt-2">
|
||||
<keep-alive>
|
||||
<!-- vision component -->
|
||||
<component
|
||||
:is="(tabs[selectedTabs[idx]] || tabs[0]).component"
|
||||
ref="component"
|
||||
v-model="$store.getters.pipeline"
|
||||
:is3d="is3D"
|
||||
@update="$emit('save')"
|
||||
/>
|
||||
</keep-alive>
|
||||
</div>
|
||||
</v-card>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-container>
|
||||
<!-- snack bar -->
|
||||
<v-snackbar
|
||||
v-model="snackbar"
|
||||
@@ -144,12 +189,13 @@
|
||||
|
||||
<script>
|
||||
import CameraAndPipelineSelect from "../components/pipeline/CameraAndPipelineSelect";
|
||||
import cvImage from '../components/common/cv-image'
|
||||
import InputTab from './PipelineViews/InputTab'
|
||||
import ThresholdTab from './PipelineViews/ThresholdTab'
|
||||
import ContoursTab from './PipelineViews/ContoursTab'
|
||||
import OutputTab from './PipelineViews/OutputTab'
|
||||
import pnpTab from './PipelineViews/3D'
|
||||
import cvImage from '../components/common/cv-image';
|
||||
import InputTab from './PipelineViews/InputTab';
|
||||
import ThresholdTab from './PipelineViews/ThresholdTab';
|
||||
import ContoursTab from './PipelineViews/ContoursTab';
|
||||
import OutputTab from './PipelineViews/OutputTab';
|
||||
import TargetsTab from "./PipelineViews/TargetsTab";
|
||||
import PnPTab from './PipelineViews/PnPTab';
|
||||
|
||||
export default {
|
||||
name: 'CameraTab',
|
||||
@@ -160,26 +206,107 @@
|
||||
ThresholdTab,
|
||||
ContoursTab,
|
||||
OutputTab,
|
||||
pnpTab,
|
||||
TargetsTab,
|
||||
PnPTab,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
selectedTab: 0,
|
||||
selectedTabs: [0, 0, 0, 0],
|
||||
snackbar: false,
|
||||
is3D: false,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
outputShowThresholded: {
|
||||
tabGroups: {
|
||||
get() {
|
||||
return this.$store.getters.currentPipelineSettings.outputShowThresholded ? 1 : 0;
|
||||
},
|
||||
set(value) {
|
||||
this.$store.commit('mutatePipeline', {'outputShowThresholded': !!value});
|
||||
let tabs = {
|
||||
input: {
|
||||
name: "Input",
|
||||
component: "InputTab",
|
||||
},
|
||||
threshold: {
|
||||
name: "Threshold",
|
||||
component: "ThresholdTab",
|
||||
},
|
||||
contours: {
|
||||
name: "Contours",
|
||||
component: "ContoursTab",
|
||||
},
|
||||
output: {
|
||||
name: "Output",
|
||||
component: "OutputTab",
|
||||
},
|
||||
targets: {
|
||||
name: "Target Info",
|
||||
component: "TargetsTab",
|
||||
},
|
||||
pnp: {
|
||||
name: "3D",
|
||||
component: "PnPTab",
|
||||
}
|
||||
};
|
||||
|
||||
// 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.state.compactMode || this.$store.getters.isDriverMode) {
|
||||
// One big tab group with all the tabs
|
||||
ret[0] = Object.values(tabs);
|
||||
} else if (this.$vuetify.breakpoint.mdAndDown) {
|
||||
// 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.output];
|
||||
ret[1] = [tabs.targets, tabs.pnp];
|
||||
} 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.output];
|
||||
ret[2] = [tabs.targets, tabs.pnp];
|
||||
} 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.output]
|
||||
ret[3] = [tabs.targets, tabs.pnp];
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
},
|
||||
selectedComponent: {
|
||||
processingMode: {
|
||||
get() {
|
||||
return (this.$store.getters.currentPipelineIndex + 1) === 0 ? "InputTab" : ["InputTab", "ThresholdTab", "ContoursTab", "OutputTab", "pnpTab"][this.selectedTab];
|
||||
return this.is3D ? 0 : 1;
|
||||
},
|
||||
set(value) {
|
||||
this.is3D = value === 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 = 0;
|
||||
if (!this.$store.getters.isDriverMode) {
|
||||
ret = this.$store.state.selectedOutputs || [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
|
||||
value.sort(); // Sort for visual consistency
|
||||
valToCommit = value;
|
||||
} else if (value) {
|
||||
// Value is assumed to be a number, so we wrap it into an array
|
||||
valToCommit = [value];
|
||||
}
|
||||
this.$store.commit("selectedOutputs", valToCommit);
|
||||
// TODO: Currently the backend just sends both streams regardless of the selected outputs value, so we don't need to send anything
|
||||
// this.handlePipelineUpdate('selectedOutputs', valToCommit);
|
||||
}
|
||||
},
|
||||
fps: {
|
||||
@@ -204,6 +331,16 @@
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.v-btn-toggle.fill {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.v-btn-toggle.fill > .v-btn {
|
||||
width: 50%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.colsClass {
|
||||
padding: 0 !important;
|
||||
}
|
||||
@@ -216,5 +353,4 @@
|
||||
width: 80px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
</style>
|
||||
@@ -1,55 +1,58 @@
|
||||
<template>
|
||||
<div>
|
||||
<CVrangeSlider
|
||||
v-model="contourArea"
|
||||
name="Area"
|
||||
:min="0"
|
||||
:max="100"
|
||||
:step="0.1"
|
||||
@input="handlePipelineData('contourArea')"
|
||||
@rollback="e=> rollback('contourArea',e)"
|
||||
/>
|
||||
<CVrangeSlider
|
||||
v-model="contourRatio"
|
||||
name="Ratio (W/H)"
|
||||
:min="0"
|
||||
:max="100"
|
||||
:step="0.1"
|
||||
@input="handlePipelineData('contourRatio')"
|
||||
@rollback="e=> rollback('contourRatio',e)"
|
||||
/>
|
||||
<CVrangeSlider
|
||||
v-model="contourExtent"
|
||||
name="Extent"
|
||||
:min="0"
|
||||
:max="100"
|
||||
@input="handlePipelineData('contourExtent')"
|
||||
@rollback="e=> rollback('contourExtent',e)"
|
||||
/>
|
||||
<CVslider
|
||||
v-model="contourSpecklePercentage"
|
||||
name="Speckle Rejection"
|
||||
:min="0"
|
||||
:max="100"
|
||||
@input="handlePipelineData('contourSpecklePercentage')"
|
||||
@rollback="e=> rollback('contourSpecklePercentage',e)"
|
||||
/>
|
||||
<CVselect
|
||||
v-model="contourGroupingMode"
|
||||
name="Target Group"
|
||||
:list="['Single','Dual']"
|
||||
@input="handlePipelineData('targetGroup')"
|
||||
@rollback="e=> rollback('targetGroup',e)"
|
||||
/>
|
||||
<CVselect
|
||||
v-model="contourIntersection"
|
||||
name="Target Intersection"
|
||||
:list="['None','Up','Down','Left','Right']"
|
||||
:disabled="contourGroupingMode === 0"
|
||||
@input="handlePipelineData('contourIntersection')"
|
||||
@rollback="e=> rollback('contourIntersection',e)"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<CVrangeSlider
|
||||
v-model="contourArea"
|
||||
name="Area"
|
||||
min="0"
|
||||
max="100"
|
||||
step="0.1"
|
||||
@input="handlePipelineData('contourArea')"
|
||||
@rollback="e=> rollback('contourArea',e)"
|
||||
/>
|
||||
<CVrangeSlider
|
||||
v-model="contourRatio"
|
||||
name="Ratio (W/H)"
|
||||
min="0"
|
||||
max="100"
|
||||
step="0.1"
|
||||
@input="handlePipelineData('contourRatio')"
|
||||
@rollback="e=> rollback('contourRatio',e)"
|
||||
/>
|
||||
<CVrangeSlider
|
||||
v-model="contourExtent"
|
||||
name="Extent"
|
||||
min="0"
|
||||
max="100"
|
||||
@input="handlePipelineData('contourExtent')"
|
||||
@rollback="e=> rollback('contourExtent',e)"
|
||||
/>
|
||||
<CVslider
|
||||
v-model="contourSpecklePercentage"
|
||||
name="Speckle Rejection"
|
||||
min="0"
|
||||
max="100"
|
||||
:slider-cols="largeBox"
|
||||
@input="handlePipelineData('contourSpecklePercentage')"
|
||||
@rollback="e=> rollback('contourSpecklePercentage',e)"
|
||||
/>
|
||||
<CVselect
|
||||
v-model="contourGroupingMode"
|
||||
name="Target Group"
|
||||
:select-cols="largeBox"
|
||||
:list="['Single','Dual']"
|
||||
@input="handlePipelineData('targetGroup')"
|
||||
@rollback="e=> rollback('targetGroup',e)"
|
||||
/>
|
||||
<CVselect
|
||||
v-model="contourIntersection"
|
||||
name="Target Intersection"
|
||||
:select-cols="largeBox"
|
||||
:list="['None','Up','Down','Left','Right']"
|
||||
:disabled="contourGroupingMode === 0"
|
||||
@input="handlePipelineData('contourIntersection')"
|
||||
@rollback="e=> rollback('contourIntersection',e)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
@@ -71,6 +74,14 @@
|
||||
return {}
|
||||
},
|
||||
computed: {
|
||||
largeBox: {
|
||||
get() {
|
||||
// Sliders and selectors should be fuller width if we're on screen size medium and
|
||||
// up and either not in compact mode (because the tab will be 100% screen width),
|
||||
// or in driver mode (where the card will also be 100% screen width).
|
||||
return this.$vuetify.breakpoint.mdAndUp && (!this.$store.state.compactMode || this.$store.getters.isDriverMode) ? 10 : 8;
|
||||
}
|
||||
},
|
||||
contourArea: {
|
||||
get() {
|
||||
return this.$store.getters.currentPipelineSettings.contourArea
|
||||
|
||||
@@ -3,16 +3,18 @@
|
||||
<CVslider
|
||||
v-model="cameraExposure"
|
||||
name="Exposure"
|
||||
:min="0"
|
||||
:max="100"
|
||||
min="0"
|
||||
max="100"
|
||||
:slider-cols="largeBox"
|
||||
@input="handlePipelineData('cameraExposure')"
|
||||
@rollback="e => rollback('cameraExposure', e)"
|
||||
/>
|
||||
<CVslider
|
||||
v-model="cameraBrightness"
|
||||
name="Brightness"
|
||||
:min="0"
|
||||
:max="100"
|
||||
min="0"
|
||||
max="100"
|
||||
:slider-cols="largeBox"
|
||||
@input="handlePipelineData('cameraBrightness')"
|
||||
@rollback="e => rollback('cameraBrightness', e)"
|
||||
/>
|
||||
@@ -20,8 +22,9 @@
|
||||
v-if="cameraGain !== -1"
|
||||
v-model="cameraGain"
|
||||
name="Gain"
|
||||
:min="0"
|
||||
:max="100"
|
||||
min="0"
|
||||
max="100"
|
||||
:slider-cols="largeBox"
|
||||
@input="handlePipelineData('cameraGain')"
|
||||
@rollback="e => rollback('cameraGain', e)"
|
||||
/>
|
||||
@@ -29,6 +32,7 @@
|
||||
v-model="inputImageRotationMode"
|
||||
name="Orientation"
|
||||
:list="['Normal','90° CW','180°','90° CCW']"
|
||||
:select-cols="largeBox"
|
||||
@input="handlePipelineData('inputImageRotationMode')"
|
||||
@rollback="e => rollback('inputImageRotationMode',e)"
|
||||
/>
|
||||
@@ -36,6 +40,7 @@
|
||||
v-model="cameraVideoModeIndex"
|
||||
name="Resolution"
|
||||
:list="resolutionList"
|
||||
:select-cols="largeBox"
|
||||
@input="handlePipelineData('cameraVideoModeIndex')"
|
||||
@rollback="e => rollback('cameraVideoModeIndex', e)"
|
||||
/>
|
||||
@@ -43,6 +48,7 @@
|
||||
v-model="outputFrameDivisor"
|
||||
name="Stream Resolution"
|
||||
:list="streamResolutionList"
|
||||
:select-cols="largeBox"
|
||||
@input="handlePipelineData('outputFrameDivisor')"
|
||||
@rollback="e => rollback('outputFrameDivisor', e)"
|
||||
/>
|
||||
@@ -65,6 +71,14 @@
|
||||
return {}
|
||||
},
|
||||
computed: {
|
||||
largeBox: {
|
||||
get() {
|
||||
// Sliders and selectors should be fuller width if we're on screen size medium and
|
||||
// up and either not in compact mode (because the tab will be 100% screen width),
|
||||
// or in driver mode (where the card will also be 100% screen width).
|
||||
return this.$vuetify.breakpoint.mdAndUp && (!this.$store.state.compactMode || this.$store.getters.isDriverMode) ? 10 : 8;
|
||||
}
|
||||
},
|
||||
cameraExposure: {
|
||||
get() {
|
||||
return parseInt(this.$store.getters.currentPipelineSettings.cameraExposure);
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
<template>
|
||||
<div>
|
||||
<span>Contour Sorting</span>
|
||||
<v-divider class="mt-2" />
|
||||
<CVselect
|
||||
v-model="contourSortMode"
|
||||
name="Sort Mode"
|
||||
@@ -27,14 +29,13 @@
|
||||
<CVswitch
|
||||
v-model="outputShowMultipleTargets"
|
||||
name="Show Multiple Targets"
|
||||
class="mb-4"
|
||||
@input="handlePipelineData('outputShowMultipleTargets')"
|
||||
|
||||
@rollback="e=> rollback('outputShowMultipleTargets', e)"
|
||||
/>
|
||||
<span>Robot Offset:</span>
|
||||
<v-divider
|
||||
dark
|
||||
color="white"
|
||||
/>
|
||||
<span>Robot Offset</span>
|
||||
<v-divider class="mt-2" />
|
||||
<CVselect
|
||||
v-model="offsetRobotOffsetMode"
|
||||
name="Robot Offset Mode"
|
||||
|
||||
@@ -1,71 +1,44 @@
|
||||
<template>
|
||||
<div>
|
||||
<v-row
|
||||
align="center"
|
||||
justify="start"
|
||||
dense
|
||||
<!-- Special hidden upload input that gets 'clicked' when the user selects the right dropdown item' -->
|
||||
<input
|
||||
ref="file"
|
||||
type="file"
|
||||
accept=".csv"
|
||||
style="display: none;"
|
||||
|
||||
@change="readFile"
|
||||
>
|
||||
<v-col :cols="6">
|
||||
<CVswitch
|
||||
v-model="value.is3D"
|
||||
:disabled="allow3D"
|
||||
name="Enable 3D"
|
||||
@input="handleData('is3D')"
|
||||
@rollback="e=> rollback('is3D',e)"
|
||||
/>
|
||||
</v-col>
|
||||
<v-col>
|
||||
<input
|
||||
ref="file"
|
||||
type="file"
|
||||
style="display: none"
|
||||
accept=".csv"
|
||||
@change="readFile"
|
||||
>
|
||||
<v-btn
|
||||
small
|
||||
@click="$refs.file.click()"
|
||||
>
|
||||
<v-icon>mdi-upload</v-icon>
|
||||
upload model
|
||||
</v-btn>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<v-select
|
||||
v-model="selectedModel"
|
||||
dark
|
||||
color="accent"
|
||||
item-color="secondary"
|
||||
label="Select a target model"
|
||||
:items="FRCtargets"
|
||||
item-text="name"
|
||||
item-value="data"
|
||||
@change="onModelSelect"
|
||||
/>
|
||||
<v-divider />
|
||||
<CVslider
|
||||
v-model="value.accuracy"
|
||||
name="Contour simplification"
|
||||
:min="0"
|
||||
:max="100"
|
||||
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)"
|
||||
@rollback="e => rollback('accuracy', e)"
|
||||
/>
|
||||
<v-divider class="pb-2" />
|
||||
<mini-map
|
||||
class="miniMapClass"
|
||||
:targets="targets"
|
||||
:horizontal-f-o-v="horizontalFOV"
|
||||
/>
|
||||
<v-row>
|
||||
<v-col>
|
||||
<mini-map
|
||||
class="miniMapClass"
|
||||
:targets="targets"
|
||||
:horizontal-f-o-v="horizontalFOV"
|
||||
/>
|
||||
</v-col>
|
||||
<v-col>
|
||||
<v-select
|
||||
v-model="selectedModel"
|
||||
:items="FRCtargets"
|
||||
item-text="name"
|
||||
item-value="data"
|
||||
dark
|
||||
color="#ffd843"
|
||||
item-color="green"
|
||||
/>
|
||||
<v-btn
|
||||
v-if="selectedModel !== null"
|
||||
small
|
||||
@click="uploadPremade"
|
||||
>
|
||||
Upload Premade
|
||||
</v-btn>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-snackbar
|
||||
v-model="snack"
|
||||
top
|
||||
@@ -79,14 +52,12 @@
|
||||
<script>
|
||||
import Papa from 'papaparse';
|
||||
import miniMap from '../../components/pipeline/3D/MiniMap';
|
||||
import CVswitch from '../../components/common/cv-switch';
|
||||
import CVslider from '../../components/common/cv-slider'
|
||||
import FRCtargetsConfig from '../../assets/FRCtargets'
|
||||
|
||||
export default {
|
||||
name: "SolvePNP",
|
||||
components: {
|
||||
CVswitch,
|
||||
CVslider,
|
||||
miniMap
|
||||
},
|
||||
@@ -94,7 +65,6 @@
|
||||
props: ['value'],
|
||||
data() {
|
||||
return {
|
||||
is3D: false,
|
||||
selectedModel: null,
|
||||
FRCtargets: null,
|
||||
snackbar: {
|
||||
@@ -107,13 +77,13 @@
|
||||
computed: {
|
||||
targets: {
|
||||
get() {
|
||||
return 330; // TODO fix
|
||||
return "FIXME"; // TODO fix
|
||||
}
|
||||
},
|
||||
horizontalFOV: {
|
||||
get() {
|
||||
let index = this.$store.state.cameraSettings.resolution;
|
||||
let FOV = this.$store.state.cameraSettings.fov;
|
||||
let index = this.$store.getters.currentPipelineSettings.cameraVideoModeIndex;
|
||||
let FOV = this.$store.getters.currentCameraSettings.fov;
|
||||
let resolution = this.$store.getters.videoFormatList[index];
|
||||
let diagonalView = FOV * (Math.PI / 180);
|
||||
let diagonalAspect = Math.hypot(resolution.width, resolution.height);
|
||||
@@ -122,14 +92,7 @@
|
||||
},
|
||||
allow3D: {
|
||||
get() {
|
||||
let index = this.$store.state.cameraSettings.resolution;
|
||||
let currentRes = this.$store.getters.videoFormatList[index];
|
||||
for (let res of this.$store.state.cameraSettings.calibrated) {
|
||||
if (currentRes.width === res.width && currentRes.height === res.height) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
return this.$store.getters.currentCameraSettings.calibrated;
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -140,6 +103,11 @@
|
||||
tmp.push({name: t, data: FRCtargetsConfig[t]})
|
||||
}
|
||||
}
|
||||
|
||||
// Special dropdown item for uploading your own model
|
||||
// data is what gets put in selectedMode, so we add a special field
|
||||
tmp.push({name: "Custom model", data: {isCustom: true}});
|
||||
|
||||
this.FRCtargets = tmp;
|
||||
},
|
||||
methods: {
|
||||
@@ -150,21 +118,30 @@
|
||||
skipEmptyLines: true
|
||||
});
|
||||
},
|
||||
onModelSelect() {
|
||||
if (this.selectedModel.isCustom) {
|
||||
this.$refs.file.click();
|
||||
} else {
|
||||
this.uploadPremade();
|
||||
}
|
||||
},
|
||||
onParse(result) {
|
||||
if (result.data.length > 0) {
|
||||
|
||||
|
||||
let data = [];
|
||||
for (let item of result.data) {
|
||||
for (let i = 0; i < result.data.length; i++) {
|
||||
let item = result.data[i];
|
||||
|
||||
let tmp = [];
|
||||
tmp.push(Number(item[0]));
|
||||
tmp.push(Number(item[1]));
|
||||
if (isNaN(tmp[0]) || isNaN(tmp[1])) {
|
||||
this.snackbar = {
|
||||
color: "error",
|
||||
text: "Error: cvs did parse correctly"
|
||||
text: `Error: custom target CSV contained a non-numeric value on line ${i + 1}`
|
||||
};
|
||||
this.snack = true;
|
||||
|
||||
this.selectedModel = null;
|
||||
return;
|
||||
}
|
||||
data.push(tmp);
|
||||
@@ -173,28 +150,32 @@
|
||||
} else {
|
||||
this.snackbar = {
|
||||
color: "error",
|
||||
text: "Error: cvs did not contain any data"
|
||||
text: "Error: custom target CSV was empty"
|
||||
};
|
||||
this.snack = true;
|
||||
|
||||
this.selectedModel = null;
|
||||
}
|
||||
},
|
||||
uploadPremade() {
|
||||
this.uploadModel(this.selectedModel);
|
||||
this.uploadModel(this.selectedModel, true);
|
||||
},
|
||||
uploadModel(model) {
|
||||
uploadModel(model, premade = false) {
|
||||
this.axios.post("http://" + this.$address + "/api/vision/pnpModel", model).then(() => {
|
||||
this.snackbar = {
|
||||
color: "success",
|
||||
text: "File uploaded successfully"
|
||||
text: premade ? "Target model changed successfully" : "Custom target model uploaded and selected successfully"
|
||||
};
|
||||
this.snack = true;
|
||||
}).catch(() => {
|
||||
this.snackbar = {
|
||||
color: "error",
|
||||
text: "An error occurred"
|
||||
text: "An error occurred selecting a target model"
|
||||
};
|
||||
this.snack = true;
|
||||
})
|
||||
|
||||
this.selectedModel = null;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -202,7 +183,10 @@
|
||||
|
||||
<style scoped>
|
||||
.miniMapClass {
|
||||
width: 50% !important;
|
||||
height: 50% !important;
|
||||
width: 400px !important;
|
||||
height: 100% !important;
|
||||
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
</style>
|
||||
100
photon-client/src/views/PipelineViews/TargetsTab.vue
Normal file
100
photon-client/src/views/PipelineViews/TargetsTab.vue
Normal file
@@ -0,0 +1,100 @@
|
||||
<template>
|
||||
<div>
|
||||
<v-row
|
||||
align="start"
|
||||
class="pb-4"
|
||||
style="height: 300px;"
|
||||
>
|
||||
<!-- Simple table height must be set here and in the CSS for the fixed-header to work -->
|
||||
<v-simple-table
|
||||
fixed-header
|
||||
height="100%"
|
||||
dense
|
||||
dark
|
||||
>
|
||||
<template v-slot:default>
|
||||
<thead style="font-size: 20px;">
|
||||
<tr>
|
||||
<th class="text-center">
|
||||
Target
|
||||
</th>
|
||||
<template v-if="!is3D">
|
||||
<th class="text-center">
|
||||
Pitch
|
||||
</th>
|
||||
<th class="text-center">
|
||||
Yaw
|
||||
</th>
|
||||
</template>
|
||||
<th class="text-center">
|
||||
Area
|
||||
</th>
|
||||
<template v-if="is3D">
|
||||
<th class="text-center">
|
||||
X
|
||||
</th>
|
||||
<th class="text-center">
|
||||
Y
|
||||
</th>
|
||||
<th class="text-center">
|
||||
Angle
|
||||
</th>
|
||||
</template>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr
|
||||
v-for="(value, index) in $store.getters.currentPipelineResults.targets"
|
||||
:key="index"
|
||||
>
|
||||
<td>{{ index }}</td>
|
||||
<template v-if="!is3D">
|
||||
<td>{{ parseFloat(value.pitch).toFixed(2) }}</td>
|
||||
<td>{{ parseFloat(value.yaw).toFixed(2) }}</td>
|
||||
</template>
|
||||
<td>{{ parseFloat(value.area).toFixed(2) }}</td>
|
||||
<template v-if="is3D">
|
||||
<!-- TODO: Make sure that units are correct -->
|
||||
<td>{{ parseFloat(value.pose.x).toFixed(2) }} m</td>
|
||||
<td>{{ parseFloat(value.pose.y).toFixed(2) }} m</td>
|
||||
<td>{{ parseFloat(value.pose.rot).toFixed(2) }}°</td>
|
||||
</template>
|
||||
</tr>
|
||||
</tbody>
|
||||
</template>
|
||||
</v-simple-table>
|
||||
</v-row>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "TargetsTab",
|
||||
props: {
|
||||
is3D: Boolean,
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<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>
|
||||
@@ -1,76 +1,75 @@
|
||||
<template>
|
||||
<div>
|
||||
<CVrangeSlider
|
||||
v-model="hsvHue"
|
||||
name="Hue"
|
||||
:min="0"
|
||||
:max="180"
|
||||
@input="handlePipelineData('hsvHue')"
|
||||
@rollback="e => rollback('hue',e)"
|
||||
/>
|
||||
<CVrangeSlider
|
||||
v-model="hsvSaturation"
|
||||
name="Saturation"
|
||||
:min="0"
|
||||
:max="255"
|
||||
@input="handlePipelineData('hsvSaturation')"
|
||||
@rollback="e => rollback('saturation',e)"
|
||||
/>
|
||||
<CVrangeSlider
|
||||
v-model="hsvValue"
|
||||
name="Value"
|
||||
:min="0"
|
||||
:max="255"
|
||||
@input="handlePipelineData('hsvValue')"
|
||||
@rollback="e => rollback('value',e)"
|
||||
/>
|
||||
<v-divider
|
||||
color="black"
|
||||
style="margin-top: 5px"
|
||||
/>
|
||||
<v-row justify="center">
|
||||
<v-btn
|
||||
style="margin: 20px;"
|
||||
color="#ffd843"
|
||||
small
|
||||
@click="setFunction(1)"
|
||||
>
|
||||
<v-icon>colorize</v-icon>
|
||||
Eye drop
|
||||
</v-btn>
|
||||
<v-btn
|
||||
style="margin: 20px;"
|
||||
color="#ffd843"
|
||||
small
|
||||
@click="setFunction(2)"
|
||||
>
|
||||
<v-icon>add</v-icon>
|
||||
Expand Selection
|
||||
</v-btn>
|
||||
<v-btn
|
||||
style="margin: 20px;"
|
||||
color="#ffd843"
|
||||
small
|
||||
@click="setFunction(3)"
|
||||
>
|
||||
<v-icon>remove</v-icon>
|
||||
Shrink Selection
|
||||
</v-btn>
|
||||
</v-row>
|
||||
<v-divider color="black"/>
|
||||
<CVswitch
|
||||
v-model="erode"
|
||||
name="Erode"
|
||||
@input="handlePipelineData('erode')"
|
||||
@rollback="e => rollback('erode',e)"
|
||||
/>
|
||||
<CVswitch
|
||||
v-model="dilate"
|
||||
name="Dilate"
|
||||
@input="handlePipelineData('dilate')"
|
||||
@rollback="e => rollback('dilate',e)"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<CVrangeSlider
|
||||
v-model="hsvHue"
|
||||
name="Hue"
|
||||
:min="0"
|
||||
:max="180"
|
||||
@input="handlePipelineData('hsvHue')"
|
||||
@rollback="e => rollback('hue',e)"
|
||||
/>
|
||||
<CVrangeSlider
|
||||
v-model="hsvSaturation"
|
||||
name="Saturation"
|
||||
:min="0"
|
||||
:max="255"
|
||||
@input="handlePipelineData('hsvSaturation')"
|
||||
@rollback="e => rollback('saturation',e)"
|
||||
/>
|
||||
<CVrangeSlider
|
||||
v-model="hsvValue"
|
||||
name="Value"
|
||||
:min="0"
|
||||
:max="255"
|
||||
@input="handlePipelineData('hsvValue')"
|
||||
@rollback="e => rollback('value',e)"
|
||||
/>
|
||||
<v-divider
|
||||
class="mt-3"
|
||||
/>
|
||||
<v-row justify="center">
|
||||
<v-btn
|
||||
color="accent"
|
||||
class="ma-5 black--text"
|
||||
small
|
||||
@click="setFunction(1)"
|
||||
>
|
||||
<v-icon>colorize</v-icon>
|
||||
Eye drop
|
||||
</v-btn>
|
||||
<v-btn
|
||||
color="accent"
|
||||
class="ma-5 black--text"
|
||||
small
|
||||
@click="setFunction(2)"
|
||||
>
|
||||
<v-icon>add</v-icon>
|
||||
Expand Selection
|
||||
</v-btn>
|
||||
<v-btn
|
||||
color="accent"
|
||||
class="ma-5 black--text"
|
||||
small
|
||||
@click="setFunction(3)"
|
||||
>
|
||||
<v-icon>remove</v-icon>
|
||||
Shrink Selection
|
||||
</v-btn>
|
||||
</v-row>
|
||||
<v-divider />
|
||||
<CVswitch
|
||||
v-model="erode"
|
||||
name="Erode"
|
||||
@input="handlePipelineData('erode')"
|
||||
@rollback="e => rollback('erode',e)"
|
||||
/>
|
||||
<CVswitch
|
||||
v-model="dilate"
|
||||
name="Dilate"
|
||||
@input="handlePipelineData('dilate')"
|
||||
@rollback="e => rollback('dilate',e)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
@@ -184,8 +183,4 @@
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="" scoped>
|
||||
|
||||
</style>
|
||||
</script>
|
||||
Reference in New Issue
Block a user