一个完善稳定的仿真测试系统对于图像处理算法的设计至关重要。这个测试系统至少要完成以下功能:
- 模拟可配置的视频流(单帧的视频视为一幅图像);
- 模拟视频捕获,生成视频数据;
- 对FPGA的处理结果进行验证。
本文以960*480分辨率的图像为例,通过RGB转YCbCr的算法得到与彩色图像的Y分量灰度图像。仿真代码如下:
`timescale 1ns / 1ps
module picture_display_tb;
integer iBmpFileId; //输入BMP图片
integer oBmpFileId; //输出BMP图片
integer iIndex = 0; //输出BMP数据索引
integer pixel_index = 0; //输出像素数据索引
integer iCode;
integer iBmpSize; //输入BMP 字节数
reg [ 7:0] rBmpData [2000000:0]; //用于寄存输入BMP图片中的字节数据(包括54字节的文件头)
reg [ 7:0] Vip_BmpData [2000000:0]; //用于寄存视频图像处理之后 的BMP图片 数据
reg clk;
reg rst_n;
localparam pic_height = 480;
localparam pic_width = 960;
reg [ 7:0] vip_pixel_data [pic_height*pic_width*3-1:0]; //960x480x3
initial begin
//分别打开 输入/输出BMP图片,以及输出的Txt文本
iBmpFileId = $fopen("picture_960_480.bmp","rb"); //此处返回的并不是图像数组,而是一个文件描述符,专门指向此次打开文件的操作
// iBmpFileId = $fopen("E:\\project\\modelsim\\bmp_sim_test\\PIC\\fengjing_320x240.bmp","rb");
oBmpFileId = $fopen("output_file_1.bmp","wb+");
//将输入BMP图片加载到数组中
iCode = $fread(rBmpData,iBmpFileId);
//根据BMP图片文件头的格式,分别计算出图片的 宽度 /高度 /像素数据偏移量 /图片字节数
iBmpSize = {rBmpData[ 5],rBmpData[ 4],rBmpData[ 3],rBmpData[ 2]}; //指的是图文件的大小,而不是专指图片的字节数
//关闭输入BMP图片
$fclose(iBmpFileId);
//延迟5ms,等待第一帧VIP处理结束
#5000000
//加载图像处理后,BMP图片的文件头和像素数据
for (iIndex = 0; iIndex < iBmpSize; iIndex = iIndex + 1) begin
if(iIndex < 54)
Vip_BmpData[iIndex] = rBmpData[iIndex]; //处理过后的图片文件头信息与原图像保持一致
else
Vip_BmpData[iIndex] = vip_pixel_data[iIndex-54]; //图像数据就需要重新赋值,vip_pixel_data就是存储的像素数据
end
for(iIndex = 0; iIndex < iBmpSize;iIndex= iIndex+1 )begin
$fwrite(oBmpFileId,"%c",Vip_BmpData[iIndex]);
end
//关闭输出BMP图片
$fclose(oBmpFileId);
end
//初始化时钟和复位信号
initial begin
clk = 1;
rst_n = 0;
#110
rst_n = 1;
end
//产生50MHz时钟(周期是20ns)
always #5 clk = ~clk;
产生摄像头时序
wire cmos_vsync ; //场同步信号
reg cmos_href;
wire cmos_clken; //
reg [23:0] cmos_data; //摄像头数据
reg [31:0] cmos_index;
parameter [10:0] IMG_HDISP = 11'd960;
parameter [10:0] IMG_VDISP = 11'd480;
localparam H_SYNC = 11'd5;
localparam H_BACK = 11'd5;
localparam H_DISP = IMG_HDISP;
localparam H_FRONT = 11'd5;
localparam H_TOTAL = H_SYNC + H_BACK + H_DISP + H_FRONT; //5+5+320+5=335个时钟周期
localparam V_SYNC = 11'd1;
localparam V_BACK = 11'd0;
localparam V_DISP = IMG_VDISP;
localparam V_FRONT = 11'd1;
localparam V_TOTAL = V_SYNC + V_BACK + V_DISP + V_FRONT; //1+0+240+1=242个行同步信号
//---------------------------------------------
//水平计数器
reg [10:0] hcnt;
always@(posedge clk or negedge rst_n) begin
if(!rst_n)
hcnt <= 11'd0;
else
hcnt <= (hcnt < H_TOTAL - 1'b1) ? hcnt + 1'b1 : 11'd0;
end
//---------------------------------------------
//竖直计数器,竖直计数器+1的判决条件就是一行的结束,计数的是行同步信号
reg [10:0] vcnt;
always@(posedge clk or negedge rst_n) begin
if(!rst_n)
vcnt <= 11'd0;
else begin
if(hcnt == H_TOTAL - 1'b1)
vcnt <= (vcnt < V_TOTAL - 1'b1) ? vcnt + 1'b1 : 11'd0;
else
vcnt <= vcnt;
end
end
//---------------------------------------------
//场同步,场同步信号就是一帧的开始,当竖直计数器达到规定值后,就让场同步信号拉低
reg cmos_vsync_r;
always@(posedge clk or negedge rst_n) begin
if(!rst_n)
cmos_vsync_r <= 1'b0; //H: Vaild, L: inVaild
else begin
if(vcnt <= V_SYNC - 1'b1) //V_SYNC为11'd1
cmos_vsync_r <= 1'b0; //H: Vaild, L: inVaild
else
cmos_vsync_r <= 1'b1; //H: Vaild, L: inVaild
end
end
assign cmos_vsync = cmos_vsync_r;
//---------------------------------------------
//Image data href vaild signal,当frame_valid_ahead信号拉高时,说明此时的数据为有效的像素数据
wire frame_valid_ahead = ( vcnt >= V_SYNC + V_BACK && vcnt < V_SYNC + V_BACK + V_DISP
&& hcnt >= H_SYNC + H_BACK && hcnt < H_SYNC + H_BACK + H_DISP )
? 1'b1 : 1'b0;
reg cmos_href_r;
always@(posedge clk or negedge rst_n) begin
if(!rst_n)
cmos_href_r <= 0;
else begin
if(frame_valid_ahead)
cmos_href_r <= 1;
else
cmos_href_r <= 0;
end
end
always@(posedge clk or negedge rst_n) begin
if(!rst_n)
cmos_href <= 0;
else
cmos_href <= cmos_href_r;
end
assign cmos_clken = cmos_href;
//-------------------------------------
//从数组中以视频格式输出像素数据
wire [10:0] x_pos;
wire [10:0] y_pos;
assign x_pos = frame_valid_ahead ? (hcnt - (H_SYNC + H_BACK )) : 0;
assign y_pos = frame_valid_ahead ? (vcnt - (V_SYNC + V_BACK )) : 0;
always@(posedge clk or negedge rst_n)begin
if(!rst_n) begin
cmos_index <= 0;
cmos_data <= 24'd0;
end
else begin
cmos_index <= (y_pos * pic_width + x_pos)*3 + 54; // 3*(y*320 + x) + 54
cmos_data <= {rBmpData[cmos_index], rBmpData[cmos_index+1] , rBmpData[cmos_index+2]}; //从rBmpData中读取数据时,索引低的是B,高的是R
end
end
//-------------------------------------------------------------------------------------------------------------------
//开始算法部分的仿真
wire per_frame_vsync = cmos_vsync ;
wire per_frame_href = cmos_href;
wire per_frame_clken = cmos_clken;
wire [7:0] per_img_red = cmos_data[7:0];
wire [7:0] per_img_green = cmos_data[15: 8];
wire [7:0] per_img_blue = cmos_data[23:16];
//算法1:RGB转YCbCr,得到Cb和Cr分量,消耗3个CLK
wire post1_frame_vsync;
wire post1_frame_href ;
wire post1_frame_clken;
wire [7:0] post1_img_Y ;
wire [7:0] post1_img_Cb ;
wire [7:0] post1_img_Cr ;
VIP_RGB888_YCbCr VIP_RGB888_YCbCr_inst(
.clk (clk) ,
.rst_n(rst_n) ,
//Image data prepred to be processd
.per_frame_vsync (per_frame_vsync) , //Prepared Image data vsync valid signal
.per_frame_href (per_frame_href) , //Prepared Image data href vaild signal
.per_frame_clken (per_frame_clken) , //Prepared Image data output/capture enable clock
.per_img_red (per_img_red), //Prepared Image red data to be processed
.per_img_green(per_img_green), //Prepared Image green data to be processed
.per_img_blue (per_img_blue), //Prepared Image blue data to be processed
//Image data has been processd
.post_frame_vsync (post1_frame_vsync) , //Processed Image data vsync valid signal
.post_frame_href (post1_frame_href) , //Processed Image data href vaild signal
.post_frame_clken (post1_frame_clken) , //Processed Image data output/capture enable clock
.post_img_Y (post1_img_Y), //Processed Image brightness output
.post_img_Cb(post1_img_Cb), //Processed Image blue shading output
.post_img_Cr(post1_img_Cr) //Processed Image red shading output
);
//输出转灰度之后的结果
wire vip_out_frame_vsync;
wire vip_out_frame_href ;
wire vip_out_frame_clken;
wire [7:0] vip_out_img_R ;
wire [7:0] vip_out_img_G ;
wire [7:0] vip_out_img_B ;
assign vip_out_frame_vsync = post1_frame_vsync;
assign vip_out_frame_href = post1_frame_href ;
assign vip_out_frame_clken = post1_frame_clken;
assign vip_out_img_R = post1_img_Y;
assign vip_out_img_G = post1_img_Y;
assign vip_out_img_B = post1_img_Y;
reg [31:0] vip_cnt;
reg vip_vsync_r; //寄存VIP输出的场同步
reg vip_out_en; //寄存VIP处理图像的使能信号,仅维持一帧的时间
always@(posedge clk or negedge rst_n)begin
if(!rst_n)
vip_vsync_r <= 1'b0;
else
vip_vsync_r <= post1_frame_vsync;
end
reg [7:0] vip_vsync_cnt;
always@(posedge clk or negedge rst_n) begin
if(!rst_n)
vip_vsync_cnt <= 8'd0;
else if(post1_frame_vsync & (!vip_vsync_r))
vip_vsync_cnt <= vip_vsync_cnt + 8'd1;
else
vip_vsync_cnt <= vip_vsync_cnt;
end
//选择第几帧图像数据输出
always@(posedge clk or negedge rst_n)begin
if(!rst_n)
vip_out_en <= 1'b0;
else if(vip_vsync_cnt == 8'd1) //只将第1帧数据写入文件
vip_out_en <= 1'b1;
else if(vip_vsync_cnt == 8'd2)
vip_out_en <= 1'b0;
end
always@(posedge clk or negedge rst_n)begin
if(!rst_n) begin
vip_cnt <= 32'd0;
end
else if(vip_out_en) begin
if(vip_out_frame_href & vip_out_frame_clken) begin
vip_cnt <= vip_cnt + 3;
vip_pixel_data[vip_cnt+0] <= vip_out_img_B;
vip_pixel_data[vip_cnt+1] <= vip_out_img_G;
vip_pixel_data[vip_cnt+2] <= vip_out_img_R;
end
end
end
endmodule
仿真结果如图所示:
原图:
灰度图:
本人在编写代码时,对如下几点进行了考量:
- 图像的大小(分辨率)不同:当输入的图像大小与代码设定的图像大小不一致时,会报错,解决办法只需改动代码中两处声明的图像的高度与宽度值;
- 图像的处理流程不同:以RGB转灰度算法和直方图均衡化算法为例,前者当像素数据“流过”时,能够直接对像素点的8bitR\G\B数据进行计算,得到灰度数据,只需要在规定时间有一帧完整的图像输入后,就可输出,而后者需要统计图像的灰度数据,因此只有当一帧图像数据完整“流过”时,统计完成后才能进行下一步图像处理,故输出的是图像处理过后的第一帧数据,而不是输入的第一帧图像,可能我的解释比较迷,但是加粗字体是关键,理解了,就可以在代码中initial块语句中改变延时时间,在输出完整的处理过后的图像前提下缩短仿真时间,提升效率。
参考文献:牟新刚 周晓 郑晓亮《基于FPGA的数字图像处理原理及应用》