资讯动态

机器视觉害虫检测实战:从数据标注到YOLO模型调参与计数

发布时间:2026/10/9 12:39:23 来源:尧图企业网站定制
简介基于机器视觉的害虫种类及数量检测毕业设计项目面向农业物联网与图像识别方向的学生和研发人员旨在缓解植保人力紧张、实现远程自动识别提供一套从田间硬件到服务器端识别的完整解决方案。压缩包共165个文件包含23个Python源码、3个训练模型、13个npy权重数据、10个xml标注、3个csv数据集、97张昆虫图像及1个UI界面整体大小14.6MB其中npy为权重、xml为标注、csv为数据表便于对照源码理解处理流程。已有241人学习下载。项目涵盖野外害虫诱捕装置设计、基于轮廓二值化的昆虫计数方法以及矩形度、延长度、圆形度、球状性、叶状性等形态特征提取并对比逻辑斯蒂回归、线性支持向量机与邻近分类器的识别性能适合作为毕业设计选题参考也可支撑后续害虫监测系统的算法优化。1. 基于机器视觉的害虫种类及数量检测这个题目到底在解决什么问题做农林害虫监测的人多半经历过这种场景灯诱设备一夜收回上百张粘虫板照片人坐在电脑前逐张数虫、认种一张图几百个斑点多得让人眼花。基于机器视觉的害虫种类及数量检测就是把这个人工环节交给模型输入一张田间或粘虫板图像输出图中每个害虫的类别标签和边界框再按类别统计数量。放在毕业设计里这是一个典型的目标检测应用技术链路清晰、成果可视化程度高、量化指标也直接。整套方案通常由数据准备与标注、检测模型训练与调参、按图或按帧统计数量的后处理以及最后在测试集上的验证与部署组成。适合准备拿这个题目做毕设、手头有一批虫情图像想跑通全流程的人也适合刚接触目标检测、想用一个真实小场景把工程链路走完的开发者。2. 数据准备与标注死磕这一步训练才不玄学很多人的血泪经验是模型效果不好九成问题出在数据而不是网络结构。害虫检测尤其如此虫体小、形态多变、背景又杂如果前期数据整理粗糙后期调参再怎么折腾都像在碰运气。这一章先把数据链路定下来后面训练才有依据。2.1 数据采集的四个原则与目录结构先别急着找开源数据集先想清楚任务对图像的要求。害虫检测的数据采集要守住四条底线一是覆盖不同虫态卵、幼虫、蛹、成虫形态差异极大只拍成虫会让模型对早期虫态完全失灵二是覆盖多环境田间叶片、粘虫板、白炽灯下的采集板都要有不同拍摄面料的纹理差异会直接影响泛化三是覆盖多尺度既要有整板远景也要有单虫近景保证模型见过大小差异明显的目标四是必须包含空背景负样本没有害虫的粘虫板、叶片、土壤背景一样要拍这是后面抑制误检的重要材料。通常我会把数据集组织成下面的结构图片和标签一一对应文件前缀保持一致pest_dataset/ ├── images/ │ ├── train/ │ ├── val/ │ └── test/ ├── labels/ │ ├── train/ │ ├── val/ │ └── test/ ├── classes.txt └── pest.yaml目录结构的逻辑很简单images 和 labels 是两棵平行的树训练时数据加载器按同名文件去 labels 里找对应标注。一个常见坑是把标注直接和图片放在同一个目录后面切分数据集时脚本容易误操作分开存放切分逻辑会清爽很多。classes.txt 里每行写一个类名顺序必须和标注文件里的类别 id 严格对应这是训练环节对不上的高频故障点。还有一点要提醒原始照片不要提前压缩或 resize 后存档保留原图。训练时模型会自己按输入尺寸缩放提前缩图等于主动丢掉小目标信息后面的精度损失很难补回来。2.2 标注工具与格式转换VOC 与 YOLO 怎么选标注阶段常见的选择是在 VOC 格式和 YOLO 格式之间二选一。VOC 格式用 XML 描述每个目标的类别和左上角、右下角坐标适合可视化查看YOLO 格式用纯文本每行记录类别 id 和归一化中心点坐标、宽度、高度训练脚本读取更直接。实际做的时候很多标注工具导出的是 VOC 格式而主流 YOLO 训练仓库需要用 YOLO 格式的 txt因此一个可靠的转换脚本是必备的。# xml_to_yolo.py import xml.etree.ElementTree as ET import os def convert_xml_to_yolo(xml_path, out_dir, class_names): tree ET.parse(xml_path) root tree.getroot() img_w int(root.find(./size/width).text) img_h int(root.find(./size/height).text) lines [] for obj in root.iter(object): name obj.find(name).text if name not in class_names: continue box obj.find(bndbox) xmin float(box.find(xmin).text) ymin float(box.find(ymin).text) xmax float(box.find(xmax).text) ymax float(box.find(ymax).text) # 归一化并转换为 YOLO 的中心点 宽高格式 cx (xmin xmax) / 2.0 / img_w cy (ymin ymax) / 2.0 / img_h bw (xmax - xmin) / img_w bh (ymax - ymin) / img_h lines.append(f{class_names.index(name)} {cx:.6f} {cy:.6f} {bw:.6f} {bh:.6f}) out_path os.path.join(out_dir, os.path.splitext(os.path.basename(xml_path))[0] .txt) with open(out_path, w) as f: f.write(\n.join(lines) \n) if __name__ __main__: class_names [瓢虫, 叶蝉, 飞蛾] convert_xml_to_yolo(data/001.xml, labels_train, class_names)代码的关键在于坐标换算VOC 的绝对坐标除以图像宽高得到归一化值再把 xmin 和 xmax 取中点得到中心 x。这里有个隐藏问题如果标注框超出了图像边界归一化后可能出现大于 1 或小于 0 的坐标训练时会被过滤掉或导致损失异常。建议在循环里做强裁剪把 xmin、ymin 限制不小于 0xmax、ymax 不超过宽高。面积太小的框比如小于整图千分之一的目标在转换时可以直接丢弃这类框多半是标注失误留着只会干扰训练。2.3 按目录切分数据集避免数据泄露的划分脚本数据切分不能直接对整个文件列表做随机抽样这是新手最容易踩的坑。害虫图像常按拍摄批次组织同一批照片的光照、角度、虫密度高度相似如果随机切分同批图片会同时出现在训练集和验证集模型相当于提前见过答案验证指标虚高换一批真实场景图立刻露馅。# split_dataset.py import os import random import shutil def split_dataset(img_src, label_src, out_root, ratios(0.8, 0.15, 0.05), seed42): random.seed(seed) imgs [f for f in os.listdir(img_src) if f.lower().endswith((.jpg, .jpeg, .png))] random.shuffle(imgs) n len(imgs) n_train int(n * ratios[0]) n_val int(n * ratios[1]) for idx, img in enumerate(imgs): if idx n_train: split train elif idx n_train n_val: split val else: split test img_dst_dir os.path.join(out_root, images, split) label_dst_dir os.path.join(out_root, labels, split) os.makedirs(img_dst_dir, exist_okTrue) os.makedirs(label_dst_dir, exist_okTrue) shutil.copy(os.path.join(img_src, img), os.path.join(img_dst_dir, img)) stem os.path.splitext(img)[0] label_src_path os.path.join(label_src, stem .txt) if os.path.exists(label_src_path): shutil.copy(label_src_path, os.path.join(label_dst_dir, stem .txt)) if __name__ __main__: split_dataset(images_raw, labels_raw, pest_dataset)脚本本身不复杂但两个细节决定了可靠性一是随机种子固定多次运行结果可复现方便对比实验二是图片和标签必须同步搬运漏掉任何一个文件训练时数据加载器会在中途报错。更进一步的做法是按拍摄日期分组把一天内的照片整体划入同一个集合而不是逐张打散。这样切出来的验证集才真正模拟“换一天去拍”的泛化考验。训练集、验证集、测试集比例 8:1.5:0.5 在这个场景下比较常见测试集可以很小但必须保证来自模型完全没见过的拍摄批次。3. 模型选型与训练用 YOLO 做害虫检测的配置与调参数据链路准备好之后下一步是把检测模型跑起来。这个环节的核心不是把某个网络的训练脚本跑通而是搞清楚为什么选目标检测、参数为什么这么设以及怎么判断模型训练得健不健康。3.1 为什么选检测网络而不选图像分类网络做害虫种类识别直觉上会先想到图像分类把每张图送进网络输出一个类别标签。但题目里还有“数量检测”四个字分类网络解决不了位置问题一张图里有三只叶蝉和两只飞蛾分类网络只能告诉你“图里有飞蛾”不能告诉你“飞蛾在哪、有几只”。想用分类网络做计数得先滑窗切图再逐块分类最后还要按位置合并去重流程繁琐且误差累积严重。目标检测网络把定位和分类合并在一次前向中完成直接输出目标的边界框、类别和置信度计数只是对输出框做统计链路干净可靠。在害虫场景里YOLO 这类单阶段检测器是更务实的选择检测精度和速度平衡好现成开源实现多训练和部署资料齐全对毕业设计这种需要短时间内跑通全流程的项目非常友好。实际做的时候我一般直接选用现有开源 YOLO 实现而不是从零写检测头把精力留给数据和后处理这才是投入产出比更高的做法。3.2 数据配置文件与超参设置示例使用现成开源 YOLO 实现训练时第一步是写数据配置文件。这个文件告诉训练脚本数据集在哪、类别有哪些。下面是一个典型的配置# pest.yaml path: ./pest_dataset train: images/train val: images/val names: 0: 瓢虫 1: 叶蝉 2: 飞蛾path 指向数据集根目录train 和 val 是相对根目录的图片文件夹路径names 的索引顺序必须和标注文件里的类别 id 完全一致。很多人训练时报 “class not found” 或指标错乱排查到最后基本都是这里和标注对不上。训练命令通常长这样python train.py --data pest.yaml --epochs 300 --batch 16 --imgsz 1280 --device 0 --patience 50参数选择要结合害虫检测的特点来看。imgsz 设成 1280 而不是默认的 640是因为虫体在整张图里占比通常很小输入分辨率提高一倍小目标在特征图上的像素数就多得多漏检率会明显下降代价是显存占用和训练时长同步增加显存不够时可以先降到 960。batch 16 在常规单卡下是个稳妥起点batch 越小训练随机性越大loss 曲线越抖。epochs 300 配合 patience 50意思是连续 50 轮验证指标不上升就提前结束既能防过拟合也能省时间。数据增强方面mosaic 增强在密集小目标场景下有时会让目标被裁剪得过于零碎如果训练后期验证集精度一直上不去可以把 mosaic 概率调低或关闭再试。3.3 从训练日志判断模型是否健康训练不是跑完就结束关键是能读懂训练日志。YOLO 训练时会在输出目录生成 results.csv里面记录了每一轮的 box_loss、cls_loss、mAP 等指标。我习惯把关键曲线画出来看趋势# plot_metrics.py import pandas as pd import matplotlib.pyplot as plt results pd.read_csv(runs/train/exp/results.csv) fig, axes plt.subplots(1, 3, figsize(12, 3)) axes[0].plot(results[epoch], results[train/box_loss], labeltrain box_loss) axes[0].plot(results[epoch], results[val/box_loss], labelval box_loss) axes[0].set_title(box loss) axes[0].legend() axes[1].plot(results[epoch], results[metrics/mAP_0.5], labelmAP0.5) axes[1].plot(results[epoch], results[metrics/mAP_0.5:0.95], labelmAP0.5:0.95) axes[1].set_title(mAP) axes[1].legend() axes[2].plot(results[epoch], results[metrics/precision], labelprecision) axes[2].plot(results[epoch], results[metrics/recall], labelrecall) axes[2].set_title(PR) axes[2].legend() plt.tight_layout() plt.savefig(training_curves.png, dpi150)几个判断标准供参考训练 loss 和验证 loss 同步下降是健康状态训练 loss 还在降验证 loss 开始反弹说明过拟合box_loss 下降正常但 mAP 长期不涨多半是标注噪声大比如框不贴合目标或类别标错mAP0.5 高但 mAP0.5:0.95 低意味着模型只会在宽松交并比下有不错表现框的定位精度不够对计数场景影响相对小但如果要做精细位置分析就要回头检查标注质量。比总 mAP 更值得关注的是每个类别的单独 AP。害虫检测通常存在类别不平衡总体 mAP 被样本多的类别拉高样本少的类别可能已经崩了。训练结束后的验证结果里一般会列出每个类别的 precision 和 recall逐类看一遍哪类低于 0.5 就要重点处理处理办法在下一章的避坑环节展开。4. 实测避坑五个让害虫检测翻车的常见问题这一章直接给结论每一条都是实际做这类项目时反复出现的问题按“现象、原因、解决”的顺序写清楚省得你再走一遍弯路。4.1 标注框太紧导致训练不收敛现象训练时 loss 下降缓慢或不下降训练出来的框总是比虫体轮廓大一圈或者在同一只虫附近来回漂移。原因标注时为了让框“完美贴合”虫体把边界紧贴触角或虫体边缘导致模型学到的正样本缺了必要的上下文像素同类目标的框在宽高比上差异巨大边界回归目标不稳定。解决统一标注规范画框时在目标四周留出约 5% 到 10% 的边距让框包含一小圈背景目标检测在回归边界时反而更稳。 如果你已经有一批过紧的标注写完标注再批量膨胀即可# inflate_boxes.py def inflate_label_line(line, ratio0.08): cls, cx, cy, bw, bh line.split() cls int(cls) cx, cy, bw, bh float(cx), float(cy), float(bw), float(bh) bw2 min(bw * (1 ratio), 1.0) bh2 min(bh * (1 ratio), 1.0) return f{cls} {cx:.6f} {cy:.6f} {bw2:.6f} {bh2:.6f}注意膨胀是在归一化坐标上直接改宽高中心点不变所以虫体位置不会偏移。膨胀比例 0.08 意味着宽高各增加 8%视觉上就是一周多了一小圈边。这个脚本可以作为标注规范执行前的补救手段但不能替代规范本身。4.2 小目标害虫大量漏检现象图片里明明有几十只虫模型只检出零星几只检出的还都是个头最大的。原因虫体在整幅图中占比太小经过网络下采样后小目标在高层特征图上只剩下几个像素特征几乎被背景淹没。解决从三个方向同时下手。一是提高输入分辨率imgsz 从 640 提到 1280 通常能显著改善小目标召回二是推理阶段把大图切成小块分别检测后合并结果三是训练时保证小目标样本在数据增强中被保留而不是被随机裁剪掉。切块推理的思路很直接但实现时有几个注意点切块要有重叠一般重叠 20% 到 50%避免目标正好被切在边界上所有块检测完后要把坐标映射回原图再做一次全局 NMS把重叠区域产生的重复框合并。伪代码逻辑如下# sliced_inference.py def infer_large_image(model, img, patch_size512, overlap0.25): h, w img.shape[:2] all_boxes [] for y in range(0, h, int(patch_size * (1 - overlap))): for x in range(0, w, int(patch_size * (1 - overlap))): patch img[y:y patch_size, x:x patch_size] boxes model(patch) # 得到当前块的检测结果 # 把坐标加上偏移量映射回原图 for box in boxes: box[x1] x; box[x2] x box[y1] y; box[y2] y all_boxes.append(box) return apply_global_nms(all_boxes, iou_thres0.45)代价是推理耗时成倍增加对离线图片统计完全可接受但如果是实时视频流要谨慎权衡。4.3 背景变化引起误检现象模型在训练集所在的背景上表现很好换一批新背景图后把叶片边缘、水渍甚至虫板边框识别成害虫置信度还不低。原因目标检测模型天然会利用背景纹理做捷径学习特别是害虫体积小、与背景对比度低时模型可能学到的是“粘虫板纹理 小暗斑”这样的组合特征而不是害虫本身。解决采集并加入大量空背景负样本让模型见过“没有目标但背景相似”的图片。负样本处理很简单字段内容为空即可for img in empty_bg/*.jpg; do stem$(basename ${img%.jpg}) touch labels_empty/$stem.txt done这些空标签文件告诉训练脚本该图没有目标模型会从中学到“这片区域没有虫”而不是把所有深色斑点都当作目标。加入负样本后误检率会明显下降。另一个有效手段是推理时提高置信度阈值但这是被动应对真正可靠的还是让训练数据覆盖更多背景类型。4.4 数量统计中的重复计数现象一张密集虫板上模型输出的框互相重叠统计数量时一只虫被算了两次甚至三次。原因同一目标被多个候选框命中NMS 阈值设置不当或置信度阈值过低导致重叠框被保留。解决严格后处理。检测模型输出的原始结果先做一次 NMS然后按类别统计统计时还要过滤低置信度框# count_pests.py def count_by_class(preds, conf_thres0.35): # preds 是 NMS 后的检测结果: [[cls, conf, x1, y1, x2, y2], ...] counts {} for cls_id, conf, x1, y1, x2, y2 in preds: if conf conf_thres: continue counts[cls_id] counts.get(cls_id, 0) 1 return counts置信度阈值不是拍脑袋定的应该从验证集的 PR 曲线上找拐点。曲线上 precision 和 recall 的交点附近通常是一个平衡点拿这个点对应的置信度作为统计阈值误计和漏计都能兼顾。如果是视频流场景重复计数问题更复杂同一只虫在连续帧里被多次统计。简化做法是每隔几帧抽样统计一次或者用目标跟踪做帧间匹配把同一轨迹只计一次。毕业设计场景下离线图片按图统计是主任务先把单图去重做好。4.5 类别不平衡让单类精度崩盘现象训练日志里总体 mAP 挺高但逐类看指标样本量少的那个类别 recall 极低甚至完全检不出。原因模型在训练时被样本量大的类别主导少数类学到的特征不足正样本梯度被稀释。解决优先做数据增强而不是急着换 loss 函数。对少数类图片做旋转、亮度扰动、随机裁剪后重采样让模型在每轮训练里都能见到足够多的少数类样本。这里有一个实操细节增强后的图片要和原图区分命名避免和原图在验证集里“亲戚相见”导致指标虚高。增强样本只加入训练集绝不要加入验证集和测试集。如果增强后单类 AP 还是上不去再考虑在损失函数里给少数类加权重。多数开源 YOLO 实现里可以按类别设置 loss 权重权重值从 1.2 到 2.0 之间尝试加太大容易让模型对少数类过拟合导致误检上升。5. 验证与进阶把检测结果变成可靠的计数出口5.1 用一批未参与训练的现场图做端到端验证训练指标只能说明模型在验证集上表现不错真正要回答的问题是“换一批新图模型数出来的数量对不对得上人工数”。我会专门留出一个完全没参与训练和验证的现场图文件夹跑一次端到端验证统计每个类别的漏检数和误检数而不是只看 mAP。# evaluate_on_test.py # 假设已有模型推理结果 preds 和人工标注 gts def evaluate_count(preds, gts, iou_thres0.5, conf_thres0.35): tp, fp, fn 0, 0, 0 matched_gt set() for p in preds: if p[conf] conf_thres: continue best_iou 0 best_gt None for idx, g in enumerate(gts): if idx in matched_gt: continue iou compute_iou(p[box], g[box]) if iou best_iou: best_iou iou best_gt idx if best_iou iou_thres: tp 1 matched_gt.add(best_gt) else: fp 1 fn len(gts) - len(matched_gt) precision tp / max(tp fp, 1) recall tp / max(tp fn, 1) return precision, recall, tp, fp, fn这份指标比 mAP 更贴近题目里的“数量检测”precision 反映数出来的数量有多少是错的recall 反映真实害虫有多少被漏掉。建议把每张测试图的检测结果可视化保存下来框、类别、置信度直接画在图上人工快速扫一遍比看数字更容易发现问题。5.2 后处理计数逻辑置信度过滤与 NMS 阈值到了部署端计数逻辑要独立成一个函数不依赖训练代码。模型导出成 ONNX 格式后加载和推理都轻量得多常见的现成 YOLO 实现都带导出脚本导出后输入输出形状固定输入是 [1, 3, H, W] 的张量输出经过 NMS 后是每个目标的类别、置信度和边界框坐标。量化方面FP16 精度基本无损失INT8 能大幅提速但通常会在小目标上损失部分 recall所以虫情计数场景我更倾向于用 FP16。最后的计数出口看起来简单但有一个习惯让它真正可靠在代码里记录每次推理使用的置信度阈值和 NMS 阈值连同统计结果一起存下来。这样换一批图结果波动时能立刻判断是模型退化还是阈值漂移。我现在拿到害虫检测这类题目都会从第一天就把“模型数出来的数对得上人工数”当作验收标准每换一次数据就全量跑一遍计数评估可视化结果按日期归档。这个习惯让我几乎不会在项目收尾时返工。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑