YUV数据格式详解与Python转换实现

沿途有光
沿途有光
发布时间:2026-10-13| 6人浏览过
原创

本文深入解析YUV色彩空间原理,对比Planar与Packed存储格式,详解YUV4:4:4至4:1:1采样率对空间占用的影响。重点剖析I420、YV12、NV12等常见格式的结构差异,并提供Python代码将YUV420P及NV21格式转换为JPEG图像的具体实现方案。

YUV数据格式概览与原理

YUV 数据格式概览

YUV 的核心原理在于将亮度与色度分离处理。它使用 Y、U、V 三个分量分别代表亮度、蓝色通道与亮度的差值,以及红色通道与亮度的差值。Y 信号不仅包含亮度(luma)信息,还蕴含大量绿色通道数据,因此仅凭 Y 分量即可还原完整的黑白图像。U 和 V 分量则分别对应蓝(blue)和红(red)的色彩信号,仅携带色度(chrominance/color)信息。正因如此,YUV 格式也常被称为 YCbCr,其中的 C 可理解为 component(分量)或 color(色彩)。

维基百科提供的 RGB 转 YUV 转换公式,能更直观地揭示 YUV 与 RGB 之间的数学关系,并解释为何业界常使用 YCbCr 这一术语。

由于 Y 分量中包含了三原色的信息且绿色通道占比最高,Y、U、V 三者结合即可还原出全彩图像。

基于此原理,业界还衍生出 YCgCb、YCgCr 等针对特定应用场景的色彩空间标准,相关研究与应用已较为成熟。

如下所示,一张图片从上至下依次展示了原图、Y 分量、U 分量和 V 分量的视觉效果。

采用 YUV 而非 RGB 的原因既有历史因素,也有技术优势。历史上,为了兼容老式黑白电视,YUV 格式在仅输出 Y 分量时即可显示黑白图像,实现了向后兼容。此外,YUV 允许根据需求选择特定的存储格式,从而有效降低裸码流的空间占用,这是 RGB 格式难以灵活做到的。

YUV存储格式与采样方式

YUV 存储格式

YUV 数据的存储格式主要分为两大类:planar(平面)和 packed(打包)。

在 planar 格式的 YUV 中,数据先连续存储所有像素点的 Y 分量,紧接着存储所有像素点的 U 分量,最后存储所有像素点的 V 分量。这种结构相当于将 Y、U、V 拆分为三个独立的平面进行存储。

在 packed 格式的 YUV 中,每个像素点的 Y、U、V 分量是连续交替存储的,例如 YUVYUV... 的形式。

YUV 码流根据采样方式的不同,进一步细分为 YUV4:4:4、YUV4:2:2、YUV4:2:0 和 YUV4:1:1 等格式,其中前三种最为常见。所谓采样,是指按照特定间隔提取数据。比例数值表示 Y、U、V 在像素中占用的比值关系。这种设计旨在优化存储和扫描效率,其原理与 DVD 的扫描线机制类似。

具体示例如下:

YUV4:4:4 表示每个像素由一个 Y、一个 U 和一个 V 组成。即每 4 个 Y 采样对应 4 个 Cb 和 4 个 Cr 采样。每个像素占用 8+8+8=24 位,图像质量最高,但空间占用也最大,与 RGB 存储时的空间开销一致。对于 M*N 分辨率的图像,该模式下的存储空间为 M*N*3 字节。

YUV4:2:2 表示每 4 个 Y 采样对应 2 个 Cb 和 2 个 Cr 采样。在解码时,部分像素点仅有亮度信息而无色度信息,缺失的色度需通过相邻色度信息按算法插值填充。此模式下,平均每个像素占用 8+4+4=16 位。对于 M*N 分辨率的图像,空间占用为 16/24,即 M*N*3*(16/24) = M*N*2 字节。

YUV4:2:0 表示每 4 个 Y 采样对应 2 个 U 采样或 2 个 V 采样。需注意,这里的“0”并非指没有 V 分量,而是指无论水平还是垂直方向,色度采样率均为亮度的一半。该格式下,平均每个像素占用 8+4+0=12 位(注:此处指色度分量总比特数分配逻辑,实际计算中 U 和 V 各占一半空间)。对于 M*N 分辨率的图像,空间占用为原来的 12/24,即 M*N*3*(12/24) = M*N*3/2 字节。由于节省较多存储空间,这是最常用的存储格式。

YUV4:1:1 表示每 4 个 Y 采样对应 1 个 U 采样和 1 个 V 采样。平均每个像素占用 8+2+2=12 位,图像空间占用情况与 YUV4:2:0 相同。由于画质损失较大,这种格式在实际应用中非常少见。

对于 packed 存储格式的具体细节,此处略过。

常见YUV格式(I420/NV12等)解析

YV12/I420/YU12/NV12/NV21

YV12、I420、YU12、NV12 和 NV21 均属于 YUV 4:2:0 格式。其中 YU12 等同于 I420,YV12 和 I420 也被称为 YUV420P(即平面格式,planar)。YV12 与标准模式 I420 的主要区别在于 U 和 V 分量的存储顺序不同。

YV12 的名称来源是 Y 分量后紧跟 V 分量(然后是 U 分量),数字“12”表示其位深为 12 位,即每个像素占用 12 位空间。

在 I420(即 YU12)格式中,U 平面紧跟在 Y 平面之后,随后是 V 平面(顺序为 YUV);而 YV12 的顺序则相反(YVU)。大多数视频解码器输出的原始图像格式为 I420(例如安卓系统中的图像通常为 I420 或 NV21),而多数硬件解码器(如 Intel MSDK、NVIDIA cuvid、iOS 硬解码)内部使用的则是 NV12 格式。

另一类 YUV420SP 格式,即 Y 分量为平面存储,而 UV 分量打包交错存储,典型代表为 NV12。NV12 与 NV21 结构类似,区别仅在于 U 和 V 的交错排列顺序不同。

可理解为以下结构:

I420: YYYYYYYY UU VV => YUV420P YV12: YYYYYYYY VV UU => YUV420P NV12: YYYYYYYY UVUV => YUV420SP NV21: YYYYYYYY VUVU => YUV420SP

维基百科上提供了 I420 和 NV12 的两张示意图,非常有助于理解其结构。

I420 的单帧结构示意图如下(采用 Planar 方式):

从该图的上半部分可以看出,Y1、Y2、Y7、Y8 等像素点共用 U1 和 V1。下方的线性数组展示了存储顺序:Y、U 和 V 分量均顺序存储。在写入数据时,先按顺序写出 Y 分量,随后再依次写出 U 和 V 分量。

NV12 的单帧结构示意图如下(采用 Planar 方式):

与 YV12 不同,NV12 的 Y 分量虽然也是顺序存储,但 U 和 V 分量是交错存储的。在写入数据时,先直接顺序写出 Y 分量,随后对 UV 交错序列依次写出。

Python实现YUV转JPG

Python的实现:将420P转为jpg


from PIL import Image
def yuv420_to_rgb888(width, height, yuv):
  # function requires both width and height to be multiples of 4
  if (width % 4) or (height % 4):
    raise Exception("width and height must be multiples of 4")
  rgb_bytes = bytearray(width*height*3)

  red_index = 0
  green_index = 1
  blue_index = 2
  y_index = 0
  for row in range(0,height):
    u_index = width * height + (row//2)*(width//2)
    v_index = u_index + (width*height)//4
    for column in range(0,width):
      Y = yuv[y_index]
      U = yuv[u_index]
      V = yuv[v_index]
      C = (Y - 16) * 298
      D = U - 128
      E = V - 128
      R = (C + 409*E + 128) // 256
      G = (C - 100*D - 208*E + 128) // 256
      B = (C + 516 * D + 128) // 256
      R = 255 if (R > 255) else (0 if (R < 0) else R)
      G = 255 if (G > 255) else (0 if (G < 0) else G)
      B = 255 if (B > 255) else (0 if (B < 0) else B)
      rgb_bytes[red_index] = R
      rgb_bytes[green_index] = G
      rgb_bytes[blue_index] = B
      u_index += (column % 2)
      v_index += (column % 2)
      y_index += 1
      red_index += 3
      green_index += 3
      blue_index += 3
  return rgb_bytes

def testConversion(source, dest):
  print("opening file")
  f = open(source, "rb")
  yuv = f.read()
  f.close()
  print("read file")
  rgb_bytes = yuv420_to_rgb888(4208,3120, yuv)
  # cProfile.runctx('yuv420_to_rgb888(1920,1088, yuv)', {'yuv420_to_rgb888':yuv420_to_rgb888}, {'yuv':yuv})
  print("finished conversion. Creating image object")
  img = Image.frombytes("RGB", (4208,3120), bytes(rgb_bytes))
  print("Image object created. Starting to save")
  img.save(dest, "JPEG")
  img.close()
  print("Save completed")

testConversion("C:/adb1031/yuveffectout/MV_F_Cap1.yuv", "C:/adb1031/yuveffectout/MV_F_Cap1.jpg")
testConversion("C:/adb1031/yuveffectout/MV_F_Cap2.yuv", "C:/adb1031/yuveffectout/MV_F_Cap2.jpg")

Python的实现:将NV21转为jpg


from PIL import Image
def yuv420_to_rgb888(width, height, yuv):
  # function requires both width and height to be multiples of 4
  if (width % 4) or (height % 4):
    raise Exception("width and height must be multiples of 4")
  rgb_bytes = bytearray(width*height*3)

  red_index = 0
  green_index = 1
  blue_index = 2
  y_index = 0

  v_index = width * height

  for row in range(0,height):

    v_index = width * height + (row//2)*width
    u_index = v_index + 1
    for column in range(0,width):
      Y = yuv[y_index]
      #print(y_index)
      U = yuv[u_index]
      V = yuv[v_index]
      C = (Y - 16) * 298
      D = U - 128
      E = V - 128
      R = (C + 409*E + 128) // 256
      G = (C - 100*D - 208*E + 128) // 256
      B = (C + 516 * D + 128) // 256
      R = 255 if (R > 255) else (0 if (R < 0) else R)
      G = 255 if (G > 255) else (0 if (G < 0) else G)
      B = 255 if (B > 255) else (0 if (B < 0) else B)
      rgb_bytes[red_index] = R
      rgb_bytes[green_index] = G
      rgb_bytes[blue_index] = B
      if column==0:
        v_index = v_index
      elif column%2==0:
        v_index = v_index + 2
      u_index = v_index + 1
      y_index += 1
      red_index += 3
      green_index += 3
      blue_index += 3
  return rgb_bytes


def testConversion(source, dest):
  print("opening file")
  f = open(source, "rb")
  yuv = f.read()
  f.close()
  print("read file")
  rgb_bytes = yuv420_to_rgb888(1280,720, yuv)
  # cProfile.runctx('yuv420_to_rgb888(1920,1088, yuv)', {'yuv420_to_rgb888':yuv420_to_rgb888}, {'yuv':yuv})
  print("finished conversion. Creating image object")
  img = Image.frombytes("RGB", (1280,720), bytes(rgb_bytes))
  print("Image object created. Starting to save")
  img.save(dest, "JPEG")
  img.close()
  print("Save completed")

testConversion("./test/4.yuv", "4.jpg")

 

以上基于 YUV 数据格式详解及 Python 实现方式的全部内容分享完毕,希望能为大家提供参考。

相关标签:

免责声明:本文内容仅供参考,不代表本站观点。转载或引用请注明出处。

Python 相关产品和应用

全部下载
Selenium

Selenium

4.51.0 · Windows

用于浏览器自动化和 Web 测试的 Python 工具。

scikit-learn

scikit-learn

1.9.1 · Windows

常用 Python 机器学习算法与模型评估工具库。

Black

Black

26.10.1 · Windows

强调一致性的 Python 自动代码格式化工具。

Ruff

Ruff

0.17.1 · macOS

由 Rust 编写的高速 Python 代码检查和格式化工具。

pytest

pytest

9.1.1 · Windows

简洁而强大的 Python 自动化测试框架。

FastAPI

FastAPI

0.146.0 · Windows

高性能 Python API 框架,支持类型提示和自动接口文档。

Flask

Flask

3.1.3 · Windows

轻量灵活的 Python Web 应用框架。

Django

Django

6.1.2 · Windows

成熟的 Python Web 框架,适合构建内容站点和业务系统。

Spyder

Spyder

6.1.7 · Windows

面向科学计算与数据分析的 Python 集成开发环境。

SciPy

SciPy

1.18.1 · Windows

面向科学、工程和技术计算的 Python 算法库。