mirror of
https://github.com/PhotonVision/photonvision
synced 2026-07-02 02:51:40 +00:00
Rename to PhotonVision
This commit is contained in:
205
photon-client/src/App.vue
Normal file
205
photon-client/src/App.vue
Normal file
@@ -0,0 +1,205 @@
|
||||
<template>
|
||||
<v-app>
|
||||
<v-app-bar
|
||||
app
|
||||
dense
|
||||
clipped-left
|
||||
dark
|
||||
>
|
||||
<img
|
||||
class="imgClass"
|
||||
src="./assets/logo.png"
|
||||
>
|
||||
<v-toolbar-title id="title">
|
||||
Chameleon Vision
|
||||
</v-toolbar-title>
|
||||
<div class="flex-grow-1" />
|
||||
<v-toolbar-items>
|
||||
<v-tabs
|
||||
background-color="#272727"
|
||||
dark
|
||||
height="48"
|
||||
slider-color="#4baf62"
|
||||
>
|
||||
<v-tab to="vision">
|
||||
Vision
|
||||
</v-tab>
|
||||
<v-tab to="settings">
|
||||
Settings
|
||||
</v-tab>
|
||||
</v-tabs>
|
||||
</v-toolbar-items>
|
||||
</v-app-bar>
|
||||
<v-content>
|
||||
<v-container
|
||||
fluid
|
||||
fill-height
|
||||
>
|
||||
<v-layout>
|
||||
<v-flex>
|
||||
<router-view @save="startTimer" />
|
||||
<v-snackbar
|
||||
v-model="saveSnackbar"
|
||||
:timeout="1000"
|
||||
top
|
||||
color="#4baf62"
|
||||
>
|
||||
<div style="text-align: center;width: 100%;">
|
||||
<h4>Saved All changes</h4>
|
||||
</div>
|
||||
</v-snackbar>
|
||||
<div v-if="isLogger">
|
||||
<keep-alive>
|
||||
<log-view
|
||||
class="loggerClass"
|
||||
:log="log"
|
||||
/>
|
||||
</keep-alive>
|
||||
</div>
|
||||
</v-flex>
|
||||
</v-layout>
|
||||
</v-container>
|
||||
</v-content>
|
||||
</v-app>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import logView from '@femessage/log-viewer'
|
||||
|
||||
export default {
|
||||
name: 'App',
|
||||
components: {
|
||||
logView
|
||||
},
|
||||
data: () => ({
|
||||
timer: undefined,
|
||||
isLogger: false,
|
||||
log: ""
|
||||
}),
|
||||
computed: {
|
||||
saveSnackbar: {
|
||||
get() {
|
||||
return this.$store.state.saveBar;
|
||||
},
|
||||
set(value) {
|
||||
this.$store.commit("saveBar", value);
|
||||
}
|
||||
}
|
||||
},
|
||||
created() {
|
||||
document.addEventListener("keydown", e => {
|
||||
switch (e.key) {
|
||||
case '`' :
|
||||
this.isLogger = !this.isLogger;
|
||||
break;
|
||||
case "z":
|
||||
if (e.ctrlKey && this.$store.getters.canUndo) {
|
||||
this.$store.dispatch('undo', {vm: this});
|
||||
}
|
||||
break;
|
||||
case "y":
|
||||
if (e.ctrlKey && this.$store.getters.canRedo) {
|
||||
this.$store.dispatch('redo', {vm: this});
|
||||
}
|
||||
break;
|
||||
|
||||
}
|
||||
});
|
||||
this.$options.sockets.onmessage = (data) => {
|
||||
try {
|
||||
let message = this.$msgPack.decode(data.data);
|
||||
for (let prop in message) {
|
||||
if (message.hasOwnProperty(prop)) {
|
||||
this.handleMessage(prop, message[prop]);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('error: ' + data.data + " , " + error);
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
handleMessage(key, value) {
|
||||
if (this.$store.state.hasOwnProperty(key)) {
|
||||
this.$store.commit(key, value);
|
||||
} else if (this.$store.state.pipeline.hasOwnProperty(key)) {
|
||||
this.$store.commit('mutatePipeline', {'key': key, 'value': value});
|
||||
} else {
|
||||
switch (key) {
|
||||
default: {
|
||||
console.log(key + " : " + value);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
saveSettings() {
|
||||
clearInterval(this.timer);
|
||||
this.saveSnackbar = true;
|
||||
this.handleInput("command", "save");
|
||||
},
|
||||
startTimer() {
|
||||
if (this.timer !== undefined) {
|
||||
clearInterval(this.timer);
|
||||
}
|
||||
this.timer = setInterval(this.saveSettings, 4000);
|
||||
},
|
||||
logMessage({message, level}) {
|
||||
const colors = ["\u001b[31m", "\u001b[32m", "\u001b[33m", "\u001b[34m"]
|
||||
const reset = "\u001b[0m"
|
||||
this.log += `${colors[level]}${message}${reset}\n`
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style>
|
||||
html {
|
||||
overflow-y: hidden !important;
|
||||
}
|
||||
|
||||
.imgClass {
|
||||
width: auto;
|
||||
height: 45px;
|
||||
vertical-align: middle;
|
||||
padding-right: 5px;
|
||||
}
|
||||
|
||||
.loggerClass {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
height: 25% !important;
|
||||
left: 0;
|
||||
right: 0;
|
||||
box-shadow: #282828 0 0 5px 1px;
|
||||
background-color: #2b2b2b;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar {
|
||||
width: 0.5em;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
-webkit-box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.3);
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background-color: #4baf62;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.container {
|
||||
background-color: #212121;
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
#title {
|
||||
color: #4baf62;
|
||||
}
|
||||
|
||||
span {
|
||||
color: white;
|
||||
}
|
||||
</style>
|
||||
20
photon-client/src/assets/FRCtargets.json
Normal file
20
photon-client/src/assets/FRCtargets.json
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"2020 Hex Goal": [
|
||||
[-19.625, 0],
|
||||
[-9.819867, -17],
|
||||
[9.819867, -17],
|
||||
[19.625,0]
|
||||
],
|
||||
"2020 Loading Station": [
|
||||
[-3.54,5.5],
|
||||
[-3.54,-5.5],
|
||||
[3.54,-5.5],
|
||||
[3.54,5.5]
|
||||
],
|
||||
"2019 Dual Target": [
|
||||
[-7.75, 3],
|
||||
[-7.75, -3],
|
||||
[7.75, -3],
|
||||
[7.75, 3]
|
||||
]
|
||||
}
|
||||
BIN
photon-client/src/assets/chessboard.png
Normal file
BIN
photon-client/src/assets/chessboard.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 61 KiB |
BIN
photon-client/src/assets/logo.png
Normal file
BIN
photon-client/src/assets/logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 70 KiB |
53
photon-client/src/components/common/cv-icon.vue
Normal file
53
photon-client/src/components/common/cv-icon.vue
Normal file
@@ -0,0 +1,53 @@
|
||||
<template>
|
||||
<div>
|
||||
<v-tooltip
|
||||
:right="right"
|
||||
:bottom="!right"
|
||||
nudge-right="10"
|
||||
>
|
||||
<template v-slot:activator="{ on }">
|
||||
<v-icon
|
||||
:class="hoverClass"
|
||||
:color="color"
|
||||
@click="handleClick"
|
||||
v-on="on"
|
||||
>
|
||||
{{ text }}
|
||||
</v-icon>
|
||||
</template>
|
||||
<span>{{ tooltip }}</span>
|
||||
</v-tooltip>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'Icon',
|
||||
props: ['color', 'tooltip', 'text', 'right', 'hover'],
|
||||
data() {
|
||||
return {}
|
||||
},
|
||||
computed: {
|
||||
hoverClass: {
|
||||
get() {
|
||||
if (this.hover !== undefined) {
|
||||
return "hover";
|
||||
}
|
||||
return "";
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
handleClick() {
|
||||
this.$emit('click');
|
||||
}
|
||||
},
|
||||
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.hover:hover {
|
||||
color: white !important;
|
||||
}
|
||||
</style>
|
||||
33
photon-client/src/components/common/cv-image.vue
Normal file
33
photon-client/src/components/common/cv-image.vue
Normal file
@@ -0,0 +1,33 @@
|
||||
<template>
|
||||
<img
|
||||
id="CameraStream"
|
||||
:style="styleObject"
|
||||
:src="address"
|
||||
crossorigin="Anonymous"
|
||||
alt=""
|
||||
@click="e => $emit('click', e)"
|
||||
>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "CvImage",
|
||||
props: ['address', 'scale'],
|
||||
data: () => {
|
||||
return {}
|
||||
},
|
||||
computed: {
|
||||
styleObject: {
|
||||
get() {
|
||||
return {
|
||||
width: `${this.scale}%`,
|
||||
height: `${this.scale}%`,
|
||||
display: 'block',
|
||||
margin: 'auto'
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
53
photon-client/src/components/common/cv-input.vue
Normal file
53
photon-client/src/components/common/cv-input.vue
Normal file
@@ -0,0 +1,53 @@
|
||||
<template>
|
||||
<div>
|
||||
<v-row
|
||||
dense
|
||||
align="center"
|
||||
>
|
||||
<v-col :cols="3">
|
||||
<span>{{ name }}</span>
|
||||
</v-col>
|
||||
<v-col :cols="9">
|
||||
<v-text-field
|
||||
v-model="localValue"
|
||||
dark
|
||||
dense
|
||||
:disabled="disabled"
|
||||
:error-messages="errorMessage"
|
||||
@keydown="handleKeyboard"
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</div>
|
||||
</template>
|
||||
s
|
||||
<script>
|
||||
export default {
|
||||
name: 'Input',
|
||||
props: ['name', 'value', 'disabled', 'errorMessage'],
|
||||
data() {
|
||||
return {}
|
||||
},
|
||||
computed: {
|
||||
localValue: {
|
||||
get() {
|
||||
return this.value;
|
||||
},
|
||||
set(value) {
|
||||
this.$emit('input', value);
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
handleKeyboard(event) {
|
||||
if (event.key === "Enter") {
|
||||
this.$emit("Enter");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="" scoped>
|
||||
|
||||
</style>
|
||||
48
photon-client/src/components/common/cv-number-input.vue
Normal file
48
photon-client/src/components/common/cv-number-input.vue
Normal file
@@ -0,0 +1,48 @@
|
||||
<template>
|
||||
<div>
|
||||
<v-row
|
||||
dense
|
||||
align="center"
|
||||
>
|
||||
<v-col :cols="2">
|
||||
<span>{{ name }}</span>
|
||||
</v-col>
|
||||
<v-col>
|
||||
<v-text-field
|
||||
v-model="localValue"
|
||||
dark
|
||||
class="mt-0 pt-0"
|
||||
hide-details
|
||||
single-line
|
||||
type="number"
|
||||
style="width: 70px"
|
||||
:step="step"
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'NumberInput',
|
||||
props: ['name', 'value', 'step'],
|
||||
data() {
|
||||
return {}
|
||||
},
|
||||
computed: {
|
||||
localValue: {
|
||||
get() {
|
||||
return this.value;
|
||||
},
|
||||
set(value) {
|
||||
this.$emit('input', parseFloat(value));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="" scoped>
|
||||
|
||||
</style>
|
||||
42
photon-client/src/components/common/cv-radio.vue
Normal file
42
photon-client/src/components/common/cv-radio.vue
Normal file
@@ -0,0 +1,42 @@
|
||||
<template>
|
||||
<div>
|
||||
<v-radio-group
|
||||
v-model="localValue"
|
||||
row
|
||||
dark
|
||||
:mandatory="true"
|
||||
>
|
||||
<v-radio
|
||||
v-for="(name,index) in list"
|
||||
:key="index"
|
||||
color="#4baf62"
|
||||
:label="name"
|
||||
:value="index"
|
||||
/>
|
||||
</v-radio-group>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'Radio',
|
||||
props: ['value', 'list'],
|
||||
data() {
|
||||
return {}
|
||||
},
|
||||
computed: {
|
||||
localValue: {
|
||||
get() {
|
||||
return this.value;
|
||||
},
|
||||
set(value) {
|
||||
this.$emit('input', value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="" scoped>
|
||||
|
||||
</style>
|
||||
107
photon-client/src/components/common/cv-range-slider.vue
Normal file
107
photon-client/src/components/common/cv-range-slider.vue
Normal file
@@ -0,0 +1,107 @@
|
||||
<template>
|
||||
<div>
|
||||
<v-row
|
||||
dense
|
||||
align="center"
|
||||
>
|
||||
<v-col :cols="2">
|
||||
<span>{{ name }}</span>
|
||||
</v-col>
|
||||
<v-col :cols="10">
|
||||
<v-range-slider
|
||||
:value="localValue"
|
||||
:max="max"
|
||||
:min="min"
|
||||
hide-details
|
||||
class="align-center"
|
||||
dark
|
||||
color="#4baf62"
|
||||
:step="step"
|
||||
@input="handleInput"
|
||||
@mousedown="$emit('rollback', localValue)"
|
||||
>
|
||||
<template v-slot:prepend>
|
||||
<v-text-field
|
||||
dark
|
||||
:value="localValue[0]"
|
||||
:max="max"
|
||||
:min="min"
|
||||
class="mt-0 pt-0"
|
||||
hide-details
|
||||
single-line
|
||||
type="number"
|
||||
style="width: 50px"
|
||||
:step="step"
|
||||
@input="handleChange"
|
||||
@focus="prependFocused = true"
|
||||
@blur="prependFocused = false"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<template v-slot:append>
|
||||
<v-text-field
|
||||
dark
|
||||
:value="localValue[1]"
|
||||
:max="max"
|
||||
:min="min"
|
||||
class="mt-0 pt-0"
|
||||
hide-details
|
||||
single-line
|
||||
type="number"
|
||||
style="width: 50px"
|
||||
:step="step"
|
||||
@input="handleChange"
|
||||
@focus="appendFocused = true"
|
||||
@blur="appendFocused = false"
|
||||
/>
|
||||
</template>
|
||||
</v-range-slider>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'RangeSlider',
|
||||
props: ['name', 'min', 'max', 'value', 'step'],
|
||||
data() {
|
||||
return {
|
||||
prependFocused: false,
|
||||
appendFocused: false
|
||||
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
localValue: {
|
||||
get() {
|
||||
return this.value;
|
||||
},
|
||||
set(value) {
|
||||
this.$emit('input', value)
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
handleChange(val) {
|
||||
let i = 0;
|
||||
if (this.prependFocused === false && this.appendFocused === true) {
|
||||
i = 1;
|
||||
}
|
||||
if (this.prependFocused || this.appendFocused) {
|
||||
this.$set(this.localValue, i, val);
|
||||
this.$emit('rollback', this.localValue)
|
||||
}
|
||||
},
|
||||
handleInput(val) {
|
||||
if (!this.prependFocused || !this.appendFocused) {
|
||||
this.localValue = val;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="" scoped>
|
||||
|
||||
</style>
|
||||
58
photon-client/src/components/common/cv-select.vue
Normal file
58
photon-client/src/components/common/cv-select.vue
Normal file
@@ -0,0 +1,58 @@
|
||||
<template>
|
||||
<div>
|
||||
<v-row
|
||||
dense
|
||||
align="center"
|
||||
>
|
||||
<v-col :cols="3">
|
||||
<span>{{ name }}</span>
|
||||
</v-col>
|
||||
<v-col :cols="9">
|
||||
<v-select
|
||||
v-model="localValue"
|
||||
:items="indexList"
|
||||
item-text="name"
|
||||
item-value="index"
|
||||
dark
|
||||
color="#4baf62"
|
||||
item-color="green"
|
||||
:disabled="disabled"
|
||||
@change="$emit('rollback', localValue)"
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'Select',
|
||||
props: ['list', 'name', 'value', 'disabled'],
|
||||
data() {
|
||||
return {}
|
||||
},
|
||||
computed: {
|
||||
localValue: {
|
||||
get() {
|
||||
return this.value;
|
||||
},
|
||||
set(value) {
|
||||
this.$emit('input', value)
|
||||
}
|
||||
},
|
||||
indexList() {
|
||||
let list = [];
|
||||
for (let i = 0; i < this.list.length; i++) {
|
||||
list.push({
|
||||
name: this.list[i],
|
||||
index: i
|
||||
});
|
||||
}
|
||||
return list;
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
</style>
|
||||
92
photon-client/src/components/common/cv-slider.vue
Normal file
92
photon-client/src/components/common/cv-slider.vue
Normal file
@@ -0,0 +1,92 @@
|
||||
<template>
|
||||
<div>
|
||||
<v-row
|
||||
dense
|
||||
align="center"
|
||||
>
|
||||
<v-col :cols="2">
|
||||
<span>{{ name }}</span>
|
||||
</v-col>
|
||||
<v-col :cols="10">
|
||||
<v-slider
|
||||
:value="localValue"
|
||||
dark
|
||||
class="align-center"
|
||||
:max="max"
|
||||
:min="min"
|
||||
hide-details
|
||||
color="#4baf62"
|
||||
:step="step"
|
||||
@start="isClicked = true"
|
||||
@end="isClicked = false"
|
||||
@change="handleclick"
|
||||
@input="handleInput"
|
||||
@mousedown="$emit('rollback', localValue)"
|
||||
>
|
||||
<template v-slot:append>
|
||||
<v-text-field
|
||||
dark
|
||||
:max="max"
|
||||
:min="min"
|
||||
:value="localValue"
|
||||
class="mt-0 pt-0"
|
||||
hide-details
|
||||
single-line
|
||||
type="number"
|
||||
style="width: 50px"
|
||||
:step="step"
|
||||
@input="handleChange"
|
||||
@focus="isFocused = true"
|
||||
@blur="isFocused = false"
|
||||
/>
|
||||
</template>
|
||||
</v-slider>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'Slider',
|
||||
props: ['min', 'max', 'name', 'value', 'step'],
|
||||
data() {
|
||||
return {
|
||||
isFocused: false,
|
||||
isClicked: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
localValue: {
|
||||
get() {
|
||||
return this.value;
|
||||
},
|
||||
set(value) {
|
||||
this.$emit('input', value)
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
handleChange(val) {
|
||||
if (this.isFocused) {
|
||||
this.localValue = parseFloat(val);
|
||||
this.$emit('rollback', this.localValue)
|
||||
}
|
||||
},
|
||||
handleInput(val) {
|
||||
if (!this.isFocused && this.isClicked) {
|
||||
this.localValue = val;
|
||||
}
|
||||
},
|
||||
handleclick(val) {
|
||||
if (!this.isFocused) {
|
||||
this.localValue = val;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="" scoped>
|
||||
|
||||
</style>
|
||||
45
photon-client/src/components/common/cv-switch.vue
Normal file
45
photon-client/src/components/common/cv-switch.vue
Normal file
@@ -0,0 +1,45 @@
|
||||
<template>
|
||||
<div>
|
||||
<v-row
|
||||
dense
|
||||
align="center"
|
||||
>
|
||||
<v-col :cols="2">
|
||||
<span>{{ name }}</span>
|
||||
</v-col>
|
||||
<v-col>
|
||||
<v-switch
|
||||
v-model="localValue"
|
||||
dark
|
||||
:disabled="disabled"
|
||||
color="#4baf62"
|
||||
@change="$emit('rollback', localValue)"
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'CVSwitch',
|
||||
props: ['name', 'value', 'disabled'],
|
||||
data() {
|
||||
return {}
|
||||
},
|
||||
computed: {
|
||||
localValue: {
|
||||
get() {
|
||||
return this.value;
|
||||
},
|
||||
set(value) {
|
||||
this.$emit('input', value)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="" scoped>
|
||||
|
||||
</style>
|
||||
187
photon-client/src/components/pipeline/3D/MiniMap.vue
Normal file
187
photon-client/src/components/pipeline/3D/MiniMap.vue
Normal file
@@ -0,0 +1,187 @@
|
||||
<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
|
||||
>
|
||||
<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>
|
||||
</v-row>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "MiniMap",
|
||||
props: {
|
||||
targets: Array,
|
||||
horizontalFOV: Number
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
ctx: undefined,
|
||||
canvas: undefined,
|
||||
x: 0,
|
||||
y: 0,
|
||||
targetWidth: 40,
|
||||
targetHeight: 6
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
hLen: {
|
||||
get() {
|
||||
return Math.tan(this.horizontalFOV / 2 * Math.PI / 180) * 150;
|
||||
}
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
targets: {
|
||||
deep: true,
|
||||
handler() {
|
||||
this.draw();
|
||||
}
|
||||
},
|
||||
horizontalFOV() {
|
||||
this.draw();
|
||||
}
|
||||
},
|
||||
mounted: function () {
|
||||
const canvas = document.getElementById("canvasId"); // getting the canvas element
|
||||
const ctx = canvas.getContext("2d"); // getting the canvas context
|
||||
this.canvas = canvas; // setting the canvas as a vue variable
|
||||
this.ctx = ctx; // setting the canvas context as a vue variable
|
||||
this.grad = this.ctx.createLinearGradient(400, 800, 400, 600);
|
||||
this.grad.addColorStop(0, "rgb(119,119,119)");
|
||||
this.grad.addColorStop(0.05, "rgba(14,92,22,0.96)");
|
||||
this.grad.addColorStop(0.8, 'rgba(43,43,43,0.48)');
|
||||
|
||||
// setting canvas context values for drawing
|
||||
|
||||
|
||||
this.ctx.font = "26px Arial";
|
||||
this.ctx.strokeStyle = "whitesmoke";
|
||||
this.ctx.lineWidth = 2;
|
||||
|
||||
this.$nextTick(function () {
|
||||
this.drawPlayer();
|
||||
|
||||
});
|
||||
},
|
||||
methods: {
|
||||
draw() {
|
||||
this.clearBoard();
|
||||
this.drawPlayer();
|
||||
for (let index in this.targets) {
|
||||
this.drawTarget(index, this.targets[index].pose);
|
||||
}
|
||||
},
|
||||
drawTarget(index, target) {
|
||||
// first save the untranslated/unrotated context
|
||||
let x = 800 - (160 * target.translation.x); // getting meters as pixels
|
||||
let y = 400 - (160 * target.translation.y);
|
||||
this.ctx.save();
|
||||
this.ctx.beginPath();
|
||||
// move the rotation point to the center of the rect
|
||||
this.ctx.translate(y + this.targetWidth / 2, x + this.targetHeight / 2); // wpi lib makes x forward and back and y left to right
|
||||
// rotate the rect
|
||||
this.ctx.rotate(target.rotation.radians * -1);
|
||||
|
||||
// draw the rect on the transformed context
|
||||
// Note: after transforming [0,0] is visually [x,y]
|
||||
// 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.fill();
|
||||
|
||||
// restore the context to its untranslated/unrotated state
|
||||
this.ctx.restore();
|
||||
this.ctx.fillStyle = "whitesmoke";
|
||||
this.ctx.beginPath();
|
||||
this.ctx.arc(y + this.targetWidth / 2, x + this.targetHeight / 2, 3, 0, 2 * Math.PI, true);
|
||||
this.ctx.fill();
|
||||
this.ctx.fillText(index, y - 30, x - 5);
|
||||
|
||||
},
|
||||
drawPlayer() {
|
||||
this.ctx.beginPath();
|
||||
this.ctx.moveTo(400, 820);
|
||||
this.ctx.lineTo(400 + this.hLen, 650);
|
||||
this.ctx.lineTo(400 - this.hLen, 650);
|
||||
this.ctx.closePath();
|
||||
this.ctx.fillStyle = this.grad;
|
||||
this.ctx.fill();
|
||||
this.ctx.beginPath();
|
||||
this.ctx.moveTo(400, 820);
|
||||
this.ctx.lineTo(400 + this.hLen, 650);
|
||||
this.ctx.stroke();
|
||||
this.ctx.moveTo(400, 820);
|
||||
this.ctx.lineTo(400 - this.hLen, 650);
|
||||
this.ctx.stroke();
|
||||
|
||||
},
|
||||
clearBoard() {
|
||||
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height); // clearing the canvas
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
#canvasId {
|
||||
width: 400px;
|
||||
height: 400px;
|
||||
background-color: #2b2b2b;
|
||||
border-radius: 5px;
|
||||
border: 2px solid grey;
|
||||
box-shadow: 0 0 5px 1px;
|
||||
}
|
||||
|
||||
th {
|
||||
width: 80px;
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,388 @@
|
||||
<template>
|
||||
<div>
|
||||
<v-row align="center">
|
||||
<v-col
|
||||
:cols="3"
|
||||
class=""
|
||||
>
|
||||
<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>
|
||||
</v-col>
|
||||
<v-col :cols="1">
|
||||
<CVicon
|
||||
v-if="isCameraNameEdit === false"
|
||||
color="#c5c5c5"
|
||||
:hover="true"
|
||||
text="edit"
|
||||
tooltip="Edit camera name"
|
||||
@click="toCameraNameChange"
|
||||
/>
|
||||
<div v-else>
|
||||
<CVicon
|
||||
color="#c5c5c5"
|
||||
style="display: inline-block;"
|
||||
:hover="true"
|
||||
text="save"
|
||||
tooltip="Save Camera Name"
|
||||
@click="saveCameraNameChange"
|
||||
/>
|
||||
<CVicon
|
||||
color="error"
|
||||
style="display: inline-block;"
|
||||
:hover="true"
|
||||
text="close"
|
||||
tooltip="Discard Changes"
|
||||
@click="discardCameraNameChange"
|
||||
/>
|
||||
</div>
|
||||
</v-col>
|
||||
<v-col
|
||||
:cols="3"
|
||||
class=""
|
||||
>
|
||||
<CVselect
|
||||
v-model="currentPipelineIndex"
|
||||
name="Pipeline"
|
||||
:list="['Driver Mode'].concat($store.getters.pipelineList)"
|
||||
@input="handleInput('currentPipeline',currentPipelineIndex - 1)"
|
||||
/>
|
||||
</v-col>
|
||||
<v-col
|
||||
v-if="currentPipelineIndex !== 0"
|
||||
:cols="1"
|
||||
class=""
|
||||
md="3"
|
||||
>
|
||||
<v-menu
|
||||
offset-y
|
||||
dark
|
||||
auto
|
||||
>
|
||||
<template v-slot:activator="{ on }">
|
||||
<v-icon
|
||||
color="white"
|
||||
v-on="on"
|
||||
>
|
||||
menu
|
||||
</v-icon>
|
||||
</template>
|
||||
<v-list dense>
|
||||
<v-list-item @click="toPipelineNameChange">
|
||||
<v-list-item-title>
|
||||
<CVicon
|
||||
color="#c5c5c5"
|
||||
:right="true"
|
||||
text="edit"
|
||||
tooltip="Edit pipeline name"
|
||||
/>
|
||||
</v-list-item-title>
|
||||
</v-list-item>
|
||||
<v-list-item @click="toCreatePipeline">
|
||||
<v-list-item-title>
|
||||
<CVicon
|
||||
color="#c5c5c5"
|
||||
:right="true"
|
||||
text="add"
|
||||
tooltip="Add new pipeline"
|
||||
/>
|
||||
</v-list-item-title>
|
||||
</v-list-item>
|
||||
<v-list-item @click="deleteCurrentPipeline">
|
||||
<v-list-item-title>
|
||||
<CVicon
|
||||
color="red darken-2"
|
||||
:right="true"
|
||||
text="delete"
|
||||
tooltip="Delete pipeline"
|
||||
/>
|
||||
</v-list-item-title>
|
||||
</v-list-item>
|
||||
<v-list-item @click="openDuplicateDialog">
|
||||
<v-list-item-title>
|
||||
<CVicon
|
||||
color="#c5c5c5"
|
||||
:right="true"
|
||||
text="mdi-content-copy"
|
||||
tooltip="Duplicate pipeline"
|
||||
/>
|
||||
</v-list-item-title>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-menu>
|
||||
</v-col>
|
||||
|
||||
<v-btn
|
||||
style="position: absolute; top:5px;right: 0;"
|
||||
tile
|
||||
color="#4baf62"
|
||||
@click="handleInput('command','save')"
|
||||
>
|
||||
<v-icon>save</v-icon>
|
||||
Save
|
||||
</v-btn>
|
||||
</v-row>
|
||||
<!--pipeline duplicate dialog-->
|
||||
<v-dialog
|
||||
v-model="duplicateDialog"
|
||||
dark
|
||||
width="500"
|
||||
height="357"
|
||||
>
|
||||
<v-card dark>
|
||||
<v-card-title
|
||||
class="headline"
|
||||
primary-title
|
||||
>
|
||||
Duplicate Pipeline
|
||||
</v-card-title>
|
||||
<v-card-text>
|
||||
<CVselect
|
||||
v-model="pipelineDuplicate.pipeline"
|
||||
name="Pipeline"
|
||||
:list="$store.getters.pipelineList"
|
||||
/>
|
||||
<v-checkbox
|
||||
v-if="$store.getters.cameraList.length > 1"
|
||||
v-model="anotherCamera"
|
||||
dark
|
||||
:label="'To another camera'"
|
||||
/>
|
||||
<CVselect
|
||||
v-if="anotherCamera === true"
|
||||
v-model="pipelineDuplicate.camera"
|
||||
name="Camera"
|
||||
:list="$store.getters.cameraList"
|
||||
/>
|
||||
</v-card-text>
|
||||
<v-divider />
|
||||
<v-card-actions>
|
||||
<v-spacer />
|
||||
<v-btn
|
||||
color="#4baf62"
|
||||
@click="duplicatePipeline"
|
||||
>
|
||||
Duplicate
|
||||
</v-btn>
|
||||
<v-btn
|
||||
color="error"
|
||||
@click="closeDuplicateDialog"
|
||||
>
|
||||
Cancel
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
<!--pipeline naming dialog-->
|
||||
<v-dialog
|
||||
v-model="namingDialog"
|
||||
dark
|
||||
width="500"
|
||||
height="357"
|
||||
>
|
||||
<v-card dark>
|
||||
<v-card-title
|
||||
class="headline"
|
||||
primary-title
|
||||
>
|
||||
Pipeline Name
|
||||
</v-card-title>
|
||||
<v-card-text>
|
||||
<CVinput
|
||||
v-model="newPipelineName"
|
||||
name="Pipeline"
|
||||
:error-message="checkPipelineName"
|
||||
@Enter="savePipelineNameChange"
|
||||
/>
|
||||
</v-card-text>
|
||||
<v-divider />
|
||||
<v-card-actions>
|
||||
<v-spacer />
|
||||
<v-btn
|
||||
color="#4baf62"
|
||||
:disabled="checkPipelineName !==''"
|
||||
@click="savePipelineNameChange"
|
||||
>
|
||||
Save
|
||||
</v-btn>
|
||||
<v-btn
|
||||
color="error"
|
||||
@click="discardPipelineNameChange"
|
||||
>
|
||||
Cancel
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import CVicon from '../common/cv-icon'
|
||||
import CVselect from '../common/cv-select'
|
||||
import CVinput from '../common/cv-input'
|
||||
|
||||
export default {
|
||||
name: "CameraAndPipelineSelect",
|
||||
components: {
|
||||
CVicon,
|
||||
CVselect,
|
||||
CVinput
|
||||
},
|
||||
data: () => {
|
||||
return {
|
||||
re: RegExp("^[A-Za-z0-9 \\-)(]*[A-Za-z0-9][A-Za-z0-9 \\-)(.]*$"),
|
||||
isCameraNameEdit: false,
|
||||
newCameraName: "",
|
||||
cameraNameError: "",
|
||||
isPipelineNameEdit: false,
|
||||
namingDialog: false,
|
||||
newPipelineName: "",
|
||||
duplicateDialog: false,
|
||||
anotherCamera: false,
|
||||
pipelineDuplicate: {
|
||||
pipeline: undefined,
|
||||
camera: -1
|
||||
},
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
checkCameraName() {
|
||||
if (this.newCameraName !== this.$store.getters.cameraList[this.currentCameraIndex]) {
|
||||
if (this.re.test(this.newCameraName)) {
|
||||
for (let cam in this.cameraList) {
|
||||
if (this.cameraList.hasOwnProperty(cam)) {
|
||||
if (this.newCameraName === this.cameraList[cam]) {
|
||||
return "Camera by that name already Exists"
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return "Camera name can only contain letters, numbers and spaces"
|
||||
}
|
||||
}
|
||||
return ""
|
||||
},
|
||||
checkPipelineName() {
|
||||
if (this.newPipelineName !== this.$store.getters.pipelineList[this.currentPipelineIndex - 1] || this.isPipelineNameEdit === false) {
|
||||
if (this.re.test(this.newPipelineName)) {
|
||||
for (let pipe in this.$store.getters.pipelineList) {
|
||||
if (this.$store.getters.pipelineList.hasOwnProperty(pipe)) {
|
||||
if (this.newPipelineName === this.$store.getters.pipelineList[pipe]) {
|
||||
return "A pipeline with this name already exists"
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return "Pipeline name can only contain letters, numbers, and spaces"
|
||||
}
|
||||
}
|
||||
return ""
|
||||
},
|
||||
currentCameraIndex: {
|
||||
get() {
|
||||
return this.$store.getters.currentCameraIndex;
|
||||
},
|
||||
set(value) {
|
||||
this.$store.commit('currentPipelineIndex', value - 1);
|
||||
}
|
||||
},
|
||||
currentPipelineIndex: {
|
||||
get() {
|
||||
return this.$store.getters.currentPipelineIndex + 1;
|
||||
},
|
||||
set(value) {
|
||||
this.$store.commit('currentPipelineIndex', value - 1);
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
toCameraNameChange() {
|
||||
this.newCameraName = this.$store.getters.cameraList[this.currentCameraIndex];
|
||||
this.isCameraNameEdit = true;
|
||||
},
|
||||
saveCameraNameChange() {
|
||||
if (this.checkCameraName === "") {
|
||||
this.handleInput("changeCameraName", this.newCameraName);
|
||||
this.discardCameraNameChange();
|
||||
}
|
||||
},
|
||||
discardCameraNameChange() {
|
||||
this.isCameraNameEdit = false;
|
||||
this.newCameraName = "";
|
||||
},
|
||||
toPipelineNameChange() {
|
||||
this.newPipelineName = this.$store.getters.pipelineList[this.currentPipelineIndex - 1];
|
||||
this.isPipelineNameEdit = true;
|
||||
this.namingDialog = true;
|
||||
},
|
||||
toCreatePipeline() {
|
||||
this.newPipelineName = "New Pipeline";
|
||||
this.isPipelineNameEdit = false;
|
||||
this.namingDialog = true;
|
||||
},
|
||||
openDuplicateDialog() {
|
||||
this.pipelineDuplicate = {
|
||||
pipeline: this.currentPipelineIndex - 1,
|
||||
camera: -1
|
||||
};
|
||||
this.duplicateDialog = true;
|
||||
},
|
||||
deleteCurrentPipeline() {
|
||||
if (this.$store.getters.pipelineList.length > 1) {
|
||||
this.handleInput('command', 'deleteCurrentPipeline');
|
||||
} else {
|
||||
this.snackbar = true;
|
||||
}
|
||||
},
|
||||
savePipelineNameChange() {
|
||||
if (this.checkPipelineName === "") {
|
||||
if (this.isPipelineNameEdit) {
|
||||
this.handleInput("changePipelineName", this.newPipelineName);
|
||||
} else {
|
||||
this.handleInput("addNewPipeline", this.newPipelineName);
|
||||
}
|
||||
this.discardPipelineNameChange();
|
||||
}
|
||||
},
|
||||
duplicatePipeline() {
|
||||
if (!this.anotherCamera) {
|
||||
this.pipelineDuplicate.camera = -1
|
||||
}
|
||||
// this.handleInput("duplicatePipeline", this.pipelineDuplicate);
|
||||
this.axios.post("http://" + this.$address + "/api/vision/duplicate", this.pipelineDuplicate);
|
||||
this.closeDuplicateDialog();
|
||||
},
|
||||
closeDuplicateDialog() {
|
||||
this.duplicateDialog = false;
|
||||
this.pipelineDuplicate = {
|
||||
pipeline: undefined,
|
||||
camera: -1
|
||||
}
|
||||
},
|
||||
discardPipelineNameChange() {
|
||||
this.namingDialog = false;
|
||||
this.isPipelineNameEdit = false;
|
||||
this.newPipelineName = "";
|
||||
},
|
||||
}
|
||||
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,92 @@
|
||||
<template>
|
||||
<div>
|
||||
<v-row
|
||||
align="center"
|
||||
justify="start"
|
||||
>
|
||||
<v-col
|
||||
style="padding-right:0"
|
||||
:cols="3"
|
||||
>
|
||||
<v-btn
|
||||
small
|
||||
color="#4baf62"
|
||||
@click="takePointA"
|
||||
>
|
||||
Take Point A
|
||||
</v-btn>
|
||||
</v-col>
|
||||
<v-col
|
||||
style="margin-left:0"
|
||||
:cols="3"
|
||||
>
|
||||
<v-btn
|
||||
small
|
||||
color="#4baf62"
|
||||
@click="takePointB"
|
||||
>
|
||||
Take Point B
|
||||
</v-btn>
|
||||
</v-col>
|
||||
<v-col>
|
||||
<v-btn
|
||||
small
|
||||
color="yellow darken-3"
|
||||
@click="clearSlope"
|
||||
>
|
||||
Clear All Points
|
||||
</v-btn>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "DualCalibration",
|
||||
props: ['rawPoint'],
|
||||
data() {
|
||||
return {
|
||||
pointA: undefined,
|
||||
pointB: undefined
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
takePointA() {
|
||||
this.pointA = this.rawPoint;
|
||||
this.calcSlope();
|
||||
},
|
||||
takePointB() {
|
||||
this.pointB = this.rawPoint;
|
||||
this.calcSlope();
|
||||
},
|
||||
calcSlope() {
|
||||
if (this.pointA !== undefined && this.pointB !== undefined) {
|
||||
let m = (this.pointB[1] - this.pointA[1]) / (this.pointB[0] - this.pointA[0]);
|
||||
let b = this.pointA[1] - (m * this.pointA[0]);
|
||||
if (isNaN(m) === false && isNaN(b) === false) {
|
||||
this.sendSlope(m, b, true);
|
||||
} else {
|
||||
this.$emit('snackbar', "Points are too close");
|
||||
}
|
||||
this.pointA = undefined;
|
||||
this.pointB = undefined;
|
||||
}
|
||||
},
|
||||
sendSlope(m, b) {
|
||||
this.handleInput('dualTargetCalibrationM', m);
|
||||
this.handleInput('dualTargetCalibrationB', b);
|
||||
this.$emit('update');
|
||||
},
|
||||
clearSlope() {
|
||||
this.sendSlope(1, 0, false);
|
||||
this.pointA = undefined;
|
||||
this.pointB = undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,55 @@
|
||||
<template>
|
||||
<div>
|
||||
<v-row
|
||||
align="center"
|
||||
justify="start"
|
||||
>
|
||||
<v-col
|
||||
style="padding-right:0"
|
||||
:cols="3"
|
||||
>
|
||||
<v-btn
|
||||
small
|
||||
color="#4baf62"
|
||||
@click="takePoint"
|
||||
>
|
||||
Take Point
|
||||
</v-btn>
|
||||
</v-col>
|
||||
<v-col>
|
||||
<v-btn
|
||||
small
|
||||
color="yellow darken-3"
|
||||
@click="clearPoint"
|
||||
>
|
||||
Clear Point
|
||||
</v-btn>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "SingleCalibration",
|
||||
props: ['rawPoint'],
|
||||
methods: {
|
||||
clearPoint() {
|
||||
this.handleInput('point', []);
|
||||
this.$emit('update');
|
||||
},
|
||||
takePoint() {
|
||||
if (this.rawPoint[0] && this.rawPoint[1]) {
|
||||
this.handleInput('point', this.rawPoint);
|
||||
this.$emit('update');
|
||||
} else {
|
||||
this.$emit('snackbar', "No target found");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
39
photon-client/src/main.js
Normal file
39
photon-client/src/main.js
Normal file
@@ -0,0 +1,39 @@
|
||||
import Vue from 'vue'
|
||||
import App from './App.vue'
|
||||
import router from './router'
|
||||
import store from './store/index'
|
||||
import vuetify from './plugins/vuetify';
|
||||
import msgPack from 'msgpack5';
|
||||
import axios from 'axios';
|
||||
import VueAxios from "vue-axios";
|
||||
|
||||
Vue.config.productionTip = false;
|
||||
|
||||
if (process.env.NODE_ENV === "production") {
|
||||
Vue.prototype.$address = location.host;
|
||||
} else if (process.env.NODE_ENV === "development") {
|
||||
Vue.prototype.$address = location.hostname + ":5800";
|
||||
}
|
||||
|
||||
const wsURL = 'ws://' + Vue.prototype.$address + '/websocket';
|
||||
|
||||
const ws = new WebSocket(wsURL);
|
||||
ws.binaryType = "arraybuffer";
|
||||
|
||||
import VueNativeSock from 'vue-native-websocket';
|
||||
|
||||
Vue.use(VueNativeSock, wsURL, {
|
||||
WebSocket: ws
|
||||
});
|
||||
Vue.use(VueAxios, axios);
|
||||
Vue.prototype.$msgPack = msgPack(true);
|
||||
|
||||
import {dataHandleMixin} from './mixins/global/dataHandleMixin'
|
||||
|
||||
Vue.mixin(dataHandleMixin);
|
||||
new Vue({
|
||||
router,
|
||||
store,
|
||||
vuetify,
|
||||
render: h => h(App)
|
||||
}).$mount('#app');
|
||||
16
photon-client/src/mixins/global/dataHandleMixin.js
Normal file
16
photon-client/src/mixins/global/dataHandleMixin.js
Normal file
@@ -0,0 +1,16 @@
|
||||
export const dataHandleMixin = {
|
||||
methods: {
|
||||
handleInput(key, value) {
|
||||
let msg = this.$msgPack.encode({[key]: value});
|
||||
this.$socket.send(msg);
|
||||
},
|
||||
handleData(val) {
|
||||
this.handleInput(val, this.value[val]);
|
||||
this.$emit('update')
|
||||
},
|
||||
rollback(val, e) {
|
||||
//TODO UPDATE VALUES INTO WEBSOCKET
|
||||
this.$store.commit('updatePipeline', {[val]: e})
|
||||
}
|
||||
}
|
||||
};
|
||||
121
photon-client/src/plugins/ColorPicker.js
Normal file
121
photon-client/src/plugins/ColorPicker.js
Normal file
@@ -0,0 +1,121 @@
|
||||
var canvas = undefined;
|
||||
var image = undefined;
|
||||
|
||||
function initColorPicker() {
|
||||
canvas = document.createElement('canvas');
|
||||
image = document.getElementById('CameraStream');
|
||||
canvas.width = image.width;
|
||||
canvas.height = image.height;
|
||||
}
|
||||
|
||||
//Called on click of the image,
|
||||
//Finds X,Y of the mouse on the image,
|
||||
//Draws the image on the (invisible) canvas
|
||||
//Reads the color values (pixelData) in X,Y of the canvas
|
||||
//calls the function to handle the button (either eyedrop,expand or shrink)
|
||||
function colorPickerClick(event, currentFunction, currentRange) {
|
||||
let rect = image.getBoundingClientRect();
|
||||
let x = Math.round(event.clientX - rect.left);
|
||||
let y = Math.round(event.clientY - rect.top);
|
||||
let context = canvas.getContext('2d');
|
||||
context.drawImage(image, 0, 0, image.width, image.height);
|
||||
let pixelData = context.getImageData(x, y, 1, 1).data;
|
||||
if (currentFunction !== undefined) {
|
||||
return currentFunction(pixelData, currentRange);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function eyeDrop(pixel) {
|
||||
let hsv = RGBtoHSV(pixel);
|
||||
let range = widenRange([hsv, hsv.slice(0)]);//sends hsv and a copy of hsv
|
||||
return range
|
||||
}
|
||||
|
||||
function expand(pixel, currentRange) {
|
||||
let hsv = RGBtoHSV(pixel);
|
||||
let widenHSV = widenRange([[].concat(hsv), hsv]);
|
||||
return createRange(currentRange.concat(widenHSV));
|
||||
}
|
||||
|
||||
function shrink(pixel, currentRange) {
|
||||
let hsv = RGBtoHSV(pixel);
|
||||
let widenHSV = widenRange([[].concat(hsv), hsv]);
|
||||
if (!shrinkRange(currentRange, widenHSV[0]))//Tries to shrink the lower part of the widen HSV
|
||||
shrinkRange(currentRange, widenHSV[1]);//If the prev attempt failed, try to shrink the higher part of the widen HSV
|
||||
return currentRange
|
||||
}
|
||||
|
||||
//numbers is an array of 3 rgb values, returns array for 3 hsv values
|
||||
function RGBtoHSV(numbers) {
|
||||
let r = numbers[0],
|
||||
g = numbers[1],
|
||||
b = numbers[2];
|
||||
r = r / 255;
|
||||
g = g / 255;
|
||||
b = b / 255;
|
||||
let minRGB = Math.min(r, Math.min(g, b));
|
||||
let maxRGB = Math.max(r, Math.max(g, b));
|
||||
let d = (r === minRGB) ? g - b : ((b === minRGB) ? r - g : b - r);
|
||||
let h = (r === minRGB) ? 3 : ((b === minRGB) ? 1 : 5);
|
||||
let H = 30 * (h - d / (maxRGB - minRGB));
|
||||
let S = 255 * (maxRGB - minRGB) / maxRGB;
|
||||
let V = 255 * maxRGB;
|
||||
if (isNaN(H))
|
||||
H = 0;
|
||||
if (isNaN(S))
|
||||
S = 0;
|
||||
if (isNaN(V))
|
||||
V = 0;
|
||||
return [Math.round(H), Math.round(S), Math.round(V)];
|
||||
}
|
||||
|
||||
//Loops though the colors array, finds the smallest and biggest value for H,S and V. Returns the range containing every color
|
||||
function createRange(HSVColors) {
|
||||
let range = [[], []];
|
||||
for (var i = 0; i < 3; i++) {
|
||||
range[0][i] = HSVColors[0][i];
|
||||
range[1][i] = HSVColors[0][i];
|
||||
for (var j = HSVColors.length - 1; j >= 0; j--) {
|
||||
range[0][i] = Math.min(HSVColors[j][i], range[0][i]);
|
||||
range[1][i] = Math.max(HSVColors[j][i], range[1][i]);
|
||||
}
|
||||
}
|
||||
return range;//[[Hmin,Smin,Vmin],[Hmax,Smax,Vmax]]
|
||||
}
|
||||
|
||||
//This function adds 10 extra units to each side of the sliders, not to be confued with the expand selection button
|
||||
function widenRange(range) {
|
||||
let expanded = [[], []];
|
||||
for (let i = 0; i < 3; i++) {
|
||||
//Expanding the range by 10
|
||||
expanded[0][i] = Math.max(0, range[0][i] - 10);
|
||||
expanded[1][i] = Math.min(255, range[1][i] + 10);
|
||||
}
|
||||
expanded[1][0] = Math.min(180, expanded[1][0]);//h is up to 180
|
||||
return expanded;
|
||||
}
|
||||
|
||||
//If color in range then take the closer range value to color and set it to color plus or minus 10
|
||||
//For example if hmax is 200 hmin is 100 and color's h is 120 range will become [130,200]
|
||||
function shrinkRange(range, color) {
|
||||
|
||||
let inside = true;
|
||||
for (let i = 0; i < color.length && inside; i++) {//Check if color is in range
|
||||
if (!(range[0][i] <= color[i] <= range[1][i]))
|
||||
inside = false;
|
||||
}
|
||||
|
||||
if (inside) {
|
||||
for (let j = 0; j < color.length; j++) {
|
||||
if (color[j] - range[0][j] < range[1][j] - color[j])
|
||||
range[0][j] = Math.min(range[0][j] + 10, range[1][j]);//shrink from min side
|
||||
else
|
||||
range[1][j] = Math.max(range[1][j] - 10, range[0][j]);//shrink from max side
|
||||
}
|
||||
}
|
||||
return inside;//returns if color is inside or not
|
||||
}
|
||||
|
||||
|
||||
export default {initColorPicker, colorPickerClick, eyeDrop, expand, shrink}
|
||||
121
photon-client/src/plugins/objectDiff.js
Normal file
121
photon-client/src/plugins/objectDiff.js
Normal file
@@ -0,0 +1,121 @@
|
||||
//https://gomakethings.com/getting-the-differences-between-two-objects-with-vanilla-js/
|
||||
export const diff = function (obj1, obj2) {
|
||||
|
||||
// Make sure an object to compare is provided
|
||||
if (!obj2 || Object.prototype.toString.call(obj2) !== '[object Object]') {
|
||||
return obj1;
|
||||
}
|
||||
|
||||
//
|
||||
// Variables
|
||||
//
|
||||
|
||||
let diffs = {};
|
||||
let key;
|
||||
|
||||
|
||||
//
|
||||
// Methods
|
||||
//
|
||||
|
||||
/**
|
||||
* Check if two arrays are equal
|
||||
* @param {Array} arr1 The first array
|
||||
* @param {Array} arr2 The second array
|
||||
* @return {Boolean} If true, both arrays are equal
|
||||
*/
|
||||
const arraysMatch = function (arr1, arr2) {
|
||||
|
||||
// Check if the arrays are the same length
|
||||
if (arr1.length !== arr2.length) return false;
|
||||
|
||||
// Check if all items exist and are in the same order
|
||||
for (let i = 0; i < arr1.length; i++) {
|
||||
if (arr1[i] !== arr2[i]) return false;
|
||||
}
|
||||
|
||||
// Otherwise, return true
|
||||
return true;
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* Compare two items and push non-matches to object
|
||||
* @param {*} item1 The first item
|
||||
* @param {*} item2 The second item
|
||||
* @param {String} key The key in our object
|
||||
*/
|
||||
const compare = function (item1, item2, key) {
|
||||
|
||||
// Get the object type
|
||||
let type1 = Object.prototype.toString.call(item1);
|
||||
let type2 = Object.prototype.toString.call(item2);
|
||||
|
||||
// If type2 is undefined it has been removed
|
||||
if (type2 === '[object Undefined]') {
|
||||
diffs[key] = null;
|
||||
return;
|
||||
}
|
||||
|
||||
// If items are different types
|
||||
if (type1 !== type2) {
|
||||
diffs[key] = item2;
|
||||
return;
|
||||
}
|
||||
|
||||
// If an object, compare recursively
|
||||
if (type1 === '[object Object]') {
|
||||
let objDiff = diff(item1, item2);
|
||||
if (Object.keys(objDiff).length > 1) {
|
||||
diffs[key] = objDiff;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// If an array, compare
|
||||
if (type1 === '[object Array]') {
|
||||
if (!arraysMatch(item1, item2)) {
|
||||
diffs[key] = item2;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Else if it's a function, convert to a string and compare
|
||||
// Otherwise, just compare
|
||||
if (type1 === '[object Function]') {
|
||||
if (item1.toString() !== item2.toString()) {
|
||||
diffs[key] = item2;
|
||||
}
|
||||
} else {
|
||||
if (item1 !== item2) {
|
||||
diffs[key] = item2;
|
||||
}
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
|
||||
//
|
||||
// Compare our objects
|
||||
//
|
||||
|
||||
// Loop through the first object
|
||||
for (key in obj1) {
|
||||
if (obj1.hasOwnProperty(key)) {
|
||||
compare(obj1[key], obj2[key], key);
|
||||
}
|
||||
}
|
||||
|
||||
// Loop through the second object and find missing items
|
||||
for (key in obj2) {
|
||||
if (obj2.hasOwnProperty(key)) {
|
||||
if (!obj1[key] && obj1[key] !== obj2[key] ) {
|
||||
diffs[key] = obj2[key];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Return the object of differences
|
||||
return diffs;
|
||||
|
||||
};
|
||||
11
photon-client/src/plugins/vuetify.js
Normal file
11
photon-client/src/plugins/vuetify.js
Normal file
@@ -0,0 +1,11 @@
|
||||
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';
|
||||
|
||||
Vue.use(Vuetify);
|
||||
|
||||
export default new Vuetify({
|
||||
icons: {}
|
||||
|
||||
});
|
||||
23
photon-client/src/router.js
Normal file
23
photon-client/src/router.js
Normal file
@@ -0,0 +1,23 @@
|
||||
import Vue from 'vue'
|
||||
import Router from 'vue-router'
|
||||
import Camera from "./views/PipelineView";
|
||||
import Settings from "./views/SettingsView";
|
||||
Vue.use(Router);
|
||||
|
||||
|
||||
export default new Router({
|
||||
// mode: 'history',
|
||||
base: process.env.BASE_URL,
|
||||
routes: [{
|
||||
path: '/',
|
||||
redirect: '/vision'
|
||||
}, {
|
||||
path: '/vision',
|
||||
name: 'Vision',
|
||||
component: Camera
|
||||
}, {
|
||||
path: '/settings',
|
||||
name: 'Settings',
|
||||
component: Settings
|
||||
}]
|
||||
})
|
||||
66
photon-client/src/store/index.js
Normal file
66
photon-client/src/store/index.js
Normal file
@@ -0,0 +1,66 @@
|
||||
import Vue from 'vue'
|
||||
import Vuex from 'vuex'
|
||||
|
||||
import pipeline from "./modules/pipeline";
|
||||
import generalSettings from "./modules/generalSettings";
|
||||
import cameraSettings from "./modules/cameraSettings";
|
||||
import undoRedo from "./modules/undoRedo";
|
||||
|
||||
Vue.use(Vuex);
|
||||
|
||||
const set = key => (state, val) => {
|
||||
Vue.set(state, key, val);
|
||||
};
|
||||
|
||||
export default new Vuex.Store({
|
||||
modules: {
|
||||
pipeline: pipeline,
|
||||
settings: generalSettings,
|
||||
cameraSettings: cameraSettings,
|
||||
undoRedo: undoRedo
|
||||
},
|
||||
state: {
|
||||
resolutionList: [],
|
||||
port: 1181,
|
||||
currentCameraIndex: 0,
|
||||
currentPipelineIndex: 0,
|
||||
cameraList: [],
|
||||
pipelineList: [],
|
||||
point: {},
|
||||
saveBar: false
|
||||
},
|
||||
mutations: {
|
||||
settings: set('settings'),
|
||||
pipeline: set('pipeline'),
|
||||
cameraSettings: set('cameraSettings'),
|
||||
resolutionList: set('resolutionList'),
|
||||
port: set('port'),
|
||||
currentCameraIndex: set('currentCameraIndex'),
|
||||
currentPipelineIndex: set('currentPipelineIndex'),
|
||||
cameraList: set('cameraList'),
|
||||
pipelineList: set('pipelineList'),
|
||||
point: set('point'),
|
||||
driverMode: set('driverMode'),
|
||||
saveBar: set("saveBar")
|
||||
},
|
||||
getters: {
|
||||
streamAddress: state => {
|
||||
return "http://" + location.hostname + ":" + state.port + "/stream.mjpg";
|
||||
},
|
||||
targets: state => {
|
||||
return state.point['targets']
|
||||
},
|
||||
cameraList: state => {
|
||||
return state.cameraList
|
||||
},
|
||||
pipelineList: state => {
|
||||
return state.pipelineList
|
||||
},
|
||||
currentCameraIndex: state => {
|
||||
return state.currentCameraIndex
|
||||
},
|
||||
currentPipelineIndex: state => {
|
||||
return state.currentPipelineIndex
|
||||
}
|
||||
}
|
||||
})
|
||||
14
photon-client/src/store/modules/cameraSettings.js
Normal file
14
photon-client/src/store/modules/cameraSettings.js
Normal file
@@ -0,0 +1,14 @@
|
||||
export default {
|
||||
state: {
|
||||
calibration: [],
|
||||
fov: 0,
|
||||
resolution: 0,
|
||||
streamDivisor: 0,
|
||||
tilt: 0
|
||||
},
|
||||
getters: {
|
||||
cameraSettings: state => {
|
||||
return state
|
||||
}
|
||||
}
|
||||
}
|
||||
10
photon-client/src/store/modules/generalSettings.js
Normal file
10
photon-client/src/store/modules/generalSettings.js
Normal file
@@ -0,0 +1,10 @@
|
||||
export default {
|
||||
state:{
|
||||
teamNumber: 1577,
|
||||
connectionType: 0,
|
||||
ip: "",
|
||||
gateway: "",
|
||||
netmask: "",
|
||||
hostname: "chameleon-vision"
|
||||
}
|
||||
}
|
||||
45
photon-client/src/store/modules/pipeline.js
Normal file
45
photon-client/src/store/modules/pipeline.js
Normal file
@@ -0,0 +1,45 @@
|
||||
import Vue from 'vue'
|
||||
|
||||
export default {
|
||||
state: {
|
||||
exposure: 0,
|
||||
brightness: 0,
|
||||
gain: 0,
|
||||
rotationMode: 0,
|
||||
hue: [0, 15],
|
||||
saturation: [0, 15],
|
||||
value: [0, 25],
|
||||
erode: false,
|
||||
dilate: false,
|
||||
area: [0, 12],
|
||||
ratio: [0, 12],
|
||||
extent: [0, 12],
|
||||
speckle: 5,
|
||||
targetGrouping: 0,
|
||||
targetIntersection: 0,
|
||||
sortMode: 0,
|
||||
multiple: false,
|
||||
isBinary: 0,
|
||||
calibrationMode: 0,
|
||||
videoModeIndex: 0,
|
||||
streamDivisor: 0,
|
||||
is3D: false,
|
||||
targetRegion: 0,
|
||||
targetOrientation: 1
|
||||
},
|
||||
mutations: {
|
||||
isBinary: (state, value) => {
|
||||
state.isBinary = value
|
||||
},
|
||||
mutatePipeline: (state, {key, value}) => {
|
||||
Vue.set(state, key, value)
|
||||
}
|
||||
|
||||
},
|
||||
actions: {},
|
||||
getters: {
|
||||
pipeline: state => {
|
||||
return state
|
||||
}
|
||||
}
|
||||
};
|
||||
72
photon-client/src/store/modules/undoRedo.js
Normal file
72
photon-client/src/store/modules/undoRedo.js
Normal file
@@ -0,0 +1,72 @@
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
import Vue from 'vue'
|
||||
|
||||
export default {
|
||||
state: {
|
||||
done: [],
|
||||
undone: [],
|
||||
newMutation: true
|
||||
},
|
||||
mutations: {
|
||||
updatePipeline: (state, val) => {
|
||||
state.done.push(val)
|
||||
if (state.newMutation) {
|
||||
state.undone = []
|
||||
}
|
||||
},
|
||||
addUndone: (state, val) => {
|
||||
state.undone.push(val);
|
||||
},
|
||||
removeLastDone: state => {
|
||||
state.done.pop()
|
||||
},
|
||||
removeLastUnDone: state => {
|
||||
state.undone.pop()
|
||||
},
|
||||
updateStatus: (state, bool) => {
|
||||
state.newMutation = bool;
|
||||
},
|
||||
},
|
||||
actions: {
|
||||
undo: (context, {vm}) => {
|
||||
let commit = context.getters.lastDone;
|
||||
context.commit('removeLastDone')
|
||||
context.commit('updateStatus', false)
|
||||
for (let key in commit) {
|
||||
if (commit.hasOwnProperty(key)) {
|
||||
context.commit('addUndone', {[key]: context.getters["pipeline"][key]});
|
||||
context.commit('mutatePipeline', {'key': key, 'value': commit[key]});
|
||||
vm.handleInput(key, commit[key]);
|
||||
}
|
||||
}
|
||||
context.commit('updateStatus', true)
|
||||
},
|
||||
redo: (context, {vm}) => {
|
||||
let commit = context.getters.lastUnDone;
|
||||
context.commit('removeLastUnDone');
|
||||
context.commit('updateStatus', false)
|
||||
for (let key in commit) {
|
||||
if (commit.hasOwnProperty(key)) {
|
||||
context.commit('mutatePipeline', {'key': key, 'value': commit[key]});
|
||||
vm.handleInput(key, commit[key]);
|
||||
}
|
||||
}
|
||||
context.commit('updateStatus', true)
|
||||
}
|
||||
},
|
||||
getters: {
|
||||
lastDone: state => {
|
||||
return state.done[state.done.length - 1]
|
||||
},
|
||||
lastUnDone: state => {
|
||||
return state.undone[state.undone.length - 1]
|
||||
},
|
||||
canUndo: state => {
|
||||
return state.done.length
|
||||
},
|
||||
canRedo: state => {
|
||||
return state.undone.length
|
||||
}
|
||||
}
|
||||
|
||||
};
|
||||
208
photon-client/src/views/PipelineView.vue
Normal file
208
photon-client/src/views/PipelineView.vue
Normal file
@@ -0,0 +1,208 @@
|
||||
<template>
|
||||
<div>
|
||||
<camera-and-pipeline-select />
|
||||
<v-row>
|
||||
<!-- vision tabs -->
|
||||
<v-col
|
||||
cols="6"
|
||||
class="colsClass"
|
||||
>
|
||||
<v-tabs
|
||||
v-if="($store.getters.currentPipelineIndex + 1) !== 0"
|
||||
v-model="selectedTab"
|
||||
fixed-tabs
|
||||
background-color="#212121"
|
||||
dark
|
||||
height="48"
|
||||
slider-color="#4baf62"
|
||||
>
|
||||
<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="6"
|
||||
class="colsClass"
|
||||
>
|
||||
<div>
|
||||
<!-- camera image tabs -->
|
||||
<v-tabs
|
||||
v-if="($store.getters.currentPipelineIndex + 1) !== 0"
|
||||
v-model="isBinaryNumber"
|
||||
background-color="#212121"
|
||||
dark
|
||||
height="48"
|
||||
slider-color="#4baf62"
|
||||
centered
|
||||
style="padding-bottom:10px"
|
||||
@change="handleInput('isBinary',$store.getters.pipeline.isBinary)"
|
||||
>
|
||||
<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">
|
||||
<cvImage
|
||||
:address="$store.getters.streamAddress"
|
||||
:scale="75"
|
||||
@click="onImageClick"
|
||||
/>
|
||||
</v-row>
|
||||
<v-row justify="end">
|
||||
<span style="margin-right: 45px">FPS:{{ parseFloat(fps).toFixed(2) }}</span>
|
||||
</v-row>
|
||||
<v-row align="center">
|
||||
<v-simple-table
|
||||
style="text-align: center;background-color: transparent; display: block;margin: auto"
|
||||
dense
|
||||
dark
|
||||
>
|
||||
<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.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>
|
||||
</v-row>
|
||||
</div>
|
||||
</div>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<!-- snack bar -->
|
||||
<v-snackbar
|
||||
v-model="snackbar"
|
||||
:timeout="3000"
|
||||
top
|
||||
color="error"
|
||||
>
|
||||
<span style="color:#000">Can not remove the only pipeline!</span>
|
||||
<v-btn
|
||||
color="black"
|
||||
text
|
||||
@click="snackbar = false"
|
||||
>
|
||||
Close
|
||||
</v-btn>
|
||||
</v-snackbar>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import CameraAndPipelineSelect from "../components/pipeline/CameraAndPipelineSelect";
|
||||
import cvImage from '../components/common/cv-image'
|
||||
import InputTab from './PipelineViewes/InputTab'
|
||||
import ThresholdTab from './PipelineViewes/ThresholdTab'
|
||||
import ContoursTab from './PipelineViewes/ContoursTab'
|
||||
import OutputTab from './PipelineViewes/OutputTab'
|
||||
import pnpTab from './PipelineViewes/3D'
|
||||
|
||||
export default {
|
||||
name: 'CameraTab',
|
||||
components: {
|
||||
CameraAndPipelineSelect,
|
||||
cvImage,
|
||||
InputTab,
|
||||
ThresholdTab,
|
||||
ContoursTab,
|
||||
OutputTab,
|
||||
pnpTab,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
selectedTab: 0,
|
||||
snackbar: false,
|
||||
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
isBinaryNumber: {
|
||||
get() {
|
||||
return this.$store.getters.pipeline.isBinary ? 1 : 0
|
||||
},
|
||||
set(value) {
|
||||
this.$store.commit('isBinary', !!value);
|
||||
}
|
||||
},
|
||||
selectedComponent: {
|
||||
get() {
|
||||
return (this.$store.getters.currentPipelineIndex + 1) === 0 ? "InputTab" : ["InputTab", "ThresholdTab", "ContoursTab", "OutputTab", "pnpTab"][this.selectedTab];
|
||||
}
|
||||
},
|
||||
fps: {
|
||||
get() {
|
||||
return this.$store.state.point.fps;
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
onImageClick(event) {
|
||||
if (this.selectedTab === 1) {
|
||||
this.$refs.component.onClick(event);
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.colsClass {
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
.videoClass {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
th {
|
||||
width: 80px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
</style>
|
||||
206
photon-client/src/views/PipelineViewes/3D.vue
Normal file
206
photon-client/src/views/PipelineViewes/3D.vue
Normal file
@@ -0,0 +1,206 @@
|
||||
<template>
|
||||
<div>
|
||||
<v-row
|
||||
align="center"
|
||||
justify="start"
|
||||
dense
|
||||
>
|
||||
<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>
|
||||
<CVslider
|
||||
v-model="value.accuracy"
|
||||
name="Contour simplification"
|
||||
:min="0"
|
||||
:max="100"
|
||||
@input="handleData('accuracy')"
|
||||
@rollback="e=> rollback('accuracy',e)"
|
||||
/>
|
||||
<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="#4baf62"
|
||||
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
|
||||
:color="snackbar.color"
|
||||
>
|
||||
<span>{{ snackbar.text }}</span>
|
||||
</v-snackbar>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<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
|
||||
},
|
||||
props: ['value'],
|
||||
data() {
|
||||
return {
|
||||
is3D: false,
|
||||
selectedModel: null,
|
||||
FRCtargets: null,
|
||||
snackbar: {
|
||||
color: "success",
|
||||
text: ""
|
||||
},
|
||||
snack: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
targets: {
|
||||
get() {
|
||||
return this.$store.state.point.targets;
|
||||
}
|
||||
},
|
||||
horizontalFOV: {
|
||||
get() {
|
||||
let index = this.$store.state.cameraSettings.resolution;
|
||||
let FOV = this.$store.state.cameraSettings.fov;
|
||||
let resolution = this.$store.state.resolutionList[index];
|
||||
let diagonalView = FOV * (Math.PI / 180);
|
||||
let diagonalAspect = Math.hypot(resolution.width, resolution.height);
|
||||
return Math.atan(Math.tan(diagonalView / 2) * (resolution.width / diagonalAspect)) * 2 * (180 / Math.PI)
|
||||
}
|
||||
},
|
||||
allow3D: {
|
||||
get() {
|
||||
let currentRes = this.$store.state.resolutionList[this.$store.state.pipeline.videoModeIndex];
|
||||
for (let res of this.$store.state.cameraSettings.calibration) {
|
||||
if (currentRes.width === res.width && currentRes.height === res.height) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
let tmp = [];
|
||||
for (let t in FRCtargetsConfig) {
|
||||
if (FRCtargetsConfig.hasOwnProperty(t)) {
|
||||
tmp.push({name: t, data: FRCtargetsConfig[t]})
|
||||
}
|
||||
}
|
||||
this.FRCtargets = tmp;
|
||||
},
|
||||
methods: {
|
||||
readFile(event) {
|
||||
let file = event.target.files[0];
|
||||
Papa.parse(file, {
|
||||
complete: this.onParse,
|
||||
skipEmptyLines: true
|
||||
});
|
||||
},
|
||||
onParse(result) {
|
||||
if (result.data.length > 0) {
|
||||
|
||||
|
||||
let data = [];
|
||||
for (let item of result.data) {
|
||||
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"
|
||||
};
|
||||
this.snack = true;
|
||||
return;
|
||||
}
|
||||
data.push(tmp);
|
||||
}
|
||||
this.uploadModel(data);
|
||||
} else {
|
||||
this.snackbar = {
|
||||
color: "error",
|
||||
text: "Error: cvs did not contain any data"
|
||||
};
|
||||
this.snack = true;
|
||||
}
|
||||
},
|
||||
uploadPremade() {
|
||||
this.uploadModel(this.selectedModel);
|
||||
},
|
||||
uploadModel(model) {
|
||||
this.axios.post("http://" + this.$address + "/api/vision/pnpModel", model).then(() => {
|
||||
this.snackbar = {
|
||||
color: "success",
|
||||
text: "File uploaded successfully"
|
||||
};
|
||||
this.snack = true;
|
||||
}).catch(() => {
|
||||
this.snackbar = {
|
||||
color: "error",
|
||||
text: "An error occurred"
|
||||
};
|
||||
this.snack = true;
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.miniMapClass {
|
||||
width: 50% !important;
|
||||
height: 50% !important;
|
||||
}
|
||||
</style>
|
||||
84
photon-client/src/views/PipelineViewes/ContoursTab.vue
Normal file
84
photon-client/src/views/PipelineViewes/ContoursTab.vue
Normal file
@@ -0,0 +1,84 @@
|
||||
<template>
|
||||
<div>
|
||||
<CVrangeSlider
|
||||
v-model="value.area"
|
||||
name="Area"
|
||||
:min="0"
|
||||
:max="100"
|
||||
:step="0.1"
|
||||
@input="handleData('area')"
|
||||
@rollback="e=> rollback('area',e)"
|
||||
/>
|
||||
<CVrangeSlider
|
||||
v-model="value.ratio"
|
||||
name="Ratio (W/H)"
|
||||
:min="0"
|
||||
:max="100"
|
||||
:step="0.1"
|
||||
@input="handleData('ratio')"
|
||||
@rollback="e=> rollback('ratio',e)"
|
||||
/>
|
||||
<CVrangeSlider
|
||||
v-model="value.extent"
|
||||
name="Extent"
|
||||
:min="0"
|
||||
:max="100"
|
||||
@input="handleData('extent')"
|
||||
@rollback="e=> rollback('extent',e)"
|
||||
/>
|
||||
<CVslider
|
||||
v-model="value.speckle"
|
||||
name="Speckle Rejection"
|
||||
:min="0"
|
||||
:max="100"
|
||||
@input="handleData('speckle')"
|
||||
@rollback="e=> rollback('speckle',e)"
|
||||
/>
|
||||
<CVselect
|
||||
v-model="value.targetGroup"
|
||||
name="Target Group"
|
||||
:list="['Single','Dual']"
|
||||
@input="handleData('targetGroup')"
|
||||
@rollback="e=> rollback('targetGroup',e)"
|
||||
/>
|
||||
<CVselect
|
||||
v-model="value.targetIntersection"
|
||||
name="Target Intersection"
|
||||
:list="['None','Up','Down','Left','Right']"
|
||||
:disabled="isDisabled"
|
||||
@input="handleData('targetIntersection')"
|
||||
@rollback="e=> rollback('targetIntersection',e)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import CVrangeSlider from '../../components/common/cv-range-slider'
|
||||
import CVselect from '../../components/common/cv-select'
|
||||
import CVslider from '../../components/common/cv-slider'
|
||||
|
||||
export default {
|
||||
name: 'Contours',
|
||||
components: {
|
||||
CVrangeSlider,
|
||||
CVselect,
|
||||
CVslider
|
||||
},
|
||||
props: ['value'],
|
||||
|
||||
data() {
|
||||
return {}
|
||||
},
|
||||
computed: {
|
||||
isDisabled() {
|
||||
return this.value.targetGroup === 0;
|
||||
|
||||
}
|
||||
},
|
||||
methods: {},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="" scoped>
|
||||
|
||||
</style>
|
||||
95
photon-client/src/views/PipelineViewes/InputTab.vue
Normal file
95
photon-client/src/views/PipelineViewes/InputTab.vue
Normal file
@@ -0,0 +1,95 @@
|
||||
<template>
|
||||
<div>
|
||||
<CVslider
|
||||
v-model="value.exposure"
|
||||
name="Exposure"
|
||||
:min="0"
|
||||
:max="100"
|
||||
@input="handleData('exposure')"
|
||||
@rollback="e => rollback('exposure', e)"
|
||||
/>
|
||||
<CVslider
|
||||
v-model="value.brightness"
|
||||
name="Brightness"
|
||||
:min="0"
|
||||
:max="100"
|
||||
@input="handleData('brightness')"
|
||||
@rollback="e => rollback('brightness', e)"
|
||||
/>
|
||||
<CVslider
|
||||
v-if="value.gain !== -1"
|
||||
v-model="value.gain"
|
||||
name="Gain"
|
||||
:min="0"
|
||||
:max="100"
|
||||
@input="handleData('gain')"
|
||||
@rollback="e => rollback('gain', e)"
|
||||
/>
|
||||
<CVselect
|
||||
v-model="value.rotationMode"
|
||||
name="Orientation"
|
||||
:list="['Normal','90° CW','180°','90° CCW']"
|
||||
@input="handleData('rotationMode')"
|
||||
@rollback="e => e => rollback('rotationMode',e)"
|
||||
/>
|
||||
<CVselect
|
||||
v-model="value.videoModeIndex"
|
||||
name="Resolution"
|
||||
:list="resolutionList"
|
||||
@input="handleData('videoModeIndex')"
|
||||
@rollback="e => rollback('videoModeIndex', e)"
|
||||
/>
|
||||
<CVselect
|
||||
v-model="value.streamDivisor"
|
||||
name="Stream Resolution"
|
||||
:list="streamResolutionList"
|
||||
@input="handleData('streamDivisor')"
|
||||
@rollback="e => rollback('streamDivisor', e)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import CVslider from '../../components/common/cv-slider'
|
||||
import CVselect from '../../components/common/cv-select'
|
||||
|
||||
export default {
|
||||
name: 'Input',
|
||||
components: {
|
||||
CVslider,
|
||||
CVselect,
|
||||
},
|
||||
// eslint-disable-next-line vue/require-prop-types
|
||||
props: ['value'],
|
||||
data() {
|
||||
return {}
|
||||
},
|
||||
computed: {
|
||||
resolutionList: {
|
||||
get() {
|
||||
let tmp_list = [];
|
||||
for (let i of this.$store.state.resolutionList) {
|
||||
tmp_list.push(`${i['width']} X ${i['height']} at ${i['fps']} FPS, ${i['pixelFormat']}`)
|
||||
}
|
||||
return tmp_list;
|
||||
}
|
||||
},
|
||||
streamResolutionList: {
|
||||
get() {
|
||||
let cam_res = this.$store.state.resolutionList[this.value.videoModeIndex];
|
||||
let tmp_list = [];
|
||||
tmp_list.push(`${Math.floor(cam_res['width'])} X ${Math.floor(cam_res['height'])}`);
|
||||
for (let x = 2; x <= 6; x += 2) {
|
||||
tmp_list.push(`${Math.floor(cam_res['width'] / x)} X ${Math.floor(cam_res['height'] / x)}`);
|
||||
}
|
||||
return tmp_list;
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
126
photon-client/src/views/PipelineViewes/OutputTab.vue
Normal file
126
photon-client/src/views/PipelineViewes/OutputTab.vue
Normal file
@@ -0,0 +1,126 @@
|
||||
<template>
|
||||
<div>
|
||||
<CVselect
|
||||
v-model="value.sortMode"
|
||||
name="Sort Mode"
|
||||
:list="['Largest','Smallest','Highest','Lowest','Rightmost','Leftmost','Centermost']"
|
||||
@input="handleData('sortMode')"
|
||||
@rollback="rollback('sortMode',e)"
|
||||
/>
|
||||
|
||||
<CVselect
|
||||
v-model="value.targetRegion"
|
||||
name="Target Region"
|
||||
:list="['Center','Top','Bottom','Left','Right']"
|
||||
@input="handleData('targetRegion')"
|
||||
@rollback="e=> rollback('targetRegion',e)"
|
||||
/>
|
||||
|
||||
<CVselect
|
||||
v-model="value.targetOrientation"
|
||||
name="Target Orientation"
|
||||
:list="['Portrait', 'Landscape']"
|
||||
@input="handleData('targetOrientation')"
|
||||
@rollback="e=> rollback('targetOrientation',e)"
|
||||
/>
|
||||
|
||||
<CVswitch
|
||||
v-model="value.multiple"
|
||||
name="Output multiple"
|
||||
@input="handleData('multiple')"
|
||||
@rollback="e=> rollback('multiple',e)"
|
||||
/>
|
||||
<span>Calibrate:</span>
|
||||
<v-divider
|
||||
dark
|
||||
color="white"
|
||||
/>
|
||||
<CVselect
|
||||
v-model="value.calibrationMode"
|
||||
name="Calibration Mode"
|
||||
:list="['None','Single point','Dual point']"
|
||||
@input="handleData('calibrationMode')"
|
||||
@rollback="e=> rollback('calibrationMode',e)"
|
||||
/>
|
||||
<component
|
||||
:is="selectedComponent"
|
||||
:raw-point="rawPoint"
|
||||
@update="doUpdate"
|
||||
@snackbar="showSnackbar"
|
||||
/>
|
||||
<v-snackbar
|
||||
v-model="snackbar"
|
||||
:timeout="3000"
|
||||
top
|
||||
color="error"
|
||||
>
|
||||
<span style="color:#000">{{ snackbarText }}</span>
|
||||
<v-btn
|
||||
color="black"
|
||||
text
|
||||
@click="snackbar = false"
|
||||
>
|
||||
Close
|
||||
</v-btn>
|
||||
</v-snackbar>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import CVselect from '../../components/common/cv-select'
|
||||
import CVswitch from '../../components/common/cv-switch'
|
||||
import DualCalibration from "../../components/pipeline/OutputTab/DualCalibration";
|
||||
import SingleCalibration from "../../components/pipeline/OutputTab/SingleCalibration";
|
||||
|
||||
|
||||
export default {
|
||||
name: 'Output',
|
||||
components: {
|
||||
CVselect,
|
||||
CVswitch,
|
||||
SingleCalibration,
|
||||
DualCalibration,
|
||||
|
||||
},
|
||||
props: ['value'],
|
||||
|
||||
data() {
|
||||
return {
|
||||
snackbar: false,
|
||||
snackbarText: ""
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
selectedComponent: {
|
||||
get() {
|
||||
switch (this.value.calibrationMode) {
|
||||
case 0:
|
||||
return "";
|
||||
case 1:
|
||||
return "SingleCalibration";
|
||||
case 2:
|
||||
return "DualCalibration"
|
||||
}
|
||||
return ""
|
||||
}
|
||||
},
|
||||
rawPoint: {
|
||||
get() {
|
||||
return this.$store.state.point.rawPoint;
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
doUpdate() {
|
||||
this.$emit('update')
|
||||
},
|
||||
showSnackbar(message) {
|
||||
this.snackbarText = message;
|
||||
this.snackbar = true;
|
||||
},
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
</style>
|
||||
162
photon-client/src/views/PipelineViewes/ThresholdTab.vue
Normal file
162
photon-client/src/views/PipelineViewes/ThresholdTab.vue
Normal file
@@ -0,0 +1,162 @@
|
||||
<template>
|
||||
<div>
|
||||
<CVrangeSlider
|
||||
v-model="value.hue"
|
||||
name="Hue"
|
||||
:min="0"
|
||||
:max="180"
|
||||
@input="handleData('hue')"
|
||||
@rollback="e => rollback('hue',e)"
|
||||
/>
|
||||
<CVrangeSlider
|
||||
v-model="value.saturation"
|
||||
name="Saturation"
|
||||
:min="0"
|
||||
:max="255"
|
||||
@input="handleData('saturation')"
|
||||
@rollback="e => rollback('saturation',e)"
|
||||
/>
|
||||
<CVrangeSlider
|
||||
v-model="value.value"
|
||||
name="Value"
|
||||
:min="0"
|
||||
:max="255"
|
||||
@input="handleData('value')"
|
||||
@rollback="e => rollback('value',e)"
|
||||
/>
|
||||
<v-divider
|
||||
color="darkgray "
|
||||
style="margin-top: 5px"
|
||||
/>
|
||||
<v-btn
|
||||
style="margin: 20px;"
|
||||
color="#4baf62"
|
||||
small
|
||||
@click="setFunction(1)"
|
||||
>
|
||||
<v-icon>colorize</v-icon>
|
||||
Eye drop
|
||||
</v-btn>
|
||||
<v-btn
|
||||
style="margin: 20px;"
|
||||
color="#4baf62"
|
||||
small
|
||||
@click="setFunction(2)"
|
||||
>
|
||||
<v-icon>add</v-icon>
|
||||
Expand Selection
|
||||
</v-btn>
|
||||
<v-btn
|
||||
style="margin: 20px;"
|
||||
color="#4baf62"
|
||||
small
|
||||
@click="setFunction(3)"
|
||||
>
|
||||
<v-icon>remove</v-icon>
|
||||
Shrink Selection
|
||||
</v-btn>
|
||||
<v-divider color="darkgray " />
|
||||
<CVswitch
|
||||
v-model="value.erode"
|
||||
name="Erode"
|
||||
@input="handleData('erode')"
|
||||
@rollback="e => rollback('erode',e)"
|
||||
/>
|
||||
<CVswitch
|
||||
v-model="value.dilate"
|
||||
name="Dilate"
|
||||
@input="handleData('dilate')"
|
||||
@rollback="e => rollback('dilate',e)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import CVrangeSlider from '../../components/common/cv-range-slider'
|
||||
import CVswitch from '../../components/common/cv-switch'
|
||||
|
||||
export default {
|
||||
name: 'Threshold',
|
||||
components: {
|
||||
CVrangeSlider,
|
||||
CVswitch
|
||||
},
|
||||
props: ['value'],
|
||||
data() {
|
||||
return {
|
||||
currentFunction: undefined,
|
||||
colorPicker: undefined,
|
||||
currentBinaryState: 0
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
pipeline: {
|
||||
get() {
|
||||
return this.$store.state.pipeline;
|
||||
}
|
||||
},
|
||||
driverState: {
|
||||
get() {
|
||||
return this.$store.state.driverMode;
|
||||
},
|
||||
set(val) {
|
||||
this.$store.commit("driverMode", val);
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted: function () {
|
||||
const self = this;
|
||||
this.colorPicker = require('../../plugins/ColorPicker').default;
|
||||
this.$nextTick(() => {
|
||||
self.colorPicker.initColorPicker();
|
||||
});
|
||||
},
|
||||
methods: {
|
||||
onClick(event) {
|
||||
if (this.currentFunction !== undefined) {
|
||||
let hsvArray = this.colorPicker.colorPickerClick(event, this.currentFunction,
|
||||
[[this.value.hue[0], this.value.saturation[0], this.value.value[0]], [this.value.hue[1], this.value.saturation[1], this.value.value[1]]]);
|
||||
this.currentFunction = undefined;
|
||||
this.value.hue = [hsvArray[0][0], hsvArray[1][0]];
|
||||
this.value.saturation = [hsvArray[0][1], hsvArray[1][1]];
|
||||
this.value.value = [hsvArray[0][2], hsvArray[1][2]];
|
||||
this.value.isBinary = this.currentBinaryState;
|
||||
let msg = this.$msgPack.encode({
|
||||
'hue': this.value.hue,
|
||||
'saturation': this.value.saturation,
|
||||
'value': this.value.value,
|
||||
'isBinary': this.value.isBinary
|
||||
});
|
||||
this.$socket.send(msg);
|
||||
this.$emit('update');
|
||||
}
|
||||
},
|
||||
setFunction(index) {
|
||||
this.currentBinaryState = this.value.isBinary;
|
||||
if (this.currentBinaryState === true) {
|
||||
this.value.isBinary = false;
|
||||
this.handleData('isBinary')
|
||||
}
|
||||
switch (index) {
|
||||
case 0:
|
||||
this.currentFunction = undefined;
|
||||
break;
|
||||
case 1:
|
||||
this.currentFunction = this.colorPicker.eyeDrop;
|
||||
break;
|
||||
case 2:
|
||||
this.currentFunction = this.colorPicker.expand;
|
||||
break;
|
||||
case 3:
|
||||
this.currentFunction = this.colorPicker.shrink;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="" scoped>
|
||||
|
||||
</style>
|
||||
88
photon-client/src/views/SettingsView.vue
Normal file
88
photon-client/src/views/SettingsView.vue
Normal file
@@ -0,0 +1,88 @@
|
||||
<template>
|
||||
<div>
|
||||
<v-row>
|
||||
<v-col
|
||||
class="colsClass"
|
||||
cols="6"
|
||||
>
|
||||
<v-tabs
|
||||
v-model="selectedTab"
|
||||
background-color="#212121"
|
||||
dark
|
||||
fixed-tabs
|
||||
height="50"
|
||||
slider-color="#4baf62"
|
||||
>
|
||||
<v-tab to="">
|
||||
General
|
||||
</v-tab>
|
||||
<v-tab to="">
|
||||
Cameras
|
||||
</v-tab>
|
||||
</v-tabs>
|
||||
<div style="padding-left:30px">
|
||||
<component
|
||||
:is="selectedComponent"
|
||||
@update="$emit('save')"
|
||||
/>
|
||||
</div>
|
||||
</v-col>
|
||||
<v-col
|
||||
v-show="selectedTab === 1"
|
||||
class="colsClass"
|
||||
>
|
||||
<div class="videoClass">
|
||||
<cvImage
|
||||
:address="$store.getters.streamAddress"
|
||||
:scale="75"
|
||||
/>
|
||||
</div>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import General from './SettingsViewes/General'
|
||||
import Cameras from './SettingsViewes/Cameras'
|
||||
import cvImage from '../components/common/cv-image'
|
||||
|
||||
|
||||
export default {
|
||||
name: 'SettingsTab',
|
||||
components: {
|
||||
cvImage,
|
||||
General,
|
||||
Cameras,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
selectedTab: 0,
|
||||
tabList: [General, Cameras]
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
selectedComponent: {
|
||||
get() {
|
||||
return this.tabList[this.selectedTab];
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.videoClass {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.videoClass img {
|
||||
padding-top: 10px;
|
||||
height: auto !important;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.colsClass {
|
||||
padding: 0 !important;
|
||||
}
|
||||
</style>
|
||||
343
photon-client/src/views/SettingsViewes/Cameras.vue
Normal file
343
photon-client/src/views/SettingsViewes/Cameras.vue
Normal file
@@ -0,0 +1,343 @@
|
||||
<template>
|
||||
<div>
|
||||
<div>
|
||||
<CVselect
|
||||
v-model="currentCameraIndex"
|
||||
name="Camera"
|
||||
:list="$store.getters.cameraList"
|
||||
@input="handleInput('currentCamera',currentCameraIndex)"
|
||||
/>
|
||||
<CVnumberinput
|
||||
v-model="cameraSettings.fov"
|
||||
name="Diagonal FOV"
|
||||
/>
|
||||
<br>
|
||||
<CVnumberinput
|
||||
v-model="cameraSettings.tilt"
|
||||
name="Camera pitch"
|
||||
:step="0.01"
|
||||
/>
|
||||
<br>
|
||||
<v-btn
|
||||
style="margin-top:10px"
|
||||
small
|
||||
color="#4baf62"
|
||||
@click="sendCameraSettings"
|
||||
>
|
||||
Save Camera Settings
|
||||
</v-btn>
|
||||
</div>
|
||||
<div style="margin-top: 15px">
|
||||
<span>3D Calibration</span>
|
||||
<v-divider
|
||||
color="white"
|
||||
style="margin-bottom: 10px"
|
||||
/>
|
||||
<v-row>
|
||||
<v-col>
|
||||
<CVselect
|
||||
v-model="resolutionIndex"
|
||||
name="Resolution"
|
||||
:list="stringResolutionList"
|
||||
/>
|
||||
</v-col>
|
||||
<v-col>
|
||||
<CVnumberinput
|
||||
v-model="squareSize"
|
||||
name="Square Size (in)"
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col>
|
||||
<v-btn
|
||||
small
|
||||
:color="calibrationModeButton.color"
|
||||
:disabled="checkResolution"
|
||||
@click="sendCalibrationMode"
|
||||
>
|
||||
{{ calibrationModeButton.text }}
|
||||
</v-btn>
|
||||
</v-col>
|
||||
<v-col>
|
||||
<v-btn
|
||||
small
|
||||
:color="cancellationModeButton.color"
|
||||
:disabled="checkCancellation"
|
||||
@click="sendCalibrationFinish"
|
||||
>
|
||||
{{ cancellationModeButton.text }}
|
||||
</v-btn>
|
||||
</v-col>
|
||||
<v-col>
|
||||
<v-btn
|
||||
color="whitesmoke"
|
||||
small
|
||||
@click="downloadBoard"
|
||||
>
|
||||
Download Checkerboard
|
||||
</v-btn>
|
||||
<a
|
||||
ref="calibrationFile"
|
||||
style="color: black; text-decoration: none; display: none"
|
||||
:href="require('../../assets/chessboard.png')"
|
||||
download="Calibration Board.png"
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row v-if="isCalibrating">
|
||||
<v-col>
|
||||
<span>Snapshot Amount: {{ snapshotAmount }}</span>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<div v-if="isCalibrating">
|
||||
<v-checkbox
|
||||
v-model="isAdvanced"
|
||||
label="Advanced Menu"
|
||||
dark
|
||||
/>
|
||||
<div v-if="isAdvanced">
|
||||
<CVslider
|
||||
v-model="$store.getters.pipeline.exposure"
|
||||
name="Exposure"
|
||||
:min="0"
|
||||
:max="100"
|
||||
@input="e=> handleInput('exposure', e)"
|
||||
/>
|
||||
<CVslider
|
||||
v-model="$store.getters.pipeline.brightness"
|
||||
name="Brightness"
|
||||
:min="0"
|
||||
:max="100"
|
||||
@input="e=> handleInput('brightness', e)"
|
||||
/>
|
||||
<CVslider
|
||||
v-if="$store.getters.pipeline.gain !== -1"
|
||||
v-model="$store.getters.pipeline.gain"
|
||||
name="Gain"
|
||||
:min="0"
|
||||
:max="100"
|
||||
@input="e=> handleInput('gain', e)"
|
||||
/>
|
||||
<CVselect
|
||||
v-model="$store.getters.pipeline.videoModeIndex"
|
||||
name="FPS"
|
||||
:list="stringFpsList"
|
||||
@input="changeFps"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<v-snackbar
|
||||
v-model="snack"
|
||||
top
|
||||
:color="snackbar.color"
|
||||
>
|
||||
<span>{{ snackbar.text }}</span>
|
||||
</v-snackbar>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import CVselect from '../../components/common/cv-select'
|
||||
import CVnumberinput from '../../components/common/cv-number-input'
|
||||
import CVslider from '../../components/common/cv-slider'
|
||||
|
||||
export default {
|
||||
name: 'CameraSettings',
|
||||
components: {
|
||||
CVselect,
|
||||
CVnumberinput,
|
||||
CVslider
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
isCalibrating: false,
|
||||
resolutionIndex: undefined,
|
||||
calibrationModeButton: {
|
||||
text: "Start Calibration",
|
||||
color: "green"
|
||||
},
|
||||
cancellationModeButton: {
|
||||
text: "Cancel Calibration",
|
||||
color: "red"
|
||||
},
|
||||
snackbar: {
|
||||
color: "success",
|
||||
text: ""
|
||||
},
|
||||
squareSize: 1.0,
|
||||
snapshotAmount: 0,
|
||||
hasEnough: false,
|
||||
snack: false,
|
||||
isAdvanced: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
checkResolution() {
|
||||
return this.resolutionIndex === undefined;
|
||||
},
|
||||
checkCancellation() {
|
||||
if (this.isCalibrating) {
|
||||
return false
|
||||
} else if (this.checkResolution) {
|
||||
return true;
|
||||
} else {
|
||||
return true
|
||||
}
|
||||
},
|
||||
currentCameraIndex: {
|
||||
get() {
|
||||
return this.$store.state.currentCameraIndex;
|
||||
},
|
||||
set(value) {
|
||||
this.$store.commit('currentCameraIndex', value);
|
||||
}
|
||||
},
|
||||
filteredResolutionList: {
|
||||
get() {
|
||||
let tmp_list = [];
|
||||
for (let i in this.$store.state.resolutionList) {
|
||||
if (this.$store.state.resolutionList.hasOwnProperty(i)) {
|
||||
let res = JSON.parse(JSON.stringify(this.$store.state.resolutionList[i]));
|
||||
if (!tmp_list.some(e => e.width === res.width && e.height === res.height)) {
|
||||
res['actualIndex'] = parseInt(i);
|
||||
tmp_list.push(res);
|
||||
}
|
||||
}
|
||||
}
|
||||
return tmp_list;
|
||||
}
|
||||
},
|
||||
filteredFpsList() {
|
||||
let selectedRes = this.$store.state.resolutionList[this.resolutionIndex];
|
||||
let tmpList = [];
|
||||
for (let i in this.$store.state.resolutionList) {
|
||||
if (this.$store.state.resolutionList.hasOwnProperty(i)) {
|
||||
let res = JSON.parse(JSON.stringify(this.$store.state.resolutionList[i]));
|
||||
if (!tmpList.some(e => e['fps'] === res['fps'])) {
|
||||
if (res.width === selectedRes.width && res.height === selectedRes.height) {
|
||||
res['actualIndex'] = parseInt(i);
|
||||
tmpList.push(res);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return tmpList;
|
||||
},
|
||||
stringFpsList() {
|
||||
let tmp = [];
|
||||
for (let i of this.filteredFpsList) {
|
||||
tmp.push(i['fps']);
|
||||
}
|
||||
return tmp;
|
||||
},
|
||||
stringResolutionList: {
|
||||
get() {
|
||||
let tmp = [];
|
||||
for (let i of this.filteredResolutionList) {
|
||||
tmp.push(`${i['width']} X ${i['height']}`)
|
||||
}
|
||||
return tmp
|
||||
}
|
||||
},
|
||||
cameraSettings: {
|
||||
get() {
|
||||
return this.$store.getters.cameraSettings;
|
||||
},
|
||||
set(value) {
|
||||
this.$store.commit('cameraSettings', value);
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
downloadBoard() {
|
||||
this.axios.get("http://" + this.$address + require('../../assets/chessboard.png'), {responseType: 'blob'}).then((response) => {
|
||||
require('downloadjs')(response.data, "Calibration Board", "image/png")
|
||||
})
|
||||
},
|
||||
changeFps() {
|
||||
this.handleInput('videoModeIndex', this.filteredFpsList[this.$store.getters.pipeline['videoModeIndex']]['actualIndex']);
|
||||
},
|
||||
sendCameraSettings() {
|
||||
const self = this;
|
||||
this.axios.post("http://" + this.$address + "/api/settings/camera", this.cameraSettings).then(
|
||||
function (response) {
|
||||
if (response.status === 200) {
|
||||
self.$store.state.saveBar = true;
|
||||
}
|
||||
}
|
||||
)
|
||||
},
|
||||
sendCalibrationMode() {
|
||||
const self = this;
|
||||
let data = {};
|
||||
let connection_string = "/api/settings/";
|
||||
if (self.isCalibrating === true) {
|
||||
connection_string += "snapshot"
|
||||
} else {
|
||||
connection_string += "startCalibration";
|
||||
data['resolution'] = this.filteredResolutionList[this.resolutionIndex].actualIndex;
|
||||
data['squareSize'] = this.squareSize;
|
||||
self.hasEnough = false;
|
||||
}
|
||||
this.axios.post("http://" + this.$address + connection_string, data).then(
|
||||
function (response) {
|
||||
if (response.status === 200) {
|
||||
if (self.isCalibrating) {
|
||||
self.snapshotAmount = response.data['snapshotCount'];
|
||||
self.hasEnough = response.data['hasEnough'];
|
||||
if (self.hasEnough === true) {
|
||||
self.cancellationModeButton.text = "Finish Calibration";
|
||||
self.cancellationModeButton.color = "green";
|
||||
}
|
||||
} else {
|
||||
self.calibrationModeButton.text = "Take Snapshot";
|
||||
self.isCalibrating = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
},
|
||||
sendCalibrationFinish() {
|
||||
const self = this;
|
||||
let connection_string = "/api/settings/endCalibration";
|
||||
let data = {};
|
||||
data['squareSize'] = this.squareSize;
|
||||
self.axios.post("http://" + this.$address + connection_string, data).then((response) => {
|
||||
if (response.status === 200) {
|
||||
self.snackbar = {
|
||||
color: "success",
|
||||
text: "calibration successful. \n" +
|
||||
"accuracy: " + response.data['accuracy'].toFixed(5)
|
||||
};
|
||||
self.snack = true;
|
||||
}
|
||||
self.isCalibrating = false;
|
||||
self.hasEnough = false;
|
||||
self.snapshotAmount = 0;
|
||||
self.calibrationModeButton.text = "Start Calibration";
|
||||
self.cancellationModeButton.text = "Cancel Calibration";
|
||||
self.cancellationModeButton.color = "red";
|
||||
}
|
||||
).catch(() => {
|
||||
self.snackbar = {
|
||||
color: "error",
|
||||
text: "calibration failed"
|
||||
};
|
||||
self.snack = true;
|
||||
self.isCalibrating = false;
|
||||
self.hasEnough = false;
|
||||
self.snapshotAmount = 0;
|
||||
self.calibrationModeButton.text = "Start Calibration";
|
||||
self.cancellationModeButton.text = "Cancel Calibration";
|
||||
self.cancellationModeButton.color = "red";
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="" scoped>
|
||||
|
||||
</style>
|
||||
188
photon-client/src/views/SettingsViewes/General.vue
Normal file
188
photon-client/src/views/SettingsViewes/General.vue
Normal file
@@ -0,0 +1,188 @@
|
||||
<template>
|
||||
<div>
|
||||
<div style="margin-top: 15px">
|
||||
<span>General Settings:</span>
|
||||
<v-divider color="white" />
|
||||
</div>
|
||||
<CVnumberinput
|
||||
v-model="settings.teamNumber"
|
||||
name="Team Number"
|
||||
/>
|
||||
<CVradio
|
||||
v-model="settings.connectionType"
|
||||
:list="['DHCP','Static']"
|
||||
/>
|
||||
<v-divider color="white" />
|
||||
<CVinput
|
||||
v-model="settings.ip"
|
||||
name="IP"
|
||||
:disabled="isDisabled"
|
||||
/>
|
||||
<CVinput
|
||||
v-model="settings.netmask"
|
||||
name="NetMask"
|
||||
:disabled="isDisabled"
|
||||
/>
|
||||
<CVinput
|
||||
v-model="settings.gateway"
|
||||
name="Gateway"
|
||||
:disabled="isDisabled"
|
||||
/>
|
||||
<v-divider color="white" />
|
||||
<CVinput
|
||||
v-model="settings.hostname"
|
||||
name="Hostname"
|
||||
/>
|
||||
<v-btn
|
||||
style="margin-top:10px"
|
||||
small
|
||||
color="#4baf62"
|
||||
@click="sendGeneralSettings"
|
||||
>
|
||||
Save General Settings
|
||||
</v-btn>
|
||||
<div style="margin-top: 20px">
|
||||
<span>Install or Update:</span>
|
||||
<v-divider color="white" />
|
||||
</div>
|
||||
<div v-if="!isLoading">
|
||||
<v-row
|
||||
dense
|
||||
align="center"
|
||||
>
|
||||
<v-col :cols="3">
|
||||
<span>Choose a newer version: </span>
|
||||
</v-col>
|
||||
<v-col :cols="6">
|
||||
<v-file-input
|
||||
v-model="file"
|
||||
accept=".jar"
|
||||
dark
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-btn
|
||||
small
|
||||
@click="installOrUpdate"
|
||||
>
|
||||
{{ fileUploadText }}
|
||||
</v-btn>
|
||||
</div>
|
||||
<div
|
||||
v-else
|
||||
style="text-align: center; margin-top: 20px"
|
||||
>
|
||||
<v-progress-circular
|
||||
color="white"
|
||||
:indeterminate="true"
|
||||
size="32"
|
||||
width="4"
|
||||
/>
|
||||
<br>
|
||||
<span>Please wait this may take a while</span>
|
||||
</div>
|
||||
<v-snackbar
|
||||
v-model="snack"
|
||||
top
|
||||
:color="snackbar.color"
|
||||
>
|
||||
<span>{{ snackbar.text }}</span>
|
||||
</v-snackbar>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import CVnumberinput from '../../components/common/cv-number-input'
|
||||
import CVradio from '../../components/common/cv-radio'
|
||||
import CVinput from '../../components/common/cv-input'
|
||||
|
||||
export default {
|
||||
name: 'General',
|
||||
components: {
|
||||
CVnumberinput,
|
||||
CVradio,
|
||||
CVinput
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
file: undefined,
|
||||
snackbar: {
|
||||
color: "success",
|
||||
text: ""
|
||||
},
|
||||
snack: false,
|
||||
isLoading: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
fileUploadText() {
|
||||
if (this.file !== undefined) {
|
||||
return "Update and run at startup"
|
||||
} else {
|
||||
return "Run current version at startup"
|
||||
}
|
||||
},
|
||||
isDisabled() {
|
||||
return this.settings.connectionType === 0;
|
||||
},
|
||||
settings: {
|
||||
get() {
|
||||
return this.$store.state.settings;
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
sendGeneralSettings() {
|
||||
const self = this;
|
||||
this.axios.post("http://" + this.$address + "/api/settings/general", this.settings).then(
|
||||
function (response) {
|
||||
if (response.status === 200) {
|
||||
self.snackbar = {
|
||||
color: "success",
|
||||
text: "Save successful, Please restart for changes to take action"
|
||||
};
|
||||
self.snack = true;
|
||||
}
|
||||
},
|
||||
function (error) {
|
||||
self.snackbar = {
|
||||
color: "error",
|
||||
text: error.response.data
|
||||
};
|
||||
self.snack = true;
|
||||
}
|
||||
)
|
||||
},
|
||||
installOrUpdate() {
|
||||
let formData = new FormData();
|
||||
formData.append('file', this.file);
|
||||
if (this.file !== undefined) {
|
||||
this.isLoading = true;
|
||||
}
|
||||
this.axios.post("http://" + this.$address + "/api/install", formData, {
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data'
|
||||
}
|
||||
}).then(() => {
|
||||
this.snackbar = {
|
||||
color: "success",
|
||||
text: "Installation successful"
|
||||
};
|
||||
this.isLoading = false;
|
||||
this.snack = true;
|
||||
}).catch(error => {
|
||||
this.snackbar = {
|
||||
color: "error",
|
||||
text: error.response.data
|
||||
};
|
||||
this.isLoading = false;
|
||||
this.snack = true;
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="" scoped>
|
||||
|
||||
</style>
|
||||
Reference in New Issue
Block a user