基于FPGA的图像边缘检测
简介:基于FPGA,摄像头实时采集图像数据,经过图像处理、乒乓缓存,通过vga显示
工具:Quartus 18.1
开发板:AIGO_C4MB_V11(CycloneIV-EP4CE6F17C8)
摄像头:OV5640
RGB颜色模型是由红(Red)、绿(Green)、蓝(Blue)三种基色以不同的比例叠加而成;而且每个像素分量(R、G、B)的值分布在0—255范围内,三种基色以不同的比例混合,能够显示出2563种颜色。 在这个项目中ov5640采集的数据是16位rgb565的数据,首先扩展为rgb888,然后进行加权求和

如图所示,灰度转换的公式
在灰度转换过程中,可能会因为取整操作引入噪声,所以接下来使用高斯滤波算法来去除灰度转化过程中引入的噪声
高斯滤波本质上是一种线性平滑滤波,即对整幅图像进行加权平均的过程,每一个像素点的值都是由其本身和邻域内的其他像素点加权平均后得到
高斯滤波的具体操作是使用一个N*N卷积模板对整幅图像扫描,用模板确定的邻域内的像素加权平均值代替模板中心像素点的值

其中,I(x,y)表示原图像中坐标为(x,y)的像素值;G(x,y)表示高斯滤波之后的值
二值化的作用是把灰度图像的像素值设置为0或者255,即纯黑或者纯白。通过二值图像,能更好地分析物体的形状和轮廓,有利于后续使用Sobel算子检测图像的边缘
二值化有多种方法,其中最常用的就是采用阈值法进行二值化
Sobel算子主要用于检测图像边缘,在物体的边缘通常都有像素的变化,反映了物体与背景之间的差异,或者两个物体之间的差异。它是一个离散差分算子,用来计算像素点上下、左右领域内像素点的加权差,根据在边缘处达到极值来检测边缘
Sobel算子在水平方向和垂直方向各采用一个模板,检测各方向上的边缘,其优点是计算简单,速度快;但是对于纹理较为复杂的图像,检测效果不理想。水平方向模板Sx和垂直方向模板Sy如下

将两个算子与图像做平面卷积,即可得到水平方向与垂直方向的梯度值;若以I表示图像矩阵,Gx表示水平方向图像梯度值,Gy表示垂直方向的梯度值,则Gx与Gy可以表示如下:

整个项目主要分为:
摄像头配置模块、图像处理模块、数据缓存模块、vga显示模块以及时钟管理模块

摄像头配置模块负责配置摄像头各个参数,在摄像头上电后需要等待20ms。然后再通过I2C发送设备ID、写地址和数据,其中地址先发送高8位再发送低8位。这里包含摄像头时钟、图像大小、帧率以及其他和图像相关的参数,按照配置表中的参数,将摄像头配置为分辨率为1280*720像素点、RGB565数据格式、VGA时序输出;然后通过I2C协议将参数配置给摄像头的每个寄存器
该模块完成图像数据的采集与处理;图像采集模块(capture)对摄像头输出的像素数据进行串并转换,然后给到后续的图像处理模块,依次进行灰度转换(rgb565_gray)、高斯滤(gs_filter)、二值化处理(gray_bin)、Sobel边缘检测(sobel)
通过乒乓缓存操作向SDRAM中读写图像数据,接口通过调用IP,主要是SDRAM读写控制逻辑(rw_control),使用两个异步FIFO跨时钟域数据处理,使用读写仲裁机制产生读写传输请求、地址等
为什么要用pp(乒乓)缓存?
如果不采用乒乓缓存,OV5640 帧率 30fps,VGA 帧率 60fps,如果摄像头输入的数据和VGA输出的数据都是连续不断的,那么刚好可以写一帧读两帧。但是一帧图像实际情况是一行行的生成和读取的,所以会出现 VGA 从SDRAM处读的上半帧是新帧,而由于SDRAM缓存的下半帧还没有被 OV5640写完,VGA 从SDRAM处读的下半帧还是旧帧,会出现错帧现象。采用乒乓缓存机制时,使用两个缓存区,写缓存区 1 时读缓存区 2,写缓存区 2 时读缓存区 1,每个缓存区存储完整的数据帧,读写隔离并且读写交替则不会出现错帧现象
为什么要读写仲裁?
仲裁:在FPGA中,当多个操作同时发出请求,容易导致操作冲突,因此我们需要根据相应的优先级来响应哪一个操作,这个过程就叫仲裁。在SDRAM中,初始化完成后,主要的功能就是突发写、突发读和自动刷新。如果同时发起写、读和刷新请求,就会出现操作冲突,从而导致SDRAM工作出错,因此这里就需要引入仲裁机制。为了简化设计,考虑将刷新与读写请求的仲裁分开考虑。由于刷新的优先级一定高于读写,因此,在底层接口中,只对读/写请求与刷新请求进行仲裁,即刷新请求的优先级一定高于读/写请求。在控制逻辑中,对读/写请求进行仲裁,保证底层接口不会同时收到读请求与写请求,从而避免底层接口中出现复杂控制
vga模块就是通过vga协议将图像显示,这个非常简单,我之前的博客也写过关于vga的操作,这里就不在过多赘述
时钟管理模块则是通过pll生成时钟供SDRAM、vga、ov5640使用也没什么技术含量
`include "param.v"
module capture(
input clk ,//像素时钟 摄像头输出的pclk
input rst_n ,
input enable ,//采集使能 配置完成
input vsync ,//摄像头场同步信号
input href ,//摄像头行参考信号
input [7:0] din ,//摄像头像素字节
output [15:0] dout ,//像素数据
output dout_sop,//包文头 一帧图像第一个像素点
output dout_eop,//包文尾 一帧图像最后一个像素点
output dout_vld //像素数据有效
);
//信号定义
reg [11:0] cnt_h ;
wire add_cnt_h ;
wire end_cnt_h ;
reg [9:0] cnt_v ;
wire add_cnt_v ;
wire end_cnt_v ;
reg [1:0] vsync_r ;//同步打拍
wire vsync_nedge ;//下降沿
reg flag ;//串并转换标志
reg [15:0] data ;
reg data_vld ;
reg data_sop ;
reg data_eop ;
//计数器
always @(posedge clk or negedge rst_n) begin
if (rst_n==0) begin
cnt_h <= 0;
end
else if(add_cnt_h) begin
if(end_cnt_h)
cnt_h <= 0;
else
cnt_h <= cnt_h+1 ;
end
end
assign add_cnt_h = flag & href;
assign end_cnt_h = add_cnt_h && cnt_h == (`H_AP << 1)-1;
always @(posedge clk or negedge rst_n) begin
if (rst_n==0) begin
cnt_v <= 0;
end
else if(add_cnt_v) begin
if(end_cnt_v)
cnt_v <= 0;
else
cnt_v <= cnt_v+1 ;
end
end
assign add_cnt_v = end_cnt_h;
assign end_cnt_v = add_cnt_v && cnt_v == `V_AP-1 ;
//vsync同步打拍
always @(posedge clk or negedge rst_n)begin
if(~rst_n)begin
vsync_r <= 2'b00;
end
else begin
vsync_r <= {vsync_r[0],vsync};
end
end
assign vsync_nedge = vsync_r[1] & ~vsync_r[0];
always @(posedge clk or negedge rst_n)begin
if(~rst_n)begin
flag <= 1'b0;
end
else if(enable & vsync_nedge)begin //摄像头配置完成且场同步信号拉低之后开始采集有效数据
flag <= 1'b1;
end
else if(end_cnt_v)begin //一帧数据采集完拉低
flag <= 1'b0;
end
end
//data
always @(posedge clk or negedge rst_n)begin
if(~rst_n)begin
data <= 0;
end
else begin
data <= {data[7:0],din};//左移
//data <= 16'b1101_1010_1111_0111;//16'hdaf7
end
end
//data_sop
always @(posedge clk or negedge rst_n)begin
if(~rst_n)begin
data_sop <= 1'b0;
data_eop <= 1'b0;
data_vld <= 1'b0;
end
else begin
data_sop <= add_cnt_h && cnt_h == 2-1 && cnt_v == 0;
data_eop <= end_cnt_v;
data_vld <= add_cnt_h && cnt_h[0] == 1'b1;
end
end
assign dout = data;
assign dout_sop = data_sop;
assign dout_eop = data_eop;
assign dout_vld = data_vld;
endmodule
`include"param.v"
module sdram_ctrl (
input clk ,
input clk_in ,
input clk_out ,
input rst_n ,
//数据输入
input [15:0] din ,//摄像头输入像素数据
input din_sop ,
input din_eop ,
input din_vld ,
//数据输出
input rdreq ,//vga的读数据请求
output [15:0] dout ,//输出给vga的数据
output dout_vld ,//输出给vga的数据有效标志
//sdram_interface
output avm_write ,//输出给sdram 接口 IP 的写请求
output avm_read ,//输出给sdram 接口 IP 的读请求
output [23:0] avm_addr ,//输出给sdram 接口 IP 的读写地址
output [15:0] avm_wrdata ,//输出给sdram 接口 IP 的写数据
input [15:0] avs_rddata ,//sdram 接口 IP 输入的读数据
input avs_rddata_vld ,
input avs_waitrequest
);
//参数定义
localparam IDLE = 4'b0001,
WRITE = 4'b0010,
READ = 4'b0100,
DONE = 4'b1000;
//信号定义
reg [3:0] state_c ;
reg [3:0] state_n ;
reg [8:0] cnt ;//突发读写计数器
wire add_cnt ;
wire end_cnt ;
reg [1:0] wr_bank ;//写bank
reg [1:0] rd_bank ;//读bank
reg [21:0] wr_addr ;//写地址 行地址 + 列地址
wire add_wr_addr ;
wire end_wr_addr ;
reg [21:0] rd_addr ;//读地址 行地址 + 列地址
wire add_rd_addr ;
wire end_rd_addr ;
reg change_bank ;//切换bank
reg wr_finish ;//一帧数据写完
reg [1:0] wr_finish_r ;//同步到写侧
reg wr_data_flag;//wrfifo写数据的标志
reg wr_flag ;
reg rd_flag ;
reg flag_sel ;
reg prior_flag ;
wire idle2write ;
wire idle2read ;
wire write2done ;
wire read2done ;
reg [15:0] rd_data ;//rfifo读数据输出
reg rd_data_vld ;
wire [17:0] wfifo_data ;
wire wfifo_rdreq ;
wire wfifo_wrreq ;
wire [17:0] wfifo_q ;
wire wfifo_empty ;
wire [10:0] wfifo_usedw ;
wire wfifo_full ;
wire [15:0] rfifo_data ;
wire rfifo_rdreq ;
wire rfifo_wrreq ;
wire [15:0] rfifo_q ;
wire rfifo_empty ;
wire rfifo_full ;
wire [10:0] rfifo_usedw ;
//状态机
always @(posedge clk or negedge rst_n)begin
if(~rst_n)begin
state_c <= IDLE;
end
else begin
state_c <= state_n;
end
end
always @(*)begin
case(state_c)
IDLE :begin
if(idle2write)
state_n = WRITE;
else if(idle2read)
state_n = READ;
else
state_n = state_c;
end
WRITE :begin
if(write2done)
state_n = DONE;
else
state_n = state_c;
end
READ :begin
if(read2done)
state_n = DONE;
else
state_n = state_c;
end
DONE :state_n = IDLE;
default:state_n = IDLE;
endcase
end
assign idle2write = state_c == IDLE && (~prior_flag && wfifo_usedw >= `USER_BL);
assign idle2read = state_c == IDLE && prior_flag && rfifo_usedw <= `RD_UT;
assign write2done = state_c == WRITE && end_cnt;
assign read2done = state_c == READ && end_cnt;
//计数器
always @(posedge clk or negedge rst_n)begin
if(!rst_n)begin
cnt <= 0;
end
else if(add_cnt)begin
if(end_cnt)
cnt <= 0;
else
cnt <= cnt + 1;
end
end
assign add_cnt = (state_c == WRITE | state_c == READ) & ~avs_waitrequest;
assign end_cnt = add_cnt && cnt== `USER_BL-1;
/************************读写优先级仲裁*****************************/
//rd_flag ;//读请求标志
always @(posedge clk or negedge rst_n)begin
if(!rst_n)begin
rd_flag <= 0;
end
else if(rfifo_usedw <= `RD_LT)begin
rd_flag <= 1'b1;
end
else if(rfifo_usedw > `RD_UT)begin
rd_flag <= 1'b0;
end
end
//wr_flag ;//写请求标志
always @(posedge clk or negedge rst_n)begin
if(!rst_n)begin
wr_flag <= 0;
end
else if(wfifo_usedw >= `USER_BL)begin
wr_flag <= 1'b1;
end
else begin
wr_flag <= 1'b0;
end
end
//flag_sel ;//标记上一次操作
always @(posedge clk or negedge rst_n)begin
if(!rst_n)begin
flag_sel <= 0;
end
else if(read2done)begin
flag_sel <= 1;
end
else if(write2done)begin
flag_sel <= 0;
end
end
//prior_flag ;//优先级标志 0:写优先级高 1:读优先级高 仲裁读、写的优先级
always @(posedge clk or negedge rst_n)begin
if(!rst_n)begin
prior_flag <= 0;
end
else if(wr_flag && (flag_sel || (~flag_sel && ~rd_flag)))begin //突发写优先级高
prior_flag <= 1'b0;
end
else if(rd_flag && (~flag_sel || (flag_sel && ~wr_flag)))begin //突发读优先级高
prior_flag <= 1'b1;
end
end
/******************************************************************/
/******************** 地址设计 ****************************/
//wr_bank rd_bank
always @(posedge clk or negedge rst_n)begin
if(~rst_n)begin
wr_bank <= 2'b00;
rd_bank <= 2'b11;
end
else if(change_bank)begin
wr_bank <= ~wr_bank;
rd_bank <= ~rd_bank;
end
end
// wr_addr rd_addr
always @(posedge clk or negedge rst_n) begin
if (rst_n==0) begin
wr_addr <= 0;
end
else if(add_wr_addr) begin
if(end_wr_addr)
wr_addr <= 0;
else
wr_addr <= wr_addr+1 ;
end
end
assign add_wr_addr = (state_c == WRITE) && ~avs_waitrequest;
assign end_wr_addr = add_wr_addr && wr_addr == `BURST_MAX-1 ;
always @(posedge clk or negedge rst_n) begin
if (rst_n==0) begin
rd_addr <= 0;
end
else if(add_rd_addr) begin
if(end_rd_addr)
rd_addr <= 0;
else
rd_addr <= rd_addr+1 ;
end
end
assign add_rd_addr = (state_c == READ) && ~avs_waitrequest;
assign end_rd_addr = add_rd_addr && rd_addr == `BURST_MAX-1;
//wr_finish 一帧数据全部写到SDRAM
always @(posedge clk or negedge rst_n)begin
if(~rst_n)begin
wr_finish <= 1'b0;
end
else if(~wr_finish & wfifo_q[17])begin //写完 从wrfifo读出eop
wr_finish <= 1'b1;
end
else if(wr_finish && end_rd_addr)begin //读完
wr_finish <= 1'b0;
end
end
//change_bank ;//切换bank
always @(posedge clk or negedge rst_n)begin
if(~rst_n)begin
change_bank <= 1'b0;
end
else begin
change_bank <= wr_finish && end_rd_addr;
end
end
/****************************************************************/
/*********************** wrfifo 写数据 ************************/
//控制像素数据帧 写入 或 丢帧
always @(posedge clk_in or negedge rst_n)begin
if(~rst_n)begin
wr_data_flag <= 1'b0;
end
else if(~wr_data_flag & ~wr_finish_r[1] & din_sop)begin//可以向wrfifo写数据
wr_data_flag <= 1'b1;
end
else if(/*wr_finish_r[1] && din_sop*/wr_data_flag & din_eop)begin//不可以向wrfifo写入数据
wr_data_flag <= 1'b0;
end
end
always @(posedge clk_in or negedge rst_n)begin //把wr_finish从wrfifo的读侧同步到写侧
if(~rst_n)begin
wr_finish_r <= 0;
end
else begin
wr_finish_r <= {wr_finish_r[0],wr_finish};
end
end
/****************************************************************/
always @(posedge clk_out or negedge rst_n)begin
if(~rst_n)begin
rd_data <= 0;
rd_data_vld <= 1'b0;
end
else begin
rd_data <= rfifo_q;
rd_data_vld <= rfifo_rdreq;
end
end
wrfifo wrfifo_inst (
.aclr (~rst_n ),
.data (wfifo_data ),
.rdclk (clk ),
.rdreq (wfifo_rdreq),
.wrclk (clk_in ),
.wrreq (wfifo_wrreq),
.q (wfifo_q ),
.rdempty(wfifo_empty),
.rdusedw(wfifo_usedw),
.wrfull (wfifo_full )
);
assign wfifo_data = {din_eop,din_sop,din};
assign wfifo_wrreq = ~wfifo_full & din_vld & ((~wr_finish_r[1] & din_sop) ||wr_data_flag);
assign wfifo_rdreq = state_c == WRITE && ~avs_waitrequest;
rdfifo u_rdfifo(
.aclr (~rst_n ),
.data (rfifo_data ),
.rdclk (clk_out ),
.rdreq (rfifo_rdreq),
.wrclk (clk ),
.wrreq (rfifo_wrreq),
.q (rfifo_q ),
.rdempty (rfifo_empty),
.wrfull (rfifo_full ),
.wrusedw (rfifo_usedw)
);
assign rfifo_data = avs_rddata;
assign rfifo_wrreq = ~rfifo_full & avs_rddata_vld;
assign rfifo_rdreq = ~rfifo_empty & rdreq;
//输出
assign dout = rd_data;
assign dout_vld = rd_data_vld;
assign avm_wrdata = wfifo_q[15:0];
assign avm_write = ~(state_c == WRITE && ~avs_waitrequest);
assign avm_read = ~(state_c == READ && ~avs_waitrequest);
assign avm_addr = (state_c == WRITE)?{wr_bank[1],wr_addr[21:9],wr_bank[0],wr_addr[8:0]}
:((state_c == READ)?{rd_bank[1],rd_addr[21:9],rd_bank[0],rd_addr[8:0]}
:0);
endmodule
我收到这个错误:RuntimeError(自动加载常量Apps时检测到循环依赖当我使用多线程时。下面是我的代码。为什么会这样?我尝试多线程的原因是因为我正在编写一个HTML抓取应用程序。对Nokogiri::HTML(open())的调用是一个同步阻塞调用,需要1秒才能返回,我有100,000多个页面要访问,所以我试图运行多个线程来解决这个问题。有更好的方法吗?classToolsController0)app.website=array.join(',')putsapp.websiteelseapp.website="NONE"endapp.saveapps=Apps.order("
我有带有Logo图像的公司模型has_attached_file:logo我用他们的Logo创建了许多公司。现在,我需要添加新样式has_attached_file:logo,:styles=>{:small=>"30x15>",:medium=>"155x85>"}我是否应该重新上传所有旧数据以重新生成新样式?我不这么认为……或者有什么rake任务可以重新生成样式吗? 最佳答案 参见Thumbnail-Generation.如果rake任务不适合你,你应该能够在控制台中使用一个片段来调用重新处理!关于相关公司
导读:随着叮咚买菜业务的发展,不同的业务场景对数据分析提出了不同的需求,他们希望引入一款实时OLAP数据库,构建一个灵活的多维实时查询和分析的平台,统一数据的接入和查询方案,解决各业务线对数据高效实时查询和精细化运营的需求。经过调研选型,最终引入ApacheDoris作为最终的OLAP分析引擎,Doris作为核心的OLAP引擎支持复杂地分析操作、提供多维的数据视图,在叮咚买菜数十个业务场景中广泛应用。作者|叮咚买菜资深数据工程师韩青叮咚买菜创立于2017年5月,是一家专注美好食物的创业公司。叮咚买菜专注吃的事业,为满足更多人“想吃什么”而努力,通过美好食材的供应、美好滋味的开发以及美食品牌的孵
C#实现简易绘图工具一.引言实验目的:通过制作窗体应用程序(C#画图软件),熟悉基本的窗体设计过程以及控件设计,事件处理等,熟悉使用C#的winform窗体进行绘图的基本步骤,对于面向对象编程有更加深刻的体会.Tutorial任务设计一个具有基本功能的画图软件**·包括简单的新建文件,保存,重新绘图等功能**·实现一些基本图形的绘制,包括铅笔和基本形状等,学习橡皮工具的创建**·设计一个合理舒适的UI界面**注明:你可能需要先了解一些关于winform窗体应用程序绘图的基本知识,以及关于GDI+类和结构的知识二.实验环境Windows系统下的visualstudio2017C#窗体应用程序三.
需求:要创建虚拟机,就需要给他提供一个虚拟的磁盘,我们就在/opt目录下创建一个10G大小的raw格式的虚拟磁盘CentOS-7-x86_64.raw命令格式:qemu-imgcreate-f磁盘格式磁盘名称磁盘大小qemu-imgcreate-f磁盘格式-o?1.创建磁盘qemu-imgcreate-fraw/opt/CentOS-7-x86_64.raw10G执行效果#ls/opt/CentOS-7-x86_64.raw2.安装虚拟机使用virt-install命令,基于我们提供的系统镜像和虚拟磁盘来创建一个虚拟机,另外在创建虚拟机之前,提前打开vnc客户端,在创建虚拟机的时候,通过vnc
我正在尝试使用Ruby2.0.0和Rails4.0.0提供的API从imgur中提取图像。我已尝试按照Ruby2.0.0文档中列出的各种方式构建http请求,但均无济于事。代码如下:require'net/http'require'net/https'defimgurheaders={"Authorization"=>"Client-ID"+my_client_id}path="/3/gallery/image/#{img_id}.json"uri=URI("https://api.imgur.com"+path)request,data=Net::HTTP::Get.new(path
2022/8/4更新支持加入水印水印必须包含透明图像,并且水印图像大小要等于原图像的大小pythonconvert_image_to_video.py-f30-mwatermark.pngim_dirout.mkv2022/6/21更新让命令行参数更加易用新的命令行使用方法pythonconvert_image_to_video.py-f30im_dirout.mkvFFMPEG命令行转换一组JPG图像到视频时,是将这组图像视为MJPG流。我需要转换一组PNG图像到视频,FFMPEG就不认了。pyav内置了ffmpeg库,不需要系统带有ffmpeg工具因此我使用ffmpeg的python包装p
有这样的事吗?我想在Ruby程序中使用它。 最佳答案 试试这个http://csl.sublevel3.org/jp2a/此外,Imagemagick可能还有一些东西 关于ruby-是否有将图像文件转换为ASCII艺术的命令行程序或库?,我们在StackOverflow上找到一个类似的问题: https://stackoverflow.com/questions/6510445/
我正在使用Dragonfly在Rails3.1应用程序上处理图像。我正在努力通过url将图像分配给模型。我有一个很好的表格:{:multipart=>true}do|f|%>RemovePicture?Dragonfly的文档指出:Dragonfly提供了一个直接从url分配的访问器:@album.cover_image_url='http://some.url/file.jpg'但是当我在控制台中尝试时:=>#ruby-1.9.2-p290>picture.image_url="http://i.imgur.com/QQiMz.jpg"=>"http://i.imgur.com/QQ
我对图像处理完全陌生。我对JPEG内部是什么以及它是如何工作一无所知。我想知道,是否可以在某处找到执行以下简单操作的ruby代码:打开jpeg文件。遍历每个像素并将其颜色设置为fx绿色。将结果写入另一个文件。我对如何使用ruby-vips库实现这一点特别感兴趣https://github.com/ender672/ruby-vips我的目标-学习如何使用ruby-vips执行基本的图像处理操作(Gamma校正、亮度、色调……)任何指向比“helloworld”更复杂的工作示例的链接——比如ruby-vips的github页面上的链接,我们将不胜感激!如果有ruby-