1. 程式人生 > >android camera2 應用以及實時濾鏡

android camera2 應用以及實時濾鏡

之前一直用android camera,但現在市面上新出的手機都是支援camera2的,專案要求也是運用camera2,於是自己學習了下,將一些知識點羅列出來,以供複習和大家參考。
一、camera2的預覽拍照流程
1、獲取相機manager,利用manager獲得相機ID列表(通過ID選擇開啟的相機)

   private void getCameraIdList ()  {
    CameraManager manager = (CameraManager) mContext.getSystemService(Context
            .CAMERA_SERVICE);
    try
{ mCameraIDList = manager.getCameraIdList(); } catch (CameraAccessException e) { e.printStackTrace(); } mCameraID = mCameraIDList[0]; }

上面的函式是將相機的id列表存在mCameraIDlist連結串列內。
2、通過相機的ID和manager我們就可以獲得對應的id的相機的內部引數,如:

 mCameraCharacteristics = manager.getCameraCharacteristics(mCameraID);

通過這個mCameraCharacteristics 我們可以獲取預覽需要的基本引數預覽size 和 角度
(1)用來得到相機支援預覽的size,從而來設定相機預覽的控制元件大小和拍照圖片大小。

//配置引數map
            StreamConfigurationMap map = mCameraCharacteristics.get(CameraCharacteristics
                    .SCALER_STREAM_CONFIGURATION_MAP);
                    //獲取相機支援的size
               Size size[] =  map.getOutputSizes
(ImageFormat.JPEG)

(2)獲取當前手相機(surface的角度),下面的方法是根據相機預覽角度和相機的預覽角度獲取當前的傾斜角度,要想預覽和成像的圖片角度一致,相機角度和窗體角度得一致。

   private int getRotateDegree(CameraCharacteristics characteristics) {
        WindowManager wm = (WindowManager) getContext().getSystemService(Context.WINDOW_SERVICE);
        int displayRotation = wm.getDefaultDisplay().getRotation();
        int degrees = 0;
        switch (displayRotation) {
            case Surface.ROTATION_0:
                degrees = 0;
                break;
            case Surface.ROTATION_90:
                degrees = 90;
                break;
            case Surface.ROTATION_180:
                degrees = 180;
                break;
            case Surface.ROTATION_270:
                degrees = 270;
                break;
        }
//獲取相機取景的角度(相對於正常豎直角度)(相機確定了,這個值也是固定的(待驗證))
        int senseOrientation = characteristics.get(CameraCharacteristics.SENSOR_ORIENTATION);
        return   (senseOrientation - degrees + 360) % 360;
    }

google的官方文件給出的demo中就利用相機的角度和視窗的角度和大小來選擇最合適的預覽size,大家可以去參考下。
(3)初始化imageReader,這個類是拍照時候獲取圖片用的,預覽size確定了,我們可以初始化它了。

 mImageReader = ImageReader.newInstance(mPreViewSize.getWidth(), mPreViewSize.getHeight(),
                    ImageFormat.JPEG, 2);

下面則是設定獲取圖片成功的回撥方法:

 mImageReader.setOnImageAvailableListener(onImageAvailableListener, null);

當然這裡的listener是拍照後,獲取圖片的時候回撥的方法。
3、開啟相機
開啟相機則是通過前面獲取的manager和相機id來開啟相應的相機

 manager.openCamera(mCameraID, cameraOpenCallBack, null);

對應的方法的原型為

   public void openCamera(@NonNull String cameraId,
            @NonNull final CameraDevice.StateCallback callback, @Nullable Handler handler)

相機id 和開啟的回撥方法必須不為空,後面1個可為空。在相機成功開啟後我們才能進行相應的操作。
注意:同一時間只能對同一個相機開啟,為了防止我們頻繁切換攝像頭或者不停的停止和開啟相機預覽時出現錯誤,一般都會設定一個相機鎖,這裡可以自己google,這裡不再詳述了。
4、相機開啟後,建立預覽引數類Builder
上面開啟相機的回撥cameraOpenCallBack中來建立builder,通過builder可以設定我們預覽時的各種引數。新增預覽的介面(target,預覽時候新增textureView的surface,而拍照時候用imageReader的surface),並建立captureSession,並在回撥中建立CameraCaptureSession,這個是預覽會話,我們最終都是利用這個來控制預覽的,因此只有會話配置成功了就可以進行預覽了。

 private CameraDevice.StateCallback cameraOpenCallBack = new CameraDevice.StateCallback() {
        @Override
        public void onOpened(CameraDevice camera) {
            Log.d(TAG, "相機已經開啟");
            try {
                mCameraDevice = camera;
                mPreViewBuilder = camera.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW);
                SurfaceTexture texture = getSurfaceTexture();//new SurfaceTexture(2);//
                texture.setDefaultBufferSize(mPreViewSize.getWidth(), mPreViewSize.getHeight());
                mSurface = new Surface(getSurfaceTexture());
                mPreViewBuilder.addTarget(mSurface);
                mCaptureSession = camera.createCaptureSession(Arrays.asList(mSurface, mImageReader.getSurface
                        ()), mSessionStateCallBack, null);
            } catch (CameraAccessException e) {
                Log.d(TAG, "相機建立session失敗");
                e.printStackTrace();
            }
        }

        @Override
        public void onDisconnected(CameraDevice camera) {

        }

        @Override
        public void onError(CameraDevice camera, int error) {

        }
    };

5 、 會話預覽建立後,開啟預覽

   private CameraCaptureSession.StateCallback mSessionStateCallBack = new CameraCaptureSession.StateCallback() {

        @Override
        public void onConfigured(CameraCaptureSession session) {
                Log.d(TAG, "onConfigured......");
                mCameraSession = session;
                mPreViewBuilder.set(CaptureRequest.CONTROL_AF_MODE,
                        CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE);
            rePreView();
            cameraLock = false;
        }

        @Override
        public void onConfigureFailed(CameraCaptureSession session) {

        }
    };

6、給Builder設定引數,並開啟預覽
其實通過上述基礎設定就可以直接呼叫 mCaptureSession .setRepeatingRequest進行預覽了,camera2很強大,我們得好好利用:
(1) 對焦
<1> 自動對焦

 mPreViewBuilder.set(CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE);
<2> 人臉聚焦

人臉聚焦,其實就是在預覽的時候,利用相機預覽介面中對人臉的定位,然後重新預覽,設定聚焦區域
其實和第三者聚焦方法是相同的,對特定區域聚焦。

 private CameraCaptureSession.CaptureCallback captureCallback = new CameraCaptureSession.CaptureCallback() {
        @Override
        public void onCaptureCompleted(CameraCaptureSession session, CaptureRequest request, TotalCaptureResult result) {
            super.onCaptureCompleted(session, request, result);
            process(result);

        }

        @Override
        public void onCaptureProgressed(CameraCaptureSession session, CaptureRequest request, CaptureResult partialResult) {
            super.onCaptureProgressed(session, request, partialResult);
        }

        private void process(CaptureResult result) {
            Face[] camera_faces = result.get(CaptureResult.STATISTICS_FACES);
            if (camera_faces != null) {
                if (camera_faces.length > 0) {
                    focusState = FOCUS_FACE_STATE;
                    faceRect = new Rect(camera_faces[0].getBounds().left, camera_faces[0].getBounds().top, camera_faces[0].getBounds().right, camera_faces[0].getBounds().bottom);
                    System.out.println("lammy  faces" + camera_faces.length );
                    startPreview(new MeteringRectangle(faceRect, MeteringRectangle.METERING_WEIGHT_MAX));
                }
            }

        }
    };

這裡startPreview方法中是對制定位置聚焦,其實主要是在重新預覽的時候builder設定了引數

            mPreViewBuilder.set(CaptureRequest.CONTROL_AF_REGIONS, new MeteringRectangle[]{rect});
            mPreViewBuilder.set(CaptureRequest.CONTROL_AE_REGIONS, new MeteringRectangle[]{rect});

<3>手指點選聚焦
其實手指點選聚焦同第二種方式一樣,對特定地區進行聚焦,這裡也不再詳述了。

(2) 閃光燈

閃光燈分2種,一種是手電筒模式,一種是拍照模式,現在分別介紹一下:

(1)手電筒

 //關閉模式
 mPreViewBuilder.set(CaptureRequest.FLASH_MODE, CameraMetadata.FLASH_MODE_OFF);
 // 閃一下模式               
 mPreViewBuilder.set(CaptureRequest.FLASH_MODE, CameraMetadata.FLASH_MODE_SINGLE);
 //長亮模式
 mPreViewBuilder.set(CaptureRequest.FLASH_MODE, CameraMetadata.FLASH_MODE_TORCH);

(2)拍照

//自動曝光
mPreViewBuilder.set(CaptureRequest.CONTROL_AE_MODE, CameraMetadata.CONTROL_AE_MODE_ON_AUTO_FLASH); 
// 強制曝光
mPreViewBuilder.set(CaptureRequest.CONTROL_AE_MODE,CameraMetadata.CONTROL_AE_MODE_ON_ALWAYS_FLASH);
//不閃光
mPreViewBuilder.set(CaptureRequest.CONTROL_AE_MODE,CameraMetadata.CONTROL_AE_MODE_ON_AUTO_FLASH_REDEYE);

閃光燈對部分手機相容性不是很好,特別是自動曝光,很多手機無效,拍照時候可以利用手電筒開啟閃光燈,拍照完畢後會關閉,當然這隻適合閃光等開關,不適合自動曝光。

7、拍照
下面是為了相容各個版本手機,特別摩托羅拉的幾款手機,閃光燈先開啟或者設定好開預覽,然後馬上拍照。這樣閃光燈可以正常使用,但部分手機的自動曝光失效了,希望知道的老鐵們,告訴小弟,如何適配或者解決自動曝光失效的問題:

  public void takePhoto(final boolean isStopPreview ,final String savePath)
    {
        if(mCameraSession == null)
            return;


        LogUtil.e("takePhoto" , "takePhoto,...............");
        this.savePath = savePath;
        try {
           if(mFlashMode == FLASH_SINGLE) {
               mPreViewBuilder.set(CaptureRequest.FLASH_MODE, CaptureRequest.FLASH_MODE_TORCH);
           }
            // 閃光燈的幾種形式,但不同的手機不一樣,通常只能生效的就是長亮 FLASH_MODE_TORCH
//            captureBuilder.set(CaptureRequest.FLASH_MODE, CaptureRequest.FLASH_MODE_SINGLE);
//              captureBuilder.set(CaptureRequest.FLASH_MODE, CaptureRequest.FLASH_MODE_TORCH);
//            captureBuilder.set(CaptureRequest.FLASH_MODE, CaptureRequest.FLASH_MODE_OFF);

//            captureBuilder.set(CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON_AUTO_FLASH_REDEYE);
//            captureBuilder.set(CaptureRequest.CONTROL_AE_MODE,CaptureRequest.CONTROL_AE_MODE_ON_ALWAYS_FLASH);
//            captureBuilder.set(CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON_AUTO_FLASH);

            mCameraSession.capture(mPreViewBuilder.build(), new CameraCaptureSession.CaptureCallback() {
                @Override
                public void onCaptureCompleted(@NonNull CameraCaptureSession session, @NonNull CaptureRequest request, @NonNull TotalCaptureResult result) {
                    if(isStopPreview) {
                        isPreview = false;
                    }
                   savePhoto(savePath + System.currentTimeMillis()+".jpg");
                    if(mFlashMode == FLASH_SINGLE) {
                        closeFlash();
                    }
                }
            }, null);

            mCameraSession.stopRepeating();

        } catch (CameraAccessException e) {
            e.printStackTrace();
        }


    }

拍照時候target為imageReader,因此拍照完畢後就會出發imageReader之前註冊的listener,然後再裡面儲存照片等一些操作

 private ImageReader.OnImageAvailableListener onImageAvailableListener = new ImageReader.OnImageAvailableListener() {
        @Override
        public void onImageAvailable(ImageReader reader) {          
            try {
                Image img = reader.acquireLatestImage();
                ByteBuffer buffer = img.getPlanes()[0].getBuffer();
                byte[] buff = new byte[buffer.remaining()];
                buffer.get(buff);
                final Bitmap bitmap = BitmapFactory.decodeByteArray(buff, 0, buff.length);

                long t1 = System.currentTimeMillis();

                Matrix matrix = new Matrix();
                if(mCameraDevice.getId().equals( 0+""))
                    matrix.postRotate(90);
                if(mCameraDevice.getId().equals( 1+""))
                   matrix.postRotate(-90);
                Bitmap bitmap2 = Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), matrix, false);


                if (takeCaptureCallback != null) {
                    takeCaptureCallback.takeCaptureSuccess(bitmap2);
                }
                bitmap.recycle();
               final File f = new File(getContext().getExternalFilesDir(null).getAbsolutePath() + "print/" + UUID.randomUUID().toString() + ".jpg");

                 saveBitmapToSD(bitmap , f.getAbsolutePath());

            } catch (Exception e) {

            } finally {
                if (reader != null) {
                    reader.close();
                }
            }
        }
    };

獲取資料,實時預覽

下面講下如何獲取相機的實時資料,這裡是通過imageReader來獲取,然後在surfaceView上繪製,大家看下面程式碼應該就明白了,如果不明白也歡迎大家留言.

 private ImageReader.OnImageAvailableListener onPreViewImageAvailableListener = new ImageReader.OnImageAvailableListener() {
        @Override
        public void onImageAvailable(ImageReader reader) {
        /**   獲得相機資料YUV420   */
            Image mImage = reader.acquireLatestImage();
            if(mImage == null) {
                Log.e(TAG, "onImageAvailable .............mImage == null");
                return;
            }
//            Log.e(TAG, "onImageAvailable ............. start");

            /**   YUV420 轉位mat ,然後用opencv轉rgb   */
            Mat mYuvMat = BitmapUtil.imageToMat(mImage);
            Mat rgbMat = new Mat(mImage.getHeight(), mImage.getWidth(), CvType.CV_8UC3);
            Imgproc.cvtColor(mYuvMat, rgbMat, Imgproc.COLOR_YUV2RGB_I420);
            /*************************更新drawBitmap,再繪製,可以達到實時濾鏡*********************/
            if(drawBitmap == null)
            {
                drawBitmap = Bitmap.createBitmap(rgbMat.width(), rgbMat.height(), Bitmap.Config.ARGB_8888);
            }

            if (isTransfer) {
                Imgproc.cvtColor(rgbMat, rgbMat, Imgproc.COLOR_RGB2GRAY);
                Utils.matToBitmap(rgbMat, drawBitmap);
            }else {
                Utils.matToBitmap(rgbMat, drawBitmap);
            }

            /*****************************更新face state***********************/
            //在changeCamera的時候切換狀態,一面繪圖時,最後一幀的時候因為cameraFace 錯誤,看到切換前一幀 運用到切換後camera的狀態
            if(mCameraID.equals(mCameraIDList[0])) {
                cameraFace = FACE_BACK;
            }
            else{
                cameraFace = FACE_FRONT;
            }
//
            /*****************************拍照狀態,儲存圖片***********************/
                // 連拍的功能,
                if(isMultipleTakePhoto && saveNumber < takeNumber){
                    Log.e(TAG, "onImageAvailable .............take some  photos");
                    savePhoto(savePath + "lammy" + System.currentTimeMillis() + ".jpg");
                    saveNumber ++;
                    if(saveNumber >= takeNumber){
                        if(mCameraSession != null && isStopPreview){
                            try {
                                mCameraSession.stopRepeating();
                            } catch (CameraAccessException e) {
                                e.printStackTrace();
                            }
                        }
                        isStopPreview = false;
                        saveNumber = 0;
                        isMultipleTakePhoto = false;
                    }
                }

//            Log.e(TAG, "onImageAvailable .............end");
            mImage.close();



            if (reader != null) {
//                    reader.close();
            }

        }
    };