简介面向目标检测与行为识别方向的开发者这份数据集涵盖1000多张室内及室外场景中人员吸烟行为的真实图像配套VOC格式XML标注文件可直接用于训练吸烟检测模型也可用于迁移学习或算法效果验证。资源特别适合需要自定义数据集完成毕业设计、竞赛项目或安防应用落地的研发人员标注信息包含目标类别与边界框坐标降低了从零标注的门槛。包体共2000个文件以486张JPG图片与1507个XML标签为主体另有少量PNG辅助图片XML文件逐一对应图像中的目标位置与类别压缩包约861.69MB整体结构便于筛选和划分训练集、验证集。目前已有241人学习下载适合从入门到进阶的目标检测研究者快速获取带标注的样本数据。除图像与标签外资源还附有检测效果参考链接便于使用者比对模型输出与标注质量减少自行采集标注的时间成本直接投入到YOLO、Faster R-CNN等常见检测框架中完成训练与评估。1. 拿到1000多张室内外吸烟检测数据集先别急着训练先看VOC标签里装了什么有人在工地上做人员吸烟检测从网上找了一个“1000多张室内室外场景人员吸烟检测数据集voc格式标签”的包解压后是JPEGImages、Annotations和ImageSets看起来是标准PASCAL VOC结构。这个量级对于检测任务来说卡在“能跑demo”和“能上线”的中间区域直接训练大概率在测试集上很漂亮部署到现场却翻车。问题不是模型不行而是忽略了VOC标签里那些细节difficult标记、边界框坐标基准、场景构成。这篇笔记把数据集拆开从标签解析、格式转换、训练配置到验证技巧按做这类小规模检测数据集的常见路径完整过一遍。适合准备用吸烟检测做验证或快速落地的工程师也适合刚接触VOC格式的人。2. 拆解这份吸烟检测数据集室内外场景差异、小目标特征与XML标签构成2.1 室内外场景差异为什么室外烟雾检测比室内更容易误报室内场景的典型位置是第一印象里的“室内”办公楼走廊、茶水间、网吧、KTV包间。光线通常恒定灯源方向单一背景以墙面、桌椅、显示器为主纹理相对干净。烟雾在这种光线下呈现明显的灰白色卷曲烟头是一个带橙色光点的短条状目标轮廓分明。模型学到的特征比较稳定训练时收敛也快。这类图在数据集中通常占比不小因为采集容易拉个手机就能拍。室外场景的坑在于“看起来能检测实际阈值很难调”。白天太阳底下烟雾颜色会被环境光带走逆光时几乎透明背景里还有树叶、水汽、车辆尾气这类和烟雾局部纹理高度相似的干扰。烟头在1080p原图里经常只有15×20像素同时伴随大量小目标漏检。夜间就更麻烦如果数据集里没有黑夜样本模型会把路灯、烟头光和反光混在一起。这个差异不是玄学它直接决定了推理时confidence阈值要放在哪一格室内可以放到0.5室外经常得降到0.25以下但阈值一低误报也会跟着涨。拿到数据集判断它好不好用先按室内、室外把图片各抽几十张出来肉眼过一遍。重点看三点背景是否足够多样室外烟雾是否有真实形态烟头小目标占多大比例。很多号称室内室外混合的数据集实际室外图是同一两个工地的连拍相当于只有一个场景泛化会出问题。2.2 读懂VOC格式标签用一段脚本统计XML里的类别与目标尺寸分布VOC格式标签的标准载体是Annotations目录下的XML文件每个XML对应一张JPEGImages里的jpg。XML里值得关注的字段是filename、size、object和object下的name、truncated、difficult、bndbox。filename有时和实际文件名不一致size写的是原图宽高object可以重复多个。其中difficult等于1表示这个目标本身很难判定很多新手在转换时把它当成普通目标后面会单独说这个坑。在跑训练之前我先做一个非常便宜的体检扫描所有XML统计类别数量以及目标框相对原图的面积分布。下面这段脚本可以直接扔到数据集根目录执行。import os import xml.etree.ElementTree as ET from collections import Counter annotations_dir Annotations class_counter Counter() box_size_stats {small: 0, medium: 0, large: 0} total_objects 0 for xml_name in os.listdir(annotations_dir): if not xml_name.endswith(.xml): continue tree ET.parse(os.path.join(annotations_dir, xml_name)) root tree.getroot() try: width int(root.find(size/width).text) height int(root.find(size/height).text) except AttributeError: print(f{xml_name} 缺少size字段请检查该标注文件) continue image_area width * height for obj in root.findall(object): class_name obj.find(name).text class_counter[class_name] 1 total_objects 1 difficult int(obj.find(difficult).text) if difficult 1: print(fdifficult样本: {xml_name} - {class_name}) bndbox obj.find(bndbox) xmin int(bndbox.find(xmin).text) ymin int(bndbox.find(ymin).text) xmax int(bndbox.find(xmax).text) ymax int(bndbox.find(ymax).text) box_area (xmax - xmin) * (ymax - ymin) ratio box_area / image_area if ratio 0.01: box_size_stats[small] 1 elif ratio 0.2: box_size_stats[medium] 1 else: box_size_stats[large] 1 print(类别分布:, dict(class_counter)) print(目标尺寸分布:, box_size_stats) print(总标注目标数:, total_objects)逻辑说明脚本按xml文件遍历读取每张图的宽高再遍历所有object统计类别和目标面积。small、medium、large的划分参照COCO关于目标尺寸的相对比例经验把小于原图面积1%的框判为小目标。difficult样本单独打印出来方便转换前统一处理。参数说明1%和20%这两个比例阈值可以按任务调整。如果整个数据集都是小目标比如烟头small占比可能超过70%这时要重点关注增强策略而不是纠结阈值设置。如果你的XML里name不是smoking而是smoke或person说明这份数据标注了多类class_id映射时要看清楚。2.3 量级判断单类别目标检测数据集1000多张处于什么水平先说结论1000多张对单类检测来说是一个够做技术验证、不够直接生产的量级。假设每张图有1到3个吸烟目标那么整个数据集的有效标注框大约是1000到3000个。这个规模可以把yolov8n训练到在训练分布上很好看的mAP但真实场景的覆盖度才是决定能否部署的关键。网上搜“抽烟数据集下载”时拿到手很多就是标准PASCAL VOC结构也就是Annotations、JPEGImages和ImageSets/Main。下载解压后先看ImageSets/Main里有没有现成的train.txt和val.txt。有的作者给了划分有的只给trainval甚至划分是按文件名随机数打散的没有按场景做分组这会在后面第4章变成大坑。正式做之前我习惯把所有图片按场景前缀重新归一下类统计每个场景的张数。从生产角度倒推一个可用的吸烟检测模型至少要覆盖十来个场景每个场景几十张正样本起步再加上逆光、夜间、远距离、遮挡这些困难case。当前数据集的室内室外混合看起来丰富但往往室内集中在中景、室外集中在一两个固定摄像头视角。这种结构对算法团队验证流程够用对交付现场则要走一段扩充数据的老路。3. 把VOC转成YOLO格式并跑通第一轮训练脚本、换算与参数清单3.1 坐标换算从(xmin,ymin,xmax,ymax)到归一化的中心点YOLO系训练不吃VOC的XML它要的是和图片同名的txt每一行是class_id x_center y_center w h全部做了归一化。VOC的bndbox记录的是矩形左上角和右下角的绝对像素坐标。转换公式不复杂x_center等于(xmin加xmax)除以2再除以原图宽度y_center同理宽度等于(xmax减xmin)除以原图宽度高度等于(ymax减ymin)除以原图高度。这里容易忽略的是坐标类型。OpenCV这种把图片读成数组的库坐标是整数像素但做检测训练时如果XML里的坐标带浮点直接int取整可能丢掉几像素。吸烟检测的目标像素很小一个烟头可能只有15个像素宽int(15.6)变成15影响不大但如果上游标注工具生成了浮点坐标建议保留到小数而不是直接截断。另外归一化后要clip到0到1之间防止标注越界把loss拉爆。3.2 转换脚本与运行方式XML转txt的完整实现下面这段脚本负责三件事读xml跳过difficult1输出同名txt。我的习惯是转换后马上抽查几个文件用txt画框到图上肉眼确认没有读写错位再进训练。import os import xml.etree.ElementTree as ET classes [smoking] # 必须和后续训练yaml里的names保持一致 def convert_xml_to_yolo(xml_path, out_dir): tree ET.parse(xml_path) root tree.getroot() width int(root.find(size/width).text) height int(root.find(size/height).text) img_name root.find(filename).text txt_name os.path.splitext(img_name)[0] .txt lines [] for obj in root.findall(object): name obj.find(name).text if name not in classes: continue difficult int(obj.find(difficult).text) if difficult 1: continue class_id classes.index(name) bndbox obj.find(bndbox) xmin int(bndbox.find(xmin).text) ymin int(bndbox.find(ymin).text) xmax int(bndbox.find(xmax).text) ymax int(bndbox.find(ymax).text) x_center (xmin xmax) / 2.0 / width y_center (ymin ymax) / 2.0 / height w (xmax - xmin) / width h (ymax - ymin) / height x_center min(max(x_center, 0.0), 1.0) y_center min(max(y_center, 0.0), 1.0) w min(max(w, 0.0), 1.0) h min(max(h, 0.0), 1.0) lines.append(f{class_id} {x_center:.6f} {y_center:.6f} {w:.6f} {h:.6f}) with open(os.path.join(out_dir, txt_name), w) as f: f.write(\n.join(lines)) # 用法示例指定Annotations目录和输出labels目录 xml_root /path/to/dataset/Annotations yolo_root /path/to/dataset/labels os.makedirs(yolo_root, exist_okTrue) for xml_file in os.listdir(xml_root): if xml_file.endswith(.xml): convert_xml_to_yolo(os.path.join(xml_root, xml_file), yolo_root) print(f处理完成: {xml_file})逻辑说明classes列表的顺序决定了类别编号后续data.yaml里的names必须和它保持一致。脚本里对difficult等于1的目标直接跳过是因为这类目标往往被遮挡严重或边界极难判断硬喂给模型等于不断给随机梯度。坐标做了clip保护避免个别坏标注让中心点跑到图片外。参数说明如果XML里的filename带路径os.path.splitext会得到带路径的文件名txt会写到错误的位置建议先打印img_name看一眼必要时换成os.path.basename。classes列表里如果把smoking排到第二位class_id会变成1转换时要注意。对多类数据集把[smoking]替换成实际类别列表即可。3.3 训练配置用yolov8训练自己的数据集时的四个必调参数转换完成之后下一步是整理目录结构并写data.yaml。常见做法是images/train、images/val、labels/train、labels/val四个目录。如果你只有一部分图片和标签先按后面第4章的划分逻辑把文件列表切好再复制或软链到对应目录。# smoking.yaml path: /path/to/smoking-dataset train: images/train val: images/val names: 0: smoking训练命令用ultralytics的CLI即可关键在于参数选择而不是命令本身。下面是一条我常用的起点命令yolo detect train datasmoking.yaml modelyolov8n.pt \ epochs150 imgsz640 batch16 patience20逻辑说明model选择yolov8n.pt而不是随机初始化原因是用COCO上训练好的权重做迁移学习对新任务收敛快很多尤其是对person检测能力可以迁移过来。epochs给到150配合patience20的早停单类小数据集一般50到80轮就稳定跑不完会自动停。imgsz默认640如果你的显卡允许建议在场景确认之后试试960室外小目标会有明显收益。参数说明batch要按显存调不能盲目拉高。16G显存大概可以跑batch1624G可以到32batch太小会导致BN统计不稳定。学习率lr0默认0.01小数据集如果loss震荡改成0.005再试。如果你更习惯用yolov5训练自己的数据集data.yaml的写法基本一致只是训练命令换成python train.py --data smoking.yaml --weights yolov5s.pt --img 640 --batch 16 --epochs 150区别点是v5的默认锚框处理和一些超参在data/hyps目录里按默认跑问题不大。4. 模型从“能跑”调到“能用”按场景划分数据、增强顺序与迁移学习4.1 数据划分按场景分组而不是按文件随机打散划分数据时最常见的错误是直接把所有文件名放进random.shuffle里然后前80%训练后20%验证。吸烟检测数据通常是同一个摄像头连续拍的同一批图在光照、背景、人物姿态上高度相似。如果同一场景的几十帧被同时分进训练集和验证集验证集mAP会虚高部署到新场景立刻掉点。这个现象在行业里叫“场景泄漏”处理不好再调模型结构也没用。按场景划分的基础是给每张图算出场景组号。常见做法是靠文件名前缀比如scene01_frame_001.jpg前缀scene01就是一个组。也可以用拍摄时间和地点信息。下面脚本用GroupShuffleSplit按组划分保证同一场景不会跨集合。import os from sklearn.model_selection import GroupShuffleSplit image_dir JPEGImages files [f for f in os.listdir(image_dir) if f.endswith(.jpg)] files.sort() # 场景组号这里取文件名第一段按你的实际命名规则改 groups [f.split(_)[0] for f in files] splitter GroupShuffleSplit(n_splits1, test_size0.2, random_state42) train_idx, val_idx next(splitter.split(files, groupsgroups)) train_files [files[i] for i in train_idx] val_files [files[i] for i in val_idx] print(f训练集 {len(train_files)} 张验证集 {len(val_files)} 张) # 生成文件列表方便后面直接喂给yolo with open(train.txt, w) as f: f.write(\n.join(os.path.join(image_dir, x) for x in train_files)) with open(val.txt, w) as f: f.write(\n.join(os.path.join(image_dir, x) for x in val_files))逻辑说明GroupShuffleSplit接受files和groups两个数组按组为单位随机分配同一组不会被拆分到两个集合。test_size0.2表示验证集占20%。random_state固定后每次复现结果一致方便对比不同训练配置。参数说明如果文件名没有前缀分组可以用父目录名做group比如把images按场景子目录组织groups改成os.path.dirname。这里只产生文件列表真正的图片是否复制到images/train和images/val由你决定用软链可以省磁盘。4.2 数据增强顺序马赛克、HSV和复制粘贴对小目标的取舍1000多张的单类检测不做增强就是裸奔。可增强也不是开得越猛越好。ultralytics默认的mosaic把四张图拼成一张对小目标检测是好事也是坏事目标数量变多但烟头本来就只有十几个像素拼完之后可能缩到三四个像素连标注框本身都不可靠。所以我会把mosaic的概率从默认的1.0降到0.5左右或者在前50轮开启mosaic、后50轮关闭让模型后期在干净的真实比例图上稳定收敛。室外场景对颜色增强比较敏感。烟雾的颜色容易受环境光影响白天发白、黄昏发黄、夜间发蓝如果只用原图训练模型会把颜色当成强特征换一个光照就漏检。我会把HSV增强里的hsv_h、hsv_s、hsv_v稍微往大里调比如hsv_h0.02、hsv_s0.8、hsv_v0.5让颜色空间有更多扰动。翻转用默认的fliplr0.5就够了flipud一般不开因为监控场景里目标不会倒过来。对烟头这种小目标复制粘贴增强比mosaic更有效。做法是把小尺寸的烟头框从原图画面上裁下来随机贴到其他背景图上同时把粘贴位置写进标签。这样既增加了小目标数量又避免了缩放损失。注意粘贴时不能破坏语义比如不要把一个烟头贴到纯黑区域然后让标注框悬空模型会学到环境关联。实现上可以先用脚本人工选几张高置信度的烟头图块生成一批增强样本后再跑训练。4.3 迁移学习用预训练权重起步的冻结策略与学习率边界用yolov8n.pt这类COCO预训练权重起步相当于让模型先认识“人”这个轮廓再学“人手中的烟”。吸烟检测的目标依赖人体上下文烟头通常在手附近烟雾有时在头部高度预训练特征能帮大忙。随机初始化从头训练1000多张很容易过拟合验证集上precision和recall两头不讨好。具体到ultralytics可以通过freeze参数冻结backbone前几层。比如freeze10表示冻结前10层适合数据量少、不想让底层特征被大幅度破坏的情况。对于1000多张的单类小数据我会从freeze10开始如果训练后val mAP上不去再减少冻结层数让模型有更多自由度。冻结层数太多会有反效果整个模型变得僵硬迁移过来的COCO特征和你的烟雾特征对不上。学习率边界要跟着数据量走。lr0默认0.01对大数据集合理对1000多张明显偏高我一般直接改成0.005。观察前20轮的loss曲线如果loss剧烈震荡或直接炸掉降到0.001如果loss平滑但收敛慢可以不动让早停机制自己判断。单类任务还不要随意改输出层结构类数变了模型输出层就是1类yaml里names写对就行权重会自动适配。5. 吸烟检测数据集落地常见的5个坑从标签错位到夜间场景翻车5.1 坑1转换时把difficult1的样本当成普通标签模型在困难样本上摇摆现象训练到一半loss不再下降验证集mAP像锯齿一样抖动把输出图翻出来看模型对同一个目标有时框住有时漏掉。原因difficult1表示这份标注连人工都觉得难判定。常见是目标被严重遮挡或者烟雾淡到接近透明。转换脚本如果把这些框原样写进txt等于给模型强行灌输不确定的监督信号梯度方向互相矛盾。解决在转换时统一跳过difficult1评估阶段另说。如果你发现这类样本很多说明数据集的标注质量本来就一般建议肉眼抽查50个difficult样本能补标就补标不能补标就不要进训练。5.2 坑2随机划分导致同一场景同时出现在训练集和验证集指标虚高现象训练日志里val mAP一直很漂亮甚至0.9以上但把模型拿到另一个摄像头画面里一测漏检多到没法看。原因同一个场景连续帧的相似度太高随机划分时训练集和验证集同时包含同一场景的帧。模型实际上是记住了这个场景的背景而不是真正学到了烟的特征背景一变就失效。解决按场景分组划分用前面4.1的GroupShuffleSplit。划分完成后打印两边的场景列表人工检查有没有重叠。这一步多花十分钟能省后面部署时的排查时间。5.3 坑3烟雾目标的边界模糊标注不一致让Loss迟迟不收敛现象训练正常启动但loss在某个水平上来回抖降不下去验证集recall还行precision很差框的位置忽大忽小。原因烟雾没有清晰的边界不同标注者对“哪里算烟、哪里不算烟”的理解不一致。有人框整个烟柱有人只框烟头周围最浓的一小片同一张图在不同标注员手里产生了互相矛盾的标签。解决写一个标注规范直接贴在团队里烟头必须完整包进框烟雾只框浓度最高的核心区域不追扩散的透明边缘。如果数据集是第三方标好的先把巨大框和极小框过滤出来比如框宽高比大于10的、面积小于全图0.1%的逐一复核。5.4 坑4室内样本多、室外样本少模型在室外误报偏高现象在室内测试准确率还能看一到室外就把树叶阴影、车辆尾气、水蒸气当烟雾误报数量明显升高。原因数据集声称的“室内室外混合”在内部并不平衡室内采集成本低样本多室外往往只有两三个工地视角模型没见过真实的室外复杂背景只能靠室内学到的颜色特征硬猜。解决先按场景维度统计样本量室外不足就用重采样把室外场景的epoch权重调大或者用复制粘贴增强给室外补小目标。如果室外样本实在太少更现实的办法是单独补采一段现场视频按5到10帧间隔抽帧手动过滤没有吸烟的片段后补进数据集。5.5 坑5目标太小resize到640后烟头只剩几个像素漏检加重现象验证集上大目标都能框住但远距离的烟头全部漏检放大输出图发现烟头在输入图上只有七八个像素。原因1080p原图被缩放到640输入烟头这种小目标在缩放中丢失了大量纹理模型拿不到有效信息训练时增强又进一步缩小目标问题被放大。解决第一选择是提高推理分辨率imgsz从640调成960或1280显存不够就用切片推理SAHI对原图分块检测再把检测结果映射回原图坐标。第二选择是在训练端用复制粘贴增强增加小目标数量让模型至少见过足够多的“小烟头”样本。第三选择是检查标注框是不是太小比如小于10×10像素的框要么补清晰大图要么直接删除别硬练。6. 把验证做扎实交叉验证、失败样本回放与一个划算的数据扩充技巧数据量小的时候单次划分的mAP说明不了太多问题。我的做法是跑五折交叉验证把1000多张分成五份轮流拿一份当验证集其余训练最后看五次的mAP均值和方差。均值代表模型稳定性方差过大说明划分或场景分布有问题。五折训练成本在yolov8n上不高却能让后面所有的配置对比都有意义。只看mAP还不够部署前要做失败样本回放。用训练好的模型在验证集上推理置信度低于某个阈值的检测结果单独存到一个目录人工一页一页翻。命令很简单yolo predict modelbest.pt sourceval_images/ conf0.25 save_txtTrue save_confTrue逻辑说明save_txt会把每个框的类别、坐标和置信度写到txt你就可以写个小脚本把置信度低于0.4的检测图挑出来。挑出来的图不需要去统计谁对谁错而是找模式是夜间全漏还是远距离全漏还是某个背景误报。找到模式就等于找到了扩充方向比盲目加数据有效得多。如果确认某个场景确实缺样本一个划算的扩充技巧是从监控视频抽帧。把包含吸烟行为的片段按每5帧抽一张自动带上时间戳命名先放下高精度标注工具用现有模型做半自动预标注人工只改框不重新画。这样1000多张的数据集补到两三千张成本比重新标低一半。我自己在这类小数据集上吃过不少亏。有一回把测试集mAP刷到0.92准备上线结果现场夜间烟雾全漏。把夜间错检图拉出来看才发现整个数据集根本没有一张夜间照片不是因为模型不行是因为数据分布本身就缺了一块。从那以后每轮训练结束我都先看错检图再谈指标。这个习惯帮我在后面好几个项目里少走了弯路希望帮到你。本文还有配套的精品资源点击获取