简介本资源面向具备一定Python与深度学习基础、希望快速上手图像分类实战的开发者与学习者提供一套基于PyTorch框架的CNN火焰识别完整方案可用于火灾预警、安防监控等场景的算法验证与二次开发。压缩包共302个文件以288张jpg与8张png图像构成多类别数据集另含3个txt说明文件与3个py脚本整体约11.72MB体积轻便便于本地部署。代码对数据集做了针对性预处理通过短边补灰边将图片统一为正方形并辅以旋转角度进行数据增强有效扩充样本多样性。运行流程清晰01脚本读取各类别图片路径与标签生成训练文本02脚本完成模型训练并保存权重至本地03脚本提供PyQt可视化界面便于交互测试。目前已有75人学习适合作为课程设计、毕业项目或算法入门的参考范例。1. 火焰识别项目拆包从 26.jpg 到 PyTorch 推理链路拿到一个名为「基于python-CNN深度学习识别是否有火焰-含数据集.zip」的压缩包时多数人第一反应是解压看图片然后被26.jpg、4.jpg、1_flip.jpg这类毫无规律的命名劝退。这恰恰是火焰识别数据集的典型特征——原始采集图片按时间戳或设备编号落盘标签信息藏在文件夹层级里而不是文件名中。这个资源要解决的核心问题很具体用 PyTorch 搭一个 CNN 二分类器判断输入图像是否包含火焰并配一个 PyQt 界面做本地推理演示。它适合刚学完卷积神经网络、想找一个完整闭环项目练手的人也适合需要快速验证火焰检测 baseline 的工程人员。整个链路是「数据集文本生成 → 模型训练 → UI 推理」三段式不依赖复杂标注工具环境配好后依次跑三个脚本就能看到结果。数据集里已经做了短边补灰边和旋转增强省掉了自己写 DataLoader 增强的功夫但这也意味着你得先理解它为什么这么处理否则换自己的数据时容易翻车。2. 环境配置与数据预处理requirement.txt 里没写的细节2.1 依赖安装的版本对齐问题压缩包里的requirement.txt通常只列了包名没锁版本。PyTorch 生态里 torch、torchvision、numpy、opencv-python 之间的版本耦合很紧尤其是 torchvision 的 transforms 接口在不同版本间有细微差异。我一般会先确认本机 CUDA 版本再决定装 GPU 版还是 CPU 版。如果只是跑这个火焰识别项目CPU 版完全够用因为输入图像会被缩放到较小尺寸模型本身也不深。# 先看显卡驱动支持的 CUDA 上限没有 NVIDIA 显卡就跳过 nvidia-smi # 创建独立环境避免和系统 Python 冲突 conda create -n fire_cnn python3.8 -y conda activate fire_cnn # 安装 PyTorchCPU 版示例有 GPU 去官网复制对应命令 pip install torch torchvision --index-url https://download.pytorch.org/whl/cpu # 其余依赖按 requirement.txt 补重点确认这几个 pip install numpy opencv-python pillow pyqt5 matplotlib这里的关键参数是python3.8。项目正文提到参考博文安装环境而 CSDN 上那篇环境教程大概率围绕 3.8 写的。PyQt5 在 3.9 上有时会出现 sip 兼容问题3.8 是稳妥选择。opencv-python用于图像读取和补边操作pillow用于 PyQt 显示时的格式转换matplotlib在训练脚本里画 loss 曲线。装完后跑一句python -c import torch; print(torch.__version__)确认没报错再往下走。2.2 短边补灰边与旋转增强的逻辑项目正文明确说了预处理包含「在较短边增加灰边使图片变为正方形」和「旋转角度扩增」。这两步不是随便加的。CNN 的输入层通常要求固定尺寸比如 224×224。如果直接把长方形图片 resize火焰的宽高比会被扭曲火焰纹理特征变形模型学到的可能是拉伸后的伪特征。补灰边保留原始比例灰边像素值取 128 左右不引入额外纹理干扰。旋转增强则是针对火焰形态多变——火苗可能倾斜、可能被风吹偏旋转后让模型对方向不敏感。import cv2 import numpy as np def pad_to_square(img, pad_value128): 短边补灰边使图像变为正方形 h, w img.shape[:2] if h w: return img size max(h, w) top (size - h) // 2 bottom size - h - top left (size - w) // 2 right size - w - left # 灰边填充避免纯黑边被卷积核当成火焰边缘 return cv2.copyMakeBorder(img, top, bottom, left, right, cv2.BORDER_CONSTANT, value(pad_value, pad_value, pad_value)) def rotate_augment(img, angle): 按指定角度旋转边缘同样用灰边填充 h, w img.shape[:2] center (w // 2, h // 2) M cv2.getRotationMatrix2D(center, angle, 1.0) return cv2.warpAffine(img, M, (w, h), borderModecv2.BORDER_CONSTANT, borderValue(128, 128, 128))pad_value128是经验值灰色在归一化后接近 0.5不会像黑色那样被 ReLU 激活成强响应。旋转角度一般取 15、30、45 度几个档位太多档位会让数据集膨胀过快训练时间线性上升。注意旋转后如果图像本身有黑边再补灰边会形成双层边框这时候要么先裁剪掉黑边再旋转要么旋转后统一裁掉边缘几个像素。我见过有人直接对已经补过灰边的图再旋转结果灰边区域被反复填充模型把灰边当成了火焰的上下文特征验证集准确率虚高但实际推理时对纯色背景误报严重。2.3 数据集目录结构与标签映射解压后数据集文件夹通常按类别分子目录比如fire/和nofire/里面才是26.jpg、4.jpg这些。01数据集文本生成制作.py的作用就是遍历这两个文件夹把每张图的路径和对应标签写进 txt。标签一般是 0 和 1具体哪个是火焰要看脚本里的class_to_idx映射。常见做法是按文件夹名排序后依次赋 0、1所以fire在前就是 0nofire在后就是 1。这个顺序必须和训练脚本、UI 脚本保持一致否则会出现「训练时火焰是 0推理时火焰是 1」的错位UI 上显示的结果就反了。import os def generate_txt(data_root, output_txt): classes sorted(os.listdir(data_root)) # 排序保证映射稳定 lines [] for idx, cls in enumerate(classes): cls_dir os.path.join(data_root, cls) if not os.path.isdir(cls_dir): continue for fname in os.listdir(cls_dir): if fname.lower().endswith((.jpg, .jpeg, .png)): path os.path.join(cls_dir, fname) lines.append(f{path}\t{idx}) with open(output_txt, w, encodingutf-8) as f: f.write(\n.join(lines)) print(f写入 {len(lines)} 条类别映射{dict(enumerate(classes))})sorted(os.listdir())这一步很关键。如果不排序不同操作系统返回的目录顺序可能不同Windows 上fire可能排在nofire前面Linux 上反过来导致同一份代码在两台机器上标签映射不一致。写进 txt 后用制表符分隔路径和标签读取时按\tsplit 即可。生成完打开 txt 扫一眼确认路径没有中文乱码、标签只有 0 和 1 两种值。3. CNN 模型训练02 脚本里的超参与训练策略3.1 网络结构选型与输入尺寸这个项目大概率用的是一个轻量 CNN可能是三四层卷积加两层全连接或者直接调 torchvision 里的 resnet18 改最后一层。从「含数据集」和「PyQt 界面」的定位看模型不会太重否则 CPU 推理卡顿UI 体验差。输入尺寸常见是 224×224 或 128×128。224 是 ImageNet 标准预训练权重直接可用128 则训练更快适合数据集规模不大的场景。火焰识别数据集如果只有几千张图128 输入配合数据增强足够收敛。import torch.nn as nn import torchvision.models as models def build_model(num_classes2, pretrainedTrue): # 用 resnet18 做 backbone替换最后全连接层 model models.resnet18(pretrainedpretrained) in_features model.fc.in_features model.fc nn.Linear(in_features, num_classes) return model # 如果不用预训练自己搭一个简单 CNN class SimpleCNN(nn.Module): def __init__(self, num_classes2): super().__init__() self.features nn.Sequential( nn.Conv2d(3, 32, 3, padding1), nn.ReLU(), nn.MaxPool2d(2), nn.Conv2d(32, 64, 3, padding1), nn.ReLU(), nn.MaxPool2d(2), nn.Conv2d(64, 128, 3, padding1), nn.ReLU(), nn.MaxPool2d(2), ) self.classifier nn.Sequential( nn.AdaptiveAvgPool2d(1), nn.Flatten(), nn.Linear(128, num_classes) ) def forward(self, x): return self.classifier(self.features(x))pretrainedTrue会下载 ImageNet 权重如果网络不通就设成 False从头训。AdaptiveAvgPool2d(1)的好处是不管输入尺寸多大输出都是 1×1这样换输入分辨率时不用改全连接层维度。火焰识别里火焰的颜色特征红橙黄和纹理特征边缘模糊、亮度高在浅层卷积就能捕捉到所以不用堆太深。我试过把 resnet18 换成 resnet50验证集准确率只涨了不到一个点但训练时间翻倍CPU 推理也变慢不划算。3.2 训练循环与验证集划分02深度学习模型训练.py会读取 01 生成的 txt按比例划分训练集和验证集。常见做法是 8:2 或 7:3。划分时要注意同一张原图旋转出来的增强图不能同时出现在训练集和验证集里否则验证集准确率会虚高。如果 01 脚本已经把增强图写进了同一个 txt那 02 里划分时最好按原图文件名前缀分组或者干脆在 01 里就分好 train/val 两个 txt。import torch from torch.utils.data import Dataset, DataLoader from PIL import Image import torchvision.transforms as T class FireDataset(Dataset): def __init__(self, txt_path, transformNone): self.samples [] with open(txt_path, r, encodingutf-8) as f: for line in f: path, label line.strip().split(\t) self.samples.append((path, int(label))) self.transform transform def __len__(self): return len(self.samples) def __getitem__(self, idx): path, label self.samples[idx] img Image.open(path).convert(RGB) if self.transform: img self.transform(img) return img, label # 训练和验证的 transform 不同验证集不做随机增强 train_tf T.Compose([ T.Resize((128, 128)), T.RandomHorizontalFlip(), T.ToTensor(), T.Normalize(mean[0.5]*3, std[0.5]*3) ]) val_tf T.Compose([ T.Resize((128, 128)), T.ToTensor(), T.Normalize(mean[0.5]*3, std[0.5]*3) ])Normalize的 mean 和 std 都设 0.5 是把像素从 [0,1] 映射到 [-1,1]这是训练 CNN 的常见做法比用 ImageNet 的均值方差更适配灰边填充后的图像。RandomHorizontalFlip对火焰识别是安全的火焰左右翻转后仍然是火焰。但不要加RandomVerticalFlip因为火焰通常向上燃烧垂直翻转会造出「倒着烧」的假样本模型学到错误先验。训练循环里用CrossEntropyLoss和Adam优化器学习率 1e-3 起步跑 20 到 30 个 epoch每个 epoch 后在验证集上算准确率保存验证准确率最高的模型权重。3.3 训练过程中的监控与早停训练脚本一般会打印每个 epoch 的 loss 和 acc。如果 loss 震荡不降先检查学习率是不是太大或者数据归一化有没有做。如果训练 acc 很高但验证 acc 很低说明过拟合加 dropout 或者减小模型容量。早停策略是验证 loss 连续 5 个 epoch 不降就停避免浪费时间。保存模型时用torch.save(model.state_dict(), fire_model.pth)只存权重不存整个模型加载时先实例化结构再load_state_dict这样换设备或换 PyTorch 版本时兼容性更好。4. PyQt 界面推理03 脚本的输入输出与常见报错4.1 界面布局与图像显示链路03pyqt_ui界面.py通常是一个窗口上面有「选择图片」按钮、图片显示区域、推理结果标签。点击按钮后弹出文件选择框选中图片后先显示在 QLabel 上同时把图片送进模型推理输出「火焰 / 无火焰」和置信度。这里容易翻车的地方是图像格式转换OpenCV 读进来是 BGRPyQt 显示需要 RGBPIL 读进来是 RGB 但 QImage 需要指定格式和步长。from PyQt5.QtWidgets import QApplication, QLabel, QPushButton, QVBoxLayout, QWidget, QFileDialog from PyQt5.QtGui import QPixmap, QImage from PIL import Image import torch import sys class FireUI(QWidget): def __init__(self, model, transform): super().__init__() self.model model self.transform transform self.label QLabel(等待选择图片) self.btn QPushButton(选择图片) self.btn.clicked.connect(self.open_image) layout QVBoxLayout() layout.addWidget(self.btn) layout.addWidget(self.label) self.setLayout(layout) def open_image(self): path, _ QFileDialog.getOpenFileName(self, 选择图片, , Images (*.jpg *.png)) if not path: return img Image.open(path).convert(RGB) # 显示 qimg QImage(img.tobytes(), img.width, img.height, img.width*3, QImage.Format_RGB888) self.label.setPixmap(QPixmap.fromImage(qimg).scaled(400, 400)) # 推理 tensor self.transform(img).unsqueeze(0) with torch.no_grad(): out self.model(tensor) prob torch.softmax(out, dim1) pred prob.argmax(dim1).item() conf prob[0][pred].item() self.label.setText(f结果{火焰 if pred 0 else 无火焰}置信度 {conf:.2f})QImage构造时img.width*3是每行字节数RGB 三通道每像素 3 字节这个参数写错会导致图像花屏或倾斜。unsqueeze(0)是加 batch 维度模型 forward 要求输入是 4D 张量。torch.no_grad()关闭梯度计算省内存也提速。推理时如果报RuntimeError: Expected 4D input就是忘了加 batch 维如果报size mismatch就是模型结构或输入尺寸和训练时不一致。4.2 模型加载与设备选择UI 脚本加载模型时要先判断有没有 GPU。如果训练时用了 GPU 保存的权重加载到 CPU 上需要map_locationcpu。反过来如果训练时是 CPU加载到 GPU 上也要显式搬过去。最稳妥的写法是统一用 CPU 推理因为 UI 交互对延迟不敏感CPU 足够。import torch device torch.device(cuda if torch.cuda.is_available() else cpu) model build_model(num_classes2, pretrainedFalse) state_dict torch.load(fire_model.pth, map_locationdevice) model.load_state_dict(state_dict) model.to(device) model.eval() # 切换到推理模式影响 dropout 和 batchnormmodel.eval()必须调用否则 dropout 还在随机丢弃神经元每次推理结果都不一样。如果 UI 上连续选同一张图结果却不同八成是忘了 eval。另外torch.load在 PyTorch 2.6 之后默认weights_onlyTrue如果保存时用了自定义类加载会报错需要显式传weights_onlyFalse但这样有安全风险只加载自己训练的权重时没问题。5. 避坑与排查火焰识别项目里最容易翻车的五件事5.1 现象训练准确率 99%实际推理全是「无火焰」原因通常是标签映射错位。01 脚本生成 txt 时按sorted(os.listdir())排序fire文件夹排在前标签 0nofire排在后标签 1。但 UI 脚本里写的是火焰 if pred 1 else 无火焰把 0 和 1 的含义搞反了。解决方法是打开 01 生成的 txt看第一行路径属于哪个文件夹确认标签值然后统一三个脚本里的映射。我习惯在 01 脚本里把class_to_idx打印出来后面两个脚本直接引用同一个字典。5.2 现象验证集准确率很高但换一张手机拍的火焰图就识别失败原因是训练数据太单一。数据集里的火焰图可能都是同一场景、同一光照、同一角度模型学到了背景特征而不是火焰特征。比如所有火焰图都有黑色背景模型把「黑色背景」当成了火焰的判别依据。解决方法是检查数据集里有没有负样本无火焰图和火焰图背景相似的情况如果没有手动加一些。另外增强时除了旋转还可以加随机亮度、对比度扰动让模型关注火焰本身的颜色和纹理。5.3 现象运行 02 脚本时报CUDA out of memory原因可能是 batch size 太大或者图像输入尺寸太大。128×128 输入、batch size 32 在 4GB 显存的卡上一般没问题但如果改成 224×224 且 batch size 64显存就不够了。解决方法是把 batch size 降到 16 或 8或者把输入尺寸降到 96×96。如果用的是 CPU不存在显存问题但内存不够也会报错同样降 batch size。另外检查有没有在训练循环里累积了计算图比如忘了optimizer.zero_grad()。5.4 现象PyQt 界面点「选择图片」没反应或者图片显示不全原因是 QLabel 没有设置固定尺寸或者QImage的步长参数写错。QLabel 默认按内容大小显示图片大了窗口就被撑开图片小了就留白。解决方法是给 QLabel 设setFixedSize(400, 400)然后用QPixmap.scaled缩放。步长参数img.width*3如果写成img.width图像会错位成斜条纹。另外文件选择框的过滤器写Images (*.jpg *.png)如果图片是.jpeg或.bmp就选不中需要补全后缀。5.5 现象换自己的数据集后01 脚本生成的 txt 是空的原因是目录结构不对。01 脚本预期data_root下直接是两个类别文件夹每个文件夹里是图片。如果自己的数据是data_root/fire/images/xxx.jpg这种多一层结构os.listdir拿到的是images文件夹而不是图片文件过滤条件endswith(.jpg)就不通过。解决方法是把图片直接放在类别文件夹下或者改 01 脚本用os.walk递归遍历。另外注意路径里不要有中文和空格OpenCV 和 PIL 对中文路径的支持时好时坏容易出玄学问题。6. 进阶技巧用混淆矩阵和阈值调优把误报压下去训练完模型只看准确率是不够的。火焰识别场景里漏报有火焰判成无火焰和误报无火焰判成有火焰的代价不同。漏报可能延误处置误报会浪费人力。用混淆矩阵能看清模型到底偏哪边。在验证集上跑一遍推理统计 TP、TN、FP、FN然后算精确率和召回率。如果召回率低说明漏报多可以把分类阈值从 0.5 降到 0.3让模型更倾向于判为火焰如果精确率低说明误报多把阈值升到 0.7。import numpy as np from sklearn.metrics import confusion_matrix, classification_report def evaluate(model, dataloader, device, threshold0.5): model.eval() all_preds, all_labels [], [] with torch.no_grad(): for imgs, labels in dataloader: imgs imgs.to(device) outputs model(imgs) probs torch.softmax(outputs, dim1)[:, 1] # 取类别 1 的概率 preds (probs threshold).long().cpu().numpy() all_preds.extend(preds) all_labels.extend(labels.numpy()) print(confusion_matrix(all_labels, all_preds)) print(classification_report(all_labels, all_preds, target_names[fire, nofire]))probs threshold这一行是阈值调优的核心。默认 0.5 是等代价假设实际场景里按需调整。调完阈值后把 UI 脚本里的argmax也改成同样的阈值判断保持训练评估和线上推理一致。另外可以画 ROC 曲线找最佳阈值点sklearn.metrics.roc_curve直接给 FPR 和 TPR选 Youden 指数最大的点。我一般会在验证集上把阈值从 0.1 到 0.9 扫一遍看哪个点的 F1 最高然后固定下来写进配置文件而不是硬编码在脚本里。从那以后我每次训练完新模型都强制走一遍混淆矩阵和阈值扫描不再只看一个准确率数字就收工。希望帮到你。本文还有配套的精品资源点击获取