DEA-Net细节增强卷积详解:图像去雾的PyTorch实现与工程实践

📅 发布时间:2026/10/5 6:08:50
DEA-Net细节增强卷积详解:图像去雾的PyTorch实现与工程实践
干图像去雾这一行的人应该都有个共同的感受这几年深度学习基本把传统先验方法按在地上摩擦从AOD-Net到FFA-Net再到各种Transformer变体网络结构越堆越复杂但真正能在工程里落地、在真实场景里跑得稳的方案真不算多。DEA-Net算是我今年复现下来观感最好的一篇去雾工作它的核心创新“细节增强卷积”没有把网络结构搞得特别花哨却把雾天图像边缘和纹理细节恢复得非常扎实。这篇博文就手把手带你把DEA-Net里的细节增强卷积从头拆解到PyTorch落地实现顺便把我训练和调参过程中踩过的坑也一并交代清楚。1. 图像去雾问题建模与DEA-Net的整体思路1.1 雾天成像的物理模型先理清楚去雾问题到底在解决什么。传统图像去雾几乎都建立在大气散射模型上公式可以写成I(x) J(x)·t(x) A·(1 - t(x))其中I(x)是观测到的雾图J(x)是我们要恢复的清晰图像t(x)是透射率图A是全局大气光。雾天情况下t(x)比较小意味着场景反射光到达相机的比例很低再加上大气光的叠加图像就发白、发灰、对比度降低。更麻烦的是t(x)和J(x)都是未知的已知的只有I(x)所以去雾本质上是个病态逆问题。传统方法比如暗通道先验、颜色衰减先验都是在想办法先估计A和t(x)再反解J(x)。这类方法的致命弱点是依赖人工先验假设一旦场景不满足假设比如大面积天空、白色物体估计出来的透射率就不准结果容易出现halo伪影或者偏色。基于深度学习的方法直接用一个神经网络从雾图映射到清晰图绕开了中间两步估计端到端训练完事从思路上就比传统方法干净很多。1.2 DEA-Net想解决的核心痛点早期端到端去雾网络虽然结构简单但在恢复图像细节方面表现并不好。原因在于普通卷积默认感受野相对固定对雾天图像里高频细节与低频雾霾信息混叠的情况单一卷积核很难同时兼顾去雾和保细节。实验结果也印证了这一点网络输出的图像整体亮度正常但边缘纹理像被磨皮了一样远处的树叶、建筑的轮廓线都很糊。DEA-NetDetail Enhancement Attention Network的出发点就是针对这个细节丢失问题。它在网络的不同层嵌入了一种叫细节增强卷积的模块用多分支卷积配合注意力门控的方式对特征中的高频细节进行显式增强同时抑制对去雾无益的冗余信息。换句话说它不是把网络做深做宽而是改变了单个卷积模块内部的运算逻辑让卷积层本身就具备提取和增强细节的能力。这种设计对工程落地非常友好不改骨干网络、不引入大的计算负担就能看到明显的细节恢复效果。2. 细节增强卷积原理拆解为什么普通卷积不够用2.1 普通卷积在去雾任务中的局限常规卷积的操作是对局部区域做加权求和每个卷积核相当于一个特征模板。去雾场景里雾天图像不仅包含边缘、纹理等高频信息还包含大量平滑的低频背景。普通卷积层统一处理这两类信息通过堆叠层数来隐式学习高频特征的提取这需要大量的参数和数据去拟合。问题的关键在于卷积核的尺寸一旦确定感受野就固定了。小卷积核如3×3对细节敏感但上下文信息不足大卷积核如7×7能看更大的范围却容易把细节平均掉。如果简单地把不同尺寸的卷积核并联起来融合方式又是固定的网络无法根据每个位置的图像内容自适应决定应该更依赖哪个分支的信息。这在去雾任务中尤其吃亏因为雾的浓度在空间上变化很大。2.2 细节增强卷积的多分支设计逻辑细节增强卷积借鉴了多尺度特征提取的思路但又往前走了一步不仅用多个分支提取特征还引入了一个可学习的融合机制让网络自己决定每个分支在当前像素位置的权重。具体拆开来看DEC模块通常包含三条并行路径基础分支用一个常规3×3卷积提取主干特征保证信息的完整传递。细节分支通过高通滤波或者拉普拉斯变换提取输入的高频分量再做一次1×1卷积把细节信息映射到特征空间。全局上下文分支用带膨胀率的卷积扩大感受野捕捉雾的浓度分布等全局信息。三个分支的输出在通道维度上拼接经过全局平均池化和一个小型卷积网络生成每个分支的注意力权重。这个权重是随输入动态变化的相当于一个软选择机制在纹理丰富的区域网络给细节分支更高权重在平坦区域网络更依赖基础分支。最终输出是三个分支特征的加权求和再接上残差路径让网络在学习增强细节的同时保留原来的信息不至于学习到错误的偏置。2.3 通道注意力在DEC中的角色DEC模块里的注意力除了分支级别的还有通道级别的。去雾后的特征图里不同通道往往对应不同的语义信息比如有的通道专门编码边缘方向有的编码颜色恒常性有的则编码透射率相关特征。如果不加区分地统一处理有用的通道会被大量无效信息淹没。通道注意力模块的思路很直接对特征图做全局平均池化和全局最大池化得到两个描述通道统计信息的向量再经过一个两层的MLP映射最后用sigmoid函数给每个通道生成一个0到1之间的权重。这样网络就能自动放大对去雾有效特征的通道抑制噪声和无关信息的通道。DEC中把分支级注意力和通道级注意力结合起来一个控制多尺度特征的融合方式一个控制通道的重要性两者协同既抓得住细节又稳得住整体信息。3. PyTorch环境准备与工程结构规划3.1 环境依赖与PyTorch安装动手写代码之前先把环境搞定。我这里使用的配置是Ubuntu 20.04 CUDA 11.7 PyTorch 2.0显卡是单张RTX 309024G显存对于去雾网络这个量级的训练任务完全够用。PyTorch安装最推荐的方式是直接去PyTorch官网找对应环境的一条命令来执行。官网会根据你选择的操作系统、包管理工具和CUDA版本自动生成安装指令比从网上搜的杂七杂八的教程靠谱得多尤其能避免版本不匹配导致的环境崩溃。我这边的安装命令大致是conda create -n dehaze python3.9 conda activate dehaze pip install torch torchvision --index-url https://download.pytorch.org/whl/cu118依赖库方面图像处理用opencv-python和Pillow数据组织用numpy训练可视化用tensorboard进度显示用tqdm。如果只做推理不训练只需要PyTorch和opencv就足够了。注意开vscode的时候把Python解释器切到dehaze这个conda环境不然导入torch会报ModuleNotFoundError。3.2 项目文件结构工程组织按照可复现的标准来把网络定义、数据集、训练测试逻辑分离开。一个典型的目录结构是这样DEA-Net-Reproduce/ ├── models/ │ ├── __init__.py │ ├── dec.py # 细节增强卷积模块 │ ├── attention.py # 通道注意力模块 │ └── dehaze_net.py # 完整去雾网络 ├── datasets/ │ ├── __init__.py │ └── resice_dataset.py # 数据集加载 ├── train.py # 训练入口 ├── test.py # 测试入口 ├── metrics.py # PSNR/SSIM实现 └── utils.py # 工具函数模型和数据处理分开写的好处是后续调试方便。如果你想换数据集或者改网络结构不用在训练脚本里大动干戈。另外一个经验是写代码时尽量把每个模块单独定义好尤其是DEC这种核心模块能独立测试就独立测试别等组装完整个网络再返工。4. 手写PyTorch代码从DEC模块到完整去雾网络4.1 细节增强卷积模块实现DEC模块是整篇博客的主角代码实现一定要扎实。我按照前面说的多分支设计思路写了一个版本核心结构如下import torch import torch.nn as nn import torch.nn.functional as F class DetailEnhancementConv(nn.Module): def __init__(self, in_channels, out_channels, dilation2): super().__init__() # 基础分支3x3卷积提取主干特征 self.base_conv nn.Conv2d( in_channels, out_channels, kernel_size3, padding1, biasFalse ) # 细节分支1x1卷积把高频信号映射到特征空间 self.detail_conv nn.Conv2d(in_channels, out_channels, kernel_size1, biasFalse) # 上下文分支带膨胀率的3x3卷积扩大感受野 self.context_conv nn.Conv2d( in_channels, out_channels, kernel_size3, paddingdilation, dilationdilation, biasFalse ) self.bn nn.BatchNorm2d(out_channels) self.relu nn.ReLU(inplaceTrue) # 分支融合权重输入是拼接后的特征 self.fusion nn.Sequential( nn.AdaptiveAvgPool2d(1), nn.Conv2d(out_channels * 3, 3, kernel_size1), nn.Softmax(dim1) ) # 高通核用register_buffer保存不参与梯度更新 laplacian torch.tensor( [[0, -1, 0], [-1, 4, -1], [0, -1, 0]], dtypetorch.float32 ).view(1, 1, 3, 3) self.register_buffer(laplacian_kernel, laplacian) def high_frequency(self, x): if x.shape[1] 1: return F.conv2d(x, self.laplacian_kernel, padding1) kernel self.laplacian_kernel.expand(x.shape[1], -1, -1, -1) return F.conv2d(x, kernel, padding1, groupsx.shape[1]) def forward(self, x): base self.base_conv(x) # 细节分支提取高频分量后再过1x1卷积 detail self.detail_conv(self.high_frequency(x)) context self.context_conv(x) # 根据输入动态计算三个分支的权重 concat torch.cat([base, detail, context], dim1) weights self.fusion(concat) # [B, 3, 1, 1] out base * weights[:, 0:1] detail * weights[:, 1:2] context * weights[:, 2:3] out self.bn(out) out self.relu(out) # 残差连接 return out x这里有几个关键细节要说明。第一高通滤波那一步我用的是一个固定拉普拉斯核不参与训练它在梯度回传的时候不会对输入产生额外的参数更新但能让细节信号在前向传播的过程中显式增强。第二分支融合权重是用全局池化加Softmax生成的它能保证权重之和为1相当于自适应加权平均避免融合后特征的幅度出现大的波动。第三最后加了残差连接这样DEC模块至少不会比普通卷积更差因为极端情况下网络可以选择不依赖增强路径。4.2 通道注意力模块实现通道注意力实现参考了SE-Net和CBAM的做法同时对全局平均池化和全局最大池化两条路径进行建模class ChannelAttention(nn.Module): def __init__(self, channels, reduction8): super().__init__() self.avg_pool nn.AdaptiveAvgPool2d(1) self.max_pool nn.AdaptiveMaxPool2d(1) self.mlp nn.Sequential( nn.Conv2d(channels, channels // reduction, kernel_size1, biasFalse), nn.ReLU(inplaceTrue), nn.Conv2d(channels // reduction, channels, kernel_size1, biasFalse) ) self.sigmoid nn.Sigmoid() def forward(self, x): avg_out self.mlp(self.avg_pool(x)) max_out self.mlp(self.max_pool(x)) scale self.sigmoid(avg_out max_out) return x * scale两个池化路径互补性很强。平均池化关注通道的全局统计信息最大池化更强调通道内响应最强的区域在去雾任务中后者对于捕捉局部浓雾区域、高光区域尤其有价值。reduction参数控制MLP的压缩比例我对比过8和16的设置reduction8的效果略好因为通道数本身不大去雾网络不像分类网络那样动辄上千通道压缩太狠会丢失判别信息。4.3 完整去雾网络组装有了DEC和通道注意力完整网络组装起来就比较清晰了。这里我采用了一个轻量级的编码器-解码器结构总共才400多万参数比动辄上千万参数的模型友好很多。网络结构是这样的class DehazeNet(nn.Module): def __init__(self, in_channels3, base_channels16): super().__init__() # 输入预处理卷积 self.head nn.Sequential( nn.Conv2d(in_channels, base_channels, kernel_size3, padding1), nn.ReLU(inplaceTrue) ) # 编码器三层特征通道依次扩大 self.enc1 self._make_stage(base_channels, base_channels * 2) # 32 self.enc2 self._make_stage(base_channels * 2, base_channels * 4) # 64 self.enc3 self._make_stage(base_channels * 4, base_channels * 8) # 128 # 瓶颈层细节增强最核心的地方 self.bottleneck nn.Sequential( DetailEnhancementConv(base_channels * 8, base_channels * 8), ChannelAttention(base_channels * 8) ) # 解码器通道逐渐压缩同时带跳连 self.dec3 self._make_stage(base_channels * 8, base_channels * 4) self.dec2 self._make_stage(base_channels * 4, base_channels * 2) self.dec1 self._make_stage(base_channels * 2, base_channels) # 输出层 self.tail nn.Conv2d(base_channels, 3, kernel_size3, padding1) # 跳连投影把编码器特征对齐到解码器通道 self.proj3 nn.Conv2d(base_channels * 8, base_channels * 4, kernel_size1) self.proj2 nn.Conv2d(base_channels * 4, base_channels * 2, kernel_size1) self.proj1 nn.Conv2d(base_channels * 2, base_channels, kernel_size1) def _make_stage(self, in_ch, out_ch): return nn.Sequential( DetailEnhancementConv(in_ch, out_ch), ChannelAttention(out_ch) ) def forward(self, x): x self.head(x) # 编码 e1 self.enc1(x) d1 F.max_pool2d(e1, 2) e2 self.enc2(d1) d2 F.max_pool2d(e2, 2) e3 self.enc3(d2) d3 F.max_pool2d(e3, 2) # 瓶颈 b self.bottleneck(d3) # 解码 u3 F.interpolate(b, scale_factor2, modebilinear, align_cornersFalse) u3 self.dec3(u3 self.proj3(e3)) # 跳连对齐 u2 F.interpolate(u3, scale_factor2, modebilinear, align_cornersFalse) u2 self.dec2(u2 self.proj2(e2)) u1 F.interpolate(u2, scale_factor2, modebilinear, align_cornersFalse) u1 self.dec1(u1 self.proj1(e1)) out self.tail(u1) return torch.sigmoid(out)这个网络的设计有几个值得说的点。第一编码器用最大池化下采样解码器用双线性插值上采样在去雾任务里比可学习的转置卷积稳定不容易产生棋盘效应。第二跳连并不是直接拼接而是先通过1×1卷积把编码器特征投影到解码器通道数再做逐元素相加。直接拼接会大幅增加解码器计算量而对于去雾来说编码器的高分辨率细节信息通过相加完全能够传递过来。第三最后一个输出层后面接了sigmoid把输出限定在0到1之间正好对应归一化之后的图像亮度值。如果想恢复非归一化图像这里需要改成无激活函数。5. 训练细节、损失函数与优化策略5.1 数据准备与预处理复现DEA-Net我推荐从RESIDE标准数据集开始。数据量不大包含室内OTS合成雾图和对应的清晰图足够验证模型结构是否有效。如果你要自己造数据利用深度图根据大气散射模型合成雾图也可以但要注意合成雾的透射率范围不要设得过于极值0.3到1.0之间比较合理太小的透射率会让网络去学硬猜而不是泛化规律。数据增强方面我使用了随机水平翻转、随机垂直翻转和随机旋转90度训练时把图像随机裁剪成256×256的块。归一化上去雾任务和图像分类不同直接用0到1范围内的原始值而不做ImageNet均值和方差标准化因为这本身是个图像到图像的回归任务标准化的统计信息会把图像的绝对亮度打乱导致网络输出的色调偏掉。5.2 损失函数组合与比例设置单用L1损失训练出来的去雾网络虽然收敛快但细节恢复欠佳图像看起来有点糊。我验证下来效果最好的组合是感知损失权重1使用VGG16的conv3_3特征计算预测图和真值图的L1距离约束语义级别的相似。L1损失权重0.6约束像素级亮度一致。SSIM损失权重0.4约束结构相似性缓解边缘模糊问题。频率域损失权重0.2对图像做小波变换或者直接用拉普拉斯算子的高频分量算L1损失强化细节恢复。感知损失比较吃显存因为需要把整图输入VGG16。我训练时输入256×256batch size设为12一张3090刚好跑得动。loss比例的选择不是拍脑袋定的感知损失如果权重过高网络会把图像生成得“过顺滑”细节反而被磨掉SSIM太低则结构保持不住。我在调试时从1:0.5:0.3的基线上开始调最终收敛到上面这个比例。5.3 优化器、学习率与训练轮数优化器选Adam初始学习率设为1e-3beta值用默认的0.9和0.999。同步使用余弦退火学习率调度器最小学习率设为1e-5总训练轮数100个epoch前5个epoch做线性warmup让学习率从1e-5慢慢升到1e-3。这个细节很重要直接上来就用大学习率训练容易在第一个epoch就把网络初始化状态破坏后面很难收敛到好的解。梯度裁剪方面我设了max_norm5防止个别batch出现大梯度导致loss突然爆炸。训练过程中验证集上的PSNR在60个epoch左右开始趋于平缓80个epoch时基本收敛。如果资源紧张训练50个epoch也足够得到一个可用的模型后面30个epoch主要是在打磨边缘细节。6. 实验对比与复现经验总结6.1 评价指标PSNR与SSIM去雾效果的量化评价主要看PSNR和SSIM。PSNR衡量像素级重建误差数值越高越好SSIM衡量结构相似度取值0到1之间越接近1越好。这两个指标的计算量不大测试时直接遍历整个验证集逐张算好再取平均。我测试时把输入输出统一缩放到0到255范围内计算PSNR避免归一化范围不一致导致指标失真。有一个很实际的坑如果网络输出经过了sigmoidPSNR是在0到1的范围内算的而传统PSNR定义需要0到255的范围这会直接造成PSNR偏低10dB左右。这就是为什么测试代码里要先乘255再做clip再计算指标。6.2 有无DEC模块的效果差异为了单独验证细节增强卷积的作用我做了消融实验一组用普通3×3卷积替换所有DEC模块其余训练设置完全一致另一组保留DEC。同样训练100个epoch后保留DEC的模型在RESIDE测试集上PSNR比基础版提高了约1.8dBSSIM也提升了0.02左右。主观视觉上差距更明显。用普通卷积的模型虽然整体图像亮度恢复正常但放大看远处的树叶和建筑边缘纹理还是模糊的。而加入了DEC模块之后边缘锐利度明显提升尤其是雾比较浓的区域恢复出来的细节轮廓更明确。这跟论文里声称的效果方向是一致的。6.3 复现过程中遇到的几个关键坑第一个坑是BatchNorm在测试时和训练时的行为不一致。训练时BN统计的是当前batch的均值和方差测试时用的是训练阶段累积的running stats。如果测试图像的分布跟训练集相差较大BN会导致颜色偏色。解决办法是如果遇到偏色把BN替换成InstanceNorm或者GroupNorm去雾任务中后者往往更稳。第二个坑是输出图像的边缘可能出现“光晕”。这大多是因为损失函数里SSIM权重偏小网络在边缘区域把过渡拉得太快。把SSIM权重从0.3调高到0.4或者0.5之后这种伪影会明显减少。第三个坑在数据集上如果只用合成雾图训练去测试真实雾图效果会打折扣。合成雾图的透射率分布通常比较均匀真实雾图则往往有大片的浓雾区域分布差异很大。建议在训练数据里混合一部分真实雾图数据或者对合成雾图做更复杂的噪声和光照扰动。7. 速查表常规问题与排查方向下面是我训练去雾网络过程中沉淀下来的一张排查速查表遇到问题先对着看一眼能省不少调试时间。现象可能原因排查方向loss一开始就很高且不下降输出层没有归一化sigmoid漏了检查网络最后是否有sigmoid训练正常但测试时颜色偏色训练时归一化统计信息与测试不一致换用InstanceNorm或固定输入范围输出图像整体偏灰损失函数里SSIM占比过低调高SSIM权重检查是否用了perceptual边缘有halo伪影跳连特征未对齐或SSIM权重低检查解码器通道投影是否匹配批量训练时显存溢出输入尺寸过大或batch size过高开启梯度累积降低batch size收敛后细节仍然模糊感知损失权重过高降低perceptual loss权重调高L1训练一段时间后loss突然飙升学习率设置过大降低初始lr加梯度裁剪7.1 推理速度优化建议模型训练完之后如果要部署到实际业务里速度就成了关键。DEC模块相比普通卷积会多一点计算量但整体可控。在单张3090上256×256的输入PyTorch默认设置下推理耗时大约是4.5ms一帧换成ONNX导出并用TensorRT优化之后可以压到2ms以内。优化思路主要有几个方向把DEC里的三个分支做结构重参数化合并虽然目前细节分支是非线性的但如果部署时能接受一定精度损失可以把它合并到主卷积上把膨胀率固定为常量在ONNX导出时减少动态shape的算子输入尺寸固定的话把interpolate换成固定尺寸的反卷积也能提升一些速度。7.2 进一步扩展的思路DEA-Net里的细节增强卷积并不局限于去雾任务把它迁移到去雨、去噪、超分等底层视觉任务也是顺理成章的事。我试过直接把DEC模块替换到几个去噪网络里发现PSNR有小幅提升尤其是对边缘和纹理保护效果明显。如果你在做低光增强或者图像修复也不妨试试把这个模块接在你的主干网络上通常都能带来一点正向收益。写在最后自己在实际复现和调参过程中最大的体会是细节增强卷积这种“小模块大收益”的设计比盲目堆叠网络层数要实用得多。它不依赖复杂的数据集不依赖夸张的算力只要结构设计合理、训练细节到位单张消费级显卡也能复现出不错的效果。最后再分享一个调试小技巧在训练初期每隔10个epoch就把同一张雾图存下来进行对比不要只盯着loss数值看。这种可视化检查能最快暴露颜色偏置、细节丢失这类问题比任何指标都好使。希望这篇文章能帮你顺利跑通这个经典的细节增强去雾方案也欢迎把你在复现中遇到的问题丢过来一起交流。