本帖最后由 正点原子01 于 2020-8-28 17:16 编辑
1)实验平台:正点原子领航者ZYNQ开发板
2)平台购买地址:https://item.taobao.com/item.htm?&id=606160108761
3)全套实验源码+手册+视频下载地址:http://www.openedv.com/thread-301505-1-1.html
4)对正点原子FPGA感兴趣的同学可以加群讨论:905624739 点击加入群聊:
5)关注正点原子公众号,获取最新资料更新
第七章OV5640摄像头Sobel边缘检测
边缘检测是图像处理和计算机视觉中的基本问题,边缘检测的目的是标识数字图像中亮度变化明显的点。在本章我们将通过OV5640摄像头Sobel边缘检测实验,来学习如何使用Vivado HLS工具生成实现Sobel边缘检测算法的IP核,以及在Vivado中对综合结果进行验证的流程。 本章包括以下几个部分: 1.1 Sobel边缘检测简介 1.2 实验任务 1.3 HLS设计 1.4 IP验证 1.5 下载验证 1.1 Sobel边缘检测简介 所谓边缘是指其周围像素灰度急剧变化的那些象素的集合,它是图像最基本的特征。边缘存在于目标、背景和区域之间,所以,它是图像分割所依赖的最重要的依据。由于边缘是位置的标志,对灰度的变化不敏感,因此,边缘也是图像匹配的重要的特征。 边缘检测和区域划分是图像分割的两种不同的方法,二者具有相互补充的特点。在边缘检测中,是提取图像中不连续部分的特征,根据闭合的边缘确定区域。而在区域划分中,是把图像分割成特征相同的区域,区域之间的边界就是边缘。由于边缘检测方法不需要将图像逐个像素地分割,因此更适合大图像的分割。边缘大致可以分为两种,一种是阶跃状边缘,边缘两边像素的灰度值明显不同;另一种为屋顶状边缘,边缘处于灰度值由小到大再到小的变化转折点处。边缘检测的主要工具是边缘检测模板。边缘检测的有很多,典型的有索贝尔算子、普里维特算子、罗伯茨交叉边缘检测等边缘检测技术,在本章设计中采用的是索贝尔算子。 索贝尔算子算法简介: 索贝尔算子(Sobeloperator)主要用作边缘检测,在技术上,它是一离散性差分算子,用来运算图像亮度函数的灰度之近似值。在图像的任何一点使用此算子,将会产生对应的灰度矢量或是其法矢量。 Sobel卷积因子为:
该算子包含两组 3x3 的矩阵,分别为横向及纵向,将之与图像作平面卷积,即可分别得出横向及纵向的亮度差分近似值。如果以 A 代表原始图像, Gx 及 Gy 分别代表经横向及纵向边缘检测的图像灰度值,其公式如下:
图像的每一个像素的横向及纵向灰度值通过以下公式结合,来计算该点灰度的大小:
通常,为了提高效率 使用不开平方的近似值,但这样做会损失精度,迫不得已的时候可以如下这样子:
如果梯度G大于某一阀值,则认为该点(x,y)为边缘点。 1.2 实验任务 本节的实验任务是使用VivadoHLS工具设计实现Sobel边缘检测算法的IP核,并在Vivado中对设计出来的IP核进行验证。 1.3 HLS设计 我们在电脑中的“F:\ZYNQ\High_Level_Synthesis”目录下新建一个名为ov5640_sobel的文件夹,作为本次实验的工程目录。然后打开Vivado HLS工具,创建一个新的工程。设置工程名为“ov5640_sobel”,选择工程路径为刚刚创建的文件夹。需要注意的是,工程名以及路径只能由英文字母、数字和下划线组成,不能包含中文、空格以及其他特殊字符。如下图所示:
设置好工程名及路径之后,点击“Next”,进入如下界面设置顶层函数:
工程创建完成后,在工程面板中的“source”目录上点击右键,然后在打开的列表中选择“New File”新建源文件,在弹出的对话框中输入源文件的名称“ov5640_sobel.cpp”,如图1.3.3所示。源文件默认的保存路径为HLS工程目录,为方便源文件的管理,我们在工程目录下新建一个名为“src”的文件下,将源文件保存在src目录下。
我们输入的源文件的后缀名为“.cpp”,即使用C++语言进行设计。这是因为我们要使用Vivado HLS视频库,那么就只能使用C++来进行设计。 “ov5640_sobel.cpp”文件源代码如下: - #include "ov5640_sobel.h"
-
- voidov5640_sobel(AXI_STREAM&INPUT_STREAM,
- AXI_STREAM& OUTPUT_STREAM,
- introws,
- intcols
- ){
-
- #pragma HLSINTERFACE axis port=INPUT_STREAM
- #pragma HLS INTERFACE axis port=OUTPUT_STREAM
- #pragma HLS INTERFACE s_axilite port=rows
- #pragma HLS INTERFACE s_axilite port=cols
- #pragma HLS INTERFACE ap_ctrl_none port=return
- #pragma HLS dataflow
- //hls::mat格式变量
- RGB_IMAGE img_0(rows,cols);
- GRAY_IMAGE img_1(rows,cols);
- GRAY_IMAGE img_2(rows,cols);
- RGB_IMAGE img_3(rows,cols);
- //将AXI4 Stream数据转换成hls::mat格式
- hls::AXIvideo2Mat(INPUT_STREAM,img_0);
- //将RGB888格式的彩色数据转换成灰度数据
- hls::CvtColor<HLS_RGB2GRAY,HLS_8UC3,HLS_8UC1>(img_0,img_1);
- //将灰度数据与Sobel算子卷积
- 29 hls::Sobel<1,0,3>(img_1,img_2);
- 30
- 31 //将灰度数据转换成三个通道的灰度图像
- hls::CvtColor<HLS_GRAY2RGB,HLS_8UC1,HLS_8UC3>(img_2,img_3);
- //将hls::mat格式数据转换成AXI4 Stream格式
- hls::Mat2AXIvideo(img_3,OUTPUT_STREAM);
- }
复制代码 本章实验的实验代码与《领航者ZYNQ之HLS开发指南》第5章“OV5640摄像头灰度显示实验”类似,不同之处是本章实验代码增加了一个边缘检测函数“Sobel”。代码的其他部分可以参考《领航者ZYNQ之HLS开发指南》第5章“OV5640摄像头灰度显示实验”。 在代码的第29行调用HLS命名空间中的“Sobel”函数,这个函数在UG902中的介绍如下图所示:
图中的“Synopsis”是概要的意思。从图中可以看出Vivado HLS视频库中定义了两个Soebl函数,这个是C++中函数重载的概念,编译器根据函数模板输入参数的不同,选择调用相应的函数。图中的“template”是函数模板的概念,它通过将类型作为参数传递给模板,可使编译器生成该类型的函数。图中 的“XORDER”和“YORDER”这两个参数可以设置采用水平方向或垂直方向的Sobel算子,Vivado HLS只支持“XORDER=1,YORDER=0”(指采用水平方向的Sobel算子)或者“XORDER=0,YORDER=1”(指采用垂直方向的Sobel算子)的设置。在本章实验中我们设置“XORDER=1,YORDER=0”表示采用水平方向的Sobel算子进行X轴方向的边缘检测。图中的“SIZE”表示设置Sobel算子模板的大小,Vivado HLS支持设置Sobel算子的大小为3、5、7。本章实验我们设置Sobel算法的大小为3,表示采用3*3的Sobel算子模板。图中的“BORDERMODE”表示设置边界的类型,由于本章我们没有传入边界类型的参数,所以编译器调用了第二个Sobel函数。图中的“SRC_T”表示输入图像的类型,图中的“DST_T”表示输出图像的类型,这里输入和输出图像都是8位无符号单通道数据类型。图中的“ROWS”和“COLS”表示输入图像像素的行和列,图中的“DROWS”和“DCOLS”表示输出图像像素的行和列。 “ov5640_sobel.h”头文件代码如下: - #ifndef _OV5640_SOBEL_H
- #define _OV5640_SOBEL_H
- #include "hls_video.h"
- #define MAX_HEIGHT 1080 //图像最大高度
- #define MAX_WIDTH 1920 //图像最大宽度
- #define INPUT_IMAGE "lena.jpg"
- #define OUTPUT_IMAGE "lena_sobel.jpg"
- typedef hls::stream<ap_axiu<24,1,1,1> > AXI_STREAM;
- typedef hls::Mat<MAX_HEIGHT,MAX_WIDTH,HLS_8UC3> RGB_IMAGE;
- typedef hls::Mat<MAX_HEIGHT,MAX_WIDTH,HLS_8UC1> GRAY_IMAGE;
- void ov5640_sobel(AXI_STREAM& INPUT_STREAM,
- AXI_STREAM& OUTPUT_STREAM,
- int rows,
- int cols
- );
- #endif
复制代码 在代码的第1行和第2行是为了防止头文件的重复定义。在代码的第4行引入了“hls_opencv.h”头文件是为了使用Vivado HLS工具给我们提供的数据类型。在代码的第6行和代码的第7行定义了图像的最大高度和宽度。在代码的第9行定义了输入图像是“lena.jpg”,在代码的第10行定义了输出图像是“lena_sobel.jpg”。在代码的第12行定义了AXI_STREAM格式的数据类型,第一个参数24表示我们采用的数据是24位宽。在代码的第13行和第14行分别定义了“RGB_IMAGE”和“GRAY_IMAGE”格式的数据类型。在代码的第16行“ov5640_sobel”是函数的声明。 当工程比较复杂的时候,可以进行C代码仿真。C代码仿真的步骤如下:在工程面板中的“Test Bench”目录上点击右键,在打开的列表中选择“New File”新建源文件,然后在弹出的对话框中输入测试文件的名称“ov5640_sobel_tb.cpp”,为方便测试文件的管理我们将测试文件保存在src目录下,如下图所示:
“ov5640_sobel_tb.cpp”程序如下所示: - #include "ov5640_sobel.h"
- #include "hls_opencv.h"
-
- intmain(void)
- {
- //获取图像数据
- IplImage*src = cvLoadImage(INPUT_IMAGE);
- IplImage*dst = cvCreateImage(cvGetSize(src),src->depth, src->nChannels);
-
- //使用HLS库进行处理
- AXI_STREAMsrc_axi, dst_axi;
- IplImage2AXIvideo(src, src_axi);
- ov5640_sobel(src_axi,dst_axi, src->height,src->width);
- AXIvideo2IplImage(dst_axi, dst);
- //保存图像
- cvSaveImage(OUTPUT_IMAGE,dst);
- //显示图像
- cvShowImage(INPUT_IMAGE,src);
- cvShowImage(OUTPUT_IMAGE,dst);
- //等待用户按下键盘上的任一按键
- cv::waitKey(0);
- }
复制代码 在代码的第2行引入了“hls_opencv.h”,这个头文件里面包含了opencv库。在代码的第4行定义了main函数,在代码的第7行调用“cvLoadImage”函数来载入输入图像。在代码的第8行 调用“cvCreateImage”来创建输出图像,参数“cvGetSize(src)”表示创建和输入图像一样大小的输出图像,参数“src->channel”表示创建的输出图像的通道和输入图像一样也是三通道 的。在代码的第11行定义了两个变量“src_axi”和“dst_axi”,这两个变量的数据类型是AXI_STREAM格式。在代码的第12行通过调用IplImage2AXIvideo函数来将opencv中的IplImage格式的图像数据类型转换成AXI4-Stream格式的图像数据流从而可以利用FPGA进行数据处理。在代码的第13行调用“ov5640_sobel”函数来将对图像进行sobel边缘处理。在代码的第14行调用 AXIvideo2IplImage将Sobel边缘处理后的图像转换成opencv可以处理的数据类型。在代码的第17行通过调用cvSaveImage来保存Sobel边缘处理后的图像。在代码的第20行和第21行通过调用cvShowImage来显示原图像和边缘处理后的图像。在代码的第24行通过调用waitKey这个函数来等待用户输入,从而避免程序运行完成,图像一闪而过。 下面进行C仿真,将“lena.jpg”图像加入到测试平台,在工程面板中的“Test bench”目录上点击右键,然后在打开的列表中选择“New File”新建源文件,为方便测试文件的管理我们将“lena.jpg”保存在src目录下,如下图所示:
点击“Run CSimulation”按钮,如下图所示:
上图中左边红框标出来的就是我们加入的输入图像,右边红框即为C仿真按钮。仿真结果如下图所示: 从仿真结果中可以看出我们实现了X轴方向的Sobel边缘检测。 接下来点击工具栏中向右的绿色三角形对C代码进行综合,如下图所示:
综合完成后,会自动打开综合结果(solution)的报告,综合报告中给出了设计的性能评估、资源评估以及接口等信息。本次实验中,我们重点关注综合工具为我们生成的接口信息,如下图所示:
从图中可以看出,设计综合出了一个“AXI4-Lite”从接口和两个“AXI4-Stream”接口(一个输入,一个输出)。其中“AX4-Lite”总线接口用于控制视频处理的分辨率,而两个“AXI4-Stream”总线接口分别用于输入待处理的视频以及输出处理之后的视频流。 在综合结果正确的情况下,在工具栏中点击黄色的“田”字按钮,导出RTL。 在导出RTL结束之后,我们到工程目录所指向的文件夹中可以看到以ZIP压缩文件形式存在的IP核,如下图所示:
HLS设计结束之后,我们将在Vivado中对导出的IP核进行验证。 1.4 IP验证 在IP验证环节,我们会使用Vivado工具的IP集成器将生成的IP核添加到Block Design中,然后完成设计后将程序下载到领航者开发板上进行验证。 用于IP验证的底层硬件可以在《领航者ZYNQ之HLS开发指南》第5章“OV5640 摄像头灰度显示”实验的基础上进行。打开该实验所对应的Vivado工程“ov5640_rgb2gray_ip_test”,将其另存为“ov5640_sobel_ip_test”工程。为了方便工程管理,我们将Vivado工程的目录与HLS工程目录保持一致,如下图所示:
在通过“另存为”的方式保存工程之后,还要将原来工程中的IP库(名为ip_repo的文件夹)复制到新的Vivado工程目录下, 然后将HLS设计过程中导出的IP核拷贝到“ip_repo”目录下并解压,解压完成后如下图所示:
在Vivado中重新将当前工程目录下的ip_repo文件夹添加到工程的IP库中,然后将HLS生成的IP核Ov5640_sobel添加到Block Design中,并将其STREAM接口分别连接到Video In to AXI4-Stream模块的video_out接口与VDMA模块的S_AXIS_S2MM接口上。最后点击上图中左上角的“Run Connection Automation”,让工具自动连接该IP核的其他端口,包括时钟、复位以及AXI-Lite从接口,[url=]最终[/url] [A1] 完成的设计如下图所示:
到这里我们的BlockDesign就设计完成了,在Diagram窗口空白处右击,然后选择“Validate Design”验证设计。验证完成后弹出对话框提示“ValidationSuccessful”表明设计无误,点击“OK”确认。最后按快捷键“Ctrl + S”保存设计。 接下来在Source窗口中右键点击Block Design设计文件“system.bd”,然后依次执行“Generate Output Products”和“Create HDLWrapper”。 最后在左侧FlowNavigator导航栏中找到PROGRAM AND DEBUG,点击该选项中的“Generate Bitstream”,对设计进行综合、实现、并生成Bitstream文件。 在生成 Bitstream 之后,在菜单栏中选择 File > Export > Export hardware 导出硬件,并在弹出的对话框 中,勾选“Include bitstream”。然后在菜单栏选择 File >Launch SDK,启动 SDK 软件。 在Vivado SDK中新建空的应用工程,工程名为“ov5640_sobel_lcd”。 然后找到《领航者ZYNQ之嵌入式开发指南》第二十三章“OV5640 摄像头 LCD 显示”实验的Vivado工程目录,将“21_ov7725_lcd\ov7725_lcd.sdk\ov7725_lcd”目录下的src文件夹拷贝到新建的应用工程目录下。 在SDK中刷新src目录,然后将“main.c”的代码修改为如下所示: - #include <stdio.h>
- #include <stdlib.h>
- #include <string.h>
- #include "xil_types.h"
- #include "xil_cache.h"
- #include "xparameters.h"
- #include "xgpio.h"
- #include "xaxivdma.h"
- #include "xaxivdma_i.h"
- #include "display_ctrl/display_ctrl.h"
- #include "vdma_api/vdma_api.h"
- #include "emio_sccb_cfg/emio_sccb_cfg.h"
- #include "ov5640/ov5640_init.h"
- #include "xov5640_sobel.h"
- //宏定义
- #define BYTES_PIXEL 3 //像素字节数,RGB888占3个字节
- #define FRAME_BUFFER_NUM 3 //帧缓存个数3
- #define DYNCLK_BASEADDR XPAR_AXI_DYNCLK_0_BASEADDR //动态时钟基地址
- #define VDMA_ID XPAR_AXIVDMA_0_DEVICE_ID //VDMA器件ID
- #define DISP_VTC_ID XPAR_VTC_0_DEVICE_ID //VTC器件ID
- //PL端 AXI GPIO 0(lcd_id)器件 ID
- #define AXI_GPIO_0_ID XPAR_AXI_GPIO_0_DEVICE_ID
- //使用AXI GPIO(lcd_id)通道1
- #define AXI_GPIO_0_CHANEL 1
- //全局变量
- //frame buffer的起始地址
- unsigned int const frame_buffer_addr = (XPAR_PS7_DDR_0_S_AXI_BASEADDR
- + 0x1000000);
- XAxiVdma vdma;
- DisplayCtrl dispCtrl;
- XGpio axi_gpio_inst; //PL端 AXI GPIO 驱动实例
- XOv5640_sobel sobel_inst; //PL端XOv5640_sobel驱动实例
- VideoMode vd_mode;
- unsigned int lcd_id;
- int main(void)
- {
- u32 status;
- u16 cmos_h_pixel; //ov56 DVP 输出水平像素点数
- u16 cmos_v_pixel; //ov5640 DVP 输出垂直像素点数
- u16 total_h_pixel; //ov5640 水平总像素大小
- u16 total_v_pixel; //ov5640 垂直总像素大小
- //获取LCD的ID
- XGpio_Initialize(&axi_gpio_inst, AXI_GPIO_0_ID);
- lcd_id = LTDC_PanelID_Read(&axi_gpio_inst,AXI_GPIO_0_CHANEL);
- xil_printf("lcd_id= %x\n\r",lcd_id);
- //根据获取的LCD的ID号来进行ov5640显示分辨率参数的选择
- switch(lcd_id){
- case 0x4342 : //4.3寸屏,480*272分辨率
- cmos_h_pixel = 480;
- cmos_v_pixel = 272;
- total_h_pixel = 1800;
- total_v_pixel = 1000;
- break;
- case 0x4384 : //4.3寸屏,800*480分辨率
- cmos_h_pixel = 800;
- cmos_v_pixel = 480;
- total_h_pixel = 1800;
- total_v_pixel = 1000;
- break;
- case 0x7084 : //7寸屏,800*480分辨率
- cmos_h_pixel = 800;
- cmos_v_pixel = 480;
- total_h_pixel = 1800;
- total_v_pixel = 1000;
- break;
- case 0x7016 : //7寸屏,1024*600分辨率
- cmos_h_pixel = 1024;
- cmos_v_pixel = 600;
- total_h_pixel = 2200;
- total_v_pixel = 1000;
- break;
- case 0x1018 : //10.1寸屏,1280*800分辨率
- cmos_h_pixel = 1280;
- cmos_v_pixel = 800;
- total_h_pixel = 2570;
- total_v_pixel = 980;
- break;
- default :
- cmos_h_pixel = 480;
- cmos_v_pixel = 272;
- total_h_pixel = 1800;
- total_v_pixel = 1000;
- break;
- }
- emio_init(); //初始化EMIO
- status = ov5640_init( cmos_h_pixel, //初始化ov5640
- cmos_v_pixel,
- total_h_pixel,
- total_v_pixel);
- if(status == 0)
- xil_printf("OV5640detected successful!\r\n");
- else
- xil_printf("OV5640detected failed!\r\n");
- //根据获取的LCD的ID号来进行video参数的选择
- switch(lcd_id){
- case 0x4342 : vd_mode =VMODE_480x272; break; //4.3寸屏,480*272分辨率
- case 0x4384 : vd_mode =VMODE_800x480; break; //4.3寸屏,800*480分辨率
- case 0x7084 : vd_mode =VMODE_800x480; break; //7寸屏,800*480分辨率
- case 0x7016 : vd_mode =VMODE_1024x600; break; //7寸屏,1024*600分辨率
- case 0x1018 : vd_mode =VMODE_1280x800; break; //10.1寸屏,1280*800分辨率
- default : vd_mode = VMODE_800x480; break;
- }
- //初始化灰度转换IP核OV5640_sobel
- XOv5640_sobel_Initialize(&sobel_inst, XPAR_OV5640_SOBEL_0_DEVICE_ID);
- //配置灰度转换IP核OV5640_sobel的行数
- XOv5640_sobel_Set_rows(&sobel_inst, vd_mode.height);
- //配置灰度转换IP核OV5640_sobel的列数
- XOv5640_sobel_Set_cols(&sobel_inst, vd_mode.width);
- //配置VDMA
- run_vdma_frame_buffer(&vdma, VDMA_ID, vd_mode.width, vd_mode.height,
- frame_buffer_addr,0,0,BOTH);
- //初始化Display controller
- DisplayInitialize(&dispCtrl, DISP_VTC_ID, DYNCLK_BASEADDR);
- //设置VideoMode
- DisplaySetMode(&dispCtrl, &vd_mode);
- DisplayStart(&dispCtrl);
- return 0;
- }
复制代码 在代码的第14行引入了“xov5640_sobel.h”头文件,这个头文件是Vivado HLS工具生成的,里面声明了灰度转换IP核的驱动函数。首先在代码的34行定义了边缘检测IP核的驱动实例sobel_inst,该变量会在后面对IP核进行配置时用到。然后在代码的第112行通过“XOv5640_sobel_Initialize()”函数来初始化Vivado HLS生成的边缘检测IP核;在代码的第116行通过传入“vd_mode.height”形参来设置灰度转换IP核的行数,在代码的第114行通过传入“vd_mode.width”形参来设置列数。这些数据类型和函数在“xlcd_rgb_color.h”头文件中均有声明。 1.5 下载验证 编译完工程之后我们就可以开始下载程序了。将 OV5640 摄像头模块插在领航者 Zynq 开发板的“OLED/CAMERA”插座上,并将 LCD 的排线接头插入开发板上的 LCD 接线座。将下载器一端连电脑,另一端与开发板上的 JTAG 端口连接,连接电源线并打开电源开关。 在 SDK 软件下方的 SDK Terminal 窗口中点击右上角的加号设置并连接串口。然后下载本次实验硬件设计过程中所生成的 BIT 文件,来对 PL 进行配置。最后下载软件程序,下载完成后在LCD上就可以看到摄像头采集的彩色图像被转换成了X方向的灰度图像,如下图所示:
|