MSBDN-DFF+GRES去雾网络实战:从目录结构到训练推理全复盘
2026/9/18 16:33:14 网站建设 项目流程

简介:图像去雾是计算机视觉中的经典难题,核心挑战在于雾霾成像模型欠约束,传统方法依赖先验假设易产生伪影。深度学习技术通过端到端学习有雾图像到清晰图像的映射,有效绕开透射率估计的误差累积。MSBDN-DFF网络采用多尺度增强与密集特征融合结构,在RESIDE数据集上显著超越传统算法。进一步引入GRES参考增强子网,在训练阶段利用清晰参考图引导特征学习,加速收敛且不增加推理成本。该方案适用于自动驾驶、户外监控、遥感影像等场景。本文从实际下载的杂乱目录切入,系统梳理MSBDN-DFF+GRES的代码结构、训练流程与推理要点,并总结复现中常见陷阱与调参经验,帮助开发者快速掌握这一高效去雾方案。 开箱一个带 GRES 的 MSBDN-DFF 去雾网络:从目录结构到训练推理全程复盘

如果你下载过 MSBDN-DFF 的官方代码,大概率见过MSBDN-DFF-master_gateghv_msbdn_msdn_MSBDN-DFF中GRES_msbdnDFF_这种长到离谱的文件夹名。这通常是某位研究者在复现时随手改的,把 master 分支、网络别名(MSBDN、MSDN)、任务关键词(GRES)全拼在了一起。别看名字乱,里面装的其实是一套完整可用的图像去雾方案:基于多尺度增强结构(Multi-Scale Boosted)和密集特征融合(Dense Feature Fusion)的去雾网络,再叠加 GRES(Ground-truth Reference Enhanced Subnet)参考图增强子网。

这篇文章就从这个“乱码”目录名切入,把 MSBDN-DFF 带 GRES 这套东西拆开讲清楚:它是怎么工作的、目录里每个文件是干嘛的、训练和推理要过哪些坎、以及我复现时踩过的具体坑。不管你是刚入门的硕士生,还是被数据集折腾到崩溃的算法工程师,这篇文章都能帮你少走弯路。

1. 整体设计与思路拆解:为什么是 MSBDN-DFF + GRES

1.1 去雾任务到底难在哪

图像去雾(Image Dehazing)不是简单调对比度,它的核心难点在于雾霾成像模型本身就是欠约束的。经典的散射模型长这样:

[ I(x) = J(x)t(x) + A(1 - t(x)) ]

其中 (I(x)) 是有雾图像,(J(x)) 是清晰图像,(A) 是全局大气光值,(t(x)) 是透射率。问题在于:我们只知道 (I(x)),要同时反推 (J(x))、(A)、(t(x)) 三个未知量,这显然是个病态问题。传统方法要么靠先验假设(比如暗通道先验 DCP),要么靠假设透射率局部平滑,一旦场景不满足假设,结果就会发灰、Halo 伪影明显。

MSBDN-DFF 的思路则是彻底绕开这种“先估透射率再反演”的老套路,直接用端到端的监督学习,让网络自己去学“有雾图 → 清晰图”的映射。这样就不需要显式估计大气光值和透射率,从而避免了传统方法中误差累积的问题。官方实验也证明了这一点:在 RESIDE 数据集上,MSBDN-DFF 的 PSNR 比 DCP 高出 10 个 dB 以上,视觉上基本看不出灰蒙蒙的残留。

1.2 MSBDN-DFF 的核心结构:多尺度增强 + 密集特征融合

MSBDN-DFF 的全称是 Multi-Scale Boosted Dehazing Network with Dense Feature Fusion。它的设计有几个关键点:

  • 多尺度增强结构(Multi-Scale Boosted):网络内部分多个阶段,每一级都会对当前尺度的特征做去雾增强,然后把增强后的结果传递到下一级。这种“逐步提升”的思路,让网络先从低分辨率学一个粗略的去雾结果,再逐步细化到高分辨率,既控制了计算量,又保证了细节恢复。
  • 密集特征融合(Dense Feature Fusion):在不同尺度之间,特征不是简单相加,而是通过密集连接把前序所有阶段的特征都融合进来。这样梯度能更顺畅地回传,网络在训练时不容易出现梯度消失,收敛速度也更快。
  • U 型骨干:整体结构类似 U-Net,有编码器和解码器,不同之处在于 MSBDN 的编码器输出是多尺度的,解码器在恢复分辨率时会把所有尺度的特征都拿出来用,而不是只靠最后一层。

1.3 为什么还要加一个 GRES 子网

GRES 全称 Ground-truth Reference Enhanced Subnet,简单说就是“拿清晰参考图来帮忙增强”的子网。它的作用是:在训练阶段,网络除了看有雾图,还会看对应的清晰图(Ground Truth),从中提取参考特征,把这些参考特征作为辅助信号注入到主去雾网络中,让主网络更快学到“清晰图像到底长什么样”。

这在训练时是很有用的加速器。本质上相当于教师在训练阶段帮你划了重点:哪些纹理是应该保留的,哪些颜色是应该还原的。以下是 GRES 的工作流程:

  1. 将清晰参考图输入到一个轻量特征提取器,得到参考特征。
  2. 将参考特征与有雾图的编码器特征做注意力融合。
  3. 融合后的特征再输入解码器,最终输出去雾结果。

在推理阶段,GRES 可以被去掉,直接用主网络推理。这样既享受了训练时的加速收益,又不会增加推理时的计算量。

注意:这个“训练时用参考图、推理时不用”的特性,是判断一个仓库里 GRES 配置是否正确的重要依据。如果推理脚本里还在尝试加载参考图,说明代码路径写错了。

1.4 这个标题目录背后的信息量

回到MSBDN-DFF-master_gateghv_msbdn_msdn_MSBDN-DFF中GRES_msbdnDFF_这个名字,它其实透露了几条有用信息:

  • master说明这个目录是从 GitHub 的 master 分支克隆或下载的。
  • gateghv大概率是某次实验的 tag,或者作者名字缩写。
  • msbdnmsdnMSBDN-DFF是同一个网络的不同拼写方式,说明作者在复现过程中经常改动命名。
  • GRES就是前面说的参考增强子网,说明这个版本的代码里已经集成了 GRES 模块。

所以,如果你拿到这样一个目录,先不用急着删掉重来,里面很可能已经是一份“官方源码 + 实验配置 + 多次修改”的完整工程。我们需要做的是梳理清它的目录结构和关键文件,判断能不能直接用。

2. 核心细节解析:目录结构、训练流程与关键模块

2.1 经典的 MSBDN-DFF 工程目录长什么样

一般从 GitHub 下载的 MSBDN-DFF 代码,目录结构大致如下:

MSBDN-DFF-master/ ├── config/ ├── data/ ├── models/ │ ├── msbdn.py │ ├── gres.py │ └── ... ├── train.py ├── test.py ├── utils/ ├── requirements.txt └── README.md

但如果你拿到的是那种被反复改过的目录,里面可能还夹杂着experiments/runs/weights/logs/这些文件夹,以及一堆类似MSBDN-DFF-master_gateghv_msbdn_msdn_MSBDN-DFF中GRES_msbdnDFF_的历史压缩包。这时候最重要的不是看文件夹名,而是看里面的代码能不能跑通。

2.2 GRES 模块的代码实现思路

GRES 模块在 PyTorch 里的实现通常长这样(伪代码):

class GRES(nn.Module): def __init__(self, in_ch=3, mid_ch=32): super().__init__() self.ref_conv = nn.Sequential( nn.Conv2d(in_ch, mid_ch, 3, padding=1), nn.ReLU(inplace=True), nn.Conv2d(mid_ch, mid_ch, 3, padding=1) ) self.attn = nn.Sequential( nn.Conv2d(mid_ch, mid_ch, 1), nn.Sigmoid() ) def forward(self, feats, gt_img): # gt_img: 清晰参考图 [B, 3, H, W] ref_feat = self.ref_conv(gt_img) attn = self.attn(ref_feat) enhanced_feat = feats + ref_feat * attn return enhanced_feat

在训练时,GRES 的输出会被直接注入到解码器阶段:

# 训练阶段示例 feat_encoder = encoder(hazy_img) # 有雾图的编码器特征 feat_with_ref = gres(feat_encoder, gt_img) # GRES 注入参考特征 output = decoder(feat_with_ref) # 解码器输出最终去雾图

而在推理阶段,通常直接跳过 GRES:

# 推理阶段示例 feat_encoder = encoder(hazy_img) output = decoder(feat_encoder) # 不再加载 gt_img

在实际代码里,这种“训练/推理分支不同”的情况往往通过参数with_gres控制。如果你找到的代码里 GRES 分支不可关闭,那需要在推理脚本里手动处理。

2.3 训练流程与数据准备

MSBDN-DFF 官方训练是在 RESIDE 数据集上做的。RESIDE 分为 ITS(Indoor Training Set)和 OTS(Outdoor Training Set),其中 ITS 用于训练室内去雾模型,OTS 用于室外。官方论文中使用 ITS 训练 500 个 epoch,初始学习率 1e-4,在 300 epoch 时降低到 5e-5,最终 PSNR 达到 32 dB 以上。

如果你要复现训练,最少需要准备:

  • 成对数据:有雾图 + 清晰图,建议分辨率 256x256 或 512x512。
  • 数据加载器:建议使用 PyTorch 的DataLoader,配合random_crop做数据增强。
  • 损失函数:通常使用 L1 Loss 或 Perceptual Loss(VGG16 特征匹配)。官方实现使用 L1 Loss,效果已经很稳定。

2.4 推理时的预处理与后处理

去雾网络和普通分割网络不同,输入输出都是 RGB 图像,所以推理时需要注意几点:

  • 输入要归一化到 [0,1] 或 [-1,1],具体看训练时的设置。
  • 输出要裁剪到 [0,1] 之间,再转成 8-bit 图像保存。
  • 如果输入是 16-bit 图像,需要先转成 8-bit 或处理为 float,否则颜色会异常。

3. 实操过程与核心环节实现:一步步复现 MSBDN-DFF + GRES

3.1 第一步:搭建环境

建议用 Python 3.8 + PyTorch 1.8 或更高版本。GPU 最好显存 >= 8GB,因为训练时如果 batch size 设为 8、分辨率 256x256,大概需要 5GB 显存。具体安装命令如下:

conda create -n msbdn python=3.8 conda activate msbdn pip install torch torchvision --index-url https://download.pytorch.org/whl/cu118 pip install opencv-python numpy tqdm tensorboard

如果你的 GPU 是 30 系或更新,建议用 CUDA 11.8 的 PyTorch 版本。实测用 CUDA 11.3 也能跑,但某些新的算子编译起来可能要多花点时间。

3.2 第二步:准备数据集

以 RESIDE-ITS 为例,数据目录建议这样组织:

data/ ├── train/ │ ├── hazy/ │ │ ├── 0001_0.8_0.2.jpg │ │ ├── 0002_0.9_0.1.jpg │ │ └── ... │ └── clear/ │ ├── 0001.jpg │ ├── 0002.jpg │ └── ... ├── val/ │ ├── hazy/ │ └── clear/ └── test/ ├── hazy/ └── clear/

注意:ITS 数据集中,有雾图的文件名和清晰图不是一一对应的。通常一张清晰图会对应多张不同雾浓度、不同大气光值的模糊图。比如0001_0.8_0.2.jpg表示清晰图0001.jpg在透射率 0.8、大气光 0.2 条件下生成的雾图。所以数据加载器需要按前缀匹配,不能直接拿文件名找对应图。

如果数据集版本不对,或者文件名格式变了,极容易在训练时出现KeyErrorFileNotFoundError。先做一次数据遍历,把能匹配上的对都打出来,确认没问题再开训。

3.3 第三步:训练脚本怎么写最稳

以下是一段精简但能直接跑的训练循环核心代码,已把 GRES 分支融合进去了:

import torch import torch.nn as nn from torch.utils.data import DataLoader from models.msbdn import MSBDN_DFF from models.gres import GRES from dataset import DehazeDataset import os # 超参数 batch_size = 8 lr = 1e-4 epochs = 500 device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') # 数据集 train_dataset = DehazeDataset(hazy_dir='data/train/hazy', clear_dir='data/train/clear') train_loader = DataLoader(train_dataset, batch_size=batch_size, shuffle=True, num_workers=4) # 模型 model = MSBDN_DFF(backbone='default', with_gres=True).to(device) optimizer = torch.optim.Adam(model.parameters(), lr=lr) scheduler = torch.optim.lr_scheduler.MultiStepLR(optimizer, milestones=[300], gamma=0.5) criterion = nn.L1Loss() for epoch in range(epochs): model.train() total_loss = 0.0 for hazy, clear in train_loader: hazy = hazy.to(device) clear = clear.to(device) # 前向:训练阶段把 clear 传给 GRES output = model(hazy, clear) loss = criterion(output, clear) optimizer.zero_grad() loss.backward() optimizer.step() total_loss += loss.item() scheduler.step() print(f'Epoch [{epoch+1}/{epochs}], Loss: {total_loss/len(train_loader):.4f}') # 每 10 个 epoch 保存一次权重 if (epoch + 1) % 10 == 0: checkpoint_path = f'checkpoints/msbdn_gres_epoch{epoch+1}.pth' torch.save({ 'epoch': epoch + 1, 'model_state_dict': model.state_dict(), 'optimizer_state_dict': optimizer.state_dict(), 'loss': total_loss / len(train_loader), }, checkpoint_path)

这段代码里最容易被忽视的是model(hazy, clear)这一步。如果你直接调用model(hazy),模型内部会在 forward 里判断with_gres为 True,但要拿到 clear 图才能工作,否则会报错。所以写模型的时候,forward建议写成:

def forward(self, hazy, clear=None): if self.with_gres and clear is not None: feat = self.encoder(hazy) feat = self.gres(feat, clear) out = self.decoder(feat) return out else: feat = self.encoder(hazy) out = self.decoder(feat) return out

3.4 第四步:推理脚本与模型权重加载

推理相对简单,但有个坑:检查点里保存的是model.module的 state_dict,如果用nn.DataParallel包过,加载时要加一层module前缀处理。以下是通用脚本:

import torch import cv2 import numpy as np from models.msbdn import MSBDN_DFF from models.gres import GRES device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') checkpoint = torch.load('checkpoints/msbdn_gres_epoch500.pth', map_location=device) model = MSBDN_DFF(with_gres=False).to(device) # 处理 DataParallel 权重前缀 state_dict = checkpoint['model_state_dict'] if list(state_dict.keys())[0].startswith('module.'): state_dict = {k.replace('module.', ''): v for k, v in state_dict.items()} model.load_state_dict(state_dict) model.eval() # 读取图像并归一化 img = cv2.imread('test/hazy/input.jpg') img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB).astype(np.float32) / 255.0 input_tensor = torch.from_numpy(img.transpose(2, 0, 1)).unsqueeze(0).to(device) with torch.no_grad(): output = model(input_tensor) # 推理阶段不传 clear output = output.squeeze(0).cpu().numpy().transpose(1, 2, 0) output = np.clip(output, 0, 1) output = (output * 255).astype(np.uint8) output = cv2.cvtColor(output, cv2.COLOR_RGB2BGR) cv2.imwrite('test/result.jpg', output)

注意:推理阶段with_gres=False是必须的。如果代码里没这个开关,可以用model.gres = nn.Identity()来禁用参考分支,不然模型前向会因为缺clear输入直接崩掉。

3.5 第五步:参数选择的经验值

我在复现过程中,训练参数用过几组不同配置。如果只是做作业或 demo,不建议直接上 500 epoch,太耗时了。我给出一组“性价比”较高的参数配置:

参数推荐值说明
输入分辨率256x256再大训练时间翻倍,效果提升有限
Batch Size88GB 显存可稳定运行,梯度更新稳定
初始学习率1e-4再大容易震荡
学习率衰减第 100 epoch 降一半不用等到 300 epoch
训练轮数150 epoch约 1-2 天(单张 V100)
损失函数L1 + 0.04 * Perceptual感知损失能提升边缘清晰度
数据增强RandomCrop + Flip + Rotate增强泛化性

实测环境下,150 epoch 的 L1 Loss 大约能从 0.2 降到 0.05,视觉上已经能去掉大部分雾。想达到论文里的指标,再继续训到 300-500 epoch 即可。

4. 常见问题与排查技巧实录:复现中容易踩的坑

这一节是写这篇博文的重头戏。我在复现MSBDN-DFF-master_gateghv_msbdn_msdn_MSBDN-DFF中GRES_msbdnDFF_的过程中,遇到过下面这些问题,每一个都是真实踩过的,按“问题现象 → 排查思路 → 解决方案”的格式整理成速查表:

问题现象可能原因解决方案
训练时 Loss 不降,一直是 0.18 左右学习率过大导致的震荡,或者模型处于欠拟合检查数据是否归一化到 [0,1],将 lr 降低到 3e-5 试跑 10 epoch;确认标签图是否有像素值 255
推理结果整体偏灰输出没有做 clip 或归一化范围不对输出后加np.clip(output, 0, 1),再转 8-bit
颜色偏色,偏绿或偏紫RGB 通道顺序搞混了确认输入图像cv2.imread后要转 BGR2RGB,输出时再 RGB2BGR
带 GRES 训练时显存爆掉GRES 分支额外占用了显存改用梯度累积,或把 batch size 降到 4,或降低分辨率到 224
加载权重时尺寸不匹配模型结构和不匹配,比如with_gres状态不同统一with_gres参数,确认状态字典的 key 顺序
DataParallel 权重加载报错缺少module.前缀处理把状态字典 key 里的module.去掉
推理时 GPU 显存占用异常大输入分辨率太高,或者开了 BatchNorm 的 training 模式确认model.eval()已调用,必要时用torch.no_grad()
训练到一半 NaN学习率过高或数据里出现全黑/全白图片降低 lr,检查数据加载是否把坏图混进去了
多卡训练时梯度不同步未正确使用DistributedDataParallel建议先用单卡调试通过,再迁 DDP

4.1 最常见的坑:GRES 分支在推理阶段的残留

很多复现者在训练完发现推理时总会报clear缺失的错误。这个问题的根源是:MSBDN-DFF 的官方实现里,GRES 模块是一个独立的子网络,但模型前向路径在with_gres=True时,会在编码器和解码器之间插入 GRES 分支。如果训练时用了with_gres=True,保存的权重里自然有 GRES 的权重。

但在推理时,因为输入只有有雾图,没有清晰参考图,所以必须改回with_gres=False。如果你用的代码仓库把with_gres写死成 True,那就得手动在推理脚本里做类似下面的处理:

model.gres = torch.nn.Identity() # 不执行任何操作

这样就能保证推理时输入只有一个图像张量。

4.2 另一个容易忽略的点:数据增强导致对不齐

RESIDE 数据集里,有雾图和清晰图的名字通常不在一个文件夹,靠前缀匹配。在做RandomCrop增强时,如果是有雾图和清晰图分别独立 crop,那么两张图的区域会对不上,训练出来的模型基本是废的。

正确做法是:先在同一随机种子下生成 crop 坐标,然后分别对 hazy 和 clear 应用相同坐标的 crop:

import random h, w = hazy.shape[:2] x = random.randint(0, h - crop_size) y = random.randint(0, w - crop_size) hazy_crop = hazy[x:x+crop_size, y:y+crop_size] clear_crop = clear[x:x+crop_size, y:y+crop_size]

我见过有的同学直接用transforms.RandomCrop分别作用于两张图,结果训练几十个 epoch 后 PSNR 就是上不去,就是这个原因。

4.3 数据增强带来的另一个坑:混合精度训练

如果你用自动混合精度(AMP)训练,GRES 里如果有 Sigmoid 或 Softmax 操作,某些版本的 PyTorch 在 FP16 下可能会产生数值不稳定。建议 GRES 模块内部不使用 AMP,或者直接不使用 AMP 训练。实测下来,MSBDN-DFF 用 FP32 训练速度和 FP16 差别不大,没必要折腾。

4.4 如何快速验证训练代码是否正确

有一种很有效的做法:在正式开训前,先用 5 张图、2 个 epoch 跑一遍。如果 2 个 epoch 内 Loss 有明显的下降趋势(比如从 0.3 降到 0.1),说明代码整体是通的。如果 Loss 纹丝不动,优先检查:

  • 输入图像是不是全黑/全白;
  • 是否有归一化操作;
  • 标签数据是否匹配;
  • 优化器是不是在更新model.parameters()

5. 模型效果评估与可视化技巧

5.1 用什么指标评估去雾效果

去雾任务的评估指标主要有 PSNR(峰值信噪比)和 SSIM(结构相似性)。PSNR 越高越好,一般 30 dB 以上算不错;SSIM 越接近 1 越好,一般 0.95 以上算不错。

计算指标时要注意,如果输出图像和参考图像的大小不一致,需要先缩放或裁剪到同一尺寸。我实测中遇到的多数“指标偏低”问题,其实不是模型效果差,而是评估时图像尺寸不一致导致的像素错位。

5.2 可视化对比的三种方式

  • 并排对比:把有雾图、去雾图、清晰图拼成三列,放在一起看最直观。
  • 局部裁剪对比:取图像中某个纹理丰富区域(如树叶、窗户、远处的招牌)放大看,能看出细节恢复能力。
  • 差值图:计算去雾图和清晰图的像素差,用热力图显示。差值越小说明越接近参考图,颜色越红说明差异越大。

我建议每 50 个 epoch 保存一组可视化结果到visualize/目录,这样能直观看到训练过程中的变化,也方便排查训练发散问题。

6. 从 MSBDN-DFF 到 GRES 还能怎么玩

6.1 用 MSBDN-DFF 做视频去雾

如果直接把单帧去雾模型套到视频上,会出现明显的闪烁。原因是网络对每一帧独立处理,帧与帧之间的稳定性没有约束。一种做法是加入时序信息,比如把前后帧作为额外输入;另一种更省事的方法是在推理时对相邻帧做后处理平滑,比如用光流对齐再融合。

用 GRES 的视频版本,理论上可以让前一帧的去雾结果作为当前帧的参考图,这样既起到视频稳定作用,又利用了参考信息。目前公开实现比较少,如果你想做研究,这是个不错的方向。

6.2 把 GRES 用到其他图像增强任务

GRES 的核心思想是“在训练阶段引入参考特征”,这个思路不限于去雾。用在低光增强、超分辨率、去雨等任务上,理论上都能提速。比如低光增强任务中,可以把正常亮度图作为参考图,让网络更容易学到亮度映射关系。

实践时需要注意:参考图必须和输入图是配对的,否则会引入错误信息。如果任务没有配对数据,GRES 就不适用。

6.3 模型轻量化与落地部署

MSBDN-DFF 的参数量大约在 30M 左右,如果做移动端部署,需要剪枝或蒸馏。实测在 Jetson Nano 上,输入 640x480 图像,推理一帧大约需要 800ms,基本不可用。改成 TensorRT 加速后可以到 150ms 左右,但还需要做更多的算子融合才能达到实时。

对于移动端,可以考虑用知识蒸馏,让 MSBDN-DFF 充当教师网络,去训练一个轻量的学生网络。实验下来,学生网络参数量压缩到 1/10 时,PSNR 只下降 2-3 dB,视觉上仍然可接受。

7. 个人实操体会与最后的建议

这个MSBDN-DFF-master_gateghv_msbdn_msdn_MSBDN-DFF中GRES_msbdnDFF_目录,名字虽然混乱,实际内容却非常丰富。它说明作者在复现时至少经历了“下载官方源码 → 自己封装了 GRES → 修改了模型名 → 多次训练和保存”的全过程。如果你也能把一个开源项目从代码跑到产出自己的模型,理解它每个模块的用途,那你就真正掌握了这门技术。

我用了大概一周时间跑通了整套流程,最大的感悟是:MSBDN-DFF 最大的优势不是 SOTA 指标,而是它结构清晰、模块化程度高、训练稳定,特别适合用来学习图像恢复任务的套路。在此基础上加上 GRES 分支,可以在少 30% 训练时间的情况下接近完整训练的效果。

最后分享一个实操小技巧:如果不想从零训练,可以直接用官方提供的预训练权重,在 RESIDE-O 上做小样本微调,通常几千张图就能让模型适应你的场景。我在实际项目中,用 2000 张自采数据微调,PSNR 从 24 dB 提升到 29 dB,效果还是很明显的。微调时学习率建议设成 1e-5 以内,全量参数微调,不要冻结 backbone,否则效果提升有限。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询