App下载 微信公众号

Camera2自定义照相机-1【Android】

技术 · 移动开发 · Android/ 作者【吾非言】/ 发布于2021-11-8/ 2.47k次浏览
2021 11/8 10:38
摘要: 自定义照相机功能在现今的APP开发当中,已经是一个非常常见的需求。我们常见的扫描二维码,身份证拍照,已经美颜相机,直播视频等等都使用到了照相机功能,那么在Android平台该如何使用Camera API进行相机开发呢?

简介:

Camera2是谷歌继Camera1之后的升级版本,从Android 5.0开始(API Level 21)就可以进行使用了。就调用而言,Camera2拥有对照相机更多更深的控制权,一些Camera1不支持或支持起来比较麻烦的功能Camera2都能支持。

对比Camera 1 新增特性:

  1. 在开启相机之前检查相机信息
    如果你是使用Camera1,那么你要获取相机的相关信息,例如:检查闪光灯是否可用,你需要先开启相机,然后通过相机对象获取相关信息。但在Camera2上提供了CameraCharacteristics对象,即可获取相机相关信息。
CameraManager.getCameraCharacteristics(@NonNull String cameraId)
  1. 在不开启预览的情况下拍照
    如果是在Camera1上,只有开启预览才能进行拍照,而Camera2可直接进行拍照功能。
CameraCaptureSession.capture(@NonNull CaptureRequest request,
            @Nullable CaptureCallback listener, @Nullable Handler handler)
  1. 一次拍摄多张不同格式和尺寸的图片
    在Camera1上一次只能拍照一张,更别提不同尺寸和格式,而Camera2支持多张拍照和连拍功能。
// 多张拍照
CameraCaptureSession .captureBurst(@NonNull List<CaptureRequest> requests,
            @Nullable CaptureCallback listener, @Nullable Handler handler)
// 连拍
CameraCaptureSession.setRepeatingRequest(@NonNull CaptureRequest request,
            @Nullable CaptureCallback listener, @Nullable Handler handler)
  1. 控制曝光时间
    在暗环境下拍照的时候,如果能够适当延长曝光时间,就可以让图像画面的亮度得到提高。在 Camera2 上,你可以在规定的曝光时长范围内配置拍照的曝光时间,从而实现拍摄长曝光图片,你甚至可以延长每一帧预览画面的曝光时间让整个预览画面在暗环境下也能保证一定的亮度。而Camera1不支持。
CaptureRequest.set(@NonNull Key<T> key, T value)
// 曝光时间
CaptureRequest.Builder.set(CaptureRequest.SENSOR_EXPOSURE_TIME, long value);
// 感光度
CaptureRequest.Builder.set(CaptureRequest.SENSOR_SENSITIVITY, int value);
  1. 灵活的 3A 控制 / 3A 模式和状态转换
    3A(AF、AE、AWB)的控制在 Camera2 上得到了最大化的放权,应用层可以根据业务需求灵活配置 3A 流程并且实时获取 3A 状态,而 Camera1 在 3A 的控制和监控方面提供的接口则要少了很多。例如你可以在拍照前进行 AE 操作,并且监听本这次拍照是否点亮闪光灯。

API详解:

Camera2 的 API 模型被设计成一个 Pipeline(管道),它按顺序处理每一帧的请求并返回请求结果给客户端。下图为官方Pipeline 的工作流程:
图片描述

在拍摄之前创建用于从Pipeline(管道,可以简单理解为获取图片数据的通道)中获取图片的CaptureRequest,CaptureRequest中存在用于获取图片数据的Surface。

一个新的 CaptureRequest 会被放入一个被称作 Pending Request Queue 的队列中等待被执行,当 In-Flight Capture Queue 队列空闲的时候就会从 Pending Request Queue 获取若干个待处理的 CaptureRequest,并且根据每一个 CaptureRequest 的配置进行 Capture 操作。最后从不同尺寸的 Surface 中获取图片数据并且还会得到一个包含了很多与本次拍照相关的信息的 CaptureResult,流程结束。

Camera2拍照流程和重要API如下图:

图片描述

补充一: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 数据的后处理等。
CameraCharacteristics.get(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL);

补充二:Capture

在 Camera2 里面所有的相机操作和参数配置都被抽象成 Capture(捕获),所以不要简单的把 Capture 直接理解成是拍照,因为 Capture 操作可能仅仅是为了让预览画面更清晰而进行对焦而已。

Capture 从执行方式上又被细分为【单次模式】、【多次模式】和【重复模式】三种

  • 单次模式(One-shot):指的是只执行一次的 Capture 操作,例如设置闪光灯模式、对焦模式和拍一张照片等。多个一次性模式的 Capture 会进入队列按顺序执行。
  • 多次模式(Burst):指的是连续多次执行指定的 Capture 操作,该模式和多次执行单次模式的最大区别是连续多次 Capture 期间不允许插入其他任何 Capture 操作,例如连续拍摄 100 张照片,在拍摄这 100 张照片期间任何新的 Capture 请求都会排队等待,直到拍完 100 张照片。多组多次模式的 Capture 会进入队列按顺序执行。
  • 重复模式(Repeating):指的是不断重复执行指定的 Capture 操作,当有其他模式的 Capture 提交时会暂停该模式,转而执行其他被模式的 Capture,当其他模式的 Capture 执行完毕后又会自动恢复继续执行该模式的 Capture,例如显示预览画面就是不断 Capture 获取每一帧画面。该模式的 Capture 是全局唯一的,也就是新提交的重复模式 Capture 会覆盖旧的重复模式 Capture。

补充三:CaptureResult

CaptureResult 是每一次 Capture 操作的结果,里面包括了很多状态信息,包括闪光灯状态、对焦状态、时间戳等等。例如你可以在拍照完成的时候,通过 CaptureResult 获取本次拍照时的对焦状态和时间戳。需要注意的是,CaptureResult 并不包含任何图像数据,前面我们在介绍 Surface 的时候说了,图像数据都是从 Surface 获取的。

实例应用:

先看应用实例图:
图片描述

一. 要实现类似这样的一个横屏照相机,首先要绘制预览图片的承接页,相机的预览是一个频繁的操作并且会产生实时数据,从性能角度考虑,这里可以采用SurfaceView实现照片预览。并且SurfaceView内置ViewOutlineProvider,支持轮廓裁剪,例如实现上图圆角效果:

// SurfaceView圆角效果
private void setSurfaceViewCorner(final float radius) {

    SurfaceView.setOutlineProvider(new ViewOutlineProvider() {
        @Override
        public void getOutline(View view, Outline outline) {
            Rect rect = new Rect();
            view.getGlobalVisibleRect(rect);
            int leftMargin = 0;
            int topMargin = 0;
            Rect selfRect = new Rect(leftMargin, topMargin,
                    rect.right - rect.left - leftMargin,
                    rect.bottom - rect.top - topMargin);
            outline.setRoundRect(selfRect, radius);
        }
    });
    SurfaceView.setClipToOutline(true);
}

二. 其次预览照片要设置一个可操作Handler,这里的Handler可以使用子线程Handler完成,例如从HandlerThread中取Handler。

Handler handler = new Handler(new HandlerThread("HandlerCamera2").getLooper());

除此之外预览要设置CaptureRequest预览模式:

CaptureRequest.Builder captureRequestBuilder = CameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW);

三. 关于ImageReader:
ImageReader中包含Surface,拍照数据通过Surface发送给ImageReader,类似一个队列,需要通过acquireLatestImage()或者acquireNextImage()方法取出Image。

Image类允许应用通过一个或多个ByteBuffers直接访问Image的像素数据, ByteBuffer包含在Image.Plane类中,同时包含了这些像素数据的配置信息。因为是作为提供raw数据使用的,所以Image不像Bitmap类可以直接填充到UI上使用。

ImageReader部分重要API:

  • acquireLatestImage() - 从ImageReader队列中获取最新的一帧Image,并且将老的Image丢弃,如果没有新的可用的Image则返回null。
    此操作将会从ImageReader中获取所有可获取到的Images,并且关闭除了最新的Image之外的Image。此功能大多数情况下比acquireNextImage更推荐使用,更加适用于视频实时处理。
    需要注意的是maxImages应该至少为2,因为丢弃除了最新的之外的所有帧需要至少两帧。换句话说,(maxImages - currentAcquiredImages < 2)的情况下,丢帧将会不正常。
  • acquireNextImage() - 从ImageReader的队列中获取下一帧Image,如果没有新的则返回null。
    Android推荐我们使用acquireLatestImage来代替使用此方法,因为它会自动帮我们close掉旧的Image,并且能让效率比较差的情况下能获取到最新的Image。acquireNextImage更推荐在批处理或者后台程序中使用,不恰当的使用本方法将会导致得到的images出现不断增长的延迟。
  • close() - 释放所有跟此ImageReader关联的资源。调用此方法后,ImageReader不会再被使用,再调用它的方法或者调用被acquireLatestImage或acquireNextImage返回的Image会抛出IllegalStateException,尝试读取之前Plane#getBuffer返回的ByteBuffers将会导致不可预测的行为。
  • newInstance(int width, int height, int format, int maxImages) - 创建新的reader以获取期望的size和format的Images。maxImages决定了ImageReader能同步返回的最大的Image的数量,申请越多的buffers会耗费越多的内存空间,使用合适的数量很重要。
  • format :reader生产的Image的格式,必须是ImageFormat或PixelFormat中的常量,并不是所有的formats都会被支持,比如ImageFormat.NV21就是不支持的,Android一般都会支持ImageFormat_420_888。那很多人可能会想,不支持你写这儿干嘛?当然这里只是说Camera不支持格式直出,并不是其他地方不认识这种格式,比如YuvImage就支持ImageFormat.NV21。
  • maxImages:缓存的最大帧数,必须大于0。
private void initImageReader() {
        if (mImageReader == null) {
            try {
                CameraCharacteristics cameraCharacteristics = mCameraManager.getCameraCharacteristics(mCameraId);
                Size size = getImgSize(cameraCharacteristics);
                int width = size.getWidth();
                int height = size.getHeight();
                Logger.info("Camera2Executor", "realWidth = " + width, false);
                Logger.info("Camera2Executor", "realHeight = " + height, false);
                // 设置图片大小
                mImageReader = ImageReader.newInstance(width, height, mImageFormat, 2);
                ImageReader.OnImageAvailableListener listener = new ImageReader.OnImageAvailableListener() {
                    @Override
                    public void onImageAvailable(ImageReader reader) {
                        // 获取照片数据
                        Image image = reader.acquireLatestImage();
                        ByteBuffer buffer = image.getPlanes()[0].getBuffer();
                        byte[] bytes = new byte[buffer.remaining()];
                        buffer.get(bytes);

                        // 字节码转文件
                        if (TextUtils.isEmpty(filePath)) {
                            filePath = mContext.getExternalCacheDir().getAbsolutePath()
                                    + File.separator + System.currentTimeMillis() + ".jpg";
                        } else {
                            String temp = filePath.toLowerCase(Locale.ROOT);
                            if (!(temp.endsWith(".jpeg")
                                    || temp.endsWith(".jpg")
                                    || temp.endsWith(".png")
                                    || temp.endsWith(".webp"))) {
                                filePath = mContext.getExternalCacheDir().getAbsolutePath()
                                        + File.separator + System.currentTimeMillis() + ".jpg";
                            }
                        }
                        Logger.info("Camera2Executor", "filePath = " + filePath, false);
                        File file = new File(filePath);
                        FileOutputStream fos = null;
                        try {
                            fos = new FileOutputStream(file);
                            fos.write(bytes);
                            fos.flush();
                        } catch (IOException e) {
                            e.printStackTrace();
                        } finally {
                            if (fos != null) {
                                try {
                                    fos.close();
                                } catch (IOException e) {
                                    e.printStackTrace();
                                }
                            }
                        }

//                        // 字节码转Bitmap
//                        Bitmap bitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.length);

                        if (mCamera2Listener != null && file.exists()) {
                            mCamera2Listener.onCameraSuccess(file);
                        }

                        image.close();
                    }
                };
                mImageReader.setOnImageAvailableListener(listener, uiHandler);
            } catch (CameraAccessException e) {
                // TOD
            }
        }
    }

四. 关于摄像头方向
CameraId:
CameraCharacteristics.LENS_FACING_FRONT 通常表示后置摄像头;
CameraCharacteristics.LENS_FACING_BACK 通常表示前置摄像头。

五. 关于照片方向

    /**
     * 获取照片方向角度
     */
    private int getRotation(CameraCharacteristics cameraCharacteristics) {
        int displayRotation = ((Activity) mContext).getWindowManager().getDefaultDisplay().getRotation();
        switch (displayRotation) {
            case Surface.ROTATION_0:
                displayRotation = 90;
                break;
            case Surface.ROTATION_90:
                displayRotation = 0;
                break;
            case Surface.ROTATION_180:
                displayRotation = 270;
                break;
            case Surface.ROTATION_270:
                displayRotation = 180;
                break;
        }
        int sensorOrientation = cameraCharacteristics.get(CameraCharacteristics.SENSOR_ORIENTATION);
        return (displayRotation + sensorOrientation + 270) % 360;
    }

六. 关于照片时照片大小计算

    /**
     * 获取图片大小,取最接近当前屏幕的宽高比的尺寸
     */
    private Size getImgSize(CameraCharacteristics cameraCharacteristics) {
        // 计算当前屏幕的宽高比
        int screenWidth = ScreenUtil.getScreenW(mContext);
        int screenHeight = ScreenUtil.getScreenH(mContext);
        float screenRatio = screenWidth * 100f / screenHeight;
        Logger.info("Camera2Executor", "screenWidth = " + screenWidth, false);
        Logger.info("Camera2Executor", "screenHeight = " + screenHeight, false);
        Logger.info("Camera2Executor", "screenRatio = " + screenRatio, false);
        // 获取相机支持的所有尺寸
        StreamConfigurationMap map = cameraCharacteristics.get(
                CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP);
        Size[] sizeList = map.getOutputSizes(mImageFormat);
        // 对比尺寸
        Map<Float, Size> tempMap = new HashMap<>();
        List<Float> tempList = new ArrayList<>();
        for (Size item : sizeList) {
            int width = item.getWidth();
            int height = item.getHeight();
            float ratio = width * 100f / height;
            float ratioDiff = Math.abs(ratio - screenRatio);
            Logger.info("Camera2Executor", "width = " + width, false);
            Logger.info("Camera2Executor", "height = " + height, false);
            Logger.info("Camera2Executor", "ratioDiff = " + ratioDiff, false);
            tempMap.put(ratioDiff, item);
            tempList.add(ratioDiff);
        }
        Collections.sort(tempList);
        // 取出最合适尺寸
        return tempMap.get(tempList.get(0));
    }

七. 常用功能方法补充

  1. 点击聚焦
    根据用户在 view 上的触摸点,映射到相机坐标系中对应的点,然后通过 CaptureRequest.Builder 的 CaptureRequest.CONTROL_AF_REGIONS 字段设置聚焦的区域。
CaptureRequest.Builder.set(CaptureRequest.CONTROL_AF_REGIONS, new MeteringRectangle[] {new MeteringRectangle(Rect rect, int meteringWeight)});
  1. 双指放大缩小
    通过 View 的点击事件,获取到双指之间的间距,并通过 CaptureRequest.Builder 的 CaptureRequest.SCALER_CROP_REGION 字段设置缩放。
CaptureRequest.Builder.set(CaptureRequest.SCALER_CROP_REGION, CaptureRequest.Builder.get(CaptureRequest.SCALER_CROP_REGION));

补充说明:如果SurfaceView预览时有拉伸,可以设置SurfaceView的渲染宽高来达到适配效果:

public void setFixedSize(int width, int height)

完整代码传送门ZRunCamera2

最后整体流程图:
图片描述

推荐阅读:

Camera2 API 采集视频并SurfaceView、TextureView 预览
Camera SurfaceView 预览拍照
Camera2教程之打开相机、开启预览、实现PreviewCallback、拍照
Android Camera、Camera2详解
自定义Camera系列之:SurfaceView + Camera2
Camera2 教程 · 第一章 · 概览
ImageReader获得预览数据
彻底搞懂摄影中的感光度、曝光指数和增益

感谢您使用伴职平台,如有侵权,请投诉删除!

全部评价

最新
查看更多评论 加载

猜你喜欢

换一批