简介1000多张室内外场景人员吸烟检测数据集采用VOC格式标注面向计算机视觉开发者、安防系统搭建者以及需要识别公共场所吸烟行为的研究人员。数据集中每张图片均附带对应xml标注文件标注了人员与吸烟区域位置可直接用于训练YOLO、Faster R-CNN、SSD等主流目标检测模型也能用于算法效果评估、数据增强和模型迁移实验。资源共2000个文件主要包含1507个xml标注文件、486个jpg图片和7个png图片标注与图片一一对应省去自行采集原始图像和人工标注的耗时环节。压缩包整体约861.69MB涵盖室内、室外多种光照条件和人群密度场景样本场景差异明显有助于提升模型在实际环境中的泛化能力与鲁棒性。目前已有240人学习下载适合刚入门目标检测的读者快速搭建训练流程也可供有经验的工程师作为额外训练数据或基准测试集。1. 1000多张室内外吸烟检测数据集为什么值得认真对待做了一段时间监控场景目标检测的人应该都有这种感觉吸烟检测这个需求到处都是工地、加油站、楼道、学校卫生间但真正让人放心能直接拿去用的数据集很少。这个标题里给出的是1000多张室内外场景人员吸烟检测数据集VOC格式标签。VOC格式意味着它不需要你做任何格式重构就能喂进常见检测框架而且1000多张的规模对于“烟头/吸烟动作”这种目标相对单一的检测任务只要处理得当是有可能用迁移学习跑出一个能上墙的模型的。它解决的问题很直接在监控画面里自动发现有人吸烟框出来生成报警事件。适合谁呢做安防、安全生产、消防隐患识别的算法工程师和集成商还有刚入门想找一个能落地练手的目标检测数据集的人。不过这1000多张图也不是拿来就能跑出好模型先得把这个数据集的底细摸清楚。2. 数据集的底细吸烟检测难点与VOC标签字段逐项拆解2.1 吸烟检测的难度等级小目标、烟雾、室内外光照三重叠加先说一个容易误解的地方。吸烟检测在实际项目里其实有两种标注粒度。一种是检测烟头本身也就是cigarette这一类目标非常小在一幅1920x1080的监控画面里一根烟可能只有20x30像素占比不到1%。另一种是检测“人正在吸烟”这个行为通常框的是人的上半身或手部区域目标相对大但难点在于“正在吸烟”这个动作的上下文特征很微妙。拿到一个数据集第一件事就是看xml里的name字段到底是cigarette还是smoking这决定了后面整套训练策略。如果标注的是烟头那这个任务的核心矛盾就是小目标检测。小目标在YOLO系列里一直是弱项因为下采样到最后一层特征图时20像素的烟头可能只剩3x3个特征点信息几乎被抹平了。更麻烦的是烟雾本身很多数据集根本不标烟雾只标烟头或打火机位置但烟雾才是人眼判断“在吸烟”的最明显信号。烟雾没有固定形态、边缘半透明、颜色随背景和光照变化让模型直接学烟雾特征基本是玄学。所以大多数场景里模型学的其实是“手边有一个高亮的、小尺寸的、类圆柱体目标”这种间接特征。室内外光照差异是这个数据集的另一个关键变量。室内环境通常有稳定的灯光色温偏暖或偏冷烟雾在灯光下会呈现淡蓝或淡白和浅色的墙面混在一起对比度很低。室外就复杂得多晴天太阳直射下烟头几乎不发光靠的是烟雾轮廓阴天烟雾灰蒙蒙融入背景到了晚上烟头反而是整个画面里最亮的小块模型很容易把路灯、反光点、白衣服上的纽扣一起拉进来。一个模型如果只在室内图上训练到室外基本会翻车。所以“室内外场景”这个关键词不是凑数它决定了数据增强和训练集划分必须按场景来做。2.2 VOC格式标签逐字段拆解xml里每个节点都在影响训练VOC格式是目标检测数据集里最通用的格式之一它的核心是每张图片对应一个同名xml文件。完整打开一个xml通常长这样annotation folderJPEGImages/folder filenameimg_001.jpg/filename size width1920/width height1080/height depth3/depth /size object namesmoking/name bndbox xmin100/xmin ymin120/ymin xmax160/xmax ymax260/ymax /bndbox truncated0/truncated difficult0/difficult /object /annotation这里面真正决定训练结果的节点就这么几个size里的width和height是原图的真实尺寸后续任何坐标系转换都以它为准object里的name是类别名注意它必须和训练配置里的names保持严格一致大小写、空格都算作不同字符bndbox里的四个值是标注框左上角和右下角的像素坐标它只是矩形框不包含旋转角度也就是说斜着拿的烟头它框的是一个外接矩形。重点说一下truncated和difficult这两个经常被忽略的字段。truncated表示目标有没有被图片边缘截断比如人站在画面边上半个身体被切掉了。difficult表示这个目标是不是难例在VOC官方评测里difficult1的样本不计入评估结果因为这些框连人工标注都不确定。YOLO系列的转换脚本通常会忽略difficult1的框但如果你处理的是吸烟检测我建议保留一部分truncated1的样本。场景里有大量遮挡和截断让模型见过“被切断的烟头”反而有助于减少漏检。2.3 拿到数据集的第一个落地动作写脚本统计样本分布不管数据集从哪来先别急着转换格式。我会先写一个统计脚本把类别分布、框尺寸分布、分辨率分布打印出来这一步能省掉后面很多麻烦。常见的坑就是光看到“1000多张”就开训训到一半才发现室内和室外严重失衡或者xml里的name有两个不同的写法。统计脚本不用复杂用Python自带的标准库就行import xml.etree.ElementTree as ET from pathlib import Path from collections import Counter ann_dir Path(Annotations) counter Counter() total_box 0 tiny_box 0 size_set set() for xml_file in sorted(ann_dir.glob(*.xml)): tree ET.parse(xml_file) root tree.getroot() w int(root.find(size/width).text) h int(root.find(size/height).text) size_set.add((w, h)) for obj in root.iter(object): name obj.find(name).text.strip() counter[name] 1 total_box 1 box obj.find(bndbox) xmin float(box.find(xmin).text) xmax float(box.find(xmax).text) ymin float(box.find(ymin).text) ymax float(box.find(ymax).text) box_w (xmax - xmin) / w box_h (ymax - ymin) / h if box_w 0.05 or box_h 0.05: tiny_box 1 print(图片总数:, len(list(ann_dir.glob(*.xml)))) print(类别统计:, dict(counter)) print(小目标框占比(相对图片小于5%): {:.1f}%.format(tiny_box / total_box * 100)) print(分辨率种类:, size_set)这个脚本的逻辑很简单遍历所有xml解析出每张图的真实宽高再遍历每个标注框按类别计数同时计算框的宽高占图片宽高的比例。把小于5%的框视为小目标做一个粗略估计。输出结果里最该关注的是小目标框占比如果超过40%后面的训练就得往高分辨率输入和小目标增强方向倾斜。另外如果xml里出现了意料之外的类别名比如cigarette和smoke同时存在这里就会立刻暴露出来这是比任何文档都靠谱的数据集体检报告。3. 把VOC喂进YOLO格式转换、训练配置与参数调优3.1 VOC目录整理与train/val划分随机划分不如按场景划分做完统计之后下一步就是把数据集整理成YOLO训练的标准目录结构。YOLOv5和YOLOv8都支持了两种方式一种是不转格式直接用VOC路径另一种是转成txt格式。实际项目里我还是推荐转txt因为txt格式的读取效率更高做数据校验也方便。标准结构是这样mkdir -p smoking_dataset/images/train smoking_dataset/images/val mkdir -p smoking_dataset/labels/train smoking_dataset/labels/val目录结构确定了再把图片和xml按比例划分到train和val。这里有一个很多新手会踩的坑随机划分。吸烟检测的图片往往来自连续的视频帧相邻帧的内容高度相似随机划分很容易让同一组几乎相同的画面同时出现在训练集和验证集里导致验证指标虚高一上现场就现原形。所以我会先看文件名有没有规律很多数据集的命名里带了场景信息比如indoor_001.jpg、outdoor_002.jpg。如果有这种前缀就按前缀把整个场景分到同一侧保证室内和室外同时出现在训练集和验证集里python split_dataset.py --train-ratio 0.8 --seed 42划分脚本的核心逻辑是先按文件名前缀分组再对组进行随机划分而不是对单张图片划分。这样划分出来的验证集才有参考价值。3.2 VOC转YOLO txt把xml坐标归一化注意三个边界坑目录准备好之后写VOC转YOLO的脚本。核心计算逻辑是把xml里的像素坐标转成YOLO需要的归一化中心坐标和归一化宽高import xml.etree.ElementTree as ET from pathlib import Path classes [smoking] # 必须和xml里的name完全一致 def voc_to_txt(xml_path, out_path, img_w, img_h): tree ET.parse(str(xml_path)) root tree.getroot() lines [] for obj in root.iter(object): name obj.find(name).text.strip() if name not in classes: print(f[警告] 未知类别 {name} 在 {xml_path}) continue cls_id classes.index(name) box obj.find(bndbox) xmin max(float(box.find(xmin).text), 0) ymin max(float(box.find(ymin).text), 0) xmax min(float(box.find(xmax).text), img_w) ymax min(float(box.find(ymax).text), img_h) if xmax xmin or ymax ymin: print(f[警告] 无效框 {xml_path}: xmin{xmin} ymin{ymin} xmax{xmax} ymax{ymax}) continue cx (xmin xmax) / 2 / img_w cy (ymin ymax) / 2 / img_h bw (xmax - xmin) / img_w bh (ymax - ymin) / img_h lines.append(f{cls_id} {cx:.6f} {cy:.6f} {bw:.6f} {bh:.6f}) with open(out_path, w) as f: f.write(\n.join(lines))这里有三个边界必须处理。第一xmin和ymin不能小于0xmax和ymax不能大于图片宽高否则归一化数值会超出[0,1]范围训练时直接报错。第二如果裁剪完之后xmax小于等于xmin说明原始标注框本身就是错的这种框要打印出来不要静默跳过。第三归一化时分母用的是xml里size节点的宽高不是图片实际读出来的宽高如果这两个不一致转换出来的坐标就会整体偏移。我在一个项目里就吃过这个亏图片被批量缩放过了但xml没同步更新结果训练出来的模型框全部偏到左下角。3.3 编写数据集yaml并启动yolov8训练参数怎么设才不浪费这1000张图转换完成后写数据集的yaml配置文件。文件路径可以直接用相对路径但为了减少出错我一般用绝对路径path: /home/user/smoking_dataset train: images/train val: images/val nc: 1 names: [smoking]这里nc必须和names里的数量一致names里的字符串必须和xml转换时的classes顺序保持一致。然后启动训练用yolov8s的预训练权重yolo detect train \ datasmoking.yaml \ modelyolov8s.pt \ epochs200 \ imgsz640 \ lr00.005 \ batch16 \ mosaic1.0 \ patience30 \ freeze10命令里每个参数都是针对这个数据集的实际选择。modelyolov8s.pt是从COCO预训练权重开始迁移学习对于1000多张的小数据集这是最关键的底牌不要从随机初始化开始训。lr0设到0.005而不是默认的0.01因为预训练模型的特征提取器已经很好用了学习率太大容易把之前学到的通用特征冲刷掉。freeze10意思是冻结前10层的参数前10层主要提取边缘、颜色、纹理这些通用特征在迁移学习初期冻结它们可以防止小数据集过拟合也显著降低显存占用。选yolov8s而不是yolov8m或yolov8l是因为这个规模撑不起大模型。1000多张图训练一个l模型基本是过拟合到参数里验证集mAP虚高但实际场景一塌糊涂。如果你更熟悉yolov5也可以改成python train.py --data smoking.yaml --weights yolov5s.pt思路一致只是命令行风格不同。至于训练自己的数据集这1000多张图最忌讳的是一口气跑几百个epoch真实有用的做法是先用200个epoch跑出基线再把验证集上漏检最严重的图收集起来补标注再微调。3.4 迁移学习与数据增强小数据集保命的两张底牌1000多张图在这个时代算很小的数据量所以训练策略必须围绕迁移学习和小心的数据增强来设计。带预训练权重的模型相当于一个已经见过大量通用图像特征的底座要做的是在这个底座上快速适应“烟头”这个特定目标而不是从零学起。我在项目里习惯先冻结前10层跑50个epoch让检测头和后面的特征层先适应新类别再解冻所有层用更小的学习率微调整个网络。数据增强方面参数需要认真对待不能全开默认值。针对吸烟检测这种小目标场景左右翻转fliplr0.5可以开因为烟头不带有方向语义尺度扰动scale我建议控制在0.3以内如果scale设太大模型会频繁看到被缩小到十几个像素的烟头学出来的特征非常不稳定mosaic增强在训练前中期开但最后20个epoch建议关闭因为mosaic合成的图片和真实监控画面差异很大末期还用它反而会让指标回不到正常分布上来。注意1000多张图里如果目标是烟头输入分辨率不要吝啬。640是YOLO的默认值但烟头在640下只有十几个像素属于极小目标。显存允许的话用960甚至1280训练检测率会有肉眼可见的提升代价是速度变慢。这属于典型的速度和精度置换做安防告警项目我一般直接上960。4. 训练避坑五连室内外场景里最容易翻车的五个细节4.1 标签坐标越界txt里出现大于1的归一化值现象训练中途报错或者loss稳定但验证集mAP一直为0。抽查转换出来的txt发现一行里某个数值大于1。原因xml的bndbox坐标超出了图片尺寸常见于标注工具允许拖拽到图片外或者图片在标注后被批量裁剪过但xml没有同步更新。转换脚本如果没做裁剪归一化之后cxbw就会大于1。解决转换脚本里必须对xmin、ymin、xmax、ymax做上下界裁剪并把裁剪后宽高小于等于0的框打印出来单独修xml。我自己的规则是“遇到坏框不静默跳过一定要打出文件名”因为这种数据往往不止一个留下来迟早还会炸。4.2 类别名混写xml里同时出现cigarette与smoke现象训练时nc1但预测阶段几乎不输出任何目标。回看统计脚本的结果发现xml里的name字段有cigarette和smoking两种写法。原因标注人员习惯不同有人写全称有人写缩写还有人把标签名不小心复制粘贴多了个空格。XML解析时如果没做strip空格也算在字符串里类别名就对不上了。解决转换脚本里做一个白名单映射遇到名单外的类别直接抛出异常而不是continue。宁可让转换中断让代码把未知类别名暴露出来也不要放过一个拼写错误的数据。这个习惯能帮你把大量顺手产生的脏数据挡在训练之前。4.3 训练loss振荡不降先查标签别先调学习率现象loss在前30个epoch一直在2附近上下跳动不收敛。这时候很容易条件反射去降lr或者改优化器结果调了一圈没变化。原因标签文件本身有问题。最常见的是图片被改名或重排但对应的txt没有同步改名导致训练时图片和标签错配模型在一个错误的上界附近震荡。另一个可能是转换出来的txt里坐标全为0也就是某张图的xml解析失败但脚本没报错。解决先做标签自检写一个检查脚本确保每个图片文件都有一个同名txt且txt里所有数值都在0到1之间from pathlib import Path import os images_dir Path(smoking_dataset/images/train) labels_dir Path(smoking_dataset/labels/train) for img in sorted(images_dir.glob(*.jpg)): label labels_dir / (img.stem .txt) if not label.exists(): print(缺少标签:, img) continue for line in label.read_text().strip().splitlines(): parts line.split() if len(parts) ! 5: print(格式错误:, label) break vals [float(v) for v in parts[1:]] if any(v 0 or v 1 for v in vals): print(坐标越界:, label, line) break把检查脚本放进训练前的pipeline里这个坑基本就不会再遇到了。4.4 验证集mAP不错现场室外漏检严重场景不均衡现象训练完看验证集mAP0.5有0.75觉得很不错结果拿到现场一测室内表现基本靠谱室外几乎全漏。原因训练集里室内图片占了一大半室外图片只有一小部分模型已经被室内场景主导了。更隐蔽的是随机划分导致的“数据泄漏”同一段视频的连续帧同时进了训练集和验证集网络相当于提前见过答案指标自然好看。解决划分数据集时按场景分组确保室内和室外都按比例进入训练和验证。如果室外样本实在不足用亮度扰动、颜色抖动和翻转对室外图片做复制增强但要注意复制增强的图片只能进训练集不能进验证集。另一个技巧是按文件名里的场景前缀做分层抽样把同一场景的帧整体分到一侧。4.5 误检打火机、亮片负样本缺失的典型症状现象部署到现场模型把打火机、手机屏幕反光、白墙上的光斑都框成了烟头报警消息一分钟刷好几条。原因训练数据里全是正样本也就是每一张图都有烟头模型没有见过“没有烟头但看起来像烟头”的画面只能靠有限的特征去猜而小目标的特征本来就模糊只要有高亮的斑点就可能触发。解决收集几段没有任何吸烟行为的监控视频抽帧生成不带标注的图片加入训练集作为负样本。YOLO的标签文件可以用空txt表示这张图没有目标。这一步对吸烟检测尤其重要因为烟头在低分辨率下就是“亮点”模型如果没见过足够多的反光点很难学会把它和真实烟头区分开。想少误报负样本和正样本要一起训练。5. 上墙前的事验证集可视化、报警阈值与难例迭代训练完成后先别急着上墙跑一遍官方验证命令把结果物拿到手yolo detect val datasmoking.yaml modelbest.pt它输出的结果里我最看重两个文件confusion_matrix.png和results.csv。confusion matrix能直接看到误报往哪个类别去了results.csv里每一行的mAP0.5和mAP0.5:0.95要分开看。对吸烟检测这个小目标任务mAP0.5:0.95偏低是正常的因为在严格的IoU标准下十几个像素的框稍微偏一点就归不进去了mAP0.5才是这个场景该盯住的指标。有了验证结果再做一轮可视化推理把验证集图片跑一遍保存带预测框的图人工翻一遍挑出最离谱的误检和漏检样本。然后把预测框面积和标注框面积做个比值从而筛选出网络对小目标漏检的典型图片。把这些难例收集起来补标注后加入训练集用原来预训练权重学习率降到之前的十分之一再训50个epoch。这个循环每次能带来几个点的提升比盲目堆epoch有效。最后要说的是报警阈值。监控场景的误报代价很高置信度阈值我一般从0.25提到0.35或者更高。我之前处理一个工地项目时把阈值从0.25调到0.45误报是降下去了但查看回放时发现有漏检白天漏了三四次。折腾了半天才发现问题不在阈值而在训练集里没有足够多的逆光室外样本。加了一批逆光负样本重训之后阈值保持0.35就已经很稳了。这个习惯后来我一直保持先看数据分布再动阈值阈值只是兜底不是解药。数据集的规模只是起点真正决定模型能不能用全看你在标注校验、场景划分和负样本补充上花了多少心思。希望上面这些从数据体检开始的土办法能帮你把这个方向顺利做下去。本文还有配套的精品资源点击获取