烟火图像识别与分类:从数据增强到时序滤波的实战方案
简介面向烟火图像识别与分类学习者与研究者的完整资源包覆盖图像预处理、特征提取、模型训练、评估与优化等核心环节可应用于安全监控、烟花表演分析、火灾预警等场景。资源以Python脚本和Jupyter Notebook形式提供去噪、灰度化、二值化等预处理实现以及SIFT/SURF传统特征与CNN深度特征提取、SVM/随机森林/AlexNet/VGG/ResNet等模型训练逻辑近3600张BMP图片构成烟火样本集另有XML配置、测试JPG等共3627个文件压缩包仅11.18MB。代码注释较完整可对照逐步复现分类流程也能基于样本集扩展数据扩增与调参实验节省环境搭建与调试时间。已有217人学习下载适合课程设计、毕业设计或计算机视觉入门者参考实践。1. 烟火图像的识别与分类最难的不是烟火森林防火摄像头每天抓到的画面里真正的烟火可能只占万分之几。做烟火图像识别与分类前期花时间最多的往往是负样本——那些长得像烟的雾、像火的晚霞、像烟花的灯光。一个只训练了正样本的模型在实景里能把一朵白云判成烟火误报率高到没法用。这件事从任务划分上看是个典型的图像分类问题给定一张图判断有没有烟火进一步区分是火焰、烟雾还是干扰物。但烟火和常规分类任务有本质差别——烟火边界的模糊性、白天和夜间的光差异、以及烟雾那种扩散性的纹理让很多在ImageNet上表现良好的模型直接失效。适合的人群也很清楚做森林防火、化工园区安防、或消防早期预警系统的算法工程师以及刚入门想找个落地场景练习深度学习图像分类的人。处理这类问题需要把数据构成、模型选型、类别不平衡、时序上下文四件事同时想清楚。这篇文章就按这套路径把烟火图像识别与分类从数据准备到部署调参讲完整。先立一个观点单帧图像分类永远做不出可用的烟火预警可靠的方案一定是在单帧模型外面再挂一层时间维度的判定逻辑。后面的章节会逐步展开把每一步的命令、参数和坑位都摆出来。2. 数据整备烟火图像分类的样本构成与标注边界烟火图像识别与分类的模型效果百分之六十由数据决定。这里的数据不只是数量还包括正负样本的比例、类别的定义边界、以及图像质量本身。什么样的图算烟火什么样的图不算这个边界如果不定义清楚标注人员会给你交出一批互相矛盾的标签模型学到的就是一锅粥。2.1 正样本与负样本怎么划定先把分类类别定下来。常见做法是三类fire火焰、smoke烟雾、normal无烟火。火焰和烟雾分开是因为它们在颜色、纹理、运动形态上都不同合成一个烟火类会让模型内部特征冲突。但也有人做二分类只区分“有烟火”和“无烟火”这种方案在实时监控里更快但代价是模型被迫同时拟合火焰的红色高饱和纹理和烟雾的低饱和扩散纹理收敛难度更大。负样本直接决定误报率。收集负样本时要刻意包含云雾、晨雾、灯光、夕阳、红色建筑外墙、蒸汽、灰尘、烧荒后的土地、夜间红色车灯。这些是烟火识别里最常见的干扰来源。负样本的占比建议在 60%~70%尤其是在多分类设定下normal 类必须过半。收集途径没什么捷径公开数据集中抽取加自己截图能拿到多少取决于项目预算和时间但至少保证总样本量在 5000 张以上按类别分别计。2.2 数据增强策略与破坏性抗干扰烟火图像在真实场景中会有大量干扰镜头脏污、雨滴、强光直射、树叶遮挡。如果训练时不加这些干扰模型在部署后会非常脆弱。数据增强里除了常规的翻转和裁剪针对性增强更重要。常规增强部分常用的参数如下表增强方法典型参数值适用场景RandomHorizontalFlipp0.5一般场景RandomResizedCropscale(0.5, 1.0)模拟变焦和不同视距ColorJitterbrightness0.3, contrast0.3, saturation0.2白天光线变化RandomRotationdegrees15摄像头安装角度差异GaussianBlurkernel5, sigma(0.1, 2.0)镜头失焦和雾气针对性增强部分一是随机遮挡RandomErasing模拟树叶和其他物体挡住火焰或烟柱的情况区域比例大概在画面 10%~30%二是加入雨滴条纹噪声对夜间火灾报警场景尤其重要——夜间单帧的火焰亮度极高雨滴的反光几乎和火焰高光长得一样三是做亮度整体偏移模拟黄昏和黎明的光线变化。2.3 用脚本做一次增强流水线下面给出一个基于 PyTorch 的增强定义代码它可以直接用在训练脚本里。import torch from torchvision import transforms import random class RandomRainDrop: 模拟摄像头镜头的雨滴干扰 def __init__(self, prob0.3, num_drops8): self.prob prob self.num_drops num_drops def __call__(self, img): if random.random() self.prob: return img img img.clone() h, w img.shape[1], img.shape[2] for _ in range(self.num_drops): x, y random.randint(0, w), random.randint(0, h) r random.randint(2, 5) # 用一个高斯圆模拟水滴折射 for i in range(x-r, xr): for j in range(y-r, yr): if 0 i w and 0 j h: if (i-x)**2 (j-y)**2 r**2: img[:, j, i] torch.tensor([0.8, 0.8, 0.8]) # 浅灰色水滴 return img def build_transform(modetrain): if mode train: return transforms.Compose([ transforms.RandomHorizontalFlip(p0.5), transforms.RandomResizedCrop(size(224, 224), scale(0.5, 1.0)), transforms.ColorJitter(brightness0.3, contrast0.3, saturation0.2), transforms.ToTensor(), RandomRainDrop(prob0.3, num_drops6), transforms.RandomErasing(p0.2, scale(0.02, 0.2)), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ]) else: # 验证集只做归一化不做增强保证评估稳定性 return transforms.Compose([ transforms.Resize((224, 224)), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ])逻辑说明训练增强里用RandomResizedCrop把物体从不同缩放和位置裁出来这比单纯 Resize 更能模拟摄像头在不同距离下看到的烟火形态。RandomRainDrop是自定义算子在张量级别把特定圆形区域覆成浅灰色模拟镜头雨滴。验证集保持干净这样模型在训练中引入的噪声不会干扰评估结果。参数说明RandomErasing的p0.2意味着 20% 的样本会被随机擦除一块区域scale控制了擦除面积占图片的比例。RandomRainDrop的num_drops6是雨滴数量一般设置在 4~10 之间太多会导致模型过度关注雨滴本身太少又起不到抗干扰作用。3. 图像分类模型选型与基线训练烟火图像的识别与分类在模型层面已经从早期的 VGG、ResNet 走到了 Vision Transformer 的时代。选型逻辑不复杂小数据量用 CNN大数据量或需要远程语义关联时用 Transformer。烟是扩散性的纹理跨度大火是高局部对比度的边缘特征强。这两种特征对模型的要求不太一样。3.1 从 CNN 到 Vision Transformer烟火场景怎么选ResNet 系列依然是烟火识别里最稳的基线。ResNet50 可以在 5000 张量级的样本上稳定收敛推理速度快容易部署到边缘设备。如果你在做一个原型验证先从 ResNet50 开始不要直接上大模型。如果一个模型要同时抓住烟的大范围扩散纹理和火的局部高对比边缘纯 CNN 的感受野可能不够——烟在上千像素的大图中是弥散的局部感受野只能看到“雾蒙蒙”看不到整体形状。这种场景下Vision Transformer 的全局注意力机制更合适。Swin Transformer 和最新的 ConvNeXt 都值得试但要注意数据量门槛Transformer 类模型需要更多数据经验值是至少 1 万张以上否则容易过拟合。轻量化方向MobileNetV3 和 ShuffleNetV2 适合边缘盒子。代价是精度会有 2~4 个百分点的下降但换来的是在瑞芯微、海思这类芯片上能跑实时。3.2 基线训练的关键参数配置直接把训练脚本写清楚参数值按烟火场景调过可以直接作为初始值使用。import torch import torch.nn as nn from torchvision import models from torch.utils.data import DataLoader, Dataset from torch.optim import AdamW from torch.optim.lr_scheduler import CosineAnnealingLR class SmokeFireDataset(Dataset): 读取烟火分类数据集的 Dataset 封装 def __init__(self, image_paths, labels, transformNone): self.image_paths image_paths self.labels labels self.transform transform def __len__(self): return len(self.image_paths) def __getitem__(self, idx): img self.image_paths[idx] label self.labels[idx] if self.transform: img self.transform(img) return img, label def train_one_epoch(model, loader, criterion, optimizer, device): model.train() running_loss 0.0 correct 0 total 0 for images, labels in loader: images, labels images.to(device), labels.to(device) optimizer.zero_grad() outputs model(images) loss criterion(outputs, labels) loss.backward() optimizer.step() running_loss loss.item() _, predicted torch.max(outputs, 1) total labels.size(0) correct (predicted labels).sum().item() return running_loss / len(loader), correct / total device torch.device(cuda if torch.cuda.is_available() else cpu) # 用 ImageNet 预训练权重初始化收敛速度远快于随机初始化 model models.resnet50(weightsmodels.ResNet50_Weights.IMAGENET1K_V2) num_classes 3 # fire, smoke, normal model.fc nn.Linear(model.fc.in_features, num_classes) model model.to(device) criterion nn.CrossEntropyLoss() optimizer AdamW(model.parameters(), lr1e-4, weight_decay5e-4) scheduler CosineAnnealingLR(optimizer, T_max30) # 训练 30 个 epochbatch_size32 for epoch in range(30): loss, acc train_one_epoch(model, train_loader, criterion, optimizer, device) scheduler.step() print(fEpoch {epoch1}, Loss: {loss:.4f}, Acc: {acc:.4f})逻辑说明分类头直接替换成 3 输出fire/smoke/normal。优化器用 AdamW 而不是 SGD因为烟火数据量不大SGD 需要精细调学习率AdamW 的默认设置更省心。学习率 1e-4 是迁移学习从预训练模型开始的稳妥选择如果从零训练需要提高到 1e-3但一般不推荐零训练。参数说明weight_decay5e-4是正则化强度数据量越少这个值越高可以防过拟合。CosineAnnealingLR的T_max30对应训练总轮数余弦退火会在后期逐渐降低学习率帮助模型收敛到更平的极小值。如果验证集 loss 出现震荡先检查 batch size 是否过小烟火图像中火焰区域占比大不同 batch 的样本难度差异明显batch size 小于 16 时 loss 会跳得比较厉害。3.3 评估指标选择不能只看准确率烟火识别与分类的准确率是个陷阱。如果 normal 类占 70%一个“全都预测 normal”的模型准确率就有 70%看起来并不差。真正要盯的是两个指标recall分类对烟火的召回率和误检率把 normal 判成烟火的样本占比。用 sklearn 可以直接算出来。from sklearn.metrics import classification_report, confusion_matrix import numpy as np def evaluate_model(model, val_loader, device): model.eval() all_preds [] all_labels [] with torch.no_grad(): for images, labels in val_loader: images, labels images.to(device), labels.to(device) outputs model(images) _, preds torch.max(outputs, 1) all_preds.extend(preds.cpu().numpy()) all_labels.extend(labels.cpu().numpy()) # target_names 对应类别顺序0fire, 1smoke, 2normal print(classification_report(all_labels, all_preds, target_names[fire, smoke, normal])) print(confusion_matrix(all_labels, all_preds)) # 使用示例: 报告里重点看 fire 和 smoke 两行的 recall # 以及 normal 行中被误判为 fire/smoke 的数量 evaluate_model(model, val_loader, device)逻辑说明classification_report的输出里每一类都有 precision、recall、f1-score。烟火识别场景下fire 和 smoke 的 recall 是核心分别代表“真实的火有多少被抓到”和“真实的烟有多少被抓到”。confusion_matrix则可以直接看到 normal 被误判成 fire 的数量这个数字除以 normal 总量就是误报率。参数说明标签的顺序必须和数据集读取时一致。如果训练时用的是文件夹名映射评估脚本里的类别名要和映射字典保持一致否则报告里的数字对不上。4. 类别不均衡与困难样本烟火分类最容易翻车的三个坑烟火图像数据天然存在类别不均衡真实场景里 normal 帧数量远大于烟火帧。除此之外还有两个隐藏问题——困难样本分布不均和标注噪声敏感。这三件事如果不处理模型的精度曲线会非常好看但一上实景就崩。4.1 类别不均衡的两种解法第一种是重采样。把 fire 和 smoke 类在每一个 epoch 里复制若干次让三个类别在训练时数量接近。第二种是改损失函数用带权重的 CrossEntropyLoss 或者 Focal Loss。import torch.nn as nn import torch.nn.functional as F class FocalLoss(nn.Module): Focal Loss: 让模型把注意力集中在难分类的样本上 用于解决烟火图像中火焰与烟雾难分、烟火与干扰物难分的问题 def __init__(self, alphaNone, gamma2.0): super().__init__() self.alpha alpha # 类别权重传 tensor 或 None self.gamma gamma # 聚焦参数越大越关注困难样本 def forward(self, inputs, targets): ce_loss F.cross_entropy(inputs, targets, weightself.alpha, reductionnone) pt torch.exp(-ce_loss) # 预测正确的置信度 focal_loss (1 - pt) ** self.gamma * ce_loss return focal_loss.mean() # alpha 按样本数的反比设置normal 多就给低权重 # fire 和 smoke 少就给高权重具体数值按训练集统计计算 alpha torch.tensor([1.0, 2.0, 0.5]).to(device) criterion FocalLoss(alphaalpha, gamma2.0)逻辑说明Focal Loss的核心是(1 - pt) ** gamma这个调制系数。当某个样本被模型高置信度预测正确pt 接近 1时调制系数接近 0loss 被压低当样本被预测错误或置信度低pt 小时loss 被保留甚至放大。这样模型会把梯度集中到那些“像烟但不是烟”“像火但是反光”的困难样本上。参数说明gamma2.0是最常用的值调大如 2.5~3.0会让模型更激进去拟合困难样本但可能引入过拟合调小如 1.0则接近普通交叉熵。alpha的三维分别对 fire、smoke、normal 三个类。注意这个顺序必须与 dataset 的 label 一致。4.2 困难样本挖掘把模型最容易看错的样本捞出来只靠 Focal Loss 还不够更直接的做法是主动把模型预测错误的样本找出来人工复核后把它们加入训练集或者复制加权。这个流程在烟火识别里非常常见因为误报和漏报样本长得太像模型自己区分不了的时候需要人为梳理边界样本。具体操作用当前最好的模型跑一遍验证集把 label 是 normal、预测是 fire 的图按置信度从高到低排列人工看这些图里有没有共同的视觉模式——比如黄色路灯、红色车尾灯、黄昏时段的建筑反光。把这些共同模式记下来回去扩充对应场景的负样本。这个环节不要自动化。烟火图像里的困难样本模式往往依赖场景知识只有熟悉部署现场的人才能判断“这种光在摄像头里会不会出现”。自动化的 hard example mining 在这里容易把噪声也挖出来。4.3 数据增强的边界别把烟火增强没了烟火图像识别的增强不能乱做。对一般分类任务RandomErasing 擦掉一个小区域没关系但对烟火识别如果你擦掉的刚好是图像里唯一的火焰区域模型看到的是一个“无火”的图但标签是“fire”等于直接灌入噪声。解决方法是给增强算子上限。擦除面积控制在 15% 以内且尽量不要擦掉图像的中心区域——烟火摄像头通常把兴趣区域放在中央。这个限制在本章第一段代码里已经有体现RandomErasing的scale(0.02, 0.2)就是按这个思路设置的经验值。5. 从单帧到序列用时间上下文把误报压下来如果你把上一章的模型直接部署到森林防火系统里单个摄像头一天能报几十次误报值班人员很快就会把报警当成狼来了。单帧图像分类的极限就在这里一帧画面里的信息太有限很多干扰物和烟火确实无法仅凭空间特征区分。烟火的核心特征是什么是时序上的变化——火焰在闪烁烟在扩散和移动。一个红色区域静止不动大概率是建筑外墙它在连续帧里都是那个形状一个红色区域在 3 帧里亮度反复变化、形状略有抖动那就是火。这个思路在图像识别领域落地就是帧间差分和时序平滑它比训练一个 3D CNN如 SlowFast 或 C3D更轻量也更可控。5.1 帧差法加状态机最简单的时序判定常见的做法是给单帧模型加一个状态机连续 N 帧里有 M 帧被分类为 fire 或 smoke才真正触发报警。比如连续 5 帧里至少 3 帧判断为烟火才让结果生效。实现上可以用一个滑动窗口或者更轻量的指数移动平均EMA。import numpy as np class TemporalFilter: 基于指数移动平均的时序过滤器 平滑单帧分类结果抑制单帧突发的误报 def __init__(self, alpha0.6, threshold0.6): self.alpha alpha # EMA 平滑系数越大越相信当前帧 self.threshold threshold # 触发报警的置信度阈值 self.ema None # 保存每个类别的平滑概率 def update(self, class_probs): class_probs: shape(num_classes,) 的 numpy 数组 例如 model 输出的 softmax 结果 if self.ema is None: self.ema class_probs else: # EMA 公式new_ema alpha * current (1-alpha) * old_ema self.ema self.alpha * class_probs (1 - self.alpha) * self.ema # 返回平滑后的类别和对应概率 pred_class np.argmax(self.ema) pred_prob self.ema[pred_class] return pred_class, pred_prob def should_alarm(self, class_probs, smoke_id1, fire_id0): 判断是否触发报警: 要求起伏后的烟火概率超过阈值 pred_class, pred_prob self.update(class_probs) if pred_class in [smoke_id, fire_id] and pred_prob self.threshold: return True return False # 使用示例 filter_ TemporalFilter(alpha0.6, threshold0.55) # 模拟连续读取摄像头帧 for frame in video_stream: probs model(frame) # 模型推理得到形状为 (3,) 的概率 probs torch.softmax(probs, dim0).cpu().numpy() if filter_.should_alarm(probs): trigger_alarm() # 触发报警逻辑说明EMA的核心是让类别概率在时间轴上做平滑。单帧模型可能在某一帧把云判成了 smoke概率 0.8但下一帧云飘过去了概率回到 0.1。EMA 把这个突刺平滑掉不会触发报警。同理真实火情是持续多帧的置信度会随时间累加越过阈值后稳定触发。参数说明alpha0.6表示当前帧权重占 60%历史信息占 40%。alpha越大反应越快但抗误报能力越弱。摄像头帧率是 25fps 时alpha0.5~0.6比较合适如果帧率低如 1fps 的抓拍图需要把alpha调大比如 0.8因为帧间间隔长历史信息的参考价值降低。threshold0.55是基于“宁可漏报也不误报”或相反需求的权衡值对烟火报警建议设得保守实际部署前用一段真实监控视频回放来标定。5.2 多帧投票替代方案EMA 之外还有一个更直观的方案是等间隔抽帧投票。比如每 2 秒抽 5 帧5 帧里至少 3 帧判定为烟火才报警。这个方案的优点是可解释性强值班人员能理解“3 帧里有火焰”缺点是需要缓存帧内存开销略大。实际工程项目里我一般会把两种方案都做出来用一段真实烟火视频做对比测试在漏报率和误报率之间找平衡点。不同现场的干扰物差异很大参数没有普适值只有适合你现场的值。6. 阈值校准、模型量化与误报闭环的小技巧到了部署阶段最关键的事情是让模型在目标设备上的表现与验证集保持接近。这里有两个容易忽略的细节softmax 概率校准和量化精度损失。softmax 输出的概率并不等于真实置信度模型在 Out-of-Distribution 样本上往往过度自信。一个常见做法是在验证集上做温度缩放Temperature Scaling把概率分布“压扁”一点让阈值判定更可靠。具体到烟火识别里这个步骤能让 EMA 滤波的阈值设置更有物理意义——0.55 的阈值对应的误报率是多少可以用验证集真实计算出来。量化是部署到边缘设备的必经一环。ResNet50 用 INT8 量化后参数量减小 4 倍推理速度提升明显但在烟火识别上通常有一个可以接受的精度回退fire 类 recall 下降 1%~3%。量化前务必用验证集做一遍精度校准如果 recall 下降超过 5%就该换更大的模型或者跳过量化直接在 GPU 上跑。下面的代码给出一段标准化量化流程import torch from torchvision import models model models.resnet50(weightsmodels.ResNet50_Weights.IMAGENET1K_V2) model.fc torch.nn.Linear(model.fc.in_features, 3) model.load_state_dict(torch.load(best_model.pth)) # 切换为推理模式并 einschalten 量化配置 model.eval() model.qconfig torch.quantization.get_default_qconfig(fbgemm) torch.quantization.prepare(model, inplaceTrue) # 在验证集子集上跑几个 batch收集激活值分布 for images, _ in val_loader: model(images) torch.quantization.convert(model, inplaceTrue) # 量化后模型可直接部署到 CPU, # 实测在 Intel 平台上的推理延迟能降低 3~4 倍 torch.save(model.state_dict(), best_model_int8.pth)逻辑说明量化前用少量验证集图片跑一遍前向是为了让量化器观察到每层激活值的动态范围避免后续部署时出现极端值溢出。get_default_qconfig(fbgemm)是 x86 CPU 上的标准配置如果部署目标为 ARM 架构需要改用qnnpack。参数说明量化校准的批次大小建议在 200~500 张之间太少会统计不准动态范围太多则耗时且收益递减。校准数据要覆盖 fire、smoke、normal 三类不能只用 normal 类校准否则烟火类别的激活值分布没有被统计进去。最后提一个容易忽略的技巧把误报样本做成回灌集合。无论是数据增强、EMA 还是阈值都不可能一次性把误报清零。上线后每天把模型报出的疑似样本导入一个独立目录每周人工核查一次确认是误报就加入负样本集重训。这是一个持续优化的闭环也是烟火识别系统真正能在现场长期稳定运行的唯一方式。本文还有配套的精品资源点击获取
上一篇/下一篇内容由系统自动关联
返回资讯列表 →