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

@ -11,11 +11,11 @@ export function getSelectedAPI(data) {
// 查询左侧历史记录
export function getImageListAPI(data) {
return request({
url: '/image/caption/getImageList',
method: 'POST',
data,
})
return request({
url: '/image/caption/getImageList',
method: 'POST',
data,
})
}
//查询右侧的历史记录详情
@ -29,23 +29,30 @@ export function getImageListDetailsAPI(data) {
//新增标注
export function addImageInfoAPI(data) {
return request({
url: '/image/caption/addImageInfo',
method: 'POST',
data,
headers: {
'Content-Type': 'multipart/form-data'
}
})
return request({
url: '/image/caption/addImageInfo',
method: 'POST',
data,
headers: {
'Content-Type': 'multipart/form-data',
},
})
}
//修改图片
export function updateImageSureAPI(data) {
return request({
url: '/image/caption/updateImageSure',
method: 'POST',
data,
})
return request({
url: '/image/caption/updateImageSure',
method: 'POST',
data,
})
}
// 修改标注
export function updateImageAnnotationAPI(data) {
return request({
url: '/image/caption/manualAnnotation',
method: 'POST',
data,
})
}

View File

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

View File

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

View File

@ -272,7 +272,7 @@
</div>
<img
v-else
:src="currentImage.url"
:src="imgSrc"
alt="preview"
class="annotation-preview-image"
/>
@ -280,19 +280,15 @@
<div v-if="isReAnnotating" class="annotation-type">
<span class="label">标注类型</span>
<el-select
v-model="annotationType"
placeholder="请选择"
size="small"
class="annotation-type-select"
>
<el-option
<el-radio-group v-model="annotationType" size="small">
<el-radio-button
v-for="option in annotationTypeOptions"
:key="option.value"
:label="option.label"
:value="option.value"
/>
</el-select>
:label="option.value"
>
{{ option.label }}
</el-radio-button>
</el-radio-group>
</div>
<div class="annotation-info">
@ -302,18 +298,14 @@
{{ originalSize.height }}
</p>
<p v-if="annotationCoords">
左上({{ annotationCoords.x1 }},
当前标注左上({{ annotationCoords.x1 }},
{{ annotationCoords.y1 }}) 右下({{
annotationCoords.x2
}}, {{ annotationCoords.y2 }})
</p>
<!-- <p v-else
>{{
isReAnnotating
? '拖拽绘制标注框'
: '点击重新标注'
}}</p
> -->
<p v-if="!annotationCoords && isReAnnotating">
请先选择标注类型然后拖拽绘制标注框
</p>
</div>
<div class="annotation-buttons">
<el-button
@ -329,16 +321,61 @@
取消标注
</el-button>
<el-button
type="primary"
type="success"
size="small"
:disabled="!annotationCoords || !annotationType"
@click="emitAnnotation"
@click="saveTemporaryAnnotation"
>
提交
临时确定
</el-button>
<el-button
type="primary"
size="small"
:disabled="annotations.length === 0"
@click="submitAllAnnotations"
>
提交全部
</el-button>
</template>
</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 v-else class="annotation-empty">请先选择图片</div>
</el-dialog>
@ -346,6 +383,10 @@
</template>
<script>
import {
getSelectedAPI,
updateImageAnnotationAPI,
} from '@/api/imageCaptioning/imageCaptioning'
export default {
name: 'ImageResults',
props: {
@ -396,13 +437,19 @@ export default {
currentRect: null,
annotationCoords: null,
annotationType: '',
annotations: [],
annotationTypeOptions: [
{ label: '人物', value: 'person' },
{ label: '物体', value: 'object' },
{ label: '场景', value: 'scene' },
// { label: '', value: 'person' },
// { label: '', value: 'object' },
// { label: '', value: 'scene' },
],
imgSrc: '',
}
},
created() {
this.getTags()
},
methods: {
isGroupImageSelected(groupIndex, imageIndex) {
//
@ -439,17 +486,17 @@ export default {
//
onClickImage(image) {
console.log('image', image)
if (!image || !image.url) {
return
}
this.currentImage = image
this.imgSrc = image.url
this.imageDialogVisible = true
this.annotationCoords = null
this.annotationType = ''
this.annotations = []
this.isReAnnotating = false
this.$nextTick(() => {
this.renderBaseImage()
})
},
//
@ -462,6 +509,8 @@ export default {
this.isReAnnotating = true
this.annotationCoords = null
this.annotationType = ''
this.imgSrc = this.currentImage.originUrl
this.annotations = []
this.$nextTick(() => {
this.renderBaseImage()
})
@ -469,9 +518,16 @@ export default {
cancelReAnnotation() {
this.isReAnnotating = false
this.clearDrawing()
//
this.annotationCoords = null
this.annotationType = ''
this.annotations = []
this.currentRect = null
this.isDrawing = false
this.startPoint = null
this.imgSrc = this.currentImage.url
//
this.clearDrawing()
},
renderBaseImage() {
@ -512,8 +568,12 @@ export default {
ctx.drawImage(img, offsetX, offsetY, drawWidth, drawHeight)
this.scaleInfo = { scale, offsetX, offsetY }
this.clearDrawing()
//
this.$nextTick(() => {
this.drawCurrentRect()
})
}
img.src = this.currentImage.url
img.src = this.imgSrc
},
clearDrawing() {
@ -524,7 +584,12 @@ export default {
},
handleMouseDown(event) {
if (!this.currentImage) return
if (!this.currentImage || !this.annotationType) {
if (!this.annotationType) {
this.$message.warning('请先选择标注类型')
}
return
}
const position = this.getCanvasPosition(event)
this.isDrawing = true
this.startPoint = position
@ -563,19 +628,32 @@ export default {
drawCurrentRect() {
const drawCanvas = this.$refs.drawCanvas
if (!drawCanvas || !this.currentRect) return
if (!drawCanvas) return
const ctx = drawCanvas.getContext('2d')
ctx.clearRect(0, 0, drawCanvas.width, drawCanvas.height)
ctx.strokeStyle = '#ff8800'
ctx.lineWidth = 2
ctx.setLineDash([6, 4])
ctx.strokeRect(
this.currentRect.x,
this.currentRect.y,
this.currentRect.width,
this.currentRect.height,
)
ctx.setLineDash([])
//
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.setLineDash([6, 4])
ctx.strokeRect(
this.currentRect.x,
this.currentRect.y,
this.currentRect.width,
this.currentRect.height,
)
ctx.setLineDash([])
}
},
calculateOriginalCoords(rect) {
@ -621,25 +699,187 @@ export default {
return Math.max(min, Math.min(max, value))
},
emitAnnotation() {
if (
!this.annotationCoords ||
!this.currentImage ||
!this.annotationType
)
convertToCanvasCoords(coords) {
const { scale, offsetX, offsetY } = this.scaleInfo
if (!scale) return { x: 0, y: 0, width: 0, height: 0 }
const x1 = coords.x1 * scale + offsetX
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
this.$emit('manual-annotation', {
image: this.currentImage,
coords: this.annotationCoords,
}
this.annotations.push({
coords: { ...this.annotationCoords },
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() {
this.currentImage = null
this.annotationCoords = null
this.annotationType = ''
this.annotations = []
this.isReAnnotating = false
this.originalSize = { width: 0, height: 0 }
this.scaleInfo = { scale: 1, offsetX: 0, offsetY: 0 }
@ -647,6 +887,17 @@ export default {
this.startPoint = 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>
@ -751,11 +1002,13 @@ export default {
width: 100%;
display: flex;
align-items: center;
gap: 8px;
gap: 12px;
}
.annotation-type-select {
flex: 1;
.annotation-type .label {
font-weight: 500;
color: #333;
white-space: nowrap;
}
.annotation-info {
@ -782,6 +1035,66 @@ export default {
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 {
position: absolute;
top: 25px;

View File

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