基于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实践
- 四、乒乓缓存
- 末、参考文章
一、图像实时采集流程
文章图片
这里有几个重要的时钟需要记一下:
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 摄像头数据处理
文章图片
摄像头的数据是把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协议实现
三、边缘检测算法的四个算子
文章图片
要检测到一帧图像的边沿需要经过四个步骤:
灰度化
-> 高斯滤波
-> 二值化
-> 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.306
、0.601
、0.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];
效果图:
文章图片
这里还有一种非常简单的灰度化方法,就是直接分离RGB三通道,将3个通道的数值直接带入灰度通道中,使其呈现三种不同的灰度图像,最后再根据自己的需求进行选择:
文章图片
2. 高斯滤波(消除噪声)
2.1 概念与算法 高斯滤波在图像处理概念下,将图像频域处理和时域处理相联系,作为低通滤波器使用,可以将低频能量(比如噪声)滤去,起到图像平滑作用。
通俗的讲,高斯滤波就是对整幅图像进行加权平均的过程,每一个像素点的值,都由其本身和邻域内的其他像素值经过加权平均后得到。
文章图片
“中间点
2
”取”周围点”的平均值,就会变成1。在数值上,这是一种平滑化
。在图形上,就相当于产生”模糊”效果,”中间点”失去细节。而计算平均值时,取值范围越大(卷积核越大),”模糊效果”越强烈。文章图片
如果使用简单平均,显然不是很合理,因为图像都是连续的,越靠近的点关系越密切,越远离的点关系越疏远。因此,加权平均更合理,距离越近的点权重越大,距离越远的点权重越小。
文章图片
权重计算过程:
正态分布显然是一种可取的权重分配模式。由于图像是二维的,所以需要使用二维的高斯函数:
文章图片
详细计算过程可以参考链接:https://blog.csdn.net/fangyan90617/article/details/100516889由于高斯滤波实质是一种加权平均滤波,为了实现平均,核还带有一个系数,例如十六分之一、八十四分之一,这些系数等于矩阵中所有数值之和的倒数。
文章图片
本次工程也将采用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)
);
文章图片
这里可以看到我们的输入位宽为[7:0],输出行数为3,每个间距为1280(由图像分辨率1280*720决定)。工作流程如下图所示:
文章图片
这样一来我们就可以放心地去卷积了(如果还要深究移位寄存器的原理可以再查查相关资料)。
卷积
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里就有这样一种操作:
原图:
文章图片
阈值:128
文章图片
阈值:180
文章图片
通过这三张图大概就能理解二值化的精髓了。
那里的波形图所表示的是对应的阈值像素点在图中的数量。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的卷积核在下列图像中有三种可能
文章图片
显然全黑和全白的地方是不能产生梯度的,黑白交界处就会产生很明显的梯度,梯度越大,边缘效果就越明显。
比如这里纯白是255,纯黑是0,梯度就是255-0=255。当然我们的本次使用的二值化是01二进制式的,所以一维梯度最大值就是1。
文章图片
遍历
文章图片
最终取值
文章图片
AI的深度学习会补充行列,全部填充0,这个叫做Padding
文章图片
这样卷积核就能顾及到每一个像素点。
计算梯度
文章图片
这样就能得到一个中心像素点的梯度
卷积核中,越近的权值越高,这一点类似于高斯滤波。另外就是右侧像素点-左侧像素点
含义:当目标点
P5
左右两列差别特别大的时候,目标点的值会很大,说明该点为边界。问题:
- 目标像素点求得的值小于0或者大于255怎么办?
OpenCV的处理方式是截断操作,即小于0按0算,大于255按255算。
- 截断操作合适吗?
不合适。
- 影该如何操作?
对于小于0的取绝对值,大于255的可按255算(最大的极差了)
文章图片
本次工程为了简化流程,使用了简化梯度的算法,但如果要使用总体度,可以选择调用平方根的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,认为其是一个边缘点
这样就得到了我们的边缘点了。同样可以通过阈值的设计来调试最终结果。
文章图片
四、乒乓缓存 乒乓操作主要?于控制数据流,在此项?中主要体现为先写SDRAM bank1的数据,同时读SDRAM bank3的数据,当两块bank的数据读写完毕后,切换操作为读bank1的数据,写bank3的数据,这样可以保持数据为完整的?帧,使显?屏帧与帧之间切换瞬间完成。
末、参考文章 MartianCoder:灰度化到底是在干什么?
来自西伯利亚:RGB图转化成灰度图的色彩权重(不同精度等级)
醉意丶千层梦: 基于FPGA的图像实时采集
推荐阅读
- 人工智能|【CV】图像数据预处理详解
- 算法|基于MATLAB的图像去噪与边缘检测技术
- 计算机视觉|IEEE(基于轻量级特征增强卷积神经网络的低空小目标检测)
- 大数据|如何挖掘银行外部数据价值(零编码接入,分钟级服务化)
- 大数据|程序员该如何构建面向未来的前端架构!
- 程序员|要啥女朋友(大神教你用Python人工智能制作AI机器人)
- 人工智能|人工智能、机器人、编程啥关系((科普))
- 人工智能|深入了解人工智能机器人的应用领域有哪些()
- 程序人生|“鸡娃”新选择(首个AI象棋机器人来了,还当起了郭晶晶家的私教!)