
边缘设备上的实时图像融合听起来是个很美好的方向但真落地的时候能把人折腾疯。尤其是多曝光、多聚焦、红外与可见光这类多路输入场景边缘设备的算力、内存带宽、NPU算子支持度全都卡着你。LUT-Fuse 这个名字核心思路其实就一句话把本来要在设备上实时跑一遍的融合网络通过可学习查找表和蒸馏技术提前“压”成一张表推理时只做查表和插值。这样既保住了深度学习的融合质量又能在几乎没有 GPU 的设备上跑到实时帧率。这篇文章我想把 LUT-Fuse 的技术路线完整拆开来讲覆盖从动机、原理、蒸馏设计到实际部署的整个流程。适合正在做边缘端图像融合、HDR 合成、多传感器融合的算法工程师也适合想了解如何把 CNN 能力“塞进”低成本硬件里的嵌入式开发者。文章里所有的参数和流程都来自我实际做过的项目不是纸上谈兵。1. 为什么边缘设备上的图像融合这么难1.1 边缘设备的硬件现实先看一下边缘设备是什么样的。以常见的 RK3588、树莓派 CM4、各种 IPC SoC 为例CPU 算力尚可但跑深度学习主要靠 NPU 或者低功耗 GPU。NPU 的问题在于算子支持非常有限很多在 PC 上很常见的操作比如动态索引、稀疏卷积、自定义插值根本没有实现。就算某些算子能跑你也得花大量精力做图优化和量化否则推理时间完全没法看。另外一个容易被忽略的瓶颈是内存带宽。图像融合不像分类输入不是一张 224×224 的小图而是两路甚至三路 1080P 的大图单帧光是把数据从 DDR 搬到计算单元就要消耗大量带宽。如果算法是多尺度或需要反复上下采样的带宽占用会进一步放大。很多 PC 上无所谓的问题在边缘设备上就是致命伤。所以做边缘端图像融合第一原则就是能一次遍历完成的绝不做多轮能查表的绝不实时计算能低精度的绝不用高精度。LUT-Fuse 正好把这几个原则全占了。1.2 传统算法和深度学习算法的算力账本传统多曝光融合常用金字塔方法。拉普拉斯金字塔、高斯金字塔每一层都要做卷积、下采样或上采样然后逐层混合再重建。说起来简单但实际代码跑起来处理一帧 1080P 图像几百毫秒是常事。如果帧率要求 30 FPS基本直接出局。深度学习方法倒是提升了融合质量但算力代价更大。一个典型的 U-Net 融合网络输入两路图像算下来单次前向推理可能需要几十 GFLOPs。边缘设备算力普遍在 1-5 TOPS 之间而且实际利用率很难达到理论峰值。我实测过一个大概 8 GFLOPs 的轻量融合网络在 NPU 上跑 1080P 输入单帧推理就要 30-40 毫秒这还没算前后处理。想再同时处理多路输入帧率只能掉到 10 FPS 以下。传统查表法的算力账本则完全不一样。LUT 的查询复杂度是 O(1)每像素只做几次内存读取和一次插值。以三线性插值为例每个像素大约需要 8 次查表、8 次乘加总共算下来也就是几十个操作比动辄上万个乘加的 CNN 低了两个数量级以上。1.3 什么样的场景需要这种实时融合我能想到的典型场景至少有三个。第一个是多曝光连拍合成 HDR。手机拍照、监控抓拍中为了兼顾亮部和暗部细节会连续拍多帧然后把它们融合成一张动态范围更高的图。这个场景对延迟敏感用户按下快门就要看到效果。第二个是多聚焦融合。显微镜自动对焦、工业检测中常需要把不同焦平面的图像合成一张全清晰图像。操作员在调焦环上转动时画面必须实时更新否则没法做精细判断。第三个是红外与可见光融合。夜视设备、无人机巡查、安防监控里需要把红外热成像和可见光图像融合让画面既有热辐射信息又有结构纹理。这类设备往往功耗有限不可能挂一块大 GPU。这些场景都有个共同点计算设备能效受限、实时性要求高、融合质量还不能牺牲太多。所以 LUT-Fuse 才会把“可学习查找表”和“蒸馏技术”这两个看似独立的技术结合到一起来解决问题。2. LUT-Fuse 的核心思想把“算”变成“查”2.1 查表法在图像处理里的老传统查表法本身不是什么新东西。图像处理领域最经典的 LUT 应用就是伽马校正和颜色分级。因为映射关系固定提前把输入值对应的输出值算好存成一张表运行时直接读表连乘法都不需要。一台很老的嵌入式设备都能轻松处理 4K 视频靠的就是这个思路。但传统 LUT 有个明显的局限它只能处理逐点映射。伽马校正是每个像素自己算自己的不需要看周围像素。而图像融合本质上是个局部问题——我能不能在亮暗剧烈变化的边缘处保留细节取决于局部邻域的结构不能只看单个像素。之前很多人试过用静态 LUT 做融合效果都不好原因就在这。融合权重需要感知局部结构简单的逐点映射表达不了这种非线性关系。这是传统 LUT 遇到的根本瓶颈。2.2 可学习 LUT 是怎么设计出来的LUT-Fuse 要解决的核心问题就是让 LUT 拥有“感知局部结构”的能力同时仍然保持查表的效率。思路其实很直接既然融合权重可以看作图像局部特征的一个函数那我先把每个像素的局部统计量算出来比如邻域均值、局部方差、梯度幅值。这些特征虽然不像深层语义特征那么抽象但它们对融合来说已经足够——曝光是否过度看局部亮度纹理是否清晰看局部梯度和方差这些都是现成的物理量。然后把这些特征量化成一个有界网格网格的每个顶点存一个融合权重向量。推理时对于每一个像素根据它的局部特征找到网格里的对应位置再做一次多维插值就能得到融合权重。这个网格就是一张可学习的 LUT。它不是在推理时由网络算出来的而是在训练阶段就已经固化了。为什么叫“可学习”因为网格顶点不是人工设定的而是经过训练得到的。你可以把这张 LUT 理解成一个人工神经网络在大规模数据上学习到的映射关系只不过这个映射关系被固化成了查表形式。2.3 查表融合的复杂度到底有多低具体算一笔账。假设我们用三张特征图来控制融合权重局部亮度、局部方差、梯度幅值。每个特征量化到 33 级那么 LUT 的规模是 33×33×33再乘上融合路数一般来说就是三五万个顶点参数。这个体量非常小完全可以放进 CPU 的 L1 Cache 里查询时几乎不会有内存缺失开销。以两路 1080P 输入为例融合时每像素的计算包括计算 3 个局部特征、一次三线性插值求权重、一次归一化、两次乘加融合。我用纯 C 语言在树莓派 4B 的单核 CPU 上实现过局部统计部分用积分图加速整帧处理大概 5-8 毫秒。这个速度在深度学习方法看来是不可思议的但它就是这么发生了因为算法复杂度从“网络推理”降级成了“内存读取加少量插值”。关键点在于LUT-Fuse 并不牺牲融合质量。融合质量由生成 LUT 的训练阶段决定只要训练得好查表阶段的精度损失只在插值误差范围内。3. 蒸馏技术把大模型的“脑子”传给轻量模型3.1 为什么不能直接训练一个小 LUT有人可能会问既然 LUT 顶点是学出来的我为什么不直接用一个小网络端到端地学这个映射还要绕一道蒸馏对这个问题我一开始也踩过坑。直接端到端学一个从局部特征到融合权重的映射理论上可行但实际上会遇到两个麻烦。第一个是拟合能力不足。小网络本身容量有限而融合权重的分布在边缘处变化非常剧烈在平坦区域又比较平缓。小网络很容易为了拟合高对比边缘而牺牲平坦区域的平滑性最后出来的融合图有明显的人工痕迹。第二个是训练不稳定。局部特征和融合权重之间的关系并不是一个简单函数。不同曝光条件下的同一种局部特征对应的融合权重可能差别很大。没有强有力的监督信号小网络很难学到其中的规律。所以需要蒸馏。让一个大网络先学会融合再让这个小映射网络去模仿大网络的行为。这样小网络不用自己去理解哪些关系重要、哪些关系不重要只需要把大网络已经学到的映射尽可能缩到一个低维表征里。3.2 蒸馏目标设计LUT-Fuse 中的蒸馏和分类任务里的 logits 蒸馏不太一样。融合是回归任务蒸馏目标不能简单用交叉熵来衡量。我实际使用的蒸馏损失由三部分组成。第一项是像素级 L1 损失。让学生网络输出的融合权重图乘以输入图像后得到的结果与教师网络输出结果的像素差异尽量小。这里我直接用融合结果做监督而不是对权重图本身做监督。因为两个网络如果内部表征方式不同权重图直接对齐没有意义但融合结果是对齐的。第二项是结构相似性损失也就是 SSIM 损失。只拉近像素均值还不够还要在局部结构上一致。比如边缘的锐利程度、纹理区域的对比度这些用 SSIM 来度量更合适。我设置的权重一般是 0.5。第三项是感知损失。用 VGG 网络的高层特征来度量两个结果的语义差异。感知损失能避免出现“像素数值差不多但看起来就是不对”的情况。感知损失的权重比较小我通常给 0.1。蒸馏过程的整体流程是这样的先在数据集上训练一个完整的大模型作为教师。教师网络可以是任意结构我用过类似 U-Net 的编码解码网络效果不错。然后把教师网络固定只训练一个极轻量的学生网络。学生网络输入的是局部特征图输出的就是融合权重图。整个蒸馏过程大约只需要教师网络三分之一的训练时间就能达到很高的相似度。3.3 蒸馏和 LUT 是如何配合的蒸馏和 LUT 并不是两个独立模块它们之间有严格的先后逻辑。蒸馏的产物是一个训练好的轻量学生网络但边缘设备最终需要的不是这个网络而是一张可以直接查表的 LUT。所以在学生网络训练好之后我会做一个“枚举采样”的操作把局部特征空间中所有可能出现的离散点全部送进学生网络推理一遍把对应的融合权重存成 LUT。这样做的好处是LUT 的每一个顶点都经过了学生网络的推理等于把网络的知识完整转移到了表里。推理时不再需要网络只需要查表。这也是我为什么强调学生网络结构要刻意设计成“适合枚举采样”的形式。如果学生网络是一个普通 CNN输入是局部特征图忍受不同茫然互相干涉那么枚举时需要考虑的感受野组合就会爆掉。但如果你把学生网络设计成逐点的多层感知机加少量卷积那么每个输出只依赖一个空间点的特征枚举每个特征组合就能生成完整的 LUT。所以 LUT-Fuse 的全链条是大模型学习融合 - 蒸馏成轻量学生 - 枚举学生输出生成 LUT - 边缘设备只加载 LUT。每一步都是上一步的产物缺一环都不行。4. 实操落地从训练到部署的完整流程4.1 教师网络训练先说数据。图像融合任务的数据集最常见的是多曝光图像序列。我用过公开的 SICE 数据集也自己从相机连拍里整理过一些序列。关键要求是每一组样本里的图像必须已经做过像素级对齐。如果没对齐教师网络学到的是“重影融合”后面蒸馏再怎么做都是错的。教师网络我用的是一个轻量级 U-Net 变体编码器三层解码器三层输入是两路图像的堆叠输出是融合权重图。损失函数是像素 L1 加 SSIM 加感知损失的组合。训练时输入尺寸取 256×256随机裁剪batch size 设为 8学习率从 1e-4 开始按余弦退火降到 1e-6一般训练 80 到 100 轮。教师网络的输出质量是整个方案的天花板。学生网络最多只能逼近教师不可能超越。所以这个阶段不要图省事如果教师本身融合质量就不行后面所有环节都是白做。4.2 学生网络的蒸馏配置学生网络我推荐设计成“统计特征提取 逐点映射”的结构。统计特征提取部分可以是一层 3×3 卷积加一个积分图模块输出局部均值、方差、梯度三个通道。逐点映射部分就是一个三输入、双输出的多层感知机隐藏层 32 个节点。整个学生网络参数量不到 10K比教师网络小三个数量级。蒸馏时把教师输出作为回归目标。我实际用的超参数是学习率 1e-3batch size 32patch size 128。做蒸馏时有个细节不要在每轮都换随机裁剪最好保持一组固定的样本对让教师输出缓存下来。否则每次迭代都要跑一遍教师网络训练速度会慢得怀疑人生。蒸馏温度的设置在融合任务里跟分类不太一样。分类里温度是用来软化概率分布的融合任务里没有概率分布但我还是引入了一个“平滑因子”对教师输出的权重图做一个小尺度高斯模糊再作为监督目标。这样做能避免学生网络去死记教师输出中的高频噪声从而保证后续生成的 LUT 更平滑。平滑因子我一般取 0.8太大会把边缘结构也抹掉。4.3 生成 LUT 的细节学生网络训练完成后就到了把网络变成 LUT 的关键步骤。这一步看起来简单但坑很多。需要先确定局部特征的取值范围和量化级数。以三特征 LUT 为例局部亮度范围是 0 到 255方差范围我截断到 0 到 255梯度范围也截断到 0 到 255。量化级数不是越多越好我用过 65 级也用过 17 级。65 级确实更精细但容易在数据稀疏区域出现过拟合17 级计算量虽然小但插值误差会导致融合权重不够准。经过对比33 级是比较均衡的选择。下面是一段生成 LUT 的示意代码核心思想就是遍历特征空间中所有网格点把特征组合送入学生网络得到权重然后存成多维数组。import numpy as np N_LEVELS 33 # LUT维度: [亮度级, 方差级, 梯度级, 输出权重数] lut np.zeros((N_LEVELS, N_LEVELS, N_LEVELS, 2), dtypenp.float32) for i in range(N_LEVELS): for j in range(N_LEVELS): for k in range(N_LEVELS): brightness i / (N_LEVELS - 1) # 归一化到 0~1 variance j / (N_LEVELS - 1) gradient k / (N_LEVELS - 1) feat np.array([[brightness, variance, gradient]], dtypenp.float32) # 学生网络的前向推理得到两路图像的融合权重 w student_net(feat).squeeze().numpy() lut[i, j, k] w这段代码只是示意。实际工程里我会把学生网络转成 ONNX再用 ONNX Runtime 批量推理速度更快也不用反复构建 Tensor。另外要注意推理时如果学生网络的输入不是归一化到 0 到 1生成 LUT 时就必须用同样的预处理否则 LUT 里的值全是对不上的。生成完 LUT 后建议做一个预验证随机挑选几张图把 LUT 插值结果和学生网络直接推理结果做逐像素对比。误差应该非常小如果超过 1%多半是特征提取流程在训练和推理时不一致需要回头查。4.4 边缘设备部署时的推理流程部署阶段整个系统已经不需要任何深度学习框架了。只需要一份 LUT 数据和一段轻量的查表插值代码。推理流程分为三个步骤。第一步是计算局部特征。对两路输入图像分别计算局部均值、局部方差和梯度幅值。为了速度我全部用积分图和 Box Filter 实现复杂度 O(1)。这一步是整个流程里计算量最大的部分但很低。第二步是查表插值。对每个像素把三个特征值映射到 LUT 的网格坐标然后用三线性插值求出两组融合权重。插值代码如果用 C 写核心就是 8 个顶点权重的累加逻辑很直接。第三步是归一化和加权融合。因为不同位置的两路权重和不一定等于 1需要先对权重做归一化再对两路输入做加权平均。要注意的是这里的输入图像在做融合前需要对齐到同一坐标系否则边缘处会出现重影。我在 RK3588 上实测过这个流程纯 CPU 单线程处理 1080P 两路输入整帧耗时大概 10 毫秒左右如果开启多线程并行降到 6 毫秒没太大问题。这已经满足 60 FPS 的融合需求了而且 CPU 占用率还不高可以同时跑其他任务。5. 踩坑实录我遇到的几个典型问题5.1 局部特征选择比想象中更重要一开始我试着用五个维度的特征除了亮度、方差、梯度还有色度分量和局部熵。维度多了理论上能表达更精细的映射但实际效果反而差因为五维 LUT 的稀疏性问题非常严重。一张 33^5 的表理论上有三千九百万个网格点但真实图像只会覆盖其中很小一部分。那些没被覆盖到的网格点学生网络根本没见过类似输入输出的权重就是瞎猜的插值的时候一旦碰到就会产生异常融合结果。后来我把特征精简到三个亮度、方差、梯度。这三个特征对曝光和清晰度的刻画能力已经足够强而且 33^3 的表只有三万多个网格点每个点都可以被大量真实数据覆盖。结论就是LUT 的维度不是越高越好关键是每个维度都要能被数据充分覆盖。5.2 不插值的后果是条带伪影最早做部署时为了图省事我直接用最近的网格点的值当作融合权重没做插值。结果融合图像在亮度平滑变化的区域出现了非常明显的条带就像 JPEG 压缩过度那种感觉。原因很简单。LUT 只记录了网格点上的权重两个网格点之间的真实权重是一个连续变化的值。不做插值就相当于把连续函数变成了阶梯函数亮度每跨过一个量化台阶权重就跳跃一次表现在图像上就是一圈圈的伪影。加上三线性插值之后条带立刻消失了。所以 LUT 插值这一步不能省。如果觉得三线性插值计算量大可以降级做双线性或者最近邻加低通滤波但三线性性能最稳妥。5.3 蒸馏时权重分配不当蒸馏损失的三个系数alpha 对应 L1beta 对应 SSIMgamma 对应感知损失。我最开始把 SSIM 的权重设得太高beta 取到了 2.0。结果学生网络输出结果在结构上跟教师很像但整体亮度偏了融合后的图像看起来发灰。后来把 beta 降到 0.5alpha 取 1.0gamma 取 0.1效果才平衡。这里面有个经验像素级损失控制在结构损失之前。如果只优化结构数值偏差会积累如果只优化像素细节会糊。要让学生网络先把每个位置的数值逼近再用结构损失去修正局部差异。最终 SSIM 和 PSNR 都能达到比较高的水平。5.4 训练和推理特征口径必须完全一致这个坑我印象最深。训练学生网络时局部均值是用 3×3 窗口算的部署到设备时为了省内存换成了 5×5 窗口。结果融合结果明显变差边缘处有光晕。一开始我还以为是 LUT 生成有问题查了两天才意识到是窗口尺寸变了。局部特征的统计口径必须严格一致否则同一批输入图像在训练时得到的特征和部署时得到的特征就不匹配查表自然查到错误的位置。我在代码里把所有窗口参数定义成同一个常量训练脚本和部署代码共用这才彻底解决。另外在部署端算局部均值时要注意边界像素的处理。我统一用镜像填充保证特征在图像边缘处也不会出现异常值。5.5 别忽略多路输入对齐的问题LUT-Fuse 本身解决的是“融合阶段”的计算开销但它不解决输入图像之间的对齐问题。多曝光序列如果是手持拍摄的相邻帧之间有位移必须先做配准再融合。否则算出来的融合权重再准融合结果也是重影的。配准这一步如果用传统特征点匹配RTK 还是会有耗时如果配准做不好实时融合的流程就卡在配准上。我目前的做法是先用相机姿态传感器做粗对齐再用一个小梯度下两帧之间的光流做精对齐。这样粗对齐几乎零开销精对齐只在一个很小的分辨率上做整体开销可控。6. 一点个人的体会做 LUT-Fuse 这个项目最大的感受是不要迷信网络本身也不要迷信传统方法关键是找到两者能结合的位置。深度学习负责“学”LUT 负责“跑”蒸馏负责“传”三者组成了一条完整的链路。很多看起来必须在强算力设备上才能完成的任务换一种表示方式就能在边缘设备上跑得飞快。这个思路还可以继续扩展。LUT-Fuse 目前处理的是两路输入融合如果扩展到三路、四路LUT 的输出维度增加但核心思路不变。同时 LUT 本身也是可以动态更新的比如根据环境光线变化定期切换不同的 LUT这样就能适应更多复杂场景。另一条路是把这个方案从融合迁移到其他逐像素任务上比如色调映射、超分辨率重建、图像去雾都是同样的套路。只要任务能拆成“局部特征到目标值的映射”就都能尝试用可学习 LUT 去固化。最后再提醒一句部署环境千差万别先明确硬件约束再设计算法结构比什么都重要。