简介:面向计算机视觉开发者、消防安全领域研究人员以及需要快速部署图像火灾识别方案的工程人员,这套基于Python的火灾检测卷积神经网络模型源码,提供图像火焰识别的完整训练与推理示例,便于在监控场景中快速分析火灾特征。压缩包共23个文件、约1.9MB,以8个Python脚本(模型定义、转换、验证及超像素处理)、6张结果示意图、Markdown说明、Shell下载脚本、YAML工作流配置等为主,覆盖数据准备、模型训练、验证到模型导出的主要流程,目录结构清晰。源码包含FireNet与Inception V1/V3/V4迁移学习模型,superpixel脚本可结合SLIC示例图观察预处理效果;download-dataset.sh和download-models.sh便于一键获取数据集与权重,pythonapp.yml提供自动化CI验证思路。已有444人学习下载,适合希望复现火灾检测实验或继续改进模型的中高级Python工程师。
1. 火灾检测CNN模型最容易踩的坑:先看这份Python源码怎么拆
火灾检测CNN模型最容易踩的坑,是把网络堆得又大又深,以为靠模型拟合就能解决小火苗漏检。我拆完这套基于Python的火灾检测CNN模型设计源码之后,反而觉得它最有价值的地方是给出了完整的对比链路:轻量的FireNet和InceptionV1/V3/V4的OnFire变体放在同一套验证脚本下,数据、预处理、评估口径完全一致,差多少一目了然。里面除了模型定义,还带数据集下载脚本、模型转换脚本、验证脚本和SLIC超像素预处理脚本,不是那种只有一个py文件的半成品。适合正在复现火灾检测实验、要把模型部署到树莓派这类边缘设备,或者想拿现成Python源码快速跑通CNN识别流程的开发者。先把这23个文件按用途分清楚,后面复现才不会迷路。
2. FireNet与InceptionVxOnFire:小模型与大骨干的取舍怎么选
2.1 火焰特征决定了CNN的基本结构
火灾检测里真正可用的视觉信号,集中在颜色分布、边缘纹理和局部亮度梯度上。火焰的颜色让R/G通道比值在局部区域明显偏高,边缘轮廓不规则而且没有固定几何形状,和背景之间的对比度通常很高。CNN在火灾检测里能做对,是因为卷积层用局部感受野滑过整张图,第一层学到颜色块和简单边缘,后续层组合出不规则纹理特征,这个过程比传统颜色阈值分割稳定得多。
这套源码同时放了FireNet和InceptionV1/V3/V4的OnFire变体,设计意图是给不同算力场景留选择余地。FireNet是几条卷积加池化堆出来的小模型,参数量小,推理延迟低,适合树莓派、监控盒子这类边缘设备做实时粗筛;InceptionVxOnFire走的是Inception模块多尺度卷积路线,1x1、3x3、5x5的卷积核并行提取不同感受野的信息,对复杂背景和远处小火苗的召回能力更强,但计算量也明显上去了。它俩不是“谁比谁强”的关系,而是精度、速度、部署成本的取舍。
| 模型系列 | 特征提取策略 | 部署倾向 | 典型应用场景 |
|---|---|---|---|
| FireNet | 常规卷积块+池化堆叠,参数少 | 边缘设备、低延迟 | 实时视频流逐帧粗筛 |
| InceptionV1/V3/V4 OnFire | Inception多尺度并行卷积 | 服务器GPU、离线批量 | 高精度复核、论文基准对比 |
仓库里的InceptionV1-OnFire.png、InceptionV3-OnFire.png、InceptionV4-OnFire.png和FireNet.png就是这四个模型的架构图,读代码之前建议先把这几张图对照着看一遍,至少把卷积层和分类头的边界在图上找出来,再进源码会顺很多。
2.2 从readme.txt开始读源码:入口顺序和核心文件职责
这套源码的阅读顺序我建议是:先打开readme.txt看作者给的复现说明,再看requirements.txt确认依赖,然后看firenet.py和inceptionVxOnFire.py里的Input shape和类别数,接着看validation脚本确认模型加载方式,最后看conversion脚本和superpixel脚本。千万别一上来就翻download-dataset.sh,那个脚本下载的数据集不一定匹配你本地目录结构,后面对不上会非常折腾。
firenet.py这种文件,核心结构通常是标准的小型CNN,可以概括成下面这种写法,实际代码里会有具体的卷积核和尺寸配置,但骨架大同小异:
# firenet.py 的典型结构,对应仓库里的同名文件 from tensorflow.keras import layers, models def build_firenet(input_shape=(64, 64, 3), num_classes=2): inputs = layers.Input(shape=input_shape) # 第一层卷积:提颜色块和粗略边缘 x = layers.Conv2D(16, (3, 3), activation="relu", padding="same")(inputs) x = layers.MaxPooling2D((2, 2))(x) # 第二层卷积:组合出不规则火焰纹理 x = layers.Conv2D(32, (3, 3), activation="relu", padding="same")(x) x = layers.MaxPooling2D((2, 2))(x) # 全局平均池化替代Flatten+Dense,大幅压参数 x = layers.GlobalAveragePooling2D()(x) outputs = layers.Dense(num_classes, activation="softmax")(x) return models.Model(inputs, outputs)这里要注意几个细节。第一,Input层的shape里不写batch size,实际训练时由数据决定,所以改输入尺寸只需要动这里。第二,Conv2D的padding="same"是为了让特征图尺寸在卷积后不缩水,小模型普遍这么设置。第三,GlobalAveragePooling2D是嵌入式部署的关键姿势,它把每个通道压成一个值,再接一个Dense,比Flatten再接几百个神经元的全连接层省下大量参数。如果你想换成自己的数据集,只需要改num_classes和输入高宽,但改完必须重新训练,不能直接加载这套预训练权重,否则最后会报shape不匹配。
2.3 训练态与部署态分离:validation和conversion为什么要拆开
这组源码里最值得学的是把模型生命周期拆成了两个阶段。h5文件是训练态产物,firenet-validation.py和inceptionVxOnFire-validation.py负责加载h5,在测试集上算准确率、召回率、F1,属于验证阶段;inceptionVxOnFire-conversion.py和firenet-conversion.py负责把训练好的h5导出成SavedModel或TFLite,属于部署阶段。
两者拆开的原因很简单:h5格式严重依赖Keras运行环境,TensorFlow版本一升级,旧h5可能直接加载失败,这在后面避坑章会具体说;而SavedModel和TFLite是标准交换格式,TensorFlow Serving、TFLite Runtime、甚至不少嵌入式推理框架都能直接消费。不要拿h5文件直接上线,这是个血泪教训。
另外,仓库里的pythonapp.yml是GitHub Actions的CI配置,它只能做语法检查、依赖安装和轻量冒烟测试,跑不了全量数据集。你要复现精度数字,必须自己拉数据和权重,CI不是干这个的。我一般会先把validation结果跑出来存档,再做conversion,转完再对一遍输出,这样每一步都有独立产物,出了问题能快速定位到底在哪个环节。
3. 数据准备与SLIC超像素预处理:搭一条能反复跑的流水线
3.1 环境准备:Python版本、虚拟环境与requirements.txt
这套源码是基于Python的CNN项目,代码本身不挑系统,但依赖需要装对。先建虚拟环境,别把TensorFlow这种重依赖直接洒到系统Python里。如果你刚入门Python,这一步尤其重要:虚拟环境隔离了项目依赖,后面不管你装opencv还是scikit-image,都不会污染其他项目。VSCode里配置Python环境时,直接选.venv这个解释器路径即可,终端里也要先激活再跑脚本。
# 建议用Python 3.8+,先建虚拟环境再装依赖 python -m venv .venv source .venv/bin/activate # Windows下用 .venv\Scripts\activate pip install --upgrade pip pip install -r requirements.txtrequirements.txt里通常会锁住TensorFlow、numpy、opencv-contrib-python、scikit-image这些核心库的大版本。注意opencv-contrib-python不是普通的opencv-python,SLIC超像素的ximgproc模块在contrib包里,装错会导致后面导入报错。装完依赖后,用pip list核对关键包的版本,别跳到下一步才发现TensorFlow和Keras对不上。
3.2 download-dataset.sh与download-models.sh:先让下载脚本可复跑
数据准备是整个复现流程里最容易被低估的一步。下载脚本一般需要先给执行权限再运行,里面用wget或者curl拉取数据集压缩包和预训练权重。脚本本身逻辑不复杂,但实际运行中有个很现实的坑:如果下载源服务器离得远或网络波动大,wget默认失败就退出,进度条卡在0字节是很常见的。
# 先给执行权限,再跑数据集下载 chmod +x download-dataset.sh download-models.sh ./download-dataset.sh ./download-models.sh如果脚本跑不动,我一般会改成curl的断点续传写法,把URL换成可用的镜像或自己内网的对象存储地址:
# 网络不稳定时的续传下载写法 curl -L -C - -o FireDataset.zip "https://your-mirror.example.com/FireDataset.zip" unzip -q FireDataset.zip -d datasets/这里的-C -是断点续传,下载中断后可以重跑同一个命令接着下;-L是跟随重定向;-o指定保存文件名。下载完数据集和预训练权重后,先解压并确认目录结构,再看readme.txt里的数据目录约定,很多报错都源于数据集路径和脚本里写死的路径不一致。
3.3 superpixel-inceptionVxOnFire.py:SLIC超像素解决小火苗漏检
为什么要上SLIC超像素?因为火灾检测的场景里,火焰区域在整张图中往往占比很小。如果把整图直接resize到64x64或224x224,远处的小火苗可能只剩几个像素,卷积层根本提不出有效特征。SLIC先把图像分割成感知均匀的超像素小块,每一块内部颜色和纹理高度一致,然后逐块判断有没有火焰,最后按块投票决定整图是否报警。仓库里的slic-stages.png和slic-ex.png就是这类分割阶段的可视化结果,跑代码之前先看图能更快理解它在干什么。
# superpixel-inceptionVxOnFire.py 的推理骨架(以opencv ximgproc实现为例) import cv2 def slic_segments(image_path, n_segments=250, compactness=15): img = cv2.imread(image_path) img_rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) # 生成超像素分割,SLICO模式不要求预设超像素个数 slic = cv2.ximgproc.createSuperpixelSLIC( img_rgb, algorithm=cv2.ximgproc.SLICO, region_size=20, ruler=15 ) slic.iterate(10) # 迭代次数越多,分割越稳定,但耗时越高 labels = slic.getLabels() segments = slic.getNumberOfSuperpixels() return img_rgb, labels, segments这里的region_size控制超像素的初始尺寸,值越大超像素块越大、数量越少;ruler控制分割的规整度,值越大越接近规则网格;iterate(10)是迭代次数,一般10次左右在速度和稳定性之间比较平衡。超像素个数直接决定后续推理开销,每个segment都要单独喂给模型一次,n_segments拉到上千会让整个验证慢到怀疑人生。我的经验是:先在全图上跑一次FireNet粗检,只有命中fire的局部区域才做SLIC细筛,这样重计算只发生在该发生的地方,而不是每帧无脑全图分割。
4. 模型转换与验证:从h5到SavedModel/TFLite的完整落地路径
4.1 conversion脚本的职责:把训练态模型变成部署态产物
inceptionVxOnFire-conversion.py和firenet-conversion.py干的是同一件事:加载训练好的h5权重,导出成TensorFlow Serving和边缘端能直接用的格式。常见做法是同时导出SavedModel和TFLite两个版本,前者给服务器端的TensorFlow Serving,后者给树莓派、Android这类移动和嵌入式场景。核心代码一般长这样:
# conversion脚本的常见姿势:h5 -> SavedModel -> TFLite import tensorflow as tf model = tf.keras.models.load_model("models/inceptionV1OnFire.h5") # 导出SavedModel格式,目录里包含变量和推理图 model.save("saved_model/inceptionV1OnFire") # 从SavedModel继续导出TFLite converter = tf.lite.TFLiteConverter.from_saved_model("saved_model/inceptionV1OnFire") converter.target_spec.supported_ops = [tf.lite.OpsSet.TFLITE_BUILTINS] tflite_model = converter.convert() with open("models/inceptionV1OnFire.tflite", "wb") as f: f.write(tflite_model)model.save会把模型结构、权重和推理图打包成一个目录,而不是单个文件,这是SavedModel和h5的重要区别。TFLiteConverter的target_spec.supported_ops用来约束算子集合,如果模型里有不支持的算子,转换时会直接报错,这时可以稍后把supported_ops扩展到SELECT_TF_OPS再试。转换产物按用途落地,见下表:
| 产物 | 格式 | 消费方 | 典型部署位置 |
|---|---|---|---|
| 原始h5 | 单文件 | Keras/TF训练环境 | 训练机、实验机 |
| SavedModel | 目录 | TensorFlow Serving、TFLite转换 | 服务器、云环境 |
| TFLite | 单文件 | TFLite Runtime、OpenCV | 边缘设备、嵌入式设备 |
4.2 validation脚本的正确打开方式:指标比Loss更可信
跑验证前先搞清楚这个脚本在加载什么。它加载的是原始h5,不是转换后的TFLite,所以验证结果只代表训练态模型的精度。命令行入口一般是这种结构:
# 跑一次验证,得到准确率、召回率、F1和误检样例 python inceptionVxOnFire-validation.py \ --model models/inceptionV1OnFire.h5 \ --data datasets/FireDataset/test验证脚本里通常会输出一整套评估指标,除了准确率还会包含召回率和F1,这对火灾检测特别重要。火灾检测里的正类是fire样本,漏检的代价远比误报高,所以只看准确率会有欺骗性,比如测试集里95%是no-fire样本,模型全猜no-fire也能拿到95%准确率,但这个模型没有任何实用价值。验证脚本一般还会把预测错误的图片路径打印出来,这部分信息一定要保留,它能告诉你模型是在哪些场景下翻车的:是光线暗的室内、还是浓烟遮挡的初期火情。
4.3 推荐复现顺序:先验证、再转换、最后对比logits
复现这套源码时,强烈建议按三步走,不要跳过任何一步:
# 第一步:验证原始h5精度 python firenet-validation.py --model models/firenet.h5 --data datasets/FireDataset/test # 第二步:转换为SavedModel/TFLite python firenet-conversion.py # 第三步:用同一批测试图对比h5和TFLite的输出 python compare_outputs.py --h5 models/firenet.h5 --tflite models/firenet.tflite第三步很多人会忽略。转换过程不是无损的,尤其是TFLite的量化可能会让输出概率偏移,所以必须在同一批图片上对比原始h5和导出模型的输出。常见做法是允许1e-3量级的浮点误差,如果偏差超过这个范围,大概率是预处理不一致或者量化丢了太多精度,需要回到转换步骤检查。
这个顺序的价值在于,每一步都有独立产物,出问题能快速分割责任边界:h5验证不达标,是数据或训练的问题;转换后对不上,是转换参数或预处理的问题。不要为了省时间把验证和转换揉在一起跑,否则精度下降时你会陷入两边都像有问题的排查泥潭。
5. 避坑排查:火灾检测项目里最常见的五个翻车现场
这套源码文件虽然不多,但踩坑点集中在数据下载、版本兼容、格式转换和预处理一致性上。下面这五条是我复盘下来最需要提前知道的。
5.1 下载脚本卡在0字节
现象:运行download-dataset.sh后,压缩包大小一直是0,脚本半天不结束。
原因:脚本写死的下载地址响应不稳定,wget默认没有重试和续传逻辑,连接超时后不会自动恢复。
解决:换成curl续传写法,curl -L -C - -o FireDataset.zip "下载地址",中断后重跑同一个命令会从断点继续。如果数据集很大,建议先下载到本地,再手动放进datasets目录,别依赖脚本每次都重新拉全量。
5.2 h5加载报Unknown layer或shape mismatch
现象:validation脚本在load_model处直接抛ValueError: Unknown layer,或者加载成功后前向推理报shape不一致。
原因:TensorFlow版本升级后,Keras的h5序列化内容发生了变化,旧模型里的自定义层或旧算子在新版本里找不到对应实现。
解决:严格按requirements.txt安装依赖,不要用自己的全局TensorFlow环境跑。如果项目没给版本,就用TensorFlow 2.10之前的版本搭配对应Keras;加载时如果报Unknown layer,在load_model调用里加custom_objects参数,把模型定义里的自定义层类和名称传进去。改这个文件之前先确认Input shape和训练时一致。
5.3 OpenCV的dnn模块加载SavedModel失败
现象:用cv2.dnn.readNetFromTensorflow加载conversion导出的模型,报错说找不到某些算子或者图形状对不上。
原因:OpenCV的dnn模块只支持TensorFlow算子的一个子集,Keras模型导出时有些算子没有映射到OpenCV能识别的格式,尤其是带自定义层的模型几乎必翻车。
解决:换路径。最省事的是直接用TensorFlow Serving的REST接口做推理,或者把SavedModel用tf2onnx转成ONNX,再用OpenCV的dnn模块读取ONNX文件。这个小模型走ONNX中转通常很顺,但要注意ONNX导出时输入输出的节点名要记下来,OpenCV读的时候要用正确的blob命名。
5.4 TFLite输出和h5原始输出差异大
现象:同一张测试图,h5输出fire概率0.87,TFLite输出只有0.31,差异远超浮点误差范围。
原因:TFLite默认可能启用动态范围量化,权重从float32压到int8,精度会有损失;更常见的是输入预处理不一致,h5推理时做了归一化到[0,1],而TFLite推理时忘了做,或者resize插值算法从双三次换成了最近邻。
解决:先用converter.optimizations = [tf.lite.Optimize.DEFAULT]配合FP16,保留更高精度;如果必须用INT8量化,需要准备校准集让转换器统计激活值范围。预处理部分,把两边的resize插值算法和归一化写法逐一比对,这一步最容易藏雷。
5.5 SLIC批量验证慢到怀疑人生
现象:superpixel脚本对单张图片处理还算正常,但跑完整测试集要几个小时,CPU占用还拉不满。
原因:脚本在循环里对每张图重新创建SLIC实例,而且每张图被切成几百个超像素块,每块都要单独过一遍模型,计算量成倍放大。图片分辨率越高,问题越严重。
解决:先降薪再干活。第一,把输入图片统一resize到脚本预期的尺寸,别让SLIC在高分辨率原图上跑;第二,用FireNet先做整图粗检,只有命中fire的区域才切成超像素细筛;第三,n_segments控制在200附近就好,盲目拉大既增加耗时又不显著提召回。
5.6 真正出问题时先查哪三个地方
排查顺序比技巧更重要。我的习惯是:先看输入预处理,再看模型输入shape,最后看依赖版本。预处理包括resize插值算法和归一化范围,90%的推理结果异常都出在这;输入shape对不上会导致报错或静默的广播错误,这个用model.summary()一眼就能确认;依赖版本问题就回到第5.2条,重装requirements.txt后再跑一次。按这个顺序查,能避免在转换参数和网络结构上浪费时间。
6. 进阶用法:把验证过的FireNet接进摄像头实时推理
6.1 滑动窗口多数投票:压掉单帧误报
验证通过之后,最顺手的进阶就是把FireNet接到摄像头实时流上。火灾检测误报很常见,因为闪光、灯光反射、甚至橙色衣服都可能让模型给出高概率。单帧判决不稳是常态,解决办法是维护一个滑动窗口做时域投票:
# 实时推理:每5帧抽1帧,用多数投票跳过单帧误报 import cv2 import numpy as np import tensorflow as tf model = tf.keras.models.load_model("models/firenet.h5") cap = cv2.VideoCapture(0) window = [] while True: ret, frame = cap.read() if not ret: break if len(window) < 5: # 预处理必须与训练保持一致:resize到模型输入高宽 resized = cv2.resize(frame, (64, 64), interpolation=cv2.INTER_LINEAR) inp = np.expand_dims(resized.astype("float32") / 255.0, axis=0) prob = model.predict(inp, verbose=0)[0, 1] window.append(prob) else: fire_score = float(np.mean(window)) if fire_score > 0.5: cv2.putText(frame, "FIRE", (20, 40), cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 0, 255), 2) cv2.imshow("firenet-live", frame) window = window[1:] if cv2.waitKey(1) & 0xFF == ord("q"): break cap.release() cv2.destroyAllWindows()这里的关键参数是窗口长度window和判断阈值0.5。窗口越长越抗噪,但报警延迟也越高,5帧在摄像头30fps下大约是0.17秒延迟,视觉上可以接受。fire_score > 0.5这个阈值可以调,宁可让误报多出现几次,也不要让漏报吞掉早期火情。INTER_LINEAR是双线性插值,如果训练时用的是其他插值算法,这里必须保持一致,否则输出会出现系统性偏移。
6.2 两级检测:粗筛加超像素细筛
把实时推理和超像素结合起来,才真正用上这套源码里那个superpixel脚本。我的做法是:FireNet先对整帧做粗筛,概率大于0.3但不高于0.7的模糊帧,才对检测框内的区域跑SLIC细筛;概率已经高于0.7的直接报警,低于0.3的直接放过。这样既避免了每帧全图SLIC的性能灾难,又保住了对小火焰的召回。模糊帧的数量通常很少,细筛带来的额外算力压力完全可控。
那次在部署现场,我发现TFLite在一张小火苗图上输出fire概率只有0.3,排查到最后是保存模型前忘了把归一化方式同步过去。从那以后我每次做完转换,都强制把原始h5和导出模型在同一批测试图上对一遍输出再上线,先保正确再谈优化。希望帮到你。
本文还有配套的精品资源,点击获取