1 Introduction
Because it needs to be used at work Android Camera2 API
, but because Camera2
it 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 CSDN
record it on the website, hoping to help more friends.
In the previous article, we Camera2
implemented the function of camera preview. In this article, we follow the previous article to realize Camera2
the 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 startPreview
in
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 targets
and 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 imageReader
to prevent imageReader
the 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 setOnImageAvailableListener
register a listener, ImageReader
called when a new image is available in.
//Start a new image queue
val imageQueue = ArrayBlockingQueue<Image>(IMAGE_BUFFER_SIZE)
There are two parameters for imageReader
setting a OnImageAvailableListener
monitor
setOnImageAvailableListener
, the first parameter is OnImageAvailableListener
the interface, and the second parameter is Handler
, we imageReaderHandler
can pass it in here. In the monitoring, the next one
OnImageAvailableListener
will be obtained and added toimageReader
image
imageQueue
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.createCaptureRequest
creatingCaptureRequest.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.CaptureCallback
callback 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 onCaptureCompleted
called, it will call the asynchronous thread, then imageQueue
take out image
, setOnImageAvailableListener
set the listener to null
and then call saveResult
the 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 saveImage
the method.
First, it will judge whether it is JPEG
a format. If it is JPEG
a format, simply save it. If it is not a format, this article will bytes
be 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 createFile
used 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.
We open the file manager, and /sdcard/Android/data/包名/files/image
we can see this picture under the folder,
but we find that the direction of this picture is wrong.
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 OrientationLiveData
this LiveData
class
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 relativeOrientation
and register an observer Observer
to 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 onCaptureCompleted
the callback, saveImage
after 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
5. Destroy the camera
When the Activity is destroyed, we also have to destroy the camera. Compared with the previous article, there are more imageRecorder
destructions
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 Camera2
completed 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.