简介:针对Python计算机视觉学习者,这份PDF简明介绍了如何借助labelme工具批量将语义分割标注的JSON文件转换为模型训练所需的图片。内容覆盖环境安装、类别映射配置以及核心函数utils.shapes_to_label的使用,可一键生成实例分割图、语义分割图和分割结果与原图叠加图,适合需要快速整理标注数据或入门语义分割数据预处理的中级开发者。资源共1个PDF文档,大小222KB,便于查阅与传播。已有355人学习浏览。文档中包含可直接运行的完整代码,并给出21种颜色定义与类别值映射示例,读者可按自身项目灵活修改背景、车辆等类别,批量输出_lbl.png、_ins.png、_color_label.png等格式,大幅提升数据准备效率。
1. 为什么要从 JSON 批量生成语义分割训练图
用过 labelme 的人都有一个共同痛点:标注一时爽,转换火葬场。标注阶段你只需要在界面上沿着物体边缘打点、命名、存成 JSON;等进入训练阶段,模型要的却是像素级的 PNG 标签图——每个像素一个整数类别 ID,或者一张彩色掩码图。手动在 labelme 界面里逐张导出?数据量一上 100 张,时间成本直接失控。更麻烦的是,训练语义分割模型时通常既需要原始标签值(0、1、2 这种整数矩阵),又需要可视化用的彩色掩码;做对比验证时还要一张原图与掩码叠加的效果图。这篇文章讲的shapes_to_label批量转换方案,核心就是把这三种输出从 JSON 一次性生成,让标注数据直接对接 DeepLabV3、UNet、SAM 微调这类分割训练管线。适合正在做自动驾驶数据集、遥感地物识别或工业质检分割的 Python 工程师,也适合刚把 labelme 标注流程跑通、正卡在数据格式转换上的初学者。读完你不仅能把这套脚本跑起来,还能知道每个参数为什么这样设置、换数据集时要改哪里。
2. shapes_to_label:把多边形转成像素级标签的换算核心
2.1 从 Polygon 到像素标签:shapes_to_label 做了什么
labelme 的 JSON 文件里存的是你手画的shapes,每一笔都是一串[x, y]坐标点,构成一个多边形(Polygon)。语义分割模型不认多边形,它需要的是一个与输入图像同尺寸的二维矩阵,矩阵里每个像素的值就是它所属的类别 ID。
labelme.utils.shapes_to_label(img_shape, shapes, label_name_to_value)这个函数,就是在 JSON 和像素矩阵之间做换算。它接收三个参数,返回两个数组lbl和ins:
lbl, ins = utils.shapes_to_label(img.shape, data['shapes'], label_name_to_value)img_shape是原图的高宽,它决定输出矩阵的尺寸;shapes是从 JSON 里读取的标注多边形列表;label_name_to_value是类别名到整数的映射字典。返回值里的lbl是语义分割标签图,同一个多边形的所有像素共享一个类别 ID;ins是实例分割标签图,同一个多边形的所有像素共享同一个对象 ID——即使两个多边形属于同一个类别,它们的像素值也不同。这个区别后面会展开讲。
2.2 label_name_to_value 映射表:背景不是 0 会出大问题
映射表是整个转换流程里最需要你手动维护的部分。原文代码里的写法是:
label_name_to_value = {'_background_': 0, "background": 1, "vehicle": 2}这段映射的含义很直接:_background_对应 0,是算法默认的背景区域;你在 labelme 里标过的background区域对应 1;vehicle对应 2。注意_background_这个 key 不能改名字,它是shapes_to_label内部识别未标注区域的约定——所有没有被任何多边形覆盖的像素,自动归类为_background_。你在 labelme 里手动画出的、名为background的多边形,则是显式标注的另一个类别,这在实际数据集中很常见:未标注区域不等于背景,也要参与训练。
换数据集时必须先想清楚类别逻辑。如果你做的是二分类分割任务,车辆和道路以外的东西都是背景,那映射表写成{'_background_': 0, "vehicle": 1}就够了。但如果你是做多分类,就必须为每个标注过的区域名称分配一个整数,且这些整数必须从 1 开始连续排列,不能跳号。否则后面生成彩色掩码的循环会白跑一次,某些类别没有对应的颜色值。
2.3 lbl 与 ins:语义分割和实例分割训练数据的分水岭
shapes_to_label同时返回lbl和ins两个数组,这是很多初学者最迷惑的地方。lbl(label)是语义分割标准,它只关心“这个像素是什么类别”,标注为vehicle的两辆车在lbl中的像素值都是 2,肉眼无法区分彼此。ins(instance)是实例分割标准,它关心“这个像素属于哪一个对象”,第一辆车的像素值是 1,第二辆车的像素值是 2,标注为vehicle的三个区域在ins里分别是 1、2、3。即使你没有接 Mask R-CNN 这类实例分割模型,ins图也有它的用途——比如做自监督预训练,或者从语义分割结果反推连通域时,它能作为先验帮助分离粘连目标。这张图是从 JSON 转换时顺手就能得到的,建议保留,不占额外标注成本。
3. 批量转换实现:遍历 JSON 目录到三张输出图
3.1 环境准备:两个库的安装差异
先把依赖装齐。labelme是标注和转换的核心库,scikit-image负责读取原图和保存像素值大于 255 的无损 PNG。安装命令很简单:
pip install labelme scikit-image pillow numpy四个库各司其职:labelme提供utils.shapes_to_label,scikit-image的io模块负责imread读取原图和imsave保存标签 PNG,pillow的Image模块完成数组到图片的转换和混合叠加,numpy则负责所有矩阵运算。labelme主程序要不要按官方说明配置环境变量,取决于你是否还需要交互式标注窗口,如果只是做数据转换,装这个库就够了。
3.2 完整转换代码骨架
这是基于原文思路整理后的完整可用版本,核心逻辑保持不变,补全了文件查找和路径拼接的部分:
import json import os import glob import warnings import numpy as np from skimage import io from PIL import Image from labelme import utils warnings.filterwarnings('ignore') def draw_json_file(json_dir=""): json_files = glob.glob(os.path.join(json_dir, '*.json')) label_name_to_value = {'_background_': 0, "background": 1, "vehicle": 2} # 最多支持 21 种颜色,索引必须与 label_name_to_value 中的数值一一对应 colors = [(0, 0, 0), (128, 0, 0), (0, 128, 0), (128, 128, 0), (0, 0, 128), (128, 0, 128), (0, 128, 128), (128, 128, 128), (64, 0, 0), (192, 0, 0), (64, 128, 0), (192, 128, 0), (64, 0, 128), (192, 0, 128), (64, 128, 128), (192, 128, 128), (0, 64, 0), (128, 64, 0), (0, 192, 0), (128, 192, 0), (0, 64, 128), (128, 64, 12)] for path in json_files: data = json.load(open(path, encoding='utf-8')) img_path = os.path.join(json_dir, data['imagePath']) img = io.imread(img_path) # lbl: 语义分割标签图 ins: 实例分割标签图 lbl, ins = utils.shapes_to_label(img.shape, data['shapes'], label_name_to_value) # 将整数标签渲染成彩色掩码图 seg_img = np.zeros((img.shape[0], img.shape[1], 3), np.uint8) for c in range(len(label_name_to_value)): seg_img[:, :, 0] += ((lbl[:, :] == c) * (colors[c][0])).astype('uint8') seg_img[:, :, 1] += ((lbl[:, :] == c) * (colors[c][1])).astype('uint8') seg_img[:, :, 2] += ((lbl[:, :] == c) * (colors[c][2])).astype('uint8') # 原图与彩色掩码叠加,0.7 是原图透明度 blended = Image.blend(Image.fromarray(img), Image.fromarray(seg_img), 0.7) # 三种输出分别保存 blended.save(path.replace(".json", '_color_label.png')) Image.fromarray(lbl).save(path.replace(".json", '_lbl.png')) Image.fromarray(ins).save(path.replace(".json", '_ins.png')) # 放大到 255 的版本,用于可视化检查 io.imsave(path.replace(".json", '_语义分割.png'), lbl, check_contrast=False) io.imsave(path.replace(".json", '_实例分割.png'), ins, check_contrast=False) print(f"processed: {path}") if __name__ == '__main__': draw_json_file(r"C:\Users\Administrator\Desktop\data\\")逻辑说明:遍历目录下所有 JSON 文件,对每个文件先读原图和标注数据,用shapes_to_label拿到语义标签和实例标签两个矩阵;然后把整数标签按颜色表渲染成 RGB 掩码,再与原图混合,得到可视化的叠加图;最后分别保存。每个 JSON 文件会产出五张图,其中_语义分割.png和_实例分割.png是io.imsave保存的,它会把标签值映射到 0-255 的灰度范围方便直接查看,而_lbl.png和_ins.png是原始整数值的无损存储,供训练读取时使用,这两个版本千万别搞混。
参数说明:check_contrast=False是scikit-image保存 PNG 时的关键参数,如果不加,imsave会检查图像数据是否为低对比度并自动缩放,导致标签值 1、2、3 被拉伸成完全不同的灰度,模型读了以后类别全部错乱;encoding='utf-8'是在 Windows 系统上读取带中文路径或中文标注名的 JSON 时避免UnicodeDecodeError的标准做法。
3.3 五种输出文件的用途对照表
| 输出文件名 | 内容 | 用途 |
|---|---|---|
_lbl.png | 语义分割整数标签(0-类别数-1) | 语义分割模型训练标签,直接喂给 loss 计算 |
_ins.png | 实例分割整数标签(每个对象独立 ID) | 实例分割、连通域分析、目标计数 |
_color_label.png | 彩色掩码与原图 0.7 混合的叠加图 | 人工质检标注效果、论文可视化 |
_语义分割.png | 灰度拉伸后的语义标签 | 快速查看标注覆盖率,不能用于训练 |
_实例分割.png | 灰度拉伸后的实例标签 | 快速检查对象边缘是否贴合,不能用于训练 |
值得注意的是_color_label.png是训练的强辅助工具,很多人在训练分割模型时只喂原图和标签图,忽略了叠加图的价值。实际上把原图和掩码叠加后,可以肉眼检查标注是否贴合物体边缘——如果掩码明显超出了物体边界或落后于边界,这张图立刻就能发现,不需要在写代码时再单独写一个可视化脚本。
4. 训练数据生产中的选型与避坑
4.1 语义分割模型应该用哪一张
训练语义分割模型时,常规做法是读_lbl.png作为监督信号,通过DataLoader的PIL.Image.open读取后转为 LongTensor。但_lbl.png里每个像素的取值是 0、1、2,在保存为 PNG 时会被无损保留;而_语义分割.png经过灰度拉伸原本 1 和 2 会被映射成不同的灰度值,训练模型读这一张图会直接导致 loss 计算错误。验证方法很简单:加载两张图,分别打印np.unique(lbl_array),正确定输出只有[0 1 2],如果出现大量中间灰度值则说明读错了文件。
实际项目中还有一个选择:训练时究竟用_lbl.png还是_color_label.png。答案是前者,因为深度学习 loss 函数需要的是类别索引,而不是颜色索引。_color_label.png的颜色值是(128, 0, 0)这种 RGB 组合,如果直接喂给模型,模型会去拟合颜色,而不是类别语义,等推理时输出的掩码颜色和你训练时的颜色表绑死,换一张颜色表整个模型就废了。颜色图只适合做可视化和人工质检,训练永远用整数标签图。
4.2 Image.blend 叠加比例的敏感度
原文中Image.blend(Image.fromarray(img), Image.fromarray(seg_img), 0.3)的第三个参数是掩码的透明度,0.3 表示掩码权重为 30%。这个值不是随便定的。掩码权重太低,叠加图里掩码颜色过淡,人眼难以辨析标注边界与背景;权重太高,原图细节被掩码覆盖,无法判断标注是否偏离了目标边缘。在遥感影像这类大面积连续地物场景里,建议掩码权重放到 0.4 到 0.5,因为地物区域大、边缘长,需要更强的颜色提示来快速扫描;在自动驾驶场景里,目标小且密集,建议保持 0.3 以下,否则多辆车叠加后颜色糊成一团。调整后重新运行脚本即可,不需要改动其他任何逻辑。
4.3 标注不规范时的数据清洗思路
批量转换中最常遇到的三个问题是:imagePath指向的文件不存在、JSON 中没有shapes字段、label_name_to_value中缺少标注文件里出现的类别名。第一个问题在跨机器拷贝数据时经常发生,因为 labelme 的imagePath在保存时会写入绝对路径或相对路径,换机器后路径失效。稳妥做法不是改代码绕过去,而是把图片和 JSON 放在同一个目录下,改imagePath为只取文件名:
data['imagePath'] = os.path.basename(data['imagePath']) img = io.imread(os.path.join(json_dir, data['imagePath']))第二个问题的处理更直接:空标注的 JSON 没有shapes,shapes_to_label会返回全 0 的lbl和ins,对应的训练图就是纯背景图,理论上不参与训练。可以在循环开头加一个过滤条件,标注为空的直接跳过。第三个问题需要你在标注阶段就严格约定类别名称,标注时随手打了"vehicle"和"Vehicle"两个名字,映射表里只有小写,转换过程不会报错,但shapes_to_label会自动丢弃映射表里不存在的类别,导致那一整个多边形从标签图里消失,而且没有任何错误提示。这个坑最隐蔽,建议在转换前用一行代码做校验:
all_names = {shape['label'] for shape in data['shapes']} missing = all_names - set(label_name_to_value.keys()) assert not missing, f"映射表缺少类别: {missing}"5. 进阶:动态生成 colors 映射表,摆脱 21 色限制
5.1 固定颜色表的局限在哪里
原文中colors列表写死了 21 组 RGB 值,与label_name_to_value的类别数绑定。这个设计在一个中等规模数据集中够用,但实际项目里类别数很容易超过 21——遥感场景中地物类别加上“背景”“其他”“未知”这些辅助类,轻松突破 30。而且手动维护一张颜色表很容易出错,新增类别后还要同步数一遍颜色数量是否足够、索引是否对齐。
5.2 从类别数动态生成颜色
一个可复用的做法是用 HSV 色相环均匀采样,保证任意类别数都有可区分的颜色:
import colorsys def create_color_map(class_num): color_map = [] for i in range(class_num): hue = (i * 137.508) / 360.0 # 黄金角分布,防止相邻颜色过于接近 rgb = colorsys.hsv_to_rgb(hue, 0.8, 0.9) color_map.append(tuple(int(255 * c) for c in rgb)) return color_map这段代码的核心是黄金角137.508度,它能让色相在圆环上均匀散开,避免相邻类别分到相似的颜色。colorsys.hsv_to_rgb把 HSV 转成 RGB,0.8是饱和度,0.9是亮度,这样得到的颜色整体偏亮,叠加到原图上依然能看清纹理细节。使用方式是把for c in range(len(label_name_to_value))改成for c in range(len(label_name_to_value))后直接替换colors为create_color_map(len(label_name_to_value))。
5.3 改成动态色表后必须验证的一件事
颜色表改为动态生成后,_color_label.png的质量要重新做一次人工抽检,因为同一类别在不同批次运行时生成的 RGB 值完全一样——这没问题,但如果你在训练中途新增了类别,整个颜色表会整体变化,之前所有已生成的_color_label.png里旧类别对应的颜色都会变。这不会影响训练,因为模型用的是_lbl.png里的整数 ID,但如果你同时在做对比实验或论文配图,旧图和新图在视觉上会出现同一类别的颜色不一致。应对方法是把create_color_map的输出结果序列化保存一份,比如存成npy文件,下次运行直接加载,保证颜色表在数据集全生命周期中稳定不变。检查方式是选一个类别密度高的原图,把新生成的_color_label.png与旧版本并排对比,确认每个类别的视觉提示依然可读、且不同类别之间没有明显近色。
本文还有配套的精品资源,点击获取