
有雾视频监控画面常年泛白、对比度差单纯靠调Gamma和对比度根本救不回来。做监控、车载、航拍这类图像处理项目时“电子透雾”ISP Dehaze几乎是刚需功能。我去年在一套基于FPGA的ISP处理链里完整实现了暗通道先验透雾算法1080P30fps实时跑通资源占用可控今天把整个从算法到硬件落地的过程记录下来。这篇东西适合两类人一类是刚开始接触FPGA图像处理、想找一个不算简单但又有明确结果的练手项目的朋友另一类是已经在做ISP相关的工程、正被“如何在实时链路里插入Dehaze”折磨的开发者。我会把方案选型、算法原理、模块拆分、定点化处理、资源估算和调试中踩过的坑都讲清楚尽量做到你看完能自己复现一条最小可用的透雾流水线。1. 方案选型为什么Electron Dehaze非得用FPGA做1.1 电子透雾与物理透雾的本质区别透雾这条需求行业里一直有两条路线。物理透雾是硬件层面的用红外相机、多光谱相机或者让镜头工作在近红外波段穿透雾气能力确实强但传感器贵、系统体积大而且对色彩影响明显。电子透雾就纯靠ISP算法从单帧彩色图像里估计大气光成分并反演恢复成本几乎为零集成在现有相机链路里也不用改光学和传感器。去雾算法能不能跑在嵌入式实时链路上关键不只看计算量还要看数据流形态。图像是一行一行扫进来的像素时钟是恒定的算法必须在严格的帧周期内完成一旦处理不过来就丢帧。这类“流式处理”的天然形态恰好是FPGA最擅长的战场。DSP和ARM能算但像素级操作的耗时和数据搬运开销很容易成为瓶颈。GPU虽然算力强但是功耗、体积和启动时间在工业相机、车载前装场景里聊不动。1.2 处理器对比FPGA、DSP、ARM的透雾计算效率在方案评估阶段我把几种处理器都列过一遍。ARM比如常见的Cortex-A系列做浮点强Linux生态丰富但瓶颈在于内存带宽——一张1080P的RGB图是6MB左右做一次全图级联操作最小值滤波多个盒式滤波至少要反复读写好几遍外存带宽和时间都吃力。DSP的MAC能力强但窗口类运算仍然受限于L2/L3缓存碰到7x7、15x15这种大窗口最小值滤波需要把邻域数据反复折腾。FPGA的逻辑不一样。它可以在片内BRAM里构建行缓冲Line Buffer同一时刻只缓存N行数据配合滑动窗口把二维卷积、最小值滤波、盒式滤波全部流式完成。整个运算过程中全图数据不需要反复进出DDR进来的像素流经过流水线后直接出去时钟速率等于像素速率。这种“数据进来多少、处理多少”的模式天然满足实时视频的实时性要求。还有一点透雾算法对延时有要求FPGA能做到几个毫秒内输出ARM或者GPU动辄几十毫秒延迟在环视和辅助驾驶场景里没法接受。1.3 这个项目的输入输出与整体场景定位做这个项目前我先定位了场景。输入是标准ISP Pipeline输出的RGB图像Sensor端可以是MIPI CSI-2接口的CMOS传感器也可以从DDR3/4里读RAW图先经过Demosaic再进来。输出是透雾增强后的RGB图后续可以直接送显示、编码或者传给识别算法。很多做图像的朋友日常用STM32H743这类MCU其实也能做小分辨率透雾比如QVGA但上了720P、1080P之后就会明显吃力。FPGA与MCU之间经常通过FMC接口通信——MCU负责参数配置、视频流控制FPGA负责行级像素处理这个协作模式在工业相机里非常常见。我推荐小团队和个人开发者参考这个分工复杂调度和上层协议交给MCU或ARM重计算和低延时留给FPGA。这样调试方便性能也有保障。2. 透雾算法原理与硬件映射2.1 从大气散射模型讲到暗通道先验透雾算法里最经典、最适合硬件化的理论框架是大气散射模型。有雾图像可以看成两部分一部分是目标物体本身的光线在传播过程中被衰减后的结果另一部分是大气光散射进入相机的成分。数学表达为I(x) J(x) * t(x) A * (1 - t(x))I是观测到的有雾图J是待恢复的无雾图A是全局大气光天空区域的亮度t是透射率描述光线从物体传播到相机的存活比例。t值越小说明雾气越浓。问题就转化为已知I估计A和t反解J。那么t怎么估何恺明提出的暗通道先验给出一个非常实用的统计规律在户外无雾图像的绝大多数局部区域内至少有一个颜色通道的强度值趋近于零。换句话说取RGB三通道中每个像素的最小值再对局部区域做一次最小值滤波得到的“暗通道”图像应当是很黑的。而有雾区域因为大气光的叠加暗通道会变亮。利用这个亮度的抬升量就能反推透射率。暗通道公式J_dark(x) min_{c ∈ {R,G,B}} ( min_{y ∈ Ω(x)} J_c(y) ) ≈ 0其中Ω(x)是以x为中心的局部窗口。透射率估计公式t(x) 1 - ω * min_{c} ( min_{y ∈ Ω(x)} ( I_c(y) / A_c ) )ω是保留因子一般取0.9~0.95目的是保留一点点雾感让画面不至于显得不自然。最后恢复公式J(x) (I(x) - A) / max(t(x), t0) At0是透射率下限一般是0.1避免分母过小导致噪声放大。这个算法流程里涉及的运算类型很少——逐像素求三通道最小值、窗口最小值滤波、全局统计大气光、逐像素除法减法。公式看着不难难点在于“局部窗口最小值”和“全图统计”这两类操作在CPU上属于典型的数据密集访问在FPGA里却能做成非常漂亮的流水线。2.2 从浮点算法到定点硬件量化、归一化与除法近似算法最初用Matlab验证时全是float到了FPGA就不可能直接float跑成本太高。我在工程里把整个计算链改成了纯定点fixed-point。图像数据是8bit灰度或者24bit RGB这个不用动。大气光A也用8bit表示0~255。真正需要小心的是透射率t的表示。t在浮点算法里是0~1之间的数我映射成8bit定点数数值范围0~255对应浮点0.0~1.0。后面恢复公式里需要除以t在硬件里直接做除法会消耗大量DSP资源。最实用的做法是做一张倒数查找表LUT256个地址每个存 1/t 的定点结果这样就把除法变成了查表乘法。代价是一个很小的Block RAM换来全链路无除法时序特别好收敛。还有一个归一化细节。透射率估计时min_c(I_c / A_c)这个操作里的除法也是按通道的。我在硬件里没直接对每个像素做除法而是先把输入像素预缩放让A变成2的幂次近似用移位代替除法。A等于127、128这种数值时除以A就近似于乘以 1/A我用厂商IP核里的定点除法器或者倒数表统一处理设计上是“单像素单周期输出”的标准流程。2.3 算法裁剪导向滤波简化成盒式滤波实时性和画质的平衡暗通道透射率估计有个先天毛病在局部窗口内假设透射率恒定导致恢复后的透射率图有块状边缘Block Effect直接恢复图像会看到一圈一圈的光晕伪影。原论文用soft matting或者导向滤波来细化透射率但导向滤波涉及大核、大量乘法和正则化项在FPGA里全尺寸实现代价很高。我的做法是把导向滤波简化成两个盒式滤波Box Filter的组合结合乘法做个近似引导。严格来说这不是完整导向滤波但对透雾来说效果已经足够。具体路径是先把透射率图送入一个7x7盒式滤波得到mean_t再利用原始灰度图I和t的乘积再做一次盒式滤波最后按导向滤波的公式组合。这套操作全部可以用行缓冲和滑窗实现一个像素进来流水线一拍出去不打破原有视频时钟。盒式滤波最大的好处是“可分离”——横向先做累加求均值纵向再做一次二维盒式滤波就变成了两次一维滤波。每行的窗口值只需维护一个累加器进入新像素就加离开旧像素就减消耗资源极少。我在实际测试中简化版和完整导向滤波的主观画质差距很小但硬件面积差不多省了60%~70%。做实时透雾建议优先走这条简化路线。3. 系统架构与模块划分3.1 整体处理链路Dehaze模块在ISP Pipeline里的位置透雾模块不是单独存在的要插入完整的ISP链路里才有效果。一个典型的FPGA ISP Pipeline顺序大概是Sensor输入MIPI/LVDS→ RAW域处理坏点校正、黑电平、Demosaic→ RGB域处理白平衡、去雾、色彩校正、Gamma→ YUV输出缩放、编码/显示。我在设计时把Dehaze插在Demosaic和白平衡之后、Gamma之前。原因有两点第一去雾算法依赖的是RGB三通道的物理统计关系必须在RGB域做第二放在Gamma之前是为了在线性光或接近线性的空间里做大气估计否则Gamma压缩后的图像会破坏暗通道先验的统计特性。另一个工程上的关键点Dehaze模块需要一帧图像的统计信息大气光A而像素流是逐行扫进来的。这意味着要么缓存整帧后再处理增加一帧延迟要么用上一帧的统计值处理当前帧。考虑到实时视频相邻帧的大气光变化很缓慢我直接采用“上一帧估计、当前帧使用”的策略帧延迟只增加1行非常适合监控类场景。3.2 行缓冲还是帧缓存Line Buffer设计的关键拆解窗口类运算在FPGA里的经典结构是“行缓冲滑动窗口”。以暗通道计算里的最小值滤波为例如果窗口大小是7x7我需要缓存7行图像数据。每进来一个新像素最老一行的数据就可以丢掉7个行缓冲中的数据形成了一个H型窗口再对当前窗口内的49个像素求RGB三通道最小值。行缓冲的具体实现我用的是Xilinx FPGA里的Shift RegisterSRL16或者Block RAM。BRAM配置成“行长度x行长字节”的环形buffer由读地址和写地址管理。选择SRL还是BRAM取决于资源分布和行长度小分辨率用SRL行数短1080P、7行、每行1920x3字节BRAM更划算。行缓冲换成DDR存储是最差的选择访问延迟和带宽都不好控制尽量把窗口数据控制在片内。滑窗内部怎么取49个像素每个时钟周期BRAM只能输出一个数据窗口的49个点在时序上不会同时到达。一个常用技巧是把滑窗通过移位寄存器阵列实现7个行缓冲的输出端各接一条7深的移位寄存器这样每拍能同时输出7x749个数据。最小值树Min Tree对这49个数据做两两比较深度为log2(49)≈6级路径延迟很小不影响工作时钟。3.3 核心子模块拆解暗通道、大气光、透射率与恢复我把Dehaze整体拆成四个子模块每个模块的功能和资源特点如下。暗通道模块输入RGB输出三通道最小值、再做局部窗口最小值。求三通道最小值只需要2个比较器窗口最小值滤波则依赖于行缓冲和比较树。窗口尺寸我用的是7x7这跟分辨率有关。720P以下可以适当放大到11x11能使暗通道估计更稳定1080P下窗口太大容易丢失边缘细节7x7比较平衡。大气光估计模块不能只看暗通道最亮的那一个像素这样容易被白色车、白色建筑物干扰。正确做法是统计当前帧暗通道直方图从最高亮度端往下取前0.1%比如1080P约2000个像素的亮度均值再用这些位置的原始像素均值作为大气光A。硬件实现里直方图统计用片上RAM加累加器帧结束计算阈值位置不需要复杂排序。透射率估计模块负责把暗通道图变换为透射率图包含通道归一化、0.95保留因子和定点化。然后透射率图经过简化的盒式滤波得到平滑版本。盒式滤波的窗口我选的也是7x7理由是透射率图的低频特性匹配7x7比较合适窗口太大会把物体的透射率边缘糊掉太小则起不到抑制块状伪影的作用。恢复模块最后做减法、乘法和截断输入原始RGB像素和透射率值查表得到1/t做乘加还原出J。这个模块是标准的算术流水线4级左右不存在反压。整套链路从模块边界来看干净利落每个模块都带一个AXI4-Stream信号valid/ready/last/user方便接线和调试。3.4 时序控制与帧率分析1080P30实时可行性做FPGA图像处理最后要落到时钟和帧率上。1080P30fps的像素时钟是74.25MHz含消隐每帧总像素约2200x1125247.5万像素有效像素1920x1080。Dehaze全链路如果每个像素只处理一次且内部无阻塞那么74.25MHz的输入时钟足够跑满。我在Vivado里的实现结果是时钟约束80MHz所有模块都收敛最差路径落在透射率恢复模块的乘法链上通过插入寄存器流水级解决了。如果要做720P60fps像素时钟同样是74.25MHz左右对FPGA逻辑的要求基本一样。真正的挑战来自外部DDR缓冲如果算法中需要缓存多帧做时间域去雾就会有带宽压力。我的单帧流式方案里DDR只在帧间放一帧透射率统计值带宽占用非常小这也是FPGA透雾比GPU更省带宽的优势。4. 实操过程与关键实现4.1 数据路径、位宽设计与资源估算表下面是我用的位宽表和资源估算可以作为复现参考。计算路径上的位宽是这样的原始RGB为8bit暗通道输出为8bitRGB三通道最小值大气光A为8bit透射率t为8bit0对应0.0255对应1.0倒数表输出16bit定点格式8Q8整数8位小数8位恢复J输出16bit中间值最后截断/饱和到8bit。资源估算以Xilinx Artix-7 XC7A35T为例足够跑1080P30大概是这样模块LUTFFBRAMDSP暗通道最小值滤波82046037行x1920x24bit实际用到BRAM约4.5块0大气光直方图统计2401801256桶0透射率估计盒式滤波10307202透射率行缓冲2恢复模块6405201倒数LUT4总体含控制逻辑28002100116这个规模在XC7A35T里只占不到三成LUT留着大量余量给ISP其他模块。上面是7x7窗口、1080P的配置。我把参数总结成一个通用经验公式供你估算每增加一行行缓冲需要BRAM约 行像素x字节/块容量 块盒式滤波每增加一个DSP可以处理2路乘法。4.2 接口与存储MIPI/LVDS输入、DDR交互、FMC配置通道实际工程中Dehaze模块的上游和下游接口很重要。我们这个项目的原始数据来自MIPI CSI-2接口的SensorFPGA需要先做MIPI RX的物理层和协议层解包。MIPI控制器可以买IP核也可以用开源方案但需要注意MIPI的byte clock和pixel clock的跨时钟域处理——所有像素进入Dehaze模块前我统一通过异步FIFO同步到74.25MHz像素时钟域避免亚稳态。接着是DDR读写的问题。由于我的Dehaze模块是单帧流式的外存只存大气光统计值和少量参数所以DDR占用非常小。但如果是红外加可见光融合透雾或者要做时域透雾就需要在FPGA里挂DDR3/DDR4控制器设计成AXI4接口此时带宽规划就要重新算。1080P30时RGB888一帧约6.2MBDDR3能轻松支撑3~5帧缓存但额外增加的延迟和功耗是需要在项目早期评估的。还需要考虑和MCU的互动。很多相机平台以STM32H743或是Zynq ARM做系统控制FPGA做像素处理。MCU与FPGA之间用FMC并行总线通信MCU写入去雾强度、窗口大小等参数FPGA返回统计信息和状态。Dehaze的强度可以做成参数可调通过改变透射率估计公式里的ω值来实现ω大、去雾强但可能偏暗ω小、画面自然但雾气残留多。我推荐在寄存器里开放这个旋钮。4.3 仿真验证与方法用ILA和模型对比定位中间结果仿真和调试在整个开发周期里占一半时间。我先在Matlab里把浮点算法和小型定点模型的结果导出来作为FPGA仿真行为的golden model。然后用Vivado的仿真环境跑Dehaze模块输入一张480p的有雾测试图逐模块比对暗通道、透射率图和恢复图。比对时不必逐像素比对一模一样因为盒式滤波的边界处理方式可能不同。我给了一个_PASS标准中间结果的PSNR大于35dB边界区域的差异允许放大。跑出波形图之后还要穿插“反向验证”——把板级调试的ILA抓取数据导回Matlab确认硬件中间值和仿真一致。ILAIntegrated Logic Analyzer是Vivado里最强大的片上逻辑分析仪可以抓取内部信号我通常在暗通道输出、透射率输出、恢复输出各挂一个ILA触发条件设为行同步信号抓几行数据就够了。4.4 硬件验证上板测试流程和主观画质评估仿真通过后我开始上板测试。测试流程分三步第一步是静态图测试。我准备了一批不同雾浓度的JPG图片转成BMP后通过串口或TF卡灌入FPGA的DDR然后由测试模块按像素时钟读出走完整条ISPDehaze链路再从HDMI输出到显示器。这里最关键的是观察画面中间亮度区域是否有色偏、高光区域是否过曝、天空是否存在色带。第二步是动态实时测试。接上MIPI Sensor对准室外有雾场景或者人为制造雾气的场景观察输出视频的流畅度、去雾效果和色彩自然度。一个常见问题是雾浓度随时间变化大气光估计的更新速度如何我通过调节“统计帧更新系数”解决——默认0.3更新太快会有闪烁感太慢则跟不上场景变化。第三步是算法效果的定量对比。拍下同一场景透雾开/关的对比视频用一些客观指标如对比度RMS contrast、暗通道均值、边缘强度Sobel梯度均值来量化提升幅度顺便把透雾前后的图像分别送入识别算法比如车牌识别看准确率变化。透雾通常能让一些弱边缘变得清晰识别准确率有明显提升。5. 常见问题与排查技巧实录5.1 恢复图像块状伪影严重怎么处理我自己第一次跑通硬件时输出的恢复图像有明显的分块感屏幕上一块一块的效果像低精度量化。排查后发现三个原因第一是透射率图的盒式滤波窗口太小当时用了3x3作用范围不足块状边缘没压平第二是暗通道窗口和盒式滤波窗口大小不匹配透射率图的噪声被放大第三是恢复模块的1/t查表精度不够256点8bit表在t偏小时步进太大。解决办法也很直接把透射率平滑窗口从3x3提到9x9暗通道窗口保持7x7同时把倒数表从8bit精度换成了16bit8Q8块状伪影肉眼几乎不可见。这里给一个经验盒式滤波窗口建议是暗通道窗口的1.2~1.5倍太小压不住块太大细节全糊。5.2 大气光估计不准偏色与整体发灰的根源另一个高频问题是恢复出来的图像蒙了一层灰或者大面积偏蓝偏黄。大气光A估计不准是第一嫌疑。取暗通道最大像素亮度的方法最容易翻车——场景里有纯白物体比如白车、白色建筑时A直接被拉到255恢复图的整体亮度被压暗。正确的实现是前面讲的取前0.1%像素的均值我实际测试后还加了一个约束A的初始值不低于图像最大亮度的80%不高于99%防止最终结果过曝或过暗。还有一类问题是A估计本身没问题但天的地方恢复偏色。这是因为天空区域不满足暗通道先验透射率被严重低估。我的处理方法是增加一个“天空保护”逻辑当像素亮度同时高于阈值比如220/255且饱和度低于阈值时强制透射率t不低于0.4避免过激恢复。这个逻辑只占用几十个LUT但对天空场景的画质提升极大。5.3 资源占用爆表如何做减法优化Dehaze模块如果按“教科书”方式全做下来浮点、大窗口、完整导向滤波资源很容易超。我的实际优化策略是这样的第一把所有浮点运算全定点化第二用可分离盒式滤波代替二维卷积第三行缓冲里只存灰度图或者Y通道不存RGB三通道暗通道的灰度版本对透射率估计影响很小因为暗通道本身的统计特性主要在亮度变化上。还有一个容易被忽略的点如果同一块BRAM既当行缓冲又当倒数表Vivado综合时资源会打架。最好在设计早期就做好物理规划把行缓冲和查找表分开。另外统计大气光的直方图RAM可以复用帧缓冲但要注意不同时钟域的访问仲裁问题我建议直接独立分配避免后期调试复杂度上升。5.4 调试建议先把模块切开再看整体最后分享一个调试习惯。我不建议一口气把所有模块全部连线后直接上板否则出了问题根本不知道是哪一块。我的做法是逐级验证先单独验证暗通道模块用测试图产生模型比对再验证大气光统计打印整帧统计结果跟Matlab比对第三步才是透射率和恢复。每个模块验证完再接入主链路。上板调试时如果发现画面花、闪、错行先检查同步信号和像素时钟域是否正确再怀疑运算逻辑。我遇到过一整个下午都查不出画面对不齐的问题最后发现是行有效信号的有效长度比实际行多了两个像素导致行缓冲错位。这类同步问题通过ILA抓行场信号一眼就能看出来。我个人在实际项目中的体会是FPGA透雾项目的难度不在算法有多深而在于“物理世界的一帧一帧图像如何精确映射到硬件数据流”这件事。暗通道先验算法本身并不复杂真正考验人的是行缓冲、窗口、流水线、跨时钟域这些硬件细节的把控。如果你也想做类似的项目建议从小分辨率480p开始先验证算法效果再逐级提高分辨率。把行缓冲结构和大气光统计做扎实整个Dehaze模块就已经成功了一大半。最后再补一句调试过程中一定要保存好每一版仿真波形和比对结果很多看似奇怪的问题回翻波形时往往一眼就能看出因果。