乐趣区

Android使用Camera2获取预览数据

一、Camera2 简介

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

二、Camera2 VS Camera

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

可以看到,和 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();}
}


注意事项

  • . 图像格式问题

经过在多台设备上测试,明明设置的预览数据格式是 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 数据的 Y、U、V 关系应该是:

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;
                }
                ...
    }
  • 图像宽度不一定为 stride(步长)

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

  • 当心数组越界

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

/**
 * 将 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;
    }
}
  • 避免频繁创建对象

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

四、示例代码

  • 示例代码

https://github.com/wangshengy…

  1. demo 功能
  2. 演示 Camera2 的使用
  3. 获取预览帧数据并隔一段时间将原始画面和处理过的画面显示到 UI 上
  4. 将预览的 YUV 数据转换为 NV21,再转换为 Bitmap 并显示到控件上,同时也将该 Bitmap 转换为相机预览效果的 Bitmap 显示到控件上,便于了解原始数据和预览画面的关系

运行效果

最后,推荐一个好用的 Android 开源人脸识别 sdk:https://ai.arcsoft.com.cn/uce…

退出移动版