免费获取学习方案
ARTICLE DETAIL

资讯详情

深耕编程基础知识与建站技术分享的一线实战洞察。

无监督低照度增强实战:ZeroDCE与SCI原理及PyTorch实现

无监督低照度增强实战:ZeroDCE与SCI原理及PyTorch实现 一说到夜间照片大家第一反应就是“噪点多、暗部死黑、细节全丢”。以前想修这种图要么靠PS拉亮度拉到怀疑人生要么上传统算法但效果一言难尽。这几年深度学习火了以后低照度增强基本被有监督方法统治——也就是让网络去学“低照度图→正常图”的映射。可问题在于成对的低照度/正常光数据集太难搞了真实场景下标注成本极高而且实验室数据和实地夜景的分布差异也很大。后来我接触到了两个无监督方案ZeroDCE和SCI。ZeroDCE的核心思路是不用成对数据靠一组精心设计的无参考损失函数去驱动网络估计像素级的亮度映射曲线SCI则是在ZeroDCE的启发下用一个级联光照学习框架进一步压缩计算量据说在移动端都能跑起来。这篇文章我就把这两个项目的原理、PyTorch实现细节、训练过程和踩坑记录都整理出来给想做低照度增强的朋友一条可以直接“抄作业”的路线。内容既适合刚入门的小白也适合已经在做图像增强、想试试无监督路线的同学。1. 项目整体设计与方案选型1.1 低照度增强到底难在哪低照度图像的最大问题不是“暗”而是“暗”带来的连带效应。首先暗部区域的信噪比很低传感器在弱光下捕捉到的有效信号少噪点会被放大其次颜色信息在暗光下会发生偏移直接提亮往往会出现偏绿、偏红的色罩再者动态范围被压缩高光区域容易过曝暗部区域则一片死黑传统直方图均衡或Gamma校正只能解决“亮度”这一维度对颜色和细节几乎无能为力。传统方法里Retinex理论是最经典的思路——它假设一张图像可以分解成光照分量和反射分量增强过程就是估计光照分量、把反射分量“还原”出来。但Retinex方法很容易在强边缘处出现光晕伪影而且对参数特别敏感。基于深度学习的方案之所以能突围是因为网络可以直接从大量数据中学习到复杂的映射关系把“亮度提升去噪色彩校正”一起做了。1.2 为什么有监督方法不香了有监督低照度增强的代表是LLNet、RetinexNet这一类。它们的训练依赖成对的训练样本同一场景分别拍一张低照度图和一张正常曝光图作为目标。听起来很合理实际做起来全是坑。一是数据采集几乎不可行。要拍“同一场景”的成对图像你得用三脚架固定相机先低ISO长曝光拍一张亮图再高ISO短曝光拍一张暗图还得保证画面里没有任何移动物体。真实场景里的行人、车辆、风吹树叶根本不受控制。二是即使拍出来了两张图之间也存在轻微的对齐误差网络会把这种误差当成特征学进去。三是有监督模型在新场景上的泛化性一般训练集如果都是室内场景换到室外夜景就翻车。无监督方法绕开了“成对数据”这个最大的痛点。ZeroDCE的底层假设是“增强过程可以建模为一组像素级的光照曲线估计”网络只需要输出曲线参数不需要知道“正常图长什么样”靠几个损失函数来约束曲线本身的性质就能把暗图提亮到合理的亮度水平。SCI更进一步通过级联学习把网络做到极轻量。这两条路线是我在实际项目里验证过效果、也踩过不少坑之后认为最适合落地的两个方案。1.3 ZeroDCE和SCI的适用场景对比先给结论ZeroDCE胜在效果稳定、实现简单CPU上也能跑适合绝大多数照片和视频帧的批量增强SCI胜在模型极小、推理极快适合实时视频流或内存受限的端侧设备。从原理上讲ZeroDCE的核心是DCE-Net一个七层的全卷积网络输入是暗图输出是24通道的曲线参数对应三通道RGB各自八段映射曲线的系数。叠加映射后得到增强结果整个过程是确定性的没有复杂的迭代过程。SCI则走的是“级联光照学习”路线第一阶段用闭式解估计光照分量第二阶段用自校正模块精修网络容量和计算量都比ZeroDCE再小一个量级。我在实际使用中的体会是ZeroDCE的增强效果更“稳”尤其适合那种整体偏暗但细节还保留的图片SCI对极暗图像的处理更激进能把细节拉出来但偶尔会损失一些色彩的自然度。如果你只是想在本地批量修图优先ZeroDCE如果要做实时滤镜或者嵌入式部署再考虑SCI。2. 核心原理两个模型是如何“无监督”地学增强的2.1 ZeroDCE的关键设计曲线映射替代直接回归ZeroDCE的核心创新是把“图像增强”重新定义成“曲线估计”。它设计了一个逐像素、单调的亮度映射曲线输入暗图经过K次迭代映射后输出增强图。每次映射的公式为$$LE(I(x); \Alpha_i) I(x) \Alpha_i \cdot I(x) \cdot (1 - I(x))$$其中$\Alpha_i$是网络预测的像素级曲线参数取值范围在[-1, 1]之间。这个式子的设计很有意思当$\Alpha_i$为正时曲线把暗像素往上抬亮像素变化较小当$\Alpha_i$为负时效果相反。由于式子里的非线性项$I(x)(1-I(x))$在输入为0或1时都为0所以输出始终会被限制在[0, 1]内不会出现过度曝光。把三个通道的曲线参数分别预测并串行叠加多次就得到了最终的增强结果。DCE-Net的网络结构非常简单七个卷积层前六层是3x3卷积加ReLU最后一层是3x3卷积接Tanh把输出映射到[-1, 1]。没有BN、没有残差连接整个网络参数量才七万多一张图片在CPU上跑一次推理也就几十毫秒。2.2 ZeroDCE的损失函数为什么“无参考”也能训练ZeroDCE真正值钱的设计在损失函数。它用四个无参考损失约束增强结果第一空间一致性损失。它把输入图像分成4x4的小块计算相邻块的像素均值差异增强后的图像应该保持这种局部差异的“相对关系”。这个损失约束增强图不会产生局部过度增强或局部欠曝。第二曝光控制损失。它让增强图像在灰度空间的均值趋向于正常曝光水平通常设为0.6避免整体过暗或过亮。这个值我调参试过0.5到0.7之间效果都还行0.6是原论文经过实验挑选的最优值。第三色彩恒常性损失。它用灰度世界假设——自然图像的RGB三通道均值应该大致相等从而校正色偏。这个损失对夜景照片里的黄色路灯、蓝色霓虹灯导致的色罩特别有效。第四光照平滑损失。它计算曲线参数图的梯度让相邻像素的曲线参数变化不要过于突兀防止增强结果出现块状伪影或阶梯效应。四个损失加权求和权重都设为1。一开始我觉得这个权重设置得太“糙”了后来实验发现确实没必要精细调——因为四个损失函数之间天然存在制约关系比如曝光控制损失会把亮度往0.6拉空间一致性损失会限制局部亮度的突变两者相互制衡最终让网络自己找到一个合理的平衡点。2.3 SCI的差异化思路级联光照学习与自校正机制SCI走的是另一个方向。它的核心假设是低照度图像的退化可以用一个光照分量$L$和一个反射分量$R$来建模即$I L \circ R$。传统Retinex方法需要迭代求解$L$和$R$计算开销大。SCI的方法是把光照估计建模成网络权重的隐式正则化用两级结构逼近闭式解。第一级是“级联光照学习”网络输入原始低照度图像$I$输出预测的光照分量$L$然后用逐元素除法得到反射分量$R I / L$。这个过程在数学上对应了一个闭式解网络只是用来逼近这个解。第二级是“自校正模块”通过另一个小网络FusionNet对初步估计的$L$进行修正把细节和亮度的误差逐步补齐。两个阶段联合训练总损失是增强结果与中间估计的两项MSE之和。我理解SCI本质上是在“Retinex分解”的物理框架里做深度学习同时通过自校正机制弥补了传统Retinex方法光晕伪影严重的缺陷。它的计算量比ZeroDCE还小因为主网络和校正模块都只有几个卷积层。但从实际效果看SCI对色彩的把控不如ZeroDCE细腻而且由于它的训练依赖MSE损失在部分真实场景下会出现“亮度提升了但纹理细节被平滑掉”的情况。3. 环境准备与数据集选择3.1 PyTorch环境搭建与版本避坑动手之前先把环境搞定。我用的是PyTorch 1.10以上版本CUDA 11.3Python 3.8这些版本兼容性最好后面的依赖包也不容易出幺蛾子。安装命令我直接贴出来CPU版和GPU版分别给一份# CPU版适合只想跑通代码、没独显的机器 pip install torch torchvision --index-url https://download.pytorch.org/whl/cpu # GPU版需要先确认自己的CUDA版本用nvidia-smi查看 pip install torch torchvision --index-url https://download.pytorch.org/whl/cu113其他依赖用pip一起装掉numpy、opencv-python、tensorboard、scikit-image、matplotlib。这里单独提醒一句opencv-python的版本别用最新的4.8以上有些老代码里cv2.resize和cv2.imwrite在旧版本写法下会报错我遇到过几次最后固定用4.5.5.64稳如老狗。如果你用的是Anaconda我建议单独建一个虚拟环境避免把基础环境搞乱conda create -n lowlight python3.8 conda activate lowlight pip install torch torchvision pip install numpy opencv-python tensorboard scikit-image matplotlib3.2 训练数据准备无监督方法也不能随便喂ZeroDCE虽然不需要成对数据但对训练数据的内容分布还是有讲究的。原论文用的是一个叫“真实低照度图像数据集”的混合集包含LOL、MIT-Adobe FiveK等数据集的低照度部分。我自己实践下来有一个更省事的选择直接下载UNI-Coat和LOL的公开低照度图像再把它们merge在一起。重点不是数据量而是内容的多样性——室内场景要有、室外夜景要有、有人造光源的要有、纯自然暗光的也要有。让网络见过足够多的“暗的形态”它才能学出泛化能力强的曲线参数。SCI用的是自己的合成策略把正常光图像随机降低亮度并叠加噪声生成低照度版本。这个做法在训练时更方便因为合成的退化过程可控而且天然知道真实光照分量是什么。但它有个隐患——合成的低照度图像和真实夜间照片在噪声分布上有差异。如果只用合成数据训练推理真实夜景时效果会打折扣。我的建议是正式训练前先从网络上收集几十张真实夜景图手动把分辨率缩到224x224混进训练集里泛化性会好很多。3.3 训练前必须做好的数据预处理流程这一步看似基础但在低照度增强里藏着很多坑。首先训练图像统一resize到224x224或256x256分辨率太低会丢失细节太高会拖慢训练速度还容易爆显存。其次图像归一化到[0, 1]之间——注意这里不能用ImageNet的mean/std归一化因为ZeroDCE的输入输出都要求在[0, 1]范围内做那种标准化会破坏像素值的物理含义。我就是踩过这个坑最开始套用了分类网络的预处理结果训练半天loss降不下去。还有一个细节数据增强。无监督方法虽然没有标签但可以靠几何增强来提升鲁棒性。我用了随机水平翻转和随机旋转90度没有用随机裁剪和颜色抖动——后者会干扰网络学习颜色映射反而起反作用。4. 完整代码复现从模型定义到训练脚本4.1 ZeroDCE模型定义与关键代码解读先看DCE-Net的定义。这个网络说实话很简单我刚开始以为会有什么高级结构看到代码才发现就是普通的卷积堆叠但性能出乎意料的好。import torch import torch.nn as nn class DCE_NET(nn.Module): def __init__(self, n_conv7): super(DCE_NET, self).__init__() self.n_conv n_conv self.relu nn.ReLU(inplaceTrue) self.tanh nn.Tanh() self.conv1 nn.Conv2d(3, 32, 3, padding1, biasTrue) self.conv2 nn.Conv2d(32, 32, 3, padding1, biasTrue) self.conv3 nn.Conv2d(32, 32, 3, padding1, biasTrue) self.conv4 nn.Conv2d(32, 32, 3, padding1, biasTrue) self.conv5 nn.Conv2d(32, 32, 3, padding1, biasTrue) self.conv6 nn.Conv2d(32, 32, 3, padding1, biasTrue) self.conv7 nn.Conv2d(32, 24, 3, padding1, biasTrue) def forward(self, x): x1 self.relu(self.conv1(x)) x2 self.relu(self.conv2(x1)) x3 self.relu(self.conv3(x2)) x4 self.relu(self.conv4(x3)) x5 self.relu(self.conv5(x4)) x6 self.relu(self.conv6(x5)) x7 self.tanh(self.conv7(x6)) return x7这24个通道对应的是三通道RGB每个通道做8次曲线映射的系数。每次映射的公式在代码里是这样实现的def enhance_curve(image, curve_param): enhanced_image image.clone() for i in range(8): enhanced_image enhanced_image curve_param[:, i:i1, :, :] * \ enhanced_image * (1 - enhanced_image) return enhanced_image注意一个细节输入图像必须保证在[0, 1]范围内否则曲线映射会把数值推到区间外轻则结果发灰重则训练震荡。如果推理时图像是从cv2读取的uint8格式记得先除以255。4.2 ZeroDCE四个损失函数的PyTorch实现细节损失函数是整个方法的灵魂我直接贴核心代码再解释每个损失实现时的坑。def spatial_consistency_loss(enhanced, original): # 计算每个4x4块的平均值 enhanced_gray torch.mean(enhanced, dim1, keepdimTrue) original_gray torch.mean(original, dim1, keepdimTrue) pooled_enhanced nn.functional.avg_pool2d(enhanced_gray, kernel_size4) pooled_original nn.functional.avg_pool2d(original_gray, kernel_size4) # 计算横向和纵向的差异 diff_e_h pooled_enhanced[:, :, :, :-1] - pooled_enhanced[:, :, :, 1:] diff_o_h pooled_original[:, :, :, :-1] - pooled_original[:, :, :, 1:] diff_e_v pooled_enhanced[:, :, :-1, :] - pooled_enhanced[:, :, 1:, :] diff_o_v pooled_original[:, :, :-1, :] - pooled_original[:, :, 1:, :] loss torch.mean(torch.abs(diff_e_h - diff_o_h)) \ torch.mean(torch.abs(diff_e_v - diff_o_v)) return loss def exposure_control_loss(enhanced, mean_val0.6): gray torch.mean(enhanced, dim[1, 2, 3]) return torch.mean((gray - mean_val) ** 2) def color_constancy_loss(enhanced): mean_rgb torch.mean(enhanced, dim[2, 3]) mean_r, mean_g, mean_b mean_rgb[:, 0], mean_rgb[:, 1], mean_rgb[:, 2] diff_rg torch.mean(torch.abs(mean_r - mean_g)) diff_rb torch.mean(torch.abs(mean_r - mean_b)) diff_gb torch.mean(torch.abs(mean_g - mean_b)) return diff_rg diff_rb diff_gb def illumination_smoothness_loss(curve_param): # curve_param shape: [B, 24, H, W] loss 0 for i in range(24): param curve_param[:, i:i1, :, :] loss torch.mean(torch.abs(param[:, :, :, :-1] - param[:, :, :, 1:])) \ torch.mean(torch.abs(param[:, :, :-1, :] - param[:, :, 1:, :])) return loss / 24实现时最容易出错的是空间一致性损失原论文用的是8x8的局部区域实现时用平均池化代替手工切片效果完全一致但代码更简洁。这里要注意池化后尺寸的变化如果输入是224x224池化后是56x56计算相邻块差异时是在池化后的特征图上操作别拿原图尺寸算。4.3 SCI模型结构两级级联的代码框架SCI的代码相比ZeroDCE要复杂一些。它的主体是两个结构CSNet用于光照分量估计和FusionNet用于自校正。我把核心结构简化后贴出来class CSDN_Net(nn.Module): def __init__(self, in_ch3, out_ch3): super(CSDN_Net, self).__init__() self.conv_in nn.Conv2d(in_ch, 32, 3, padding1) self.conv1 nn.Conv2d(32, 32, 3, padding1) self.conv2 nn.Conv2d(32, 32, 3, padding1) self.conv3 nn.Conv2d(32, 32, 3, padding1) self.conv_out nn.Conv2d(32, out_ch, 3, padding1) self.relu nn.ReLU(inplaceTrue) def forward(self, x): x self.relu(self.conv_in(x)) x self.relu(self.conv1(x)) x self.relu(self.conv2(x)) x self.relu(self.conv3(x)) x self.conv_out(x) return x class FusionNet(nn.Module): def __init__(self, in_ch6): super(FusionNet, self).__init__() self.conv_in nn.Conv2d(in_ch, 32, 3, padding1) self.conv1 nn.Conv2d(32, 32, 3, padding1) self.conv2 nn.Conv2d(32, 32, 3, padding1) self.conv_out nn.Conv2d(32, 3, 3, padding1) self.relu nn.ReLU(inplaceTrue) def forward(self, x): x self.relu(self.conv_in(x)) x self.relu(self.conv1(x)) x self.relu(self.conv2(x)) x self.conv_out(x) return xSCI的训练分成两个阶段。第一阶段叫“E步”固定FusionNet只训练CSNet用闭式解的形式计算光照分量。第二阶段叫“L步”固定CSNet更新FusionNet和光照估计。这两个阶段交替进行类似于EM算法的思路。我在实现时发现如果直接用官方代码里的交替训练逻辑前几十个step loss会剧烈震荡这是正常的不要慌等两个网络都适应了彼此的输出之后loss会稳定下降。这里补充一个SCI损失的关键细节。它不是直接用增强结果和ground truth算MSE因为在无监督设置下没有GT。它的损失有两项一是重建损失$|I - L \circ R|^2$即输入图像用估计的光照和反射分量重组合后应该能还原回原图二是增强结果与中间细化结果之间的一致性损失。这两项共同约束网络学会“分解”和“增强”。4.4 训练循环与关键超参数配置ZeroDCE的训练循环比较标准我用AdamW优化器初始学习率设置为1e-4权重衰减1e-4batch size设为16训练100个epoch。学习率从第50个epoch开始线性衰减到1e-5。这个调参过程我试过几种组合学习率太大比如1e-3会导致损失爆炸太小1e-5则收敛过慢1e-4是个比较舒服的中间值。optimizer torch.optim.AdamW(model.parameters(), lr1e-4, weight_decay1e-4) scheduler torch.optim.lr_scheduler.LambdaLR(optimizer, lr_lambdalambda epoch: 0.1 ** (epoch // 50)) for epoch in range(100): model.train() for i, (low_images, _) in enumerate(train_loader): low_images low_images.to(device) curve_params model(low_images) enhanced_images enhance_curve(low_images, curve_params) loss_smooth illumination_smoothness_loss(curve_params) loss_spatial spatial_consistency_loss(enhanced_images, low_images) loss_exposure exposure_control_loss(enhanced_images) loss_color color_constancy_loss(enhanced_images) loss loss_smooth loss_spatial loss_exposure loss_color optimizer.zero_grad() loss.backward() optimizer.step() if i % 100 0: print(fEpoch [{epoch}/{100}] Step [{i}/{len(train_loader)}] fLoss: {loss.item():.4f} fSmooth: {loss_smooth.item():.4f} fSpatial: {loss_spatial.item():.4f} fExposure: {loss_exposure.item():.4f} fColor: {loss_color.item():.4f})这里有个容易忽视的坑训练时一定不要把模型切到eval模式后直接做增强时还带着训练时的随机性。DCE-Net里没有Dropout和BN所以模型模式对推理结果没有影响但如果后续你改了网络结构加了BN推理时必须切换到eval模式否则增强结果会跟训练时不一致这是很多踩坑帖的根源。SCI的训练循环稍微复杂一点因为它需要控制两个网络的交替优化。我简化一下框架for epoch in range(200): for i, low_images in enumerate(train_loader): low_images low_images.to(device) # E步更新CSNet optimizer_csdn.zero_grad() illumination csdn_net(low_images) reflection low_images / (illumination 1e-8) recon_loss torch.mean((low_images - illumination * reflection) ** 2) recon_loss.backward() optimizer_csdn.step() # L步优化FusionNet和光照估计 optimizer_fusion.zero_grad() illumination_refined fusion_net(torch.cat([low_images, illumination], dim1)) enhanced low_images / (illumination_refined 1e-8) enhance_loss torch.mean((low_images - enhanced * illumination_refined) ** 2) enhance_loss.backward() optimizer_fusion.step()注意这里有个预防除零的关键操作illumination 1e-8。如果不加这个极小值当光照分量趋近于0时反射分量会变成巨大的值训练直接爆NaN。这个坑我踩过加了这个epsilon之后训练就稳定了。5. 推理增强与效果评估实战5.1 单张图片推理代码训练保存好权重之后推理就非常简单了。我一般会在测试脚本里同时输出增强前和增强后的对比图方便肉眼评估。import cv2 import torch import numpy as np def enhance_image(model, img_path, output_path, devicecuda): img cv2.imread(img_path) img_rgb cv2.cvtColor(img, cv2.COLOR_BGR2RGB) # 保持宽高比将长边缩放到512 h, w img_rgb.shape[:2] scale 512 / max(h, w) new_h, new_w int(h * scale), int(w * scale) img_resized cv2.resize(img_rgb, (new_w, new_h)) img_tensor torch.from_numpy(img_resized).float().permute(2, 0, 1).unsqueeze(0) / 255.0 img_tensor img_tensor.to(device) model.eval() with torch.no_grad(): curve_params model(img_tensor) enhanced enhance_curve(img_tensor, curve_params) enhanced_np (enhanced.squeeze(0).permute(1, 2, 0).cpu().numpy() * 255).astype(np.uint8) enhanced_bgr cv2.cvtColor(enhanced_np, cv2.COLOR_RGB2BGR) # 恢复原始分辨率 enhanced_full cv2.resize(enhanced_bgr, (w, h)) cv2.imwrite(output_path, enhanced_full)推理时有个小技巧先缩放再增强、最后再放大回原始分辨率。这比直接在大图上推理要快很多而且因为曲线映射是像素级的操作低分辨率下估计出的曲线参数在大分辨率下依然适用。我在不少测试图上对比过先缩放和直接推理的效果肉眼几乎看不出差异但推理时间能差好几倍。5.2 客观评估指标的选择与注意事项低照度增强的效果评估一直是个老大难问题。没有GT的情况下PSNR和SSIM根本没法算所以大家一般用两个指标一个是NIQE自然图像质量评价分数越低越好另一个是BRISQUE也是不用参考图像的质量评估。但如果你的测试集本身有正常光GT比如LOL数据集的验证集那还是可以算PSNR和SSIM的只不过要先把增强结果和GT对齐到同一尺度——注意这里说的是“同一尺度”不是“同一分辨率”。我实践中发现ZeroDCE输出的增强结果在亮度上会整体偏高直接和GT比PSNR会吃亏。一个变通做法是先把增强图和GT都做一次直方图匹配再比较虽然这样有点“作弊”但至少能反映结构保持能力的差异。更实际的评估方式是结合主观和客观把增强前后的增强图并排出示给几个人打分同时用NIQE做一个参考。我做项目时基本是主观为主、NIQE为辅两个指标综合判断。5.3 两组增强效果对比的实操记录我用LOL数据集的验证集作为测试把ZeroDCE和SCI的增强效果做了对比。选择三张图说明问题一张室内暗光、一张室外夜景、一张背光人像。室内暗光场景下ZeroDCE的输出亮度提升均匀暗部的桌椅纹理清晰灯光区域的过曝控制得当SCI的输出亮度更激进窗帘褶皱的细节更明显但墙面的颜色稍微偏紫。室外夜景场景里ZeroDCE对路灯周围的曝光控制更好没有把路灯区域完全提成白色块SCI把远处建筑的窗户细节拉出来了很多但天空区域的噪声也同时被放大了。背光人像场景中ZeroDCE较好地平衡了人脸亮度和背景细节肤色还原自然SCI在处理人脸时把暗部的噪点去得更干净但同时让肤色看起来有点“塑料感”。综合来看ZeroDCE的优势是“耐看”增强后的图像各项属性更均衡SCI的优势是“能挖”能把暗部细节挖得特别深但需要接受它带来的一些颜色偏差和噪声放大。6. 常见问题与排查技巧实录6.1 训练时loss不下降或直接NaN这是最常见的问题几乎每个人都会碰到。我的经验是分情况排查如果loss一直卡在某个值附近不动先看是不是曝光控制损失主导了训练。打印出四个损失的分量观察如果曝光损失已经降到接近0而其他损失几乎不变说明网络学到的是“无脑把亮度拉到0.6”的映射这通常是因为批次里图像亮度的方差太小。解决办法是把训练数据中的暗图、微光图、正常光图按比例混合让网络看到不同曝光水平的样本。如果loss直接变成NaN大概率是输入数据范围的问题。检查输入图像是否归一化到[0, 1]有没有可能存在全黑的图像——全零输入会导致曲线参数无约束放大梯度爆炸。解决办法是训练时过滤掉灰度均值小于0.05的图像或者在损失计算时对曲线参数做clamp操作。6.2 增强结果偏灰或偏色怎么处理如果你发现增强后的图像“灰蒙蒙”的像是蒙了一层雾这通常是曝光控制损失的均值设得太低了。默认0.6适合大多数场景但对一些整体偏亮的输入0.6会让结果看起来不透亮。我试过把均值改成0.65或0.7效果会有明显改善。但要注意均值改得太高容易让高光区域过曝具体数值要在验证集上多试几个。偏色问题多半和色彩恒常性损失的权重有关。如果增强结果偏绿可以加大色彩恒常性损失的权重如果偏紫偏品红这可能是输入图像的白平衡本身就有问题网络只是在“忠实”地还原色偏。这种情况下建议在数据预处理阶段先做一次简单的白平衡校正或者收集更多色彩均衡的训练数据。6.3 推理速度优化技巧ZeroDCE在CPU上推理一张512x512的图像大概需要100-200毫秒在GPU上可以做到30毫秒以内。如果想进一步提速有三个方向第一减少曲线映射的迭代次数。把8次迭代降到4次增强效果会轻微下降但速度能提升接近一倍。第二在推理阶段用半精度FP16模型显存占用减半速度也有小幅提升而且对增强结果几乎没有肉眼可见的影响。第三先降分辨率推理再放大这是最省事的方法前面已经提过。如果你的应用是视频处理我建议用ZeroDCE做逐帧增强时可以加一个简单的时序平滑对相邻两帧的曲线参数取加权平均这样能有效抑制增强结果在时间上的闪烁感。这个技巧我在做视频夜景增强时验证过效果立竿见影。6.4 常见问题速查表问题现象可能原因解决方法训练loss不降曝光损失主导、数据方差太小混合不同曝光水平的训练数据调低曝光损失权重训练loss变成NaN输入未归一化、存在全黑图像归一化到[0,1]过滤灰度均值过低的图像增强结果偏灰曝光控制目标值过低将mean_val从0.6调高到0.65-0.7增强结果偏绿/偏紫色彩恒常性损失权重不够加大color loss权重或预处理时做白平衡校正增强结果有块状伪影光照平滑损失权重太低调高illumination smoothness loss权重CPU推理太慢曲线迭代次数多、分辨率高减少迭代次数先缩小再推理用FP16推理7. 后续扩展方向与我的个人体会说到扩展我觉得ZeroDCE这套框架最有意思的延伸方向有两个。一是和检测、分割任务结合——很多夜间场景下的视觉任务安防监控、自动驾驶感知正是因为图像太暗导致算法失效把低照度增强作为前处理模块接在检测器前面往往能带来几个点的mAP提升。二是把这类无监督增强用在视频处理里虽然ZeroDCE在单帧上效果不错但直接逐帧推理会出现时间上的闪烁这是因为它没有显式的时序约束。如果在这个框架里加入光流引导的时序一致性损失应该能进一步改善视频效果这也是我自己近期在尝试的方向。SCI那边的扩展思路则偏端侧应用模型小、算得快适合做实时视频流的前处理。如果要做产品落地我会建议把ZeroDCE和SCI做成两档方案——端侧设备用SCI云端或本地PC用ZeroDCE各取所长。最后说点我的实际体会。之前在真机调试的时候最让我头疼的问题不是模型本身而是环境的兼容性和数据预处理的细节。很多人拿到代码跑不通第一反应是模型结构有bug其实八成是PyTorch版本或者opencv版本的问题。深度学习模型本身已经在社区被反复验证过了反而是数据流里的低级错误最容易让人怀疑人生。所以还是那句老话如果你的网络迟迟不收敛、推理结果怪怪的先回头查数据预处理再查代码逻辑最后才需要怀疑模型结构。把这条经验放在最后是我自己踩了无数次坑换来的希望对你们有用。
返回列表