1. 程式人生 > >Android Camera2 開發實踐指南

Android Camera2 開發實踐指南

我們知道 Android 中相機開發是有兩套 API 可以使用的,一個是 Camera,這個適用於 Android 5.0 以下,另外一個是 Camera2,這個適用於 Android 5.0 以上。但是這僅僅是系統的建議,其實開發中由於國內廠商對 Camera2 的支援程度各不相同,即便是 5.0 以上的手機,也可能對 Camera2 支援非常差的情況,我們可能還得降級使用 Camera 來開發。

 

使用 Camera2 開發會涉及到一些系統方法的呼叫,我們需要大概瞭解一下他們的作用。

1.相機的管理主要由以下兩個類提供:

CameraManager:相機管理類,可以獲取相機個數,以及開啟或關閉相機等操作。

CameraCharacteristics:獲取相機的配置引數,比如獲取相機支援的拍攝解析度大小、ISO範圍、曝光時間等,系統提供了大概78個配置選項。

2.相機的預覽和拍攝主要由下面的類管理:

CameraDevice:這個相當於是開啟相機後當前攝像頭的表示,相機開發後會傳入一個CameraDevice,我們可以使用此類來建立與相機的連線。

CameraCaputreSession:由CameraDevice配置好後產生的session,用於處理相機預覽或者是拍照等處理,就相當於是已經建立連線了,然後現在通過這個CameraCaptureSession處理與相機進行對話。

CaptureRequest:控制本次獲取影象的配置,比如配置圖片的ISO,對焦方式和曝光時間等。

CaptureResult:描述拍照完成後的結果。

ImageReader:可以用這個類來做簡單的捕獲影象處理。

3.展示預覽影象可以使用 SurfaceView 或 TextureView:

SurfaceView:介面渲染可以放在單獨執行緒,自身不能支援使用動畫。

TextureView:只能在擁有硬體加速層層的Window繪製,效能不如SurfaceView,並且可能丟幀,但是可以做一些動畫效果。

兩者詳細差別分析可以參考:https://groups.google.com/a/chromium.org/forum/#!topic/graphics-dev/Z0yE-PWQXc4

 

Camera2 開發的整個流程就上面介紹的那樣:

先請求相機許可權
使用 CameraManager找到你想要的攝像頭,前置或是後置。
通過 CameraCharacteristics 獲取相機的配置資訊,方便之後調整相機的各種引數。
通過 CameraManager 開啟相機,得到當前的 CameraDevice。
通過 CameraDevice 建立本次會話,得到本次會話的 CameraCaptureSession。
使用 CaptureRequest 設定獲取圖片的引數資訊,設定到 CameraCaptureSession 中。
在 ImageReader 或 CaptureResult 處理得到的圖片。
關閉相機(不關閉有可能會導致相機資源佔用,導致別的相機無法正常開啟)
 

開發之前先來了解一些相機的配置介紹:

AE:Automatic Exposure(自動曝光)。

AF:Auto Focus(自動對焦)。

ISO:International Orization for Standardization,這個是國際標準化組織,由於照相機的感光度最終由這個組織釋出,所以稱為感光度ISO值。

EV:Exposure value(曝光值)

F:F-number(光圈)

我們通過自己設定這些引數可以拍出非常有意思的照片,下面說一下 Android 為我們提供的與攝像頭互動的一些類,以及如何配置自己的相機引數來開發相機。

下面將使用 TextureView 結合 Camera2 API 製作一個簡單的相機預覽

1. 在TextureView可用狀態時開啟相機。

    //監聽view的事件
    mTextureView.surfaceTextureListener = this
    
    override fun onSurfaceTextureAvailable(surface: SurfaceTexture?, width: Int, height: Int) {
        //可用的時候會回調當前方法,width是此view的寬,height是高。
        openCamera(width,height)
    }
2.首先在 openCamera 中檢查是否有相機許可權

    private fun openCamera(width : Int,height : Int) {
        //判斷是否有相機許可權
        if (ContextCompat.checkSelfPermission(activity!!,         Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED) {
            requestCameraPermission()
            return
        }
        //設定要使用的攝像頭以及獲取攝像頭的配置
        setUpCameraOutputs(width, height)
        //開啟攝像頭
        waitOpen()
    }
 
    //申請許可權
    private fun requestCameraPermission() {
        requestPermissions(arrayOf(Manifest.permission.CAMERA), REQUEST_CAMERA_PERMISSION)
    }
3.配置相關屬性

    private fun setUpCameraOutputs(width: Int, height: Int) {
        val activity = act
        val manager = activity.getSystemService(Context.CAMERA_SERVICE) as CameraManager
        manager.cameraIdList.forEach { cameraId ->
            val characteristics = manager.getCameraCharacteristics(cameraId)
 
            val facing = characteristics.get(CameraCharacteristics.LENS_FACING)
            //這裡我們不使用前置攝像頭
            if (facing != null && facing == CameraCharacteristics.LENS_FACING_FRONT) {
                return@forEach
            }
            //得到相機支援的流配置(包括支援的圖片解析度等),不支援就返回
            val map = characteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP)
                    ?: return@forEach
            //獲取支援的iso範圍
            val isoRange = characteristics.get(CameraCharacteristics.SENSOR_INFO_SENSITIVITY_RANGE)
            //得到最高和最低值
            if (isoRange != null) {
                val isoMin = isoRange.lower
                val isoMax = isoRange.upper
            }
            //獲取支援的影象曝光時間範圍,單位納秒
            val timeRange = characteristics.get(CameraCharacteristics.SENSOR_INFO_EXPOSURE_TIME_RANGE)
            //得到最大最小值
            if (timeRange != null) {
                val timeMin = timeRange.lower
                val timeMax = timeRange.upper
            }
 
            //為了方便,這裡我們獲取支援攝像頭支援的最大尺寸來儲存
            //我們直接使用了 JPEG 的圖片格式,android 支援的圖片格式可以檢視ImageFormat這個類
            val largest = Collections.max(
                    Arrays.asList(*map.getOutputSizes(ImageFormat.JPEG)),
                    CompareSizesByArea())
            //建立一個用於獲取攝像頭圖片的 ImageReader,最多接受 2 個
            mImageReader = ImageReader.newInstance(largest.width, largest.height, ImageFormat.JPEG, 2)
            //監聽接受到圖片的事件
            mImageReader?.setOnImageAvailableListener(mOnImageAvailableListener, mHandler)
            //檢查是否支援 flash
            mFlashSupported = characteristics.get(CameraCharacteristics.FLASH_INFO_AVAILABLE) ?: false
            //得到當前的攝像頭id
            mCameraId = cameraId
        }
    }
4.根據 id 開啟相機

    private fun waitOpen() {
        val activity = act
        val manager = activity.getSystemService(Context.CAMERA_SERVICE) as CameraManager
        if (mCameraId == null){
            //沒有找到符合的攝像頭
            return
        }
        manager.openCamera(mCameraId,mStateCallback,mHandler)
    }
 
    //在這裡得到開啟相機的各種回撥
    private val mStateCallback : CameraDevice.StateCallback = object : CameraDevice.StateCallback(){
        override fun onOpened(camera: CameraDevice) {
            //得到當前攝像頭
            mCameraDevice = camera
            //建立預覽請求
            createCameraPreviewSession()
        }
 
        override fun onDisconnected(camera: CameraDevice) {
        }
 
        override fun onError(camera: CameraDevice, error: Int) {
        }
    }
5.建立預覽請求

    private fun createCameraPreviewSession(){
        //獲取view的surface,這裡的寬高應該是獲取適合攝像頭當前的寬高,這裡為了方便就直接使用螢幕寬高了
        val texture = mTextureView.surfaceTexture
        texture.setDefaultBufferSize(act.getWidth(),act.getHeight())
        val surface = Surface(texture)
        //構建預覽請求
        mPreviewRequestBuilder = mCameraDevice?.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW)
        mPreviewRequestBuilder?.addTarget(mImageReader?.surface)
        mPreviewRequestBuilder?.addTarget(surface)
        //建立預覽會話
        mCameraDevice?.createCaptureSession(listOf(surface, mImageReader?.surface),mSessionCallBack,null)
    }
   //會在mSessionCallBack得到我們本次的session
    private val mSessionCallBack : CameraCaptureSession.StateCallback = object : CameraCaptureSession.StateCallback(){
        override fun onConfigureFailed(session: CameraCaptureSession) {}
 
        override fun onConfigured(session: CameraCaptureSession) {
            if (mCameraDevice == null){
                return
            }
            //得到本次的會話類
            mCaptureSession= session
            //設定為自動對焦的會話預覽
           mPreviewRequestBuilder?.set(CaptureRequest.CONTROL_AF_MODE,CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE)
            //將imageReader新增進來即可在此類中得到預覽的圖片
            mPreviewRequestBuilder?.addTarget(mImageReader?.surface)
            mPreviewRequest = mPreviewRequestBuilder?.build()
            //設定為連續請求
            mCaptureSession?.setRepeatingRequest(mPreviewRequest,mCaptureCallback,mHandler)
        }
    }
這裡還可以設定別的模式,比如設定可以自己調節ISO大小,值的大小可以根據從相機配置讀取的引數範圍設定

    /**
     * 設定ios感光度以及曝光時間
     * ae 自動曝光 Automatic Exposure
     * af 自動對焦 Auto Focus
     * @param iso 靈敏度
     * @param exposure 曝光時間
     * @param frame 幀持續時間
     */
    private fun setIsoMode(iso: Int, exposure: Long, frame: Long) {
            //禁用所有自動設定
            //            mPreviewRequestBuilder.set(CaptureRequest.CONTROL_MODE, CaptureRequest.CONTROL_MODE_OFF);
            //只是禁用曝光,白平衡繼續開啟,自己設定iso等值,必須禁用曝光
            mPreviewRequestBuilder?.set(CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_OFF)
            mPreviewRequestBuilder?.set(CaptureRequest.SENSOR_EXPOSURE_TIME, exposure)
            mPreviewRequestBuilder?.set(CaptureRequest.SENSOR_SENSITIVITY, iso)
            mPreviewRequestBuilder?.set(CaptureRequest.SENSOR_FRAME_DURATION, frame)
            mPreviewRequestBuilder?.addTarget(mImageReader?.surface)
            //需要預覽的話改成這個並且新增到下面的createCaptureSession裡
            mPreviewRequest = mPreviewRequestBuilder?.build()
            mCaptureSession?.setRepeatingRequest(mPreviewRequest,
                    mCaptureCallback, mHandler)
    }
6.在 ImageReader 中處理圖片結果

    //這個是剛剛為mImageReader新增的回撥介面
    private val mOnImageAvailableListener = (ImageReader.OnImageAvailableListener { reader ->
        //獲取下一張圖片
        val image = reader.acquireNextImage()
        //這裡的planes大小和所選的圖片格式有關,比如YUV_444就有三個通道
        val buffer = image.planes[0].buffer
        val bytes = ByteArray(buffer.remaining())
        val bitmap = BitmapFactory.decodeByteArray(bytes,0,bytes.size)
        //使用完記得回收
        image.close()
    })
7.操作完之後就記得釋放相機資源

    private fun closeCamera(){
        if (mCaptureSession != null) {
            mCaptureSession?.close()
            mCaptureSession = null
        }
        if (mCameraDevice != null) {
            mCameraDevice?.close()
            mCameraDevice = null
        }
        if (mImageReader != null) {
            mImageReader?.close()
            mImageReader = null
        }
    }<