简介:这套吸烟检测数据集面向目标检测开发者和算法训练人员,包含991张原始JPG图片和991个PASCAL VOC格式的XML标注文件,共1982个文件,压缩包大小约44.92MB。图片均为真实场景原始采集,XML标注文件包含完整的类别与边界框信息,可直接用于YOLO、Faster R-CNN、SSD等主流目标检测框架的训练、验证与测试。数据量适中,既适合入门级模型快速迭代,也可作为补充数据扩充既有训练集;经过前期验证,该数据集平均识别率可达88.3%,为算法效果提供了可靠的基线参考。目前已有262人学习浏览,是评估和选用吸烟检测数据集的有益参照。获取后无需自行采集和标注,可直接开展数据格式转换、模型训练与业务验证,显著降低开发成本,适用于公共场所禁烟监控、安全生产等场景的吸烟行为识别应用。
1. 991张图、88.3%识别率:这套吸烟数据集到底能不能直接拿去做检测
拿到991张原始图片、平均识别率88.3%、PASCAL VOC XML格式标注的吸烟数据集,第一反应别是高兴,而是先问三个问题:这88.3%是什么口径?991张图够不够训练?标注质量能不能直接吃?做工地安全帽、加油站明火、厨房监控这类场景的工程师大概率都遇到过同一个尴尬:吸烟检测有需求,但标注数据要么贵要么找不到。这套数据集的价值在于它按PASCAL VOC标准用XML做了目标级标注,能直接喂给YOLO、MMDetection这类主流检测框架,适合用来验证流程、做算法选型、搭第一版预警原型。想直接上生产,建议把它当起点数据而不是终点。标题里的PASICAL是PASCAL的常见笔误,不影响使用,重点看格式和指标口径。
2. PASCAL VOC XML格式拆解:991张图的标注里到底存了什么
2.1 标准VOC目录结构与991张图的组织方式
数据集解压后,第一件事不是写代码,而是先摸清目录结构。PASCAL VOC从2007年那版开始,目录布局基本固定,这套991张图的数据集大概率也是按这个风格组织的:
VOCdevkit/ ├── JPEGImages/ # 原始图片,991张jpg ├── Annotations/ # 每张图对应一个xml标注 └── ImageSets/ └── Main/ # 划分文件:train.txt / val.txt / test.txtJPEGImages和Annotations是一一对应的,文件名相同、扩展名不同。比如一张图叫IMG_0231.jpg,它的标注就是Annotations/IMG_0231.xml。ImageSets/Main下通常有train.txt、val.txt、test.txt三个文本文件,每行写一个不带扩展名的文件名,模型训练时就是按这个列表去JPEGImages和Annotations里取数的。
这套数据我也见过另一种组织方式:991张图和xml平铺在一个目录里,另外附一个划分脚本或data.yaml。这种结构多见于标注平台直接导出的数据,不是严格按VOC树来放。顺序反过来没关系,但要先搞清楚一件事:xml里filename字段写的路径,和你实际解压后的路径是否一致。不一致的话,后面转格式时会有大量匹配失败,而且失败原因不好定位。
这里有个新手容易忽略的坑:JPEGImages里的图片可能是不同分辨率混放的,有的来自网络爬虫,有的来自监控截图,有的来自手机拍摄。这意味着XML标注里的size字段必须和真实图片尺寸一致,不然坐标归一化时宽高比会错位。建议先写一个核对脚本,把每张图的真实尺寸和XML里的size做个比对,不一致的先修正再往下走。这一步听着繁琐,但在991张这种小数据集上,花几十分钟核对能省后面几天的排查时间。
2.2 一个XML标注文件里到底有哪些字段
VOC格式的XML标注文件结构非常直白。用VS Code、Notepad++或者直接在Linux下用vim打开一个标注文件就能看到全貌。这里给一个典型结构,也是xml解析时最常见的字段集合:
<?xml version="1.0" encoding="utf-8"?> <annotation> <folder>JPEGImages</folder> <filename>IMG_0231.jpg</filename> <path>/home/user/smoking_dataset/JPEGImages/IMG_0231.jpg</path> <source> <database>Smoking Dataset</database> </source> <size> <width>1920</width> <height>1080</height> <depth>3</depth> </size> <segmented>0</segmented> <object> <name>smoking</name> <pose>Unspecified</pose> <truncated>0</truncated> <difficult>0</difficult> <bndbox> <xmin>642</xmin> <ymin>380</ymin> <xmax>758</xmax> <ymax>530</ymax> </bndbox> </object> </annotation>用xml.etree.ElementTree解析这个结构很方便,但有几个字段直接影响后面训练。第一个是size下的width和height,所有坐标归一化都要拿它们做分母;第二个是object下的name,如果要区分"吸烟"和"打火机"这种多类别场景,name映射表必须提前定好;第三个是bndbox,xmin/ymin/xmax/ymax四个值都是像素坐标,原点在图片左上角,右下方向为正。
字段含义整理成表会更清楚:
| 字段 | 类型 | 含义 | 训练时的用途 |
|---|---|---|---|
| filename | str | 图片文件名 | 匹配JPEGImages里的实际图片 |
| size/width、size/height | int | 图片宽高 | 坐标归一化的分母 |
| object/name | str | 目标类别名 | 映射到class_id |
| object/truncated | int 0/1 | 目标是否被图像边缘截断 | 可选过滤 |
| object/difficult | int 0/1 | 是否属于难例 | 建议过滤 |
| object/bndbox | 4个int | 左上角+右下角坐标 | 转成YOLO中心点宽高格式 |
注意一个细节:一张图里可能有多个object节点。比如一个人手里拿着烟,另一个人手里拿着打火机,那这张图的xml里就有两个object,每个object对应一个检测框。991张图不等于991个标注框,实际目标数可能多不少,这会影响后面对"识别率88.3%"的理解。
2.3 为什么这套数据选XML而不是COCO的JSON
这也是一个值得说道的点。PASCAL VOC的XML标注格式是LabelImg这类工具的默认导出格式,标注人员下拉一个框、选一个类别、点保存,直接生成XML,学习成本几乎为零。COCO的JSON格式虽然信息密度更高,但手工书写几乎不可能,必须靠脚本转换。从工程角度看,XML的好处是:单个文件独立、损坏一个不影响其他、用git管理时diff相对友好;从生态看,YOLO系列、MMDetection、Detectron2都有专门的VOC转换工具,绕一圈都能转成模型需要的格式。
三种格式放一起对比更直观:
| 格式 | 存储方式 | 手工可读性 | 训练框架支持 | 典型工具 |
|---|---|---|---|---|
| VOC XML | 每张图一个XML | 可读,结构清晰 | 需转换 | LabelImg |
| COCO JSON | 全部标注集中在一个JSON | 可读性差 | 部分框架原生 | labelme等 |
| YOLO txt | 每张图一个txt | 可读 | YOLO系列原生 | LabelImg导出 |
解析XML时最常见的坑是编码。一份标注文件如果以GBK或者ANSI编码保存,在Linux下直接用open()读取、不指定encoding,xml.etree.ElementTree的parse阶段很可能直接抛ParsingError。遇到这个问题,我一般的做法是先把文件头读出来做编码探测,或者用errors='ignore'兜底,但更稳的方法是批量解析前先统一转成UTF-8。另外给零基础读者一句实在话:就算不会写代码,也可以用VS Code直接打开xml文件阅读和编辑,看清结构再写脚本,比盲改配置稳得多。xml文件怎么打开和编辑这个问题,本质上就是找一个支持语法高亮的编辑器,然后按上面表格里的字段逐个对照。
3. 训练吸烟检测模型:VOC转YOLO格式、数据划分与训练命令
3.1 把VOC XML转成YOLO需要的txt标注
训练检测模型,从VOC格式起步是最常见的路径。标题里给的是VOC XML标注,但YOLO系列训练需要的是txt格式标注,每行是class cx cy w h,且坐标全部归一化到0到1之间。第一步先写转换脚本,常见做法是用Python内置的xml.etree.ElementTree解析,完全不依赖第三方库:
import xml.etree.ElementTree as ET import os # class顺序一旦确定不要随便改,训练和推理共用同一份 CLASS_DICT = {"smoking": 0, "cigarette": 1} # 按自己数据集的类别改 def voc2yolo(xml_path, label_dir): tree = ET.parse(xml_path) root = tree.getroot() # 读取图片宽高,坐标归一化全靠它 size = root.find("size") img_w = int(size.find("width").text) img_h = int(size.find("height").text) txt_name = os.path.basename(xml_path).replace(".xml", ".txt") lines = [] for obj in root.iter("object"): name = obj.find("name").text if "difficult" in [f.tag for f in obj] and int(obj.find("difficult").text) == 1: continue # 难例跳过,避免干扰训练 if name not in CLASS_DICT: continue # 不在类别表里的直接忽略 bndbox = obj.find("bndbox") xmin = float(bndbox.find("xmin").text) ymin = float(bndbox.find("ymin").text) xmax = float(bndbox.find("xmax").text) ymax = float(bndbox.find("ymax").text) # VOC格式是左上角+右下角,YOLO要中心点+宽高,全部除以图片宽高归一化 x_center = (xmin + xmax) / 2.0 / img_w y_center = (ymin + ymax) / 2.0 / img_h box_w = (xmax - xmin) / img_w box_h = (ymax - ymin) / img_h lines.append(f"{CLASS_DICT[name]} {x_center:.6f} {y_center:.6f} {box_w:.6f} {box_h:.6f}") # 一张图可能没有目标或全部被跳过,此时写空文件 with open(os.path.join(label_dir, txt_name), "w") as f: if lines: f.write("\n".join(lines)) else: f.write("") # 空文件,训练时会忽略该图 # 批量处理991张图的xml xml_dir = "Annotations" label_dir = "labels" os.makedirs(label_dir, exist_ok=True) for xml_file in os.listdir(xml_dir): if xml_file.endswith(".xml"): voc2yolo(os.path.join(xml_dir, xml_file), label_dir)逻辑说明:CLASS_DICT的键值对顺序就是训练时类别的索引,后面data.yaml里的names列表必须和它保持一致。对每个object节点,取出bndbox的四个坐标,把VOC的左上有右下格式换算成YOLO的中心点加宽高,并除以图片宽高完成归一化。difficult=1的框直接跳过,这是VOC规范里标注的难例,强行训练会干扰模型。最后写的空txt文件对应没有有效目标的图片,YOLO训练时自动忽略但不会报错,如果直接删掉txt会导致图片和标注数量对不上。
参数说明:这里最需要注意的是img_w和img_h来自XML里的size字段,而不是来自图片本身。如果XML里写的是1920x1080,而图片解码出来是720x480,那归一化坐标全部错位,训练出来的框全偏。脚本跑完以后,抽查几个txt文件,用文本编辑器打开,确认坐标值都在0到1之间,再进入下一步。
3.2 数据划分与data.yaml准备
标注转换完,还要划分训练集和验证集。991张图的小数据集,我一般直接用train_test_split,固定随机种子,保证每次复现的结果一致:
import os, random from sklearn.model_selection import train_test_split images = [f for f in os.listdir("JPEGImages") if f.endswith(".jpg")] train_files, val_files = train_test_split(images, test_size=0.15, random_state=42) with open("train.txt", "w") as f: for name in train_files: f.write(f"./JPEGImages/{name}\n") # 相对路径,训练机迁移不用改 with open("val.txt", "w") as f: for name in val_files: f.write(f"./JPEGImages/{name}\n")这里用15%做验证集,991张图就是训练集841张、验证集150张。这个比例对单类别检测来说验证集略小,但对小数据集是合理的折中,验证集太小会导致评估结果抖动大。图片路径写相对路径而不是绝对路径,不然换机器训练全废。写完txt后要自查一遍:train.txt和val.txt里的每一行,对应的图片和txt标注必须同时存在,缺一个就在训练时报错。
然后准备data.yaml:
train: ./train.txt val: ./val.txt nc: 2 names: ['smoking', 'cigarette']nc的值必须和CLASS_DICT里的类别数量一致,names的顺序必须和CLASS_DICT的key顺序一致。这两个顺序对不上,训练不会报错,但训练完的权重在推理时类别全乱,这是后面第5章要细说的坑。
3.3 用YOLOv8把模型训起来
数据齐了,用YOLOv8训练是当前最省心的选择。v5和v8命令大同小异,以ultralytics的YOLOv8为例:
yolo detect train \ data=smoking.yaml \ model=yolov8s.pt \ imgsz=640 \ batch=16 \ epochs=150 \ cache=True \ patience=20 \ workers=4参数说明:imgsz=640是大多数场景下的安全值,烟头属于小目标,后面可以尝试768甚至960,但显存和推理速度要同步考虑;batch=16在991张图这个规模下比较稳,太大容易后期过拟合,太小收敛慢;epochs=150对这个小数据集偏多,配合patience=20会在验证集指标连续20轮不涨时提前停,实际跑到60到80轮左右就会自动停。model=yolov8s.pt是官方预训练权重,迁移学习在小数据上的效果明显优于从零训练,强烈建议保留。
训练完看runs/detect/train目录下的weights/best.pt和last.pt,best是验证集上表现最好的权重,推理时一律用best。整个流程在单张消费级显卡上大约20到40分钟。
训练参数落在同一个表里方便调参:
| 参数 | 取值 | 说明 |
|---|---|---|
| imgsz | 640 | 输入分辨率,烟头小可提升到768/960 |
| batch | 16 | 显存不够降到8 |
| epochs | 150 | 配patience提前停 |
| patience | 20 | 验证集不涨则停 |
| cache | True | 991张图可以全缓存加速 |
这里解释一下为什么用s模型而不是n或m:n的参数少,在小数据集上容易欠拟合;m和l参数量大,991张图不够喂,会明显过拟合;s是性价比平衡点。等以后数据量超过3000张再考虑切m。
4. 识别率88.3%的水分在哪里:指标口径、复现评估与两个虚高信号
4.1 平均识别率不等于mAP,先搞清口径
"平均识别率在88.3%"这句话,在目标检测任务里没有标准定义。检测任务通用的汇报口径是mAP@0.5或者mAP@0.5:0.95,但"识别率"更接近分类任务里的accuracy,也就是所有测试样本里模型正确识别的比例。这两个口径相差很远,拿到数据集必须先确认这一点。
| 指标 | 全称 | 定义 | 适合场景 |
|---|---|---|---|
| accuracy | 识别准确率 | 正确预测数/总样本数 | 每张图只有一个类别 |
| precision | 精确率 | TP/(TP+FP) | 关心误报的场景,如明火预警 |
| recall | 召回率 | TP/(TP+FN) | 关心漏报的场景,如安全巡检 |
| mAP@0.5 | 均值平均精度 | IOU阈值0.5下全类别AP均值 | 目标检测通用指标 |
对吸烟检测来说,precision和recall通常比accuracy重要。在加油站场景,漏报一次可能出事故,recall优先;在办公区禁烟场景,误报太多会让人疲化,precision优先。88.3%如果是accuracy,说明有11.7%的图片整张被分错;如果是mAP@0.5,说明框定位和分类综合正确率是88.3%。两种解释差了很远,拿到数据集后先按下面方法复现一遍,再判断这个数字可不可信。
4.2 用验证命令复现一个可信的数字
自己训练完,评估这一步建议用官方命令,不要自己写评估脚本:
yolo detect val \ model=runs/detect/train/weights/best.pt \ data=smoking.yaml \ conf=0.25 \ iou=0.5conf=0.25是置信度阈值,iou=0.5是NMS用的IOU阈值,这两个值直接影响输出指标。如果把conf设成0.01,precision会下降、recall会上升,最后算出的mAP会完全不同,所以评估时保持默认值。官方会在终端输出mAP50、mAP50-95、precision、recall四项。拿自己的模型跑一遍,看mAP50离88.3%差多远。如果差得不多,说明88.3%大概率是mAP50口径;如果差很多,说明发布者的测试集划分或者数据增强策略和你不同,这个数字没法直接复现,不需要强行对齐。
多补一句:991张图上跑出的mAP50如果是88.3%,本身是个不错的基线,但这个数字有"小数据集乐观偏差"。样本少,训练集和验证集分布差异就小,评估结果偏乐观。这也是很多数据集页面上标的指标好看、部署出去就翻车的原因。真要上线,数据量至少是十倍起。
4.3 991张数据量下指标虚高的两个信号
第一个信号:验证集loss曲线在最后20个epoch像心电图一样上下抖动,说明模型在记忆训练集特征,验证样本稍微一变就错。这不是收敛问题,是数据量撑不起模型容量。
第二个信号:把模型输出的框可视化,把置信度阈值拉低到0.1再看一遍,会发现很多框其实是框到了电线、充电宝、烟盒这类长条形物体上。这是88.3%掩盖下的真实水平,也是吸烟检测最常见的误报来源。可视化这一步不要省,直接看图和框比看任何指标都直观。
5. 吸烟检测训练中的5个高频坑:现象、原因与排查方法
5.1 图片文件名和XML里的filename对不上,训练集加载了0张图
现象:训练一开始日志显示数据集为空,或者训练loss一直是NaN不下降。
原因:XML里filename字段写的是IMG_0231.jpg,但实际解压的图片叫img_0231.JPG,大小写不一致。Linux下这个情况会导致文件打开失败,Windows下偶尔能打开但标注对应错乱。还有一种是xml里的filename带了路径前缀,和train.txt里的相对路径拼接后变成双重路径。
解决:写一个校验脚本,读取每个XML的filename,再在JPEGImages里探测实际文件名,统一重命名或改XML里的filename。这个操作必须在转换标注之前完成,否则转换后的txt全是脏数据。
5.2 class_id顺移一位,模型把打火机学成了香烟
现象:训练loss正常下降,推理时把打火机识别成香烟,或者把无烟的框标成了smoking。
原因:CLASS_DICT的顺序、data.yaml里names的顺序、模型推理输出类别索引三者不一致。检测模型输出的是索引号,转成类别名靠names列表,顺序一错全错。
解决:把CLASS_DICT和data.yaml放在同一个地方维护,任何一次增删类别都同时改这两处。改完以后重新转一遍标注再训练,不要手改txt里的编号,手动改很容易顾此失彼。
5.3 数据增强把烟头和其他目标拼在一起,误检率突然上升
现象:开了Mosaic增强,训练几十个epoch后验证集precision还可以,实际部署时误报特别多。
原因:Mosaic把四张图拼成一张,等于把不同图片里的目标放进了同一场景。如果烟头被裁剪成小块,再拼到另一张图的背景上,模型会学到"背景里有桌子加长条形物体就是烟",而不是"手和嘴附近的长条形才是烟"。烟头本身是小目标,拼图操作对小目标的伤害尤其大。
解决:对小目标密集的吸烟检测,建议把Mosaic关掉或者把增强比例从1.0调到0.1左右。也可以在训练的最后一阶段关闭增强,让模型在干净数据上收尾。具体做法是训练时在ultralytics的配置里把mosaic参数单独降低,只针对这个数据集调整,不影响其他项目。
5.4 随机划分数据集导致验证集泄漏,指标虚高
现象:mAP50高到不敢信,线上视频里同一个画面反复误报。
原因:991张图里很可能包含同一段监控视频的连续帧,连续帧内容几乎一样。随机划分时这些相似帧一部分进了训练集,一部分进了验证集,评估结果天然偏高。模型实际没有见过真正的新场景,验证集测试了个寂寞。
解决:划分前按文件名前缀或拍摄时间聚类,确保同一批次视频帧只在train或val其中一边。991张图的场景如果来源分散,这个问题就轻很多;如果来源是几段监控视频,必须手动按视频ID划分,不能直接random split。
5.5 忘了过滤difficult=1的样本,训练集里混入噪声
现象:训练loss曲线总体正常,但偶尔出现尖刺,评估时mAP忽高忽低不稳定。
原因:数据集里difficult=1的框表示目标非常小、遮挡严重或本身就模糊。拿这些框当正样本,模型学到的特征不可靠,反向传播时会产生抖动梯度。
解决:转换脚本里显式跳过difficult==1,验证集里如果出现difficult样本也建议剔除,否则mAP计算时同样会被拉低。前面的voc2yolo脚本已经内置了跳过逻辑,前提是XML里存在difficult字段且值正确。有些标注工具导出的XML没有difficult字段,这种情况直接忽略即可,不要手动补。
6. 把88.3%往上顶:难例挖掘与切图训练两个实操技巧
6.1 难例挖掘:让模型自己的错误变成训练数据
训练完best.pt以后,在验证集上跑一遍推理,把置信度在0.1到0.5之间徘徊的框全部导出来,重新标注这些难例,加入训练集再训练一轮。两轮迭代往往就能把88.3%的基线推到90%以上。注意难例挖掘不能只挖正样本,也要挖负样本,比如手握成拳头、嘴里叼笔杆这类容易误判为吸烟的图像,同样应该作为负样本加入。991张图的正样本分布可能不均衡,难例挖掘是性价比最高的提升手段。我在实际项目里用过一次,把工地场景的吸烟识别从81%提到91%,没有改任何模型结构,只是加了两轮难例和负例。数据清洗的优先级永远在调参之前,模型结构不是黑匣子,数据才是真正决定上限的地方。
6.2 烟头小目标的切图训练与大分辨率权衡
如果部署场景里烟头只有20到40像素,在640分辨率下训练几乎学不到纹理特征。常见做法是把原图按512或640滑动切块,overlap设置50%,把切出来的子图重新生成标注再训练。推理时同样切块,再合并NMS结果。代价是推理时间涨三倍左右,需要自己权衡。还有一条便宜路线:把输入分辨率从640提到960,同时关掉Mosaic,能让模型对烟头更敏感。具体效果还是得用验证集mAP说话,参数不是玄学,是你在训练脚本里反复试出来的。
我第一次接吸烟检测单子时,一上来就折腾模型结构,换来换去识别率始终卡在90%,后来排查发现是验证集划分泄漏和difficult样本没过滤。把数据洗干净以后,同一套模型直接跳到93%以上。数据质量决定模型上限,这个教训到现在都适用。希望帮到你。
本文还有配套的精品资源,点击获取