资讯动态

YOLOv8垃圾分类实战:从模型训练到部署与语音联动

发布时间:2026/9/12 22:31:02 来源:尧图企业网站定制
简介基于YOLOv8的社区垃圾分类语音指导项目是面向毕业设计、课程设计等场景的一站式计算机视觉方案。资源整体约15.91MB共8个文件包含3个Python源码、3个PyTorch模型权重和2个说明文档其中源码对应可视化界面、视频检测与模型训练三个模块权重文件覆盖多个YOLO版本配套说明文档提供部署指引。项目集成完整数据集运行后可输出核心指标曲线、混淆矩阵、F1分数曲线、精确率-召回率曲线、验证集预测结果及标签分布图能够支撑毕设答辩展示。代码已经过测试可直接运行配合可视化界面与部署说明从模型训练到界面演示形成闭环。目前已有30人学习下载适合目标检测与深度学习入门者、相关专业学生及需要快速搭建项目的开发者直接使用或二次修改。1. YOLOv8在垃圾分类场景里的真实定位社区垃圾房的摄像头通常盯着人手里的垃圾袋。目标检出之后要做的不是存档而是让人把东西扔进对的桶。YOLOv8在这类方案里承担的是“看见并分类”这一步它后面还连着可视化界面和语音播报所以单纯把mAP刷高并不能让项目交付。你真正要打通的是完整链路摄像头取流YOLOv8推理出类别和置信度界面实时画框再根据类别触发对应语音。下面按模型到界面的顺序把链路拆开讲覆盖网络结构、数据集组织、训练参数、三种部署路径和GUI联动。适合两类人一类是做毕设或课程设计、需要完整demo的学生另一类是给社区垃圾房做低成本视觉提示的工程师。建议前置准备一台能跑推理的电脑、几百到几千张带标注垃圾图片以及一块能出声的屏幕。2. 先把YOLOv8网络结构和垃圾分类数据集对齐2.1 一张640×640的图片在YOLOv8网络结构里怎么走YOLOv8的网络结构可以简化成三段Backbone提取特征Neck做多尺度融合Head输出分类和框回归结果。Backbone里的核心模块是C2f它把输入通道拆成两条支路其中一条经过若干个Bottleneck后再与另一条拼接这样每一层都能看到更丰富的梯度流比上一代YOLOv5的C3结构在同样参数量下特征表达更细。Neck沿用PAN-FPN结构把高层语义信息往下传同时把底层细节往上带最终输出P3、P4、P5三个尺度的特征图。特征层下采样倍数输出尺寸主要服务对象P38倍80×80小目标烟头、瓶盖、小纸团P416倍40×40中等目标易拉罐、饮料瓶、纸盒P532倍20×20大目标大纸箱、整袋垃圾社区垃圾房的真实镜头里小目标占比很高尤其是烟头和瓶盖所以P3这一层对最终效果影响很大。很多项目训练时直接把imgsz降到416省显存结果就是小目标漏检明显。Head部分则是Anchor-Free的Decoupled Head分类和回归各走各的分支不依赖预置锚框正样本分配用TASKAlignedAssigner对遮挡和形变的容忍度比锚框方案更好。理解这一层之后你再去网络上找yolov8网络结构图会发现很多图把C2f和C3画混对照上面的三段式和特征表基本就能看明白。2.2 社区垃圾分类数据集的目录和TXT标注格式拿到一个数据集先别急着训练先确认目录结构是不是YOLOv8默认约定的images/labels并列形式。常见做法是下面这种datasets/ ├── images/ │ ├── train/ # 训练图片jpg/png均可 │ └── valid/ # 验证图片 ├── labels/ │ ├── train/ # 与图片同名的txt │ └── valid/ └── data.yaml标注文件是txt每一行代表一个目标格式为“类别ID x_center y_center width height”其中坐标都做了归一化。看一个实例# datasets/labels/train/img_001.txt 0 0.523 0.412 0.318 0.426 3 0.120 0.830 0.056 0.098第一行表示类别0的目标中心点在图片的52.3%和41.2%位置宽高分别占图片的31.8%和42.6%。归一化坐标的好处是不同分辨率的图片可以共用一套标注训练时resize到640也不会导致框错位。社区垃圾分类在演示项目里通常简化为四类对应关系如下类别ID名称常见实物0recyclable 可回收物纸箱、易拉罐、矿泉水瓶1kitchen 厨余垃圾剩菜剩饭、果皮、菜叶2hazardous 有害垃圾电池、过期药品、灯管3other 其他垃圾纸巾、一次性餐具、污损塑料袋很多开源数据集是二十几个细分小类比如单把“塑料袋”和“一次性餐盒”分开标。拿到这种数据后我一般会先做类别合并再按上面四类重新映射ID因为在垃圾房现场用户只需要知道投哪个桶不需要知道具体是什么品牌。合并之后如果某个类别图片数明显偏少再考虑补拍或者做增强否则训练出来的mAP会被少数类拖低。2.3 data.yaml里的路径和类别ID为什么容易踩坑data.yaml是整个训练和推理的数据入口下载下来的项目里它经常是第一个出问题的地方。常见写法path: D:/trash_dataset # 数据集根目录改成你自己机器上的路径 train: images/train val: images/valid nc: 4 names: 0: recyclable 1: kitchen 2: hazardous 3: otherpath可以是绝对路径也可以是相对路径。如果你把项目文件放在U盘里带到别的电脑跑绝对路径一旦变化训练时就会报“Dataset not found”。我一般建议把path直接删掉让train和val写成完整的相对路径比如train: D:/..../images/train不够稳妥更好的做法是固定项目根目录后用相对路径如下path: . train: datasets/images/train val: datasets/images/valid这里path: .表示data.yaml所在的目录。注意names的数量必须和nc一致且标注文件里不能出现大于nc-1的类别ID否则加载数据时直接报错或者静默丢掉这些框。模型训练前先快速验证一下标注是否正确用下面这条命令把标注框画到图上yolo detect train modelyolov8s.pt datadata.yaml epochs1 imgsz640第一次训练只能跑通不代表数据没问题最好再配合Python脚本抽查几张图我习惯直接打开images和labels同名文件比对坐标确认没有出现“标签对不上图”的情况再正式训练。3. YOLOv8训练自己的数据集一条命令和六个关键参数3.1 YOLOv8训练自己的数据集最小命令和六个关键参数数据准备好之后训练命令本身很简单yolo detect train \ modelyolov8s.pt \ datadata.yaml \ epochs100 \ imgsz640 \ batch16 \ patience15 \ device0 \ projectruns/detect \ nametrash_cls第一次训练时如果本地没有yolov8s.ptUltralytics会自动下载对应权重离线环境里提前把权重文件放到命令行所在目录训练时就不会卡在下载环节。用预训练权重做微调而不是随机初始化是垃圾分类这类小数据集任务必须坚持的做法COCO上学到的纹理和边缘特征对瓶罐、纸盒这类目标迁移效果非常明显。参数推荐值说明modelyolov8s.pt显存小于6G用yolov8n内存和精度要平衡选yolov8simgsz640小目标多时试768显存吃紧再退回640epochs100配合patience早停不用纠结具体轮数batch168G显存建议8显存不足先降batch而不是降imgszpatience15验证集指标连续15轮不涨就自动停止device0CPU训练写cpu但速度会慢一个量级如果你的显卡只有4G显存imgsz640batch2也能跑只是bn统计量不稳效果会比batch16差不少。这种情况下梯度累积可以通过Ultralytics的参数配置来做或者干脆用yolov8n。Windows上配置yolov8环境时pip install ultralytics会自动装好主要依赖最容易出问题的是torch和CUDA版本不匹配建议先装好对应CUDA版PyTorch再装ultralytics。3.2 训练完先看results.png再画自定义损失曲线训练结束后runs/detect/trash_cls/目录下会生成results.png里面有train/box_loss、val/box_loss、metrics/precision、recall、mAP50、mAP50-95一共八条子图。判断训练状态时我会按四条规则来看train损失和val损失同步下降属于正常收敛val损失先降后升train损失还在降说明过拟合回看是否数据增强开少了mAP50还凑合但mAP50-95明显偏低说明框定位不准问题多在标注框贴合度不够mAP50和mAP50-95最后几轮还在缓慢上升可以继续训练不用急着停。如果想看某一段epoch区间的曲线或者把训练和验证损失画到同一张图里对比Ultralytics的results.csv里保存了全部原始数据import pandas as pd import matplotlib.pyplot as plt df pd.read_csv(runs/detect/trash_cls/results.csv) df.columns [c.strip() for c in df.columns] # 列名带前导空格必须先清理 plt.figure(figsize(10, 6)) plt.plot(df[epoch], df[train/box_loss], labeltrain box loss) plt.plot(df[epoch], df[val/box_loss], labelval box loss) plt.xlabel(epoch) plt.ylabel(loss) plt.legend() plt.grid(alpha0.3) plt.savefig(box_loss_curve.png, dpi200)results.csv第一行是列名但列名前可能带着空格直接用df[train/box_loss]取列会报KeyError所以我先做了一次列名strip。很多人在这一步卡住其实不是画图代码的问题就是列名没清理。把这段脚本存成plot_curve.py每次训练完跑一遍就能拿到自定义的yolov8损失函数曲线图。3.3 训练遇到显存、mAP为0和版本问题时先查这三处下面的表总结了我多次排错后最常遇到的三个问题如果你训练时也报类似错误优先往这几个方向查现象原因与操作训练中途报CUDA out of memorybatch降到4或8再不行把imgsz降到480但小目标场景不要长期用低分辨率mAP始终为0或接近0先检查labels目录里的txt是否有空文件再检查类别ID是否超过nc-1最后看data.yaml的val路径是否写对导入ultralytics时报版本冲突用pip check查看依赖冲突常见是numpy版本过新或过旧conda环境重建最省事训练集和验证集不能有重叠图片否则mAP会虚高。很多人把一个文件夹既当train又当val训练完看到mAP 0.95很高兴拿到现场一测直接露馅。社区垃圾分类的样本里同一个瓶子的多张连续帧也算重叠做数据集切分时最好按拍摄批次切而不是按单张随机切。4. 部署路径怎么选PyTorch直出、ONNX转换与RK3588边缘盒子4.1 先算算垃圾房需要多少帧率语音交互不追帧部署前先算需求账。语音播报从检测到发声一般需要0.8到1.5秒也就是说哪怕检测端只有2到3帧每秒交互也完全跟得上。社区垃圾房没有自动驾驶那种高帧率需求真正的瓶颈在语音链路和界面响应不在目标检测。这个结论直接影响选型如果只在演示电脑上跑PyTorch直出就够用如果垃圾房要长期开机功耗和可靠性优先再考虑ONNX或边缘盒子。部署路径运行设备模型格式适用场景方案A PyTorch直出带显卡的演示电脑.pt毕设展示、功能调试方案B ONNX推理x86工控机、无显卡主机.onnx已有x86主机追求低资源推理方案C RKNN边缘部署RK3588等开发板.rknn长时间无人值守、低功耗节点方案A直接跑一句yolo predict modelbest.pt sourcetest.jpg就能验证模型适合刚训练完的快速检查。方案B和C的差异主要在硬件约束下面重点讲ONNX的导出和验证。4.2 用ONNX导出并在onnxruntime里验证推理导出ONNX直接用Ultralytics命令行yolo export modelruns/detect/trash_cls/weights/best.pt formatonnx imgsz640 opset12 simplifyTrue导出后在onnxruntime里做一次推理验证确认输入输出匹配。注意一个关键细节当你的类别数nc4时输出张量shape是(1, 8, 8400)其中8 4个框坐标 4个类别得分8400 80×80 40×40 20×20。网上大量YOLOv8后处理代码默认输出是(1, 84, 8400)那是COCO 80类的写法直接套用必然越界或解码错位。import cv2 import numpy as np import onnxruntime as ort session ort.InferenceSession(best.onnx) input_name session.get_inputs()[0].name # 预处理按YOLOv8的letterbox方式缩放到640x640再除以255 # 这里省略完整letterbox实现可参考ultralytics源码中的同名函数 blob letterbox(img, (640, 640)) blob blob[:, :, ::-1].transpose(2, 0, 1)[None].astype(np.float32) / 255.0 out session.run(None, {input_name: blob})[0] # (1, 8, 8400) pred out[0].T # 转成 (8400, 8) cx, cy, w, h pred[:, 0], pred[:, 1], pred[:, 2], pred[:, 3] x1, y1 cx - w / 2, cy - h / 2 x2, y2 cx w / 2, cy h / 2 boxes_xyxy np.stack([x1, y1, x2, y2], axis1) scores pred[:, 4:].max(axis1) class_ids pred[:, 4:].argmax(axis1) # cv2.dnn.NMSBoxes要求输入为(x, y, w, h)不是x1y1x2y2 boxes_xywh np.stack([x1, y1, w, h], axis1) keep cv2.dnn.NMSBoxes(boxes_xywh.tolist(), scores.tolist(), 0.25, 0.45)导出端在DFL解码上已经做了处理onnx输出直接就是坐标和分类得分不需要自己实现分布焦点损失解码。这里最容易错的是最后一步cv2.dnn.NMSBoxes的前两个参数分别是框坐标列表和得分列表坐标必须是(x, y, w, h)形式而很多人习惯把xyxy直接传进去NMS结果就会乱。除此之外置信度阈值0.25和NMS阈值0.45是分开的两个参数在垃圾场景里如果发现易拉罐和矿泉水瓶互相误判先把置信度阈值提到0.35或0.4仔细观察不要急着动NMS阈值。4.3 RK3588转RKNN时的标定图和色彩通道注意ONNX验证通过后如果要把模型部署到RK3588后面的流程是把best.onnx转成RKNN格式再通过RKNN-Toolkit2的工具链完成模型转换和量化。转换时通常需要准备几百张有代表性的图片作为量化标定集标定图和训练集同分布是最基本要求。有些人直接拿训练集里的清晰特写照去量化结果在垃圾房暗光场景下掉点严重原因是数据分布不一致。转换前还要确认两件事一是导出的ONNX用opset 11或12比较通用opset过高可能引入工具链不支持的算子二是预处理时的通道顺序。YOLOv8训练时用的是RGB如果你在板端采集的是BGR图像又不做转换就直接喂给RKNN模型瓶罐这类依赖颜色的类别会立刻出现误判。建议在转模型时就把输入格式固定成训练一致的模式并在板端代码里强制转换。C2f算子在较新的RKNN-Toolkit2版本里有较好支持但每个小版本行为有差异转换前先跑一遍自带的yolov8 demo能通过再做自定义类别否则会分不清是模型问题还是工具链问题。5. 可视化界面与语音指导联动GUI、TTS和防抖播报5.1 用Tkinter搭一个带实时画框的可视化界面可视化界面在毕设展示里的需求其实很固定显示摄像头画面、画检测框、显示类别和置信度、放一个开始按钮。Tkinter完全够用代码量最小依赖最少。下面是一个能跑通的基础框架import tkinter as tk import cv2 from PIL import Image, ImageTk from ultralytics import YOLO model YOLO(weights/best.pt) cap cv2.VideoCapture(0) playing False def update_frame(): if not playing: return ok, frame cap.read() if not ok: return results model.predict(frame, conf0.30, verboseFalse)[0] for box in results.boxes: x1, y1, x2, y2 map(int, box.xyxy[0]) cls int(box.cls[0]) conf float(box.conf[0]) cv2.rectangle(frame, (x1, y1), (x2, y2), (0, 255, 0), 2) cv2.putText(frame, f{model.names[cls]} {conf:.2f}, (x1, max(0, y1 - 8)), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 255, 0), 2) img_rgb cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) img_tk ImageTk.PhotoImage(Image.fromarray(img_rgb)) label_video.configure(imageimg_tk) label_video.image img_tk root.after(50, update_frame) # 50ms一次约20fps def start(): global playing playing True root.after(50, update_frame) def stop(): global playing playing False root tk.Tk() root.title(社区垃圾分类语音指导) label_video tk.Label(root) label_video.pack() btn_frame tk.Frame(root) btn_frame.pack(pady8) tk.Button(btn_frame, text开始识别, commandstart, width12).pack(sidetk.LEFT, padx6) tk.Button(btn_frame, text停止, commandstop, width12).pack(sidetk.LEFT, padx6) root.mainloop()root.after(50, update_frame)是Tkinter里做周期调用的正确方式不能用time.sleep否则整个界面会卡死。CPU推理扛不住每帧检测时把间隔从50改成100或150也就是每0.1到0.15秒检测一次视觉上依然流畅CPU占用却明显下降。不要在按钮回调里直接写检测循环模型推理会阻塞主线程界面表现为拖动窗口时彻底无响应。5.2 语音指导的三种播报方案TTS还是预录音频检测结果的最后一步是语音播报。常用的方案有三种各自适用场景差别很大方案离线可用音质依赖适用场景pyttsx3支持机械感明显pip install pyttsx3Linux需espeak-ng工控机无外网edge-tts不支持自然需要网络访问在线服务演示时效果最好预录音频支持取决于录制质量只需要音频播放库四分类固定话术社区垃圾房的播报话术通常是固定几句话比如“这是可回收物请投入蓝色垃圾桶”最省心的其实是预录音频连TTS引擎都不依赖。如果话术需要动态拼接比如带上置信度或当前时间再用pyttsx3现场合成import pyttsx3 engine pyttsx3.init() engine.setProperty(rate, 180) # 语速默认200偏快 def play_voice(class_name): engine.say(f这是{class_name}请投入对应垃圾桶) engine.runAndWait()runAndWait是阻塞的一秒钟的语音会让程序等一秒。如果直接把它放在GUI主线程里画面会跟着语音一顿一顿听长句子时界面就像死机。合理做法是把语音播放丢给单独线程或者接受现场“先播报、画面暂时低于正常帧率”的体验。需要注意的是pyttsx3在Windows下通常可直接运行在Linux上需要提前安装espeak-ng否则初始化时会报错。5.3 防抖播报同一瓶水不应该被重复提醒三遍直接按每帧结果播报的界面会有一个很尴尬的现场效果人拿着矿泉水瓶在镜头前犹豫两秒语音把同一句话连续喊三遍。解决思路不是加延时那么简单而是做一个短时状态机last_class -1 same_count 0 PLAY_AFTER_FRAMES 10 # 连续10帧判定为同一类才播报 min_conf 0.35 def on_detect(class_id, conf): global last_class, same_count if conf min_conf: return if class_id last_class: same_count 1 else: last_class class_id same_count 1 if same_count PLAY_AFTER_FRAMES: play_voice(class_id)同一目标在画面里停留一段时间后连续10帧识别为同一类别才触发一次语音类别一旦切换计数器立刻重置。演示时如果想要“检测到就马上播报”把PLAY_AFTER_FRAMES调到2或3即可防抖依然有效。这个计数器占用的资源几乎可以忽略但交互体验提升非常明显也是毕设答辩时评审最容易感知到的细节。6. 现场提准的三个技巧阈值闸门、ROI限定和逐帧投票6.1 按类别单独调置信度阈值训练完的通用conf阈值无法适配所有类别。厨余垃圾和其他垃圾外观经常交叉误报率会明显偏高可回收物中的易拉罐和矿泉水瓶轮廓清晰置信度通常较高。实际做法是给每个类别单独设一个阈值在GUI或推理脚本里用一个字典管理thresholds { 0: 0.30, # 可回收物特征强阈值放宽 1: 0.45, # 厨余垃圾易与其他混淆阈值收紧 2: 0.30, # 有害垃圾特征明显 3: 0.45 # 其他垃圾模糊样本多 } if score thresholds.get(cls, 0.35): draw_and_play(cls, score)阈值怎么定不要拍脑袋。训练完先看runs/detect/trash_cls/confusion_matrix.png找出哪两个类别互相误判最多对被误判的类别提高阈值。对垃圾房这种场景宁可漏检两个瓶子也不要频繁播报错误类别因为错误引导比不引导更影响信任。6.2 用ROI把区域外目标过滤掉社区垃圾房的摄像头视野里除了投放口可能还有墙壁、绿植和路过的人。只靠类别过滤不彻底因为背景里的杂物一旦被认成垃圾就会触发错误播报。更可靠的做法是在画面里画一个投放区域ROI只保留目标中心点落在区域内的检测框import numpy as np import cv2 roi np.array([(120, 80), (520, 80), (520, 420), (120, 420)]) def inside_roi(cx, cy): return cv2.pointPolygonTest(roi, (int(cx), int(cy)), False) 0ROI坐标第一次运行时不准确很正常在可视化界面里加一个“框选投放区”的功能拖动鼠标画一遍保存到配置文件即可。这个过滤在检测前和检测后做都可以检测前截取ROI区域能省一点算力但会丢失边缘目标检测后过滤中心点更稳妥目标被部分遮挡时依然能触发识别。6.3 最近5帧投票替代单帧判定阈值和ROI解决的是误报问题但遇到暗光、摄像头轻微晃动时单帧结果会来回跳防抖播报也会被频繁重置。一种非常有效的做法是维护一个最近5帧的类别队列取众数作为最终判定from collections import deque vote_bucket deque(maxlen5) def update_vote(class_id): vote_bucket.append(class_id) if len(vote_bucket) 5: return None return max(set(vote_bucket), keyvote_bucket.count)队列满5帧后取出现次数最多的类别只有这个类别作为播报依据。投票机制会让响应延迟5帧左右但换来了明显的稳定性。实际现场测试时先用这个投票结果替代单帧类别大部分阈值调参动作都可以简化如果你发现模型延迟感太强把队列长度从5改成3仍然比单帧判定平滑。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价