简介:这是一套面向计算机视觉开发者与数据标注人员的半自动图像标注工具源码,基于 Segment Anything Model 实现,只需鼠标左键点击一次即可完成目标分割与标注,并支持多目标、多类别批量处理及 YOLO 数据格式转换,适合需要快速构建检测数据集的中高级用户。压缩包共 31 个文件,约 44KB,以 22 个 Python 脚本为核心,涵盖 SAM 模型调用、掩码转 YOLO、VOC 与 YOLO 格式互转、图像形态学后处理等模块,另含 5 个 XML 配置、说明文档与依赖清单,结构紧凑便于二次开发。目前已有 493 人学习下载。资源提供完整使用教程,读者可掌握点位选取、右键撤回、按 S 保存等交互流程,理解按类别逐轮标注直至完成的策略,并能通过调整形态学操作的 kernel_size 与 iterations 去除误分割噪点,快速搭建属于自己的半自动标注流水线。
1. 半自动标注到底省了哪段力气:从一张图到一批掩码的真实链路
做过检测和分割项目的人都清楚,模型精度卡住的时候,八成不是网络结构的问题,而是标注数据不够、不细、不一致。传统多边形标注一张图少则几十秒,多则几分钟,遇到密集遮挡场景直接劝退。Segment Anything Model(SAM)出来之后,很多人第一反应是「这玩意儿能不能替我把标注干了」。答案是:能替你把最耗时的「勾轮廓」这一步干掉,但「告诉它勾哪个、勾成什么类别」还得人来点一下。这就是半自动数据标注工具的核心逻辑——人负责语义决策,SAM 负责像素级边界。
这个方案适合谁?如果你手头有几百到几千张图需要做分割标注,预算请不起标注团队,又不想纯手工点像素,那这套基于 SAM 的半自动标注工具就是为你准备的。它不要求你训练模型,不要求你写推理服务,只需要你会装 Python 环境、能跑命令行、知道怎么把点或者框喂给 SAM。源码加使用教程的组合,本质上是把「SAM 推理 + 交互式标注界面 + 结果导出」这条链路打包好,让你改改配置就能用。接下来我会把这条链路拆开,从环境搭建到批量导出,再到踩过的坑,一步步讲清楚。
2. 把 SAM 跑起来之前:环境、权重与推理后端怎么选
2.1 为什么不是所有场景都无脑上 SAM
SAM 有三个版本:ViT-B、ViT-L、ViT-H。参数量分别是 91M、308M、636M。很多人一上来就下最大的 ViT-H,觉得效果一定最好。实际用下来,ViT-H 在边缘细节上确实更稳,但推理速度在单张 1024×1024 图像上,CPU 要跑到十几秒,GPU(比如 RTX 3060)也要 1 到 2 秒。如果你要标几千张图,这个延迟累积起来非常可观。ViT-B 在 GPU 上能压到 0.1 秒以内,边缘稍微毛糙一点,但配合人工微调完全够用。
我的建议是:先拿 ViT-B 跑通全流程,确认标注界面和导出格式没问题,再根据实际边缘质量决定要不要换大模型。另外,SAM 的推理后端有两种常见选择:一种是官方segment-anything库直接加载.pth权重,另一种是 ONNX Runtime 或者 TensorRT 加速。官方库最省事,兼容性好;ONNX 适合部署到没有 PyTorch 的环境,但导出和量化会引入精度损失。半自动标注工具源码里通常默认走官方库,因为标注阶段对延迟不敏感,稳定优先。
2.2 从零搭一个能跑 SAM 的 Python 环境
下面这套步骤我在 Ubuntu 22.04 和 Windows 11 上都验证过,Python 版本锁定在 3.10,因为 3.11 之后有些依赖轮子还没跟上。
# 创建独立环境,避免和系统里的 torch 冲突 conda create -n sam-label python=3.10 -y conda activate sam-label # 安装 PyTorch,根据你的 CUDA 版本选对应命令 # CUDA 11.8 的情况 pip install torch==2.1.0 torchvision==0.16.0 --index-url https://download.pytorch.org/whl/cu118 # 安装 SAM 官方库和标注工具常用依赖 pip install segment-anything opencv-python pillow numpy matplotlib pip install gradio # 如果工具带 Web 界面这段命令的关键点有三个:第一,conda create指定 Python 3.10,避免版本漂移;第二,PyTorch 的 index-url 必须和你的 CUDA 驱动匹配,装错了会回退到 CPU 版本,推理慢十倍;第三,segment-anything只提供模型定义和推理接口,不包含权重文件,权重需要单独下载。
权重文件一般放在项目根目录的weights/文件夹下,命名通常是sam_vit_b_01ec64.pth、sam_vit_l_0b3195.pth、sam_vit_h_4b8939.pth。文件大小分别是 375MB、1.25GB、2.56GB。下载完之后用md5sum校验一下,避免传输损坏导致加载时报unexpected EOF。
2.3 加载模型与单张图推理的最小代码
环境好了之后,先用一段最小代码验证 SAM 能不能正常出掩码。这段代码不涉及标注界面,只做「给一个点,返回一个掩码」。
import cv2 import numpy as np import torch from segment_anything import sam_model_registry, SamPredictor # 选择模型类型,b/l/h 对应 ViT-B/L/H sam_checkpoint = "weights/sam_vit_b_01ec64.pth" model_type = "vit_b" device = "cuda" if torch.cuda.is_available() else "cpu" # 加载模型并推到设备 sam = sam_model_registry[model_type](checkpoint=sam_checkpoint) sam.to(device=device) # 创建预测器,内部会做图像编码 predictor = SamPredictor(sam) # 读取图像并转 RGB image = cv2.imread("test.jpg") image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB) # 设置图像,这一步会计算 image embedding,比较耗时 predictor.set_image(image) # 给一个前景点,坐标格式是 (x, y) input_point = np.array([[500, 375]]) input_label = np.array([1]) # 1 表示前景,0 表示背景 # 预测掩码,multimask_output=True 会返回三个候选 masks, scores, logits = predictor.predict( point_coords=input_point, point_labels=input_label, multimask_output=True, ) # 取分数最高的掩码保存 best_idx = np.argmax(scores) best_mask = masks[best_idx].astype(np.uint8) * 255 cv2.imwrite("mask_result.png", best_mask)逻辑说明:set_image是整条链路里最重的一步,它把图像编码成 embedding,后续所有点、框、掩码提示都复用这个 embedding。所以交互式标注工具会把set_image放在用户打开一张图的时候执行一次,之后每次点击都只跑轻量的掩码解码器。multimask_output=True返回三个候选掩码,分别对应不同粒度,标注工具通常会让用户在这三个里选一个,或者用分数自动选。
参数说明:input_point的坐标是原图像素坐标,不是归一化坐标。input_label里 1 代表前景点,0 代表背景点。如果你给多个点,前景和背景可以混着给,SAM 会根据这些提示分割出目标。predict返回的scores是 IoU 预测分数,不是概率,但可以用来排序。
3. 半自动标注工具源码拆解:交互、缓存与导出三块怎么改
3.1 交互层:点、框、掩码三种提示怎么组合
SAM 支持三种提示:点、框、掩码。点提示最直观,适合孤立目标;框提示适合目标边界比较规整的场景,比如车辆、屏幕;掩码提示适合迭代修正,比如第一次分割多了,把多余区域涂掉再喂回去。半自动标注工具源码里,交互层通常用 OpenCV 的setMouseCallback或者 Gradio 的Image组件来捕获用户点击。
一个常见的翻车点是:用户点了一个点,SAM 返回的掩码把整个背景都包进去了。原因通常是这个点落在了低对比度区域,或者图像本身纹理太复杂。解决办法是让用户补一个背景点,或者切换到框提示。源码里如果只支持单点,建议自己加一个「添加背景点」的按钮,把input_label里对应的值改成 0。
3.2 缓存层:embedding 复用与显存管理
set_image算一次 embedding,ViT-B 在 GPU 上大约占 1GB 显存,ViT-H 要 3GB 以上。如果标注工具同时打开多张图,或者用户频繁切换图片,显存很容易爆。源码里一般会做一个 LRU 缓存,只保留最近 N 张图的 embedding。N 取 1 到 3 比较合理,再多收益不大。
另一个细节是:SamPredictor对象本身是有状态的,set_image会覆盖上一次的 embedding。如果你在多线程环境里用同一个 predictor,必须加锁,否则会出现「A 图的点打到 B 图的 embedding 上」这种玄学 bug。我一般会在源码里把 predictor 封装成一个类,每次set_image前检查当前图像 ID,不同 ID 才重新计算。
3.3 导出层:从掩码到 COCO 格式的转换脚本
标注完了要导出成训练框架能吃的格式。分割任务最通用的是 COCO 格式,每个目标一个segmentation多边形和bbox。SAM 返回的是二值掩码,需要转成多边形。下面这个脚本把掩码转成 COCO 的 annotation 列表。
import cv2 import numpy as np from pycocotools import mask as mask_utils def mask_to_coco_annotation(mask, image_id, category_id, ann_id): """ mask: 二值掩码,H x W,值为 0 或 1 返回 COCO 格式的 annotation 字典 """ # 确保掩码是 uint8 且 Fortran 顺序,pycocotools 要求 mask = np.asfortranarray(mask.astype(np.uint8)) # 编码成 RLE rle = mask_utils.encode(mask) rle["counts"] = rle["counts"].decode("utf-8") # 计算面积和 bbox area = float(mask_utils.area(rle)) bbox = mask_utils.toBbox(rle).tolist() return { "id": ann_id, "image_id": image_id, "category_id": category_id, "segmentation": rle, "area": area, "bbox": bbox, "iscrowd": 0, }逻辑说明:pycocotools的encode要求掩码是 Fortran 顺序(列优先),直接传 C 顺序的数组会报ValueError。counts字段在 Python 3 里是 bytes,存 JSON 前要 decode 成 str。area和bbox都可以从 RLE 直接算,不用再遍历像素。
参数说明:image_id和category_id要和你的 COCO 数据集 JSON 里的images和categories对应。ann_id全局唯一,建议用递增整数。如果你的标注工具支持多个类别,每个类别一个category_id,导出时按类别分组。
提示:如果后续要转成 YOLO 格式,多边形点可以直接从 RLE 解码后取轮廓,但要注意 YOLO 的坐标是归一化的,且多边形点顺序要一致。
4. 避坑与排查:标注到一半崩了、掩码偏移、类别错乱怎么救
4.1 掩码整体偏移几十个像素
现象:点选目标后,返回的掩码位置和实际目标差了一截,像是被平移过。原因通常是图像在预处理时被 resize 了,但点坐标没有同步缩放。SAM 官方实现里,set_image内部会把图像长边缩到 1024,短边按比例缩放。如果你传给predict的点还是原图坐标,就会偏移。
解决:在set_image之前记录缩放比例,把用户点击的坐标乘以比例再传给predict。或者直接用SamPredictor的transform.apply_coords方法,它内部会处理坐标变换。源码里如果没做这一步,自己补上。
4.2 显存不足导致进程被 kill
现象:标注了十几张图之后,程序突然退出,终端显示Killed或CUDA out of memory。原因是 embedding 缓存没有释放,或者SamPredictor对象一直持有旧图的 tensor。
解决:每次set_image前调用torch.cuda.empty_cache(),并把 predictor 的features、original_size等属性显式置空。如果用的是 ViT-H,把缓存数量降到 1。另外,标注工具如果开了多个进程,每个进程都会加载一份模型,显存占用翻倍,建议用单进程加队列。
4.3 同一张图多次标注结果不一致
现象:同一张图,同样的点,两次运行返回的掩码不一样。原因通常是multimask_output为 True 时,三个候选掩码的排序不稳定,或者模型没有设成 eval 模式。
解决:在加载模型后调用sam.eval(),并设置torch.no_grad()。如果还是不稳定,把multimask_output设为 False,只返回一个掩码。另外,随机种子也会影响某些后处理,固定torch.manual_seed(42)能减少玄学。
4.4 导出的 COCO JSON 在训练时读不出来
现象:用pycocotools加载导出的 JSON 时报KeyError: 'segmentation'或者TypeError: Object of type bytes is not JSON serializable。原因是 RLE 的counts没 decode,或者segmentation字段写成了多边形列表但格式不对。
解决:导出前统一走一遍mask_utils.encode,确保counts是 str。如果要用多边形格式,用cv2.findContours提取轮廓后转成[[x1, y1, x2, y2, ...]]的列表,注意点数不能少于 3 个,且要按顺时针或逆时针排列。
4.5 类别 ID 和图像 ID 冲突
现象:标注工具里给目标选了类别,导出后训练时所有目标都变成同一个类。原因是category_id在导出时被硬编码成 1,或者多个类别的 ID 重复。
解决:在标注工具的配置里维护一个category_map,比如{"person": 1, "car": 2},导出时从 map 里取。图像 ID 同理,用文件名哈希或者数据库自增 ID,不要用列表索引,否则增删图片后会错位。
5. 把半自动标注接进真实流水线:批量预标注与人工复核的节奏控制
单张交互标注跑通之后,真正省时间的是批量预标注。思路是:先用一个粗糙的检测模型或者简单的网格点,给每张图生成一批候选掩码,然后人工只做「保留、删除、改类别」三个动作。SAM 的SamAutomaticMaskGenerator就是干这个的,它会在全图撒点,生成大量掩码,再用 NMS 去重。
from segment_anything import SamAutomaticMaskGenerator # 配置自动掩码生成器 mask_generator = SamAutomaticMaskGenerator( model=sam, points_per_side=32, # 每边撒 32 个点,总共 1024 个提示 pred_iou_thresh=0.88, # IoU 预测阈值,低于这个丢弃 stability_score_thresh=0.92, # 稳定性分数阈值 crop_n_layers=1, # 多尺度裁剪层数 min_mask_region_area=100, # 小于这个面积的掩码丢弃 ) # 对一张图生成所有掩码 masks = mask_generator.generate(image) # masks 是一个列表,每个元素包含 segmentation、bbox、area、predicted_iou 等参数说明:points_per_side越大,掩码越细,但耗时线性增长。32 在 ViT-B 上单张图大约 10 到 20 秒,64 要一分钟以上。pred_iou_thresh和stability_score_thresh是过滤低质量掩码的关键,调高会减少误检但可能漏掉小目标。crop_n_layers设为 1 会做一次 2×2 裁剪,对小目标更友好,但耗时翻倍。
批量预标注的节奏控制:我一般会把points_per_side设为 16 先跑一遍,生成粗掩码,人工快速过一遍,把明显不对的删掉。然后对保留的区域,再用交互式单点精修。这样比一上来就 64 个点快得多,因为人工复核的时间远大于推理时间。
验证方法:拿 50 张图做人工全标注,再用半自动流程跑一遍,计算两者的 IoU。如果平均 IoU 低于 0.85,说明预标注质量不够,需要调pred_iou_thresh或者换 ViT-L。如果高于 0.95,说明人工复核可以只做抽查。
最后说一个我自己的习惯:每次改完标注工具的配置,先拿 5 张图跑一遍完整流程,从加载模型到导出 JSON,确认没有报错再上批量。这个习惯帮我省了很多次「跑了一晚上发现导出格式错了」的后悔药。半自动标注不是全自动,人的判断始终在环里,工具只是把重复劳动压缩掉。希望帮到你。
本文还有配套的精品资源,点击获取