HLS Videolib能够实现的功能,xfopencv也都能实现。
下面重点实现几个之前的例子,看看从videolib移植到xfopencv需要注意哪些要点。
(一)CFLAGS
被编译的文件,例如TOP函数的CPP,或者子函数的CPP,以及TB的CPP,都需要添加CFLAGS。
"-D__SDSVHLS__ --std=c++0x"
设置CFLAGS:
方法一,在GUI中,在project setting中,设置各个文件的CFLAG。
方法二,找到vivado_hls.app文件,用vscode打开,修改CFLAGS。
例如,类似的描述块中,cpp文件,都添加上CFLAGS。
<file name="src/top.cpp" sc="0" tb="false" cflags="-D__SDSVHLS__ --std=c++0x" blackbox="false"/>
<file name="../../src/test.cpp" sc="0" tb="1" cflags="-D__SDSVHLS__ --std=c++0x"/>
注意,在CSIM和COSIM中,必须添加–std=c++0x,
但是在RTL时,又要再去掉这个标志去完成综合。
推荐使用TCL模式建工程,这样,方便于设置每个CPP文件的CFLAG。
建好工程后,再使用GUI执行后续任务。
(二)searching path
由于xfopencvlib是我们自行下载的,HLS并不会找到xfopencv的搜索路径。
我们需要给HLS指定xfopencv的搜索路径。
方法一,vivado_hls.app文件,用vscode打开,修改testbench以及所有CPP的CFLAGS,添加
-ID:/Xilinx/Vivado/2019.1/xfopencv/include
注意不要有空格。
方法二,修改run_hls.tcl,重新生成工程。
(三)namespace
之前使用的hls::Mat,现在改为使用xf::Mat。
注意,xf::Mat没有重载<<和>>,只能使用read和write函数。
hls videolib中的14个接口函数,只保留了两个在xfopencv中。
cvMat2AXIvideo
AXIvideo2cvMat
这是用在CSIM中的函数。
在RTL中使用的,是
xf::AXIvideo2xfMat
xf::xfMat2AXIvideo
替代之前的hls video lib中的等价函数。
使用它们,需要包含
common/xf_infra.h文件。这个文件里已经包含了hls_stream.h文件,所以不需要显式包含hls_stream.h了。
(四)编码风格
在HLS域中编程时,我们往往会使用typedef预先定义好需要的类型,然后再使用。
在XF域中编程时,我们往往选择直接在代码行中具象实例化一个模板类或者模板函数,统一使用预定义的常量宏。
两种风格都是可取的。
但是前一种风格下,好处在于逻辑更清晰,坏处在于,大量使用了模板参数推断,这依赖于编译器。
后一种风格下,模板参数全部显式手工完成,代码更严谨。
+++++++++++++++++++++++++++++++++++++++++
来看第一个例子,xfopencv提供的standalone的例子。
首先是定义自己的top.h文件。这里命名的是xf_dilation_config.h。即top级的配置文件。
#ifndef _XF_DILATION_CONFIG_H_
#define _XF_DILATION_CONFIG_H_
最开始,一定是头文件保护。
#include "hls_stream.h"
#include "ap_int.h"
#include "xf_common.h"
#include "common/xf_infra.h"
#include "common/xf_utility.h"
#include "imgproc/xf_dilation.hpp"
#include "xf_config_params.h"
然后是include所需要的头文件。包括HLS下的和xfopencv下的。
这里,将一些参数专门提取出来,放到了top级的参数文件中。这里是xf_config_params.h文件。
(--------------------------------------------------------------------------内容如下:
/* Optimization type */
#define RO 0 // Resource Optimized (8-pixel implementation)
#define NO 1 // Normal Operation (1-pixel implementation)
#define GRAY 1
#define FILTER_SIZE 3
#define KERNEL_SHAPE XF_SHAPE_CROSS
#define ITERATIONS 1
定义了各种参数,这些参数主要是一些开关常量宏,后面会用到,
---------------------------------------------------------------------------------------------)
然后是define各种常量宏。
/* config width and height */
#define WIDTH 1920
#define HEIGHT 1080
然后是define各种由开关宏控制内容的常量宏。
#if NO
#define NPC1 XF_NPPC1
#endif
#if RO
#define NPC1 XF_NPPC8
#endif
#if GRAY
#define TYPE XF_8UC1
#else
#define TYPE XF_8UC3
#endif
用xfopencv定义的各种枚举常量给常量宏赋值。
然后是声明函数原型。
void dilation_accel(
xf::Mat<TYPE, HEIGHT, WIDTH, NPC1> &_src,
xf::Mat<TYPE, HEIGHT, WIDTH, NPC1> &_dst,
unsigned char kernel[FILTER_SIZE*FILTER_SIZE]);
函数的数据路径,使用的xfmat对象,配置路径,使用的是数组。
再来看top函数。这里是xf_dilation_accel.cpp。
void dilation_accel(
xf::Mat<TYPE, HEIGHT, WIDTH, NPC1> &_src,
xf::Mat<TYPE, HEIGHT, WIDTH, NPC1> &_dst,
unsigned char kernel[FILTER_SIZE*FILTER_SIZE])
{
xf::dilate<XF_BORDER_CONSTANT, TYPE ,HEIGHT, WIDTH, KERNEL_SHAPE, FILTER_SIZE, FILTER_SIZE, ITERATIONS, NPC1>(_src, _dst,kernel);
}
整个函数,就是对模版函数dilate的具象化实例函数进行了一次封装。
因为HLS中,top函数不允许使用模板函数的具象函数,所以定义了一个普通函数,封装模板具象函数。
虽然TOP函数是普通函数,但是TOP函数中,可以使用模板类的具象类型作为形参,这里可以看到,形参引用使用了两个xfMat的具象类型的对象引用。
再来看testbench。这里是xf_dilation_tb.cpp。
首先是包含需要的头文件。
#include "xf_headers.h"
#include "xf_dilation_config.h"
一个是tb级自己使用的头文件xf_headers.h,一个是top函数的对应头文件。
(----------------------------------------------------------------------------------------------------------
来看看这个文件有什么。
最开始是头文件保护。
#ifndef _XF_HEADERS_H_
#define _XF_HEADERS_H_
然后是包含CSIM需要的std文件。
#include <stdio.h>
#include <stdlib.h>
然后是包含CSIM需要的系统自带的标准opencv文件。
#include "opencv/cv.h"
#include "opencv/highgui.h"
#include "opencv2/imgproc/imgproc.hpp"
然后是包含xfopencv提供的软件工具文件。
#include "common/xf_sw_utils.h"
例如imread,imwrite等。
---------------------------------------------------------------------------------------------------------------)
然后就是主函数。
int main(int argc, char** argv)
{
cv::Mat in_img,in_img1,out_img,ocv_ref;
cv::Mat in_gray,in_gray1,diff;
#if GRAY
// reading in the color image
in_gray = cv::imread(SRCIMAGE, 0);
#else
// reading in the color image
in_gray = cv::imread(SRCIMAGE, 1);
#endif
if (in_gray.data == NULL)
{
fprintf(stderr,"Cannot open image at %s\n", SRCIMAGE);
return 0;
}
// create memory for output images
#if GRAY
ocv_ref.create(in_gray.rows,in_gray.cols,CV_8UC1);
out_img.create(in_gray.rows,in_gray.cols,CV_8UC1);
diff.create(in_gray.rows,in_gray.cols,CV_8UC1);
#else
ocv_ref.create(in_gray.rows,in_gray.cols,CV_8UC3);
out_img.create(in_gray.rows,in_gray.cols,CV_8UC3);
diff.create(in_gray.rows,in_gray.cols,CV_8UC3);
#endif
cv::Mat element = cv::getStructuringElement( KERNEL_SHAPE,cv::Size(FILTER_SIZE, FILTER_SIZE), cv::Point(-1, -1));
cv::dilate(in_gray, ocv_ref, element, cv::Point(-1, -1), ITERATIONS, cv::BORDER_CONSTANT);
cv::imwrite("out_ocv.jpg", ocv_ref);
static xf::Mat<TYPE, HEIGHT, WIDTH, NPC1> imgInput(in_gray.rows,in_gray.cols);
static xf::Mat<TYPE, HEIGHT, WIDTH, NPC1> imgOutput(in_gray.rows,in_gray.cols);
unsigned char structure_element[FILTER_SIZE*FILTER_SIZE];
for(int i=0;i<(FILTER_SIZE*FILTER_SIZE);i++)
{
structure_element[i]=element.data[i];
}
imgInput.copyTo(in_gray.data);
HLS TOP function call /
dilation_accel(imgInput, imgOutput, structure_element);
// Compute Absolute Difference
xf::absDiff(ocv_ref, imgOutput, diff);
cv::imwrite("out_error.jpg", diff);
// Find minimum and maximum differences.
double minval=256,maxval=0;
int cnt = 0;
for (int i=0; i<in_gray.rows; i++)
{
for(int j=0; j<in_gray.cols; j++)
{
uchar v = diff.at<uchar>(i,j);
if (v>0)
cnt++;
if (minval > v)
minval = v;
if (maxval < v)
maxval = v;
}
}
float err_per = 100.0*(float)cnt/(in_gray.rows*in_gray.cols);
fprintf(stderr,"Minimum error in intensity = %f\n Maximum error in intensity = %f\n Percentage of pixels above error threshold = %f\n",minval,maxval,err_per);
if(err_per > 0.0f)
return 1;
else
return 0;
}
来具体分析这个代码。
和所有的testbench 一样,分为几个部分。
(一)准备数据上下文
cv::Mat in_img,in_img1,out_img,ocv_ref;
cv::Mat in_gray,in_gray1,diff;
定义了一系列的cvmat。图像是一个二维矩阵。注意,在CSIM中,上下游任务间交接的对象是cvmat。cvmat对象,实质上是一个句柄。
#if GRAY
// reading in the color image
in_gray = cv::imread(SRCIMAGE, 0);
#else
// reading in the color image
in_gray = cv::imread(SRCIMAGE, 1);
#endif
从FILE中读取图像数据,存放内存中,实例图像被cmmat句柄控制。这里,使用到了之前定义的开关宏。
if (in_gray.data == NULL)
{
fprintf(stderr,"Cannot open image at %s\n", SRCIMAGE);
return 0;
}
这里有一个良好的编码风格,交接检查。
被调用的子函数返回时,与调用者存在数据交接。有多种方式的数据交接,
例如返回值表示执行状态,或者利用指针进行内存变量读写等,这些交接数据,都需要被检查。
// create memory for output images
#if GRAY
ocv_ref.create(in_gray.rows,in_gray.cols,CV_8UC1);
out_img.create(in_gray.rows,in_gray.cols,CV_8UC1);
diff.create(in_gray.rows,in_gray.cols,CV_8UC1);
#else
ocv_ref.create(in_gray.rows,in_gray.cols,CV_8UC3);
out_img.create(in_gray.rows,in_gray.cols,CV_8UC3);
diff.create(in_gray.rows,in_gray.cols,CV_8UC3);
#endif
为其他的cvmat对象分配内存用来存放实例图像。cvmat对象,实质上是一个句柄。
cv::Mat element = cv::getStructuringElement( KERNEL_SHAPE,cv::Size(FILTER_SIZE, FILTER_SIZE), cv::Point(-1, -1));
cv::dilate(in_gray, ocv_ref, element, cv::Point(-1, -1), ITERATIONS, cv::BORDER_CONSTANT);
cv::imwrite("out_ocv.jpg", ocv_ref);
创建一个算子核,算子核也是一个二维矩阵,所以算子核也是用cvmat作为句柄。
用输入的图像和算子核图像,作为参数,调用cvdilate函数,生成金样。
然后将cv生成的金样,用imwrite函数,写入FILE中。
接下来,为xfopencv准备数据上下文。
static xf::Mat<TYPE, HEIGHT, WIDTH, NPC1> imgInput(in_gray.rows,in_gray.cols);
static xf::Mat<TYPE, HEIGHT, WIDTH, NPC1> imgOutput(in_gray.rows,in_gray.cols);
定义了两个xfmat对象,抽取了输入图像的属性作为参考,rows和cols。xfmat对象实质上也是句柄的作用。
unsigned char structure_element[FILTER_SIZE*FILTER_SIZE];
for(int i=0;i<(FILTER_SIZE*FILTER_SIZE);i++)
{
structure_element[i]=element.data[i];
}
用一个for循环,对一个一维向量进行逐点处理。
imgInput.copyTo(in_gray.data);
输入对象的data指针,指向的是图像实例的存储数据。
这个函数,从输入对象的data存储区读取数据,拷贝imgInput的数据区中。
(二)调用DUT。
dilation_accel(imgInput, imgOutput, structure_element);
调用top函数,输出结果由xfmat对象imgoutput作为句柄来控制。
(三)结果输出。
本例中,不输出DUT的结果。直接进入结果比对。
(四)结果比对。
xf::absDiff(ocv_ref, imgOutput, diff);
cv::imwrite("out_error.jpg", diff);
利用sw_util中的函数adfdiff,计算cvmat和xfmat之间的差,结果存入一个cvmat对象作为句柄控制的
图像实例。
然后用imwrite函数,将diff写入FILE。
// Find minimum and maximum differences.
double minval=256,maxval=0;
int cnt = 0;
for (int i=0; i<in_gray.rows; i++)
{
for(int j=0; j<in_gray.cols; j++)
{
uchar v = diff.at<uchar>(i,j);
if (v>0)
cnt++;
if (v < minval)
minval = v;
if (v > maxval)
maxval = v;
}
}
遍历二维矩阵,更新计分变量。
用一个两层嵌套for循环,对diff图像进行逐点处理。
用cvmat对象的成员函数at,可以抽取像素点的值。然后最像素点做后续判断和处理。
一个任务是,判断像素点的值,进行计数。
第二个任务是,记录最小值,采用的方式是遍历更新,在逐点处理时,如果当前处理的像素值,小于当前记录的最小值,则用当前像素值,更新记录的最小值。遍历结束的时候,记录的就是整个二维矩阵中的最小值。
第三个任务是,记录最大值,采用的方式是遍历更新。
遍历结束时,三个计分变量,已经全部完成更新。
(五) 收尾
float err_per = 100.0*(float)cnt/(in_gray.rows*in_gray.cols);
fprintf(stderr,"Minimum error in intensity = %f\n Maximum error in intensity = %f\n Percentage of pixels above error threshold = %f\n",minval,maxval,err_per);
if(err_per > 0.0f)
return 1;
else
return 0;
计分变量cnt,记录的是差值超标的像素的个数,计算像素占比,然后输出。
根据err_per的数值,决定返回的执行状态值。
++++++++++++++++++++++++++++++++++++++++++++
再来看xfopencv中提供的第二个例子。
大部分和第一个相同。所不同的是,
前面的例子,使用的数据路径,是xfmat,本例中,使用的数据路径,是hlsstream。
所以,需要进行一次转换。
我们定义的图像处理函数仍然不变, 只是在它的基础上,进行二次封装即可。
例如,dilation_accel函数,仍然使用xfmat作为数据路径。
二次封装函数ip_accel_app函数,使用hlsstream作为数据路径。
来看看这个函数。
void ip_accel_app(
hls::stream< ap_axiu<8,1,1,1> >& _src,
hls::stream< ap_axiu<8,1,1,1> >& _dst,
int height,int width,
unsigned char kernel[FILTER_SIZE*FILTER_SIZE])
{
#pragma HLS INTERFACE axis register both port=_src
#pragma HLS INTERFACE axis register both port=_dst
xf::Mat<TYPE, HEIGHT, WIDTH, NPC1> imgInput1(height,width);
xf::Mat<TYPE, HEIGHT, WIDTH, NPC1> imgOutput1(height,width);
#pragma HLS stream variable=imgInput1.data dim=1 depth=1
#pragma HLS stream variable=imgOutput1.data dim=1 depth=1
#pragma HLS dataflow
xf::AXIvideo2xfMat(_src, imgInput1);
dilation_accel(imgInput1,imgOutput1, kernel);
xf::xfMat2AXIvideo(imgOutput1, _dst);
}
这里另一了两个xfmat的临时对象,imgInput1和imgOutput1。
整个函数,按照上下游顺序进行任务划分,任务之间,通过交接对象实现数据交接。
这里需要注意的编码技巧是,streamlize。
本质上,数组也是指针,HLS中,倾向于使用数组来代替使用指针,xfmat的data成员,被HLS理解为是一个数组。我们对这个数组使用了stream约束,让HLS能够正确理解data的硬件实现方式。
在testbench中,也有一些小的改动。
uint16_t height = in_img.rows;
uint16_t width = in_img.cols;
这是一个良好的编码风格,将抽取出来的属性rows和cols,暂存给临时变量。
hls::stream< ap_axiu<8,1,1,1> > _src,_dst;
现在DUT的数据路径,变成了hlsstream,不再是xfmat,所以这里在准备数据时,准备的是hlsstream临时对象。
cvMat2AXIvideoxf<NPC1>(in_img, _src);
ip_accel_app(_src, _dst,height,width, structure_element);
AXIvideo2cvMatxf<NPC1>(_dst, in_img1);
调用DUT时,先用xf_axi中的函数cvMat2AXIvideoxf,将cvmat对象转换成hlsstream对象,传给DUT,然后,再将DUT输出的结果hlsstream对象,通过AXIvideo2cvMatxf函数,转换成cvmat对象。
cv::imwrite("hls.jpg", in_img1);
由于DUT输出的结果,被转换成了cvmat对象,
所以,这时可以使用imwrite函数,将DUT的结果,输出到FILE中去。