天天看点

FPGA图像处理13_常用算法_图像放大

图像放大

此处只说明图像放大,不涉及图像缩小,因为大多数图像处理场景下图像缩小的需求可以直接使用像素点抽取的方式实现,而且在缩小之后还涉及到与需求关联紧密的图像拼接,很难有通用化的设计。

算法原理

坐标转换

图像放大有以下关键设计:

  1. 坐标转换:输出图像像素点坐标转化为输入图像内的坐标,并找到包围该坐标的像素点用于插值计算。
  2. 数据缓冲:输入图像数据截取与缓冲,将用于插值计算的像素点值缓冲,并且在缓冲(FIFO)将满时向数据流上游反压。
  3. 插值计算:常用的图像放大插值计算有双线性插值(BiLinear)和双立方插值(BiCubic),前者计算简单,但是会造成图像模糊(相当于低通滤波);后者计算复杂,但是比较好的保留了图像边缘。
  4. 参数传递:由于算法实现涉及到多级数据缓冲及非均匀的流水线处理,而且参数在各个缓冲阶段都需要使用,因此在各级缓冲及流水线处理时都需要使用该处理阶段专用的参数,并且在输入1帧图像数据前更新。如果所有缓冲和计算流水线都共用一组参数的话,则会造成上级缓冲参数更新完成时,下级缓冲还在处理前一帧图像的数据。

以下设计场景是将输入图像内指定范围的图像子块放大至与输入图像相同的分辩率。

图像放大的计算参数包括:

  • 图像子块的左上角顶点在输入图像内的坐标:(x0, y0) 浮点值;
  • 图像子块内像素点的间距:z 浮点值,以输入图像内相邻像素点间距为 1,表示图像子块内像素点的间距,由于是放大计算,z<1,且图像放大倍数为 1 z 2 \frac{1}{z^2} z21​;
  • 用于插值计算的输入图像的像素点坐标范围:xbegin、xend、ybegin、yend,全部为整数值,用于截取输入图像数据。

令输入图像分辩率为 W × H W\times H W×H,由 (x0, y0) 和 z 计算得到全部计算参数,不同的插值算法有不同的参数计算。

双线性插值,使用包围当前输出像素点(绿点)的 4 个输入像素点(红点)进行插值。

一般情况:

FPGA图像处理13_常用算法_图像放大

特殊情况,输出像素点与输入像素点的左上角点重合。

即输出像素点与某个输入像素点重合情况下的特殊处理,可以选择 4 个输入像素点中任一点作为重合点,但是由于图像坐标系由左上角开始,因此与左上角点重合比较便于计算。

FPGA图像处理13_常用算法_图像放大

x b e g i n ≤ x 0 x b e g i n = f l o o r ( x 0 ) x_{begin}\leq x_0\\ x_{begin}=floor(x_0) xbegin​≤x0​xbegin​=floor(x0​)

y b e g i n ≤ y 0 y b e g i n = f l o o r ( y 0 ) y_{begin}\leq y_{0}\\ y_{begin}=floor(y_0) ybegin​≤y0​ybegin​=floor(y0​)

输出图像的列坐标最大值为 x 0 + ( W − 1 ) × z x_0+(W-1)\times z x0​+(W−1)×z

输出图像的行坐标最大值为 y 0 + ( H − 1 ) × z y_0+(H-1)\times z y0​+(H−1)×z

x e n d > x 0 + ( W − 1 ) × z x e n d = f l o o r ( x 0 + ( W − 1 ) × z + 1 ) x_{end}>x_0+(W-1)\times z\\ x_{end}=floor(x_0+(W-1)\times z+1) xend​>x0​+(W−1)×zxend​=floor(x0​+(W−1)×z+1)

y e n d > y 0 + ( H − 1 ) × z y e n d = f l o o r ( y 0 + ( H − 1 ) × z + 1 ) y_{end}>y_0+(H-1)\times z\\ y_{end}=floor(y_0+(H-1)\times z+1) yend​>y0​+(H−1)×zyend​=floor(y0​+(H−1)×z+1)

设输出像素点在输出图像中的坐标值为 (X, Y),该点对应的输入图像的坐标为:

( x 0 + X × z , y 0 + Y × z ) (x_0+X\times z, y_0+Y\times z) (x0​+X×z,y0​+Y×z)

则计算该输出像素点的输入图像的 4 个插值点的坐标为:

FPGA图像处理13_常用算法_图像放大

x 00 = x 10 = f l o o r ( x 0 + X × z ) x 01 = x 11 = f l o o r ( x 0 + X × z + 1 ) y 00 = y 01 = f l o o r ( y 0 + Y × z ) y 10 = y 11 = f l o o r ( y 0 + Y × z + 1 ) x00=x10=floor(x_0+X\times z)\\ x01=x11=floor(x_0+X\times z+1)\\ y00=y01=floor(y_0+Y\times z)\\ y10=y11=floor(y_0+Y\times z+1) x00=x10=floor(x0​+X×z)x01=x11=floor(x0​+X×z+1)y00=y01=floor(y0​+Y×z)y10=y11=floor(y0​+Y×z+1)

双立方插值,使用包围当前输出像素点(绿点)的 16 个输入像素点(红点)进行插值。

一般情况:

FPGA图像处理13_常用算法_图像放大

特殊情况,输出像素点与输入像素点的第 2 行第 2 列的像素点重合。

即输出像素点与某个输入像素点重合情况下的特殊处理,可以选择个红框上 4 个输入像素点任一点作为重合点,但是由于图像坐标系由左上角开始,因此与红框的左上角点重合比较便于计算。

FPGA图像处理13_常用算法_图像放大

x b e g i n ≤ ( x 0 − 1 ) x b e g i n = f l o o r ( x 0 − 1 ) x_{begin}\leq (x_0-1)\\ x_{begin}=floor(x_0-1) xbegin​≤(x0​−1)xbegin​=floor(x0​−1)

y b e g i n ≤ ( y 0 − 1 ) y b e g i n = f l o o r ( y 0 − 1 ) y_{begin}\leq (y_{0}-1)\\ y_{begin}=floor(y_0-1) ybegin​≤(y0​−1)ybegin​=floor(y0​−1)

x e n d > x 0 + ( W − 1 ) × z + 1 x e n d = f l o o r ( x 0 + ( W − 1 ) × z + 1 + 1 ) x_{end}>x_0+(W-1)\times z+1\\ x_{end}=floor(x_0+(W-1)\times z+1+1) xend​>x0​+(W−1)×z+1xend​=floor(x0​+(W−1)×z+1+1)

y e n d > y 0 + ( H − 1 ) × z + 1 y e n d = f l o o r ( y 0 + ( H − 1 ) × z + 1 + 1 ) y_{end}>y_0+(H-1)\times z+1\\ y_{end}=floor(y_0+(H-1)\times z+1+1) yend​>y0​+(H−1)×z+1yend​=floor(y0​+(H−1)×z+1+1)

设输出像素点在输出图像中的坐标值为 (X, Y),该点对应的输入图像的坐标为:

( x 0 + X × z , y 0 + Y × z ) (x_0+X\times z, y_0+Y\times z) (x0​+X×z,y0​+Y×z)

则计算该输出像素点的输入图像的 16 个插值点的坐标为:

FPGA图像处理13_常用算法_图像放大

x 00 = x 10 = x 20 = x 30 = f l o o r ( x 0 + X × z − 1 ) x 01 = x 11 = x 21 = x 31 = f l o o r ( x 0 + X × z ) x 02 = x 12 = x 22 = x 32 = f l o o r ( x 0 + X × z + 1 ) x 03 = x 13 = x 23 = x 33 = f l o o r ( x 0 + X × z + 2 ) y 00 = y 01 = y 02 = y 03 = f l o o r ( y 0 + Y × z − 1 ) y 10 = y 11 = y 12 = y 13 = f l o o r ( y 0 + Y × z ) y 20 = y 21 = y 22 = y 23 = f l o o r ( y 0 + Y × z + 1 ) y 30 = y 31 = y 32 = y 33 = f l o o r ( y 0 + Y × z + 2 ) x00=x10=x20=x30=floor(x_0+X\times z-1)\\ x01=x11=x21=x31=floor(x_0+X\times z)\\ x02=x12=x22=x32=floor(x_0+X\times z+1)\\ x03=x13=x23=x33=floor(x_0+X\times z+2)\\ y00=y01=y02=y03=floor(y_0+Y\times z-1)\\ y10=y11=y12=y13=floor(y_0+Y\times z)\\ y20=y21=y22=y23=floor(y_0+Y\times z+1)\\ y30=y31=y32=y33=floor(y_0+Y\times z+2) x00=x10=x20=x30=floor(x0​+X×z−1)x01=x11=x21=x31=floor(x0​+X×z)x02=x12=x22=x32=floor(x0​+X×z+1)x03=x13=x23=x33=floor(x0​+X×z+2)y00=y01=y02=y03=floor(y0​+Y×z−1)y10=y11=y12=y13=floor(y0​+Y×z)y20=y21=y22=y23=floor(y0​+Y×z+1)y30=y31=y32=y33=floor(y0​+Y×z+2)

数据缓冲

在 FPGA 实现中,每个时钟周期计算产生 1 个输出像素点的情况下,多个输出像素点的计算可能使用同一组输入像素点进行插值,而每个时钟周期都有新的输入像素点进入,因此需要对输入数据进行缓冲。

而且在参数设置的放大倍数较大的情况下,用于放大的图像子块较小,只需要少量的输入数据进行插值计算,导致输入数据占用时长远小于输出数据占用时长,因此需要控制上游模块暂停数据输入,防止由于缓冲空间不足,导致用于插值计算的输入数据被新的输入数据冲走。

数据缓冲分为 3 个阶段:

阶段 1:将输入图像数据送入二维缓冲产生与插值计算相同格式的并行数据。双线性插值为 2 × 2 2\times 2 2×2,双立方插值为 4 × 4 4\times 4 4×4。

阶段 2:将二维缓冲输出的并行数据存入 FIFO,根据其行列坐标值选择插值范围内的并行数据写入 FIFO。

阶段 3:将 FIFO 内的数据按行读出,并依次循环交替写入 2 个 Block RAM,实现 ping/pong切换。使用 RAM 而不用 FIFO 的原因在于这 1 行输入像素点数据可能多次用于多行输出像素点的插值计算,需要反复多次读取;用 ping/pong 切换的原因在于可以实现 1 个 RAM 读出数据用于插值计算的同时,另 1 个 RAM 可以写入 FIFO 读出的下 1 行输入图像数据。

双立方插值计算

双立方插值,即 OpenCV 中 resize 函数的 INTER_CUBIC 插值算法。

插值系数 W 的计算公式:

W ( x ) = { ( a + 2 ) × ∣ x ∣ 3 − ( a + 3 ) × ∣ x ∣ 2 + 1 ∣ x ∣ ≤ 1 a × ∣ x ∣ 3 − 5 × a × ∣ x ∣ 2 + 8 × a × ∣ x ∣ − 4 × a 1 < ∣ x ∣ < 2 0 e l s e W(x)=\begin{cases} (a+2)\times \lvert x\rvert^3-(a+3)\times\lvert x\rvert^2+1&\lvert x\rvert\leq1\\ a\times \lvert x\rvert^3-5\times a\times\lvert x\rvert^2+8\times a\times\lvert x\rvert-4\times a&1<\lvert x\rvert <2\\ 0&else \end{cases} W(x)=⎩⎪⎨⎪⎧​(a+2)×∣x∣3−(a+3)×∣x∣2+1a×∣x∣3−5×a×∣x∣2+8×a×∣x∣−4×a0​∣x∣≤11<∣x∣<2else​

上式中 a 值取 -0.5。

设输出像素点的坐标为 (x, y),用于计算的插值像素点 (xi, yi) 取其邻近的 4 × 4 4\times 4 4×4 个输入像素点。

根据坐标转换部分的说明:

  • 16 点的 x 坐标取值 floor(x)-1、floor(x)、ceil(x)、ceil(x)+1
  • 16 点的 y 坐标取值 floor(y)-1、floor(y)、ceil(y)、ceil(y)+1

插值计算公式如下:

f ( x , y ) = ∑ i = 0 3 ∑ j = 0 3 f ( x i , y i ) × W ( x − x i ) × W ( y − y i ) f(x,y)=\sum^3_{i=0}\sum^3_{j=0}f(x_i,y_i)\times W(x-x_i)\times W(y-y_i) f(x,y)=i=0∑3​j=0∑3​f(xi​,yi​)×W(x−xi​)×W(y−yi​)

FPGA 实现

Verilog 设计

完整的设计分为 4 个步骤:

  1. 输入图像数据进入 5 × 5 5\times 5 5×5 的二维缓冲(借用常用的二维缓冲模块,不用专门开发 4 × 4 4\times 4 4×4 的二维缓冲),从中截取 4 × 4 4\times 4 4×4 的并行输出,注意以 pix22 作为当前输出像素点,输出坐标及 fv、lv 都与 pix22 对齐;
  2. 二维缓冲输出的 4 × 4 4\times 4 4×4 并行输出数据存入缓冲 FIFO;
  3. FIFO 数据读出并且依次循环写入 2 个 Block RAM;
  4. sysgen 插值计算模块根据其插值计算的输入像素点列坐标(作为读地址)从 Block RAM 中读出数据,并根据插值计算输入像素点的行坐标选择切换 ping/pong Block RAM。

二维缓冲例化代码,二维缓冲输出的并行数据、fv、lv、x和y相当于下个步骤的数据输入:

wire [7:0] buf_11;
wire [7:0] buf_12;
wire [7:0] buf_13;
wire [7:0] buf_14;

wire [7:0] buf_21;
wire [7:0] buf_22;
wire [7:0] buf_23;
wire [7:0] buf_24;

wire [7:0] buf_31;
wire [7:0] buf_32;
wire [7:0] buf_33;
wire [7:0] buf_34;

wire [7:0] buf_41;
wire [7:0] buf_42;
wire [7:0] buf_43;
wire [7:0] buf_44;

(*keep = "TRUE"*) wire buf_fv;
(*keep = "TRUE"*) wire buf_lv;

(*keep = "TRUE"*) wire [15:0] buf_x;
(*keep = "TRUE"*) wire [15:0] buf_y;

buf_5x5_zoom buf_5x5_u (
.clk(clk),              // input wire clk
.rst(rst),              // input wire [0 : 0] rst
.in_pix(in_pix),        // input wire [7 : 0] in_pix
.in_lv(in_lv),          // input wire [0 : 0] in_lv
.out_pix00(),  // output wire [7 : 0] out_pix00
.out_pix01(),  // output wire [7 : 0] out_pix01
.out_pix02(),  // output wire [7 : 0] out_pix02
.out_pix03(),  // output wire [7 : 0] out_pix03
.out_pix04(),  // output wire [7 : 0] out_pix04
.out_pix10(),  // output wire [7 : 0] out_pix10
.out_pix11(buf_11),  // output wire [7 : 0] out_pix11
.out_pix12(buf_12),  // output wire [7 : 0] out_pix12
.out_pix13(buf_13),  // output wire [7 : 0] out_pix13
.out_pix14(buf_14),  // output wire [7 : 0] out_pix14
.out_pix20(),  // output wire [7 : 0] out_pix20
.out_pix21(buf_21),  // output wire [7 : 0] out_pix21
.out_pix22(buf_22),  // output wire [7 : 0] out_pix22
.out_pix23(buf_23),  // output wire [7 : 0] out_pix23
.out_pix24(buf_24),  // output wire [7 : 0] out_pix24
.out_pix30(),  // output wire [7 : 0] out_pix30
.out_pix31(buf_31),  // output wire [7 : 0] out_pix31
.out_pix32(buf_32),  // output wire [7 : 0] out_pix32
.out_pix33(buf_33),  // output wire [7 : 0] out_pix33
.out_pix34(buf_34),  // output wire [7 : 0] out_pix34
.out_pix40(),  // output wire [7 : 0] out_pix40
.out_pix41(buf_41),  // output wire [7 : 0] out_pix41
.out_pix42(buf_42),  // output wire [7 : 0] out_pix42
.out_pix43(buf_43),  // output wire [7 : 0] out_pix43
.out_pix44(buf_44),  // output wire [7 : 0] out_pix44
.out_x(buf_x),      // output wire [15 : 0] out_col
.out_y(buf_y),      // output wire [15 : 0] out_row
.out_fv(buf_fv),        // output wire [0 : 0] out_fv
.out_lv(buf_lv)        // output wire [0 : 0] out_lv
);
           

二维缓冲输出的 fv 用于第 1 次参数传递,将端口参数送至 FIFO 写接口,参考更新办法见前述的参数更新流程:

//更新FIFO缓冲的参数
always @(posedge clk) begin
	if (rst == 1'b1) begin
		z <= 32'hFFFF_FFFF;
		x0 <= 32'hFFFF_FFFF;
		y0 <= 32'hFFFF_FFFF;
		x_begin <= 16'hFFFF;
		y_begin <= 16'hFFFF;
		x_end <= 16'hFFFF;
		y_end <= 16'hFFFF;
		col_len <= 16'hFFFF;
		row_len <= 16'hFFFF;
	end
	else begin
		case ({buf_fv_d1, buf_fv, param_valid})
			{1'b1, 1'b0, 1'b1}: begin
				//fv下降沿,且参数有效,则更新参数值
				z <= param_z;
				x0 <= param_x0;
				y0 <= param_y0;
				x_begin <= param_x_begin;
				y_begin <= param_y_begin;
				x_end <= param_x_end;
				y_end <= param_y_end;
				col_len <= param_col_len;
				row_len <= param_row_len;
			end

			default: begin
				//保持
				z <= z;
				x0 <= x0;
				y0 <= y0;
				x_begin <= x_begin;
				y_begin <= y_begin;
				x_end <= x_end;
				y_end <= y_end;
				col_len <= col_len;
				row_len <= row_len;
			end
		endcase
	end
end
           

用状态机控制截取输入图像数据写入 FIFO。

注意下方代码的 state_en 寄存器,用于控制算法使能,在算法非使能情况下直接输出二维缓冲的结果,数据不写入 FIFO。

always @(posedge clk) begin
	if (rst == 1'b1) begin
		state_fifo_wr <= FIFO_WR_WAIT;//复位后等待fv下降沿才开始工作
		fifo_wr_en <= 1'b0;
		fifo_din <= {128{1'b1}};
	end
	else begin
		case (state_fifo_wr)
			FIFO_WR_WAIT: begin
				//在fv下降沿检查算法使能状态,用于启动状态机
				case ({buf_fv_d1, buf_fv, state_en})
					{1'b1, 1'b0, ENABLED}: begin
						state_fifo_wr <= FIFO_WR_DATA;
					end

					default: begin
						state_fifo_wr <= state_fifo_wr;
					end
				endcase

				fifo_wr_en <= 1'b0;
				fifo_din <= {128{1'b1}};
			end

			FIFO_WR_DATA: begin
				if ((buf_x >= x_begin) && (buf_x <= x_end) && (buf_y >= y_begin) && (buf_y <= y_end)) begin
					//二维缓冲输出的并行数据坐标在截取范围内
					fifo_wr_en <= buf_fv & buf_lv;//二缓缓冲输出并行数据有效
				end
				else begin
					fifo_wr_en <= 1'b0;
				end

				//FIFO写入数据为并行数据拼接
				fifo_din <= {buf_11, buf_12, buf_13, buf_14,
				buf_21, buf_22, buf_23, buf_24,
				buf_31, buf_32, buf_33, buf_34,
				buf_41, buf_42, buf_43, buf_44};

				case ({buf_x, buf_y, buf_fv, buf_lv})
					{x_end, y_end, 1'b1, 1'b1}: begin
						//有效范围内的最后1个像素点从二维缓冲输出
						state_fifo_wr <= FIFO_WR_WAIT;
					end

					default: begin
						//状态保持
						state_fifo_wr <= state_fifo_wr;
					end
				endcase
			end

			default: begin
				state_fifo_wr <= FIFO_WR_WAIT;
				fifo_wr_en <= 1'b0;
				fifo_din <= {128{1'b1}};
			end
		endcase
	end
end
           

FIFO 读出状态机如下,在从 FIFO 读出 1 帧数据之前先寄存 FIFO 写接口的参数,完成第 2 次参数传递,再根据 Block RAM 的可写状态切换数据写入的 RAM,直到 1 帧图像在 FIFO 内缓冲的数据全部读出。

always @(posedge clk) begin
	if (rst == 1'b1) begin
		fifo_rd_en <= 1'b0;
		state_fifo_rd <= FIFO_RD_PARAM;
	end
	else begin
		case (state_fifo_rd)
			FIFO_RD_PARAM: begin
				//FIFO非空,表示新一帧图像数据已进入,更新参数
				if (fifo_empty == 1'b0) begin
					state_fifo_rd <= FIFO_RD_LINE_WAIT_PING;
				end
				else begin
					state_fifo_rd <= state_fifo_rd;
				end

				fifo_rd_en <= 1'b0;
			end

			FIFO_RD_LINE_WAIT_PING: begin
				//等待FIFO内装入1行数据量sg_col_len,且ping可写
				if ((fifo_data_count >= bram_col_len) && (bram_state_ping == 1'b0)) begin
					fifo_rd_en <= 1'b1;
					state_fifo_rd <= FIFO_RD_LINE_PING;
				end
				else begin
					fifo_rd_en <= 1'b0;
					state_fifo_rd <= state_fifo_rd;
				end
			end

			FIFO_RD_LINE_PING: begin
				//状态保持sg_col_len个时钟周期,从FIFO内读出1行数据写入ping
				//在当前状态下fifo_rd_en保持有效
				case (cnt_bram_col_len)
					bram_col_len: begin
						//完成sg_col_len个时钟周期计数,1行数据读出完成
						fifo_rd_en <= 1'b0;

						case (cnt_bram_row_len)
							bram_row_len: begin
								//完成sg_row_len行的数据读出,即1帧图像读出完成,状态机复位
								state_fifo_rd <= FIFO_RD_PARAM;
							end

							default: begin
								//未完成1帧图像读出,接下来读出下1行数据,切换至pong
								state_fifo_rd <= FIFO_RD_LINE_WAIT_PONG;
							end
						endcase
					end

					default: begin
						fifo_rd_en <= 1'b1;
						state_fifo_rd <= state_fifo_rd;
					end
				endcase
			end

			FIFO_RD_LINE_WAIT_PONG: begin
				//等待FIFO内装入1行数据量sg_col_len,且pong可写
				if ((fifo_data_count >= bram_col_len) && (bram_state_pong == 1'b0)) begin
					fifo_rd_en <= 1'b1;
					state_fifo_rd <= FIFO_RD_LINE_PONG;
				end
				else begin
					fifo_rd_en <= 1'b0;
					state_fifo_rd <= state_fifo_rd;
				end
			end

			FIFO_RD_LINE_PONG: begin
				//状态保持bram_col_len个时钟周期,从FIFO内读出1行数据写入pong
				//在当前状态下fifo_rd_en保持有效
				case (cnt_bram_col_len)
					bram_col_len: begin
						//完成sg_col_len个时钟周期计数,1行数据读出完成
						fifo_rd_en <= 1'b0;

						case (cnt_bram_row_len)
							bram_row_len: begin
								//完成bram_row_len行的数据读出,即1帧图像读出完成,状态机复位
								state_fifo_rd <= FIFO_RD_PARAM;
							end

							default: begin
								//未完成1帧图像读出,接下来读出下1行数据,切换至ping
								state_fifo_rd <= FIFO_RD_LINE_WAIT_PING;
							end
						endcase
					end

					default: begin
						fifo_rd_en <= 1'b1;
						state_fifo_rd <= state_fifo_rd;
					end
				endcase
			end

			default: begin
				fifo_rd_en <= 1'b0;
				state_fifo_rd <= FIFO_RD_PARAM;
			end
		endcase
	end
end

//FIFO读出1帧数据之前,更新bram写参数
always @(posedge clk) begin
	if (rst == 1'b1) begin
		{bram_z, bram_x0, bram_y0, bram_x_begin, bram_y_begin, bram_x_end, bram_y_end, bram_col_len, bram_row_len} <= {192{1'b1}};
	end
	else begin
		case (state_fifo_rd)
			FIFO_RD_PARAM: begin
				{bram_z, bram_x0, bram_y0, bram_x_begin, bram_y_begin, bram_x_end, bram_y_end, bram_col_len, bram_row_len} <= {z, x0, y0, x_begin, y_begin, x_end, y_end, col_len, row_len};
			end

			default: begin
				//保持
				{bram_z, bram_x0, bram_y0, bram_x_begin, bram_y_begin, bram_x_end, bram_y_end, bram_col_len, bram_row_len} <= {bram_z, bram_x0, bram_y0, bram_x_begin, bram_y_begin, bram_x_end, bram_y_end, bram_col_len, bram_row_len};
			end
		endcase
	end
end
           

Block RAM 定义为双口 RAM,a 口专用于写入,b 口专用于读出。

注意:RAM 的写读地址与输入像素点列坐标一致,方便 sysgen 根据列坐标读出用于插值的数据。

Block RAM 写入流程由 FIFO 读出状态机控制:

//例化FIFO读出后保存行数据的bram
//用ping/pong切换的方式,实现1个bram写入的过程中另1个bram可以读出
reg wea_ping = 1'b0;
reg wea_pong = 1'b0;

reg [127:0] dina_ping = {128{1'b1}};
reg [127:0] dina_pong = {128{1'b1}};

reg [9:0] addra_ping = 10'h3FF;
reg [9:0] addra_pong = 10'h3FF;

//port b保持读状态,用addrb更新读出数值
wire [9:0] addrb_ping;
wire [9:0] addrb_pong;

wire [127:0] doutb_ping;
wire [127:0] doutb_pong;

//port a专用于写入
//port b专用于读出
bram_zoom bram_ping
(
.clka(clk),    // input wire clka
.wea(wea_ping),      // input wire [0 : 0] wea
.addra(addra_ping),  // input wire [9 : 0] addra
.dina(dina_ping),    // input wire [127 : 0] dina
.douta(),  // output wire [127 : 0] douta
.clkb(clk),    // input wire clkb
.web(1'b0),      // input wire [0 : 0] web
.addrb(addrb_ping),  // input wire [9 : 0] addrb
.dinb(128'd0),    // input wire [127 : 0] dinb
.doutb(doutb_ping)  // output wire [127 : 0] doutb
);

bram_zoom bram_pong
(
.clka(clk),    // input wire clka
.wea(wea_pong),      // input wire [0 : 0] wea
.addra(addra_pong),  // input wire [9 : 0] addra
.dina(dina_pong),    // input wire [127 : 0] dina
.douta(),  // output wire [127 : 0] douta
.clkb(clk),    // input wire clkb
.web(1'b0),      // input wire [0 : 0] web
.addrb(addrb_pong),  // input wire [9 : 0] addrb
.dinb(128'd0),    // input wire [127 : 0] dinb
.doutb(doutb_pong)  // output wire [127 : 0] doutb
);

//bram写接口,写地址即输入像素点坐标
always @(posedge clk) begin
	case (state_fifo_rd)
		FIFO_RD_LINE_PING: begin
			//FIFO数据写入ping
			wea_ping <= 1'b1;
			dina_ping <= fifo_dout;
			addra_ping <= addra_ping+10'd1;
		end

		default: begin
			wea_ping <= 1'b0;
			dina_ping <= 128'd0;
			addra_ping <= bram_x_begin[9:0]-10'd1;//用于在FIFO_RD_LINE_PING首个时钟周期+1后得到bram_x_begin
		end
	endcase
end

always @(posedge clk) begin
	case (state_fifo_rd)
		FIFO_RD_LINE_PONG: begin
			//FIFO数据写入pong
			wea_pong <= 1'b1;
			dina_pong <= fifo_dout;
			addra_pong <= addra_pong+10'd1;
		end

		default: begin
			wea_pong <= 1'b0;
			dina_pong <= 128'd0;
			addra_pong <= bram_x_begin[9:0]-10'd1;//用于在FIFO_RD_LINE_PONG首个时钟周期+1后得到bram_x_begin
		end
	endcase
end
           

Block RAM 的读出状态机,用输入 sysgen 模块的 lv 信号控制 sysgen 的计算流程,并且进行第 3 次参数传递,将参数供 sysgen 计算使用。

注意下方代码中 backpressure_in 信号的处理,即收到下游模块送入的反压信号时,Block RAM 读出状态机将停止工作。

//bram数据读出控制

//来自于sysgen的输入图像坐标请求和对应的输出图像坐标
(*keep = "TRUE"*) wire coord_en;//坐标有效
(*keep = "TRUE"*) wire [15:0] coord_x;//输出图像列坐标
(*keep = "TRUE"*) wire [15:0] coord_y;//输出图像行坐标
(*keep = "TRUE"*) wire [15:0] coord_req_x;//请求输入图像的列坐标,用于bram读地址
(*keep = "TRUE"*) wire [15:0] coord_req_y;//请求输入图像的行坐标,切换bram ping/pong

//bram读地址直连sysgen的请求输入图像列坐标
assign addrb_ping = coord_req_x[9:0];
assign addrb_pong = coord_req_x[9:0];

//coord_en延迟1个时钟周期,用于同步bram读出数据,即比coord_req_x延迟1个时钟周期
reg coord_en_d1 = 1'b0;
always @(posedge clk) begin
	coord_en_d1 <= coord_en;
end

//coord_y延迟1个时钟周期,用于判断输出1帧结果
reg [15:0] coord_y_d1 = 16'hFFFF;
always @(posedge clk) begin
	coord_y_d1 <= coord_y;
end

//coord_req_y延迟1个时钟周期,用于查看请求输入图像的行坐标的变化,判断切换bram ping/pong
reg [15:0] coord_req_y_d1 = 16'hFFFF;
always @(posedge clk) begin
	coord_req_y_d1 <= coord_req_y;
end

//从bram写流程中取出的参数,用于sysgen计算,完成1帧插值计算,从ping中读出首行插值数据之前寄存
(*keep = "TRUE"*) reg [31:0] sg_z = 32'hFFFF_FFFF;
(*keep = "TRUE"*) reg [31:0] sg_x0 = 32'hFFFF_FFFF;
(*keep = "TRUE"*) reg [31:0] sg_y0 = 32'hFFFF_FFFF;
(*keep = "TRUE"*) reg [15:0] sg_y_begin = 16'hFFFF;
(*keep = "TRUE"*) reg [15:0] sg_y_end = 16'hFFFF;

//bram读状态机计数
reg [15:0] cnt_bram_rd = 16'd1;//计数范围

//bram读状态机,由bram内数据状态和sysgen坐标请求状态控制
localparam BRAM_RD_WAIT_PING = 6'b000001;
localparam BRAM_RD_PING = 6'b000010;
localparam BRAM_RD_HOLD_PING = 6'b000100;
localparam BRAM_RD_WAIT_PONG = 6'b001000;
localparam BRAM_RD_PONG = 6'b010000;
localparam BRAM_RD_HOLD_PONG = 6'b100000;
(*keep = "TRUE"*) reg [5:0] state_bram_rd = BRAM_RD_WAIT_PING;

always @(posedge clk) begin
	if (rst == 1'b1) begin
		state_bram_rd <= BRAM_RD_WAIT_PING;
	end
	else begin
		case (state_bram_rd)
			BRAM_RD_WAIT_PING: begin
				//等待ping可读,且无反压输入
				if ((bram_state_ping == 1'b1) && (backpressure_in == 1'b0)) begin
					state_bram_rd <= BRAM_RD_PING;
				end
				else begin
					state_bram_rd <= state_bram_rd;
				end
			end

			BRAM_RD_PING: begin
				//保持COLS个时钟周期,生成输入sysgen的lv,长度为COLS,启动sysgen计算
				case (cnt_bram_rd)
					COLS: begin
						state_bram_rd <= BRAM_RD_HOLD_PING;
					end

					default: begin
						state_bram_rd <= state_bram_rd;
					end
				endcase
			end

			BRAM_RD_HOLD_PING: begin
				//前1个状态生成的lv送出全部的的坐标请求
				case ({coord_en_d1, coord_en})
					{1'b1, 1'b0}: begin
						//完成1行坐标请求
						if ((coord_y == 16'd0) && (coord_y_d1 != 16'd0)) begin
							//完成一帧图像最后1行的请求
							//状态机复位
							state_bram_rd <= BRAM_RD_WAIT_PING;
						end
						/*
						由于浮点计算误差,可能导致一帧数据计算过程中,读取bram的次数多于row_width,从FIFO中多读出下帧的若干行
						因此必须通过ori_y有效值限制切换
						ori_y值小于row_begin,表示起点行误差,在ori_y>ori_y_d1情况下发现ori_y值小于等于row_begin,表示ori_y_d1小于row_begin,则不切换
						ori_y值大于row_end,表示终点行误差,在ori_y>ori_y_d1情况下发现ori_y值大于row_end,不切换
						*/
						else if ((coord_req_y > coord_req_y_d1) && (coord_req_y <= sg_y_end) && (coord_req_y > sg_y_begin)) begin
							//计算输入的原始图像坐标已切换至下一行
							state_bram_rd <= BRAM_RD_WAIT_PONG;
						end
						else begin
							//不切换bram,仍使用ping
							state_bram_rd <= BRAM_RD_WAIT_PING;
						end
					end
				endcase
			end

			BRAM_RD_WAIT_PONG: begin
				//等待pong可读,且无反压输入
				if ((bram_state_pong == 1'b1) && (backpressure_in == 1'b0)) begin
					state_bram_rd <= BRAM_RD_PONG;
				end
				else begin
					state_bram_rd <= state_bram_rd;
				end
			end

			BRAM_RD_PONG: begin
				//保持COLS个时钟周期,生成输入sysgen的lv,启动sysgen计算
				case (cnt_bram_rd)
					COLS: begin
						state_bram_rd <= BRAM_RD_HOLD_PONG;
					end

					default: begin
						state_bram_rd <= state_bram_rd;
					end
				endcase
			end

			BRAM_RD_HOLD_PONG: begin
				//等待sysgen完成1行数据请求
				case ({coord_en_d1, coord_en})
					{1'b1, 1'b0}: begin
						//完成1行坐标请求
						if ((coord_y == 16'd0) && (coord_y_d1 != 16'd0)) begin
							//完成一帧图像最后1行的请求
							//状态机复位
							state_bram_rd <= BRAM_RD_WAIT_PING;
						end
						/*
						由于浮点计算误差,可能导致一帧数据计算过程中,读取bram的次数多于row_width,从FIFO中多读出下帧的若干行
						因此必须通过ori_y有效值限制切换
						ori_y值小于row_begin,表示起点行误差,在ori_y>ori_y_d1情况下发现ori_y值小于等于row_begin,表示ori_y_d1小于row_begin,则不切换
						ori_y值大于row_end,表示终点行误差,在ori_y>ori_y_d1情况下发现ori_y值大于row_end,不切换
						*/
						else if ((coord_req_y > coord_req_y_d1) && (coord_req_y <= sg_y_end) && (coord_req_y > sg_y_begin)) begin
							//计算输入的原始图像坐标已切换至下一行
							state_bram_rd <= BRAM_RD_WAIT_PING;
						end
						else begin
							//不切换bram,仍使用pong
							state_bram_rd <= BRAM_RD_WAIT_PONG;
						end
					end
				endcase
			end

			default: begin
				state_bram_rd <= BRAM_RD_WAIT_PING;
			end
		endcase
	end
end

//更新sysgen参数
always @(posedge clk) begin
	if (rst == 1'b1) begin
		sg_z <= 32'hFFFF_FFFF;
		sg_x0 <= 32'hFFFF_FFFF;
		sg_y0 <= 32'hFFFF_FFFF;
		sg_y_begin <= 16'hFFFF;
		sg_y_end <= 16'hFFFF;
	end
	else begin
		case ({state_bram_rd, bram_state_ping, backpressure_in, coord_x, coord_y})
			{BRAM_RD_WAIT_PING, 1'b1, 1'b0, 16'd0, 16'd0}: begin
				//1帧数据首次进入BRAM_RD_PING状态时用bram参数更新sg参数
				sg_z <= bram_z;
				sg_x0 <= bram_x0;
				sg_y0 <= bram_y0;
				sg_y_begin <= bram_y_begin;
				sg_y_end <= bram_y_end;
			end

			default: begin
				//保持
				sg_z <= sg_z;
				sg_x0 <= sg_x0;
				sg_y0 <= sg_y0;
				sg_y_begin <= sg_y_begin;
				sg_y_end <= sg_y_end;
			end
		endcase
	end
end
           

Block RAM 的可写可读状态根据前述状态机的控制:

always @(posedge clk) begin
	if (rst == 1'b1) begin
		bram_state_ping <= 1'b0;//默认为可写状态
	end
	else begin
		case (bram_state_ping)
			1'b0: begin
				//见bram写接口控制
				case ({wea_ping, addra_ping})
					{1'b1, bram_x_end[9:0]}: begin
						//下个时钟周期完成1行内最后1个像素点写入
						bram_state_ping <= 1'b1;
					end

					default: begin
						//保持
						bram_state_ping <= bram_state_ping;
					end
				endcase
			end

			1'b1: begin
				//即state_bram_rd在BRAM_RD_HOLD_PING状态下的转移条件
				case (state_bram_rd)
					BRAM_RD_HOLD_PING: begin
						//前1个状态生成的lv送出全部的的坐标请求
						case ({coord_en_d1, coord_en})
							{1'b1, 1'b0}: begin
								//完成1行坐标请求
								if ((coord_y == 16'd0) && (coord_y_d1 != 16'd0)) begin
									//完成一帧图像最后1行的请求
									//状态机复位
									bram_state_ping <= 1'b0;
								end
								else if ((coord_req_y > coord_req_y_d1) && (coord_req_y <= sg_y_end) && (coord_req_y > sg_y_begin)) begin
									//计算输入的原始图像坐标已切换至下一行
									bram_state_ping <= 1'b0;
								end
								else begin
									//不切换bram,仍使用ping
									//保持
									bram_state_ping <= bram_state_ping;
								end
							end
						endcase
					end
					
					default: begin
						//保持
						bram_state_ping <= bram_state_ping;
					end
				endcase
			end
		endcase
	end
end

always @(posedge clk) begin
	if (rst == 1'b1) begin
		bram_state_pong <= 1'b0;//默认为可写状态
	end
	else begin
		case (bram_state_pong)
			1'b0: begin
				//见bram写接口控制
				case ({wea_pong, addra_pong})
					{1'b1, bram_x_end[9:0]}: begin
						//下个时钟周期完成1行内最后1个像素点写入
						bram_state_pong <= 1'b1;
					end

					default: begin
						//保持
						bram_state_pong <= bram_state_pong;
					end
				endcase
			end

			1'b1: begin
				//即state_bram_rd在BRAM_RD_HOLD_PONG状态下的转移条件
				case (state_bram_rd)
					BRAM_RD_HOLD_PONG: begin
						//前1个状态生成的lv送出全部的的坐标请求
						case ({coord_en_d1, coord_en})
							{1'b1, 1'b0}: begin
								//完成1行坐标请求
								if ((coord_y == 16'd0) && (coord_y_d1 != 16'd0)) begin
									//完成一帧图像最后1行的请求
									//状态机复位
									bram_state_pong <= 1'b0;
								end
								else if ((coord_req_y > coord_req_y_d1) && (coord_req_y <= sg_y_end) && (coord_req_y > sg_y_begin)) begin
									//计算输入的原始图像坐标已切换至下一行
									bram_state_pong <= 1'b0;
								end
								else begin
									//不切换bram,仍使用pong
									//保持
									bram_state_pong <= bram_state_pong;
								end
							end
						endcase
					end
					
					default: begin
						//保持
						bram_state_pong <= bram_state_pong;
					end
				endcase
			end
		endcase
	end
end
           

模块的反压控制除了向数据流上游输出反压以外,还负责将下游的反压信号向上游传递:

//在数据流上级模块中将根据反压停止数据送出状态机
parameter BACKPRESSURE_THRESHOLD = 12'd768;

reg backpressure_out = 1'b0;
always @(posedge clk) begin
	if (fifo_data_count >= BACKPRESSURE_THRESHOLD) begin
		backpressure_out <= 1'b1;
	end
	else begin
		backpressure_out <= backpressure_in;//向上游传递反压
	end
end
           

sysgen 设计

sysgen 与 Verilog 的配合方法在于 sysgen 向 Verilog 请求数据,Verilog 根据请求数据的坐标向 sysgen 送入用于插值的数据。

Verilog 在 Block RAM 读状态机中产生向 sysgen 输入的 in_lv,其持续的时间即输出图像的列数目,sysgen 计算产生其输出像素点的坐标 (coord_x, coord_y),以及该像素点插值计算的输入像素点的坐标 (coord_req_x, coord_req_y),计算方法与前述的坐标转换一致。

FPGA图像处理13_常用算法_图像放大
FPGA图像处理13_常用算法_图像放大

插值计算过程全部使用浮点数,在输出之前转化为无符号整数输出。

插值计算的实现过程与双立方插值计算一致。

先进行坐标计算:

FPGA图像处理13_常用算法_图像放大

再进行 16 点插值系数计算:

FPGA图像处理13_常用算法_图像放大
FPGA图像处理13_常用算法_图像放大

最后将 Block RAM 读入的并行数据与插值系数进行乘加完成插值计算。

实机验证

由于数据接口比较复杂,不方便在 sysgen 环境下仿真,于是实机试验。

试验图片来源于视频截图:https://www.youku.com/

输入原始图像:

FPGA图像处理13_常用算法_图像放大

参数 z 值为 0.5 的放大图像:

FPGA图像处理13_常用算法_图像放大

参数 z 值为 0.3 的放大图像:

FPGA图像处理13_常用算法_图像放大