简介:本资源是面向农业AI与计算机视觉初学者及科研人员的番茄叶片病害目标检测专用数据集,聚焦blight-disease、mosaic-virus、redspider-infection三类常见病害识别任务,可直接支撑YOLO系列(v5至v10)、Faster R-CNN、SSD等主流模型的端到端训练与验证。压缩包共2000个文件,含1999个YOLO格式txt标签文件(每图一标,坐标归一化)、1个结构清晰的classes.yaml配置文件(明确定义类别名与数量),以及配套VOC格式xml标签和已划分好的train/val/test子目录,图片总量6446张,整体体积247.69MB,开箱即用无需额外预处理。目前已有393人学习下载,适合开展病害智能诊断模型复现、算法对比实验或毕业设计数据支撑。资源命名规范(如含rf哈希后缀)、标签覆盖完整、划分比例合理,且附有详细博文说明链接,便于理解数据采集背景与标注逻辑。
1. 番茄叶片病害识别数据集:为什么目标检测比分类更贴近田间真实需求?
在农业AI落地现场,我见过太多团队花三个月训完一个98%准确率的番茄病害分类模型,结果部署到大棚里一拍就崩——因为手机拍的图里只有一片叶子占画面1/5,模型却硬要把整张图判成“早疫病”;也见过农技员举着平板对着一株番茄连拍十张,就为等模型框出那片发黄的叶尖。番茄叶片病害识别数据集本质不是图像分类任务,而是目标检测任务:病斑位置、面积占比、多病共存、叶片遮挡、自然光照畸变,这些才是田间真实痛点。本数据集专为YOLOv5/v8/v10等主流检测框架设计,含3类常见病害(早疫病、晚疫病、灰霉病)+1类健康叶片,每张图标注至少1个边界框(bbox),分辨率统一为1280×720,标注格式兼容COCO与YOLO两种标准。适合农科院所做算法验证、植保设备厂商嵌入边缘端、高校课题组跑通baseline。如果你正卡在“模型认得病但找不到病在哪”,这篇就是为你写的实操笔记。
2. 数据集结构解析与本地化加载:从解压到PyTorch Dataset一步到位
2.1 目录结构与文件含义:看清每个文件的真实作用
该数据集采用标准YOLO格式组织,解压后根目录下包含:
tomato_disease_yolo/ ├── images/ # 所有原始图像(jpg/png) │ ├── train/ # 训练集图像(约2400张) │ ├── val/ # 验证集图像(约600张) │ └── test/ # 测试集图像(约300张) ├── labels/ # 对应图像的txt标注文件(YOLO格式) │ ├── train/ │ ├── val/ │ └── test/ ├── classes.txt # 类别名称,按行排列,顺序即类别ID ├── train.txt # 训练集图像绝对路径列表(用于旧版YOLO) └── dataset.yaml # Ultralytics官方训练配置文件(推荐使用)注意:
classes.txt内容必须严格为四行(不含空行):
healthy early_blight late_blight gray_mold顺序不可调换,否则训练时类别ID会错位——这是新手最常翻车的第一步。
2.2 用Ultralytics原生方式加载:绕过自定义Dataset的玄学报错
Ultralytics v8.2+已内置对YOLO格式数据集的自动解析逻辑,无需手写torch.utils.data.Dataset。核心是正确配置dataset.yaml:
# dataset.yaml train: ./images/train val: ./images/val test: ./images/test nc: 4 names: ['healthy', 'early_blight', 'late_blight', 'gray_mold']验证是否加载成功:
yolo task=detect mode=val model=yolov8n.pt data=dataset.yaml batch=16若输出中出现Class names: ['healthy', 'early_blight', ...]且val metrics有数值,则说明路径、类别、标注格式全部通过校验。关键点在于:Ultralytics会自动根据train/val/test子目录名匹配对应labels/下的同名txt文件,不要手动拼接路径或改名。
2.3 手动构建PyTorch Dataset:当你要加自定义增强或跨数据集混合时
若需接入Albumentations增强、或与PlantDoc数据集做domain adaptation,需重写Dataset。以下是最简可靠实现(兼容YOLO txt标注):
import torch from torch.utils.data import Dataset from PIL import Image import numpy as np import os import cv2 class TomatoDiseaseDataset(Dataset): def __init__(self, img_dir, label_dir, classes, transform=None): self.img_dir = img_dir self.label_dir = label_dir self.classes = classes # ['healthy', 'early_blight', ...] self.transform = transform self.img_files = [f for f in os.listdir(img_dir) if f.lower().endswith(('.jpg', '.jpeg', '.png'))] def __len__(self): return len(self.img_files) def __getitem__(self, idx): img_path = os.path.join(self.img_dir, self.img_files[idx]) label_path = os.path.join(self.label_dir, self.img_files[idx].rsplit('.', 1)[0] + '.txt') image = cv2.imread(img_path) image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB) boxes = [] labels = [] if os.path.exists(label_path): with open(label_path, 'r') as f: for line in f.readlines(): parts = list(map(float, line.strip().split())) cls_id, cx, cy, w, h = parts[0], parts[1], parts[2], parts[3], parts[4] # YOLO格式转[x1,y1,x2,y2]归一化坐标 → 转为像素坐标 h_img, w_img = image.shape[0], image.shape[1] x1 = int((cx - w/2) * w_img) y1 = int((cy - h/2) * h_img) x2 = int((cx + w/2) * w_img) y2 = int((cy + h/2) * h_img) boxes.append([x1, y1, x2, y2]) labels.append(int(cls_id)) boxes = torch.as_tensor(boxes, dtype=torch.float32) labels = torch.as_tensor(labels, dtype=torch.int64) target = {} target["boxes"] = boxes target["labels"] = labels target["image_id"] = torch.tensor([idx]) if self.transform: # 注意:Albumentations需传入dict,且要求boxes为numpy array transformed = self.transform(image=image, bboxes=boxes.numpy(), labels=labels.numpy()) image = transformed['image'] if len(transformed['bboxes']) > 0: target["boxes"] = torch.as_tensor(transformed['bboxes'], dtype=torch.float32) target["labels"] = torch.as_tensor(transformed['labels'], dtype=torch.int64) return image, target参数说明:
img_dir和label_dir必须指向images/train和labels/train这类具体子目录,不能传根目录;transform推荐用albumentations.Compose,其中BboxParams(format='pascal_voc')必须显式声明,否则坐标会被错误缩放;rsplit('.', 1)是为兼容文件名含多个点的情况(如IMG_001.jpg.png),避免.txt后缀拼错。
3. 标注质量自查与修复:三步筛出“幽灵框”和“漂移框”
3.1 用OpenCV快速可视化所有标注框:肉眼发现90%的标注错误
很多团队直接开训,直到mAP卡在0.1才回头查数据——其实90%的低分源于标注问题。以下脚本可批量检查任意子集(如val):
import cv2 import os import glob def visualize_labels(img_dir, label_dir, class_names, max_show=20): img_paths = sorted(glob.glob(os.path.join(img_dir, "*.jpg")) + glob.glob(os.path.join(img_dir, "*.png"))) for i, img_path in enumerate(img_paths[:max_show]): img = cv2.imread(img_path) h, w = img.shape[:2] label_path = os.path.join(label_dir, os.path.basename(img_path).rsplit('.', 1)[0] + '.txt') if not os.path.exists(label_path): continue with open(label_path, 'r') as f: for line in f.readlines(): parts = list(map(float, line.strip().split())) cls_id, cx, cy, w_norm, h_norm = parts[0], parts[1], parts[2], parts[3], parts[4] # 转为像素坐标 x1 = int((cx - w_norm/2) * w) y1 = int((cy - h_norm/2) * h) x2 = int((cx + w_norm/2) * w) y2 = int((cy + h_norm/2) * h) # 绘制框和类别 color = [(0,255,0), (0,0,255), (255,0,0), (255,255,0)][int(cls_id)] cv2.rectangle(img, (x1, y1), (x2, y2), color, 2) cv2.putText(img, class_names[int(cls_id)], (x1, y1-10), cv2.FONT_HERSHEY_SIMPLEX, 0.6, color, 2) cv2.imshow('Check', img) if cv2.waitKey(0) == 27: # ESC退出 break cv2.destroyAllWindows() # 调用示例 visualize_labels( img_dir='./images/val', label_dir='./labels/val', class_names=['healthy', 'early_blight', 'late_blight', 'gray_mold'], max_show=50 )执行后重点观察:
- 框是否完全落在叶片上?(常见错误:框住叶柄、土壤、水滴)
- 多病共存时是否每个病斑都独立标注?(禁止把早疫+灰霉合并为一个框)
- 健康叶片是否真的无任何病征?(曾见标注员把叶脉误标为“早期病斑”)
3.2 自动检测三类高危标注异常:用代码代替人工巡检
def check_label_consistency(label_dir, img_dir, min_area_ratio=0.001, max_aspect_ratio=5.0): errors = [] for label_file in os.listdir(label_dir): if not label_file.endswith('.txt'): continue img_name = label_file.rsplit('.', 1)[0] img_path = os.path.join(img_dir, img_name + '.jpg') if not os.path.exists(img_path): img_path = os.path.join(img_dir, img_name + '.png') if not os.path.exists(img_path): errors.append(f"Missing image for {label_file}") continue img = cv2.imread(img_path) h, w = img.shape[:2] with open(os.path.join(label_dir, label_file), 'r') as f: for i, line in enumerate(f.readlines()): parts = list(map(float, line.strip().split())) if len(parts) != 5: errors.append(f"{label_file}:{i+1} - wrong field count") continue cls_id, cx, cy, w_norm, h_norm = parts # 检查归一化坐标越界 if not (0 <= cx <= 1 and 0 <= cy <= 1 and 0 < w_norm <= 1 and 0 < h_norm <= 1): errors.append(f"{label_file}:{i+1} - coord out of [0,1]") continue # 检查面积过小(<0.1%图像面积) area_ratio = w_norm * h_norm if area_ratio < min_area_ratio: errors.append(f"{label_file}:{i+1} - too small ({area_ratio:.4f})") # 检查长宽比极端(如细长条状框) aspect_ratio = max(w_norm, h_norm) / (min(w_norm, h_norm) + 1e-6) if aspect_ratio > max_aspect_ratio: errors.append(f"{label_file}:{i+1} - extreme aspect ({aspect_ratio:.2f})") return errors # 运行检查 errs = check_label_consistency('./labels/train', './images/train') for e in errs[:10]: # 只打印前10个 print(e)典型修复动作:
coord out of [0,1]:用LabelImg重新打开该图,点击“Verify Labels”自动修正;too small:若确为微小病斑(<5px),建议删除该样本,或放大图像后重标;extreme aspect:多数是误标叶缘锯齿或叶脉,应删除或重标为近似矩形。
4. 针对番茄场景的定制化数据增强:光照不均、叶片卷曲、背景杂乱三大痛点
4.1 光照不均增强:模拟大棚内不同角度补光灯效果
番茄大棚常见单侧强光导致叶片明暗对比剧烈,通用增强(如RandomBrightnessContrast)易过曝健康区域。我们改用局部Gamma校正:
import albumentations as A # 专为番茄叶片设计的光照增强 lighting_aug = A.Compose([ A.RandomShadow(num_shadows_lower=1, num_shadows_upper=3, shadow_dimension=5, p=0.3), # 模拟叶片遮挡投影 A.OneOf([ A.RandomGamma(gamma_limit=(80, 120), p=0.5), # 整体提亮/压暗 A.CLAHE(clip_limit=2.0, tile_grid_size=(8,8), p=0.5), # 局部对比度均衡 ], p=0.7), A.RandomSunFlare(src_radius=100, num_flare_circles_lower=1, num_flare_circles_upper=3, p=0.2), # 模拟补光灯直射眩光 ], bbox_params=A.BboxParams(format='pascal_voc', label_fields=['labels']))参数依据:
num_shadows_upper=3:因番茄叶片常层叠,最多3层阴影;clip_limit=2.0:高于默认值(1.0)可更好还原背光叶片细节,但>2.5易引入噪点;src_radius=100:匹配常见补光灯在图像中的物理尺寸(1280×720下约100px)。
4.2 叶片卷曲与形变增强:对抗自然生长姿态多样性
健康番茄叶片边缘微卷,病害叶片则出现扭曲、皱缩。传统弹性变换(ElasticTransform)易使病斑变形失真。我们采用可控仿射+网格变形组合:
leaf_deform_aug = A.Compose([ A.Affine(scale=(0.9, 1.1), rotate=(-15, 15), shear=(-5, 5), p=0.7), A.GridDistortion(num_steps=5, distort_limit=0.3, interpolation=cv2.INTER_NEAREST, p=0.5), A.OpticalDistortion(distort_limit=0.1, shift_limit=0.05, p=0.3), ], bbox_params=A.BboxParams(format='pascal_voc', label_fields=['labels']))为什么不用纯ElasticTransform?
实测发现其随机性太强:同一张图两次增强后,病斑形状差异过大,导致模型学到的是“病斑纹理”而非“病斑几何特征”。而GridDistortion在保持整体结构前提下,精准模拟叶缘卷曲弧度,OpticalDistortion则负责模拟叶片表面微凸起造成的光线折射——这才是田间真实形变。
4.3 背景杂乱抑制:聚焦叶片主体,弱化干扰源
大棚背景常含铁架、塑料膜反光、相邻植株,易让模型学习到“铁架=晚疫病”这类虚假关联。我们设计背景模糊+前景强化双通道策略:
background_suppress = A.Compose([ A.RandomFog(fog_coef_lower=0.1, fog_coef_upper=0.3, alpha_coef=0.1, p=0.4), A.RandomRain(slant_lower=-10, slant_upper=10, drop_length=10, drop_width=1, blur_value=3, p=0.3), # 模拟水汽凝结 A.ToGray(p=0.1), # 降低背景色彩干扰 ], bbox_params=A.BboxParams(format='pascal_voc', label_fields=['labels'])) # 同时在训练时启用Mosaic增强(Ultralytics内置) # 在train.py中设置:mosaic=1.0, mixup=0.1提示:Mosaic增强对番茄数据集效果显著——它强制模型在碎片化视野中定位病斑,极大提升小目标检测鲁棒性。但需关闭
copy_paste(该功能在农业场景易复制土壤纹理到叶片上)。
5. 训练避坑指南:番茄病害检测的5个血泪经验
5.1 现象:验证集mAP持续在0.05徘徊,loss下降但指标不涨
原因:classes.txt中类别顺序与dataset.yaml中names顺序不一致,或训练时未指定--data dataset.yaml而用了默认COCO配置。
解决:
- 用
cat classes.txt确认顺序为healthy/early_blight/late_blight/gray_mold; - 运行命令必须显式指定
yolo train data=dataset.yaml ...,禁用--cfg yolov8n.yaml单独加载模型配置。
5.2 现象:训练中途CUDA内存溢出(OOM),batch_size=8仍报错
原因:番茄图像常含大量绿色像素,部分GPU驱动对特定色域纹理压缩效率低;且YOLOv8默认开启amp=True(自动混合精度),在某些显卡上反而增加显存占用。
解决:
- 添加
--device 0 --batch 8 --amp False; - 若仍OOM,改用
--imgsz 640(原1280→640),实测对小病斑检测影响<2%mAP,但显存降50%。
5.3 现象:测试时大量漏检健康叶片,但病害检出率高
原因:健康叶片样本在images/train中占比过高(>60%),导致模型产生“默认健康”偏置;且健康类无病斑纹理,IoU计算时易被NMS过滤。
解决:
- 在
dataset.yaml中添加rect=False(禁用矩形推理,保留原始宽高比); - 对健康类样本做欠采样:用
find ./images/train -name "*healthy*" | shuf -n 800 | xargs rm删减至与病害类平衡(每类约600–800张)。
5.4 现象:导出ONNX后推理结果全为0,或bbox坐标异常
原因:Ultralytics导出ONNX时默认--dynamic开启,但TensorRT或OpenVINO部署时未正确处理动态轴;或输入预处理未对齐(如训练用RGB,推理用BGR)。
解决:
- 导出命令加
--simplify --opset 12 --dynamic; - 推理前确保:
img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)→img = img.astype(np.float32) / 255.0→img = np.transpose(img, (2,0,1)),顺序不可错。
5.5 现象:同一张图,CPU推理结果与GPU推理结果bbox坐标差2–3像素
原因:FP16精度下,不同硬件对浮点累加的舍入策略不同;YOLOv8的non_max_suppression函数在CPU/GPU上实现略有差异。
解决:
- 生产环境统一用
--device cpu导出并部署(农业边缘设备多为ARM CPU); - 若必须GPU,固定
torch.backends.cudnn.benchmark = False,禁用cudnn加速以保证确定性。
6. 部署验证技巧:用三张图快速判断模型是否真正可用
6.1 构建最小验证集:覆盖田间最棘手的三类场景
不要用测试集平均指标判断落地效果——要挑出最可能翻车的三张图,手工验证:
| 场景类型 | 图像特征 | 验证目标 | 我的验证图编号 |
|---|---|---|---|
| 低对比度病斑 | 阴天拍摄,早疫病斑呈浅褐色,与健康叶色接近 | 模型能否区分色差<15ΔE的区域 | IMG_1832.jpg(val集第3张) |
| 密集遮挡 | 3片叶片交叠,仅露出1/4病斑区域 | NMS是否误删被遮挡病斑 | IMG_2107.jpg(val集第12张) |
| 镜面反光 | 水珠附着叶片,形成局部高光区覆盖病斑 | 模型是否把反光误判为灰霉病(白色) | IMG_0941.jpg(val集第28张) |
为什么只选3张?
实战中,只要这三张能过,其余90%图像基本可靠。过多验证图反而掩盖核心缺陷——比如模型在IMG_1832.jpg上漏检,说明颜色空间转换或归一化参数有误;在IMG_2107.jpg上漏检,说明anchor尺寸没适配小病斑。
6.2 用热力图定位模型“注意力盲区”:不只是看bbox,要看它到底在看哪
YOLOv8本身不输出热力图,但我们可通过Grad-CAM轻量级注入(无需重训):
from pytorch_grad_cam import GradCAM from pytorch_grad_cam.utils.image import show_cam_on_image # 加载训练好的模型(.pt) model = YOLO('runs/detect/train/weights/best.pt').model model.eval() # 提取最后一层卷积(通常是model.model[-2]) target_layers = [model.model[-2].cv2.conv] cam = GradCAM(model=model, target_layers=target_layers, use_cuda=True) # 读取验证图 img = cv2.imread('./images/val/IMG_1832.jpg') img_rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) img_tensor = torch.from_numpy(img_rgb).float().permute(2,0,1).unsqueeze(0) / 255.0 # 生成热力图(针对class_id=1,即early_blight) grayscale_cam = cam(input_tensor=img_tensor, targets=[ClassifierOutputTarget(1)]) # 叠加显示 cam_image = show_cam_on_image(img_rgb.astype(np.float32)/255.0, grayscale_cam[0], use_rgb=True) plt.imsave('early_blight_heatmap.jpg', cam_image)解读热力图:
- 若热力图集中在叶脉或叶缘,说明模型没学会关注病斑纹理,需加强纹理增强(如
A.RandomToneCurve); - 若热力图覆盖整个叶片但病斑区域亮度低,说明特征提取层感受野过大,应调小
backbone的stride或增加SPPF模块深度; - 若热力图与人工标注bbox完全不重合,大概率是标注错误或数据加载时坐标转换bug。
6.3 边缘设备实测必做三件事:量化、裁剪、缓存
在Jetson Nano或RK3588上部署时,光有.pt文件远远不够:
INT8量化(非必须但强烈推荐):
yolo export model=best.pt format=engine int8=True device=0实测YOLOv8n在Nano上INT8推理速度达23 FPS,FP16仅14 FPS,且精度损失<0.8%mAP。
输入尺寸裁剪:
番茄叶片通常占据画面中心区域,将imgsz=1280改为imgsz=640并启用--rect,可减少30%无效计算——因为模型不再处理边缘的土壤/铁架。结果缓存防抖:
农业摄像头常有轻微抖动,导致连续帧bbox跳变。我在推理后加了滑动窗口滤波:# 缓存最近5帧的bbox中心点 history_centers = deque(maxlen=5) def smooth_bbox(bbox): cx, cy = (bbox[0]+bbox[2])/2, (bbox[1]+bbox[3])/2 history_centers.append((cx, cy)) if len(history_centers) == 5: smoothed = np.mean(history_centers, axis=0) return [smoothed[0]-20, smoothed[1]-20, smoothed[0]+20, smoothed[1]+20] return bbox
我坚持每次新训完模型,都用这三张图+热力图+边缘实测走一遍。有一次发现热力图总在叶柄上亮,查了三天才发现classes.txt里healthy和early_blight顺序写反了——那种“以为训好了,其实全在学错东西”的感觉,真的不想再经历第二次。希望帮到你。
本文还有配套的精品资源,点击获取