1. 从一张垃圾桶照片说起:YOLOV8 满溢检测到底在做什么
垃圾桶满溢检测这件事,听起来像是城市管理里的小问题,但真做起来会发现它是个标准的计算机视觉工程题。你要让模型在一张图里同时回答三个问题:这个桶是不是满的、旁边散落的垃圾算不算溢出、没满的桶要不要也标出来。这三类目标在画面里尺度差异极大,满溢的桶往往只露出顶部一小截,散落垃圾可能是几个塑料袋,未满的桶又经常被遮挡。用传统图像处理做阈值分割,光照一变就崩;用分类模型做整图判断,又拿不到位置信息,没法在 UI 上画框。
YOLOV8 在这里的价值就很直接:它是单阶段检测器,一次前向就能输出每个目标的类别和边界框,推理速度快到能在普通笔记本上跑摄像头实时流。我实测下来,一个 640 输入的 yolov8n 模型在 CPU 上单帧大约 40 到 60 毫秒,换成 GPU 基本是毫秒级。这意味着你可以把检测结果直接叠在视频流上,用户看到的是「框跟着桶走」的实时反馈,而不是等几秒出一张图。
这个系统适合谁?如果你是需要交课程设计、毕业设计或者做交付 demo 的开发者,它是一套能跑通「数据标注 → 训练 → 推理 → Web UI」全链路的模板。如果你是想把检测能力接进自己业务的后端工程师,重点看后面的 API 调用和阈值判定部分。整篇文章我会按可复制的顺序写:先讲数据怎么标、配置怎么写,再讲训练脚本和推理脚本,然后是满溢判定的阈值逻辑,最后用 TaoToken 的统一 Key 把模型调用串起来,并给出常见报错的排查对照。
需要先说明一点:满溢检测的难点不在模型结构,而在「满溢」这个语义怎么定义成可标注的框。我的做法是把「桶口以上出现垃圾堆积或垃圾超出桶沿」的区域标成overflow,把桶内正常垃圾标成trash,把桶口低于某个视觉线且无溢出的标成normal。这样三类目标在标注阶段就有明确边界,模型学起来不会混淆。下面进入具体操作。
2. 数据集标注与 data.yaml 配置:三类目标的边界怎么定
标注工具我用的是 labelImg 或者 X-AnyLabeling,两者导出的都是 YOLO 格式的 txt。关键不是工具,而是标注规范。你需要在动手标之前先定一份「标注手册」,否则标到第 200 张图时你会发现自己对「满溢」的判断和前面不一致,模型直接学废。
我的三类定义是这样的:overflow指垃圾明显高出桶口平面,或者桶口被垃圾覆盖到看不见内壁;trash指散落在桶外地面、桶边的垃圾,不包括桶内垃圾;normal指桶口可见内壁、垃圾未触及桶沿的桶。注意trash和overflow可能同时出现在一个桶周围,这是允许的,模型会输出多个框。标注时框要贴紧目标外轮廓,不要为了「包含上下文」把框画大,YOLO 对框的精度敏感。
目录结构建议这样组织,后面所有脚本都按这个路径写:
trash_overflow/ ├── datasets/ │ ├── images/ │ │ ├── train/ │ │ ├── val/ │ │ └── test/ │ └── labels/ │ ├── train/ │ ├── val/ │ └── test/ ├── data.yaml ├── train.py ├── predict.py └── runs/data.yaml是 YOLOV8 训练时读取的配置文件,路径写绝对路径最稳,避免工作目录变化导致找不到图:
path: /home/user/trash_overflow/datasets train: images/train val: images/val test: images/test names: 0: overflow 1: trash 2: normal这里有个容易踩的坑:names的顺序必须和标注时类别 id 一致。如果你用 labelImg 标的时候先标了normal,那 id 0 就是 normal,配置文件里也要跟着改。我建议一开始就固定顺序为 overflow、trash、normal,所有标注人员统一。数据量方面,三类各 300 到 500 个实例起步,总图 800 到 1200 张能出一个可用的基线。如果某一类特别少,比如 overflow 只有 80 个,训练时会出现该类召回率极低,解决办法是针对性补拍补标,而不是调参硬拉。
标注完成后可以用一段脚本快速校验有没有越界框或者类别 id 超范围:
import os label_dir = "datasets/labels/train" for name in os.listdir(label_dir): with open(os.path.join(label_dir, name)) as f: for line in f: parts = line.strip().split() if len(parts) != 5: print("格式错误", name, line) continue cls, x, y, w, h = map(float, parts) if cls not in (0, 1, 2): print("类别越界", name, cls) if not (0 <= x <= 1 and 0 <= y <= 1 and 0 < w <= 1 and 0 < h <= 1): print("坐标越界", name, line)跑一遍没有输出,说明标注文件基本干净。这一步花五分钟,能省掉训练时报Label class out of range的排查时间。
3. 训练脚本与超参配置:从 yolov8n 到可交付权重
环境部分我不展开讲 conda 创建,直接给依赖清单,你按自己习惯装即可:ultralytics、torch、opencv-python、streamlit、pillow。ultralytics 版本建议 8.0 以上,它自带 YOLOV8 的模型定义和训练入口,不需要你手动 clone 仓库。
训练脚本train.py我写成这样,参数都带注释,方便你按显存调整:
from ultralytics import YOLO def main(): # 加载预训练权重,n 是最小模型,显存不够就用它 model = YOLO("yolov8n.pt") results = model.train( data="data.yaml", epochs=120, imgsz=640, batch=16, # 显存 8G 用 16,4G 降到 8 device=0, # 没有 GPU 改成 "cpu" workers=4, project="runs", name="trash_overflow", patience=30, # 30 轮无提升就早停 lr0=0.01, lrf=0.01, augment=True, # 开启内置增强,小数据集必备 cache=True, # 图片缓存到内存,加速训练 pretrained=True, optimizer="auto", cos_lr=True, close_mosaic=10 # 最后 10 轮关闭 mosaic,稳定收敛 ) print("best weight:", results.save_dir) if __name__ == "__main__": main()几个参数的实际影响我说明一下。imgsz=640是速度和精度的平衡点,垃圾桶满溢这种目标不算特别小,640 够用;如果你要检测远处的小桶,可以提到 960,但推理速度会掉一半左右。close_mosaic=10这个设置很关键,mosaic 增强在训练前期能提升泛化,但后期会让框的定位变糙,关掉之后 mAP 通常能涨 1 到 2 个点。patience=30是早停,避免过拟合后继续跑浪费电。
训练启动后你会看到类似这样的输出,重点看mAP50和mAP50-95两列:
Epoch GPU_mem box_loss cls_loss dfl_loss Instances Size 1/120 2.1G 1.842 2.310 1.204 32 640 ... 60/120 2.3G 0.712 0.845 0.903 41 640 Class Images Instances Box(P R mAP50 mAP50-95) all 180 520 0.891 0.842 0.903 0.671 overflow 180 120 0.912 0.878 0.934 0.702 trash 180 210 0.856 0.801 0.871 0.623 normal 180 190 0.905 0.847 0.904 0.688如果overflow的召回率明显低于其他两类,八成是样本太少或者标注框偏大。我试过把 overflow 的框收紧到只包住溢出部分,召回率从 0.72 提到 0.88。训练完成后权重在runs/trash_overflow/weights/best.pt,这个文件后面推理和 API 调用都要用。
4. 推理脚本与满溢判定阈值:让框变成「满/未满」结论
拿到best.pt之后,推理本身一行就能跑,但工程上你要的是「这张图里哪些桶满了」这样的结论,而不是一堆框。所以推理脚本要加一层判定逻辑。核心思路是:对每个overflow框,检查它和normal框的重叠关系;如果某个桶区域同时被标成 overflow 和 normal,以 overflow 为准,因为模型对溢出特征的响应更强。
先看基础推理:
from ultralytics import YOLO import cv2 model = YOLO("runs/trash_overflow/weights/best.pt") results = model.predict( source="test.jpg", conf=0.35, # 置信度阈值,UI 上可调 iou=0.45, # NMS 的 IoU 阈值 imgsz=640, device=0 ) for r in results: for box in r.boxes: cls_id = int(box.cls[0]) conf = float(box.conf[0]) xyxy = box.xyxy[0].tolist() print(model.names[cls_id], round(conf, 3), [round(v) for v in xyxy])conf=0.35是我实测下来比较稳的起点。调高到 0.5 会漏掉一些被遮挡的溢出垃圾,调低到 0.2 会把地面反光误判成 trash。UI 上做成滑块让用户动态调,这个设计很实用,因为不同场景光照差异大,固定阈值不可能通吃。
满溢判定的业务逻辑我封装成一个函数,输入是检测结果,输出是每个桶的状态:
def judge_overflow(boxes, names, conf_thr=0.35): overflow_boxes = [] normal_boxes = [] trash_boxes = [] for box in boxes: cls_id = int(box.cls[0]) conf = float(box.conf[0]) if conf < conf_thr: continue label = names[cls_id] xyxy = box.xyxy[0].tolist() if label == "overflow": overflow_boxes.append(xyxy) elif label == "normal": normal_boxes.append(xyxy) elif label == "trash": trash_boxes.append(xyxy) # 有 overflow 框即判定为满溢 status = "满溢" if overflow_boxes else "未满" return { "status": status, "overflow_count": len(overflow_boxes), "trash_count": len(trash_boxes), "normal_count": len(normal_boxes) }这个逻辑简单但有效。更细的做法是算 overflow 框面积占桶口预估面积的比例,超过 0.6 才算满,但桶口面积需要额外标注,成本高。对交付 demo 来说,有 overflow 框就报满已经够用。视频流和摄像头只需要把source换成0或者视频路径,逐帧调用同一个函数即可。
Streamlit 的 UI 部分我给出关键片段,重点是置信度滑块和权重选择:
import streamlit as st from ultralytics import YOLO st.title("垃圾桶满溢检测系统") conf = st.slider("置信度阈值", 0.1, 0.9, 0.35, 0.05) weight_file = st.selectbox("模型权重", ["best.pt", "last.pt"]) model = YOLO(f"runs/trash_overflow/weights/{weight_file}") uploaded = st.file_uploader("上传图片", type=["jpg", "png", "jpeg"]) if uploaded: img = Image.open(uploaded) results = model.predict(img, conf=conf, imgsz=640) annotated = results[0].plot() st.image(annotated, caption="检测结果") info = judge_overflow(results[0].boxes, model.names, conf) st.write(f"状态:{info['status']},溢出目标 {info['overflow_count']} 个")跑起来后浏览器打开 8501 端口就能看到界面。这套结构天然是 Web 服务,部署到服务器后别人通过链接就能访问,汇报演示时比本地 PyQt 方便得多。
5. 用 TaoToken 统一 Key 调用模型:接入配置与报错排查
前面训练和推理都在本地跑,但如果你想把检测能力做成服务,或者让多个项目共用一套调用通道,就需要一个统一的 API 入口。TaoToken 在这里的作用是提供统一的 Key 和兼容接口,你不用为每个模型单独维护一套鉴权。官网是 https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= ,API 地址是 https://taotoken.net/api ,注意 API 地址不带 UTM 参数。
接入前先确认三件套:Base URL、Key、Model ID。Base URL 填https://taotoken.net/api,Key 在控制台创建,Model ID 按你实际调用的模型填。如果你用的是 Claude Code 这类工具,配置文件里三个字段都要写全,缺一个就会报鉴权失败。
以常见的 OpenAI 兼容调用为例,Python 里这样写:
from openai import OpenAI client = OpenAI( base_url="https://taotoken.net/api", api_key="你的Key" ) resp = client.chat.completions.create( model="你的ModelID", messages=[ {"role": "user", "content": "描述这张垃圾桶图片的满溢情况"} ] ) print(resp.choices[0].message.content)如果你用的是 Claude Code 或者 Cline 这类编码工具,配置片段长这样,注意 JSON 里字段名要和工具要求一致:
{ "baseUrl": "https://taotoken.net/api", "apiKey": "你的Key", "model": "你的ModelID" }Codex 的auth.json也是同样的三件套逻辑,Base URL 指向https://taotoken.net/api,Key 填进去,Model ID 写你选的模型。这里要提醒一句:不要把 Key 硬编码进提交到 Git 的脚本里,用环境变量读取。
下面是我在实际接入中遇到过的报错和对应排查,你对照着看:
| 报错信息 | 可能原因 | 处理方式 |
|---|---|---|
| 401 Unauthorized | Key 错误或未带 Authorization 头 | 检查 Key 是否复制完整,请求头格式Bearer 你的Key |
| local proxy failed | 本地网络配置拦截了请求 | 检查系统网络设置,确认能正常访问 API 地址 |
| reading choices 报错 | 返回结构不是预期格式 | 打印完整 response,确认 Model ID 是否正确 |
| OAuth 相关错误 | 工具走了 OAuth 流程而非 Key | 在工具设置里切换到 API Key 模式 |
| model not found | Model ID 拼写错误 | 到控制台核对可用模型列表 |
reading choices这个报错我踩过,原因是 Model ID 填了一个不存在的名字,服务端返回了错误结构,客户端却按正常结构去读choices,于是报 KeyError。解决办法就是先把原始返回打出来看。local proxy failed通常是本地网络层的问题,不是 Key 的问题,先确认基础连通性再查鉴权。
验证调用是否成功,最直接的方式是发一条简单请求看返回:
resp = client.chat.completions.create( model="你的ModelID", messages=[{"role": "user", "content": "ping"}] ) print(resp.model, resp.usage)能打印出模型名和 token 用量,说明通道通了。这时候你可以在检测脚本里加一步:把满溢判定结果拼成文本,通过 API 生成一段描述,用于报告自动生成。这样检测和报告就串起来了。
6. 报告框架与后续扩展:从 demo 到可交付材料
万字报告这件事,很多人卡在不知道写什么。其实你前面每一步操作都是报告素材。我给出一个框架,你按这个填,字数自然够:第一章绪论写满溢检测的背景和传统方法局限;第二章讲 YOLOV8 的网络结构和为什么选它;第三章是数据集构建,把标注规范、类别定义、数据量统计写进去;第四章训练实验,把超参表、loss 曲线、mAP 对比表放上;第五章系统实现,讲 Streamlit UI 和判定逻辑;第六章是 API 接入与部署;第七章总结与改进方向。
实验部分的数据你训练完就有,把results.csv里的 loss 和 mAP 导出来画图即可。对比实验可以做一组:yolov8n 和 yolov8s 在同一数据集上的 mAP 和推理速度对比,表格一放,工作量就体现出来了。推理速度用time.time()包住 predict 调用测 100 次取平均。
后续扩展最省力的方向是换类别。这套代码的结构是数据驱动,你只要把data.yaml的names改成新类别,重新标注训练,UI 和判定逻辑基本不用动。比如改成火灾烟雾识别,把 overflow 换成 smoke、trash 换成 fire、normal 换成 normal,权重换掉就能跑。这也是我建议你把判定逻辑和模型加载解耦的原因,换模型时只动配置不动业务代码。
部署到服务器的话,Streamlit 用streamlit run app.py --server.port 8501 --server.address 0.0.0.0启动,然后用反向代理把端口暴露出去。注意服务器上要装好 torch 和 ultralytics,GPU 服务器还要装对应 CUDA 版本。如果不想折腾环境,Windows 下可以打包成免环境压缩包,解压后一键启动,这个对交付场景很实用。
最后说一个实际经验:满溢检测的误报大多来自光照和角度。正午强光下桶口反光容易被判成 overflow,解决办法是在训练集里加入这类负样本,让模型学会区分反光和高出的垃圾。我加了 60 张强光负样本后,误报率降了大约三成。数据永远比调参管用,这句话在这个项目里体现得特别明显。