天天看點

一文掌握 YUV 圖像的基本處理YUV 的由來YUV 幾種常見采樣方式YUV 幾種常用的格式YUV 圖像的基本操作聯系與交流

作者:位元組流動

來源:

https://blog.csdn.net/Kennethdroid/article/details/94031821

YUV 的由來

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

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

在計算機系統中應用尤為廣泛,利用 YUV 色彩編碼模型可以降低圖檔資料的記憶體占用,提高資料處理效率。

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

YUV 幾種常見采樣方式

一文掌握 YUV 圖像的基本處理YUV 的由來YUV 幾種常見采樣方式YUV 幾種常用的格式YUV 圖像的基本操作聯系與交流

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

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

其中最常用的采樣方式是 YUV422 和 YUV420 。

YUV 格式也可按照 YUV 三個分量的組織方式分為打包(Packed)格式和平面格式(Planar)。

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

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      

一幅 720P (1280x720分辨率) 的圖檔,使用 YUV422 采樣時占用存儲大小為:

Y 分量:1280 * 720  = 921600 位元組
U 分量:1280 * 720 * 0.5 = 460800 位元組
V 分量:1280 * 720 * 0.5 = 460800 位元組
總大小:Y 分量 + U 分量 + V 分量 = (1280 * 720 + 1280 * 720 * 0.5 * 2) / 1024 / 1024 = 1.76 MB       

由上面計算可以看出 YUV422 采樣的圖像比 RGB 模型圖像節省了 1/3 的存儲空間。,在傳輸時占用的帶寬也會随之減小。

YV12/YU12 (YUV420 采樣方式)

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

一文掌握 YUV 圖像的基本處理YUV 的由來YUV 幾種常見采樣方式YUV 幾種常用的格式YUV 圖像的基本操作聯系與交流

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      

一幅 720P (1280x720分辨率) 的圖檔,使用 YUV420 采樣時(格式 YV12/YU12 )占用存儲大小為:

Y 分量:1280 * 720  = 921600 位元組
U 分量:1280 * 720 * (1/4) = 230400 位元組
V 分量:1280 * 720 * (1/4) = 230400 位元組
總大小:Y 分量 + U 分量 + V 分量 = (1280 * 720 + 1280 * 720 * (1/4)* 2) / 1024 / 1024 = 1.32 MB       

由上面計算可以看出 YUV420 采樣(格式 YV12/YU12 )的圖像比 RGB 模型圖像節省了 1/2 的存儲空間。

NV21/NV12 (YUV420 采樣方式)

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

一文掌握 YUV 圖像的基本處理YUV 的由來YUV 幾種常見采樣方式YUV 幾種常用的格式YUV 圖像的基本操作聯系與交流

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 分量排列的先後順序不同。

一幅 720P (1280x720分辨率) 的圖檔,使用 YUV420 采樣時(格式 NV21/NV12 )占用存儲大小為:

Y 分量:1280 * 720  = 921600 位元組
UV 分量:1280 * 720 * (1/2) = 460800 位元組
總大小:Y 分量 + UV 分量 = (1280 * 720 + 1280 * 720 * (1/2)) / 1024 / 1024 = 1.32 MB       

由上面計算可以看出 YUV420 采樣(格式 NV21/NV12 )的圖像比 RGB 模型圖像也節省了 1/2 的存儲空間。

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 度時從平面右上角進行縱向拷貝。

一文掌握 YUV 圖像的基本處理YUV 的由來YUV 幾種常見采樣方式YUV 幾種常用的格式YUV 圖像的基本操作聯系與交流
一文掌握 YUV 圖像的基本處理YUV 的由來YUV 幾種常見采樣方式YUV 幾種常用的格式YUV 圖像的基本操作聯系與交流
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)次.

一文掌握 YUV 圖像的基本處理YUV 的由來YUV 幾種常見采樣方式YUV 幾種常用的格式YUV 圖像的基本操作聯系與交流

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

一文掌握 YUV 圖像的基本處理YUV 的由來YUV 幾種常見采樣方式YUV 幾種常用的格式YUV 圖像的基本操作聯系與交流
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 圖。

一文掌握 YUV 圖像的基本處理YUV 的由來YUV 幾種常見采樣方式YUV 幾種常用的格式YUV 圖像的基本操作聯系與交流
一文掌握 YUV 圖像的基本處理YUV 的由來YUV 幾種常見采樣方式YUV 幾種常用的格式YUV 圖像的基本操作聯系與交流
// 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 測試

原圖

一文掌握 YUV 圖像的基本處理YUV 的由來YUV 幾種常見采樣方式YUV 幾種常用的格式YUV 圖像的基本操作聯系與交流

測試代碼

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);
}      

測試結果

一文掌握 YUV 圖像的基本處理YUV 的由來YUV 幾種常見采樣方式YUV 幾種常用的格式YUV 圖像的基本操作聯系與交流
一文掌握 YUV 圖像的基本處理YUV 的由來YUV 幾種常見采樣方式YUV 幾種常用的格式YUV 圖像的基本操作聯系與交流
一文掌握 YUV 圖像的基本處理YUV 的由來YUV 幾種常見采樣方式YUV 幾種常用的格式YUV 圖像的基本操作聯系與交流
一文掌握 YUV 圖像的基本處理YUV 的由來YUV 幾種常見采樣方式YUV 幾種常用的格式YUV 圖像的基本操作聯系與交流
一文掌握 YUV 圖像的基本處理YUV 的由來YUV 幾種常見采樣方式YUV 幾種常用的格式YUV 圖像的基本操作聯系與交流

聯系與交流

技術交流可以添加我的微信:Byte-Flow

「視訊雲技術」你最值得關注的音視訊技術公衆号,每周推送來自阿裡雲一線的實踐技術文章,在這裡與音視訊領域一流工程師交流切磋。
一文掌握 YUV 圖像的基本處理YUV 的由來YUV 幾種常見采樣方式YUV 幾種常用的格式YUV 圖像的基本操作聯系與交流