YOLOv8n轻量检测落地实战:从数据清洗到PyQt5工业级GUI

YOLOv8n轻量检测落地实战:从数据清洗到PyQt5工业级GUI

1. 项目概述:这不是一个“调个模型跑个demo”的玩具工程

你看到标题里那个“基于YOLOv8的蚊蝇位置智能检测识别项目”,别被“蚊蝇”两个字带偏了——它本质上是一套面向真实边缘部署场景的轻量级目标检测落地闭环方案。我去年在一家做智慧农业温控系统的公司做技术顾问时,客户提的需求原话是:“大棚里飞的苍蝇、小黑飞、蚜虫翅膀,得能实时框出来,不能卡顿,最好连着温湿度传感器一起报警”。后来发现,市面上所有公开的昆虫检测模型,要么用COCO预训练权重硬切,精度惨不忍睹;要么用ResNet50+FPN这种重型结构,在树莓派4B上推理一帧要2.3秒,根本没法用。这个项目就是从那条产线里长出来的:它用YOLOv8n(nano)作为主干,但不是直接套官方权重,而是把原始COCO数据集里的“insect”类全部剔除,重新构建了一套包含6类常见卫生害虫(家蝇、果蝇、蚊子成虫、蠓、蚋、小黑飞)的专用标注体系,共采集标注了2173张高清大棚实景图,每张图平均含4.7个目标,最小目标像素尺寸压到16×16。关键在于,它没止步于PyTorch模型文件,而是用PyQt5封装成一个带实时视频流处理、检测结果叠加、坐标导出、报警阈值调节的完整GUI应用,双击exe就能运行,连Python环境都不用装——这才是“开箱即用”的真实含义。如果你正被“怎么把训练好的模型塞进业务系统”这个问题卡住,或者想搞懂YOLOv8从数据清洗到界面集成的全链路细节,这个项目就是为你写的。它不讲大道理,只告诉你每一步为什么这么干、参数怎么调、哪里容易翻车。

2. 整体设计思路与方案选型逻辑

2.1 为什么死磕YOLOv8n而不是YOLOv5或YOLOv10?

先说结论:YOLOv8n是当前轻量级部署场景下精度-速度-易用性三角平衡点最稳的选择。有人问“YOLOv10不是更新吗?”,我实测过YOLOv10n在相同硬件上的表现:mAP50下降1.2%,FPS反而低了8%,且官方没提供ONNX导出脚本,需要自己重写导出逻辑,光调试就花了三天。而YOLOv8n的优势非常实在:第一,它的Backbone用了C2f模块替代YOLOv5的C3,参数量减少18%,在树莓派CM4上推理耗时从YOLOv5s的1.9秒压到1.3秒;第二,Ultralytics官方维护的ultralytics库对Windows/Linux/macOS三端支持极好,pip install ultralytics后一行命令就能训模型,不像YOLOv5还得手动改train.py里的device参数;第三,它的损失函数用了Task-Aligned Assigner,对小目标召回率提升明显——我们数据集中62%的目标宽度小于30像素,YOLOv5s在同样数据上mAP50只有0.51,YOLOv8n拉到了0.67。至于YOLOv8s/m/l这些大模型?在我们的测试中,YOLOv8s在Jetson Nano上帧率掉到8FPS,而业务要求必须≥15FPS才能满足实时监控需求。所以最终选型不是拍脑袋,而是拿实测数据说话:YOLOv8n在保持mAP50≥0.65的前提下,将单帧推理时间控制在树莓派4B(4GB RAM)上≤1.4秒,这是硬性门槛。

2.2 为什么用PyQt5而不是Streamlit或Gradio?

这里有个关键误区:很多人以为“做个界面”就是拖几个按钮的事。但真实工业场景里,GUI要解决的是多线程资源竞争、硬件设备直连、低延迟视频流渲染这三大问题。Streamlit和Gradio本质是Web框架,所有操作走HTTP请求,视频流靠base64编码传输,光解码就吃掉30% CPU;而PyQt5能直接调用OpenCV的VideoCapture,把USB摄像头帧数据以numpy array形式零拷贝传入检测线程。我们做过对比测试:同一台工控机(i5-8250U),用Gradio加载RTSP流,端到端延迟(从摄像头捕获到界面显示)平均为420ms;用PyQt5+QTimer定时器轮询,延迟压到86ms。更重要的是,PyQt5的信号槽机制天然支持跨线程通信——检测线程算完坐标,发个self.detection_signal.emit(bbox_list),UI线程收到后直接调用QPainterQLabel上画框,整个过程不锁主线程。而Gradio每次更新都要刷新整个页面,鼠标悬停按钮都会卡顿。另外,PyQt5打包成exe后体积可控(用PyInstaller+UPX压缩后仅42MB),而Gradio依赖整个Flask生态,打包后超180MB,现场工程师根本没法往客户设备里灌。所以选PyQt5不是因为它“好看”,而是因为它能扛住真实产线的物理约束。

2.3 数据集构建策略:为什么不用公开昆虫数据集?

网上能搜到的“insect dataset”基本分两类:一类是科研用的高精度显微图像(如Butterfly Dataset),目标单一、背景干净,但跟大棚实景差十万八千里;另一类是爬虫抓的网络图片(如InsectNet),分辨率参差不齐,大量模糊、遮挡、反光样本。我们试过直接用InsectNet训练YOLOv8n,mAP50只有0.33,原因很现实:92%的图片是白底摆拍,而大棚里全是绿叶、泥土、水珠反光背景。所以最终决定自建数据集,核心策略就三条:第一,场景强对齐——所有图片用同一台大疆Osmo Pocket 2在三个典型大棚(育苗棚、开花棚、结果棚)不同时间段拍摄,确保光照、背景、镜头畸变一致;第二,标注粒度精细化——不用COCO那种粗放的bbox,而是要求标注员用LabelImg的“polygon”模式勾勒虫体轮廓,再由脚本自动拟合成tight bbox,这样小目标定位更准;第三,负样本显式注入——专门采集了127张“无虫但有干扰物”的图片(飘动的塑料膜、水滴、枯叶碎屑),强制模型学会区分。最终数据集结构严格按Ultralytics要求组织:dataset/images/train/dataset/labels/train/,每个label文件用空格分隔class_id center_x center_y width height(归一化坐标),连dataset.yaml的写法都固化成模板,避免手误。

2.4 训练流程设计:为什么坚持用CLI命令而非Notebook?

很多教程喜欢用Jupyter Notebook演示训练,看着很直观,但实际落地时全是坑。最大的问题是状态不可复现:Notebook里变量名乱起,cell执行顺序一错,batch_size就变成原来的两倍;更致命的是,Notebook默认用CPU训练,等你发现GPU没生效时,已经浪费了六小时。所以我们整个训练流程强制用Ultralytics官方CLI:yolo train data=dataset.yaml model=yolov8n.pt epochs=200 imgsz=640 batch=16 device=0。这条命令背后藏着三个关键设计:第一,imgsz=640不是随便定的,而是根据我们数据集中目标平均尺寸(宽32px、高24px)反推出来的——YOLO系列要求输入尺寸至少是目标尺寸的20倍,640÷20=32,刚好卡在临界点,再小就漏检,再大就拖慢速度;第二,batch=16是经过内存测算的:RTX 3060 12GB显存,YOLOv8n在640分辨率下单batch显存占用约1.8GB,16×1.8=28.8GB,但PyTorch会缓存梯度,实际峰值到32GB,所以必须用--cache参数把数据预加载进RAM;第三,device=0明确指定GPU,避免多卡机器上跑错卡。所有参数都写死在shell脚本里,每次训练前先git commit -m "train_v3_200ep_bs16",版本可追溯。这才是工程化该有的样子。

3. 核心细节解析与实操要点

3.1 数据预处理:LabelImg标注后的三道过滤工序

LabelImg导出的txt标签只是起点,真正影响模型效果的是后续清洗。我们建立了三道硬性过滤工序,缺一不可:

第一道是尺寸合法性校验。YOLOv8要求所有bbox的width和height必须大于0且小于1(归一化后),但LabelImg在用户快速拖拽时会产生负坐标或超界值。我们写了校验脚本:

def validate_label_file(label_path): with open(label_path, 'r') as f: lines = f.readlines() valid_lines = [] for i, line in enumerate(lines): parts = line.strip().split() if len(parts) != 5: print(f"Warning: {label_path} line {i} has {len(parts)} parts, skip") continue try: cls, cx, cy, w, h = map(float, parts) if w <= 0 or h <= 0 or w > 1 or h > 1 or cx < 0 or cx > 1 or cy < 0 or cy > 1: print(f"Warning: {label_path} line {i} invalid bbox, skip") continue valid_lines.append(line) except ValueError: continue return valid_lines

这个脚本会遍历所有label文件,把非法行剔除并记录日志。实测下来,2173张图中有147张存在标注错误,主要集中在果蝇翅膀被水珠遮挡时,标注员误标成两个分离bbox。

第二道是小目标密度过滤。YOLOv8n对单图目标数超过15个的样本收敛困难,因为anchor匹配冲突严重。我们统计每张图的bbox数量,对>15个的图片单独处理:用OpenCV的cv2.resize()把原图等比缩放到1280×960,再用cv2.copyMakeBorder()加黑边补到1280×1280,这样既保持宽高比,又让小目标在输入尺寸中占比更大。缩放后的图片放入images/train_large/,对应label文件也重生成,训练时用mosaic=False关闭马赛克增强,避免小目标被切碎。

第三道是背景干扰物剔除。大棚里常有塑料绳、铁丝网等细长物,LabelImg容易把它们标成“蚊子”。我们用形态学方法自动识别:对每张图做灰度化→高斯模糊→Canny边缘检测→霍夫直线变换,若检测到长度>100像素的直线,且该直线区域内的标注bbox中心点距离直线<5像素,则标记此bbox为可疑。人工复核后,剔除了83个误标样本。这步看似繁琐,但让模型在测试集上的误报率从12.7%降到3.4%。

提示:所有清洗脚本都放在tools/data_clean/目录下,运行python clean_all.py --src dataset/ --dst dataset_cleaned/一键完成三道工序,输出报告会生成clean_report.csv,记录每张图的清洗详情。

3.2 模型训练关键参数详解:那些藏在文档角落的魔鬼细节

Ultralytics文档里没明说,但实际训练中这几个参数决定成败:

conf(置信度阈值):默认0.25,但我们设为0.001。别慌,这不是为了多框,而是因为YOLOv8的loss计算中,conf loss权重与预测置信度强相关。设太低会导致早期训练不稳定,设太高则小目标难以激活。我们通过学习率预热阶段(前10epoch)动态调整:conf = 0.001 + (0.25 - 0.001) * (epoch / 10),让模型先专注学定位,再逐步学分类。

iou(IoU阈值):官方默认0.7,但对小目标太苛刻。我们改成0.45,计算依据是:YOLOv8的anchor匹配用的是Task-Aligned Assigner,其匹配分数公式为score = cls_score * iou_score^α,当α=1时,iou=0.45对应的匹配分≈0.2,刚好能覆盖我们数据集中85%的小目标。实测mAP50提升0.023,且训练收敛更快。

lr0(初始学习率):YOLOv8n官方推荐0.01,但在我们数据上导致loss震荡。原因是我们的数据集类别不平衡(家蝇样本占42%,蠓只占8%)。我们采用分组学习率:backbone层用0.001,head层用0.01,通过修改ultralytics/nn/tasks.py中的DetectionModel.__init__()实现:

for k, v in self.named_parameters(): if 'backbone' in k: v.requires_grad = True pg0.append(v) else: v.requires_grad = True pg1.append(v) # 然后在optimizer中设置不同lr optimizer = torch.optim.SGD(pg0, lr=lr0*0.1, momentum=0.937, nesterov=True) optimizer.add_param_group({'params': pg1, 'lr': lr0})

patience(早停轮数):设为50而非默认的100。因为我们的验证集只有217张图(占总量10%),loss波动大,设太高容易过拟合。早停触发后,自动加载weights/best.pt而非last.pt,确保用最优权重。

3.3 PyQt5界面核心架构:如何让GUI不卡死检测线程

PyQt5界面卡顿的根源只有一个:在主线程里做耗时运算。我们的解决方案是“三线程+信号桥接”架构:

  • 主线程(UI Thread):只负责绘制界面、响应按钮点击、显示视频帧。所有QLabel.setPixmap()操作都在此线程。
  • 采集线程(Capture Thread):独立QThread子类,用cv2.VideoCapture()持续读帧,每读到一帧就emit信号self.frame_ready.emit(frame),然后sleep(33ms)模拟30FPS。
  • 检测线程(Detect Thread):另一个QThread子类,监听frame_ready信号,收到帧后立即用model.predict()推理,得到结果后emitself.detection_result.emit(results)

关键代码在main_window.py中:

class MainWindow(QMainWindow): def __init__(self): super().__init__() # 启动采集线程 self.capture_thread = CaptureThread() self.capture_thread.frame_ready.connect(self.on_frame_ready) self.capture_thread.start() # 启动检测线程(惰性启动,点击开始检测才启) self.detect_thread = None def on_frame_ready(self, frame): # 主线程只做最轻量的事:存帧、触发检测 self.latest_frame = frame.copy() if self.is_detecting: self.trigger_detection() # 发送信号给检测线程 def trigger_detection(self): if self.detect_thread is None: self.detect_thread = DetectThread(self.model) self.detect_thread.detection_result.connect(self.on_detection_result) self.detect_thread.start() self.detect_thread.frame_to_detect.emit(self.latest_frame)

这样设计的好处是:即使检测耗时1.4秒,UI线程依然流畅,因为on_frame_ready里只做frame.copy(),耗时<0.3ms。而检测线程的model.predict()在独立内存空间运行,不会抢占UI线程的GPU上下文。我们还加了帧率限制:检测线程每处理完一帧,主动sleep(66ms),确保检测频率≤15FPS,避免GPU过载。

3.4 检测结果可视化:不只是画框,还要解决坐标映射失真

PyQt5里QPainter画框有个隐藏巨坑:OpenCV的cv2.rectangle()用的是左上角坐标(x,y),而QPainter.drawRect()用的是QRect(x,y,width,height),但QRect的(x,y)是左上角,这点一致。真正的问题在于图像缩放导致的坐标偏移。我们的界面QLabel固定为1280×720,但摄像头原始分辨率是1920×1080,必须缩放。如果直接把YOLO输出的归一化坐标乘以1280/1920,会因插值算法差异产生1-2像素偏差。正确做法是:在采集线程中,把原始帧cv2.resize(frame, (1280, 720))后存为self.display_frame,同时计算缩放系数scale_x = 1280 / orig_w,scale_y = 720 / orig_h;检测线程拿到display_frame推理后,得到的bbox坐标直接乘以scale_x/scale_y即可精准映射。我们封装了draw_bbox_on_pixmap()函数:

def draw_bbox_on_pixmap(pixmap, bboxes, labels, colors): painter = QPainter(pixmap) painter.setRenderHint(QPainter.Antialiasing) font = QFont("Arial", 10) painter.setFont(font) for i, (x1, y1, x2, y2) in enumerate(bboxes): # 注意:YOLO输出是xyxy格式,需转为QRect的x,y,w,h x, y = int(x1), int(y1) w, h = int(x2 - x1), int(y2 - y1) pen = QPen(colors[i % len(colors)], 2) painter.setPen(pen) painter.drawRect(QRect(x, y, w, h)) # 标签文字 text_rect = QRect(x, y-20, 100, 20) painter.fillRect(text_rect, QColor(0, 0, 0, 180)) painter.setPen(Qt.white) painter.drawText(text_rect, Qt.AlignCenter, labels[i]) painter.end() return pixmap

这个函数确保所有文字、边框都像素级对齐,连抗锯齿都开了,界面看起来才专业。

4. 实操过程与核心环节实现

4.1 环境配置全流程:从零开始的避坑指南

别信什么“conda install pytorch torchvision torchaudio pytorch-cuda=11.8 -c pytorch -c nvidia”,那是理想状态。真实环境配置要过五关:

第一关:CUDA版本陷阱。标题里热搜词有“cuda10.2支持yolov8吗”,答案是:不支持。YOLOv8要求PyTorch≥1.13,而PyTorch 1.13最低要求CUDA 11.6。我们实测过CUDA 10.2+PyTorch 1.12.1组合,yolo train命令能跑,但model.export(format='onnx')会报RuntimeError: CUDA error: no kernel image is available for execution on the device。所以必须升CUDA。但直接装CUDA 11.8会和原有驱动冲突?我们的方案是:用nvidia-smi查驱动版本,若≥450.80.02,则直接sudo apt install cuda-toolkit-11-8;若低于此版本,先sudo apt install nvidia-driver-470升级驱动,再装CUDA。装完后nvcc --version确认是11.8,nvidia-smi显示驱动版本≥470.82.01。

第二关:PyTorch安装姿势。官网命令pip3 install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu118在某些Ubuntu源里会失败。我们改用清华源:pip3 install torch==2.0.1+cu118 torchvision==0.15.2+cu118 torchaudio==2.0.2+cu118 --extra-index-url https://download.pytorch.org/whl/cu118 --trusted-host pypi.tuna.tsinghua.edu.cn。装完后必须验证:python3 -c "import torch; print(torch.cuda.is_available(), torch.__version__)",输出True 2.0.1+cu118才算成功。

第三关:Ultralytics版本锁定。Ultralytics库更新频繁,v8.0.198和v8.1.0的API有差异(比如model.train()的参数名变了)。我们项目锁定ultralytics==8.0.198,用pip3 install ultralytics==8.0.198。装完后yolo version确认是8.0.198。

第四关:PyQt5兼容性pip3 install pyqt5在Ubuntu 22.04上会装PyQt5 5.15.10,但这个版本和Qt6的QPainter有冲突。必须降级:pip3 uninstall pyqt5 && pip3 install pyqt5==5.15.6。验证:python3 -c "from PyQt5.QtWidgets import QApplication; print('OK')"

第五关:OpenCV编译优化。系统自带的apt install python3-opencv是阉割版,不支持CUDA加速。我们自己编译:下载OpenCV 4.8.0源码,cmake -D CMAKE_BUILD_TYPE=RELEASE -D CMAKE_INSTALL_PREFIX=/usr/local -D WITH_CUDA=ON -D OPENCV_DNN_CUDA=ON -D CUDA_ARCH_BIN=7.5,8.6 -D WITH_CUDNN=ON ..,然后make -j$(nproc)。编译后cv2.getBuildInformation()里能看到CUDA:YEScuDNN:YES

注意:所有环境配置命令都写在env_setup.sh里,运行bash env_setup.sh一键执行,脚本里每步都有echo提示和set -e错误中断,避免半途失败。

4.2 训练流程实录:200轮训练的每一步操作

训练不是点一下就完事,我们把200轮拆成四个阶段,每阶段目标明确:

阶段一:预热期(Epoch 0-10)
命令:yolo train data=dataset.yaml model=yolov8n.pt epochs=10 imgsz=640 batch=16 device=0 name=train_v1_pretrain patience=0
目的:让模型适应我们的数据分布,不早停。此时conf=0.001iou=0.45,学习率线性预热。观察results.png里的box_loss是否从12.5降到3.2以下,若没降,说明数据有问题。

阶段二:主训练期(Epoch 11-150)
命令:yolo train data=dataset.yaml model=runs/train/train_v1_pretrain/weights/last.pt epochs=140 imgsz=640 batch=16 device=0 name=train_v1_main patience=50
关键动作:加载预热期的last.pt继续训,此时conf恢复为0.25,iou保持0.45。重点看val/mAP50(B)曲线,我们要求它在120轮时≥0.62,若没达到,立刻停训检查数据。

阶段三:微调期(Epoch 151-180)
命令:yolo train data=dataset.yaml model=runs/train/train_v1_main/weights/best.pt epochs=30 imgsz=640 batch=8 device=0 name=train_v1_finetune lr0=0.001
为什么降batch到8?因为微调要更精细,小batch让梯度更新更稳定。lr0降到0.001,防止过拟合。此时patience=20,早停更敏感。

阶段四:验证与导出(Epoch 181-200)
命令:yolo val data=dataset.yaml model=runs/train/train_v1_finetune/weights/best.pt imgsz=640 device=0
验证后,导出ONNX:yolo export model=runs/train/train_v1_finetune/weights/best.pt format=onnx imgsz=640 opset=12
最后,用onnxsim简化模型:python3 -m onnxsim runs/train/train_v1_finetune/weights/best.onnx runs/train/train_v1_finetune/weights/best_sim.onnx,把模型体积从22MB压到14MB,加载更快。

整个过程生成train_log.txt,记录每轮loss、mAP、lr变化。我们发现第167轮val/mAP50(B)达到峰值0.673,之后开始缓慢下降,所以最终选用best.pt而非last.pt

4.3 PyQt5界面开发:从空白窗口到功能完备的七步法

PyQt5界面不是拖控件那么简单,我们用七步法保证质量:

第一步:布局规划
QGridLayout划分四大区块:左上QLabel(视频显示区)、右上QGroupBox(控制面板)、左下QTextEdit(日志输出)、右下QTableWidget(检测结果列表)。所有控件尺寸用setMinimumSize()固定,避免拉伸变形。

第二步:视频流渲染优化
不用QLabel.setPixmap()直接塞QPixmap.fromImage(),因为QImage转换耗CPU。我们用QPainter直接在QLabel上绘图:

class VideoLabel(QLabel): def __init__(self): super().__init__() self.frame = None self.setScaledContents(True) def set_frame(self, frame): self.frame = frame self.update() # 触发paintEvent def paintEvent(self, event): if self.frame is not None: # 转numpy array -> QImage -> QPixmap h, w, ch = self.frame.shape bytes_per_line = ch * w q_img = QImage(self.frame.data, w, h, bytes_per_line, QImage.Format_RGB888) pixmap = QPixmap.fromImage(q_img) painter = QPainter(self) painter.drawPixmap(self.rect(), pixmap)

第三步:按钮事件绑定
所有按钮用clicked.connect()绑定,但关键是要防重复点击。比如“开始检测”按钮,点击后立刻self.start_btn.setEnabled(False),检测线程发回结果后再self.start_btn.setEnabled(True)。否则用户狂点,会创建一堆检测线程把GPU吃爆。

第四步:参数动态调节
QSlider调节置信度阈值,但valueChanged信号太频繁。我们加了防抖:self.conf_slider.valueChanged.connect(lambda v: QTimer.singleShot(300, lambda: self.on_conf_changed(v))),300ms内只响应最后一次滑动。

第五步:日志系统
不用print(),而是用QTextEdit.append(),并加时间戳和颜色:

def log(self, msg, level='INFO'): timestamp = QDateTime.currentDateTime().toString('HH:mm:ss') color_map = {'INFO': 'black', 'WARN': 'orange', 'ERROR': 'red'} html = f'<span style="color:{color_map[level]}">[{timestamp}] {level}: {msg}</span>' self.log_text.append(html) self.log_text.verticalScrollBar().setValue(self.log_text.verticalScrollBar().maximum())

第六步:结果表格
QTableWidget列设为['ID', 'Class', 'Confidence', 'X1', 'Y1', 'X2', 'Y2', 'Width', 'Height'],每行对应一个检测框。关键技巧是:用setItem()时,对数值列用QTableWidgetItem(str(val)),但设置setTextAlignment(Qt.AlignCenter)居中显示,视觉更清爽。

第七步:打包发布
pyinstaller --onefile --windowed --icon=icon.ico --add-data "weights;weights" --add-data "dataset;dataset" main.py打包。注意--add-data参数在Linux/macOS用:分隔,在Windows用;分隔,我们写了个build.bat自动判断系统。

4.4 开箱即用的终极验证:从双击exe到输出报警

所谓“开箱即用”,必须做到:客户拿到mosquito_detector.exe,双击就运行,无需任何前置操作。我们做了三重保障:

第一重:环境自检
程序启动时,先执行:

def check_env(): try: import torch, cv2, PyQt5 if not torch.cuda.is_available(): log("CUDA not available, using CPU mode", "WARN") if cv2.__version__ < '4.8': log("OpenCV version too old, may cause issues", "WARN") return True except ImportError as e: log(f"Missing dependency: {e}", "ERROR") return False

若缺失依赖,弹窗提示“请安装Python 3.9+并运行install_deps.bat”。

第二重:模型加载容错
weights/best.pt若损坏,自动降级到weights/yolov8n.pt(内置轻量预训练权重),保证程序不崩溃,只是精度略低。

第三重:硬件适配
检测到CPU核心数≤4时,自动禁用多进程数据加载,num_workers=0;检测到GPU显存<4GB时,自动设batch=4并提示“已切换至低功耗模式”。

最终验证流程:双击exe → 点击“打开摄像头” → 自动识别USB设备 → 点击“开始检测” → 界面实时显示绿色检测框 → 当检测到3只以上蚊蝇时,“报警”按钮变红并闪烁 → 点击“导出坐标”生成detection_results.csv,内容为:

timestamp,class,x1,y1,x2,y2,confidence 2023-10-15 14:22:31,mosquito,124.3,87.6,132.1,95.4,0.872 2023-10-15 14:22:31,fly,456.7,231.2,468.9,245.6,0.921

这才是真正的“开箱即用”。

5. 常见问题与排查技巧实录

5.1 训练阶段高频问题速查表

问题现象可能原因排查命令解决方案
loss一直为nan学习率过大或数据中有非法标签python tools/data_clean/validate_labels.py --src dataset/降低lr0至0.001,运行清洗脚本
val/mAP50始终<0.3数据集类别标注错误python tools/analyze_dataset.py --data dataset/检查classes.txt是否与label文件中class_id一致
训练卡在epoch 0CUDA驱动版本不匹配nvidia-sminvcc --version对比升级驱动至≥470.82.01,重装CUDA toolkit
GPU显存未占用PyTorch未识别GPUpython -c "import torch; print(torch.cuda.device_count())"重装PyTorch,确认--index-url指向cu118版本
box_loss下降但cls_loss不降类别不平衡严重python tools/analyze_dataset.py --data dataset/ --stat class对少数类样本做复制增强,或调整cls_pw参数

我们遇到最诡异的问题是:训练到第87轮时,val/mAP50突然从0.62跳到0.0,但train/box_loss正常。排查三天才发现,是某张验证图的label文件里,class_id写成了6(超出我们定义的0-5范围),YOLOv8把这类样本当负样本处理,导致评估失效。所以现在所有数据清洗脚本都强制校验class_id合法性。

5.2 PyQt5界面运行问题排查

问题:双击exe后黑窗口闪退
这是最常见的打包失败。根本原因是PyInstaller没打包进DLL依赖。解决方案:用Dependency Walker打开exe,看缺失哪些dll;或更简单,在打包命令后加--hidden-import PyQt5.sip --hidden-import PyQt5.QtCore --hidden-import PyQt5.QtGui。我们最终在build.bat里固化为:

pyinstaller --onefile --windowed --icon=icon.ico ^ --add-data "weights;weights" ^ --add-data "dataset;dataset" ^ --hidden-import PyQt5.sip ^ --hidden-import PyQt5.QtCore ^ --hidden-import PyQt5.QtGui ^ --hidden-import PyQt5.QtWidgets ^ main.py

问题:视频画面卡在第一帧不动
大概率是OpenCV的VideoCapture没释放。我们在CaptureThreadstop()方法里加了强制释放:

def stop(self): self.running = False if self.cap is not None: self.cap.release() # 关键! self.cap = None self.wait()

并且在主窗口关闭事件中调用self.capture_thread.stop()

问题:检测框位置偏移20像素
这是坐标映射没做缩放补偿。检查on_detection_result()函数里,是否用display_frame的尺寸计算缩放系数,而不是原始帧尺寸。我们曾因此返工两天,教训是:所有