1. 项目缘起与整体设计思路
1.1 为什么是FPGA来做图像透雾
前几天整理项目素材时翻到了去年调透雾算子的记录,觉得整个过程还挺有代表性的——从算法选型到RTL落地再到板级调试,几乎踩遍了硬件图像处理该踩的坑。今天就把这个"基于FPGA的图像电子透雾(ISP Dehaze)"项目完整拆一遍,给正在做或者准备做类似工作的朋友一个参考。
先回答一个很多人问过的问题:透雾算法用软件跑不就行了,为什么非得用FPGA?
确实,OpenCV里几行代码就能实现暗通道透雾,在PC上跑一张图也就是毫秒级。但问题在于场景——如果是车载摄像头的实时视频流,或者监控相机的全天候视频,你需要的是每一帧都在几十毫秒内完成处理,而不是"隔几秒处理一张图"。CPU的瓶颈在于串行执行,GPU的瓶颈在于功耗和成本,而FPGA恰好站在了"实时性"和"能效比"的交叉点上。更关键的是,透雾处理如果放在ISP管线里,可以直接在RAW域或者RGB域操作,避免了后端做了一遍又一遍的色彩空间转换,这是架构层面的优势。
另一个容易被忽视的原因是算法迭代的灵活性。透雾算法本身还在快速演进——从最经典的暗通道先验,到后来的颜色衰减先验、深度学习透雾。FPGA相比ASIC的优势就在于,算法大版本升级时,你重新综合一遍工程就行,不用重新流片。我见过不少团队用FPGA做ISP原型验证,等算法彻底收敛了再固化到ASIC里,这个路径本身就很成熟。
1.2 项目核心需求拆解
说回项目本身。标题是"基于FPGA的图像电子透雾(ISP Dehaze)",拆开看有三个关键词:FPGA、ISP、Dehaze。
FPGA部分,需要实现完整的硬件图像处理管线,从视频输入到透雾处理再到显示输出。ISP部分,意味着透雾不是孤立的一个算法,而是嵌在整套ISP pipeline里的一个模块——具体来说,输入可能是RAW域数据,经过Bayer插值变RGB,然后进透雾模块,出来再做白平衡、色彩校正、Gamma,最后输出YUV给显示或者编码。Dehaze部分,则是整个项目的核心算法,需要把雾天降质的图像恢复成清晰图像。
之所以强调"ISP Dehaze"而不是单纯的"FPGA Dehaze",是因为透雾算法在ISP管线中的位置很讲究。放在Bayer域做?放在RGB域做?各有各的优缺点,后面我会详细展开。但提前说结论:大多数实际工程里,透雾放在RGB域、Bayer插值之后做,这是性能和效果的平衡点。
从需求场景来看,这个项目最典型的应用就是安防监控的雨天雾天增强和车载相机的雾天视觉增强。我当时的测试素材也来自这两类场景——一组是高速公路雾天的监控视频,一组是城市道路的雨天视频。这两类场景对实时性的要求都很硬,帧率要求至少30fps,延时要求在100ms以内,所以用FPGA做硬件加速是正确选择。
项目的最终交付物是一套可在FPGA开发板上运行的透雾演示系统,包含完整的RTL代码、UART配置接口、HDMI显示通路,以及用MATLAB/Python脚本做效果对比的半自动化验证流程。
1.3 技术选型的前期调研
正式开始写RTL之前,我花了不少时间调研透雾算法在硬件上的可实现性。其实这个环节非常关键,因为很多算法在MATLAB里跑得飞起,一落到FPGA上就各种水土不服——要么是运算量太大导致资源爆炸,要么是复杂度过高导致时序收敛困难。
当时的主要候选方案有三种。
第一种是暗通道先验(Dark Channel Prior)算法,它由何恺明团队提出,核心思想是:绝大多数户外无雾图像的局部区域里,至少有一个颜色通道的像素值非常低(趋近于0)。基于这个先验,可以估算出大气光和透射率,再反解出清晰图像。这个算法的优点是物理意义明确、无雾恢复效果好,而且可以做成行流水的硬件架构;缺点是全局大气光估算需要统计全图信息,会引入一定的帧间延时。
第二种是基于**颜色衰减先验(Color Attenuation Prior)**的方法,核心思想是雾的浓度与像素亮度和饱和度的差值成正比。这个算法计算量更小,硬件实现更简单,但效果对场景比较敏感,天空区域容易偏色。
第三种是深度学习透雾,效果最好,但硬件成本极高——至少需要DSP阵列或者AI加速器辅助,纯逻辑资源很难干下来。
综合对比后,我选了暗通道先验作为主算法,理由是它效果好、原理清晰、适合流水线架构,而且在FPGA上的资源消耗可控。这就是"用最成熟的方案优先解决工程问题",不是所有项目都要上最新最潮的技术。
2. 透雾算法原理与硬件化适配
2.1 暗通道先验的数学原理
暗通道先验的公式是学术界的老朋友了,但还是花点篇幅讲讲它的物理含义,毕竟后面所有硬件模块都是围绕它设计的。
在无雾的图像中,对于任意的像素点,定义它的暗通道为:
$$J^{dark}(x) = \min_{y \in \Omega(x)} \left( \min_{c \in {R,G,B}} J^c(y) \right)$$
其中$c$表示RGB三个颜色通道,$\Omega(x)$是以像素$x$为中心的局部区域(通常取15x15或更小)。这个式子的意思是:在图像的任意一个小局部区域里,至少有一个通道的亮度值很低。如果这个值趋近于0,就是暗通道先验成立的区域。
那雾天图像的成像模型,咱们用一个经典的大气散射模型来描述:
$$I(x) = J(x)t(x) + A(1 - t(x))$$
其中$I(x)$是有雾图像(也就是传感器直接拍到的),$J(x)$是清晰无雾的图像(我们希望恢复的),$t(x)$是透射率,表示光线穿过大气到达相机的比例,$A$是全局大气光。
这个公式的物理含义其实很直观:相机接收到的光由两部分组成,一部分是景物反射的光经过雾气衰减后到达相机的$J(x)t(x)$,另一部分是大气光$A$被雾气散射进相机的$A(1-t(x))$。雾越浓,$t(x)$越小,大气光贡献越大,图像看起来就是白蒙蒙的。
要恢复$J(x)$,就得知道$A$和$t(x)$。暗通道先验在这里的价值是:它告诉我们,在无雾图像的暗通道里,$J^{dark}(x)$趋近于0。把大气散射模型代入暗通道的计算式,再结合这个先验,就能解出透射率:
$$t(x) = 1 - \omega \cdot \frac{I^{dark}(x)}{A}$$
这里的$\omega$是一个[0,1]之间的常数(通常取0.95),用于保留一点雾气感避免恢复过头,让图像看起来更自然。$I^{dark}(x)$是输入有雾图像的暗通道。大气光$A$通常取暗通道中亮度最高的前0.1%像素对应位置的原图像素值。
恢复公式则是:
$$J(x) = \frac{I(x) - A}{\max(t(x), t_0)} + A$$
其中$t_0$是一个下限阈值(通常取0.1),防止透射率太小导致除以0和噪点放大。
这些公式转成硬件实现时,我的处理思路是把问题拆成四步:一算暗通道,二估大气光,三求透射率,四做恢复映射。四个步骤分别对应独立的硬件模块,模块之间用FIFO或者行缓存衔接。
2.2 算法到硬件的数据流重构
上面的数学推导听起来挺顺畅,但把算法落到FPGA上的时候,你会发现"软件思维"和"硬件思维"之间有道坎。
软件实现里,暗通道计算是在整帧图像上先做一次滑动窗口统计,存下整帧的暗通道图,然后再找全局大气光,再逐像素运算。这种"先全图统计、后逐像素处理"的思路在FPGA上行不通,因为FPGA的内存资源有限,不可能把整帧图像缓存下来随便访问。
硬件上必须改成行流水架构,让数据像水一样流过每个处理阶段。具体来说:
- 暗通道计算:需要$N\times N$的窗口(比如5x5),则需要$N$行的行缓存(Line Buffer)。每个时钟周期进来一个像素,同时输出这个像素周围窗口内的最小值。这一步是纯组合逻辑加移位寄存器,非常容易流水化。
- 大气光估计:这是暗通道先验里最不适合硬件实现的部分,因为需要全图的暗通道统计结果。我的做法是双帧处理的思路:当前帧计算暗通道并做透射率估计,同时把暗通道的统计结果用来估算大气光,下一帧的透射率恢复使用这一帧得到的大气光。对于视频流来说,相邻帧的大气光基本不变,所以这个近似完全够用,却能把非行流水的全局统计变成可流水的帧级处理。
- 透射率估计:有了暗通道值和大气光,透射率就是一两个减法、一次除法、一次乘法的事。除法在FPGA里比较奢侈,我用了查表法——把大气光A的倒数预先算好存在BRAM里,透射率就用乘法替代除法。
- 恢复映射:也是按像素级的公式直接算,配合一个透射率下限钳位。
除了算法本身,还要考虑数据的位宽和精度。我用的是8-bit RGB输入,中间暗通道用8-bit,透射率用12-bit定点数(8-bit整数部分+4-bit小数部分)来保证精度。处理完成后还要做饱和度调整和亮度调整,这两个模块是透雾效果能不能"看着舒服"的关键,后面会细说。
2.3 透雾模块在ISP管线中的位置
前面说过,透雾模块不是独立存在的,它嵌在ISP pipeline里。我当时用的是自己设计的一条简化ISP链路,顺序是:Sensor RAW输入 -> Bayer插值(RGB) -> 黑电平校正 -> 透雾Dehaze -> 白平衡 -> 色彩校正 -> Gamma -> YUV输出。
为什么把透雾放在白平衡前面?这里有个工程上的权衡。
一种方案是把透雾放在RAW域、Bayer插值之前做。优点是RAW域数据还没有经过色彩处理,数据量小(单通道),处理带宽低,资源省。缺点是Bayer域的透雾算法需要处理的是单通道的暗通道信息,颜色信息没有完全解出来,恢复效果打折。
另一种方案是把透雾放在RGB域。缺点是数据量大三倍(三通道),BRAM资源更紧张,但好处是按真实颜色计算暗通道,效果明显更好。
我最终选了RGB域处理。原因很简单:效果优先,而且现代FPGA的BRAM容量早就不是瓶颈了。Xilinx 7系列/Kintex系列动辄几百个BRAM块,做行缓存绰绰有余。
还有几个细节值得注意。透雾处理会导致整体亮度偏暗,需要加一个增益补偿。我是在透雾模块内部做了一个可配置增益,范围1.0~1.5,通过寄存器配置。另外,透雾模块对天空区域容易过度增强,产生光晕效应,我用的是引导滤波的近似简化版——做一个边缘保护的平滑处理,降低光晕感。这个模块说复杂也不复杂,本质上是一个小尺寸的低通滤波器配合自适应阈值判定,代价很小,但对画面观感提升明显。
3. FPGA硬件架构与模块设计
3.1 系统整体架构
整个系统的架构按照标准视频处理流程来搭,顶层结构可以分成五块:视频输入模块、ISP预处理模块、Dehaze核心模块、ISP后处理模块和视频输出模块。
我用的是Xilinx Artix-7系列的开发板(具体型号XC7A200T),板载HDMI输入输出接口、DDR3存储、UART接口。FPGA内部通过AXI4总线互联,DDR3主要用于帧缓存——不过这里有个设计取舍,透雾模块本身是行流水处理,不需要整帧缓存,DDR3主要是给视频输入输出做帧率匹配和图像缩放用的。
输入端的做法是:HDMI信号进来,先过silicon image的接收芯片解出并行RGB数据,然后在FPGA内部转成行场同步信号,经过一个FIFO隔离时钟域,进入ISP预处理。输出端则是反向的流程,ISP后处理完的数据经过FIFO跨时钟域后,送到HDMI发送芯片编码输出。
值得说明的是,这个系统跑的是1080p@30fps。1080p每帧1920x1080,每像素3字节(RGB888),一帧原始数据约6MB,帧率30fps,数据率约180MB/s。这个带宽在Artix-7上没什么压力,关键是时序上要保证每个像素在300MHz的像素时钟内完成从输入到输出的全链路处理。
3.2 行缓存与窗口生成器设计
行缓存是整个透雾模块的基础设施,它负责把逐行输入的像素流变成窗口形式的像素块,供暗通道计算使用。
我的窗口大小选的是5x5。这个尺寸是权衡后的结果——窗口太小,暗通道估计不准,透射率图噪声大;窗口太大,需要更多行缓存,资源翻倍。5x5是个经验值,测试下来在透雾效果和资源消耗之间取得了不错的平衡。
行缓存的实现方式有两种选择。一种是用FPGA的BRAM搭建,另一种是用移位寄存器(SRL16E)搭建。BRAM方式的优点是容量大,缺点是只能顺序读写;移位寄存器方式的优点是灵活,适合窗口滑动,但深度有限。
我最终用的是BRAM构成的多行缓存结构:用两个BRAM构建5行缓存,每行深度1920,宽度8-bit(单通道暗通道计算只需要一个通道的最大值/最小值,实际上我缓存三个通道,深度1920,宽度24-bit,这样一次读就能取出窗口内三通道的数据)。窗口生成器每个时钟周期从行缓存里读出第0行、第1行、第2行、第3行、第4行的当前像素,组成一个5x5窗口。
实现上的一个经验是行缓存的写地址和读地址要错开,写地址超前读地址,这样可以避免读到未更新的数据。另外一个容易被坑的地方是窗口数据和当前像素的时序对齐——窗口中心像素要和其他位置的像素对齐到同一个时钟周期,否则后面的计算全都错位。我的做法是在窗口生成器输出端加几级寄存器对齐,保证每个窗口数据的中心像素绝对同步。
3.3 暗通道计算模块的RTL实现
暗通道计算模块要做的事情非常纯粹:对5x5窗口内的25个像素,分别取R、G、B三个通道的最小值,再把三个最小值再取一次最小值,得到该像素的暗通道值。
用伪代码描述就是:
for (i = 0; i < 5; i++) { for (j = 0; j < 5; j++) { min_r = min(min_r, window[i][j].r); min_g = min(min_g, window[i][j].g); min_b = min(min_b, window[i][j].b); } } dark_channel = min(min_r, min_g, min_b);RTL实现的时候,我分了三级流水:第一级做25个像素的R/G/B通道各自的最小值,第二级做9个分组最小值,第三级做最终最小值。这样每一级的组合逻辑深度都控制在2~3个比较器以内,时序收敛很容易。
资源消耗方面,这个模块几乎不用DSP(比较器用LUT实现),纯逻辑资源大概占用800个LUT左右,非常轻量。运行频率在200MHz以上没有压力。
这里有一个关键细节:暗通道计算要有一个使能信号控制边框像素。图像的边缘像素凑不齐5x5窗口,硬件上通常的做法是边缘保持原值(不做透雾),或者对边缘窗口做镜像填充。我选择的是保持原值,即边缘一圈不做透雾处理,视觉上完全看不出差别,实现却简单很多。
3.4 大气光与透射率硬件实现
大气光估计在硬件上是个微妙的部分。假如原样照搬论文做法——对全图暗通道做统计、取前0.1%最亮像素,在硬件里几乎没有直接实现方式,因为每来一个像素就更新统计,全图处理完再计算,这天然就不是行流水的路子。
我的妥协方案是分块统计:把整帧图像在垂直方向分成16个块,每个块独立统计暗通道的最大值和对应的原图RGB值。帧结束时,把16个块的统计结果汇总,选择最大的那个作为全局大气光A。这个做法在效果上非常接近全局统计,因为大气光通常是天空或者远处的雾气,无论怎么分块,最亮的块一定能捕获到它。
透射率的计算过程相对简单。暗通道值$I^{dark}$出来了,大气光A也定了,那么透射率:
$$t(x) = 1 - \omega \cdot \frac{I^{dark}(x)}{A}$$
硬件上实现除法不划算,我预先用软件算出$1/A$,存成16-bit定点数(8-bit整数部分+8-bit小数部分)放进寄存器。然后透射率就是一次乘法、一次减法的事。$\omega$取0.95,透射率下限$t_0$取0.1,用比较器做钳位。
透射率图做出来后,还需要做一次最小值滤波平滑。这一步在软件里是导向滤波,硬件上我用的是3x3的最小值滤波做替代。效果上,虽然不如导向滤波那么精细,但光晕抑制的效果已经很不错,而且资源消耗极其有限。如果对画质有更高要求,可以考虑用分离的横向+纵向一维滤波近似二维滤波,能省不少BRAM。
3.5 透雾映射与后处理管线
透射率求出来了,大气光求出来了,最后的恢复公式:
$$J(x) = \frac{I(x) - A}{\max(t(x), t_0)} + A$$
硬件实现同样要规避除法。我预先算好一张透射率映射表——透射率的值域是0.1到1.0,量化成256级,每一级对应一个16-bit的增益系数$1/\max(t, t_0)$,存在BRAM里。实际处理时,直接用透射率查表得到增益,然后做乘法和加减法运算。
这里还有一个工程细节:恢复出来的$J(x)$可能出现超过8-bit范围的值,需要对输出做饱和截断,而不是简单的位截断。我用的是两个比较器实现饱和处理,大于255输出255,小于0输出0。这个细节很多初学者会忽略,导致图像出现条纹。
恢复完的RGB图像还要经过两级后处理。第一级是亮度/饱和度调整。透雾后的图像通常会显得有点灰(因为整体被拉伸了),我加了一个可配置的饱和度增益寄存器(范围0.7~1.3),R、G、B三个通道做了基于亮度系数的饱和度扩展。第二级是一个简单的边缘增强,用的是3x3拉普拉斯算子的近似版,把边缘信息叠加回原图,提升清晰度。这个边缘增强模块是可以旁路的,通过寄存器配置。
4. 系统调试与效果调优实录
4.1 仿真验证与常见bug
开发过程中,验证占了大半时间。FPGA开发有个铁律:仿真阶段没查出来的bug,上板调试的成本要高一个数量级。
我的验证策略是三层。第一层是模块级仿真,用ModelSim或者Vivado Simulator跑透雾核心模块,输入用MATLAB生成的有雾测试图转成Hex文件,读入仿真,输出存成Hex文件,再转回图片和MATLAB的参考结果做对比。这个环节最容易抓到的bug是位宽越界和时序错位。位宽越界体现在仿真波形里就是出现X态或者毛刺,查起来反而不难;时序错位则诡异得多,输出图像的边缘会出现错位条纹,看起来像鬼影重叠,实际上就是行缓存的读写时序没有对齐。
第二个高发bug是饱和截断被遗漏。有一次我在仿真里发现恢复出来的图像的天空区域出现了许多黑色斑点,查了很久,最后发现是$J(x)$超出了255之后没有饱和截断,直接截断低位导致负数被映射到了0。这类问题如果仿真时只用干净的无雾图像做测试几乎发现不了,必须用天空占比较大的雾天图像测试。
第三类bug是跨时钟域问题。HDMI的像素时钟和FPGA内部处理时钟不同源,输入输出FIFO如果深度不够或者标志位逻辑错误,就会出现画面撕裂。我的解决方法是只让FIFO跨越时钟域,不在跨时钟域路径上做任何组合逻辑。
4.2 上板调试的避坑经验
仿真跑通了不等于板子能跑,上板调试才是真正考验工程能力的时候。
我遇到的第一块硬骨头是大气光模块的帧同步问题。前面说了大气光用的双帧估算,当前帧的结果要给下一帧用。但板子上电的初始几帧,大气光寄存器的值是全零,会导致透射率计算异常——第一帧恢复出来是全黑的。解决方法是加了一个帧计数复位逻辑,在检测到前几帧的VBLANK(帧消隐)时,用默认值填充大气光寄存器,等统计结果有效后再切换到估算值。
第二个问题是行缓存初始态。每次VSYNC信号到来时,行缓存里还残留着上一帧的数据。如果不做清空,新帧的头部像素会和残留数据混在一起,产生画面顶部的一条斜纹。解决办法是在VSYNC到来时,用复位信号清空行缓存的写地址,并且把有效数据标志位关掉几个周期。
第三个是DDR3带宽瓶颈。虽然透雾模块本身不需要DDR3,但整条系统链路上,HDMI输入写入DDR3、从DDR3读出做缩放和叠加、再输出HDMI,三个通路共享DDR3带宽。在1080p@60fps下,DDR3带宽利用率接近90%,偶尔会出现刷新冲突导致的画面闪烁。我把刷新优先级调低,并把输出通路的AXI突发长度改成16,总算把问题压下去了。
4.3 定量调优:从PSNR到主观观感
说到调优,必须提一个观点:算法的客观指标和主观观感,有时候并不一致,而工程上需要优先照顾主观观感。
我用MATLAB脚本对透雾前后图像的PSNR(峰值信噪比)做了对比。对于标准的薄雾图像,PSNR可以提升8~12dB;对于浓雾图像,提升只有3~5dB——浓雾场景下暗通道信息几乎丢失,算法能恢复的信息有限。而对比SSIM(结构相似性指数),提升通常在0.1~0.2之间。
但更重要的是调出一组让眼睛舒服的参数。这里有三个关键寄存器值要重点调:
- 透射率下限$t_0$:调太大会导致恢复不彻底,图像还是有点蒙;调太小天空区域会出现伪影。我用的是0.1到0.2之间微调,最终稳定在0.15。
- $\omega$参数:控制保留雾气的程度。0.95是论文原值,但实际调试发现0.85~0.9在监控场景下观感更好,因为完全去掉雾气反而显得图像"发干",不自然。
- 饱和度和亮度增益:这两个参数直接决定最终画面观感。我调的默认值是饱和度1.15倍、亮度增益1.2倍,测试视频里无论白天还是夜晚的效果都能接受。
4.4 资源占用与性能报告
整个系统在Artix-7 XC7A200T上的资源占用如下:
| 资源类型 | 使用量 | 总容量 | 占用率 |
|---|---|---|---|
| LUT | 41200 | 134800 | 30.5% |
| Flip-Flop | 28600 | 269200 | 10.6% |
| BRAM (36Kb) | 86 | 365 | 23.6% |
| DSP48E1 | 42 | 740 | 5.7% |
| 时钟频率 | 200MHz | - | - |
透雾核心模块本身的资源占比大约是整个系统的40%,其余被视频输入输出、缩放、叠加等逻辑消耗。时序方面,关键路径在透射率查表和恢复映射模块之间,编译后WNS(最差负时序裕量)约0.8ns,余量充足。
性能方面,实测1080p@30fps下,从HDMI输入到HDMI输出的端到端延迟约为3帧时间(约100ms)。其中真正由透雾算法引入的延迟只有不到2行的时间,剩下的延迟基本来自帧缓存同步和显示刷新的固有等待。如果不需要叠加OSD和多帧缓存,透雾模块的纯行流水延时就几十个时钟周期,这个量级在工业场景下完全可以接受。
5. 效果对比与常见问题速查
5.1 不同场景的透雾效果对比
我用了三类典型场景做了系统化的测试,分别是薄雾场景、浓雾场景、雨天场景。因为github或者社区上找不到统一评测集,样本主要来自实际拍摄和公开测试图库,测试框是1080p视频流。
薄雾场景下(能见度200m左右),恢复后的图像对比度明显提升,远处山体和建筑的轮廓清晰度改善显著。主观观感上,天空区域颜色略微偏冷(因为大气光估计偏蓝),但完全在可接受范围内。
浓雾场景(能见度50m以内),恢复效果有限,暗处能看到细节但整体还是白茫茫的。这里有个物理学规律无法绕过:雾太浓时,传感器根本没有捕捉到足够的反射光,任何算法都不可能无中生有。工程上对浓雾场景的常规处理不是"完全恢复",而是"尽量增强对比度,有没有用另说"。
雨天场景比较特殊。雨滴会造成局部强反射亮斑,暗通道先验容易把雨滴反射误判为"雾气",进而过度处理。我的应对方案是在Bayer插值后、透雾之前,加一个简单的雨滴检测模块——通过高亮和位置特征标记雨滴区域,在暗通道计算时把标记区域的像素排除在最小值统计之外。这样一来,雨滴的影响被明显抑制,透雾效果也能保留。
5.2 透雾参数调节建议
如果你在自己的系统上复现透雾模块,我建议按照下面的顺序调参:
- 先定窗口大小。5x5是通用值,如果你的场景里雾比较均匀(比如海面),用3x3也行,动态范围和细节保留会更好;如果雾分布很不均匀(比如城市街道),7x7窗口更稳。
- 再调透射率下限$t_0$。从0.1起步,观察天空区域。如果出现色块或伪影,上调到0.15;如果恢复不彻底、天空发白,下调到0.08。
- 然后调**$\omega$**。从0.9起步,观察远处细节。太闷就降低,太干就升高。
- 最后调饱和度和亮度增益。这两个参数的调节没有通用公式,只能靠人眼积累感觉。
一个容易被忽视的经验是:参数要按场景分组保存,因为单组参数很难覆盖所有天气条件。我在系统里做了4组预设——薄雾/中雾/浓雾/雨天,动了寄存器切换的自动模式。接入光传感器的话,还可以全自动切换。
5.3 常见问题与排查速查表
这里列一份我调试过程中碰到的典型问题和排查方法,基本都是踩过坑之后的总结。
| 问题现象 | 可能原因 | 排查方法 |
|---|---|---|
| 输出图像全黑 | 大气光寄存器初始为0,透射率计算异常 | 检查帧计数复位逻辑是否生效,确保前几帧用默认大气光填充 |
| 画面边缘有条纹错位 | 行缓存读写时序未对齐 | 检查窗口生成器的对齐寄存器,确认中心像素和窗口数据同步 |
| 天空区域出现紫色斑块 | 透射率$t_0$太小,恢复时放大噪声 | 上调透射率下限到0.15左右;检查是否有饱和截断 |
| 图像整体偏暗 | 透雾后未做增益补偿 | 检查后处理模块的亮度增益寄存器是否配置正确 |
| 画面顶部一段斜纹 | VSYNC复位时行缓存未清空 | 在VSYNC后清空行缓存地址,关掉几个周期的数据有效信号 |
| 动态场景出现光晕 | 暗通道窗口太大或透射率未平滑 | 减小窗口尺寸,或增加3x3最小值滤波做透射率平滑 |
| 雨滴区域过度增强 | 雨滴反射被误判为雾 | 加入雨滴检测/排除模块,或调整暗通道计算的掩膜 |
| 帧率上不去 | DDR3带宽饱和或时序路径太长 | 检查AXI突发长度设置,优化关键路径的流水级数 |
5.4 透雾效果的主观评估方法
搞硬件的人往往习惯用数据说话,但透雾效果这件事,硬件指标不能完全替代主观观察。我强烈建议在调试阶段建立一套简单的"主观评分流程":固定几个测试视频片段,包含薄雾、浓雾、雨天、逆光等场景,每调整一次参数,就用HDMI实时输出到显示器或者接采集卡录制,然后对比透雾前和透雾后的画面。
评分维度可以包括:细节恢复程度、天空区域自然度、边缘过度增强程度、色彩还原度、动态场景稳定性。每个维度打1到5分,最后求加权总分。这样做的好处是,当你改动一个参数导致PSNR提升了0.5dB但主观观感明显变差时,不会被数字骗过去——这种"指标上升、观感下降"的情况,我在调饱和度增益和边缘增强强度时碰到过不止一次。
6. 工程优化空间与扩展方向
6.1 算法层面:从暗通道到深度学习
如果要把这个项目继续往前推,算法层面有几个明确的方向。
第一个方向是用深度学习模型替换暗通道先验。现在轻量级的透雾网络(比如AOD-Net的硬件友好变体)已经可以做到在FPGA上跑实时,不过资源消耗比暗通道大很多。如果你用的是Zynq UltraScale+这类带AI引擎的FPGA,可以考虑把透雾网络部署到AI引擎上,其他ISP处理留在可编程逻辑里。
第二个方向是把单帧透雾扩展为视频透雾。单帧的暗通道先验算法,对视频流的时序一致性其实是不够的,帧与帧之间的亮度突变和透射率抖动会让人眼感到闪烁。改进方法是加入时域滤波器——对透射率图做时间上的低通滤波,或者引入光流信息做运动补偿。不过时域滤波会显著增加硬件复杂度,属于"效果天花板比较低、工程复杂度比较高"的方向。
第三个方向是透雾与HDR(高动态范围)的融合。雾天场景往往天空很亮、地面很暗,动态范围极大。把透雾和HDR放在一条管线里做,比如对暗通道估计做自适应分区,可以实现更自然的动态范围压缩。这个方向目前学术界有论文支持,工程上还比较新鲜,感兴趣的可以关注。
6.2 架构层面:多路摄像头并行透雾
在实际的安防或者车载场景里,一个FPGA芯片上往往要跑多路视频输入——比如车载的环视系统是四路鱼眼摄像头同时工作。
对于多路透雾,有两种架构选择。第一种是时分复用:多路视频源通过仲裁器轮流进入透雾模块,由于每路视频的像素时钟是独立的,需要先做像素缓冲和时钟域转换,再复用一套透雾IP。这种方案资源最省,但时延会变大,而且四路输入的分辨率不能太高,否则中间缓冲的FIFO深度会撑不住。第二种是模块复制:把透雾模块例化四份,每一路独享一套硬件。资源消耗翻四倍,但每一路的延迟保持最低,适合对实时性要求极高的场景。
我建议先按第二种方案做原型验证——因为在Artix-7上,整个透雾模块的资源占用量并不大,四路复制后总占用率仍然能控制在可接受范围内。等算法收敛、场景明确了,再优化成时分复用,把资源省下来。
6.3 供应链与长期维护视角
作为一个嵌入式方向的FPGA工程师,选型时还得多想一步长期可维护性。
芯片选型上,我用的Artix-7在工业级温度范围、供货稳定性、工具链成熟度方面都有保障。如果你做的是车规级项目,可以关注一下Zynq UltraScale+或者国产的紫光同创、安路等FPGA方案。在国产FPGA上移植这个项目的成本主要在于工具链的适配,RTL代码本身是跨平台的。
尤其是提到Xilinx Vivado工具链的话,Xilinx的Vivado对Artix-7的支持已经非常成熟,IP核可以直接用免费的Vivado ML Standard版生成;如果切到国产FPGA,比如紫光同创的PDS或者高云的IDE,大部分RTL代码可以无缝移植,但需要重新评估IP核的兼容性,像DDR控制器、HDMI收发这些通常要换成厂商自己的IP或第三方兼容方案。
另外提醒一个非常实际的点:IP核的授权和工具链版本要提前锁定。有些厂商的IP核授权是绑定工具版本的,如果你中途升级Vivado或者PDS,之前生成的IP核可能全部要重新生成,非常影响项目进度。我自己吃过这个亏,现在做项目会先把工具链版本写死在配置管理文档里。
6.4 扩展应用:嵌入式透雾不止于ISP
最后说一个让我自己都觉得意外的发现:透雾算法并不仅仅用于ISP。它有相当多的扩展场景,而且每个场景都能复用这套硬件架构。
医疗影像:内窥镜图像中,组织表面覆盖的体液就像"雾气",透雾算法的暗通道先验可以直接套用,帮助医生看清组织细节。
水下成像:水下图像的散射模型和大气散射模型极其相似,本质区别只是介质从空气变成了水。暗通道先验在水下图像增强里有大量论文验证,FPGA实现的架构几乎原封不动。
工业检测:恶劣环境下的视觉检测,比如粉尘车间里的设备监控、蒸汽环境里的管道检测,透雾都能做实时增强。
遥感图像:卫星或者无人机航拍图像受大气散射影响严重,透雾在预处理阶段做增强,能提升后续目标检测的准确率。
这些方向的应用需求,核心算法都能回归到成像模型和透射率估计上,而我已经调通的FPGA行流水架构,可以快速复用——只需要调整参数、接口和分辨率,硬件主体不需要大改。这也算是我做这个项目最大的收获,一套架构,多种应用,投入产出比很高。
我自己在项目后期的体会是,不要被"FPGA透雾"这个具体的名字框住思路。这个项目真正交付的是一套"基于暗通道先验的实时图像增强系统的硬件实现方案",透雾只是它的第一个实例。掌握了这套方案,就等于掌握了在FPGA上做实时图像增强类算法的基本方法论——行缓存怎么搭、全局统计怎么近似成行流水的局部处理、除法怎么用查表替代、算法效果和硬件资源怎么权衡。这些方法论,比透雾本身的值钱多了。