受 LabelImg 启发的基于 web 的图像标注工具,基于 Vue 框架
受 LabelImg 启发的基于 web 的图像标注工具,基于 Vue 框架哟,网友们好,年更鸽子终于想起了他的博客园密码。如标题所述,今天给大家带来的是一个基于 vue2 的图像标注工具。至于它诞生的契机呢,应该是我导 pass 掉了我的提议(让甲方使用 LabelImg 进行数据标注),说是要把功能集成起来。截止到写这篇文章时完成度应该有90%,至于剩下的10%嘛,问就是相信网友的智慧(其实就是不包括数据持久化),想必一定难不倒看文章的各位。那么废话不多说,下面进入正文。
项目地址:https://github.com/xiao-qi-w/LabelVue.git
视频演示:敬请期待...
首先我们对 LabelImg 进行一个简单的介绍,这样屏幕前的你会对我的设计思路有更准确地认知。
LabelImg 是一个开源的图像标注工具,主要用于创建机器学习模型所需的训练数据。它支持标注图像中的对象,通过提供界面来创建矩形框(bounding boxes)并对其进行分类。主要特点包括:
[*]图形用户界面:允许用户通过拖拽来标注图像中的目标。
[*]支持多种格式:可以导出为 Pascal VOC XML、YOLO TXT 和 COCO JSON 格式。
[*]支持图像和视频:可用于标注单张图像或视频帧。
[*]易于使用:界面简洁直观,适合快速标注和管理数据集。
适合用于物体检测任务的数据准备阶段。
其工作界面及基本功能介绍如下:
从图中不难看出其实要实现的功能并不多,重点在于矩形框标注的绘制、拖动与缩放上面。而前端想要实现这些操作,当然是推荐使用 canvas。
canvas 是 HTML5 提供的一个元素,用于在网页上绘制图形和动画。它允许在网页中直接绘制和操作图像、形状和文本,主要通过 JavaScript 进行控制。主要特点包括:
[*]绘图 API:通过 CanvasRenderingContext2D 接口提供丰富的绘图功能,如绘制线条、矩形、圆形和图像。
[*]动画:可以用来创建平滑的动画效果。
[*]图像处理:支持图像数据的操作和处理。
[*]交互:可以与用户交互,实现例如图形编辑和游戏等应用。
使用元素可以创建动态、交互式的图形和视觉效果。
在这里鸣谢B站 up 主 渡一教育-提薪课 和 尚硅谷,我的 vue 和 canvas 功底全靠二位的视频撑着。
介绍完了前置内容,下面来看看核心代码。
首先是页面布局,我是按照下面的方式进行划分的,代码结构和 css如下:
代码结构:
css:
布局样式 介绍完布局后,我们再来看看需要用到的各种响应式变量:
响应式状态data() {
return {
/* 图片相关 */
images: [ // 每个图像可以是更复杂的对象结构,但要保证具备可访问到的相对路径(url)
{
id: 1,
url: require('@/assets/cat.jpg'),
},
{
id: 2,
url: require('@/assets/bay.jpg'),
},
],
/* 状态变量 */
creating: false, // 是否正在创建
canvasChanged: false, // 画布状态是否改变
showNameInput: false, // 是否显示标注命名弹窗
showSaveAlert: false, // 是否显示保存提示弹窗
/* 缩放相关 */
dpr: 1, // 设备像素比
scale: 0, // 缩放倍率
maxScale: 3.0, // 最大缩放倍率
minScale: 0.1, // 最小缩放倍率
adaptiveScale: 0, // 自适应缩放倍率
scaleStep: 0.1, // 缩放变化幅度
/* 鼠标上一刻所在位置 */
prevX: 0,
prevY: 0,
/* 鼠标实时位置 */
currentX: 0,
currentY: 0,
/* 缓存 */
currentImage: null, // 当前图像
currentImageIndex: 0, // 当前图像在图像列表中的下标
targetImageIndex: -1, // 目标图像在图像列表中的下标,切换图片时使用
wrapper: null, // canvas 父级元素 DOM
canvas: null, // 当前 canvas
bufferCanvas: null, // 离屏 canvas,缓存用
currentRect: null, // 当前矩形
selectedRect: null, // 选中矩形
selectedRectIndex: -1, // 选中矩形在矩形列表中的下标
labelName: "", // 矩形标签
rects: [], // 保存当前图片的矩形
};
}, 然后是图像部分,使用 canvas 绘制并展示,主要体现在以下方法中:
加载当前图片loadImage() {
this.currentImage = new Image();
this.currentImage.src = this.imagePath;
this.currentImage.onload = () => {
this.currentImage.width *= this.dpr;
this.currentImage.height *= this.dpr;
this.setSize();
this.drawCanvas();
};
}设置画布大小setSize() { // 未设置缩放倍率 if (this.scale === 0) { // 获取所在容器宽高 const width = this.wrapper.clientWidth * this.dpr; const height = this.wrapper.clientHeight * this.dpr; // 计算缩放比例 const scaleX = width / this.currentImage.width; const scaleY = height / this.currentImage.height; this.scale = Math.min(scaleX, scaleY); this.adaptiveScale = this.scale; } // 计算缩放后的图片尺寸 const scaledWidth = this.currentImage.width * this.scale; const scaledHeight = this.currentImage.height * this.scale; // 设置画布宽高 this.canvas.width = scaledWidth; this.canvas.height = scaledHeight; this.canvas.style.width = `${scaledWidth / this.dpr}px`; this.canvas.style.height = `${scaledHeight / this.dpr}px`; // 设置离屏画布宽高 this.bufferCanvas.width = scaledWidth; this.bufferCanvas.height = scaledHeight; this.bufferCanvas.style.width = `${scaledWidth / this.dpr}px`; this.bufferCanvas.style.height = `${scaledHeight / this.dpr}px`; // 设置居中 this.$nextTick(() => { // 设置垂直居中 if (this.wrapper.clientHeightthis.maxX) { temp = this.minX; this.minX = this.maxX; this.maxX = temp; } if (this.minY > this.maxY) { temp = this.minY; this.minY = this.maxY; this.maxY = temp; }}/** * 绘制矩形 * @param scale 缩放倍率 */draw(scale) { if (this.minX === this.maxX || this.minY === this.maxY) { return; } this.realScale = 1 / this.scale * scale; const factor = this.realScale * this.dpr; const minX = this.minX * factor; const minY = this.minY * factor; const maxX = this.maxX * factor; const maxY = this.maxY * factor; this.ctx.beginPath(); this.ctx.moveTo(minX, minY); this.ctx.lineTo(maxX, minY); this.ctx.lineTo(maxX, maxY); this.ctx.lineTo(minX, maxY); this.ctx.lineTo(minX, minY); this.ctx.fillStyle = this.color; this.ctx.strokeStyle = "#fff"; this.ctx.lineWidth = 1; this.ctx.lineCap = 'square'; this.ctx.fill(); this.ctx.stroke(); // 绘制四个顶点 this.drawVertex(minX, maxX, minY, maxY);}/** * 绘制矩形四个顶点 * @param minX 缩放后的最小横坐标 * @param maxX 缩放后的最大横坐标 * @param minY 缩放后的最小纵坐标 * @param maxY 缩放后的最大纵坐标 */drawVertex(minX, maxX, minY, maxY) { if (this.dragging || this.resizing) { this.ctx.fillStyle = '#FF4500'; // 拖动或缩放状态,红色顶点 } else { this.ctx.fillStyle = '#A7FC00'; // 正常状态,青色顶点 } const size = this.vertexSize; this.ctx.fillRect(minX - size / 2, minY - size / 2, size, size); this.ctx.fillRect(maxX - size / 2, minY - size / 2, size, size); this.ctx.fillRect(maxX - size / 2, maxY - size / 2, size, size); this.ctx.fillRect(minX - size / 2, maxY - size / 2, size, size);}/** * 根据坐标(x, y)判断矩形是否被选中 * @param x 横坐标 * @param y 纵坐标 */isSelected(x, y) { return this.isPointInside(x, y) || this.isPointInsideVertex(x, y) !== -1;}/** * 判断坐标(x, y)是否在矩形内部 * @param x 横坐标 * @param y 纵坐标 */isPointInside(x, y) { x = x / this.realScale; y = y / this.realScale; return x >= this.minX && x = this.minY && y = vx - size && x = vy - size && y
页:
[1]