目录
引言
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个像素值