
简介这份吸烟数据集面向计算机视觉方向的目标检测学习者与算法开发者可用于训练和验证吸烟行为识别模型适用于课堂实验、课程设计及算法对比等场景。资源包共收录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 txtPASCAL VOC XML 和 YOLO txt 的核心差异在于坐标表达方式和元信息承载量。VOC 用绝对像素坐标记录xmin, ymin, xmax, ymax同时把图片宽高、通道数、标注类别名都写进同一个 XML 文件里YOLO txt 则只存归一化后的class_id cx cy w h图片尺寸得另外去查。对于吸烟检测这种小目标场景——烟头、手持香烟的区域往往只占图片很小一块——VOC 的绝对坐标在标注阶段更直观标注员不容易因为归一化换算把框画偏。但训练时主流框架吃的是 YOLO 格式所以转换这一步绕不开。我一般会先抽三到五张 XML 出来核对结构确认没有缺字段或者坐标越界的情况。下面是一个典型的 VOC XML 片段annotation foldersmoking_dataset/folder filename1277_jpg.rf.9c8828412f0497d36739ffc3e4553b84.jpg/filename size width640/width height480/height depth3/depth /size object namesmoking/name poseUnspecified/pose truncated0/truncated difficult0/difficult bndbox xmin213/xmin ymin147/ymin xmax298/xmax ymax302/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_ratio0.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_okTrue) 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 风格的配置参考参数推荐值说明imgsz640低于 512 小目标召回率明显下降batch16显存不足降到 8同步调小学习率epochs150小数据集 100 轮后基本收敛lr00.01配合 cosine 衰减conf_thres0.25评估时用实际部署可调到 0.4iou_thres0.45NMS 阈值吸烟框重叠少可适当降低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 rangeVOC XML 里的name字段如果有大小写差异比如Smoking和smoking混用转换脚本里CLASSES.index(name)会直接抛异常。跑转换之前先用grep -r name annotations/ | sort | uniq -c统计一下所有类别名的出现次数确认只有一种写法。如果有多种统一替换后再转。4.4 现象训练到一半突然报 CUDA out of memory991 张图里可能混了几张分辨率特别大的比如 1920x1080 甚至更高。YOLO 默认会把所有图 resize 到统一尺寸但数据加载阶段如果开了多 worker 且cacheTrue大图会占额外显存。解决办法是先跑一遍图片尺寸统计把超过 1280 的图单独拎出来要么统一 resize 到 640 再存一份要么在 dataloader 里设rectFalse强制方形填充。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 曲线画出来给业务方看让他们自己选工作点。希望帮到你。本文还有配套的精品资源点击获取