尧图精选

基于Python CNN的火焰识别二分类实战:从数据准备到模型部署

🕒 发布时间:2026/9/28 9:17:40 📁 来源:尧图网络
简介基于Python与PyTorch构建的火焰识别项目面向深度学习图像分类初学者及消防、安防监控场景开发者解决图片中是否存在火焰的二分类识别问题。压缩包共302个文件主要包含288张jpg样本图片、8张png图片、3个py脚本和3个txt说明文件整体大小仅11.72MB适合快速部署。代码按功能拆分为三个Python脚本数据集文本生成脚本负责读取各类别图片路径通过短边补灰边转为正方形、随机旋转等方式扩增数据并输出标签文件模型训练脚本基于生成文本完成CNN训练并保存权重UI脚本提供PyQt可视化界面可加载模型对图片实时判别火焰。配套的requirement.txt详细列出PyTorch环境依赖与安装指引降低配置门槛。资源已有75人学习适合用于课程设计、毕业设计或火灾预警原型开发从数据处理到界面部署均可按需参考。1. 基于python-CNN深度学习识别火焰二分类问题与最小落地路径仓库监控、森林防火、加油站安全这些场景里有没有火焰是优先级最高的判断。基于python-CNN深度学习识别是否有火焰这套方案本质上是一个图像二分类任务给一张图片模型输出有火或无火数据集zip里就是用来训练和验证的火焰/非火焰图片。传统OpenCV颜色阈值方案在晴天、日落、车灯下误报率很高而CNN能学到火焰的颜色、边缘和纹理组合稳定性好不少。适合有Python基础、想快速跑通一个图像分类项目的工程师也适合拿来做深度学习入门的第一个实战。2. 数据准备把数据集zip整理成CNN能直接读取的格式2.1 解压与目录划分统一成ImageFolder结构拿到含数据集.zip第一件事不是直接开训练而是先解压、看目录、清洗坏图。常见的数据集打包方式有两种一种是根目录下直接分fire/和nofire/两个文件夹另一种是一堆图片加一张labels.csv。我习惯先把它们都转成train/val/test按类别分目录的结构这样Keras的flow_from_directory和PyTorch的ImageFolder都能直接吃不用手写标签映射。import os, shutil, zipfile, random # 1. 解压 with zipfile.ZipFile(含数据集.zip, r) as z: z.extractall(dataset) # 2. 创建目标目录 classes [fire, nofire] for split in [train, val, test]: for cls in classes: os.makedirs(fdataset/{split}/{cls}, exist_okTrue) # 3. 如果原始结构是 dataset/raw/fire 和 dataset/raw/nofire random.seed(42) for cls in classes: src_dir fdataset/raw/{cls} images os.listdir(src_dir) random.shuffle(images) n len(images) div1, div2 int(0.7 * n), int(0.9 * n) for i, img in enumerate(images): src os.path.join(src_dir, img) if i div1: dst fdataset/train/{cls}/{img} elif i div2: dst fdataset/val/{cls}/{img} else: dst fdataset/test/{cls}/{img} shutil.copy(src, dst)这段脚本里random.seed(42)保证每次运行划分结果一致方便复现实验。shutil.copy而不是os.rename是为了让原始数据保留一份万一某个文件夹分配错了不用重新解压。划分比例7:2:1是图片分类的常用值如果火焰样本很少比如一共才300张建议改成8:1:1验证集太少会让指标波动很大。划分完要立即检查数量代码可以简单打印每个目录的文件数for split in [train, val, test]: for cls in classes: path fdataset/{split}/{cls} print(split, cls, len(os.listdir(path)))如果发现某个val/fire只有个位数说明样本量不够先回数据集层面补图不要急着调模型。2.2 坏图清洗与csv标签转换训练前先排掉隐患zip在传输、重命名过程里经常混入损坏文件比如0字节文件、扩展名是jpg但实际是png的伪图片。CNN训练到一半遇到坏图会直接崩还不好定位。所以划分之前先做一次完整校验。from PIL import Image for cls in classes: src_dir fdataset/raw/{cls} for img in os.listdir(src_dir): path os.path.join(src_dir, img) try: with Image.open(path) as im: im.verify() # 只检查文件头不完整解码 except Exception: print(remove bad image:, path) os.remove(path)im.verify()比im.load()快很多因为它只校验文件头和解码参数不加载全部像素。对几百张图来说几秒钟就扫完。注意verify()之后不能再直接取im.size否则会报错需要重新open。如果你的zip里带的是labels.csv转换逻辑也很简单。常见csv至少有两列filename和labellabel是0/1或fire/nofire字符串。如果是0/1先做映射再复制到对应目录import pandas as pd label_map {0: nofire, 1: fire} df pd.read_csv(dataset/labels.csv) for _, row in df.iterrows(): src os.path.join(dataset/raw, row[filename]) label str(row[label]) label label_map.get(int(label), label) # 兼容0/1和字符串 dst_dir os.path.join(dataset/temp, label) os.makedirs(dst_dir, exist_okTrue) shutil.copy(src, os.path.join(dst_dir, row[filename]))注意csv里的文件名如果带子目录路径join时要先os.path.dirname处理否则路径会叠加。做完这步再按2.1的脚本做二次划分。2.3 数据增强与归一化小数据集防过拟合的关键火焰训练集通常只有几百到上千张直接训练CNN很容易过拟合。过拟合的表现是训练集准确率接近100%验证集却一直在70%上下。数据增强就是在训练时在线做随机变换让模型每轮看到的图片都略有不同相当于把数据集扩大了。from tensorflow.keras.preprocessing.image import ImageDataGenerator train_datagen ImageDataGenerator( rescale1.0 / 255, rotation_range20, width_shift_range0.2, height_shift_range0.2, brightness_range[0.8, 1.2], horizontal_flipTrue, fill_modenearest ) val_datagen ImageDataGenerator(rescale1.0 / 255) train_gen train_datagen.flow_from_directory( dataset/train, target_size(128, 128), batch_size32, class_modebinary, shuffleTrue ) val_gen val_datagen.flow_from_directory( dataset/val, target_size(128, 128), batch_size32, class_modebinary, shuffleFalse )参数里最值得说的是rescale1/255CNN在0到1的输入上训练更稳定梯度的数值范围更正常。rotation_range20表示随机旋转±20度width_shift_range和height_shift_range是水平垂直平移20%的宽度/高度模拟摄像头视角变化brightness_range让亮度在0.8到1.2倍之间随机变化对光照变化强的监控场景很重要。fill_modenearest是旋转和平移产生空白区域时用最近邻像素填充比填0更好因为填0会引入大量黑色边缘和真实画面不符。验证集和测试集只做rescale绝不做随机增强。原因很直白评估时要看模型在真实分布上的表现增强只会让验证结果虚高。另外一个容易被忽略的点是flow_from_directory的标签顺序它按文件夹名的字母顺序生成fire在nofire之前所以fire的标签是0nofire是1。这个顺序后面用混淆矩阵时要记得。2.4 类别不均衡用class_weight解决有火样本过少大多数火焰数据集的通病是nofire图片比fire多一截。如果数据集里nofire占90%模型什么都不学全部输出无火就能有90%准确率。这种情况必须处理常用做法是对少数类加权。def count_files(path): return {cls: len(os.listdir(os.path.join(path, cls))) for cls in classes} train_counts count_files(dataset/train) total sum(train_counts.values()) n_classes len(classes) class_weight {} for idx, cls in enumerate(sorted(classes)): # 按字母序fire0, nofire1 class_weight[idx] total / (n_classes * train_counts[cls]) print(class_weight)这个公式是Scikit-learn的balanced权重每个类权重等于总样本数 / (类别数 * 该类别样本数)。比如fire有200张、nofire有800张总样本1000fire的权重就是1000/(2200)2.5nofire是1000/(2800)0.625。这样loss里一条火图样本的贡献相当于2.5条无火样本模型不会再躺平。训练时把它传给fit(..., class_weightclass_weight)。如果数据规模更大也可以用Focal Loss但新手先试class_weight效果好且不用改网络结构。3. 模型构建与训练手写CNN和调参要点3.1 CNN网络结构用3个卷积块提取火焰特征CNN解决的是看图里的局部模式火焰的边缘、亮色核心、烟雾纹理都靠卷积核来捕捉。火焰识别不需要很深很大的网络一个三层小网络在几百张图上就能收敛。我用Keras的Sequential实现因为它对新手最友好出问题也好排查。from tensorflow.keras import layers, models model models.Sequential([ layers.Input(shape(128, 128, 3)), layers.Conv2D(32, (3, 3), activationrelu, paddingsame), layers.BatchNormalization(), layers.MaxPooling2D((2, 2)), layers.Conv2D(64, (3, 3), activationrelu, paddingsame), layers.BatchNormalization(), layers.MaxPooling2D((2, 2)), layers.Conv2D(128, (3, 3), activationrelu, paddingsame), layers.BatchNormalization(), layers.MaxPooling2D((2, 2)), layers.Flatten(), layers.Dense(128, activationrelu), layers.Dropout(0.5), layers.Dense(1, activationsigmoid) ]) model.summary()Conv2D(32, (3,3))表示第一层有32个3x3卷积核输出是32通道的特征图。paddingsame保持输出尺寸不变128x128输入经过第一层后还是128x128只是通道变成32这样可以减少边缘信息过早丢失。BatchNormalization在每层卷积后做归一化让激活值分布稳定训练快很多也能稍微缓解过拟合。MaxPooling2D((2,2))把特征图宽高各减半从128到64再到32最后16靠它逐步放大感受野后面卷积核能看到更大的火焰区域。Flatten把16x16x128的特征拉平成一维向量送入全连接层。Dropout(0.5)是训练时随机丢弃一半神经元强制网络不依赖单个节点这是防过拟合最有效的手段。最后一层只有一个节点sigmoid输出0到1的概率对应有火的概率。这里有个设计细节两个全连接层之间只用了128个神经元而不是像分类大模型那样用1024。火焰识别的输入是128x128特征复杂度不高神经元数过多只会更快过拟合。3.2 训练配置优化器、损失函数、回调函数训练参数决定了模型是快速收敛还是原地打转。我一般用Adam优化器初始学习率1e-3损失函数binary_crossentropy指标用accuracy另外挂上三个回调EarlyStopping、ReduceLROnPlateau、ModelCheckpoint。from tensorflow.keras.optimizers import Adam from tensorflow.keras.callbacks import EarlyStopping, ReduceLROnPlateau, ModelCheckpoint model.compile( optimizerAdam(learning_rate1e-3), lossbinary_crossentropy, metrics[accuracy] ) callbacks [ EarlyStopping(monitorval_loss, patience10, restore_best_weightsTrue), ReduceLROnPlateau(monitorval_loss, factor0.5, patience4, min_lr1e-6), ModelCheckpoint(fire_model.keras, monitorval_loss, save_best_onlyTrue) ] history model.fit( train_gen, validation_dataval_gen, epochs50, callbackscallbacks, class_weightclass_weight )Adam是自适应矩估计优化器它会根据每个参数的梯度历史自动调整学习率比纯SGD稳定。学习率1e-3是Adam的默认值火焰数据集小这个值够用如果loss出现振荡就降到5e-4。binary_crossentropy是二分类的标准损失配合最后一层的sigmoid输出。三个回调的作用各有分工。EarlyStopping监控验证集loss如果连续10个epoch不下降patience10就停止训练并restore_best_weights回滚到验证loss最好的轮次。它是我认为最像后悔药的东西能避免你手动盯着训练曲线。ReduceLROnPlateau是验证loss进入平台期时把学习率减半patience4表示4轮不降就减一次min_lr1e-6防止减到零。ModelCheckpoint只保存验证loss最好的模型命名成fire_model.keras训练结束后不用自己从一堆epoch里挑。注意class_weightclass_weight这里传的是字典key必须是整数标签0和1。如果代码里忘传模型大概率会偏样本多的无火类。还要注意epochs50只是上限实际训练会在EarlyStopping触发时提前结束通常20-30轮。不要惊讶日志里只到第几轮就停了那是正常行为。3.3 训练曲线判读怎么看是不是过拟合训练过程中最直观的是loss曲线。我习惯用matplotlib把训练和验证loss画到同一张图只跑几个epoch就开始看趋势。import matplotlib.pyplot as plt plt.plot(history.history[loss], labeltrain_loss) plt.plot(history.history[val_loss], labelval_loss) plt.xlabel(epoch) plt.ylabel(loss) plt.legend() plt.savefig(loss_curve.png)正常的曲线是两条loss一起下降然后都在低位走平。如果你看到train_loss一路降到0.1以下val_loss却先降后升这是典型的过拟合模型开始死记训练图对没见过的验证图失去泛化能力。处理办法按优先级排加大Dropout到0.6、减小全连接神经元数、再增加一点数据增强的强度。如果两条loss都不降甚至准确率只有50%问题大概率不在模型而在数据预处理——先回第2章检查目录标签和归一化。还有一个容易被忽略的点训练日志里验证集准确率第一次就到90%以上别高兴太早。先打印val_gen.class_indices确认标签映射再在验证集上多跑几次预测。火焰数据集如果类别不均衡准确率会在类别失衡时虚高真正的判断要看下一章的混淆矩阵。训练阶段只要能稳定下降就可以先保存模型不要在一个网络结构上反复消耗时间。4. 模型评估与推理从准确率数字到真实可用的判断4.1 精确率、召回率和混淆矩阵别被准确率骗了二分类里准确率很容易骗人。假设测试集有90张无火和10张有火模型全输出无火准确率是90%但这个模型完全不能用。火焰场景我们最怕的是漏报也就是有火却判成无火这比误报严重得多。所以我评估模型时看精确率、召回率和混淆矩阵。from sklearn.metrics import classification_report, confusion_matrix val_gen.reset() y_pred_prob model.predict(val_gen) y_pred (y_pred_prob 0.5).astype(int) y_true val_gen.labels[: len(y_pred)] print(classification_report(y_true, y_pred, target_names[fire, nofire])) print(confusion_matrix(y_true, y_pred))val_gen.labels是验证集的全部标签顺序和生成器输出的顺序一致。因为batch_size不一定整除总数最后一批样本可能不足所以用[:len(y_pred)]截断防止标签和预测数量对不上。classification_report会给出每个类的precision、recall和F1。作为安全应用fire这一行的recall要尽量高比如0.95以上意思是100张真实烟火图里漏掉的不能超过5张。如果fire的recall只有0.8就把预测阈值从0.5降到0.3试试trade-off是误报增多。4.2 单张图片推理预处理和训练必须严格一致模型训完很多人直接把一张监控截图喂进去就报错或者预测概率完全不对。常见原因只有一个预处理不一致。训练时用的是RGB、128x128、0-1归一化推理时也必须一模一样。from tensorflow.keras.preprocessing import image import numpy as np def predict_fire(img_path, threshold0.5): img image.load_img(img_path, target_size(128, 128)) x image.img_to_array(img) / 255.0 x np.expand_dims(x, axis0) # (1, 128, 128, 3) prob model.predict(x, verbose0)[0][0] label 有火 if prob threshold else 无火 return label, float(prob) print(predict_fire(test_samples/example.jpg))image.load_img默认以RGB加载target_size自动完成resize。除以255后是0-1浮点数。np.expand_dims加batch维度是新手最容易漏的一步模型接受的输入是四维(batch, h, w, c)单张图只有三维不补维度就会直接报错。函数里的threshold0.5是默认判断点生产环境可以调。如果摄像头噪点大阈值提高一点减少误报如果更要防漏阈值降低一点。补充一个经验不要在脚本里硬编码阈值。我习惯把阈值放到配置文件里调参时只改配置不改代码。因为不同现场的光照差异很大同一个阈值在钢厂和森林里表现完全不同。4.3 视频帧监控一个带防抖的简易报警器分类模型不能输出火焰的坐标框但可以逐帧判断画面里有没有火。配合OpenCV能做出一个很实用的报警器。为了避免单帧误报我一般做跳帧加连续确认。import cv2 cap cv2.VideoCapture(monitor.mp4) # 摄像头传0 frame_idx 0 fire_count 0 while cap.isOpened(): ret, frame cap.read() if not ret: break if frame_idx % 5 0: rgb cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) img cv2.resize(rgb, (128, 128)) / 255.0 x np.expand_dims(img, axis0) prob model.predict(x, verbose0)[0][0] fire_count fire_count 1 if prob 0.5 else 0 if fire_count 3: cv2.putText(frame, FIRE!, (50, 50), cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 0, 255), 2) cv2.imshow(fire monitor, frame) if cv2.waitKey(1) 0xFF ord(q): break cap.release() cv2.destroyAllWindows()这个脚本里第一坑是BGR转RGB。OpenCV用cv2.imread和摄像头读到的帧都是BGR直接喂给模型等于红蓝通道互换火焰的橙色会变成蓝色模型当然报无火。这里用cvtColor转成RGB和训练时保持一致。第二坑是跳帧每5帧才跑一次模型否则摄像头30帧全跑CNNCPU直接跑满。第三坑是连续确认连续3帧都有火才报警单帧闪烁引发的误报会被滤掉。如果想把报警从文本变成行动可以在fire_count 3分支里写截图保存或调用报警接口。这个方案的缺点是CPU上跑不了太高的实时性128x128的单帧预测在普通PC大约0.1秒5帧抽1帧可以接受。想更快就去看第6章的TFLite量化。5. 避坑/常见问题火焰识别训练与部署中的5个翻车现场训练火焰识别模型时十个问题里有八个不在模型结构而在数据和IO。下面几条是我在项目里遇到过且带共性的坑每一条都按现象-原因-解决写建议训练前对照过一遍。5.1 loss一直卡在0.69准确率稳在50%不上不下现象训练好几轮loss完全没有下降趋势验证准确率一直在50%附近跳动像是抛硬币。 原因标签和图片内容对不上。最常见的是数据集目录里fire文件夹混进了几张nofire图片或者flow_from_directory的class_mode设置不对。0.69这个数字我记得特别清楚它接近二分类交叉熵在均匀随机猜测时的理论值模型完全没学到东西。 解决先停训练回到dataset/train分别抽查fire和nofire目录里的前20张图用脚本核对文件内容是否和目录名一致。再打印train_gen.class_indices确认fire0、nofire1如果你心里认为fire应该是1后面混淆矩阵解读会全反。我在做数据清洗时就会顺手输出几行文件名把目录名拼进日志减少这种低级错误。5.2 验证集准确率很高一到夜间现场就疯狂误报现象白天测试集上准确率95%以上拿夜间监控视频一测路灯、摩托车尾灯全被报成火。 原因训练集以白天图片为主模型学到的主要是偏亮的橙红色但路灯和尾灯也具有这个特征。CNN再强训练分布里没见过夜间干扰就会把同类颜色误判。 解决给数据集补充夜晚、黄昏和逆光条件下的无火图片至少占nofire的20%-30%。如果没有现成夜间图一个变通做法是把训练图像随机调暗并加一点蓝色偏色模拟夜视摄像头再用brightness_range扩大亮度扰动。但变通是临时方案根治还是要采集现场的负样本。我把这个坑写在最前面是因为它最隐蔽白天好晚上炸原因全在数据分布。5.3 小火焰、远处小火苗全部漏报现象大火图都能识别但照片里只有一小团火或者浓烟中隐约有火光模型直接判无火。 原因输入尺寸只有128x128火焰占比小经过三次卷积和池化后小目标的特征被压缩到很小区域信息损失太多了。分类模型本身也缺乏目标定位能力它不会去找小火苗。 解决优先把输入尺寸从128加到224或256给小火苗多留像素。如果尺寸加大后显存不够就减少batch_size。更工程化的做法是先用颜色阈值或运动检测找出可疑候选区裁剪放大后再喂给CNN这种传统算法深度学习的两级方案在火焰识别项目里很常见能兼顾速度和漏报率。不要指望纯分类模型直接搞定稀疏目标识别那是目标检测的活。5.4 训练时报错CUDA out of memory或是Python进程被杀死现象训练刚起来几十秒GPU掉显存或者小内存服务器直接OOM。 原因输入尺寸、batch_size和数据增强叠加时显存峰值太高。数据增强本身也需要额外显存来保存变换后的张量ImageDataGenerator在做随机旋转时还会把中间结果放在GPU上。 解决把target_size从224降到128batch_size从64降到16一般能解决90%的OOM。在TensorFlow里还可以设置显存按需增长在程序最前面写os.environ[TF_GPU_ALLOW_GROWTH]true避免一上来就占满显存。如果CPU数据加载也卡把fit里的workers设为2、max_queue_size设为8限制数据线程缓冲防止内存被队列吃光。显存不够时优先减batch_size不要减图片尺寸尺寸直接影响小火苗的漏报率。5.5 用OpenCV读取图片后推理结果全乱却找不到原因现象同一个模型Keras自带的图片读取方式预测很准用cv2.imread读同一张图有火判无火、无火判有火。 原因cv2.imread返回的是BGR通道顺序而训练时ImageDataGenerator和image.load_img默认是RGB。火焰的橙色在BGR里被解析成蓝色和绿色模型输出的概率自然乱套。 解决在推理入口做一次cv2.cvtColor(img, cv2.COLOR_BGR2RGB)再把HWC转成BCHW。更稳的做法是写一个统一的图片加载函数内部只允许RGB输出训练和推理都调它杜绝散落的预处理。我吃过这个亏三次以后养成了习惯所有入口函数第一行都写死把输入转为RGB并断言通道数后续接手的同事也踩不到这个坑。6. 进阶把CNN模型压缩到边缘设备并用真实视频验证6.1 导出TFLite模型量化后留在监控端训练好的模型可以直接在PC上用但要部署到树莓派、Android或边缘网关就要转成TensorFlow Lite格式。转换代码很简单from tensorflow.keras.models import load_model import tensorflow as tf model load_model(fire_model.keras) converter tf.lite.TFLiteConverter.from_keras_model(model) converter.optimizations [tf.lite.Optimize.DEFAULT] tflite_model converter.convert() with open(fire_model.tflite, wb) as f: f.write(tflite_model)optimizations[tf.lite.Optimize.DEFAULT]会做权重量化模型体积减小到原来的四分之一左右推理速度更快但精度可能有微弱下降。如果火焰样本量小、精度本来就紧张就先不量化只做基础转换。之后在边缘端写一个TFLite解释器推理脚本输入输出和Keras模型几乎一致。我在一个Linux开发板上测试过量化后的128x128模型单帧能做到几十毫秒足够每5帧抽一帧使用。6.2 端到端验证用一段没参与训练的视频算误报率和漏报率模型部署前不能只看测试集准确率。我的做法是准备一段5到10分钟的现场监控视频里面既要有火情也要有路灯、车灯、阳光等干扰用脚本逐帧推理并和人工标注对拍统计两个数字每1000帧的误报警次数和真实火情漏报的帧数。如果漏报为0误报在可接受范围比如每小时误报不超过3次才敢上线。如果误报率高先调阈值而不是重训模型。把这些判断参数写成配置文件后就能快速在现场做对比测试。我在这个项目上最大的教训是模型训练只完成了60%的工作另外40%在数据和部署侧的工程细节。现在每次拿到新场景我都会先花时间看负样本长什么样而不是急着改网络结构。这个习惯帮我避开了不少测试集骗局。希望帮到你。本文还有配套的精品资源点击获取
上一篇/下一篇内容由系统自动关联 返回资讯列表 →