基于Python机器学习的水稻病虫害自动识别系统实践
简介基于Python机器学习的水稻病虫害自动识别系统源码与配套数据面向计算机、人工智能、自动化等相关专业的学生、老师及从业者适用于毕业设计、期末课程设计和课程大作业等场景。压缩包共311个文件大小仅2.56MB以XML、Java、Class、Vue、JavaScript、CSS等类型为主分别承担项目配置、后端逻辑、前端页面与样式展示等功能结构清晰便于按模块查阅。目前已有294人学习/下载代码经过调试测试可稳定运行。项目源自答辩评审98分的个人毕设具备较高学习借鉴价值既能帮助小白快速上手Python机器学习项目也可供进阶者扩展前后端功能、调整模型与界面实现定制化水稻病虫害识别方案。1. 基于python机器学习的水稻病虫害自动识别是什么一个被低估的落地场景在水稻种植大省植保站的技术员每天要处理几十张病害叶片照片靠肉眼判断再翻图谱比对效率低且误判率高。一个基于python机器学习的水稻病虫害自动识别系统本质上是把“看图识病”这件事交给图像分类模型输入一张水稻叶片照片输出“稻瘟病”“纹枯病”“稻曲病”或“健康”等结论。免费python源码大全里能跑通的项目不少但能真正落到农业场景的很少。这套系统的价值在于它把机器学习应用流程完整走了一遍——数据准备、特征工程、模型训练、调参与部署适合有python基础、想拿一个完整项目练手或做农业信息化交付的开发者。下面按我自己的实现路径把这个系统的关键环节和踩过的坑完整拆开讲。2. 数据集是第一道门槛从手机拍照到可训练的样本库2.1 数据从哪来采集规范与公开数据集做图像识别数据质量直接决定模型上限。常见做法是优先找公开的水稻病虫害图像数据集像PlantVillage这类数据集覆盖了常见水稻病害但注意它是在控制环境下拍摄的背景干净、光照均匀和田间实拍差异很大。我一般会先用公开数据把流程跑通再补一部分自己采集的田间照片做微调。采集环节有几个规范要提前定死每张照片只拍一片叶子病害区域占画面三分之一以上背景尽量用纯色板或直接放在稻田土壤上避免杂草干扰同一病害采集至少 300 张健康叶片也要 300 张以上否则模型会把“健康”当成一个少数类预测时大概率误判。还有一个反直觉的点不要用同一株水稻连续拍很多张这些图像高度相似放进训练集和验证集后会造成数据泄漏后面验证集准确率虚高这个坑我在第 5 章展开讲。2.2 目录结构、划分与清洗三个命令搞定训练集数据组织方式直接影响代码复杂度。我习惯把数据按类别分目录存放然后用脚本按比例划分训练集、验证集和测试集。目录结构如下rice_pest_dataset/ ├── 稻瘟病/ │ ├── 001.jpg │ ├── 002.jpg │ └── ... ├── 纹枯病/ ├── 稻曲病/ ├── 胡麻叶斑病/ └── 健康/划分脚本我一般这么写import os import random from shutil import copy2 # 固定随机种子保证每次划分结果一致便于复现实验 random.seed(42) # data_root 是原始图片目录split_root 是划分后的目录 data_root rice_pest_dataset split_root dataset_split # 类别名建议用中文拼音或英文避免路径编码问题 classes [稻瘟病, 纹枯病, 稻曲病, 胡麻叶斑病, 健康] for cls in classes: cls_dir os.path.join(data_root, cls) images os.listdir(cls_dir) random.shuffle(images) # 按 7:1.5:1.5 划分训练/验证/测试 train_count int(len(images) * 0.7) val_count int(len(images) * 0.15) for i, img in enumerate(images): if i train_count: split train elif i train_count val_count: split val else: split test dst os.path.join(split_root, split, cls, img) os.makedirs(os.path.dirname(dst), exist_okTrue) copy2(os.path.join(cls_dir, img), dst)这段脚本的逻辑很简单遍历每个类别把文件名随机打乱按比例复制到对应目录。有几个参数值得注意random.seed(42) 固定随机种子这是实验可复现的前提否则每次跑结果都不一样划分比例 7:1.5:1.5 是图像分类的常见配置样本量很小的时候可以改成 8:1:1。这里我特意用的是 copy2 而不是 move因为原始数据还要留着做增广和交叉验证。如果你的图像是带标注框的还需要在这一步同步划分标注文件我这里用的是目录即标签的简化方案后面训练代码会直接按目录读标签。清洗方面脚本跑完后我通常会抽查每个子目录里的图片删掉模糊的、带严重水印的、以及明显不是水稻叶片的图片。可以用下面一行命令看每个类别的图片数量和尺寸分布import cv2 import os for cls in classes: cls_dir os.path.join(data_root, cls) for img_name in os.listdir(cls_dir)[:5]: img cv2.imread(os.path.join(cls_dir, img_name)) print(cls, img_name, img.shape)这一步能在训练前发现图片损坏或尺寸异常的问题。常见做法是顺手写个清洗脚本把无法被 cv2.imread 正确读取的文件列出来直接剔除。我一般会花半天时间做清洗这比后期调模型参数划算得多。3. 特征工程与模型选型传统机器学习还是迁移学习3.1 传统机器学习路径HOG HSV SVM很多新手拿到图像数据第一反应就是上深度学习但水稻病虫害识别这个场景有它的特殊性数据量通常只有几千张而且病害之间的差异集中在颜色和纹理上。我习惯先跑一版传统机器学习做基线既是为了快速验证数据质量也是给后面的深度学习模型一个对照标准。传统路径的核心是特征工程用 HOG 提取纹理和形状特征用 HSV 颜色直方图提取颜色特征拼接后喂给 SVM 分类器。下面是完整的特征提取和训练代码import cv2 import numpy as np from skimage.feature import hog from sklearn.svm import SVC from sklearn.preprocessing import StandardScaler from sklearn.pipeline import make_pipeline # 特征提取函数HOG 描述叶片纹理和病斑形状HSV 直方图描述颜色差异 def extract_features(image_path): img cv2.imread(image_path) img cv2.resize(img, (224, 224)) # 统一尺寸否则特征维度不一致 # HOG 参数9 个方向 bin每个 cell 为 8x8 像素每个 block 为 2x2 个 cell # 这套参数是行人检测的经典配置在叶片纹理上表现也稳定 hog_feat hog(img, orientations9, pixels_per_cell(8, 8), cells_per_block(2, 2), channel_axis-1) # HSV 颜色直方图H、S、V 各 32 个 bin # H 通道范围是 0-180S 和 V 是 0-255 hsv cv2.cvtColor(img, cv2.COLOR_BGR2HSV) hist_h cv2.calcHist([hsv], [0], None, [32], [0, 180]).ravel() hist_s cv2.calcHist([hsv], [1], None, [32], [0, 255]).ravel() hist_v cv2.calcHist([hsv], [2], None, [32], [0, 255]).ravel() color_feat np.concatenate([hist_h, hist_s, hist_v]) # 归一化颜色特征避免图像亮度差异主导分类结果 color_feat color_feat / (np.sum(color_feat) 1e-6) return np.concatenate([hog_feat, color_feat]) # 遍历训练集提取特征 X_train, y_train [], [] for cls in classes: cls_dir os.path.join(dataset_split/train, cls) for img_name in os.listdir(cls_dir): X_train.append(extract_features(os.path.join(cls_dir, img_name))) y_train.append(cls) X_train np.array(X_train) y_train np.array(y_train) # 用 Pipeline 串联标准化和 SVM避免数据尺度不一致 # RBF 核的 C10, gammascale 是默认参数中比较稳的组合 pipeline make_pipeline( StandardScaler(), SVC(kernelrbf, C10, gammascale, probabilityTrue, random_state42) ) pipeline.fit(X_train, y_train)这段代码里两个点值得展开。HOG 的 parameters 是固定的orientations9 表示把梯度方向分成 9 个区间pixels_per_cell(8, 8) 控制纹理细节的粒度调小能捕捉更细的纹理但会让特征维度爆炸调大则丢失病斑边缘信息8x8 是实践中最稳的选择。SVM 里 gammascale 会根据特征维度自动计算核宽度这比手动设一个固定 gamma 值要稳妥得多。C10 是惩罚系数C 越大越容易过拟合。特征拼接后HOG 维度是 37764 左右颜色特征是 96 维量级差距悬殊所以 Pipeline 里必须先做 StandardScaler 再做 SVM否则颜色特征会被 HOG 完全淹没。3.2 深度学习路径迁移学习 ResNet50传统机器学习跑通后再上深度学习就有了对照基线。水稻病虫害数据量通常不够从零训练一个 CNN迁移学习是标准做法。用 ImageNet 上预训练好的 ResNet50 作为特征提取器冻结主干网络只训练顶部分类头。完整代码import tensorflow as tf from tensorflow.keras.applications import ResNet50 from tensorflow.keras import layers, models # 输入尺寸统一为 224x224和预训练权重要求一致 IMG_SIZE (224, 224) BATCH_SIZE 32 NUM_CLASSES len(classes) # 加载预训练模型去掉分类头 # include_topFalse 表示不要 ImageNet 的 1000 类分类层 base_model ResNet50(weightsimagenet, include_topFalse, input_shape(224, 224, 3)) # 冻结主干网络参数训练时只更新分类头 base_model.trainable False model models.Sequential([ base_model, layers.GlobalAveragePooling2D(), # 把特征图压缩成向量比 Flatten 更抗过拟合 layers.Dense(128, activationrelu), layers.Dropout(0.5), # 随机丢弃一半神经元防止小数据集过拟合 layers.Dense(NUM_CLASSES, activationsoftmax) ]) model.compile( optimizertf.keras.optimizers.Adam(learning_rate1e-3), losscategorical_crossentropy, metrics[accuracy] )这里几个设计决策是踩坑换来的。GlobalAveragePooling2D 而不是 Flatten是因为 Flatten 会把 ResNet50 最后一层 7x7x2048 的特征图拉成 100352 维向量参数量直接爆炸在小数据集上必然过拟合平均池化把每个通道压缩成一个值保留特征的同时大幅减少参数。Dropout(0.5) 是最后的防线如果验证集准确率比训练集低 5 个百分点以上优先把 Dropout 加到 0.6 或者再加一层 Dropout。learning_rate1e-3 是 Adam 的常用起步值后面训练到平稳阶段要降到 1e-4 再做微调。3.3 选型对照什么时候别硬上深度学习很多读者会纠结到底用传统机器学习还是深度学习这里给一个实操判断标准维度传统机器学习HOGHSVSVM深度学习迁移学习 ResNet50数据量要求每类 200 张起步每类 500 张起步较稳300 张可跑但易过拟合训练环境普通 CPU 笔记本即可建议有 GPU没有 GPU 训练速度会非常难受推理速度单张 20ms 左右单张 50ms 左右GPUCPU 会到 200ms特征解释性可追溯是纹理还是颜色起主要作用黑匣子只能靠 Grad-CAM 类工具辅助解释鲁棒性换拍摄环境容易失效对光照和背景变化更鲁棒但需要更多数据支撑我的建议是如果你的目标是把一个 demo 快速跑通并交付传统机器学习足够如果你的目标是做一个能应对复杂田间环境的系统深度学习是必选项但前置条件是数据量要够。数据量不够而硬上深度学习结果往往不如调好的 SVM。一个折中方案是把 ResNet50 当作特征提取器取它的倒数第二层输出作为特征向量再喂给 SVM这种“深度特征传统分类器”的做法在中小数据集上经常比端到端的深度学习效果更好。4. 训练与调参让模型在真实农田里站得住4.1 标签编码与类别不平衡处理深度学习训练前标签要做 one-hot 编码目录名是中文时要注意编码一致性。我一般用 sklearn 的 LabelEncoder 先把类别名转成数字再转 one-hot避免手写映射表出错from sklearn.preprocessing import LabelEncoder, OneHotEncoder import numpy as np # 假设 y_train 是从目录读出的类别名列表 label_encoder LabelEncoder() y_train_encoded label_encoder.fit_transform(y_train) # 转 one-hotkeras 的 categorical_crossentropy 要求这种格式 y_train_onehot tf.keras.utils.to_categorical(y_train_encoded, num_classesNUM_CLASSES) # 查看每个类别的样本数判断是否不平衡 from collections import Counter counter Counter(y_train) print(counter)类别不平衡是农业图像数据的常态。稻瘟病样本可能占了 60%胡麻叶斑病只有 10%模型会学到一个偷懒策略——把什么都预测成稻瘟病因为这样准确率最高。时间充裕的做法是给少数类做过采样但更简单的方案是给类别加权重。用 sklearn 计算类别权重from sklearn.utils.class_weight import compute_class_weight class_weights compute_class_weight( class_weightbalanced, classesnp.unique(y_train_encoded), yy_train_encoded ) class_weight_dict dict(enumerate(class_weights)) # 训练时传入 class_weight 参数 model.fit( train_generator, validation_dataval_generator, epochs30, class_weightclass_weight_dict )class_weightbalanced 会让少数类的损失权重按比例放大平均每类的损失贡献差不多。如果手工调权重我习惯先看 Counter 输出把多数类权重设为 1.0少数类设为多数类样本数除以少数类样本数这样语义更清晰。4.2 数据增强小样本的后悔药数据增强是这个项目的后悔药。水稻叶片图像对翻转和色彩扰动有很好的容忍度因为叶片姿态本身多变。用 Keras 的 ImageDataGenerator 可以一行配置完成from tensorflow.keras.preprocessing.image import ImageDataGenerator # 训练数据增强旋转、平移、翻转、亮度调整 # 注意不要用太大的旋转角度超过 30 度会产生大量无效背景 train_datagen ImageDataGenerator( rotation_range15, width_shift_range0.1, height_shift_range0.1, brightness_range[0.8, 1.2], horizontal_flipTrue, vertical_flipFalse, # 水稻叶片通常不会倒置关闭垂直翻转 zoom_range0.1, fill_modenearest ) # 验证集和测试集只做缩放不做增强 val_datagen ImageDataGenerator(rescale1./255) train_generator train_datagen.flow_from_directory( dataset_split/train, target_sizeIMG_SIZE, batch_sizeBATCH_SIZE, class_modecategorical ) val_generator val_datagen.flow_from_directory( dataset_split/val, target_sizeIMG_SIZE, batch_sizeBATCH_SIZE, class_modecategorical )参数设置有几个实战考量。rotation_range15 而不是 30因为水稻叶片在田间的姿态差异主要在平面旋转超过 30 度后背景区域占比过大模型会学到背景特征而不是叶片特征。brightness_range[0.8, 1.2] 是模拟一天中不同时段的光照变化这个参数对田间实测数据非常重要因为手机上拍的照片亮度差异极大。vertical_flip 我特意关掉了水稻叶片正反面的纹理差异是区分某些病害的关键特征垂直翻转会把这些信息抹掉。fill_modenearest 是旋转和平移导致边缘空洞时的填充方式用最近邻填充不会引入太多人工痕迹。注意 flow_from_directory 要求目录结构就是 类别文件夹/图片 的形式和前面脚本生成的 dataset_split 结构正好对上。4.3 参数怎么调网格搜索与学习率深度学习模型的超参数调起来没有银弹但有两个必经步骤先用网格搜索找传统机器学习模型的参数上限再对深度学习模型做分阶段学习率调整。传统机器学习部分我习惯先跑一轮 GridSearchCV 确定 SVM 的参数范围from sklearn.model_selection import GridSearchCV from sklearn.svm import SVC from sklearn.pipeline import make_pipeline from sklearn.preprocessing import StandardScaler pipeline make_pipeline(StandardScaler(), SVC(probabilityTrue, random_state42)) param_grid { svc__C: [0.1, 1, 10, 100], svc__gamma: [scale, 0.01, 0.001], svc__kernel: [rbf] } # scoring 用 f1_weighted 而不是 accuracy因为类别不平衡时 accuracy 会骗人 grid GridSearchCV( pipeline, param_grid, cv5, # 5 折交叉验证 scoringf1_weighted, n_jobs-1, # 用全部 CPU 核心 verbose1 ) grid.fit(X_train, y_train) print(grid.best_params_) print(grid.best_score_)这里的核心是 scoringf1_weighted。农作物病害识别里稻瘟病误判为健康比稻瘟病误判为纹枯病要严重得多accuracy 不会区分这两种错误的代价差异。f1_weighted 对少数类的惩罚更重选出来的参数一般对各类别更均衡。C 和 gamma 的搜索范围按 10 的幂次取通常跑 16 组组合就够了。深度学习部分的学习率我采用先 1e-3 后 1e-4 的两段式策略前 10 个 epoch 用 1e-3 快速收敛第 11 个 epoch 开始降到 1e-4 精调。实现方式是用回调函数动态改学习率from tensorflow.keras.callbacks import LearningRateScheduler, EarlyStopping def lr_scheduler(epoch): if epoch 10: return 1e-3 else: return 1e-4 early_stop EarlyStopping( monitorval_loss, patience8, # 连续 8 个 epoch 验证损失不下降就停 restore_best_weightsTrue ) model.fit( train_generator, validation_dataval_generator, epochs30, callbacks[LearningRateScheduler(lr_scheduler), early_stop], class_weightclass_weight_dict )EarlyStopping 的 patience8 是平衡点太小会在损失曲线波动时过早停掉太大则会浪费时间。restore_best_weightsTrue 会在训练结束后把权重恢复到验证集表现最好的那个 epoch这是防止后期过拟合的最后一道保险。5. 避坑指南五个让我返工最多的问题5.1 训练准确率 99%验证集却只有 60%这是我第一个水稻病害模型遇到的坑也是最隐蔽的坑。现象是训练集准确率一路冲到 99%但验证集卡在 60% 左右怎么都上不去。原因很直接数据泄漏。我当时从同一株水稻的不同叶片采样这些叶片虽然外观略有差异但光照条件、拍摄角度、背景土壤高度相似模型学到的是“这张图的背景是上午拍的”而不是“这个病斑是稻瘟病”。解决方法是按采样来源划分训练集和验证集而不是按单个文件随机划分。我后来把同一块田、同一天拍的照片全部放进同一个集合验证集准确率从 60% 涨到 82%。这也是为什么前面划分脚本里我强调用随机种子但随机种子的粒度要按拍摄批次而不是按文件。5.2 稻瘟病和胡麻叶斑病总是互相认错现象是模型的混淆矩阵里稻瘟病和胡麻叶斑病之间的误判率特别高。原因是这两种病的症状在颜色上非常相似——都是褐色斑点只是纹理细节不同。HOG 特征在 8x8 cell 的粒度下能捕捉的纹理差异不够。解决方法是把 HOG 的 pixels_per_cell 从 (8, 8) 改成 (4, 4)强制模型关注更细的纹理另外在 HSV 直方图里增加 H 通道的 bin 数量到 64把色相差异分得更细。这个改动让混淆矩阵里这两个类别的误判率下降了 18%。如果还不行就该用深度学习模型了因为 CNN 能学习多尺度的特征组合。5.3 换一台手机拍照模型就像换了双眼现象是训练集和验证集都来自同一个相机准确率很漂亮但模型部署后用户用另一台手机拍照预测结果明显变差。原因是相机的白平衡、传感器色彩曲线、默认锐化强度都不同导致图像色彩分布整体偏移。这个问题的标准解法是色彩归一化和域适应。我采取的务实措施是训练数据里混入不同手机的实拍照片同时在数据增强里加大 brightness_range 的扰动范围到 [0.7, 1.3]再额外做一次 RGB 通道的随机小幅偏移。另外一个实用技巧是对图像做白平衡校正用灰界算法把每张图的 R、G、B 均值拉平减弱不同相机之间的色彩风格差异。这个技巧单独就能让跨设备的准确率提升 10 个百分点以上。5.4 模型对所有图片都预测“稻瘟病”现象是模型基本不工作预测输出几乎全是样本数最多的那个类别。原因是两类问题叠加类别严重不平衡且损失函数没有做任何调整。模型发现把所有样本都预测成多数类损失依然很低。解决方法是前面提到的 class_weightbalanced同时检查训练集的 Counter 输出如果某个类别的样本数占比超过 60%优先考虑过采样少数类把该类的样本复制到其他类别的平均数量级再配合类别权重做训练。我踩这个坑的深层教训是任何类别不平衡的项目第一步永远先看数据分布而不是先改模型结构。还有一个小概率原因是标签映射表出了问题比如 one-hot 编码和目录读取顺序不一致也表现为预测全部集中在某一个类别上用测试集里每类抽一张图片逐个打印预测结果就能排查出来。5.5 GridSearchCV 跑了 8 小时没结果、Python 进程被杀掉现象是网格搜索跑了大半天还在第 2 个参数组合系统内存告急。原因是 HOG 特征维度很高每个样本特征超过 3.7 万维全部样本的特征矩阵会占掉几个 GB 内存GridSearchCV 还要复制多份做交叉验证。解决方法是先用 PCA 把特征降到 500 维再进 SVM。这一步不会损失太多分类能力因为 HOG 特征本身有大量冗余信息from sklearn.decomposition import PCA # 降到 500 维保留主要纹理和形状信息 pca PCA(n_components500, random_state42) X_train_pca pca.fit_transform(X_train) # 后面的 SVM 用降维后的特征 pipeline make_pipeline(StandardScaler(), SVC(probabilityTrue, random_state42)) pipeline.fit(X_train_pca, y_train)PCA 让内存占用降了一个数量级网格搜索时间从 8 小时压缩到 40 分钟。如果项目里数据量更大还可以考虑增量 PCA。这是传统机器学习路径上性价比最高的一次改造。6. 从模型到系统把识别脚本变成可交付的软件6.1 模型导出与单张预测脚本训练好的模型无论是 sklearn 的 SVM 还是 keras 的深度模型都要导出成独立文件部署时直接加载。SVM 模型我用 joblib 导出import joblib joblib.dump(pipeline, rice_pest_svm.joblib) joblib.dump(label_encoder, label_encoder.joblib)深度模型用 keras 的 save_modelmodel.save(rice_pest_resnet.h5)导出后写两个文件一个单张图片预测脚本一个批量预测脚本。单张预测脚本是系统的基础功能import cv2 import numpy as np import joblib from tensorflow.keras.models import load_model # 加载模型和标签编码器 svm_model joblib.load(rice_pest_svm.joblib) label_encoder joblib.load(label_encoder.joblib) # 或者深度模型 # cnn_model load_model(rice_pest_resnet.h5) def predict_single(image_path): img cv2.imread(image_path) img cv2.resize(img, (224, 224)) # 如果是传统机器学习模型走特征提取分支 features extract_features(image_path) # 复用前面定义的特征提取函数 features features.reshape(1, -1) proba svm_model.predict_proba(features)[0] # 按概率从高到低排序取出前 3 个可能结果 top3_idx np.argsort(proba)[::-1][:3] result [(label_encoder.classes_[i], proba[i]) for i in top3_idx] return result # 测试 print(predict_single(test_images/稻瘟病_001.jpg))这里输出的不只是最可能的类别而是 Top-3 概率。农业场景里给出三个候选并附概率比硬给一个结论更容易让植保人员接受因为他们可以根据田间经验做最终判断。6.2 接一个简单的 Web 接口或桌面界面系统的落地形态决定了用户愿不愿意用。我做过两种方案一种是用 Flask 搭一个本地 Web 服务用户打开浏览器上传照片就能看到识别结果另一种是做个简单的 Tkinter 桌面工具植保站工作人员不需要打开浏览器。两者核心都是复用上面的 predict_single 函数。Flask 的最小实现如下from flask import Flask, request, jsonify import os app Flask(__name__) app.route(/predict, methods[POST]) def predict(): file request.files[image] file.save(temp.jpg) result predict_single(temp.jpg) # 返回 JSON包含类别名、置信度和处理时间 return jsonify({ predictions: [{label: label, probability: prob} for label, prob in result], status: success }) if __name__ __main__: app.run(host0.0.0.0, port5000)desktop 工具的核心差异只是把 get_image 函数接上文件对话框其余逻辑完全复用。我自己的习惯是先接 Flask 验证识别逻辑形态确认没问题再花时间做桌面端避免白做。最后提醒一句模型只是系统的一半水稻病虫害识别的现场照片输入质量参差不齐建议在系统里加一个简单的图像质量检查——判断图片模糊程度拉普拉斯方差低于某个阈值就提醒重新拍摄能显著减少用户的无效上传。这个功能对真实部署很重要却常常被漏掉。回看这个项目我最深的感受是水稻病虫害自动识别系统的难点不在模型而在数据组织的严谨性和对田间环境的敬畏。数据泄漏、类别不平衡、跨设备失效这三个坑每一个都能让你调参一周后还莫名其妙。希望这篇笔记帮你把这些弯路走直。本文还有配套的精品资源点击获取