Android Camera2 教程 · 第四章 · 拍照 您所在的位置:网站首页 相机快门声音怎么调整的 Android Camera2 教程 · 第四章 · 拍照

Android Camera2 教程 · 第四章 · 拍照

2024-07-16 16:40| 来源: 网络整理| 查看: 265

上一章《Camera2 预览》我们学习了如何配置预览,接下来我们来学习如何拍照。

阅读完本章,你将会学到以下几个知识点:

理解 Capture 工作流程如何拍摄单张照片如何连续拍摄多张照片如何连拍照片如何配置缩略图尺寸如何播放快门音效如何矫正图片方向如何切换前后置摄像头

你可以在 https://github.com/darylgo/Camera2Sample 下载相关的源码,并且切换到 Tutorial4 标签下。

1 理解 Capture 工作流程

在正式介绍如何拍照之前,我们有必要深入理解几种不同模式的 Capture 的工作流程,只要理解它们的工作流程就很容易掌握各种拍照模式的实现原理,在第一章《Camera2 概览》 里我们介绍了 Capture 有以下几种不同模式:

单次模式(One-shot):指的是只执行一次的 Capture 操作,例如设置闪光灯模式、对焦模式和拍一张照片等。多个单次模式的 Capture 会进入队列按顺序执行。

多次模式(Burst):指的是连续多次执行指定的 Capture 操作,该模式和多次执行单次模式的最大区别是连续多次 Capture 期间不允许插入其他任何 Capture 操作,例如连续拍摄 100 张照片,在拍摄这 100 张照片期间任何新的 Capture 请求都会排队等待,直到拍完 100 张照片。多组多次模式的 Capture 会进入队列按顺序执行。

重复模式(Repeating):指的是不断重复执行指定的 Capture 操作,当有其他模式的 Capture 提交时会暂停该模式,转而执行其他被模式的 Capture,当其他模式的 Capture 执行完毕后又会自动恢复继续执行该模式的 Capture,例如显示预览画面就是不断 Capture 获取每一帧画面。该模式的 Capture 是全局唯一的,也就是新提交的重复模式 Capture 会覆盖旧的重复模式 Capture。

我们举个例子来进一步说明上面三种模式,假设我们的相机应用程序开启了预览,所以会提交一个重复模式的 Capture 用于不断获取预览画面,然后我们提交一个单次模式的 Capture,接着我们又提交了一组连续三次的多次模式的 Capture,这些不同模式的 Capture 会按照下图所示被执行:

Capture 工作原理

下面是几个重要的注意事项:

无论 Capture 以何种模式被提交,它们都是按顺序串行执行的,不存在并行执行的情况。

重复模式是一个比较特殊的模式,因为它会保留我们提交的 CaptureRequest 对象用于不断重复执行 Capture 操作,所以大多数情况下重复模式的 CaptureRequest 和其他模式的 CaptureRequest 是独立的,这就会导致重复模式的参数和其他模式的参数会有一定的差异,例如重复模式不会配置 CaptureRequest.AF_TRIGGER_START,因为这会导致相机不断触发对焦的操作。

如果某一次的 Capture 没有配置预览的 Surface,例如拍照的时候,就会导致本次 Capture 不会将画面输出到预览的 Surface 上,进而导致预览画面卡顿的情况,所以大部分情况下我们都会将预览的 Surface 添加到所有的 CaptureRequest 里。

2 如何拍摄单张照片

拍摄单张照片是最简单的拍照模式,它使用的就是单次模式的 Capture,我们会使用 ImageReader 创建一个接收照片的 Surface,并且把它添加到 CaptureRequest 里提交给相机进行拍照,最后通过 ImageReader 的回调获取 Image 对象,进而获取 JPEG 图像数据进行保存。

2.1 定义回调接口

当拍照完成的时候我们会得到两个数据对象,一个是通过 onImageAvailable() 回调给我们的存储图像数据的 Image,一个是通过 onCaptureCompleted() 回调给我们的存储拍照信息的 CaptureResult,它们是一一对应的,所以我们定义了如下两个回调接口:

private val captureResults: BlockingQueue = LinkedBlockingDeque() private inner class CaptureImageStateCallback : CameraCaptureSession.CaptureCallback() { @MainThread override fun onCaptureCompleted(session: CameraCaptureSession, request: CaptureRequest, result: TotalCaptureResult) { super.onCaptureCompleted(session, request, result) captureResults.put(result) } } private inner class OnJpegImageAvailableListener : ImageReader.OnImageAvailableListener { @WorkerThread override fun onImageAvailable(imageReader: ImageReader) { val image = imageReader.acquireNextImage() val captureResult = captureResults.take() if (image != null && captureResult != null) { // Save image into sdcard. } } } 2.2 创建 ImageReader

创建 ImageReader 需要我们指定照片的大小,所以首先我们要获取支持的照片尺寸列表,并且从中筛选出合适的尺寸,假设我们要求照片的尺寸最大不能超过 4032x3024,并且比例必须是 4:3,所以会有如下筛选尺寸的代码片段:

@WorkerThread private fun getOptimalSize(cameraCharacteristics: CameraCharacteristics, clazz: Class, maxWidth: Int, maxHeight: Int): Size? { val streamConfigurationMap = cameraCharacteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP) val supportedSizes = streamConfigurationMap?.getOutputSizes(clazz) return getOptimalSize(supportedSizes, maxWidth, maxHeight) } @AnyThread private fun getOptimalSize(supportedSizes: Array?, maxWidth: Int, maxHeight: Int): Size? { val aspectRatio = maxWidth.toFloat() / maxHeight if (supportedSizes != null) { for (size in supportedSizes) { if (size.width.toFloat() / size.height == aspectRatio && size.height 90.0F ExifInterface.ORIENTATION_ROTATE_180 -> 180.0F ExifInterface.ORIENTATION_ROTATE_270 -> 270.0F else -> 0.0F } var thumbnail = if (exifInterface.hasThumbnail()) { exifInterface.thumbnailBitmap } else { val options = BitmapFactory.Options() options.inSampleSize = 16 BitmapFactory.decodeFile(jpegPath, options) } if (orientation != 0.0F && thumbnail != null) { val matrix = Matrix() matrix.setRotate(orientation) thumbnail = Bitmap.createBitmap(thumbnail, 0, 0, thumbnail.width, thumbnail.height, matrix, true) } return thumbnail } 2.6 设置定位信息

拍照的时候,通常都会在图片的 Exif 写入定位信息,我们可以通过 CaptureRequest.JPEG_GPS_LOCATION 配置定位信息,代码如下:

@WorkerThread private fun getLocation(): Location? { val locationManager = getSystemService(LocationManager::class.java) if (locationManager != null && ContextCompat.checkSelfPermission(this, android.Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED) { return locationManager.getLastKnownLocation(LocationManager.PASSIVE_PROVIDER) } return null } val location = getLocation() captureImageRequestBuilder[CaptureRequest.JPEG_GPS_LOCATION] = location 2.7 播放快门音效

在进行拍照之前,我们还需要配置拍照时播放的快门音效,因为 Camera2 和 Camera1 不一样,拍照时不会有任何声音,需要我们在适当的时候通过 MediaSoundPlayer 播放快门音效,通常情况我们是在 CaptureStateCallback.onCaptureStarted() 回调的时候播放快门音效:

private val mediaActionSound: MediaActionSound = MediaActionSound() private inner class CaptureImageStateCallback : CameraCaptureSession.CaptureCallback() { @MainThread override fun onCaptureStarted(session: CameraCaptureSession, request: CaptureRequest, timestamp: Long, frameNumber: Long) { super.onCaptureStarted(session, request, timestamp, frameNumber) // Play the shutter click sound. cameraHandler?.post { mediaActionSound.play(MediaActionSound.SHUTTER_CLICK) } } @MainThread override fun onCaptureCompleted(session: CameraCaptureSession, request: CaptureRequest, result: TotalCaptureResult) { super.onCaptureCompleted(session, request, result) captureResults.put(result) } } 2.8 拍照并保存图片

经过一连串的配置之后,我们终于可以开拍照了,直接调用 CameraCaptureSession.capture() 方法把 CaptureRequest 对象提交给相机就可以等待相机输出图片了,该方法要求我们设置三个参数:

request:本次 Capture 操作使用的 CaptureRequest 对象。listener:监听 Capture 状态的回调接口。handler:回调 Capture 状态监听接口的 Handler 对象。 captureSession.capture(captureImageRequest, CaptureImageStateCallback(), mainHandler)

如果一切顺利,相机在拍照完成的时候会通过 CaptureStateCallback.onCaptureCompleted() 回调一个 CaptureResult 对象给我们,里面包含了本次拍照的所有信息,另外还会通过 OnImageAvailableListener.onImageAvailable() 回调一个代表图像数据的 Image 对象给我们。在我们的 Demo 中,我们将获取到的 CaptureResult 对象保存到一个阻塞队列中,在 OnImageAvailableListener.onImageAvailable() 回调的时候就从这个阻塞队列获取 CaptureResult 对象,结合 Image 对象对图片进行保存操作,并且还会在图片保存完毕的时候获取图片的缩略图用于刷新 UI,代码如下所示:

private inner class OnJpegImageAvailableListener : ImageReader.OnImageAvailableListener { private val dateFormat: DateFormat = SimpleDateFormat("yyyyMMddHHmmssSSS", Locale.getDefault()) private val cameraDir: String = "${Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM)}/Camera" @WorkerThread override fun onImageAvailable(imageReader: ImageReader) { val image = imageReader.acquireNextImage() val captureResult = captureResults.take() if (image != null && captureResult != null) { image.use { val jpegByteBuffer = it.planes[0].buffer// Jpeg image data only occupy the planes[0]. val jpegByteArray = ByteArray(jpegByteBuffer.remaining()) jpegByteBuffer.get(jpegByteArray) val width = it.width val height = it.height saveImageExecutor.execute { val date = System.currentTimeMillis() val title = "IMG_${dateFormat.format(date)}"// e.g. IMG_20190211100833786 val displayName = "$title.jpeg"// e.g. IMG_20190211100833786.jpeg val path = "$cameraDir/$displayName"// e.g. /sdcard/DCIM/Camera/IMG_20190211100833786.jpeg val orientation = captureResult[CaptureResult.JPEG_ORIENTATION] val location = captureResult[CaptureResult.JPEG_GPS_LOCATION] val longitude = location?.longitude ?: 0.0 val latitude = location?.latitude ?: 0.0 // Write the jpeg data into the specified file. File(path).writeBytes(jpegByteArray) // Insert the image information into the media store. val values = ContentValues() values.put(MediaStore.Images.ImageColumns.TITLE, title) values.put(MediaStore.Images.ImageColumns.DISPLAY_NAME, displayName) values.put(MediaStore.Images.ImageColumns.DATA, path) values.put(MediaStore.Images.ImageColumns.DATE_TAKEN, date) values.put(MediaStore.Images.ImageColumns.WIDTH, width) values.put(MediaStore.Images.ImageColumns.HEIGHT, height) values.put(MediaStore.Images.ImageColumns.ORIENTATION, orientation) values.put(MediaStore.Images.ImageColumns.LONGITUDE, longitude) values.put(MediaStore.Images.ImageColumns.LATITUDE, latitude) contentResolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values) // Refresh the thumbnail of image. val thumbnail = getThumbnail(path) if (thumbnail != null) { runOnUiThread { thumbnailView.setImageBitmap(thumbnail) thumbnailView.scaleX = 0.8F thumbnailView.scaleY = 0.8F thumbnailView.animate().setDuration(50).scaleX(1.0F).scaleY(1.0F).start() } } } } } } } 2.9 前置摄像头拍照的镜像问题

如果你使用前置摄像头进行拍照,虽然照片的方向已经被我们矫正了,但是你会发现画面却是相反的,例如你在预览的时候人脸在左边,拍出来的照片人脸却是在右边。出现这个问题的原因是默认情况下相机不会对 JPEG 图像进行镜像操作,导致输出的原始画面是非镜像的。解决这个问题的一个办法是拿到 JPEG 数据之后再次对图像进行镜像操作,然后才保存图片。

3 如何连续拍摄多张图片

在我们的 Demo 中有一个特殊的拍照功能,就是当用户双击快门按钮的时候会连续拍摄 10 张照片,其实现原理就是采用了多次模式的 Capture,所有的配置流程和拍摄单张照片一样,唯一的区别是我们使用 CameraCaptureSession.captureBurst() 进行拍照,该方法要求我们传递一下三个参数:

requests:按顺序连续执行的 CaptureRequest 对象列表,每一个 CaptureRequest 对象都可以有自己的配置,在我们的 Demo 里出于简化的目的,10 个 CaptureRequest 对象实际上的都是同一个。listener:监听 Capture 状态的回调接口,需要注意的是有多少个 CaptureRequest 对象就会回调该接口多少次。handler:回调 Capture 状态监听接口的 Handler 对象。 val captureImageRequest = captureImageRequestBuilder.build() val captureImageRequests = mutableListOf() for (i in 1..burstNumber) { captureImageRequests.add(captureImageRequest) } captureSession.captureBurst(captureImageRequests, CaptureImageStateCallback(), mainHandler)

接下来所有的流程就和拍摄单招照片一样了,每输出一张图片我们就将其保存到 SD 卡并且刷新媒体库和缩略图。

4 如何连拍

连拍这个功能在 Camera2 出现之前是不可能实现的,现在我们只需要使用重复模式的 Capture 就可以轻松实现连拍功能。在《Camera2 预览》里我们使用了重复模式的 Capture 来实现预览功能,而这一次我们不仅要用该模式进行预览,还要在预览的同时也输出照片,所以我们会使用 CameraCaptureSession.setRepeatingRequest() 方法开始进行连拍:

val captureImageRequest = captureImageRequestBuilder.build() captureSession.setRepeatingRequest(captureImageRequest, CaptureImageStateCallback(), mainHandler)

停止连拍有以下两种方式:

调用 CameraCaptueSession.stopRepeating() 方法停止重复模式的 Capture,但是这会导致预览也停止。调用 CameraCaptueSession.setRepeatingRequest() 方法并且使用预览的 CaptureRequest 对象,停止输出照片。

在我们的 Demo 里使用了第二种方式:

@MainThread private fun stopCaptureImageContinuously() { // Restart preview to stop the continuous image capture. startPreview() } 5 如何切换前后置摄像头

切换前后置摄像头是一个很常见的功能,虽然和本章的主要内容不相关,但是在 Demo 中已经实现,所以这里也顺便提一下。我们只要按照以下顺序进行操作就可以轻松实现前后置摄像头的切换:

关闭当前摄像头开启新的摄像头创建新的 Session开启预览

下面是代码片段,详细代码大家可以自行查看 Demo 源码:

@MainThread private fun switchCamera() { val cameraDevice = cameraDeviceFuture?.get() val oldCameraId = cameraDevice?.id val newCameraId = if (oldCameraId == frontCameraId) backCameraId else frontCameraId if (newCameraId != null) { closeCamera() openCamera(newCameraId) createCaptureRequestBuilders() setPreviewSize(MAX_PREVIEW_WIDTH, MAX_PREVIEW_HEIGHT) setImageSize(MAX_IMAGE_WIDTH, MAX_IMAGE_HEIGHT) createSession() startPreview() } } 6 总结

本章主要讲述了如何实现几种常见的拍照模式,其核心要领就是理解【重复模式】、【单词模式】和【多次模式】的工作流程,根据实际业务情况灵活运用,下面是几个小建议:

重复模式和多次模式都可以实现连拍功能,其中重复模式适合没有连拍上限的情况,而多次模式适合有连拍上限的情况。一个 CaptureRequest 可以添加多个 Surface,这就意味着你可以同时拍摄多张照片。拍照获取 CaptureResult 和 Image 对象走的是两个不同的回调接口,灵活运用子线程的阻塞操作可以简化你的代码逻辑。

作者:HJDaryl 链接:https://www.jianshu.com/p/2ae0a737c686 来源:简书 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。



【本文地址】

公司简介

联系我们

今日新闻

    推荐新闻

    专题文章
      CopyRight 2018-2019 实验室设备网 版权所有