Add solvePNP, 3d tab on the UI, and some other misc bug fixes (#35)

* Rebase solvePNP on master

* added 3D tab minimap and csv reader

* More solvePNP

* Create draw pipe for pnp data

* SolvePNP piping work

* Move sorting into solvepnppipe

* Create calibration pipeline

* Update CalibrateSolvePNPPipeline.java

* add camera tilt angle

* Add calibration slider and snapshot button to 3D view

* Mirror updates in the socket handler

* add 3d calibration mode to the pipeline manager

* created calibration functions in ui and backend

* Start plumbing calibration

* Add snapshot and other handling to the RequestHandler

* added select resolution before starting calibration

* Rename solvePNPPipe to bounding box solve pnp pipe

* Update BoundingBoxSolvePNPPipe.java

* Add Mat serializer and CameraCalibrationConfig

* Begun calibration saving, fixed UI/Backend snapshot count mismatch

* Add (unplumbed) option to set checkerboard size

This will allow users to change the units their calibration is in

* Create chessboard.png

* Fix calibration NPE

* changed string serialization to a json send

* bug fixed cancellation button

* Fix spelling of snapshot in 3d.vue

* Plumb resolution change

* Set resolution during config, start on config serialization

* Update .gitignore

* Config fixes

* Start transition away from cvpipeline3d

* fix NPE on uncalibrated cameras

* clear list on fail

* Fix video mode index error

* ignore getters in camera calibration config

* Create json constructor for jsonmat

* get solvePNP mostly returning sane values

* Fix solvePNP bug and add unit test

* FIx calibration mat truncation

* added capture amount model upload and minimap data

* Standardize on meters in calibration and bounding box

* fix json out of bounds and handle null calibration more gracefully

* don't put text on calibrate image, go back to inches

* convert distance to meters

this means calibration will need to be in inches

* Actually save raw contor

* Update GroupContoursPipe.java

* Add all calibration return to camera capture

* hard code 2019 target

* bugfixed draw2d added fail calib popup, merge end and cancel

added the res index to the calib start

* Clarify error message and draw more fancy rectangles

* Cleanup memory in solvepnp

* re did minimap component

* fix npe if left/right is null

* remove references to 2d

* try-catch running the current pipeline

* Add method to find corners using the harris corner detector

* Possibly fix left/right missmatch

* Fix 3D Tab error

* FIx file permissions, mat serializer adjustments

* fixed mini map for field coordinates

* mini map changes fov

* Update SolvePNPPipe.java

* get rid of target corners

* some memory leak fixes

* fixed mini map location

* added position under minimap

* changed player fov look

* put all targets in the web send

* re did target send to ui added target tables, bugfix calibration

* fixed y position

* Add tilt angle to capture properties

* maybe fix y axis in minimap

* Add square size to onCalibrationEnding

* Possibly add square size to UI

* fix NPE with pitch

* Fix bug with sending multiple targets

* Only instantiate 3d stuff if we are in 3d mode

* Fix array list exceptions

* Fix bug in sort contors

list was truncated too early

* added download chess, tilt setting and ordinal tilt,

* added square size connection

* removed unused code

* Update pom version to 2.1-RELEASE

* Send camera calibrations to UI

* Stream pose list to a LIst

* Only stream necessary parts of the aux list entry

* Make broadcastMessage synchronized to prevent ConcurrentModificationExceptions

* added fps counter changed squaresize steps bug fixes in tables

* bugfix camera settings cam wont change

Authored-by: oriagranat9 <oriagranat9@gmail.com>
This commit is contained in:
Matt
2019-12-31 04:53:20 -08:00
committed by oriagranat9
parent d8c027dae7
commit 1decd2c3d7
70 changed files with 3029 additions and 297 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

View File

@@ -0,0 +1,140 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
version="1.1"
id="svg3713"
width="219"
height="230"
viewBox="0 0 219 230"
sodipodi:docname="robotIcon.svg"
inkscape:version="0.92.4 (5da689c313, 2019-01-14)">
<metadata
id="metadata3719">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title />
</cc:Work>
</rdf:RDF>
</metadata>
<defs
id="defs3717">
<linearGradient
id="linearGradient937"
inkscape:collect="always">
<stop
id="stop933"
offset="0"
style="stop-color:#228a42;stop-opacity:1" />
<stop
id="stop935"
offset="1"
style="stop-color:#5cb34a;stop-opacity:1" />
</linearGradient>
<linearGradient
id="linearGradient931"
inkscape:collect="always">
<stop
id="stop927"
offset="0"
style="stop-color:#228a42;stop-opacity:1" />
<stop
id="stop929"
offset="1"
style="stop-color:#5cb34a;stop-opacity:1" />
</linearGradient>
<linearGradient
inkscape:collect="always"
id="linearGradient925">
<stop
style="stop-color:#228a42;stop-opacity:1"
offset="0"
id="stop921" />
<stop
style="stop-color:#5cb34a;stop-opacity:1"
offset="1"
id="stop923" />
</linearGradient>
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient925"
id="linearGradient4820"
x1="108.25"
y1="186.25"
x2="109.5"
y2="21"
gradientUnits="userSpaceOnUse" />
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient937"
id="linearGradient4822"
x1="108.25"
y1="186.25"
x2="109.5"
y2="21"
gradientUnits="userSpaceOnUse" />
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient931"
id="linearGradient4824"
x1="108.25"
y1="186.25"
x2="109.5"
y2="21"
gradientUnits="userSpaceOnUse" />
</defs>
<sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1920"
inkscape:window-height="1017"
id="namedview3715"
showgrid="false"
inkscape:zoom="4"
inkscape:cx="110.62282"
inkscape:cy="130.60249"
inkscape:window-x="-8"
inkscape:window-y="-8"
inkscape:window-maximized="1"
inkscape:current-layer="svg3713"
showguides="false"
inkscape:snap-page="false"
inkscape:snap-others="true"
inkscape:snap-object-midpoints="true" />
<path
style="opacity:1;fill:url(#linearGradient4824);fill-opacity:1;stroke:#000000;stroke-width:0.49889764;stroke-linecap:butt;stroke-linejoin:round;stroke-opacity:1;stroke-miterlimit:4;stroke-dasharray:none"
d="m 167,156.45722 -41,-2 -14,-17 V 55.457215 Z"
id="path3725-6"
inkscape:connector-curvature="0"
sodipodi:nodetypes="ccccc" />
<path
style="fill:url(#linearGradient4822);stroke:#020000;stroke-width:0.49889764;stroke-linecap:butt;stroke-linejoin:round;stroke-opacity:1;fill-opacity:1;opacity:1;stroke-miterlimit:4;stroke-dasharray:none"
d="m 76.866,158.246 13,-0.496 19.567,11.61661 19.567,-11.39151 13,0.2709 -32.5675,19.39302 z"
id="path3742"
inkscape:connector-curvature="0"
sodipodi:nodetypes="ccccccc" />
<path
style="fill:url(#linearGradient4820);stroke:#020000;stroke-width:0.49889764;stroke-linecap:butt;stroke-linejoin:round;stroke-opacity:1;fill-opacity:1;opacity:1;stroke-miterlimit:4;stroke-dasharray:none"
d="m 51.864904,156.45722 41,-2 13.999996,-17 V 55.457209 Z"
id="path3725-6-5"
inkscape:connector-curvature="0"
sodipodi:nodetypes="ccccc" />
</svg>

After

Width:  |  Height:  |  Size: 4.4 KiB

View File

@@ -0,0 +1,165 @@
<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" justify="middle">
<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
}
},
watch: {
targets: {
deep: true,
handler() {
this.draw();
}
},
horizontalFOV() {
this.draw();
}
},
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);
// 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
}
},
computed: {
hLen: {
get() {
return Math.tan(this.horizontalFOV / 2 * Math.PI / 180) * 150;
}
}
},
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();
});
}
}
</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

@@ -6,7 +6,7 @@
</v-col>
<v-col>
<v-text-field dark v-model="localValue" class="mt-0 pt-0" hide-details single-line type="number"
style="width: 70px"/>
style="width: 70px" :step="step"/>
</v-col>
</v-row>
</div>
@@ -15,7 +15,7 @@
<script>
export default {
name: 'NumberInput',
props: ['name', 'value'],
props: ['name', 'value', 'step'],
data() {
return {}
},

View File

@@ -37,7 +37,8 @@ export default new Vuex.Store({
isBinary: 0,
calibrationMode: 0,
videoModeIndex:0,
streamDivisor:0
streamDivisor:0,
is3D:false
},
cameraSettings: {},
resolutionList: [],

View File

@@ -75,6 +75,7 @@
<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>
<div style="padding-left:30px">
@@ -96,10 +97,40 @@
<div v-else style="height: 58px"></div>
<!-- camera image stream -->
<div class="videoClass">
<img id="CameraStream" v-if="cameraList.length > 0" :src="streamAddress" @click="onImageClick"
crossorigin="Anonymous"/>
<span v-else>No Cameras Are connected</span>
<h5 id="Point">{{point}}</h5>
<v-row align="center">
<img id="CameraStream" style="display: block;margin: auto; width: 70%;height: 70%;"
v-if="cameraList.length > 0"
:src="streamAddress" @click="onImageClick"
crossorigin="Anonymous"/>
<span v-else>No Cameras Are connected</span>
</v-row>
<v-row justify="end">
<span style="margin-right: 45px">FPS:{{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 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>
@@ -154,6 +185,7 @@
import ThresholdTab from './CameraViewes/ThresholdTab'
import ContoursTab from './CameraViewes/ContoursTab'
import OutputTab from './CameraViewes/OutputTab'
import pnpTab from './CameraViewes/3D'
import CVselect from '../components/cv-select'
import CVicon from '../components/cv-icon'
import CVinput from '../components/cv-input'
@@ -165,6 +197,7 @@
ThresholdTab,
ContoursTab,
OutputTab,
pnpTab,
CVselect,
CVicon,
CVinput
@@ -319,19 +352,21 @@
return "ContoursTab";
case 3:
return "OutputTab";
case 4:
return "pnpTab";
}
return "";
}
},
point: {
targets: {
get: function () {
let p = this.$store.state.point.calculated;
let fps = this.$store.state.point.fps;
if (p !== undefined) {
return `Pitch: ${parseFloat(p['pitch']).toFixed(2)}, Yaw: ${parseFloat(p['yaw']).toFixed(2)}, Area: ${parseFloat(p['area']).toFixed(2)}, FPS: ${parseFloat(fps).toFixed(2)}`
} else {
return undefined;
}
return this.$store.state.point.targets;
}
},
fps: {
get() {
return this.$store.state.point.fps;
}
},
currentCameraIndex: {
@@ -384,17 +419,16 @@
text-align: center;
}
.videoClass img {
max-height: 70vh;
max-width: 70%;
.tableClass {
padding-top: 5px;
width: 70%;
object-fit: cover;
vertical-align: middle;
text-align: center;
}
#Point {
padding-top: 5px;
th {
width: 80px;
text-align: center;
color: #f4f4f4;
}
</style>

View File

@@ -0,0 +1,86 @@
<template>
<div>
<v-row align="center" justify="start" dense>
<v-col :cols="6">
<CVswitch v-model="value.is3D" name="Enable 3D" @input="handleData('is3D')"/>
</v-col>
<v-col>
<CVnumberInput name="Max Height:" v-model="value.targetHeight" @input="handleData('targetHeight')"/>
<CVnumberInput name="Max Width:" v-model="value.targetWidth" @input="handleData('targetWidth')"/>
</v-col>
<!-- <v-col>-->
<!-- <input type="file" ref="file" style="display: none" accept=".csv" @change="readFile">-->
<!-- <v-btn @click="$refs.file.click()" small>-->
<!-- upload model-->
<!-- </v-btn>-->
<!-- </v-col>-->
</v-row>
<mini-map class="miniMapClass" :targets="targets" :horizontal-f-o-v="horizontalFOV"/>
</div>
</template>
<script>
import miniMap from '../../components/3D/MiniMap';
import CVswitch from '../../components/cv-switch';
import CVnumberInput from '../../components/cv-number-input';
import Papa from 'papaparse';
export default {
name: "solvePNP",
props: ['value'],
components: {
CVswitch,
CVnumberInput,
miniMap
},
data() {
return {
is3D: false,
isPNPCalibration: false,
}
},
methods: {
handleData(val) {
this.handleInput(val, this.value[val]);
this.$emit('update')
},
readFile(event) {
let file = event.target.files[0];
Papa.parse(file, {
complete: this.onParse,
skipEmptyLines: true
});
},
onParse(result) {
// console.log(result.data);
this.axios.post("http://" + this.$address + "/api/vision/pnpModel", result.data);
},
},
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)
}
}
}
}
</script>
<style scoped>
.miniMapClass {
width: 50% !important;
height: 50% !important;
}
</style>

View File

@@ -3,7 +3,7 @@
<CVselect name="SortMode" v-model="value.sortMode"
:list="['Largest','Smallest','Highest','Lowest','Rightmost','Leftmost','Centermost']"
@input="handleData('sortMode')"/>
<CVswitch name="Output multiple" v-model="value.multiple" @input="handleData('multiple')"></CVswitch>
<CVswitch name="Output multiple" v-model="value.multiple" @input="handleData('multiple')"/>
<span>Calibrate:</span>
<v-divider dark color="white"/>
<CVselect name="Calibration Mode" v-model="value.calibrationMode" :list="['None','Single point','Dual point']"

View File

@@ -1,8 +1,54 @@
<template>
<div>
<CVselect name="Camera" :list="cameraList" v-model="currentCameraIndex"/>
<CVnumberinput name="Diagonal FOV" v-model="cameraSettings.fov"/>
<v-btn style="margin-top:10px" small color="#4baf62" @click="sendCameraSettings">Save Camera Settings</v-btn>
<div>
<CVselect name="Camera" :list="cameraList" v-model="currentCameraIndex" @input="handleInput('currentCamera',currentCameraIndex)"/>
<CVnumberinput name="Diagonal FOV" v-model="cameraSettings.fov"/>
<br>
<CVnumberinput name="Camera pitch" v-model="cameraSettings.tilt" :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 name="Resolution" v-model="resolutionIndex" :list="resolutionList"/>
</v-col>
<v-col>
<CVnumberinput name="Square Size (mm)" v-model="squareSize"/>
</v-col>
</v-row>
<v-row>
<v-col>
<v-btn small :color="calibrationModeButton.color" @click="sendCalibrationMode"
:disabled="checkResolution">
{{calibrationModeButton.text}}
</v-btn>
</v-col>
<v-col>
<v-btn small :color="cancellationModeButton.color" @click="sendCalibrationFinish"
:disabled="checkCancelation">
{{cancellationModeButton.text}}
</v-btn>
</v-col>
<v-col>
<v-btn color="whitesmoke" small><a style="color: black; text-decoration: none"
:href="require('../../assets/chessboard.png')"
download="Calibration Board.png">Download Checkerboard</a>
</v-btn>
</v-col>
</v-row>
<v-row v-if="isCalibrating">
<v-col>
<span>Snapshot Amount: {{snapshotAmount}}</span>
</v-col>
</v-row>
</div>
<v-snackbar v-model="snack">
<span>Calibration Failed</span>
</v-snackbar>
</div>
</template>
@@ -17,7 +63,22 @@
CVnumberinput
},
data() {
return {}
return {
isCalibrating: false,
resolutionIndex: undefined,
calibrationModeButton: {
text: "Start Calibration",
color: "green"
},
cancellationModeButton: {
text: "Cancel Calibration",
color: "red"
},
squareSize: 2.54,
snapshotAmount: 0,
hasEnough: false,
snack: false
}
},
methods: {
sendCameraSettings() {
@@ -30,9 +91,69 @@
}
)
},
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.resolutionIndex;
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(
function (response) {
if (response.status === 500) {
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";
}
);
}
},
computed: {
checkResolution() {
return this.resolutionIndex === undefined;
},
checkCancelation() {
if (this.isCalibrating) {
return false
} else if (this.checkResolution) {
return true;
} else {
return true
}
},
currentCameraIndex: {
get() {
return this.$store.state.currentCameraIndex;
@@ -49,6 +170,15 @@
this.$store.commit('cameraList', value);
}
},
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;
}
},
cameraSettings: {
get() {
return this.$store.state.cameraSettings;