1. 项目背景与核心价值
在计算机视觉和图像处理领域,去雾算法一直是个热门研究方向。特别是在自动驾驶、安防监控等实际应用中,雾霾、烟雾等恶劣天气条件会严重影响图像质量。传统基于CPU或GPU的去雾算法虽然效果不错,但难以满足实时性要求。这就是为什么我们要在FPGA上实现实时去雾——它能提供硬件级的并行处理能力,同时保持低功耗特性。
我这次使用的黑金AX301开发板搭载的是Altera Cyclone IV系列FPGA,搭配OV5640摄像头模组,整个系统成本控制在千元以内,却能达到720p@30fps的实时处理性能。这个项目最吸引我的地方在于,它完美展示了如何将复杂的图像算法映射到硬件逻辑上,这种软硬件协同设计的思路在实际工程中非常实用。
2. 系统架构设计
2.1 硬件平台选型
选择黑金AX301开发板主要基于几个考量:
- 内置的Cyclone IV EP4CE6F17C8 FPGA提供6K逻辑单元,对于这个规模的图像处理足够用
- 板载的MIPI CSI-2接口可以直接连接OV5640摄像头,省去了额外的转接电路
- 开发板配套的资料和例程丰富,能大幅降低开发门槛
OV5640是个500万像素的传感器,我们实际使用1280x720分辨率,这样既能保证图像质量,又不会给FPGA带来太大压力。摄像头通过I2C接口配置,输出YUV422格式的数据流。
2.2 处理流水线设计
整个处理流程采用典型的流水线架构:
code复制摄像头采集 → RGB转换 → 暗通道计算 → 大气光估计 → 透射率计算 → 图像恢复 → HDMI输出
每个模块都设计为独立的工作单元,通过FIFO缓冲连接,形成生产者-消费者模型。这种设计有两个明显优势:
- 各模块可以并行工作,提高吞吐量
- 时钟域隔离更简单,每个模块可以用最适合自己的时钟频率
3. 暗通道去雾算法详解
3.1 算法原理剖析
暗通道先验理论认为,在非天空区域的局部块中,至少有一个颜色通道的强度值非常低。数学表达为:
code复制J_dark(x) = min_{c∈{r,g,b}}( min_{y∈Ω(x)}( J^c(y) ) )
其中Ω(x)是以x为中心的局部区域,J^c是彩色图像的c通道。
基于这个先验,我们可以估计大气光A和透射率t(x):
code复制A ≈ max_{x∈I}( J_dark(x) )
t(x) ≈ 1 - ω * J_dark(x)/A
最后通过大气散射模型恢复无雾图像:
code复制J(x) = [I(x) - A]/max(t(x),t0) + A
3.2 FPGA实现的关键挑战
在FPGA上实现这个算法有几个难点:
- 局部最小值计算:需要设计滑动窗口电路
- 除法运算:FPGA不适合做浮点除法,需要转换为定点数或查找表
- 内存带宽:处理高分辨率图像时需要合理设计缓存机制
我采用的解决方案是:
- 使用3x3的滑动窗口计算局部最小值
- 将除法转换为16位定点数乘法(Q8.8格式)
- 采用双缓冲机制,一块RAM用于当前帧处理,另一块用于下一帧输入
4. 关键模块实现
4.1 图像采集与预处理
OV5640输出的YUV422数据需要先转换为RGB格式。这里有个细节需要注意:摄像头输出的数据流是YCbCr 4:2:2格式,需要先做色度上采样。Verilog实现片段:
verilog复制module yuv2rgb (
input clk,
input [7:0] y, cb, cr,
output [23:0] rgb
);
// YCbCr转RGB矩阵运算
wire [15:0] r = y + 1.402*(cr-128);
wire [15:0] g = y - 0.34414*(cb-128) - 0.71414*(cr-128);
wire [15:0] b = y + 1.772*(cb-128);
// 饱和处理
assign rgb[23:16] = (r[15]) ? 8'd0 : (r[14:8] > 255) ? 8'd255 : r[7:0];
assign rgb[15:8] = (g[15]) ? 8'd0 : (g[14:8] > 255) ? 8'd255 : g[7:0];
assign rgb[7:0] = (b[15]) ? 8'd0 : (b[14:8] > 255) ? 8'd255 : b[7:0];
endmodule
4.2 暗通道计算优化
原始算法需要对每个像素的3x3邻域求最小值,直接实现需要大量比较器。我采用了一种流水线优化方案:
- 设计3行缓存(Line Buffer)存储最近3行图像
- 对每行的3个像素用比较器树求最小值
- 对3行的最小值再次比较得到最终结果
这样只需要3个行缓冲和少量比较器,大幅节省了逻辑资源。核心代码:
verilog复制module min3x3 (
input clk,
input [7:0] pixel_in,
output [7:0] min_out
);
// 三行缓存
reg [7:0] line0 [0:2];
reg [7:0] line1 [0:2];
reg [7:0] line2 [0:2];
// 行内最小值
wire [7:0] row0_min = min3(line0[0], line0[1], line0[2]);
wire [7:0] row1_min = min3(line1[0], line1[1], line1[2]);
wire [7:0] row2_min = min3(line2[0], line2[1], line2[2]);
// 最终最小值
assign min_out = min3(row0_min, row1_min, row2_min);
function [7:0] min3;
input [7:0] a,b,c;
reg [7:0] t;
begin
t = (a < b) ? a : b;
min3 = (t < c) ? t : c;
end
endfunction
endmodule
4.3 大气光估计实现
大气光理论上应该取暗通道图中亮度前0.1%的像素的平均值,但在硬件上实现排序太耗资源。我的简化方案是:
- 将图像分成16x16的子块
- 记录每个子块的暗通道最大值
- 用比较器树找出全局最大值
虽然这会略微降低精度,但实测对最终效果影响不大。实现代码:
verilog复制module atmospheric_light (
input clk,
input [7:0] dark_in,
input frame_start,
output reg [7:0] A
);
reg [7:0] block_max [0:255];
reg [7:0] global_max;
always @(posedge clk) begin
if (frame_start) begin
global_max <= 0;
// 初始化块最大值
for (integer i=0; i<256; i=i+1)
block_max[i] <= 0;
end
else begin
// 更新当前块的最大值
if (dark_in > block_max[block_idx])
block_max[block_idx] <= dark_in;
// 每帧结束时计算全局最大值
if (pixel_cnt == IMAGE_SIZE-1) begin
global_max <= block_max[0];
for (integer i=1; i<256; i=i+1)
if (block_max[i] > global_max)
global_max <= block_max[i];
end
end
end
always @(posedge clk) begin
A <= global_max;
end
endmodule
5. 系统集成与优化
5.1 时序收敛技巧
在实现过程中,最头疼的就是时序违例问题。特别是暗通道计算模块,由于组合逻辑路径太长,经常无法满足100MHz的时钟要求。我采用了几个优化手段:
- 流水线重定时:将长的组合逻辑拆分成多级流水
- 寄存器复制:对高扇出信号(如时钟使能)进行局部复制
- 逻辑重构:用查找表替代复杂的算术运算
例如,原始的RGB转XYZ模块就经历了这样的优化过程:
verilog复制// 优化前(组合逻辑太长)
always @(*) begin
x = (r*0.4124 + g*0.3576 + b*0.1805);
y = (r*0.2126 + g*0.7152 + b*0.0722);
z = (r*0.0193 + g*0.1192 + b*0.9505);
end
// 优化后(三级流水)
always @(posedge clk) begin
// 第一级:乘法
r_coef1 <= r * 0.4124;
g_coef1 <= g * 0.3576;
// ...其他系数
// 第二级:加法
x_sum1 <= r_coef1 + g_coef1;
// ...其他求和
// 第三级:最终累加
x <= x_sum1 + b_coef1;
end
5.2 资源利用优化
Cyclone IV的资源有限,必须精打细算。几个关键优化点:
- BRAM共享:多个模块共用同一块内存,通过时分复用
- DSP块利用:将乘法运算映射到专用的DSP块上
- 状态机编码:使用独热码(one-hot)优化大型状态机
资源使用对比表:
| 模块 | 优化前(LEs) | 优化后(LEs) | 节省比例 |
|---|---|---|---|
| 颜色空间转换 | 1200 | 680 | 43% |
| 暗通道计算 | 2500 | 1500 | 40% |
| 大气光估计 | 800 | 450 | 44% |
6. 实测效果与性能分析
6.1 质量评估
使用标准测试图像集进行主观评价,去雾效果明显:
- 雾天图像的对比度提升约60%
- 细节保留良好,无明显光晕伪影
- 色彩自然,没有出现严重偏色
定量指标方面,在FRIDA数据集上测得:
- 平均PSNR:28.6dB
- 处理延迟:33ms(3帧缓冲)
6.2 性能指标
系统最终达到的性能:
- 处理分辨率:1280x720
- 帧率:30fps(实际可达35fps)
- 功耗:2.1W(核心电压1.2V时)
- 资源占用:
- 逻辑单元:78%
- 存储器:65%
- DSP块:40%
7. 常见问题与调试经验
7.1 图像出现条纹干扰
现象:输出图像有规律的明暗条纹
排查:
- 检查时序约束,发现行消隐周期设置错误
- 用SignalTap抓取信号,发现FIFO读空
解决:重新计算视频时序参数,调整FIFO深度
7.2 去雾效果不稳定
现象:连续帧间亮度跳动
原因:大气光估计模块没有做帧间平滑
改进:增加IIR滤波器做时间域平滑
verilog复制// 简单的一阶IIR滤波
always @(posedge clk) begin
A_smooth <= (A_smooth * 7 + A) >> 3;
end
7.3 资源不足导致编译失败
现象:布局布线阶段报错
分析:暗通道模块占用过多LE
优化:
- 将3x3窗口改为十字形5像素窗口
- 用移位加代替部分乘法
- 重新平衡流水线级数
8. 扩展与改进方向
这个项目虽然已经实现了基本功能,但还有不少可以优化的地方:
- 自适应窗口大小:根据图像内容动态调整暗通道计算的窗口尺寸
- 多尺度处理:先对下采样图像计算粗略透射率图,再上采样细化
- 硬件加速接口:添加AXI-Stream接口,方便与其他IP核集成
在实际部署中还发现,针对特定场景(如城市道路、森林等)微调算法参数能获得更好的效果。比如对于高速公路监控,可以适当增强远处车辆的可见性,这可以通过调整透射率的下限值t0来实现。