前言
系列文章:
《iOS視頻開發(fā)(一):視頻采集》
《iOS視頻開發(fā)(二):視頻H264硬編碼》
《iOS視頻開發(fā)(三):視頻H264硬解碼》
《iOS視頻開發(fā)(四):通俗理解YUV數(shù)據(jù)》
前面我們已經(jīng)介紹了在iOS開發(fā)中如果調(diào)用攝像頭進行視頻數(shù)據(jù)的采集和編解碼。但折騰了這么多呐籽,對于YUV這玩意兒還是不是特別理解锋勺。其實在我的個人實踐過程中我也一直搞不懂這個YUV,一頓惡補之后狡蝶,我們來通俗一點地講YUV這個數(shù)據(jù)格式庶橱。
1、YUV & RGB概述
2贪惹、YUV的采樣方式
3苏章、YUV的儲存方式及常見格式
4、YUV數(shù)據(jù)量計算
5奏瞬、YUV裁剪
YUV & RGB概述
RGB色彩模式是工業(yè)界的一種顏色標準布近,我們知道三原色(RGB)通過互相疊加可以得到各式各樣的顏色,是目前運用最廣的顏色系統(tǒng)之一丝格。
與我們熟知的RGB一樣撑瞧,YUV也是一種顏色編碼方法,主要運用在電視系統(tǒng)及模擬視頻領域显蝌。YUV的原理是把亮度和色度分離预伺,利用人眼對亮度的敏感度超過色度這個特性,我們偷摸摸把色度信息減少一些曼尊,人眼也很難察覺到酬诀。甚至沒有色度信息(UV分量)依舊可以顯示完整的圖像,只不過是黑白的骆撇。這樣的設計就很好地解決了彩色電視機跟黑白電視機的兼容問題瞒御。并且由于可以減少一些色度信息,YUV數(shù)據(jù)的總尺寸相對RGB數(shù)據(jù)要小一些神郊。YUV這三個字母中肴裙,Y表示亮度(灰度值),U和V表示色度(色彩及飽和度)涌乳。YUV是編譯true-color顏色空間(colorspace)的種類蜻懦,Y'UV,YUV,YCbCr,YPbPr等專有名詞都可以稱為YUV夕晓。
YUV的采樣方式
YUV的主流采樣方式有:YUV4:4:4
宛乃、YUV4:2:2
、YUV4:2:0
,這是個什么意思呢征炼?我們看下面這張圖析既,黑色實心的點表示Y分量,黑色空心的點表示UV分量谆奥,那么下圖的意思就是:
YUV4:4:4
每一個Y分量都有對應一組UV分量
YUV4:2:2
每兩個Y分量共用一組UV分量
YUV4:2:0
每四個Y分量共用一組UV分量
上圖我們可以看到:
YUV4:4:4
:也就是說每4個Y采樣渡贾,就有相對應的4個U和4個V采樣,即畫面中每個像素都有Y分量和UV分量雄右,這種格式儲存了圖像所有的亮度和色度信息空骚。YUV4:2:2
:也就是說每4個Y采樣渐行,就有相對應的2個U和2個V采樣嚼摩。它的色度信號的掃描線(也就是上面圖里的橫線)和亮度信號一樣多仍翰,但每條掃描線上的色度采樣點只有亮度信號的一半(看4:2:2圖一根橫線上罢防,空心圓只有實心圓的一半)服鹅。當4:2:2信號被解碼的時候热凹,“缺失”的色度采樣通常由一定的內(nèi)插補點算法通過它兩側的色度信息運算補充项棠。YUV4:2:0
按字面意思應該4個Y采樣几苍,就有相對應的2個U和0個V采樣肃廓,事實上并不是這樣的智厌,事實上,4:2:0的意思是盲赊,色度采樣在每條橫向掃描線上只有亮度采樣的一半铣鹏,掃描線的條數(shù)上,也只有亮度的一半0ⅰ(看上面4:2:0這個圖诚卸,空心是上下共用的,如果放到第一根線上绘迁,那就是空心只有實心的一半合溺。再看前4根線,4根亮度線對應兩行色度缀台,也就是說空心的掃描線也只有實心的一半棠赛。)換句話說,無論是橫向還是縱向膛腐, 色度信號的分辨率都只有亮度信號的一半睛约。說得再通俗一些就是,如果第一行是4:2:0依疼,那么第二行就是4:0:2痰腮,第三行就是4:2:0這樣交替。舉個例子律罢,如果整張畫面的尺寸是720x480,那么亮度信號是720x480,色度信號只有360x240误辑。
YUV的儲存方式
YUV的儲存格式有兩大類:
planar
:先連續(xù)存儲所有像素點的Y沧踏,緊接著存儲所有像素點的U,隨后是所有像素點的V巾钉。
packed
:每個像素點的Y翘狱、U、V是連續(xù)交替存儲的
YUYV(YUY2)格式(屬于YUV422)
YUYV
格式為像素保留Y砰苍,而UV在水平空間上相隔二個像素采樣一次(Y0U0Y1V0)潦匈,(Y2U2Y3V2)…其中,(Y0U0Y1V0)就是一個macro-pixel(宏像素)赚导,它表示了2個像素茬缩,(Y2U2Y3V2)是另外的2個像素。以此類推吼旧。
YVYU凰锡、UYVY格式(屬于YUV422)
跟YUY2類似,只不過是排列順序不一樣而已圈暗。
例如UYVY格式掂为,那么排列順序就是(U0Y0V0Y1)、(U2Y2V2Y3)...
YUV422P格式(屬于YUV422)
YUV422P
也屬于YUV422的一種员串,但它并不是將YUV數(shù)據(jù)交錯存儲勇哗,而是先存放所有的Y分量,然后存儲所有的U(Cb)分量寸齐,最后存儲所有的V(Cr)分量智绸,其每一個像素點的YUV值提取方法也是遵循YUV422格式的最基本提取方法,即兩個Y共用一個UV访忿。
YV12格式(屬于YUV420)
YV12
是Plane模式的瞧栗,也就是Y、U海铆、V三個分量分別打包迹恐,依次存儲。例如2x2圖像:YYYYVU卧斟,4*4圖像:YYYYYYYYYYYYYYYYVVVVUUUU
NV12殴边、NV21格式(屬于YUV420)
NV12
和NV21
是一種two-plane模式,即Y和UV分為兩個Plane珍语,其中U和V是交錯儲存的锤岸。NV12
是U在前,V在后板乙,而NV21
是V在前U在后是偷。例如4x4圖像:NV12
:YYYYYYYYYYYYYYYYUVUVUVUVNV21
:YYYYYYYYYYYYYYYYVUVUVUVU
I420格式(屬于YUV420)
I420是 planar 存儲方式拳氢,分量存儲順序依次是 Y, Cb(U), Cr(V),例如4x4圖像:YYYYYYYYYYYYYYYYUUUUVVVV
更多格式可參考:YUV pixel formats
YUV數(shù)據(jù)量計算
我們以YUV444
蛋铆、YUV422
馋评、YUV420
這三種采樣格式舉例。我們知道刺啦,RGB圖像三個分量都必須全部存儲留特,例如一張4x4像素的RGB圖像大小為4x4x3=48字節(jié)。
-
YUV444
為一個像素點有一個Y玛瘸、一個U蜕青、一個V,所以一張4x4像素的YUV444
圖像大小為4x4x3=48字節(jié)糊渊,跟RGB一樣大右核。Y、U再来、V三個分量的大小都是4x4=16字節(jié) -
YUV422
為兩個Y共用一個U和V蒙兰,Y分量為全采樣,即4x4=16字節(jié)芒篷,U分量和V分量只有Y分量的一半搜变,即U分量為4x4/2=8字節(jié),V分量也是4x4/2=8字節(jié)针炉,也就是說一張4x4像素的YUV422
圖像大小為4x4x2=32字節(jié)挠他。 -
YUV420
為4個Y共用一個U和V,Y分量為全采樣篡帕,及4x4=16字節(jié)殖侵,U分量和V分量只有Y分量的四分之一,即U分量和V分量的大小均為4x4/4=4字節(jié)镰烧,也就是說一張4x4像素的YUV420
圖像大小為:4x4x(3/2)=24字節(jié)拢军。
YUV裁剪
我們來運用一下上面所講的關于YUV的知識,下面我們拿一張I420的YUV圖像來對其進行裁剪怔鳖。
上面說到茉唉,I420的排列方式就是先全部放Y,然后放U结执,然后放V度陆,由于I420是4:2:0采樣的,那么一幅YUV圖像的總大小為4x4x(3/2)=24字節(jié)献幔,前4x4字節(jié)存放全部的Y懂傀,從4x4到4x4x(5/4)存放U,最后從4x4x(5/4)到4x4x(3/2)存放V蜡感。
先從簡單的入手
假如把一幅4x4的圖像蹬蚁,將其左上角2x2的畫面裁剪出來恃泪,應該怎么做呢?我們把YUV三個分量畫成下面的圖來分析一下:
如上圖所示缚忧,首先Y分量每個像素點都有一個Y悟泵,那么總用就有16個Y杈笔,U和V水平和垂直都是Y的一半闪水,也就是只有4個U和4個V,其中蒙具,Y1球榆、Y2、Y5禁筏、Y6共用U1和V1持钉,以此類推,那么我們要裁剪左上角2x2像素篱昔,不就是把Y1每强,Y2,Y5州刽,Y6空执,U1,V1取出來穗椅,按I420的方式排列辨绊,就這么簡單!
也就是說匹表,原來4x4的I420圖像數(shù)據(jù)為Y1Y2Y3Y4Y5Y6Y7Y8Y9Y10Y11Y12Y13Y14Y15Y16U1U2U3U4V1V2V3V4门坷,我們裁出的左上角2x2圖像數(shù)據(jù)為Y1Y2Y5Y6U1V1。就這么簡單袍镀!
加強一下
我們用一張256x256大小的圖片默蚌,來做裁剪,這次我們要裁中間的那部分:將256x256的圖片裁出(x, y, w, h) = (64, 64, 128, 128)的那部分苇羡。大致就是下面這張圖上的紅框區(qū)域
我們分幾步走:
- 讀取源圖像數(shù)據(jù)绸吸,記為sourceData
- 開辟一塊內(nèi)存來放裁剪后的數(shù)據(jù),記為destData
- 讀取sourceData的Y分量并提取要裁剪的Y分量數(shù)據(jù)寫入destData
- 讀取sourceData的U分量并提取要裁剪的U分量數(shù)據(jù)寫入destData
- 讀取sourceData的V分量并提取要裁剪的V分量數(shù)據(jù)寫入destData
- destData寫入到文件
我們直接把I420裁剪過程封裝成一個方法宣虾,代碼加注釋看看如何一步一步實現(xiàn):
/**
I420裁剪
@param sourceData 源圖像數(shù)據(jù)
@param sourceW 源圖像寬度
@param sourceH 源圖像高度
@param destData 裁剪后圖像數(shù)據(jù)
@param x 從源圖像的x坐標開始裁剪
@param y 從源圖像的y坐標開始裁剪
@param w 裁剪寬度
@param h 裁剪高度
*/
void clipI420(const unsigned char *sourceData, const int sourceW, const int sourceH, unsigned char *destData, int x, int y, const int w, const int h)
{
// 拷貝一只指針出來使喚
const unsigned char *source = sourceData;
// 把要裁剪的x和y都換成偶數(shù)惯裕,因為我們2行Y用一行U和一行V,奇數(shù)就難搞了
x = (int)(x + 1) / 2 * 2;
y = (int)(y + 1) / 2 * 2;
// 指針移動到要裁剪的Y數(shù)據(jù)的開頭
source += y * sourceW + x;
//總共要掃描要裁剪的高的行數(shù)绣硝,將里面在區(qū)域內(nèi)的Y值提取出來
for (int i = 0; i < h; i++)
{
// 裁剪這一行的目標Y分量
memcpy(destData, source, w);
// 移動指針到下一行
source += sourceW;
destData += w;
}
//把源數(shù)據(jù)指針移到U分量的開頭
source = sourceData + sourceW * sourceH;
// 把源數(shù)據(jù)的指針移到要裁剪的U分量的地方蜻势,UV分量都是行列只有Y分量的一半,所以U分量的要裁剪的y坐標應該是在(y/2)*(sourceW/2)的地方鹉胖,再加上UV分量的x坐標的偏移量
source += (y * sourceW / 4 + x / 2);
// UV分量都是行列都只有Y分量的一半握玛,所以指針的移動也是一半的步長
for (int i = 0; i < h / 2; i++)
{
memcpy(destData, source, w / 2);
source += sourceW / 2;
destData += w / 2;
}
//把源數(shù)據(jù)指針移到V分量的開頭
source = sourceData + sourceW * sourceH * 5 / 4;
// 以下跟U分量一個道理
source += (y * sourceW / 4 + x / 2);
for (int i = 0; i < h / 2; i++)
{
memcpy(destData, source, w / 2);
source += sourceW / 2;
destData += w / 2;
}
}
void clipLena()
{
int sourceW = 256;
int sourceH = 256;
int souceDataLength = sourceW * sourceH * 3 / 2;
// 讀取源圖像
FILE *fp = fopen("lena_256x256_yuv420p.yuv", "rb+");
unsigned char *sourceData = (unsigned char *)malloc(souceDataLength);
fread(sourceData, 1, souceDataLength, fp);
fclose(fp);
// 開辟儲存裁剪后的圖像的內(nèi)存
int clipX = 64;
int clipY = 64;
int clipW = 128;
int clipH = 128;
int destDataLength = clipW * clipH * 3 / 2;
unsigned char *destData = (unsigned char *)malloc(souceDataLength);
// 進行裁剪
clipI420(sourceData, sourceW, sourceH, destData, clipX, clipY, clipW, clipH);
// 裁剪后數(shù)據(jù)寫入文件
FILE *fp1 = fopen("lena_64x64_yuv420p","wb+");
fwrite(destData, 1, destDataLength, fp1);
fclose(fp1);
free(sourceData);
free(destData);
}
我自己在做這個的時候的一些疑問:
1够傍、裁剪的x和y為什么得偶數(shù)?
4:2:0這種格式不就是兩行用的是同一個UV挠铲,兩列用的也是同一個UV冕屯,例如第一行和第二行Y共用了第一行UV,針對這個256x256的圖像來說拂苹,我們把數(shù)據(jù)分為三部分:|----Y----|-U-|-V-|安聘,前兩行像素(0, 0, 256, 2),取的是數(shù)據(jù)是Y部分的前256x2字節(jié)瓢棒,U部分的前128字節(jié)浴韭,V部分的前128字節(jié),也就是兩行Y脯宿,一行U和一行V念颈。如果我們只取一行像素(0, 0, 256, 1),那就沒法玩了连霉,因為這第一行的U和V跟第二行Y共享的榴芳。
2、怎么計算UV分量數(shù)據(jù)從哪里開始榷搴场窟感?
還是拿4x4圖像舉例,4行4列有16個像素财边,也就是有16個Y肌括,4個U和4個V。也就是數(shù)據(jù)最前面4x4=16字節(jié)是Y分量酣难,U分量是接著2x2=4字節(jié)谍夭,V分量是最后2x2=4字節(jié)。UV的行列數(shù)都是Y分量的一半憨募〗羲鳎回到256x256的圖像,我們裁(64, 64, 128, 128)這一塊菜谣,第64行Y的地方是第32行U珠漂,每行U是128字節(jié),那就是U分量128x32尾膊,這也就是要裁剪的y坐標媳危。一行U是128字節(jié),那么在原圖64列的位置U應該是在這一行的64/2=32的位置冈敛。這也就是source += (y * sourceW / 4 + x / 2);這一行的由來待笑。
3、換成其他格式怎么裁剪呢抓谴?
例如NV21怎么裁呢暮蹂?其實也就是根據(jù)數(shù)據(jù)儲存格式和采樣方式從源數(shù)據(jù)中取出我們想要的數(shù)據(jù)并按規(guī)則排列就可以實現(xiàn)了寞缝。
4、還有哪些知識點沒碰到仰泻?
給YUV圖像加水印荆陆、YUV旋轉、翻轉集侯、YUV與RGB互轉等等被啼。留坑后面再補。浅悉。趟据。
總結
YUV其實沒那么神秘券犁,主要是理解采樣比例和儲存方式也就沒什么太大的難度了术健。本篇沒單獨整理Demo,上面I420裁剪的代碼是在Mac上寫的粘衬,理論上Windows也可以跑荞估。YUV文件大家可以在下面雷神的文章《[視音頻數(shù)據(jù)處理入門:RGB、YUV像素數(shù)據(jù)處理》里頭找到稚新。
最近在學習研究OpenGL和Metal渲染相關的知識勘伺,下一篇來講一下關于視頻渲染相關的知識吧!
參考文獻
YUV pixel formats
視音頻數(shù)據(jù)處理入門:RGB褂删、YUV像素數(shù)據(jù)處理
YUV格式詳解
YUV和RGB格式分析
YUV 數(shù)據(jù)格式完全解析