文章目录

一、申请 Camera 权限二、用 Preview 预览摄像头三、用 ImageCamera 拍照四、用 ImageAnalysis 分析各帧五、用 VideoCapture 录像

一、申请 Camera 权限

首先,新建名为 CametaXApp 的项目,项目github代码详见

打开 CameraXApp.app 模块的 build.gradle 文件,并添加 CameraX 依赖项:

dependencies {

def camerax_version = "1.2.0-alpha04"

implementation "androidx.camera:camera-core:${camerax_version}"

implementation "androidx.camera:camera-camera2:${camerax_version}"

implementation "androidx.camera:camera-lifecycle:${camerax_version}"

implementation "androidx.camera:camera-video:${camerax_version}"

implementation "androidx.camera:camera-view:${camerax_version}"

implementation "androidx.camera:camera-extensions:${camerax_version}"

}

使用 ViewBinding,因此请使用以下代码(在 android{} 代码块末尾)启用它:

android {

buildFeatures {

viewBinding true

}

}

然后,在 res/layout/activity.xml 中 添加如下布局:

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=".MainActivity">

android:id="@+id/viewFinder"

android:layout_width="match_parent"

android:layout_height="match_parent" />

android:id="@+id/image_capture_button"

android:layout_width="110dp"

android:layout_height="110dp"

android:layout_marginEnd="50dp"

android:layout_marginBottom="50dp"

android:elevation="2dp"

android:text="@string/take_photo"

app:layout_constraintBottom_toBottomOf="parent"

app:layout_constraintEnd_toStartOf="@id/vertical_centerline" />

android:id="@+id/video_capture_button"

android:layout_width="110dp"

android:layout_height="110dp"

android:layout_marginStart="50dp"

android:layout_marginBottom="50dp"

android:elevation="2dp"

android:text="@string/start_capture"

app:layout_constraintBottom_toBottomOf="parent"

app:layout_constraintStart_toEndOf="@id/vertical_centerline" />

android:id="@+id/vertical_centerline"

android:layout_width="wrap_content"

android:layout_height="wrap_content"

android:orientation="vertical"

app:layout_constraintGuide_percent=".50" />

布局效果如下:

因为 build.gradle 中设置了 viewBinding = true,所以会为每个布局都生成对应的绑定类(即 activity_main.xml 自动生成 ActivityMainBinding 类)。

在 MainActivity.kt 中设置检查相机权限,设置 Button 的响应事件,效果如下:

package com.bignerdranch.android.cameraxapp

import android.Manifest

import android.content.pm.PackageManager

import android.os.Bundle

import androidx.appcompat.app.AppCompatActivity

import androidx.camera.core.ImageCapture

import androidx.camera.video.Recorder

import androidx.camera.video.Recording

import androidx.camera.video.VideoCapture

import androidx.core.app.ActivityCompat

import androidx.core.content.ContextCompat

import com.bignerdranch.android.cameraxapp.databinding.ActivityMainBinding

import java.util.concurrent.ExecutorService

import java.util.concurrent.Executors

typealias LumaListener = (luma: Double) -> Unit

class MainActivity : AppCompatActivity() {

private lateinit var viewBinding: ActivityMainBinding

private var imageCapture: ImageCapture? = null

private var videoCapture: VideoCapture? = null

private var recording: Recording? = null

private lateinit var cameraExecutor: ExecutorService

override fun onCreate(savedInstanceState: Bundle?) {

super.onCreate(savedInstanceState)

viewBinding = ActivityMainBinding.inflate(layoutInflater)

setContentView(viewBinding.root)

// Request camera permissions

if (allPermissionsGranted()) {

startCamera()

} else {

ActivityCompat.requestPermissions(this, REQUIRED_PERMISSIONS, REQUEST_CODE_PERMISSIONS)

}

// Set up the listeners for take photo and video capture buttons

viewBinding.imageCaptureButton.setOnClickListener { takePhoto() }

viewBinding.videoCaptureButton.setOnClickListener { captureVideo() }

cameraExecutor = Executors.newSingleThreadExecutor()

}

private fun takePhoto() {}

private fun captureVideo() {}

private fun startCamera() {}

private fun allPermissionsGranted() = REQUIRED_PERMISSIONS.all {

ContextCompat.checkSelfPermission(baseContext, it) == PackageManager.PERMISSION_GRANTED

}

override fun onDestroy() {

super.onDestroy()

cameraExecutor.shutdown()

}

companion object {

private const val TAG = "CameraXApp"

private const val FILENAME_FORMAT = "yyyy-MM-dd-HH-mm-ss-SSS"

private const val REQUEST_CODE_PERMISSIONS = 10

private val REQUIRED_PERMISSIONS =

mutableListOf(Manifest.permission.CAMERA, Manifest.permission.RECORD_AUDIO).apply { }.toTypedArray()

}

}

在 AndroidManifest.xml 中申请摄像头权限,其中 android.hardware.camera.any 可确保设备配有相机。指定 .any 表示它可以是前置摄像头,也可以是后置摄像头。配置如下:

android:maxSdkVersion="28" />

在 MainActivity 添加如下函数,会根据用户批准的权限,执行对应的回调函数,代码如下:

override fun onRequestPermissionsResult(requestCode: Int, permissions: Array, grantResults: IntArray) {

super.onRequestPermissionsResult(requestCode, permissions, grantResults)

if (requestCode == REQUEST_CODE_PERMISSIONS) {

if (allPermissionsGranted()) {

startCamera()

} else {

Toast.makeText(this, "Permissions not granted by the user", Toast.LENGTH_SHORT).show()

finish()

}

}

}

运行后,会请求用户权限,效果如下:

二、用 Preview 预览摄像头

在 MainActivity 中实现 startCamera() 函数,代码如下:

private fun startCamera() {

// 用于将相机的生命周期绑定到生命周期所有者(MainActivity)。 这消除了打开和关闭相机的任务,因为 CameraX 具有生命周期感知能力。

val cameraProviderFuture = ProcessCameraProvider.getInstance(this)

// 向 cameraProviderFuture 添加监听器。添加 Runnable 作为一个参数。我们会在稍后填写它。添加 ContextCompat.getMainExecutor() 作为第二个参数。这将返回一个在主线程上运行的 Executor。

cameraProviderFuture.addListener({

// 将相机的生命周期绑定到应用进程中的 LifecycleOwner。

val cameraProvider: ProcessCameraProvider = cameraProviderFuture.get()

val preview = Preview.Builder().build().also { it.setSurfaceProvider(viewBinding.viewFinder.surfaceProvider) } // preview 作为 usecase

val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA

try {

cameraProvider.unbindAll() // Unbind use cases before rebinding

cameraProvider.bindToLifecycle(this, cameraSelector, preview) // Bind use cases to camera: 把 cameraSelector 和 preview 绑定

} catch (exc: Exception) {

Log.e(TAG, "Use case binding failed", exc) // 有多种原因可能会导致此代码失败,例如应用不再获得焦点。在此记录日志。

}

}, ContextCompat.getMainExecutor(this))

}

运行后,效果如下:

三、用 ImageCamera 拍照

在 MainActivity 中实现 takePhoto() 函数,代码如下:

private fun takePhoto() {

// Get a stable reference of the modifiable image capture use case

val imageCapture = imageCapture ?: return

// 存图路径和参数(时间、文件类型)

val name = SimpleDateFormat(FILENAME_FORMAT, Locale.US).format(System.currentTimeMillis())

val contentValues = ContentValues().apply {

put(MediaStore.MediaColumns.DISPLAY_NAME, name)

put(MediaStore.MediaColumns.MIME_TYPE, "image/jpeg")

}

// 我们希望将输出保存在 MediaStore 中,以便其他应用可以显示它

val outputOptions =

ImageCapture.OutputFileOptions.Builder(contentResolver, MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentValues).build()

// 拍照后的回调函数

imageCapture.takePicture(outputOptions, ContextCompat.getMainExecutor(this),

object : ImageCapture.OnImageSavedCallback {

override fun onError(exc: ImageCaptureException) {

Log.e(TAG, "Photo capture failed: ${exc.message}", exc)

}

override fun onImageSaved(outputFileResults: ImageCapture.OutputFileResults) {

val msg = "Photo capture succeeded: ${outputFileResults.savedUri}"

Toast.makeText(baseContext, msg, Toast.LENGTH_SHORT).show()

Log.d(TAG, msg)

}

})

}

在 MainActivity 的 startCamera() 函数中,添加如下 imageCapture = ImageCapture.Builder().build() 来初始化摄像头的 use case,并绑定到 cameraProvider.bindToLifecycle() 中,完整代码如下。

private fun startCamera() {

// 用于将相机的生命周期绑定到生命周期所有者(MainActivity)。 这消除了打开和关闭相机的任务,因为 CameraX 具有生命周期感知能力。

val cameraProviderFuture = ProcessCameraProvider.getInstance(this)

// 向 cameraProviderFuture 添加监听器。添加 Runnable 作为一个参数。我们会在稍后填写它。添加 ContextCompat.getMainExecutor() 作为第二个参数。这将返回一个在主线程上运行的 Executor。

cameraProviderFuture.addListener({

// 将相机的生命周期绑定到应用进程中的 LifecycleOwner。

val cameraProvider: ProcessCameraProvider = cameraProviderFuture.get()

val preview = Preview.Builder().build().also { it.setSurfaceProvider(viewBinding.viewFinder.surfaceProvider) } // preview 作为 usecase

imageCapture = ImageCapture.Builder().build()

val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA

try {

cameraProvider.unbindAll() // Unbind use cases before rebinding

cameraProvider.bindToLifecycle(this, cameraSelector, preview, imageCapture) // Bind use cases to camera

} catch (exc: Exception) {

Log.e(TAG, "Use case binding failed", exc) // 有多种原因可能会导致此代码失败,例如应用不再获得焦点。在此记录日志。

}

}, ContextCompat.getMainExecutor(this))

}

运行后,效果如下:

可以查看照片信息,经验证时间和图片名称确实设置成功,效果如下:

四、用 ImageAnalysis 分析各帧

使用 ImageAnalysis 功能可让相机应用变得更加有趣。它允许定义实现 ImageAnalysis.Analyzer 接口的自定义类,并使用传入的相机帧调用该类。我们无需管理相机会话状态,甚至无需处理图像;与其他生命周期感知型组件一样,仅绑定到应用所需的生命周期就足够了。

为 MainActivity 添加 LuminosityAnalyzer 内部类,代码如下:

private class LuminosityAnalyzer(private val listener: LumaListener) : ImageAnalysis.Analyzer {

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

}

override fun analyze(image: ImageProxy) {

val buffer = image.planes[0].buffer

val data = buffer.toByteArray()

val pixels = data.map { it.toInt() and 0xFF }

val luma = pixels.average()

listener(luma)

image.close()

}

}

然后,在 startCamera() 函数中,实例化 imageAnalyzer 对象,通过 setAnalyzer() 设置其回调函数来打印 luma(亮度),并绑定到 cameraProvider.bindToLifecycle() 上,代码如下:

private fun startCamera() {

// 用于将相机的生命周期绑定到生命周期所有者(MainActivity)。 这消除了打开和关闭相机的任务,因为 CameraX 具有生命周期感知能力。

val cameraProviderFuture = ProcessCameraProvider.getInstance(this)

// 向 cameraProviderFuture 添加监听器。添加 Runnable 作为一个参数。我们会在稍后填写它。添加 ContextCompat.getMainExecutor() 作为第二个参数。这将返回一个在主线程上运行的 Executor。

cameraProviderFuture.addListener({

// 将相机的生命周期绑定到应用进程中的 LifecycleOwner。

val cameraProvider: ProcessCameraProvider = cameraProviderFuture.get()

val preview = Preview.Builder().build().also { it.setSurfaceProvider(viewBinding.viewFinder.surfaceProvider) } // preview 作为 usecase

imageCapture = ImageCapture.Builder().build()

val imageAnalyzer = ImageAnalysis.Builder().build().also {

it.setAnalyzer(cameraExecutor, LuminosityAnalyzer { luma ->

Log.d(TAG, "Average luminosity: $luma")

})

}

val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA

try {

cameraProvider.unbindAll() // Unbind use cases before rebinding

cameraProvider.bindToLifecycle(this, cameraSelector, preview, imageCapture, imageAnalyzer) // Bind use cases to camera

} catch (exc: Exception) {

Log.e(TAG, "Use case binding failed", exc) // 有多种原因可能会导致此代码失败,例如应用不再获得焦点。在此记录日志。

}

}, ContextCompat.getMainExecutor(this))

}

其实是通过 LuminosityAnalyzer.analyze() 函数内的 listener(luma) 将 luma 参数传给 listener() 函数,然后我们通过 setAnalyzer() 自定义了 listener() 函数,其接收亮度,并通过 Logcat 打印。

运行后,App 会在 Logcat 中,每帧图像均打印亮度,日志如下:

五、用 VideoCapture 录像

在 MainActivity 中实现 captureVideo() 函数如下:

private fun captureVideo() {

val videoCapture = this.videoCapture ?: return

viewBinding.videoCaptureButton.isEnabled = false

val curRecording = recording

if (curRecording != null) {

curRecording.stop()

recording = null

return

}

// create and start a new recording session

val name = SimpleDateFormat(FILENAME_FORMAT, Locale.US).format(System.currentTimeMillis())

val contentValues = ContentValues().apply {

put(MediaStore.MediaColumns.DISPLAY_NAME, name)

put(MediaStore.MediaColumns.MIME_TYPE, "video/mp4")

}

val mediaStoreOutputOptions = MediaStoreOutputOptions

.Builder(contentResolver, MediaStore.Video.Media.EXTERNAL_CONTENT_URI)

.setContentValues(contentValues)

.build()

recording = videoCapture.output

.prepareRecording(this, mediaStoreOutputOptions)

.apply {

if (PermissionChecker.checkSelfPermission(

this@MainActivity, Manifest.permission.RECORD_AUDIO

) == PermissionChecker.PERMISSION_GRANTED

) {

withAudioEnabled()

}

}

.start(ContextCompat.getMainExecutor(this)) { recordEvent ->

when (recordEvent) {

is VideoRecordEvent.Start -> {

viewBinding.videoCaptureButton.apply {

text = getString(R.string.stop_capture)

isEnabled = true

}

}

is VideoRecordEvent.Finalize -> {

if (!recordEvent.hasError()) {

val msg = "Video capture succeeded: ${recordEvent.outputResults.outputUri}"

Toast.makeText(baseContext, msg, Toast.LENGTH_SHORT).show()

Log.d(TAG, msg)

} else {

recording?.close()

recording = null

Log.e(TAG, "Video capture ends with error: ${recordEvent.error}")

}

viewBinding.videoCaptureButton.apply {

text = getString(R.string.start_capture)

isEnabled = true

}

}

}

}

}

然后,在 MainActivity 的 startCamera() 函数中,将 videoCapture 绑定到 cameraProvider.bindToLifecycle() 函数中,因为camera 同时只能绑定3种use case,所以本节在拍照、摄像、预览、分析中,选择了前3种用途,代码如下:

private fun startCamera() {

// 用于将相机的生命周期绑定到生命周期所有者(MainActivity)。 这消除了打开和关闭相机的任务,因为 CameraX 具有生命周期感知能力。

val cameraProviderFuture = ProcessCameraProvider.getInstance(this)

// 向 cameraProviderFuture 添加监听器。添加 Runnable 作为一个参数。我们会在稍后填写它。添加 ContextCompat.getMainExecutor() 作为第二个参数。这将返回一个在主线程上运行的 Executor。

cameraProviderFuture.addListener({

// 将相机的生命周期绑定到应用进程中的 LifecycleOwner。

val cameraProvider: ProcessCameraProvider = cameraProviderFuture.get()

val preview = Preview.Builder().build().also { it.setSurfaceProvider(viewBinding.viewFinder.surfaceProvider) } // preview 作为 use case

imageCapture = ImageCapture.Builder().build()

// val imageAnalyzer = ImageAnalysis.Builder().build().also {

// it.setAnalyzer(cameraExecutor, LuminosityAnalyzer { luma ->

// Log.d(TAG, "Average luminosity: $luma")

// })

// }

val recorder = Recorder.Builder().setQualitySelector(QualitySelector.from(Quality.HIGHEST)).build()

videoCapture = VideoCapture.withOutput(recorder)

val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA

try {

cameraProvider.unbindAll() // Unbind use cases before rebinding

cameraProvider.bindToLifecycle(this, cameraSelector, preview, imageCapture, videoCapture) // Bind use cases to camera

} catch (exc: Exception) {

Log.e(TAG, "Use case binding failed", exc) // 有多种原因可能会导致此代码失败,例如应用不再获得焦点。在此记录日志。

}

}, ContextCompat.getMainExecutor(this))

}

运行后,可预览、拍照和录像,效果如下:

好文链接

评论可见,请评论后查看内容,谢谢!!!
 您阅读本篇文章共花了: