Android JetPack组件CameraX使用及修改显示图像

2020-12-17 14:20:47 浏览数 (1)

学更好的别人,

做更好的自己。

——《微卡智享》

本文长度为3743,预计阅读9分钟

前言

好久没用Kotlin写Android的代码了,刚开始写起来还有点生,不过适应了一阵也算是恢复过来了。今天这篇主要是说说Android JepPack组件中CameraX的使用,其实网上也有不少简单的例子,本篇也是参考了网的一篇文章后实现的。主要要说的还是后面,怎么在原有的图像上进行编辑显示,文末有源码的链接。

实现效果

划重点

要在CameraX中实现图像上显示修改的图像,需要在PreviewView的上层再自己写一个View,使用Canvas.draw的方式进行绘制显示,而无法直接在Preview中进行图像的更改。这个和我以前文章《Android通过OpenCV和TesserartOCR实时进行识别》直接在OpenCV中修改了图像后在通过SurfaceView显示是完全不一样的。

CameraX的使用

微卡智享

01

导入依赖包

在项目的build.gradle中加入依赖项

代码语言:javascript复制
    implementation "androidx.camera:camera-camera2:1.0.0-beta12"
    implementation "androidx.camera:camera-view:1.0.0-alpha19"
    implementation "androidx.camera:camera-extensions:1.0.0-alpha19"
    implementation "androidx.camera:camera-lifecycle:1.0.0-beta12"

02

申请权限

在Androidmanifests下的manifests的节点加申请权限内容

代码语言:javascript复制
    <!-- 摄像头权限 -->
    <uses-permission android:name="android.permission.CAMERA" />
    <!-- 具备摄像头 -->
    <uses-feature android:name="android.hardware.camera.any" />
    <!-- 存储图像或者视频权限 -->
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /> <!-- 录制音频权限 -->
    <uses-permission android:name="android.permission.RECORD_AUDIO" />

在Android的6.0后需要动态申请权限,所以要加入动态申请权限的代码

动态申请权限相关代码

代码语言:javascript复制
    //权限列表
    companion object {
        private const val REQUEST_CODE_PERMISSIONS = 10
        private val REQUIRED_PERMISSIONS = arrayOf(
            Manifest.permission.CAMERA, Manifest.permission.WRITE_EXTERNAL_STORAGE,
            Manifest.permission.READ_EXTERNAL_STORAGE, Manifest.permission.RECORD_AUDIO
        )
    }
    
    //判断是否有权限
    private fun allPermissionsGranted() = REQUIRED_PERMISSIONS.all {
        ContextCompat.checkSelfPermission(baseContext, it) == PackageManager.PERMISSION_GRANTED
    }
    
    //请求权限返回的函数
    override fun onRequestPermissionsResult(
        requestCode: Int,
        permissions: Array<out String>,
        grantResults: IntArray
    ) {
        if (requestCode == REQUEST_CODE_PERMISSIONS) {
            if (allPermissionsGranted()) {
                startCamera()
            } else {
                Toast.makeText(this, "未开启权限.", Toast.LENGTH_SHORT).show()
                finish()
            }
        }
    }

03

创建实时图像上层的View

我们先创建一个ViewOverLay继承自View,用于实现摄像机上层修改的显示图层,其中定义了显示的文字,输出的坐标及drawtext的绘制文字方法

代码语言:javascript复制
package dem.vaccae.camerax

import android.content.Context
import android.graphics.*
import android.util.AttributeSet
import android.view.View
import androidx.core.content.ContextCompat
import androidx.core.graphics.red

/**
 * 作者:Vaccae
 * 邮箱:3657447@qq.com
 * 创建时间:2020-11-26 14:51
 * 功能模块说明:
 */
class ViewOverLay constructor(context: Context?, attributeSet: AttributeSet?) :
    View(context, attributeSet) {

    private var mText: String? = null
    private var mPoint: PointF? = null

    private val textpaint = Paint().apply {
        style = Paint.Style.FILL
        color = ContextCompat.getColor(context!!, android.R.color.holo_blue_light)
        strokeWidth = 10f
        textSize = 150f
        isFakeBoldText = true
    }

    override fun onDraw(canvas: Canvas?) {
        super.onDraw(canvas)
        mText?.let {
            canvas?.drawText(it, mPoint!!.x, mPoint!!.y, textpaint)
        }

    }

    fun drawText(str: String, ptr: PointF) {
        mText = str
        mPoint = ptr;
        invalidate()
    }
}

04

布局文件

代码语言:javascript复制
<?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" >

    </androidx.camera.view.PreviewView>

    <dem.vaccae.camerax.ViewOverLay
        android:id="@ id/viewOverlay"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:visibility="visible" />

</androidx.constraintlayout.widget.ConstraintLayout>

布局文件中除了加入PreviewView,然后再加入了刚才创建的ViewOverLay,两个都是全屏的显示。

05

MainActivity关键代码

在OnCreate加载时需要注意自己创建的View一定要显示在PreviewView的上层,所以要加上bringToFront()

代码语言:javascript复制
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        viewFinder = findViewById(R.id.viewFinder)
        vOverLay = findViewById(R.id.viewOverlay)
        //显示在最上层
        vOverLay.bringToFront()

        if (allPermissionsGranted()) {
            startCamera()
        } else {
            ActivityCompat.requestPermissions(this, REQUIRED_PERMISSIONS, REQUEST_CODE_PERMISSIONS)
        }
    }

全局变量

代码语言:javascript复制
    private lateinit var cameraExecutor: ExecutorService
    var cameraProvider: ProcessCameraProvider? = null//相机信息
    var preview: Preview? = null//预览对象
    var cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA//当前相机
    var camera: Camera? = null//相机对象
    private var imageCapture: ImageCapture? = null//拍照用例
    var videoCapture: VideoCapture? = null//录像用例
    var imageAnalyzer: ImageAnalysis? = null//图片分析

开启摄像机

代码语言:javascript复制
    private fun startCamera() {
        cameraExecutor = Executors.newSingleThreadExecutor()
        val cameraProviderFuture = ProcessCameraProvider.getInstance(this)
        cameraProviderFuture.addListener(Runnable {
            cameraProvider = cameraProviderFuture.get()//获取相机信息

            //预览配置
            preview = Preview.Builder()
                .build()
                .also {
                    it.setSurfaceProvider(viewFinder.surfaceProvider)
                }
            //拍照用例配置
            imageCapture = ImageCapture.Builder().build()
            //图像分析接口
            imageAnalyzer = ImageAnalysis.Builder()
                .build()
                .also { it ->
                    it.setAnalyzer(cameraExecutor, { p->
                        count  ;
                        //每20帧执行一次
                        if (count % 20 == 0) {
                            val idx = idxarray % 4
                            vOverLay.drawText(strarray[idx], ptrarray[idx])
                            idxarray  
                        }
                        //这里的ImageProxy如果不close,不会显示下一帧
                        p.close()
                    })
                }

            cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA//使用后置摄像头
            videoCapture = VideoCapture.Builder()//录像用例配置
/*                .setTargetAspectRatio(AspectRatio.RATIO_16_9) //设置高宽比
                .setTargetRotation(viewFinder.display.rotation)//设置旋转角度
                .setAudioRecordSource(AudioSource.MIC)//设置音频源麦克风*/
                .build()

            try {
                cameraProvider?.unbindAll()//先解绑所有用例
                camera = cameraProvider?.bindToLifecycle(
                    this, cameraSelector, preview, imageCapture,
                    imageAnalyzer
                )//绑定用例
            } catch (exc: Exception) {
                Log.e(TAG, "Use case binding failed", exc)
            }

        }, ContextCompat.getMainExecutor(this))
    }

划重点

进行图像的分析是通过ImageAnalysis的接口实现,上图中Analyzer里的使用lambda表达中p代表着传入的参数ImageProxy,每当我们处理完当前帧操作时,要记得将其close,否则后面是无变化的。

上面代码就是实现了每20帧更新显示一个新的汉字的效果,就是我们开头的动图中的效果实现。

通过上面的代码,一个简单的Camera的效果就实现了,可以看出来,用CameraX的调用,要比原来Camera写起来简单了好多,不过要注意一点是CameraX就是Camera2的封装,所以Android的最低版本要求是5.1。

接下来我会用CameraX调用摄像头配合OpenCV,做点小东西了。

源码地址

https://github.com/Vaccae/AndroidCameraXDemo.git

0 人点赞