SAM+SAM2如何落地半自动图像标注:从原理到工程实践
简介面向机器学习与计算机视觉开发者的交互式半自动图像标注工具基于SAM及SAM2模型实现用户提供少量标注即可自动完成相似特征扩展可服务于自动驾驶、医学影像、安全监控等领域的数据预处理。压缩包共499个文件、约143.06MB核心为178个Python脚本涵盖模型调用、交互逻辑与后端服务另含60个GIF演示、22个PNG与11个JPG等图像素材、UI界面及HTML/JS前端文件以及3个ipynb示例笔记和Sphinx生成的文档资源便于源码阅读与二次开发。已有649人学习/下载说明其作为实战项目的参考价值获社区认可。对希望快速搭建标注流程的开发者可从项目结构、前端交互到模型推理完整研读覆盖模型调用、提示点输入与掩码生成等关键环节直接修改扩展形成自有工具降低从零实现SAM/SAM2应用的技术门槛。1. 为什么 SAMSAM2 成了半自动图像标注工具的新底座一批巡检视频放在眼前标注员要逐帧把目标抠出来几千张遥感图需要导出训练掩码手工画到半夜。SAM 与 SAM2 这两代分割模型支撑起来的图像标注工具解决的就是这个矛盾SAM 负责在单帧上吃提示、出掩码SAM2 把单帧能力扩展到视频帧间跟踪两者配合后标注操作从头到尾手工描边变成了点几个点、框一个框、偶尔修一下边缘。说是“自动打标”本质上仍是人做关键决策、模型做重复劳动的半自动流程但这恰恰是当下攒训练数据性价比最高的路子。适合给 YOLO、分割网络或任意像素级任务备数据的团队。2. SAM 与 SAM2 的分割机制差异交互提示、帧间跟踪与模型档位怎么选2.1 SAM 的点/框/掩码提示如何决定分割结果SAM 能成为标注引擎不是因为它的分割精度天下第一而是因为它把“如何让模型听人话”这件事做到了极致。它接受三类 prompt点提示、框提示、掩码提示模型内部避不开的核心链路是图像编码器把整张图算成一份 image embeddingprompt 编码器把当前那次点击或框也编成向量然后 mask decoder 在 embedding 上做交叉注意力一次性给你若干个候选掩码。对标注工具而言关键是 image embedding 只需算一次。第一张图进来时set_image()可能要花几秒之后每点一次提示再预测走的是轻量 decoder几十毫秒内能出结果。这意味着交互式打标可以在画布上做到“点完就有轮廓”尺度完全能接受。用负样本点压制过分割同样是在这个环节完成的一个目标连带着背景被一起割出来时追加一个point_labels0的点击decoder 会把注意力拉回正样本一侧。提示并不要求点的位置精准。只要点在目标内部掩码质量通常不会出现断崖式下跌真正影响边界细粒度的是 prompt 的组合方式单点 正样本适用于中心清晰的目标多点混合正负样本适用于有粘连、需要扣边界的场景框提示单独用精度最高但要求框紧贴目标外接矩形高品质标注工具里最常见的交互就是“先拉一个贴近目标的框模型立刻出 mask”。2.2 SAM2 的视频传播能力与标注工具的选型边界SAM2 不是 SAM 的简单升级它把静态图分割与视频分割统一到同一套权重里。图像场景里它同样接受点/框/掩码提示视频场景里加入了一条记忆路径“当前帧之前的所有 frame 状态”会被编码进一个 memory bank后续帧预测时decoder 不只依赖当前帧的 image embedding还会读取历史记忆做时序关联。这就是它能在视频里逐帧跟住掩码的原因。选择模型档位时要同时看显存和任务性质。处理单帧图像SAM 的 vit_b 档位在 6G 显存上能顺畅交互vit_h 精度更高但显存压力大如果标注对象是视频直接上 SAM2因为让 SAM 逐帧点击失去了半自动的意义。SAM2 也吃显存视频预测时记忆帧会持续累积片源超过一两分钟时建议切段处理。对比维度SAM第一代SAM2图像单帧分割交互式打标主力秒级出掩码同样支持权重选择更灵活视频帧间传播不支持只能逐帧点支持记忆跟踪可首帧标注后续自动传显存开销vit_b 最省6G 可跑视频模式有记忆累积长视频需切段适合场景图像数据集批量打标视频抽帧、连续帧掩码标注、帧间一致性要求高的任务选型边界在“目标被遮挡后重现”这个场景上最明显。SAM2 对短时遮挡、目标再出现的情况能靠记忆找回但如果遮挡持续几十帧、目标完全离开画面后又回来掩码大概率跳到背景或其他物体上。这不是 bug而是模型记忆机制的极限。半自动标注工具必须设计“关键帧纠错”这个动作而不是指望模型从第一帧锁到最后一帧不动。一句话总结选型逻辑图像标注用 SAM 或者 SAM2 的 image 模式都行看出力和显存视频标注必须上 SAM2但要在工具里留人工巡检的口子。模型档位先用 small/base 跑通全流程确认精度不够再上大模型别一上来就把显存打满。3. 本地跑通最小半自动标注流程SAM 环境、交互预测与 SAM2 视频打标3.1 起一个最小可用的 SAM 交互预测脚本先说环境。Python 3.10PyTorch 2.0然后从官方仓库安装 segment-anything把对应的权重文件下载到本地 checkpoints 目录。下面是第一个能跑的交互脚本用 OpenCV 窗口模拟标注器的点击交互左键加前景点右键加背景点每次点击实时出掩码。import numpy as np import cv2 import torch from segment_anything import sam_model_registry, SamPredictor CHECKPOINT checkpoints/sam_vit_b.pth DEVICE cuda if torch.cuda.is_available() else cpu sam sam_model_registry[vit_b](checkpointCHECKPOINT).to(DEVICE) predictor SamPredictor(sam) # 用 BGR 读入后转 RGBSAM 按 RGB 训练顺序错了掩码质量会明显下降 img cv2.imread(imgs/001.jpg) img_rgb cv2.cvtColor(img, cv2.COLOR_BGR2RGB) predictor.set_image(img_rgb) points [] labels [] def run_predict(): if not points: return masks, scores, _ predictor.predict( point_coordsnp.array(points), point_labelsnp.array(labels), multimask_outputTrue, # 返回多个候选取分数最高者 ) best int(np.argmax(scores)) mask (masks[best] 0).astype(np.uint8) * 255 overlay img_rgb.copy() mask_bgr cv2.cvtColor(mask, cv2.COLOR_GRAY2BGR) overlay cv2.addWeighted(img_rgb, 0.6, mask_bgr, 0.4, 0) cv2.imshow(label, cv2.cvtColor(overlay, cv2.COLOR_RGB2BGR)) def on_mouse(event, x, y, flags, param): if event cv2.EVENT_LBUTTONDOWN: points.append([x, y]) labels.append(1) run_predict() elif event cv2.EVENT_RBUTTONDOWN: points.append([x, y]) labels.append(0) # 背景提示用来削掉多余边缘 run_predict() cv2.namedWindow(label) cv2.setMouseCallback(label, on_mouse) print(左键加前景点右键加背景点ESC 退出) while True: cv2.imshow(label, cv2.cvtColor(img_rgb, cv2.COLOR_RGB2BGR)) if cv2.waitKey(1) 0xFF 27: break cv2.destroyAllWindows()multimask_outputTrue是整个脚本里最影响体验的参数。它会让 decoder 给你多个不同细粒度的候选掩码脚本里只取了分数最高的一个实际标注工具里更合理的做法是把候选全部列在画布右侧让标注员手动挑。point_labels必须是 0 或 10 表示该点属于背景1 表示前景。背景点点击位置要尽量靠近目标边缘而非远离目标否则对减少过分割帮助不大。3.2 把 SAM2 加进来视频序列逐帧传播与关键帧修正视频打标的可执行路径是先把视频抽帧成目录然后交给 SAM2 做“首帧标注、逐帧传播”。官方 video predictor 的用法在意料之内地清晰from sam2.build_sam import build_sam2_video_predictor predictor build_sam2_video_predictor( config_filesam2_hiera_base.yaml, ckpt_pathcheckpoints/sam2_hiera_base.pt, devicetorch.device(cuda), ) state predictor.init_state(video_pathframes/) # 第 0 帧添加一个正样本点并给目标一个 obj_id predictor.add_new_points( inference_statestate, frame_idx0, obj_id1, pointsnp.array([[300, 200]]), labelsnp.array([1]), ) # 逐帧传播掩码拿到就存成 PNG for frame_idx, obj_id, mask in predictor.propagate_in_video(state): png (mask 0).astype(np.uint8) * 255 cv2.imwrite(flabels/{frame_idx:06d}.png, png)这段代码跑通只解决了“传播”这一步真正决定打标质量的是修正节奏。propagate_in_video是逐帧自动跟踪的不用每帧人工介入但遮挡、快速运动、同类别目标靠近时掩码可能在某一帧悄悄跳到别的目标上。我一般会把传播结果按每 20 帧抽一帧给标注员校验发现掩码漂移就在当前帧重新加一次提示点覆盖纠正然后继续传播。不要等到整段视频跑完再统一看结果。那样的话第 30 帧就漂了的掩码会一直污染到第 300 帧返工量远大于中途纠错。视频打标工具的核心逻辑不是“自动生成完所有掩码”而是“自动生成 低频率人工巡检”的混合节奏。SAM2 的传播能力把原来逐帧画轮廓的成本压缩到了首帧标注加周期性修正这正是它值得被集成进标注工具的原因。3.3 半自动流程里更省事的做法先用检测框粗筛再让 SAM 精修如果项目本身已有可用的检测模型——比如正在迭代的 YOLO 模型——不要浪费它的粗定位能力。把检测框喂给 SAM 做box提示掩码质量通常比纯点提示更稳定也省去了一次次点目标内部的时间。流程上就是检测模型先批量出一堆框标注员只做两个动作确认框对不对、微调掩码边缘。# 框坐标来自检测模型输出格式 x_min, y_min, x_max, y_max box np.array([120, 80, 360, 420]) masks, scores, _ predictor.predict( point_coordsNone, point_labelsNone, boxbox[None, :], multimask_outputFalse, )box提示要求矩形尽量贴合目标边界。框大了掩码容易带上背景框小了目标边缘被裁掉。检测模型输出的框大多偏松因此在工具里要允许标注员在预测前先调整检测框位置或者预测后追加几个负样本点把多余的边削掉。“检测粗筛 SAM 精修”的顺序能把单张图像的标注时间压到纯手工的五分之一以下。4. 看懂附源码项目里的工程结构核心模块划分与 COCO/YOLO 导出脚本4.1 源码工程按什么维度拆模块标着“基于 SAMSAM2 实现”的标注工具源码工程结构上绕不开四块交互界面、推理引擎、数据读写、格式导出。界面管人怎么点鼠标推理引擎统一封装 SAM 与 SAM2 的预测调用数据读写管图像目录、视频抽帧、掩码的落盘格式导出负责把掩码转成训练要用的 COCO、YOLO 格式。下面是一份常见且合理的目录组织方式auto_label/ ├── app.py # 入口负责启动 GUI 和加载模型 ├── engine/ │ ├── sam_engine.py # SAM/SAM2 统一推理封装 │ ├── prompts.py # 点、框、掩码 prompt 的数据结构 │ └── amg.py # 自动掩码生成与候选后处理 ├── ui/ │ ├── canvas.py # 画布缩放/平移/标注层渲染 │ └── tools.py # 交互工具点选、框选、擦除 ├── io/ │ ├── dataset_reader.py # 读取图像目录/视频抽帧 │ └── export.py # COCO/YOLO/LabelMe 导出 └── configs/ ├── sam_config.yaml # 模型档位、设备、半精度开关 └── label_rules.yaml # 类别映射、默认保存格式这个拆分最关键的一点是把推理封装做成全局单例。模型加载和 image embedding 计算都很重如果每个按钮动作都重新加载一次模型工具会卡到没法用。sam_engine.py里一般会维护一个全局 predictor 实例set_image只在切换图片时调用后续预测全部复用已有 embedding。UI 线程与推理线程分开也是必须的大图上首次编码可能要几秒不拆分线程界面会假死标注员会认为工具崩溃。prompts.py单独成一个模块是因为半自动流程里的 prompt 不只是“一个点”而是画布上累积的一整组操作记录第几次点击、正样本还是负样本、是否附带框。这些记录要支撑撤销、重做和保存不抽象成独立数据结构后面改需求会极其痛苦。amg.py则是给“自动预标注”用的刚打开一张图时工具可以用自动掩码生成铺一批候选标注员只做筛选和确认而不是从零开始点。4.2 掩码导出成 COCO 与 YOLO 格式的转换细节半自动打标的终点是给训练管线喂数据。导出这一步最常出问题因为产品的 GUI 显示分辨率和原图分辨率往往不一致掩码坐标系与导出坐标系没有对齐。先看掩码转多边形的核心函数import cv2 import numpy as np def mask_to_polygon(mask: np.ndarray, epsilon: float 2.0) - list: mask (mask 0).astype(np.uint8) contours, _ cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) polys [] for cnt in contours: if cnt.shape[0] 3: continue cnt cv2.approxPolyDP(cnt, epsilon, True) polys.append(cnt.reshape(-1, 2).tolist()) return polysepsilon是approxPolyDP的逼近精度参数它决定了多边形点数。设成 0 会得到极高的点数导出的 COCO 文件膨胀到几十兆设太大则边界锯齿消失但轮廓失真。我一般从 2.0 起步如果目标边缘复杂就降到 1.0如果导出的 JSON 太大就往上抬到 3.0。mask必须是单通道二值数组并且与原始图像尺寸一致如果画布做过缩放导出前要把多边形坐标除以缩放比例还原到原图坐标系。转 YOLO 分割格式时坐标要归一化到图像宽高输出小数尽量保留六位def to_yolo_seg(poly, img_w, img_h): norm [(x / img_w, y / img_h) for x, y in poly] return .join(f{x:.6f} {y:.6f} for x, y in norm)做目标检测标注时还要用到掩码外接框def mask_to_bbox(mask): ys, xs np.where(mask 0) if len(xs) 0: return None # 空掩码必须跳过 x_min, x_max xs.min(), xs.max() y_min, y_max ys.min(), ys.max() return x_min, y_min, x_max, y_max导出成 YOLO 检测格式时中心点与宽高都要除以图像宽高导出成 COCO 时外接框格式为[x_min, y_min, width, height]且坐标保留浮点。很多标注工具导出的框是像素整数喂给检测模型时边界掉点不严重但放到实例分割模型里掩码边缘的锯齿会直接影响训练效果。另外注意YOLO 分割与 COCO 分割的类别 id 映射不要混用统一从label_rules.yaml读取。5. 半自动标注跑起来的 5 个避坑点显存、漂移、小目标与格式坐标5.1 显存 OOM6G 显卡不要直接上 vit_h现象加载 vit_h 权重首次set_image还没跑完报 CUDA out of memory。原因很直白SAM 的 image encoder 在 ViT-H 规模下需要大量显存自动掩码生成又是多批次并行推理显存会瞬间打满。解决方式是分三步降负载换 vit_b 档位推理时开启半精度把输入图片长边缩到 1024 以内再送进模型。半精度对掩码质量的损失在标注场景下几乎不可感但显存占用直接砍半。# 半精度推理只需要把权重转成 float16 sam sam_model_registry[vit_b](checkpointCHECKPOINT).to(device) sam sam.half()需要注意fp16模式下某些 CPU 设备不支持先把DEVICE固定到 CUDA。另外自动掩码生成的points_per_batch参数默认值在低显存卡上要调小到 64 或者 32否则一次候选生成就把显存打穿。这个参数藏在 SAM 的SamAutomaticMaskGenerator里手动标注模式下不涉及。5.2 SAM2 视频掩码漂移遮挡后跳目标现象视频第 40 帧目标被卡车挡住第 60 帧重新出现后掩码跟在了另一辆颜色相近的车上。原因是 SAM2 的记忆机制在目标消失期间持续累积了错误上下文目标重现时无法正确关联原对象信息。解决方法是把“巡检帧”机制加进流程每传播 20 到 30 帧工具弹出一帧让标注员确认。发现跟丢在该帧重新点击一次目标区域SAM2 会从这个新提示开始重建记忆后续帧继续传播。巡检频率不是越高越好。每 5 帧确认一次会把半自动的效率优势抹掉每 50 帧以上确认一次目标一旦漂移污染范围会很大。一般选择“目标运动速度越快、画面越拥挤巡检越频繁”的动态策略由标注员自行调整。5.3 小目标与粘连边缘单点提示经常割不全现象一小块仪表盘上的字符单点正样本只割出半个字两个紧挨着的零件掩码把两个都包进来了。原因是 SAM 的训练目标偏向完整语义对象小尺寸目标在图像 embedding 中的特征占比较低负样本点才是控制边界的有效手段。对极小的目标不要在原图上硬点先把目标区域裁剪放大 2 到 4 倍在放大图上做分割再把掩码坐标平移回原图坐标系效果会明显改善。粘连问题的处理要改变交互习惯先拉框锁定单个目标再在相邻目标的边界处加负样本点。只加正样本点不断尝试往往徒劳负样本提示才是 SAM 削边的核心手段。5.4 BGR/RGB 顺序混乱导致掩码烂得莫名其妙现象同一张图换了一种读图方式分割结果从完整轮廓变成大面积漏检。原因是很多工具底层用 OpenCV 读图得到的是 BGR 数组直接喂给模型后颜色通道语义翻转模型看到的完全是另一张图。解决方案只有一个统一在数据入口做一次cv2.COLOR_BGR2RGB转换并且只在最后显示时转回 BGR。这个坑隐蔽在于个别图视觉上似乎也能出掩码但边界质量明显下降让人误以为是模型能力问题。批量处理时要对全部图片做同一个预处理管线。5.5 YOLO/COCO 导出坐标越界缩放标记没还原现象导出的 YOLO 标签出现负坐标或者中心点超出 1.0COCO 的 bbox 面积比目标实际大小小得多。原因是 GUI 画布为了显示方便做了缩放但导出时直接把画布上的坐标拿去归一化没有除以缩放比例还原到原始分辨率。解决方法是给坐标转换引入统一入口并在导出后做完整性校验python scripts/validate_labels.py train/labels 640 480校验脚本至少要检查三件事坐标全部落在 [0,1] 区间、bbox 宽高大于 0、每个样本文件存在。空掩码也是一个高频来源目标在画面边缘被裁掉一半时findContours可能拿到退化轮廓应在导出前直接跳过并打印文件名避免生成全零标签干扰训练。6. 把标注效率再抬一档Embedding 缓存与视频自动打标的验证技巧6.1 预计算 image embedding交互延迟从几秒压到 0.1 秒SAM 交互的卡顿瓶颈不在 predict而在set_image。一套成熟的标注工具会把每张图的 image embedding 提前算好并存盘标注员打开图时直接加载首次点击的出掩码延迟从 2 到 4 秒降到 0.1 秒左右。实现思路不复杂批量读图 → 逐张set_image→ 取出predictor.image_embedding保存为 npz → 下次加载时直接predictor.set_image_embedding。np.savez_compressed( cache/001.npz, embeddingpredictor.get_image_embedding().cpu().numpy(), original_sizepredictor.original_size, input_sizepredictor.input_size, )这个缓存在图像数据集上效果显著一顿饭的工夫能预处理万张图换了几张图重新进入标注时画布响应手感完全不一样。视频场景下缓存同样适用SAM2 视频预测器初始化时就会加载全部帧的 embedding因此视频目录不要频繁切换保持一次会话内完成整段标注。6.2 用掩码质量指标代替肉眼巡检验证半自动工具是否真的值得投入固定抽 30 张图做两轮标注一轮纯手工描边一轮用 SAMSAM2 流程打标统计每张图的平均耗时和导出标签的掩码 IoU。SAM 流程耗时低于手工一半且掩码 IoU 稳定在 0.9 附近这个工具就达到了可以投入的门槛。如果 IoU 波动大优先检查预处理、提示策略和坐标还原而不是换更大的模型。我现在的习惯是每次批量导出后挑 20 张原图和对应掩码叠图抽查重点看目标边缘有没有被裁、有没有粘连未修正。人工确认这一步省不了但它能拦住绝大部分训练数据污染问题。希望帮到你。本文还有配套的精品资源点击获取
上一篇/下一篇内容由系统自动关联
返回资讯列表 →