mirror of
https://github.com/PhotonVision/photonvision
synced 2026-06-19 00:41:41 +00:00
## Description <!-- What changed? Why? (the code + comments should speak for itself on the "how") --> <!-- Fun screenshots or a cool video or something are super helpful as well. If this touches platform-specific behavior, this is where test evidence should be collected. --> <!-- Any issues this pull request closes or pull requests this supersedes should be linked with `Closes #issuenumber`. --> This adds a template modal that can be used for confirming that the user wants to delete something. The main goal is to reduce complication and duplicated code, and standardize the way we handle deletion. closes #2175 ## Meta Merge checklist: - [x] Pull Request title is [short, imperative summary](https://cbea.ms/git-commit/) of proposed changes - [x] The description documents the _what_ and _why_ - [x] This PR has been [linted](https://docs.photonvision.org/en/latest/docs/contributing/linting.html). - [ ] If this PR changes behavior or adds a feature, user documentation is updated - [ ] If this PR touches photon-serde, all messages have been regenerated and hashes have not changed unexpectedly - [ ] If this PR touches configuration, this is backwards compatible with settings back to v2025.3.2 - [ ] If this PR touches pipeline settings or anything related to data exchange, the frontend typing is updated - [ ] If this PR addresses a bug, a regression test for it is added --------- Co-authored-by: Devolian <devondoyle@outlook.com>
501 lines
19 KiB
Vue
501 lines
19 KiB
Vue
<script setup lang="ts">
|
|
import { ref, computed, inject } from "vue";
|
|
import { useStateStore } from "@/stores/StateStore";
|
|
import { useSettingsStore } from "@/stores/settings/GeneralSettingsStore";
|
|
import { type ObjectDetectionModelProperties } from "@/types/SettingTypes";
|
|
import PvDeleteModal from "@/components/common/pv-delete-modal.vue";
|
|
import { useTheme } from "vuetify";
|
|
import { axiosPost } from "@/lib/PhotonUtils";
|
|
|
|
const theme = useTheme();
|
|
const showImportDialog = ref(false);
|
|
const showInfo = ref({ show: false, model: {} as ObjectDetectionModelProperties });
|
|
const confirmDeleteDialog = ref({ show: false, model: {} as ObjectDetectionModelProperties });
|
|
const showRenameDialog = ref({
|
|
show: false,
|
|
model: {} as ObjectDetectionModelProperties,
|
|
newName: ""
|
|
});
|
|
|
|
const address = inject<string>("backendHost");
|
|
|
|
const importModelFile = ref<File | null>(null);
|
|
const importLabels = ref<string | null>(null);
|
|
const importHeight = ref<number | null>(null);
|
|
const importWidth = ref<number | null>(null);
|
|
const importVersion = ref<string | null>(null);
|
|
|
|
// TODO gray out the button when model is uploading
|
|
const handleImport = async () => {
|
|
if (importModelFile.value === null) return;
|
|
|
|
const formData = new FormData();
|
|
|
|
formData.append("modelFile", importModelFile.value);
|
|
formData.append("labels", importLabels.value?.toString() || "");
|
|
formData.append("height", importHeight.value?.toString() || "");
|
|
formData.append("width", importWidth.value?.toString() || "");
|
|
formData.append("version", importVersion.value?.toString() || "");
|
|
|
|
useStateStore().showSnackbarMessage({
|
|
message: "Importing Object Detection Model...",
|
|
color: "secondary",
|
|
timeout: -1
|
|
});
|
|
|
|
axiosPost("/objectdetection/import", "import an object detection model", formData, {
|
|
headers: { "Content-Type": "multipart/form-data" },
|
|
onUploadProgress: ({ progress }) => {
|
|
const uploadPercentage = (progress || 0) * 100.0;
|
|
if (uploadPercentage < 99.5) {
|
|
useStateStore().showSnackbarMessage({
|
|
message: "Object Detection Model Upload in Process, " + uploadPercentage.toFixed(2) + "% complete",
|
|
color: "secondary",
|
|
timeout: -1
|
|
});
|
|
} else {
|
|
useStateStore().showSnackbarMessage({
|
|
message: "Processing uploaded Object Detection Model...",
|
|
color: "secondary",
|
|
timeout: -1
|
|
});
|
|
}
|
|
}
|
|
});
|
|
|
|
showImportDialog.value = false;
|
|
|
|
importModelFile.value = null;
|
|
importLabels.value = null;
|
|
importHeight.value = null;
|
|
importWidth.value = null;
|
|
importVersion.value = null;
|
|
};
|
|
|
|
const deleteModel = async (model: ObjectDetectionModelProperties) => {
|
|
axiosPost("/objectdetection/delete", "delete an object detection model", {
|
|
modelPath: model.modelPath
|
|
});
|
|
};
|
|
|
|
const renameModel = async (model: ObjectDetectionModelProperties, newName: string) => {
|
|
useStateStore().showSnackbarMessage({
|
|
message: "Renaming Object Detection Model...",
|
|
color: "secondary",
|
|
timeout: -1
|
|
});
|
|
|
|
axiosPost("/objectdetection/rename", "rename an object detection model", {
|
|
modelPath: model.modelPath.replace("file:", ""),
|
|
newName: newName
|
|
});
|
|
showRenameDialog.value.show = false;
|
|
};
|
|
|
|
// Filters out models that are not supported by the current backend, and returns a flattened list.
|
|
const supportedModels = computed(() => {
|
|
const { availableModels, supportedBackends } = useSettingsStore().general;
|
|
const isSupported = (model: any) => {
|
|
// Check if model's family is in the list of supported backends
|
|
return supportedBackends.some((backend: string) => backend.toLowerCase() === model.family.toLowerCase());
|
|
};
|
|
|
|
// Filter models where the family is supported and flatten the list
|
|
return availableModels.filter(isSupported);
|
|
});
|
|
|
|
const exportModels = ref();
|
|
const openExportPrompt = () => {
|
|
exportModels.value.click();
|
|
};
|
|
|
|
const exportIndividualModel = ref();
|
|
const openExportIndividualModelPrompt = () => {
|
|
exportIndividualModel.value.click();
|
|
};
|
|
|
|
const showNukeDialog = ref(false);
|
|
const nukeModels = () => {
|
|
axiosPost("/objectdetection/nuke", "clear and reset object detection models");
|
|
};
|
|
|
|
const showBulkImportDialog = ref(false);
|
|
const importFile = ref<File | null>(null);
|
|
const handleBulkImport = () => {
|
|
if (importFile.value === null) return;
|
|
|
|
const formData = new FormData();
|
|
formData.append("data", importFile.value);
|
|
|
|
axiosPost("/objectdetection/bulkimport", "import object detection models", formData, {
|
|
headers: { "Content-Type": "multipart/form-data" },
|
|
onUploadProgress: ({ progress }) => {
|
|
const uploadPercentage = (progress || 0) * 100.0;
|
|
if (uploadPercentage < 99.5) {
|
|
useStateStore().showSnackbarMessage({
|
|
message: "Object Detection Models Upload in Progress",
|
|
color: "secondary",
|
|
timeout: -1,
|
|
progressBar: uploadPercentage,
|
|
progressBarColor: "primary"
|
|
});
|
|
} else {
|
|
useStateStore().showSnackbarMessage({
|
|
message: "Importing New Object Detection Models...",
|
|
color: "secondary",
|
|
timeout: -1
|
|
});
|
|
}
|
|
}
|
|
});
|
|
showImportDialog.value = false;
|
|
importFile.value = null;
|
|
};
|
|
</script>
|
|
|
|
<template>
|
|
<v-card class="mb-3" color="surface">
|
|
<v-card-title>Object Detection</v-card-title>
|
|
<div class="pa-5 pt-0">
|
|
<v-row>
|
|
<v-col cols="12" sm="6">
|
|
<v-btn
|
|
color="buttonActive"
|
|
class="justify-center"
|
|
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
|
|
@click="() => (showImportDialog = true)"
|
|
>
|
|
<v-icon start class="open-icon"> mdi-import </v-icon>
|
|
<span class="open-label">Import Model</span>
|
|
</v-btn>
|
|
<v-dialog
|
|
v-model="showImportDialog"
|
|
width="600"
|
|
@update:modelValue="
|
|
() => {
|
|
importModelFile = null;
|
|
importLabels = null;
|
|
importHeight = null;
|
|
importWidth = null;
|
|
importVersion = null;
|
|
}
|
|
"
|
|
>
|
|
<v-card color="surface" dark>
|
|
<v-card-title class="pb-0">Import New Object Detection Model</v-card-title>
|
|
<v-card-text>
|
|
<span v-if="useSettingsStore().general.supportedBackends?.includes('RKNN')"
|
|
>Upload a new object detection model to this device that can be used in a pipeline. Note that ONLY
|
|
640x640 YOLOv5, YOLOv8, and YOLOv11 models trained and converted to `.rknn` format for RK3588 SOCs are
|
|
currently supporter!</span
|
|
>
|
|
<span v-else-if="useSettingsStore().general.supportedBackends?.includes('RUBIK')"
|
|
>Upload a new object detection model to this device that can be used in a pipeline. Note that ONLY
|
|
640x640 YOLOv8 and YOLOv11 models trained and converted to `.tflite` format for QCS6490 compatible
|
|
backends are currently supported!
|
|
</span>
|
|
<span v-else>
|
|
If you're seeing this, something broke; please file a ticket and tell us the details of your
|
|
situation.</span
|
|
>
|
|
<div class="pa-5 pb-0">
|
|
<v-file-input
|
|
v-model="importModelFile"
|
|
variant="underlined"
|
|
label="Model File"
|
|
:accept="
|
|
useSettingsStore().general.supportedBackends?.includes('RKNN')
|
|
? '.rknn'
|
|
: useSettingsStore().general.supportedBackends?.includes('RUBIK')
|
|
? '.tflite'
|
|
: ''
|
|
"
|
|
/>
|
|
<v-text-field
|
|
v-model="importLabels"
|
|
label="Labels"
|
|
placeholder="Comma separated labels, no spaces"
|
|
type="text"
|
|
variant="underlined"
|
|
/>
|
|
<v-text-field v-model="importWidth" variant="underlined" label="Width" type="number" />
|
|
<v-text-field v-model="importHeight" variant="underlined" label="Height" type="number" />
|
|
<v-select
|
|
v-model="importVersion"
|
|
variant="underlined"
|
|
label="Model Version"
|
|
:items="
|
|
useSettingsStore().general.supportedBackends?.includes('RKNN')
|
|
? ['YOLOv5', 'YOLOv8', 'YOLO11']
|
|
: ['YOLOv8', 'YOLO11']
|
|
"
|
|
/>
|
|
<v-btn
|
|
color="buttonActive"
|
|
width="100%"
|
|
:disabled="
|
|
importModelFile === null ||
|
|
importLabels === null ||
|
|
importWidth === null ||
|
|
importHeight === null ||
|
|
importVersion === null
|
|
"
|
|
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
|
|
@click="handleImport()"
|
|
>
|
|
<v-icon start class="open-icon" size="large"> mdi-import </v-icon>
|
|
<span class="open-label">Import Object Detection Model</span>
|
|
</v-btn>
|
|
</div>
|
|
</v-card-text>
|
|
</v-card>
|
|
</v-dialog>
|
|
</v-col>
|
|
<v-col cols="12" sm="6">
|
|
<v-btn
|
|
color="buttonActive"
|
|
class="justify-center"
|
|
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
|
|
@click="() => (showBulkImportDialog = true)"
|
|
>
|
|
<v-icon start class="open-icon"> mdi-import </v-icon>
|
|
<span class="open-label">Bulk Import</span>
|
|
</v-btn>
|
|
<v-dialog v-model="showBulkImportDialog" width="600">
|
|
<v-card color="surface" dark>
|
|
<v-card-title class="pb-0">Import Multiple Object Detection Models</v-card-title>
|
|
<v-card-text>
|
|
Upload a zip file containing multiple object detection models to this device. Note this zip file should
|
|
only come from a previous export of object detection models.
|
|
<div class="pa-5 pb-0">
|
|
<v-file-input v-model="importFile" variant="underlined" label="Zip File" accept=".zip" />
|
|
<v-btn
|
|
color="buttonActive"
|
|
width="100%"
|
|
:disabled="importFile === null"
|
|
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
|
|
@click="handleBulkImport()"
|
|
>
|
|
<v-icon start class="open-icon" size="large"> mdi-import </v-icon>
|
|
<span class="open-label">Bulk Import</span>
|
|
</v-btn>
|
|
</div>
|
|
</v-card-text>
|
|
</v-card>
|
|
</v-dialog>
|
|
</v-col>
|
|
<v-col cols="12" sm="6">
|
|
<v-btn
|
|
color="buttonPassive"
|
|
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
|
|
@click="openExportPrompt"
|
|
>
|
|
<v-icon start class="open-icon"> mdi-export </v-icon>
|
|
<span class="open-label">Export Models</span>
|
|
</v-btn>
|
|
<a
|
|
ref="exportModels"
|
|
style="color: black; text-decoration: none; display: none"
|
|
:href="`http://${address}/api/objectdetection/export`"
|
|
download="photonvision-object-detection-models-export.zip"
|
|
target="_blank"
|
|
/>
|
|
</v-col>
|
|
<v-col cols="12" sm="6">
|
|
<v-btn
|
|
color="error"
|
|
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
|
|
@click="() => (showNukeDialog = true)"
|
|
>
|
|
<v-icon left class="open-icon"> mdi-trash </v-icon>
|
|
<span class="open-label">Clear and reset models</span>
|
|
</v-btn>
|
|
</v-col>
|
|
</v-row>
|
|
<v-row>
|
|
<v-col cols="">
|
|
<v-table fixed-header height="100%" density="compact" dark>
|
|
<thead style="font-size: 1.25rem">
|
|
<tr>
|
|
<th>Model Nicknames</th>
|
|
<th>Labels</th>
|
|
<th>Delete</th>
|
|
<th>Edit</th>
|
|
<th>Info</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<tr v-for="model in supportedModels" :key="model.modelPath">
|
|
<td>{{ model.nickname }}</td>
|
|
<td>{{ model.labels.join(", ") }}</td>
|
|
<td class="text-right">
|
|
<v-btn
|
|
icon
|
|
small
|
|
color="error"
|
|
title="Delete Model"
|
|
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
|
|
@click="() => (confirmDeleteDialog = { show: true, model })"
|
|
>
|
|
<v-icon size="large">mdi-trash-can-outline</v-icon>
|
|
</v-btn>
|
|
</td>
|
|
<td class="text-right">
|
|
<v-btn
|
|
icon
|
|
small
|
|
color="buttonActive"
|
|
title="Rename Model"
|
|
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
|
|
@click="() => (showRenameDialog = { show: true, model, newName: '' })"
|
|
>
|
|
<v-icon size="large">mdi-pencil</v-icon>
|
|
</v-btn>
|
|
</td>
|
|
<td class="text-right">
|
|
<v-btn
|
|
icon
|
|
small
|
|
color="buttonPassive"
|
|
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
|
|
@click="() => (showInfo = { show: true, model })"
|
|
>
|
|
<v-icon size="large">mdi-information</v-icon>
|
|
</v-btn>
|
|
</td>
|
|
</tr>
|
|
</tbody>
|
|
</v-table>
|
|
|
|
<pv-delete-modal
|
|
v-model="confirmDeleteDialog.show"
|
|
:width="500"
|
|
:on-confirm="() => deleteModel(confirmDeleteDialog.model)"
|
|
title="Delete Object Detection Model"
|
|
:description="`Are you sure you want to delete the model ${confirmDeleteDialog.model.nickname}?`"
|
|
delete-text="Delete model"
|
|
/>
|
|
|
|
<v-dialog v-model="showRenameDialog.show" width="600">
|
|
<v-card color="surface" dark>
|
|
<v-card-title>Rename Object Detection Model</v-card-title>
|
|
<v-card-text class="pt-0">
|
|
Enter a new name for the model "{{ showRenameDialog.model.nickname }}":
|
|
<div class="pa-5 pb-0">
|
|
<v-text-field v-model="showRenameDialog.newName" hide-details label="New Name" variant="underlined" />
|
|
</div>
|
|
<v-card-actions class="pt-5 pb-0 pr-0" style="justify-content: flex-end">
|
|
<v-btn
|
|
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
|
|
color="error"
|
|
@click="showRenameDialog.show = false"
|
|
>Cancel</v-btn
|
|
>
|
|
<v-btn
|
|
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
|
|
color="buttonActive"
|
|
@click="renameModel(showRenameDialog.model, showRenameDialog.newName)"
|
|
>Rename</v-btn
|
|
>
|
|
</v-card-actions>
|
|
</v-card-text>
|
|
</v-card>
|
|
</v-dialog>
|
|
<v-dialog v-model="showInfo.show" width="600">
|
|
<v-card color="surface" dark>
|
|
<v-card-title>Object Detection Model Info</v-card-title>
|
|
<v-card-text class="pt-0">
|
|
<v-btn
|
|
color="buttonPassive"
|
|
width="100%"
|
|
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
|
|
@click="openExportIndividualModelPrompt"
|
|
>
|
|
<v-icon left class="open-icon" size="large"> mdi-export </v-icon>
|
|
<span class="open-label">Export Model</span>
|
|
</v-btn>
|
|
<a
|
|
ref="exportIndividualModel"
|
|
style="color: black; text-decoration: none; display: none"
|
|
:href="`http://${address}/api/objectdetection/exportIndividual?modelPath=${showInfo.model.modelPath.replace('file:', '')}`"
|
|
:download="`${showInfo.model.nickname}_${showInfo.model.family}_${showInfo.model.version}_${showInfo.model.resolutionWidth}x${showInfo.model.resolutionHeight}_${showInfo.model.labels.join('_')}.${showInfo.model.family.toLowerCase()}`"
|
|
target="_blank"
|
|
/>
|
|
<div class="pt-5">
|
|
<p>Model Path: {{ showInfo.model.modelPath }}</p>
|
|
<p>Model Nickname: {{ showInfo.model.nickname }}</p>
|
|
<p>Model Family: {{ showInfo.model.family }}</p>
|
|
<p>Model Version: {{ showInfo.model.version }}</p>
|
|
<p>Model Label(s): {{ showInfo.model.labels.join(", ") }}</p>
|
|
<p>Model Resolution: {{ showInfo.model.resolutionWidth }} x {{ showInfo.model.resolutionHeight }}</p>
|
|
</div>
|
|
</v-card-text>
|
|
</v-card>
|
|
</v-dialog>
|
|
</v-col>
|
|
</v-row>
|
|
</div>
|
|
|
|
<pv-delete-modal
|
|
v-model="showNukeDialog"
|
|
:on-backup="openExportPrompt"
|
|
:on-confirm="nukeModels"
|
|
title="Delete and Reset All Object Detection Models"
|
|
:description="'This will delete ALL object detection models and re-extract the default object detection models. This action cannot be undone.'"
|
|
:expected-confirmation-text="'Delete Models'"
|
|
delete-text="Delete all models"
|
|
/>
|
|
</v-card>
|
|
</template>
|
|
|
|
<style scoped lang="scss">
|
|
.v-col-12 > .v-btn {
|
|
width: 100%;
|
|
}
|
|
|
|
.pt-10px {
|
|
padding-top: 10px !important;
|
|
}
|
|
|
|
@media only screen and (max-width: 351px) {
|
|
.open-icon {
|
|
margin: 0 !important;
|
|
}
|
|
.open-label {
|
|
display: none;
|
|
}
|
|
}
|
|
.v-table {
|
|
width: 100%;
|
|
height: 100%;
|
|
text-align: center;
|
|
|
|
th,
|
|
td {
|
|
font-size: 1rem !important;
|
|
color: white !important;
|
|
text-align: center !important;
|
|
}
|
|
|
|
td {
|
|
font-family: monospace !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: rgb(var(--v-theme-accent));
|
|
border-radius: 10px;
|
|
}
|
|
}
|
|
</style>
|