简介:YOLO系列算法的火灾与人员探测数据集,面向目标检测开发者和算法学习者,主要解决智能监控、消防预警、应急巡查等场景下的人、烟、火三类目标识别问题。压缩包内共2000个标注文件,包含VOC格式的XML与YOLO格式的TXT两种标签,每个目标框均记录类别索引与归一化中心点、宽高坐标,并划分好训练集与验证集,压缩包整体141.83MB,便于直接投入YOLOv5、YOLOv7、YOLOv8、YOLOv9、YOLOv10、YOLO11等主流版本的训练与测试。数据集附带data.yaml配置文件,省去自行整理路径和类别映射的步骤,同时标签文件名末尾带有类别关键词,便于快速了解目标类型,方便人工抽检与二次清洗。目前已有46人学习下载,适合正在备赛、做毕业设计或需要快速验证目标检测效果的开发者,可帮助缩短数据预处理时间、专注算法调优与模型部署。
1. 火灾和人员探测数据集:3039张带标签图像,人、烟、火三类怎么选
做火灾检测的同行都有体会:公开数据集里,要么是纯火苗特写,要么是实验室环境下的烟雾图,真正贴近监控视角、包含人员、烟雾、火焰三类目标且带干净标签的很少。这个标题里的数据集一共3039张图像,标注按人、烟、火三分类给出,图像量和类别粒度都处于一个适合算法调优的量级——既不像几万张的COCO那样训练周期长,也不是几百张那种过拟合到没法看。实际用YOLO算法跑这类任务时,模型结构往往不是瓶颈,数据集的标签质量和类别平衡才是决定mAP上限的关键。这篇就沿着数据体检、YOLOv8训练、烟火目标调优、验证与部署这条线,把一套能落地的流程讲透,适合正在做消防预警、安防监控、无人机巡检或相关算法POC的人参考。
2. 拿到zip先别训练:数据集结构与标签体检
2.1 先解压并核对目录与类目顺序
常见的YOLO格式数据集压缩包,解压后通常长这样:
unzip fire_person_dataset.zip -d ./fire_data cd ./fire_data tree -L 2fire_data/ ├── train/ │ ├── images/ │ └── labels/ ├── valid/ │ ├── images/ │ └── labels/ └── test/ ├── images/ └── labels/train、valid、test三个目录都各自包含images和labels子目录,标签文件是txt格式,与图像文件同名。先不要急着跑训练脚本,第一步应该在打开几个标签文件,确认类别id的真实含义:
cat train/labels/000001.txt输出可能是:
0 0.44140625 0.5234375 0.072265625 0.111328125 1 0.6822916667 0.2916666667 0.060546875 0.08203125 2 0.8359375 0.4453125 0.0390625 0.06640625这里每一行的第一个数字是类别id。绝大多数火灾数据集习惯把“人”放第0类,但也有一些来自Roboflow或自标注平台转换的数据集把“烟”设为0,把“火”设为1。如果类目顺序和你的预期不一致,直接套用预训练模型做迁移学习时分类头会错位,轻则训练混乱,重则推理结果完全没法用。我一般会先写一个一行命令统计标签类别分布:
cat train/labels/*.txt | awk '{print $1}' | sort | uniq -c看到三个类别(0、1、2)的数量比例接近时,说明数据基本平衡;如果某一类的数量远少于其他类,后面就要考虑做类别加权或针对性增强。
2.2 YOLO标签格式的四个归一化坐标
YOLO系列的txt标签格式是固定的,和PascalVOC的XML、COCO的JSON格式都不同。每行内容为:
<类别id> <x_center> <y_center> <width> <height>后四个数值全部基于图像宽度和高度做了归一化,取值范围在0到1之间。列一个字段说明表:
| 字段 | 含义 | 取值范围 | 典型值 |
|---|---|---|---|
| 第1列 | 类别id,从0开始 | 0 ~ 类别数-1 | 0代表人,1代表烟,2代表火 |
| 第2列 | 目标中心的x坐标,除以图像宽度 | 0.0 ~ 1.0 | 0.44 |
| 第3列 | 目标中心的y坐标,除以图像高度 | 0.0 ~ 1.0 | 0.52 |
| 第4列 | 目标框宽度,除以图像宽度 | 0.0 ~ 1.0 | 0.07 |
| 第5列 | 目标框高度,除以图像高度 | 0.0 ~ 1.0 | 0.11 |
理解归一化坐标还有一个实际用处:当你做图像超分辨率重建或裁剪增强时,标注必须跟着图像尺寸同步变换。比如把原图放大2倍,归一化坐标值不用改,但一旦你做了crop,就必须按裁剪区域重新计算每个目标框的坐标和面积占比,否则训练时损失函数计算出的IoU全是错的。
2.3 用脚本做一次标签体检
写一段脚本把整个数据集的标签质量过一次,重点查三类问题:坐标越界、标签与图像文件名错位、空标签文件。
import os from PIL import Image IMG_DIR = "train/images" LBL_DIR = "train/labels" IMG_EXTENSIONS = {".jpg", ".jpeg", ".png", ".bmp", ".webp"} bad_files = [] img_name_set = set() label_count = {} for fname in os.listdir(IMG_DIR): stem, ext = os.path.splitext(fname) if ext.lower() in IMG_EXTENSIONS: img_name_set.add(stem) for lbl_name in os.listdir(LBL_DIR): stem, ext = os.path.splitext(lbl_name) if ext != ".txt": continue # 检查同名图像是否存在 if stem not in img_name_set: bad_files.append(("image_missing", lbl_name)) continue lbl_path = os.path.join(LBL_DIR, lbl_name) img_path = None for ext in IMG_EXTENSIONS: candidate = os.path.join(IMG_DIR, stem + ext) if os.path.exists(candidate): img_path = candidate break if img_path is None: bad_files.append(("image_file_not_found", lbl_name)) continue img = Image.open(img_path) W, H = img.size with open(lbl_path, "r") as fp: lines = fp.readlines() if len(lines) == 0: bad_files.append(("empty_label", lbl_name)) continue for idx, line in enumerate(lines): parts = line.strip().split() if len(parts) != 5: bad_files.append(("bad_line_format", f"{lbl_name}:{idx+1}")) continue cls_id = int(parts[0]) xc, yc, w, h = map(float, parts[1:]) label_count[cls_id] = label_count.get(cls_id, 0) + 1 # 归一化坐标不应越界 if xc < 0 or xc > 1 or yc < 0 or yc > 1 or w < 0 or h > 1: bad_files.append(("coord_out_of_range", f"{lbl_name}:{idx+1}")) # 边界框预测目标中心应落在图像内 cx, cy = xc * W, yc * H if cx < 0 or cx > W or cy < 0 or cy > H: bad_files.append(("center_out_of_image", f"{lbl_name}:{idx+1}")) # 宽度高度占比与实际匹配(粗略检查) if w * W > W or h * H > H: bad_files.append(("box_larger_than_image", f"{lbl_name}:{idx+1}")) print("各类别统计:", label_count) print("异常数量:", len(bad_files)) for item in bad_files: print(item)这段脚本做了四层检查:文件名是否一一对应、是否有空标签、是否格式错位、归一化坐标与边界框是否合理。值得注意的是,中心点在图像内但边界框超出图像范围的情况在火灾数据集里很常见,因为烟雾是扩散状的,标注员把边缘烟雾标进去时不自觉把框拉到了画幅外。遇到这种样本,可以保留中心点在图像内、框面积超过图像但交并比损失仍能正确计算的标签,也可以直接删掉这种框,取决于你要不要利用画幅外的上下文语义。建议优先保留下载时的原始标签,只把“中心点落在图片外”的样本过滤掉。
3. 用YOLOv8训练自己的数据集:配置、命令与关键参数
3.1 数据集YAML配置与类名映射
YOLOv8训练自定义数据集,需要一个data.yaml文件。针对这个火灾人员探测数据集,可以写成:
path: /home/user/fire_data train: train/images val: valid/images test: test/images names: 0: person 1: smoke 2: fire这里要注意,path建议用绝对路径。用相对路径时,YOLOv8的解析逻辑以当前工作目录为基准,如果你在别的目录启动训练脚本,经常会出现dataset not found的报错。另外,train和val指向的是images目录而不是labels目录,框架会自动在同级目录下找labels文件夹。
3.2 训练集划分:别把同一场景的连续帧切到两个集合
标题里说3039张图像,如果下载的压缩包已经是train/valid/test拆分好的,直接用即可。但如果给你的是全部图像加标签的混合目录,需要自己划分时,务必注意:火灾视频抽帧出来的数据,相邻帧之间的背景高度相似,随机打乱切分会导致同一场景的帧同时出现在训练集和验证集里,val的mAP虚高很多。正确的做法是先按文件名前缀分组,再把整组分配到train或val:
import os import random from collections import defaultdict import shutil IMG_DIR = "all_images" LBL_DIR = "all_labels" TRAIN_RATIO = 0.85 # 假设文件名格式为 scene001_frame0001.jpg groups = defaultdict(list) for fname in os.listdir(IMG_DIR): if fname.endswith(".jpg"): scene_id = fname.split("_")[0] groups[scene_id].append(fname) random.seed(42) scene_ids = list(groups.keys()) random.shuffle(scene_ids) train_size = int(len(scene_ids) * TRAIN_RATIO) train_scenes = set(scene_ids[:train_size]) for i, (scene_id, files) in enumerate(groups.items()): split_dir = "train" if scene_id in train_scenes else "val" for fname in files: stem = os.path.splitext(fname)[0] shutil.copy(os.path.join(IMG_DIR, fname), f"{split_dir}/images/{fname}") shutil.copy(os.path.join(LBL_DIR, stem + ".txt"), f"{split_dir}/labels/{stem}.txt")监控摄像头固定机位下,火灾可能从一个小火苗慢慢蔓延到整个画面,前几帧的火焰面积远小于后几帧。如果按帧随机划分,模型会从训练集学到“小火苗”,又从验证集看到同一个场景同一时间段的大火苗,导致val loss波动巨大。按照场景分组划分更贴近真实部署场景——模型要面对的是没见过的视角和没见过的时间段。
3.3 训练命令与关键参数表
基于YOLOv8,最小可用的训练命令是:
yolo detect train \ data=fire_person.yaml \ model=yolov8s.pt \ imgsz=640 \ epochs=100 \ batch=16 \ patience=20 \ optimizer=AdamW \ lr0=0.001 \ augment=True从yolov8s.pt开始训练是迁移学习的做法。COCO上预训练好的特征提取器对边缘、纹理、颜色响应的初始化较好,火灾场景虽然不在COCO类别里,但低层特征的复用价值很高。如果直接从yolov8n.pt开始且数据量又小,容易在训练初期就出现loss震荡。
以下参数在这个数据集上值得手动调:
| 参数 | 推荐值 | 说明 |
|---|---|---|
| imgsz | 640或768 | 火焰小目标多时用768,但显存占用按平方增长 |
| epochs | 100起步 | 看val loss是否还有下降趋势,不要机械拉长 |
| batch | 能放进显存的最大值 | 显存不足时优先降imgsz,不要过度降batch |
| patience | 15~20 | 验证集指标连续不提升时提前停止 |
| mosaic | 1.0,最后10轮设0 | 小目标容易在mosaic中裁掉,最后阶段关闭可稳定精调 |
| hsv_h / hsv_s | 0.015 / 0.7 | 烟火颜色受光照影响大,适当的颜色增强提升泛化 |
| fliplr | 0.5垂直翻转 | 水平翻转会改变火焰上升方向,不建议对火使用 |
| copy_paste | 0.3 | 将烟/火目标粘贴到其他背景上,缓解小目标样本不足 |
训练结束后,模型权重保存在runs/detect/train/weights/best.pt。在火灾检测这类样本量较小的场景里,不要只看best.pt的val mAP,还要对比last.pt,如果两者差距很小,说明训练收敛平稳;如果best和last的mAP差了2个点以上,说明验证集划分可能偏小或数据分布不均。
3.4 训练日志里值得盯的三个信号
终端输出的loss分成box_loss、cls_loss、dfl_loss。经常出现的异常是:box_loss一直降,cls_loss却卡在某个平台期。这种情况在这类人员/烟火数据集上尤其常见——因为“人”这个类别外型差异小但尺度变化大,而“烟”恰好相反,半透明、无固定轮廓、颜色从白到黑都有。cls_loss不降说明分类头没有把烟和普通雾气、水蒸气区分开。常见的应对是增加一个epoch的Warmup,或把cls损失的系数调大一点。YOLOv8里可以通过改loss系数实现,但更省事的做法是回到数据层面,专门给烟这个类别加样本或做裁剪增强。还有一种做法是换yolov8m甚至yolov8l模型,把模型容量提上来,但火灾数据集通常只有几千张,大模型很容易在烟这个类别上过拟合,反倒是s和m这两个档位处于平衡点。
4. 误检与漏检重灾区:烟火的形态学特征与增强策略
4.1 烟火检测最难处理的三种样本形态
用YOLO算法跑通一次训练并不难,真正让工程落地困难的场景,集中在三类样本上。
第一类是半透明烟雾与背景的边界。烟雾没有锐利边缘,标注员在画框时本身就带有主观性,同一个烟团,两个人标出来的框可能差出30%的面积。这类标签噪声属于“固有噪声”,模型学到的是标注框的重心位置而不是真实的烟雾边界,所以你在验证集上看到的定位误差统计会明显偏大。
第二类是灯光、云层、白色建筑反射与火焰/烟雾的混淆。傍晚的阳光反射、路灯下的暖黄色光晕、监控画面里的白色水汽,在YOLO的特征空间里往往距离火焰很近。这不是靠改模型结构能完全消除的,需要对训练集做负样本增强,把这类易混淆图片作为背景图或粘贴源加入训练。如果数据包里没有提供负样本,可以自己从监控视频中截取包含灯光/云层的画面,放到train/images下且不配标签文件,让模型学会“这些不是目标”。
第三类是真正的早期小火苗,面积可能只有整张图的0.5%甚至更小。YOLOv8在640分辨率下,8倍下采样后原图中16×16像素以下的目标在feature map上只剩2×2个格点,特征响应极弱。针对这类小目标,有一个实用的前置手段是对标签面积占比较小的那一批图像做超分辨率重建,把图像放大到768或1024,同时用标签框面积按比例放大——这里要注意,归一化坐标本身不随缩放变化,但模型输入分辨率变大后,小目标占据的像素自然变多,能有效提升召回。如果数据量允许,也可以把这个超分模型集成到训练pipeline里,而不是只做离线增强。
4.2 用训练后的曲线反推最优置信度阈值
默认的置信度阈值是0.25,但这个值不一定适合每个类别。火和烟的样本特征差异很大,烟的外观变异性远大于火,同一个confidence阈值下,烟的precision可能已经掉到0.7,而火还在0.9以上。常见的做法是在验证集上对每个类别单独画Precision-Recall曲线,选择F1分数最大的点作为推理阈值。可以用YOLOv8的模型验证结果来做:
from ultralytics import YOLO model = YOLO("runs/detect/train/weights/best.pt") metrics = model.val( data="fire_person.yaml", imgsz=640, conf=0.001, # 故意给低阈值,让所有预测框都参与评估 iou=0.6 ) print(metrics.box.f1_curve)把conf设成0.001跑一次验证,得到的是完整F1曲线数据。YOLOv8的f1_curve返回的是不同置信度下的F1分数数组,配合metrics.box.ap_class_index可以定位每个类别的最优阈值。实际部署时,我一般会为“人”设定一个阈值(0.4左右),为“火”设定更高的阈值(0.5以上),为“烟”设定较低阈值(0.2~0.3),因为火灾预警场景里烟的漏检代价远高于误检代价。
4.3 训练后洗标签:把置信度高的预测框反查成新标签
这里的思路是:第一轮训练出的模型对多数样本已经有不错的检测能力,那些模型给出高置信度预测、但原始标签里没有对应框的位置,很可能就是标注遗漏。把这些候选位置提取出来,和原标签做IoU比对,小于0.1的视为“疑似漏标”:
import cv2 import numpy as np from ultralytics import YOLO model = YOLO("best.pt") results = model.predict("train/images", conf=0.6, iou=0.5, verbose=False) for res in results: img_path = res.path stem = img_path.split("/")[-1].split(".")[0] label_file = f"train/labels/{stem}.txt" orig_boxes = [] if os.path.exists(label_file): with open(label_file) as fp: for line in fp: parts = list(map(float, line.split())) orig_boxes.append(parts) orig_boxes = np.array(orig_boxes) if orig_boxes else np.zeros((0, 5)) for box in res.boxes: conf = box.conf[0].item() if conf < 0.6: continue cls_id = int(box.cls[0].item()) xywhn = box.xywhn[0].tolist() # 和已有标签做IoU检查 max_iou = 0.0 for ob in orig_boxes: if int(ob[0]) == cls_id: # 简单IoU计算逻辑,面积用xywhn直接算 inter = max(0.0, min(xywhn[0] + xywhn[2], ob[1] + ob[3]) - max(xywhn[0], ob[1])) \ * max(0.0, min(xywhn[1] + xywhn[3], ob[2] + ob[4]) - max(xywhn[1], ob[2])) union = xywhn[2] * xywhn[3] + ob[3] * ob[4] - inter max_iou = max(max_iou, inter / union if union > 0 else 0) if max_iou < 0.1: print(f"疑似漏标: {img_path}, class={cls_id}, conf={conf:.3f}, box={xywhn}")这里要控制conf阈值,0.6以上才作为候选,否则预测框本身存在大量误检,洗标签反而会引入新的噪声。产出的疑似漏标列表建议人工过一遍再写回标签文件,不要全自动合并。这个洗标签过程跑一轮就够,重复多轮会导致标签体系向模型自身预测偏移,越洗越丧失对“真实边界”的刻画能力。如果你的数据包里还附带未标注的原始图像,也可以先让模型预测,再人工筛选后作为新增训练样本喂回去,这比只围绕原标签打转效果好得多。
5. 验证与部署:用混淆矩阵和ONNX Runtime把模型压到边缘设备
5.1 验证结果不只是看一个mAP数字
训练完用下面的命令可以得到完整的验证结果:
yolo detect val \ model=runs/detect/train/weights/best.pt \ data=fire_person.yaml \ split=val结果目录里的confusion_matrix.png是第一个要看的图,不要只看mAP50。混淆矩阵能直观展示烟被误分为火、人被漏检的频率。火灾场景下最常见的混淆不是人和烟火之间的混淆,而是“烟”与背景之间的误检——矩阵里烟那一行除了真正例,背景占比往往很高,说明模型把大量树影、水汽、工厂排放当成了烟。一旦看到这种模式,优先加负样本,而不是急着调NMS参数。
5.2 导出ONNX与边缘部署推理
把训练好的权重导出为ONNX格式:
yolo export model=best.pt format=onnx imgsz=640 opset=12 simplify=True导出时opset建议用12或13,版本太高在Jetson等设备的TensorRT上可能不受支持。ONNX Runtime推理代码如下:
import cv2 import onnxruntime as ort import numpy as np sess = ort.InferenceSession("best.onnx", providers=["CUDAExecutionProvider", "CPUExecutionProvider"]) input_name = sess.get_inputs()[0].name input_shape = sess.get_inputs()[0].shape # [1, 3, 640, 640] img = cv2.imread("test.jpg") img_resized = cv2.resize(img, (640, 640)) blob = img_resized[:, :, ::-1].transpose(2, 0, 1).astype(np.float32) / 255.0 blob = np.expand_dims(blob, axis=0) preds = sess.run(None, {input_name: blob})[0] # [1, 84, 8400]这里拿到的输出维度是[1, 84, 8400],84表示4个坐标信息加80个COCO类别分数,换成这个数据集就是4加3类,后面处理时需要按自己的类别数切片。YOLOv8导出ONNX后输出端自带NMS——这里特别容易踩坑。如果你的版本导出的ONNX仍包含NMS节点,在边缘设备上这个节点会拖慢整体推理速度;如果要追求低延迟,可以导出时去掉聚合NMS,把原始preds拿回到后处理里用普通非极大值抑制逻辑处理,这样能省下几十毫秒,代价是后处理代码变多。对火灾预警这种多路摄像头上屏场景,CUDAExecutionProvider不一定比CPU快,因为小batch推理时数据拷贝的开销可能吃掉加速收益,优先在目标设备上各跑一遍再定provider。
5.3 现场优化技巧与快速验证清单
在边缘设备上把推理跑通之后,如果发现FPS不达标,我先查的不是模型本身,而是这三个点:
| 检查项 | 预期结果 | 不达标时的调整 |
|---|---|---|
| 输入分辨率 | 640x640 | 降到544或480,mAP损失通常0.3%以内 |
| 推理后端 | TensorRT FP16优先于ONNX Runtime | Jetson上用trtexec重新优化 |
| 预处理耗时 | 单帧小于5ms | 用cv2.resize加GPU上的仿射变换替代逐像素操作 |
| NMS后处理 | 单帧小于2ms | 用向量化numpy替代for循环,或降低max_det |
一组图像验证完成之后,可以专门截取一段监控视频按帧丢给模型,观察连续帧之间检测框的抖动程度。火焰和烟雾在视觉上是动态变化的,如果连续两帧之间同一个火源的框中心跳跃超过目标宽度的30%,说明模型对帧间漂移敏感,这时可以在后处理里对检测框做时间维度的平滑,而不是再去重新训练一个模型。
最后提醒一个容易被忽视的细节:从压缩包直接获取的数据集,原图分辨率不一定统一。YOLOv8训练时会自动做letterbox缩放,但验证集里如果有极端宽高比的图像,比如2.35:1的宽银幕抽帧,letterbox会引入大量黑边,影响小目标检测精度。这类图像建议单独处理,要么裁掉黑边再送入模型,要么在训练配置的letterbox参数里调整填充策略,别用默认的灰边填充硬扛。
本文还有配套的精品资源,点击获取