ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

苹果叶片病害识别数据集:1700张标注图片的CNN与YOLOv5分类实战

苹果叶片病害识别数据集:1700张标注图片的CNN与YOLOv5分类实战 简介苹果叶片病害图像识别数据集面向计算机视觉入门与农业AI应用开发者用于训练和评估叶片病害分类模型。数据已专业标注共约1700张图片划分为健康、生锈、痂三个类别并预先切分训练集与验证集便于直接开展模型训练与泛化能力评估。压缩包共1733个文件以1730张jpg图像为主体另含1个py可视化脚本、1个json标注文件与1个png说明图整体约354.45MB运行show脚本即可快速浏览样本分布与图像质量。目前已有340人学习下载。资源同时适配CNN分类网络与YOLOv5分类方案配套项目链接可辅助搭建、调参与优化识别系统适合课程设计、毕业设计及农业病害识别研究等场景帮助读者省去数据采集与标注成本快速验证分类思路并迭代模型效果。1. 苹果叶片病害识别数据集1700 张已标注图片能跑出什么结果果园里最怕的不是虫是叶片上那些说不清道不明的斑。健康、生锈、痂这三种状态在肉眼看来有时只差一个色号但防治窗口期就那么几天认错了就是整片果园的损失。这份苹果叶片病害图像识别数据集约 1700 张已标注图片按 3 类分好健康、生锈、痂训练集和验证集各自独立存放还附带一个 show 脚本用来可视化。它解决的不是“有没有数据”的问题而是“拿到就能直接喂给 CNN 或 YOLOv5 分类头”的落地问题。适合做农业视觉的算法工程师、带学生做课设的高校老师以及想验证自己分类 pipeline 是否靠谱的从业者。我拆过不少农业数据集这个规模不算大但类别干净、划分明确拿来当 baseline 训练和调参的起点很合适。2. 数据集结构与标注格式先看清目录再动手2.1 目录布局与文件命名规律拿到压缩包后别急着写 DataLoader先把目录树看清楚。从项目正文给出的文件名来看图片命名是Train_1616.jpg、Train_1699.jpg、Train_948.jpg这种格式前缀Train_标识训练集后面跟数字编号。验证集大概率是Val_或Valid_前缀具体以实际解压为准。类别信息不在文件名里而是通过存放目录或 JSON 文件来区分。常见做法是每个类别一个子文件夹比如dataset/ ├── train/ │ ├── healthy/ │ │ ├── Train_1616.jpg │ │ └── ... │ ├── rust/ │ │ ├── Train_1699.jpg │ │ └── ... │ └── scab/ │ ├── Train_948.jpg │ └── ... └── val/ ├── healthy/ ├── rust/ └── scab/这种结构对torchvision.datasets.ImageFolder和tf.keras.preprocessing.image_dataset_from_directory都是开箱即用。如果实际是 JSON 标注文件里面会记录每张图的类别标签那就需要自己写解析逻辑。先确认这一点后面所有代码都依赖它。2.2 标注文件解析与类别映射摘要里提到“具体查看 json 文件”说明标注信息可能不在目录名里而是集中在一个 JSON 中。我一般会先跑一段脚本把 JSON 结构打印出来确认字段名是label、category还是class_id以及类别是字符串还是数字。import json with open(annotations.json, r, encodingutf-8) as f: ann json.load(f) # 先看顶层结构是列表还是字典 print(type(ann)) if isinstance(ann, list): print(ann[0]) # 打印第一条看字段 elif isinstance(ann, dict): print(list(ann.keys())[:5]) # 打印前几个 key逻辑说明这段代码不涉及任何模型纯粹是摸清标注文件的组织方式。参数上注意encodingutf-8农业数据集的 JSON 里可能有中文类别名不加编码容易在 Windows 上翻车。如果打印出来是{image_id: Train_1616.jpg, label: healthy}这种结构那就可以直接构建image_path - label的映射字典后续自定义 Dataset 类时用。提示如果 JSON 里类别是数字0/1/2务必确认 0 对应的是健康还是病害搞反了训练出来的模型会把健康叶判成病叶这种错误在验证集上不一定暴露但上线就是事故。2.3 训练集与验证集的划分逻辑摘要明确说了“划分了训练集、验证集”这意味着你不需要自己再跑train_test_split。但要注意一点验证集的类别分布是否均衡。约 1700 张图分 3 类平均每类 560 张左右如果验证集里某一类只有几十张评估指标就会抖动得厉害。我一般会先统计一下各类别的图片数量import os from collections import Counter root dataset/train counts Counter() for cls in os.listdir(root): cls_dir os.path.join(root, cls) if os.path.isdir(cls_dir): counts[cls] len([f for f in os.listdir(cls_dir) if f.endswith(.jpg)]) print(counts) # 输出示例Counter({healthy: 580, rust: 540, scab: 496})逻辑说明os.listdir遍历类别文件夹Counter统计每类图片数。如果发现某类明显偏少比如不到 300训练时就要考虑用WeightedRandomSampler做重采样或者在 loss 里加类别权重。参数上endswith(.jpg)是硬编码如果实际有.png或.JPG大写后缀这里会漏统计建议改成f.lower().endswith((.jpg, .jpeg, .png))。3. 用 CNN 跑通分类 baseline从 DataLoader 到第一轮训练3.1 自定义 Dataset 与数据增强策略如果目录结构是train/healthy/、train/rust/这种直接用ImageFolder最省事。但如果标注在 JSON 里就需要自己写 Dataset。下面是一个通用写法兼顾两种场景import os import json from PIL import Image from torch.utils.data import Dataset from torchvision import transforms class AppleLeafDataset(Dataset): def __init__(self, img_dir, ann_fileNone, transformNone): self.img_dir img_dir self.transform transform self.samples [] if ann_file: with open(ann_file, r, encodingutf-8) as f: ann json.load(f) # 假设 ann 是 [{image_id: Train_1616.jpg, label: healthy}, ...] for item in ann: self.samples.append((item[image_id], item[label])) else: # 目录结构方式 for cls in os.listdir(img_dir): cls_dir os.path.join(img_dir, cls) if os.path.isdir(cls_dir): for fname in os.listdir(cls_dir): if fname.lower().endswith((.jpg, .jpeg, .png)): self.samples.append((os.path.join(cls, fname), cls)) # 构建类别到索引的映射 self.classes sorted(set(label for _, label in self.samples)) self.class_to_idx {c: i for i, c in enumerate(self.classes)} def __len__(self): return len(self.samples) def __getitem__(self, idx): path, label self.samples[idx] img Image.open(os.path.join(self.img_dir, path)).convert(RGB) if self.transform: img self.transform(img) return img, self.class_to_idx[label]逻辑说明__init__里分两条路走——有 JSON 就读 JSON没有就扫目录。class_to_idx用sorted保证类别顺序稳定避免每次运行映射关系变化导致模型输出对不上。__getitem__里.convert(RGB)是必须的农业图像里可能有灰度图或 RGBA 图不转 RGB 会在ToTensor时直接报错。数据增强方面苹果叶片病害的判别依赖纹理和颜色我一般会用train_transform transforms.Compose([ transforms.Resize((224, 224)), transforms.RandomHorizontalFlip(p0.5), transforms.RandomVerticalFlip(p0.3), transforms.ColorJitter(brightness0.2, contrast0.2, saturation0.2), transforms.RandomRotation(15), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ])参数说明Resize((224, 224))是 ImageNet 预训练模型的标准输入如果你用 ResNet50 或 EfficientNet保持这个尺寸最省心。RandomVerticalFlip概率设 0.3 而不是 0.5因为叶片在自然场景中上下翻转的概率低于左右翻转过度增强反而引入噪声。ColorJitter的幅度控制在 0.2 左右太大可能把生锈的黄色调成健康绿标签就废了。Normalize用的是 ImageNet 统计量迁移学习场景下必须对齐。3.2 模型选型与训练循环关键参数1700 张图不算多从零训一个 CNN 容易过拟合。常见做法是拿resnet18或efficientnet_b0做迁移学习冻结前几层只训分类头。下面是一个最小训练循环import torch import torch.nn as nn from torch.utils.data import DataLoader from torchvision import models device torch.device(cuda if torch.cuda.is_available() else cpu) # 数据加载 train_ds AppleLeafDataset(dataset/train, transformtrain_transform) val_ds AppleLeafDataset(dataset/val, transformval_transform) train_loader DataLoader(train_ds, batch_size32, shuffleTrue, num_workers4) val_loader DataLoader(val_ds, batch_size32, shuffleFalse, num_workers4) # 模型迁移学习 model models.resnet18(weightsmodels.ResNet18_Weights.IMAGENET1K_V1) model.fc nn.Linear(model.fc.in_features, 3) # 3 类 model model.to(device) # 损失与优化器 criterion nn.CrossEntropyLoss() optimizer torch.optim.AdamW(model.parameters(), lr1e-3, weight_decay1e-4) scheduler torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max20) for epoch in range(20): model.train() for imgs, labels in train_loader: imgs, labels imgs.to(device), labels.to(device) optimizer.zero_grad() loss criterion(model(imgs), labels) loss.backward() optimizer.step() scheduler.step() # 验证阶段省略核心是每轮跑一次 val_loader 看准确率逻辑说明model.fc替换成 3 输出因为数据集只有健康、生锈、痂三类。AdamW的weight_decay1e-4比 SGD 的默认值更稳小数据集上不容易震荡。CosineAnnealingLR的T_max20对应总 epoch 数让学习率从 1e-3 平滑降到接近 0。batch_size32在 1700 张图下大约每轮 50 个 iteration显存占用低适合单卡 8G 以内的环境。注意如果验证集准确率在前 5 轮就冲到 95% 以上然后不动了大概率是数据泄漏——训练集和验证集里有重复图片。用md5对图片做一次去重检查别问我怎么知道的。3.3 用 show 脚本做数据可视化验证摘要里提到“可以运行资源中的 show 脚本”这个脚本的作用通常是随机抽样展示图片和对应标签。我一般会先跑它确认三件事图片能不能正常打开、标签和图片内容是否对得上、有没有明显脏数据比如全黑图、标注框跑偏。如果 show 脚本是 Python 写的大概率长这样import matplotlib.pyplot as plt import random from PIL import Image # 假设 show.py 在数据集根目录 samples random.sample(train_ds.samples, 9) fig, axes plt.subplots(3, 3, figsize(10, 10)) for ax, (path, label) in zip(axes.flat, samples): img Image.open(os.path.join(dataset/train, path)) ax.imshow(img) ax.set_title(label) ax.axis(off) plt.show()逻辑说明随机抽 9 张图排成 3x3 网格标题显示标签。重点看“生锈”和“痂”这两类有没有混淆——如果人眼都分不清模型分错也正常这时候要考虑是不是标注标准本身有歧义。参数上random.sample的种子没固定每次运行结果不同如果想复现某次可视化加random.seed(42)。4. 基于 YOLOv5 的分类任务把检测框架当分类器用4.1 YOLOv5 分类模式与检测模式的区别YOLOv5 官方仓库支持--task classify但很多人不知道这个模式。它和检测模式的核心区别在于分类模式不需要标注框只需要图片和类别标签网络结构上把检测头换成了分类头输出直接是类别概率。对于苹果叶片病害这种“整张图就是一个类别”的场景分类模式比检测模式更合适——你不需要定位病斑位置只需要判断这片叶子是健康还是生病。摘要里给了基于 YOLOv5 的分类链接说明作者已经验证过这条路能走通。我一般会先确认 YOLOv5 版本因为 v6 和 v7 的 classify 接口有差异。常见做法是git clone https://github.com/ultralytics/yolov5 cd yolov5 pip install -r requirements.txt然后把数据集整理成 YOLOv5 分类模式要求的格式dataset/ ├── train/ │ ├── healthy/ │ ├── rust/ │ └── scab/ └── val/ ├── healthy/ ├── rust/ └── scab/注意这里和 CNN 的目录结构完全一致YOLOv5 的classify模式直接复用ImageFolder的逻辑不需要额外的标注文件。4.2 训练命令与超参数配置YOLOv5 分类训练的命令行入口是classify/train.py核心参数如下python classify/train.py \ --model yolov5s-cls.pt \ --data dataset \ --epochs 50 \ --batch-size 32 \ --imgsz 224 \ --lr0 0.001 \ --device 0参数说明--model yolov5s-cls.pt是官方提供的预训练分类权重比从零训快很多。--data指向数据集根目录YOLOv5 会自动识别train/和val/子目录。--imgsz 224和 CNN 保持一致方便对比结果。--lr0 0.001是初始学习率YOLOv5 默认用 SGD这个值比 Adam 的 1e-3 略保守但分类任务上够用。--device 0指定第一块 GPU没有 GPU 就删掉这个参数走 CPU但 1700 张图在 CPU 上训 50 轮大概要几个小时。训练过程中重点看top1_acc和top5_acc3 分类任务 top5 没意义只看 top1。如果top1_acc在 10 轮内没超过 80%检查一下--data路径下是不是少了val目录YOLOv5 找不到验证集时不会报错只会跳过评估。4.3 分类结果解读与混淆矩阵训练完成后YOLOv5 会在runs/train-cls/exp/下生成混淆矩阵和各类别的 precision/recall。我一般会重点看“生锈”和“痂”之间的误判率因为这两类在视觉上最接近。如果混淆矩阵显示生锈被大量判成痂有两种可能一是标注标准里这两类的边界本身模糊二是数据增强时颜色抖动把生锈的黄色调成了痂的褐色。import numpy as np import matplotlib.pyplot as plt from sklearn.metrics import confusion_matrix, ConfusionMatrixDisplay # 假设 y_true 和 y_pred 是验证集上的真实标签和预测标签 cm confusion_matrix(y_true, y_pred, labels[0, 1, 2]) disp ConfusionMatrixDisplay(cm, display_labels[healthy, rust, scab]) disp.plot(cmapBlues) plt.show()逻辑说明confusion_matrix的labels参数必须和class_to_idx的顺序一致否则矩阵行列会对不上。display_labels用可读的类别名方便直接截图放进报告。如果发现某一类的召回率明显低于其他类优先检查该类在训练集中的样本量是否偏少而不是急着调模型结构。5. 避坑与排查标注、划分、训练里的五个血泪教训5.1 类别名大小写不一致导致标签错乱现象训练时 loss 正常下降但验证集准确率始终在 33% 左右相当于随机猜。原因目录里同时存在Healthy和healthy两个文件夹ImageFolder把它们当成两个不同的类实际类别数变成了 4 或 5而模型输出层还是 3。解决统一类别文件夹命名全部用小写。跑一遍os.listdir检查有没有重复类别。5.2 训练集和验证集图片重复现象验证集准确率异常高但换一批新图片测试就崩。原因划分时没有去重同一张图既在训练集又在验证集。解决用hashlib.md5对图片内容做哈希检查两个集合的交集。import hashlib def file_md5(path): with open(path, rb) as f: return hashlib.md5(f.read()).hexdigest() train_hashes {file_md5(os.path.join(dataset/train, p)) for p in train_paths} val_hashes {file_md5(os.path.join(dataset/val, p)) for p in val_paths} print(len(train_hashes val_hashes)) # 应该是 05.3 JSON 标注里的图片路径与实际文件不匹配现象自定义 Dataset 报FileNotFoundError但图片明明在目录里。原因JSON 里存的是相对路径而代码里拼接的根目录不对或者 JSON 里带了./前缀。解决打印第一条image_id和实际os.path.join后的路径肉眼比对。常见做法是用os.path.normpath统一路径分隔符。5.4 数据增强过猛导致标签语义漂移现象训练集准确率上不去模型学不到有效特征。原因ColorJitter的hue参数设得太大把健康叶的绿色调成了黄色模型看到黄色就判生锈。解决ColorJitter只开brightness和contrastsaturation控制在 0.2 以内hue直接不设。5.5 YOLOv5 分类模式找不到验证集现象训练日志里只有训练 loss没有验证准确率。原因--data指向的目录下没有val子目录或者val目录为空。解决确认dataset/val/下每个类别文件夹里至少有一张图。YOLOv5 不会主动报错只会静默跳过验证。6. 进阶技巧用混淆矩阵反推标注质量与模型边界跑通 baseline 之后真正有价值的是从混淆矩阵里读出数据本身的问题。我一般会把验证集上的预测结果导出成 CSV按置信度排序重点看那些“高置信度但预测错误”的样本。这些样本往往不是模型的问题而是标注本身有歧义。import pandas as pd import torch from torch.nn.functional import softmax model.eval() records [] with torch.no_grad(): for imgs, labels in val_loader: imgs imgs.to(device) logits model(imgs) probs softmax(logits, dim1) preds probs.argmax(dim1) for i in range(len(labels)): records.append({ true: val_ds.classes[labels[i]], pred: val_ds.classes[preds[i]], confidence: probs[i][preds[i]].item(), correct: labels[i].item() preds[i].item() }) df pd.DataFrame(records) # 高置信度错误样本 hard_cases df[(df[correct] False) (df[confidence] 0.9)] print(hard_cases.head(10))逻辑说明softmax把 logits 转成概率confidence是预测类别的概率值。筛选confidence 0.9且correct False的样本这些是模型“非常自信但错了”的案例。我一般会把这些图片单独导出来人工看一遍如果发现标注确实错了就修正标签重新训练如果标注没错但模型就是学不会说明这一类需要更多样本或更强的数据增强。另一个技巧是用sklearn的classification_report看每类的 F1from sklearn.metrics import classification_report print(classification_report(df[true], df[pred]))如果“痂”的 F1 明显低于其他两类而“生锈”的召回率很高说明模型倾向于把痂判成生锈。这时候可以在 loss 里给痂类加权重或者专门对痂类做过采样。我一般会先试WeightedRandomSampler不行再动模型结构。从那以后我每次拿到新数据集都强制先跑一遍混淆矩阵和硬样本分析再决定要不要调模型。数据本身的问题不解决换什么网络都是玄学。希望帮到你。本文还有配套的精品资源点击获取
返回列表