吸烟检测数据集制作与YOLOv8训练实战:格式转换与避坑指南 简介面向AI与机器学习算法工程师及学习者的吸烟检测数据集带完整标注专门用于训练吸烟行为识别模型可应用于公共场所监控预警、行为分析等视觉任务也适合作为模型调优与科研实验的基准数据。压缩包共1567个文件其中783张JPG图像与783个XML标注文件一一对应另有1个TXT说明文档整体大小31.71MB轻量易传输便于快速获取高质量监督数据。目前已有161人学习使用适用于目标检测、图像分类等监督学习场景也可用于算法训练、模型对比与科研验证。标注信息规范XML中记录目标类别与位置框可方便地转换为Pascal VOC、YOLO等数据格式开发者可自行划分训练/验证/测试集快速评估模型效果减少数据预处理成本。样本文件命名统一便于批量读取与扩展可高效搭建吸烟行为识别应用。1. 做吸烟检测先盘清这份带标注的吸烟数据集它解决什么、适合谁做吸烟检测这类视觉项目第一步不是调模型而是拿到一份带标注的吸烟数据集。摄像头采集几万张原图很容易但每一根烟头、每一处嘴部火光都要有人工框出来这个成本多数团队扛不住所以一份现成、格式干净、类别定义清楚的标注数据直接决定你两天还是两周能跑通第一版模型。这篇文章面向算法工程师、做安防合规检测的开发者以及拿目标检测数据集练手的学生从数据格式、转换脚本、训练参数到常见坑一条线走完。先说个反直觉的结论很多吸烟检测项目不是死在模型精度而是死在标注数据本身——类别编号错一位、坐标没归一化、正负样本失衡哪一个都能让训练过程表面正常、实际彻底翻车。2. 一份带标注的吸烟数据集长什么样目录结构、标注格式与类别编号2.1 VOC 与 YOLO 两种标注格式的取舍拿到手的吸烟数据集常见有两种标注格式。一种是 Pascal VOC 的 XML 格式每个图像文件对应一个同名 XML框的坐标是像素绝对值另一种是 YOLO 格式的 TXT 文件每一行是class_id center_x center_y width height值都归一化到 01 之间。两种格式各有各的适用场景VOC 格式可读性好用标注工具打出来默认就是它方便人工核查YOLO 格式则被 YOLOv5、YOLOv8 等训练管线直接消费省去训练时的解析步骤。实际项目里我更建议你拿到数据后先确认一件事这份数据集里的类别到底有几个。最简化的吸烟数据集通常只定义一个类别框住烟头或香烟本体类别名叫smoke或cigarette做得细一点的数据集会区分smoke、hand、mouth甚至把打火机也算一类。类别定义越细模型能学到的上下文越多但人工标注成本也越高。如果你拿到的数据集加了人名目录或者按时间段分目录别急着删训练时保持目录结构往往比打平所有文件更不容易出错。标注格式决定不了模型上限但决定了你的预处理代码怎么写。先给出一份目录结构的最常见形态smoke_dataset/ ├── images/ │ ├── img_0001.jpg │ ├── img_0002.jpg │ └── ... ├── annotations/ │ ├── img_0001.xml │ ├── img_0002.xml │ └── ... └── class_names.txtclass_names.txt里通常只有一行smoke。如果数据集给的是 YOLO 格式则不是annotations目录装 XML而是每个图片同级的labels目录装 TXT后面转格式那一步就可以省掉。判断一份标注数据能不能直接用于训练看三个点类别编号从 0 开始还是从 1 开始、坐标有没有归一化、标注框有没有超出图像边界。这三个点也恰好是后续最容易埋雷的地方。2.2 先写个脚本盘点数据集统计类别、框数与目标尺寸拿到数据集先别急着转格式用一段脚本把家底盘清楚。目标很简单统计每个类别的框数量、框的宽高分布、以及有没有空标注文件。分类别统计尤其重要——如果smoke类只有 200 个框另外 3000 张图是背景负样本训练出来的模型就会疯狂误检。import os from xml.etree import ElementTree as ET xml_dir annotations class_counter {} size_list [] for xml_file in os.listdir(xml_dir): if not xml_file.endswith(.xml): continue tree ET.parse(os.path.join(xml_dir, xml_file)) root tree.getroot() # 读取图像真实宽高VOC 标注里一般都有 size 字段 img_w int(root.find(size/width).text) img_h int(root.find(size/height).text) for obj in root.iter(object): cls obj.find(name).text class_counter[cls] class_counter.get(cls, 0) 1 bnd obj.find(bndbox) xmin float(bnd.find(xmin).text) ymin float(bnd.find(ymin).text) xmax float(bnd.find(xmax).text) ymax float(bnd.find(ymax).text) # 用像素宽高记录目标尺寸分布 w xmax - xmin h ymax - ymin size_list.append((w, h, img_w, img_h)) print(类别统计:, class_counter) print(样本数:, len(os.listdir(xml_dir))) # 打印最小框面积用来判断是否存在小目标问题 min_size min(size_list, keylambda s: s[0] * s[1]) print(f最小框: {min_size[0]:.1f} x {min_size[1]:.1f} 像素)这段脚本的逻辑是逐个解析 XML提取图像宽高和每个目标的类别与坐标最后汇总统计。参数说明里有一个值得注意的点我特意从 XML 的size字段读取图像宽高而不是假设图片文件存在。很多标注数据集在搬运过程中会丢图只留下标注文件等你转 YOLO 格式时才发现取不到分辨率坐标归一化直接卡死。另外size_list里同时记录了图像宽高这是为了后面排查比例异常——如果某个框的宽度超过图像宽度说明标注工具导出时出了问题这类样本要尽早剔除不要指望训练时的数据增强帮你纠正。空标注文件也是盘点重点。吸烟场景里难免有「人入镜但没吸烟」的图正规数据集会保留这些图并给一个空标签文件。处理负样本的正确方式后面避坑章再展开这里只需要统计出来annotations目录下文件数比images少或者大量 XML 里没有object节点都会直接影响训练时的采样逻辑。3. 把吸烟数据转成 YOLO 训练格式XML 转 TXT 脚本与坐标归一化3.1 转换脚本的完整实现与参数说明YOLOv8 训练时读的是标签 TXT每行格式固定为class_id cx cy w h且全部是相对图像的归一化值。把 VOC 的 XML 转成这个格式核心就三步读 XML、读图片尺寸、按公式归一化。下面这段脚本我按最直接的路径来写没有依赖额外的标注库只用标准库就能跑。import os from xml.etree import ElementTree as ET class_mapping {smoke: 0, hand: 1, mouth: 2} def xml_to_yolo(xml_path, img_w, img_h, out_path): tree ET.parse(xml_path) root tree.getroot() with open(out_path, w) as f: for obj in root.iter(object): cls_name obj.find(name).text if cls_name not in class_mapping: continue bnd obj.find(bndbox) xmin float(bnd.find(xmin).text) ymin float(bnd.find(ymin).text) xmax float(bnd.find(xmax).text) ymax float(bnd.find(ymax).text) # 计算中心点与宽高后做归一化 x_center (xmin xmax) / 2.0 / img_w y_center (ymin ymax) / 2.0 / img_h w (xmax - xmin) / img_w h (ymax - ymin) / img_h # 边界截断防止标注越界导致训练报错 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) cls_id class_mapping[cls_name] f.write(f{cls_id} {x_center:.6f} {y_center:.6f} {w:.6f} {h:.6f}\n) xml_dir annotations img_dir images label_dir labels os.makedirs(label_dir, exist_okTrue) for xml_file in os.listdir(xml_dir): if not xml_file.endswith(.xml): continue stem os.path.splitext(xml_file)[0] img_path os.path.join(img_dir, stem .jpg) # 用图像文件本身读取宽高而不是依赖 XML 的 size 字段 from PIL import Image img Image.open(img_path) img_w, img_h img.size xml_to_yolo(os.path.join(xml_dir, xml_file), img_w, img_h, os.path.join(label_dir, stem .txt))这里有两个参数点要说明清楚。第一class_mapping字典里的编号决定了模型输出的类别索引我写成smoke0, hand1, mouth2只是常见做法如果你的数据集只有smoke一类字典就只留{smoke: 0}后续训练 YAML 的names也得是[smoke]两者必须对齐。第二我用 PIL 读取真实图像尺寸来归一化备用方案是从 XML 的size节点取但遇到 XML 尺寸和实际图像不一致的情况前者更可靠。归一化后做了 01 截断这是防止个别框画出边界后给训练器喂负数的保底操作代价是不再能反映标注原始状态所以截断前最好先打印一条警告日志后面排查时能对上号。3.2 划分训练集与验证集文件列表的生成逻辑转换完成后下一步是生成训练集和验证集的划分文件。这里要强调一点划分的是图像路径列表不是把文件复制一份。YOLOv8 用data.yaml里指定的train和val路径读图你可以直接指向两个.txt文件每个 txt 一行一个图像绝对路径或相对路径。我习惯把它跟转换脚本串成一条流水线避免人工复制贴错。import os import random image_dir images train_ratio 0.8 seed 42 images sorted(os.listdir(image_dir)) random.seed(seed) random.shuffle(images) split_idx int(len(images) * train_ratio) train_list images[:split_idx] val_list images[split_idx:] with open(train.txt, w) as f: for name in train_list: f.write(os.path.abspath(os.path.join(image_dir, name)) \n) with open(val.txt, w) as f: for name in val_list: f.write(os.path.abspath(os.path.join(image_dir, name)) \n) print(f训练样本数: {len(train_list)}) print(f验证样本数: {len(val_list)})这段脚本里有两个习惯值得借鉴。第一个是sorted排序之后再random.shuffle而不是os.listdir之后直接 shuffle保证每次运行时文件顺序一致配合固定seed可以让两次训练的实验对比站在同一起跑线。第二个是划分只针对图像文件名因为标签文件与图像同名训练时 YOLO 会自动在labels目录里找同名 TXT所以不必在这里显式处理标签。如果你想让验证集更贴近真实场景可以把划分粒度从「整个数据集」改成「按目录/时间段分组」比如同一摄像头同一天采集的图放进同一组防止过度乐观的评估分数——这一点我会在避坑章里单独展开。4. 用 YOLOv8 训练吸烟目标检测数据集数据配置、参数表与训练命令4.1 数据 YAML 怎么写路径、类别名与类别数转换脚本跑完你需要一份 YAML 文件告诉 YOLOv8 去哪找图、类别叫什么。这是最容易写错的一步因为 YAML 里的names顺序必须和转换脚本里的class_mapping完全一致。类别名顺序错了模型训练不会报错但预测结果的类别含义全乱。path: /home/user/smoke_dataset train: train.txt val: val.txt names: 0: smoke 1: hand 2: mouthpath是数据集的根目录train和val可以写相对path的路径也可以写包含图像路径列表的.txt文件名YOLOv8 两者都认。names的索引正是训练时标签的第一列编号顺序错位会导致训练指标正常但实际推理张冠李戴。如果你的数据集只有smoke一类names就写成0: smoke单行不要保留其它类别名类别数错了对模型容量的影响比多数人想的更明显。4.2 训练命令与 5 个关键参数单卡还是多卡显存不够怎么办数据配置就绪后训练命令本身不长但几个参数直接影响能否收敛以及小目标漏检程度。下面是一份吸烟检测的实用起点yolo train modelyolov8m.pt datasmoke.yaml \ epochs120 imgsz1280 batch8 device0 \ patience25 optimizerAdamW lr00.001 \ close_mosaic10参数选择逻辑拆开讲。model用yolov8m.pt作为预训练起点不用从头训练吸烟检测的烟头往往只有十几像素模型下采样到 80×80 特征层时如果原图是小目标信息会丢得比较多所以用 m 型号而不是 n 型号做平衡。imgsz1280是这组参数里最值得斟酌的一项——把你的训练图从默认 640 提到 1280小目标漏检率通常能下降三分之一以上代价是显存占用翻倍显存不够就把batch从 8 降到 4这是比换小模型更优先的调整。下表是我在吸烟数据上常用的参数区间照着设不会出错但要根据自己的显卡量级微调。参数建议值说明epochs100~150数据集小就拉高配patience早停防止过拟合imgsz640 起步1280 提精度小目标明显时用 1280注意显存和训练时间batch4/8/16按显存选爆显存先降 batch不要动 imgszdevice0或mps单卡写 0没有 CUDA 的 Mac 写mpspatience20~30连续多个 epoch 验证集指标不涨就提前停止close_mosaic8~12最后若干 epoch 关闭 mosaic让模型适应单图分布close_mosaic这个参数多数人第一次容易忽略。Mosaic 增强把小图拼成大图对烟头这种小目标其实不友好训练最后阶段关掉它让模型在接近真实分布的输入上做精调mAP 往往能再涨一截。训练结束后看runs/detect/train目录下的results.png框的损失曲线和验证集 mAP 曲线是判断收敛状态的第一手材料。5. 吸烟数据集训练的 5 个常见坑从标注错位到样本不均衡5.1 类别编号错位模型不报错但全部漏检现象训练过程 loss 曲线正常下降验证集 mAP 也说得过去但把模型拉到实际场景里测框全打在背景上或者把打火机当成烟头类别完全对不上。原因XML 里类别名转换时字典映射顺序和训练 YAML 不一致比如 XML 里类别顺序是hand, smoke转换脚本却按smoke0, hand1写死导致所有标签整体错位。解决训练前用一行命令抽样检查标签内容awk {print $1} labels/*.txt | sort | uniq -c如果smoke类在标签里出现的编号不是 0说明映射写错了。这类问题不会报错全靠这种统计去暴露所以我会把这段校验写进转换脚本的固定流程里。5.2 黑背景烟头与暗光场景检测框乱飘现象模型在白天场景 mAP 接近 90%一拍卫生间、走廊、工地夜班这类暗光场景误检率飙到没法看经常把屏幕反光、烟灰缸、甚至墙面污渍识别成烟头。原因数据集里白天的亮背景图占了绝大多数模型学到的其实是「高亮度和高对比的小物体区域」这一特征夜间的烟头虽然也是亮的但周围环境特征跟白天完全不同。解决数据层面补充夜间图和暗光样本如果数据集本身没有就用增强把过半白天图做一次 gamma 压低和对比度拉伸模拟夜间响应训练层面在data.yaml同目录建个增强配置把hsv_h、hsv_s适当调高。这一步做完夜间的误检往往能恢复一大半剩下的要靠 5.3 的上下文信息来解决。5.3 小目标漏检只标烟不标手和嘴模型学不到上下文现象单张图里烟头就 10×10 像素模型在训练时把这类小框当成噪声忽略验证集上小目标一类的 AP 只有 20% 出头实际场景里拿摄像头拍三米外的人基本漏检。原因标注风格只框烟头本体目标过小导致有效特征太少尤其经过下采样之后只剩几个像素。解决分两步走第一步是回到标注层面把烟头立即附近的嘴部或手部区域纳入同一个框让模型学习「手在嘴边」这个上下文结构第二步是训练层面把imgsz拉高到 1280同时用yolov8m.pt而不是 n 型号——小目标需求对模型容量和输入分辨率都更敏感。这是我最推荐优先调的两个参数因为它们对精度的提升是结构性的不是碰运气的玄学。5.4 标注与图像对不上文件名乱序导致验证集「作弊」现象训练 loss 收敛得很好验证集 mAP 也很高一上真实环境就崩。检查验证集图片才发现训练集和验证集里出现了同一摄像头同一时段的连拍帧模型在验证集上「背题」了。原因划分训练集时没有按采集场景或时间段分组随机打散之后同一批连续帧被分到了两边。解决给划分脚本增加分组逻辑——如果数据集有按摄像头目录组织的结构就以目录为单位做划分而不是以单张图为单位。我再加一条兜底操作用图像的感知哈希给所有图做去重把重复帧从验证集里删掉。这条不解决模型能力问题但能让你的评估指标可信后续调参才有依据。5.5 正负样本不均衡负样本是背景不是空白标签现象模型训练完误检极多尤其是空旷场景下经常对着墙角、柱子出框。打开数据统计发现正样本图占了 95%负样本图只有零头。原因吸烟检测的目标是「人吸烟」这个动作而识别的基础是「烟」这个物体——模型如果没有见过足够多的无烟干净背景就会把任何带高亮小物体的区域往外报。解决训练数据里加背景负样本常见比例是三到四张正样本配一张纯背景图。负样本的标签文件是空文件YOLO 会把它当作no object参与损失计算。有一种常见误用是删掉这些空标签文件只保留正样本图结果训练时模型没有任何反驳信号误检一封一个准。我一般会额外引入一些夜间环境的无烟背景图把误检率再往下压一档。6. 模型训完先别急着上线抽验可视化、指标筛选与光照适配6.1 一行脚本批量抽验预测结果训练结束别先看 mAP 数字先抽一批图上画框看实际效果。把预测框和真实框叠在同一张图上用肉眼就能发现 mAP 读不出来的问题比如同一根烟头被反复框了三次或者算法把烟灰缸的圆形轮廓当成烟头。from ultralytics import YOLO import cv2 model YOLO(runs/detect/train/weights/best.pt) for img_path in [val_samples/img_0102.jpg, val_samples/img_0157.jpg]: img cv2.imread(img_path) results model(img_path, conf0.25, imgsz1280) for r in results: for box in r.boxes: x1, y1, x2, y2 map(int, box.xyxy[0]) cls int(box.cls[0]) conf float(box.conf[0]) cv2.rectangle(img, (x1, y1), (x2, y2), (0, 255, 0), 2) cv2.putText(img, f{model.names[cls]} {conf:.2f}, (x1, y1 - 5), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 255, 0), 1) cv2.imwrite(check_ img_path.split(/)[-1], img)这段脚本的conf0.25是部署时一个比较中性的阈值起点实际项目里我会专门跑一段阈值扫描来定而不是拍脑袋。抽验的图建议覆盖三个维度白天的室外、黄昏的逆光、夜间的暗光每种至少五张。如果某个场景的框大面积错位说明训练数据在那类光照条件下有缺失需要回到 5.2 的增强策略补数据而不是继续调阈值。6.2 按置信度区间和场景切分评估抽验做完了再用一段脚本把验证集预测 JSON 落盘按置信度区间统计误检和漏检。做法是把model.predict(..., save_jsonTrue)的输出收集起来把真实标签也加载进来按conf分桶统计每一档的命中数。这一步能找到那些「置信度 0.8 却完全框错位置」的难例——这类样本 mAP 不会体现惩罚但上线后最让人头疼。切分维度用场景标注字段来做前提是数据集目录名或文件名里带了场景信息我在 5.4 里强调过保留目录结构就是为了让这一类分析跑得通。6.3 光照适配的最后一个技巧Hue 与亮度增强单独调夜间吸烟检测还有一个实战技巧训练时不要只调整体亮度增强要对hsv_h和hsv_s分开设置。烟头火光在不同色温下会从橙黄偏向橙红统一调hsv_h容易把整个画面色调带偏我习惯设置hsv_h0.02、hsv_s0.5配合 gamma 增强处理暗部再在augment参数里把mosaic0.5调低减少小目标在拼接中被截断的概率。这个组合不能替代真实夜间数据但能在没有夜拍资源的情况下先把误检压住。我自己的教训是曾经只看 mAP 就着急部署结果夜里大量误报把客户惹火了之后每次训完都先花半小时做光照场景抽验再做下一步。这些功夫不花在模型上而花在数据和评估体系上长期看才是节约时间的地方希望帮到你。本文还有配套的精品资源点击获取