安卓camera2 API获取YUV420_888格式详解

Posted 周末的音视频

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了安卓camera2 API获取YUV420_888格式详解相关的知识,希望对你有一定的参考价值。

        安卓音视频开发中的一个环节是摄像头采集数据,android平台上摄像头采集的API有两套,camera1和camera2。本文主要讲的是camera2这套API采集数据,并指明YUV420_888格式时候获取到的摄像头YUV数据格式,具体是怎么样的。

一、背景/问题

        之所以写这篇文章也是因为作者在开发过程中遇到了一些坑,然后在网上查阅资料后总结了一下内容。首先先说一下我遇到的问题:

        按照API的写法,获取摄像头数据,是在预览的回调中去获取数据,常用的会设置获取数据的格式为YUV_420_888,如下:

mImageReader = ImageReader.newInstance(1920, 1080, ImageFormat.YUV_420_888,2);

        然后在mImageReader.setOnImageAvailableListener回调中去获取数据,比如如下:

mImageReader.setOnImageAvailableListener(new ImageReader.OnImageAvailableListener() 
    private byte[] y;
    private byte[] u;
    private byte[] v;
    private ReentrantLock lock = new ReentrantLock();
    @Override
    public void onImageAvailable(ImageReader reader) 
        Image image = reader.acquireNextImage();
        // Y:U:V == 4:2:2
        if (image.getFormat() == ImageFormat.YUV_420_888) 
            Image.Plane[] planes = image.getPlanes();
            // 加锁确保y、u、v来源于同一个Image
            lock.lock();
            // 重复使用同一批byte数组,减少gc频率
            if (y == null || u == null || v == null) 
                y = new byte[planes[0].getBuffer().limit() - planes[0].getBuffer().position()];
                u = new byte[planes[1].getBuffer().limit() - planes[1].getBuffer().position()];
                v = new byte[planes[2].getBuffer().limit() - planes[2].getBuffer().position()];
            
            if (image.getPlanes()[0].getBuffer().remaining() == y.length) 
                planes[0].getBuffer().get(y);
                planes[1].getBuffer().get(u);
                planes[2].getBuffer().get(v);
                
                encodeData(y,u,v);    //拿到y,u,v数据然后处理
            
            lock.unlock();
        
        image.close();
    
, mainHandler);

        作者在一开始的时候,觉得拿到y,u,v数据了,那就可以直接拿去处理了,但是发现最终出来的图像是有问题的。于是觉得有点蹊跷,就开始去查阅相关资料。下面继续:

二、API官方解释

        在Android的官方文档上会对这个YUV_420_888获取到的格式作出一些说明:Android官方文档对YUV_420_888格式说明

        大体意思呢,结合网上大佬们的解读,总结如下:

        1、它是YCbCr的泛化格式,不会具体指明是YU12,YV12,NV12,或是是NV21。它能够表示任何4:2:0的平面和半平面格式,每个分量用8 bits 表示。

        2、带有这种格式的图像使用3个独立的Buffer表示,每一个Buffer表示一个颜色平面(Plane),除了Buffer外,它还提供rowStride、pixelStride来描述对应的Plane。这两个rowStride、pixelStride是重点。下面会讲解。

        3、使用Image的getPlanes()获取plane数组:Image.Plane[] planes = image.getPlanes();

        它保证planes[0] 总是Y ,planes[1] 总是U(Cb), planes[2]总是V(Cr)。并保证Y-Plane永远不会和U/V交叉。

        4、U/V-Plane总是有相同的rowStridepixelStride

三、YUV420_888格式详解

        经过上面第二点的官方解读,能大体知道意思,但是具体是怎么样的呢?下面就来讲解一下。首先我们知道yuv420分为Planar格式(P)和Semi-Planar格式(SP),这两种由于存储格式不同,在这里的获取的三个planes中的分布也会非常不同。

1、rowStridepixelStride

        先来解读两个最重要的参数:

        (1)、pixelStride(通过getPixelStride()获得):像素步长,有可能是1、有可能是2。它代表的是行内连续两个颜色值之间的距离(步长)。

        也就是说,如果是1,那么每一行中的同一个颜色分量,比如Y分量,是连续的,也就是行内索引为0,1,2,...的颜色分量都是它的。如果是2,那么每一行中的同一个颜色分量,是不连续的,是会中间间隔1个元素的,也就是行内索引为0,2,4,6,...的颜色分量才是它的。

        这里还有个重要的点:假如是步长为2,意味索引间隔的原色才是有效的元素,中间间隔的元素其实是没有意义的。而Android中确实也是这么做的,比如某个plane[1](U分量)的步长是2,那么数组下标0,2,4,6,...的数据就是U分量的,而中间间隔的元素Android会补上V分量,也就是会是UVUVUV......这样去分布。但是当最后一个U分量出现后,最后一个没有意义的元素Android就不补了,也就是最后的V分量就不会补了,即是这样分布:UVUVUV...UVUVU。

        (2)、rowStride:(通过getRowStride()获得)“每行数据”的“宽度”,这个跟分辨率的宽度不是同个回事,它是每一行实际存储的宽度,下面的分析会再详细讲述;

2、其他相关参数

        除了上面2个重要参数外,还有几个参数也是需要了解的,如下:

(1)、width和height

        通过getWidth()和getHeight()获得,一般与视频分辨率一致。

(2)、buffer size

        这个主要就是plane数组的大小,一般就通过plane[i].length获取即可。

3、YUV数据的分布和排列

        解读了rowStridepixelStride这两个参数后,我们可以假设一些例子来理解一下拿到的YUV数据的排列。理论上,这两个参数在每个plane中是可以任意设置的,这样组合起来的格式可能也是多种多样的。但是在实际场景中,由于YUV420分为Planar格式(P)和Semi-Planar格式(SP)两大类存储格式。所以实际上我们遇到的大体上会分为下面两大类:

(1)、Planar格式(P):

        1、先看一下6*4的假设图片:

         plane[0]的pixelStride是1,说明没有间隔,Y是连续的,rowStride是6,也就是每行6个,length数量是24,24/6 = 4,共4行。

        plane[1]的pixelStride也是1,说明没有间隔,U是连续的,rowStride是3,也就是每行3个,length数量是6,6/3 = 2,共2行,符合YUV420的情况,横纵都是2:1采样。

        plane[2]与plane[1]相同。

        这种其实就是YUV420P的标准格式,跟我们期望的差不多,不用做多解析,直接按照这样将y、u、v分别取出,即是正确的数据。可惜的是,目前测到的手机大部分不是这样的格式,而是下面要介绍的这类SP的情况出现的多一些。

(2)、Semi-Planar格式(SP):

        还是先看一下6*4的假设图片:

        plane[0]的pixelStride是1,说明没有间隔,Y是连续的,rowStride是6,也就是每行6个,length数量是24,24/6 = 4,共4行。这个Y分量跟Planar格式是一样的。

        plane[1]的pixelStride是2,说明有间隔,U是间隔采取的,这里就回到上面我们分析的两个参数的时候,当pixelStride为2的时候,在U分量中,就会间隔插入了V分量,因此每一行由本来是Y的一半也就是3,变成了6(也就是rowStride的值)。同时就像上面分析的一样,会放弃掉最后一个无意义的V分量,所以就lenght会看到是6*2-1=11的,行数还是2,纵向是不变的。

        plane[2]与plane[1]相同。

        对于这种Semi-Planar格式的,安卓提供的这种方式确实就让人很意向不到,在这种格式下,其实我们有几种取数据方式,首先Y是完整的,直接取即可。对于UV分量可以有2种方式:

        I、plane[1]中以索引0,2,4间隔方式去取U分量,plane[2]中以索引0,2,4间隔方式去取V分量,这样就取到了最准确的U和V分量;

        II、我们其实可以看到,在plane[1]中其实就包含了U和V分量了,只不过丢掉了最后一个V,对于人眼来说,少点一个V,是完全没有影响的。因此其实可以直接拿plane[1]的数据,就拿到U和V。plane[2]同理其实也是有V和U,那么这样的话,其实就可以plane[0] + plane[1] 可得NV12格式;或者plane[0] + plane[2] 可得NV21格式。

        Semi-Planar格式在大多数手机中会经常出现,经过上面的分析也能理解为什么U和V的rowStride会和Y一样,而不是一半。以及为什么U和V的数量最后会少一个分量的原因。

(3)、特殊情况:

        rowStride除了有P和SP格式而导致不同之外,它其实还有一个重要的作用,就是在一些特殊的摄像头sensor采集的时候,因为芯片处理器要字节对齐取数据而导致的补齐操作等等原因,可能就有点类似之前的文章中视频相关的一些基本概念的stride。

        我们举个例子,比如还是图像是6*4的,但是由于某些原因,相机输出的时候,假如rowStride+2了,如下:

        plane[0]的pixelStride是1,说明没有间隔,Y是连续的,rowStride本来应该是6,但是这里是8,后面补了两个空的字节,也就是每行8个,length数量是32,32/8 = 4,共4行。这里分析的时候可以这样判断,getWidth()和getHeight()获取到是6和4,6*4是24,发现32与24不对应,就可以初步判断是有补齐的情况了。而Y的rowStride是8,比getWidth()的6多了2,也就可以推测是每行补齐了2个字节。

        这样的话在取数据的时候,就需要每行都去丢弃最后的2个空字节。同理plane[1]和plane[2]也是类似。因此对于这类特殊的camera,我们需要根据pixelStride和rowStride与分辨率的关系,去进行一些特殊的处理才行。

四、总结

        根据上面的总体分析,可以总结如下:

        1、根据rowStride、pixelStride与分辨率的关系,判断是否有补齐的特殊情况,若有,则需要对数据进行空字节丢弃的处理。一般大部分的手机都不会有这么特殊的情况。若没有,则属于正常情况。

        2、正常情况下,先判断pixelStride的值,可以知道是P还是SP存储格式;若pixelStride为1,是P格式,直接可以顺序取Y、U、V;若pixelStride为2,则可以考虑上述第三点中Semi-Planar格式的取数据方式。

五、问题解决

        最后回到我一开始遇到的问题,之所以会出问题就是因为我没有判断pixelStride和rowStride就直接取了,实际上我的手机是以SP的方式的,所以U和V的pixelStride是2,所以没法直接赋值。根据这种逻辑,我修改代码如下,判断pixelStride,并采用分别取U和V的方式,暂时不考虑字节补齐的特殊情况。

        mImageReader.setOnImageAvailableListener(new ImageReader.OnImageAvailableListener() 
            private byte[] y;
            private byte[] u;
            private byte[] v;
            private ReentrantLock lock = new ReentrantLock();
            @Override
            public void onImageAvailable(ImageReader reader) 
                Image image = reader.acquireNextImage();
                // Y:U:V == 4:2:2
                if (image.getFormat() == ImageFormat.YUV_420_888) 
                    Image.Plane[] planes = image.getPlanes();
                    // 加锁确保y、u、v来源于同一个Image
                    lock.lock();
                    // 重复使用同一批byte数组,减少gc频率
                    if (y == null || u == null || v == null) 
                        y = new byte[planes[0].getBuffer().limit() - planes[0].getBuffer().position()];
                        u = new byte[planes[1].getBuffer().limit() - planes[1].getBuffer().position()];
                        v = new byte[planes[2].getBuffer().limit() - planes[2].getBuffer().position()];
                    
                    if (image.getPlanes()[0].getBuffer().remaining() == y.length) 
                        planes[0].getBuffer().get(y);
                        planes[1].getBuffer().get(u);
                        planes[2].getBuffer().get(v);


                        //数据前期处理
                        //判断是p还是sp
                        if(planes[1].getPixelStride() == 1)   //p
                            //无须处理
                            encodeDataTopush(y,u,v);
                        else if(planes[1].getPixelStride() == 2)      //sp
                            //提取U分量
                            byte[] temp_u = new byte[(u.length+1)/2];       //这里需要+1,需要注意,具体原理不展开阐述
                            byte[] temp_v = new byte[(v.length+1)/2];
                            int index_u = 0;
                            int index_v = 0;
                            for(int i=0;i < u.length;i++)
                                if(0 == (i%2))
                                    temp_u[index_u] = u[i];
                                    index_u++;
                                
                            
                            for(int j=0;j < v.length;j++)
                                if(0 == (j%2))
                                    temp_v[index_v] = v[j];
                                    index_v++;
                                
                            
                            encodeData(y,temp_u,temp_v);
                        
                    
                    lock.unlock();
                
                image.close();
            
        

参考文章:

Android: Image类浅析(结合YUV_420_888)

Camera2 YUV420_888

android camera2 拿到的yuv420数据到底是什么样的?​​​​​​​

以上是关于安卓camera2 API获取YUV420_888格式详解的主要内容,如果未能解决你的问题,请参考以下文章

使用新的 Android camera2 api 从 YUV_420_888 进行 JPEG 编码时的绿色图像

camera2 如何从图像读取器侦听器中的 YUV_420_888 图像中获取 Exif 数据

如何保存 YUV_420_888 图像?

在Android camera2下将YUV_420_888转换为位图的图像不正确

将android.media.Image(YUV_420_888)转换为Bitmap

Android camera2 输出到 ImageReader 格式 YUV_420_888 仍然很慢