美文网首页java&Android收藏文档
Android使用Camera2获取预览数据

Android使用Camera2获取预览数据

作者: 省油的灯_wsy | 来源:发表于2019-07-03 17:07 被阅读0次

    一、Camera2简介

    Camera2是Google在Android 5.0后推出的一个全新的相机API,Camera2和Camera没有继承关系,是完全重新设计的,且Camera2支持的功能也更加丰富,但是提供了更丰富的功能的同时也增加了使用的难度。Google的官方Demo:https://github.com/googlesamples/android-Camera2Basic

    二、Camera2 VS Camera

    以下分别是使用Camera2和Camera打开相机进行预览并获取预览数据的流程图。


    Camera2 API使用流程 Camera API使用流程

    可以看到,和Camera相比,Camera2的调用明显复杂得多,但同时也提供了更强大的功能:

    • 支持在非UI线程获取预览数据
    • 可以获取更多的预览帧
    • 对相机的控制更加完备
    • 支持更多格式的预览数据
    • 支持高速连拍

    但是具体能否使用还要看设备的厂商有无实现。

    三、如何使用Camera2

    • 获取预览数据

    一般情况下,大多设备其实只支持ImageFormat.YUV_420_888ImageFormat.JPEG格式的预览数据,而ImageFormat.JPEG是压缩格式,一般适用于拍照的场景,而不适合直接用于算法检测,因此我们一般取ImageFormat.YUV_420_888作为我们获取预览数据的格式,对于YUV不太了解的同学可以戳这里

    mImageReader = ImageReader.newInstance(mPreviewSize.getWidth(), mPreviewSize.getHeight(),
                    ImageFormat.YUV_420_888, 2);
    mImageReader.setOnImageAvailableListener(
                   new OnImageAvailableListenerImpl(), mBackgroundHandler);
    

    其中OnImageAvailableListenerImpl的实现如下

      private class OnImageAvailableListenerImpl implements 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 (camera2Listener != null && image.getFormat() == ImageFormat.YUV_420_888) {
                    Image.Plane[] planes = image.getPlanes();
                    // 加锁确保y、u、v来源于同一个Image
                    lock.lock();
                    // 重复使用同一批byte数组,减少gc频率
                    if (y == 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);
                        camera2Listener.onPreview(y, u, v, mPreviewSize, planes[0].getRowStride());
                    }
                    lock.unlock();
                }
                image.close();
            }
        }
    
    • 注意事项

    1. 图像格式问题
    经过在多台设备上测试,明明设置的预览数据格式是ImageFormat.YUV_420_888(4个Y对应一组UV,即平均1个像素占1.5个byte,12位),但是拿到的数据却都是YUV_422格式(2个Y对应一组UV,即平均1个像素占2个byte,16位),且UV的长度都少了一些(在Oneplus 5和Samsung Tab s3上长度都少了1),也就是:
    (u.length == v.length) && (y.length / 2 > u.length) && (y.length / 2 ≈ u.length)
    YUV_420_888数据的YUV关系应该是:
    y.length / 4 == u.length == v.length
    且系统API中android.graphics.ImageFormat类的getBitsPerPixel方法可说明上述Y、U、V数据比例不对的问题,内容如下:

     public static int getBitsPerPixel(int format) {
            switch (format) {
                ...
                case YUV_420_888:
                    return 12;
                case YUV_422_888:
                    return 16;
                ...
            }
            return -1;
        }
    

    以及android.media.ImageUtils类的imageCopy(Image src, Image dst)函数中有这么一段注释说明的确可能会有部分像素丢失:

    public static void imageCopy(Image src, Image dst) {
                    ...
                    for (int row = 0; row < effectivePlaneSize.getHeight(); row++) {
                        if (row == effectivePlaneSize.getHeight() - 1) {
                            // Special case for NV21 backed YUV420_888: need handle the last row
                            // carefully to avoid memory corruption. Check if we have enough bytes to
                            // copy.
                            int remainingBytes = srcBuffer.remaining() - srcOffset;
                            if (srcByteCount > remainingBytes) {
                                srcByteCount = remainingBytes;
                            }
                        }
                        directByteBufferCopy(srcBuffer, srcOffset, dstBuffer, dstOffset, srcByteCount);
                        srcOffset += srcRowStride;
                        dstOffset += dstRowStride;
                    }
                    ...
        }
    

    2. 图像宽度不一定为stride(步长)
    在有些设备上,回传的图像的rowStride不一定为previewSize.getWidth(),比如在OPPO K3手机上,选择的分辨率为1520x760,但是回传的图像数据的rowStride却是1536,且总数据少了16个像素(Y少了16,U和V分别少了8)。

    • frameworks/base/media/jni/android_media_ImageReader.cpp
    static jobjectArray Image_createSurfacePlanes(JNIEnv* env, jobject thiz,
            int numPlanes, int readerFormat)
    {
        ...
        // Create all SurfacePlanes
        for (int i = 0; i < numPlanes; i++) {
            Image_getLockedImageInfo(env, &lockedImg, i, halReaderFormat,
                    &pData, &dataSize, &pixelStride, &rowStride);
            byteBuffer = env->NewDirectByteBuffer(pData, dataSize);
            if ((byteBuffer == NULL) && (env->ExceptionCheck() == false)) {
                jniThrowException(env, "java/lang/IllegalStateException",
                        "Failed to allocate ByteBuffer");
                return NULL;
            }
    
            // Finally, create this SurfacePlane.
            jobject surfacePlane = env->NewObject(gSurfacePlaneClassInfo.clazz,
                        gSurfacePlaneClassInfo.ctor, thiz, rowStride, pixelStride, byteBuffer);
            env->SetObjectArrayElement(surfacePlanes, i, surfacePlane);
        }
    
        return surfacePlanes;
    }
    
    static void Image_getLockedImageInfo(JNIEnv* env, LockedImage* buffer, int idx,
            int32_t writerFormat, uint8_t **base, uint32_t *size, int *pixelStride, int *rowStride) {
        ALOGV("%s", __FUNCTION__);
    
        status_t res = getLockedImageInfo(buffer, idx, writerFormat, base, size,
                pixelStride, rowStride);
        if (res != OK) {
            jniThrowExceptionFmt(env, "java/lang/UnsupportedOperationException",
                                 "Pixel format: 0x%x is unsupported", buffer->flexFormat);
        }
    }
    
    • frameworks/base/media/jni/android_media_Utils.cpp
    
    status_t getLockedImageInfo(LockedImage* buffer, int idx,
            int32_t containerFormat, uint8_t **base, uint32_t *size, int *pixelStride, int *rowStride) {
        ...
        switch (fmt) {
            case HAL_PIXEL_FORMAT_YCbCr_420_888:
                pData =
                    (idx == 0) ?
                        buffer->data :
                    (idx == 1) ?
                        buffer->dataCb :
                    buffer->dataCr;
                // only map until last pixel
                if (idx == 0) {
                    pStride = 1;
                    rStride = buffer->stride;
                    dataSize = buffer->stride * (buffer->height - 1) + buffer->width;
                } else {
                    // 对于 U 和 V,pStride(也就是buffer->chromaStep)一般都是2,因此dataSize一般都是个奇数
                    pStride = buffer->chromaStep;
                    rStride = buffer->chromaStride;
                    dataSize = buffer->chromaStride * (buffer->height / 2 - 1) +
                            buffer->chromaStep * (buffer->width / 2 - 1) + 1;
                }
                break;
                ...
        }
        ...
    }
    

    3. 当心数组越界
    上述说到,Camera2设置的预览数据格式是ImageFormat.YUV_420_888时,回传的Y,U,V的关系一般是
    (u.length == v.length) && (y.length / 2 > u.length) && (y.length / 2 ≈ u.length)
    UV是有部分缺失的,因此我们在进行数组操作时需要注意越界问题,示例如下:

        /**
         * 将Y:U:V == 4:2:2的数据转换为nv21
         *
         * @param y      Y 数据
         * @param u      U 数据
         * @param v      V 数据
         * @param nv21   生成的nv21,需要预先分配内存
         * @param stride 步长
         * @param height 图像高度
         */
        public static void yuv422ToYuv420sp(byte[] y, byte[] u, byte[] v, byte[] nv21, int stride, int height) {
            System.arraycopy(y, 0, nv21, 0, y.length);
            // 注意,若length值为 y.length * 3 / 2 会有数组越界的风险,需使用真实数据长度计算
            int length = y.length + u.length / 2 + v.length / 2;
            int uIndex = 0, vIndex = 0;
            for (int i = stride * height; i < length; i += 2) {
                nv21[i] = v[vIndex];
                nv21[i + 1] = u[uIndex];
                vIndex += 2;
                uIndex += 2;
            }
        }
    

    4. 避免频繁创建对象
    若选择的图像格式是ImageFormat.YUV_420_888,那么相机回传的Image数据包将含3个plane,分别代表Y,U,V,但是一般情况下我们可能需要的是其组合的结果,如NV21I420等。由于Java的gc会影响性能,在从plane中获取Y、U、V数据和Y、U、V转换为其他数据的过程中,我们需要注意对象的创建频率,我们可以创建一次对象重复使用。不仅是Y,U,V这三个对象,组合的对象,如NV21,也可以用同样的方式处理,但若有将 NV21传出当前线程,用于异步处理的操作,则需要做深拷贝,避免异步处理时引用数据被修改

    四、示例代码

    • 示例代码
      https://github.com/wangshengyang1996/Camera2Demo
    • demo功能
      • 演示Camera2的使用
      • 获取预览帧数据并隔一段时间将原始画面和处理过的画面显示到UI上
      • 将预览的YUV数据转换为NV21,再转换为Bitmap并显示到控件上,同时也将该Bitmap转换为相机预览效果的Bitmap显示到控件上,便于了解原始数据和预览画面的关系
    • 运行效果
      效果图

    相关文章

      网友评论

        本文标题:Android使用Camera2获取预览数据

        本文链接:https://www.haomeiwen.com/subject/ahmqhctx.html