如何利用Segment Anything生成的mask数据在Canvas中绘制掩码图像
实现Segment Anything掩码的Web端Canvas绘制
一、Masks数据核心可用字段说明
通过SamAutomaticMaskGenerator.generate()生成的masks是字典列表,每个字典中和绘制相关的核心字段:
segmentation: 二维布尔数组(np.ndarray类型),对应原图每个像素是否属于当前掩码区域(True代表在掩码范围内)bbox: 掩码的边界框,格式为[x, y, width, height],可用于快速定位掩码在原图中的位置area: 掩码覆盖的像素面积,可用来过滤过小或过大的无效掩码
二、后端处理:将Masks转换为前端可接收的格式
直接传递numpy数组给前端不可行,这里提供两种实用转换方案:
方案1:转换为Base64图片(快速落地)
把每个掩码转成带透明通道的PNG,再编码为Base64,方便前端直接加载:
from segment_anything import SamAutomaticMaskGenerator, sam_model_registry import numpy as np from PIL import Image import base64 from io import BytesIO sam = sam_model_registry["vit_b"](checkpoint="/Users/jiangxingshang/Downloads/sam_vit_b_01ec64.pth") sam.to(device='cuda') mask_generator = SamAutomaticMaskGenerator(sam) img = Image.open("/Users/jiangxingshang/Pictures/8435e5dde71190ef699eced5cc1b9d16fdfa6056.jpg") imgnp = np.array(img) masks = mask_generator.generate(imgnp) # 处理每个掩码为Base64格式 processed_masks = [] for mask in masks: # 布尔数组转8位灰度图(255为掩码区域,0为背景) mask_img = (mask["segmentation"] * 255).astype(np.uint8) # 转为带透明通道的PIL图片 pil_mask = Image.fromarray(mask_img).convert("L") # 编码为Base64 buffer = BytesIO() pil_mask.save(buffer, format="PNG") base64_str = base64.b64encode(buffer.getvalue()).decode("utf-8") # 打包掩码数据 processed_masks.append({ "base64": f"data:image/png;base64,{base64_str}", "bbox": mask["bbox"] }) # 后续可通过FastAPI/Flask等框架将processed_masks返回给前端
方案2:使用RLE编码(高效传输)
SAM生成的掩码默认包含RLE编码(字典格式,含counts和size字段),直接提取传输能大幅减小数据体积:
# 提取RLE及辅助字段 processed_masks = [] for mask in masks: processed_masks.append({ "rle": mask["rle"], "bbox": mask["bbox"], "color": [np.random.randint(0,255), np.random.randint(0,255), np.random.randint(0,255)] # 提前生成随机颜色,前端直接复用 }) # 通过接口返回给前端即可
三、前端Canvas绘制实现
假设前端已获取后端返回的processed_masks和原图尺寸,以下是两种方案的绘制代码:
方案1:基于Base64图片的绘制
<canvas id="maskCanvas"></canvas> <script> const canvas = document.getElementById('maskCanvas'); const ctx = canvas.getContext('2d'); // 假设已通过接口获取:masksList(processed_masks)、imageWidth、imageHeight async function drawMasks() { // 匹配Canvas尺寸与原图 canvas.width = imageWidth; canvas.height = imageHeight; // 先绘制原图(可选) const img = new Image(); img.src = "原图的Base64或URL"; await img.decode(); ctx.drawImage(img, 0, 0); // 逐个绘制掩码,添加半透明效果 for (const mask of masksList) { const maskImg = new Image(); maskImg.src = mask.base64; await maskImg.decode(); ctx.globalAlpha = 0.5; // 随机给掩码添加色调区分 ctx.filter = `hue-rotate(${Math.random()*360}deg)`; // 绘制到对应位置 ctx.drawImage(maskImg, mask.bbox[0], mask.bbox[1], mask.bbox[2], mask.bbox[3]); ctx.globalAlpha = 1; ctx.filter = 'none'; } } drawMasks(); </script>
方案2:基于RLE编码的绘制(无图片传输,更高效)
<canvas id="maskCanvas"></canvas> <script> const canvas = document.getElementById('maskCanvas'); const ctx = canvas.getContext('2d'); // RLE解码为ImageData的工具函数 function rleToImageData(rle, width, height) { const imageData = ctx.createImageData(width, height); const data = imageData.data; let idx = 0; let currentPixel = 0; const counts = rle.counts; for (let i = 0; i < counts.length; i++) { const count = counts[i]; // 偶数索引为背景(透明),奇数为掩码区域(半透明) const isMask = i % 2 === 1; for (let j = 0; j < count; j++) { if (currentPixel >= width * height) break; data[idx] = isMask ? 255 : 0; data[idx+1] = isMask ? 255 : 0; data[idx+2] = isMask ? 255 : 0; data[idx+3] = isMask ? 127 : 0; // 半透明 idx += 4; currentPixel++; } } return imageData; } async function drawMasks() { canvas.width = imageWidth; canvas.height = imageHeight; // 绘制原图(可选) const img = new Image(); img.src = "原图的Base64或URL"; await img.decode(); ctx.drawImage(img, 0, 0); // 逐个解码RLE并绘制掩码 for (const mask of masksList) { const maskHeight = mask.rle.size[0]; const maskWidth = mask.rle.size[1]; const imageData = rleToImageData(mask.rle, maskWidth, maskHeight); // 创建临时Canvas绘制掩码 const tempCanvas = document.createElement('canvas'); tempCanvas.width = maskWidth; tempCanvas.height = maskHeight; const tempCtx = tempCanvas.getContext('2d'); tempCtx.putImageData(imageData, 0, 0); // 叠加带颜色的掩码到主画布 ctx.globalAlpha = 0.5; ctx.fillStyle = `rgb(${mask.color[0]}, ${mask.color[1]}, ${mask.color[2]})`; ctx.drawImage(tempCanvas, mask.bbox[0], mask.bbox[1]); ctx.globalCompositeOperation = 'source-atop'; ctx.fillRect(mask.bbox[0], mask.bbox[1], mask.bbox[2], mask.bbox[3]); ctx.globalCompositeOperation = 'source-over'; ctx.globalAlpha = 1; } } drawMasks(); </script>
内容的提问来源于stack exchange,提问作者user1434702
相关产品推荐
相关产品推荐

