文章详情

短信预约-IT技能 免费直播动态提醒

请输入下面的图形验证码

提交验证

短信预约提醒成功

Android 使用CameraX实现预览/拍照/录制视频/图片分析/对焦/缩放/切换摄像头等操作

2023-08-20 11:52

关注

1. CameraX架构

看官方文档 CameraX架构
有如下这一段话

使用CameraX,借助名为"用例"的抽象概念与设备的相机进行交互。

不同用例可以组合使用,也可以同时处于活跃状态。
例如,应用中可以加入预览用例,以便让用户查看进入相机视野的画面
加入图片分析用例,以确定照片里的人物是否在微笑
还可以加入图片拍摄用例,以便在人物微笑时拍摄照片

第一次看的时候,一脸懵逼,“用例”,是个什么鬼玩意。

后来,研究了一下,知道"用例"的英文原文叫做Use Case,CameraX中的每一项操作,对应着一种UseCase

可以看到,这几个类都是继承自UseCase.java类的

public final class Preview extends UseCase {//...}public final class ImageAnalysis extends UseCase {//...}public final class ImageCapture extends UseCase {//...}public final class VideoCapture extends UseCase {//...}

接下来让我们来尝试使用一下。

2. 前置操作

首先,我们需要新建一个项目,然后引入依赖

// CameraX core library using the camera2 implementationdef camerax_version = "1.2.0-alpha02" //1.2.0-alpha02// The following line is optional, as the core library is included indirectly by camera-camera2implementation "androidx.camera:camera-core:${camerax_version}"implementation "androidx.camera:camera-camera2:${camerax_version}"// If you want to additionally use the CameraX Lifecycle libraryimplementation "androidx.camera:camera-lifecycle:${camerax_version}"// If you want to additionally use the CameraX VideoCapture libraryimplementation "androidx.camera:camera-video:${camerax_version}"// If you want to additionally use the CameraX View classimplementation "androidx.camera:camera-view:${camerax_version}"// If you want to additionally add CameraX ML Kit Vision Integrationimplementation "androidx.camera:camera-mlkit-vision:${camerax_version}"// If you want to additionally use the CameraX Extensions libraryimplementation "androidx.camera:camera-extensions:${camerax_version}"

AndroidManifest.xml里添加权限

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

别忘了申请权限

ActivityCompat.requestPermissions(        this, arrayOf(            Manifest.permission.CAMERA,            Manifest.permission.WRITE_EXTERNAL_STORAGE,            Manifest.permission.READ_EXTERNAL_STORAGE,            Manifest.permission.RECORD_AUDIO        ), 123    )

3. 预览 : Preview.java

首先修改activity_main.xml

<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"    xmlns:app="http://schemas.android.com/apk/res-auto"    android:id="@+id/camera_container"    android:layout_width="match_parent"    android:layout_height="match_parent"    android:background="@android:color/black">    <androidx.camera.view.PreviewView        android:id="@+id/previewView"        android:layout_width="0dp"        android:layout_height="0dp"        app:layout_constraintBottom_toBottomOf="parent"        app:layout_constraintLeft_toLeftOf="parent"        app:layout_constraintRight_toRightOf="parent"        app:layout_constraintTop_toTopOf="parent" />androidx.constraintlayout.widget.ConstraintLayout>

修改MainActivity.kt

class MainActivity : AppCompatActivity() {    private lateinit var binding: ActivityMainBinding    private lateinit var cameraProvider: ProcessCameraProvider    private var preview: Preview? = null    private var camera: Camera? = null    override fun onCreate(savedInstanceState: Bundle?) {        super.onCreate(savedInstanceState)        binding = ActivityMainBinding.inflate(layoutInflater)        setContentView(binding.root)        //TODO 省略了权限申请,具体看文章中 "前置操作" 部分        setUpCamera(binding.previewView)    }    private fun setUpCamera(previewView: PreviewView) {        val cameraProviderFuture: ListenableFuture<ProcessCameraProvider> =            ProcessCameraProvider.getInstance(this)        cameraProviderFuture.addListener({            try {                cameraProvider = cameraProviderFuture.get()                bindPreview(cameraProvider, previewView)            } catch (e: Exception) {                e.printStackTrace()            }        }, ContextCompat.getMainExecutor(this))    }    private fun bindPreview(        cameraProvider: ProcessCameraProvider,        previewView: PreviewView    ) {    //解除所有绑定,防止CameraProvider重复绑定到Lifecycle发生异常        cameraProvider.unbindAll()        preview = Preview.Builder().build()        camera = cameraProvider.bindToLifecycle(            this,            CameraSelector.DEFAULT_BACK_CAMERA, preview        )        preview?.setSurfaceProvider(previewView.surfaceProvider)    }}

看下效果
在这里插入图片描述

4. 图像分析 : ImageAnalysis.java

图像分析用例ImageAnalysis为应用提供可实时分析的图像数据,我们可以对这些图像执行图像处理、计算机视觉或机器学习推断。

val imageAnalysis = ImageAnalysis.Builder()    // enable the following line if RGBA output is needed.    // .setOutputImageFormat(ImageAnalysis.OUTPUT_IMAGE_FORMAT_RGBA_8888)    .setTargetResolution(Size(1280, 720))    .setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)    .build()imageAnalysis.setAnalyzer(executor, ImageAnalysis.Analyzer { imageProxy ->    val rotationDegrees = imageProxy.imageInfo.rotationDegrees    // insert your code here.    // 在这里处理图片的解析,比如解析成二维码之类的    ...    // after done, release the ImageProxy object    imageProxy.close()})

在调用cameraProvider.bindToLifecycle()时,进行传入

cameraProvider.bindToLifecycle(this, CameraSelector.DEFAULT_BACK_CAMERA, preview, imageAnalysis)

5. 拍照 : ImageCapture.java

5.1 仅拍照

这里,我们需要先创建一个imageCapture

imageCapture = ImageCapture.Builder().setCaptureMode(ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY)//.setTargetAspectRatio(screenAspectRatio)//.setTargetRotation(binding.previewView.display.rotation).build()

然后,在调用cameraProvider.bindToLifecycle()时,进行传入

camera = cameraProvider.bindToLifecycle(    this,CameraSelector.DEFAULT_BACK_CAMERA, preview, imageCapture)

增加takePicture()方法进行拍照

//进行拍照private fun takePicture() {    imageCapture?.let { imageCapture ->        val mainExecutor = ContextCompat.getMainExecutor(this)        imageCapture.takePicture(mainExecutor, object : ImageCapture.OnImageCapturedCallback() {            override fun onCaptureSuccess(image: ImageProxy) {                super.onCaptureSuccess(image)            }            override fun onError(exception: ImageCaptureException) {                super.onError(exception)            }        })        // 让画面闪一下,营造拍照的感觉        // We can only change the foreground Drawable using API level 23+ API        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {            // Display flash animation to indicate that photo was captured            binding.root.postDelayed({                binding.root.foreground = ColorDrawable(Color.WHITE)                binding.root.postDelayed(                    { binding.root.foreground = null }, 50L                )            }, 100L)        }    }}

5.2 拍照并保存到本地存储

我们也可以拍照后,保存到本地存储中

private fun createFile(baseFolder: File, format: String, extension: String) =    File(        baseFolder, SimpleDateFormat(format, Locale.US)            .format(System.currentTimeMillis()) + extension    )fun getOutputDirectory(context: Context): File {    val appContext = context.applicationContext    val mediaDir = context.externalMediaDirs.firstOrNull()?.let {        File(it, appContext.resources.getString(R.string.app_name)).apply { mkdirs() }    }    return if (mediaDir != null && mediaDir.exists())        mediaDir else appContext.filesDir}companion object {    private const val FILENAME = "yyyy-MM-dd-HH-mm-ss-SSS"    private const val PHOTO_EXTENSION = ".jpg"}//进行拍照并保存到本地private fun takePictureSaveToDisk() {    imageCapture?.let { imageCapture ->        // Create output file to hold the image        val photoFile = createFile(getOutputDirectory(this), FILENAME, PHOTO_EXTENSION)        Log.i(TAG, "photoFile:$photoFile")        // Setup image capture metadata        val metadata = ImageCapture.Metadata().apply {            // Mirror image when using the front camera            isReversedHorizontal = lensFacing == CameraSelector.LENS_FACING_FRONT        }        // Create output options object which contains file + metadata        val outputOptions = ImageCapture.OutputFileOptions.Builder(photoFile)            .setMetadata(metadata)            .build()        // Setup image capture listener which is triggered after photo has been taken        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(output: ImageCapture.OutputFileResults) {                    val savedUri = output.savedUri ?: Uri.fromFile(photoFile)                    Log.d(TAG, "Photo capture succeeded: $savedUri")                    // Implicit broadcasts will be ignored for devices running API level >= 24                    // so if you only target API level 24+ you can remove this statement                    if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {                        application.sendBroadcast(Intent(android.hardware.Camera.ACTION_NEW_PICTURE, savedUri)                        )                    }                    // If the folder selected is an external media directory, this is                    // unnecessary but otherwise other apps will not be able to access our                    // images unless we scan them using [MediaScannerConnection]                    val mimeType = MimeTypeMap.getSingleton()                        .getMimeTypeFromExtension(savedUri.toFile().extension)                    MediaScannerConnection.scanFile(                        application,                        arrayOf(savedUri.toFile().absolutePath),                        arrayOf(mimeType)                    ) { _, uri ->                        Log.d(TAG, "Image capture scanned into media store: $uri")                    }                }            }) // 让画面闪一下,营造拍照的感觉        // We can only change the foreground Drawable using API level 23+ API        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {            // Display flash animation to indicate that photo was captured            binding.root.postDelayed({                binding.root.foreground = ColorDrawable(Color.WHITE)                binding.root.postDelayed(                    { binding.root.foreground = null }, 50L                )            }, 100L)        }    }}

然后,我们可以在相册里找到这张图片了,图片的真实位置位于/storage/emulated/0/Android/media/你的包名/项目名/中。

6. 视频录制 : VideoCapture.java

视频录制用的是VideoCapture

videoCapture = VideoCapture.Builder()//.setTargetRotation(previewView.getDisplay().getRotation()).setVideoFrameRate(25).setBitRate(3 * 1024 * 1024).build()

在调用cameraProvider.bindToLifecycle()时,进行传入。

camera = cameraProvider.bindToLifecycle(    this,CameraSelector.DEFAULT_BACK_CAMERA, preview, videoCapture )

需要注意的是,videoCapture无法和imageAnalysisimageCapture一起使用。
如果同一个页面中这几个功能融合在一起,则需要通过标志位来进行判断。

if (isVideo) {    mCamera = cameraProvider.bindToLifecycle(this, cameraSelector,            preview, videoCapture);} else {    mCamera = cameraProvider.bindToLifecycle(this, cameraSelector,            preview, imageCapture, imageAnalysis);}

开始录制

private val RECORDED_FILE_NAME = "recorded_video"private val RECORDED_FILE_NAME_END = "video/mp4"@SuppressLint("RestrictedApi")private fun startRecording() {//TODO 这里省略了RECORD_AUDIO、PERMISSION_GRANTED权限的判断    val contentValues = ContentValues()    contentValues.put(        MediaStore.MediaColumns.DISPLAY_NAME,        RECORDED_FILE_NAME + "_" + System.currentTimeMillis()    )    contentValues.put(MediaStore.MediaColumns.MIME_TYPE, RECORDED_FILE_NAME_END)    val outputFileOptions = VideoCapture.OutputFileOptions.Builder(        getContentResolver(),        MediaStore.Video.Media.EXTERNAL_CONTENT_URI, contentValues    ).build()    videoCapture.startRecording(        outputFileOptions,        ContextCompat.getMainExecutor(this),        object : VideoCapture.OnVideoSavedCallback {            override fun onVideoSaved(outputFileResults: VideoCapture.OutputFileResults) {                Log.i(TAG, "视频保存成功:${outputFileResults.savedUri}")            }            override fun onError(                videoCaptureError: Int,                message: String,                cause: Throwable?            ) {                Log.i(TAG, "当出现异常 cause:$cause")            }        }    )}

停止视频录制

videoCapture.stopRecording()

当我们执行停止视频录制之后,就可以在相册里看到多了一个录制的视频了。

介绍了CameraX里一些常用的UseCase,我们接下来来看下CameraX中的其他一些功能。

7. 切换前后摄像头

我们之前使用cameraProvider.bindToLifecycle()的时候,有一个参数是CameraSelector
CameraX默认给我们提供了前置摄像头和后置摄像头的CameraSelector

public final class CameraSelector {    @NonNull    public static final CameraSelector DEFAULT_FRONT_CAMERA =            new CameraSelector.Builder().requireLensFacing(LENS_FACING_FRONT).build();    @NonNull    public static final CameraSelector DEFAULT_BACK_CAMERA =            new CameraSelector.Builder().requireLensFacing(LENS_FACING_BACK).build();//...}

我们去切换摄像头的时候,就是重新调用一下bindPreview方法,传入新的cameraSelector值就好了

private fun bindPreview(        cameraProvider: ProcessCameraProvider,        previewView: PreviewView,        cameraSelector : CameraSelector    ) {        // 解除所有绑定,防止CameraProvider重复绑定到Lifecycle发生异常        cameraProvider.unbindAll()        preview = Preview.Builder().build()        camera = cameraProvider.bindToLifecycle(            this,            cameraSelector, preview        )        preview?.setSurfaceProvider(previewView.surfaceProvider)    }

CameraX还为我们提供了判断前置/后置摄像头是否存在的方法

private fun hasBackCamera(): Boolean {    return cameraProvider?.hasCamera(CameraSelector.DEFAULT_BACK_CAMERA) ?: false}private fun hasFrontCamera(): Boolean {    return cameraProvider?.hasCamera(CameraSelector.DEFAULT_FRONT_CAMERA) ?: false}

看下效果
在这里插入图片描述

8. 对焦

当点击androidx.camera.view.PreviewView的时候,去调用CameraX的对焦方法startFocusAndMetering()就好了。

onCreate()中添加如下代码

binding.previewView.setOnTouchListener { view, event ->    val action = FocusMeteringAction.Builder(        binding.previewView.getMeteringPointFactory()            .createPoint(event.getX(), event.getY())    ).build();    showTapView(event.x.toInt(), event.y.toInt())    camera?.getCameraControl()?.startFocusAndMetering(action)    true}

增加showTapView()

private fun showTapView(x: Int, y: Int) {    val popupWindow = PopupWindow(        ViewGroup.LayoutParams.WRAP_CONTENT,        ViewGroup.LayoutParams.WRAP_CONTENT    )    val imageView = ImageView(this)    imageView.setImageResource(R.drawable.ic_focus_view)    popupWindow.contentView = imageView    popupWindow.showAsDropDown(binding.previewView, x, y)    binding.previewView.postDelayed({ popupWindow.dismiss() }, 600)    binding.previewView.playSoundEffect(SoundEffectConstants.CLICK)}

看下效果
在这里插入图片描述

9. 缩放

通过GestureDetector监听缩放事件,然后在回调的时候进行执行如下代码,就可以使用双指放大缩小图像

override fun zoom(delta: Float) {val zoomState = camera?.cameraInfo?.zoomStatezoomState?.value?.let {val currentZoomRatio = it.zoomRatiocamera?.cameraControl?.setZoomRatio(currentZoomRatio * delta)}}

缩放操作具体详见这篇文章 Android使用CameraX实现相机快速实现放大缩小

10. 本文Demo下载

本文的源码Demo详见 : Android CameraX Demo : 实现预览/拍照/录制视频/图片分析/对焦/切换摄像头等操作

推荐阅读 我的另一篇关于Camera的文章 : Android 从零开发一个简易的相机App

参考
Android Developer | CameraX
新技术介绍来了,CameraX 一统江湖?

来源地址:https://blog.csdn.net/EthanCo/article/details/125603671

阅读原文内容投诉

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

软考中级精品资料免费领

  • 历年真题答案解析
  • 备考技巧名师总结
  • 高频考点精准押题
  • 2024年上半年信息系统项目管理师第二批次真题及答案解析(完整版)

    难度     801人已做
    查看
  • 【考后总结】2024年5月26日信息系统项目管理师第2批次考情分析

    难度     348人已做
    查看
  • 【考后总结】2024年5月25日信息系统项目管理师第1批次考情分析

    难度     311人已做
    查看
  • 2024年上半年软考高项第一、二批次真题考点汇总(完整版)

    难度     432人已做
    查看
  • 2024年上半年系统架构设计师考试综合知识真题

    难度     220人已做
    查看

相关文章

发现更多好内容

猜你喜欢

AI推送时光机
位置:首页-资讯-后端开发
咦!没有更多了?去看看其它编程学习网 内容吧
首页课程
资料下载
问答资讯