简介:这份资源面向计算机视觉初学者与安全监控、火灾预警方向的开发者,围绕烟火图像的识别与分类任务,提供从图像预处理、特征提取到模型训练与评估的完整实践素材。压缩包共3627个文件,以3617张bmp烟火样本图片为主,另含xml标注、jpg示例图、py训练脚本、ipynb预处理与实验笔记及项目配置,整体约11.18MB,目录结构便于按类别检索。已有217人学习下载。读者可借助现成样本与代码,动手完成去噪、灰度化、二值化等预处理,对比SIFT、SURF与CNN特征提取效果,并训练SVM、随机森林或ResNet等模型,结合准确率、精确率、召回率与F1分数评估优化,快速搭建可复用的烟火识别流程。
1. 烟火图像识别与分类:从一堆 BMP 文件名到能跑通的分类器
手里拿到一批烟火图像,文件名长这样:1-1004.BMP、0-00383.BMP、0-00382.BMP、1-1186.BMP、0-001002.BMP。前缀的 0 和 1 就是类别标签,后面是序号。这种命名方式在烟火识别与分类项目里非常典型——正样本是烟花绽放或火焰燃烧的画面,负样本是普通夜景、灯光、云霞等容易混淆的场景。整个资源包围绕这条数据主线展开:code.ipynb负责图像预处理和特征可视化,main.py负责模型训练与验证,temp.jpg是单张推理的样本入口,.idea是开发环境配置。它解决的核心问题是:给定一张烟火图像,判断它属于哪一类,并给出可复现的训练和评估流程。适合做安全监控预警、烟花表演分析、火灾早期识别的从业者,也适合想拿一个完整图像分类 pipeline 练手的人。
2. 图像预处理与数据管道:把 BMP 喂进模型前要做的四件事
2.1 为什么预处理决定了模型上限
烟火图像有个很麻烦的特点:火焰和烟花本身是高亮、高饱和区域,但背景可能是深夜、烟雾、城市灯光。如果直接把原始 BMP 丢进网络,模型很容易学到“亮的就是烟火”这种捷径特征,换一个场景就翻车。所以预处理不是走形式,它直接决定模型能不能学到真正的形状和纹理。
常见做法是四步走:去噪、增强、灰度化、二值化。去噪用高斯滤波或中值滤波,高斯适合高斯噪声,中值适合椒盐噪声,烟火图像里传感器热噪声偏多,我一般先用高斯。增强用直方图均衡化或 CLAHE,把火焰边缘的对比度拉出来。灰度化看后续模型:如果用传统特征如 SIFT、SURF,灰度图足够;如果用 CNN,保留 RGB 三通道更好,因为颜色分布本身就是烟火的重要判别信息。二值化只适合做形状分析或连通域统计,不要拿二值图去训练深度模型,会丢掉太多信息。
code.ipynb里通常会把这几步串成一个可视化流程,方便你逐张检查预处理效果。下面这段代码是我按这个资源场景补全的常见写法,你可以直接对照 notebook 里的单元格改。
import cv2 import numpy as np import os def preprocess_firework(img_path, use_gray=False, use_binary=False): # 读取 BMP,OpenCV 默认 BGR img = cv2.imread(img_path) if img is None: raise FileNotFoundError(f"读不到图像: {img_path}") # 高斯滤波去噪,kernel 取 5x5,sigma 自动 denoised = cv2.GaussianBlur(img, (5, 5), 0) # 转 LAB 空间,对 L 通道做 CLAHE,避免 RGB 直接均衡化偏色 lab = cv2.cvtColor(denoised, cv2.COLOR_BGR2LAB) l, a, b = cv2.split(lab) clahe = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8, 8)) l_eq = clahe.apply(l) enhanced = cv2.merge((l_eq, a, b)) enhanced = cv2.cvtColor(enhanced, cv2.COLOR_LAB2BGR) if use_gray: enhanced = cv2.cvtColor(enhanced, cv2.COLOR_BGR2GRAY) if use_binary: # 自适应阈值,适合光照不均的烟火场景 enhanced = cv2.adaptiveThreshold( enhanced, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY, 11, 2 ) return enhanced # 批量处理示例 data_dir = "./firework_data" save_dir = "./preprocessed" os.makedirs(save_dir, exist_ok=True) for fname in os.listdir(data_dir): if fname.lower().endswith(".bmp"): out = preprocess_firework(os.path.join(data_dir, fname)) cv2.imwrite(os.path.join(save_dir, fname), out)逻辑说明:先读图,再做高斯去噪,然后在 LAB 空间对亮度通道做 CLAHE,这样不会把颜色通道一起拉爆。clipLimit=2.0是经验值,调高到 4.0 会让火焰边缘更锐,但噪声也会放大。tileGridSize=(8,8)适合 512×512 左右的图,图更大就改成 16×16。二值化那一步只建议在传统特征流程里用,CNN 训练时把use_binary关掉。
2.2 数据集划分与标签解析
文件名前缀就是标签,0-开头是一类,1-开头是另一类。解析的时候不要用split("-")[0]就完事,因为有的文件名可能是0-001002.BMP,序号部分有前导零,但类别位是稳定的。我一般写一个正则,把类别和序号都提出来,顺便检查有没有漏标或错标。
import re import pandas as pd from sklearn.model_selection import train_test_split pattern = re.compile(r"^(\d+)-(\d+)\.BMP$", re.IGNORECASE) records = [] for fname in os.listdir(data_dir): m = pattern.match(fname) if m: label = int(m.group(1)) idx = int(m.group(2)) records.append({"filename": fname, "label": label, "index": idx}) df = pd.DataFrame(records) print(df["label"].value_counts()) # 先看类别是否均衡 # 按 7:2:1 划分训练、验证、测试 train_df, temp_df = train_test_split( df, test_size=0.3, stratify=df["label"], random_state=42 ) val_df, test_df = train_test_split( temp_df, test_size=1/3, stratify=temp_df["label"], random_state=42 )参数说明:stratify=df["label"]保证划分后各类比例一致,烟火数据里如果 0 类远多于 1 类,不加这个参数会导致验证集里某一类几乎消失。random_state=42固定随机种子,方便复现。如果value_counts()出来差距超过 5:1,就要考虑过采样或类别权重,不然后面召回率会很难看。
提示:BMP 文件体积大,批量读取时不要一次性全部加载到内存。用
tf.data或DataLoader做流式读取,或者先统一转成 JPEG 再训练,能省不少显存和时间。
3. 特征提取与模型选型:SIFT、SURF 还是直接上 CNN
3.1 传统特征和深度特征的分界线
这个资源里同时提到了 SIFT、SURF 和 CNN,说明作者想覆盖两条路线。我的经验是:数据量小于 2000 张、类别差异主要体现在形状和边缘时,SIFT + SVM 仍然能打,而且训练快、可解释。数据量上去以后,CNN 端到端学习明显更稳,因为烟火图像的类内差异很大——同样是烟花,不同颜色、不同曝光、不同背景,手工特征很难覆盖全。
SIFT 的关键点检测对尺度变化鲁棒,但计算慢,一张 512×512 的图可能要几百毫秒。SURF 是它的加速版,但专利问题在一些场景下要留意。实际做烟火分类,我一般会先跑一个小实验:用 SIFT 提取关键点,聚成 BoW 词袋,再喂 SVM,看验证集准确率能不能过 80%。如果能过,说明形状特征够用;如果过不了,直接换 CNN,别在传统特征上死磕。
code.ipynb里如果有 SIFT 的可视化单元格,重点看关键点是不是落在火焰边缘和烟花轮廓上。如果关键点大量落在背景灯光上,说明预处理没做到位,或者需要加掩膜把高亮区域单独提出来。
3.2 CNN 模型结构怎么选
main.py里通常会定义一个 CNN。烟火图像分类不需要上来就 ResNet-152,参数量太大,小数据集上反而容易过拟合。我一般从一个小型 VGG 或自定义 4 层卷积开始,配合全局平均池化,最后接全连接分类头。下面是一个可复现的结构示例。
import torch import torch.nn as nn class FireworkCNN(nn.Module): def __init__(self, num_classes=2): super().__init__() self.features = nn.Sequential( # 输入 3x224x224 nn.Conv2d(3, 32, kernel_size=3, padding=1), nn.BatchNorm2d(32), nn.ReLU(inplace=True), nn.MaxPool2d(2), # 112x112 nn.Conv2d(32, 64, kernel_size=3, padding=1), nn.BatchNorm2d(64), nn.ReLU(inplace=True), nn.MaxPool2d(2), # 56x56 nn.Conv2d(64, 128, kernel_size=3, padding=1), nn.BatchNorm2d(128), nn.ReLU(inplace=True), nn.MaxPool2d(2), # 28x28 nn.Conv2d(128, 256, kernel_size=3, padding=1), nn.BatchNorm2d(256), nn.ReLU(inplace=True), nn.AdaptiveAvgPool2d(1) # 1x1 ) self.classifier = nn.Sequential( nn.Flatten(), nn.Dropout(0.5), nn.Linear(256, num_classes) ) def forward(self, x): x = self.features(x) return self.classifier(x)逻辑说明:四层卷积,通道数从 32 翻到 256,每层后面跟 BatchNorm 和 ReLU,池化用 MaxPool。最后用AdaptiveAvgPool2d(1)把空间维度压成 1×1,再接 Dropout 和全连接。Dropout(0.5)在小数据集上是必要的,不然训练准确率很快到 99%,验证集却不动。num_classes=2对应二分类,如果后面要分多种烟火类型,改这个数就行。
训练循环里,损失函数用CrossEntropyLoss,优化器用Adam,学习率从1e-3开始,每 10 个 epoch 乘 0.1。批大小 32 或 64,看显存。如果验证集准确率震荡,把学习率降到1e-4,或者加学习率预热。
3.3 数据扩增的参数怎么设
烟火图像的拍摄角度、距离、光照变化大,扩增是必须的。但不要无脑加,旋转角度太大可能把烟花转成不合理的形态。我一般用:随机水平翻转、±15 度旋转、亮度对比度微调、随机裁剪。颜色抖动要谨慎,因为颜色是烟火的重要特征,抖动太猛会把红色烟花变成橙色,标签就不对了。
from torchvision import transforms train_tf = transforms.Compose([ transforms.Resize((224, 224)), transforms.RandomHorizontalFlip(p=0.5), transforms.RandomRotation(15), transforms.ColorJitter(brightness=0.2, contrast=0.2, saturation=0.1), transforms.ToTensor(), transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]) ]) val_tf = transforms.Compose([ transforms.Resize((224, 224)), transforms.ToTensor(), transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]) ])参数说明:RandomRotation(15)是角度范围,超过 30 度烟火轨迹会变得不自然。ColorJitter里saturation=0.1调得很轻,就是为了保住颜色特征。归一化用的 ImageNet 均值方差,如果你从零训练,也可以改成自己数据集的统计值,但差别通常不大。
4. 训练、评估与调参:准确率上不去先查这五个地方
4.1 训练循环里必须记录的指标
main.py的训练部分,除了 loss,一定要记录每个 epoch 的训练准确率和验证准确率。只看 loss 容易被误导——loss 在降,但验证准确率可能已经过拟合了。评估指标用准确率、精确率、召回率和 F1。烟火识别场景里,召回率往往比精确率更重要,因为漏报一个真实烟火比误报一个普通灯光代价更大。
from sklearn.metrics import classification_report, confusion_matrix def evaluate(model, loader, device): model.eval() all_preds, all_labels = [], [] with torch.no_grad(): for imgs, labels in loader: imgs, labels = imgs.to(device), labels.to(device) outputs = model(imgs) preds = outputs.argmax(dim=1) all_preds.extend(preds.cpu().numpy()) all_labels.extend(labels.cpu().numpy()) print(confusion_matrix(all_labels, all_preds)) print(classification_report(all_labels, all_preds, digits=4))逻辑说明:model.eval()关掉 Dropout 和 BatchNorm 的训练行为,torch.no_grad()省显存。混淆矩阵能直接看出哪一类被误判成哪一类。如果 0 类被大量判成 1 类,说明模型对负样本的区分能力不够,要么加负样本,要么调类别权重。
4.2 超参数调整的优先级
调参不要一把抓,按这个顺序来:先调学习率,再调批大小,然后看网络深度,最后才是正则化系数。学习率是影响最大的,1e-3不行就试1e-4和3e-4。批大小受显存限制,但太小会让 BatchNorm 不稳定,建议至少 16。如果训练集准确率远高于验证集,先加 Dropout 或权重衰减,再考虑减网络层数。
temp.jpg可以作为单张推理的入口,训练完拿它跑一次,看输出的类别概率。如果概率接近 0.5,说明模型没学到东西,回去查预处理和标签。
注意:不要用测试集调参。测试集只在最后跑一次,调参阶段用验证集。这个坑我见过太多次,调完参测试集准确率虚高,上线就崩。
5. 避坑与排查:烟火分类项目里最容易翻车的五个地方
现象一:训练准确率 99%,验证准确率 60%。原因:过拟合,模型记住了训练集的背景噪声而不是烟火特征。 解决:加数据扩增、加 Dropout、减小网络容量,或者检查训练集和验证集是不是来自同一批拍摄条件。如果验证集全是白天场景而训练集全是夜晚,分布不一致,再怎么调都没用。
现象二:模型把亮灯窗户全判成烟火。原因:预处理没做掩膜,模型学到了“高亮区域=烟火”的捷径。 解决:在预处理阶段加一个亮度阈值掩膜,把非烟火的高亮区域压暗,或者用颜色空间过滤,只保留火焰色域内的区域再送进网络。
现象三:BMP 读取出来是 None,程序直接崩。原因:文件名里有中文路径、空格,或者 OpenCV 不支持某些 BMP 编码格式。 解决:用cv2.imdecode(np.fromfile(path, dtype=np.uint8), cv2.IMREAD_COLOR)替代cv2.imread,能绕过路径编码问题。另外检查文件是不是 16 位 BMP,OpenCV 读 16 位图会出问题,先转 8 位。
现象四:验证集 loss 突然变成 NaN。原因:学习率太大,或者某张图归一化后出现极端值。 解决:先把学习率降一个数量级,然后在 Dataset 里加断言,检查每张图归一化后的均值和方差是否在合理范围。如果某张图全黑或全白,直接跳过。
现象五:类别不平衡导致召回率极低。原因:0 类样本远多于 1 类,模型倾向于全预测成多数类。 解决:在损失函数里加weight参数,按类别频率的倒数设置权重;或者用WeightedRandomSampler在采样阶段就平衡。不要只靠过采样,复制样本会让过拟合更严重。
6. 从单张推理到批量验证:一个能落地的收尾技巧
训练完模型,别只看验证集数字。我习惯做一件事:把temp.jpg和测试集里随机抽的 20 张图拼成一张网格图,每张图上面标真实标签和预测标签,一眼就能看出模型在哪些场景下翻车。这个习惯帮我省了很多“数字好看但实际不能用”的后悔药。
具体做法是用main.py里已经训练好的模型,加载权重后跑一个predict_batch函数。下面这段代码可以直接接在训练脚本后面。
import matplotlib.pyplot as plt import numpy as np from PIL import Image def visualize_predictions(model, image_paths, labels, device, n=20): model.eval() fig, axes = plt.subplots(4, 5, figsize=(15, 12)) axes = axes.flatten() for i, (path, true_label) in enumerate(zip(image_paths[:n], labels[:n])): img = Image.open(path).convert("RGB") tensor = val_tf(img).unsqueeze(0).to(device) with torch.no_grad(): pred = model(tensor).argmax(dim=1).item() axes[i].imshow(img) color = "green" if pred == true_label else "red" axes[i].set_title(f"T:{true_label} P:{pred}", color=color, fontsize=10) axes[i].axis("off") plt.tight_layout() plt.savefig("prediction_grid.png", dpi=150) print("已保存 prediction_grid.png")逻辑说明:val_tf是验证集的变换,不要用训练集的扩增变换,否则预测结果会随机。标题里T是真实标签,P是预测标签,绿色表示正确,红色表示错误。跑完打开prediction_grid.png,重点看红色标题的图——如果错的全是某种特定背景,比如霓虹灯招牌,那就针对性地补这类负样本。
参数方面,n=20可以改成你想要的张数,但别超过测试集大小。figsize和子图行列数按n调整,保证每张图看得清。保存的 dpi 设 150 足够发报告用。
从那以后我每次训练完烟火分类模型,都强制走一遍这个可视化流程,不看到红色错误样本的具体样子,就不敢说模型能上线。希望帮到你。
本文还有配套的精品资源,点击获取