
简介面向工业视觉、包装质检与高校课程实践场景的方便面调料正反目标检测数据集标注正常放置为one、反方向异常放置为two适合YOLO系列模型训练、验证与方向异常识别实验。压缩包共904个文件含441张海康拍摄的JPG图像、442个TXT标注文件及21个XML文件整体约758.95MB图像与标注一一对应便于直接划分训练集、验证集并复核标注质量。已有110人学习下载说明其在调料包方向判别任务中具有一定参考价值。手动标注数据可直接用于YOLOv8检测实验作者反馈mAP约90%读者可据此复现正反分类检测流程、分析误检与漏检样本并在此基础上做数据增强或扩充自有产线数据快速搭建包装方向异常检测基线也可用于课堂演示、算法对比和产线质检原型验证。1. 调料包正反检测为什么值得单独做一个数据集方便面调料包在包装线上是双面投放的正面朝上算正常反面朝上就是异常。这一步过去靠人眼在高速线上盯盯久了眼睛花、漏检率就上来了。这套方便面调料包正反目标检测数据集把“正常放置”标成 one、“反向放置”标成 two直接给你一个二分类检测的起点。它适合三类人做食品包装视觉检测的集成方、想快速验证小目标二分类方案可行性的算法同学、手上有工业相机却缺标注素材的产线团队。真正的难点不在模型结构而在正反差异小、袋面反光强、异常样本天然偏少这三件事上。这三件事决定了你不能拿一个通用检测模型直接套上去就完事得先看清数据是怎么组织的、标签是怎么定的再谈训练。2. 先看清数据one/two 两类怎么定、怎么落盘拿到一套别人采的数据集最怕的是闷头就训。类别名对不上、标签坐标系搞错、图片和标签文件名不匹配这些问题在训练开始前不暴露训到一半 loss 不降才回头查浪费的是整晚的机器时间。这一章先把数据的骨架理清楚包括目录结构、类别映射、标注框的边界约定以及一套可以在训练前跑一遍的一致性校验脚本。这套数据的采集方式是工业面阵相机俯拍传送带上的调料包单包为主画面里偶尔有相邻包的边缘入镜。类别只有两个one 表示正面朝上two 表示反面朝上。注意这里是“检测”任务而不是“分类”任务也就是说标签是外接框加类别不是整图一个标签。为什么用检测而不是分类理由很实际产线上调料包的位置不固定分类模型需要先切出 ROI而 ROI 切不准的时候分类器拿到的就是背景检测模型则把定位和分类一起做了后处理还能拿到框的坐标去做剔除动作。2.1 目录结构与类别映射YOLO 系列的数据组织方式是当前最省事的图片和标签分目录存放标签文件名与图片同名、扩展名换成 txt。下面这个结构是我拿到任何一套检测数据后第一件事要核对的东西目录名可以不一样但映射关系必须一一对上。dataset/ ├── images/ │ ├── train/ # 训练图jpg/png │ └── val/ # 验证图与 train 不同批次 ├── labels/ │ ├── train/ # 与 images/train 同名的 .txt │ └── val/ └── data.yaml # 类别名与路径配置配套的data.yaml长这样names的顺序就是标签文件里第一个字段的整数编号写反了模型会把 one 学成 two而且 loss 照样降只是混淆矩阵会告诉你两类在互相当成对方。path: ./dataset train: images/train val: images/val nc: 2 names: 0: one # 正常放置正面朝上 1: two # 异常放置反面朝上这里有个容易被忽略的点names用字典还是列表取决于你用的框架版本字典写法兼容性更好列表写法在部分旧脚本里会报索引错误。我一般统一用字典省得换环境时再改。另外path建议写相对路径数据集整体拷贝到别的机器上时不用改配置。标签文件里每一行是五个数class_id cx cy w h后四个都是归一化到 0 到 1 的相对值cx cy是框中心点w h是框宽高。这个坐标系和你用 OpenCV 画图时看到的像素坐标不是一回事直接拿像素值写进去训练时框会全跑到左上角表现为模型什么都学不到。2.2 标注框的边界约定正反检测这类任务标注框画大一点画小一点单看一张图问题不大但一批数据里标准不一致模型学到的就是模糊的边界。下面这张表是我在标注规范和验收时实际用的约定可以直接抄。场景处理方式理由整包完整可见画整包外轮廓含封边锯齿封边是判断正反的辅助特征包体出画可见面积 ≥ 70%画可见部分的外接框保留样本量框不虚构包体出画可见面积 70%整图剔除不标边界信息不足容易教坏模型两包轻微重叠各画各的框允许框重叠检测任务允许多实例反光导致局部过曝仍按整包画框不因过曝缩小过曝是成像问题不是标注问题包体严重褶皱变形单独放一个 hard 子目录训练时按需加入避免污染主训练集分布表格里最后一条是血泪经验。早期我把所有褶皱样本一股脑混进训练集结果模型在正常样本上的框变得特别松因为褶皱样本的轮廓本身就散。后来改成单独存放训练时按比例采样验证集只放正常形态指标才稳定下来。还有一个细节one 和 two 的判定依据是印刷面朝向不是袋子在画面里的旋转角度。同一个正面朝上的包在传送带上转 90 度、180 度都是 one。这一点如果在标注时混淆了把“图案倒过来”误判成 two整套标签就废了。标注前一定要让所有标注员看同一批样图统一口径。2.3 一致性校验与格式转换标注完成不等于数据可用。下面这段脚本在训练前跑一遍能挡掉八成低级错误包括缺标签、空标签、类别编号越界、坐标越界、宽高非正、框超出图像边界。from pathlib import Path from PIL import Image ROOT Path(./dataset) IMG_EXTS {.jpg, .jpeg, .png, .bmp} problems [] for split in (train, val): img_dir ROOT / images / split lbl_dir ROOT / labels / split for img_path in sorted(img_dir.iterdir()): if img_path.suffix.lower() not in IMG_EXTS: continue lbl_path lbl_dir / (img_path.stem .txt) if not lbl_path.exists(): problems.append(f缺标签: {img_path.name}) continue w, h Image.open(img_path).size lines [l.strip() for l in lbl_path.read_text().splitlines() if l.strip()] if not lines: problems.append(f空标签: {lbl_path.name}) for ln, line in enumerate(lines, 1): parts line.split() if len(parts) ! 5: problems.append(f{lbl_path.name}:{ln} 字段数{len(parts)}) continue cid int(parts[0]) cx, cy, bw, bh map(float, parts[1:]) if cid not in (0, 1): problems.append(f{lbl_path.name}:{ln} 类别越界 {cid}) if not all(0.0 v 1.0 for v in (cx, cy, bw, bh)): problems.append(f{lbl_path.name}:{ln} 坐标越界) if bw 0 or bh 0: problems.append(f{lbl_path.name}:{ln} 宽高非正) # 归一化框允许贴边但不允许明显超出 if cx - bw / 2 -1e-6 or cx bw / 2 1 1e-6: problems.append(f{lbl_path.name}:{ln} 横向超出图像) if cy - bh / 2 -1e-6 or cy bh / 2 1 1e-6: problems.append(f{lbl_path.name}:{ln} 纵向超出图像) print(f共发现 {len(problems)} 处问题) for p in problems[:50]: print(p)脚本逻辑很直白遍历每个 split 下的图片找同名标签逐行解析五元组。类别的合法集合硬编码成(0, 1)换成别的数据集时记得同步改。坐标越界判断留了1e-6的容差因为标注工具导出时可能在边界值上带浮点误差卡太死会误报。如果手上的标签是 Pascal VOC 的 xml 格式转换时注意 VOC 的xmin ymin xmax ymax是像素值、原点在左上角而 YOLO 要的是归一化中心点加宽高转换公式是cx(xminxmax)/2/W、bw(xmax-xmin)/W别把宽高算成半宽半高。转换完再跑一遍上面的校验两遍都过再进训练。3. 从原图到可训练集切分、增强与不平衡处理数据校验通过只是“能用”离“训得准”还差三步切分方式、增强策略、类别平衡。这三步里最容易翻车的是切分和增强因为它们不报错只是让验证指标好看等上了产线才发现模型根本不认现场的光照和角度。3.1 按拍摄批次切分而不是随机切分连续拍摄的数据集相邻帧之间差异极小——同一个包在传送带上移动几厘米画面几乎一样。如果按随机种子切分同一批次的近邻帧会同时进入训练集和验证集验证集就变成了“训练集的复印件”mAP 能刷到 0.95 以上换个批次立刻掉到 0.6。这个坑我在不同项目里见过不止一次属于典型的分布泄漏。正确的切法是按采集批次或时间段分组整组进训练或整组进验证。文件名里通常带批次前缀比如batch03_20240612_000123.jpg按前缀分组即可。import random from pathlib import Path raw sorted(Path(./raw/images).glob(*.jpg)) # 按文件名前缀聚成批次前缀相同视为同一批采集 batches {} for p in raw: batch_id p.stem.split(_)[0] batches.setdefault(batch_id, []).append(p) batch_ids sorted(batches) random.seed(42) # 固定种子保证每次切分结果一致 random.shuffle(batch_ids) n_val max(1, int(len(batch_ids) * 0.2)) # 约 20% 的批次做验证 val_batches set(batch_ids[:n_val]) for bid in batch_ids: split val if bid in val_batches else train for p in batches[bid]: # 这里换成实际的拷贝逻辑保持 images/labels 同步 print(f{split}\t{p.name})参数上验证集比例固定 20% 是因为批次数量本身不多切太少验证指标波动大切太多训练样本不够。种子固定成 42 只是习惯换成任何常数都行关键是要固定否则下次重跑切分结果变了指标就没法对比。还有一个变体做法如果批次信息拿不到就用时间戳做窗口切分比如每 50 帧为一个窗口窗口之间不允许交叉。效果比随机切分好但不如真实批次干净。3.2 增强白名单与黑名单增强这块最反直觉的一条镜像翻转对这套数据是有害的。调料包的正反面印刷图案是固定的水平或垂直镜像之后图案在现实中根本不存在。模型在镜像样本上学到的纹理特征到了现场就是干扰项。更隐蔽的是镜像不会让 one 变成 two两类标签都没错所以离线指标不会报警只是模型变得对方向敏感、泛化变差。允许的增强是绕光轴的旋转。包体在传送带上本来就可能任意角度出现转 90 度、180 度都是真实分布内的变化。# 增强配置片段白名单为主 hsv_h: 0.015 # 色相微调应付不同批次的印刷色差 hsv_s: 0.5 # 饱和度模拟光源色温波动 hsv_v: 0.4 # 亮度模拟曝光差异 degrees: 180.0 # 绕光轴旋转合法增强 translate: 0.1 # 平移模拟包体位置抖动 scale: 0.4 # 缩放模拟传送带高度误差 shear: 0.0 # 剪切关闭避免框变形失真 perspective: 0.0 # 透视关闭相机是垂直俯拍 flipud: 0.0 # 禁止垂直镜像 fliplr: 0.0 # 禁止水平镜像 mosaic: 1.0 # 四图拼接提升小目标上下文 mixup: 0.0 # 关闭两类混叠会制造不存在的样本degrees: 180.0这个值取决于你的工艺。如果现场包体总是同向进入视野、只有小幅偏转那 180 度旋转就是分布外样本会拖慢收敛这时候改成 15 到 30 度更合适。判断依据不是“能不能转”而是“现场会不会这么转”拿一段现场录像逐帧看包体角度分布比拍脑袋定参数靠谱。mosaic打开会让单张图里出现四个缩放后的小包对小目标检测有帮助但会把框的绝对尺寸变小如果调料包本身在画面里只占几十个像素mosaic 加得太狠会让模型彻底学不动。我的习惯是训练最后 15 到 20 个 epoch 关掉 mosaic让模型在真实尺度上收敛这一步对最终指标的影响比学习率调参还明显。3.3 类别不平衡重采样与损失权重正常放置的样本天然远多于反向放置比例常见在 3:1 到 8:1 之间。不平衡的直接后果是 two 类的召回上不去模型倾向于把所有样本判成 one因为这样整体准确率最高。解决思路两条采样层面让 two 多出现损失层面让 two 的错分代价更大。采样层面的做法是给每张图算一个采样权重图里包含 two 的权重大然后用加权采样器构造 batch。import numpy as np from torch.utils.data import WeightedRandomSampler # 统计所有训练图的类别分布 per_image_classes [] # 每张图包含的类别 id 列表由标签解析得到 counts np.zeros(2, dtypenp.float64) for cls_list in per_image_classes: for c in set(cls_list): counts[c] 1 # 类别权重取倒数样本越少的类权重越大 class_w 1.0 / np.maximum(counts, 1.0) class_w class_w / class_w.sum() # 单图权重取其包含类别的最大权重保证含 two 的图被优先采到 img_w [] for cls_list in per_image_classes: img_w.append(max((class_w[c] for c in set(cls_list)), default0.0)) sampler WeightedRandomSampler( weightsimg_w, num_sampleslen(img_w), # 每个 epoch 采样数与数据集等长 replacementTrue # 有放回少数类会被重复采 )replacementTrue是必须的否则加权采样退化成一次遍历。num_samples设成数据集大小是为了让 epoch 长度稳定方便和学习率调度对齐。这套采样器接进 DataLoader 时注意shuffle要关掉两者同时开会冲突。损失层面的做法是给 two 类更大的分类损失权重。多数检测框架的分类损失是交叉熵或变体可以在构建损失时传入每类权重也可以在配置文件里指定。两条路选一条就行同时上容易过犹不及把 two 的误报率推高。我一般先试采样采样后混淆矩阵里 two 的召回还是明显低于 one再补损失权重。4. 跑通基线训练配置、混淆矩阵与阈值取舍前面三章把数据理干净了这一章进入训练。基线模型不追求 SOTA追求的是“能在半天内跑完、指标可解释、每个参数都能说清为什么这么设”。有了基线后面所有的改进才有对比参照。4.1 基线配置与显存换算输入分辨率是这套数据最关键的参数。调料包在画面里的短边可能只有一两百像素正反面的印刷标识更小分辨率给低了标识直接糊成一个色块。我的做法是先量一下标注框的像素尺寸分布取短边的中位数再乘 3 到 4 倍作为输入尺寸通常落在 960 到 1280 之间。yolo detect train \ modelyolov8s.pt \ datadataset/data.yaml \ imgsz960 \ epochs150 \ batch16 \ lr00.01 \ lrf0.01 \ warmup_epochs3 \ cos_lrTrue \ close_mosaic20 \ patience40 \ cacheram \ workers8 \ projectruns/det \ nameone_two_baseline逐个参数说。imgsz960是按上面那条规则算出来的显存不够就降到 800 或 640但别低于标注框短边的 2 倍。batch16在 24G 显存的卡上是安全值12G 的卡降到 8 并同步把lr0降到 0.005 左右学习率和 batch 大致成线性关系。epochs150配合patience40意思是连续 40 轮验证指标不涨就早停实际跑下来通常 90 到 110 轮就停了。close_mosaic20在最后 20 轮关掉拼接增强让模型在真实尺度上收尾。cacheram把图片缓存到内存数据集在几千张量级时能省掉大量磁盘 IO内存不够就改成cachedisk。训练启动后前 5 轮看 loss 曲线分类 loss 和框回归 loss 同时下降是正常如果框回归 loss 一直平着不动八成是标签坐标有问题回头跑第 2 章那个校验脚本。10 轮之后看验证集 mAP正常应该在 0.5 以上低于这个值要么数据有问题要么学习率设大了。4.2 混淆矩阵把两类错误的代价摆到桌面上训练完拿到best.pt第一件事不是看 mAP是看混淆矩阵。mAP 是一个被平均过的数它不告诉你错误发生在哪个方向上。对正反检测来说把 two 判成 one漏检异常和把 one 判成 two误报正常代价完全不同前者是坏包流出后者是误剔除产线停一下就能纠正。from ultralytics import YOLO model YOLO(runs/det/one_two_baseline/weights/best.pt) metrics model.val( datadataset/data.yaml, imgsz960, conf0.001, # 低阈值导出全部预测便于分析 iou0.6, plotsTrue, # 生成混淆矩阵和 PR 曲线 ) cm metrics.confusion_matrix.matrix print(行真实, 列预测, 最后一行/列背景) print(cm)conf0.001是刻意设低的只有把低置信度的预测也导出来才能看清模型在边界上的犹豫程度。iou0.6是检测任务的常用匹配阈值比分类任务松因为框的位置本身有抖动。矩阵的读法第 0 行第 1 列是真实 one 被判成 two 的数量第 1 行第 0 列是真实 two 被判成 one 的数量后者是重点盯的指标。如果第 1 行第 0 列明显偏大说明 two 的特征没学够回到第 3 章加大过采样比例或者检查 two 样本里是不是混进了标注错误的 one。如果第 0 行第 1 列偏大说明模型过于敏感通常是反光区域被学成了 two 的特征需要在数据侧处理光源而不是一味调阈值。4.3 分阈值部署与置信度校准检测框架默认对两类用同一个置信度阈值这在代价不对称的场景里不合适。正确做法是给 two 类单独设一个更低的阈值让模型更愿意报异常。def decide(dets, thr_one0.50, thr_two0.25): dets: [(cls_id, score, xyxy), ...]已过 NMS 返回整图判定结果two 优先其次 one都没有则 empty twos [d for d in dets if d[0] 1 and d[1] thr_two] ones [d for d in dets if d[0] 0 and d[1] thr_one] if twos: return two, max(d[1] for d in twos) if ones: return one, max(d[1] for d in ones) return empty, 0.0两个阈值的差值不是拍脑袋定的要在验证集上扫一遍。固定thr_one在 0.5把thr_two从 0.05 扫到 0.6记录每一档下的漏检率和误报率选一个漏检率满足工艺要求、误报率还在可接受范围的点。多数产线能接受 1% 到 3% 的误报但漏检要求压到千分位所以thr_two往往落在 0.2 到 0.3 之间。decide里 two 优先于 one 是业务逻辑不是技术逻辑同一张图里如果同时检出两类说明画面里有多个包只要有一个是反的就要报警。如果你的产线一图只允许一个包那这个优先级逻辑还得配上“取最大框”的规则避免相邻包的边缘被误检成第二个目标。5. 避坑排查五个让离线指标虚高的动作这一章是我自己踩过、也见过别人踩的坑按“现象、原因、解决”三段写。共同点是这些坑在训练日志里都不报错指标还很好看问题全留到产线才爆发。5.1 镜像增强开着离线 mAP 冲到 0.96上线就崩现象训练收敛很快验证集 mAP 稳定在 0.95 以上混淆矩阵几乎是对角阵部署到产线后误检率超过 15%模型对包体角度特别敏感。原因增强配置里fliplr和flipud开了默认值 0.5训练时一半样本被镜像。镜像后的印刷图案现实中不存在模型学到的是镜像纹理而现场相机拍到的永远是真实图案。离线验证集同样被镜像污染所以指标虚高。解决两个 flip 参数全部置 0重训。同时把验证集单独抽出来人工过一遍确认没有镜像样本混入。改完之后 mAP 通常会掉 2 到 4 个点但上线误检率能降一个数量级。5.2 随机切分导致验证集是训练集的复印件现象换一个批次的图做测试mAP 从 0.92 掉到 0.58同一批次的图怎么测都好。原因按随机种子切分连续拍摄的近邻帧同时进了训练和验证两边的图差异只有几个像素模型等于在背答案。解决改成按批次或时间窗口整组切分第 3 章那段脚本就是干这个的。切完检查一下训练集和验证集的文件名前缀有没有交集有交集就是没切干净。5.3 反光区域把 two 的特征盖住模型系统性漏检现象two 的召回在正常光照样本上是 0.9在强反光样本上掉到 0.4混淆矩阵第 1 行第 0 列集中出现在某几个批次。原因环形光源角度固定袋面封边处的镜面反射在某个角度下正好盖住正反标识模型看到的是过曝白块没有可用的判别特征。这不是模型问题是成像问题。解决先动光源把环形光改成低角度暗场照明或者加偏振片让反光区挪开标识位置。光源改不了的情况下在预处理里对高亮区域做掩膜把过曝像素替换成邻域中值同时在训练集里补充这类样本别指望模型自己学会忽略。5.4 类别不平衡没处理模型退化成“全判 one”现象整体准确率 0.85 看起来还行但 two 的召回只有 0.3混淆矩阵里 two 几乎全被判成 one。原因正常样本是异常样本的 6 倍交叉熵损失下模型发现“全判 one”就能拿到不错的整体准确率梯度上没有任何动力去学 two。解决按第 3 章上加权采样把 two 的采样比例拉上来同时监控 two 的召回而不是整体准确率。改完之后整体准确率可能掉到 0.8 以下但 two 的召回能到 0.9这才是这套数据真正要的东西。5.5 标注框松紧不一致模型框越训越大现象验证集 mAP 看着正常但部署后框明显偏大经常把传送带背景框进来后处理按框面积过滤时误杀正常包。原因不同标注员对封边的处理不一致有人贴着印刷面画有人把锯齿封边全包进去同一类样本的框尺寸方差很大。模型学到的是一个平均意义上的松框。解决定一份带样图的标注规范明确封边包含规则然后对训练集做一轮抽检随机抽 50 张叠框看松紧不一致的回炉重标。规范定完之后新标注员上岗前先标 20 张做一致性比对IoU 低于 0.85 的不放行。6. 产线侧收尾多帧投票与复核队列的一个小技巧单帧判定的稳定性在产线上是不够的。传送带速度快的时候同一包会连续出现在 5 到 10 帧里中间夹杂运动模糊帧和反光帧单帧误判很常见。我一般会在检测之后加一层帧级投票用最近 N 帧的判定结果做加权把偶发的单帧异常压下去。具体做法是维护一个长度 7 的滑动窗口每帧的判定结果按置信度加权投票two 的票权乘 1.5因为漏检代价更高。窗口内 two 的加权票数超过阈值就触发剔除否则放行。参数上窗口长度取 7 是因为现场帧率 30fps、单包停留约 0.25 秒7 帧刚好覆盖一个包的完整通过过程再长就会把相邻包的结果混进来。参数取值说明窗口长度7 帧覆盖单包通过过程不跨包two 票权1.5高于 one体现代价不对称触发阈值加权票数 ≥ 3.0约等于两帧高置信 two复核队列容量200 条存疑样本落盘供人工抽检窗口长度和触发阈值要一起调。窗口短、阈值高响应快但漏检多窗口长、阈值低稳但延迟大。我的经验是先按上面这组值跑一周统计复核队列里的样本把人工确认的漏检和误报各抽 20 条看分布再微调阈值一般两轮就能收敛。存疑样本落盘这件事值得单独说。凡是0.15 score 0.35的 two 预测判定结果不确定全部存图加日志不直接触发剔除进复核队列。这一条让我在一个项目里抓到了光源老化导致的渐进式漂移——现场灯光用了三个月后亮度掉了 15%two 的置信度整体下移单看每天的指标发现不了是复核队列里存疑样本的比例从 2% 涨到 11% 才暴露出来。从那以后我每次部署这类正反检测都强制走一遍“批次切分验证 分阈值扫描 存疑落盘”这三步少一步心里就不踏实。希望这套数据和上面的流程能帮你把调料包正反检测这件事一次做对。本文还有配套的精品资源点击获取