HLS第三十五课(XAPP1167,基于videolib实现图像处理)

XAPP1167几个例子,是基于videolib实现图像处理的。

最核心的概念,是hls::Mat。这是image class。
例如:

cv::Mat image(1080, 1920, CV_8UC3);

这是CV中的表示方法。

hls::Mat<2047, 2047, HLS_8UC3> image(1080, 1920);
//hls::Mat<1080, 1920, HLS_8UC3> image();

这是hls中的表示方法。

来看一个例子,scale,

cv::Mat src(1080, 1920, CV_8UC3);
cv::Mat dst(1080, 1920, CV_8UC3);
cvScale(src, dst, 2.0, 0.0);

这是CV中的算法编程。

hls::Mat<1080, 1920, HLS_8UC3> src;
hls::Mat<1080, 1920, HLS_8UC3> dst;
hls::Scale(src, dst, 2.0, 0.0);

这是HLS中的算法描述。

++++++++++++++++++++++++++++++++++++++++
HLS中,TOP函数接口,通常是video AXIS,即(axivideo),要想进行图像处理,首先要转换成MAT。

hls::AXIvideo2Mat
hls::Mat2AXIvideo

这两个函数完成这个任务。

如果在CSIM中使用,有一些不可综合的图像转换函数可以使用,

hls::cvMat2AXIvideo
hls::IplImage2AXIvideo
hls::CvMat2AXIvideo

hls::AXIvideo2cvMat
hls::AXIvideo2IplImage
hls::AXIvideo2CvMat

Streaming access also implies that if an image is processed by more than one function, then it must first be duplicated into two streams, such as by using the hls::Duplicate<> function.

hls::Duplicate<>

++++++++++++++++++++++++++++++++++++++++
第一个例子,pass through
首先第一部分,将输入的axivideo转换成mat,
然后第二部分,由于不对mat进行任何处理,所以中间的处理部分为空,
然后第三部分,将mat转换成axivideo输出。

在工程文件组织上,首先是一个H文件top.h。
首先是头文件保护宏。

#ifndef _TOP_H_
#define _TOP_H_

然后包含库文件。

#include "hls_video.h"

需要包含hls_video.h文件。

然后定义各种常量宏。

#define MAX_WIDTH  1920
#define MAX_HEIGHT 1080

#define INPUT_IMAGE           "test_1080p.bmp"
#define OUTPUT_IMAGE          "result_1080p.bmp"
#define OUTPUT_IMAGE_GOLDEN   "result_1080p_golden.bmp"

然后定义各种typedef类型。

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;

输入的图像是YUV422,所以stream中的元素,是ap_axiu<16,1,1,1>,TDATA是16位的。对于axivideo,后面三个参数都是1。
由YUV422组成的图像,是两个通道的,所以mat中的数据类型是HLS_8UC2。
类似的,由RGB444组成的图像,是三个通道的,所以mat中的数据类型是HLS_8UC3。
用scalar定义了像素对象,对于unsigned char类型的2个通道的像素对象,定义为YUV_PIXEL。
用scalar定义了像素对象,对于unsigned char类型的3个通道的像素对象,定义为RGB_PIXEL。

然后是声明函数原型。

void image_filter(AXI_STREAM& src_axi, AXI_STREAM& dst_axi, int rows, int cols);

来看TOP.cpp文件。
首先是包含所需的对应同名头文件。

#include "top.h"

然后是函数主体。

void image_filter(
	AXI_STREAM& video_in, 
	AXI_STREAM& 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=return bundle=CONTROL_BUS
#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 ap_stable port=rows
#pragma HLS INTERFACE ap_stable port=cols

    YUV_IMAGE img_0(rows, cols);
    
#pragma HLS dataflow
    hls::AXIvideo2Mat(video_in, img_0);
    hls::Mat2AXIvideo(img_0, video_out);
}

这个函数,是最终被HLS实现为模块的TOP函数。
先看输入参数。
数据路径上,是两个axivideo,所以,这里我们用了两个axivideo对象的引用作为形参。
配置路径上,是两个integer,所以,这里我们用了另个int变量作为形参变量。

函数中定义了临时变量img_0,作为上下游任务之间的交接数据。
任务已经被划分为了上下游顺序,在函数级,使用dataflow约束。

重点注意这里使用的接口约束,
数据路径上的两个形参,约束到axis接口。
配置路径上的形参,以及返回值,被约束到AXILITE总线中。
这些配置参数,我们希望它们在reset之后就不再被修改,所以额外施加了ap_stable约束。通常不推荐使用这个额外约束。
这样,在使用时,CPU需要先通过AXILITE总线写入配置参数,然后再reset这个模块。

+++++++++++++++++++++++++++++++++++++++++++
来看看一个testbench。这个testbench基于opencv。
首先是包含头文件。

#include "top.h"
#include "hls_opencv.h"

#include "opencv_top.h"
#include "image_utils.h"

其中调用了DUT,所以包含了top.h。
我们需要使用到hlsopencv库,所以包含了hls_opencv.h。
我们额外定义了一些可以在csim中使用的不可综合的函数,所以包含了opencv_top.h。
另一部分可以在csim中使用的不可综合的函数,包含在了imag_utils.h中。

然后指定默认命名空间。

using namespace cv;

然后是csim的函数主体。

int main (int argc, char** argv) 
{
	int ret;
    Mat src_rgb = imread(INPUT_IMAGE);
    if (!src_rgb.data) {
        printf("ERROR: could not open or find the input image!\n");
        return -1;
    }
    
    Mat src_yuv(src_rgb.rows, src_rgb.cols, CV_8UC2);
    Mat dst_yuv(src_rgb.rows, src_rgb.cols, CV_8UC2);
    Mat dst_rgb(src_rgb.rows, src_rgb.cols, CV_8UC3);
    
    cvtcolor_rgb2yuv422(src_rgb, src_yuv);
    
    IplImage src = src_yuv;
    IplImage dst = dst_yuv;
    
    hls_image_filter(&src, &dst);
    
    cvtColor(dst_yuv, dst_rgb, CV_YUV2BGR_YUYV);
    imwrite(OUTPUT_IMAGE, dst_rgb);

    opencv_image_filter(&src, &dst);
    cvtColor(dst_yuv, dst_rgb, CV_YUV2BGR_YUYV);
    imwrite(OUTPUT_IMAGE_GOLDEN, dst_rgb);

	ret = image_compare(OUTPUT_IMAGE, OUTPUT_IMAGE_GOLDEN);
	if(ret)
		printf("compare failed");
	else
		printf("compare passed");
    return ret; 
}

整个testbench大致分为几个部分。
pre-process,
主要是imread,从文件中读图像,保存到cv::mat对象中。
定义多个临时对象mat,作为上下游任务间的交接对象。由于cv::mat是包含多个属性的,例如rows,cols,所以可以从src_image中抽取这些属性,作为后续的其他mat的参考。
输入的图像如果是bmp格式,那么就是RGB444的。
用到了色彩变换。在Csim中,是用函数实现的,这些函数在image_utils.h中定义。

call dut,
定义的TOP函数,并没有在csim中被直接调用,而是经过了一层封装hls_image_filter,可以在csim中被使用。
IplImage图像对象,会以关联的方式,指向一个cvMat图像对象,当函数操作一个IplImage图像对象时,实际修改的图像数据,位于关联的cvMat中。即,IplImage图像对象,实际上可以理解成它是一个cvMat对象的句柄。

post-process,
先经过色彩变换,将YUV422的Mat对象转换成RGB的Mat对象,
然后再imwrite写图像到文件中。

为了生成金样,在后处理中,额外使用了opencv_image_filter函数,做比对滤波,然后色彩变换,写入金样文件。

compare result,
将DUT的输出结果和金样的输出结果进行比较。

来看看其中用到的函数。
这些函数需要用到hlsopencv的函数,所以要包含头文件。

#include "hls_opencv.h"

来看函数定义。

void hls_image_filter(IplImage *src, IplImage *dst) {
    AXI_STREAM src_axi, dst_axi;
    
    IplImage2AXIvideo(src, src_axi);
    
    image_filter(src_axi, dst_axi, src->height, src->width);
    
    AXIvideo2IplImage(dst_axi, dst);
}

这个函数,封装了TOP函数。
定义了临时对象,作为任务间的交接对象。
首先将IplImage对象转换成axivideo对象,
然后调用TOP函数,
然后将axivideo对象转换成IplImage对象。

void opencv_image_filter(IplImage *src, IplImage *dst) {
    cvCopy(src, dst);
}

直接调用cvcopy函数控制IplImage的数据拷贝。无需转换成axivideo对象。

void cvtcolor_rgb2yuv422(Mat& rgb, Mat& yuv) {
    Mat yuv444(rgb.rows, rgb.cols, CV_8UC3);
    
    cvtColor(rgb, yuv444, CV_BGR2YUV);
    
    // chroma subsampling: yuv444 -> yuv422;
    for (int row = 0; row < yuv444.rows; row++) {
        for (int col = 0; col < yuv444.cols; col+=2) {
            Vec3b p0_in = yuv444.at<Vec3b>(row, col);
            Vec3b p1_in = yuv444.at<Vec3b>(row, col+1);
            Vec2b p0_out, p1_out;
            p0_out.val[0] = p0_in.val[0];
            p0_out.val[1] = p0_in.val[1];
            p1_out.val[0] = p1_in.val[0];
            p1_out.val[1] = p0_in.val[2];
            yuv.at<Vec2b>(row, col) = p0_out;
            yuv.at<Vec2b>(row, col+1) = p1_out;
        }
    }
}

调用cvtColor函数,将RGB444转换成YUV444,
然后手动降采样,将YUV444转换成YUV422。
用一个两层嵌套for循环,进行逐点处理。

int image_compare(const char* output_image, const char* golden_image) {
   if (!(output_image) || !(golden_image)) {
       printf("Failed to open images...exiting.\n");
       return -1;
   } 
  
   Mat o = imread(output_image);
   Mat g = imread(golden_image);
   
   assert(o.rows == g.rows && o.cols == g.cols);
   assert(o.channels() == g.channels() && o.depth() == g.depth());
   printf("rows = %d, cols = %d, channels = %d, depth = %d\n", o.rows, o.cols, o.channels(), o.depth());
   
   int flag = 0;
   
   for (int i = 0; i < o.rows && flag == 0; i++) {
       for (int j = 0; j < o.cols && flag == 0; j++) {
           for (int k = 0; k < o.channels(); k++) {
               unsigned char p_o = (unsigned char)*(o.data + o.step[0]*i + o.step[1]*j + k);
               unsigned char p_g = (unsigned char)*(g.data + g.step[0]*i + g.step[1]*j + k);
               if (p_o != p_g) {
                   printf("First mismatch found at row = %d, col = %d\n", i, j);
                   printf("(channel%2d) output:%5d, golden:%5d\n", k, p_o, p_g);
                   flag = 1;
                   break;
               }
           }
       }
   }
   
   if (flag)
       printf("Test Failed!\n");
   else
       printf("Test Passed!\n");

   return flag;
   
}

在一个两层嵌套for循环中,进行逐点处理。
由于每个点,具有多个通道,这被理解为一个一维向量,所以在处理彩色的像素点时,按照一维向量来处理。所以在最内层,使用一个for循环来处理单个像素点。

+++++++++++++++++++++++++++++++++++++++++++++++++++
来看第二个例子。demo。
首先来看Top.h文件。
头文件保护宏,包含头文件,定义常量宏,这些与之前相同。
定义typedef有一些不同。

typedef hls::stream<ap_axiu<16,1,1,1> >               AXI_STREAM;
typedef hls::Scalar<1, unsigned char>                 PIXEL_C1;
typedef hls::Mat<MAX_HEIGHT, MAX_WIDTH, HLS_8UC1>     IMAGE_C1;
typedef hls::Scalar<2, unsigned char>                 PIXEL_C2;
typedef hls::Mat<MAX_HEIGHT, MAX_WIDTH, HLS_8UC2>     IMAGE_C2;
typedef hls::Scalar<3, unsigned char>                 PIXEL_C3;
typedef hls::Mat<MAX_HEIGHT, MAX_WIDTH, HLS_8UC3>     IMAGE_C3;

用stream定义了YUV422的流对象,命名为AXI_STREAM。
用mat定义了图像矩阵对象,对于8UC1类型的元素,命名为IMAGE_C1。
用mat定义了图像矩阵对象,对于8UC2类型的元素,命名为IMAGE_C2。
用mat定义了图像矩阵对象,对于8UC3类型的元素,命名为IMAGE_C3。
用scalar定义了像素对象,对于unsigned char类型的1个通道的像素对象,定义为PIXEL_C1。
用scalar定义了像素对象,对于unsigned char类型的2个通道的像素对象,定义为PIXEL_C2。
用scalar定义了像素对象,对于unsigned char类型的3个通道的像素对象,定义为PIXEL_C3。

再来看top.cpp文件。
首先是包含头文件。然后是函数主体。

void image_filter(
	AXI_STREAM& video_in, 
	AXI_STREAM& 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=return bundle=CONTROL_BUS
#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 ap_stable port=rows
#pragma HLS INTERFACE ap_stable port=cols

    IMAGE_C2 img_0(rows, cols);
    IMAGE_C2 img_1(rows, cols);
    IMAGE_C2 img_2(rows, cols);
    IMAGE_C2 img_3(rows, cols);
    IMAGE_C2 img_4(rows, cols);
    
    PIXEL_C2 pix(50, 50);
    
#pragma HLS dataflow
    hls::AXIvideo2Mat(video_in, img_0);
    hls::SubS(img_0, pix, img_1);
    hls::Scale(img_1, img_2, 2, 0);
    hls::Erode(img_2, img_3);
    hls::Dilate(img_3, img_4);
    hls::Mat2AXIvideo(img_4, video_out);
}

函数主体内,按照上下游的顺序进行了任务划分。每个任务,通过调用子函数完成。
任务之间,通过临时变量进行数据交接。
这里,使用的是一系列临时对象,作为上下游任务之间的交接对象。
函数级,使用了dataflow约束。

接口约束,与第一个例子相同。

这里使用了typedef的类型来实例化各个临时对象。
对于Mat类型,实例化时,需要传入的参数是rows和cols。
对于scalar类型,实例化时,需要传入的参数是val_c1,val_c2。

上下游顺序下的任务,
首先是将axivideo对象转换成mat对象,
然后是将输入的mat对象,利用pixel的值作为阈值,进行阈值分割,结果输出到另一个mat对象。
然后将上游交接的mat对象,逐个像素的值放大2倍,结果输出到另一个mat对象。
然后将上游交接的mat对象,腐蚀一次,结果输出到另一个mat对象。
然后将上游交接的mat对象,膨胀一次,结果输出到另一个mat对象。
最后将上游交接的mat对象,转换成axivideo对象。

再来看testbench。
大体上和上一个例子相同。
区别在于opencv_image_filter函数。

void opencv_image_filter(IplImage *src, IplImage *dst) {
    IplImage* tmp = cvCreateImage(cvGetSize(src), src->depth, src->nChannels);
    
    cvCopy(src, tmp);
    cvSubS(tmp, cvScalar(50, 50), dst);
    cvScale(dst, tmp, 2, 0);
    cvErode(tmp, dst);
    cvDilate(dst, tmp);
    cvCopy(tmp, dst);
    
    cvReleaseImage(&tmp);
}

这个函数,作为金样使用,里面用CV实现了相同的上下游任务。其中,动态分配了一个堆对象,作为临时对象使用,用一个指针作为句柄,所以,在函数退出时,释放了这个动态分配的堆对象。

 IplImage* tmp = cvCreateImage(cvGetSize(src), src->depth, src->nChannels);
 
 cvReleaseImage(&tmp);

++++++++++++++++++++++++++++++++++++++++++++++++++
再看第三个例子。
top.h和第二个例子相同。
来看TOP函数的主体。

void image_filter(AXI_STREAM& video_in, AXI_STREAM& 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=return bundle=CONTROL_BUS
#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 ap_stable port=rows
#pragma HLS INTERFACE ap_stable port=cols

    IMAGE_C2 img_0(rows, cols);
    IMAGE_C2 img_1_0(rows, cols);
    IMAGE_C2 img_1_1(rows, cols);
    IMAGE_C1 img_1_Y(rows, cols);
    IMAGE_C1 img_1_UV(rows, cols);
    IMAGE_C2 img_2(rows, cols);
    IMAGE_C1 mask(rows, cols);
    IMAGE_C1 dmask(rows, cols);
    PIXEL_C2 color(255,0);
    
#pragma HLS dataflow

#pragma HLS stream depth=20000 variable=img_1_1.data_stream
    hls::AXIvideo2Mat(video_in, img_0);
    hls::Duplicate(img_0, img_1_0, img_1_1);
    hls::Split(img_1_0, img_1_Y, img_1_UV);
    hls::Consume(img_1_UV);
    hls::FASTX(img_1_Y, mask, 20, true);
    hls::Dilate(mask, dmask);
    hls::PaintMask(img_1_1, dmask, img_2, color);
    hls::Mat2AXIvideo(img_2, video_out);
}

函数设计思想,没有什么不同。
主要编码技巧是,上下游顺序任务划分,临时对象作为交接对象,函数级dataflow约束。

这里重点是img_1_1的使用问题。
在整体dataflow的情况下, HLS对上下游任务的数据交接,使用FIFO方式。
但是复制后的img_1_0和img_1_1,它们的消费速度是不同步的。
在数据路径上,img_1_0很快就被下游的任务split消费掉了。但是img_1_1却要等到更下游的paintmask处才能被消费掉。这段时间内,duplicate仍然在不断的生产数据,同时提供给img_1_0和img_1_1。所以,img_1_1需要具备足够大的FIFO depth。

#pragma HLS stream depth=20000 variable=img_1_1.data_stream

这条约束,指定了Mat对象的内部存储数组,显式地被HLS理解为FIFO(stream),并且深度为20000。

来看testbench。
TB和上一个例子相同。

主要区别在于opencv_image_filter。

void opencv_image_filter(IplImage *_src, IplImage *_dst) {
    Mat src(_src);
    Mat dst(_dst);
    
    Mat mask(src.rows, src.cols, CV_8UC1);
    Mat dmask(src.rows, src.cols, CV_8UC1);
    
    std::vector<Mat> layers;
    std::vector<KeyPoint> keypoints;
    
    cvCopy(_src, _dst);
    split(src, layers);
    FAST(layers[0], keypoints, 20, true);
    GenMask(mask, keypoints);
    dilate(mask, dmask, getStructuringElement(MORPH_RECT, Size(3,3), Point(1,1)));
    PaintMask(dst, dmask, Scalar(255,0));
}

定义了两个临时对象,Mat类型。这两个临时对象构造时,参考的是传入的IplImage对象,抽取其中的属性,例如rows和cols。Mat对象中的数据存储数组,和IplImage对象指向的是同一个。即,通过这种方式构造的Mat对象,实际上也是一个句柄。并没有分配全新的数据存储数组。
定义了两个Mat对象,mask和dmask,这两个图像的元素类型都是CV_8UC1。传入的参数,参考自Mat对象src的属性rows和cols。
定义了一个vector容器对象,容器对象的元素类型是Mat,容器对象可以理解为一个一维向量。
定义了一个vector容器对象,容器对象的元素类型是Keypoint,容器对象可以理解为一个一维向量。

函数在执行时,首先复制IplImage对象。
然后调用split,将Mat对象按照通道,分割成三个Mat,并放到vector中。
然后调用FAST函数,从分割出来的Mat对象中(这里是layer[0]),找到keypoint,并放入vector中。
然后调用genmask,从Vector容器对象keypoints中,逐个抽取keypoint,生成Mat对象mask。
然后调用dilate,生成Mat对象dmask。
然后调用paintmask,在Mat对象dst上,打印dmask,指定的颜色为Scalar(255,0)。
注意,Mat对象dst实质上也是一个句柄,和IplImage指针_src,指向是同一个实体图像。

来看看genmask。

void GenMask(Mat &mask, std::vector<KeyPoint> keypoints) {
    for (int i = 0; i < mask.rows; i++) {
        for (int j = 0; j < mask.cols; j++) {
            mask.at<unsigned char>(i, j) = 0;
        }
    }
    
    for (int i = 0; i < keypoints.size(); i++) {
        mask.at<unsigned char>(keypoints[i].pt.y, keypoints[i].pt.x) = 1;
    }
}

首先初始化图像,用一个两层嵌套for循环,逐点处理,将Mat的每个元素,赋值为0。
然后,在一个for循环中,对一维向量进行逐个处理。

再看看paintmask。

void PaintMask(Mat &img, Mat &mask, Scalar s) {
    for (int i = 0; i < mask.rows; i++) {
        for (int j = 0; j < mask.cols; j++) {
            if (mask.at<unsigned char>(i, j) > 0) {
                for (int k = 0; k < img.channels(); k++)
                    *(img.data + img.step[0]*i + img.step[1]*j + k) = s.val[k];
            }
        }
    }
}

用一个两层嵌套for循环,逐点处理。
在逐点处理时,使用条件控制,当mask不为0时,需要修改图像,否则不修改。
在if块内,由于彩色图像每个像素具有三个通道,所以可以单个像素的数据,可以理解为一维向量,所以,这里用一个for循环来处理一个一维向量,逐通道处理。

这里需要注意的是,
Mat对象中的data成员,是一个指针,这是图像数据存储区的base addr。如果需要找到对应的像素,对应的通道,需要手工计算偏移地址,然后赋值。

*(img.data + img.step[0]*i + img.step[1]*j + k) = s.val[k];

step存储的是各级步进值,
step[0]是一行的步进值,
step[1]是一列的步进值,
k则代表一个像素内的通道偏移值。

+++++++++++++++++++++++++++++++++++++++++++++
来看一个逐像素处理的例子。

void image_filter(
	AXI_STREAM& video_in, 
	AXI_STREAM& video_out, 
	int rows, 
	int cols)
{
		...
		YUV_PIXEL p;
		
		for(int row = 0; row < rows; row++) {
//#pragma HLS loop_flatten off
	       for(int col = 0; col < cols; col++) {
#pragma HLS pipeline II=1
	            
	            img_0 >> p;
	
	            p.val[0] &= 0xE0;
	            p.val[1] &= 0xE0;
	
	            img_1 << p;
	        }
	    }
	    ...
}

用一个两层嵌套的for循环,进行逐点处理。
这里要注意的是,逐点处理的流程。
首先,是从源图像的流对象中读出一个像素,给临时对象。
中间的操作过程,都是在临时对象上进行操作。
最后,是将临时对象,写入结果图像的流对象。
这样的代码结构,能够让HLS正确的理解dataflow的数据路径。

PIXEL的成员val,是一个数组,里面每个成员对应一个通道。

+++++++++++++++++++++++++++++++++++++++++++++++++
再来看另一个逐点处理的例子,中值滤波。

void image_filter(
	AXI_STREAM& video_in, 
	AXI_STREAM& video_out, 
	int rows, 
	int cols) 
{
	...
	YUV_PIXEL buffer[3];
	YUV_PIXEL p;
	...
	for(int row = 0; row < rows; row++) {
#pragma HLS loop_flatten off
       for(int col = 0; col < cols; col++) {
#pragma HLS pipeline II=1
            
            img_0 >> p;
            
            buffer[2] = buffer[1];
            buffer[1] = buffer[0];
            buffer[0] = p;

            if((col > 1) && (col < cols )) {
                for(int i = 0; i < 2; i++) {
                    bool a = buffer[2].val[i] > buffer[1].val[i];
                    bool b = buffer[2].val[i] > buffer[0].val[i];
                    bool c = buffer[1].val[i] > buffer[0].val[i];

                    if(a && c) {
                        p.val[i] = buffer[1].val[i];
                    } else if(b && !c) {
                        p.val[i] = buffer[0].val[i];
                    } else {
                        p.val[i] = buffer[2].val[i];
                    }
                }
            }
            else if ((col == 1) || (col  == cols )) {
            	for(i = 0; i < 2; i++){
					bool c = buffer[1].val[i] > buffer[0].val[i];
	
					if(c)
						p.val[i] = buffer[1].val[i];
					else
						p.val[i] = buffer[0].val[i];
				}				
			}
			else{
			}
            
            img_1 << p;
        }
    }
	...
}

这个逐点处理的语句块,区别在于,使用了line window。
流程仍然是和上面的例子类似,
首先是从源图像的流对象中,读出一个像素,暂存到临时对象中,
然后中间是处理过程,处理操作,全部在临时对象上完成,
最后是将临时对象,写入结果图像的流对象中。

由于使用了line window,所以要在处理过程的一开始,就进行window moving处理。
window moving的顺序是,以新盖旧,先旧后新。
先用older数据覆盖oldest数据,然后用newer数据覆盖older数据,依次进行,最后用newest数据覆盖newer数据。
从上面的代码可以看出,buffer[i]就是一个delay chain。index越大,数据越旧。在window moving执行完后,buffer[0]中,就存储的是最新的数据p了。

(window moving 有两种实现方式,上述的是第一种方式,即 moving before process,这里面可以看到,临时对象p和buffer[0],实际上是相同的数据。这相当于浪费了p的存储空间。但是好处也是明显的,就是整个窗口,看起来逻辑清晰。
第二种方式,即moving after process,在最后才移动窗口,这样,每次处理的时候,p要参与进来,作为window 的一部分。
推荐使用的是第一种方式。即 moving before process。)

在使用了line window之后,完整的窗口计算,就局限在了图像的中间区域,边界上的运算,是不完整的运算,所以需要边界处理。
边界处理,是通过循环体内的条件控制语句块来完成的。在本例中,通过控制
if(col == XXX)
来完成边界判断。

注意,一个编码技巧是,连续赋值。这个在line buffer和matrix window中很常见。

t[0] = buffer[0] = p;
//t[0] = (buffer[0] = p);

最终结果是,p先赋值给buffer[0],然后穿透过去,p又赋值给t[0]。

  • 1
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值