简介:基于BP神经网络的火焰识别资源面向机器学习初学者、图像识别研究人员及MATLAB开发者,适用于火灾预警与安全监控中的图像分类场景。压缩包共845个文件,体积约467MB,包含813张jpg火焰样本图片、17个m功能脚本、4个mat数据文件、10张png辅助图及1个mp4演示视频;代码覆盖BP网络结构定义、训练主流程、颜色直方图特征提取、快速主成分分析降维和数据标准化处理,并内置图形用户界面,方便直观测试与结果展示。目前已有209人学习下载,资源以可复跑的完整工程清晰演示了从图像读取、预处理、特征提取到模型训练与评估的核心步骤,包括灰度化、归一化、梯度下降等处理;同时体现误差反向传播、调整权重与阈值等关键原理。既适合系统学习BP神经网络,也可作为火灾识别研究的基础代码库,尤其适合课程设计、毕业设计及算法竞赛参考,配合结构化目录与注释,便于二次开发、对比实验以及拓展到其他分类任务。
1. BP神经网络火焰识别:这套MATLAB项目到底在解决什么问题
监控室的大屏上同时切着十几个摄像头画面,值班员盯着其中一路,直到火光蹿起来才发现异常,这时候烟感早就被吹得乱响。火焰识别要解决的就是这种「人眼看不过来、烟感又滞后」的尴尬:把摄像头画面里的火光区域自动判出来。BP神经网络做火焰识别,本质上是把它当成一个图像分类问题——输入图像的颜色、纹理特征,输出「有火 / 无火」的判定。这套MATLAB项目从数据读取、颜色直方图计算、PCA降维,到BP网络的构建、训练和GUI识别,把整个流程串成了一整套可复现的代码。适合手里有图像样本、想在MATLAB里快速跑通一个识别模型的工程师和研究生,也适合想了解BP分类器完整落地路径的初学者。
2. 数据读取与预处理:ReadColorData 脚本与图像标准化实操
2.1 这套项目里的图像数据是怎么读进来的
项目根目录下的ReadColorData0.m和ReadColorData.m承担的是同一件事:把文件夹里的 jpg 图片读进来,转成训练矩阵。项目自带的样本里能看到52.jpg、11.jpg、59.jpg、45.jpg、29.jpg、40.jpg这一批文件,命名看起来没规律,但目录结构决定了标签怎么打。我一般会把正样本(有火焰的图像)和负样本(正常背景图像)分开放两个文件夹,或者干脆在文件名里带fire/normal标记,这样读取脚本可以用文件名直接生成标签,不用额外维护一份标注文件。
function [featureMat, labels] = ReadColorData0(folderPath, resizeSize) imgFiles = dir(fullfile(folderPath, '*.jpg')); n = length(imgFiles); featureMat = zeros(n, resizeSize(1) * resizeSize(2) * 3); labels = zeros(n, 1); for i = 1:n img = imread(fullfile(folderPath, imgFiles(i).name)); img = imresize(img, resizeSize, 'bilinear'); featureMat(i, :) = img(:)'; if contains(imgFiles(i).name, 'fire') labels(i) = 1; else labels(i) = 0; end end end这段代码的逻辑不复杂:先dir列出文件夹下所有 jpg,预分配一个n × (宽×高×3)的矩阵,然后逐个imread、imresize到统一尺寸,再img(:)'把二维图像拉成一维行向量。最后用contains判断文件名里有没有fire关键字来生成标签。尺寸统一很关键——BP 网络的输入维度在创建时就是固定的,如果训练时进来一批 640×480、测试时进来一批 720×576,矩阵维度对不上,网络直接报错。resizeSize一般取[64 64]或者[128 128],不是越大越好,后面接 PCA 降维,尺寸太大只会拖慢计算。
另一个细节是img(:)'的拉直顺序。MATLAB 按列优先展开,同一张图用img(:)'和reshape(img, 1, [])结果一样,但如果你在 Python 和 MATLAB 之间来回倒数据,就得注意行列顺序差异。这个坑我在跨语言复现时踩过,后面避坑章会展开讲。
2.2 归一化和标签对齐:scaling 脚本的两种写法
图像读进来是 uint8 类型,取值范围 0~255。BP 网络用的激活函数(比如 logsig、tansig)输出范围有限,输入数值太大或者跨度太大,训练时梯度容易饱和,收敛慢甚至直接震荡。scaling.m这个文件做的就是标准化这件事,常见做法是mapminmax,它能把每一行特征映射到 [-1, 1] 区间。注意mapminmax默认是按行处理的,也就是每个样本自己独立归一化,这对图像特征来说不一定合适。
% 方法一:mapminmax 按行归一化 [featureMatScaled, ps] = mapminmax(featureMat', 0, 1); featureMatScaled = featureMatScaled'; % 方法二:全局最大最小值归一化 minVal = min(featureMat(:)); maxVal = max(featureMat(:)); featureMatScaled = (featureMat - minVal) / (maxVal - minVal);第一种写法里,featureMat'转置是因为mapminmax把每行当作一个样本,需要先把数据转成「特征×样本」的排列,处理完再转回来。ps结构体里存了归一化的参数(最小值、最大值),测试阶段必须复用这组参数,不能重新计算。第二种写法是全局归一化,整批数据共享同一个最大值和最小值,样本之间的相对亮度关系保留得更完整。对于火焰识别这个场景,火焰区域通常比背景亮很多,用全局归一化能把这种亮度差异保留下来,我个人更倾向第二种。
标签对齐是新手最容易忽略的环节。读图的时候labels和featureMat的行一一对应,这个顺序在后面做 PCA、训练、测试时都不能乱。如果中间对样本做了乱序(比如randperm),标签必须跟着同步重排。另一个问题是标签的值域:BP 输出层用 logsig 时输出范围是 [0, 1],标签用 0/1 正合适;如果输出层是线性函数,标签用 1/-1 也可以,但得跟输出层的激活函数匹配好。项目里result.m输出预测结果时,一般会把网络输出做一次阈值判断,out > 0.5判为火焰,这个 0.5 就是隐含的标签阈值。
3. 颜色直方图与 PCA 降维:特征维度怎么压下来还不丢信息
3.1 火焰图像用颜色直方图比原始像素更合适
把图像像素直接铺开喂给网络是最朴素的做法,但一张 128×128 的彩色图拉直后就是 49152 维,直接训练不仅慢,还容易过拟合——样本量就几十张 jpg,几千个维度对应几十个样本,网络学到的全是噪声。colorhist1.m做的事情就是改用颜色直方图当特征。火焰的颜色有很强的先验:火焰核心是白色偏黄,外焰偏红,整体色相集中在色相环的红橙区域。用颜色分布替代空间位置,刚好抓住火焰最稳定的视觉特征,又不受火焰形状变化的影响。
function histFeat = colorhist1(imgRGB, binCount) % 转 HSV,H 通道对光照变化不那么敏感 imgHSV = rgb2hsv(imgRGB); H = imgHSV(:, :, 1); S = imgHSV(:, :, 2); V = imgHSV(:, :, 3); % H 通道直方图,bin 数量决定特征维度 histFeat = histcounts(H(:), 0:1/binCount:1); % 可选:把 S 通道低饱和度区域剔除,排除白色噪声干扰 mask = S(:) > 0.1 & V(:) > 0.1; histFeatS = histcounts(H(mask), 0:1/binCount:1); histFeat = [histFeat, histFeatS]; end这段代码用了 HSV 颜色空间而不是 RGB,原因在于 RGB 三个通道高度相关,受光照影响大;H(色调)通道把颜色信息单独拎出来,S(饱和度)和 V(亮度)分开之后,火焰的色相特征更集中。binCount取 16 或 32 比较常见——16 个 bin 意味着 H 通道被分成 16 段,每一段代表 22.5 度的色相范围,红色区域大致落在第 0~2 个 bin;取太细(比如 128)会把同一火焰的颜色抖动放大,反而降低区分度。直方图特征维度是binCount × 2(含一个基于掩码的 H 直方图),比原始像素低了几个数量级,但依然有几十维,后面还要过 PCA。
3.2 fastPCA:累计贡献率阈值是 95% 还是 99%
fastPCA.m在这套项目里的角色是降维,把颜色直方图特征进一步压缩,去掉冗余维度。BP 网络对输入维度的容忍度有限,维度越高,需要的训练样本量指数增长。PCA 的原理是用协方差矩阵的特征向量,把原始特征空间旋转到新坐标系,取前 k 个主成分,让投影后方差最大。
function [proj, eigVec, eigVal] = fastPCA(X, k) X = double(X); n = size(X, 1); mu = mean(X, 1); Xc = X - repmat(mu, n, 1); % 对高维低样本数据用 SVD 比直接算协方差矩阵更快更稳 [U, S, ~] = svd(Xc, 'econ'); eigVal = diag(S) .^ 2 / (n - 1); % 按特征值降序排列 [eigVal, idx] = sort(eigVal, 'descend'); eigVec = U(:, idx); proj = Xc * eigVec(:, 1:k); endsvd(Xc, 'econ')用的是经济型奇异值分解,对「样本数远小于维度」的数据,比eig(Xc'*Xc)快得多,数值上也更稳。diag(S).^2 / (n-1)是把奇异值平方再除以样本数减一,得到的就是协方差矩阵的特征值。这里有个容易算错的地方:特征值要从大到小排序,对应的特征向量顺序也要跟着调,否则投影矩阵和特征值对不上,后续画累计贡献率曲线时曲线乱跳。
k 值怎么选?常见做法是看累计贡献率,也就是前 k 个特征值之和占总特征值之和的比例:
% 在训练集上计算累计贡献率 tot = sum(eigVal); ratio = cumsum(eigVal) / tot; k = find(ratio >= 0.95, 1);选 95% 还是 99% 取决于你对误报的容忍度。火焰识别这种应用,我一般取 95%——保留主要变化方向,丢掉那些可能是噪声的尾巴维度。取 99% 往往把样本噪声也保留进来,训练集准确率好看,换一组现场图像就翻车。下表是三种设定在这类小样本场景下的典型对比,具体数值随数据而变,但趋势一致:
| 累计贡献率阈值 | 保留维度 | 训练耗时 | 泛化倾向 |
|---|---|---|---|
| 90% | 较少 | 短 | 欠拟合,容易漏报 |
| 95% | 中等 | 中等 | 训练集与验证集表现均衡 |
| 99% | 较多 | 长 | 过拟合风险上升 |
PCA 还有一个关键约束:mu(均值)和eigVec(投影矩阵)必须从训练集算出来,测试时直接套用。绝不能把训练集和测试集混在一起算 PCA,这是数据泄漏,会让测试准确率虚高到离谱。后面避坑章专门讲这个。
4. 构建 BP 网络并完成训练:神经元个数、trainlm 参数和 GUI 整合
4.1 createBP 的网络结构怎么定:三层结构与参数先验
createBP.m是定义网络骨架的文件。BP神经网络的原理一句话概括:输入层接收特征,隐藏层做非线性变换,输出层给出分类概率,误差从输出层反向传播回各层,用梯度下降更新权值和阈值。项目常见的结构是三层——输入层、一层隐藏层、输出层,隐藏层神经元数量靠经验公式估算,比如sqrt(输入维数 + 输出维数) + a,a 取 1~10 之间的整数,然后再用交叉验证微调。
% 输入维度由 PCA 降维后的特征维度决定 inputDim = size(trainFeatures, 2); % 隐藏层节点数经验公式:sqrt(inputDim + 1) + 5 hiddenDim = round(sqrt(inputDim + 1) + 5); net = feedforwardnet(hiddenDim, 'trainlm'); net.input.processFcns = {'removeconstantrows'}; net.output.processFcns = {'mapminmax'}; net.trainParam.epochs = 1000; net.trainParam.goal = 1e-3; net.trainParam.lr = 0.05; % 划分训练集、验证集、测试集 net.divideFcn = 'dividerand'; net.divideParam.trainRatio = 0.7; net.divideParam.valRatio = 0.15; net.divideParam.testRatio = 0.15;feedforwardnet的第一个参数是隐藏层神经元个数,可以传向量[10 5]表示两层隐藏层,但对火焰识别这种二分类问题,一层隐藏层就够用;层数加深并不能带来明显收益,反而更容易过拟合。trainlm是 Levenberg-Marquardt 训练函数,收敛快,适合中小规模网络——它利用近似二阶导数信息更新权值,迭代次数远少于普通梯度下降。trainParam.lr是学习率,0.05 属于保守取值。学习率调大(0.1 以上)训练前期收敛猛,后期容易在最优解附近震荡;调小(0.01 以下)收敛慢,1000 次迭代可能不够用。trainParam.goal设 1e-3,意思是均方误差降到 0.001 就提前停。这个值别设太小,小样本场景下硬逼到 1e-5,网络会把训练样本的噪声细节都背下来。
4.2 训练脚本 train.m 里要盯的四个信号
train.m调用的核心就是train(net, x, t),但在点回车之前,我一般会先确认四件事:数据矩阵的行列方向对不对、标签向量维度和样本数是否匹配、输入特征有没有归一化、divideFcn的随机种子是否固定。MATLAB 的train函数默认会自动划分验证集和测试集,这本来是好事,但每次运行随机划分结果都不一样,导致两次训练出来的准确率差别很大。复现实验时我会加一行rng(2024)固定随机种子,否则你调好参数后睡一觉再跑,结果可能变了两个点。
rng(2024); % 固定随机种子,保证实验可复现 % trainFcn 是 trainlm,自动使用验证集做 early stopping [net, trainInfo] = train(net, trainFeatures', trainLabels');trainFeatures'和trainLabels'这里的转置是必须的:MATLAB 神经网络工具箱约定「每列是一个样本」,而我们前面构造的 featureMat 是「每行是一个样本」,不转置直接开训,维度对不上或者训练结果完全乱掉。训练过程中弹出的窗口里,最值得看的是三条曲线:训练集误差、验证集误差、测试集误差。验证集误差降到最低点后开始回升,就是过拟合的信号;训练集误差反复跳动不下降,说明学习率太大或者数据没归一化干净。trainInfo结构体里存了bestEpoch,那个就是验证集误差最小时的迭代轮数。
训练完成后,net对象里存了训练好的权重、阈值、归一化参数。用save('trainedNet.mat', 'net')存盘,GUIrecg.m里再load进来做识别,这就是整个项目的工作流。
4.3 GUI 结果展示:GUIrecg 的控件与回调逻辑
GUI.m和GUIrecg.m做的是同一个界面,区别在于是用 GUIDE 自动生成还是纯代码编写。这套项目里 GUI 的核心就三个控件:一个坐标轴axes1用来显示待识别图像,一个按钮pushbutton用来触发识别,一个文本框text_result显示判别结果。
function pushbutton1_Callback(hObject, eventdata, handles) [filename, pathname] = uigetfile({'*.jpg;*.png'}, '选择火焰图像'); if filename == 0 return; end img = imread(fullfile(pathname, filename)); axes(handles.axes1); imshow(img); % 特征提取:和训练时走同一套流程 feat = extractFeature(img); feat = scaling(feat, ps); % 复用训练时的归一化参数 % 网络预测:输出层是 1 个节点,值在 0~1 之间 out = net(feat); if out > 0.5 set(handles.text_result, 'String', sprintf('火焰概率:%.2f,判定为火焰', out)); else set(handles.text_result, 'String', sprintf('火焰概率:%.2f,判定为正常', out)); end end这里的extractFeature就是前面colorhist1的封装,关键点是:识别阶段走的预处理管道必须和训练阶段完全一致——同一种图像缩放尺寸、同一个直方图 bin 数、同一组归一化参数。net(feat)是 MATLAB 神经网络对象的调用语法,等价于sim(net, feat)。如果输出层用了 logsig 函数,输出天然落在 0~1,直接和 0.5 阈值比就行。阈值怎么调更合理,我在最后一张的进阶部分专门讲。实际部署时按钮回调里经常遇到out是矩阵而不是标量——输入特征如果传成了行向量而不是列向量,输出就是一个 1×1 的 cell 数组,判断前先out = out(1)剥出来更省事。
5. MATLAB 火焰识别避坑:五个翻车现场的排查记录
5.1 数据处理环节的三个坑
坑一:mapminmax 把负样本特征归到负数区间,训练死活不收敛。现象是训练集误差一开始下降很快,几十轮之后就在 0.5 附近震荡。原因是我把mapminmax的默认目标区间设成了 [-1, 1],而输出层用 logsig 输出范围是 [0, 1],前向传播的信息经过隐藏层 tansig 还能消化,但误差反传时负数区间让梯度方向来回横跳。解决方法是归一化目标区间改成 [0, 1],和 logsig 的输出范围对齐;或者输出层换成purelin,彻底不限制输出范围。从那以后我统一了一个习惯:输入归一化区间和目标函数的取值区间必须配对。
坑二:PCA 混进了测试集,准确率虚高到 99%,实测一塌糊涂。现象是跑验证集时准确率 98%,拿着训练好的模型去处理手机拍的现场图,几乎全判错。原因是我偷懒,把所有样本合在一起算 PCA 的均值和投影矩阵,再切训练集测试集。这等于测试集的分布信息提前泄漏给了训练过程,测试准确率严重失真。解决方法是严格分开:训练集上算mu和eigVec,测试集直接用这两个参数投影,不能重新算。我在fastPCA函数里加了两个输出参数,训练阶段保存pcaModel.mu和pcaModel.eigVec到 mat 文件,测试阶段加载复用。
坑三:颜色直方图 bin 数设成 256,火焰和夕阳的区分度反而下降。现象是加了直方图特征后准确率还不如原始像素。原因是我把 H 通道分成 256 个 bin,每个 bin 只覆盖 1.4 度的色相范围,同一团火焰内部颜色轻微波动就散到七八个 bin 里,直方图形态对旋转和缩放变得过度敏感。解决方法是 bin 数降到 16~32,让直方图在「细节分辨」和「分布稳定性」之间取平衡。另外,夜间图像火焰区域经常过曝变成纯白色,V 通道接近 255、S 通道接近 0,这部分像素的色相是噪声,直方图里要单独处理——用S > 0.1的掩码过滤掉低饱和度的过曝像素,只统计真正有颜色信息的区域。这个操作能显著降低路灯、白墙的误报。
5.2 训练与识别环节的两个坑
坑四:trainlm 训练到一半报内存不足,换 trainbr 反而更慢。现象是 1000 个样本、128 维输入的网络,trainlm迭代 200 轮就报错退出。原因在于trainlm需要计算近似的 Hessian 矩阵,内存开销和网络参数数量的平方成正比,样本特征维度高一点就撑不住。解决方法是先 PCA 降维到 20 维以内再训练,绝大多数情况下trainlm就没问题了;如果降维后还报错,就换trainbr(贝叶斯正则化),它对小样本更友好,也能自动压制过拟合,代价是收敛慢很多。我一般会把两条路线都跑一遍,对比验证集误差再做决定——这不是玄学,是trainlm和trainbr的目标函数本来就不一样,适合的数据规模也不同。
坑五:GUI 里识别一张图像和训练时尺寸不一致,网络输出全是同一个值。现象是 GUI 点击识别后,无论放什么图,输出概率都稳定在 0.7 左右。原因是训练时图像统一缩放到 128×128,而 GUI 回调里忘调imresize,直接把 640×480 的原图拉成一维向量去喂网络,输入维度变了,网络输出自然乱套。解决方法是把预处理抽成一个独立的extractFeature函数,训练和 GUI 识别共用一个入口,尺寸、归一化、直方图参数全部封装在函数内部。从那以后我每次拿到新项目,第一件事就是找预处理有没有重复实现,有就合并成一个函数。
6. 更扎实的一点进阶习惯:用交叉验证和阈值优化把误报压下来
6.1 用 k 折交叉验证定隐藏层节点数
BP 网络最烦人的就是隐藏层节点数没有解析解,只能试。拿固定训练集试出来的节点数,换一批数据可能就废了。我一般用 5 折交叉验证来选:把训练集分成 5 份,每次用 4 份训练、1 份验证,轮流五次,取平均准确率。隐藏层节点数从 3 扫到 20,每跑一次记录准确率和训练耗时。下面是一份典型的手动扫描结果——具体数字因数据和初始权重不同可能有波动,但趋势很有代表性。
| 隐藏层节点数 | 5折平均准确率 | 训练耗时 | 观测 |
|---|---|---|---|
| 3 | 82% | 短 | 欠拟合,漏报较多 |
| 5 | 91% | 短 | 训练集验证集差距小 |
| 8 | 94% | 中等 | 验证集误差最低 |
| 12 | 92% | 中等 | 开始出现轻微过拟合 |
| 20 | 88% | 长 | 过拟合明显,曲线震荡 |
节点数从 8 加到 20,训练集准确率一路升到 99%,验证集反而从 94 掉到 88,这就是典型的过拟合。选节点数时盯着验证集表现看,不要被训练集的高分迷惑。注意神经网络的初始权重是随机的,同一节点数跑两次结果会略有差别,交叉验证只是让这个差别平均化,并不能完全消除。
6.2 输出阈值不一定要用 0.5
BP 网络输出层的值本质上是「这个样本像火焰的程度」。很多教程默认跟 0.5 比,但实际场景里,漏报一次火灾和误报一次火警的代价完全不同——监控场景里误报顶多让值班员跑去确认一眼,漏报可能直接错过初期火情。这时候应该在验证集上把阈值从 0.1 到 0.9 都扫一遍,画出误报率和漏报率的关系:阈值调低,误报变多、漏报减少;阈值调高,情况反转。选阈值没有绝对最优,只能按你的现场偏好找平衡点。我在一次园区监控项目里,最终把阈值定在 0.35——宁可多看几次误报,也不放过真火情。
特别是当 BP 网络和视频帧序列结合时——单帧误报是随机噪声,真正的火焰火光会连续多帧出现——可以加一个简单的帧间判定:连续 3 帧输出超过阈值才触发报警,单帧超阈值只做记录。这个习惯能滤掉大量瞬间闪光干扰,比一味调阈值更有效。
把交叉验证、阈值扫描、帧间确认这三步固化下来之后,这套项目的识别流程才算完整——模型训练只是第一步,怎么让它在现场不掉链子才是真正花时间的部分。从那以后我每次拿到新的训练数据,都强制走一遍「预处理函数统一 → 交叉验证选节点 → 阈值扫描 → 帧间确认」这个流程,跳过任何一步,后面都可能要拿现场事故换教训。希望帮到你。
本文还有配套的精品资源,点击获取