ARTICLE DETAIL

资讯详情

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

YOLOv8+PyQt5水稻害虫检测:自建数据集训练到桌面部署

YOLOv8+PyQt5水稻害虫检测:自建数据集训练到桌面部署 水稻害虫检测这个题目我在农业视觉方向的项目里前后碰过好几次。最早一版用的是传统图像处理加SVM叶片一抖动、光线一变就全乱套后来换成 YOLOv8 做目标检测配合 Python 写的推理服务再用 PyQt5 套一层桌面界面才算真正做成一个能拿给植保站同事直接用的东西。这篇就把这套YOLOv8 PyQt5 自建数据集 训练代码的完整链路拆开讲重点放在那些教程里通常不会写、但一定会绊你一脚的细节上。整套东西的定位很明确给有一定 Python 基础、想做深度学习实战落地的人参考尤其是想把模型从 notebook 里搬出来变成双击就能跑的软件的这类需求。不管你之前有没有跑通过目标检测只要你手里有一批稻田照片、一台带显卡的机器这套流程基本都能复现。区别只在于数据集质量和调参细节决定你的模型是能用还是不能用。1. 稻田里的虫情识别为什么不能直接套用通用检测模型1.1 水稻害虫检测的场景特殊性先说清楚这件事到底难在哪不然很容易低估工作量。水稻害虫识别的难点不在于检测这个任务本身而在于目标物和拍摄条件的特殊性。第一是目标极小。稻飞虱成虫体长普遍在 2 到 4 毫米即便手机贴着叶片拍距离控制在 20 厘米左右一只虫在 4000×3000 的原图里也就占几十个像素。缩到网络输入的 640×640 之后可能只剩 10×10 像素甚至更小。这个尺度下浅层特征图能不能保留住虫体轮廓直接决定了召回率。第二是目标密度极不均匀。稻飞虱是群集性害虫茎基部一窝能聚几十上百只互相重叠而稻纵卷叶螟幼虫藏在卷起来的叶苞里面一张图里可能只有一条还半遮半掩。这意味着同一张图上模型既要处理密集小目标又要处理稀疏大目标对多尺度检测能力要求很高。第三是背景干扰强。稻田背景是高频纹理叶脉、叶尖、水珠、反光、枯黄斑块还有不少形态相近的非目标——比如叶蝉、蜘蛛、蚂蚁、小土粒。模型很容易把这些东西误判成害虫尤其是幼苗期叶片上的褐色斑点跟某些虫体颜色几乎一致。第四是拍摄条件不可控。田间采集不可能像实验室那样打光摆拍清晨有露水反光正午是硬阴影阴天漫射光反而最理想。同一个物种在不同光照、不同角度下的外观差异可能比不同物种之间的差异还大。把这四点合起来看你就明白为什么这个任务不能用随便找个数据集跑跑的思路对付。1.2 通用预训练权重的水土不服体现在哪很多人第一步就会想直接下个 yolov8n.pt改个类别数就开跑。这个思路本身没错但要知道它的边界在哪。COCO 数据集里的 80 个类别中压根没有稻飞虱这种细粒度类别最接近的只有birdcat这类泛类甚至没有insect。所以预训练权重能给你的只是通用边缘、纹理、形状特征的提取能力也就是骨干网络Backbone那部分。检测头Head和分类分支必须完整重训因为分类 logits 的语义空间跟你的害虫类别完全不搭。实际做下来我总结出预训练权重真正的价值在两块一是收敛速度快小数据集几千张量级从头训很容易训崩用预训练权重一般 20 到 30 个 epoch 就能看到 mAP 抬头二是小目标检测的稳定性更好因为骨干网络已经在海量自然图像上学到过各种尺度的纹理响应。但有两个地方必须改。一是输入分辨率。COCO 预训练用的是 640×640对于极小的虫体可以尝试提到 960 甚至 1280代价是显存和推理耗时线性甚至平方级上升。在 6GB 显存的卡比如 GTX 1660 Ti 这类上1280 输入基本只能跑 batch2 到 4训练会很慢所以要权衡。二是数据增强策略。通用检测任务里 mixup、copy_paste 这些强增强通常是有益的但在害虫检测里copy_paste 会把虫体粘到不合理的背景上比如粘到天空或者水面反而污染数据分布。这个后面第 2 章会详细讲。提示如果手里数据量少于 1500 张建议先用 yolov8n 或 yolov8s不要一上来就上 yolov8m/l。小模型在小数据集上过拟合风险更低训练也快先把链路跑通再考虑涨点。1.3 这套系统最终交付的形态与功能边界说清楚做成什么样比说用什么技术更重要。我在做这类项目时习惯先用一张表把功能边界钉死避免后面无限加需求。功能模块输入输出备注单图检测单张 JPG/PNG标注框叠加图 类别计数最基础用于验证模型批量检测文件夹逐图结果 汇总 CSV植保站批量筛查用视频检测MP4/AVI带框视频 逐帧统计用于田间录像回看实时检测摄像头实时画面 计数需控制延迟结果导出检测结果CSV / Excel便于后续统计分析模型切换权重文件动态加载支持不同版本模型对比这个边界很重要因为很多教程只做单图检测就结束了但真正拿去用的时候用户第一个问题一定是能不能选一整个文件夹。而批量处理一旦引入就有线程、进度反馈、内存占用这些工程问题了。我个人的建议是先把单图检测做到 100% 可用再逐个加模块。不要一开始就搭一个什么都能干的界面最后每个模块都是半成品。2. 数据集决定了这个项目的天花板2.1 类别体系设计识别物种还是指导防治这是整个项目最容易走偏的地方。我见过不少人一上来就分 30 个类结果每个类只有几十张图训练完 mAP 惨不忍睹。类别体系的设计要回到这套系统给谁用、用来干什么这个问题上。如果给植保站用他们关心的不是这是灰飞虱还是白背飞虱而是这块田里主要是哪几类害虫、密度大概多少、要不要打药。那么类别粒度就该按防治决策的重要性来划分而不是按生物分类学。一个比较务实的起步类别体系大概是这样稻飞虱可合并褐飞虱、白背飞虱、灰飞虱为一大类稻纵卷叶螟二化螟 / 三化螟可合并为螟虫类稻蓟马稻蝗稻苞虫负样本类明确标注为非害虫的干扰物可选7 个类别左右每个类至少 300 到 500 张有效图像这是能训出可用模型的下限。如果某个类实在数据太少有两个选择要么降级合并到上位类要么先不做这个类。强行保留一个只有 50 张图的类别只会拉低整体的精确率。长尾问题在这里特别致命。假设你有 5000 张图稻飞虱占了 3500 张稻蝗只有 80 张。训练时模型会学到只要像虫就往稻飞虱上靠的捷径稻蝗的召回率会低到几乎为零。处理方法后面 3.5 节讲。2.2 现场采集光照、遮挡与密集堆叠的处理采集环节的质量直接决定标注环节的痛苦程度也决定模型上限。这块我踩过的坑最多说几条实操经验。光照方面阴天漫射光是最理想的虫体细节保留最完整。清晨露水期虽然虫情活跃但叶片反光会在图像上形成大面积高亮区域模型容易把反光斑当目标。正午直射光则会产生硬阴影把虫体边缘压暗边界框很难画准。如果必须在强光下拍建议用一块白板在侧上方遮挡直射光或者用环形 LED 补光。环形光的好处是阴影柔和、四周均匀不会产生方向性硬边。遮挡问题上有个很实用的土办法白盆抖落法。拿一个白色搪瓷盆放在稻丛下方用手或小棍轻拍稻株害虫会掉进盆里然后在白色背景下单独拍摄。这样得到的样本干净、无遮挡、背景统一特别适合做类别识别的训练数据。缺点是丢失了自然田野背景这个信息所以要和田间直接拍摄的图混合使用大致 1:2 的比例比较合适。密集堆叠的处理要点是标注尺度的一致性。假设一窝稻飞虱聚集在一起你是每一只都单独画框还是整窝画一个大框我的做法是能分辨出独立个体的就单独画框实在分不清的整窝画一个框但不标类别细节。训练时如果两种标法混用模型会困惑。每类建议至少采集 300 张其中至少 1/3 应该是困难样本——小目标、遮挡、强反光、密集堆叠这几种情况都要覆盖。全是清晰大图的训练集训出来的模型一到真实场景就崩。2.3 标注规范与 VOC 转 YOLO 的实操标注工具用 LabelImg 就够了快捷键 W 画框、A 上一张、D 下一张效率很高。但有几个细节必须提前定好不然后面改起来很痛苦。框要贴紧虫体不要为了保险留一圈大边距。框留太大模型学到的就是背景也是目标的一部分推理时框会飘。类名用英文或拼音别用中文。这一点很多人吃过亏中文路径和中文类别名在某些环境下会触发编码问题LabelImg 保存的 XML 里如果带中文转 YOLO 格式时容易出现乱码。建议用rice_planthopper、leaf_roller这种命名。同一只虫不要重复标注一个实例一个框这是目标检测的基本要求。LabelImg 可以直接选 YOLO 格式保存如果已经标成 VOC 的 XML需要转换。转换脚本不长关键是把归一化坐标算对import xml.etree.ElementTree as ET import os from pathlib import Path CLASSES [rice_planthopper, leaf_roller, stem_borer, thrips, rice_grasshopper, rice_skipper] CLASS_MAP {name: i for i, name in enumerate(CLASSES)} def voc_to_yolo(xml_path, out_dir, img_w, img_h): tree ET.parse(xml_path) root tree.getroot() lines [] for obj in root.iter(object): cls_name obj.find(name).text.strip() if cls_name not in CLASS_MAP: continue # 过滤未定义的类别避免训练时报未知类 bbox obj.find(bndbox) xmin float(bbox.find(xmin).text) ymin float(bbox.find(ymin).text) xmax float(bbox.find(xmax).text) ymax float(bbox.find(ymax).text) # 坐标裁剪防止越界导致训练报错 xmin, xmax max(0, xmin), min(img_w, xmax) ymin, ymax max(0, ymin), min(img_h, ymax) if xmax xmin or ymax ymin: continue xc (xmin xmax) / 2.0 / img_w yc (ymin ymax) / 2.0 / img_h w (xmax - xmin) / img_w h (ymax - ymin) / img_h lines.append(f{CLASS_MAP[cls_name]} {xc:.6f} {yc:.6f} {w:.6f} {h:.6f}) out_path Path(out_dir) / (Path(xml_path).stem .txt) out_path.write_text(\n.join(lines), encodingutf-8)这里有个坑值得单独说图像尺寸必须从原图读取不能从 XML 里的size节点读。有些标注工具写入的size字段和真实图像尺寸不一致尤其是图像被压缩或旋转过用错了会导致所有框整体偏移训练时 loss 一直降不下去但你又看不出哪里错了。2.4 数据增强的取舍哪些增强会帮倒忙YOLOv8 的默认增强参数是给通用场景设计的直接套到害虫检测上有几项要调。增强参数默认值建议值理由mosaic1.01.0对小目标有益四图拼接增加密度close_mosaic1010最后 10 轮关闭稳定收敛hsv_h0.0150.015色相扰动模拟不同光照hsv_s0.70.5饱和度扰动别太大虫体颜色是重要特征hsv_v0.40.4亮度扰动模拟阴晴变化degrees0.05.0小角度旋转田间拍摄必然有角度差translate0.10.1平移防止位置偏置scale0.50.5缩放模拟不同拍摄距离fliplr0.50.5左右翻转虫体形态对称可用flipud0.00.0上下翻转不自然禁用mixup0.00.0造出非自然组合害虫检测不推荐copy_paste0.00.0同上容易污染分布我特别想强调flipud和copy_paste这两项。上下翻转在自然场景里几乎不可能出现没人倒着拍稻田开了只会让模型学到假的分布copy_paste是把分割出来的目标贴到别的图上如果没有像素级的分割标注它只能贴矩形区域会出现明显的边界伪影模型学到的可能是矩形边缘这个虚假特征。另外mosaic虽然对小目标友好但它把四张图缩放到 1/4 再拼等于把原本就小的虫体又缩小了一半。如果你的目标本来就只有 15×15 像素mosaic 之后变成 7×7反而不利于学习。这种情况我建议把imgsz提上去比如 960或者把mosaic调到 0.5 左右降低比例。3. YOLOv8 训练链路从环境对齐到权重产出3.1 环境配置中最容易翻车的版本对齐环境这块十个新手有八个卡在版本不匹配上。核心逻辑是显卡驱动决定 CUDA 版本上限CUDA 版本决定 PyTorch 版本PyTorch 版本决定 ultralytics 能不能正常装。先用nvidia-smi看驱动支持的最高 CUDA 版本然后按这个去装对应的 PyTorch。别去官网随便下最新的先看驱动。# 第一步确认驱动和显卡 nvidia-smi # 第二步建一个干净的 conda 环境 conda create -n ricepest python3.10 -y conda activate ricepest # 第三步装 PyTorch以 CUDA 11.8 为例具体版本按 nvidia-smi 输出选 pip install torch torchvision --index-url https://download.pytorch.org/whl/cu118 # 第四步装 ultralytics 和界面依赖 pip install ultralytics opencv-python PyQt5 pandas # 第五步验证 GPU 是否真的可用 python -c import torch; print(torch.cuda.is_available(), torch.cuda.get_device_name(0))最后那行验证必须做输出True和显卡型号才算成功。很多人装完直接开训跑了半小时发现是 CPU 在跑速度慢几十倍。组件建议版本检查方式Python3.9 - 3.11python -VPyTorch2.0 及以上torch.__version__ultralytics8.xultralytics.__version__OpenCV4.5 及以上cv2.__version__PyQt55.15PyQt5.QtCore.QT_VERSION_STR注意如果你用的是 6GB 显存级别的显卡训练时遇到CUDA out of memory不要急着换卡先把batch降到 4 或 8再把imgsz从 640 降到 512 试试。多数情况下是这两个参数吃显存。3.2 data.yaml 与目录结构的细节YOLOv8 对目录结构有硬性要求这点必须先搞对否则训练脚本会找不到标签然后静默跳过样本——你只会看到训练正常跑但 mAP 一直是 0。标准结构长这样rice_pest_dataset/ ├── images/ │ ├── train/ │ ├── val/ │ └── test/ ├── labels/ │ ├── train/ │ ├── val/ │ └── test/ └── data.yaml关键点images和labels下的子目录名必须完全对应标签文件名必须和图像文件名同名只是扩展名换成 .txt。YOLOv8 是通过把路径里的/images/替换成/labels/来定位标签的所以目录结构一旦不一致就全乱。data.yaml内容path: /home/user/rice_pest_dataset train: images/train val: images/val test: images/test names: 0: rice_planthopper 1: leaf_roller 2: stem_borer 3: thrips 4: rice_grasshopper 5: rice_skippernames的键必须从 0 开始连续编号中间不能跳号。类别顺序必须和你在标注转换时用的CLASSES列表顺序一致这是最容易出错的地方——顺序错了一位模型照样训练照样出结果但所有类别标签都错位了而且从 loss 曲线上完全看不出来。数据集划分比例上我一般用 8:1:1。但有个细节划分要按采集批次分不能随机打乱分。同一个叶片上连拍的十张图如果被随机分到 train 和 val 里验证集就等于在考训练集见过的内容mAP 会虚高。正确做法是整批一起分。3.3 训练参数逐项拆解与调参逻辑参数不是抄来的每个都得知道为什么这么设。下面是我在稻虫项目上比较稳的一套配置附上理由。from ultralytics import YOLO model YOLO(yolov8n.pt) # 预训练权重起步 results model.train( datadata.yaml, imgsz960, # 小目标场景提高输入分辨率 epochs200, # 给足轮数配合早停 batch8, # 6GB 显存下的安全值 lr00.01, # 初始学习率 lrf0.01, # 最终学习率 lr0 * lrf momentum0.937, weight_decay0.0005, warmup_epochs3.0, # 小数据集适当延长预热 patience50, # 50 轮无提升就停 optimizerauto, freeze10, # 冻结骨干前 10 层缓解小数据过拟合 mosaic1.0, close_mosaic10, hsv_s0.5, degrees5.0, projectruns/rice_pest, nameexp1, device0, workers4, seed42, )几个参数单独解释一下。imgsz960是我在这个项目里最愿意花成本的地方。虫体只有十几像素640 输入下基本就是极限了。提到 960 之后 mAP50 通常能涨 3 到 6 个百分点代价是训练时间增加约一倍。如果你的卡带不动 960至少要保证 768。freeze10这个参数在有预训练权重、数据量又不足 3000 张的时候特别有用。冻结骨干前 10 层意味着不让这些层的参数被小数据集带偏保留通用特征提取能力。实测能明显减少训练后期的过拟合。close_mosaic10是最后 10 轮关闭 mosaic 增强。这个设计的原因是 mosaic 拼接会引入不自然的边界训练最后阶段需要让模型适应真实分布所以关掉它做最后的微调。patience50配合epochs200实际训练往往在 100 到 140 轮就停了。不用怕浪费早停机制会自动挑出最好的权重存成best.pt。命令行版本也顺手给一下方便脚本化yolo detect train datadata.yaml modelyolov8n.pt imgsz960 epochs200 batch8 freeze10 patience50 device03.4 训练过程中的指标判读loss、mAP 与混淆矩阵训练跑起来之后runs/rice_pest/exp1/下面会有一堆图和 CSV。看不懂这些你就是在盲训。先看results.csv里的几条 loss。YOLOv8 有三个损失box_loss边界框回归、cls_loss分类、dfl_loss分布焦点损失。正常情况下三条曲线都应该单调下降然后趋于平缓。诊断规则大致是这样现象可能原因处理方向box_loss 震荡不降学习率过大 / 标注框质量差降 lr0 到 0.005检查标注cls_loss 降不下去类别混淆严重 / 类别标注错位看混淆矩阵检查 names 顺序train loss 降但 val loss 抬头过拟合加数据、加 freeze、降模型规模所有 loss 都是 NaN学习率爆炸 / 数据有脏标签检查 lr、清洗标签mAP 一直为 0标签路径不对 / 类别不匹配检查目录结构和 data.yamlconfusion_matrix.png是我最看重的图。它直接告诉你哪些类别之间在互相混淆。比如在稻虫项目里很常见的是灰飞虱和白背飞虱互相误判因为两者形态差异极小只有在显微镜下看翅斑才分得清。看到这种情况要么合并类别要么补充更多能区分的样本。results.png里的 mAP50 曲线和 mAP50-95 曲线要一起看。mAP50 高但 mAP50-95 很低说明框的位置不够准可能是标注框偏大偏松。这时候回去重新检查标注质量比调任何超参都有效。还有PR_curve.png和F1_curve.png。F1 曲线上的峰值点对应的置信度就是你推理时该用的 conf 阈值这比拍脑袋定 0.25 科学得多。3.5 调参实战小目标、密集目标、类别不平衡的处理前面提到的那几个难点对应到调参上有具体做法。小目标漏检最有效的三招按优先级排提高imgsz、增加小目标样本占比、修改模型结构加一个 P2 检测层。前两个成本低第三个要改 yaml 结构P2 层对应 160×160 的特征图对小目标敏感但也会带来大量误检和显存开销要谨慎。密集目标主要是 NMS 阶段的问题。密集堆叠时相邻目标的框重叠度高NMS 可能会把正确的框误删。这时候把iou阈值从 0.45 提到 0.6 到 0.7能保留更多相邻框。代价是可能出现少量重复框。类别不平衡有两条路。一条是在数据层面给少样本类别做过采样或者在 mosaic 拼接时优先选含少样本类的图另一条是在损失层面加权但这需要改训练代码成本较高。我一般先用数据层面的方法简单有效。另外一个容易被忽略的点是负样本。如果你的误检率很高往训练集里加一批明确没有害虫的稻田图和有干扰物但无害虫的图标签文件留空。YOLOv8 支持空的标签文件这些图会作为背景样本参与训练。实测加 5% 到 10% 的负样本能把误检率降下来一截。3.6 导出与部署ONNX、TensorRT 与边缘设备模型训好了best.pt只是一个 PyTorch 权重直接拿去部署会有性能和依赖问题所以通常要导出。from ultralytics import YOLO model YOLO(runs/rice_pest/exp1/weights/best.pt) # 导出 ONNX通用性最好 model.export(formatonnx, opset12, simplifyTrue, dynamicFalse) # 导出 TensorRT engineNVIDIA 卡上性能最好 model.export(formatengine, halfTrue, device0)ONNX 适合跨平台OpenCV 的dnn模块就能直接读。TensorRT 适合 NVIDIA 显卡上的极致性能但有个大坑engine 文件跟生成它的 GPU 架构和 TensorRT 版本强绑定在 A 卡上生成的 engine 拿到 B 卡上大概率加载失败必须重新生成。如果目标是边缘设备比如带 NPU 的嵌入式板卡流程又不一样通常需要转成设备厂商自己的格式还要做量化校准。量化这一步特别讲究校准集必须从你的真实验证集里抽不能随便找几张图否则量化后的精度掉得很难看。我一般的做法是从 val 集里抽 200 到 500 张覆盖所有类别和各种光照条件。如果设备资源特别紧张就只能在模型规模上做取舍。YOLOv8n 的权重本身就不到 7MB量化到 INT8 之后还能再小一截这类轻量模型在边缘端跑实时检测是完全可行的。4. 用 PyQt5 把模型包装成能落地的桌面工具4.1 线程模型为什么推理必须离开主线程这是新手做界面最容易踩的坑也是最致命的一个。PyQt5 的主线程负责处理界面事件循环如果你在主线程里调model.predict()一次推理几百毫秒到几秒界面就会完全卡死——不响应点击、不刷新、甚至显示无响应。正确做法是把推理放在QThread里通过信号pyqtSignal把结果传回主线程更新界面。from PyQt5.QtCore import QThread, pyqtSignal import cv2 from ultralytics import YOLO class InferWorker(QThread): frame_done pyqtSignal(object, object) # 原图, 绘制后的图 progress pyqtSignal(int) finished_all pyqtSignal() def __init__(self, model_path, source, conf0.25, iou0.45): super().__init__() self.model YOLO(model_path) self.source source self.conf conf self.iou iou self._running True def stop(self): self._running False def run(self): cap cv2.VideoCapture(self.source) total int(cap.get(cv2.CAP_PROP_FRAME_COUNT)) or 1 idx 0 while self._running: ret, frame cap.read() if not ret: break results self.model.predict(frame, confself.conf, iouself.iou, verboseFalse) annotated results[0].plot() self.frame_done.emit(frame, annotated) idx 1 self.progress.emit(int(idx / total * 100)) cap.release() self.finished_all.emit()几个要点_running标志位用于优雅停止用户关窗口时不能让线程悬着verboseFalse关掉 ultralytics 的日志输出否则控制台会刷屏results[0].plot()返回的是带框的 numpy 数组可以直接转成 QImage 显示。4.2 界面控件的组织与结果展示设计界面布局我习惯用三栏式左边是文件/列表区中间是图像显示区右边是结果统计区。中间显示区建议用QGraphicsViewQGraphicsScene而不是QLabel。QLabel也能显示图片但缩放、拖拽、鼠标坐标映射都要自己写QGraphicsView这些是内置的还支持滚轮缩放和拖拽平移。右侧结果区用QTableWidget展示逐条检测结果列包括序号、类别、置信度、位置坐标。如果是批量处理再加一个QTreeWidget展示文件列表每个文件节点下面挂它的检测结果子节点。QTreeWidgetItem展开/折叠的状态记得单独维护不然刷新数据后展开状态会丢。结果汇总用QLabel或者只读的QTextEdit显示内容包括总检测数、各类别计数、平均置信度。如果要导出用pandas直接写 CSVimport pandas as pd def export_csv(records, out_path): df pd.DataFrame(records, columns[file, class, conf, x1, y1, x2, y2]) # utf-8-sig 让 Excel 打开不乱码这个细节踩过才知道 df.to_csv(out_path, indexFalse, encodingutf-8-sig)utf-8-sig这个编码格式值得记住。默认的utf-8导出中文用 Excel 打开会显示乱码加个 BOM 头就正常了。这是纯工程细节但用户第一次打开文件看到乱码对整套系统的信任度会直线下降。4.3 图片、视频、摄像头三种输入的统一处理三种输入源看起来差别很大其实可以统一成一个帧生成器的抽象。图片读一次产出一帧就结束视频cv2.VideoCapture(file_path)循环读取摄像头cv2.VideoCapture(0)循环读取但需要控制帧率避免堆积def frame_source(source): if isinstance(source, str) and source.lower().endswith( (.jpg, .jpeg, .png, .bmp)): img cv2.imread(source) if img is not None: yield img else: cap cv2.VideoCapture(source) while True: ret, frame cap.read() if not ret: break yield frame cap.release()摄像头这块有个细节相机采集速度往往比推理速度快如果每帧都推理队列会越积越长画面延迟越来越大。解决办法是跳帧推理——每处理完一帧就丢掉缓冲区里堆积的旧帧只取最新的一帧。OpenCV 里可以通过循环cap.grab()只取最后一帧来实现虽然粗暴但效果立竿见影。视频检测的结果保存我习惯用时间戳命名避免覆盖YOLOv8-稻虫检测-20250101-153000.mp4。视频编码用mp4v兼容性最好虽然压缩率一般。4.4 打包分发与显示异常的排查代码在自己机器上跑通了发给别人往往就出问题。两个最常见的故障。第一个是权重文件路径找不到。PyInstaller 打包后程序运行时的工作目录会变用相对路径加载best.pt必然失败。解决办法是用sys._MEIPASS定位资源目录import sys from pathlib import Path def resource_path(relative): base getattr(sys, _MEIPASS, Path(__file__).parent) return str(Path(base) / relative) model YOLO(resource_path(weights/best.pt))同时打包时要用--add-data把权重和字体文件一起打进去。ultralytics 自己还依赖一些配置文件建议用--collect-all ultralytics一次性收全否则会缺模块。第二个是界面完全不显示。这个症状很迷惑程序能启动、控制台有输出但窗口要么一片空白要么干脆看不到。原因通常是 OpenGL 初始化失败——某些老显卡驱动或者远程桌面环境下Qt 默认的 OpenGL 后端拿不到上下文。解决办法是强制走软件渲染import os os.environ[QT_OPENGL] software from PyQt5.QtCore import Qt from PyQt5.QtWidgets import QApplication QApplication.setAttribute(Qt.AA_UseSoftwareOpenGL) app QApplication(sys.argv)或者在系统环境变量里设QT_OPENGLsoftware。软件渲染性能会差一些但界面至少能正常出来。如果一定要用硬件加速可以在QApplication创建前设置Qt.AA_ShareOpenGLContexts但兼容性不如软件渲染稳。还有一个高频问题是高分屏适配。在 4K 屏或者缩放到 150% 的笔记本上界面控件会变得极小或者图片模糊。加这两行QApplication.setAttribute(Qt.AA_EnableHighDpiScaling, True) QApplication.setAttribute(Qt.AA_UseHighDpiPixmaps, True)注意这两个属性必须在QApplication实例化之前设置写在app QApplication(...)后面就无效了。5. 实测中反复出现的漏检与误检我是怎么处理的5.1 密集小目标漏检切片推理与输入分辨率模型训完第一次实测最典型的反馈就是明明图上有一堆稻飞虱为什么只框出来几个。先别急着调参先确认一件事把原图缩放到 960 之后虫体还剩多少像素。如果原图是 4000×3000缩到 960 等于缩了 4 倍多一只原本 40 像素的虫就剩不到 10 像素。这个尺度下漏检是必然的不是模型不行。解决路径有三条按成本从低到高排。第一条是提高推理分辨率。训练用什么尺寸推理就用什么尺寸甚至可以略高。训练用 960推理用 1280实测召回率会涨。代价是速度变慢但在桌面端处理图片的场景下完全可以接受。第二条是切片推理。把大图切成若干带重叠的小块比如 640×640重叠 20%每块单独推理最后把所有框映射回原图坐标再做一次全局 NMS。这个思路对小目标密集场景效果非常明显实测能把漏检率降一大截。切片加 NMS 的合并逻辑要自己写核心是坐标偏移和去重import numpy as np from ultralytics import YOLO def sliced_infer(model, img, slice_size640, overlap128, conf0.25): h, w img.shape[:2] step slice_size - overlap boxes_all [] for y in range(0, max(h - overlap, 1), step): for x in range(0, max(w - overlap, 1), step): y2 min(y slice_size, h) x2 min(x slice_size, w) patch img[y:y2, x:x2] if patch.shape[0] 32 or patch.shape[1] 32: continue res model.predict(patch, confconf, verboseFalse)[0] if res.boxes is None or len(res.boxes) 0: continue xyxy res.boxes.xyxy.cpu().numpy() cls res.boxes.cls.cpu().numpy() confs res.boxes.conf.cpu().numpy() # 关键把局部坐标偏移回全局坐标 xyxy[:, [0, 2]] x xyxy[:, [1, 3]] y for b, c, cf in zip(xyxy, cls, confs): boxes_all.append([*b, cf, c]) # 跨切片去重用简单的 IoU 抑制 return merge_boxes(np.array(boxes_all), iou_thr0.5) if boxes_all else []第三条是改模型结构加 P2 层。这个前面提过效果有但对显存要求高而且容易带来误检建议前两条都试过再考虑。另外切片推理在最后合并的时候一定要做一次全局 NMS否则重叠区域的同一个目标会被两个切片各检测一次出现重复框。5.2 形近虫态与背景干扰带来的误检误检通常来自两个方向一个是形近物种一个是背景干扰物。形近物种的问题本质上是类别定义的问题不是模型的问题。灰飞虱、白背飞虱、褐飞虱这三个成虫形态在肉眼和普通照片尺度下差异极小主要靠翅斑和体色区分而这些特征在低分辨率下早就丢了。如果非要分开标注一致性会很难保证不同的人标同一张图结果可能不同模型学到的边界也是模糊的。我的处理方式是合并成稻飞虱一类需要细分的时候再单独训练一个高分辨率分类模型走检测定位 分类细分的两阶段路线。背景干扰物的处理更直接一些加负样本。把叶尖、水珠、土粒、叶斑病斑这些容易误判的区域单独拍一批或者从现有图里裁一批作为负样本加入训练集标签留空。这个操作成本不高但对降低误检率效果明显。还有一个技巧是提高训练集中困难负样本的比例。所谓困难负样本就是那些模型目前会误判的图。可以用训好的模型对训练集跑一遍推理把误检的图挑出来人为复查并修正标签或加入负样本库再训一轮。这个叫难例挖掘迭代两三轮通常能明显改善。5.3 置信度阈值和后处理的取舍推理时的conf和iou两个阈值直接决定你看到的最终结果值得认真调。conf阈值控制的是多确定才算数。调低召回上升但误检增多调高精确率上升但漏检增多。这个取舍取决于使用场景如果是自动计数统计虫情密度宁可多检出一些也不要漏conf可以设到 0.2 到 0.25如果是需要人工复核的辅助标注conf设 0.35 到 0.45 减少干扰。具体数值别猜去训练结果目录里找F1_curve.png曲线峰值对应的 conf 就是在这个数据集上的最优平衡点。我多数项目里这个值落在 0.28 到 0.38 之间。iou阈值控制的是 NMS 的合并力度。密集堆叠场景要调高0.6 到 0.7否则挨着的两只虫会被合并成一个框。稀疏场景用默认 0.45 就够。视频检测还有个额外问题帧间计数重复。同一条虫在连续帧里会被反复检测到如果直接把每帧的检测数加起来统计结果会严重偏高。简单的处理办法是按空间位置做帧间去重——记录每个目标的中心点如果新帧里某个框的中心点离上一帧某个已计数的框很近比如距离小于框宽的一半就认为是同一个体不重复计数。更严谨的做法是用目标跟踪算法但对于虫情密度统计这个场景中心点距离法已经够用了。最后说一个我觉得挺重要但常被忽略的点检测结果要给出可读的结论而不是只给一堆框。植保人员看到界面上标了 87 个稻飞虱他下一步想知道的是这个密度算高还是低。所以在结果展示区加一段基于阈值的判断文本比如按百丛虫量估算密度等级会让整套系统从技术演示变成能用的工具。这一步不需要多复杂的算法几行判断逻辑就够但对使用体验的提升非常明显。我个人在这类项目上最大的体会是模型精度到一定程度之后继续抠 1 到 2 个百分点的 mAP投入产出比很低而把数据采集规范、标注一致性、界面交互、结果可读性这几件事做好用户实际感受到的价值反而更大。很多项目卡住不是因为模型不行而是数据太脏或者软件太难用。至于后续还能怎么扩展比较自然的方向是加一个历史记录管理把每次检测的结果存进本地数据库按时间和地块做趋势对比再往前一步就是把现场采集和模型更新做成一个闭环用真实场景里积累的错误样本持续迭代权重。这套链路搭起来之后你会发现换一个作物、换一类害虫大部分代码都能直接复用真正要重做的只有数据集和类别定义这两块。
返回列表