• Kotlin 集成 Camera2 API 在安卓上,非常棒


        今天,我们将要去看一下怎么使用安卓创意集成的 Camera2 API 去得到一个集成相机。具体来说,我们将尽最大努力去用 SurfaceView 展示我们输入源的一张图片,和同时建立 ImageReader 处理来自相同来源的单独的帧。
        
        正如你所知,在Android平台上构建东西时,需要考虑很多未知因素。有这么多不同的设备类型,而且无法预测该设备的服务和硬件会是什么样子。这就是为什么我们需要与API接口,为我们解决这些未知值,我们需要安全地处理代码中的所有事件。Camera2让这一切变得简单。

        因此,当您在某个设备上运行应用程序时,您可能想询问的第一件事是,我可以使用哪种设备?该设备是否同时具有正面和背面摄像头?它支持哪些决议?我们使用CameraManager回答这些问题。

        顺便说一下,我已经开始了一个新的、空的项目,我将从那里开始在MainActivity中构建。使用此API不需要任何外部依赖项。        

    需要权限

            实际上,让我们回想一下。首先我们需要请求许可才能使用相机。这里有一个帮助您完成此操作的助手:

    1. /** 请求相机权限助手 */
    2. object CameraPermissionHelper {
    3. private const val CAMERA_PERMISSION_CODE = 0
    4. private const val CAMERA_PERMISSION = Manifest.permission.CAMERA
    5. /** 请检查我们是否拥有此应用程序所需的权限。 */
    6. fun hasCameraPermission(activity: Activity): Boolean {
    7. return ContextCompat.checkSelfPermission(activity, CAMERA_PERMISSION) == PackageManager.PERMISSION_GRANTED
    8. }
    9. /** 请检查我们是否拥有此应用程序所需的权限,如果没有,请求他们。 */
    10. fun requestCameraPermission(activity: Activity) {
    11. ActivityCompat.requestPermissions(
    12. activity, arrayOf(CAMERA_PERMISSION), CAMERA_PERMISSION_CODE)
    13. }
    14. /** 检查是否需要显示此许可的理由。 */
    15. fun shouldShowRequestPermissionRationale(activity: Activity): Boolean {
    16. return ActivityCompat.shouldShowRequestPermissionRationale(activity, CAMERA_PERMISSION)
    17. }
    18. /** 启动应用程序设置以授予权限 */
    19. fun launchPermissionSettings(activity: Activity) {
    20. val intent = Intent()
    21. intent.action = Settings.ACTION_APPLICATION_DETAILS_SETTINGS
    22. intent.data = Uri.fromParts("package", activity.packageName, null)
    23. activity.startActivity(intent)
    24. }
    25. }

    实现这个回调

    1. override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
    2. if (!CameraPermissionHelper.hasCameraPermission(this)) {
    3. Toast.makeText(this, "Camera permission is needed to run this application", Toast.LENGTH_LONG)
    4. .show()
    5. if (!CameraPermissionHelper.shouldShowRequestPermissionRationale(this)) {
    6. // 检查“不再询问”时权限被拒绝。
    7. CameraPermissionHelper.launchPermissionSettings(this)
    8. }
    9. finish()
    10. }
    11. recreate()
    12. }

    确保你加入了这一行在 Manifest 文件中

    "android.permission.CAMERA" />
    

    然后调用

    1. override fun onCreate(savedInstanceState: Bundle?) {
    2. super.onCreate(savedInstanceState)
    3. setContentView(R.layout.activity_main)
    4. if (!CameraPermissionHelper.hasCameraPermission(this)) {
    5. CameraPermissionHelper.requestCameraPermission(this)
    6. return
    7. }
    8. }

    使用CameraManager访问设备

    1. private fun startCameraSession() {
    2. val cameraManager = getSystemService(Context.CAMERA_SERVICE) as CameraManager
    3. if (cameraManager.cameraIdList.isEmpty()) {
    4. // 没有相机
    5. return
    6. }
    7. val firstCamera = cameraIdList[0]
    8. cameraManager.openCamera(firstCamera, object: CameraDevice.StateCallback() {
    9. override fun onDisconnected(p0: CameraDevice) { }
    10. override fun onError(p0: CameraDevice, p1: Int) { }
    11. override fun onOpened(cameraDevice: CameraDevice) {
    12. // 使用相机
    13. val cameraCharacteristics = cameraManager.getCameraCharacteristics(cameraDevice.id)
    14. cameraCharacteristics[CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP]?.let { streamConfigurationMap ->
    15. streamConfigurationMap.getOutputSizes(ImageFormat.YUV_420_888)?.let { yuvSizes ->
    16. val previewSize = yuvSizes.last()
    17. }
    18. }
    19. }
    20. }, Handler { true })
    21. }

            在这里,我们使用 manager 抓取列表中的第一个相机。然后我们调用 CameraManager.openCamera(),传递 id 以访问该相机设备。一旦成功打开,我们将调用CameraManager.getCameraCharacteristics以查询该相机设备的详细信息。如果您想更详细地了解要使用哪台相机,可以在打开相机之前调用 getCameraCharacteristics 来检查它是否具有所需的属性。

            在本例中,我检索相机的 StreamConfigurationMap ,其中包含有关设备支持的输出格式的所有信息。接下来,我获取YUV_420_888格式(如果存在),因为这是我的项目所需要的。您可以根据需要使用其他格式。我对此不太了解。然后,我从该列表中获取最后一个大小,这将是本示例所需的最低分辨率大小。

           下一步,我们需要做一些新人可能不会想到的至关重要的事情。我们需要检查Android设备的方向和摄像头输出的数据的方向是否互换!设备可能是纵向的,但相机仍然根据相机传感器输出横向的图像。Camera2示例项目提供了一种方法来确定是否存在这种情况: 

    1. private fun areDimensionsSwapped(displayRotation: Int, cameraCharacteristics: CameraCharacteristics): Boolean {
    2. var swappedDimensions = false
    3. when (displayRotation) {
    4. Surface.ROTATION_0, Surface.ROTATION_180 -> {
    5. if (cameraCharacteristics.get(CameraCharacteristics.SENSOR_ORIENTATION) == 90 || cameraCharacteristics.get(CameraCharacteristics.SENSOR_ORIENTATION) == 270) {
    6. swappedDimensions = true
    7. }
    8. }
    9. Surface.ROTATION_90, Surface.ROTATION_270 -> {
    10. if (cameraCharacteristics.get(CameraCharacteristics.SENSOR_ORIENTATION) == 0 || cameraCharacteristics.get(CameraCharacteristics.SENSOR_ORIENTATION) == 180) {
    11. swappedDimensions = true
    12. }
    13. }
    14. else -> {
    15. // 显示旋转无效
    16. }
    17. }
    18. return swappedDimensions
    19. }

    然后返回 startCameraSession:

    1. val previewSize = yuvSizes.last()
    2. // 账户.
    3. val displayRotation = windowManager.defaultDisplay.rotation
    4. val swappedDimensions = areDimensionsSwapped(displayRotation, cameraCharacteristics)
    5. // 如果需要交换长和宽
    6. val rotatedPreviewWidth = if (swappedDimensions) previewSize.height else previewSize.width
    7. val rotatedPreviewHeight = if (swappedDimensions) previewSize.width else previewSize.height

    设置 SurfaceView

            此时,我们准备好与 SurfaceView 集成。要做到这一点其实很简单。首先,我们用  SurfaceView 替换自动生成的“Hello World!”文本视图

    1. "1.0" encoding="utf-8"?>
    2. <android.support.constraint.ConstraintLayout
    3. xmlns:android="http://schemas.android.com/apk/res/android"
    4. xmlns:tools="http://schemas.android.com/tools"
    5. xmlns:app="http://schemas.android.com/apk/res-auto"
    6. android:layout_width="match_parent"
    7. android:layout_height="match_parent"
    8. tools:context=".MainActivity">
    9. <SurfaceView
    10. android:id="@+id/surfaceView"
    11. android:layout_width="match_parent"
    12. android:layout_height="match_parent"
    13. app:layout_constraintBottom_toBottomOf="parent"
    14. app:layout_constraintEnd_toEndOf="parent"
    15. app:layout_constraintStart_toStartOf="parent"
    16. app:layout_constraintTop_toTopOf="parent" />
    17. android.support.constraint.ConstraintLayout>

            我们已经有了相机设备特性给出的输出大小,所以我们只需将 holder 上的预览固定大小设置为输出大小:

    surfaceView.holder.setFixedSize(rotatedPreviewWidth, rotatedPreviewHeight)

    现在我们只需要连接输入源,即我们的相机设备和 surface。我们使用CameraCaptureSession.CaptureCallback()执行此操作:

    1. val previewSurface = surfaceView.holder.surface
    2. val captureCallback = object : CameraCaptureSession.StateCallback()
    3. {
    4. override fun onConfigureFailed(session: CameraCaptureSession) {}
    5. override fun onConfigured(session: CameraCaptureSession) {
    6. // 会话已配置
    7. val previewRequestBuilder = cameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW)
    8. .apply {
    9. addTarget(previewSurface)
    10. }
    11. session.setRepeatingRequest(
    12. previewRequestBuilder.build(),
    13. object: CameraCaptureSession.CaptureCallback() {},
    14. Handler { true }
    15. )
    16. }
    17. }
    18. cameraDevice.createCaptureSession(mutableListOf(previewSurface), captureCallback, Handler { true })

            如果你试着运行这个,一切都会好起来的。但是你不会看到任何预览出现…这是因为我们还没有调用startCameraSession()!事实上,它并没有那么简单,因为SurfaceView必须异步准备自己,即使在“活动”是交互式的之后,它也可能没有准备好。
            所以我们需要一个回调:

    1. val surfaceReadyCallback = object: SurfaceHolder.Callback {
    2. override fun surfaceChanged(p0: SurfaceHolder?, p1: Int, p2: Int, p3: Int) { }
    3. override fun surfaceDestroyed(p0: SurfaceHolder?) { }
    4. override fun surfaceCreated(p0: SurfaceHolder?) {
    5. startCameraSession()
    6. }
    7. }

            回到onCreate(),访问Kotlin启用的同一合成视图引用:

    surfaceView.holder.addCallback(surfaceReadyCallback)

            现在,当你运行时,你应该会看到一个漂亮的小预览…

     使用ImageReader处理图像

            让我们快速完成向应用程序添加另一个输出源的脚手架:ImageReader。Camera2最酷的地方是它可以将任意数量的输入连接到各种输出。因此,我们可以显示预览并处理来自以下来源的图像:

    1. //账户
    2. surfaceView.holder.setFixedSize(rotatedPreviewWidth, rotatedPreviewHeight)
    3. // 配置图像读取器
    4. val imageReader = ImageReader.newInstance(rotatedPreviewWidth, rotatedPreviewHeight,
    5. ImageFormat.YUV_420_888, 2)
    6. imageReader.setOnImageAvailableListener({
    7. // 做一些事
    8. }, Handler { true })

    我们配置 ImageReader 和 surface view 用相同的图像格式

    ImageReader将其数据渲染到Surface,我们可以直接访问该Surface:

    1. // cont.
    2. val previewSurface = surfaceView.holder.surface
    3. val recordingSurface = imageReader.surface

            将其添加到我们的回调中。通过这样做,我们确保对于进入预览的每个图像,也会将其发送到 ImageReader:

    1. // cont.
    2. val previewRequestBuilder = camera.createCaptureRequest(TEMPLATE_PREVIEW).apply {
    3. addTarget(previewSurface)
    4. addTarget(recordingSurface)
    5. }

    并将其添加到我们session中的surfaces列表中:

    1. // cont.
    2. cameraDevice.createCaptureSession(mutableListOf(previewSurface, recordingSurface), captureCallback, Handler { true })

            生成的图像将是一个包含输入图像缓冲区的对象,如果您愿意,可以使用层和所有您需要的东西来分解和重新组合图像的每一位。
            就是这样!我希望这简化了您理解此API的过程。请务必查看主要来源,尤其是样本项目,以获得进一步的澄清。还有一个关于我使用上述代码的应用程序的快速简介:
            KanjiReader是一个图像处理应用程序,可以让您即时翻译日语汉字,并查看其含义和读数的读数。​​​​​​​

    实现效果:

    Androidhttps://play.google.com/store/apps/details?id=tylerwalker.io.kanjireader

    iOS‎Kanji-Reader on the App Store

    参考文章1

    引用

    1. ​​​​​​​Camera2 Documentation
    2. Camera2 Sample Project

    文章来源

  • 相关阅读:
    el-switch组件在分页情况下的使用
    【Mysql】查询mysql的版本
    python学习笔记(2)—— 控制流
    【VRTK】【VR开发】【Unity】7-配置交互能力和向量追踪
    Java8与JDK1.8与JDK8之间的关系是什么?
    flask基础开发知识学习
    java版本企业电子招标采购系统源码Spring Cloud + Spring Boot +二次开发
    高等数学(第七版)同济大学 总习题六 个人解答
    声网首席科学家钟声:感知实时互联网
    探索DrissionPage:结合浏览器自动化与数据包操控的先进工具
  • 原文地址:https://blog.csdn.net/qq_61735602/article/details/127588820