02.29 Google Jetpack 新組件 CameraX 介紹與實踐

近期,Google 的 Jetpack 組件又出了新的庫:CameraX 。

顧名思義:CameraX 就是用來進行 Camera 開發的官方庫了,而且後續會有 Google 進行維護和升級。這對於廣大 Camera 開發工程師和即將成為 Camera 的程序員來說,真是個好消息~~~

CameraX 介紹

官方有給出一個示例的工程,我 fork 了之後,加入使用 OpenGL 黑白濾鏡渲染的操作,具體地址如下:

https://github.com/glumes/camera

官方並沒有提到 CameraX 庫具體如何進行 OpenGL 線程渲染的, 繼續往下看,你會找到答案的~~~

關於 CameraX 更多的介紹,建議看看 Google I/O 大會上的視頻記錄,比看文檔能瞭解更多內容~~~

https://www.youtube.com/watch?v=kuv8uK-5CLY

在視頻中提到,目前有很多應用都開始接入了 CameraX,比如 Camera360、Tik Tok 等。

Google Jetpack 新組件 CameraX 介紹與實踐

簡述 Camera 開發

關於 Camera 的開發,之前也有寫過相關的文章

Android 相機開發中的尺寸和方向問題

https://glumes.com/post/android/android-camera-aspect-ratio-and-orientation/

Android Camera 模型及 API 接口演變

https://glumes.com/post/android/android-camrea-api-evolution/

對於一個簡單能用的 Camera 應用(Demo 級別)來說,關注兩個方面就好了:預覽和拍攝。

而預覽和拍攝的圖像都受到分辨率、方向的影響。Camera 最必備的功能就是能針對預覽和拍攝提供兩套分辨率,因此就得區分場景去設置。

對於拍攝還好說一點,要獲得最好的圖像質量,就選擇同比例中分辨率最大的吧。

而預覽的圖像最終要呈現到 Android 的 Surface 上,因此選擇分辨率的時候要考慮 Surface 的寬高比例,不要出現比例不匹配導致圖像拉伸的現象。

另外,如果要做美顏、濾鏡類的應用,就要把 Camera 預覽的圖像放到 OpenGL 渲染的線程上去,然後由 OpenGL 去做圖像相關的操作,也就沒 Camera 什麼事了。等到拍攝圖片時,可以由 OpenGL 去獲取圖像內容,也可以由 Camera 獲得圖像內容,然後經過 OpenGL 做離屏處理~~~

至於 Camera 開發的其他功能,比如對焦、曝光、白平衡、HDR 等操作,不一定所有的 Camera 都能夠支持,而且也可以在上面的基礎上當做 Camera 的一個 feature 去拓展開發,並不算難事,這也是一個 Camera 開發工程師進階所要掌握的內容~~

CameraX 開發實踐

CameraX 目前的版本是 1.0.0-alpha01 ,在使用時要添加如下的依賴:

<code>1    // CameraX2    def camerax_version = "1.0.0-alpha01"3    implementation "androidx.camera:camera-core:${camerax_version}"4    implementation "androidx.camera:camera-camera2:${camerax_version}"/<code>

CameraX 向後兼容到 Android 5.0(API Level 21),並且它是基於 Camera 2.0 的 API 進行封裝的,解決了市面上絕大部分手機的兼容性問題~~~

相比 Camera 2.0 複雜的調用流程,CameraX 就簡化很多,只關心我們需要的內容就好了,不像前者得自己維護 CameraSession 會話等狀態,並且 CameraX 和 Jetpack 主打的 Lifecycle 綁定在一起了,什麼時候該打開相機,什麼時候該釋放相機,都交給 Lifecycle 生命週期去管理吧

上手 CameraX 主要關注三個方面:

  • 圖像預覽(Image Preview)
  • 圖像分析(Image analysis)
  • 圖像拍攝(Image capture)

預覽

不管是 預覽 還是 圖像分析、圖像拍攝,CameraX 都是通過一個建造者模式來構建參數 Config 類,再由 Config 類創建預覽、分析器、拍攝的類,並在綁定生命週期時將它們傳過去。

<code>1// // Apply declared configs to CameraX using the same lifecycle owner2CameraX.bindToLifecycle(3               lifecycleOwner: this, preview, imageCapture, imageAnalyzer)/<code>

既可以綁定 Activity 的 Lifecycle,也可以綁定 Fragment 的。

當需要解除綁定時:

<code>1// Unbinds all use cases from the lifecycle and removes them from CameraX.2 CameraX.unbindAll()/<code>

關於預覽的參數配置,如果你有看過之前的文章:Android 相機開發中的尺寸和方向問題 想必就會很瞭解了。

提供我們的目標參數,由 CameraX 去判斷當前 Camera 是否支持,並選擇最符合的。

<code> 1fun buildPreviewUseCase(): Preview { 2    val previewConfig = PreviewConfig.Builder() 3        // 寬高比 4        .setTargetAspectRatio(aspectRatio) 5        // 旋轉 6        .setTargetRotation(rotation) 7        // 分辨率 8        .setTargetResolution(resolution) 9        // 前後攝像頭10        .setLensFacing(lensFacing)11        .build()1213    // 創建 Preview 對象14    val preview = Preview(previewConfig)15    // 設置監聽16    preview.setOnPreviewOutputUpdateListener { previewOutput ->17        // PreviewOutput 會返回一個 SurfaceTexture18        cameraTextureView.surfaceTexture = previewOutput.surfaceTexture19    }2021    return preview22}/<code>

通過建造者模式創建 Preview 對象,並且一定要給 Preview 對象設置 OnPreviewOutputUpdateListener 接口回調。

相機預覽的圖像流是通過 SurfaceTexture 來返回的,而在項目例子中,是通過把 TextureView 的 SurfaceTexture 替換成 CameraX 返回的 SurfaceTexture,這樣實現了 TextureView 控件顯示 Camera 預覽內容。

另外,還需要考慮到設備的選擇方向,當設備橫屏變為豎屏了,TextureView 也要相應的做旋轉。

<code> 1preview.setOnPreviewOutputUpdateListener { previewOutput -> 2    cameraTextureView.surfaceTexture = previewOutput.surfaceTexture 3 4    // Compute the center of preview (TextureView) 5    val centerX = cameraTextureView.width.toFloat() / 2 6    val centerY = cameraTextureView.height.toFloat() / 2 7 8    // Correct preview output to account for display rotation 9    val rotationDegrees = when (cameraTextureView.display.rotation) {10        Surface.ROTATION_0 -> 011        Surface.ROTATION_90 -> 9012        Surface.ROTATION_180 -> 18013        Surface.ROTATION_270 -> 27014        else -> return@setOnPreviewOutputUpdateListener15    }1617    val matrix = Matrix()18    matrix.postRotate(-rotationDegrees.toFloat(), centerX, centerY)1920    // Finally, apply transformations to TextureView21    cameraTextureView.setTransform(matrix)22}/<code>

TextureView 旋轉的設置同樣在 OnPreviewOutputUpdateListener 接口中去完成。

圖像分析

在 bindToLifecycle 方法中,imageAnalyzer 參數並不是必需的。

ImageAnalysis 可以幫助我們做一些圖像質量的分析,需要我們去實現 ImageAnalysis.Analyzer 接口的 analyze 方法。

<code> 1fun buildImageAnalysisUseCase(): ImageAnalysis { 2    // 分析器配置 Config 的建造者 3    val analysisConfig = ImageAnalysisConfig.Builder() 4        // 寬高比例 5        .setTargetAspectRatio(aspectRatio) 6        // 旋轉 7        .setTargetRotation(rotation) 8        // 分辨率 9        .setTargetResolution(resolution)10        // 圖像渲染模式11        .setImageReaderMode(readerMode)12        // 圖像隊列深度13        .setImageQueueDepth(queueDepth)14        // 設置回調的線程15        .setCallbackHandler(handler)16        .build()1718    // 創建分析器 ImageAnalysis 對象19    val analysis = ImageAnalysis(analysisConfig)2021    // setAnalyzer 傳入實現了 analyze 接口的類22    analysis.setAnalyzer { image, rotationDegrees ->23        // 可以得到的一些圖像信息,參見 ImageProxy 類相關方法24        val rect = image.cropRect25        val format = image.format26        val width = image.width27        val height = image.height28        val planes = image.planes29    }3031    return analysis32}/<code>

在圖像分析器的相關配置中,有個 ImageReaderMode 和 ImageQueueDepth 的設置。

ImageQueueDepth 會指定相機管線中圖像的個數,提高 ImageQueueDepth 的數量會對相機的性能和內存的使用造成影響

其中,ImageReaderMode 有兩種模式:

  • ACQUIRE_LATEST_IMAGE
    • 該模式下,獲得圖像隊列中最新的圖片,並且會清空隊列已有的舊的圖像。
  • ACQUIRE_NEXT_IMAGE
    • 該模式下,獲得下一張圖像。

在圖像分析的 analyze 方法中,能通過 ImageProxy 類拿到一些圖像信息,並基於這些信息做分析。

拍攝

拍攝同樣有一個 Config 參數構建者類,而且設定的參數和預覽相差不大,也是圖像寬高比例、旋轉方向、分辨率,除此之外還有閃光燈等配置項。

<code> 1fun buildImageCaptureUseCase(): ImageCapture { 2    val captureConfig = ImageCaptureConfig.Builder() 3        .setTargetAspectRatio(aspectRatio) 4        .setTargetRotation(rotation) 5        .setTargetResolution(resolution) 6        .setFlashMode(flashMode) 7        // 拍攝模式 8        .setCaptureMode(captureMode) 9        .build()1011    // 創建 ImageCapture 對象12    val capture = ImageCapture(captureConfig)13    cameraCaptureImageButton.setOnClickListener {14        // Create temporary file15        val fileName = System.currentTimeMillis().toString()16        val fileFormat = ".jpg"17        val imageFile = createTempFile(fileName, fileFormat)1819        // Store captured image in the temporary file20        capture.takePicture(imageFile, object : ImageCapture.OnImageSavedListener {21            override fun onImageSaved(file: File) {22                // You may display the image for example using its path file.absolutePath23            }2425            override fun onError(useCaseError: ImageCapture.UseCaseError, message: String, cause: Throwable?) {26                // Display error message27            }28        })29    }3031    return capture32}/<code>

在圖像拍攝的相關配置中,也有個 CaptureMode 的設置。

它有兩種選項:

  • MIN_LATENCY
    • 該模式下,拍攝速度會相對快一點,但圖像質量會打折扣
  • MAX_QUALITY
    • 該模式下,拍攝速度會慢一點,但圖像質量好

OpenGL 渲染

以上是關於 CameraX 的簡單應用方面的內容,更關心的是如何用 CameraX 去做 OpenGL 渲染實現美顏。濾鏡等效果。

還記得在圖像預覽 Preview 的 setOnPreviewOutputUpdateListener 方法中,會返回一個 SurfaceTexture ,相機的圖像流就是通過它返回的。

那麼要實現 OpenGL 線程的渲染,首先就要基於 EGL 去創建 OpenGL 繪製環境,然後利用 SurfaceTexture 的 attachToGLContext 方法,將 SurfaceTexture 添加到 OpenGL 線程去。

attachToGLContext 的參數是一個紋理 ID ,這個紋理就必須是 OES 類型的紋理。

然後再把這紋理 ID 繪製到 OpenGL 對應的 Surface 上,這可以看成是兩個不同的線程在允許,一個 Camera 預覽線程,一個 OpenGL 繪製線程。

如果你不是很理解的話,建議還是看看上面提供的代碼地址:

https://github.com/glumes/camera

也可以關注我的微信公眾號 【紙上淺談】,裡面有一些關於 OpenGL 學習和實踐的文章~~~

CameraX 的拓展

如果你看了 Google I/O 大會的視頻,那肯定了解 CameraX 的拓展屬性。

在視頻中提到 Google 也正在和華為、三星、LG、摩托摩拉等廠商進行合作,為了獲得廠商系統相機的一些能力,比如 HDR 等。

不過考慮到目前的形勢,可能和華為的合作難以繼續下去了吧…

但還是期待 CameraX 能給帶來更多的新特性吧~~~

參考

  1. https://www.youtube.com/watch?v=kuv8uK-5CLY
  2. https://proandroiddev.com/android-camerax-preview-analyze-capture-1b3f403a939


分享到:


相關文章: