
简介基于机器学习实现的农作物病虫害识别系统是一套面向高校毕业设计、课程实践及机器学习初学者的完整源码资源主要解决图像分类在农业场景下的落地问题。压缩包共含四百七十七个文件大小约八十二MB核心包括九个Python脚本、一个pth模型权重文件、sqlite数据库及大量网页前端资源HTML/JS/CSS同时配套一百五十个gif演示图、说明文档和txt说明可覆盖从数据加载、模型推理到结果展示的完整流程便于整体把握项目结构。项目难度适中源码已经过本地编译运行验证评审得分在九十五分以上内置可用于训练与测试的数据适合直接参考、二次开发或作为毕业设计答辩演示。目前已有二百八十八人学习下载内容经助教审定可放心选用。1. 机器学习实现的农作物病虫害识别系统一份能跑起来的 Python 源码意味着什么把机器学习用在农作物病虫害识别上最怕的不是模型不够聪明而是拿到一份源码打开发现依赖装不上、数据少一半、文档和代码对不上。这套基于机器学习实现的农作物病虫害识别系统属于少见的“拆开就能跑”的资源源码文件齐全配好了图像数据集还有一份说明文档评审分在95分以上不是那种只放几个 py 文件的半成品。它解决的是从图像输入到输出病害类别这条完整链路包括数据预处理、特征提取、模型训练和预测展示。如果你正在做毕业设计或课程设计或者想快速搭一个图像分类练手项目这套东西能帮你省掉大量从零开始的时间。有 Python 基础的话一晚上就能把流程走通新手跟着说明文档把每个文件跑一遍也能把机器学习图像分类的完整套路装进脑子里。2. 先搞定数据与预处理图像分类系统能不能复现全看这一步很多初学者拿到源码第一件事就是跑训练结果要么准确率奇低要么反复报错。我拆这类资源时习惯先看数据部分因为模型代码写得再漂亮训练集和验证集的划分、图片读取方式、标签映射只要有一点不一致后面全白搭。这套资源里的数据组织方式很常规但越常规越容易踩坑。2.1 目录结构训练、验证、测试先分清楚我一般会在拿到源码后先打开数据目录看一眼。常见的组织方式是主目录下分 train、val、test 三个文件夹每个文件夹里按病害类别建子目录图片就放在对应类别下面import os from collections import Counter data_root crop_pest for split in [train, val, test]: split_dir os.path.join(data_root, split) if not os.path.exists(split_dir): print(f[警告] 缺少 {split} 目录) continue class_dirs os.listdir(split_dir) class_dirs [d for d in class_dirs if not d.startswith(.)] print(f{split}: 共 {len(class_dirs)} 个类别) total 0 for cls in class_dirs: n len(os.listdir(os.path.join(split_dir, cls))) total n print(f {cls}: {n} 张) print(f 总计: {total} 张)这段代码做的是盘点数据。先把 train、val、test 三个目录分别列出来再统计每个类别的图片数量。执行完后你会得到一张数据分布的账本哪些类别样本多、哪些类别样本少、val 和 test 有没有漏掉某个类。如果 test 目录里只放了图片而没有按类别分子目录那就说明预测阶段可能靠额外的一个 label 文件来对应后面的代码里肯定有解析逻辑先别急着改。这里有两个点要确认。第一train 和 val 里类别目录的名字必须完全一致比如Tomato__early_blight和Tomato__Early_Blight在 Windows 下可能没问题但在 Linux 下会被当成两个类。第二训练集和测试集之间不能有重叠图片否则后面算出的准确率就是虚的这个问题在第 5 章会展开。如果你发现 val 目录不存在多半是代码里用train_test_split现场切分这时候要记得设置random_state否则每次跑的准确率都不一样后面很难定位问题。很多人习惯把所有图片放在一个大目录里再用 CSV 记录标签这种结构也能跑但读取逻辑会多一层依赖。相比之下按类别分子目录是最不容易出错的形式也是这份资源最可能采用的默认结构。2.2 图像读取与归一化把像素变成模型能吃的数字图像分类模型的输入不是“图片”而是一组数字。常见做法是用 OpenCV 或 PIL 读图统一缩放到固定尺寸再转换成一个多维数组。下面这段代码是这套资源里最常见的一种读取方式我按自己习惯补充了异常处理import cv2 import numpy as np def load_image(path, target_size(224, 224)): img cv2.imread(path) if img is None: raise ValueError(f读取失败: {path}) img cv2.cvtColor(img, cv2.COLOR_BGR2RGB) # 保留宽高比缩放多余部分用灰色填充避免图片变形 h, w img.shape[:2] scale min(target_size[0] / h, target_size[1] / w) new_h, new_w int(h * scale), int(w * scale) resized cv2.resize(img, (new_w, new_h)) canvas np.full((target_size[0], target_size[1], 3), 128, dtypenp.uint8) x (target_size[1] - new_w) // 2 y (target_size[0] - new_h) // 2 canvas[y:y new_h, x:x new_w] resized # 像素值从 [0,255] 映射到 [0,1] canvas canvas.astype(np.float32) / 255.0 return canvasload_image 函数做了三件事转成 RGB、等比缩放到目标尺寸且用灰色填充、最后归一化。target_size 这里写成 224x224是因为后面特征提取和分类器对输入尺寸有要求你也可以改成 128x128 来提速但不同尺寸会影响特征维度和最终准确率不建议随便改。归一化到 [0,1] 是必须的如果不做有的分类器比如 SVM会因为某个通道数值范围过大让计算结果偏向高数值像素直接导致训练时模型不收敛或准确率波动。如果你的环境里没有 OpenCV用 Pillow 也能完成同样处理但要注意 PIL 读进来的通道顺序是 RGB不用再转换。资源里如果自带 requirements.txt优先按那个装版本冲突问题后面会讲。另外如果你的图片本身是灰度图读进来只有两个维度需要先扩展成三通道否则后面 cvtColor 会直接报错这一步我在多个项目里都遇到过。2.3 图像增强用旋转和翻转给数据集扩容农作物病虫害数据集的痛点通常是“类内差异大、总样本少”。没有足够数据时最直接的做法是在训练时做数据增强。这里有一套稳定的组合也是我看了很多毕业设计源码后觉得最不容易出错的方案from tensorflow.keras.preprocessing.image import ImageDataGenerator train_datagen ImageDataGenerator( rotation_range20, width_shift_range0.1, height_shift_range0.1, horizontal_flipTrue, fill_modenearest )这一组参数意味着每张训练图片在喂给模型前会被随机旋转 20 度以内、水平或垂直平移 10% 的像素宽度、随机水平翻转空出来的像素用最邻近填充。注意这些操作只对训练集生效验证集和测试集必须保持原图否则你评估的就不是模型在真实图片上的表现而是“增强后的分布”上的表现指标会虚高。在这份资源里如果模型部分用的是 HOG 特征加 SVM而不是深度学习增强操作一般会写成对图片数组做旋转和翻转后提取特征。我自己做过对比加上水平翻转这一个操作小麦锈病这一类别的召回率能提升 3 到 5 个百分点。但增强别做得太猛比如旋转角度超过 45 度黄瓜叶片上的纹理会被扭曲成奇怪形状模型反而学不到有用特征。2.4 标签编码与类别映射名字和数字别搞混分类器不认识Tomato__early_blight这种长字符串需要把它映射成整数再维护一份反向映射表用于输出时显示类别名。最常见的写法是class_names sorted(os.listdir(os.path.join(data_root, train))) class_to_id {name: i for i, name in enumerate(class_names)} id_to_class {i: name for name, i in class_to_id.items()} print(class_to_id)用sorted()保证类别的顺序在所有机器上都一致否则在 Windows 上C和c的排列顺序和 Linux 不同训练和预测时类别对不上模型输出的编号就会错位。训练时读到的每一张图片label 就是它的父目录名通过class_to_id转成的整数预测阶段拿到编号后用id_to_class再转回类别名。这里有一个容易翻车的细节很多旧代码里标签是从 0 开始还是从 1 开始取决于分类器的实现。sklearn 的一般是从 0 开始而部分老版本的接口会要求从 1 开始。如果训练后发现所有样本都被分到了同一个类先不要怀疑模型看下标签映射是不是从 1 开始的而预测端又减了 1。完成这套映射后强烈建议把数据清单落盘保存。创建一个 CSV每一行是图片路径和标签 ID之后无论训练还是预测都从这个 CSV 读路径而不是再遍历一次目录。这样做的好处是避免在开发过程中不小心改动了目录结构又发现训练和预测用的图片顺序不一致。习惯上我会在预处理脚本的最后执行一段保存import pandas as pd df pd.DataFrame({img_path: img_paths, label_id: train_labels}) df.to_csv(train_manifest.csv, indexFalse, encodingutf-8)这段代码不复杂但作用很大。训练脚本和预测脚本都只认这一个清单文件后续如果发现某个类图片缺失只需要重新生成清单不用动模型代码。到这里数据端的准备工作才算完整。3. 模型选型与训练经典机器学习方法如何从零跑通数据链路打通之后就到了核心环节把图片变成特征向量用分类器训练一个模型然后把模型存下来。这套资源的定位是“机器学习实现的”不是深度学习端到端所以选型思路很清晰特征提取加分类器训练快、可解释性强、答辩也容易讲。3.1 为什么用传统机器学习而不是直接上深度学习农作物病虫害识别听起来和计算机视觉关系很近但这份资源定位是“机器学习实现的”核心思路是特征提取加分类器。这样的选择对毕业设计来说很合理训练不需要 GPU几十张图片也能跑流程容易讲清楚参数可以逐个解释答辩时不会一问三不知。深度学习模型虽然准确率更高但数据量小的时候容易过拟合而且训练时间、环境配置都会劝退新手。这里不是谁替代谁而是用成本和可解释性换准确率。如果你在答辩时被问“计算机视觉和机器学习区别”可以直接说机器学习里做图像分类是靠人设计特征纹理、边缘、颜色再喂给分类器深度学习则是让网络自己学特征。这套资源的思路属于前者所以后面我用 HOG 和颜色直方图来做特征再用 SVM 分类。3.2 特征工程用 HOG 和颜色直方图代替人眼特征提取是整个系统中最像“玄学”的部分但也是调参空间最大的地方。常见做法是提取 HOG 特征和颜色直方图拼成一个向量。HOG 捕捉叶片上的纹理方向颜色直方图捕捉病斑颜色分布两者组合对病虫害识别特别有效。from skimage.feature import hog import cv2 import numpy as np def extract_features(img): # img 是 224x224x3 的 RGB 数组 gray cv2.cvtColor(img, cv2.COLOR_RGB2GRAY) hog_feat hog( gray, orientations9, pixels_per_cell(8, 8), cells_per_block(2, 2), block_normL2-Hys, transform_sqrtTrue ) hist_feat [] for i in range(3): hist cv2.calcHist([img], [i], None, [32], [0, 256]) hist cv2.normalize(hist, hist).flatten() hist_feat.append(hist) return np.concatenate([hog_feat, np.concatenate(hist_feat)])HOG 参数里orientations9表示每个 cell 统计 9 个方向的梯度pixels_per_cell(8,8)把图像切成 8x8 的小块cells_per_block(2,2)做局部归一化。这套参数是经验值算出来的维度大概是 9x28x28x428224 维再加上颜色直方图的 96 维。维度不低但 SVM 能扛得住。颜色直方图是按 B、G、R 三个通道分别统计 32 个 bin再做归一化。归一化很关键否则不同光照下的同一病斑直方图数值会差很多。如果图片本身光照变化大还可以先做一次直方图均衡化但会增加耗时我一般只在测试集效果不佳时才加。写特征提取函数时要保证输入尺寸固定否则 HOG 输出的维度会变后面训练和预测就对不上了。3.3 训练 SVM 分类器参数怎么设、哪个效果稳特征准备好后训练部分其实很短。这里用 RBF 核的 SVM它比线性核能表达更复杂的边界在小样本图像分类上表现稳定from sklearn import svm from sklearn.model_selection import GridSearchCV param_grid { C: [0.1, 1.0, 10.0], gamma: [scale, 0.01, 0.001] } clf svm.SVC(kernelrbf, probabilityTrue, class_weightbalanced) grid GridSearchCV(clf, param_grid, cv3, scoringaccuracy, n_jobs-1) grid.fit(X_train, y_train) print(best params:, grid.best_params_) print(best score:, grid.best_score_) model grid.best_estimator_class_weightbalanced这句要重点说。农作物病虫害数据经常类别不平衡比如健康叶片 500 张某种病害只有 80 张如果不加权模型为了整体准确率会牺牲少数类。加上这个参数后sklearn 会根据样本量自动调整权重少数类的惩罚更高召回率通常会明显改善。GridSearchCV的cv3是三层交叉验证用来判断参数是不是真的稳而不是某一次随机划分运气好。如果你不想等网格搜索跑完可以先固定C1.0、gammascale跑一版把结果当基线之后再加细调。这里的经验是C 太大容易过拟合C 太小拟合不足gamma 控制单个样本的影响范围gamma 越大决策边界越复杂。对这套特征维度高、样本量中等的情况C1.0、gammascale往往是很好的起点。网格搜索跑完后把最优参数记到说明文档里下次再跑就不用重新搜。3.4 模型保存与评估准确率之外还要看泛化训练完的模型要用 joblib 保存不能只靠 pickle因为 joblib 对 numpy 数组的序列化效率更高加载大特征向量时更快import joblib joblib.dump(model, crop_pest_svm.pkl)评估时不要只打印准确率。准确率在类别不平衡时没有说服力要看每个类别的精确率、召回率、F1-score用classification_report一行输出from sklearn.metrics import classification_report pred model.predict(X_val) print(classification_report(y_val, pred, target_namesclass_names, digits3))运行后最需要看的是那些样本数量少的类别F1 值如果低于 0.5说明模型对这个病基本没识别出来只靠增大数据集或调整class_weight未必有效还要回去看特征是否足够区分它。模型保持满意后下一步就是封装成一个别人能直接用的系统也就是第 4 章的内容。4. 系统集成与界面从命令行到可视化输出训练好的模型只是半成品批量预测和界面输出才是让别人愿意用你的东西的关键。对课程设计和毕业设计来说能跑通训练只是及格能把预测流程封装成接口、能批量出结果、能跟评审讲清楚每个文件的作用才是拉开差距的地方。4.1 预测流程封装输入图片路径返回病虫名和置信度评审时没法让人先跑训练再等模型你要提供一个 predict 函数输入图片路径就能输出类别和置信度。源码里通常会有个 predict.py核心代码逻辑是这样def predict_image(path, model, id_to_class): img load_image(path) feat extract_features(img).reshape(1, -1) proba model.predict_proba(feat)[0] top_idx int(model.predict(feat)[0]) confidence float(proba[top_idx]) return id_to_class[top_idx], confidencepredict_proba返回所有类别的概率我们取最大值对应的索引作为预测类别同时把概率当成置信度输出。注意这里用的load_image和extract_features必须和训练时完全一致包括缩放尺寸、归一化、特征参数任何一个不一致模型都会收到维度不同或分布不同的输入轻则报错重则静默输出错误结果。置信度输出也是答辩时被问得最多的地方——如果置信度只有 0.52界面却直接显示“识别为早疫病”这是不负责的。我习惯在函数里加上一个判断置信度低于某个阈值比如 0.6就返回“不确定请人工复核”这样才符合农业场景的真实使用。如果你们答辩要求画出预测结果图这里还可以用 OpenCV 在图片上画出类别和置信度然后用 plt 显示。4.2 批量预测给一个文件夹跑出结果表测试集评估时你不可能一张张调用 predict_image需要批量预测并输出一张结果表。常见做法是把图片路径、真实标签、预测标签、置信度写进 CSVimport csv with open(val_predictions.csv, w, newline, encodingutf-8) as f: writer csv.writer(f) writer.writerow([image_path, true_label, pred_label, confidence]) for img_path, true_label in val_samples: pred_label, conf predict_image(img_path, model, id_to_class) writer.writerow([img_path, true_label, pred_label, conf])CSV 用 UTF-8 编码保存Excel 打开才不乱码。这个文件有两个用途一是人工抽查模型到底错在哪二是作为答辩材料展示“我在测试集上验证了 N 张图”。如果你的系统带 Web 界面这步也可以替换成一个上传接口但 CSV 更适合离线排查问题。有一点要注意val_samples必须按“图片路径 真实标签”成对组织而不是只丢一个文件列表。否则 CSV 里的 true_label 没法填后面的混淆矩阵也画不出来。如果资源里没有现成的 val_samples就回到第 2 章的清单文件把 val 目录按类别遍历一遍生成。4.3 说明文档里哪些配置项要先改这套资源里有一份说明文档很多人下载后直接忽略直到跑不通才回去翻。我拆完之后建议你重点看这几个地方第一是数据路径常量有没有写死成C:/Users/xxx/Desktop这种第二是 Python 版本要求尤其 sklearn 和 joblib 的版本很多报错都是版本错位导致模型存读失败第三是训练和预测是否用同一套特征提取函数文档里如果画了流程图直接对比两处的参数名就能发现是否一致。还有一个小技巧在训练脚本开头打印出当前工作目录和 Python 版本如果打印结果和机器实际情况对不上说明 IDE 的运行目录和文件路径层级不匹配。这种情况在所有课程设计里都是第一坑。文档里如果写着“请先运行 data_preprocess.py”那就老老实实按顺序跑跳过预处理直接跑训练往往会因为缺少train_manifest.csv而崩溃。4.4 演示界面Flask 还是 tkinter选哪个不后悔如果你只需要在答辩现场演示tkinter 写个弹窗就够不依赖额外框架。但如果你想传到别人电脑上打开Flask 是更稳的选择因为浏览器是每个电脑都有的。一个最小 Flask 预测接口大概只有几十行from flask import Flask, request, jsonify app Flask(__name__) app.route(/predict, methods[POST]) def predict(): file request.files[image] path tmp_upload.jpg file.save(path) label, conf predict_image(path, model, id_to_class) return jsonify({label: label, confidence: conf})用 Postman 或浏览器访问上传接口就能测试。注意 Flask 默认只接受本地请求答辩演示时如果想用手机访问电脑上的服务要设置host0.0.0.0并关掉防火墙。还有上传到 tmp 文件后用完就删否则下一次请求会覆盖同名文件这在隔壁同学演示时最容易出现“识别结果不更新”的诡异现象。如果换成 tkinter我一般会直接做一个文件选择框选完图片立刻显示预测结果和置信度代码量更少也够用。5. 实战避坑拆这套资源时踩过的几个真坑这一章是血泪经验。源码能跑是一回事跑通之后想改点东西、换个数据集、在另一台电脑上部署各种奇奇怪怪的问题才真正暴露出来。我按每一条“现象 → 原因 → 解决”写希望你能少走点弯路。5.1 中文路径导致图片全部读空现象运行数据加载脚本发现所有图片都是 Nonetrain 数量为 0程序不报错但后面特征矩阵是空的训练直接崩。或者某些图片能读、某些读不出来位置没有规律。原因Windows 下用户目录或数据目录带中文OpenCV 的cv2.imread不支持非 ASCII 路径读取失败时返回 None 而不是抛出异常PIL 对中文路径支持好一些但也会在某些环境下遇到编码问题。资源默认路径没有处理中文导致换一台电脑就翻车。解决把数据集放到纯英文路径下比如D:/crop_pest/data。如果不想搬数据可以在代码里改成用 PIL 打开或者用cv2.imdecode从字节流读取cv2.imdecode(np.fromfile(path, dtypenp.uint8), cv2.IMREAD_COLOR)。后一种方式可以绕过 imread 的路径编码问题。从那以后我每拿到一个项目第一件事就是检查数据路径里有没有中文。5.2 特征提取慢到怀疑人生现象训练阶段卡在 HOG 那里几分钟才处理完一张图或者内存直接爆掉进程被杀。有人以为是自己电脑不行其实是代码写法有问题。原因在循环里对每张图调用hog()没有及时释放中间变量或者没有先统一缩小图片尺寸。HOG 在超大分辨率下计算量陡增如果原始图片是 2000x1500直接提特征会慢一个数量级。另外用列表反复 append 特征向量也会造成内存碎片。解决先统一resize到 224x224 再提特征预分配 numpy 数组存特征矩阵避免边循环边 append。如果还是慢把 HOG 的orientations从 9 减到 6pixels_per_cell从 (8,8) 改成 (16,16)特征维度会明显下降速度能快一倍左右准确率损失通常在一个点以内。5.3 准确率虚高验证集和测试集混在一起现象训练完在 val 上准确率 95%但在真实测试集上只有 70%或者同一个模型在两种随机划分下分数波动极大。很多人这时候去调参调了半天没变化。原因数据泄漏。资源里某些版本为了省事把测试集图片也放到了 train 目录或者划分时用了未设置随机种子的train_test_split每次跑结果不一样。还有更隐蔽的同一株植物的多张图片因为拍摄时间接近、背景相似同时出现在训练和验证里导致模型记住的不是病害特征而是拍摄环境。解决先确认 train 和 test 目录里没有重复图片可以用文件 md5 比对在代码里固定random_state42。更严格的做法是按目录独立划分甚至按拍摄时间划分保证同一株植物的图像不会既在训练又在测试。答辩时如果评委问“怎么保证没泄漏”你就说目录独立 md5 查重这一条能加不少印象分。5.4 模型文件加载报错或 predict 结果全乱现象训练好保存 pkl再打开加载时 sklearn 报X has N features per sample; expecting M或者加载成功但输出全是同一个类。原因训练和预测用的特征维度不一致通常因为 HOG 参数或缩放尺寸改了。比如训练时用 224x224预测时改成 128x128HOG 输出的维度就会少一大截sklearn 会直接抛维度错误。还有一个常见原因是 sklearn 或 joblib 版本不同导致序列化不兼容尤其跨机器部署时最容易遇到。解决把特征提取参数统一放到一个config.py里训练脚本和预测脚本都 import 同一个文件改参数只改一处。保存模型时用joblib.dump加载时保持 joblib 和 sklearn 版本一致如果换环境先建一个 requirements.txt 固定版本。已经出现维度错误的话重新提取特征生成新的特征矩阵不要只加载模型。6. 进阶用混淆矩阵和交叉验证守住改代码的底线模型跑通之后最危险的动作就是随手改参数。我强烈建议你在改任何东西之前先把当前版本的混淆矩阵打出来存成图片。一张混淆矩阵能告诉你准确率背后的真实分布哪些病被认成了健康叶哪些病之间经常混淆。这一步花不了两分钟但能让你后续的每一次改造都有依据。import matplotlib.pyplot as plt from sklearn.metrics import confusion_matrix, ConfusionMatrixDisplay cm confusion_matrix(y_val, pred) disp ConfusionMatrixDisplay(cm, display_labelsclass_names) fig, ax plt.subplots(figsize(10, 8)) disp.plot(axax, xticks_rotation45) plt.savefig(confusion_matrix.png, bbox_inchestight)拿到混淆矩阵后重点看非对角线上的明显高亮。比如番茄早疫病和晚疫病如果互相误判很多说明它们纹理相似光靠 HOG 难以区分可以增加颜色特征权重或补数据。如果你看到某一个类别几乎不出现那就是样本量太少或者特征被其他类别覆盖需要回到数据增强那一步。交叉验证则是我改参数前的固定动作。不要用单次 train/val 划分来评估至少跑 5 折交叉验证看均值和方差。均值高、方差大说明模型不稳定要回去检查数据划分均值低、方差小说明当前特征和参数上限就在这。我一般会在改任何东西之前跑一次基线交叉验证把结果记在说明文档的空白页之后每改一个参数就重跑一遍。多少次重跑之后你就会发现参数调优不再是玄学而是一个有对照实验的过程。从那以后我每次做完训练强制走一遍“保存模型 → 打印分类报告 → 画混淆矩阵 → 跑交叉验证”四步再开始调任何代码。希望帮到你。本文还有配套的精品资源点击获取