资讯动态

从labelme到YOLO分割格式:苹果叶病害数据集转换与YOLOv8-seg训练

发布时间:2026/9/12 22:00:56 来源:尧图企业网站定制
简介面向农业视觉与病害识别场景的苹果叶片分割数据集采用labelme格式包含295张真实田间叶片jpg原图及对应多边形标注json文件共计590个文件压缩包约13.54MB。标注覆盖Rust、Scab、Healthy三个类别分别提供101、100、98个标注框适合用于语义分割、实例分割模型的训练与评估。数据集中不同病害与健康叶片均有覆盖可支撑分类、检测及分割任务的前期实验。数据集以labelme5.5.0工具绘制polygon用户可直接打开编辑或自行转换为YOLO、COCO等格式。目前已有281人学习下载适合计算机视觉初学者或农业智能化项目开发者用于数据扩充、模型验证和算法调试。1. 拿到手先别打开 labelme这类病害分割数据集的价值在 JSON 内部一个包含 295 张苹果叶病害图片的 labelme 数据集真正值钱的地方不是图片本身而是旁边那 295 个同名 JSON 文件。这些 JSON 里记录的是 Rust、Scab、Healthy 三个类别的多边形坐标坐标的质量直接决定后续训练语义分割模型还是实例分割模型的上限。如果你以为“labelme 格式 直接能训练的标注”第一课就错了它只是中间格式需要先审计、再转换才能进入 YOLO 或 COCO 等训练管线。本文从 JSON 结构讲起给出一套可复用的审计脚本、labelme 转 YOLO 分割格式的完整代码再覆盖 labelme 5.5.0 的安装与标注工作流最后落到 YOLOv8-seg 在小数据集上的训练验证技巧适合正在构造分割数据集或准备训练自定义病害模型的工程人员。2. 解剖 labelme JSON多边形坐标、flags 与类别映射的一致性审计2.1 labelme 的 JSON 并非“框”而是一组可变长坐标点集labelme 的标注文件是一个典型的嵌套 JSON。拿到这个苹果叶子数据集时随便打开一个firc_apple_196.json核心字段就那么几个version记录工具版本flags通常是空对象shapes是标注数组每个元素包含label类别名、points多边形顶点坐标、shape_type此处全部是polygon以及group_id同一实例的关联标记。文件根部还有imagePath对应图片文件名、imageDatabase64 编码的图片数据、imageHeight和imageWidth。值得注意的一个细节是这个数据集不包含预生成的 mask 文件shapes里只有多边形顶点坐标。这意味着你不能直接把数据集扔给训练脚本必须自己把points解析成掩码或转换成 YOLO 分割格式的归一化坐标。顶点坐标是绝对像素坐标单位是像素范围受限于imageWidth和imageHeight。标注工具版本是 labelme 5.5.0shape_type 是 polygon画的是叶片轮廓或病斑轮廓不是矩形框。矩形框在语义分割场景里信息量太少叶片边缘的病斑形状不规则用多边形才能描述边界。标注数量上可以看到一个有意思的点三类标注的框数分别为 Rust101、Scab100、Healthy98合计 299。而图片只有 295 张说明存在同一张图上标了多个实例的情况计算类别样本量时不能简单按图片数去匹配。做数据划分时如果直接按 8:2 切分图片会破坏同一张图内多个标注的相关性这一点在第 3 章的划分逻辑里会处理。2.2 用一段审计脚本验证类别与框数的可信度拿到别人整理的数据集第一步不是训练而是审计 JSON 的完整性否则后续所有指标都不可信。下面这段 Python 脚本会遍历数据集目录检查每个 JSON 是否能正常解析、图片文件是否存在、shapes是否为空并统计类别分布、多边形顶点数的上下限以及是否存在shape_type不一致的情况。import json import os from collections import Counter, defaultdict dataset_dir firc_apple_dataset json_files [f for f in os.listdir(dataset_dir) if f.endswith(.json)] print(fJSON 文件数量: {len(json_files)}) label_counter Counter() points_stats defaultdict(list) shape_type_set set() missing_images [] for jf in sorted(json_files): json_path os.path.join(dataset_dir, jf) with open(json_path, r, encodingutf-8) as f: data json.load(f) img_path os.path.join(dataset_dir, data.get(imagePath, )) if not os.path.exists(img_path): missing_images.append(jf) for shape in data.get(shapes, []): label shape.get(label, unknown) pts shape.get(points, []) label_counter[label] 1 points_stats[label].append(len(pts)) shape_type_set.add(shape.get(shape_type, )) print(类别统计:, dict(label_counter)) for label, lengths in points_stats.items(): print(f{label}: 多边形数量{len(lengths)}, 顶点数范围{min(lengths)}-{max(lengths)}) print(shape_type 种类:, shape_type_set) print(缺失图片的 JSON:, missing_images if missing_images else 无)脚本逻辑说明先按.json后缀收集文件对每个文件用json.load解析取出imagePath后在相同目录下检查图片是否存在防止标注文件里引用的图片被误删或路径写错。随后遍历shapes数组按label分组统计多边形数量并记录每个类别的顶点数范围。shape_type收集到集合里可以快速发现混入了rectangle或circle类型。这个数据集的三类数量是 101、100、98加起来 299如果你跑出来的数字对不上说明标注文件有缺漏或某张图的shapes为空。参数说明dataset_dir按实际解压路径修改这段脚本只读不改不会破坏原始标注。当前数据集的imagePath中图片名和 JSON 名同名例如firc_apple_196.jpg对应firc_apple_196.json但通用场景下不一定同名脚本里用 JSON 里的imagePath字段不用文件名强行替换更稳妥。3. 从 labelme 到 YOLO 分割格式归一化顶点与划分干净的数据集3.1 YOLO 分割标签的规则与多边形取值边界YOLO 系列的分割任务如 YOLOv8-seg使用一种比 COCO-RLE 更紧凑的标签格式每个 txt 文件对应一张图片文件名与图片名完全一致例如firc_apple_196.txt。txt 的每一行代表一个目标实例格式是class_id x1 y1 x2 y2 ... xn yn坐标全部是归一化到 0 到 1 之间的浮点数。归一化的计算方式很简单x_norm x_pixel / imageWidthy_norm y_pixel / imageHeight。有两个边界情况需要专门处理。第一当多边形的某顶点落在图像边缘时归一化值会等于 1.0YOLO 训练时对边界值的容忍度比 COCO 低一般建议钳制在0.0001到0.9999之间防止个别端点在 resize 或增强时越界。第二labelme 标注时允许画逆时针或顺时针多边形YOLO 格式不要求特定方向但要求顶点按顺序相连后不自交。如果某些图形是凹多边形或包含孔洞一个 polygon 无法表达需要拆成多个 polygon 或用 group_id 关联当前数据集没有这类复杂情况。类别映射也要提前定死。这个数据集里的Rust、Scab、Healthy三个类别名转换时映射为0, 1, 2写入data.yaml的names列表。顺序必须固定否则训练和推理时类别名称对不上。3.2 转换脚本JSON 转 YOLO 分割格式并切分数据集下面的脚本把整个数据集目录转为 YOLO 分割格式同时按--val-size比例切分训练集和验证集并生成data.yaml。同样图片和同名 JSON 放在同一个目录中转换后输出到yolo_dataset/。import json import os import shutil import random import argparse def convert_labelme_to_yolo(json_path, out_txt_path, category_map): with open(json_path, r, encodingutf-8) as f: data json.load(f) img_w int(data.get(imageWidth, 0)) img_h int(data.get(imageHeight, 0)) if img_w 0 or img_h 0: print(f跳过 {json_path}: 图片尺寸无效) return False lines [] for shape in data.get(shapes, []): label shape.get(label, unknown) if label not in category_map: print(f警告: 未知类别 {label} 在 {json_path}) continue category_id category_map[label] points shape.get(points, []) if len(points) 3: continue coords [] for x, y in points: x_norm min(max(x / img_w, 0.0001), 0.9999) y_norm min(max(y / img_h, 0.0001), 0.9999) coords.append(f{x_norm:.6f}) coords.append(f{y_norm:.6f}) lines.append(f{category_id} .join(coords)) os.makedirs(os.path.dirname(out_txt_path), exist_okTrue) with open(out_txt_path, w, encodingutf-8) as f: f.write(\n.join(lines)) return True def main(): parser argparse.ArgumentParser(descriptionlabelme JSON 转 YOLO 分割格式) parser.add_argument(--json-dir, requiredTrue, help存放 json 和 jpg 的目录) parser.add_argument(--out-dir, defaultyolo_dataset, help输出根目录) parser.add_argument(--val-size, typefloat, default0.2, help验证集比例) parser.add_argument(--seed, typeint, default42, help随机种子) args parser.parse_args() category_map {Rust: 0, Scab: 1, Healthy: 2} json_files [f for f in os.listdir(args.json_dir) if f.endswith(.json)] random.seed(args.seed) random.shuffle(json_files) val_count int(len(json_files) * args.val_size) val_files set(json_files[:val_count]) # 按图片级切分 for split in [train, val]: os.makedirs(os.path.join(args.out_dir, images, split), exist_okTrue) os.makedirs(os.path.join(args.out_dir, labels, split), exist_okTrue) for jf in json_files: base os.path.splitext(jf)[0] img_file base .jpg split val if jf in val_files else train src_img os.path.join(args.json_dir, img_file) dst_img os.path.join(args.out_dir, images, split, img_file) shutil.copy2(src_img, dst_img) src_json os.path.join(args.json_dir, jf) dst_txt os.path.join(args.out_dir, labels, split, base .txt) convert_labelme_to_yolo(src_json, dst_txt, category_map) train_count len(json_files) - val_count with open(os.path.join(args.out_dir, data.yaml), w, encodingutf-8) as f: f.write(fpath: {os.path.abspath(args.out_dir)}\n) f.write(train: images/train\n) f.write(val: images/val\n) f.write(names:\n) for idx, name in category_map.items(): f.write(f {idx}: {name}\n) print(f完成: train{train_count}, val{val_count}) if __name__ __main__: main()运行方式python labelme2yolo_seg.py --json-dir firc_apple_dataset --out-dir yolo_dataset --val-size 0.2 --seed 42转换逻辑说明脚本先解析imageWidth和imageHeight这是归一化的分母所以这两个字段缺失时必须跳过。每个shapes元素是一个实例points是二维嵌套列表形如[[x1, y1], [x2, y2], ...]。对每个顶点做除法后格式化为 6 位小数避免浮点数过长导致训练时 IO 变慢。最后按val-size比例做图片级随机切分直接把 jpg 和同名 txt 复制到images/train、labels/train两个目录。参数说明--seed用来固定随机划分让多次转换结果一致方便追踪模型效果。--val-size建议保持 0.2 或 0.3因为总共只有 295 张图给 60 张左右做验证比较合理。category_map里用字典记录类别到数字 ID 的映射新数据集按实际类别改这一处即可。3.3 转换后的抽查与常见问题转换完不要急着训练先用一个快速可视化脚本把归一化坐标还原到原图上确认多边形是贴合叶片边界的。核心做法是遍历labels/val里的 txt 文件把每行坐标乘回图片宽高然后用cv2.polylines画多边形轮廓叠加类别名称打印到图上。常见问题有几个。第一points里出现少于 3 个顶点的情况脚本里直接continue跳过但这说明原标注有多边形未闭合或误操作第二某张 JSON 的imageWidth或imageHeight为 0通常是 labelme 的 JSON 在编辑中损坏需要回到 labelme 里重新保存一次第三val集合里某个类别的样本数恰好为 0例如 Scab 类别的病斑全部集中在训练集验证时 mAP 曲线会失真此时应回看第 2 章的审计统计手动把包含特定类别的图片补充到 val 集合。4. labelme 5.5.0 安装与标注工作流从打开图片到产出干净 JSON4.1 环境安装与版本锁定labelme 的版本直接影响 JSON 的字段行为。5.5.0 版本在保存时会自动把图片以 base64 编码写到imageData字段这个字段会让 JSON 体积膨胀好几倍。后续做版本升级时保存的 JSON 结构可能改变例如flags的默认值或group_id的写入方式所以要么固定labelme5.5.0要么统一升级后重新导出混用版本会导致转 YOLO 时检查逻辑不统一。安装推荐用 conda 建独立环境Python 版本选 3.8 到 3.10 之间conda create -n labelme python3.9 -y conda activate labelme pip install labelme5.5.0 python -m labelme命令行启动后打开数据集某张图片左侧工具栏里点 Create Polygons沿叶片病斑边界逐点点击闭合后弹出类别选择框。5.5.0 支持从本地文件读取类别预定义列表先准备一个labels.txtRust Scab Healthy __ignore__ _background_启动 labelme 时指定类别文件python -m labelme --labels labels.txt --nodata。--nodata的作用是不把图片 base64 写入 JSON减少体积如果后续要使用 labelme 自带的labelme_json_to_dataset生成 mask需要取消这个参数。4.2 快捷键操作与差异对比标注效率集中体现在快捷键上。CtrlN新建文件CtrlS保存当前 JSONCtrlZ撤销上一节点CtrlShiftZ重做。绘制多边形过程中按Enter闭合多边形按Esc取消当前绘制。针对叶片这种边缘不规则的物体放大到 200% 标注比直接缩放着画更准快捷键是Ctrl加号和Ctrl减号。修改已画好的多边形也不难工具栏选择 Edit Polygons拖动顶点微调按住Ctrl点击边线可以插入新顶点。遇到病斑和健康区域边界模糊的图把缩放再提高一档宁可多花时间画细一点也不要到处用直线段拟合弧线否则 YOLO 训练时多边形顶点间距差异过大归一化后小目标区域容易失真。4.3 用这个数据集检查自己的标注质量拿到现成的 295 张数据后一个很好的质检方式是随机抽 10 张打开 labelme逐个检查多边形的闭合性和类别标签是否正确。重点看三类目标被叶子边缘截断的 Rust 病斑、和阴影混淆的 Scab、健康叶片误标成病斑。如果发现某个 JSON 里shape_type不是polygon而是rectangle一定要在 labelme 里打开重新标注。这个数据集本身是干净的多边形标注转换脚本能直接跑通。但对你自己标注的图注意一个常见坑imageData字段留空时JSON 里的imagePath必须正确指向同目录图片labelme 5.5.0 在--nodata模式下仍然会把imagePath写成原始文件名所以不要重命名图片而不改 JSON否则训练脚本读取时图片缺失。5. 用 YOLOv8-seg 跑通这个小数据集的训练与指标验证最后一步是把转换后的 YOLO 格式直接喂给 YOLOv8-seg。安装 ultralytics 后一条命令即可启动训练pip install ultralytics yolo segment train datayolo_dataset/data.yaml modelyolov8n-seg.pt epochs200 imgsz960 batch8 patience20参数说明modelyolov8n-seg.pt是 COCO 预训练权重背后是语义分割骨干网络直接用比随机初始化收敛快得多imgsz960对叶片细长病斑来说比默认 640 更合适叶子边缘需要更高分辨率才能保留小面积纹理patience20开启早停连续 20 个 epoch 验证集 mAP 不提升就终止batch8在 295 张图的规模下足够稳定。训练完成后在验证集上跑yolo segment val modelruns/segment/train/weights/best.pt datayolo_dataset/data.yaml观察 mask mAP50 和 mAP50-95 两个指标。小数据集 200 轮内大概率过拟合当 mAP50 涨、mAP50-95 不涨时需要退回 imgsz640 做对比或开启更强的数据增强参数。295 张的规模下先把这三类在验证集上的 per-class mAP 列出来哪类比均值低 5 个点以上回到 labelme 检查那一类的标注边界是否统一是这个场景里性价比最高的调优手段。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价