【Android -- 相机】Camera2 实现拍照 & 预览功能

前言

上篇文章,我们已经用 Camera1 实现了预览和拍照的功能,但也说到,在API21的时候,Camera1已经被标注为弃用,因为它的API功能和灵活性满足不了现在日益复杂的相机开发了,所以在 API21之后,引入了 Camera2 。

从功能来讲,Camera2 废弃了 Camera1 的框架,它支持更多的功能,比如:

  • 获取更多的帧(预览/拍照)信息,以及每一帧的参数配置
  • 支持更多的图片格式(yuv/raw)等
  • 一些新特性

效果图
在这里插入图片描述

一、概念

1. Pipeline
Camera2 的 API 模型被设计成一个 Pipeline(管道),它按顺序处理每一帧的请求并返回请求结果给客户端。下面这张来自官方的图展示了 Pipeline 的工作流程,我们会通过一个简单的例子详细解释这张图。
在这里插入图片描述
为了解释上面的示意图,假设我们想要同时拍摄两张不同尺寸的图片,并且在拍摄的过程中闪光灯必须亮起来。整个拍摄流程如下:

创建一个用于从 Pipeline 获取图片的 CaptureRequest
修改 CaptureRequest 的闪光灯配置,让闪光灯在拍照过程中亮起来。
创建两个不同尺寸的 Surface 用于接收图片数据,并且将它们添加到 CaptureRequest 中。
发送配置好的 CaptureRequest 到 Pipeline 中等待它返回拍照结果。
一个新的 CaptureRequest 会被放入一个被称作 Pending Request Queue 的队列中等待被执行,当 In-Flight Capture Queue 队列空闲的时候就会从 Pending Request Queue 获取若干个待处理的 CaptureRequest,并且根据每一个 CaptureRequest 的配置进行 Capture操作。最后我们从不同尺寸的 Surface 中获取图片数据并且还会得到一个包含了很多与本次拍照相关的信息的 CaptureResult,流程结束。

2. Supported Hardware Level
相机功能的强大与否和硬件息息相关,不同厂商对 Camera2 的支持程度也不同,所以 Camera2 定义了一个叫做 Supported Hardware Level 的重要概念,其作用是将不同设备上的 Camera2 根据功能的支持情况划分成多个不同级别以便开发者能够大概了解当前设备上 Camera2 的支持情况。截止到 Android P 为止,从低到高一共有 LEGACY、LIMITED、FULL 和 LEVEL_3 四个级别:

  • LEGACY:向后兼容的级别,处于该级别的设备意味着它只支持 Camera1 的功能,不具备任何 Camera2 高级特性。
  • LIMITED:除了支持 Camera1 的基础功能之外,还支持部分 Camera2 高级特性的级别。
  • FULL:支持所有 Camera2 的高级特性。
  • LEVEL_3:新增更多 Camera2 高级特性,例如 YUV 数据的后处理等。

想了解更多,请查看 Android Camera2 教程 · 第一章 · 概览

二、相机预览

要注意的是,Camera2 与 Camera1 是两个不同的框架,不要被 Camera1 的思想禁锢,把它当做新知识学习即可,一个相机的流程图如下:
在这里插入图片描述
1. 在清单文件中,添加权限:

	<uses-permission android:name="android.permission.CAMERA" />
    <!-- 支持相机才能运行 -->
    <uses-feature
        android:name="android.hardware.camera"
        android:required="true" />
    <uses-feature android:name="android.hardware.camera.autofocus" />

2. 布局文件

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <com.hjq.bar.TitleBar
        android:id="@+id/title_bar"
        android:layout_width="match_parent"
        android:background="@color/teal_200"
        android:layout_height="?android:attr/actionBarSize"
        app:title="Camera2 使用"
        app:titleStyle="bold"
        app:titleSize="18sp"
        app:backButton="false"
        app:titleColor="@color/white"/>

    <androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent">
        <TextureView
            android:id="@+id/surface"
            android:layout_width="0dp"
            android:layout_height="0dp"
            app:layout_constraintTop_toTopOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintDimensionRatio="H,3:4"/>

        <Button
            android:id="@+id/btn_trans"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            app:layout_constraintTop_toBottomOf="@id/surface"
            app:layout_constraintTop_toTopOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            android:layout_margin="10dp"
            android:text="切换摄像头"/>
        <Button
            android:id="@+id/btn_takePhoto"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            app:layout_constraintTop_toBottomOf="@id/surface"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            android:text="拍照"/>

        <ImageView
            android:id="@+id/image"
            android:layout_width="100dp"
            android:layout_height="0dp"
            app:layout_constraintTop_toTopOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintDimensionRatio="H,3:4"/>

    </androidx.constraintlayout.widget.ConstraintLayout>
</LinearLayout>

3. 获取相机信息
从上面的流程图支持,我们需要通过 CameraManagergetCameraCharacteristics() 方法,来获取相机的信息;CameraManager 是一个负责查询和建立相机连接的系统服务,它的功能不多,主要如下:

  • 将相机信息装载到 CameraCharacteristics 中。
  • 根据指定的相机 ID 连接相机
  • 提供将闪光灯设置为手电筒的快捷方式
	/**
     * 初始化相机和配置相关属性
     */
    private void initCamera() {
        try {
            //获取相机服务 CameraManager
            mCameraManager = (CameraManager) getSystemService(Context.CAMERA_SERVICE);

            //遍历设备支持的相机 ID ,比如前置,后置等
            String[] cameraIdList = mCameraManager.getCameraIdList();
            for (String cameraId : cameraIdList) {
                // 拿到装在所有相机信息的  CameraCharacteristics 类
                CameraCharacteristics characteristics = mCameraManager.getCameraCharacteristics(cameraId);
                //拿到相机的方向,前置,后置,外置
                Integer facing = characteristics.get(CameraCharacteristics.LENS_FACING);

                if (facing != null) {
                    //后置摄像头
                    if (facing == CameraCharacteristics.LENS_FACING_BACK) {
                        mBackCameraId = cameraId;
                        mBackCameraCharacteristics = characteristics;
                    }else if (facing == CameraCharacteristics.LENS_FACING_FRONT){
                        //前置摄像头
                        mFrontCameraId = cameraId;
                        mFrontCameraCharacteristics = characteristics;
                    }
                    mCameraId = cameraId;
                }

                //是否支持 Camera2 的高级特性
                Integer level = characteristics.get(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL);
                /**
                 * 不支持 Camera2 的特性
                 */
                if (level == null || level == CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY){
                    //  Toast.makeText(this, "您的手机不支持Camera2的高级特效", Toast.LENGTH_SHORT).show();
                    //   break;
                }

            }

        } catch (CameraAccessException e) {
            e.printStackTrace();
        }
    }

4. 打开摄像头

	/**
     * 打开摄像头
     *
     */
    @SuppressLint("MissingPermission")
    private void openCamera(int width, int height) {
        //判断不同摄像头,拿到 CameraCharacteristics
        CameraCharacteristics characteristics = mCameraId.equals(mBackCameraId) ? mBackCameraCharacteristics : mFrontCameraCharacteristics;
        //拿到配置的map
        StreamConfigurationMap map = characteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP);

        //获取摄像头传感器的方向
        mSensorOrientation = characteristics.get(CameraCharacteristics.SENSOR_ORIENTATION);
        //获取预览尺寸
        Size[] previewSizes = map.getOutputSizes(SurfaceTexture.class);
        //获取最佳尺寸
        Size bestSize = getBestSize(width, height, previewSizes);
        /**
         * 配置预览属性
         * 与 Cmaera1 不同的是,Camera 是把尺寸信息给到 Surface (SurfaceView 或者 ImageReader),
         * Camera 会根据 Surface 配置的大小,输出对应尺寸的画面;
         * 注意摄像头的 width > height ,而我们使用竖屏,所以宽高要变化一下
         */
        mTextureView.getSurfaceTexture().setDefaultBufferSize(bestSize.getHeight(),bestSize.getWidth());

        /**
         * 设置图片尺寸,这里图片的话,选择最大的分辨率即可
         */
        Size[] sizes = map.getOutputSizes(ImageFormat.JPEG);
        Size largest = Collections.max(
                Arrays.asList(sizes),
                new CompareSizesByArea());
        //设置imagereader,配置大小,且最大Image为 1,因为是 JPEG
        mImageReader = ImageReader.newInstance(largest.getWidth(),largest.getHeight(),
                ImageFormat.JPEG,1);

        //拍照监听
        mImageReader.setOnImageAvailableListener(new ImageAvailable(),null);

        try {
            //打开摄像头,监听数据
            mCameraManager.openCamera(mCameraId,new CameraDeviceCallback(),null);
        } catch (CameraAccessException e) {
            e.printStackTrace();
        }
    }

在用 mCameraManager.openCamera() 打开摄像头之前,我们通过

//拿到配置的map
StreamConfigurationMap map = characteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP);

而与 Camera1 不同的是,Camera2 会根据 Surface 配置的大小,输出对应尺寸的画面,所以这里设置 mTextureView 的大小即可,注意摄像头的 width > height ,而我们使用竖屏,所以宽高要变化一下。

接着设置图片的尺寸,这里当然是越轻越好了,所以选择最大尺寸即可。I

最后调用 mCameraManager.openCamera() ,它有三个参数:

  • cameraIdCameraID,比如前置、后置和外置
  • CameraDevice.StateCallback :当连接到相机时,该回调就会被调用,生成 CameraDevice
  • handler : 调用 CameraDevice.StateCallbackHandler,传 null,则调用主线程,建议传入 HandlerThread 的hander,毕竟这种都是耗时的。

5. CameraDevice
CameraDevice 代表当前连接的相机设备,它的职责有以下四个:

  • 根据指定的参数创建 CameraCaptureSession
  • 根据指定的模板创建 CaptureRequest
  • 关闭相机设备
  • 监听相机设备的状态,例如断开连接、开启成功和开启失败等
    class CameraDeviceCallback extends CameraDevice.StateCallback{

        @Override
        public void onOpened(@NonNull CameraDevice camera) {
            mCameraDevice = camera;
            //此时摄像头已经打开,可以预览了
            createPreviewPipeline(camera);
        }

        @Override
        public void onDisconnected(@NonNull CameraDevice camera) {
            camera.close();
        }

        @Override
        public void onError(@NonNull CameraDevice camera, int error) {
            camera.close();
        }
    }

在 onOpened 创建我们 CaptureRequest ,配置相机参数信息,如下:

/**
     * 创建 Session
     */
    private void createPreviewPipeline(CameraDevice cameraDevice){
        try {
            //创建作为预览的 CaptureRequst.builder
            final CaptureRequest.Builder captureBuilder = cameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW);
            Surface surface = new Surface(mTextureView.getSurfaceTexture());
            //添加 surface 容器
            captureBuilder.addTarget(surface);
            // 创建CameraCaptureSession,该对象负责管理处理预览请求和拍照请求,这个必须在创建 Seesion 之前就准备好,传递给底层用于皮遏制 pipeline
            cameraDevice.createCaptureSession(Arrays.asList(surface, mImageReader.getSurface()), new CameraCaptureSession.StateCallback() {
                @Override
                public void onConfigured(@NonNull CameraCaptureSession session) {
                    mCameraCaptureSession = session;
                    try {
                        //设置自动聚焦
                        captureBuilder.set(CaptureRequest.CONTROL_AE_MODE,CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE);
                        //设置自动曝光
                        captureBuilder.set(CaptureRequest.CONTROL_AE_MODE,CaptureRequest.CONTROL_AE_MODE_ON_AUTO_FLASH);

                        //创建 CaptureRequest
                        CaptureRequest build = captureBuilder.build();
                        //设置预览时连续捕获图片数据
                        session.setRepeatingRequest(build,null,null);
                    }catch (Exception e){

                    }
                }

                @Override
                public void onConfigureFailed(@NonNull CameraCaptureSession session) {
                    Toast.makeText(MainActivity.this, "配置失败", Toast.LENGTH_SHORT).show();
                }
            },null);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

在开启预览之前,我们需要先创建 CaptureRequest ,上面已经说过 CaptureRequest 是向 CameraCaptureSession 提交 Capture 请求的信息载体,内部包含了本次的 Capture 参数配置和接受图像数据的 Surface。

CaptureRequest 可以配置的信息非常多,比如图像格式、图像分辨率、聚焦、闪光灯控制等,可以说绝大部分配置都是通过 CaptureRequest 配置的。

上面通过 cameraDevice.createCaptureRequest() 来创建一个 CaptureRequest.Builder 对象,其中createCaptureRequest() 方法的参数是 templateType 用于指定哪种模板,Camera2 根据不同场景,为我们配置了一些常用的参数模板:

  • TEMPLATE_PREVIEW:适用于配置预览的模板
  • TEMPLATE_RECORD:适用于视频录制的模板。
  • TEMPLATE_STILL_CAPTURE:适用于拍照的模板。
  • TEMPLATE_VIDEO_SNAPSHOT:适用于在录制视频过程中支持拍照的模板。
  • TEMPLATE_MANUAL:适用于希望自己手动配置大部分参数的模板。

这里我们需要一个预览的 CaptureRequest ,所以选择 TEMPLATE_PREVIEW的模板。

接着,需要设置要承载图像数据的 Surface,我们用到两个,一个是 TextureView 用来预览的,一个是 ImageReader 用来拍照的。

最后,通过 cameraDevice.createCaptureSession() 创建 CameraCaptureSession ,然后再配置一下聚焦和曝光的配置,就可以把 CaptureRequest 通过 Session 发送给底层了:

			// 创建CameraCaptureSession,该对象负责管理处理预览请求和拍照请求,这个必须在创建 Seesion 之前就准备好,传递给底层用于配置 pipeline
            cameraDevice.createCaptureSession(Arrays.asList(surface, mImageReader.getSurface()), new CameraCaptureSession.StateCallback() {
                @Override
                public void onConfigured(@NonNull CameraCaptureSession session) {
                    mCameraCaptureSession = session;
                   try {
                       //设置自动聚焦
                       captureBuilder.set(CaptureRequest.CONTROL_AE_MODE,CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE);
                       //设置自动曝光
                       captureBuilder.set(CaptureRequest.CONTROL_AE_MODE,CaptureRequest.CONTROL_AE_MODE_ON_AUTO_FLASH);

                       //创建 CaptureRequest
                       CaptureRequest build = captureBuilder.build();
                        //设置预览时连续捕获图片数据
                       session.setRepeatingRequest(build,null,null);
                   }catch (Exception e){

                   }
                }

                @Override
                public void onConfigureFailed(@NonNull CameraCaptureSession session) {
                    Toast.makeText(MainActivity.this, "配置失败", Toast.LENGTH_SHORT).show();
                }
            },null);

6. 开启和关闭预览
Camera2 中,本质上是不断的重复 Captrue 的过程,每一次 Capture 都会把预览的数据输出到对应的 Surface 中,所以,为了达到预览的效果,需要使用:

    session.setRepeatingRequest(build,null,null);

它的三个参数如下:

  • request : 在不断重复执行 Capture 时使用的 CaptureRequest对象

  • callback :监听每一次 Capture 状态的 CameraCaptureSession.CaptureCallback 对象,例如 onCaptureStarted() 意味着一次 Capture 的开始,而 onCaptureCompleted() 意味着一次 Capture 的结束。

  • handler :用于执行 CameraCaptureSession.CaptureCallback 的Handler 对象,传null为主线程,也可以使用其他线程的 Handler

关闭预览

  //停止预览
  mCameraCaptureSession.stopRepeating();

三、拍照

拍照其实也是一个 Captrue,这样的话,我们就可以再创建一个 CaptureRequest 去执行拍照的就可以了,代码如下:

		try {
                    //创建一个拍照的 session
                    final CaptureRequest.Builder captureRequest = mCameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_STILL_CAPTURE);
                    //设置装在图像数据的 Surface
                    captureRequest.addTarget(mImageReader.getSurface());
                    //聚焦
                    captureRequest.set(CaptureRequest.CONTROL_AF_MODE,
                            CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE);
                    //自动曝光
                    captureRequest.set(CaptureRequest.CONTROL_AF_MODE,
                            CaptureRequest.CONTROL_AE_MODE_ON_AUTO_FLASH);
                    // 获取设备方向
                    int rotation = getWindowManager().getDefaultDisplay().getRotation();
                    // 根据设备方向计算设置照片的方向
                    captureRequest.set(CaptureRequest.JPEG_ORIENTATION
                            , getOrientation(rotation));
                    // 先停止预览
                    mCameraCaptureSession.stopRepeating();

                    mCameraCaptureSession.capture(captureRequest.build(), new CameraCaptureSession.CaptureCallback() {
                        @Override
                        public void onCaptureCompleted(@NonNull CameraCaptureSession session, @NonNull CaptureRequest request, @NonNull TotalCaptureResult result) {
                            super.onCaptureCompleted(session, request, result);
                            try {
                                //拍完之后,让它继续可以预览
                                CaptureRequest.Builder captureRequest1 = mCameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW);
                                captureRequest1.addTarget(new Surface(mTextureView.getSurfaceTexture()));
                                mCameraCaptureSession.setRepeatingRequest(captureRequest1.build(),null,null);
                            } catch (CameraAccessException e) {
                                e.printStackTrace();
                            }
                        }
                    },null);

                } catch (CameraAccessException e) {
                    e.printStackTrace();
                }

1. 保存图片
在 Camrea2 中,Imageread 是获取图像数据的一个重要途径,我们可以通过它获取各种各样格式的图像数据,比如 JPEG、YUV和 RAW 等。通过 ImageReader.newInstance() 方法创建 ImageReader 对象,如下:

//拍照监听
mImageReader.setOnImageAvailableListener(new ImageAvailable(),null);     

2. ImageReader
在 Camrea2 中,Imageread 是获取图像数据的一个重要途径,我们可以通过它获取各种各样格式的图像数据,比如 JPEG、YUV和 RAW 等。通过 ImageReader.newInstance() 方法创建 ImageReader 对象,如下:

//设置imagereader,配置大小,且最大Image为 1,因为是 JPEG
mImageReader = ImageReader.newInstance(largest.getWidth(),largest.getHeight(),
        ImageFormat.JPEG,1);
mImageReader.setOnImageAvailableListener(new ImageAvailable(),null);   

3. 获取图片

			FileOutputStream fos = null;
            Image image = null;
            try {
                fos = new FileOutputStream(file);
                //获取捕获的照片数据
                image = imageReader.acquireLatestImage();
                //拿到所有的 Plane 数组
                Image.Plane[] planes = image.getPlanes();
                //由于是 JPEG ,只需要获取下标为 0 的数据即可
                ByteBuffer buffer = planes[0].getBuffer();
                data = new byte[buffer.remaining()];
                //把 bytebuffer 的数据给 byte数组
                buffer.get(data);
                Bitmap bitmap = BitmapFactory.decodeByteArray(data,0,data.length);
                //旋转图片
                if (mCameraId.equals(mFrontCameraId)){
                    bitmap = BitmapUtils.rotate(bitmap,270);
                    bitmap = BitmapUtils.mirror(bitmap);
                }else{
                    bitmap = BitmapUtils.rotate(bitmap,90);
                }
                bitmap.compress(Bitmap.CompressFormat.JPEG,100,fos);
                fos.flush();
                return bitmap;
            }catch (Exception e){
                Log.d(TAG, "zsr doInBackground: "+e.toString());
            }finally {
                CloseUtils.close(fos);
                //记得关闭 image
                if (image != null) {
                    image.close();
                }
            }
            return null;
  • 1
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Kevin-Dev

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值