ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

图像去雾模型核心技术解析:纹理恢复与主观质量优化

图像去雾模型核心技术解析:纹理恢复与主观质量优化 简介这是一份面向图像去雾任务的人工智能与机器学习课程期末作业作者基于大气散射模型引入物理约束项同时采用SE通道注意力、残差结构、多尺度特征提取与非局部注意力机制并结合预训练VGG16感知损失、数据增强和随机Dropout训练策略能明显改善纹理细节恢复和主观视觉质量。压缩包内共41个文件主要包括Python源码训练/测试脚本、pth模型权重、png可视化结果以及README说明整体仅14.3MB目录按模型定义、训练数据、结果输出等模块组织易于查阅。资源目前已有64人学习下载。学习者可以获得完整的去雾模型实现与训练细节既能用于课程项目复现与答辩展示也可作为后续开展图像增强、物理引导深度学习研究的参考基线。1. 去雾模型为什么把纹理恢复和主观质量当作核心指标图像去雾这个任务如果只看 PSNR 和 SSIM往往会得出一个反直觉的结论客观指标刷得很高视觉效果却仍然“发闷”——雾是去了但纹理像被磨皮一样糊掉了边缘发虚、细节纹理丢失观感上甚至不如带一点薄雾的原图。这正是去雾模型区别于超分辨率、去噪任务的地方去雾的难点不在“把雾移除”而在“移除雾的同时把被散射削弱的纹理重新立起来”。第六组期末作业这个题目把“纹理细节恢复”和“主观视觉质量”并列提出实际上是在强调去雾模型的评估标准已经从像素误差的数值游戏转向了人类视觉系统真正感知到的质量。背后的原因不难理解。大气散射模型把带雾图像拆解为场景辐射和大气光两部分但真实的雾天环境远比这个模型复杂——不均匀的雾浓度、大气光的空间变化、传感器噪声都会让模型在恢复时产生歧义。模型为了压低 L2 损失倾向于生成平滑的平均解这就是纹理丢失的根源。本文按照“物理模型 → 网络结构 → 损失函数 → 训练与评估”这条线展开把我做去雾项目时验证过的一套方案完整梳理出来代码和参数都可以直接借鉴。2. 去雾模型的物理基础与两类技术路线的取舍2.1 大气散射模型去雾问题的数学起点去雾问题能被建模几乎都绕不开大气散射方程。它的退化表达式为I(x) J(x) t(x) A (1 - t(x))其中I(x)是观测到的带雾图像J(x)是待恢复的清晰场景辐射t(x)是介质传输率取值范围 0 到 1A是全局大气光。重写一下就是J(x) \frac{I(x) - A}{t(x)} A这个式子清晰地揭示了病态性已知I(x)但t(x)和A都是未知量一个方程两个未知数必须附加先验或约束才能求解。传统算法中的暗通道先验Dark Channel Prior就是人工设定的约束——统计发现户外无雾图像的局部块中至少一个颜色通道的强度趋近于零。但这个先验在天空区域、白色物体和强光源区域不成立处理不好就会出现色偏和伪影纹理细节自然无从谈起。深度学习路线绕开了显式先验的局限。网络端到端地从数据中学习I(x)到J(x)的映射不再依赖手工先验潜在的好处就是模型能隐式学习纹理结构的分布规律。常见做法是让网络直接回归J(x)不单独预测t(x)和A因为级联估计误差会逐级放大最终影响纹理重现的准确性。2.2 为什么端到端回归比先估计传输率再复原更稳早期基于 CNN 的去雾方案延续了传统思路第一步用网络预测传输率图t(x)第二步用大气散射公式做像素级还原。但这里有一个工程上很棘手的累积误差问题——传输率估计的一个小偏差在J(x)的计算中会因为除法而被放大尤其在t(x)趋近于零的区域数值不稳定直接导致纹理拉花。业内更倾向的做法是采用一体化网络。以 AOD-Net 为代表的结构通过变量代换把大气散射公式合并进网络前向计算中网络只输出一个中间结果K(x)# K(x) 的构造方式参考 AOD-Net 思路的简化实现 def reformulation(input_image, t_est, a_est): # t_est: 估计的传输率图, a_est: 估计的大气光 k (1.0 - t_est) / t_est # 重新参数化融合 t 和 A 的影响 # 将带雾图与 K 组合直接输出去雾结果 return k * input_image - k * a_est a_est这种重参数化的优点很实际网络不再需要高精度地单独预测t(x)和A(x)只需要让最终重建图像误差最小即可。我做对比实验时发现两阶段模型在合成数据上表现尚可但迁移到真实雾天图像时纹理区域树叶、织物纹路、建筑边缘经常出现伪纹理或结构断裂而端到端一体化的网络在这一项上明显更稳。改参数时注意K(x)的通道数要和输入保持一致否则逐像素计算时维度不匹配。2.3 合成雾与真实雾的分布差异决定了模型的天花板网络训练需要成对的清晰/带雾图像但真实雾天几乎无法采集到严格的配对样本所以业界主要还是靠合成数据。合成雾的做法通常是用清晰图像加随机深度估计生成随机的传输率图再按大气散射模型叠加大气光# 合成带雾数据的常用流程伪代码级操作示意 # 1. 用单目深度估计模型如 MiDaS为清晰图生成深度图 # 2. 由深度图按指数衰减生成传输率 t exp(-beta * d) # 3. 全局大气光 A 在 [0.7, 1.0] 区间内随机采样 # 4. 按 I J * t A * (1 - t) 合成带雾图像 # 5. 对 t 做局部平滑扰动模拟真实雾浓度不均匀的情况这里有一个关键陷阱合成雾的t(x)通常是空间平滑的而真实雾因为大气湍流和光源变化传输率存在局部剧烈波动。模型在平滑的合成传输率上训练学到的是低频信息的主导映射对真实雾图中的高频纹理衰减缺乏适应能力。我一般会在训练时对传输率图做随机块状扰动相当于一种数据增强能够在某些样本上模拟出更接近真实雾的纹理损失效果。3. 面向纹理恢复的模型结构设计与关键模块选择3.1 编码器-解码器的偏置为什么直接堆卷积不行在去雾任务上感受野大小直接决定了模型能否利用全局上下文来消除雾的影响。雾对图像的影响是低频的——大气光散射本质上是全局效应一片区域的颜色偏移往往由远处的大气状况决定。如果感受野太小模型只能看到局部对比度容易把景深不同的区域搞混纹理恢复就成了空中楼阁。编码器-解码器结构的价值就在这里编码器逐步下采样扩大感受野让网络感知全局雾浓度解码器逐步恢复空间分辨率在语义信息的指导下重建纹理细节。需要注意的是单纯的下采样会丢失高频信息所以跳跃连接skip connection几乎是必需的设计。U-Net 风格的编解码结构是这个任务里最常见的骨架当然实际操作中有多种变体可供选择。# 一个用于去雾的编码器-解码器核心结构示例PyTorch 风格 class DehazeBlock(nn.Module): def __init__(self, in_ch, out_ch): super().__init__() self.conv1 nn.Conv2d(in_ch, out_ch, 3, padding1) self.conv2 nn.Conv2d(out_ch, out_ch, 3, padding1) self.relu nn.ReLU(inplaceTrue) def forward(self, x): out self.relu(self.conv1(x)) out self.relu(self.conv2(out)) return out class EncoderDecoder(nn.Module): def __init__(self): super().__init__() self.enc1 DehazeBlock(3, 64) self.pool1 nn.MaxPool2d(2) self.enc2 DehazeBlock(64, 128) self.pool2 nn.MaxPool2d(2) self.mid DehazeBlock(128, 256) self.up2 nn.Upsample(scale_factor2, modebilinear, align_cornersFalse) self.dec2 DehazeBlock(128 128, 128) self.up1 nn.Upsample(scale_factor2, modebilinear, align_cornersFalse) self.dec1 DehazeBlock(64 64, 64) self.output nn.Conv2d(64, 3, 3, padding1) def forward(self, x): e1 self.enc1(x) e2 self.enc2(self.pool1(e1)) m self.mid(self.pool2(e2)) d2 self.dec2(torch.cat([self.up2(m), e2], dim1)) d1 self.dec1(torch.cat([self.up1(d2), e1], dim1)) return self.output(d1)代码里dec2和dec1的上采样结果与对应编码器层在通道维度上拼接Upsample 用的是双线性插值align_cornersFalse能避免像素中心点对齐误差带来的网格伪影。实际操作中我会把MaxPool2d替换为步长为 2 的卷积下采样过程可学习纹理信息保留得更多训练也相对稳定。另外上采样操作在解码器中起到了关键作用——如果用转置卷积容易出现棋盘格伪影这对纹理细节的恢复是致命的双线性插值虽然简单但没有可学习参数不会引入这个风险。3.2 注意力机制在雾浓度不均匀场景中的必要性真实雾天图像的一大特点是雾浓度随景深变化远景雾浓、近景雾淡。全局共享的卷积核天然不擅长处理这种空间异质性。注意力机制能让模型按像素位置动态调整特征权重浓雾区域增强特征响应薄雾区域抑制过度增强。比较常用的方案有两类。通道注意力如 SENet 的 SE 模块在通道维度做重标定保留对去雾有用的高频纹理通道。而空间注意力生成一个二维权重图逐像素调整特征强度在空间维度上让模型更关注浓雾区域。# 结合通道与空间注意力的残差块FFA-Net 风格的核心思想 class AttentionBlock(nn.Module): def __init__(self, channels): super().__init__() self.channel_avg nn.AdaptiveAvgPool2d(1) self.channel_max nn.AdaptiveMaxPool2d(1) self.fc nn.Sequential( nn.Conv2d(channels * 2, channels // 8, 1), nn.ReLU(inplaceTrue), nn.Conv2d(channels // 8, channels, 1) ) self.spatial nn.Sequential( nn.Conv2d(2, 1, 7, padding3), nn.Sigmoid() ) def forward(self, x): # 通道注意力平均池化捕获全局信息最大池化捕获显著纹理 avg_out self.fc(torch.cat([self.channel_avg(x), self.channel_max(x)], dim1)) channel_weight torch.sigmoid(avg_out) x x * channel_weight # 空间注意力拼接平均/最大池化特征图7x7 卷积学习空间权重 avg_spatial torch.mean(x, dim1, keepdimTrue) max_spatial, _ torch.max(x, dim1, keepdimTrue) spatial_cat torch.cat([avg_spatial, max_spatial], dim1) spatial_weight self.spatial(spatial_cat) return x * spatial_weight参数上的两个细节值得注意通道注意力的瓶颈层压缩比设channels // 8压缩比太狠会丢信息太浅则参数膨胀空间注意力部分用 7×7 卷积核是因为需要较大的感受野来感知空间结构3×3 在这个任务上偏弱。我把这个模块放在编码器的每个 Block 之后而不是所有位置统一加因为浅层特征是纹理高频信息的主要载体深层语义特征的注意力加权收益不高但计算开销实在。3.3 多尺度特征融合的落地方式雾对图像的影响在不同尺度上表现不同——浓雾区域需要更大感受野来估计大气光纹理细节却依赖高分辨率特征。单尺度模型很难两头兼顾。多尺度融合的常见做法是把不同分辨率的特征图上采样到同一尺寸后沿通道拼接再通过 1×1 卷积降维融合。这个操作很简单但要注意拼接顺序和上采样方式要保持一致否则不同尺度的空间对齐误差会被后续卷积放大。更轻量级的替代方案是空洞卷积dilated convolution在不降低分辨率的前提下扩大感受野。设计时保持空洞率递增的序列如 1→2→4→8避免出现网格伪影。我用空洞卷积替换部分下采样层后模型参数量没有增加但纹理边缘的连续性有明显改善。需要留意的是空洞卷积对大分辨率输入显存消耗较高如果显存不够回到多尺度拼接更现实。4. 损失函数组合纹理恢复的关键所在4.1 L1 损失的局限性以及为什么不只盯像素误差很多同学做去雾课程作业第一反应是用 L2MSE损失优化网络因为实现最直接。但 L2 损失对应高斯噪声假设下的最大似然估计优化它倾向于生成所有可能结果的均值——在去雾问题上就是纹理被抹平。L1 损失对离群点的惩罚更温和能保留更多边缘信息但单独使用仍然缺乏对感知结构的约束。# 像素损失推荐用 L1 而非 L2训练更稳且留住的纹理更多 criterion_pixel nn.L1Loss() # 训练循环中的核心损失计算片段 for batch in train_loader: hazy_img, clean_img batch output model(hazy_img) loss_l1 criterion_pixel(output, clean_img) loss loss_l1 # 后续加上感知损失和对抗损失 optimizer.zero_grad() loss.backward() optimizer.step()原因在于 L1 的梯度是常数不会像 L2 那样对大误差区域产生过强的梯度信号避免了模型为了少数困难像素而牺牲大范围纹理结构。实际训练中 L2 收敛后的 PSNR 可能不低但放大看纹理区域会有明显的涂抹感L1 在主观细节上领先是普遍的结论。参数上初始学习率建议2e-4使用 Adam 优化器这两个设置在大多数去雾模型上都适用。4.2 感知损失让网络输出与人类视觉对齐感知损失的基本思路是让输出图像和真实图像在预训练网络的深层特征空间上接近而不是在像素空间上接近。深层特征经过大量语义信息编码像素位置的微小偏移被抑制结构性和纹理模式被强调。去雾任务里我一般用 VGG16 的relu3_3层作为特征提取器import torchvision.models as models class PerceptualLoss(nn.Module): def __init__(self): super().__init__() vgg models.vgg16(pretrainedTrue).features[:16] # 到 relu3_3 self.vgg vgg.eval() for param in self.vgg.parameters(): param.requires_grad False def forward(self, pred, target): # 输入需归一化到 VGG 期望的分布 pred_feat self.vgg(pred) target_feat self.vgg(target) return nn.functional.l1_loss(pred_feat, target_feat)冻结预训练权重是必须的否则感知损失会退化成另一个像素损失。选relu3_3的考量在于它包含足够的纹理信息又不过度依赖语义分类结果选太深的relu5_3容易丢失高频纹理。实际训练时感知损失的权重设在0.01到0.1之间太高会让网络过拟合 VGG 的特征分布反而损害真实图像上的纹理自然度。4.3 对抗损失与纹理细节的逼真感Gan 类的对抗损失对纹理恢复的贡献比较明显它迫使生成图像的纹理分布接近真实图像的纹理分布而不仅仅是靠近某种均值。PatchGAN 判别器是稳的选择输出一个 N×N 的矩阵每个位置的值是该局部区域真假的置信度——强制生成器在每个局部块上都保真适合纹理细节的逼真感。class PatchDiscriminator(nn.Module): def __init__(self, in_ch3): super().__init__() self.conv nn.Sequential( nn.Conv2d(in_ch, 64, 4, stride2, padding1), nn.LeakyReLU(0.2, inplaceTrue), nn.Conv2d(64, 128, 4, stride2, padding1), nn.BatchNorm2d(128), nn.LeakyReLU(0.2, inplaceTrue), nn.Conv2d(128, 256, 4, stride2, padding1), nn.BatchNorm2d(256), nn.LeakyReLU(0.2, inplaceTrue), nn.Conv2d(256, 1, 4, padding1) # 输出为 N x N 的置信度图 ) def forward(self, x): return self.conv(x)训练时生成器和判别器的更新比例很关键。我通常每个 step 更新一次生成器、更新两次判别器判别器训练得更充分后能为生成器提供更强的梯度信号。WGAN-GP 的梯度惩罚项也能稳定训练但实现成本略高。课程作业时间有限的话常规 GAN 损失加较小的权重0.01左右配合感知损失是最稳妥的搭配。5. 训练调参与评估的完整闭环5.1 数据预处理与增强的实操方案训练去雾模型时图像的尺寸和处理顺序直接影响收敛速度。一般做法是随机裁剪成 256×256 的块配合水平翻转和旋转 90 度做数据增强同时保持清晰图和带雾图的变换严格一致。这里有一个容易忽略的点预处理中的归一化参数不能随意改用 ImageNet 的均值和标准差是多数预训练骨架的默认输入分布自己统计数据集算出的均值反而会让预训练权重失去意义。from torchvision import transforms # 对输入和标签做相同的增强随机翻转和旋转必须在拼接维度上保持同步 transform_hazy transforms.Compose([ transforms.RandomCrop(256), transforms.RandomHorizontalFlip(p0.5), transforms.ColorJitter(brightness0.2, contrast0.2), ]) # 清晰图只做几何变换不做颜色抖动 transform_clean transforms.Compose([ transforms.RandomCrop(256), transforms.RandomHorizontalFlip(p0.5), ])合成数据时雾浓度参数beta的取值区间直接影响模型的泛化能力。beta太小小于 0.5雾太薄模型学不到去雾能力太大图像几乎被雾完全掩盖模型会学习到过度增强的偏置容易在薄雾图上产生过曝伪影。区间放在[0.6, 1.8]比较均衡每隔若干 epoch 重采样一次雾浓度让模型在训练过程中不断见到新的退化组合。5.2 学习率计划与训练停止时机的判断去雾网络常见的训练曲线特征很鲜明前 20 个 epochPSNR 涨得很快之后进入平台期像素损失下降变慢但感知质量还在缓慢改善。如果在验证集上 PSNR 已经不再上升不代表该停止训练——这时候往往需要切换损失权重或降低学习率继续精调纹理细节会在这时逐渐浮出来。阶段学习率epoch 范围损失权重感知:对抗:L1前期粗训练2e-40–300.01 : 0 : 1中期精调5e-530–600.05 : 0.005 : 1后期感知优化1e-560–1000.1 : 0.01 : 1学习率我习惯用余弦退火而不是按 epoch 阶梯下降。阶梯下降的问题是每个台阶开始时损失会剧烈震荡去雾网络的纹理恢复对这种震荡非常敏感震荡后往往回不到之前的视觉质量水平。余弦退火会平滑地过渡纹理细节微调时不容易被破坏。5.3 客观指标 PSNR/SSIM 与主观质量的矛盾评估时最常遇到的问题就是 PSNR 和 SSIM 与主观感受不一致。举例来说一个把纹理完全磨平但大气光估计准确的模型PSNR 可能比一个纹理生动但像素位置略有偏移的模型高出 1-2 个 dB。原因是 PSNR本质上就是 MSE 的对数变换对像素级精确匹配敏感而人类视觉对结构相似度更敏感。SSIM 虽然考虑了亮度、对比度和结构三个维度但在纹理复杂区域仍不够敏感——它能反映某个局部窗口的统计相似性却无法判断纹理是否被“磨平”还是“伪影叠加”。所以课程作业如果只报 PSNR/SSIM并不能充分证明纹理细节恢复得好。我的做法还会额外算一个指标边缘保持指数EPIEdge Preservation Index。计算方式是用 Sobel 算子提取输出图和真实图的边缘强度图然后做两者的相关性分析。注意一个大坑用不同厂家的学术测试集和真实场景测试集评估模型表现可能完全相反。学术测试集如 RESIDE是合成雾模型学到的纹理模式高度匹配但换到真实雾天照片性能会明显下降。报告里我会把合成集和真实图分开列指标防止对模型能力给出过于乐观的评价。6. 轻量化部署与验证技巧把去雾模型推到真实场景6.1 轻量化改造的优先级排序课程作业的单模型方案如果只在测试集上跑个指标就浪费了这套流程的复用价值。真实场景中图像去雾可能部署到移动端或边缘设备轻量化改造会让模型实用性显著提升。优先做三件事把普通卷积替换为深度可分离卷积Depthwise Separable Convolution剪掉解码器中贡献度低的通道如果精度有余量用 INT8 量化替换 FP32。# 以 PyTorch 为例导出 TorchScript 并量化操作示意 model.eval() example_input torch.randn(1, 3, 256, 256) traced_model torch.jit.trace(model, example_input) traced_model.save(dehaze_model.pt) # 量化的前提是模型已经收敛量化后需重新跑一遍指标 quantized_model torch.quantization.quantize_dynamic( traced_model, {nn.Conv2d, nn.Linear}, dtypetorch.qint8 ) torch.jit.save(quantized_model, dehaze_model_int8.pt)深度可分离卷积能把参数量降到原来的 1/8 到 1/9计算量也有同量级的下降。代价是通道间信息交互变弱纹理恢复精度会掉一些通过加宽通道数可以部分补偿。量化在去雾模型上效果不错因为卷积权重分布相对集中INT8 量化后 PSNR 损失通常控制在 0.3 dB 以内对主观质量几乎无感。6.2 网格状伪影与色偏的排查方法训练结束如果发现输出图有规则的网格伪影第一步检查上采样方式。转置卷积是最常见的元凶尤其当卷积核大小不能被步长整除时。把ConvTranspose2d替换成Upsample(modebilinear) Conv2d伪影会立刻消失。还有一个隐蔽来源是数据预处理时对图像做了不等比缩放比如宽高比不是整数倍时发生的插值会在输出图上留下沿某个方向的规则条纹。色偏问题通常指向大气光估计环节。端到端模型内部不会显式输出大气光但如果训练数据中含大量天空区域模型容易学会把亮区过度压暗。缓解手段是在损失函数里加一个颜色一致性惩罚项约束输出图像的 RGB 三个通道的均值比例与清晰图接近# 颜色一致性损失约束输出与标签在通道均值比例上接近 def color_consistency_loss(pred, target): pred_mean pred.mean(dim[2, 3]) # [B, C] target_mean target.mean(dim[2, 3]) # 计算通道均值之间的余弦相似度越接近 1 越好 cos_sim nn.functional.cosine_similarity(pred_mean, target_mean, dim1) return (1.0 - cos_sim).mean()6.3 一个可落地的验证流程模型训练完成后建议按固定的三步流程做最终验证先跑合成测试集的指标确认没有回归再跑真实雾天图片做目测对比重点看边缘轮廓和精细纹理区域最后做一次用户调研式的主观对比找同学盲评输出去雾图和原图把偏好结果和 SSIM 做对照。主观评价收集 10 人以上的意见就具备参考意义比只看 PSNR 数字有说服力得多。这三个验证步骤都能通过脚本半自动完成核心是确认一个事实网络确实恢复出了雾后面的纹理结构而不只是把图像的对比度拉高而已。本文还有配套的精品资源点击获取
返回列表