Labelme图像分割标注实战:环境搭建、JSON转掩码与数据校验 简介这份文档聚焦图像分割与标注工具Labelme的入门使用面向计算机视觉、机器学习及数据科学方向中需要制作标注数据的初学者。内容围绕Anaconda环境准备、Python2与Python3两种环境下的虚拟环境创建、pyqt与pyqt5依赖安装、Labelme安装与启动以及通过labelme_json_to_dataset将JSON标注文件转换为可视化结果展开并给出命令示例和Windows下打开cmd等注意事项可帮助读者较快完成工具环境搭建并理解标注结果查看流程。资源包仅含1个docx文档大小约14KB篇幅精简、重点集中适合作为随手查阅的速查材料。目前已有15266人学习说明其在Labelme入门场景中具有一定参考价值。1. Labelme 安装与使用从零搭建一个能出训练数据的标注环境很多人第一次做图像分割卡住的地方不是模型收敛而是标注数据出不来。手里一堆原始图片却不知道用什么工具画多边形、也不知道画完之后怎么变成模型能吃的掩码。Labelme 就是解决这一步的它是一个基于 PyQt 的图像标注工具支持多边形、矩形、点、线段、圆等多种标注方式输出 JSON 格式的标注文件配合labelme_json_to_dataset可以把 JSON 反解成掩码图、可视化图和标签表。这篇笔记按「能把工具跑起来 → 能画对 → 能把标注转成训练数据 → 能把坑避开」的顺序往下走适合刚接图像分割任务的新手也适合被旧版 Python 环境折腾过的老手复查参数。2. Labelme 安装路线conda 虚拟环境与 PyQt 依赖怎么配安装 Labelme 最容易翻车的地方不是pip install那一下而是环境不隔离、Qt 依赖冲突。我一般会强制把 Labelme 装进独立 conda 环境跟主环境的 PyTorch、OpenCV 彻底隔开这样后面装其他标注工具或者跑训练都不会互相打架。2.1 为什么选 conda 而不是全局 pipLabelme 依赖 PyQt5旧版本是 PyQt4而很多机器的 base 环境里已经装过别的 Qt 系软件或者 OpenCV 的 GUI 组件直接pip install labelme经常出现窗口能启动但一保存就闪退、或者文件对话框打不开的情况。conda 的价值在于它能把 Python 解释器和二进制依赖Qt、zlib 之类一起锁在环境目录里出问题直接删环境重来不用在系统层面清理。另一个现实原因是 Labelme 对 Python 版本有要求。原始教程里给了 Python2.7 和 Python3.6 两条路线Python2 现在基本没人用了除非你在维护一个很老的项目否则统一走 Python3。要注意的是不要盲目照抄python3.6Labelme 新版本对 Python 有最低版本要求3.6 已经偏旧装的时候会拉到很老的 Labelme 版本或者干脆解不出依赖。常见做法是把小版本抬到 3.83.10 这个区间兼容性和包可用性都比较稳。具体操作是这样先确认 conda 可用# 确认 conda 版本能输出版本号即可 conda --version # 查看当前已有的环境列表避免重名 conda env listconda --version用来确认 Anaconda 或者 Miniconda 已经装好并进了 PATH。如果提示找不到命令去检查安装时是否勾选了「加入环境变量」或者手动打开 Anaconda Prompt 再执行。conda env list是为了看一眼有没有叫 labelme 的同名环境避免 create 报已存在。2.2 创建环境并安装 PyQt5 与 Labelme创建环境这一步参数就三个要留意--name指定环境名python指定解释器版本。# 创建名为 labelme 的独立环境Python 用 3.8 conda create --namelabelme python3.8 -y # 激活环境 conda activate labelme # 确认当前解释器路径落在 labelme 环境里 python -c import sys; print(sys.executable)-y是自动确认省掉交互。激活后一定要看一眼sys.executable的输出路径里面必须包含envs/labelme。这一步是血泪经验很多人在 Windows 上conda activate没生效shell 未初始化后面 pip 装进了 base结果运行时又找不到模块绕一大圈。环境激活正确后再装依赖# 先装 PyQt5注意是新版本用 pyqt5不是 pyqt pip install pyqt5 # 再装 labelme pip install labelme # 验证安装查看版本号 labelme --version # 查看 json 转换脚本是否可用 python -c import labelme; print(labelme.__file__)这里有两个关键点。第一PyQt5 的包名是pyqt5原始教程里 Python2 路线写的是pyqt对应 PyQt4照抄到 Python3 上会装错包。第二一定要先装 Qt 再装 Labelme反过来的话 pip 可能会拉一个不匹配的 Qt 版本出现「could not find or load the Qt platform plugin windows」这类报错。labelme --version能正常输出说明命令行入口已经注册。import labelme打印出文件路径说明 Python 层也能找到包。两个都过了才算装完。2.3 启动方式与工作目录习惯启动就一条命令但工作目录很关键# 激活环境后启动 conda activate labelme labelme启动后默认打开的是当前命令行所在的目录。我的习惯是每次先cd到图片所在文件夹再启动labelme这样左侧文件列表直接列出待标注图片不用一个个点「打开」。如果你在 Windows 上直接双击 Anaconda 里的快捷方式启动工作目录可能是用户目录找图会很麻烦。如果要指定起始目录可以带上参数# 直接指定图片目录打开 labelme /path/to/images这个路径参数新手经常忽略其实它比在 GUI 里翻目录高效得多尤其是标注集分散在多个子文件夹时。提示安装完成后建议立刻用一两张图走一遍「打开 → 画多边形 → 保存」的全流程。如果保存时崩溃大概率是 Qt 版本冲突重建环境比修依赖快。3. 标注实操从画多边形到导出 JSON 的完整链路环境通了之后真正决定数据质量的是标注环节。Labelme 的标注结果全部落到 JSON 里JSON 里存的是点坐标序列加上图像路径、图像尺寸、标签名。理解这个结构后面转掩码、改标签、写脚本才有依据。3.1 界面元素与一次完整标注启动后左侧是文件列表中间是画布右侧是标签与标注对象列表顶部工具栏是各种绘制工具。一次标准的语义分割标注流程第一步打开图片。点击「Open」或直接把图片拖进窗口画布会显示图像右上角显示当前文件名。第二步选工具。做分割用「Create Polygons」多边形做检测框用「Create Rectangle」关键点用「Create Point」。分割任务里多边形是主力。第三步画闭合区域。沿目标边缘依次点击最后一个点和第一个点重合时闭合弹出标签输入框。标签名建议用英文小写加下划线比如person、road_mark别用中文和空格——后面转数据集、写训练配置时能省掉大量编码问题。第四步微调。画歪了不用重来选中标注对象后可以拖动顶点。边缘复杂的目标建议放大画布再点顶点密度直接影响掩码质量。第五步保存。快捷键CtrlS会在图片同目录生成同名.json。注意它不会覆盖图片只多一个文件。标注过程中右侧列表可以随时改标签名、删除误标对象。整个工程我建议图片和 JSON 放在同一目录转数据集脚本默认就是按这种同级关系找文件的。3.2 JSON 结构一个文件里到底存了什么打开一个保存后的 JSON关键字段大致长这样结构示意不是逐字复制字段含义用途versionLabelme 版本号排查兼容性问题时看imagePath图片相对路径转数据集和训练时定位原图imageData图片 base64可选保存方式不同可能为空imageHeight/imageWidth原图尺寸坐标反算、缩放还原shapes标注对象数组核心每个对象含 label、points、shape_typeflags自定义标记可存难易度、遮挡等附加信息shapes里每一项的points是[x, y]像素坐标数组shape_type常见值有polygon、rectangle、point、linestrip、circle。掩码生成就是把每个 polygon 的坐标序列填充成一张单通道图。这里有个容易忽略的点JSON 里的坐标是相对原图分辨率的绝对像素值如果你标注过程中用 GUI 缩放过图片保存的仍是原图坐标不会失真。但如果你用脚本批量改过图片尺寸却没同步 JSON坐标就全错了转出来的掩码会整体偏移。判断依据是imageHeight和实际读到的图片高度是否一致。3.3 把 JSON 转成可视化结果与掩码Labelme 自带一个转换脚本原始教程里写作python labelme_json_to_dataset 文件名.json。这条命令现在多半会报找不到文件因为新版本里这个入口被挪成了模块调用方式正确写法是# 激活环境 conda activate labelme # 单个 json 转数据集 labelme_json_to_dataset 0001.json -o 0001_out-o指定输出目录不指定会输出到 JSON 同级的同名文件夹里。如果labelme_json_to_dataset这个命令不可用用模块方式跑# 模块方式调用等价于上面的命令 python -m labelme.cli.json_to_dataset 0001.json -o 0001_out转换完成后输出目录里通常会有这几个文件文件名内容label.png单通道掩码像素值为类别索引label_names.txt索引与标签名的对应关系label_viz.png叠加了颜色的可视化结果info.yaml记录标签列表等元信息label.png是后面做训练标签的主力label_viz.png用来肉眼检查标注是否正确——这两张图我每次转换后都会打开看一眼比盯着 JSON 靠谱得多。注意label.png的像素值是 0、1、2 这样的索引不是 0/255 二值图直接拿去当二值掩码用会出错。批量转换拿单条命令跑太慢写个循环更实际# 批量转换当前目录下所有 json每个输出到独立子目录 for f in *.json; do name${f%.json} labelme_json_to_dataset $f -o ${name}_out done${f%.json}是去掉后缀取文件名输出目录按名字_out建避免不同样本的输出混在一个文件夹里互相覆盖。跑之前先拿一两个样本验证确认标签名和像素索引对得上再全量跑。4. 从掩码到类别图JSON 转数据集之后还要做的三件事很多人以为label_viz.png出来就万事大吉其实那只是可视化图。真正喂给模型的是类别索引图中间还有路径映射、索引对齐、数据划分这几步要做。这一章把转换后的收尾工作补完。4.1 索引与标签名对齐label_names.txt里按行列出标签名行号顺序就是label.png里的像素值。也就是说第 0 行对应像素值 0第 1 行对应 1。问题在于这个顺序不是固定的它取决于标注时标签出现的顺序两个不同批次的数据集很可能person在一个里是 1、在另一个里是 2。直接混用会导致模型学错类别。稳妥做法是维护一份固定的标签映射表转换完成后统一重映射import numpy as np from PIL import Image # 固定映射标签名 - 训练用类别 id fixed_map {background: 0, person: 1, car: 2, road: 3} # 读取当前数据集的标签顺序 with open(0001_out/label_names.txt, r, encodingutf-8) as f: names [line.strip() for line in f if line.strip()] # 构建旧索引 - 新索引的查找表 lut np.zeros(256, dtypenp.uint8) for old_idx, name in enumerate(names): if name in fixed_map: lut[old_idx] fixed_map[name] else: print(f未在映射表中的标签{name}) # 对掩码做查表重映射 mask np.array(Image.open(0001_out/label.png)) new_mask lut[mask] Image.fromarray(new_mask).save(0001_out/label_fixed.png)这段代码的核心是lut查找表把旧像素值当索引去查新值一次批量替换比逐类循环快得多。lut长度给 256 是覆盖 uint8 全部取值标签数超过 255 就不适用了不过语义分割一般不会到这个量级。转换完记得抽查几个像素点跟label_viz.png对照一下颜色分布是否一致。4.2 图片路径与数据集划分JSON 里的imagePath存的是相对路径如果标注时图片和 JSON 同级这个字段通常就是文件名。批量整理成训练集时我一般会建立标准结构dataset/ images/ masks/ train.txt val.txt用脚本把图片和重映射后的掩码分别拷进去并统一命名import json import shutil from pathlib import Path root Path(.) img_dir root / dataset/images mask_dir root / dataset/masks img_dir.mkdir(parentsTrue, exist_okTrue) mask_dir.mkdir(parentsTrue, exist_okTrue) for js in root.glob(*.json): stem js.stem with open(js, r, encodingutf-8) as f: meta json.load(f) img_name Path(meta[imagePath]).name src_img js.parent / img_name src_mask root / f{stem}_out/label_fixed.png if src_img.exists() and src_mask.exists(): shutil.copy(src_img, img_dir / f{stem}{src_img.suffix}) shutil.copy(src_mask, mask_dir / f{stem}.png)Path(meta[imagePath]).name只取文件名规避 JSON 里存了绝对路径或者带反斜杠的 Windows 路径问题。加上exists()判断是后悔药数据集中偶尔有删除图片但 JSON 残留的情况不判断会中途报错中断。划分训练验证集时按文件名打乱后写入 txt比例常见 8:2小数据集可以 7:3保证验证集里各类别都有样本。4.3 转成训练框架要的格式不同框架对掩码格式的要求不一样。语义分割多数要求单通道索引图实例分割需要每个实例独立掩码检测任务则需要从shapes的 bounding box 字段直接读框。如果走的是通用格式可以从points自己算外接框import json with open(0001.json, r, encodingutf-8) as f: data json.load(f) h, w data[imageHeight], data[imageWidth] lines [] for shape in data[shapes]: if shape[shape_type] ! polygon: continue xs [p[0] for p in shape[points]] ys [p[1] for p in shape[points]] # 归一化到 0-1适配 YOLO 风格 cx (min(xs) max(xs)) / 2 / w cy (min(ys) max(ys)) / 2 / h bw (max(xs) - min(xs)) / w bh (max(ys) - min(ys)) / h lines.append(f{shape[label]} {cx:.6f} {cy:.6f} {bw:.6f} {bh:.6f}) with open(0001.txt, w, encodingutf-8) as f: f.write(\n.join(lines))这里用的多边形外接框只是近似框对细长或 L 形目标会偏大做检测任务时如果精度要求高建议单独用矩形工具补标。归一化除的是原图宽高不是缩放后画布尺寸这一点别搞错。提示转换脚本跑完抽样把label_fixed.png和原图叠加显示一次。叠加后轮廓严丝合缝才说明坐标和尺寸没问题这一步能拦住绝大多数静默错误。5. 避坑排查安装与标注里最容易翻车的五个地方这一章按「现象 → 原因 → 解决」记录我实际遇到过的问题都是装完能跑、一用就出事的类型。5.1 启动报 Qt platform plugin 错误现象命令行执行labelme后弹窗报「could not find or load the Qt platform plugin windows」程序直接退出。原因PyQt5 的二进制插件和当前环境里的 Qt 库版本不匹配常见于先装了 Labelme 后补装 Qt或者 base 环境里残留了另一个 Qt 版本被优先加载。解决删环境重建严格按「建环境 → 装 pyqt5 → 装 labelme」的顺序。已经装了的话可以先pip uninstall pyqt5 labelme再按顺序装回。Windows 上还要确认没有把 Anaconda 的 Library\bin 手动塞进 PATH 造成冲突。5.2 保存 JSON 时崩溃或文件为空现象画完多边形按CtrlS程序闪退或者生成的 JSON 只有几字节。原因多数是磁盘权限或者路径含中文、特殊符号。Labelme 写文件时对路径编码处理不够健壮中文目录下偶发写入失败。解决把图片和标注目录换成纯英文路径比如D:/data/seg/。已经标了一半的先备份 JSON 再换目录别直接移动。5.3 label.png 全是黑图现象转换成功但label.png打开是纯黑。原因掩码像素值是 0、1、2 这种小索引普通看图软件按 0255 显示1 和 2 看着就是黑的不是转换失败。解决用 Python 读数组看唯一值np.unique(np.array(Image.open(label.png)))能列出 0、1、2 就说明没问题。要看效果直接用label_viz.png。5.4 label_names.txt 顺序不一致现象两批数据各自转换都正常合并训练后模型对某一类完全学不会。原因不同批次标注时标签录入顺序不同导致同一类别在不同label.png里像素值不一样模型看到的是矛盾标签。解决按 4.1 的查找表统一重映射转换流程里固定加一步别指望人工核对。5.5 标注点太稀疏导致掩码锯齿现象转换出的掩码边缘呈明显多边形折线跟目标轮廓差很多。原因画多边形时顶点太少尤其曲线边缘只用几个点概括。解决曲线部分加密顶点或者转完后用形态学开闭运算平滑边缘。精度要求高的场景我一般要求标注时放大到 200% 以上再点宁可多点几下。6. 提效技巧批量标注与质量校验的固定动作用熟了之后真正省时间的是两个动作一是把重复标注交给脚本预处理二是每次转换后强制做一遍质量校验。批量预处理这块我习惯先用一个轻量模型跑一遍推理把预测框或者粗分割结果写成 Labelme 能读的 JSON人工只做修正。JSON 结构自己拼就行shapes里放rectangle或polygonimagePath填对应图片名imageData留 null。注意version字段随便填个兼容版本字符串即可Labelme 打开时主要看shapes和图像字段。这样预标注能省掉大量「从零画框」的时间尤其目标密集的图。质量校验我固定跑一段脚本检查四件事JSON 能否被解析、shapes是否为空、坐标是否越界、标签名是否都在白名单里。import json from pathlib import Path whitelist {person, car, road} bad [] for js in Path(.).glob(*.json): try: data json.loads(js.read_text(encodingutf-8)) except Exception as e: bad.append((js.name, f解析失败: {e})) continue if not data.get(shapes): bad.append((js.name, 无标注对象)) continue w, h data[imageWidth], data[imageHeight] for s in data[shapes]: if s[label] not in whitelist: bad.append((js.name, f未知标签 {s[label]})) for x, y in s[points]: if not (0 x w and 0 y h): bad.append((js.name, 坐标越界)) break for name, msg in bad: print(name, msg) print(f共检查完成异常 {len(bad)} 条)这段代码把最隐蔽的错误提前暴露空标注、错标签、越界点。坐标越界往往是图片被替换过但 JSON 没更新导致的越界点会让训练时裁剪出全黑区域表现为「某些样本 loss 异常高但看不出原因」。跑完把异常清单丢回给标注环节返工比训练阶段再回头查成本低得多。还有一个习惯值得养成每批数据转换完随机抽 10 张把label_fixed.png和原图按 0.5 透明度叠加存成一张对比图。肉眼看一遍轮廓错位、类别串标这类问题基本跑不掉。从那以后我每次交付数据集前都会把校验脚本和叠加抽检这两步强制走一遍再急也不跳过——省下的不是几分钟是后面调模型时几天的排查。希望帮到你。本文还有配套的精品资源点击获取