mirror of
https://github.com/PhotonVision/photonvision
synced 2026-06-24 01:31:44 +00:00
[PhotonClient] Vite and Typescript complete refactor (#884)
This commit is contained in:
85
photon-client/src/components/dashboard/tabs/AprilTagTab.vue
Normal file
85
photon-client/src/components/dashboard/tabs/AprilTagTab.vue
Normal file
@@ -0,0 +1,85 @@
|
||||
<script setup lang="ts">
|
||||
import { useCameraSettingsStore } from "@/stores/settings/CameraSettingsStore";
|
||||
import { PipelineType } from "@/types/PipelineTypes";
|
||||
import CvSelect from "@/components/common/cv-select.vue";
|
||||
import CvSlider from "@/components/common/cv-slider.vue";
|
||||
import CvSwitch from "@/components/common/cv-switch.vue";
|
||||
import { computed, getCurrentInstance } from "vue";
|
||||
import { useStateStore } from "@/stores/StateStore";
|
||||
|
||||
// TODO fix pipeline typing in order to fix this, the store settings call should be able to infer that only valid pipeline type settings are exposed based on pre-checks for the entire config section
|
||||
// Defer reference to store access method
|
||||
const currentPipelineSettings = useCameraSettingsStore().currentPipelineSettings;
|
||||
|
||||
const interactiveCols = computed(() => (getCurrentInstance()?.proxy.$vuetify.breakpoint.mdAndDown || false) && (!useStateStore().sidebarFolded || useCameraSettingsStore().isDriverMode)) ? 9 : 8;
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="currentPipelineSettings.pipelineType === PipelineType.AprilTag">
|
||||
<cv-select
|
||||
v-model="currentPipelineSettings.tagFamily"
|
||||
label="Target family"
|
||||
:items="['AprilTag Family 36h11', 'AprilTag Family 25h9', 'AprilTag Family 16h5']"
|
||||
:select-cols="interactiveCols"
|
||||
@input="value => useCameraSettingsStore().changeCurrentPipelineSetting({tagFamily: value}, false)"
|
||||
/>
|
||||
<cv-slider
|
||||
v-model="currentPipelineSettings.decimate"
|
||||
class="pt-2"
|
||||
:slider-cols="interactiveCols"
|
||||
label="Decimate"
|
||||
tooltip="Increases FPS at the expense of range by reducing image resolution initially"
|
||||
:min="1"
|
||||
:max="8"
|
||||
@input="value => useCameraSettingsStore().changeCurrentPipelineSetting({decimate: value}, false)"
|
||||
/>
|
||||
<cv-slider
|
||||
v-model="currentPipelineSettings.blur"
|
||||
class="pt-2"
|
||||
:slider-cols="interactiveCols"
|
||||
label="Blur"
|
||||
tooltip="Gaussian blur added to the image, high FPS cost for slightly decreased noise"
|
||||
:min="0"
|
||||
:max="5"
|
||||
:step="0.1"
|
||||
@input="value => useCameraSettingsStore().changeCurrentPipelineSetting({blur: value}, false)"
|
||||
/>
|
||||
<cv-slider
|
||||
v-model="currentPipelineSettings.threads"
|
||||
class="pt-2"
|
||||
:slider-cols="interactiveCols"
|
||||
label="Threads"
|
||||
tooltip="Number of threads spawned by the AprilTag detector"
|
||||
:min="1"
|
||||
:max="8"
|
||||
@input="value => useCameraSettingsStore().changeCurrentPipelineSetting({threads: value}, false)"
|
||||
/>
|
||||
<cv-switch
|
||||
v-model="currentPipelineSettings.refineEdges"
|
||||
class="pt-2"
|
||||
label="Refine Edges"
|
||||
tooltip="Further refines the AprilTag corner position initial estimate, suggested left on"
|
||||
@input="value => useCameraSettingsStore().changeCurrentPipelineSetting({refineEdges: value}, false)"
|
||||
/>
|
||||
<cv-slider
|
||||
v-model="currentPipelineSettings.decisionMargin"
|
||||
class="pt-2 pb-4"
|
||||
:slider-cols="interactiveCols"
|
||||
label="Decision Margin Cutoff"
|
||||
tooltip="Tags with a 'margin' (decoding quality score) less than this wil be rejected. Increase this to reduce the number of false positive detections"
|
||||
:min="0"
|
||||
:max="250"
|
||||
@input="value => useCameraSettingsStore().changeCurrentPipelineSetting({decisionMargin: value}, false)"
|
||||
/>
|
||||
<cv-slider
|
||||
v-model="currentPipelineSettings.numIterations"
|
||||
class="pt-2 pb-4"
|
||||
:slider-cols="interactiveCols"
|
||||
label="Pose Estimation Iterations"
|
||||
tooltip="Number of iterations the pose estimation algorithm will run, 50-100 is a good starting point"
|
||||
:min="0"
|
||||
:max="500"
|
||||
@input="value => useCameraSettingsStore().changeCurrentPipelineSetting({numIterations: value}, false)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
50
photon-client/src/components/dashboard/tabs/ArucoTab.vue
Normal file
50
photon-client/src/components/dashboard/tabs/ArucoTab.vue
Normal file
@@ -0,0 +1,50 @@
|
||||
<script setup lang="ts">
|
||||
import { useCameraSettingsStore } from "@/stores/settings/CameraSettingsStore";
|
||||
import { PipelineType } from "@/types/PipelineTypes";
|
||||
import CvSlider from "@/components/common/cv-slider.vue";
|
||||
import { computed, getCurrentInstance } from "vue";
|
||||
import { useStateStore } from "@/stores/StateStore";
|
||||
|
||||
// TODO fix pipeline typing in order to fix this, the store settings call should be able to infer that only valid pipeline type settings are exposed based on pre-checks for the entire config section
|
||||
// Defer reference to store access method
|
||||
const currentPipelineSettings = useCameraSettingsStore().currentPipelineSettings;
|
||||
|
||||
const interactiveCols = computed(() => (getCurrentInstance()?.proxy.$vuetify.breakpoint.mdAndDown || false) && (!useStateStore().sidebarFolded || useCameraSettingsStore().isDriverMode)) ? 9 : 8;
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="currentPipelineSettings.pipelineType === PipelineType.Aruco">
|
||||
<cv-slider
|
||||
v-model="currentPipelineSettings.decimate"
|
||||
class="pt-2"
|
||||
:slider-cols="interactiveCols"
|
||||
label="Decimate"
|
||||
tooltip="Increases FPS at the expense of range by reducing image resolution initially"
|
||||
:min="1"
|
||||
:max="8"
|
||||
@input="value => useCameraSettingsStore().changeCurrentPipelineSetting({decimate: value}, false)"
|
||||
/>
|
||||
<cv-slider
|
||||
v-model="currentPipelineSettings.numIterations"
|
||||
class="pt-2"
|
||||
:slider-cols="interactiveCols"
|
||||
label="Corner Iterations"
|
||||
tooltip="How many iterations are going to be used in order to refine corners. Higher values are lead to more accuracy at the cost of performance"
|
||||
:min="30"
|
||||
:max="1000"
|
||||
:step="5"
|
||||
@input="value => useCameraSettingsStore().changeCurrentPipelineSetting({numIterations: value}, false)"
|
||||
/>
|
||||
<cv-slider
|
||||
v-model="currentPipelineSettings.cornerAccuracy"
|
||||
class="pt-2"
|
||||
:slider-cols="interactiveCols"
|
||||
label="Corner Accuracy"
|
||||
tooltip="Minimum accuracy for the corners, lower is better but more performance intensive "
|
||||
:min="0.01"
|
||||
:max="100"
|
||||
:step="0.01"
|
||||
@input="value => useCameraSettingsStore().changeCurrentPipelineSetting({cornerAccuracy: value}, false)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
213
photon-client/src/components/dashboard/tabs/ContoursTab.vue
Normal file
213
photon-client/src/components/dashboard/tabs/ContoursTab.vue
Normal file
@@ -0,0 +1,213 @@
|
||||
<script setup lang="ts">
|
||||
import { useCameraSettingsStore } from "@/stores/settings/CameraSettingsStore";
|
||||
import { PipelineType } from "@/types/PipelineTypes";
|
||||
import CvRangeSlider from "@/components/common/cv-range-slider.vue";
|
||||
import CvSelect from "@/components/common/cv-select.vue";
|
||||
import CvSlider from "@/components/common/cv-slider.vue";
|
||||
import { computed, getCurrentInstance } from "vue";
|
||||
import { useStateStore } from "@/stores/StateStore";
|
||||
|
||||
// TODO fix pipeline typing in order to fix this, the store settings call should be able to infer that only valid pipeline type settings are exposed based on pre-checks for the entire config section
|
||||
// Defer reference to store access method
|
||||
const currentPipelineSettings = useCameraSettingsStore().currentPipelineSettings;
|
||||
|
||||
// TODO fix cv-range-slider so that store access doesn't need to be deferred
|
||||
const contourArea = computed<[number, number]>({
|
||||
get: () => Object.values(useCameraSettingsStore().currentPipelineSettings.contourArea) as [number, number],
|
||||
set: v => useCameraSettingsStore().currentPipelineSettings.contourArea = v
|
||||
});
|
||||
const contourRatio = computed<[number, number]>({
|
||||
get: () => Object.values(useCameraSettingsStore().currentPipelineSettings.contourRatio) as [number, number],
|
||||
set: v => useCameraSettingsStore().currentPipelineSettings.contourRatio = v
|
||||
});
|
||||
const contourFullness = computed<[number, number]>({
|
||||
get: () => Object.values(useCameraSettingsStore().currentPipelineSettings.contourFullness) as [number, number],
|
||||
set: v => useCameraSettingsStore().currentPipelineSettings.contourFullness = v
|
||||
});
|
||||
const contourPerimeter = computed<[number, number]>({
|
||||
get: () => currentPipelineSettings.pipelineType === PipelineType.ColoredShape ? Object.values(currentPipelineSettings.contourPerimeter) as [number, number] : [0, 0] as [number, number],
|
||||
set: v => {
|
||||
if(currentPipelineSettings.pipelineType === PipelineType.ColoredShape) {
|
||||
currentPipelineSettings.contourPerimeter = v;
|
||||
}
|
||||
}
|
||||
});
|
||||
const contourRadius = computed<[number, number]>({
|
||||
get: () => currentPipelineSettings.pipelineType === PipelineType.ColoredShape ? Object.values(currentPipelineSettings.contourRadius) as [number, number] : [0, 0] as [number, number],
|
||||
set: v => {
|
||||
if(currentPipelineSettings.pipelineType === PipelineType.ColoredShape) {
|
||||
currentPipelineSettings.contourRadius = v;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const interactiveCols = computed(() => (getCurrentInstance()?.proxy.$vuetify.breakpoint.mdAndDown || false) && (!useStateStore().sidebarFolded || useCameraSettingsStore().isDriverMode)) ? 9 : 8;
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<cv-range-slider
|
||||
v-model="contourArea"
|
||||
label="Area"
|
||||
:min="0"
|
||||
:max="100"
|
||||
:slider-cols="interactiveCols"
|
||||
:step="0.01"
|
||||
@input="value => useCameraSettingsStore().changeCurrentPipelineSetting({contourArea: value}, false)"
|
||||
/>
|
||||
<cv-range-slider
|
||||
v-if="useCameraSettingsStore().currentPipelineType !== PipelineType.ColoredShape"
|
||||
v-model="contourRatio"
|
||||
label="Ratio (W/H)"
|
||||
tooltip="Min and max ratio between the width and height of a contour's bounding rectangle"
|
||||
:min="0"
|
||||
:max="100"
|
||||
:slider-cols="interactiveCols"
|
||||
:step="0.1"
|
||||
@input="value => useCameraSettingsStore().changeCurrentPipelineSetting({contourRatio: value}, false)"
|
||||
/>
|
||||
<cv-select
|
||||
v-model="useCameraSettingsStore().currentPipelineSettings.contourTargetOrientation"
|
||||
label="Target Orientation"
|
||||
tooltip="Used to determine how to calculate target landmarks, as well as aspect ratio"
|
||||
:items="['Portrait', 'Landscape']"
|
||||
:select-cols="interactiveCols"
|
||||
@input="value => useCameraSettingsStore().changeCurrentPipelineSetting({contourTargetOrientation: value}, false)"
|
||||
/>
|
||||
<cv-range-slider
|
||||
v-if="useCameraSettingsStore().currentPipelineType === PipelineType.ColoredShape"
|
||||
v-model="contourFullness"
|
||||
label="Fullness"
|
||||
tooltip="Min and max ratio between a contour's area and its bounding rectangle"
|
||||
:min="0"
|
||||
:max="100"
|
||||
:slider-cols="interactiveCols"
|
||||
@input="value => useCameraSettingsStore().changeCurrentPipelineSetting({contourFullness: value}, false)"
|
||||
/>
|
||||
<cv-range-slider
|
||||
v-if="currentPipelineSettings.pipelineType === PipelineType.ColoredShape"
|
||||
v-model="contourPerimeter"
|
||||
label="Perimeter"
|
||||
tooltip="Min and max perimeter of the shape, in pixels"
|
||||
min="0"
|
||||
max="4000"
|
||||
:slider-cols="interactiveCols"
|
||||
@input="value => useCameraSettingsStore().changeCurrentPipelineSetting({contourPerimeter: value}, false)"
|
||||
/>
|
||||
<cv-slider
|
||||
v-model="useCameraSettingsStore().currentPipelineSettings.contourSpecklePercentage"
|
||||
label="Speckle Rejection"
|
||||
tooltip="Rejects contours whose average area is less than the given percentage of the average area of all the other contours"
|
||||
:min="0"
|
||||
:max="100"
|
||||
:slider-cols="interactiveCols"
|
||||
@input="value => useCameraSettingsStore().changeCurrentPipelineSetting({contourSpecklePercentage: value}, false)"
|
||||
/>
|
||||
<template v-if="currentPipelineSettings.pipelineType === PipelineType.Reflective">
|
||||
<cv-slider
|
||||
v-model="currentPipelineSettings.contourFilterRangeX"
|
||||
label="X Filter Tightness"
|
||||
tooltip="Rejects contours whose center X is further than X standard deviations left/right of the mean X location"
|
||||
:min="0.1"
|
||||
:max="4"
|
||||
:step="0.1"
|
||||
:slider-cols="interactiveCols"
|
||||
@input="value => useCameraSettingsStore().changeCurrentPipelineSetting({contourFilterRangeX: value}, false)"
|
||||
/>
|
||||
<cv-slider
|
||||
v-model="currentPipelineSettings.contourFilterRangeY"
|
||||
label="Y Filter Tightness"
|
||||
tooltip="Rejects contours whose center Y is further than X standard deviations above/below the mean Y location"
|
||||
:min="0.1"
|
||||
:max="4"
|
||||
:step="0.1"
|
||||
:slider-cols="interactiveCols"
|
||||
@input="value => useCameraSettingsStore().changeCurrentPipelineSetting({contourFilterRangeY: value}, false)"
|
||||
/>
|
||||
<cv-select
|
||||
v-model="useCameraSettingsStore().currentPipelineSettings.contourGroupingMode"
|
||||
label="Target Grouping"
|
||||
tooltip="Whether or not every two targets are paired with each other (good for e.g. 2019 targets)"
|
||||
:select-cols="interactiveCols"
|
||||
:items="['Single','Dual','Two or More']"
|
||||
@input="value => useCameraSettingsStore().changeCurrentPipelineSetting({contourGroupingMode: value}, false)"
|
||||
/>
|
||||
<cv-select
|
||||
v-model="useCameraSettingsStore().currentPipelineSettings.contourIntersection"
|
||||
label="Target Intersection"
|
||||
tooltip="If target grouping is in dual mode it will use this dropdown to decide how targets are grouped with adjacent targets"
|
||||
:select-cols="interactiveCols"
|
||||
:items="['None','Up','Down','Left','Right']"
|
||||
:disabled="useCameraSettingsStore().currentPipelineSettings.contourGroupingMode === 0"
|
||||
@input="value => useCameraSettingsStore().changeCurrentPipelineSetting({contourIntersection: value}, false)"
|
||||
/>
|
||||
</template>
|
||||
<template v-else-if="currentPipelineSettings.pipelineType === PipelineType.ColoredShape">
|
||||
<v-divider class="mt-3" />
|
||||
<cv-select
|
||||
v-model="currentPipelineSettings.contourShape"
|
||||
label="Target Shape"
|
||||
tooltip="The shape of targets to look for"
|
||||
:select-cols="interactiveCols"
|
||||
:items="['Circle', 'Polygon', 'Triangle', 'Quadrilateral']"
|
||||
@input="value => useCameraSettingsStore().changeCurrentPipelineSetting({contourShape: value}, false)"
|
||||
/>
|
||||
<cv-slider
|
||||
v-model="currentPipelineSettings.accuracyPercentage"
|
||||
:disabled="currentPipelineSettings.contourShape < 1"
|
||||
label="Shape Simplification"
|
||||
tooltip="How much we should simply the input contour before checking how many sides it has"
|
||||
:min="0"
|
||||
:max="100"
|
||||
:slider-cols="interactiveCols"
|
||||
@input="value => useCameraSettingsStore().changeCurrentPipelineSetting({accuracyPercentage: value}, false)"
|
||||
/>
|
||||
<cv-slider
|
||||
v-model="currentPipelineSettings.circleDetectThreshold"
|
||||
:disabled="currentPipelineSettings.contourShape !== 0"
|
||||
label="Circle match distance"
|
||||
tooltip="How close the centroid of a contour must be to the center of a circle in order for them to be matched"
|
||||
:min="1"
|
||||
:max="100"
|
||||
:slider-cols="interactiveCols"
|
||||
@input="value => useCameraSettingsStore().changeCurrentPipelineSetting({circleDetectThreshold: value}, false)"
|
||||
/>
|
||||
<cv-range-slider
|
||||
v-model="contourRadius"
|
||||
:disabled="currentPipelineSettings.contourShape !== 0"
|
||||
label="Radius"
|
||||
:min="0"
|
||||
:max="100"
|
||||
:slider-cols="interactiveCols"
|
||||
@input="value => useCameraSettingsStore().changeCurrentPipelineSetting({contourRadius: value}, false)"
|
||||
/>
|
||||
<cv-slider
|
||||
v-model="currentPipelineSettings.maxCannyThresh"
|
||||
:disabled="currentPipelineSettings.contourShape !== 0"
|
||||
label="Max Canny Threshold"
|
||||
:min="1"
|
||||
:max="100"
|
||||
:slider-cols="interactiveCols"
|
||||
@input="value => useCameraSettingsStore().changeCurrentPipelineSetting({maxCannyThresh: value}, false)"
|
||||
/>
|
||||
<cv-slider
|
||||
v-model="currentPipelineSettings.circleAccuracy"
|
||||
:disabled="currentPipelineSettings.contourShape !== 0"
|
||||
label="Circle Accuracy"
|
||||
:min="1"
|
||||
:max="100"
|
||||
:slider-cols="interactiveCols"
|
||||
@input="value => useCameraSettingsStore().changeCurrentPipelineSetting({circleAccuracy: value}, false)"
|
||||
/>
|
||||
<v-divider class="mt-3" />
|
||||
</template>
|
||||
<cv-select
|
||||
v-model="useCameraSettingsStore().currentPipelineSettings.contourSortMode"
|
||||
label="Target Sort"
|
||||
tooltip="Chooses the sorting mode used to determine the 'best' targets to provide to user code"
|
||||
:select-cols="interactiveCols"
|
||||
:items="['Largest','Smallest','Highest','Lowest','Rightmost','Leftmost','Centermost']"
|
||||
@input="value => useCameraSettingsStore().changeCurrentPipelineSetting({contourSortMode: value}, false)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
132
photon-client/src/components/dashboard/tabs/InputTab.vue
Normal file
132
photon-client/src/components/dashboard/tabs/InputTab.vue
Normal file
@@ -0,0 +1,132 @@
|
||||
<script setup lang="ts">
|
||||
import CvSlider from "@/components/common/cv-slider.vue";
|
||||
import { useCameraSettingsStore } from "@/stores/settings/CameraSettingsStore";
|
||||
import CvSwitch from "@/components/common/cv-switch.vue";
|
||||
import CvSelect from "@/components/common/cv-select.vue";
|
||||
import { computed, getCurrentInstance } from "vue";
|
||||
import { useSettingsStore } from "@/stores/settings/GeneralSettingsStore";
|
||||
import { useStateStore } from "@/stores/StateStore";
|
||||
|
||||
// Due to something with libcamera or something else IDK much about, the 90° rotations need to be disabled if the libcamera drivers are being used.
|
||||
const cameraRotations = computed(() => ["Normal", "90° CW", "180°", "90° CCW"].map((v, i) => ({ name: v, value: i, disabled: useSettingsStore().gpuAccelerationEnabled ? [1, 3].includes(i) : false })));
|
||||
|
||||
const streamDivisors = [1, 2, 4, 6];
|
||||
const getFilteredStreamDivisors = (): number[] => {
|
||||
const currentResolutionWidth = useCameraSettingsStore().currentVideoFormat.resolution.width;
|
||||
return streamDivisors.filter(x =>
|
||||
useCameraSettingsStore().isDriverMode
|
||||
|| !useSettingsStore().gpuAccelerationEnabled
|
||||
|| currentResolutionWidth / x < 400);
|
||||
};
|
||||
const getNumberOfSkippedDivisors = () => streamDivisors.length - getFilteredStreamDivisors().length;
|
||||
|
||||
const cameraResolutions = computed(() => useCameraSettingsStore().currentCameraSettings.validVideoFormats.map(f => `${f.resolution.width} X ${f.resolution.height} at ${f.fps} FPS, ${f.pixelFormat}`));
|
||||
const handleResolutionChange = (value: number) => {
|
||||
useCameraSettingsStore().changeCurrentPipelineSetting({ cameraVideoModeIndex: value }, false);
|
||||
|
||||
useCameraSettingsStore().changeCurrentPipelineSetting({ streamingFrameDivisor: getNumberOfSkippedDivisors() }, false);
|
||||
useCameraSettingsStore().currentPipelineSettings.streamingFrameDivisor = 0;
|
||||
|
||||
if(!useCameraSettingsStore().isCurrentVideoFormatCalibrated) {
|
||||
useCameraSettingsStore().changeCurrentPipelineSetting({ solvePNPEnabled: false }, true);
|
||||
}
|
||||
};
|
||||
|
||||
const streamResolutions = computed(() => {
|
||||
const streamDivisors = getFilteredStreamDivisors();
|
||||
const currentResolution = useCameraSettingsStore().currentVideoFormat.resolution;
|
||||
return streamDivisors
|
||||
.map(x => `${Math.floor(currentResolution.width / x)} X ${Math.floor(currentResolution.height / x)}`);
|
||||
});
|
||||
const handleStreamResolutionChange = (value: number) => {
|
||||
useCameraSettingsStore().changeCurrentPipelineSetting({ streamingFrameDivisor: value + getNumberOfSkippedDivisors() }, false);
|
||||
};
|
||||
|
||||
const interactiveCols = computed(() => (getCurrentInstance()?.proxy.$vuetify.breakpoint.mdAndDown || false) && (!useStateStore().sidebarFolded || useCameraSettingsStore().isDriverMode)) ? 9 : 8;
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<cv-slider
|
||||
v-model="useCameraSettingsStore().currentPipelineSettings.cameraExposure"
|
||||
:disabled="useCameraSettingsStore().currentCameraSettings.pipelineSettings.cameraAutoExposure"
|
||||
label="Exposure"
|
||||
tooltip="Directly controls how much light is allowed to fall onto the sensor, which affects apparent brightness"
|
||||
:min="0"
|
||||
:max="100"
|
||||
:slider-cols="interactiveCols"
|
||||
:step="0.1"
|
||||
@input="args => useCameraSettingsStore().changeCurrentPipelineSetting({cameraExposure: args}, false)"
|
||||
/>
|
||||
<cv-slider
|
||||
v-model="useCameraSettingsStore().currentPipelineSettings.cameraBrightness"
|
||||
label="Brightness"
|
||||
:min="0"
|
||||
:max="100"
|
||||
:slider-cols="interactiveCols"
|
||||
@input="args => useCameraSettingsStore().changeCurrentPipelineSetting({cameraBrightness: args}, false)"
|
||||
/>
|
||||
<cv-switch
|
||||
v-model="useCameraSettingsStore().currentPipelineSettings.cameraAutoExposure"
|
||||
class="pt-2"
|
||||
label="Auto Exposure"
|
||||
:switch-cols="interactiveCols"
|
||||
tooltip="Enables or Disables camera automatic adjustment for current lighting conditions"
|
||||
@input="args => useCameraSettingsStore().changeCurrentPipelineSetting({cameraAutoExposure: args}, false)"
|
||||
/>
|
||||
<cv-slider
|
||||
v-if="useCameraSettingsStore().currentPipelineSettings.cameraGain >= 0"
|
||||
v-model="useCameraSettingsStore().currentPipelineSettings.cameraGain"
|
||||
label="Camera Gain"
|
||||
tooltip="Controls camera gain, similar to brightness"
|
||||
:min="0"
|
||||
:max="100"
|
||||
:slider-cols="interactiveCols"
|
||||
@input="args => useCameraSettingsStore().changeCurrentPipelineSetting({cameraGain: args}, false)"
|
||||
/>
|
||||
<cv-slider
|
||||
v-if="useCameraSettingsStore().currentPipelineSettings.cameraRedGain !== -1"
|
||||
v-model="useCameraSettingsStore().currentPipelineSettings.cameraRedGain"
|
||||
label="Red AWB Gain"
|
||||
:min="0"
|
||||
:max="100"
|
||||
:slider-cols="interactiveCols"
|
||||
tooltip="Controls red automatic white balance gain, which affects how the camera captures colors in different conditions"
|
||||
@input="args => useCameraSettingsStore().changeCurrentPipelineSetting({cameraRedGain: args}, false)"
|
||||
/>
|
||||
<cv-slider
|
||||
v-if="useCameraSettingsStore().currentPipelineSettings.cameraBlueGain !== -1"
|
||||
v-model="useCameraSettingsStore().currentPipelineSettings.cameraBlueGain"
|
||||
label="Blue AWB Gain"
|
||||
:min="0"
|
||||
:max="100"
|
||||
:slider-cols="interactiveCols"
|
||||
tooltip="Controls blue automatic white balance gain, which affects how the camera captures colors in different conditions"
|
||||
@input="args => useCameraSettingsStore().changeCurrentPipelineSetting({cameraBlueGain: args}, false)"
|
||||
/>
|
||||
<cv-select
|
||||
v-model="useCameraSettingsStore().currentPipelineSettings.inputImageRotationMode"
|
||||
label="Orientation"
|
||||
tooltip="Rotates the camera stream"
|
||||
:items="cameraRotations"
|
||||
:select-cols="interactiveCols"
|
||||
@input="args => useCameraSettingsStore().changeCurrentPipelineSetting({inputImageRotationMode: args}, false)"
|
||||
/>
|
||||
<cv-select
|
||||
v-model="useCameraSettingsStore().currentPipelineSettings.cameraVideoModeIndex"
|
||||
label="Resolution"
|
||||
tooltip="Resolution and FPS the camera should directly capture at"
|
||||
:items="cameraResolutions"
|
||||
:select-cols="interactiveCols"
|
||||
@input="args => handleResolutionChange(args)"
|
||||
/>
|
||||
<cv-select
|
||||
v-model="useCameraSettingsStore().currentPipelineSettings.streamingFrameDivisor"
|
||||
label="Stream Resolution"
|
||||
tooltip="Resolution to which camera frames are downscaled for streaming to the dashboard"
|
||||
:items="streamResolutions"
|
||||
:select-cols="interactiveCols"
|
||||
@input="args => handleStreamResolutionChange(args)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
25
photon-client/src/components/dashboard/tabs/Map3DTab.vue
Normal file
25
photon-client/src/components/dashboard/tabs/Map3DTab.vue
Normal file
@@ -0,0 +1,25 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from "vue";
|
||||
import type { PhotonTarget } from "@/types/PhotonTrackingTypes";
|
||||
import { useStateStore } from "@/stores/StateStore";
|
||||
import Photon3dVisualizer from "@/components/app/photon-3d-visualizer.vue";
|
||||
|
||||
const trackedTargets = computed<PhotonTarget[]>(() => useStateStore().pipelineResults?.targets || []);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<v-row style="width: 100%">
|
||||
<v-col>
|
||||
<span class="white--text">Target Visualization</span>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row style="width: 100%">
|
||||
<v-col style="display: flex; align-items: center; justify-content: center">
|
||||
<photon3d-visualizer
|
||||
:targets="trackedTargets"
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</div>
|
||||
</template>
|
||||
178
photon-client/src/components/dashboard/tabs/OutputTab.vue
Normal file
178
photon-client/src/components/dashboard/tabs/OutputTab.vue
Normal file
@@ -0,0 +1,178 @@
|
||||
<script setup lang="ts">
|
||||
import CvSelect from "@/components/common/cv-select.vue";
|
||||
import { useCameraSettingsStore } from "@/stores/settings/CameraSettingsStore";
|
||||
import { PipelineType, RobotOffsetPointMode } from "@/types/PipelineTypes";
|
||||
import CvSwitch from "@/components/common/cv-switch.vue";
|
||||
import { computed, getCurrentInstance } from "vue";
|
||||
import { RobotOffsetType } from "@/types/SettingTypes";
|
||||
import { useStateStore } from "@/stores/StateStore";
|
||||
|
||||
const isTagPipeline = computed(() => useCameraSettingsStore().currentPipelineType === PipelineType.AprilTag || useCameraSettingsStore().currentPipelineType === PipelineType.Aruco);
|
||||
|
||||
interface MetricItem {
|
||||
header: string,
|
||||
value?: string
|
||||
}
|
||||
|
||||
const offsetPoints = computed<MetricItem[]>(() => {
|
||||
switch (useCameraSettingsStore().currentPipelineSettings.offsetRobotOffsetMode) {
|
||||
case RobotOffsetPointMode.Single:
|
||||
const value = Object.values(useCameraSettingsStore().currentPipelineSettings.offsetSinglePoint);
|
||||
return [{ header: "Offset Point", value: `(${value[0].toFixed(2)}°, ${value[1].toFixed(2)}°)` }];
|
||||
case RobotOffsetPointMode.Dual:
|
||||
const firstPoint = Object.values(useCameraSettingsStore().currentPipelineSettings.offsetDualPointA);
|
||||
const firstPointArea = useCameraSettingsStore().currentPipelineSettings.offsetDualPointAArea;
|
||||
const secondPoint = Object.values(useCameraSettingsStore().currentPipelineSettings.offsetDualPointB);
|
||||
const secondPointArea = useCameraSettingsStore().currentPipelineSettings.offsetDualPointBArea;
|
||||
return [{ header: "First Offset Point", value: `(${firstPoint[0].toFixed(2)}°, ${firstPoint[1].toFixed(2)}°)` },
|
||||
{ header: "First Offset Point Area", value: `${firstPointArea.toFixed(2)}%` },
|
||||
{ header: "Second Offset Point", value: `(${secondPoint[0].toFixed(2)}°, ${secondPoint[1].toFixed(2)}°)` },
|
||||
{ header: "Second Offset Point Area", value: `${secondPointArea.toFixed(2)}%` }
|
||||
];
|
||||
default:
|
||||
case RobotOffsetPointMode.None:
|
||||
return [];
|
||||
}
|
||||
});
|
||||
|
||||
const interactiveCols = computed(() => (getCurrentInstance()?.proxy.$vuetify.breakpoint.mdAndDown || false) && (!useStateStore().sidebarFolded || useCameraSettingsStore().isDriverMode)) ? 9 : 8;
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<cv-select
|
||||
v-model="useCameraSettingsStore().currentPipelineSettings.contourTargetOffsetPointEdge"
|
||||
label="Target Offset Point"
|
||||
tooltip="Changes where the 'center' of the target is (used for calculating e.g. pitch and yaw)"
|
||||
:items="['Center','Top','Bottom','Left','Right']"
|
||||
:select-cols="interactiveCols"
|
||||
@input="value => useCameraSettingsStore().changeCurrentPipelineSetting({contourTargetOffsetPointEdge: value}, false)"
|
||||
/>
|
||||
<cv-select
|
||||
v-if="!isTagPipeline"
|
||||
v-model="useCameraSettingsStore().currentPipelineSettings.contourTargetOrientation"
|
||||
label="Target Orientation"
|
||||
tooltip="Used to determine how to calculate target landmarks (e.g. the top, left, or bottom of the target)"
|
||||
:items="['Portrait', 'Landscape']"
|
||||
:select-cols="interactiveCols"
|
||||
@input="value => useCameraSettingsStore().changeCurrentPipelineSetting({contourTargetOrientation: value}, false)"
|
||||
/>
|
||||
<cv-switch
|
||||
v-model="useCameraSettingsStore().currentPipelineSettings.outputShowMultipleTargets"
|
||||
label="Show Multiple Targets"
|
||||
tooltip="If enabled, up to five targets will be displayed and sent to user code, instead of just one"
|
||||
:disabled="isTagPipeline"
|
||||
:switch-cols="interactiveCols"
|
||||
@input="value => useCameraSettingsStore().changeCurrentPipelineSetting({outputShowMultipleTargets: value}, false)"
|
||||
/>
|
||||
<v-divider />
|
||||
<table
|
||||
v-if="useCameraSettingsStore().currentPipelineSettings.offsetRobotOffsetMode !== RobotOffsetPointMode.None"
|
||||
class="metrics-table mt-3 mb-3"
|
||||
>
|
||||
<tr>
|
||||
<th
|
||||
v-for="(item, itemIndex) in offsetPoints"
|
||||
:key="itemIndex"
|
||||
class="metric-item metric-item-title"
|
||||
>
|
||||
{{ item.header }}
|
||||
</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td
|
||||
v-for="(item, itemIndex) in offsetPoints"
|
||||
:key="itemIndex"
|
||||
class="metric-item"
|
||||
>
|
||||
{{ item.value }}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<cv-select
|
||||
v-model="useCameraSettingsStore().currentPipelineSettings.offsetRobotOffsetMode"
|
||||
label="Robot Offset Mode"
|
||||
tooltip="Used to add an arbitrary offset to the location of the targeting crosshair"
|
||||
:items="['None','Single Point','Dual Point']"
|
||||
:select-cols="interactiveCols"
|
||||
@input="value => useCameraSettingsStore().changeCurrentPipelineSetting({offsetRobotOffsetMode: value}, false)"
|
||||
/>
|
||||
<v-row
|
||||
v-if="useCameraSettingsStore().currentPipelineSettings.offsetRobotOffsetMode !== RobotOffsetPointMode.None"
|
||||
align="center"
|
||||
justify="start"
|
||||
>
|
||||
<v-row v-if="useCameraSettingsStore().currentPipelineSettings.offsetRobotOffsetMode === RobotOffsetPointMode.Single">
|
||||
<v-col>
|
||||
<v-btn
|
||||
small
|
||||
color="accent"
|
||||
style="width: 100%;"
|
||||
class="black--text"
|
||||
@click="useCameraSettingsStore().takeRobotOffsetPoint(RobotOffsetType.Single)"
|
||||
>
|
||||
Take Point
|
||||
</v-btn>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row v-else-if="useCameraSettingsStore().currentPipelineSettings.offsetRobotOffsetMode === RobotOffsetPointMode.Dual">
|
||||
<v-col>
|
||||
<v-btn
|
||||
small
|
||||
color="accent"
|
||||
style="width: 100%;"
|
||||
class="black--text"
|
||||
@click="useCameraSettingsStore().takeRobotOffsetPoint(RobotOffsetType.DualFirst)"
|
||||
>
|
||||
Take First Point
|
||||
</v-btn>
|
||||
</v-col>
|
||||
<v-col>
|
||||
<v-btn
|
||||
small
|
||||
color="accent"
|
||||
style="width: 100%;"
|
||||
class="black--text"
|
||||
@click="useCameraSettingsStore().takeRobotOffsetPoint(RobotOffsetType.DualSecond)"
|
||||
>
|
||||
Take Second Point
|
||||
</v-btn>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-col>
|
||||
<v-btn
|
||||
small
|
||||
color="yellow darken-3"
|
||||
style="width: 100%;"
|
||||
@click="useCameraSettingsStore().takeRobotOffsetPoint(RobotOffsetType.Clear)"
|
||||
>
|
||||
Clear All Points
|
||||
</v-btn>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.metrics-table{
|
||||
border-collapse: separate;
|
||||
border-spacing: 0;
|
||||
border-radius: 5px;
|
||||
border: 1px solid white;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.metric-item {
|
||||
padding: 1px 15px 1px 10px;
|
||||
border-right: 1px solid;
|
||||
font-weight: normal;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.metric-item-title {
|
||||
font-size: 18px;
|
||||
text-decoration: underline;
|
||||
text-decoration-color: #ffd843;
|
||||
}
|
||||
</style>
|
||||
41
photon-client/src/components/dashboard/tabs/PnPTab.vue
Normal file
41
photon-client/src/components/dashboard/tabs/PnPTab.vue
Normal file
@@ -0,0 +1,41 @@
|
||||
<script setup lang="ts">
|
||||
import CvSelect from "@/components/common/cv-select.vue";
|
||||
import { useCameraSettingsStore } from "@/stores/settings/CameraSettingsStore";
|
||||
import { TargetModel } from "@/types/PipelineTypes";
|
||||
import CvSlider from "@/components/common/cv-slider.vue";
|
||||
import { computed, getCurrentInstance } from "vue";
|
||||
import { useStateStore } from "@/stores/StateStore";
|
||||
|
||||
const interactiveCols = computed(() => (getCurrentInstance()?.proxy.$vuetify.breakpoint.mdAndDown || false) && (!useStateStore().sidebarFolded || useCameraSettingsStore().isDriverMode)) ? 9 : 8;
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<cv-select
|
||||
v-model="useCameraSettingsStore().currentPipelineSettings.targetModel"
|
||||
label="Target Model"
|
||||
:items="[
|
||||
{name: '2020 High Goal Outer', value: TargetModel.InfiniteRechargeHighGoalOuter},
|
||||
{name: '2020 High Goal Inner', value: TargetModel.InfiniteRechargeHighGoalInner},
|
||||
{name: '2019 Dual Target', value: TargetModel.DeepSpaceDualTarget},
|
||||
{name: '2020 Power Cell (7in)', value: TargetModel.CircularPowerCell7in},
|
||||
{name: '2022 Cargo Ball (9.5in)', value: TargetModel.RapidReactCircularCargoBall},
|
||||
{name: '2016 High Goal', value: TargetModel.StrongholdHighGoal},
|
||||
{name: '200mm AprilTag', value: TargetModel.Apriltag_200mm},
|
||||
{name: '6in (16h5) Aruco', value: TargetModel.Aruco6in_16h5},
|
||||
{name: '6in (16h5) AprilTag', value: TargetModel.Apriltag6in_16h5}
|
||||
]"
|
||||
:select-cols="interactiveCols"
|
||||
@input="value => useCameraSettingsStore().changeCurrentPipelineSetting({targetModel: value}, false)"
|
||||
/>
|
||||
<cv-slider
|
||||
v-model="useCameraSettingsStore().currentPipelineSettings.cornerDetectionAccuracyPercentage"
|
||||
class="pt-2"
|
||||
:slider-cols="interactiveCols"
|
||||
label="Contour simplification Percentage"
|
||||
:min="0"
|
||||
:max="100"
|
||||
@input="value => useCameraSettingsStore().changeCurrentPipelineSetting({cornerDetectionAccuracyPercentage: value}, false)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
132
photon-client/src/components/dashboard/tabs/TargetsTab.vue
Normal file
132
photon-client/src/components/dashboard/tabs/TargetsTab.vue
Normal file
@@ -0,0 +1,132 @@
|
||||
<script setup lang="ts">
|
||||
import { useCameraSettingsStore } from "@/stores/settings/CameraSettingsStore";
|
||||
import { PipelineType } from "@/types/PipelineTypes";
|
||||
import { useStateStore } from "@/stores/StateStore";
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<v-row
|
||||
align="start"
|
||||
class="pb-4"
|
||||
style="height: 300px;"
|
||||
>
|
||||
<!-- Simple table height must be set here and in the CSS for the fixed-header to work -->
|
||||
<v-simple-table
|
||||
fixed-header
|
||||
height="100%"
|
||||
dense
|
||||
dark
|
||||
>
|
||||
<template #default>
|
||||
<thead style="font-size: 1.25rem;">
|
||||
<tr>
|
||||
<th class="text-center">
|
||||
Target Count
|
||||
</th>
|
||||
<th
|
||||
v-if="useCameraSettingsStore().currentPipelineType === PipelineType.AprilTag || useCameraSettingsStore().currentPipelineType === PipelineType.Aruco"
|
||||
class="text-center"
|
||||
>
|
||||
Fiducial ID
|
||||
</th>
|
||||
<template v-if="!useCameraSettingsStore().currentPipelineSettings.solvePNPEnabled">
|
||||
<th class="text-center">
|
||||
Pitch θ°
|
||||
</th>
|
||||
<th class="text-center">
|
||||
Yaw θ°
|
||||
</th>
|
||||
<th class="text-center">
|
||||
Skew θ°
|
||||
</th>
|
||||
<th class="text-center">
|
||||
Area %
|
||||
</th>
|
||||
</template>
|
||||
<template v-else>
|
||||
<th class="text-center">
|
||||
X meters
|
||||
</th>
|
||||
<th class="text-center">
|
||||
Y meters
|
||||
</th>
|
||||
<th class="text-center">
|
||||
Z Angle θ°
|
||||
</th>
|
||||
</template>
|
||||
<template v-if="useCameraSettingsStore().currentPipelineType === PipelineType.AprilTag && useCameraSettingsStore().currentPipelineSettings.solvePNPEnabled">
|
||||
<th class="text-center">
|
||||
Ambiguity %
|
||||
</th>
|
||||
</template>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr
|
||||
v-for="(target, index) in useStateStore().pipelineResults?.targets"
|
||||
:key="index"
|
||||
>
|
||||
<td>{{ index }}</td>
|
||||
<td v-if="useCameraSettingsStore().currentPipelineType === PipelineType.AprilTag || useCameraSettingsStore().currentPipelineType === PipelineType.Aruco">
|
||||
{{ target.fiducialId }}
|
||||
</td>
|
||||
<template v-if="!useCameraSettingsStore().currentPipelineSettings.solvePNPEnabled">
|
||||
<td>{{ target.pitch.toFixed(2) }}°</td>
|
||||
<td>{{ target.yaw.toFixed(2) }}°</td>
|
||||
<td>{{ target.skew.toFixed(2) }}°</td>
|
||||
<td>{{ target.area.toFixed(2) }}°</td>
|
||||
</template>
|
||||
<template v-else-if="useCameraSettingsStore().currentPipelineSettings.solvePNPEnabled">
|
||||
<td>{{ target.pose?.x.toFixed(2) }} m</td>
|
||||
<td>{{ target.pose?.y.toFixed(2) }} m</td>
|
||||
<td>{{ (target.pose?.angle_z * 180.0 / Math.PI).toFixed(2) }}°</td>
|
||||
</template>
|
||||
<template v-if="useCameraSettingsStore().currentPipelineType === PipelineType.AprilTag && useCameraSettingsStore().currentPipelineSettings.solvePNPEnabled">
|
||||
<td>{{ target.ambiguity?.toFixed(2) }}%</td>
|
||||
</template>
|
||||
</tr>
|
||||
</tbody>
|
||||
</template>
|
||||
</v-simple-table>
|
||||
</v-row>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.v-data-table {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
text-align: center;
|
||||
background-color: #006492 !important;
|
||||
|
||||
th, td {
|
||||
background-color: #006492 !important;
|
||||
font-size: 1rem !important;
|
||||
}
|
||||
|
||||
td {
|
||||
font-family: monospace !important;
|
||||
}
|
||||
|
||||
tbody :hover td {
|
||||
background-color: #005281 !important;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar {
|
||||
width: 0;
|
||||
height: 0.55em;
|
||||
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: #ffd843;
|
||||
border-radius: 10px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
255
photon-client/src/components/dashboard/tabs/ThresholdTab.vue
Normal file
255
photon-client/src/components/dashboard/tabs/ThresholdTab.vue
Normal file
@@ -0,0 +1,255 @@
|
||||
<script setup lang="ts">
|
||||
import { useCameraSettingsStore } from "@/stores/settings/CameraSettingsStore";
|
||||
import { computed, getCurrentInstance, onBeforeUnmount, onMounted } from "vue";
|
||||
import CvRangeSlider from "@/components/common/cv-range-slider.vue";
|
||||
import CvSwitch from "@/components/common/cv-switch.vue";
|
||||
import { useStateStore } from "@/stores/StateStore";
|
||||
import { ColorPicker, type HSV } from "@/lib/ColorPicker";
|
||||
|
||||
const averageHue = computed<number>(() => {
|
||||
const isHueInverted = useCameraSettingsStore().currentPipelineSettings.hueInverted;
|
||||
let val = Object.values(useCameraSettingsStore().currentPipelineSettings.hsvHue).reduce((a, b) => a + b, 0);
|
||||
|
||||
if(isHueInverted) val += 180;
|
||||
if (val > 360) val -= 360;
|
||||
|
||||
return val;
|
||||
});
|
||||
|
||||
// TODO fix cv-range-slider so that store access doesn't need to be deferred
|
||||
const hsvHue = computed<[number, number]>({
|
||||
get: () => Object.values(useCameraSettingsStore().currentPipelineSettings.hsvHue) as [number, number],
|
||||
set: v => useCameraSettingsStore().currentPipelineSettings.hsvHue = v
|
||||
});
|
||||
const hsvSaturation = computed<[number, number]>({
|
||||
get: () => Object.values(useCameraSettingsStore().currentPipelineSettings.hsvSaturation) as [number, number],
|
||||
set: v => useCameraSettingsStore().currentPipelineSettings.hsvSaturation = v
|
||||
});
|
||||
const hsvValue = computed<[number, number]>({
|
||||
get: () => Object.values(useCameraSettingsStore().currentPipelineSettings.hsvValue) as [number, number],
|
||||
set: v => useCameraSettingsStore().currentPipelineSettings.hsvValue = v
|
||||
});
|
||||
|
||||
let selectedEventMode: 0 | 1 | 2 | 3 = 0;
|
||||
const handleStreamClick = (event: MouseEvent) => {
|
||||
if(!useStateStore().colorPickingMode || selectedEventMode === 0) return;
|
||||
|
||||
const cameraStream = document.getElementById("input-camera-stream");
|
||||
if(cameraStream === null) return;
|
||||
|
||||
const canvas = document.createElement("canvas");
|
||||
canvas.width = cameraStream.clientWidth;
|
||||
canvas.height = cameraStream.clientHeight;
|
||||
|
||||
// Get the (x, y) position of the click with (0, 0) in the top left corner
|
||||
const rect = cameraStream.getBoundingClientRect();
|
||||
const x = Math.round((event.clientX - rect.left) / rect.width * cameraStream.clientWidth);
|
||||
const y = Math.round((event.clientY - rect.top) / rect.height * cameraStream.clientHeight);
|
||||
|
||||
const context = canvas.getContext("2d");
|
||||
if(context === null) return;
|
||||
|
||||
context.drawImage(cameraStream as CanvasImageSource, 0, 0, cameraStream.clientWidth, cameraStream.clientHeight);
|
||||
const colorPicker = new ColorPicker(context.getImageData(x, y, 1, 1).data);
|
||||
|
||||
// Calculate HSV values based on the mode
|
||||
let selectedHSVData: [HSV, HSV] = [[0, 0, 0], [0, 0, 0]];
|
||||
if(selectedEventMode === 1) {
|
||||
selectedHSVData = colorPicker.selectedColorRange();
|
||||
} else {
|
||||
const currentHue = Object.values(useCameraSettingsStore().currentPipelineSettings.hsvHue);
|
||||
const currentSaturation = Object.values(useCameraSettingsStore().currentPipelineSettings.hsvSaturation);
|
||||
const currentValue = Object.values(useCameraSettingsStore().currentPipelineSettings.hsvValue);
|
||||
|
||||
const currentData: [HSV, HSV] = [
|
||||
[currentHue[0], currentSaturation[0], currentValue[0]],
|
||||
[currentHue[1], currentSaturation[1], currentValue[1]]
|
||||
];
|
||||
|
||||
if(selectedEventMode === 2) {
|
||||
selectedHSVData = colorPicker.expandColorRange(currentData);
|
||||
} else if(selectedEventMode === 3) {
|
||||
selectedHSVData = colorPicker.shrinkColorRange(currentData);
|
||||
}
|
||||
}
|
||||
|
||||
// Update the store and backend with the new HSV values
|
||||
useCameraSettingsStore().changeCurrentPipelineSetting({
|
||||
hsvHue: [selectedHSVData[0][0], selectedHSVData[1][0]],
|
||||
hsvSaturation: [selectedHSVData[0][1], selectedHSVData[1][1]],
|
||||
hsvValue: [selectedHSVData[0][2], selectedHSVData[1][2]]
|
||||
}, true);
|
||||
|
||||
disableColorPicking();
|
||||
};
|
||||
|
||||
// Put some default values in case color picking was enabled before the enableColorPicking method is called
|
||||
let inputShowing = true;
|
||||
let outputShowing = false;
|
||||
const enableColorPicking = (mode: 1 | 2 | 3) => {
|
||||
useStateStore().colorPickingMode = true;
|
||||
inputShowing = useCameraSettingsStore().currentPipelineSettings.inputShouldShow;
|
||||
outputShowing = useCameraSettingsStore().currentPipelineSettings.outputShouldShow;
|
||||
useCameraSettingsStore().changeCurrentPipelineSetting({ outputShouldDraw: false, inputShouldShow: true, outputShouldShow: false }, true);
|
||||
selectedEventMode = mode;
|
||||
};
|
||||
const disableColorPicking = () => {
|
||||
useStateStore().colorPickingMode = false;
|
||||
useCameraSettingsStore().changeCurrentPipelineSetting({ outputShouldDraw: true, inputShouldShow: inputShowing, outputShouldShow: outputShowing }, true);
|
||||
selectedEventMode = 0;
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
const cameraStream = document.getElementById("input-camera-stream");
|
||||
if(cameraStream === null) return;
|
||||
|
||||
cameraStream.addEventListener("click", handleStreamClick);
|
||||
});
|
||||
onBeforeUnmount(() => {
|
||||
const cameraStream = document.getElementById("input-camera-stream");
|
||||
if(cameraStream === null) return;
|
||||
|
||||
cameraStream.removeEventListener("click", handleStreamClick);
|
||||
});
|
||||
|
||||
const interactiveCols = computed(() => (getCurrentInstance()?.proxy.$vuetify.breakpoint.mdAndDown || false) && (!useStateStore().sidebarFolded || useCameraSettingsStore().isDriverMode)) ? 9 : 8;
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="threshold-modifiers"
|
||||
:style="{'--averageHue': averageHue}"
|
||||
>
|
||||
<cv-range-slider
|
||||
id="hue-slider"
|
||||
v-model="hsvHue"
|
||||
:class="useCameraSettingsStore().currentPipelineSettings.hueInverted ? 'inverted-slider' : 'normal-slider'"
|
||||
label="Hue"
|
||||
tooltip="Describes color"
|
||||
:min="0"
|
||||
:max="180"
|
||||
:slider-cols="interactiveCols"
|
||||
:inverted="useCameraSettingsStore().currentPipelineSettings.hueInverted"
|
||||
@input="value => useCameraSettingsStore().changeCurrentPipelineSetting({hsvHue: value}, false)"
|
||||
/>
|
||||
<cv-range-slider
|
||||
id="sat-slider"
|
||||
v-model="hsvSaturation"
|
||||
class="normal-slider"
|
||||
label="Saturation"
|
||||
tooltip="Describes colorfulness; the smaller this value the 'whiter' the color becomes"
|
||||
:min="0"
|
||||
:max="255"
|
||||
:slider-cols="interactiveCols"
|
||||
@input="value => useCameraSettingsStore().changeCurrentPipelineSetting({hsvSaturation: value}, false)"
|
||||
/>
|
||||
<cv-range-slider
|
||||
id="value-slider"
|
||||
v-model="hsvValue"
|
||||
class="normal-slider"
|
||||
label="Value"
|
||||
tooltip="Describes lightness; the smaller this value the 'blacker' the color becomes"
|
||||
:min="0"
|
||||
:max="255"
|
||||
:slider-cols="interactiveCols"
|
||||
@input="value => useCameraSettingsStore().changeCurrentPipelineSetting({hsvValue: value}, false)"
|
||||
/>
|
||||
<cv-switch
|
||||
v-model="useCameraSettingsStore().currentPipelineSettings.hueInverted"
|
||||
label="Invert Hue"
|
||||
:switch-cols="interactiveCols"
|
||||
tooltip="Selects the hue range outside of the hue slider bounds instead of inside"
|
||||
@input="value => useCameraSettingsStore().changeCurrentPipelineSetting({hueInverted: value}, false)"
|
||||
/>
|
||||
<v-divider
|
||||
class="mt-3"
|
||||
/>
|
||||
<div>
|
||||
<div class="pt-3 white--text">
|
||||
Color Picker
|
||||
</div>
|
||||
<v-row
|
||||
justify="center"
|
||||
class="mt-3 mb-3"
|
||||
>
|
||||
<template v-if="!useStateStore().colorPickingMode">
|
||||
<v-btn
|
||||
color="accent"
|
||||
class="ma-2 black--text"
|
||||
small
|
||||
@click="enableColorPicking(useCameraSettingsStore().currentPipelineSettings.hueInverted ? 2 : 3)"
|
||||
>
|
||||
<v-icon left>
|
||||
mdi-minus
|
||||
</v-icon>
|
||||
Shrink Range
|
||||
</v-btn>
|
||||
<v-btn
|
||||
color="accent"
|
||||
class="ma-2 black--text"
|
||||
small
|
||||
@click="enableColorPicking(1)"
|
||||
>
|
||||
<v-icon left>
|
||||
mdi-plus-minus
|
||||
</v-icon>
|
||||
{{ useCameraSettingsStore().currentPipelineSettings.hueInverted ? "Exclude" : "Set to" }} Average
|
||||
</v-btn>
|
||||
<v-btn
|
||||
color="accent"
|
||||
class="ma-2 black--text"
|
||||
small
|
||||
@click="enableColorPicking(useCameraSettingsStore().currentPipelineSettings.hueInverted ? 3: 2)"
|
||||
>
|
||||
<v-icon left>
|
||||
mdi-plus
|
||||
</v-icon>
|
||||
Expand Range
|
||||
</v-btn>
|
||||
</template>
|
||||
<template v-else>
|
||||
<v-btn
|
||||
color="accent"
|
||||
class="ma-2 black--text"
|
||||
style="width: 30%;"
|
||||
small
|
||||
@click="disableColorPicking"
|
||||
>
|
||||
Cancel
|
||||
</v-btn>
|
||||
</template>
|
||||
</v-row>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="css">
|
||||
.threshold-modifiers {
|
||||
--averageHue: 0;
|
||||
}
|
||||
#hue-slider >>> .v-slider {
|
||||
background: linear-gradient( to right, #f00 0%, #ff0 17%, #0f0 33%, #0ff 50%, #00f 67%, #f0f 83%, #f00 100% );
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 0 5px #333, inset 0 0 3px #333;
|
||||
}
|
||||
#sat-slider >>> .v-slider {
|
||||
background: linear-gradient( to right, #fff 0%, hsl(var(--averageHue), 100%, 50%) 100% );
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 0 5px #333, inset 0 0 3px #333;
|
||||
}
|
||||
#value-slider >>> .v-slider {
|
||||
background: linear-gradient( to right, #000 0%, hsl(var(--averageHue), 100%, 50%) 100% );
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 0 5px #333, inset 0 0 3px #333;
|
||||
}
|
||||
>>> .v-slider__thumb {
|
||||
outline: black solid thin;
|
||||
}
|
||||
.normal-slider >>> .v-slider__track-fill {
|
||||
outline: black solid thin;
|
||||
}
|
||||
|
||||
.inverted-slider >>> .v-slider__track-background {
|
||||
outline: black solid thin;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user