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-zoom-in" @click="zoomIn" 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>
</div>
@ -121,7 +121,10 @@ import {
submitAuditResultAPI //
} from '@/api/data/audit'
import {decryptWithSM4} from "@/utils/sm";
import {
getLabelingTaskLabelTreeAPI,
getLabelPicturesDetailAPI
} from '@/api/data/labeling';
export default {
name: 'LabelingAudit',
data() {
@ -131,6 +134,17 @@ export default {
polygon: '#0099ff',
circle: '#ff9900'
},
// /
labelDrawConfig: {
baseLineWidth: 3,
minLineWidth: 1,
maxLineWidth: 6,
textFontSize: 16,
textPadding: 3,
bgOpacity: 0.4, //
shapeFillOpacity: 0.1, //
textColor: "#ffffff"
},
imageList: [],
currentImageIndex: 0,
currentImage: {},
@ -139,10 +153,12 @@ export default {
labelLoading: false,
ailabelInstance: null,
zoomScale: 1.0,
//
shapeLabelMap: {},
labelShapeMap: {},
//
labelingTaskId: this.convertToNumber(decryptWithSM4(this.$route.query.labelingTaskId || '')),
isAuditing: false, //
//
rejectDialogVisible: false,
auditRemark: '' //
}
@ -157,7 +173,7 @@ export default {
this.loadLabelTreeData()
},
methods: {
//
//
convertToNumber(value) {
if (!value || value === '' || value === 'undefined' || value === 'null') {
return '';
@ -166,7 +182,46 @@ export default {
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() {
try {
this.imageLoading = true
@ -179,24 +234,25 @@ export default {
console.log('审核图片接口返回:', res)
if (res.code === 200 && res.data && Array.isArray(res.data)) {
//
// labelingSampleId
const imageMap = {}
res.data.forEach(item => {
const key = item.labelingSampleId
//
if (!imageMap[key]) {
imageMap[key] = {
id: item.labelingSampleId,
datasetSampleId: item.datasetSampleId || item.labelingSampleId,
filePath: item.filePath,
isInvalidData: item.isInvalidData || '1',
shapes: [],
shapeLabelMap: {},
labelShapeMap: {}
shapes: [], //
shapeLabelMap: {}, // ID -> ID
labelShapeMap: {} // ID -> ID
}
}
//
// JSON
let shapeData = {}
try {
shapeData = JSON.parse(item.labelingCoordinate || '{}')
@ -205,10 +261,10 @@ export default {
shapeData = {}
}
// ID
// ID
const shapeId = `shape_${item.labelingSampleId}_${item.labelId}_${Date.now()}`
//
// labelName/color
const shape = {
id: shapeId,
type: this.getShapeTypeByBoxType(item.boxType),
@ -219,14 +275,15 @@ export default {
radius: shapeData.radius || 0,
points: shapeData.points || [],
labelId: item.labelId,
labelName: item.labelName || '未知标签', //
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].shapeLabelMap[shapeId] = item.labelId
if (!imageMap[key].labelShapeMap[item.labelId]) {
imageMap[key].labelShapeMap[item.labelId] = []
@ -234,7 +291,7 @@ export default {
imageMap[key].labelShapeMap[item.labelId].push(shapeId)
})
//
//
this.imageList = Object.values(imageMap)
if (this.imageList.length === 0) {
@ -253,33 +310,44 @@ export default {
}
},
//
//
async loadLabelTreeData() {
try {
this.labelLoading = true
//
//
this.labelGroups = []
this.labelLoading = true;
if (!this.labelingTaskId) return;
const res = await getLabelingTaskLabelTreeAPI({ labelingTaskId: this.labelingTaskId });
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) {
console.error('加载标签树失败:', error)
this.$message.error('标签数据加载失败,请刷新重试')
this.labelGroups = []
console.error('加载标签树失败:', error);
this.$message.error('标签数据加载失败,请刷新重试');
} finally {
this.labelLoading = false
this.labelLoading = false;
}
},
// box_type
// boxType
getShapeTypeByBoxType(boxType) {
switch (boxType) {
case "1": return 'rect'
case "2": return 'polygon'
case "3": return 'circle'
case "1": return 'rect' //
case "2": return 'polygon' //
case "3": return 'circle' //
default: return 'rect'
}
},
//
// +
onImageLoad() {
this.calcFitScale()
if (this.ailabelInstance?.setScale) {
@ -293,12 +361,12 @@ export default {
})
},
//
//
onImageError() {
this.$message.error(`图片加载失败:${this.currentImage.filePath}`)
},
//
//
calcFitScale() {
const container = this.$refs.labelContainer
const image = this.$refs.labelImage
@ -315,28 +383,28 @@ export default {
this.zoomScale = Math.max(0.1, Math.min(2.0, this.zoomScale))
},
//
//
initAILabel() {
const container = this.$refs.labelContainer
const image = this.$refs.labelImage
if (!container || !image || !this.currentImage.filePath) return
//
//
this.destroyAILabel()
try {
let instance = null
//
// AILabelLib
if (AILabelLib.create) {
instance = AILabelLib.create({
el: container,
image: image,
config: {
enableRect: false,
enablePolygon: false,
enableCircle: false,
enableDrag: false,
enableScale: false,
enableRect: false, //
enablePolygon: false, //
enableCircle: false, //
enableDrag: false, //
enableScale: false, //
style: {
rect: { strokeStyle: '#00ff00', lineWidth: 2, fillStyle: 'rgba(0,255,0,0.1)' },
polygon: { strokeStyle: '#0099ff', lineWidth: 2, fillStyle: 'rgba(0,153,255,0.1)' },
@ -350,13 +418,14 @@ export default {
} else if (window.AILabel) {
instance = window.AILabel({ el: container, image: image })
} else {
instance = this.createSimpleLabelTool(container, image)
//
instance = this.createSimpleLabelTool(container, image, this)
}
if (instance) {
this.ailabelInstance = instance
if (instance.setScale) instance.setScale(this.zoomScale)
this.loadLabelData()
this.loadLabelData() //
}
} catch (e) {
console.error('标注工具初始化失败:', e)
@ -364,7 +433,7 @@ export default {
}
},
//
//
destroyAILabel() {
if (this.ailabelInstance) {
try {
@ -374,8 +443,8 @@ export default {
}
},
//
createSimpleLabelTool(container, image) {
//
createSimpleLabelTool(container, image, instance) {
const tool = {
el: container,
image,
@ -383,9 +452,11 @@ export default {
mainCanvas: null,
mainCtx: null,
zoomScale: 1.0,
instance: instance, //
resizeCanvas: null, // resize
init() {
//
//
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.width = container.offsetWidth
@ -394,27 +465,28 @@ export default {
this.mainCanvas = mainCanvas
this.mainCtx = mainCanvas.getContext('2d')
//
window.addEventListener('resize', () => this.resizeCanvas())
// resizethis
this.resizeCanvas = () => {
this.mainCanvas.width = this.el.offsetWidth
this.mainCanvas.height = this.el.offsetHeight
this.redrawMainCanvas()
}
//
window.addEventListener('resize', this.resizeCanvas)
//
//
this.redrawMainCanvas()
return this
},
resizeCanvas() {
this.mainCanvas.width = this.el.offsetWidth
this.mainCanvas.height = this.el.offsetHeight
this.redrawMainCanvas()
},
//
setScale(scale) {
this.zoomScale = scale
this.resizeCanvas()
},
//
//
drawRect(x, y, w, h) {
const shape = { id: `shape_${Date.now()}`, type: 'rect', x, y, width: w, height: h }
this.shapes.push(shape)
@ -422,6 +494,7 @@ export default {
return shape.id
},
//
drawCircle(x, y, radius) {
const shape = { id: `shape_${Date.now()}`, type: 'circle', x, y, radius }
this.shapes.push(shape)
@ -429,6 +502,7 @@ export default {
return shape.id
},
//
drawPolygon(points) {
const shape = { id: `shape_${Date.now()}`, type: 'polygon', points }
this.shapes.push(shape)
@ -436,25 +510,24 @@ export default {
return shape.id
},
//
clearShapes() {
this.shapes = []
this.mainCtx.clearRect(0, 0, this.mainCanvas.width, this.mainCanvas.height)
},
getShapes() {
return [...this.shapes]
},
// labelName/color
setShapeData(id, data) {
const shape = this.shapes.find(s => s.id === id)
if (shape) Object.assign(shape, data)
},
// ++
redrawMainCanvas() {
this.mainCtx.clearRect(0, 0, this.mainCanvas.width, this.mainCanvas.height)
this.mainCtx.save()
//
// +/
this.mainCtx.translate(
this.el.offsetWidth / 2,
this.el.offsetHeight / 2
@ -465,7 +538,7 @@ export default {
-this.image.naturalWidth / 2,
-this.image.naturalHeight / 2
)
//
//
this.mainCtx.drawImage(
this.image,
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 => {
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') {
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.fillRect(s.x, s.y, s.width, s.height)
} 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.arc(s.x, s.y, s.radius, 0, Math.PI * 2)
this.mainCtx.closePath()
this.mainCtx.fill()
this.mainCtx.stroke()
} else if (s.type === 'polygon') {
this.mainCtx.strokeStyle = s.color || '#0099ff'
this.mainCtx.fillStyle = 'rgba(0,153,255,0.1)'
} else if (s.type === 'polygon' && s.points.length >= 3) {
this.mainCtx.beginPath()
s.points.forEach((point, idx) => {
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.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()
},
// +
destroy() {
this.mainCanvas?.remove()
window.removeEventListener('resize', this.resizeCanvas)
@ -513,22 +635,27 @@ export default {
return tool.init()
},
//
// labelName/color
loadLabelData() {
if (!this.ailabelInstance || !this.currentImage) return
//
this.ailabelInstance.clearShapes?.()
//
// -
if (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 || []
shapes.forEach(shapeData => {
try {
let shapeId = ''
//
if (shapeData.type === 'rect' && shapeData.x !== undefined) {
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) {
@ -537,10 +664,12 @@ export default {
shapeId = this.ailabelInstance.drawCircle?.(shapeData.x, shapeData.y, shapeData.radius) || shapeData.id
}
//
// ID
if (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) {
@ -549,7 +678,7 @@ export default {
})
},
//
// /线
handleWheelZoom(e) {
e.preventDefault()
const delta = e.deltaY > 0 ? -0.1 : 0.1
@ -558,30 +687,35 @@ export default {
if (this.ailabelInstance?.setScale) {
this.ailabelInstance.setScale(this.zoomScale)
}
//
this.ailabelInstance?.redrawMainCanvas?.()
},
//
// 0.12.0
zoomIn() {
this.zoomScale = Math.min(2.0, this.zoomScale + 0.1)
if (this.ailabelInstance?.setScale) {
this.ailabelInstance.setScale(this.zoomScale)
}
this.ailabelInstance?.redrawMainCanvas?.()
},
//
// 0.10.1
zoomOut() {
this.zoomScale = Math.max(0.1, this.zoomScale - 0.1)
if (this.ailabelInstance?.setScale) {
this.ailabelInstance.setScale(this.zoomScale)
}
this.ailabelInstance?.redrawMainCanvas?.()
},
//
// 1.0
resetZoom() {
this.zoomScale = 1.0
if (this.ailabelInstance?.setScale) {
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) {
this.currentImageIndex = index
this.currentImage = this.imageList[index]
this.shapeLabelMap = {}
this.labelShapeMap = {}
//
// DOMDOM
this.$nextTick(() => {
this.initAILabel()
})
},
//
// 1
async auditPass() {
await this.submitAuditResult(1)
},
//
//
openRejectDialog() {
//
this.auditRemark = ''
this.rejectDialogVisible = true
},
//
//
handleDialogClose() {
this.rejectDialogVisible = false
this.auditRemark = ''
},
//
// + 2
async confirmReject() {
//
if (!this.auditRemark.trim()) {
this.$message.warning('请填写审核不通过的原因!')
return
}
//
await this.submitAuditResult(2)
//
this.rejectDialogVisible = false
},
//
// 1-2-
async submitAuditResult(auditStatus) {
if (this.isAuditing) return
if (this.isAuditing) return //
if (!this.currentImage.id) {
this.$message.error('当前图片ID为空无法提交审核')
return
@ -656,26 +800,24 @@ export default {
this.isAuditing = true
const loadingInstance = this.$loading({ text: '提交审核结果中...' })
//
//
const submitParams = {
labelingTaskId: this.labelingTaskId,
labelingSampleId: this.currentImage.id,
auditStatus: auditStatus, // 1-2-
datasetSampleId: this.currentImage.datasetSampleId || '',
auditRemark: auditStatus === 2 ? this.auditRemark.trim() : '' //
auditRemark: auditStatus === 2 ? this.auditRemark.trim() : '' //
}
console.log('提交审核参数:', submitParams)
//
const res = await submitAuditResultAPI(submitParams)
if (res.code === 200) {
this.$message.success(auditStatus === 1 ? '审核通过提交成功!' : '审核不通过提交成功!')
//
if (this.currentImageIndex < this.imageList.length - 1) {
this.nextImage()
await this.loadAuditImages();
this.nextImage1()
}
} else {
this.$message.error('提交失败:' + (res.msg || '接口返回异常'))
@ -696,7 +838,7 @@ export default {
//
.labeling-page-container {
width: 100%;
height: 92vh;
height: 90.5vh;
display: flex;
flex-direction: column;
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;
margin-bottom: 8px;
font-size: 14px;
color: black;
color: #333;
}
.reject-textarea {
@ -907,7 +1056,7 @@ export default {
}
}
//
//
::v-deep .el-scrollbar__bar {
width: 6px;
height: 6px;
@ -916,5 +1065,8 @@ export default {
::v-deep .el-scrollbar__thumb {
background-color: #ccc;
border-radius: 3px;
&:hover {
background-color: #999;
}
}
</style>

View File

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

View File

@ -554,6 +554,17 @@ export default {
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) {
this.currentImageIndex = index;
this.currentImage = this.imageList[index];
@ -630,7 +641,8 @@ export default {
this.$message.success(auditStatus === 1 ? '审核通过提交成功!' : '审核不通过提交成功!');
//
if (this.currentImageIndex < this.imageList.length - 1) {
this.nextImage();
await this.loadAuditImages();
this.nextImage1();
}
} else {
this.$message.error('提交失败:' + (res.msg || '接口返回异常'));

View File

@ -524,7 +524,8 @@ export default {
datasetSampleId: this.currentImage.datasetSampleId || '',
isInvalidData: this.isInvalidData ? "0" : "1", //
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-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="saveLabel" class="save-btn">保存</el-button>-->
<el-button type="primary" @click="saveAndContinue" class="save-btn">保存并继续</el-button>
</div>
</div>
@ -190,7 +190,7 @@ import {
getLabelPicturesAPI, //
saveLabelingDataAPI
} from '@/api/data/labeling'
import {decryptWithSM4} from "@/utils/sm"; //
import {decryptWithSM4, encryptWithSM4} from "@/utils/sm"; //
export default {
name: 'LabelingAdd',
@ -713,21 +713,21 @@ export default {
},
onImageLoad() {
this.imageLoaded = true
this.calcFitScale()
//
if (this.ailabelInstance?.setScale) {
this.ailabelInstance.setScale(this.zoomScale)
this.imageLoaded = true;
// 1.
this.calcFitScale();
// 2. image
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(() => {
if (this.ailabelInstance?.redrawMainCanvas) {
this.ailabelInstance.redrawMainCanvas()
}
// this.recordState()
this.ailabelInstance?.redrawMainCanvas()
})
this.ailabelInstance?.redrawMainCanvas?.();
});
},
getDynamicLineWidth() {
@ -1163,12 +1163,15 @@ export default {
}
},
// isShapeOutOfImage
isShapeOutOfImage(shape) {
if (!this.image) return true; //
//
const imgWidth = this.image.naturalWidth;
const imgHeight = this.image.naturalHeight;
//
// this.image
const currentImage = this.instance.$refs.labelImage;
if (!currentImage) return true; //
//
const imgWidth = currentImage.naturalWidth;
const imgHeight = currentImage.naturalHeight;
//
switch (shape.type) {
case 'rect':
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;
}
},
//
// 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) {
//
const currentShapes = this.ailabelInstance?.getShapes?.() || [];
if (currentShapes.length > 0 || this.isInvalidData) {
try {
//
await this.saveLabel();
} catch (err) {
//
const confirm = await this.$confirm('当前图片标注未保存,是否放弃修改并切换?', '提示', {
type: 'warning'
});
if (!confirm) return;
}
}
//
// 1.
this.destroyAILabel();
// 2.
this.currentImageIndex = index;
this.currentImage = this.imageList[index];
this.imageLoaded = false;
this.imageLoaded = false; //
this.resetLabelState();
// /
this.undoStack = [];
this.redoStack = [];
this.zoomScale = 1.0; // 沿
//
// 3. initAILabelonImageLoad
// imgonImageLoad
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) {
this.$message.success('标注保存成功!');
//
this.saveCurrentLabelData();
await this.loadTreeData();
// /
this.undoStack = [];
this.redoStack = [];
@ -2090,10 +2090,12 @@ export default {
//
await this.saveLabel();
this.isInvalidData = false;
if (this.currentImageIndex < this.imageList.length - 1) {
this.nextImage();
if (this.currentImageIndex <= this.imageList.length - 1) {
this.nextImage1();
} else {
this.$message.info('已是最后一张图片!');
this.$router.push({
path: '/data/labeling',
});
}
} catch (err) {
this.$message.error('保存失败,无法继续');

View File

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

View File

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