首页 / 资讯中心 / 文章详情

labelme标注工具实战:从安装到转换YOLO与分割mask的避坑指南

labelme标注工具实战:从安装到转换YOLO与分割mask的避坑指南 ★ FEATURED ARTICLE
简介LabelMe深度学习数据集制作工具是一份面向计算机视觉开发者的开源标注方案源码包适合需要构建语义分割、目标检测或关键点检测训练数据的算法工程师与研究人员。资源共251个文件压缩包约12.4MB包含45个Python脚本、23个JSON标注文件、75张JPG与48张PNG示例图片、14个NPY数据文件以及Markdown文档、YAML/XML配置和Dockerfile等。Python脚本涵盖标注启动、格式转换与工具链集成JSON样例可直接参考标注结构图片用于测试标注流程。已有1073人学习下载。借助包内的labelme2voc.py和labelme2coco.py可将标注结果转换为PASCAL VOC或COCO格式无缝接入Mask R-CNN、YOLO、U-Net等主流模型训练流程。同时提供桌面快捷启动文件、应用图标及跨平台容器化配置帮助开发者快速搭建标注环境并规避依赖安装问题。1. labelme不只是画框工具深度学习数据集制作的现场经验第一次用labelme做深度学习数据集时我以为把目标框画完、保存成json就大功告成了。结果模型训到第三个epoch分割结果把整片天空都当成了前景当时直接麻了。回去排查才发现问题出在标注阶段一张坏的标签、一次没闭合的多边形都会在训练时被成倍放大。labelme是制作深度学习数据集最常见的标注工具解决的是从原始图片到监督信号的这一步要检测什么、分割什么得先让模型有正确答案可学。无论后面跑YOLO做目标检测还是用分割网络做像素分类都绕不开这道工序。这篇围绕labelme整理的是落地笔记怎么把环境装顺、怎么组织标注对象、json里哪些字段有用、怎么转成训练格式以及几个最容易让训练翻车的坑。适合正在自己攒数据集的算法工程师和研究生尤其是那种标注了一两千张图、正打算开训的那批人。2. 用conda装labelme环境隔离、安装命令与启动验证2.1 为什么先用conda建独立环境再装labelme深度学习这台机器上最怕的不是装不上而是环境互相串。今天给旧项目装numpy明天给新框架装protobuf任何一个依赖版本被顶掉另一个项目就启动不了。labelme本身不大但它依赖PyQt、opencv-python、Pillow一整套东西和训练框架挤在同一个base环境里是翻车常见原因。网上搜“conda 安装labelme”能看到大量报错帖其中一半是环境混乱导致的。我的做法是先给labelme单独开一个房间conda负责建环境、装Pythonpip负责装labelme本体。这样训练环境和标注环境完全隔离互不干预。将来不小心把某个包装坏了直接删掉环境重建就行不用重装驱动、不用动其他项目。这个思路比纠结用conda还是用pip更重要。conda create -n labelme python3.8 -y conda activate labelme pip install labelme这三条命令的顺序不能乱。conda create是创建一个名字叫labelme的新环境python3.8表示这个环境里的Python版本固定在3.8。很多新机器默认装的是Python 3.10或3.12直接在这些版本上安装labelme时可能遇到Pillow或opencv找不到预编译包的情况降到3.8这一档最稳妥。conda activate labelme是把当前终端切换到这个环境当中后面的pip install labelme装到哪里就由这个环境决定了。如果所在网络下载Python包特别慢可以在pip install时加镜像参数把下载源切成国内节点。实际使用中这类镜像能明显减少卡在下载环节的时间企业内网环境则可以换成你自己私有源的地址。pip install时可以直接在命令里指定镜像源也可以写进pip配置文件命令行写法最直观pip install labelme -i https://pypi.tuna.tsinghua.edu.cn/simple装完之后要确认当前终端到底在哪个环境里。经常有人忘了activate直接输python发现版本不对或者pip list里根本没有labelme。此时输入python --version和which python看路径是否落在conda的envs目录下。路径不对就说明你还在base环境里跑回头再activate一次就好。2.2 启动验证与界面检查labelme命令能做什么环境装好的第一件事不是急着标图而是先启动验证。常见做法是在终端输入labelme --help如果能看到一屏英文参数说明说明命令能用。接着直接输入labelme会弹出一个带有菜单栏和文件列表的标注窗口。这个黑匣子一旦能打开就说明Qt渲染和图像模块都是通的。窗口打不开或者一闪而过绝大多数是PyQt相关依赖没装对重启后强制重新安装一遍PyQt5再启动之前遇到过两次都是这样解决的。Windows用户如果启动时提示缺少MSVCP140.dll说明本机缺Visual C运行库这个跟labelme本身无关去微软官网下载对应运行库装上即可。MacOS用户则要留意首次打开时弹出的“无法确认开发者”警告在系统设置的隐私与安全性里选择仍然打开就好。另外图像目录所在路径尽量不要带中文和空格部分PyQt版本在解析非ASCII路径时会静默失败表现为Open Dir后列表为空。到这里环境就真的通了。不过labelme能打开只是起点真正的工程问题在数据组织、标注操作和格式转换这三个环节下面逐一展开。3. 标注流程拆解命名、快捷键与json关键字段3.1 标注前先整理数据命名、目录和图像格式很多人拿到一批图片直接打开labelme开始画画到一半发现图片命名乱、格式不统一导出后脚本处理起来极其痛苦。我一般会先把原始素材规整一遍再进入标注。目录结构通常长这样mkdir -p dataset/images dataset/json dataset/labels dataset/masksdataset/images放原始图像dataset/json放labelme生成的标注文件dataset/labels和dataset/masks分别留给目标检测和分割任务转换后的结果。这样一张图对应一份json、一份训练标签文件关系一目了然。命名上建议带场景前缀和编号比如road_001.jpg、bird_002.jpg而不是直接沿用相机的IMG_20231205_183049这种。做鸟类目标检测的数据集、车牌检测这类任务时带有场景语义的命名能让你在发现问题时快速定位是哪批数据出的错。图像格式方面尽量统一成.jpg或.png不要同一批数据里一半是bmp一半是webp。训练框架读取图像时往往按扩展名决定解码方式混用格式会引入不可见的读取路径差异。有些相机拍出来是raw或者大尺寸png可以先压缩成适当尺寸的jpg再标注。常见做法是先用python脚本批量转换再人工抽看几张确认颜色没有偏差。还要注意一点标注时尽量保持原分辨率不要为了标注方便先把图缩小。目标边界在低分辨率下会模糊尤其是分割任务的轮廓稍微缩一下图标注出来的多边形边缘就和真实物体对不上了。真想提速可以用labelme的缩放快捷键在界面里放大局部来标而不是改原始文件。3.2 进入labelme创建多边形、管理标签和常用快捷键打开labelme后左侧的文件夹目录可以定位到dataset/images。操作逻辑是先通过菜单Open Dir打开图片所在目录然后用Create Polygons在图像上画标注画完一个目标弹出一个输入框填类别名接着按CtrlS保存json。反复就这三个动作但工程上有几个细节决定了标注效率。首先是画矩形还是画多边形。labelme表面上区分create polygons和create rectangle但保存到json里的points都是多边形顶点列表。画矩形时存储四个顶点画不规则物体时存的是你点击的每个点。目标检测任务即使最终只需要外接框标注时也可以画四点矩形转换脚本再取外接矩形分割任务则必须老老实实点出物体轮廓顶点越贴合边缘mask质量越高。其次是标签名的统一问题。填类别时手一抖把car和Car都填进去模型就会多学一个几乎没意义的类别。这个坑后面避坑部分会详细讲这里只要记住一个原则所有类别名只用小写英文并且不要带空格。比如统一用person、car、traffic_light不要出现Person、pedestrian、 car这类混搭。如果多人协作标注最好先在文本文件里固定一份允许填写的标签清单放桌面当字典用。常用快捷键方面最值得记的是复制和粘贴标签快捷键作用备注CtrlC复制当前选中标注的标签名配合CtrlV快速复用CtrlV粘贴标签名到新标注连续标注同类目标时最省时间CtrlZ撤销上一步手滑点错时救场CtrlS保存当前json建议每标完一张就按一次CtrlDelete删除当前标注误标或画错时使用复制粘贴标签能显著减少重复输入类别名的时间标注几百张图后体感特别明显。对于一张图上密集排布的小目标可以连续画框、连续CtrlV效率比每次手动填类别高很多。3.3 读懂json结构转换脚本全靠这三个字段标注保存后每个图像对应一个json文件。很多人直接把json丢给别人写的转换脚本里出问题时完全看不懂等于拿着黑匣子开工。我一般会先用python把json打开看一眼import json with open(dataset/json/road_001.json, r, encodingutf-8) as f: data json.load(f) print(imagePath:, data[imagePath]) print(imageData:, data.get(imageData, 没有内嵌图像)) for idx, shape in enumerate(data[shapes]): print(idx, shape[label], shape[shape_type], len(shape[points]), 个顶点)这个脚本读出来的字段就是后续所有转换工作的基础。imagePath记录原始图片文件名shapes是一个列表里面每一个元素对应一个标注目标label是类别名points是这个目标的多边形顶点坐标imageWidth和imageHeight记录图片宽高目标检测转归一化坐标时必须用到。imageData这个字段比较特殊它是图片的base64编码勾选保存时“Save with image data”才会存在会让json膨胀到几十MB训练时根本用不到转换前最好清掉。在实际项目里我通常会把对json的检查脚本写到转换工具的最前面当作一个数据健康检查环节。每次拿到一批新标注先跑一遍看看有多少文件、每个文件里有哪些类别、有没有异常的多边形。这一步能在转换之前就把明显问题暴露出来避免到训练阶段才去查数据问题。4. 从labelme的json转到训练格式转换脚本与参数说明4.1 json转YOLO格式目标检测的最小转换脚本labelme的json不能直接喂给YOLO需要先转成类别id加归一化中心坐标和宽高的txt格式。以yolov8训练自己的数据集为例每张图片对应的txt文件里每一行是一个目标格式为class_id x_center y_center width height四个坐标都归一化到0到1之间。这个转换脚本我一般这样写import json import os def labelme2yolo(json_file, out_file, class_names): with open(json_file, r, encodingutf-8) as f: data json.load(f) img_w data[imageWidth] img_h data[imageHeight] lines [] for shape in data[shapes]: label shape[label].strip().lower() if label not in class_names: print(f跳过未知标签: {label} in {json_file}) continue cls_id class_names.index(label) points shape[points] xs [p[0] for p in points] ys [p[1] for p in points] x1, y1, x2, y2 min(xs), min(ys), max(xs), max(ys) x1 max(0, min(x1, img_w - 1)) y1 max(0, min(y1, img_h - 1)) x2 max(0, min(x2, img_w - 1)) y2 max(0, min(y2, img_h - 1)) if x2 - x1 2 or y2 - y1 2: print(f忽略过小目标: {label} in {json_file}) continue x_center ((x1 x2) / 2) / img_w y_center ((y1 y2) / 2) / img_h width (x2 - x1) / img_w height (y2 - y1) / img_h lines.append(f{cls_id} {x_center:.6f} {y_center:.6f} {width:.6f} {height:.6f}) with open(out_file, w, encodingutf-8) as f: f.write(\n.join(lines))这个脚本的关键逻辑有三点一是把label统一转成小写再去查类别表避免Car和car被当成两个类别二是对坐标做clip标注时手滑拖出边界的情况会被兜住三是过滤掉宽或高小于2像素的目标这类框要么是误点要么是本来就标注错了留到训练里只会产生无效正样本。这里不区分shape_type不管json里存的是rectangle还是polygon都统一取所有顶点的外接矩形这样最稳。class_names这个列表的排序决定了每个类别在txt里的数字id这个顺序必须和后面训练配置里的class列表完全一致否则模型学到的标签和实际标签会错位是最隐蔽的翻车点。注意class_names的顺序就是训练配置里的类别id两者必须完全一致。换数据集时最容易在这里出错只改class.txt一个文件就可以同步两端。调用时脚本需要三个参数json文件路径、输出txt路径、类别表。类别表通常放在一个class.txt文本文件里一行一个类别名脚本读取后转成列表with open(class.txt, r) as f: class_names [line.strip().lower() for line in f if line.strip()]为什么要从文件读而不是直接在代码里写死因为数据集迭代时类别可能会增删写在文件里改起来更快而且多个脚本可以共用同一份类别表减少不同步的概率。批量转换时再写一个外层循环遍历dataset/json目录下所有json逐张调用上面的函数输出到dataset/labels目录。做旋转目标检测时这个脚本的矩形转换就不适用了旋转框需要保留多边形的四个顶点作为旋转框四点坐标像mmrotate训练dota数据集那类任务labelme多边形点的保存方式和这种需求正好匹配转换时跳过外接矩形步骤即可。4.2 json转分割mask多边形填充与类别索引分割任务与目标检测不同json里的坐标最终要变成一张与原始图像等大小的单通道mask图图上每个像素的值就是它所属的类别id。背景是0第一个前景类别是1第二个是2相当于每张图配一张类别索引图。转换时用OpenCV的fillPoly把多边形内部填充成对应的类别值import json import os import numpy as np import cv2 def json_to_mask(json_file, out_dir, class_map): with open(json_file, r, encodingutf-8) as f: data json.load(f) h, w data[imageHeight], data[imageWidth] mask np.zeros((h, w), dtypenp.uint8) for shape in data[shapes]: label shape[label].strip().lower() cls_id class_map.get(label, 0) if cls_id 0: continue if shape[shape_type] circle: cx, cy shape[points][0] x2, y2 shape[points][1] r int(((x2 - cx) ** 2 (y2 - cy) ** 2) ** 0.5) cv2.circle(mask, (int(cx), int(cy)), r, cls_id, -1) else: pts np.array(shape[points], dtypenp.int32) cv2.fillPoly(mask, [pts], cls_id) base os.path.splitext(data[imagePath])[0] out_file os.path.join(out_dir, base _mask.png) cv2.imwrite(out_file, mask)class_map是类别名到整数id的映射需要在转换前根据完整类别表构造。常见做法是给类别表按顺序编号背景固定为0第一个实际类别从1开始。这样mask图只要用np.unique就能看到每张图里出现了哪些类别训练时直接把它当作标签张量不用再做颜色映射。这里有一个很多人容易踩的误区把labelme自带的可视化图片直接当成mask用。labelme的json转可视化图时每个类别被涂成一种颜色看起来花里胡哨但多类别的颜色值是跳变的并不是连续递增的类别id。模型训练时如果直接拿这种彩色图当标签loss和指标全都会乱掉。自己用fillPoly生成干净的单通道mask才是正路。很多通用训练框架以COCO格式为标准coco2017数据集结构里用segmentation字段存多边形坐标labelme的json本质也是多边形坐标迁移起来就是字段名的映射关系。如果物体中间有孔洞比如甜甜圈或者带镂空的工业零件fillPoly一次会把孔洞也填成前景。解决办法是先填外轮廓再用孔洞的轮廓把内部重新填充成背景0。实际操作中这类结构出现频率不高可以单独处理。4.3 划分train/val/test随机种子与场景切分数据转换完成后还需要把数据划分成训练集、验证集和测试集。最常犯的错误是直接按文件名排序后按比例截断比如前80%训练、后20%验证。这样通常会导致验证集与训练集图像内容高度相似模型的实际泛化能力被高估。正确做法是先打乱再划分并且固定随机种子保证每次重新执行结果一致import os import random random.seed(42) json_files [f for f in os.listdir(dataset/json) if f.endswith(.json)] random.shuffle(json_files) n len(json_files) train_files json_files[: int(n * 0.8)] val_files json_files[int(n * 0.8) : int(n * 0.9)] test_files json_files[int(n * 0.9) :] for split, files in [(train, train_files), (val, val_files), (test, test_files)]: with open(fdataset/{split}.txt, w, encodingutf-8) as f: f.write(\n.join(os.path.splitext(x)[0] for x in files))这个脚本输出三个txt文件每个文件一行一个样本名不带扩展名。训练时再用代码把这些样本名拼成图像路径和标签路径。8:1:1是常见默认比例数据量小的时候可以把test数据临时并入val数据量大时也可以调整到9:0.5:0.5关键不是数字而是val和test之间的独立性要足够。这里有个更隐蔽的坑是连续帧。如果图片来自车载摄像头视频抽帧同一段路上连续几帧的内容高度相似。直接随机打乱会把第100帧分到训练集、第101帧分到验证集验证分数虚高。合理做法是先按场景或视频片段分组再以组为单位划分。比如文件名带road_001、road_002这样的前缀就按前缀分组后对组随机打乱确保一个场景的所有帧都落在同一个集合里。做自动驾驶数据集、鸟类目标检测数据集这类连续采集的任务时这个策略尤其重要。提示如果总图数量不足100张不建议强行拆出test验证集保留10到20张即可测试阶段可以最后再补拍或补充数据。5. labelme标注避坑指南5个高频翻车现场与排查5.1 mask全黑或全白训练第一轮就崩现象用转换后的mask训练分割模型第一轮batch的损失就是nan或者模型输出一整张图全是背景。原因排查后通常是两种要么某些json文件里的多边形顶点数少于3个fillPoly填充时得到空区域导致mask全0要么类别id映射错误前景被写成了背景0。这个现象在刚开始标注的人身上特别常见因为画到一半随手保存或双击结束多边形时误操作产生了一个无效标注。解决办法有两层。第一层在标注端每标完一个文件用json检查脚本扫一遍把所有顶点数少于3的shape打印出来并重新标注。第二层在转换端写脚本时加上过滤逻辑顶点数小于3直接跳过并在终端输出警告。代码里加一行判断就能挡住大部分无效数据。if len(shape[points]) 3: print(f警告{json_file}中多边形顶点数不足3个) continue这行判断不能省哪怕你觉得自己标注时很小心。数据量一大手滑的概率是线性的。5.2 标签大小写和空格不一致类别数凭空多出来现象训练完看分类数比预期多了两三个类比如car和Car各占一个id。精度表上这两个类的mAP都很低因为同一个目标被拆成了两个稀疏类别。原因是多人标注或同一人不同时段输入习惯不一致填了car、Car、car带前后空格等变体。这个问题在目标检测和分割任务里都会出现而且不会让你训练报错属于典型的静默翻车。解决思路是在转换脚本入口做标签归一化。最简单可靠的是统一转小写并去除首尾空格再配合一个别名映射表把同义词和拼写变体归并到同一个标准类别名下。比如person、Person、people统一映射成person。映射表写在文件里新增标签时只改文件不动代码这对团队协作特别管用。写完映射后可以加一个统计函数把所有json里出现的原始标签全部打印出来和标准类别表逐条对照一次就能发现所有不一致的变体。5.3 json文件内嵌imageData体积膨胀几十MB现象标注几百张图后json文件夹比原始图片还大好几倍转换脚本跑起来很慢读取一个json要卡几秒。原因是在保存json时勾选了“Save with image data”选项labelme把整张图片的base64编码塞进了json里。这个选项本意是让json文件自带图像数据、方便单独分享但对训练流程来说完全多余。解决方法是转换前统一清理读出json把imageData字段置为None再重新保存。注意重新保存时用ensure_asciiFalse和缩进参数控制输出格式data[imageData] None with open(json_file, w, encodingutf-8) as f: json.dump(data, f, ensure_asciiFalse, indent2)清理后json只剩标注信息文件体积通常会缩小到原来的几十分之一。保留缩进格式是为了便于人工检查转换脚本读取时不受影响。如果还想更省事也可以在保存json时直接不勾选imageData选项但已经产生的文件还是得靠脚本清理。5.4 坐标越界训练时触发张量边界断言现象训练在数据加载阶段抛出类似x_min越界或coordinates should be in [0, 1]的断言错误但抽查标注文件时看起来又正常。原因是画框时鼠标拖出了图像边界labelme仍然把这个越界坐标存进了json。肉眼在界面里很难注意到几像素的越界但训练框架对坐标上下界检查很严格一碰到就中断。解决方式是在转换脚本里对坐标做clip把多边形所有点的x夹在0和imageWidth-1之间y夹在0和imageHeight-1之间。这个操作在4.1的转换脚本里已经有了但对旧数据集还是要单独跑一遍。写一个校验脚本扫描所有json打印出坐标越界的样本列表和越界程度重跑一遍比逐个打开labelme检查高效得多。顺手还可以把越界量大的标注文件人工检查一下因为大幅越界往往意味着标注位置本身就错了clip只是兜底不是修改错误结果。5.5 直接依赖labelme自带转换命令mask不干净现象网上一搜“json转label”很多人会告诉你用labelme自带的labelme_json_to_dataset命令转出来一个文件夹里面包含img.png、label.png等文件。但如果直接把label.png当训练mask用训练出来的分割效果会非常奇怪。原因是这个命令生成的label.png本质是基于颜色映射的可视化图不同类别之间用随机或预设颜色区分像素值并不是从0开始递增的类别id。背景和前景、前景和前景之间的颜色关系不连续也不保证和训练框架期望的标签编码一致。解决方法是把这个命令当预览工具用而不是当转换工具用。我一般用它在标注完成后快速抽查一下标注是否贴合目标轮廓真正进入训练流程的mask还是自己写的fillPoly转换脚本生成。一个最简单的判定标准如果mask像素值里出现了3、5、7这种跳变的类别值那一定不是干净的索引图需要换成自己转的版本。6. 标注完先跑小批量用一次快速巡检避免白训一场数据转换完之后最值得投入的时间是跑一个最小训练循环做验证。所谓最小循环就是只加载一小批数据跑一个epoch不追求精度只检查数据管道和模型前向是否正常。这一步能拦截掉绝大多数隐藏的数据问题比直接启动完整训练省下好几个小时。检查分两个层面。第一个层面是静态检查写几行脚本加载图像和对应标签确认shape能对上、类别id都在合法范围内import cv2 import numpy as np img cv2.imread(dataset/images/road_001.jpg) mask cv2.imread(dataset/masks/road_001_mask.png, cv2.IMREAD_GRAYSCALE) print(图像shape:, img.shape) print(mask类别值:, np.unique(mask)) assert mask.max() num_classes, mask出现了未知类别id如果是目标检测格式就检查txt每一行的五个数字是否都在合理区间中心坐标和宽高都在0到1之间且宽高乘积大于某个最小阈值。这种一次性检查脚本可以长期沉淀在项目里每次更新数据集都跑一遍成本和收益比非常高。第二个层面是动态检查直接用训练脚本加载数据集设置batch_size为8或16跑一个epoch。重点观察两件事一是loss能不能在几十步内正常下降二是训练日志里有没有nan。如果loss一开始就nan或者训练到一半突然崩掉先回头查数据加载而不是调学习率。绝大多数情况下问题出在某个样本的标签异常上。我现在的习惯是任何新标注的数据集到手后先跑这个小循环没问题才上完整训练。这个习惯帮我挡住了至少十次以上的无效训练。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?
咨询建站