最後的效果如下:(實現相機預覽和顯示圖片亮度)
CameraX(主要關注三塊內容前三個內容)
- 圖像預覽(Image Preview):就是將畫面顯示在手機上
圖像分析(Image analysis):分析圖片隊列裏的圖片,比如:計算圖片亮度,深度學習的圖像識別和圖片分類等
圖像拍攝(Image capture):就是拍照片,保存到相冊
圖像觸碰(Image touch):點擊屏幕聚焦、拍攝圖片等
圖像縮放(Image zoom):放大縮小 - 這裏給一個鏈接:https://proandroiddev.com,上面的內容講的都很全,但是圖像分析那塊,還是將官方給的代碼重複一遍,沒有實際應用意義
上手第一步:添加依賴
// CameraX core library using the camera2 implementation
def camerax_version = "1.0.0-beta01"
implementation "androidx.camera:camera-camera2:${camerax_version}"
// If you want to use the CameraX View class
implementation "androidx.camera:camera-view:1.0.0-alpha08"
// If you want to use the CameraX Extensions library
implementation "androidx.camera:camera-extensions:1.0.0-alpha08"
// If you want to use the CameraX Lifecycle library
implementation "androidx.camera:camera-lifecycle:${camerax_version}"
注:這裏是有坑的,1.0.0-beta01是第一個穩定版本也是當前最新的版本,以前的alpha01到alpha10不堪回首,每一版API變動都很大,所以儘量使用穩定版…
申請權限
相機權限:<uses-permission android:name="android.permission.CAMERA" />
注:動態權限申請activity、fragment和fragment的子fragment的申請方式不一樣!!!,參考我的另一篇文章:https://blog.csdn.net/ydduong
頁面佈局
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".vision.CameraXFragment">
<androidx.camera.view.PreviewView
android:id="@+id/textureView"
android:layout_width="wrap_content"
android:layout_height="0dp"
android:layout_marginBottom="16dp"
app:layout_constraintBottom_toTopOf="@+id/textView2"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/textView2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="32dp"
android:text="TextView"
android:textSize="30sp"
app:layout_constraintBottom_toTopOf="@+id/textView3"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
注:用於顯示相機的組件,有TextureView,PreviewView等,alpha版使用TextureView,beta版使用的是PreviewView (我看官方代碼是這麼寫的,不知道爲啥)
配置相機
- 先有一個整體印象:相機的生命週期什麼的統統不用管,應爲CameraX便捷之處就是將相機的生命週期綁定在activity或者fragment的生命週期上,同時CameraX的典型用法是繼承CameraXConfig.Provider類,並重載getCameraXConfig方法,是樣板代碼。此外CameraX的功能(拍照、預覽、分析、縮放…)都是相對獨立的,需要什麼就添加什麼
- 圖片分析(這纔是本文的核心),圖片分析功能,必須要有一個分析器(也可稱之爲圖片分析類,必須要繼承ImageAnalysis.Analyzer並重載analyze方法),因爲這個分析器是沒有返回值的,但是可以傳參進去,這裏我們用回調,回調出我們要的結果,但是回調之後的結果是在後臺線程的,所以要再將數據送到主線程,最後再是顯示數據
package com.example.image3.vision
import android.Manifest
......
import kotlin.collections.ArrayList
typealias LumaListener = (luma: Double) -> Unit // 圖像分析監聽的放回類型,(typealias是取個別名)
private const val REQUEST_CODE_PERMISSIONS = 10 // 權限標識符
private val REQUIRED_PERMISSIONS = arrayOf(Manifest.permission.CAMERA) // 相機權限
// 繼承CameraXConfig.Provider,重載getCameraXConfig方法
class CameraXFragment : Fragment(), CameraXConfig.Provider {
override fun getCameraXConfig(): CameraXConfig {
return Camera2Config.defaultConfig()
}
private lateinit var viewModel: CameraXViewModel // viewModel只有一個變量:平均亮度
private lateinit var cameraProviderFuture : ListenableFuture<ProcessCameraProvider> // 相機的控制者
private lateinit var imagePreview: Preview // 圖像預覽
private lateinit var imageAnalysis: ImageAnalysis // 圖像分析
private val executor = Executors.newSingleThreadExecutor() // 後臺線程
private lateinit var previewView: PreviewView // xml裏顯示相機的組件
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return inflater.inflate(R.layout.camera_x_fragment, container, false)
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
cameraProviderFuture = ProcessCameraProvider.getInstance(requireContext())
viewModel = ViewModelProviders.of(this).get(CameraXViewModel::class.java)
previewView = textureView
// 顯示數據
viewModel.imageLight.observe(this, androidx.lifecycle.Observer {
textView2.text = it.toString()
})
// 檢查相機權限
if (allPermissionsGranted()) {
Log.v(CameraXFragment::class.java.simpleName, "已有權限,執行")
previewView.post { startCamera() }
} else {
Log.v(CameraXFragment::class.java.simpleName, "沒有權限,請求權限")
requestPermissions(REQUIRED_PERMISSIONS, REQUEST_CODE_PERMISSIONS)
}
}
// 相機操作
private fun startCamera() {
// 預覽
imagePreview = Preview.Builder().apply {
setTargetAspectRatio(AspectRatio.RATIO_16_9)
setTargetRotation(previewView.display.rotation)
}.build()
imagePreview.setSurfaceProvider(previewView.previewSurfaceProvider)
// 分析:LuminosityAnalyzer 就是分析器,回調值就是it; MainScope().launch就是主線程
imageAnalysis = ImageAnalysis.Builder().apply {
setImageQueueDepth(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
setTargetResolution(Size(224, 224))
}.build()
imageAnalysis.setAnalyzer(executor, LuminosityAnalyzer {
MainScope().launch {
viewModel.imageLight.value = it
}
Log.v(CameraXFragment::class.java.simpleName, it.toString())
})
// 綁定
val cameraSelector = CameraSelector.Builder().requireLensFacing(CameraSelector.LENS_FACING_BACK).build()
cameraProviderFuture.addListener(Runnable {
val cameraProvider = cameraProviderFuture.get()
cameraProvider.bindToLifecycle(this, cameraSelector, imagePreview, imageAnalysis)
}, ContextCompat.getMainExecutor(requireContext()))
}
// 權限請求結果回調
override fun onRequestPermissionsResult(
requestCode: Int, permissions: Array<String>, grantResults: IntArray
) {
Log.v(CameraXFragment::class.java.simpleName, "執行回調結果")
if (requestCode == REQUEST_CODE_PERMISSIONS) {
if (allPermissionsGranted()) {
textureView.post { startCamera() }
} else {
Toast.makeText(
requireContext(),
"Permissions not granted by the user.",
Toast.LENGTH_SHORT
).show()
// 這裏應該是返回上一層,或者跳轉到該頁面的時候,就先判斷有沒有權限
activity?.finish()
}
}
}
// 檢查權限
private fun allPermissionsGranted() = REQUIRED_PERMISSIONS.all {
ContextCompat.checkSelfPermission(
requireContext(), it
) == PackageManager.PERMISSION_GRANTED
}
// 平均亮度
private class LuminosityAnalyzer(listener: LumaListener? = null) : ImageAnalysis.Analyzer {
private val frameRateWindow = 8
private val frameTimestamps = ArrayDeque<Long>(5)
private val listeners = ArrayList<LumaListener>().apply { listener?.let { add(it) } }
private var lastAnalyzedTimestamp = 0L
var framesPerSecond: Double = -1.0
private set
/**
* Used to add listeners that will be called with each luma computed
*/
fun onFrameAnalyzed(listener: LumaListener) = listeners.add(listener)
/**
* Helper extension function used to extract a byte array from an image plane buffer
*/
private fun ByteBuffer.toByteArray(): ByteArray {
rewind() // Rewind the buffer to zero
val data = ByteArray(remaining())
get(data) // Copy the buffer into a byte array
return data // Return the byte array
}
/**
* Analyzes an image to produce a result.
*
* <p>The caller is responsible for ensuring this analysis method can be executed quickly
* enough to prevent stalls in the image acquisition pipeline. Otherwise, newly available
* images will not be acquired and analyzed.
*
* <p>The image passed to this method becomes invalid after this method returns. The caller
* should not store external references to this image, as these references will become
* invalid.
*
* @param image image being analyzed VERY IMPORTANT: Analyzer method implementation must
* call image.close() on received images when finished using them. Otherwise, new images
* may not be received or the camera may stall, depending on back pressure setting.
*
*/
override fun analyze(image: ImageProxy) {
// If there are no listeners attached, we don't need to perform analysis
if (listeners.isEmpty()) {
image.close()
return
}
// Keep track of frames analyzed
val currentTime = System.currentTimeMillis()
frameTimestamps.push(currentTime)
// Compute the FPS using a moving average
while (frameTimestamps.size >= frameRateWindow) frameTimestamps.removeLast()
val timestampFirst = frameTimestamps.peekFirst() ?: currentTime
val timestampLast = frameTimestamps.peekLast() ?: currentTime
framesPerSecond = 1.0 / ((timestampFirst - timestampLast) /
frameTimestamps.size.coerceAtLeast(1).toDouble()) * 1000.0
// Analysis could take an arbitrarily long amount of time
// Since we are running in a different thread, it won't stall other use cases
lastAnalyzedTimestamp = frameTimestamps.first
// Since format in ImageAnalysis is YUV, image.planes[0] contains the luminance plane
val buffer = image.planes[0].buffer
// Extract image data from callback object
val data = buffer.toByteArray()
// Convert the data into an array of pixel values ranging 0-255
val pixels = data.map { it.toInt() and 0xFF }
// Compute average luminance for the image
val luma = pixels.average()
// Call all listeners with new value
listeners.forEach { it(luma) }
image.close()
}
}
}
注:如果你要傳參到分析器內部,那麼你的回調類型,一定要放在最後一個參數的位置像這樣定義:private class LuminosityAnalyzer(st: String, listener: LumaListener? = null) : ImageAnalysis.Analyzer { }