mirror of
https://github.com/PhotonVision/photonvision
synced 2026-06-22 01:11:40 +00:00
Currently, there is a difficult-to-reproduce bug where the backend reports that camera calibration was successful in logs via the logger but then throws an exception causing the backend to return a 500 error code with no request body which causes the frontend to interpret this as a failed calibration attempt. This ultimately leads to the entire instance of photonvision crashing and requiring the entire pi to be restarted. It is believed this issue resides inside the ConfigManager's saving action following the calibration update but is not confirmed.
830 lines
30 KiB
Vue
830 lines
30 KiB
Vue
<template>
|
|
<div>
|
|
<v-row
|
|
no-gutters
|
|
class="pa-3"
|
|
>
|
|
<v-col
|
|
cols="12"
|
|
md="7"
|
|
>
|
|
<!-- Camera card -->
|
|
<v-card
|
|
class="mb-3 pr-6 pb-3"
|
|
color="primary"
|
|
dark
|
|
>
|
|
<v-card-title>Camera Settings</v-card-title>
|
|
<div class="ml-5">
|
|
<CVselect
|
|
v-model="currentCameraIndex"
|
|
name="Camera"
|
|
:list="$store.getters.cameraList"
|
|
:select-cols="$vuetify.breakpoint.mdAndUp ? 10 : 7"
|
|
@input="handleInput('currentCamera',currentCameraIndex)"
|
|
/>
|
|
<CVnumberinput
|
|
v-model="cameraSettings.fov"
|
|
:tooltip="cameraSettings.isFovConfigurable ? 'Field of view (in degrees) of the camera measured across the diagonal of the frame, in a video mode which covers the whole sensor area.' : 'This setting is managed by a vendor'"
|
|
name="Maximum Diagonal FOV"
|
|
:disabled="!cameraSettings.isFovConfigurable"
|
|
:label-cols="$vuetify.breakpoint.mdAndUp ? undefined : 7"
|
|
/>
|
|
<br>
|
|
<v-btn
|
|
style="margin-top:10px"
|
|
small
|
|
color="secondary"
|
|
@click="sendCameraSettings"
|
|
>
|
|
<v-icon left>
|
|
mdi-content-save
|
|
</v-icon>
|
|
Save Camera Settings
|
|
</v-btn>
|
|
</div>
|
|
</v-card>
|
|
|
|
<!-- Calibration card -->
|
|
<v-card
|
|
class="pr-6 pb-3"
|
|
color="primary"
|
|
dark
|
|
>
|
|
<v-card-title>Camera Calibration</v-card-title>
|
|
|
|
<div class="ml-5">
|
|
<v-row>
|
|
<!-- Calibration input -->
|
|
<v-col
|
|
cols="12"
|
|
md="6"
|
|
>
|
|
<v-form
|
|
ref="form"
|
|
v-model="settingsValid"
|
|
>
|
|
<CVselect
|
|
v-model="selectedFilteredResIndex"
|
|
name="Resolution"
|
|
select-cols="7"
|
|
:list="stringResolutionList"
|
|
:disabled="isCalibrating"
|
|
tooltip="Resolution to calibrate at (you will have to calibrate every resolution you use 3D mode on)"
|
|
/>
|
|
<CVselect
|
|
v-model="streamingFrameDivisor"
|
|
name="Decimation"
|
|
tooltip="Resolution to which camera frames are downscaled for detection. Calibration still uses full-res"
|
|
:list="calibrationDivisors"
|
|
select-cols="7"
|
|
@rollback="e => rollback('streamingFrameDivisor', e)"
|
|
/>
|
|
<CVselect
|
|
v-model="boardType"
|
|
name="Board Type"
|
|
select-cols="7"
|
|
:list="['Chessboard', 'Dot Grid']"
|
|
:disabled="isCalibrating"
|
|
tooltip="Calibration board pattern to use"
|
|
/>
|
|
<CVnumberinput
|
|
v-model="squareSizeIn"
|
|
name="Pattern Spacing (in)"
|
|
tooltip="Spacing between pattern features in inches"
|
|
:disabled="isCalibrating"
|
|
:rules="[v => (v > 0) || 'Size must be positive']"
|
|
:label-cols="$vuetify.breakpoint.mdAndUp ? 5 : 7"
|
|
/>
|
|
<CVnumberinput
|
|
v-model="boardWidth"
|
|
name="Board Width"
|
|
tooltip="Width of the board in dots or chessboard squares"
|
|
:disabled="isCalibrating"
|
|
:rules="[v => (v >= 4) || 'Width must be at least 4']"
|
|
:label-cols="$vuetify.breakpoint.mdAndUp ? 5 : 7"
|
|
/>
|
|
<CVnumberinput
|
|
v-model="boardHeight"
|
|
name="Board Height"
|
|
tooltip="Height of the board in dots or chessboard squares"
|
|
:disabled="isCalibrating"
|
|
:rules="[v => (v >= 4) || 'Height must be at least 4']"
|
|
:label-cols="$vuetify.breakpoint.mdAndUp ? 5 : 7"
|
|
/>
|
|
</v-form>
|
|
</v-col>
|
|
|
|
<!-- Calibrated table -->
|
|
<v-col
|
|
cols="12"
|
|
md="6"
|
|
>
|
|
<v-row
|
|
align="start"
|
|
class="pb-4"
|
|
>
|
|
<v-simple-table
|
|
fixed-header
|
|
height="100%"
|
|
dense
|
|
>
|
|
<thead style="font-size: 1.25rem;">
|
|
<tr>
|
|
<th class="text-center">
|
|
<tooltipped-label text="Resolution" />
|
|
</th>
|
|
<th class="text-center">
|
|
<tooltipped-label
|
|
tooltip="Average reprojection error of the calibration, in pixels"
|
|
text="Mean Error"
|
|
/>
|
|
</th>
|
|
<th class="text-center">
|
|
<tooltipped-label
|
|
tooltip="Standard deviation of the mean error, in pixels"
|
|
text="Standard Deviation"
|
|
/>
|
|
</th>
|
|
<th class="text-center">
|
|
<tooltipped-label
|
|
tooltip="Estimated Horizontal FOV, in degrees"
|
|
text="Horizontal FOV"
|
|
/>
|
|
</th>
|
|
<th class="text-center">
|
|
<tooltipped-label
|
|
tooltip="Estimated Vertical FOV, in degrees"
|
|
text="Vertical FOV"
|
|
/>
|
|
</th>
|
|
<th class="text-center">
|
|
<tooltipped-label
|
|
tooltip="Estimated Diagonal FOV, in degrees"
|
|
text="Diagonal FOV"
|
|
/>
|
|
</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<tr
|
|
v-for="(value, index) in filteredResolutionList"
|
|
:key="index"
|
|
>
|
|
<td> {{ value.width }} X {{ value.height }}</td>
|
|
<td>
|
|
{{ isCalibrated(value) ? value.mean.toFixed(2) + "px" : "—" }}
|
|
</td>
|
|
<td> {{ isCalibrated(value) ? value.standardDeviation.toFixed(2) + "px" : "—" }} </td>
|
|
<td> {{ isCalibrated(value) ? value.horizontalFOV.toFixed(2) + "°" : "—" }} </td>
|
|
<td> {{ isCalibrated(value) ? value.verticalFOV.toFixed(2) + "°" : "—" }} </td>
|
|
<td> {{ isCalibrated(value) ? value.diagonalFOV.toFixed(2) + "°" : "—" }} </td>
|
|
</tr>
|
|
</tbody>
|
|
</v-simple-table>
|
|
</v-row>
|
|
<v-row justify="center">
|
|
<v-chip
|
|
v-show="isCalibrating"
|
|
label
|
|
:color="snapshotAmount < 25 ? 'grey' : 'secondary'"
|
|
>
|
|
Snapshots: {{ snapshotAmount }} of at least {{ minSnapshots }}
|
|
</v-chip>
|
|
</v-row>
|
|
</v-col>
|
|
</v-row>
|
|
|
|
<v-row v-if="isCalibrating">
|
|
<v-col
|
|
cols="12"
|
|
class="pt-0"
|
|
>
|
|
<CVslider
|
|
v-model="$store.getters.currentPipelineSettings.cameraExposure"
|
|
:disabled="$store.getters.currentPipelineSettings.cameraAutoExposure"
|
|
name="Exposure"
|
|
:min="0"
|
|
:max="100"
|
|
slider-cols="8"
|
|
step="0.1"
|
|
tooltip="Directly controls how much light is allowed to fall onto the sensor, which affects apparent brightness"
|
|
@input="e => handlePipelineUpdate('cameraExposure', e)"
|
|
/>
|
|
<CVslider
|
|
v-model="$store.getters.currentPipelineSettings.cameraBrightness"
|
|
name="Brightness"
|
|
:min="0"
|
|
:max="100"
|
|
slider-cols="8"
|
|
@input="e => handlePipelineUpdate('cameraBrightness', e)"
|
|
/>
|
|
<CVswitch
|
|
v-model="$store.getters.currentPipelineSettings.cameraAutoExposure"
|
|
class="pt-2"
|
|
name="Auto Exposure"
|
|
tooltip="Enables or Disables camera automatic adjustment for current lighting conditions"
|
|
@input="e => handlePipelineUpdate('cameraAutoExposure', e)"
|
|
/>
|
|
<CVslider
|
|
v-if="cameraGain >= 0"
|
|
v-model="cameraGain"
|
|
name="Camera Gain"
|
|
min="0"
|
|
max="100"
|
|
tooltip="Controls camera gain, similar to brightness"
|
|
:slider-cols="largeBox"
|
|
@input="handlePipelineData('cameraGain')"
|
|
@rollback="e => rollback('cameraGain', e)"
|
|
/>
|
|
<CVslider
|
|
v-if="$store.getters.currentPipelineSettings.cameraRedGain !== -1"
|
|
v-model="$store.getters.currentPipelineSettings.cameraRedGain"
|
|
name="Red AWB Gain"
|
|
min="0"
|
|
max="100"
|
|
tooltip="Controls red automatic white balance gain, which affects how the camera captures colors in different conditions"
|
|
:slider-cols="8"
|
|
@input="e => handlePipelineData('cameraRedGain', e)"
|
|
/>
|
|
<CVslider
|
|
v-if="$store.getters.currentPipelineSettings.cameraBlueGain !== -1"
|
|
v-model="$store.getters.currentPipelineSettings.cameraBlueGain"
|
|
name="Blue AWB Gain"
|
|
min="0"
|
|
max="100"
|
|
tooltip="Controls blue automatic white balance gain, which affects how the camera captures colors in different conditions"
|
|
:slider-cols="8"
|
|
@input="e => handlePipelineData('cameraBlueGain', e)"
|
|
/>
|
|
</v-col>
|
|
</v-row>
|
|
|
|
<v-row>
|
|
<v-col align-self="center">
|
|
<v-btn
|
|
small
|
|
color="secondary"
|
|
style="width: 100%;"
|
|
:disabled="disallowCalibration"
|
|
@click="sendCalibrationMode"
|
|
>
|
|
{{ isCalibrating ? "Take Snapshot" : "Start Calibration" }}
|
|
</v-btn>
|
|
</v-col>
|
|
<v-col align-self="center">
|
|
<v-btn
|
|
small
|
|
:color="hasEnough ? 'accent' : 'red'"
|
|
:class="hasEnough ? 'black--text' : 'white---text'"
|
|
style="width: 100%;"
|
|
:disabled="checkCancellation"
|
|
@click="sendCalibrationFinish"
|
|
>
|
|
{{ hasEnough ? "Finish Calibration" : "Cancel Calibration" }}
|
|
</v-btn>
|
|
</v-col>
|
|
<v-col>
|
|
<v-btn
|
|
color="accent"
|
|
small
|
|
outlined
|
|
style="width: 100%;"
|
|
:disabled="!settingsValid"
|
|
@click="downloadBoard"
|
|
>
|
|
<v-icon left>
|
|
mdi-download
|
|
</v-icon>
|
|
Download Calibration Target
|
|
</v-btn>
|
|
</v-col>
|
|
<v-col>
|
|
<v-btn
|
|
color="secondary"
|
|
small
|
|
style="width: 100%;"
|
|
@click="$refs.importCalibrationFromCalibdb.click()"
|
|
>
|
|
<v-icon left>
|
|
mdi-upload
|
|
</v-icon>
|
|
Import From CalibDB
|
|
</v-btn>
|
|
</v-col>
|
|
</v-row>
|
|
</div>
|
|
</v-card>
|
|
</v-col>
|
|
<v-col
|
|
class="pl-md-3 pt-3 pt-md-0"
|
|
cols="12"
|
|
md="5"
|
|
>
|
|
<template>
|
|
<CVimage
|
|
:id="cameras-cal"
|
|
:idx="1"
|
|
:disconnected="!$store.state.backendConnected"
|
|
scale="100"
|
|
style="border-radius: 5px;"
|
|
/>
|
|
<v-dialog
|
|
v-model="snack"
|
|
width="500px"
|
|
:persistent="true"
|
|
>
|
|
<v-card
|
|
color="primary"
|
|
dark
|
|
>
|
|
<v-card-title> Camera Calibration </v-card-title>
|
|
<div
|
|
class="ml-3"
|
|
>
|
|
<v-col align="center">
|
|
<template v-if="calibrationInProgress && !calibrationFailed">
|
|
<v-progress-circular
|
|
indeterminate
|
|
:size="70"
|
|
:width="8"
|
|
color="accent"
|
|
/>
|
|
<v-card-text>Camera is being calibrated. This process may take several minutes...</v-card-text>
|
|
</template>
|
|
<template v-else-if="!calibrationFailed">
|
|
<v-icon
|
|
color="green"
|
|
size="70"
|
|
>
|
|
mdi-check-bold
|
|
</v-icon>
|
|
<v-card-text>Camera has been successfully calibrated at {{ stringResolutionList[selectedFilteredResIndex] }}!</v-card-text>
|
|
</template>
|
|
<template v-else>
|
|
<v-icon
|
|
color="red"
|
|
size="70"
|
|
>
|
|
mdi-close
|
|
</v-icon>
|
|
<v-card-text>Camera calibration failed! Make sure that the photos are taken such that the rainbow grid circles align with the corners of the chessboard, and try again. More information is available in the program logs.</v-card-text>
|
|
</template>
|
|
</v-col>
|
|
</div>
|
|
<v-card-actions>
|
|
<v-spacer />
|
|
<v-btn
|
|
v-if="!calibrationInProgress || calibrationFailed"
|
|
color="white"
|
|
text
|
|
@click="closeDialog"
|
|
>
|
|
OK
|
|
</v-btn>
|
|
</v-card-actions>
|
|
</v-card>
|
|
</v-dialog>
|
|
</template>
|
|
</v-col>
|
|
</v-row>
|
|
|
|
<!-- Special hidden upload input that gets 'clicked' when the user imports calibdb data -->
|
|
<input
|
|
ref="importCalibrationFromCalibdb"
|
|
type="file"
|
|
accept=".json"
|
|
style="display: none;"
|
|
@change="readImportedCalibration"
|
|
>
|
|
|
|
<v-snackbar
|
|
v-model="uploadSnack"
|
|
top
|
|
:color="uploadSnackData.color"
|
|
timeout="-1"
|
|
>
|
|
<span>{{ uploadSnackData.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';
|
|
import CVswitch from '../components/common/cv-switch';
|
|
import CVimage from "../components/common/cv-image";
|
|
import TooltippedLabel from "../components/common/cv-tooltipped-label";
|
|
import jsPDF from "jspdf";
|
|
import "../jsPDFFonts/Prompt-Regular-normal.js";
|
|
|
|
export default {
|
|
name: 'Cameras',
|
|
components: {
|
|
TooltippedLabel,
|
|
CVselect,
|
|
CVnumberinput,
|
|
CVslider,
|
|
CVswitch,
|
|
CVimage
|
|
},
|
|
data() {
|
|
return {
|
|
snack: false,
|
|
calibrationInProgress: false,
|
|
calibrationFailed: false,
|
|
filteredVideomodeIndex: 0,
|
|
settingsValid: true,
|
|
unfilteredStreamDivisors: [1, 2, 4],
|
|
uploadSnackData: {
|
|
color: "success",
|
|
text: "",
|
|
},
|
|
uploadSnack: false,
|
|
}
|
|
},
|
|
computed: {
|
|
disallowCalibration() {
|
|
return !(this.calibrationData.boardType === 0 || this.calibrationData.boardType === 1) || !this.settingsValid;
|
|
},
|
|
checkCancellation() {
|
|
if (this.isCalibrating) {
|
|
return false
|
|
} else if (this.disallowCalibration) {
|
|
return true;
|
|
} else {
|
|
return true
|
|
}
|
|
},
|
|
currentCameraIndex: {
|
|
get() {
|
|
return this.$store.state.currentCameraIndex;
|
|
},
|
|
set(value) {
|
|
this.$store.commit('currentCameraIndex', value);
|
|
}
|
|
},
|
|
|
|
cameraGain: {
|
|
get() {
|
|
return parseInt(this.$store.getters.currentPipelineSettings.cameraGain)
|
|
},
|
|
set(val) {
|
|
this.$store.commit("mutatePipeline", {"cameraGain": parseInt(val)});
|
|
}
|
|
},
|
|
|
|
calibrationDivisors: {
|
|
get() {
|
|
return this.unfilteredStreamDivisors.filter(item => {
|
|
const res = this.stringResolutionList[this.selectedFilteredResIndex].split(" X ").map(it => parseInt(it));
|
|
console.log(res);
|
|
console.log(item);
|
|
// Realistically, we need more than 320x240, but lower than this is
|
|
// basically unusable. For now, don't allow decimations that take us
|
|
// below that
|
|
const ret = ((res[0] / item) >= 300 && (res[1] / item) >= 220) || (item === 1);
|
|
console.log(ret);
|
|
return ret;
|
|
})
|
|
}
|
|
},
|
|
|
|
// Makes sure there's only one entry per resolution
|
|
filteredResolutionList: {
|
|
get() {
|
|
let list = this.$store.getters.videoFormatList;
|
|
let filtered = [];
|
|
list.forEach((it, i) => {
|
|
if (!filtered.some(e => e.width === it.width && e.height === it.height)) {
|
|
it['index'] = i;
|
|
const calib = this.getCalibrationCoeffs(it);
|
|
if (calib != null) {
|
|
it['standardDeviation'] = calib.standardDeviation;
|
|
it['mean'] = calib.perViewErrors.reduce((a, b) => a + b) / calib.perViewErrors.length;
|
|
it['horizontalFOV'] = 2 * Math.atan2(it.width/2,calib.intrinsics[0]) * (180/Math.PI);
|
|
it['verticalFOV'] = 2 * Math.atan2(it.height/2,calib.intrinsics[4]) * (180/Math.PI);
|
|
it['diagonalFOV'] = 2 * Math.atan2(Math.sqrt(it.width**2 + (it.height/(calib.intrinsics[4]/calib.intrinsics[0]))**2)/2,calib.intrinsics[0]) * (180/Math.PI);
|
|
}
|
|
filtered.push(it);
|
|
}
|
|
});
|
|
filtered.sort((a, b) => (b.width + b.height) - (a.width + a.height));
|
|
return filtered
|
|
}
|
|
},
|
|
stringResolutionList: {
|
|
get() {
|
|
return this.filteredResolutionList.map(res => `${res['width']} X ${res['height']}`);
|
|
}
|
|
},
|
|
cameraSettings: {
|
|
get() {
|
|
return this.$store.getters.currentCameraSettings;
|
|
},
|
|
set(value) {
|
|
this.$store.commit('cameraSettings', value);
|
|
}
|
|
},
|
|
|
|
streamingFrameDivisor: {
|
|
get() {
|
|
return this.$store.getters.currentPipelineSettings.streamingFrameDivisor;
|
|
},
|
|
set(val) {
|
|
this.$store.commit("mutatePipeline", {"streamingFrameDivisor": val});
|
|
this.handlePipelineUpdate("streamingFrameDivisor", val);
|
|
}
|
|
},
|
|
|
|
boardType: {
|
|
get() {
|
|
return this.calibrationData.boardType
|
|
},
|
|
set(value) {
|
|
this.$store.commit('mutateCalibrationState', {['boardType']: value});
|
|
}
|
|
},
|
|
snapshotAmount: {
|
|
get() {
|
|
return this.calibrationData.count
|
|
}
|
|
},
|
|
minSnapshots: {
|
|
get() {
|
|
return this.calibrationData.minCount
|
|
}
|
|
},
|
|
hasEnough: {
|
|
get() {
|
|
return this.calibrationData.hasEnough
|
|
}
|
|
},
|
|
boardWidth: {
|
|
get() {
|
|
return this.calibrationData.patternWidth
|
|
},
|
|
set(value) {
|
|
this.$store.commit('mutateCalibrationState', {['patternWidth']: value})
|
|
}
|
|
},
|
|
boardHeight: {
|
|
get() {
|
|
return this.calibrationData.patternHeight
|
|
},
|
|
set(value) {
|
|
this.$store.commit('mutateCalibrationState', {['patternHeight']: value})
|
|
}
|
|
},
|
|
squareSizeIn: {
|
|
get() {
|
|
return this.calibrationData.squareSizeIn
|
|
},
|
|
set(value) {
|
|
this.$store.commit('mutateCalibrationState', {['squareSizeIn']: value})
|
|
}
|
|
},
|
|
calibrationData: {
|
|
get() {
|
|
return this.$store.state.calibrationData
|
|
}
|
|
},
|
|
isCalibrating: {
|
|
get() {
|
|
return this.$store.getters.currentPipelineIndex === -2;
|
|
}
|
|
},
|
|
selectedFilteredResIndex: {
|
|
get() {
|
|
return this.filteredVideomodeIndex
|
|
},
|
|
set(i) {
|
|
console.log(`Setting filtered index to ${i}`);
|
|
this.filteredVideomodeIndex = i;
|
|
this.$store.commit('mutateCalibrationState', {['videoModeIndex']: this.filteredResolutionList[i].index});
|
|
}
|
|
},
|
|
},
|
|
methods: {
|
|
readImportedCalibration(event) {
|
|
// let formData = new FormData();
|
|
// formData.append("zipData", event.target.files[0]);
|
|
const filename = event.target.files[0].name;
|
|
|
|
event.target.files[0].text().then(fileText => {
|
|
const data = {
|
|
"cameraIndex": this.$store.getters.currentCameraIndex,
|
|
"payload": fileText,
|
|
"filename": filename,
|
|
};
|
|
|
|
this.axios
|
|
.post("http://" + this.$address + "/api/calibration/import", data, {
|
|
headers: { "Content-Type": "text/plain" },
|
|
})
|
|
.then(() => {
|
|
this.uploadSnackData = {
|
|
color: "success",
|
|
text:
|
|
"Calibration imported successfully!",
|
|
};
|
|
this.uploadSnack = true;
|
|
})
|
|
.catch((err) => {
|
|
if (err.response) {
|
|
this.uploadSnackData = {
|
|
color: "error",
|
|
text:
|
|
"Error while uploading calibration file! Could not process provided file.",
|
|
};
|
|
} else if (err.request) {
|
|
this.uploadSnackData = {
|
|
color: "error",
|
|
text:
|
|
"Error while uploading calibration file! No respond to upload attempt.",
|
|
};
|
|
} else {
|
|
this.uploadSnackData = {
|
|
color: "error",
|
|
text: "Error while uploading calibration file!",
|
|
};
|
|
}
|
|
this.uploadSnack = true;
|
|
});
|
|
|
|
})
|
|
},
|
|
closeDialog() {
|
|
this.snack = false;
|
|
this.calibrationInProgress = false;
|
|
this.calibrationFailed = false;
|
|
},
|
|
getCalibrationCoeffs(resolution) {
|
|
const calList = this.$store.getters.calibrationList;
|
|
let ret = null;
|
|
calList.forEach(cal => {
|
|
if (cal.width === resolution.width && cal.height === resolution.height) {
|
|
ret = cal
|
|
}
|
|
});
|
|
return ret;
|
|
},
|
|
downloadBoard() {
|
|
const config = {
|
|
type: this.boardType === 0 ? "chessboard" : "dotgrid",
|
|
boardWidthIn: this.boardWidth,
|
|
boardHeightIn: this.boardHeight,
|
|
patternSpacingIn: this.squareSizeIn
|
|
}
|
|
|
|
const doc = new jsPDF({ unit: "in", format: "letter" })
|
|
|
|
doc.setFont("Prompt-Regular")
|
|
doc.setFontSize(12)
|
|
|
|
const paperWidth = 8.5
|
|
const paperHeight = 11.0
|
|
|
|
// Draw the selected pattern to the document
|
|
switch (config.type) {
|
|
case "chessboard":
|
|
// eslint-disable-next-line no-case-declarations
|
|
const chessboardStartX = (paperWidth - config.boardWidthIn * config.patternSpacingIn) / 2
|
|
// eslint-disable-next-line no-case-declarations
|
|
const chessboardStartY = (paperHeight - config.boardWidthIn * config.patternSpacingIn) / 2
|
|
|
|
for (let squareY = 0; squareY < config.boardHeightIn; squareY++) {
|
|
for (let squareX = 0; squareX < config.boardWidthIn; squareX++) {
|
|
const xPos = chessboardStartX + squareX * config.patternSpacingIn
|
|
const yPos = chessboardStartY + squareY * config.patternSpacingIn
|
|
|
|
// Only draw the odd squares to create the chessboard pattern
|
|
if ((xPos + yPos + 0.25) % 2 === 0) {
|
|
doc.rect(xPos, yPos, config.patternSpacingIn, config.patternSpacingIn, "F")
|
|
}
|
|
}
|
|
}
|
|
break
|
|
case "dotgrid":
|
|
// eslint-disable-next-line no-case-declarations
|
|
const dotgridStartX = (paperWidth - (2 * (config.boardWidthIn - 1) + ((config.boardHeightIn - 1) % 2)) * config.patternSpacingIn) / 2.0
|
|
// eslint-disable-next-line no-case-declarations
|
|
const dotgridStartY = (paperHeight - (config.boardHeightIn - config.patternSpacingIn)) / 2
|
|
|
|
for (let squareY = 0; squareY < config.boardHeightIn; squareY++) {
|
|
for (let squareX = 0; squareX < config.boardWidthIn; squareX++) {
|
|
const xPos = dotgridStartX + (2 * squareX + (squareY % 2)) * config.patternSpacingIn
|
|
const yPos = dotgridStartY + squareY * config.patternSpacingIn
|
|
|
|
doc.circle(xPos, yPos, config.patternSpacingIn / 4, "F")
|
|
}
|
|
}
|
|
break
|
|
}
|
|
|
|
// Draw ruler pattern
|
|
const lineStartX = 1.0
|
|
const lineEndX = paperWidth - lineStartX
|
|
const lineY = paperHeight - 1.0
|
|
|
|
doc.setLineWidth(0.01)
|
|
doc.line(lineStartX, lineY, lineEndX, lineY)
|
|
|
|
for (let tickX = lineStartX; tickX <= lineEndX; tickX++) {
|
|
doc.line(tickX, lineY, tickX, lineY + 0.25)
|
|
doc.text(`${tickX - 1}${tickX - 1 === 0 ? " in" : ""}`, tickX + 0.1, lineY + 0.25)
|
|
}
|
|
|
|
// Add branding
|
|
const logoImage = new Image();
|
|
logoImage.src = require('@/assets/logos/logoMono.png');
|
|
doc.addImage(logoImage, 'PNG', 1.0, 0.75, 1.4, 0.5);
|
|
|
|
doc.text(`${config.boardWidthIn} x ${config.boardHeightIn} | ${config.patternSpacingIn}in`, paperWidth - 1, 1.0,
|
|
{
|
|
maxWidth: (paperWidth - 2.0) / 2,
|
|
align: "right",
|
|
}
|
|
)
|
|
doc.save(`calibrationTarget-${config.type}.pdf`)
|
|
},
|
|
sendCameraSettings() {
|
|
this.axios.post("http://" + this.$address + "/api/settings/camera", {
|
|
"settings": this.cameraSettings,
|
|
"index": this.$store.state.currentCameraIndex
|
|
}).then(response => {
|
|
if (response.status === 200) {
|
|
this.$store.state.saveBar = true;
|
|
}
|
|
}
|
|
)
|
|
},
|
|
isCalibrated(resolution) {
|
|
return this.$store.getters.currentCameraSettings.calibrations
|
|
.some(e => e.width === resolution.width && e.height === resolution.height);
|
|
},
|
|
sendCalibrationMode() {
|
|
let data = {
|
|
['cameraIndex']: this.$store.state.currentCameraIndex
|
|
};
|
|
|
|
if (this.isCalibrating) {
|
|
data['takeCalibrationSnapshot'] = true
|
|
} else {
|
|
// This store prevents an edge case of a user not selecting a different resolution, which causes the set logic to not be called
|
|
this.$store.commit('mutateCalibrationState', {['videoModeIndex']: this.filteredResolutionList[this.selectedFilteredResIndex].index});
|
|
const calData = this.calibrationData;
|
|
calData.isCalibrating = true;
|
|
data['startPnpCalibration'] = calData;
|
|
console.log("starting calibration with index " + calData.videoModeIndex);
|
|
}
|
|
this.$store.commit('currentPipelineIndex', -2);
|
|
this.$store.state.websocket.ws.send(this.$msgPack.encode(data));
|
|
},
|
|
sendCalibrationFinish() {
|
|
console.log("finishing calibration for index " + this.$store.getters.currentCameraIndex);
|
|
|
|
this.snack = true;
|
|
this.calibrationInProgress = true;
|
|
|
|
this.axios.post("http://" + this.$address + "/api/settings/endCalibration", {idx: this.$store.getters.currentCameraIndex})
|
|
.then((response) => {
|
|
if (response.status === 200) {
|
|
this.calibrationInProgress = false;
|
|
} else {
|
|
this.calibrationFailed = true;
|
|
}
|
|
}
|
|
).catch(() => {
|
|
this.calibrationFailed = true;
|
|
});
|
|
}
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<style>
|
|
::-webkit-scrollbar{
|
|
height: 0.55em;
|
|
}
|
|
</style>
|
|
|
|
<style scoped>
|
|
.v-data-table {
|
|
text-align: center;
|
|
background-color: transparent !important;
|
|
width: 100%;
|
|
height: 100%;
|
|
overflow-y: auto;
|
|
}
|
|
|
|
.v-data-table th {
|
|
background-color: #006492 !important;
|
|
}
|
|
|
|
.v-data-table th, td {
|
|
font-size: 1rem !important;
|
|
}
|
|
</style>
|