之前写一个Camera2的使用,今天继续介绍视频采集的另一种方式使用CameraX采集。
CameraX 是什么?
CameraX 是一个用于开发 Android 摄像头应用程序的 Jetpack 组件库。它提供了一组简单易用的 API,用于实现摄像头功能,包括预览、拍照和录制视频等操作。通过使用 CameraX,开发者可以更轻松地访问和控制设备的摄像头,并实现各种自定义的相机特性。
CameraX 可以帮助开发者解决传统 Camera API 使用上的复杂性和兼容性问题。它提供了一种一致和简化的方式来管理摄像头和图像处理流程。CameraX 还支持多种设备和系统版本,帮助开发者更好地适配不同的 Android 设备。
CameraX 提供了一套强大的功能,如自动对焦、曝光控制、图像分析和图像捕获等。开发者可以根据自己的需求选择使用这些功能,并根据需要进行自定义扩展。CameraX 还提供了一种类似于生命周期的方式来管理相机资源,确保在应用程序的不同生命周期阶段正确地打开和释放相机。
总而言之,CameraX 为开发者提供了简洁、灵活和易用的方式来实现 Android 摄像头应用程序,帮助他们更高效地开发出功能丰富、稳定可靠的相机应用。
CameraX的优点有哪些?
-
简化的 API:CameraX 提供了一组简洁、易于使用的 API,使开发者能够更轻松地实现摄像头功能。相比传统的 Camera API,CameraX 的 API 更加直观和统一,减少了开发中的复杂性和学习曲线。
-
兼容性:CameraX 支持各种设备和系统版本。它提供了对多个 Android 设备的适配,包括前置摄像头、后置摄像头、多摄像头设备等,使开发者能够在不同的设备上稳定运行应用程序。
-
生命周期感知:CameraX 采用了一种类似于生命周期的方式来管理相机资源。它与 Android 生命周期组件(如 Activity 和 Fragment)无缝集成,自动处理相机的打开和释放,确保在应用程序的不同生命周期阶段正确地管理相机资源。
-
高级功能支持:CameraX 提供了许多高级功能,如自动对焦、曝光控制、图像分析和图像捕获等。开发者可以根据需求选择使用这些功能,并进行自定义扩展,以满足特定的应用需求。
-
强大的图像分析:CameraX 提供了用于图像分析的 API,使开发者能够实现实时的图像处理和计算功能。这为开发人员提供了许多机会,可以在相机应用中实现各种智能功能,如人脸检测、物体识别等。
-
社区支持和文档丰富:CameraX 是由 Google 推出的 Jetpack 组件库之一,拥有庞大的开发者社区和丰富的文档资源。开发者可以轻松地找到示例代码、教程和解决方案,以帮助他们更好地使用和理解 CameraX。
总体而言,CameraX 的优点在于其简化的 API、兼容性、生命周期感知、高级功能支持、强大的图像分析能力以及社区支持和文档丰富,使开发者能够更快速、高效地开发出功能丰富、稳定可靠的 Android 摄像头应用程序。
CameraX如何使用?
要使用 CameraX,需要按照以下步骤进行设置和编码:
- 添加依赖项:在您的项目中的 build.gradle 文件中添加以下依赖项:
groovy
// CameraX core library
def camerax_version = '1.1.0-beta01'
implementation "androidx.camera:camera-core:$camerax_version"
// CameraX Camera2 extensions
implementation "androidx.camera:camera-camera2:$camerax_version"
// CameraX Lifecycle library
implementation "androidx.camera:camera-lifecycle:$camerax_version"
// CameraX View class
implementation "androidx.camera:camera-view:$camerax_version"
- 设置权限:在您的 AndroidManifest.xml 文件中,确保添加了适当的摄像头权限,例如:
xml
<uses-permission android:name="android.permission.CAMERA" />
- 创建预览布局:在您的布局文件中,创建一个用于显示相机预览的视图,例如:
xml
<androidx.camera.view.PreviewView
android:id="@+id/viewFinder"
android:layout_width="match_parent"
android:layout_height="match_parent" />
- 设置相机用例:在Activity 或 Fragment 中,创建一个相机用例,并将其与 PreviewView 绑定起来,例如:
kotlin
typealias LumaListener = (luma: Double) -> Unit
class CameraXTakePhotoActivity : AppCompatActivity() {
lateinit var viewFinder: PreviewView
lateinit var cameraCaptureButton: Button
lateinit var cameraSwitchButton: Button
private val TAG = "CameraXTakePhotoActivity---"
private var displayId: Int = -1
private var lensFacing: Int = CameraSelector.LENS_FACING_BACK
private var preview: Preview? = null
private var imageCapture: ImageCapture? = null
private var imageAnalyzer: ImageAnalysis? = null
private var camera: Camera? = null
private var cameraProvider: ProcessCameraProvider? = null
private lateinit var windowManager: WindowManager
private lateinit var cameraExecutor: ExecutorService
private val displayManager by lazy {
application.getSystemService(Context.DISPLAY_SERVICE) as DisplayManager
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_camera_xtake_photo)
viewFinder = findViewById(R.id.viewFinder)
cameraCaptureButton = findViewById(R.id.cameraCaptureButton)
cameraSwitchButton = findViewById(R.id.cameraSwitchButton)
// 检查相机权限
if (ContextCompat.checkSelfPermission(
this, Manifest.permission.CAMERA
) != PackageManager.PERMISSION_GRANTED
) {
// 请求相机权限
ActivityCompat.requestPermissions(
this, arrayOf(Manifest.permission.CAMERA), 1005
)
} else {
// 初始化线程池
cameraExecutor = Executors.newSingleThreadExecutor()
// 初始化WindowManager以检索显示指标
windowManager = WindowManager(this)
updateCameraUi()
lifecycleScope.launch {
setUpCamera()
}
}
}
/** 用于重新绘制相机UI控件,每次配置更改时调用。 */
private fun updateCameraUi() {
}
/** 初始化CameraX,并准备绑定相机用例 */
@SuppressLint("NewApi")
private suspend fun setUpCamera() {
cameraProvider = ProcessCameraProvider.getInstance(this).await()
lensFacing = when {
hasBackCamera() -> CameraSelector.LENS_FACING_BACK
hasFrontCamera() -> CameraSelector.LENS_FACING_FRONT
else -> throw IllegalStateException("Back and front camera are unavailable")
}
// 启用或禁用摄像头之间的切换
updateCameraSwitchButton()
// 构建并绑定相机用例
bindCameraUseCases()
}
/**
* 绑定Camera
*/
@RequiresApi(Build.VERSION_CODES.R)
private fun bindCameraUseCases() {
// 获取用于设置相机全屏分辨率的屏幕指标
val metrics = windowManager.getCurrentWindowMetrics().bounds
Log.d(TAG, "Screen metrics: ${metrics.width()} x ${metrics.height()}")
// 计算宽高比
val screenAspectRatio = aspectRatio(metrics.width(), metrics.height())
Log.d(TAG, "Preview aspect ratio: $screenAspectRatio")
//获取 PreviewView方向
val rotation = viewFinder.display.rotation
// CameraProvider
val cameraProvider = cameraProvider
?: throw IllegalStateException("Camera initialization failed.")
// CameraSelector
val cameraSelector = CameraSelector.Builder().requireLensFacing(lensFacing).build()
// Preview
preview = Preview.Builder()
// 设置宽高比
.setTargetAspectRatio(screenAspectRatio)
// 设置方向
.setTargetRotation(rotation)
.build()
// ImageCapture.Metadata 设置输出文件名和其他相关参数
val metadata = ImageCapture.Metadata().apply {
// 使用前置摄像头时,处理镜像
isReversedHorizontal =
lensFacing == CameraSelector.LENS_FACING_FRONT
}
// imageCapture 用于拍摄照片
imageCapture = ImageCapture.Builder()
// CAPTURE_MODE_MINIMIZE_LATENCY:捕获模式为最小化延迟模式,适用于快速响应和低延迟的拍摄场景。
.setCaptureMode(ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY)
// 设定宽高比
.setTargetAspectRatio(screenAspectRatio)
//设置方向
.setTargetRotation(rotation)
// .setMetadata(metadata)
.build()
// ImageAnalysis:分析相机捕获的实时预览图像数据
imageAnalyzer = ImageAnalysis.Builder()
// 设定宽高比
.setTargetAspectRatio(screenAspectRatio)
// 设置方向
.setTargetRotation(rotation)
.build()
// 将分析器分配给实例
.also {
// 在别的线程做分析 需要传入一个线程对象,和一个分析对象
it.setAnalyzer(cameraExecutor, LuminosityAnalyzer { luma ->
Log.d(TAG, "Average luminosity: $luma")
})
}
// 在重新绑定用例之前必须解除绑定
cameraProvider.unbindAll()
if (camera != null) {
// 必须从之前的相机实例中移除观察者
removeCameraStateObservers(camera!!.cameraInfo)
}
try {
// 通过 cameraProvider 绑定生命周期得到Camera对象
camera = cameraProvider.bindToLifecycle(
this, cameraSelector, preview, imageCapture, imageAnalyzer)
// 设置 surfaceProvider,这样数据就会源源不断流到 viewFinder(PreviewView)控件上了,这样就完成了预览了
preview?.setSurfaceProvider(viewFinder.surfaceProvider)
// 观察相机状态
observeCameraState(camera?.cameraInfo!!)
} catch (exc: Exception) {
Log.e(TAG, "Use case binding failed", exc)
}
}
private fun aspectRatio(width: Int, height: Int): Int {
val previewRatio = max(width, height).toDouble() / min(width, height)
if (abs(previewRatio - RATIO_4_3_VALUE) <= abs(previewRatio - RATIO_16_9_VALUE)) {
return AspectRatio.RATIO_4_3
}
return AspectRatio.RATIO_16_9
}
/**
* 切换摄像头button
*/
private fun updateCameraSwitchButton() {
try {
cameraSwitchButton.isEnabled = hasBackCamera() && hasFrontCamera()
} catch (exception: CameraInfoUnavailableException) {
cameraSwitchButton.isEnabled = false
}
}
private fun hasBackCamera(): Boolean {
return cameraProvider?.hasCamera(CameraSelector.DEFAULT_BACK_CAMERA) ?: false
}
private fun hasFrontCamera(): Boolean {
return cameraProvider?.hasCamera(CameraSelector.DEFAULT_FRONT_CAMERA) ?: false
}
private fun removeCameraStateObservers(cameraInfo: CameraInfo) {
cameraInfo.cameraState.removeObservers(this)
}
private fun observeCameraState(cameraInfo: CameraInfo) {
cameraInfo.cameraState.observe(this) { cameraState ->
run {
when (cameraState.type) {
// 即将打开
CameraState.Type.PENDING_OPEN -> {
Log.i(TAG,"CameraState: Pending Open")
}
// 正在打开 ,展示相机UI
CameraState.Type.OPENING -> {
// Show the Camera UI
Log.i(TAG,"CameraState: Opening")
}
//打开完成,设置相机资源并开始处理
CameraState.Type.OPEN -> {
Log.i(TAG,"CameraState: Open")
}
// 正在关闭,关闭CameraUI
CameraState.Type.CLOSING -> {
// Close camera UI
Log.i(TAG,"CameraState: Closing")
}
// 关闭完成,释放相机资源
CameraState.Type.CLOSED -> {
Log.i(TAG, "CameraState: Closed")
}
}
}
cameraState.error?.let { error ->
when (error.code) {
// 打开出错,确保正确地设置了用例
CameraState.ERROR_STREAM_CONFIG -> {
// Make sure to setup the use cases properly
Log.i(TAG, "Stream config error")
}
//相机正在使用
CameraState.ERROR_CAMERA_IN_USE -> {
// 关闭摄像头或要求用户关闭另一个正在使用的摄像头
Log.i(TAG, "Camera in use")
}
// 正在使用的最大摄像机
CameraState.ERROR_MAX_CAMERAS_IN_USE -> {
//关闭应用中另一个打开的摄像头,或者要求用户关闭正在使用该摄像头的另一个摄像头应用
Log.i(TAG, "Max cameras in use")
}
CameraState.ERROR_OTHER_RECOVERABLE_ERROR -> {
Log.i(TAG, "Other recoverable error")
}
// 关闭错误
CameraState.ERROR_CAMERA_DISABLED -> {
// 要求用户开启设备的摄像头
Log.i(TAG, "Camera disabled")
}
CameraState.ERROR_CAMERA_FATAL_ERROR -> {
// 要求用户重新启动设备以恢复相机功能
Log.i(TAG, "Fatal error")
}
// Closed errors
CameraState.ERROR_DO_NOT_DISTURB_MODE_ENABLED -> {
// 请用户禁用"请勿打扰"模式,然后重新打开相机
Log.i(TAG, "Do not disturb mode enabled")
}
}
}
}
}
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
/**
* 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()
}
}
companion object {
private const val FILENAME = "yyyy-MM-dd-HH-mm-ss-SSS"
private const val PHOTO_TYPE = "image/jpeg"
private const val RATIO_4_3_VALUE = 4.0 / 3.0
private const val RATIO_16_9_VALUE = 16.0 / 9.0
}
}
通过 cameraProvider 绑定生命周期得到Camera对象,设置 surfaceProvider,这样数据就会源源不断流到 viewFinder(PreviewView)控件上了,这样就完成了预览了。
拍照的实现
通过上面的实现,完成了预览功能,接下的拍照主要是通过 ImageCapture 来完成。
kotlin
cameraCaptureButton.setOnClickListener {
// ImageCapture 是用于拍摄照片的用例之一。它提供了一个简单、易于使用的 API,用于控制相机硬件以进行图像捕获操作。
imageCapture?.let { imageCapture ->
// 创建带有时间戳的名称和MediaStore
val name = SimpleDateFormat(FILENAME, Locale.US)
.format(System.currentTimeMillis())
val contentValues = ContentValues().apply {
put(MediaStore.MediaColumns.DISPLAY_NAME, name)
put(MediaStore.MediaColumns.MIME_TYPE, PHOTO_TYPE)
if(Build.VERSION.SDK_INT > Build.VERSION_CODES.P) {
val appName = resources.getString(R.string.app_name)
put(MediaStore.Images.Media.RELATIVE_PATH, "Pictures/${appName}")
}
}
// Metadata:图像捕获配置和控制的属性
val metadata = ImageCapture.Metadata().apply {
// 使用到前摄像头 镜像值为true
isReversedHorizontal =
lensFacing == CameraSelector.LENS_FACING_FRONT
}
val outputOptions = ImageCapture.OutputFileOptions
.Builder(this.contentResolver,
MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
contentValues)
.setMetadata(metadata)
.build()
// imageCapture.takePicture() 方法是用于拍摄照片的方法。使用此方法,您可以执行图像捕获操作,并将结果传递给指定的 ImageCapture.OnImageSavedCallback。
imageCapture.takePicture(
outputOptions, cameraExecutor, object : ImageCapture.OnImageSavedCallback {
override fun onError(exc: ImageCaptureException) {
Log.e(TAG, "Photo capture failed: ${exc.message}", exc)
}
/**
* 当图像成功保存到指定文件时被调用。您可以在该方法中执行任何后续操作,例如显示保存后的图像或上传到服务器等。
*/
override fun onImageSaved(output: ImageCapture.OutputFileResults) {
val savedUri = output.savedUri
Log.d(TAG, "Photo capture succeeded: $savedUri")
// Android N 以下的发送一个广播更新相册
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
@Suppress("DEPRECATION")
sendBroadcast(
//发送广播更新相册
Intent(android.hardware.Camera.ACTION_NEW_PICTURE, savedUri)
)
}
}
})
}
}
这样就完成了拍照。
旋转摄像头
更换配置后,要重新初始化。
scss
cameraSwitchButton.setOnClickListener {
lensFacing = if (CameraSelector.LENS_FACING_FRONT == lensFacing) {
CameraSelector.LENS_FACING_BACK
} else {
CameraSelector.LENS_FACING_FRONT
}
// 更新配置后,重新初始化
bindCameraUseCases()
}
使用到的一些API说明:
ProcessCameraProvider
-
生命周期管理:ProcessCameraProvider 提供了与生命周期相关的方法,可以在适当的生命周期阶段,例如 onCreate()、onStart() 或 onDestroy() 中创建、启动或释放相机资源。这确保了相机资源的正确管理和释放,避免了内存泄漏和资源浪费。
-
相机绑定:ProcessCameraProvider 可以将相机绑定到指定的预览视图上,从而实现实时预览。可以通过调用 bindToLifecycle() 方法将相机绑定到指定的生命周期所有者(LifecycleOwner)上,并指定预览视图(如 TextureView、SurfaceView 等),从而在界面上显示相机预览。
-
相机配置:ProcessCameraProvider 提供了一组配置方法,用于设置相机的参数和选项。例如,可以指定要使用的摄像头(前置或后置)、预览分辨率、图像分辨率、闪光灯模式、对焦模式等。这使得您可以根据应用需求自定义相机的行为和功能。
-
组合多个用例:ProcessCameraProvider 支持多个用例的组合。可以同时使用预览、图像捕获、图像分析等用例,并将它们一起配置和绑定到相机提供者上。这样,您可以在一个应用程序中实现多种相机功能,并灵活地进行配置。
ImageCapture
ImageCapture 是 Android Jetpack CameraX 中的一个用例,用于拍摄照片。它提供了一个简单、易于使用的 API,用于控制相机硬件并执行图像捕获操作。
ImageCapture 用例的主要功能如下:
-
拍摄照片:使用 ImageCapture 可以轻松地拍摄照片。可以调用 takePicture() 方法来触发照片捕获操作,并指定保存图像的文件和其他参数。
-
配置捕获设置:ImageCapture 提供了一系列可配置的设置,例如图片格式、闪光灯模式、焦点模式等。可以根据需求来自定义这些设置,以满足特定的应用场景。
-
监听拍摄结果:ImageCapture 提供了一个回调接口,通过实现 ImageCapture.OnImageSavedCallback 接口,可以处理图像保存成功或失败时的结果。这样,可以根据需要对保存的图像进行进一步处理或处理可能发生的错误。
-
集成图像分析:ImageCapture 可以与图像分析用例结合使用,实现更复杂的图像处理和分析功能。例如,可以在拍摄照片后,使用图像分析来检测人脸或识别物体等。
ImageAnalysis
ImageAnalysis 是 Android Jetpack CameraX 库中的一个用例,用于实时分析和处理相机捕获的图像数据。它提供了一种方便的方式来执行实时图像处理、计算和分析的操作。
ImageAnalysis 的功能包括:
-
实时图像分析:ImageAnalysis 允许对相机捕获的实时预览图像数据进行实时分析。可以使用图像分析算法来处理和提取图像中的特征、对象或信息。
-
分析器回调:通过设置分析器(Analyzer),可以定义自己的图像分析逻辑,并在每次有新的图像可用时接收回调。在回调中,可以获取图像数据,并对其进行处理、计算和分析。这使得可以实时地获取和处理相机捕获的图像,并根据需要执行相应的操作。
-
图像处理和计算:ImageAnalysis 提供了一个用于处理和计算图像数据的接口。可以使用 OpenCV、TensorFlow 等库进行图像处理、计算和机器学习操作。例如,可以执行人脸检测、图像识别、条码扫描等操作,以实现各种应用场景。
-
背压策略和图像队列:ImageAnalysis 还提供了背压策略和图像队列的支持。背压策略定义了当图像处理速度慢于图像生成速度时的处理方式,而图像队列用于存储等待分析的图像数据。可以根据实际需求配置背压策略和图像队列深度,以平衡性能和系统资源的使用。
CameraState.Type
在 Android 相机开发中,CameraState.Type 是一个枚举类,表示相机的状态类型。以下是 CameraState.Type 的一些常见类型及其含义:
-
OPENED:表示相机已经被打开并且处于活动状态。可以开始预览、拍照或录制视频。
-
CLOSED:表示相机已经关闭。不再接收预览请求或拍照请求。
-
UNINITIALIZED:表示相机未初始化。相机可能还没有被打开,或者在打开之前出现错误。
-
SURFACE_READY:表示相机使用的输出 Surface 已经准备就绪,可以开始渲染预览画面。
-
PREVIEW_STARTED:表示预览已经开始。相机正在输出实时预览画面。
-
RECORDING_STARTED:表示录制视频已经开始。相机正在输出视频帧用于录制。
ImageCapture.Metadata
ImageCapture.Metadata 是用于捕获图像时附加元数据的类。它是 CameraX 相机库中的一部分。
ImageCapture.Metadata 类提供了一些可用于图像捕获配置和控制的属性,例如闪光灯模式、曝光补偿、对焦模式、白平衡等。可以通过创建一个 ImageCapture.Metadata 对象,并通过设置相应的属性来自定义图像捕获的行为。
以下是 ImageCapture.Metadata 类的一些常见属性:
-
FlashMode:闪光灯模式,指定在图像捕获期间是否使用闪光灯。可选值有自动、关闭、打开等。
-
ExposureCompensation:曝光补偿值,用于调整图像的亮度。可以通过设置一个浮点数值来进行曝光补偿。
-
FocusMode:对焦模式,用于控制相机的对焦方式。可选值有自动对焦、连续对焦、固定对焦等。
-
WhiteBalance:白平衡模式,用于校正图像中的色温。可选值包括自动、日光、阴影、白炽灯等。
除了上述属性外,还可以使用 ImageCapture.Metadata 类来设置其他一些属性,例如图像质量、图像方向等。
总结
- 简单的介绍了 CameraX,和CameraX的优点;
- 介绍了CameraX如何实现预览、拍照和转换摄像头功能;
- 最后介绍了一些关键类,通过了解这些类后更好的实现一些功能的开发。
- CameraX 的录制和视频编码放在下一篇文章。