2019年的Google开发者大会上,Google发布了CameraX包,这个包是Camera1和Camera2的升级版。之前的Camera1由于性能差被大家诟病,Camera2作为升级版在拓展性上有了很大的提升,但是Api太多太难用,对于开发者来说属实头大。CameraX作为大哥推出,它不仅消除了前两个包在使用上的障碍,还与LifeCycler相结合,这样系统就可以自动管理相机的生命周期,我们就不需要去考虑什么时候释放相机。
配置CameraX
CameraX 由两个概念来完成实现 -- Camera View 和 Camera Core。Camera View 可被单独用于处理基本的相机要求,比如拍照,录视频,生命周期管理以及相机切换等。而核心库能够搭配 Camera View 处理更复杂的 CameraX 实现(比如在当前的相机上下文提供一个取景器)。
-
CameraView
CameraView 给开发者提供了方法,使他们不需要太多困难就可以在 app 里提供基础的 camear 实现。我们能够在布局文件里直接添加这个组件。这个 CameraView 类是一个 ViewGroup,本质上包含了一个 TextureView 来显示 camera 流,以及配置这个组件的一些属性。
这些 xml 属性既可以在布局文件里设置,也可以在代码里设置。所以,如果你想提供 UI 控件控制上面这些属性 ,你可以使用 ClickListener 来设置这些属性。
既然我们是在 Activity 里布局的 CameraView,我们可以用 CameraView 的 bindToLifeCycle 方法将这个 View 与当前组件的生命周期绑定。
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
...
view_camera.bindToLifecycle(this)
}
}
现在我们有了一个准备好了的简单的 CameraView 来捕获媒体。需要说明的是,CameraView 不能被单独扩展来提供更多的功能。CameraView 的目标是提供一个简化的可以方便地以 View 的形式使用的相机实现。如果你想要实现更多的功能,你需要使用 CameraX Core 库,我们将在另一篇文章里聊到它。
完成了上面的配置,那就能够打开相机并且在屏幕上看到预览了。CameraView 提供了一些当用户操作 UI 时我们可以触发的方法。
当要使用拍照功能时,takePicture 方法可以从相机捕获图片。这里我们需要提供一个图片数据保存位置的文件引用,以及一个在图片成功保存或者出现错误时使用的 Listener。
camera_view.takePicture(File("some_file_path"),
object : ImageCaptureUseCase.OnImageSavedListener { override fun onImageSaved(file: File) {
// 处理被保存的图片
}
override fun onError( error: ImageCaptureUseCase.UseCaseError,
message: String,
throwable: Throwable? ) {
// 处理错误
}
})
takePicture 还有另一种形式,这种形式只使用一个 OnImageCaptureListener 回调参数。这个回调用来监听图片被捕捉(或者出现了错误),然后开发者可以根据情况处理结果数据。前面的 takePicture 使用更简单,但这个 takePicture 提供了更多的灵活性。
camera_view.takePicture(
object : ImageCaptureUseCase.OnImageCapturedListener() {
override fun onCaptureSuccess(
image: ImageProxy,
rotationDegrees: Int
) {
// 处理捕捉的图片
}
override fun onError(
useCaseError: ImageCaptureUseCase.UseCaseError?,
message: String?,
cause: Throwable?
) {
// 处理图片捕获错误
}
})
也可以使用 CameraView 来录视频。这时候我们需要使用 startRecoring() 方法—只需要传递一个用来保存结果的文件引用,以及一个 来处理操作结果(成果或者失败)的 listener。
camera_view.startRecording(
File("some_file_path"),
object : VideoCaptureUseCase.OnVideoSavedListener {
override fun onVideoSaved(file: File?) {
// Handle video saved
}
override fun onError(
error: VideoCaptureUseCase.UseCaseError?,
message: String?,
throwable: Throwable?
) {
// Handle video error
}
})
可以看到,onVideSaved 方法给我们返回一个被保存的视频数据的文件实例。我们也有 onError 方法用来处理错误状态,在我们的 UI 上根据情况 作出对应的反馈。
希望停止拍摄视频时,我们只需要调用 **stopRecording **方法让用例 停止拍摄。
最后,当我们使用 CameraView 完毕后,我们必须通过调用CameraX.unBindAll()确保解绑相机,释放被用到的资源。
-
Camera Core
核心库像我们带来了所谓用例的概念,这些用例实现特定的功能,可以用来简化常见摄像机要求的实现过程,在CameraX中已经有集中不同的用例实现:
1.预览 - 用于为相机预览准备查找器。可以在Context中多次绑定。
2.图像捕获 - 用于低延迟图像捕获,只能在Context中绑定一次。
3.图像分析 - 用于对图像执行分析,可以在Context中多次绑定。
用例配置
每个用例类采用Config实例的形式进行配置,此接口用于定义一组通用功能,这些功能用于每个用例的每个子类。如果跳转到 Config 文件的源文件,会注意到有很多特定于库的定义。Config 类用于保存相应用例的配置详细信息的选项和值的集合,可以通过用例类操作这些值。
-
预览用例 Preview
这个用例可以用于在SurfaceTextureView中提供当前摄像机流的预览 - 然后,此视图可以连接到相应的TextureView,以在屏幕上显示摄像机内容。因此,首先需要向布局添加TextureView,以便能够容纳预览中的内容:
<TextureView android:id="@+id/preview" android:layout_width="match_parent" android:layout_height="match_parent" />
在实现用例之前,我们将配置一些将用于查找器的选项。此配置采用Preview Config类的形式,我们将使用其Builder来配置用例的一些选项:
val previewConfig = PreviewConfig.Builder().apply { setLensFacing(CameraX.LensFacing.FRONT) // configure other options }.build()
在配置预览实例时,我们可以应用许多选项:
- setTargetName() – 设置用于标识配置的唯一名称。
- setTargetResolution() – 以预览的最小边界区域的形式设置目标分辨率。
- setLensFacing() – 以LensFaceing值的形式设置用于取景的镜头,前置或后置。
- setTargetAspectRatio() – 采用一个Rational实例定义用于图像的纵横比。
- setTargetRotation() – 设置屏幕方向。
注意上述方式在最新的alpha09中已经废弃,需要通过perView.Builder()来构建,不需要使用previewConfig了
- setCallbackHandler() – 提供一个处理CallBack的Handler。
之后通过定义的PreViewConfig创建Preview类的新实例,并设置给实例一个OnPreviewOutputUpdateListener监听。
val previewUseCase = Preview(previewConfig)
previewUseCase.onPreviewOutputUpdateListener = Preview.OnPreviewOutputUpdateListener {
val viewGroup = view_texture.parent as ViewGroup
viewGroup.removeView(view_texture)
viewGroup.addView(view_texture)
view_texture.surfaceTexture = it.surfaceTexture
}
在上面的实例中可以看到将TextureView的surfaceTexture 指向布局中额度surfaceTexture 。这个surfaceTexture 包含相机源中的内容,因此在这里我们只是重新路由此输出以在Activity/Fragment中显示。removeView(view_texture)之后又addView(view_texture)是为了可以再次布局内容确保可以正确地显示。
我们在onPreviewOutputUpdateListener的回调中收到的 PreviewOutput是一组数据,通过提供的接口方法检索:
-
getSurfaceTexture() – 返回包含指定图像数据的SurfaceTexture实例
-
getTextureSize() –返回指定的SurfaceTexture的大小
-
getRotationDegrees() – 返回一个int值表示SurfaceTexture的旋转值
通过阅读源码,会注意到在OnPreviewOutputUpdateListener 中回调了下面的updateTransform方法:
private fun updateTransform() {
val matrix = Matrix()
// Compute the center of the view finder
val centerX = viewFinder.width / 2f
val centerY = viewFinder.height / 2f
// Correct preview output to account for display rotation
val rotationDegrees = when(viewFinder.display.rotation) {
Surface.ROTATION_0 -> 0
Surface.ROTATION_90 -> 90
Surface.ROTATION_180 -> 180
Surface.ROTATION_270 -> 270
else -> return
}
matrix.postRotate(-rotationDegrees.toFloat(), centerX, centerY)
// Finally, apply transformations to our TextureView
viewFinder.setTransform(matrix)
}
这段代码用于在设备方向更改时允许补偿,确保取景器保持直立位置。可以看到在代码中得到取景器的中心,利用取景器接受其显示属性的当前旋转值,然后使用上述计算的旋转来变换取景器。
虽然这有助于我们考虑设备上的这些方向更改,但是使用预览用例时我们需要考虑其他事项。在某些情况下,可能需要180的设备旋转,有的时候甚至会使用非方型的取景器,其中横纵比例会根据设备的方向发生变化。此时需要通过updateTransform()方法来处理这些需求:
private fun updateTransform(textureView: TextureView?, rotation: Int?, newBufferDimens: Size, newViewFinderDimens: Size) {
val textureView = textureView ?: return
if (rotation == viewFinderRotation &&
Objects.equals(newBufferDimens, bufferDimens) &&
Objects.equals(newViewFinderDimens, viewFinderDimens)) {
// Nothing has changed, no need to transform output again
return
}
if (rotation == null) {
// Invalid rotation - wait for valid inputs before setting matrix
return
} else {
// Update internal field with new inputs
viewFinderRotation = rotation
}
if (newBufferDimens.width == 0 || newBufferDimens.height == 0) {
// Invalid buffer dimens - wait for valid inputs before setting matrix
return
} else {
// Update internal field with new inputs
bufferDimens = newBufferDimens
}
if (newViewFinderDimens.width == 0 || newViewFinderDimens.height == 0) {
// Invalid view finder dimens - wait for valid inputs before setting matrix
return
} else {
// Update internal field with new inputs
viewFinderDimens = newViewFinderDimens
}
val matrix = Matrix()
// Compute the center of the view finder
val centerX = viewFinderDimens.width / 2f
val centerY = viewFinderDimens.height / 2f
// Correct preview output to account for display rotation
matrix.postRotate(-viewFinderRotation!!.toFloat(), centerX, centerY)
// Buffers are rotated relative to the device's 'natural' orientation: swap width and height
val bufferRatio = bufferDimens.height / bufferDimens.width.toFloat()
val scaledWidth: Int
val scaledHeight: Int
// Match longest sides together -- i.e. apply center-crop transformation
if (viewFinderDimens.width > viewFinderDimens.height) {
scaledHeight = viewFinderDimens.width
scaledWidth = Math.round(viewFinderDimens.width * bufferRatio)
} else {
scaledHeight = viewFinderDimens.height
scaledWidth = Math.round(viewFinderDimens.height * bufferRatio)
}
// Compute the relative scale value
val xScale = scaledWidth / viewFinderDimens.width.toFloat()
val yScale = scaledHeight / viewFinderDimens.height.toFloat()
// Scale input buffers to fill the view finder
matrix.preScale(xScale, yScale, centerX, centerY)
// Finally, apply transformations to our TextureView
textureView.setTransform(matrix)
}
-
updateTransform()可以帮助我们确保显示取景器内容。这个方法和之前的一个updateTransform()采用了类似的过程:
1.检索取景器的尺寸和旋转
2.计算取景器的中心
3.根据计算的旋转角度旋转内容
4.根据每个x轴和y轴的计算比例缩放取景器内容
之后可以在OnPreviewOutputUpdateListener中调用任意的updateTransform()方法然后将生命周期绑定到application中。
- 图像捕获 Image Capture
ImageCapture用例可以用于使用设备摄像机捕获图像,这个用例将拍摄照片并提供图像数据,我们只需要处理这些数据即可。和前面一样,先创建ImageCapture Config实例的形式用于ImageCapture
val imageCaptureConfig = ImageCaptureConfig.Builder().apply {
setFlashMode(FlashMode.AUTO)
}.build()
在配置ImageCapture Config实例时,可以应用许多选项:
-
setFlashMode() - 使用FlashMode的值设置图像捕获的闪存状态,可以选择AUTO,ON,OFF
-
setLensFacing() - 设置用于捕获图像的摄像头,可以是FRONT或BACK
-
setCaptureMode() -设置图像捕获期间画面质量/延迟的优先级。这可以设置为MAX_QUAILTY(画质优先)或者MIN_LATENCY(低延迟)
-
setTargetAspectRatio() - 传入一个Rational实例,该实例将用于为通过此配置捕获的图像分配纵横比。
-
setTargetRotation() - 传入一个Surface的旋转值,该值将用于设置通过此配置捕获的图像旋转值。可以设置为Surface.ROTATION_0, Surface.ROTATION_90, Surface.ROTATION_180 or Surface.ROTATION_270。当设置这个值时,可以使用ViewFinder TextureView中的Display实例。这个Display持有一个rotation的引用,该属性可用于设置ImageCapture用例的目标旋转角度。
之后需要创建一个ImageCaptureUseCase的实例,传入刚刚定义的配置,和以前一样,我们将绑定到当前Context的生命周期。
val imageCaptureUseCase = ImageCapture(imageCaptureConfig)
CameraX.bindToLifecycle(this, imageCaptureUseCase)
至此图像捕获的用例就已经定义完成并且可供使用,可以继续使用它捕获图像。在这里可以调用takePicture方法,可以用于从相机捕获图像,需要传入一个文件引用路径,表示捕获的图像数据将传到哪里。还要传入一个映像保存成功或发生错误的监听器。
imageCaptureUseCase.takePicture(File("some_file_path"),
object : ImageCaptureUseCase.OnImageSavedListener {
override fun onImageSaved(file: File) {
// Handle image saved
}
override fun onError(
error: ImageCapture.UseCaseError,
message: String,
throwable: Throwable?
) {
// Handle image error
}
})
这个方法存在一个替代的方法,可以传递一个ImageCapture.MetaData类的实例作为最后的参数,可以传递一些有关图像的额外详细信息:
location:有关图片地理位置的详细信息
isReveredHorizontal:表示图像是否水平反转
isReveredVerticial: 表示图像是否垂直反转
最后还有一种takePicture()方法,采用OnImageCaptureListener的实例作为唯一参数,可以用于监听捕获图像的时机或是否发生错误,然后处理相应的结果。虽然之前的takePicture()方法更简单,但是这种方法提供了更多的灵活性。
imageCaptureUseCase.takePicture(object : ImageCapture.OnImageCapturedListener() {
override fun onCaptureSuccess(
image: ImageProxy,
rotationDegrees: Int
) {
// Handle image captured
}
override fun onError(
error: ImageCapture.UseCaseError?,
message: String?,
cause: Throwable?
) {
// Handle image capture error
}
})
这种方法意味着和之前的方法不同,图像不会保存。完成后即可将相机绑定到LifeCycler。
CameraX.bindToLifecycle(this, viewFinderUseCase)
- 图像分析 Image Analysis
图像分析用例可用于对相机源中显示的图像进行分析,这个用例允许我们从它扩展以便于创建我们自己的分析类。从而允许我们对摄像机介质执行特定的分析操作,与前面的用例一样,需要首先创建Config实例。
val imageAnalysisConfig = ImageAnalysisConfig.Builder().apply {
setImageReaderMode(
ImageAnalysis.ImageReaderMode.ACQUIRE_LATEST_IMAGE)
}.build()
- setImageReaderMode() - 设置用于从媒体队列获取图像进行分析的方法,可以取ACQUIRE_LATEST_IMAGE(使用媒体队列中的最新图像,同时丢弃任何比最新图像旧的图像),ACQUIRE_NEXT_IMAGE (使用媒体队列中下一个图像)。
- setImageQueueDepth() - 设置媒体队列的长度
- setLensFacing() 设置前置或后置相机。LensFacing_Facing / LensFacing_BACK。
- setCallBackHandler() 提供一个handler用于处理回调
对于图像分析,必须创建自己的分析类,这个类从ImageAnalysis.Analyzer接口扩展,CameraX提供了一个analyze()方法 ,这个方法是我们必须要重写的一个方法,比如分析图像的亮度:
private class LuminosityAnalyzer : ImageAnalysis.Analyzer {
private var lastAnalyzedTimestamp = 0L
private fun ByteBuffer.toByteArray(): ByteArray {
rewind()
val data = ByteArray(remaining())
get(data)
return data
}
override fun analyze(image: ImageProxy, rotationDegrees: Int) {
val currentTimestamp = System.currentTimeMillis()
if (currentTimestamp - lastAnalyzedTimestamp >=
TimeUnit.SECONDS.toMillis(1)) {
val buffer = image.planes[0].buffer
val data = buffer.toByteArray()
val pixels = data.map { it.toInt() and 0xFF }
val luma = pixels.average()
Log.d("CameraXApp", "Average luminosity: $luma")
lastAnalyzedTimestamp = currentTimestamp
}
}
}
创建好Anayzer之后可以将其指定给相机
val imageAnalysisUseCase = ImageAnalysis(imageAnalysisConfig).apply {
analyzer = MyAnalyser()
}
之后将取景器绑定到应用程序当前的生命周期即可。
绑定多个用例
使用 CameraX 核心库时,我们不妨使用多个用例。例如,我们可能希望捕获图像,并针对捕获的媒体执行分析。为此,我们可以简单地定义用例并将它们全部绑定到当前生命周期:
CameraX.bindToLifecycle(this, imageCaptureUseCase, imageAnalysisUseCase)
网友评论