This commit is contained in:
parent
b85889486c
commit
24affac448
|
|
@ -34,8 +34,8 @@ export function addImageInfoAPI(data) {
|
|||
method: 'POST',
|
||||
data,
|
||||
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,
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -55,7 +55,7 @@
|
|||
|
||||
<el-button
|
||||
type="primary"
|
||||
@click="$emit('start-upload')"
|
||||
@click="$emit('start-upload', null)"
|
||||
:disabled="
|
||||
fileList.length === 0 || selectedTag.length === 0
|
||||
"
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
|
|
@ -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,10 +628,22 @@ 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'
|
||||
|
||||
// 绘制已保存的标注框
|
||||
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(
|
||||
|
|
@ -576,6 +653,7 @@ export default {
|
|||
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,
|
||||
},
|
||||
}
|
||||
}),
|
||||
),
|
||||
// 获取标签去重后的数据(Set转数组后join)
|
||||
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;
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}),
|
||||
)
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue