圖像處理之YUV編碼


RGB顏色空間

最常用的用途就是顯示器系統,通過RGB數字驅動RGB電子槍發射電子,並激發顯示屏上的熒光粉發出不同亮度的光線,並通過混合產生各種顏色。在RGB顏色空間中,任意色光F都可以用R、G、B三色不同分量的相加混合而成

YUV編碼系統

YUV是一種彩色編碼系統,相比於RGB顏色空間(用紅綠藍三基色描述),設計YUV的目的就是為了編碼、傳輸的方便,減少帶寬占用。

Y表示亮度luma, UV其實是指CbCr,表示色度(chroma)。YUV編碼將亮度和色度分離,如果只有Y分量,那么圖像就是黑白的,其實當時YUV的設計初衷就是為了使彩色電視能夠兼容黑白電視。

人眼的視覺特點是對亮度更敏感,對位置、色彩相對來說不敏感。一個像素如果有YUV三個分量,每個分量用8bit來表示,那么一個像素就需占用3*8 = 24bit = 3byte的大小。為了降低帶寬,我們可以保存更多的亮度信息Y,保存較少的色度信息UV,這叫做色度二次采樣。原則:1、每個圖形像素都要包含亮度Y信息;2、幾個圖形像素個共用一個CbCr值,一般是2、4、8個像素。

通常有YUV444,YUV422,YUV420等編碼格式,對於YUV后面的數字要如何理解,我們可以通過一張圖來表示(來源:https://zhuanlan.zhihu.com/p/85620611

 

 

 上圖中,左側一列,每一個小矩形表示圖形像素,小黑點是表示色度像素值(Cb+Cr),表示圖形像素和色度像素在水平和垂直方向的比例關系。我們一般用4*2的像素區域來表示其中的比例關系,比如:

4:4:0 水平方向是1/1,垂直方向是1/2,在4*2像素框中一個色度像素對應了兩個圖形像素

4:2:2 水平方向是1/2,垂直方向是1/1,表示一個色度像素對應了兩個圖形像素。

4:2:0 水平方向是1/2,垂直方向是1/2,表示一個色度像素對應了四個圖形像素。

右側一列是二次采樣模式記號表示, 是 J:a:b 模式,實心黑色圓圈表示包含色度像素(Cb+Cr),空心圓圈表示不包含色度像素。對於 J:a:b 模式,主要是圍繞參考塊的概念定義的,這個參考塊是一個 J x 2 的矩形,J 通常是 4。這樣,此參考塊就是寬度有 4 個像素、高度有 2 個像素的矩形。a 表示參考塊的第一行包含的色度像素樣本數,b 表示在參考塊的第二行包含的色度像素樣本數

4:4:0 參考塊第一行包含四個色度樣本,第二行沒有包含色度樣本。

4:2:2 參考塊第一行包含兩個色度樣本,第二行也包含兩個色度樣本,他們是交替出現。

4:2:0 參考塊第一行包含兩個色度樣本,第二行沒有包含色度樣本。(代表每四個圖形像素共用一個色度像素)。

現在我們發現 yuv444,yuv422,yuv420 yuv 等像素格式的本質是:每個圖形像素都會包含亮度值,但是幾個圖形像素會共用一個色度值,這個比例關系就是通過 4 x 2 的矩形參考塊來定的。這樣很容易理解類似 yuv440,yuv420 這樣的格式了

存儲方式

平面格式

平面格式是指用三個不同的數組來表示 YCbCr 的三個 Component,每一個 Component 都是通過不同的平面表示。為此,每一個 Component 會對應一個 plane

YUV420表示的width*High的圖片大小計算

每個分量用8bit二進制表示,我們把8bit成為位深度,圖片大小 = (w* h)*(1 + 1/4 + 1/4) = w * h * 3/2,上述1/4表示的是4個像素點共用一個色度分量u,所以只有(w*h)*1/4個u分量,以及4個像素點共用一個色度分量v。

壓縮格式

壓縮格式是指用一個數組表示 YCbCr,每一個 component 是交替出現的。

常見的存儲格式:(來源https://www.cnblogs.com/daner1257/p/10767570.html

YU12/I420

該格式屬於4:2:0類型,存儲方式上面已經說過,就是先存儲把全部的Y分量存完,再存U分量,最后存V分量,從網上找了一張很形象的圖:

可以看到,第一行的Y1Y2和第二行的Y7Y8共同使用一組UV分量U1V1。

YV12

該格式與YU12基本一樣,唯一的區別是先存儲V分量再存儲U分量,對應到上圖把第五行和第六行位置互換一下就是了。

以上兩種格式我們可以看到都是4:2:0的,因為都是planar方式存儲,簡稱420p。

除了上面兩種,還有兩種4:2:0,NV12和NV21,這兩種是比較特殊的存儲格式,是planar和packed混合存儲的,分別看下

NV12

該格式是先存儲全部的Y分量,然后UV分量交叉存儲,用圖像表示下:

 

很直觀,不多說了。

NV21

該格式與NV21的區別和上面YU12/YV12一樣,唯一的區別只是UV分量交叉的順序不同,NV12是U排前面,NV21是V排前面,用圖像表示如下:

 

 

上面兩種雖然也是4:2:0類型,但是並不是完全的planar格式,所以又稱為420sp,與420p進行區分。

上面說的都是4:2:0類型的,下面說幾個4:2:2類型較常見的

YUV422P

名字中帶P表示是planar格式存儲,該格式存儲方式與I420是一樣的,唯一的區別是UV分量的數量不同,I420中四個Y共用一組UV,而該格式中兩個Y共用一組UV,也就是說UV分量相對於I420在數量上多了一倍,從網上找了一張圖,如下:

 

如上圖,在渲染時Y00與Y01會共用U00和V00.

YUYV/YUY2

該格式屬於4:2:2類型,且是用packed形式存儲的,上面也簡單的說過,存儲方式如下圖:

 

可以看到,每兩個Y分量共用一組UV分量,存儲順序是YUYV。

YVYU

該格式與YUYV相似,只是存儲時UV分量順序不同而已,為YVYU。

UYVY

該格式也是4:2:2類型,與上面兩種方式並無大的不同,從網上找了一張圖如下:

 

 可以看到存儲時YUV分量的順序如名字所示:UYVY。

YUV圖像基本處理

以下內容轉載自:一文掌握 YUV 圖像的基本處理 - 雲+社區 - 騰訊雲 (tencent.com)

YUV 圖

可以通過FFmpeg來將jpeg圖片轉換為YUV格式圖片。

1. YUV 的由來

YUV 是一種色彩編碼模型,也叫做 YCbCr,其中 “Y” 表示明亮度(Luminance),“U” 和 “V” 分別表示色度(Chrominance)和濃度(Chroma)。

YUV 色彩編碼模型,其設計初衷為了解決彩色電視機與黑白電視的兼容問題,利用了人類眼睛的生理特性(對亮度敏感,對色度不敏感),允許降低色度的帶寬,降低了傳輸帶寬。

在計算機系統中應用尤為廣泛,利用 YUV 色彩編碼模型可以降低圖片數據的內存占用,提高數據處理效率。

另外,YUV 編碼模型的圖像數據一般不能直接用於顯示,還需要將其轉換為 RGB(RGBA) 編碼模型,才能夠正常顯示。

2. YUV 幾種常見采樣方式

YUV 圖像主流的采樣方式有三種:

  • YUV 4:4:4,每一個 Y 分量對於一對 UV 分量,每像素占用3 字節 (Y + U + V = 8 + 8 + 8 = 24bits);
  • YUV 4:2:2,每兩個 Y 分量共用一對 UV 分量,每像素占用 2 字節 (Y + 0.5U + 0.5V = 8 + 4 + 4 = 16bits);
  • YUV 4:2:0,每四個 Y 分量共用一對 UV 分量,每像素占用1.5 字節 (Y + 0.25U + 0.25V = 8 + 2 + 2 = 12bits);

其中最常用的采樣方式是 YUV422 和 YUV420 。 YUV 格式也可按照 YUV 三個分量的組織方式分為打包(Packed)格式和平面格式(Planar)。

  • 打包(Packed)格式:每個像素點的 YUV 分量是連續交叉存儲的,如 YUYV 格式;
  • 平面格式(Planar):YUV 圖像數據的三個分量分別存放在不同的矩陣中,這種格式適用於采樣,如 YV12、YU12 格式。

3. YUV 幾種常用的格式

下面以一幅分辨率為 4x4 的 YUV 圖為例,說明在不同 YUV 格式下的存儲方式(括號內范圍表示內存地址索引范圍,默認以下不同格式圖片存儲使用的都是連續內存)。

YUYV (YUV422 采樣方式)

YUYV 格式的存儲格式

(0 ~ 7) Y00 U00 Y01 V00 Y02 U01 Y03 V01 (8 ~ 15) Y10 U10 Y11 V10 Y12 U11 Y13 V11 (16 ~ 23) Y20 U20 Y21 V20 Y22 U21 Y23 V21 (24 ~ 31) Y30 U30 Y31 V30 Y32 U31 Y33 V31

YV12/YU12 (YUV420 采樣方式)

YV12/YU12 也屬於 YUV420P ,即 YUV420 采樣方式的平面模式,YUV 三個分量分別存儲於 3 個不同的矩陣(平面)。 YV12 格式的存儲方式

(0 ~ 3) Y00 Y01 Y02 Y03 (4 ~ 7) Y10 Y11 Y12 Y13 (8 ~ 11) Y20 Y21 Y22 Y23 (12 ~ 15) Y30 Y31 Y32 Y33 (16 ~ 17) V00 V01 (18 ~ 19) V10 V11 (20 ~ 21) U00 U01 (22 ~ 23) U10 U11

YU12(也稱 I420) 格式的存儲方式

(0 ~ 3) Y00 Y01 Y02 Y03 (4 ~ 7) Y10 Y11 Y12 Y13 (8 ~ 11) Y20 Y21 Y22 Y23 (12 ~ 15) Y30 Y31 Y32 Y33 (16 ~ 17) U00 U01 (18 ~ 19) U10 U11 (20 ~ 21) V00 V01 (22 ~ 23) V10 V11

NV21/NV12 (YUV420 采樣方式)

NV21/NV12 屬於 YUV420SP ,YUV420SP 格式有 2 個平面,Y 分量存儲於一個平面,UV 分量交錯存儲於另一個平面。

NV21 格式的存儲方式

(0 ~ 3) Y00 Y01 Y02 Y03 (4 ~ 7) Y10 Y11 Y12 Y13 (8 ~ 11) Y20 Y21 Y22 Y23 (12 ~ 15) Y30 Y31 Y32 Y33 (16 ~ 19) V00 U00 V01 U01 (20 ~ 23) V10 U10 V11 U11

NV12 格式的存儲方式

(0 ~ 3) Y00 Y01 Y02 Y03 (4 ~ 7) Y10 Y11 Y12 Y13 (8 ~ 11) Y20 Y21 Y22 Y23 (12 ~ 15) Y30 Y31 Y32 Y33 (16 ~ 19) U00 V00 U01 V01 (20 ~ 23) U10 V10 U11 V11

NV21 與 NV12 格式的區別僅在於 UV 分量排列的先后順序不同。

4. YUV 圖像的基本操作

下面以最常用的 NV21 圖為例介紹其旋轉、縮放和剪切的基本方法。

YUV 圖片的定義、加載、保存及內存釋放。

//YUV420SP  NV21 or NV12 

typedef struct
{ int width; // 圖片寬 int height; // 圖片高 unsigned char *yPlane; // Y 平面指針 unsigned char *uvPlane; // UV 平面指針 } YUVImage; void LoadYUVImage(const char *filePath, YUVImage *pImage) { FILE *fpData = fopen(filePath, "rb+"); if (fpData != NULL) { fseek(fpData, 0, SEEK_END); int len = ftell(fpData); pImage->yPlane = malloc(len); fseek(fpData, 0, SEEK_SET); fread(pImage->yPlane, 1, len, fpData); fclose(fpData); fpData = NULL; } pImage->uvPlane = pImage->yPlane + pImage->width * pImage->height; } void SaveYUVImage(const char *filePath, YUVImage *pImage) { FILE *fp = fopen(filePath, "wb+"); if (fp) { fwrite(pImage->yPlane, pImage->width * pImage->height, 1, fp); fwrite(pImage->uvPlane, pImage->width * (pImage->height >> 1), 1, fp); } } void ReleaseYUVImage(YUVImage *pImage) { if (pImage->yPlane) { free(pImage->yPlane); pImage->yPlane = NULL; pImage->uvPlane = NULL; } }

NV21 圖片旋轉

以順時針旋轉 90 度為例,Y 和 UV 兩個平面分別從平面左下角進行縱向拷貝,需要注意的是每對 UV 分量作為一個整體進行拷貝。以此類比,順時針旋轉 180 度時從平面右下角進行橫向拷貝,順時針旋轉 270 度時從平面右上角進行縱向拷貝。

Y 平面旋轉

UV 平面旋轉

Y00  Y01  Y02  Y03              Y30  Y20  Y10  Y00
Y10  Y11  Y12  Y13    旋轉90度   Y31  Y21  Y11  Y01
Y20  Y21  Y22  Y23    -----> Y32 Y22 Y12 Y02 Y30 Y31 Y32 Y33 Y33 Y23 Y13 Y03 V00 U00 V01 U01 -----> V10 U10 V00 U00 V10 U10 V11 U11 V11 U11 V01 U01

代碼實現:

//angle 90,  270, 180
void RotateYUVImage(YUVImage *pSrcImg, YUVImage *pDstImg, int angle) { int yIndex = 0; int uvIndex = 0; switch (angle) { case 90: { // y plane for (int i = 0; i < pSrcImg->width; i++) { for (int j = 0; j < pSrcImg->height; j++) { *(pDstImg->yPlane + yIndex) = *(pSrcImg->yPlane + (pSrcImg->height - j - 1) * pSrcImg->width + i); yIndex++; } } //uv plane for (int i = 0; i < pSrcImg->width; i += 2) { for (int j = 0; j < pSrcImg->height / 2; j++) { *(pDstImg->uvPlane + uvIndex) = *(pSrcImg->uvPlane + (pSrcImg->height / 2 - j - 1) * pSrcImg->width + i); *(pDstImg->uvPlane + uvIndex + 1) = *(pSrcImg->uvPlane + (pSrcImg->height / 2 - j - 1) * pSrcImg->width + i + 1); uvIndex += 2; } } } break; case 180: { // y plane for (int i = 0; i < pSrcImg->height; i++) { for (int j = 0; j < pSrcImg->width; j++) { *(pDstImg->yPlane + yIndex) = *(pSrcImg->yPlane + (pSrcImg->height - 1 - i) * pSrcImg->width + pSrcImg->width - 1 - j); yIndex++; } } //uv plane for (int i = 0; i < pSrcImg->height / 2; i++) { for (int j = 0; j < pSrcImg->width; j += 2) { *(pDstImg->uvPlane + uvIndex) = *(pSrcImg->uvPlane + (pSrcImg->height / 2 - 1 - i) * pSrcImg->width + pSrcImg->width - 2 - j); *(pDstImg->uvPlane + uvIndex + 1) = *(pSrcImg->uvPlane + (pSrcImg->height / 2 - 1 - i) * pSrcImg->width + pSrcImg->width - 1 - j); uvIndex += 2; } } } break; case 270: { // y plane for (int i = 0; i < pSrcImg->width; i++) { for (int j = 0; j < pSrcImg->height; j++) { *(pDstImg->yPlane + yIndex) = *(pSrcImg->yPlane + j * pSrcImg->width + (pSrcImg->width - i - 1)); yIndex++; } } //uv plane for (int i = 0; i < pSrcImg->width; i += 2) { for (int j = 0; j < pSrcImg->height / 2; j++) { *(pDstImg->uvPlane + uvIndex + 1) = *(pSrcImg->uvPlane + j * pSrcImg->width + (pSrcImg->width - i - 1)); *(pDstImg->uvPlane + uvIndex) = *(pSrcImg->uvPlane + j * pSrcImg->width + (pSrcImg->width - i - 2)); uvIndex += 2; } } } break; default: break; } }

NV21 圖片縮放

將 2x2 的 NV21 圖縮放成 4x4 的 NV21 圖,原圖橫向每個像素的 Y 分量向右拷貝 1(放大倍數-1)次,縱向每列元素以列為單位向下拷貝 1(放大倍數-1)次.

NV21 圖片上采樣

將 4x4 的 NV21 圖縮放成 2x2 的 NV21 圖,實際上就是進行下采樣。

NV21 圖片下采樣

代碼實現:

void ResizeYUVImage(YUVImage *pSrcImg, YUVImage *pDstImg) { if (pSrcImg->width > pDstImg->width) { //縮小 int x_scale = pSrcImg->width / pDstImg->width; int y_scale = pSrcImg->height / pDstImg->height; for (size_t i = 0; i < pDstImg->height; i++) { for (size_t j = 0; j < pDstImg->width; j++) { *(pDstImg->yPlane + i*pDstImg->width + j) = *(pSrcImg->yPlane + i * y_scale *pSrcImg->width + j * x_scale); } } for (size_t i = 0; i < pDstImg->height / 2; i++) { for (size_t j = 0; j < pDstImg->width; j += 2) { *(pDstImg->uvPlane + i*pDstImg->width + j) = *(pSrcImg->uvPlane + i * y_scale *pSrcImg->width + j * x_scale); *(pDstImg->uvPlane + i*pDstImg->width + j + 1) = *(pSrcImg->uvPlane + i * y_scale *pSrcImg->width + j * x_scale + 1); } } } else { // 放大 int x_scale = pDstImg->width / pSrcImg->width; int y_scale = pDstImg->height / pSrcImg->height; for (size_t i = 0; i < pSrcImg->height; i++) { for (size_t j = 0; j < pSrcImg->width; j++) { int yValue = *(pSrcImg->yPlane + i *pSrcImg->width + j); for (size_t k = 0; k < x_scale; k++) { *(pDstImg->yPlane + i * y_scale * pDstImg->width + j * x_scale + k) = yValue; } } unsigned char *pSrcRow = pDstImg->yPlane + i * y_scale * pDstImg->width; unsigned char *pDstRow = NULL; for (size_t l = 1; l < y_scale; l++) { pDstRow = (pDstImg->yPlane + (i * y_scale + l)* pDstImg->width); memcpy(pDstRow, pSrcRow, pDstImg->width * sizeof(unsigned char )); } } for (size_t i = 0; i < pSrcImg->height / 2; i++) { for (size_t j = 0; j < pSrcImg->width; j += 2) { int vValue = *(pSrcImg->uvPlane + i *pSrcImg->width + j); int uValue = *(pSrcImg->uvPlane + i *pSrcImg->width + j + 1); for (size_t k = 0; k < x_scale * 2; k += 2) { *(pDstImg->uvPlane + i * y_scale * pDstImg->width + j * x_scale + k) = vValue; *(pDstImg->uvPlane + i * y_scale * pDstImg->width + j * x_scale + k + 1) = uValue; } } unsigned char *pSrcRow = pDstImg->uvPlane + i * y_scale * pDstImg->width; unsigned char *pDstRow = NULL; for (size_t l = 1; l < y_scale; l++) { pDstRow = (pDstImg->uvPlane + (i * y_scale + l)* pDstImg->width); memcpy(pDstRow, pSrcRow, pDstImg->width * sizeof(unsigned char )); } } } }

NV21 圖片裁剪

圖例中將 6x6 的 NV21 圖按照橫縱坐標偏移量為(2,2)裁剪成 4x4 的 NV21 圖。

Y 平面剪切

UV 平面剪切

代碼實現:

// x_offSet ,y_offSet % 2 == 0
void CropYUVImage(YUVImage *pSrcImg, int x_offSet, int y_offSet, YUVImage *pDstImg) { // 確保裁剪區域不存在內存越界 int cropWidth = pSrcImg->width - x_offSet; cropWidth = cropWidth > pDstImg->width ? pDstImg->width : cropWidth; int cropHeight = pSrcImg->height - y_offSet; cropHeight = cropHeight > pDstImg->height ? pDstImg->height : cropHeight; unsigned char *pSrcCursor = NULL; unsigned char *pDstCursor = NULL; //crop yPlane for (size_t i = 0; i < cropHeight; i++) { pSrcCursor = pSrcImg->yPlane + (y_offSet + i) * pSrcImg->width + x_offSet; pDstCursor = pDstImg->yPlane + i * pDstImg->width; memcpy(pDstCursor, pSrcCursor, sizeof(unsigned char ) * cropWidth); } //crop uvPlane for (size_t i = 0; i < cropHeight / 2; i++) { pSrcCursor = pSrcImg->uvPlane + (y_offSet / 2 + i) * pSrcImg->width + x_offSet; pDstCursor = pDstImg->uvPlane + i * pDstImg->width; memcpy(pDstCursor, pSrcCursor, sizeof(unsigned char ) * cropWidth); } }

Sample 測試

原圖

原圖(NV21 圖都已轉為 png 便於顯示)

測試代碼:

void main() { YUVImage srcImg = { 0 }; srcImg.width = 840; srcImg.height = 1074; LoadYUVImage("IMG_840x1074.NV21", &srcImg); YUVImage rotateDstImg = { 0 }; rotateDstImg.width = 1074; rotateDstImg.height = 840; rotateDstImg.yPlane = malloc(rotateDstImg.width * rotateDstImg.height*1.5); rotateDstImg.uvPlane = rotateDstImg.yPlane + rotateDstImg.width * rotateDstImg.height; RotateYUVImage(&srcImg, &rotateDstImg, 270); SaveYUVImage("D:\\material\\IMG_1074x840_270.NV21", &rotateDstImg); RotateYUVImage(&srcImg, &rotateDstImg, 90); SaveYUVImage("D:\\material\\IMG_1074x840_90.NV21", &rotateDstImg); rotateDstImg.width = 840; rotateDstImg.height = 1074; RotateYUVImage(&srcImg, &rotateDstImg, 180); SaveYUVImage("D:\\material\\IMG_840x1074_180.NV21", &rotateDstImg); YUVImage resizeDstImg = { 0 }; resizeDstImg.width = 420; resizeDstImg.height = 536; resizeDstImg.yPlane = malloc(resizeDstImg.width * resizeDstImg.height*1.5); resizeDstImg.uvPlane = resizeDstImg.yPlane + resizeDstImg.width * resizeDstImg.height; ResizeYUVImage(&srcImg, &resizeDstImg); SaveYUVImage("D:\\material\\IMG_420x536_Resize.NV21", &resizeDstImg); YUVImage cropDstImg = { 0 }; cropDstImg.width = 300; cropDstImg.height = 300; cropDstImg.yPlane = malloc(cropDstImg.width * cropDstImg.height*1.5); cropDstImg.uvPlane = cropDstImg.yPlane + cropDstImg.width * cropDstImg.height; CropYUVImage(&srcImg, 100, 500, &cropDstImg); SaveYUVImage("D:\\material\\IMG_300x300_crop.NV21", &cropDstImg); ReleaseYUVImage(&srcImg); ReleaseYUVImage(&rotateDstImg); ReleaseYUVImage(&resizeDstImg); ReleaseYUVImage(&cropDstImg); }

測試結果:

IMG_1074x840_270

IMG_1074x840_90

IMG_1074x840_180

IMG_420x536_Resize

IMG_300x300_crop

-- END --


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM