资讯动态

深度光照估计:欠曝照片增强的Retinex建模与复现

发布时间:2026/10/16 19:26:47 来源:尧图企业网站定制
处理欠曝照片大概是图像增强里最“看着容易、做好很难”的事情。一张夜景或者逆光环境里随手抓拍的照片暗部几乎全丢直方图整个堆在暗端。大多数人的第一反应就是拉亮度、调Gamma、做直方图均衡化我一开始也是这么干的。结果一样画面确实变亮了但当场就“假”了——该黑的地方泛灰该有纹理的地方糊成一片边缘还常常出现光晕噪点全部被激活。这些现象指向同一个病根传统方法只是在像素值层面做单调映射完全没有“场景光照”的概念。这篇文章是图像增强系列的第二篇这次把欠曝场景单独拎出来用深度光照估计Deep Illumination Estimation的思路解决让网络先估计画面里各个区域被照亮的程度得到一张光照图再把光照分量修正回来在尽量不破坏内容结构的前提下恢复暗部细节。我会从原理、网络结构设计、训练数据构造、完整复现流程到实际训练中翻过的车一次性讲清楚。适合正在做低光照增强方向的同学也适合做拍照类产品后期模块、想搞明白“为什么无脑拉亮不行”的开发者。1. 欠曝照片的病根像素值映射救不了“光照分层”1.1 直方图均衡化和Gamma校正为什么越调越假Gamma校正本质是对每个像素做I_out I_in^(1/γ)γ大于1时把整体亮度抬起来。直方图均衡化则是把像素累计分布掰成一条直线让暗部占用更多灰度级。它们都属于像素级映射输入同一个值时输出同一个值不关心这个像素在画面里到底处于哪个位置。问题恰恰出在这。一张欠曝图里像素值接近的两个区域实际物理成因可能完全不同一个是物体本身颜色深比如黑色的背包另一个是物体本身是浅色但刚好被阴影盖住比如一堆被屋檐挡住的白纸。前者的反射率低后者反射率不低、只是光照暗。映射算法分不清这两种情况只能一视同仁地拉升。结果就是黑色的背包被提成灰色白纸被提亮了一些但依然没有层次。局部映射不一致还会在边缘和渐变区域形成光晕拉得越狠越明显。这就是我开头说的“光照分层”没被建模的典型症状。直方图均衡化的局部版本CLAHE稍微好一点因为它是在局部窗口里做对比度重新分配能保留一些局部层次。但CLAHE很依赖窗口参数窗口太小会把噪声增强成颗粒窗口太大又跟全局均衡化没什么区别。而且它对偏色场景几乎无能为力只是把三个通道分别拉伸色偏会被进一步放大。所以这些传统方法可以作为后处理辅助但作为增强主力远远不够。1.2 Retinex理论才是欠曝增强的正确入口欠曝增强真正需要的是把“光照”和“内容”分开。这就不得不提经典的Retinex模型传感器拍到的图像S(x,y)可以看成物体反射率R(x,y)和光照L(x,y)的逐像素乘积。S R * L其中R表示物体固有的颜色和纹理L表示场景里的照明分布。一张欠曝照片之所以暗主要不是颜色变了而是L整体偏低尤其是某些区域L几乎趋近于零。如果把L估计出来就能通过R S / L把被压暗的内容恢复出来。关键是L怎么估计。早年的Retinex算法用高斯滤波或者双边滤波去平滑S把平滑结果当作L。这个思路开销低但有硬伤滤波器区分不了“光照突变”和“物体纹理突变”。白色墙面上一道黑缝平滑后缝的边缘会被当成光照边界除法之后产生一圈光晕学术上叫halo伪影。而且大尺度滤波会把遮挡产生的软阴影全部抹平恢复出来的内容对比度也被一起冲淡。所以问题的核心从“怎么滤波”变成了“怎么学出一个合理的光照先验”。光照分布虽然是低频主成分但也需要语义信息配合太阳的位置、物体之间的遮挡关系、阴影落在哪这些都要看整张图才能推断。传统滤波器只看局部邻域无法跨区域理解场景深度网络天然能聚合上下文这就为Deep Illumination Estimation提供了条件。网络要做的不是对像素做映射而是预测一张逐像素的光照图再在光照域上做校正。这一章已经把“为什么”讲完了传统方法是像素级映射Retinex思路是分层处理而深度学习负责解决其中最难的光照估计部分。接下来的核心就是网络结构和损失设计。2. 深度光照估计网络到底在估什么结构设计与损失函数2.1 网络骨干多尺度编码逐步精修光照图我用的方案是一个比较轻量的多尺度编码器输入是欠曝RGB图输出是3通道光照图L_est数值范围压在0到1之间1表示该区域基本没有被压低接近0表示暗部几乎没光。整体是U形结构4个编码块把分辨率逐步降到1/8在最低尺度用1x1卷积输出一张粗略光照图然后经过3个解码块逐步恢复到原分辨率每一级都跟对应编码层的特征做跳跃连接最后再接一层1x1卷积精修成最终光照图。粗尺度输出的光照图不是废弃的我会在训练时给它单独加一个平滑损失做深监督逼着网络在低分辨率阶段先把大面积的明暗关系摆对。这个设计是有讲究的光照本质上是一个低频量缩到1/8分辨率后网络更容易忽略高频纹理专注于“哪个区域更亮更暗”这种全局判断。到了全分辨率精修阶段跳跃连接又能把边缘轮廓信息带回来让光照图和实际物体边界贴合得更准避免出现整块泛光的感觉。早先我也试过直接用一个全分辨率的小网络做端到端回归效果很不稳定经常出现亮部更亮、暗部更暗的极端化整体对比度也不均匀。换成多尺度结构之后光照图的稳定性和边缘贴合度都明显好一截。坦白讲这个结构并不复杂更多的复杂度来自后面这几个损失函数怎么配合。如果你在自己的数据上试建议先用这个轻量结构跑通再根据瓶颈换更强的主干。2.2 三个关键损失重建、平滑、色彩一致性之间的平衡损失函数是这套方法里最需要花心思的部分。我最终用的是四个损失的加权和各自解决不同问题。重建损失用L1而不是MSE计算增强结果I_est和真值I_gt的逐像素误差。L1对略微偏移的强边缘惩罚比MSE温和得到的增强图细节更清晰不会糊成一团。平滑损失用在全分辨率光照图上直接对光照图的梯度取L1范数做全变分正则防止光照图被噪声污染成一堆毛刺。但全变分要是权重给高了会把光照图整体抹平暗部和亮部的分界也没了。这个矛盾我给了一个加权方案光照梯度的惩罚项乘上一个由输入图像梯度算出的自适应权重。输入图像纹理强的地方我允许光照图也有一点变化输入图像完全平滑的区域则强令光照图也必须平滑。这样既限制了噪声又保留了真实光影边界。色彩一致性损失是我后来加的。单一的光照估计网络如果只预测灰阶光照对偏色严重的场景会留下颜色误差尤其在蓝色调的夜景里很明显。我加的是在Lab空间对a、b色彩通道约束增强结果和真值在色度上不要差太远。这部分不参与亮度恢复只纠偏颜色。感知损失用LPIPS这个损失很能反映人眼观感。直接目的是避免出现那种“PSNR很高但看起来就是不对”的情况因为LPIPS是用深度特征比较两张图的语义和纹理距离。四个损失的组合我最终调成了总损失 L_rec 0.05×L_tv 0.2×L_color 0.1×L_perc。这几个系数不要想着从论文里抄它们跟数据分布和网络深度强相关我后面会讲怎么根据现象反过来调。2.3 训练数据的获得自建合成配对管线深度光照估计最容易被卡住的一步其实是数据。公开的真实低光数据集很难同时拿到同一场景的清晰正常光参考图就算有拍摄时间、机位、白平衡也会引入不一致的噪声。考虑到复现的确定性我最后选择以“合成欠曝数据为主、真实低光数据微调为辅”的路线。合成管线非常简单准备一批内容丰富的正常曝光高清图对每张图随机采样一个Gamma值通常在1.6到2.4之间把像素值做幂次压暗模拟不同程度的欠曝。再叠加两层扰动一层是随机亮度乘子模拟曝光补偿差异一层是轻度的高斯噪声或泊松噪声模拟高ISO下传感器读数波动。有些图还会额外做一点色温偏移让网络见过“暗且偏色”的输入。这样合成的优点很明显真值是干净的原图暗图和真值严格像素对齐没有任何注册误差训练时梯度信号非常稳定。缺点也明显合成分布跟真实夜景还是有gap真实欠曝图的暗部往往有大量结构性噪声和杂光。所以我的做法是先用大批合成数据训练到收敛再准备一小批真实低光照片用网络自己增强的结果作为伪真值做微调微调步数控制在总步数的10%以内防止学坏。这样兼顾了训练稳定性和真实场景的适应性。3. 从零复现一套可跑的欠曝增强管线3.1 环境准备与核心模型定义依赖比较简单Python 3.8以上PyTorch 1.10以上OpenCV、numpy、scipy再加一个lpips用来算感知损失。如果机器显存紧张可以把输入patch缩到192效果没有那么明显。核心模型我定义成一个轻量EncoderDecoder核心代码如下class ConvBlock(nn.Module): def __init__(self, in_ch, out_ch): super().__init__() self.conv nn.Sequential( nn.Conv2d(in_ch, out_ch, 3, padding1), nn.BatchNorm2d(out_ch), nn.ReLU(inplaceTrue) ) def forward(self, x): return self.conv(x) class IlluminationNet(nn.Module): def __init__(self, in_ch3): super().__init__() self.enc1 ConvBlock(3, 32) self.enc2 ConvBlock(32, 64) self.enc3 ConvBlock(64, 128) self.enc4 ConvBlock(128, 256) self.pool nn.MaxPool2d(2) self.coarse nn.Conv2d(256, 3, 1) self.up1 nn.Upsample(scale_factor2, modebilinear, align_cornersFalse) self.dec3 ConvBlock(256 128, 128) self.dec2 ConvBlock(128 64, 64) self.dec1 ConvBlock(64 32, 32) self.final nn.Sequential( nn.Conv2d(32, 3, 1), nn.Sigmoid() ) def forward(self, x): e1 self.enc1(x) e2 self.enc2(self.pool(e1)) e3 self.enc3(self.pool(e2)) e4 self.enc4(self.pool(e3)) coarse self.coarse(self.pool(e4)) d3 self.dec3(torch.cat([self.up1(e4), e3], dim1)) d2 self.dec2(torch.cat([self.up1(d3), e2], dim1)) d1 self.dec1(torch.cat([self.up1(d2), e1], dim1)) return self.final(d1) def enhance_via_illumination(dark, illu, gamma_adj0.4): illu illu.clamp(1e-4, 1.0) adjusted illu ** gamma_adj result dark / illu * adjusted return result.clamp(0, 1)注意编码最后下采样到输入图的1/8所以coarse预测的实际是1/8分辨率的粗略光照图解码阶段通过双线性上采样逐步恢复每级都对齐尺寸。这个网络显存占用很小输入256x256batch开到16基本没压力。3.2 合成数据管线的具体参数合成数据不要写死参数要随机化但随机范围需要根据目标场景调整。我做夜景Demo时用的默认区间参考下面的定义。def synthesize_underexposed(image, gamma_range(1.6, 2.4), brightness_range(0.8, 1.05), noise_sigma0.02): img image.astype(np.float32) / 255.0 gamma np.random.uniform(*gamma_range) dark np.power(img, gamma) brightness np.random.uniform(*brightness_range) dark dark * brightness noise np.random.normal(0, noise_sigma, dark.shape).astype(np.float32) dark dark noise return np.clip(dark, 0, 1), np.clip(img, 0, 1), gamma这里的Gamma范围1.6到2.4对应中等偏强的欠曝如果你的场景是极暗监控视频可以把上限拉到3.0但网络更难学了。亮度乘子模拟曝光补偿的细微差异噪声sigma给0.02对应大概ISO 800到1600的颗粒感不要给太大否则网络会把去噪任务也一起背下来反而干扰光照估计。训练时我对合成图做数据增强随机水平翻转、90度旋转、随机裁剪到256x256再随机对RGB三通道乘一个0.9到1.1的小扰动模拟白平衡误差。这类增强对光照估计任务意义很大因为网络必须学会在各种色温下仍然只看亮度结构不至于把色偏当作光照变化来弥补。3.3 训练策略与初始化技巧训练策略上我推荐“由粗到精低学习率”的组合。先用1/4分辨率或缩小patch训练50轮让光照估计的基本形状先稳定下来再恢复全分辨率微调这时候网络只需要学细化边缘收敛很快也不容易振荡。两阶段的优化器都用Adam初始学习率2e-4到3e-4之间用余弦退火慢慢衰减到1e-5。下降太快的LR会让光照图粗糙太慢又浪费时间。梯度裁剪几乎必须打开MaxNorm设置5.0。光照图输出经过Sigmoid之后天然落在0到1但前几轮网络很可能在暗部预测到接近0的值训练时一旦这些值出现在除法分母上梯度会瞬间爆炸。裁剪能保住训练不出现NaN。这里再提示一个关键习惯所有输入输出统一放缩到[0,1]区间不要做ImageNet风格的标准化。光照估计依赖的是相对亮度你把均值减掉之后暗部可能是负数网络学起来非常别扭。如果不想从随机初始化开始训可以先用一个通用的图像增强编码器做预训练冻结encoder的前两层只训练光照估计分支。这样能省不少时间起步Loss就低。没有现成预训练也没关系这个网络本来就不深从零开始训练也完全可行。3.4 增强后的处理与主观评价推理阶段并不是简单把光照图除回去。直接用dark / L_est很容易让本来就亮的区域过曝观感很差。我的做法是先给光照图做一次软压缩adjusted L_est 的 0.4 次幂然后增强结果 dark / L_est × adjusted。相当于把亮部光照压低一点同时放大暗部。这个0.4是我在自己测试集上反复试出来的推荐起点可以做可视化的滑块调范围0.2到0.6之间。增强之后还有两个轻量后处理值得做。第一个是在Lab空间对L通道做一次限制对比度自适应直方图均衡clipLimit设1.5窗口8x8用来把局部对比度拉回正常水平但强度一定要低——这个后处理强度一高就会出现灰蒙蒙的老照片感。第二个是做轻量去噪推荐用OpenCV的快速非局部均值去噪强度参数h设3左右只处理亮度通道。这一步能明显改善暗部噪点但别指望它能处理严重噪点去噪强度太大会把增强出来的纹理又抹掉。评估时我同时看客观指标和主观截图。客观指标用PSNR、SSIM和LPIPS三个就够了PSNR看像素误差SSIM看结构LPIPS看人眼感知。主观上重点看三处暗部区域是否还有一团死黑、亮部边缘有没有光晕、以及肤色和天空色有没有偏色。我自建测试集上的实测结果如表所示可以当参考数量级不要拿去跟别人的榜单比较因为测试集和训练集同源。方法PSNR(dB)SSIMLPIPSGamma校正20.30.840.137CLAHE21.50.860.121单通道光照估计23.80.890.084多尺度光照估计后处理25.20.920.057只能说在自己构建的数据分布下多尺度光照估计加自适应平滑和后处理这一整套确实能在量化指标和主观观感上都拉开差距。4. 实测评估与翻车现场训练光照估计网络的几个深坑4.1 光照图被“抹平”了怎么办第一次跑通时我特别开心训练Loss真往下走了PSNR也不错。但把增强图放大一看问题来了——整张图的明暗关系被压得特别平均远处路灯周围应该很亮结果跟昏暗小巷差不多亮本应该有立体的树影结果全部成了一片。这就是光照图被“抹平”了。原因是全变分平滑损失权重太大。我一开始把L_tv设成0.3网络发现最省事的方式是让光照图接近常数反正L1重建损失在暗部也能凑合。后来我把L_tv权重降到了0.05并且改成用输入图像梯度做加权光照图在纹理密集区域允许发生变化在平滑区域必须平滑。这个改动立竿见影光照图重新出现了正确的大面积明暗层次局部细节也不再发毛。如果你也出现“增强后整张图灰度均匀”先检查的应该就是这个平滑项不要把锅甩给网络结构。4.2 颜色偏成“青灰风”三通道光照还是单通道光照另一个高频翻车现场是颜色。早期版本我只预测单通道光照整张图所有通道除以同一个数理论上是保色相的但实际增强出来总是偏青偏灰。仔细分析后发现网络只看到亮度信息遇到夜景里的有色光源时会用一个折中的全局系数去补结果R通道和B通道的差距被拉错了方向场景里的暖色灯光全变成了苍白一片。于是我把输出改成3通道光照图让每个颜色通道各自估计光照倍数。这样暖光源周围的光照图会在R通道亮一点B通道暗一点色相确实保住了。但副作用是网络胆子太大经常把颜色补偿过头出现明显的黄褐或紫红边缘。最终我采取的是混合方案仍然输出3通道光照但训练时在色彩一致性损失里加一个不太严格的约束允许光照图有三个通道但要求三个通道之间的比例不要太极端。这个折中让颜色恢复准确又没有过度校正的问题。4.3 增强把噪点也放大了欠曝图本身信噪比就不高而除光照这个操作本质是给暗部乘了一个大于1的增益噪点必然跟着被放大。第一版结果出来暗部全是彩色噪点观感比增强前还差。我尝试过把低通平滑直接加到增强结果上但细节也一起糊了。真正有效的是两条路一起走。第一条是在合成数据里加入适量噪声让网络在训练阶段就见过带噪输入迫使光照估计分支对噪点产生抑制第二条是在推理阶段增强后再做一次轻量去噪。另外还有个被很多人忽略的细节先对输入图做一个极轻微的去噪再去估计光照图光照图会干净很多最终增强结果的噪声控制要更好。顺序很重要我试过在增强结果上做很强的去噪细节保留反而更差因为噪声早就被增益放大了。4.4 训练震荡学习率、归一化、梯度裁剪训练过程还有一个经典问题Loss曲线开始正常下降到第20轮左右开始剧烈震荡甚至跳出NaN。我逐项排查下来最大嫌疑是学习率偏高和光照图数值过小。学习率从5e-4降回3e-4之后震荡明显缓解梯度裁剪加上之后NaN几乎不再出现。归一化方式这里要专门强调一遍。光照估计网络的输入最好保持0到1的相对亮度不要做减均值除方差的标准化。标准化会破坏“暗部接近0”这个天然表征而光照预估的物理含义就是暗部系数很小。我自己就是把输入直接除以255喂进去输出经过Sigmoid约束到0到1一切正常。如果你迁移了别的检测模型习惯把输入做成标准化很可能要踩一个很大的坑且很难从Loss上看出问题只会发现增强结果怪怪的。遇到振荡先按这个顺序排查学习率、输入取值范围、除零保护、梯度裁剪。写到这里这套欠曝增强流程就算完整了。坦白说这个方向我在真正动手之前也以为难点在网络结构实际做完才发现光照图的质量、损失权重的平衡、数据合成方式、后处理顺序每一个环节都比网络本身更影响最终观感。尤其是“先轻噪再估光照”和“自适应加权TV”这两个点是我在多次返工里沉积下来的经验直接拿过来用能省不少排查时间。如果你也在做类似方向的增强管线建议先按这里的参数跑通一个小规模测试再根据自己的目标场景调整Gamma范围和损失权重。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑