当前位置: 首页 > article >正文

Android --- CameraX讲解

预备知识

surface   surfaceView   SurfaceHolder

surface 是什么?

一句话来说: surface是一块用于填充图像数据的内存。

surfaceView 是什么?

它是一个显示surface 的View。 在app中仍在 ViewHierachy 中,但在wms 中可以理解为Server端,它与宿主窗口是分离的。(它拥有独立的绘制表面 , 它不与其它宿主窗口共享一个绘制表面,可以单独在一个线程进行绘制,并不会占用主线程的资源,这样,绘制就会比较高效,游戏、视频播放,还有最近几年比较热门的直播,都可以用surface  )

surfaceHolder 是什么?

 它是一个接口,给持有surface的对象使用。可以控制surface的大小和格式。编辑surface中的像素,以及监听surface的变化这个接口通过surfaceView获取。

CameraX

CameraX是Jetpack 支持库

特点:

1、利用的是 camera2 的功能

2、它具有生命周期的感应,使用更加简单,代码量也减少了不少。可以灵活的录制视频和拍照。

3、抹平设备兼容问题。

使用

第一步:

引入依赖

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
	}
}

第二步:

添加布局控件。   androidx.camera.view.PreViewView

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    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">

    <androidx.camera.view.PreviewView
        android:id="@+id/viewFinder"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

    <Button
        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" />

    <Button
        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" />

    <androidx.constraintlayout.widget.Guideline
        android:id="@+id/vertical_centerline"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:orientation="vertical"
        app:layout_constraintGuide_percent=".50" />

</androidx.constraintlayout.widget.ConstraintLayout>

因为 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<Recorder>? = 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 表示它可以是前置摄像头,也可以是后置摄像头。配置如下:

<uses-feature android:name="android.hardware.camera.any" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
   android:maxSdkVersion="28" />

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

    override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, 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()
            }
        }
    }

第四步:

预览 摄像头、 拍照、 图片分析

初始化cameraProviderFuture 并得到cameraProvider 。

得到cameraProvider 后初始化 PreView 、ImageCapture 、ImageAnalysis。  可以看到三个单独配置的。 所以完全解耦。

 tips:

PreView: 它是相机预览的数据流图像数据通过它来传输,比如输出到surface中。

ImageCapture : 用于拍照 这里的builder 都可以设置参数,比如 大小、曝光度。

ImageAnalysis : 用于图像分析,如二维码识别、人脸等。

预览 摄像头

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 接口的自定义类,并使用传入的相机帧调用该类。我们无需管理相机会话状态,甚至无需处理图像;与其他生命周期感知型组件一样,仅绑定到应用所需的生命周期就足够了。

    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 打印。

用 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))
    }

​​​​​​​

结束。

参考: 

https://juejin.cn/post/7354922285092847668

https://juejin.cn/post/7354937037391986722

使用Preview预览和自定义SurfaceView或者TextureView实现CameraX预览功能_preview.setsurfaceprovider-CSDN博客

Android之SurfaceView和TextureView的分析_surfaceview和textview的区别-CSDN博客​​​​​​​
 

在Android CameraX中,PreviewView和SurfaceView的关系主要体现在它们都可以作为相机的预览显示视图,但PreviewView提供了更多的功能和灵活性。

PreviewView和SurfaceView的区别和联系

  1. 功能差异‌:

    • PreviewView‌:这是一个专门为CameraX设计的视图,支持裁剪、缩放和旋转,确保预览的正确显示。它提供了更多的控制和灵活性,例如可以设置不同的预览分辨率和帧率‌1。
    • SurfaceView‌:是一个更传统的视图,用于展示相机预览图像。它不需要布局文件中的声明,可以直接在代码中创建和使用。SurfaceView适合简单的预览需求,但在CameraX中,PreviewView是更推荐的选择‌12。
  2. 使用场景‌:

    • PreviewView‌:适用于需要高度自定义预览显示的场景,如实时视频处理、预览画面的特殊效果等。它能够更好地与CameraX的其他组件集成,提供更好的用户体验‌1。
    • SurfaceView‌:适用于简单的预览需求,不需要复杂的预览处理。虽然SurfaceView在CameraX中不是首选,但在一些旧的项目或特定的需求中仍然可以使用‌12。

如何在CameraX中使用PreviewView

  1. 添加PreviewView到布局文件‌:

     

    xmlCopy Code

    <FrameLayout android:id="@+id/container" android:layout_width="match_parent" android:layout_height="match_parent"> <androidx.camera.view.PreviewView android:id="@+id/previewView" android:layout_width="match_parent" android:layout_height="match_parent"/> </FrameLayout>

  2. 初始化ProcessCameraProvider并绑定PreviewView‌:

     

    kotlinCopy Code

    override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) if (savedInstanceState == null) {

Android OpenGL ES 学习(一) -- 基本概念-CSDN博客

Android 音视频编解码(一) -- MediaCodec 初探_安卓音视频解码-CSDN博客


http://www.kler.cn/a/528875.html

相关文章:

  • Spring Boot项目中解决跨域问题(四种方式)
  • 索引的底层数据结构、B+树的结构、为什么InnoDB使用B+树而不是B树呢
  • Maya软件安装步骤与百度网盘链接
  • Java小白入门教程:三种注释+快捷方式
  • 苍穹外卖——数据统计
  • 项目测试之MockMvc
  • AI开发之 ——Anaconda 介绍
  • Easy系列PLC尺寸测量功能块ST代码(激光微距仪应用)
  • 动态规划DP 背包问题 完全背包问题(题目分析+C++完整代码)
  • 算法【有依赖的背包】
  • 第26节课:内容安全策略(CSP)—构建安全网页的防御盾
  • 【MyDB】4-VersionManager 之 3-死锁及超时检测
  • 3D 对象的属性
  • plot(rrt_path(:, 1), rrt_path(:, 2), ‘b-‘, ‘LineWidth‘, 2); % 蓝色线条表示RRT路径
  • PDCA 循环法
  • 苍穹外卖第一天
  • 【股票数据API接口45】如何获取股票指历史分时MACD数据之Python、Java等多种主流语言实例代码演示通过股票数据接口获取数据
  • (9) 上:学习与验证 linux 里的 epoll 对象里的 EPOLLIN、 EPOLLHUP 与 EPOLLRDHUP 的不同
  • 深入剖析C语言字符串操作函数:my_strlen与my_strcpy
  • 【问题记录】DeepSeek本地部署遇到问题
  • Python-列表
  • HTB:LinkVortex[WriteUP]
  • STM32 AD多通道
  • 一文讲解Java中的ArrayList和LinkedList
  • 【Convex Optimization Stanford】Lec4 CVX-opt-promblem
  • os开发基础知识(1)