mirror of
https://github.com/PhotonVision/photonvision
synced 2026-06-27 02:01: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,36 +1,86 @@
|
||||
<template>
|
||||
<v-app>
|
||||
<v-app-bar
|
||||
app
|
||||
dense
|
||||
clipped-left
|
||||
color="#006492"
|
||||
<!-- Although most of the app runs with the "light" theme, the navigation drawer needs to have white text and icons so it uses the dark theme-->
|
||||
<v-navigation-drawer
|
||||
dark
|
||||
app
|
||||
permanent
|
||||
:mini-variant="compact"
|
||||
color="primary"
|
||||
>
|
||||
<img
|
||||
class="imgClass"
|
||||
src="./assets/logo.png"
|
||||
>
|
||||
<div class="flex-grow-1" />
|
||||
<v-toolbar-items>
|
||||
<v-tabs
|
||||
background-color="#006492"
|
||||
dark
|
||||
height="48"
|
||||
slider-color="#ffd843"
|
||||
<v-list>
|
||||
<!-- List item for the heading; note that there are some tricks in setting padding and image width make things look right -->
|
||||
<v-list-item :class="compact ? 'pr-0 pl-0' : ''">
|
||||
<v-list-item-icon class="mr-0">
|
||||
<img
|
||||
v-if="!compact"
|
||||
class="logo"
|
||||
src="./assets/logoLarge.png"
|
||||
>
|
||||
<img
|
||||
v-else
|
||||
class="logo"
|
||||
src="./assets/logoSmall.png"
|
||||
>
|
||||
</v-list-item-icon>
|
||||
</v-list-item>
|
||||
|
||||
<v-divider />
|
||||
|
||||
<v-list-item
|
||||
link
|
||||
to="dashboard"
|
||||
>
|
||||
<v-tab to="vision">
|
||||
Vision
|
||||
</v-tab>
|
||||
<v-tab to="settings">
|
||||
Settings
|
||||
</v-tab>
|
||||
<v-tab to="docs">
|
||||
Docs
|
||||
</v-tab>
|
||||
</v-tabs>
|
||||
</v-toolbar-items>
|
||||
</v-app-bar>
|
||||
<v-list-item-icon>
|
||||
<v-icon>mdi-view-dashboard</v-icon>
|
||||
</v-list-item-icon>
|
||||
<v-list-item-content>
|
||||
<v-list-item-title>Dashboard</v-list-item-title>
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
<v-list-item
|
||||
link
|
||||
to="settings"
|
||||
>
|
||||
<!-- TODO: Expandable sub-elements? -->
|
||||
<v-list-item-icon>
|
||||
<v-icon>mdi-settings</v-icon>
|
||||
</v-list-item-icon>
|
||||
<v-list-item-content>
|
||||
<v-list-item-title>Settings</v-list-item-title>
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
<v-list-item
|
||||
link
|
||||
to="docs"
|
||||
>
|
||||
<v-list-item-icon>
|
||||
<v-icon>mdi-bookshelf</v-icon>
|
||||
</v-list-item-icon>
|
||||
<v-list-item-content>
|
||||
<v-list-item-title>Documentation</v-list-item-title>
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
|
||||
<v-list-item
|
||||
v-if="this.$vuetify.breakpoint.mdAndUp"
|
||||
link
|
||||
@click.stop="toggleCompactMode"
|
||||
>
|
||||
<v-list-item-icon>
|
||||
<v-icon v-if="compact">
|
||||
mdi-chevron-right
|
||||
</v-icon>
|
||||
<v-icon v-else>
|
||||
mdi-chevron-left
|
||||
</v-icon>
|
||||
</v-list-item-icon>
|
||||
<v-list-item-content>
|
||||
<v-list-item-title>Advanced Mode</v-list-item-title>
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-navigation-drawer>
|
||||
<v-content>
|
||||
<v-container
|
||||
fluid
|
||||
@@ -43,7 +93,7 @@
|
||||
v-model="saveSnackbar"
|
||||
:timeout="1000"
|
||||
top
|
||||
color="#ffd843"
|
||||
color="accent"
|
||||
>
|
||||
<div style="text-align: center;width: 100%;">
|
||||
<h4>Saved All changes</h4>
|
||||
@@ -65,7 +115,7 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import logView from '@femessage/log-viewer'
|
||||
import logView from '@femessage/log-viewer';
|
||||
|
||||
export default {
|
||||
name: 'App',
|
||||
@@ -75,7 +125,7 @@
|
||||
data: () => ({
|
||||
timer: undefined,
|
||||
isLogger: false,
|
||||
log: ""
|
||||
log: "",
|
||||
}),
|
||||
computed: {
|
||||
saveSnackbar: {
|
||||
@@ -85,6 +135,15 @@
|
||||
set(value) {
|
||||
this.$store.commit("saveBar", value);
|
||||
}
|
||||
},
|
||||
compact: {
|
||||
get() {
|
||||
return this.$store.state.compactMode === undefined ? this.$vuetify.breakpoint.smAndDown : this.$store.state.compactMode || this.$vuetify.breakpoint.smAndDown;
|
||||
},
|
||||
set(value) {
|
||||
// compactMode is the user's preference for compact mode; it overrides screen size
|
||||
this.$store.commit("compactMode", value);
|
||||
},
|
||||
}
|
||||
},
|
||||
created() {
|
||||
@@ -138,6 +197,9 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
toggleCompactMode() {
|
||||
this.compact = !this.compact;
|
||||
},
|
||||
saveSettings() {
|
||||
clearInterval(this.timer);
|
||||
this.saveSnackbar = true;
|
||||
@@ -163,12 +225,10 @@
|
||||
</style>
|
||||
|
||||
<style>
|
||||
|
||||
.imgClass {
|
||||
width: auto;
|
||||
height: 45px;
|
||||
vertical-align: middle;
|
||||
padding-right: 5px;
|
||||
.logo {
|
||||
width: 100%;
|
||||
height: 70px;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.loggerClass {
|
||||
@@ -209,4 +269,11 @@
|
||||
span {
|
||||
color: white;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style>
|
||||
/* Hack */
|
||||
.v-divider {
|
||||
border-color: #23add9 !important;
|
||||
}
|
||||
</style>
|
||||
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 13 KiB |
BIN
photon-client/src/assets/logoSmall.png
Normal file
BIN
photon-client/src/assets/logoSmall.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 34 KiB |
@@ -3,7 +3,6 @@
|
||||
id="CameraStream"
|
||||
:style="styleObject"
|
||||
:src="address"
|
||||
crossorigin="Anonymous"
|
||||
alt=""
|
||||
@click="e => $emit('click', e)"
|
||||
>
|
||||
@@ -13,19 +12,27 @@
|
||||
export default {
|
||||
name: "CvImage",
|
||||
// eslint-disable-next-line vue/require-prop-types
|
||||
props: ['address', 'scale'],
|
||||
props: ['address', 'scale', 'maxHeight', 'maxHeightMd', 'maxHeightXl'],
|
||||
data: () => {
|
||||
return {}
|
||||
},
|
||||
computed: {
|
||||
styleObject: {
|
||||
get() {
|
||||
return {
|
||||
width: `${this.scale}%`,
|
||||
height: `${this.scale}%`,
|
||||
display: 'block',
|
||||
margin: 'auto'
|
||||
let ret = {
|
||||
"object-fit": "contain",
|
||||
"max-height": this.maxHeight,
|
||||
width: `${this.scale}%`,
|
||||
height: `${this.scale}%`,
|
||||
};
|
||||
|
||||
if (this.$vuetify.breakpoint.xl) {
|
||||
ret["max-height"] = this.maxHeightXl;
|
||||
} else if (this.$vuetify.breakpoint.mdAndUp) {
|
||||
ret["max-height"] = this.maxHeightMd;
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -4,10 +4,10 @@
|
||||
dense
|
||||
align="center"
|
||||
>
|
||||
<v-col :cols="3">
|
||||
<span>{{ name }}</span>
|
||||
<v-col cols="4">
|
||||
<span class="ml-2">{{ name }}</span>
|
||||
</v-col>
|
||||
<v-col :cols="9">
|
||||
<v-col cols="8">
|
||||
<v-text-field
|
||||
v-model="localValue"
|
||||
dark
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
<template>
|
||||
<div>
|
||||
<v-row dense align="center">
|
||||
<v-row
|
||||
dense
|
||||
align="center"
|
||||
>
|
||||
<v-col :cols="2">
|
||||
<span>{{ name }}</span>
|
||||
</v-col>
|
||||
|
||||
@@ -4,18 +4,18 @@
|
||||
dense
|
||||
align="center"
|
||||
>
|
||||
<v-col :cols="3">
|
||||
<v-col :cols="12 - (selectCols || 9)">
|
||||
<span>{{ name }}</span>
|
||||
</v-col>
|
||||
<v-col :cols="9">
|
||||
<v-col :cols="selectCols || 9">
|
||||
<v-select
|
||||
v-model="localValue"
|
||||
:items="indexList"
|
||||
item-text="name"
|
||||
item-value="index"
|
||||
dark
|
||||
color="#fcf6de"
|
||||
item-color="blue"
|
||||
color="accent"
|
||||
item-color="secondary"
|
||||
:disabled="disabled"
|
||||
@change="$emit('rollback', localValue)"
|
||||
/>
|
||||
@@ -28,7 +28,7 @@
|
||||
export default {
|
||||
name: 'Select',
|
||||
// eslint-disable-next-line vue/require-prop-types
|
||||
props: ['list', 'name', 'value', 'disabled'],
|
||||
props: ['list', 'name', 'value', 'disabled', 'selectCols'],
|
||||
data() {
|
||||
return {}
|
||||
},
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
<template>
|
||||
<div>
|
||||
<v-row dense align="center">
|
||||
<v-col :cols="2">
|
||||
<v-row
|
||||
dense
|
||||
align="center"
|
||||
>
|
||||
<v-col :cols="12 - (sliderCols || 8)">
|
||||
<span>{{ name }}</span>
|
||||
</v-col>
|
||||
<v-col :cols="10">
|
||||
<v-col :cols="sliderCols || 8">
|
||||
<v-slider
|
||||
:value="localValue"
|
||||
dark
|
||||
@@ -12,7 +15,8 @@
|
||||
:max="max"
|
||||
:min="min"
|
||||
hide-details
|
||||
color="#ffd843"
|
||||
color="accent"
|
||||
:disabled="disabled"
|
||||
:step="step"
|
||||
@start="isClicked = true"
|
||||
@end="isClicked = false"
|
||||
@@ -25,6 +29,7 @@
|
||||
dark
|
||||
:max="max"
|
||||
:min="min"
|
||||
:disabled="disabled"
|
||||
:value="localValue"
|
||||
class="mt-0 pt-0"
|
||||
hide-details
|
||||
@@ -47,7 +52,7 @@
|
||||
export default {
|
||||
name: "Slider",
|
||||
// eslint-disable-next-line vue/require-prop-types
|
||||
props: ["min", "max", "name", "value", "step"],
|
||||
props: ["min", "max", "name", "value", "step", "sliderCols", "disabled"],
|
||||
data() {
|
||||
return {
|
||||
isFocused: false,
|
||||
@@ -63,7 +68,7 @@ export default {
|
||||
set(value) {
|
||||
this.$emit("input", value);
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
handleChange(val) {
|
||||
|
||||
@@ -1,59 +1,25 @@
|
||||
<template>
|
||||
<div>
|
||||
<v-row
|
||||
style="width: 400px;"
|
||||
align="center"
|
||||
>
|
||||
<canvas
|
||||
id="canvasId"
|
||||
width="800"
|
||||
height="800"
|
||||
/>
|
||||
</v-row>
|
||||
<v-row
|
||||
style="width: 400px;"
|
||||
align="center"
|
||||
>
|
||||
<v-simple-table
|
||||
style="text-align: center;background-color: transparent; display: block;margin: auto"
|
||||
dense
|
||||
dark
|
||||
<v-row>
|
||||
<v-col
|
||||
align="center"
|
||||
cols="12"
|
||||
>
|
||||
<template v-slot:default>
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="text-center">
|
||||
Target
|
||||
</th>
|
||||
<th class="text-center">
|
||||
X
|
||||
</th>
|
||||
<th class="text-center">
|
||||
Y
|
||||
</th>
|
||||
<th class="text-center">
|
||||
Angle
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr
|
||||
v-for="(target, index) in targets"
|
||||
:key="index"
|
||||
>
|
||||
<td>{{ index }}</td>
|
||||
<td>{{ target.pose.translation.x.toFixed(2) }}</td>
|
||||
<td>{{ target.pose.translation.y.toFixed(2) }}</td>
|
||||
<td>{{ target.pose.rotation.radians.toFixed(2) }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</template>
|
||||
</v-simple-table>
|
||||
<span class="text--white">Target Location</span>
|
||||
<canvas
|
||||
id="canvasId"
|
||||
class="mt-2"
|
||||
width="800"
|
||||
height="800"
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import theme from "../../../theme";
|
||||
|
||||
export default {
|
||||
name: "MiniMap",
|
||||
props: {
|
||||
@@ -136,7 +102,7 @@
|
||||
// so the rect needs to be offset accordingly when drawn
|
||||
this.ctx.rect(-this.targetWidth / 2, -this.targetHeight / 2, this.targetWidth, this.targetHeight);
|
||||
|
||||
this.ctx.fillStyle = "#01a209";
|
||||
this.ctx.fillStyle = theme.accent;
|
||||
this.ctx.fill();
|
||||
|
||||
// restore the context to its untranslated/unrotated state
|
||||
@@ -176,7 +142,7 @@
|
||||
#canvasId {
|
||||
width: 400px;
|
||||
height: 400px;
|
||||
background-color: #2b2b2b;
|
||||
background-color: #232C37;
|
||||
border-radius: 5px;
|
||||
border: 2px solid grey;
|
||||
box-shadow: 0 0 5px 1px;
|
||||
|
||||
@@ -2,27 +2,31 @@
|
||||
<div>
|
||||
<v-row align="center">
|
||||
<v-col
|
||||
:cols="3"
|
||||
class=""
|
||||
cols="10"
|
||||
md="5"
|
||||
lg="10"
|
||||
class="pt-0 pb-0 pl-6"
|
||||
>
|
||||
<div style="padding-left:30px">
|
||||
<CVselect
|
||||
v-if="isCameraNameEdit === false"
|
||||
v-model="currentCameraIndex"
|
||||
name="Camera"
|
||||
:list="$store.getters.cameraList"
|
||||
@input="handleInput('currentCamera',currentCameraIndex)"
|
||||
/>
|
||||
<CVinput
|
||||
v-else
|
||||
v-model="newCameraName"
|
||||
name="Camera"
|
||||
:error-message="checkCameraName"
|
||||
@Enter="saveCameraNameChange"
|
||||
/>
|
||||
</div>
|
||||
<CVselect
|
||||
v-if="isCameraNameEdit === false"
|
||||
v-model="currentCameraIndex"
|
||||
name="Camera"
|
||||
:list="$store.getters.cameraList"
|
||||
@input="handleInput('currentCamera',currentCameraIndex)"
|
||||
/>
|
||||
<CVinput
|
||||
v-else
|
||||
v-model="newCameraName"
|
||||
name="Camera"
|
||||
:error-message="checkCameraName"
|
||||
@Enter="saveCameraNameChange"
|
||||
/>
|
||||
</v-col>
|
||||
<v-col :cols="1">
|
||||
<v-col
|
||||
cols="2"
|
||||
md="1"
|
||||
lg="2"
|
||||
>
|
||||
<CVicon
|
||||
v-if="isCameraNameEdit === false"
|
||||
color="#c5c5c5"
|
||||
@@ -51,8 +55,10 @@
|
||||
</div>
|
||||
</v-col>
|
||||
<v-col
|
||||
:cols="3"
|
||||
class=""
|
||||
cols="10"
|
||||
md="5"
|
||||
lg="10"
|
||||
class="pt-0 pb-0 pl-6"
|
||||
>
|
||||
<CVselect
|
||||
v-model="currentPipelineIndex"
|
||||
@@ -62,14 +68,12 @@
|
||||
/>
|
||||
</v-col>
|
||||
<v-col
|
||||
v-if="currentPipelineIndex !== 0"
|
||||
:cols="1"
|
||||
class=""
|
||||
md="3"
|
||||
cols="2"
|
||||
md="1"
|
||||
lg="2"
|
||||
>
|
||||
<v-menu
|
||||
offset-y
|
||||
dark
|
||||
auto
|
||||
>
|
||||
<template v-slot:activator="{ on }">
|
||||
@@ -125,14 +129,14 @@
|
||||
</v-menu>
|
||||
</v-col>
|
||||
|
||||
<v-btn
|
||||
outlined
|
||||
color="#ffd843"
|
||||
@click="handleInput('command','save')"
|
||||
>
|
||||
<v-icon>save</v-icon>
|
||||
Save
|
||||
</v-btn>
|
||||
<!-- <v-btn-->
|
||||
<!-- outlined-->
|
||||
<!-- color="accent"-->
|
||||
<!-- @click="handleInput('command','save')"-->
|
||||
<!-- >-->
|
||||
<!-- <v-icon>save</v-icon>-->
|
||||
<!-- Save-->
|
||||
<!-- </v-btn>-->
|
||||
</v-row>
|
||||
<!--pipeline duplicate dialog-->
|
||||
<v-dialog
|
||||
@@ -265,15 +269,15 @@
|
||||
for (let cam in this.cameraList) {
|
||||
if (this.cameraList.hasOwnProperty(cam)) {
|
||||
if (this.newCameraName === this.cameraList[cam]) {
|
||||
return "Camera by that name already exists"
|
||||
return "A camera by that name already Exists"
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return "Camera name can only contain letters, numbers and spaces"
|
||||
return "A camera name can only contain letters, numbers and spaces"
|
||||
}
|
||||
}
|
||||
return ""
|
||||
return "";
|
||||
},
|
||||
checkPipelineName() {
|
||||
if (this.newPipelineName !== this.$store.getters.pipelineList[this.currentPipelineIndex - 1] || this.isPipelineNameEdit === false) {
|
||||
@@ -286,7 +290,7 @@
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return "Pipeline name can only contain letters, numbers, and spaces"
|
||||
return "A pipeline name can only contain letters, numbers, and spaces"
|
||||
}
|
||||
}
|
||||
return ""
|
||||
|
||||
@@ -25,6 +25,16 @@ export const dataHandleMixin = {
|
||||
this.$socket.send(msg);
|
||||
this.$emit('update')
|
||||
},
|
||||
handlePipelineUpdate(key, val) {
|
||||
let msg = this.$msgPack.encode({
|
||||
["changePipelineSetting"]: {
|
||||
[key]: val,
|
||||
["cameraIndex"]: this.$store.getters.currentCameraIndex
|
||||
}
|
||||
});
|
||||
this.$socket.send(msg);
|
||||
this.$emit('update')
|
||||
},
|
||||
handleTruthyPipelineData(val) {
|
||||
let msg = this.$msgPack.encode({
|
||||
["changePipelineSetting"]: {
|
||||
|
||||
@@ -2,10 +2,22 @@ import '@mdi/font/css/materialdesignicons.css';
|
||||
import 'material-design-icons-iconfont/dist/material-design-icons.css'
|
||||
import Vue from 'vue';
|
||||
import Vuetify from 'vuetify/lib';
|
||||
import theme from "../theme";
|
||||
|
||||
Vue.use(Vuetify);
|
||||
Vue.use(Vuetify, {});
|
||||
|
||||
// Although you *can* set up theming here, it's so frequently inappropriate that we do it in the markup
|
||||
export default new Vuetify({
|
||||
icons: {}
|
||||
|
||||
theme: {
|
||||
themes: {
|
||||
light: theme,
|
||||
dark: theme,
|
||||
}
|
||||
},
|
||||
breakpoint: {
|
||||
thresholds: {
|
||||
md: 1460,
|
||||
lg: 2000,
|
||||
},
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import Vue from 'vue'
|
||||
import Router from 'vue-router'
|
||||
import Camera from "./views/PipelineView";
|
||||
import Dashboard from "./views/PipelineView";
|
||||
import Settings from "./views/SettingsView";
|
||||
import Docs from "./views/DocsView";
|
||||
Vue.use(Router);
|
||||
@@ -10,11 +10,11 @@ export default new Router({
|
||||
base: process.env.BASE_URL,
|
||||
routes: [{
|
||||
path: '/',
|
||||
redirect: '/vision'
|
||||
redirect: '/dashboard'
|
||||
}, {
|
||||
path: '/vision',
|
||||
name: 'Vision',
|
||||
component: Camera
|
||||
path: '/dashboard',
|
||||
name: 'Dashboard',
|
||||
component: Dashboard
|
||||
}, {
|
||||
path: '/settings',
|
||||
name: 'Settings',
|
||||
|
||||
@@ -21,14 +21,17 @@ export default new Vuex.Store({
|
||||
undoRedo: undoRedo
|
||||
},
|
||||
state: {
|
||||
currentCameraIndex: 0,
|
||||
saveBar: false,
|
||||
compactMode: undefined, // Compact mode is initially unset on purpose
|
||||
currentCameraIndex: 0,
|
||||
selectedOutputs: [0, 1], // 0 indicates normal, 1 indicates threshold
|
||||
cameraSettings: [ // This is a list of objects representing the settings of all cameras
|
||||
{
|
||||
tiltDegrees: 0.0,
|
||||
currentPipelineIndex: 0,
|
||||
pipelineNicknames: ["Unknown"],
|
||||
streamPort: 1181,
|
||||
outputStreamPort: 1181,
|
||||
inputStreamPort: 1182,
|
||||
nickname: "Unknown",
|
||||
videoFormatList: [
|
||||
{
|
||||
@@ -66,7 +69,6 @@ export default new Vuex.Store({
|
||||
contourIntersection: 0,
|
||||
contourSortMode: 0,
|
||||
outputShowMultipleTargets: false,
|
||||
outputShowThresholded: 0,
|
||||
offsetRobotOffsetMode: 0,
|
||||
solvePNPEnabled: false,
|
||||
targetRegion: 0,
|
||||
@@ -93,11 +95,13 @@ export default new Vuex.Store({
|
||||
]
|
||||
},
|
||||
mutations: {
|
||||
cameraSettings: set('cameraSettings'),
|
||||
saveBar: set('saveBar'),
|
||||
compactMode: set('compactMode'),
|
||||
cameraSettings: set('cameraSettings'),
|
||||
currentCameraIndex: set('currentCameraIndex'),
|
||||
pipelineResults: set('pipelineResults'),
|
||||
networkSettings: set('networkSettings'),
|
||||
selectedOutputs: set('selectedOutputs'),
|
||||
|
||||
currentPipelineIndex: (state, val) => {
|
||||
const settings = state.cameraSettings[state.currentCameraIndex];
|
||||
@@ -110,7 +114,9 @@ export default new Vuex.Store({
|
||||
if (!payload.hasOwnProperty(key)) continue;
|
||||
const value = payload[key];
|
||||
const settings = state.cameraSettings[state.currentCameraIndex].currentPipelineSettings;
|
||||
if (key === "selectedOutputs") console.log(settings);
|
||||
if (settings.hasOwnProperty(key)) {
|
||||
if (key === "selectedOutputs") console.log('here');
|
||||
Vue.set(settings, key, value);
|
||||
}
|
||||
}
|
||||
@@ -129,9 +135,11 @@ export default new Vuex.Store({
|
||||
}
|
||||
},
|
||||
getters: {
|
||||
isDriverMode: state => state.cameraSettings[state.currentCameraIndex].currentPipelineIndex === -1,
|
||||
pipelineSettings: state => state.pipelineSettings,
|
||||
streamAddress: state =>
|
||||
"http://" + location.hostname + ":" + state.cameraSettings[state.currentCameraIndex].streamPort + "/stream.mjpg",
|
||||
["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],
|
||||
|
||||
8
photon-client/src/theme.js
Normal file
8
photon-client/src/theme.js
Normal file
@@ -0,0 +1,8 @@
|
||||
const theme = Object.freeze({
|
||||
primary: "#006492",
|
||||
secondary: "#39A4D5",
|
||||
accent: "#FFD843",
|
||||
background: "#232C37",
|
||||
});
|
||||
|
||||
export default theme;
|
||||
@@ -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>
|
||||
@@ -93,6 +93,7 @@ public class PhotonConfiguration {
|
||||
public int currentPipelineIndex;
|
||||
public List<String> pipelineNicknames;
|
||||
public HashMap<Integer, HashMap<String, Object>> videoFormatList;
|
||||
public int streamPort;
|
||||
public int outputStreamPort;
|
||||
public int inputStreamPort;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,7 +50,7 @@ public class MJPGFrameConsumer implements FrameConsumer {
|
||||
|
||||
@Override
|
||||
public void accept(Frame frame) {
|
||||
if (!frame.image.getMat().empty()) {
|
||||
if (frame != null && !frame.image.getMat().empty()) {
|
||||
if (divisor != FrameDivisor.NONE) {
|
||||
var tempMat = new Mat();
|
||||
Imgproc.resize(
|
||||
|
||||
@@ -20,6 +20,7 @@ package org.photonvision.vision.pipeline;
|
||||
import java.util.List;
|
||||
import org.apache.commons.lang3.tuple.Pair;
|
||||
import org.opencv.core.Mat;
|
||||
import org.opencv.imgproc.Imgproc;
|
||||
import org.photonvision.common.util.math.MathUtils;
|
||||
import org.photonvision.vision.frame.Frame;
|
||||
import org.photonvision.vision.frame.FrameStaticProperties;
|
||||
@@ -92,10 +93,6 @@ public class ReflectivePipeline extends CVPipeline<CVPipelineResult, ReflectiveP
|
||||
new HSVPipe.HSVParams(settings.hsvHue, settings.hsvSaturation, settings.hsvValue);
|
||||
hsvPipe.setParams(hsvParams);
|
||||
|
||||
OutputMatPipe.OutputMatParams outputMatParams =
|
||||
new OutputMatPipe.OutputMatParams(settings.outputShowThresholded);
|
||||
outputMatPipe.setParams(outputMatParams);
|
||||
|
||||
FindContoursPipe.FindContoursParams findContoursParams =
|
||||
new FindContoursPipe.FindContoursParams();
|
||||
findContoursPipe.setParams(findContoursParams);
|
||||
@@ -161,6 +158,7 @@ public class ReflectivePipeline extends CVPipeline<CVPipelineResult, ReflectiveP
|
||||
solvePNPPipe.setParams(solvePNPParams);
|
||||
}
|
||||
|
||||
@SuppressWarnings("DuplicatedCode")
|
||||
@Override
|
||||
public CVPipelineResult process(Frame frame, ReflectivePipelineSettings settings) {
|
||||
setPipeParams(frame.frameStaticProperties, settings);
|
||||
@@ -180,12 +178,10 @@ public class ReflectivePipeline extends CVPipeline<CVPipelineResult, ReflectiveP
|
||||
sumPipeNanosElapsed += hsvPipeResult.nanosElapsed;
|
||||
|
||||
// mat leak fix attempt
|
||||
// the first is the raw input mat, the second is the hsvpipe result
|
||||
outputMats.first = rawInputMat;
|
||||
outputMats.second = hsvPipeResult.result;
|
||||
|
||||
CVPipeResult<Mat> outputMatResult = outputMatPipe.apply(outputMats);
|
||||
sumPipeNanosElapsed += outputMatResult.nanosElapsed;
|
||||
|
||||
CVPipeResult<List<Contour>> findContoursResult = findContoursPipe.apply(hsvPipeResult.result);
|
||||
sumPipeNanosElapsed += findContoursResult.nanosElapsed;
|
||||
|
||||
@@ -224,30 +220,54 @@ public class ReflectivePipeline extends CVPipeline<CVPipelineResult, ReflectiveP
|
||||
targetList = collect2dTargetsResult;
|
||||
}
|
||||
|
||||
CVPipeResult<Mat> result;
|
||||
CVPipeResult<Mat> drawOnInputResult, drawOnOutputResult;
|
||||
|
||||
CVPipeResult<Mat> draw2dCrosshairResult =
|
||||
draw2dCrosshairPipe.apply(Pair.of(outputMatResult.result, targetList.result));
|
||||
sumPipeNanosElapsed += draw2dCrosshairResult.nanosElapsed;
|
||||
// the first is the raw input mat, the second is the hsvpipe result
|
||||
// Draw on input
|
||||
|
||||
CVPipeResult<Mat> draw2dContoursResult =
|
||||
CVPipeResult<Mat> draw2dCrosshairResultOnInput =
|
||||
draw2dCrosshairPipe.apply(Pair.of(outputMats.first, targetList.result));
|
||||
sumPipeNanosElapsed += draw2dCrosshairResultOnInput.nanosElapsed;
|
||||
|
||||
CVPipeResult<Mat> draw2dContoursResultOnInput =
|
||||
draw2DTargetsPipe.apply(
|
||||
Pair.of(draw2dCrosshairResult.result, collect2dTargetsResult.result));
|
||||
sumPipeNanosElapsed += draw2dContoursResult.nanosElapsed;
|
||||
Pair.of(draw2dCrosshairResultOnInput.result, collect2dTargetsResult.result));
|
||||
sumPipeNanosElapsed += draw2dCrosshairResultOnInput.nanosElapsed;
|
||||
|
||||
if (settings.solvePNPEnabled) {
|
||||
result =
|
||||
drawOnInputResult =
|
||||
draw3dTargetsPipe.apply(
|
||||
Pair.of(draw2dCrosshairResult.result, collect2dTargetsResult.result));
|
||||
sumPipeNanosElapsed += result.nanosElapsed;
|
||||
Pair.of(draw2dContoursResultOnInput.result, collect2dTargetsResult.result));
|
||||
sumPipeNanosElapsed += drawOnInputResult.nanosElapsed;
|
||||
} else {
|
||||
result = draw2dContoursResult;
|
||||
drawOnInputResult = draw2dContoursResultOnInput;
|
||||
}
|
||||
|
||||
// Draw on output
|
||||
|
||||
Imgproc.cvtColor(outputMats.second, outputMats.second, Imgproc.COLOR_GRAY2BGR, 3);
|
||||
CVPipeResult<Mat> draw2dCrosshairResultOnOutput =
|
||||
draw2dCrosshairPipe.apply(Pair.of(outputMats.second, targetList.result));
|
||||
sumPipeNanosElapsed += draw2dCrosshairResultOnOutput.nanosElapsed;
|
||||
|
||||
CVPipeResult<Mat> draw2dContoursResultOnOutput =
|
||||
draw2DTargetsPipe.apply(
|
||||
Pair.of(draw2dCrosshairResultOnOutput.result, collect2dTargetsResult.result));
|
||||
sumPipeNanosElapsed += draw2dContoursResultOnOutput.nanosElapsed;
|
||||
|
||||
if (settings.solvePNPEnabled) {
|
||||
drawOnOutputResult =
|
||||
draw3dTargetsPipe.apply(
|
||||
Pair.of(draw2dContoursResultOnOutput.result, collect2dTargetsResult.result));
|
||||
sumPipeNanosElapsed += drawOnOutputResult.nanosElapsed;
|
||||
} else {
|
||||
drawOnOutputResult = draw2dContoursResultOnOutput;
|
||||
}
|
||||
|
||||
// TODO: Implement all the things
|
||||
return new CVPipelineResult(
|
||||
MathUtils.nanosToMillis(sumPipeNanosElapsed),
|
||||
collect2dTargetsResult.result,
|
||||
new Frame(new CVMat(result.result), frame.frameStaticProperties));
|
||||
new Frame(new CVMat(outputMats.second), frame.frameStaticProperties),
|
||||
new Frame(new CVMat(outputMats.first), frame.frameStaticProperties));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,12 +27,19 @@ public class CVPipelineResult implements Releasable {
|
||||
public final double processingMillis;
|
||||
public final List<TrackedTarget> targets;
|
||||
public final Frame outputFrame;
|
||||
public final Frame inputFrame;
|
||||
|
||||
public CVPipelineResult(double processingMillis, List<TrackedTarget> targets, Frame outputFrame) {
|
||||
public CVPipelineResult(
|
||||
double processingMillis, List<TrackedTarget> targets, Frame outputFrame, Frame inputFrame) {
|
||||
this.processingMillis = processingMillis;
|
||||
this.targets = targets;
|
||||
|
||||
this.outputFrame = Frame.copyFrom(outputFrame);
|
||||
this.inputFrame = inputFrame != null ? Frame.copyFrom(inputFrame) : null;
|
||||
}
|
||||
|
||||
public CVPipelineResult(double processingMillis, List<TrackedTarget> targets, Frame outputFrame) {
|
||||
this(processingMillis, targets, outputFrame, null);
|
||||
}
|
||||
|
||||
public boolean hasTargets() {
|
||||
@@ -44,6 +51,7 @@ public class CVPipelineResult implements Releasable {
|
||||
tt.release();
|
||||
}
|
||||
outputFrame.release();
|
||||
if (inputFrame != null) inputFrame.release();
|
||||
}
|
||||
|
||||
public double getLatencyMillis() {
|
||||
|
||||
@@ -60,12 +60,13 @@ public class VisionModule {
|
||||
private final LinkedList<FrameConsumer> frameConsumers = new LinkedList<>();
|
||||
private final NTDataPublisher ntConsumer;
|
||||
private final int moduleIndex;
|
||||
private final MJPGFrameConsumer uiStreamer;
|
||||
|
||||
private long lastUIResultUpdateTime = 0;
|
||||
private long lastRunTime = 0;
|
||||
private MedianFilter fpsAverager = new MedianFilter(10);
|
||||
|
||||
private MJPGFrameConsumer dashboardStreamer;
|
||||
private MJPGFrameConsumer dashboardOutputStreamer;
|
||||
private MJPGFrameConsumer dashboardInputStreamer;
|
||||
|
||||
public VisionModule(PipelineManager pipelineManager, VisionSource visionSource, int index) {
|
||||
logger =
|
||||
@@ -84,11 +85,20 @@ public class VisionModule {
|
||||
|
||||
DataChangeService.getInstance().addSubscriber(new VisionSettingChangeSubscriber());
|
||||
|
||||
dashboardStreamer =
|
||||
new MJPGFrameConsumer(visionSource.getSettables().getConfiguration().uniqueName);
|
||||
uiStreamer = new MJPGFrameConsumer(visionSource.getSettables().getConfiguration().nickname);
|
||||
addFrameConsumer(dashboardStreamer);
|
||||
addFrameConsumer(uiStreamer);
|
||||
dashboardOutputStreamer =
|
||||
new MJPGFrameConsumer(
|
||||
visionSource.getSettables().getConfiguration().uniqueName + "-output");
|
||||
dashboardInputStreamer =
|
||||
new MJPGFrameConsumer(visionSource.getSettables().getConfiguration().uniqueName + "-input");
|
||||
|
||||
addResultConsumer(
|
||||
result -> {
|
||||
dashboardInputStreamer.accept(result.inputFrame);
|
||||
});
|
||||
addResultConsumer(
|
||||
result -> {
|
||||
dashboardOutputStreamer.accept(result.outputFrame);
|
||||
});
|
||||
|
||||
ntConsumer =
|
||||
new NTDataPublisher(
|
||||
@@ -140,7 +150,7 @@ public class VisionModule {
|
||||
|
||||
setPipeline(visionSource.getSettables().getConfiguration().currentPipelineIndex);
|
||||
|
||||
dashboardStreamer.setFrameDivisor(
|
||||
dashboardOutputStreamer.setFrameDivisor(
|
||||
pipelineManager.getCurrentPipelineSettings().outputFrameDivisor);
|
||||
}
|
||||
|
||||
@@ -215,6 +225,11 @@ public class VisionModule {
|
||||
setPipeline(index);
|
||||
saveAndBroadcast();
|
||||
return;
|
||||
case "selectedOutputs":
|
||||
// 0 indicates normal, 1 indicates thresholded
|
||||
var outputs = (ArrayList<Integer>) newPropValue;
|
||||
// TODO
|
||||
return;
|
||||
}
|
||||
|
||||
// special case for camera settables
|
||||
@@ -265,7 +280,7 @@ public class VisionModule {
|
||||
|
||||
// special case for extra tasks to perform after setting PipelineSettings
|
||||
if (propName.equals("outputFrameDivisor")) {
|
||||
dashboardStreamer.setFrameDivisor(
|
||||
dashboardOutputStreamer.setFrameDivisor(
|
||||
pipelineManager.getCurrentPipelineSettings().outputFrameDivisor);
|
||||
}
|
||||
|
||||
@@ -322,10 +337,10 @@ public class VisionModule {
|
||||
visionSource.getSettables().getConfiguration().nickname = newName;
|
||||
ntConsumer.updateCameraNickname(newName);
|
||||
|
||||
frameConsumers.remove(dashboardStreamer);
|
||||
dashboardStreamer =
|
||||
frameConsumers.remove(dashboardOutputStreamer);
|
||||
dashboardOutputStreamer =
|
||||
new MJPGFrameConsumer(visionSource.getSettables().getConfiguration().nickname);
|
||||
frameConsumers.add(dashboardStreamer);
|
||||
frameConsumers.add(dashboardOutputStreamer);
|
||||
saveAndBroadcast();
|
||||
}
|
||||
|
||||
@@ -355,8 +370,9 @@ public class VisionModule {
|
||||
temp.put(k, internalMap);
|
||||
}
|
||||
ret.videoFormatList = temp;
|
||||
ret.streamPort = dashboardStreamer.getCurrentStreamPort();
|
||||
// ret.uiStreamPort = uiStreamer.getCurrentStreamPort();
|
||||
ret.outputStreamPort = dashboardOutputStreamer.getCurrentStreamPort();
|
||||
ret.inputStreamPort = dashboardInputStreamer.getCurrentStreamPort();
|
||||
// ret.uiStreamPort = uiStreamer.getCurrentStreamPort();
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
@@ -43,7 +43,7 @@ public class TrackedTarget implements Releasable {
|
||||
private double m_yaw;
|
||||
private double m_area;
|
||||
|
||||
private Pose2d m_robotRelativePose;
|
||||
private Pose2d m_robotRelativePose = new Pose2d();
|
||||
|
||||
private Mat m_cameraRelativeTvec, m_cameraRelativeRvec;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user