Android使用Camera2獲取預覽數據

1、Camera2簡介

Camera2是Google在Android 5.0後推出的一個全新的相機API,Camera2和Camera沒有繼承關係,是徹底從新設計的,且Camera2支持的功能也更加豐富,可是提供了更豐富的功能的同時也增長了使用的難度。
Google的官方Demo:https://github.com/googlesamp...android

2、Camera2 VS Camera

如下分別是使用Camera2和Camera打開相機進行預覽並獲取預覽數據的流程圖。git

Camera2 API使用流程

Camera API使用流程

能夠看到,和Camera相比,Camera2的調用明顯複雜得多,但同時也提供了更強大的功能:github

  • 支持在非UI線程獲取預覽數據
  • 能夠獲取更多的預覽幀
  • 對相機的控制更加完備
  • 支持更多格式的預覽數據
  • 支持高速連拍

可是具體可否使用還要看設備的廠商有無實現。算法

3、如何使用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),也就是:ide

(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手機上,選擇的分辨率爲1520x760,可是回傳的圖像數據的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,可是通常狀況下咱們可能須要的是其組合的結果,如NV2一、I420等。因爲Java的gc會影響性能,在從plane中獲取Y、U、V數據和Y、U、V轉換爲其餘數據的過程當中,咱們須要注意對象的建立頻率,咱們能夠建立一次對象重複使用。不只是Y,U,V這三個對象,組合的對象,如NV21,也能夠用一樣的方式處理,但如有將NV21傳出當前線程,用於異步處理的操做,則須要作深拷貝,避免異步處理時引用數據被修改。

4、示例代碼

  • 示例代碼

https://github.com/wangshengy...

  1. demo功能
  2. 演示Camera2的使用
  3. 獲取預覽幀數據並隔一段時間將原始畫面和處理過的畫面顯示到UI上
  4. 將預覽的YUV數據轉換爲NV21,再轉換爲Bitmap並顯示到控件上,同時也將該Bitmap轉換爲相機預覽效果的Bitmap顯示到控件上,便於瞭解原始數據和預覽畫面的關係

運行效果

效果圖

最後,推薦一個好用的Android開源人臉識別sdk:https://ai.arcsoft.com.cn/uce...

相關文章
相關標籤/搜索