UI Redesign (#22)

* Rework UI into a new, responsive layout

* Send two streams (only one is currently downscaled)
This commit is contained in:
Declan Freeman-Gleason
2020-07-13 19:34:31 -07:00
committed by GitHub
parent aed92e7132
commit 8b46ad1cab
29 changed files with 945 additions and 559 deletions

View File

@@ -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>

View File

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

View File

@@ -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;
}
}

View File

@@ -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

View File

@@ -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>

View File

@@ -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 {}
},

View File

@@ -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) {

View File

@@ -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;

View File

@@ -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 ""

View File

@@ -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"]: {

View File

@@ -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,
},
}
});

View File

@@ -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',

View File

@@ -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],

View File

@@ -0,0 +1,8 @@
const theme = Object.freeze({
primary: "#006492",
secondary: "#39A4D5",
accent: "#FFD843",
background: "#232C37",
});
export default theme;

View File

@@ -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>

View File

@@ -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>

View File

@@ -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

View File

@@ -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);

View File

@@ -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"

View File

@@ -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>

View 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) }}&nbsp;m</td>
<td>{{ parseFloat(value.pose.y).toFixed(2) }}&nbsp;m</td>
<td>{{ parseFloat(value.pose.rot).toFixed(2) }}&deg;</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>

View File

@@ -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>

View File

@@ -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;
}
}

View File

@@ -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(

View File

@@ -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));
}
}

View File

@@ -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() {

View File

@@ -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;
}

View File

@@ -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;