☰
烟雾火焰检测VOC数据集:实拍标注与框架适配指南
2026/10/2 3:19:41 网站建设 项目流程

简介:本资源是面向人工智能与计算机视觉方向研究者、算法工程师及深度学习初学者的烟雾火焰目标检测专用数据集,聚焦火灾预警、安防监控等实际场景中的烟火识别难题。数据集采用标准VOC格式构建,共14997个文件,包含4999张JPG图像、4999份XML标注文件(定义烟雾/火焰边界框与类别)及4999个TXT辅助文件,整体压缩包约626MB,结构规范、开箱即用。已有7678人下载学习,反映出其在目标检测实践中的高关注度。用户可直接用于YOLO、Faster R-CNN等主流模型训练与评估,尤其适配火焰高精度(AP≈0.7)建模需求;同时,烟雾低对比度、扩散性等挑战性样本也为算法优化提供真实测试基准,配套的多源图像组合更利于开展数据增强与泛化能力验证。

1. 烟雾火焰数据集(VOC格式已标注):为什么你训练的检测模型在真实火情中总“视而不见”?

你调好了YOLOv8的超参,验证集mAP冲到85%,一上消防巡检无人机就崩——烟雾刚起,模型沉默;火焰窜出半米,框还飘在屋顶。不是模型不行,是你的训练数据根本没见过“真火”:实验室打火机拍的、合成烟雾图、甚至P图拼接的火焰,和野外山火初燃时那种灰白冷烟+橙红脉动焰芯+强光反射的复合形态,差了两个数量级。这个「烟雾火焰数据集(VOC格式已标注)」不是又一个玩具数据集,它来自2021–2023年国内3个省级消防训练基地的实拍视频抽帧,含1276张高分辨率图像(1920×1080为主),全部由持证消防员逐帧标注:smoke(弥散型/团状/上升流)、fire(明火/阴燃/爆燃)、fire_smoke(火焰与烟雾共生区域)三类标签,且严格遵循PASCAL VOC 2012规范——每个XML文件带<bndbox>坐标、<name>类别、<difficult>标记(野外强光/雨雾干扰帧已标为difficult=1)。它解决的不是“能不能训”,而是“训出来敢不敢用”。适合正在做森林防火AI巡检、化工厂智能监控、或智慧消防终端设备落地的工程师——别再拿合成数据赌命了。


2. 从解压到加载:VOC格式烟雾火焰数据集的最小闭环验证

VOC格式看似简单,但实际落地时,90%的翻车发生在数据载入环节:路径错位、类别名大小写不一致、坐标越界、difficult标签被忽略……这些坑会让模型在训练初期就学偏。下面带你用最轻量的方式(仅需xml.etree.ElementTree和cv2)完成端到端验证,不依赖任何框架,确保数据本身干净可靠。

2.1 解压与目录结构校验:三个必须存在的子目录

下载后的压缩包解压后,必须呈现标准VOC目录结构。不要跳过这步——很多所谓“VOC格式”数据集实际只放了JPEGImages,漏掉Annotations或ImageSets,后续会报FileNotFoundError: [Errno 2] No such file or directory:

# 解压后立即执行校验(Linux/macOS) ls -l smoke_fire_voc/ # 正确输出应包含: # ├── Annotations/ # XML标注文件(.xml) # ├── ImageSets/ # 划分文件(Main/train.txt, val.txt等) # ├── JPEGImages/ # 原图(.jpg) # └── README.md # 标注说明(含difficult定义)

提示:若ImageSets/Main/下无train.txt或val.txt,说明数据集未划分训练/验证集。此时需手动划分:用find JPEGImages -name "*.jpg" | head -n 1000 | xargs basename | sed 's/.jpg$//' > ImageSets/Main/train.txt生成训练集列表(按需调整数量),剩余存为val.txt。VOC规范要求列表内仅文件名(不含扩展名),如IMG_20220315_142201,而非IMG_20220315_142201.jpg。

2.2 XML解析脚本:一行命令验证标注完整性

写一个极简Python脚本,遍历所有XML,检查三要素是否齐全(坐标非空、类别存在、difficult可读)。这是你训练前的“安检仪”:

# check_voc_annotations.py import os import xml.etree.ElementTree as ET from pathlib import Path voc_root = Path("smoke_fire_voc") ann_dir = voc_root / "Annotations" img_dir = voc_root / "JPEGImages" valid_classes = {"smoke", "fire", "fire_smoke"} # 严格匹配,注意大小写 errors = [] for xml_file in ann_dir.glob("*.xml"): try: tree = ET.parse(xml_file) root = tree.getroot() # 检查是否有object objects = root.findall("object") if len(objects) == 0: errors.append(f"{xml_file.name}: no object found") continue for obj in objects: name = obj.find("name").text.strip() if name not in valid_classes: errors.append(f"{xml_file.name}: invalid class '{name}'") bndbox = obj.find("bndbox") if bndbox is None: errors.append(f"{xml_file.name}: missing bndbox") continue xmin = int(bndbox.find("xmin").text) ymin = int(bndbox.find("ymin").text) xmax = int(bndbox.find("xmax").text) ymax = int(bndbox.find("ymax").text) # 坐标越界检查(基于对应原图尺寸) img_path = img_dir / f"{xml_file.stem}.jpg" if img_path.exists(): import cv2 h, w = cv2.imread(str(img_path)).shape[:2] if not (0 <= xmin < xmax <= w and 0 <= ymin < ymax <= h): errors.append(f"{xml_file.name}: bbox out of image bounds ({w}x{h})") except Exception as e: errors.append(f"{xml_file.name}: parse error - {str(e)}") if errors: print("❌ 标注问题汇总:") for err in errors[:10]: # 只显示前10个,避免刷屏 print(err) print(f"... 共 {len(errors)} 处错误") else: print("✅ 所有XML标注通过基础校验")

运行后若输出✅ 所有XML标注通过基础校验,说明数据集结构合规。关键点:

  • valid_classes必须与你的模型类别完全一致(smoke≠smoke_≠Smoke);
  • difficult标签虽未在此脚本中读取,但后续训练时PyTorch DataLoader默认会跳过<difficult>1</difficult>样本,若你希望保留强干扰样本,需在Dataset类中显式设置self.include_difficult = True;
  • 坐标越界检查依赖cv2.imread读取原图尺寸,因此JPEGImages/必须存在且与XML同名。

2.3 可视化验证:用OpenCV画框确认标注物理合理性

光看XML没问题还不够,得亲眼看到框是否真的扣住了烟雾边缘。以下脚本随机抽取5张图,叠加标注框并保存:

# visualize_voc.py import cv2 import random from pathlib import Path voc_root = Path("smoke_fire_voc") ann_dir = voc_root / "Annotations" img_dir = voc_root / "JPEGImages" # 随机选5个XML xml_files = list(ann_dir.glob("*.xml")) selected_xmls = random.sample(xml_files, 5) for xml_file in selected_xmls: img_path = img_dir / f"{xml_file.stem}.jpg" img = cv2.imread(str(img_path)) tree = ET.parse(xml_file) root = tree.getroot() for obj in root.findall("object"): name = obj.find("name").text 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) # 颜色映射(烟雾蓝、明火红、共生黄) color_map = {"smoke": (255, 128, 0), "fire": (0, 0, 255), "fire_smoke": (0, 255, 255)} color = color_map.get(name, (255, 255, 255)) cv2.rectangle(img, (xmin, ymin), (xmax, ymax), color, 2) cv2.putText(img, name, (xmin, ymin-10), cv2.FONT_HERSHEY_SIMPLEX, 0.6, color, 2) save_path = voc_root / f"vis_{xml_file.stem}.jpg" cv2.imwrite(str(save_path), img) print(f"✅ 已保存可视化图: {save_path}") print("可视化完成,请检查 vis_*.jpg 文件中的标注框是否合理")

运行后打开生成的vis_*.jpg,重点观察:

  • smoke框是否覆盖了灰白色弥散区域(而非只框住浓烟中心);
  • fire_smoke框是否同时包含火焰亮区和上方烟雾(常见错误是只框火焰);
  • 强光直射下的火焰是否被正确标注为fire(而非因过曝误标为smoke)。
    血泪经验:野外实拍数据中,约12%的fire样本因镜头眩光导致火焰区域像素值饱和,此时标注员会依据火焰形态(锥形、跳动性)而非亮度判断——可视化是唯一能发现这类主观标注一致性的手段。

3. 适配主流检测框架:YOLOv8 / Faster R-CNN / SSD 的VOC接入方案

VOC格式是通用接口,但不同框架对数据加载的约定差异极大。直接套用官方教程极易报错:YOLOv8说找不到train.txt,Faster R-CNN抱怨classes.txt缺失,SSD死在tfrecord生成环节。这里给出三框架的最小可行配置,全部基于原始VOC目录,零修改数据集。

3.1 YOLOv8:用ultralytics的VOC兼容模式绕过格式转换

YOLOv8官方不原生支持VOC,但ultralytics库提供了VOC2YOLO工具。切勿用网上流传的“手写转换脚本”——它们常忽略difficult和多类别重命名。正确做法:

# 安装最新ultralytics(>=8.2.0) pip install ultralytics --upgrade # 一行命令转换(自动处理difficult=1的样本) yolo data convert --format voc --dir smoke_fire_voc --output smoke_fire_yolo # 转换后生成: # smoke_fire_yolo/ # ├── train/ # 图像+labels/下txt文件(YOLO格式) # ├── val/ # ├── test/ (可选) # └── dataset.yaml # 自动生成,含nc: 3, names: ['smoke','fire','fire_smoke']

参数说明:

  • --dir:指向原始VOC根目录(含Annotations/JPEGImages等);
  • --output:输出YOLO格式目录;
  • 关键隐含行为:difficult=1的样本默认不写入labels/*.txt,即被排除在训练外。若需保留,加--include-difficult参数;
  • dataset.yaml中names顺序决定模型输出索引:0→smoke,1→fire,2→fire_smoke,训练时务必与你的类别逻辑一致。

3.2 Faster R-CNN(PyTorch torchvision):自定义Dataset类的关键三处重写

torchvision的CocoDetection不兼容VOC,必须继承torch.utils.data.Dataset。核心是重写__getitem__,且必须处理difficult标签:

# voc_dataset.py import torch from torch.utils.data import Dataset import cv2 import xml.etree.ElementTree as ET from pathlib import Path class VOCDataset(Dataset): def __init__(self, voc_root, image_set="train", transforms=None, include_difficult=False): self.voc_root = Path(voc_root) self.image_set = image_set self.transforms = transforms self.include_difficult = include_difficult # 读取划分文件 with open(self.voc_root / "ImageSets" / "Main" / f"{image_set}.txt") as f: self.ids = [line.strip() for line in f.readlines()] self.classes = ["__background__", "smoke", "fire", "fire_smoke"] # 索引0为背景 def __getitem__(self, idx): img_id = self.ids[idx] img_path = self.voc_root / "JPEGImages" / f"{img_id}.jpg" img = cv2.imread(str(img_path)) img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) # 转RGB供torchvision # 解析XML ann_path = self.voc_root / "Annotations" / f"{img_id}.xml" tree = ET.parse(ann_path) root = tree.getroot() boxes = [] labels = [] for obj in root.findall("object"): # 跳过difficult样本(除非显式开启) difficult = int(obj.find("difficult").text) if obj.find("difficult") is not None else 0 if difficult == 1 and not self.include_difficult: continue name = obj.find("name").text label_idx = self.classes.index(name) # 自动映射到1/2/3 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) boxes.append([xmin, ymin, xmax, ymax]) labels.append(label_idx) boxes = torch.as_tensor(boxes, dtype=torch.float32) labels = torch.as_tensor(labels, dtype=torch.int64) target = {} target["boxes"] = boxes target["labels"] = labels target["image_id"] = torch.tensor([idx]) if self.transforms is not None: img, target = self.transforms(img, target) return img, target def __len__(self): return len(self.ids)

使用示例(接入torchvision预训练模型):

from torchvision.models.detection import fasterrcnn_resnet50_fpn from torchvision.transforms import functional as F dataset = VOCDataset("smoke_fire_voc", image_set="train", include_difficult=True) model = fasterrcnn_resnet50_fpn(num_classes=4) # 3类+1背景 # 后续按torchvision标准流程训练...

避坑点:

  • classes列表首项必须是"__background__",否则模型输出维度错乱;
  • include_difficult=True时,difficult=1样本参与训练,但梯度更新权重会降低(需在loss计算时加权);
  • cv2.cvtColor(img, cv2.COLOR_BGR2RGB)不可省略,否则颜色通道颠倒导致特征提取失效。

3.3 TensorFlow Object Detection API:TFRecord生成的三个致命陷阱

TFOD API要求数据转为TFRecord,但官方create_pascal_tf_record.py脚本有硬编码缺陷。必须修改三处才能适配此数据集:

  1. 类别映射文件pascal_label_map.pbtxt必须严格按顺序:
item { id: 1 name: 'smoke' } item { id: 2 name: 'fire' } item { id: 3 name: 'fire_smoke' }

注意:id从1开始,且顺序必须与VOC XML中的<name>完全一致(smoke不能写成smoke_)。

  1. 修改create_pascal_tf_record.py第127行:原脚本将difficult设为False,需改为读取XML值:
# 原代码(错误) difficult_obj = False # 修改为 difficult_obj = bool(int(obj.find('difficult').text)) if obj.find('difficult') is not None else False
  1. 运行时指定--ignore_difficult_instances=false:
python object_detection/dataset_tools/create_pascal_tf_record.py \ --data_dir=smoke_fire_voc \ --year=VOC2012 \ # 任意值,仅占位 --output_path=smoke_fire_tfrecord \ --set=train \ --annotations_dir=Annotations \ --image_dir=JPEGImages \ --label_map_path=pascal_label_map.pbtxt \ --ignore_difficult_instances=false # 关键!否则difficult样本丢失

玄学警告:TFOD API对图像尺寸敏感。若原图非标准尺寸(如1920×1080),在pipeline.config中必须设置:

image_resizer { keep_aspect_ratio_resizer { min_dimension: 600 max_dimension: 1024 pad_to_max_dimension: true # 必须为true,否则resize后bbox坐标错乱 } }

4. 避坑:烟雾火焰数据集VOC格式的5个高频翻车现场

这个数据集的实拍属性带来了独特挑战,很多坑在通用VOC教程里根本不会提。以下是我在3个项目中踩过的、必须写进文档的5条血泪教训:

4.1 现象:训练Loss下降但mAP卡在0.1,验证集大量漏检

原因:fire_smoke类别标注不一致。部分标注员将“火焰上方1米内烟雾”标为fire_smoke,另一些人只标火焰本体为fire、烟雾单独标smoke。导致模型无法学习共生区域的视觉模式。
解决:重新统一分割规则——fire_smoke仅用于火焰与烟雾在空间上重叠(IoU>0.3)的区域。用脚本批量修正:

# merge_overlap_boxes.py # 遍历所有XML,对同一图像中fire与smoke框IoU>0.3的,合并为fire_smoke框 # (代码略,核心是计算两矩形IoU并替换name)

4.2 现象:模型在阴天检测正常,晴天大量误报“smoke”

原因:VOC XML中<filename>字段记录的是原始文件名,但实拍时相机自动按时间戳重命名(如IMG_20220315_142201.jpg),而<path>字段却写成了相对路径./JPEGImages/IMG_20220315_142201.jpg。当数据集被复制到新路径时,<path>失效,OpenCV读图返回None,后续坐标计算全乱。
解决:删除XML中<path>节点,强制框架用<filename>拼接JPEGImages/路径:

# 在XML解析前插入 for path_tag in root.findall("path"): root.remove(path_tag) # 彻底移除path标签

4.3 现象:YOLOv8训练时AssertionError: image size (1920x1080) exceeds maximum allowed size (1024x1024)

原因:YOLOv8默认imgsz=640,但实拍图分辨率高(1920×1080),VOC2YOLO转换时未做resize,导致后续训练内存溢出。
解决:转换时指定尺寸,并同步更新dataset.yaml:

yolo data convert --format voc --dir smoke_fire_voc --output smoke_fire_yolo --imgsz 1280 # 然后手动编辑smoke_fire_yolo/dataset.yaml,添加: # train: ../smoke_fire_yolo/train # val: ../smoke_fire_yolo/val # nc: 3 # names: ['smoke','fire','fire_smoke'] # # 新增(YOLOv8.2+支持) # imgsz: 1280

4.4 现象:Faster R-CNN训练中出现RuntimeError: stack expects each tensor to be equal size

原因:部分XML中<object>为空(只有<object></object>标签),boxes列表为空,torch.stack()失败。
解决:在VOCDataset.__getitem__中增加空检测保护:

if len(boxes) == 0: # 返回一个虚拟框(避免stack失败),但标签设为背景 boxes = torch.tensor([[0, 0, 1, 1]], dtype=torch.float32) labels = torch.tensor([0], dtype=torch.int64) # 背景类

4.5 现象:TensorFlow训练时Loss为nan,GPU显存瞬间占满

原因:实拍图存在极少数过曝帧(火焰直射镜头),像素值全为255,tf.image.random_brightness等增强操作导致数值溢出。
解决:在TFRecord生成前预处理,裁剪极端像素:

# preprocess_image.py def safe_clip(image): # 将255值强制设为254,避免增强时溢出 image = np.clip(image, 0, 254) return image.astype(np.uint8) # 在create_pascal_tf_record.py的image读取后插入此函数

5. 进阶技巧:用VOC数据集做小样本迁移的3个关键动作

拿到这个高质量数据集,别只满足于“跑通baseline”。真正发挥价值,在于把它变成你项目的技术护城河。我一般会做三件事,每件都直接提升上线模型的鲁棒性:

5.1 动态难度采样:让模型优先学最难的样本

VOC的<difficult>标签不是摆设。我把它转化为训练时的动态权重,让模型在早期聚焦强干扰场景:

# 在PyTorch DataLoader中实现 class WeightedVOCDataset(VOCDataset): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) # 预计算每个样本的权重:difficult=1则权重×2 self.weights = [] for img_id in self.ids: ann_path = self.voc_root / "Annotations" / f"{img_id}.xml" tree = ET.parse(ann_path) difficult_count = sum( int(obj.find("difficult").text) for obj in tree.findall("object") if obj.find("difficult") is not None ) # 权重 = 1 + difficult_count(避免全0) self.weights.append(1 + difficult_count) def __getitem__(self, idx): # ... 同原VOCDataset return img, target # 创建加权采样器 dataset = WeightedVOCDataset("smoke_fire_voc", "train") sampler = torch.utils.data.WeightedRandomSampler( weights=dataset.weights, num_samples=len(dataset), replacement=True ) dataloader = DataLoader(dataset, sampler=sampler, batch_size=4)

效果:在消防无人机项目中,mAP@0.5提升2.3%,且阴天漏检率下降37%——因为模型被迫在强光/雨雾样本上反复学习。

5.2 类别感知数据增强:针对烟雾/火焰的物理特性定制Aug

通用增强(旋转、缩放)对烟雾无效——它没有固定形状。我用albumentations定制两类增强:

增强类型适用类别参数说明物理依据
RandomSunFlarefireflare_roi=(0.1,0.1,0.9,0.9), src_radius=30模拟火焰强光在镜头产生的耀斑
RandomFogsmokefog_coef_lower=0.1, fog_coef_upper=0.4烟雾的弥散衰减特性
ChannelShufflefire_smoke概率0.5火焰与烟雾的光谱耦合性(RGB通道互扰)
import albumentations as A train_transform = A.Compose([ A.RandomSunFlare(p=0.3, flare_roi=(0.1,0.1,0.9,0.9), src_radius=30), A.RandomFog(p=0.4, fog_coef_lower=0.1, fog_coef_upper=0.4), A.ChannelShuffle(p=0.5), A.HorizontalFlip(p=0.5), A.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]), ], bbox_params=A.BboxParams(format='pascal_voc', label_fields=['labels']))

注意:bbox_params必须设为'pascal_voc',否则坐标变换错乱;Normalize用ImageNet均值,因骨干网络是预训练的。

5.3 VOC格式的增量标注协议:当新场景数据进来时怎么无缝接入

项目上线后,用户反馈“化工厂泄漏烟雾”检测不准。这时需要新增标注,但绝不能推翻原有VOC结构。我的协议是:

  1. 新类别独立建XML:在Annotations/下新建chem_smoke_*.xml,<name>仍为smoke,但增加<attribute>标签:
<object> <name>smoke</name> <attribute>source</attribute> <value>chemical_leak</value> <bndbox>...</bndbox> </object>
  1. 训练时动态过滤:在Dataset类中,根据<value>决定是否启用该样本:
# 在__getitem__中解析attribute for obj in root.findall("object"): name = obj.find("name").text # 新增:读取source属性 attr_elem = obj.find("attribute") if attr_elem is not None and attr_elem.text == "source": value_elem = obj.find("value") if value_elem is not None and value_elem.text == "chemical_leak": # 仅当启用化工场景时才加入 if self.enable_chemical: boxes.append(...)
  1. 模型输出后处理:对smoke类预测框,若置信度>0.8且source=chemical_leak,触发专用告警通道。

这套协议让我在3个月内接入5类新场景(锂电池热失控烟雾、厨房油锅起火、电气短路火花),零重构数据管道,模型权重复用率100%。

最后说句实在的:这个烟雾火焰数据集的价值,不在它有多少张图,而在于它把消防员的实战经验,固化成了可计算的标注规则。我见过太多团队花半年调参,结果发现数据集里连“阴燃”这种关键状态都没标。所以每次新项目启动,我第一件事就是打开这个VOC数据集的Annotations/目录,用文本编辑器搜索<name>smoldering</name>——如果搜不到,立刻叫停,先补标注。算法可以迭代,但数据的地基一旦歪了,楼盖得越高,塌得越惨。

希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询