简介:本资源是面向深度学习初学者与图像复原研究者的PyTorch版DehazeNet去雾实现方案,聚焦单幅图像雾霾去除这一经典低层视觉任务,适用于遥感、自动驾驶、监控视频增强等实际场景。压缩包共21个文件(114KB),包含9个核心Python模块(如net.py定义网络结构、train_*.py支持室内外数据集训练)、2个预训练.pth模型(best_indoor.pth与best_outdoor.pth)、3个MATLAB辅助函数(guidedfilter.m等用于对比验证)及完整readme与说明文档,结构清晰、开箱即用。已有40人学习下载,用户可直接加载预训练权重进行推理演示,亦能基于现有代码快速开展消融实验、数据增强或网络改进。项目兼顾工程实用性与教学可读性,所有模块解耦明确,便于理解DehazeNet的特征提取、多尺度融合与端到端优化机制。 图像去雾这个方向,很多年了一直没凉过。从早期的暗通道先验,到后来的深度学习算法,再到如今各种基于CNN和Transformer的端到端网络,学术界和工业界都投入了大量的研究。不过真正常被拿来当基线、跑通就能用的经典网络并不多,DehazeNet算一个。网上关于它的论文解读不少,但大多只讲了原理,真正把PyTorch复现、预训练模型加载、图像恢复全流程跑通的文章很少。这篇文章就基于我实际跑过的一个项目,把DehazeNet的完整实现过程掰开揉碎讲清楚,顺便把我踩过的坑和优化的细节一并整理出来,希望看完这篇内容,你也能顺利跑出满意的去雾效果。
DehazeNet是2016年提出的一种基于CNN的单图像去雾网络,核心思想是通过卷积神经网络直接估计雾图的透射率,再结合大气散射模型恢复出清晰图像。相比传统方法,它不需要手工设计特征,端到端训练,推理速度快,而且效果稳定。用PyTorch实现这个网络,配合训练好的预训练模型,几行代码就能完成单张图片的去雾处理。
这篇文章适合这几类读者:刚入门图像去雾、想跑通一个经典算法练手的研究生;需要在工程里快速集成去雾模块的开发者;以及对PyTorch模型训练和推理流程还不熟悉,想通过一个完整项目加深理解的同学。
1. 项目背景与整体设计思路
1.1 图像去雾的技术本质
先来聊一个核心问题:雾天图像到底是怎么形成的?为什么看起来灰蒙蒙的?
其实在计算机视觉里有一个非常经典的大气散射模型,它把雾图建模成清晰图像和大气光的线性组合。公式长这样:
I(x) = J(x) * t(x) + A * (1 - t(x))
其中I(x)是观测到的雾图,J(x)是我们要恢复的清晰图像,A是全局大气光,t(x)是透射率图。透射率反映了光线穿过雾气到达相机的衰减程度,取值范围在0到1之间,雾越浓,透射率越低。
这个公式是整个去雾任务的理论基础。给定一张雾图I,如果我们能估计出大气光A和透射率t,恢复清晰图像J就是一个简单的代数运算。所以几乎所有基于物理模型的去雾算法,本质上都在解决同一个问题:怎么把透射率图估准。
DehazeNet的定位非常明确,它就是不做任何手工先验,直接训练一个CNN,输入雾图,输出透射率图。整个系统的关键点就在这个透射率估计网络上面。
1.2 为什么选择DehazeNet
去雾算法发展到现在,大体经历了三个时代。第一代是传统的图像增强方法,比如直方图均衡化、Retinex等,简单但物理意义不足,容易产生颜色失真。第二代是基于物理模型的先验方法,最典型的就是何恺明提出的暗通道先验,效果不错,但处理天空区域容易偏色、耗时也高,需要配合导向滤波等优化手段。第三代就是深度学习方法,思路各不相同:有的直接端到端生成清晰图(如AOD-Net),有的先估计透射率再恢复(如DehazeNet、MSCNN)。
我为什么在这个项目里选了DehazeNet,有几点考虑:
第一,它的设计思路涉及的知识点非常完整。网络结构包含多尺度特征提取、拼接融合、局部极值约束、非线性激活等经典操作,训练过程涉及合成数据生成、损失函数设计、模型评估指标,很适合作为学习图像去雾的入门项目。
第二,它的规模适中。DehazeNet是一个很轻量的网络,参数量只有几百万级别,CPU上都能跑推理。不像现在很多SOTA模型动辄上亿参数,需要多卡训练。用好DehazeNet能让你把整个训练和推理链路吃透,而不是把时间浪费在调参和等训练上。
第三,效果上限不低。虽然DehazeNet不是目前效果最好的算法,但它的输出结果经过适当的后处理,比如配合导向滤波细化透射率,能达到比较专业的去雾水平,在很多实际场景下已经够用。
1.3 系统总体架构设计
我搭建这个系统的时候,没有只做一个孤立的模型文件,而是设计成了一个完整的处理流程。这个流程从输入一张雾图开始,到输出清晰图结束,中间主要包含四个模块:
雾图输入之后,先做预处理,包括尺寸调整和归一化;然后进入DehazeNet模型,得到透射率估计;接着通过暗通道方法计算大气光;最后把大气光和透射率代入大气散射模型的反演公式,得到恢复后的清晰图像,再做一次后处理输出。
这个设计思路借鉴了很多去雾工具箱的做法,但我在实现细节上做了不少调整,比如归一化方式的选择、大气光计算时的稳健性处理、输出图像的范围裁剪等。这些细节在后面的章节里会逐一展开,每一步改动都是有原因的。
2. 环境准备与依赖安装
2.1 PyTorch环境搭建
既然是"基于PyTorch"的项目,环境搭建是绕不开的第一步。这里我假设你已经在电脑上装好了Python和Anaconda,如果还没有,建议先装Anaconda,用它来管理虚拟环境非常方便,可以避免不同项目之间的依赖冲突。
我的建议是用conda创建独立的环境,不要把依赖安装到base环境里。具体命令如下:
conda create -n dehaze python=3.9 -y conda activate dehaze然后安装PyTorch。这里要特别注意,不同版本的PyTorch安装命令不一样,需要根据你的CUDA版本和硬件来选。如果用的是NVIDIA显卡,推荐安装GPU版本,速度会快很多。如果只是学习或者没有独显,安装CPU版本也完全能跑通这个项目,只是训练和推理慢一些。
# 有NVIDIA显卡,安装CUDA 11.8版本对应的PyTorch pip install torch torchvision --index-url https://download.pytorch.org/whl/cu118 # 或者没有显卡,安装CPU版本 pip install torch torchvision --index-url https://download.pytorch.org/whl/cpu这里有一个容易踩的坑:如果你本机已经装了CUDA,但安装PyTorch时版本不匹配,运行时不会报错,但你调用torch.cuda.is_available()会一直返回False,然后你就只能用CPU慢慢跑。所以装完之后一定要先验证一下:
import torch print(torch.__version__) print(torch.cuda.is_available())如果显卡驱动是支持的CUDA版本,但PyTorch识别不了,多半是安装的PyTorch版本对应的CUDA比你本机驱动支持的CUDA更高。这时候换个低一点的CUDA版本重新装即可。这个排查思路在第六章会再讲。
2.2 依赖清单与项目结构
除了PyTorch之外,这个项目还需要几个常用的Python库。我在requirements.txt里是这样写的:
torch>=1.13 torchvision>=0.14 numpy>=1.21 opencv-python>=4.5 pillow>=9.0 matplotlib>=3.5 scipy>=1.7scipy用来做导向滤波里的某些矩阵运算,但其实opencv也有引导滤波的实现,看个人习惯。OpenCV负责图像I/O和基本图像处理,比如缩放、颜色空间转换。如果不想装OpenCV,也可以用Pillow+NumPy替代,但OpenCV更顺手,处理速度也快。
接下来是项目目录结构。一个清晰的结构能让你后续维护省很多心,我采用的是这样一个布局:
dehazenet_pytorch/ ├── models/ │ ├── __init__.py │ └── dehazenet.py # DehazeNet网络定义 ├── utils/ │ ├── __init__.py │ ├── data_utils.py # 数据集加载与预处理 │ ├── image_utils.py # 图像I/O与后处理 │ └── haze_model.py # 大气散射模型辅助函数 ├── pretrained/ │ └── dehazenet.pth # 预训练权重文件 ├── test_images/ │ └── hazy_input.jpg # 测试图片 ├── run_inference.py # 单图/批量推理入口 ├── train.py # 训练脚本 └── requirements.txt这样的结构把网络定义、工具函数、模型权重、推理入口分开,每一层职责清楚。后面要加新的功能,比如视频去雾、批量处理文件夹,只需要新写一个脚本调用已有的模块就行。
2.3 预训练模型获取
DehazeNet的官方权重是Caffe框架下训练的。如果直接拿Caffe的权重转到PyTorch,流程比较麻烦,得先把Caffe的模型和参数读出来,再一层层映射到PyTorch的state_dict。我在项目里其实用的是一份从训练数据上重新训练得到的PyTorch权重,这样避免了框架转换的坑,效果也和原论文的指标基本一致。
如何获取预训练模型?通常有几个途径:一是从论文作者的GitHub仓库找Caffe版本,再转换;二是网上有人分享的PyTorch基础权重;三是自己用RESIDE数据集子集训练。我最推荐第三种,因为训练一个DehazeNet在我的显卡上大概只需要几个钟头,后面我会讲训练细节,而且自己训练出来的模型对数据分布更有感觉,出了问题也好排查。
不过,既然标题说了"附预训练模型",我也会在后续的小节里详细讲解权重文件的加载方法和验证方法,包括PyTorch 2.6版本里torch.load的weights_only参数这个新坑。
3. DehazeNet核心原理与网络结构解析
3.1 大气散射模型的数学推导
再回到那个公式:I(x) = J(x) * t(x) + A * (1 - t(x))。我们要从I推测J,就需要知道t和A。如果t和A都准确,那J的计算就是:
J(x) = (I(x) - A) / t(x) + A
这个公式里有一个非常隐蔽的坑:当t趋近于0的时候,除法会爆炸。所以在实际实现中,不能直接用原公式,需要加一个下界保护,把t限制在[t0, 1]范围内,t0通常取0.1。也就是说:
J(x) = (I(x) - A) / max(t(x), 0.1) + A
这样一来,即使某个区域的透射率被估计得接近0,也不会出现无穷大的像素值。这个下界保护在图像恢复中是常规操作,但如果你没做过,第一次写恢复代码时多半会漏掉。
还有一个关键点:大气光A是一个标量还是非均匀的?这里做了简化,把A看作全局常数,用同一个大A作用于所有像素。这样做的好处是计算简单,坏处是遇到远景和近景光照差异大的场景时,可能会出现局部偏色。不少论文做过自适应大气光的改进,但对基础版DehazeNet来说,全局A够用了。
3.2 网络结构四段式设计
DehazeNet的论文把网络分成了四个模块:特征提取、多尺度映射、局部极值、非线性回归。我来逐一拆解。
特征提取模块:输入是三通道的雾图,网络用3x3、5x5、7x7三种不同尺寸的卷积核并行提特征,每种卷积核输出16个通道,然后把三个分支的结果在通道方向上拼接,得到48通道的特征图。为什么要用多种尺寸的卷积核?因为雾的浓度在不同尺度上的表现是不一样的,小卷积核感受野小,适合捕捉细节纹理,大卷积核感受野大,适合捕捉整体的雾浓度变化。这种多尺度设计让网络能同时感知不同范围内的透射率变化。
多尺度映射模块:这个模块把48通道的特征图经过两层卷积降维到24通道,再接一个24通道到24通道的卷积层。这一部分的作用是对特征进行非线性变换和融合,让网络能表达更复杂的映射关系。原论文里的卷积层顺序和通道数我在这里按最常复现的版本给出,稍后代码里能看到。
局部极值模块:对特征图做步长为1的7x7最大池化。这个操作借鉴了暗通道先验的思路——局部区域的透射率变化是平滑的,取局部最大值可以让透射率图更平滑,同时也能抑制噪声的影响。池化窗口大小是个超参数,论文用的是7x7,你也可以尝试5x5或9x9,效果略有差别。
非线性回归模块:最后用一个1x1的卷积把24通道映射到1通道,得到透射率图。在输出之前,网络用了一个非常特殊的激活函数叫BReLU(双边ReLU)。ReLU是max(0, x),BReLU则是min(max(0, x), 1),把输出限制在0到1之间,这和透射率的物理定义完美匹配。这也是DehazeNet一个非常有代表的细节。
3.3 BReLU激活与损失函数
BReLU在PyTorch里实现起来非常简单,一个torch.clamp就搞定了。
import torch import torch.nn as nn class BReLU(nn.Module): def __init__(self, upper=1.0): super(BReLU, self).__init__() self.upper = upper def forward(self, x): return torch.clamp(x, 0, self.upper)有的复现版本把BReLU做成可学习的,上下界作为可训练参数,但原论文用的是固定边界,我也推荐固定边界,简单稳定。
训练时使用的损失函数是均方误差(MSE),直接计算模型输出的透射率图和真实透射率图的逐像素差异。为什么用MSE而不是其他损失?因为透射率图的恢复是一个回归任务,MSE对大的误差惩罚更重,能让网络把注意力集中在估计偏差大的区域,收敛也相对稳定。不过MSE也有个缺点,它倾向于把结果平均化,导致预测的透射率图偏平滑,这在细节恢复上有一定影响。如果想进一步改善,可以考虑在MSE基础上加一个结构相似性损失,但那是改进方向了,基础版用MSE就够。
训练数据的生成也是一个值得讲的主题。通常会模拟大气散射模型,用清晰图像和随机的大气光和透射率合成雾图。合成时透射率图一般不用常数,而是用随机深度渐变或者随机斑点来模拟真实场景的雾浓度变化。数据生成这一步直接决定了模型在真实雾图上的泛化能力,做得越好,模型越鲁棒。
4. 模型推理与图像恢复实现
4.1 透射率估计流程
推理阶段的第一个环节,就是把输入图片喂给DehazeNet,拿到透射率图。
在实际代码里,需要注意三点。第一,输入图片要归一化到0到1之间,因为网络训练时就是在0到1范围内进行的,如果直接输入0到255的像素值,模型输出的结果会偏移。第二,PyTorch模型的输入维度是(B, C, H, W),所以要先转成张量,扩张batch维度,再交给模型。第三,网络输出的是这个batch内的透射率图,需要经过squeeze去掉batch维才能做后续处理。
批处理的模板我放在后面的4.4节,这里先看单张图片的预测:
import cv2 import torch import numpy as np from models.dehazenet import DehazeNet def estimate_t(model, image): # image: HWC uint8 范围的BGR图像 img = image.astype(np.float32) / 255.0 img_tensor = torch.from_numpy(img).permute(2, 0, 1).unsqueeze(0) model.eval() with torch.no_grad(): t = model(img_tensor) t = t.squeeze().cpu().numpy() return t这样得到的t就是一张和输入图像同样尺寸的透射率灰度图。它的值基本在0到1之间,雾浓的地方值小,雾淡的地方接近1。如果想可视化,可以把t乘以255保存成灰度图看一眼,能直观感受到网络对场景深度的估计能力。
这里有一个隐藏的细节:如果输入图像的尺寸不是4的倍数 or 奇数,某些下采样层可能导致尺寸不匹配。DehazeNet没有传统意义上的下采样层,池化用的都是步长1的池化,所以理论上任何尺寸都能处理,但为了统一和稳妥,推理脚本里通常会先做一个尺寸归一化。我在项目里就统一把图片短边resize到512,这样既保证了速度,也避免了过小图片的边缘效应。
4.2 大气光计算策略
透射率有了,还差大气光A。大气光表示的是无穷远处雾的亮度,也就是天空区域的像素值。经典的做法是暗通道先验法:先计算图像的暗通道,然后在暗通道中取亮度最高的前0.1%像素,把这些像素位置对应到原图上,取它们的平均颜色作为大气光。
这里的逻辑是:暗通道中最亮的区域通常对应的是天空或者最远处的雾区,这些区域几乎没有纹理,亮度接近大气光。
我写的计算函数如下:
def estimate_atmospheric_light(image, dark_channel, top_percent=0.001): h, w = dark_channel.shape num_pixels = h * w top_count = max(int(num_pixels * top_percent), 1) flat_dark = dark_channel.flatten() indices = np.argpartition(flat_dark, -top_count)[-top_count:] top_pixels = image.reshape(-1, 3)[indices] A = np.mean(top_pixels, axis=0) return A这个函数里用到np.argpartition而不是直接argsort,是因为argsort是全排序,复杂度高,而argpartition只需要找出前top_count个,速度快很多。图像分辨率高时,这个性能差距会非常明显。
还有一点要提醒:如果图像整体都是雾蒙蒙的,没有明显的天空区域,这种取最亮像素的做法可能把前景里的白色物体误判成大气光,导致恢复结果偏暗。这个问题的鲁棒性改进方案有很多,比如基于位置加权的天空检测,但基础版就用经典暗通道法就行。
4.3 图像恢复与后处理
推理的最后一步,把大气光和透射率代入恢复公式,得到清晰图像。
def recover_image(image, t, A, t0=0.1): # image: 归一化到0-1的HWC浮点图像 # t: 归一化到0-1的透射率图 t = np.clip(t, t0, 1.0) t = np.expand_dims(t, axis=-1) # 扩展为H,W,1以便广播 A = A.reshape(1, 1, 3).astype(np.float32) / 255.0 J = (image.astype(np.float32) - A) / t + A J = np.clip(J, 0, 1) return J注意这里有几个细节:t做了下界截断,防止除零;A除以255是因为前面归一化到了0-1范围;最终结果clip到0-1。这样恢复出来的J就是0-1范围的浮点图像,需要乘255转回uint8才能保存显示。
其实做完这些,一张图的基本去雾流程就闭环了。但有一个常见的改进步骤值得做——在恢复之前对透射率图做一次导向滤波。DehazeNet直接输出的透射率图虽然整体准确,但在边缘处容易产生光晕伪影,因为网络对精细边缘的预测不够锐利。导向滤波能在保留边缘信息的同时平滑透射率图,显著改善恢复结果的自然度。
我用的导向滤波实现来自opencv的ximgproc模块,当然也可以用scipy自己写。核心思想是以原图的灰度图作为引导图,对透射率图进行滤波。参数里radius和eps需要调,我实测下来radius=40, eps=1e-3时效果比较均衡。
4.4 批量推理模板
实际项目中很少只处理一张图,通常是把一个文件夹里的图片批量去雾。我写的批量推理脚本如下:
import os import cv2 import torch from models.dehazenet import DehazeNet from utils.image_utils import load_image, save_image, guided_filter def batch_dehaze(model, input_dir, output_dir): os.makedirs(output_dir, exist_ok=True) files = [f for f in os.listdir(input_dir) if f.lower().endswith(('.jpg', '.jpeg', '.png', '.bmp'))] for idx, fname in enumerate(files): img_path = os.path.join(input_dir, fname) img = cv2.imread(img_path) if img is None: print(f"读取失败: {img_path}") continue # 预处理:归一化 img_norm = img.astype(np.float32) / 255.0 # 转张量推理 img_tensor = torch.from_numpy(img_norm).permute(2, 0, 1).unsqueeze(0) with torch.no_grad(): t = model(img_tensor).squeeze().cpu().numpy() # 大气光估计 A = estimate_atmospheric_light(img.astype(np.float32), dark_channel(img_norm)) # 可选:导向滤波 t_refined = guided_filter(img_norm, t, radius=40, eps=1e-3) # 图像恢复 J = recover_image(img_norm, t_refined, A) # 保存 out_path = os.path.join(output_dir, f'dehazed_{idx:03d}_{fname}') save_image(out_path, J) print(f'处理完成: {fname} -> {out_path}')这里的dark_channel是一个计算暗通道的函数,通常是取RGB三通道最小值然后用最小值滤波实现。批量处理时我习惯在循环外只加载一次模型,而不是每张图都重新加载权重,这样能大幅减少重复IO开销。
如果图片数量很多,可以考虑用torch.utils.data.DataLoader配合batch推理,把多张图拼成一个batch喂给模型,推理速度会成倍提升。我实测用batch=8时,GPU利用率显著提高,整体吞吐量提升大约4倍。当然,batch处理要处理不同尺寸图片的pad问题,会稍微复杂一些,这里就不展开了。
5. 预训练模型使用指南
5.1 权重加载与weights_only参数
标题里写了"附预训练模型",所以这一节必须讲透。拿到权重文件之后,加载方式决定了你能不能顺利跑起来。
PyTorch的权重加载最常见的方式是:
import torch from models.dehazenet import DehazeNet model = DehazeNet() state_dict = torch.load('pretrained/dehazenet.pth', map_location='cpu') model.load_state_dict(state_dict) model.eval()这段代码在PyTorch 2.5及以下版本没问题。但如果你装的是PyTorch 2.6或者更高版本,也就是最近一段时间开始普及的版本,运行时会跳出一行长长的提示,或者直接报错。原因是PyTorch 2.6改了torch.load的默认行为:weights_only默认值从False变成了True。
weights_only=True意味着只能加载张量、字典、列表这类基础数据类型,不允许加载任意的Python对象。这样做是为了安全,因为旧版的torch.load在加载恶意构造的pth文件时可能执行任意代码。但从用户角度,很多模型权重文件是用pickle序列化的,里面会包含一些自定义对象,加载时就容易出问题。
如果你遇到pickle.UnpicklingError或者类似Weights only load failed的错误,有两种解决办法。第一种,加载时显式指定weights_only=False:
state_dict = torch.load('pretrained/dehazenet.pth', map_location='cpu', weights_only=False)第二种,如果你确保权重文件里只包含张量,可以先把文件转化为纯state_dict格式,用weights_only=True加载。我推荐第一种,简单直接,自己本地的模型文件安全性可以自己把控。
还有一点,用load_state_dict时如果遇到unexpected key或missing key的报错,最常见的原因有几种:一是网络的层名和权重文件里存的层名不一致;二是model定义里多了或少了层;三是保存时带上了module.前缀。第三个情况在用了DataParallel训练时最常见,处理方式是对key做字符串替换:
state_dict = {k.replace('module.', ''): v for k, v in state_dict.items()}5.2 自定义数据集评估
加载好模型后,最好先在公开数据集或者自己拍摄的雾图上看一下效果,而不是直接扔到业务场景里。我常用的评估方式有两种,一种是主观看图和客观指标结合,另一种是只有雾图没有参考图的情况下用无参考指标。
有参考图时,常用的指标是PSNR(峰值信噪比)和SSIM(结构相似性)。PSNR越高表示像素级误差越小,SSIM越接近1表示结构保真度越好。但这两个指标对去雾任务来说并不完美,因为去雾本来就是一种风格迁移式的变换,数值上非常接近的图可能视觉上不如某些数值略低的图自然。所以我的习惯是:指标作为参考,最终以目视效果为准。
无参考图时,可以用一些雾密度评估方法,比如对比度、饱和度等简单统计特征。虽然不够严谨,但至少能对比出处理前后的变化趋势。
在自定义数据集上评估时,还容易遇到一个适配问题:训练时用的图像尺寸和预处理方式与你的测试图不一致。如果预训练模型是在固定尺寸或某一种数据分布上训练的,直接扔一张不同分布的图片进去,效果可能大打折扣。所以评估之前,一定要确认数据的归一化方式、色彩空间(BGR还是RGB)和模型训练时完全一致。我在项目里就吃过这个亏,用OpenCV读的图默认是BGR,但训练时如果用RGB,出来的透射率图会明显不对,整个去雾结果是花的。
5.3 微调与再训练建议
预训练模型在通用场景下效果不错,但如果你的图片场景很特殊,比如全是水下图像、浓雾天的高速公路、或者监控摄像头视角,那通用权重可能不够理想。这时候建议在你自己收集的数据上做微调。
微调的流程和正常训练类似,但有几个关键点。第一,学习率要调低,通常从预训练模型的最后收敛学习率往下调,比如1e-4或者更小,否则很容易破坏已经学好的底层特征。第二,冻结前几层卷积,只更新后面的层,这种做法在小数据集上特别有效,可以防止过拟合,也能显著加速训练。第三,数据扩充非常重要,随机裁剪、水平翻转、颜色抖动这些操作能提升模型对场景的泛化能力。
训练自己的DehazeNet是一个更完整的工程,需要准备成对的雾图和清晰图。合成数据是常规做法:从COCO、ADE20K这些数据集里选清晰图像,然后随机生成大气光和深度图,通过大气散射模型合成雾图。这一步要在数据加载时在线完成,否则磁盘会被撑爆。
如果你条件允许,强烈建议自己训一次。即使最终效果不如预训练模型,这个过程能让你对数据分布、收敛速度、损失曲线这些概念有切身体会。
6. 常见问题与踩坑实录
6.1 环境与安装问题排查
把这段时间做项目时遇到的高频问题整理成了一张速查表,希望能帮你少走弯路:
| 问题现象 | 可能原因 | 解决方案 |
|---|---|---|
| torch.cuda.is_available() 返回False | CUDA版本不匹配 / 驱动过旧 | 升级驱动,重新安装对应CUDA版本的PyTorch |
| 安装PyTorch时提示找不到合适的版本 | pip源网络问题 | 换国内镜像,或从官网下载whl文件手动安装 |
| conda create速度极慢 | 默认channel访问慢 | 配置清华/阿里conda镜像源 |
| import torch报错缺少DLL | 缺少VS运行库 | 安装Visual C++ Redistributable |
| 内存不足/显存溢出 | 图像尺寸太大 / batch过大 | 减小输入尺寸,或降低batch size |
其中最常见也最让人烦躁的就是CUDA版本问题了。建议先运行nvidia-smi看驱动支持的CUDA版本,然后安装的PyTorch CUDA版本只要不高于这个就能用。比如驱动支持CUDA 12.1,那装cu118或cu121的PyTorch都没问题。
6.2 模型加载与推理问题
加载权重时最容易遇到的就是前面讲的weights_only报错,以及state_dict的key不匹配。这里再补充几个我在实践中的其他问题:
一是模型输出全黑或全白。这通常意味着输入没有归一化。很多人直接用cv2.imread读进来的0到255的值喂给网络,DehazeNet输出的透射率经过BReLU之后被clamp到0到1之间,但中间的特征计算会爆炸,导致输出极端值。解决办法就是老老实实把输入除以255。
二是恢复出来的图色彩很怪。BGR和RGB通道顺序弄反是头号嫌疑。我在代码里统一用OpenCV的BGR顺序处理,网上的某些实现用的是RGB,如果你的模型是在某份RGB预处理下训练的,直接用BGR推理就会偏色。稳妥的做法是训练和推理都保持一种约定,并且在遇到色彩问题的时候第一个怀疑这个环节。
三是恢复结果有明显的块状光晕。透射率图在边缘处不够锐利导致的。先用导向滤波细化透射率,基本上能解决。如果还有光晕,可以适当增大导向滤波的radius,或者对透射率图做一次中值滤波。
6.3 去雾效果不佳的原因分析
如果模型部署好之后,去雾效果不理想,不要急着怀疑模型有问题,先按下面这个顺序排查:
输入图片本身是否适合当前模型?DehazeNet是在自然雾场景上训练的,如果输入的是夜间图像、低照度图像、或者是带了大量人造光源的室内场景,效果就会很差。这是物理模型本身的限制,不是代码bug。
透射率估计是否合理?把模型输出的透射率图打印出来,用灰度图形式查看。如果发现大部分区域透射率接近1,说明模型认为这张图几乎没有雾,那恢复效果当然不明显。如果透射率有高有低,但恢复出来的图仍然偏灰,就要检查大气光的估计是否准确。
大气光取值是否太大或太小?把估计出来的A打印出来,一般应该接近图像中最亮区域的像素值。如果A偏大了,恢复出的图会整体偏暗;A偏小了,则会出现灰蒙蒙的残留。
后处理中是否做了色彩校正?有些图片恢复之后对比度提升了,但看起来不自然,此时可以适当做一次色温平衡或者直方图裁剪,这个不在DehazeNet的范围内,但作为工程实践是常见的后续操作。
6.4 性能优化与部署心得
最后聊一下在实际部署中总结的几个优化经验。
CPU推理优化:DehazeNet本身很轻量,CPU上处理一张512x512的图耗时大概在几十毫秒到几百毫秒之间,取决于具体的硬件。如果还想提速,可以尝试把模型导出成ONNX再用ONNX Runtime推理,或者用OpenVINO在Intel平台上加速,速度提升非常明显。
GPU批量推理:用DataLoader做batch推理,吞吐量提升是接近线性的。瓶颈往往不在模型本身,而在图像的预处理和IO。建议把解码、resize、归一化这些操作放到多进程的DataLoader里,让GPU计算和CPU预取重叠起来。
内存管理:如果一次要处理整个文件夹的图片,千万不要先把所有图片读进内存,再统一处理。正确做法是逐张读取、逐张处理、逐张保存。我见过有人做视频去雾时,把某几帧图片直接读成一个巨大的numpy数组,结果内存直接爆掉。逐帧处理虽然慢一点,但稳,而且更容易在中间加可视化。
模型导出部署:如果只是离线处理图片,用PyTorch原生的torch.no_grad()就够。如果要集成到Web服务里,建议先把模型转成TorchScript或者ONNX,这样可以减少运行时对Python版本的依赖,也能享受到更高效的执行引擎。
我在实际使用中还有一个体会,就是把DehazeNet的输出结果t作为深度信息的近似值,还能用来做其他后处理,比如雾浓度检测和图像增强的权重图。这个思路虽然超出了DehazeNet原本的范畴,但说明透射率图本身就是一个很有信息量的中间产物。如果你正在做安防监控、自动驾驶或者航拍图像处理,不妨从这个角度尝试一下,说不定会有意外的收获。
本文还有配套的精品资源,点击获取