UNPKG

simple-image-label

Version:

Javascript image annotate, use in deep learning

607 lines (600 loc) 26 kB
import "./simpleImageLabel.css"; "use strict"; function $9031b660573524d6$export$b7d58db314e0ac27(obj) { let objClone = Array.isArray(obj) ? [] : {}; if (obj && typeof obj === "object") { for(let key in obj)if (obj.hasOwnProperty(key)) { obj[key] && typeof obj[key]; objClone[key] = obj[key]; } } return objClone; } function $9031b660573524d6$export$8d92423c30b5888a() { var s = []; var hexDigits = "0123456789abcdef"; for(var i = 0; i < 36; i++)s[i] = hexDigits.substr(Math.floor(Math.random() * 0x10), 1); s[14] = "4"; // bits 12-15 of the time_hi_and_version field to 0010 s[8] = s[13] = s[18] = s[23] = "-"; var uuid = s.join(""); return uuid; } function $9031b660573524d6$export$248d38f6296296c5(obj1, obj2) { if (typeof obj1 !== "object" || typeof obj2 !== "object") return obj1 === obj2; for(let key in obj1)if (obj1.hasOwnProperty(key)) { if (obj1[key] !== obj2[key]) return false; } return true; } function $9031b660573524d6$export$526bad2e66ad9dff(str) { if (str.indexOf("%") > -1) return +(str.replace("%", "") / 100); if (str.indexOf("px") > -1) return +(str.replace("px", "") / 100); } function $9031b660573524d6$export$42dea53aa0f06782(num) { return num * 100 + "%"; } function $9031b660573524d6$export$2176b0e68c10508f(imgUrl) { return new Promise((resolve, reject)=>{ const labelImage = new Image(); labelImage.src = imgUrl; labelImage.onload = ()=>{ resolve({ width: labelImage.width, height: labelImage.height }); }; }); } "use strict"; class $4b8e7a2fa3c0207e$var$SimpleImageLabel { constructor(options){ // 默认图片标注区域 this.imageLabelAreaEl = document.getElementById(options.el || "imageLabelArea"); // 图片路径 this.imageUrl = options.imageUrl; // 标签集合 this.labels = options.labels || []; // label被左键点击的回调 this.labelClick = options.labelClick; // label右键点击回调 this.contextmenu = options.contextmenu; // 发生错误的回调 this.error = options.error; // 只读 this.readOnly = options.readOnly || false; // 鼠标按下时 this.isMouseDown = false; // labels容器,即id为labelsContainer的div this.labelsContainer = null; this.defaultZIndex = 0; // labelsContainer的真实宽高 this.$w = 0; this.$h = 0; // 8个缩放点 this.resizeDotClasses = [ "n", "s", "w", "e", "nw", "ne", "sw", "se" ]; // 上,下,左,右,左上,右上,左下, 右下 this.resizeDotName = null; // 当前被点击的点名称 // 新建的label this.labelItem = null; this.labelItemTemp = null; // 开始及结束点 this.startPoint = null; this.startPointTemp = null; this.endPoint = null; // 当前label相对于labelsContainer左上顶点的位置 this.labelRelativePointContainer = { x: null, y: null }; // 当前被激活的label的uuid this.activeUuid = null; // 图片详情 {width} {height} this.imageInfo = null; this.init(); } init() { this.imageInfo = null; if (!this.labelsContainer) // 初始化创建DOM元素 this.imageLabelAreaEl.innerHTML = ` <div class="__simple-image-label__"> <div class="s-image-content"> <img id="label-bg_img" src="${this.imageUrl}"> <div id="labelsContainer" class="label-content"></div> </div> </div> `; const img = document.getElementById("label-bg_img"); if (img.src !== this.imageUrl) img.src = this.imageUrl; img.onload = ()=>{ this.labelAreaEvent(); this.resizeImage(); }; img.onerror = (err)=>{ if (this.error && typeof this.error === "function") this.error(err); }; this.labelsContainer = document.getElementById("labelsContainer"); this.initLabelElement(); } // 根据图片进行缩放宽度,如果图片宽度小于当前可视区域则__simple-image-label__宽度就 = 图片宽度,否则为100% resizeImage() { const setImg = ()=>{ const { width: width } = this.imageInfo; const imageContent = this.imageLabelAreaEl.querySelector(".__simple-image-label__"); if (this.imageLabelAreaEl.clientWidth >= width) imageContent.style.width = (0, $9031b660573524d6$export$42dea53aa0f06782)(width / this.imageLabelAreaEl.clientWidth); else imageContent.style.width = "100%"; this.$w = this.labelsContainer.clientWidth; this.$h = this.labelsContainer.clientHeight; }; if (this.imageInfo) { setImg(); return; } // 获取图片原始宽高 (0, $9031b660573524d6$export$2176b0e68c10508f)(this.imageUrl).then((res)=>{ this.imageInfo = res; setImg(); }); } initLabelElement() { // 如果有传入labels this.labels.forEach((label)=>{ // 初始化uuid if (!label.uuid) label.uuid = (0, $9031b660573524d6$export$8d92423c30b5888a)(); this.createLabelElement(label); }); } // 事件 labelAreaEvent() { this.$w = this.labelsContainer.clientWidth; this.$h = this.labelsContainer.clientHeight; this.startPoint = { x: 0, y: 0 }; this.endPoint = { x: 0, y: 0 }; this.labelItem = { uuid: "", x: 0, y: 0, width: 0, height: 0, name: "", color: "" }; this.labelsContainer.onmousedown = (e)=>this.mousedown(e); // 鼠标移动事件 this.labelsContainer.onmousemove = (e)=>this.mousemove(e); // 鼠标抬起事件 this.labelsContainer.onmouseup = (e)=>this.mouseup(e); // 右键点击 this.labelsContainer.oncontextmenu = (e)=>{ e.preventDefault(); if (this.contextmenu && typeof this.contextmenu === "function") this.contextmenu(e); }; if (this.readOnly) { window.removeEventListener("resize", this.resize.bind(this), false); this.labelsContainer.style.cursor = "default"; this.clearAllLabelActive(); } else this.labelsContainer.style.cursor = "crosshair"; // 监听浏览器缩放,改变label的宽高 window.addEventListener("resize", this.resize.bind(this), false); this.resizeDotDisplayStatus(); } resize() { this.$w = this.labelsContainer.clientWidth; this.$h = this.labelsContainer.clientHeight; this.resizeImage(); } resizeDotDisplayStatus() { const allResizeDot = document.querySelectorAll(".resize-dot"); allResizeDot.forEach((el)=>{ if (this.readOnly) el.style.display = "none"; else el.style.display = "block"; }); } mousedown(e) { if (this.readOnly) return; if (e.button !== 0) return; // 不是左键点击则不操作 this.isMouseDown = true; const isLabelText = e.target.className.includes("label-text"); const isLabelItem = e.target.className.includes("label-item"); const isResizeDot = e.target.className.includes("resize-dot"); const uuid = isLabelItem ? e.target.id : e.target.parentNode.id; // 点击了边框上的点 if (isResizeDot) { this.labelItem = this.getLabelByUuid(e.target.parentNode.id); this.labelItemTemp = (0, $9031b660573524d6$export$b7d58db314e0ac27)(this.labelItem); this.endPoint = { x: this.labelItemTemp.x + this.labelItemTemp.width, y: this.labelItemTemp.y + this.labelItemTemp.height }; this.resizeDotName = this.getLabelDot(e); this.dragListen(uuid, isLabelText); this.removeDragListen(); return; } else // 点击整个label时,设置激活状态 if (isLabelItem || isLabelText) { this.clearAllLabelActive(); this.setLabelActive(uuid); this.dragListen(uuid, isLabelText); return; } const clientLeft = e.clientX - this.getLabelsContainerRelativePoints().x; const clientTop = e.clientY - this.getLabelsContainerRelativePoints().y; let x = clientLeft / this.$w; // 转换为百分比 let y = clientTop / this.$h; this.startPoint = { x: x, y: y }; if (!this.startPointTemp) this.startPointTemp = (0, $9031b660573524d6$export$b7d58db314e0ac27)(this.startPoint); // 设置labelItem this.labelItem.uuid = (0, $9031b660573524d6$export$8d92423c30b5888a)(); this.labelItem.x = x; this.labelItem.y = y; this.labelItem.width = 0; this.labelItem.height = 0; this.createLabelElement(this.labelItem); } mousemove(e) { if (this.readOnly) return; const isLabelText = e.target.className.includes("label-text"); const isLabelItem = e.target.className.includes("label-item"); const isResizeDot = e.target.className.includes("resize-dot"); const uuid = isLabelItem ? e.target.id : e.target.parentNode.id; if (!isResizeDot) { if (isLabelItem || isLabelText) { if (this.isMouseDown) this.dragListen(uuid, isLabelText); else { this.mouseEnterLabel(uuid); this.dragListen(uuid, isLabelText); } } } else this.removeDragListen(uuid, isLabelText); const clientLeft = e.clientX - this.getLabelsContainerRelativePoints().x; const clientTop = e.clientY - this.getLabelsContainerRelativePoints().y; let x = clientLeft / this.$w; let y = clientTop / this.$h; this.endPoint = { x: x, y: y }; if (this.labelItem.uuid) this.calc(this.labelItem, this.startPoint, this.endPoint); } mouseup(e) { if (this.readOnly) return; const clientLeft = e.clientX - this.getLabelsContainerRelativePoints().x; const clientTop = e.clientY - this.getLabelsContainerRelativePoints().y; let x = clientLeft / this.$w; let y = clientTop / this.$h; this.endPoint = { x: x, y: y }; this.calc(this.labelItem, this.startPoint, this.endPoint); // 如果开始点与结束点一样,则删除当前dom元素 if ((0, $9031b660573524d6$export$248d38f6296296c5)(this.startPoint, this.endPoint) || this.labelItem.width === 0 && this.labelItem.height === 0) this.removeLabelByUuid(this.labelItem.uuid); // 重置 this.labelItem = {}; this.isMouseDown = false; this.resizeDotName = null; this.labelItemTemp = null; } // 根据clientX及clientY获取labelsContainer相对于body的左侧x,y点的位置 getLabelsContainerRelativePoints() { const labelExternalEl = document.querySelector(".__simple-image-label__"); return { x: labelExternalEl.getBoundingClientRect().x, y: labelExternalEl.getBoundingClientRect().y }; } createLabelElement(labelItem) { const { x: x, y: y, width: width, height: height, uuid: uuid, color: color, name: name } = labelItem; this.clearAllLabelActive(); // 创建label元素 const labelElement = document.createElement("div"); labelElement.className = "label-item"; labelElement.id = uuid; labelElement.style.left = (0, $9031b660573524d6$export$42dea53aa0f06782)(x); labelElement.style.top = (0, $9031b660573524d6$export$42dea53aa0f06782)(y); labelElement.style.width = (0, $9031b660573524d6$export$42dea53aa0f06782)(width); labelElement.style.height = (0, $9031b660573524d6$export$42dea53aa0f06782)(height); labelElement.style.position = "absolute"; labelElement.style.border = "1px solid rgb(58,238,121)"; labelElement.style.backgroundColor = "rgba(191,191,191,.5)"; labelElement.style.zIndex = this.labels.length; if (color) labelElement.style.borderColor = color; // 设置data-uuid,可以通过e.target.uuid获取uuid的值 // labelElement.setAttribute('data-uuid', uuid) // 创建可以拖动的点 this.resizeDotClasses.forEach((resizeDotClass)=>{ const labelDragElement = document.createElement("div"); labelDragElement.className = `resize-dot resize-dot-${resizeDotClass}`; labelElement.appendChild(labelDragElement); }); // 设置文字 const labelText = document.createElement("div"); labelText.className = "label-text"; labelText.innerText = name; if (name) { labelText.style.display = "block"; if (color) labelText.style.color = color; } labelElement.appendChild(labelText); this.labelsContainer.appendChild(labelElement); } // 计算两点的宽高 calc(labelItem, startPoint, endPoint) { // 如果开始与结束的点一样,则不进行任何操作 if (!(0, $9031b660573524d6$export$248d38f6296296c5)(startPoint, endPoint)) { labelItem.width = endPoint.x - startPoint.x; labelItem.height = endPoint.y - startPoint.y; if (labelItem.uuid && !this.getLabelByUuid(labelItem.uuid)) // 添加到labels this.labels.push(labelItem); else this.changeLabelSize(labelItem, endPoint); } } // 动态改变label的宽高 changeLabelSize(labelItem, endPoint) { const label = document.getElementById(labelItem.uuid); if (!label) return; const { x: x, y: y, width: width, height: height, uuid: uuid } = labelItem; if (!this.resizeDotName) { const w = Math.abs(width); const h = Math.abs(height); label.style.width = (0, $9031b660573524d6$export$42dea53aa0f06782)(width <= 0 ? 0 : w); label.style.height = (0, $9031b660573524d6$export$42dea53aa0f06782)(height <= 0 ? 0 : h); label.style.left = (0, $9031b660573524d6$export$42dea53aa0f06782)(x); label.style.top = (0, $9031b660573524d6$export$42dea53aa0f06782)(y); } else { // 原底部Y轴的距离 const oldBottomY = this.labelItemTemp.y + this.labelItemTemp.height; if (this.resizeDotName.includes("n")) { const _y = this.labelItemTemp.y - endPoint.y; const _h = _y > 0 ? this.labelItemTemp.height + Math.abs(_y) : this.labelItemTemp.height - Math.abs(_y); // 新旧label底部y距离顶部的距离 const top = endPoint.y >= oldBottomY ? oldBottomY : endPoint.y; label.style.top = (0, $9031b660573524d6$export$42dea53aa0f06782)(top); label.style.height = (0, $9031b660573524d6$export$42dea53aa0f06782)(_h < 0 ? 0 : _h); } if (this.resizeDotName.includes("s")) { const _h = endPoint.y - y; label.style.height = (0, $9031b660573524d6$export$42dea53aa0f06782)(_h <= 0 ? 0 : _h); } if (this.resizeDotName.includes("w")) { const _l = this.labelItemTemp.x + this.labelItemTemp.width - endPoint.x <= 0 ? this.labelItemTemp.x + this.labelItemTemp.width : endPoint.x; label.style.left = (0, $9031b660573524d6$export$42dea53aa0f06782)(_l); const _w = this.labelItemTemp.x - endPoint.x < 0 ? this.labelItemTemp.width - Math.abs(this.labelItemTemp.x - endPoint.x) : this.labelItemTemp.width + Math.abs(this.labelItemTemp.x - endPoint.x); label.style.width = (0, $9031b660573524d6$export$42dea53aa0f06782)(_w <= 0 ? 0 : _w); } if (this.resizeDotName.includes("e")) label.style.width = (0, $9031b660573524d6$export$42dea53aa0f06782)(x <= 0 ? endPoint.x : endPoint.x - x); } const attr = { x: (0, $9031b660573524d6$export$526bad2e66ad9dff)(label.style.left), y: (0, $9031b660573524d6$export$526bad2e66ad9dff)(label.style.top), height: (0, $9031b660573524d6$export$526bad2e66ad9dff)(label.style.height), width: (0, $9031b660573524d6$export$526bad2e66ad9dff)(label.style.width) }; this.setLabelByUuid(uuid, attr); } // 根据uuid查询label getLabelByUuid(uuid) { const item = this.labels.find((item)=>item.uuid === uuid); return item; } setLabelByUuid(uuid, attr = {}) { const label = document.getElementById(uuid); if (!label) return; const labelNameEl = label.querySelector(".label-text"); const keys = Object.keys(attr); this.labels.forEach((item)=>{ if (item.uuid === uuid) keys.forEach((key)=>{ // if (Object.hasOwn(item, key)) { item[key] = attr[key]; if (item.name) { labelNameEl.style.display = "block"; labelNameEl.innerText = item.name; } if (item.color) { label.style.borderColor = item.color; labelNameEl.style.color = item.color; } // } }); }); } // 根据uuid删除label removeLabelByUuid(uuid) { const label = document.getElementById(uuid); this.labels = Object.assign([], this.labels.filter((item)=>item.uuid !== uuid)); if (label) label.parentNode.removeChild(label); if (this.labelClick && typeof this.labelClick === "function") this.labelClick(null); } // 删除所有label removeAllLabels() { const labels = document.querySelectorAll(".label-item"); labels.forEach((item)=>{ item.parentNode.removeChild(item); }); this.labels = []; } // 清除所有标签的激活状态 clearAllLabelActive() { // 获取所有labels元素 const labelElements = document.querySelectorAll(".label-item"); // 如果存在labelElements,则移除className为label-active if (labelElements.length) labelElements.forEach((item)=>{ item.classList.remove("label-item-active"); }); } // 设置当前label的激活状态 setLabelActive(uuid) { const label = document.getElementById(uuid); if (!label) return; // 设置当前label的激活状态 label.classList.add("label-item-active"); if (this.labelClick && typeof this.labelClick === "function") this.labelClick(this.getLabelByUuid(uuid)); } // 根据dot获取label getLabelDot(e) { const dotType = e.target.className.replace("resize-dot resize-dot-", ""); this.clearAllLabelActive(); this.setLabelActive(e.target.parentNode.id); return dotType; } // 鼠标移入label mouseEnterLabel(uuid) { if (this.readOnly) return; const label = document.getElementById(uuid); if (!label) return; label.style.cursor = "default"; } // 拖动事件 dragListen(uuid, isText = false) { const label = document.getElementById(uuid); if (!label) return; let textEl = isText ? label.querySelector(".label-text") : null; if (isText) { textEl.onmousedown = (e)=>this.dragStart(e); textEl.onmousemove = (e)=>this.dragLabel(e); textEl.onmouseup = (e)=>this.dragEnd(e); return; } label.onmousedown = (e)=>this.dragStart(e); label.onmousemove = (e)=>this.dragLabel(e); label.onmouseup = (e)=>this.dragEnd(e); } removeDragListen(uuid, isText = false) { const label = document.getElementById(uuid); if (!label) return; let textEl = isText ? label.querySelector(".label-text") : null; label.onmousedown = null; label.onmousemove = null; label.onmouseup = null; if (isText) { textEl.onmousedown = null; textEl.onmousemove = null; textEl.onmouseup = null; } } // 监听拖拽事件 dragStart(e) { if (this.readOnly) return; const label = document.getElementById(e.target.id || e.target.parentNode.id); if (!label) return; label.style.cursor = "move"; this.labelRelativePointContainer.x = e.pageX - label.offsetLeft; this.labelRelativePointContainer.y = e.pageY - label.offsetTop; this.defaultZIndex = label.style.zIndex; // 将选中的label的z-index设置为最高 label.style.zIndex = 100; } // 拖拽label dragLabel(e) { if (this.readOnly) return; e.preventDefault(); if (!this.labelRelativePointContainer.x || !this.labelRelativePointContainer.y) return; const label = document.getElementById(e.target.id || e.target.parentNode.id); // 拖拽事件获取拖拽最终的坐标 if (!label) return; if (label.style.cursor === "default") return; const currentLabelWidth = label.style.width ? (0, $9031b660573524d6$export$526bad2e66ad9dff)(label.style.width) : 0; const currentLabelHeight = label.style.height ? (0, $9031b660573524d6$export$526bad2e66ad9dff)(label.style.height) : 0; const leftPointPercent = 1 - currentLabelWidth; // 原label div 左侧的点与x轴0点的距离 const topPointPercent = 1 - currentLabelHeight; // 原label div 顶部的点与y轴0点的距离 const leftDistance = e.pageX - this.labelRelativePointContainer.x; // 根据移动,计算当前label div的左侧距离x轴0点的距离 const topDistance = e.pageY - this.labelRelativePointContainer.y; // 根据移动,计算当前label div的顶部的距离y轴0点的距离 label.style.left = leftDistance / this.$w <= 0 ? 0 : leftDistance / this.$w >= leftPointPercent ? (0, $9031b660573524d6$export$42dea53aa0f06782)(leftPointPercent) : (0, $9031b660573524d6$export$42dea53aa0f06782)(leftDistance / this.$w); label.style.top = topDistance / this.$h <= 0 ? 0 : topDistance / this.$h >= topPointPercent ? (0, $9031b660573524d6$export$42dea53aa0f06782)(topPointPercent) : (0, $9031b660573524d6$export$42dea53aa0f06782)(topDistance / this.$h); } // 监听拖拽结束事件 dragEnd(e) { if (this.readOnly) return; const label = document.getElementById(e.target.id || e.target.parentNode.id); if (!label) return; label.style.cursor = "default"; this.labels.forEach((item)=>{ if (item.uuid === e.target.id) { item.x = (0, $9031b660573524d6$export$526bad2e66ad9dff)(label.style.left); item.y = (0, $9031b660573524d6$export$526bad2e66ad9dff)(label.style.top); } }); this.labelRelativePointContainer.x = null; this.labelRelativePointContainer.y = null; // 恢复label的z-index label.style.zIndex = this.defaultZIndex; } // 获取所有label getLabels() { return this.labels; } // 获取激活的label activeLabel() { const uuid = document.querySelector(".label-item-active").id; return this.labels.find((item)=>item.uuid === uuid); } // 重新设置图片 setImage(imageUrl) { this.removeAllLabels(); this.imageUrl = imageUrl; this.init(); } // 重设labels setLabels(labels) { this.labels = labels; this.init(); } // 获取图片原始大小 getImageInfo() { return this.imageInfo; } // 获取默认 (voc) 坐标{x: 左上顶点的x轴,y:左上顶点的y轴,x1:右下顶点的x轴,y1:右下顶点的y轴} getCoordinate(label) { const { height: height, width: width } = this.imageInfo; return { x: label.x * width, y: label.y * height, x1: (label.x + label.width) * width, y1: (label.y + label.height) * height }; } // 获取所有的默认坐标 getLabelsCoordinate() { return this.labels.map((label)=>this.getCoordinate(label)); } // 转换为YOLO坐标[label中心x轴 / 图像总宽度,label中心y轴 / 图像总宽度,label宽百分比(相对于图片宽),label高百分比(相对于图片的高)] convertToYoloCoordinate(label) { const coordinate = this.getCoordinate(label); const { height: height, width: width } = this.imageInfo; const labelCenterX = (coordinate.x + coordinate.x1) / 2; const labelCenterY = (coordinate.y + coordinate.y1) / 2; return [ labelCenterX / width, labelCenterY / height, label.width, label.height ]; } // 获取所有labels的YOLO坐标集合 getLabelsYoloCoordinate() { return this.labels.map((label)=>this.convertToYoloCoordinate(label)); } // 设置是否为只读 setReadOnly(readOnly) { this.readOnly = readOnly; this.labelAreaEvent(); } } var $4b8e7a2fa3c0207e$export$2e2bcd8739ae039 = $4b8e7a2fa3c0207e$var$SimpleImageLabel; export {$4b8e7a2fa3c0207e$export$2e2bcd8739ae039 as default};