【YUV】YUV图像基本处理

YUV简介

YUV是一种色彩编码模型,也叫YCbCr,Y:明亮度 U(Chromosnnance):色度 V:浓度(Chroma)

YUV编码模型的图像一般不能直接用于显示,需要转换为RGB(RGBA)编码模型,才能够正常显示图像

2.YUV的几种采样方式

image

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 、NV21 格式;
  • 平面格式(Planar):YUV 图像数据的三个分量分别存放在不同的矩阵中,这种格式适用于采样,如 YV12、YU12 格式。

2.1YUV的几种常见的格式

对 YUV 图像处理中,YUYV 、YU12(I420)、NV21 和 NV12 最为常用,下面介绍下这几种格式的存储方式。

以一幅分辨率为 4x4 的 YUV 图为例,说明在不同 YUV 格式下的存储方式(括号内范围表示内存地址索引范围,默认以下不同格式图片存储使用的都是连续内存)。

2.1.1YUYV(YUV422采样方式)

YUYV 是 2 个Y 分量共用一对 UV 分量,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 的存储空间。,在传输时占用的带宽也会随之减小。

2.1.2 YV12/YU12(YUV420采样方式)

YV12/YU12 也属于 YUV420P ,即 YUV420 采样方式的平面模式,YUV 三个分量分别存储于 3 个不同的矩阵(平面)。

image

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 的存储空间。

2.1.3 NV12、NV21(YUV420采样方式)

NV21/NV12 属于 YUV420SP ,YUV420SP 格式有 2 个平面,Y 分量存储于一个平面,UV 分量交错存储于另一个平面。

image

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

3.YUV图像的基本操作

下面以最常用的 NV21 图为例介绍其旋转、缩放和剪切的基本方法。

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

3.2 NV12图片旋转

以顺时针旋转 90 度为例,Y 和 UV 两个平面分别从平面左下角进行纵向拷贝,需要注意的是一对 UV 分量作为一个整体进行拷贝。

以此类比,顺时针旋转 180 度时从平面右下角进行横向拷贝,顺时针旋转 270 度时从平面右上角进行纵向拷贝。

image

image

存储空间表示:

旋转90度
Y00 Y01 Y02 Y03  --->   Y30 Y20 Y10 Y00
Y10 Y11 Y12 Y13         Y31 Y21 Y11 Y01
Y20 Y21 Y22 Y23         Y32 Y22 Y12 Y02
Y30 Y31 Y32 Y33         Y33 Y23 Y13 Y03
旋转90度
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;
    }
}

3.3 NV12图片缩放

将 2x2 的 NV21 图缩放成 4x4 的 NV21 图,原图横向每个像素的 Y 分量向右拷贝 1(放大倍数-1)次,纵向每列元素以列为单位向下拷贝 1(放大倍数-1)次.

image

4x4 的 NV21 图缩放成 2x2 的 NV21 图,实际上就是进行采样。

image

代码实现:

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

3.4NV12图片裁剪

图例中将 6x6 的 NV21 图按照横纵坐标偏移量为(2,2)裁剪成 4x4 的 NV21 图。

image
24.cnblogs.com/blog/2800270/202402/2800270-20240229235708303-1041592512.jpg)

image

代码实现:

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

3.5 测试代码

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

转载文章:

https://zhuanlan.zhihu.com/p/683452602?utm_campaign=shareopn&utm_medium=social&utm_oi=909142489515040768&utm_psn=1744642023649378304&utm_source=wechat_session

posted @ 2024-02-29 23:49  -zx-  阅读(22)  评论(0编辑  收藏  举报