CameraX
是 Google
为了简化 Android
的 camera
开发而提供的一个库. 这个库包含在 Jetpack
中. 在 2019 Google IO 大会
上发布了 alpha
版本, 并且在今年发布了 beta
版本.
本文介绍使用 CameraX
来预览摄像头图像, 拍照并分析来自相机的图像流
新建项目
打开 Android Studio
新建一个 Android
项目. (语言选择 Kotlin
)
添加 Gradle 依赖
打开 build.gradle(Module: app)
文件, 添加 CameraX
的依赖:
1 | implementation fileTree(dir: "libs", include: ["*.jar"]) |
CameraX
需要一些 Java 8
的方法, 所以在 buildTypes
里要设置编译参数:
1 | compileOptions { |
修改完成后点击同步, 就可以在 app
中调用 CameraX
的依赖了.
创建布局
先创建一个 values/strings.xml
文件:
1 | <resources> |
然后打开 layout/activity_main.xml
, 删除其中的 <Text/>
标签, 加上一个 Button
和一个 androidx.camera.view.PreviewView
(预览摄像头画面)
修改后代码如下:
1 |
|
设置 MainActivity.kt
用下面的骨架代码替换 MainActivity.kt
原有代码:
1 | import androidx.appcompat.app.AppCompatActivity |
骨架包括导入语句, 将要实例化的变量, 将要实现的函数和常量. onCreate()
已经实现了检查相机权限, 启动相机功能, 按钮的监听onClickListener()
, 并实现 outputDirectory
和 cameraExecutor
. 目前相机也将无法工作, 需要在之后实现那些方法.
现在的程序运行后可以看到:
获得摄像头访问权限
在调用摄像头前, 需要获得摄像头访问权限
声明权限
首先打开 AndroidManifest.xml
然后把下面的代码粘贴在 application
标签前面:
1 | <uses-feature android:name="android.hardware.camera.any" /> |
第一行使用 android.hardware.camera.any
可确保该设备具有摄像头, 而 .any
则意味着它可以是前置摄像头或后置摄像头.如果你没有使用 .any
, 马尔如果你在没有后置摄像头的设备上运行程序就不会工作
第二行才是真正 app
添加了访问该摄像机的权限
判断权限申请状态
首先修改 allPermissionsGranted()
方法, 在骨架代码中这个方法默认返回 false
, 重写这个方法:
1 | private fun allPermissionsGranted() = REQUIRED_PERMISSIONS.all { |
在 MainActivity.kt
中重写 onRequestPermissionsResult
来获取权限请求的结果:
1 | override fun onRequestPermissionsResult( |
上面的代码首先使用:
1 | if (requestCode == REQUEST_CODE_PERMISSIONS) { |
调用上面修改的 allPermissionsGranted()
方法来检查请求代码是否正确, 如果不正确则忽略. 如果正确则检查用户是否授予了权限.
如果授权则启动摄像头:
1
2
3if (allPermissionsGranted()) {
startCamera()
}如果未授予权限就用
Tosat
通知用户未授予权限:1
2
3
4
5
6else {
Toast.makeText(this,
"Permissions not granted by the user.",
Toast.LENGTH_SHORT).show()
finish()
}
现在重新运行程序, 首次打开的时候会在应用内请求访问摄像头的权限:
显示摄像头预览图像
在得到访问摄像头的权限后, 会调用 startCamera()
方法来启动摄像头, 这里在 startCamera()
方法实现获取摄像头实例并把摄像头预览图像显示出来:
1 | private fun startCamera() { |
首先创建一个 ProcessCameraProvider
的实例:
1 | val cameraProviderFuture = ProcessCameraProvider.getInstance(this) |
这用于绑定摄像机的生命周期. CameraX
库接管了摄像头的生命周期, 因此使用的时候不需要关心打开和关闭相机.
然后在 cameraProviderFuture
添加一个监听:
第一个参数是 Runnable {}
, 第二个参数是 ContextCompat.getMainExecutor()
. ContextCompat.getMainExecutor()
将会返回一个在主线程上运行的 Executor
1 | cameraProviderFuture.addListener(Runnable {}, ContextCompat.getMainExecutor(this)) |
在上面地一个参数 Runnable
中, 添加 ProcessCameraProvider
, 用于把摄像头的生命周期绑定到应用程序的 LifecycleOwner
:
1 | val cameraProvider: ProcessCameraProvider = cameraProviderFuture.get() |
上面的代码已经实现了托管摄像头的生命周期, 接下来要实现摄像头画面预览就只需要实例化一个 Preview
对象, 并绑定到摄像头的生命周期内即可. 首先初始化一个 Preview
对象:
1 | preview = Preview.Builder().build() |
然后创建一个 CameraSelector
对象,然后使用它的 CameraSelector.Builder.requireLensFacing
方法调用你想访问的摄像头(CameraSelector.LENS_FACING_BACK
表示后置摄像头)
1 | val cameraSelector = CameraSelector.Builder().requireLensFacing(CameraSelector.LENS_FACING_BACK).build() |
最后编写一个 try-catch
代码块. 在该代码块内, 先使用 unbindAll
确保没有任何内容绑定到 cameraProvider
, 然后将前面的 cameraSelector
和预览对象 unbindAll
绑定到 cameraProvider
. 再将 viewFinder
的 SurfaceProvider
设置为 preview
的 SurfaceProvider
:
1 | try { |
运行程序, 你就可以看到摄像头捕获的预览画面:
实现图像捕获的功能
在骨架代码中, 预留了一个按钮用于捕获图片. 在按钮点击后会运行 takePhoto()
方法, 所以只需要在该方法实现图像捕获的业务逻辑即可.
实现 takePhoto()
方法
首先获取 ImageCapture
对象, 如果获取不到则直接返回:
1 | val imageCapture = imageCapture ?: return |
然后创建一个 File
对象用于指定储存一会捕获到的图片的位置和文件名:
1 | val photoFile = File( |
创建一个 OutputFileOptions
对象用于制定输出. 这里需要将输出保存在我们刚刚创建的文件中, 因此刚才创建的 photoFile
:
1 | val outputOptions = ImageCapture.OutputFileOptions.Builder(photoFile).build() |
最后就可以调用 imageCapture
对象的 takePicture()
方法. 传入 outputOptions
, 执行程序 {...}
和保存图像的 callback
方法:
1 | imageCapture.takePicture( |
如果图像捕获失败或保存图像捕获失败:
1 | override fun onError(exc: ImageCaptureException) { |
如果图像捕获成功, 则照片保存到之前创建的文件中, 并打印保存路径:
1 | override fun onImageSaved(output: ImageCapture.OutputFileResults) { |
最后 takePhoto()
完整的代码如下:
1 | private fun takePhoto() { |
创建 ImageCapture
对象并绑定到摄像头生命周期
前面图像捕获的功能用到了 ImageCapture
, 所以之前的 Preview
一样, 要用相同的步骤把 ImageCapture
也绑定到生命周期:
在 startCamera()
方法的创建 Preview
代码下添加创建 ImageCapture
的代码:
1 | imageCapture = ImageCapture.Builder() |
然后在 cameraProvider.bindToLifecycle
中把上面的创建的 ImageCapture
实例绑定到摄像头生命周期上:
1 | camera = cameraProvider.bindToLifecycle(this, cameraSelector, preview, imageCapture) |
现在运行程序, 点击按钮就会保存捕获图片, 并且你可以在本地图库找到保存的图片.
实现图像分析功能
上面介绍了 CameraX
库进行摄像头预览和摄像头图片捕获的功能, 最后一个图像分析的功能和上面过程类似. 也是创建一个图像分析的对象, 然后绑定到摄像头生命周期里.
添加平均亮度分析类
首先在 MainActivity
里添加一个内部类 LuminosityAnalyzer
用于分析图像的平均亮度:
1 | private class LuminosityAnalyzer(private val listener: LumaListener) : ImageAnalysis.Analyzer { |
创建 ImageAnalysis
对象并绑定到摄像头生命周期
图像分析对象的绑定方法和画面预览, 图像捕获对象绑定方法一样, 都是首先实例化一个对象. 在 startCamera()
方法中, 在实例化 ImageCapture
对象下面添加实例化 ImageAnnalysis
对象的代码:
1 | imageAnalyzer = ImageAnalysis.Builder() |
实例化 ImageAnnalysis
对象的时候, 传入了一个 LuminosityAnalyzer
对象用于分析i并打印图片平均亮度的结果.
最后在 cameraProvider.bindToLifecycle
方法里添加实例化的 ImageAnalysis
对象:
1 | camera = cameraProvider.bindToLifecycle(this, cameraSelector, preview, imageCapture, imageAnalyzer) |
运行程序, 可以看到 logcat
中输出图片分析的结果: