当前位置:   article > 正文

Android之camera1和2的简单使用_android:name="android.hardware.camera

android:name="android.hardware.camera

前言

Android Framework提供Camera API来实现拍照与录制视频的功能,目前Android有三类API,

Camera
此类是用于控制设备相机的旧版 API,现已弃用,在Android5.0以下使用
Camera2
此软件包是用于控制设备相机的主要 API,Android5.0以上使用
CameraX
基于Camera 2 API封装,简化了开发流程,并增加生命周期控制

每个版本也有一些重要的更新点,需要的时候搜一下Android开发者官网即可。今天先对比下老旧的camera1和camera2。

Camera1 预览、拍照

 首先,先申请权限:

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

获取相机个数

一般手机中,都有前置摄像头和后置摄像头,我们可以根据 Camera 的 getNumberOfCameras() 方法,来获取这些信息。比如:

  1. //获取相机个数
  2. int numberOfCameras = Camera.getNumberOfCameras();
  3. for (int i = 0; i < numberOfCameras; i++) {
  4. Camera.CameraInfo info = new Camera.CameraInfo();
  5. //获取相机信息
  6. Camera.getCameraInfo(i, info);
  7. //前置摄像头
  8. if (Camera.CameraInfo.CAMERA_FACING_FRONT == info.facing) {
  9. mFrontCameraId = i;
  10. mFrontCameraInfo = info;
  11. } else if (Camera.CameraInfo.CAMERA_FACING_BACK == info.facing) {
  12. mBackCameraId = i;
  13. mBackCameraInfo = info;
  14. }
  15. }

可以看到,通过 Camera.getCameraInfo(i, info) 就可以拿到当前的 CameraInfo 的信息,里面有个参数我们需要注意一下,就是 facing,它表示当前摄像机面对的方向,理解为前置和后置,然后我们把这些信息也保存起来。

打开摄像头

接着,我们可以使用 Camera.open(cameraid) 去打开摄像头

  1. //根据 cameraId 打开不同摄像头
  2. mCamera = Camera.open(cameraId);

打开我们的摄像头之后,可以对它进行一些配置,比如设置预览方向等,这个话题我们等到下面出现了再说。

配置摄像头属性

在开启相机预览之前,我们需要对相机进行一些参数配置,比如聚焦,预览尺寸等;这里我使用的是 SurfaceView,所以等SurfaceView 创建好之后,可以对它进行一些参数的设置:

  1. @Override
  2. public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
  3. startPreview(width, height);
  4. }
  5. # startPreview
  6. private void startPreview(int width, int height) {
  7. //配置camera参数
  8. initPreviewParams(width, height);
  9. //设置预览 SurfaceHolder
  10. Camera camera = mCamera;
  11. if (camera != null) {
  12. try {
  13. camera.setPreviewDisplay(mSurfaceView.getHolder());
  14. } catch (IOException e) {
  15. e.printStackTrace();
  16. }
  17. }
  18. //开始显示
  19. camera.startPreview();
  20. }

在Camra 中,我们可以通过 camera.getParameters() 拿到相机默认的参数,如果要配置自己的参数,可以使用 camera.setParameters(parameters) 去设置,不过这个比较比较好使,所以相机的配置开启这些,可以使用 HandlerThread 去开启,这里就不增加多余代码了。
initPreviewParams 的完整代码如下:

  1. private void initPreviewParams(int shortSize, int longSize) {
  2. Camera camera = mCamera;
  3. if (camera != null) {
  4. Camera.Parameters parameters = camera.getParameters();
  5. //获取手机支持的尺寸
  6. List<Camera.Size> sizes = parameters.getSupportedPreviewSizes();
  7. Camera.Size bestSize = getBestSize(shortSize, longSize, sizes);
  8. //设置预览大小
  9. parameters.setPreviewSize(bestSize.width, bestSize.height);
  10. //设置图片大小,拍照
  11. parameters.setPictureSize(bestSize.width, bestSize.height);
  12. //设置格式,所有的相机都支持 NV21格式
  13. parameters.setPreviewFormat(ImageFormat.NV21);
  14. //设置聚焦
  15. parameters.setFocusMode(Camera.Parameters.FOCUS_MODE_CONTINUOUS_PICTURE);
  16. camera.setParameters(parameters);
  17. }
  18. }

相机预览大小

首先,应该根据自己UI的大小去设置相机预览的大小,如果你的控件为 200x200,但相机的数据为 1920x1080 ,这样填充过去,画面肯定是会被拉伸的。所以,可以通过

List<Camera.Size> sizes = parameters.getSupportedPreviewSizes()

拿到手机相机支持的所有尺寸;所以,我们需要找到比例相同,或者近似的大小,跟UI配合,这样画面才不会拉伸,注意相机的 width > height,所以获取一个最佳的预览尺寸可以这样写:

  1. /**
  2. * 获取预览最后尺寸
  3. */
  4. private Camera.Size getBestSize(int shortSize, int longSize, List<Camera.Size> sizes) {
  5. Camera.Size bestSize = null;
  6. float uiRatio = (float) longSize / shortSize;
  7. float minRatio = uiRatio;
  8. for (Camera.Size previewSize : sizes) {
  9. float cameraRatio = (float) previewSize.width / previewSize.height;
  10. //如果找不到比例相同的,找一个最近的,防止预览变形
  11. float offset = Math.abs(cameraRatio - minRatio);
  12. if (offset < minRatio) {
  13. minRatio = offset;
  14. bestSize = previewSize;
  15. }
  16. //比例相同
  17. if (uiRatio == cameraRatio) {
  18. bestSize = previewSize;
  19. break;
  20. }
  21. }
  22. return bestSize;
  23. }

当 UI 的比例跟相机支持的比例相同,直接返回,否则则找近似的。

效果如下:

 发现预览的方向是反的;这个时候就需要使用 setDisplayOrientation() 去设置预览方向了。

调整预览方向

首先,在调整预览方向钱,我们需要先了解一些知识。

屏幕坐标: Android 坐标系中,在 (0,0) 坐标那,向右为 x 轴,向下为 y 轴。
自然方向: 设置的自然方向,比如手机默认就是竖直是自然方向,平板的话,横向就是自然方向
图片传感器方向: 手机的图片数据都来自摄像头硬件传感器,这个传感器有个默认的方向,一般是手机是横向的,这就跟手机的自然方向成 90° 关系了。

所以,我们要做的就是,就是把传感器拿到的图片,进行一个角度的变化,使图像能跟自然方向一致:

 所以,我们的方向调整可以这样写:

  1. private void adjustCameraOrientation(Camera.CameraInfo info) {
  2. //判断当前的横竖屏
  3. int rotation = getWindowManager().getDefaultDisplay().getRotation();
  4. int degress = 0;
  5. //获取手机的方向
  6. switch (rotation) {
  7. case Surface.ROTATION_0:
  8. degress = 0;
  9. break;
  10. case Surface.ROTATION_90:
  11. degress = 90;
  12. break;
  13. case Surface.ROTATION_180:
  14. degress = 180;
  15. break;
  16. case Surface.ROTATION_270:
  17. degress = 270;
  18. break;
  19. }
  20. int result = 0;
  21. //后置摄像头
  22. if (info.facing == Camera.CameraInfo.CAMERA_FACING_BACK) {
  23. result = (info.orientation - degress + 360) % 360;
  24. } else if (info.facing == Camera.CameraInfo.CAMERA_FACING_FRONT) {
  25. //先镜像
  26. result = (info.orientation + degress) % 360;
  27. result = (360 - result) % 360;
  28. }
  29. mCamera.setDisplayOrientation(result);
  30. }

最后可得:

 切换摄像头

现在用到的都是后置摄像头,切换也比较简单,首先先释放相机支援,然后再从配置参数,预览再来一遍即可:

  1. //关闭摄像头
  2. closeCamera();
  3. mCameraID = mCameraID == mFrontCameraId ? mBackCameraId : mFrontCameraId;
  4. //打开相机
  5. openCamera(mCameraID);
  6. //开启预览
  7. startPreview(mSurfaceView.getWidth(), mSurfaceView.getHeight());
  8. #closeCamera
  9. private void closeCamera() {
  10. //停止预览
  11. mCamera.stopPreview();
  12. mCamera.release();
  13. mCamera = null;
  14. }

拍照及调整图片方向

Camera 的拍照也比较简单,使用 takePicture(ShutterCallback shutter, PictureCallback raw, PictureCallback jpeg) 方法即可,它的三个参数如下:

  • ShutterCallback :拍照瞬间调用,如果空回调,则由声音,传 null ,则没效果
  • PictureCallback :图片的原始数据,即没处理过的
  • PictureCallback : 图片的 JPEG 数据

拿到 byte 数据后,转换成bitmap即可,如下:

  1. Camera camera = mCamera;
  2. camera.takePicture(new Camera.ShutterCallback() {
  3. @Override
  4. public void onShutter() {
  5. }
  6. }, null, new Camera.PictureCallback() {
  7. @Override
  8. public void onPictureTaken(byte[] data, Camera camera) {
  9. new SavePicAsyncTask(data).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
  10. }
  11. });

这里的图片保存,用一个 AsyncTask 来保存:

  1. /**
  2. * 保存图片
  3. */
  4. class SavePicAsyncTask extends AsyncTask<Void, Void, File> {
  5. byte[] data;
  6. File file;
  7. public SavePicAsyncTask(byte[] data) {
  8. this.data = data;
  9. File dir = new File(Constants.PATH);
  10. if (!dir.exists()) {
  11. dir.mkdirs();
  12. }
  13. String name = "test.jpg";
  14. file = new File(dir, name);
  15. }
  16. @Override
  17. protected File doInBackground(Void... voids) {
  18. Bitmap bitmap = BitmapFactory.decodeByteArray(data, 0, data.length);
  19. if (bitmap == null) {
  20. return null;
  21. }
  22. FileOutputStream fos = null;
  23. try {
  24. fos = new FileOutputStream(file);
  25. //保存之前先调整方向
  26. Camera.CameraInfo info = mCameraID == mFrontCameraId ? mFrontCameraInfo : mBackCameraInfo;
  27. if (info.facing == Camera.CameraInfo.CAMERA_FACING_BACK) {
  28. bitmap = BitmapUtils.rotate(bitmap, 90);
  29. } else {
  30. bitmap = BitmapUtils.rotate(bitmap, 270);
  31. }
  32. bitmap.compress(Bitmap.CompressFormat.JPEG, 100, fos);
  33. } catch (FileNotFoundException e) {
  34. e.printStackTrace();
  35. } finally {
  36. CloseUtils.close(fos);
  37. }
  38. return file;
  39. }
  40. @Override
  41. protected void onPostExecute(File file) {
  42. super.onPostExecute(file);
  43. if (file != null) {
  44. Toast.makeText(Camera1Activity.this, "图片保存成功", Toast.LENGTH_SHORT).show();
  45. } else {
  46. Toast.makeText(Camera1Activity.this, "图片保存失败", Toast.LENGTH_SHORT).show();
  47. }
  48. }
  49. }
  50. #BitmapUtils#rotate
  51. public static Bitmap rotate(Bitmap bitmap,float degress){
  52. Matrix matrix = new Matrix();
  53. matrix.postRotate(degress);
  54. return Bitmap.createBitmap(bitmap,0,0,bitmap.getWidth(),bitmap.getHeight(),matrix,true);
  55. }

当拿到 byte[] 数据时,使用 BitmapFactory.decodeByteArray 解析 bitmap ,但此时的图片也是不对的,需要对它进行一个旋转,如上所示,这样,我们的拍照就也完成了。


Camera2 的使用

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

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

Pipeline

Camera2 的将摄像头包装成管道(Pipeline),它会捕获单个帧的输入请求,每个请求捕获单个图像,然后把这些数据包装成数据包,从而把这些图像数据输出到图片缓冲区中。

这些请求时按顺序处理的,它按顺序处理每一帧的请求,并返回请求结果给客户端,看下面这张图:

假设我们要同时拍摄两张不同尺寸的图片,那么它的过程应该是这样的:

  • 创建一个用于 Pipeline 获取图片信息的 CaptureRequest
  • 创建两个不同的 Surface ,用来接收图片数据,并把他们加到 CaptureRequest 中
  • 发送配置好的的 CaptureRequest 到Pipeline 中,等待返回拍照结果

上面需要记住的是,CaptureReuqest 创建之前,我们已经把相机的数据都配置好,比如聚焦、闪光灯等,接着才把它输入给 Camrea2 的底层,它会被放入到一个被叫做 In-Flight Capture Queue 的队列中,当 In-Flight Capture Queue 队列空闲时,我们就可以从它拿到不同的图片数据给到Surface ,且能拿到CaptureResult 这个返回结果信息。

Supported Hardware Level

我们支持,Camera 支持了很多新功能的特性,但这也要看你的手机厂商的支持程度,所以,为了方便区分,Camera2 使用 Supported Hardware Level 来判断你是否支持 Camera2 的特性,它分为4个登记:

  • LEGACY :向后兼容模式,支持Camera1 的功能,不支持 Camera2 的新特性
  • LIMITED :除了支持 Camera1 的特性,还支持部分 Camera2 的高级特性
  • FULL :支持所有 Camera2 的高级特性
  • LEVEL_3 :新增更多的 Camera2 特性,利于YUV 数据等

再了解一些主要的开发类:

CameraManager
相机系统服务,用于管理和连接相机设备

CameraDevice
相机设备类,和Camera1中的Camera同级

CameraCharacteristics
主要用于获取相机信息,内部携带大量的相机信息,包含摄像头的正反(LENS_FACING)、AE模式、AF模式等,和Camera1中的Camera.Parameters类似

CaptureRequest
相机捕获图像的设置请求,包含传感器,镜头,闪光灯等

CaptureRequest.Builder
CaptureRequest的构造器,使用Builder模式,设置更加方便

CameraCaptureSession
请求抓取相机图像帧的会话,会话的建立主要会建立起一个通道。一个CameraDevice一次只能开启一个CameraCaptureSession。
源端是相机,另一端是 Target,Target可以是Preview,也可以是ImageReader。

ImageReader
用于从相机打开的通道中读取需要的格式的原始图像数据,可以设置多个ImageReader。

根据以上这些特性,完成 Camera2 的开发。

相机预览

申请权限

  1. <uses-permission android:name="android.permission.CAMERA" /> <!-- 支持相机才能运行 -->
  2. <!--需要设备有相机-->
  3. <uses-feature
  4. android:name="android.hardware.camera"
  5. android:required="true" />
  6. <uses-feature android:name="android.hardware.camera.autofocus" />

简单流程如下:

  1. 通过context.getSystemService(Context.CAMERA_SERVICE) 获取CameraManager.
  2. 通过 getCameraCharacteristics() 方法,拿到相机的所有信息,比如支持的预览大小,level 等
  3. 通过 CameraManager 的 openCamera() 方法,从回调中拿到 CameraDevice ,它表示当前相机设备
  4. CameraDevice 通过 createCaptureRequest 创建 CaptureRequest.Builder ,用来配置相机属性,通过 createCaptureSession 创建 CameraCaptureSession ,它是 Pipeline 的实例,然后交给底层处理

获取相机信息

通过 CameraManager 的 getCameraCharacteristics() 方法,来获取相机的信息;CameraManager 是一个负责查询和建立相机连接的系统服务,它的功能不多,主要如下:

  • 将相机信息装载到 CameraCharacteristics 中。
  • 根据指定的相机ID 连接相机
  • 提供将闪光灯设置为手电筒的快捷方式

所以它的代码如下:

  1. try {
  2. //获取相机服务 CameraManager
  3. mCameraManager = (CameraManager) getSystemService(Context.CAMERA_SERVICE);
  4. //遍历设备支持的相机 ID ,比如前置,后置等
  5. String[] cameraIdList = mCameraManager.getCameraIdList();
  6. for (String cameraId : cameraIdList) {
  7. // 拿到装在所有相机信息的 CameraCharacteristics 类
  8. CameraCharacteristics characteristics = mCameraManager.getCameraCharacteristics(cameraId);
  9. //拿到相机的方向,前置,后置,外置
  10. Integer facing = characteristics.get(CameraCharacteristics.LENS_FACING);
  11. if (facing != null) {
  12. //后置摄像头
  13. if (facing == CameraCharacteristics.LENS_FACING_BACK) {
  14. mBackCameraId = cameraId;
  15. mBackCameraCharacteristics = characteristics;
  16. }else if (facing == CameraCharacteristics.LENS_FACING_FRONT){
  17. //前置摄像头
  18. mFrontCameraId = cameraId;
  19. mFrontCameraCharacteristics = characteristics;
  20. }
  21. mCameraId = cameraId;
  22. }
  23. //是否支持 Camera2 的高级特性
  24. Integer level = characteristics.get(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL);
  25. /**
  26. * 不支持 Camera2 的特性
  27. */
  28. if (level == null || level == CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY){
  29. // Toast.makeText(this, "您的手机不支持Camera2的高级特效", Toast.LENGTH_SHORT).show();
  30. // break;
  31. }
  32. }
  33. } catch (CameraAccessException e) {
  34. e.printStackTrace();
  35. }

上面的注释都很清晰了,我们是通过 CameraCharacteristics 去知道当前的相机方向;除了这些,它还包含相机的其他信息,比如:

  • 是否有闪光灯 FLASH_INFO_AVAILABLE
  • 是否有 AE 模式 CONTROL_AE_AVAILABLE_MODES
  • 光爆等,如果你对 Camera1 比较熟悉,那么 CameraCharacteristics 有点像 Camera1 的 Camera.CameraInfo 或者 Camera.Parameters

打开摄像头

比 Camera1 好的就是,Camera2 在打开摄像头之前,就可以进行参数的配置,比如预览尺寸等。
我们知道,摄像头需要 Surface 来装载数据,这里使用的是 TextureView 来装载:

  mTextureView = findViewById(R.id.surface);

所以,当它创建完成拿到宽高之后,我们就可以打开摄像头了:

  1. private void openCamera(int width, int height) {
  2. //判断不同摄像头,拿到 CameraCharacteristics
  3. CameraCharacteristics characteristics = mCameraId.equals(mBackCameraId) ? mBackCameraCharacteristics : mFrontCameraCharacteristics;
  4. //拿到配置的map
  5. StreamConfigurationMap map = characteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP);
  6. //获取摄像头传感器的方向
  7. mSensorOrientation = characteristics.get(CameraCharacteristics.SENSOR_ORIENTATION);
  8. //获取预览尺寸
  9. Size[] previewSizes = map.getOutputSizes(SurfaceTexture.class);
  10. //获取最佳尺寸
  11. Size bestSize = getBestSize(width, height, previewSizes);
  12. /**
  13. * 配置预览属性
  14. * 与 Cmaera1 不同的是,Camera2 是把尺寸信息给到 Surface (SurfaceView 或者 ImageReader),
  15. * Camera2 会根据 Surface 配置的大小,输出对应尺寸的画面;
  16. * 注意摄像头的 width > height ,而我们使用竖屏,所以宽高要变化一下
  17. */
  18. mTextureView.getSurfaceTexture().setDefaultBufferSize(bestSize.getHeight(),bestSize.getWidth());
  19. /**
  20. * 设置图片尺寸,这里图片的话,选择最大的分辨率即可
  21. */
  22. Size[] sizes = map.getOutputSizes(ImageFormat.JPEG);
  23. Size largest = Collections.max(
  24. Arrays.asList(sizes),
  25. new CompareSizesByArea());
  26. //设置imagereader,配置大小,且最大Image为 1,因为是 JPEG
  27. mImageReader = ImageReader.newInstance(largest.getWidth(),largest.getHeight(),
  28. ImageFormat.JPEG,1);
  29. //拍照监听
  30. mImageReader.setOnImageAvailableListener(new ImageAvailable(),null);
  31. try {
  32. //打开摄像头,监听数据
  33. mCameraManager.openCamera(mCameraId,new CameraDeviceCallback(),null);
  34. } catch (CameraAccessException e) {
  35. e.printStackTrace();
  36. }
  37. }

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

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

拿到可用的map,这样就可以通过 getOutputSizes() 等拿到相机支持的所有尺寸了,通过前面的camera1也解释了,为啥要有最佳尺寸,不然会有图片拉伸的问题。

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

接着设置图片的尺寸,这里当然是越轻越好了,所以选择最大尺寸即可。ImageReader 等到后面拍照时再讲解。

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

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

CameraDevice

CameraDevice 表示当前的相机设备,它的主要职责有:

  • 根据指定的参数创建 CameraCaptureSession
  • 根据指定的模板创建 CaptureRequest
  • 关闭相机设备
  • 监听相机状态,比如断开,开启成功失败的监听

如下:

  1. class CameraDeviceCallback extends CameraDevice.StateCallback{
  2. @Override
  3. public void onOpened(@NonNull CameraDevice camera) {
  4. mCameraDevice = camera;
  5. //此时摄像头已经打开,可以预览了
  6. createPreviewPipeline(camera);
  7. }
  8. @Override
  9. public void onDisconnected(@NonNull CameraDevice camera) {
  10. camera.close();
  11. }
  12. @Override
  13. public void onError(@NonNull CameraDevice camera, int error) {
  14. camera.close();
  15. }
  16. }

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

  1. private void createPreviewPipeline(CameraDevice cameraDevice){
  2. try {
  3. //创建作为预览的 CaptureRequst.builder
  4. final CaptureRequest.Builder captureBuilder = cameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW);
  5. Surface surface = new Surface(mTextureView.getSurfaceTexture());
  6. //添加 surface 容器
  7. captureBuilder.addTarget(surface);
  8. // 创建CameraCaptureSession,该对象负责管理处理预览请求和拍照请求,这个必须在创建 Seesion 之前就准备好,传递给底层用于遏制 pipeline
  9. cameraDevice.createCaptureSession(Arrays.asList(surface, mImageReader.getSurface()), new CameraCaptureSession.StateCallback() {
  10. @Override
  11. public void onConfigured(@NonNull CameraCaptureSession session) {
  12. mCameraCaptureSession = session;
  13. try {
  14. //设置自动聚焦
  15. captureBuilder.set(CaptureRequest.CONTROL_AE_MODE,CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE);
  16. //设置自动曝光
  17. captureBuilder.set(CaptureRequest.CONTROL_AE_MODE,CaptureRequest.CONTROL_AE_MODE_ON_AUTO_FLASH);
  18. //创建 CaptureRequest
  19. CaptureRequest build = captureBuilder.build();
  20. //设置预览时连续捕获图片数据
  21. session.setRepeatingRequest(build,null,null);
  22. }catch (Exception e){
  23. }
  24. }
  25. @Override
  26. public void onConfigureFailed(@NonNull CameraCaptureSession session) {
  27. Toast.makeText(Camera2Activity.this, "配置失败", Toast.LENGTH_SHORT).show();
  28. }
  29. },null);
  30. } catch (Exception e) {
  31. e.printStackTrace();
  32. }
  33. }

在开启预览之前,我们需要先创建 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 用来拍照的

由于这个 CaptureRequest 是用来预览的,所以通过 addTarget 设置进去。最后通过 cameraDevice.createCaptureSession() 创建 CameraCaptureSession ,然后再配置一下聚焦和曝光的配置,就可以把 CaptureRequest 通过 Session 发送给底层了。

开启和关闭预览

在 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

关闭预览

通过上面的理解,关闭预览也很简单啦:

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

拍照

上面我们学习了预览,也提到了 ImageReader 是用来接收图像数据的,那怎么拍照呢?

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

  1. //创建一个拍照的 session
  2. final CaptureRequest.Builder captureRequest = mCameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_STILL_CAPTURE);
  3. //设置装在图像数据的 Surface
  4. captureRequest.addTarget(mImageReader.getSurface());
  5. //聚焦
  6. captureRequest.set(CaptureRequest.CONTROL_AF_MODE,
  7. CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE);
  8. //自动曝光
  9. captureRequest.set(CaptureRequest.CONTROL_AF_MODE,
  10. CaptureRequest.CONTROL_AE_MODE_ON_AUTO_FLASH);
  11. // 获取设备方向
  12. int rotation = getWindowManager().getDefaultDisplay().getRotation();
  13. // 根据设备方向计算设置照片的方向
  14. captureRequest.set(CaptureRequest.JPEG_ORIENTATION
  15. , getOrientation(rotation));
  16. // 先停止预览
  17. mCameraCaptureSession.stopRepeating();

代码比较好理解,只是通过 mCameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_STILL_CAPTURE) 创建了一个 拍照的模板,而且在执行拍照之前,先停止预览。

接着就可以使用 mCameraCaptureSession.capture() 执行拍照了:

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

可以看到,当拍照结束的时候,我们又让它重新预览了,当然这里也看你的需求去设置。

保存图片

那保存图片在哪里弄呢?还记得我们在打开摄像头的时候,配置了 ImageReader 监听:

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

在保存之前,我们先来了解一下,什么是 ImageReader

ImageReader

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

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

其中前面两个号理解,第三个参数,则是你要获取的图像数据的格式,这里使用 JPEG 即可,而最后一个参数,则表示最大 Image 的个数,可以理解成 图像池的大小。

当有图像数据生成时,就是调用 ImageReader.OnImageAvailableListener 里面的 onImageAvailable() 方法

  1. /**
  2. * 拍照监听,当有图片数据时,回调该接口
  3. */
  4. class ImageAvailable implements ImageReader.OnImageAvailableListener{
  5. @Override
  6. public void onImageAvailable(ImageReader reader) {
  7. new SavePicAsyncTask(reader).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
  8. }
  9. }

接着,我们可以调用 ImageReader 的acquireNextImage()方法,来获取存有最新的 Image 对象,而 Image 对象里的图像数据又根据不同格式被划分成多个部分,分别存储在单独的 Plane 对象里,我们可以调用 Image.getPalnes() 获取所有有的 Palne 对象的数组,如下:

  1. //获取捕获的照片数据
  2. Image image = imageReader.acquireLatestImage();
  3. //拿到所有的 Plane 数组
  4. Image.Plane[] planes = image.getPlanes();

最后则通过 Plane.getBuffer() 获取每一个在 Plane 里存储的图像数据 ByteBuffer。比如

格式Image个数Plane 层级
JPEG1压缩过的数据,所以行数为0,解压缩需要使用BitmapFactory#decodeByteArray
YUV3一个明度通道+两个色彩CbCr通道,UV的宽高是Y的一半。

YUV 可以用下图来表示

 所以,我们获取的图片如下:

  1. FileOutputStream fos = null;
  2. Image image = null;
  3. try {
  4. fos = new FileOutputStream(file);
  5. //获取捕获的照片数据
  6. image = imageReader.acquireLatestImage();
  7. //拿到所有的 Plane 数组
  8. Image.Plane[] planes = image.getPlanes();
  9. //由于是 JPEG ,只需要获取下标为 0 的数据即可
  10. ByteBuffer buffer = planes[0].getBuffer();
  11. data = new byte[buffer.remaining()];
  12. //把 bytebuffer 的数据给 byte数组
  13. buffer.get(data);
  14. Bitmap bitmap = BitmapFactory.decodeByteArray(data,0,data.length);
  15. //旋转图片
  16. if (mCameraId.equals(mFrontCameraId)){
  17. bitmap = BitmapUtils.rotate(bitmap,270);
  18. bitmap = BitmapUtils.mirror(bitmap);
  19. }else{
  20. bitmap = BitmapUtils.rotate(bitmap,90);
  21. }
  22. bitmap.compress(Bitmap.CompressFormat.JPEG,100,fos);
  23. fos.flush();
  24. return bitmap;
  25. }catch (Exception e){
  26. Log.d(TAG, "zsr doInBackground: "+e.toString());
  27. }finally {
  28. CloseUtils.close(fos);
  29. //记得关闭 image
  30. if (image != null) {
  31. image.close();
  32. }
  33. }

ok,camera1和camera2的简单使用就到这里了。

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/article/detail/46146?site
推荐阅读
相关标签
  

闽ICP备14008679号