简介:基于Python机器视觉的害虫种类及数量检测项目,面向高校毕业设计与课程设计场景,适合计算机视觉、农业信息化方向的本科生和研究生作为实战练习。项目覆盖图像预处理、特征提取、机器学习/深度学习建模、结果评估与数据可视化等完整流程,并附环境配置说明,便于快速运行。压缩包共163个文件,包含23个Python脚本、97张JPG图像样本、10个XML标注、3个模型文件及CSV数据表、PDF说明文档等,整体约14.58MB;目录结构清晰,能直接对应数据处理、训练与预测等模块。目前已有171人学习下载,可用于课题设计、答辩展示或进一步改进识别算法,是兼顾工程实现与理论学习价值的优质项目包。
1. 机器视觉也能数害虫:一份能跑通的 Python 毕业设计底子
做毕业设计最怕拿到一个空壳项目,没有数据也没有完整链路。这份基于 Python 机器视觉实现害虫种类及数量检测的课程设计项目,难得的是同时带上了训练表、测试表和问题表三份 CSV 数据,还有一组害虫图片样本。覆盖了从图像读取、预处理、特征抽取、模型训练到结果评估的完整流程,适合直接拿来做农业图像识别方向的毕设底子。拿到手之后,我第一件事不是跑模型代码,而是先把 CSV 和图片的对应关系理清楚——这一步理不顺,后面所有识别结果都是空中楼阁。想快速搭出完整技术栈的应届生、要给学生一个真实可运行任务的老师,都适合从这份资源开始。
2. 数据集结构确认与图像预处理:先让害虫图像“干净”起来
2.1 先看清三份 CSV:data.csv、datatest.csv、ques.csv 分别装了什么
解压压缩包后,直接可见的是 data.csv、datatest.csv、ques.csv 和若干 fly 开头图片,另外还有一个 Insect_Identification.iml 的 IDE 模块文件。iml 文件说明这个项目在 PyCharm 或 IntelliJ 系工具里跑过,环境上用 Python 3.x 即可。真正决定项目能不能跑通的,是那三张表怎么读、怎么对齐。
在图像识别类的毕业设计里,有一个非常常见的设计:先把所有图片做预处理和特征提取,把结果整理成特征表存成 CSV,再交给 sklearn 训练。这样做的好处是特征表体积小、训练速度快、答辩时也好解释“特征工程”这一环。所以这里的三份 CSV,大概率不是原始图片,而是特征向量加标签的组合。data.csv 通常是训练集,datatest.csv 是测试集,ques.csv 是待预测的问题集,可能没有标签,要靠模型预测后写回去。
拿到手先别急着训练,我一般第一步是打印三张表的形状、列名和前几行。下面的脚本可以直接抄:
import pandas as pd train_df = pd.read_csv("data.csv") test_df = pd.read_csv("datatest.csv") ques_df = pd.read_csv("ques.csv") print("train shape:", train_df.shape) print("test shape:", test_df.shape) print("ques shape:", ques_df.shape) print("train columns:", train_df.columns.tolist()) print("test columns:", test_df.columns.tolist()) print(train_df.head(3)) print(train_df.iloc[:, -1].value_counts())这段脚本的逻辑是先看 shape,shape[0] 是样本数,shape[1] 是列数。columns 能确认训练和测试的特征列名是否一致,这是最容易翻车的地方。最后一列的value_counts()能看出类别分布是不是均衡,比如到底是二分类害虫还是多分类。如果训练集最后一列是标签,那特征就应该取train_df.iloc[:, :-1],标签取train_df.iloc[:, -1]。datatest.csv 如果只有特征列没有标签列,那它的任务就只是预测并输出结果。
如果解压后发现 .py 脚本不在明面上,别慌。只要三份 CSV 的结构确认了,训练代码十分钟就能补出来。特征列和标签列的对应关系,就是这个项目最重要的信息。
2.2 图像预处理:灰度化、高斯滤波、直方图均衡化的顺序与参数
图片不会直接进模型。田间拍到的害虫图受光照、叶片阴影、尘土干扰很大,直接提特征噪声太重。预处理的核心目的是把虫体从背景里“拎”出来。我给这个项目配的预处理函数是灰度化、高斯滤波、直方图均衡化三步走,顺序是有讲究的:先灰度化降通道,再滤波去噪,最后均衡化拉对比度。
import cv2 import numpy as np def preprocess_image(image_path, size=(224, 224)): img = cv2.imread(image_path) if img is None: raise ValueError(f"图片读取失败: {image_path} | 检查路径是否含中文") gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) gray = cv2.GaussianBlur(gray, (5, 5), 0) gray = cv2.equalizeHist(gray) gray = cv2.resize(gray, size) return gray逻辑说明:cvtColor把 BGR 转灰度,是因为灰度图保留了亮度信息,又去掉了颜色噪声;GaussianBlur的核 (5, 5) 适合中等颗粒噪声,sigma 设 0 表示让 OpenCV 按核大小自动算标准差;equalizeHist是全局直方图均衡化,把偏暗或偏亮的叶片背景拉回正常范围,浅色害虫在深色背景上会明显很多;最后resize到固定尺寸,保证后面提取特征时维度一致。
这里有个使用细节:size不要随意改。如果后续接 HOG 特征,窗口尺寸必须和图像尺寸一致,224×224 是一个比较稳妥的默认值。如果追求速度,可以降到 128×128,特征维度会少很多。我在跑这个项目的时候,是先跑一遍预处理函数,把每张图单独存出来看一遍,确认虫体没有被滤波抹掉才开始提特征——这一步花五分钟,能省后面调模型两小时。
2.3 形态学后处理:二值化与开运算的边界
如果图片背景是绿色叶片、虫体颜色偏深,预处理后还可以加二值化和形态学操作,把虫体区域变成白底黑块的连通区域。这里有一个容易踩的坑:开运算的 kernel 不能贪大。
_, binary = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU) kernel = np.ones((3, 3), np.uint8) opened = cv2.morphologyEx(binary, cv2.MORPH_OPEN, kernel)逻辑说明:cv2.threshold配合 Otsu 标志,会根据灰度直方图自动算出一个分割阈值,不需要手工调;MORPH_OPEN是先腐蚀后膨胀,作用是去掉虫体边缘的毛刺和小噪点。kernel 用 3×3 起步,因为有些细长害虫的触角和腿很细,kernel 太大容易把虫体腐蚀断,导致后面的特征提取完全失真。我实际调试时从 5×5 试过,效果反而不如 3×3,这个参数在不同图像分辨率下差异很大,建议按像素大小调,别直接抄网上大核参数。
3. 特征提取与模型选型:把图像变成 SVM 能吃的一行特征
3.1 特征到底提什么:HSV 颜色直方图与 HOG 的互补逻辑
直接拉平像素喂 SVM 是最常见的翻车操作:维度上万、对光照和位移极其敏感。害虫识别这个场景里,不同害虫之间的区分点主要在两方面:一是颜色,二是轮廓和纹理。颜色特征我用 HSV 颜色直方图,形状纹理特征我用 HOG,两套特征拼起来再进模型。
为什么用 HSV 而不是 RGB?因为害虫图是田间拍的,光照变化剧烈。RGB 的三个通道对光照变化都很敏感,而 HSV 里的 H(色调)通道相对稳定,绿色叶片和褐色虫身、黑色虫身之间的色相差异不会因为太阳角度变化而消失。S(饱和度)和 V(明度)能反映环境光强,但它们噪声大,特征提取时通常只取 H 通道或把三个通道的直方图都算出来再拼接。
HOG 负责回答“形状像不像”。它统计图像局部区域内的梯度方向分布:翅膀边缘、腿的轮廓、肚子弧度,这些都会在梯度方向直方图上留下明显差异。相比 SIFT 和 SURF,HOG 不需要检测关键点,对整幅图做密集统计,特别适合这种单目标占画面主体的图片。SIFT 的优势是尺度和旋转不变,但这在害虫识别里反而容易过度敏感,而且计算量大;ORB 虽然快,但在模糊叶面背景下匹配稳定性一般。所以这个项目里 SVM 路线我首选 HOG 加颜色直方图,稳定且答辩时好解释。
| 特征 | 回答的问题 | 对光照敏感性 | 维度量级 | 本项目适用性 |
|---|---|---|---|---|
| HSV 直方图 | 什么颜色 | 中等 | 几十到几百 | 高 |
| HOG | 什么形状 | 低 | 几千 | 高 |
| SIFT | 什么关键点模式 | 低 | 与关键点数相关 | 中 |
| ORB | 什么关键点模式 | 中 | 与关键点数相关 | 中 |
我把三种特征都试过一遍之后,最终留的是 HSV 直方图加 HOG 的组合。SIFT 不是不好,而是特征维度不稳定,样本量小时容易出现过拟合,对毕设这种几百张样本的数据集反而难调。
3.2 把一张害虫图片变成一行特征向量的代码
下面这段函数是在这个项目里可以直接复用的特征提取代码,输入一张图片路径,输出一个一维数组,这个数组就是 CSV 里一行特征列的来源。
def extract_features(image_path, hist_size=32): img = cv2.imread(image_path) if img is None: raise ValueError(f"读取失败: {image_path}") hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV) hist_h = cv2.calcHist([hsv], [0], None, [hist_size], [0, 180]) cv2.normalize(hist_h, hist_h, norm_type=cv2.NORM_L2) gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) gray = cv2.resize(gray, (256, 256)) hog = cv2.HOGDescriptor((256, 256), (16, 16), (8, 8), (8, 8), 9) hog_feat = hog.compute(gray).flatten() feat = np.hstack([hist_h.flatten(), hog_feat]) return feat逻辑说明:calcHist只统计 H 通道,[0, 180]对应 OpenCV 里 H 通道的取值范围;hist_size=32把 0 到 180 分成 32 个区间,区间越小特征越细但越容易过拟合。normalize用 L2 范数,让直方图总和变成 1,这样不管图片亮度高低,特征向量都在同一个尺度上。HOG 部分先把灰度图 resize 到 256×256,再以 (16, 16) 为 cell、(8, 8) 为 block 步长、9 个方向 bin 计算,得到几千维的梯度方向特征。最后np.hstack把两个特征拼成一行。
这里要特别强调一点:cv2.HOGDescriptor的窗口尺寸必须和输入图像尺寸一致。如果图像不是 256×256,而窗口保持 (256, 256),compute会直接报错。我习惯把尺寸定义成常量,预处理和特征提取共用同一个值,避免两处不一致。
3.3 模型怎么选:SVM、随机森林、KNN 的对比与 SVM 实战
特征维度在几千左右、样本量在几百到几千之间时,SVM 是性价比最高的选择。随机森林对噪声稳健,但特征维度高时容易过拟合;KNN 逻辑最简单,但预测时要遍历全部样本,而且维度越高越容易受距离度量失效影响。CNN 理论上上限最高,但这个数据量下很容易练不出泛化能力,毕设里除非有大量增强数据,不然传统机器学习更容易拿分。
训练代码骨架如下:
from sklearn.model_selection import train_test_split from sklearn.preprocessing import StandardScaler from sklearn.svm import SVC from sklearn.metrics import classification_report X = train_df.iloc[:, :-1].values y = train_df.iloc[:, -1].values X_train, X_val, y_train, y_val = train_test_split( X, y, test_size=0.2, stratify=y, random_state=42 ) scaler = StandardScaler() X_train = scaler.fit_transform(X_train) X_val = scaler.transform(X_val) model = SVC(kernel='rbf', C=10, gamma='scale') model.fit(X_train, y_train) print(classification_report(y_val, model.predict(X_val)))参数说明:stratify=y按类别比例划分训练和验证集,害虫数据常常一个类样本特别多、另一个类特别少,不 stratify 很容易让验证集里某个类一个样本都没有;StandardScaler把每个特征缩放到均值 0、方差 1,防止面积类的大数值特征压过直方图类的小数值特征;SVC里C=10表示对误分类的惩罚比较大,gamma='scale'是根据特征维度自动适配的默认值,小样本先这么跑,之后再看混淆矩阵决定要不要调。
如果是随机森林路线,把第四段换成RandomForestClassifier(n_estimators=200, random_state=42)即可,效果差距一般不太大。两个模型都能跑通之后再细看评估指标,下面一章说的就是这事。
4. 评估指标与结果可视化:识别准确率之外更要看漏检率
4.1 准确率之外:漏检比错分更危险的害虫识别场景
分类准确率是最直观的指标,但在害虫检测里它不够用。假设数据集里 90% 是无虫叶片、10% 有虫,一个把所有图片都判成“无虫”的模型,准确率也有 90%,可它一个害虫都没发现——这对农业场景是完全失败的模型。
害虫检测里最致命的错误不是把 A 虫认成 B 虫,而是压根没看出有虫。漏检意味着没打药、没上报疫情、虫害在扩散。所以评估时应该优先看召回率(recall),也就是“所有真实的害虫图片里,模型找出了多少”。精确率管的是“模型说是有虫的图片,到底有多少是真有虫”,这个指标也很重要,但优先级低于召回率。
具体到项目里,可以用下面的代码逐类看指标,而不是只看一个总准确率:
from sklearn.metrics import classification_report y_pred = model.predict(X_val) report = classification_report(y_val, y_pred, output_dict=True) for cls in model.classes_: info = report[str(cls)] print(f"类 {cls}: precision={info['precision']:.3f}, recall={info['recall']:.3f}, f1={info['f1-score']:.3f}")这段代码把classification_report的结果转成字典,然后按类别逐个打印。正常模式下 sklearn 默认只打印所有类别的平均结果,转成output_dict=True之后才能逐个类看。如果发现某个类别 recall 明显低于其他类,优先怀疑样本数量不够或特征区分度不足,而不是马上调模型参数。
4.2 混淆矩阵可视化与每类别召回率检查
混淆矩阵能一眼看出哪些类别之间互相干扰。比如两种蛾类害虫颜色相近,模型反复把它们互相认错,靠准确率根本看不出来,混淆矩阵第二眼就能定位到。
import matplotlib.pyplot as plt from sklearn.metrics import confusion_matrix cm = confusion_matrix(y_val, y_pred, labels=model.classes_) plt.figure(figsize=(8, 6)) plt.imshow(cm, cmap="Blues") plt.colorbar() plt.xticks(range(len(model.classes_)), model.classes_, rotation=45) plt.yticks(range(len(model.classes_)), model.classes_) plt.xlabel("预测类别") plt.ylabel("真实类别") plt.tight_layout() plt.savefig("confusion_matrix.png", dpi=150)逻辑说明:confusion_matrix的labels参数强制指定类别顺序,否则矩阵的横纵轴顺序可能和训练类别不一致,看起来像识别错了实际没错;imshow配合cmap="Blues"展示热力分布,颜色越深代表数量越多,对角线越亮说明识别效果越好。保存成图片之后直接放进论文结果章节,比纯文字表格更有说服力。
这里要提醒一句:如果用了 seaborn 画热力图,注意 seaborn 的heatmap会把矩阵数值标注出来,但它的坐标轴标签默认不显示。我一般在sns.heatmap里显式传xticklabels=model.classes_和yticklabels=model.classes_,不然图是出来了,别人根本不知道哪个格子对应哪个类。
4.3 对 datatest.csv 做批量预测并生成结果表
评估指标满足要求之后,要拿测试集跑一遍最终预测,把结果写回 CSV。这里最容易犯的错误是对测试集重新做了一遍fit_transform,但这必须用训练好的scaler直接transform,否则数据分布被改变两次,测试结果会莫名崩掉。
test_X = test_df.iloc[:, :].values test_X = scaler.transform(test_X) pred = model.predict(test_X) test_df["predicted_label"] = pred test_df.to_csv("datatest_result.csv", index=False)逻辑说明:第一行取所有列作为特征,前提是确认过 datatest.csv 里没有标签列也没有 id 列;如果有 id 列,要改成test_df.iloc[:, 1:]。scaler.transform用的是训练集拟合好的均值方差,这一点必须注意。最后把预测结果写回 DataFrame 再保存,答辩时给老师看的就是这份 datatest_result.csv,逻辑清晰。
如果 ques.csv 也参与了最终交付,同一个流程再跑一遍,输出文件取名 result.csv,保持和项目里原本的文件命名习惯一致。这三张表处理完,整个“训练评估预测”闭环就完整了。
5. 避坑手册:跑通这个害虫检测项目的五个常见问题
没跑过这个方向的人,最容易在环境配置和数据对齐上掉链子,模型反而放到最后。下面五条都是我在实际拆这个包时踩过的坑,按“现象 → 原因 → 解决”写成,照着排查比从头看报错快得多。
5.1 OpenCV 读中文路径图片返回 None
现象:cv2.imread("data/害虫图片/fly11.jpg")返回None,后面预处理全部报错,图显示出来是黑的。
原因:OpenCV 的imread对中文路径支持不好,Windows 下只要路径里带“害虫”这种中文目录就大概率读不出来。
解决:把整个项目目录改成纯英文,所有路径用正斜线。如果必须处理带中文路径的图片,用cv2.imdecode:
import cv2 import numpy as np img = cv2.imdecode(np.fromfile("D:/pest_data/害虫/fly11.jpg", dtype=np.uint8), cv2.IMREAD_COLOR)np.fromfile以二进制模式读取文件内容,绕开imread的路径解析,再把字节数组交给imdecode解码。这个方法在 Windows 下处理中文路径图片是常见做法,我几乎每个图像项目都会备一个。
5.2 训练集与测试集特征列顺序不一致
现象:训练时 acc 正常,一到预测 datatest.csv 就报 “X has N features per sample; expecting M”,或者分数奇低。
原因:data.csv 和 datatest.csv 的特征列顺序不一样,或者 datatest 前端多了一列 id 没删掉。
解决:先打印两个表的columns对比,再统一取数方式。我给这个项目写排查脚本时,习惯先跑print(test_df.columns.tolist()),一旦发现有 id 列,就改成test_df.iloc[:, 1:]。严格的做法是训练和预测都按列名取特征子集并排序,保证列顺序完全一致。这个坑是毕设项目里最隐蔽的,因为两表结构看着一样,实际差一列,肉眼根本看不出来。
5.3 特征表里混入 NaN 和字符串
现象:训练时报Input contains NaN,或者could not convert string to float。
原因:CSV 原始数据里某个单元格是空的,或者采集时把“无”写到了数值列里,数据类型不再是纯 float。
解决:先定位再清洗:
print(train_df.isnull().sum()) for col in train_df.columns: if train_df[col].dtype == "object": print(col, train_df[col].unique()[:10])逻辑说明:isnull().sum()按列统计空值数量,能快速定位到污染列;循环里检查 object 类型的列,找出混进去的字符串。清洗时用train_df[col] = pd.to_numeric(train_df[col], errors="coerce")把非数值强行转 NaN,再统一fillna(0)或dropna()。这个流程我每次读取外部 CSV 都会强制走一遍,看起来多花三十秒,实际省的是后面整个训练环节的排查时间。
5.4 新版 OpenCV 里 SIFT 接口报错
现象:代码写的cv2.xfeatures2d.SIFT_create(),运行时报module 'cv2' has no attribute 'xfeatures2d'。
原因:OpenCV 4.x 之后xfeatures2d模块从默认的opencv-python移到了opencv-contrib-python里,而且 SIFT 接口也换成了cv2.SIFT_create()。
解决:环境里只装一个 OpenCV 包,别同时装opencv-python和opencv-contrib-python,否则会导致cv2模块被覆盖,部分函数找不到。我建议固定使用opencv-contrib-python==4.5.5.64,代码里写成cv2.SIFT_create()。如果确认项目用的是 SIFT 特征,这一步必须在第一天就配好,不然临到答辩前装环境最容易翻车。
5.5 对测试集重复 fit_transform
现象:训练集和验证集指标都正常,测试集结果却惨不忍睹,感觉像玄学。
原因:对测试集单独调用了scaler.fit_transform(test_X),让模型看到了测试集的统计量,而测试集和训练集的均值方差不同,特征分布被改掉了,模型自然不稳定。
解决:训练集只fit_transform一次,测试集和预测集永远只用transform。我通常把这句写成注释放在代码里:# 这里必须是 transform,不是 fit_transform。从那以后我每次写完评估流程,都强制检查数据预处理部分有没有出现第二次 fit,这个动作已经成为习惯了。
6. 数量检测技巧:用滑动窗口让单张图输出“种类+数量”
6.1 用滑动窗口把“单图分类”变成“位置检测”
前面的模型回答的是“这张图里有哪种害虫”,没法回答“图里有几只”。数量检测需要把图切块,一块一块判。常用做法是固定大小的窗口按步长扫过全图,每块独立走一遍预处理、特征提取、模型预测,超过置信度阈值的窗口就当成一个候选害虫区域。
def slide_detect(img, model, scaler, step=32, thresh=0.7): h, w = img.shape[:2] boxes = [] for y in range(0, h - 64, step): for x in range(0, w - 64, step): patch = img[y:y+64, x:x+64] feat = extract_features_from_patch(patch) score = model.decision_function(scaler.transform([feat]))[0] if score > thresh: boxes.append([x, y, x + 64, y + 64, score]) return boxes逻辑说明:窗口固定 64×64,步长 32 意味着相邻窗口有一半重叠,宁可重复也不漏检;decision_function返回的是 SVM 超平面距离,不是 0 到 1 的概率,所以thresh=0.7只是一个经验起点,要在验证集上试几个值再定。步长越小检出越稳但越慢,图片大时建议先缩小到宽度 800 像素以内再滑窗,不然跑一轮要好几分钟。
6.2 NMS 合并重叠框后统计数量:数量检测的最终输出
滑窗之后同一个害虫会产生好几个重叠框,直接用框的数量当虫数会虚高。非极大值抑制(NMS)是目标检测里最常见的后处理,思路很简单:置信度高的框先留着,其他跟它重叠度高的框全部丢掉。
def nms(boxes, iou_thresh=0.3): boxes.sort(key=lambda b: b[4], reverse=True) keep = [] for b in boxes: if all(iou(b, k) < iou_thresh for k in keep): keep.append(b) return keep这里iou就是两个框交集面积除并集面积,重叠越多越接近 1。iou_thresh=0.3偏严格,只有重叠很严重的框才合并,防止两只离得近的害虫被并成一个;如果测试集上发现虫体边缘经常只框出一半,可以把阈值调到 0.5 左右再试。最终害虫数量就是len(nms(候选框)),种类则由每个框对应模型预测类别决定,数量和种类两个指标同时拿到手。
从那以后我每次拿到图像类的毕设项目,都会先跑一遍滑窗检测,去看漏检的窗口长什么样——这个动作不到一个小时,却比调一个月参数更管用。数量检测是分类模型落地到实际场景的一条捷径,希望帮到你。
本文还有配套的精品资源,点击获取