2.1 Gaussian 模糊
2.1.1 数学模型
Gaussian 模糊是图像处理中最常用的低通滤波器,用于去除高频噪声,也是 Canny 边缘检测的预处理步骤。
二维 Gaussian 函数:
G(x,y) = (1 / 2πσ²) × exp(-(x² + y²) / 2σ²)
σ=1.0 时的 5×5 离散近似核(整数化):
2 4 5 4 2
4 9 12 9 4
5 12 15 12 5
4 9 12 9 4
2 4 5 4 2
归一化因子:所有系数之和 = 159
输出:G_out(y,x) = Σ K(i,j) × I(y+i, x+j) / 159
2.1.2 朴素实现的问题
5×5 卷积的朴素实现:
每个输出像素需要 25 次乘法 + 24 次加法
25 次乘法 -> 25 个 DSP48(资源消耗大)
关键路径长 -> 时序难以收敛
1080P60 资源估算(朴素实现):
DSP48:25 个
LUT:约 800 个
BRAM36:4 个(4 条 Line Buffer)
FF:约 1000 个
2.1.3 可分离滤波器优化
二维 Gaussian 核可以分解为两个一维核的外积,这是最重要的优化手段:
G(x,y) = Gx(x) × Gy(y)
一维 Gaussian 核(σ=1.0,5 点):
[2, 4, 5, 4, 2],归一化因子 = 17
两步实现:
Step 1:水平方向一维卷积
H(y,x) = Σ Kx(j) × I(y, x+j),j ∈ {-2,-1,0,1,2}
Step 2:垂直方向一维卷积
G(y,x) = Σ Ky(i) × H(y+i, x),i ∈ {-2,-1,0,1,2}
资源对比:
朴素实现:25 次乘法
可分离实现:5 + 5 = 10 次乘法
节省 60% 的乘法资源
2.1.4 完整 RTL 架构
// ─────────────────────────────────────────────
// 可分离 Gaussian 5×5 滤波器
// 分两级流水线:水平卷积 -> 垂直卷积
// ─────────────────────────────────────────────
module gaussian_5x5 (
input wire clk,
input wire rst_n,
input wire i_valid,
input wire i_tuser,
input wire i_tlast,
input wire [7:0] i_pixel,
output wire o_valid,
output wire o_tuser,
output wire o_tlast,
output wire [7:0] o_pixel
);
// ─── 水平卷积 ───────────────────────────────
// 系数:[2, 4, 5, 4, 2],归一化因子 17
// 需要 5 个像素:x-2, x-1, x, x+1, x+2
// 因果实现:使用 x-4, x-3, x-2, x-1, x
// 即 4 级移位寄存器
reg [7:0] h_sr [0:3]; // 水平移位寄存器
integer i;
always @(posedge clk) begin
if (i_valid) begin
h_sr[0] <= i_pixel;
for (i = 1; i <= 3; i = i+1)
h_sr[i] <= h_sr[i-1];
end
end
// 水平卷积计算(1 级流水线)
reg [11:0] h_sum;
reg h_valid, h_tuser, h_tlast;
always @(posedge clk) begin
h_sum <= 2*h_sr[3] + 4*h_sr[2] + 5*h_sr[1]
+ 4*h_sr[0] + 2*i_pixel;
h_valid <= i_valid;
h_tuser <= i_tuser;
h_tlast <= i_tlast;
end
// 除以 17(近似:>> 4 + 微调,或用 DSP 实现精确除法)
// 精确方法:乘以 (1/17 的定点近似) = 乘以 15 再右移 8 位
// 1/17 ≈ 15/256,误差 < 0.1%
wire [7:0] h_out = h_sum[11:4]; // 简化:右移 4 位(除以 16,近似除以 17)
// ─── 垂直卷积 ───────────────────────────────
// 需要 5 行历史数据:4 条 Line Buffer
parameter WIDTH = 1920;
(* ram_style = "block" *) reg [7:0] lb [0:3][0:WIDTH-1];
reg [10:0] v_addr;
reg [7:0] v_row [0:4]; // 5 行窗口
always @(posedge clk) begin
if (h_valid) begin
// 更新 Line Buffer(级联)
lb[0][v_addr] <= h_out;
lb[1][v_addr] <= lb[0][v_addr];
lb[2][v_addr] <= lb[1][v_addr];
lb[3][v_addr] <= lb[2][v_addr];
// 读出 5 行数据
v_row[0] <= lb[3][v_addr]; // y-4 行
v_row[1] <= lb[2][v_addr]; // y-3 行
v_row[2] <= lb[1][v_addr]; // y-2 行
v_row[3] <= lb[0][v_addr]; // y-1 行
v_row[4] <= h_out; // 当前行
v_addr <= (v_addr == WIDTH-1) ? 0 : v_addr + 1;
end
end
// 垂直卷积计算(1 级流水线)
reg [11:0] v_sum;
reg v_valid, v_tuser, v_tlast;
always @(posedge clk) begin
v_sum <= 2*v_row[0] + 4*v_row[1] + 5*v_row[2]
+ 4*v_row[3] + 2*v_row[4];
v_valid <= h_valid;
v_tuser <= h_tuser;
v_tlast <= h_tlast;
end
assign o_pixel = v_sum[11:4]; // 右移 4 位
assign o_valid = v_valid;
assign o_tuser = v_tuser;
assign o_tlast = v_tlast;
endmodule
2.1.5 DSP48 精确除法实现
// 精确除以 17:乘以 15 再右移 8 位
// 1/17 ≈ 15/256 = 0.05859,实际 1/17 = 0.05882,误差 0.4%
// 对于 8bit 图像,最大误差 < 1 个灰度级,可接受
// 使用 DSP48 实现乘法
// DSP48 配置:A = h_sum(12bit),B = 15(4bit),P = A×B(16bit)
// 输出:P[15:8](右移 8 位)
// Verilog 推断 DSP48:
wire [15:0] div_result = h_sum * 15;
wire [7:0] h_out_precise = div_result[15:8];
// 综合工具会自动将此乘法映射到 DSP48
// 每个方向(水平/垂直)各需要 1 个 DSP48
// 总计:2 个 DSP48(相比朴素实现的 25 个,节省 92%)
2.1.6 资源对比总结
5×5 Gaussian 三种实现方案对比:
方案 LUT FF BRAM36 DSP48 最高频率
─────────────────────────────────────────────────
朴素实现 800 1000 4 25 150MHz
可分离(移位)400 600 4 0 200MHz
可分离(DSP) 200 400 4 2 250MHz
推荐方案:可分离 + DSP48
- 资源最省
- 时序最好
- 精度最高
2.2 中值滤波
2.2.1 为什么中值滤波难以实现
中值滤波是非线性算法,无法用卷积表示,也无法用可分离滤波器优化。3×3 中值滤波需要对 9 个像素排序,找出中间值。
朴素排序方法:
冒泡排序:需要 36 次比较,无法流水线化
快速排序:递归结构,无法直接映射到 RTL
FPGA 解决方案:排序网络(Sorting Network)
排序网络是一种固定拓扑的比较器网络
可以完全流水线化,每个时钟周期输出一个结果
2.2.2 3×3 中值滤波排序网络
9 个输入的最优排序网络需要 25 个比较器,6 级流水线。
但我们只需要中间值(第 5 大),可以简化。
简化策略(只求中值):
Step 1:将 9 个像素分成 3 组,每组 3 个,分别求最小值、中值、最大值
Step 2:取 3 组最大值中的最小值(min_of_max)
Step 3:取 3 组最小值中的最大值(max_of_min)
Step 4:取 3 组中值中的中值(med_of_med)
Step 5:中值 = median(min_of_max, max_of_min, med_of_med)
总比较器数量:9 + 3 + 3 + 3 = 18 个(比完整排序网络少 28%)
2.2.3 比较器单元与流水线实现
// 基本比较器单元:输出较小值和较大值
module compare_swap (
input wire [7:0] a, b,
output wire [7:0] lo, hi
);
assign lo = (a < b) ? a : b;
assign hi = (a < b) ? b : a;
endmodule
// 3 个数的排序(输出 min, med, max)
module sort3 (
input wire [7:0] a, b, c,
output reg [7:0] s_min, s_med, s_max
);
wire [7:0] lo_ab, hi_ab, lo_bc, hi_bc;
compare_swap cs0(.a(a), .b(b), .lo(lo_ab), .hi(hi_ab));
compare_swap cs1(.a(b), .b(c), .lo(lo_bc), .hi(hi_bc));
always @(*) begin
// 3 个数排序只需 3 个比较器
if (a <= b && b <= c) begin s_min=a; s_med=b; s_max=c; end
else if (a <= c && c <= b) begin s_min=a; s_med=c; s_max=b; end
else if (b <= a && a <= c) begin s_min=b; s_med=a; s_max=c; end
else if (b <= c && c <= a) begin s_min=b; s_med=c; s_max=a; end
else if (c <= a && a <= b) begin s_min=c; s_med=a; s_max=b; end
else begin s_min=c; s_med=b; s_max=a; end
end
endmodule
// 3×3 中值滤波核心
module median_3x3 (
input wire clk,
input wire i_valid,
input wire [7:0] p00,p01,p02,
p10,p11,p12,
p20,p21,p22,
output reg o_valid,
output reg [7:0] o_median
);
// Stage 1:每行排序
wire [7:0] r0_min, r0_med, r0_max;
wire [7:0] r1_min, r1_med, r1_max;
wire [7:0] r2_min, r2_med, r2_max;
sort3 s0(.a(p00),.b(p01),.c(p02),.s_min(r0_min),.s_med(r0_med),.s_max(r0_max));
sort3 s1(.a(p10),.b(p11),.c(p12),.s_min(r1_min),.s_med(r1_med),.s_max(r1_max));
sort3 s2(.a(p20),.b(p21),.c(p22),.s_min(r2_min),.s_med(r2_med),.s_max(r2_max));
// Stage 2:列方向求中值
reg [7:0] max_of_min, med_of_med, min_of_max;
reg valid_s1;
always @(posedge clk) begin
// 3 个最小值中取最大值
max_of_min <= (r0_min > r1_min) ?
((r0_min > r2_min) ? r0_min : r2_min) :
((r1_min > r2_min) ? r1_min : r2_min);
// 3 个中值中取中值
med_of_med <= (r0_med >= r1_med && r0_med <= r2_med) ? r0_med :
(r0_med >= r2_med && r0_med <= r1_med) ? r0_med :
(r1_med >= r0_med && r1_med <= r2_med) ? r1_med :
(r1_med >= r2_med && r1_med <= r0_med) ? r1_med : r2_med;
// 3 个最大值中取最小值
min_of_max <= (r0_max < r1_max) ?
((r0_max < r2_max) ? r0_max : r2_max) :
((r1_max < r2_max) ? r1_max : r2_max);
valid_s1 <= i_valid;
end
// Stage 3:最终中值
always @(posedge clk) begin
o_median <= (max_of_min >= med_of_med && max_of_min <= min_of_max) ? max_of_min :
(max_of_min >= min_of_max && max_of_min <= med_of_med) ? max_of_min :
(med_of_med >= max_of_min && med_of_med <= min_of_max) ? med_of_med :
(med_of_med >= min_of_max && med_of_med <= max_of_min) ? med_of_med :
min_of_max;
o_valid <= valid_s1;
end
endmodule
2.2.4 中值滤波资源估算
3×3 中值滤波资源估算(Xilinx 7 系列):
比较器网络:
每个 8bit 比较器约需 8 个 LUT
18 个比较器 = 144 个 LUT
流水线寄存器(3 级):
约 200 个 FF
Line Buffer(2 条):
2 个 BRAM36
合计:
LUT:约 200~300
FF:约 300
BRAM36:2
DSP:0
与 Sobel 资源相当,但延迟为 3 个时钟周期(比 Sobel 少 1 级)
2.3 Canny 边缘检测
2.3.1 算法流程与 FPGA 挑战
Canny 是最经典的边缘检测算法,由 5 个步骤组成:
Step 1:Gaussian 模糊(去噪)
输入:原始灰度图
输出:平滑图像
FPGA 实现:5×5 可分离 Gaussian(已在 2.1 节实现)
Step 2:Sobel 梯度计算
输入:平滑图像
输出:梯度幅值 G 和梯度方向 θ
FPGA 实现:第一章 Sobel 核(扩展输出方向信息)
Step 3:非极大值抑制(NMS)
输入:G 和 θ
输出:细化边缘(单像素宽)
FPGA 挑战:需要比较梯度方向上的相邻像素
Step 4:双阈值检测
输入:NMS 输出
输出:强边缘和弱边缘
FPGA 实现:简单比较器,容易实现
Step 5:边缘连接(Hysteresis)
输入:强边缘和弱边缘
输出:最终边缘图
FPGA 挑战:需要连通性分析,通常需要帧缓存
2.3.2 梯度方向量化
Sobel 输出的梯度方向 θ = atan2(Gy, Gx),范围 [-π, π]。
NMS 需要比较梯度方向上的两个相邻像素。
FPGA 实现策略:将方向量化为 4 个方向(0°, 45°, 90°, 135°)
量化规则:
θ ∈ [-22.5°, 22.5°) 或 [157.5°, 180°] -> 方向 0(水平)
θ ∈ [22.5°, 67.5°) -> 方向 1(45°)
θ ∈ [67.5°, 112.5°) -> 方向 2(垂直)
θ ∈ [112.5°, 157.5°) -> 方向 3(135°)
避免 atan2 的方法(FPGA 友好):
直接用 Gx 和 Gy 的符号和比值判断方向
不需要三角函数,只需比较器和除法器
// 梯度方向量化(无需 atan2)
module grad_direction (
input wire signed [10:0] gx, gy,
output reg [1:0] direction
// 0: 水平(0°), 1: 对角(45°), 2: 垂直(90°), 3: 反对角(135°)
);
wire signed [10:0] abs_gx = gx[10] ? -gx : gx;
wire signed [10:0] abs_gy = gy[10] ? -gy : gy;
// 判断条件:
// 水平:|Gy|/|Gx| < tan(22.5°) ≈ 0.414
// 等价于:|Gy| < 0.414 × |Gx|
// 整数近似:2×|Gy| < |Gx|(tan(26.6°),近似)
// 垂直:|Gx|/|Gy| < 0.414,等价于 2×|Gx| < |Gy|
// 其余:对角方向
always @(*) begin
if (abs_gy < (abs_gx >>> 1)) // |Gy| < |Gx|/2
direction = 2'd0; // 水平
else if (abs_gx < (abs_gy >>> 1)) // |Gx| < |Gy|/2
direction = 2'd2; // 垂直
else if (gx[10] ^ gy[10]) // Gx 和 Gy 异号
direction = 2'd3; // 135°
else
direction = 2'd1; // 45°
end
endmodule
2.3.3 非极大值抑制(NMS)RTL 实现
// NMS:在梯度方向上比较当前像素与两个相邻像素
// 如果当前像素不是局部最大值,则抑制(输出 0)
module nms_3x3 (
input wire clk,
input wire i_valid,
// 3×3 梯度幅值窗口
input wire [7:0] g00,g01,g02,
g10,g11,g12,
g20,g21,g22,
// 中心像素的梯度方向
input wire [1:0] dir,
output reg o_valid,
output reg [7:0] o_pixel
);
always @(posedge clk) begin
o_valid <= i_valid;
case (dir)
2'd0: // 水平方向:比较左右像素 g10 和 g12
o_pixel <= (g11 >= g10 && g11 >= g12) ? g11 : 8'd0;
2'd1: // 45° 方向:比较 g00 和 g22
o_pixel <= (g11 >= g00 && g11 >= g22) ? g11 : 8'd0;
2'd2: // 垂直方向:比较上下像素 g01 和 g21
o_pixel <= (g11 >= g01 && g11 >= g21) ? g11 : 8'd0;
2'd3: // 135° 方向:比较 g02 和 g20
o_pixel <= (g11 >= g02 && g11 >= g20) ? g11 : 8'd0;
endcase
end
endmodule
2.3.4 双阈值检测
// 双阈值检测:将像素分为强边缘、弱边缘、非边缘
// 强边缘:pixel >= high_thresh
// 弱边缘:low_thresh <= pixel < high_thresh
// 非边缘:pixel < low_thresh
module double_threshold (
input wire clk,
input wire i_valid,
input wire [7:0] i_pixel,
input wire [7:0] low_thresh, // 典型值:50
input wire [7:0] high_thresh, // 典型值:150
output reg o_valid,
output reg [1:0] o_edge
// 2'b10: 强边缘, 2'b01: 弱边缘, 2'b00: 非边缘
);
always @(posedge clk) begin
o_valid <= i_valid;
if (i_pixel >= high_thresh)
o_edge <= 2'b10;
else if (i_pixel >= low_thresh)
o_edge <= 2'b01;
else
o_edge <= 2'b00;
end
endmodule
2.3.5 Canny 完整流水线架构
完整 Canny 流水线(模块串联):
输入图像(AXI4-Stream)
↓
[Gaussian 5×5] 延迟:2 级流水 + 4 行 Line Buffer
↓
[Sobel 核] 延迟:4 级流水 + 2 行 Line Buffer
↓
[方向量化] 延迟:1 级流水(组合逻辑)
↓
[NMS 3×3] 延迟:1 级流水 + 2 行 Line Buffer
↓
[双阈值] 延迟:1 级流水
↓
输出边缘图(AXI4-Stream)
总流水线延迟:约 9 个时钟周期 + 8 行 Line Buffer
总 BRAM36 消耗:8 个(Gaussian 4 条 + Sobel 2 条 + NMS 2 条)
总 LUT 消耗:约 800~1200 个
总 DSP48 消耗:2 个(Gaussian 除法)
注意:Hysteresis(边缘连接)步骤需要帧缓存,
通常在 PS 端(ARM)或独立帧处理模块中完成,
不在流式流水线中实现。
2.4 直方图统计
2.4.1 算法描述与 FPGA 挑战
直方图统计计算图像中每个灰度级的像素数量:
hist[v] = Σ (I(y,x) == v),v ∈ [0, 255]
输出:256 个计数值,每个计数值最大为 1920×1080 = 2,073,600
需要位宽:ceil(log2(2073600)) = 21bit
FPGA 实现的核心挑战是读-改-写(Read-Modify-Write)冲突:
每个像素到达时,需要:
1. 读取 hist[pixel_value](当前计数)
2. 加 1
3. 写回 hist[pixel_value]
问题:如果连续两个像素的灰度值相同(例如连续两个像素都是 128),
时钟 0:读取 hist[128] = N,准备写入 N+1
时钟 1:读取 hist[128] = N(写入还未完成!),准备写入 N+1
时钟 2:写入 N+1(时钟 0 的结果)
时钟 3:写入 N+1(时钟 1 的结果,覆盖了时钟 0 的结果!)
最终 hist[128] = N+1,而不是 N+2,计数错误!
2.4.2 解决方案一:流水线旁路(Bypass)
// 检测连续相同地址,直接旁路 BRAM 读取
module histogram (
input wire clk,
input wire rst_n,
input wire i_valid,
input wire [7:0] i_pixel,
input wire i_frame_start, // 帧开始,清零直方图
// 读取接口(帧结束后读取结果)
input wire [7:0] rd_addr,
output wire [20:0] rd_data
);
// 直方图存储(BRAM,256 × 21bit)
(* ram_style = "block" *) reg [20:0] hist_mem [0:255];
// 流水线寄存器
reg [7:0] addr_d1, addr_d2;
reg [20:0] data_d1, data_d2;
reg valid_d1, valid_d2;
// 旁路逻辑
wire bypass_d1 = valid_d1 && (addr_d1 == i_pixel);
wire bypass_d2 = valid_d2 && (addr_d2 == i_pixel);
// BRAM 读取(1 个周期延迟)
reg [20:0] bram_dout;
always @(posedge clk)
bram_dout <= hist_mem[i_pixel];
// 选择正确的读取值(旁路或 BRAM)
wire [20:0] current_val =
bypass_d1 ? data_d1 : // 最近 1 个周期有相同地址
bypass_d2 ? data_d2 : // 最近 2 个周期有相同地址
bram_dout; // 无冲突,使用 BRAM 读取值
// 写回(加 1)
always @(posedge clk)