Browse Source

feat: 数据标注

main
少轻狂 2 years ago
parent
commit
d253c19deb
No known key found for this signature in database GPG Key ID: 80E176093D33A9AB
5 changed files with 617 additions and 20 deletions
  1. +1
    -0
      .vscode/settings.json
  2. +120
    -0
      app/api/annotation_data/data.json
  3. +145
    -0
      app/api/annotation_data/dataset.json
  4. +49
    -6
      app/api/api.py
  5. +302
    -14
      src/pages/Personalization/annotations.vue

+ 1
- 0
.vscode/settings.json View File

@@ -10,6 +10,7 @@
"pnpm",
"pyinstaller",
"pywebview",
"supercategory",
"unocss",
"unplugin",
"Vite",


+ 120
- 0
app/api/annotation_data/data.json View File

@@ -0,0 +1,120 @@
{
"2022-12-22-15-30-01.jpg": {
"image": {
"file_name": "2022-12-22-15-30-01.jpg",
"height": 480,
"width": 640
},
"annotation": [
{
"area": 690,
"iscrowd": 0,
"bbox": [
357,
268,
46,
15
],
"category_id": 3,
"ignore": 0
},
{
"area": 946,
"iscrowd": 0,
"bbox": [
453,
260,
43,
22
],
"category_id": 3,
"ignore": 0
},
{
"area": 4900,
"iscrowd": 0,
"bbox": [
391,
353,
98,
50
],
"category_id": 2,
"ignore": 0
}
]
},
"2022-12-22-15-38-16.jpg": {
"image": {
"file_name": "2022-12-22-15-38-16.jpg",
"height": 480,
"width": 640
},
"annotation": [
{
"area": 680,
"iscrowd": 0,
"bbox": [
211,
356,
40,
17
],
"category_id": 3,
"ignore": 0
},
{
"area": 728,
"iscrowd": 0,
"bbox": [
289,
347,
52,
14
],
"category_id": 3,
"ignore": 0
}
]
},
"2022-12-22-15-44-48.jpg": {
"image": {
"file_name": "2022-12-22-15-44-48.jpg",
"height": 480,
"width": 640
},
"annotation": []
},
"2022-12-22-15-44-49.jpg": {
"image": {
"file_name": "2022-12-22-15-44-49.jpg",
"height": 480,
"width": 640
},
"annotation": []
},
"2022-12-22-15-44-55.jpg": {
"image": {
"file_name": "2022-12-22-15-44-55.jpg",
"height": 480,
"width": 640
},
"annotation": []
},
"2022-12-26-10-03-41.jpg": {
"image": {
"file_name": "2022-12-26-10-03-41.jpg",
"height": 480,
"width": 640
},
"annotation": []
},
"2022-12-26-10-40-15.jpg": {
"image": {
"file_name": "2022-12-26-10-40-15.jpg",
"height": 480,
"width": 640
},
"annotation": []
}
}

+ 145
- 0
app/api/annotation_data/dataset.json View File

@@ -0,0 +1,145 @@
{
"images": [
{
"file_name": "2022-12-22-15-30-01.jpg",
"height": 480,
"width": 640,
"id": 0
},
{
"file_name": "2022-12-22-15-38-16.jpg",
"height": 480,
"width": 640,
"id": 1
},
{
"file_name": "2022-12-22-15-44-48.jpg",
"height": 480,
"width": 640,
"id": 2
},
{
"file_name": "2022-12-22-15-44-49.jpg",
"height": 480,
"width": 640,
"id": 3
},
{
"file_name": "2022-12-22-15-44-55.jpg",
"height": 480,
"width": 640,
"id": 4
},
{
"file_name": "2022-12-26-10-03-41.jpg",
"height": 480,
"width": 640,
"id": 5
},
{
"file_name": "2022-12-26-10-40-15.jpg",
"height": 480,
"width": 640,
"id": 6
}
],
"type": "instances",
"annotations": [
{
"area": 690,
"iscrowd": 0,
"bbox": [
357,
268,
46,
15
],
"category_id": 3,
"ignore": 0,
"image_id": 0,
"id": 0
},
{
"area": 946,
"iscrowd": 0,
"bbox": [
453,
260,
43,
22
],
"category_id": 3,
"ignore": 0,
"image_id": 0,
"id": 1
},
{
"area": 4900,
"iscrowd": 0,
"bbox": [
391,
353,
98,
50
],
"category_id": 2,
"ignore": 0,
"image_id": 0,
"id": 2
},
{
"area": 680,
"iscrowd": 0,
"bbox": [
211,
356,
40,
17
],
"category_id": 3,
"ignore": 0,
"image_id": 1,
"id": 3
},
{
"area": 728,
"iscrowd": 0,
"bbox": [
289,
347,
52,
14
],
"category_id": 3,
"ignore": 0,
"image_id": 1,
"id": 4
}
],
"categories": [
{
"supercategory": "none",
"id": 1,
"name": "closed_eye",
"color": "#E3170D85"
},
{
"supercategory": "none",
"id": 2,
"name": "closed_mouth",
"color": "#1E90FF85"
},
{
"supercategory": "none",
"id": 3,
"name": "open_eye",
"color": "#32CD3285"
},
{
"supercategory": "none",
"id": 4,
"name": "open_mouth",
"color": "#C0C0C085"
}
]
}

+ 49
- 6
app/api/api.py View File

@@ -148,7 +148,7 @@ class API:
if(os.path.exists(getFile("primary_img"))==False):
#不存在则创建
os.mkdir(getFile("primary_img"))
#返回该文件夹下png文件的数量和列表
#返回该文件夹下jpg文件的数量和列表
return [len(os.listdir(getFile("primary_img"))),os.listdir(getFile("primary_img"))]
def setPersonalizationPrimaryImg(self,type="不裁剪"):
#判断是否存在"primary_img"文件夹
@@ -159,7 +159,7 @@ class API:
# 图片名字是当前时间
# 三种处理图片的方式:不裁剪、居中裁剪成224*224、居中裁剪成320*320。
if(type=="不裁剪"):
cv2.imwrite(getFile("primary_img")+"/"+time.strftime("%Y-%m-%d-%H-%M-%S", time.localtime())+".png",get_photo(self.cap))
cv2.imwrite(getFile("primary_img")+"/"+time.strftime("%Y-%m-%d-%H-%M-%S", time.localtime())+".jpg",get_photo(self.cap))
elif(type=="居中裁剪成224*224"):
img = get_photo(self.cap)
height=len(img)
@@ -172,7 +172,7 @@ class API:
x2 = x0+112
y2 = y0+112
img = img[y1:y2, x1:x2]
cv2.imwrite(getFile("primary_img")+"/"+time.strftime("%Y-%m-%d-%H-%M-%S", time.localtime())+".png",img)
cv2.imwrite(getFile("primary_img")+"/"+time.strftime("%Y-%m-%d-%H-%M-%S", time.localtime())+".jpg",img)
elif(type=="居中裁剪成320*320"):
img = get_photo(self.cap)
height=len(img)
@@ -185,8 +185,8 @@ class API:
x2 = x0+160
y2 = y0+160
img = img[y1:y2, x1:x2]
cv2.imwrite(getFile("primary_img")+"/"+time.strftime("%Y-%m-%d-%H-%M-%S", time.localtime())+".png",img)
#返回该文件夹下png文件的数量和列表
cv2.imwrite(getFile("primary_img")+"/"+time.strftime("%Y-%m-%d-%H-%M-%S", time.localtime())+".jpg",img)
#返回该文件夹下jpg文件的数量和列表
return [len(os.listdir(getFile("primary_img"))),os.listdir(getFile("primary_img"))]
def openPrimaryImgFiles(self):
#判断是否存在"primary_img"文件夹
@@ -194,4 +194,47 @@ class API:
#不存在则创建
os.mkdir(getFile("primary_img"))
#打开文件夹
os.startfile(getFile("primary_img"))
os.startfile(getFile("primary_img"))
def getPrimaryListImg(self,img_name):
#判断是否存在"primary_img"文件夹
if(os.path.exists(getFile("primary_img"))==False):
#不存在则创建
os.mkdir(getFile("primary_img"))
#读取图片
img = cv2.imread(getFile("primary_img")+"/"+img_name)
#返回图片
return mat2base64(img)
def saveAnnotationData(self,data,dataset):
#判断是否存在"annotation_data"文件夹
if(os.path.exists(getFile("annotation_data"))==False):
#不存在则创建
os.mkdir(getFile("annotation_data"))
#保存COCO数据集
with open(getFile("annotation_data")+"/"+"dataset.json",'w',encoding='utf8')as fp:
json.dump(dataset,fp,ensure_ascii=False,indent=4)
#保存标注数据
with open(getFile("annotation_data")+"/"+"data.json",'w',encoding='utf8')as fp:
json.dump(data,fp,ensure_ascii=False,indent=4)
def getAnnotationData(self):
#判断是否存在"annotation_data"文件夹
if(os.path.exists(getFile("annotation_data"))==False):
#不存在则创建
os.mkdir(getFile("annotation_data"))
#判断是否存在"dataset.json"文件
if(os.path.exists(getFile("annotation_data")+"/"+"dataset.json")):
#存在则读取
with open(getFile("annotation_data")+"/"+"dataset.json",'r',encoding='utf8')as fp:
dataset = json.load(fp)
else:
#不存在则创建
return {}
#判断是否存在"data.json"文件
if(os.path.exists(getFile("annotation_data")+"/"+"data.json")):
#存在则读取
with open(getFile("annotation_data")+"/"+"data.json",'r',encoding='utf8')as fp:
data = json.load(fp)
else:
#不存在则创建
return {}
#返回数据
return {"mergeData":dataset,"annotationData":data}

+ 302
- 14
src/pages/Personalization/annotations.vue View File

@@ -1,5 +1,268 @@
<script setup>
import ToastCreator from '~/components/daisy/DToast/toastCreator'
const { t } = useI18n()
let ImgList = $ref([])
let currentImg = $ref('')
let editImg = $ref()
const getImgFolder = async () => {
ImgList = (await window.pywebview.api.getPersonalizationPrimaryImg())[1]
currentImg = 0
}

// onBeforeMount(async () => {
// // await getImgFolder()
// // await getEditImg()
// })

let Drag = $ref(false)

const categories = [
{ supercategory: 'none', id: 1, name: 'closed_eye', color: '#E3170D85' },
{ supercategory: 'none', id: 2, name: 'closed_mouth', color: '#1E90FF85' },
{ supercategory: 'none', id: 3, name: 'open_eye', color: '#32CD3285' },
{ supercategory: 'none', id: 4, name: 'open_mouth', color: '#C0C0C085' },
]

let classIndex = $ref(2)
const changeIndex = (index) => {
classIndex = index
}

const saveData = reactive({
image: {
file_name: '',
height: 320,
width: 320,
},
annotation: [],
})

const expertData = reactive({
annotationData: {}, // 前端格式
mergeData: {
// COCO格式
images: [],
type: 'instances',
annotations: [],
categories,
},
})

// 绑定DOM元素
const ImgLayer = $ref()
const TempLayer = $ref()
const DrawLayer = $ref()
let DrawCtx = $ref()
let myContext = $ref()
let TempCtx = $ref()
const canvasContainer = $ref() // 画布容器

// 变量
const editData = reactive({
startX: 0,
startY: 0,
cRect: null,
offsetX: null,
offsetY: null,
})

const saveImgAnnotationData = () => {
expertData.annotationData[saveData.image.file_name] = JSON.parse(
JSON.stringify(saveData),
)
// saveData.annotation = [];
// console.log(expertData.annotationData);
const toast = new ToastCreator({ message: '图片标注结果已保存', type: 'success', duration: 1500 })
toast.createToast()
}

const clearCanvas = (isSave = true) => {
DrawCtx.clearRect(0, 0, DrawLayer.width, DrawLayer.height)
saveData.annotation = []
if (saveData.image.file_name !== '' && isSave)
saveImgAnnotationData()
}

const getEditImg = async (num = 0) => {
if (currentImg + num < ImgList.length && currentImg + num >= 0) {
currentImg += num
}
else {
(new ToastCreator({
message: '到底/顶啦!',
type: 'success',
duration: 1500,
})).createToast()
}
const imgData = (await window.pywebview.api.getPrimaryListImg(ImgList[currentImg]))
editImg = `data:image/png;base64,${imgData}`
const myImage = new Image()
myImage.src = editImg
// console.log(e.target.result)
myImage.onload = function (ev) {
clearCanvas(false)
saveData.image.file_name = ImgList[currentImg]
saveData.image.height = myImage.height
saveData.image.width = myImage.width

DrawLayer.setAttribute('width', myImage.width)
DrawLayer.setAttribute('height', myImage.height)
TempLayer.setAttribute('width', myImage.width)
TempLayer.setAttribute('height', myImage.height)
ImgLayer.setAttribute('width', myImage.width)
ImgLayer.setAttribute('height', myImage.height)
canvasContainer.style.width = `${myImage.width}px`
canvasContainer.style.height = `${myImage.height}px`

// 将图片读入canvas

myContext.drawImage(
myImage,
0,
0,
myImage.width,
myImage.height,
0,
0,
myImage.width,
myImage.height,
)

TempCtx.fillStyle = categories[classIndex].color
DrawCtx.fillStyle = categories[classIndex].color

// let imgData = ImgLayer.toDataURL("image/jpeg",0.75)
// console.log(imgData)

// 读取已有的标注数据
if (expertData.annotationData[saveData.image.file_name]) {
saveData.annotation = expertData.annotationData[
saveData.image.file_name
].annotation
saveData.annotation.forEach((item) => {
// 更换画笔颜色
DrawCtx.fillStyle = categories[item.category_id - 1].color
DrawCtx.fillRect(
item.bbox[0],
item.bbox[1],
item.bbox[2],
item.bbox[3],
)
})
}
}
}

const DrawLayerMouseup = (e) => {
Drag = false

const mouseX = Math.round(e.clientX - editData.offsetX)
const mouseY = Math.round(e.clientY - editData.offsetY)

const Width = mouseX - editData.startX
const Height = mouseY - editData.startY

TempCtx.clearRect(0, 0, TempLayer.width, TempLayer.height)
DrawCtx.fillRect(editData.startX, editData.startY, Width, Height)

const annotation = {
area: Math.round(Width * Height),
iscrowd: 0,
bbox: [editData.startX, editData.startY, Width, Height],
category_id: categories[classIndex].id,
ignore: 0,
// "image_id": saveData.image.id,
// "id": saveData.annotation.length
}
if (annotation.area !== 0)
saveData.annotation.push(annotation)
}

const DrawLayerMousedown = (e) => {
editData.cRect = TempLayer.getBoundingClientRect()
editData.offsetX = editData.cRect.left
editData.offsetY = editData.cRect.top
const cX = Math.round(e.clientX - editData.offsetX)
const cY = Math.round(e.clientY - editData.offsetY)

Drag = true
editData.startX = cX
editData.startY = cY

TempCtx.fillStyle = categories[classIndex].color
DrawCtx.fillStyle = categories[classIndex].color
}

const DrawLayerMousemove = (e) => {
if (Drag === true) {
const mouseX = Math.round(e.clientX - editData.offsetX)
const mouseY = Math.round(e.clientY - editData.offsetY)

const Width = mouseX - editData.startX
const Height = mouseY - editData.startY

TempCtx.clearRect(
0,
0,
TempLayer.width,
TempLayer.height,
)
TempCtx.fillRect(editData.startX, editData.startY, Width, Height)
}
}

const expertAllData = async () => {
const imgData = []
let annotationData = []
let id = 0
let anno_id = 0
for (const key in expertData.annotationData) {
const img = JSON.parse(
JSON.stringify(expertData.annotationData[key].image),
)
img.id = id
imgData.push(img)
const annotation = JSON.parse(
JSON.stringify(expertData.annotationData[key].annotation),
)
for (let i = 0; i < annotation.length; i++) {
annotation[i].image_id = id
annotation[i].id = anno_id
anno_id++
}
annotationData = [...annotationData, ...annotation]
id++
}
expertData.mergeData.images = imgData
expertData.mergeData.annotations = annotationData
console.log(JSON.parse(JSON.stringify(expertData.mergeData)))
await window.pywebview.api.saveAnnotationData(expertData.annotationData, expertData.mergeData)

const toast = new ToastCreator({ message: '数据集标注结果已保存', type: 'success', duration: 1500 })
toast.createToast()
}

watch(
() => classIndex,
(newVal, oldVal) => {
TempCtx.fillStyle = categories[newVal].color
DrawCtx.fillStyle = categories[newVal].color
},
)
onBeforeMount(async () => {
const getAnnotationData = await window.pywebview.api.getAnnotationData()
if (JSON.stringify(getAnnotationData) !== '{}') {
expertData.annotationData = getAnnotationData.annotationData
expertData.mergeData = getAnnotationData.mergeData
}
})
onMounted(async () => {
await getImgFolder()
await getEditImg()
myContext = ImgLayer.getContext('2d')
DrawCtx = DrawLayer.getContext('2d')
TempCtx = TempLayer.getContext('2d')
})
</script>

<template>
@@ -41,15 +304,21 @@ const { t } = useI18n()
<li class="menu-title">
<span>标注类别-眼睛</span>
</li>
<li>
<a class="active"><div i-mingcute-eye-line />眼睛开</a>
<li @click="changeIndex(2)">
<a :class="classIndex === 2 ? 'active' : ''"><div i-mingcute-eye-line />眼睛开</a>
</li>
<li @click="changeIndex(0)">
<a :class="classIndex === 0 ? 'active' : ''"><div i-mingcute-eye-close-fill />眼睛闭</a>
</li>
<li><a><div i-mingcute-eye-close-fill />眼睛闭</a></li>
<li class="menu-title">
<span>标注类别-嘴巴</span>
</li>
<li><a><div i-icon-park-outline-surprised-face-with-open-big-mouth />嘴巴开</a></li>
<li><a><div i-icon-park-outline-face-without-mouth />嘴巴闭</a></li>
<li @click="changeIndex(3)">
<a :class="classIndex === 3 ? 'active' : ''"><div i-icon-park-outline-surprised-face-with-open-big-mouth />嘴巴开</a>
</li>
<li @click="changeIndex(1)">
<a :class="classIndex === 1 ? 'active' : ''"><div i-icon-park-outline-face-without-mouth />嘴巴闭</a>
</li>
</ul>
</div>
<div />
@@ -59,7 +328,7 @@ const { t } = useI18n()
<div i-ri-quill-pen-fill />已标记
</div>
<div class="stat-value">
200
{{ ImgList.length }}
</div>
</div>

@@ -68,32 +337,42 @@ const { t } = useI18n()
<div i-ep-warn-triangle-filled />未标记
</div>
<div class="stat-value">
18
{{ ImgList.length - Object.keys(expertData.annotationData).length }}
</div>
</div>
</div>
</div>
<div class="label-center flex ">
<img class="max-w-full h-100 rounded-lg bg-contain bg-clip-border" src="/Snipaste_2022-12-01_22-59-42.png" alt="image description">
<!-- <img class="max-w-full h-100 rounded-lg bg-contain bg-clip-border" :src="editImg" alt="image description"> -->
<div ref="canvasContainer" class="container border" style="width:320px;height:320px;">
<div class="CanvasWrapper">
<canvas ref="ImgLayer" width="320" height="320" />
<canvas ref="TempLayer" width="320" height="320" />
<canvas
ref="DrawLayer" width="320" height="320" @mousemove="DrawLayerMousemove"
@mouseup="DrawLayerMouseup" @mousedown="DrawLayerMousedown"
/>
</div>
</div>
</div>
<div class="label-right ml-4 flex flex-col space-y-6">
<div class="tooltip tooltip-right tooltip-primary" data-tip="上一张">
<button class="btn btn-square btn-primary ">
<button class="btn btn-square btn-primary " @click="getEditImg(-1)">
<div i-material-symbols-keyboard-double-arrow-up-rounded text-2xl />
</button>
</div>
<div class="tooltip tooltip-right tooltip-primary" data-tip="清除">
<button class="btn btn-square btn-primary ">
<button class="btn btn-square btn-primary " @click="clearCanvas">
<div i-carbon-clean text-2xl />
</button>
</div>
<div class="tooltip tooltip-right tooltip-primary" data-tip="保存">
<div class="tooltip tooltip-right tooltip-primary" data-tip="保存" @click="saveImgAnnotationData">
<button class="btn btn-square btn-primary ">
<div i-material-symbols-save-as-rounded text-2xl />
</button>
</div>
<div class="tooltip tooltip-right tooltip-primary" data-tip="下一张">
<button class="btn btn-square btn-primary ">
<button class="btn btn-square btn-primary " @click="getEditImg(1)">
<div i-material-symbols-keyboard-double-arrow-down-rounded text-2xl />
</button>
</div>
@@ -128,12 +407,12 @@ const { t } = useI18n()
</div>
<div class="flex-col items-end">
<div class="btn-group">
<button class="btn hover:btn-primary">
<button class="btn hover:btn-primary" @click="expertAllData()">
<router-link to="/Personalization/train" class="flex items-center">
<div i-mingcute-cloud-line />云端训练
</router-link>
</button>
<button class="btn hover:btn-primary">
<button class="btn hover:btn-primary" @click="expertAllData()">
<router-link to="/Personalization/train" class="flex items-center">
<div i-mingcute-computer-line />本地训练
</router-link>
@@ -214,4 +493,13 @@ const { t } = useI18n()
padding-top: .5rem;
padding-bottom: .5rem;
}

.CanvasWrapper {
position: relative;
width: 100%;
}

.CanvasWrapper canvas {
position: absolute;
}
</style>

Loading…
Cancel
Save