YU12/I420/IYUV命名纠葛:YUV420P内存布局与实战解析
2026/9/8 2:52:27 网站建设 项目流程

1. 一个误导了很多人的命名纠葛

1.1 我亲身经历的一次"彩屏"事故

早几年做视频采集模块时,遇到过一件让我印象深刻的事。设备端编码器输出的数据,SDK文档上清清楚楚写着"IYUV",我按I420的布局去解析,结果画面颜色完全错乱——人脸是蓝绿色的,天空是橘红色的。当时第一反应是数据没取对,逐字节dump出来核对了半天,Y平面和U平面的数据量都对得上,可颜色就是不对。

后来请教了一位做编解码底层的老同事,他看了一眼就笑了:"IYUV就是I420,只是名字不一样。你颜色错乱,八成是U、V两个平面的读取顺序搞反了。"我回去仔细一查,果然——数据处理流程里有个上游模块把YV12的布局误当成I420在传。也就是说,问题根源不是解码,而是有人把"看起来相似的名字"当成了"不同的格式"。

这个经历给我留下的教训很深:YUV这个领域,命名混乱导致的隐性bug,远比编解码本身的复杂度更折腾人。YU12、I420、IYUV,这三个名字频繁出现在各种SDK、播放器、网络协议和芯片文档里,表面上像三种不同格式,实际上它们的字节布局完全一致。

1.2 三个名字对应的唯一内存布局

说结论之前,先把最核心的概念往前放:YU12、I420、IYUV,三者都是YUV 4:2:0 planar(平面)格式的别名,数据排列方式完全一致——一帧图像按"先Y平面,后U平面,再V平面"的顺序连续存储。在FFmpeg里对应的是AV_PIX_FMT_YUV420P,在Android里叫ImageFormat.YU12,在Intel的老款SDK和很多多媒体框架里则写成IYUV。

我遇到过不少开发者,把这三个名字当成分属不同色彩格式,花了大量时间做没必要的格式转换。所以这篇文章不做高深理论,只做一件事:把这几个名字的来龙去脉、内存布局、实际验证方法、以及真正容易踩的坑一次讲透。看完之后,你至少能少走我当年绕过的那些弯路。

2. 从采样到内存布局:懂YUV420P才能真正理解三个名字

2.1 4:2:0采样在说什么

要理解这三个名字,绕不开YUV 4:2:0这个基础概念。先说人话版本:人眼对亮度(Luma,Y)的敏感度远高于对颜色(Chroma,U/V)的敏感度。因此视频编码领域很早就想到一个省流量的办法——亮度的分辨率完整保留,色度的分辨率大幅缩减。4:2:0采样指的就是:每4个亮度像素点,对应1个U色度点和1个V色度点,而且U、V各自独立采样。

从数值上讲,一张width x height的图像,Y平面就是width x height个字节;U平面和V平面分别是(width/2) x (height/2)个字节。图像的YUV420数据总大小就是:

Y平面大小 = width * height U平面大小 = (width / 2) * (height / 2) V平面大小 = (width / 2) * (height / 2) 一帧总大小 = width * height * 3 / 2

注意这里的前提是宽高均为偶数。实际编码器在遇到奇数宽高时通常会对齐到偶数,但如果是你手动组装数据,务必把这一点算清楚。

2.2 Y、U、V三个平面的物理排布

YUV420P里的P就是Planar,意思是三个分量分别存在三个独立的平面里。这三个平面在内存中是连续排列的,顺序为:Y平面在前,U平面居中,最后一个平面是V。

举个例子,一张640x480的I420帧:

Y平面: 640 * 480 = 307200 字节 U平面: 320 * 240 = 76800 字节 V平面: 320 * 240 = 76800 字节 总大小: 307200 + 76800 + 76800 = 460800 字节

对应到C语言的指针操作就是:

uint8_t *y_plane = data; uint8_t *u_plane = data + width * height; uint8_t *v_plane = data + width * height + (width / 2) * (height / 2);

Y平面内同样按行存储,第0行、第1行、第2行……连续排下去;U和V平面同理,只是行列各减半。U平面代表的是每个2x2亮度块对应的一个色度值,所以U的坐标映射关系是:

U[x][y] 对应亮度坐标 (x*2, y*2) 处2x2区域的颜色

很多人在做缩放、裁剪时没考虑到这种映射关系,导致局部区域颜色错位。这里先记个印象,后面会专门讲。

2.3 YU12、I420、IYUV的布局完全一致

现在回到标题本身。大前提已经清楚了:只要满足"Y平面 + U平面 + V平面连续存储"这一条,任何名字指向的物理数据都是同一回事。

  • I420:最早来自视频编码标准里的常见命名方式,I代表Interleaved的逆向——实际是Planar,但叫习惯了没改。顺序是Y、U、V,也就是U平面在前、V平面在后。
  • YU12:这个名字几乎只在Android和部分国产平台出现。Android官方的ImageFormat.YU12注释里直接写明"equivalent to I420"。
  • IYUV:这是比较老派的名字,常见于Intel早期多媒体SDK、一些直接基于DirectShow的滤镜和部分播放器内核。同样是Y、U、V的布局。

我见过有人把IYUV和YV12混在一起,这两个才是真正容易搞混的东西。YV12的顺序是Y、V、U——也就是V平面在前、U平面在后,与I420恰好是U、V互换。**IYUV和I420是同一个格式,IYUV和YV12不是。**这一句话,值得记十年。

3. 为什么同一个格式要留三个名字:平台生态与历史遗留

3.1 I420:编解码器世界的事实标准

I420这个名字之所以流传最广,主要是因为视频编码标准的发展路径。H.264、H.265的编码器内部,几乎都把YUV420P当成默认输入输出格式。虽然规范文档中很少直接使用"I420"这个字符串,但它作为描述性名字,已经深入到了FFmpeg、libvpx、x264、OpenH264这些项目的代码和注释里。

我在实际项目中看到的典型情况是:FFmpeg解码H.264裸流,输出像素格式为AV_PIX_FMT_YUV420P;如果你把这个帧直接送到GL着色器或者OpenCV处理,绝大多数库都会默认按I420去解析。所以在一个典型的视频处理链里,I420就是那个"对接标准"——上游、下游默认都认它。

3.2 YU12:Android与国产平台的习惯

Android的Camera2、MediaCodec等接口在输出预览数据时,经常出现YU12这个字符串。Android官方开发文档中ImageFormat.YU12的定义如下:它是一个YUV 4:2:0 planar格式,Y平面、U平面、V平面依次排列,U/V plane的宽高都是Y平面的一半。

这里有一个很多人踩过的坑:Android的Image对象里,每个plane都有独立的getRowStride()getPixelStride(),并不是你简单按width去算偏移就一定对。很多设备上Y平面的行字节数是按照16或64字节对齐过的,导致实际布局是"带padding的YU12",而不是教科书式的紧凑排列。后面专门讲stride时会展开。

国产平台方面,海思、瑞芯微、安霸等芯片厂商的SDK里,"YU12"这个名字出现频率非常高。比如海思的采样格式枚举中,PIXEL_FORMAT_YUV_SEMIPLANAR_420对应的是NV12,而PIXEL_FORMAT_YUV_PLANAR_420对应的数据就是YU12/I420。芯片手册里写YU12,应用层代码里写I420,这两者实际是同一块内存。

3.3 IYUV:老牌软件遗留下来的叫法

IYUV这个名字要追溯到多媒体技术早期。Intel在推广其视频处理库和硬件编解码解决方案时,习惯使用IYUV来指代一个YUV 4:2:0 planar帧。很多老旧的视频采集SDK、视频播放滤镜、以及部分文档,一直沿用这个叫法。

在Windows平台上,DirectShow的MEDIASUBTYPE_IYUVMEDIASUBTYPE_I420就经常交替出现,且很多实现内部完全一致。VLC、ffplay这类播放器在识别FourCC编码时,也会把I420IYUVYU12统一映射到同一种像素格式。

四字符码(FourCC)是理解这个问题的另一个角度。同样的布局数据,可以用不同FourCC标记:

FourCC字符串十六进制整数平面顺序本质
I4200x30323449Y、U、V与YU12/IYUV一致
YU120x32315559Y、U、V与I420/IYUV一致
IYUV0x56555949Y、U、V与I420/YU12一致
YV120x32315659Y、V、U唯一真正不同的格式

这份表我建议直接收藏。排查颜色异常时,先对着这个表核对一遍U/V顺序,能省出大量调试时间。

4. 不要混淆的同胞兄弟:I420与YV12的U/V交换问题

4.1 U和V交换后会怎样

U和V在YUV色彩空间里,一个决定蓝色差分量,一个决定红色差分量。两个平面一旦对调,画面最直观的表现就是颜色大范围错误:偏蓝、偏紫、肤色呈暗绿色,或者画面像老式照片的负片效果。

具体来说,U通道(Cb)描述的是蓝色分量与亮度的差值,V通道(Cr)描述的是红色分量与亮度的差值。当你把U、V对调后,相当于每个像素的色度信息整体错位,但在灰度图上可能完全看不出来——因为亮度分量Y没有动。这也是为什么很多人对着黑白纹理解析半天,始终找不到问题。

4.2 如何快速识别U/V是否反了

先说一个最快的方法:找一块肤色区域(比如人脸),看颜色是否偏红。肤色在YUV里通常是U偏小、V偏大。如果画面里人脸区域整体发蓝绿色,大概率就是U、V对调了。也可以用一块纯红色测试图,I420下红色区域应该是U值接近128、V值接近一定阈值;如果换成YV12解析,红色区域就变了样。

还有个更工程化的办法:拆出U平面和V平面各算一个平均亮度值。标准测试图的U平面平均值和V平面平均值有明显差异,如果你发现两者与已知参考值交换了位置,基本可以确认布局是YV12。实际项目中,我习惯在调试阶段打一条日志,输出前64字节的U平面和V平面十六进制内容,用肉眼就能看出规律——U平面和V平面如果内容互换,模式会完全不同。

4.3 平台适配时如何避免踩坑

跨平台视频管线中,最容易出现U/V反转的环节是:硬件解码器输出、显卡显存数据回读、以及Unity/Unreal引擎的纹理上传。NVIDIA的Video Codec SDK输出通常是NV12(半平面),而很多老平台的DXVA输出却是YV12,你如果直接把这数据标成I420去处理,颜色必乱。

处理方案很简单:在任何涉及YUV420P数据对接的地方,不要用"猜"的,要在初始化阶段做一次显式检测。检测思路是喂入一帧已知颜色分布的标准图,解析后在关键区域取色,与期望值比对。这个自动化测试步骤如果每次接入新设备都跑一遍,能挡掉80%以上的兼容性问题。

5. 实战验证:写代码确认三个名字指向同一块数据

5.1 用FFmpeg生成统一的YUV420P数据

理论讲再多,不如直接把数据dump出来看。下面是一套非常实操的验证方案,用FFmpeg把任意视频源转为YUV420P原始帧,再通过哈希校验和十六进制对比确认布局一致。

# 把视频的前30帧转成raw I420格式 ffmpeg -i input.mp4 -t 1 -c:v rawvideo -pix_fmt yuv420p output.yuv

FFmpeg的yuv420p像素格式就是I420/YU12/IYUV布局。如果你的输入源本身是I420的raw文件,转换前后字节应该完全不变;如果输入源是其他格式,FFmpeg会负责转换。

验证脚本来一段Python,直接读YUV文件的前几个字节:

import hashlib with open("output.yuv", "rb") as f: data = f.read() # 计算整段数据的MD5,用于跨格式对比 md5 = hashlib.md5(data).hexdigest() print(f"总字节数: {len(data)}") print(f"MD5: {md5}") # 假设宽高为 640x480 y_size = 640 * 480 uv_size = 320 * 240 y_plane = data[:y_size] u_plane = data[y_size:y_size + uv_size] v_plane = data[y_size + uv_size:y_size + uv_size * 2] print(f"Y平面前16字节: {y_plane[:16].hex()}") print(f"U平面前16字节: {u_plane[:16].hex()}") print(f"V平面前16字节: {v_plane[:16].hex()}")

这段代码能直观看到Y、U、V三个平面的字节分布。同一份数据文件,你可以在代码里分别用I420命名的解析器和YU12命名的解析器去读,只要都是按"Y、U、V"顺序取的,结果必然一致。

5.2 在Android上用ImageReader实测YU12

Android端更贴近日常项目。使用Camera2的ImageReader时,指定ImageFormat.YU12,回调里拿到的Image对象就是标准的YUV420P。你可以在代码里直接取出三个平面:

Image image = reader.acquireLatestImage(); Image.Plane[] planes = image.getPlanes(); ByteBuffer yBuffer = planes[0].getBuffer(); ByteBuffer uBuffer = planes[1].getBuffer(); ByteBuffer vBuffer = planes[2].getBuffer(); // 注意这三个plane的rowStride和pixelStride可能不同 int yRowStride = planes[0].getRowStride(); int uRowStride = planes[1].getRowStride(); int uvPixelStride = planes[2].getPixelStride();

关键提醒:Android的YU12在不少设备上存在padding,不能直接按width连续读取。你需要使用getRowStride()拿到每一行实际的字节数,再逐行拷贝到紧凑数组中。这一步做不好,图像会呈现斜切错位或绿边现象,而且难以用肉眼直接判断格式问题。

5.3 借助libyuv进行格式间的显式转换

Google的libyuv库是一个非常好用的参考。它在内部定义了一组FourCC枚举,把FOURCC_I420FOURCC_YU12FOURCC_IYUV分别标记出来,而函数libyuv::CanonicalFourCC()会返回它们统一的规范格式——实际上都落到FOURCC_I420

#include "libyuv/convert.h" #include "libyuv/convert_argb.h" #include "libyuv/row.h" // 无论源标记为I420、YU12还是IYUV,这里都能正确转为ARGB libyuv::I420ToARGB( src_y, width, src_u, width / 2, src_v, width / 2, argb, width * 4, width, height);

所以在libyuv的视角里,这三种FourCC在转换为内核函数时,走的完全是同一个I420ToARGB路径。库设计者显然清楚它们是一个东西。

6. 比命名更重要的两个细节:stride对齐与宽高边界

6.1 stride不等于width的场景

这是整个I420话题里最容易被忽视的工程细节。很多初学者拿到的YUV420P数据,按width * height计算Y平面大小后直接跳转到U平面,结果画面出现左右错位、斜条纹。真正的原因通常是:每一行数据实际占用的字节数大于图像宽度,多出来的部分是硬件为了内存对齐而填充的无效字节。

这个"实际每行字节数"就是stride(行距)。在海思、瑞芯微等芯片平台以及Android的ImageReader中,stride普遍存在。典型值:

  • ARM平台:经常按16字节对齐
  • GPU回读数据:经常按64字节甚至256字节对齐
  • 部分VPU硬件:按宏块(16x16)边界对齐

处理方式不复杂,逐行拷贝即可:

for (int h = 0; h < height; h++) { memcpy(dst + h * width, src + h * y_stride, width); }

U、V平面同理,只是高度和宽度都减半,且各平台U/V平面的stride可能与Y平面不相同。写代码时务必分别获取,不要只拿Y平面的stride去套U、V平面。

6.2 非偶数宽高带来的问题

I420严格要求宽高为偶数。实际项目中,如果遇到1921x1080这种奇数宽度,直接用公式计算就会出错。因为U/V平面的宽是width/2,在C语言里整数除法会向下取整,导致U/V平面的像素数量计算结果和硬件实际采样不一致,从而引发色度通道偏移。

业界标准做法是先做对齐:

  • 宽度对齐到偶数:aligned_width = (width + 1) & ~1
  • 高度对齐到偶数:aligned_height = (height + 1) & ~1

然后再用对齐后的值计算平面大小。如果你手里的数据是奇数宽高的I420,要么找一份已对齐的数据源,要么在接收数据前让上游统一处理。

6.3 推荐的上手排查链路

结合我这些年处理YUV数据的经验,给出一套排查链路,遇到颜色异常或布局错乱时,照着走基本能定位:

  1. 确认源格式字符串,明确是I420/YU12/IYUV还是YV12。如果是后者,直接交换U/V处理。
  2. 打印Y、U、V三个平面的起始地址和总长度,和理论值比对。
  3. 检查stride。用第一行数据做连续比对,确认一行实际字节数。
  4. 对宽高做偶数校验,不对齐的数据源尽早拒绝。
  5. 用已知标准图做端到端测试,绕开真实视频源的颜色干扰。

在实际工作中,把这套流程固化成自动化脚本,每次接入新设备、新SDK、新格式源时自动跑一遍,能避免非常多看起来神秘实际全是布局问题的bug。

最后再分享一个小技巧:如果你手头没有现成的检测工具,可以临时生成一张纯红色测试图,直接编码成I420,然后用待验证的解析函数去解码。纯红色在YUV里对应的U、V值有明确特征,只要输出颜色不是红色,立刻就能判断是U/V顺序的问题还是stride的问题。这个小工具我几乎每个项目都会写一遍,比任何文档都可靠。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询