This commit is contained in:
BianLzhaoMin 2025-11-26 15:27:45 +08:00
parent b85889486c
commit 24affac448
5 changed files with 414 additions and 80 deletions

View File

@ -34,8 +34,8 @@ export function addImageInfoAPI(data) {
method: 'POST', method: 'POST',
data, data,
headers: { headers: {
'Content-Type': 'multipart/form-data' 'Content-Type': 'multipart/form-data',
} },
}) })
} }
@ -48,4 +48,11 @@ export function updateImageSureAPI(data) {
}) })
} }
// 修改标注
export function updateImageAnnotationAPI(data) {
return request({
url: '/image/caption/manualAnnotation',
method: 'POST',
data,
})
}

View File

@ -55,7 +55,7 @@
<el-button <el-button
type="primary" type="primary"
@click="$emit('start-upload')" @click="$emit('start-upload', null)"
:disabled=" :disabled="
fileList.length === 0 || selectedTag.length === 0 fileList.length === 0 || selectedTag.length === 0
" "

View File

@ -40,7 +40,7 @@
(file, fileList) => $emit('file-change', file, fileList) (file, fileList) => $emit('file-change', file, fileList)
" "
@remove-image="(index) => $emit('remove-image', index)" @remove-image="(index) => $emit('remove-image', index)"
@start-upload="$emit('start-upload')" @start-upload="$emit('start-upload', remarkInfo)"
/> />
</div> </div>
</div> </div>
@ -108,6 +108,17 @@ export default {
this.$emit('hand-click', result, index) this.$emit('hand-click', result, index)
}, },
}, },
watch: {
groupedImageResults: {
handler(newVal) {
if (newVal.length > 0) {
this.remarkInfo = newVal[0].remark
}
},
immediate: true,
},
},
} }
</script> </script>

View File

@ -272,7 +272,7 @@
</div> </div>
<img <img
v-else v-else
:src="currentImage.url" :src="imgSrc"
alt="preview" alt="preview"
class="annotation-preview-image" class="annotation-preview-image"
/> />
@ -280,19 +280,15 @@
<div v-if="isReAnnotating" class="annotation-type"> <div v-if="isReAnnotating" class="annotation-type">
<span class="label">标注类型</span> <span class="label">标注类型</span>
<el-select <el-radio-group v-model="annotationType" size="small">
v-model="annotationType" <el-radio-button
placeholder="请选择"
size="small"
class="annotation-type-select"
>
<el-option
v-for="option in annotationTypeOptions" v-for="option in annotationTypeOptions"
:key="option.value" :key="option.value"
:label="option.label" :label="option.value"
:value="option.value" >
/> {{ option.label }}
</el-select> </el-radio-button>
</el-radio-group>
</div> </div>
<div class="annotation-info"> <div class="annotation-info">
@ -302,18 +298,14 @@
{{ originalSize.height }} {{ originalSize.height }}
</p> </p>
<p v-if="annotationCoords"> <p v-if="annotationCoords">
左上({{ annotationCoords.x1 }}, 当前标注左上({{ annotationCoords.x1 }},
{{ annotationCoords.y1 }}) 右下({{ {{ annotationCoords.y1 }}) 右下({{
annotationCoords.x2 annotationCoords.x2
}}, {{ annotationCoords.y2 }}) }}, {{ annotationCoords.y2 }})
</p> </p>
<!-- <p v-else <p v-if="!annotationCoords && isReAnnotating">
>{{ 请先选择标注类型然后拖拽绘制标注框
isReAnnotating </p>
? '拖拽绘制标注框'
: '点击重新标注'
}}</p
> -->
</div> </div>
<div class="annotation-buttons"> <div class="annotation-buttons">
<el-button <el-button
@ -329,16 +321,61 @@
取消标注 取消标注
</el-button> </el-button>
<el-button <el-button
type="primary" type="success"
size="small" size="small"
:disabled="!annotationCoords || !annotationType" :disabled="!annotationCoords || !annotationType"
@click="emitAnnotation" @click="saveTemporaryAnnotation"
> >
提交 临时确定
</el-button>
<el-button
type="primary"
size="small"
:disabled="annotations.length === 0"
@click="submitAllAnnotations"
>
提交全部
</el-button> </el-button>
</template> </template>
</div> </div>
</div> </div>
<!-- 标注列表 -->
<div
v-if="isReAnnotating && annotations.length > 0"
class="annotation-list"
>
<div class="annotation-list-title"
>已标注列表{{ annotations.length }}</div
>
<div class="annotation-list-content">
<div
v-for="(item, index) in annotations"
:key="index"
class="annotation-list-item"
>
<span class="annotation-item-info">
<span class="annotation-item-type">{{
getTypeLabel(item.type)
}}</span>
<span class="annotation-item-coords"
>({{ item.coords.x1 }},
{{ item.coords.y1 }}) - ({{
item.coords.x2
}}, {{ item.coords.y2 }})</span
>
</span>
<el-button
type="text"
size="mini"
icon="el-icon-delete"
@click="removeAnnotation(index)"
>
删除
</el-button>
</div>
</div>
</div>
</div> </div>
<div v-else class="annotation-empty">请先选择图片</div> <div v-else class="annotation-empty">请先选择图片</div>
</el-dialog> </el-dialog>
@ -346,6 +383,10 @@
</template> </template>
<script> <script>
import {
getSelectedAPI,
updateImageAnnotationAPI,
} from '@/api/imageCaptioning/imageCaptioning'
export default { export default {
name: 'ImageResults', name: 'ImageResults',
props: { props: {
@ -396,13 +437,19 @@ export default {
currentRect: null, currentRect: null,
annotationCoords: null, annotationCoords: null,
annotationType: '', annotationType: '',
annotations: [],
annotationTypeOptions: [ annotationTypeOptions: [
{ label: '人物', value: 'person' }, // { label: '', value: 'person' },
{ label: '物体', value: 'object' }, // { label: '', value: 'object' },
{ label: '场景', value: 'scene' }, // { label: '', value: 'scene' },
], ],
imgSrc: '',
} }
}, },
created() {
this.getTags()
},
methods: { methods: {
isGroupImageSelected(groupIndex, imageIndex) { isGroupImageSelected(groupIndex, imageIndex) {
// //
@ -439,17 +486,17 @@ export default {
// //
onClickImage(image) { onClickImage(image) {
console.log('image', image)
if (!image || !image.url) { if (!image || !image.url) {
return return
} }
this.currentImage = image this.currentImage = image
this.imgSrc = image.url
this.imageDialogVisible = true this.imageDialogVisible = true
this.annotationCoords = null this.annotationCoords = null
this.annotationType = '' this.annotationType = ''
this.annotations = []
this.isReAnnotating = false this.isReAnnotating = false
this.$nextTick(() => {
this.renderBaseImage()
})
}, },
// //
@ -462,6 +509,8 @@ export default {
this.isReAnnotating = true this.isReAnnotating = true
this.annotationCoords = null this.annotationCoords = null
this.annotationType = '' this.annotationType = ''
this.imgSrc = this.currentImage.originUrl
this.annotations = []
this.$nextTick(() => { this.$nextTick(() => {
this.renderBaseImage() this.renderBaseImage()
}) })
@ -469,9 +518,16 @@ export default {
cancelReAnnotation() { cancelReAnnotation() {
this.isReAnnotating = false this.isReAnnotating = false
this.clearDrawing() //
this.annotationCoords = null this.annotationCoords = null
this.annotationType = '' this.annotationType = ''
this.annotations = []
this.currentRect = null
this.isDrawing = false
this.startPoint = null
this.imgSrc = this.currentImage.url
//
this.clearDrawing()
}, },
renderBaseImage() { renderBaseImage() {
@ -512,8 +568,12 @@ export default {
ctx.drawImage(img, offsetX, offsetY, drawWidth, drawHeight) ctx.drawImage(img, offsetX, offsetY, drawWidth, drawHeight)
this.scaleInfo = { scale, offsetX, offsetY } this.scaleInfo = { scale, offsetX, offsetY }
this.clearDrawing() this.clearDrawing()
//
this.$nextTick(() => {
this.drawCurrentRect()
})
} }
img.src = this.currentImage.url img.src = this.imgSrc
}, },
clearDrawing() { clearDrawing() {
@ -524,7 +584,12 @@ export default {
}, },
handleMouseDown(event) { handleMouseDown(event) {
if (!this.currentImage) return if (!this.currentImage || !this.annotationType) {
if (!this.annotationType) {
this.$message.warning('请先选择标注类型')
}
return
}
const position = this.getCanvasPosition(event) const position = this.getCanvasPosition(event)
this.isDrawing = true this.isDrawing = true
this.startPoint = position this.startPoint = position
@ -563,10 +628,22 @@ export default {
drawCurrentRect() { drawCurrentRect() {
const drawCanvas = this.$refs.drawCanvas const drawCanvas = this.$refs.drawCanvas
if (!drawCanvas || !this.currentRect) return if (!drawCanvas) return
const ctx = drawCanvas.getContext('2d') const ctx = drawCanvas.getContext('2d')
ctx.clearRect(0, 0, drawCanvas.width, drawCanvas.height) ctx.clearRect(0, 0, drawCanvas.width, drawCanvas.height)
ctx.strokeStyle = '#ff8800'
//
this.annotations.forEach((annotation) => {
const rect = this.convertToCanvasCoords(annotation.coords)
ctx.strokeStyle = '#ff0000'
ctx.lineWidth = 2
ctx.setLineDash([])
ctx.strokeRect(rect.x, rect.y, rect.width, rect.height)
})
//
if (this.currentRect) {
ctx.strokeStyle = '#409eff' // 线
ctx.lineWidth = 2 ctx.lineWidth = 2
ctx.setLineDash([6, 4]) ctx.setLineDash([6, 4])
ctx.strokeRect( ctx.strokeRect(
@ -576,6 +653,7 @@ export default {
this.currentRect.height, this.currentRect.height,
) )
ctx.setLineDash([]) ctx.setLineDash([])
}
}, },
calculateOriginalCoords(rect) { calculateOriginalCoords(rect) {
@ -621,25 +699,187 @@ export default {
return Math.max(min, Math.min(max, value)) return Math.max(min, Math.min(max, value))
}, },
emitAnnotation() { convertToCanvasCoords(coords) {
if ( const { scale, offsetX, offsetY } = this.scaleInfo
!this.annotationCoords || if (!scale) return { x: 0, y: 0, width: 0, height: 0 }
!this.currentImage || const x1 = coords.x1 * scale + offsetX
!this.annotationType const y1 = coords.y1 * scale + offsetY
) const x2 = coords.x2 * scale + offsetX
const y2 = coords.y2 * scale + offsetY
return {
x: Math.min(x1, x2),
y: Math.min(y1, y2),
width: Math.abs(x2 - x1),
height: Math.abs(y2 - y1),
}
},
saveTemporaryAnnotation() {
if (!this.annotationCoords || !this.annotationType) {
this.$message.warning('请先完成标注并选择类型')
return return
this.$emit('manual-annotation', { }
image: this.currentImage, this.annotations.push({
coords: this.annotationCoords, coords: { ...this.annotationCoords },
type: this.annotationType, type: this.annotationType,
typeName: this.getTypeLabel(this.annotationType),
}) })
this.$message.success('坐标已生成') this.annotationCoords = null
this.annotationType = ''
this.clearDrawing()
this.$nextTick(() => {
this.drawCurrentRect()
})
this.$message.success('标注已保存')
},
removeAnnotation(index) {
this.annotations.splice(index, 1)
//
this.currentRect = null
this.annotationCoords = null
this.isDrawing = false
//
this.$nextTick(() => {
this.clearDrawing()
this.drawCurrentRect()
})
// this.$message.success('')
},
getTypeLabel(value) {
const option = this.annotationTypeOptions.find(
(opt) => opt.value === value,
)
return option ? option.label : value
},
async submitAllAnnotations() {
if (this.annotations.length === 0) {
this.$message.warning('请至少添加一个标注')
return
}
if (!this.currentImage) {
this.$message.warning('图片信息缺失')
return
}
try {
// base64
const imageCanvas = this.$refs.imageCanvas
const drawCanvas = this.$refs.drawCanvas
if (!imageCanvas || !drawCanvas) {
this.$message.error('无法获取画布')
return
}
// canvas
const finalCanvas = document.createElement('canvas')
finalCanvas.width = this.originalSize.width
finalCanvas.height = this.originalSize.height
const finalCtx = finalCanvas.getContext('2d')
//
const img = new Image()
img.crossOrigin = 'Anonymous'
await new Promise((resolve, reject) => {
img.onload = () => {
//
finalCtx.drawImage(
img,
0,
0,
this.originalSize.width,
this.originalSize.height,
)
//
this.annotations.forEach((annotation) => {
const { coords } = annotation
finalCtx.strokeStyle = '#03FB02'
finalCtx.lineWidth = 1.5
finalCtx.strokeRect(
coords.x1,
coords.y1,
coords.x2 - coords.x1,
coords.y2 - coords.y1,
)
})
resolve()
}
img.onerror = reject
img.src = this.imgSrc
})
// base64
const base64Image = finalCanvas.toDataURL('image/png')
//
// this.$emit('manual-annotation', {
// image: this.currentImage,
// annotations: this.annotations,
// base64Image: base64Image,
// })
console.log(
'base64Image',
base64Image,
'annotations',
this.annotations,
'image',
this.currentImage,
)
// this.annotations
const tagList = new Set(this.annotations.map((e) => e.typeName))
console.log('tagList', tagList)
//
const params = {
id: this.currentImage.id,
bast64: base64Image,
jsonData: JSON.stringify(
this.annotations.map((e) => {
return {
class: e.typeName,
confidence: 0,
bbox: {
x1: e.coords.x1,
y1: e.coords.y1,
x2: e.coords.x2,
y2: e.coords.y2,
},
}
}),
),
// Setjoin
type: Array.from(tagList).join(','),
}
console.log('params', params)
const res = await updateImageAnnotationAPI(params)
console.log('res', res)
if (res.code === 200) {
this.$message.success('标注已提交')
this.cancelReAnnotation()
this.imageDialogVisible = false
}
// this.$message.success('')
//
} catch (error) {
console.error('提交标注失败:', error)
this.$message.error('提交标注失败,请重试')
}
}, },
resetAnnotation() { resetAnnotation() {
this.currentImage = null this.currentImage = null
this.annotationCoords = null this.annotationCoords = null
this.annotationType = '' this.annotationType = ''
this.annotations = []
this.isReAnnotating = false this.isReAnnotating = false
this.originalSize = { width: 0, height: 0 } this.originalSize = { width: 0, height: 0 }
this.scaleInfo = { scale: 1, offsetX: 0, offsetY: 0 } this.scaleInfo = { scale: 1, offsetX: 0, offsetY: 0 }
@ -647,6 +887,17 @@ export default {
this.startPoint = null this.startPoint = null
this.currentRect = null this.currentRect = null
}, },
//
getTags() {
getSelectedAPI().then((res) => {
// console.log('res', res)
this.annotationTypeOptions = res.data.map((item) => ({
label: item.name,
value: item.id,
}))
})
},
}, },
} }
</script> </script>
@ -751,11 +1002,13 @@ export default {
width: 100%; width: 100%;
display: flex; display: flex;
align-items: center; align-items: center;
gap: 8px; gap: 12px;
} }
.annotation-type-select { .annotation-type .label {
flex: 1; font-weight: 500;
color: #333;
white-space: nowrap;
} }
.annotation-info { .annotation-info {
@ -782,6 +1035,66 @@ export default {
padding: 30px 0; padding: 30px 0;
} }
.annotation-list {
width: 100%;
border: 1px solid #e4e7ed;
border-radius: 4px;
background: #f5f7fa;
}
.annotation-list-title {
padding: 12px 16px;
background: #e4e7ed;
font-weight: 500;
color: #333;
border-bottom: 1px solid #e4e7ed;
}
.annotation-list-content {
max-height: 200px;
overflow-y: auto;
}
.annotation-list-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 10px 16px;
border-bottom: 1px solid #e4e7ed;
transition: background-color 0.2s;
}
.annotation-list-item:last-child {
border-bottom: none;
}
.annotation-list-item:hover {
background-color: #ecf5ff;
}
.annotation-item-info {
display: flex;
align-items: center;
gap: 12px;
flex: 1;
}
.annotation-item-type {
padding: 2px 8px;
background: #409eff;
color: #fff;
border-radius: 3px;
font-size: 12px;
font-weight: 500;
white-space: nowrap;
}
.annotation-item-coords {
color: #606266;
font-size: 13px;
font-family: monospace;
}
.icon-hand { .icon-hand {
position: absolute; position: absolute;
top: 25px; top: 25px;

View File

@ -179,7 +179,7 @@ export default {
this.addId = '' this.addId = ''
}, },
startUpload() { startUpload(remarkInfo = '') {
console.log('开始上传') console.log('开始上传')
if (this.selectedTag.length === 0) { if (this.selectedTag.length === 0) {
this.$message.warning('请选择需要标注的内容标签') this.$message.warning('请选择需要标注的内容标签')
@ -224,7 +224,7 @@ export default {
// params // params
const params = { const params = {
param: selectedTagNames, param: selectedTagNames,
remark: this.remarkInfo, remark: remarkInfo || this.remarkInfo,
id: this.addId, id: this.addId,
} }
formData.append('params', JSON.stringify(params)) formData.append('params', JSON.stringify(params))
@ -250,6 +250,7 @@ export default {
operaName: record.operaName, operaName: record.operaName,
images: record.fileVoList.map((item) => ({ images: record.fileVoList.map((item) => ({
url: item.bjUrl || '未找到图片地址', url: item.bjUrl || '未找到图片地址',
originUrl: item.imageUrl,
id: item.imageId || item.id, id: item.imageId || item.id,
name: item.originalName, name: item.originalName,
contentImage: item.contentImage, contentImage: item.contentImage,
@ -511,7 +512,8 @@ export default {
index: index, // index: index, //
operaName: record.operaName, operaName: record.operaName,
images: record.fileVoList.map((item) => ({ images: record.fileVoList.map((item) => ({
url: item.bjUrl || item.url, url: item.bjUrl,
originUrl: item.imageUrl,
id: item.imageId || item.id, id: item.imageId || item.id,
name: item.originalName, name: item.originalName,
contentImage: item.contentImage, contentImage: item.contentImage,
@ -524,6 +526,7 @@ export default {
isSure: record.isSure, isSure: record.isSure,
operId: record.operaId, operId: record.operaId,
id: record.id, id: record.id,
remark: record.remark,
}), }),
) )