尧图精选

基于PyTorch的火灾检测CNN模型设计:从数据准备到实时视频部署

🕒 发布时间:2026/10/1 19:19:20 📁 来源:尧图网络
简介这是一份基于Python的火灾检测CNN模型设计源码包面向计算机视觉学习者、消防监测系统开发者以及需要快速搭建火灾识别原型的研究人员解决传统监测手段效率低、误报率高等问题。项目围绕FireNet及InceptionVx系列模型展开包含数据预处理、模型训练、验证、转换及部署等完整环节的Python脚本并配有Shell脚本辅助下载数据集和模型YAML文件用于自动化工作流配置适合中高级Python开发者学习迁移。包体共23个文件主要类型包括8个Python源码、6张模型架构示意图、2个Shell脚本、2个Markdown说明文档和2个文本文档压缩包仅1.9MB结构轻量但覆盖从训练到推理的关键流程。已有444人学习下载。透过清晰的目录与图片说明读者可以理解CNN火灾检测的建模思路掌握模型转换与验证方法并可直接基于源码改造成自己的检测方案用于科研实验或工程预研。1. 火灾检测CNN这个Python项目到底解决什么问题如果你拿到一套“基于Python的火灾检测CNN模型设计源码”大概率不是想要一篇论文而是想尽快得到一个能跑、能看、能交代结果的程序。火灾检测的难点不在“识别火焰本身”而在场景太杂白天阳光打在红色车身上会误报夜间路灯和车灯连成一片会把模型逼疯厨房里煎牛排的烟雾和火灾烟雾又长得一模一样。一个只靠颜色阈值判断的OpenCV程序在这种场景下准确率不到六成而一个设计合理的CNN二分类网络在公开火灾图像集上通常能把准确率做到92%以上。这篇笔记就围绕这条主线来写用Python和PyTorch从零设计一个火灾检测CNN模型把数据组织、网络设计、训练调参、常见踩坑、推理部署完整走一遍。适合三类人要做课程设计或毕业设计的在校生想在公司demo里快速验证视觉方案的工程师以及第一次接触图像分类、想搞清楚CNN各层到底怎么配参数的人。代码可以照抄再改成自己的数据重点是理解每个参数为什么这么设。2. 数据准备是火灾检测的第一道坎图像采集、标注与目录组织2.1 火灾数据集从哪里来公开集、自采与数据增强的取舍火灾检测本质上是一个图像二分类问题模型只回答“这张图里有没有火”。听起来简单但实际项目里数据获取成本往往被严重低估。公开的火灾/烟雾图像数据集确实存在常见的是几百到几千张的规模类别一般就两类fire和nonfire。先用公开集把流程跑通是性价比最高的做法因为标注已经做好直接用ImageFolder读就行。等你需要做真实落地验证时光靠公开集不够。火灾场景的分布和普通图像差异很大室内火灾有家具、窗帘、墙壁反光室外火灾有植被、天空、地形夜间火灾还有大量暗光。这时候就要自采或爬取补充。自采的常见做法是用摄像头对着燃烧的纸张、蜡烛、小型火焰堆录几分钟视频然后按帧抽图每几秒抽一帧把模糊帧去掉剩下的打成训练集。注意一个边界问题火焰是动态的单帧图像里的小火苗可能只有几十个像素这种样本模型很容易直接忽略掉导致后续视频检测时漏检。处理办法后面模型设计里会讲。类别不平衡是火灾检测数据准备里最容易被忽视的坑。很多数据集里nonfire图片多、fire图片少比例甚至到5:1。CNN在这种数据上训练模型会倾向于把所有图都判成nonfire因为这样损失最小。所以数据划分时至少要让两个类别数量接近做法包括对fire类别做离线增强旋转、翻转、裁剪或者对nonfire类别做下采样。我一般把比例控制在1:1到1.5:1之间验证集上表现稳定很多。2.2 用PyTorch加载火灾图像ImageFolder与train/test划分代码PyTorch的ImageFolder要求数据按类别放在不同子目录下目录结构长这样data/ train/ fire/ 001.jpg 002.jpg nonfire/ 001.jpg val/ fire/ nonfire/这种组织方式的好处是不用手写解析逻辑ImageFolder会自动按子目录名生成类别标签。下面是加载和划分的完整代码可以直接放进项目里跑。import torch from torch.utils.data import DataLoader from torchvision import datasets, transforms from torch.utils.data import random_split import os # 基础预处理统一尺寸、转Tensor、归一化 base_transform transforms.Compose([ transforms.Resize((224, 224)), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ]) # 先读取全部数据再做划分适合中小规模数据集 full_dataset datasets.ImageFolder(rootdata/train, transformbase_transform) # 按7:3划分训练集和验证集seed固定保证可复现 train_size int(0.7 * len(full_dataset)) val_size len(full_dataset) - train_size train_dataset, val_dataset random_split( full_dataset, [train_size, val_size], generatortorch.Generator().manual_seed(42) ) train_loader DataLoader(train_dataset, batch_size32, shuffleTrue, num_workers2, pin_memoryTrue) val_loader DataLoader(val_dataset, batch_size32, shuffleFalse, num_workers2, pin_memoryTrue) print(f训练集{len(train_dataset)} 张验证集{len(val_dataset)} 张) print(f类别{full_dataset.classes})这里有几个参数值得说清楚。Resize((224, 224))是把所有图片统一到224x224这个尺寸是ImageNet时代的标配也是后面迁移学习时预训练模型的输入要求。Normalize的mean和std用的是ImageNet的统计值如果你不用预训练权重、从头训练CNN可以不改但如果用resnet18预训练权重这组参数必须保留。random_split在7:3比例下看起来没问题但它做的是随机抽样如果原始数据本身按时间顺序存放前几分钟的火灾场景可能全被分到训练集后几分钟的火灾场景全在验证集这就会造成“看起来验证集很好、实际场景很差”的假象。对于火灾视频抽帧的数据我强烈建议先按视频文件切分再在每个视频内抽帧保证同一视频的帧不要同时出现在训练和验证里。2.3 数据增强参数翻转、裁剪、色彩抖动怎么设才不误伤火焰颜色数据增强是火灾检测里最讲究的地方因为火焰的视觉特征高度依赖颜色和亮度。用常规的RandomHorizontalFlip没问题但RandomResizedCrop这类裁剪增强有一个隐患它会把大图中很小的火焰区域放大成“整张图都是火”模型学会的其实是“画面整体偏红就是fire”而不是“局部区域有火焰的纹理和边缘结构”。这样的模型在真实场景里遇到夕阳、红墙时误报率会明显上升。我常用的一组增强配置是这样的augment_transform transforms.Compose([ transforms.RandomHorizontalFlip(p0.5), transforms.RandomRotation(degrees10), transforms.ColorJitter(brightness0.2, contrast0.2, saturation0.2, hue0.05), transforms.Resize((224, 224)), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ])RandomRotation(degrees10)只做小角度旋转因为火灾图像没有固定的方向性转个10度不影响语义但大角度旋转会引入黑色边框边缘模型可能去学边框特征。ColorJitter的hue只给到0.05hue一旦调大火焰的红色会被改成紫色或橙色反而让模型学到错误的颜色映射。brightness0.2是给夜间火灾留的余地但不要超过0.3否则白天正常光照图像会被提亮成类似火光的样子造成验证时误报。如果数据量特别少总共不到500张可以再加RandomAffine(translate(0.1, 0.1), scale(0.9, 1.1))模拟不同距离下的火灾视角。但注意数据增强不是魔法它只能缓解过拟合救不了数据分布本身的问题。3. 模型设计与训练从自建轻量CNN到迁移学习的选择3.1 为什么不用“很深”的网络火灾检测的算力与实时性约束火灾检测项目里网络结构的选择不是越深越好。一个典型监控摄像头场景视频流每秒25帧如果模型推理一帧需要100毫秒以上实时检测基本就废了。ResNet152、EfficientNet这种大模型虽然准确率高但单张224x224图像在CPU上的推理时间普遍超过200毫秒只有GPU才能勉强跑实时。常见做法是分两档选型第一档是从零训练一个自建的轻量CNN参数量控制在几十万到一两百万CPU推理单帧50毫秒以内适合快速出demo、做课程设计第二档是微调ResNet18或MobileNetV2的预训练权重准确率能再提2到3个百分点但要求机器有GPU适合做结题验收或真实产品原型。如果标题里的“模型设计”指的是自己写网络结构那就先做第一档因为CNN的各组件职责清楚好解释也好调参。从零训练和迁移学习在火灾检测上的差距没有ImageNet那么大因为火灾图像和普通物体图像分布差异本身就大预训练模型在ImageNet上学到的“形状先验”对火焰这种无固定形态的目标帮助有限。我做过对比在1500张火灾图像上自建CNN准确率89%迁移ResNet18是93%差距没有想象中悬殊。所以时间紧就自建效果优先就迁移。3.2 一个可直接跑的CNN网络结构Conv-BN-ReLU堆叠与Dropout下面这个网络结构是针对火灾检测设计的特点是轻量、训练稳定、不容易过拟合。核心思路是三层卷积提取特征每层都挂BatchNorm稳定训练最后用全局平均池化代替Flatten再接两层全连接输出分类。import torch import torch.nn as nn class FireCNN(nn.Module): def __init__(self, num_classes2): super(FireCNN, self).__init__() # 三层卷积特征提取通道数 32 - 64 - 128 self.features nn.Sequential( # 第一层3-32通道保持分辨率提取边缘和颜色块 nn.Conv2d(3, 32, kernel_size3, padding1), nn.BatchNorm2d(32), nn.ReLU(inplaceTrue), nn.MaxPool2d(2), # 112x112 # 第二层32-64通道提取纹理和局部形状 nn.Conv2d(32, 64, kernel_size3, padding1), nn.BatchNorm2d(64), nn.ReLU(inplaceTrue), nn.MaxPool2d(2), # 56x56 # 第三层64-128通道提取高层语义 nn.Conv2d(64, 128, kernel_size3, padding1), nn.BatchNorm2d(128), nn.ReLU(inplaceTrue), nn.MaxPool2d(2), # 28x28 ) # 分类头全局平均池化后接全连接 self.classifier nn.Sequential( nn.AdaptiveAvgPool2d(1), # 不管输入多大都压成1x1x128 nn.Flatten(), nn.Linear(128, 256), nn.ReLU(inplaceTrue), nn.Dropout(0.5), nn.Linear(256, num_classes), ) def forward(self, x): x self.features(x) x self.classifier(x) return x这个结构有几个参数要认真说明。kernel_size3, padding1组合输入输出分辨率不变卷积只改变通道数空间信息由MaxPool2d负责压缩这样每一层的语义抽象更清晰。BatchNorm2d几乎是必须的它把每层输出归一化到0均值附近避免训练过程中梯度爆炸或消失尤其是火灾图像里有大量高亮区域火焰中心亮度极高没有BN的话loss在头几十个epoch会跳动非常剧烈。Dropout(0.5)放在最后一层全连接之前只对全连接层生效因为卷积层的参数共享本身就有正则化效果再加Dropout反而会拖慢收敛。如果你想把这个结构改成更深的版本把第三层的128改成256再加一层卷积即可。但我建议先用这个规模跑通全流程再决定是否加深。火灾检测任务中真正限制准确率的往往不是网络容量而是数据量和标注质量。3.3 训练参数学习率、batch size、损失函数与早停训练代码虽然长但核心参数就那么几个调好了一个项目就成功了一半。下面是一份完整的训练脚本包含交叉熵损失、Adam优化器、学习率衰减和早停逻辑。import torch.nn as nn import torch.optim as optim model FireCNN(num_classes2) device torch.device(cuda if torch.cuda.is_available() else cpu) model.to(device) criterion nn.CrossEntropyLoss() optimizer optim.Adam(model.parameters(), lr1e-3) scheduler optim.lr_scheduler.ReduceLROnPlateau( optimizer, modemin, factor0.5, patience3, verboseTrue ) num_epochs 30 best_val_loss float(inf) patience_counter 0 early_stop_patience 5 for epoch in range(num_epochs): model.train() running_loss 0.0 correct 0 total 0 for images, labels in train_loader: images, labels images.to(device), labels.to(device) outputs model(images) loss criterion(outputs, labels) optimizer.zero_grad() loss.backward() optimizer.step() running_loss loss.item() * images.size(0) _, predicted torch.max(outputs, 1) total labels.size(0) correct (predicted labels).sum().item() train_loss running_loss / total train_acc correct / total # 验证阶段 model.eval() val_loss 0.0 val_correct 0 val_total 0 with torch.no_grad(): for images, labels in val_loader: images, labels images.to(device), labels.to(device) outputs model(images) loss criterion(outputs, labels) val_loss loss.item() * images.size(0) _, predicted torch.max(outputs, 1) val_total labels.size(0) val_correct (predicted labels).sum().item() val_loss val_loss / val_total val_acc val_correct / val_total scheduler.step(val_loss) print(fEpoch {epoch1}/{num_epochs} | fTrain Loss: {train_loss:.4f} Acc: {train_acc:.4f} | fVal Loss: {val_loss:.4f} Acc: {val_acc:.4f}) # 保存验证loss最低的模型 早停 if val_loss best_val_loss: best_val_loss val_loss patience_counter 0 torch.save(model.state_dict(), best_fire_model.pth) else: patience_counter 1 if patience_counter early_stop_patience: print(验证loss连续未下降触发早停) breaklr1e-3对Adam来说是稳妥的起点火灾检测不是特别复杂的任务不需要像训练ImageNet那样从0.1起步。ReduceLROnPlateau的factor0.5表示验证loss不下降时就减半学习率patience3是减半前等3个epoch这比固定每N轮衰减更实用因为训练进度快慢不稳定。early_stop_patience5配合学习率衰减可以防止你半夜跑训练时模型在过拟合边缘反复横跳。batch size这里给32如果显存不够降到16但别再小了。火灾图像里火焰细节多batch太小会放大单张图的噪声影响导致训练loss曲线震荡。反过来batch太大128以上会过度平滑梯度让模型错过一些区分火焰和红色物体的关键特征。一个经验值在224x224分辨率下batch 32到64之间最稳。4. 火灾检测CNN的常见踩坑与排查记录4.1 训练loss下降但验证集准确率上不去现象每个epoch训练loss都在降从0.7一路降到0.1但验证集准确率卡在80%上下不动甚至开始下跌。原因这是典型的过拟合更准确说是模型记住了训练集里的背景特征而不是火焰特征。火灾数据集最常见的泄漏是同一视频的连续帧同时出现在训练集和验证集模型每张“验证图”都在训练集里见过几乎相同的画面验证loss自然失真。另一个原因是数据量太少模型容量有富余就开始记图像中的噪声和背景纹理。解决先检查数据集划分是否按视频文件切分确保同源帧不跨集合再检查训练集和验证集里fire:nonfire比例是否接近如果验证集里火焰图占八成模型只要全输出fire就拿到高准确率不代表真本事如果这两项都正常减少模型容量把第三层128降到64或者把Dropout从0.5提到0.6观察验证loss是否跟随训练loss同步下降。4.2 把晚霞、红色公交车误判成火现象模型在测试图上把大面积红色、橙色区域的图片判为fire包括晚霞、红色外墙、消防车、落日。训练集和验证集准确率都挺好一放到真实场景就崩。原因CNN学到的是“颜色分布统计”当训练集中fire类别大部分是大面积亮橙色火焰时模型会把这个颜色分布当作最重要的判别依据。火焰和晚霞在RGB空间上的分布高度重叠只靠颜色特征本来就分不开。解决在训练集里加“硬负样本”——大量包含夕阳、红色灯光、橙色建筑但不含火焰的图让模型必须依赖火焰的动态纹理和边缘特征。另一个有效手段是数据增强里把ColorJitter的saturation和brightness加大迫使模型不依赖绝对颜色。如果条件允许我在这个阶段会改用两阶段的检测思路先用CNN做图像级分类再用目标检测框出候选区域把“图像里有火”细化成“火在哪个区域”误报率会降一个量级。4.3 验证集效果很好但实拍视频漏检频繁现象单独抽帧测图片准确率95%放到监控视频里火焰燃起的前几秒基本没反应等火烧大了才报警。原因视频检测里模型看到的是动态画面而训练集里是静态图片。小火焰刚出现时在整帧画面中占比极小可能只有几十个像素CNN下采样后这些像素在最后一层特征图里只剩一两个点分类头直接把它们当噪声忽略。另一个因素是推理时帧与帧之间火焰形态变化快模型在单帧上的输出不稳定出现闪烁。解决解决办法分两层。第一层是推理时用滑动窗口平滑不直接输出单帧结果而是取最近N帧的平均概率连续多帧超过阈值才报警大幅降低闪烁漏检。第二层从数据入手把训练集中的火焰目标用小图裁剪成局部图单独训练一个“小火苗识别器”让模型见过小目标的形态。如果项目时间紧我一般先做滑动窗口平滑因为改代码只要十行不需要重新训练。4.4 loss突然变成NaN训练直接崩现象训练跑到第几十个epochloss从0.1突然变成NaN然后所有指标变成nan只能从头开始。原因最常见的是学习率过大导致梯度爆炸尤其用较大batch size时Adam的梯度一阶动量来不及修正极端值。火灾图像里高亮火焰区域的像素值很大经过网络后激活值爆炸乘上梯度后更新量过大loss就冲破了数值范围。还有一个少见但很气人的原因某张训练图是纯黑的夜间火灾数据里常有所有像素经过Normalize后变成很大的负均值卷积层输出直接饱和。解决先把学习率从1e-3降到3e-4重跑问题通常解决。如果还没解决在数据加载后加torch.clamp把输入限制在合理范围或者检查数据增强里的RandomRotation是否把图像旋转出了黑色边框导致大量0像素。还有一招后悔药每N个epoch保存一次checkpointloss变成NaN时回退到上一个正常epoch换小学习率继续不用全盘重来。5. 把训练好的模型用起来单图推理、视频检测与模型导出5.1 单张图片推理从加载checkpoint到输出置信度训练保存的best_fire_model.pth是state_dict格式推理时先实例化模型再load权重。完整代码如下import torch import torch.nn as nn from PIL import Image from torchvision import transforms device torch.device(cuda if torch.cuda.is_available() else cpu) model FireCNN(num_classes2) model.load_state_dict(torch.load(best_fire_model.pth, map_locationdevice)) model.to(device) model.eval()推理预处理要和训练保持一致否则输入分布对不上输出就是黑匣子。加载图片后按训练时的顺序做Resize、ToTensor、Normalize还要加一个batch维度。infer_transform transforms.Compose([ transforms.Resize((224, 224)), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ]) def predict_fire(image_path, threshold0.5): image Image.open(image_path).convert(RGB) input_tensor infer_transform(image).unsqueeze(0) # (1, 3, 224, 224) input_tensor input_tensor.to(device) with torch.no_grad(): outputs model(input_tensor) probs torch.softmax(outputs, dim1) fire_prob probs[0, 1].item() # 类别1是fire if fire_prob threshold: return True, fire_prob return False, fire_probtorch.softmax把网络输出的两个原始分数转成概率probs[0, 1]取的是fire类别的概率。注意threshold0.5并不是唯一选择如果更看重不漏报阈值降到0.35如果更看重不误报阈值提到0.7。这个阈值一定要在真实场景数据上调不要在验证集上调因为验证集的分布和真实场景总有偏差。5.2 用OpenCV做实时摄像头火灾检测帧率与阈值用摄像头做实时检测是火检项目最常见的落地形式。核心思路是逐帧读取、预处理、推理、叠加结果。这里最需要注意的是不要把预处理放在视频循环里重复定义每帧都重建transform和模型推理的开销会直接拖垮帧率。import cv2 import torch from torchvision import transforms from PIL import Image import numpy as np cap cv2.VideoCapture(0) # 0表示本机摄像头 fps cap.get(cv2.CAP_PROP_FPS) print(f摄像头帧率{fps:.1f}) # 预热模型 model.eval() # 滑动窗口保存最近7帧的fire概率做平滑 prob_history [] window_size 7 ALARM_THRESHOLD 0.6 while True: ret, frame cap.read() if not ret: break # OpenCV的BGR转成RGB再走推理预处理 rgb_frame cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) pil_image Image.fromarray(rgb_frame) input_tensor infer_transform(pil_image).unsqueeze(0).to(device) with torch.no_grad(): outputs model(input_tensor) fire_prob torch.softmax(outputs, dim1)[0, 1].item() prob_history.append(fire_prob) if len(prob_history) window_size: prob_history.pop(0) smoothed_prob sum(prob_history) / len(prob_history) is_fire smoothed_prob ALARM_THRESHOLD label fFIRE {smoothed_prob:.2f} if is_fire else fSAFE {smoothed_prob:.2f} color (0, 0, 255) if is_fire else (0, 255, 0) cv2.putText(frame, label, (20, 40), cv2.FONT_HERSHEY_SIMPLEX, 1.0, color, 2) cv2.imshow(Fire Detection, frame) if cv2.waitKey(1) 0xFF ord(q): break cap.release() cv2.destroyAllWindows()滑动窗口是这里最值得保留的机制。火灾在视频里不是瞬间出现的平滑后连续几帧概率都高于阈值才报警能过滤掉单帧的闪烁噪声和摄像头自带的自动曝光跳变。代码里ALARM_THRESHOLD0.6配合window_size7大约等效于“约1.5秒内过半置信才报警”实际场景里可以根据监控区域的误报容忍度调整。补充一点摄像头自动曝光在夜间会频繁调整导致火焰区域的亮度忽高忽低模型输出抖动明显如果做夜间场景建议先用cv2.createCLAHE做一次对比度增强再送进模型。5.3 模型导出为ONNX部署到边缘设备前的最后一步如果你要把模型部署到Jetson、RK3588或者手机端PyTorch的权重文件在Python环境外无法直接加载ONNX是跨平台推理的通用格式。导出代码很短但有一个坑值得提前说明训练时模型在GPU上导出的ONNX算子可能与CPU推理后端不兼容所以导出前先把模型放到CPU上能少踩很多兼容性问题。import torch import onnxruntime as ort # 导出转成CPU再导出 model_cpu FireCNN(num_classes2) model_cpu.load_state_dict(torch.load( best_fire_model.pth, map_locationcpu)) model_cpu.eval() dummy_input torch.randn(1, 3, 224, 224) torch.onnx.export( model_cpu, dummy_input, fire_model.onnx, input_names[input], output_names[output], dynamic_axes{input: {0: batch_size}, output: {0: batch_size}}, opset_version12 )导出后用onnxruntime跑一遍验证确认数值和PyTorch推理结果接近import numpy as np ort_session ort.InferenceSession(fire_model.onnx) random_image np.random.randn(1, 3, 224, 224).astype(np.float32) ort_output ort_session.run([output], {input: random_image})[0] print(ONNX输出形状, ort_output.shape)dynamic_axes设为batch维动态这样导出的模型既能跑单帧也能一次喂多帧做批量推理。opset_version12是兼容性和新算子之间的平衡点太老的版本不支持某些池化算子太新的版本在旧设备上可能找不到推理后端。ONNX模型大约比PyTorch权重小30%,因为去掉了自动求导图相关的信息部署体积会更小。这套流程走完后你就拥有了一个完整的火灾检测CNN项目从原始图像数据到训练好的模型再到视频实时推理和ONNX部署包。我在跑类似项目的经验是第一版模型别追求高精度先保证全流程能通把坑都踩一遍之后再回来换网络结构、调阈值、补数据集。这样迭代时每改一个参数都有基准线可以对比而不是闷头重训一个黑匣子。希望这篇笔记能帮你在做火灾检测这个方向上少走一段弯路。本文还有配套的精品资源点击获取
上一篇/下一篇内容由系统自动关联 返回资讯列表 →