一文掌握 YUV 图像的基本处理

简介: YUV 是一种色彩编码模型,也叫做 YCbCr,其中 “Y” 表示明亮度(Luminance),“U” 和 “V” 分别表示色度(Chrominance)和浓度(Chroma)。

作者:字节流动

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


YUV 的由来

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

YUV 色彩编码模型,其设计初衷为了解决彩色电视机与黑白电视的兼容问题,利用了人类眼睛的生理特性(对亮度敏感,对色度不敏感),允许降低色度的带宽,降低了传输带宽。

在计算机系统中应用尤为广泛,利用 YUV 色彩编码模型可以降低图片数据的内存占用,提高数据处理效率。

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

YUV 几种常见采样方式

image.png

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 个不同的矩阵(平面)。

image.png

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 分量交错存储于另一个平面。

image.png

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 度时从平面右上角进行纵向拷贝。

image.png

image.png

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)次.

image.png

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

image.png

代码实现:

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 图。

image.pngimage.png


代码实现:

// 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 测试

原图

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

测试结果

image.png

image.png

image.png

image.png

image.png

联系与交流

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


「视频云技术」你最值得关注的音视频技术公众号,每周推送来自阿里云一线的实践技术文章,在这里与音视频领域一流工程师交流切磋。

阿里云视频云@凡科快图.png

相关文章
|
传感器 数据库
一种多源信息融合方法及其应用(Matlab代码实现)
一种多源信息融合方法及其应用(Matlab代码实现)
515 0
Mac Source Tree拉代码报错,remote: CODING 提示: Authentication failed. remote: 认证失败,请确认您输入了正确的账号密码。 fatal...
Mac Source Tree拉代码报错,remote: CODING 提示: Authentication failed. remote: 认证失败,请确认您输入了正确的账号密码。 fatal...
2376 0
Mac Source Tree拉代码报错,remote: CODING 提示: Authentication failed. remote: 认证失败,请确认您输入了正确的账号密码。 fatal...
|
存储 安全 算法
【C++ 包装器类 std::atomic 】全面入门指南:深入理解并掌握C++ std::atomic 原子操作 的实用技巧与应用
【C++ 包装器类 std::atomic 】全面入门指南:深入理解并掌握C++ std::atomic 原子操作 的实用技巧与应用
1483 1
|
9月前
|
人工智能 测试技术 API
Ollama本地模型部署+API接口调试超详细指南
本文介绍了如何使用Ollama工具下载并部署AI大模型(如DeepSeek-R1、Llama 3.2等)。首先,访问Ollama的官方GitHub页面下载适合系统的版本并安装。接着,在终端输入`ollama`命令验证安装是否成功。然后,通过命令如`ollama run Llama3.2`下载所需的AI模型。下载完成后,可以在控制台与AI模型进行对话,或通过快捷键`control+d`结束会话。为了更方便地与AI互动,可以安装GUI或Web界面。此外,Ollama还提供了API接口,默认支持API调用,用户可以通过Apifox等工具调试这些API。
|
缓存 网络协议 物联网
UDP的可靠性传输
UDP的可靠性传输
483 1
|
运维 监控 安全
堡垒机是用来干什么的?堡垒机的好处有什么?
堡垒机能保障网络和数据不受来自外部和内部用户的入侵和破坏,运用各种技术手段实时收集和监控网络环境。
1860 1
|
设计模式 安全 数据库连接
|
Linux 开发工具 异构计算
【ZYNQ】QSPI Flash 固化程序全攻略
【ZYNQ】QSPI Flash 固化程序全攻略
3845 0
|
网络安全 数据安全/隐私保护
【已解决】mac端 sourceTree 解决remote: HTTP Basic: Access denied报错
又是在一次使用sourcetree拉取或者提交代码时候,遇到了sourcetree报错; 排查了一会,比如查看了SSH keys是否有问题、是否与sourcetree账户状态有问题等等,最终才发现并解决问题
|
Unix Linux API
开源库介绍:libusb 及其使用
开源库介绍:libusb 及其使用
6722 0
开源库介绍:libusb 及其使用