抽烟检测数据集构建实战:格式转换、标注规范与质量校验
简介面向深度学习、机器学习目标检测任务聚焦公共场所与安防场景中的吸烟行为识别是一份可直接用于模型训练与算法验证的标注数据集。图片来自真实抽烟场景包含不同角度、远近与光照条件下的样本适合作为YOLO、SSD等检测网络训练数据也可用于毕业设计、竞赛实践或工业级抽烟识别系统的数据准备。包内按Annotations、Imagenet、JPEGImages三个目录组织原图与XML标注一一对应文件总数约2000个以jpg图片与xml标注文件为主另含少量txt划分清单、Python转换脚本和说明文档压缩包约298MB整体便于按目录快速取用。配合格式转换脚本可快速将VOC标注转为YOLO训练所需格式省去人工整理与清洗时间。已有7526人学习下载适合目标检测入门及需要现成监督数据的开发者。1. 抽烟检测数据集一张标注图片背后看不见的工程拿到一个「抽烟检测数据集」的压缩包很多人第一反应是解压、看图片、数一数多少个文件夹然后直接扔给训练脚本跑一晚上。实际上做过一次完整落地的人都知道一个能用的抽烟检测数据集价值不取决于图片张数而取决于标注是谁做的、怎么做出来的、以及里面藏了多少看不见的脏数据。图片是素材标注是语义只有两者都对得上训练出来的模型才敢往楼梯间、工地入口、车窗边这种真实场景里部署。这篇文章面向准备自己做数据集或者要评估别人数据集的从业者从数据来源、格式转换、标注规范、质量校验到最小训练验证把整个链路里实际会碰到的坑逐个说一遍。2. 数据源头与格式选型公开数据、自采数据与VOC/YOLO/COCO怎么选2.1 三种获取渠道各自的适用场景抽烟检测数据集常见来源有三条路公开数据集、项目自采、外包标注。公开数据集的好处是省事坏处是场景往往和你实际部署环境不匹配。比如网上能下到的抽烟图片很多是欧美街拍、摄影棚摆拍背景是咖啡馆和酒吧光线充足人物正对镜头。你自己要部署的可能是工厂车间、学校走廊、地铁出入口摄像头是俯视视角光线偏暗人物背对镜头。这种差异会让模型在公开数据上训练得不错一上现场就大量误报漏报。项目自采是更可靠的方向拿一台手机或复用现场的摄像头在目标场景里连续采集一周覆盖早中晚、逆光顺光、晴天雨天框出真实人物动作。自采的问题在于数据量有限通常只有几百到一两千张需要和公开数据混合使用。外包标注则适合已经有原始图片、但没有人力逐张画框的团队关键在于交付标准和验收流程要提前写好否则回工率会让你怀疑人生。2.2 VOC、YOLO、COCO三种标注格式怎么选格式选型是所有后续脚本的地基。VOC格式是XML文件图片名和XML同名框坐标是左上角X、左上角Y、右下角X、右下角Y四个绝对值优点是直观、跨工具兼容性好很多开源标注工具默认导出就是VOC。YOLO格式是每张图片一个txt文件每行内容为类别编号 x_center y_center width height四个值全部归一化到0到1之间训练读取时不用实时解析XML速度更快是目前YOLO系列训练的主流输入。COCO格式是单个JSON文件把所有图片的标注聚合在一起适合需要统一管理的大规模数据集。我的建议很简单如果训练用YOLO系列直接存成YOLO格式如果数据还要给别人复用来做多轮评测VOC格式更保险COCO格式通常不必自己造除非你要跑的目标检测框架只吃COCO。从VOC转YOLO是最高频的操作因为很多公开数据以VOC形态下发。2.3 把VOC标注转成YOLO格式转换脚本与四个边界坑下面是一个从VOC XML转YOLO txt的常用脚本骨架核心逻辑是解析XML里的bndbox坐标结合图片实际宽高计算出归一化的中心点和宽高。import xml.etree.ElementTree as ET import os from PIL import Image def voc_to_yolo(xml_path, img_path, out_txt_path, class_map): tree ET.parse(xml_path) root tree.getroot() # 用图片真实尺寸做归一化而不是XML里的size节点 with Image.open(img_path) as img: img_w, img_h img.size lines [] for obj in root.findall(object): name obj.find(name).text if name not in class_map: print(f跳过未定义类别: {name} in {xml_path}) continue class_id class_map[name] 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) # 夹取到图片边界防止坐标越界 xmin max(0, xmin) ymin max(0, ymin) xmax min(img_w, xmax) ymax min(img_h, ymax) if xmax xmin or ymax ymin: print(f空框跳过: {xml_path} {name}) continue x_center (xmin xmax) / 2 / img_w y_center (ymin ymax) / 2 / img_h width (xmax - xmin) / img_w height (ymax - ymin) / img_h lines.append(f{class_id} {x_center:.6f} {y_center:.6f} {width:.6f} {height:.6f}) with open(out_txt_path, w) as f: f.write(\n.join(lines) \n) class_map {smoking: 0, smoke: 0, cigarette: 0}这个脚本的要点有两个第一归一化用图片真实读取的宽高而不是XML里写的size节点很多数据集在压缩图片时改了分辨率但XML没同步更新用XML尺寸会导致训练时框全部错位这是最隐蔽的坑第二类别映射表要提前统一同一个数据集里可能出现smoking和smoke两种写法必须在转换时归并成同一个编号。处理完之后还要检查输出txt是否为空——有些图标注了其他物体但没有抽烟目标会生成空文件这个正常但不能删除对应图片因为训练时图片会被当作负样本使用。需要额外注意的边界坑有三个一是坐标值在XML里可能是整数过粗离边界太近的小目标会被max(0, xmin)夹到边线上如果原图本身就把香烟截了一半宁可放弃这张图也不要用残缺框二是脚本要处理XML里没有bndbox节点或只有部分坐标的脏文件建议遇到异常直接打印文件名并跳过三是「图片和txt同名」这个约定不要在转换后改动YOLO训练逻辑默认按前缀名配对改一个文件名就要同步改另一个漏改一张就白跑一轮。2.4 公开数据混合自采数据的比例建议混合数据时如果以自采数据为主公开数据作补充常见做法是公开数据占20%到30%其余为自采数据。比例不是拍脑袋定的核心是保证自采数据的分布占主导避免模型被公开数据的背景风格带偏。如果公开数据量很大而自采只有几百张就反过来用公开数据预训练一个权重再用自采数据微调。剪枝和清洗公开数据比调整比例参数重要得多——同场景连拍图、带水印图、分辨率过低的图都要先删掉否则混合后噪声会翻倍。3. 标注工具与标注规范把「有人抽烟」变成机器能学习的框3.1 标注工具选型与项目目录约定标注环节决定数据质量的底线工具选型不需要太复杂。常见的开源标注工具里LabelImg依然是很多团队的默认选择因为它导出VOC格式直接、操作快捷键顺手、对低配电脑友好。另外一个更现代的选项是label-studio如果团队有规模化标注需求可以往后端多人协作的方式靠拢。但要注意换工具意味着换导出格式和标注习惯团队内部不要混用两种工具标注同一个数据集否则验收成本会非常高。目录组织建议固定成下面这套结构后续所有校验脚本和训练脚本都以它为约定dataset/ ├── images/ │ ├── train/ │ ├── val/ │ └── test/ ├── labels/ │ ├── train/ │ ├── val/ │ └── test/ └── classes.txtimages和labels分开存放训练和验证子目录一一对应。classes.txt里写类别名每行一个顺序和标注时的类别编号保持一致。这个结构即使不用固定工具手动整理也能一眼看出问题。3.2 标注框规范抽烟检测的六个细节约定标注框怎么画直接决定模型学到什么。以目标为「香烟」而非「整个人」的检测任务为例六个约定是我会提前写进标注说明文档的第一框住香烟本体可以包含持烟的手指但不要框到整只手。框大了模型会学到「手」而不是「烟」背景里有只手握拳也会误报。第二香烟是倾斜的框要尽量贴合香烟的旋转矩形外接框不需要画旋转框但不要把烟直立时的框和水平时的框混用一种比例习惯。第三香烟只冒出一小截、其余被手掌遮挡时仍然标注只框可见部分。第四电子烟的发热端不一定像传统香烟一样有燃烧点如果数据集定位是传统抽烟行为电子烟建议不标注或者单独开一个类别由项目需求决定。第五远处的小目标香烟只有几个像素标注时尽力而为如果连续多张图都看不清就删除该图而不是画一个臆测的框。第六一张图里有多个目标时全部标注不要「代表性地」只标最明显的那个否则模型会漏掉次要目标。这六条写清楚之后标注人员的返工率会明显下降。还有一个习惯是标注完成后导出一次VOC格式人工随机抽50张图检查框位置和图片内容是否对得上。这个动作成本低但能拦住一批「为了赶进度乱点」的低质量框。3.3 标注类别的设定单一类别与多类别的取舍「抽烟检测数据集」究竟标几个类别不是越多越好。常见方案是只标smoking一个类别所有形式的香烟、烟头、持烟动作都归为这一类好处是类别简单、标注速度快、模型专注度高。另一种方案是把cigarette、electronic_cigarette、smoke分开标好处是部署时可以分别做策略比如电子烟不告警坏处是标注难度骤增电子烟形态多样很容易标出大量不一致样本。如果项目刚起步建议先用单类别把流程跑通模型能稳定检出后再考虑拆分。数据如果有后续增强计划单类别基底也更容易做重标注。4. 质量校验脚本用数据说话把坏标注在训练前拦住4.1 先跑一遍图片与标注的完整性检查千辛万苦整理好的数据最怕训练跑到一半报错或者模型训练完才发现标注文件是坏的。第一个动作就是写一个完整性检查脚本一次性扫出图片能不能打开、标注文件和图片是否一一对应、标注坐标有没有越界、类别编号有没有超出类别数。下面这段脚本可以复用import os from PIL import Image def validate_folder(img_dir, lbl_dir, num_classes): errors [] img_names set() # 第一遍检查图片完整性 for f in os.listdir(img_dir): if not f.lower().endswith((.jpg, .jpeg, .png)): continue img_path os.path.join(img_dir, f) try: with Image.open(img_path) as img: img.verify() except Exception: errors.append(f图片损坏: {img_path}) continue img_names.add(os.path.splitext(f)[0]) # 第二遍检查标签完整性 lbl_names set() for f in os.listdir(lbl_dir): if not f.endswith(.txt): continue lbl_names.add(os.path.splitext(f)[0]) lbl_path os.path.join(lbl_dir, f) for line in open(lbl_path): parts line.strip().split() if len(parts) ! 5: errors.append(f标注行格式错误: {lbl_path}: {line.strip()}) continue class_id int(parts[0]) if class_id num_classes: errors.append(f类别编号越界: {lbl_path}: {class_id}) x_c, y_c, w, h map(float, parts[1:]) if not (0 x_c 1 and 0 y_c 1 and 0 w 1 and 0 h 1): errors.append(f归一化坐标越界: {lbl_path}: {line.strip()}) # 第三遍图片与标签一一对应 for name in img_names - lbl_names: errors.append(f图片缺少标注: {name}) for name in lbl_names - img_names: errors.append(f标注缺少图片: {name}) return errors errors validate_folder(dataset/images/train, dataset/labels/train, 1) for e in errors: print(e) print(f共发现 {len(errors)} 个问题)这个脚本输出的错误信息直接定位到文件名和具体行处理起来很快。其中img.verify()只检查文件头完整度不保证解码无误但足够拦住大多数损坏文件。要注意的细节是类别编号检查里num_classes要按实际类别数传不要传大了否则类别越界查不出来。4.2 用框的数量和尺寸分布识别漏标完整性检查只能发现「格式坏」的问题发现不了「漏标」的问题。漏标的典型特征是训练集中一部分图片的标注框数量明显偏少或者验证集里零标注图片比例异常。可以用统计脚本快速摸底统计每张图片的框数量、框宽度的像素分布、框面积的分布再画出直方图人工看一眼。如果大多数图片都是1到2个框突然出现一批10个框的图片以及一批0框的图片前者可能叠加了烟雾误标后者可能存在大面积漏标。零标注图片在数据集里是合法的负样本但如果占比超过10%并且集中在同一批拍摄场景里说明当时采集的人可能把「没人抽烟」理解成「没有目标」而忘了标注负样本图。抽样打开这批图复核一遍是值得的。4.3 用相似图片检测处理重复帧自采数据最容易混入重复帧尤其是用视频抽帧生成的图片同一段吸烟动作会被连续抽出十几张几乎一样的画面。这些图片进训练集会让模型过拟合到某个固定姿态进验证集会让验证分数虚高。处理思路是提前做去重。一种成本可控的做法是计算每张图片的感知哈希两两比较汉明距离距离小于阈值就归为重复。实际落地时更快的路径是先检查文件名是否有抽帧规律比如frame_00123、frame_00124这种连续命名直接按间隔抽帧。如果你拿到的数据集已经生成完成没有原始视频那就只能跑一遍哈希去重或者人工翻查相似图片。5. 抽烟检测数据集避坑记录五个最容易翻车的环节5.1 场景单一化模型把电梯间当成了烟现象模型在训练集上精度很高拿到现场以后把消防栓、黑色墙角、墙壁阴影全部报成烟。原因数据集里大量图片来自同一个场所的早晚两个时段背景高度相似模型实际学到的是「这个颜色纹理的区域手上的物体」的组合特征一旦背景里有同色系物体就触发误报。这属于数据分布问题不是模型问题。解决扩充不同场所的图片至少包含室内白墙、室外夜晚、车内、走廊逆光四种典型背景每种不少于100张。如果不想重新标注可以在现有图片上做背景替换增强但效果不如真实采集。5.2 重复图片导致验证集虚高现象训练完验证精度到0.95部署到现场却只有0.6差距大得离谱。原因数据切分前没有去重同一个场景的重复帧同时出现在训练集和验证集里模型相当于「背过答案」验证Loss完全失真。解决先做去重再做划分。至少保证验证集里的图片与训练集任何一张的相似度都不超过阈值可以通过感知哈希批量比对。5.3 标注框随意导致边界样本失控现象模型对完整露出的烟检测很好对用手遮住一半、嘴里叼着只露过滤嘴的情况几乎全部漏检。原因标注人员只框「整个香烟」对遮挡样本要么不标、要么框到手指上导致模型没见过遮挡形态。解决规范里明确规定遮挡只标可见部分并在标注说明里放三张示例图完整烟、半遮挡烟、只露过滤嘴的烟要求所有标注人员照图作业。关键测试时模型漏检最多的就是这类边界样本。5.4 类别失衡冷烟头拖累整体精度现象模型对正在燃烧的香烟检出率高对没点燃的烟卷和烟灰缸里的烟头检出率很低。原因很多人把烟头和香烟归为不同语义而不标注或者烟头样本太少被当作背景忽略模型自然而然学会忽略这类目标。解决先明确项目需求如果烟头也需要告警就要单独增加烟头样本并把烟头标成独立类别或统一归入smoking类如果只关心吸烟动作烟头可以不标但要保证不标的烟头图片里没有其他烟目标否则会污染负样本。5.5 图片压缩与EXIF旋转训练正常、推理方向不对现象训练时数据增强一切正常换到部署端却出现检测框偏转尤其是手机拍摄的竖图。原因部分图片带有EXIF旋转信息训练框架读取时做了自动纠正部署端读图库没有做同样处理图片实际是旋转后的状态标注却按旋转前画的框全部错位。解决所有图片在进入数据集前统一做一次转正处理去掉EXIF旋转信息并按标准RGB保存。这一步不能跳过用图像处理库批量转一遍训练和部署就统一了。6. 用数据集跑通最小训练流程从yaml配置到一次真实校验6.1 训练前的数据配置数据集整理完成后用它跑通一次最小训练验证是必须的。这个过程不追求最优精度只求链路通畅。以YOLO系列为例第一步是写数据配置文件常见内容是path: ./dataset train: images/train val: images/val nc: 1 names: 0: smokingpath指向数据集根目录train和val用相对路径nc是类别数names是一个类别名列表。这里最容易犯的错是把val配成images/train的子集导致训练和验证互相污染。配置完成后先跑一个val占位验证确认模型能读取到数据和标注再去调整训练参数。6.2 几个必调的参数训练参数里epochs、imgsz、batch、patience四个值优先调整。第一次跑通建议epochs100让模型充分收敛imgsz设置为训练集里最常见的图片分辨率不要盲目调到最大图片集里大量小目标的话用imgsz640足够缩放到1280只会增加显存占用batch受显存限制一般调成8到32之间的2的幂次不够就降低imgsz而不是硬堆batchpatience控制早停一般设20到30如果训练集小patience15也能接受。训练完成后用独立的测试集做一次推理验证统计每张图的置信度和框数量重点看漏检的图是不是集中在前面提到的那几类边界样本上。验证通过之前数据集和训练流程都不算交付更不要急着追加新图片。我自己的习惯是每完成一轮数据清洗就保留一份当时的校验报告和测试集推理结果方便后续扩充数据集时对比精度变化。这个习惯帮我避免了很多次「数据越加越差」的困惑。希望帮到你祝你的数据集和模型一次跑通。本文还有配套的精品资源点击获取