news 2026/10/2 3:04:57

DehazeNet 去雾实战:PyTorch 实现、训练与部署全流程

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
DehazeNet 去雾实战:PyTorch 实现、训练与部署全流程

简介:这份资源是面向具备深度学习基础的研究者与图像处理方向学习者的PyTorch版DehazeNet图像去雾实现,提供从网络结构定义、训练流程到推理演示的完整代码链路,并附带已训练好的室内与室外场景预训练权重,可直接加载使用,也便于在现有架构上做扩展实验。压缩包共21个文件、约114KB,以9个Python脚本为核心,覆盖网络搭建、数据加载与训练入口;另有4个MATLAB脚本用于引导滤波等对比与演示,2个pth权重文件对应室内外模型,并配有说明文档与备份文件,整体结构紧凑、模块划分清晰。目前已有86人学习关注。借助预训练模型与配套脚本,读者可快速复现去雾效果、理解DehazeNet的训练与推理细节,并在此基础上开展消融或改进实验,适合作为课程设计、科研入门与算法对比的参考实现。

1. 从一张雾霾照片说起:DehazeNet 能解决什么

去年冬天帮朋友处理一批无人机航拍素材,原图灰蒙蒙一片,远处建筑轮廓几乎糊成一团。试过直方图均衡化和暗通道先验,前者把天空拉爆,后者在天空区域出现明显光晕。后来换成 DehazeNet,同一张图进去,雾感明显减弱,天空过渡自然,边缘也没有出现块状伪影。这就是深度学习去雾和传统方法最直观的差别——它不是靠固定先验硬算,而是从大量「有雾-无雾」图像对里学出映射关系。

DehazeNet 本身结构不复杂,核心是一个轻量卷积网络,输入有雾图像,输出透射率图,再结合大气光值按大气散射模型反推无雾图像。它比后来那些动辄几十层的大模型更适合入门和落地,显存占用低,推理速度快,一张 1080P 图在消费级显卡上基本是毫秒级。这份资源包含完整 PyTorch 实现和预训练模型,适合想跑通去雾全流程、又不想从零复现论文的开发者。下面按「结构原理 → 环境搭建 → 数据与训练 → 推理与部署 → 避坑 → 进阶技巧」的顺序拆开讲。

2. DehazeNet 结构拆解与 PyTorch 模块实现

2.1 为什么选 DehazeNet 而不是暗通道或大气光先验

传统去雾方法里,暗通道先验(DCP)是最常被拿来对比的基线。它的假设是:无雾图像的非天空区域,总有一个颜色通道的局部最小值趋近于零。这个假设在大多数户外场景成立,但遇到大片天空、白色墙面、雪地就失效,透射率估计会偏小,导致去雾后画面偏暗或出现光晕。大气光先验则对光源位置敏感,夜间或逆光场景容易估错。

DehazeNet 的思路是把透射率估计交给卷积网络。网络学的是局部纹理和雾浓度的统计关系,不依赖「暗通道趋零」这种硬假设。它的输入不是整张原图直接回归无雾图,而是先估计透射率 t(x),再用大气散射模型 I(x) = J(x)t(x) + A(1-t(x)) 反推 J(x)。这样做的好处是物理模型可解释,网络只需要专注估计一个中间量,训练难度比端到端回归无雾图低不少。

从工程角度看,DehazeNet 的参数量小,预训练模型文件通常只有几 MB 到十几 MB,加载快,适合嵌入到已有图像处理流水线里。如果你手头是嵌入式设备或边缘盒子,这个体量比很多去雾大模型友好得多。

2.2 网络层组成与 PyTorch 模块定义

DehazeNet 的结构可以概括为:浅层特征提取 → 多尺度特征融合 → 透射率回归。常见实现里,前几层用不同尺寸的卷积核并行提取特征,再拼接,后面接几个 1×1 或 3×3 卷积做通道融合,最后用一个卷积输出单通道透射率图。激活函数方面,原论文用了 BReLU(双边整流线性单元),目的是把透射率限制在 [0,1] 区间,同时保留双边约束。

下面是一个可直接用的 PyTorch 模块定义,我按常见复现方案整理,卷积核数量和通道数可以根据显存调整:

import torch import torch.nn as nn import torch.nn.functional as F class DehazeNet(nn.Module): def __init__(self): super(DehazeNet, self).__init__() # 多尺度特征提取:3x3、5x5、7x7 并行 self.conv1 = nn.Conv2d(3, 16, kernel_size=3, padding=1) self.conv2 = nn.Conv2d(3, 16, kernel_size=5, padding=2) self.conv3 = nn.Conv2d(3, 16, kernel_size=7, padding=3) # 特征融合 self.fuse = nn.Conv2d(48, 32, kernel_size=3, padding=1) self.conv4 = nn.Conv2d(32, 16, kernel_size=3, padding=1) # 透射率回归,输出单通道 self.conv5 = nn.Conv2d(16, 1, kernel_size=3, padding=1) self.relu = nn.ReLU(inplace=True) def forward(self, x): # 多尺度分支 f1 = self.relu(self.conv1(x)) f2 = self.relu(self.conv2(x)) f3 = self.relu(self.conv3(x)) # 通道拼接 feat = torch.cat([f1, f2, f3], dim=1) feat = self.relu(self.fuse(feat)) feat = self.relu(self.conv4(feat)) t = torch.sigmoid(self.conv5(feat)) # 约束到 [0,1] return t

这段代码里,conv1、conv2、conv3分别用 3×3、5×5、7×7 卷积核提取不同感受野的特征,拼接后通道数变成 48。fuse层把通道压到 32,再经过conv4降到 16,最后conv5输出单通道透射率。torch.sigmoid替代了原论文的 BReLU,效果接近且实现更简单。如果你要严格复现 BReLU,可以自定义一个nn.Module,在正区间做线性、负区间做有界处理。

参数方面,输入尺寸不固定,全卷积结构支持任意分辨率。但训练时建议裁剪到 256×256 或 512×512 的 patch,显存占用和 batch size 更好平衡。推理时可以整图输入,只要显存够。

2.3 大气散射模型与去雾反推

网络输出透射率 t 之后,还需要估计大气光值 A。常见做法是取有雾图像中暗通道最亮的前 0.1% 像素,对应到原图取均值作为 A。这个估计不完美,但在大多数场景够用。反推公式:

J(x) = (I(x) - A) / max(t(x), t0) + A

其中 t0 是透射率下限,防止分母过小导致噪声放大,一般取 0.1。下面是对应的 PyTorch 实现:

def recover_image(hazy, t, A, t0=0.1): # hazy: [B,3,H,W], t: [B,1,H,W], A: [B,3,1,1] t_clamped = torch.clamp(t, min=t0) # 按通道广播 J = (hazy - A) / t_clamped + A return torch.clamp(J, 0, 1)

t0这个参数很关键。设得太小,暗部噪声会被放大;设得太大,浓雾区域去不干净。我一般先在验证集上扫一遍 0.05 到 0.3,看哪个值在 PSNR 和主观观感之间平衡最好。A 的估计如果偏大,整体画面会偏亮;偏小则偏暗。如果场景里有大面积天空,建议对 A 做一次上限约束,比如不超过 0.95。

3. 环境搭建与预训练模型加载

3.1 PyTorch 环境配置:从 conda 到 CUDA 版本对齐

这份资源依赖 PyTorch,环境搭建是第一个容易翻车的地方。我习惯用 conda 建独立环境,避免和系统 Python 混在一起。下面是一套在 Windows 和 Linux 上都通用的流程:

conda create -n dehazenet python=3.9 -y conda activate dehazenet # 根据显卡驱动选择 CUDA 版本,这里以 CUDA 11.8 为例 pip install torch torchvision --index-url https://download.pytorch.org/whl/cu118 pip install opencv-python pillow numpy matplotlib tqdm

如果你用的是 30 系或 40 系显卡,CUDA 11.8 及以上版本兼容性较好。安装完成后用下面几行验证:

import torch print(torch.__version__) print(torch.cuda.is_available()) print(torch.cuda.get_device_name(0) if torch.cuda.is_available() else "CPU only")

torch.cuda.is_available()返回 False 是最常见的翻车点。原因通常有三个:装的是 CPU 版 PyTorch、CUDA 版本和驱动不匹配、或者 conda 环境里混入了多个 torch 安装。解决办法是先pip uninstall torch torchvision彻底卸载,再按官网对应版本重装。如果不想折腾 CUDA,CPU 版也能跑推理,只是训练会慢很多。

3.2 预训练模型加载与推理入口

资源里的预训练模型一般是.pth或.pt格式,加载时先实例化网络结构,再load_state_dict。注意map_location要设对,否则在无 GPU 机器上加载会报错:

import torch from model import DehazeNet # 假设网络定义在 model.py device = torch.device("cuda" if torch.cuda.is_available() else "cpu") net = DehazeNet().to(device) state_dict = torch.load("dehazenet_pretrained.pth", map_location=device) net.load_state_dict(state_dict) net.eval() # 读取图像并预处理 import cv2 import numpy as np img = cv2.imread("hazy.jpg")[:, :, ::-1] / 255.0 # BGR 转 RGB 并归一化 tensor = torch.from_numpy(img).permute(2, 0, 1).unsqueeze(0).float().to(device) with torch.no_grad(): t = net(tensor) # 估计大气光并反推 A = estimate_atmospheric_light(tensor) # 自定义函数,见 2.3 节思路 dehazed = recover_image(tensor, t, A)

net.eval()必须调用,否则 BatchNorm 和 Dropout 会按训练模式运行,推理结果不稳定。torch.no_grad()关闭梯度计算,能省显存并提速。预处理里归一化到 [0,1] 要和训练时一致,如果训练用了 ImageNet 均值方差,推理也要照做,否则透射率估计会偏。

3.3 输入尺寸与显存占用关系

DehazeNet 是全卷积结构,理论上支持任意尺寸,但实际推理时显存占用和输入分辨率成正比。下面是一组实测参考(batch size=1,FP32):

输入分辨率显存占用(约)单张推理耗时(RTX 3060)
512×5120.8 GB12 ms
1024×10242.1 GB38 ms
1920×10803.6 GB75 ms
3840×216012 GB+可能 OOM

如果显存不够,常见做法是把大图切成有重叠的 patch 分别推理,再拼接。重叠区域取平均可以避免接缝。切 patch 的尺寸建议是 32 的倍数,因为网络里有多次下采样和上采样对齐。

4. 数据准备、训练流程与损失函数选择

4.1 有雾-无雾图像对的获取与合成

DehazeNet 是监督学习,训练需要成对的「有雾图」和「无雾图」。公开数据集里 RESIDE、Haze4K 比较常用,但如果你手头只有无雾图,也可以按大气散射模型合成有雾图:

import numpy as np def synthesize_haze(clear, A=0.85, beta=1.2): # clear: [H,W,3] in [0,1] h, w = clear.shape[:2] # 生成随机深度图,模拟不同距离 depth = np.random.rand(h, w).astype(np.float32) t = np.exp(-beta * depth) # 透射率 t = np.clip(t, 0.1, 1.0) A_map = np.ones_like(clear) * A hazy = clear * t[..., None] + A_map * (1 - t[..., None]) return np.clip(hazy, 0, 1), t

beta控制雾的浓度,值越大雾越浓。A是大气光值,一般取 0.7 到 0.95。合成数据的优点是标签精确,缺点是分布和真实雾图有差距。我一般会混合使用:公开真实雾图占 70%,合成图占 30%,这样模型既见过真实分布,也有足够的透射率监督信号。

4.2 训练循环与损失函数

损失函数方面,原论文用了 MSE 监督透射率,但实际训练中只监督透射率容易导致去雾结果偏色。常见改进是加一项无雾图像重建损失:

import torch.nn as nn class DehazeLoss(nn.Module): def __init__(self, w_t=1.0, w_j=0.5): super().__init__() self.w_t = w_t self.w_j = w_j self.mse = nn.MSELoss() def forward(self, t_pred, t_gt, J_pred, J_gt): loss_t = self.mse(t_pred, t_gt) loss_j = self.mse(J_pred, J_gt) return self.w_t * loss_t + self.w_j * loss_j

w_t和w_j是两个损失的权重。如果透射率标签质量高,w_t可以设大一点;如果透射率标签是合成的、噪声大,就降低w_t,让重建损失主导。训练时用 Adam 优化器,学习率 1e-3 起步,每 20 个 epoch 衰减 0.5。batch size 根据显存选 8 到 32。

训练循环里记得每个 epoch 后在验证集上算 PSNR 和 SSIM,这两个指标比 loss 更能反映去雾质量。如果 PSNR 涨但 SSIM 不涨,可能是透射率估计偏平滑,边缘细节丢失。

4.3 数据增强与过拟合判断

去雾任务的数据增强要小心,不是所有增强都适用。水平翻转、随机裁剪、小角度旋转没问题。但颜色抖动要慎用,因为颜色偏移会改变大气光假设,可能让模型学到错误的透射率映射。我一般只做几何增强,颜色方面最多加一点亮度微调。

过拟合的判断信号:训练 loss 持续下降,验证 PSNR 在某个 epoch 后开始下降。这时候可以加 dropout、减小模型通道数、或者增加合成数据的比例。DehazeNet 参数量小,过拟合风险比大模型低,但如果训练集少于 1000 对,还是容易过拟合。

5. 推理部署与常见翻车排查

5.1 从 PyTorch 到 ONNX 的导出与验证

如果要把模型部署到非 Python 环境,导出 ONNX 是常见做法。下面这段代码把 DehazeNet 导出为 ONNX,并做一次数值验证:

import torch net.eval() dummy = torch.randn(1, 3, 512, 512).to(device) torch.onnx.export( net, dummy, "dehazenet.onnx", input_names=["input"], output_names=["transmission"], dynamic_axes={"input": {2: "h", 3: "w"}, "transmission": {2: "h", 3: "w"}}, opset_version=11 ) # 验证 ONNX 输出和 PyTorch 输出是否一致 import onnxruntime as ort sess = ort.InferenceSession("dehazenet.onnx") onnx_out = sess.run(None, {"input": dummy.cpu().numpy()})[0] torch_out = net(dummy).cpu().detach().numpy() print("最大误差:", np.abs(onnx_out - torch_out).max())

dynamic_axes让 ONNX 支持动态分辨率,否则导出后只能跑固定尺寸。opset_version建议 11 及以上,兼容性好。验证时最大误差在 1e-5 以内算正常,如果超过 1e-3,检查是否有不支持的自定义算子。

5.2 推理结果偏暗、偏色、光晕的排查

去雾结果不理想,先别急着改网络,按下面顺序排查:

现象可能原因解决
整体偏暗大气光 A 估计偏小提高 A 的取值,或改用暗通道最亮 0.1% 均值
天空区域光晕透射率在天空区域估计偏小对天空区域做透射率下限约束,或单独训练天空分支
画面偏蓝/偏黄训练数据颜色分布不均衡检查训练集白平衡,推理时加颜色校正
暗部噪声放大t0 设得太小把 t0 从 0.1 提到 0.2 或 0.3
边缘块状伪影输入尺寸不是 32 的倍数把输入 padding 到 32 的倍数再推理

这张表是我自己踩坑后整理的,大部分问题出在预处理和后处理,而不是网络本身。尤其是 A 的估计,很多人直接取全局最大值,结果画面整体偏亮,暗部细节全丢。

5.3 避坑清单:五条血泪经验

现象:加载预训练模型报Missing key(s) in state_dict。原因:网络定义和预训练模型的层名不一致,常见于自己改了模块命名。 解决:打印net.state_dict().keys()和torch.load(...).keys()对比,用strict=False先加载能匹配的层,再手动映射剩余层。

现象:推理结果全黑或全白。原因:输入图像没有归一化,或者归一化范围不对。训练用 [0,1],推理用了 [0,255],透射率输出会饱和。 解决:统一预处理,在推理入口加一行img = img / 255.0,并确认和训练时一致。

现象:GPU 显存够但报 OOM。原因:PyTorch 默认缓存显存,多次推理后碎片化。 解决:在推理循环里加torch.cuda.empty_cache(),或者把 batch size 降到 1。

现象:训练 loss 不下降。原因:学习率太大导致梯度爆炸,或者透射率标签范围不对。 解决:先把学习率降到 1e-4 试跑 10 个 epoch,确认 loss 有下降趋势再调回去。检查标签是否在 [0,1],如果标签是 [0,255] 要归一化。

现象:ONNX 推理结果和 PyTorch 不一致。原因:导出时用了training=True,或者有算子不支持动态尺寸。 解决:导出前调net.eval(),检查dynamic_axes设置,必要时换 opset 版本。

6. 进阶技巧:用分块推理处理 4K 图与 t0 自适应

前面提到大图推理会 OOM,分块推理是实用解法。但简单切块会有接缝,我一般用带重叠的滑窗,重叠区域取加权平均。下面是一个可复用的分块推理函数:

def infer_large_image(net, img_tensor, patch_size=512, overlap=64, device="cuda"): # img_tensor: [1,3,H,W] b, c, h, w = img_tensor.shape stride = patch_size - overlap output = torch.zeros_like(img_tensor) weight = torch.zeros_like(img_tensor) for y in range(0, h, stride): for x in range(0, w, stride): y1, y2 = y, min(y + patch_size, h) x1, x2 = x, min(x + patch_size, w) patch = img_tensor[:, :, y1:y2, x1:x2] with torch.no_grad(): t_patch = net(patch.to(device)) # 简单平均权重,边缘可以加高斯权重 output[:, :, y1:y2, x1:x2] += t_patch.cpu() weight[:, :, y1:y2, x1:x2] += 1 return output / weight.clamp(min=1)

patch_size建议 512,overlap取 64 到 128。重叠越大接缝越不明显,但计算量也越大。如果追求更平滑,可以把权重换成高斯核,边缘权重低、中心权重高。

另一个技巧是 t0 自适应。固定 t0=0.1 在浓雾图上会放大噪声,在薄雾图上又去不干净。我一般根据透射率图的均值动态调 t0:

def adaptive_t0(t, base=0.1): mean_t = t.mean().item() if mean_t < 0.3: # 浓雾 return min(base * 2, 0.3) elif mean_t > 0.7: # 薄雾 return base * 0.5 return base

这个策略不复杂,但在实际批量处理时能减少很多手动调参。从那以后我每次部署去雾模型,都会先跑一遍透射率统计,再决定 t0 和 A 的取值,而不是一套参数用到底。希望帮到你。

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/2 3:04:57

云服务器成本优化实战:从选型到架构的降本指南

上个月整理自己的云资源账单时&#xff0c;我发现一台2核4G的云服务器实例已经连续运行了47天&#xff0c;而它承载的只是一个几乎没人访问的内部演示环境。月底看到那笔并没有创造实际价值的支出时&#xff0c;我第一次真正意识到&#xff1a;云服务器这种东西&#xff0c;开起…

作者头像 李华
网站建设 2026/10/2 3:04:48

RealVNC企业级批量部署:基于AD域的静默安装与集中授权方案

1. 项目概述&#xff1a;为什么企业必须把VNC服务激活和管理“当回事”RealVNC是Windows环境下最主流的远程桌面协议&#xff08;RDP&#xff09;补充方案之一&#xff0c;尤其在需要跨平台、低延迟、图形界面交互强的场景中——比如IT支持团队远程协助产线工控机、研发人员调试…

作者头像 李华
网站建设 2026/10/2 3:03:36

维普AIGC检测超标怎么办?比话AI降AI实测全流程记录

每年到了论文送审和软著提交通道开放的那几周&#xff0c;我的私信就会准时热闹起来。问题高度一致&#xff1a;“维普AIGC检测显示我的论文AI疑似度40%&#xff0c;怎么办&#xff1f;”“软著文档AIGC检出率高&#xff0c;补正通知已经下了&#xff0c;还能救吗&#xff1f;”…

作者头像 李华
网站建设 2026/10/2 3:03:23

OpenClaw实战:从WSL2环境配置到跑通第一句Hello

如果你关注AI智能体&#xff08;Agent&#xff09;方向&#xff0c;最近大概率刷到过OpenClaw这个名字。它是一个开源的、本地优先的个人AI助手运行时&#xff0c;和市面上那些套壳ChatBot完全不同&#xff0c;它更像是给大模型装上了一套能收消息、能执行任务、能记住上下文的…

作者头像 李华
网站建设 2026/10/2 3:03:06

vSphere 8.0.2 中文手册实战指南:从 ESXi 安装到 DRS/HA 排错

简介&#xff1a;这份资源是VMware vSphere 8.0.2全套中文官方手册的离线PDF合集&#xff0c;面向虚拟化运维工程师、数据中心管理员以及正在备考相关认证的技术人员&#xff0c;用于解决无网络环境下查阅官方文档、系统学习ESXi与vCenter Server的问题。压缩包共866个文件&…

作者头像 李华
网站建设 2026/10/2 3:01:52

C语言Socket编程实战:从TCP/UDP基础到网络排错全指南

我第一次用Java写Socket程序的时候&#xff0c;觉得这事简直太简单了—— new Socket(host, port)&#xff0c;然后拿流读写就完事了。直到后来线上服务出现大批连接超时&#xff0c;日志里刷着"socket read timed out"&#xff0c;我对着连接池代码一筹莫展&#xf…

作者头像 李华