Jetpack CameraX实践,预览(preview)及分析(analysis)

最后的效果如下:(实现相机预览和显示图片亮度)

CameraX(主要关注三块内容前三个内容)

  • 图像预览(Image Preview):就是将画面显示在手机上
    图像分析(Image analysis):分析图片队列里的图片,比如:计算图片亮度,深度学习的图像识别和图片分类等
    图像拍摄(Image capture):就是拍照片,保存到相册
    图像触碰(Image touch):点击屏幕聚焦、拍摄图片等
    图像缩放(Image zoom):放大缩小
  • 这里给一个链接:https://proandroiddev.com,上面的内容讲的都很全,但是图像分析那块,还是将官方给的代码重复一遍,没有实际应用意义

上手第一步:添加依赖

    // CameraX core library using the camera2 implementation
    def camerax_version = "1.0.0-beta01"
    implementation "androidx.camera:camera-camera2:${camerax_version}"
    // If you want to use the CameraX View class
    implementation "androidx.camera:camera-view:1.0.0-alpha08"
    // If you want to use the CameraX Extensions library
    implementation "androidx.camera:camera-extensions:1.0.0-alpha08"
    // If you want to use the CameraX Lifecycle library
    implementation "androidx.camera:camera-lifecycle:${camerax_version}"

注:这里是有坑的,1.0.0-beta01是第一个稳定版本也是当前最新的版本,以前的alpha01到alpha10不堪回首,每一版API变动都很大,所以尽量使用稳定版…

申请权限

相机权限:<uses-permission android:name="android.permission.CAMERA" />
注:动态权限申请activity、fragment和fragment的子fragment的申请方式不一样!!!,参考我的另一篇文章:https://blog.csdn.net/ydduong

页面布局

<?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=".vision.CameraXFragment">

    <androidx.camera.view.PreviewView
        android:id="@+id/textureView"
        android:layout_width="wrap_content"
        android:layout_height="0dp"
        android:layout_marginBottom="16dp"
        app:layout_constraintBottom_toTopOf="@+id/textView2"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintHorizontal_bias="0.0"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <TextView
        android:id="@+id/textView2"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginBottom="32dp"
        android:text="TextView"
        android:textSize="30sp"
        app:layout_constraintBottom_toTopOf="@+id/textView3"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

注:用于显示相机的组件,有TextureView,PreviewView等,alpha版使用TextureView,beta版使用的是PreviewView (我看官方代码是这么写的,不知道为啥)

配置相机

  • 先有一个整体印象:相机的生命周期什么的统统不用管,应为CameraX便捷之处就是将相机的生命周期绑定在activity或者fragment的生命周期上,同时CameraX的典型用法是继承CameraXConfig.Provider类,并重载getCameraXConfig方法,是样板代码。此外CameraX的功能(拍照、预览、分析、缩放…)都是相对独立的,需要什么就添加什么
  • 图片分析(这才是本文的核心),图片分析功能,必须要有一个分析器(也可称之为图片分析类,必须要继承ImageAnalysis.Analyzer并重载analyze方法),因为这个分析器是没有返回值的,但是可以传参进去,这里我们用回调,回调出我们要的结果,但是回调之后的结果是在后台线程的,所以要再将数据送到主线程,最后再是显示数据
package com.example.image3.vision

import android.Manifest
......
import kotlin.collections.ArrayList


typealias LumaListener = (luma: Double) -> Unit // 图像分析监听的放回类型,(typealias是取个别名)

private const val REQUEST_CODE_PERMISSIONS = 10 // 权限标识符
private val REQUIRED_PERMISSIONS = arrayOf(Manifest.permission.CAMERA) // 相机权限

// 继承CameraXConfig.Provider,重载getCameraXConfig方法
class CameraXFragment : Fragment(), CameraXConfig.Provider {
    override fun getCameraXConfig(): CameraXConfig {
        return Camera2Config.defaultConfig()
    }

    private lateinit var viewModel: CameraXViewModel // viewModel只有一个变量:平均亮度
    private lateinit var cameraProviderFuture : ListenableFuture<ProcessCameraProvider> // 相机的控制者
    private lateinit var imagePreview: Preview // 图像预览
    private lateinit var imageAnalysis: ImageAnalysis // 图像分析
    private val executor = Executors.newSingleThreadExecutor() // 后台线程
    private lateinit var previewView: PreviewView // xml里显示相机的组件
 
    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        return inflater.inflate(R.layout.camera_x_fragment, container, false)
    }

    override fun onActivityCreated(savedInstanceState: Bundle?) {
        super.onActivityCreated(savedInstanceState)

        cameraProviderFuture = ProcessCameraProvider.getInstance(requireContext())
        viewModel = ViewModelProviders.of(this).get(CameraXViewModel::class.java)
        previewView = textureView
		
		// 显示数据
        viewModel.imageLight.observe(this, androidx.lifecycle.Observer {
            textView2.text = it.toString()
        })

        // 检查相机权限
        if (allPermissionsGranted()) {
            Log.v(CameraXFragment::class.java.simpleName, "已有权限,执行")
            previewView.post { startCamera() }
        } else {
            Log.v(CameraXFragment::class.java.simpleName, "没有权限,请求权限")
            requestPermissions(REQUIRED_PERMISSIONS, REQUEST_CODE_PERMISSIONS)
        }

    }

    // 相机操作 
    private fun startCamera() {
        // 预览
        imagePreview = Preview.Builder().apply {
            setTargetAspectRatio(AspectRatio.RATIO_16_9)
            setTargetRotation(previewView.display.rotation)
        }.build()
        imagePreview.setSurfaceProvider(previewView.previewSurfaceProvider)

        // 分析:LuminosityAnalyzer 就是分析器,回调值就是it; MainScope().launch就是主线程
        imageAnalysis = ImageAnalysis.Builder().apply {
            setImageQueueDepth(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
            setTargetResolution(Size(224, 224))
        }.build()
        imageAnalysis.setAnalyzer(executor, LuminosityAnalyzer {
            MainScope().launch {
                viewModel.imageLight.value = it
            }
            Log.v(CameraXFragment::class.java.simpleName, it.toString())
        })


        // 绑定
        val cameraSelector = CameraSelector.Builder().requireLensFacing(CameraSelector.LENS_FACING_BACK).build()
        cameraProviderFuture.addListener(Runnable {
            val cameraProvider = cameraProviderFuture.get()
            cameraProvider.bindToLifecycle(this, cameraSelector, imagePreview, imageAnalysis)
        }, ContextCompat.getMainExecutor(requireContext()))
    }


    // 权限请求结果回调
    override fun onRequestPermissionsResult(
        requestCode: Int, permissions: Array<String>, grantResults: IntArray
    ) {
        Log.v(CameraXFragment::class.java.simpleName, "执行回调结果")
        if (requestCode == REQUEST_CODE_PERMISSIONS) {
            if (allPermissionsGranted()) {
                textureView.post { startCamera() }
            } else {
                Toast.makeText(
                    requireContext(),
                    "Permissions not granted by the user.",
                    Toast.LENGTH_SHORT
                ).show()
                // 这里应该是返回上一层,或者跳转到该页面的时候,就先判断有没有权限
                activity?.finish()
            }
        }
    }

    // 检查权限
    private fun allPermissionsGranted() = REQUIRED_PERMISSIONS.all {
        ContextCompat.checkSelfPermission(
            requireContext(), it
        ) == PackageManager.PERMISSION_GRANTED
    }

    // 平均亮度
    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

        /**
         * Used to add listeners that will be called with each luma computed
         */
        fun onFrameAnalyzed(listener: LumaListener) = listeners.add(listener)

        /**
         * 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()
        }
    }
}

注:如果你要传参到分析器内部,那么你的回调类型,一定要放在最后一个参数的位置像这样定义:private class LuminosityAnalyzer(st: String, listener: LumaListener? = null) : ImageAnalysis.Analyzer { }

发布了107 篇原创文章 · 获赞 61 · 访问量 4万+

猜你喜欢

转载自blog.csdn.net/y_dd6011/article/details/104727122