
简介面向计算机视觉目标检测学习者的吸烟行为检测数据集包含991张原始图片及一一对应的PASCAL VOC格式XML标注可用于YOLO、SSD、Faster R-CNN等主流检测模型的训练与评估也可用于行为识别算法验证应用场景覆盖智慧安防、公共场所禁烟管理和边缘端实时监测等方向。压缩包共1982个文件由991张jpg原图与991个xml标注文件组成整体约44.92MBjpg提供真实环境中的吸烟姿态图像xml按PASCAL VOC规范记录每个目标的类别和边界框坐标文件一一对应且目录结构简洁可直接按比例划分训练集与验证集也便于转换为COCO等其他标注格式。平均识别率88.3%表明该数据的标注一致性与质量较为稳定当前已有262人学习下载。针对目标检测入门者、算法验证人员以及需要构建吸烟识别系统的开发者这份数据集能省去自行采集图像和人工标注的繁琐环节用户可直接加载训练88.3%的基准识别率也可作为复现实验、算法调优或横向对比的参照显著缩短项目前期的数据准备周期。对于毕业设计、竞赛练手或科研预研均具有实用价值。1. 吸烟数据集991张原图够不够训练一个能落地的检测器吸烟数据集在公开渠道里一直是稀缺资源。工业安全、工位行为识别、公共场所违规预警都需要检测人手里有没有烟这个目标而大部分通用目标检测数据集里根本没有这个类。这份数据集一共991张原始图片标注格式是PASICAL VOC XML项目标注者给出的平均识别率在88.3%——注意这个数字是一个基线参考不是买到手就能直接复现的保证它跟你选的模型、训练参数、验证集划分和置信度阈值都强相关。它真正的价值在于标注干净、类别单一、图片来自真实场景拿来跑一遍数据解析→格式转换→模型训练→指标验证的完整目标检测流程素材完全够用。适合两类人第一次想跑通数据标注到模型训练全流程的新手以及需要快速验证吸烟检测方案可行性的工程师。2. 数据集的真实构成991张原图与PASICAL VOC XML的字段拆解2.1 文件名里的信息Roboflow导出的典型特征拿到数据集先别急着训练先看文件名。以1277_jpg.rf.9c8828412f0497d36739ffc3e4553b84.jpg为例这段命名是有规律的1277是原始图片编号_jpg是Roboflow平台对.jpg扩展名的转写点号被替换成下划线rf是Roboflow标识后面一长串是导出时生成的随机hash。对应的XML文件叫1277_jpg.rf.9c8828412f0497d36739ffc3e4553b84.xml也就是说图片和标注文件的主文件名完全一致只有扩展名不同。我在拿到这类Roboflow导出数据时第一件事一定是做配对校验统计jpg和xml数量是否一致。991张图就意味着应该有991个XML文件如果数量对不上后面训练时一定会报找不到标签的错。用两条命令就能确认ls images/*.jpg | wc -l ls annotations/*.xml | wc -l这两条命令分别统计图片目录和标注目录里的文件数量。wc -l是统计行数因为ls每个文件输出一行所以行数就是文件数。如果两个数字不一致再用comm命令找出差异文件这个在第四章避坑部分会详细展开。另外还要顺手看一下图片尺寸是否统一这份数据里图片大多是640x480左右的分辨率但也有少量不同尺寸后续做训练时需要统一resize这个变量会直接影响标注框归一化坐标的准确性。2.2 打开一个XML标注五个关键节点决定检测框是否可用VOC格式的XML本质上是一个纯文本文件可以用任何文本编辑器打开。Windows下直接用记事本就能看但结构化浏览和编辑我一般推荐VSCode装个XML插件缩进和标签高亮会让字段关系一目了然。随机打开一个标注文件典型结构如下?xml version1.0 encodingutf-8? annotation folderimages/folder filename1277_jpg.rf.9c8828412f0497d36739ffc3e4553b84.jpg/filename source databasesmoking_dataset/database /source size width640/width height480/height depth3/depth /size object namesmoking/name bndbox xmin165/xmin ymin98/ymin xmax286/xmax ymax215/ymax /bndbox /object /annotation这里的关键节点有五个filename对应原图文件名size节点里的width和height是图片原始尺寸object节点里的name是类别名这里就是smokingbndbox子节点里的xmin、ymin、xmax、ymax四个值标定了目标框的左上角和右下角坐标。注意一个细节VOC格式允许一张图里有多个object节点也就是说一张图可以同时标注多个人、多个烟头。我写了个小脚本扫描整份数据集统计每张图的目标框数量分布这样能快速判断数据集的复杂度import xml.etree.ElementTree as ET from pathlib import Path from collections import Counter cnt Counter() for xml_file in Path(annotations).glob(*.xml): root ET.parse(xml_file).getroot() obj_count len(root.findall(object)) cnt[obj_count] 1 print(cnt) # 输出示例Counter({1: 871, 2: 102, 3: 18})这段代码用xml.etree.ElementTree遍历所有XML文件findall(object)取出一张图里所有目标节点Counter统计出现频次。执行后能看到大部分图片是单目标场景少部分有两到三个目标这说明数据分布偏向简单场景模型训练起来不会太难但也意味着密集场景下的泛化能力需要自己额外补充验证。对新手来说先确认991张图、单类别、每图1~3个目标这个基本面再开始训练会踏实很多。2.3 平均识别率88.3%的含义先别急着把它当预期指标项目描述里的平均识别率在88.3%这个数字很多新手会理解成我训练完能达到88.3%实际上它是标注者在某个模型、某个验证集划分下测出来的一个基准值。在目标检测领域识别率最常见的量化指标是mAP0.5即IoU阈值设为0.5时的平均精确度。88.3%放在单类别检测任务里属于中等偏上的水平不算惊艳但说明数据质量没问题标注框和类别基本一致模型能学到足够的特征。我一般会把这个数字当作这份数据能跑通且效果不差的信号而不是精确的预期目标。原因在于mAP的数值和验证集划分方式极度敏感——如果991张图里随机抽20%做验证那么同一场景、同一人物的多张相似图很可能同时出现在训练集和验证集里测出来的指标天然虚高。真正可靠的验证方式应该是按场景、按时间段划分这个后面第四章和第五章都会讲到。所以拿到数据先别纠结88.3%和你自己训练出的指标差多少先理解VOC格式的解析和转换把整个流程跑通再回头评价这个基准值。3. 把VOC转成YOLO训练格式转换脚本与数据集划分3.1 为什么要转格式VOC是通用标准训练器需要的是归一化txtVOC XML是数据交换层面的标准格式它完整保留了标注的语义信息适合人工阅读和跨工具迁移。但主流目标检测框架在训练时并不直接读取XML比如YOLO系列用的是纯文本格式每一行代表一个目标框类别ID 归一化中心x 归一化中心y 归一化宽度 归一化高度。归一化的意思是将坐标值除以图片宽高让所有数值落在0到1之间这样无论原始图片是640x480还是1920x1080模型输入的坐标空间都是一致的。这里顺带解释一个容易混淆的点VOC里的xmin/ymin/xmax/ymax是绝对像素坐标而YOLO需要的是中心点坐标和宽高且需要归一化。转换公式很简单x_center (xmin xmax) / 2 / img_widthwidth (xmax - xmin) / img_width高度方向同理。只要理解了这个换算关系转换脚本本质上就是一个XML解析加四则运算的问题没有任何黑匣子。3.2 转换脚本用ElementTree解析XML并计算归一化坐标下面这段脚本我直接拿来处理这份数据集可以完整地读取annotations目录下的所有XML并输出对应的YOLO格式txt文件。建议把脚本和数据集目录放在同一级运行前先确认路径正确。import os import xml.etree.ElementTree as ET from pathlib import Path # 路径配置 xml_dir annotations # VOC XML 所在目录 txt_dir labels # 输出 YOLO txt 的目录 os.makedirs(txt_dir, exist_okTrue) # 类别映射类别名 - 类别ID单类从0开始 classes {smoking: 0} def clip_coord(val, low, high): 将坐标裁剪到合法范围内处理标注越界 return max(low, min(high, val)) for xml_path in sorted(Path(xml_dir).glob(*.xml)): root ET.parse(xml_path).getroot() # 从 size 节点读取图片原始尺寸 size root.find(size) img_w int(size.find(width).text) img_h int(size.find(height).text) lines [] for obj in root.findall(object): name obj.find(name).text if name not in classes: continue # 跳过未定义类别 cls_id classes[name] box obj.find(bndbox) xmin float(box.find(xmin).text) ymin float(box.find(ymin).text) xmax float(box.find(xmax).text) ymax float(box.find(ymax).text) # 坐标裁剪到图片范围内 xmin clip_coord(xmin, 0, img_w) xmax clip_coord(xmax, 0, img_w) ymin clip_coord(ymin, 0, img_h) ymax clip_coord(ymax, 0, img_h) # VOC 左上右下 - YOLO 中心点宽高再归一化 x_center (xmin xmax) / 2 / img_w y_center (ymin ymax) / 2 / img_h box_w (xmax - xmin) / img_w box_h (ymax - ymin) / img_h lines.append(f{cls_id} {x_center:.6f} {y_center:.6f} {box_w:.6f} {box_h:.6f}) # 写出同名 txt out_path Path(txt_dir) / (xml_path.stem .txt) out_path.write_text(\n.join(lines), encodingutf-8) if not lines: print(f[warning] {xml_path.name} 没有可用目标已生成空文件)这段脚本的逻辑按顺序做了四件事遍历XML、读取图片尺寸、解析每个目标框、按公式转换坐标并写文件。clip_coord函数是我后来加的因为实际跑转换时发现少量XML的xmax或ymax比图片宽高还大应该是标注时手滑拉过了头直接归一化会产生大于1的异常坐标。参数方面需要注意的是classes字典目前只有smoking一个类别如果以后要扩展多类别按行追加即可输出文件名的.stem取自XML的主文件名和原图主文件名一致这样YOLO训练时能通过同名规则找到标签。转换完成后顺手做一次校验确认输出的txt都非空、坐标值都在0到1之间find labels -name *.txt | xargs grep -l ^0 [1-9]\|1\.\d | head -n 20这条命令的逻辑是查找所有txt文件中不符合0 0.x格式的行如果输出为空说明坐标基本合法。反过来说如果有一堆文件被列出来就要回到对应的XML检查数据质量问题。3.3 数据集划分与训练参数991张图怎么分、怎么跑转换完成后就是划分数据集。991张图我按80/20的比例划分即792张训练、199张验证。这里用的随机划分只是为了快速验证流程能跑通更严谨的按场景划分方法在第四章会讲。划分脚本如下import random from pathlib import Path import shutil random.seed(42) # 固定随机种子保证可复现 images list(Path(images).glob(*.jpg)) random.shuffle(images) val_count int(len(images) * 0.2) splits { train: images[val_count:], val: images[:val_count], } for split_name, imgs in splits.items(): img_dir Path(fsmoking_dataset/images/{split_name}) lbl_dir Path(fsmoking_dataset/labels/{split_name}) img_dir.mkdir(parentsTrue, exist_okTrue) lbl_dir.mkdir(parentsTrue, exist_okTrue) for img in imgs: shutil.copy(img, img_dir / img.name) label Path(labels) / (img.stem .txt) if label.exists(): shutil.copy(label, lbl_dir / label.name)这段划分脚本的核心是利用random.shuffle打乱列表顺序然后按比例切分。random.seed(42)是固定随机种子这样每次运行得到的划分结果一致方便复现和对比实验。注意val_count是验证集数量前val_count张进验证集其余进训练集。复制时同时复制图片和对应的txt标签保持目录结构符合YOLO训练器的预期。划分完成后写一个data.yaml配置文件供YOLO训练使用path: smoking_dataset train: images/train val: images/val names: 0: smoking这里path指向数据集根目录train和val用相对路径names是一个字典键是类别ID值是类别名。ID必须和转换脚本里的classes映射保持一致。训练命令我一般这样起yolo detect train datasmoking.yaml modelyolov8s.pt epochs100 imgsz640 batch16参数含义逐一说清modelyolov8s.pt表示使用YOLOv8s预训练权重作为起点s版本是速度和精度均衡的选择991张图用n版本更小也可以epochs100是训练轮数这个数据量下50到100轮足够轮数太多反而容易过拟合imgsz640是输入分辨率如果原图本身就是640x480这个值和原始尺寸接近训练效果最好batch16是批大小显存小于6GB的建议降到8。训练过程重点盯两个指标train/box_loss是否持续下降、metrics/mAP50(B)是否在验证集上稳步上升。如果训练集loss在降但验证集mAP停滞不前说明开始过拟合了需要提前停止或加大数据增强。4. 训练避坑991张小数据集最常见的五类翻车现场4.1 图片与XML配对失败文件名hash与后缀不一致现象训练刚启动就报错提示No labels found in /smoking_dataset/labels/train或者一堆图片找不到对应的txt。原因Roboflow导出时图片和标注文件的主文件名理论上一一对应但数据集经过多次拷贝、解压可能出现文件缺失另一个常见情况是转换脚本里用xml_path.stem生成txt文件名但XML主文件名里如果混入了空格或特殊字符和图片的主文件名就对不上了。解决写一个两行脚本先做配对检查找出所有在图片目录有jpg、却在labels目录没有txt的文件cd smoking_dataset for img in images/train/*.jpg; do base$(basename $img .jpg) [ ! -f labels/train/${base}.txt ] echo missing: $img done这条命令遍历所有训练图片用basename取主文件名再检查对应txt是否存在。输出的missing列表就是问题文件逐个确认后删除或重新转换即可。从那以后我每次拿到新数据集都会先跑一遍这种配对校验再往后走流程。4.2 标注框越界坐标比图片宽高还大现象训练过程中loss出现NaN或者推理时检测框一部分超出了图片边缘。原因标注人员在标注时把框拉出了图片边界或者VOC XML里手误填了超出尺寸的坐标值。如果不加处理直接归一化xmax - xmin可能为负数或大于1模型学到的框几何关系就是乱的。解决转换脚本里加clip_coord函数做边界裁剪我在3.2节的脚本里已经内置了这段逻辑另外建议转换后跑一遍全量统计检查所有txt里是否存在小于0或大于1的坐标值awk {if ($20 || $21 || $30 || $31 || $40 || $41 || $50 || $51) print $0} labels/*.txtawk的$1到$5分别对应类别ID、中心x、中心y、宽度、高度。如果这个命令输出为空说明坐标全部合法一旦有输出找到对应的XML人工核对。这里的教训是标注工具给的坐标不能盲信一定要在转换阶段做一次合法性兜底。4.3 验证集指标虚高同一场景被随机划分成了两份现象训练时mAP达到90%以上自己拿手机拍了一段视频测试漏检特别严重。原因随机划分数据集时同一摄像头、同一时间段、同一人物的多张连续帧被分配到了训练集和验证集两边。模型在验证时等于是看到了训练时见过的场景指标自然虚高但一到新场景就露馅。解决划分前先看文件名里的原始编号把同一编号前缀的图片归为一组按组划分而不是按单张划分。具体做法是把文件名按_jpg前面的数字分组同一组全部进训练集或验证集。991张图的数据量不大手工分组几分钟就能完成但这几分钟能省掉后面调模型的几天时间。4.4 过拟合训练集准确率99%验证集mAP只有70%现象训练到30轮以后训练集的cls_loss降到0.01以下验证集的mAP却从85%跌到70%左右并且继续训练也不反弹。原因991张图对一个检测模型来说属于非常小的数据量模型把训练集里的背景、光线、人物姿态都背下来了没有泛化到验证集。解决少用大规模预训练模型微调换成更小的模型版本如yolov8n.pt训练轮数控制在50轮左右配上patience15让它在验证集指标连续15轮不提升时自动停止数据增强开大一点尤其是随机翻转、色彩抖动和Mosaic。另外最有效的一招是补充负样本——收集一些完全没有吸烟行为的人脸、手部图片标注成背景类让模型学会没有烟就是没有烟。4.5 labelImg打开XML报错版本兼容问题现象想用labelImg复查标注结果打开部分XML时直接报错或者加载图片后看不到目标框。原因Roboflow导出的XML和旧版labelImg生成的XML在标签结构上有差异labelImg对某些字段顺序和缺失字段比较敏感。解决优先用VSCode直接查看XML内容或者用我上面的Python脚本做数据质量统计不一定非要经过labelImg。如果确实需要人工复查标注用labelImg打开图片后重新保存一次XML让工具按自己的格式重写再传给训练流程。这个问题的本质是标注工具之间的格式方言差异花时间去找兼容方案性价比很低直接用代码路径绕过它。5. 让88.3%可复现用混淆矩阵与测试集压测验证真实识别率训练完成后验证工作比训练本身更重要。先用YOLO自带的验证接口跑一遍测试集指标from ultralytics import YOLO model YOLO(runs/detect/train/weights/best.pt) results model.val(datasmoking.yaml, conf0.25, iou0.5) print(fmAP50: {results.box.map50:.3f}) print(fprecision: {results.box.mp:.3f}) print(frecall: {results.box.mr:.3f})这里conf0.25是置信度阈值表示只有当模型对目标预测的概率超过0.25时才把它当作检测结果iou0.5是IoU阈值判定预测框和标注框是否匹配的重合度标准。map50就是平均mAP0.5mp是平均精确率mr是平均召回率。输出结果里results.box.map50就是这块数据在你这套配置下的真实识别率基线。接着看混淆矩阵。YOLO训练完会在runs/detect/train/目录下自动生成confusion_matrix.png里面能看到四类数值被正确检测为正样本的目标、被漏检为正样本但模型没检出的目标、误检为吸烟的目标、真正检出的背景。对单类别数据集来说混淆矩阵的价值在于帮你数清楚漏检和误检分别是多少然后针对性地调conf阈值。如果漏检多召回率低把conf降到0.1或0.15代价是误检增多如果误检多精确率低把conf提高到0.4或更高代价是漏检增多。我一般会按0.05的步长从0.1到0.5做一组对比把每组mAP、精确率、召回率记下来然后挑业务场景里更关心的指标作为最终配置。最后做一个压测把标注数据集里没有的场景类型单独整理成一个小测试包比如户外逆光、多人同时吸烟、手遮挡烟头这类难例喂给模型看表现。991张图覆盖不到的长尾场景用这种方式补出真实边界。我这次跑完后发现逆光场景的漏检率比正常光照高出一倍后来靠补充这类训练图和调低conf才压回来。从那以后我每次跑完小数据集都会强制走一遍测试集压测→混淆矩阵核对→conf调参这个循环把88.3%当起点而不是终点模型的泛化边界才算真正摸清。希望帮到你。本文还有配套的精品资源点击获取