简介:这份资源面向计算机视觉入门者与安全监控、火灾预警方向的开发者,围绕烟火图像的识别与分类任务,提供从图像预处理、特征提取到模型训练与评估的完整实践素材。压缩包共3627个文件,以3617张bmp图像为主体,覆盖0与1两类烟火样本,另含xml标注、1张jpg示例图、1个py训练脚本、1个ipynb预处理与实验笔记及项目配置,整体约11.18MB,目录结构便于按类别直接读取数据。已有217人学习下载。读者可借助现成数据集与代码,快速复现去噪、增强、灰度化、二值化等预处理流程,对比SIFT、SURF与CNN特征提取思路,并基于SVM、随机森林或ResNet等模型完成训练、验证与超参数调优,同时结合准确率、精确率、召回率与F1分数评估效果,为烟花表演分析或实时烟火异常检测项目提供可复用的数据与排错参考。
1. 烟火图像的识别与分类:从误报到秒级响应的落地拆解
森林防火监控、城市高空瞭望、化工厂区安全巡检,这些场景里都绕不开同一个技术点:烟火图像的识别与分类。但真正做过的人都知道,难点从来不是“能不能识别出火”,而是“怎么把夕阳、车灯、红色工装、焊接火花和真实烟火区分开”。我见过太多项目在实验室里 mAP 跑到 0.9,一上现场就被晚霞和雾霾搞得满屏误报,值班员直接把告警关了。烟火识别本质上是一个小目标 + 类间差异极小 + 负样本极度不平衡的检测分类问题,它需要的不只是一个模型,而是一套从数据构造、模型选型到后处理抑制的完整链路。这篇文章面向想把这个方向真正落地的一线开发和算法工程师,我会把选型理由、可复现的训练流程、参数配置和踩过的坑都摊开讲,让你少走几个月弯路。
2. 烟火识别到底难在哪:数据、模型与场景的三重约束
2.1 烟火图像的四个技术特性决定了方案走向
先把这个任务的底层特性说清楚,不然后面选型全是拍脑袋。烟火图像和通用目标检测最大的区别在于四点。第一,早期火焰面积极小,在 1080P 画面里可能只有 20×20 像素,YOLO 下采样 32 倍之后特征几乎消失。第二,烟雾是半透明、无固定形状的,它没有清晰边界,纹理特征弱,和云、雾、水汽在灰度上高度相似。第三,类间差异极小,晚霞的橙红色和火焰的橙红色在 RGB 空间里可能只差十几个数值,车灯的高亮和火点的高亮也几乎一致。第四,负样本无穷无尽,你永远不知道下一个误报来自哪里——红色广告牌、反光玻璃、秋叶、焊光,每一种都能让模型翻车。
这四点直接决定了三件事:输入分辨率不能太低、数据增强必须针对小目标、后处理必须做时序和颜色空间的联合抑制。很多人一上来就套 COCO 预训练的 YOLO,结果小目标全丢,就是因为忽略了第一点。
2.2 检测还是分类:先分清你的任务边界
“识别与分类”这个说法其实包含两个子任务,落地时一定要拆开。**识别(检测)**是回答“画面里有没有烟火、在哪里”,输出的是边界框;分类是回答“这是烟还是火、是明火还是阴燃、是初期还是猛烈燃烧”,输出的是类别标签。实际工程里通常是“先检测后分类”的两阶段,或者用多类别检测头一次输出。
如果你的场景只需要报警“有火”,那单类别检测就够了,别过度设计。但如果要联动灭火系统、要区分烟雾和明火走不同的应急预案,那就必须做细分类。我一般会建议:第一版先做“烟/火/背景”三分类检测,跑通链路之后再往“白烟/黑烟/明火/阴燃”细分。因为细分类对标注质量要求极高,标注员自己都分不清阴燃和初起明火,强行上多分类只会引入噪声。
2.3 数据集从哪来:公开数据 + 自采 + 合成三条腿
没有数据一切免谈。公开的烟火数据集规模都不大,常见做法是拿几个公开集做预训练,再用自己场景的数据做微调。但自采数据有个致命问题:真实火灾样本极少,你不可能天天等着着火。所以合成和增强就变得关键。
我一般的配比是:公开数据占 30%,自采正样本占 20%,自采负样本(易混淆场景)占 40%,合成数据占 10%。负样本一定要下狠功夫,把现场所有会误报的东西都拍下来——夕阳、车灯、红色屋顶、焊接、蒸汽、扬尘。下面是一个把 VOC 格式标注转成 YOLO 格式的脚本,这是数据准备的第一步:
import xml.etree.ElementTree as ET import os # 类别映射:0=smoke 1=fire,背景不参与训练 CLASS_MAP = {"smoke": 0, "fire": 1} def voc_to_yolo(xml_path, img_w, img_h): tree = ET.parse(xml_path) root = tree.getroot() lines = [] for obj in root.findall("object"): name = obj.find("name").text.strip().lower() if name not in CLASS_MAP: continue # 跳过背景类和未定义类 cls_id = CLASS_MAP[name] 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) # YOLO 格式:中心点归一化坐标 + 宽高归一化 cx = (xmin + xmax) / 2.0 / img_w cy = (ymin + ymax) / 2.0 / img_h w = (xmax - xmin) / img_w h = (ymax - ymin) / img_h # 过滤掉宽高为 0 的脏标注,这是血泪经验 if w <= 0 or h <= 0: continue lines.append(f"{cls_id} {cx:.6f} {cy:.6f} {w:.6f} {h:.6f}") return lines这段代码的关键在最后那个宽高过滤。标注工具偶尔会产出 xmin 等于 xmax 的框,直接转过去会让训练时 loss 变 NaN,排查半天找不到原因。参数上,CLASS_MAP一定要和你的data.yaml里的names顺序严格对应,错一位整个训练就废了。归一化坐标保留 6 位小数足够,再多是浪费。
3. 模型选型与训练:从 YOLO 到注意力机制的取舍
3.1 为什么我优先选 YOLO 系列而不是 Faster R-CNN
烟火识别是典型的实时场景,监控视频 25 帧,你至少要做到 10 帧以上才有意义。Faster R-CNN 精度可能高一点,但推理速度在边缘设备上根本扛不住。YOLO 系列里,我一般从 YOLOv8 起步,因为它的工程化最成熟,导出 ONNX、TensorRT 都顺。但要注意,原版 YOLO 对小目标的检测头不够用,需要加一个 P2 层( stride 4 的高分辨率特征图)。
加 P2 的代价是计算量上升约 30%,显存多占 1.5G 左右。如果你的场景里烟火目标普遍大于 64×64,那不加也行;但森林防火这种远距离场景,P2 几乎是必须的。下面是一个 Ultralytics 风格的模型配置片段,展示怎么在 neck 里接 P2:
# yolov8-p2-fire.yaml 关键部分 backbone: - [-1, 1, Conv, [64, 3, 2]] # P1/2 - [-1, 1, Conv, [128, 3, 2]] # P2/4 # ... 后续层省略 head: - [-1, 1, nn.Upsample, [None, 2, "nearest"]] - [[-1, 6], 1, Concat, [1]] # 融合 P2 特征 - [-1, 3, C2f, [128]] # P2 检测头输入参数说明:[64, 3, 2]表示输出通道 64、卷积核 3、步长 2。P2 层对应的 stride 是 4,能保留更多小目标细节。但别盲目堆,P2 层特征图很大,显存吃紧时优先降 batch size 而不是砍 P2。
3.2 训练参数怎么设:一份可直接抄的配置
训练烟火模型,学习率和锚框是两个最容易翻车的点。我一般用 SGD 而不是 AdamW,因为 SGD 在检测任务上泛化更稳。初始学习率设 0.01,用 cosine 衰减到 0.0001。warmup 开 3 个 epoch,避免一开始就震荡。锚框一定要用你的数据集重新聚类,COCO 的默认锚框和烟火尺寸分布差太远。
# 训练命令,基于 Ultralytics yolo detect train \ data=fire_smoke.yaml \ model=yolov8s-p2-fire.yaml \ epochs=200 \ imgsz=960 \ batch=16 \ lr0=0.01 \ lrf=0.0001 \ warmup_epochs=3 \ mosaic=1.0 \ mixup=0.1 \ copy_paste=0.3 \ degrees=10.0 \ translate=0.1 \ scale=0.5 \ fliplr=0.5 \ device=0逐项说:imgsz=960是为了小目标,别用 640;mosaic=1.0增强小目标上下文,但最后 10 个 epoch 要关掉,否则框的分布和真实场景不一致;copy_paste=0.3对小目标特别有效,它把目标抠出来贴到别的图上,等于免费扩充正样本;scale=0.5让目标尺度变化更丰富。mixup别开太大,0.1 够了,开大了烟雾的半透明特性会被破坏。
3.3 分类头怎么加:烟与火的细分类实现
如果你要做烟/火细分类,有两种做法。一是多类别检测头,直接在 YOLO 的 cls 分支输出 2 类;二是检测后裁剪 ROI 再送一个分类网络。前者简单,后者精度高但慢。我一般先用前者跑 baseline,如果烟和火混淆严重,再上后者。
多类别检测只需要改data.yaml的nc: 2和names: ['smoke', 'fire'],其余不用动。但要注意,烟和火的样本数量往往极不平衡,火样本通常远少于烟样本。这时候要在 loss 里加类别权重,或者对火样本做过采样。我一般用cls_pw=0.5这种类别权重参数,让少数类获得更高梯度。
4. 避坑与排查:那些让模型在现场翻车的细节
4.1 晚霞和车灯误报:颜色空间与时序联合抑制
现象:模型在傍晚时段疯狂报警,把晚霞识别成火焰;夜间把车灯识别成火点。原因:RGB 空间里晚霞和火焰的色相几乎重叠,单帧模型无法区分。解决:加 HSV 颜色约束 + 多帧时序确认。火焰的饱和度通常高于晚霞,且火焰区域在连续帧里有闪烁抖动,而晚霞是静止的。我一般会在后处理里加一个 3 帧确认窗口,连续 3 帧都检出才报警,误报率能降 70% 以上。
4.2 小目标漏检:别只怪模型,先查标注
现象:远处的小火点总是漏检,模型明明加了 P2 还是不行。原因:很多时候不是模型问题,是标注问题——标注员把小目标框画大了,或者干脆漏标了。解决:用脚本统计标注框的尺寸分布,如果小于 32×32 的框占比异常低,说明标注有系统性遗漏。另外,训练时把imgsz提到 1280 再试一次,如果 recall 明显上升,那就是分辨率不够,不是模型结构问题。
4.3 烟雾和云雾混淆:负样本要“像”而不是“多”
现象:山区的雾、工厂的蒸汽被识别成烟雾。原因:负样本里缺少和烟雾形态相似的样本,模型没学过“像烟但不是烟”的东西。解决:负样本不是越多越好,而是要难负样本。专门去采集晨雾、蒸汽、扬尘、汽车尾气这些和烟雾高度相似的场景,每个场景至少 200 张,加进训练集。我一般会把难负样本的 loss 权重调高,让模型重点学这些边界。
4.4 训练 loss 震荡不收敛:检查锚框和数据
现象:训练到 50 epoch loss 还在剧烈震荡,mAP 不涨。原因:八成是锚框和你的数据尺寸分布不匹配,或者数据里有脏标注。解决:先用 k-means 重新聚类锚框,命令是yolo detect train ... anchors=9让它自动聚类。然后跑一遍数据校验脚本,把宽高为 0、坐标越界、类别 ID 超范围的标注全清掉。这两个动作做完,90% 的震荡问题能解决。
4.5 边缘设备推理慢:量化和剪枝的取舍
现象:模型在服务器上跑得好好的,一上 Jetson 或瑞芯微就掉到 3 帧。原因:没做量化,FP32 模型在边缘芯片上算力吃紧。解决:优先做 INT8 量化,用 TensorRT 或 RKNN 工具链。量化后精度一般掉 1-2 个点,但速度能翻 3 倍。如果精度掉太多,试试只量化 backbone,检测头保留 FP16。剪枝要谨慎,烟火模型本身参数就不多,剪狠了小目标直接丢。
5. 进阶技巧:用测试时增强和模型集成把召回率再拉一截
前面讲的都是单模型链路,如果你对召回率有极致要求——比如森林防火这种漏报代价极高的场景——那可以上测试时增强(TTA)和模型集成。TTA 的思路是推理时对同一张图做多种变换(翻转、多尺度),把结果融合。Ultralytics 里直接加augment=True就能开,但速度会慢 2-3 倍。我一般只在告警确认环节用 TTA,第一遍快速筛,第二遍对疑似目标做 TTA 复核。
模型集成更直接:训一个 YOLOv8s 和一个 YOLOv8m,推理时用 WBF(加权框融合)合并结果。WBF 比 NMS 更适合集成,因为它能保留不同模型的互补框。下面是一个简化的 WBF 融合逻辑:
def weighted_box_fusion(boxes_list, scores_list, iou_thr=0.55): # boxes_list: 每个模型的框列表 [N,4] # scores_list: 对应的置信度 [N] all_boxes = [] all_scores = [] for boxes, scores in zip(boxes_list, scores_list): for box, score in zip(boxes, scores): all_boxes.append(box) all_scores.append(score) # 按置信度降序,逐个融合 IoU 超阈值的框 order = sorted(range(len(all_scores)), key=lambda i: -all_scores[i]) fused = [] for i in order: matched = False for f in fused: if iou(all_boxes[i], f["box"]) > iou_thr: # 加权平均坐标,权重为置信度 w1 = all_scores[i] w2 = f["score"] f["box"] = [(a * w1 + b * w2) / (w1 + w2) for a, b in zip(all_boxes[i], f["box"])] f["score"] = max(f["score"], all_scores[i]) matched = True break if not matched: fused.append({"box": all_boxes[i], "score": all_scores[i]}) return fused参数上,iou_thr设 0.55 是个经验值,设太高融合不充分,设太低会把相邻的两个火点错误合并。集成带来的召回提升通常在 3-5 个点,但推理成本翻倍,所以只建议在算力充裕的服务端用,边缘端还是老老实实单模型加 TTA。
最后说个我自己的习惯:每次模型上线前,我一定会拿过去三个月的误报录像跑一遍回归测试,看看新模型有没有把老问题重新引入。烟火识别这个方向,模型精度只是及格线,真正的功夫在数据闭环和误报抑制上。我踩过最大的坑就是太信 mAP,忽略了现场值班员的真实体验。希望帮到你。
本文还有配套的精品资源,点击获取