Rename to PhotonVision

This commit is contained in:
Matt
2020-06-27 14:58:03 -07:00
parent b28d0e046e
commit bdbd6b9d18
394 changed files with 1656 additions and 979 deletions

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

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

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

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

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

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

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

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

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

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

View File

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

View File

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

View File

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