YUV色彩格式总结
上一篇文章结合OpenCV的源代码介绍了BGR转YUV的方法(YUV444)。本文主要介绍YUV的3种采样,YUV444,YUV422, YUV420,以及后两种格式转BGR的方法,和BGR转YUV系列的方法。本系列介绍的公式都是结合OpenCV根据OpenCV的计算方法提供的。
YUV格式的采样方式
YUV格式有3中采样方式,分别是YUV444、YUV422、YUV420;其中YUV444也就是我们通常意义上的YUV,YUV420就是平时使用的NV21和NV12,其中NV12和NV21仅仅是存储顺序的差异。YUV422平时使用的相对较少。
上篇文章中介绍过BGR转YUV,我们知道每一组BGR都会获得一组YUV,所以YUV444就是原始的YUV,是不经过采样的。
YUV444
上一篇文章介绍了BGR转YUV444,每一组BGR转换为一组YUV;转换公式如下:
Y = (4899 * R + 9617 * G + 1868 * B) >> 14; V = ((R - Y) * 14369 + delta) >> 14; U = ((B - Y) * 8061 + delta) >> 14; delta = (255 / 2 + 1) * (1 << 14);
YUV444是BGR直接转换,不进行采样的结果;而YUV422以及YUV420是在YUV444的基础上进行采样得到的。如下图所示,展示了YUV444的一种演示方式:
image
其中实心黑圈作为整体表示UV分量,空心圈表示Y分量;所以每一个Y拥有一组UV分量。需要注意的是,这仅仅是示意图,表示采样方式,不表示数据的真是存储方式。444可以理解为第一行Y和UV的比是4(第一个4):4(第二个4);第二行Y和UV的比是4(第一个4):4(第三个4);因此使用YUV444表示这种采样方式。这也表示水平采样是4:4;垂直采样是4:4.
YUV422
BGR转YUV422的的公式是一样的,只是对YUV444进行采样,便可以得到YUV422. YUV422的表示如下图所示:
image
可以看到,第一行Y与UV的比例是4:2(第一个2);第二行也是4:2(第二个2);也可以理解为,水平方向上的采样比例为4:2;垂直方向是也为4:2.
以上是YUV422的采样方式。
所以在计算的时候,就可以少计算一半的UV分量;数据量也少一半的UV分量,也就是说,YUV422的数据量只有YUV444的2/3.
YUV420
YUV420的表示方式如下图所示:
image
可以看到,每4个Y拥有一组UV;第一行的采样是4:2;第二行的采样是4:0;所以取名YUV420.但是YUV420又有NV21和NV12两种格式,这两种格式的区别,仅仅是UV分量存储方式上的区别。同时,在数据量上,YUV420仅仅是YUV444的1/2.
以上是关于YUV的3种格式的采样方式的介绍,下面会介绍这YUV格式数据的存储方式。
YUV格式的存储方式
YUV与YCr、Cb
YUV444的存储比较单一,Y单独存储,UV交叉存储,这里主要区分一下YUV444和YCrCb;YCrCb和YUV的区别在两方面:
- 计算系数
- 存储顺序
下面是RGB转YUV的代码
{ typedef _Tp channel_type; RGB2YCrCb_i(int _srccn, int _blueIdx, bool _isCrCb) : srccn(_srccn), blueIdx(_blueIdx), isCrCb(_isCrCb) { //设置系数 static const int coeffs_crb[] = { R2Y, G2Y, B2Y, YCRI, YCBI }; static const int coeffs_yuv[] = { R2Y, G2Y, B2Y, R2VI, B2UI }; //yuv和YCrCb的系数不同 memcpy(coeffs, isCrCb ? coeffs_crb : coeffs_yuv, 5*sizeof(coeffs[0])); //RGB和BGR的区别,需要交换B分量和R分量的位置 if(blueIdx==0) std::swap(coeffs[0], coeffs[2]); } void operator()(const _Tp* src, _Tp* dst, int n) const { int scn = srccn, bidx = blueIdx; //区分是YUV还是YCrCb int yuvOrder = !isCrCb; //1 if YUV, 0 if YCrCb int C0 = coeffs[0], C1 = coeffs[1], C2 = coeffs[2], C3 = coeffs[3], C4 = coeffs[4]; //color.hpp +26 : yuv_shift = 14 int delta = ColorChannel<_Tp>::half()*(1 << yuv_shift); n *= 3; for(int i = 0; i < n; i += 3, src += scn) { int Y = CV_DESCALE(src[0]*C0 + src[1]*C1 + src[2]*C2, yuv_shift); int Cr = CV_DESCALE((src[bidx^2] - Y)*C3 + delta, yuv_shift); int Cb = CV_DESCALE((src[bidx] - Y)*C4 + delta, yuv_shift); dst[i] = saturate_cast<_Tp>(Y); //YUV和YCrCb计算系数不同 dst[i+1+yuvOrder] = saturate_cast<_Tp>(Cr); dst[i+2-yuvOrder] = saturate_cast<_Tp>(Cb); } } int srccn, blueIdx; bool isCrCb; int coeffs[5]; };
具体区别在代码中注释了,首先看计算公式:
Y = (4899 * R + 9617 * G + 1868 * B) >> 14; Cr = ((R - Y) * 11682 + delta) >> 14; Cb = ((B - Y) * 9241 + delta) >> 14; delta = (255 / 2 + 1) * (1 << 14);
存储顺序:
dst[i+1+yuvOrder] = saturate_cast<_Tp>(Cr); dst[i+2-yuvOrder] = saturate_cast<_Tp>(Cb);
可以看到,YCrCb刚好对应YVU,所以仅仅是UV分量的存储顺序有区别;
YUV420
YUV格式的存储方式有很多,YUV格式的数据存储分为two-plane和three-plane两种方式;所谓的two-plane是指Y单独存储一个plane,UV交叉存储,占用一个plane;three-plane是Y U V分别占用一个plane,一共三个plane.three-plane一般叫做YUV420p,two-plane叫做YUV420sp,我们熟知的NV21和NV12便是YUV420sp。
下面是OpenCV种RGB转YUV420的代码,其中有两个标志位interleaved和swapUV,分别用于区分YUV420p和YUV420sp以及NV21和NV12;NV21的存储是VU,而NV12是UV顺序。
struct RGB888toYUV420pInvoker: public ParallelLoopBody { RGB888toYUV420pInvoker(const uchar * _src_data, size_t _src_step, uchar * _y_data, uchar * _uv_data, size_t _dst_step, int _src_width, int _src_height, int _scn, bool swapBlue_, bool swapUV_, bool interleaved_) : src_data(_src_data), src_step(_src_step), y_data(_y_data), uv_data(_uv_data), dst_step(_dst_step), src_width(_src_width), src_height(_src_height), scn(_scn), swapBlue(swapBlue_), swapUV(swapUV_), interleaved(interleaved_) { } void operator()(const Range& rowRange) const CV_OVERRIDE { const int w = src_width; const int h = src_height; const int cn = scn; for( int i = rowRange.start; i < rowRange.end; i++ ) { const uchar* brow0 = src_data + src_step * (2 * i); const uchar* grow0 = brow0 + 1; const uchar* rrow0 = brow0 + 2; const uchar* brow1 = src_data + src_step * (2 * i + 1); const uchar* grow1 = brow1 + 1; const uchar* rrow1 = brow1 + 2; if (swapBlue) { std::swap(brow0, rrow0); std::swap(brow1, rrow1); } uchar* y = y_data + dst_step * (2*i); uchar* u; uchar* v; //区分two-plane or three-plane if (interleaved) { u = uv_data + dst_step * i; v = uv_data + dst_step * i + 1; } else { u = uv_data + dst_step * (i/2) + (i % 2) * (w/2); v = uv_data + dst_step * ((i + h/2)/2) + ((i + h/2) % 2) * (w/2); } //区分NV21 or NV12 if (swapUV) { std::swap(u, v); } for( int j = 0, k = 0; j < w * cn; j += 2 * cn, k++ ) { int r00 = rrow0[j]; int g00 = grow0[j]; int b00 = brow0[j]; int r01 = rrow0[cn + j]; int g01 = grow0[cn + j]; int b01 = brow0[cn + j]; int r10 = rrow1[j]; int g10 = grow1[j]; int b10 = brow1[j]; int r11 = rrow1[cn + j]; int g11 = grow1[cn + j]; int b11 = brow1[cn + j]; const int shifted16 = (16 << ITUR_BT_601_SHIFT); const int halfShift = (1 << (ITUR_BT_601_SHIFT - 1)); int y00 = ITUR_BT_601_CRY * r00 + ITUR_BT_601_CGY * g00 + ITUR_BT_601_CBY * b00 + halfShift + shifted16; int y01 = ITUR_BT_601_CRY * r01 + ITUR_BT_601_CGY * g01 + ITUR_BT_601_CBY * b01 + halfShift + shifted16; int y10 = ITUR_BT_601_CRY * r10 + ITUR_BT_601_CGY * g10 + ITUR_BT_601_CBY * b10 + halfShift + shifted16; int y11 = ITUR_BT_601_CRY * r11 + ITUR_BT_601_CGY * g11 + ITUR_BT_601_CBY * b11 + halfShift + shifted16; y[2*k + 0] = saturate_cast<uchar>(y00 >> ITUR_BT_601_SHIFT); y[2*k + 1] = saturate_cast<uchar>(y01 >> ITUR_BT_601_SHIFT); y[2*k + dst_step + 0] = saturate_cast<uchar>(y10 >> ITUR_BT_601_SHIFT); y[2*k + dst_step + 1] = saturate_cast<uchar>(y11 >> ITUR_BT_601_SHIFT); const int shifted128 = (128 << ITUR_BT_601_SHIFT); int u00 = ITUR_BT_601_CRU * r00 + ITUR_BT_601_CGU * g00 + ITUR_BT_601_CBU * b00 + halfShift + shifted128; int v00 = ITUR_BT_601_CBU * r00 + ITUR_BT_601_CGV * g00 + ITUR_BT_601_CBV * b00 + halfShift + shifted128; if (interleaved) { u[k*2] = saturate_cast<uchar>(u00 >> ITUR_BT_601_SHIFT); v[k*2] = saturate_cast<uchar>(v00 >> ITUR_BT_601_SHIFT); } else { u[k] = saturate_cast<uchar>(u00 >> ITUR_BT_601_SHIFT); v[k] = saturate_cast<uchar>(v00 >> ITUR_BT_601_SHIFT); } } } } }
BGR转YUV420的转换公式为:
Y = (R * 269484 + G * 528482 + B * 102760 + (1 << 19) + (1 << 16)) >> 20; U = (R * (-155188) + G * (-305135) + B * 460324 + (1 << 19) + (128 << 20)) >> 20; V = (R * 460324 + G * (-385875) + B * (-74448) + (1 << 19) + (128 << 20)) >> 20;
另外需要注意的是,YUV420在计算过程中是需要采样的,每4个Y共同使用一组UV,而这组UV则是取的2x2左上角的点——(0,0);代码如下:
int u00 = ITUR_BT_601_CRU * r00 + ITUR_BT_601_CGU * g00 + ITUR_BT_601_CBU * b00 + halfShift + shifted128; int v00 = ITUR_BT_601_CBU * r00 + ITUR_BT_601_CGV * g00 + ITUR_BT_601_CBV * b00 + halfShift + shifted128;
可以看到OpenCV在计算的时候取用的是(0,0)位置的点。在别的代码中也可能采取其他的采样方式,比如水平方向上对U采样,垂直方向上对V采样,等等;
另外关于转换系数,根据精度不同,系数也会有出入。表现在移动位数不同,比如OpenCV中,目前移动的位数是20;上一篇文章中介绍BGR转YUV,移动的位数是14;所以在自定义的实现中,可以根据对精度的需求进行修改,当然如果移动位数变少,精度也会下降。