ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

基于HOG+SVM的农作物病虫害识别系统:从特征提取到Flask部署实战

基于HOG+SVM的农作物病虫害识别系统:从特征提取到Flask部署实战 简介这是一份面向人工智能、机器学习方向毕业设计或课程项目的农作物病虫害识别系统完整资源包适合需要从零搭建图像分类应用的学生与开发者。系统利用计算机视觉与机器学习算法对农作物叶片图像进行自动检测与分类能够帮助用户快速掌握数据预处理、特征提取、模型训练与部署的整体流程。资源共包含477个文件压缩包约82.03MB核心文件包括Python源码、用于模型训练的pth权重文件、SQLite数据库文件以及大量HTML、CSS、JavaScript、GIF、PNG/JPG等网页端展示与界面素材整合后可直接运行或二次开发。目前已有1323人学习下载内容结构清晰既有数据集与标注信息也包含数个可视化页面和说明文档可辅助理解系统设计思路与关键实现细节。无论是用于毕业设计答辩还是作为机器学习实战练手项目都有较高的参考价值。1. 先想清楚这套作物病虫害识别系统要交付什么python 人工智能毕业设计里基于机器学习实现的农作物病虫害识别系统是一个出现频率很高的选题。它要做的事很具体用户上传一张叶子照片系统判断是哪种病并给出置信度最好还能看到哪几个类别在竞争。难点不在“识别”本身而在于把数据、特征、模型、界面四条线串起来让评审相信你已经完整走通了流程。这篇笔记按我实际做过的一套方案来写覆盖数据集怎么摆、特征怎么提、模型怎么调、Flask 怎么包、以及最容易翻车的地方。适合打算快速出成果、又要在答辩里扛得住追问的本科毕设同学。2. 选机器学习而不是深度学习这个毕业设计的边界与理由很多同学一上来就奔着 YOLO、ResNet 去结果发现数据不够、显卡不够、调参时间更不够。农作物病虫害识别在毕设这个体量下用 sklearn 里的支持向量机或随机森林配合人工设计特征反而更容易交差因为每一环都能在 CPU 上复现答辩时也能把原理讲清楚。这里的机器学习指的是传统图像识别流程预处理、特征提取、分类器三个步骤。下面先说清楚为什么这么选。2.1 数据量小、算力有限的场景传统 ML 反而更稳深度学习不是不好而是它的最低启动成本太高。CNN 一般在每类上千张、整体上万张的数据规模下才有收益如果每类只有三五百张还得靠数据增强防止过拟合训练时间以小时计。而 SVM 在这种规模下往往几十秒就收敛并且对特征质量更敏感反而逼着你去想清楚「这张图里的哪些信息能区分病害类别」。另一个现实问题是设备。实验室电脑没有独显很正常Anaconda 环境下装 TensorFlow 或 PyTorch 能装上但训练时风扇狂转、一个 epoch 要几分钟调参效率极低。传统机器学习用 scikit-learn 和 scikit-imageCPU 就能跑训练和推理都能在一个普通笔记本上完成。这个边界对毕设来说是很实在的优势。再加上答辩层面的考量。深度学习模型像个黑匣子老师问「为什么这张图识别错了」只能答「数据不够」。而机器学习路线里HOG 特征为什么对纹理敏感、SVM 的间隔最大化在解决什么问题、RBF 核如何把低维特征映射到高维这些都是教材里有标准答案的内容。你能接得住问题分数自然不一样。为了更直观看清边界下面这张表是我常用的对比维度对比项传统 MLHOG SVM深度学习CNN每类最少样本量200 张左右可出效果建议 1000 张以上训练硬件CPU 足够强烈建议 GPU单轮训练时间分钟级小时级可解释性特征和核函数可讲基本靠可视化答辩友好程度高中等追问容易卡壳结论很直接毕设的投入产出比传统 ML 路线更高。除非你的题目明确写了要深度学习否则 HOG SVM 是够用的。如果后续想扩展把 SVM 换成一个浅层 MLP 也不难代码框架不用大改。2.2 数据集怎么组织目录、命名与划分这一步比模型还重要。数据集的组织方式决定了后面所有脚本的写法读取方便、标签统一、划分清晰。常见做法是建立 train / val / test 三个目录每个目录下按类别建文件夹文件夹名就是类别标签。下面是可以直接用的命令# 在项目根目录下建立 train / val / test 结构 mkdir -p dataset/{train,val,test} # 为每个病虫害类别创建目录目录名用英文小写加下划线 for cls in apple_scab apple_black_rot apple_rust leaf_healthy \ tomato_late_blight tomato_healthy; do mkdir -p dataset/train/$cls dataset/val/$cls dataset/test/$cls done这段 bash 做了两件事先建立三个数据分区根目录再按类别建子目录。后面的 Python 脚本只要os.listdir(dataset/train)就能拿到全部标签再按子目录收集图片路径规整且不容易出错。类别名不要用中文也不要带空格否则后续 OpenCV 和 Flask 的路由会遇到编码问题。划分比例一般是 train 占 70%、val 占 15%、test 占 15%。如果原始数据按类别目录放好了可以用下面这个脚本按文件区间均匀分到对应分区而不是把所有图片混在一起再随机切分。因为只有从同一个子目录里做分层划分才能保证每个类别在每个分区占比一致。import os, random, shutil src raw_dataset # 原始数据目录每个类别一个子目录 splits {train: [], val: [], test: []} for cls in os.listdir(src): files os.listdir(os.path.join(src, cls)) random.shuffle(files) n len(files) tr_end int(n * 0.7) val_end int(n * 0.85) splits[train] [(cls, f) for f in files[:tr_end]] splits[val] [(cls, f) for f in files[tr_end:val_end]] splits[test] [(cls, f) for f in files[val_end:]] for split, items in splits.items(): for cls, f in items: out_dir fdataset/{split}/{cls} os.makedirs(out_dir, exist_okTrue) shutil.copy(os.path.join(src, cls, f), out_dir)这个脚本用random.shuffle打乱文件列表后按 70%、15%、15% 的绝对区间切分。注意先打乱再切分避免原始目录按时间或拍摄顺序排列时带来的分布偏移。如果你拿到的原始数据里存在某些类别图片极少需要先把小类合并成更粗的类别再走这一步否则验证集会非常脆弱。2.3 先从公开数据集起步用什么、注意什么常见做法是用 PlantVillage 这类公开的叶片病害数据集起步它有苹果、番茄、玉米等作物的常见病害图片类别力矩和背景都相对干净适合验证整体流程。另一个可用的是 AI Challenger 里的农作物病虫害检测子集带边界框标注如果要往检测方向扩展再考虑它。不过要注意公开数据集的图片并不都适合直接进 SVM一部分样本带复杂背景、遮挡或水印需要先筛选掉。建议拿到数据后先做一次人工抽检每个类别随机看 10 张删掉模糊、重复、严重过曝的图。这一步机械化但必要。如果你用的是网上找的「源码数据集」压缩包里面的数据很可能已经被原作者切分过但划分方式不明可能已经有过样本重叠。后面避坑章节会专门讲怎么排查。如果你打算自己采集数据别相信手机随便拍就能用。同一片叶子的正反两面、不同光照、不同角度拍出来的图在模型眼里差异巨大。我自己一般要求每个类别至少 300 张包含光线较暗、清晨露水、叶片卷曲等自然变化。没有这些变化模型在演示现场很容易被一张手机照片打回原形。3. 特征提取与模型训练用 HOG SVM 把识别跑通数据准备好后核心问题就是拿什么特征代表一张叶子。农作物病害在视觉上通常表现为病斑的形状、边缘纹理和颜色变化常见做法是把颜色特征和纹理特征拼接在一起再交给 SVM。我下面给出的组合是颜色直方图 HOG 纹理特征。跟直接把原始像素丢给 SVM 相比这个组合能把单张图压缩到几百维训练快精度也稳。3.1 为什么要组合特征单一颜色特征在田间光线下的局限纯颜色直方图最大的问题是光线。同一片叶子在晴天、阴天、遮阴下拍出来的 RGB 分布差很大而病害类别之间的颜色差异未必比光照差异更大。HOG 描述局部梯度方向能抓住病斑边缘纹理但 HOG 对颜色不敏感叶片枯黄和健康叶子的颜色差异就体现不出来。所以把两类特征串起来用颜色抓住「黄不黄」用 HOG 抓住「斑块形状」。颜色特征建议在 HSV 空间提取而不是 RGB。HSV 把色调、饱和度、亮度分开光照变化主要体现在 V 通道低光照下 H 和 S 仍相对稳定。实际操作是提取 H 和 S 通道的直方图各 32 个 bin得到 64 维HOG 特征用 9 个方向的梯度直方图接在后面。注意拼接前要做标准化否则 SVM 会偏向数值范围大的维度。3.2 训练脚本读取图像、提取特征、训练并保存模型下面的脚本可以直接放进train.py运行。它做的事是遍历训练目录对每张图提取颜色直方图和 HOG 特征构成特征向量留出验证集训练 SVM 并保存模型。import os import numpy as np import joblib import cv2 from sklearn.svm import SVC from sklearn.model_selection import train_test_split from sklearn.preprocessing import StandardScaler from skimage.feature import hog IMG_SIZE (128, 128) H_BINS 32 S_BINS 32 ORIENTATIONS 9 PIXELS_PER_CELL (8, 8) def extract_features(img): img cv2.resize(img, IMG_SIZE) hsv cv2.cvtColor(img, cv2.COLOR_BGR2HSV) hist_h cv2.calcHist([hsv], [0], None, [H_BINS], [0, 180]) hist_s cv2.calcHist([hsv], [1], None, [S_BINS], [0, 256]) hist_h cv2.normalize(hist_h, hist_h).flatten() hist_s cv2.normalize(hist_s, hist_s).flatten() gray cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) hog_feat hog(gray, orientationsORIENTATIONS, pixels_per_cellPIXELS_PER_CELL, cells_per_block(2, 2), block_normL2-Hys) return np.concatenate([hist_h, hist_s, hog_feat])这个函数里cv2.calcHist第一个参数[0]表示 H 通道H 的范围是 0 到 180S 通道范围是 0 到 256。cv2.normalize(hist, hist)默认做 L2 归一化能把不同曝光度的图片拉到一个量级。最后np.concatenate把颜色直方图和 HOG 特征串成一维数组作为样本的特征向量。接着是训练主体X, y [], [] for cls in os.listdir(dataset/train): cls_dir os.path.join(dataset/train, cls) if not os.path.isdir(cls_dir): continue for fname in os.listdir(cls_dir): img cv2.imread(os.path.join(cls_dir, fname)) if img is None: print(skip read failed:, fname) continue X.append(extract_features(img)) y.append(cls) X np.array(X) y np.array(y) scaler StandardScaler() X_scaled scaler.fit_transform(X) X_train, X_val, y_train, y_val train_test_split( X_scaled, y, test_size0.2, stratifyy, random_state42) svm SVC(C1.0, kernelrbf, gammascale, class_weightbalanced, probabilityTrue) svm.fit(X_train, y_train) print(val acc:, svm.score(X_val, y_val)) joblib.dump(svm, models/pest_svm.pkl) joblib.dump(scaler, models/pest_scaler.pkl)这里的stratifyy很重要类别不均衡时它保证训练集和验证集里各类占比一致否则可能出现验证集里某个小类一张都没有准确率虚高。class_weightbalanced让 SVM 自动给少数类更高的惩罚权重。probabilityTrue会启用 Platt 缩放让模型能输出概率后面 Web 系统做 Top-3 置信度显示就靠它。模型和标准化器一起保存推理时不能少任何一边。3.3 三个必调参数C、gamma、HOG 方向数SVM 里影响结果最大的是 C 和 gamma。C 是误分类惩罚系数调大容易过拟合调小容易欠拟合常见做法是在 0.1 到 10 之间做网格搜索。gamma 在 RBF 核里控制单个样本的影响半径gamma 太大决策边界只贴着样本附近过拟合gamma 太小所有样本糊在一起欠拟合。gammascale是 scikit-learn 推荐默认值它会按特征维数自动缩放但换数据集后不一定最优。HOG 参数里orientations影响最大。9 代表把 180 度梯度方向平均分成 9 个区间对叶片这种边缘方向丰富的对象够用提高到 12 或 15 能略微提升细节分辨力但特征维数和计算时间同步上涨128 分辨率下通常不值得。另一个容易被忽略的参数是block_norm建议固定为L2-Hys它对局部光照变化更鲁棒比默认的 L1 稳定。如果想把参数选得更扎实可以用网格搜索但注意别一上来就跑全量from sklearn.model_selection import GridSearchCV param_grid {C: [0.1, 1, 10], gamma: [0.001, 0.01, 0.1, scale]} grid GridSearchCV(SVC(kernelrbf, probabilityTrue), param_grid, cv3, n_jobs-1) grid.fit(X_train, y_train) print(grid.best_params_)网格搜索在高维 HOG 特征上会比较慢。我的习惯是先按 C1.0、gammascale 跑通全流程记录 val acc再小范围搜索。如果默认参数已经到 0.9 以上优先去优化数据划分和特征而不是继续堆算力。4. 把模型包成可用的 Web 识别系统Flask 接口与页面训练完模型评审要看的是「系统」。常见做法是用 Flask 包一个网页前端提供上传控件后端接图、提取特征、调用模型、返回 Top-3 结果。选 Flask 而不是 PyQt是因为 Web 界面不需要装客户端演示时用浏览器就能跑代码量也更小适合毕设这种要快速交付的场景。4.1 为什么用 Flask 而不是 PyQt 包装PyQt 写桌面程序要处理事件循环、布局、文件对话框工作量主要耗在 GUI 上而 Flask 只要一个路由函数就能完成上传和响应前端用 HTML 模板就能跑。Flask 的另一个好处是和 sklearn 模型结合自然进程启动时加载一次模型后续请求只走推断不用重复读文件。代码结构上我一般把特征提取函数放到独立的feature_utils.py训练脚本和 Flask 应用都从这里 import保证训练和推理特征完全一致。如果不这样做训练脚本里改了一个参数、Web 端忘了同步接口就会报维度错误。这个坑在避坑章节还会提到。4.2 推理接口代码上传图片、预处理、返回 Top-3下面是一份可以直接运行的app.py核心逻辑。模型加载放在模块层Flask 启动后只加载一次。import os import numpy as np import joblib import cv2 from flask import Flask, request, jsonify, render_template from skimage.feature import hog from sklearn.preprocessing import StandardScaler app Flask(__name__) MODEL joblib.load(models/pest_svm.pkl) SCALER joblib.load(models/pest_scaler.pkl) CLASSES sorted(os.listdir(dataset/train)) IMG_SIZE (128, 128) H_BINS 32 S_BINS 32 ORIENTATIONS 9 PIXELS_PER_CELL (8, 8) def extract_from_bytes(data): arr np.frombuffer(data, np.uint8) img cv2.imdecode(arr, cv2.IMREAD_COLOR) img cv2.resize(img, IMG_SIZE) hsv cv2.cvtColor(img, cv2.COLOR_BGR2HSV) hist_h cv2.calcHist([hsv], [0], None, [H_BINS], [0, 180]) hist_s cv2.calcHist([hsv], [1], None, [S_BINS], [0, 256]) hist_h cv2.normalize(hist_h, hist_h).flatten() hist_s cv2.normalize(hist_s, hist_s).flatten() gray cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) hog_feat hog(gray, orientationsORIENTATIONS, pixels_per_cellPIXELS_PER_CELL, cells_per_block(2, 2), block_normL2-Hys) feat np.concatenate([hist_h, hist_s, hog_feat]) return SCALER.transform(feat.reshape(1, -1))这段代码里有一个 Web 端很关键的小技巧用np.frombuffer(data, np.uint8)配合cv2.imdecode直接读取上传二进制不依赖文件路径也就不会撞上中文文件名问题。cv2.resize固定到 128 像素保证和训练时一致。最后SCALER.transform把特征标准化后再交给模型。接着是路由app.route(/predict, methods[POST]) def predict(): f request.files.get(image) if f is None: return jsonify({error: 请选择图片}), 400 feat extract_from_bytes(f.read()) prob MODEL.predict_proba(feat)[0] top3 np.argsort(prob)[::-1][:3] result [{label: CLASSES[i], prob: round(float(prob[i]), 4)} for i in top3] return jsonify({top3: result}) app.route(/) def index(): return render_template(index.html)predict_proba返回各类别概率np.argsort(prob)[::-1]做降序排序取前 3 个索引。注意CLASSES的顺序必须和训练时保持一致。如果训练集目录没有变化os.listdir排序也稳定一旦换了机器顺序可能不同所以更稳妥的做法是训练时把CLASSES也存到 pickle 里部署时直接读。4.3 模板页与本地运行上传表单与启动命令前端模板放在templates/index.html上传控件加一点 JavaScript直接调/predictform iduploadForm enctypemultipart/form-data input typefile nameimage acceptimage/* required button typesubmit识别/button /form div idresult/div script document.getElementById(uploadForm).onsubmit async (e) { e.preventDefault(); const data new FormData(e.target); const resp await fetch(/predict, {method: POST, body: data}); const json await resp.json(); document.getElementById(result).innerText JSON.stringify(json.top3, null, 2); }; /scriptfetch在现代浏览器里都能跑演示够用。如果想更美观可以加一个图片预览用 URL.createObjectURL 即可。启动命令很简单export FLASK_ENVdevelopment flask run --port 5000Windows 下把export换成set。启动后访问http://127.0.0.1:5000。开发模式下 Flask 会监听文件改动自动重载但模型文件会被重新加载一次启动变慢。正式演示时用python app.py直接跑更稳。5. 避坑清单这 5 个坑我当年都踩过这套流程看着简单实际跑起来翻车点多在数据组织和路径上。下面按现象、原因、解决写清我踩过的 5 个坑每一条都值得在演示前检查一遍。5.1 训练集和验证集图片来自同一片叶子数据泄露现象训练集准确率 0.99验证集 0.93一到自采的测试集只有 0.72差异非常大。原因很多公开数据集是原始大图切出来的小图切图后被同时分进了训练和验证目录或者同一叶片的多个视角被随机切开模型其实在「认图」而不是「认病害」。解决先按叶片个体分组再按组划分数据集。如果无法追溯原图至少用文件名前缀做分组关键字确保同一前缀只进入一个分区。5.2 类别不均衡准确率虚高召回率拉胯现象某类虫害只有 80 张健康叶片 600 张模型整体准确率 0.93但小类召回率只有 0.3。原因SVM 默认对所有类别同等看待少数类的错误被多数类淹没。解决训练时加class_weightbalanced评估时打印每个类别的 f1-score不要只看总准确率。如果数据悬殊超过 1:5建议对小类做翻转、小角度旋转等简单增强或者采集更多样本。5.3 OpenCV 读取中文路径图片失败imread 返回 None现象训练脚本跑着跑着突然报错打印发现一张图片读出来是 None。原因Windows 上 OpenCV 的cv2.imread不支持中文路径而很多数据集解压后自带中文目录名。解决所有目录名一律改成英文如果已经拿到中文目录先写一个重命名脚本批量处理。也可以统一用cv2.imdecode(np.fromfile(path, dtypenp.uint8), cv2.IMREAD_COLOR)绕过imread的限制。我的习惯是一开始就全用英文命名把路径坑从根上断掉。5.4 训练和部署的特征流水线不一致维度对不上现象训练时好好的部署时接口报dimension mismatch。原因训练脚本里resize写的是(128, 128)Flask 里写成了(64, 64)或者训练时没保存 StandardScalerWeb 端直接把原始特征丢给模型。SVM 对尺度敏感训练和推理必须完全同一条流水线。解决把特征提取和标准化封装成同一个函数放在公共模块里训练脚本和 Flask 都调用它模型和 scaler 同时保存同时加载。5.5 模型路径硬编码换机器就找不到文件现象自己电脑跑得好好的拷到答辩机器上 Flask 启动报FileNotFoundError。原因代码里写的models/pest_svm.pkl相对路径依赖当前工作目录而 IDE 启动和命令行启动的工作目录可能不一样。解决用基于__file__的绝对路径拼接BASE_DIR os.path.dirname(os.path.abspath(__file__)) model_path os.path.join(BASE_DIR, models, pest_svm.pkl)这样无论从哪个目录启动都能找到模型。这是最省心的路径写法。6. 往答辩里加分的验证技巧混淆矩阵、单类指标与置信度阈值模型能跑只是及格答辩加分项在于你证明了「模型为什么可接受」。建议在测试集上做一次完整评估而不是只报一个准确率。下面这段代码输出每个类别的精确率、召回率、f1-score以及混淆矩阵方便你在论文实验部分直接截图。from sklearn.metrics import classification_report, confusion_matrix import pandas as pd y_pred MODEL.predict(X_test) print(classification_report(y_test, y_pred, target_namesCLASSES)) cm confusion_matrix(y_test, y_pred) df_cm pd.DataFrame(cm, indexCLASSES, columnsCLASSES) print(df_cm)看结果时重点盯召回率偏低的类别它们通常对应病斑不明显的早期症状。如果某个类经常被误判成健康叶片说明特征里缺少对这个病害最关键的颜色或纹理线索。这时不要急着换模型先回训练集采样几张看看这些图片的共性问题往往比换算法更有效。另一个实用技巧是给置信度设阈值。SVM 的predict_proba严格说不是真实概率尤其类别多时最高概率可能只有 0.4。我会在返回结果时过滤掉置信度过低的预测改成输出「无法判断」而不是硬给一个结果。这样在演示现场用手机随手拍一张陌生的图系统会诚实地告诉你置信度低而不是给出一个看着就错的三类答案。我最后一次做这个项目时把混淆矩阵和置信度阈值说明都放进了系统右侧栏答辩时老师顺着界面问到了「低置信度怎么办」我把代码指出来讲了阈值的选择思路这个问题就算接住了。这也是我做每个图像识别毕设都会保留的习惯先把识别率做出来再把「它什么时候会认错、系统怎么回应」做成功能给评审的信任感会高很多。希望帮到你。本文还有配套的精品资源点击获取
返回列表