☰
YOLOv8货道缺货检测实战:从数据集到部署全流程解析
2026/10/11 7:20:16 网站建设 项目流程

简介:面向计算机视觉方向毕业设计与课程设计场景,这是一份基于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(f"dataset/{split}/images", exist_ok=True) os.makedirs(f"dataset/{split}/labels", exist_ok=True) 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(f"dataset/{split}/images", fname) dst_lbl = os.path.join(f"dataset/{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_slot

path是数据集根目录的绝对路径或相对路径,train和val相对于path来写。nc是类别数,这个项目只有「缺货区域」一个目标类别,所以写 1。如果你的场景里还想检测「商品存在」「货道遮挡」「异物」等多类情况,把nc和names对应加上就行。

训练命令的核心参数如下:

yolo detect train \ model=yolov8s.pt \ data=data.yaml \ epochs=100 \ imgsz=640 \ batch=16 \ lr0=0.01 \ optimizer=AdamW \ patience=15 \ project=runs/detect \ name=exp_vending_hd

model=yolov8s.pt用 s 版本而不是 n 版本,是因为货道缺货的边界框普遍偏小,n 版本的特征提取能力吃紧,s 版本在精度和推理速度之间平衡更好。patience=15表示连续 15 轮验证集指标没有提升就提前停止,能省不少训练时间。optimizer=AdamW在目标检测里收敛比 SGD 稳,尤其是数据集只有几百张图时,SGD 那种硬朗的收敛曲线在数据不足时容易抖。

imgsz 这里要特别说一句。如果你的原始图像是 1920×1080 的监控画面,直接 imgsz=640 意味着图片被缩放到三分之一,货道上的小目标细节几乎丢光。我复现时先看了一遍项目包里的训练曲线,发现 mAP50 在 0.85 左右,mAP50-95 只有 0.52——典型的小目标精度瓶颈。后来把 imgsz 提到 960,mAP50-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 \ model=yolov8s.pt \ data=data.yaml \ epochs=100 \ imgsz=640 \ batch=16 \ flipud=0.0 \ mosaic=0.5 \ mixup=0.2

flipud=0.0关闭上下翻转,fliplr=0.5保留左右翻转——不同售货机摄像头安装位置不一样,同一个货道可能从左边拍也可能从右边拍,左右翻转是有意义的。mosaic=0.5表示马赛克增强的概率降到 0.5,因为这个数据集单张图片里的目标数量本身不多,马赛克把四张图拼一起会让每个目标的尺寸进一步缩小,小目标更小,训练效率变差。mixup=0.2保留较低的混合增强,增加一点鲁棒性。

另一个容易踩的坑是degrees旋转增强。售货机货道是规整的矩形结构,旋转超过 10 度就会让模型看到现实中几乎不会出现的倾斜货道,白白消耗模型容量。我把degrees=0显式关掉,测试集 mAP 反而涨了 2 个点。这类参数看起来不起眼,但对「场景规律性很强」的检测任务影响极大。

4. 让检测结果看得见:可视化界面与推理管线设计

4.1 界面线程分离是硬门槛

项目包里带的可视化界面是基于 PyQt5 实现的。核心逻辑不是「把检测结果画在界面上」,而是「检测不能卡界面」。

很多同学拿到源码直接跑,发现界面拖拽卡顿,那是因为把model.predict()直接丢在了 GUI 主线程里。YOLOv8 推理一次 640×640 的图,CPU 上大约 200~400ms,GPU 上 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_thres=0.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, conf=self.conf, verbose=False) dt = (cv2.getTickCount() - t0) / cv2.getTickFrequency() annotated = results[0].plot() self.result_ready.emit(annotated, dt) cap.release() def stop(self): self.running = False

result_ready信号把标注后的图片和推理耗时发回主线程,主线程只做setPixmap。耗时的计算用cv2.getTickCount()而不是time.time(),因为 OpenCV 的高精度计时器不受系统时钟跳变影响。conf=0.35是初始置信度阈值,实际使用时要根据 F1_curve 来调,后面会说。

这里还有一个细节:results[0].plot()返回的是标注后的 BGR 图像,Qt 显示前必须转成 RGB,否则界面里商品和框的颜色是反的。这个看起来特别蠢的问题,确实能把人卡一下午。

4.2 缺货判断的业务逻辑实现

检测模型输出的是「缺货区域」的框,但用户要的不是框,是「第 3 列货道缺货」这句话。这个转换逻辑要写在检测之后,而不是让模型直接输出这句话。项目里的做法是预设货道区域坐标,然后做矩形相交判断:

def map_detection_to_aisle(det_boxes, aisle_regions, iou_thres=0.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_status

aisle_regions是手动标定的每个货道在图像中的矩形坐标,通常是一次性标定,摄像头位置不变就不用动。判断用的是 IoU 而非中心点包含,是因为检测框如果稍微偏移,中心点可能落到货道边界之外,但 IoU 仍然能正确匹配。iou_thres=0.3表示检测框和预设货道区域的重叠度超过 30% 就认为属于这条货道。

这个业务映射的价值在于:模型层只负责「看到空位」,业务层负责「告诉补货员去哪」。后续如果想扩展成「缺了几件」,可以在检测框内再做一次商品计数模型,形成两级检测链路——但这已经不是这个项目的范围了。

4.3 置信度阈值不是随便拍的

推理时conf参数应该从哪里来?项目里给的是 0.35,我建议你看一眼训练出来的F1_curve.png。这张图的横轴是置信度阈值,纵轴是 F1 分数,曲线最高点对应的阈值就是理论最优值。

缺货检测跟通用检测不一样的点在于:漏检的代价远大于误检。漏检意味着货道明明空了但系统说正常,补货员白跑;误检顶多是补货员多看一眼。所以实际阈值要在 F1 最优值的左边偏移,把阈值调低一些,宁可多框几个假目标。我在项目基础上把阈值从 0.35 降到 0.25,F1 从 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 像素。imgsz=640 时,整图缩到 640×640,单条货道变成 40×27 像素,小目标特征彻底丢失。

解决:两个方向。一是把 imgsz 提到 960 或 1280,显存不够就用半精度推理model.predict(..., half=True)。二是做区域推理——把画面按货道区域切成 4 块,每块独立送入模型。后者不需要提升训练分辨率,推理时间还能压缩,是我更推荐的做法。

5.3 GUI 推理线程没分离,界面假死

现象:点击开始检测,界面卡住 2 秒后恢复,再点一次直接无响应,程序未报错但窗口不能拖动。

原因:检测逻辑写在主线程。model.predict()是一个高耗时阻塞操作,PyQt 的消息循环在此期间无法处理窗口重绘和鼠标事件,表现就是「死了但没完全死」——任务栏能看到进程还活着。

解决:把推理放进 QThread。上面 4.1 节的代码就是标准模板,核心是QThread+pyqtSignal+ 主线程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 版,版本错位就会出现这种问题。

解决:先按部署教程里的环境要求重建虚拟环境,这个项目大概率锁定的是python=3.10+torch==2.x。不要直接用已有的环境硬装,你会被 CUDA 和 torch 的版本匹配问题绕进去。我自己的习惯是用 conda 新建环境:

conda create -n yolo_vending python=3.10 -y conda activate yolo_vending pip install ultralytics==8.2.0 opencv-python PyQt5

装完之后先跑一次yolo predict model=yolov8s.pt source=bus.jpg验证环境通不通,再跑项目里的main.py。这一步能省掉后面一大半「我明明装了为什么报错」的排查时间。

6. 从 demo 到真部署:ONNX 导出与二次开发方向

界面能跑、检测稳定之后,下一步是把模型从 PyTorch 换到更轻量的推理后端。项目包自带的部署教程里应该有这一环,我这里补充一下实际操作。YOLOv8 导出 ONNX 只需要一条命令:

yolo export model=runs/detect/exp_vending_hd/weights/best.pt format=onnx imgsz=640 opset=12 simplify=True

format=onnx指定导出格式,opset=12是在兼容性和算子支持之间的折中——opset 太高,老版本 ONNX Runtime 跑不了;太低,部分算子没法优化。simplify=True用 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% 的偶发误报。

说到规则,想起一个真实翻车经历。有次我在测试时发现同一节货道上午检测不到缺货、下午一直报警,排查了一下午,最后发现是上午的阳光直射货道,商品包装反光让模型判定为「有货」,下午光线变了就暴露了。从那以后我每次做检测场景都会先录一整天视频,按小时切片看模型输出,确认光照变化不会造成系统性误判。这种做法听起来费时间,但比事后被现场反馈打脸要划算得多。希望这份拆解能帮你少走几步弯路。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询