☰
YOLO火灾检测数据集:从标签格式到训练部署全流程
2026/10/5 5:27:04 网站建设 项目流程

简介:面向YOLO系列目标检测算法的火灾与人员探测数据集,适合算法学习者、安防消防领域开发者快速获取带标注的训练数据。内容覆盖人、烟、火等检测类别,数据已划分训练/验证/测试集,并附data.yaml配置文件,可直接适配yolov5、yolov7、yolov8、yolov9、yolov10、yolo11等常见版本,省去数据转换与整理环节。压缩包总大小141.83MB,标签文件共2000个,以VOC格式xml标注为主,同时提供YOLO格式txt标注,两种格式分别存于独立文件夹,便于项目切换标注体系。xml文件名能直观看出对应类别,配合图片即可用于模型训练、验证与测试。由于标签格式说明清晰,入门者也能较快上手,直接搭好训练流程。已有48人学习下载,适合需要快速验证火灾/人员识别方案、开展算法对比实验的开发者参考。

1. 火灾与人员探测:这份YOLO目标检测数据集到底能直接拿来干什么

先想清楚一个场景:防火监控里真正要紧的检测目标不外乎人、烟、火三类。YOLO算法目标检测数据集市面上不少,但多数是通用COCO类目,要落地到消防告警还得自己清洗、改标签、重划分,费半天劲。这份资源给的是3039张带标签图像,同时提供yolo txt和voc xml两种格式,自带data.yaml,训练集、验证集、测试集已经划分好,拿到手不用做标签转换就能直接跑yolov5到yolo11的整个链路。适合谁用?做过目标检测但没时间整理消防领域数据的人,或者刚入门YOLO、想用真实数据走通全流程的新手。后面会把这套数据的格式、配置、训练命令、常见翻车点逐一拆开,都是实际跑过才写出来的。

2. 标签格式拆解:YOLO txt与VOC xml两种坐标体系

2.1 类别分布与文件名命名规律

数据集标题已经写明人、烟、火三类,对应到标签文件里通常就是person、smoke、fire。项目正文里列出的xml文件名,比如img_0398_1633.xml,这类编号本身不带类别信息,真正的类别在文件内容里。解压后打开任意xml,看<object><name>字段就能确认完整类别列表,不要凭文件名猜类别顺序。

# 在labels/yolo目录下统计每个类别出现的总框数 cat labels/train/*.txt | awk '{print $1}' | sort | uniq -c

统计结果是理解类别平衡性的第一手资料。消防场景里火焰和烟雾通常占大头,人员框数可能只有前者的五分之一,这个比例直接影响训练时的类别权重配置。如果person类别只有一两百个框,宁可先用数据增强把它拉起来,也不要直接上模型硬训。

三类目标在监控画面里的特征差异很明显:person是刚性物体,轮廓稳定;smoke是半透明的弥散区域,边界模糊;fire则是高亮色块,颜色饱和度高但形状不稳定。这些差异决定了后面增强参数怎么调。

2.2 归一化坐标到底存了什么

YOLO格式的txt文件每一行就是:

<class> <x_center> <y_center> <width> <height>

五个数字全部用空格分隔,其中x_center、y_center、width、height都是归一化比例值,范围在0到1之间。什么意思?以一张宽1280、高720的图像为例,某个火焰框的像素坐标是左上角(320,180)、右下角(960,540),那么:

x_center = (320 + 960) / 2 / 1280 = 0.5 y_center = (180 + 540) / 2 / 720 = 0.5 width = (960 - 320) / 1280 = 0.5 height = (540 - 180) / 720 = 0.5

归一化的好处是坐标不随图像分辨率变化。训练时YOLO统一把图像resize到640x640,标签不需要跟着改;如果换成了1280x1280的输入,标签也原样能用。这一点和VOC的绝对像素坐标有本质区别,也是新手最容易混的地方。

检查标签越界可以用下面这段Python脚本,任何坐标值超出[0,1]都会打印文件名:

from pathlib import Path for f in Path("labels/train").glob("*.txt"): for line in f.read_text().strip().splitlines(): parts = list(map(float, line.split())) if len(parts) != 5: print(f"[格式错误] {f.name}: {line}") if parts[1] < 0 or parts[1] > 1 or parts[2] < 0 or parts[2] > 1: print(f"[越界] {f.name}: {line}")

这段脚本的思路很简单:先把每行拆成5个浮点数,再检查中心点坐标是否落在[0,1]区间。实际项目里我还会加一条width和height不能为0的判断,空框会让损失函数算出一个奇怪的nan,训练直接崩溃。

2.3 用Python把XML转成YOLO格式

数据集同时给了yolo和voc两种标签,但逻辑上txt和xml应该是同一批目标的不同表达。如果想把xml当权威来源、重新生成txt,或者反过来检查两种格式是否一致,需要写一个转换脚本。核心逻辑是先从xml的size节点读图像宽高,再把bndbox里的绝对坐标换算成归一化坐标:

import xml.etree.ElementTree as ET from pathlib import Path def voc_to_yolo(xml_path, out_txt_path, class_map): tree = ET.parse(xml_path) root = tree.getroot() img_width = float(root.find("size/width").text) img_height = float(root.find("size/height").text) lines = [] for obj in root.iter("object"): name = obj.find("name").text.strip() if name not in class_map: continue cls_id = class_map[name] box = obj.find("bndbox") x1 = float(box.find("xmin").text) y1 = float(box.find("ymin").text) x2 = float(box.find("xmax").text) y2 = float(box.find("ymax").text) if x1 >= x2 or y1 >= y2: print(f"[跳过异常框] {xml_path}: {name} {x1} {y1} {x2} {y2}") continue x_center = ((x1 + x2) / 2.0) / img_width y_center = ((y1 + y2) / 2.0) / img_height w = (x2 - x1) / img_width h = (y2 - y1) / img_height lines.append(f"{cls_id} {x_center:.6f} {y_center:.6f} {w:.6f} {h:.6f}") Path(out_txt_path).write_text("\n".join(lines), encoding="utf-8") class_map = {"person": 0, "smoke": 1, "fire": 2} voc_to_yolo("img_0398_1633.xml", "img_0398_1633.txt", class_map)

几个关键点。class_map的类别顺序必须和data.yaml里的names保持一致,否则模型学到的类别索引和实际物体对不上,训练完的模型会在推理时把火焰标成人。x_center保留6位小数足够,多几位也不会提高精度。脚本里对x1>=x2做了防御,人工标注的xml里偶尔会出现这种脏数据,硬转成txt会让模型看到一个宽为负的框,训练时梯度直接乱掉。

注意:转换完成后别急着开训,把txt和xml的数量对一遍。一个xml对应一个txt,哪个多出来都要查清楚,多出来的往往是空标签文件在作祟。

3. 训练前的数据配置:把data.yaml和目录结构一次弄对

3.1 目录结构与data.yaml字段

数据集已经划分好,标准目录结构是这样:

dataset/ ├── images/ │ ├── train/ # 约2000+张 │ ├── val/ # 约500张 │ └── test/ # 约500张 ├── labels/ │ ├── train/ │ ├── val/ │ └── test/ └── data.yaml

images和labels必须一一对应,每张jpg都要有一个同名txt。划分好的数据集一般不会出错,但解压路径变化后,yaml里的path字段需要重新核对。data.yaml的内容大致如下:

path: /root/fire-dataset train: images/train val: images/val test: images/test nc: 3 names: 0: person 1: smoke 2: fire

path字段我通常直接写成绝对路径,而不是相对路径。原因有两个:第一,绝对路径在训练、验证、导出onnx三个环节都不会迷路;第二,如果数据集挪到另一台服务器上,只改这一行就够了,train、val、test这些后缀路径永远不要动。

lc字段和names列表是另一个容易出问题的地方。nc必须等于names的数量,names的索引顺序必须和txt标签里的类别索引完全一致。有些数据集会把names写成person、fire、smoke,和训练脚本里class_map的顺序不一致,最后mAP曲线会非常难看。拿到数据后第一件事就是打开一个txt文件,看第一个数字是几,再和yaml对照。

3.2 用Ultralytics训练YOLOv8的命令

装好ultralytics包之后,训练命令非常干净:

pip install ultralytics # 在COCO预训练权重yolov8s.pt基础上迁移学习 yolo train model=yolov8s.pt data=/root/fire-dataset/data.yaml \ epochs=100 imgsz=640 batch=16 device=0

model=yolov8s.pt会自动下载COCO预训练权重。对这份数据来说,人员类别在COCO里已经有很强的先验,迁移学习后person的AP起步就高;烟和火则需要后面几十个epoch自己学。如果机器没有网络,提前把权重下载好放在本地,再把model参数改成权重文件的路径。

参数怎么选?imgsz先取640,这是精度和速度的平衡点。火焰本身是大目标居多,640够用;如果想要更精细的小火苗检测,可以试768,代价是显存占用和推理耗时同步上升。batch大小取决于显存,8G显存跑yolov8s配batch 16比较稳妥,OOM就直接降到8。epochs建议先给100,训练完看val曲线再决定是继续训练还是早停。

提示:如果用的是yolov5的官方仓库,训练命令不同。python train.py --data data.yaml --weights yolov5s.pt --batch-size 16 --img 640是yolov5的标准写法,yolov7用户则需要改hyp配置文件。这份数据集的标签格式是通用的,换到哪个框架都能直接读。

3.3 验证命令与三类评估指标

训练完成后,验证命令同样简单:

yolo val model=runs/detect/train/weights/best.pt \ data=/root/fire-dataset/data.yaml \ imgsz=640

验证结果会给出mAP50、mAP50-95、precision、recall四个主要指标。对火灾告警场景来说,mAP50的价值远高于mAP50-95,因为告警逻辑不要求预测框和真实框完全重合,只要大致框住火焰区域就够了。经验值:mAP50超过0.85就基本可用,mAP50-95超过0.6已经相当能打。

只看总体mAP会掩盖类别问题。val命令的输出里会打印每个类别的ap50,如果person只有0.3而fire有0.9,说明人员框数量太少或标注质量差,需要针对person做数据增强,而不是盲目调全局参数。这个习惯能节省大量调参时间。

4. 避坑指南:标注数据训练时最常见的四类问题

4.1 现象:loss收敛但mAP一直卡在0.4

跑完100个epoch,train loss曲线很漂亮,val的mAP50却始终在0.4上下浮动。原因主要是类别不平衡加标签噪声。火灾场景里person框数量远少于smoke和fire,网络先学会烟和火,人员框就是学不进去。另一个常见原因是标签里有漏标或错标,验证集上模型预测对了但ground truth是错的,AP直接被拉低。解决方法是先看类别分布,再逐个检查标签:

from pathlib import Path import collections cnt = collections.Counter() for f in Path("labels/train").glob("*.txt"): for line in f.read_text().strip().splitlines(): cnt[int(line.split()[0])] += 1 print(cnt)

如果person只有一百来个框,别急着改网络结构。常见做法是把增强里的copy-paste打开,把人员目标复制粘贴到不同背景上,或者用crop把包含人的区域放大后单独训练。标签噪声的排查更花时间,我的做法是随机抽200张训练图,把模型预测结果画出来和原标签对比,错得离谱的框十有八九是标注源问题,人工补一轮比调参划得来。

4.2 现象:XML转TXT后坐标出现负值或大于1

第一次做格式转换就遇到框跑到画面外面,画出来的标签严重错位。原因几乎都是宽度和高度没有分别归一化,或者xml里xmin和xmax写反了。还有一种情况是某些标注工具导出的坐标会超出图像边界,比如标注时手抖多画了两像素。解决方法是转换脚本里加约束检查,任何坐标不在[0,1]区间都打印文件名:

# 全量扫描标签目录 for f in Path("labels/train").glob("*.txt"): for line in f.read_text().strip().splitlines(): parts = list(map(float, line.split())) if any(c < 0 or c > 1 for c in parts[1:]): print(f"[越界] {f.name}: {line}")

我的习惯是转换后全量扫描一遍,遇到越界先回xml里确认是脏数据还是公式bug。如果xml里xmax本来就不大于xmin,直接丢弃该目标框,这种框喂给模型只会让边框回归头学到错误信息。注意扫描脚本只检查坐标范围,格式错误如小数位数缺失、负号位置不对也得单独排查,自动化脚本挡不住所有脏数据。

4.3 现象:浓烟区域里的火焰被漏检

烟雾和火焰在视觉上天然重叠,烟雾半透明会稀释火焰的纹理特征,模型把烟和火框重叠时,NMS会优先保留置信度更高的smoke,fire就被压掉了。另一个原因是标注时烟和火框交叠严重,两个正样本在同一个位置冲突。解决办法分训练和推理两层:

训练前检查重叠框,当fire框和smoke框的IoU大于0.4时,优先保留fire标签,同时把smoke框缩到不重叠的区域。推理时把fire的conf阈值放宽到0.3,smoke阈值提到0.5,给火焰更多通过机会。这样会带来一些误报,但告警场景宁可多报也不漏报,误报可以靠延迟确认来过滤,漏报可能造成实质损失。

4.4 现象:白天误把白墙、水泥地当烟雾

问题根子是负样本太少。数据集里几乎所有图像都包含烟火目标,模型没学过“没有烟的普通墙面长什么样”,于是把浅色光滑区域都当成烟。解决方法是找几百张不含烟火的普通场景图,不打标签,只生成空txt文件放进训练目录重新训练。我第一次这么干的时候效果立竿见影,误报率降了一半还多。另一个实用手段是在推理链路里加帧间差分或背景建模,只处理运动区域内的检测结果,静止的白墙天然被过滤掉。

5. 从模型到告警:推理脚本与阈值策略

5.1 单张图片推理与模型选择

训练完先跑单张图验证效果:

from ultralytics import YOLO model = YOLO("runs/detect/train/weights/best.pt") results = model.predict( source="test_fire.jpg", conf=0.35, iou=0.5, imgsz=640, save=True, )

conf=0.35表示置信度低于0.35的预测框被丢弃,iou=0.5是NMS阶段用的IoU阈值,框与框的重叠超过0.5时保留置信度更高的。这两个参数直接影响漏检率和误检率,现场调优时可以画一条PR曲线来找平衡点。模型选择上,不同规模权重的差异很现实:

模型参数量单帧耗时参考适合场景
yolov8n3.2M5ms边缘AI盒子
yolov8s11M12ms常规监控服务器
yolov8m26M25ms中心算力充足

如果推理设备是低功耗的RK3588或者Jetson Nano,yolov8n配合fp16精度是更合理的选择;如果跑在普通GPU服务器上,yolov8s是精度和速度的甜点。没必要为了零点几个点的mAP上yolov8m,火灾检测这类场景对帧率的要求往往比对精度的要求更硬。

5.2 视频流检测与丢帧策略

接RTSP流做实时检测时,代码要稍微工程化一点:

import cv2 import time from ultralytics import YOLO model = YOLO("runs/detect/train/weights/best.pt") cap = cv2.VideoCapture("rtsp://192.168.1.100:554/stream1") while cap.isOpened(): ret, frame = cap.read() if not ret: continue results = model(frame, conf=0.4, iou=0.5, verbose=False) for r in results: for box in r.boxes: cls_id = int(box.cls[0]) conf = float(box.conf[0]) if cls_id == 2 and conf > 0.4: # fire类别索引为2 trigger_alarm(time.time()) cv2.imshow("fire-detect", results[0].plot()) if cv2.waitKey(1) & 0xFF == ord("q"): break

trigger_alarm建议用异步调用,不要在推理线程里做写日志、发短信这类阻塞操作。视频流解码和推理如果共用主线程,解码耗时高的时候帧率会骤降。常见做法是开一个解码线程,把帧放进队列,推理线程只管消费,队列超过5就丢最旧的帧,保证告警延迟可控。我遇到过解码线程卡住导致推理线程空转的情况,后来在循环里加了超时判断,连续30帧读不到就重连RTSP。

5.3 导出ONNX与部署注意事项

模型验证没问题后要部署,导出onnx:

yolo export model=runs/detect/train/weights/best.pt \ format=onnx imgsz=640 dynamic=False

导出时imgsz必须和训练尺寸一致,否则某些算子的输入维度对不上会直接报错。dynamic=False表示固定输入尺寸,部署到TensorRT时推理速度更快;如果输入分辨率会变化,才需要dynamic=True,但代价是预处理耗时增加。ONNX导出后还有个容易踩的坑:Python的opencv读图默认是BGR,而模型训练时用的是RGB。如果推理脚本直接喂BGR帧给onnx,烟雾和火焰的颜色特征会偏差很大,检测效果明显下降。转换时要么在预处理里加cv2.cvtColor(frame, cv2.COLOR_BGR2RGB),要么在导出时把模型输入改成BGR。

6. 数据增强与场景复用:把3039张图的边界撑开

6.1 增强参数按场景调

火灾检测里火焰对颜色敏感,烟雾对纹理和透明度更敏感,增强参数不能一刀切。我用ultralytics训练时习惯这样配:

model.train( data="data.yaml", epochs=100, imgsz=640, batch=16, augment=True, mosaic=1.0, mixup=0.2, hsv_h=0.015, hsv_s=0.7, hsv_v=0.4, fliplr=0.5, scale=0.5, )

mosaic=1.0表示每轮训练里约100%的batch会做拼图,四张图随机缩放拼接成一张,对包含小目标的场景很有帮助。hsv_s=0.7是饱和度扰动,让模型在浓烟灰白和火焰橘红之间学会区分颜色边界。如果误报多,我一般会关掉fliplr,火灾现场左右对称性并不强,水平翻转可能让模型学到错误的空间关系。这些参数不是越大越好,mixup超过0.4后烟雾这种纹理本来就模糊的目标会变得更难收敛。

6.2 新场景复用与数据闭环

拿到这套数据集训练出基线模型后,如果要用到另一个车间、隧道或林场,不需要重新标注几千张。我的做法是只收集300到500张目标场景的图像,其中大部分是不含烟火目标的负样本,少量是新增角度的正样本。把这批图按同样格式放进images/train和labels/train,然后加载best.pt权重继续训练30到50轮。同一套数据划分不用重做,新增样本并进去之后跑同一份训练命令即可。这套数据下回来解压之后,也别急着开训,先花十分钟把labels和images对齐检查一遍,再跑第3章的流程。从那以后,我每次拿到新数据集都强制走一遍标签完整性检查加类别分布统计,再进训练,这套流程不花多少时间,但能省下至少半天调参功夫,希望帮到你。

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

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

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

立即咨询