ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

图像去雾实战:基于PyTorch的轻量级端到端去雾模型搭建

图像去雾实战:基于PyTorch的轻量级端到端去雾模型搭建 如果你手里有一张雾天拍摄的街景照片第一反应很可能是打开修图软件拉高对比度降低灰雾感。但对真正需要处理海量图像的工程场景来说这种“手动修图”思路几乎不可复用。雾天图像的问题不只是“看起来模糊”它背后有一套明确的物理退化过程场景反射光在传播时被大气散射叠加了一层环境光最终导致图像对比度下降、颜色偏灰。《雾里》这个项目名天然让人想到“雾里看花”的状态。把它落到技术层面最合适的切入点就是图像去雾。本文将围绕一个基于 PyTorch 的轻量级端到端去雾实战项目来展开从物理模型讲起完成合成雾图、构建去雾网络、训练、推理和效果验证最终给你一套可以直接改用的代码骨架。读完这篇文章你能得到三样东西第一理解雾天成像的物理模型和去雾的基本原理第二跑通一个不算复杂的去雾模型从数据准备到推理验证第三知道这类项目在实际工程中哪里容易翻车以及怎么避坑。1. 为什么要做图像去雾真实工程里的刚需图像去雾听起来是个偏学术的研究方向但它在真实工程里出现频率很高。安防监控在雾天看不清车牌自动驾驶的车载摄像头在雨雾天气识别不准行人和障碍物无人机巡检在低能见度条件下难以辨别设备状态遥感影像也经常因为云层和薄雾影响判读效果。在这些场景中图像去雾通常不是最终目标而是整个视觉链路里的前置步骤。也就是说先去雾、再做目标检测、分类或分割。因此去雾模型需要具备几个工程属性推理速度要快显存占用不能太高输出图像要稳定而不引入明显伪影。这也决定了我们在选择模型结构时不能盲目追求深度网络。《雾里》项目的定位就是一个轻量级去雾实验工程。它不需要动辄数百层的骨干网络而是用一套相对简洁的端到端结构在保证可训练、可部署的前提下把图像去雾的完整流程走通。从项目实践角度本文重点解决下面几个问题雾天图像的退化过程如何建模传统去雾方法和深度学习去雾方法的本质差别在哪里如何自己合成带雾图像解决训练数据不足的问题一个轻量级网络如何完成图像到图像的映射训练完成后如何用客观指标验证效果。如果你正在做图像增强、目标识别相关的项目并且每次拿到雾天图片都靠 OpenCV 的对比度调整凑合那么这篇文章给出的思路会更值得参考。2. 基础概念雾天成像物理模型与去雾原理2.1 大气散射模型在图像去雾领域最常用的是大气散射模型。它的表达式很简洁I(x) J(x) * t(x) A * (1 - t(x))其中I(x) 是观测到的带雾图像J(x) 是干净场景辐射也就是我们希望恢复的无雾图像t(x) 是透射率表示场景反射光经过介质后到达相机的比例A 是全局大气光通常认为是天空区域或远端环境光的颜色。当透射率 t(x) 接近 1 时图像几乎不受雾影响当 t(x) 接近 0 时图像几乎完全被雾覆盖场景信息极度衰减。这个模型的工程意义在于它把去雾问题拆成了两部分估计透射率 t(x)以及估计大气光 A。只要能拿到这两个量理论上就能反推出 J(x)。2.2 暗通道先验的核心思想在深度学习没有大规模流行之前最具代表性的传统方法是何恺明等人提出的暗通道先验。它的观察基于一个统计规律在晴天的无雾图像中绝大多数局部区域里的某个颜色通道会存在很低的像素值这个通道被称为暗通道。带雾图像因为叠加了大气光暗通道的像素值会被抬高雾越浓暗通道越亮。所以只要统计图像局部区域的暗通道就可以反推透射率 t(x)再结合大气光估计就能还原出清晰图像。这套方法的优点是原理清晰、不需要训练数据但问题也很明显当场景本身包含大片天空或白色物体时暗通道先验失效透射率估计之后通常需要 soft matting 等后处理速度慢处理结果容易在边缘区域出现光晕伪影对光照复杂、雾浓度不规律的图像稳定性一般。因此工程上越来越多的方案转向深度学习。2.3 深度学习去雾的两条路线深度学习去雾大致分为两条路线。第一条是端到端映射路线。直接把带雾图像输入网络输出清晰图像网络内部自动完成透射率、大气光的隐式估计。AOD-Net 是这类方法的代表之一它的特点是结构简单、参数量小特别适合教学和工程快速验证。第二条是生成对抗路线。用生成器做去雾用判别器判断输出是否接近真实无雾图像。代表方法有基于 CycleGAN 的无监督去雾思路。它的优势是不需要严格成对的训练数据但训练不稳定且输出质量波动较大。对《雾里》这个项目来说更适合采用第一条路线。原因是它更容易控制训练过程依赖更少部署成本也低。3. 传统方法与深度学习方案的对比在开始实战之前值得把传统去雾和深度学习去雾放在一起做个比较。对比维度暗通道先验深度学习端到端是否需要训练数据不需要需要成对或非成对数据原理可解释性强基于物理统计中等隐式学习映射天空区域处理容易失效训练数据覆盖后可改善推理速度偏慢需要后处理快单次前向传播工程部署难度低需要模型转换和推理框架泛化能力依赖先验假设依赖训练数据分布需要强调的是深度学习方案并不是在所有场景都强于传统算法。如果你只有零星几张图片需要处理不想引入模型训练和 GPU 资源直接用暗通道先验反而更合适。但如果你要处理大量来自监控摄像头或车载设备的图像并且需要把去雾模块嵌入到自动化流水线里端到端深度学习是更现实的选择。《雾里》项目选择深度学习路线核心是希望读者理解一个完整的工程链路数据生产、模型训练、效果评估、问题排查。这些能力是只懂 OpenCV 函数调用解决不了的。4. 环境准备与项目结构4.1 运行环境与依赖安装图像去雾模型的训练需要 Python 和深度学习框架。推荐环境如下操作系统Windows、Linux 均可Linux 更利于长期训练Python3.9 或更高版本深度学习框架PyTorch建议选择当前稳定版本具体以实际环境为准图像处理库OpenCV、NumPy可视化库Matplotlib用于查看去雾效果。首先创建一个虚拟环境然后安装依赖python -m venv wuli_env # Windows wuli_env\Scripts\activate # Linux / macOS source wuli_env/bin/activate pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu118如果电脑没有 NVIDIA GPUCPU 版本也可以跑通流程只是训练时间会明显变长。建议先用几十张小图验证流程再根据实际算力扩大数据规模。随后安装图像处理相关依赖pip install opencv-python numpy matplotlib安装完成后输入以下命令确认环境正常python -c import torch; print(torch.__version__); import cv2; print(cv2.__version__)4.2 项目目录结构整个项目按功能拆分为几个模块wuli/ ├── data/ │ ├── clear/ # 清晰图像用于合成带雾图像 │ └── test/ │ └── foggy.jpg # 测试用带雾图像 ├── checkpoints/ # 模型权重保存目录 ├── fog_synthesis.py # 雾图合成模块 ├── model.py # 去雾网络结构 ├── train.py # 训练脚本 ├── infer.py # 推理脚本 └── evaluate.py # 指标评估脚本在开始写代码之前需要准备一批清晰图像作为训练数据的底图。可以从公开数据集中取一部分自然图像也可以使用自己拍摄的照片。需要注意版权合规问题不要使用来源不明的图像做大规模训练和展示。5. 合成雾图用大气散射模型造训练数据5.1 为什么需要合成数据深度学习去雾需要成对的训练样本也就是“带雾图像 对应清晰图像”。但真实场景很难同时拍到完全相同的画面一张有雾、一张没雾而且还要保证场景静止、光照一致。所以主流的做法是用物理模型在清晰图像上合成雾。合成过程分为三步生成一张与图像尺寸相同的深度图选择全局大气光 A用大气散射模型生成带雾图像。深度图的生成方法有很多。最理想的是使用深度相机采集的真实深度但普通项目没有这个条件。工程上常用随机噪声平滑生成近似深度然后叠加在图像上。这样虽然不够物理严谨但足以训练一个端到端模型。5.2 雾图合成代码下面是 fog_synthesis.py 的完整实现# 文件路径fog_synthesis.py import cv2 import numpy as np def generate_depth_map(shape, seedNone): 生成一张平滑的随机深度图模拟场景中的远近分布 if seed is not None: np.random.seed(seed) h, w shape[:2] # 构造基本渐变模拟近处亮、远处暗的深度分布 ramp_x np.linspace(0, 1, w, dtypenp.float32) ramp_y np.linspace(0, 1, h, dtypenp.float32) grid_x, grid_y np.meshgrid(ramp_x, ramp_y) depth grid_x * 0.3 grid_y * 0.5 # 添加随机噪声再平滑让深度图看起来自然 noise np.random.randn(h, w).astype(np.float32) * 0.1 depth depth noise depth cv2.GaussianBlur(depth, (0, 0), sigmaX5) # 归一化到 [0, 1] depth (depth - depth.min()) / (depth.max() - depth.min() 1e-6) return depth def synthesize_fog(image, beta1.0, ANone): 根据大气散射模型合成带雾图像 I J * t A * (1 - t) image image.astype(np.float32) / 255.0 if A is None: A np.array([0.9, 0.9, 0.9], dtypenp.float32) # 让大气光在合理范围内随机波动提升数据多样性 A A np.random.uniform(-0.1, 0.1, 3).astype(np.float32) A np.clip(A, 0.7, 1.0) depth generate_depth_map(image.shape) t np.exp(-beta * depth).astype(np.float32) t np.clip(t, 0.1, 1.0) fogged image * t[..., np.newaxis] A * (1 - t[..., np.newaxis]) fogged np.clip(fogged, 0, 1) return (fogged * 255).astype(np.uint8), t, A这段代码的核心逻辑是把清晰图像按照深度图进行衰减再加上大气光。这里需要注意两个细节。第一透射率 t 被限制在 0.1 到 1 之间。如果 t 接近 0原始场景信息几乎损失殆尽模型不仅无法学习还会因为目标图像过暗而产生不稳定的梯度。第二大气光 A 被设置为接近白色光的取值并允许在 0.7 到 1.0 之间波动。这样模型能够适应轻度色偏的雾图。如果你使用真实雾图数据需要在合成时考虑真实场景的大气光范围。5.3 使用合成函数生成示例合成代码可以直接在 Python 交互环境中验证import cv2 from fog_synthesis import synthesize_fog clear_img cv2.imread(data/clear/001.jpg) foggy_img, t, A synthesize_fog(clear_img, beta1.0) cv2.imwrite(data/test/foggy_demo.jpg, foggy_img)每次调用时beta 和 A 都可以随机变化生成不同浓度的雾图。训练时建议在数据集类中随机采样而不是提前生成固定的一批雾图这样同一个清晰图像可以在训练时被多次合成为不同雾浓度的样本相当于做了数据增强。6. 轻量级去雾模型AOD-Net 思路实现6.1 模型的整体想法AOD-Net 的核心贡献是提出了一种简单但有效的做法不直接分开估计透射率和大气光而是学习一个能整合两者的参数 K(x)再通过公式恢复无雾图像。本文实现的是一个借鉴 AOD-Net 思想的教学简化版。它使用少量卷积层输入带雾图像输出一个单通道参数图 K然后通过如下方式恢复清晰图像J(x) K(x) * I(x) - K(x) 1其中常数 1 可以理解为大气光相关的偏移量。虽然这个实现不会严格复现论文里的每一层结构但已经足够作为入门项目来理解端到端去雾的原理。6.2 模型代码下面是 model.py 的代码# 文件路径model.py import torch import torch.nn as nn class AODNetSimplified(nn.Module): 借鉴 AOD-Net 思想的简化端到端去雾模型 输入带雾图像 [B, 3, H, W] 输出去雾图像 [B, 3, H, W] def __init__(self): super(AODNetSimplified, self).__init__() self.conv1 nn.Conv2d(3, 3, kernel_size5, padding2) self.conv2 nn.Conv2d(6, 3, kernel_size3, padding1) self.conv3 nn.Conv2d(6, 1, kernel_size3, padding1) self.relu nn.ReLU(inplaceTrue) def forward(self, x): # 生成第一层特征 x1 self.relu(self.conv1(x)) # 与原始输入拼接保留细节信息 concat1 torch.cat([x1, x], dim1) x2 self.relu(self.conv2(concat1)) # 再次与原始输入拼接避免信息丢失 concat2 torch.cat([x2, x], dim1) k torch.sigmoid(self.conv3(concat2)) # 使用估计出的 K 完成去雾映射 b torch.tensor(1.0, devicex.device) j k * x - k b return j这段代码的关键点有三个。第一网络输出被 sigmoid 限制到了 0 到 1 之间这对应参数图 K 的物理范围。Sigmoid 能让训练初期参数变化更平滑避免输出出现极端值。第二模型两次把原始输入拼接进特征图。图像到图像的映射任务中低层细节很难完全恢复直接在中间层保留原图信息可以降低训练难度。第三恢复公式非常轻量没有复杂的后处理模块因此推理速度极快适合后续部署到 CPU 或边缘设备。6.3 为什么不选择深层网络如果使用标准的 U-Net 或 ResNet 作为骨干去雾效果可能在指标上更好但训练周期长、显存占用高且对小数据集很容易过拟合。这个项目的目标是提供一套快速可复现的流程因此模型结构越简单越好。在实际工程中你会先跑通一个轻量模型的完整链路确认数据、训练、评估都没有问题再逐步替换成更复杂的网络。这是一种更稳妥的推进方式。7. 训练脚本与损失函数设计7.1 数据加载器训练时数据加载器需要完成三个操作读取清晰图像、随机合成雾图、转换为 PyTorch Tensor。下面是 train.py 中数据集的实现# 文件路径train.py import os import cv2 import numpy as np import torch from torch.utils.data import Dataset, DataLoader from fog_synthesis import synthesize_fog class FoggyDataset(Dataset): def __init__(self, image_dir, size(256, 256)): self.paths [ os.path.join(image_dir, f) for f in os.listdir(image_dir) if f.lower().endswith((.jpg, .png, .jpeg)) ] self.size size def __len__(self): return len(self.paths) def __getitem__(self, idx): path self.paths[idx] image cv2.imread(path) if image is None: image np.zeros((self.size[1], self.size[0], 3), dtypenp.uint8) image cv2.cvtColor(image, cv2.COLOR_BGR2RGB) image cv2.resize(image, self.size) # 随机选取雾浓度增强模型对不同雾情的适应能力 beta np.random.uniform(0.5, 1.5) fogged, _, _ synthesize_fog(image, betabeta) clear_tensor torch.from_numpy( image.astype(np.float32) / 255.0 ).permute(2, 0, 1) fogged_tensor torch.from_numpy( fogged.astype(np.float32) / 255.0 ).permute(2, 0, 1) return fogged_tensor, clear_tensor这里特意把 beta 控制在 0.5 到 1.5 之间。beta 太小雾太淡模型学不到明显提升beta 太大图像几乎被白色覆盖训练不稳定。7.2 训练主循环训练脚本相对简单使用 MSE 损失和 Adam 优化器。# 文件路径train.py from model import AODNetSimplified def main(): device torch.device(cuda if torch.cuda.is_available() else cpu) print(train device:, device) train_dir data/clear batch_size 8 epochs 20 lr 1e-3 dataset FoggyDataset(train_dir, size(256, 256)) dataloader DataLoader(dataset, batch_sizebatch_size, shuffleTrue) model AODNetSimplified().to(device) optimizer torch.optim.Adam(model.parameters(), lrlr) criterion torch.nn.MSELoss() model.train() for epoch in range(epochs): total_loss 0.0 batch_count 0 for fogged, clear in dataloader: fogged fogged.to(device) clear clear.to(device) out model(fogged) loss criterion(out, clear) optimizer.zero_grad() loss.backward() optimizer.step() total_loss loss.item() batch_count 1 avg_loss total_loss / batch_count print(fepoch {epoch 1}/{epochs}, loss{avg_loss:.4f}) torch.save(model.state_dict(), fcheckpoints/aodnet_{epoch 1}.pth) print(training finished) if __name__ __main__: main()7.3 损失函数选型的经验MSE 是图像到图像任务中最容易上手的损失函数但它会对所有像素一视同仁容易导致输出图像偏平滑。若后续要追求更清晰锐利的去雾效果可以在 MSE 的基础上叠加感知损失或 SSIM 损失。这里不引入额外依赖先用 MSE 跑通流程。当模型能够稳定降低 loss 并恢复出清晰轮廓时再考虑替换损失函数会更清楚每一步改动带来的影响。7.4 训练启动命令在项目根目录执行python train.py如果没有报错你会看到类似下面的输出train device: cuda epoch 1/20, loss0.0231 epoch 2/20, loss0.0175 ...随着训练进行loss 应当逐步下降。当 loss 下降到 0.01 以下时说明模型已经开始逼近清晰图像。这里需要提醒loss 数值只反映训练集上的拟合程度最终效果必须通过推理验证来判断。8. 推理与效果验证8.1 推理脚本训练完成后需要一个独立的推理脚本对单张雾图执行去雾并把结果保存下来。# 文件路径infer.py import cv2 import numpy as np import torch from model import AODNetSimplified def load_model(model_path, device): model AODNetSimplified().to(device) model.load_state_dict(torch.load(model_path, map_locationdevice)) model.eval() return model def dehaze_image(model, image_path, device): img cv2.imread(image_path) img_rgb cv2.cvtColor(img, cv2.COLOR_BGR2RGB) input_tensor ( torch.from_numpy(img_rgb.astype(np.float32) / 255.0) .permute(2, 0, 1) .unsqueeze(0) .to(device) ) with torch.no_grad(): out model(input_tensor) out_np out.squeeze(0).permute(1, 2, 0).cpu().numpy() out_np np.clip(out_np, 0, 1) out_bgr cv2.cvtColor((out_np * 255).astype(np.uint8), cv2.COLOR_RGB2BGR) return img, out_bgr if __name__ __main__: device torch.device(cuda if torch.cuda.is_available() else cpu) model load_model(checkpoints/aodnet_20.pth, device) haze_img, clear_img dehaze_image(model, data/test/foggy.jpg, device) cv2.imwrite(data/test/result.jpg, np.hstack([haze_img, clear_img])) print(result saved: data/test/result.jpg)推理时的关键点是加torch.no_grad()关闭梯度计算。这样能显著减少显存占用和推理时间。输出结果在转回 NumPy 数组后要做一次np.clip防止模型输出越界导致图像出现纯黑或纯白区域。8.2 客观指标PSNR 与 SSIM对合成雾图来说原清晰图像就是参考图可以计算 PSNR 和 SSIM。PSNR 衡量像素级误差数值越高越好SSIM 衡量结构相似性越接近 1 越好。下面是一个轻量评估脚本# 文件路径evaluate.py import cv2 import numpy as np def psnr(img1, img2): mse np.mean((img1.astype(np.float32) - img2.astype(np.float32)) ** 2) if mse 0: return float(inf) return 10 * np.log10(255.0 ** 2 / mse) def main(): clear_img cv2.imread(data/test/clear_reference.jpg) result_img cv2.imread(data/test/result.jpg) h, w clear_img.shape[:2] result_img cv2.resize(result_img, (w, h)) score psnr(clear_img, result_img) print(fPSNR: {score:.2f} dB) if __name__ __main__: main()从一般项目经验看合成雾图测试 PSNR 在 20 dB 到 25 dB 左右属于可接受区间。这个数值取决于雾的浓度、场景复杂度以及模型是否收敛。真实雾图因为没有参考图无法计算 PSNR只能通过人眼判断对比度、颜色和细节恢复程度。8.3 如何判断模型是否成功判断模型效果不要只看 loss 数字。建议分三层第一层视觉主观判断。去雾后的图像是否明显更清晰边缘是否自然天空是否有过曝色彩是否严重偏离原始场景。第二层客观指标。对合成数据计算 PSNR 和 SSIM确保模型在数值上有正向提升。第三层下游任务验证。如果项目目标是接入目标检测或车牌识别需要把去雾结果送入下游任务观察准确率是否提升。这比单独看 PSNR 更有说服力。9. 常见问题与排查思路下表汇总了图像去雾项目中比较常见的问题和排查方式。问题现象可能原因排查方式解决方案训练 loss 不下降学习率不合适或数据量太小打印每个 batch 的 loss 变化调整学习率增加图像数量推理结果明显偏暗模型未收敛或输出没有 clip查看训练日志和输出像素范围训练更久推理时加 np.clip图像颜色偏色严重合成数据大气光设置与真实场景偏差大检查合成雾图颜色分布扩大大气光随机范围或加入真实雾图微调显存不足batch_size 或图像尺寸过大查看显存占用降低 batch_size或把输入缩到 224x224训练完成后输出是纯灰色归一化或维度反转错误检查 Tensor 的 shape 和取值范围确保 permute、除以 255 正确真实雾图效果很差训练数据是合成雾分布差异大用真实雾图做定性测试使用真实雾图数据做域适应或微调边缘出现光晕伪影深度图太平滑模型学到模糊映射查看去雾结果的边缘合成数据中增加更多深度细节排错时最忌讳直接改代码建议按顺序检查数据是否正常、模型输入输出维度、训练 loss 变化、推理结果像素范围。实际上大多数新手问题都出在维度或归一化上。10. 最佳实践与工程建议10.1 数据层面的建议合成雾是训练数据的主体但不能只依赖一种随机深度图生成方式。建议在不同深度分布、大气光颜色、雾浓度下分别采样。你也可以把合成之后的雾图再做一次轻度高斯模糊或增加噪声提升模型对真实传感器噪声的鲁棒性。训练集图像质量很重要。如果原始清晰图像本身就模糊、曝光不足合成出来的雾图也会带有缺陷模型会把这些缺陷当成正常特征学习。10.2 训练策略建议项目初始阶段先用 50 到 100 张小图跑通流程确认数据加载、模型前向、反向传播都没有问题再扩大数据集。如果一开始就塞入几千张图片出现问题后很难定位是数据问题还是模型问题。训练结束后建议多保存几个 epoch 的权重而不是只保留最后一轮。有时候最后一个 epoch 可能出现过拟合倒数几个 epoch 的权重效果反而更好。10.3 模型部署与升级建议轻量模型训练完成后可以考虑导出为 ONNX 格式用 ONNX Runtime 部署到 CPU 上降低对 GPU 的依赖。如果后续要追求更好的效果可以在网络后面增加更深的骨干结构但要注意推理速度的取舍。另外去雾模型不是万能的它不能替代目标检测或图像分类模型。在安防、自动驾驶等对安全要求较高的场景中去雾后的结果仍需要人工复核或与原始图像一起保留避免因去雾处理引入错误信息。10.4 代码工程规范项目中涉及的所有随机操作例如深度图生成、雾浓度采样都应该在构建数据和训练测试时固定随机种子保证实验可复现。文件名和目录结构要清晰训练脚本与推理脚本分离。这些习惯对于长期维护项目非常重要。11. 总结与后续方向《雾里》作为一个图像去雾实战项目已经走通了从物理建模到模型部署验证的完整链路。你现在可以基于这套代码准备一批清晰图像合成雾图训练一个简化的端到端模型然后在自己的测试图上验证效果。后续可以往三个方向深入第一改进网络结构。把简化模型替换为 U-Net 或注意力机制网络观察去雾效果的提升幅度。第二改进损失函数。在 MSE 基础上叠加感知损失或 SSIM 损失让输出图像更清晰自然。第三解决真实场景分布差异。使用少量真实雾图做微调或者尝试无监督去雾方案解决合成数据泛化不足的问题。在实际项目中图像去雾通常只是第一步。下一步你可以把去雾结果接入人脸检测、目标跟踪或图像分类流程用下游任务的指标来衡量去雾模块是否真的发挥作用。这套实验框架建议保存下来后面的几个方向都可以在此基础上快速迭代。
返回列表