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

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

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>
复制代码


  1. 预览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展示出来


先展示一下最终效果:


代码展示

  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()
    }
}
复制代码


YuvToRgbConverter代码:
复制代码
  1. github.com/android/cam…
  2. xml配置参考 基本功能实现预览


使用CameraX拍照

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


效果展示


代码展示

  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
        )
    }
}
复制代码


  1. 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) {
        }
    })



相关文章
|
20天前
|
编解码 测试技术 Android开发
Android经典实战之用 CameraX 库实现高质量的照片和视频拍摄功能
本文详细介绍了如何利用CameraX库实现高质量的照片及视频拍摄功能,包括添加依赖、初始化、权限请求、配置预览与捕获等关键步骤。此外,还特别针对不同分辨率和帧率的视频拍摄提供了性能优化策略,确保应用既高效又稳定。
50 1
Android经典实战之用 CameraX 库实现高质量的照片和视频拍摄功能
|
29天前
|
图形学 Android开发
小功能⭐️Unity调用Android常用事件
小功能⭐️Unity调用Android常用事件
|
3月前
|
数据库 Android开发 数据安全/隐私保护
在 Android Studio 中结合使用 SQLite 数据库实现简单的注册和登录功能
在 Android Studio 中结合使用 SQLite 数据库实现简单的注册和登录功能
149 2
|
3月前
|
Android开发
Android中如何快速的实现RecycleView的拖动重排序功能
使用`ItemTouchHelper`和自定义`Callback`,在`RecyclerView`中实现拖动排序功能。定义`ItemTouchHelperAdapter`接口,`Adapter`实现它以处理`onItemMove`方法。`SimpleItemTouchHelperCallback`设置拖动标志,如`LEFT`或`RIGHT`(水平拖动),并绑定到`RecyclerView`以启用拖动。完成这些步骤后,即可实现拖放排序。关注公众号“AntDream”获取更多内容。
77 3
|
4月前
|
移动开发 监控 Android开发
构建高效Android应用:从内存优化到电池寿命代码之美:从功能实现到艺术创作
【5月更文挑战第28天】 在移动开发领域,特别是针对Android系统,性能优化始终是关键议题之一。本文深入探讨了如何通过细致的内存管理和电池使用策略,提升Android应用的运行效率和用户体验。文章不仅涵盖了现代Android设备上常见的内存泄漏问题,还提出了有效的解决方案,包括代码级优化和使用工具进行诊断。同时,文中也详细阐述了如何通过减少不必要的后台服务、合理管理设备唤醒锁以及优化网络调用等手段延长应用的电池续航时间。这些方法和技术旨在帮助开发者构建更加健壮、高效的Android应用程序。
|
3月前
|
存储 数据库 Android开发
在 Android Studio 中结合使用 SQLite 数据库实现简单的注册和登录功能
在 Android Studio 中结合使用 SQLite 数据库实现简单的注册和登录功能
73 0
|
4月前
|
Android开发 数据安全/隐私保护 iOS开发
ios和安卓测试包发布网站http://fir.im的注册与常用功能
ios和安卓测试包发布网站http://fir.im的注册与常用功能
100 0
ios和安卓测试包发布网站http://fir.im的注册与常用功能
|
4月前
|
Android开发
Android SystemUI去掉拖动亮度条QSPanel界面隐藏功能
Android SystemUI去掉拖动亮度条QSPanel界面隐藏功能
82 0
|
4月前
|
Java Android开发
Android Mediatek 应用层重置USB设备功能
Android Mediatek 应用层重置USB设备功能
39 0
|
4月前
|
Linux Android开发
Android 内核关闭CAN 串口设备回显功能
Android 内核关闭CAN 串口设备回显功能
33 0