
简介基于SAM与SAM2模型构建的交互式半自动图像标注工具适合计算机视觉开发者、科研人员以及需要高效处理海量图像数据的团队能够以少量标注点自动推断并完成相似特征标注显著降低人工成本。资源包内共499个文件包含178个Python源码、60个GIF操作演示、57个SVG图标、17个JSON配置、14个HTML页面及3个Jupyter笔记本另有CUDA与C源码用于高性能模块整体约143MB目录结构清晰便于查阅。作为优质项目实战案例该项目已在实践中验证其可用性目前已有649人学习下载。用户可直接获取完整核心代码根据业务场景修改扩展或参考GIF演示与笔记快速上手适用于自动驾驶、医学影像、安全监控等领域的图像预处理工作能够显著缩短算法从实验到落地的周期。1. 交互式半自动标注SAM2 让「人机协作打标」第一次真正落地这套基于 SAMSAM2 实现的交互式半自动图像标注工具源码核心玩法很简单标注员用鼠标在目标内部点一下模型立刻吐出一个完整的对象 mask点错了再在背景上点一下mask 自动收缩修正。和传统多边形标注逐顶点描边相比单张图的目标标注耗时能从 30 秒降到 3 秒以内这个差距放大到几千张图就是几个工时的成本差。它适合三类人被像素级标注折磨的数据标注工程师、需要快速给 YOLO 或检测模型攒分割真值的算法工程师以及做遥感影像、医疗切片、工业质检这类专业域数据清洗的从业者。先说一个反直觉的结论SAM2 的价值不止是比 SAM 快而是它的时序记忆机制让视频与序列标注真正可用了这正是这份资源最值得下手的点。2. 先搞懂选型SAM 与 SAM2 的差异决定标注效率的三处设计2.1 从 ViT 到 Hiera两代模型的骨干差异SAMSegment Anything Model用的是 ViT 作为图像编码器思路是把整张图切成 patch 序列过一遍全局自注意力最后输出一个全图 embedding。这个设计精度没问题但代价是计算量很大尤其是 ViT-H 这种大尺寸变体单张 1080p 图在消费级显卡上要等几秒。SAM2 把骨干换成了 Hiera一个带层次化多尺度设计的视觉 Transformer。多尺度特性意味着小目标的细节特征在浅层就被保留下来而不是像 ViT 那样把所有信息压进一个尺度的序列里。对标注工具来说这个差异直接体现在两个地方。第一是速度同尺寸下 SAM2 比 SAM 快一个档次官方口径是快约 6 倍实际交互中体感是从「等两秒」变成「几乎无感」。第二是小目标召回率Hiera 的多尺度特征让细长物体、密集小目标的 mask 边界更稳这在遥感标注和医疗切片场景里特别明显。如果你手里的数据集以 512 以下的裁剪图为主SAM2 的 Hiera-Tiny 甚至能跑到实时这是 SAM 的 ViT-B 给不了的体验。2.2 提示机制为什么「点一下」就能出 mask两代模型共用一个核心交互范式prompt提示驱动分割。prompt 分三类point点、box框、mask已有的掩码。交互式标注里最常用的是 point 加正负样本——正向点告诉模型「这是目标」负向点告诉模型「这不是目标」。模型内部通过 prompt encoder 把稀疏的点坐标和密集的图像 embedding 融合再交给 mask decoder 输出候选 mask。SAM2 的 mask decoder 沿用并改进了这个结构让点提示的响应更积极。这里有个关键设计predict 默认输出三个候选 mask对应「只含目标核心」「目标加部分边缘」「目标加更大上下文」三种粒度。交互工具必须把这三种候选都展示给用户而不是只取分数最高的那个。原因后面避坑章会详细讲——高分 mask 往往是最大的那个而不是最准的那个。理解了这个机制你就知道所谓「半自动标注」本质是「人给提示模型给候选人再纠偏」的闭环。2.3 选型判断什么场景用 SAM什么场景用 SAM2我拆这种标注项目时第一件事不是看代码而是确认它到底该用哪个模型。下面这个表是我自己整理的选择依据可以直接抄。对比维度SAMsegment-anythingSAM2骨干网络ViT-B / ViT-L / ViT-HHiera-Tiny / Base / Large单帧交互速度ViT-B 可用ViT-H 偏慢Tiny/Base 接近实时视频/时序支持不支持逐帧独立原生支持 streaming memory小目标分割依赖 ViT 全局特征容易糊多尺度特征召回更好显存占用ViT-H 约 12GBHiera-Large 约 10GB 左右适用标注场景单张静态图、简单目标视频帧、遥感时序、批量小图决策建议很简单只做单张图像的实例分割标注SAM 已经够用源码更简单要标注视频、序列帧或者像遥感影像这种一张图几百个目标的场景直接上 SAM2。这份资源同时接了两套模型所以两种场景都覆盖了但实际跑批量标注时我一般固定用 SAM2避免在两条代码路径之间来回切。3. 环境搭建与三种提示方式把这个源码包跑起来的完整路径3.1 环境与权重准备拿到源码之后第一步不是急着读代码而是把运行环境复现出来。SAM2 官方仓库给的是 PyTorch 生态Python 版本建议 3.10 或 3.11CUDA 环境取决于你的显卡驱动。创建一个干净的 conda 环境再安装依赖能避免和已有项目打架。conda create -n sam2label python3.10 -y conda activate sam2label # 安装 SAM2 官方库 pip install githttps://github.com/facebookresearch/sam2.git # 安装 SAM 官方库单图场景和自动 mask 生成会用到 pip install githttps://github.com/facebookresearch/segment-anything.git # 图像处理与标注导出依赖 pip install opencv-python pycocotools matplotlib # 如果机器上有 NVIDIA GPU先确认 torch 是 CUDA 版 python -c import torch; print(torch.cuda.is_available())官方库安装需要从 GitHub clone 源码网络不通畅时很容易中断。我一般会先单独 clone 下来再本地 pip install而不是直接走 githttps。权重文件是另一个容易卡住的点SAM2 的 checkpoint 体积不小建议先建一个 weights 目录然后从 SAM2 仓库 README 的 Model Checkpoints 小节找到对应链接手动下载把 .ckpt 或 .pth 文件放进去。这一步没做好后面所有代码都会报找不到文件的错。3.2 单帧交互核心路径set_image 与 predict单帧图像标注是这个工具最基础的调用路径理解这一段就理解了整个交互逻辑。核心是 SAM2ImagePredictor先加载模型权重再对当前图像做一次编码之后每一次点击都复用这次编码的结果不用重新算整张图。import cv2 import numpy as np from sam2.build_sam import build_sam2 from sam2.sam2_image_predictor import SAM2ImagePredictor # 1. 构建模型config 文件和 ckpt 必须配套 sam2 build_sam2( config_fileconfigs/sam2.1/sam2.1_hiera_l.yaml, ckpt_pathweights/sam2.1_hiera_large.ckpt, devicecuda, ) predictor SAM2ImagePredictor(sam2) # 2. 读取图像并交给 predictor 做编码 image cv2.imread(demo.jpg) image cv2.cvtColor(image, cv2.COLOR_BGR2RGB) predictor.set_image(image) # 3. 交互提示正向点(目标内部) 负向点(背景) point_coords np.array([[320, 240], [500, 400]], dtypenp.float32) point_labels np.array([1, 0], dtypenp.int32) # 1目标, 0背景 # 4. 预测 mask masks, scores, _ predictor.predict( point_coordspoint_coords, point_labelspoint_labels, multimask_outputTrue, ) # 5. 展示三个候选让用户选择 best int(np.argmax(scores)) mask masks[best] # shape: [H, W] 的布尔数组这段代码有几个参数必须理解。point_coords 的坐标单位是原始图像像素坐标不要自己去缩放。point_labels 里 1 是正样本、0 是负样本交互工具每新增一个点就往这两个数组里追加一行。multimask_outputTrue 时返回三个 maskFalse 时只返回分数最高的一个——后面我们会专门说为什么不建议关掉这个参数。predictor.set_image 是整条链路里最重的操作它会完整跑一遍图像编码器一个交互工具里这个操作只能出现在「打开新图」的时刻而不是出现在每次点击里。3.3 视频与序列标注init_state 与 add_new_points_or_box视频标注是 SAM2 相对 SAM 的杀手锏。SAM2 内部维护了一个记忆库上一帧的预测结果和图像特征会以状态形式传递给下一帧所以只需要在第一帧点一下目标后续帧会自动传播 mask。修正时再在出问题的关键帧上补点即可。from sam2.sam2_video_predictor import SAM2VideoPredictor video_predictor SAM2VideoPredictor.from_pretrained( configs/sam2.1/sam2.1_hiera_l.yaml, weights/sam2.1_hiera_large.ckpt, ) # 初始化视频状态传入视频帧目录内部会按帧序读取 state video_predictor.init_state(video_pathframes/) # 在第一帧添加目标提示obj_id 是目标编号支持一帧多目标 frame_idx, obj_id, mask video_predictor.add_new_points_or_box( inference_statestate, frame_idx0, obj_id1, pointsnp.array([[320, 240]], dtypenp.float32), labelsnp.array([1], dtypenp.int32), ) # 向后续帧传播返回每一帧的 mask 更新 for frame_idx, obj_ids, masks in video_predictor.propagate_in_video(state): save_mask(frame_idx, obj_ids, masks)这里要留意 propagate_in_video 是生成器逐帧产出一组 mask视频长的话需要边传播边落盘不能攒在内存里。obj_id 是目标身份标识同一段视频里追踪多个目标时每个目标要有独立 id新增目标时再调一次 add_new_points_or_box 并给一个新 id。我通常在每个目标第一次出现的帧上做提示后续帧完全交给传播平均一个目标只需两三次交互。3.4 从 mask 到标注格式导出 COCO polygon交互标注的终点是拿到训练可用的数据格式。COCO 是实例分割最通用的格式核心是把 mask 转成多边形坐标。这里有三个常见做法转 polygon、转 RLE、直接存 png 单通道标签图。COCO 官方推荐 polygon但 polygon 转回来做训练时会有锯齿问题所以导出前要做轮廓简化。import cv2 import numpy as np def mask_to_polygon(mask: np.ndarray): 把布尔 mask 转成 COCO 多边形坐标列表 mask_u8 mask.astype(np.uint8) contours, _ cv2.findContours( mask_u8, cv2.RETR_EXTERNAL, # 只取外轮廓忽略内部空洞 cv2.CHAIN_APPROX_SIMPLE, ) polygons [] for c in contours: if len(c) 3: continue # epsilon 控制简化力度值越大顶点越少0.005 是经验值 epsilon 0.005 * cv2.arcLength(c, True) approx cv2.approxPolyDP(c, epsilon, True) polygons.append(approx.reshape(-1).tolist()) return polygons # 使用示例 polygon mask_to_polygon(mask) annotation { segmentation: [polygon], bbox: [int(x) for x in cv2.boundingRect(mask.astype(np.uint8))], area: int(mask.sum()), category_id: 1, iscrowd: 0, }RETR_EXTERNAL 只取外轮廓这一步能过滤掉 mask 内部的小孔洞避免 polygon 出现大量内环。approxPolyDP 的 epsilon 参数决定了轮廓被压成多少顶点0.005 倍周长是我试下来对大多数目标都比较稳的起点如果训练时发现 mask 边缘和真值对不齐把系数降到 0.002顶点更密但更贴合。4. 踩坑记录权重路径、显存暴涨与边界框失真的五个典型问题4.1 每点一次就卡顿甚至闪退set_image 被反复调用现象交互工具里点一下目标程序要卡两三秒才出 mask点十几次之后显存直接 OOM进程被杀。原因把 set_image 写进了点击事件的回调里。每次点击都重新跑一次图像编码器这完全不必要。图像编码器是全模型最重的部分一次 set_image 的开销抵得上几十次 predict。解决把 set_image 放在图像打开事件里只执行一次点击回调里只调用 predict。如果嫌每次点击都传全图坐标麻烦可以在打开图像时把原始坐标和缩放后的坐标换算关系缓存下来后面直接复用。4.2 小目标点哪都不准multimask_output 被关掉了现象在遥感影像里点一辆车出来的 mask 覆盖了一大片道路和建筑怎么点负样本都收不回来。换成默认参数之后突然变准了。原因multimask_outputFalse 时predict 只返回分数最高的一个 mask。分数高不等于边界准——模型倾向于给「大而平滑」的预测打高分小目标在这种模式下很容易被忽略。解决永远保持 multimask_outputTrue然后把三个候选 mask 同时展示给用户用数字键 1/2/3 切换。交互工具的界面设计也要跟着改不能只画一个 mask而是画三个半透明叠加层让用户挑最像的那个而不是替用户做决定。4.3 点坐标对不上提示点和 mask 位置错位现象在图像中心点了一下生成的 mask 却在左上角或者覆盖了完全不相关的区域。原因坐标单位没有统一。SAM2 的 predict 接口内部会处理图像缩放接收的是原始图像坐标但如果你在预处理里手动 resize 了图像再拿 resize 后的坐标去提示位置就全乱了。解决不要在交给 SAM2 之前手动改图。如果因为内存原因必须缩放比如 4K 遥感图就在 set_image 之前统一缩放一次并把缩放系数存下来所有交互坐标都乘这个系数还原到原始分辨率导出标注时再乘一次系数映射回去。我一般直接在 set_image 前后各放一个断点检查坐标范围能省掉一整类玄学 bug。4.4 视频标注第二帧起目标就丢了用了图像接口逐帧预测现象用 SAM2ImagePredictor 对视频的每一帧单独预测第一帧效果很好第二帧开始 mask 漂移目标出了画面后再回来就彻底丢了。原因图像接口没有记忆机制每一帧都被当成独立图像处理。SAM2 的视频能力不在 ImagePredictor 里而在 SAM2VideoPredictor 的 inference state 里——那个记忆库才是它比 SAM 强的地方不用就是白瞎了这套模型。解决视频标注统一走 init_state 加 add_new_points_or_box 加 propagate_in_video 的路线。目标丢失时不要重新初始化整个 state在丢目标的那一帧调用 add_new_points_or_box 补充提示点模型会从这一帧继续传播前面帧的历史结果不会丢失。4.5 导出的 polygon 全是锯齿和空洞没有做轮廓简化现象COCO 格式标注在模型训练时 loss 正常但 mask 预测边界特别糙可视化一看polygon 顶点几百上千个还带一堆内环空洞。原因findContours 用了 RETR_TREE 把所有层级的轮廓全捞出来了包括内部孔洞而且没有做 approxPolyDP 简化mask 的锯齿边缘被完整保留polygon 数据量大且难看。解决改用 RETR_EXTERNAL 只取外轮廓按 0.003 到 0.008 倍周长做轮廓近似。如果目标形状特别复杂、简化后变形严重干脆改用 RLE 格式存储 mask——pycocotools 支持 RLE 压缩字符串不会丢细节训练时反解出来的 mask 也更平滑。5. 工程化提速从 embedding 缓存到「检测器 SAM2」组合流水线5.1 复用 embedding把交互从两秒压到两百毫秒交互式标注的帧率取决于 predict 的速度而 predict 的速度取决于模型对当前提示的响应时间。真正拖慢速度的是 set_image 那个重计算所以工程化第一件事就是把「打开图」和「点击」两步彻底解耦同一张图上的所有交互共享一次图像编码结果。class InteractiveLabeler: 把 set_image 和 predict 的调用时机锁死禁止重复编码 def __init__(self, predictor): self.predictor predictor self._image_ready False def open_image(self, image): self.predictor.set_image(image) self._image_ready True def click(self, points, labels): if not self._image_ready: raise RuntimeError(必须先 open_image 再点击) masks, scores, _ self.predictor.predict( point_coordspoints, point_labelslabels, multimask_outputTrue, ) return masks, scores这个类的关键限制是 click 方法只接受坐标和标签不做任何图像重编码。如果交互工具前端把 set_image 和点击绑在一起就失去了缓存的意义。另外SAM 的 SamPredictor 和 SAM2 的 SAM2ImagePredictor 都支持传入低分辨率特征如果你对每张图都保存一份图像 embedding 到磁盘批量标注时可以直接加载 embedding 跳过编码阶段内存换时间值。5.2 多进程并行按图分片别按点分片半自动标注不是只有人在点批量预标注同样吃计算资源。多进程的思路是在多个 GPU 或一张卡上并行处理多张图进程之间互不共享模型每个 worker 独立加载一次权重。from concurrent.futures import ProcessPoolExecutor def prelabel_one(args): image_path, ckpt_path, config_path args # 每个 worker 独立构建模型不能跨进程共享 sam2 build_sam2(config_fileconfig_path, ckpt_pathckpt_path, devicecuda) predictor SAM2ImagePredictor(sam2) image load_image(image_path) predictor.set_image(image) # 用自动 mask 生成做预标注 from sam2.automatic_mask_generator import SAM2AutomaticMaskGenerator gen SAM2AutomaticMaskGenerator(predictor) masks gen.generate(image) return image_path, masks image_paths [img_001.jpg, img_002.jpg, img_003.jpg] with ProcessPoolExecutor(max_workers2) as pool: for path, masks in pool.map(prelabel_one, image_paths): save_masks(path, masks)注意这里有个坑进程数不是越多越好。每个进程都会完整加载模型权重显存按 worker 数量线性增长一张 12GB 的卡跑 Hiera-Large 最多并两个进程。卡多的话优先按卡分片每张卡一个进程指定不同的 CUDA_VISIBLE_DEVICES而不是一张卡上开八个 worker。5.3 检测器 分割器YOLO 给框SAM2 给 mask做实例分割标注时最省力的组合不是纯靠人点而是先用目标检测器把目标框出来再把框或中心点作为提示喂给 SAM2。这个思路在工程里非常成熟检测器擅长找目标位置SAM2 擅长精确抠边界。def detect_to_prompt(boxes: np.ndarray): 把检测框转成 SAM2 的点提示用框中心作为正向点 centers boxes[:, :2] boxes[:, 2:] / 2.0 point_coords centers.astype(np.float32) point_labels np.ones(len(boxes), dtypenp.int32) return point_coords, point_labels # 假设 yolo 是训练好的检测模型 boxes yolo.predict(image) # shape: [N, 4]x1,y1,x2,y2 point_coords, point_labels detect_to_prompt(boxes) masks, scores, _ predictor.predict( point_coordspoint_coords, point_labelspoint_labels, multimask_outputFalse, # 检测框已限定区域单 mask 足够 )这里用 multimask_outputFalse 是合理的因为检测框已经约定了目标范围不需要三个候选来回切。注意 detect_to_prompt 里的坐标换算如果检测器的输入尺寸和原始图不一致要先把 box 还原到原始坐标再过提示。5.4 自动 mask 生成让模型先干一遍粗活除了点提示SAM 系列还提供了全图自动分割能力也就是不给任何提示直接生成整张图的所有候选 mask。这个能力做预标注特别合适——先让模型把图像里所有可能的目标都切出来人工只需要删掉误检的、补上漏检的比从零逐一点击快得多。我跑批量预标注的经验是自动生成的质量对小目标密集场景一般曲线要等人工修正后才可信但对大目标、背景干净的场景自动 mask 直接可用的比例超过七成。工程实现上先用 AutomaticMaskGenerator 生成候选 mask再按面积阈值过滤太小和太大的剩下的转成 polygon 存进标注文件人工审核阶段只做删除和补标。6. 交付前最后一公里可视化抽检与 IoU 一致性验证标注工具做得再顺交付前也得有质检环节否则脏数据进了训练集模型上线后翻车的是你。我的抽检流程分三步每一步都不复杂但能拦住绝大多数问题。第一步是可视化叠加把 mask 半透明地画在原图上用颜色区分不同实例逐张扫一遍。这一步主要看边界贴合度重点看细长物体、遮挡边缘和阴影区域。第二步是面积异常检测统计每个实例的面积分布面积小于整张图千分之一的实例单独列出来复查。第三步是 IoU 一致性抽查如果在同一批数据里有两份独立标注比如不同标注员各标了一部分用 IoU 衡量两份标注的重合度低于阈值就是边界不稳的信号。import numpy as np def mask_iou(mask_a: np.ndarray, mask_b: np.ndarray) - float: 计算两个布尔 mask 的 IoU用于抽查标注一致性 inter np.logical_and(mask_a, mask_b).sum() union np.logical_or(mask_a, mask_b).sum() if union 0: return 1.0 return inter / union # 抽检规则每类随机抽 10% 的标注和基准标注做比对 for category in categories: samples random.sample(category_anns[category], max(1, len(category_anns[category]) // 10)) for ann in samples: iou mask_iou(load_mask(ann[image_id]), load_gt_mask(ann[image_id])) if iou 0.8: print(f低置信标注: {ann[image_id]}, IoU{iou:.3f})除了 IoU还要检查 polygon 本身是否合法轮廓点数少于 3 的、面积为零的、顶点坐标超出图像边界的都要直接过滤掉。这个检查放在导出脚本里每生成一条标注就校验一次。这套源码我从头到尾完整跑过一遍最贵的教训就是第四章那几条尤其是 set_image 复用和 multimask_output 的取舍。从那以后我每次交付标注数据都强制走一遍「可视化扫检 IoU 抽查 polygon 合法性检查」这个三重流程再急的项目也不敢跳过第二步。希望帮到你也祝你跑通之后少踩几个我踩过的坑。本文还有配套的精品资源点击获取