简介:香烟盒子检测是目标检测入门中常见的单类别场景,便于聚焦算法本身。这是一份面向YOLO系列目标检测学习与实战的香烟盒子图像数据集,适合需要训练吸烟检测、商品识别或自定义目标检测模型的开发者与研究人员。数据集共961个文件,包含320张jpg原图、320个txt标签和320个xml标签,同时附带1个data.yaml配置文件,整体压缩包约29.47MB。图片与标签一一对应,且已划分好训练集与验证集,可直接搭配YOLOv5、YOLOv7、YOLOv8、YOLOv9、YOLOv10、YOLO11等算法进行模型训练与测试。标签提供两种常见格式:yolo格式的txt文件记录类别索引与归一化后的目标框中心点坐标、宽高;voc格式的xml文件便于使用Pascal VOC风格流程或在LabelImg等工具中查看校对。对刚接触YOLO流程的新手而言,免去了自己标注和转换格式的麻烦;对有经验的工程师则可用于快速验证网络结构或做迁移学习。目前已有134人学习下载,适合作为入门练习或小型检测任务的基础数据。
1. 人手一个烟盒检测模型:这份带标签数据集到底能做什么
把 320 张带标签的香烟盒子图像丢给 YOLOv8,从零训练一个能识别烟盒的模型,是我拆这份数据集时干的第一件事。yolo算法跑目标检测早就不是新鲜事,但真正卡住新手的往往不是算法本身,而是找不到一份标注干净、类别单一、能直接喂进 YOLO 训练管线的数据。这份资源解压开就是 images 和 labels 两个目录,txt 标签格式,意味着你不需要做任何格式转换,直接就能进 YOLOv5、YOLOv8 甚至 YOLO11 的训练流程。它解决的场景很具体:零售终端烟柜陈列识别、库存盘点、货架合规检测。适合两类人——刚学目标检测、想拿真实数据练手的新手,以及正在做零售视觉项目、需要现成正样本做迁移学习的工程师。后续我会把标注格式核查、训练参数、避坑记录和增强技巧完整拆开。
2. 先摸底:这 320 张图的目录结构与标签格式
2.1 解压后的目录长什么样
拿到任何目标检测数据集,第一件事不是急着训练,而是先摸清目录结构。常见做法是先看顶层目录,再抽查几个标签文件确认格式。这份数据集解压后的结构通常是这样:
cigarette_box_dataset/ ├── images/ │ ├── img_001.jpg │ ├── img_002.jpg │ └── ... ├── labels/ │ ├── img_001.txt │ ├── img_002.txt │ └── ... └── classes.txtimages 目录放原始图像,labels 目录放同名 txt 标签,classes.txt 里是类别名。这里有个关键点:图像文件名和标签文件名必须一一对应,YOLO 训练时就是靠这个同名匹配去找标签的。如果发现某张图像没有对应的 txt,训练时会直接跳过这张图,但日志里会报 warning,容易被忽略。
| 文件/目录 | 作用 | 注意事项 |
|---|---|---|
| images/ | 原始图像,jpg 或 png 格式 | 尺寸不统一时 YOLO 会自动 resize |
| labels/ | 同名 txt 标签,YOLO 格式 | 每行一个目标,5 列 |
| classes.txt | 类别清单 | 一行一个类别名,顺序对应 class_id |
2.2 打开一个标签文件:逐行解析 YOLO 格式
YOLO 格式的 txt 标签每一行代表一个目标框,格式是class_id cx cy w h,其中 cx、cy 是归一化后的中心点坐标,w、h 是归一化后的宽高。我用一段 Python 脚本把某个标签读出来,方便你直观理解。
# 读取一个标签文件并格式化输出 label_path = "labels/img_001.txt" with open(label_path, "r") as f: lines = f.readlines() for line in lines: parts = line.strip().split() class_id = int(parts[0]) cx, cy, w, h = map(float, parts[1:]) print(f"类别: {class_id}, 中心点: ({cx:.3f}, {cy:.3f}), 宽: {w:.3f}, 高: {h:.3f}")这段代码做的事情很简单:按空格切分每一行,把第一列转成整数作为类别 ID,后面四列转成浮点数作为归一化坐标。注意 cx、cy、w、h 都是 0 到 1 之间的小数,它们不是像素值。如果你看到某个坐标大于 1,那标签一定有问题,后面避坑章节会细说。像classes.txt里如果有多个类别,class_id 就对应它的行号,从 0 开始。
2.3 用 Python 批量复核标注质量
拿到数据集后做一次质量巡检是值得的,特别是标注框的坐标是否越界、宽高是否合理。我一般会写一段小脚本统计所有标签的异常情况,这一步能提前发现很多训练翻车的原因。
import os label_dir = "labels" issues = {"oob": 0, "zero_size": 0, "total_targets": 0} for fname in os.listdir(label_dir): if not fname.endswith(".txt"): continue with open(os.path.join(label_dir, fname), "r") as f: for line in f: parts = line.strip().split() if len(parts) != 5: continue cx, cy, w, h = map(float, parts[1:]) issues["total_targets"] += 1 # 坐标越界:归一化坐标必须落在 [0, 1] if not (0 <= cx <= 1 and 0 <= cy <= 1 and 0 <= w <= 1 and 0 <= h <= 1): issues["oob"] += 1 # 宽高为 0 的退化框 if w == 0 or h == 0: issues["zero_size"] += 1 print(issues)这个脚本统计了三类信息:越界坐标的数量、零宽高的退化框、以及总目标数。越界坐标通常出现在手工标注后处理不当的数据集里,会导致训练时损失函数出现 NaN 或模型收敛异常。零宽高的框同样要警惕,它在计算 IoU 时会出问题。我一般要求越界和零宽高框的数量为 0,才敢把数据喂给模型。
3. 把数据集灌进 YOLOv8:完整训练流程
3.1 数据划分与目录组织
YOLOv8 要求数据集目录里明确区分 train、val、test 三个子集。常见做法是先把 320 张图按 8:1:1 拆成训练、验证、测试三份,得到约 256、32、32 张,再分别建立软链接或直接拷贝文件。
import os import random import shutil random.seed(42) images = os.listdir("images") random.shuffle(images) n_train = int(len(images) * 0.8) n_val = int(len(images) * 0.1) splits = { "train": images[:n_train], "val": images[n_train:n_train + n_val], "test": images[n_train + n_val:] } for split, imgs in splits.items(): os.makedirs(f"dataset/{split}/images", exist_ok=True) os.makedirs(f"dataset/{split}/labels", exist_ok=True) for img in imgs: shutil.copy(f"images/{img}", f"dataset/{split}/images/") label = img.rsplit(".", 1)[0] + ".txt" if os.path.exists(f"labels/{label}"): shutil.copy(f"labels/{label}", f"dataset/{split}/labels/")固定随机种子很关键,不固定的话每次拆出来的验证集会变,实验结果没法复现。我这里用的是random.seed(42),你也可以换成任意整数。按 8:1:1 拆分在 320 张的规模下比较合理,验证集和测试集各留 32 张,足够观察模型的泛化趋势。
3.2 写 data.yaml:指定路径与类别
YOLOv8 用 data.yaml 来描述数据集路径和类别信息。这里最容易翻车的是路径写法,要写绝对路径或用相对./前缀,否则容易找不到数据。
# data.yaml path: ./dataset train: train/images val: val/images test: test/images names: 0: cigarette_boxnames下面直接映射 class_id 到类别名。如果类别名和classes.txt不一致,训练会继续但标签对不上。你如果用这份数据集做迁移学习,原有类别名可能是cigarette或cigarette_box,建议统一成同一个名字,避免推理时输出混乱。
3.3 训练命令与关键参数
数据准备好之后,直接用一行命令启动训练。我推荐从 yolov8s 或 yolov8n 起步,320 张图的数据量不支持直接上大模型。
yolo detect train \ data=data.yaml \ model=yolov8s.pt \ epochs=100 \ imgsz=640 \ batch=16 \ device=0 \ workers=4 \ patience=20| 参数 | 推荐值 | 说明 |
|---|---|---|
| epochs | 100 | 320 张图,100 轮足够;配合 patience 早停 |
| imgsz | 640 | 默认 640;烟盒算中小目标,不要低于 416 |
| batch | 16 | 显存不够就降到 8,效果差别不大 |
| patience | 20 | 连续 20 轮验证集没提升就停,省时间 |
| device | 0 | CPU 训练改成 cpu,但会慢很多 |
这个命令里model=yolov8s.pt加载的是 COCO 预训练权重,属于迁移学习。对 320 张的数据量来说,从 COCO 权重起步比从零训练收敛快得多,mAP 通常能高 5 到 10 个点。.pt文件会自动下载,如果网络不稳定,手动放到当前目录即可。
3.4 训练日志怎么读:关注 loss 和 mAP
训练过程中 YOLOv8 会在终端实时打印 loss 指标,结束后在runs/detect/train/目录下生成 curves 图和权重文件。我一般重点看train/box_loss是否持续下降、metrics/mAP50(B)是否还在上升,以及val/box_loss有没有和训练 loss 拉开差距。
mAP50是指 IoU 阈值 0.5 时的平均精度,烟盒这类单类别检测任务通常能到 0.9 以上才算合格。mAP50-95更严格,它把 IoU 从 0.5 到 0.95 取平均,对边界框的精确度要求更高。你手里这份数据如果标注质量过关,320 张图训练出的模型 mAP50-95 在 0.6 到 0.75 之间都算正常。如果 mAP50 一直卡在 0.8 以下,优先检查标注框是否有偏移,而不是急着调超参数。
4. 验证与部署:从 val 指标到真实场景推理
4.1 用验证集评估权重
训练结束后,用best.pt跑一次 validation,拿到精确率、召回率和 mAP 的完整报表。注意 YOLOv8 的 weight 目录里会同时存在best.pt和last.pt,评估一定要用best.pt。
yolo detect val \ model=runs/detect/train/weights/best.pt \ data=data.yaml \ split=val跑完会在终端打印 Precision、Recall、mAP50、mAP50-95 四项指标。对香烟盒这种单类检测,我习惯同时看 Precision 和 Recall:Precision 高说明误检少,Recall 高说明漏检少。实际场景里如果误检要付出代价(比如把其他商品识别成香烟),就优先保 Precision;如果漏检更严重,就优先保 Recall。这四行输出值得你截图留下来,后续调阈值的时候要回来对照。
4.2 导出 ONNX 并在 Python 里推理
验证满意后,把模型导出成 ONNX 格式,便于脱离 ultralytics 环境部署。
yolo export model=runs/detect/train/weights/best.pt format=onnx opset=12导出成功后在同目录下生成best.onnx,用 onnxruntime 加载推理。
import onnxruntime as ort import cv2 import numpy as np session = ort.InferenceSession("best.onnx") input_name = session.get_inputs()[0].name img = cv2.imread("test.jpg") img_resized = cv2.resize(img, (640, 640)) input_tensor = img_resized[:, :, ::-1].transpose(2, 0, 1)[None].astype(np.float32) / 255.0 outputs = session.run(None, {input_name: input_tensor}) boxes, scores, class_ids = outputs[0], outputs[1], outputs[2]这里要注意两点。第一,预处理必须和训练时一致:缩放后除以 255 归一化,通道顺序要转成 RGB,维度是(1, 3, 640, 640)。第二,ONNX 输出格式和 PyTorch 稍有不同,精简版输出直接就是框、分数、类别三个数组,省掉了 NMS 过程。有些部署框架要求你手动做 NMS,但 onnxruntime 导出的默认格式已经处理过。
4.3 置信度阈值与 NMS 参数的现场调法
部署时最常动的参数是置信度阈值和 NMS 的 IoU 阈值。默认置信度阈值 0.25 在干净场景够用,但烟柜照片往往有反光、遮挡,这时阈值要现场调。
# 推理后过滤低置信度框 conf_threshold = 0.35 iou_threshold = 0.45 keep = scores >= conf_threshold boxes, scores, class_ids = boxes[keep], scores[keep], class_ids[keep]置信度阈值从 0.25 调到 0.35,通常能显著减少把红色包装商品误判成香烟的情况,代价是可能会漏掉一些被遮挡的暗光烟盒。NMS 的 IoU 阈值保持 0.45 即可,烟盒几乎不会堆叠,不用动。如果你用 OpenCV 做后处理,cv2.dnn.NMSBoxes的score_threshold和nms_threshold两个参数对应上面的两个值,按现场效果微调即可。
5. 避坑:香烟盒数据集训练里的五条踩坑记录
5.1 训练 mAP 很高但现场漏检严重
现象:训练集和验证集 mAP50 都到 0.95 以上,拿到现场手机拍的照片里,侧面摆放的烟盒一个都检不出来。
原因:数据集里大部分图像是正面或俯拍角度,侧面、斜侧面的样本占比很低,模型学到的其实是「正面烟盒」的特征,不是「烟盒」的通用特征。320 张图里如果角度分布不均衡,这个问题会被 mAP 掩盖。
解决:先统计训练集里图像的拍摄角度分布。如果侧面样本确实少,我一般会去补拍一批侧面和斜侧面的照片,再手动标注后加进训练集。另一个折中方案是数据增强时把旋转角度调大,比如degrees=15,但增强只能缓解,补数据才是根治办法。
5.2 标签坐标出现大于 1 的异常值
现象:训练到第 20 轮时train/box_loss突然变成 NaN,日志里还报了RuntimeError。
原因:标注工具导出时把像素坐标当成了归一化坐标,或者某张超宽图像做归一化时用了错误的宽高基准。我用脚本检查发现大约 3% 的标签里 cx 或 w 大于 1。
解决:用前文 2.3 的复核脚本扫描全部标签,把越界的那几行修正或直接删除。具体做法是把出问题的图像和标签提取出来重新标注,如果只是个别坐标偏移,手动算出正确归一化值改掉即可。从那以后我每次拿到新数据集都会先跑一遍这个脚本,不再跳过。
5.3 验证集指标高但测试集崩盘
现象:train 和 val 的 mAP 都正常,但用split=test跑测试集时 mAP50 掉到 0.5 以下。
原因:数据划分没做随机打乱,或者划分时把同一批次拍摄的图像全放进了训练集,测试集里出现了训练时没见过的光照风格。320 张图里如果包含多个拍摄环境,分布不均就会这样。
解决:重新划分数据集,确保同一拍摄场景的图像比例在训练集、验证集、测试集中接近一致。最简单的做法是按拍摄批次做分组,然后按组拆分,而不是按单张图随机拆。如果数据里每个场景就十几张图,干脆把测试集合并进验证集,用 k 折交叉验证评估更稳妥。
5.4 训练时显存爆掉或训练速度极慢
现象:batch=16直接 OOM,或者显卡利用率不到 30%,训练 100 轮要跑几个小时。
原因:显存不足往往不是图像尺寸的问题,而是 batch 太大;利用率低则通常是数据加载瓶颈,workers太小导致 GPU 在等数据。
解决:显存不够就把 batch 降到 8 或 4,效果差异不大;同时把workers从 4 提到 8。如果预算允许,imgsz从 640 降到 512 也是常用手段,对烟盒这种目标大小影响很小。我用 8G 显存显卡训练,参数设为batch=8, imgsz=640, workers=8刚好跑满且稳定。
5.5 迁移学习权重加载报错或收敛变慢
现象:model=yolov8x.pt加载时报显存不足,换成yolov8n.pt后 mAP 反而比从零训练还低。
原因:YOLOv8x 参数量太大,320 张图的数据量撑不起这么大的模型容量,过拟合提前发生。用 n 版本时如果学习率还是默认的 0.01,预训练权重被破坏,收敛反而更慢。
解决:320 张图这个数据规模,我一般只用yolov8n.pt或yolov8s.pt。如果选 n 版本,把学习率调低到 0.005 试试。预训练权重的作用是给你一个好的初始化,但数据量太小的时候,权重里的 COCO 特征反而会干扰烟盒这类领域特定目标的训练。
6. 进阶:把 320 张图的价值榨干——增强策略与半自动标注
第 6.1 节 离线增强:翻转让 mAP 再涨两个点
训练时 YOLOv8 自带在线增强,但 320 张图的基数还是太小。我一般会在训练前多做一步离线增强,把数据集扩充到 1000 张左右再进训练管线。常见做法是每张图做左右翻转、上下小角度旋转、亮度抖动三种变换,各生成一张新图,标签文件里的坐标也跟着一起变换。
import cv2 import numpy as np img = cv2.imread("images/img_001.jpg") h, w = img.shape[:2] # 左右翻转 flipped = cv2.flip(img, 1) # 标签中心点 cx 变成 1 - cx with open("labels/img_001.txt") as f: lines = f.readlines() new_lines = [] for line in lines: parts = line.strip().split() cx, cy, bw, bh = map(float, parts[1:]) new_lines.append(f"{parts[0]} {1 - cx:.6f} {cy:.6f} {bw:.6f} {bh:.6f}\n") cv2.imwrite("aug/img_001_flip.jpg", flipped) with open("aug/img_001_flip.txt", "w") as f: f.writelines(new_lines)左右翻转时只有 cx 坐标要变成1 - cx,cy、w、h 都不动。亮度抖动更简单,用cv2.convertScaleAbs(img, alpha=1.1, beta=10)就能实现。增强后的数据要和原始数据混合在一起再重新划分,不要单独放一个目录然后又重复拆训练验证测试。这里有个细节:增强图只进训练集,不进验证集和测试集,否则评估结果会虚高。
第 6.2 节 迁移学习的正确姿势:权重选择与冻结策略
如果你的检测场景和 COCO 差异很大,比如要识别特定品牌的烟盒包装,可以考虑冻结 YOLOv8s 的 backbone 层先训几个 epoch,再解冻全部层微调。
yolo detect train \ data=data.yaml \ model=yolov8s.pt \ epochs=50 \ freeze=10freeze=10表示冻结前 10 层。冻结期间 GPU 显存占用更小,学习率可以从默认的 0.01 提高到 0.02,帮模型先适配新数据的特征分布。解冻后再用小学习率如 0.005 训练 50 轮来精调。这个二阶段策略在 320 张图的小数据集上,比一上来就全量微调稳定得多,也更容易复现。
第 6.3 节 半自动标注回灌:拿初版模型补数据
当模型 mAP50 到 0.9 以上后,可以用它给新采样的图片做预标注,你只需要人工检查修正,不用从零画框。具体做法是拿best.pt对新图跑一次推理,把置信度高且 NMS 过滤后的结果直接写入 YOLO 格式的 txt。
from ultralytics import YOLO model = YOLO("runs/detect/train/weights/best.pt") results = model.predict(source="new_imgs/", save_txt=True, conf=0.85) # 生成的标签在 new_imgs/labels/ 下,人工过一遍修正错框置信度阈值设 0.85 可以保证预标注的准确率,低于这个值的框宁可不标,人工补标比矫正一个错框省力。我用这个流程把数据集从 320 张扩到 1500 张只花了一个下午。从那以后我每次做小数据量目标检测,都会先训一个初版模型,再走一遍半自动标注,把人工标注成本降下来。希望这份数据和这些习惯能帮你少踩几个坑,直接跑通自己的烟盒检测项目。
本文还有配套的精品资源,点击获取