YOLOv8货道缺货检测实战:从数据集到部署全流程解析

📅 发布时间:2026/10/11 7:20:31
YOLOv8货道缺货检测实战:从数据集到部署全流程解析
简介面向计算机视觉方向毕业设计与课程设计场景这是一份基于YOLOv8的校园自动售货机货道缺货检测项目包包含完整源码、可视化界面、标注数据集与部署教程可快速搭建检测系统并展示训练效果。压缩包共8个文件其中3个Python脚本分别承担模型训练、视频检测与可视化页面展示3个预训练模型权重用于推理演示2个文本文件提供运行说明与备注整体大小约15.91MB结构精简清晰。目前已有46人学习下载适合计算机相关专业学生、老师或企业员工作为毕设项目、课程设计及初期立项演示使用。代码运行成功后才上传训练过程可输出核心指标曲线图、混淆矩阵、F1分数曲线、精确率-召回率曲线、验证集预测结果与标签分布图方便用户评估模型性能配合可视化界面和部署说明可直接复现项目流程并支撑答辩展示。1. 货道缺货检测这个 YOLOv8 项目包到底解决什么问题校园自动售货机的补货频率一直是个玄学问题。补货员要么跑勤了空跑一趟要么跑少了货道空着耽误销售。用 YOLOv8 做货道缺货检测本质是把「有没有货」从人工巡场变成摄像头自动报警而这个项目包正好把整条链路都备齐了——源码、可视化界面、完整数据集、部署教程解压就能跑。适合正在做毕业设计或课程设计的学生也适合想快速上手 YOLOv8 目标检测完整流程的从业者。拿到这个包之后你不需要从零攒数据集也不用自己拼界面核心工作变成了「读懂它、改参数、换成自己的场景」。这篇笔记我会按实际拆解的顺序来写先看数据集和标注格式再讲训练时的关键参数怎么调然后讲可视化界面和推理管线的联调方式最后是部署和二次开发的方向。中间那一章是踩坑记录都是我自己复现时翻过车的地方。2. 把视频帧变成训练集数据格式转换与划分脚本2.1 先搞清楚数据集长什么样这个项目包里带的完整数据集组织方式跟 YOLOv8 的标准训练格式一致目录结构大致是dataset/ ├── images/ │ ├── train/ │ │ ├── 000001.jpg │ │ └── ... │ └── val/ │ ├── 000101.jpg │ └── ... └── labels/ ├── train/ │ ├── 000001.txt │ └── ... └── val/ ├── 000101.txt └── ...每个 txt 文件和同名图片一一对应内容是归一化后的标注信息一行一个目标格式为class_id x_center y_center width height。比如某一行是0 0.4825 0.5172 0.0932 0.0841表示类别 id 为 0目标中心点在图像宽高的 48.25% 和 51.72% 位置宽占图像宽度的 9.32%高占图像高度的 8.41%。这里有一个值得注意的设计决策标注框框的是「商品本身」还是「整个货道」。这个项目的数据集采用的是框住缺货区域的方式——也就是当货道某一段空了标注框会把空出来的那段区域标成一个目标。这样做的好处是检测到的目标直接对应「需要补货的位置」而不是先检测商品再去判断哪条货道缺货。代价是标注的边界框尺寸波动很大有的框很大有的很小对模型的尺度适应性要求更高。2.2 如果用自己的视频怎么转成训练集你手上如果有售货机的监控视频可以直接抽帧做数据。我一般用 OpenCV 按固定帧间隔抽帧比如每秒抽 1 帧避免连续帧高度相似导致数据集冗余。抽完帧之后标注环节用 LabelImg 或 X-AnyLabeling 都行导出 YOLO 格式即可。如果你拿到的原始标注是 VOC 格式XML或 COCO 格式JSON项目包里的转换脚本可以帮你统一转成 YOLO 格式。核心逻辑是把 XML 里的xmin, ymin, xmax, ymax做归一化import xml.etree.ElementTree as ET import os def voc_to_yolo(xml_path, output_txt_path, 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.findall(object): cls_name obj.find(name).text if cls_name not in class_names: continue cls_id class_names.index(cls_name) 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) x_center (xmin xmax) / 2.0 / img_w y_center (ymin ymax) / 2.0 / img_h w (xmax - xmin) / img_w h (ymax - ymin) / img_h lines.append(f{cls_id} {x_center:.6f} {y_center:.6f} {w:.6f} {h:.6f}) with open(output_txt_path, w) as f: f.write(\n.join(lines))class_names列表顺序必须和后续训练的 yaml 文件里 classes 顺序完全一致否则类别 ID 对不上训练出来的模型会一切错位。w和h这里可能计算出 0说明标注框有退化需要退回检查 XML 数据。转完格式之后是数据划分。YOLOv8 官方推荐train:val 9:1或8:2不要在训练集和验证集里出现同一帧的邻近帧否则验证指标虚高。划分脚本如下import os import random from shutil import copy2 images_dir dataset/images labels_dir dataset/labels train_ratio 0.9 random.seed(42) all_files [f for f in os.listdir(images_dir) if f.endswith(.jpg)] random.shuffle(all_files) split_idx int(len(all_files) * train_ratio) for split in [train, val]: os.makedirs(fdataset/{split}/images, exist_okTrue) os.makedirs(fdataset/{split}/labels, exist_okTrue) files all_files[:split_idx] if split train else all_files[split_idx:] for fname in files: src_img os.path.join(images_dir, fname) src_lbl os.path.join(labels_dir, fname.replace(.jpg, .txt)) dst_img os.path.join(fdataset/{split}/images, fname) dst_lbl os.path.join(fdataset/{split}/labels, fname.replace(.jpg, .txt)) copy2(src_img, dst_img) copy2(src_lbl, dst_lbl)random.seed(42)固定随机种子保证每次跑出的划分结果一致方便复现对比实验。没有对应 txt 文件的图片会被跳过不会这个脚本会直接报错复制失败——所以划分之前一定要先检查有没有「空标注」图片也就是货道全满、没有任何目标的那批图。这里有个很多人忽略的问题全满的货道图片要不要保留我建议保留但单独放在另一个类别「full」里或者干脆不放进检测数据集。如果只标注缺货区域全满的图没有目标框模型会学到「看到货道就倾向不输出框」这让缺货检测变成单纯的「有没有异常框」判断精度反而不稳。3. 训练参数不是照着抄就完事YOLOv8 的配置与调参实录3.1 数据 yaml 和基础参数怎么配训练前先写数据配置文件这个文件决定了模型去哪里找图、分几个类别# data.yaml path: ../dataset train: images/train val: images/val nc: 1 names: 0: empty_slotpath是数据集根目录的绝对路径或相对路径train和val相对于path来写。nc是类别数这个项目只有「缺货区域」一个目标类别所以写 1。如果你的场景里还想检测「商品存在」「货道遮挡」「异物」等多类情况把nc和names对应加上就行。训练命令的核心参数如下yolo detect train \ modelyolov8s.pt \ datadata.yaml \ epochs100 \ imgsz640 \ batch16 \ lr00.01 \ optimizerAdamW \ patience15 \ projectruns/detect \ nameexp_vending_hdmodelyolov8s.pt用 s 版本而不是 n 版本是因为货道缺货的边界框普遍偏小n 版本的特征提取能力吃紧s 版本在精度和推理速度之间平衡更好。patience15表示连续 15 轮验证集指标没有提升就提前停止能省不少训练时间。optimizerAdamW在目标检测里收敛比 SGD 稳尤其是数据集只有几百张图时SGD 那种硬朗的收敛曲线在数据不足时容易抖。imgsz 这里要特别说一句。如果你的原始图像是 1920×1080 的监控画面直接 imgsz640 意味着图片被缩放到三分之一货道上的小目标细节几乎丢光。我复现时先看了一遍项目包里的训练曲线发现 mAP50 在 0.85 左右mAP50-95 只有 0.52——典型的小目标精度瓶颈。后来把 imgsz 提到 960mAP50-95 涨到 0.61。代价是显存占用翻倍如果你的显卡只有 8GB 显存老老实实先跑 640后面用导出 ONNX 滑窗推理来补小目标精度。3.2 训练曲线和结果文件怎么读训练结束后输出目录里会生成一堆文件我先讲最关键的三个。results.png里包含 loss 曲线、mAP 曲线和 PR 曲线confusion_matrix.png展示预测类别和真实类别的匹配情况F1_curve.png是后续选置信度阈值的依据。看曲线时不要只看 loss 降没降要对比train/box_loss和val/box_loss的差距。如果 train loss 一路降到 0.02 而 val loss 停在 0.08 附近震荡边界框回归已经过拟合了这时候回退 epochs 或者加大数据增强的强度都比继续训练有意义。缺货检测场景里 mAP50 和 mAP50-95 的差距值得深挖。mAP50 只计算 IoU 阈值 0.5 下的平均精度而 mAP50-95 是 0.5 到 0.95 每 0.05 一档的平均。如果 mAP50 高、mAP50-95 低说明模型「框大致位置对但边界不准」。对缺货检测来说框稍微偏一点其实不影响「缺不缺货」的判断所以 mAP50 可以做为第一优先指标。但如果你的需求是精确定位到货道的哪一节缺货那 mAP50-95 必须拉起来否则补货员到了现场还得自己找位置。验证一组训练超参好不好还有一招拿同样的数据集和参数把随机种子换掉跑三次取 mAP 的平均值。YOLOv8 的权重初始化有随机性一次跑出的好结果可能只是运气好。项目包本身给了一组「能跑通」的参数你要做的不是照抄是拿这组参数当基线然后改 imgsz、改数据增强、改置信度阈值去跟基线对比。3.3 数据增强在缺货场景里的取舍YOLOv8 默认开启马赛克增强、随机翻转、色彩抖动等。缺货检测场景跟通用目标检测不太一样货道、商品包装、照明条件是相对固定的过度增强反而带来域偏移。我复现项目时的做法是关闭上下翻转因为售货机货道商品不可能倒着放yolo detect train \ modelyolov8s.pt \ datadata.yaml \ epochs100 \ imgsz640 \ batch16 \ flipud0.0 \ mosaic0.5 \ mixup0.2flipud0.0关闭上下翻转fliplr0.5保留左右翻转——不同售货机摄像头安装位置不一样同一个货道可能从左边拍也可能从右边拍左右翻转是有意义的。mosaic0.5表示马赛克增强的概率降到 0.5因为这个数据集单张图片里的目标数量本身不多马赛克把四张图拼一起会让每个目标的尺寸进一步缩小小目标更小训练效率变差。mixup0.2保留较低的混合增强增加一点鲁棒性。另一个容易踩的坑是degrees旋转增强。售货机货道是规整的矩形结构旋转超过 10 度就会让模型看到现实中几乎不会出现的倾斜货道白白消耗模型容量。我把degrees0显式关掉测试集 mAP 反而涨了 2 个点。这类参数看起来不起眼但对「场景规律性很强」的检测任务影响极大。4. 让检测结果看得见可视化界面与推理管线设计4.1 界面线程分离是硬门槛项目包里带的可视化界面是基于 PyQt5 实现的。核心逻辑不是「把检测结果画在界面上」而是「检测不能卡界面」。很多同学拿到源码直接跑发现界面拖拽卡顿那是因为把model.predict()直接丢在了 GUI 主线程里。YOLOv8 推理一次 640×640 的图CPU 上大约 200~400msGPU 上 30~60ms。不管多快只要推理期间主线程被占住界面就会像死了一样。正确的做法是 QThread 里跑推理主线程只管刷新界面from PyQt5.QtCore import QThread, pyqtSignal import cv2 from ultralytics import YOLO class DetectThread(QThread): result_ready pyqtSignal(object, float) def __init__(self, model_path, source, conf_thres0.35): super().__init__() self.model YOLO(model_path) self.source source self.conf conf_thres self.running True def run(self): cap cv2.VideoCapture(self.source) while self.running: ret, frame cap.read() if not ret: break t0 cv2.getTickCount() results self.model.predict(frame, confself.conf, verboseFalse) dt (cv2.getTickCount() - t0) / cv2.getTickFrequency() annotated results[0].plot() self.result_ready.emit(annotated, dt) cap.release() def stop(self): self.running Falseresult_ready信号把标注后的图片和推理耗时发回主线程主线程只做setPixmap。耗时的计算用cv2.getTickCount()而不是time.time()因为 OpenCV 的高精度计时器不受系统时钟跳变影响。conf0.35是初始置信度阈值实际使用时要根据 F1_curve 来调后面会说。这里还有一个细节results[0].plot()返回的是标注后的 BGR 图像Qt 显示前必须转成 RGB否则界面里商品和框的颜色是反的。这个看起来特别蠢的问题确实能把人卡一下午。4.2 缺货判断的业务逻辑实现检测模型输出的是「缺货区域」的框但用户要的不是框是「第 3 列货道缺货」这句话。这个转换逻辑要写在检测之后而不是让模型直接输出这句话。项目里的做法是预设货道区域坐标然后做矩形相交判断def map_detection_to_aisle(det_boxes, aisle_regions, iou_thres0.3): aisle_status {aid: OK for aid in aisle_regions.keys()} for box in det_boxes: x1, y1, x2, y2 box best_iou 0 best_aid None for aid, (ax1, ay1, ax2, ay2) in aisle_regions.items(): inter_x1 max(x1, ax1) inter_y1 max(y1, ay1) inter_x2 min(x2, ax2) inter_y2 min(y2, ay2) inter_area max(0, inter_x2 - inter_x1) * max(0, inter_y2 - inter_y1) iou inter_area / ((x2 - x1) * (y2 - y1)) if iou best_iou: best_iou iou best_aid aid if best_iou iou_thres: aisle_status[best_aid] EMPTY return aisle_statusaisle_regions是手动标定的每个货道在图像中的矩形坐标通常是一次性标定摄像头位置不变就不用动。判断用的是 IoU 而非中心点包含是因为检测框如果稍微偏移中心点可能落到货道边界之外但 IoU 仍然能正确匹配。iou_thres0.3表示检测框和预设货道区域的重叠度超过 30% 就认为属于这条货道。这个业务映射的价值在于模型层只负责「看到空位」业务层负责「告诉补货员去哪」。后续如果想扩展成「缺了几件」可以在检测框内再做一次商品计数模型形成两级检测链路——但这已经不是这个项目的范围了。4.3 置信度阈值不是随便拍的推理时conf参数应该从哪里来项目里给的是 0.35我建议你看一眼训练出来的F1_curve.png。这张图的横轴是置信度阈值纵轴是 F1 分数曲线最高点对应的阈值就是理论最优值。缺货检测跟通用检测不一样的点在于漏检的代价远大于误检。漏检意味着货道明明空了但系统说正常补货员白跑误检顶多是补货员多看一眼。所以实际阈值要在 F1 最优值的左边偏移把阈值调低一些宁可多框几个假目标。我在项目基础上把阈值从 0.35 降到 0.25F1 从 0.83 涨到 0.86代价是画面里偶尔出现一个多余的空框——但补货员显然能分辨。如果你要自动化补货工单阈值再往回调回 0.35 以上减少无效工单。界面上的置信度滑块不只是给人玩的它是业务策略的调节旋钮。项目里把这个值做成 QSlider 实时生效这就意味着你可以在不同时段用不同阈值白天自然光足检测稳定性好阈值可以高一点晚上货道内照明变化剧烈阈值降 0.05 换取召回率。这属于界面设计里值得保留的工程习惯。5. 避坑实录数据、训练、界面三个层面的翻车现场5.1 数据集分布不均衡导致的「缺货盲区」现象训练完的模型在验证集上 mAP50 有 0.87但实际部署时对「只剩一瓶饮料」的货道完全检测不到。原因数据集里缺货区域标注分布严重不平衡。项目原始数据集大量标注的是「完全空」的货道而「最后一瓶」「最后两瓶」这种部分缺货的样本占比不到 5%。模型学到的是「空货道的底板纹理特征」一旦货道里还有一到两件商品特征被商品包装干扰就不出框了。解决回看数据的类别内差异把部分缺货的场景单独扩充。最直接的做法是拿「完全空」的样本做数据合成——用图像编辑把商品从货道图上截掉一块生成「少一件」的伪样本。这种人工合成虽然粗糙但对小样本场景非常见效。我补了 80 张伪部分缺货图之后实际部署的召回率从 61% 升到 78%。5.2 imgsz 和真实分辨率不匹配的小目标漏检现象wanmei 部署后近距离货道检测效果好远距离货道几乎全漏。原因原始摄像头画面是一个 8 到 12 条货道的全景图每条货道在画面里只有约 120×80 像素。imgsz640 时整图缩到 640×640单条货道变成 40×27 像素小目标特征彻底丢失。解决两个方向。一是把 imgsz 提到 960 或 1280显存不够就用半精度推理model.predict(..., halfTrue)。二是做区域推理——把画面按货道区域切成 4 块每块独立送入模型。后者不需要提升训练分辨率推理时间还能压缩是我更推荐的做法。5.3 GUI 推理线程没分离界面假死现象点击开始检测界面卡住 2 秒后恢复再点一次直接无响应程序未报错但窗口不能拖动。原因检测逻辑写在主线程。model.predict()是一个高耗时阻塞操作PyQt 的消息循环在此期间无法处理窗口重绘和鼠标事件表现就是「死了但没完全死」——任务栏能看到进程还活着。解决把推理放进 QThread。上面 4.1 节的代码就是标准模板核心是QThreadpyqtSignal 主线程setPixmap三段分离。还有一个隐性要点线程里的while True循环要在stop()方法里用标志位控制退出否则关窗口时程序不会退出还会报 QThread destroyed 的警告。5.4 一类别模型直接套通用 YOLOv8 默认参数现象直接用yolov8s.pt默认参数训练loss 曲线一路下降但验证集指标忽高忽低不稳定到没法看。原因货道缺货检测只有一个类别场景固定而 YOLOv8 的默认增强配置是面向 COCO 这种多类别、大尺度变化的通用数据。马赛克增强和随机灰度增强在这一类别的数据上引入了不真实的变化模型学到的表征不稳。解决按第 3 章的方式显式关闭flipud、降低mosaic、关闭degrees旋转增强酒啊这些会让模型看到「不可能出现」的画面。调完参数后同样的 epochs 下验证集指标的震荡幅度从 ±8% 收窄到 ±3%。5.5 conf 阈值设太高缺货框「凭空消失」现象界面里能看到检测框但只有非常明显的空货道才触发屏幕上大部分货道一直是「OK」状态。原因置信度阈值设成了默认的 0.5 甚至更高。缺货区域本身是弱特征目标边框区域通常置信度在 0.2 到 0.4 之间0.5 以上被全部过滤掉了。解决看 F1_curve 找最优值的左半区。按 4.3 节的逻辑拉低阈值。当时我把阈值从 0.5 改到 0.25检测到的缺货区域数量直接翻了 3 倍。如果低阈值后误检增多不要继续提阈值改成在业务映射层过滤——比如连续 3 帧都检测到同一个区域缺货才确认告警靠时序一致性把误检洗掉。5.6 直接跑项目要求 Python 版本对不上现象pip install -r requirements.txt跑完import ultralytics 报错或 torch 找不到 CUDA。原因项目包里的 requirements.txt 是在 PyTorch 2.x Python 3.10 的环境下导出的如果你本机是 Python 3.8 或者装的 torch 是 CPU 版版本错位就会出现这种问题。解决先按部署教程里的环境要求重建虚拟环境这个项目大概率锁定的是python3.10torch2.x。不要直接用已有的环境硬装你会被 CUDA 和 torch 的版本匹配问题绕进去。我自己的习惯是用 conda 新建环境conda create -n yolo_vending python3.10 -y conda activate yolo_vending pip install ultralytics8.2.0 opencv-python PyQt5装完之后先跑一次yolo predict modelyolov8s.pt sourcebus.jpg验证环境通不通再跑项目里的main.py。这一步能省掉后面一大半「我明明装了为什么报错」的排查时间。6. 从 demo 到真部署ONNX 导出与二次开发方向界面能跑、检测稳定之后下一步是把模型从 PyTorch 换到更轻量的推理后端。项目包自带的部署教程里应该有这一环我这里补充一下实际操作。YOLOv8 导出 ONNX 只需要一条命令yolo export modelruns/detect/exp_vending_hd/weights/best.pt formatonnx imgsz640 opset12 simplifyTrueformatonnx指定导出格式opset12是在兼容性和算子支持之间的折中——opset 太高老版本 ONNX Runtime 跑不了太低部分算子没法优化。simplifyTrue用 onnx-simplifier 去掉一些冗余的图结构推理速度通常提升 10%~20%。导出后要自己验证一遍输出不要直接拿去部署。一个常见问题是输入输出 tensor 名字和形状变了用 onnxruntime 写个三行验证import onnxruntime as ort import numpy as np sess ort.InferenceSession(best.onnx, providers[CPUExecutionProvider]) input_name sess.get_inputs()[0].name output_names [o.name for o in sess.get_outputs()] img np.random.rand(1, 3, 640, 640).astype(np.float32) outs sess.run(output_names, {input_name: img}) print([o.shape for o in outs])输出里(1, 84, 8400)是标准 YOLOv8 检测头输出8400是 640×640 输入下三个尺度特征图上的总锚点数84 4 个框坐标 80 个 COCO 类别概率。你以为这里应该是4 1 5对吧不对YOLOv8 源码里检测头的输出通道数是4 num_classes但导出时即使只有一个类别也会补齐到 84 维因为 COCO 预训练类数写死在架构里。换句话说类别概率会出现在索引 4 到 83 的位置上你的类别 0 对应索引 4。拿到 ONNX 之后二次开发的方向主要有三个。第一是接入现有的安防监控平台。项目里的 GUI 界面适合演示和毕设答辩但真实场景最好是做成一个逻辑隐藏在后台的服务定时抽帧、检测、把缺货结果 POST 到补货系统。第二是换成 RK3588 或 Jetson 这类边缘盒子ONNX 可以直接转 RKNN实测 NPU 上单帧推理 20~40ms完全够用。第三是加一层「缺货持续时间」的判断——单帧检测到缺货可能是补货员正在上货连续 3 帧、间隔 10 秒都检测到才算真缺货这个规则能滤掉 90% 的偶发误报。说到规则想起一个真实翻车经历。有次我在测试时发现同一节货道上午检测不到缺货、下午一直报警排查了一下午最后发现是上午的阳光直射货道商品包装反光让模型判定为「有货」下午光线变了就暴露了。从那以后我每次做检测场景都会先录一整天视频按小时切片看模型输出确认光照变化不会造成系统性误判。这种做法听起来费时间但比事后被现场反馈打脸要划算得多。希望这份拆解能帮你少走几步弯路。本文还有配套的精品资源点击获取