android CameraX牛刀小试-预览、抽帧、拍照功能实现

3,861 阅读2分钟

Camera功能介绍

简介

ameraX 是一个 Jetpack 支持库,旨在帮助您简化相机应用的开发工作。它提供一致且易用的 API 接口,适用于大多数 Android 设备,并可向后兼容至 Android 5.0(API 级别 21)。

虽然 CameraX 利用了 camera2 的功能,但采取了一种具有生命周期感知能力且基于用例的更简单方式。它还解决了设备兼容性问题,因此您无需在代码库中添加设备专属代码。这些功能减少了将相机功能添加到应用时需要编写的代码量。

最后,借助 CameraX,开发者只需两行代码就能实现与预安装的相机应用相同的相机体验和功能。

优点

  1. 使用CameraX不需要在activity的onResume和onPause中放置启动和停止方法,只需要让相机关联上组件生命周期就可以自动管理了
  2. CameraX 会自动确定要使用的最佳分辨率。所有这些操作均由库进行处理,无需您编写设备专属代码。(做过直播的人应该深有体会,简直不要更痛苦)

缺点

  1. 支持最低版本为Android 5.0(API 级别 21),如果你的应用最低支持版本低于21的话要慎用,极有可能会导致部分用户无法使用相机功能

Camera功能

  • 预览:接受用于显示预览的 Surface,例如 PreviewView
  • 图片分析:为分析(例如机器学习)提供 CPU 可访问的缓冲区。
  • 图片拍摄:拍摄并保存照片。

集成配置

    1. app.gradle配置
    android {
        compileOptions {
            sourceCompatibility = JavaVersion.VERSION_1_8
            targetCompatibility = JavaVersion.VERSION_1_8
        }
        // For Kotlin projects
        kotlinOptions {
            jvmTarget = "1.8"
        }
    }
    
  1. depend依赖

    dependencies {
      // CameraX core library using the camera2 implementation
      def camerax_version = "1.0.0"
      // The following line is optional, as the core library is included indirectly by camera-camera2
      implementation "androidx.camera:camera-core:${camerax_version}"
      implementation "androidx.camera:camera-camera2:${camerax_version}"
      // If you want to additionally use the CameraX Lifecycle library
      implementation "androidx.camera:camera-lifecycle:${camerax_version}"
      // If you want to additionally use the CameraX View class
      implementation "androidx.camera:camera-view:1.0.0-alpha24"
      // If you want to additionally use the CameraX Extensions library
      implementation "androidx.camera:camera-extensions:1.0.0-alpha24"
    }
    

功能实现

git代码地址

github.com/ymeddmn/Cam…

必须配置

class App : Application() ,CameraXConfig.Provider{
    override fun getCameraXConfig(): CameraXConfig {
        return Camera2Config.defaultConfig()
    }
}

基本功能实现预览(照搬官方)(simple-use分支)

  1. 必须配置

  2. 引入camera权限

  3. xml中引入布局

    <FrameLayout
        android:id="@+id/container"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        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>
    
  4. 预览activity代码

    class MainActivity : AppCompatActivity() {
        private lateinit var cameraProviderFuture: ListenableFuture<ProcessCameraProvider>
    
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            setContentView(R.layout.activity_main)
            ActivityCompat.requestPermissions(this, arrayOf<String>(Manifest.permission.CAMERA), 100)//请求权限
            cameraProviderFuture = ProcessCameraProvider.getInstance(this)//获得provider实例
    
        }
    
        override fun onRequestPermissionsResult(
            requestCode: Int,
            permissions: Array<out String>,
            grantResults: IntArray
        ) {
            super.onRequestPermissionsResult(requestCode, permissions, grantResults)
            //默认你会同意权限,不同意就是自己的事了
            cameraProviderFuture.addListener(Runnable {
                val cameraProvider = cameraProviderFuture.get()
                bindPreview(cameraProvider)
            }, ContextCompat.getMainExecutor(this))
        }
    
        /**
         * 绑定预览view
         */
        fun bindPreview(cameraProvider: ProcessCameraProvider) {
            var preview: Preview = Preview.Builder()
                .build()
    
            var cameraSelector: CameraSelector = CameraSelector.Builder()
                .requireLensFacing(CameraSelector.LENS_FACING_BACK)
                .build()
    
            preview.setSurfaceProvider(previewView.getSurfaceProvider())
    
            var camera = cameraProvider.bindToLifecycle(this as LifecycleOwner, cameraSelector, preview)
        }
    }
    

分析图片功能(analysis-pic分支)

setAnalyzer使用

使用如下代码可以达到分析每一帧的功能

val imageAnalysis = ImageAnalysis.Builder()
    .setTargetResolution(Size(1280, 720))
    .setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
    .build()
var executor = Executors.newFixedThreadPool(5)
imageAnalysis.setAnalyzer(executor, ImageAnalysis.Analyzer { image ->
    val rotationDegrees = image.imageInfo.rotationDegrees
    iv.setImageBitmap(imageProxyToBitmap(image))
    image.close()
})

实现大小窗口预览效果

实现方式就是抽取每一帧转换为bitmap,然后使用Imageview展示出来

先展示一下最终效果:

haha

代码展示

  1. MainActivity代码

    class MainActivity : AppCompatActivity() {
        private lateinit var cameraProviderFuture: ListenableFuture<ProcessCameraProvider>
        var scope = MainScope()
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            setContentView(R.layout.activity_main)
            ActivityCompat.requestPermissions(
                this,
                arrayOf<String>(Manifest.permission.CAMERA),
                100
            )//请求权限
            cameraProviderFuture = ProcessCameraProvider.getInstance(this)//获得provider实例
    
        }
    
        @SuppressLint("UnsafeOptInUsageError")
        override fun onRequestPermissionsResult(
            requestCode: Int,
            permissions: Array<out String>,
            grantResults: IntArray
        ) {
            super.onRequestPermissionsResult(requestCode, permissions, grantResults)
            //默认你会同意权限,不同意就是自己的事了
    
            val imageAnalysis = ImageAnalysis.Builder()
                .setTargetResolution(Size(1280, 720))
                .setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
                .build()
            var executor = Executors.newFixedThreadPool(5)
            imageAnalysis.setAnalyzer(executor, ImageAnalysis.Analyzer { image ->
                //这里的回调会回调每一帧的信息                                                      
                val bitmap = Bitmap.createBitmap(image.width, image.height, Bitmap.Config.ARGB_8888)//创建一个空的Bitmap
                thread {
                    YuvToRgbConverter(this@MainActivity).yuvToRgb(
                        image = image.image!!,
                        bitmap
                    )//将image转化为bitmap,参考:https://github.com/android/camera-samples/blob/3730442b49189f76a1083a98f3acf3f5f09222a3/CameraUtils/lib/src/main/java/com/example/android/camera/utils/YuvToRgbConverter.kt
                    image.close()//这里调用了close就会继续生成下一帧图片,否则就会被阻塞不会继续生成下一帧
                    runOnUiThread {
                        iv.setImageBitmap(bitmap)//回到主线程更新ui
                    }
                }
    //            scope.launch(Dispatchers.IO) {
    //                val bitmap = Bitmap.createBitmap(image.width,image.height,Bitmap.Config.ARGB_8888)
    //                YuvToRgbConverter(this@MainActivity).yuvToRgb(image = image.image!!,bitmap)//将image转化为bitmap,参考:https://github.com/android/camera-samples/blob/3730442b49189f76a1083a98f3acf3f5f09222a3/CameraUtils/lib/src/main/java/com/example/android/camera/utils/YuvToRgbConverter.kt
    //                image.close()//这里调用了close就会继续生成下一帧图片
    //                withContext(Dispatchers.Main){//这里更新ui会崩溃,搞不懂为啥,很郁闷
    //                    iv.setImageBitmap(bitmap)//回到主线程更新ui
    //                }
    //            }
    
            })
            cameraProviderFuture.addListener(Runnable {
                val cameraProvider = cameraProviderFuture.get()
                bindPreview(cameraProvider, imageAnalysis)
            }, ContextCompat.getMainExecutor(this))
    
        }
    
        /**
         * 绑定预览view
         */
        fun bindPreview(cameraProvider: ProcessCameraProvider, imageAnalysis: ImageAnalysis) {
            var preview: Preview = Preview.Builder()
                .build()
    
            var cameraSelector: CameraSelector = CameraSelector.Builder()
                .requireLensFacing(CameraSelector.LENS_FACING_BACK)
                .build()
    
            preview.setSurfaceProvider(previewView.getSurfaceProvider())
    
            var camera = cameraProvider.bindToLifecycle(//绑定生命周期
                this as LifecycleOwner,
                cameraSelector,
                imageAnalysis,
                preview
            )
        }
    
        override fun onDestroy() {
            super.onDestroy()
            scope.cancel()
        }
    }
    
  2. YuvToRgbConverter代码:
    

    github.com/android/cam…

  3. xml配置参考 基本功能实现预览

使用CameraX拍照

实现的效果:进入预览页面,单机拍照按钮,拍照并跳转到新页面展示图片

效果展示

gif效果

代码展示

  1. MainActivity代码

    class MainActivity : AppCompatActivity() {
        private lateinit var imageCapture: ImageCapture
        private lateinit var cameraProviderFuture: ListenableFuture<ProcessCameraProvider>
    
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            setContentView(R.layout.activity_main)
            ActivityCompat.requestPermissions(
                this,
                arrayOf<String>(
                    Manifest.permission.CAMERA,
                    Manifest.permission.READ_EXTERNAL_STORAGE,
                    Manifest.permission.WRITE_EXTERNAL_STORAGE
                ),
                100
            )//请求权限
            cameraProviderFuture = ProcessCameraProvider.getInstance(this)//获得provider实例
            tv_takepic.setOnClickListener {
                val path = filesDir.absolutePath + File.separator + System.currentTimeMillis() + ".jpg"//使用内部存储存储最终图片
                var photoFile= File(path)
                val outputFileOptions = ImageCapture.OutputFileOptions.Builder(photoFile).build()//构建输出选项
                var cameraExecutor = Executors.newSingleThreadExecutor()
                //点击拍照
                imageCapture.takePicture(outputFileOptions, cameraExecutor,
                    object : ImageCapture.OnImageSavedCallback {
                        override fun onError(error: ImageCaptureException) {
                        }
                        override fun onImageSaved(outputFileResults: ImageCapture.OutputFileResults) {
                            val savedUri = outputFileResults.savedUri ?: Uri.fromFile(photoFile)//获取uri
                            // insert your code here.
                            println("拍照成功")
                            startActivity(Intent(this@MainActivity,ImageShowActivity::class.java).apply {//跳转到新页面展示图片
                                putExtra("path",savedUri)
    
                            })
                        }
                    })
            }
        }
    
        override fun onRequestPermissionsResult(
            requestCode: Int,
            permissions: Array<out String>,
            grantResults: IntArray
        ) {
            super.onRequestPermissionsResult(requestCode, permissions, grantResults)
            //默认你会同意权限,不同意就是自己的事了
            cameraProviderFuture.addListener(Runnable {
                val cameraProvider = cameraProviderFuture.get()
                bindPreview(cameraProvider)
            }, ContextCompat.getMainExecutor(this))
        }
    
        /**
         * 绑定预览view
         */
        fun bindPreview(cameraProvider: ProcessCameraProvider) {
            var preview: Preview = Preview.Builder()
                .build()
    
            var cameraSelector: CameraSelector = CameraSelector.Builder()
                .requireLensFacing(CameraSelector.LENS_FACING_BACK)
                .build()
    
            preview.setSurfaceProvider(previewView.surfaceProvider)
            imageCapture = ImageCapture.Builder()
                .setTargetRotation(previewView.display.rotation)
                .build()
            var camera = cameraProvider.bindToLifecycle(
                this as LifecycleOwner,
                cameraSelector,
                imageCapture,//这个参数必须加上才能进行拍照
                preview
            )
        }
    }
    
  2. ImageShowActivity展示图片代码

    class ImageShowActivity : AppCompatActivity() {
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            setContentView(R.layout.activity_main2)
            intent.getParcelableExtra<Uri>("path")?.let {
                val decodeFile = BitmapFactory.decodeFile(it.path)
                val createScaledBitmap = Bitmap.createScaledBitmap(decodeFile, 200, 200, true)//这里必须进行压缩,我的手机上直接绘制根本绘制不出来这么大的图片
                iv.setImageBitmap(createScaledBitmap)
            }
        }
    }
    

图像旋转

参考官方解释就ok,官方写的很明白了(不过这点新手似乎不是很好理解,如果有超过五条留言要求详细解说的我可以单独加一下这里的详细解说)

developer.android.google.cn/training/ca…

首发后补充

前置和后置摄像头配置20210906

摄像头切换使用CameraSelector完成 前置摄像头:LENS_FACING_FRONT 后置摄像头:LENS_FACING_BACK

 fun bindPreview(cameraProvider: ProcessCameraProvider) {
        var preview: Preview = Preview.Builder()
            .build()
        var cameraSelector: CameraSelector = CameraSelector.Builder()
            .requireLensFacing(CameraSelector.LENS_FACING_FRONT)//前置摄像头:LENS_FACING_FRONT  后置摄像头:LENS_FACING_BACK
            .build()
        preview.setSurfaceProvider(previewView.surfaceProvider)
//        preview.setSurfaceProvider(mSurfaceProvider)
        imageCapture = ImageCapture.Builder()
            .setTargetRotation(previewView.display.rotation)
            .build()
        var camera = cameraProvider.bindToLifecycle(
            this as LifecycleOwner,
            cameraSelector,
            imageCapture,//这个参数必须加上才能进行拍照
            preview
        )
    }

采坑,多次进入相机有概率崩溃20210908

解决办法

在onDestory中

val cameraProvider: ProcessCameraProvider
cameraProvider.shutdown()

image.png

image.png

对拍摄的照片变换20210925新增

对抽帧的图片进行旋转

抽帧的方法前面的章节有介绍过,主要api如下

imageAnalysis.setAnalyzer

我们知道我们拿到的图片是和相机和手机的方向有关系的,当我们竖屏拍摄的时候大部分手机抽帧拿到的图片是水平方向的,所以我们需要对图片进行旋转从而将图片改为垂直方向 实现方式如下:

首先我们需要创建一个新的bitmap,拿到旋转的角度,将image转换为bitmap

val rotation = image.imageInfo.rotationDegrees//这个角度表示我们将图片旋转会正常的垂直方向需要顺时针旋转的角度
var bitmap = Bitmap.createBitmap(image.width, image.height, Bitmap.Config.ARGB_8888)
image.image?.let { innerImage ->
    YuvToRgbConverter(context).yuvToRgb(//这个方法可以把抽帧的image转换为bitmap
        image = innerImage,
        bitmap
    )//将image转化为bitmap,参考:https://github.com/android/camera-samples/blob/3730442b49189f76a1083a98f3acf3f5f09222a3/CameraUtils/lib/src/main/java/com/example/android/camera/utils/YuvToRgbConverter.kt
}

将bitmap进行顺时针方向的旋转

bitmap = BitmapUtils.rotate(bitmap, rotation)
/**
 * 旋转图片
 * @param bitmap
 * @param degress
 * @return
 */
fun rotate(bitmap: Bitmap, degress: Int): Bitmap {
    if(degress==0){
        return bitmap
    }
    val matrix = Matrix()
    matrix.postRotate(degress.toFloat())
    return Bitmap.createBitmap(bitmap, 0, 0, bitmap.width, bitmap.height, matrix, true)
}

前置摄像头拍摄的图片如何进行左右对称旋转

我们都知道安卓前置摄像头拍摄的图片是巨烦人的,自定义的情况下都是左右颠倒的,所以有些情况(例如活体识别需求)我们需要将图片进行旋转改为正常的方式。

/**
 * 水平和垂直翻转
 */
fun horverImage(bitmap: Bitmap, H: Boolean, V: Boolean): Bitmap? {
    val bmpWidth = bitmap.width
    val bmpHeight = bitmap.height
    val matrix = Matrix()
    if (H) matrix.postScale(-1f, 1f) //水平翻转H
    if (V) matrix.postScale(1f, -1f) //垂直翻转V
    if (H && V) matrix.postScale(-1f, -1f) //水平&垂直翻转HV
    return Bitmap.createBitmap(bitmap, 0, 0, bmpWidth, bmpHeight, matrix, true)
}

拍摄照片,将照片存在在内存中,自己实现序列化

本例代码所在地址

代码传送门

前面的章节中我们已经有了拍摄照片的方法了,但是上面章节的实现方式是基于提前传入一个文件路径的方法,也就是说我们拍摄完成CameraX Api会自动的帮我们将图片存储到指定的路径,但是有时候这种方式可能不满足我们的需求,

例如:你可能在存储前想对图片进行变换以得到你预期中的结果,或者你压根就没想对图片进行存储,此时上面的方法将是一个极其不合适的方式

但是使用他重构的方法却可以完美解决,方法定义和使用如下两段代码:

public void takePicture(@NonNull Executor executor,
        final @NonNull OnImageCapturedCallback callback)

fun  takePicToMemory(context: Context, scope: CoroutineScope, callback:(path:String)->Unit){
    var file = File(context.getExternalFilesDir(""), "File")
    if (!file.exists()) {
        file.mkdirs()
    }
//        val photoFile = File(file, "${System.currentTimeMillis()}.jpg")
    var cameraExecutor = Executors.newSingleThreadExecutor()
    imageCapture.takePicture(cameraExecutor,object : ImageCapture.OnImageCapturedCallback() {
        @RequiresApi(Build.VERSION_CODES.N)
        @SuppressLint("UnsafeOptInUsageError")
        override fun onCaptureSuccess(image: ImageProxy) {
            super.onCaptureSuccess(image)
            scope.launch (Dispatchers.IO){
                // 将 imageProxy 转为 byte数组
                val buffer: ByteBuffer = image.planes[0].buffer
                // 新建指定长度数组
                val byteArray = ByteArray(buffer.remaining())
                // 倒带到起始位置 0
                buffer.rewind()
                // 数据复制到数组, 这个 byteArray 包含有 exif 相关信息,
                // 由于 bitmap 对象不会包含 exif 信息,所以转为 bitmap 需要注意保存 exif 信息
                buffer.get(byteArray)
                // 获取照片 Exif 信息
//                    val byteArrayInputStream = ByteArrayInputStream(byteArray)
//                    val orientation = ExifInterface(byteArrayInputStream)

                var bitmap = BitmapFactory.decodeByteArray(byteArray,0,byteArray.size)
                bitmap=BitmapUtils.rotate(bitmap,image.imageInfo.rotationDegrees.toFloat())
                val savedPath = BitmapUtils.saveImageToGallery(
                    bitmap,
                    file.absolutePath,
                    "${System.currentTimeMillis()}.jpg"
                )
                withContext(Dispatchers.Main){
                    callback.invoke(savedPath)

                }

                image.close()

            }
        }

    })
}

前置摄像头拍照设置图片方向不变(2021-12-19)

方法我暂时还没试过,将来有需求再试

地址

https://juejin.cn/post/7036206769248403469#heading-6

关键代码摘抄

val metadata = ImageCapture.Metadata()
metadata.isReversedHorizontal = !isBackCamera
val outputFileOption =
    ImageCapture.OutputFileOptions.Builder(File(savePath)).setMetadata(metadata).build()
imageCapture?.takePicture(
    outputFileOption,
    ContextCompat.getMainExecutor(this as Context),
    object : ImageCapture.OnImageSavedCallback {
        override fun onError(error: ImageCaptureException) {
        }

        override fun onImageSaved(outputFileResults: ImageCapture.OutputFileResults) {
        }
    })