当前位置:   article > 正文

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

xapp1167

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

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

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

这是CV中的表示方法。

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

这是hls中的表示方法。

来看一个例子,scale,

cv::Mat src(1080, 1920, CV_8UC3);
cv::Mat dst(1080, 1920, CV_8UC3);
cvScale(src, dst, 2.0, 0.0);
  • 1
  • 2
  • 3

这是CV中的算法编程。

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

这是HLS中的算法描述。

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

hls::AXIvideo2Mat
hls::Mat2AXIvideo
  • 1
  • 2

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

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

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

hls::AXIvideo2cvMat
hls::AXIvideo2IplImage
hls::AXIvideo2CvMat
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

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<>
  • 1

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

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

#ifndef _TOP_H_
#define _TOP_H_
  • 1
  • 2

然后包含库文件。

#include "hls_video.h"
  • 1

需要包含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"
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

然后定义各种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;
  • 1
  • 2
  • 3
  • 4
  • 5

输入的图像是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);
  • 1

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

#include "top.h"
  • 1

然后是函数主体。

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);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23

这个函数,是最终被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"
  • 1
  • 2
  • 3
  • 4
  • 5

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

然后指定默认命名空间。

using namespace cv;
  • 1

然后是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; 
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34

整个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"
  • 1

来看函数定义。

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);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

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

void opencv_image_filter(IplImage *src, IplImage *dst) {
    cvCopy(src, dst);
}
  • 1
  • 2
  • 3

直接调用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;
        }
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20

调用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;
   
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38

在一个两层嵌套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;
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

用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);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34

函数主体内,按照上下游的顺序进行了任务划分。每个任务,通过调用子函数完成。
任务之间,通过临时变量进行数据交接。
这里,使用的是一系列临时对象,作为上下游任务之间的交接对象。
函数级,使用了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);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

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

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

++++++++++++++++++++++++++++++++++++++++++++++++++
再看第三个例子。
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);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34

函数设计思想,没有什么不同。
主要编码技巧是,上下游顺序任务划分,临时对象作为交接对象,函数级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
  • 1

这条约束,指定了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));
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

定义了两个临时对象,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;
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

首先初始化图像,用一个两层嵌套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];
            }
        }
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

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

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

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

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;
	        }
	    }
	    ...
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24

用一个两层嵌套的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;
        }
    }
	...
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54

这个逐点处理的语句块,区别在于,使用了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);
  • 1
  • 2

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

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/2023面试高手/article/detail/592071
推荐阅读
相关标签
  

闽ICP备14008679号