• 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];
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    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
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    一般地,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
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

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

        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];
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29

    效果图:

    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      )
    	);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    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         ;//三行的加权和
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    首先是参数,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
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40

    第一个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;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    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      )
    	);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    矩阵赋值

        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
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    卷积

    /*
    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   
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    可以看到这里只有第一行、第三行和第一列、第三列,这是因为两个卷积核中第二行、第二列都是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,认为其是一个边缘点
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

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

    image-20220810143858809

    四、乒乓缓存

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

    末、参考文章

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

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

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

  • 相关阅读:
    【记录】使用Python读取Tiff图像的几种方法
    让我看看你们公司的代码规范都是啥样的?
    交换机命令
    【leetcode】【2022/9/11】857. 雇佣 K 名工人的最低成本
    有望引领下轮牛市的5大加密主题
    计算机毕业设计之java+ssm新冠肺炎疫苗接种管理系统
    Java中ArrayList和LinkedList区别
    AI系统ChatGPT源码+详细搭建部署教程+AI绘画系统+支持GPT4.0+Midjourney绘画+已支持OpenAI GPT全模型+国内AI全模型
    SLAM从入门到精通(camera数据读取)
    【无标题】
  • 原文地址:https://blog.csdn.net/ChenJ_1012/article/details/126268119