尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

labelme标注转换与YOLOv8-seg训练:苹果叶病害分割实战

labelme标注转换与YOLOv8-seg训练:苹果叶病害分割实战 简介苹果叶片病害分割数据集以LabelMe格式提供包含295张苹果叶片jpg图片与295个对应的json标注文件覆盖Rust、Scab、Healthy三个类别适合用于植物病害语义分割、实例分割模型的训练与算法验证。所有标注均采用多边形框形式类别框数分别为一百零一个、一百个、九十八个标注工具为LabelMe 5.5.0用户可直接打开检查或二次编辑。资源包共590个文件每个jpg对应一个同名json一一对应便于批量处理与脚本读取主体为图像和标注文件压缩包大小约13.54MB结构简洁、易于下载。目前已有281人学习了解适用于计算机视觉方向的学生、研究者以及农业智能化项目开发者还能为智慧农业病害识别系统提供基础数据可作为课程设计、毕业设计的实验数据支撑。需要特别说明的是标注文件不包含掩膜使用时可自行转换为Mask、YOLO或COCO格式数据集仅保证标注准确合理不对后续训练模型的精度做任何承诺。1. 拿到一份 labelme 格式的苹果叶子病害分割数据集后该做什么295 张图像、3 个类别、labelme 格式这是很多农业视觉项目起步时的真实状态数据量不小但远算不上大标注格式通用但直接没法喂给训练框架。苹果叶子病害分割任务和通用物体分割最大的不同在于目标特征——病斑边缘模糊、颜色与健康组织接近、同一片叶子上可能同时出现多种病害这些特性决定了标注格式的解析方式和模型训练的参数选择都跟标准 COCO 数据集有明显区别。这篇文章围绕这份 labelme 格式数据集把从 JSON 解析、格式转换到模型训练的全流程讲透重点覆盖三类人拿到图却读不懂标注的初学者、需要把 labelme 转成 YOLO 或 COCO 的训练工程师、以及想从 295 张小数据里榨出更高精度的算法研究员。2. labelme 标注 JSON 的结构与批量化解析2.1 一份标注文件里的核心字段labelme 导出的标注文件本质是一个 JSON每一张图像对应一个同名.json文件。打开任意一个标注文件会看到以下结构{ version: 5.3.1, flags: {}, shapes: [ { label: rust, points: [[156.3, 78.5], [170.1, 82.3], [183.6, 95.2], [176.4, 120.1]], group_id: null, shape_type: polygon, flags: {} } ], imagePath: leaf_001.jpg, imageData: null, imageHeight: 1024, imageWidth: 1024 }解析这类文件时shapes数组是绝对核心每个元素对应一个标注对象。label字段存类别名points是多边形顶点坐标的二维数组shape_type标注形态——分割数据集里绝大多数是polygon。需要注意的坑是imageData字段新版 labelme 默认在 GUI 中标注后会嵌入 base64 编码的图像数据如果解压后发现同一目录下没有 jpg 原图就从这里取图像。imagePath字段也有隐藏问题。部分 labelme 版本写入的是绝对路径部分写入的只有文件名。还有极少数标注工具链会把路径分隔符写反在 Windows 上标注的数据拿到 Linux 上训练时路径拼接直接报错。稳妥的做法是在解析阶段只取 basename再与 JSON 同目录拼回完整路径。2.2 批量化读取与归类统计295 张图像手动一个个打开 JSON 显然不现实。遍历目录批量解析是第一步import json import glob import os from collections import Counter def parse_labelme_json(json_path): with open(json_path, r, encodingutf-8) as f: data json.load(f) # 兼容不同版本的 imagePath img_path os.path.join( os.path.dirname(json_path), os.path.basename(data[imagePath]) ) # 如果原图不存在于磁盘从 imageData 中恢复 if not os.path.exists(img_path) and data.get(imageData): import base64 image_bytes base64.b64decode(data[imageData]) with open(img_path, wb) as f: f.write(image_bytes) print(f从 imageData 恢复图像: {img_path}) shapes [] for shape in data[shapes]: if shape[shape_type] ! polygon: print(f警告: {json_path} 中存在非多边形标注: {shape[shape_type]}) continue shapes.append({ label: shape[label], points: shape[points] }) return { image_path: img_path, shapes: shapes, width: data[imageWidth], height: data[imageHeight] } all_annotations [] label_counter Counter() for json_file in sorted(glob.glob(annotations/*.json)): ann parse_labelme_json(json_file) all_annotations.append(ann) for shape in ann[shapes]: label_counter[shape[label]] 1 print(f共解析 {len(all_annotations)} 张图像的标注) print(类别分布:, dict(label_counter))这段代码的关键逻辑在imagePath处理和imageData回退恢复。前者解决跨平台路径兼容问题后者应对解压后原图缺失的情况。类别分布统计输出是下一步格式转换的依据。2.3 类别命名混乱的清洗苹果叶子病害数据集的标注往往来自不同标注人员3 个类别在label字段中常常出现拼写不统一的情况。例如rust与Rust、leaf_rust同时存在或者中英文混用。这一步不做清洗后续转 COCO 和 YOLO 格式时会直接产生类别索引错位。LABEL_NORMALIZE { rust: rust, Rust: rust, leaf_rust: rust, scab: scab, Scab: scab, 黑星病: scab, healthy: healthy, normal: healthy, 健康: healthy } for ann in all_annotations: for shape in ann[shapes]: original shape[label] if original not in LABEL_NORMALIZE: print(f未映射类别: {original}) shape[label] LABEL_NORMALIZE.get(original, original)清洗后建议再跑一次分布统计确认三个类别的实例数。这一步虽然枯燥却是整个数据管线里回报率最高的操作。295 张图上如果有 30 张标注类别名不一致模型训练时等于凭空多出好几个类别分割结果必然混乱。3. 把 labelme JSON 转换为 COCO 与 YOLO 分割格式3.1 为什么不能拿 JSON 直接训练labelme JSON 是标注过程中的中间格式面向的是人工标注和人工审查不是训练框架的原生输入。YOLO 系列分割需要每行一个目标的归一化坐标文件MMDetection 和 Detectron2 需要 COCO 格式的单一 JSON 文件。直接拿 labelme JSON 训练要么需要额外写一层运行时转换逻辑要么遇到框架实现不兼容。所以拿到数据后第一件正事是一次性转换成目标框架需要的格式而不是在训练脚本里做适配。3.2 labelme 多边形坐标转二值 mask分割数据转换最底层的操作是“多边形到 mask”。虽然 labelme JSON 直接记录了顶点坐标但很多训练管线需要的是像素级 mask 图像而非顶点列表。用 OpenCV 的fillPoly完成转换import numpy as np import cv2 def polygon_to_mask(points, img_h, img_w): 将多边形顶点坐标填充为二值 mask points: [[x1, y1], [x2, y2], ...] 浮点坐标 poly np.array(points, dtypenp.int32).reshape(-1, 1, 2) mask np.zeros((img_h, img_w), dtypenp.uint8) cv2.fillPoly(mask, [poly], 1) return mask这里有个隐含的精度问题fillPoly内部会做整数化处理而 labelme 顶点坐标是浮点类型。当多边形尺寸很小——比如病斑的像素直径只有 10 到 20 个像素时坐标取整会造成 1 到 2 个像素的偏移mask 面积和真实面积差距可达 10% 以上。对于苹果叶片上的早期病斑这类小目标建议在 mask 生成后做一次轻微膨胀操作补偿坐标取整损失。3.3 转换为 COCO 分割格式COCO 格式以数据集为单位生成一个 JSON 文件最核心的字段是images、annotations和categories。转换脚本如下import json import os categories [ {id: 0, name: rust}, {id: 1, name: scab}, {id: 2, name: healthy} ] category_map {c[name]: c[id] for c in categories} coco_data { images: [], annotations: [], categories: categories } annotation_id 0 for img_id, ann in enumerate(all_annotations): h, w ann[height], ann[width] coco_data[images].append({ id: img_id, file_name: os.path.basename(ann[image_path]), width: w, height: h }) for shape in ann[shapes]: points shape[points] xs [p[0] for p in points] ys [p[1] for p in points] # COCO segmentation 需要展平的一维坐标数组 flat_seg [coord for p in points for coord in p] # bbox 使用多边形外接矩形 bbox [min(xs), min(ys), max(xs) - min(xs), max(ys) - min(ys)] coco_data[annotations].append({ id: annotation_id, image_id: img_id, category_id: category_map[shape[label]], segmentation: [flat_seg], bbox: bbox, area: cv2.contourArea(np.array(points, dtypenp.float32)), iscrowd: 0 }) annotation_id 1 with open(apple_leaf_coco.json, w, encodingutf-8) as f: json.dump(coco_data, f, ensure_asciiFalse) print(fCOCO 转换完成: {len(coco_data[images])} 张图, {annotation_id} 个实例)COCO 转换有两个高频错误点。第一segmentation必须是展平的一维数组写成嵌套二维数组会导致 Detectron2 读取异常。第二area字段建议用cv2.contourArea计算多边形真实面积不要用 bbox 面积替代——虽然 COCO 官方工具都能读但训练时的 mask 损失计算会依赖真实的像素面积面积偏差会干扰小目标的正负样本分配。3.4 转换为 YOLO segment 格式YOLO 分割格式是每张图一个 txt 文件每行代表一个实例class_id x1 y1 x2 y2 ...所有坐标归一化到 0 到 1。转换方法def labelme_to_yolo_seg(ann, category_map, output_txt_path): h, w ann[height], ann[width] lines [] for shape in ann[shapes]: cls_id category_map[shape[label]] # 归一化坐标 norm_points [] for x, y in shape[points]: norm_points.append(f{x / w:.6f}) norm_points.append(f{y / h:.6f}) lines.append(f{cls_id} .join(norm_points)) with open(output_txt_path, w, encodingutf-8) as f: f.write(\n.join(lines))这一步最常见的坑是图像的imageWidth和imageHeight与实际读入图片尺寸不一致。有些标注工具会把图像缩略显示时输出的宽高写入 JSON导致归一化坐标整体错位。转换前务必用PIL.Image.open或cv2.imread读取真实图像尺寸而不是信任 JSON 里的宽高字段。3.5 三种格式的对比选型格式文件组织主要使用框架优势劣势labelme JSON每图一个 JSON 原图labelme 可视化工具便于人工查看和修正框架不原生支持COCO JSON全数据集一个 JSONDetectron2, MMDetection生态成熟内置评估指标大注解文件加载慢YOLO txt每图一个 txt 原图YOLOv5/v8, ultralytics读取快路径自由缺少面积等信息295 张小数据集场景下我一般转换出 COCO 和 YOLO 两份分别用于研究实验和部署训练。COCO 留给分割算法的消融实验YOLO 格式直接进 YOLOv8-seg。两份数据源的标注内容完全一致只是组织方式不同避免后续重复转换。4. 用 295 张图训苹果叶片害分割模型的参数设定与坑4.1 数据划分策略不能无脑随机切分295 张图的标注数据量训练集、验证集划分比例通常取 8:2 或 9:1。但苹果叶片数据有个特有风险——同一批采集的图像背景高度相似随机划分会造成验证集与训练集图像来自同一棵树的同一角度评估指标虚高。建议按采集来源或文件名前缀分组后划分确保同一来源的图像只出现在一个集合里。import random from collections import defaultdict # 假设文件名格式为: apple_rust_001.jpg, apple_rust_002.jpg source_groups defaultdict(list) for ann in all_annotations: basename os.path.basename(ann[image_path]) source_prefix _.join(basename.split(_)[:2]) # 按来源归组 source_groups[source_prefix].append(ann) # 按组划分 all_groups list(source_groups.values()) random.seed(42) random.shuffle(all_groups) split_idx int(len(all_groups) * 0.8) train_groups all_groups[:split_idx] val_groups all_groups[split_idx:] train_annotations [ann for group in train_groups for ann in group] val_annotations [ann for group in val_groups for ann in group]类别分布检查也必须在划分后重新做一次。苹果叶病斑的类别天然不均衡——锈病往往多于黑星病健康叶片区域更大但实例数更少。如果验证集里某个类别只有几个实例评估出来的 mIoU 方差会很大不足以说明模型好坏。4.2 YOLOv8-seg 训练配置的必调参数用 ultralytics 训练分割模型核心配置如下# apple_leaf.yaml path: ./apple_leaf_dataset train: images/train val: images/val names: 0: rust 1: scab 2: healthy启动训练yolo segment train \ modelyolov8n-seg.pt \ dataapple_leaf.yaml \ epochs200 \ imgsz640 \ batch16 \ patience30 \ projectruns/segment \ nameapple_leaf_yolov8n针对 295 张小数据集的特性几个参数需要特殊说明epochs200配合patience30。小数据集收敛很快通常在 60 到 80 个 epoch 就达到最优后面的迭代在过拟合。patience 设为 30 意味着验证指标连续 30 个 epoch 不上升就停止省时间也防止指标反弹被误判。imgsz640是精度与显存的折中。苹果叶片病斑尺度普遍较小如果叶片上单个病斑的像素直径远小于图像的十分之一考虑提高到 768 或 1024。但 295 张训练图配合高分辨率输入显存要求翻倍且小数据集上高分辨率更容易过拟合到背景纹理上。可以先训一版 640 检查 mAP再决定要不要升分辨率。batch16对显存依赖大但不建议为了迁就显卡把 batch 降到 4 以下。分割模型在小 batch 下 BatchNorm 统计量不稳定mask 预测会出现边界抖动。显存不够时优先降imgsz而非 batch。4.3 数据增强小数据集的胜负手苹果叶子病害分割的增强策略和通用目标检测不同过度增强反而有害。推荐配置yolo segment train \ modelyolov8n-seg.pt \ dataapple_leaf.yaml \ epochs200 \ imgsz640 \ batch16 \ mosaic0.6 \ fliplr0.5 \ scale0.3 \ hsv_h0.02 \ hsv_s0.4 \ hsv_v0.4各参数含义和选值逻辑mosaic0.6mosaic 拼接四张图增强上下文多样性但苹果叶片数据中如果一张图只有一个病斑实例四张拼接后单目标面积进一步缩小。0.6 的启用比例兼顾了多样性和目标尺度保真。hsv_h0.02色相抖动必须设得很小。苹果叶病害的颜色是重要诊断特征锈病的橙黄色和黑星病的墨绿色有明确差异。色相抖动太猛会让模型混淆两类病斑。scale0.3随机缩放幅度。病斑尺度在同一批数据里相对稳定0.3 的幅度已经足够增强尺度鲁棒性再大就会让病斑缩到无法辨认。另外建议在 YOLO 之外用albumentations做一次离线增强将训练集扩到 600 到 900 张。这种方式配合基础训练集收敛更稳定尤其是在原始数据只有 295 张的情况下离线增强可以规避在线增强的不确定性。4.4 DeepLabV3 语义分割备选路线如果任务只关心“病斑位置在哪里”不需要区分单个病斑实例可以走语义分割路线。将 labelme 多边形转成单通道 PNG mask 后用 DeepLabV3 这类模型import torch import torchvision.models.segmentation as segmentation model segmentation.deeplabv3_resnet50( weightssegmentation.DeepLabV3_ResNet50_Weights.COCO_WITH_VOC_LABELS_V1 ) model.classifier[4] torch.nn.Conv2d(256, 3, kernel_size(1, 1))这里把最后的分类头改成 3 类输出。295 张图训练语义分割模型DeepLabV3_ResNet50的参数量偏大有两条缓解路径一是换用 MobileNetV3-Large 作为 backbone二是冻结 backbone 前几层的 BN 参数只微调最后两层的 BatchNorm。农业图像场景和 COCO 预训练分布差异很大全量微调反而容易在收敛初期震荡。4.5 训练监控指标怎么看295 张图训练出的模型不能只盯最终的 mIoU要同时关注三类曲线第一个是train/box_loss和train/seg_loss的下降斜率。如果 loss 在前 20 个 epoch 内下降剧烈但之后完全走平说明模型容量对当前数据量已经饱和继续训是浪费时间。第二个是val/mask_mAP的波动范围。小数据集上 mAP 曲线会比大数据集抖动更明显单看最高点会误判模型性能。正确做法是取早停前最后 10 个 epoch 的均值作为能力基线。第三个是不同类别各自的 IoU而不是只盯平均。294 张图中如果锈病实例占 70%平均 IoU 会被锈病拉高掩盖黑星病分割效果差的问题。训练结束后用验证集逐类打印from ultralytics.utils.metrics import ConfusionMatrix # 验证集推理后用混淆矩阵看待每个类别的表现5. 小数据集分割模型的验证与纠错三板斧5.1 预测结果叠加可视化检查模型训练完成后第一件事不是看指标而是把预测 mask 叠加到原图上人工过目。病斑分割错误模式多种多样——边缘锯齿、小病斑整块漏检、健康区域被误标为病斑——这些很难从 mIoU 数字上看出来。import cv2 import numpy as np from ultralytics import YOLO model YOLO(runs/segment/apple_leaf_yolov8n/weights/best.pt) results model.predict( sourceapple_leaf_dataset/images/val, saveTrue, save_txtFalse, projectruns/predict, namevisual_check )逐张翻看验证集预测结果。重点寻找三类问题小病斑是否被连续漏检、不同类别之间是否出现系统性误判、病斑边缘的 mask 是否稳定贴合真实轮廓。如果发现边缘呈现明显的锯齿状且病斑周长较长例如锈病蔓延到半片叶先看标注本身边缘是否平滑。labelme 标注时如果点数取得太少多边形边缘会严重偏离真实病斑轮廓。5.2 用标注边界 IoU 评估数据质量训练效果不好时先判断是模型问题还是数据问题方法是用轮廓距离度量标注质量from scipy.spatial.distance import directed_hausdorff def boundary_distance(mask_a, mask_b): 计算两个 mask 边界的 Hausdorff 距离 pts_a np.argwhere(mask_a 0) pts_b np.argwhere(mask_b 0) if len(pts_a) 0 or len(pts_b) 0: return None d_ab directed_hausdorff(pts_a, pts_b)[0] d_ba directed_hausdorff(pts_b, pts_a)[0] return max(d_ab, d_ba)如果发现同类别标注间边界距离大于 5 个像素且普遍存在说明标注标准不统一模型会学到“两套答案”。这种情况调模型参数没有意义回去统一标注规范更划算。295 张图重标成本不高比带着脏数据反复试模型强得多。5.3 类别增量标注的优先级判断如果 3 个类别中某一类的分割效果明显差于其他两类且样本数确实少最优策略不是调增强参数而是补标。295 张数据基础上每类补 50 到 100 张新图往往能提升 3 到 5 个点的 mIoU比把训练时间翻倍更有效。优先级排序参考检查项优先级原因小病斑连续漏检最高病斑平均面积小AP 对尺度敏感类别间误判锈病黑星病高颜色特征被增强策略破坏大目标边缘锯齿中标注点数不足模型学不到平滑轮廓背景误报中叶片纹理与病斑混淆可加背景类操作顺序上先做小病斑漏检的补标这类问题对最终 mAP 的影响最大补标成本也最低——只需要在原图的病斑密集区域画多边形。补标后再重新训练一轮观察对应类别的 IoU 是否明显抬升。最后的验证技巧将补标前后两个模型在固定验证集上的 mask 叠加对比如果补标类别的 IoU 提升超过 2 个百分点说明补标方向正确继续扩展样本如果提升微弱问题可能出在标注边界质量而非实例数量此时优先修正已有标注的边界贴合度。本文还有配套的精品资源点击获取
返回列表