Files
PhotonVision/photon-client/src/components/dashboard/tabs/ContoursTab.vue
Sam Freund 2372e110f9 TypeCheck Frontend (#2394)
We recently had an error that would've been caught by type checking in the frontend (see #2393). This PR implements type checking so that future errors will be caught.

Additionally, this PR contains miscellaneous frontend cleanup that's tangentially related to type-checking.
2026-05-05 15:24:19 +00:00

296 lines
12 KiB
Vue

<script setup lang="ts">
import { useCameraSettingsStore } from "@/stores/settings/CameraSettingsStore";
import {
type ActivePipelineSettings,
PipelineType,
ContourSortMode,
ContourTargetOrientation,
ContourGroupingMode,
ContourIntersection,
ContourShape
} from "@/types/PipelineTypes";
import PvRangeSlider from "@/components/common/pv-range-slider.vue";
import PvSelect from "@/components/common/pv-select.vue";
import PvSlider from "@/components/common/pv-slider.vue";
import { computed } from "vue";
import { useStateStore } from "@/stores/StateStore";
import { useDisplay } from "vuetify";
// 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 = computed<ActivePipelineSettings>(
() => useCameraSettingsStore().currentPipelineSettings
);
// TODO fix pv-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.value.pipelineType === PipelineType.ColoredShape
? (Object.values(currentPipelineSettings.value.contourPerimeter) as [number, number])
: ([0, 0] as [number, number]),
set: (v) => {
if (currentPipelineSettings.value.pipelineType === PipelineType.ColoredShape) {
currentPipelineSettings.value.contourPerimeter = v;
}
}
});
const contourRadius = computed<[number, number]>({
get: () =>
currentPipelineSettings.value.pipelineType === PipelineType.ColoredShape
? (Object.values(currentPipelineSettings.value.contourRadius) as [number, number])
: ([0, 0] as [number, number]),
set: (v) => {
if (currentPipelineSettings.value.pipelineType === PipelineType.ColoredShape) {
currentPipelineSettings.value.contourRadius = v;
}
}
});
const { mdAndDown } = useDisplay();
const interactiveCols = computed(() =>
mdAndDown.value && (!useStateStore().sidebarFolded || useCameraSettingsStore().isDriverMode) ? 8 : 7
);
</script>
<template>
<div>
<pv-select
v-model="useCameraSettingsStore().currentPipelineSettings.contourTargetOrientation"
label="Target Orientation"
tooltip="Used to determine how to calculate target landmarks, as well as aspect ratio"
:items="[
{ value: ContourTargetOrientation.Portrait, name: 'Portrait' },
{ value: ContourTargetOrientation.Landscape, name: 'Landscape' }
]"
:select-cols="interactiveCols"
@update:modelValue="
(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ contourTargetOrientation: value }, false)
"
/>
<pv-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="[
{ value: ContourSortMode.Largest, name: 'Largest' },
{ value: ContourSortMode.Smallest, name: 'Smallest' },
{ value: ContourSortMode.Highest, name: 'Highest' },
{ value: ContourSortMode.Lowest, name: 'Lowest' },
{ value: ContourSortMode.Rightmost, name: 'Rightmost' },
{ value: ContourSortMode.Leftmost, name: 'Leftmost' },
{ value: ContourSortMode.Centermost, name: 'Centermost' }
]"
@update:modelValue="
(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ contourSortMode: value }, false)
"
/>
<pv-range-slider
v-model="contourArea"
label="Area"
:min="0"
:max="100"
:slider-cols="interactiveCols"
:step="0.01"
@update:modelValue="
(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ contourArea: value }, false)
"
/>
<pv-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"
@update:modelValue="
(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ contourRatio: value }, false)
"
/>
<pv-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"
@update:modelValue="
(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ contourFullness: value }, false)
"
/>
<pv-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"
@update:modelValue="
(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ contourPerimeter: value }, false)
"
/>
<pv-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"
@update:modelValue="
(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ contourSpecklePercentage: value }, false)
"
/>
<template v-if="currentPipelineSettings.pipelineType === PipelineType.Reflective">
<pv-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"
@update:modelValue="
(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ contourFilterRangeX: value }, false)
"
/>
<pv-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"
@update:modelValue="
(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ contourFilterRangeY: value }, false)
"
/>
<pv-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="[
{ value: ContourGroupingMode.Single, name: 'Single' },
{ value: ContourGroupingMode.Dual, name: 'Dual' },
{ value: ContourGroupingMode.TwoOrMore, name: 'Two or More' }
]"
@update:modelValue="
(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ contourGroupingMode: value }, false)
"
/>
<pv-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="[
{ value: ContourIntersection.None, name: 'None' },
{ value: ContourIntersection.Up, name: 'Up' },
{ value: ContourIntersection.Down, name: 'Down' },
{ value: ContourIntersection.Left, name: 'Left' },
{ value: ContourIntersection.Right, name: 'Right' }
]"
:disabled="useCameraSettingsStore().currentPipelineSettings.contourGroupingMode === 0"
@update:modelValue="
(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ contourIntersection: value }, false)
"
/>
</template>
<template v-else-if="currentPipelineSettings.pipelineType === PipelineType.ColoredShape">
<pv-select
v-model="currentPipelineSettings.contourShape"
label="Target Shape"
tooltip="The shape of targets to look for"
:select-cols="interactiveCols"
:items="[
{ value: ContourShape.Circle, name: 'Circle' },
{ value: ContourShape.Polygon, name: 'Polygon' },
{ value: ContourShape.Triangle, name: 'Triangle' },
{ value: ContourShape.Quadrilateral, name: 'Quadrilateral' }
]"
@update:modelValue="
(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ contourShape: value }, false)
"
/>
<pv-slider
v-if="currentPipelineSettings.contourShape >= 1"
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"
@update:modelValue="
(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ accuracyPercentage: value }, false)
"
/>
<pv-slider
v-if="currentPipelineSettings.contourShape === 0"
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"
@update:modelValue="
(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ circleDetectThreshold: value }, false)
"
/>
<pv-slider
v-if="currentPipelineSettings.contourShape === 0"
v-model="currentPipelineSettings.maxCannyThresh"
:disabled="currentPipelineSettings.contourShape !== 0"
label="Max Canny Threshold"
:min="1"
:max="100"
:slider-cols="interactiveCols"
@update:modelValue="
(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ maxCannyThresh: value }, false)
"
/>
<pv-slider
v-if="currentPipelineSettings.contourShape === 0"
v-model="currentPipelineSettings.circleAccuracy"
:disabled="currentPipelineSettings.contourShape !== 0"
label="Circle Accuracy"
:min="1"
:max="100"
:slider-cols="interactiveCols"
@update:modelValue="
(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ circleAccuracy: value }, false)
"
/>
<pv-range-slider
v-if="currentPipelineSettings.contourShape === 0"
v-model="contourRadius"
:disabled="currentPipelineSettings.contourShape !== 0"
label="Radius"
:min="0"
:max="100"
:slider-cols="interactiveCols"
@update:modelValue="
(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ contourRadius: value }, false)
"
/>
</template>
</div>
</template>