Zynq UltraScale+ MPSoC智能视频平台5:HLS实现Bayer转RGB

发布于:2022-11-03 ⋅ 阅读:(222) ⋅ 点赞:(0)

  HLS是Xilinx的高级开发工具,高级的地方不止体现在可以使用C语言写Verilog,而且还有点曲高和寡的意思,复杂的语法逻辑让初学者摸不着头脑,学习曲线非常的抖,水也非常的深。一方面惊叹于当时开发这套工具的人有多NB,另一方面又替这门技术担忧,简单易用的编程逻辑永远是编程语言传播的第一要义,如果HLS做到可以和Python语言那样,我也不会想着写一些HLS的博客,做一些HLS的教程。话说回来,难归难,这个工具用起来是真心好用。如果能够忍受HLS工具学习过程中的不确定性、所见非所得和莫名其妙的错误,那么它是你做FPGA逻辑的神器!尤其在现在各行各业都这么卷的环境下,卷什么都是卷,还不如卷一个准入门槛比较高的工具,绝对不会辜负你的时间。

  Xilinx的很多IP核尤其是图像领域,大量的使用HLS工具实现。图像是矩阵数据,非常适合HLS工具中的并行处理。HLS丰富的接口尤其是对AXI4协议的支持,可以轻松实现数据流、配置寄存器和读写DDR等操作。上节讲到的Sensor Demosaic就是使用HLS实现的,在完成底层FPGA逻辑的实现后,相应的驱动会集成在向Vitis提供的xsa驱动文件中,用户只用执行简单的几句调用语句就可以配置FPGA。只是Sensor Demosaic没有调试成功,出来的图像颜色不对,所以决定自己写一个。这个就是HLS工具带来的生产力,你如果觉得某个IP核不好或者不会用,那么可以立即自己做一个出来,在其他的一些图像算法上也一样,HLS的快速实现给了我们足够多的底气。

  ug902是HLS工具的官方文档,也是一开始学习HLS必看的手册,类似于教课书。教科书的特点是讲的特别全面,所有用到和设计到的知识都会讲,缺点是光看这个手册有点云里雾里,发散性太强,没有重点,也没有足够多的实例Demo,而且其中的好多指令以及代码标准在实际中并不会用到,相信大家在学习其他编程语言看教科书的过程中也有相似的体会。如果单从设计一个图像处理IP核的角度出发,那就变得简单多了,不用操心根本用不到的优化指令,只用在三个方面把握好了,基本就没有问题:接口、II=1和仿真。

  接口

  如果大家有关注过Xilinx自己使用HLS实现的一些图像IP的话应该对于AXI4的接口不陌生,包括Axi Lite、Axi Full和Axi Stream,前边在讲标准接口时也提到这些AXI4的接口在Vivado中用的非常多,能做到即插即用,非常的好使。Verilog中可以按照例程手写出这些接口,那么在HLS中是怎么实现的呢?答案是interface的pragma命令。

void AxiStream_Bayer2RGB(stream<type_streamin> &DataIn, stream<type_streamout> &DataOut,
								unsigned short InputWidth, unsigned short InputHeight)
{
#pragma HLS INTERFACE axis register both port=DataIn
#pragma HLS INTERFACE axis register both port=DataOut
#pragma HLS INTERFACE s_axilite port=InputWidth
#pragma HLS INTERFACE s_axilite port=InputHeight
#pragma HLS INTERFACE s_axilite port=return

......
}

  上述命令的结果是DataIn和DataOut使用Axi Stream接口,InputWidth和InputHeight这两个参数使用Axi Lite接口,最后一句pragma代表这个IP核的启动、停止以及查询和使能IP核中断等操作使用Axi Lite接口控制,不受FPGA的信号控制,FPGA中也看不到这些控制信号了。有兴趣可以在加和不加pragma后编译,看生成的IP核长什么样子。下图是利用上述接口命令后IP核在Vivado中的样子,非常的简洁。

    II=1

  HLS中有一个非常重要的名词”启动间隔“,官方释义是:“Initiation interval (II): Number of clock cycles before the function can accept new input data”,新数据输入时需要等待的时钟个数。这个指标越小表示功能的吞吐量越大,II的最小值是1,也就是说,一个时钟可以接收一个数据,亦即一个时钟处理一个数据。如果能够做到II=1,那么所有的类似于带有valid信号数据都可以输入到模块中并且得到实时处理,这也是带有行场同步信号的图像数据所想要的,相机产生一个数据,后端处理模块能够立马消化掉,这样的方式不用使用额外的BRAM或者DDR缓存数据,实时处理且消耗资源少。

  所以设计出II=1是我们的最终目标,也是HLS的难点所在,考验的是FPGA、C语言以及HLS使用的综合能力,既不能无脑堆资源,又不能对某一种资源倾斜,前者往往会导致综合不过去,后者会导致资源不够,找到巧妙的设计能平衡资源和性能才是设计成功的关键。

   如上图所示为一幅Bayer图像数据,第4行4列的B1为Bayer像素,如果要恢复出这一点的RGB数据,一个简单的算法如下,即周围相邻像素取均值来求出缺失的R和G。

R = (R1 + R2 + R3 + R4) / 4
G = (G1 + G2 + G3 + G4) / 4
B = B1

  这就涉及到一个问题,在CPU中,图像是存储在内存中的,CPU想随便访问哪一个数据都可以,但是FPGA不一样,图像数据是一个接一个在FPGA中串行流动的,没有存储,同时访问多个数据是不可能的。所以,得首先定义一个缓存,但是这个缓存又不能太大,在CPU得内存中,上万张图片都可以存得下,FPGA中,一张图就把BRAM资源用没。从算法的层面也可以看出,也不需要缓存整张图片,缓存目标像素周围的像素就可以了,进一步,图像像素是一行一行来的,按横坐标递增的方式,我们在内存中缓存两行旧的数据,和新的一行实时数据组成3行,这样就满足了算法3x3矩阵需求。

  又遇到一个新的问题,如果了解FPGA的话,FPGA存储数据有两种方式:寄存器和BRAM。寄存器可以随机且同时访问但能存储的数据量不多,BRAM每次只能访问1~2个数据但是存储量大。两行行缓存的数据量是2*1620,这个用寄存器实现不太现实,没有那么多寄存器,即使有,HLS工具也不可能综合出结果(往往是HLS运行几个小时跑死在那里),用BRAM的问题是没办法同时访问3x3矩阵的9个数据。这个矛盾如何解决呢,可以两种资源结合使用,BRAM用于存储行数据,寄存器存储3x3矩阵的9个数据,在BRAM行缓存上滑窗得到寄存器数据,这样就完美的解决了问题!也实现了数据得流水。

  如上图所示,绿色的实线框代表BRAM存储的两行缓存,蓝色代表实时数据,红色代表寄存器实现的窗(window),存储9个数据。它们3部分分工合作,各司其职,行缓存实现向window提供前两行数据,实时数据提供给行缓存和window,window滑窗改变数据在窗中的位置,并同时送入计算模块。HLS提供hls::Window这个类以及它的类函数insert_pixel和shift_pixels_left,满足滑窗以及写入数据等的所有操作。

  在拿到了含有9个Bayer数据的window之后,恢复出RGB的计算就非常的简单。大家观察Bayer图像矩阵可以发现一个规律,如上图所示,如果当前Bayer像素是R的话,那么相邻像素的排布方式和Window 1一致;如果当前Bayer像素是G的话,那么相邻像素的排布方式和Window 2或Window3一致,具体需要用奇数行还是偶数行区分;如果当前Bayer像素是B的话,那么相邻像素的排布方式和Window 4一致。所以我们能够写出恢复RGB的公式,

ap_uint<24> RecoverR(Window<3, 3, unsigned char> ImageWindow)
{
	unsigned char R;
	unsigned char G;
	unsigned char B;
	ap_uint<24> RGB;

	R = ImageWindow.getval(1, 1);
	G = (ImageWindow.getval(0, 1) + ImageWindow.getval(1, 0) + ImageWindow.getval(1, 2) + ImageWindow.getval(2, 1)) / 4;
	B = (ImageWindow.getval(0, 0) + ImageWindow.getval(0, 2) + ImageWindow.getval(2, 0) + ImageWindow.getval(2, 2)) / 4;

	RGB.range(23, 16) = R;
	RGB.range(15, 8) = G;
	RGB.range(7, 0) = B;

	return RGB;
}

ap_uint<24> RecoverG(Window<3, 3, unsigned char> ImageWindow, unsigned char isEven)
{
	unsigned char R;
	unsigned char G;
	unsigned char B;
	ap_uint<24> RGB;

	G = (ImageWindow.getval(0, 1) + ImageWindow.getval(1, 0) + ImageWindow.getval(1, 1) + ImageWindow.getval(1, 2) + ImageWindow.getval(2, 1)) / 5;
	if(isEven == 0)
	{
		R = (ImageWindow.getval(1, 0) + ImageWindow.getval(1, 2)) / 2;
		B = (ImageWindow.getval(0, 1) + ImageWindow.getval(2, 1)) / 2;
	}
	else
	{
		R = (ImageWindow.getval(0, 1) + ImageWindow.getval(2, 1)) / 2;
		B = (ImageWindow.getval(1, 0) + ImageWindow.getval(1, 2)) / 2;
	}

	RGB.range(23, 16) = R;
	RGB.range(15, 8) = G;
	RGB.range(7, 0) = B;

	return RGB;
}

ap_uint<24> RecoverB(Window<3, 3, unsigned char> ImageWindow)
{
	unsigned char R;
	unsigned char G;
	unsigned char B;
	ap_uint<24> RGB;

	R = (ImageWindow.getval(0, 0) + ImageWindow.getval(0, 2) + ImageWindow.getval(2, 0) + ImageWindow.getval(2, 2)) / 4;
	G = (ImageWindow.getval(0, 1) + ImageWindow.getval(1, 0) + ImageWindow.getval(1, 2) + ImageWindow.getval(2, 1)) / 4;
	B = ImageWindow.getval(1, 1);

	RGB.range(23, 16) = R;
	RGB.range(15, 8) = G;
	RGB.range(7, 0) = B;

	return RGB;
}

  具体三个函数什么时候调用,观察Bayer图像矩阵中RGB的位置可以得到如下得遍历条件:在偶数行偶数列时调用RecoverR,在奇数行奇数列时调用RecoverB,其余情况调用RecoverG,并将奇偶行表达式送入参数。

	for(i = 0; i < InputHeight; i++)
	{
#pragma HLS LOOP_TRIPCOUNT min=1024 max=1024 avg=1024
		for(j = 0; j < InputWidth; j++)
		{
#pragma HLS LOOP_TRIPCOUNT min=1024 max=1024 avg=1024
#pragma HLS PIPELINE
            ......

			if((i % 2 == 0) && (j % 2 == 0))
			{
				StreamDataOut.data = RecoverR(ImageWindow);
			}
			else if((i % 2 == 1) && (j % 2 == 1))
			{
				StreamDataOut.data = RecoverB(ImageWindow);
			}
			else
			{
				StreamDataOut.data = RecoverG(ImageWindow, i % 2);
			}

			......
		}
	}

   综合后生成报告,各部分的资源使用量非常的少,两个BRAM存储用于行缓存,2个DSP48E用于计算,还有少量的FF即LUT实现控制逻辑以及一个3x3的window。

  仿真

  FPGA在仿真时遇到图像等大量数据的情况下,运行速度非常的慢,也显示不了图片,仿真结果会不好观察。然而HLS在仿真方面却非常的强大,毫不夸张的说,能将开发效率提升100倍,简直是效率神器。主要原因得益于C语言的仿真环境,可以轻松的加载和写入文件,这些文件使用Matlab或ImageJ查看能迅速的看到结果,从而返回来迭代,30秒即可运行完一次仿真并查看,这个是在Vivado纯Isim环境下不可能达到的。仿真代码也比较简单,和C语言的函数调用一致,传入参数,数据流的输入和输出采用write和read函数。

#include "AxiStream_Bayer2RGB.h"

enum
{
	InputWidth = 1620,
	InputHeight = 1236
};

unsigned char ucTempIn[InputWidth * InputHeight];
unsigned char ucTempOut[InputWidth * InputHeight * 3];

int main() {
	stream<type_streamin> DataIn;
	stream<type_streamout> DataOut;
	int i, j;
	type_streamin StreamDataIn;
	type_streamout StreamDataOut;
	FILE *fp;

	fp = fopen("DataIn.raw", "rb");
	fread(ucTempIn, 1, InputWidth * InputHeight, fp);

	for(i = 0; i < InputHeight * InputWidth; i++)
	{
		StreamDataIn.data = ucTempIn[i];
		DataIn.write(StreamDataIn);
	}
	fclose(fp);

	AxiStream_Bayer2RGB(DataIn, DataOut, InputWidth, InputHeight);

	fp = fopen("DataOut.raw","wb");
	for(i = 0; i < InputHeight * InputWidth; i++)
	{
		DataOut.read(StreamDataOut);
		ucTempOut[3 * i] = StreamDataOut.data.range(7, 0);
		ucTempOut[3 * i + 1] = StreamDataOut.data.range(15, 8);
		ucTempOut[3 * i + 2] = StreamDataOut.data.range(23, 16);
	}
	fwrite(ucTempOut, 1, InputWidth * InputHeight * 3, fp);
	fclose(fp);

	return 0;
}

  仿真用到的所有代码如上,从DataIn.raw中读出Bayer图像数据写入数组,然后写入DataIn的Stream数据流中,AxiStream_Bayer2RGB处理输入数据流,将处理后的数据写入Stream输出数据流,最后写入DataOut.raw文件中,使用ImageJ查看DataIn.raw和DataOut.raw文件,可以看到成功的复原出了RGB颜色。

  综合和仿真都没有问题,就可以生成IP核,生成IP核的步骤查看官方文档ug902,也比较简单。IP核在Vivado中的使用和上节讲到的官方的Sensor Demosaic一致,连线也一摸一样,驱动函数也类似,可以说自己实现的AxiStream_Bayer2RGB是官方的重构版,区别是在这个IP核前边多了一个时序调整的IP核,目的是将Stream进来的信号按照图片的usr和last整理,这个功能也可以放入AxiStream_Bayer2RGB中,但是为了方便其他IP核也可以使用这个功能,就独立了出来。

  把官方IP通过自己的方式重新实现了,在板子上运行后效果也非常的nice,触类旁通,再多再复杂的功能都能通过HLS做出来,而且效率比传统Verilog快上不知道多少倍,这也就是HLS的NB之处。

 


网站公告

今日签到

点亮在社区的每一天
去签到