214 lines
5.9 KiB
Vue
214 lines
5.9 KiB
Vue
<script setup>
|
||
import { provide, ref } from 'vue';
|
||
import { CirclePlusFilled } from '@element-plus/icons-vue';
|
||
import { ElMessage, ElSplitter, ElSplitterPanel, ElCard, ElButton, ElIcon } from 'element-plus';
|
||
import ClassBox from './teachableModel/ClassBox.vue';
|
||
import CameraBox from './teachableModel/CameraBox.vue';
|
||
import ModelArea from './teachableModel/ModelArea.vue';
|
||
import TrainArea from './teachableModel/TrainArea.vue';
|
||
|
||
// import 'element-plus/theme-chalk/src/message.scss';
|
||
// import 'element-plus/theme-chalk/src/splitter.scss';
|
||
// import 'element-plus/theme-chalk/src/splitter-panel.scss';
|
||
// import 'element-plus/theme-chalk/src/card.scss';
|
||
// import 'element-plus/theme-chalk/src/button.scss';
|
||
// import 'element-plus/theme-chalk/src/icon.scss';
|
||
|
||
|
||
// 存放所有拍摄的内容
|
||
const shotList = ref([])
|
||
provide('shotList', shotList)
|
||
|
||
// 存放类别和每个类别内含有的图片样本
|
||
const picList = ref([
|
||
{ title: '类别 1', list: [], disabled: false },
|
||
{ title: '类别 2', list: [], disabled: false },
|
||
])
|
||
provide('picList', picList)
|
||
|
||
// 训练状态
|
||
const states = ref({
|
||
isTraining: 0, // 0为未开始,1为训练中,2为训练完成
|
||
})
|
||
provide('states', states)
|
||
|
||
// 向某个类别添加一个样本
|
||
async function handleAddSample(idx, data) {
|
||
// 类别已被禁用
|
||
if (picList.value[idx].disabled) {
|
||
ElMessage.error('该类别已被禁用')
|
||
return
|
||
}
|
||
// 可能传入 base64 字符串上传图片
|
||
if (data) {
|
||
// data 是 base64,说明是上传图片
|
||
picList.value[idx].list.push(data)
|
||
return
|
||
}
|
||
// 否则是拍摄,调用摄像头拍摄
|
||
await handleParentShot()
|
||
// 检查是否成功拍摄到图片
|
||
if (
|
||
!shotList.value[shotList.value.length - 1]
|
||
|| shotList.value[shotList.value.length - 1] === 'data:,'
|
||
) {
|
||
ElMessage.error('未获取到有效样本')
|
||
return
|
||
}
|
||
// 拍摄到的图片添加到类别中
|
||
picList.value[idx].list.push(shotList.value[shotList.value.length - 1])
|
||
}
|
||
|
||
// 添加一个类别
|
||
function handleAddClass() {
|
||
picList.value.push({
|
||
title: `类别 ${picList.value.length + 1}`,
|
||
list: [],
|
||
disabled: false,
|
||
})
|
||
}
|
||
|
||
// 删除一个类别
|
||
function handleDeleteClass(idx) {
|
||
picList.value.splice(idx, 1)
|
||
}
|
||
|
||
// 禁用/启用一个类别
|
||
function handleDisableClass(idx) {
|
||
picList.value[idx].disabled = !picList.value[idx].disabled
|
||
}
|
||
|
||
// 重命名一个类别
|
||
function handleRenameClass(idx, newTitle) {
|
||
picList.value[idx].title = newTitle
|
||
}
|
||
|
||
// 清空一个类别的样本
|
||
function handleClearSamples(idx) {
|
||
picList.value[idx].list = []
|
||
}
|
||
|
||
// 显示模型区域与否
|
||
const showModelArea = ref(true)
|
||
|
||
// 令摄像头拍摄一张图片加入shotList
|
||
const cameraBoxRef = ref(null)
|
||
async function handleParentShot() {
|
||
await cameraBoxRef.value?.captureShot()
|
||
}
|
||
|
||
// 训练模型
|
||
const modelAreaRef = ref(null)
|
||
async function handleTrain() {
|
||
await modelAreaRef.value.train()
|
||
}
|
||
</script>
|
||
|
||
<template>
|
||
<div class="main-splitter">
|
||
<ElSplitter>
|
||
<ElSplitterPanel size="70%" :min="200">
|
||
<div class="class-area">
|
||
<ClassBox v-for="(item, idx) in picList" :key="idx" :title="item.title" :pic-list="item.list"
|
||
:disabled="item.disabled" @add-sample="(data) => handleAddSample(idx, data)" @delete-sample="
|
||
(url) => (picList[idx].list = picList[idx].list.filter((i) => i !== url))
|
||
" @delete-class="handleDeleteClass(idx)" @disable-class="handleDisableClass(idx)"
|
||
@rename-class="(newTitle) => handleRenameClass(idx, newTitle)"
|
||
@clear-samples="handleClearSamples(idx)">
|
||
<ElButton type="primary" size="mini" @click="handleAddSample(idx)">
|
||
添加样本
|
||
</ElButton>
|
||
</ClassBox>
|
||
<ElCard class="add-class-card" @click="handleAddClass">
|
||
<div class="add-class-content">
|
||
添加一个类别
|
||
<ElIcon>
|
||
<CirclePlusFilled />
|
||
</ElIcon>
|
||
</div>
|
||
</ElCard>
|
||
</div>
|
||
</ElSplitterPanel>
|
||
<ElSplitterPanel :min="200">
|
||
<CameraBox ref="cameraBoxRef" />
|
||
<transition name="fade-scale">
|
||
<ModelArea v-if="showModelArea" ref="modelAreaRef" @shot="handleParentShot" />
|
||
</transition>
|
||
<TrainArea @train="handleTrain" />
|
||
</ElSplitterPanel>
|
||
</ElSplitter>
|
||
</div>
|
||
</template>
|
||
|
||
<style scoped>
|
||
.title {
|
||
font-size: 24px;
|
||
font-weight: 500;
|
||
color: #011216;
|
||
margin: 10px;
|
||
padding: 10px;
|
||
border-radius: 5px;
|
||
background: linear-gradient(to right, #d7dee1, transparent, transparent, transparent);
|
||
text-align: left;
|
||
}
|
||
|
||
.teachable-model-container {
|
||
width: 80%;
|
||
height: 80vh;
|
||
margin: 20px;
|
||
padding: 5px;
|
||
border: 2px solid #000;
|
||
border-radius: 5px;
|
||
box-shadow: 0px 0px 10px #000;
|
||
}
|
||
|
||
.main-splitter {
|
||
height: 100%;
|
||
width: 100%;
|
||
box-shadow: var(--el-border-color-light) 0px 0px 10px;
|
||
user-select: none;
|
||
}
|
||
|
||
.add-class-card {
|
||
margin: 10px auto;
|
||
border: 1px dotted #000;
|
||
max-width: 50vw;
|
||
cursor: default;
|
||
user-select: none;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
.add-class-card:hover {
|
||
cursor: pointer;
|
||
color: #409eff;
|
||
}
|
||
|
||
.fade-scale-enter-active {
|
||
transition: all 0.5s cubic-bezier(0.55, 0, 0.1, 1);
|
||
}
|
||
|
||
.fade-scale-leave-active {
|
||
transition: all 0.3s cubic-bezier(0.55, 0, 0.1, 1);
|
||
}
|
||
|
||
.fade-scale-enter-from {
|
||
opacity: 0;
|
||
transform: scale(0.8);
|
||
}
|
||
|
||
.fade-scale-enter-to {
|
||
opacity: 1;
|
||
transform: scale(1);
|
||
}
|
||
|
||
.fade-scale-leave-from {
|
||
opacity: 1;
|
||
transform: scale(1);
|
||
}
|
||
|
||
.fade-scale-leave-to {
|
||
opacity: 0;
|
||
transform: scale(0.8);
|
||
}
|
||
</style>
|