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

Posted Heiko-Android

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Android 使用CameraX实现预览/拍照/录制视频/图片分析/对焦/缩放/切换摄像头等操作相关的知识,希望对你有一定的参考价值。

1. CameraX架构

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

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

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

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

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

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

  • 预览 : Preview.java
  • 图片分析 : ImageAnalysis.java
  • 图片拍摄 : ImageCapture.java
  • 视频拍摄 : VideoCapture.java

可以看到,这几个类都是继承自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 implementation
def camerax_version = "1.2.0-alpha02" //1.2.0-alpha02
// 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 VideoCapture library
implementation "androidx.camera:camera-video:$camerax_version"
// If you want to additionally use the CameraX View class
implementation "androidx.camera:camera-view:$camerax_version"
// If you want to additionally add CameraX ML Kit Vision Integration
implementation "androidx.camera:camera-mlkit-vision:$camerax_version"
// If you want to additionally use the CameraX Extensions library
implementation "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 拍照并保存到本地存储

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

/** Helper function used to create a timestamped file */
private fun createFile(baseFolder: File, format: String, extension: String) =
    File(
        baseFolder, SimpleDateFormat(format, Locale.US)
            .format(System.currentTimeMillis()) + extension
    )

/** Use external media if it is available, our app's file directory otherwise */
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还为我们提供了判断前置/后置摄像头是否存在的方法

/** Returns true if the device has an available back camera. False otherwise */
private fun hasBackCamera(): Boolean 
    return cameraProvider?.hasCamera(CameraSelector.DEFAULT_BACK_CAMERAAndroid CameraX使用,预览拍照获取静态图像

Android 使用CameraX拍照预览

Android 使用CameraX拍照预览

使用CameraX几行代码实现摄像头预览和拍照

Android的camerax预览拍照

Android的camerax预览拍照