YUV2RGB源码详解(参考Opencv4.1)

目录

引言

知识直通车:

YUV2RGB原语

YUV2RGB NEON加速


引言

opencv4.x版本开始对YUV2RGB做了neon加速,这篇文章对转换源码进行了详细分析,想要了解实现细节的同学可以做个了解,也比较简单。

 

知识直通车:

对YUV结构不了解的看这篇:https://blog.csdn.net/xjhhjx/article/details/80291465

对YUV2RGB不了解的看这篇:https://blog.csdn.net/xiaoyafang123/article/details/82153279

 

YUV2RGB原语

/***********************************************************************
入参:unsigned char* dst_data:目标图像指针 
     size_t dst_step:目标图像每行间隔数据的大小=通道数x宽度
     int dst_width:目标图像宽度
     int dst_height:目标图像高度
     size_t src_step:源图像每行间隔数据的大小=通道数x宽度 
     const unsigned char* y1:源图像y数据指针
     const unsigned char* uv:源图像uv数据指针
*************************************************************************/
inline void cvtYUV420sp2RGB(unsigned char* dst_data, size_t dst_step, int dst_width, int dst_height, size_t src_step, const unsigned char* y1, const unsigned char* uv)
{
	for (int j = 0; j < dst_height; j += 2, y1 += (src_step << 1), uv += src_step) {
		unsigned char* row1 = dst_data + dst_step * j;         //目标图像当前第一行数据指针
		unsigned char* row2 = dst_data + dst_step * (j + 1);   //目标图像当前第二行数据指针
		const unsigned char* y2 = y1 + src_step;               //源图像当前第二行数据指针
		int i = 0;

                //每次求得目标图像的4个像素值(两行,每行两个,每个像素储存rgb三个值,row+6)
		for (; i < dst_width; i += 2, row1 += 6, row2 += 6)
		{
                        //uIdx决定uv的存储顺序,按照YUV格式决定,NV12为UV的存储顺序,NV21为VU的存储顺序
			unsigned char u = uv[i + 0 + uIdx]; 
			unsigned char v = uv[i + 1 - uIdx];

			unsigned char vy01 = y1[i];
			unsigned char vy11 = y1[i + 1];
			unsigned char vy02 = y2[i];
			unsigned char vy12 = y2[i + 1];
            
                        //uv+y转换rgb主函数
			cvtYuv42xxp2RGB8<bIdx, dcn, true>(u, v, vy01, vy11, vy02, vy12, row1, row2);
		}
	}
}

上面为YUV2RGB 的主函数,思路很简单啊:

上下行分别2个y共用一个UV,那么计算的时候直接通过原图像的第一行y1及第二行y2的指针再加上uv,

即可求得目标图像的4个像素的rgb值,分别对于代码的row1及row2(rgb值按通道排列即hwc格式因此for循环每次+6,6=2x3)详细的解释可参考注释。

template<int bIdx, int dcn, bool is420>
static inline void cvtYuv42xxp2RGB8(const unsigned char u, const unsigned char v,
	const unsigned char vy01, const unsigned char vy11, const unsigned char vy02, const unsigned char vy12,
	unsigned char* row1, unsigned char* row2)
{
	int ruv, guv, buv;
    
        //计算rgb中与uv相关的分量ruv、guv、buv
	uvToRGBuv(u, v, ruv, guv, buv);

	unsigned char r00, g00, b00, a00;
	unsigned char r01, g01, b01, a01;

        //结合y以及uv相关的分量ruv、guv、buv计算最终的rgb分量的值
	yRGBuvToRGBA(vy01, ruv, guv, buv, r00, g00, b00, a00);
	yRGBuvToRGBA(vy11, ruv, guv, buv, r01, g01, b01, a01);


        //bIdx为0则为bgr格式,bIdx为2则为rgb格式
	row1[2 - bIdx] = r00;
	row1[1] = g00;
	row1[bIdx] = b00;
	if (dcn == 4)
		row1[3] = a00;//如果转换为rgba格式,a通道赋值为0xff

	row1[dcn + 2 - bIdx] = r01;
	row1[dcn + 1] = g01;
	row1[dcn + 0 + bIdx] = b01;
	if (dcn == 4)
		row1[7] = a01;

        //如果源图片为420采样模式,代表4个y共用uv,因此需要计算第二行的像素值,
        //若为422或者444采样格式则不需要计算,具体可以参考上面给的直通车链接
	if (is420)
	{
		unsigned char r10, g10, b10, a10;
		unsigned char r11, g11, b11, a11;

		yRGBuvToRGBA(vy02, ruv, guv, buv, r10, g10, b10, a10);
		yRGBuvToRGBA(vy12, ruv, guv, buv, r11, g11, b11, a11);

		row2[2 - bIdx] = r10;
		row2[1] = g10;
		row2[bIdx] = b10;
		if (dcn == 4)
			row2[3] = a10;

		row2[dcn + 2 - bIdx] = r11;
		row2[dcn + 1] = g11;
		row2[dcn + 0 + bIdx] = b11;
		if (dcn == 4)
			row2[7] = a11;
	}
}

本段代码实现了uv+y转rgb的功能,相关注释已经很清楚了,内部主要包含的两个函数:uvToRGBuv、yRGBuvToRGBA。uvToRGBuv的功能主要是将uv值转换为最终rgb公式中与uv相关的分量;yRGBuvToRGBA的功能是将uvToRGBuv求得的ruv/guv/buv分量结合y得到最终的rgb分量的值。下面分别介绍这两个函数:

//R = 1.164(Y - 16) + 1.596(V - 128)
//G = 1.164(Y - 16) - 0.813(V - 128) - 0.391(U - 128)
//B = 1.164(Y - 16)                  + 2.018(U - 128)

//定点处理,将各个系数乘以2^20,加1<<19四舍五入
//R = (1220542(Y - 16) + 1673527(V - 128)                  + (1 << 19)) >> 20
//G = (1220542(Y - 16) - 852492(V - 128) - 409993(U - 128) + (1 << 19)) >> 20
//B = (1220542(Y - 16)                  + 2116026(U - 128) + (1 << 19)) >> 20

static inline void uvToRGBuv(const unsigned char u, const unsigned char v, int& ruv, int& guv, int& buv)
{
	int uu, vv;
	uu = int(u) - 128;
	vv = int(v) - 128;

        //const int ITUR_BT_601_CY = 1220542;
        //const int ITUR_BT_601_CUB = 2116026;
        //const int ITUR_BT_601_CUG = -409993;
        //const int ITUR_BT_601_CVG = -852492;
        //const int ITUR_BT_601_CVR = 1673527;
        //const int ITUR_BT_601_SHIFT = 20;

        //计算rgb中uv分量
	ruv = (1 << (ITUR_BT_601_SHIFT - 1)) + ITUR_BT_601_CVR * vv;
	guv = (1 << (ITUR_BT_601_SHIFT - 1)) + ITUR_BT_601_CVG * vv + ITUR_BT_601_CUG * uu;
	buv = (1 << (ITUR_BT_601_SHIFT - 1)) + ITUR_BT_601_CUB * uu;
}
static inline void yRGBuvToRGBA(const unsigned char vy, const int ruv, const int guv, const int buv,
	unsigned char& r, unsigned char& g, unsigned char& b, unsigned char& a)
{
	int yy = int(vy);
        //y-16之后要做饱和处理
	int y = maxValue(0, yy - 16) * ITUR_BT_601_CY; 
	r = saturate_cast<unsigned char>((y + ruv) >> ITUR_BT_601_SHIFT);//除以2^20,还原
	g = saturate_cast<unsigned char>((y + guv) >> ITUR_BT_601_SHIFT);
	b = saturate_cast<unsigned char>((y + buv) >> ITUR_BT_601_SHIFT);
	a = (unsigned char)(0xff);
}

上面两段代码意思很简单了,就是利用y+uv根据转换矩阵计算rgb分量。

需要注意两点:1、对浮点运算做了定点,乘以2^20转换为int,最后将结果再除以2^20

                          2、y-16之后要做饱和处理,不然最后转换出来的图像灰度小的地方就是亮点

 

YUV2RGB NEON加速

uint8x16_t a = vdupq_n_u8((unsigned char)(0xFF));
for (; i <= dst_width - (u8_nlanes << 1); i += (u8_nlanes << 1), row1 += (u8_nlanes*dcn << 1), row2 += (u8_nlanes*dcn << 1))
{
	uint8x16_t u, v;
	v_load_deinterleave(uv + i, u, v);//分别加载16个u及16个v,uv分别放于两个neon寄存器

	if (uIdx) swap(u, v);//参考原语逻辑

	uint8x16_t vy[4];
	v_load_deinterleave(y1 + i, vy[0], vy[1]);//分别加载16个u及16个v,uv分别放于两个neon寄存器
	v_load_deinterleave(y2 + i, vy[2], vy[3]);

	int32x4_t ruv[4], guv[4], buv[4]; 
	uvToRGBuv(u, v, ruv, guv, buv); //每对uv计算得到一组ruv、guv、buv;16对uv产生16组数据

	uint8x16_t r[4], g[4], b[4];
	for (int k = 0; k < 4; k++)
	{
                //同样利用ruv、guv、buv,计算最终的rgb值
		yRGBuvToRGBA(vy[k], ruv, guv, buv, r[k], g[k], b[k]);
	}

	if (bIdx)
	{
		for (int k = 0; k < 4; k++)
			swap(r[k], b[k]);
	}

	// [r0...], [r1...] => [r0, r1, r0, r1...], [r0, r1, r0, r1...]
	uint8x16_t r0_0, r0_1, r1_0, r1_1;
	v_zip(r[0], r[1], r0_0, r0_1);
	v_zip(r[2], r[3], r1_0, r1_1);
	uint8x16_t g0_0, g0_1, g1_0, g1_1;
	v_zip(g[0], g[1], g0_0, g0_1);
	v_zip(g[2], g[3], g1_0, g1_1);
	uint8x16_t b0_0, b0_1, b1_0, b1_1;
	v_zip(b[0], b[1], b0_0, b0_1);
	v_zip(b[2], b[3], b1_0, b1_1);

	if (dcn == 4)
	{
		v_store_interleave(row1 + 0 * u8_nlanes, b0_0, g0_0, r0_0, a);
		v_store_interleave(row1 + 4 * u8_nlanes, b0_1, g0_1, r0_1, a);

		v_store_interleave(row2 + 0 * u8_nlanes, b1_0, g1_0, r1_0, a);
		v_store_interleave(row2 + 4 * u8_nlanes, b1_1, g1_1, r1_1, a);
	}
	else //dcn == 3
	{
		v_store_interleave(row1 + 0 * u8_nlanes, b0_0, g0_0, r0_0);
		v_store_interleave(row1 + 3 * u8_nlanes, b0_1, g0_1, r0_1);

		v_store_interleave(row2 + 0 * u8_nlanes, b1_0, g1_0, r1_0);
		v_store_interleave(row2 + 3 * u8_nlanes, b1_1, g1_1, r1_1);
	}
}

neon加速主要是利用单指令执行可并行执行的部分,在YUV2RGB转换中,像素与像素之间的计算都是无关的,因此多个像素的计算完全可以并行执行,主要考虑最大化的利用neon寄存器即可。

从上面代码可以看出,每次计算目标图像的64个像素,分为两行,每行32个像素。由于4个y共用uv,因此每次计算需要16组uv值。

理解了原语的代码逻辑再理解neon加速的版本就很容易了,逻辑都是一样的。这里主要说明一下neon指令涉及到的计算,不理解的地方上面的代码也有相应的注释,应该很容易理解。

static inline void uvToRGBuv(const uint8x16_t& u, const uint8x16_t& v, int32x4_t(&ruv)[4], int32x4_t(&guv)[4], int32x4_t(&buv)[4])
{
	uint8x16_t v128 = vdupq_n_u8((unsigned char)(128));
	int8x16_t su = vreinterpretq_s8_u8(vsubq_u8(u, v128));
	int8x16_t sv = vreinterpretq_s8_u8(vsubq_u8(v, v128));

        //将16对uv进行位扩展,u、v分别扩展到4个128bit neon寄存器,每个寄存器4个32bit数据
	int16x8_t uu0, uu1, vv0, vv1;
	v_expand_i8_16(su, uu0, uu1);
	v_expand_i8_16(sv, vv0, vv1);
	int32x4_t uu[4], vv[4];
	v_expand16_32(uu0, uu[0], uu[1]); v_expand16_32(uu1, uu[2], uu[3]);
	v_expand16_32(vv0, vv[0], vv[1]); v_expand16_32(vv1, vv[2], vv[3]);

        //相应系数乘以2^20,每个数据占用32bit
	int32x4_t vshift = vdupq_n_s32(1 << (ITUR_BT_601_SHIFT - 1));
	int32x4_t vr = vdupq_n_s32(ITUR_BT_601_CVR);
	int32x4_t vg = vdupq_n_s32(ITUR_BT_601_CVG);
	int32x4_t ug = vdupq_n_s32(ITUR_BT_601_CUG);
	int32x4_t ub = vdupq_n_s32(ITUR_BT_601_CUB);

        //计算rgb中与uv相关的分量,共16组ruv、guv、buv
	for (int k = 0; k < 4; k++)
	{
		ruv[k] = vaddq_s32(vshift, vr * vv[k]);
		guv[k] = vaddq_s32(vshift, vaddq_s32(vg * vv[k], ug * uu[k]));
		buv[k] = vaddq_s32(vshift, ub * uu[k]);
	}
}

 


static inline void yRGBuvToRGBA(const uint8x16_t& vy,
			        const int32x4_t(&ruv)[4],
			        const int32x4_t(&guv)[4],
			        const int32x4_t(&buv)[4],
			        uint8x16_t& rr, uint8x16_t& gg, uint8x16_t& bb)
{
	uint8x16_t v16 = vdupq_n_u8(16);
	uint8x16_t posY = vqsubq_u8(vy, v16); //饱和相减指令,<0的值等于0

        //y值扩展到32bit,与ruv、guv、buv相对应
	uint16x8_t yy0, yy1;
	v_expand_u8_16(posY, yy0, yy1);
	int32x4_t yy[4];
	v_expand16_32(vreinterpretq_s16_u16(yy0), yy[0], yy[1]);
	v_expand16_32(vreinterpretq_s16_u16(yy1), yy[2], yy[3]);

	int32x4_t vcy = vdupq_n_s32(ITUR_BT_601_CY);

	int32x4_t y[4], r[4], g[4], b[4];
	for (int k = 0; k < 4; k++)
	{
	        y[k] = yy[k] * vcy;
		r[k] = vshrq_n_s32(vaddq_s32(y[k], ruv[k]), ITUR_BT_601_SHIFT);
		g[k] = vshrq_n_s32(vaddq_s32(y[k], guv[k]), ITUR_BT_601_SHIFT);
		b[k] = vshrq_n_s32(vaddq_s32(y[k], buv[k]), ITUR_BT_601_SHIFT);
	}

        //将r[0]-r[4]转化为uint8合并到一个neon寄存器中,gb同理
        int16x8_t r0, r1, g0, g1, b0, b1;
        r0 = v_pack(r[0], r[1]);
        r1 = v_pack(r[2], r[3]);
        g0 = v_pack(g[0], g[1]);
        g1 = v_pack(g[2], g[3]);
        b0 = v_pack(b[0], b[1]);
        b1 = v_pack(b[2], b[3]);

        rr = v_pack_u(r0, r1);
        gg = v_pack_u(g0, g1);
        bb = v_pack_u(b0, b1);
}

熟悉neon指令的你对上面代码很容易理解了,没啥好说的。主要思想就是最大化利用neon寄存器实现并行操作,每次读取64个y值,16对uv值,一次计算两行分别32个像素值

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值