简介:这份PDF文献面向图像处理、计算机视觉方向的研究者与消防救援技术相关人员,聚焦火场灰度图像中烟雾干扰导致监控画面模糊、对比度下降的问题,提出一套基于深度学习的去烟算法方案。资源为单篇学术论文,压缩包内仅含1个PDF文件,大小约1.26MB,便于快速查阅与引用。论文核心由检测子网络与去除子网络串联构成:前者借助残差学习网络定位烟雾区域,缓解深层网络梯度消失;后者采用密集连接的U型网络,通过Dense Block将低层特征复用到高层,在保留原始背景的同时去除烟雾。文中还系统比较了直方图均衡化、Retinex等传统增强方法在火场场景下的局限,并给出主观视觉与PSNR、SSIM等客观指标的实验对比。目前已有376人学习下载,适合需要了解深度学习去烟网络结构设计、实验评价方法及火场视频清晰化思路的读者参考借鉴。
1. 火场监控画面糊成一片,这套串联网络怎么把烟“抠”掉
建筑内部起火后,监控画面最先失效的不是摄像头,而是画面本身。大量烟雾颗粒对光线的吸收和散射,让灰度监控图像的对比度断崖式下跌,消防员盯着屏幕也分不清哪里是门、哪里是人。传统做法是把去雾算法搬过来用,但火场烟雾分布极不均匀,室内又不存在稳定的物理退化模型,暗通道先验、多尺度 Retinex 这类方法在真实火场图像上经常越处理越糊,甚至把噪声放大。这篇来自《计算机与现代化》2020 年第 10 期的论文,思路是把“找烟”和“去烟”拆成两个串联的子网络:检测子网络用残差学习定位烟雾区域,去除子网络用密集连接的 U 型网络在保留背景的前提下抹掉烟雾。它适合做图像复原、安防视频增强方向的工程师,尤其是手头有灰度监控数据、想复现一套端到端去烟流程的人。
2. 检测子网络:残差学习怎么定位烟雾区域
火场烟雾没有固定形状,边界模糊、浓度不均,直接让网络回归一张干净图像,训练很容易崩。论文的做法是先解决“烟在哪”,再解决“怎么去”。检测子网络承担的就是定位任务,它的输出不是一张掩膜图,而是为后续去除提供空间先验。
2.1 为什么用残差学习而不是普通卷积堆叠
图像退化类任务里,网络越深,理论上拟合能力越强,但实际训练中梯度消失会让浅层参数几乎不更新。残差学习(Residual Learning)的核心是让网络学输入与输出之间的残差映射,而不是直接学完整映射。论文的检测子网络包含 3 个残差学习模块,每个模块由 10 个残差单元组成,单个残差单元内部是两层 3×3 卷积加批归一化,再通过跳跃连接把输入加到输出上。
这种设计在火场场景下的实际收益是:浅层卷积能保留烟雾边缘的纹理信息,深层卷积负责判断“这块区域是不是烟”,跳跃连接保证梯度能回传到前几层。论文还额外引入了正则化中间层来抑制梯度消失,这一点在复现时容易被忽略。
2.2 检测子网络的 PyTorch 结构复现
下面这段代码按论文描述搭出检测子网络的主干,残差单元数量按“3 模块 × 10 单元”配置,输入为单通道灰度图。
import torch import torch.nn as nn class ResidualUnit(nn.Module): """单个残差单元:两层 3x3 卷积 + BN + 跳跃连接""" def __init__(self, channels=64): super().__init__() self.conv1 = nn.Conv2d(channels, channels, 3, padding=1) self.bn1 = nn.BatchNorm2d(channels) self.conv2 = nn.Conv2d(channels, channels, 3, padding=1) self.bn2 = nn.BatchNorm2d(channels) self.relu = nn.ReLU(inplace=True) def forward(self, x): identity = x out = self.relu(self.bn1(self.conv1(x))) out = self.bn2(self.conv2(out)) out = out + identity # 残差相加,缓解梯度消失 return self.relu(out) class DetectionSubNet(nn.Module): """检测子网络:3 个残差模块,每模块 10 个残差单元""" def __init__(self, in_ch=1, base_ch=64, num_blocks=3, units_per_block=10): super().__init__() self.head = nn.Sequential( nn.Conv2d(in_ch, base_ch, 3, padding=1), nn.BatchNorm2d(base_ch), nn.ReLU(inplace=True) ) blocks = [] for _ in range(num_blocks): blocks.append(nn.Sequential( *[ResidualUnit(base_ch) for _ in range(units_per_block)] )) self.blocks = nn.Sequential(*blocks) # 输出单通道烟雾位置响应图 self.tail = nn.Conv2d(base_ch, 1, 3, padding=1) def forward(self, x): feat = self.head(x) feat = self.blocks(feat) return torch.sigmoid(self.tail(feat)) # 归一化到 [0,1] 作为烟雾概率逻辑说明:ResidualUnit里的out + identity是残差学习的核心,保证反向传播时梯度至少有一条恒等路径。DetectionSubNet的tail用 1×1 感受野之外的 3×3 卷积输出单通道响应图,sigmoid把响应压到 0~1,方便后续和去除子网络做逐像素加权。参数上base_ch=64是论文隐含的通道规模,显存吃紧时可以降到 32,但检测精度会掉,建议先在 4GB 显存上跑通再调。
2.3 检测输出的两种用法
检测子网络的输出有两种接法:一种是直接作为掩膜乘到原图上,另一种是作为注意力权重送进去除子网络。论文采用的是后者,因为火场烟雾是半透明叠加,硬掩膜会在边界产生明显割裂。复现时如果发现去烟结果在烟雾边缘有黑边,大概率是掩膜二值化太激进,改成软权重即可。
3. 去除子网络:Dense U-Net 如何保留背景去烟
定位到烟雾之后,去除子网络要解决的是一个更棘手的问题:把烟抹掉的同时,不能把烟后面的门框、楼梯、人影一起抹掉。论文选择 U-Net 作为骨架,并在其中嵌入 Dense Block,这个组合是整篇论文工程价值最高的部分。
3.1 U-Net 的收缩路径与扩张路径分工
U-Net 由收缩路径(编码器)和扩张路径(解码器)组成。收缩路径通过卷积和池化逐层下采样,捕捉图像的上下文信息,回答“这是什么场景”;扩张路径通过反卷积逐层上采样,恢复空间分辨率,回答“这个像素属于什么”。两者之间的跳跃连接把编码器的高分辨率特征直接送到解码器,这是 U-Net 在图像复原任务里比普通自编码器强的地方。
论文的去除子网络里,池化层采用步长为 2 的下采样,反卷积层负责上采样,批归一化配合 ReLU 激活。这些是标准配置,真正拉开差距的是 Dense Block 的插入位置。
3.2 Dense Block 的低层特征复用机制
Dense Block 的核心思想是:每一层的输出都连接到后面所有层的输入。假设一个 Dense Block 有 L 层,第 l 层的输入是前面所有层输出的拼接。这样做的好处是低层特征(烟雾边缘、纹理)不需要经过层层传递就能直达高层,特征复用率大幅提升。
在火场去烟任务里,这个机制直接对应一个痛点:烟雾浓度高的区域,高层特征已经被烟雾主导,如果低层纹理不能复用上来,去烟后背景就是一片平滑的糊。论文把 Dense Block 嵌入 U-Net 的对应层级,让编码器提取的细节特征在解码器阶段还能被调用。
class DenseBlock(nn.Module): """密集连接块:每层输出拼接到后续所有层输入""" def __init__(self, in_ch, growth_rate=32, num_layers=4): super().__init__() self.layers = nn.ModuleList() for i in range(num_layers): self.layers.append(nn.Sequential( nn.BatchNorm2d(in_ch + i * growth_rate), nn.ReLU(inplace=True), nn.Conv2d(in_ch + i * growth_rate, growth_rate, 3, padding=1) )) def forward(self, x): features = [x] for layer in self.layers: out = layer(torch.cat(features, dim=1)) # 拼接前面所有层输出 features.append(out) return torch.cat(features, dim=1)逻辑说明:torch.cat(features, dim=1)是 Dense Block 的灵魂,通道维度不断增长,growth_rate=32控制每层新增通道数。num_layers=4时输出通道为in_ch + 4×32。参数调整上,growth_rate越大特征越丰富但显存占用线性上升,4GB 显存建议不超过 32;num_layers超过 6 后收益递减,论文没有给出具体层数,复现时从 4 起步比较稳。
3.3 损失函数选 L1 而不是 MSE
论文明确采用 L1 损失而非均方误差(MSE)。原因在图像复原任务里很实际:MSE 对大误差惩罚重,会倾向于输出模糊的平均结果,PSNR 可能好看但视觉上发糊;L1 对异常值更鲁棒,在 PSNR 和 SSIM 两项指标上通常表现更好。公式为生成图像与目标图像逐像素绝对差之和。
l1_loss = nn.L1Loss() pred = removal_net(detected_feat) # 去除子网络输出 loss = l1_loss(pred, clean_gt) # clean_gt 为无烟真值图参数说明:nn.L1Loss()默认对 batch 内所有像素求平均,如果训练时发现损失下降但 SSIM 不涨,可以改成对烟雾区域加权,权重来自检测子网络的输出,让网络更关注烟雾覆盖的像素。
3.4 训练配置与数据增强
论文在 Nvidia GTX 1080(4GB 显存)上训练,框架用 TensorFlow,优化器 Adam,学习率 0.001,迭代 100 个周期收敛。数据集是合成的 6960 对室内模拟起火/未起火灰度图,训练集 6000 对,测试集 960 对。数据增强用了旋转、裁剪和翻转。
复现时如果显存不够,把 batch size 降到 4 或 8,学习率同步降到 0.0005,否则 Adam 在小子批下容易震荡。100 个周期是论文的收敛点,实际训练时建议每 10 个周期存一次权重,用测试集的 PSNR 做早停判断。
4. 复现排错:PSNR、SSIM 与实时性怎么对齐
论文给出的定量结果里,本文算法 PSNR 20.32 dB、SSIM 0.826、运行时间 9.71 ms,在对比的 6 种算法中 PSNR 和 SSIM 最高,运行时间排第二。复现时如果指标对不上,问题通常出在数据、训练和推理三个环节。
4.1 指标对不上的常见原因
| 现象 | 可能原因 | 排查方向 |
|---|---|---|
| PSNR 低于 18 dB | 训练集与测试集分布差异大 | 检查合成数据的烟雾浓度范围是否覆盖测试集 |
| SSIM 高但视觉发糊 | 损失函数用了 MSE | 换回 L1,或对烟雾区域加权 |
| 运行时间超过 30 ms | 推理未关梯度、未用半精度 | 加torch.no_grad(),尝试 FP16 |
| 烟雾边缘有黑边 | 检测掩膜二值化过激 | 改用软权重,阈值从 0.5 调到 0.3 |
| 背景纹理丢失 | Dense Block 层数不足 | 增加num_layers或提高growth_rate |
4.2 推理阶段的显存与速度优化
论文的 9.71 ms 是在 Python 环境下测的,复现时如果直接跑原始模型,很容易超过 20 ms。常见做法是推理前把模型切到 eval 模式并关闭梯度,再用torch.cuda.amp做半精度推理。
model.eval() with torch.no_grad(): with torch.cuda.amp.autocast(): output = model(input_tensor)逻辑说明:model.eval()固定批归一化的统计量,避免推理时用当前 batch 的均值方差导致结果波动;torch.no_grad()省掉反向图构建,显存和耗时都能降;autocast在支持的 GPU 上自动用 FP16 计算,速度提升明显,但要注意检测子网络的sigmoid输出在 FP16 下可能精度不足,必要时对输出层强制 FP32。
4.3 真实火场图像上的泛化问题
论文在真实图像对比实验里提到一个关键现象:GL-MSR 在真实火场图像上的视觉效果反而比 MSR 差,说明传统方法鲁棒性不足。深度学习方法的优势在于端到端学习,但前提是训练数据要覆盖真实场景的退化模式。如果手头只有合成数据,建议先用少量真实火场监控图做微调,冻结检测子网络,只训练去除子网络的后几层,学习率设 0.0001,跑 20 个周期左右,通常能把真实场景的残留烟雾压下去。
5. 从灰度到彩色:把串联网络迁移到新场景的技巧
论文结尾提到下一步是研究火场彩色图像的清晰化,这个方向在工程上比灰度更有价值,因为彩色监控能提供更多救援判断信息。迁移时不需要重写整个网络,核心改动集中在输入通道和损失函数上。
5.1 输入通道与输出通道的调整
灰度模型第一层卷积是in_ch=1,彩色场景改成in_ch=3,输出层从 1 通道改成 3 通道。检测子网络的输出仍然保持单通道,因为烟雾位置与颜色无关,这样检测子网络的预训练权重可以直接复用,只微调去除子网络。
# 灰度模型加载后,替换首尾层以适配彩色输入 model.detection.head[0] = nn.Conv2d(3, 64, 3, padding=1) model.removal.tail[0] = nn.Conv2d(64, 3, 3, padding=1)逻辑说明:只替换首尾层,中间所有残差单元和 Dense Block 的权重保持不变,这样能在小规模彩色数据上快速收敛。替换后建议先用 0.0001 的学习率跑 10 个周期,再视 PSNR 决定是否解冻更多层。
5.2 彩色场景下的损失函数微调
彩色图像用 L1 损失时,三个通道等权求和,但火场烟雾对蓝通道的衰减通常比红绿通道更明显。一个实用技巧是给蓝通道损失加 1.2 的权重,让网络更关注蓝通道的复原。
def weighted_l1(pred, gt): diff = torch.abs(pred - gt) weight = torch.tensor([1.0, 1.0, 1.2]).view(1, 3, 1, 1).to(pred.device) return (diff * weight).mean()参数说明:weight的三个值对应 R、G、B 通道,蓝通道权重 1.2 是经验值,实际调参时可以从 1.0 到 1.5 之间试,观察 SSIM 的蓝通道分量是否提升。如果训练数据里烟雾偏黄,红绿通道权重也要相应上调。
5.3 一个验证迁移是否成功的快速方法
迁移完成后,不要只看整体 PSNR。把测试集按烟雾浓度分成低、中、高三档,分别统计 PSNR 和 SSIM。如果低浓度档指标正常但高浓度档掉得厉害,说明 Dense Block 的特征复用还不够,需要增加num_layers或把检测子网络的输出更早地注入去除子网络。这个分档验证方法比单一指标更能暴露模型在火场极端场景下的短板。
本文还有配套的精品资源,点击获取