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。