简介:一份用于火焰识别与检测的标注数据集,面向计算机视觉初学者和YOLO系列模型开发者,主要解决火灾预警、视频监控等场景下的目标检测训练数据匮乏问题。包体共5260个文件,压缩包大小约194MB,其中包含1553张JPG火焰图片、1854个txt格式标签文件和1553个xml标注文件,txt与xml可分别适配YOLO和VOC等常见训练框架。该数据集经YOLOv5实测,mAP@.5达到0.953,mAP@.5:.95为0.679,说明标注质量与检测效果均较理想,适合用于模型训练、算法验证及毕业设计课题。目前已有4901人学习下载,配套标注齐全,可直接按YOLO格式或VOC格式接入训练流程,节省人工标注时间,方便复现结果并对比不同网络结构的检测性能。
1. 火焰数据集:为什么检测模型总在真实火情里翻车,问题可能出在数据上
做过烟火识别的人基本都遇到过这个场面:模型在测试集上 mAP 能跑到 0.9,一到现场视频里就被路灯、红色卡车、夕阳反光反复打脸。问题往往不在模型结构,而在你拿到的火焰数据集本身——很多公开数据集的"火焰"都是火焰,但"非火焰"却是随便截的风景图,负样本和真实监控场景完全脱节。这个标题里"含标注好的标签"是关键信息,意味着你不用从零开始画框,可以省掉最费人力的标注环节。但省力不等于无脑用,一份火焰数据集拿到手,先看什么、怎么转格式、训练时怎么划分,这些步骤做不对,标注白给。这篇文章就按我自己的使用路径,从数据核验讲到训练验证,再把最容易翻车的几个坑提前给你排掉。适合正在做火焰检测、烟火识别、或者拿现成数据集做算法验证的工程师,按步骤走能省一两天试错时间。
2. 火焰数据集的构成与标注格式:先看懂标签再谈训练
2.1 火焰检测的标注格式流派:VOC、YOLO 与 COCO 怎么选
火焰检测和数据集的格式是绑定关系。做目标检测,标注格式主流有三种:Pascal VOC 的 XML 文件、YOLO 的 TXT 文件、COCO 的 JSON 文件。拿到一份火焰数据集,第一件事不是把图片丢进训练脚本,而是打开标注文件看三样东西:框的坐标系是绝对像素还是归一化的、类别名是单类还是多类、背景样本有没有单独标注。
我一般先看 YOLO 格式的 txt,因为现在检测模型基本都支持。它每一行对应一个目标,五个数字:class_id x_center y_center width height。对火焰检测来说,x_center、y_center 是归一化后的中心点坐标,width、height 是归一化后的框宽高,取值都在 0 到 1 之间。打开长这样:
0 0.514286 0.460317 0.285714 0.365079 0 0.713333 0.260000 0.150000 0.320000第一列的 0 是火焰类别编号,如果这份数据集只有火焰一类,那所有标注文件里第一列应该都是 0。如果出现过 FLAME、fire、smoke 之类的多类标签,训练时类别配置就要对得上。后面四个数要检查有没有超过 1 或者小于 0 的异常值——坐标越界通常意味着标注工具导出时出了 bug,这种样本混进训练集会直接让损失值爆炸。
VOC 格式则是 XML 文件,每个目标一个<bndbox>节点,存的是xmin, ymin, xmax, ymax绝对像素坐标,需要你对着图片尺寸做归一化再喂给 YOLO 系模型。COCO 格式是 JSON 词典结构,标注存在annotations列表里,框的格式是[x, y, width, height]。三者的转换本质是数学换算,但最容易错的不是换算公式,而是 VOC 坐标开闭区间——XML 里的 xmax 在解码以后不等于图像宽度,有时候要减 1。这份数据集如果号称"含标注好的标签",你要确认它给的是哪种格式,再决定要不要写转换脚本。
2.2 数据集内部分布核验:图片数量、标注密度和背景多样性
拿到数据集后先别急着训练。我会写几行代码,把标注文件全部扫一遍,统计几个基础指标:图片总数、标注框总数、每张图的平均框数、框面积占比。火焰检测有个特点,早期火灾中火焰往往只占画面很小一块区域,如果数据集的框都很大很正,部署到真实场景大概率性能打折。
用 Python 的 glob 加 cv2 扫一遍,逻辑很短:
import glob import cv2 txt_files = glob.glob("labels/*.txt") total_boxes = 0 small_boxes = 0 for txt in txt_files: with open(txt, 'r') as f: lines = f.readlines() total_boxes += len(lines) for line in lines: parts = line.strip().split() w = float(parts[3]) h = float(parts[4]) # 框面积占整图比例小于 0.1 的算是小目标 if w * h < 0.1: small_boxes += 1 print(f"图片数: {len(txt_files)}") print(f"总框数: {total_boxes}") print(f"平均每图框数: {total_boxes / len(txt_files):.2f}") print(f"小框占比: {small_boxes / total_boxes:.2f}")这里small_boxes的阈值 0.1 是我常用的经验值,火焰在监控画面里如果占比不足 10%,检测难度会明显上台。跑完这组统计,你能立刻判断这份数据集的边界:如果平均每图框数小于 1,说明大量图片是纯背景,训练时要考虑负样本的采样方式;如果小框占比很低,那模型在早期火情上的表现可能虚高。这两组数字直接决定后面要不要做数据增强、要不要专门挑小框样本做重采样。
2.3 视觉抽查不能省:标注错位和类别污染是隐藏杀手
统计只能发现数值异常,视觉上有没有标错得靠人眼抽查。我见过一份火焰数据集,几百张图中混入了电焊火花、舞台聚光灯,标注框倒是画得整整齐齐,但类别是错的——这类样本喂进去,模型学到的不是火焰纹理,而是"亮的东西"。抽查方法也很简单,打开一张图,把标注框画上去,连续翻几十张找规律。
import cv2 import glob img_files = glob.glob("images/*.jpg") for img_path in img_files[:50]: # 抽查前50张 img = cv2.imread(img_path) label_path = img_path.replace("images", "labels").replace(".jpg", ".txt") with open(label_path, 'r') as f: for line in f: parts = line.strip().split() x_c = float(parts[1]) * img.shape[1] y_c = float(parts[2]) * img.shape[0] w = float(parts[3]) * img.shape[1] h = float(parts[4]) * img.shape[0] x = int(x_c - w / 2) y = int(y_c - h / 2) cv2.rectangle(img, (x, y), (int(x + w), int(y + h)), (0, 0, 255), 2) cv2.imwrite(f"check_{len(glob.glob('check_*'))}.jpg", img)这段代码把 YOLO 坐标反算回像素坐标画框,框就是模型要学习的监督信号。抽查时重点看两类:一是框有没有偏大半圈把火焰周围的光晕全包进去,光晕和火焰纹理完全不同,框大了模型会误把橙色区域当特征;二是负样本里有没有"看起来很像火焰但不是"的干扰项,比如红色灯笼、橙色尾灯,这类样本如果数量太少,模型学不出判别边界。我自己的习惯是抽查 10% 的图片,如果发现 30% 以上的框有错位或者类别混入,这份数据集的标注质量就得打个问号,宁可直接换一份。
3. 把标注转成训练框架要的格式:转换脚本与四个边界坑
3.1 相同格式不同版本的转换陷阱
很多人以为格式转换是复制粘贴的活,实际上翻车概率最高的就在这里。同一个 YOLO 格式,不同版本框架对类别编号的起点定义不同——有些从 0 开始,有些从 1 开始。如果你的火焰数据集是 VOC XML 格式,要转成 YOLO txt,转换脚本写起来不长:
import xml.etree.ElementTree as ET import os def convert_voc_to_yolo(xml_path, output_dir, class_list): tree = ET.parse(xml_path) root = tree.getroot() img_width = int(root.find('size/width').text) img_height = int(root.find('size/height').text) yolo_lines = [] for obj in root.findall('object'): class_name = obj.find('name').text if class_name not in class_list: continue class_id = class_list.index(class_name) bbox = obj.find('bndbox') xmin = int(bbox.find('xmin').text) ymin = int(bbox.find('ymin').text) xmax = int(bbox.find('xmax').text) ymax = int(bbox.find('ymax').text) x_center = (xmin + xmax) / 2 / img_width y_center = (ymin + ymax) / 2 / img_height box_w = (xmax - xmin) / img_width box_h = (ymax - ymin) / img_height yolo_lines.append(f"{class_id} {x_center:.6f} {y_center:.6f} {box_w:.6f} {box_h:.6f}") with open(os.path.join(output_dir, os.path.basename(xml_path).replace('.xml', '.txt')), 'w') as f: f.write('\n'.join(yolo_lines)) # 使用示例:class_list 根据数据集的类别定义调整 convert_voc_to_yolo("data/annotations/001.xml", "data/yolo_labels", ["fire"])这段脚本核心是把像素坐标归一化。(xmin + xmax) / 2是框的中心点横坐标,除以img_width后,整张图宽度变成 1,每个目标的位置就是 0 到 1 之间的小数。box_w和box_h分别是框的宽高比例。这里有个容易踩的细节:VOC 的xmax和ymax在有的标注工具里代表的是"尾部像素 + 1",比如框覆盖了第 100 到 200 个像素,有的工具写xmax=200,有的写xmax=201。相差 1 个像素对最终 IoU 的影响几乎可以忽略,但如果你是密集小目标场景,这一个像素的偏移会导致框的中心点计算出偏差,训练初期损失值会比预期高一点。
3.2 类别编号与配置文件对齐
转换完之后最让人崩溃的报错就是:训练时AssertionError: class number out of range。原因通常是标签文件里有类别编号 5,但你在配置文件里只声明了 4 个类别。火焰数据集如果包含 smoke 和 fire 两类,class_list 应该是["fire", "smoke"]而不是["fire"],不然所有 smoke 标签都会被脚本跳过、变成空文件。
我建议在转换脚本里加一个统计,记录每个类别实际出现的次数,和数据集说明文档对照。类别数量对不上,要么是数据集文档写错了,要么是转换脚本的映射关系有问题。这一步排查花不了两分钟,但能省下训练跑到一半才发现损失函数异常的修罗场。
3.3 边界样本的框越界处理
归一化坐标出现负值或者大于 1,原因一般是标注的人把框拖到了图像边缘外,或者数据增强时旋转导致框超出画布。JSON 转 YOLO 时最容易遇到。我处理这类样本的原则是:框整体越界超过 50% 的,直接删掉这条标注;仅仅小部分越界的,把坐标 clamp 到 0 到 1 之间。
def clamp_bbox(x_center, y_center, w, h): x_min = max(x_center - w / 2, 0) y_min = max(y_center - h / 2, 0) x_max = min(x_center + w / 2, 1) y_max = min(y_center + h / 2, 1) new_w = x_max - x_min new_h = y_max - y_min return (x_min + new_w / 2, y_min + new_h / 2, new_w, new_h)clamp 之后框会稍微变小一点,但方向和位置大体保留,不算破坏监督信号。真正要警惕的是那些标注框只有几个像素大的样本——火焰目标再小,框内也得有可辨认的纹理信息,纯一两像素的框基本都是标注误差,直接丢掉比保留更安全。
3.4 数据集划分要按场景而非按文件随机
这个坑最隐蔽。火焰数据集如果来自某个视频片段抽帧,相邻帧的火焰形状几乎一致,随机划分测试集会高估模型性能。同一个火焰从出现到蔓延,连续 100 帧里前 80 帧当训练集、后 20 帧当测试集,模型在测试集上的表现会异常优秀,因为它在训练集里已经见过了同一团火的早期形态。
我的习惯是按视频来源划分数据集。如果数据集说明里没提视频来源,就看图片的文件名结构,多数抽帧数据集的文件名里会带时间戳或者帧序号。划分之前先做一次文件名前缀分组,把同一来源的图片全部塞进同一个子集。校验方法很简单:训练集和测试集之间的平均像素差应该足够大,如果一张图扣掉灰度均值后的残差和另一张图几乎一致,说明两个集合里有重复帧。
4. 用火焰数据集快速验证一个检测模型:训练与效果评估
4.1 从零配置一个火焰检测的最小训练命令
拿到的数据集格式和划分都验过以后,下一步就是跑通训练。常见做法是直接用 YOLO 系列的训练脚本,它自带数据增强和并行策略,省去自己造轮子的时间。你需要准备一份数据集描述文件,里面写出训练集、验证集的图片路径和类别数量。
# fire.yaml path: ./fire_dataset train: images/train val: images/val nc: 1 names: 0: fire这个 YAML 文件是数据集和模型之间的唯一桥梁。path是数据集根目录,train和val填图片文件夹的相对路径,nc是类别数量,火焰检测只有一个类别所以是 1,names的 0 号名称要和标签文件里的 class_id 一一对应。配置完成后,启动训练:
python train.py --data fire.yaml --weights yolov8s.pt --img 640 --epochs 100 --batch-size 16--img 640表示输入图片在训练时会被缩放到 640 像素,火焰这种小目标场景我建议至少用 640,条件允许就上 768。--epochs先设 100 看看收敛曲线,火焰数据集的样本量一般不大,100 个 epoch 足够判断模型学得动学不动。--batch-size根据显存调整,16 是一个起步值,如果你的显卡显存不够,降到 8 也能跑,只是 batch norm 的统计量会稍微不稳定。
4.2 训练过程中的关键观察点:loss 曲线和验证集指标
训练开始后,不要只看最终 mAP,要盯住三样东西:box_loss、cls_loss、验证集的 Precision/Recall。火焰检测的难点是正负样本不均衡,负样本(没有火的图)在数据集中可能占一半以上,模型很容易出现"不管有没有火都输出一个空白框"的偷懒行为。这时候观察验证集的 Recall 比 Precision 更有意义——漏报一次火灾的代价远高于误报一次。
火焰场景的 mAP 评估还有一个特殊点:IoU 阈值要调。COCO 默认的 mAP 是 IoU 从 0.5 到 0.95 的平均值,火焰目标边界模糊、光晕范围大,人工标注的框本身就带主观性,强求 IoU 0.75 以上的精确框不太现实。我看火焰模型的指标一般用mAP@0.5为主、mAP@0.5:0.95为辅,前者衡量的是"有没有找到火焰",后者衡量的是"框得准不准"。如果mAP@0.5在 0.9 以上但mAP@0.5:0.95不到 0.5,说明框偏大或偏小,视觉上就是预测框总是往一个方向偏移。
4.3 训练集增强策略:针对火焰场景调增广参数
火焰数据集规模普遍在几千张到几万张之间,靠增强来扩充是必须的。但火焰检测的增强参数有一个反直觉的点:颜色抖动不要开太强。火焰的颜色分布本身就比较复杂,从橙黄到深红都有,如果增强时把色调偏移调得太大,模型会学到错误的颜色关联,把"偏红的静止物体"也当成火焰。
我常用的一组增强参数参考如下:
# augment 配置 hsv_h: 0.01 # 色调偏移调到很低,火焰颜色不能乱变 hsv_s: 0.5 # 饱和度可以适当增强,适应不同环境下的火 hsv_v: 0.4 # 明度增强适中 fliplr: 0.5 # 水平翻转,火焰朝向不影响检测 scale: 0.5 # 尺度缩放,模拟远近距离的火 mosaic: 1.0 # 马赛克增强,默认开fliplr开 0.5 是因为火焰没有左右语义,水平翻转不会产生错误样本。mosaic增强对火焰检测尤其重要——它把四张图拼在一起,等于人为制造了多个小目标场景,对提升小火焰的 Recall 帮助很大。hsv_h是这里最需要保守的参数,火焰在视觉上最稳定的特征是"内部亮白、外部橙红"的渐变结构,而不是某一种特定颜色,色调偏移太大会让模型去记忆错误的颜色统计规律。
5. 火焰数据集的避坑清单:5 条实测翻车记录
5.1 现象:训练 Loss 正常下降,但验证集 Recall 始终为 0
原因:数据集中所有负样本图片都在验证集里,训练集几乎没有"无火焰"的图片。模型学到的规则是"只要看到图像就输出一个框",训练集里每个真实框都能被预测覆盖,Loss 当然很低。但验证集里负样本一多,模型找不到"空"的输出模式,Recall 直接崩掉。
解决:重新划分数据集,确保训练集、验证集、测试集里都有合理的正负样本比例。我通常要求负样本占比在 20% 到 40% 之间,太少模型学不到拒绝能力,太多模型会倾向于输出空检测。检查方法是统计训练集标签文件里空文件的个数,YOLO 格式下没有目标时 txt 文件是 0 字节,写个遍历统计一下就行。
5.2 现象:模型把红色消防栓、红色卡车识别成火焰
原因:数据集里的负样本是通用场景图片,和实际部署场景的负样本差异太大。公开火焰数据集的负样本大部分是山林、草原、普通街景,而工业场景里的红色物体非常多,模型在没见过这些"红色但不是火"的样本时,只能依据颜色判断。
解决:收集部署场景的负样本图片,加入训练集重训。这个步骤没有任何模型技巧能替代——数据分布不匹配,什么结构都白搭。一个可行的做法是把自己现场拍照的红色物体、橙色光照区域全部收进负样本文件夹,至少加 500 张,训练时和原数据集混合。
5.3 现象:标注框把火苗周围的大片光晕也包进去了
原因:标注人员的标准是先框住视觉上最亮的部分,但火焰的光晕在图片里边界模糊,不同标注员画的框差异巨大。框大了以后,模型学到的是"橙色渐变区域=火焰",而真实场景里橙色渐变可能是夕阳、灯光。
解决:清洗标注框,统一标准为"只框住火焰核心燃烧区域,不框光晕"。如果数据集的框普遍偏大,可以用脚本把标注框按中心点缩小 20% 到 30%,再人工复查一遍。不要试图靠 IoU 判断标注差异,因为光晕的渐进边缘没有标准答案。
5.4 现象:同一个火焰在连续帧里被标成不同的类别
原因:数据集从视频抽帧,火焰大小和形态逐帧变化,分类标准没有统一。比如某帧火苗是明亮的小火,标注成 fire,下一帧火势变大冒烟,又标注成 smoke,结果就是同一个目标的两个类别在训练时互相竞争。
解决:火焰检测数据集尽量做成单类,只在必要时区分"明火"和"烟雾"。多类别火焰检测在实际工程里会带来类别不均衡问题,火焰目标一般远多于烟雾目标,烟雾类别学不出来。我用过很多火焰数据集,单类别的稳定性和可解释性都远好于多类别。
5.5 现象:训练时图片加载到一半直接报错退出
原因:数据集中存在损坏的图片文件,或者某些图片的通道数不统一——有的是三通道彩色,有的是四通道带 Alpha 的 PNG。火焰数据集通常来自网络爬取或者视频截帧,这两种来源都有概率混入异常图片。
解决:训练前先跑一遍全量图片检查,用 cv2.imread 打开所有图片,如果返回 None 直接列入黑名单。同时检查图片尺寸是否为 0 或者单通道,统一转成 RGB 三通道再进训练。
import cv2 import glob corrupted = [] for img_path in glob.glob("fire_dataset/images/*/*.jpg"): img = cv2.imread(img_path) if img is None: corrupted.append(img_path) continue if img.ndim != 3: corrupted.append(img_path) if corrupted: print(f"发现 {len(corrupted)} 张异常图片") for p in corrupted: print(p) else: print("所有图片正常")这段检查代码的核心是img is None判断和img.ndim != 3判断,前者捕获文件损坏,后者捕获通道异常。数据集里少量异常图片的占比可能不到千分之一,但训练脚本在 DataLoader 里遇到会直接抛异常中断,相当于浪费一整晚的训练时间。
6. 从静态检测到动态预警:让火焰数据集发挥更大作用的一个验证方案
数据集和模型验证完,一个常见瓶颈就出现了:单帧检测在真实场景里会产生大量误报和跳变——这帧有火、下一帧没有、再下一帧又有,而真实业务要的是持续稳定的预警信号。我的做法是在检测模型后面加一个时间维度的确认机制,用连续帧投票代替单帧判断,这样即使单帧误报,也不会直接触发报警。
class FireAlertBuffer: def __init__(self, window_size=5, threshold=3): self.window_size = window_size self.threshold = threshold self.history = [] def update(self, is_fire): self.history.append(is_fire) if len(self.history) > self.window_size: self.history.pop(0) return sum(self.history) >= self.threshold # 使用示例:每帧送入模型检测结果,连续 5 帧中至少 3 帧有火才触发 alert_buffer = FireAlertBuffer(window_size=5, threshold=3) for frame_result in video_stream: if alert_buffer.update(frame_result): print("触发火焰预警")window_size=5表示看过去 5 帧,threshold=3表示其中至少 3 帧检出火焰才认为是真实火情,这两个参数一个管响应速度、一个管误报率。窗口越大越稳定,但对快速蔓延的火情响应越慢;阈值越高误报越少,但早期小火苗可能被漏掉。我做安防项目时常用 5 帧 3 票的配置做平衡,部署在 15 帧每秒的摄像头下大约有 1 秒的确认延迟,既能滤掉检测模型的单帧抖动,又能在一秒内响应真实火情。
帧间隔也需要根据实际帧率调整。监控视频通常接入后做抽帧处理,如果每 3 秒才取一帧,那 5 帧窗口代表 15 秒的观察期,响应速度就不太够了,要改成 3 帧 2 票这种短窗口。这套缓冲逻辑很轻量,CPU 上跑毫无压力,但它把检测模型的输出从"松散的逐帧告警"变成了"稳定的状态机信号",实际部署价值比单纯提 mAP 更直接。
我自己的血泪教训是:数据集指标再漂亮,都不如在现场连续跑一晚验证来得踏实。火焰数据集的标注质量决定了模型的上限,场景匹配决定了实际效果,而时间维度的平滑机制决定了从算法到产品之间那层可靠性。希望这篇笔记能帮你在做火焰检测时少走几步弯路,把省下来的时间花在真正影响落地效果的环节上。
本文还有配套的精品资源,点击获取