当我们需要在FPGA上实现实时图像缩放时,双线性插值算法往往会遇到三个关键瓶颈:浮点运算开销、内存带宽压力和时序收敛难题。我在去年设计智能摄像头ISP流水线时就深有体会——原本在MATLAB里运行完美的算法,移植到FPGA后却出现了画面撕裂和资源爆表的问题。
浮点运算在硬件中会消耗大量DSP资源。以Xilinx的Zynq-7020为例,单个浮点乘法器就要消耗近200个LUT和2个DSP单元。而一次完整的双线性插值需要至少4次浮点乘加运算,这对资源受限的嵌入式设备简直是灾难。更麻烦的是,传统实现中像(x-[x])这样的坐标偏移量计算会产生非对齐内存访问,导致DDR控制器效率直线下降。
第一步是把浮点坐标转换为定点数。我习惯用Q1.15格式表示0-1之间的归一化坐标(1位整数+15位小数),这样既能保证亚像素级精度,又方便用简单的移位操作代替除法。具体操作如下:
verilog复制// 坐标归一化示例 (输入坐标(x,y), 输出Q1.15格式)
module coord_normalize (
input [9:0] x_in, // 10bit输入坐标
output reg [15:0] x_out // Q1.15输出
);
always @(*) begin
x_out = {1'b0, x_in, 5'b0}; // 等价于x_in/1023*(2^15-1)
end
endmodule
双线性插值的核心是四个权重系数计算。通过预计算可以发现,当使用Q1.15格式时,四个权重系数之和永远等于32767(即0x7FFF)。这个特性让我们可以只计算三个权重,第四个用减法得到:
verilog复制// 优化后的权重计算模块
module weight_calc (
input [15:0] u, v, // Q1.15格式坐标偏移
output [15:0] w11, w21, w12, w22
);
wire [31:0] w11_tmp = (32767 - u) * (32767 - v);
wire [31:0] w21_tmp = u * (32767 - v);
wire [31:0] w12_tmp = (32767 - u) * v;
assign w11 = w11_tmp >> 15;
assign w21 = w21_tmp >> 15;
assign w12 = w12_tmp >> 15;
assign w22 = 32767 - w11 - w21 - w12; // 关键优化点
endmodule
实测这个改动能节省25%的乘法器资源,在Artix-7上LUT使用量从843降到621。
为了达到实时处理1080p@60fps的要求,我设计了如图所示的四级流水线:
verilog复制// 流水线控制示例
always @(posedge clk) begin
// Stage1: 坐标生成
x_frac <= target_x * scale_factor;
y_frac <= target_y * scale_factor;
// Stage2: 像素预取
pix11 <= ddr_read(x_base, y_base);
pix21 <= ddr_read(x_base+1, y_base);
// ...其他像素读取
// Stage3: 权重计算
w11 <= (x_frac[15:0] * y_frac[15:0]) >> 15;
// ...其他权重计算
// Stage4: 混合输出
out_pixel <= (pix11*w11 + pix21*w21 + pix12*w12 + pix22*w22) >> 15;
end
图像边缘处理是个大坑。我的经验是采用镜像扩展法,用简单的地址映射解决边界问题:
verilog复制// 边界安全读取模块
function [7:0] safe_read;
input [10:0] x, y;
input [10:0] width, height;
begin
x = (x >= width) ? (2*width-1-x) : x;
y = (y >= height) ? (2*height-1-y) : y;
safe_read = ddr_read(x, y);
end
endfunction
这种方法比补零法PSNR能提高3-5dB,而且几乎不增加硬件开销。
在XC7A100T上做的对比测试显示出有趣现象:
| 位宽配置 | LUT消耗 | DSP消耗 | PSNR(dB) |
|---|---|---|---|
| Q1.7 | 412 | 2 | 28.7 |
| Q1.11 | 587 | 4 | 34.2 |
| Q1.15 | 843 | 8 | 38.9 |
| Q1.19 | 1211 | 16 | 39.1 |
可以看到Q1.15到Q1.19的PSNR提升不到0.2dB,但资源消耗却暴涨。对于大多数监控场景,Q1.11已经足够。
更聪明的做法是对YUV色彩空间区别对待。人眼对亮度更敏感,可以采用:
配合以下代码实现精度转换:
verilog复制// YUV混合精度处理
wire [15:0] y_weight = weight; // 亮度保持高精度
wire [11:0] uv_weight = weight >> 4; // 色度降精度
always @(*) begin
y_out = (y1*y_weight + y2*(32767-y_weight)) >> 15;
u_out = (u1*uv_weight + u2*(2047-uv_weight)) >> 11;
end
这个技巧在保持主观画质的同时,节省了约30%的BRAM带宽。