FPGA实时透雾ISP设计:暗通道先验算法与1080P@60fps工程落地
做实时透雾这些年FPGA 算是我踩坑最多也收获最大的方向。手上这个基于 FPGA 的图像电子透雾ISP Dehaze项目从最初验证算法到最终跑通 1080P60fps 实时处理前后折腾了将近四个月。中间换过方案、推倒过架构、也跟时序收敛搏斗过好几天最后沉淀下来的这套东西我觉得值得写出来给想入坑 FPGA 图像处理的朋友当一份参考地图。先说清楚这个项目到底在解决什么问题。雾天或者灰霾天气下摄像头拍到的画面普遍存在对比度低、色彩发灰发白、细节模糊的问题。传统的光学透雾是用物理滤光片靠不同波段光的穿透特性差异来改善成像但成本高而且对可见光成像的提升有限。电子透雾则是纯算法方案在图像信号处理器ISPImage Signal Processor链路里对采集到的图像做实时增强处理把大气散射造成的退化效果逆向补偿回来。对于安防监控、车载辅助驾驶、无人机航拍这类对实时性要求极高的场景FPGA 是实现电子透雾最合适的载体没有之一。这套方案最核心的价值在于它在不更换镜头、不加装任何光学器件的前提下通过 FPGA 内部的流水线运算让普通摄像头具备了穿透薄雾的能力。整个项目落地后在雾天实拍场景下画面清晰度提升肉眼可见动态范围保持良好运动目标没有拖影延迟控制在三帧以内。下面我把整个项目的设计和实操过程掰开揉碎讲清楚。1. 项目整体方案与设计思路1.1 第一次做透雾别急着上 FPGA很多人拿到这个需求第一反应就是直接找一篇 Dehaze 论文把暗通道先验Dark Channel Prior算法搬到 FPGA 上。这个思路方向是对的但直接动手写 RTL 之前我强烈建议先在 PC 端用 OpenCV 或者 MATLAB 把算法链路完整跑通一遍拿到真实雾天图像的中间结果。原因很简单FPGA 调试一个 bug 的周期以小时计PC 端改一行代码重新出结果只要几秒钟算法层面的问题必须在 PC 端全部暴露并解决之后才值得花精力做硬件移植。我最初踩的一个大坑就是拿到暗通道先验算法后直接在 Vivado 里开写结果写出来的模块在仿真里跑得好好的上板之后画面亮度跳水暗部细节全没了。后来回到 PC 端一查发现是透射率估计里一个下限裁剪参数设得太激进导致透射率图整体偏低。这种算法层面的参数问题如果先做充分的 PC 端验证五分钟就能发现但在 FPGA 上排查从信号抓取到波形比对整整耗了我一天半。所以整个项目的推进路线我建议严格分成四个阶段算法原型验证、定点化仿真、RTL 实现与仿真、上板调试。每个阶段之间要有明确的交付物和验收标准。算法原型验证阶段的交付物是一套能在 PC 端跑通的透雾脚本输入任意一张雾天图能够输出透雾后的效果图定点化仿真阶段要确认 8bit 或 12bit 数据宽度下算法退化可以接受RTL 仿真阶段要在测试平台里对比定点模型输出和 RTL 输出的一致性最后才是上板调时序、调参数。1.2 技术选型为什么是暗通道先验而不是深度学习透雾算法目前市场上主流的流派有三类基于图像增强的方法比如直方图均衡化、Retinex、基于物理模型的复原方法代表作就是暗通道先验、以及基于深度学习的方法。基于深度学习的透雾网络在效果上确实能达到当前最佳尤其是对浓雾场景的复原能力和色彩还原的稳健性都要好于传统方法。但它在 FPGA 落地上有几个硬伤模型参数量动辄几十 MB 到几百 MB对片内存储是巨大压力卷积计算量集中在乘加运算对于没有 DSP 资源的低端 FPGA 来说算力严重不足即使能用帧率往往只能做到 720P30fps 的水平跟工业场景要求的 1080P60fps 差距明显。暗通道先验的优势在于它的计算结构极其规整最小值滤波、均值滤波、像素级算术运算全部是规则化操作非常适合 FPGA 的流水线并行架构。尤其在 Xilinx 7 系列器件上这些操作可以直接映射到 Line Buffer 和 DSP48E1 切片上资源利用率高时序也容易收敛。实测下来在一个中等规模的 Artix-7 芯片上做到 1080P60fps逻辑资源占用在四成左右BRAM 占用不到一半还有充足的余量去做白平衡、降噪等其他 ISP 模块。硬件平台我选的是 Xilinx Artix-7 系列具体型号是 XC7A100T。选它的理由有三个一是性价比高相比 Kintex 和 Virtex 系列便宜一个数量级二是资源够用1080P 的实时透雾算法用不到高端器件三是开发工具链成熟Vivado 的生态对图像处理类项目支持很完善。如果项目要求更高分辨率比如 4K可以考虑 Zynq UltraScale 系列利用 PS 端跑 Linux 做参数配置和显示控制PL 端做透雾核心算法。1.3 整体架构ISP 管线中插入两个模块在说架构之前先简单交代一下 ISP 管线的背景。摄像头 sensor 输出的原始数据通常是 Bayer 格式只有 R/G/B 三通道中的一通道亮度值需要经过黑电平校正、去马赛克Demosaic、白平衡AWB、颜色校正CCM、伽马校正Gamma等一系列处理才能变成人眼看起来正常的彩色图像。透雾算法在 ISP 管线里的位置不是固定的可以在去马赛克之前做灰度域透雾也可以在 RGB 恢复之后做彩图透雾。我的方案选择在去马赛克和 RGB 色彩空间转换之后做透雾。理由很直接暗通道先验理论建立的模型是基于 RGB 三通道的大气散射模型在 RGB 域直接套用最符合物理意义。如果在 Bayer 域做虽然能减少一个去马赛克模块的数据量但单通道的暗通道统计意义会弱化透射率估计精度会下降。整个系统的数据流是这样的sensor 输出的 Bayer 数据先进 RAW 域预处理完成坏点校正和黑电平补偿然后经过去马赛克得到 RGB 图像RGB 图像进入透雾模块模块内部做暗通道计算、大气光估计、透射率估计与细化、场景辐射恢复输出透雾后的 RGB 图像再往下透雾后的图像经过自动白平衡和伽马校正最终输出到显示端。透雾模块虽然只占了 ISP 管线中的一环但它改变了整个链路的色彩基准所以白平衡和伽马参数需要重新标定这一点很多第一次做的人容易忽略。2. 透雾算法原理与硬件等价变换2.1 大气散射模型公式拆解电子透雾算法建立在经典的大气散射模型之上公式是这样的I(x) J(x) * t(x) A * (1 - t(x))其中 I(x) 是传感器接收到的有雾图像的像素值J(x) 是场景原本的无雾辐射亮度也就是我们要恢复的目标A 是大气光值通常对应图像中最亮区域的亮度t(x) 是透射率表示光线从场景传播到传感器的过程中未被散射衰减的比例。公式可以直观地理解为眼睛看到的画面等于场景真实内容按透射率衰减后的部分加上大气光按1- 透射率加权叠加进来的部分。雾越浓透射率 t(x) 越小大气光的干扰就越大画面看起来就越白、越灰、越模糊。透雾要做的事情就是通过估计每个像素点的透射率 t(x) 和全局大气光 A从有雾图像 I(x) 中反解出场景辐射 J(x)。把公式变形J(x) (I(x) - A) / t(x) A这个计算在数学上非常干净难点在于如何准确估计 t(x) 和 A。暗通道先验理论提供了一套行之有效的估计方法。2.2 暗通道先验的物理依据暗通道先验的核心观察来自对大量户外无雾图像的统计分析在无雾的清晰图像中绝大多数局部区域除了天空区域的至少一个颜色通道的亮度值非常低趋近于零。也就是对于图像中的大部分像素点在它周围的一个小窗口内RGB 三通道的最小值非常小这就是“暗通道”的来历。物理上的解释是彩色物体表面反射的光一般难以同时在 R、G、B 三个通道都保持高强度总有一个通道的反射率偏低。比如绿叶反射绿光为主红色通道的值就很小红色墙壁反射红光为主蓝绿通道的值很小。而在有雾图像中大气光的散射会给每个像素的每个通道都叠加上一个正的偏置量这个偏置量的大小和透射率直接相关。雾越浓叠加的偏置越大暗通道的值也就越高。所以暗通道图本身就可以作为雾浓度的估计图。基于这个先验透射率的估计公式可以写成t(x) 1 - ω * min_c(min_y∈Ω(x)(I_c(y) / A_c))其中 c 表示 RGB 颜色通道Ω(x) 表示以像素 x 为中心的一个局部窗口min 操作取窗口内所有像素的最小值。ω 是一个保雾参数取值通常在 0.85 到 0.95 之间目的是保留少量雾气让复原后的图像不至于过于生硬、失真。这里需要注意窗口大小直接影响透雾效果窗口太小暗通道值容易偏高透射率估计偏大透雾不彻底窗口太大暗通道值偏低透射率估计偏小画面容易过暗和出现光晕。经过对比测试15x15 到 20x20 的窗口在 1080P 分辨率下效果比较均衡。大气光值 A 的估计相对简单在暗通道图中取亮度最高的前 0.1% 的像素这些像素通常位于场景中距离摄像头最远、雾气最浓的区域然后在原始有雾图像中取这些像素对应位置的最大亮度值作为 A。在 FPGA 实现时为了提高硬件友好性也可以用暗通道图的直方图统计来近似将暗通道图累加直方图从最亮端累加到总像素数的 0.1% 处记录对应的灰度值再映射回原始图像找对应亮度。2.3 从浮点到定点FPGA 移植前的关键一步在 FPGA 上实现任何算法都要面对浮点运算转定点运算的问题。FPGA 虽然能做浮点运算但资源消耗大、时序收敛难在实时视频处理链路中几乎都用定点代替。以透射率计算这一步为例原始公式是浮点的乘除运算在硬件里我做了这样的量化和简化。首先为了减少一个除法器我把透射率计算拆成预先计算好的查找表形式定义透射率 t(x) 的范围为 0.1 到 1.0用 8bit 定点数表示量化精度 1/255。然后输入的最小值归一化后作为查找表的索引直接查表得到透射率。这样原本的浮点除法变成了一次访存操作DSP 资源省下来给后续的图像增强用。透射率细化是一个计算量比较大的环节。原始暗通道先验算法用软抠图Soft Matting来细化透射率图这个操作在 FPGA 上几乎不可行因为涉及求解大型稀疏线性方程组。工程上常用的替代方案是导向滤波Guided Filter它的计算复杂度跟滤波窗口大小呈线性关系在 FPGA 上可以用可分离的均值滤波来实现。进一步降低资源的方法是用一个 3x3 或 5x5 的均值滤波近似虽然透射率图的边缘保持能力会弱一些但处理速度大幅提升整体画质损失在可接受范围内。在数据宽度设计上我做了如下决策原始 RGB 数据用 12bit因为在处理过程中经过多次乘加运算8bit 数据容易产生截断噪声暗通道中间结果用 14bit因为最小值滤波会跨窗口传递位宽不够会出现暗部噪声透射率用 8bit 定点两位小数精度足够最终输出的 RGB 图像再量化回 8bit。这个小细节看起来不起眼但对画质的影响非常直观。3. FPGA 系统架构与核心模块实现3.1 顶层模块划分与数据流设计整个透雾系统的 RTL 设计我按照功能边界划分成五大模块统计模块、暗通道模块、大气光估计模块、透射率细化模块、以及场景辐射恢复模块。另外还有一个寄存器配置模块用来接收外部 CPU 或者上位机通过串口或 I2C 下发的参数比如保雾系数 ω、透射率下限 t0、图像增益等。这样划分的好处是每个模块的职责单一可以独立仿真和调试出了问题能够快速定位。数据流上RGB 图像以像素流的方式逐行进入模块。这一点和 PC 端算法有本质区别PC 端处理一张图像是先把整幅图读入内存然后随机访问任意像素FPGA 只能对像素流做顺序访问要获取某个像素周围的邻域信息必须通过行缓存和窗口缓存来“制造”时间上的并行。整个架构中物理上比较核心的存储资源全部集中在这几个地方暗通道计算需要 15 行缓存透射率均值平滑需要 3 行缓存导向滤波需要 5 行缓存。这些行缓存是 BRAM 的主要消耗来源。框架上我采用像素级流水线让数据像流水一样依次流过各个模块。模块之间的接口协议统一为 valid-ready 握手上一级模块拉高 valid 表示当前像素有效下一级模块拉高 ready 表示可以接收。当 valid 和 ready 同时为高时数据在时钟上升沿被锁存传递。整个流水线的吞吐率由一个主时钟驱动1080P60fps 时像素时钟约为 148.5MHzArtix-7 完全可以轻松满足。3.2 暗通道计算行缓存与滑动窗口的实现暗通道计算是整条流水线的第一站也是最容易写错的地方。它的任务是对每个像素取它周围 15x15 窗口内 RGB 三通道的最小值。FPGA 上实现任意窗口大小的最小值滤波标准做法是二维滑动窗口拆分成行方向和列方向两趟处理。行方向上的逻辑是用 15 个行缓存构成 15 行数据并行输出每一行对应一个 15bit 的寄存器链当新的像素进入时每个缓存行都输出当前列位置的值从而在每一拍都能拿到 15 行同列数据。这 15 个值再经过一个 15 输入的最小值比较树得到列方向上的最小值。同时每一个行缓存内部也维护了一个长度 15 的滑动寄存器组对这个窗口内 15 个像素求最小值。列方向的最小值结果按行流出来后再经过长度为 15 的滑动最小值运算就得到了完整的 15x15 二维窗口最小值。这句话看起来复杂但实际代码量并不大。需要注意的是行缓存的深度必须等于图像一行像素的个数而不仅仅是窗口宽度因为行缓存的作用是延迟整行数据让第 N 行能和它之后的若干行在同一时钟周期对齐。这里有一个很容易踩的性能坑如果直接用 15 组独立的 Block RAM 来实现行缓存那么每拍需要读 15 个地址、写 15 个地址BRAM 的端口数量不够用就必须把 BRAM 复制多份资源开销成倍增加。我实际用下来的优化手段是采用基于移位寄存器的行缓存替代方案。Xilinx 的 Vivado 提供了基于 SRL16 原语的移位寄存器 IP用 LUT 实现行缓存在小深度行缓存场景下深度小于 2048比 BRAM 更省资源而且天然支持多路并行输出。对于 1080P 的 1920 像素行宽来说15 行缓存用 SRL16 移位寄存器实现在 XC7A100T 上逻辑资源可以承受。如果你用更大的窗口如 20x20建议混合使用 BRAM 和 SRL。3.3 透射率计算与大气光估计的硬件策略透射率计算模块接收暗通道模块的输出结合大气光估计模块给出的大气光值 A 和用户配置的保雾系数 ω计算出每个像素的透射率。实际代码里我用了查找表方案先把大气光 A 取倒数得到定点数 1/A然后设定一个查找表输入是暗通道值和 1/A 的乘积输出就是 1 减去 ω 乘以该乘积的结果最后做一个饱和截断使透射率落在 [t0, 1.0] 区间内t0 默认设置为 0.1。这个查找表设计避免了复杂的浮点除法逻辑上就是一次乘加和一次查表非常紧凑。大气光估计是一个帧级统计任务它的硬件实现策略比较特殊。由于暗通道图是一帧完整图像处理完才能得到全局统计结果所以我采用了“上一帧统计本帧使用”的策略在当前帧的垂直消隐期间统计模块把这一帧暗通道图的直方图累加完成计算出大气光估计值下一帧图像到来时透射率计算模块使用这个大气光值。这样做有一个帧延迟但实际画面效果几乎无感因为大气光值在短时间内变化很缓慢对透雾效果的影响可以忽略。在 FPGA 上实现大气光估计我用了一个 Block RAM 作为 4096 深度的直方图存储14bit 暗通道值总共 16384 个 bin实际实现时我压缩到了 1024 个 bin用 10bit 索引统计完一帧后通过查表逻辑从高地址端向低地址端累加直到累加数达到总像素数的 0.1%记录此时的地址作为暗通道阈值再将该阈值输入到原始图像亮度统计模块中映射得到大气光 A。3.4 透射率细化用导向滤波替代软抠图透射率细化这一步直接决定了透雾后图像的光晕严重程度。暗通道先验算出的粗透射率图在物体边缘处会出现明显的块状效应如果不做细化就去做场景恢复得到的图像边缘会有一圈圈的白色光晕画面非常塑料感。我在项目里使用的细化方案是导向滤波输入导向图是原始有雾图像的灰度图输入待滤波图是粗透射率图滤波半径设定为 8正则化参数 ε 设定为 1e-3。导向滤波的算法过程包含均值滤波、方差计算、线性回归系数计算、二次均值滤波在 FPGA 上实现时我把整个过程拆成了四个可分离的均值滤波器外加若干乘法器和加法器。由于导向滤波的系数计算涉及数值除法我采用了一个实用的简化方案在系数 a 的计算环节将除法替换为移位近似。因为分母是局部方差加 ε在大部分平坦区域方差接近于零a 接近于 0b 接近于均值在边缘区域方差大a 接近于 1b 接近于 0。我直接根据方差值的大小做分段线性映射用查找表做近似避免除法器。实测下来的细化效果和精确除法相比在 1080P 图像上肉眼几乎分辨不出差异但 DSP 资源省了一半以上。3.5 场景辐射恢复与色彩保真处理透射率细化完成后最后一步是场景辐射恢复根据公式 J(x) (I(x) - A) / t(x) A对 RGB 三个通道分别做计算。公式本身是简单的像素级运算在 FPGA 上就是三个通道各自做一次减法、一次乘法乘以透射率的倒数、再加回大气光值。这里的核心难点不是运算本身而是数值稳定性。当透射率 t(x) 非常小的时候除法会导致像素值爆炸式放大噪声被无限放大画面出现大量雪花噪点。所以我在场景辐射恢复之前做了一个透射率下限保护t 值小于 0.1 的强制拉低到 0.1。这个操作是必要的——宁可少去一点雾也不能让噪声污染画面。色彩失真问题同样值得注意。暗通道先验在天空区域是失效的因为天空区域本身没有暗通道算法会把天空误判为浓雾区域导致恢复后的天空区域出现偏色斑块。针对这个问题我增加了一个基于亮度的自适应混合策略当像素亮度接近大气光值时给透雾后像素一个向原图回归的权重让天空区域保持自然过渡。实际做法是计算一个权重因子 w clamp((I_max - A) / A, 0, 1)然后 J_out w * J_dehazed (1 - w) * I_original。这个权重因子实现起来就是一个 LUT不会占用太多逻辑资源但对天空区域的处理改善非常大。4. 工程落地时序收敛、资源优化与整机联调4.1 1080P60fps 的时序约束与收敛实战写透雾 RTL 和写一般的 FPGA 逻辑不同图像处理流水线对时序的要求极其苛刻。因为像素流是连续不断的任何一个组合逻辑路径上的延迟超标都会直接导致整条流水线错位画面出现撕裂或花屏。我一上来就给自己定了一个目标主时钟 148.5MHz建立时间裕量不小于 0.2ns。实际开发过程中最难收敛的地方是透射率细化模块中的乘法器链。导向滤波中计算均值、方差、协方差时连续使用多个 DSP48E1乘法器级联较多组合逻辑延迟拉长。我做了几轮优化第一轮先做了寄存器重定时Retiming调整在乘法器链的中间插入额外流水寄存器把长路径拆断。代价是增加了几个时钟周期的 latency但换来了时序收敛。第二轮针对 BRAM 行缓存输出路径做了输出寄存器调优让 BRAM 的输出直接进入寄存器而不是直接连到下一级的组合逻辑。第三轮利用了 Vivado 的物理优化选项打开了时序优化模式下的重定时和寄存器复制综合策略选择了 Performance Explore。三轮做完之后最差路径的时序裕量从初始的 -0.5ns 改善到 0.35ns顺利收敛。如果项目时间充裕我建议在上板之前至少做一次完整的时序仿真尤其是跨时钟域的部分要重点验证。4.2 资源使用情况与优化心得整个透雾系统在 XC7A100T 上的资源消耗大致如下LUT 约 28600 个占总量约 45%FF 约 31700 个占约 25%DSP48E1 使用 86 个占约 40%BRAM 18Kb 使用了约 78 块占约 35%没有使用任何硬核 IP全部是纯逻辑实现。整体资源占比约四成留有足够的余量给外围接口和以后扩展其他 ISP 算法。资源优化方面有几个心得可以分享。透射率细化模块的导向滤波如果严格按照教科书公式实现需要 6 个二维均值滤波器和大量乘法器资源消耗会翻倍。我的做法是让均值滤波器采用积分图方式实现积分图在 FPGA 上需要维护一个宽度为图像宽度加一的累加行缓存虽然增加了 BRAM 使用量但把均值滤波的复杂度从 O(r^2) 降到了 O(1)窗口大小可配置对资源的大头——DSP——节省非常明显。此外暗通道模块的 15 输入最小值比较树我把它拆成两级先五个一组求最小得到三个中间结果再一次求最小。这种多级比较结构比单级大扇入比较器在时序上更友好。如果遇到资源不够的情况可以考虑三个方向把暗通道窗口从 15x15 缩减为 11x11窗口对 BRAM 和 LUT 的消耗是线性增长的减小窗口能立竿见影导向滤波半径从 8 调整为 4均值滤波的积分图存储可以直接减半把 RGB 通道的处理改为分时复用三个通道共用一套运算单元以牺牲吞吐为代价换取资源。4.3 视频链路联调MIPI 接入与 HDMI 输出的配合透雾算法模块不是独立运行的它嵌在完整的视频链路里。我这边的输入是摄像头 sensor 通过 MIPI CSI-2 接口输出 RAW10 格式数据用 Xilinx 的 MIPI CSI-2 RX 子系统 IP 做协议解析然后自己写了一个 Bayer 到 RGB 的去马赛克模块进入透雾处理出来之后经过 AXI4-Stream 转 Video Out 的 IP 输出到 HDMI TX 芯片驱动显示器实时预览。联调阶段有一个容易被忽略的环节去马赛克模块和透雾模块之间的行同步信号对齐。透雾模块的行缓存架构要求输入的像素流必须包含正确的行同步和帧同步信息否则行缓存错位画面会沿水平方向出现斜切。我在去马赛克和透雾之间加了一个同步信号重定时模块使用 FIFO 对行场同步打拍对齐确保递进到透雾模块时数据和同步信号严格对齐。另一个坑是色彩空间转换之后的像素大小变化。去马赛克输出是 RGB888透雾中间处理用 12bit输出又回到 8bit这个量化过程如果处理不好会出现色彩条带。我用了带抖动的截断方法在低两位加入一点随机抖动噪声再向下取整视觉上基本消除了条带。5. 调试工具、参数标定与画质评估5.1 芯片内部可视化调试方法FPGA 图像处理调试最痛苦的事情就是黑盒看不到中间变量。你只知道最终画面不对但不知道是哪一级出了问题。我的经验是一定不要等整个系统写完再调试而是每写完一个模块就给它留一个调试观测口。系统集成时通过一个 ILAIntegrated Logic Analyzer核把这些观测信号引出在 Vivado 硬件管理器里实时抓取波形。实际调试透雾参数时ILA 的采样深度要足够大。我抓取一帧图像的暗通道数据流时采样深度设置到了 8192这样才能覆盖一行多像素的数据观察行缓存对齐是否正确。另一个技巧是给 ILA 的信号增加数据宽度把每路数据的 valid 信号也同时引出方便对照数据有效性和像素位置。如果图像画面的问题在像素域很难定位还可以在 FPGA 内部把中间计算结果通过视频接口输出出来。我的做法是在透射率模块后面加一个便捷的透射率图可视化通道把 8bit 的透射率值直接当作灰度图输出到 HDMI。这样透射率图长什么样、边缘是否清晰、块效应是否明显在显示器上一眼就能看出来比看波形直观多了。这个调试技巧在 ISP 调试中极度推荐调试效率能提升几倍。5.2 透雾参数标定的实际操作透雾参数虽然可以在寄存器里动态调整但具体取什么值还是需要科学标定。我建立了一套半自动标定流程采集雾天环境下的标准色卡图和梯度图然后依次调整保雾系数 ω、透射率下限 t0、天空保护强度、输出增益四个参数每次只调整一个变量记录主观画质评分和客观指标数据。保雾系数 ω 的标定逻辑最直观。ω 越接近 1去雾越彻底但暗部细节丢失越多色彩越浓重ω 越小画面越保守透雾效果越不明显。经过实测安防监控场景下 ω 0.92 到 0.95 效果比较好车牌、人脸等关键目标的细节保留和整体通透感之间能取得平衡。车载场景我会建议 ω 0.88 左右因为车载摄像头前向视野的天空比例大过强的透雾会让天空部分显得不自然。透射率下限 t0 的标定要结合 sensor 的噪声水平一起看。sensor 噪声大的场景t0 要适当拉高防止暗部被过度放大sensor 噪声小的场景t0 可以设低一些去雾能力会更强。我这边测试的 sensor 是 Sony IMX290噪声控制很好所以 t0 设成了 0.08 就能获得比较好的效果但换用另一款老式 sensor 后同样的参数画面出现了明显的噪点后来把 t0 调到 0.15 才恢复正常。5.3 客观画质指标与主观效果对比项目验收时除了人眼观察效果还要有客观数据支撑。我用的客观指标有三个对比度提升率、信息熵、以及灰度方差。对比度用 Michelson 对比度公式 C (Lmax - Lmin) / (Lmax Lmin)信息熵用 MATLAB 的 entropy 函数计算灰度方差就是像素值标准差。以一组雾天实拍的 1080P 图像为例原始图的 Michelson 对比度约 0.23透雾后提升到 0.51信息熵从 6.31 提升到 7.05灰度方差从 32.7 提升到 59.4。这些指标表明透雾后图像携带的细节信息明显增多灰度层次拉宽。同时我需要强调客观指标不是越高越好。对比度过高往往意味着噪点放大和色彩失真所以我在标定参数时始终以主观观感为主、客观指标为辅两者结合才能找到最优平衡点。6. 常见问题与排查技巧实录6.1 画面偏蓝/偏紫怎么处理透雾后画面偏蓝是最常见的问题。根本原因有两个一个是大气光 A 的估计偏高导致场景辐射恢复时蓝通道被过度放大另一个是透雾之后没有及时重新校正白平衡增益。排查步骤我建议这样走先在寄存器里把透雾强度调到最低看输入到透雾模块之前的图像是否偏色。如果偏色问题在 sensor 或白平衡模块如果不偏色把透雾强度调回来然后抓透射率图看是否有异常区域。如果透射率图正常就把大气光估计值打出来和原始图中最亮区域的亮度值对比看是否偏高。实际操作中我发现大气光估计如果在暗通道直方图的顶端取最大值容易受高光噪声影响导致 A 值偏高把统计分位数降低到 99.9% 之后情况改善很多。6.2 移动目标出现拖影和鬼影FPGA 实时透雾对运动物体的处理跟拍静态照片的透雾有本质区别。暗通道先验中的最小值滤波是基于局部空间窗口的统计如果窗口内存在运动目标会导致透射率估计在目标边缘产生错误画面出现拖影和鬼影。我这个项目当初测试时用一辆行驶中的白色轿车做验证透雾后车辆边缘出现了一圈淡色的光环随着车辆移动而移动。刚开始我以为是时序问题或帧同步问题排查了很久才发现是算法的固有现象。解决办法有两个方向一是缩小暗通道窗口从 15x15 缩小到 11x11边缘错位范围缩小鬼影减少但透雾能力有所下降二是对透射率图做时域滤波把当前帧的透射率与上一帧的透射率做 0.5/0.5 的加权平均平滑掉突然变化的区域但代价是运动目标在透射率上会存在轻微滞后。最终我把时域滤波系数调成了 0.3/0.7当前帧权重 0.3拖影消除效果明显画面延时也控制在可接受范围内。6.3 暗部噪声放大严重怎么办透雾本质上是对暗部像素做加法增益运算暗部噪声会被同步放大。这是透雾算法目前绕不开的物理限制只能通过工程手段压到可接受水平。我的方案是引入串行降噪和空域降噪结合在透雾模块之前对 RAW 域做一次 3x3 中值滤波滤除孤立噪点在透雾模块之后对亮度通道做一次基于边缘保护的低通滤波。边缘保护低通滤波的原理是如果当前像素是边缘像素梯度大于阈值则不做滤波保留锐度如果是平坦区域像素则做 3x3 均值滤波将随机噪声抹平。这个模块增加了少量资源但对暗部噪点的抑制效果非常明显。此外透射率下限 t0 的设置也直接影响暗部噪声t0 越大暗部被抑制得越多。6.4 帧率跑不满卡在性能瓶颈如果最终系统帧率跑不满性能瓶颈通常出现在两个地方传感器链路限速或者 FPGA 处理主频不达标。排查方法是在 Vivado 里看时序报告如果最差路径裕量是负的说明主频不达标如果裕量正常就检查一下 MIPI 接收频率、行场同步时序和输出接口带宽。很多情况下帧率不足不是算法的锅而是 DDR 带宽或输出接口的带宽被某个环节浪费了。我遇到过一次帧率上不去的情况排查发现是 AXI4-Stream 接口两侧时钟不同步导致 FIFO 频繁空满切换吞吐率大幅下降。解决办法是把 FIFO 深度加大到足够吸收两侧时钟频率差异并且在写侧使用几乎满标志提前停止写入在读侧使用几乎空标志提前请求数据降低乒乓切换频率吞吐率立刻恢复。6.5 常见问题速查表问题现象可能原因排查方法解决方案画面偏蓝偏紫A估计偏高、白平衡失效对比原始图和透雾图调低统计分位数、重标定AWB边缘光晕透射率细化不足查看透射率可视化图增加导向滤波迭代、增大窗口暗部噪点t0过低、sensor噪声调整透雾下限提高t0、增加前后降噪运动拖影暗通道窗口过大观察边缘鬼影范围缩小窗口、时域滤波帧率不足时序不收敛或接口瓶颈查时序裕量和FIFO状态优化流水线、调整FIFO深度天空区域偏色暗通道先验失效查看天空区域透射率亮度自适应权重混合7. 项目迭代方向与扩展可能性透雾模块在 ISP 管线中的位置让它天然具备了良好的扩展性。目前项目只做了透雾这一项功能但后续要叠加去噪、宽动态、夜视增强等功能时架构完全不需要动只需要在管线中插入新的模块节点即可。其中一个我已经在做的扩展是自动曝光联动透雾处理后图像整体变暗是常见现象如果能在透雾模块输出端做一次亮度统计然后把统计数据反馈给 sensor 的曝光控制算法让 sensor 自动增加曝光时间或增益就能自动补偿透雾带来的亮度损失。这个功能在强雾环境下尤其有用。另外一个值得探索的方向是参数自动调节。目前的 ω 和 t0 都是手动配置的在不同雾浓度下需要手动切换。后续可以增加一个轻量级的雾浓度估计模块通过暗通道图的统计均值来自动判断当前场景的雾浓度然后自动匹配一组透雾参数从手动模式变成全自动模式。这个模块的算法逻辑不复杂资源开销也小是性价比很高的一次升级。还有一个方向是 YUV 域的透雾改造。目前在 RGB 域做透雾处理链路里色彩空间转换难免有误差。如果改成 YUV 域只对 Y 通道做透雾色彩信息保持不变理论上计算量能降低三分之一资源还能再省一截。但这个方案需要注意透雾后亮度提升对色度的影响处理不当会出现色彩变浅。这个方案我还没有完整验证过后续可以作为优化方向继续研究。最后说一点个人感觉透雾这个功能听起来好像很高大上但真正落地的难点从来不是算法本身而是把算法转换成适配硬件流水线的结构。暗通道先验的数学原理在论文里一页纸就能写完但把它跑在真实场景、真实 sensor、真实镜头下你会遇到数不清的工程细节问题。如果你正准备做类似的项目我的建议是先从 PC 端把算法和参数吃透再动 FPGAFPGA 设计过程中多留调试口、多打可视化通道这两件事能帮你省下大量的排错时间。希望这篇笔记对你的项目有实际帮助。