A web-based image annotation tool inspired by LabelImg, based on the Vue framework.
Yo, hello netizens, the yearly shift pigeon has finally remembered his blogland password. As the title suggests, today I bring you a vue2 based image annotation tool. As for its birth, I think it was my guide who passed on my proposal (Let Party A use LabelImg for data annotation), said to integrate the function. As of the time of writing this article should be 90% complete, as for the remaining 10% well, ask is to believe that the wisdom of the Internet (in fact, is not included in the data persistence), it must not be difficult to read the article everyone. So not much to say, the following into the main text.
Project Address:/xiao-qi-w/
Video demo: stay tuned...
Let's start with a brief introduction to LabelImg, so that you in front of the screen will have a more accurate idea of my design thinking.
LabelImg is an open source image annotation tool, mainly used to create the training data needed for machine learning models. It supports labeling objects in the image by providing an interface to create rectangular boxes (bounding boxes) and their classification. The main features include:
- graphical user interface (GUI): Allows users to label targets in an image by dragging and dropping.
- Supports multiple formats: Can be exported to Pascal VOC XML, YOLO TXT and COCO JSON formats.
- Image and video support: Can be used to label single images or video frames.
- easy-to-use: The interface is simple and intuitive, suitable for quickly labeling and managing datasets.
Suitable for use in the data preparation phase of object detection tasks.
Its working interface and basic functions are described as follows:
From the figure it is not difficult to see in fact to achieve the function is not much, the focus is on the rectangular box labeled drawing, dragging and scaling above. The front-end want to realize these operations, of course, is recommended to use canvas.
canvas is an element provided by HTML5 for drawing graphics and animations on web pages. It allows images, shapes, and text to be drawn and manipulated directly on a web page, and is controlled primarily through JavaScript. Key features include:
- Drawing API: Provides rich drawing capabilities such as drawing lines, rectangles, circles and images through the CanvasRenderingContext2D interface.
- anime: Can be used to create smooth animation effects.
- image processing: Supports the manipulation and processing of image data.
- each other: Can interact with the user to realize applications such as graphic editing and games.
Use the <canvas>Elements create dynamic, interactive graphics and visual effects.
We'd like to thank the uploader of the B-siteWatanabe Education - Salary Enhancement Course cap (a poem)Silicon ValleyMy vue and canvas skills depend on your videos.
After introducing the front-end content, here's a look at the core code.
The first thing is the page layout, I have divided it in the following way, the code structure and css are as follows:
Code Structure:
css:
Layout styles
<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%; /* leveling off */
overflow-y: auto; /* Show scrollbar on content overflow */
margin-bottom: 10px;
}
/deep/ .cell-clicked {
background: #fdf5e6 !important;
}
</style>
After introducing the layout, let's look at the various responsive variables that need to be used:
responsive state
data() {
return {
/* Image related */
images: [ // Each image can be a more complex object structure, but is guaranteed to have a relative path (url) that can be accessed.
{
id: 1,
url: require('@/assets/'),
},
{
id: 2, url: require('@/assets/'), }, {
url: require('@/assets/'), }, { id: 2, url: require('@/assets/'), }
}, }
]
/* Status variables */
creating: false, // if the canvas is being created
canvasChanged: false, // if the canvas state has changed
showNameInput: false, // If or not the name input popup is displayed.
showSaveAlert: false, // If or not show save alert popup.
/* Zoom related */
dpr: 1, // device pixel ratio
scale: 0, // the scale of the device
maxScale: 3.0, // Maximum zoom ratio
minScale: 0.1, // Minimum scaling factor
adaptiveScale: 0, // Adaptive scaling ratio
scaleStep: 0.1, // Scaling step
/* Previous mouse position */
prevX: 0, // The position of the mouse at the previous moment */.
prevY: 0, // Mouse position in real time */
/* Mouse position in real time */
currentX: 0,
currentY: 0, /* Cache */
currentX: 0, currentY: 0, /* cached */
currentImage: null, // current image
currentImageIndex: 0, // current image index in the image list
targetImageIndex: -1, // targetImageIndex in the image list, used when switching images
wrapper: null, // canvas parent DOM element
canvas: null, // current canvas
bufferCanvas: null, // Offscreen canvas, used for caching.
currentRect: null, // current rectangle
selectedRect: null, // selected rectangle
selectedRectIndex: -1, // subscript of the selected rectangle in the rectangle list
labelName: "", // the label of the rectangle
rects: [], // the rectangle that holds the current image
}
}, // The current image is saved as a rectangle.
Then comes the image part, which is drawn and displayed using canvas, mainly in the following methods:
Load current image
loadImage() {
= new Image();
= ;
= () => {
*= ;
*= ;
();
();
};
}
Setting the canvas size
setSize() {
// No scaling is set.
if ( === 0) {
// Get the width and height of the container
const width = * ;= // Get the width and height of the container.
const width = * ; const height = * ;
// Calculate the scale
const scaleX = width / ;
const scaleY = height / ; // Calculate the scale.
= (scaleX, scaleY); const scaleX = ;; const scaleY = height / ;; // Calculate scale.
= ;
}
// Calculate the scaled image size
const scaledWidth = * ;= (scaleX, scaleY, scaleY, scaleY, scaleY, scaleY, scaleY)
const scaledHeight = * ;}
// Set the canvas width and height
= scaledWidth; ; const scaledHeight = * ; // Set the canvas width and height.
= scaledHeight.
= `${scaledWidth / }px`; // Set the canvas width and height.
// Set the offscreen canvas width and height
= scaledWidth; = scaledHeight; = `${scaledWidth / }px`; = `${scaledHeight / }px`; // Set offscreen canvas width and height.
= scaledHeight.
= `${scaledWidth / }px`; = `${scaledHeight / }px`; // Set the offscreen canvas width and height.
= `${scaledHeight / }px`;
// Set the center
this.$nextTick(() => {
// Set vertical centering
if ( <= scaledHeight / ) {
// Unset center if the canvas height exceeds the parent element's viewport height.
= '';
} else {
// If the canvas height does not exceed the parent element's viewport height, re-center it
= 'center'; }
}
// Set the horizontal center
if ( <= scaledWidth / ) {
// If the canvas width exceeds the parent's viewport width, un-center the canvas.
= ''; } else {
} else {
// If the canvas width does not exceed the parent element's viewport width, re-center it
= 'center'; }
}
}); }
}
canvas (artist's painting)
drawCanvas() {
const ctx = ('2d');
const bufferCtx = ('2d');
const width = ;
const height = ;
// Draw scaled image to off-screen canvas
(0, 0, width, height);
(, 0, 0, width, height);
// Drawing the created rectangle
if () {
();
}
for (const rect of ) {
if (rect === ) {
= 'rgba(255, 0, 0, 0.3)';
} else {
= 'rgba(0, 0, 255, 0.3)';
}
();
}
// Drawing the scaled image to the main canvas
(, 0, 0, width, height);
}
The drawing method uses bufferCanvas, a hidden canvas element as a buffer, mainly to avoid flickering when drawing rectangular box annotations due to the high frequency of redrawing. The drawing effect is as follows:
With the picture, the next step is to consider how to draw a rectangular box labeled, mainly the mouse down event, mouse move event and mouse lift event. Code is as follows:
Mouse over
handleMouseDown(e) {
const mouseX = ;
const mouseY = ;
= mouseX;
= mouseY;
// Find the selected rectangle
= null;
= -1;
for (let i = - 1; i > -1; i--) {
const rect = [i];
if ((mouseX, mouseY)) {
= rect;
= i;
break;
}
}
if () {
// newly built
const bufferCtx = ('2d');
= new Rect(bufferCtx, , mouseX, mouseY, );
} else if () {
// Drag or Zoom
(mouseX, mouseY);
}
}
Mouse over
handleMouseMove(e) {
// Get the coordinates of the mouse in the Canvas.
const mouseX = ;
const mouseY = ;
const mouseX = ;const mouseY = ;
= mouseY.
const ctx = ('2d'); if () { { mouseX; = mouseY
if () {
// New
(0, 0, , ); (, 0, 0); const ctx = ('2d'); if () { // new
(, 0, 0);
// Draw the cross helper line
(); (mouseX * , 0); (, 0, 0); // Draw the crosshair.
(mouseX * , , 0); // Draw the crosshair.
(mouseX * , ); (0, mouseY * ); // Draw the crosshair.
(0, mouseY * ); (, mouseY * ); (, mouseY * )
(, mouseY * ); // Set the color of the line.
= 'red'; // Set the line color
();
if (!) return; return; = 'red'; if (!); if (!)
= mouseX.
= mouseY.
} else if () {
// Drag or zoom
(e, this); }
}
// Re-render if the canvas state changes
if ( || ) {
(); // Draw the background and the existing rectangle.
}
}
Mouse up
handleMouseUp(e) {
if () {
// New
= ;
= ;= false; }
= false; // Add the rectangle to the Rectangle collection.
// Rectangle shape is legal, add it to the rectangle collection.
if ( ! ==
&& ! == ) {
= true; }
}
} else if () {
// Drag or zoom
(, ); }
}
(); }
}
These three mouse events and the actual rectangular box labeled drawing can not be separated from the methods provided by the custom rectangle class, rectangle class definition is as follows:
Custom Rectangle Class
export default class Rect {
constructor(ctx, dpr, startX, startY, scale) {
= 'undefined';
= ();
/* Drawing Related */
= ctx;
= dpr;
= 'rgba(0, 0, 255, 0.3)';
= startX;
= startY;
= startX;
= startY;
= 8 * dpr;
/* Zoom Related */
= scale;
= scale;
/* state-related */
= false;
= false;
= true;
= -1;
}
/**
* Adjustment of start and stop coordinates
*/
adjustCoordinate() {
let temp = 0;
if ( > ) {
temp = ;
= ;
= temp;
}
if ( > ) {
temp = ;
= ;
= temp;
}
}
/**
* Drawing Rectangles
* @param scale zoom ratio
*/
draw(scale) {
if ( === || === ) {
return;
}
= 1 / * scale;
const factor = * ;
const minX = * factor;
const minY = * factor;
const maxX = * factor;
const maxY = * factor;
();
(minX, minY);
(maxX, minY);
(maxX, maxY);
(minX, maxY);
(minX, minY);
= ;
= "#fff";
= 1;
= 'square';
();
();
// Draw four vertices
(minX, maxX, minY, maxY);
}
/**
* Drawing Rectangles四个顶点
* @param minX Minimum horizontal coordinate after scaling
* @param maxX Maximum horizontal coordinate after scaling
* @param minY Minimum vertical coordinate after scaling
* @param maxY Maximum vertical coordinate after scaling
*/
drawVertex(minX, maxX, minY, maxY) {
if ( || ) {
= '#FF4500'; // Drag or zoom state,red vertex
} else {
= '#A7FC00'; // normal state,Cyan Vertex
}
const size = ;
(minX - size / 2, minY - size / 2, size, size);
(maxX - size / 2, minY - size / 2, size, size);
(maxX - size / 2, maxY - size / 2, size, size);
(minX - size / 2, maxY - size / 2, size, size);
}
/**
* According to the coordinates(x, y)Determines whether a rectangle is selected or not
* @param x horizontal coordinate
* @param y vertical coordinate
*/
isSelected(x, y) {
return (x, y) || (x, y) !== -1;
}
/**
* determine the coordinates(x, y)Whether inside the rectangle
* @param x horizontal coordinate
* @param y vertical coordinate
*/
isPointInside(x, y) {
x = x / ;
y = y / ;
return x >= && x <= && y >= && y <= ;
}
/**
* determine the coordinates(x, y)Whether or not it is inside a rectangular vertex
* @param x
* @param y
*/
isPointInsideVertex(x, y) {
x = x / ;
y = y / ;
const vertices = [
{x: , y: },
{x: , y: },
{x: , y: },
{x: , y: }
];
const size = / 2;
let index = -1;
for (let i = 0; i < ; 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;
}
/**
* normalize to yolo specification
* @param width The width of the image
* @param height Height of the image
*/
normalize(width, height) {
const scaledWidth = width * / ;
const scaledHeight = height * / ;
const rectWidth = ( - ) / scaledWidth;
const rectHeight = ( - ) / scaledHeight;
const centerX = ( + ) / 2 / scaledWidth;
const centerY = ( + ) / 2 / scaledHeight;
return {
x: centerX,
y: centerY,
w: rectWidth,
h: rectHeight,
}
}
/**
* mouse click event,Press the coordinates(x, y)
* @param x
* @param y
*/
mouseDown(x, y) {
= (x, y);
if ( !== -1) {
= true;
} else if ((x, y)) {
= true;
}
}
/**
* mouseover event
* @param e mouse event
* @param that vuesubassemblies
*/
mouseMove(e, that) {
const mouseX = ;
const mouseY = ;
if () {
= true;
// Drag Rectangle
const deltaX = mouseX - ;
const deltaY = mouseY - ;
const scaledDeltaX = (mouseX - ) / ;
const scaledDeltaY = (mouseY - ) / ;
+= scaledDeltaX;
+= scaledDeltaY;
+= scaledDeltaX;
+= scaledDeltaY;
+= deltaX;
+= deltaY;
}
if () {
= true;
// Zoom Rectangle
const scaledX = mouseX / ;
const scaledY = mouseY / ;
switch () {
case 0: // upper-left vertex (math.)
= scaledX;
= scaledY;
break;
case 1: // upper right vertex (math.)
= scaledX;
= scaledY;
break;
case 2: // lower right vertex (math.)
= scaledX;
= scaledY;
break;
case 3: // lower left vertex (math.)
= scaledX;
= scaledY;
break;
}
}
();
}
/**
* Mouse up event
* @param width The width of the image
* @param height Height of the image
*/
mouseUp(width, height) {
= false;
= false;
();
// Avoid shrinking the rectangle to an invisible point during scaling
if ( === ) {
+= 1;
}
if( === ) {
+= 1;
}
}
}
At this point, the core function is basically realized, as for the rectangular box of the naming, saving and deleting and other operations, are relatively simple, the demo video has been mentioned, here do not do too much introduction. The final effect is as follows (see the video at the beginning of the article for a complete demonstration of the function):
---------- ---- I--am-- -Split- -cut- -line- ---------- -
Growing up is one day at a time, and a year has slipped by without a word. Compared to me this time last year, I still don't seem to have made much progress, and my rate of progress is too slow even on a yearly basis, so I hope you'll all take this as a warning. See you next year!