HLS第三十九课(XAPP1167,用xfopencv改写hls videolib的函数)

HLS Videolib能够实现的功能,xfopencv也都能实现。
下面重点实现几个之前的例子,看看从videolib移植到xfopencv需要注意哪些要点。

+++++++++++++++++++++++++++++++
第一个例子,pass through。
首先是头文件保护宏,

#ifndef _TOP_H_
#define _TOP_H_

然后是加入xfopencv的头文件,

#include "xf_common.h"
#include "common/xf_infra.h"

使用xfopencv,一定需要上述头文件。
特别注意,包含头文件要注意顺序,xfopencv的头文件之间存在耦合关系,必须按照上述顺序包含。

然后是定义需要的常量宏。

// maximum image size
#define MAX_WIDTH  1920
#define MAX_HEIGHT 1080

// I/O Image Settings
#define INPUT_IMAGE           "test_1080p.bmp"
#define OUTPUT_IMAGE          "result_1080p.bmp"
#define OUTPUT_IMAGE_GOLDEN   "result_1080p_golden.bmp"

然后是定义需要使用的模板参数宏,这里,直接写在top.h文件中,不再单独使用一个top_parameter.h文件。

#define MY_W 24
#define MY_NPPC XF_NPPC1
#define MY_TYPE XF_8UC3

注意,xfopencv中由很多预定义的宏,为了不引起错误,我们自定义的宏,要使得宏名不和预定义的宏名相同。
约定,自定义的宏名,需要加上前缀或者后缀。建议加上TOP函数的简写字母。

然后是声明函数原型。

void image_filter(hls::stream<ap_axiu<MY_W,1,1,1>>& src_axi, hls::stream<ap_axiu<MY_W,1,1,1>>& dst_axi, int rows, int cols);

再来看看TOP函数文件。
首先是包含TOP头文件。

#include "top.h"

然后是TOP函数体。

void image_filter(
		hls::stream<ap_axiu<MY_W,1,1,1>>& video_in,
		hls::stream<ap_axiu<MY_W,1,1,1>>& video_out,
		int rows, int cols) {
    //Create AXI streaming interfaces for the core
#pragma HLS INTERFACE axis port=video_in bundle=INPUT_STREAM
#pragma HLS INTERFACE axis port=video_out bundle=OUTPUT_STREAM

#pragma HLS INTERFACE s_axilite port=rows bundle=CONTROL_BUS offset=0x14
#pragma HLS INTERFACE s_axilite port=cols bundle=CONTROL_BUS offset=0x1C
#pragma HLS INTERFACE s_axilite port=return bundle=CONTROL_BUS

#pragma HLS INTERFACE ap_stable port=rows
#pragma HLS INTERFACE ap_stable port=cols

    //YUV_IMAGE img_0(rows, cols);
    xf::Mat<MY_TYPE,MAX_HEIGHT, MAX_WIDTH, MY_NPPC> img_1(rows,cols);
#pragma HLS dataflow
    xf::AXIvideo2xfMat(video_in, img_1);
    xf::xfMat2AXIvideo(img_1, video_out);
}

这个函数中,主要就是定义了一个xfmat的临时对象,作为上下游任务间的数据交接对象。
调用了转换函数,并用临时对象作为中间结果。

再来看testbench。
首先是包含top头文件。

#include "top.h"

然后是包含HLS中的用于OPENCV的文件。

#include "opencv/cv.h"
#include "opencv/highgui.h"

然后是xfopencv中的用于仿真的文件。

#include "common/xf_sw_utils.h"
#include "common/xf_axi.h"

----特别注意,上述包含顺序不能错,否则头文件在解析过程中,会出现多种错误。

然后是定义testbench中需要使用到的宏。

#define MYSIM_TYPE CV_8UC3

然后是声明默认命名空间。

using namespace cv;

再来看看Main函数。

int main (int argc, char** argv) {
    cv::Mat out_img,ocv_ref;
	cv::Mat in_img,in_img1,diff;
    in_img = cv::imread(INPUT_IMAGE, 1);
    if (in_img.data == NULL)
	{
		fprintf(stderr,"Cannot open image at %s\n", INPUT_IMAGE);
		return 0;
	}
    ocv_ref.create(in_img.rows,in_img.cols,MYSIM_TYPE );
	diff.create(in_img.rows,in_img.cols,MYSIM_TYPE );
	in_img1.create(in_img.rows,in_img.cols,MYSIM_TYPE );

	uint16_t height = in_img.rows;
	uint16_t width = in_img.cols;


    hls::stream< ap_axiu<MY_W,1,1,1> > _src,_dst;

    cvMat2AXIvideoxf<MY_NPPC>(in_img, _src);

    image_filter(
		_src,
		_dst,
		height, width);

    AXIvideo2cvMatxf<MY_NPPC>(_dst, in_img1);

    cv::imwrite(OUTPUT_IMAGE, in_img1);

    printf("test ok\n");
    return 0;

}

这是一个最基本的testbench。
在CSIM中,使用cvmat对象作为句柄,来控制一个图像。
在调用DUT之前,需要使用cvMat2AXIvideoxf函数来将cvmat转换成hlsstream对象。
调用DUT之后,需要使用AXIvideo2cvMatxf函数来将hlsstream对象转换成cvmat对象。
有了存放结果图像的cvmat对象,就可以使用imwrite输出图像到FILE中。

+++++++++++++++++++++++++++++++++++++++++++++
第二个例子,demo
首先是定义top.h。和上一个例子类似。区别在于将需要使用的算法函数的头文件分别添加进来。具体函数位于哪个文件,可以查看XAPP1233。例如,
subs位于core/xf_arithm.hpp,
scale位于imgproc/xf_convertScaleAbs.hpp
erode位于imgproc/xf_erosion.hpp,
dilate位于imgproc/xf_dilation.hpp,

然后是定义top.cpp,将之前的hls域下的算法函数,替换成xf域下的算法函数,
注意要在每个子任务之间设置临时对象,作为任务间数据交接对象。
这些设置的临时对象,要添加stream约束,给他们的成员变量data,这是一个数组,所以要显式指定stream约束。
注意,使用xfopencv提供的函数时,如果使用模板参数推理置换,很多时候会报错,
所以,最好的风格时,
显式具象化模板函数以及模板类。
(————————————————————————————
使用模板函数时,很多代码中,习惯于使用模板参数推理置换。这在很多时候,可以简化代码,但是,这并不是推荐的代码风格。
推荐的代码风格是,显式使用完整的模板参数,具象化模板类或者模板函数。
我们在具象化一个模板类时,代码风格通常是良好的,因为我们清楚这些类应该被具象化成什么类型。
但是在具象化一个模板函数时,却会经常偷懒,希望借助于模板参数推理置换。这是C++一个特性,可以尽量少的使用这个推理置换的特性。
—————————————————————————————)

然后是定义test.cpp,将之前的DUT,替换成现在的DUT。
注意,其中有一段制造算子核矩阵的程序,

	cv::Mat element = cv::getStructuringElement(
    						XF_SHAPE_CROSS,
							cv::Size(3, 3),
							cv::Point(-1, -1));
    unsigned char structure_element[9];


	for(int i=0; i < 9; i++)
	{
		structure_element[i]=element.data[i];
	}

使用getStructuringElement函数,利用指定的point和指定的size,构造一个算子核矩阵。
然后,在一个for循环中,用算子核矩阵的成员data,填充一个一维向量。

+++++++++++++++++++++++++++++++++++++++++++++++
第三个例子,色彩分离,posterize,
首先是top.h文件,类似于前面的例子。
然后是top.cpp文件,类似于前面的例子。
注意其中的主体部分的区别。
首先是,需要加入assert进行参数检查。保证运行时参数安全。在确保参数是安全的时候,进入处理主体。

	assert(rows <= MAX_HEIGHT);
    assert(cols <= MAX_WIDTH);

然后是处理主体。

	int k = 0;
	for(int row = 0; row < rows; row++) {
#pragma HLS loop_flatten off
       for(int col = 0; col < cols; col++) {
#pragma HLS pipeline II=1
            XF_TNAME(MY_TYPE,MY_NPPC) p;
            p = img_0.read(k);

            p(7,0) = p(7,0) & 0xE0;
            p(15,8) = p(15,8) & 0xE0;
            p(23,16) = p(23,16) & 0xE0;

            img_1.write(k,p);
            k++;
        }
    }

这里使用了一个两层嵌套for循环,进行逐点处理。
在逐点处理的主体内,首先是定义了临时对象,作为local cache,或者称为操作对象。
首先是从上游xfmat中读取一个值,存入操作对象,然后修改这个操作对象的成员。
然后将更新后的操作对象,写入下游xfmat中。

注意这里使用的编码技巧,
在xfopencv中,不再支持<<和>>流操作符,所以,这里必须使用read和write,
read函数,返回值是一个ap_int<>,是一个位向量,所以,这里的临时对象,用的是XF_TNAME来定义的变量,另外,read函数需要明确指定读取的位置,这里,为了显式体现对xfmat的流式操作方式,我们使用了额外的索引变量k。
write函数,需要明确指定写入的位置,这里,为了显式体现对xfmat的流式操作方式,我们使用了额外的索引变量k,写入的值,也是要求一个ap_int<>的位向量。
在处理主体的最后,显式递增额外的索引变量k。

当然,也可以使用标准写法。
用i,j配合stride,计算出对应位置,HLS可以自动推断出,i*width+j是单调递增的。

val_src = (XF_SNAME(WORDWIDTH_SRC)) (_src_mat.read(i*width+j));
_dst_mat.write(i*width+j,(val_dst));

在处理主体中,夹在read和write之间的语句块,就是实际的处理操作。
这里,我们使用了HLSC++扩展的截位运算符(),类似于Verilog中的使用方式。这里有一点需要注意的是,ap_int<>位向量,并没有重载复合赋值运算符,所以,需要显式使用完整的先运算再赋值的表达式,这是为了和verilog保持风格一致。

这里的约束技巧是,最内层使用pipline约束,外层显式约束,不允许循环展平,确保逐点处理。

然后来看test.cpp文件。类似于前面的例子。
然后是run_hls.tcl文件,类似于前面的例子。
++++++++++++++++++++++++++++++++++++++++++++
第四个例子,中值滤波。
首先是top.h,类似于之前的例子。
然后是top.cpp,类似于前面的例子。区别在于,需要对line window进行处理。

			buff[2] = buff[1];
            buff[1] = buff[0];
            buff[0] = p;

采用的是moving before processing的方式。
在处理之前,预先构造好line window。
然后是进入处理块。

			if(col > 1)
            {
                for(int i=0;i<2;i++){
                    bool a = buff[2](i*8+7, i*8) > buff[1](i*8+7, i*8);
                    bool b = buff[2](i*8+7, i*8) > buff[0](i*8+7, i*8);
                    bool c = buff[1](i*8+7, i*8) > buff[0](i*8+7, i*8);

                    if(a && c){
                        p(i*8+7, i*8) = buff[1](i*8+7, i*8);
                    }
                    else if(!a && b){
                        p(i*8+7, i*8) = buff[2](i*8+7, i*8);
                    }
                    else{
                        p(i*8+7, i*8) = buff[0](i*8+7, i*8);
                    }
                }          
            }

彩色图像,具有三个通道,所以这里采用截位分段的方式,用一个for循环,分别处理各个通道。
注意,xfopencv从图像文件中读取数据后,三个通道在位向量中的排布方式,是BGR。
即p(7,0) = B ,p(15,8) = G, p(23,16) = R。

p是操作对象,在前面的任务中,它负责暂存从xfmat中读取的数据,并用它更新窗口,此后,p就处于standby状态,即p中的数据可以被覆盖,也不影响数据完整性。所以,在处理块中,p可以作为操作数,参与到暂存处理结果的任务中。

然后是test.cpp文件,类似于前面的例子。
++++++++++++++++++++++++++++++++++++++++++++++
第五个例子,sobel,
首先是top.h,类似于前面的例子。
然后是top.cpp,类似于前面的例子。区别在于库函数的使用。
xfsobel会生成两个图片,分别是x方向和y方向的。
然后是test.cpp,类似于前面的例子。

+++++++++++++++++++++++++++++++++++++++++++++
第六个例子,sobel,手写,
区别在于,使用
XF_TNAME宏来获取存储像素的ap_int<24>类型,获取每个通道,使用截位操作符()。
使用xfLineBuffer和xfWindow作为local cache。

猜你喜欢

转载自blog.csdn.net/weixin_42418557/article/details/121182850
HLS