ARTICLE DETAIL

资讯详情

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

YUV格式全解析:从采样原理到内存排布与NV12/I420转换

YUV格式全解析:从采样原理到内存排布与NV12/I420转换 做视频和图像处理的人十有八九都被这一串名字折磨过YU12、YV12、NV12、NV21、YUY2、UYVY、YUYV、YVYU、AYUV、I420、IYUV、NV16。它们看起来像某个硬件平台上的配置密文其实说穿了就是同一件事——一张彩色图像在YUV颜色空间下用不同采样精度、不同存储顺序组合出来的十几套字节排布规矩。搞懂它们你就能在任何平台之间自由搬运视频帧数据不慌、不猜、不靠试错。这篇文章适合所有跟视频帧打过交道的人Android/iOS相机开发、FFmpeg滤镜处理、安防监控的协议对接、视频采集卡调试、GPU纹理上传甚至是想搞明白“为什么下载的yuv文件在播放器里颜色发绿”的初学者。看完之后你会得到一个彻底的底层视角看到一个格式名立刻知道它占多少内存、字节按什么顺序排列、怎么转成自己需要的格式。先给你一个总抓手任何YUV格式只需要回答三个问题就全明白了。第一色度采样是4:4:4、4:2:2还是4:2:0也就是U、V分量到底砍了多少信息。第二Y、U、V三块数据怎么摆放是各自一块连续内存还是Y单独一块、UV交错放在一起还是每个像素内Y和UV紧挨着打包。第三如果U和V分离存放谁在前谁在后。这三个问题搞清楚剩下的全是排列组合。1. YUV编码与采样逻辑先搞清楚底层原理1.1 为什么是YUV而不是RGBRGB是最直观的颜色表示方式每个像素用红、绿、蓝三个分量叠出颜色。但它在做视频存储和传输时有两个天生劣势一是三个分量的数据量完全一致没法修剪二是人眼对红绿蓝各自的敏感度是不一样的分别编码等于把宝贵的带宽浪费在了不重要的细节上。YUV的思路是把颜色信息拆成“亮度”和“色度”两部分。Y代表亮度也就是画面的明暗细节U和V代表色度记录颜色偏离灰色的方向。你可以这样理解Y通道是一张黑白底片U、V通道是一张低分辨率的着色图播放时把着色图叠到黑白底片上人眼看到的就是完整的彩色画面。这个拆法符合人眼的生理特性我们对亮度的分辨力远高于对颜色的分辨力所以可以在色度通道上大刀阔斧地砍数据视觉上几乎察觉不到。RGB到YUV的转换有标准公式BT.601里最常见的一组是Y 0.299R 0.587G 0.114B U 0.564(B - Y) -0.147R - 0.289G 0.436B V 0.713(R - Y) 0.615R - 0.515G - 0.100B注意一个概念细节严格来说模拟时代叫YUV数字化之后应该叫YCbCrY、Cb、Cr是量化后的数字分量。但工程圈子里“YUV”这个词已经被叫顺了FFmpeg、DirectShow、V4L2的接口里到处是YUV实际存的都是YCbCr数据。这篇文章里我们按行业习惯统称YUV心里知道它是数字分量就行。1.2 采样密度4:4:4、4:2:2、4:2:0到底省了多少采样密度的数字写法来自电视广播时代描述的是在一组像素内Y、U、V出现的频率。理解它最直接的方法是把像素排成格子看。第一个数字永远是4表示一行里的亮度采样基准。第二个数字表示水平方向色度采样的密度第三个数字表示第二行色度采样的密度。4:4:4的意思是每个像素都带完整的Y、U、V不砍任何信息一帧画面完全是“无损”的色彩表达。4:2:2的意思是第一行里每两个Y像素共享一对U、V第二行同样如此也就是说水平方向色度信息减半垂直方向不减。4:2:0的意思是第一行每两个Y像素共享一对U、V第二行完全不再存色度直接用第一行的U、V覆盖过来水平和垂直方向各减半整体色度数据只有4:4:4的四分之一。拿8位精度、宽W高H的一帧图像算一下4:4:4需要W×H×3字节4:2:2需要W×H×2字节4:2:0只需要W×H×3/2字节。所以现代视频编码H.264/H.265/VP9几乎全部使用4:2:0因为它在视觉损失极小的情况下直接省掉了一半以上的原始数据量。4:2:2多用于广播级制作和高端采集场景4:4:4则更多出现在电影特效合成、高质量色键抠像这类需要精确色彩边界的环节。这里有个值得记住的知识点4:2:0采样中色度不是随便从一个像素里取的标准规定了U、V对应的采样位置。MPEG-2及以后的编码标准普遍使用“水平居中、垂直居中”的采样相位而MPEG-1使用水平居左、垂直居中的相位。如果你在做高精度的格式转换这个差别会影响画面边缘的锐度但绝大多数场景下直接按像素块映射处理视觉上看不出区别。1.3 从模拟信号到数字存储CVBS信号与YUV的传承之所以先提CVBS复合视频广播信号是因为YUV的整套思路就是从那会儿传下来的。模拟时代CVBS信号把亮度、色度载波、同步信号以不同频率叠加到一根线里传输好处是只需要一根线缺点是亮色串扰严重画面容易出现“爬行”噪点。为了画质后来又出现了YUV分量信号用三根线分别传输亮度Y和两个色差信号Pb、Pr彻底解决串扰问题。到了数字时代这种“亮色分离、色度降采样”的理念被原封不动继承下来只是模拟波形变成了离散的字节流。我们今天讨论的YU12、NV12、YUY2本质上就是CVBS那个时代的工程师智慧落到现代数字存储和传输体系里的具体形态。理解这条传承线你就不会觉得这一堆格式是凭空发明出来折磨人的它们都有明确的历史和物理逻辑。2. YUV数据的内存排布三大流派2.1 planar平面模式各归各的planar模式中文叫平面模式特点是Y、U、V三个分量各自占据一整块连续内存互不交杂。以I420为例一帧图像的内存结构是最前面是W×H字节的Y平面紧跟着是(W/2)×(H/2)字节的U平面最后是同样大小的V平面。每个平面内部按行优先顺序从左到右、从上到下排列像素。planar模式的最大优点是逻辑清晰滤波器、缩放器、编码器可以独立访问某一个分量。比如说做边缘增强只需要操作Y平面U、V平面完全不用动这对计算效率非常友好。它的缺点是访问一个像素的完整YUV需要跳到三个不同位置访存局部性差一些另外如果U、V平面的顺序约定不同就派生出I420和YV12两个看起来很像但完全不能互通的格式——差异仅仅是U和V谁放在前面。2.2 semi-planar半平面模式UV成对出现semi-planar模式中文常叫半平面模式是硬件设备最喜欢的排布方式。它的结构是一块连续的Y平面后面跟着一块“UV交错”的数据区。所谓交错就是U和V字节一个一个交替排列U0、V0、U1、V1这样一直往下排。NV12是semi-planar里最典型的代表。为什么硬件喜欢它因为色度数据在绝大多数图像处理流程里总是成对出现的亮度转换、色度插值、颜色矫正都需要同时拿U和V交错排布让它们紧紧挨在一起访存连续硬件引擎处理起来非常顺手。代价是如果你只想单独取U数据就得隔一个字节跳一次软件处理时稍微绕一点。NV21和NV12的唯一区别就是交错区里U、V的先后顺序调换了一个是UV一个是VU这个差别后续会细说。2.3 packed打包模式像素内连续排列packed模式中文叫打包模式是把Y、U、V按固定顺序直接塞进每个像素的字节序列里。以YUYV为例内存里每4个字节组成一个“宏像素”顺序是Y0、U0、Y1、V0。这4个字节表达了两个像素第一个像素的Y是Y0第二个像素的Y是Y1它们共用U0和V0。下一个宏像素继续按相同规律排列。这种模式的好处是结构规整数据连续非常适合线缆传输和原始显示输出比如很多HDMI采集卡、USB摄像头默认输出的就是YUY2或UYVY。缺点是每个像素只有部分分量做缩放或旋转时必须先把UV数据拆出来重排算法上多一层开销。2.4 三大流派怎么选场景决定格式日常开发里你会明显感觉到不同场景对排布方式的偏好。软件解码器和解码框架倾向于输出I420/YUV420P这种planar格式因为解码后直接送滤镜或编码器最高效。硬件编解码器、摄像头和图形API则清一色偏好semi-planar比如NV12因为硬件流水线访问交错数据最顺畅。而采集卡、显示设备、传统视频接口更常见packed格式比如YUYV、UYVY因为数据可以逐像素连续送出去不需要额外拼接。这不是谁替代谁的问题而是各自适应了不同传输介质和硬件结构。理解这一点你在做格式转换选型的时候就会心里有数——如果你的数据最终要送进GPU纹理NV12往往是省事的选择如果目标是交给软件算法库做复杂处理I420通常更好操作。3. 逐个拆解12个常用格式的字节排列与典型应用3.1 4:2:0 planar三兄弟I420/YU12、IYUV、YV12先看4:2:0里的planar组它们是视频圈子里出现频率很高的一组也是最容易互相混淆的一组。I420和YU12其实是同一个东西只是命名体系不同。I420是国际通用的叫法在FFmpeg里对应AV_PIX_FMT_YUV420P在Video for Windows和DirectShow时代就已经是标准格式。YU12是国内安防行业和GB/T 28181国标体系下的叫法字节布局完全相同先是Y平面再是U平面最后是V平面。所以你在对接海康、大华的码流或者RTSP拉流时看到YU12完全可以按I420处理。IYUV也是I420的另一个名字主要出现在一些老驱动的FOURCC标识里内存布局一模一样纯粹是命名冗余。YV12则是一个容易踩坑的变体。它和I420/YU12的唯一区别是U、V平面的顺序颠倒先是Y平面再是V平面最后是U平面。很多Windows平台的播放器、OpenGL视频纹理库默认输出YV12而FFmpeg解码默认输出I420如果直接拿I420的解析逻辑去读YV12数据画面会变成蓝色和紫色混杂的“鬼图”。这三个格式的完整内存布局可以这样看假设一帧图像宽W高HU/V平面都是W/2宽、H/2高格式内存顺序典型使用场景I420 / YU12 / IYUVY平面 → U平面 → V平面FFmpeg解码输出、安防国标对接YV12Y平面 → V平面 → U平面Windows播放器、旧式图形驱动3.2 4:2:0 semi-planar两兄弟NV12和NV21NV12是当今硬件平台上的绝对主力。Android从Camera2时代开始大部分设备的预览数据回调就是NV21或YV12但MediaCodec编解码的输入输出默认强烈推荐NV12iOS的VideoToolbox和CoreVideo框架里CVPixelBuffer默认的YUV格式就是NV12Windows上的DXVA、Intel/AMD/NVIDIA的硬件编解码器普遍也都吃NV12。NV12的内存布局是一整块Y平面紧跟着一块UV交错的平面交错顺序是U、V交替。也就是说在Y平面结束之后的内存地址里第一个字节是U0第二个字节是V0第三个字节是U1第四个字节是V1依此类推。UV交错平面里的U、V数量各占Y平面的四分之一整个数据区长度是Y平面的二分之一。NV21和NV12长得几乎一样但交错区变成V、U交替即第一个字节是V0第二个字节是U0。这个顺序差异带来一个经典坑Android老版本Camera1的预览回调默认输出NV21而很多国产ROM和优化包里也可能给你NV12如果把NV21数据直接当NV12送给编码器画面倒不会花但颜色整体会变成“紫绿互换”——红变成绿绿变成红蓝基本不受影响。遇到这种颜色诡异的情况第一反应就应该是检查U、V顺序有没有调转。3.3 4:2:2家族YUY2/YUYV、UYVY、YVYU、NV164:2:2采样在消费级领域不算主流但在广播制作、桌面采集、部分工业相机里很常见。4:2:2家族里最常见的四个格式需要一个个捋清楚。YUY2和YUYV本质是同一个格式的两种叫法。YUY2是微软DirectShow和很多Windows视频采集SDK里的FOURCC名称YUYV是V4L2和FFmpeg更常用的叫法FFmpeg里对应AV_PIX_FMT_YUYV422。它们的内存布局是Y0、U0、Y1、V0循环每4字节表达两个像素。你在USB摄像头、视频采集卡设备上看到“YUY2输出”说白了就是这种每两个亮度共享一对色度的打包格式。UYVY是YUY2的“兄弟变体”字节序变成U0、Y0、V0、Y1。从语义上讲它依然是每4字节两个像素但色度字节排在了前面。UYVY在专业视频设备、SDI采集、不少HDMI采集盒里是标准输出格式因为它的字节排列对某些硬件总线更友好。YVYU则是把V放在U前面的打包格式Y0、V0、Y1、U0主要用于一些特定日本厂商的硬件和部分游戏机视频采集方案。NV16可以理解为NV12的4:2:2版本。它延续了semi-planar的思路先是一块完整的Y平面后面跟着UV交错的色度平面区别在于色度平面的宽度不再是W/2而是W高度仍然是H/2。也就是说垂直方向每两行共享一对UV但水平方向每个像素都有一对UV。NV16的应用主要集中在高清视频芯片、部分专业编解码硬件和高端摄像头里FFmpeg对应的格式是AV_PIX_FMT_NV16。如果你在视频处理流水线里看到用户说“NV16是NV12的亲戚”那就是这个意思——一样的管理思路更高的色度密度。3.4 4:4:4与AlphaAYUV的特殊身份AYUV是这一整串格式里最特殊的一个因为它不只包含YUV还带了一个Alpha透明通道并且色度采样是完整的4:4:4。每个像素固定占用4个字节一个字节给Alpha三个字节分别给Y、U、V。在DirectShow和部分Windows图形框架里AYUV的四字节顺序通常是A、Y、U、V但要注意不同库和驱动里的FOURCC实现可能不同有的版本是Y、U、V、A有的版本是V、U、Y、A。所以用AYUV之前先看你对接的SDK文档里怎么定义字节序不要拿一个固定顺序硬套所有接口。AYUV的场景多集中在字幕叠加、视频特效合成、高端调色和游戏录屏里因为它直接支持透明通道又能保留完整的颜色信息。普通视频处理项目遇到它的概率不高但一旦遇到它的内存计算方式最直观一帧大小永远是W×H×4字节没有任何减省。3.5 格式速查总表为了让你以后一眼能对照我把上述所有格式的关键信息收成一张表格式采样排布内存大小8bitW×H关键特点I420 / YU124:2:0planarW×H×1.5Y、U、V顺序IYUV4:2:0planarW×H×1.5同I420YV124:2:0planarW×H×1.5V在U前NV124:2:0semi-planarW×H×1.5Y平面UV交错NV214:2:0semi-planarW×H×1.5Y平面VU交错YUY2 / YUYV4:2:2packedW×H×2Y0 U0 Y1 V0UYVY4:2:2packedW×H×2U0 Y0 V0 Y1YVYU4:2:2packedW×H×2Y0 V0 Y1 U0NV164:2:2semi-planarW×H×2Y平面UV交错AYUV4:4:4packedW×H×4带Alpha通道4. 内存占用计算与格式转换实操4.1 一帧图像到底占多少字节很多人在写视频缓冲分配时凭感觉多开一块内存结果不是浪费就是越界。实际上每种格式一帧占用的字节数都有固定公式。YUV 4:2:0格式I420、YU12、YV12、NV12、NV21的统一公式是total_size W * H * 3 / 2其中Y平面占W×H字节U、V平面各占W×H/4字节。以1920×1080为例一帧NV12占1920×1080×1.53110400字节约2.97MB。30fps下每秒数据量为3110400×3093312000字节约89MB换算成网络传输速率就是约746Mbps。看到这个数字你就明白直播和视频通话为什么必须要做编码压缩了——30fps的原始YUV420数据流已经逼近千兆网线的极限。4:2:2格式YUY2、UYVY、YVYU、NV16的统一公式是total_size W * H * 24:4:4格式AYUV的统一公式是total_size W * H * 4这里要额外注意一个问题很多硬件平台的内存对齐策略。比如某些采集卡或GPU引擎要求每行数据对齐到16字节或64字节这时候一帧数据的总大小可能比理论公式略大。处理这类数据时宽度计算必须使用“对齐后的stride”而不是图像的真实宽度否则解析出来的画面会一行一行地斜切。4.2 NV12与I420互转代码级演示NV12和I420是实际开发中互相转换频率最高的一对因为解码器给I420、硬件编码器要NV12的情况太多了。转换的核心思想很简单NV12的颜色分量是交错存的I420是分开存的把交错的UV拆开、分别放进U平面和V平面即可。C语言版本直接处理内存bufferstatic void nv12_to_i420(const unsigned char *nv12, unsigned char *i420, int width, int height) { int y_size width * height; int uv_size y_size / 4; // U或V单个平面的大小 // Y平面直接复制 memcpy(i420, nv12, y_size); // 拆分UV交错区NV12中UV从y_size位置开始顺序是U、V交替 const unsigned char *uv_src nv12 y_size; unsigned char *u_dst i420 y_size; unsigned char *v_dst i420 y_size uv_size; for (int i 0; i uv_size; i) { u_dst[i] uv_src[i * 2]; // 取偶数位 - U v_dst[i] uv_src[i * 2 1]; // 取奇数位 - V } }反向转换I420转NV12也很直观static void i420_to_nv12(const unsigned char *i420, unsigned char *nv12, int width, int height) { int y_size width * height; int uv_size y_size / 4; memcpy(nv12, i420, y_size); const unsigned char *u_src i420 y_size; const unsigned char *v_src i420 y_size uv_size; unsigned char *uv_dst nv12 y_size; for (int i 0; i uv_size; i) { uv_dst[i * 2] u_src[i]; uv_dst[i * 2 1] v_src[i]; } }用Python和NumPy做同样的事会更适合快速验证import numpy as np def nv12_to_i420(nv12_buf, width, height): y_size width * height uv_size y_size // 4 y nv12_buf[:y_size] uv nv12_buf[y_size:y_size uv_size * 2] u uv[0::2] # 取0,2,4... 字节 v uv[1::2] # 取1,3,5... 字节 return y u.tobytes() v.tobytes()如果你不想写代码FFmpeg命令也可以直接做转换。有一个裸的YUV文件先用fplay确认格式再转成需要的排列# 查看NV12裸数据 ffplay -f rawvideo -pix_fmt nv12 -s 1920x1080 input_nv12.yuv # NV12转I420/YUV420P ffmpeg -f rawvideo -pix_fmt nv12 -s 1920x1080 -i input_nv12.yuv \ -f rawvideo -pix_fmt yuv420p output_i420.yuv这个命令在调采集卡驱动或验证解码器输出时特别实用省得每次写程序才能看画面。4.3 stride对齐最容易踩的暗坑如果你照着公式算出大小、写好转换、把数据送到播放器里发现画面像被“洗牌”一样斜着切开了十有八九是stride出了问题。stride指一帧图像中每行数据实际占用的字节数它经常不等于宽度乘像素字节数。举个例子一张宽1280像素、8位NV12的图像理论行字节数是1280但某些硬件平台会把每行对齐到256字节实际stride是1280或者1344、1408。Y平面的每一行在内存里都按这个更大的stride存放行尾多出来的字节是无效的填充数据。如果你仍然用1280作为行宽去读数据那么从第二行开始每个像素都会被前面的填充字节推偏画面呈现一层一层错开的斜纹。排查stride问题的方法很简单从驱动或解码器接口里拿到真正的stride值计算所有平面偏移时一律用stride而不是width。常见错误是把Y平面大小直接算成stride×heightUV平面的偏移则基于这个值计算。严谨的代码应当像下面这样int y_stride v4l2_fourcc_stride(width); // 例如从设备驱动获取 int y_size y_stride * height; int uv_stride y_stride / 2; int uv_size uv_stride * (height / 2);如果项目里拿不到stride信息还有一个省事办法直接输出一帧到文件里用十六进制编辑器看第二行起始位置是否和第一行紧密相接再反推对齐宽度。这个方法在调试采集设备时救了我很多次。5. 常见坑与排查实录5.1 颜色发绿、发紫、色彩怪异多半是U/V顺序问题视频处理里最经典的故障现象就是“颜色不对但轮廓清晰”。全画面偏绿偏紫说明Y分量是对的亮度结构完整但U、V通道被调换了。比如把YV12的数据当I420解析U、V互换后红和绿会整体反转画面呈现一种诡异的“紫绿感”。人脸尤其明显肤色会变成接近紫色或暗绿色背景的绿草则会变成偏红。排查逻辑很有规律先确认Y平面位置和大小是否正确这个没问题就检查色度平面的顺序和采样密度假设。如果你在Android上收的是NV21不小心按NV12处理现象也是红绿互换。遇到颜色异常时固定思路是“交换U、V再试一次”或者在代码里加一个开关允许运行时切换U/V顺序这在调试硬件驱动时非常高效。5.2 花屏、斜切、条带先查stride和平面偏移花屏和斜切是第二类高频故障和颜色问题的最大区别是画面结构已经坏了轮廓都是歪的。这种情况几乎全部指向平面偏移计算错误。Y平面大小算错、UV平面开始位置偏移、stride没对齐都会导致后一段数据整体移位。排查建议按这个顺序来第一步确认Y平面偏移是0这是最不可能错的第二步打印或者估算Y平面实际大小检查是否等于stride×height如果等于width×height而实际stride更大就是这里出问题第三步确认UV平面偏移是否落在Y平面结束后紧接的位置第四步跳过UV检查先只显示Y平面如果Y是黑白正常的问题就锁定在色度部分。我遇到过最隐蔽的一次是某个硬件编码器的UV平面偏移不是紧接Y平面而是Y平面结束后的第64字节处因为芯片在Y平面尾部塞了一段调试信息。面对这种非标硬件最好的办法就是先dump出来用ffplay不同格式反复试再结合十六进制数据找到真正的平面边界。5.3 各平台兼容性自查清单不同平台和框架的默认格式差异很大我把最常见的对应关系整理一下遇到格式疑惑直接查表平台 / 框架常见格式备注Android Camera1NV21 / YV12预览回调多为NV21Android MediaCodecNV12输入输出推荐格式iOS VideoToolboxNV12CVPixelBuffer默认FFmpeg软解码I420 / YUV420Plibx264输出也是它V4L2摄像头YUYV / NV12可协商切换DirectShow采集YUY2 / UYVYWindows采集常见安防GB28181YU12 / I420国内设备协议常用OpenCV视频接口BGR读入后自行转YUV最后提醒一件经常被忽略的事很多平台虽然写着支持多种格式实际驱动只对其中一种做了完整优化其他格式要么画质下降、要么延迟更高。在真机调试时不要只看驱动声明最好用fplay或自写工具实测几个帧确认颜色、对齐和帧率都达标后再在项目里固化格式。我在实际项目中总结出的习惯是所有视频格式相关代码都要留两个调试接口一个是“运行时指定输入格式”一个是“直接导出YUV裸文件”。前者让你在设备和协议变化时不用改代码后者让你能快速用ffplay定位是颜色问题、对齐问题还是采样假设错误。很多看起来玄学的视频花屏最后都被这两个接口五分钟内锁定原因。这一堆YUV格式说到底并不复杂核心就是不断追问三件事——采样砍了多少数据怎么摆UV谁在前只要每帧数据都按这三个维度去验证几乎所有格式问题都能以最直接的方式被拆穿。
返回列表