Compare commits

..

2 Commits

Author SHA1 Message Date
LHD_HY 82370d687c Merge remote-tracking branch 'origin/main' 2026-02-06 09:08:12 +08:00
LHD_HY 228cedab07 bug修改 2026-02-06 09:07:32 +08:00
7 changed files with 429 additions and 258 deletions

View File

@ -8,7 +8,7 @@
<el-button icon="el-icon-arrow-right" @click="nextImage" title="下一张" :disabled="imageList.length <= 1"></el-button> <el-button icon="el-icon-arrow-right" @click="nextImage" title="下一张" :disabled="imageList.length <= 1"></el-button>
<el-button icon="el-icon-zoom-in" @click="zoomIn" title="放大"></el-button> <el-button icon="el-icon-zoom-in" @click="zoomIn" title="放大"></el-button>
<el-button icon="el-icon-zoom-out" @click="zoomOut" title="缩小"></el-button> <el-button icon="el-icon-zoom-out" @click="zoomOut" title="缩小"></el-button>
<el-button icon="el-icon-refresh" @click="resetZoom" title="重置视图"></el-button> <!-- <el-button icon="el-icon-refresh" @click="resetZoom" title="重置视图"></el-button>-->
</el-button-group> </el-button-group>
</div> </div>
@ -121,7 +121,10 @@ import {
submitAuditResultAPI // submitAuditResultAPI //
} from '@/api/data/audit' } from '@/api/data/audit'
import {decryptWithSM4} from "@/utils/sm"; import {decryptWithSM4} from "@/utils/sm";
import {
getLabelingTaskLabelTreeAPI,
getLabelPicturesDetailAPI
} from '@/api/data/labeling';
export default { export default {
name: 'LabelingAudit', name: 'LabelingAudit',
data() { data() {
@ -131,6 +134,17 @@ export default {
polygon: '#0099ff', polygon: '#0099ff',
circle: '#ff9900' circle: '#ff9900'
}, },
// /
labelDrawConfig: {
baseLineWidth: 3,
minLineWidth: 1,
maxLineWidth: 6,
textFontSize: 16,
textPadding: 3,
bgOpacity: 0.4, //
shapeFillOpacity: 0.1, //
textColor: "#ffffff"
},
imageList: [], imageList: [],
currentImageIndex: 0, currentImageIndex: 0,
currentImage: {}, currentImage: {},
@ -139,10 +153,12 @@ export default {
labelLoading: false, labelLoading: false,
ailabelInstance: null, ailabelInstance: null,
zoomScale: 1.0, zoomScale: 1.0,
//
shapeLabelMap: {},
labelShapeMap: {},
// //
labelingTaskId: this.convertToNumber(decryptWithSM4(this.$route.query.labelingTaskId || '')), labelingTaskId: this.convertToNumber(decryptWithSM4(this.$route.query.labelingTaskId || '')),
isAuditing: false, // isAuditing: false, //
//
rejectDialogVisible: false, rejectDialogVisible: false,
auditRemark: '' // auditRemark: '' //
} }
@ -157,7 +173,7 @@ export default {
this.loadLabelTreeData() this.loadLabelTreeData()
}, },
methods: { methods: {
// //
convertToNumber(value) { convertToNumber(value) {
if (!value || value === '' || value === 'undefined' || value === 'null') { if (!value || value === '' || value === 'undefined' || value === 'null') {
return ''; return '';
@ -166,7 +182,46 @@ export default {
return isNaN(num) ? '' : num; return isNaN(num) ? '' : num;
}, },
// // 116RGBA#fff/#ffffff
hexToRgba(hex, opacity) {
const cleanHex = hex.replace('#', '');
const fullHex = cleanHex.length === 3
? cleanHex.split('').map(c => c + c).join('')
: cleanHex;
const r = parseInt(fullHex.substring(0, 2), 16);
const g = parseInt(fullHex.substring(2, 4), 16);
const b = parseInt(fullHex.substring(4, 6), 16);
return `rgba(${r}, ${g}, ${b}, ${opacity})`;
},
// 216>0.5
isLightColor(hex) {
const cleanHex = hex.replace('#', '');
const fullHex = cleanHex.length === 3
? cleanHex.split('').map(c => c + c).join('')
: cleanHex;
const r = parseInt(fullHex.substring(0, 2), 16) / 255;
const g = parseInt(fullHex.substring(2, 4), 16) / 255;
const b = parseInt(fullHex.substring(4, 6), 16) / 255;
const luminance = (0.299 * r + 0.587 * g + 0.114 * b);
return luminance > 0.5;
},
// 线线
getDynamicLineWidth() {
const { baseLineWidth, minLineWidth, maxLineWidth } = this.labelDrawConfig
const dynamicWidth = baseLineWidth / this.zoomScale
return Math.max(minLineWidth, Math.min(maxLineWidth, dynamicWidth))
},
//
getDynamicFontSize() {
const { textFontSize } = this.labelDrawConfig
const dynamicSize = textFontSize / this.zoomScale
return Math.max(10, Math.min(24, dynamicSize))
},
//
async loadAuditImages() { async loadAuditImages() {
try { try {
this.imageLoading = true this.imageLoading = true
@ -179,24 +234,25 @@ export default {
console.log('审核图片接口返回:', res) console.log('审核图片接口返回:', res)
if (res.code === 200 && res.data && Array.isArray(res.data)) { if (res.code === 200 && res.data && Array.isArray(res.data)) {
// // labelingSampleId
const imageMap = {} const imageMap = {}
res.data.forEach(item => { res.data.forEach(item => {
const key = item.labelingSampleId const key = item.labelingSampleId
//
if (!imageMap[key]) { if (!imageMap[key]) {
imageMap[key] = { imageMap[key] = {
id: item.labelingSampleId, id: item.labelingSampleId,
datasetSampleId: item.datasetSampleId || item.labelingSampleId, datasetSampleId: item.datasetSampleId || item.labelingSampleId,
filePath: item.filePath, filePath: item.filePath,
isInvalidData: item.isInvalidData || '1', isInvalidData: item.isInvalidData || '1',
shapes: [], shapes: [], //
shapeLabelMap: {}, shapeLabelMap: {}, // ID -> ID
labelShapeMap: {} labelShapeMap: {} // ID -> ID
} }
} }
// // JSON
let shapeData = {} let shapeData = {}
try { try {
shapeData = JSON.parse(item.labelingCoordinate || '{}') shapeData = JSON.parse(item.labelingCoordinate || '{}')
@ -205,10 +261,10 @@ export default {
shapeData = {} shapeData = {}
} }
// ID // ID
const shapeId = `shape_${item.labelingSampleId}_${item.labelId}_${Date.now()}` const shapeId = `shape_${item.labelingSampleId}_${item.labelId}_${Date.now()}`
// // labelName/color
const shape = { const shape = {
id: shapeId, id: shapeId,
type: this.getShapeTypeByBoxType(item.boxType), type: this.getShapeTypeByBoxType(item.boxType),
@ -219,14 +275,15 @@ export default {
radius: shapeData.radius || 0, radius: shapeData.radius || 0,
points: shapeData.points || [], points: shapeData.points || [],
labelId: item.labelId, labelId: item.labelId,
labelName: item.labelName || '未知标签', //
textContent: item.textContent || '', textContent: item.textContent || '',
color: item.labelingColor || this.shapeColorMap[this.getShapeTypeByBoxType(item.boxType)] color: item.labelingColor || this.shapeColorMap[this.getShapeTypeByBoxType(item.boxType)] //
} }
// //
imageMap[key].shapes.push(shape) imageMap[key].shapes.push(shape)
// // -
imageMap[key].shapeLabelMap[shapeId] = item.labelId imageMap[key].shapeLabelMap[shapeId] = item.labelId
if (!imageMap[key].labelShapeMap[item.labelId]) { if (!imageMap[key].labelShapeMap[item.labelId]) {
imageMap[key].labelShapeMap[item.labelId] = [] imageMap[key].labelShapeMap[item.labelId] = []
@ -234,7 +291,7 @@ export default {
imageMap[key].labelShapeMap[item.labelId].push(shapeId) imageMap[key].labelShapeMap[item.labelId].push(shapeId)
}) })
// //
this.imageList = Object.values(imageMap) this.imageList = Object.values(imageMap)
if (this.imageList.length === 0) { if (this.imageList.length === 0) {
@ -253,33 +310,44 @@ export default {
} }
}, },
// //
async loadLabelTreeData() { async loadLabelTreeData() {
try { try {
this.labelLoading = true this.labelLoading = true;
// if (!this.labelingTaskId) return;
// const res = await getLabelingTaskLabelTreeAPI({ labelingTaskId: this.labelingTaskId });
this.labelGroups = [] if (res.code === 200 && res.data) {
console.log('res',res)
this.labelGroups = res.data.map(group => ({
...group,
expanded: true,
labels: (group.labels || []).map(label => ({
...label,
ocrResults: []
}))
}));
} else {
this.$message.warning('标签数据加载失败:' + (res.msg || '接口返回异常'));
}
} catch (error) { } catch (error) {
console.error('加载标签树失败:', error) console.error('加载标签树失败:', error);
this.$message.error('标签数据加载失败,请刷新重试') this.$message.error('标签数据加载失败,请刷新重试');
this.labelGroups = []
} finally { } finally {
this.labelLoading = false this.labelLoading = false;
} }
}, },
// box_type // boxType
getShapeTypeByBoxType(boxType) { getShapeTypeByBoxType(boxType) {
switch (boxType) { switch (boxType) {
case "1": return 'rect' case "1": return 'rect' //
case "2": return 'polygon' case "2": return 'polygon' //
case "3": return 'circle' case "3": return 'circle' //
default: return 'rect' default: return 'rect'
} }
}, },
// // +
onImageLoad() { onImageLoad() {
this.calcFitScale() this.calcFitScale()
if (this.ailabelInstance?.setScale) { if (this.ailabelInstance?.setScale) {
@ -293,12 +361,12 @@ export default {
}) })
}, },
// //
onImageError() { onImageError() {
this.$message.error(`图片加载失败:${this.currentImage.filePath}`) this.$message.error(`图片加载失败:${this.currentImage.filePath}`)
}, },
// //
calcFitScale() { calcFitScale() {
const container = this.$refs.labelContainer const container = this.$refs.labelContainer
const image = this.$refs.labelImage const image = this.$refs.labelImage
@ -315,28 +383,28 @@ export default {
this.zoomScale = Math.max(0.1, Math.min(2.0, this.zoomScale)) this.zoomScale = Math.max(0.1, Math.min(2.0, this.zoomScale))
}, },
// //
initAILabel() { initAILabel() {
const container = this.$refs.labelContainer const container = this.$refs.labelContainer
const image = this.$refs.labelImage const image = this.$refs.labelImage
if (!container || !image || !this.currentImage.filePath) return if (!container || !image || !this.currentImage.filePath) return
// //
this.destroyAILabel() this.destroyAILabel()
try { try {
let instance = null let instance = null
// // AILabelLib
if (AILabelLib.create) { if (AILabelLib.create) {
instance = AILabelLib.create({ instance = AILabelLib.create({
el: container, el: container,
image: image, image: image,
config: { config: {
enableRect: false, enableRect: false, //
enablePolygon: false, enablePolygon: false, //
enableCircle: false, enableCircle: false, //
enableDrag: false, enableDrag: false, //
enableScale: false, enableScale: false, //
style: { style: {
rect: { strokeStyle: '#00ff00', lineWidth: 2, fillStyle: 'rgba(0,255,0,0.1)' }, rect: { strokeStyle: '#00ff00', lineWidth: 2, fillStyle: 'rgba(0,255,0,0.1)' },
polygon: { strokeStyle: '#0099ff', lineWidth: 2, fillStyle: 'rgba(0,153,255,0.1)' }, polygon: { strokeStyle: '#0099ff', lineWidth: 2, fillStyle: 'rgba(0,153,255,0.1)' },
@ -350,13 +418,14 @@ export default {
} else if (window.AILabel) { } else if (window.AILabel) {
instance = window.AILabel({ el: container, image: image }) instance = window.AILabel({ el: container, image: image })
} else { } else {
instance = this.createSimpleLabelTool(container, image) //
instance = this.createSimpleLabelTool(container, image, this)
} }
if (instance) { if (instance) {
this.ailabelInstance = instance this.ailabelInstance = instance
if (instance.setScale) instance.setScale(this.zoomScale) if (instance.setScale) instance.setScale(this.zoomScale)
this.loadLabelData() this.loadLabelData() //
} }
} catch (e) { } catch (e) {
console.error('标注工具初始化失败:', e) console.error('标注工具初始化失败:', e)
@ -364,7 +433,7 @@ export default {
} }
}, },
// //
destroyAILabel() { destroyAILabel() {
if (this.ailabelInstance) { if (this.ailabelInstance) {
try { try {
@ -374,8 +443,8 @@ export default {
} }
}, },
// //
createSimpleLabelTool(container, image) { createSimpleLabelTool(container, image, instance) {
const tool = { const tool = {
el: container, el: container,
image, image,
@ -383,9 +452,11 @@ export default {
mainCanvas: null, mainCanvas: null,
mainCtx: null, mainCtx: null,
zoomScale: 1.0, zoomScale: 1.0,
instance: instance, //
resizeCanvas: null, // resize
init() { init() {
// //
const mainCanvas = document.createElement('canvas') const mainCanvas = document.createElement('canvas')
mainCanvas.style.cssText = 'position:absolute;top:0;left:0;pointer-events:none;z-index:10;width:100%;height:100%;' mainCanvas.style.cssText = 'position:absolute;top:0;left:0;pointer-events:none;z-index:10;width:100%;height:100%;'
mainCanvas.width = container.offsetWidth mainCanvas.width = container.offsetWidth
@ -394,27 +465,28 @@ export default {
this.mainCanvas = mainCanvas this.mainCanvas = mainCanvas
this.mainCtx = mainCanvas.getContext('2d') this.mainCtx = mainCanvas.getContext('2d')
// // resizethis
window.addEventListener('resize', () => this.resizeCanvas()) this.resizeCanvas = () => {
this.mainCanvas.width = this.el.offsetWidth
this.mainCanvas.height = this.el.offsetHeight
this.redrawMainCanvas()
}
//
window.addEventListener('resize', this.resizeCanvas)
// //
this.redrawMainCanvas() this.redrawMainCanvas()
return this return this
}, },
resizeCanvas() { //
this.mainCanvas.width = this.el.offsetWidth
this.mainCanvas.height = this.el.offsetHeight
this.redrawMainCanvas()
},
setScale(scale) { setScale(scale) {
this.zoomScale = scale this.zoomScale = scale
this.resizeCanvas() this.resizeCanvas()
}, },
// //
drawRect(x, y, w, h) { drawRect(x, y, w, h) {
const shape = { id: `shape_${Date.now()}`, type: 'rect', x, y, width: w, height: h } const shape = { id: `shape_${Date.now()}`, type: 'rect', x, y, width: w, height: h }
this.shapes.push(shape) this.shapes.push(shape)
@ -422,6 +494,7 @@ export default {
return shape.id return shape.id
}, },
//
drawCircle(x, y, radius) { drawCircle(x, y, radius) {
const shape = { id: `shape_${Date.now()}`, type: 'circle', x, y, radius } const shape = { id: `shape_${Date.now()}`, type: 'circle', x, y, radius }
this.shapes.push(shape) this.shapes.push(shape)
@ -429,6 +502,7 @@ export default {
return shape.id return shape.id
}, },
//
drawPolygon(points) { drawPolygon(points) {
const shape = { id: `shape_${Date.now()}`, type: 'polygon', points } const shape = { id: `shape_${Date.now()}`, type: 'polygon', points }
this.shapes.push(shape) this.shapes.push(shape)
@ -436,25 +510,24 @@ export default {
return shape.id return shape.id
}, },
//
clearShapes() { clearShapes() {
this.shapes = [] this.shapes = []
this.mainCtx.clearRect(0, 0, this.mainCanvas.width, this.mainCanvas.height) this.mainCtx.clearRect(0, 0, this.mainCanvas.width, this.mainCanvas.height)
}, },
getShapes() { // labelName/color
return [...this.shapes]
},
setShapeData(id, data) { setShapeData(id, data) {
const shape = this.shapes.find(s => s.id === id) const shape = this.shapes.find(s => s.id === id)
if (shape) Object.assign(shape, data) if (shape) Object.assign(shape, data)
}, },
// ++
redrawMainCanvas() { redrawMainCanvas() {
this.mainCtx.clearRect(0, 0, this.mainCanvas.width, this.mainCanvas.height) this.mainCtx.clearRect(0, 0, this.mainCanvas.width, this.mainCanvas.height)
this.mainCtx.save() this.mainCtx.save()
// // +/
this.mainCtx.translate( this.mainCtx.translate(
this.el.offsetWidth / 2, this.el.offsetWidth / 2,
this.el.offsetHeight / 2 this.el.offsetHeight / 2
@ -465,7 +538,7 @@ export default {
-this.image.naturalWidth / 2, -this.image.naturalWidth / 2,
-this.image.naturalHeight / 2 -this.image.naturalHeight / 2
) )
// //
this.mainCtx.drawImage( this.mainCtx.drawImage(
this.image, this.image,
0, 0, 0, 0,
@ -474,24 +547,31 @@ export default {
) )
} }
// //
const dynamicLineWidth = this.instance.getDynamicLineWidth()
const dynamicFontSize = this.instance.getDynamicFontSize()
const { textPadding, bgOpacity, shapeFillOpacity } = this.instance.labelDrawConfig
// +
this.shapes.forEach(s => { this.shapes.forEach(s => {
if (!s.color) return; //
//
this.mainCtx.lineWidth = dynamicLineWidth
this.mainCtx.strokeStyle = s.color
this.mainCtx.fillStyle = this.instance.hexToRgba(s.color, shapeFillOpacity)
//
if (s.type === 'rect') { if (s.type === 'rect') {
this.mainCtx.strokeStyle = s.color || '#00ff00'
this.mainCtx.fillStyle = 'rgba(0,255,0,0.1)'
this.mainCtx.strokeRect(s.x, s.y, s.width, s.height) this.mainCtx.strokeRect(s.x, s.y, s.width, s.height)
this.mainCtx.fillRect(s.x, s.y, s.width, s.height) this.mainCtx.fillRect(s.x, s.y, s.width, s.height)
} else if (s.type === 'circle') { } else if (s.type === 'circle') {
this.mainCtx.strokeStyle = s.color || '#ff9900'
this.mainCtx.fillStyle = 'rgba(255,153,0,0.1)'
this.mainCtx.beginPath() this.mainCtx.beginPath()
this.mainCtx.arc(s.x, s.y, s.radius, 0, Math.PI * 2) this.mainCtx.arc(s.x, s.y, s.radius, 0, Math.PI * 2)
this.mainCtx.closePath() this.mainCtx.closePath()
this.mainCtx.fill() this.mainCtx.fill()
this.mainCtx.stroke() this.mainCtx.stroke()
} else if (s.type === 'polygon') { } else if (s.type === 'polygon' && s.points.length >= 3) {
this.mainCtx.strokeStyle = s.color || '#0099ff'
this.mainCtx.fillStyle = 'rgba(0,153,255,0.1)'
this.mainCtx.beginPath() this.mainCtx.beginPath()
s.points.forEach((point, idx) => { s.points.forEach((point, idx) => {
idx === 0 ? this.mainCtx.moveTo(point.x, point.y) : this.mainCtx.lineTo(point.x, point.y) idx === 0 ? this.mainCtx.moveTo(point.x, point.y) : this.mainCtx.lineTo(point.x, point.y)
@ -500,11 +580,53 @@ export default {
this.mainCtx.fill() this.mainCtx.fill()
this.mainCtx.stroke() this.mainCtx.stroke()
} }
// labelName
if (!s.labelName) return;
//
let textX, textY;
if (s.type === 'rect') {
textX = s.x + s.width / 2;
textY = s.y + s.height / 2;
} else if (s.type === 'circle') {
textX = s.x;
textY = s.y;
} else if (s.type === 'polygon' && s.points.length >= 3) {
const totalX = s.points.reduce((sum, p) => sum + p.x, 0);
const totalY = s.points.reduce((sum, p) => sum + p.y, 0);
textX = totalX / s.points.length;
textY = totalY / s.points.length;
} else {
return; //
}
//
this.mainCtx.font = `${dynamicFontSize}px Microsoft YaHei`
this.mainCtx.textAlign = 'center'
this.mainCtx.textBaseline = 'middle'
//
const textWidth = this.mainCtx.measureText(s.labelName).width
const textHeight = dynamicFontSize
// 使+
this.mainCtx.fillStyle = this.instance.hexToRgba(s.color, bgOpacity)
this.mainCtx.fillRect(
textX - (textWidth + 2 * textPadding) / 2,
textY - (textHeight + 2 * textPadding) / 2,
textWidth + 2 * textPadding,
textHeight + 2 * textPadding
)
//
this.mainCtx.fillStyle = this.instance.isLightColor(s.color) ? '#000000' : '#ffffff'
this.mainCtx.fillText(s.labelName, textX, textY)
}) })
this.mainCtx.restore() this.mainCtx.restore()
}, },
// +
destroy() { destroy() {
this.mainCanvas?.remove() this.mainCanvas?.remove()
window.removeEventListener('resize', this.resizeCanvas) window.removeEventListener('resize', this.resizeCanvas)
@ -513,22 +635,27 @@ export default {
return tool.init() return tool.init()
}, },
// // labelName/color
loadLabelData() { loadLabelData() {
if (!this.ailabelInstance || !this.currentImage) return if (!this.ailabelInstance || !this.currentImage) return
//
this.ailabelInstance.clearShapes?.() this.ailabelInstance.clearShapes?.()
// // -
if (this.currentImage.shapeLabelMap) { if (this.currentImage.shapeLabelMap) {
this.shapeLabelMap = JSON.parse(JSON.stringify(this.currentImage.shapeLabelMap)) this.shapeLabelMap = JSON.parse(JSON.stringify(this.currentImage.shapeLabelMap))
} }
if (this.currentImage.labelShapeMap) {
this.labelShapeMap = JSON.parse(JSON.stringify(this.currentImage.labelShapeMap))
}
// // /
const shapes = this.currentImage.shapes || [] const shapes = this.currentImage.shapes || []
shapes.forEach(shapeData => { shapes.forEach(shapeData => {
try { try {
let shapeId = '' let shapeId = ''
//
if (shapeData.type === 'rect' && shapeData.x !== undefined) { if (shapeData.type === 'rect' && shapeData.x !== undefined) {
shapeId = this.ailabelInstance.drawRect?.(shapeData.x, shapeData.y, shapeData.width, shapeData.height) || shapeData.id shapeId = this.ailabelInstance.drawRect?.(shapeData.x, shapeData.y, shapeData.width, shapeData.height) || shapeData.id
} else if (shapeData.type === 'polygon' && shapeData.points && shapeData.points.length >= 3) { } else if (shapeData.type === 'polygon' && shapeData.points && shapeData.points.length >= 3) {
@ -537,10 +664,12 @@ export default {
shapeId = this.ailabelInstance.drawCircle?.(shapeData.x, shapeData.y, shapeData.radius) || shapeData.id shapeId = this.ailabelInstance.drawCircle?.(shapeData.x, shapeData.y, shapeData.radius) || shapeData.id
} }
// // ID
if (shapeId) { if (shapeId) {
this.ailabelInstance.setShapeData?.(shapeId, { this.ailabelInstance.setShapeData?.(shapeId, {
color: shapeData.color || this.shapeColorMap[shapeData.type] labelName: shapeData.labelName || '未知标签',
color: shapeData.color || this.shapeColorMap[shapeData.type],
labelId: shapeData.labelId
}) })
} }
} catch (e) { } catch (e) {
@ -549,7 +678,7 @@ export default {
}) })
}, },
// // /线
handleWheelZoom(e) { handleWheelZoom(e) {
e.preventDefault() e.preventDefault()
const delta = e.deltaY > 0 ? -0.1 : 0.1 const delta = e.deltaY > 0 ? -0.1 : 0.1
@ -558,30 +687,35 @@ export default {
if (this.ailabelInstance?.setScale) { if (this.ailabelInstance?.setScale) {
this.ailabelInstance.setScale(this.zoomScale) this.ailabelInstance.setScale(this.zoomScale)
} }
//
this.ailabelInstance?.redrawMainCanvas?.()
}, },
// // 0.12.0
zoomIn() { zoomIn() {
this.zoomScale = Math.min(2.0, this.zoomScale + 0.1) this.zoomScale = Math.min(2.0, this.zoomScale + 0.1)
if (this.ailabelInstance?.setScale) { if (this.ailabelInstance?.setScale) {
this.ailabelInstance.setScale(this.zoomScale) this.ailabelInstance.setScale(this.zoomScale)
} }
this.ailabelInstance?.redrawMainCanvas?.()
}, },
// // 0.10.1
zoomOut() { zoomOut() {
this.zoomScale = Math.max(0.1, this.zoomScale - 0.1) this.zoomScale = Math.max(0.1, this.zoomScale - 0.1)
if (this.ailabelInstance?.setScale) { if (this.ailabelInstance?.setScale) {
this.ailabelInstance.setScale(this.zoomScale) this.ailabelInstance.setScale(this.zoomScale)
} }
this.ailabelInstance?.redrawMainCanvas?.()
}, },
// // 1.0
resetZoom() { resetZoom() {
this.zoomScale = 1.0 this.zoomScale = 1.0
if (this.ailabelInstance?.setScale) { if (this.ailabelInstance?.setScale) {
this.ailabelInstance.setScale(this.zoomScale) this.ailabelInstance.setScale(this.zoomScale)
} }
this.ailabelInstance?.redrawMainCanvas?.()
}, },
// //
@ -602,51 +736,61 @@ export default {
} }
}, },
//
nextImage1() {
if (this.currentImageIndex <= this.imageList.length - 1) {
this.switchImage(this.currentImageIndex)
} else {
this.$message.info('已是最后一张!')
this.$router.push({
path: '/data/audit',
});
}
},
//
switchImage(index) { switchImage(index) {
this.currentImageIndex = index this.currentImageIndex = index
this.currentImage = this.imageList[index] this.currentImage = this.imageList[index]
this.shapeLabelMap = {}
this.labelShapeMap = {}
// // DOMDOM
this.$nextTick(() => { this.$nextTick(() => {
this.initAILabel() this.initAILabel()
}) })
}, },
// // 1
async auditPass() { async auditPass() {
await this.submitAuditResult(1) await this.submitAuditResult(1)
}, },
// //
openRejectDialog() { openRejectDialog() {
//
this.auditRemark = '' this.auditRemark = ''
this.rejectDialogVisible = true this.rejectDialogVisible = true
}, },
// //
handleDialogClose() { handleDialogClose() {
this.rejectDialogVisible = false this.rejectDialogVisible = false
this.auditRemark = '' this.auditRemark = ''
}, },
// // + 2
async confirmReject() { async confirmReject() {
//
if (!this.auditRemark.trim()) { if (!this.auditRemark.trim()) {
this.$message.warning('请填写审核不通过的原因!') this.$message.warning('请填写审核不通过的原因!')
return return
} }
//
await this.submitAuditResult(2) await this.submitAuditResult(2)
//
this.rejectDialogVisible = false this.rejectDialogVisible = false
}, },
// // 1-2-
async submitAuditResult(auditStatus) { async submitAuditResult(auditStatus) {
if (this.isAuditing) return if (this.isAuditing) return //
if (!this.currentImage.id) { if (!this.currentImage.id) {
this.$message.error('当前图片ID为空无法提交审核') this.$message.error('当前图片ID为空无法提交审核')
return return
@ -656,26 +800,24 @@ export default {
this.isAuditing = true this.isAuditing = true
const loadingInstance = this.$loading({ text: '提交审核结果中...' }) const loadingInstance = this.$loading({ text: '提交审核结果中...' })
// //
const submitParams = { const submitParams = {
labelingTaskId: this.labelingTaskId, labelingTaskId: this.labelingTaskId,
labelingSampleId: this.currentImage.id, labelingSampleId: this.currentImage.id,
auditStatus: auditStatus, // 1-2- auditStatus: auditStatus, // 1-2-
datasetSampleId: this.currentImage.datasetSampleId || '', datasetSampleId: this.currentImage.datasetSampleId || '',
auditRemark: auditStatus === 2 ? this.auditRemark.trim() : '' // auditRemark: auditStatus === 2 ? this.auditRemark.trim() : '' //
} }
console.log('提交审核参数:', submitParams) console.log('提交审核参数:', submitParams)
//
const res = await submitAuditResultAPI(submitParams) const res = await submitAuditResultAPI(submitParams)
if (res.code === 200) { if (res.code === 200) {
this.$message.success(auditStatus === 1 ? '审核通过提交成功!' : '审核不通过提交成功!') this.$message.success(auditStatus === 1 ? '审核通过提交成功!' : '审核不通过提交成功!')
// //
if (this.currentImageIndex < this.imageList.length - 1) { if (this.currentImageIndex < this.imageList.length - 1) {
this.nextImage() await this.loadAuditImages();
this.nextImage1()
} }
} else { } else {
this.$message.error('提交失败:' + (res.msg || '接口返回异常')) this.$message.error('提交失败:' + (res.msg || '接口返回异常'))
@ -696,7 +838,7 @@ export default {
// //
.labeling-page-container { .labeling-page-container {
width: 100%; width: 100%;
height: 92vh; height: 90.5vh;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
overflow: hidden; overflow: hidden;
@ -885,6 +1027,13 @@ export default {
} }
} }
} }
.no-label-data {
height: 100%;
display: flex;
align-items: center;
justify-content: center;
}
} }
} }
} }
@ -898,7 +1047,7 @@ export default {
display: block; display: block;
margin-bottom: 8px; margin-bottom: 8px;
font-size: 14px; font-size: 14px;
color: black; color: #333;
} }
.reject-textarea { .reject-textarea {
@ -907,7 +1056,7 @@ export default {
} }
} }
// //
::v-deep .el-scrollbar__bar { ::v-deep .el-scrollbar__bar {
width: 6px; width: 6px;
height: 6px; height: 6px;
@ -916,5 +1065,8 @@ export default {
::v-deep .el-scrollbar__thumb { ::v-deep .el-scrollbar__thumb {
background-color: #ccc; background-color: #ccc;
border-radius: 3px; border-radius: 3px;
&:hover {
background-color: #999;
}
} }
</style> </style>

View File

@ -684,7 +684,7 @@ export default {
// //
.labeling-page-container { .labeling-page-container {
width: 100%; width: 100%;
height: 92vh; height: 90.5vh;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
overflow: hidden; overflow: hidden;

View File

@ -554,6 +554,17 @@ export default {
this.$message.info('已是最后一张!'); this.$message.info('已是最后一张!');
} }
}, },
nextImage1() {
if (this.currentImageIndex <= this.imageList.length - 1) {
this.switchImage(this.currentImageIndex + 1);
} else {
this.$message.info('已是最后一张!')
this.$router.push({
path: '/data/audit',
});
}
},
switchImage(index) { switchImage(index) {
this.currentImageIndex = index; this.currentImageIndex = index;
this.currentImage = this.imageList[index]; this.currentImage = this.imageList[index];
@ -630,7 +641,8 @@ export default {
this.$message.success(auditStatus === 1 ? '审核通过提交成功!' : '审核不通过提交成功!'); this.$message.success(auditStatus === 1 ? '审核通过提交成功!' : '审核不通过提交成功!');
// //
if (this.currentImageIndex < this.imageList.length - 1) { if (this.currentImageIndex < this.imageList.length - 1) {
this.nextImage(); await this.loadAuditImages();
this.nextImage1();
} }
} else { } else {
this.$message.error('提交失败:' + (res.msg || '接口返回异常')); this.$message.error('提交失败:' + (res.msg || '接口返回异常'));

View File

@ -524,7 +524,8 @@ export default {
datasetSampleId: this.currentImage.datasetSampleId || '', datasetSampleId: this.currentImage.datasetSampleId || '',
isInvalidData: this.isInvalidData ? "0" : "1", // isInvalidData: this.isInvalidData ? "0" : "1", //
labelingDataList: this.selectedLabelIds.map(labelId => ({ labelingDataList: this.selectedLabelIds.map(labelId => ({
labelId: labelId labelId: labelId,
boxType: '0'
})) }))
}; };

View File

@ -54,8 +54,8 @@
<!-- 无效数据标记 --> <!-- 无效数据标记 -->
<el-checkbox v-model="isInvalidData" label="标为无效数据"></el-checkbox> <el-checkbox v-model="isInvalidData" label="标为无效数据"></el-checkbox>
<!-- 保存按钮组 --> <!-- 保存按钮组 -->
<el-button type="primary" @click="saveLabel" class="save-btn">保存</el-button> <!-- <el-button type="primary" @click="saveLabel" class="save-btn">保存</el-button>-->
<el-button type="success" @click="saveAndContinue" class="save-btn">保存并继续</el-button> <el-button type="primary" @click="saveAndContinue" class="save-btn">保存并继续</el-button>
</div> </div>
</div> </div>
@ -190,7 +190,7 @@ import {
getLabelPicturesAPI, // getLabelPicturesAPI, //
saveLabelingDataAPI saveLabelingDataAPI
} from '@/api/data/labeling' } from '@/api/data/labeling'
import {decryptWithSM4} from "@/utils/sm"; // import {decryptWithSM4, encryptWithSM4} from "@/utils/sm"; //
export default { export default {
name: 'LabelingAdd', name: 'LabelingAdd',
@ -713,21 +713,21 @@ export default {
}, },
onImageLoad() { onImageLoad() {
this.imageLoaded = true this.imageLoaded = true;
this.calcFitScale() // 1.
// this.calcFitScale();
if (this.ailabelInstance?.setScale) { // 2. image
this.ailabelInstance.setScale(this.zoomScale) if (this.ailabelInstance) {
this.ailabelInstance.image = this.$refs.labelImage;
} }
this.initAILabel() // 3. ++
if (this.ailabelInstance?.setScale) {
this.ailabelInstance.setScale(this.zoomScale);
}
this.initAILabel();
this.$nextTick(() => { this.$nextTick(() => {
if (this.ailabelInstance?.redrawMainCanvas) { this.ailabelInstance?.redrawMainCanvas?.();
this.ailabelInstance.redrawMainCanvas() });
}
// this.recordState()
this.ailabelInstance?.redrawMainCanvas()
})
}, },
getDynamicLineWidth() { getDynamicLineWidth() {
@ -1163,12 +1163,15 @@ export default {
} }
}, },
// isShapeOutOfImage
isShapeOutOfImage(shape) { isShapeOutOfImage(shape) {
if (!this.image) return true; // // this.image
// const currentImage = this.instance.$refs.labelImage;
const imgWidth = this.image.naturalWidth; if (!currentImage) return true; //
const imgHeight = this.image.naturalHeight; //
// const imgWidth = currentImage.naturalWidth;
const imgHeight = currentImage.naturalHeight;
//
switch (shape.type) { switch (shape.type) {
case 'rect': case 'rect':
return shape.x < 0 || shape.y < 0 || shape.x + shape.width > imgWidth || shape.y + shape.height > imgHeight; return shape.x < 0 || shape.y < 0 || shape.x + shape.width > imgWidth || shape.y + shape.height > imgHeight;
@ -1180,7 +1183,6 @@ export default {
return false; return false;
} }
}, },
// //
// drawTempShape线 // drawTempShape线
drawTempShape() { drawTempShape() {
@ -1837,37 +1839,35 @@ export default {
} }
}, },
nextImage1() {
if (this.currentImageIndex < this.imageList.length - 1) {
this.switchImage(this.currentImageIndex)
} else {
this.$message.info('已是最后一张!')
}
},
// //
async switchImage(index) { async switchImage(index) {
// // 1.
const currentShapes = this.ailabelInstance?.getShapes?.() || []; this.destroyAILabel();
if (currentShapes.length > 0 || this.isInvalidData) { // 2.
try {
//
await this.saveLabel();
} catch (err) {
//
const confirm = await this.$confirm('当前图片标注未保存,是否放弃修改并切换?', '提示', {
type: 'warning'
});
if (!confirm) return;
}
}
//
this.currentImageIndex = index; this.currentImageIndex = index;
this.currentImage = this.imageList[index]; this.currentImage = this.imageList[index];
this.imageLoaded = false; this.imageLoaded = false; //
this.resetLabelState(); this.resetLabelState();
// /
this.undoStack = []; this.undoStack = [];
this.redoStack = []; this.redoStack = [];
this.zoomScale = 1.0; // 沿
// // 3. initAILabelonImageLoad
// imgonImageLoad
this.$nextTick(() => { this.$nextTick(() => {
this.initAILabel(); const img = this.$refs.labelImage;
if (img) {
img.src = ''; // src
img.src = this.currentImage.filePath; // onImageLoad
}
}); });
}, },
@ -2064,7 +2064,7 @@ export default {
if (res.code === 200) { if (res.code === 200) {
this.$message.success('标注保存成功!'); this.$message.success('标注保存成功!');
// //
this.saveCurrentLabelData(); await this.loadTreeData();
// / // /
this.undoStack = []; this.undoStack = [];
this.redoStack = []; this.redoStack = [];
@ -2090,10 +2090,12 @@ export default {
// //
await this.saveLabel(); await this.saveLabel();
this.isInvalidData = false; this.isInvalidData = false;
if (this.currentImageIndex < this.imageList.length - 1) { if (this.currentImageIndex <= this.imageList.length - 1) {
this.nextImage(); this.nextImage1();
} else { } else {
this.$message.info('已是最后一张图片!'); this.$router.push({
path: '/data/labeling',
});
} }
} catch (err) { } catch (err) {
this.$message.error('保存失败,无法继续'); this.$message.error('保存失败,无法继续');

View File

@ -74,9 +74,9 @@
<div <div
class="label-item" class="label-item"
v-for="(label, labelIndex) in group.labels" v-for="(label, labelIndex) in group.labels"
:key="label.labelId || labelIndex" :key="label.id || labelIndex"
> >
<span class="label-name">#{{ labelIndex + 1 }} {{ label.labelName }} </span> <span class="label-name">#{{ labelIndex + 1 }} {{ label.name }} </span>
<span class="shape-count" v-if="label.shapeIds && label.shapeIds.length"> <span class="shape-count" v-if="label.shapeIds && label.shapeIds.length">
({{ label.shapeIds.length }}个标注) ({{ label.shapeIds.length }}个标注)
</span> </span>
@ -109,28 +109,17 @@ export default {
name: 'LabelingDetail', name: 'LabelingDetail',
data() { data() {
return { return {
// // 线
labelDrawConfig: { labelDrawConfig: {
baseLineWidth: 3, baseLineWidth: 3,
minLineWidth: 1, minLineWidth: 1,
maxLineWidth: 6, maxLineWidth: 6,
textFontSize: 16, textFontSize: 16,
textPadding: 3, textPadding: 3,
shapeMainColor: { bgOpacity: 0.4, //
rect: '#00ff00', // shapeFillOpacity: 0.1, //
polygon: '#0099ff', //
circle: '#ff9900' //
},
selectedColor: '#ff0000', // /
bgOpacity: 0.4, //
shapeFillOpacity: 0.4, //
textColor: "#ffffff" textColor: "#ffffff"
}, },
shapeColorMap: {
rect: '#00ff00', // - 绿
polygon: '#0099ff', // -
circle: '#ff9900' // -
},
// //
imageList: [], imageList: [],
currentImageIndex: 0, currentImageIndex: 0,
@ -174,7 +163,7 @@ export default {
return isNaN(num) ? '' : num; return isNaN(num) ? '' : num;
}, },
// 116RGBA#fff/#ffffff // 116RGBA#fff/#ffffff
hexToRgba(hex, opacity) { hexToRgba(hex, opacity) {
const cleanHex = hex.replace('#', ''); const cleanHex = hex.replace('#', '');
const fullHex = cleanHex.length === 3 const fullHex = cleanHex.length === 3
@ -186,7 +175,7 @@ export default {
return `rgba(${r}, ${g}, ${b}, ${opacity})`; return `rgba(${r}, ${g}, ${b}, ${opacity})`;
}, },
// 216>0.5 // 216>0.5
isLightColor(hex) { isLightColor(hex) {
const cleanHex = hex.replace('#', ''); const cleanHex = hex.replace('#', '');
const fullHex = cleanHex.length === 3 const fullHex = cleanHex.length === 3
@ -195,35 +184,22 @@ export default {
const r = parseInt(fullHex.substring(0, 2), 16) / 255; const r = parseInt(fullHex.substring(0, 2), 16) / 255;
const g = parseInt(fullHex.substring(2, 4), 16) / 255; const g = parseInt(fullHex.substring(2, 4), 16) / 255;
const b = parseInt(fullHex.substring(4, 6), 16) / 255; const b = parseInt(fullHex.substring(4, 6), 16) / 255;
//
const luminance = (0.299 * r + 0.587 * g + 0.114 * b); const luminance = (0.299 * r + 0.587 * g + 0.114 * b);
return luminance > 0.5; return luminance > 0.5;
}, },
// 3线 // 线线
getDynamicLineWidth() { getDynamicLineWidth() {
const { baseLineWidth, minLineWidth, maxLineWidth } = this.labelDrawConfig const { baseLineWidth, minLineWidth, maxLineWidth } = this.labelDrawConfig
// 线/
const dynamicWidth = baseLineWidth / this.zoomScale const dynamicWidth = baseLineWidth / this.zoomScale
return Math.max(minLineWidth, Math.min(maxLineWidth, dynamicWidth)) return Math.max(minLineWidth, Math.min(maxLineWidth, dynamicWidth))
}, },
// 4 //
getDynamicFontSize() { getDynamicFontSize() {
const { textFontSize } = this.labelDrawConfig const { textFontSize } = this.labelDrawConfig
//
const dynamicSize = textFontSize / this.zoomScale const dynamicSize = textFontSize / this.zoomScale
return Math.max(10, Math.min(24, dynamicSize)) // / return Math.max(10, Math.min(24, dynamicSize)) //
},
// ID
findLabelById(labelId) {
for (const group of this.labelGroups) {
for (const label of group.labels) {
if (label.labelId === labelId) return label
}
}
return null
}, },
// //
@ -255,7 +231,7 @@ export default {
} }
}, },
// // labelName/labelingColor
async loadTreeData() { async loadTreeData() {
try { try {
this.imageLoading = true this.imageLoading = true
@ -266,7 +242,7 @@ export default {
console.log('标注详情接口返回:', res) console.log('标注详情接口返回:', res)
if (res.code === 200 && res.data && Array.isArray(res.data)) { if (res.code === 200 && res.data && Array.isArray(res.data)) {
// // labelingSampleId
const imageMap = {} const imageMap = {}
// labeling_sample_id // labeling_sample_id
@ -295,9 +271,9 @@ export default {
} }
// ID // ID
const shapeId = `shape_${item.labelingSampleId}_${item.id || Date.now()}` const shapeId = `shape_${item.labelingSampleId}_${item.labelId}_${Date.now()}`
// // labelNamelabelingColor
const shape = { const shape = {
id: shapeId, id: shapeId,
type: this.getShapeTypeByBoxType(item.boxType), // box_type type: this.getShapeTypeByBoxType(item.boxType), // box_type
@ -308,8 +284,9 @@ export default {
radius: shapeData.radius || 0, radius: shapeData.radius || 0,
points: shapeData.points || [], points: shapeData.points || [],
labelId: item.labelId, labelId: item.labelId,
labelName: item.labelName || '未知标签', //
textContent: item.textContent || '', textContent: item.textContent || '',
color: item.labelingColor || this.shapeColorMap[this.getShapeTypeByBoxType(item.boxType)] color: item.labelingColor || '#00ff00' //
} }
// //
@ -426,7 +403,7 @@ export default {
} else if (window.AILabel) { } else if (window.AILabel) {
instance = window.AILabel({ el: container, image: image }) instance = window.AILabel({ el: container, image: image })
} else { } else {
// //
instance = this.createSimpleLabelTool(container, image, this) instance = this.createSimpleLabelTool(container, image, this)
} }
@ -453,21 +430,22 @@ export default {
} }
}, },
// //
createSimpleLabelTool(container, image, parentInstance) { createSimpleLabelTool(container, image, instance) {
const tool = { const tool = {
el: container, el: container,
image, image,
shapes: [], shapes: [],
selectedShape: null, selectedShape: null,
mainCanvas: null, mainCanvas: null,
tempCanvas: null,
mainCtx: null, mainCtx: null,
tempCtx: null,
zoomScale: 1.0, zoomScale: 1.0,
// instance: instance, //
parent: parentInstance,
init() { init() {
// + // +
const mainCanvas = document.createElement('canvas') const mainCanvas = document.createElement('canvas')
mainCanvas.style.cssText = 'position:absolute;top:0;left:0;pointer-events:none;z-index:10;width:100%;height:100%;' mainCanvas.style.cssText = 'position:absolute;top:0;left:0;pointer-events:none;z-index:10;width:100%;height:100%;'
mainCanvas.width = container.offsetWidth mainCanvas.width = container.offsetWidth
@ -492,13 +470,22 @@ export default {
this.redrawMainCanvas() this.redrawMainCanvas()
}, },
//
drawTempShape() {},
//
cancelDrawing() {},
//
setDrawType() {},
// //
setScale(scale) { setScale(scale) {
this.zoomScale = scale this.zoomScale = scale
this.resizeCanvas() this.resizeCanvas()
}, },
// // ID
drawRect(x, y, w, h) { drawRect(x, y, w, h) {
const shape = { id: `shape_${Date.now()}`, type: 'rect', x, y, width: w, height: h } const shape = { id: `shape_${Date.now()}`, type: 'rect', x, y, width: w, height: h }
this.shapes.push(shape) this.shapes.push(shape)
@ -531,12 +518,12 @@ export default {
return [...this.shapes] return [...this.shapes]
}, },
// + // -+
redrawMainCanvas() { redrawMainCanvas() {
this.mainCtx.clearRect(0, 0, this.mainCanvas.width, this.mainCanvas.height) this.mainCtx.clearRect(0, 0, this.mainCanvas.width, this.mainCanvas.height)
this.mainCtx.save() this.mainCtx.save()
// + // +
this.mainCtx.translate( this.mainCtx.translate(
this.el.offsetWidth / 2, this.el.offsetWidth / 2,
this.el.offsetHeight / 2 this.el.offsetHeight / 2
@ -556,24 +543,20 @@ export default {
) )
} }
// // 线
const dynamicLineWidth = this.parent.getDynamicLineWidth() const dynamicLineWidth = this.instance.getDynamicLineWidth()
const dynamicFontSize = this.parent.getDynamicFontSize() const dynamicFontSize = this.instance.getDynamicFontSize()
const { textPadding, bgOpacity, shapeFillOpacity, shapeMainColor } = this.parent.labelDrawConfig const { textPadding, bgOpacity, shapeFillOpacity } = this.instance.labelDrawConfig
// + // +
this.shapes.forEach(s => { this.shapes.forEach(s => {
// 1. if (!s.color) return; //
const shapeColor = s.color || shapeMainColor[s.type] || '#000000' //
const labelId = s.labelId || ''
const label = this.parent.findLabelById(labelId)
// 2.
this.mainCtx.lineWidth = dynamicLineWidth this.mainCtx.lineWidth = dynamicLineWidth
this.mainCtx.strokeStyle = shapeColor this.mainCtx.strokeStyle = s.color
this.mainCtx.fillStyle = this.parent.hexToRgba(shapeColor, shapeFillOpacity) this.mainCtx.fillStyle = this.instance.hexToRgba(s.color, shapeFillOpacity)
// 3. //
if (s.type === 'rect') { if (s.type === 'rect') {
this.mainCtx.strokeRect(s.x, s.y, s.width, s.height) this.mainCtx.strokeRect(s.x, s.y, s.width, s.height)
this.mainCtx.fillRect(s.x, s.y, s.width, s.height) this.mainCtx.fillRect(s.x, s.y, s.width, s.height)
@ -583,7 +566,7 @@ export default {
this.mainCtx.closePath() this.mainCtx.closePath()
this.mainCtx.fill() this.mainCtx.fill()
this.mainCtx.stroke() this.mainCtx.stroke()
} else if (s.type === 'polygon') { } else if (s.type === 'polygon' && s.points.length >= 3) {
this.mainCtx.beginPath() this.mainCtx.beginPath()
s.points.forEach((point, idx) => { s.points.forEach((point, idx) => {
idx === 0 ? this.mainCtx.moveTo(point.x, point.y) : this.mainCtx.lineTo(point.x, point.y) idx === 0 ? this.mainCtx.moveTo(point.x, point.y) : this.mainCtx.lineTo(point.x, point.y)
@ -593,36 +576,36 @@ export default {
this.mainCtx.stroke() this.mainCtx.stroke()
} }
// 4. // labelName
if (!label) return if (!s.labelName) return;
// //
let textX, textY let textX, textY;
if (s.type === 'rect') { if (s.type === 'rect') {
textX = s.x + s.width / 2 textX = s.x + s.width / 2;
textY = s.y + s.height / 2 textY = s.y + s.height / 2;
} else if (s.type === 'circle') { } else if (s.type === 'circle') {
textX = s.x textX = s.x;
textY = s.y textY = s.y;
} else if (s.type === 'polygon') { } else if (s.type === 'polygon' && s.points.length >= 3) {
const totalX = s.points.reduce((sum, p) => sum + p.x, 0) const totalX = s.points.reduce((sum, p) => sum + p.x, 0);
const totalY = s.points.reduce((sum, p) => sum + p.y, 0) const totalY = s.points.reduce((sum, p) => sum + p.y, 0);
textX = totalX / s.points.length textX = totalX / s.points.length;
textY = totalY / s.points.length textY = totalY / s.points.length;
} else {
return; //
} }
// 5. //
this.mainCtx.font = `${dynamicFontSize}px Microsoft YaHei` this.mainCtx.font = `${dynamicFontSize}px Microsoft YaHei`
this.mainCtx.textAlign = 'center' // this.mainCtx.textAlign = 'center'
this.mainCtx.textBaseline = 'middle' // this.mainCtx.textBaseline = 'middle'
// 6. //
const bgRgba = this.parent.hexToRgba(shapeColor, bgOpacity) const textWidth = this.mainCtx.measureText(s.labelName).width
const textColor = this.parent.isLightColor(shapeColor) ? '#000000' : '#ffffff'
// 7.
const textWidth = this.mainCtx.measureText(label.labelName).width
const textHeight = dynamicFontSize const textHeight = dynamicFontSize
this.mainCtx.fillStyle = bgRgba
// +
this.mainCtx.fillStyle = this.instance.hexToRgba(s.color, bgOpacity)
this.mainCtx.fillRect( this.mainCtx.fillRect(
textX - (textWidth + 2 * textPadding) / 2, textX - (textWidth + 2 * textPadding) / 2,
textY - (textHeight + 2 * textPadding) / 2, textY - (textHeight + 2 * textPadding) / 2,
@ -630,15 +613,18 @@ export default {
textHeight + 2 * textPadding textHeight + 2 * textPadding
) )
// 8. //
this.mainCtx.fillStyle = textColor this.mainCtx.fillStyle = this.instance.isLightColor(s.color) ? '#000000' : '#ffffff'
this.mainCtx.fillText(label.labelName, textX, textY) this.mainCtx.fillText(s.labelName, textX, textY)
}) })
this.mainCtx.restore() this.mainCtx.restore()
}, },
// labelId/color //
on() {},
// labelName/color
setShapeData(id, data) { setShapeData(id, data) {
const shape = this.shapes.find(s => s.id === id) const shape = this.shapes.find(s => s.id === id)
if (shape) Object.assign(shape, data) if (shape) Object.assign(shape, data)
@ -657,7 +643,7 @@ export default {
return tool.init() return tool.init()
}, },
// // /线
handleWheelZoom(e) { handleWheelZoom(e) {
e.preventDefault() e.preventDefault()
const delta = e.deltaY > 0 ? -0.1 : 0.1 const delta = e.deltaY > 0 ? -0.1 : 0.1
@ -666,9 +652,11 @@ export default {
if (this.ailabelInstance?.setScale) { if (this.ailabelInstance?.setScale) {
this.ailabelInstance.setScale(this.zoomScale) this.ailabelInstance.setScale(this.zoomScale)
} }
//
this.ailabelInstance?.redrawMainCanvas?.()
}, },
// labelId // labelName/color
loadLabelData() { loadLabelData() {
if (!this.ailabelInstance || !this.currentImage) return if (!this.ailabelInstance || !this.currentImage) return
@ -683,12 +671,12 @@ export default {
this.labelShapeMap = JSON.parse(JSON.stringify(this.currentImage.labelShapeMap)) this.labelShapeMap = JSON.parse(JSON.stringify(this.currentImage.labelShapeMap))
} }
// // /
const shapes = this.currentImage.shapes || [] const shapes = this.currentImage.shapes || []
shapes.forEach(shapeData => { shapes.forEach(shapeData => {
try { try {
let shapeId = '' let shapeId = ''
// //
if (shapeData.type === 'rect' && shapeData.x !== undefined) { if (shapeData.type === 'rect' && shapeData.x !== undefined) {
shapeId = this.ailabelInstance.drawRect?.(shapeData.x, shapeData.y, shapeData.width, shapeData.height) || shapeData.id shapeId = this.ailabelInstance.drawRect?.(shapeData.x, shapeData.y, shapeData.width, shapeData.height) || shapeData.id
} else if (shapeData.type === 'polygon' && shapeData.points && shapeData.points.length >= 3) { } else if (shapeData.type === 'polygon' && shapeData.points && shapeData.points.length >= 3) {
@ -697,11 +685,12 @@ export default {
shapeId = this.ailabelInstance.drawCircle?.(shapeData.x, shapeData.y, shapeData.radius) || shapeData.id shapeId = this.ailabelInstance.drawCircle?.(shapeData.x, shapeData.y, shapeData.radius) || shapeData.id
} }
// labelIdcolor //
if (shapeId) { if (shapeId) {
this.ailabelInstance.setShapeData?.(shapeId, { this.ailabelInstance.setShapeData?.(shapeId, {
labelId: shapeData.labelId, // ID labelName: shapeData.labelName || '未知标签',
color: shapeData.color || this.shapeColorMap[shapeData.type] // color: shapeData.color || '#00ff00',
labelId: shapeData.labelId
}) })
} }
} catch (e) { } catch (e) {
@ -710,28 +699,31 @@ export default {
}) })
}, },
// //
zoomIn() { zoomIn() {
this.zoomScale = Math.min(2.0, this.zoomScale + 0.1) this.zoomScale = Math.min(2.0, this.zoomScale + 0.1)
if (this.ailabelInstance?.setScale) { if (this.ailabelInstance?.setScale) {
this.ailabelInstance.setScale(this.zoomScale) this.ailabelInstance.setScale(this.zoomScale)
} }
this.ailabelInstance?.redrawMainCanvas?.()
}, },
// //
zoomOut() { zoomOut() {
this.zoomScale = Math.max(0.5, this.zoomScale - 0.1) this.zoomScale = Math.max(0.5, this.zoomScale - 0.1)
if (this.ailabelInstance?.setScale) { if (this.ailabelInstance?.setScale) {
this.ailabelInstance.setScale(this.zoomScale) this.ailabelInstance.setScale(this.zoomScale)
} }
this.ailabelInstance?.redrawMainCanvas?.()
}, },
// //
resetZoom() { resetZoom() {
this.zoomScale = 1.0 this.zoomScale = 1.0
if (this.ailabelInstance?.setScale) { if (this.ailabelInstance?.setScale) {
this.ailabelInstance.setScale(this.zoomScale) this.ailabelInstance.setScale(this.zoomScale)
} }
this.ailabelInstance?.redrawMainCanvas?.()
}, },
// //
@ -774,6 +766,7 @@ export default {
handleShapeDeleted() {}, handleShapeDeleted() {},
selectLabel() {}, selectLabel() {},
findShapeIdsByLabelId() { return [] }, findShapeIdsByLabelId() { return [] },
findLabelById() { return null },
removeLabelById() {}, removeLabelById() {},
confirmAddLabel() {}, confirmAddLabel() {},
deleteSelected() {}, deleteSelected() {},
@ -794,7 +787,7 @@ export default {
// //
.labeling-page-container { .labeling-page-container {
width: 100%; width: 100%;
height: 92vh; height: 90.5vh;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
overflow: hidden; overflow: hidden;

View File

@ -34,8 +34,8 @@
<!-- 无效数据标记 --> <!-- 无效数据标记 -->
<el-checkbox v-model="isInvalidData" label="标为无效数据"></el-checkbox> <el-checkbox v-model="isInvalidData" label="标为无效数据"></el-checkbox>
<!-- 保存按钮组 --> <!-- 保存按钮组 -->
<el-button type="primary" @click="saveLabel" class="save-btn">保存</el-button> <!-- <el-button type="primary" @click="saveLabel" class="save-btn">保存</el-button>-->
<el-button type="success" @click="saveAndContinue" class="save-btn">保存并继续</el-button> <el-button type="primary" @click="saveAndContinue" class="save-btn">保存并继续</el-button>
</div> </div>
</div> </div>
@ -1057,6 +1057,14 @@ export default {
} }
}, },
nextImage1() {
if (this.currentImageIndex < this.imageList.length - 1) {
this.switchImage(this.currentImageIndex);
} else {
this.$message.info('已是最后一张!');
}
},
async switchImage(index) { async switchImage(index) {
// 1. // 1.
if (this.ailabelInstance?.getShapes()?.length > 0 || this.isInvalidData) { if (this.ailabelInstance?.getShapes()?.length > 0 || this.isInvalidData) {
@ -1182,7 +1190,7 @@ export default {
height: Number(shape.height) || 0, height: Number(shape.height) || 0,
points: points.map(p => ({ x: p[0], y: p[1] })) points: points.map(p => ({ x: p[0], y: p[1] }))
}), }),
boxType: shape.type === 'rect' ? "1" : "2", // 1- 2- boxType: '4', // 1- 2-
textContent: ocrData.textContent || '' // OCRtextContent textContent: ocrData.textContent || '' // OCRtextContent
}); });
} }
@ -1201,6 +1209,7 @@ export default {
this.$message.success('OCR标注保存成功'); this.$message.success('OCR标注保存成功');
this.currentImage.isInvalid = this.isInvalidData; this.currentImage.isInvalid = this.isInvalidData;
this.imageList[this.currentImageIndex] = this.currentImage; this.imageList[this.currentImageIndex] = this.currentImage;
await this.loadTreeData();
return Promise.resolve(res); return Promise.resolve(res);
} else { } else {
const errorMsg = '保存失败:' + (res.msg || '接口异常'); const errorMsg = '保存失败:' + (res.msg || '接口异常');
@ -1221,10 +1230,12 @@ export default {
try { try {
await this.saveLabel(); await this.saveLabel();
this.isInvalidData = false; this.isInvalidData = false;
if (this.currentImageIndex < this.imageList.length - 1) { if (this.currentImageIndex <= this.imageList.length - 1) {
this.nextImage(); this.nextImage1();
} else { } else {
this.$message.info('已是最后一张图片!'); this.$router.push({
path: '/data/labeling',
});
} }
} catch (err) { } catch (err) {
this.$message.error('保存失败,无法继续'); this.$message.error('保存失败,无法继续');