YUV色彩格式總結(jié)
上一篇文章結(jié)合OpenCV的源代碼介紹了BGR轉(zhuǎn)YUV的方法(YUV444)。本文主要介紹YUV的3種采樣,YUV444,YUV422, YUV420,以及后兩種格式轉(zhuǎn)BGR的方法,和BGR轉(zhuǎn)YUV系列的方法。本系列介紹的公式都是結(jié)合OpenCV根據(jù)OpenCV的計算方法提供的。
YUV格式的采樣方式
YUV格式有3中采樣方式,分別是YUV444、YUV422、YUV420;其中YUV444也就是我們通常意義上的YUV,YUV420就是平時使用的NV21和NV12,其中NV12和NV21僅僅是存儲順序的差異。YUV422平時使用的相對較少。
上篇文章中介紹過BGR轉(zhuǎn)YUV,我們知道每一組BGR都會獲得一組YUV,所以YUV444就是原始的YUV,是不經(jīng)過采樣的。
YUV444
上一篇文章介紹了BGR轉(zhuǎn)YUV444,每一組BGR轉(zhuǎn)換為一組YUV;轉(zhuǎn)換公式如下:
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直接轉(zhuǎn)換,不進行采樣的結(jié)果;而YUV422以及YUV420是在YUV444的基礎(chǔ)上進行采樣得到的。如下圖所示,展示了YUV444的一種演示方式:

其中實心黑圈作為整體表示UV分量,空心圈表示Y分量;所以每一個Y擁有一組UV分量。需要注意的是,這僅僅是示意圖,表示采樣方式,不表示數(shù)據(jù)的真是存儲方式。444可以理解為第一行Y和UV的比是4(第一個4):4(第二個4);第二行Y和UV的比是4(第一個4):4(第三個4);因此使用YUV444表示這種采樣方式。這也表示水平采樣是4:4;垂直采樣是4:4.
YUV422
BGR轉(zhuǎn)YUV422的的公式是一樣的,只是對YUV444進行采樣,便可以得到Y(jié)UV422. YUV422的表示如下圖所示:

可以看到,第一行Y與UV的比例是4:2(第一個2);第二行也是4:2(第二個2);也可以理解為,水平方向上的采樣比例為4:2;垂直方向是也為4:2.
以上是YUV422的采樣方式。
所以在計算的時候,就可以少計算一半的UV分量;數(shù)據(jù)量也少一半的UV分量,也就是說,YUV422的數(shù)據(jù)量只有YUV444的2/3.
YUV420
YUV420的表示方式如下圖所示:

可以看到,每4個Y擁有一組UV;第一行的采樣是4:2;第二行的采樣是4:0;所以取名YUV420.但是YUV420又有NV21和NV12兩種格式,這兩種格式的區(qū)別,僅僅是UV分量存儲方式上的區(qū)別。同時,在數(shù)據(jù)量上,YUV420僅僅是YUV444的1/2.
以上是關(guān)于YUV的3種格式的采樣方式的介紹,下面會介紹這YUV格式數(shù)據(jù)的存儲方式。
YUV格式的存儲方式
YUV與YCr、Cb
YUV444的存儲比較單一,Y單獨存儲,UV交叉存儲,這里主要區(qū)分一下YUV444和YCrCb;YCrCb和YUV的區(qū)別在兩方面:
- 計算系數(shù)
- 存儲順序
下面是RGB轉(zhuǎn)YUV的代碼
{
typedef _Tp channel_type;
RGB2YCrCb_i(int _srccn, int _blueIdx, bool _isCrCb)
: srccn(_srccn), blueIdx(_blueIdx), isCrCb(_isCrCb)
{
//設(shè)置系數(shù)
static const int coeffs_crb[] = { R2Y, G2Y, B2Y, YCRI, YCBI };
static const int coeffs_yuv[] = { R2Y, G2Y, B2Y, R2VI, B2UI };
//yuv和YCrCb的系數(shù)不同
memcpy(coeffs, isCrCb ? coeffs_crb : coeffs_yuv, 5*sizeof(coeffs[0]));
//RGB和BGR的區(qū)別,需要交換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;
//區(qū)分是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計算系數(shù)不同
dst[i+1+yuvOrder] = saturate_cast<_Tp>(Cr);
dst[i+2-yuvOrder] = saturate_cast<_Tp>(Cb);
}
}
int srccn, blueIdx;
bool isCrCb;
int coeffs[5];
};
具體區(qū)別在代碼中注釋了,首先看計算公式:
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剛好對應(yīng)YVU,所以僅僅是UV分量的存儲順序有區(qū)別;
YUV420
YUV格式的存儲方式有很多,YUV格式的數(shù)據(jù)存儲分為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轉(zhuǎn)YUV420的代碼,其中有兩個標(biāo)志位interleaved和swapUV,分別用于區(qū)分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;
//區(qū)分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);
}
//區(qū)分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轉(zhuǎn)YUV420的轉(zhuǎn)換公式為:
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采樣,等等;
另外關(guān)于轉(zhuǎn)換系數(shù),根據(jù)精度不同,系數(shù)也會有出入。表現(xiàn)在移動位數(shù)不同,比如OpenCV中,目前移動的位數(shù)是20;上一篇文章中介紹BGR轉(zhuǎn)YUV,移動的位數(shù)是14;所以在自定義的實現(xiàn)中,可以根據(jù)對精度的需求進行修改,當(dāng)然如果移動位數(shù)變少,精度也會下降。
本文轉(zhuǎn)自鏈接YUV色彩格式總結(jié)