| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| (function() { |
| 'use strict'; |
|
|
| class SegmentationToolManager { |
| |
| |
| |
| |
| |
| constructor(canvas, width, height) { |
| this.canvas = canvas; |
| this.width = width; |
| this.height = height; |
|
|
| |
| this.maskCanvas = document.createElement('canvas'); |
| this.maskCanvas.width = width; |
| this.maskCanvas.height = height; |
| this.maskCanvas.style.cssText = 'position:absolute;top:0;left:0;pointer-events:none;opacity:0.4;'; |
| this.maskCtx = this.maskCanvas.getContext('2d'); |
|
|
| |
| this.workCanvas = document.createElement('canvas'); |
| this.workCanvas.width = width; |
| this.workCanvas.height = height; |
| this.workCtx = this.workCanvas.getContext('2d'); |
|
|
| |
| this.masks = {}; |
| this.activeTool = null; |
| this.activeLabel = null; |
| this.eraserSize = 20; |
| this.fillTolerance = 32; |
|
|
| |
| if (canvas.parentElement) { |
| canvas.parentElement.style.position = 'relative'; |
| canvas.parentElement.insertBefore(this.maskCanvas, canvas.nextSibling); |
| } |
| } |
|
|
| setTool(tool) { |
| this.activeTool = tool; |
| } |
|
|
| setLabel(label, color) { |
| this.activeLabel = label; |
| this.activeColor = color || '#FF0000'; |
| } |
|
|
| setEraserSize(size) { |
| this.eraserSize = size; |
| } |
|
|
| setFillTolerance(tolerance) { |
| this.fillTolerance = tolerance; |
| } |
|
|
| |
| |
| |
| floodFill(x, y) { |
| if (!this.activeLabel) return null; |
|
|
| x = Math.round(x); |
| y = Math.round(y); |
| if (x < 0 || x >= this.width || y < 0 || y >= this.height) return null; |
|
|
| |
| var ctx = this.canvas.getContext('2d'); |
| var imageData = ctx.getImageData(0, 0, this.width, this.height); |
| var pixels = imageData.data; |
|
|
| |
| var idx = (y * this.width + x) * 4; |
| var targetR = pixels[idx]; |
| var targetG = pixels[idx + 1]; |
| var targetB = pixels[idx + 2]; |
|
|
| |
| if (!this.masks[this.activeLabel]) { |
| this.masks[this.activeLabel] = new Uint8Array(this.width * this.height); |
| } |
| var mask = this.masks[this.activeLabel]; |
|
|
| |
| var tolerance = this.fillTolerance; |
| var visited = new Uint8Array(this.width * this.height); |
| var stack = [[x, y]]; |
| var filled = 0; |
|
|
| while (stack.length > 0) { |
| var point = stack.pop(); |
| var px = point[0]; |
| var py = point[1]; |
|
|
| if (px < 0 || px >= this.width || py < 0 || py >= this.height) continue; |
|
|
| var pidx = py * this.width + px; |
| if (visited[pidx]) continue; |
| visited[pidx] = 1; |
|
|
| var ci = pidx * 4; |
| var dr = Math.abs(pixels[ci] - targetR); |
| var dg = Math.abs(pixels[ci + 1] - targetG); |
| var db = Math.abs(pixels[ci + 2] - targetB); |
|
|
| if (dr <= tolerance && dg <= tolerance && db <= tolerance) { |
| mask[pidx] = 1; |
| filled++; |
|
|
| stack.push([px + 1, py]); |
| stack.push([px - 1, py]); |
| stack.push([px, py + 1]); |
| stack.push([px, py - 1]); |
| } |
| } |
|
|
| this.renderMasks(); |
|
|
| return { |
| type: 'mask', |
| label: this.activeLabel, |
| rle: this.encodeRLE(mask, this.width, this.height), |
| pixelsFilled: filled |
| }; |
| } |
|
|
| |
| |
| |
| erase(x, y) { |
| x = Math.round(x); |
| y = Math.round(y); |
| var r = Math.round(this.eraserSize / 2); |
| var changed = false; |
|
|
| |
| for (var label in this.masks) { |
| var mask = this.masks[label]; |
| for (var dy = -r; dy <= r; dy++) { |
| for (var dx = -r; dx <= r; dx++) { |
| if (dx * dx + dy * dy <= r * r) { |
| var px = x + dx; |
| var py = y + dy; |
| if (px >= 0 && px < this.width && py >= 0 && py < this.height) { |
| var idx = py * this.width + px; |
| if (mask[idx]) { |
| mask[idx] = 0; |
| changed = true; |
| } |
| } |
| } |
| } |
| } |
| } |
|
|
| if (changed) { |
| this.renderMasks(); |
| } |
| return changed; |
| } |
|
|
| |
| |
| |
| renderMasks() { |
| this.maskCtx.clearRect(0, 0, this.width, this.height); |
| var imageData = this.maskCtx.createImageData(this.width, this.height); |
| var data = imageData.data; |
|
|
| for (var label in this.masks) { |
| var mask = this.masks[label]; |
| var color = this._parseColor(this._getLabelColor(label)); |
|
|
| for (var i = 0; i < mask.length; i++) { |
| if (mask[i]) { |
| var pi = i * 4; |
| |
| data[pi] = Math.min(255, data[pi] + color.r); |
| data[pi + 1] = Math.min(255, data[pi + 1] + color.g); |
| data[pi + 2] = Math.min(255, data[pi + 2] + color.b); |
| data[pi + 3] = 200; |
| } |
| } |
| } |
|
|
| this.maskCtx.putImageData(imageData, 0, 0); |
| } |
|
|
| |
| |
| |
| encodeRLE(mask, width, height) { |
| var counts = []; |
| var currentVal = 0; |
| var currentCount = 0; |
|
|
| for (var i = 0; i < mask.length; i++) { |
| var val = mask[i] ? 1 : 0; |
| if (val === currentVal) { |
| currentCount++; |
| } else { |
| counts.push(currentCount); |
| currentVal = val; |
| currentCount = 1; |
| } |
| } |
| counts.push(currentCount); |
|
|
| return { counts: counts, size: [height, width] }; |
| } |
|
|
| |
| |
| |
| decodeRLE(rle) { |
| var size = rle.size; |
| var counts = rle.counts; |
| var mask = new Uint8Array(size[0] * size[1]); |
| var pos = 0; |
| var val = 0; |
|
|
| for (var i = 0; i < counts.length; i++) { |
| for (var j = 0; j < counts[i]; j++) { |
| if (pos < mask.length) { |
| mask[pos] = val; |
| pos++; |
| } |
| } |
| val = 1 - val; |
| } |
|
|
| return mask; |
| } |
|
|
| |
| |
| |
| loadMask(label, rle) { |
| this.masks[label] = this.decodeRLE(rle); |
| this.renderMasks(); |
| } |
|
|
| |
| |
| |
| getMaskAnnotations() { |
| var annotations = []; |
| for (var label in this.masks) { |
| var mask = this.masks[label]; |
| |
| var hasPixels = false; |
| for (var i = 0; i < mask.length; i++) { |
| if (mask[i]) { hasPixels = true; break; } |
| } |
| if (hasPixels) { |
| annotations.push({ |
| type: 'mask', |
| label: label, |
| rle: this.encodeRLE(mask, this.width, this.height) |
| }); |
| } |
| } |
| return annotations; |
| } |
|
|
| |
| |
| |
| clearAll() { |
| this.masks = {}; |
| this.maskCtx.clearRect(0, 0, this.width, this.height); |
| } |
|
|
| |
| |
| |
| clearLabel(label) { |
| delete this.masks[label]; |
| this.renderMasks(); |
| } |
|
|
| _getLabelColor(label) { |
| |
| var btn = document.querySelector('.label-btn[data-label="' + label + '"]'); |
| if (btn) return btn.dataset.color || '#FF0000'; |
| return this.activeColor || '#FF0000'; |
| } |
|
|
| _parseColor(hex) { |
| hex = hex.replace('#', ''); |
| if (hex.length === 3) { |
| hex = hex[0] + hex[0] + hex[1] + hex[1] + hex[2] + hex[2]; |
| } |
| return { |
| r: parseInt(hex.substring(0, 2), 16), |
| g: parseInt(hex.substring(2, 4), 16), |
| b: parseInt(hex.substring(4, 6), 16) |
| }; |
| } |
| } |
|
|
| |
| window.SegmentationToolManager = SegmentationToolManager; |
| })(); |
|
|