资讯动态

014、YUV 格式详解与内存布局

发布时间:2026/10/27 22:30:41 来源:尧图企业网站定制
014、YUV 格式详解与内存布局写这篇东西的起因是上个月调一个平台camera preview图像整个左半屏是绿的右半屏是花的像被狗啃过。logcat里没有报错ISP输出也正常最后定位到是APP那边拿到的buffer格式是NV21而HAL层给的是NV12颜色分量错位导致整个画面崩坏。这种问题在刚开始做camera HAL的人身上几乎必出现因为YUV格式看着简单实际内存布局一错就是灾难。今天就顺着这个坑把YUV格式和内存布局彻底捋一遍。先明确一件事YUV不是一种格式是一族格式。Y是亮度U和V是色度人眼对亮度敏感对色度不敏感所以可以压缩色度采样来省带宽。常见的采样方式有4:4:4、4:2:2、4:2:0数值含义是每4个像素水平方向上Y、U、V的样本数。4:4:4就是每个像素都有完整的YUV4:2:2是每2个像素共享一对UV4:2:0是每4个像素2x2共享一对UV。手机camera几乎全是4:2:0因为人眼根本分辨不出那点色度损失。但同样是4:2:0内存里怎么摆各家方案完全不同。先分清三个概念planar平面、semi-planar半平面、packed打包。planar是Y一块、U一块、V一块三块独立存储。semi-planar是Y一块然后UV交错在同一块里NV12和NV21就是这种。packed是每个像素或每对像素的Y和UV按固定顺序打包在一起像YUYV、UYVY这种在USB camera和HDMI接收里常见sensor输出很少用了。重点说Android camera里最常碰到的四个格式I420也叫YUV420P、YV12、NV12、NV21。I420和YV12都是planarY平面在前后面跟U、V平面区别是顺序。I420是Y U VYV12是Y V U就这一个差别。NV12和NV21是semi-planar前一半是Y平面后一半是UV交错NV12是UV顺序NV21是VU顺序。很多SoC的ISP硬件只支持其中一种例如高通平台常用NV12而老一点的视频编码器偏爱NV21所以HAL层经常要做格式转换。内存布局的细节才是最坑人的地方尤其stride和alignment。很多新手只算分辨率宽高然后直接拿widthheight3/2去申请内存结果在分辨率不是16或32对齐的时候图像会斜着裂开因为实际的每行字节数不等于width。camera硬件里每一行Y或者UV都有对齐要求比如8、16、32、64字节对齐取决于ISP的AXI总线位宽和RAM burst大小。所以分配buffer时必须用stride也叫pitch而不是width。stride是硬件实际读取每行数据的字节数width是有效像素数。如果img_width1920而stride1920很幸运但如果width1080一行Y数据要1080字节对齐到16后是1088字节那实际Y平面大小就是strideheight而不是widthheight。我之前调一个1080p的camera一直觉得108019203/2算出来的size用着没事直到换了一颗sensor输出分辨率是1920x1080其实没错但ISP配置成了1920x1080stride也是1920所以没踩坑。可如果改成1280x720720不是16的倍数吗720/1645刚好对齐也没事。但分辨率变成640x480480也是16的倍数。很多常见VGA分辨率恰好对齐导致大家忽视了stride。真正出问题的是像1920x1080这种高度1080不是16的倍数其实1080/1667.5所以Y平面每行1920字节对齐没问题但UV平面呢UV是半分辨率UV平面的width是960height是540。960是16的倍数但540不是所以UV平面每行字节数960高度540但540/1633.75。有些硬件要求UV平面也是高度对齐比如整体size要align到16那分配大小就要按stridealign(height,16) stride/22*align(height/2,16)这个计算很容易错。写段伪代码说明一下比如摄像头输出NV12分辨率WxH硬件stride为align(W, 16)则Y平面size stride * align(H, 2)UV平面size stride * align(H/2, 2)不对NV12是UV交错UV平面每行有stride个字节但是UV样本数是Y的一半所以每行字节数等于stride因为U和V各占一个字节交错后每对UV占2字节但宽度是W/2所以最自然的做法是UV平面的stride和Y平面一样都是align(W,16)因为硬件通常按固定行宽访问。所以UV平面的总大小是stride * align(H/2, 2)而不是stride/2 * 2 * … 总之不能直接WH3/2。再看看实际各个平台的差异。RK的ISP驱动里你从V4L2拿到的buf.index对应的planes长度可能和你想的不一样用VIDIOC_QUERYBUF会返回bytesperline和sizeimage那个bytesperline就是stridesizeimage已经包含所有plane的对齐。三星平台的fimc和exynos ISP驱动里s_output结构有四个plane的offset信息。我记得有一次从驱动拿到的offset[0]0, offset[1]sizeY, offset[2]sizeUV然后sizeUV不等于sizeY/2因为UV plane里还填充了CSC系数或padding其实是为了cacheline对齐。所以最可靠的办法永远是问驱动要offset和stride别自己算。接口层面Android camera HAL3的StreamBuffer里有个stride字段这玩意经常被人忽略。在processCaptureRequest里你要填的是buffer_handle_t而且stream配置时用的是android_rect的size但HAL实现里必须检查stream-stride。很多HAL直接用width来写bayer到YUV转换的寄存器结果在某个特定分辨率下画面错位。老经验是所有涉及内存拷贝的地方一律用stride做行宽所有分配的地方一律用stride*align(height)算。再说一下packed格式YUYV也叫YUY2是4:2:2打包内存里每4字节表示两个像素Y0 U Y1 V。这个格式在UVC camera里很常见很多模拟摄像头也输出这个。MJPEG解码后经常得到YUYV然后转成NV12给编码器。YUV422还有一个变种是UYVY顺序不同。在调试UVC camera时如果你拿到的帧是YUYV但当成NV12处理图像会完全没有颜色或者颜色像被泼了一桶颜料。因为NV12的UV平面在Y平面后面而YUYV是交错在Y里解析位置全错。除了常见的4:2:0和4:2:2还有YUV444就是每个像素独立YUV没有色度压缩在HDMI和某些专业相机输出里用到。内存布局上YUV444基本都是planar或semi-planarsize是WH3。还有YUV400就是灰度图只有Y平面也常用于一些特效或单色模式。另一个关键概念是color rangeYUV的取值范围有两种full range0-255和limited range16-235。在camera HAL里如果ISP输出是full range但编码器默认limited range颜色会发白或者发灰。这个和内存布局无关但是调试排错时经常和格式混在一起容易误判。对了还有color matrix比如BT.601和BT.709YUV转RGB的系数不同。如果你把BT.709的YUV数据硬套BT.601转RGB整个画面会偏绿——注意又是绿色。所以看到绿色偏色时先别急着质疑格式也要想想是不是矩阵配错了。调试YUV问题我的建议是先在PC上用工具直接dump原始数据看。比如抓一个NV12帧存成文件用Python或者ImageJ打开把宽高设置对Y平面应该是一张灰度图如果看起来像是被切碎的画面那是stride没对上如果看起来是正常的灰度图但颜色诡异那是UV plane的顺序或offset错了。还可以用一个灰度渐变测试图Y值从0到255UV值设成128中性色如果图像有彩色出现说明UV数据没对齐或stride错了。这一步能快速缩小范围。实际工程中还有个常见坑buffer cache属性。有些平台DMA buffer是write-combine的CPU读起来性能很低而且有cache一致性问题。在HAL里做格式转换时如果直接memcpy整个buffer图像会出条纹。需要用ion或dmabuf的cache maintain操作或者用NEON优化时注意cache line。我用过某个平台需要调用ion_sync_cpu_device每次转换前做同步否则下一次抓帧永远是上一帧或者一半新一半旧。讲一个我记忆深刻的调试经历。某项目前置摄像头输出NV12预览正常但拍照出来是“下移”的——图像上半部分是绿色条纹下半部分正常且整体向上偏移。后来发现是因为拍照时走的是另一条ISP通路那边配置的stride是主预览通路的两倍因为做了水平binning不对是sensor crop后width变了但stride没更新。HAL里的buffer大小是按预览通路的stride分配的拍照通路往同一个buffer里写数据时每行多写了几个字节导致行错位。最后在拍照通路设置stride为实际width才对。这个案例告诉我同一个buffer在不同通路间共享时必须保证所有通路对stride的定义一致否则宁可多分配一个buffer。还有一次是NV12转I420因为编码器要求I420转换函数里把UV复制顺序搞反了。NV12的UV是交错I420是分离转换时要先取U再取V我写成先取V再取U结果图像偏紫。后来代码注释里写了“这里踩过坑V和U顺序千万别搞反可以用色卡验证”。关于内存对齐最后给个实用的公式。设width和height要求stride对齐到a通常16或32那么y_stride align(width, a)uv_stride y_stride // semi-planar中UV平面行宽等于Y行宽因为UV交错y_size y_stride * align(height, 2) // 有些平台要求高度对齐到2其实4:2:0本来高度就是偶数uv_size uv_stride * align(height / 2, 2) // 注意height/2可能不是偶数对齐到2total_size y_size uv_size但这里的align(height,2)不是必须看硬件要求。很多ISP内部按64字节突发如果y_stride不是64的倍数会多读一些数据但内存分配够大就没关系。所以保险起见总大小可以多算一些比如再加上64字节的余量。我在HAL里经常直接分配total_size 64反正内存浪费几十字节不算什么但避免万一硬件多读几个cacheline导致越界。写C代码的时候访问像素位置用宏#define Y(pixel_y, pixel_x) (y_base[(pixel_y)*y_stride (pixel_x)])#define UV(pixel_y, pixel_x) (uv_base[((pixel_y)/2)*uv_stride ((pixel_x)/2)*2])注意pixel_y和pixel_x是像素坐标UV的坐标要除以2。如果直接用y坐标去访问uv平面那必然会踩到别的地方。还有一个小细节Y平面的末尾和UV平面的开头之间可能有一个padding区域有些硬件的offset[1]可能不是y_stride*y_height而是再对齐到某个边界。所以访问UV平面一定要用驱动提供的offset不能假设紧跟Y平面。有些驱动在Y平面末尾会放一些元数据比如3A统计信息如果你把那个区域当成UV去拷贝整个颜色就乱了。在Android这边你从CameraDevice获取的buffer是Gralloc分配的它的格式信息在buffer_handle_t的description或者gralloc的usage里。HAL层在configure_streams时stream-format可能是HAL_PIXEL_FORMAT_YCrCb_420_SP或HAL_PIXEL_FORMAT_YV12注意Android的YCrCb_420_SP其实是NV21就是U和V颠倒的NV12。而HAL_PIXEL_FORMAT_YCbCr_420_SP是NV12。这两个宏极易混淆我当年就在这个地方栽过。在HAL里处理IMPLEMENTATION_DEFINED时要根据platform拿到的实际格式来映射不能想当然。最后建议做camera HAL的兄弟手里必备一个YUV格式分析工具或者自己写个小程序把raw buffer存成文件后用十六进制编辑器看前几行。Y平面上一个渐变的图像在hex里能看出来Y值递增UV平面如果全是0x80128说明是中性色如果有其他值说明色度信号正常。再不行就生成一个标准彩条测试图各条色块的Y、U、V理论值已知对照数据即可知道格式和stride。我电脑上一直放着一个彩条.bin文件每次怀疑格式问题时先输出一帧用看门狗其实是imx看几秒问题就暴露了。以后遇到画面花、绿、斜、错位先问自己stride对吗plane offset对吗UV顺序对吗color range对吗四个问题问完80%的YUV毛病都能解决。剩下20%可能是硬件坏了那就换板子吧。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑