mirror of
https://github.com/PhotonVision/photonvision
synced 2026-07-05 03:21:40 +00:00
256 lines
9.3 KiB
Vue
256 lines
9.3 KiB
Vue
|
|
<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>
|