1. 程式人生 > 其它 >Android Camera 開發簡單例項(一): Preview

Android Camera 開發簡單例項(一): Preview

技術標籤:Android Camerajavaandroid

目前正在學習camera開發,初步學習camera1,即android.hardware.camera。

一.Function描述

preview功能是camera功能的基礎,相機進入拍照狀態必需要在preview之後,且使用者的體驗很大程度上與preview的幀率以及解析度有關。
關於相機工作流程,以拍攝一次舉例:
1.判斷surface 是否可用
2.open camera
3.設定preview引數
4.start preview
5.takePicture
take picture start
take picture done

picture store complete
6.回到1

二.Preview設定

preview的設定可以通過設定Camera.Parameters來完成。
以下是Parameters的可設定內容:
Parameters的可設定方法
除此之外,通過Camera例項還可以設定相機的自動對焦,顯示角度調整等。

        Camera.Parameters parameters = camera.getParameters();
        //設定最大幀率
        List<int[]> fpsList = parameters.getSupportedPreviewFpsRange();
        if
(fpsList != null && fpsList.size() > 0) { int[] maxFps = fpsList.get(0); for (int[] fps : fpsList) { if (maxFps[0] * maxFps[1] < fps[0] * fps[1]) { maxFps = fps; } } parameters.setPreviewFpsRange
(maxFps[0], maxFps[1]); Log.d("yuanj", maxFps[0] + " " + maxFps[1]); } //設定Preview解析度 parameters.setPreviewSize(1920, 1080); //設定preview的orientation,與下面的setDisplayOrientation一起用 parameters.set("orientation", "portrait"); ... //使設定引數生效 camera.setParameters(parameters); //設定顯示旋轉90度,一般的camera都是顯示為逆時針90度 camera.setDisplayOrientation(90); //設定自動對焦 camera.setAutoFocusMoveCallback(new Camera.AutoFocusMoveCallback() { @Override public void onAutoFocusMoving(boolean start, Camera camera) { } });

三.Camera 預覽幀獲取/顯示

Camera預覽幀的獲取/顯示目前有兩種方式。通過native window將資料給surface holder完成顯示或使用Preview Callback的回撥方法onPreviewFrame來獲取幀資料。

1.Native window

這種方式只能進行幀資料顯示,無法對預覽幀進行後續處理,做預覽特效一般使用Callback方式。但是這種方式是直接取得資料,幀數高且無預覽畫面旋轉問題。

2.Preview Callback

此種方式需要在回撥中進行每一幀的處理和繪畫,因此效能方面會有一定影響。
進行預覽幀處理可以使用GLSurfaceView,來做出自定義預覽畫面。

本次例項使用基礎View SurfaceView來實現基本的preview功能。

四.預覽幀顯示功能實現

1.獲取Camera許可權(版本高記得申請許可權)

    <uses-permission android:name="android.permission.CAMERA" />
    <uses-feature android:name="android.hardware.camera" />
    <uses-feature android:name="android.hardware.camera.autofocus" />
    <uses-permission android:name="android.permission.WEITE_EXTERNAL_STORAGE" />

2.建立SurfaceView並設定Callback

        surfaceView = findViewById(R.id.surface);
        surfaceView.getHolder().addCallback(this);

3.openCamera

    @Override
    public void surfaceCreated(@NonNull SurfaceHolder holder) {
        mCamera = Camera.open();
        try {
            mCamera.setPreviewDisplay(holder);
        } catch (Exception e) {
            mCamera.release();
            mCamera = null;
        }
    }

4.設定Perview引數並開始Preview

    @Override
    public void surfaceChanged(@NonNull SurfaceHolder holder, int format, int width, int height) {
        Camera.Parameters parameters = camera.getParameters();
        //設定最大幀率範圍
        List<int[]> fpsList = parameters.getSupportedPreviewFpsRange();
        if (fpsList != null && fpsList.size() > 0) {
            int[] maxFps = fpsList.get(0);
            for (int[] fps : fpsList) {
                if (maxFps[0] * maxFps[1] < fps[0] * fps[1]) {
                    maxFps = fps;
                }
            }
            parameters.setPreviewFpsRange(maxFps[0], maxFps[1]);
            Log.d("Test", maxFps[0] + " " + maxFps[1]);
        }
//        parameters.setRecordingHint(true); 強制開啟Preview
        parameters.setPreviewSize(1920, 1080);
        parameters.set("orientation", "portrait");
        mCamera.setDisplayOrientation(90);
        mCamera.setParameters(parameters);

        mCamera.startPreview();
    }

五.預覽幀獲取並顯示

在採用第一種方式的情況下,可以通過Callback獲取每一幀的資料。
有一些特殊需求如Preview原畫面和載入特效畫面的比較可以通過Callback在另一個Surface上畫特效。
這裡只使用一個Surface完成預覽畫面的顯示,因此不能將holder傳遞下去。
需要在surfaceCreated時進行以下修改:

    @Override
    public void surfaceCreated(@NonNull SurfaceHolder holder) {
        mCamera = Camera.open();
        try {
        //這裡的mSurfaceTexture是成員變數,防止引用被回收
        //感興趣的話,這裡的surfaceTexture可以直接用 new SurfaceTexture(0)嘗試一下
            mCamera.setPreviewTexture(mSurfaceTexture);
        } catch (Exception e) {
            mCamera.release();
            mCamera = null;
        }
    }

在設定Preview引數時設定PreviewCallback

 @Override
    public void surfaceChanged(@NonNull SurfaceHolder holder, int format, int width, int height) {
        Camera.Parameters parameters = camera.getParameters();
        List<int[]> fpsList = parameters.getSupportedPreviewFpsRange();
        if (fpsList != null && fpsList.size() > 0) {
            int[] maxFps = fpsList.get(0);
            for (int[] fps : fpsList) {
                if (maxFps[0] * maxFps[1] < fps[0] * fps[1]) {
                    maxFps = fps;
                }
            }
            parameters.setPreviewFpsRange(maxFps[0], maxFps[1]);
            Log.d("Test", maxFps[0] + " " + maxFps[1]);
        }
//        設定幀率30
//        parameters.setPreviewFrameRate(30);

//        parameters.setRecordingHint(true);
        parameters.setPreviewSize(1920, 1080);
        //新增Preview Callback
        PreviewCallback previewCallback = new PreviewCallback();
        mCamera.setPreviewCallback(previewCallback);
        
        parameters.set("orientation", "portrait");
        mCamera.setDisplayOrientation(90);
        mCamera.setParameters(parameters);

        mCamera.startPreview();
    }

在回撥中獲取資料,解析成Bitmap後繪製到Surface上

    private class PreviewCallback implements Camera.PreviewCallback {
        @Override
        public void onPreviewFrame(byte[] data, Camera camera) {
			Camera.Size size = camera.getParameters().getPreviewSize();//獲得預覽影象設定的尺寸
            YuvImage img = new YuvImage(data, ImageFormat.NV21, size.width, size.height, null);
            ByteArrayOutputStream stream = new ByteArrayOutputStream();
            img.compressToJpeg(new Rect(0, 0, size.width, size.height), 80, stream);
            Bitmap bitmap = BitmapFactory.decodeByteArray(stream.toByteArray(), 0, stream.size());
            try {
                stream.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
            Canvas canvas = surfaceView.getHolder().lockCanvas();
            canvas.drawBitmap(bitmap, null, getImageRect(bitmap.getWidth(), bitmap.getHeight()), new Paint());
            bitmap.recycle();
            surfaceView.getHolder().unlockCanvasAndPost(canvas);
        }
    }

六.回撥方式效能優化

原生camera設定PreviewCallback的方式有三種:

        PreviewCallback previewCallback = new PreviewCallback();
        camera.setPreviewCallbackWithBuffer(previewCallback);
        camera.setOneShotPreviewCallback(previewCallback);
        camera.setPreviewCallback(previewCallback);

這裡做效能優化使用第一種方式。
為什麼能優化呢?
這是基於底層傳遞資料的方式:
無buffer設定時,底層每捕捉到一幀資料,就需要new一個buffer來進行傳遞。每次傳遞結束,由於回收機制,都需要將剛剛new的buffer進行回收,這樣就不斷new一個buffer並進行回收。
而當有buffer時,底層會有一個Vector來存放buffer。當Vector不為空時,底層才會使用Vector中的空閒buffer,當Vector為空時,底層則會切換為無buffer設定情況。

說到這裡,可能會奇怪,為什麼Vector會空呢?
每次通過Vector中的buffer傳遞資料,都會將該buffer從Vector中移除。如果不往Vector中再新增buffer,自然Vector會空。

如何新增buffer呢?
通過addCallbackBuffer來新增。

mCamera.addCallbackBuffer(new byte[size]);

何時新增buffer呢?
開始preview時,底層需要buffer傳遞,因此在preview前需要新增;
當Callback返回資料後,承載資料的buffer已經可以釋放,這時候需要將該buffer添加回去。

那麼buffer 的size應該如何定呢?
buffer的size通過previewSize來計算,計算公式為width * height * 3 / 2

        parameters.setPreviewSize(1920, 1080);
        // 一般3個buffer是肯定夠的
        for (int i = 0; i < 3; i++) {
        // 這裡3110400 = 1920 * 1080 *3 /2
            mCamera.addCallbackBuffer(new byte[3110400]);
        }
        PreviewCallback previewCallback = new PreviewCallback();
        mCamera.setPreviewCallbackWithBuffer(previewCallback);

關於解析資料,這裡的data是YUV格式的資料,直接使用Bitmap轉換是不行的,會報空指標,需要使用YuvImage來轉。
但是在回撥裡頭做資料處理肯定會影響效能,於是我想到用Handler將data轉移出來做處理。

    private class PreviewCallback implements Camera.PreviewCallback {
        @Override
        public void onPreviewFrame(byte[] data, Camera camera) {
            Message message = Message.obtain();
            message.obj = data;
            mHandler.sendMessage(message);
            camera.addCallbackBuffer(data);
        }

    }

在Handler中進行資料處理,可以感受到幀率有一定提升。
但是也會有缺點,當裝置效能不足時會出現跳幀,丟幀問題。

    private class DecodeDataHandler extends Handler {
        @Override
        public void handleMessage(@NonNull Message msg) {
            byte[] data = (byte[]) msg.obj;
            Camera.Size size = camera.getParameters().getPreviewSize();//獲得預覽影象設定的尺寸
            //關於ImageFormat,目前只支援NV21和YUY2,其他的會報錯
            YuvImage img = new YuvImage(data, ImageFormat.NV21, size.width, size.height, null);
            ByteArrayOutputStream stream = new ByteArrayOutputStream();
            // 80表示轉Jpeg的質量,最高100,會影響到預覽效能,越高幀率越低
            img.compressToJpeg(new Rect(0, 0, size.width, size.height), 80, stream);
            Bitmap bitmap = BitmapFactory.decodeByteArray(stream.toByteArray(), 0, stream.size());
            try {
                stream.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
            Canvas canvas = surfaceView.getHolder().lockCanvas();
            canvas.drawBitmap(bitmap, null, getImageRect(bitmap.getWidth(), bitmap.getHeight()), new Paint());
            bitmap.recycle();
            surfaceView.getHolder().unlockCanvasAndPost(canvas);
        }
    }

七.遺留問題

回撥處理時,返回的資料是逆時針90度的,無論是否使用了setDisplayOrientation。
我嘗試在回撥處理時進行Image的90度偏轉,會極大的影響幀率表現。
偏轉處理嘗試的方法包括YUV資料轉換和Bitmap資料轉換,甚至嘗試了ObjcetAnimator的偏轉動畫,結果都不盡人意。
基礎的SurfaceView可能解決不了該問題,使用GL執行緒的GLSurfaceView有望解決,該問題留待後續解決。
大家如果有什麼好的建議,歡迎留言!