
1. 为什么FPGA图像透雾是刚需而不是算法炫技做图像处理的人对透雾这两个字应该都不陌生雾霾天拍出来的监控画面白茫茫一片对比度低、色彩发灰人眼看着费劲人脸识别和车牌识别的准确率更是断崖式下跌。过去很多项目组第一反应是上深度学习去雾模型跑在GPU上效果确实不错但一放到嵌入式设备就傻眼了——功耗压不住、成本超预算、延时还不可控。我早几年接过一个安防项目客户需求很简单前端IPC网络摄像机要能实时输出透雾画面体积不能变大功耗不能超过原来的板卡延时必须控制在两帧以内。这个条件下GPU方案直接出局DSP方案算力也不太够用最后把目光落在了FPGA上。FPGA做ISP透雾的逻辑其实很直白透雾本质上是一个像素级的图像增强处理流程完全可以并行化而这正好是FPGA最擅长的领域。它不像CPU那样一条指令一条指令地跑也不像GPU那样有复杂的调度开销它是把算法直接烧进硬件逻辑里数据从Sensor进来经过一整套流水线处理直接输出干净的RGB/YUV数据中间几乎不产生额外延时。这里要给刚入门的朋友先捋清楚一个概念FPGA图像电子透雾到底透的是什么雾。物理上除雾靠的是光学镜头加滤光片电子透雾靠的是后端图像算法两者解决的是不同阶段的问题。我们说的ISP Dehaze就是指在图像信号处理器ISP的流程中加入去雾处理模块对已经变成数字信号的图像数据进行算法补偿把雾气造成的对比度损失和偏色拉回来。实际工程项目里FPGA透雾算法主要有三个派系基于大气散射模型的暗通道先验、基于灰度世界假设的色彩校正、基于频域增强的同态滤波。三者各有各的适用场景和坑下面我会逐一展开来讲包括它们的工程化落地细节、资源消耗、以及在FPGA上的实现取舍。这篇文章不是CMOS摄像头效PPT是实实在在能拿去用、能帮你少走弯路的内容。2. 暗通道先验的FPGA化从Matlab验证到RTL流水线的完整推导2.1 大气散射模型是绕不开的底层数学基础要搞透FPGA上的暗通道先验算法得先理解它背后的物理模型。McCarney提出的大气散射模型把相机收到的光看成两部分一部分是物体反射光经过雾霾衰减后的剩余能量另一部分是大气光在传播路径上散射进入相机的能量。写成公式就是I(x) J(x)t(x) A(1 - t(x))其中I(x)是观察到的有雾图像J(x)是无雾原景A是全局大气光t(x)是透射率。在雾浓的地方t(x)趋近于0图像就完全由大气光A主导看到的是一团灰色在近处无雾区域t(x)接近1图像几乎不受影响。何恺明团队正是基于这个模型提出暗通道先验在无雾图像的绝大多数非天空局部区域内总存在至少一个颜色通道的亮度值很低、趋近于0。这个先验规律在自然图像上统计上非常成立。有了这个先验就可以先估计出大气光A和透射率t(x)然后反解出无雾图像J(x)。FPGA上实现暗通道去雾基本步骤分为四步计算暗通道图、估计大气光、计算透射率图、根据模型反演还原图像。听起来很简单但每一步在FPGA上落地都有软硬件协同的考虑。2.2 暗通道计算的窗口滑动设计是整个流水线的第一道关暗通道的定义是取每个像素RGB三通道的最小值再在局部区域比如15x15窗口内做最小值滤波。Matlab里用ordfilt2一步搞定但FPGA里没有现成的排序函数需要自己搭窗口缓存和比较器阵列。我实际项目里用的是3x3窗口。原因很现实FPGA的片上资源LUT和FF是有限的15x15窗口意味着每个像素输出都需要225个像素参与比较即便做了金字塔式分层比较资源的开销依然相当可观。而3x3窗口在性价比上是一个平衡点——暗通道估计的窗口大小对最终效果的影响远没有你想象的那么敏感窗口小一点边缘细节保留更好窗口大一点透射率图更平滑3x3配合后续的导向滤波或软抠图处理效果足够满足工程需求。RTL实现上3x3窗口需要两行缓存Line Buffer三列数据拼接。两行缓存的深度等于图像行宽用FPGA内部的BRAM或分布式RAM实现。数据流进来时每打三拍就能拼出一个3x3的窗格然后分三级比较器树求出9个值的最小值。这里有经验的工程师会在中间加上寄存器打拍避免比较器树出现过长的组合逻辑路径导致时序收敛不了。我的经验是比较器树建议做两级打拍第一级做三个3输入最小值第二级做三个结果的最小值这样路径延时压得低综合频率基本能上200MHz以上。最小值比较器在Vivado里可以直接用clb原语或者纯行为级代码写但有一个优化技巧三个输入取最小值可以用两级两输入比较器先把前两个比了结果和第三个再比这样只消耗两个比较器而不是一组复杂的查找表。Vivado综合器很聪明会帮你优化但我在代码里还是喜欢手工展开方便控制流水线级数。暗通道计算核心伪代码如下// 3x3窗口暗通道计算输入rgb数据并行对齐 // 本模块为像素级流水线每个时钟周期输出一个像素的暗通道值 always (posedge clk or negedge rst_n) begin if (!rst_n) begin min_r 0; min_g 0; min_b 0; dark_pixel 0; end else begin // 第一步取RGB三通道最小值 min_rg (pixel_r pixel_g) ? pixel_r : pixel_g; min_rgb (min_rg pixel_b) ? min_rg : pixel_b; dark_pixel min_rgb; end end // 行缓存与窗口拼接逻辑省略核心思路为两级LineBuffer实现行对齐算法不是越复杂越好FPGA这个领域里不做没必要的计算才是第一性原则。暗通道的作用是估计透射率和大气光它的精度不需要达到浮点级8bit量化足够。连大气光都不需要精确到单一像素只要取暗通道图前0.1%亮度的像素值做平均就算很稳的做法了。2.3 透射率计算时把浮点除法玩成固定点的移位运算透射率t(x)的估计公式是t(x) 1 - ω * min_c( min_y∈Ω(x)( I_c(y) / A_c ) )其中ω是常数通常取0.95用来保留少量雾气让图像看起来更自然防止彻底去雾后出现黑斑效应。在PC上这一步就是浮点运算但在FPGA上浮点除法是资源大户综合出一个除法器动辄几百个LUT还拖慢时序。工程化做法是把除法换成查表。观察这个公式除法的分母A_c是整幅图像估计出的一个全局常数分子是0-255的像素值那么I_c / A_c的取值范围是0到255/A_c。可以对A_c归一化到2的幂次将除法近似成移位操作。更常见的做法是直接调整公式形态既然透射率只参与后续的反演计算而反演运算本身有缩放因子就不需要精确的除法结果。用移位近似除法饱和截断就能得到视觉无损的透射率图。我在Xilinx FPGA上这样实现后透射率计算路径的LUT消耗控制在200以内两个周期完成一个像素的处理。透射率计算出来之后还有个问题它是一张和原图同尺寸的灰度图需要做平滑才能避免去雾后的块效应。Matlab里用guidedfilter导向滤波作为透射率精化步骤但这玩意儿在FPGA上就是个无底洞——引导滤波涉及窗口协方差计算、均值滤波、除法光是均值滤波就得串好几级。我实际项目中舍弃了它改用3x3中值滤波对透射率图做平滑处理。中值滤波虽然不如导向滤波精细但胜在资源开销小、流水线简单、效果好配合后续的色调映射视觉副作用几乎看不出来。如果你是做高端安防或车载项目对画质要求极致可以考虑用双边滤波替代中值滤波原理类似但保边效果更好代价是多消耗一些DSP slice和BRAM带宽。2.4 最终反演还原防溢出和防噪点放大是工程分水岭透射率t(x)求出来后无雾图像的重建公式为J(x) (I(x) - A) / t(x) A这里有个经典的工程问题当t(x)很小时除法会把噪声成倍放大。比如t0.1时I-A中的噪声会被放大10倍画面会出现密集的椒盐噪点。所以实际工程里都会给t(x)设一个下限值我通常用t0 0.1把透射率clip到[0.1, 1]之间。这个思路和何恺明论文里的t0参数一致但在FPGA里需要显式实现饱和逻辑用Verilog的if判断加clamp操作不能依赖浮点运算的自动归一化。另外一个坑是A的估计方式。很多移植到FPGA上的工程用暗通道图像最亮的前0.1%像素对应的原图亮度均值来估计A这在FPGA上需要整帧统计完才能得出结果造成一帧的延时。工业相机和安防监控对实时性要求高这个延时往往不能接受。我的做法是做一个双帧复用用上一帧统计出的A值处理当前帧当前帧的统计结果供下一帧使用。因为相邻帧的大气光变化极慢除非场景发生剧烈变化或镜头被遮挡这种近似在视觉效果上完全看不出差异。在FPGA实现上只需要在帧结束信号VSync到来时锁存统计值下帧开始前更新到工作寄存器不增加任何额外流水线延时。3. 透雾之外的必备配套色彩恢复与动态范围处理3.1 为什么单纯去雾后画面惨白白平衡和灰度世界法的联动实际调试暗通道去雾算法时你会发现一个情况雾是去了但整张图偏灰白像贴了一层磨砂滤镜。这是因为大气光估计不准确导致色彩偏移而去雾算法本身没有颜色恒常性修正能力。所以我的ISP Dehaze模块里一定还会串一个白平衡子模块。白平衡算法在FPGA上最常用的是灰度世界法假设一幅正常光照下的图像RGB三通道的平均灰度值应该相等。先用统计模块累加每通道像素和帧结束算平均得到三个通道增益系数再用增益系数逐像素做乘法校正。这里有一个容易踩的坑灰度世界法在雾天场景下会失效。因为雾气让全图都偏向灰白色RGB通道平均值本身被拉高了、也拉平了此时算出的增益接近1根本起不到校正作用。所以白平衡统计必须在透雾处理之前、但要在暗通道估计的过程中同时进行——准确说统计的是原图的RGB均值但增益的确定要结合透雾后的大气光A。我通常的做法是白平衡增益 目标灰度 / 原图通道均值 - A * 雾气占比雾气占比可以从透射率的均值反推出来。这么处理后透雾画面的色彩能恢复到一个相当自然的状态。3.2 透雾后的对比度增强别把直方图均衡做成鬼影制造机透雾处理之后图像的对比度往往会偏低特别是原来雾比较均匀的场景。这时候加一级对比度增强是很有必要的。但直方图均衡这种全局算法在FPGA上有个老问题它需要先统计整帧直方图再做累积分布函数映射这带来两帧的延时而且容易过度增强平坦区域导致块状伪影和鬼影。我的替代方案是限制对比度自适应直方图均衡CLAHE的简化版把图像分成8x8的块每个块单独统计直方图做裁剪限制再用双线性插值把块之间的过渡平滑掉。这个算法在FPGA上实现不复杂关键难点是块与块之间的过渡区域需要缓存以及对每个块的裁剪阈值要动态调整。我用的裁剪阈值是直方图均值的1.5倍这个参数在绝大多数场景下效果都稳定。CLAHE在Xilinx FPGA上实现时消耗资源主要集中在插值计算和块直方图存储器。8x8块每块直方图需要256x8bit的不同存储地址用BRAM很合适。双线性插值需要4个块的直方图同时可读因此BRAM要分Bank或复制几份这部分是资源大头。如果你的FPGA资源紧张可以退一步做全局直方图均衡加Gamma校正的参数化调整效果略逊但对硬件要求低很多。3.3 Gamma和色调映射最后一个不说就没人注意的细节FPGA输出的图像最终要显示在屏上或传给编码器显示设备的伽马曲线和图像传感器的线性响应之间存在非线性匹配问题。透雾算法会改变图像的亮度分布如果不做Gamma校正画面会显得发灰或发暗特别是暗部区域的细节容易丢失。我的模组里一般加一个分段线性Gamma查找表用BRAM存储256个8bit的映射值启动时由CPU/AXI总线初始化。Gamma值根据场景可配默认2.2。为什么用查找表而不是在线计算因为在线计算的幂运算在FPGA上非常昂贵而查表只需要一个时钟周期。查找表另一个好处是可以通过修改表格内容实现多种色调映射风格比如在逆光场景下用S形曲线压高光提暗部一个表解决多种场景适配。这个小设计被很多同事评价为代码里的隐形功臣。4. 硬件架构实测DDR3带宽、流水线时序与资源利用率4.1 为什么透雾模块不能放在Sensor数据路径之外很多第一次接触ISP Dehaze的工程师会问既然透雾算法可以对YUV数据进行处理那为什么不挂在ISP后端显示前再处理一帧这样不就不影响sensor数据路径了吗我在实际调试中给过这样的设计方案也吃过亏。挂在ISP后端有两个致命问题一是需要整帧缓存DDR带宽瞬间翻倍二是透雾处理后如果像素数据有截断或溢出再进编码器处理时会引入新的伪影。正确的架构是把Dehaze模块插入到ISP流水线的中间——位置在RAW域去噪之后、在RGB域色彩校正之前。这样透雾的输入是干净的去噪RAW转RGB数据输出再进入白平衡和色彩校正模块链路清晰各模块互不干扰。4.2 DDR3缓存架构实战以1080p30视频流为例透雾算法本身是逐像素流式处理的为什么还需要DDR因为暗通道统计需要至少缓存若干行数据行缓存透射率平滑又需要透射率图的缓存加上用户的sensor输出是1080p30fps这些中间数据量巨大片内BRAM根本hold不住。我的设计里采用行缓冲为主、帧缓冲为辅的策略行缓存解决窗口处理需求透射率图整体缓存在DDR中。计算一下带宽需求。1080p30输入是1920x1080x30帧62208000像素每秒RGB888就是大约186MB/s的写入带宽。假设透射率图以8bit灰度图缓存就额外增加62MB/s的写带宽和62MB/s的读带宽。整体DDR3带宽峰值约310MB/s这还没算显示、编码等其他DDR主设备的占用。DDR3-1600提供12.8GB/s的理论带宽我这边用的是16bit DDR3实际可用带宽大约6-8GB/s所以单从数字上看根本没压力。但带宽不是唯一的问题访存效率才是。FPGA控制DDR3如果每次读写都是32bit小数据效率会低到让人绝望。正确的做法是突发传输Burst一次读写64B甚至128B对齐的数据块。透射率图以行块形式缓存每行缓存按256字节对齐一次burst发32个64bit数据效率拉满。我在代码里专门写了一个DDR控制封装层提供连续读写接口内部自动处理地址对齐和burst组合上层透雾模块只关心读到一行灰度数据这个抽象不用管DDR时序。4.3 时序收敛与流水线划分200MHz不是白来的很多人在FPGA上做图像处理都遇到过时序不过的问题透雾这种多级流水线更是重灾区。我的经验教训是宁可多打一拍不赌一眼的时序裕量。具体来说暗通道3x3窗口比较器树、透射率mul/div计算、反演还原的乘加组合这几段都是组合逻辑延迟热点必须每层之间插寄存器。我把整条透雾流水线划为8级输入对齐与行缓存写入窗口数据拼接暗通道三分量取最小值透射率查表与饱和处理透射率中值滤波反演还原乘加组合白平衡增益乘法Gamma查表每一级之间用valid/ready握手信号配合行场同步信号对齐保证数据流正确。这样划分后综合频率在Artix-7上稳定跑到200MHz1080p30只需148.5MHz像素时钟裕量充足。在Kintex-7或Zynq UltraScale上还能跑更高50MHz裕量留给后续扩展更多模块完全够用。这里给个实际资源消耗的参考我用的芯片是Xilinx Artix-7 XC7A200T整个ISP Dehaze模块的资源资源类型消耗量占比LUT14200约10%FF16800约12%BRAM26块36Kb约9%DSP48E6个约2.8%像素频率200MHz时序裕量充足资源占用低到完全可以和MIPI CSI-2接收、DDR控制、显示输出放在同一颗芯片上。4.4 实测画面与性能表现夜视、强逆光、浓雾三组实测算法效果最终还是要看画面。我的测试环境是配合OV5640 Sensor和Xilinx FPGA开发板进行室内外测试。浓雾场景下能见度低于50米透雾前的画面白茫茫几乎无法识别目标透雾后可以清晰辨识50米外的车牌轮廓。强逆光场景下透雾前暗部细节完全被吞掉透雾后暗部细节得到明显补偿人脸轮廓可辨。夜视场景下透雾算法的效果不如白天明显但配合红外补光画面对比度提升依然可感知。延时方面从Sensor输入到HDMI输出的全链路处理延时实测是3行时间加若干固定流水线延时大约0.7ms以内按1080p30计算整个透雾处理不产生帧级别的缓存延时。这在需要实时监控、自动驾驶等对时延敏感的场景下是一个关键优势。传统用CPU跑透雾算法至少需要33ms1帧以上的延时碰到处理能力弱的芯片甚至要3-5帧的buffer。这也是FPGA方案在ISP领域无法被取代的根本原因。5. 工程落地的坑与优化我从透雾项目里学到的事5.1 大雾场景边界算法失效时该怎么办透雾算法不是万能的。当雾气极其浓烈能见度小于20米大气散射模型中透射率t(x)接近0图像信噪比极低此时无论是FPGA还是GPU算法都无力回天。业界标准做法是加入近红外成像通道融合——近红外波段的散射比可见光弱很多即使大雾天气下也能拍到一定程度的场景辐射。安防上高端摄像头会做双通道融合但这超出ISP Dehaze模块本身的范围如果你的系统有NIR通道输入可以考虑做简单的亮部替换融合效果立竿见影。另一个容易被忽视的问题是和降噪模块的协同。透雾算法本身有噪声放大效应我去雾模块的设计位置是在降噪模块之后。否则降噪会把透雾恢复出的边缘细节一并抹掉。所以正确链路是RAW域去噪 → 透雾 → 色彩校正 → 亮度/对比度增强 → 降噪轻微 → 输出。第二级降噪强度不需要太高轻微均值滤波即可主要去掉透雾强行拉出来的minor噪声点。5.2 Vivado工程结构设计与调试技巧透雾模块的代码组织我一般按照功能划分一个模块对应一个Verilog文件方便维护和综合优化。顶层模块用AXI-Lite接口做寄存器配置调试时可以随时调整ω、t0、Gamma表等参数不必重新综合。Vivado的Mark Debug信号检查配合ILA是排查流水线bug的核心手段。我用ILA观测暗通道中间值和透射率值验证RTL行为和Matlab参考模型是否一致逐级比对输出像素值与ModelSim仿真的期望值是否吻合。这里分享一个关于透雾参数的调试顺序心得先调暗通道窗口大小再调ω最后才调白平衡增益和Gamma。很多人上来就堆参数结果几个变量互相影响越调越乱。同一场景下先固定一个接近全局最优的参数集再针对具体画面微调效率会高很多。5.3 纯FPGA方案与Zynq异构方案的取舍心得做透雾项目时团队里经常纠结一个问题到底是把整个ISP Dehaze都做成纯PL逻辑还是在Zynq里一部分算法跑PS端Linux、一部分跑PL我的结论是逐像素型操作全部放PL全局统计型操作可以放PS软件。暗通道、透射率计算、反演还原、白平衡增益这些都是逐像素流水线放PL天经地义但大气光A的全局统计、透射率图中值滤波这类操作也可以放PS软件只要带宽足够PS端用NEON指令做中值滤波也非常快。不过从稳定性和实时性角度我建议高帧率项目还是全PL实现。PS软件受中断和操作系统调度影响帧间隔抖动不可避免这在安防监控这种需要稳定帧率的场合是不被允许的。Zynq方案的真正优势在于灵活配置参数和跑更复杂的大气光估计逻辑适合高端产品差异化需求。我做完这一版纯PL透雾后在Zynq平台上把参数配置和场景切换接给了PS端Linux应用程序相当于又白拿了一层软件可玩性。5.4 给想抄作业的人一条明确路线图如果你也想在自己的项目里落地FPGA透雾我的建议路线是先别急着写Verilog用Matlab或Python完整跑通暗通道先验算法理解每个参数的效果边界生成多组仿真激励数据搭建FPGA最小图像处理平台Sensor采集 → 帧缓存 → HDMI显示打通数据通路在数据通路上插入透雾模块先用真实图像对比RTL仿真结果和Matlab结果确认数据一致性整机验证重点观察时序收敛、资源占用、画面质量三个维度如果在画质上还有提升空间再加入CLAHE和自适应白平衡等增强模块我自己当初从零做完这套流程大概用了两个多月其中算法调参和RTL移植各占一半时间。最耗时的不是写代码而是去雾效果和色彩还原之间的平衡调节——雾气减得越狠色彩失真越容易出现。这个平衡点没有通解只能结合具体应用场景去试。6. 后续演进从单帧透雾到视频透雾的进阶思路单帧透雾的坑在于时间一致性。连续视频帧中如果每帧独立估计大气光A和透射率t(x)帧与帧之间的参数会产生抖动反映到画面上就是闪烁、亮度跳变。我见过很多Demo单帧效果惊艳一跑视频就露馅。解决时间一致性的常用思路是给A加低通滤波——对估计出的A做时间维度滑动平均。这样大气光变化达到秒级平滑画面基本稳定。更好的方案是引入光流/运动估计对运动区域做时空联合透雾但这在FPGA上资源消耗巨大我评估过在XC7A200T上实现全局光流至少需要额外消耗40%的LUT。从工程角度看性价比太低目前还是以A平滑为主加上对透射率图做时域递归滤波效果已经足够大部分监控场景使用。另外现在不少高端FPGA芯片内置了硬核视频编解码器可以实现透雾后直接H.264/H.265编码输出。透雾本身改变了画面统计特性编码质量会受益于对比度的提升这算是个隐藏加分项——透雾做得越好压缩码率可以越低。从行业趋势看图像电子透雾算法正在从能不能透转向透得自然、透得智能。FPGA的角色也从纯算法引擎变成智能感知系统里的实时预处理单元。做透雾项目这几年我最大的体会是算法和硬件从来不是零和博弈关键是把每一行RTL代码的价值发挥到极限用最低的延迟、最少的资源交付用户能感知的画质提升。