资讯动态

HDRNet复现笔记:双边网格与深度学习实时图像增强

发布时间:2026/10/15 13:25:13 来源:尧图企业网站定制
最近把SIGGRAPH 2017那篇经典的“Deep Bilateral Learning for Real-Time Image Enhancement”通常叫HDRNet重新翻出来复现了一遍感慨挺多。这篇论文算是打通了“全局语义理解”和“局部像素级操作”的一个典型代表用一个不到1M参数的网络跑通了1080p图像的实时增强放在今天看依然有很强的借鉴意义。这篇笔记不打算做成简单的论文翻译而是从“为什么需要这个结构”“每个模块背后在解决什么问题”“复现时哪些坑最值得注意”这三个角度来聊适合正在做实时图像处理、移动端视觉算法或者打算把深度学习模型落到视频/相机管线上的人参考。1. 论文到底在解决什么问题1.1 传统图像增强的痛点图像增强这个老话题从色调映射Tone Mapping、对比度调整、局部对比度压缩到美白、去雾、夜间降噪本质上是把一个输入像素的值通过某种映射关系变成输出像素的值。过去很多经典算法效果不错但大多依赖复杂的优化过程或者局部统计信息一次处理可能需要几十毫秒甚至上百毫秒。比如不少色调映射算法要构建亮度金字塔在多层尺度上做引导滤波跑一张几百万像素的图在普通CPU上很难达到实时。而视频流要求每帧处理时间严格可控手机端的算力又远低于桌面GPU所以工业界一直没有特别好的统一解法。1.2 深度学习方案为什么也不是直接搬就行用CNN做图像到图像的映射直观想法是直接做一个encoder-decoder结构输入整张图输出增强后的整张图。但这有两个致命问题。第一高分辨率图像直接进CNN显存和计算量都吃不消训练时缩到小图推理时又希望高清两者之间天然存在scale gap。第二像素级的颜色变换很“局部”但好的增强效果往往要依赖“全局”的语义信息——比如一张夜景照片暗部该不该提亮取决于场景是城市夜景还是星空这需要网络理解图像内容。可是语义理解又需要较大的感受野而能够保留细节的精细操作又要求高分辨率特征这两者在传统CNN结构里很难兼得。1.3 HDRNet的核心思路把处理放低分辨率把映射放高分辨率HDRNet最聪明的一点是将计算拆成两条路径一条路负责理解和决策基于低分辨率输入生成一个“双边网格”Bilateral Grid另一条路负责保留细节直接拿原始高分辨率图像作为引导去采样这个网格得到最终每个像素的颜色变换系数。这样既获得了大感受野的语义信息又保留了高清细节。低分辨率路径的计算量可以压得很低高分辨率路径只是查表和插值天然适合并行加速。这就是整个网络的基本哲学决策在云端之心实操在像素指尖。2. 网络结构深度拆解三支分工一次优雅的空间换时间2.1 三条子网络的职责HDRNet整体分为三部分一个低分辨率输入的特征提取网络Identity一个产生双边网格系数的网络Lookup以及一个最终指导上采样的引导网络Guide。用论文里的话说就是 identity network 处理内容、lookup network 生成变换系数、guide network 保持空间边缘。这种分工很像团队协作有人负责分析场景有人负责定策略最后有人负责具体执行。特征提取网络的输入是低分辨率版本的图像例如原始分辨率缩小到宽256或高256通过几层卷积提取语义特征。这里的特征图分辨率很低所以可以用较多的通道例如64或128而不会造成计算量爆炸。再往后一个像素级别的小网络基于这些特征预测若干组系数这些系数最终被填充到双边网格中。2.2 双边网格是什么为什么要用双边网格双边网格可以理解成一个三维的查询表3D LUT。传统颜色映射LUT是输入色值到输出色值的映射维度是通道数例如RGB三通道的3D LUT。但是这样完全没有考虑像素的空间位置和局部邻域结构。双边网格额外加入两个空间坐标x和y或者说它把图像看成一个三维的体数据空间位置和颜色值。网格中每个格子保存着一个仿射变换系数例如3x3的线性变换矩阵和偏移项当我们需要处理某个像素时就通过它的空间坐标和颜色值去这个3D网格中做插值采样得到一个针对该像素的变换参数。用生活类比来说传统LUT像是全体人员都按统一要求填一张表但不同人具体情况差很远导致很多人不服。双边网格则是一个“分区域分类型”的管理方式把图像分成若干空间区域每个区域又按颜色白黑、亮暗分成几档每档都有自己的调整策略。所以它能同时感知“这个像素在画面中的位置”和“这个像素本身的颜色亮度”从而在保持局部边缘的前提下对内容做自适应变换。2.3 从低分辨率系数到全分辨率映射切片与上采样论文中预测的并不是最终增强后的像素值而是一组“仿射变换系数”。具体来说对于每一个像素网络输出的是3x4矩阵或者论文中的9个转化通道加3个偏移项作用在输入RGB向量上得到变换后的RGB值。为什么用线性仿射变换而不是直接预测像素值呢因为高分辨率路径上我们没有深度特征只有原始图像能用线性变换表达的操作范围很广比如颜色矩阵、增益、gamma都可以近似。而且线性变换非常稳定不容易出现生成图像中常见的色彩伪影。高分辨率路径的操作过程是这样的先将原始高分辨率图缩小到低分辨率输入网络生成双边网格系数的体数据。然后对原始高分辨率图像中的每个像素计算它对应的空间坐标和颜色坐标在这个体数据里做三线性插值取出该点的3x4系数和原始像素值做矩阵乘得到输出。整个插值过程用GPU实现非常简单例如写成可微的自定义算子前向和反向都容易实现。2.4 关键参数与计算量估算论文中的网络参数量比较小通常不到100万以我们复现时的实际统计来看大约在几十万个参数到一百万之间取决于具体的层宽和网格深度。双边网格的分辨率通常设为宽高各16到32格颜色维深度从8到32不等。例如常用的配置是16x16x16也就是4096个格子每个格子存储一组3x4变换参数整个系数体量大约4096*12个浮点数这和特征图动辄几百个通道相比小得多。计算量的重头其实在高分辨率的上采样查表阶段。不过这个操作没有卷积只是按像素做插值所以在GPU上非常快。实际测试中处理1080p图像在现代桌面GPU上单帧大约能到2到4毫秒移动端GPU也能做到十几毫秒这已经接近实时。更妙的是这个结构天然支持动态分辨率你想保速度就降低网格分辨率或者用半分辨率输入。3. 训练细节与损失函数为什么不能只看重建误差3.1 训练数据的一般构造方式HDRNet原论文训练时用的是HDR到LDR或有参考的增强图像对简单理解就是输入普通曝光图输出目标版本的增强图。实际上训练图像对的获取有很多讲究。我们普通复现时可以采用公开的曝光序列数据集把曝光不足和正常曝光或经过HDR合成的结果组成训练对。更实用的做法是用一个手动调整好的图像处理管线的结果作为ground truth让网络模仿这个管线。这样你不需要重新标数据只要你有风格一致的处理流程就可以造出大量训练对。3.2 损失函数组成与权重示意图HDRNet的主体损失是一个加权的最小二乘损失也就是输出图像和Ground Truth逐像素做差平方然后乘以一个空间权重图。权重图往往用来关注重要的图像区域比如边缘区域、高饱和区域、人脸区域等避免网络为了平均误差表现而牺牲了关键部分。论文里也提到了可以额外用一个小分支来预测权重图但核心还是重建损失。我自己复现时发现一个容易翻车的点如果只用L2损失网络很容易学成整体亮度偏移的均值解细节恢复得不够锐利。这时可以加一个小的感知损失比如在VGG特征上做L1比较或者加一个梯度差分损失让输出保留输入的梯度结构。不过加感知损失会增加训练难度和显存开销实际项目里最好是先将L2收敛好了再微调加入感知损失。3.3 训练时还需要注意数值稳定性双边网格里存的是仿射系数这些系数可能一个不小心就会导致输出颜色过饱和或直接数值溢出。训练时建议对输入做归一化同时把输出范围钳制在0到1或-1到1之间。另外由于网格采样相对来说“结构化”很强在小批量训练时如果batch size太小类似网格位置的参数更新会很抖。我的经验是至少用8以上的batch size配合Adam优化器初始学习率在1e-4左右比较稳。4. 复现实操指南从零搭建一个精简版HDRNet4.1 环境选型和模型框架我复现时用的是PyTorch实现了低分辨率特征提取、系数体生成和切片上采样三个模块。整体框架不复杂核心大概只有两百行左右的有效网络代码。需要注意的是切片上采样的自定义算子如果用纯PyTorch的grid_sample实现会有一定的性能损耗但训练阶段无所谓。推理阶段可以换成CUDA实现或者TensorRT加速。下面给一个精简的网络结构伪代码示例只展示核心逻辑便于大家理解。import torch import torch.nn as nn import torch.nn.functional as F class HDRNet(nn.Module): def __init__(self, grid_h16, grid_w16, grid_d16, guide_channels3): super().__init__() self.grid_h, self.grid_w, self.grid_d grid_h, grid_w, grid_d # 低分辨率特征提取网络 self.low_features nn.Sequential( nn.Conv2d(3, 24, 3, padding1), nn.ReLU(inplaceTrue), ) # 生成网格系数的卷积层输出 3*4 12 self.to_grid nn.Sequential( nn.Conv2d(24, 16, 3, padding1), nn.ReLU(inplaceTrue), nn.Conv2d(16, 12, 3, padding1), ) # 引导网路也可以省略或共享高分辨率细节 self.guide nn.Sequential( nn.Conv2d(3, 8, 3, padding1), nn.Conv2d(8, 1, 3, padding1), ) def forward(self, low_res, full_res): h, w full_res.shape[-2:] low F.interpolate(full_res, sizelow_res.shape[-2:]) feat self.low_features(low) # 在低分辨率上生成网格系数 coeffs self.to_grid(feat) # [B,12,H,W] # 构建3D网格reshape插值到 grid_h,grid_w,grid_d grid self.make_grid(coeffs) # [B,12,grid_d,grid_h,grid_w] # 根据full_res的坐标做三线性插值得到每个像素的系数 transformed self.slice_and_apply(grid, full_res) return transformed4.2 双边网格的建立和采样实现细节这一步是整个模型的核心。双边网格的维度是可以调节的一般宽高方向对应低分辨率特征图的空间维度深度方向对应颜色维度。颜色维度的坐标值需要从输入图像的颜色值归一化后映射到[0,1]然后线性变换到[0, grid_d-1]。空间坐标则直接使用低分辨率特征的坐标网格。建立网格时需要将低分辨率特征中的每一个像素按其空间位置和颜色值加权扩散到网格的相邻格子然后再进行归一化。这样做是为了保证颜色维度上采样时能平滑过渡。在PyTorch中实现slice这一步可以借助grid_sample。我们把体数据作为输入把每个像素的归一化坐标做grid_sample取出对应的12通道系数。grid_sample默认使用双线性插值对于3D数据使用5D输入和5D网格不过手写一个遍历式实现更容易理解。简单来说就是把网格系数的体数据切面上采样回原分辨率原分辨率图像的每个像素的RGB数值决定了它落在哪个颜色深度切面上。4.3 训练与推理的差异低分辨率输入从哪里来训练时最理想的模式是先将原图随机裁剪成一定大小比如256x256然后缩到低分辨率如64x64作为低分辨率分支输入同时把256x256原图作为高分辨率分支输入。但这里有一个很多人踩过的坑低分辨率输入不能直接用随机裁剪块缩小因为这样会丢失全局背景信息。建议在训练时使用完整图缩放后做低分辨率输入而高分辨率分支还是用原图或者较大裁剪块这样网络能真正看到全局内容避免局部块导致的语义偏差。推理时低分辨率输入就直接把完整帧缩小到固定尺寸比如256x256高分辨率分支就是原始画面。如果你想追求更高的速度可以再把低分辨率缩到128x128或者降低网格深度但效果会略差。实测下来在保持网格16x16x16不变的情况下低分辨率输入从256降到128速度能提升约30%但局部对比度保留会弱一些。4.4 代码实现时容易忽略的细节有几个小地方容易影响最终效果。第一是低分辨率分支的输入需要进行颜色空间归一化很多图像处理网络的输入是0到255但这里建议统一到0到1后再输入否则网格颜色维度的坐标会严重溢出导致颜色断层。第二是网格的深度方向采样时默认的网格深度是16你可以尝试32但深度多一倍意味着体数据多一倍显存和带宽也会上浮。第三是仿射系数中的偏移项bias在学习时最好加入一个小的权重衰减或者单独控制学习率防止网格中某些区域偏移过大导致输出太亮。5. 常见问题与排查技巧实录5.1 输出图像出现明显色块和网格感这个问题的根源通常是双边网格的空间分辨率太低比如网格只有8x8而高分辨率分支直接使用原图坐标采样就会导致相邻像素采样到同一格形成块状感。解决思路有两个一是把空间网格提到16x16或32x32二是在采样前对系数体做了平滑比如对网格深度方向做一次高斯模糊。倒是可以学习一下传统双边网格的方法在网格建立时用更平滑的分摊权重不要硬塞。5.2 训练loss能降但增强效果很弱一般表现为模型输出的图像和输入差不多没有明显的风格改变。这往往是网络偷懒了认为“直接拷贝输入”就能获得低loss。尤其当训练数据里来源图像和目标图像差异大的样本占比不足时模型就会走捷径。解决方法适当增加目标图像变换强度或者在损失项中加入输出与输入差异的一个正则项强制网络学会做非恒等变换。从一个我自己的经验来看把单调的L2 loss改成L1L2混合通常可以减少这种退化。5.3 处理视频时会出现闪动HDRNet在单帧上效果不错但视频帧与帧之间参数抖动会导致闪烁。因为网络每次处理一帧完全不知道前后帧的信息。要缓解这个问题可以在推理时对网格系数做时间域低通滤波也就是把上一帧和当前帧的网格系数做指数滑动平均。这个操作开销很小因为网格本身数据量不大但效果立竿见影。另外也可以把输入的低分辨率特征拼接一帧差分信息让网络感知运动幅度。5.4 网格数值溢出导致的NaN训练初期偶尔会遇到loss变成NaN一般是因为网格系数过大。网格的某些位置缺乏有效训练数据更新时可能走飞。我常用两个手段一是把网格系数输出接一个tanh激活函数限制取值范围二是给系数体加一个很小的L2正则相当于给“没见过的颜色区域”设置一个默认恒等变换的先验。5.5 不同设备的性能差异过大这个问题在移动端部署时尤其突出。HDRNet的slice操作是访存密集型在台式机GPU上用显存宽带换性能但手机GPU的访存带宽和内存延迟都不如桌面导致速度下降明显。建议针对移动端做两件事一是把网格数据的通道顺序调整成“空间优先”配合Cache命中二是把slice过程完全改成像素着色器实现不经过深度学习框架这样能进一步压榨性能。6. 从论文到实用HDRNet的扩展与思考6.1 延伸应用场景HDRNet最初始提出是面向HDR渲染和曝光增强但它的结构可以泛化到很多任务。目前在手机端常见的实时夜景降噪、单像素色彩校准、饱和度调节、美颜中的肤色调整本质上都可以套用这个框架。低分辨率分支负责理解图像的场景比如是人像还是风景高分辨率分支负责对每根发丝都做精细调整。而且我们不需要更换主体模型只需要收集不同的输入输出训练对就能让模型学会新的风格。我在自己的项目中还尝试过把HDRNet秒变“实时风格迁移”用一张艺术风格化的输出图作为训练目标输入真实照片训练完成后模型就能实时把视频帧变成风格画的质感。由于高分辨率分支只做线性仿射变换输出画面的边界不会出现明显扭曲风格迁移的稳定性比单纯encoder-decoder要好很多。6.2 后续衍生思路多头网格与注意力机制HDRNet的网格是单一的三维LUT表达能力有限。后来有不少工作通过多组网格、多尺度网格或者用注意力机制动态融合网格系数来提升效果。我们也可以手动改造把lookup网络输出为多个网格计算每个网格的注意力加权权重然后再对原始图像做多组仿射变换。这样的好处是不同颜色区域分别使用不同的模型参数相当于在网络内部做了“专家混合”在复杂光照条件下效果比单一网格更稳。6.3 落地时的效率优化实际工程落地时模型推理瓶颈往往不是卷积而是slice操作。如果使用GPU可以考虑将其合并到一个小型的自定义kernel中例如每个线程处理一个像素直接在常量内存中读取网格数据进行插值。如果使用CPU则建议把slice操作转化成线性查找表操作并用多线程并行按行处理。总体来看HDRNet之所以能成为经典不是因为它有多花哨的结构而是因为它把图像滤波里的空间/颜色联合边缘保持思想成功地“深度学习化”了。7. 复现之外的一点心得这篇论文我前后读过不下五遍每次都会有新的理解。一开始觉得它只是个巧妙的工程网络后来才发现它揭示了深度学习图像处理中一个很实用的诀窍“如果某一步很贵就不要在全分辨率上做如果某一信息很重要就在小分辨率上充分表达。”这件事说起来容易做起来难但HDRNet做到了。在实际复现过程中最让我惊讶的是这样一个参数少得可怜的网络居然能产生那么锐利的边缘和那么强的新奇感。这也促使我反思我们在设计深度学习模型时是不是太容易无脑堆叠卷积层和上采样模块了。有时候传统的图像处理思想——比如双边滤波、引导图、颜色LUT——反而能给我们提供更清晰的结构思路。换个角度看HDRNet其实是把可微的双边网格当作了一种通用表达这才是它最大的贡献。如果你正在做实时美颜、视频滤镜、或者任何低功耗图像处理任务我真的很建议你把HDRNet源码扎扎实实读一遍自己动手写一下slice和splat。动手写一遍之后你对“如何在低算力条件下保留高分辨率细节”这件事的理解会上升一个台阶。之后再做模型压缩、剪枝或者NAS的时候你也会更容易判断哪些操作是不可避免的哪些操作其实是可替代的。最后分享一个小技巧。如果你只想快速验证HDRNet的效果而手头又暂时没有成对数据可以先拿一张高清图用任意图像处理软件生成一个“增强后”的版本然后做一个只有一对样本的过拟合实验。如果在这个极端情况下网络都能从模糊中恢复出合理的图像边缘说明你的实现基本是对的。这个小实验花不了几分钟但能帮你快速排除代码里的低级错误。祝你复现顺利。

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

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

免费获取报价 →
↑