ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

基于机器视觉的害虫种类与数量检测:模型选型与工程落地避坑指南

基于机器视觉的害虫种类与数量检测:模型选型与工程落地避坑指南 简介一份面向农业信息化与计算机视觉方向的毕业设计项目完整实现了基于机器视觉的害虫种类及数量检测。资源包含Python源码、训练模型、昆虫图像数据集及文档适合作为图像识别、数据分析类课题的参考。包体共165个文件涵盖23个py脚本、97个jpg样本、13个npy特征数据、10个xml标注文件以及模型、CSV统计表等整体约14.6MB结构清晰。已有241人学习下载。通过该资源可系统掌握昆虫图像二值化预处理、轮廓计数以及矩形度、延长度等形态特征提取方法并了解逻辑斯蒂回归、SVM、K邻近分类器的对比应用还能借鉴远程害虫自动识别系统的整体设计思路。1. 基于机器视觉的害虫种类及数量检测先想清楚这三件事再动手拿着“基于机器视觉的害虫种类及数量检测”这个题目做毕业设计很多同学的第一反应是赶紧训练一个模型但真做起来才发现卡人的往往不是模型精度而是数据标注、数量统计和现场泛化。这个题目本质上是“检测计数”的组合任务输入一张作物图像模型要先输出每个害虫的位置和类别再按类别统计个数。适合计算机视觉方向的学生也适合想用低成本视觉方案做农业监测的入门者。难点集中在小目标漏检、遮挡重叠、光照变化和计数口径四个点上。下面从选型、数据、训练到落地踩坑逐步讲清楚照着做能省下大量试错时间。2. 害虫检测系统的整体架构与模型选型从图像到数量要过哪几关2.1 为什么用检测模型而不是分类模型位置信息是计数的前提常见的误区是把“害虫种类及数量检测”拆成“分类计数”。有同学先用分类模型判断整张图是哪种害虫再去数数量结果一到现场就翻车。原因是分类模型只输出图像级别的标签它并不关心虫子长在图片的哪个位置当图里同时出现两种害虫时分类模型根本没法给出每种的数量。计数的基础是定位你得先知道“哪里有虫子”才能回答“每类有几只”。检测模型天生自带定位能力它输出一组边界框每个框包含类别、置信度和坐标。数量统计时直接对每个类别的框计数即可。如果场景里虫子密集、严重重叠检测框会互相覆盖计数会偏少。这时候可以考虑实例分割模型它能给出像素级轮廓但也意味着标注工作量成倍增加。对毕业设计来说检测模型是性价比最高的方案标注成本可控公开资料多能在几个月内跑完整链路又足够支撑问题说明。从项目链路看一个完整的害虫检测系统大致要过四关图像采集与标注、模型训练、数量统计模块、界面或输出呈现。每一关都有各自的坑。先别急着敲代码而是先把这四关的输入输出定下来。图像采集决定数据分布标注格式决定训练入口数量统计逻辑决定最终评价指标能不能闭环。很多中期检查没通过的项目都是因为没想清楚数据流一上来就调模型最后整个演示流程像拼凑出来的零件课设答辩时一问就露馅。2.2 模型选型YOLO系列、轻量版与边缘设备的取舍检测模型里当前毕业设计最常见的选型是YOLO系列。它把检测做成端到端的回归问题速度快、部署资料多、训练生态也成熟适合没有太多底层优化经验的学生。与之对比Faster R-CNN这类两阶段模型精度上限可能更高尤其在小目标场景下有优势但训练和推理速度都慢调参细节多如果设备是普通笔记本每次跑实验都像煎熬。还有一条路线是用轻量分类骨干网络配合SSD或MobileNet-SSD这类方案对边缘设备友好但需要自己写训练代码的部分逻辑Debug成本高。对大部分毕业设计我建议用YOLO系列作为主力理由有三点第一数据标注格式和训练脚本的“坑”网上都有公开记录第二模型本身带有数据增强、多尺度训练等机制适合新手从“能跑通”起步第三后期如果要接入树莓派或摄像头YOLO的轻量版本能直接复用。下面是几个常见方案的对比你可以根据自己手头的设备和数据量来选方案小目标能力推理速度标注成本适合情况YOLO系列中等调参后可提升快中数据量中等、需要快速完成系统Faster R-CNN较强慢高数据集质量高、对精度要求高MobileNet-SSD较弱很快中需要跑在低算力设备上实例分割模型强慢很高遮挡严重且必须精确计数实际选型时不要只看指标还要考虑你所在实验室或导师熟悉的工具链。如果团队里有人用过YOLO你踩坑时能有人问这会比模型本身差的那点精度重要得多。反过来如果项目要求最终跑在一台只有CPU的机器上那YOLO的轻量版也未必扛得住需要进一步做剪枝和量化这属于进阶内容后面会提到。模型选型这个环节不需要追求最新关键看两周内能不能自己独立跑通一个最小例子。2.3 典型项目目录与数据流可交付的毕业设计长什么样很多毕业设计最后卡在交不上东西不是因为模型没训出来而是代码和数据的组织方式一团乱。我一般会建议项目采用下面这种目录结构把数据、代码、权重和结果分开方便自己调试也方便答辩展示pest_detection/ ├── data/ │ ├── images/ │ │ ├── train/ │ │ ├── val/ │ │ └── test/ │ ├── labels/ │ │ ├── train/ │ │ ├── val/ │ │ └── test/ │ └── data.yaml ├── scripts/ │ ├── split_dataset.py │ ├── voc2yolo.py │ └── count_pests.py ├── weights/ │ └── best.pt ├── runs/ │ ├── train/ │ └── val/ ├── configs/ │ └── pest_config.yaml └── requirements.txt其中data.yaml是训练阶段必须的数据配置里面写清楚类别名称、训练/验证图片路径以及类别数量。scripts目录放所有自己写的处理脚本比如数据集划分、格式转换和数量统计这些代码在答辩时很容易成为加分项因为它体现了工程能力而不是只调了一个现成模型。数据流是原始图像 → 标注工具生成坐标 → 转成训练格式 → 划分训练/验证/测试 → 读入data.yaml训练 → 得到权重 → 推理脚本输出检测框 → 计数模块按类统计。建议把每一步都做成独立脚本保留中间产物这样出问题能定位到具体环节。不要在一个文件里把所有逻辑揉在一起否则后期的“玄学”问题会多到让你怀疑人生。目录结构里还要注意权重文件体积很大如果同步到网盘或Git仓库要确认平台对大文件的限制。可以直接把weights/和runs/加入忽略列表答辩提交时单独打包。中间产物也不要乱放训练日志按时间命名比如exp_0912_1400这样将来复盘时能知道当时改了哪些参数而不是盯着一个叫final的文件夹发呆。3. 数据集准备与标注训练前最耗时的环节不能省3.1 图像采集数量、角度、光照怎么定每次看到有人拿着两三百张图训练完就说“识别效果差”我第一反应多半是数据没到位。害虫检测对数据数量和质量的要求比一般物体检测更高因为害虫个体小、类间相似度高一张图里可能同时出现几类近似的虫子。常见做法是每类至少准备300张以上带有标注的图片并且覆盖不同拍摄距离、角度和光照条件。拍摄时不要只拍正对着的清晰大图要多拍一些带遮挡、带背景干扰的图比如叶子背后的虫、泥地里的虫。还要注意把患病的作物叶片和正常叶片也拍进来因为模型很容易把病斑、虫咬痕迹当成害虫这是现场识别差的重要原因。如果实在凑不够真实图片可以先从公开数据集找同类害虫图像做预训练再用自己的少量图片微调但要清楚标注尺度的一致性不然迁移后框的位置会不匹配。光照上要有意识地在不同时段采集早晨、正午、傍晚各来一部分。不要全部用闪光灯补光否则模型会把高光区域和害虫混淆。背景复杂度也要记录这对后面调整模型输入尺寸有帮助。采集阶段多花三天能省下后面两星期的训练调参时间这笔账很划算。如果项目有条件可以用手机支架固定拍摄角度分多个高度拍摄同一片叶片这样数据里既有远景又有近景模型对小目标的适应能力会明显改善。3.2 标注工具与导出格式从LabelImg到YOLO格式的转换细节标注工具可以用开源的LabelImg或Label Studio。LabelImg轻量适合单机标注Label Studio支持团队协作和多种标注类型但配置稍复杂。标注时先建立类别列表尽量保持类别名稳定不要一会儿写“pest_1”一会儿写“fly”。命名不规范会让后面的数据配置反复返工。标注完成后LabelImg默认保存成Pascal VOC的XML文件而YOLO系列训练需要的是每个图片对应一个txt文件每行是class_id x_center y_center width height其中坐标是经过图像宽高归一化后的值。转换代码并不复杂但归一化这一步最容易出错写错后训练出来的框要么偏移要么变成一条线。下面是一个通用的VOC转YOLO脚本import os import xml.etree.ElementTree as ET def voc_to_yolo(xml_path, target_path, class_names): tree ET.parse(xml_path) root tree.getroot() img_width int(root.find(size/width).text) img_height int(root.find(size/height).text) lines [] for obj in root.findall(object): name obj.find(name).text if name not in class_names: continue class_id class_names.index(name) xmin int(float(obj.find(bndbox/xmin).text)) ymin int(float(obj.find(bndbox/ymin).text)) xmax int(float(obj.find(bndbox/xmax).text)) ymax int(float(obj.find(bndbox/ymax).text)) x_center ((xmin xmax) / 2) / img_width y_center ((ymin ymax) / 2) / img_height width (xmax - xmin) / img_width height (ymax - ymin) / img_height lines.append(f{class_id} {x_center:.6f} {y_center:.6f} {width:.6f} {height:.6f}) with open(target_path, w) as f: f.write(\n.join(lines)) if __name__ __main__: classes [pest_a, pest_b] for xml_file in os.listdir(annotations): if not xml_file.endswith(.xml): continue voc_to_yolo( os.path.join(annotations, xml_file), os.path.join(labels, xml_file.replace(.xml, .txt)), classes )这段代码把annotations目录下的所有XML逐个解析提取每个目标的类别和边界框坐标然后按YOLO格式写入对应txt文件。关键的参数是classes列表它的顺序必须和后面data.yaml里的类别顺序一致顺序错位会导致类别标签全部张冠李戴。另外如果图片中存在翻转后的标签建议保留原图方向避免坐标参考系混乱。转换完成后要记得检查输出txt比如看坐标值是否都在0到1之间宽度和高度是否为正数。常见问题是有些标注框超出了图片边界如果不做裁剪归一化后的坐标可能小于0或大于1训练时会被框架直接丢弃或报错。建议在转换脚本里加边界裁剪逻辑把这些异常框压回图片范围内。更省事的办法是写一个批量校验函数遍历所有txt文件发现异常立即打印文件名和行列号别让脏数据一路带到训练阶段。3.3 数据集划分与增强用Python脚本一键完成数据划分要保证训练集和验证集来自不同图像千万不要把同一张图的不同增强版本同时放进训练集和验证集否则验证分数虚高现场一测就现原形。最简单的做法是按文件名随机划分。下面脚本会把每张图片对应的图片和标签一起移动并输出三个集合的文件列表import os import random import shutil random.seed(42) image_dir data/images_all label_dir data/labels_all train_dir, val_dir, test_dir data/images/train, data/images/val, data/images/test label_train, label_val, label_test data/labels/train, data/labels/val, data/labels/test ratios [0.7, 0.2, 0.1] for d in [train_dir, val_dir, test_dir, label_train, label_val, label_test]: os.makedirs(d, exist_okTrue) images [f for f in os.listdir(image_dir) if f.endswith(.jpg)] random.shuffle(images) train_count int(len(images) * ratios[0]) val_count int(len(images) * ratios[1]) for i, img_name in enumerate(images): label_name img_name.replace(.jpg, .txt) if i train_count: img_dst, label_dst train_dir, label_train elif i train_count val_count: img_dst, label_dst val_dir, label_val else: img_dst, label_dst test_dir, label_test shutil.copy(os.path.join(image_dir, img_name), os.path.join(img_dst, img_name)) shutil.copy(os.path.join(label_dir, label_name), os.path.join(label_dst, label_name)) print(f划分完成训练集 {train_count} 张验证集 {val_count} 张测试集剩余 {len(images) - train_count - val_count} 张)脚本里的random.seed(42)用来固定随机种子保证每次划分结果一致。ratios是按照7:2:1划分训练、验证、测试实际调整时要注意验证集不能太小至少要有几十张否则精度评估的波动会很大。这里用的是copy原始图片还保留一份避免误删后要重新采集。至于数据增强我建议优先用训练框架内置的在线增强比如随机翻转、缩放、色彩抖动和马赛克增强而不是把增强后的图片直接写进磁盘。离线增强只适合数据量特别小的情况而且会成倍增加训练时间。你可以把增强参数放在训练配置里让每次epoch随机生成不同的训练样本相当于免费扩大了数据量下面一章会具体说。如果非要做离线增强记得给文件名加前缀区分比如aug_flip_001.jpg否则后续想溯源数据来源时完全对不上号。4. 训练检测模型一次跑通的命令与关键参数4.1 环境配置与预训练权重选择环境配置是整个项目里最容易出现“翻车”的地方。常见坑是显卡驱动、深度学习框架和编译环境版本不匹配。动手之前先确认自己电脑有没有独立显卡显存多大。没有GPU的话可以考虑使用云端GPU或只训练非常小的模型。建议用Anaconda创建一个独立环境避免把系统环境弄乱。在项目根目录创建虚拟环境并安装依赖命令大致如下conda create -n pest python3.8 conda activate pest pip install -r requirements.txtrequirements.txt里至少要有图像处理库、深度学习和训练框架相关的包。不同框架版本之间的兼容性很容易出问题建议先装深度学习框架再装其他依赖。如果没有特殊原因不要追求最新版本选择当前稳定版本即可。装完之后运行一个简单的导入测试能顺利执行说明环境基本可用。别在一开始就折腾CUDA手动配置先确认预编译的包能不能直接用很多时候是额外装的东西把环境搞乱了。预训练权重是训练能否早点收敛的关键。常见做法是使用在大型公开数据集上训练过的权重做初始化哪怕这些数据集里没有害虫模型也已经学会了边缘、纹理和基础形状特征。对毕业设计来说从预训练权重开始微调通常只需要几百张标注图就能达到可用效果。相反如果从随机权重开始训练不仅慢而且很容易过拟合到你的小数据集上。4.2 训练配置img size、batch、epochs、学习率怎么配对训练配置里几个核心参数是输入图像尺寸img、批大小batch、训练轮数epochs、学习率lr。它们不是独立调节的互相之间有很强的联系。输入图像尺寸直接决定小目标在特征图上的大小。害虫普遍小我建议img至少设为640如果显存充裕且虫子更小可以加到960或1280但推理速度会明显变慢。batch由显存决定显存不足时优先减小batch不要一开始就开很大的batch去赌模型能收敛。epochs要看训练曲线不要固定一个值比如先跑100轮如果验证集还在持续降低就继续加50轮。学习率推荐从框架默认值附近的0.01开始用余弦退火或自定义衰减策略。如果训练开始阶段loss暴涨往往不是学习率问题而是数据标注或归一化出错了。下面是一组针对害虫小目标场景比较稳妥的初始参数表参数推荐值调参方向img640小目标多就上调显存不足就下调batch16显存不足降为8或4配合梯度累积epochs100以验证集指标为基准未收敛继续加lr0.01模型发散就降为0.001optimizerSGD或AdamW小数据集用SGD更稳定参数表只是起点。真正要盯的是验证集的mAP和PR曲线训练集loss低不代表模型好只能说明它在背训练数据。每次只改一个参数记录结果不要一次动多个变量不然你根本分不清是哪个改动起了作用。这也是很多毕业设计中期答辩时被导师连环追问时答不上来的原因。4.3 训练命令与日志解读从loss到mAP怎么看进入训练阶段我一般会在YOLO开源仓库的目录下执行类似下面的命令python train.py \ --data data.yaml \ --weights pretrained.pt \ --img 640 \ --batch 16 \ --epochs 100 \ --cache \ --project runs/pest \ --name experiment_01这条命令的意思是用data.yaml指定数据集用预训练权重初始化输入尺寸为640批大小16训练100轮--cache将图片预加载到内存中加快读取--project和--name用来保存每次实验的输出日志和权重。data.yaml里的nc必须和标注类别数一致names要和标注脚本里的类别顺序一致这两处是训练前最值得反复核对的地方。训练过程会打印两类日志一类是每个batch的loss另一类是每个epoch结束后的验证指标。刚开始训练时box loss和cls loss会同时下降这属于正常现象。如果loss在几十个epoch后显示为nan先检查学习率是否过大、基础环境是否稳定再到标注数据里找是否有空标签文件或异常坐标。不要盲目重启训练先定位再重训。验证阶段重点看两个数字mAP50和mAP50-95。mAP50指IoU阈值0.5时的平均精度它更贴近“检测出来没”这个直觉mAP50-95是多个IoU阈值下的平均更能反映定位质量。论文里通常两个都放如果只想写一个建议用mAP50-95因为它更严格也更不容易被质疑。另外要看每个类别单独的AP某个类别AP明显偏低就说明这个类别数据量不足或实例太像其他类需要补数据而不是继续调参。5. 害虫检测常见问题与避坑五个翻车现场和补救方案5.1 小目标害虫漏检不是模型不行是输入尺寸和锚框没调现象图片里很小的害虫在训练集上能检测到但验证集里漏检率很高尤其是一张图里有多个小虫子时模型只输出了其中一两个。原因输入图像默认尺寸偏小小目标经过多次下采样后在特征图上只占几个像素特征信息几乎消失另外默认锚框大小是针对通用目标设计的对害虫这种小目标覆盖不足。解决先把img提高到960或以上再看看漏检率是否有改善。如果显存不够可以降低batch并用梯度累积或者裁剪原图分区域推理。同时打开多尺度训练让模型适应不同大小的目标。还有一个容易被忽略的点是标注框如果比原图缩小很多说明目标本身就极小这种情况下需要在损失函数里提高小目标分支的权重或者改用专门针对小目标的检测头。调参方向比盲目换网络结构重要先用最简单的办法验证到底是不是尺寸问题。5.2 数量统计反复不对重叠、遮挡与边界框如何取舍现象检测出来的框是对的但按类别计数时总比实际少严重的重叠目标被合并成了一个框或者一个目标被重复计数。原因检测模型对密集重叠目标输出的框经常互相覆盖如果计数逻辑按“非极大值抑制后的框”直接数重叠目标会被丢弃反之如果模型对同一目标产生了多个高置信度框又会被重复统计。解决计数不能直接拿模型输出框数要先按类别分组再用置信度阈值和IoU后处理把重叠框合并。对于遮挡特别严重的图片可以考虑把计数区域划分为网格只统计网格内部的框跨越边界的框按中心点归属避免在视野边缘反复进入导致重复计数。另一种做法是引入跟踪算法对视频中的每一帧做跟踪关联按轨迹去重这会把问题复杂度提高不少但如果课题是视频监测这一步反而是核心创新点。计数口径一定要在需求阶段定清楚是按一张静态图统计还是按视频一段时间的最大值统计不同口径得到的结果差很多。5.3 显存不足或训练过慢batch size、workers、fp16的组合现象训练刚开始没几分钟就报CUDA out of memory或者batch明明不大但每个epoch时间很长。原因显存不足一般不是单因素而是输入尺寸、batch、数据加载线程和模型参数量叠加的结果。训练过慢则常常是数据读取速度成了瓶颈GPU在等CPU。解决显存不足时先降batch降到4还不行就降img。如果不想降低指标可以在配置文件里开启梯度累积把有效batch变大。数据加载方面把workers设为CPU核心数的1到2倍并开启--cache让图片常驻内存。还可以用半精度训练显存占用直接减半速度也更快但要注意少部分运算在不支持半精度的库上会报错这时需要禁用半精度相关选项。还有一个容易踩的坑是同时开了Too many workers和cache导致内存不够直接死机这要按设备实际内存调整不能一味拉高并行数。5.4 测试集精度高、现场识别差光照泛化和背景混淆现象在自己的测试集上mAP能到0.85但拿到田间或者现场拍的照片准确率掉到不足五成大量误检。原因测试集和现场图像的数据分布不一致。拍摄测试集时往往背景单一、光线均匀而现场有强烈反光、阴影、类似叶片纹理的背景模型学到的更多是“这种背景区域有虫”而不是虫本身。解决在现场采集阶段就要刻意增加负样本也就是没有害虫的作物图片让模型学会在这些背景下输出“无目标”。训练时同时做色彩增强和随机擦除增强减少对背景纹理的依赖。另一招是做图像白平衡把输入图像统一色温降低不同时段灯光差异的影响。最关键的是最终测试集应该从现场数据里单独留一部分而不是全部用网图和室内图否则验证分数再高也是虚高。现场数据少的话哪怕只有全数据量的20%也要单独划出来当验证集宁可训练数据少一点。5.5 模型输出的是框不是数量计数逻辑必须独立成模块现象界面上的数量统计在图片变化后偶尔多一个少一个看起来毫无规律代码里到处是临时加的if count 10这类补丁。原因把计数逻辑和检测逻辑耦合在了一起检测框稍微变化计数结果就跟着抖动补丁越打越多最后自己都说不清规则。解决把计数单独抽成一个函数或类输入是检测框输出输出是各类别数量。框的合并去重、按类别统计、边界归属判断都放在这个模块里并给模块写单测。这样检测模型升级了计数逻辑不用大改反过来计数规则调整也不会影响模型训练。答辩时这个模块的代码结构和测试用例很容易解释清楚因为它体现了工程上“模块独立”的思想。写单测时不要只测理想情况要专门构造重叠框和跨边界框把真实场景里最容易出错的输入喂进去这样才知道模块的边界在哪。6. 收尾技巧用视频回放和导出加速验证你的检测系统6.1 用视频回放验证计数稳定性静态图片的检测精度不代表真实场景可用我习惯把测试图片拼成模拟视频或直接用现场短视频来做验证。把检测框画到每帧上再叠加当前帧各类别数量连续播放观察数跳不跳。特别是害虫爬动、进出画面边缘时数量在相邻几帧间剧烈变化就说明计数逻辑还有边界问题。import cv2 def count_by_class(detections): count_dict {} for det in detections: cls det[0] count_dict[cls] count_dict.get(cls, 0) 1 return count_dict这段代码只是计数的最简形式实际使用时要先对检测结果做置信度过滤和NMS去重再按类别统计。你可以在自己项目里扩展成读取视频帧、调用模型、统计并绘制结果的脚本核心是观察数量序列的稳定性而不是只看某一帧的截图。相信我这一步能提前暴露很多答辩现场才被发现的问题。6.2 导出ONNX并测推理速度把模型从训练环境抽离出来训练结束后把权重导出成ONNX格式再用ONNX Runtime做推理是毕业设计里很稳的加分点。导出时要注意固定输入尺寸动态尺寸会让部署复杂度变高。导出后先用少量图片对比原始权重和导出模型的输出确认误差在可接受范围再测推理速度。导出这一步能让你摆脱训练框架的束缚以后在客户端或边缘设备上做性能测试时不再需要拖着一个庞大的依赖环境。最后说个我自己的习惯模型和数据永远留三个版本原始采集数据、标注完成数据、增强版本数据权重也保留最后一个epoch和best精度两个文件。别删中间文件它们是你回溯和复现的唯一线索。很多同学后期改需求改到崩溃就是因为原始中间件已经删干净了只能从头再来。希望这篇文章能帮你在害虫检测这个题目上少走弯路把精力花在真正影响结果的地方做出一份能讲清楚、能演示、能让人信服的作品。本文还有配套的精品资源点击获取
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进