正点原子 发表于 2020-9-27 10:44:54

【正点原子FPGA连载】第十一章基于OV5640的自适应二值化实验--摘自【正点原子】领航者ZYNQ之HLS 开发指南

1)实验平台:正点原子领航者ZYNQ开发板
2)购买链接:https://item.taobao.com/item.htm?id=606160108761
3)全套实验源码+手册+视频下载地址:http://www.openedv.com/docs/boards/fpga/zdyz_linhanz.html
4) 正点原子官方B站:https://space.bilibili.com/394620890
5)对正点原子FPGA感兴趣的同学可以加群讨论:876744900 点击加入:                                                                                                                
6)关注正点原子公众号,获取最新资料



第十一章基于OV5640的自适应二值化实验


在数字图像分析与处理中,一幅图片通常含有人们感兴趣和不感兴趣的两部分。为了提取或突出人们感兴趣的目标,常用的方法就是对图像进行二值化分割。二值化的方法有很多,其中自适应二值化(OTSU)是图像二值化最常用的一种算法。本章我们将在HLS中实现图像的自适应二值化。
本章包括以下几个部分:
1111.1简介
11.2实验任务
11.3HLS设计
11.4IP验证
11.5下载验证


11.1简介
图像二值化(Image Binarization)就是将图像上的像素点的灰度值设置为最大(白色)或最小(黑色),也就是将整个图像呈现出明显的黑白效果的过程。这里我们以8bit表示的灰度图像为例(灰度值的范围为0~255),二值化就是通过选取适当的阈值,与图像中的256个灰度等级进行比较。亮度高于阈值的像素点设置为白色(255),低于阈值的像素点设置为黑色(0),从而明显地反映出图像的整体和局部特征。
我们把图像进行二值化是为了将感兴趣目标和背景分离,在数字识别和指纹识别等场景中,二值化应用非常广泛。

图 11.1.1 指纹识别
在指纹识别中,由于我们只关注图像的指纹信息,所以我们需要将背景信息全部清除掉,如上图所示,图像经过二值化处理后只保留了关键的指纹信息。      
二值化在数字图像处理中占有非常重要的地位,特别是在实时的图像处理中,通过二值图像处理实现而构成的系统是很多的。要进行二值图像的处理与分析,首先要把灰度图像二值化,得到二值化图像,这样有利于在对图像做进一步处理时,图像的集合性质只与像素值为0或255的点的位置有关,不再涉及像素的多级值,使处理变得简单,而且数据的处理和压缩量小。为了得到理想的二值图像,一般采用封闭、连通的边界定义不交叠的区域。所有灰度大于或等于阈值的像素被判定为属于特定物体,其灰度值为255表示,否则这些像素点被排除在物体区域以外,灰度值为0,表示背景或者例外的物体区域。
      实现二值化有两种方法,一种是手动指定一个阈值,通过阈值来进行二值化处理;另一种是一个自适应阈值二值化方法(OTSU算法等)。使用第一种方法计算量小速度快,但是在处理不同图像时颜色分布差别很大;使用第二种方法适应性强,能直接观测处图像的轮廓,但相对计算更复杂。当图像的直方图分布没有明显的低峰和高峰时,直接指定阈值可能会导致图像中某些像素的关键信息丢失,所以我们在本章采用第二种方法来实现图像的二值化。
      OTSU算法计算出来的阈值可以使图像的前景目标区域的像素的平均灰度值、背景区域像素的平均灰度值与整幅图像像素的平均灰度值之间的差别最大,这种差异使用方差来表示的,故名最大类间方差法,该方法由日本学者大津于1978年提出,故又名大津法。
      OTSU算法的原理非常简单,该算法假定一幅图像根据双模直方图(前景图像像素和背景图像像素)被分为两类像素,它要计算能将两类分开的最佳阈值,使得它们的类间方差最大,也就是前景图像和背景图像间像素的离散程度最大。OTSU算法类间方差的定义如下:

图 11.1.2 类间方差公式
如图所示,其中w0表示前景图像像素出现的概率,u0表示前景图像像素的平均灰度,w1表示背景图像像素出现的概率,u1表示背景图像像素的平均灰度,u表示整幅图像像素的平均灰度。
为了实现找到使类间方差最大的阈值这个目标。我们需要遍历0~255个灰度等级,这每一个灰度等级都将图像分为两类像素(前景图像像素和背景图像像素)。然后在每一个灰度等级下算出前景图像像素的平均值(u0)和出现概率(w0)以及背景图像像素的平均值(u1)和出现概率(u),同时我们还要计算出整幅图像的平均灰度(u)。最后代入上面的公式计算出类间方差σ2。我们就是要找到一个灰度值得这个类间方差最大,然后将这个灰度值作为图像二值化的阈值。
      下面给出一幅灰度图像以及它做了指定阈值和OTSU自适应二值化之后的图像来加深我们对OTSU自适应二值化的理解:

图 11.1.3 图像自适应二值化
如图所示,左边的这幅图像是灰度图,中间的这副图像是我们手动指定阈值操作的二值化图像,右边的这副图像是灰度图做了自适应二值化算法处理后的图像,可以发现图像经过自适应二值化计算出来的阈值比手动指定阈值二值化的效果更好。
11.2实验任务
本节的实验任务是使用Vivado HLS设计OTSU自适应二值化的IP核,并在Vivado中对设计出来的IP核进行验证。
11.3HLS设计
我们在电脑中的“F:\ZYNQ\High_Level_Synthesis”目录下新建一个名为otsu_threshold的文件夹,作为本次实验的工程目录。然后打开Vivado HLS工具,创建一个新的工程。设置工程名为“otsu_threshold”,选择工程路径为刚刚创建的文件夹。需要注意的是,工程名以及路径只能由英文字母、数字和下划线组成,不能包含中文、空格以及其他特殊字符。如下图所示:

图 11.3.1 工程配置界面
设置好工程名及路径之后,点击“Next”,进入如下界面设置顶层函数:

图 11.3.2 设置顶层函数
工程创建完成后,在工程面板中的“source”目录上点击右键,然后在打开的列表中选择“New File”新建源文件,在弹出的对话框中输入源文件的名称“otsu_threshold.cpp”,如图1.3.3所示。源文件默认的保存路径为HLS工程目录,为方便源文件的管理,我们在工程目录下新建一个名为“src”的文件下,将源文件保存在src目录下。

图 11.3.3 输入源文件名
我们在这里使用C++语言来设计,那么后缀名需要设置为“.cpp”。设置好文件名和路径之后,点击“保存”。
“otsu_threshold.cpp”文件源代码如下:
1#include "otsu_threshold.h"
2
3void ov5640_otsu_threshold(AXI_STREAM & INPUT_STREAM,
4                           AXI_STREAM & OUTPUT_STREAM,
5                           int rows,
6                           int cols
7){
8
9#pragma HLS INTERFACE axis port=INPUT_STREAM
10 #pragma HLS INTERFACE axis port=OUTPUT_STREAM
11 #pragma HLS INTERFACE s_axilite port=rows
12 #pragma HLS INTERFACE s_axilite port=cols
13 #pragma HLS INTERFACE ap_ctrl_none port=return
14 #pragma HLS dataflow
15
16   //hls::mat 格式变量
17   RGB_IMAGE img_0(rows,cols);
18   GRAY_IMAGE img_1(rows,cols);
19   GRAY_IMAGE img_2(rows,cols);
20   RGB_IMAGE img_3(rows,cols);
21
22   //将 AXI4 Stream 数据转换成 hls::mat 格式
23   hls::AXIvideo2Mat(INPUT_STREAM,img_0);
24
25   //将 RGB888 格式的彩色数据转换成灰度数据
26   hls::CvtColor<HLS_RGB2GRAY,HLS_8UC3,HLS_8UC1>(img_0,img_1);
27
28   //对灰度图像进行OTSU自适应二值化
29   hls::Otsu_threshold<HLS_8UC1,HLS_8UC1,MAX_HEIGHT,MAX_WIDTH>(img_1,img_2);
30
31   //将灰度数据转换成三个通道的灰度图像
32   hls::CvtColor<HLS_GRAY2RGB,HLS_8UC1,HLS_8UC3>(img_2,img_3);
33
34   //将 hls::mat 格式数据转换成 AXI4 Stream 格式
35   hls::Mat2AXIvideo(img_3,OUTPUT_STREAM);
36 }
代码的主体部分与《基于OV5640的直方图均衡实验》非常类似,只是在代码的第29行我们使用的是hls::Otsu_threshold这个函数来实现自适应二值化算法。这个函数在Vivado HLS视频库中没有相关定义,需要我们自己实现。
“otsu_threshold.h”代码如下:
1   #ifndef OTSU_THRESHOLD_H
2   #define OTSU_THRESHOLD_H
3   
4   #include "hls_math.h"
5   #include "hls_video.h"
6   
7   namespace hls {
8   
9   template<int SRC_T, int DST_T,int ROW, int COL>
10void Threshold(
11      Mat<ROW, COL, SRC_T>    &_src,
12      Mat<ROW, COL, DST_T>    &_dst,
13      ap_uint<8> &threshold)
14{
15      const int NUM_STATES=4;
16      Window<1,NUM_STATES,ap_uint<8> > addr_win;
17
18      ap_uint<BitWidth<ROW*COL>::Value> hist_out;
19      Window<1,NUM_STATES,ap_uint<BitWidth<ROW*COL>::Value> > hist_win;
20
21      ap_uint<BitWidth<ROW*COL>::Value> hist;
22      ap_uint<8> addr;
23      ap_uint<8> addr_last;
24      ap_uint<BitWidth<ROW*COL>::Value> hist_last;
25      ap_uint<8> addr_flag;
26      ap_uint<BitWidth<ROW*COL>::Value> hist_flag;
27      ap_uint<8> addr_w;
28      ap_uint<BitWidth<ROW*COL>::Value> hist_w;
29
30      ap_uint<BitWidth<ROW*COL>::Value> tmp=0;
31
32      float pixel_probability;
33      for(int i=0;i<NUM_STATES;i++) {
34      #pragma HLS UNROLL
35          addr_win(0,i)=i;
36          hist_win(0,i)=0;
37      }
38
39      for(int i=0;i<256;i++) {
40          hist_out=0;
41          pixel_probability = 0.0f;
42      }
43
44      int cols=_src.cols;
45      int rows=_src.rows;
46      assert(rows<=ROW);
47      assert(cols<=COL);
48   loop_height: for(int i=0;i<rows;i++)
49      {
50      loop_width: for(int j=0;j<cols;j++)
51          {
52#pragma HLS PIPELINE
53#pragma HLS LOOP_FLATTEN OFF
54#pragma HLS DEPENDENCE array inter false
55            ap_uint<4> flag=NUM_STATES;
56            HLS_TNAME(SRC_T) tempsrc=0;
57            HLS_TNAME(DST_T) tempdst=0;
58            _src.data_stream.read(tempsrc);
59            tempdst=tempsrc > threshold ? 255 : 0;
60            _dst.data_stream<<tempdst;
61
62            for (int m=0; m<NUM_STATES; m++) {
63                  if (tempsrc==addr_win(0,m)) {
64                      flag = m;
65                      break;
66                  }
67            }
68
69            latency_region:{
70            #pragma HLS latency min=0 max=1
71            addr_last = addr_win(0,NUM_STATES-1);
72            hist_last = hist_win(0,NUM_STATES-1)+1;
73            for (int m=NUM_STATES-1; m>0; m--) {
74                  addr = addr_win(0,m-1);
75                  hist = hist_win(0,m-1);
76                  if (m==NUM_STATES/2) {
77                      addr_w = addr;
78                      if (m==flag+1) {
79                        hist_w = hist+1;
80                      } else {
81                        hist_w = hist;
82                      }
83                  }
84                  if (m==flag+1) {
85                      addr_flag = addr;
86                      hist_flag = hist+1;
87                      addr_win(0,m) = addr_flag;
88                      hist_win(0,m) = hist_flag;
89                  } else {
90                      addr_win(0,m) = addr;
91                      hist_win(0,m) = hist;
92                  }
93            }
94            if (flag==NUM_STATES) {
95                  hist_win(0,0) = hist_out+1;
96                  addr_win(0,0) = tempsrc;
97            } else if (flag==NUM_STATES-1) {
98                  addr_win(0,0) = addr_last;
99                  hist_win(0,0) = hist_last;
100             } else if (flag>=NUM_STATES/2) {
101               addr_win(0,0) = addr_flag;
102               hist_win(0,0) = hist_flag;
103             } else {
104               addr_win(0,0) = addr_w;
105               hist_win(0,0) = hist_w;
106             }
107             hist_out = hist_w;
108             }
109         }
110   }
111
112   for (int m=0; m<NUM_STATES/2; m++) {
113   #pragma HLS PIPELINE
114         hist_out=hist_win(0,m);
115   }
116
117   int         front_pixel_count;          //前景图像像素个数
118   int         back_pixel_count;         //背景图像像素个数
119   float       front_pixel_probability;    //前景图像像素出现的概率
120   float       back_pixel_probability;   //背景图像像素出现的概率
121   int         front_gray_count;         //前景灰度总和
122   int         back_gray_count;            //背景灰度总和
123   int         total_gray;               //整幅图像灰度总和
124   float       front_gray_average;         //前景平均灰度
125   float       back_gray_average;          //背景平均灰度
126   float       total_gray_average;         //整幅图像的平均灰度
127   int         threshold_tmp;            //临时阈值
128   float       interclass_variance_tmp;    //临时类间方差
129   float       interclass_variance_max;    //最大类间方差
130   for(threshold_tmp = 0; threshold_tmp < 256; threshold_tmp++){
131
132         front_pixel_count = 0;
133         back_pixel_count = 0;
134         front_pixel_probability = 0;
135         back_pixel_probability = 0;
136         front_gray_count = 0;
137         back_gray_count = 0;
138         front_gray_average = 0;
139         back_gray_average = 0;
140         total_gray_average = 0;
141
142         for(int j = 0; j < 256; j++){
143             //前景部分
144             if(j <= threshold_tmp){
145               //以threshold_tmp为阈值分类,计算前景图像像素出现的个数和灰度总和
146               front_pixel_count += hist_out;
147               front_gray_count += j * hist_out;
148             }
149             //背景部分
150             else{
151               //以threshold_tmp为阈值分类,计算背景图像像素出现的个数和灰度总和
152               back_pixel_count += hist_out;
153               back_gray_count += j * hist_out;
154             }
155         }
156
157         //前景图像像素出现的概率
158         front_pixel_probability = (float)front_pixel_count / (rows*cols);
159         //背景图像像素出现的概率
160         back_pixel_probability = (float)back_pixel_count / (rows*cols);
161         //整幅图像灰度总和
162         total_gray = front_gray_count + back_gray_count;
163         //前景平均灰度
164         front_gray_average = (float)front_gray_count / front_pixel_count;
165         //背景平均灰度
166         back_gray_average = (float)back_gray_count / back_pixel_count;
167         //整幅图像平均灰度
168         total_gray_average = (float)total_gray / (rows*cols);
169
170         //计算类间方差
171         interclass_variance_tmp = front_pixel_probability *
172                                 (front_gray_average - total_gray_average) *
173                                 (front_gray_average - total_gray_average)
174                                 + back_pixel_probability *
175                                 (back_gray_average - total_gray_average) *
176                                 (back_gray_average - total_gray_average);
177         //找出最大类间方差以及对应的阈值
178         if (interclass_variance_tmp > interclass_variance_max){
179             interclass_variance_max = interclass_variance_tmp;
180             threshold = threshold_tmp;
181         }
182   }
183 }
184
185 //otsu自适应二值化函数
186 staticap_uint<8> threshold;
187 template<int SRC_T, int DST_T,int ROW, int COL>
188 void Otsu_threshold(
189         Mat<ROW, COL, SRC_T>    &_src,
190         Mat<ROW, COL, DST_T>    &_dst)
191   {
192   #pragma HLS INLINE
193         Threshold(_src, _dst, threshold);
194   }
195 }
196
197 #define MAX_HEIGHT 800 //图像最大高度
198 #define MAX_WIDTH 1024 //图像最大宽度
199 typedef hls::stream<ap_axiu<24,1,1,1> > AXI_STREAM;
200 typedef hls::Mat<MAX_HEIGHT,MAX_WIDTH,HLS_8UC3> RGB_IMAGE;
201 typedef hls::Mat<MAX_HEIGHT,MAX_WIDTH,HLS_8UC1> GRAY_IMAGE;
202
203 void ov5640_otsu_threshold(AXI_STREAM & INPUT_STREAM,
204                            AXI_STREAM & OUTPUT_STREAM,
205                            int rows,
206                            int cols
207 );
208
209 #endif
实现OTSU算法,需要先统计图像各个像素值出现的个数,也就是统计图像的直方图。统计直方图这部分代码我们参考Vivado HLS视频库中直方图均衡化中的部分源码。下面简单讲解一下这个源码的实现思路。
我们想要获取一幅图像的直方图,就需要定义一个数组,这个数组可以保存每个像素值出现的个数。其中数组的地址代表的是图像像素的灰度等级0~255,数组里存储的就是每一个灰度等级出现的个数,在代码的第18行就定义了“hist_out”这样的一个数组。我们在获取到输入图像像素的灰度值后,把这个灰度值作为数组的地址,从数组中取出数据,并将得到的数据加1后再写回数组对应的地址。这样一帧图像输入完成后,数组中就存储了整幅图像的直方图信息,即实现了直方图的统计。
在获取一帧图像的直方图之后,需要根据图像的直方图来计算类间方差。在计算类间方差的时候,我们需要缓存后续输入的图像数据,为计算上一帧图像的OTSU阈值留出时间。代码的第16行“addr_win”缓存了图像像素的灰度值,代码的第19行“hist_win”缓存了图像像素出现的个数,这里NUM_STATUS定义成4表示我们缓存的大小是4。
代码的第58行是读取输入像素的灰度值并存储到“tempsrc”中,代码的第59行是通过三目运算符,判断输入图像像素值与阈值之间的大小,如果输入图像像素值比阈值大,则将输出图像像素设置为255,否则将输出图像像素设置为0,这样就实现了图像的二值化操作。
在代码的第130行到第180行就是通过OTSU算法来计算图像的阈值。我们首先遍历灰度级0~255,针对每一个灰度,把它作为阈值。如代码的第130行所示:“threshold_tmp”这个变量就是我们设置的这个阈值。然后以这个阈值分类,分别计算前景图像像素和背景图像像素出现的个数和灰度总和。这里计算灰度总和的方式就是拿每个像素的灰度级乘以它出现的个数然后做一个累加。如代码第144到第155行所示:“j”代表的是前景或背景图像像素的灰度值,“hist_out”数组里存储的是每个灰度级出现的个数。接下来我们就需要根据简介里所介绍的类间方差公式,来分别计算公式中各个符号的值。如下图所示:

图 11.3.4 类间方差公式
其中“w0”代表的是前景图像像素出现的概率,对应于代码的第158行“front_pixel_probability”这个变量。我们在这里是拿前景图像像素出现的个数除以一帧像素的个数来计算前景图像像素出现的概率;“u0”代表的是前景图像像素的平均灰度,对应于代码的第164行“front_gray_average”这个变量。我们在这里是通过前景图像像素的灰度总和除以前景图像像素出现的个数来计算前景图像像素的平均灰度。“u”代表的是整幅图像的平均灰度,对应于代码的第168行“total_gray_average”这个变量。我们在这里是通过整幅图像像素的灰度总和除以一帧像素的个数来得到的。在代码的第178行到第180行就是找出最大的类间方差,并且将最大类间方差所对应的像素值赋值给“threshold”这个变量,作为我们二值化的阈值。
我们程序用到了一些优化指令,下面对这些优化指令做一些介绍:
在代码的第34行“pragma HLS UNROLL”是HLS优化指令,表示我们展开循环创建多个独立的操作,这将会导致我们可以在单个时钟周期里并行执行for循环中的操作,而基于处理器的架构导致它执行这些操作步骤都是串行执行的。在FPGA内部数据可以并行处理,这体现了用FPGA并行加速的优势。如下图所示:

图 11.3.5 循环展开
左边的这个“Rolled Loop”是滚动循环,它表示每次迭代都在单独的时钟周期内执行,这个实现需要四个时钟周期,只需要用FPGA的一个乘法器就可以实现;中间的这个“Partially Unrolled Loop”是部分展开循环,在这个例子中,这个实现需要两个时钟周期,需要用FPGA的两个乘法器来实现;右边的这个“Unroolled Loop”是展开循环,在这个例子中,循环被完全展开,我们可以在单个时钟周期内执行所有循环操作。然而这个实现需要四个乘法器,更重要的是,这个实现需要能够在相同的时钟周期内执行4次读取和4次写入操作,由于在FPGA的块RAM最多只有两个端口,因此我们在实现这个的时候需要对阵列进行分区。需要注意的是,循环展开后,如果循环的一次迭代中的操作需要前一次迭代的结果,那么它们不能并行执行,而是在数据可用时立即执行。
在代码的第52行“pragma HLS PIPELINE”是为了提高吞吐率而进行的优化。“PIPELINE”指的是流水线操作,流水线操作允许操作同时发生,任务可以不必在开始下一个操作之前完成所有操作。流水线可以应用于函数和循环。函数的吞吐率改进如下图所示:

图 11.3.6 函数流水线操作
如图所示,如果没有流水线操作,这个函数每3个时钟周期读取一个输入,并且每2个时钟周期输出一个值。这个函数的启动间隔(initiation interval)为3,延迟(latency)为2。通过流水线操作,每个周期(initiation interval = 1)读取一个新的输入,而不会改变输出延迟或使用的资源。
循环流水线操作允许循环中的操作以并发方式实现,如图所示:

图 11.3.7 循环流水线操作
左边的这个图是默认的顺序操作,其中每个输入读取(initiation interval = 3)之间有3个时钟周期,并且在执行最后一次输出写入之前需要8个时钟周期。右边的这个图是循环流水线之后的结果,经过流水线处理后可以每个周期(initiation interval = 1)读取一个新的输入样本,并且仅在4个时钟周期后写入最终输出。在这里我们使用这个指令来提高系统的吞吐率。
在代码的第53行“pragma HLS LOOP_FLATTEN OFF”是为了防止这个嵌套循环在FPGA实现中被展平为单个循环层次结构。在Vivado HLS中默认情况下不会将嵌套循环展开,但可以定义优化的程度,如果优化等级定义过高,在综合的时候还是有可能将嵌套的循环结构展平为单个循环层次结构,添加这个指令,就是为了防止Vivado HLS做这种优化。
程序创建完成后,点击工具栏中向右的绿色三角形对C++代码进行综合,综合完成后在工具栏中点击黄色的“田”字按钮,导出RTL,如下图所示:

图 11.3.4 导出RTL
在弹出的对话框中保持默认设置,直接点击“OK”,如下图所示:

图 11.3. 将设计导出成IP
设计导出完成后,HLS设计部分就结束了,我们在HLS工程目录下可以找到导出的IP核,如下图红色方框所示:

图 11.3.18导出得到的IP
HLS设计结束之后,我们将在Vivado中对导出的IP核进行验证。
11.4IP验证
在IP验证环节,我们会使用Vivado工具的IP集成器将生成的IP核添加到Block Design中,然后完成设计后将程序下载到领航者开发板上进行验证。
用于IP验证的底层硬件可以在《领航者ZYNQ之HLS开发指南》第5章“OV5640 摄像头灰度显示”实验的基础上进行。打开该实验所对应的Vivado工程“ov5640_rgb2gray_ip_test”,将其另存为“otsu_threshold_ip_test”工程。为了方便工程管理,我们将Vivado工程的目录与HLS工程目录保持一致,如下图所示:

图 11.4.1 创建Vivado工程
在通过“另存为”的方式保存工程之后,还要将原来工程中的IP库(名为ip_repo的文件夹)复制到新的Vivado工程目录下, 然后将HLS设计过程中导出的IP核拷贝到“ip_repo”目录下并解压。
在Vivado中重新将当前工程目录下的ip_repo文件夹添加到工程的IP库中,然后将HLS生成的IP核text_overlay添加到Block Design中,并将其STREAM接口分别连接到Video In to AXI4-Stream模块的video_out接口与VDMA模块的S_AXIS_S2MM接口上。最后点击上图中左上角的“Run Connection Automation”,让工具自动连接该IP核的其他端口,包括时钟、复位以及AXI-Lite从接口,最终完成的设计如下图所示:
然后点击“Run Connnection Automation”,下面列出了会自动连接的模块及其接口,勾选“All Automation”, 然后点击“OK”按钮。最终完成的设计如下图所示:

图 11.4.2 完成后的Block Design
到这里我们的Block Design就设计完成了,在Diagram窗口空白处右击,然后选择“Validate Design”验证设计。验证完成后弹出对话框提示“Validation Successful”表明设计无误,点击“OK”确认。最后按快捷键“Ctrl + S”保存设计。
接下来在Source窗口中右键点击Block Design设计文件“system.bd”,然后依次执行“Generate Output Products”和“Create HDL Wrapper”。
最后在左侧Flow Navigator导航栏中找到PROGRAM AND DEBUG,点击该选项中的“Generate Bitstream”,对设计进行综合、实现、并生成Bitstream文件。
在生成 Bitstream 之后,在菜单栏中选择 File > Export > Export hardware 导出硬件,并在弹出的对话框 中,勾选“Include bitstream”。然后在菜单栏选择 File > Launch SDK,启动 SDK 软件。
在Vivado SDK中新建空的应用工程,工程名为“ov5640_otsu_threshold”。
然后找到《领航者ZYNQ之嵌入式开发指南》第二十三章“OV5640 摄像头 LCD 显示”实验的Vivado工程目录,将“21_ov7725_lcd\ov7725_lcd.sdk\ov7725_lcd”目录下的src文件夹拷贝到新建的应用工程目录下。
在SDK中刷新src目录,然后将“main.c”的代码修改为如下所示:
1   #include <stdio.h>
2   #include <stdlib.h>
3   #include <string.h>
4   #include "xil_types.h"
5   #include "xil_cache.h"
6   #include "xparameters.h"
7   #include "xgpio.h"
8   #include "xaxivdma.h"
9   #include "xaxivdma_i.h"
10#include "display_ctrl/display_ctrl.h"
11#include "vdma_api/vdma_api.h"
12#include "emio_sccb_cfg/emio_sccb_cfg.h"
13#include "ov5640/ov5640_init.h"
14#include "xov5640_otsu_threshold.h"
15
16//宏定义
17#define BYTES_PIXEL      3                           //像素字节数,RGB888占3个字节
18#define FRAME_BUFFER_NUM   3                           //帧缓存个数3
19#define DYNCLK_BASEADDR    XPAR_AXI_DYNCLK_0_BASEADDR//动态时钟基地址
20#define VDMA_ID            XPAR_AXIVDMA_0_DEVICE_ID    //VDMA器件ID
21#define DISP_VTC_ID      XPAR_VTC_0_DEVICE_ID      //VTC器件ID
22//PL端AXI GPIO 0(lcd_id)器件 ID
23#define AXI_GPIO_0_ID      XPAR_AXI_GPIO_0_DEVICE_ID   
24//使用AXI GPIO(lcd_id)通道1
25#define AXI_GPIO_0_CHANEL1                           
26
27//全局变量
28//frame buffer的起始地址
29unsigned int const frame_buffer_addr = (XPAR_PS7_DDR_0_S_AXI_BASEADDR
30                                          + 0x1000000);
31XAxiVdma               vdma;
32DisplayCtrl            dispCtrl;
33XGpio                  axi_gpio_inst;         //PL端 AXI GPIO 驱动实例
34XOv5640_otsu_threshold   otsu_threshold_inst;   //PL端otsu_threshold驱动实例
35VideoMode                vd_mode;
36unsigned int lcd_id;
37
38int main(void)
39{
40      u32 status;
41      u16 cmos_h_pixel;   //ov5640 DVP 输出水平像素点数
42      u16 cmos_v_pixel;   //ov5640 DVP 输出垂直像素点数
43      u16 total_h_pixel;//ov5640 水平总像素大小
44      u16 total_v_pixel;//ov5640 垂直总像素大小
45
46      //获取LCD的ID
47      XGpio_Initialize(&axi_gpio_inst, AXI_GPIO_0_ID);
48      lcd_id = LTDC_PanelID_Read(&axi_gpio_inst,AXI_GPIO_0_CHANEL);
49      xil_printf("lcd_id = %x\n\r",lcd_id);
50
51      //根据获取的LCD的ID号来进行ov5640显示分辨率参数的选择
52      switch(lcd_id){
53          case 0x4342 ://4.3寸屏,480*272分辨率
54            cmos_h_pixel = 480;
55            cmos_v_pixel = 272;
56            total_h_pixel = 1800;
57            total_v_pixel = 1000;
58            break;
59          case 0x4384 ://4.3寸屏,800*480分辨率
60            cmos_h_pixel = 800;
61            cmos_v_pixel = 480;
62            total_h_pixel = 1800;
63            total_v_pixel = 1000;
64            break;
65          case 0x7084 ://7寸屏,800*480分辨率
66            cmos_h_pixel = 800;
67            cmos_v_pixel = 480;
68            total_h_pixel = 1800;
69            total_v_pixel = 1000;
70            break;
71          case 0x7016 ://7寸屏,1024*600分辨率
72            cmos_h_pixel = 1024;
73            cmos_v_pixel = 600;
74            total_h_pixel = 2200;
75            total_v_pixel = 1000;
76            break;
77          case 0x1018 ://10.1寸屏,1280*800分辨率
78            cmos_h_pixel = 1280;
79            cmos_v_pixel = 800;
80            total_h_pixel = 2570;
81            total_v_pixel = 980;
82            break;
83          default :
84            cmos_h_pixel = 480;
85            cmos_v_pixel = 272;
86            total_h_pixel = 1800;
87            total_v_pixel = 1000;
88            break;
89      }
90
91      emio_init();                         //初始化EMIO
92      status = ov5640_init( cmos_h_pixel,//初始化ov5640
93                            cmos_v_pixel,
94                           total_h_pixel,
95                           total_v_pixel);
96      if(status == 0)
97          xil_printf("OV5640 detected successful!\r\n");
98      else
99          xil_printf("OV5640 detected failed!\r\n");
100
101   //根据获取的LCD的ID号来进行video参数的选择
102   switch(lcd_id){
103         case 0x4342 : vd_mode = VMODE_480x272;break;//4.3寸屏,480*272分辨率
104         case 0x4384 : vd_mode = VMODE_800x480;break;//4.3寸屏,800*480分辨率
105         case 0x7084 : vd_mode = VMODE_800x480;break;//7寸屏,800*480分辨率
106         case 0x7016 : vd_mode = VMODE_1024x600; break;//7寸屏,1024*600分辨率
107         case 0x1018 : vd_mode = VMODE_1280x800; break;//10.1寸屏,1280*800分辨率
108         default : vd_mode = VMODE_800x480; break;
109   }
110
111   //初始化自适应二值化IP核otsu_threshold
112   XOv5640_otsu_threshold_Initialize(&otsu_threshold_inst, 0);
113   //配置自适应二值化IP核otsu_threshold
114   XOv5640_otsu_threshold_Set_rows(&otsu_threshold_inst, vd_mode.height);
115   //配置自适应二值化IP核otsu_threshold
116   XOv5640_otsu_threshold_Set_cols(&otsu_threshold_inst, vd_mode.width);
117
118   //配置VDMA
119   run_vdma_frame_buffer(&vdma, VDMA_ID, vd_mode.width, vd_mode.height,
120                           frame_buffer_addr,0,0,BOTH);
121   //初始化Display controller
122   DisplayInitialize(&dispCtrl, DISP_VTC_ID, DYNCLK_BASEADDR);
123   //设置VideoMode
124   DisplaySetMode(&dispCtrl, &vd_mode);
125   DisplayStart(&dispCtrl);
126
127   return 0;
128 }
在代码的第14行引入了“xov5640_otsu_threshold.h”头文件,这个头文件是Vivado HLS工具生成的,里面声明了OTSU自适应二值化IP核的驱动函数。首先在代码的34行定义了OTSU自适应二值化IP核的驱动实例otsu_threshold_inst,该变量会在后面对IP核进行配置时用到。然后在代码的第112行通过“XOv5640_otsu_threshold_Initialize ()”函数来初始化Vivado HLS生成的OTSU自适应二值化IP核;在代码的第116行通过传入“vd_mode.height”形参来设置OTSU自适应二值化IP核的行数,在代码的第114行通过传入“vd_mode.width”形参来设置列数。这些数据类型和函数在“xov5640_otsu_threshold.h”头文件中均有声明。
11.5下载验证
编译完工程之后我们就可以开始下载程序了。将 OV5640 摄像头模块插在领航者Zynq开发板的“OLED/CAMERA”插座上,并将LCD的排线接头插入开发板上的 LCD 接线座。将下载器一端连电脑,另一端与开发板上的 JTAG 端口连接,连接电源线并打开电源开关。
在SDK软件下方的SDK Terminal窗口中点击右上角的加号设置并连接串口。然后下载本次实验硬件设计过程中所生成的BIT文件,来对PL进行配置。最后下载软件程序,下载完成后在LCD上就可以看到液晶屏上显示了经过OTSU自适应二值化处理后的图像,如下图所示:

图 11.5.1 OTSU自适应二值化效果图

页: [1]
查看完整版本: 【正点原子FPGA连载】第十一章基于OV5640的自适应二值化实验--摘自【正点原子】领航者ZYNQ之HLS 开发指南