简介:这是一套面向目标检测与计算机视觉学习者的VOC格式烟火数据集,专门用于烟雾和明火两类目标的检测模型训练与评估。资源包总大小约687.88MB,包含6460张jpg原图和对应的6460个xml标注文件,另附使用说明txt一份。数据集由labelImg工具完成矩形框标注,标注类别为smoke和fire,其中烟雾标注框7901个、明火标注框11066个;每张xml均记录对应图片中的目标类别与边界框坐标,便于直接解析训练。图像内容覆盖交通事故、森林火灾、建筑失火、柴木生火等多类烟雾场景,以及蜡烛、柴火、奥运火炬等多种明火形态,有助于增强模型在多样化环境下的适应能力与泛化效果。压缩包目录结构清晰,jpg与xml一一对应,可直接接入主流检测框架;目前已有2559人学习使用,适合需要进行烟火识别算法研究或相关项目开发的数据集需求方。
1. 烟火检测数据集选型:VOC 格式的 6460 张为什么是复现首选
做烟火识别项目,最磨人的往往不是模型结构,而是数据集。我接过好几版网上流传的烟雾明火图,要么缺标注,要么类别写错,要么标注坐标直接越界,跑起来全是玄学问题。这份 [数据集][VOC][202206][重制版] 烟火数据集烟雾明火2类别数据集VOC-6460张,是标准的 Pascal VOC 组织格式,一共 6460 张标注图像,覆盖烟雾、明火两个目标类别,重制版重点是把标注质量和划分文件重新清理过一遍。
它的价值在于"通用"。VOC 格式的 XML 标注几乎是目标检测的通用语言,YOLO、SSD、Faster R-CNN、DETR 都能直接解析,拿来当基线数据集非常合适。做消防通道监控、油烟检测、森林防火这类场景的从业者,拿到手就能开始做模型验证。
当然,直接丢进训练脚本里还是会翻车。类不平衡、坐标越界、划分重叠、难样本被误删,这些坑我在后面按踩坑记录一条条讲清楚,照着做能少折腾好几天。
2. 数据集结构与标注解析:JPEGImages、Annotations 与 XML 字段
拿到压缩包先别急着解压训练,先把目录结构摸清楚。Pascal VOC 的目录约定很固定,这个重制版沿用的是同一套规范,只是把图像和标注重新整理过一遍。标准布局如下:
dataset/ ├── JPEGImages/ # 全部图像,jpg 格式 ├── Annotations/ # 每张图像对应一个同名 XML 标注 └── ImageSets/ └── Main/ ├── train.txt # 训练集图像名列表 ├── val.txt # 验证集图像名列表 └── trainval.txt # 训练+验证全集2.1 目录布局与数量核对:先统计再动手
JPEGImages 放原图,Annotations 放 XML,两者靠文件名一一对应。比如 JPEGImages/smoke_000123.jpg 一定对应 Annotations/smoke_000123.xml,缺一个就说明数据集有残缺。ImageSets/Main 下的 txt 每行一个图像名,不带扩展名,用来划分训练集和验证集。
我拿到任何数据集的第一件事就是做数量核对,不会直接信说明文档。命令行三分钟能完成:
$ ls JPEGImages | wc -l 6460 $ ls Annotations | wc -l 6460 $ sort ImageSets/Main/train.txt | head -5 smoke_000001 smoke_000002 smoke_000120 fire_000004 fire_000011两个文件数对不上,说明有图没标或有标没图,这种数据集直接训练会出莫名其妙的漏检。还要对比 train.txt、val.txt 和 trainval.txt 的关系,正规做法是 trainval 是全集,train 和 val 互不重叠且并集等于 trainval。用 comm 命令可以快速查重叠:
$ comm -12 <(sort ImageSets/Main/train.txt) <(sort ImageSets/Main/val.txt)这条命令输出空才是正常,有输出就说明划分重叠,后面我会解释为什么这是个大坑。这一步属于"花钱买防坑保险",最多五分钟就能做完整套数据一致性检查。
提示:解压后如果 JPEGImages 和 Annotations 数量一致,但 ImageSets 里的文件名在两张表里都找不到,大概率是重制时换了命名前缀,先用 basename 对照再跑脚本。
2.2 XML 标注字段逐项解析:理解框架怎么读你的数据
VOC 的标注文件是 XML,每个 object 节点就是一个目标框。随便打开一张标注:
<annotation> <folder>VOC2022</folder> <filename>smoke_000123.jpg</filename> <size> <width>1920</width> <height>1080</height> <depth>3</depth> </size> <object> <name>smoke</name> <pose>Unspecified</pose> <truncated>0</truncated> <difficult>0</difficult> <bndbox> <xmin>231</xmin> <ymin>84</ymin> <xmax>1204</xmax> <ymax>910</ymax> </bndbox> </object> </annotation>size 节点里的 width 和 height 是归一化坐标的分母,必须和 jpg 实际尺寸一致,不一致时转 YOLO 格式坐标会整体漂移。object 节点里 name 是类别名,bndbox 是框坐标。VOC 约定坐标从 1 开始计数,xmin、ymin 最小值是 1 而不是 0,xmax、ymax 最大可以等于图像宽高,转换时要注意这一刀切在哪里。
truncated 表示目标在图像边缘被截断,difficult 表示这个目标本身模糊、难辨认。这两个字段在转换时是保留还是丢弃,直接影响最终效果,第 5 章我会专门讲 difficult 的处理。另外注意一个常见误读:folder 和 filename 字段在解析时完全可以忽略,训练框架只认文件实际路径,不认 XML 里写的名字,很多人在这里被带偏,以为改 XML 里的 filename 就能换图。
2.3 类别与数量分布:先摸清家底再训练
烟火数据集最容易出现类别不均衡。常规情况是烟雾样本多、明火样本少,因为现实中烟雾出现频率远高于明火。动手前先统计一下每个类别的目标框总数:
import xml.etree.ElementTree as ET from pathlib import Path cnt = {'smoke': 0, 'fire': 0} both = 0 empty = 0 for xml_file in Path('Annotations').glob('*.xml'): root = ET.parse(str(xml_file)).getroot() names = [obj.find('name').text for obj in root.iter('object')] if not names: empty += 1 for n in names: cnt[n] = cnt.get(n, 0) + 1 if len(set(names)) == 2: both += 1 print('类别框数:', cnt) print('同时含烟雾+明火的图:', both) print('无目标的图:', empty)这段脚本用 ElementTree 解析全部 XML,iter('object') 拿到所有目标节点,再按 name 统计。empty 统计的是没有任何目标的背景图,训练时背景图是用来压误检的,数量太多或太少都会影响模型对负样本的判别。both 统计的是单张图同时出现两类目标的样本,这类是烟火联动场景里最有价值的训练素材,后面做难例挖掘时优先从这里下手。
统计结果如果 smoke 是 fire 的三倍以上,就要考虑类别加权或者后续做数据增强补足。我见过有人跳过这一步直接开训,最后 mAP50 只有 0.6,一查发现 fire 的 recall 才 0.3,白白浪费几十个小时的显卡时间。这个统计脚本建议保存下来,换数据集时改一下类别名就能复用。
3. VOC 转 YOLO 与 COCO:两条转换路线的脚本与参数细节
3.1 为什么非要转格式:训练框架只认自己的标注
VOC 的 XML 虽然通用,但现代训练框架都不直接吃 XML。YOLOv8 要的是每张图一个 txt 文件,每行"类别ID cx cy w h",坐标全部归一化到 0~1;MMDetection 和 Detectron2 更习惯 COCO 的单个 json 文件;YOLOv5 的旧版本还要额外的 dataset.yaml 和数据目录约定。所以拿到 VOC 数据集的第一步,是把它转成你要用的框架格式。
转换脚本本身不难,坑全在坐标约定和边界处理上。我自己拆过不下十个数据集,VOC、COCO、KITTI、BDD 之间互相切,最后总结出一条规律:先搞清楚源格式的坐标原点是 0 还是 1,再做转换,否则差一个像素的偏移在 640 分辨率下看不出来,到了 1280 训练时就开始掉点。这套切换流程同样适用于 COCO2017 结构的数据集,原理是一模一样的。
3.2 VOC 转 YOLO:归一化坐标脚本与边界处理
我一般用下面这段脚本做转换,兼容 Python 3,只用标准库,不用额外依赖:
import xml.etree.ElementTree as ET from pathlib import Path CLASSES = ['smoke', 'fire'] # 类别顺序就是类别 ID,定下来就不能改 INPUT_DIR = Path('Annotations') OUTPUT_DIR = Path('labels') OUTPUT_DIR.mkdir(exist_ok=True) def convert(xml_path: Path) -> None: root = ET.parse(str(xml_path)).getroot() img_w = int(root.find('size/width').text) img_h = int(root.find('size/height').text) lines = [] for obj in root.iter('object'): name = obj.find('name').text if name not in CLASSES: continue if int(obj.find('difficult').text) == 1: continue # 默认丢弃 difficult=1 的难样本,理由见第 5 章 box = obj.find('bndbox') x1 = max(float(box.find('xmin').text), 0) y1 = max(float(box.find('ymin').text), 0) x2 = min(float(box.find('xmax').text), img_w) y2 = min(float(box.find('ymax').text), img_h) if x2 <= x1 or y2 <= y1: continue # 越界后框宽高为负,直接跳过 x_center = ((x1 + x2) / 2) / img_w y_center = ((y1 + y2) / 2) / img_h w = (x2 - x1) / img_w h = (y2 - y1) / img_h lines.append(f'{CLASSES.index(name)} {x_center:.6f} ' f'{y_center:.6f} {w:.6f} {h:.6f}') out = OUTPUT_DIR / f'{xml_path.stem}.txt' out.write_text('\n'.join(lines)) for xml_file in INPUT_DIR.glob('*.xml'): convert(xml_file) print('转换完成,输出目录:', OUTPUT_DIR)这里有三处参数要特别说明。第一,CLASSES 列表的顺序决定了类别 ID,smoke 是 0、fire 是 1,后面训练 yaml 里的 names 必须和这里完全一致,顺序反了就是模型学到一半整个标签错位,推理结果全乱。第二,x1/y1/x2/y2 做了 clamp,把越界的坐标拉回到图像范围内,防止归一化后出现大于 1 的坐标导致 loss 异常。第三,丢弃 difficult=1 的框需要谨慎,这个数据集如果 difficult 标注得比较松,直接全丢会损失一批真实的难样本,先按 5.3 的方法统计数量再决定。
提示:转换后检查一下标签文件总数。labels 目录下 txt 数量应该等于 Annotations 里 XML 数量减去空标注的数量,对不上就回头查日志,别急着开训练。
3.3 转 COCO 的关键字段与组织方式
如果要用 MMDetection 或 Detectron2,就得转 COCO json。COCO 的核心是三个顶层字段:categories、images、annotations。categories 记录类别 ID 和名称;images 记录每张图的 id、文件名、宽高;annotations 记录每个框的 bbox(x, y, w, h,左上角坐标)、area、image_id、category_id。下面这个表把最容易出错的字段列出来:
| COCO 字段 | 来源 | 常见错误 |
|---|---|---|
| bbox | (xmin, ymin, xmax-xmin, ymax-ymin) | 误把 xmax/ymax 直接塞进去,框会偏大 |
| area | (xmax-xmin) * (ymax-ymin) | 不填或用宽高之积以外的算法 |
| image_id | 与 images 里的 id 对应 | 漏掉或对不上,训练时报 KeyError |
| annotation id | 全 json 唯一自增 | 从 0 重复,coco eval 直接乱 |
VOC 的 bndbox 是 xmin/ymin/xmax/ymax,COCO 要的是 x/y/width/height,转换时减一下就行,很多人栽在"忘了减"上。area 字段在评估时用于大小目标分组,如果和图实际面积对不上,小目标那一档的 AP 会失真,看起来 mAP 挺高,实际落地时小目标全漏。
转完 COCO 后建议用官方 pycocotools 的 loadRes 加载一次,能通过说明 json 结构没问题,这一步能挡住一半以上的低级错误。顺带一提,很多做自动驾驶数据集转换的同事也踩过同样的坑,COCO 的 annotation id 全局唯一这件事,几乎每个手写转换脚本的人都会忘。
4. 用 YOLOv8 跑通烟雾明火训练:数据配置与超参调优
4.1 数据 yaml 与目录准备
转到 YOLO 格式之后,把文件按训练目录约定摆好。ultralytics 的规则是 images 和 labels 分目录,且文件名一一对应,image 目录用 train/val 区分,labels 目录结构要完全镜像:
smoke_fire/ ├── images/ │ ├── train/ # 约 80% 图像 │ └── val/ # 约 20% 图像 └── labels/ ├── train/ └── val/划分直接用第 2 章核对过的 train.txt、val.txt。我一般把官方划分当成默认值定死,只在明显不均衡的时候手动重划。然后在数据集根目录放一个数据描述文件:
path: /data/smoke_fire # 数据集根目录,绝对路径或相对路径 train: images/train val: images/val names: 0: smoke 1: firenames 的顺序必须和转换脚本里的 CLASSES 一致,不能因为 fire 更重要就把它排到 0,ID 一旦乱了模型训练出来就是废的。nc(类别数)不需要手写,yaml 里有几个 names 就自动识别几个,这也是最容易偷懒出错的地方——有人把 names 写成 1: smoke, 0: fire,训练能启动,但标签全反,最后看 confuse matrix 才反应过来。
4.2 训练命令与关键参数
数据准备好后,一条命令就能开训:
yolo detect train \ data=smoke_fire.yaml \ model=yolov8s.pt \ epochs=100 \ imgsz=640 \ batch=16 \ patience=20 \ workers=4model=yolov8s.pt 是带 COCO 预训练权重的 s 版本,烟火检测这种目标对比度强、结构不算复杂的任务,s 级足够,没必要上来就上 l 或 x,先跑通流程再按需升级。imgsz 默认 640,但明火很多是小目标,640 下可能只有十几个像素,有条件就上 1280。imgsz 改成 1280 后显存占用大约翻两倍,batch 要从 16 降到 8,显卡不够时先保证分辨率再保 batch,分辨率对烟火小目标的影响远大于 batch size。
patience=20 是早停轮数,20 个 epoch 内验证集 mAP 没提升就自动停,省时间。workers 是数据加载线程数,Windows 上设 0 最稳,Linux 上 4~8 都行,设太高会频繁报 DataLoader worker 崩溃。
关于预训练权重有一点要提醒:COCO 预训练模型没见过"烟雾"这种半透明、无锐利边缘的目标,前几十个 epoch 的 loss 会掉得很慢,这是正常的,不是代码写错了。想加速收敛可以先用 3 个 epoch 的小跑测通流程,再开正式训练,这一步能避免你对着一个跑了一半才报错的命令干瞪眼。
4.3 验证指标:mAP、混淆矩阵与 PR 曲线
训练结束后,ultralytics 会在 runs/detect/train 下输出 results.csv。我用最简单的方式看最后一行的 mAP:
import csv with open('runs/detect/train/results.csv') as f: rows = list(csv.DictReader(f)) last = rows[-1] print('mAP50:', last['metrics/mAP50(B)']) print('mAP50-95:', last['metrics/mAP50-95(B)'])mAP50 是 IoU>0.5 时的平均精度,工业上常用来判断"能不能用";mAP50-95 对框的位置要求更苛刻,学术报告偏爱用它。做烟火检测我主要看 mAP50,定位要求是别把烟囱当烟、别把灯光当火,框稍微差几个像素不致命。
val_batch 预测图能直观看到漏检和误检,但我更推荐看混淆矩阵。混淆矩阵在 runs/detect/train/confusion_matrix.png,按行是真实类别,按列是预测类别。对角线亮说明正常,如果 fire 那一行大部分落到了 background,就是明火小目标漏检严重,对应第 5 章第 2 条的处理方法。PR 曲线则用来判断置信度阈值放在哪,曲线右下角弯曲说明低置信度区间误检多,部署时阈值就得往上抬。
5. 烟火数据集避坑指南:五条翻车记录与排查思路
5.1 训练 loss 变 nan:坐标越界与空标签
现象:训练跑十几个 batch 后 loss 直接变成 nan,或者某张图反复报"all labels empty"。
原因:VOC 的坐标写成 (0, 0, 0, 0) 或者 xmax 超过图像宽度,转 YOLO 时归一化坐标出现负数或大于 1;另一类情况是某些 XML 里 object 节点存在但 bndbox 为空,转换出的 txt 是空文件,YOLO 对这种空标签图会直接跳过,多了就影响 batch 统计。
解决:转换脚本里对 x,y,w,h 做 clamp 和"x2<=x1 就跳过"的防护,这是第一道保险。第二道保险是数空标签文件,labels 目录下大小为 0 的 txt 要单独列表,确认是有意保留的背景图还是坏数据。
$ find labels -name "*.txt" -size 0 | wc -l这个数字如果是几十且和 JPEGImages 里的纯背景图对得上,可以留;如果几百个,就得回源头查 XML。我踩过一次,训练到一半全崩,查了两天才发现是某个批次下载的 XML 被截断,bndbox 只剩了半个标签。
5.2 明火几乎检不出来:类别不均衡与输入尺寸
现象:训练完 mAP50 看着还行,但 val_batch 图上火苗一个没框住,或者 fire 的 recall 只有 0.2 左右。
原因:一个是数据不均衡,烟雾框动辄占图面积的 40%,明火框只有几十个像素,模型天然倾向学"大块灰白区域";另一个是 imgsz 太小,20 像素的火焰在 640 分辨率下被下采样到 5 个像素以内,特征直接消失。
解决:先按 2.3 的统计确认 fire 框数量,少了就做复制增强:对含 fire 的图做随机裁剪放大、旋转、亮度扰动,把 fire 样本量补到烟雾的一半以上。同时把 imgsz 升到 1280 重训,mAP50 通常能涨 3~5 个点。这条是我做过烟火项目里性价比最高的改动,没有之一。
5.3 验证集虚高、实拍翻车:difficult 样本被误删
现象:训练集上 mAP50 有 0.85,拿到现场摄像头一测,烟雾浓、遮挡多、目标糊的场景全部漏检。
原因:转换脚本把 difficult=1 的框全丢了。这个重制版里部分难样本确实标了 difficult,但它代表的是"难"不是"错",全删等于把最难学的样本从训练集里抹掉了,模型学到的全是"标准清晰目标",泛化自然差。
解决:先统计 difficult=1 的框数,少于总数 10% 就保留,只在训练时把难样本单独提出来做困难样本挖掘;如果确实噪点多,也要单独留出一部分做验证,不能全删。从那以后我转换脚本默认不再硬删 difficult,而是先打印数量再人工决策,这个习惯救了我好几次。
5.4 train 和 val 出现同一张图:划分文件重叠
现象:训练 loss 收敛很快,val mAP 冲到 0.9+,但换全新场景就崩,分布完全不像这个量级。
原因:train.txt 和 val.txt 有重叠,模型在训练时已经见过验证图,验证失去意义,这叫数据泄漏。常见于多批数据合并时文件名重排,或者有人手动往 val 里补图之后没同步改 train。
解决:第 2 章的 comm 命令专门用来查这个。发现重叠就把重叠部分从 val 里剔除,并保证 trainval 并集覆盖全集。最关键的是这个检查要写进每次换数据集的固定流程,不能只看 loss 曲线,曲线会骗人,泄漏不会。
5.5 烟雾大目标被 mosaic 切碎:数据增强没调
现象:训练指标正常,但推理时大片烟雾被切成好几个框,或者漏掉半透明边缘。
原因:YOLOv8 默认开 mosaic,把四张图拼成一张,烟雾这种跨半张图的大目标在拼图时被切得只剩角落,模型学到的目标形态是"碎片",自然学不会完整的烟柱。
解决:大目标占比高的数据集建议把 mosaic 关掉或降到 0.5 以下,同时把 scale 增强范围收窄。ultralytics 里可以直接在训练命令加 mosaic=0.0 参数,或者改配置文件,保留轻度翻转和亮度增强就够。烟雾检测和常规的目标检测不一样的地方就在这——别人怕目标太小,烟火项目怕目标太大被增强切没。
6. 进阶:难例挖掘与 TTA 把烟火模型的漏检压到最低
这算是我每次交付烟火检测模型前的固定收尾动作。基础训练跑完之后,先用低置信度阈值把验证集整个过一遍:
yolo detect predict \ model=runs/detect/train/weights/best.pt \ source=val_images \ conf=0.05 \ save_txt=True \ save_conf=Trueconf 设成 0.05 而不是默认的 0.25,目的是逼模型把所有"它其实有点想框但犹豫了"的目标都吐出来。预测结果里每个 txt 带置信度,按置信度升序排,取最低的那批图人工看:
from pathlib import Path scores = [] for txt in Path('runs/detect/predict/labels').glob('*.txt'): for line in txt.read_text().splitlines(): parts = line.split() scores.append((float(parts[-1]), txt.stem)) scores.sort() for conf, name in scores[:50]: print(f'{name}: {conf:.3f}')这批低置信度框只有两种命运:确实是漏检的难例,把它们连同原图抽出来,混进训练集重新训一轮;确实是不存在的误检,则说明模型对这类背景还没建立负样本概念,收集对应背景图补进去。我的一贯做法是"难例优先,背景图跟上",两边都缺模型就不稳。
另一个低成本手段是 TTA(测试时增强),火灾报警这类场景误报代价高,宁可慢一点也要稳。ultralytics 预测时加 --augment 参数,模型会对同一张图做多尺度推理再合并结果,小目标召回率有明显提升。缺点是推理耗时翻几倍,实时视频流慎用,做离线审核很合适。
烟火数据集的迭代终点不是 mAP 刷到多高,而是"难例库再也没有新增"。我把每次线上漏检的截图都丢回难例池,三个月后模型对逆光、薄雾、远处小火苗都稳了。从那以后,我每次拿到新数据集都强制自己走一遍:先数文件数,再查拆分重叠,然后统计类别分布,最后才动训练命令。这一步省下来的调试时间,远大于它花掉的那十分钟。这套流程同样适用于你手里的这份重制版烟火数据集——文件名记得是 [数据集][VOC][202206][重制版] 烟火数据集烟雾明火2类别数据集VOC-6460张,下载后先对照第 2 章的目录结构核对一遍再开工。希望帮到你。
本文还有配套的精品资源,点击获取