FPGA-图像处理系列 基于Sobel算法的边缘检测设计与实现

学习内容

以FIFO实现3行数据求和为基础,学习图像处理方面的基于Sobel算法的边缘检测,边缘检测在计算机视觉、图像分析和图像处理等应用中起着重要作用。

实现功能

输入图片,将图片中内容的边缘进行提取显示。

开发环境

开发板:特权同学xilinx spartan6开发板
开发软件:ISE14.7、modelsim10.5
编程语言:verilog

一、理论知识学习

1.边缘检测

边缘是图像的基本特征,包含了用于图像识别的有用信息,在计算机视觉、图像分析和图像处理等应用中起着重要作用。边缘检测,针对的是灰度图像,顾名思义,检测图像的边缘,是针对图像像素点的一种计算,目的是标识数字图像中灰度变化明显的点,图像的边缘检测,在保留了图像的重要结构信息的同时,剔除了可以认为不相关的信息,大幅度减少了数据量,便于图像的传输和处理。
边缘检查的方法大致可以分为两类:基于查找的一类,通过寻找图像一阶导数中最大值和最小值来检测边界,包括 Sobel 算法、Roberts Cross 算法等;基于零穿越的一类,通过寻找图像二阶导数零穿越来寻找边界,包括 Canny 算法,Laplacian 算法等。
本次学习主要专注于Sobel这种方法。

2.Sobel算法简介

Sobel检测算法比较简单,虽然准确度比较低但是在实际应用中效率较高,在一些对纹理要求不严格的场景下,这一算法是我们的首选。
Sobel算法的核心是Sobel算子,该算子包含两组3×3的矩阵,如下图所示

FPGA-图像处理系列 基于Sobel算法的边缘检测设计与实现_第1张图片
对于图像而言,取 3 行 3 列的图像数据,将图像数据与对应位置的算子的值相乘再相加,得到 x 方向的 Gx,和 y 方向的 Gy,将得到的 Gx 和 Gy,平方后相加,再取算术平方根,得到 Gxy,近似值为 Gx 和 Gy 绝对值之和,将计算得到的 Gxy 与我们设定的阈值相比较,Gxy 如果大于阈值,表示该点为边界点,此点显示黑点,否则显示白点。具体见下图。
FPGA-图像处理系列 基于Sobel算法的边缘检测设计与实现_第2张图片

Gxy计算公式

二、实战演练

要实现Sobel算法首先要利用FIFO3行数据求和,其次掌握上述的边缘检测和算法的具体理论,结合这两个部分来实现算法就比较简单了。我们把整个过程分为下面四个步骤:

  1. 通过GxGy的计算公式结合FIFO求和算法求取GxGy的值。
  2. 求得GxGy的绝对值。
  3. GxGy带入Gxy计算公式,求得Gxy的值。
  4. 将求得的Gxy与设定的阈值相比较,当Gxy大于等于阈值,赋值rgb为黑色,否则赋值rgb为白色。

需要注意的是,图片经过Sobel算法之后,输出的图片相比输入的图片会少2行2列数据,这是因为使用FIFO求和算法时这一算法只有当第2行或第2列数据输入时才开始执行。所以会丢失。
FPGA-图像处理系列 基于Sobel算法的边缘检测设计与实现_第3张图片

1.实战要求
  1. VGA显示模式:640×480@60
  2. 输出图片分辨率大小“100×100
2.图片预处理

在Sobel算法之前我们首先要把彩色图片转化为灰度图,之后将灰度图的高三位取出来存放到txt中,这一个预处理选择用matlab来实现,相关.m文件代码如下

clc;                            %清理命令行窗口
clear all;                      %清理工作区
image = imread('cat.png');     %使用imread函数读取图片数据
figure;
imshow(image);                  %窗口显示图片
R = image(:,:,2);               %提取图片中的红色层生成灰度图像
figure;
imshow(R);                      %窗口显示灰色图像
[ROW,COL] = size(R);            %灰色图像大小参数
data = zeros(1,ROW*COL);        %定义一个初值为0的数组,存储转换后的图片数据
for r = 1:ROW
    for c = 1 : COL
        data((r-1)*COL+c) = bitshift(R(r,c),-5);    %红色层数据右移5位
    end
end
fid = fopen('cat.txt','w+');                       %打开或新建一个txt文件
for i = 1:ROW*COL;
    fprintf(fid,'%02x ',data(i));                   %写入图片数据
end
fclose(fid);

(首先打开一个文件夹的工作区,在文件夹中放好图片,并且命名与代码相对应,之后运行即可产生一个我们所需的txt文件)

3.实验工程

Sobel算法进行边缘检测的整个实验工程如下图所示,其中clk_gen为PLL IP核产生25MHz和50MHz的时钟,rxtx为uart串口通信部分,VGA为显示部分,sobel_ctrl为整个工程的核心部分包含了Sobel算法的具体实现。
FPGA-图像处理系列 基于Sobel算法的边缘检测设计与实现_第4张图片
FPGA-图像处理系列 基于Sobel算法的边缘检测设计与实现_第5张图片

具体工作流程:

  1. 开发板系统时钟为25Mhz,系统上电后,时钟信号传入PLL分频产生25Mhz和50Mhz的时钟,其中50Mhz的时钟用于串口收发、Sobel求和、RAM写的工作时钟;25MHz时钟用于图像生成模块和VGA时序控制模块的工作时钟。
  2. PC 机将图片数据通过串口 RS232 传输给 FPGA,数据在 uart_rx 模块中完成拼接传给 sobel_ctrl 模块进行 Sobel 运算,输出结果同时传给 vga 模块和 uart_tx 模块;vga 模块将接收到的经 sobel 算法处理后的图像数据存入图像数据生成模块中的RAM 中;uart_tx 模块将输出结果回传给 PC 机验证数据完整性。
  3. 图像数据生成模块以 VGA 时序控制模块传入的像素点坐标(pix_x,pix_y)为约束条件,生成待显示图像的色彩信息(pix_data),图像色彩信息包括彩条背景和经 sobel算法处理后的图片信息。
  4. 图像数据生成模块生成的图像色彩信息传入VGA时序控制模块,在模块内部使用使能信号滤除掉非图像显示有效区域的图像数据,产生RGB色彩信息(rgb),在行、场同步信号(hsync、vsync)的同步作用下,将RGB色彩信息扫描显示到VGA显示器,显示出经Sobel算法处理过的图像。

整体的工程已经做了一个简单的介绍,其中的串口收发模块、VGA驱动模块、PLL IP核等均为子模块直接按照需求调用即可。本文主要介绍顶层模块和Sobel求和模块

4.数据求和模块

FPGA-图像处理系列 基于Sobel算法的边缘检测设计与实现_第6张图片

信号 位宽 类型 功能描述
sys_clk 1Bit Input 工作时钟,频率
sys_rst_n 1Bit Input 复位信号,低电平有效
pi_flag 1Bit Input 输入数据标志信号
pi_data 8Bit Input 输入拼接后的图像数据
po_flag 1Bit Output 输出数据标志信号
po_data 8Bit Output 输出经sobel算法处理后的图像数据

这个模块的设计主要包含两个部分:

  1. 图片数据自输入模块到数据写入fifo
  2. 数据从FIFO读出经过Sobel运算进行输出

第一个部分(参考FIFO三行求和的工程)
本功能模块的作用是对输入的图片进行 sobel 算法处理并输出处理后的数据,由前文可知,要实现 sobel 算法的求解,要使用 sobel 算子求出 Gx、Gy,进而求出 Gxy,求解后的 Gxy 与设定阈值比较,确定图像边界,完成 sobel 算法处理。
Gx、Gy 的求解分别是对图形 3 行、3 列图形数据的处理,我们可以参考上一章节的FIFO 求和实验对 3 行数据的处理方式,实现 Gx、Gy 数据的求解。
与 FIFO 求和实验类似,sobel_ctrl 模块内部同样调用两个 FIFO 用作数据缓存。使用同样的方式将串口接收模块传入的图片数据按要求暂存到两个 FIFO 中。
两 FIFO 的时钟信号为系统时钟 sys_clk 与串口接收模块时钟相同;我们需要在模块内部声明 FIFO 写使能信号,声明 fifo1 写使能信号为 wr_en1,数据输入信号为 data_in1,声明 fifo2 写使能信号为 wr_en2,数据输入信号为 data_in2;声明两 FIFO 共用读使能信号rd_en。
wr_en1:当第 0 行数据输入,wr_en1 写使能信号由数据标志信号 pi_flag 赋值,滞后
pi_flag 信号 1 个时钟周期,第 1 行数据输入时,wr_en1 写使能信号保持无效,自第 2 行数据输入到数据输入结束,wr_en1 写使能信号由数据标志信号 dout_flag 赋值,滞后dout_flag 信号 1 个时钟周期;
data_in1:当第 0 行数据输入且写使能有效时, 将第 0 行数据写入 fifo1:当第 2-98 行数据写入且写使能有效时,将 fifo2 读出的 1-97 行数据写入 fifo1;
wr_en2:当第 1-98 行数据输入时,wr_en2 写使能信号由数据标志信号 pi_flag 赋值,滞后 pi_flag 信号 1 个时钟周期,其他时刻写使能信号 wr_en2 均无效;
data_in2:当 fifo2 的写使能信号 wr_en2 有效时,将传入的 pi_data 赋值给 data_in2,数据写入 fifo2,写使能无效时,data_in2 保持原有状态;
rd_en:fifo1 和 fifo2 共用读使能信号,该使能信号在第 0 行和第 1 行数据输入是始终保持无效状态,自第 2 行数据开始输入到数据输入完成,读使能信号 rd_en 由 pi_flag 赋值,滞后 pi_flag 信号 1 个时钟周期;
dout_flag 信号只有在 wr_en2 信号和 rd_en 信号均有效时才有效,其他时刻均无效,目的是赋值给在第 2 行数据输入后的 wr_en1 使能信号。
时序图如下
FPGA-图像处理系列 基于Sobel算法的边缘检测设计与实现_第7张图片
第二个部分

实现 sobel 算法就要求出 Gxy,Gxy 由 Gx、Gy 运算得到,Gx、Gy 由 sobel
算子与图像数据运算得到。参与运算的图像数据要包含图像 3 行 3 列的像素信息。
这就表示只有在图像的第 2 行的第 2 个数据传入模块时,才能开始 Gx、Gy 的运算。
要准确定位运算开始的时刻,我们需要声明计数器,用来计数读出数据个数,判断 Gx、Gy 的运算时刻,之后根据公式来进行运算,具体逻辑不在此处赘述,对应代码已经附在下面,读者可以自行理解设计。

module  sobel_ctrl
(
    input   wire            sys_clk     ,   //输入系统时钟,频率50MHz
    input   wire            sys_rst_n   ,   //复位信号,低有效
    input   wire    [7:0]   pi_data     ,   //rx传入的数据信号
    input   wire            pi_flag     ,   //rx传入的标志信号

    output  reg     [7:0]   po_data     ,   //fifo加法运算后的信号
    output  reg             po_flag         //输出标志信号
);

//********************************************************************//
//****************** Parameter and Internal Signal *******************//
//********************************************************************//
//parameter define
parameter   LENGTH_P    =   10'd100         ,   //图片长度
            WIDE_P      =   10'd100         ;   //图片宽度
parameter   THRESHOLD   =   8'b000_011_00   ;   //比较阈值
parameter   BLACK       =   8'b0000_0000    ,   //黑色
            WHITE       =   8'b1111_1111    ;   //白色

//wire  define
wire    [7:0]   data_out1   ;   //fifo1数据输出
wire    [7:0]   data_out2   ;   //fifo2数据输出

//reg   define
reg     [7:0]   cnt_h       ;   //行计数
reg     [7:0]   cnt_v       ;   //场计数
reg     [7:0]   pi_data_dly ;   //pi_data数据寄存
reg             wr_en1      ;   //fifo1写使能
reg             wr_en2      ;   //fifo2写使能
reg     [7:0]   data_in1    ;   //fifo1写数据
reg     [7:0]   data_in2    ;   //fifo2写数据
reg             rd_en       ;   //fifo1,fifo2共用读使能
reg     [7:0]   data_out1_dly   ;   //fifo1数据输出寄存
reg     [7:0]   data_out2_dly   ;   //fifo2数据输出寄存
reg             dout_flag   ;   //使能信号
reg             rd_en_dly1  ;   //输出数据标志信号,延后rd_en一拍
reg             rd_en_dly2  ;   //a,b,c赋值标志信号
reg             gx_gy_flag  ;   //gx,gy计算标志信号
reg             gxy_flag    ;   //gxy计算标志信号
reg             compare_flag;   //阈值比较标志信号
reg     [7:0]   cnt_rd      ;   //读出数据计数器
reg     [7:0]   a1          ;
reg     [7:0]   a2          ;
reg     [7:0]   a3          ;
reg     [7:0]   b1          ;
reg     [7:0]   b2          ;
reg     [7:0]   b3          ;
reg     [7:0]   c1          ;
reg     [7:0]   c2          ;
reg     [7:0]   c3          ;   //图像数据
reg     [8:0]   gx          ;
reg     [8:0]   gy          ;   //gx,gy
reg     [7:0]   gxy         ;   //gxy

//********************************************************************//
//***************************** Main Code ****************************//
//********************************************************************//
//cnt_h:行数据个数计数器
always@(posedge sys_clk or  negedge sys_rst_n)
    if(sys_rst_n == 1'b0)
        cnt_h   <=  8'd0;
    else    if((cnt_h == (LENGTH_P - 1'b1)) && (pi_flag == 1'b1))
        cnt_h   <=  8'd0;
    else    if(pi_flag == 1'b1)
        cnt_h   <=  cnt_h + 1'b1;

//cnt_v:场计数器
always@(posedge sys_clk or  negedge sys_rst_n)
    if(sys_rst_n == 1'b0)
        cnt_v   <=  8'd0;
    else    if((cnt_v == (WIDE_P - 1'b1)) && (pi_flag == 1'b1)
            && (cnt_h == (LENGTH_P - 1'b1)))
        cnt_v   <=  8'd0;
    else    if((cnt_h == (LENGTH_P - 1'b1)) && (pi_flag == 1'b1))
        cnt_v   <=  cnt_v + 1'b1;

//cnt_rd:fifo数据读出个数计数,用来判断何时对gx,gy进行运算
always@(posedge sys_clk or  negedge sys_rst_n)
    if(sys_rst_n == 1'b0)
        cnt_rd   <=  8'd0;
    else    if((cnt_rd == (LENGTH_P - 1'b1)) && (rd_en == 1'b1))
        cnt_rd   <=  8'd0;
    else    if(rd_en == 1'b1)
        cnt_rd   <=  cnt_rd + 1'b1;

//wr_en1:fifo1写使能,高电平有效
always@(posedge sys_clk or  negedge sys_rst_n)
    if(sys_rst_n == 1'b0)
        wr_en1  <=  1'b0;
    else    if((cnt_v == 8'd0) && (pi_flag == 1'b1))
        wr_en1  <=  1'b1;      //0行写入fifo1
    else
        wr_en1  <=  dout_flag;  //2-198行写入fifo1

//wr_en2,fifo2的写使能,高电平有效
always@(posedge sys_clk or  negedge sys_rst_n)
    if(sys_rst_n == 1'b0)
        wr_en2  <=  1'b0;
    else    if((cnt_v >= 8'd1)&&(cnt_v <= ((WIDE_P - 1'b1) - 1'b1))
            && (pi_flag == 1'b1))
        wr_en2  <=  1'b1;      //2-199行写入fifo2
    else
        wr_en2  <=  1'b0;

//data_in1:fifo1的数据写入
always@(posedge sys_clk or  negedge sys_rst_n)
    if(sys_rst_n == 1'b0)
        data_in1    <=  8'b0;
    else    if((pi_flag == 1'b1) && (cnt_v == 8'b0))
        data_in1    <=  pi_data;
    else    if(dout_flag == 1'b1)
        data_in1    <=  data_out2;
    else
        data_in1    <=  data_in1;

//data_in2:fifo2的数据写入
always@(posedge sys_clk or  negedge sys_rst_n)
    if(sys_rst_n == 1'b0)
        data_in2    <=  8'b0;
    else    if((pi_flag == 1'b1) && (cnt_v >= 8'd1)
            && (cnt_v <= ((WIDE_P - 1'b1) - 1'b1)))
        data_in2    <=  pi_data;
    else
        data_in2    <=  data_in2;

//rd_en:fifo1和fifo2的共用读使能,高电平有效
always@(posedge sys_clk or  negedge sys_rst_n)
    if(sys_rst_n == 1'b0)
        rd_en   <=  1'b0;
    else    if((pi_flag == 1'b1) && (cnt_v >= 8'd2)
            && (cnt_v <= (WIDE_P - 1'b1)))
        rd_en   <=  1'b1;  
    else
        rd_en   <=  1'b0;


//dout_flag:控制fifo1写使能wr_en1
always@(posedge sys_clk or  negedge sys_rst_n)
    if(sys_rst_n == 1'b0)
        dout_flag   <=  1'b0;
    else    if((wr_en2 == 1'b1) && (rd_en == 1'b1))
        dout_flag   <=  1'b1;
    else
        dout_flag   <=  1'b0;

//rd_en_dly1:输出数据标志信号
always@(posedge sys_clk or  negedge sys_rst_n)
    if(sys_rst_n == 1'b0)
        rd_en_dly1  <=  1'b0;
    else    if(rd_en == 1'b1)
        rd_en_dly1  <=  1'b1;
    else
        rd_en_dly1  <=  1'b0;

//data_out1_dly:data_out1数据寄存
always@(posedge sys_clk or  negedge sys_rst_n)
    if(sys_rst_n == 1'b0)
        data_out1_dly   <=  8'b0;
    else    if(rd_en_dly1 == 1'b1)
        data_out1_dly   <=  data_out1;

//data_out2_dly:data_out2数据寄存
always@(posedge sys_clk or  negedge sys_rst_n)
    if(sys_rst_n == 1'b0)
        data_out2_dly   <=  8'b0;
    else    if(rd_en_dly1 == 1'b1)
        data_out2_dly   <=  data_out2;

//pi_data_dly:输入数据pi_data寄存
always@(posedge sys_clk or  negedge sys_rst_n)
    if(sys_rst_n == 1'b0)
        pi_data_dly <=  8'b0;
    else    if(rd_en_dly1 == 1'b1)
        pi_data_dly <=  pi_data;

//rd_en_dly2:a,b,c赋值标志信号
always@(posedge sys_clk or  negedge sys_rst_n)
    if(sys_rst_n == 1'b0)
        rd_en_dly2  <=  1'b0;
    else    if(rd_en_dly1 == 1'b1)
        rd_en_dly2  <=  1'b1;
    else
        rd_en_dly2  <=  1'b0;

//gx_gy_flag:gx,gy计算标志信号
always@(posedge sys_clk or  negedge sys_rst_n)
    if(sys_rst_n == 1'b0)
        gx_gy_flag  <=  1'b0;
    else    if((rd_en_dly2 == 1'b1) && ((cnt_rd >= 8'd3) || (cnt_rd == 8'd0)))
        gx_gy_flag  <=  1'b1;
    else
        gx_gy_flag  <=  1'b0;

//gxy_flag:gxy计算标准信号
always@(posedge sys_clk or  negedge sys_rst_n)
    if(sys_rst_n == 1'b0)
        gxy_flag    <=  1'b0;
    else    if(gx_gy_flag == 1'b1)
        gxy_flag    <=  1'b1;
    else
        gxy_flag    <=  1'b0;

//compare_flag,阈值比较标志信号
always@(posedge sys_clk or  negedge sys_rst_n)
    if(sys_rst_n == 1'b0)
        compare_flag    <=  1'b0;
    else    if(gxy_flag == 1'b1)
        compare_flag    <=  1'b1;
    else
        compare_flag    <=  1'b0;

//a,b,c赋值
always@(posedge sys_clk or  negedge sys_rst_n)
    if(sys_rst_n == 1'b0)
    begin
        a1  <=  8'd0;
        a2  <=  8'd0;
        a3  <=  8'd0;
        b1  <=  8'd0;
        b2  <=  8'd0;
        b3  <=  8'd0;
        c1  <=  8'd0;
        c2  <=  8'd0;
        c3  <=  8'd0;
    end
    else    if(rd_en_dly2==1)
    begin
        a1  <=  data_out1_dly;
        b1  <=  data_out2_dly;
        c1  <=  pi_data_dly;
        a2  <=  a1;
        b2  <=  b1;
        c2  <=  c1;
        a3  <=  a2;
        b3  <=  b2;
        c3  <=  c2;
    end

//gx:计算gx
always@(posedge sys_clk or  negedge sys_rst_n)
    if(sys_rst_n == 1'b0)
        gx  <=  9'd0;
    else    if(gx_gy_flag == 1'b1)
        gx  <=  a3 - a1 + ((b3 - b1) << 1) + c3 - c1;
    else
        gx  <=  gx;

//gy:计算gy
always@(posedge sys_clk or  negedge sys_rst_n)
    if(sys_rst_n == 1'b0)
        gy  <=  9'd0;
    else    if(gx_gy_flag == 1'b1)
        gy  <=  a1 - c1 + ((a2 - c2) << 1) + a3 - c3;
    else
        gy  <=  gy;

//gxy:gxy计算
always@(posedge sys_clk or  negedge sys_rst_n)
    if(sys_rst_n == 1'b0)
        gxy <=  0;
    else    if((gx[8] == 1'b1) && (gy[8] == 1'b1) && (gxy_flag == 1'b1))
        gxy <=  (~gx[7:0] + 1'b1) + (~gy[7:0] + 1'b1);
    else    if((gx[8] == 1'b1) && (gy[8] == 1'b0) && (gxy_flag == 1'b1))
        gxy <=  (~gx[7:0] + 1'b1) + (gy[7:0]);
    else    if((gx[8] == 1'b0) && (gy[8] == 1'b1) && (gxy_flag == 1'b1))
        gxy <=  (gx[7:0]) + (~gy[7:0] + 1'b1);
    else    if((gx[8] == 1'b0) && (gy[8] == 1'b0) && (gxy_flag == 1'b1))
        gxy <=  (gx[7:0]) + (gy[7:0]);

//po_data:通过gxy与阈值比较,赋值po_data
always@(posedge sys_clk or  negedge sys_rst_n)
    if(sys_rst_n == 1'b0)
        po_data <=  8'b0;
    else    if((gxy >= THRESHOLD) && (compare_flag == 1'b1))
        po_data <=  BLACK;
    else    if(compare_flag == 1'b1)
        po_data <=  WHITE;

//po_flag:输出标志位
always@(posedge sys_clk or  negedge sys_rst_n)
    if(sys_rst_n == 1'b0)
        po_flag <=  1'b0;
    else    if(compare_flag == 1'b1)
        po_flag <=  1'b1;
    else
        po_flag <=  1'b0;

//********************************************************************//
//*************************** Instantiation **************************//
//********************************************************************//
//-------------fifo_pic_inst1--------------
fifo_pic    fifo_pic_inst1
(
    .clk    (sys_clk    ),  // input clk
    .din    (data_in1   ),  // input [7 : 0] din
    .wr_en  (wr_en1     ),  // input wr_en
    .rd_en  (rd_en      ),  // input rd_en

    .dout   (data_out1  )   // output [7 : 0] dout
);

//-------------fifo_pic_inst2--------------
fifo_pic    fifo_pic_inst2
(
    .clk    (sys_clk    ),  // input clk
    .din    (data_in2   ),  // input [7 : 0] din
    .wr_en  (wr_en2     ),  // input wr_en
    .rd_en  (rd_en      ),  // input rd_en
     
    .dout   (data_out2  )   // output [7 : 0] dout
);

endmodule

测试结果

测试原图1:
在这里插入图片描述
对应灰度图1:
在这里插入图片描述
经过Sobel算法后:


测试原图2:
FPGA-图像处理系列 基于Sobel算法的边缘检测设计与实现_第8张图片
对应灰度图2:
在这里插入图片描述
经过Sobel算法进行边缘检测后:
FPGA-图像处理系列 基于Sobel算法的边缘检测设计与实现_第9张图片

参考资料

野火开源图书资料
特权同学

你可能感兴趣的:(FPGA,图像处理)