☰
YOLOv11叶片病斑识别与分级系统:从环境配置到边缘部署的实战指南
2026/9/30 19:24:45 网站建设 项目流程

简介:这是一份面向农业植保人员、科研院校师生及计算机视觉开发者的技术文档,围绕农业病虫害智能化防治场景,系统讲解如何基于YOLOv11完成叶片病斑识别与分级系统的设计与实现。文档涵盖YOLO系列算法演进、YOLOv11网络结构创新点、数据集采集与预处理、模型训练与调优方法,以及病斑分级、防治建议、结果展示等完整功能模块设计,并附带系统测试与评估思路,便于读者从理论到实践全面掌握目标检测落地流程。资源包共1个文件,文件类型为PDF,体积2.3MB,共51页,支持目录跳转与大纲快速定位,文字、图表显示清晰完整。已有67人学习浏览,适合作为目标检测入门进阶或农业AI应用开发的参考资料。

1. 从一张病斑图到分级报告:YOLOv11做叶片病斑识别到底解决什么问题

农业病虫害防治里,叶片病斑的识别和分级一直是植保系统开发里最依赖人力的环节。农田里拍回一张水稻叶瘟照片,过去要等植保专家肉眼判断“零星发病、轻度、中度、重度”,同一张叶子不同人能评出两个等级,遇到发病早期的针尖状小斑更是一人一个说法。基于YOLOv11的叶片病斑识别与分级系统,就是把这件事从“靠人盯”变成“先检测、再统计、后分级”的流水线:YOLOv11负责把叶片上的病斑框出来,系统按病斑面积占比映射到严重度等级,最后输出一份带坐标、置信度和分级结论的结构化结果。这套方案适合两类人:一类是做农业信息化项目的开发者,需要把识别模型接进植保App或监测平台;另一类是刚接触YOLOv11、想用真实农业数据集练手的算法工程师。我下面讲的是按照完整系统开发实录整理的落地路径,覆盖环境配置、训练调参、分级设计和边缘部署,跟着做就能跑通最小可用版本。

2. 搭建YOLOv11叶片病斑识别环境:版本对齐、安装命令与数据准备

2.1 为什么先卡版本:YOLOv11与PyTorch的兼容边界

做YOLOv11叶片病斑识别,第一个坑往往不在模型,而在环境配置。很多人在自己电脑上装ultralytics时习惯直接pip install ultralytics,让它自动带出torch,结果训练到一半报错CUDA error: no kernel image is available for execution on the device,或者推理时提示算子不支持。原因很简单:YOLOv11的官方实现依赖PyTorch里较新的CUDNN算子,而你的显卡驱动、CUDA运行时和PyTorch三者的版本没对齐。

我的开发环境固定成一套组合:Ubuntu 22.04、显卡驱动550系列、CUDA 11.8运行时、Python 3.10。选择CUDA 11.8而不是12.x,是因为Jetson Nano这类边缘设备的JetPack固件长期停留在CUDA 11.4或10.2,本地开发版本和部署目标版本跨度越小,后面导出engine时踩的兼容坑越少。ultralytics本身的依赖不复杂,核心就torch、torchvision、opencv-python、numpy这几个,真正影响系统开发进度的是torch与CUDA的配对关系。

还有一个容易忽略的点:YOLOv11的网络结构里大量使用C2PSA模块,这种模块对PyTorch版本有隐性要求,太老的torch版本导出ONNX时会出现aten::_upsample_nearest2d算子兼容问题。所以我的建议是,不要追求最新版本,装一个经过验证的组合,然后锁定版本号。农业项目的数据采集周期长,模型训一次要跑十几个小时,中途环境崩了重新配一次的成本远大于你花十分钟把版本卡死。

2.2 用conda复现开发环境:安装命令与可用性核验

YOLOv11环境配置的完整命令我按顺序贴在下面,在全新Ubuntu机器上可以一步步照抄。注意先建conda虚拟环境再装torch,不要让ultralytics顺手装它自带的torch版本。

conda create -n leaf_yolo python=3.10 -y conda activate leaf_yolo pip install torch==2.1.0 torchvision==0.16.0 --index-url https://download.pytorch.org/whl/cu118 pip install ultralytics==8.3.40 pip install pandas openpyxl python -c "import torch; print(torch.__version__, torch.cuda.is_available())" python -c "from ultralytics import YOLO; print(YOLO.__name__)"

第一行创建python 3.10环境,这是YOLOv11官方测试覆盖较好的版本,3.11以上有些依赖的wheel还没完全跟上。第二行指定PyTorch 2.1.0的CUDA 11.8版本,用--index-url固定下载源,防止装成CPU版。第三行装ultralytics时我锁了8.3.40,这个版本对YOLOv11的各网络结构支持最完整,后面做小目标优化时可以直接用yolo11n.yaml改网络。最后的核验命令很重要,第一个输出必须看到True,如果显示False说明CUDA版torch没装上,后面训练速度会慢到让人怀疑人生。

2.3 叶片图像标注准备:从VOC标注到YOLO格式的转换脚本

环境装好之后,卡住最多人的是数据准备。叶片病斑数据集有一个天然特点:一张叶片上病斑数量多、单个面积小,而且不同病害的病斑形态差异大。我见过不少团队用LabelImg标注完就直接开训,结果YOLOv11死活不收敛,最后发现是标注框坐标没有归一化到0到1之间。

常见的标注工具导出的是VOC格式的XML文件,YOLO格式需要的是class_id x_center y_center width height这种归一化后的txt文本。这个转换脚本我每次做数据准备都会用,直接按下面的逻辑写就行:

import xml.etree.ElementTree as ET import os class_map = {'rice_blast': 0, 'leaf_spot': 1, 'healthy': 2} def voc_to_yolo(xml_path, out_dir): tree = ET.parse(xml_path) root = tree.getroot() img_w = int(root.find('size/width').text) img_h = int(root.find('size/height').text) lines = [] for obj in root.iter('object'): name = obj.find('name').text if name not in class_map: continue box = obj.find('bndbox') x1 = float(box.find('xmin').text) / img_w y1 = float(box.find('ymin').text) / img_h x2 = float(box.find('xmax').text) / img_w y2 = float(box.find('ymax').text) / img_h w = x2 - x1 h = y2 - y1 lines.append(f"{class_map[name]} {x1 + w/2:.6f} {y1 + h/2:.6f} {w:.6f} {h:.6f}") txt_name = os.path.basename(xml_path).replace('.xml', '.txt') with open(os.path.join(out_dir, txt_name), 'w') as f: f.write('\n'.join(lines)) for xml_file in os.listdir('annotations'): if xml_file.endswith('.xml'): voc_to_yolo(os.path.join('annotations', xml_file), 'labels')

脚本的核心逻辑是把XML里的绝对坐标除以图片宽高完成归一化,再转成中心点加宽高的格式。特别注意img_w和img_h必须从XML的size字段读取,不能自己假设图片尺寸,因为叶片数据集里不同相机拍的图片宽高比差异很大。class_map里的类别ID要固定下来,后续训练配置文件和推理脚本中的类别名必须和这里一一对应,否则会出现检测框对不上类别名的低级错误。

3. 训练叶片病斑检测模型:参数取值、收敛判断与小目标优化的先后顺序

3.1 数据划分与样本平衡:病斑在叶片上分布不均时怎么办

叶片病斑数据集和通用物体检测数据集有一个显著区别:正样本极度不均衡。一张健康的叶片上可能一个病斑都没有,一张重病叶上密密麻麻几十个病斑,而且病斑尺寸可能只占整张图片的0.5%。如果直接随机划分训练集和验证集,很容易出现验证集里全是健康叶、训练集里全是重病叶的情况,模型学到的只是背景和病斑的相关性,而不是病斑本身。

我的处理方法是按图片而不是按病斑做分层抽样,先按病斑数量把图片分成0、1-5、6-20、20以上四组,再在每组内按7:2:1比例切分。这样能保证训练集、验证集、测试集里的病斑密度分布大致对齐。脚本逻辑不复杂,核心是随机数种子固定住,方便复现:

import os, random, shutil random.seed(42) images_dir = 'images' labels_dir = 'labels' imgs = [f for f in os.listdir(images_dir) if f.endswith('.jpg')] def count_boxes(img_name): txt_name = img_name.replace('.jpg', '.txt') with open(os.path.join(labels_dir, txt_name)) as f: return len(f.readlines()) buckets = {'0': [], '1_5': [], '6_20': [], '20+': []} for img in imgs: n = count_boxes(img) if n == 0: buckets['0'].append(img) elif n <= 5: buckets['1_5'].append(img) elif n <= 20: buckets['6_20'].append(img) else: buckets['20+'].append(img) train, val, test = [], [], [] for key, bucket in buckets.items(): random.shuffle(bucket) cut1 = int(len(bucket) * 0.7) cut2 = int(len(bucket) * 0.9) train += bucket[:cut1] val += bucket[cut1:cut2] test += bucket[cut2:]

关键点在于buckets分组的阈值不是随便定的。6到20这个区间对应轻度到中度发病的叶片,20以上对应重度发病。如果总样本量不足,宁可砍掉20+组的部分样本做数据增强,也不能让验证集里出现极端样本。训练完成后用测试集做最终评估时,要单独统计不同密度分组下的mAP,这个指标能直观反映模型在早发病情上的泛化能力。

3.2 训练命令与核心超参数:imgsz、batch、epochs的落地取值

YOLOv11的训练命令本身不复杂,复杂的是参数选择。我一开始在叶片数据集上用默认的imgsz=640直接训,结果病斑漏检率高达40%,原因很简单:叶片原图是3000像素宽的单反照片,640分辨率下单个病斑可能只占几个像素,特征根本保留不下来。后来把imgsz调到1280,小病斑的召回率上去了,但显存占用翻了四倍,batch只能减半。

最终在单张RTX 3090上跑通的配置是imgsz=1280、batch=8、epochs=200。如果你用的是RTX 4060这类8G显存的卡,imgsz降到960是折中点。训练命令和对应的数据配置文件如下:

yolo train model=yolo11n.pt data=leaf.yaml imgsz=1280 batch=8 epochs=200 patience=30 project=runs name=leaf_v1

leaf.yaml文件内容按YOLOv11规范组织:

path: /data/leaf_dataset train: images/train val: images/val names: 0: rice_blast 1: leaf_spot 2: healthy

这里的model=yolo11n.pt选择nano版本而不是s或m版本,是考虑到后续要部署到Jetson Nano。nano版本在1280分辨率下对小目标的检测能力比s版本弱一些,但通过小目标优化可以弥补,后面会讲到。patience=30表示验证集指标连续30个epoch不提升就早停,这是叶片数据集的标准设置,因为样本量小,训练后期很容易过拟合。

一个容易忽略的参数是close_mosaic,YOLOv11默认训练最后10个epoch会自动关闭Mosaic增强。叶片病斑场景里Mosaic增强会导致病斑被拼接边界切断,标注框跨图,模型前期学到的特征很乱。建议显式设置close_mosaic=15,给模型留出更多时间在真实分布上收敛。

3.3 YOLOv11小目标优化:切片推理与注意力改进的实际收益

YOLOv11小目标优化是个绕不开的话题,尤其叶片病斑这种小目标密集场景。我的实践顺序是:先做切片推理,再改网络结构,不要一上来就换attention模块。切片推理的做法是把原图切成四块重叠区域分别推理,再把结果合并,相当于用计算量换小目标的分辨率,实现成本最低。

from ultralytics import YOLO import cv2 import numpy as np model = YOLO('runs/leaf_v1/weights/best.pt') img = cv2.imread('field_photo.jpg') h, w = img.shape[:2] crops = [] crops.append(img[0:int(h*0.6), 0:int(w*0.6)]) crops.append(img[0:int(h*0.6), int(w*0.4):w]) crops.append(img[int(h*0.4):h, 0:int(w*0.6)]) crops.append(img[int(h*0.4):h, int(w*0.4):w]) all_boxes = [] for crop in crops: results = model(crop, conf=0.25, imgsz=1280) for box in results[0].boxes: x1, y1, x2, y2 = box.xyxy[0].cpu().numpy() all_boxes.append([x1, y1, x2, y2, box.conf[0].item(), int(box.cls[0].item())])

切片之间的重叠区域设成20%,目的是防止病斑正好落在切片边界上被截断。合并结果后的去重处理可以用NMS,或者简单按IoU阈值0.3过滤重叠框。这种做法在叶片病斑上能让小目标召回率提升15%左右,代价是单张图片推理时间从0.8秒变成3秒,对离线分析场景完全可接受。

如果切片推理后仍然漏检,再考虑改网络结构。YOLOv11的head部分对微小目标的敏感度不够,常见做法是在P2层加一个小目标检测头,或者引入CA注意力机制让网络更关注病斑区域。改网络时要直接基于yolo11n.yaml修改,而不是改训练好的权重文件,这样网络结构的改动才能在训练中生效。

4. 从检测坐标到严重度分级:病斑面积估算、阈值设计与人机复核

4.1 用检测框换算病斑面积占比:从坐标到占比的几何估算

检测模型输出的是病斑的边界框坐标,但分级系统需要的是病斑面积占叶片面积的比例。直接用边界框面积除以整图面积会严重高估,因为一个边界框里可能只有30%的区域是真正的病斑,其余是健康组织。我这里的处理方式分两步:先用检测结果生成二值分割图,再统计病斑像素占比。

import numpy as np from PIL import Image def compute_lesion_ratio(result, img_path, mask_size=(640, 640)): img = Image.open(img_path).resize(mask_size) mask = np.zeros(mask_size, dtype=np.uint8) for box in result.boxes.xyxy.cpu().numpy(): x1 = int(box[0] / img.width * mask_size[0]) y1 = int(box[1] / img.height * mask_size[1]) x2 = int(box[2] / img.width * mask_size[0]) y2 = int(box[3] / img.height * mask_size[1]) mask[y1:y2, x1:x2] = 1 # 简单背景扣除:假设绿色叶片像素在HSV空间的H通道60-90范围内 hsv = np.array(Image.open(img_path).resize(mask_size).convert('HSV')) green = (hsv[:, :, 0] >= 60) & (hsv[:, :, 0] <= 90) non_green_mask = ~green lesion_pixels = np.sum(mask & non_green_mask) leaf_pixels = np.sum(~green) return lesion_pixels / max(leaf_pixels, 1) ratio = compute_lesion_ratio(results[0], 'test.jpg') print(f"病斑面积占比: {ratio:.2%}")

代码里通过HSV色彩空间的绿色通道判断叶片区域,把非绿色的像素视为病斑候选。这个逻辑的假设是病斑颜色和健康叶片有明显差异,对稻瘟病、叶斑病这类褐色、灰白色病斑有效。如果遇到病斑颜色和叶片颜色接近的情况,需要用分割模型替换颜色判断。

4.2 分级阈值怎么定:参考标准、实测数据与人工复核的修正

病斑面积占比算出后,映射到严重度等级是植保业务层面的决策。国家行业标准里对稻瘟病严重度的定义是按病斑面积占叶片面积的百分比划分的,但不同作物、不同病害的阈值差异很大,不能照搬。我的做法是先按通用经验设置初始阈值,再用人工标注的验证集做校准。

下表是经过两轮人工复核修正后的分级标准,适用于水稻叶瘟和玉米大斑病:

严重度等级病斑面积占比生产建议
0级(健康)<1%无需处理
1级(轻度)1% - 10%监测为主,暂不施药
2级(中度)10% - 30%建议局部施药
3级(重度)>30%立即全田施药

阈值修正的正向依据是植保专家对100张实测叶片的独立评级结果。专家评为“中度”的叶片,病斑占比分布在8%到35%之间,这个分布重叠区很大,说明人眼分级本身存在主观性。所以我最终采用的策略是把10%到15%之间定义为“灰色地带”,系统输出等级同时附带占比数值,由农技员做最终决策,避免系统硬分级和专家意见冲突导致信任崩塌。

4.3 保存推理结果:YOLOv11预测结果落盘的结构设计与字段定义

分级系统的输出不能只是一张画了框的图片,要把检测框、置信度、类别、面积占比和分级结论全部保存成结构化数据,方便后续生成植保报表。YOLOv11保存推理结果默认是在图片上画框,但项目开发里更需要的是JSON或者CSV形式的落盘数据。

from ultralytics import YOLO import json model = YOLO('runs/leaf_v1/weights/best.pt') results = model('field_photo.jpg', conf=0.25, imgsz=1280) records = [] for r in results: boxes = r.boxes.xyxy.cpu().numpy().tolist() confs = r.boxes.conf.cpu().numpy().tolist() clss = r.boxes.cls.cpu().numpy().tolist() for box, conf, cls in zip(boxes, confs, clss): records.append({ 'x1': round(box[0], 2), 'y1': round(box[1], 2), 'x2': round(box[2], 2), 'y2': round(box[3], 2), 'confidence': round(conf, 4), 'class_id': int(cls) }) output = { 'image': 'field_photo.jpg', 'lesion_ratio': round(ratio, 4), 'level': level, 'count': len(records), 'boxes': records } with open('result.json', 'w', encoding='utf-8') as f: json.dump(output, f, indent=2, ensure_ascii=False)

字段设计上lesion_ratio和level放在最外层,因为报表统计时最关心的是分级结论而不是每个框。坐标保留两位小数足够,置信度保留四位小数用于后续过滤阈值调整。如果系统里有多张图片,建议改成每张图片一个JSON文件加一个汇总CSV的结构,CSV里只放图片名、占比、等级、病斑个数四个字段,方便植保站用Excel直接做统计分析。

5. YOLOv11叶片系统开发避坑实录:五个让项目翻车的高频问题

5.1 训练到一半显存溢出

现象:训练跑了十几个epoch,突然报CUDA out of memory,而且是在同一个epoch的同一个batch上稳定崩溃,不是偶发。

原因:Mosaic增强在训练过程中会拼接多张图片,导致单张训练样本的实际分辨率波动很大。叶片数据集里图片尺寸极不统一,有的来自手机,有的来自单反,Mosaic拼接后尺寸可能超过预设imgsz,显存峰值出现在增强后而不是模型前向传播时。

解决:把cache=True改成cache=False,同时限制数据加载时的最大尺寸。更根本的解决办法是在数据预处理阶段把所有图片统一resize到一个固定尺寸池,比如640、800、960、1280四档,告诉yolo只从这个池子里取尺寸。这样显存峰值变得可预期。

5.2 小病斑漏检:验证集mAP高但田间新叶表现差

现象:验证集上mAP有0.85,分类别看叶斑病AP也有0.78,但拿无人机在田间新拍的照片一测,小病斑漏掉一大半,而训练集里明明有很多小病斑样本。

原因:训练集里的“小目标”和田间实拍的小目标尺度分布不一致。标注时为了省事,很多小病斑用大框框进去了,框的中心点可能落在健康组织上,模型学到的感受野和实际病斑尺度错位。

解决:用脚本统计训练集里所有标注框的面积分布,过滤掉那些面积小于图片面积0.3%的标注框,重新核对一遍。另外检查标注框是否紧贴病斑边缘,很多标注工具的默认框会比实际病斑大一圈。处理完标注再配合3.3节提到的切片推理,田间漏检率能降一半以上。

5.3 叶片互相遮挡导致误检:置信度阈值调不动怎么办

现象:把置信度阈值从0.25调到0.5,误检少了,但遮挡较多的叶片病斑也全部被过滤掉,调回0.25又出现大量把叶片边缘误检成病斑的情况。

原因:遮挡区域的真实病斑置信度本身就不高,和边缘误检的置信度分布重叠,单靠置信度阈值无法区分。问题出在训练数据里缺少遮挡样本。

解决:数据增强阶段开启随机裁剪和多尺度训练,模拟叶片重叠效果。更重要的是在标注时专门标注“遮挡病斑”这一类别,让模型学习到遮挡状态下病斑的特征表达。我在第二轮迭代时增加了300张人工遮挡的模拟样本,误检率从12%降到5%。

5.4 Jetson Nano推理只有几帧:部署性能翻车的常见原因

现象:在PC上单张图片推理耗时0.8秒觉得还行,部署到Jetson Nano后单张要5秒以上,完全达不到实时监测的需求。

原因:直接用PyTorch权重在Jetson上跑,模型没有转成TensorRT engine格式,而且YOLOv11默认输入尺寸是1280,Jetson的GPU算力跑不动这个分辨率。

解决:把推理分辨率降到640,并先导出TensorRT的float16 engine。Jetson Nano上640分辨率加float16精度,单张推理可以压缩到1.2秒左右。如果还需要提速,就需要用5.5节的分级策略,对每张图只做粗筛,疑似病害图才用1280分辨率精细分析。

5.5 分级结果与人工判断差异大:别急着改模型

现象:系统的病斑占比计算结果是28%,植保专家肉眼评了“轻度”,也就是10%以下,差异巨大,第一反应是面积估算算法有问题。

原因:占比计算用的是检测框,不是分割掩码。当病斑密集时框与框重叠面积被重复计算,加上叶片本身卷曲导致投影面积偏小,占比就被大幅高估。

解决:把4.1节的面积估算从检测框统计升级为分割掩码统计,或者对密集区域做二次聚类合并。我排查这个问题时发现,单靠调整阈值根本解决不了,最后引入实例分割模型替代检测模型才把占比误差控制在5个百分点以内。所以项目时间允许的话,分级系统的面积估算环节优先考虑YOLOv11-seg,别只盯着检测框。

6. 部署到Jetson Nano之后的推理优化:把识别分级跑进果园

6.1 TensorRT导出与half精度:稳住精度的同时提速

Jetson Nano部署YOLOv11的核心是把模型导出成TensorRT的engine格式,导出命令里的精度选项直接影响推理速度和检测效果。

yolo export model=best.pt format=engine device=0 half=True imgsz=640

half=True启用float16精度,在Jetson Nano上能带来近一倍的提速,代价是mAP下降约0.5到1个百分点,对病斑检测这种粗粒度任务影响不大。导出完成后用engine_path = 'best.engine'直接加载,推理方式和PyTorch权重完全一致,但速度差异立竿见影。

6.2 int8量化后的精度回退:哪些层需要保留float

如果追求更高帧率,可以尝试int8量化,但叶片病斑的小目标特征对量化非常敏感,病斑边缘的纹理信息在int8精度下会严重丢失,漏检率上升明显。我的建议是:除非你的推理设备实在扛不住float16的算力消耗,否则别轻易上int8。如果必须用,检查哪些层的输出特征图对病斑边缘敏感,把这些层单独保留float32,其他层用int8,这个操作在TensorRT的per-layer精度配置里可以做。

6.3 端到端串联:摄像头采集、识别分级与结果回传

最终部署形态是Jetson Nano接USB摄像头,图像采集用OpenCV的VideoCapture,每帧缩放到640分辨率送入engine推理,计算病斑占比和等级后通过串口或Wi-Fi把分级结果回传到农技站服务器。在果园实地跑起来后我发现,光照变化对分级结果的影响比模型本身还大,直射阳光下叶片反光会被误判为病斑。解决办法是在采集端加偏振片,固定白平衡,如果图像整体亮度超过阈值就先做一次曝光补偿。

这次开发里我最后悔的是在第一版就急着做分级,没有先在面积估算上多花时间验证,导致同样的标注数据返工了一轮标注。叶片病斑识别与分级系统真正能落地,瓶颈往往不是模型精度,而是面积估算的准确性和阈值设计的可解释性。希望你做的时候把这两块放在模型训练同等重要的位置,希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询