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

为了统一和兼容各个平台图像数据格式的差异,和提供更丰富的相机参数设置,Android5.0之后推出了camera2 API,一般的我们会使用相机有几种需求

  • 预览
  • 拍照
  • 录像
  • 获取图像原始数据

这些需求在官方给的一系列demo中都有示例,我也对Camera2Basic写过一篇笔记android-camera2basic源码逻辑流程解析 ,今天还是一个笔记,记录下第四个需求的问题。
首先,YUV420是一系列格式,从这个名字只能确定Y:U:V是4:1:1,具体的有YUV420P、YUV420SP、NV21、YV12等等。

新API提供了ImageReader这样一个类,来帮助开发者获取每一帧的原始数据,同时提供了实例化方法

ImageReader reader = ImageReader.newInstance(int width, int height, int format, int maxImages);

这里第三个参数是指定从ImageReader中获取的原始数据的格式。提个醒,不要太相信这个参数,这里指示不具体是一,就算指定了,摄像头不一定支持这个参数,可能会给出相似的格式的数据也说不准。

先说一个Android里面很有趣的玩笑。跳到newInstance()源码里面可以看到这句

if (format == ImageFormat.NV21) {
    throw new IllegalArgumentException(
            "NV21 format is not supported");
}

Android提供另一个YUV数据的帮助类YuvImage,构造函数

 public YuvImage(byte[] yuv, int format, int width, int height, int[] strides) {
        if (format != ImageFormat.NV21 &&
                format != ImageFormat.YUY2) {
            throw new IllegalArgumentException(
                    "only support ImageFormat.NV21 " +
                    "and ImageFormat.YUY2 for now");
        }
       //ellipsis code 
        mData = yuv;
        mFormat = format;
        mWidth = width;
        mHeight = height;
    }

ImageReader不支持NV21
YuvIamge只支持NV21和YUY2
[捂脸]What?这是什么意思?有了解这块为什么这样的同学,请不吝赐教。

说回正点上。

在ImageReader实例化时传入ImageFormat.YUV_420_888,得到的数据到底是什么样的?网上有很多经典的讲YUV数据的各种格式,简单来讲就是三个通道数据比例是4:1:1的,有效数据量大小为width*height×3/2,但是从Android中拿到的Plane中的byte怎么提取,却很少有人提及。
也就是说,一般我们调用其他的api,比如人脸识别、物体识别等API的时候,要求传入的yuvData都是byte[]类型的。我们需要从Image->Plane->Buffer->byte[],这样拿到最终的byte,这个过程看着简单,网上的其他解析文章也都是,简单的直接从三个Plane的Buffer中直接执行如下代码,然后将三个byte直接拼接就行了。[捂脸]

 byte[] bytes = new byte[buffer.capacity()];
 buffer.get(bytes);

但是我从Iamge的Plane中拿数据的时候确遇到了很多问题。直接按正确的取数据的过程说吧。

  1. Image中拿到width,height,和Planes[]
  2. 每一个plane中拿到pixelsStride和rowStride
  3. pixelsStride:像素步长,有可能是1、有可能是2,如果是1也就是说U、V的数据是紧密排列的,如果是2,就是每隔一位是有效的,可以理解在U和V的buffer中数据是类似u0u0u0u0u0u0u0u…和v0v0v0v0v0v0v0v0…0表示那一位byte无效。理解是可以这么理解,但实际上数据的排列是uvuvuvuvuvuvu…vuvuvuvuvuvuv…,也就是说API从sensor那边取过来的时候的数据就是uv交错的,因为这个有效位置的作用,U的数据把第一位去掉,最后补上一位之后就成了V的数据。所以像YUV420sp格式本身就要求图像是UV交错,可以直接取U的数据,补上最后一位,就OK了。但官方没有指出可以这么取,所以正确性并不能保证。实际中我试着这么取过,只有图像右下角最后一个像素是有问题的。如果不介意这个,可以尝试直接取U的数据。这一点在另外一片文章里面看到的,说的比较清楚。链接在这Image类浅析(结合YUV_420_888) 这里其实也好理解,sensor在处理的时候是逐行扫描的,YUV都是连续生成的,为了节省存储,可能将uv的数据交错合并输出给Android的framework层。
  4. rowStride:“每行数据”的“宽度”,注意这里也有个坑,这个rowStride不一定是和width一样,有的相机输出的比图片本身的width要大,需要“逐行截取”。
  5. 还有一种情况,就是上面那个链接中提到的CropRect的问题,应该会是显示部分正确图像,鉴于我没遇见过,就不瞎猜了。

结合上面的理解,我自己画了一些图。以6*4的图片为例,bytebuffer的排列可以理解如下:
这里写图片描述
在这里width=6,height=4,rowStride=6或者8,等于8时,最后两列会由于某些原因空一些byte,如果你转成rgb图像预览发现有规律的绿色栅格,那么考虑rowStride>width这种情况。
当然这张图只是说可以这么理解,实际上拿到的一维的byte数组,是每行数据接出来的如下:
这里写图片描述
且有:

yBytes.length==w*h;
uBytes.length==w*h/4;
vBytes.length==w*h/4;
plane[0]==rowStride*h;
if(pixelsStride==2)
    rowStride==w/2+temp;
	plane[1].length==plane[2].length==rowStride*h/2-1
else if(pixelsStride=1)
    rowStride==w/2+temp;
    plane[1].length==plane[2].length==rowStride*h/2

最后附上我写的工具类:(这里为了节省内存,Y的数据直接copy到了最终的bytes里面,也可以)

/**
 * yuv420p:  yyyyyyyyuuvv
 * yuv420sp: yyyyyyyyuvuv
 * nv21:     yyyyyyyyvuvu
 */

public class ImageUtil {
    public static final int YUV420P = 0;
    public static final int YUV420SP = 1;
    public static final int NV21 = 2;
    private static final String TAG = "ImageUtil";

    /***
     * 此方法内注释以640*480为例
     * 未考虑CropRect的
     */
    public static byte[] getBytesFromImageAsType(Image image, int type) {
        try {
            //获取源数据,如果是YUV格式的数据planes.length = 3
            //plane[i]里面的实际数据可能存在byte[].length <= capacity (缓冲区总大小)
            final Image.Plane[] planes = image.getPlanes();
            
            //数据有效宽度,一般的,图片width <= rowStride,这也是导致byte[].length <= capacity的原因
            // 所以我们只取width部分
            int width = image.getWidth();
            int height = image.getHeight();

            //此处用来装填最终的YUV数据,需要1.5倍的图片大小,因为Y U V 比例为 4:1:1
            byte[] yuvBytes = new byte[width * height * ImageFormat.getBitsPerPixel(ImageFormat.YUV_420_888) / 8];
            //目标数组的装填到的位置
            int dstIndex = 0;

            //临时存储uv数据的
            byte uBytes[] = new byte[width * height / 4];
            byte vBytes[] = new byte[width * height / 4];
            int uIndex = 0;
            int vIndex = 0;

            int pixelsStride, rowStride;
            for (int i = 0; i < planes.length; i++) {
                pixelsStride = planes[i].getPixelStride();
                rowStride = planes[i].getRowStride();

                ByteBuffer buffer = planes[i].getBuffer();

                //如果pixelsStride==2,一般的Y的buffer长度=640*480,UV的长度=640*480/2-1
                //源数据的索引,y的数据是byte中连续的,u的数据是v向左移以为生成的,两者都是偶数位为有效数据
                byte[] bytes = new byte[buffer.capacity()];
                buffer.get(bytes);

                int srcIndex = 0;
                if (i == 0) {
                    //直接取出来所有Y的有效区域,也可以存储成一个临时的bytes,到下一步再copy
                    for (int j = 0; j < height; j++) {
                        System.arraycopy(bytes, srcIndex, yuvBytes, dstIndex, width);
                        srcIndex += rowStride;
                        dstIndex += width;
                    }
                } else if (i == 1) {
                    //根据pixelsStride取相应的数据
                    for (int j = 0; j < height / 2; j++) {
                        for (int k = 0; k < width / 2; k++) {
                            uBytes[uIndex++] = bytes[srcIndex];
                            srcIndex += pixelsStride;
                        }
                        if (pixelsStride == 2) {
                            srcIndex += rowStride - width;
                        } else if (pixelsStride == 1) {
                            srcIndex += rowStride - width / 2;
                        }
                    }
                } else if (i == 2) {
                    //根据pixelsStride取相应的数据
                    for (int j = 0; j < height / 2; j++) {
                        for (int k = 0; k < width / 2; k++) {
                            vBytes[vIndex++] = bytes[srcIndex];
                            srcIndex += pixelsStride;
                        }
                        if (pixelsStride == 2) {
                            srcIndex += rowStride - width;
                        } else if (pixelsStride == 1) {
                            srcIndex += rowStride - width / 2;
                        }
                    }
                }
            }

            image.close();

            //根据要求的结果类型进行填充
            switch (type) {
                case YUV420P:
                    System.arraycopy(uBytes, 0, yuvBytes, dstIndex, uBytes.length);
                    System.arraycopy(vBytes, 0, yuvBytes, dstIndex + uBytes.length, vBytes.length);
                    break;
                case YUV420SP:
                    for (int i = 0; i < vBytes.length; i++) {
                        yuvBytes[dstIndex++] = uBytes[i];
                        yuvBytes[dstIndex++] = vBytes[i];
                    }
                    break;
                case NV21:
                    for (int i = 0; i < vBytes.length; i++) {
                        yuvBytes[dstIndex++] = vBytes[i];
                        yuvBytes[dstIndex++] = uBytes[i];
                    }
                    break;
            }
            return yuvBytes;
        } catch (final Exception e) {
            if (image != null) {
                image.close();
            }
            Log.i(TAG, e.toString());
        }
        return null;
    }
}

结论:基于一些特殊的sensor,Android API给出的YUV数据,需要根据rowStride,pixelsStride重新筛选拼接,才能得到正确的数据。

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


展开阅读全文
©️2020 CSDN 皮肤主题: 大白 设计师: CSDN官方博客 返回首页
实付0元
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、C币套餐、付费专栏及课程。

余额充值