十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

YU12/I420/IYUV命名纠葛:YUV420P内存布局与实战解析

YU12/I420/IYUV命名纠葛:YUV420P内存布局与实战解析 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这个基础概念。先说人话版本人眼对亮度LumaY的敏感度远高于对颜色ChromaU/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 YU12Android与国产平台的习惯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_IYUV和MEDIASUBTYPE_I420就经常交替出现且很多实现内部完全一致。VLC、ffplay这类播放器在识别FourCC编码时也会把I420、IYUV、YU12统一映射到同一种像素格式。四字符码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.yuvFFmpeg的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(fMD5: {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(fY平面前16字节: {y_plane[:16].hex()}) print(fU平面前16字节: {u_plane[:16].hex()}) print(fV平面前16字节: {v_plane[:16].hex()})这段代码能直观看到Y、U、V三个平面的字节分布。同一份数据文件你可以在代码里分别用I420命名的解析器和YU12命名的解析器去读只要都是按Y、U、V顺序取的结果必然一致。5.2 在Android上用ImageReader实测YU12Android端更贴近日常项目。使用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_I420、FOURCC_YU12、FOURCC_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数据的经验给出一套排查链路遇到颜色异常或布局错乱时照着走基本能定位确认源格式字符串明确是I420/YU12/IYUV还是YV12。如果是后者直接交换U/V处理。打印Y、U、V三个平面的起始地址和总长度和理论值比对。检查stride。用第一行数据做连续比对确认一行实际字节数。对宽高做偶数校验不对齐的数据源尽早拒绝。用已知标准图做端到端测试绕开真实视频源的颜色干扰。在实际工作中把这套流程固化成自动化脚本每次接入新设备、新SDK、新格式源时自动跑一遍能避免非常多看起来神秘实际全是布局问题的bug。最后再分享一个小技巧如果你手头没有现成的检测工具可以临时生成一张纯红色测试图直接编码成I420然后用待验证的解析函数去解码。纯红色在YUV里对应的U、V值有明确特征只要输出颜色不是红色立刻就能判断是U/V顺序的问题还是stride的问题。这个小工具我几乎每个项目都会写一遍比任何文档都可靠。
返回列表