前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >安卓Camera2 YUV_420_888

安卓Camera2 YUV_420_888

作者头像
ppchao
修改2020-12-01 15:43:39
2.3K0
修改2020-12-01 15:43:39
举报
文章被收录于专栏:写个呆萌写个呆萌

简介

Image类在API 19中引入,但真正开始发挥作用还是在API 21引入CameraDevice和MediaCodec的增强后。API 21引入了Camera2,deprecated掉了Camera,确立Image作为相机得到的原始帧数据的载体;硬件编解码的MediaCodec类加入了对Image和Image的封装ImageReader的全面支持。可以预见,Image将会用来统一Android内部混乱的中间图片数据(这里中间图片数据指如各式YUV格式数据,在处理过程中产生和销毁)管理。

本文主要介绍YUV_420_888格式的图片数据如何在Image中存储和管理。

从YUV420谈起

YUV即通过Y、U和V三个分量表示颜色空间,其中Y表示亮度,U和V表示色度。不同于RGB中每个像素点都有独立的R、G和B三个颜色分量值,YUV根据U和V采样数目的不同,分为如YUV444、YUV422和YUV420等,而YUV420表示的就是每个像素点有一个独立的亮度表示,即Y分量;而色度,即U和V分量则由每4个像素点共享一个。举例来说,对于4x4的图片,在YUV420下,有16个Y值,4个U值和4个V值。

YUV420根据颜色数据的存储顺序不同,又分为了多种不同的格式,如YUV420Planar、YUV420PackedPlanar、YUV420SemiPlanar和YUV420PackedSemiPlanar,这些格式实际存储的信息还是完全一致的。举例来说,对于4x4的图片,在YUV420下,任何格式都有16个Y值,4个U值和4个V值,不同格式只是Y、U和V的排列顺序变化。I420(YUV420Planar的一种)则为YYYYYYYYYYYYYYYYUUUUVVVV,NV21(YUV420SemiPlanar)则为YYYYYYYYYYYYYYYYUVUVUVUV。也就是说,YUV420是一类格式的集合,YUV420并不能完全确定颜色数据的存储顺序。

Image

这么多眼花缭乱的格式名字自然是不利于程序开发的,Image就这样横空出世了。

官网文档介绍

Android PAI 对 YUV420_888的介绍 ,大致意思如下:

它是YCbCr的泛化格式,能够表示任何4:2:0的平面和半平面格式,每个分量用8 bits 表示。带有这种格式的图像使用3个独立的Buffer表示,每一个Buffer表示一个颜色平面(Plane),除了Buffer外,它还提供rowStride、pixelStride来描述对应的Plane。 使用Image的getPlanes()获取plane数组: Image.Plane[] planes = image.getPlanes(); 它保证planes[0] 总是Y ,planes[1] 总是U(Cb), planes[2]总是V(Cr)。并保证Y-Plane永远不会和U/V交叉(yPlane.getPixelStride()总是返回 1 )。U/V-Plane总是有相同的rowStridepixelStride()(即有:uPlane.getRowStride() == vPlane.getRowStride() 和 uPlane.getPixelStride() == vPlane.getPixelStride();)。

U/V的平(Planar)面和半平面(Semi-Planar)

U/V的Planar存储(YUV420P)

我测试不同安卓设备,找到存储格式是Planar的设备:

代码语言:java
复制
               Log.i(TAG,"image format: " +image.getFormat());
                // 从image里获取三个plane
                Image.Plane[] planes = image.getPlanes();

                for (int i = 0; i < planes.length; i++) {
                    ByteBuffer iBuffer = planes[i].getBuffer();
                    int iSize = iBuffer.remaining();
                    Log.i(TAG, "pixelStride  " + planes[i].getPixelStride());
                    Log.i(TAG, "rowStride   " + planes[i].getRowStride());
                    Log.i(TAG, "width  " + image.getWidth());
                    Log.i(TAG, "height  " + image.getHeight());
                    Log.i(TAG, "Finished reading data from plane  " + i);
                    }
                getPixelStride() 获取行内连续两个颜色值之间的距离(步长)。
                getRowStride() 获取行间像素之间的距离。

输出如下:

代码语言:java
复制
image format: 35
pixelStride 1
rowStride 1920
width 1920
height 1080
buffer size 2073600
Finished reading data from plane 0
pixelStride 1
rowStride 960
width 1920
height 1080
buffer size 518400
Finished reading data from plane 1
pixelStride 1
rowStride 960
width 1920
height 1080
buffer size 518400
Finished reading data from plane 2

在ImageFormat中,YUV_420_888格式的数值是35,如上所示,可知当前Preview格式是YUV_420_888,根据image的分辨率是 1920 x 1080 ,像素点个数是2073600 。下面分别对plane[0]、plane[1]、plane[2]作分析。

  • plane[0]表示Y,rowStride是1920 ,其pixelStride是1 ,说明Y存储时中间无间隔,每行1920个像素全是Y值,buffer size 是 plane[0]的1/4 ,buffer size / rowStride= 1080可知Y有1080行。
  • plane[1]表示U,rowStride是960 ,其pixelStride也是1,说明连续的U之间没有间隔,每行只存储了960个数据,buffer size 是 plane[0]的1/4 ,buffer size / rowStride = 540 可知U有540行,对于U来说横纵都是1/2采样。
  • pane[2]和plane[1]相同。、

此时,YUV三个量分离,每一块数据单独存储在独立的plane里。此时的YUV420叫做YUV420P或I420,以分辨率8 x 4 为例其存储结构:

I420
I420

U/V的Semi-Planar存储 (YUV420SP)

部分华为机都属于此类,采用相同的代码输出如下:

代码语言:java
复制
image format: 35
pixelStride 1
rowStride 1920
width 1920
height 1080
buffer size 2073600
Finished reading data from plane 0
pixelStride 2
rowStride 1920
width 1920
height 1080
buffer size 1036800
Finished reading data from plane 1
pixelStride 2
rowStride 1920
width 1920
height 1080
buffer size 1036800
Finished reading data from plane 2

image格式依然是YUV_420_888,分辨率是1920 x 1080 。

  • plane[0] 是Y数据,从rowStride是1920和 pixelStride是1,可知每行1920个像素且Y数据之间无间隔,从buffer size / rowStride = 1080 Y数据有1080行。
  • plane[1] 是U数据,rowStride 是1920, rowStride是2 ,说明每行1920个像素中每两个连续的U之间隔了一个像素,buffer中索引为: 0 , 2 , 4, 6, 8 … 是U数据,即步长为2。 每行实际的U数据只占1/2 ,buffer size / rowStride = 540 只有540行,说明纵向采样也是1/2 ,但buffer size 是 plane[0]的 1/2而不是1/4, 连续的U之间到底存储了什么数据,才使得buffer size 变为plane[0]的1/2了?
  • 同plane[1]。

通过如下方法分别打印Y、U、V三个buffer 到文件中(十六进制格式),来看一下plane[1]和plane[2]中存储数据的特点:

代码语言:c#
复制
                    // Y-buffer
                    ByteBuffer yBuffer = planes[0].getBuffer();
                    int ySize = yBuffer.remaining();
                    byte[] yBytes = new byte[ySize];
                    yBuffer.get(yBytes);

                    // U-buffer
                    ByteBuffer uBuffer = planes[1].getBuffer();
                    int uSize = uBuffer.remaining();
                    byte[] uBytes = new byte[uSize];
                    uBuffer.get(uBytes);

                    // V-buffer
                    ByteBuffer vBuffer = planes[2].getBuffer();
                    int vSize = vBuffer.remaining();
                    byte[] vBytes = new byte[vSize];
                    vBuffer.get(vBytes);

                    String yFileName = "Y";
                    String uFileName = "U";
                    String vFileName = "V";

                    // 保存目录
                    File dir = new File(mRootDir + File.separator + "YUVV");

                    if (!dir.exists()) {
                        dir.mkdir();
                    }

                    // 文件名
                    File yFile = new File(dir.getAbsolutePath() + File.separator + yFileName + ".yuv");
                    File uFile = new File(dir.getAbsolutePath() + File.separator + uFileName + ".yuv");
                    File vFile = new File(dir.getAbsolutePath() + File.separator + vFileName + ".yuv");


                    try {

                        // 以字符方式书写
                        Writer yW = new FileWriter(yFile);
                        Writer uW = new FileWriter(uFile);
                        Writer vW = new FileWriter(vFile);

                        for (int i = 0; i < ySize; i++) {

                            String preValue = Integer.toHexString(yBytes[i]); // 转为16进制
                            // 因为byte[] 元素是一个字节,这里只取16进制的最后一个字节
                            String lastValue = preValue.length() > 2 ? preValue.substring(preValue.length() - 2) : preValue;
                            yW.write(" " + lastValue + " "); // 写入文件
                            if ((i + 1) % 20 == 0) {  // 每行20个
                                yW.write("\n");
                            }
                        }
                        yW.close();


                        for (int i = 0; i < uSize; i++) {
                            String preValue = Integer.toHexString(uBytes[i]);
                            String lastValue = preValue.length() > 2 ? preValue.substring(preValue.length() - 2) : preValue;
                            uW.write(" " + lastValue + " ");
                            if ((i + 1) % 20 == 0) {
                                uW.write("\n");
                            }
                        }
                        uW.close();


                        for (int i = 0; i < vSize; i++) {
                            String preValue = Integer.toHexString(vBytes[i]);
                            String lastValue = preValue.length() > 2 ? preValue.substring(preValue.length() - 2) : preValue;
                            vW.write(" " + lastValue + " ");
                            if ((i + 1) % 20 == 0) {
                                vW.write("\n");
                            }
                        }
                        vW.close();


                    } catch (FileNotFoundException e) {
                        e.printStackTrace();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }

打开U.yuv和V.yuv :

U.yuv文件 :

代码语言:text
复制
 80  7b  80  7b  80  7b  80  7b  80  7b  80  7b  80  7b  80  7c  80  7c  80  7c 
 ...

V.yuv文件:

代码语言:text
复制
 7b  80  7b  80  7b  80  7b  80  7b  80  7b  80  7b  80  7b  80  7c  80  7c  80 
 ...

将V.yuv错开一位 :

代码语言:text
复制
U :  80  7b  80  7b  80  7b  80  7b  80  7b  80  7b  80  7b  80  7c  80  7c  80  7c ...
V :      7b  80  7b  80  7b  80  7b  80  7b  80  7b  80  7b  80  7b  80  7c  80  7c  80 ...

可以发现U和V错开一位后,对应位相等,实际上:

代码语言:css
复制
plane[1] : UVUVUVUVUVUVUVUV...
plane[2] : VUVUVUVUVUVUVUVU...

这就是为什么plane[1]和plane[2]的buffer size 是plane[0]的1/2而不是1/4的原因。

看8 x 4的NV12存储结构(NV21只是UV交错顺序相反):

NV12
NV12

结论

  1. plane[0] + plane[1] 可得NV12
  2. plane[0] + plane[2] 可得NV21

参考3中获取I420和NV21的方法是:先从plane[0]中提取出Y数据,然后在plane[1]中提取U数据,最后在plane[2]中提取V数据。

参考

参考: Android: Image类浅析(结合YUV_420_888)

本文系转载,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文系转载前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 简介
  • 从YUV420谈起
  • Image
  • 官网文档介绍
  • U/V的平(Planar)面和半平面(Semi-Planar)
    • U/V的Planar存储(YUV420P)
      • U/V的Semi-Planar存储 (YUV420SP)
      • 结论
      • 参考
      相关产品与服务
      对象存储
      对象存储(Cloud Object Storage,COS)是由腾讯云推出的无目录层次结构、无数据格式限制,可容纳海量数据且支持 HTTP/HTTPS 协议访问的分布式存储服务。腾讯云 COS 的存储桶空间无容量上限,无需分区管理,适用于 CDN 数据分发、数据万象处理或大数据计算与分析的数据湖等多种场景。
      领券
      问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档