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

资讯详情

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

视频开发必知:YUV格式采样、内存排布与NV12/I420/YUY2转换实战

视频开发必知:YUV格式采样、内存排布与NV12/I420/YUY2转换实战 1. 先说清楚为什么做视频开发的都在聊YUV做视频采集、编码、显示相关的开发迟早要跟一串看起来跟乱码一样的字符串打交道YU12、NV12、YUY2、AYUV……我第一次接触这些名词是在调试USB摄像头预览画面的时候画面花成彩虹色排查了半天才发现是格式没对上。从那以后我就意识到搞懂这些YUV格式的命名、布局和内存排布是踩平视频开发这条路的第一步。这批格式看起来很多其实归纳下来就三类事情采样方式亮度通道和色度通道的比例、存储结构平面还是打包、通道排布顺序U和V谁先谁后Y在什么位置。搞清楚这三个维度标题里那十几个名词就能全部串联起来不用一个一个死记硬背。这篇文章就按我的理解把这些格式拆开讲配上内存排布图和换算逻辑顺便把我调试时踩过的坑一并交代出来。无论你是做Android相机开发、FFmpeg推流、音视频SDK集成还是嵌入式ISP调试这篇都值得花十分钟看完。1.1 YUV不是一种格式是一族格式先说基础概念。视频采集出来最原始的是RGB但RGB三个通道各存各的数据量大不说而且对压缩不友好。后来业界搞出YUV这套色彩空间把画面拆成一个亮度分量Y和两个色度分量U、V。这里的关键是人眼对亮度的变化比对颜色的变化更敏感。所以在存储和压缩的时候可以把色度信息减半甚至减到四分之一画质损失肉眼几乎看不出来数据量却大幅减少。这个减半策略就是所谓的色度采样chroma subsampling。最常见的采样格式有三种4:4:4——每个像素都保留完整的Y、U、V画质最好体积最大主要用在专业调色和计算机生成内容。4:2:2——每两个像素共享一对U和V亮度Y每像素都保留水平分辨率减半。4:2:0——每四个像素共享一对U和V也就是Y保留全部U和V在水平和垂直方向都减半这是视频存储和传输的绝对主力。标题里那一堆格式本质上就是不同采样比例、不同内存布局下的YUV具体落地方案。用生活化的话来说YUV是菜谱4:2:0是口味偏好少放点颜色NV12、I420这些都是这道菜的具体摆盘方式。2. 存储结构平面Planar、半平面Semi-Planar和打包Packed到底差在哪搞懂YUV格式第一步是分清三类存储结构。这决定了你在写代码时一个数据buffer的U、V分量到底在哪个偏移位置取。平面格式Planar把整个画面的Y存成一块连续内存U存成另一块V再存成一块三块互不相干。典型的代表是I420即IYUV和YV12。这种格式对编码器友好很多软件编码器直接吃这种布局。半平面格式Semi-PlanarY单独一块U和V交织在一起存成一块UV交替排列。代表是NV12和NV21。Android相机和大多数硬编解码器默认输出这种因为让U和V交错存储访存带宽更友好。打包格式Packed每个像素的Y、U、V顺序交织在同一个内存块里类似于RGB888的排布方式。YUY2、UYVY、YVYU、AYUV都属于这一类。这类格式不需要复杂的偏移计算逐个像素读取即可适合采集设备和显示链路直接对接。区分这三类看名字也有窍门。名字里带数字的一般是平面或半平面NV12、NV21、YU12名字里两个字节成对出现的多半是打包格式YUY2、UYVY就是四个字母代表两个像素的YUV数据块。后面会逐个拆解。2.1 平面格式I420、YV12、YU12的差异全在UV顺序上I420和YV12可能是最容易被混淆的两个。它们的内存布局在尺寸上完全相同先一整块Y然后一整块U最后一整块V。唯一的区别是U块和V块的先后顺序。I420Y U V也叫IYUV两者完全等价只是叫法不同YV12Y V U就这么点差别写代码时搞反了画面就会整体偏色或者发紫。我曾经在某个播放器项目里把解码出来的I420数据当成YV12送去渲染整个画面变成诡异的青紫色调排查了整整一个下午最后是用十六进制dumping数据比对才发现的。YU12这个名字其实也是I420的另一个说法主要出现在一些Linux V4L2框架和部分硬件平台的驱动文档里。简单记YU12、I420、IYUV是同一个东西的三种叫法布局都是Y后接U再接V。以一张1920x1080的I420画面来计算内存排布Y平面大小 1920 x 1080 2073600字节U平面大小 1920/2 x 1080/2 518400字节V平面大小 518400字节总大小 3110400字节所以Y的偏移量是0U的偏移量是2073600V的偏移量是2073600 518400 2592000。但凡你要做格式转换或者OpenGL纹理上传这些偏移值就是硬规矩记错了就是花屏。2.2 半平面格式NV12与NV21Android开发者的老朋友NV12和NV21是Android平台出场率最高的YUV格式Camera2默认输出、MediaCodec硬件编码器的输入输出、SurfaceTexture的getBuffer都是这类。它们的布局逻辑是Y平面单独连续存储大小和I420的Y平面一样。紧接着的那块内存存放的是交织排列的UV数据总大小同样是1920/2 x 1080/2 x 2字节。差别在于UV交织的先后顺序NV12按U、V、U、V的顺序循环排列NV21按V、U、V、U的顺序循环排列很多人刚上手做Android相机开发时习惯直接拿字节数组的NV21去做人脸检测因为老版本的FaceDetector只认NV21后来换到Camera2预览就会踩到NV12的坑。我建议所有做移动端视频的开发者动手写代码前先把这两个格式的内存排布图在纸上画一遍尤其是UV交织的细节。值得一提的是NV16就是NV12的4:2:2版本Y平面大小和画面完全一样不缩小UV数据按行存储每行U和V的样本数跟Y一样多只是垂直方向减半。整体比NV12多出三分之一的UV数据用在广播级视频采集和部分专业相机上。3. 打包格式逐个拆解YUY2、UYVY、YVYU与AYUV打包格式Packed的逻辑比平面格式直观得多它们把Y、U、V按固定顺序塞进连续字节流。这类格式通常不单独存平面而是直接在内存里交织。拿4:2:2采样来说每两个像素称为一个“宏像素”macropixel占4个字节。这4个字节里包含两个Y值和一个U值、一个V值。3.1 YUY2、UYVY、YVYU的字节排布对比YUY2是很多USB摄像头和采集卡的默认输出格式也经常写作YUYV同一个东西YUY2是微软DirectShow体系的名字YUYV是V4L2的写法。它的4字节排布是Y0、U0、Y1、V0。也就是两个像素共享一对U和V。用像素序号表示像素0用Y0和U0、V0像素1用Y1和U0、V0。等于说每两个像素有4字节平均每像素2字节数据量正好比RGB565多一点。UYVY的排布则是U0、Y0、V0、Y1。U和V比Y提前一格。很多专业采集卡更喜欢UYVY因为U先落地硬件处理时可以先把色度信息提取出来做色彩校正和去隔行时访存更顺。YVYU的排布是Y0、V0、Y1、U0。相当于把YUY2的U和V位置互换。这个格式不太常见但我在某些外置采集盒上见过做兼容测试时也得认识。这三种格式的排列规律其实很统一4字节一个块两个Y固定不变U和V的位置决定了格式名。Y在前的叫YUY2/YVYUU在前的叫UYVY。3.2 AYUV4:4:4全精度打包格式AYUV是打包格式里的“高富帅”。它不做任何色度缩减每个像素占据4个字节按A、Y、U、V的顺序排列——A是Alpha透明通道Y、U、V全量保留。虽然数据量非常大同样1080p要1920x1080x4字节约8.3MB一帧但胜在色度信息零损失。现在不少高端视频编辑软件、游戏录屏和视觉特效管线里会用到AYUV因为后期调色、抠像、合成需要一个不丢色彩信息的中间格式。Windows的GDI和DXVA2接口里也常见AYUV写DirectShow滤镜的兄弟应该不陌生。它和RGB32的转换逻辑也最直接基本就是矩阵运算加通道重排CPU开销可控。4. 实操如何判断当前数据流是什么格式常见坑位在哪里讲完理论上实操。做视频开发最头疼的问题是手上有一堆字节流但不知道是什么格式。我从实际项目里总结出几条判断思路。4.1 判断格式的三种方法从软到硬第一种看源头设备的格式声明。V4L2采集卡看v4l2-ctl --list-formats-ext的输出Android Camera2的ImageReader拿Image.getPlanes()DirectShow用AM_MEDIA_TYPE的subtype。这些接口都会明确告诉你格式名犯错的概率最小但前提是设备驱动靠谱别交叉编译时把格式值搞串了。第二种看数据特征。拿到一个原始YUV buffer先检查Y平面大小如果Y平面大小刚好等于宽乘高再往后数几字节看U和V是分块存还是交织存——分块是平面格式交织是半平面。再用十六进制工具看U和V的顺序很快就能锁定具体格式。这个方法在逆向别人SDK的demo时特别好用。第三种直接转换试错。写一个小工具把buffer按不同格式硬解转成RGB或者直接存成YUV图片用看图软件比如YUVView、7yuv打开看颜色和纹理对不对。画面颜色正确就是格式对了偏紫偏绿多半是UV顺序或排列反了。4.2 我在实际项目中踩过的三个坑第一个坑分辨率对齐问题。YUV420系列格式对宽高都有偶数的软性要求。如果你画面宽度是801像素U、V平面根本排不齐需要padding。某个项目里CIF分辨率352x288还好一旦遇到960x540这种奇数宽度的输入源忘了做对齐就会出鬼影。第二个坑行对齐stride问题。很多硬件的Y平面并非紧贴相邻而是有行对齐stride要求16字节或64字节对齐在NV12转I420时如果直接用宽乘高去算偏移出来的画面右半边会斜切。Android的Image.getPlanes()[0].getRowStride()拿到的值往往不等于width就是这个原因。第三个坑大小端和补位字节。部分格式特别是打包格式会有额外的padding位或Alpha通道占位比如一些隐藏内存格式长度是4字节的倍数但实际只用了3字节。拷贝buffer时用了memcpy(des, src, width*height*2)这种想当然的长度就会把数据截掉或者多拷贝出垃圾字节最终导致画面末尾出现彩条。碰到这些问题建议你手头常备YUV分析工具YUVViewMac/Windows都有、7yuvWindows老牌工具以及ffmpeg的ffplay配合rawvideodemuxer做快速预览。这几个工具在我平时验证格式时起了大作用。5. 格式转换与内存计算拿走直接用的方案格式转换是绕不开的日常操作。NV12转I420、I420转YUY2项目里每周都要写好几次。背后的核心逻辑就是把缓冲区拆成Y、U、V三块重新组合。5.1 通用转换思路与一份NV12转I420示例代码先给一份C语言实现把最常见场景NV12互转I420讲透其他格式类推即可。// src: 输入NV12数据dst: 输出I420数据width/height: 画面宽高 void nv12_to_i420(unsigned char* src, unsigned char* dst, int width, int height) { int y_size width * height; int uv_size y_size / 4; // U或V各自大小 // 1. Y平面直接拷贝 memcpy(dst, src, y_size); // 2. 拆分NV12中交织的UV为分离的U、V平面 unsigned char* src_uv src y_size; unsigned char* dst_u dst y_size; unsigned char* dst_v dst y_size uv_size; for (int i 0; i uv_size; i) { dst_u[i] src_uv[i * 2]; // NV12顺序U、V dst_v[i] src_uv[i * 2 1]; // 如果是NV21把这两个赋值对调 } }这个简单循环处理1080p一帧大概耗时0.2到0.5毫秒取决于CPU和编译器优化性能完全够用。如果要踩进SIMD优化或NEON优化维度直接把循环做成4字节一组批量处理即可不过普通APP场景直接memcpy 循环完全没问题。同理I420转NV12就是逆操作先把Y拷贝过去再把U、V交织写入。YUY2转NV12则要先从打包数据里解出Y、U、V分量再做平面化组合。5.2 1080p帧内存占用速查表方便你写代码时快速预估buffer大小我把常见格式1920x1080一帧所需内存整理如下格式名采样存储结构每像素字节数1080p一帧大小字节I420 / IYUV / YU124:2:0平面YUV1.53,110,400YV124:2:0平面YVU1.53,110,400NV124:2:0半平面YUV交织1.53,110,400NV214:2:0半平面YVU交织1.53,110,400NV164:2:2半平面YUV交织2.04,147,200YUY2 / YUYV4:2:2打包Y0 U Y1 V2.04,147,200UYVY4:2:2打包U Y0 V Y12.04,147,200YVYU4:2:2打包Y0 V Y1 U2.04,147,200AYUV4:4:4打包A Y U V4.08,294,400同一分辨率下420采样格式最省内存4:2:2多三分之一4:4:4多一倍多。做内存紧张的嵌入式开发时优先选NV12或I420做高质量采集和剪辑场景再考虑4:2:2。5.3 配合FFmpeg快速看格式的小技巧日常调试时我经常用FFmpeg来做格式验证一条命令就能把裸数据按指定格式读出来并转成PNG预览# 把NV12裸流显示为画面 ffplay -f rawvideo -pixel_format nv12 -video_size 1920x1080 input.raw # 把YUV数据批量转成JPEG预览 ffmpeg -f rawvideo -pixel_format yuyv422 -s 1920x1080 -i input.raw output_%d.jpg这套命令最大的价值在于你可以立刻验证手里的裸流格式名是否正确。我曾经接到过一个客户SDK的raw数据代码里写着NV12我用-pixel_format nv12打开花屏换成-pixel_format nv21就正常了省得再去写测试工程验证。6. 相关概念扫盲CVBS信号与YUV格式信号的边界看到标题里的备用热词提到了“CVBS信号和YUV格式信号”顺便说两句。CVBS是复合视频广播信号Composite Video Blanking and Sync它是模拟时代的技术把亮度、色度、同步信号调制到同一根线缆上传输。而YUV格式信号是数字领域的描述指的是YCbCr数字分量信号本身。两者最核心的区别CVBS是模拟复合信号YUV是数字分量信号。CVBS需要解码器分离亮度和色度画质损耗大YUV从源头就走分量独立传输不存在串扰问题。现代视频系统里CVBS基本被数字接口取代只偶尔出现在老旧的监控系统、车载倒车影像和部分工业设备里。做嵌入式开发时如果遇到CVBS转YUV的解码器芯片如TVP5150、SAA7113调试要点是先把同步信号搞对再谈色彩空间转换——同步不稳会导致整幅画面撕裂或滚动跟色彩格式无关别搞混排查方向。7. 写在最后的实操建议7.1 我建议你记住的记忆锚点接触过的格式越来越多之后我个人是靠三个锚点来快速定位格式类型名字里有“AV”或“UV”涉及交织的优先怀疑是半平面格式NV12、NV21交织顺序看第二个字母U在前就是NV12V在前就是NV21。名字四个字母且两个Y连在一起出现的优先怀疑是打包格式YUY2、UYVY、YVYU具体看Y在四个字节里的位置。四个字母但每三个前缀里没有连续Y的大概率是平面格式I420、YV12看第三第四字母的顺序U在前是I420V在前是YV12。7.2 从项目初期的格式选型说起如果让我给项目选型提一句建议Android和主流硬件平台无脑选NV12兼容性和性能都最好软件编码器和FFmpeg相关的用I420工具链支持最完善采集卡和图像采集用YUY2或UYVY硬件直接输出不用转后期合成才考虑AYUV。格式选型其实会影响后续一系列决策。比如做实时美颜切换NV12到RGB做特效再回NV12中间要经过两次转换对耗时和显存都很敏感直接在YUV域做处理可以省掉一半内存拷贝但算法复杂度高。这种取舍最终都得落到格式本身的特性上。这行做得越久越发现基础知识点往往决定了最终的调试效率。今天把这些YUV格式的差别理清楚下次碰到花屏、偏色、绿条纹的时候至少能快速定位到是采样方式、存储结构还是通道顺序出了问题。希望这篇整理能帮你在视频开发的路上少走几个弯路。
返回列表