资讯详情

批量转换Labelme JSON为语义分割标签图:shapes_to_label详解

📅 2026/9/18 15:59:29 | 华诺云谱 👁 阅读
批量转换Labelme JSON为语义分割标签图:shapes_to_label详解
简介针对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和inslbl, ins utils.shapes_to_label(img.shape, data[shapes], label_name_to_value)img_shape是原图的高宽它决定输出矩阵的尺寸shapes是从 JSON 里读取的标注多边形列表label_name_to_value是类别名到整数的映射字典。返回值里的lbl是语义分割标签图同一个多边形的所有像素共享一个类别 IDins是实例分割标签图同一个多边形的所有像素共享同一个对象 ID——即使两个多边形属于同一个类别它们的像素值也不同。这个区别后面会展开讲。2.2 label_name_to_value 映射表背景不是 0 会出大问题映射表是整个转换流程里最需要你手动维护的部分。原文代码里的写法是label_name_to_value {_background_: 0, background: 1, vehicle: 2}这段映射的含义很直接_background_对应 0是算法默认的背景区域你在 labelme 里标过的background区域对应 1vehicle对应 2。注意_background_这个 key 不能改名字它是shapes_to_label内部识别未标注区域的约定——所有没有被任何多边形覆盖的像素自动归类为_background_。你在 labelme 里手动画出的、名为background的多边形则是显式标注的另一个类别这在实际数据集中很常见未标注区域不等于背景也要参与训练。换数据集时必须先想清楚类别逻辑。如果你做的是二分类分割任务车辆和道路以外的东西都是背景那映射表写成{_background_: 0, vehicle: 1}就够了。但如果你是做多分类就必须为每个标注过的区域名称分配一个整数且这些整数必须从 1 开始连续排列不能跳号。否则后面生成彩色掩码的循环会白跑一次某些类别没有对应的颜色值。2.3 lbl 与 ins语义分割和实例分割训练数据的分水岭shapes_to_label同时返回lbl和ins两个数组这是很多初学者最迷惑的地方。lbllabel是语义分割标准它只关心“这个像素是什么类别”标注为vehicle的两辆车在lbl中的像素值都是 2肉眼无法区分彼此。insinstance是实例分割标准它关心“这个像素属于哪一个对象”第一辆车的像素值是 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_labelscikit-image的io模块负责imread读取原图和imsave保存标签 PNGpillow的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, encodingutf-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_contrastFalse) io.imsave(path.replace(.json, _实例分割.png), ins, check_contrastFalse) print(fprocessed: {path}) if __name__ __main__: draw_json_file(rC:\Users\Administrator\Desktop\data\\)逻辑说明遍历目录下所有 JSON 文件对每个文件先读原图和标注数据用shapes_to_label拿到语义标签和实例标签两个矩阵然后把整数标签按颜色表渲染成 RGB 掩码再与原图混合得到可视化的叠加图最后分别保存。每个 JSON 文件会产出五张图其中_语义分割.png和_实例分割.png是io.imsave保存的它会把标签值映射到 0-255 的灰度范围方便直接查看而_lbl.png和_ins.png是原始整数值的无损存储供训练读取时使用这两个版本千万别搞混。参数说明check_contrastFalse是scikit-image保存 PNG 时的关键参数如果不加imsave会检查图像数据是否为低对比度并自动缩放导致标签值 1、2、3 被拉伸成完全不同的灰度模型读了以后类别全部错乱encodingutf-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 没有shapesshapes_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 转成 RGB0.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与旧版本并排对比确认每个类别的视觉提示依然可读、且不同类别之间没有明显近色。本文还有配套的精品资源点击获取
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。