Take photos with Android Camera2 camera in ten minutes

1 Introduction

Because it needs to be used at work Android Camera2 API, but because Camera2it is more complicated and the online information is messy, there is a certain entry threshold, so it took a few days to systematically study it and CSDNrecord it on the website, hoping to help more friends.
In the previous article, we Camera2implemented the function of camera preview. In this article, we follow the previous article to realize Camera2the function of camera taking pictures.

2. Pre-operation

2.1 Declare camera parameters and member variables

First of all, declare camera parameters and member variables, which have been added compared to the previous

private lateinit var imageReader: ImageReader
//JPEG格式,所有相机必须支持JPEG输出,因此不需要检查
private val pixelFormat = ImageFormat.JPEG
//imageReader最大的图片缓存数
private val IMAGE_BUFFER_SIZE: Int = 3
//线程池
private val threadPool = Executors.newCachedThreadPool()
private val imageReaderThread = HandlerThread("imageReaderThread").apply {
    
     start() }
private val imageReaderHandler = Handler(imageReaderThread.looper)

The complete camera parameters and member variables that need to be declared are as follows

//后摄 : 0 ,前摄 : 1
private val cameraId = "0"
private val TAG = CameraActivity::class.java.simpleName
private lateinit var cameraDevice: CameraDevice
private val cameraThread = HandlerThread("CameraThread").apply {
    
     start() }
private val cameraHandler = Handler(cameraThread.looper)
private val cameraManager: CameraManager by lazy {
    
    
    getSystemService(Context.CAMERA_SERVICE) as CameraManager
}
private val characteristics: CameraCharacteristics by lazy {
    
    
    cameraManager.getCameraCharacteristics(cameraId)
}
private lateinit var session: CameraCaptureSession

private lateinit var imageReader: ImageReader
//JPEG格式,所有相机必须支持JPEG输出,因此不需要检查
private val pixelFormat = ImageFormat.JPEG
//imageReader最大的图片缓存数
private val IMAGE_BUFFER_SIZE: Int = 3
//线程池
private val threadPool = Executors.newCachedThreadPool()
private val imageReaderThread = HandlerThread("imageReaderThread").apply {
    
     start() }
private val imageReaderHandler = Handler(imageReaderThread.looper)

2.2 Initialize imageReader

We need to initialize at the right time imageReader
Here we put it startPreviewin

private fun startPreview() {
    
    
    // Initialize an image reader which will be used to capture still photos
    val size = characteristics.get(
        CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP
    )!!.getOutputSizes(pixelFormat).maxByOrNull {
    
     it.height * it.width }!!

    imageReader = ImageReader.newInstance(
        size.width, size.height, pixelFormat, IMAGE_BUFFER_SIZE
    )

	//...原本的代码...
}

2.3 Associate imageReader with Session

First of all, we need to startPreview()modify targets
the original in the method targetsand only pass inbinding.surfaceView.holder.surface

val targets = listOf(binding.surfaceView.holder.surface)

Now pass in one moreimageReader.surface

val targets = listOf(binding.surfaceView.holder.surface,imageReader.surface)

The complete code is as follows

private fun startPreview() {
    
    
	fitSize = characteristics.get(
	    CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP
	)!!.getOutputSizes(pixelFormat).maxByOrNull {
    
     it.height * it.width }!!
	
	imageReader = ImageReader.newInstance(
	    fitSize.width, fitSize.height, pixelFormat, IMAGE_BUFFER_SIZE
	)
	val targets = listOf(binding.surfaceView.holder.surface,imageReader.surface)
	cameraDevice.createCaptureSession(targets, object : CameraCaptureSession.StateCallback() {
    
    
        override fun onConfigured(session: CameraCaptureSession) {
    
    
            this@CameraActivity2.session = session

            val captureRequest = cameraDevice.createCaptureRequest(
                CameraDevice.TEMPLATE_PREVIEW
            ).apply {
    
     addTarget(binding.surfaceView.holder.surface) }

            //这将不断地实时发送视频流,直到会话断开或调用session.stoprepeat()
            session.setRepeatingRequest(captureRequest.build(), null, cameraHandler)
        }

        override fun onConfigureFailed(session: CameraCaptureSession) {
    
    
            Toast.makeText(application, "session configuration failed", Toast.LENGTH_SHORT)
                .show()
        }
    }, cameraHandler)
}

3. Realize the camera function

3.1 Empty imageReader

First of all, you need to clear it imageReaderto prevent imageReaderthe cache

// Flush any images left in the image reader
 while (imageReader.acquireNextImage() != null) {
    
    
 }

3.2 Set OnImageAvailableListener to monitor

Then create a new queue Queue, call to setOnImageAvailableListenerregister a listener, ImageReadercalled when a new image is available in.

//Start a new image queue
val imageQueue = ArrayBlockingQueue<Image>(IMAGE_BUFFER_SIZE)

There are two parameters for imageReadersetting a OnImageAvailableListenermonitor
setOnImageAvailableListener, the first parameter is OnImageAvailableListenerthe interface, and the second parameter is Handler, we imageReaderHandlercan pass it in here. In the monitoring, the next one
OnImageAvailableListenerwill be obtained and added toimageReaderimageimageQueue

imageReader.setOnImageAvailableListener({
    
     reader ->
     val image = reader.acquireNextImage()
     Log.d(TAG, "Image available in queue: ${
      
      image.timestamp}")
     imageQueue.add(image)
 }, imageReaderHandler)

3.3 Create CaptureRequest.Builder

Next, by session.device.createCaptureRequestcreatingCaptureRequest.Builder

val captureRequest = session.device.createCaptureRequest(
        CameraDevice.TEMPLATE_STILL_CAPTURE
    ).apply {
    
     addTarget(imageReader.surface) }

3.4 Call session.capture() to take pictures

Then call session.capture()to take pictures, need to pass in captureRequest, CameraCaptureSession.CaptureCallbackcallback andHandler

session.capture(captureRequest.build(),
   object : CameraCaptureSession.CaptureCallback() {
    
    
       override fun onCaptureCompleted(
           session: CameraCaptureSession,
           request: CaptureRequest,
           result: TotalCaptureResult
       ) {
    
    
           super.onCaptureCompleted(session, request, result)
           //待实现
       }
   },
   cameraHandler
)

3.5 Photo callback processing

When onCaptureCompletedcalled, it will call the asynchronous thread, then imageQueuetake out image, setOnImageAvailableListenerset the listener to null
and then call saveResultthe method to save the picture to the local storage.

threadPool.execute {
    
    
    val image = imageQueue.take()
    imageReader.setOnImageAvailableListener(null, null)
    val file = saveImage(image)
    if (file.exists()) {
    
    
        runOnUiThread {
    
    
            Toast.makeText(application, "拍照成功", Toast.LENGTH_SHORT).show()
        }
    }
}

3.6 Save the picture locally

Let’s take a look at saveImagethe method.
First, it will judge whether it is JPEG a format. If it is JPEGa format, simply save it. If it is not a format, this article will bytesbe skipped and not implemented. Friends who need it can go to the official demo.
JPEG

private fun saveImage(image: Image): File {
    
    
    when (image.format) {
    
    
        //当format是JPEG或PEPTH JPEG时,我们可以简单地保存bytes
        ImageFormat.JPEG, ImageFormat.DEPTH_JPEG -> {
    
    
            val buffer = image.planes[0].buffer
            val bytes = ByteArray(buffer.remaining()).apply {
    
     buffer.get(this) }
            try {
    
    
                val output = createFile(this@CameraActivity2, "jpg")
                FileOutputStream(output).use {
    
     it.write(bytes) }
                return output
            } catch (exc: IOException) {
    
    
                Log.e(TAG, "Unable to write JPEG image to file", exc)
                throw exc
            }
        }
        //本示例未实现其他格式
        else -> {
    
    
            val exc = RuntimeException("Unknown image format: ${
      
      image.format}")
            throw exc
        }
    }
}

Here is createFileused to get a file path

fun createFile(context: Context, extension: String): File {
    
    
    val sdf = SimpleDateFormat("yyyy_MM_dd_HH_mm_ss_SSS", Locale.US)
    val imageDir = context.getExternalFilesDir("image")
    return File(imageDir, "IMG_${
      
      sdf.format(Date())}.$extension")
}

3.7 The complete code of the photo taking part

Let's take a look at the complete code of the photo taking part

binding.btnTakePicture.setOnClickListener {
    
    
	// Flush any images left in the image reader
	@Suppress("ControlFlowWithEmptyBody")
	while (imageReader.acquireNextImage() != null) {
    
    
	}
	
	// Start a new image queue
	val imageQueue = ArrayBlockingQueue<Image>(IMAGE_BUFFER_SIZE)
	imageReader.setOnImageAvailableListener({
    
     reader ->
	    val image = reader.acquireNextImage()
	    Log.d(TAG, "Image available in queue: ${
      
      image.timestamp}")
	    imageQueue.add(image)
	}, imageReaderHandler)
	
	val captureRequest = session.device.createCaptureRequest(
	    CameraDevice.TEMPLATE_STILL_CAPTURE
	).apply {
    
     addTarget(imageReader.surface) }
	session.capture(
	    captureRequest.build(),
	    object : CameraCaptureSession.CaptureCallback() {
    
    
	        override fun onCaptureCompleted(
	            session: CameraCaptureSession,
	            request: CaptureRequest,
	            result: TotalCaptureResult
	        ) {
    
    
	            super.onCaptureCompleted(session, request, result)
	
	            threadPool.execute {
    
    
	                val image = imageQueue.take()
	                imageReader.setOnImageAvailableListener(null, null)
	                val file = saveImage(image)
	                if (file.exists()) {
    
    
	                    runOnUiThread {
    
    
	                        Toast.makeText(application, "拍照成功", Toast.LENGTH_SHORT).show()
	                    }
	                }
	            }
	        }
	    },
	    cameraHandler
	)
}

private fun saveImage(image: Image): File {
    
    
    when (image.format) {
    
    
        //当format是JPEG或PEPTH JPEG时,我们可以简单地保存bytes
        ImageFormat.JPEG, ImageFormat.DEPTH_JPEG -> {
    
    
            val buffer = image.planes[0].buffer
            val bytes = ByteArray(buffer.remaining()).apply {
    
     buffer.get(this) }
            try {
    
    
                val output = createFile(this@CameraActivity2, "jpg")
                FileOutputStream(output).use {
    
     it.write(bytes) }
                return output
            } catch (exc: IOException) {
    
    
                Log.e(TAG, "Unable to write JPEG image to file", exc)
                throw exc
            }
        }
        //本示例未实现其他格式
        else -> {
    
    
            val exc = RuntimeException("Unknown image format: ${
      
      image.format}")
            throw exc
        }
    }
}

fun createFile(context: Context, extension: String): File {
    
    
    val sdf = SimpleDateFormat("yyyy_MM_dd_HH_mm_ss_SSS", Locale.US)
    val imageDir = context.getExternalFilesDir("image")
    return File(imageDir, "IMG_${
      
      sdf.format(Date())}.$extension")
}

We run the program, click the button to take a picture, and we can find that the picture is taken successfully.
insert image description here
We open the file manager, and /sdcard/Android/data/包名/files/imagewe can see this picture under the folder,
but we find that the direction of this picture is wrong.

insert image description here

4. Correct image orientation

We can see the picture taken before, the direction is wrong, so we need to correct the direction of the picture

First add OrientationLiveDatathis LiveDataclass

import android.content.Context
import android.hardware.camera2.CameraCharacteristics
import android.view.OrientationEventListener
import android.view.Surface
import androidx.lifecycle.LiveData


/**
 * Calculates closest 90-degree orientation to compensate for the device
 * rotation relative to sensor orientation, i.e., allows user to see camera
 * frames with the expected orientation.
 */
class OrientationLiveData(
        context: Context,
        characteristics: CameraCharacteristics
): LiveData<Int>() {
    
    

    private val listener = object : OrientationEventListener(context.applicationContext) {
    
    
        override fun onOrientationChanged(orientation: Int) {
    
    
            val rotation = when {
    
    
                orientation <= 45 -> Surface.ROTATION_0
                orientation <= 135 -> Surface.ROTATION_90
                orientation <= 225 -> Surface.ROTATION_180
                orientation <= 315 -> Surface.ROTATION_270
                else -> Surface.ROTATION_0
            }
            val relative = computeRelativeRotation(characteristics, rotation)
            if (relative != value) postValue(relative)
        }
    }

    override fun onActive() {
    
    
        super.onActive()
        listener.enable()
    }

    override fun onInactive() {
    
    
        super.onInactive()
        listener.disable()
    }

    companion object {
    
    

        /**
         * Computes rotation required to transform from the camera sensor orientation to the
         * device's current orientation in degrees.
         *
         * @param characteristics the [CameraCharacteristics] to query for the sensor orientation.
         * @param surfaceRotation the current device orientation as a Surface constant
         * @return the relative rotation from the camera sensor to the current device orientation.
         */
        @JvmStatic
        private fun computeRelativeRotation(
                characteristics: CameraCharacteristics,
                surfaceRotation: Int
        ): Int {
    
    
            val sensorOrientationDegrees =
                    characteristics.get(CameraCharacteristics.SENSOR_ORIENTATION)!!

            val deviceOrientationDegrees = when (surfaceRotation) {
    
    
                Surface.ROTATION_0 -> 0
                Surface.ROTATION_90 -> 90
                Surface.ROTATION_180 -> 180
                Surface.ROTATION_270 -> 270
                else -> 0
            }

            // Reverse device orientation for front-facing cameras
            val sign = if (characteristics.get(CameraCharacteristics.LENS_FACING) ==
                    CameraCharacteristics.LENS_FACING_FRONT) 1 else -1

            // Calculate desired JPEG orientation relative to camera orientation to make
            // the image upright relative to the device orientation
            return (sensorOrientationDegrees - (deviceOrientationDegrees * sign) + 360) % 360
        }
    }
}

Declare in the Activity relativeOrientationand register an observer Observerto be notified when the orientation changes.

// Used to rotate the output media to match device orientation
relativeOrientation = OrientationLiveData(this, characteristics).apply {
    
    
    observe(this@CameraActivity2, Observer {
    
     orientation ->
        Log.d(TAG, "Orientation changed: $orientation")
    })
}

Then in onCaptureCompletedthe callback, saveImageafter that, add the following code to modify the orientation of the image

// Compute EXIF orientation metadata
val rotation = relativeOrientation.value ?: 0
val mirrored = characteristics.get(CameraCharacteristics.LENS_FACING) ==
        CameraCharacteristics.LENS_FACING_FRONT
val exifOrientation = computeExifOrientation(rotation, mirrored)

val exif = ExifInterface(file.absolutePath)
exif.setAttribute(
    ExifInterface.TAG_ORIENTATION, exifOrientation.toString()
)
exif.saveAttributes()
/** Transforms rotation and mirroring information into one of the [ExifInterface] constants */
fun computeExifOrientation(rotationDegrees: Int, mirrored: Boolean) = when {
    
    
    rotationDegrees == 0 && !mirrored -> ExifInterface.ORIENTATION_NORMAL
    rotationDegrees == 0 && mirrored -> ExifInterface.ORIENTATION_FLIP_HORIZONTAL
    rotationDegrees == 180 && !mirrored -> ExifInterface.ORIENTATION_ROTATE_180
    rotationDegrees == 180 && mirrored -> ExifInterface.ORIENTATION_FLIP_VERTICAL
    rotationDegrees == 270 && mirrored -> ExifInterface.ORIENTATION_TRANSVERSE
    rotationDegrees == 90 && !mirrored -> ExifInterface.ORIENTATION_ROTATE_90
    rotationDegrees == 90 && mirrored -> ExifInterface.ORIENTATION_TRANSPOSE
    rotationDegrees == 270 && mirrored -> ExifInterface.ORIENTATION_ROTATE_270
    rotationDegrees == 270 && !mirrored -> ExifInterface.ORIENTATION_TRANSVERSE
    else -> ExifInterface.ORIENTATION_UNDEFINED
}

Run the program again, and the direction of the picture will be normal
insert image description here

5. Destroy the camera

When the Activity is destroyed, we also have to destroy the camera. Compared with the previous article, there are more imageRecorderdestructions

override fun onStop() {
    
    
    super.onStop()
    try {
    
    
        cameraDevice.close()
    } catch (exc: Throwable) {
    
    
        Log.e(TAG, "Error closing camera", exc)
    }
}

override fun onDestroy() {
    
    
    super.onDestroy()
    cameraThread.quitSafely()
    imageReaderThread.quitSafely()
}

So far we have Camera2completed the camera function.

6. Other

6.1 Download the source code of this article

Download link: Android Camera2 Demo - Realize camera preview, photo taking, recording video functions

6.2 Android Camera2 Series

For more Camera2 related articles, please see
Realize Android Camera2 Camera Preview in
Ten Minutes
-CSDN blog

6.3 Android camera related articles

Android uses CameraX to achieve preview/photograph/record video/picture analysis/focus/zoom/switch camera
etc.

Guess you like

Origin blog.csdn.net/EthanCo/article/details/131414981