Android--Camera2的学习

最近在学习有关人脸识别的闸机系统,对用到的知识点进行总结.
Android5.0后就弃用了Camera1,而Camera1和Camera2的差别还是很大的,Camera2提供了很多新特性.本文使用Camera2实现了相机预览,拍照等功能.
开发工具:AS 3.4.1 真机:荣耀10

以下是部分重要代码,文章最后附有完整的Demo地址.

主要的api:
1.CameraManager:摄像头管理者,用于检测、描述和连接到照相机设备;
2.CameraCharacteristics:摄像头的属性信息,可以获取摄像头的FPS,支持尺寸等属性;
3.CameraDevice:表示摄像头设备;
4.CameraCaptureSession:相机捕捉会话,通过setRepeatingRequest不断请求捕捉图像;
5.ImageReader.OnImageAvailableListener中的onImageAvailable:获取预览的图像数据,可进行转化,保存图像等操作.

主要流程:
Android--Camera2的学习_第1张图片
1.使用TextureView作为载体展示预览内容,
2.动态申请权限,权限通过后进行视图的初始化,遍历摄像头,创建工作线程,以及监听SurfaceTexture的状态;
这里的匿名内部类用到了jdk8的新特性Lambda表达式(后面写个关于Lambda的博客)
权限模板可参考我另一边博客:https://blog.csdn.net/sunyFS/article/details/98175130

        //java 1.8的Lambda表达式
        setPermissions(mPermissions, () -> {
            Toast.makeText(MainActivity.this, "权限已全部允许,可进行初始化操作", Toast.LENGTH_SHORT).show();
            initView();
            initLooper();
        });
    }
    private void initView() {
        btn_switch = findViewById(R.id.btn_switch);
        ttvPreview = findViewById(R.id.ttvPreview);
        btn_switch.setOnClickListener(this);
        //遍历摄像头,检查摄像头是否可用,以及获取摄像头支持的尺寸,FPS等属性
        cameraList();
        //监听SurfaceTexture状态,SurfaceTexture可用时回调onSurfaceTextureAvailable方法
        ttvPreview.setSurfaceTextureListener(this);
    }
  private void initLooper() {
        //创建HandlerThread,用"CAMERA2"标记
        mThreadHandler = new HandlerThread("CAMERA2");
        //启动线程
        mThreadHandler.start();
        //创建工作线程Handler
        mHandler = new Handler(mThreadHandler.getLooper());
    }

3.遍历摄像头cameraList(),这里我默认使用后置摄像头mCameraId = mBackCameraId;
通过getSystemService(Context.CAMERA_SERVICE);获取摄像头的管理者cameraManager,
foreach遍历所有的摄像头:
CameraMetadata.LENS_FACING_BACK为后置摄像头,也可以用字符串"0"表示;
CameraMetadata.LENS_FACING_FRONT为前置摄像头"1";

cameraManager.getCameraCharacteristics(cameraId);传入指定id获取该摄像头的characteristics,
通过CameraCharacteristics.CONTROL_AE_AVAILABLE_TARGET_FPS_RANGES字段获取支持的FPS(前后摄像头一样);
通过CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP字段获取设备支持的预览尺寸;

预览尺寸指相机把画面输出到手机屏幕上供用户预览的尺寸,一般手机都会支持多个不同尺寸供用户选择,通常在不超过手机分辨率下,选择越大的尺寸,预览画面越清晰.
(荣耀10 分辨率为2280x1080,我选择map.getOutputSizes(SurfaceTexture.class)[2],即2160x1064)

    /**
     * 遍历所有摄像头,检查是否可用
     */
    private void cameraList() {
        //获得所有摄像头的管理者CameraManager
        cameraManager = (CameraManager) getSystemService(Context.CAMERA_SERVICE);
        CameraCharacteristics characteristics = null;
        try {
            for (String cameraId : cameraManager.getCameraIdList()) {
                characteristics = cameraManager.getCameraCharacteristics(cameraId);
                //后置摄像头"0"
                if (characteristics.get(CameraCharacteristics.LENS_FACING) == CameraMetadata.LENS_FACING_BACK) {
                    mBackCameraId = cameraId;
                    //摄像头支持的FPS范围,前后摄像头一样
                    FpsRanges = characteristics.get(CameraCharacteristics.CONTROL_AE_AVAILABLE_TARGET_FPS_RANGES);
                    //cameraList: backFpsRanges[[12, 15], [15, 15], [14, 20], [20, 20], [14, 25], [25, 25], [14, 30], [30, 30]]
                    Log.i(TAG, "cameraList: backFpsRanges" + Arrays.toString(FpsRanges));
                }
                //前置摄像头"1"
                if (characteristics.get(CameraCharacteristics.LENS_FACING) == CameraMetadata.LENS_FACING_FRONT) {
                    mFontCameraId = cameraId;
                }
            }
              if (mBackCameraId != null) {
                mCameraId = mBackCameraId;//默认打开后置摄像头
            } else if (mFontCameraId != null) {
                mCameraId = mFontCameraId;
                Toast.makeText(this, "后置摄像头不可用,已切换前置摄像头", Toast.LENGTH_SHORT).show();
            } else {
                Toast.makeText(this, "摄像头不可用,请检查设备", Toast.LENGTH_SHORT).show();
            }
        } catch (CameraAccessException e) {
            e.printStackTrace();
        }
        //获取摄像头支持的所有输出格式和尺寸的管理者StreamConfigurationMap
        StreamConfigurationMap map = characteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP);
        //取摄像头支持的预览尺寸
        mPreviewSize = map.getOutputSizes(SurfaceTexture.class)[2];
        //cameraList: mPreviewSize    [0->]2816x2112 (预览清晰)		[2]->2160x1064      [15]->208x144(预览模糊)
        Log.i(TAG, "cameraList: mPreviewSize    " + mPreviewSize.toString());
    }

4.我们用 ttvPreview.setSurfaceTextureListener(this);对SurfaceTexture进行监听;
当SurfaceTexture可用时回调onSurfaceTextureAvailable方法
在该方法中做打开相机:openCamera(String cameraId, CameraDevice.StateCallback callback, Handler handler);
第一个参数是传入指定的摄像头Id,一般的"0"是后置摄像头,"1"是前置摄像头;
第二个参数是相机状态监听的回调;
第三个参数是接收消息使用的mHandler,表示在哪个线程调用,如果为null,就在当前线程使用.

  @Override
    public void onSurfaceTextureAvailable(SurfaceTexture surfaceTexture, int i, int i1) {
        if (mCameraId != null) {
            openCamera(mCameraId);
        }
    }
    private void openCamera(String mCameraId) {
    cameraManager.openCamera(mCameraId, mCameraDeviceStateCallback, mHandler);
    }

5.接着我们要创建CameraDevice.StateCallback对象,这个抽象类是对相机设备状态的监听;
当相机成功打开时回调onOpened方法;
我们在该方法中做开始预览的操作startPreview(mCameraDevice);
若相机链接失败或者报错时关闭设备mCameraDevice.close();

private CameraDevice.StateCallback mCameraDeviceStateCallback = new CameraDevice.StateCallback() {
        @Override
        public void onOpened(CameraDevice cameraDevice) {
            mCameraDevice = cameraDevice;
            startPreview(mCameraDevice);
        }
      @Override
        public void onDisconnected(CameraDevice cameraDevice) {
            if (mCameraDevice != null) {
                mCameraDevice.close();
                mCameraDevice = null;
            }
        }
        @Override
        public void onError(CameraDevice cameraDevice, int i) {
            if (mCameraDevice != null) {
                mCameraDevice.close();
                mCameraDevice = null;
            }
        }
    };

6.开始预览startPreview(CameraDevice cameraDevice),
通过 getSurfaceTexture()获取SurfaceTexture:
两者关系可以理解为TextureView是一幅画,SurfaceTexture是画布,真正渲染的载体是SurfaceTexture.

根据我们在cameraList获取支持的预览尺寸设置预览尺寸:
设置捕获请求模式为预览模式:cameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW);
也有其他模式:TEMPLATE_STILL_CAPTURE(拍照) TEMPLATE_RECORD(录像)
这里我们还可以相机进行曝光,对焦,FPS等设置

为了得到预览数据,我们需要ImageReader对象,
ImageReader.newInstance(mImageWidth, mImageHeight, ImageFormat.JPEG, 2);
第一个和第二个参数是指定图片的大小,
第三个是指定图片的格式,
第四个参数是ImageReader获取到的图片数量(2+1张)
有了它我们就可以做预览数据的回调:
mImageReader.setOnImageAvailableListener(mOnImageAvailableListener, mHandler);
创建ImageReader.OnImageAvailableListener对象

    private ImageReader.OnImageAvailableListener mOnImageAvailableListener = (ImageReader imageReader) -> {
        //从ImageReader队列获取下一个图像
        Image img = imageReader.acquireNextImage();
        //转格式,保存等操作
        //注意不用要关闭,否则会报错
        img.close();

    };

7.现在还不能捕捉到图像,还需要建立和相机的捕捉会话
首先要添加两个surface,一个TextureView的,用于页面显示;另一个ImageReader的,用于预览数据回调.
mPreviewBuilder.addTarget(surface);
mPreviewBuilder.addTarget(mImageReader.getSurface());
然后创建捕捉会话:cameraDevice.createCaptureSession(Arrays.asList(surface, mImageReader.getSurface()), mSessionStateCallback, mHandler);

以下是开始预览操作startPreview全部代码:

    private void startPreview(CameraDevice cameraDevice) {
        SurfaceTexture texture = ttvPreview.getSurfaceTexture();
        //设置的就是预览大小
        texture.setDefaultBufferSize(mPreviewSize.getWidth(), mPreviewSize.getHeight());
        Surface surface = new Surface(texture);
        try {
            //设置捕获请求模式为预览
            mPreviewBuilder = cameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW);
        } catch (CameraAccessException e) {
            e.printStackTrace();
        }
        //对相机参数进行设置
        setCamera();
        //图片的大小,格式以及捕捉数量(2+1)
        mImageReader = ImageReader.newInstance(mImageWidth, mImageHeight, ImageFormat.JPEG, 2);
        mImageReader.setOnImageAvailableListener(mOnImageAvailableListener, mHandler);
        //添加两个surface,一个TextureView的,另一个ImageReader的,用于页面显示和预览数据回调
        mPreviewBuilder.addTarget(surface);
        mPreviewBuilder.addTarget(mImageReader.getSurface());
        try {
            cameraDevice.createCaptureSession(Arrays.asList(surface, mImageReader.getSurface()), mSessionStateCallback, mHandler);
        } catch (CameraAccessException e) {
            e.printStackTrace();
        }

    }
    private void setCamera() {
        //曝光
        mPreviewBuilder.set(CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON_AUTO_FLASH);
        //FPS
        mPreviewBuilder.set(CaptureRequest.CONTROL_AE_TARGET_FPS_RANGE, FpsRanges[5]);
    }

8.创建CameraCaptureSession.StateCallback对象,在回调方法onConfigured中进行请求捕捉图像;

cameraCaptureSession.setRepeatingRequest(mPreviewBuilder.build(), null, mHandler);
第一个参数是CaptureRequest.Builder的build()方法得到CaptureRequest对象;
第二个参数是监听预览过程的回调,不需要特殊处理的时候,可以传null;
第三个参数上面有说明了.
该方法会不断的重复捕捉图像,就会不断回调onImageAvailable方法.
而capture(CaptureRequest request,CaptureCallback listener, Handler handler)方法只会请求捕捉一次,可实现拍照功能.

    private CameraCaptureSession.StateCallback mSessionStateCallback = new CameraCaptureSession.StateCallback() {
        @Override
        public void onConfigured(CameraCaptureSession cameraCaptureSession) {
            mCaptureSession = cameraCaptureSession;
            try {
                cameraCaptureSession.setRepeatingRequest(mPreviewBuilder.build(), null, mHandler);
            } catch (CameraAccessException e) {
                e.printStackTrace();
            }
        }
        @Override
        public void onConfigureFailed(CameraCaptureSession cameraCaptureSession) {
        }
    };

9.结束后要释放资源:

   @Override
    protected void onDestroy() {
        closeCamera();
        super.onDestroy();
    }
        private void closeCamera() {
        if (mCaptureSession != null) {
            mCaptureSession.close();
            mCaptureSession = null;
        }
        if (mCameraDevice != null) {
            mCameraDevice.close();
            mCameraDevice = null;
        }
        if (mImageReader != null) {
            mImageReader.close();
            mImageReader = null;
        }
    }

前后置摄像头切换功能:先关闭当前摄像头,判断当前摄像头是前还是后,然后设置mCameraId为另一个,接着判断TextureView是否可用,可用直接openCamera(mCameraId);不可用则重新获取TextureView;

    @Override
    public void onClick(View v) {

        switch (v.getId()) {
            case R.id.btn_switch:
                closeCamera();
                if (mCameraId != null) {
                    if (mCameraId.equals(mBackCameraId) && mFontCameraId != null) {//后置切换前置
                        mCameraId = mFontCameraId;
                    } else if (mCameraId.equals(mFontCameraId) && mBackCameraId != null) {//前置切换成后置
                        mCameraId = mBackCameraId;
                    } else {
                        Toast.makeText(this, "摄像头不可用,请检查设备", Toast.LENGTH_SHORT).show();
                        break;
                    }
                    if (ttvPreview.isAvailable()) {
                        openCamera(mCameraId);
                    } else {
                        //重新获取textureView
                        ttvPreview.setSurfaceTextureListener(this);
                    }
                    Toast.makeText(this, "切换成功", Toast.LENGTH_SHORT).show();
                } else {
                    Toast.makeText(this, "摄像头不可用,请检查设备", Toast.LENGTH_SHORT).show();
                }
                break;
            default:
                break;
        }
    }

后续还会完善功能!
参考链接:https://blog.csdn.net/matrix_laboratory/article/details/80693537

camera2中文文档链接:https://blog.csdn.net/zhangbijun1230/article/details/80556903
这个链接非常有用,不懂的字段,api可以自行查看,里面都有说明.
github:https://github.com/sunfusong/Camera2Demo.git

你可能感兴趣的:(Android,Camera2)