FPGA图像处理(一):边缘检测

基于FPGA的图像处理(一):边缘检测

一、图像实时采集流程

在这里插入图片描述

这里有几个重要的时钟需要记一下:

xclk:摄像头工作频率 24Mhz

pclk:像素时钟 84Mhz

clk_100m: SDRAM工作时钟 100M

clk_75m:异步fifo时钟,VGA时钟

为什么用SDRAM不用FIFO?SDRAM比较大,相比FIFO更适合存储图像数据。

1. 摄像头模块

1.1 摄像头配置

摄像头模块里面负责处理摄像头采集的数据,根据ov5640摄像头手册说明,我们需要先通过SCCB协议去配置摄像头相关寄存器的参数。在摄像头上电后需要等待20ms。然后再通过I2C发送设备ID、写地址和数据,其中地址先发送高8位再发送低8位(四相写)。这里包含摄像头时钟、图像大小、帧率以及其他和图像相关的参数。这里最重要的配置参数就是摄像头的图像分辨率和图像的色彩格式,这里通过配置的分辨率为1280*720,RGB565格式(16位宽)。

因为SCCB跟I2C很像,不同之处在于不支持连续读写,所以这里直接把之前写的I2C接口拿过来改一下就能用了。

1.2 摄像头数据处理

在这里插入图片描述

摄像头的数据是把16位RGB拆分为高八位和低八位发送的,我们需要通过移位+位拼接的方式把两个8bit数据合并成16bit数据输出。同时为了SDRAM模块更好的识别帧头和帧尾,在图像的第一个像素点以及最后一个像素点的时候分别拉高sop和eop信号,其余像素点这拉低。

    always  @(posedge clk or negedge rst_n)begin
        if(~rst_n)begin
            data <= 0;
        end
        else begin
            data <= {data[7:0],din};//左移
        end
    end

    assign pixel = data;
    assign sop = cnt_h == 1 && cnt_v == 0;
    assign eop = data_eop;
    assign vld = cnt_h[0];

2. SDRAM模块

2.1 SDRAM_CTRL

由于摄像头数据时钟(84M)和vga时钟(75M)不一样,为了避免读写数据速度不一致就需要把摄像头的数据进行缓存,常见的缓存可以通过fifo,但是这里的数据量十分庞大,故需要通过SDRAM进行缓存。缓存的方式则是通过乒乓缓存,把SDRAM的两个blank单独作为数据的写入和读出,并在读写完成后切换读写blank,并且需要通过丢帧和复读的操作来保证读写图像的完整性。

详细请移步至醉意丶千层梦: 基于FPGA的图像实时采集

二、VGA协议

FPGA实验记录四:基于FPGA的VGA协议实现

三、边缘检测算法的四个算子

image-20220809214257014

要检测到一帧图像的边沿需要经过四个步骤:灰度化 -> 高斯滤波 -> 二值化 -> Sobel算子

灰度化负责将图像数据简化为单通道,以便于后续处理;

高斯滤波则将灰度化后的图像通过卷积核进行消除噪声,使图像更加平滑;

二值化负责将图像变得棱角分明,以便于Sobel算子进行更精准检测;

Sobel算子使用卷积核对二值化的图像像素点进行处理得到梯度,当梯度大于指定阈值则为图像边缘;

1. 灰度化 (减少计算量)

1.1 概念与算法

摄像头采集到的图像数据是彩色的RGB三通道,这样在后续处理时会非常麻烦,所以我们可以用灰度化算法将图像数据处理为单通道(黑与白的0,255),这样一来后续的处理就会简单很多。

通常这个值是RGB三个通道加权计算得到,人眼对RGB颜色的敏感度不同:对绿色最敏感,所以权值最高。对蓝色不敏感,权值最低。

这里的灰度化不是恒定值,需要开发者根据转化精度去选择,以下是2位-20位的精度转化公式(Verilog):

Gray = (R*1 + G*2 + B*1) >> 2
Gray = (R*2 + G*5 + B*1) >> 3
Gray = (R*4 + G*10 + B*2) >> 4
Gray = (R*9 + G*19 + B*4) >> 5
Gray = (R*19 + G*37 + B*8) >> 6
Gray = (R*38 + G*75 + B*15) >> 7
Gray = (R*76 + G*150 + B*30) >> 8
Gray = (R*153 + G*300 + B*59) >> 9
Gray = (R*306 + G*601 + B*117) >> 10
Gray = (R*612 + G*1202 + B*234) >> 11
Gray = (R*1224 + G*2405 + B*467) >> 12
Gray = (R*2449 + G*4809 + B*934) >> 13
Gray = (R*4898 + G*9618 + B*1868) >> 14
Gray = (R*9797 + G*19235 + B*3736) >> 15
Gray = (R*19595 + G*38469 + B*7472) >> 16
Gray = (R*39190 + G*76939 + B*14943) >> 17
Gray = (R*78381 + G*153878 + B*29885) >> 18
Gray = (R*156762 + G*307757 + B*59769) >> 19
Gray = (R*313524 + G*615514 + B*119538) >> 20

一般地,10位的精度较为常用(精度太高,效率会降低),本次工程也将使用10位精度的公式;

看到这里可能有一个疑惑,为什么这里要使用位移运算符>>?

这里以10位精度为例,在C语言或者Python等高级语言中,权值是0.3060.6010.117,也就是说都是小数,而Verilog不支持小数运算,所以只能先消除小数点来得到乘积,最后再通过移位缩小至近似原来的大小。

所以这里的精度位数也并不是指小数点后几位,而是2的10次方。(512<601<1024)

1.2 Verilog实践

首先,本次工程中,OV5640摄像头采集图像时使用的是RGB565格式(像素点位宽16),为了灰度化算法,我们需要通过补位将其变为三通道相对平均的RGB888格式。

    input   [15:0]  din         ,//RGB565

	reg     [7:0]       data_r  ;
    reg     [7:0]       data_g  ;
    reg     [7:0]       data_b  ;

    always  @(posedge clk or negedge rst_n)begin
        if(~rst_n)begin
            data_r <= 0;
            data_g <= 0;
            data_b <= 0;
        end
        else if(din_vld)begin
            data_r <= {din[15:11],din[13:11]};      //带补偿的  r5,r4,r3,r2,r1, r3,r2,r1
            data_g <= {din[10:5],din[6:5]}   ;
            data_b <= {din[4:0],din[2:0]}    ;
        end
    end

补充完后就可以开始进行权值计算了:

    reg     [17:0]      pixel_r ;
    reg     [17:0]      pixel_g ;
    reg     [17:0]      pixel_b ;
	reg     [07:0]      pixel   ;
    //第一拍
    always  @(posedge clk or negedge rst_n)begin
        if(~rst_n)begin
            pixel_r <= 0;
            pixel_g <= 0;
            pixel_b <= 0;
        end
        else if(vld[0])begin
            pixel_r <= data_r * 306;
            pixel_g <= data_g * 601;
            pixel_b <= data_b * 117;
        end
    end
    //第二拍
    always  @(posedge clk or negedge rst_n)begin
        if(~rst_n)begin
            pixel <= 0;
        end
        else if(vld[1])begin
            pixel <= (pixel_r + pixel_g + pixel_b)>>10;
        end
    end
//或者
	reg     [17:0]      pixel   ;
	assign dout = pixel[10 +:8];

效果图:

image-20211222210357656

这里还有一种非常简单的灰度化方法,就是直接分离RGB三通道,将3个通道的数值直接带入灰度通道中,使其呈现三种不同的灰度图像,最后再根据自己的需求进行选择:

image-20211222174313255

2. 高斯滤波(消除噪声)

2.1 概念与算法

高斯滤波在图像处理概念下,将图像频域处理和时域处理相联系,作为低通滤波器使用,可以将低频能量(比如噪声)滤去,起到图像平滑作用。

通俗的讲,高斯滤波就是对整幅图像进行加权平均的过程,每一个像素点的值,都由其本身和邻域内的其他像素值经过加权平均后得到。

这里写图片描述

“中间点2”取”周围点”的平均值,就会变成1。在数值上,这是一种平滑化。在图形上,就相当于产生”模糊”效果,”中间点”失去细节。而计算平均值时,取值范围越大(卷积核越大),”模糊效果”越强烈。

img

如果使用简单平均,显然不是很合理,因为图像都是连续的,越靠近的点关系越密切,越远离的点关系越疏远。因此,加权平均更合理,距离越近的点权重越大,距离越远的点权重越小。

img

权重计算过程:

正态分布显然是一种可取的权重分配模式。由于图像是二维的,所以需要使用二维的高斯函数:

这里写图片描述

详细计算过程可以参考链接:https://blog.csdn.net/fangyan90617/article/details/100516889

由于高斯滤波实质是一种加权平均滤波,为了实现平均,核还带有一个系数,例如十六分之一、八十四分之一,这些系数等于矩阵中所有数值之和的倒数。

image-20220810092905636

本次工程也将采用1/16的卷积核。

2.2 Verilog实践

移位寄存器

因为高斯滤波涉及到卷积核对二维图像的一个卷积,所以我们首先需要一个移位寄存器来将串行数据变为并行数据。这里调用一个IP核即可:

    wire    [7:0]   taps0       ; 
    wire    [7:0]   taps1       ; 
    wire    [7:0]   taps2       ; 
//缓存3行数据
    gs_line_buf	gs_line_buf_inst (
	.aclr       (~rst_n     ),
	.clken      (din_vld    ),
	.clock      (clk        ),
    /*input*/
	.shiftin    (din        ),
	.shiftout   (           ),
    /*output*/
	.taps0x     (taps0      ),
	.taps1x     (taps1      ),
	.taps2x     (taps2      )
	);

image-20220810133715570

这里可以看到我们的输入位宽为[7:0],输出行数为3,每个间距为1280(由图像分辨率1280*720决定)。工作流程如下图所示:

img

这样一来我们就可以放心地去卷积了(如果还要深究移位寄存器的原理可以再查查相关资料)。

卷积

    reg     [7:0]   line0_0     ;
    reg     [7:0]   line0_1     ;
    reg     [7:0]   line0_2     ;

    reg     [7:0]   line1_0     ;
    reg     [7:0]   line1_1     ;
    reg     [7:0]   line1_2     ;

    reg     [7:0]   line2_0     ;
    reg     [7:0]   line2_1     ;
    reg     [7:0]   line2_2     ;

    reg     [9:0]   sum_0       ;//第0行加权和 
    reg     [9:0]   sum_1       ;//第1行加权和
    reg     [9:0]   sum_2       ;//第2行加权和
    
	reg     [7:0]  sum         ;//三行的加权和

首先是参数,lineX_Y表示第X第Y行,我们需要创造一个3*3的矩阵寄存器,所以这里共需要9个寄存器,并且需要求的加权和,所以每一行再分配一个sum,最后再分配一个总sum;

/*
高斯滤波系数,加权平均
	1	2	1
	2	4	2
	1	2	1
*/
	always  @(posedge clk or negedge rst_n)begin
        if(~rst_n)begin
            line0_0 <= 0;line0_1 <= 0;line0_2 <= 0;      
            line1_0 <= 0;line1_1 <= 0;line1_2 <= 0;         
            line2_0 <= 0;line2_1 <= 0;line2_2 <= 0;
        end
        else if(vld[0])begin
            line0_0 <= taps0;line0_1 <= line0_0;line0_2 <= line0_1;           
            line1_0 <= taps1;line1_1 <= line1_0;line1_2 <= line1_1;       
            line2_0 <= taps2;line2_1 <= line2_0;line2_2 <= line2_1;
        end
    end

    always  @(posedge clk or negedge rst_n)begin
        if(~rst_n)begin
            sum_0 <= 0;
            sum_1 <= 0;
            sum_2 <= 0;
        end
        else if(vld[1])begin
            sum_0 <= {2'd0,line0_0} + {1'd0,line0_1,1'd0} + {2'd0,line0_2};
            sum_1 <= {1'd0,line1_0,1'd0} + {line1_1,2'd0} + {1'd0,line1_2,1'd0};
            sum_2 <= {2'd0,line2_0} + {1'd0,line2_1,1'd0} + {2'd0,line2_2};
        end
    end

    always  @(posedge clk or negedge rst_n)begin
        if(~rst_n)begin
            sum <= 0;
        end
        else if(vld[2])begin
            sum <= (sum_0 + sum_1 + sum_2)>>4;
        end
    end

第一个Always完成的是矩阵寄存器对图像数据的存储;

第二个Always通过位拼接来实现与卷积核的乘法(直接乘也没问题,只是这样写能显示出老练的技法)与每行的求和;

第三个Always就是求总和,这里也和灰度寄存器一样使用了移位运算符,这也是因为Verilog不能进行小数运算,实际运算的卷积核内的参数都是扩大了16倍(2^4)。

3. 二值化(1 or 0)

3.1 概念与算法

有着非常简单的概念与算法,这就是一个把图像变得非黑即白的、黑白分明的、容易辨别的、中肯的、客观的、抽象的、形而上学的玩意儿。

因为此时图像已经是灰度单通道的(0,255),所以只需要设定一个阈值,当图像数据<它时,就设置为0(黑色),>大于它时就设置为1(白色),根据阈值的不同,最终得到的结果也不一样,PS里就有这样一种操作:

原图:

image-20220810140714287

阈值:128

image-20220810140542696

阈值:180

image-20220810140614962

通过这三张图大概就能理解二值化的精髓了。

那里的波形图所表示的是对应的阈值像素点在图中的数量。

3.2 Verilog实践

    input   [7:0]   din         ,//灰度输入
    output          dout         //二值输出 
	always  @(posedge clk or negedge rst_n)begin
        if(~rst_n)begin
            binary     <= 0 ;
        end
        else begin
            binary     <= din>100 ;//二值化阈值可自定义
        end
    end
    assign dout     = binary;

4. Sobel算子(边缘检测)

4.1 概念与算法

索贝尔算子是计算机视觉领域的一种重要处理方法。主要用于获得数字图像的一阶梯度,常见的应用和物理意义是边缘检测。索贝尔算子是把图像中每个像素的上下左右四领域的灰度值加权差,在边缘处达到极值从而检测边缘。

  • 图像边缘检测的重要算子之一。

  • 与梯度密不可分

  • 处理二值化后的图像

梯度:

什么情况下产生?

一个3*3的卷积核在下列图像中有三种可能

image-20220809102709621

显然全黑和全白的地方是不能产生梯度的,黑白交界处就会产生很明显的梯度,梯度越大,边缘效果就越明显。

比如这里纯白是255,纯黑是0,梯度就是255-0=255。当然我们的本次使用的二值化是01二进制式的,所以一维梯度最大值就是1。

image-20220809102953311

遍历

image-20220809103322525

最终取值

image-20220809103624072

AI的深度学习会补充行列,全部填充0,这个叫做Padding

image-20220809103720644

这样卷积核就能顾及到每一个像素点。

计算梯度

image-20220809103857029

这样就能得到一个中心像素点的梯度

卷积核中,越近的权值越高,这一点类似于高斯滤波。另外就是右侧像素点-左侧像素点

含义:当目标点P5左右两列差别特别大的时候,目标点的值会很大,说明该点为边界。

问题:

  1. 目标像素点求得的值小于0或者大于255怎么办?

    OpenCV的处理方式是截断操作,即小于0按0算,大于255按255算。

  2. 截断操作合适吗?

    不合适。

  3. 影该如何操作?

    对于小于0的取绝对值,大于255的可按255算(最大的极差了)

image-20220809113318219

本次工程为了简化流程,使用了简化梯度的算法,但如果要使用总体度,可以选择调用平方根的IP核。

4.2 Verilog实践

因为都涉及到卷积核,所以和高斯滤波一样需要一个移位寄存器,基本参数都一样。

	input           din     ,//输入二值图像
    output          dout    ,

	wire            taps0   ; 
    wire            taps1   ; 
    wire            taps2   ; 
//缓存3行

sobel_line_buf	sobel_line_buf_inst (
	.aclr       (~rst_n     ),
	.clken      (din_vld    ),
	.clock      (clk        ),
	.shiftin    (din        ),
	.shiftout   (           ),
	.taps0x     (taps0      ),
	.taps1x     (taps1      ),
	.taps2x     (taps2      )
	);

矩阵赋值

    always  @(posedge clk or negedge rst_n)begin
        if(~rst_n)begin
            line0_0 <= 0;line0_1 <= 0;line0_2 <= 0;
            line1_0 <= 0;line1_1 <= 0;line1_2 <= 0;
            line2_0 <= 0;line2_1 <= 0;line2_2 <= 0;
        end
        else if(vld[0])begin
            line0_0 <= taps0;line0_1 <= line0_0;line0_2 <= line0_1;
            line1_0 <= taps1;line1_1 <= line1_0;line1_2 <= line1_1;
            line2_0 <= taps2;line2_1 <= line2_0;line2_2 <= line2_1;
        end
    end

卷积

/*
Sobel算子模板系数:
y 				x
-1	0	1		1	2	1
-2	0	2		0	0	0
-1	0	1		-1	-2	-1

g = |x_g| + |y_g|
*/
	always  @(posedge clk or negedge rst_n)begin
        if(~rst_n)begin
            x0_sum <= 0;
            x2_sum <= 0;
            y0_sum <= 0;
            y2_sum <= 0;
        end
        else if(vld[1])begin
            x0_sum <= {2'd0,line0_0} + {1'd0,line0_1,1'd0} + {2'd0,line0_2};
            x2_sum <= {2'd0,line2_0} + {1'd0,line2_1,1'd0} + {2'd0,line2_2};
            y0_sum <= {2'd0,line0_0} + {1'd0,line1_0,1'd0} + {2'd0,line2_0};
            y2_sum <= {2'd0,line0_2} + {1'd0,line1_2,1'd0} + {2'd0,line2_2};
        end
    end   

可以看到这里只有第一行、第三行和第一列、第三列,这是因为两个卷积核中第二行、第二列都是0,没有计算意义。

求和与简化梯度

    //计算x 、y方向梯度绝对值
    always  @(posedge clk or negedge rst_n)begin
        if(~rst_n)begin
            x_abs <= 0;
            y_abs <= 0;
        end
        else if(vld[2])begin
            x_abs <= (x0_sum >= x2_sum)?(x0_sum-x2_sum):(x2_sum-x0_sum);
            y_abs <= (y0_sum >= y2_sum)?(y0_sum-y2_sum):(y2_sum-y0_sum);
        end
    end
    always  @(posedge clk or negedge rst_n)begin
        if(~rst_n)begin
            g <= 0;
        end
        else if(vld[3])begin
            g <= x_abs + y_abs;//绝对值之和 近似 平方和开根号
        end
    end
assign  dout = g >= 3;//阈值假设为3 当某一个像素点的梯度值大于3,认为其是一个边缘点

这样就得到了我们的边缘点了。同样可以通过阈值的设计来调试最终结果。

image-20220810143858809

四、乒乓缓存

乒乓操作主要⽤于控制数据流,在此项⽬中主要体现为先写SDRAM bank1的数据,同时读SDRAM bank3的数据,当两块bank的数据读写完毕后,切换操作为读bank1的数据,写bank3的数据,这样可以保持数据为完整的⼀帧,使显⽰屏帧与帧之间切换瞬间完成。

末、参考文章

MartianCoder:灰度化到底是在干什么?

来自西伯利亚:RGB图转化成灰度图的色彩权重(不同精度等级)

醉意丶千层梦: 基于FPGA的图像实时采集

文章出处登录后可见!

已经登录?立即刷新

共计人评分,平均

到目前为止还没有投票!成为第一位评论此文章。

(0)
xiaoxingxing的头像xiaoxingxing管理团队
上一篇 2023年2月25日
下一篇 2023年2月25日

相关推荐