基于PyTorch的水稻叶病害识别:从单图分类到视频实时检测

📅 发布时间:2026/10/20 20:14:30
基于PyTorch的水稻叶病害识别:从单图分类到视频实时检测
搞农业视觉的朋友应该都躲不开一个问题作物病害识别。尤其水稻稻瘟病、白叶枯病、纹枯病这几样每年不知道要吃掉多少产量。传统方式全靠植保专家下田巡诊看到可疑叶片拍照回实验室镜检效率低不说等结果出来可能已经蔓延了。所以用深度学习做水稻叶病害识别不是赶时髦是真的能落地的需求。这几个月我做了个基于PyTorch的水稻叶病害识别项目不仅能对单张图片分类还能对连续图片序列做批量识别甚至可以读视频流做实时检测把结果直接画在画面上。这篇文章把整个项目的设计思路、实现细节、踩过的坑都摊开讲适合正在做农业图像识别、或者在PyTorch框架下搞分类检测应用的同学参考。你不需要植保背景也能看懂但如果你懂一点水稻常识会更容易理解为什么有些坑必须避。1. 项目背景与整体思路拆解1.1 为什么选水稻叶病害作为切入点水稻叶部病害种类多、发生频繁而且症状都出现在叶片表面非常适合用视觉模型来判断。稻瘟病危害最大从苗期到穗期都能发病叶片上形成梭形病斑严重时整片叶子枯死白叶枯病是细菌性病害病斑沿叶缘或叶脉扩展初期呈水渍状干燥后变成灰白色纹枯病主要在分蘖期到抽穗期高发病斑呈云纹状由下向上蔓延。这三种病如果只靠肉眼新手很容易搞混特别是在发病早期。另一方面水稻种植面积大人工巡田成本很高无人机加定点相机越来越普及但拍回来的海量图片如果靠人看根本看不过来。所以这个项目的定位很明确做一个能自动识别这三种病和健康叶片的工具可以处理单张图片、连续图片序列还能直接对视频流做实时判断。这样植保人员在田边拿手机扫一圈就能知道大概有没有病、是什么病、分布情况如何。1.2 技术路线从单图识别到连续图像与视频识别我做第一版的时候只考虑了单张图片分类输入一张叶片照片输出病害类别和置信度。后来在实际场景中发现单张图片能获得的信息太有限。田间的叶片是层层叠叠的一张照片里往往有好几片叶子、好几个病斑而且光照、角度变化很大。如果只看单张误判率不低。所以我把技术路线改成了“先检测、后分类”的两阶段结构先用目标检测模型把每一片明显的叶片或者病斑区域找出来然后在每个区域内做病害分类。检测模型我用的是YOLOv5s分类模型用ResNet50和MobileNetV3做对比。到了视频识别阶段还要加上抽帧、时序平滑和后处理。整个处理流程大概是视频或连续图片序列进入系统先按设定的间隔抽帧每一帧送入检测模型定位叶片和病斑区域再对每个区域做分类最后把单帧的结果放入一个滑动窗口中做投票消除抖动输出稳定的病害判断。这样做的好处是即便某一帧因为运动模糊或遮挡导致分类错误前后帧也能把它“纠正”回来。1.3 环境选型PyTorch为什么够用图像分类和目标检测这块PyTorch不是唯一选择但确实是最省心的选择。它用的是动态计算图写起来跟写普通Python差不多调试的时候可以直接打印中间变量不用像静态图那样先构建计算图再执行。还有一个很现实的原因PyTorch生态里的预训练模型太多了torchvision自带ResNet、MobileNet、EfficientNetYOLOv5有现成的PyTorch实现改一下配置就能训练。推理部署也有TorchScript和ONNX作为后端即使以后要嵌入到边缘设备也不至于推倒重来。我最终用的环境是Python 3.8、PyTorch 1.12、CUDA 11.3、OpenCV 4.6外加Albumentations做数据增强。这套组合在普通办公电脑上也能跑GPU只是加速不是必需。如果你的电脑没有NVIDIA显卡用CPU推理单张图片大概300到500毫秒视频识别会吃力一点但至少开发调试没问题。2. 数据准备与预处理细节2.1 数据集怎么收集和整理数据是识别模型的地基这块不能糊弄。我用的数据来源有三个一是公开的植物病害数据集比如PlantVillage但里面水稻叶片数据偏少只能作为辅助二是某农业院校植保团队提供的大田拍摄照片包含了不同生育期、不同光照、不同角度的叶片这部分质量最高三是自己用手机在试验田拍的短视频再抽帧得到图片。整理的时候要统一文件名命名规则比如“生育期_品种_病害类型_编号.jpg”方便回溯。目录结构我建议按训练集和验证集分开验证集里再按类别分文件夹。如果是做检测还需要病灶的框坐标。我用YOLO格式的txt文件保存标注信息每行是“类别编号 中心点x 中心点y 框宽 框高”这些坐标值都归一化到0到1之间。有一个细节很容易被忽略同一个田间小区连续拍摄的多张照片内容高度相似如果同时放进训练集和验证集那验证结果会虚高因为你等于提前把答案告诉了模型。所以我按田块划分数据同一个田块的照片只进其中一个集合。2.2 病害类别与标签体系设计我最初只设了四个类别稻瘟病、白叶枯病、纹枯病、健康叶片。训练了几天发现准确率卡在85%左右很多稻瘟病和纹枯病互相混淆。后来复盘发现问题出在“一个类别对应多种病斑形态”上。稻瘟病有急性型和慢性型急性型病斑是暗绿色水渍状慢性型才是典型的纺锤形两者特征差异很大放在同一个类别里让模型学模型容易学出一个“四不像”。我的解决办法是在标注阶段增加“病斑形态”属性比如“稻瘟病-急性型”“稻瘟病-慢性型”“纹枯病-早期”“纹枯病-后期”训练时再把它们映射回最终需要的病名。这样模型不是直接把图片对应到病名而是先学习病斑的视觉特征再通过特征组合判断病名效果立竿见影准确率直接提升到了93%。另外类别数量不能太少每个类别至少要有500张以上最好超过1000张不然模型学不到足够的类内差异。2.3 数据增强策略与样本不均衡处理田间照片最大的变量是光照和角度所以数据增强要围绕这两个维度做。我用Albumentations里的RandomBrightnessContrast、HueSaturationValue、RandomGamma模拟早中晚的光照变化用HorizontalFlip、RandomRotate90模拟拍摄方向变化用Scale和RandomCrop模拟距离远近用CoarseDropout随机挖掉一小块区域模拟泥点或积水的遮挡。有一个原则增强不能脱离真实场景。我不会用那种特别夸张的变形和颜色反转因为水稻叶片就是绿色的变成紫色对模型没有任何帮助反而会让模型学到错误的信息。样本不均衡也是老问题健康叶片照片多病害叶照片少。我用两种策略一是Focal Loss让模型更关注难分类的少数类样本二是在构造DataLoader时用WeightedRandomSampler按类别频率倒数给每个样本分配采样权重每轮迭代里少数类被采到的次数更多。这两种方案我都试过WeightedRandomSampler更好调不会像Focal Loss那样出现gamma参数一改就掉点的尴尬。3. 模型训练与评估的关键参数3.1 模型选型从ResNet到轻量化网络模型选型要跟着使用场景走不能一味追求高精度。我一开始用ResNet50做分类baseline验证集准确率92%但推理一张图要100毫秒左右做视频识别很吃力。后来换成EfficientNet-B3准确率到95%但速度更慢。为了在视频流上达到实时效果我最终把方案定为检测用YOLOv5s分类用MobileNetV3-Large。YOLOv5s负责框出叶片和病斑候选区域MobileNetV3对每个区域做快速分类。这样组合下来在NVIDIA GTX 1660显卡上640x640输入视频能达到25FPS左右基本满足手持摄像头巡检的需求。如果你只做离线单张图片分析直接用ResNet50就好没必要牺牲精度如果要做实时视频建议优先考虑轻量化网络。还有一个思路是把检测和分类合并成一个多任务模型比如用YOLOv5不单是检测再添加一个分类分支但这样训练和调参会复杂不少对于小团队来说两阶段更稳排查问题也方便。3.2 训练超参设置与loss设计训练超参我直接给出能跑通的配置方便你参考。检测模型YOLOv5s输入尺寸640x640batch size 16初始学习率0.01SGD优化器CosineAnnealing学习率调度训练100个epoch。分类模型ResNet50或MobileNetV3输入尺寸224x224batch size 64AdamW优化器初始学习率0.001weight decay 5e-4训练50个epoch。Loss方面分类用CrossEntropyLoss就够了如果出现类别不均衡换成FocalLossgamma设为2alpha按类别频率的倒数设置。这里特别说一下学习率调整如果你因为显存问题把batch size从64降到32学习率最好也相应地从0.001降到0.0005否则收敛会变慢甚至震荡。我一开始没注意这个损失一直下不去白跑了好几个epoch。另外训练时要用ModelCheckpoint保存最佳模型不要只保存最后一个epoch的权重因为最后一个epoch往往已经过拟合。3.3 评估指标与混淆矩阵分析准确率只是参考在大田场景下更该关注的是误报和漏报哪个代价更高。如果模型把健康的叶子判断成稻瘟病农户会白白打药如果把稻瘟病判断成健康叶子病害会扩散。所以我评估时看的指标是每个类别的Precision、Recall、F1-score以及混淆矩阵。我画完混淆矩阵后发现纹枯病被误判成稻瘟病的情况最多原因是纹枯病早期病斑也是褐色小点和稻瘟病慢性型病斑在低分辨率下确实很像。找到这个问题后我没有急着改模型结构而是去补充了纹枯病早期的田间照片重新训练后误判率明显下降。这说明一个道理当模型表现不好时首先要看是不是数据问题而不是换更大的模型。另外TTATest Time Augmentation能提升准确率1到2个百分点做法是推理时对图像做水平翻转、多尺度缩放把多次结果取平均。但它会让推理速度慢三倍视频场景不建议用。4. 图像连续识别功能实现4.1 连续图像序列的读取与预处理流“图像连续识别”这个需求来自一个真实场景定点相机每隔几秒拍一张水稻叶片照片一天下来积累了几千张需要自动判断病害发生情况。如果一张一张手动跑模型再把结果汇总效率太低。我的做法是写一个批量序列识别脚本流程是遍历目录下所有图片逐张读入做预处理送入模型保存结果。核心代码片段如下。import os import cv2 import torch from torchvision import transforms transform transforms.Compose([ transforms.ToPILImage(), transforms.Resize((224, 224)), transforms.ToTensor(), transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]) ]) model torch.load(best_model.pth, map_locationcuda) model.eval() image_dir field_images results [] for img_name in sorted(os.listdir(image_dir)): img_path os.path.join(image_dir, img_name) img_bgr cv2.imread(img_path) if img_bgr is None: continue img_rgb cv2.cvtColor(img_bgr, cv2.COLOR_BGR2RGB) input_tensor transform(img_rgb).unsqueeze(0).to(cuda) with torch.no_grad(): output model(input_tensor) pred output.argmax(dim1).item() conf torch.softmax(output, dim1).max().item() results.append((img_name, pred, conf))这段代码里有两个容易踩的坑。第一个是模型一定要放在循环外面不能在每张图片里都执行一次torch.load否则光加载模型的时间就够你喝一壶的。第二个是OpenCV读入的通道顺序是BGR而torchvision的预训练模型用的是RGB如果不做cvtColor转换颜色信息就是错的模型准确率会暴跌。我在开发时踩了第二次这个坑排查了很久才发现是颜色通道的问题后来把所有预处理统一封装成一个类就不再犯这种低级错误了。4.2 帧间结果平滑与疑似病斑连续跟踪连续图片不像视频那样每帧强相关但同一个田块的定点相机每次拍摄间隔短的话叶片上的病斑面积是渐变的不会一下子从健康变成重度发病。基于这个先验我加了一个简单的滑动窗口投票机制维护一个长度为5的列表存最近5帧的分类结果当前帧的输出取这5帧里出现次数最多的类别。这样即使某一帧因为反光或遮挡被误判只要前后帧是对的最终结果依然正确。我在测试集上模拟了一下单帧准确率从92%提升到了96%左右。对于检测框的位置我也做了指数移动平均平滑公式是smoothed_box alpha * current_box (1 - alpha) * previous_smoothed_boxalpha设为0.3。这样盒子不会在相邻帧之间跳来跳去。需要注意的是如果相邻帧间隔超过10秒就不要再做这种平滑了因为叶片可能已经变化或者拍摄位置可能已经移动强行平滑反而会引入错误。4.3 批量识别结果导出与可视化裸奔的识别结果对业务人员没有意义所以我在批量识别脚本里加了两个输出模块。一个是CSV导出包含文件名、类别ID、类别名称、置信度、检测框坐标和识别时间另一个是可视化模块用OpenCV把检测框和标签画在图片上。画框的颜色我做了统一约定健康叶子用绿色稻瘟病用红色纹枯病用棕色白叶枯病用黄色。这样即使不懂技术的人一眼也能看懂病害分布。另外我还会用matplotlib画一个各类别数量占比的柱状图方便项目汇报。这里有一个体验上的细节画文字标签时如果直接用cv2.putText字体会随着分辨率变化显得很小或很大。我写了一个根据图片宽度动态调整字体大小的函数图片宽度小于800就用0.8倍字体大于1600就用1.5倍视觉效果会好很多。5. 视频识别功能实现5.1 视频流读取与抽帧策略视频识别是标题里的另一个重头戏。我先说离线视频文件用OpenCV的VideoCapture读取逐帧处理。如果你要实时识别摄像头画面方法也差不多但要注意读取缓冲导致延迟。我写了一个可配置的抽帧间隔参数--skip_frames默认每2帧处理一次也就是跳过1帧。之所以不每帧处理是因为水稻叶病害的变化比较缓慢相邻两帧的图像差异很小处理了也基本是重复结果还浪费算力。对于1080p的视频我建议先把帧缩放到640x480或640x360再推理这样速度能快好几倍而且小病斑不容易被丢掉再小就很难识别了。读取视频时有一个兼容性问题OpenCV对某些编码格式的MP4支持不好打开会失败或者读不出来。我建议统一用H264编码的MP4实在不行用imageio库读视频。5.2 检测框与置信度叠加到视频帧视频识别不仅要给出判断结果还要让用户直观看到模型在“看”什么。我在每一帧上都画了检测框和分类标签但画法有讲究。如果直接把整个叶片框用实心颜色填满会挡住叶片纹理用户根本看不到病斑长什么样。所以我的画法是只画细线框线宽设为2像素然后在框左上角添加一个带背景色的文字标签。文字内容是“稻瘟病 0.87”这样的格式保留两位小数。字体大小和线宽要根据视频分辨率调整1080p用1.2倍字体720p用1.0倍480p用0.8倍。还有一个关键点是在画框之前要做检测结果过滤我这里把置信度低于0.3的框全部丢弃只保留高置信度的结果。否则画面上会全是乱七八糟的小框根本没法看。如果模型对某个区域置信度很低我会在终端打印“需人工复核”但不会在画面上显示病情这是为了减少误导。5.3 实时性优化与推理速度调优视频识别最烦人的就是卡顿和延迟。我踩过的坑可以列出一串。第一个坑是推理时没有关闭梯度计算导致显存消耗巨大速度也慢。解决办法是在所有推理代码外面包一层with torch.no_grad():能省下不少内存。第二个坑是模型没有切换到eval模式BatchNorm层在train模式下会持续更新统计量导致输出不稳定。所以每次加载模型后必须调用model.eval()。第三个坑是输入分辨率过高我用640x640在GPU上能跑到30FPS如果换成1280x1280帧率直接掉到8FPS而小病斑并没有多识别出来多少所以要根据场景选择合适的分辨率。第四个坑是后处理时间太长NMS用原生实现很慢换成torchvision.ops.nms后速度快不少。第五个坑是如果同时处理多路视频读取和推理混在同一个线程里视频帧会堆积延迟越来越大。我的解决方案是使用一个带最大长度限制的队列读取线程放帧推理线程取帧队列满时直接丢弃旧帧保证实时性优先。经过这一系列的优化我在一台NVIDIA GTX 1660显卡的普通游戏本上把视频识别帧率从最初的8FPS提升到了22FPS左右基本能满足田间实时巡检的要求。6. 常见问题与排查实录6.1 显存不足与批量大小调整训练过程中最常遇到的就是“CUDA out of memory”。我一开始batch size设128直接OOM降到64还是不行最后用32加梯度累积才跑起来。梯度累积的代码很简单正常算loss后调用loss.backward()但不立刻optimizer.step()等到累积到一定步数再更新优化器。比如batch size为32你想模拟batch size为128的效果就累积4步。注意学习率要相应调整我前面说过batch size减半学习率最好也减半否则收敛会变慢。还有一个被忽略的点是验证阶段也会OOM。有些人只在训练代码里用了torch.no_grad()验证时又忘了内存直接爆掉。所以我建议在训练和验证的推理部分都统一用with torch.no_grad():包裹。另外可以试试混合精度训练用PyTorch自带的torch.cuda.amp能在基本不掉点的情况下把显存占用降低一半速度还提升不少。6.2 视频识别卡顿与跳帧处理视频识别卡顿不一定是模型推理慢还有可能是读取和推理速度不匹配。如果读取视频的速度远大于推理速度视频帧会在内存里堆积导致延迟越来越大画面看起来越来越卡。我用了一个有界队列来解决队列最大长度设为10满了就对新的帧做“覆盖写入”也就是把队头最旧的帧挤掉。这样系统总是优先处理最新帧不会一直堆积旧帧。还有一个问题就是视频编码格式不兼容OpenCV打开某些手机拍摄的MP4会打不开或者帧率异常。我的建议是先用格式工厂或者FFmpeg把视频统一转成H264编码这样最省心。如果是USB摄像头带宽不足也可能导致帧率低可以通过降低分辨率解决。另外读取视频时尽量避免在每一帧都执行VideoCapture.grab()和retrieve()之外的额外操作比如读属性、打印日志都会拖慢速度。6.3 模型过拟合与鲁棒性问题在实验室数据集上训练得很好一到田间就“拉胯”这个问题相信很多人遇到过。我分析原因主要是训练数据和实际使用场景的域差异实验室照片背景干净、光照均匀、叶片完整田间照片背景杂乱、光照剧烈、叶片还有遮挡和病斑重叠。缓解手段有几个。第一把田间拍摄的真实数据加入训练集哪怕只有100张也能明显提升泛化性能因为模型真正看到了它以后要面对的场景。第二用风格迁移生成一些田间风格的样本比如把实验室正常的照片改成强光照或者偏色版本这相当于一种高级数据增强。第三在推理前对输入图像做预处理比如轻微锐化或去噪减少运动模糊的影响但锐化强度要控制好太强会产生虚假纹理反而误导模型。还有一个更务实的做法是引入“需人工复核”机制当模型输出的最高置信度低于0.3时不给出具体病害结论而是提示人工复核。在农业场景里模型承认自己不确定远比硬给一个错误的结论有价值。最后说一句我自己的体会。农业视觉项目真正难住人的往往不是模型有多高级而是你有没有把数据整理干净、有没有在处理流程上做工程化、有没有在出错时耐心看混淆矩阵。PyTorch给了我们很顺手的工具但工具只是起点。这个水稻叶病害识别项目做到后面我越来越觉得把连续图像和视频识别的细节做好比追求那一个百分点的准确率更让人有成就感。如果你也在做类似的项目建议先从单张图片分类跑通再慢慢扩展到视频流每加一个环节就验证一次这样出问题的时候你才能快速定位。