HLS第三十七课(基于xfopencv实现图像处理)

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了。

扫描二维码关注公众号,回复: 13586103 查看本文章

(四)编码风格
在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中去。

猜你喜欢

转载自blog.csdn.net/weixin_42418557/article/details/121151765