ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

工业质检实战:基于YOLOv8的调料包朝向二分类检测全流程

工业质检实战:基于YOLOv8的调料包朝向二分类检测全流程

简介:本资源为基于海康设备实拍的方便面调料正反目标检测数据集,面向从事工业质检、食品包装检测及目标检测算法学习的研究者与开发者。数据集将正常放置的调料包标注为one,反方向异常放置标注为two,可用于训练模型识别产线上的放置方向异常,适用于YOLO系列等主流检测框架的实战训练与验证。压缩包共904个文件,包含441张jpg实拍图像、442个txt标注文件及21个xml标注文件,整体约758.95MB,图像与标注一一对应,便于直接投入训练。所有样本均经手动标注,使用YOLOv8训练检测map可达90%左右,验证了标注质量与数据可用性。目前已有110人学习下载,读者可借此快速搭建正反方向检测基线,理解工业场景下的数据组织与标注规范,并在此基础上进行模型调优或扩充增强数据集。

1. 从一包调料的朝向说起:这个数据集到底在解决什么问题

产线上最容易被低估的环节,往往不是机械臂抓取,而是「看一眼」——看一包方便面调料有没有放反。听起来简单,但真到了每分钟上百包的速度下,人眼盯久了必然疲劳,漏检率会悄悄爬上去。这个数据集就是冲着这个场景来的:它把「正常放置」标注为 one,「反方向放置」标注为 two,用两个类别把二分类的朝向判断问题框死。它适合谁?做工业质检落地的算法工程师、想拿真实产线数据练手目标检测的学生、以及需要快速验证一个轻量检测模型能不能上边缘设备的人。数据集本身不复杂,但它的价值在于「问题定义得足够干净」——没有几十个类别互相干扰,就是正反两个朝向,这让模型收敛快、部署门槛低,也方便你在它基础上做增广和迁移。下面我从数据组织、训练链路、改造空间三个层面把它拆开讲。

2. 数据集结构与标注逻辑:one/two 两类怎么组织才不踩坑

2.1 目录布局与文件命名约定

拿到一个目标检测数据集,第一件事不是急着写训练脚本,而是把目录结构摸清楚。这个数据集按常见的 YOLO 风格组织,图片和标签分离存放,标签用 txt 逐行描述框。典型结构如下:

dataset/ ├── images/ │ ├── train/ │ │ ├── 0001.jpg │ │ └── ... │ └── val/ │ ├── 0101.jpg │ └── ... ├── labels/ │ ├── train/ │ │ ├── 0001.txt │ │ └── ... │ └── val/ │ └── ... └── data.yaml

图片和标签必须同名,只换扩展名,这是 YOLO 系列读取时的硬约定。我见过太多人把0001.jpg配成0001_label.txt,训练时 loss 直接变 NaN,排查半天才发现是文件名对不上。标签文件里每一行是一个目标,格式为class_id x_center y_center width height,后四个都是归一化到 0~1 的相对值,不是像素坐标。这一点新手最容易翻车:直接拿标注工具导出的像素值塞进去,模型学出来的框会全部挤在左上角。

2.2 类别映射:one 和 two 的语义边界

data.yaml是这个数据集的「说明书」,它决定了类别索引和名称的对应关系:

# data.yaml path: ./dataset train: images/train val: images/val nc: 2 names: 0: one # 正常放置 1: two # 反方向放置

这里有个细节值得说:类别名用 one/two 而不是 normal/abnormal,是原数据集的定义方式,训练时模型只认索引 0 和 1,名字只是给人看的。但正因为名字抽象,你在做可视化验证时一定要把映射关系写死在代码注释里,否则过两周自己都忘了 0 到底代表正还是反。我一般会在data.yaml旁边再放一个README,把「one=正常、two=异常」这句话重复三遍,血泪经验。

2.3 标注质量自检:三个必查项

数据集再干净,也建议跑一遍自检脚本再开训。我通常查三件事:一是标签文件里有没有空文件(对应图片里其实没目标,但被误标了);二是坐标有没有越界(x_center 加减 width/2 超出 0~1);三是类别索引有没有出现大于 nc-1 的值。下面这段脚本就是干这个的:

import os import glob def check_labels(label_dir, nc=2): issues = [] for txt in glob.glob(os.path.join(label_dir, "*.txt")): if os.path.getsize(txt) == 0: issues.append(f"空标签: {txt}") continue with open(txt) as f: for i, line in enumerate(f): parts = line.strip().split() if len(parts) != 5: issues.append(f"字段数异常: {txt} 第{i}行") continue cls, x, y, w, h = map(float, parts) if cls >= nc: issues.append(f"类别越界: {txt} 第{i}行 cls={cls}") # 检查框是否超出图像边界 if not (0 <= x - w/2 and x + w/2 <= 1 and 0 <= y - h/2 and y + h/2 <= 1): issues.append(f"坐标越界: {txt} 第{i}行") return issues if __name__ == "__main__": for msg in check_labels("./dataset/labels/train"): print(msg)

这段逻辑很直白:逐行解析,先看字段数,再验类别索引,最后算框的四条边有没有跑出 0~1。参数nc默认给 2,和data.yaml保持一致。跑完如果输出为空,说明标注基本干净,可以进入训练环节;如果有输出,先修数据再谈模型,别指望网络能自己学会忽略脏样本。

3. 训练链路搭建:从零跑通一个二分类检测模型

3.1 环境准备与依赖版本

这个数据集规模不大,二分类任务也轻,所以没必要上重型框架。我一般用 Ultralytics 的 YOLO 系列,装起来快、文档全、导出部署也方便。环境用 conda 隔离,避免和系统里的其他包打架:

conda create -n noodle_det python=3.10 -y conda activate noodle_det pip install ultralytics opencv-python matplotlib

Python 选 3.10 是因为它在 torch 各版本间兼容性最稳,3.12 有些算子还没跟上。ultralytics会自动带上 torch 和 torchvision,不用单独装。装完跑一句yolo checks确认环境没问题,它会打印出 CUDA 是否可用、版本号等信息。如果显示 CPU only,训练会慢一个数量级,建议先解决显卡驱动再继续。

3.2 训练脚本与关键参数

训练入口可以命令行直接调,也可以写成脚本方便改参数。我倾向写脚本,因为参数一多,命令行容易敲错:

from ultralytics import YOLO # 加载预训练权重,二分类任务用 nano 版就够 model = YOLO("yolov8n.pt") results = model.train( data="./dataset/data.yaml", # 指向数据集配置 epochs=100, # 数据量小,100 轮足够收敛 imgsz=640, # 输入尺寸,和标注时分辨率匹配 batch=16, # 显存 8G 以内建议 16 lr0=0.01, # 初始学习率 patience=20, # 20 轮无提升就早停 project="runs/noodle", # 输出目录 name="exp1", augment=True, # 开启内置增广 device=0 # 用第 0 号 GPU )

逐项说下为什么这么设。epochs=100是因为二分类、目标单一,模型很快就能学到「朝向」这个特征,再多容易过拟合。imgsz=640要和你的原图分辨率对齐,如果原图是 1280 宽,缩到 640 会丢细节,调料包上的文字纹理可能就糊了,这时应该调大 imgsz 或先裁剪。batch=16是显存和稳定性的折中,太小梯度噪声大,太大显存爆。patience=20是后悔药,防止你盯着 loss 曲线干等。augment=True会做随机翻转、色彩抖动等,但注意——如果你的任务是判断朝向,水平翻转增广会把 one 变成 two 的镜像,反而制造矛盾样本,这种情况要关掉翻转,只保留色彩类增广。

3.3 训练过程监控与指标解读

训练启动后,runs/noodle/exp1/下会生成results.csv和一堆曲线图。重点看三个指标:metrics/mAP50、metrics/precision、metrics/recall。mAP50 是综合指标,二分类任务通常能跑到 0.95 以上;precision 高说明误报少,recall 高说明漏报少。产线场景里,漏检(把反的判成正的)比误检更致命,所以我会优先保 recall,必要时调低置信度阈值。

如果训练中途发现 loss 震荡不降,先别怀疑模型,回头查数据:是不是 one 和 two 的样本数量严重失衡?如果正常放置的图有 800 张、反方向只有 80 张,模型会倾向于全预测成 one,recall 直接崩。解决办法是过采样少数类,或者在 loss 里加类别权重。这个数据集如果本身均衡,那就不用折腾;如果不均衡,这一步不能省。

4. 推理与部署:把模型塞进产线相机的实时链路

4.1 单张推理与结果解析

训练完先别急着部署,拿几张验证集外的图跑推理,肉眼确认框和类别对不对:

from ultralytics import YOLO import cv2 model = YOLO("runs/noodle/exp1/weights/best.pt") results = model.predict( source="./test_images/", conf=0.5, # 置信度阈值 iou=0.45, # NMS 的 IoU 阈值 save=True, # 保存带框的结果图 device=0 ) # 解析单张结果 for r in results: for box in r.boxes: cls_id = int(box.cls) conf = float(box.conf) xyxy = box.xyxy.tolist()[0] label = "one" if cls_id == 0 else "two" print(f"{label} 置信度{conf:.2f} 坐标{xyxy}")

conf=0.5是起步值,实际部署要按产线误检容忍度调。iou=0.45控制重叠框合并,调料包一般不会密集堆叠,这个值够用。解析部分把类别索引翻译回 one/two,方便日志里直接看。注意box.xyxy返回的是像素坐标,和训练时的归一化坐标不同,别搞混。

4.2 视频流实时推理的改造点

产线是视频流,不是单张图。把source换成摄像头索引或 RTSP 地址即可,但有两个坑要提前处理。一是帧率,YOLOv8n 在 1080p 下用中端 GPU 大概能跑 60~80 FPS,如果相机是 120 帧,要么抽帧要么换更小的模型。二是结果平滑,单帧误判会导致产线误动作,常见做法是连续 N 帧里超过 M 帧判为 two 才触发报警:

from collections import deque history = deque(maxlen=5) # 保留最近5帧 def decide(frame_result): history.append(frame_result) # frame_result 为 0 或 1 if history.count(1) >= 3: # 5帧里3帧判为异常 return "two" return "one"

这个滑动窗口逻辑简单但有效,能过滤掉偶发的单帧抖动。窗口大小和触发阈值要根据产线速度调,速度越快,窗口越短,否则响应延迟会累积。

4.3 模型导出与边缘设备适配

如果产线用的是边缘盒子而不是工控机,需要把模型导出成 ONNX 或 TensorRT:

yolo export model=runs/noodle/exp1/weights/best.pt format=onnx opset=12 yolo export model=runs/noodle/exp1/weights/best.pt format=engine half=True

ONNX 通用性好,TensorRT 在 NVIDIA 设备上更快但绑定硬件。half=True是 FP16 量化,速度能提一截,精度掉得很少,二分类任务基本无感。导出后记得用同一批测试图对比一下导出前后的输出,确认没有精度损失再上线。

5. 避坑与常见问题:那些让训练白跑的细节

5.1 类别名混淆导致标签反了

现象:模型训练 loss 正常下降,但验证时 one 和 two 的预测完全颠倒。原因:标注时把 one 和 two 的索引写反了,或者data.yaml里 names 顺序和标注文件里的 class_id 不一致。解决:拿一张确定的正常放置图,看它的标签文件里 class_id 是 0 还是 1,再对照data.yaml确认。我习惯在标注完成后随机抽 20 张做可视化,把框和类别名画在图上,肉眼过一遍。

5.2 图片和标签路径不匹配

现象:训练启动即报错No labels found,或者 loss 一直是 0。原因:data.yaml里的train路径写的是相对路径,但实际运行时工作目录变了;或者 images 和 labels 的目录层级不对应。解决:把path写成绝对路径最稳,或者用os.path.abspath在脚本里动态拼。另外确认 images/train 和 labels/train 是平级关系,不是嵌套关系。

5.3 增广策略与任务语义冲突

现象:训练集准确率很高,但验证集一塌糊涂,尤其反方向样本识别率低。原因:开了水平翻转增广,把正常放置的图翻成了反方向的外观,但标签还是 one,模型学懵了。解决:朝向判断类任务禁用几何翻转增广,只保留亮度、对比度、噪声这类色彩增广。如果一定要用几何增广,旋转角度也要限制在小范围内。

5.4 置信度阈值设太高导致漏检

现象:验证集 mAP 很高,但实际跑视频时反方向的包经常不报警。原因:conf默认 0.25,但产线光照变化大,模型对某些样本的置信度只有 0.3~0.4,被阈值滤掉了。解决:把conf降到 0.15~0.2,同时用第 4.2 节的滑动窗口逻辑压误报。宁可多报几次让人复核,也别漏掉一个反方向的包。

5.5 显存不足导致 batch 被迫调小

现象:训练报CUDA out of memory,只能把 batch 降到 4 甚至 2,训练变得很慢且不稳定。原因:imgsz设太大,或者模型选了 s/m 版本而不是 n 版本。解决:二分类任务优先用 nano 版,imgsz从 640 起步,显存还不够就降到 416。另外可以开梯度累积,用batch=4累积 4 次等效于batch=16,代价是训练时间略长。

6. 二次开发与改造:让这个数据集适配你自己的产线

拿到这个数据集,最直接的改造方向是迁移到你自己产品的朝向检测上。做法不复杂:保留 one/two 的二分类框架,把图片换成你产线的样本,重新标注。但有几个技巧能让迁移更顺。第一,先用这个数据集训练一个基础模型,再用你的少量样本做微调,比从 COCO 预训练直接上收敛快得多,因为模型已经学到了「朝向」这类低级几何特征。第二,如果你的产品正反差异比调料包更细微(比如标签上的一个角标),建议把imgsz提到 960 甚至 1280,否则细节在缩放中丢失,模型再强也学不到。

另一个改造点是类别扩展。如果你除了正反,还要判断「有没有放调料包」,那就从二分类变成三分类:one、two、missing。这时data.yaml的nc改成 3,names 加一项,标注时把空位也框出来。注意 missing 类的样本要足够多,否则模型会把「没检测到」和「missing」混为一谈。我一般会保证每个类别至少 200 个实例,低于这个数就做过采样。

验证改造效果时,别只看 mAP。产线关心的是「单位时间内的漏检次数」和「误报次数」。我会写一个离线评估脚本,拿一段录制的产线视频跑推理,统计两个数:反方向包的总数、模型报出的反方向次数,算出漏检率和误报率。这两个数比 mAP 更能说服产线负责人。脚本核心就是逐帧推理加计数,配合第 4.2 节的滑动窗口,跑完输出一份报表。

最后说个我自己的习惯:每次改完数据集或训练参数,我都会固定用同一段 5 分钟的视频做回归测试,记录漏检和误报的变化。有一次我为了提速度把imgsz从 640 降到 416,mAP 只掉了 0.5 个点,但漏检率翻了一倍——因为小尺寸下反方向包上的一个关键纹理糊掉了。从那以后我每次调imgsz都强制走一遍这段视频,不再只看验证集指标。希望这个思路帮到你,少走点我踩过的弯路。

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

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进