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

资讯详情

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

苹果叶片病害分割数据集:labelme转YOLO实战与质量校验

苹果叶片病害分割数据集:labelme转YOLO实战与质量校验 简介苹果叶子病害分割数据集以LabelMe格式提供295张苹果叶片原图及对应JSON标注文件面向计算机视觉入门与农业病害识别场景适合用于语义分割或实例分割模型训练前的数据准备与算法验证。包体共590个文件其中295个jpg图像与295个json标注一一对应标注类别涵盖Rust、Scab、Healthy三类框数分别为101、100、98分布均衡合计299个多边形标注框。压缩包采用7z格式整体约13.54MB便于下载与分发。当前已有281人学习/下载。数据集使用LabelMe 5.5.0进行多边形标注用户可直接在LabelMe中打开编辑也可根据自身需求转换生成mask、YOLO或COCO格式适配不同框架。压缩包内未附mask文件需要使用时自行转换既保证原始标注的纯净也便于按需定制。标注规则明确、类别分布均衡能为研究者提供干净规范的真实苹果叶片病害样本适合快速开展对比实验与教学实践。1. 苹果叶子病害分割数据集295张图能撑起一个小型项目吗打开压缩包之前先给结论295张、3类别的labelme格式分割数据集正好卡在“能跑通全流程”和“需要扩充”的临界点上。它不值得用来做最终训练集但非常适合你要验证“数据怎么进模型、标注怎么修、训练怎么收敛”的完整链路。实际项目里我见过不少团队拿这类小数据集先跑通labelme标注、json转yolo、训练、验证的流程等评估指标符合预期了再回头扩充数据。这比一上来就在几千张图上做标注要稳妥得多。这篇文就按“拆开数据集、看懂labelme格式、转成yolo分割能直接训练、检查标注质量、用脚本做一致性校验”这条路径走一遍每一步都会给能直接执行的代码和命令。2. 解析labelme格式数据集从目录结构看到JSON坐标体系2.1 解压之后先看清目录图片、JSON、类别顺序苹果叶子病害分割数据集以.7z格式分发解压后典型结构是图片与标注JSON混排在一个目录也可能拆成images和annotations两个子目录。不管哪种你第一步要做的是把图片和JSON的对应关系理清楚。用一条命令就能看出全貌7z x apple_leaf_disease_seg.7z -o./apple_leaf_dataset cd apple_leaf_dataset ls -la | head -20 find . -name *.json | wc -l find . -name *.jpg -o -name *.png | wc -l逻辑说明7z x解压到指定目录-o参数后直接跟目标路径注意-o与路径之间没有空格。后面两条find命令分别统计JSON数量和图片数量。如果两个数字都是295说明每张图都有一份对应标注没有缺失。参数说明常见图片后缀还有.jpeg、.bmp如果一条find找不到就多写两个-o条件用括号包起来。缺失一个JSON就意味着这张图没法参与训练后面转换时会因为找不到文件而中断。2.2 labelme的JSON字段到底存了什么labelme格式本质是SVG多边形标注的JSON序列化核心字段就几个标注对象列表shapes、图片路径imagePath、图片尺寸imageWidth和imageHeight。每个shapes里又包含多边形顶点的像素坐标points、类别名label、形状类型shape_type。一张苹果叶病害图的分割标注典型内容是{ version: 5.3.1, flags: {}, shapes: [ { label: apple_scab, points: [[132.5, 241.0], [148.0, 235.5], [171.0, 251.0], [165.5, 270.0], [141.5, 269.5]], group_id: null, shape_type: polygon, flags: {} }, { label: leaf_rust, points: [[302.0, 118.5], [321.5, 107.0], [345.0, 112.5], [341.5, 132.0], [316.0, 137.0]], group_id: 1, shape_type: polygon, flags: {} } ], imagePath: leaf_001.jpg, imageData: null, imageHeight: 600, imageWidth: 800 }字段含义对照表如下字段取值类型作用versionstringlabelme版本号5.x的JSON结构一致可忽略shapesarray所有标注对象一张图可能有多个病斑labelstring类别名必须与训练配置里的类别严格一致pointsarray多边形顶点每个元素是[x, y]像素坐标shape_typestringpolygon表示多边形分割标注group_idint/null同一实例的编号类别分割时通常用不到imagePathstring对应图片文件名注意可能是相对路径imageWidth/Heightint标注时图片的宽高检查坐标是否越界的关键这里最容易被忽略的是points不是归一化坐标而是原始像素坐标。这意味着一旦你缩放图片坐标就必须同步换算否则标注会全部偏移。2.3 用Python脚本批量读取JSON统计类别和多边形数量拿到数据集以后不建议人工逐个看JSON写个小脚本就能把三个类别的数量、每张图的平均标注个数统计出来import json from pathlib import Path from collections import Counter json_dir Path(./apple_leaf_dataset) label_counter Counter() polygon_counter [] for jf in json_dir.glob(*.json): with open(jf, encodingutf-8) as f: data json.load(f) for shape in data[shapes]: label_counter[shape[label]] 1 polygon_counter.append(len(data[shapes])) print(类别分布) for label, count in label_counter.most_common(): print(f{label}: {count} 个标注) print(f平均每张图标注数量: {sum(polygon_counter)/len(polygon_counter):.2f})逻辑说明遍历目录下所有json文件外层循环读取整个标注文件内层循环遍历shapes数组每次匹配到一个label就加一。polygon_counter列表记录每张图的标注数量最后取平均值。参数说明labels的命名完全取决于数据集制作者的标注习惯。常见命名有apple_scab、leaf_rust、healthy对应的黑星病、锈病、健康叶片。如果你拿到的数据集类别名不统一比如有的叫scab有的叫Apple Scab要先把名称归一化否则后面类别映射表会出错。3. 将labelme格式转换为YOLO分割格式喂给yolov8训练3.1 为什么要从labelme格式转成YOLO格式labelme格式是给人看的JSON里存的是像素坐标的多边形点。但yolov8训练实例分割模型时读取的标注文件是归一化坐标的txt文件一行一个目标格式是类别编号每个多边形顶点的x和y。这两者不能混用Ultralytics仓库加载数据集直接用YAML配置不会去读labelme的JSON。所以转换脚本是必经之路。3.2 类别映射与坐标归一化转YOLO txt的核心公式YOLO分割格式的核心公式很简单x归一化 像素x坐标 / 图片宽度y归一化 像素y坐标 / 图片高度。类别那一栏用数字编号替代字符串类别名。写转换脚本前先把数据集里实际的类别名提取出来手动建立映射category_map { apple_scab: 0, leaf_rust: 1, healthy: 2 }映射表里0、1、2的顺序就是后续训练时的类别顺序data.yaml里的names列表必须和它保持一致。顺序问题导致模型输出错位是实例分割任务里最高频的翻车点。3.3 用Python脚本实现labelme转yolov8分割格式import json import cv2 import numpy as np from pathlib import Path labelme_dir Path(./apple_leaf_dataset) yolo_dir Path(./yolo_dataset) yolo_images_dir yolo_dir / images yolo_labels_dir yolo_dir / labels yolo_images_dir.mkdir(parentsTrue, exist_okTrue) yolo_labels_dir.mkdir(parentsTrue, exist_okTrue) category_map {apple_scab: 0, leaf_rust: 1, healthy: 2} for json_path in labelme_dir.glob(*.json): with open(json_path, encodingutf-8) as f: data json.load(f) img_path labelme_dir / data[imagePath] img cv2.imread(str(img_path)) if img is None: print(f图片读取失败: {img_path}) continue h, w img.shape[:2] txt_lines [] for shape in data[shapes]: label shape[label] if label not in category_map: print(f未知类别: {label} 文件: {json_path.name}) continue category_id category_map[label] points np.array(shape[points], dtypenp.float32) points[:, 0] / w points[:, 1] / h points points.flatten() points_str .join(f{p:.6f} for p in points) txt_lines.append(f{category_id} {points_str}) txt_path yolo_labels_dir / f{json_path.stem}.txt with open(txt_path, w, encodingutf-8) as f: f.write(\n.join(txt_lines)) img_out_path yolo_images_dir / data[imagePath] cv2.imwrite(str(img_out_path), img) print(转换完成)逻辑说明先读JSON拿图片文件名和尺寸再遍历shapes数组把每个多边形的坐标除以宽高得到0-1之间的归一化值。所有点拉平成一维数组后用空格拼接成一行文本。图片直接复制到新目录目录结构是yolo标准格式。参数说明中有三个坑值得专门说第一cv2.imread读不了含中文路径的文件如果解压目录带中文名必须先用os.chdir切到纯英文路径或把数据挪走。第二points里如果混入了矩形或圆形标注shape_type不是polygon脚本会直接当成多边行处理导致点顺序错乱。转换前要过滤掉非polygon的shape。第三浮点数保留6位精度已经足够再多只会让文件变大不会提升拟合精度。3.4 生成data.yaml并划分训练集与验证集yolov8训练需要三个东西训练图片、对应的txt标注、一个描述类别和路径的data.yaml。已有295张图通常按8:2划分训练集和验证集。用脚本完成划分cd yolo_dataset mkdir -p train/images train/labels val/images val/labels python3 - EOF from pathlib import Path import random import shutil random.seed(42) image_files list(Path(images).glob(*.jpg)) random.shuffle(image_files) split_idx int(len(image_files) * 0.8) train_files image_files[:split_idx] val_files image_files[split_idx:] for split, files in [(train, train_files), (val, val_files)]: for img_path in files: label_path Path(labels) / f{img_path.stem}.txt if not label_path.exists(): print(f标注缺失: {label_path}) continue shutil.copy(img_path, Path(split) / images / img_path.name) shutil.copy(label_path, Path(split) / labels / label_path.name) print(f训练集: {len(train_files)} 张, 验证集: {len(val_files)} 张) EOF逻辑说明脚本先拿到所有图片固定随机种子确保每次运行划分结果一致。80%进train目录20%进val目录。复制时同步检查同名txt标签是否存在防止有图无标的情况流进训练集。参数说明train和val的划分比例可以根据数据总量调整。295张图的话8:2是常见做法因为验证集太小会导致评估指标方差大。如果你后续要加数据增强可以提高到9:1验证集只保留30张左右反正这类小数据集的验证更多看loss趋势而不是绝对精度。4. 数据集质量检查坐标越界、多类别重叠与漏标问题4.1 可视化验证用labelme命令直接看标注效果转换完后直接开训是大忌。先用labelme自带的可视化命令抽查几张图确认多边形贴合病斑轮廓labelme --labels apple_scab,leaf_rust,healthy --nodata yolo_dataset/val/images/leaf_037.jpg逻辑说明--labels参数把类别列表传给标注界面--nodata表示不加载JSON中的base64图像数据而是直接读图片文件。这样可以快速浏览原始标注。如果看到多边形明显偏移叶片边缘或框住了无关背景说明坐标换算或标注本身有问题。参数说明这条命令要求labelme已安装且当前环境能弹出图形界面。如果你在无桌面的服务器上跑训练可以skip这一步改用纯脚本做数值校验。4.2 坐标越界与面积过小标注的批量排查分割数据集最隐蔽的问题不在类别而在坐标质量。多边形边缘超出图像边界、一个病斑被标成几个重叠的多边形、某些标注只有几个像素这些问题肉眼难查但模型一训一个偏。写个脚本批量排查import json from pathlib import Path labelme_dir Path(./apple_leaf_dataset) issues [] for jf in labelme_dir.glob(*.json): with open(jf, encodingutf-8) as f: data json.load(f) img_w data.get(imageWidth, 0) img_h data.get(imageHeight, 0) for idx, shape in enumerate(data[shapes]): pts shape[points] xs [p[0] for p in pts] ys [p[1] for p in pts] if min(xs) 0 or max(xs) img_w or min(ys) 0 or max(ys) img_h: issues.append(f{jf.name} shape[{idx}] 坐标越界: bbox[{min(xs)}, {min(ys)}, {max(xs)}, {max(ys)}]) polygon_area 0.5 * abs( sum(xs[i] * ys[(i1) % len(ys)] - ys[i] * xs[(i1) % len(xs)] for i in range(len(xs))) ) if polygon_area 10: issues.append(f{jf.name} shape[{idx}] 面积过小: {polygon_area:.2f} px) if issues: for issue in issues: print(issue) else: print(坐标全部在图像范围内未发现面积过小的标注)逻辑说明坐标越界用min/max和imageWidth、imageHeight比对得到。面积计算用的鞋带公式两个数组逐点叉积求和取绝对值的一半结果是像素面积。小于10像素的标注对分割任务基本没有学习价值可能是误标注或噪声。参数说明面积阈值10不是固定值。如果你的图片是4000x3000的高分辨率一个真实病斑动辄几千像素10可以提高到50甚至100来筛。相反如果图像缩到416x41610像素的物体几乎不可见直接删掉反而是优化。4.3 重复标注与类别间重叠检测同类别重复标注会让模型把同一区域学两遍不同类别重叠则会造成梯度方向冲突。处理思路是遍历同图内所有shapes用shapely库做多边形相交面积计算from shapely.geometry import Polygon import json from pathlib import Path labelme_dir Path(./apple_leaf_dataset) duplicates [] overlaps [] for jf in labelme_dir.glob(*.json): with open(jf, encodingutf-8) as f: data json.load(f) shapes data[shapes] for i in range(len(shapes)): for j in range(i1, len(shapes)): poly_i Polygon(shapes[i][points]) poly_j Polygon(shapes[j][points]) if not poly_i.is_valid or not poly_j.is_valid: continue inter_area poly_i.intersection(poly_j).area if inter_area 0: continue area_i poly_i.area overlap_ratio inter_area / area_i if shapes[i][label] shapes[j][label] and overlap_ratio 0.8: duplicates.append(f{jf.name}: {shapes[i][label]} 重叠 {overlap_ratio:.2%}) elif shapes[i][label] ! shapes[j][label] and overlap_ratio 0.3: overlaps.append(f{jf.name}: {shapes[i][label]} 与 {shapes[j][label]} 重叠 {overlap_ratio:.2%}) print(f疑似重复标注: {len(duplicates)} 处) for d in duplicates[:5]: print(d) print(f疑似类别间重叠: {len(overlaps)} 处) for o in overlaps[:5]: print(o)逻辑说明双层循环逐个比对同一张图内的多边形对相交面积除以较小面积得到重叠比例。同类别重叠超过80%判定为重复标注不同类别重叠超过30%判定为标签冲突。参数说明shapely库如果没装就执行pip install shapely。这套逻辑在叶片病斑这种密集小目标场景里很管用因为病斑边缘常常紧挨着轻微重叠是正常的阈值宁可高不要低否则会把有效的边界信息当噪声清理掉。5. 验证imageWidth与imageHeight改图不换JSON的隐藏地雷做分割数据集最常见的一个隐患是图片被压缩、裁剪或重采样后JSON里的imageWidth和imageHeight字段没有同步更新。这就导致两个问题一是坐标与图像错位转YOLO格式时按错误的宽高归一化标注会整体偏移二是模型训练时不报错因为yolov8只认读取到的实际图像尺寸txt里的归一化坐标本身不检查是否越界于是标注偏差直到上了验证集看mAP才暴露排错成本极高。解决这个问题有一个干净的技巧逐张比对JSON里记录的尺寸和真实图片尺寸不一致就直接重算坐标并回写。用Python的PIL库可以一次性完成from PIL import Image import json from pathlib import Path labelme_dir Path(./apple_leaf_dataset) fixed_count 0 for jf in labelme_dir.glob(*.json): with open(jf, encodingutf-8) as f: data json.load(f) img_path labelme_dir / data[imagePath] with Image.open(img_path) as img: actual_w, actual_h img.size json_w data.get(imageWidth) json_h data.get(imageHeight) if json_w actual_w and json_h actual_h: continue scale_x actual_w / json_w scale_y actual_h / json_h for shape in data[shapes]: shape[points] [[x * scale_x, y * scale_y] for x, y in shape[points]] data[imageWidth] actual_w data[imageHeight] actual_h with open(jf, w, encodingutf-8) as f: json.dump(data, f, ensure_asciiFalse, indent2) fixed_count 1 print(f修正: {jf.name} 标注尺寸 {json_w}x{json_h} - {actual_w}x{actual_h}) print(f共修正 {fixed_count} 个文件的坐标)逻辑说明PIL库用Image.open读取真实宽高比例因子分别作用于x和y坐标。横纵比例可能不同所以必须分开算不能用一个统一scale。回写JSON时用json.dump保持结构一致缩进参数不影响后续转换脚本读取。参数说明这个脚本假设图片缩放是线性变换坐标直接乘比例就能对齐。如果图片做过裁剪或填充乘比例就不对了那种情况需要回到labelme里重新标注。跑完后重新执行3.3节的转换脚本生成新的YOLO格式标注再开始训练。后面再遇到类似的分割数据集解压后第一件事就是跑一遍这条校验它能筛掉相当一部分劣质数据。某个标注文件坐标跑偏训练出来的结果会毫无征兆地差一截你甚至不会怀疑到数据集头上。本文还有配套的精品资源点击获取
返回列表