HLS第三十六课(XAPP1167,基于videolib实现sobel算子)

sobel算子的实现,体现了多种编码技巧。

首先,是top.h文件。
和之前类似,包含了多个常规部分。

#ifndef _TOP_H_
#define _TOP_H_

#include "hls_video.h"

// 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"

这里的区别是,额外定义了两个宏拟函数,用作内联。

#define ABSDIFF(x,y)	((x>y)? x - y : y - x)
#define ABS(x)          ((x>0)? x : -x)

注意,虽然宏拟函数是类型不安全的内联函数,但是在确知传入的参数类型合法时,宏拟函数显然是高效的。

然后是typedef 类型定义部分

// typedef video library core structures
typedef hls::stream<ap_axiu<16,1,1,1> >               AXI_STREAM;
typedef hls::Scalar<2, unsigned char>                 YUV_PIXEL;
typedef hls::Mat<MAX_HEIGHT, MAX_WIDTH, HLS_8UC2>     YUV_IMAGE;
typedef hls::Scalar<3, unsigned char>                 RGB_PIXEL;
typedef hls::Mat<MAX_HEIGHT, MAX_WIDTH, HLS_8UC3>     RGB_IMAGE;
typedef hls::Window<3, 3, unsigned char>              Y_WINDOW;
typedef hls::LineBuffer<3, MAX_WIDTH, unsigned char>  Y_BUFFER;

然后是声明函数原型。

void image_filter(
	AXI_STREAM& INPUT_STREAM, 
	AXI_STREAM& OUTPUT_STREAM, 
	int rows, int cols,
    int C_XR0C0, int C_XR0C1, int C_XR0C2, 
    int C_XR1C0, int C_XR1C1, int C_XR1C2, 
    int C_XR2C0, int C_XR2C1, int C_XR2C2,
    int C_YR0C0, int C_YR0C1, int C_YR0C2, 
    int C_YR1C0, int C_YR1C1, int C_YR1C2, 
    int C_YR2C0, int C_YR2C1, int C_YR2C2,
    int c_high_thresh, int c_low_thresh, int c_invert);

+++++++++++++++++++++++++++++++++++++++++++++++++++++
来看看top.cpp文件。
先来看看top函数image_filter。

void image_filter(AXI_STREAM& video_in, AXI_STREAM& video_out, int rows, int cols,
                  int C_XR0C0, int C_XR0C1, int C_XR0C2, int C_XR1C0, int C_XR1C1, int C_XR1C2, int C_XR2C0, int C_XR2C1, int C_XR2C2,
                  int C_YR0C0, int C_YR0C1, int C_YR0C2, int C_YR1C0, int C_YR1C1, int C_YR1C2, int C_YR2C0, int C_YR2C1, int C_YR2C2,
                  int c_high_thresh, int c_low_thresh, int c_invert)
{
    //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

#pragma HLS INTERFACE s_axilite port=C_XR0C0 bundle=CONTROL_BUS
#pragma HLS INTERFACE s_axilite port=C_XR0C1 bundle=CONTROL_BUS
#pragma HLS INTERFACE s_axilite port=C_XR0C2 bundle=CONTROL_BUS
#pragma HLS INTERFACE s_axilite port=C_XR1C0 bundle=CONTROL_BUS
#pragma HLS INTERFACE s_axilite port=C_XR1C1 bundle=CONTROL_BUS
#pragma HLS INTERFACE s_axilite port=C_XR1C2 bundle=CONTROL_BUS
#pragma HLS INTERFACE s_axilite port=C_XR2C0 bundle=CONTROL_BUS
#pragma HLS INTERFACE s_axilite port=C_XR2C1 bundle=CONTROL_BUS
#pragma HLS INTERFACE s_axilite port=C_XR2C2 bundle=CONTROL_BUS

#pragma HLS INTERFACE s_axilite port=C_YR0C0 bundle=CONTROL_BUS
#pragma HLS INTERFACE s_axilite port=C_YR0C1 bundle=CONTROL_BUS
#pragma HLS INTERFACE s_axilite port=C_YR0C2 bundle=CONTROL_BUS
#pragma HLS INTERFACE s_axilite port=C_YR1C0 bundle=CONTROL_BUS
#pragma HLS INTERFACE s_axilite port=C_YR1C1 bundle=CONTROL_BUS
#pragma HLS INTERFACE s_axilite port=C_YR1C2 bundle=CONTROL_BUS
#pragma HLS INTERFACE s_axilite port=C_YR2C0 bundle=CONTROL_BUS
#pragma HLS INTERFACE s_axilite port=C_YR2C1 bundle=CONTROL_BUS
#pragma HLS INTERFACE s_axilite port=C_YR2C2 bundle=CONTROL_BUS

#pragma HLS INTERFACE s_axilite port=c_high_thresh bundle=CONTROL_BUS
#pragma HLS INTERFACE s_axilite port=c_low_thresh bundle=CONTROL_BUS
#pragma HLS INTERFACE s_axilite port=c_invert bundle=CONTROL_BUS

    YUV_IMAGE img_0(rows, cols);
    YUV_IMAGE img_1(rows, cols);
#pragma HLS dataflow
    hls::AXIvideo2Mat(video_in, img_0);
    sobel_filter_core(img_0, img_1, rows, cols,
                      C_XR0C0, C_XR0C1, C_XR0C2, C_XR1C0, C_XR1C1, C_XR1C2, C_XR2C0, C_XR2C1, C_XR2C2,
                      C_YR0C0, C_YR0C1, C_YR0C2, C_YR1C0, C_YR1C1, C_YR1C2, C_YR2C0, C_YR2C1, C_YR2C2,
                      c_high_thresh, c_low_thresh, c_invert);
    hls::Mat2AXIvideo(img_1, video_out);
}

整个程序按照上下游顺序进行任务划分,各个任务之间使用mat对象完成交接。

设计的核心处理程序,是其中调用的子程序sobel_filter_core。

函数的接口约束,分为数据路径和配置路径,数据路径的形参,分别约束为axis接口,配置路径的形参,全部约束到同一个axilite总线中。
值得注意的是,rows和cols,额外施加了ap_stabel约束,这样,HLS在处理时,可以对它们做跟多的资源优化。

+++++++++++++++++++++++++++++++++++++++++++++++++
来看看testbench。
和之前的例子并无区别。

+++++++++++++++++++++++++++++++++++++++++++++++
来看看sobel_filter_core子程序。

void sobel_filter_core(
	YUV_IMAGE& src, 
	YUV_IMAGE& dst, 
	int rows, int cols,
    int C_XR0C0, int C_XR0C1, int C_XR0C2, 
    int C_XR1C0, int C_XR1C1, int C_XR1C2, 
    int C_XR2C0, int C_XR2C1, int C_XR2C2,
    int C_YR0C0, int C_YR0C1, int C_YR0C2, 
    int C_YR1C0, int C_YR1C1, int C_YR1C2, 
    int C_YR2C0, int C_YR2C1, int C_YR2C2,
    int c_high_thresh, int c_low_thresh, int c_invert)
{
  	Y_BUFFER buff_A;
  	Y_WINDOW buff_C;

	unsigned char temp;
    YUV_PIXEL tempx;
  	YUV_PIXEL edge;
  	
  	for(int row = 0; row < rows+1; row++){
    	for(int col = 0; col < cols+1; col++){
#pragma HLS loop_flatten off
#pragma HLS dependence variable=&buff_A false
#pragma HLS PIPELINE II = 1
      		
      		//Line Buffer fill
      		if(col < cols){		
          		temp = buff_A.getval(0,col);
          		buff_A.shift_down(col);
      		}      		
	      	if(col < cols && row < rows){
	          	YUV_PIXEL new_pix;
	          	src >> new_pix;
	          	tempx = new_pix;
	          	buff_A.insert_bottom(tempx.val[0],col);
	      	}
	      	
      		buff_C.shift_right();
      		if(col < cols){
          		buff_C.insert(buff_A.getval(2,col),2,0);
          		buff_C.insert(temp,1,0);
          		buff_C.insert(tempx.val[0],0,0);
      		}
      		

      		//The sobel operator only works on the inner part of the image
      		//This design assumes there are no edges on the boundary of the image
      		if( row <= 1 || col <= 1 || row > (rows-1) || col > (cols-1)){
          		edge.val[0] = 0;
          		edge.val[1] = 128;
      		} 
      		else {
          		//Sobel operation on the inner portion of the image
          		edge = sobel_operator(&buff_C,
                                C_XR0C0, C_XR0C1, C_XR0C2, C_XR1C0, C_XR1C1, C_XR1C2, C_XR2C0, C_XR2C1, C_XR2C2,
                                C_YR0C0, C_YR0C1, C_YR0C2, C_YR1C0, C_YR1C1, C_YR1C2, C_YR2C0, C_YR2C1, C_XR2C2,
                                c_high_thresh, c_low_thresh, c_invert);
      		}

      		if(row > 0 && col > 0) {
          		dst << edge;
      		}
    	}
  	}
}

与之前的例子不同的是,本函数的数据路径,不是axivideo,而是mat。
在顶层函数中,已经施加了函数级的dataflow约束,
所以,虽然这里出现的形参对象是mat,我们仍然要清楚,在编码风格上,仍然要严格遵守符合stream的编码风格,
寻址访问单调递增,每个元素只访问一次,要么只读,要么只写。

这里使用了HLS提供的linebuffer和window类。
虽然我们也可以手工设计离散的行缓冲和离散的窗口,但是这并不符合OOD的设计思想。
如果要符合OOD设计思想,我们需要将自己设计的行缓冲和窗口封装成class,
那么既然如此,我们又为什么不使用HLS提供的linebuffer和window呢?

typedef hls::LineBuffer<3, MAX_WIDTH, unsigned char>  Y_BUFFER;

我们已经在H文件中,将模板类linbuffer具象实例化成了一个具象类,并typedef了一个名称。
这里可以看出,这个具象类的linebuffer,有3行,每行有1920个元素,每个元素的类型是uchar。
实质上,linebuffer已经是一个二维向量了。

typedef hls::Window<3, 3, unsigned char>              Y_WINDOW;

我们已经在H文件中,将模板类window具象实例化成了一个具象类,并typedef了一个名称。
这里可以看出,这个具象类的window,有3行,3列,每个元素的类型是uchar。

在函数的开始,定义了一个linebuffer的临时对象,一个window的临时对象。

然后,在一个两层嵌套for循环里,进行逐点处理。

++++++++++++++++++++++++++++++++++++++++++
来分析逐点处理的过程。

由于使用了窗口,所以需要边界判断。
一开始,就需要移动linebuffer,腾出位置,为下一步将当前值填充到Linebuffer中做好准备。
在条件控制下,进行移动。

	if(col < cols){      	   	          	
          	temp = buff_A.getval(0,col);
          	buff_A.shift_down(col);
    }

linebuffer的成员函数,shift_down,完成的功能是按列移位:
将指定的列的数据,按照以新盖旧,先旧后新的原则,完成移位。之后,linebuffer中的最新一行中的对应列位置,就腾空了,可以写入当前数据。
linebuffer的成员函数,getval,获取对应行列坐标位置的数据。

然后,是读取当前像素,并填充linebuffer和window。

if(col < cols && row < rows){
	 src >> tempx ;
	 buff_A.insert_bottom(tempx.val[0],col);
}

这里,从src中读取一个像素,暂存到tempx中。
然后,将tempx中的第一个通道的值,插入linebuffer。
这里,使用了linebuffer的成员函数,insert_bottom。

接下来,是处理window。
首先是移动window。

buff_C.shift_right();

使用了window的成员函数,shift_right,将最旧的数据挤出,腾出位置,给最新的数据。

然后是填充window,

if(col < cols){
    buff_C.insert(buff_A.getval(2,col),2,0);
    buff_C.insert(temp,1,0);
    buff_C.insert(tempx.val[0],0,0);
}

使用了window的成员函数,insert,通过制定行号,列号,将数据插入到合适的位置。
如前所述,linebuffer或者window,都是delay chain,index越大,数据越旧。
由于之前已经移动并填充了linebuffer,所以,linebuffer中,已经填入了当前数据,是有效的数据上下文环境。
此时,只需要将对应行列的数据,填入window的每行的列号为0的位置,即可完成对window的填充。
temp中暂存的,是linebuffer移动前的最新数据,所以移动后,就是delay[1]的数据,
tempx暂存的,是当前数据,也就是delay[0]的数据。

在准备好数据上下文环境后,就可以进入处理过程了。

	  if( row <= 1 || col <= 1 || row > (rows-1) || col > (cols-1)){
          edge.val[0] = 0;
          edge.val[1] = 128;
      } 
      else {
          edge = sobel_operator(&buff_C,
                                C_XR0C0, C_XR0C1, C_XR0C2, C_XR1C0, C_XR1C1, C_XR1C2, C_XR2C0, C_XR2C1, C_XR2C2,
                                C_YR0C0, C_YR0C1, C_YR0C2, C_YR1C0, C_YR1C1, C_YR1C2, C_YR2C0, C_YR2C1, C_XR2C2,
                                c_high_thresh, c_low_thresh, c_invert);
      }

由于使用了窗口,所以必须要有边界判断。
使用条件控制来完成边界判断。
如果是有效区域,说明window中的数据也都是有效的,可以执行完整的算子运算,这里调用了子函数sobel_operator。
如果是在边界上,那么window中的数据也就不全是有效的,不能执行完整的算子运算。
这里,对于边界的处理,采用的方法是,常数化。

逐点处理的最后一步,

			if(row > 0 && col > 0) {
          		dst << edge;
      		}

将临时对象中的结果,写入结果对象中。

+++++++++++++++++++++++++++++++++++++++++++++++++++
来看看子函数sobel_operator。

YUV_PIXEL sobel_operator(Y_WINDOW *window,
                   int XR0C0, int XR0C1, int XR0C2, int XR1C0, int XR1C1, int XR1C2, int XR2C0, int XR2C1, int XR2C2,
                   int YR0C0, int YR0C1, int YR0C2, int YR1C0, int YR1C1, int YR1C2, int YR2C0, int YR2C1, int YR2C2,
                   int high_thresh, int low_thresh, int invert)
{
  short x_weight = 0;
  short y_weight = 0;

  short edge_weight;
  unsigned char edge_val;
  YUV_PIXEL pixel;

  char i;
  char j;


  const char x_op[3][3] = {
   
   {XR0C0,XR0C1,XR0C2},
                           {XR1C0,XR1C1,XR1C2},
                           {XR2C0,XR2C1,XR2C2}};

  const char y_op[3][3] = {
   
   {YR0C0,YR0C1,YR0C2},
                           {YR1C0,YR1C1,YR1C2},
                           {YR2C0,YR2C1,YR2C2}};


  //Compute approximation of the gradients in the X-Y direction
  for(i=0; i < 3; i++){
    for(j = 0; j < 3; j++){

      // X direction gradient
      x_weight += (window->getval(i,j) * x_op[i][j]);

      // Y direction gradient
      y_weight += (window->getval(i,j) * y_op[i][j]);
    }
  }

  edge_weight = ABS(x_weight) + ABS(y_weight);

  if (edge_weight < 255)
    edge_val = (255-(unsigned char)(edge_weight));
  else
    edge_val = 0;

  //Edge thresholding
  if(edge_val > high_thresh)
    edge_val = 255;
  else if(edge_val < low_thresh)
    edge_val = 0;

  // Invert
  if (invert == 1)
    edge_val = 255 - edge_val;
  else
    edge_val = edge_val;

  pixel.val[0] = edge_val;
  pixel.val[1] = 128;

  return pixel;
}

内部定义了临时数组,x_op和y_op,用来存放参数,这是一个良好的编码风格,形参变量只读取了一次,后面的操作全部基于临时数组完成。

用一个两层嵌套for循环,描述算子遍历,即算子内逐点处理。
sobel算子中,
首先实现的是卷积操作。在C描述中,归结为窗口与系数模板的逐点乘累加。
由于需要在两个维度上实现卷积,所以,在C描述中,窗口分别与两个系数模板完成了逐点乘累加。
得到x_weight 和y_weight 。
然后实现的是绝对值求和。得到edge_weight。
然后,对edge_weight进行处理,映射到edge_val。
然后,对edge_val进行阈值判断,二值化。
然后,根据形参的要求,对edge_val进行反色处理。
然后,将edge_val填充到临时变量pixel中。
最后,将pixel返回给调用者。

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

猜你喜欢

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