
简介这份红外火灾检测数据集面向从事火灾预警、工业安全监控与智能安防的算法工程师及AI研究者提供真实监控场景下的红外热成像图像用于训练火焰与火源目标检测模型解决可见光条件下烟雾遮挡、夜间识别困难等问题。资源包共2000个文件包含1608个YOLO格式标注txt、390张红外图像jpg以及1个yaml配置和1份docx说明文档压缩包约49.06MB可直接接入YOLO系列等主流检测框架。数据集划分训练集1467张、验证集93张、测试集48张标注严格区分Fire与Foc两类边界框聚焦热成像核心区域覆盖森林、山地、建筑与设备等多种自然和人工场景有助于提升模型泛化能力。目前已有264人学习下载适合构建火灾早期报警、高温区域动态分析及消防应急研究等应用也可作为消防培训中红外火灾特征识别的素材。1. 红外火灾检测数据集到底装了什么从一次误报翻车说起凌晨两点厂区值班室打电话过来说监控画面里着火了。我打开后台一看是阳光照在金属料堆上反射进红外镜头温度阈值直接顶格报警器响得理直气壮。这就是纯阈值方案最经典的翻车现场——它只认温度高低不认这团高温到底长什么样。后来我把思路换成用红外火灾检测数据集训练目标检测模型让模型学火焰在红外波段下的形状、纹理和动态特征误报率才压下来。这篇笔记讲的就是这套数据集从拿到手到跑通训练的全过程它里面装的是什么、标注格式怎么读、怎么转成 YOLO 能吃的格式、训练参数怎么调、以及我踩过的那些坑。适合正在做工业消防、森林防火、储能电站热失控预警的算法和嵌入式工程师也适合手上有红外模组、想把数据用起来的硬件同学。红外火灾检测数据集不是一张张火苗照片那么简单它背后是一整套红外成像特性、标注规范和训练策略的组合搞不清这些模型上线就是下一个半夜报警。2. 红外火灾检测数据集的结构与标注格式拆解2.1 红外图像和可见光图像差在哪为什么不能混着训先把一个常见误解说清楚红外火灾检测数据集里的图像绝大多数是长波红外LWIR一般 814μm热成像不是可见光加了滤镜。这意味着三件事。第一图像是单通道灰度或伪彩像素值代表辐射温度而不是反射亮度所以火焰在红外里通常是高亮团块但烟雾几乎不可见——烟雾温度接近环境红外对比度极低。第二金属、玻璃这类低发射率材质在红外里会骗人反射环境热源形成假高温区这就是我开头翻车的根源。第三红外镜头普遍分辨率偏低常见 256×192、384×288、640×512比可见光动辄 1920×1080 差一个量级小目标火焰可能只占十几个像素。所以选型第一条别拿可见光火灾数据集比如某些公开的火焰烟雾数据集直接混进红外数据训练域差异会让模型在红外上表现崩掉。如果一定要混合得先做域对齐或者干脆分开训两个模型做决策融合。红外火灾检测数据集的价值恰恰在于它提供了同域的真实红外样本这是自己拿可见光数据补不出来的。2.2 标注格式VOC XML、COCO JSON 还是 YOLO txt拿到数据集先别急着训第一步是搞清楚标注格式。红外火灾检测数据集常见的标注组织方式有三种我按遇到频率排格式典型文件坐标表示适用框架VOCAnnotations/*.xml左上右下绝对像素早期检测框架、LabelImg 默认COCOannotations/*.json左上宽高绝对像素Detectron2、MMDetectionYOLOlabels/*.txt归一化中心宽高YOLOv5/v8、Ultralytics 全家桶判断方法很直接看有没有 xml 文件夹、有没有一个大的 json、或者 labels 里是不是一堆 txt。红外火灾检测数据集如果是从标注平台导出的多半是 VOC 或 COCO如果是别人转好给 YOLO 用的就是 txt。这里有个血泪经验有些数据集号称 YOLO 格式但 txt 里写的是绝对坐标没归一化直接训会全乱。验证方法下面给。2.3 用一段脚本快速体检数据集在动手转换前我习惯先跑一段体检脚本把类别分布、图像尺寸、标注框大小统计出来。这一步能提前发现类别极度不平衡、空标注、坐标越界等问题。import os, glob from collections import Counter from PIL import Image root infrared_fire_dataset # 数据集根目录 img_dir os.path.join(root, images) lbl_dir os.path.join(root, labels) cls_counter Counter() box_areas [] bad_files [] for txt in glob.glob(os.path.join(lbl_dir, *.txt)): with open(txt) as f: lines [l.strip() for l in f if l.strip()] if not lines: bad_files.append(txt) # 空标注文件训练时会当负样本 continue for line in lines: parts line.split() if len(parts) ! 5: bad_files.append(txt) continue c, x, y, w, h int(parts[0]), *map(float, parts[1:]) cls_counter[c] 1 # 归一化坐标应落在 0~1越界说明没归一化或标错 if not (0 x 1 and 0 y 1 and 0 w 1 and 0 h 1): bad_files.append(txt) box_areas.append(w * h) print(类别分布:, dict(cls_counter)) print(框数量:, len(box_areas)) if box_areas: box_areas.sort() print(框面积中位数:, box_areas[len(box_areas)//2]) print(最小框面积:, box_areas[0]) print(可疑文件数:, len(set(bad_files)))逻辑说明脚本遍历 labels 目录下所有 txt逐行解析 YOLO 五元组。类别计数用 Counter框面积用归一化宽高相乘能反映目标尺度分布。参数上root改成你自己的路径即可bad_files收集空文件、字段数不对、坐标越界的文件这三类是训练前必须处理的。如果最小框面积小于 0.0001约 10×10 像素在 640 图上说明存在极小目标后面训练要开小目标增强否则这些样本基本学不到。3. 把红外火灾检测数据集转成 YOLO 可训练格式3.1 VOC 转 YOLO坐标归一化和类别映射如果你的红外火灾检测数据集是 VOC 格式转换核心就两步读 xml 里的 bndbox除以图像宽高做归一化再按类别名映射成整数 id。下面这段脚本我用了很多次稳。import os, glob import xml.etree.ElementTree as ET from PIL import Image voc_dir infrared_fire_dataset/Annotations img_dir infrared_fire_dataset/JPEGImages out_dir infrared_fire_dataset/labels os.makedirs(out_dir, exist_okTrue) # 类别名到 id 的映射顺序必须和训练时 data.yaml 的 names 一致 classes [fire, smoke, hotspot] cls2id {c: i for i, c in enumerate(classes)} for xml_path in glob.glob(os.path.join(voc_dir, *.xml)): tree ET.parse(xml_path) root tree.getroot() # 用对应图像的真实尺寸做归一化别用固定值 img_name root.find(filename).text img_path os.path.join(img_dir, img_name) W, H Image.open(img_path).size lines [] for obj in root.findall(object): name obj.find(name).text.strip() if name not in cls2id: continue # 跳过未定义类别避免 id 错乱 bbox obj.find(bndbox) xmin float(bbox.find(xmin).text) ymin float(bbox.find(ymin).text) xmax float(bbox.find(xmax).text) ymax float(bbox.find(ymax).text) # 裁剪到图像边界防止标注越界产生负宽高 xmin, ymin max(0, xmin), max(0, ymin) xmax, ymax min(W, xmax), min(H, ymax) cx (xmin xmax) / 2 / W cy (ymin ymax) / 2 / H bw (xmax - xmin) / W bh (ymax - ymin) / H lines.append(f{cls2id[name]} {cx:.6f} {cy:.6f} {bw:.6f} {bh:.6f}) out_path os.path.join(out_dir, os.path.splitext(img_name)[0] .txt) with open(out_path, w) as f: f.write(\n.join(lines))逻辑说明关键是归一化必须用每张图自己的 W、H不能图省事用 640 或 416 固定值否则非标准尺寸的图全错。cls2id的顺序要和后面 data.yaml 的 names 严格一致这是新手最容易翻车的地方——训练时类别对不上模型把火识别成烟。坐标裁剪那两行是后悔药标注平台偶尔会导出越界框不裁会产生负宽高训练直接报错。3.2 COCO 转 YOLO处理 segmentation 和 iscrowdCOCO 格式稍微麻烦点因为它的 bbox 是 [x, y, w, h] 绝对像素且可能带 segmentation。红外火灾检测数据集如果来自 COCO 标注转换时注意两点一是 bbox 的 w、h 是绝对宽高不是右下角坐标二是 iscrowd1 的框通常不参与训练建议跳过。import json, os from PIL import Image coco_json infrared_fire_dataset/annotations/instances.json img_dir infrared_fire_dataset/images out_dir infrared_fire_dataset/labels os.makedirs(out_dir, exist_okTrue) with open(coco_json) as f: data json.load(f) # COCO 的 category_id 往往不连续重映射成 0 起始 cats sorted(data[categories], keylambda x: x[id]) cat_map {c[id]: i for i, c in enumerate(cats)} print(类别映射:, {c[name]: cat_map[c[id]] for c in cats}) img_info {im[id]: im for im in data[images]} # 按图像聚合标注 from collections import defaultdict per_img defaultdict(list) for ann in data[annotations]: if ann.get(iscrowd, 0) 1: continue # 跳过 crowd 区域 per_img[ann[image_id]].append(ann) for img_id, anns in per_img.items(): im img_info[img_id] W, H im[width], im[height] lines [] for ann in anns: x, y, w, h ann[bbox] cx (x w / 2) / W cy (y h / 2) / H lines.append(f{cat_map[ann[category_id]]} {cx:.6f} {cy:.6f} {w/W:.6f} {h/H:.6f}) name os.path.splitext(im[file_name])[0] with open(os.path.join(out_dir, name .txt), w) as f: f.write(\n.join(lines))逻辑说明COCO 的 category_id 经常是 1、3、7 这种不连续值直接拿来当 YOLO 类别 id 会出问题所以用cat_map重映射成 0 起始连续值。iscrowd跳过是常规操作crowd 区域标注本身就不精确。参数上coco_json指向 instances 那个 json别指成 captions。转换完记得再跑一遍 2.3 的体检脚本确认。3.3 划分训练验证集并写 data.yaml转换完标注还要划分 train/val 并写配置文件。我一般按 8:2 分且同一段视频抽出的帧要分到同一侧否则相邻帧同时出现在训练和验证集会造成数据泄漏验证指标虚高。# 假设 images 和 labels 已就位按 8:2 随机划分 python - EOF import os, random, shutil random.seed(42) img_dir infrared_fire_dataset/images lbl_dir infrared_fire_dataset/labels for split in [train, val]: os.makedirs(fdataset/{split}/images, exist_okTrue) os.makedirs(fdataset/{split}/labels, exist_okTrue) imgs [f for f in os.listdir(img_dir) if f.lower().endswith((.jpg,.png,.jpeg))] random.shuffle(imgs) n_val int(len(imgs) * 0.2) for i, img in enumerate(imgs): split val if i n_val else train shutil.copy(os.path.join(img_dir, img), fdataset/{split}/images/{img}) lbl os.path.splitext(img)[0] .txt src os.path.join(lbl_dir, lbl) if os.path.exists(src): shutil.copy(src, fdataset/{split}/labels/{lbl}) EOF对应的 data.yamlpath: ./dataset train: train/images val: val/images nc: 3 names: [fire, smoke, hotspot]逻辑说明random.seed(42)保证划分可复现团队协作时大家拿到同一份划分。nc和names必须和转换脚本里的类别顺序完全一致这是硬约束。如果数据集本身按视频序列组织建议改成按序列划分而不是随机划分避免上面说的帧泄漏。4. 红外火灾检测数据集训练避坑5 个真实踩坑记录4.1 坑一验证集 mAP 很高上线全是误报现象训练完 mAP0.5 到 0.9 以上部署到现场误报率却高得离谱阳光反射、暖气片、人体都触发报警。原因验证集和训练集同分布都是干净的红外火灾场景没有负样本无火但高温干扰的画面。模型没学过高温但不是火的负例自然把一切高亮团块当火。解决往训练集里补负样本尤其是阳光反射金属、车辆排气、人体、工业热源这几类。负样本不需要标注框作为背景图加入即可比例控制在正样本的 20%30%。这一步比调任何超参都管用。4.2 坑二小目标火焰全漏检现象大火焰检测没问题远处或初期的小火苗十几像素几乎全漏。原因红外分辨率本来就低小目标经过下采样后特征几乎消失加上默认 anchor 尺寸偏大匹配不上小框。解决训练分辨率往上提640 起有条件用 1280开 mosaic 和 copy-paste 增强anchor 用 k-means 在你自己数据集上重新聚类。YOLOv8 这类 anchor-free 的模型省了这步但小目标增强还是要开。4.3 坑三类别 id 错位火被识别成烟现象训练 loss 正常下降但推理时类别全乱fire 框标成 smoke。原因转换脚本里的类别顺序和 data.yaml 的 names 顺序不一致或者 COCO 的 category_id 没重映射。解决转换后立刻用 2.3 的脚本打印类别分布再和 data.yaml 逐字比对。养成习惯类别映射表只维护一份转换和配置都引用它。4.4 坑四图像尺寸不一致导致训练报错现象训练启动就报 tensor 尺寸不匹配或者某些图被拉伸变形。原因红外火灾检测数据集里混了不同分辨率的相机数据256×192 和 640×512 混在一起且没统一 resize。解决训练前统一 resize 到目标尺寸保持长宽比做 letterbox 填充别直接拉伸。Ultralytics 的 dataloader 默认会做 letterbox但如果你自己写 dataset 类这一步要手动实现。4.5 坑五把红外当灰度图三通道复制浪费算力还掉点现象为了套用预训练权重把单通道红外图复制成三通道结果精度没提升推理还慢。原因ImageNet 预训练权重是 RGB 三通道直接吃单通道会报错所以很多人复制通道。但红外和可见光分布差异大预训练权重迁移收益有限复制通道反而增加计算。解决两种做法都试。一是改模型第一层卷积为单通道从头训或加载时跳过第一层二是保留三通道但做域适配微调。我实测在红外火灾检测数据集上单通道从头训 充分增强效果不比三通道迁移差推理还省三分之一算力。5. 让红外火灾检测模型真正能上线的两个进阶技巧5.1 用时序信息压误报单帧不够就上多帧单帧红外检测有个天花板静态高温物体和真火焰在单帧里长得像。但火焰有动态特征——闪烁、形变、面积变化静态热源没有。我的做法是在检测模型后面接一个轻量时序判断对同一位置连续 N 帧一般 510 帧的检测框做跟踪统计框内像素均值方差和面积波动。真火焰方差大、面积抖动静态热源方差小。这个后处理模块几十行代码能把误报再压一半。import numpy as np from collections import deque class FireTemporalFilter: def __init__(self, window8, var_thr15.0, area_thr0.15): self.window window # 观察帧数 self.var_thr var_thr # 像素方差阈值火焰通常更大 self.area_thr area_thr # 面积波动比例阈值 self.hist deque(maxlenwindow) def update(self, roi_gray): # roi_gray: 当前帧检测框内的灰度区域 self.hist.append((roi_gray.mean(), roi_gray.var(), roi_gray.size)) if len(self.hist) self.window: return False # 帧数不够先不报警 means [h[0] for h in self.hist] areas [h[2] for h in self.hist] area_fluct (max(areas) - min(areas)) / (np.mean(areas) 1e-6) var_mean np.mean([h[1] for h in self.hist]) # 方差大且面积抖动判为真火焰 return var_mean self.var_thr and area_fluct self.area_thr逻辑说明window是观察窗口太小容易误判太大响应慢8 帧在 25fps 下约 0.3 秒兼顾灵敏和稳定。var_thr和area_thr要在你的场景里标定不同相机、不同距离数值差别大别照搬。这个过滤器只对已检测到的框做二次判断不增加检测模型负担。5.2 验证方法别只看 mAP要看场景级指标mAP 是帧级指标但消防要的是一次火灾有没有报出来、一天误报几次。所以我上线前一定跑场景级评估把测试集按视频段组织统计每个真实火情段是否至少触发一次报警漏报率以及无火段触发了多少次误报次数/小时。这两个指标才决定系统能不能用。具体做法是给每段视频打标签跑完推理后按段聚合报警事件算检出率和误报率。我一般要求漏报率低于 2%、误报低于 1 次/天达不到就回去补负样本或调时序阈值。最后说个习惯每次换相机型号或安装角度我都会重新采一批现场数据微调哪怕只标几十张。红外成像对镜头、环境温度、发射率太敏感实验室训好的模型直接搬到新现场十有八九要翻车。这个方案值不值得做如果你面对的是储能电站、变电站、森林防火这类 7×24 小时无人值守场景红外加检测模型是目前性价比最高的组合比纯阈值可靠得多比可见光方案在夜间和无光环境强太多。数据集是起点真正拉开差距的是负样本和时序后处理这两块。希望帮到你。本文还有配套的精品资源点击获取