Getting started with CameraX
- 1. Before you start
- 2. Create project
- 3. Request camera permission
- 4. Implementation preview case
- 5. Implement ImageCapture use case
- 6. Implement image analysis use case
- 7. Congratulations!
This is the official translation of CameraX, the original page https://codelabs.developers.google.com/codelabs/camerax-getting-started/#5 , of course, it needs to climb over the wall. I have helped you climb over the wall and finished the translation. Please feel free to use it!
CameraX's source code Demo is here https://github.com/android/camera-samples/tree/master/CameraXBasic It can be downloaded directly without turning over the wall.
After personal use, it feels simpler and easier to use than camera and Camera2, and has better compatibility. There will be no common focus failure and other problems.
Blog writing is not easy, your likes and collections are my driving force, don't forget to like and collect_ ^!
1. Before you startIn this code lab, you will learn how to create a camera application that uses CameraX to display the viewfinder, take pictures, and analyze image streams from the camera.
To do this, we'll introduce the concept of use cases in CameraX, which you can use for a variety of camera operations, from displaying viewfinders to analyzing frames in real time.
precondition
- Basic Android development experience.
What can you do?
- Learn how to add CameraX dependencies.
- Learn how to display a camera preview in an activity. (preview use case)
- Build a photo taking application and save it to storage. (ImageCapture use case)
- Learn how to analyze frames in your camera in real time. (ImageAnalysis use case)
What do you need?
- Android devices. Android Studio's emulator can also be used. We recommend R and later. The image analysis use case is not applicable to any object lower than R.
- The minimum supported API level is 21.
- Android Studio 3.6 or later.
1) Project new
- Using the Android Studio menu, start a new project and choose clear activity when prompted.
- Next, name the application CameraX App. Make sure the language is set to Kotlin, the minimum API level is 21 (which is the minimum required for CameraX), and that you are using Android x artifacts.
2) Add Gradle dependency
1… open build.gradle(Module: app) file, and add CameraX dependency to the dependency section of our application Gradle file:
def camerax_version = "1.0.0-beta03" // CameraX core library using camera2 implementation implementation "androidx.camera:camera-camera2:$camerax_version" // CameraX Lifecycle Library implementation "androidx.camera:camera-lifecycle:$camerax_version" // CameraX View class implementation "androidx.camera:camera-view:1.0.0-alpha10"
- CameraX requires some methods in Java 8, so we need to set the compile options accordingly. At the end of the android block, followed by buildTypes, add the following:
compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 }
When prompted, click synchronize now and we'll be ready to use CameraX in our application.
3) Create viewfinder layout
Let's replace the default layout with
- Open activity_main layout file and replace it with this code.
<?xml version="1.0" encoding="utf-8"?> <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".MainActivity"> <Button android:id="@+id/camera_capture_button" android:layout_width="100dp" android:layout_height="100dp" android:layout_marginBottom="50dp" android:scaleType="fitCenter" android:text="Take Photo" app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintRight_toRightOf="parent" app:layout_constraintBottom_toBottomOf="parent" android:elevation="2dp" /> <androidx.camera.view.PreviewView android:id="@+id/viewFinder" android:layout_width="match_parent" android:layout_height="match_parent" /> </androidx.constraintlayout.widget.ConstraintLayout>
4) Settings MainActivity.kt
- Replace code in with MainActivity.kt . It includes import statements, variables to be instantiated, functions to be implemented, and constants.
onCreate() has implemented the cameraExecutor function that you check the camera permission, start the camera, onClickListener() sets and implements outputDirectory and for the Photo button. Even if onCreate() is implemented for you, the camera won't work until you implement the methods in the file.
package com.example.cameraxapp import androidx.appcompat.app.AppCompatActivity import android.os.Bundle import android.Manifest import android.content.pm.PackageManager import android.net.Uri import android.util.Log import android.widget.Button import android.widget.Toast import androidx.core.app.ActivityCompat import androidx.core.content.ContextCompat import java.util.concurrent.Executors import androidx.camera.core.* import androidx.camera.lifecycle.ProcessCameraProvider import androidx.camera.view.PreviewView import kotlinx.android.synthetic.main.activity_main.* import java.io.File import java.nio.ByteBuffer import java.text.SimpleDateFormat import java.util.* import java.util.concurrent.ExecutorService typealias LumaListener = (luma: Double) -> Unit class MainActivity : AppCompatActivity() { private var preview: Preview? = null private var imageCapture: ImageCapture? = null private var imageAnalyzer: ImageAnalysis? = null private var camera: Camera? = null private lateinit var outputDirectory: File private lateinit var cameraExecutor: ExecutorService override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) // Request camera permissions if (allPermissionsGranted()) { startCamera() } else { ActivityCompat.requestPermissions( this, REQUIRED_PERMISSIONS, REQUEST_CODE_PERMISSIONS) } // Setup the listener for take photo button camera_capture_button.setOnClickListener { takePhoto() } outputDirectory = getOutputDirectory() cameraExecutor = Executors.newSingleThreadExecutor() } private fun startCamera() { // TODO } private fun takePhoto() { // TODO } private fun allPermissionsGranted() = false fun getOutputDirectory(): File { val mediaDir = externalMediaDirs.firstOrNull()?.let { File(it, resources.getString(R.string.app_name)).apply { mkdirs() } } return if (mediaDir != null && mediaDir.exists()) mediaDir else filesDir } companion object { private const val TAG = "CameraXBasic" private const val FILENAME_FORMAT = "yyyy-MM-dd-HH-mm-ss-SSS" private const val REQUEST_CODE_PERMISSIONS = 10 private val REQUIRED_PERMISSIONS = arrayOf(Manifest.permission.CAMERA) } }
- Run the code as follows:
Before the application can open the camera, you need to obtain the user's permission to do so. In this step, you will implement camera permissions.
- open AndroidManifest.xml And add these lines before the application tag.
<uses-feature android:name="android.hardware.camera.any" /> <uses-permission android:name="android.permission.CAMERA" />
Add action android.hardware.camera.any ensures that the device has a camera. Specifying. Any means that it can be a front camera or a rear camera.
If you android.hardware.camera Without. any, your device cannot be used if it does not have a rear camera (for example, most chromebooks). The second line adds access to the camera.
- Copy this code to MainActivity.kt .
In the bullet below, the code you just copied is exploded.
override fun onRequestPermissionsResult( requestCode: Int, permissions: Array<String>, grantResults: IntArray) { if (requestCode == REQUEST_CODE_PERMISSIONS) { if (allPermissionsGranted()) { startCamera() } else { Toast.makeText(this, "Permissions not granted by the user.", Toast.LENGTH_SHORT).show() finish() } } }
- Check that the request code is correct; if it is not ignored.
if (requestCode == REQUEST_CODE_PERMISSIONS) { }
- If permission is granted, call startCamera().
if (allPermissionsGranted()) { startCamera() }
- If permission is not granted, raise your glass to inform the user that permission is not granted.
else { Toast.makeText(this, "Permissions not granted by the user.", Toast.LENGTH_SHORT).show() finish() }
- Replace this allPermissionsGranted() method with:
private fun allPermissionsGranted() = REQUIRED_PERMISSIONS.all { ContextCompat.checkSelfPermission( baseContext, it) == PackageManager.PERMISSION_GRANTED }
- Run the application.
Now, it should get permission to use the camera:
In the camera application, the viewfinder is used to let users preview the photos to be taken. You can use the CameraX Preview class to implement the viewfinder.
To use Preview, you first need to define a configuration and then use it to create an instance of the use case. The generated instance is one that you need to bind to the CameraX lifecycle.
- Copy this code into the startCamera() function.
The following points will break down the code you just copied.
private fun startCamera() { val cameraProviderFuture = ProcessCameraProvider.getInstance(this) cameraProviderFuture.addListener(Runnable { // Used to bind the lifecycle of cameras to the lifecycle owner val cameraProvider: ProcessCameraProvider = cameraProviderFuture.get() // Preview preview = Preview.Builder() .build() // Select back camera val cameraSelector = CameraSelector.Builder().requireLensFacing(CameraSelector.LENS_FACING_BACK).build() try { // Unbind use cases before rebinding cameraProvider.unbindAll() // Bind use cases to camera camera = cameraProvider.bindToLifecycle( this, cameraSelector, preview) preview?.setSurfaceProvider(viewFinder.createSurfaceProvider(camera?.cameraInfo)) } catch(exc: Exception) { Log.e(TAG, "Use case binding failed", exc) } }, ContextCompat.getMainExecutor(this)) }
- Created instance ProcessCameraProvider. This is used to bind the camera's lifecycle to the lifecycle owner. Because CameraX has lifecycle awareness, you don't have to worry about turning the camera on and off.
val cameraProviderFuture = ProcessCameraProvider.getInstance(this)
- Listener added to cameraProviderFuture. Add a Runnable as a parameter, which we'll populate later. Add as the second parameter, which will return one running on the main thread. ContextCompat.getMainExecutor()Executor
cameraProviderFuture.addListener(Runnable {}, ContextCompat.getMainExecutor(this))
- In Runnable, add ProcessCameraProvider to bind the camera's lifecycle to the lifecycle owner in the application process.
val cameraProvider: ProcessCameraProvider = cameraProviderFuture.get()
- Initialize your Preview object.
preview = Preview.Builder().build()
- A cameraselector object, and then use the CameraSelector.Builder.requireLensFacing Method to transfer your favorite shots.
val cameraSelector = CameraSelector.Builder().requireLensFacing(CameraSelector.LENS_FACING_BACK).build()
- Create a try block. Within this block, make sure that nothing is bound to the cameraProvider, and then bind the cameraSelector and preview object to the cameraProvider. Attach the Surface provider of the viewFinder to the preview use case.
try { cameraProvider.unbindAll() camera = cameraProvider.bindToLifecycle( this, cameraSelector, preview) preview?.setSurfaceProvider(viewFinder.createSurfaceProvider(camera?.cameraInfo)) }
- There are several ways this code can fail, such as if the application is no longer being watched. Wrap this code in a catch block to record if it failed.
catch(exc: Exception) { Log.e(TAG, "Use case binding failed", exc) }
- Run the application and you should see a camera preview!
Other use cases work much like Preview. First, you must define a configuration object that instantiates the actual use case object. To capture photos, you need to implement the takePhoto() method, which is called when you press the capture button.
Copy this code into the takePhoto() method.
The following points will break down the code you just copied.
private fun takePhoto() { // Get a stable reference of the modifiable image capture use case val imageCapture = imageCapture ?: return // Create timestamped output file to hold the image val photoFile = File( outputDirectory, SimpleDateFormat(FILENAME_FORMAT, Locale.US ).format(System.currentTimeMillis()) + ".jpg") // Create output options object which contains file + metadata val outputOptions = ImageCapture.OutputFileOptions.Builder(photoFile).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) } override fun onImageSaved(output: ImageCapture.OutputFileResults) { val savedUri = Uri.fromFile(photoFile) val msg = "Photo capture succeeded: $savedUri" Toast.makeText(baseContext, msg, Toast.LENGTH_SHORT).show() Log.d(TAG, msg) } }) }
- First, get a reference to the ImageCapture use case. If the use case is empty, this function is returned. If you click the Photo button before setting image capture, this property is null. If there is no return declaration, the application will crash null.
val imageCapture = imageCapture ?: return
- Next, create a file to save the image. Add a time stamp so that the file name is unique.
val photoFile = File( outputDirectory, SimpleDateFormat(FILENAME_FORMAT, Locale.US ).format(System.currentTimeMillis()) + ".jpg")
- Create an OutputFileOptions object. You can specify content about the output effect in this object. You want to save the output in the file we just created, so add your photoFile.
val outputOptions = ImageCapture.OutputFileOptions.Builder(photoFile).build()
- Call the upper imageCapture object of takePicture(). Pass in outputOptions, execute the program and save the image's callback. Next, you will fill in the callback.
imageCapture.takePicture( outputOptions, ContextCompat.getMainExecutor(this), object : ImageCapture.OnImageSavedCallback {} )
- In case of image capture failure or saving image capture failure, add error condition to record its failure.
override fun onError(exc: ImageCaptureException) { Log.e(TAG, "Photo capture failed: $", exc) }
- If the shooting does not fail, the photo has been taken successfully! Save the photo to the file you created earlier, make a toast to let the user know that the photo was successful, and then print the log statement.
override fun onImageSaved(output: ImageCapture.OutputFileResults) { val savedUri = Uri.fromFile(photoFile) val msg = "Photo capture succeeded: $savedUri" Toast.makeText(baseContext, msg, Toast.LENGTH_SHORT).show() Log.d(TAG, msg) }
- Go to the startCamera() method and copy the code under the code for preview.
imageCapture = ImageCapture.Builder() .build()
This shows where to paste the code in the method:
private fun startCamera() { ... preview = Preview.Builder() .build() // Paste image capture code here! val cameraSelector = CameraSelector.Builder().requireLensFacing(CameraSelector.LENS_FACING_BACK).build() ... }
- Finally, bindtolife() updates the call to in the try block to include the new use case:
camera = cameraProvider.bindToLifecycle( this, cameraSelector, preview, imageCapture)
- Run the application again and press take a picture.
You will see the toast and the messages in the log on the screen.
View Photos
6. Check the log statement, and you will see a log to announce that the photo was captured successfully.
2020-04-24 15:13:26.146 11981-11981/com.example.cameraxapp D/CameraXBasic: Photo capture succeeded: file:///storage/emulated/0/Android/media/com.example.cameraxapp/CameraXApp/2020-04-24-15-13-25-746.jpg
- Copy the storage location of the file, omit file:// prefix.
/storage/emulated/0/Android/media/com.example.cameraxapp/CameraXApp/2020-04-24-15-13-25-746.jpg
- In the Android Studio terminal, run the following command:
adb shell cp [INSERT THE FILE FROM STEP 2 HERE] /sdcard/Download/photo.jpg
- Run this ADB command and exit the shell:
adb pull /sdcard/Download/photo.jpg
- You can view the names saved in the current folder as photo.jpg The photos in the file.
If you are running Q or earlier, preview is implemented at the same time, image capture and image analysis will not be applicable to Android Studio's device simulator. We recommend using real equipment to test this part of the code lab.
Use this imageanalysis feature to make your camera application more interesting. It allows you to define the ImageAnalysis.Analyzer Interface, which will be called with the incoming camera frame. You don't have to worry about managing camera session states or even images. The life cycle required to bind to our application is enough, just like other life cycle aware components.
- Add this parser as an inner class in MainActivity.kt .
The analyzer records the average brightness of the image. To create an analyzer, you need analyze to implement the ImageAnalysis.Analyzer The function is overridden in the class of the interface.
private class LuminosityAnalyzer(private val listener: LumaListener) : ImageAnalysis.Analyzer { private fun ByteBuffer.toByteArray(): ByteArray { rewind() // Rewind the buffer to zero val data = ByteArray(remaining()) get(data) // Copy the buffer into a byte array return data // Return the byte array } override fun analyze(image: ImageProxy) { val buffer = image.planes[0].buffer val data = buffer.toByteArray() val pixels = data.map { it.toInt() and 0xFF } val luma = pixels.average() listener(luma) image.close() } }
With our class implementation ImageAnalysis.Analyzer All we need to do is instantiate an instance of luminosity analyzer before imageanalysis like all other use cases and update startCamera() function to call again CameraX.bindToLifecycle():
- In the startCamera() method, add this code to imageCapture() under the code.
imageAnalyzer = ImageAnalysis.Builder() .build() .also { it.setAnalyzer(cameraExecutor, LuminosityAnalyzer { luma -> Log.d(TAG, "Average luminosity: $luma") }) }
This shows where to paste the code in the method:
private fun startCamera() { ... imageCapture = ImageCapture.Builder() .setCaptureMode(ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY) .build() // Paste image analyzer code here! val cameraSelector = CameraSelector.Builder().requireLensFacing(CameraSelector.LENS_FACING_BACK).build() ... }
- Update bindtolife() on to call cameraProvider to include imageAnalyzer.
camera = cameraProvider.bindToLifecycle( this, cameraSelector, preview, imageCapture, imageAnalyzer)
- Run the application now! It will generate a similar message in logcat about every second.
D/CameraXApp: Average luminosity: ...7. Congratulations!
You have successfully implemented the following into your new Android app from scratch:
- CameraX dependencies included in the project.
- Shows the camera viewfinder (using the preview use case)
- Implement photo capture and save images to storage (using the ImageCapture use case)
- Perform frame analysis from the camera in real time (using the ImageAnalysis use case)
Blog writing is not easy, your likes and collections are my driving force, don't forget to like and collect_ ^!
Related links: