第5章 FPGA图像前处理 5.1色彩滤波矩阵IP核的仿真 5.1.1色彩滤波矩阵介绍 1. 科普CFA CFA(Color Filter Array,色彩滤波阵列)也就是我们常说的CMOS色彩滤镜,应该说是一个比较重要、厂商在宣传的时候也会偶尔提及的东西。但是对于这个东西如何起作用,不同的排列又有什么优缺点,可能很多人就不太清楚了。拜耳阵列是当前应用最成熟最广泛的CFA,下面就以拜耳阵列的工作原理为例进行介绍。 对于彩色图像,需要采集多种最基本的颜色,如红(R)、绿(G)和蓝(B)三种颜色。最简单的方法就是采用滤镜的方法,红色的滤镜透过红色的波长,绿色的滤镜透过绿色的波长,蓝色的滤镜透过蓝色的波长。如果要采集R、G和B三个基本色,则需要三块滤镜,这样价格昂贵,且不好制造,因为三块滤镜都必须保证每一个像素点都对齐。柯达公司的科学家Bryce Bayer(拜耳)发明了以其名字命名的拜耳阵列被广泛运用于数字图像传感器中,拜耳阵列并不需要每个像素都放置三块滤镜,它有效地利用了人眼对绿色比较敏感、但对红色和蓝色相对不敏感的特性,在每个像素点只放置一个滤镜的情况下,让绿色的滤镜间隔出现,而红色和蓝色滤镜只需要在每2像素×2像素矩阵放置一块。虽然拜耳阵列采集到的每个像素都只有一个色彩的图像数据,但是通过后端的数字算法(即我们这个实例要实现的CFA算法)可以实现每个像素点的其他色彩的重建。 图5.1马赛克晶格(见彩插) 拜耳阵列俗称为“马赛克传感器”,如图5.1所示,这种排列方式看起来的确有点像花花绿绿的马赛克晶格。 很明显可以看到拜耳阵列是由一行RGRGRG……和一行BGBGBG……交错排列而成,每一个像素点只能读取单独的颜色信息。其中绿色像素的采样频率是输出像素的1/2,红、蓝色像素的采样频率是输出像素的1/4,故有拜耳阵列传感器的分辨率是由绿色像素决定的这一说法。 拜耳阵列传感器采样生成的图像要输出我们常见的全色彩图像必须经过反马赛克运算——但这跟我们平时俗称的“猜色”的字面意义不同,拜耳阵列的颜色并不是猜的,而是每个2×2方块经过9次矩阵运算计算出来的,也就是说不存在猜这回事,每个像素的颜色其实是一个确定值(矩阵运算是线性运算,这并不是一个混沌系统)。但没有争议的是拜耳阵列确实存在欠采样问题,这也使得它会出现摩尔纹和伪色(摩尔纹出现的原因就是输入信号的最高频率成分超过了传感器的奈奎斯特采样定理中的极限值,也就是说传感器的高频采样能力存在一些不足),100%查看时的画质也不是特别理想。 但是,看一个结构或者说架构是否强势,重点要看其成熟度与可扩增性,有时候一些“暴力美学”解决起问题来反而十分优雅。 拜耳阵列就是这么一个典型,简单的结构与成熟的工艺让它“堆”起像素来十分容易,只要光电管足够,3000万、4000万、5000万甚至上亿像素也没问题。虽然结构本身存在欠采样问题,但理论上来说,当拜耳阵列传感器的实际像素数超越无CFA或X3这种传感器的输出像素2.8倍的时候,在输出同样大小的图片时便可以获取超越全色传感器的分辨率和100%查看画质。 2. CFA插值运算 CFA虽然解决了像素问题,却带来了新的色彩问题。从图像传感器采集过来的Bayer Raw数据,每个像素只提供一个色彩的颜色数据(Red、Green或Blue)。但是,最终显示或还原每个像素的色彩信息,却是每个像素都需要具备R、G、B三色数据。怎么解决这个问题?没错,色彩插值!在CFA发明之前,前人也的确是通过每个像素分别摆放R、G、B三个滤光片来获得每个像素的R、G、B数据。但是,聪明的Kodak科学家Bryce Bayer发现,通过CFA方式进行后期插值可以几乎不失真地还原每个像素的R、G、B信息。用最节能环保的方式实现性能相当的产品,这才是工程实现的最高境界。 一种常见的Bayer Raw图像的色彩排布如图5.2所示,插值的基本原理很简单,每个像素没有采集到的色彩,可以通过周边的对应色彩值进行一定的运算获得。 图5.2Bayer Raw色彩阵列(见彩插) 在Xilinx的Vivado中,提供了Sensor Demosaic IP用于实现CFA的插值运算,内部功能框图如图5.3所示。多行图像的缓存,对不同像素值位置的判断,相应邻近色彩数据的运算处理,边界上还需要特殊的判断和处理,整个控制上还是略有些复杂。作者早年由于项目需要,做过一个CFA插值的实现,各种分支判断处理,极费脑力。好在今天Xilinx提供的这个Sensor Demosaic IP省去了设计者大量的时间和精力。 图5.3Sensor Demosaic IP内部功能框图 5.1.2基于MATLAB的CFA处理 在MATLAB中,调用函数demosaic可以实现Bayer RAW转RGB图像的功能。运行工 程文件夹at7_img_ex03\matlab下的MATLAB脚本beyer2RGB_matlab.m,可以实现Bayer Raw格式的图像mandi_bayer_raw.tif转换为RGB图像mandi_rgb.tif。MATLAB源码如下: clc;clear `all;close all; %load origin bayer raw image I = imread('mandi_bayer_raw.tif'); %convert a bayer raw image to RGB image J = demosaic(I,'bggr'); %write image as .tif imwrite(J,'mandi_rgb.tif'); %show origin bayer raw and RGB image figure(1) subplot(1,2,1);imshow(I); title('Origin Bayer Raw Image') subplot(1,2,2);imshow(J); title('RGB Image'); Bayer Raw图像和RGB色彩插值后的图像比对如图5.4所示。 图5.4MATLAB实现的Bayer Raw原始图像和转换后的RGB图像(见彩插) 5.1.3Demosaic IP配置与接口说明 官方文档pg286vdemosaic.pdf对Sensor Demosaic IP的使用做了很详细的说明介绍,这里只做简单说明。 1. Demosaic IP配置 打开Vivado,如图5.5所示,在IP Catalog窗口中的Search栏中输入sensor,可以在Vivado Repository→Video & Image Processing下看到名为Sensor Demosaic的IP核,双击它。 图5.5IP分类中的Sensor Demosaic IP 弹出配置页面如图5.6所示,单击OK完成配置。 图5.6Sensor Demosaic IP核配置页面  设定采样时钟数(Samples per Clock)为1,数据位宽(Maximum Data Width)为8(位)。  最大列分辨率(Maximum Number of Columns)为8192(pixel)。  最大行分辨率(Maximum Number of Rows)为4320pixel。  插值方式选择高分辨率插值法(High Resolution Interpolation)。 如图5.7所示,单击Generate按钮生成IP文件,可能需要较长时间。 图5.7Sensor Demosaic IP生成页面 接着还需要使用AXI4Lite总线接口对IP核做初始化配置,才能让IP核正常工作起来。Sensor Demosaic IP寄存器功能描述如表5.1所示。寄存器0x04、0x08、0x0c目前都是保留不用的,无须设置。我们只需要对寄存器0x00、0x10、0x18、0x28进行设置即可。0x00地址是控制(Control)寄存器,若希望IP核能够持续工作,位0(ap_start)和位7(auto_restart)都需要拉高。0x10地址是图像的有效宽度(Active Width)寄存器,0x18地址是图像的有效高度(Active Height)寄存器。例如,我们的图像分辨率为640×480像素,则分别设定0x10地址为640,0x18地址为480。0x28地址是拜耳格式(Bayer Phase)寄存器,该寄存器取值范围是0~3,分别表示输入的Bayer Raw图像的格式为: 0RG/GB,1GR/BG,2GB/RG,3BG/GR。在初始化配置时,通常需要先配置好0x10、0x18、0x28寄存器的值,最后配置0x00寄存器值。 表5.1Sensor Demosaic IP寄存器功能描述 地址 寄存器名称 读/写 功 能 描 述 0x00 控制寄存器 读/写 位0: ap_start(读/写) 位1: ap_done(只读) 位2: ap_idle(只读) 位3: ap_ready(只读) 位6~4: 保留不用 位7: auto_restart(读/写) 位31~8: 保留不用 续表 地址 寄存器名称 读/写 功 能 描 述 0x04 全局中断使能寄存器 读/写 保留不用 0x08 中断使能寄存器 读/写 保留不用 0x0c 中断状态寄存器 读 保留不用 0x10 有效像素宽度寄存器 读/写 每行图像的有效像素宽度 0x18 有效像素高度寄存器 读/写 每帧图像的有效像素行数 0x28 拜耳格式寄存器 读/写 Bayer Raw图像的格式 2. Demosaic IP接口说明 添加好IP后,可以在IP Sources中找到新产生的v_demosaic_0的IP。如图5.8所示,展开Instantiation Template后,可以找到Verilog的例化模板v_demosaic_0.veo,双击打开。 图5.8v_demosaic_0的IP文件夹 v_demosaic_0.veo文件中可以复制下面这段例化模板到设计源码中进行必要的修改。 v_demosaic_0 your_instance_name ( .s_axi_CTRL_AWADDR(s_axi_CTRL_AWADDR), // input wire [5 : 0] s_axi_CTRL_AWADDR .s_axi_CTRL_AWVALID(s_axi_CTRL_AWVALID), // input wire s_axi_CTRL_AWVALID .s_axi_CTRL_AWREADY(s_axi_CTRL_AWREADY), // output wire s_axi_CTRL_AWREADY .s_axi_CTRL_WDATA(s_axi_CTRL_WDATA), // input wire [31 : 0] s_axi_CTRL_WDATA .s_axi_CTRL_WSTRB(s_axi_CTRL_WSTRB), // input wire [3 : 0] s_axi_CTRL_WSTRB .s_axi_CTRL_WVALID(s_axi_CTRL_WVALID), // input wire s_axi_CTRL_WVALID .s_axi_CTRL_WREADY(s_axi_CTRL_WREADY), // output wire s_axi_CTRL_WREADY .s_axi_CTRL_BRESP(s_axi_CTRL_BRESP), // output wire [1 : 0] s_axi_CTRL_BRESP .s_axi_CTRL_BVALID(s_axi_CTRL_BVALID), // output wire s_axi_CTRL_BVALID .s_axi_CTRL_BREADY(s_axi_CTRL_BREADY), // input wire s_axi_CTRL_BREADY .s_axi_CTRL_ARADDR(s_axi_CTRL_ARADDR), // input wire [5 : 0] s_axi_CTRL_ARADDR .s_axi_CTRL_ARVALID(s_axi_CTRL_ARVALID), // input wire s_axi_CTRL_ARVALID .s_axi_CTRL_ARREADY(s_axi_CTRL_ARREADY), // output wire s_axi_CTRL_ARREADY .s_axi_CTRL_RDATA(s_axi_CTRL_RDATA), // output wire [31 : 0] s_axi_CTRL_RDATA .s_axi_CTRL_RRESP(s_axi_CTRL_RRESP), // output wire [1 : 0] s_axi_CTRL_RRESP .s_axi_CTRL_RVALID(s_axi_CTRL_RVALID), // output wire s_axi_CTRL_RVALID .s_axi_CTRL_RREADY(s_axi_CTRL_RREADY), // input wire s_axi_CTRL_RREADY .ap_clk(ap_clk), // input wire ap_clk .ap_rst_n(ap_rst_n), // input wire ap_rst_n .interrupt(interrupt), // output wire interrupt .s_axis_video_TVALID(s_axis_video_TVALID), // input wire s_axis_video_TVALID .s_axis_video_TREADY(s_axis_video_TREADY), // output wire s_axis_video_TREADY .s_axis_video_TDATA(s_axis_video_TDATA), // input wire [7 : 0] s_axis_video_TDATA .s_axis_video_TKEEP(s_axis_video_TKEEP), // input wire [0 : 0] s_axis_video_TKEEP .s_axis_video_TSTRB(s_axis_video_TSTRB), // input wire [0 : 0] s_axis_video_TSTRB .s_axis_video_TUSER(s_axis_video_TUSER), // input wire [0 : 0] s_axis_video_TUSER .s_axis_video_TLAST(s_axis_video_TLAST), // input wire [0 : 0] s_axis_video_TLAST .s_axis_video_TID(s_axis_video_TID), // input wire [0 : 0] s_axis_video_TID .s_axis_video_TDEST(s_axis_video_TDEST), // input wire [0 : 0] s_axis_video_TDEST .m_axis_video_TVALID(m_axis_video_TVALID), // output wire m_axis_video_TVALID .m_axis_video_TREADY(m_axis_video_TREADY), // input wire m_axis_video_TREADY .m_axis_video_TDATA(m_axis_video_TDATA), // output wire [23 : 0] m_axis_video_TDATA .m_axis_video_TKEEP(m_axis_video_TKEEP), // output wire [2 : 0] m_axis_video_TKEEP .m_axis_video_TSTRB(m_axis_video_TSTRB), // output wire [2 : 0] m_axis_video_TSTRB .m_axis_video_TUSER(m_axis_video_TUSER), // output wire [0 : 0] m_axis_video_TUSER .m_axis_video_TLAST(m_axis_video_TLAST), // output wire [0 : 0] m_axis_video_TLAST .m_axis_video_TID(m_axis_video_TID), // output wire [0 : 0] m_axis_video_TID .m_axis_video_TDEST(m_axis_video_TDEST) // output wire [0 : 0] m_axis_video_TDEST ); 对IP的接口简单说明如下。  ap_clk为同步时钟信号; ap_rst_n为低电平有效的复位信号; interrupt为中断信号,目前保留不用。  s_axi_CTRL_*为AXI4Lite总线接口,用于IP核的寄存器配置。通过这个接口,可以实现IP核的分辨率设定、Bayer Raw输入模式设定和开关等设定。上电初始,必须通过这个接口进行配置后IP核才能工作。  s_axis_video_*和m_axis_video_*为AXI4Stream Video总线接口。其中s_axis_video_*为输入到IP核的Bayer Raw数据流以及控制信号,m_axis_video_*为IP核输出的经过转换的RGB数据流以及控制信号。 3. AXI4Lite总线接口简介 AXI4Lite总线是ARM推出的一个轻量级AXI总线接口标准,控制相对简单。针对Demosaic IP核的AXI4Lite总线接口信号描述如表5.2所示。 表5.2AXI4Lite总线接口信号描述 信 号 名 称 方向 位宽 功 能 描 述 s_axi_ctrl_awvalid 输入 1位 写地址通道的写地址有效信号 s_axi_ctrl_awread 输出 1位 写地址通道的写地址准备好信号,指示IP核已经准备好接收写入地址 s_axi_ctrl_awaddr 输入 32位 写地址通道的写入地址总线 s_axi_ctrl_wvalid 输入 1位 写数据通道的写数据有效信号 s_axi_ctrl_wready 输出 1位 写数据通道的写数据准备好信号,指示IP核已经准备好接收写入数据 s_axi_ctrl_wdata 输入 32位 写数据通道的写入数据总线 s_axi_ctrl_bresp 输出 2位 写响应通道的响应信息,指示写入操作的完成状态 s_axi_ctrl_bvalid 输出 1位 写响应通道的响应信息有效信号 s_axi_ctrl_bready 输入 1位 写响应通道的准备好信号,指示FPGA逻辑准备好接收IP的响应信息 s_axi_ctrl_arvalid 输入 1位 读地址通道的读地址有效信号 s_axi_ctrl_arready 输出 1位 读地址通道的读地址准备好信号,指示IP核已经准备好接收读地址 s_axi_ctrl_araddr 输入 32位 读地址通道的读地址总线 s_axi_ctrl_rvalid 输出 1位 读数据通道的读数据有效信号 s_axi_ctrl_rready 输入 1位 读数据通道的读数据准备好信号,指示FPGA已经准备好接收读出的数据 s_axi_ctrl_rdata 输出 32位 读数据通道的读数据总线 s_axi_ctrl_rresp 输出 2位 读数据通道的响应信息,指示读操作的完成状态 关于AXI总线接口的控制时序介绍,可以参考第3章的内容。 4. AXI4Stream Video总线接口简介 AXI4Stream Video总线接口信号及功能描述如表5.3所示。 表5.3AXI4Stream Video总线接口信号及功能描述 信 号 名 称 方向 位宽 功 能 描 述 s_axis_video_tdata 输入 8位 输入视频数据总线 s_axis_video_tvalid 输入 1位 输入视频数据有效信号 s_axis_video_tready 输出 1位 主机准备好接收输入视频数据 s_axis_video_tuser 输入 1位 输入视频帧起始信号 s_axis_video_tlast 输入 1位 输入视频行结束信号 s_axi_video_tstrb 输入 1位 输入数据的字节有效信号 续表 信 号 名 称 方向 位宽 功 能 描 述 s_axi_video_tkeep 输入 1位 输入视频流数据的字节有效信号 s_axi_video_tid 输入 1位 输入视频数据流的识别号 s_axi_video_tdest 输入 1位 输入视频数据的路由信息 m_axis_video_tdata 输出 8位 输出视频数据总线 m_axis_video_tvalid 输出 1位 输出视频数据有效 m_axis_video_tready 输入 1位 从机准备好接收输出视频数据 m_axis_video_tuser 输出 1位 输出视频帧起始信号 m_axis_video_tlast 输出 1位 输出视频行结束信号 m_axi_video_tstrb 输出 1位 输出数据的字节有效信号 m_axi_video_tkeep 输出 1位 输出视频流数据的字节有效信号 m_axi_video_tid 输出 1位 输出视频数据流的识别号 m_axi_video_tdest 输出 1位 输出视频数据的路由信息 一个基本的握手传输时序波形如图5.9所示。每个时钟周期(ACLK)的上升沿,主机送出的数据有效信号(VALID,对应接口中的*_tvalid信号)拉高,且从机反馈的准备好信号(READY,对应接口中的*_tready信号)也为高,那么此时的数据总线(DATA,对应接口中的*_tdata信号)上的数据有效且被从机接收。主机发出的帧起始信号(SOF,对应接口中的*_tuser信号)或行结束信号(EOL,对应接口中的*_tlast信号),会一直保持到VALID和READY信号同时拉高,即从机正常接收到该信号。 图5.9基本的握手传输时序波形 如图5.10所示,主机发出的帧起始信号(SOF,对应接口中的*_tuser信号)拉高,则表示一帧图像(或者说一幅图像)的第一个有效数据正在传输。主机发出的行结束信号(EOL,对应接口中的*_tlast信号)拉高,则表示一行图像的最后一个有效数据正在传输。对于一帧图像的传输,只有一个SOF信号,而有多少行的图像数据就有多少个EOL信号。 图5.10EOL和SOF信号的使用 5.1.4FPGA测试脚本解析 at7_img_ex03工程中的仿真测试脚本at7_bayer2rgb_sim.v主要由以下几部分代码实现对Sensor Demosaic IP核的仿真验证。 图5.11Sensor Demosaic IP核 仿真流程图  接口的声明和参数的定义;  例化Demosaic的IP核 v_demosaic_0;  $readmemh语句读取Bayer Raw图像;  Initial语句产生测试激励;  时钟产生;  $fopen语句创建结果存储文件w1_file;  $fwrite语句将RGB图像写入w1_file文件中。 Initial语句中基本的处理流程如图5.11所示。Demosaic IP核提供了AXI4Lite总线接口,供读写IP核内的寄存器,对IP核进行初始化配置。Initial语句中首先对所有的寄存器、接口做初始化赋值; 然后依次读取Demosaic IP核中的0x00、0x10、0x18、0x28寄存器的默认值,再写配置数据到这些寄存器中,最后再读取这些寄存器值以确认正确写入并生效了; 接着产生一幅640×480像素的Bayer Raw图像到IP核例化的模块中; 最后延时1ms,结束仿真测试。 5.1.5FPGA仿真说明 首先,MATLAB中运行at7_img_ex03\matlab文件夹下的脚本image_txt_generation.m,将Bayer Raw图像mandi_bayer_raw.tif生成十六进制数据存储到image_in_hex.txt文本中。 复制image_in_hex.txt文本,粘贴到at7_img_ex03\at7.sim文件夹下。 Vivado 19.1版本中打开工程at7_img_ex03,如图5.12所示,确认Sources→Simulation Sources→sim_1下的at7_bayer2rgb_sim.v模块为top module,选择Flow Navigator→SIMULATION→Run Simulation启动仿真。整个过程编译时间较长,需要耐心等待。 图5.12at7_img_ex03工程界面 运行仿真后,波形如图5.13所示。 图5.13at7_img_ex03仿真波形 初始化时,AXI4Lite读数据的时序波形如图5.14所示。 图5.14AXI4Lite读数据的时序波形 初始化时,AXI4Lite写数据的时序波形如图5.15所示。 图5.15AXI4Lite写数据的时序波形 仿真运行结束后,在文件夹at7_img_ex03\at7.sim\sim_1\behav\xsim下生成的FPGA输出的RGB色彩数据存放在文本FPGA_CFA_Image.txt中。 运行matlab文件夹下的脚本draw_image_from_FPGA_result.m,可以将FPGA转换出的RGB图像(文本FPGA_CFA_Image.txt)和MATLAB转换出的RGB图像一同绘制出来供比对。 draw_image_from_FPGA_result.m源码如下: clc;clear `all;close all; IMAGE_WIDTH = 3039; IMAGE_HIGHT = 2014; IMAGE_SIZE = 3*IMAGE_WIDTH*IMAGE_HIGHT; %load CFA image data from txt fid1 = fopen('FPGA_CFA_Image.txt', 'r'); img = fscanf(fid1,'%x'); fclose(fid1); img = uint8(img); img2 = reshape(img,3,IMAGE_WIDTH,IMAGE_HIGHT); img3 = permute(img2,[3,2,1]); I = uint8(img3); imwrite(I,'FPGA_CFA_Image.tif'); I = imread('FPGA_CFA_Image.tif'); %load origin bayer raw image J = imread('mandi_rgb.tif'); %show origin bayer raw and RGB image figure(1) subplot(1,2,1);imshow(J); title('RGB Image with MATLAB') subplot(1,2,2);imshow(I); title('RGB Image with FPGA'); MATLAB和FPGA分别做插值产生的RGB图像比对见图5.16,肉眼看上去效果基本相当。 图5.16MATLAB和FPGA产生的RGB图像比对(见彩插) 5.2色彩滤波矩阵的FPGA实现 5.2.1FPGA功能概述 5.1节的实例,我们结合MATLAB对Vivado 2019.1版本上的Sensor Demosaic IP核进行了仿真验证,初步了解这个Demosaic IP核的使用。在本实例中,要把这个IP核应用到实际工程中进行验证。 如图5.17所示,这是整个视频采集和处理系统的功能框图。上电初始,FPGA通过SCCB接口对OV5640进行寄存器初始化配置。这些初始化的基本参数,即初始化地址对应的初始化数据都存储在FPGA内,以查找表(LUT)的形式逐个写入OV5640中。在初始化配置完成后,OV5640就能够持续输出标准Bayer Raw的视频数据流,FPGA通过对其同步信号,如时钟、行频和场频信号进行检测,从数据总线上实时地采集图像数据。 在FPGA内部,采集到的视频数据先通过一个异步FIFO,将原本时钟域为25MHz下同步的数据流转换到50MHz下。这个FIFO中的数据一旦达到一定数量,会被读取到2个不同的后续模块中处理: 其中一个模块将Bayer Raw格式的图像写入DDR3中缓存,LCD显示驱动模块将读取DDR3中Bayer Raw图像以灰度形式显示到VGA显示器的左侧; 另一个模块会在原始图像缓存到DDR3之前,把这个Bayer Raw格式的图像经过Sensor Demosaic IP核(CFA处理)处理后,转为RGB色彩图像,写入DDR3另一片存储空间中,LCD显示驱动模块将会读取DDR3中的这部分RGB图像显示到VGA显示器的右侧。 图5.17色彩滤波矩阵功能框图 5.2.2FPGA设计说明 FPGA工程的层次结构如图5.18所示。 图5.18at7_img_ex04工程源码层次 at7_img_ex04工程模块的功能描述如表5.4所示。 1. Demosaic IP配置 打开Vivado,在IP Catalog窗口Search中输入sensor,可以在Vivado Repository→Video & Image Processing下看到名为Sensor Demosaic的IP核,双击它。 弹出Demosaic IP核配置页面如图5.19所示,单击OK完成配置。 表5.4at7_img_ex04工程模块功能描述 模 块 名 称 功 能 描 述 clk_wiz_0 该模块是PLL IP核的例化模块,该PLL用于产生系统中所需要的不同频率时钟信号 mig_7series_0 该模块是DDR3控制器IP核的例化模块。FPGA内部逻辑读写访问DDR3都是通过该模块实现,该模块包含与DDR3芯片连接的物理层接口 Image_controller 该模块及其子模块实现SCCB接口对OV5640的初始化、OV5640输出图像的采集控制等。这个模块内部例化了两个子模块: I2C_OV5640_Init_RGB565模块实现SCCB接口通信协议和初始化配置,其下例化的I2C_Controller模块实现SCCB协议,I2C_OV5640_RGB565_Config模块用于产生图像传感器的初始配置数据,SCCB接口的初始化配置控制实现则在I2C_OV5640_Init_RGB565模块中实现; image_capture模块实现图像采集功能 ddr3_cache 该模块主要用于缓存读或写DDR3的数据,其下例化了两个FIFO。该模块衔接FPGA内部逻辑与DDR3 IP核(mig_7series_0.v模块)之间的数据交互 bayer2rgb 该模块实现Bayer Raw图像转换为RGB888图像的处理。该模块例化了Sensor Demosaic IP核,通过AXI4Lite接口对IP核初始化,通过AXI4Stream Video接口实现FPGA逻辑与IP核之间的图像传输 lcd_driver 该模块驱动VGA显示器,同时产生读取DDR3中图像数据的控制逻辑 led_controller 该模块控制LED闪烁,指示工作状态 图5.19Demosaic IP核配置页面  设定采样时钟数(Samples per Clock)为1,数据位宽(Maximum Data Width)为8(位)。  最大列分辨率(Maximum Number of Columns)为640(单位: 像素)。  最大行分辨率(Maximum Number of Rows)为480(单位: 行)。  插值方式选择高分辨率插值法(High Resolution Interpolation)。 2. bayer2rgb.v模块代码解析 bayer2rgb.v模块实现Sensor Demosaic IP核的例化和初始化操作。该模块功能框图如图5.20所示。上电后定时计数逻辑工作,产生4组(8个)定时脉冲,使用AXI4Lite接口对Demosaic IP核进行初始化操作,对其4个主要的寄存器做初始化赋值。输入的Bayer Raw视频流通过Sensor Demosaic IP核处理后,输出RGB格式的视频流。同时,使用Bayer Raw图像帧结束信号i_bayer_image_eof作为复位计数器的启动脉冲,以产生RGB图像的复位信号o_rgb_image_rst。 图5.20bayer2rgb.v模块功能框图 该模块的接口定义如下。i_bayer_image_*接口来自OV5640摄像头采集到的Bayer Raw格式图像。o_rgb_image_*接口为经过Demosaic IP核处理后转成Color RGB的彩色图像。 `timescale 1ns/1ps module bayer2rgb( input clk, input rst_n, //input Image Data Flow input i_bayer_image_vld, output o_bayer_image_tready, input[7:0] i_bayer_image_data, input i_bayer_image_sof, input i_bayer_image_eof, input i_bayer_image_eol, //output Image Data Flow output reg o_rgb_image_rst, output o_rgb_image_vld, output[23:0] o_rgb_image_data ); bayer2rgb.v模块接口定义如表5.5所示。 表5.5bayer2rgb.v模块接口定义 信号名 方向 描述 clk Input 时钟信号 rst_n Input 复位信号,低电平有效 i_bayer_image_vld Input Bayer Raw图像数据有效信号 o_bayer_image_tready Output Demosaic IP核传输准备好 i_bayer_image_data[7:0] Input Bayer Raw图像数据 i_bayer_image_sof Input Bayer Raw图像帧起始信号 i_bayer_image_eof Input Bayer Raw图像帧结束信号 i_bayer_image_eol Input Bayer Raw图像行结束信号 o_rgb_image_rst Output RGB图像复位信号,高电平有效 o_rgb_image_vld Output RGB图像数据有效信号 o_rgb_image_data[23:0] Output RGB图像数据,位23~16为R数据,位15~8为B数据,位7~ 0为G数据 bayer2rgb.v模块内部寄存器等接口声明如下。 reg[15:0] cnt; reg[5:0] i_axi_ctrl_awaddr; reg i_axi_ctrl_awvalid; wire o_axi_ctrl_awready; reg[31:0] i_axi_ctrl_awdata; reg i_axi_ctrl_wvalid; wire o_axi_ctrl_wready; 分辨率参数定义如下。 parameter IMAGE_WIDTH = 32'd640; parameter IMAGE_HIGHT = 32'd480; Demosaic IP核的例化如下,具体接口的定义可以参考at7_img_ex03实例(见5.1.3节)的介绍。 //////////////////////////////////////////////////// //demosaic IP例化 v_demosaic_1uut_v_demosaic_1 ( .s_axi_CTRL_AWADDR(i_axi_ctrl_awaddr),// input wire [5 : 0] s_axi_CTRL_AWADDR .s_axi_CTRL_AWVALID (i_axi_ctrl_awvalid), // input wire s_axi_CTRL_AWVALID .s_axi_CTRL_AWREADY (o_axi_ctrl_awready), // output wire s_axi_CTRL_AWREADY .s_axi_CTRL_WDATA (i_axi_ctrl_awdata), // input wire [31 : 0] s_axi_CTRL_WDATA .s_axi_CTRL_WSTRB (4'hf/*s_axi_CTRL_WSTRB*/),// input wire [3 : 0] s_axi_CTRL_WSTRB .s_axi_CTRL_WVALID (i_axi_ctrl_wvalid), // input wire s_axi_CTRL_WVALID .s_axi_CTRL_WREADY (o_axi_ctrl_wready), // output wire s_axi_CTRL_WREADY .s_axi_CTRL_BRESP (/*s_axi_CTRL_BRESP*/), // output wire [1 : 0] s_axi_CTRL_BRESP .s_axi_CTRL_BVALID (/*s_axi_CTRL_BVALID*/), // output wire s_axi_CTRL_BVALID .s_axi_CTRL_BREADY (1'b1/*s_axi_CTRL_BREADY*/), // input wire s_axi_CTRL_BREADY .s_axi_CTRL_ARADDR (6'd0), // input wire [5 : 0] s_axi_CTRL_ARADDR .s_axi_CTRL_ARVALID (1'b0), // input wire s_axi_CTRL_ARVALID .s_axi_CTRL_ARREADY (), // output wire s_axi_CTRL_ARREADY .s_axi_CTRL_RDATA (), // output wire [31 : 0] s_axi_CTRL_RDATA .s_axi_CTRL_RRESP (/*s_axi_CTRL_RRESP*/),// output wire [1 : 0] s_axi_CTRL_RRESP .s_axi_CTRL_RVALID (), // output wire s_axi_CTRL_RVALID .s_axi_CTRL_RREADY (1'b1/*s_axi_CTRL_RREADY*/), // input wire s_axi_CTRL_RREADY .ap_clk (clk), // input wire ap_clk .ap_rst_n (rst_n), // input wire ap_rst_n .interrupt (/*interrupt*/), // output wire interrupt .s_axis_video_TVALID (i_bayer_image_vld), // input wire s_axis_video_TVALID .s_axis_video_TREADY (o_bayer_image_tready), // output wire s_axis_video_TREADY .s_axis_video_TDATA (i_bayer_image_data), // input wire [7 : 0] s_axis_video_TDATA .s_axis_video_TKEEP (1'b1/*s_axis_video_TKEEP*/),// input wire [0 : 0] s_axis_video_TKEEP .s_axis_video_TSTRB (1'b1/*s_axis_video_TSTRB*/),// input wire [0 : 0] s_axis_video_TSTRB .s_axis_video_TUSER (i_bayer_image_sof), // input wire [0 : 0] s_axis_video_TUSER .s_axis_video_TLAST (i_bayer_image_eol), // input wire [0 : 0] s_axis_video_TLAST .s_axis_video_TID (1'b1/*s_axis_video_TID*/), // input wire [0 : 0] s_axis_video_TID .s_axis_video_TDEST (1'b1/*s_axis_video_TDEST*/),// input wire [0 : 0] s_axis_video_TDEST .m_axis_video_TVALID (o_rgb_image_vld), // output wire m_axis_video_TVALID .m_axis_video_TREADY (1'b1/*m_axis_video_TREADY*/),// input wire m_axis_video_TREADY .m_axis_video_TDATA (o_rgb_image_data), //output wire [23 : 0] m_axis_video_TDATA .m_axis_video_TKEEP (/*m_axis_video_TKEEP*/),// output wire [2 : 0] m_axis_video_TKEEP .m_axis_video_TSTRB (/*m_axis_video_TSTRB*/),// output wire [2 : 0] m_axis_video_TSTRB .m_axis_video_TUSER (/*m_axis_video_TUSER*/),// output wire [0 : 0] m_axis_video_TUSER .m_axis_video_TLAST (/*m_axis_video_TLAST*/),// output wire [0 : 0] m_axis_video_TLAST .m_axis_video_TID (/*m_axis_video_TID*/),// output wire [0 : 0] m_axis_video_TID .m_axis_video_TDEST (/*m_axis_video_TDEST*/) // output wire [0 : 0] m_axis_video_TDEST ); 以下初始化计数与时序控制逻辑实现Sensor Demosaic IP核的初始化配置,将其设定为640×480分辨率,输入Bayer Raw格式为GR/BG,启动运行。这部分代码的基本功能如图5.21所示,类似一个软件顺序执行的初始化控制。 图5.21Sensor Demosaic IP核的初始化配置流程 //////////////////////////////////////////////////// //demosaic IP初始化 always @(posedge clk) if(!rst_n) cnt <= 16'd0; else if(cnt < 16'hffff) cnt <= cnt+1'b1; wire timer_1 = (cnt == 16'h8000); wire timer_2 = (cnt == 16'h8004); wire timer_3 = (cnt == 16'h9000); wire timer_4 = (cnt == 16'h9004); wire timer_5 = (cnt == 16'ha000); wire timer_6 = (cnt == 16'ha004); wire timer_7 = (cnt == 16'hb000); wire timer_8 = (cnt == 16'hb004); always @(posedge clk) if(!rst_n) begin i_axi_ctrl_awaddr <= 6'd0; i_axi_ctrl_awvalid <= 1'b0; i_axi_ctrl_awdata <= 32'd0; i_axi_ctrl_wvalid <= 1'b0; end //register 0x0010 (active width) = IMAGE_WIDTH else if(timer_1) begin i_axi_ctrl_awaddr <= 6'h10; i_axi_ctrl_awvalid <= 1'b1; i_axi_ctrl_awdata <= 32'd0; i_axi_ctrl_wvalid <= 1'b0; end else if(timer_2) begin i_axi_ctrl_awaddr <= 6'd0; i_axi_ctrl_awvalid <= 1'b0; i_axi_ctrl_awdata <= IMAGE_WIDTH; i_axi_ctrl_wvalid <= 1'b1; end //register 0x0018 (active height) = IMAGE_HIGHT else if(timer_3) begin i_axi_ctrl_awaddr <= 6'h18; i_axi_ctrl_awvalid <= 1'b1; i_axi_ctrl_awdata <= 32'd0; i_axi_ctrl_wvalid <= 1'b0; end else if(timer_4) begin i_axi_ctrl_awaddr <= 6'd0; i_axi_ctrl_awvalid <= 1'b0; i_axi_ctrl_awdata <= IMAGE_HIGHT; i_axi_ctrl_wvalid <= 1'b1; end //register 0x0028 (bayer phase) = 0- RG/GB, 1 - GR/BG, 2 - GB/RG, 3- BG/GR else if(timer_5) begin i_axi_ctrl_awaddr <= 6'h28; i_axi_ctrl_awvalid <= 1'b1; i_axi_ctrl_awdata <= 32'd0; i_axi_ctrl_wvalid <= 1'b0; end else if(timer_6) begin i_axi_ctrl_awaddr <= 6'd0; i_axi_ctrl_awvalid <= 1'b0; i_axi_ctrl_awdata <= 32'd1;  //GR/BG //i_axi_ctrl_awdata <= 32'd2;  //GB/RG //i_axi_ctrl_awdata <= 32'd3;  // BG/GR i_axi_ctrl_wvalid <= 1'b1; end //register 0 (ctrl): bit7=1 (auto_restart) else if(timer_7) begin i_axi_ctrl_awaddr <= 6'd0; i_axi_ctrl_awvalid <= 1'b1; i_axi_ctrl_awdata <= 32'd0; i_axi_ctrl_wvalid <= 1'b0; end else if(timer_8) begin i_axi_ctrl_awaddr <= 6'd0; i_axi_ctrl_awvalid <= 1'b0; i_axi_ctrl_awdata <= 32'h0000_0081; i_axi_ctrl_wvalid <= 1'b1; end else begin i_axi_ctrl_awaddr <= 6'd0; i_axi_ctrl_awvalid <= 1'b0; i_axi_ctrl_awdata <= 32'd0; i_axi_ctrl_wvalid <= 1'b0; end //////////////////////////////////////////////////// //延时计数器,产生复位信号 reg[11:0] dly; always @(posedge clk) if(!rst_n) dly <= 12'd0; else if(i_bayer_image_eof) dly <= 12'd1; else if(dly != 12'd0) dly <= dly+1'b1; else dly <= 12'd0; always @(posedge clk) if(!rst_n) o_rgb_image_rst <= 1'b0; else if((dly >= 12'd3200) && (dly <= 12'd3300)) o_rgb_image_rst <= 1'b1; else o_rgb_image_rst <= 1'b0; endmodule 5.2.3FPGA板级调试 连接好OV5640摄像头模块、VGA模块和FPGA开发板,同时连接好FPGA的下载器并给板子供电。 使用Vivado 2019.1打开工程at7_img_ex04,将at7_img_ex04\at7.runs\impl_1文件夹下的at7.bit文件烧录到板子上。如图5.22所示,可以看到VGA显示器同时显示左右两个图像,左侧图像为原始Bayer Raw灰度图像(看上去会有很明显的点阵式的感觉),右侧显示转换后的RGB彩色图像(比较接近真实的彩色图像)。 图5.22OV5640色彩滤波矩阵实现效果图(见彩插) 5.3伽马校正的FPGA实现 5.3.1伽马(Gamma)介绍 1. Gamma的由来 Gamma是一个出现较早的技术。话说由于早期CRT显示器输入单位电压并不会产生等量的亮度(所以是非线性),为了正确地显示画面颜色亮度,刻意制定一个曲线关系(x轴为输入,y轴为输出),让最终输出的影像为线性颜色亮度的影像。即使现代能够制造出线性反应的液晶屏,这种现象仍然深深地影响影像处理,不管是后制、合成、调色或是3D算图渲染都离不开Gamma技术。 人类接收外界信息,视觉占了所有感官的一半以上。无论是输入还是输出或是介于其中,Gamma已经深入人们的生活中,过去使用CRT显示器,现在使用LCD液晶屏,它们有何差异?为什么Mac要使用Gamma 1.8,而PC要使用Gamma 2.2呢?为什么做设计的人比较偏好苹果系列计算机? Gamma是一个描述阶调(Tone)特性的对数。字典里定义Gamma为一个数,指示影像明暗的对比等级,它可以是一条直线。还可以把Gamma描述为非线性指数函数,这个函数是以两个变数来定义: f(x)=xy。Gamma描述一条线性曲线或是在对数尺度的一条直线。 简单来说,Gamma描述了相机或显示屏的非线性(Nonlinear)特性。当一个相机接收到两倍的光强时,相机并不会把这个光转换成两倍的RGB值。其中有感光本身的问题,也有显示(主要是早期的CRT屏幕)的问题,它们都可能使得像素的光亮强度与输入的电压强度并不是呈现线性关系。 现代液晶显示器(Thin Film Transistor Liquid Crystal Display,TFTLCD)本身虽然没有先天的Gamma问题,但是为了要适应传统的工作流程,TFTLCD液晶屏会刻意模拟出Gamma的效果。 2. 现实世界中的Gamma 所有的屏幕都有非线性的输入/输出反应,这是有意为之。 大多数的2D软件都会以线性的颜色模型来处理,所以它假定,255数值的亮度是128数值的两倍。但由于显示器对于信号的输入/输出是非线性的,所以产生的亮度会是不正确的。事实上,大多数屏幕(Gamma=2.2),如果想要显示出数值为255亮度的50%亮度,那就必须要输入(0.51/2.2)×255=186的数值。如果不考虑Gamma的问题,输入128数值,就只会产生约(128/255)2.2=22%的亮度。 数码相机基本上具有线性的输入/输出效果,但因为通常我们会在计算机屏幕上查看拍摄出来的照片,所以数码相机会故意在照片里面嵌入Gamma。JPG格式是带有Gamma的,但是Raw格式是线性的,当把Raw格式转成JPG格式时就会产生非线性的照片了。因此,如果用2D软件打开拍出的JPG图片时,必须要把Gamma补偿回来(去Gamma)。 如果图片是在2D软件产生的(基本上这张图片是线性的),当把这张照片显示在带Gamma的屏幕上,也是要做Gamma补偿的。 Gamma不是缺陷,它是一个功能,因为人的眼睛对光线的亮度具有非线性的感光反应。如果每个颜色只有8 位来记录,如何利用这8 位正确地重现人眼的感光效果很重要,它必须是非线性的编码方式。即使是新一点的屏幕仍然有Gamma: 通常显示卡会用8 位来处理每种颜色避免色带问题,这8 位必须每个强度看起来间距是相等的。 现今大多数计算机屏幕都以sRGB (standard RGB)的标准来显示,也就是Gamma 2.2。大多数的数码相机也以sRGB存储相片,如果是扫描进来的图或是合成图像就不会带有Gamma 2.2。但几乎所有的浮点纪录HDR资料都是线性的,即Gamma为1.0。 对于图片而言,Gamma代表了强度是如何被记录的。换句话说,图片的Gamma是为了要让图片在屏幕上能正确地显示出来。 有些图片会带有Gamma标签,但这是不可靠的,因为很多绘图软件会忽略这个标签。因此,要准确知道图的Gamma数值并不容易。如果显示器有Gamma 2.2而显示的图片看起来很正常,那该图片可能本身就带有Gamma 2.2。 3. Gamma校正 Gamma校正的思路是在最终的颜色输出上应用显示器Gamma的倒数。如图5.23所示,那条向上的虚曲线,它是显示器Gamma曲线的翻转曲线。 图5.23Gamma曲线图(见彩插) 在颜色送到显示器的时候把每个颜色输出都加上这个翻转的Gamma曲线,这样应用了显示器的Gamma以后最终的颜色将会变为线性的。我们所得到的中间色调就会更亮,所以虽然显示器使它们变暗,但是我们又将其平衡回来了。 来看另一个例子。取值为(0.5,0.0,0.0)的暗红色。在将颜色送到显示器之前,先对颜色应用Gamma校正曲线。线性的颜色显示在显示器上相当于降低了2.2次幂的亮度,所以倒数就是1/2.2次幂。Gamma校正后的暗红色就会成为(0.5,0.0,0.0)1/2.2=(0.5,0.0,0.0)0.45=(0.73,0.0,0.0)。校正后的颜色接着被发送给显示器,最终显示出来的颜色是(0.73,0.0,0.0)2.2 = (0.5,0.0,0.0)。可以发现使用了Gamma校正,显示器最终会显示出我们在应用中设置的那种线性的颜色。 2.2通常是大多数显示设备的平均Gamma值。基于Gamma 2.2的颜色空间叫作sRGB颜色空间。每个显示器的Gamma曲线都有所不同,但是Gamma 2.2在大多数显示器上表现都不错。出于这个原因,游戏经常都会为玩家提供改变游戏Gamma设置的选项,以适应每个显示器。 4. Gamma计算公式 过去的CRT显示器是使用电子显像管,通过控制电流大小来调整显示屏幕上的亮度。然而亮度和电流之间的关系并非是线性的,也就是说电流强度变为2倍,显示的亮度并非是2倍,而是由式(5.1)决定: Vout=VinGamma (5.1) 其中,Gamma为CRT显示器的伽马值。 然而对于现实中的大部分摄像机或成像设备而言,输入能量和记录在图片文件中的颜色亮度之间的关系却是线性的,这就导致显示器显示的图像与摄像设备捕捉的实际图像不一致,为了校正这个差异,摄像机在保存图像时会自动对数据进行Gammer校正,公式如下: Vout=Vin1/Gamma(5.2) Gamma依旧为显示器的伽马值。这样,当显示器显示图像时,式(5.2)的输出作为式(5.1)的输入,最后抵消了显示器的Gamma 值造成的误差。 Vdisplay = (Vcamera1/Gamma)Gamma = Vcamera 5.3.2MATLAB生成Gamma校正的LUT at7_img_ex05/matlab文件夹下的MATLAB脚本文件gammaCorrection.m可用于生成0.45(1/2.2)的Gamma校正数据,这组256个点的数据以Vivado中可用的ROM初始化文件(gamma_lut.coe)形式保存下来。 gammaCorrection.m的代码如下。 for r = 0:1:255 I(r+1) = round(255*exp((log(r/255))*0.45),0); %I(r+1) = round(255*((r/255)^0.45),0); %J(r+1) = round(255*exp((log10(r/255))*0.45),0); end %% output peak fid20 = fopen('gamma_lut.coe', 'wt'); fprintf(fid20, 'memory_initialization_radix = 16;\n'); fprintf(fid20, 'memory_initialization_vector = \n'); for r = 1:1:255 fprintf(fid20, '%.2x,\n', I(r)); end fprintf(fid20, '%.2x;\n', I(256)); fid20 = fclose(fid20); 0.45的Gamma校正数据绘制的曲线如图5.24所示。 图5.240.45的Gamma校正曲线 5.3.3FPGA功能概述 如图5.25所示,这是整个视频采集和处理系统的功能框图。上电初始,FPGA需要通过SCCB接口对CMOS Sensor进行寄存器初始化配置。这些初始化的基本参数,即初始化地址对应的初始化数据都存储在FPGA内。在初始化配置完成后,CMOS Sensor就能够持续输出标准Bayer Raw的视频数据流,FPGA通过对其同步信号,如时钟、行频和场频进行检测,从而从数据总线上实时地采集图像数据。 图5.25OV5640伽马校正功能框图 在FPGA内部,采集到的视频数据先通过一个FIFO,将原本时钟域为25MHz下同步的数据流转换到50MHz下。接着将这个数据再送入写DDR3缓存的异步FIFO中,这个FIFO中的数据一旦达到一定数量,会被读取并进行Bayer Raw转Color RGB的处理,随后图像送往两个不同的模块: 一个模块将图像直接写入DDR3中,最终被读取并显示到VGA显示器的左侧; 另一个模块会在Color RGB图像缓存到DDR3之前,对图像进行Gamma校正处理,然后再写入DDR3另一片存储空间中,最终被读取的Gamma校正后图像显示到VGA显示器的右侧。 5.3.4FPGA设计说明 at7_img_ex05工程源码的层次结构如图5.26所示。 图5.26at7_img_ex05工程源码层次结构 at7_img_ex05工程模块功能描述如表5.6所示。 表5.6at7_img_ex05工程模块功能描述 模 块 名 称 功 能 描 述 clk_wiz_0 该模块是PLL IP核的例化模块,该PLL用于产生系统中所需要的不同频率时钟信号 mig_7series_0 该模块是DDR3控制器IP核的例化模块。FPGA内部逻辑读写访问DDR3都是通过该模块实现,该模块包含与DDR3芯片连接的物理层接口 image_controller 该模块及其子模块实现SCCB接口对OV5640的初始化、OV5640输出图像的采集控制等。这个模块内部例化了两个子模块,I2C_OV5640_Init_RGB565模块实现SCCB接口通信协议和初始化配置,其下例化的I2C_Controller模块实现SCCB协议,I2C_OV5640_RGB565_Config模块用于产生图像传感器的初始配置数据,SCCB接口的初始化配置控制实现则在I2C_OV5640_Init_RGB565模块中实现; image_capture模块实现图像采集功能 ddr3_cache 该模块主要用于缓存读或写DDR3的数据,其下例化了两个FIFO。该模块连接FPGA内部逻辑与DDR3 IP核(mig_7series_0模块)之间的数据交互 bayer2rgb 该模块实现Bayer Raw图像转换为RGB888图像的处理。该模块例化了Demosaic IP核,通过AXI4Lite接口对IP核初始化,通过AXI4Stream Video接口实现FPGA逻辑与IP核之间的图像传输 gamma_correction 该模块使用一个预定义好的ROM存储Gamma校正的查找表(LUT),用输入的图像数据作为ROM的地址进行查表,输出的新数据便是Gamma校正后的图像 lcd_driver 该模块驱动LCD,同时产生读取DDR3中图像数据的控制逻辑 led_controller 该模块控制LED闪烁,指示工作状态 1. Gamma校正模块 Gamma校正在gamma_correction.v模块中实现,功能实现其实很简单,只要使用一个预定义好的ROM存储Gamma校正的查找表,用输入的图像数据作为ROM的地址进行查表,输出的新数据便是Gamma校正后的图像。 该模块的层次结构如图5.27所示。 图5.27gamma_correction.v模块层次结构 该模块的内部功能框图如图5.28所示。R、G、B这3个通道分别需要一个ROM存储查找表,它们的查找表内容是一致的,因此只需要配置一个ROM,加载查找表并做3次例化即可。输入R、G、B的数据作为ROM的地址,获取相应地址的输出即Gamma校正后的结果。 图5.28gamma_correction.v模块内部功能框图 2. ROM添加与配置 gamma_correction.v模块中配置了3个ROM(R、G、B通道各1个),这3个ROM的配置完全一致。 在IP Catalog中找到Block Memory Generator的IP,如图5.29所示,双击添加。 图5.29IP Catalog中的Block Memory Generator IP 弹出Basic配置页面,如图5.30所示,配置Memory Type为Single Port ROM。 图5.30Block Memory Generator IP基本配置页面 如图5.31所示,Port A Options页面配置Memory Size为256×8位。 如图5.32所示,Other Options页面勾选Load Init File,加载预先准备好的gamma_lut.coe作为ROM初始化文件。注意这个gamma_lut.coe的生成可以使用at7_img_ex05/matlab下的MATLAB脚本文件gammaCorrection.m生成。 图5.31Block Memory Generator IP端口A配置页面 图5.32Block Memory Generator IP的其他选项配置页面 5.3.5FPGA板级调试 图5.33Gamma校正图像效果(见彩插) 连接好OV5640摄像头模块、VGA模块和FPGA开发板,同时连接好FPGA的下载器并给板子供电。 使用Vivado 2019.1打开工程at7_img_ex05,将at7_img_ex05\at7.runs\impl_1文件夹下的at7.bit文件烧录到板子上。如图5.33所示,可以看到VGA显示器的左右两侧同时有两个图像,左侧图像为原始的图像,右侧图像为进行Gamma校正后的图像。设计者可以使用不同的Gamma校正表,以实现不同的Gamma效果。 5.4白平衡校正的FPGA实现 5.4.1白平衡介绍 1. 为什么需要白平衡 白平衡的英文名称为White Balance,简写为WB。对于摄影和图像处理,白平衡调节的主要就是为了让图片中的白色渲染更接近实际的白色,或者通过白平衡的调节来改变图像色调,营造某种氛围。为了获得更准确的白色,在相机和修图工具中都有自定义白平衡的功能,而在我们大多数手机上却并没有自定义白平衡选项,对于手机来说,白平衡的校准是由软件自动完成的,并不需要我们参与,所以这也导致该功能最容易被我们忽略,但其实白平衡对整幅图像色彩的准确度起着至关重要的作用。 不同性质的光源会在画面中产生不同的色彩倾向,比如说,蜡烛的光线会使画面偏橘黄色,而黄昏过后的光线则会为景物披上一层蓝色的冷调。由于我们的视觉系统会自动对不同的光线作出补偿,所以无论在暖调还是冷调的光线环境下,我们看一张白纸永远还是白色的。但相机则不然,它只会直接记录呈现在它面前的色彩,这就会导致画面色彩偏暖或偏冷。因此,为了让实际环境中白色的物体在所拍摄的画面中也呈现出“真正”的白色,就需要有“白平衡校正”。 如图5.34所示,在不同的光照条件下,即存在着不同的色温,物体“偏光”的程度是不同的。理想情况下,当使用图像采集设备做图像采集时,若知道当前的光照条件(色温),那么通过一定的校正手段,就可以将图像的“偏光”校正回来,这便是白平衡的基本思想。当然现实情况下,我们往往不知道当前的色温状况,那么就需要一套通过当前图像计算出色温信息的算法,然后再去做校正,这便是很多成像设备中的“自动白平衡”功能。 图5.34不同光照条件下的色温(见彩插) 注: 色温是表示光源光色的尺度,其单位为K(kelvin)。 2. 如何进行白平衡校正 白平衡是一个很抽象的概念,最通俗的理解就是让白色所成的像依然为白色,如果白是“白”,那其他景物的成像也就会接近人眼的色彩视觉习惯。调整白平衡的过程叫作白平衡调整。 彩色图像传感器内部有三个感光元件,它们分别感受蓝色、绿色、红色的光线,在默认情况下这三个感光电路电子放大比例是相同的,为1∶1∶1的关系,白平衡的调整就是根据被调校的景物改变了这种比例关系。比如被调校景物的蓝、绿、红色光的比例关系是2∶1∶1(蓝光比例多,色温偏高),那么白平衡调整后的比例关系为1∶2∶2,调整后的电路放大比例中明显蓝的比例减少,增加了绿和红的比例,所拍摄的影像经过这样的白平衡调整处理之后,蓝、绿、红的比例才会相同。也就是说,如果被调校的白色偏一点蓝,那么白平衡调整就改变正常的比例关系减弱蓝电路的放大,同时增加绿和红的放大比例,使所成影像依然为白色。这是白平衡校正最基本的原理。 很多图像采集设备内都有自动白平衡功能,有一套对当前采光状况做计算判断后生成白平衡校正参数并加以应用的算法。而这个实例中,我们并不去研究这一整套复杂的自动校准算法,而是用最简单、基本的一组运算,实时地做白平衡调整,让大家去感受手动调整白平衡的乐趣。 按照前面的理论,假设原始采集图像的色彩数据分别为Ri、Gi、Bi,白平衡处理后的色彩数据分别为Ro、Go、Bo,白平衡调整后色彩的最大取值为VIO_R、VIO_G、VIO_B,那么它们的公式如下: Ro=(VIO_G/VIO_R)×Ri Go=(VIO_G/VIO_G)×Gi=Go Bo=(VIO_G/VIO_B)×Bi 这组公式如何理解呢?当我们需要做白平衡调整(实际光照通常不是理想的光照条件),并且根据当前的色温推断(或者查表)采集到的图像色彩的最大取值为VIO_R、VIO_G、VIO_B(大多数色温下,色彩值是到不了最大值的),那么只要把这三个色彩通道的每个像素值按照相应的比例关系调整到一样的“幅值”后,白平衡校正就做到了。而G色彩是人眼最敏感的,通常是以G色彩(VIO_G)当前的“幅值”作为标准,VIO_R和VIO_B要做必要的运算以归一化到以VIO_G作为“幅值”。因此,从公式上看,(VIO_G/VIO_R)和(VIO_G/VIO_B)就是要算出这个归一化的比例关系,然后再乘以当前的色彩值Ri和Bi,就获得归一化以后的色彩值(白平衡校正后的色彩值)。 5.4.2FPGA功能概述 如图5.35所示,这是整个视频采集和处理系统的功能框图。上电初始,FPGA需要通过SCCB接口对CMOS Sensor进行寄存器初始化配置。这些初始化的基本参数,即初始化地址对应的初始化数据都存储在FPGA内。在初始化配置完成后,CMOS Sensor就能够持续输出标准Bayer Raw的视频 图5.35白平衡校正功能框图 数据流,FPGA通过对其同步信号,如时钟、行频和场频进行检测,从而从数据总线上实时地采集图像数据。 在FPGA内部,采集到的视频数据先通过一个FIFO,将原本时钟域为25MHz下同步的数据流转换到50MHz下。接着将这个数据再送入写DDR3缓存的异步FIFO中,这个FIFO中的数据一旦达到一定数量,会被读取并进行Bayer Raw转Color RGB的处理,随后图像送往两个不同的模块: 一个模块是将图像直接写入DDR3中,最终被读取并显示到VGA显示器的左侧; 另一个模块会在图像缓存到DDR3之前,对图像进行白平衡校正处理,然后再写入DDR3另一片存储空间中,最终被读取的白平衡校正后图像显示在VGA显示器的右侧。与此同时,白平衡校正值的输入是通过连接JTAG接口,在Vivado中的Virtual IO在线调试界面进行配置实现的。 5.4.3FPGA设计说明 at7_img_ex06工程源码的层次结构如图5.36所示。 图5.36at7_img_ex06工程源码层次结构 at7_img_ex06工程模块及功能描述如表5.7所示。 表5.7at7_img_ex06工程模块及功能描述 模 块 名 称 功 能 描 述 clk_wiz_0 该模块是PLL IP核的例化模块,该PLL用于产生系统中所需要的不同频率时钟信号 mig_7series_0 该模块是DDR3控制器IP核的例化模块。FPGA内部逻辑读写访问DDR3都是通过该模块实现,该模块对外直接控制DDR3芯片 Image_controller 该模块及其子模块实现SCCB接口对OV5640的初始化、OV5640输出图像的采集控制等。这个模块内部例化了两个子模块,I2C_OV5640_Init_RGB565模块实现SCCB接口通信协议和初始化配置,其下例化的I2C_Controller模块实现SCCB协议,I2C_OV5640_RGB565_Config模块用于产生图像传感器的初始配置数据,SCCB接口的初始化配置控制实现则在I2C_OV5640_Init_RGB565模块中实现; image_capture模块实现图像采集功能 bayer2rgb 该模块实现Bayer Raw图像转换为RGB888图像的处理。该模块例化了Demosaic IP核,通过AXI4Lite接口对IP核初始化,通过AXI4Stream Video接口实现FPGA逻辑与IP核之间的图像传输 续表 模 块 名 称 功 能 描 述 ddr3_cache 该模块主要用于缓存读或写DDR3的数据,其下例化了两个FIFO。该模块连接FPGA内部逻辑与DDR3 IP核(mig_7series_0模块)之间的数据交互 white_balance 该模块实现白平衡处理功能,例化的VIO模块,其3个输入值作为白平衡校正计算的主要参数 lcd_driver 该模块驱动LCD,同时产生读取DDR3中图像数据的控制逻辑 led_controller 该模块控制LED闪烁,指示工作状态 如图5.37所示,这是white_balance.v模块的功能框图。VIO IP输入的参数进行除法运算后,分别与R和B的输入(i_rgb_image_data[23:16]和i_rgb_image_data[15:8])进行乘法运算,最后对结果做溢出处理判断; G的输入(i_rgb_image_data[7:0])只需要缓存几拍,与R和B的乘除运算保持同步即可; 输入图像有效信号i_rgb_imag_vld也只需要打3拍和输出数据保持同步,输出o_wb_imag_vld即可。 图5.37white_balance.v模块的功能框图 关于R和B的运算,已经推导的公式如下: Ro=(VIO_G/VIO_R)×Ri Go=(VIO_G/VIO_G)×Gi=Go Bo=(VIO_G/VIO_B)×Bi Go由于赋值不变,所以无须做乘除运算。对于Go和Bo的运算,由于VIO_G/VIO_R或VIO_G/VIO_B的取值是一个小数,在FPGA中实现小数运算,必须先放大为整数。因此,第一步除法运算获得中间结果的公式如下: Rc=VIO_G×1024/VIO_R Bc=VIO_G×1024/VIO_B 在FPGA中,“×1024”的运算,通过左移10位来实现。除法器的IP核中,被除数表示为{6'd0,vio_g,10'd0},以此实现VIO_G×1024。 第二步乘法运算后,相应地需要缩小为原来的1/1024,公式如下: Ro=Rc×Ri/1024 Bo=Bc×Bi/1024 在FPGA中,“/1024”的运算,通过右移10位来实现,即最终的乘法运算结果,取mul_r[17:10]和mul_b[17:10]作为最终输出。 对于溢出判断部分,我们用最终获取的Ro、Go、Bo分别和VIO_G进行比较,若大于VIO_G则以VIO_G的值替代,否则保持原值。 5.4.4FPGA板级调试 连接好OV5640摄像头模块、VGA模块和FPGA开发板,同时连接好FPGA的下载器并给板子供电。 使用Vivado 2019.1打开工程at7_img_ex06,将at7_img_ex06\at7.runs\impl_2文件夹下的at7.bit文件以及debug_nets.ltx文件烧录到FPGA器件中,可以看到VGA显示器同时显示左右两个图像,左侧图像为原始图像,右侧显示进行白平衡校正后的图像。通过VIO调试界面可以调整白平衡校正的三个最大R、G、B通道取值,可以改变右侧白平衡校正图像的显示效果。 如图5.38(a)所示,双击打开hw_vio_1的调试界面,将vio_b、vio_g、vio_r这3组信号添加到调试主界面中,默认情况下的取值都是255,即显示器上的左和右两个视频图像是一样的效果。如图5.38(b)所示,将当前的vio_b、vio_g、vio_r取值分别设置为250、180和255。 图5.38VIO调试界面数值 图5.38(续) 如图5.39所示,修改了VIO参数后,原本整体有些发红的图像(图5.39左侧),在做了白平衡校正后,图像(图5.39右侧)发红的现象有很大改善,基本能够还原真实的白色了(当然了,按照我们当前的基本算法,色彩的整体亮度会有一定的牺牲)。 图5.39白平衡校正效果图(见彩插) 5.5色彩空间转换与图像增强IP核的仿真 5.5.1图像增强IP简介 Xilinx的Vivado中集成的图像增强(Image Enhancement)IP可以有效降低图像噪声并增强图像边缘。该IP使用了2D滤波方式,可以在达到更好的图像噪声抑制的同时,保留并增强图像边缘。 对于一个比较经典的图像前端处理,图像增强也是一个必不可少的步骤。在这个实例中,我们需要让图像分别经过RGB to YCbCr模块、图像增强模块和YCbCr to RGB模块的处理,这3个模块在Vivado中都有可用的IP核。 图像增强IP的功能框图如图5.40所示。该IP输入和输出的图像数据必须为YUV444或YUV422模式; 待处理图像进入IP后,首先需要多行缓存,然后分别通过降噪(Noise Reduction)、边缘检测(Edge Map Morphology)、边缘增强(Edge Enhance)模块。完成处理后的图像再拟合在一块,最后会通过可选的光环抑制(Antihalo)和锯齿消除(Antialias)模块,完成最终图像输出。边缘增强和噪声抑制实际上是两个完全相反的图像处理方式,为了保证两个模块能够更好地实现增强图像的效果,在这个IP中,第一步做的是图像的形态检测(Edge Map Morphology),然后再根据这个结果,对图像中需要降噪的部分和边缘增强的部分分别处理。 图5.40图像处理IP功能框图 1. 图像形态检测 图像形态检测是整个图像增强的第一步,它用于指示后续需要对图像进行的降噪或边缘增强操作。图像形态检测主要包括下面两步: (1) 经过二维的FIR滤波器,从水平、垂直以及两个对角共4个维度提取边缘信息。 (2) 使用拉长、正交的结构单元和形态学处理,用于提供清晰的各个方向边缘信息。 2. 降噪处理 降噪处理是基于中心像素点以及特定的临近像素点的滤波实现的。算法实现类似高斯的定向低通滤波。噪声门限由IP核的设置决定。图像形态检测信息标定出的边缘不会做任何的降噪处理。降噪处理功能框图如图5.41所示。 图5.41降噪处理功能框图 3. 边缘增强 IP核设定的边缘增强参数决定了边缘增强的幅度。根据边缘检测形态信息,边缘增强模块对标记处的边缘做拉普拉斯滤波,实现边缘增强效果。边缘增强功能框图如图5.42所示。 4. 光环抑制与锯齿消除 光环抑制(Antihalo)和锯齿消除(Antialias)模块是可选的功能块。前面进行噪声抑制和边缘增强后的图像,可能存在图像边缘被放大或抑制的情况。光环抑制和锯齿消除,通过每个新的像素值与原图像的像素值以及邻近8个像素值的比较,以判断其是否需要进行相应的处理并实现图像的优化。 图5.42边缘增强功能框图 如图5.43所示,原图在图像增强后可能出现光环现象,那么经过光环抑制后图像就能够实现最优化。 图5.43原图、光环图像和光环抑制图像 5.5.2IP添加与配置 Vivado的IP Catalog中,在Video & Image Processing分类下,如图5.44所示,可以看到有很多可用的图像处理IP核。我们需要用到的RGB to YCbCr、Image Enhancement和YCbCr to RGB这3个IP核,在该分类下都可以找到。 图5.44Video & Image Processing IP核 1. RGB to YCbCr IP配置 双击IP Catalog中的RGB to YCrCb ColorSpace Converter这个IP核,进入Features配置页面,如图5.45所示。设置图像位宽8位,分辨率640×480像素,YUV(YCbCr)格式,输出图像取值范围0~255。 图5.45RGB to YCrCb IP核的Features配置页面 如图5.46所示,在Custom配置页面中,可以看到RGB to YCbCr转换的基本公式参数。 2. Image Enhancement IP配置 双击IP Catalog中的Image Enhancement IP核,进入配置页面如图5.47所示。设定图像位宽8位,图像分辨率640×480像素,图像噪声抑制(Image Noise Reduction)水平(取值0~255),图像边缘增强(Image Edge Enhancement)水平(取值0~1.0),以及可选的光环抑制(Halo Suppression)和锯齿消除(AntiAlias Filtering)。 3. YCbCr to RGB IP配置 双击IP Catalog中的YCrCb to RGB ColorSpace Converter这个IP核,如图5.48所示,进入Feature配置页面,设置图像位宽8位,分辨率640×480像素,YUV格式,输出图像取值范围0~255。 如图5.49所示,Custom配置页面中,可以看到YCbCr to RGB转换的基本公式参数。 图5.46RGB to YCrCb IP核的Custom配置页面 图5.47Image Enhancement IP核配置页面 图5.48YCrCb to RGB IP核Features配置页面 图5.49YCrCb to RGB IP核Custom配置页面 5.5.3协同仿真的MATLAB脚本说明 使用at7_img_ex07\matlab文件夹下的MATLAB源码image_txt_generation.m产生作为FPGA仿真输入的测试图像数据,存储在文本image_in_hex.txt中。FPGA仿真测试运行后,将会产生图像增强后的图像数据,存储在文本FPGA_Enhancement_Image.txt中,使用MATLAB的脚本draw_image_from_FPGA_result.m可以调用这个文本中的图像数据,同时显示图像增强前后的效果供比对。MATLAB产生与调用文本的示意图如图5.50所示。 图5.50MATLAB产生与调用文本示意图 1. 测试激励图像产生 工程路径“\at7_img_ex07\matlab”下的image_txt_generation.m脚本,在MATLAB中运行后,会将同路径下的test.bmp图像的色彩信息转换为txt文本(同路径下的image_in_hex.txt)以十六进制保存。 clc;clear `all;close all; IMAGE_WIDTH = 640; IMAGE_HIGHT = 480; %load origin image %I = imread('Lena_gray_niose.bmp'); I = imread('test.bmp'); I = rgb2gray(I); %fclose(fid1); %% output image data in hex file raw_image = reshape(I, IMAGE_HIGHT, IMAGE_WIDTH); raw_image = raw_image'; fid2 = fopen('image_in_hex.txt', 'wt'); fprintf(fid2, '%04x\n', raw_image); fid2 = fclose(fid2); %show origin image figure,imshow(I); title('Original image'); 2. 测试结果比对 工程路径“\at7_img_ex07\matlab”下的draw_image_from_FPGA_result.m脚本,在MATLAB中运行后,会将同路径下的test.bmp图像以及FPGA仿真生成的FPGA_Enhancement_Image.txt文本中所存储的图像增强后的图像同时显示供查看比对。FPGA_Enhancement_Image.txt文本在FPGA仿真完成后,需要从“at7_img_ex07\at7.sim\sim_1\behav”文件夹复制到“\at7_img_ex07\matlab”文件夹下。 clc;clear `all;close all; IMAGE_WIDTH = 640; IMAGE_HIGHT = 480; %load fft fiter image data from txt fid1 = fopen('FPGA_Enhancement_Image.txt', 'r'); img = fscanf(fid1,'%x'); fclose(fid1); img2 = reshape(img,IMAGE_WIDTH,IMAGE_HIGHT); img2 = img2'; %load origin image I = imread('test.bmp'); I = rgb2gray(I); %show origin image figure,imshow(I); title('Original image'); %show fft fiter image with FPGA figure,imshow(img2,[]) title('Image Enhancement with FPGA'); 5.5.4FPGA仿真说明 如图5.51所示,MATLAB产生的原始图像数据image_in_hex.txt需要在仿真开始前放置在at7_img_ex07\at7.sim文件夹下。 图5.51仿真原始图像存储文本 使用Vivado打开at7_img_ex07工程,在Sources面板中,展开Simulation Sources→sim_1,将at7_image_enhance_sim.v文件设置为top module。选择Flow Navigator面板的Simulation→Run Simulation打开仿真页面。 如图5.52所示,仿真测试结果位于at7_img_ex07\at7.sim\sim_1\behav\xsim文件夹下。 图5.52仿真结果存储文本 在设定Noise Threshold=192,Enhancement Strength=0.0,Halo Suppression=0.125时,用MATLAB比对图像如图5.53所示。 图5.53比对图像1 在设定Noise Threshold=192,Enhancement Strength=0.125,Halo Suppression=0.75时,与MATLAB比对图像如图5.54所示。 图5.54比对图像2 在设定Noise Threshold=192,Enhancement Strength=1.0,Halo Suppression=0.75时,用MATLAB比对图像如图5.55所示。 图5.55比对图像3 5.6色彩空间转换的FPGA实现 5.6.1功能概述 如图5.56所示,这是整个视频采集和处理系统的功能框图。上电初始,FPGA需要通过SCCB接口对CMOS Sensor进行寄存器初始化配置。这些初始化的基本参数,即初始化地址对应的初始化数据都存储在FPGA内。在初始化配置完成后,CMOS Sensor就能够持续输出标准Bayer Raw的视频数据流,FPGA通过对其同步信号,如时钟、行频和场频进行检测,从而从数据总线上实时地采集图像数据。 图5.56RGB2YUV与YUV2RGB实现功能框图 在FPGA内部,采集到的视频数据先通过一个FIFO,将原本时钟域为25MHz下同步的数据流转换到50MHz下。接着将这个数据再送入写DDR3缓存的异步FIFO中,这个FIFO中的数据一旦达到一定数量,会被读取并进行Bayer Raw转Color的处理,随后图像送往两个不同的模块: 一个模块将图像直接写入DDR3中,最终读取DDR3中的彩色图像显示到VGA显示器的左侧; 另一个模块会在彩色图像缓存到DDR3之前,对图像进行RGB和YUV的互转(即RGB先转换为YUV,然后YUV再转换为RGB,此例子主要是为了应用这2个IP核)处理,然后再写入DDR3另一片存储空间中,最终读取DDR3中的这部分图像显示到VGA显示器的右侧。 5.6.2RGB与YUV介绍 1. 基本概念 1) 什么是RGB 对一种颜色进行编码的方法统称为“颜色空间”或“色域”。用最简单的话说,世界上任何一种颜色的“颜色空间”都可定义成一个固定的数字或变量。RGB(红、绿、蓝)只是众多颜色空间的一种。采用这种编码方法,每种颜色都可用三个变量来表示,即红色、绿色和蓝色的强度。存储或显示彩色图像时,RGB是最常见的一种方案。 2) 什么是YUV YUV是被欧洲电视系统所采用的一种颜色编码方法(属于PAL),是PAL和SECAM模拟彩色电视制式采用的颜色空间。 在现代彩色电视系统中,通常采用三管彩色摄影机或彩色CCD摄影机进行取像,然后把取得的彩色图像信号经分色、分别放大校正后得到RGB,再经过矩阵变换电路得到亮度信号Y和两个色差信号BY(即U)、RY(即V),最后发送端将这三个信号分别进行编码,用同一信道发送出去。这种色彩的表示方法就是所谓的YUV色彩空间表示。 由此可见,RGB和YUV都属于颜色空间(或者叫“色彩空间”)。 3) YUV与YCbCr是否一样 YCbCr 其实是YUV经过缩放和偏移的翻版,其中的Y与YUV 中的Y含义一致,Cb和Cr同样都指色彩,只是在表示方法上不同而已。YCbCr中的Y是指亮度分量,Cb指蓝色色度分量,而Cr指红色色度分量。YCbCr应用领域很广,JPEG、MPEG均采用此格式。YCbCr可以被认为是与YUV等同的一种色彩表示方式,一般人们所讲的YUV大多是指YCbCr。 2. RGB和YUV的优缺点 RGB缺乏与早期黑白显示系统的良好兼容性。因此,许多电子电器厂商普遍采用的做法是,将RGB转换成YUV颜色空间,以维持兼容,再根据需要转换回RGB格式,以便在计算机显示器上显示彩色图形。 YUV主要用于优化彩色视频信号的传输,使其向后兼容老式黑白电视。与RGB视频信号传输相比,它最大的优点在于只需占用极少的频宽(RGB要求3个独立的视频信号同时传输)。 YUV色彩空间最重要性的是它的亮度信号Y和色度信号U、V是分离的。如果只有Y信号分量而没有U、V分量,那么这样表示的图像就是黑白灰度图像。彩色电视采用YUV色彩空间正是为了用亮度信号Y解决彩色电视机与黑白电视机的兼容问题,使黑白电视机也能接收彩色电视信号。 3. YUV和RGB的实现原理 RGB是从颜色发光的原理来设计制定的,通俗点说它的颜色混合方式就好像有红、绿、蓝三盏灯,当它们的光相互叠合的时候,色彩相混,而亮度却等于两者亮度之和,越混合亮度越高,即加法混合。 红、 绿、蓝三盏灯的叠加情况,中心三色最亮的叠加区为白色,加法混合的特点: 越叠加越明亮。 红、 绿、蓝三个颜色通道中每种颜色各分为256阶亮度,在0时“灯”最弱——是关掉的,而在255时“灯”最亮。当三色灰度数值相同时,产生不同灰度值的灰色调,即三色灰度都为0时,是最暗的黑色调; 三色灰度都为255时,是最亮的白色调。 RGB颜色称为加成色,因为将 R、G 和 B 添加在一起(即所有光线反射回眼睛)可产生白色。加成色用于照明光、电视和计算机显示器。例如,显示器通过红色、绿色和蓝色荧光粉发射光线产生颜色。绝大多数可视光谱都可表示为红、绿、蓝 (RGB) 三色光在不同比例和强度上的混合。这些颜色若发生重叠,则产生青、洋红和黄。 在YUV中,“Y”表示明亮度(Luminance或Luma),也就是灰阶值; 而“U”和“V”表示的则是色度(Chrominance或Chroma),作用是描述影像色彩及饱和度,用于指定像素的颜色。“亮度”是通过RGB输入信号来建立的,方法是将RGB信号的特定部分叠加到一起。“色度”则定义了颜色的两个方面——色调与饱和度,分别用Cr和Cb来表示。其中,Cr反映了RGB输入信号红色部分与RGB信号亮度值之间的差异; 而Cb反映的是RGB输入信号蓝色部分与RGB信号亮度值之间的差异。 4. YUV的格式 YUV码流的存储格式其实与其采样的方式密切相关,主流的格式有三种: YUV4:4:4、YUV4:2:2和YUV4:2:0,后两种格式都有一定的色彩信息丢失,但由于YUV格式中人眼敏感的亮度信息集中在了Y分量上,人眼相对不敏感的U和V分量的下采样并不会带来太大的人眼视觉冲击。因此,相比于RGB格式,YUV格式的下采样在尽可能降低图像质量的前提下,大大降低了传输图像的数据带宽要求。 用三个图来直观地表示采样方式如图5.57所示,以黑点表示采样该像素点的Y分量,以空心圆圈表示采用该像素点的UV分量。 图5.57YUV的三种采样示意图 YUV 4:4:4采样,每一个Y对应一组UV分量。 YUV 4:2:2采样,每两个Y共用一组UV分量。 YUV 4:2:0采样,每四个Y共用一组UV分量。 5. RGB和YUV转换公式 YUV可以从8位 RGB 直接计算,如下: Y = 0.299 R + 0.587 G + 0.114 B U = - 0.1687 R - 0.3313 G + 0.5 B + 128 V = 0.5 R - 0.4187 G - 0.0813 B + 128 反过来,RGB 也可以直接从YUV计算: R = Y + 1.402 (V-128) G = Y - 0.34414 (U-128) - 0.71414 (V-128) B = Y + 1.772 (U-128) RGB转换为YCbCr格式,如下: Y’ = 0.257*R' + 0.504*G' + 0.098*B' + 16 Cb' = -0.148*R' - 0.291*G' + 0.439*B' + 128 Cr' = 0.439*R' - 0.368*G' - 0.071*B' + 128 反过来,RGB 也可以直接从YCbCr计算: R' = 1.164*(Y’-16) + 1.596*(Cr'-128) G' = 1.164*(Y’-16) - 0.813*(Cr'-128) - 0.392*(Cb'-128) B' = 1.164*(Y’-16) + 2.017*(Cb'-128) 注意上面各个符号都带了一撇('),表示该符号在原值基础上进行了Gamma校正,有助于弥补在抗锯齿的过程中线性分配Gamma值所带来的细节损失,使图像细节更加丰富。在没有采用Gamma校正的情况下,暗部细节不容易显现出来,而采用了这一图像增强技术以后,图像的层次更加明晰了。 5.6.3FPGA设计说明 at7_img_ex08工程源码的层次结构如图5.58所示。 图5.58at7_img_ex08工程源码层次结构 at7_img_ex08工程模块及功能描述如表5.8所示。 表5.8at7_img_ex08工程模块及功能描述 模 块 名 称 功 能 描 述 clk_wiz_0 该模块是PLL IP核的例化模块,该PLL用于产生系统中所需要的不同频率时钟信号 mig_7series_0 该模块是DDR3控制器IP核的例化模块。FPGA内部逻辑读写访问DDR3都是通过该模块实现,该模块对外直接控制DDR3芯片 Image_controller 该模块及其子模块实现SCCB接口对OV5640的初始化、OV5640输出图像的采集控制等。这个模块内部例化了两个子模块,I2C_OV5640_Init_RGB565.v模块实现SCCB接口通信协议和初始化配置,其下例化的I2C_Controller.v模块实现SCCB协议,I2C_OV5640_RGB565_Config.v模块用于产生图像传感器的初始配置数据,SCCB接口的初始化配置控制实现则在I2C_OV5640_Init_RGB565.v模块中实现; image_capture.v模块实现图像采集功能 续表 模 块 名 称 功 能 描 述 bayer2rgb 该模块实现Bayer Raw图像转换为RGB888图像的处理。该模块例化了Demosaic IP核,通过AXI4Lite接口对IP核初始化,通过AXI4Stream Video接口实现FPGA逻辑与IP核之间的图像传输 ddr3_cache 该模块主要用于缓存读或写DDR3的数据,其下例化了两个FIFO。该模块连接FPGA内部逻辑与DDR3 IP核(mig_7series_0.v模块)之间的数据交互 rgb2yuv2rgb 该模块依次对输入的RGB数据做RGB转YUV、YUV转RGB的处理 lcd_driver 该模块驱动LCD,同时产生读取DDR3中图像数据的控制逻辑 led_controller 该模块控制LED闪烁,指示工作状态 1. RGB to YCbCr IP配置 双击IP Catalog中的RGB to YCrCb ColorSpace Converter这个IP核,进入Features配置页面,如图5.59所示。设置图像位宽8位,分辨率640×480像素,YUV(YCbCr)格式,输出图像取值范围0~255。 图5.59RGB to YCrCb IP核的Features配置页面 如图5.60所示,在Custom配置页面中,可以看到RGB to YCbCr转换的基本公式参数。 图5.60RGB to YCrCb IP核的Custom配置页面 2. YCbCr to RGB IP配置 双击IP Catalog中的YCrCb to RGB ColorSpace Converter这个IP核,如图5.61所示,进入Features配置页面,设置图像位宽8位,分辨率640×480像素,YUV格式,输出图像取值范围0~255。 图5.61YCrCb to RGB IP核Features配置页面 如图5.62所示,Custom配置页面中,可以看到YCbCr to RGB转换的基本公式参数。 图5.62YCrCb to RGB IP核Custom配置页面 5.6.4FPGA板级调试 连接好OV5640摄像头模块、VGA模块和FPGA开发板,同时连接好FPGA的下载器并给板子供电。 使用Vivado 2019.1打开工程at7_img_ex08,将at7_img_ex08\at7.runs\impl_1文件夹下的at7.bit文件烧录到板子上,可以看到VGA显示器同时显示左右两个图像,左侧图像为原始图像,右侧图像为做过RGB和YUV互转的效果。由于在RGB和YUV转换过程中,有一定的图像精度损失,所以通过左右两个图像比对可以看出,右侧的图像质量相比左侧的图像会稍差一些。 5.7坏点校正的FPGA实现 5.7.1FPGA功能概述 如图5.63所示,这是整个视频采集系统的功能框图。MT9V034传感器默认的寄存器配置即可输出正常的视频流, FPGA通过对其同步信号,如时钟、行频和场频进行检测,从而从数据总线上实时地采集图像数据。 图5.63MT9V034坏点校正工程功能框图 在FPGA内部,采集到的视频数据先通过一个FIFO,将原本时钟域为25MHz下同步的数据流转换到50MHz下。接着视频数据流会做模拟坏点的生成,生成的视频数据流既会被直接写入DDR3中,也会被送往下一个模块做坏点校正,坏点校正后的视频数据流最后也写入DDR3的另一片地址空间中。与此同时,VGA显示驱动模块产生DDR3数据读取的时序,将DDR3缓存的两组图像数据读出,并送往VGA显示器进行显示。在VGA显示器上,左侧的视频图像是模拟坏点后的图像,在图像上会有多个明显的黑色坏点; 右侧的视频图像则是做了坏点校正的图像,黑色坏点已经消失。 5.7.2FPGA设计说明 at7_img_ex09工程源码的层次结构如图5.64所示。 图5.64at7_img_ex09工程源码层次结构 at7_img_ex09工程模块及功能描述如表5.9所示。 表5.9at7_img_ex09工程模块及功能描述 模 块 名 称 功 能 描 述 clk_wiz_0 该模块是PLL IP核的例化模块,该PLL用于产生系统中所需要的不同频率时钟信号 mig_7series_0 该模块是DDR3控制器IP核的例化模块。FPGA内部逻辑读写访问DDR3都是通过该模块实现,该模块对外直接控制DDR3芯片 image_controller 该模块及其子模块实现MT9V034输出图像的采集控制等。image_capture模块实现图像采集功能 ddr3_cache 该模块主要用于缓存读或写DDR3的数据,其下例化了两个FIFO。该模块连接FPGA内部逻辑与DDR3 IP核(mig_7series_0模块)之间的数据交互 defect_pixel_generation 该模块对输入的视频流固定的几个像素点坐标做特殊处理,使其成为黑色坏点 defec_pixel_correction 该模块使用邻近像素点求平均的算法对坏点坐标的像素做坏点校正处理,使输出图像的坏点消除 lcd_driver 该模块驱动VGA显示器,同时产生读取DDR3中图像数据的控制逻辑 led_controller 该模块控制LED闪烁,指示工作状态 1. 坏点生成 defect_pixel_generation.v模块将原始采集到的MT9V034视频流人为地加入4个坏点,这4个坏点始终输出数据0x00(黑色),坏点坐标分别为(5,5)、(5,8)、(8,10)、(8,12)。 如图5.65所示,defect_pixel_generation.v模块中,根据输入的图像同步信号(如复位信号、数据有效信号和行结束信号)进行每行的像素计数和数据行的计数,以此判断当前输出像素坐标是否为模拟产生坏点的坐标。对于模拟产生坏点的坐标,对应的输出像素数据为0x00,即始终为黑色; 而非坏点坐标的像素数据,则保持正常的视频图像数据。与此同时,由于坏点像素的产生使得数据的输出相比输入有一个时钟周期的延时,所以在这个模块对应输出的图像同步信号(如复位信号、数据有效信号和行结束信号)都延时一个时钟周期后输出。 图5.65defect_pixel_generation.v模块功能框图 这个模拟坏点产生的模块,只是为了模拟图像传感器的坏点。在实际的工程应用中,当然不需要也一定不会有这个模拟坏点的模块。我们这个工程实例中,坏点是人为加入的,那么坏点的坐标也都是事先设定好的,因此在后面的坏点校正模块中,我们也知道需要进行坏点校正的像素坐标,直接找到这个坐标进行处理即可。但在实际工程应用中,坏点是图像传感器本身随机存在的,传感器本身也不会携带任何坏点坐标信息,我们如何能判定并且获取坏点坐标信息呢?一般情况下,这类图像传感器在焊接到电路板制成成品后,都会经过一道图像校准的工序,这道工序的原理基本是这样的: 让图像传感器拍摄标准的白板和黑板,将对应的图像数据进行后处理,以此检测并判定是否有坏点存在,并且标定坏点坐标,然后将这些坐标信息传递给产品。在产品初始化时,可以加载这些标定好的坏点坐标信息并加以使用,比如使用FPGA做图像采集和坏点校正,那么每个特定的产品都会有一组特定的坏点坐标信息保存在本地,在FPGA初始化时这组坏点坐标信息被加载,接着FPGA就使用这组坏点坐标信息进行相应的坏点校正。 2. 坏点校正 defec_pixel_correction.v模块实现坏点校正功能,对模拟产生的4个坏点进行邻近像素点的均值填充。在该模块的设计中,若坏点像素处于边缘(即第1行、最后1行、第1列或最后1列),则不做处理。此外,对坏点像素,使用其周围像素点做如图5.66所示的运算(取上、下、左、右4个点做平均)。 例如,1~8像素是(x,y)点周围邻近的8个像素点。若(x,y)这个点不处于边缘像素,且该像素是坏点,那么就用它左(8)、右(4)、上(2)、下(6)这4个像素点求平均值替代原来的(x,y)点。 图5.66坏点像素值计算 该模块功能框图如图5.67所示,使用2个FIFO分别缓存前后行,即进入图像处理的3组数据流分别是第n-1行、第n行和第n+1行的图像,控制输入数据流和2个FIFO缓存的图像在同一个位置。此外,对前后2个像素的图像值进行缓存,这样便可实现坏点像素坐标以及前后列、上下行之间数据的同步,以此就能获取坏点像素坐标的邻近像素的平均值。通过对几个同步信号的判断,进行每行的像素计数和数据行的计数,以此判断当前坐标是否为坏点坐标,对坏点坐标输出的像素数据赋邻近像素值的平均值,非坏点坐标赋原值输出。 图5.67defec_pixel_correction.v功能框图 5.7.3FPGA板级调试 连接好MT9V034摄像头模块、VGA模块和FPGA开发板,同时连接好FPGA的下载器并给板子供电。 使用Vivado 2019.1打开工程at7_img_ex09,将at7_img_ex09\at7.runs\impl_1文件夹下的at7.bit文件烧录到板子上,可以看到VGA显示器同时显示左右两幅图像,左侧图像为包含模拟黑色坏点的原始图像,将摄像头对准白色背景时,可以看到图像左上角有多个黑色坏点; 右侧图像对坏点做了校正处理,图像看上去和正常没有坏点的效果几乎一样的。 5.8图像直方图统计与实时显示的FPGA实现 5.8.1FPGA系统概述 如图5.68所示,这是整个视频采集系统的功能框图。MT9V034传感器默认的寄存器配置即可输出正常的视频流, FPGA通过对其同步信号,如时钟、行频和场频进行检测,从而从数据总线上实时地采集图像数据。 图5.68MT9V034直方图显示工程功能框图 在FPGA内部,采集到的视频数据先通过一个FIFO,将原本时钟域为25MHz下同步的数据流转换到50MHz下。接着将这个数据再送入写DDR3缓存的异步FIFO中,这个FIFO中的数据一旦达到一定数量,就会写入DDR3中。与此同时,读取DDR3中缓存的图像数据,缓存到FIFO中,并最终送往VGA显示驱动模块进行显示。VGA显示驱动模块不断地发出读图像数据的请求,并驱动VGA显示器显示视频图像。 本实例除了前面提到的对原始图像做DDR3缓存和显示,还会在原始图像缓存到DDR3之前,对当前图像做直方图统计(以帧为单位做统计),统计后的直方图结果做归一化处理,便于后续VGA显示器显示的直方图绘制,归一化的直方图结果取值范围是0~448,用256个10位数据表示,存入双口RAM中。根据VGA显示模块的请求,从双口RAM读取实时图像的归一化直方图统计结果在VGA显示器右侧绘制直方图。最终在VGA液晶显示器上,可以看到左侧图像是原始的图像,右侧图像是经过归一化处理的直方图图像。 5.8.2FPGA设计说明 at7_img_ex15工程源码的层次结构如图5.69所示。 图5.69at7_img_ex15工程源码层次结构 各个模块及功能描述如表5.10所示。 表5.10at7_img_ex15工程模块及功能描述 模 块 名 称 功 能 描 述 clk_wiz_0 该模块是PLL IP核的例化模块,该PLL用于产生系统中所需要的不同频率时钟信号 mig_7series_0 该模块是DDR3控制器IP核的例化模块。FPGA内部逻辑读写访问DDR3都是通过该模块实现,该模块对外直接控制DDR3芯片 image_controller 该模块及其子模块实现MT9V034输出图像的采集控制等。image_capture模块实现图像采集功能 histogram_calculation 该模块对每帧输入的原始图像做256级的直方图统计,并对统计结果进行归一化处理 dual_ram_cache 该模块对直方图统计并归一化后的结果做缓存,写入双口RAM中,同时LCD驱动模块产生的读控制信号可以对双口RAM做读取控制 ddr3_cache 该模块主要用于缓存读或写DDR3的数据,其下例化了两个FIFO。该模块连接FPGA内部逻辑与DDR3 IP核(mig_7series_0模块)之间的数据交互 lcd_driver 该模块驱动VGA显示器,同时产生读取DDR3中图像数据的控制逻辑 led_controller 该模块控制LED闪烁,指示工作状态 工程文件夹at7_img_ex15\at7.srcs\sources_1\new下的histogram_calculation.v模块实现了图像的直方图统计与归一化处理。该模块有一个包含6个状态的状态机,如图5.70所示。 图5.70直方图统计状态机图 以这个状态机为主轴的设计思路如下: (1) 上电初始状态STATE_IDLE,复位结束后即进入下一状态STATE_HIST。 (2) STATE_HIST状态下,进行实时图像的256级直方图统计,统计结果存放在寄存器histogram_cnt[255:0][19:0]中; 图像接收信号i_image_ddr3_frame_end拉高时,切换到下一个状态STATE_FMAX。 (3) STATE_FMAX状态下,遍历一遍直方图统计结果寄存器histogram_cnt[255:0][19:0],找出最大值存放在寄存器max_histogramcnt[19:0]中; 找到最大值后,切换到下一状态STATE_CNTC。 (4) STATE_CNTC状态下,直接转换到下一个状态STATE_OUTP。该状态主要为了清零计数器dlycnt。 (5) STATE_OUTP状态下,依次将256个直方图统计结果乘以448(=256+128+64),作为被除数,实际乘以448是通过3个移位结果(分别对统计结果左移8位、左移7位和左移6位)进行累加实现。而max_histogramcnt[19:0]则作为除数,依次输出256个进行除法归一化后的直方图统计结果(o_image_hc_wren拉高时o_image_hc_wrdb[9:0]有效)。完成后进入下一状态STATE_WAIT。 (6) STATE_WAIT状态下,直接切换到STATE_IDLE。 在第(5)步进行的归一化处理,其基本思想是找到256个直方图统计结果的最大值,作为归一化的1(其他值都小于1); 而其他结果都会以此为标准获取对应的归一化值; 例如最大值若为40000,那么归一化后为1,某个统计结果是1000,那么归一化后是0.025; 而实际我们需要将这个归一化后的直方图结果显示到VGA显示器上,VGA显示器上希望最高的直方图可以取448像素来显示,那么用448乘以归一化后的结果即可。 实际VGA显示器是720p的驱动分辨率,最大可以给到720像素的高度,但是因为左侧的原始采集图像显示是640×480像素,为了显示美观,最好给出一个不超过480像素的最高直方图高度显示,而取448其实是考虑到它等于256+128+64,可以不消耗FPGA的乘法器资源,用移位累加来实现。 5.8.3MATLAB与FPGA协同仿真说明 1. 直方图统计与归一化结果仿真 在at7_img_ex15\at7.srcs\sources_1\new\testbench文件夹下,测试脚本sim_histogram_calculation.v用于对模块histogram_calculation.v进行仿真。 Vivado打开at7_img_ex15工程,在Sources面板中,展开Simulation Sources→sim_1,将sim_histogram_calculation.v文件设置为top module。在Flow Navigator面板中,展开Simulation,单击Run Simulation,在弹出的菜单中单击Run Behavioral Simulation进行仿真。 测试脚本中,读取at7_img_ex15\at7.sim文件夹下的640×480像素图像数据image_in_hex.txt(该文件由at7_img_ex15\matlab文件夹下的image_txt_generation.m产生,原始图像为test.bmp)。一组完整的图像数据经过histogram_calculation.v模块处理后,产生256个归一化直方图结果,写入histogram_result.txt文本中(仿真测试结果位于project\at7_img_ex15\at7.sim\sim_1\behav文件夹下)。 使用at7_img_ex15\matlab文件夹下的draw_histogram_from_FPGA_result.m脚本,可以同时比对MATLAB和FPGA统计的直方图输出结果如图5.71所示。由于FPGA统计结果是一个归一化结果,所以和MATLAB实际统计结果的数值可能不一样,但是从比对图上可以看出,它们的趋势和分布完全一致。 图5.71MATLAB和FPGA统计的直方图比对 2. 图像与直方图显示结果仿真 在at7_img_ex15\at7.srcs\sources_1\new\testbench文件夹下,测试脚本sim_at7.v用于对模块histogram_calculation.v、dual_ram_cache.v和lcd_driver.v进行仿真。 Vivado打开at7_img_ex15工程,在Sources面板中,展开Simulation Sources→sim_1,将sim_zstar.v文件设置为top module。在Flow Navigator面板中,展开Simulation,单击Run Simulation,在弹出的菜单中单击Run Behavioral Simulation进行仿真。 测试脚本中,读取at7_img_ex15\at7.sim文件夹下的640×480像素图像数据image_in_hex.txt(该文件由at7_img_ex15\matlab文件夹下的image_txt_generation.m产生,原始图像为test.bmp)。一组完整的图像数据经过histogram_calculation.v模块处理后,产生256个归一化直方图结果,缓存到dual_ram_cache.v模块的双口RAM中,lcd_driver.v模块根据显示驱动需要读取双口RAM中的数据,将直方图显示在液晶屏的右侧。测试脚本中,根据lcd_driver.v模块的显示驱动信号,将一帧的显示图像写入monitor_display_image.txt文本中(仿真测试结果位于project\at7_img_ex15\at7.sim\sim_1\behav文件夹下)。 使用at7_img_ex15\matlab文件夹下的draw_image_from_FPGA.m脚本,可以打印monitor_display_image.txt文本中输出的图像。如图5.72所示,就是最终的VGA显示器中将会显示的界面示意图,左侧是原始图像,右侧是其直方图分布。可以看到,这个直方图分布情况和前面MATLAB计算出来的结果也是一致的。 图5.72MATLAB对VGA显示器效果的模拟 5.8.4FPGA板级调试 连接好MT9V034摄像头模块、VGA模块和FPGA开发板,同时连接好FPGA的下载器并给板子供电。 使用Vivado 2019.1打开工程at7_img_ex15,将at7_img_ex15\at7.runs\impl_1文件夹下的at7.bit文件烧录到板子上,如图5.73所示,可以看到VGA显示器同时显示左右两个图像,左侧图像为原始图像,右侧图像为直方图。 图5.73直方图实时显示效果图