mirror of
https://github.com/PhotonVision/photonvision
synced 2026-06-22 01:11:40 +00:00
250 lines
9.2 KiB
Vue
250 lines
9.2 KiB
Vue
<script setup lang="ts">
|
|
import { useCameraSettingsStore } from "@/stores/settings/CameraSettingsStore";
|
|
import { computed, getCurrentInstance, onBeforeUnmount, onMounted } from "vue";
|
|
import PvRangeSlider from "@/components/common/pv-range-slider.vue";
|
|
import PvSwitch from "@/components/common/pv-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 pv-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 }">
|
|
<pv-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)"
|
|
/>
|
|
<pv-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)"
|
|
/>
|
|
<pv-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)"
|
|
/>
|
|
<pv-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;
|
|
/* prettier-ignore */
|
|
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;
|
|
/* prettier-ignore */
|
|
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;
|
|
/* prettier-ignore */
|
|
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>
|