受 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 接口提供丰富的绘图功能,如绘制线条、矩形、圆形和图像。
- 动画:可以用来创建平滑的动画效果。
- 图像处理:支持图像数据的操作和处理。
- 交互:可以与用户交互,实现例如图形编辑和游戏等应用。
使用 <canvas> 元素可以创建动态、交互式的图形和视觉效果。
在这里鸣谢B站 up 主 渡一教育-提薪课 和 尚硅谷,我的 vue 和 canvas 功底全靠二位的视频撑着。
介绍完了前置内容,下面来看看核心代码。
首先是页面布局,我是按照下面的方式进行划分的,代码结构和 css如下:
代码结构:
css:
布局样式
<style scoped>
.container {
display: flex;
height: 95vh;
}
.left,
.right {
height: 100%;
flex: 20%;
padding: 1vw;
overflow-y: auto;
display: flex;
flex-direction: column;
}
.left {
flex: 80%;
}
.left-top {
flex: 90%;
height: 94vh;
margin-top: 3vh;
display: flex;
flex-direction: column;
overflow: auto;
box-shadow: 0 2px 4px rgba(0, 0, 0, .12), 0 0 6px rgba(0, 0, 0, .04);
}
.left-bottom {
margin-top: 1vh;
padding: 1vh;
display: flex;
justify-content: center;
justify-items: center;
box-shadow: 0 2px 4px rgba(0, 0, 0, .12), 0 0 6px rgba(0, 0, 0, .04);
}
.right > .label-list,
.right > .image-list {
flex: 50%; /* 平分高度 */
overflow-y: auto; /* 内容溢出时显示滚动条 */
margin-bottom: 10px;
}
/deep/ .cell-clicked {
background: #fdf5e6 !important;
}
</style>
介绍完布局后,我们再来看看需要用到的各种响应式变量:
响应式状态
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.clientHeight <= scaledHeight / this.dpr) {
// 画布高度超过父元素视窗高度时,取消居中设置
this.wrapper.style.justifyContent = '';
} else {
// 画布高度未超过父元素视窗高度时,重新居中设置
this.wrapper.style.justifyContent = 'center';
}
// 设置水平居中
if (this.wrapper.clientWidth <= scaledWidth / this.dpr) {
// 画布宽度超过父元素视窗宽度时,取消居中设置
this.wrapper.style.alignItems = '';
} else {
// 画布宽度未超过父元素视窗宽度时,重新居中设置
this.wrapper.style.alignItems = 'center';
}
});
}
绘制画布
drawCanvas() {
const ctx = this.canvas.getContext('2d');
const bufferCtx = this.bufferCanvas.getContext('2d');
const width = this.canvas.width;
const height = this.canvas.height;
// 绘制缩放后的图片到离屏画布
bufferCtx.clearRect(0, 0, width, height);
bufferCtx.drawImage(this.currentImage, 0, 0, width, height);
// 绘制已创建矩形
if (this.currentRect) {
this.currentRect.draw(this.scale);
}
for (const rect of this.rects) {
if (rect === this.selectedRect) {
rect.color = 'rgba(255, 0, 0, 0.3)';
} else {
rect.color = 'rgba(0, 0, 255, 0.3)';
}
rect.draw(this.scale);
}
// 将缩放后的图片绘制到主画布
ctx.drawImage(this.bufferCanvas, 0, 0, width, height);
}
绘制方法中使用到了 bufferCanvas,一个隐藏的 canvas 元素作为缓存,主要是为了避免绘制矩形框标注时因重绘频率过高产生的画面闪烁现象。绘制效果如下:

有了图片,接下来就是考虑如何绘制矩形框标注了,主要是鼠标按下事件,鼠标移动事件和鼠标抬起事件。代码如下:
鼠标按下
handleMouseDown(e) {
const mouseX = e.offsetX;
const mouseY = e.offsetY;
this.prevX = mouseX;
this.prevY = mouseY;
// 找出被选中的矩形
this.selectedRect = null;
this.selectedRectIndex = -1;
for (let i = this.rects.length - 1; i > -1; i--) {
const rect = this.rects[i];
if (rect.isSelected(mouseX, mouseY)) {
this.selectedRect = rect;
this.selectedRectIndex = i;
break;
}
}
if (this.creating) {
// 新建
const bufferCtx = this.bufferCanvas.getContext('2d');
this.currentRect = new Rect(bufferCtx, this.dpr, mouseX, mouseY, this.scale);
} else if (this.selectedRect) {
// 拖动或缩放
this.selectedRect.mouseDown(mouseX, mouseY);
}
}
鼠标移动
handleMouseMove(e) {
// 获取鼠标在Canvas中的坐标
const mouseX = e.offsetX;
const mouseY = e.offsetY;
this.currentX = mouseX;
this.currentY = mouseY;
const ctx = this.canvas.getContext('2d');
if (this.creating) {
// 新建
ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
ctx.drawImage(this.bufferCanvas, 0, 0);
// 绘制交叉辅助线
ctx.beginPath();
ctx.moveTo(mouseX * this.dpr, 0);
ctx.lineTo(mouseX * this.dpr, this.canvas.height);
ctx.moveTo(0, mouseY * this.dpr);
ctx.lineTo(this.canvas.width, mouseY * this.dpr);
ctx.strokeStyle = 'red'; // 设置线条颜色
ctx.stroke();
if (!this.currentRect) return;
this.currentRect.maxX = mouseX;
this.currentRect.maxY = mouseY;
} else if (this.selectedRect) {
// 拖动或缩放
this.selectedRect.mouseMove(e, this);
}
// 画布状态发生变化重新渲染
if (this.creating || this.selectedRect) {
this.drawCanvas(); // 绘制背景和已有矩形
}
}
鼠标抬起
handleMouseUp(e) {
if (this.creating) {
// 新建
this.currentRect.maxX = e.offsetX;
this.currentRect.maxY = e.offsetY;
this.creating = false;
// 矩形形状合法,加入到矩形集合
if (this.currentRect.minX !== this.currentRect.maxX
&& this.currentRect.minY !== this.currentRect.maxY) {
this.showNameInput = true;
}
} else if (this.selectedRect) {
// 拖动或缩放
this.selectedRect.mouseUp(this.currentImage.width, this.currentImage.height);
}
this.drawCanvas();
}
这三种鼠标事件与实际矩形框标注的绘制离不开自定义矩形类提供的方法,矩形类定义如下:
自定义矩形类
export default class Rect {
constructor(ctx, dpr, startX, startY, scale) {
this.name = 'undefined';
this.timestamp = Date.now();
/* 绘制相关 */
this.ctx = ctx;
this.dpr = dpr;
this.color = 'rgba(0, 0, 255, 0.3)';
this.minX = startX;
this.minY = startY;
this.maxX = startX;
this.maxY = startY;
this.vertexSize = 8 * dpr;
/* 缩放相关 */
this.scale = scale;
this.realScale = scale;
/* 状态相关 */
this.dragging = false;
this.resizing = false;
this.changed = true;
this.vertexIndex = -1;
}
/**
* 调整起止坐标
*/
adjustCoordinate() {
let temp = 0;
if (this.minX > this.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.maxX && y >= this.minY && y <= this.maxY;
}
/**
* 判断坐标(x, y)是否在矩形顶点内部
* @param x
* @param y
*/
isPointInsideVertex(x, y) {
x = x / this.realScale;
y = y / this.realScale;
const vertices = [
{x: this.minX, y: this.minY},
{x: this.maxX, y: this.minY},
{x: this.maxX, y: this.maxY},
{x: this.minX, y: this.maxY}
];
const size = this.vertexSize / 2;
let index = -1;
for (let i = 0; i < vertices.length; i++) {
const vx = vertices[i].x;
const vy = vertices[i].y;
if (x >= vx - size && x <= vx + size && y >= vy - size && y <= vy + size) {
// return i;
index = i; break;
}
}
return index;
}
/**
* 归一化为 yolo 格式
* @param width 所在图片宽度
* @param height 所在图片高度
*/
normalize(width, height) {
const scaledWidth = width * this.scale / this.dpr;
const scaledHeight = height * this.scale / this.dpr;
const rectWidth = (this.maxX - this.minX) / scaledWidth;
const rectHeight = (this.maxY - this.minY) / scaledHeight;
const centerX = (this.maxX + this.minX) / 2 / scaledWidth;
const centerY = (this.maxY + this.minY) / 2 / scaledHeight;
return {
x: centerX,
y: centerY,
w: rectWidth,
h: rectHeight,
}
}
/**
* 鼠标按下事件,按下坐标(x, y)
* @param x
* @param y
*/
mouseDown(x, y) {
this.vertexIndex = this.isPointInsideVertex(x, y);
if (this.vertexIndex !== -1) {
this.resizing = true;
} else if (this.isPointInside(x, y)) {
this.dragging = true;
}
}
/**
* 鼠标移动事件
* @param e 鼠标事件
* @param that vue组件
*/
mouseMove(e, that) {
const mouseX = e.offsetX;
const mouseY = e.offsetY;
if (this.dragging) {
this.changed = true;
// 拖动矩形
const deltaX = mouseX - that.prevX;
const deltaY = mouseY - that.prevY;
const scaledDeltaX = (mouseX - that.prevX) / this.realScale;
const scaledDeltaY = (mouseY - that.prevY) / this.realScale;
this.minX += scaledDeltaX;
this.minY += scaledDeltaY;
this.maxX += scaledDeltaX;
this.maxY += scaledDeltaY;
that.prevX += deltaX;
that.prevY += deltaY;
}
if (this.resizing) {
this.changed = true;
// 缩放矩形
const scaledX = mouseX / this.realScale;
const scaledY = mouseY / this.realScale;
switch (this.vertexIndex) {
case 0: // 左上角顶点
this.minX = scaledX;
this.minY = scaledY;
break;
case 1: // 右上角顶点
this.maxX = scaledX;
this.minY = scaledY;
break;
case 2: // 右下角顶点
this.maxX = scaledX;
this.maxY = scaledY;
break;
case 3: // 左下角顶点
this.minX = scaledX;
this.maxY = scaledY;
break;
}
}
this.draw();
}
/**
* 鼠标抬起事件
* @param width 所在图片宽度
* @param height 所在图片高度
*/
mouseUp(width, height) {
this.dragging = false;
this.resizing = false;
this.adjustCoordinate();
// 避免缩放过程中把矩形缩成看不见的一点
if (this.minX === this.maxX) {
this.maxX += 1;
}
if(this.minY === this.maxY) {
this.maxY += 1;
}
}
}
至此,核心功能基本实现,至于对矩形框的命名、保存与删除等操作,都比较简单,演示视频中已经提到了,这里不做过多介绍。最终效果如下(完整功能演示请看文章开头的视频):

——————————————我———是———分———割———线—————————————
长大后的日子是一天快过一天,一年的时间就这么一声不吭地溜走了,对比去年这个时候的我,貌似还是没有太大的长进,我这进步速度就算是按年算也过于迟缓了,望各位引以为戒。我们有缘明年再见ヾ(•ω•`)o
受 LabelImg 启发的基于 web 的图像标注工具,基于 Vue 框架的更多相关文章
- 图像标注工具labelImg使用方法
最近在做打标签的工作,为了与大家参考学习,总结了在windows的环境下,基于anaconda的图像标注工具labellmg的一种使用方法! 1 搭建anaconda 以前写过怎么搭建anaconda ...
- 基于Web的IIS管理工具
Servant:基于Web的IIS管理工具 Servant for IIS是个管理IIS的简单.自动化的Web管理工具.安装Servant的过程很简单,只要双击批处理文件Install Serva ...
- 给ambari集群里的kafka安装基于web的kafka管理工具Kafka-manager(图文详解)
不多说,直接上干货! 参考博客 基于Web的Kafka管理器工具之Kafka-manager的编译部署详细安装 (支持kafka0.8.0.9和0.10以后版本)(图文详解)(默认端口或任意自定义端口 ...
- Linux 基于WEB开源的系统管理工具webmin
Webmin是目前功能最强大的基于Web的Unix系统管理工具.管理员通过浏览器访问Webmin的各种管理功能并完成相应的管理动作.目前Webmin支持绝大多数的Unix系统,这些系统除了各种版本的l ...
- 开源图像标注工具labelme的安装使用及汉化
一 LabelMe简介 labelme是麻省理工(MIT)的计算机科学和人工智能实验室(CSAIL)研发的图像标注工具,人们可以使用该工具创建定制化标注任务或执行图像标注,项目源代码已经开源. 项目开 ...
- 深度学习图像标注工具VGG Image Annotator (VIA)使用教程
VGG Image Annotator (VIA)是一款开源的图像标注工具,由Visual Geometry Group开发. 可以在线和离线使用,可标注矩形.圆.椭圆.多边形.点和线.标注完成后,可 ...
- javaer有福了,基于Web的mysql数据库管理工具TreeSoft发布了。
基于Web的数据库管理工具TreeSoft数据库管理系统发布快3年了,版本不断更快速迭代开发中,QQ群也积累了300多位粉丝,虽然时不时也掉粉,但总体还是增加中.大家虽然捐赠不多,但是苍蝇再小也是肉啊 ...
- 一款基于 Web 的通用数据管理工具(转载)
一款基于 WEB 的通用数据管控工具 - CloudQuery 前言 前段时间,公司因为业务发展,数据量攀升,老板迫切需要一个工具对数据进行精细化管理,一是确实需要精细化管理:二是因为我们公司小,数据 ...
- Servant:基于Web的IIS管理工具
Servant for IIS是个管理IIS的简单.自动化的Web管理工具.安装Servant的过程很简单,只要双击批处理文件Install Servant Service.bat,然后按照屏幕上的说 ...
- LabelImg 图像图像标注工具
https://github.com/tzutalin/labelImg windos版本请不要把存放在中文目录下,否则打不开.
随机推荐
- 2024已过半,还没试过在vue3中使用ioc容器吗?
Vue3 已经非常强大和灵活了,为什么还要引入 IOC 容器呢?IOC 容器离不开 Class,那么我们就从 Class 谈起 Class的应用场景 一提起 Class,大家一定会想到这是 Vue 官 ...
- Vue源码学习(二十):$emit、$on实现原理
好家伙, 0.一个例子 <!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset= ...
- supervisor.conf部署及维护
启动服务 supervisord -c /etc/supervisord.conf 启动服务 supervisorctl start 关闭服务 supervisorctl stop
- 可视化—gojs 超多超实用经验分享(三)
目录 32.go.Palette 一排放两个 33.go.Palette 基本用法 34.创建自己指向自己的连线 35.设置不同的 groupTemplate 和 linkTemplate 36.监听 ...
- [oeasy]python0026_刷新时间_延迟时间_time_sleep_死循环_while_True
刷新时间 回忆上次内容 time 是一个 module import 他可以做和时间相关的事情 time.time() 得到当前时间戳 time.localtime() 得到本地时间元组 l ...
- oeasy教您玩转vim - 51 - # 读写文件
读写文件 回忆上节课内容 命令行的光标控制 方向键️️️️️可以控制左右移动 shift+️️️️️按照word左右移动光标 ctrl+b 到开头begin ctrl+e 到结尾end ctrl+w ...
- 可视化—gojs 超多超实用经验分享(四)
目录 41.监听连线拖拽结束后的事件 42.监听画布的修改事件 43.监听节点被 del 删除后回调事件(用于实现调用接口做一些真实的删除操作) 44.监听节点鼠标移入移出事件,hover 后显示特定 ...
- AT_agc017_b 题解
洛谷链接&Atcoder 链接 本篇题解为此题较简单做法,请放心阅读. 题目简述 一共有 \(n\) 个格子,给定两个整数 \(A,B\) 分别位于第 \(1\) 和第 \(n\) 格,中间有 ...
- Jenkins 配合Pipeline使用Docker
配合Pipeline使用Docker 许多组织使用Docker跨机器统一构建和测试环境,并为部署应用程序提供高效机制.从Pipeline 2.5及更高版本开始,Pipeline内置了从Jenkinsf ...
- java面试一日一题:1.6/7/8Java内存区域有什么不同吗
问题:请讲下在JDK6 JDK7 JDK8中java内存区域有什么不同吗 分析:该问题主要考察对JVM运行时区域的了解,首先要了解最基本的内存区域划分,然后再去掌握其中的变化,再延申一点,为什么要这样 ...