
简介这份资源是一套基于机器学习实现的农作物病虫害识别系统面向Python人工智能方向的毕业设计、课程设计或期末大作业人群提供可直接运行的源码与配套数据集。项目覆盖了从数据收集预处理、病虫害标签标注、特征提取到卷积神经网络与支持向量机等模型训练评估、系统部署和实时识别等完整工作流代码含详细注释模块划分清晰新手也能看懂关键逻辑方便二次开发与答辩讲解。压缩包共477个文件约82MB主要包含Python脚本、图片样本、HTML前端页面、JS/CSS等界面与交互资源另有模型权重文件与SQLite数据库构成可展示可运行的前后端闭环。目前已有664人学习使用适合需要快速搭建高分毕设项目或系统学习图像分类实战流程的读者。1. 基于机器学习实现的农作物病虫害识别系统从叶片照片到分类结果的完整链路种过地或者去过田间的人都知道作物叶片一出问题找到农技员确认病害种类往往要等很久而且很多病害在早期长得非常像靠肉眼很难判断。这个基于机器学习实现的农作物病虫害识别系统把这件事做成了一套能跑的代码数据整理、图像预处理、特征提取、模型训练与评估最后还带一个可以直接调用的识别接口。下载下来之后图片是按类别分好目录的标签已经写在文件夹名里不需要再用标注工具手工框选按流程跑就能完整复现识别效果。它适合两类人一类是正在准备 Python 人工智能方向毕业设计、课程设计的学生因为这套流程和论文的章节几乎是逐一对应的另一类是已经在做图像分类、想找一个完整参考实现快速迁移到其他识别场景的工程师。2. 数据准备与标注把杂乱图片整理成模型能吃的输入2.1 图片目录即标签省掉手工标注的第一步这个项目的数据集组织方式是标准的文件夹即标签每个子目录的名字就是类别名目录下所有图片都属于这一类。公开数据集里最常见的命名是 Tomato___Late_blight、Potato___Early_blight 这种作物___病害格式也有直接用中文名的。好处很直接不需要额外的 CSV 或 XML 标注文件遍历目录就能把标签拿全也天然避免了标注错位的问题。我拿到数据集的第一件事是先写一小段脚本把整个目录扫描一遍生成一个包含图片路径、类别名、类别编号的表格。后续的划分、训练、评估全部基于这张表操作不要一边训练一边去翻目录。import os import pandas as pd dataset_dir dataset/PlantVillage class_names sorted(os.listdir(dataset_dir)) label_map {name: idx for idx, name in enumerate(class_names)} records [] for class_name in class_names: class_dir os.path.join(dataset_dir, class_name) if not os.path.isdir(class_dir): continue for img_name in os.listdir(class_dir): if img_name.lower().endswith((.jpg, .jpeg, .png)): records.append({ image_path: os.path.join(class_dir, img_name), label: class_name, label_id: label_map[class_name] }) df pd.DataFrame(records) print(样本总数:, len(df)) print(df[label].value_counts())这段代码先把类别列表按字母排序后生成 label_map保证每个类别的编号稳定。然后逐类别扫描子目录只收 .jpg、.jpeg、.png 三种常见格式。最后的 value_counts 会输出每个类别的样本数一眼就能看出数据集平不平衡。如果某个类别图片特别少后面训练时必须专门处理这个信息非常关键。如果目录名是中文DataFrame 处理没有问题但保存模型和映射表的时候要小心编码。某些 Windows 环境下 Python 默认用 GBK 读文件训练脚本报 UnicodeDecodeError 的话在打开 CSV 或写 label_map 的位置显式加上 encodingutf-8 就能解决。这个细节在答辩现场很常见。2.2 预处理三件套resize、去噪、归一化拿到图片后不能直接喂给模型原因有三点图片尺寸不统一有的 300×300有的 1200×800而模型输入要求固定大小手机拍摄的叶片照片多少带噪点像素值范围是 0-255而大部分机器学习算法在 0-1 或者 -1 到 1 的区间里表现更稳定。所以预处理基本就是三件事——统一尺寸、去噪、归一化。import cv2 import numpy as np def load_and_preprocess(path, size(128, 128)): img cv2.imread(path) img cv2.cvtColor(img, cv2.COLOR_BGR2RGB) img cv2.resize(img, size, interpolationcv2.INTER_AREA) img cv2.medianBlur(img, 3) img img.astype(np.float32) / 255.0 return imgsize 这里给的是 (128, 128)如果后面要上 CNN我会改成 (224, 224)这是预训练网络最常用的输入尺寸也是显存和精度之间的平衡点。medianBlur 是中值滤波核大小取 3 就能去掉大部分椒盐噪声又不会把叶片纹理抹掉太多。归一化直接除以 255把 RGB 三通道压缩到 0-1。注意这一步返回的是 float32 数组后面不管是提取特征还是喂给深度学习框架都是通用格式。2.3 数据集划分与数据增强划分数据集有一条铁律测试集从头到尾不参与训练和调参验证集用来判断模型是否过拟合训练集才是真正学参数的地方。常见做法是先分离测试集再把剩余部分按 7:3 切成训练集和验证集而且必须分层抽样让每个类别的比例在三个集合里保持一致。from sklearn.model_selection import train_test_split train_df, test_df train_test_split(df, test_size0.15, stratifydf[label], random_state42) train_df, val_df train_test_split(train_df, test_size0.15 / 0.85, stratifytrain_df[label], random_state42) print(len(train_df), len(val_df), len(test_df))test_size0.15 表示测试集占全量的 15%第二次划分的 test_size 写成 0.15/0.85是因为要在剩余 85% 里再分出 15% 的验证集这样最终三者的比例是 70:15:15。stratify 参数按 label 分层避免某个类别在测试集里一个样本都没有。random_state 固定成 42保证每次跑出来的划分结果完全一致这对实验复现非常重要没有固定种子的话两次运行结果不同调参就无从谈起。如果走 CNN 路线数据增强要在训练时做。常用的组合是随机水平翻转、旋转 10-15 度、亮度抖动和随机缩放from tensorflow.keras.preprocessing.image import ImageDataGenerator train_datagen ImageDataGenerator( rescale1.0/255, rotation_range15, width_shift_range0.1, height_shift_range0.1, horizontal_flipTrue )增强只作用在训练集验证集和测试集必须保持原图否则评估结果会被增强过的图片污染。另外要注意ImageDataGenerator 的 flow_from_directory 要求目录结构是 train/类别名/*.jpg划分好之后需要把图片按 train、val、test 三个子目录重新归置一遍复制而不是移动防止误删原图。3. 特征提取与模型选型CNN 表现好但 SVM 是一条更省算力的路线3.1 手工特征颜色直方图 GLCM 纹理特征走 SVM 手工特征路线时前面 2.2 的 load_and_preprocess 反而不适用了。特征提取要的是原始像素信息而不是归一化后的浮点图所以下面的 extract_features 函数直接接收 cv2.imread 出来的原图。颜色特征我用 HSV 空间而不是 RGB。原因是 HSV 把色调、饱和度、明度分开光照变化对色调通道的影响远小于对 RGB 三通道的直接影响。纹理特征用 GLCM 灰度共生矩阵它统计的是像素对在某个方向和距离上同时出现的概率。实际使用中我不会把 GLCM 的全部属性都取出来只取对比度、能量、同质性三个指标相关性在叶片病害上的区分度太弱取进来反而增加噪声。import cv2 import numpy as np from skimage.feature import graycomatrix, graycoprops def extract_features(image): image cv2.resize(image, (128, 128), interpolationcv2.INTER_AREA) hsv cv2.cvtColor(image, cv2.COLOR_BGR2HSV) hist cv2.calcHist([hsv], [0, 1], None, [8, 8], [0, 180, 0, 256]) hist cv2.normalize(hist, hist).flatten() gray cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) glcm graycomatrix(gray, distances[1], angles[0, np.pi/4, np.pi/2, 3*np.pi/4], levels256, symmetricTrue, normedTrue) contrast graycoprops(glcm, contrast).mean() energy graycoprops(glcm, energy).mean() homogeneity graycoprops(glcm, homogeneity).mean() return np.hstack([hist, [contrast, energy, homogeneity]])calcHist 的 bins 取 [8, 8]也就是色调和饱和度各分 8 个区间拼出 64 维颜色特征。GLCM 的 distances[1] 表示只统计距离 1 个像素的相邻关系angles 取 0 度到 135 度四个方向然后取均值避免因叶片旋转造成特征突变。levels256 对应灰度图的灰度级normedTrue 让矩阵值归一化成概率。最终特征向量是 64 3 67 维这个维度对 SVM 来说非常友好训练速度可以接受。3.2 SVM 和 CNN 怎么选先看数据和算力做识别系统之前先想清楚走哪条路线。两条路线在毕设场景里都能过区别在论文的侧重点不同SVM 路线重点写特征工程和参数分析CNN 路线重点写网络设计和训练策略。对比项SVM 手工特征CNN数据量需求几千张图片就能训练数据量越大越好万级起步硬件需求CPU 训练分钟级有 GPU 训练小时级特征设计需要手工设计颜色、纹理特征网络自动学习特征可解释性特征含义明确论文好写黑匣子但准确率上限更高调参难度主要调 C 和 gamma学习率、结构、正则化多维度我的判断标准很简单如果手头只有几千张图或者电脑没有独立显卡先跑 SVM 基线。公开的作物病害数据集上SVM 手工特征通常能到 80%-90% 的准确率作为毕设已经完全够用。如果后续发现瓶颈明显再切 CNN 往上冲。项目源码里两种路线都有对应脚本的话正好可以做对比实验这也是答辩时很加分的点。3.3 构造特征矩阵先跑一个基线版本选定了 SVM 路线第一步是把所有训练图片过一遍特征提取组装成特征矩阵。这里有个效率问题用列表推导式逐张处理一千张图可能要一两分钟耐心等不要中途把图片尺寸调得太大128×128 足够。from sklearn.svm import SVC from sklearn.preprocessing import StandardScaler from sklearn.pipeline import make_pipeline def read_and_extract(path): img cv2.imread(path) return extract_features(img) X_train np.array([read_and_extract(p) for p in train_df[image_path]]) X_val np.array([read_and_extract(p) for p in val_df[image_path]]) y_train train_df[label_id].values y_val val_df[label_id].values baseline make_pipeline(StandardScaler(), SVC(kernelrbf, C1.0, random_state42)) baseline.fit(X_train, y_train) print(baseline val acc:, baseline.score(X_val, y_val))为什么要把 StandardScaler 和 SVC 串成 pipeline因为 SVM 依赖样本之间的距离特征不做标准化的话像素直方图这类取值范围大的特征会主导距离计算。用 make_pipeline 包起来fit 的时候只对训练集计算均值和方差验证集用同一套参数做变换避免数据泄漏。C1.0 只是基线值不用纠结下一章网格搜索会统一调。4. 训练评估与调优准确率之外还要盯住 F1 和混淆矩阵4.1 网格搜索C 和 gamma 到底怎么调SVM 的 RBF 核有两个关键超参数C 是误分类惩罚系数C 越大模型越想把每个训练样本都分对容易过拟合gamma 控制 RBF 核的影响半径gamma 越大决策边界越弯曲同样容易过拟合。手动试 C 和 gamma 的组合非常累直接用 GridSearchCV 跑网格搜索。from sklearn.model_selection import GridSearchCV import joblib pipeline make_pipeline(StandardScaler(), SVC(probabilityTrue, random_state42)) param_grid { svc__C: [0.1, 1, 10], svc__gamma: [scale, 0.01, 0.001] } grid GridSearchCV(pipeline, param_grid, cv3, scoringf1_macro, n_jobs-1) grid.fit(X_train, y_train) print(best params:, grid.best_params_) joblib.dump(grid.best_estimator_, svm_model.joblib)param_grid 的键名写成 svc__C 而不是 C是因为 pipeline 内部步骤名为 svc参数要通过步骤名__参数名的形式访问。这个双下划线经常有人漏写报 KeyError 的十有八九是这个问题。scoring 用 f1_macro 而不是 accuracy原因是类别不平衡时 accuracy 没有参考价值。cv3 是 3 折交叉验证数据集不大时够用。n_jobs-1 让所有 CPU 核一起跑网格搜索会快很多。模型训练完成后用 joblib 保存整个 pipeline注意是保存 pipeline 而不是只保存 SVC这样 StandardScaler 的均值和方差也一起存进去了后面部署时少一个文件要管理。同时把标签映射表存成 JSON方便 Web 接口读取import json label_map_json {str(v): k for k, v in label_map.items()} with open(label_map.json, w, encodingutf-8) as f: json.dump(label_map_json, f, ensure_asciiFalse, indent2)4.2 评估指标不要只报一个 accuracy训练完模型之后打印一份完整的分类报告。很多同学只看 accuracy但这个指标在类别不平衡的数据集上会骗人假设健康叶片占 60%模型把每张图都猜成健康accuracy 就有 60%看起来及格了实际一个病害都没识别出来。from sklearn.metrics import classification_report, confusion_matrix y_pred grid.best_estimator_.predict(X_val) print(classification_report(y_val, y_pred, target_nameslist(label_map.keys()))) print(confusion_matrix(y_val, y_pred))classification_report 里每一行有 precision、recall、f1-score 三个值。我最先看每个类别的 recall也就是该类别的真实样本里模型正确找回了多少。recall 最低的那一类就是模型最容易漏的类。比如某个病害类别的 recall 只有 61%说明有近四成的样本被误判成了其他类这通常是病斑早期特征不明显或者和健康叶片的颜色分布太接近。答辩时不要只给一个准确率 95%的结论把分类报告里表现最差的类挑出来解释一两个原因再说明你用什么手段去弥补这个深度和只会跑通代码完全是两个印象分。4.3 如果数据量够用 CNN 把准确率再往上推SVM 手工特征的天花板取决于你手工设计的特征够不够好。当数据量上升到上万张或者你觉得手工特征已经榨不出提升空间就切到 CNN。CNN 不需要设计特征网络自己从像素里学准确率通常能比 SVM 高出五到十个百分点。from tensorflow.keras.models import Sequential from tensorflow.keras.layers import Conv2D, MaxPooling2D, Flatten, Dense, Dropout from tensorflow.keras.callbacks import EarlyStopping model Sequential([ Conv2D(32, (3, 3), activationrelu, input_shape(128, 128, 3)), MaxPooling2D((2, 2)), Conv2D(64, (3, 3), activationrelu), MaxPooling2D((2, 2)), Flatten(), Dropout(0.5), Dense(64, activationrelu), Dense(len(label_map), activationsoftmax) ]) model.compile(optimizeradam, losssparse_categorical_crossentropy, metrics[accuracy]) early_stop EarlyStopping(monitorval_loss, patience5, restore_best_weightsTrue) model.fit(train_images, train_labels, validation_data(val_images, val_labels), batch_size32, epochs30, callbacks[early_stop])loss 用 sparse_categorical_crossentropy是因为我们的标签是整数编号不需要转 one-hot。batch_size 从 32 起步显存不够就降到 16。early stopping 的 patience5 表示验证集 loss 连续 5 轮不下降就停止训练restore_best_weightsTrue 保证最后拿到的不是过拟合那一步的权重。如果训练过程中 loss 剧烈震荡先把学习率从默认的 0.001 降到 0.0001这是最常见的调参手段。5. 避坑图像分类毕设最容易翻车的五个环节5.1 训练集验证集划分不当结果虚高不再可信现象验证集准确率 97%自己找几张图一测只有 65%差距大到离谱。原因图片来自同一个植株或同一次拍摄批次划分时没有做任何分组处理同一个叶片的多张照片同时落进训练集和验证集。模型在训练时已经见过这个叶片的纹理验证时自然认识。另一种可能是 random_state 没有固定每次运行划分结果不同你调参时依据的验证集一直在变。解决固定 random_state42划分前强制 shuffle。如果数据集里同一植株有多张图片按文件名前缀或拍摄批次做分组划分比如文件名前 8 位是植株编号就把相同编号的图片全部划到同一集合里。毕设数据量不大手动写个按前缀分组的函数几分钟就能完成。5.2 预测时漏掉预处理链路接口准确率暴跌现象训练时准确率不错把模型包成接口后传一张图进去返回的标签完全不靠谱。原因训练脚本里写了 resize、去噪、归一化预测代码里直接 cv2.imread 就把原图丢给模型。训练和预测的输入分布不一致模型看到的特征空间完全变了。解决把特征提取和预处理封装成独立模块训练脚本和预测脚本 import 同一个函数。我在项目里习惯把这些函数放一个 feature_extractor.py 里避免两边各写一份。# app 和 train 都从同一个模块导入 from feature_extractor import extract_features # 预测时 features extract_features(img).reshape(1, -1) proba model.predict_proba(features)[0]这是部署翻车率最高的一处没有之一任何图像分类项目都是同一个道理。5.3 类别不平衡时模型永远猜健康现象模型对每张图都输出健康叶片整体准确率还挺高但病害类别一个都识别不出来。原因健康样本在数据集中占 60% 以上SVM 优化的是整体损失多数类贡献的误差更大决策边界自然偏向多数类。解决SVC 里加一个参数 class_weightbalanced让少数类的误分类惩罚权重自动放大。SVC(kernelrbf, C1.0, class_weightbalanced, random_state42)如果加了之后效果还不够对少数类做简单过采样复制少数类样本到多数类的数量级或者用 imbalanced-learn 库里的 SMOTE 合成少数类样本。这一步做完再看 F1比看准确率有意义得多。5.4 训练准确率 99%验证准确率 75%过拟合来了现象训练 loss 持续下降验证 loss 在某轮之后不降反升训练集准确率和验证集准确率的差距越来越大。原因模型把训练集里的噪声和细节背下来了没有学到可泛化的规律。叶子照片里的光照、背景土块、阴影都可能被当成特征。解决先加 EarlyStopping 兜底连续几轮验证 loss 不降就停然后给全连接层加 Dropout比例从 0.5 开始再对训练集做数据增强。如果还是过拟合就减小网络容量CNN 的卷积核数量减半SVM 则把 C 调小、gamma 调小。5.5 换一台电脑模型加载失败现象在自己电脑上 joblib.load 一切正常换到答辩电脑上报错 AttributeError 或者 ImportError还有可能提示找不到模型文件。原因模型里保存了自定义类或使用了不兼容的版本。比如你为了做特征选择写了一个自定义 Transformerpickle 在序列化时会把它所在的模块路径写死换一台机器没有这个模块就加载失败。另一个常见问题是模型路径写的是绝对路径换电脑就失效。解决模型统一用 joblib 保存避免 pickle 序列化自定义类scikit-learn 主版本号在训练机和部署机上保持一致大版本不一致时 SVM 模型经常加载失败路径用相对路径或基于file拼接。from pathlib import Path model_path Path(__file__).parent / model / svm_model.joblib部署前把 requirements.txt 准备好列清楚依赖库和版本范围这是答辩前最容易补救的一项。6. 部署与验证用 Flask 把识别模型包成可用的 Web 接口6.1 一个能直接复用的 Flask 预测接口把模型落地成接口最省事的方式就是用 Flask 包一层 HTTP 服务。核心逻辑只有三步接收上传的图片走和训练时完全相同的特征提取然后返回预测结果和置信度。import json import cv2 import numpy as np from flask import Flask, request, jsonify from joblib import load from feature_extractor import extract_features app Flask(__name__) model load(model/svm_model.joblib) with open(model/label_map.json, r, encodingutf-8) as f: label_map json.load(f) app.route(/predict, methods[POST]) def predict(): file request.files.get(image) if file is None: return jsonify({error: no image uploaded}), 400 data np.frombuffer(file.read(), np.uint8) img cv2.imdecode(data, cv2.IMREAD_COLOR) if img is None: return jsonify({error: invalid image format}), 400 features extract_features(img).reshape(1, -1) proba model.predict_proba(features)[0] idx int(np.argmax(proba)) label label_map[str(idx)] confidence float(proba[idx]) if confidence 0.6: return jsonify({label: unknown, confidence: confidence, message: 置信度过低请更换清晰的叶片照片}) return jsonify({label: label, confidence: confidence}) if __name__ __main__: app.run(host0.0.0.0, port5000)这里直接复用第 3 章的 extract_features保证预测链路和训练链路完全一致。注意 label_map 的键是字符串因为 JSON 序列化时整数键会被转成字符串所以取结果时用 str(idx) 才能匹配上。6.2 用 requests 做一次端到端验证接口写完之后不要只打开浏览器看页面用脚本做一次真正的请求测试确认返回结果和本地 eval 一致。import requests url http://127.0.0.1:5000/predict with open(test_images/late_blight_sample.jpg, rb) as f: resp requests.post(url, files{image: f}) print(resp.json())测试图片一定要选训练集之外、并且和训练图片不是同一植株拍摄的样本。多传几次SVM 是确定性模型同样的图片反复请求应该返回完全一样的结果。如果两次结果不一致检查接口里有没有混入随机操作。6.3 置信度阈值让系统知道它认不出来很多识别系统翻车不是模型不够好而是系统在模型置信度很低的时候仍然硬给一个答案。一张空白背景、手部特写或者完全不相关的图片模型的 softmax 概率分布依然会有一个最大值只不过那个值只有 0.3 左右。加一个置信度阈值过滤低于阈值的统一返回 unknown让前端可以提示用户重新拍摄而不是给一个错误诊断。阈值 0.6 是我在类似项目里的常用起点。如果验证集上的整体表现因为阈值被卡掉过多样本就调低到 0.5如果某个类别频繁出现低置信度误判就单独调高。把阈值当成一个和 C、gamma 一样的超参数来对待而不是写死一个数字。当初我部署完一个版本急着演示跳过了这一步现场传了一张没有叶片的背景图系统信心满满地给出番茄晚疫病置信度只有 0.32。从那以后我每做一个识别接口都会把阈值过滤加上并且每次换数据集就用验证集把阈值重新调一遍。项目源码和数据集的完整内容下载下来之后建议从第 2 章的目录扫描脚本开始跑每一步的输出都先打印出来看一眼再往下走希望帮到你。本文还有配套的精品资源点击获取