十分钟实现 Android Camera2 相机拍照

news2024/9/24 13:22:11

1. 前言

因为工作中要使用Android Camera2 API,但因为Camera2比较复杂,网上资料也比较乱,有一定入门门槛,所以花了几天时间系统研究了下,并在CSDN上记录了下,希望能帮助到更多的小伙伴。
上篇文章 我们使用Camera2实现了相机预览的功能,这篇文章我们接着上文,来实现Camera2相机拍照的功能。

2. 前置操作

2.1 声明相机参数和成员变量

首先还是声明相机参数和成员变量,比起前文增加了这些

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)

完整的需要声明的相机参数和成员变量如下

//后摄 : 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 初始化imageReader

我们需要在合适的时机去初始化imageReader
这里我们把它放到startPreview

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 将imageReader关联到Session

首先我们要在startPreview()方法里面,修改targets
原本的targets,只传入了binding.surfaceView.holder.surface

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

现在要多传入一个imageReader.surface

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

完整代码如下

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. 实现拍照功能

3.1 清空imageReader

首先需要清空imageReader,防止imageReader里还有缓存

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

3.2 设置OnImageAvailableListener监听

然后创建一个新的队列Queue,调用setOnImageAvailableListener注册一个监听器,在ImageReader中有新图像可用时调用。

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

imageReader设置一个OnImageAvailableListener监听
setOnImageAvailableListener一共有两个参数,第一个参数是OnImageAvailableListener接口,第二个参数是Handler,这里我们传入imageReaderHandler即可。
OnImageAvailableListener监听里,会去获取imageReader的下一个image,并添加到imageQueue

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

3.3 创建CaptureRequest.Builder

接着,通过session.device.createCaptureRequest创建CaptureRequest.Builder

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

3.4 调用调用session.capture()执行拍照

然后调用session.capture()来进行拍照,需要传入captureRequestCameraCaptureSession.CaptureCallback回调和Handler

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

3.5 拍照回调处理

onCaptureCompleted调用后,会调用异步线程,然后从imageQueue中取出image,并把setOnImageAvailableListener监听设为null
然后调用saveResult方法,将图片保存到本地存储中。

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 将图片保存到本地

来看下saveImage方法
首先会判断是否是JPEG 格式,如果是JPEG格式,那就简单地保存bytes即可
如果不是JPEG格式,本文就略过未实现了,有需要的小伙伴可以去看下官方Demo。

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
        }
    }
}

这里的createFile用来获取一个文件路径

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 拍照部分完整代码

再来看一下拍照部分完整的代码

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")
}

我们运行程序,点击按钮拍照,可以发现提示拍照成功
在这里插入图片描述
我们打开文件管理器,在/sdcard/Android/data/包名/files/image文件夹下,可以看到这张图片
但是我们发现这张照片的方向是不对的

在这里插入图片描述

4. 修正图片方向

我们可以看到之前拍摄的图片,方向是不对的,所以需要对图片的方向进行修正

首先添加OrientationLiveData这个LiveData

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
        }
    }
}

在Activity中声明relativeOrientation,并注册观察者Observer,当方向改变会通知。

// 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")
    })
}

接着在onCaptureCompleted回调里,saveImage之后,添加如下代码来修改图片的方向

// 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
}

再来运行程序,看到图片的方向就正常了
在这里插入图片描述

5. 销毁相机

当Activity销毁的时候,我们也要去销毁相机,比起上篇文章,多了个imageRecorder的销毁

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()
}

至此我们就用Camera2完成相机拍照功能了。

6. 其他

6.1 本文源码下载

下载地址 : Android Camera2 Demo - 实现相机预览、拍照、录制视频功能

6.2 Android Camera2 系列

更多Camera2相关文章,请看
十分钟实现 Android Camera2 相机预览_氦客的博客-CSDN博客
十分钟实现 Android Camera2 相机拍照_氦客的博客-CSDN博客
十分钟实现 Android Camera2 视频录制_氦客的博客-CSDN博客

6.3 Android 相机相关文章

Android 使用CameraX实现预览/拍照/录制视频/图片分析/对焦/缩放/切换摄像头等操作_氦客的博客-CSDN博客
Android 从零开发一个简易的相机App_android开发简易app_氦客的博客-CSDN博客

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/696725.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【数据分享】1929-2022年全球站点的逐月平均压力数据(Shp\Excel\12000个站点)

气象数据是在各项研究中都经常使用的数据&#xff0c;气象指标包括气温、风速、降水、能见度等指标&#xff0c;说到气象数据&#xff0c;最详细的气象数据是具体到气象监测站点的数据&#xff01; 对于具体到监测站点的气象数据&#xff0c;之前我们分享过1929-2022年全球气象…

paddlespeech 声纹识别embedding向量提取;TTS文本合成语音

1、声纹识别embedding向量提取 参考&#xff1a; https://aistudio.baidu.com/aistudio/projectdetail/4353348 https://github.com/PaddlePaddle/PaddleSpeech/blob/develop/demos/speaker_verification/README_cn.md https://aistudio.baidu.com/aistudio/projectdetail/4…

Apache Zeppelin 番外篇——参与开源的得与失

背景 经常在公司做一些业务开发&#xff0c;公司里面由于各种人员流动等问题&#xff0c;导致代码质量也是参差不齐&#xff0c;最终问题就是很难维护&#xff0c;前期还想着能够优化代码&#xff0c;但是大部分时间都是需求都是倒排期&#xff0c;所以也导致不再想进行代码优…

车联网 CAN Bus 协议介绍与数据实时流处理

什么是 CAN Bus&#xff1f; CAN&#xff08;Control Area Network&#xff09;Bus 是一种串行通信协议&#xff0c;能够让设备之间可靠而高效地传输数据。它广泛应用于车辆领域&#xff0c;像神经系统一样连接车辆内部的各个电子控制单元。 CAN Bus 最初由博世公司在 20 世纪…

matlab合并/拼接多个excel表

一、 说明&#xff1a;Excel中数据含有 日期&#xff0c;double如何实现多个表合并 解&#xff1a; path E:\xxx\; namelist dir([path,*.csv]); L length(namelist); a []; for i 1:Lfilename{i} [path,namelist(i).name];bb readtable(filename{i},Range,A1:G2881)…

智能指针

目录 RAII auto_ptr unique_ptr shared_ptr shared_ptr的循环引用 weak_ptr 删除器 智能指针的出现主要是针对程序的资源泄露问题而产生的。 RAII RAII&#xff08;Resource Acquisition Is Initialization)是种利用对象生命周期来控制程序资源的简单技术。 在对象构…

2024考研408-计算机组成原理第四章-指令系统学习笔记

文章目录 前言一、指令系统现代计算机的结构1.1、指令格式1.1.1、指令的定义1.1.2、指令格式1.1.3、指令—按照地址码数量分类①零地址指令②一地址指令&#xff08;1个操作数、2个操作数情况&#xff09;③二地址指令④三地址指令⑤四地址指令 1.1.4、指令-按照指令长度分类1.…

c#泛型类

派生抽象基类 public abstract class cal<T> {public abstract T add(); }public class ab : cal<string> {public override string add(){return null;} }泛型类的静态成员 public class cal<T> {public static int X9; }public class ab : cal<strin…

detr(detection transformer)模型训练自己的数据集

目录 1.detr源码下载 2. 编译配置 3. 编译报错问题 4. 训练过程打印参数解读 1.detr源码下载 GitHub - facebookresearch/detr: End-to-End Object Detection with Transformers 2. 编译配置 编译参数只需要传递数据集路径即可&#xff0c;数据集格式是coco数据集类型 数…

投稿TMC的感受

投稿任务告与段落了&#xff0c;最终的结果是被TMC给early reject了。这神一样的审稿意见让我真的是老头地铁看手机啊&#xff01;所以虽然TMC没有给我rebuttal的机会。所以我还是打算在CSDN进行一次rebuttal。 其实我做的东西很简单&#xff0c;就是把时间序列中的将时间序列转…

FreeRTOS学习笔记—基础知识

文章目录 一、什么是RTOS二、前后台系统三、实时内核&#xff08;可剥夺型内核&#xff09;四、RTOS系统五、FreeRTOS系统简介六、FreeRTOS源码下载 一、什么是RTOS RTOS全称为:Real Time OS&#xff0c;就是实时操作系统&#xff0c;核心在于实时性。实时操作系统又分为硬实时…

git查看/切换远程仓库

文章目录 一、查看远程仓库地址二、切换远程仓库地址三、整体演示 一、查看远程仓库地址 命令&#xff1a; git remote -v二、切换远程仓库地址 命令&#xff1a; git remote set-url <新的远程仓库地址>三、整体演示

pg报错attempted to delete invisible tuple

问题描述 postgresql数据库执行delete报错&#xff1a;attempted to delete invisible tuple&#xff0c;执行同样条件的select不报错 delete from lzltab1; select count(*) from lzltab1;执行全表删除和全表查询的结果&#xff1a; M# delete from lzltab1; ERROR: 5500…

有一个三位数,他的各个位数的阶乘相加得到这个数

有一个三位数,他的各个位数的阶乘相加得到这个数 1.描述 有一个三位数,它的各个位数的阶乘相加得到这个数 2.代码 输入数据 依次对个位&#xff0c;十位和百位进行拆解计算阶乘 然后相加看看是不是和原来的数据是相等 public class Mian4 {public static void main(String[…

ROS系列报错与解决方法

6.28 一、问题描述 ROS运行roscore命令后发现提示log文件(日志文件)大小超过1G,需要清理 Checking log directory for disk usage. This may take awhile. Press Ctrl-C to interrupt WARNING: disk usage in log directory [/home/wht/.ros/log] is over 1GB. Its recom…

红外线探测防盗报警器电路设计

该报警器能探测人体发出的红外线&#xff0c;当人进入报警器的监视区域内&#xff0c;即可发出报警声&#xff0c;适用于家庭、办公室、仓库、实验室等比较重要场合防盜报警。 一、电路工作原理 电路原理如图4所示。 该装置由红外线传感器、信号放大电路、电压比较器、延时…

这可能是最简单的Page Object库

做过web自动化测试的同学&#xff0c;对Page object设计模式应该不陌生。 Page object库应该根据以下目标开发&#xff1a; Page object应该易于使用 清晰的结构 PageObjects 对于页面对象 PageModules对于页面内容 只写测试&#xff0c;而不是基础。 在可能的情况下防止样…

【AI 充电】KServe + Fluid 加速大模型推理

作者&#xff1a;黄驰琳、露营、车漾 背景 KServe 是 Kubernetes 上的标准模型推理平台&#xff0c;专为高度可扩展的场景而构建&#xff0c;支持现代 Serverless 推理工作负载&#xff0c;用于在任意框架上提供机器学习&#xff08;ML&#xff09;模型服务。它提供高性能、高…

菜鸡shader:L5 fresnel、matcap和cubemap

文章目录 Fresnelshader forge实现UE4蓝图实现 Matcapshader forge实现UE4蓝图实现 CubeMapSD和PS制作所需的贴图shader forge实现unity代码实现UE4蓝图实现(未实现) Fresnel shader forge实现 个人理解是&#xff0c;使用观察方向和法向方向点乘&#xff0c;那就相当于我们的视…

adb-命令大全

目录 一、ADB简介 二、为什么要用ADB 三、ADB架构和原理 四、ADB日志状态 五、ADB常用命令 &#x1f381;更多干货 完整版文档下载方式&#xff1a; 一、ADB简介 ADB全称Android Debug Bridge&#xff0c;起到调试桥的作用&#xff0c;是一个客户端-服务端程序。其中客…