尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

YOLO森林火灾检测:从数据集清洗到部署的完整避坑指南

YOLO森林火灾检测:从数据集清洗到部署的完整避坑指南 简介YOLO森林火灾数据集是一份面向目标检测任务的专业资源针对林火监测场景提供“不起火/火”两类目标标注覆盖YOLOv5、v7、v8、v9、v10及YOLO11等主流系列算法。数据集已完成训练集、验证集、测试集划分并附带data.yaml配置文件解压后即可直接用于模型训练与精度验证无需自行整理标注与路径配置。包内共包含2000个文件以XML标签文件为主同时提供YOLO格式TXT与VOC格式XML两种标注目标框坐标均已归一化至01免去图像尺寸换算适配不同输入分辨率文件命名中带有类别片段便于快速定位与筛选样本。压缩包整体约58.57MB体量适中适合本地反复下载与多次实验。目前已有308人学习浏览尤其适合需要森林烟火识别基准数据的初学者也能为研究者省去数据清洗和格式转换环节快速评估不同YOLO模型的检测效果。1. 这个数据集是什么YOLO森林火灾检测的第一个坑不在算法拿到“YOLO算法-森林火灾数据集-2860张图像带标签-不起火-火.zip”这个压缩包第一感觉是训练素材齐了2860张图、带标签、二分类YOLO直接开跑。但你要真这么干大概率会在验证集和部署阶段连续翻车。森林火灾检测的特殊性在于“火”是极稀疏的小目标一张高分辨率林区航拍图里可能只有几百像素的火焰而“不起火”类里全是阳光、反光、云雾、红色岩石这类视觉干扰物。这个数据集真正的价值不在数量而是逼你先处理类别不平衡和难负样本问题。适合谁适合刚接触YOLO目标检测、想用一个真实任务跑通“数据清洗→训练→部署”全流程的开发者也适合已经在做边缘端烟火识别但误检率下不来的人。本文会按这个思路带你走完整条链路。2. 解压后先别急着训练用脚本先看清2860张图的标签长什么样YOLO训练前最忌讳的是拿到 zip 直接扔给 train.py。森林火灾场景里火焰目标经常只有几十个像素标签框如果标注成整幅图模型学会的就不是火苗而是整张图的亮度模式如果类别顺序和 names 定义对不上训练过程会非常痛苦。所以第一步是把压缩包里的内容翻个底朝天确认三件事图片和标签是否成对、两个类别的样本量是否均衡、标注框坐标有没有明显越界。这半小时的检查能省掉后面一整天的排查。2.1 解压、查目录、确认图像和标签是成对出现的先建立独立目录避免压缩包把散文件混进你现有项目里。unzip fire-dataset.zip -d fire-dataset cd fire-dataset find . -maxdepth 2 -type d find . -name *.txt | wc -l find . -name *.jpg | wc -lunzip -d fire-dataset把内容解压到指定文件夹不会污染当前目录。find . -maxdepth 2 -type d打印两级目录结构能看出压缩包是否自带 images/labels 子目录。后面两个wc -l分别统计 .txt 和 .jpg 文件数。正常情况下标签数量应该等于图片数量如果你发现 .txt 比 .jpg 少很多说明有图片没有标注这类图片会被 YOLO 当作纯背景处理无妨但如果少了太多比如一半图片没标签那训练时就会频繁提示 “found no labels”。这时先别训回头找数据发布页面看是不是解压层级不对有些打包会多套一层目录。2.2 读标签文件统计两个类别各自的样本量这个数据集叫“不起火-火”但类别编号顺序不一定是你想的那样。部署时如果搞错映射报警逻辑就会整个反掉。用一段独立 Python 脚本把所有 .txt 标签扫一遍import os from collections import Counter label_dir fire-dataset/labels cls_count Counter() total_boxes 0 for fn in os.listdir(label_dir): if not fn.endswith(.txt): continue with open(os.path.join(label_dir, fn)) as f: for line in f: parts line.strip().split() if len(parts) 5: continue cls_id int(parts[0]) cls_count[cls_id] 1 total_boxes 1 print(类别编号分布:, cls_count) print(目标框总数:, total_boxes)脚本逻辑很简单遍历 labels 目录的所有 .txt每行是一个标注对象第 0 个字段是类别编号后四个是归一化坐标和宽高。统计结果里常见的情况是类别 0 有 1800 个、类别 1 只有 1060 个或者反过来。这里真正要看的是“火”样本是否属于少数类。如果火样本只占两成后续训练就要做重采样或者调 loss 权重。另外还要打开数据集里附带的 classes.txt 或 data.yaml 确认编号含义别靠猜。2.3 把框画到原图上肉眼确认标签没有张冠李戴类别统计只能证明文件存在不能证明标注质量。火焰标注最容易出的问题是框没贴住火苗、框跨了两个目标、火苗被整体圈成背景。用 OpenCV 把 10 张图的标注画出来看一眼import cv2 import os label_dir fire-dataset/labels image_dir fire-dataset/images class_names [non-fire, fire] # 以你自己的 classes.txt 为准 for idx in range(10): label_path os.path.join(label_dir, f{idx}.txt) image_path os.path.join(image_dir, f{idx}.jpg) img cv2.imread(image_path) h, w img.shape[:2] if not os.path.exists(label_path): continue with open(label_path) as f: for line in f: parts line.strip().split() if len(parts) 5: continue cls, xc, yc, bw, bh parts x1 int((float(xc) - float(bw) / 2) * w) y1 int((float(yc) - float(bh) / 2) * h) x2 int((float(xc) float(bw) / 2) * w) y2 int((float(yc) float(bh) / 2) * h) cv2.rectangle(img, (x1, y1), (x2, y2), (0, 0, 255), 2) cv2.putText(img, class_names[int(cls)], (x1, y1 - 5), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 0, 255), 1) cv2.imwrite(fcheck_{idx}.jpg, img)标签文件里存的是相对坐标范围在 0~1 之间画到图上必须乘以图片宽高。这一步能直观暴露两类问题一是框把整张图圈起来二是框中心点不在火苗上。如果发现 10 张里有 3 张以上存在明显错框这个数据集要用就得先清洗。清洗 YOLO 标签没有现成图形化工具时我一般会先用这段脚本把所有图导出成 JPEG再到 LabelImg 里手工重画只重标有问题的图。3. 把zip整理成YOLO标准格式目录划分、数据增强和类别平衡YOLOv8 默认要求的目录结构是 dataset 根目录下分 images 和 labels再各自分 train/val。大多数压缩包从标注工具导出时只有平铺的 images 和 labels 两个文件夹没有划分。YOLO 训练时如果没有指定验证集会直接拿训练集当验证集结果就是 mAP 虚高、部署时被打回原形。森林火灾数据集还有一个更隐蔽的坑类别不平衡。所以这章重点不是简单的文件移动而是保证验证集和训练集里“火”样本比例一致再用合适的数据增强撑住小目标检测。3.1 分层划分让火样本在train和val里都不缺席随机划分的坏处是可能把火样本大部分分到训练集验证集里只剩“不起火”图片。模型训练完验证集 loss 看着很低mAP 也高但一到真实森林环境就漏报。分层抽样按图片是否包含火目标做分组再分别按比例抽取能规避这个问题。import os import random random.seed(42) root fire-dataset val_ratio 0.2 images [f for f in os.listdir(os.path.join(root, images)) if f.endswith(.jpg)] def has_fire(img_name): label img_name.replace(.jpg, .txt) path os.path.join(root, labels, label) if not os.path.exists(path): return False with open(path) as f: for line in f: parts line.split() if parts and parts[0] 0: # 假设 0 是火 return True return False fire_imgs [i for i in images if has_fire(i)] non_fire_imgs [i for i in images if not has_fire(i)] random.shuffle(fire_imgs) random.shuffle(non_fire_imgs) val_fire fire_imgs[:int(val_ratio * len(fire_imgs))] val_non_fire non_fire_imgs[:int(val_ratio * len(non_fire_imgs))] train_fire fire_imgs[int(val_ratio * len(fire_imgs)):] train_non_fire non_fire_imgs[int(val_ratio * len(non_fire_imgs)):]这样划分后训练集和验证集里火样本的占比接近原始总体比例。parts[0] 0这一步依赖类别编号如果数据集里“火”的编号是 1把这里改成 1 就行。随机种子固定下来每次跑结果一致排查问题不会因为数据划分不同而玄学。划分完还要把文件复制进新目录mkdir -p fire-dataset/images/{train,val} fire-dataset/labels/{train,val}再用 Python 按刚才得到的四组名单批量复制注意复制标签时要把后缀从 .jpg 替换成 .txt。建议用复制而不是移动原始 zip 解压目录保留一份后面清洗样本时还有后悔药可吃。3.2 数据增强如何处理火焰小目标和夜间场景森林火灾数据集里夜间样本往往少得可怜而白天和夜间的火焰外观差异非常大。直接拿默认增强训练出来的模型放到夜间监控上会频繁误报白光和车灯。YOLO 提供了一系列在线增强参数但火情场景不能照搬通用目标检测的配置。参数推荐值理由hsv_v0.2~0.4模拟昼夜亮度差异但太大会把火焰增亮成白色误检率上升degrees5~10允许轻微旋转无人机视角和手持设备抖动都适用flipud0.0上下翻转会让天空和地面颠倒训练出来语义混乱fliplr0.5左右翻转风险小对火苗形状没有方向性依赖translate0.1模拟抖动偏移但超过 0.2 容易让小目标框跑出图像边界scale0.5模拟远近变化火焰大小变化极大建议开启在数据集中夜间样本不足 100 张时先在白天图上调低 hsv_v 模拟夜晚再用强 gamma 校正把暗部压下去比直接复制夜间图片过采样效果要稳。如果使用 YOLOv8 命令行训练这些参数可以写在训练命令里。数据增强不是越多越好本数据集的目标是区分“火”和“不容易火的东西”增强过度会让模型把高亮背景学成火苗。3.3 样本重采样对火样本过采样而不是简单复制当火样本只有 600 张而“不起火”有 2200 张时每个 batch 里大多数图片都是纯背景。模型会越来越倾向于输出“全部是背景”因为这样训练损失平均下来也低。最简单的类别均衡做法是构建一个自定义 Dataset在__getitem__里按概率多取含火图片。import random from ultralytics.data.dataset import YOLODataset class BalancedFireDataset(YOLODataset): def __getitem__(self, index): if self.fire_indices and random.random() 0.5: index random.choice(self.fire_indices) return super().__getitem__(index)使用方式是在训练脚本里替换默认的 dataset。这里fire_indices存储所有含火样本的下标每次抽样有一半概率从火样本里选另一半概率走原始随机路径。这个策略比简单复制图片稳健不会让某个火样本在训练中被反复看而产生过拟合。如果不想改代码也可以在训练命令里用mosaic0.5做平衡但 mosaic 增强会破坏火焰小目标的原有尺度效果不如直接过采样火样本。重采样最大的副作用是让验证集指标变得乐观因为验证集里火的比例也被提高了所以重采样后必须额外准备一组贴近真实分布的“野外测试集”做最终评估。4. 用YOLOv8跑通森林火灾训练五个必调参数与最小命令数据准备好以后进入训练阶段。我一般选 YOLOv8s 预训练模型作为起点而不是最大或最小的权重。v8n 在边缘端部署友好但小目标表现偏弱v8l 精度高3080 以下显卡跑起来很吃力且这个数据集只有 2860 张模型容量过大容易过拟合。v8s 是性价比均衡点。启动训练前先确认本机已经装好 ultralytics并准备好 yolo 预训练模型下载。yolov8s.pt首次运行时会自动下载到~/.config/Ultralytics/缓存目录如果环境离线需要先在能联网的机器上下载好再拷贝到目标机器。这个下载过程没有图形界面容易被忽略实际不少朋友第一次跑训练就卡在这里。4.1 准备data.yaml在数据集根目录放一个 data.yamlpath: /your/abs/path/fire-dataset train: images/train val: images/val nc: 2 names: [non-fire, fire]path建议写绝对路径。YOLO 解析相对路径时工作目录不同会直接找不到图片。train和val是相对于 path 的路径。names的顺序必须与标注文件里的类别编号一致如果标注时 0 是火names 里就要把 fire 写在第一位。很多新手把 names 写成 [fire, non-fire]但标签里 0 对应 negative训练到一半就会出现 target class index 与输出头不一致的报错那时再排查就费时间了。4.2 最小训练命令与五个核心参数yolo detect train \ modelyolov8s.pt \ datadata.yaml \ imgsz640 \ batch16 \ epochs100 \ patience20 \ lr00.002 \ optimizerAdamW \ cacheTrue这是我在 2860 张森林火灾数据集上的起步配置。关键参数说明参数作用建议与理由model预训练权重用 yolov8s.pt比 v8n 保留更多小目标特征imgsz输入尺寸640 平衡显存与小目标召回512 会漏掉小火焰batch每批图片数16 适合 24G 显存显存不够先降 batch 再降 imgszepochs训练轮数100 轮对 2860 张图足够再多会学背景纹理patience早停轮数20 轮 mAP 不涨就停避免夜间挂机浪费电lr0初始学习率0.002 起步默认 0.01 在这个小数据集上不稳4.3 看训练指标别把loss当真相训练完成后runs/detect/train/下会有results.png、confusion_matrix.png和best.pt。results.png 里最显眼的是train/box_loss曲线但它只反映模型对训练 batch 的拟合程度不代表真的学会了火苗。重点看两个指标metrics/mAP50(B)40% 以下说明模型没有真正抓住火焰目标。metrics/mAP50-95(B)火灾小目标多这个指标普遍偏低不必太过纠结。另一个值得看的是混淆矩阵也就是confusion_matrix.png。很多人在解读时发现所有格子相加不等于 1这其实是 Ultralytics 默认输出 raw counts 而不是归一化比例。要判断是否漏报直接看“真实为火但预测为背景”那一格的绝对数量如果 unmber 巨大说明召回率差回训练阶段调整类别权重如果“真实为背景但预测为火”数量大说明误报高部署时提高置信度阈值。5. 森林火灾训练与部署常见问题避坑清单以下五条是这类二分类火灾数据集在 YOLO 流程里最高频的问题按“现象、原因、解决”三段写逐个排查能省去大量试错。5.1 训练中loss变成NaN曲线突然断裂现象训练 loss 在某个 epoch 骤降成 NaN之后所有指标全部消失。原因最常见是学习率过高尤其在小数据集上用默认 lr00.01 时容易梯度爆炸其次是标签里有坐标越界值比如超过 0~1 的框在计算 loss 时产生 inf。森林火灾数据集的标签如果从某个标注工具导出偶尔会混入1.000001这类微小越界值。解决先把 lr0 降到 0.002如果还崩用下面命令筛查标签越界awk { if ($20 || $21 || $30 || $31) print } labels/*.txt输出越界行后定位对应图片重新标注。这类问题不作为 bug 处理而当成数据质量问题处理才彻底。5.2 mAP看着高到了夜间监控画面疯狂误报现象验证集 mAP 九成部署到夜间摄像头后反光、雾气、车灯全部报火警。原因训练集大部分是白天样本模型学到的是“高亮橙黄色区域”而不是“火”。夜间监控里的白色车灯、路灯反光都能满足这个视觉特征。这是典型的数据分布漂移不是模型调参能解决的问题。解决从训练集里抽出一部分白天“不起火”图片再单独收集 200~300 张夜间负样本放到验证集里重新评估。如果加入负样本后 mAP 崩溃说明模型原本的“高精度”只是假象。之后再把这些负样本补进训练集模型才开始真正区分。5.3 混淆矩阵总合不唯一现象confusion_matrix.png 里所有格子的数字加起来不是 1有人怀疑模型有问题。原因Ultralytics 的混淆矩阵默认是 raw counts显示的是样本数量不是归一化比例总和当然不是 1。如果启用 normalize 模式也不是所有类别都能加总为 1因为每行会单独归一化到各自类别的样本总数类别不平衡时总和必然不固定。解决不要关心总和。只看对角线相对数量和“背景被预测为某类”那一块的数量。如果背景误报数量明显高需要在 data.yaml 里给背景类增加负样本并考虑调高置信度阈值。5.4 训练中BN崩溃验证集mAP突然变成0现象训练前几轮正常某轮过后验证集输出全是背景precision 和 recall 同时归零。原因batch 太小时 BN 统计量抖动量太大尤其 batch4 或 batch2 时很常见。2860 张图的数据集如果用高分辨率很容易因为显存不够把 batch 调得过低从而触发 BN 崩溃。解决优先把 batch 固定在 16 以上。显存如果不够先降 imgsz 到 512再考虑梯度累积。YOLOv8 的 cacheTrue 也可以减少 CPU 读取瓶颈但对 BN 崩溃没有直接帮助。把 optimizer 换成 AdamW 并降低 lr0能降低 BN 崩溃概率。5.5 边缘设备上检测速度不达标误检率反而更高现象用训练好的权重导出 ONNX 后部署到 RK3588 或树莓派上帧率不到 15 FPS而且误检率比 PC 上更严重。原因输入分辨率 640 yolov8s 权重卷积计算量对边缘 NPU 偏大。更关键的是 ONNX 导出时没有做 INT8 量化模型仍然跑在 FP16 精度精度下降后原本处于置信度阈值边缘的小目标会被错误分类。解决边缘端老老实实用 yolov8n 重新训练或把训练好的模型做 INT8 量化。量化前准备至少 300 张与部署场景分布一致的校准图片白天夜晚都要有。导出命令yolo export modelbest.pt formatonnx opset12之后在 RKNN/TensorRT 工具里做 INT8 校准。部署时把置信度阈值从默认的 0.25 提高到 0.4误报率会明显降低代价是可能漏掉一些极小火点。防火场景宁可漏报一次也不希望摄像头天天乱响这个尺度要按现场情况反复调。6. 从模型文件到可用的火情检测导出部署和指标验证训练产出的 best.pt 只是权重要做成能实时报警的程序还需导出、加载并验证。这里说最常用的 ONNX Runtime 部署路径。6.1 导出ONNX并用图像测试输出yolo export modelruns/detect/train/best.pt formatonnx dynamicTrue导出后得到 best.onnx。写一个最小推理脚本import cv2 import numpy as np import onnxruntime as ort session ort.InferenceSession(best.onnx) input_name session.get_inputs()[0].name img cv2.imread(test.jpg) img cv2.resize(img, (640, 640)) img img[:, :, ::-1].transpose(2, 0, 1) # BGR转RGBHWC转CHW img np.ascontiguousarray(img, dtypenp.float32) / 255.0 inputs np.expand_dims(img, axis0) outputs session.run(None, {input_name: inputs})[0]outputs 包含候选框、置信度和类别 id。注意类别映射如果部署脚本里写cls 1代表火那 data.yaml 的 names 必须把 fire 放在第 2 位。这个映射关系在模型导出时已经固化部署时改 names 不会生效。6.2 在摄像头画面里画告警框实时检测可以用 OpenCV 读取视频流。我的做法是每 3 帧跑一次推理中间帧沿用上一次结果这样 CPU 占用会低很多。检测到火目标后画框并打印告警同时可以联动报警输出。这个脚本可以跑在 PC 上验证效果也可以移植到边缘设备。cap cv2.VideoCapture(0) while True: ok, frame cap.read() if not ok: break resized cv2.resize(frame, (640, 640)) # 推理得到 boxes, scores, class_ids for box, score, cls in zip(boxes, scores, class_ids): if score 0.4 and cls 1: cv2.rectangle(frame, (int(box[0]), int(box[1])), (int(box[2]), int(box[3])), (0, 0, 255), 2) cv2.putText(frame, ffire {score:.2f}, (int(box[0]), int(box[1]) - 5), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 0, 255), 1) cv2.imshow(fire, frame) if cv2.waitKey(1) 0xFF ord(q): break cap.release() cv2.destroyAllWindows()这段代码的阈值直接写 0.4比 YOLO 默认 0.25 高因为防火报警宁可少报不可乱报。如果你在测试时发现某个视频场景大量误报不要急着改代码先统计误目标特征再决定是调阈值还是补训练数据。6.3 验证模型是否达到防火监控的最低标准部署前不要只跑几张测试图就开始吹效果。用一个连续 10 分钟的真实场景视频做端到端验证记录三件事模型从火焰出现到第一帧报警的延迟、整个视频中的误报次数、平均帧率。火情召回率要求火出现后 2 秒内必须触发报警否则系统不具备实际价值误报率在 24 小时连续运行中控制在个位数以内才能让值班人员信任这套系统。我自己的习惯是先把摄像头装到现场录一个小时的原始视频拿回办公室跑离线检测边跑边数误报画面。等离线验证稳定了再部署到设备上做真机测试。这个流程看起来慢但能避免上线当天就被真实环境打脸。希望这份从数据校验到部署验证的完整思路能帮到你少走几趟弯路。本文还有配套的精品资源点击获取
返回列表