FPGA纯verilog代码实现sobel 边缘检测,提供2套工程源码和技术支持

Posted 9527华安

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了FPGA纯verilog代码实现sobel 边缘检测,提供2套工程源码和技术支持相关的知识,希望对你有一定的参考价值。

目录

1、前言

边缘检测是一种常用的图像分割技术,常用的边缘检测算子有 Roberts Cross 算子、Prewitt 算子、Sobel
算子和 Kirsch 算子等。Sobel 算子是根据像素点上下左右邻点灰度加权差在边缘处达到极值这一现象来检
测边缘。其对噪声具有平滑作用,提供较为精确的边缘方向信息,当对精度要求不是很高时,是一种较为
常用的边缘检测方法。在本章实验中将进行基于 sobel 算子的边缘检测实验。
本设计与全网其他参考代码相比之优势:
1、 sobel采用纯verilog代码实现,没有用到任何IP,得到代码的同时还得到了verilog代码的fifo模型;
2、通用性达到天花板,由于采用纯verilog代码实现,可在Xilinx、Intle、国产FPGA等各种FPGA上运行;
3、移植性达到天花板,由于没有IP,可在vivado、Quartus、Pango等开发工具之间任意移植;
4、学习性达到天花板,里面涵盖了i2c控制器、图像采集、颜色空间转换、图像卷积、3x3卷积滑窗的获取、DDR3控制器使用、AXI4协议使用、图像缓存、vga时序等等,由一个简单的sobel 边缘检测,直接一步到位的学到了FPGA图像处理领域的高端技术;

2、理论基础

图像分割就是把图像分成若干个特定的、具有独特性质的区域并提出有意义目标的技术和过程。它是由图像处理到图像分析的关键步骤。现有的图像分割方法主要分以下几类:基于阈值的分割方法、基于区域的分割方法、基于边缘的分割方法以及基于特定理论的分割方法等。从数学角度来看,图像分割是将数字图像划分成互不相交的区域的过程。图像分割的过程也是一个标记过程,即把属于同一区域的像素赋予相同的编号。
图像分割的一种重要方法就是边缘检测,即检测图像灰度级或者结构发生突变的像素点,表明一个区域的终结,也是另一个区域开始的地方。这种一副图像中灰度级或结构突变的像素点的集合称为边缘。不同的图像灰度变化不同,在边界处一般有明显的灰度变化,检测图像的灰度变化是一种常用的边缘检测方法。
图像的灰度值具有不连续性,图像局部的灰度突变可以用微分来检测,因为这种突变时间十分短促,因此一阶微分和二阶微分十分适合这种突变的检测。但使用二阶微分对噪声的响应更加强烈,其对图像的质量要求较高。
为了对有意义的边缘点进行分类,与这个点相联系的灰度级的变化必须比在这一点的背景上的变化更有效(灰度变化更剧烈)。由于本次实验用局部计算处理,因此决定一个值是否有效的方法就是设定一个阈值。当一个点的二维一阶导数(图像是一个二维函数)比指定阈值大,就认为这个点是一个边缘点。
一幅图像可以看做是一个二维函数,图像的的一阶导数可以用梯度表示(梯度的本意是一个向量(矢量),表示某一函数在某一点处的方向导数沿着该方向取得最大值。我们用∇f 表示梯度,如下图所示为像素点(x,y)的梯度表达式:

∇f 它指出了 f(图像)在点(x,y)处最大变化率的方向。
向量∇f 的大小(长度)表示为 M(x,y),M(x,y)的表达式如下:

它是梯度向量方向变化率的值。值得注意的是这些求和、平方及开平方都是阵列操作。
梯度的方向可以用图 49.1.3 的公式来表示,它是以 x 轴为度量基准表示的,它与图像的边缘垂直。

本次实验要计算梯度的大小,首先要计算梯度关于 x 和 y 方向的分量 g y 和 g x ,比较常用的计算梯度的分量的方法是使用一些空间域的模板。大家通常把用于计算梯度偏导数的滤波器模板称为梯度算子(也称为差分算子、边缘算子和边缘检测子),例如用于边缘检测的 Sobel 算子、Roberts 算子和 Prewitt 算子等。
如下图就是几种常用的边缘检测模板:

罗伯特交叉梯度算子是最早尝试具有对角优势的模板之一,它是一个 2x2 模板。2x2 的模板在概念上很简单,但它是一个偶数模板,偶数模板对于中心点的确认很不便,因此一般情况下大家使用奇数的模板,最小的奇数模板是 3x3 的模板。这些奇数模板考虑了中心点对端数据的性质,并携带有关于边缘方向的更多信息,能够更好的表达图像的边缘。
Sobel 算子和 Prewitt 算子都是实际应用中常见的算子,但相比用 Prewitt算子,Sobel 算子有更好的抑制噪声性能。下面给出使用 Sobel 算子计算梯度分量的算术表达式:

在这些公式中,3x3 区域的第三行与第一行的差近似为 x 方向的偏导数,第三列与第一列的差近似为 y方向的偏导数。上式用 Verilog 实现可以用下图的计算方法表示:

在上图的算式中,g y1 和 g y3 分别表示第一行和第三列行的值。g x1 和 g x3 分别表示第一行列第三列的值。
在进行边缘检测时首先需要求出每一个像素在 x 和 y 方向的偏导数 g y 和 g x ,再求出像素点的梯度大小M (x,y) ,然后把 M (x,y) 与本次实验设置的阈值进行比较,大于阈值则表示该像素点为边缘,反之,则表示该像素点不是边缘。

3、设计思路和架构

设计思路和架构如下:

4、图像输入

提供2套工程,一个是ov5640输入,分辨率1280x720,另一个是hdmi输入,分辨率1920x1080,hdmi输入采用silicon9011解码为rgb数据,silicon9011需要iic配置才能使用,关于silicon9011的配置和使用,请参考我之前写得文章点击查看:silicon9011的配置和使用

5、RGB转灰度

RGB转灰度,很简单,公式如下所示:

由于 Verilog HDL 无法进行浮点运算,因此使用扩大 256 倍,再向右移 8Bit 的方式,来转换公式,如下所示:

为了防止运算过程中出现负数,对上述公式进行进一步变换,得到如下公式:

RGB转灰度顶层接口如下:

module rgb2ycbcr(
    input               clk       ,   // 模块驱动时钟
    input               rst_n     ,   // 复位信号
    input               i_rgb_vs  ,   // vsync信号
    input               i_rgb_hs  ,   // hsync信号
    input               i_rgb_de  ,   // data enable信号
    input [23:0]        i_rbg_data,
    output              o_YCbCr_vs,   // vsync信号
    output              o_YCbCr_hs,   // hsync信号
    output              o_YCbCr_de,   // data enable信号
    output      [7:0]   o_YCbCr_Y ,   // 输出图像Y数据
    output      [7:0]   o_YCbCr_Cb,   // 输出图像Cb数据
    output      [7:0]   o_YCbCr_Cr	  // 输出图像Cr数据
);

6、3x3卷积滑窗获取

在图像处理过程中,常常需要对图像进行滤波操作,进行滤波操作需要使用算子模板,也可以称之为扫描窗模板,一般有 3x3、5x5、7x7 等模板形式。本文重在阐述算子模板的概念及使用 verilog 来描述其产生过程。下面咱们从图像上来认识算子模板。
如图所示,展示了一个宽度为 15 个像素,高度为 8 个像素的图片,每个白色方块代表一个像素。

实际的图像处理过程中会选定一个模板,假设使用 3x3 的模板,如图所示,灰色代表图像的边界,蓝色框表示 3x3的模板,对整幅图像进行扫窗操作,在图之中标注了第一个和最后一个模板扫描的操作位置。

我们从图中可以观察到边界一行的像素是无法进行运算的,这个也是扫窗的一个缺点,无法对边界进行处理,而且随着算子的变大边界无法处理的像素也会变多。后面进行具体操作时,会提出一种补充处理的方法。如图所示,对浅灰色的边界进行扩展,就是图中的深灰色部分内容,一般简单处理,有复制边界为零的,也可以将原来的边界进行复制。

前面已经分析了算子模板的原理,下面来介绍一下如何使用 verilog 来描述。我们使用 FIFO 来缓存图像的行数据,如果取 3x3 的模板,那么至少需要两个深度大于图像行数据的长度的 FIFO,通过首尾相连的形式来缓存两行数据,等第三行数据到来之时,同时输出三行数据。注意,最后的 FIFO 输出数据是最早的数据,也就是第一行的数据,如图所示:

3x3卷积滑窗获取顶层接口如下:

module image_3x3 #(
	parameter  H_ACTIVE = 1920,
	parameter  V_ACTIVE = 1080
)(
	input   wire		i_clk    ,
	input   wire		i_rst_n  ,
	input	wire		i_en     ,
	input	wire [7:0]	i_data   ,
	output	reg 		o_en     ,
	output  reg [7:0]	o_temp_11,
	output  reg [7:0]	o_temp_12,
	output  reg [7:0]	o_temp_13,	
	output  reg [7:0]	o_temp_21,
	output  reg [7:0]	o_temp_22,
	output  reg [7:0]	o_temp_23,		
	output  reg [7:0]	o_temp_31,
	output  reg [7:0]	o_temp_32,
	output  reg [7:0]	o_temp_33
);

7、Sobel卷积运算

所谓边缘是指其周围像素灰度急剧变化的那些象素的集合,它是图像最基本的特征。边缘存在于目标、背景和区域之间,所以,它是图像分割所依赖的最重要的依据。由于边缘是位置的标志,对灰度的变化不敏感,因此,边缘也是图像匹配的重要的特征。sobel 算子利用像素点上下左右四个方向像素权重算法,根据在边缘点处达到极值这一现象进行边缘检测。Sobel 算子对噪声具有平滑作用,提供较为精确的边缘方向信息,是一种较为常见的边缘检测方法。
Soble 边缘检测通常带有方向性,可以只检测竖直边缘或垂直边缘或都检测。
所以我们先定义两个梯度方向的系数:

然后我们来计算梯度图像,我们知道边缘点其实就是图像中灰度跳变剧烈的点,所以先计算梯度图像,然后将梯度图像中较亮的那一部分提取出来就是简单的边缘部分。 Sobel 算子是 prewitt 算子的改进形式,改进之处在于 sobel算子认为,邻域的像素对当前像素产生的影响不是等价的,所以距离不同的像素具有不同的权值,对算子结果产生的影响也不同。一般来说,距离越远,产生的影响越小。正因为 Sobel 算子对于像素的位置的影响做了加权,与 Prewitt
算子、Roberts 算子相比因此效果更好。相比较 Prewitt 算子,Sobel 模板能够较好的抑制(平滑)噪声。 sobel 要比prewitt 更能准确检测图像边缘。
Sobel 算子使用 3*3 的模板如下:

之后将 x,y 方向的导数叠加。
Sobel卷积运算顶层接口如下:

module helai_sobel #(
	parameter  H_ACTIVE = 1920,
	parameter  V_ACTIVE = 1080
)(
	input   wire		i_clk    ,
	input   wire		i_rst_n  ,
	input	wire		i_hsyn   ,
	input	wire		i_vsyn   ,
	input	wire		i_en     ,
	input	wire [7:0]	i_Y      ,
	input	wire [7:0] 	threshold,	
	output	wire 		o_hs     ,
	output	wire 		o_vs     ,
	output	wire 		o_en     ,	
	output  wire [7:0]	o_Y      
);

8、FDMA图像缓存

FDMA图像缓实现图像到DDR3的三帧缓存,关于FDMA三帧缓存,请参考我之前写的文章点击查看:FDMA三帧缓存方案
这里,我们将FDMA做了修改,使得输入分辨率由原来的参数类型变为输入类型,这样的好处时方便配置;

9、图像输出

生成1920x1080@60Hz分辨率的VGA时序,给到芯片silicon9134做HDMI编码,silicon9134需要iic配置才能使用,关于silicon9134的配置和使用,请参考我之前写得文章点击查看:silicon9134的配置和使用

10、工程1详解:ov5640输入

开发板:Xilinx Artix7开发板;
开发环境:vivado2019.1;
输入:ov5640摄像头,1280x720;
输出:HDMI,1920x1080;
工程BD如下:

工程代码架构如下:

VIO可调节sobel阈值,默认为40,阈值不同边沿检测效果也不同;VIO调节如下:

顶层源码如下:

module top(  
//ddr3  
  output [14:0]DDR3_0_addr,
  output [2:0]DDR3_0_ba   ,
  output DDR3_0_cas_n     ,
  output [0:0]DDR3_0_ck_n ,
  output [0:0]DDR3_0_ck_p ,
  output [0:0]DDR3_0_cke  ,
  output [0:0]DDR3_0_cs_n ,
  output [3:0]DDR3_0_dm   ,
  inout [31:0]DDR3_0_dq   ,
  inout [3:0]DDR3_0_dqs_n ,
  inout [3:0]DDR3_0_dqs_p ,
  output [0:0]DDR3_0_odt  ,
  output DDR3_0_ras_n     ,
  output DDR3_0_reset_n   ,
  output DDR3_0_we_n      , 
  
  input        CLK_IN1_D_0_clk_n,
  input        CLK_IN1_D_0_clk_p,
  output       ddr3_ok          ,  

  inout        cmos_scl,          //cmos i2c clock
  inout        cmos_sda,          //cmos i2c data
  input        cmos_vsync,        //cmos vsync
  input        cmos_href,         //cmos hsync refrence,data valid
  input        cmos_pclk,         //cmos pxiel clock
  output       cmos_xclk,         //cmos externl clock
  input   [7:0]cmos_db,           //cmos data
  output       cmos_rst_n,        //cmos reset
  output       cmos_pwdn,         //cmos power down
  inout        hdmi_scl         ,           //HDMI I2C clock
  inout        hdmi_sda         ,           //HDMI I2C data
//hdmi_out  
  output       vout_hs          ,            //horizontal synchronization for 9134
  output       vout_vs          ,            //vertical synchronization for 9134
  output       vout_de          ,            //data valid for 9134
  output       vout_clk         ,           //clock for 9134
  output[23:0] vout_data        ,            //data for 9134
  output       hdmi_nreset      
);

wire        clk_25m   ;
wire	    clk_200m  ;
wire        clk_hdmi  ;
wire        pll_resetn;
wire [0:0]  resetn;
wire        ud_r_0_ud_rclk;
wire [31:0] ud_r_0_ud_rdata;
wire        ud_r_0_ud_rde;
wire        ud_r_0_ud_rvs;
wire        ud_w_0_ud_wclk;
wire [31:0] ud_w_0_ud_wdata;
wire        ud_w_0_ud_wde;
wire        ud_w_0_ud_wvs;
wire        ui_clk_100m;

wire [9:0]   lut_index;
wire [31:0]  lut_data; 
wire [9:0]   lut_index_hdmi;
wire [31:0]  lut_data_hdmi ; 

wire [23:0] ov5640_rgb;
wire 	    ov5640_de ;
wire 	    ov5640_vs ;
wire        o_data_req;

wire       YCbCr_vs;
wire       YCbCr_hs;
wire       YCbCr_de;
wire [7:0] YCbCr_Y ;
wire       sobel_vs;
wire       sobel_de;
wire [7:0] sobel_oy;
wire [7:0] threshold;

wire [23:0] i_rgb;  
wire 	    o_hs ;  
wire 	    o_vs ;  
wire 	    o_de ;  
wire [23:0] o_rgb;  
wire hdmi_clk_rstn;
assign hdmi_nreset   =pll_resetn;  
//assign hdmi_in_nreset=pll_resetn;
assign ud_w_0_ud_wclk =cmos_pclk ;
assign ud_w_0_ud_wvs  =sobel_vs;
assign ud_w_0_ud_wde  =sobel_de;
assign ud_w_0_ud_wdata=sobel_oy,sobel_oy,sobel_oy;
assign ud_r_0_ud_rclk=clk_hdmi;
assign ud_r_0_ud_rvs=o_vs;
assign ud_r_0_ud_rde=o_data_req;
assign i_rgb=ud_r_0_ud_rdata[23:0];
assign vout_clk=clk_hdmi;
assign vout_hs=o_hs;
assign vout_vs=o_vs;
assign vout_de=o_de;
assign vout_data=o_rgb;
assign cmos_rst_n = 1'b1;
assign cmos_pwdn  = 1'b0;

i2c_config i2c_config_ov5640(
.rst            (~pll_resetn    ),
.clk            (clk_200m       ),
.clk_div_cnt    (16'd500        ),
.i2c_addr_2byte (1'b1           ),
.lut_index      (lut_index      ),
.lut_dev_addr   (lut_data[31:24]),
.lut_reg_addr   (lut_data[23:8] ),
.lut_reg_data   (lut_data[7:0]  ),
.error          (               ),
.done           (               ),
.i2c_scl        (cmos_scl       ),
.i2c_sda        (cmos_sda       )
);

ov5640_reg_cfg #(
	.DISPAY_H(1280),
	.DISPAY_V(720 )
)
u_ov5640_reg_cfg(
	.lut_index(lut_index),   //Look-up table address
	.lut_data (lut_data )    //Device address (8bit I2C address), register address, register data
);

i2c_config i2c_config_hdmi(
	.rst            (~pll_resetn    ),
	.clk            (clk_200m       ),
	.clk_div_cnt    (16'd500        ),
	.i2c_addr_2byte (1'b0           ),
	.lut_index      (lut_index_hdmi      ),
	.lut_dev_addr   (lut_data_hdmi[31:24]),
	.lut_reg_addr   (lut_data_hdmi[23:8] ),
	.lut_reg_data   (lut_data_hdmi[7:0]  ),
	.error          (               ),
	.done           (               ),
	.i2c_scl        (hdmi_scl       ),
	.i2c_sda        (hdmi_sda       )
);

lut_hdmi u_lut_hdmi(
	.lut_index(lut_index_hdmi),   //Look-up table address
	.lut_data (lut_data_hdmi)    //Device address (8bit I2C address), register address, register data
);

uiSensorRGB565 u_uiSensorRGB565(
	.cmos_clk_i  (clk_25m),//cmos senseor clock.
	.rst_n_i     (resetn ),//system reset.active low.
	.cmos_pclk_i (cmos_pclk),//input pixel clock.
	.cmos_href_i (cmos_href),//input pixel hs signal.
	.cmos_vsync_i(cmos_vsync),//input pixel vs signal.
	.cmos_data_i (cmos_db),//data.
	.cmos_xclk_o (cmos_xclk),//output clock to cmos sensor.
    .rgb_o       (ov5640_rgb),
    .de_o        (ov5640_de ),
    .vs_o        (ov5640_vs ),
    .hs_o        ()
    );

rgb2ycbcr u_rgb2ycbcr(
    .clk       (cmos_pclk  ),   // 模块驱动时钟
    .rst_n     (resetn     ),   // 复位信号
    .i_rgb_vs  (cmos_vsync ),   // vsync信号
    .i_rgb_hs  (),   // hsync信号
    .i_rgb_de  (ov5640_de  ),   // data enable信号
    .i_rbg_data(ov5640_rgb ),
    .o_YCbCr_vs(YCbCr_vs   ),   // vsync信号
    .o_YCbCr_hs(),   // hsync信号
    .o_YCbCr_de(YCbCr_de   ),   // data enable信号
    .o_YCbCr_Y (YCbCr_Y    ),   // 输出图像Y数据
    .o_YCbCr_Cb(),   // 输出图像Cb数据
    .o_YCbCr_Cr()	  // 输出图像Cr数据
);

vio_0 u_vio_0 (
  .clk(vin_clk),                // input wire clk
  .probe_out0(threshold)  // output wire [7 : 0] probe_out0
);

helai_sobel #(
	.H_ACTIVE (1280),
	.V_ACTIVE (720)
)u_helai_sobel(
	.i_clk    (cmos_pclk  ),
	.i_rst_n  (resetn     ),
	.i_hsyn   (),
	.i_vsyn   (YCbCr_vs   ),
	.i_en     (YCbCr_de   ),
	.i_Y      (YCbCr_Y    ),
	.threshold(threshold),	
	.o_hs     (),
	.o_vs     (sobel_vs),
	.o_en     (sobel_de),	
	.o_Y      (sobel_oy)
);

design_1_wrapper u_design_1_wrapper
   (
    .CLK_IN1_D_0_clk_n(CLK_IN1_D_0_clk_n),
    .CLK_IN1_D_0_clk_p(CLK_IN1_D_0_clk_p),
    .DDR3_0_addr      (DDR3_0_addr      ),
    .DDR3_0_ba        (DDR3_0_ba        ),
    .DDR3_0_cas_n     (DDR3_0_cas_n     ),
    .DDR3_0_ck_n      (DDR3_0_ck_n      ),
    .DDR3_0_ck_p      (DDR3_0_ck_p      ),
    .DDR3_0_cke       (DDR3_0_cke       ),
    .DDR3_0_cs_n      (DDR3_0_cs_n      ),
    .DDR3_0_dm        (DDR3_0_dm        ),
    .DDR3_0_dq        (DDR3_0_dq        ),
    .DDR3_0_dqs_n     (DDR3_0_dqs_n     ),
    .DDR3_0_dqs_p     (DDR3_0_dqs_p     ),
    .DDR3_0_odt       (DDR3_0_odt       ),
    .DDR3_0_ras_n     (DDR3_0_ras_n     ),
    .DDR3_0_reset_n   (DDR3_0_reset_n   ),
    .DDR3_0_we_n      (DDR3_0_we_n      ),
    .clk_200m         (clk_200m         ),
	.clk_hdmi         (clk_hdmi         ),
    .ddr3_ok          (ddr3_ok          ),
    .pll_resetn       (pll_resetn       ),
    .resetn           (resetn           ),
    .ud_r_0_ud_rclk   (ud_r_0_ud_rclk   ),
    .ud_r_0_ud_rdata  (ud_r_0_ud_rdata  ),
    .ud_r_0_ud_rde    (ud_r_0_ud_rde    ),
    .ud_r_0_ud_rempty (ud_r_0_ud_rempty ),
    .ud_r_0_ud_rvs    (ud_r_0_ud_rvs    ),
    .ud_w_0_ud_wclk   (ud_w_0_ud_wclk   ),
    .ud_w_0_ud_wdata  (ud_w_0_ud_wdata  ),
    .ud_w_0_ud_wde    (ud_w_0_ud_wde    ),
    .ud_w_0_ud_wfull  (ud_w_0_ud_wfull  ),
    .ud_w_0_ud_wvs    (ud_w_0_ud_wvs    ),
    .ui_clk_100m      (ui_clk_100m      ),
	.clk_25m          (clk_25m          ),
	.FDMA_XSIZE_0     (1280             ),
	.FDMA_YSIZE_0     (720              )
	);	

video_timing_control vga(
	.i_clk     (clk_hdmi   ),	
	.i_rst_n   (pll_resetn ), 
	.i_start_x (0),
	.i_start_y (0),
	.i_disp_h  (1280),
	.i_disp_v  (720 ),	
	.i_rgb     (i_rgb      ),
	.o_hs      (o_hs       ),
	.o_vs      (o_vs       ),
	.o_de      (o_de       ),
	.o_rgb     (o_rgb      ),
	.o_data_req(o_data_req )
);
endmodule
<

以上是关于FPGA纯verilog代码实现sobel 边缘检测,提供2套工程源码和技术支持的主要内容,如果未能解决你的问题,请参考以下文章

基于FPGA的Sobel边缘检测的实现

FPGA/数字IC手撕代码3——通过纯verilog实现简单的ROM

FPGA纯verilog代码实现4路视频缩放拼接 提供工程源码和技术支持

三帧差,边缘检测,FPGA基于FPGA的三帧差+边缘检测的Verilog实现

FPGA纯verilog代码实现图像缩放,两种插值算法任意尺寸缩放,提供3套工程源码

Sobel边缘检测-FPGA