YUV数据格式详解与Python转换实现
本文深入解析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 实现方式的全部内容分享完毕,希望能为大家提供参考。
免责声明:本文内容仅供参考,不代表本站观点。转载或引用请注明出处。









