Android使用Camera2獲取預覽資料

feishixin123發表於2019-07-09

一、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)

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顯示到控制元件上,便於瞭解原始資料和預覽畫面的關係
  • 執行效果
     
    效果圖


最後,推薦給大家一個比較好用的開源安卓人臉識別sdk:

https://ai.arcsoft.com.cn/ucenter/user/reg?utm_source=cnblogs&utm_medium=referral

相關文章