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

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

  • 基于FPGA的图像处理(一):边缘检测
    • 一、图像实时采集流程
      • 1. 摄像头模块
        • 1.1 摄像头配置
        • 1.2 摄像头数据处理
      • 2. SDRAM模块
        • 2.1 SDRAM_CTRL
    • 二、VGA协议
    • 三、边缘检测算法的四个算子
      • 1. 灰度化 (减少计算量)
        • 1.1 概念与算法
        • 1.2 Verilog实践
      • 2. 高斯滤波(消除噪声)
        • 2.1 概念与算法
        • 2.2 Verilog实践
      • 3. 二值化(1 or 0)
        • 3.1 概念与算法
        • 3.2 Verilog实践
      • 4. Sobel算子(边缘检测)
        • 4.1 概念与算法
        • 4.2 Verilog实践
    • 四、乒乓缓存
    • 末、参考文章

一、图像实时采集流程 FPGA|FPGA图像处理(一)(边缘检测)
文章图片

这里有几个重要的时钟需要记一下:
xclk:摄像头工作频率 24Mhz
pclk:像素时钟 84Mhz
【FPGA|FPGA图像处理(一)(边缘检测)】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 摄像头数据处理 FPGA|FPGA图像处理(一)(边缘检测)
文章图片

摄像头的数据是把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 endassign 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协议实现
三、边缘检测算法的四个算子 FPGA|FPGA图像处理(一)(边缘检测)
文章图片

要检测到一帧图像的边沿需要经过四个步骤:灰度化 -> 高斯滤波 -> 二值化 -> 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];

效果图:
FPGA|FPGA图像处理(一)(边缘检测)
文章图片

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

2. 高斯滤波(消除噪声)
2.1 概念与算法 高斯滤波在图像处理概念下,将图像频域处理和时域处理相联系,作为低通滤波器使用,可以将低频能量(比如噪声)滤去,起到图像平滑作用。
通俗的讲,高斯滤波就是对整幅图像进行加权平均的过程,每一个像素点的值,都由其本身和邻域内的其他像素值经过加权平均后得到。
FPGA|FPGA图像处理(一)(边缘检测)
文章图片

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

如果使用简单平均,显然不是很合理,因为图像都是连续的,越靠近的点关系越密切,越远离的点关系越疏远。因此,加权平均更合理,距离越近的点权重越大,距离越远的点权重越小。
FPGA|FPGA图像处理(一)(边缘检测)
文章图片

权重计算过程:
正态分布显然是一种可取的权重分配模式。由于图像是二维的,所以需要使用二维的高斯函数:
FPGA|FPGA图像处理(一)(边缘检测)
文章图片

详细计算过程可以参考链接:https://blog.csdn.net/fangyan90617/article/details/100516889
由于高斯滤波实质是一种加权平均滤波,为了实现平均,核还带有一个系数,例如十六分之一、八十四分之一,这些系数等于矩阵中所有数值之和的倒数。
FPGA|FPGA图像处理(一)(边缘检测)
文章图片

本次工程也将采用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) );

FPGA|FPGA图像处理(一)(边缘检测)
文章图片

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

这样一来我们就可以放心地去卷积了(如果还要深究移位寄存器的原理可以再查查相关资料)。
卷积
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 endalways@(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 endalways@(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里就有这样一种操作:
原图:
FPGA|FPGA图像处理(一)(边缘检测)
文章图片

阈值:128
FPGA|FPGA图像处理(一)(边缘检测)
文章图片

阈值:180
FPGA|FPGA图像处理(一)(边缘检测)
文章图片

通过这三张图大概就能理解二值化的精髓了。
那里的波形图所表示的是对应的阈值像素点在图中的数量。
3.2 Verilog实践
input[7:0]din,//灰度输入 outputdout//二值输出 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的卷积核在下列图像中有三种可能
FPGA|FPGA图像处理(一)(边缘检测)
文章图片

显然全黑和全白的地方是不能产生梯度的,黑白交界处就会产生很明显的梯度,梯度越大,边缘效果就越明显。
比如这里纯白是255,纯黑是0,梯度就是255-0=255。当然我们的本次使用的二值化是01二进制式的,所以一维梯度最大值就是1。
FPGA|FPGA图像处理(一)(边缘检测)
文章图片

遍历
FPGA|FPGA图像处理(一)(边缘检测)
文章图片

最终取值
FPGA|FPGA图像处理(一)(边缘检测)
文章图片

AI的深度学习会补充行列,全部填充0,这个叫做Padding
FPGA|FPGA图像处理(一)(边缘检测)
文章图片

这样卷积核就能顾及到每一个像素点。
计算梯度
FPGA|FPGA图像处理(一)(边缘检测)
文章图片

这样就能得到一个中心像素点的梯度
卷积核中,越近的权值越高,这一点类似于高斯滤波。另外就是右侧像素点-左侧像素点
含义:当目标点P5左右两列差别特别大的时候,目标点的值会很大,说明该点为边界。
问题:
  1. 目标像素点求得的值小于0或者大于255怎么办?
    OpenCV的处理方式是截断操作,即小于0按0算,大于255按255算。
  2. 截断操作合适吗?
    不合适。
  3. 影该如何操作?
    对于小于0的取绝对值,大于255的可按255算(最大的极差了)
FPGA|FPGA图像处理(一)(边缘检测)
文章图片

本次工程为了简化流程,使用了简化梯度的算法,但如果要使用总体度,可以选择调用平方根的IP核。
4.2 Verilog实践 因为都涉及到卷积核,所以和高斯滤波一样需要一个移位寄存器,基本参数都一样。
inputdin,//输入二值图像 outputdout, wiretaps0; wiretaps1; wiretaps2; //缓存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算子模板系数: yx -1 0 11 2 1 -2 0 20 0 0 -1 0 1-1 -2 -1g = |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 assigndout = g >= 3; //阈值假设为3 当某一个像素点的梯度值大于3,认为其是一个边缘点

这样就得到了我们的边缘点了。同样可以通过阈值的设计来调试最终结果。
FPGA|FPGA图像处理(一)(边缘检测)
文章图片

四、乒乓缓存 乒乓操作主要?于控制数据流,在此项?中主要体现为先写SDRAM bank1的数据,同时读SDRAM bank3的数据,当两块bank的数据读写完毕后,切换操作为读bank1的数据,写bank3的数据,这样可以保持数据为完整的?帧,使显?屏帧与帧之间切换瞬间完成。
末、参考文章 MartianCoder:灰度化到底是在干什么?
来自西伯利亚:RGB图转化成灰度图的色彩权重(不同精度等级)
醉意丶千层梦: 基于FPGA的图像实时采集

    推荐阅读