简介:这份吸烟数据集面向计算机视觉方向的目标检测学习者与算法开发者,可用于训练和验证吸烟行为识别模型,适用于课堂实验、课程设计及算法对比等场景。资源包共收录1982个文件,包含991张原始jpg图片与991个对应的PASCAL VOC XML标注文件,图片与标注一一配对,压缩包整体约44.92MB,体积轻便,便于快速下载与本地部署。标注采用标准VOC格式,可直接接入YOLO、Faster R-CNN等主流检测框架,省去自行标注与格式转换的繁琐工作。据描述,该数据集在常规训练条件下平均识别率可达88.3%,能够为模型效果提供较为可靠的基准参考。目前已有262人学习下载,适合需要快速搭建吸烟检测原型、验证算法性能或补充训练样本的读者使用,也可作为数据增强与迁移学习的素材来源。
1. 吸烟数据集落地:991 张原始图片与 88.3% 平均识别率的真实边界
手上这个吸烟数据集,991 张原始图片,PASCAL VOC XML 格式标注,官方给出的平均识别率是 88.3%。乍一看数字不算惊艳,但如果你真在公共场所行为识别、工地安全监控或者禁烟区巡检这类场景里跑过模型,就会知道这个量级的数据能把 mAP 稳在 88% 上下,已经能撑起一个可用的原型了。问题在于,很多人拿到数据集第一反应是直接丢进 YOLO 训练,跑完发现指标对不上,然后开始怀疑数据质量。实际上 88.3% 这个数字背后有明确的评估条件——它是在特定输入尺寸、特定置信度阈值和特定 IoU 判定下测出来的,脱离这些参数谈识别率就是耍流氓。这份资源适合两类人:一类是想快速验证吸烟行为检测可行性的算法工程师,另一类是需要一个干净 VOC 标注样本做迁移学习起点的开发者。图片文件名里那些_jpg.rf.后缀说明数据经过了一轮清洗和去重,不是随手爬下来的脏数据,这一点在后续训练稳定性上很关键。
2. PASCAL VOC XML 标注结构拆解:从文件名到边界框的完整链路
2.1 为什么这个数据集选 VOC 而不是 YOLO txt
PASCAL VOC XML 和 YOLO txt 的核心差异在于坐标表达方式和元信息承载量。VOC 用绝对像素坐标记录xmin, ymin, xmax, ymax,同时把图片宽高、通道数、标注类别名都写进同一个 XML 文件里;YOLO txt 则只存归一化后的class_id cx cy w h,图片尺寸得另外去查。对于吸烟检测这种小目标场景——烟头、手持香烟的区域往往只占图片很小一块——VOC 的绝对坐标在标注阶段更直观,标注员不容易因为归一化换算把框画偏。但训练时主流框架吃的是 YOLO 格式,所以转换这一步绕不开。
我一般会先抽三到五张 XML 出来核对结构,确认没有缺字段或者坐标越界的情况。下面是一个典型的 VOC XML 片段:
<annotation> <folder>smoking_dataset</folder> <filename>1277_jpg.rf.9c8828412f0497d36739ffc3e4553b84.jpg</filename> <size> <width>640</width> <height>480</height> <depth>3</depth> </size> <object> <name>smoking</name> <pose>Unspecified</pose> <truncated>0</truncated> <difficult>0</difficult> <bndbox> <xmin>213</xmin> <ymin>147</ymin> <xmax>298</xmax> <ymax>302</ymax> </bndbox> </object> </annotation>这里有几个字段值得盯一下。difficult标记为 1 的样本在 VOC 评估里会被跳过,但很多转换脚本直接忽略这个字段,导致训练时把本该排除的难样本也算进 loss,指标自然往下掉。truncated表示目标是否被截断,吸烟检测里手部出画的情况不少,这个字段对后续做数据增强有参考价值。pose在吸烟场景基本是 Unspecified,不用管。
2.2 批量校验 XML 完整性的脚本
拿到 991 张图对应的 XML,第一步不是转换,是校验。我见过太多因为 XML 里xmax小于xmin导致训练时 loss 直接 NaN 的翻车案例。下面这段脚本遍历所有 XML,检查坐标合法性、类别名一致性和图片文件是否存在:
import os import xml.etree.ElementTree as ET from pathlib import Path def validate_voc_annotations(images_dir, annotations_dir): issues = [] image_files = {f.stem for f in Path(images_dir).glob("*.jpg")} xml_files = list(Path(annotations_dir).glob("*.xml")) for xml_path in xml_files: tree = ET.parse(xml_path) root = tree.getroot() filename = root.find("filename").text stem = Path(filename).stem # 检查图片是否存在 if stem not in image_files: issues.append(f"缺失图片: {filename}") continue # 检查尺寸字段 size = root.find("size") w = int(size.find("width").text) h = int(size.find("height").text) for obj in root.findall("object"): name = obj.find("name").text bbox = obj.find("bndbox") xmin = int(bbox.find("xmin").text) ymin = int(bbox.find("ymin").text) xmax = int(bbox.find("xmax").text) ymax = int(bbox.find("ymax").text) # 坐标合法性 if xmin >= xmax or ymin >= ymax: issues.append(f"坐标异常: {filename} -> {xmin},{ymin},{xmax},{ymax}") if xmin < 0 or ymin < 0 or xmax > w or ymax > h: issues.append(f"越界: {filename} -> 图片{w}x{h}, 框{xmax},{ymax}") if name != "smoking": issues.append(f"类别名不一致: {filename} -> {name}") return issues if __name__ == "__main__": problems = validate_voc_annotations("./images", "./annotations") print(f"共发现 {len(problems)} 个问题") for p in problems[:20]: print(p)这段脚本的逻辑很直白:先建立图片文件名索引,再逐个解析 XML。xmin >= xmax这种判断能拦住大部分标注事故,越界检查则防止框跑到图片外面去。类别名统一性检查是因为有些数据集在合并时会把smoke、smoking、cigarette混在一起,训练时框架会当成多个类处理,直接拉低单类精度。跑完如果问题数在个位数,手动修一修就行;超过二十个,建议直接联系数据提供方确认版本。
2.3 VOC 转 YOLO 格式的转换脚本与参数说明
校验通过后开始转换。核心是把绝对坐标归一化到 0 到 1 之间,同时生成classes.txt和train.txt/val.txt划分文件:
import xml.etree.ElementTree as ET import os import random from pathlib import Path CLASSES = ["smoking"] def voc_to_yolo(xml_path, output_dir, img_w, img_h): tree = ET.parse(xml_path) root = tree.getroot() lines = [] for obj in root.findall("object"): name = obj.find("name").text if name not in CLASSES: continue cls_id = CLASSES.index(name) bbox = obj.find("bndbox") xmin = float(bbox.find("xmin").text) ymin = float(bbox.find("ymin").text) xmax = float(bbox.find("xmax").text) ymax = float(bbox.find("ymax").text) # 归一化并计算中心点与宽高 cx = (xmin + xmax) / 2.0 / img_w cy = (ymin + ymax) / 2.0 / img_h w = (xmax - xmin) / img_w h = (ymax - ymin) / img_h # 裁剪到 [0,1] 防止浮点误差越界 cx = max(0, min(1, cx)) cy = max(0, min(1, cy)) w = max(0, min(1, w)) h = max(0, min(1, h)) lines.append(f"{cls_id} {cx:.6f} {cy:.6f} {w:.6f} {h:.6f}") out_path = Path(output_dir) / (Path(xml_path).stem + ".txt") with open(out_path, "w") as f: f.write("\n".join(lines)) def split_dataset(images_dir, output_txt_dir, val_ratio=0.2): all_images = sorted(Path(images_dir).glob("*.jpg")) random.seed(42) random.shuffle(all_images) split_idx = int(len(all_images) * (1 - val_ratio)) train_list = all_images[:split_idx] val_list = all_images[split_idx:] os.makedirs(output_txt_dir, exist_ok=True) with open(os.path.join(output_txt_dir, "train.txt"), "w") as f: f.write("\n".join([str(p.resolve()) for p in train_list])) with open(os.path.join(output_txt_dir, "val.txt"), "w") as f: f.write("\n".join([str(p.resolve()) for p in val_list])) return len(train_list), len(val_list)voc_to_yolo里那个max(0, min(1, ...))的裁剪操作看着多余,实际上能救你一命——有些 XML 的坐标因为标注工具精度问题会算出 1.000001 这种值,YOLO 训练时直接报错退出。split_dataset固定了随机种子 42,保证每次划分结果一致,方便复现实验。991 张图按 8:2 切,训练集 792 张,验证集 199 张,这个量级做单类检测够用了,但要注意验证集里至少得有 30 个以上的正样本,否则 mAP 波动会很大。
3. 训练参数配置与 88.3% 识别率的复现路径
3.1 输入尺寸与 batch size 的取舍
官方 88.3% 的识别率不是随便跑出来的。吸烟检测的目标尺度偏小,输入尺寸如果压到 416,烟头区域可能只剩几个像素,特征提取网络根本抓不住。我建议输入尺寸至少 640,显存够的话上 768 或 896。batch size 方面,991 张图属于小数据集,batch 太大容易过拟合,16 或者 8 比较稳妥。下面是一份 YOLOv5 风格的配置参考:
| 参数 | 推荐值 | 说明 |
|---|---|---|
| imgsz | 640 | 低于 512 小目标召回率明显下降 |
| batch | 16 | 显存不足降到 8,同步调小学习率 |
| epochs | 150 | 小数据集 100 轮后基本收敛 |
| lr0 | 0.01 | 配合 cosine 衰减 |
| conf_thres | 0.25 | 评估时用,实际部署可调到 0.4 |
| iou_thres | 0.45 | NMS 阈值,吸烟框重叠少可适当降低 |
conf_thres这个参数是很多人对不上 88.3% 的元凶。官方评估时用的置信度阈值大概率在 0.25 附近,你如果拿 0.5 去测,召回率直接掉一截,算出来的 mAP 自然低。iou_thres影响的是 NMS 阶段,吸烟场景里一个人手上可能同时出现烟盒和香烟两个框,阈值设太高会把其中一个滤掉。
3.2 数据增强策略的边界
小数据集训练离不开增强,但吸烟检测的增强有禁区。水平翻转没问题,烟在左手还是右手翻转后语义不变。Mosaic 增强能提升小目标检测能力,但要注意 Mosaic 概率别开到 1.0,否则训练前期 loss 震荡厉害。HSV 色调增强可以加,但饱和度调整幅度别太大,香烟的白色烟身和橙色滤嘴对颜色敏感,调过头模型会学偏。
我一般会关掉上下翻转,因为吸烟动作里手部位置和面部朝向有强关联,上下翻转后出现“倒着抽烟”的样本,模型学到的特征反而被污染。旋转增强也要谨慎,小角度(±10 度)可以,大角度旋转会让边界框变得很松,回归分支学不准。
3.3 从训练日志判断是否复现成功
训练跑起来后,别只盯着最后那个 mAP 数字。前 10 个 epoch 看box_loss和obj_loss是否稳定下降,如果obj_loss来回跳,大概率是学习率太大或者 batch 里正负样本失衡。第 50 个 epoch 左右看验证集的precision和recall曲线,理想情况下两条线应该同步上升然后趋于平稳。如果precision很高但recall很低,说明模型太保守,把conf_thres降一点再评估。如果recall高但precision低,模型在乱报,检查一下验证集里有没有标注漏掉的吸烟样本——这种情况在小数据集里特别常见,标注员漏标了几张,模型检出来反而被算成误报。
4. 避坑与排查:991 张图训练时最容易翻车的五个点
4.1 现象:训练 loss 正常下降但 mAP 始终卡在 60% 以下
原因通常是验证集划分出了问题。991 张图如果按文件名排序后直接切前 80% 做训练、后 20% 做验证,而数据提供方恰好是按场景分批命名的,验证集可能全是某个特定场景的图片,模型没见过这种分布,指标自然崩。解决办法是用split_dataset里的随机打乱逻辑,并且固定种子,确保每次划分一致。另外检查一下验证集里正样本数量,少于 30 个的话 mAP 没有统计意义。
4.2 现象:推理时同一张图检出多个重叠的吸烟框
这是 NMS 阈值没调好。吸烟检测里香烟和手部区域高度重叠,模型可能对同一支烟输出多个候选框。把iou_thres从默认的 0.45 降到 0.3 到 0.35 之间,能有效合并重叠框。如果降了还有,检查一下训练时是不是把smoking和hand标成了两个类,有些标注员会把拿烟的手也框进去,模型学混了。
4.3 现象:XML 转换后类别 ID 对不上,训练时报 index out of range
VOC XML 里的name字段如果有大小写差异,比如Smoking和smoking混用,转换脚本里CLASSES.index(name)会直接抛异常。跑转换之前先用grep -r "<name>" annotations/ | sort | uniq -c统计一下所有类别名的出现次数,确认只有一种写法。如果有多种,统一替换后再转。
4.4 现象:训练到一半突然报 CUDA out of memory
991 张图里可能混了几张分辨率特别大的,比如 1920x1080 甚至更高。YOLO 默认会把所有图 resize 到统一尺寸,但数据加载阶段如果开了多 worker 且cache=True,大图会占额外显存。解决办法是先跑一遍图片尺寸统计,把超过 1280 的图单独拎出来,要么统一 resize 到 640 再存一份,要么在 dataloader 里设rect=False强制方形填充。
4.5 现象:模型在测试集上表现好,实际部署后误报率飙升
这是典型的域偏移。991 张图大概率来自有限几个场景,模型学到了背景里的某些相关性——比如某个品牌的烟盒颜色、某种光照条件下的肤色。部署到新场景后这些相关性不存在了,误报就上来了。缓解办法是在训练时加入随机裁剪和色彩抖动,强迫模型关注香烟本身的形状和纹理,而不是背景线索。另外部署前拿几十张新场景的图做一轮人工评估,别只看测试集指标。
5. 小目标召回优化:从 88.3% 再往上挤几个点的实操技巧
88.3% 是个不错的起点,但如果你要把它用到实际项目里,通常还得再往上推一推。吸烟检测的难点在于香烟目标小、遮挡多、和手部/面部粘连严重。我一般会从三个方向下手。
第一个方向是调整 anchor 尺寸。YOLO 默认的 anchor 是基于 COCO 数据集聚类出来的,最小那组 anchor 对应的是 10x13 像素左右的框。吸烟数据集里香烟的边界框可能只有 20x40 像素,和默认 anchor 匹配度不够。用 k-means 在训练集的所有 GT 框上重新聚类一遍,把 anchor 数量设成 6 组,最小那组调到 15x25 附近,召回率通常能涨两到三个点。聚类脚本用sklearn.cluster.KMeans就行,输入是所有框的宽高,注意要先归一化到输入尺寸的尺度。
第二个方向是切片推理。如果部署环境允许,把输入图切成 2x2 的四块分别推理,再合并结果。香烟在整图里只占几十个像素,切块后相对尺寸翻倍,小目标检测网络更容易抓到。代价是推理耗时变成四倍,适合对实时性要求不高的巡检场景。切片时注意块与块之间留 10% 到 15% 的重叠区域,否则跨块的香烟会被切断,两边都检不出来。
第三个方向是难例挖掘。跑完第一轮训练后,用模型在训练集上推理一遍,把置信度在 0.1 到 0.3 之间的预测框对应的图片挑出来,人工复核。这些图要么是标注漏了,要么是目标确实模糊。把漏标的补上,模糊的根据业务需求决定是否保留。补完标注后重新训练一轮,mAP 通常能再涨一两个点。这个流程我每次拿到新数据集都会走一遍,虽然费时间,但比盲目加数据有效得多。
还有一个容易被忽略的点是评估时的conf_thres和部署时的conf_thres要分开设。评估时为了对齐官方 88.3%,用 0.25;部署时为了压误报,可以提到 0.4 甚至 0.5,代价是召回率降一些。具体设多少取决于你的业务能容忍多少误报——禁烟区巡检宁可漏报也别误报,那就往高设;行为分析做统计报表,漏报影响更大,那就往低设。从那以后我每次交付模型前,都会把这两个阈值分开跑一遍评估,把 precision-recall 曲线画出来给业务方看,让他们自己选工作点。希望帮到你。
本文还有配套的精品资源,点击获取