简介:LabelMe是由MIT开发的开源图像标注工具,这个压缩包包含其完整源码与配套文件,面向计算机视觉研究者、深度学习开发者及数据标注人员,用于高效制作语义分割、目标检测与关键点检测等任务所需的标注数据集。包内共251个文件,压缩后约12.4MB,以45个Python源码与依赖脚本为核心,搭配75张jpg和48张png示例图像、23个json标注结果、14个npy数据文件、9个md说明文档,还有yml/yaml/Dockerfile等环境配置与容器化部署支持,以及图标、桌面入口等辅助资源,目录结构清晰,便于本地安装和二次开发。随包提供的labelme命令行工具支持polygon、box、point三种标注方式,可自由绘制多边形、边界框和关键点,并附带labelme2voc.py、labelme2coco.py等转换脚本,直接将标注结果转为PASCAL VOC或COCO格式,无缝对接Mask R-CNN、YOLO、U-Net等主流模型训练流程。已有1072人学习使用该资源,适合需要快速搭建标注环境、系统掌握数据制作全流程的读者,可为深度学习项目构建高质量数据集。
1. LabelMe 不只是画框工具:标注格式的取舍决定模型上限
做计算机视觉的工程师基本都经历过同一个阶段:公开数据集跑通模型之后,面对自己的业务场景,必须从零开始构建标注数据。市面上的标注工具不少,商业化产品带 AI 预标注、云协作和自动推送,但 GitHub 上 LabelMe 的下载量和实际使用量依旧排在前面。原因不在界面,而在输出格式——一个标注结果就是一个 JSON 文件,顶层字段就shapes、imagePath、imageData这几个,没有数据库,没有权限模型,坐标直接写在数组里。这意味着可以徒手写脚本做批量修改、格式转换,甚至在训练代码里直接解析。按安装、标注、格式转换到接入训练管线的顺序,来看实际项目里怎么用、坑在哪、参数怎么调。
2. 环境搭建与标注工具链的选型细节
2.1 Python 虚拟环境、依赖安装与开发模式的选择
LabelMe 通过 PyPI 分发,一条命令就能装,但直接装进全局环境不是好习惯。先隔离虚拟环境,避免和项目里已有的 PyQt、opencv-python 版本打架:
# 创建并激活虚拟环境 python -m venv labelme_env source labelme_env/bin/activate # Windows 下换成 labelme_env\Scripts\activate # 从 PyPI 安装稳定版 pip install labelme # 查看依赖树 pip show labelme安装完成后环境里会多出 labelme 可执行文件,同时自动拉入 PyQt5、numpy、Pillow、imgviz 等依赖。imgviz 是一个容易被忽略的库,负责标注结果的图像渲染,包括多边形轮廓、分割掩膜配色等。后续做批量可视化校验时,可以直接在脚本里import imgviz,渲染出来的结果和 GUI 里看到的基本一致,方便做自动化预览。
如果需要在源码层面做定制,比如新增一种自定义标注形状,建议从 GitHub 拉取源码后执行pip install -e .。开发模式安装让代码改动即时生效,不用反复重新安装。有一点要注意:GUI 依赖 PyQt 事件循环,在没有显示器的 Linux 服务器上,启动前需要设置QT_QPA_PLATFORM=offscreen,否则会提示could not connect to display。即便只是用脚本做格式转换,只要代码 import 了 labelme 的 GUI 模块,这个环境变量同样需要配置。
2.2 CLI 参数与数据目录组织
安装完成之后先别急着打开界面。建议在标注开始前就把数据目录和类别清单定义好,后面转换、训练才能一条路走到黑。
# 典型启动方式:指定类别文件、自动保存、不嵌入图像数据 labelme --labels labels.txt --autosave --nodata --output annotations| 参数 | 作用 | 建议值 |
|---|---|---|
--labels | 指向类别清单文件,一行一个类别名 | 类别名不要包含中文和空格,避免转换脚本出现编码问题 |
--nodata | 设置 JSON 中不保存 base64 图像数据 | 建议开启,否则标注文件动辄几 MB |
--autosave | 切换图片时自动保存当前标注 | 批量标注时建议开启 |
--output | 指定 JSON 输出目录 | 与图像目录分开,便于增量备份 |
--labels的作用是提供类别下拉框供选择,没有强制校验功能。如果标注时临时输入了类别清单之外的标签,LabelMe 也会正常保存,后续转换阶段才会暴露问题。为避免这种情况,通常把类别清单作为受控文件固定下来,转换脚本从同一个文件读取类别集合,保证两侧一致。这样出现标签拼写错误时,只需检查一个文件。
目录结构建议这样组织:
dataset/ ├── images/ # 原始图像 ├── annotations/ # LabelMe 输出的 JSON ├── labels.txt # 类别清单 ├── data_voc/ # VOC 格式输出 └── data_coco/ # COCO 格式输出图像和标注分开存放,是因为两者生命周期不同:原始图像基本不变,标注文件会频繁修改。分开之后可以用增量同步单独处理标注目录,Git 版本管理也更容易追溯标注变更。
2.3 界面启动与首标操作
在同目录执行labelme images启动 GUI 后,左侧文件列表会列出目录下所有图片。用工具栏的 polygon、rectangle、point 工具开始标注。polygon 工具每单击一次落一个点,右键选择类别,回到第一个点闭合图形。rectangle 工具只需拖拽一次,保存格式为两个对角点,但要注意这两个点的拖拽顺序并不固定,后续解析时需要自行归一化。
一个常见误区是:标注完成后直接关掉窗口,但没有保存。如果没开--autosave,关闭当前图片时 LabelMe 会弹出保存确认,不要直接忽略。批量标注场景下,建议先标注 3~5 张图,打开 JSON 检查字段结构,确认无误后再继续,可以在早期拦截方向性错误。
3. JSON Schema 分工与三种标注类型的实现细节
3.1 顶层字段如何影响下游解析
当你在 LabelMe 里保存一个文件时,写出的是这样一个 JSON 文档:
{ "version": "5.3.1", "flags": {}, "shapes": [ { "label": "pedestrian", "points": [[552, 132], [614, 140], [661, 189], [553, 193]], "group_id": null, "shape_type": "polygon", "flags": {} } ], "imagePath": "frame_001.jpg", "imageData": null, "imageHeight": 720, "imageWidth": 1280 }训练代码真正消费的核心是shapes数组和三个图像维度字段。shapes中的每一条记录对应一个标注对象,label是类别名,points是坐标点二维数组,shape_type决定 points 的解释方式,group_id用于把多个形状归到同一个实例,这在实例分割中很有用,比如同一个行人被多个多边形拼起来标注时,可以共享一个 group_id。
imageData字段比较特殊。默认情况下保存整张图的 base64 编码,体积很大。启动时加--nodata后,这个字段为null,下游解析可以走imagePath读取文件。无论哪种方式,imageHeight和imageWidth都对应原始图像分辨率,坐标不会随 GUI 显示缩放发生变化。
3.2 polygon、rectangle、circle 的坐标语义
LabelMe 支持多种shape_type,不同形状的 points 语义差异明显,直接决定转换代码怎么写:
| shape_type | points 含义 | 适用场景 |
|---|---|---|
polygon | 多边形顶点,按标注顺序连线 | 语义分割、实例分割 |
rectangle | 仅 2 个点:两个对角点 | 目标检测 |
circle | 2 个点:圆心和圆上一点 | 圆形目标,如轮胎、细胞 |
line | 2 个点:线段端点 | 车道线、血管中心线 |
point | 1 个点 | 关键点检测 |
用 rectangle 标注时,points 里的两个对角点顺序不固定,和拖拽方向有关。稳妥的做法是解析时做归一化:xmin = min(points[0][0], points[1][0]),ymin = min(points[0][1], points[1][1]),再算出xmax、ymax。很多人在 YOLO 格式转换时报坐标越界或宽高为负,根因就在这。
circle 的半径需要根据第二个点计算欧氏距离,圆心的语义由第一个点给出。这类几何计算必须在转换阶段完成,不能直接套用 polygon 的遍历逻辑。
3.3 flags 与关键点标注的隐藏语义
flags字段有两层用途:顶层 flags 描述整张图像的属性,比如{"is_blurred": true};每个 shape 内部的 flags 用来存单个目标的属性,比如{"occluded": true}。GUI 里可以通过工具栏快速切换这些标记状态。
关键点标注场景中,真正重要的是shape_type: "point"。以人脸关键点为例,一张图上有几十个关键点,每个点作为独立 shape 输出,类别名会非常长。更常用的做法是利用group_id将属于同一目标的点绑定,再配合固定顺序的类别名解析。后续转换到 COCO keypoints 格式时,需要按 group_id 分组并按固定顺序重新排列点序列,这个排序决定了关键点索引到模型的映射关系,顺序一旦错乱,训练出的模型关键点位置就是乱的。
3.4 标注实施后的合法性校验脚本
标注完成后立刻跑一遍合法性检查,可以避免后期训练时才暴露数据问题。一个实用脚本如下:
import json, os, glob for json_path in glob.glob("annotations/*.json"): with open(json_path) as f: data = json.load(f) for shape in data["shapes"]: pts = shape["points"] if shape["shape_type"] == "polygon" and len(pts) < 3: print(f"错误: {json_path} 中 {shape['label']} 多边形点数不足") for x, y in pts: if x < 0 or y < 0 or x >= data["imageWidth"] or y >= data["imageHeight"]: print(f"越界: {json_path} 中坐标 {x},{y} 超出图像范围")这段脚本检查两类最基础的问题:多边形点数不足和坐标越界。实际项目中还会加上类别名合法性检查、两个 shape 重叠面积占比检查。坐标越界时,VOC 或 COCO 的解析库默认会做裁剪,但裁剪后得到的是残缺掩膜,模型在这个区域的预测就没有可靠标签了。越界检查比想象中重要。
4. 从 JSON 到 VOC/COCO/YOLO:转换脚本的使用与改造
4.1 官方转换脚本的用法与输出结构
LabelMe 仓库的 examples 目录下提供了两个官方转换脚本,覆盖语义分割和目标检测的主流格式需求:
# 转换为 PASCAL VOC 格式 python labelme2voc.py annotations data_voc --labels labels.txt # 转换为 COCO 格式 python labelme2coco.py annotations data_coco --labels labels.txtlabelme2voc.py的输出目录结构如下:
data_voc/ ├── JPEGImages/ # 图像副本 ├── SegmentationClass/ # 按类别上色的可视化分割图 ├── SegmentationClassPNG/ # 单通道标注图,像素值=类别编号 ├── SegmentationObject/ # 实例分割图 ├── Visualization/ # 标注叠加可视化 ├── class_names.txt └── ...关键点是SegmentationClassPNG里的像素值就是类别编号。如果 labels.txt 顺序是_background_、person、car,那么 person 区域的像素值是 1,car 是 2。普通 PNG 位深为 8 位,类别数超过 255 时需要考虑其他编码方案。这里再次强调,labels.txt 的顺序一旦确定,训练前不要随意改动,否则所有已生成的掩膜都要重新转换。
4.2 转换中的高频错误定位
| 格式 | 标签呈现方式 | 适配的模型 |
|---|---|---|
| VOC SegmentationClassPNG | 单通道像素值 | U-Net、DeepLab 等分割模型 |
| COCO JSON | 多边形顶点列表加 bbox | Mask R-CNN、mmdetection、Detectron2 |
| YOLO txt | 归一化中心坐标和宽高 | YOLOv5、YOLOv8 等检测模型 |
转 COCO 时有一个隐藏得很深的坑:COCO 对多边形要求闭合,而 LabelMe 的多边形默认不闭合,pycocotools 的某些版本在计算面积时对不闭合多边形有兼容逻辑,但 OpenMMLab 的严格校验模式会直接跳过面积计算失败的样本。表现症状是训练日志里 epoch 开始时提示某几张图没有 annotation。排查方法很简单,把 segmentation 的多边形输出通过 cv2.arcLength 检查闭合性。
转 VOC 方向也有一个常见问题:不要拿SegmentationClass彩色可视化图当训练标签。那张图是为人工查看设计的,里面的颜色是类别到 RGB 的映射,直接读入训练代码,分类损失会完全乱掉。训练请用SegmentationClassPNG。
4.3 自定义脚本:LabelMe 转 YOLO txt
如果用 YOLOv8 训练自己的数据集,官方工具链里没有直接消费 LabelMe 格式的入口。可以让 LabelMe 数据直接转成 YOLO 的 txt:
import json, os, glob img_w, img_h = 1920, 1080 # 实际项目中从图像读取,不要硬编码 with open("labels.txt") as f: classes = [line.strip() for line in f.readlines()] for json_path in glob.glob("annotations/*.json"): with open(json_path) as f: data = json.load(f) txt_path = os.path.splitext(json_path)[0] + ".txt" with open(txt_path, "w") as out: for shape in data["shapes"]: if shape["shape_type"] != "rectangle": continue # 只处理矩形框 label = shape["label"] if label not in classes: print(f"警告: {json_path} 中存在未注册类别 {label}") continue cls_id = classes.index(label) x1, y1 = shape["points"][0] x2, y2 = shape["points"][1] x_center = (x1 + x2) / 2 / img_w y_center = (y1 + y2) / 2 / img_h w = abs(x2 - x1) / img_w h = abs(y2 - y1) / img_h out.write(f"{cls_id} {x_center:.6f} {y_center:.6f} {w:.6f} {h:.6f}\n")这里有几个值得注意的细节。abs()保证宽高始终为正,避免拖拽方向不同带来的负宽高问题。归一化计算是先求中心坐标再除以图像宽高,不要先写x1 / img_w + x2 / img_w再除以 2,浮点误差会稍大。如果归一化后出现大于 1 或小于 0 的值,说明原始标注越界,需要回到第 3 章的校验脚本去修数据。
如果是做实例分割的 YOLOv5-seg,则不能只导出 box 信息,需要把 polygon 的顶点做归一化后,按<class> <x1> <y1> <x2> <y2> ...的顺序写入。顶点数量建议控制在 100 个以内,超过的话先用 Douglas-Peucker 抽稀,YOLOv5-seg 对超长序列的处理效率不高。
4.4 批量处理的性能要点
官方labelme2voc.py默认是单进程循环,一张张处理。几千张图时,大部分时间耗在 JSON 解码和掩膜填充上。简单改造法是把整个转换函数丢进 concurrent.futures.ProcessPoolExecutor,图像解码和 fillPoly 天然是 CPU 密集且相互独立的。核数不用拉满,物理核数减半通常能得到最优吞吐。
注意:进程池模式下每个 worker 都会重新加载一次类别列表和 label map,所以 labels.txt 不要放在每个进程里动态读取,应该在主进程读好后通过 initializer 传递。
5. 把标注结果直接接进训练管线:Dataset 实现与质量校验
5.1 直接在 PyTorch Dataset 中解析 LabelMe JSON
语义分割任务可以不转中间格式,在 Dataset 类里直接读 JSON 并渲染掩膜。这样省掉一次磁盘写放大,也方便在训练代码里维护版本一致性:
import json import cv2 import numpy as np from torch.utils.data import Dataset from PIL import Image class LabelMeSegDataset(Dataset): def __init__(self, json_files, label_map, img_dir): self.json_files = json_files self.label_map = label_map self.img_dir = img_dir def __len__(self): return len(self.json_files) def __getitem__(self, idx): with open(self.json_files[idx]) as f: data = json.load(f) img = Image.open(os.path.join(self.img_dir, data["imagePath"])) mask = np.zeros((data["imageHeight"], data["imageWidth"]), dtype=np.uint8) for shape in data["shapes"]: pts = np.array(shape["points"], dtype=np.int32) cv2.fillPoly(mask, [pts], self.label_map[shape["label"]]) return np.array(img), maskcv2.fillPoly的时间复杂度与多边形顶点数近似线性,普通标注场景开销可忽略。但如果存在几千个顶点的大多边形,每个 epoch 都重绘一次会拖慢训练,建议第一次加载后把 mask 缓存成 npy 文件或直接用 labelme2voc 转成 PNG 一次性落盘。
5.2 数据增强必须同步掩膜
构建完数据集后,一个容易被忽略的问题是增强如何作用于标注。用 albumentations 可以很干净地处理图和掩膜同步变换:
import albumentations as A transform = A.Compose([ A.RandomResizedCrop(height=512, width=512, scale=(0.5, 1.0)), A.HorizontalFlip(p=0.5), A.ColorJitter(), ]) augmented = transform(image=img_array, mask=mask)注意:ColorJitter 这类像素级增强会自动跳过 mask,只有空间变换才会作用到 mask。不需要额外传递 additional_targets,除非一张图有多个掩膜。flip 和 crop 会同步修改 mask 和 image,保证坐标语义一致。如果用 PyTorch 原生的RandomResizedCrop处理 mask,需要保证两次随机采样的参数一致,实际操作麻烦很多,这也是推荐直接用 albumentations 的原因。
5.3 批量渲染标注结果用于抽检
训练前最后一步,把 JSON 和原图渲染成叠加图做人工抽检:
import os import cv2 import json import numpy as np for json_path in json_files: data = json.load(open(json_path)) img = cv2.imread(os.path.join(img_dir, data["imagePath"])) for shape in data["shapes"]: pts = np.array(shape["points"], dtype=np.int32) cv2.polylines(img, [pts], True, (0, 255, 0), 2) cv2.imwrite(f"viz/{os.path.basename(json_path)}.jpg", img)渲染结果重点看三类问题:多边形顶点是否错位、矩形框是否没有贴合目标、类别名称是否配错。如果渲染速度异常慢,多数情况是标注文件过大或者多边形顶点数过多,可以先用最小外接矩形近似替代,保证抽检效率。
本文还有配套的精品资源,点击获取