用华为CameraKit实现预览和拍照

2,716 阅读8分钟

前言

前面研究了一下如何在Android手机上获取超广角镜头一些获取您的Android设备超广角能力的思路 - 掘金 (juejin.cn)。发现HUAWEI官方有推出过一个相机库CameraKit,就想着自己接入一下看看效果,顺便记录一些遇到的坑。

流程

使用Gradle集成比较常规,看文档即可:

CameraKit - 相机能力接入准备

官方提供的集成流程如下图: image.png

CameraKit提供了一个Mode类作为一次拍照流程的相关抽象,可理解为一个Session。

CameraKit的生命周期:

  • 模式创建:CameraKit提供了多种相机的模式,譬如:普通拍照、人像、夜景等,当然还有录像相关的。 详情可参考文档:Mode.Type
  • 模式配置:主要是配置预览分辨率、拍照分辨率等,还有关于在该模式下的一些操作事件的回调、数据的回调。
  • 基于模式的操作:比较好理解的是利用Mode类进行预览、拍照、缩放等。
  • 操作回调:每当触发一个操作后,会通过在模式配置下注册的回调中回调相关事件或数据。
  • 模式释放:不需要时释放资源。

接入

在使用CameraKit时,一切的前提是需要实例化出CameraKit对象,它是一个饿汉式的单例,在实例化前会判断一些约束条件,符合条件后才会创建。

CameraKit cameraKit = CameraKit.getInstance(getApplicationContext());

模型创建

在预览的视图准备好之后,就可以开始创建模式了,譬如在TextureView#onSurfaceTextureAvailable后。在创建前还需要新建一个HandlerThread作为整个相机运作的线程

private final ModeStateCallback mModeStateCallback = new ModeStateCallback() {
    @Override
    public void onCreated(Mode mode) {
        super.onCreated(mode);
        mMode = mode;
        configMode();  // Mode创建成功,可以开始进行模式配置
    }

    @Override
    public void onCreateFailed(String cameraId, int modeType, int errorCode) {
        super.onCreateFailed(cameraId, modeType, errorCode);
    }

    @Override
    public void onConfigured(Mode mode) {
        super.onConfigured(mode);
        mMode.startPreview();  // Mode配置成功,可以开始预览
    }

    @Override
    public void onConfigureFailed(Mode mode, int errorCode) {
        super.onConfigureFailed(mode, errorCode);
    }

    @Override
    public void onReleased(Mode mode) {
        super.onReleased(mode);
    }

    @Override
    public void onFatalError(Mode mode, int errorCode) {
        super.onFatalError(mode, errorCode);
    }
};

cameraKit.createMode(CameraInfo.FacingType.CAMERA_FACING_BACK,
        Mode.Type.NORMAL_MODE, mModeStateCallback, mHandler);
  • CameraKit中有对于手机物理摄像头进行抽象,在应用层只会提供前置/后置两个枚举。这里使用后置摄像头CameraInfo.FacingType.CAMERA_FACING_BACK
  • Mode.Type.NORMAL_MODE为普通拍照模式,如果有拍摄人像、夜景等其他需求,可对应传入。
  • ModeStateCallback用于监听Mode对象的事件。
  • 最后还需要一个属于HandlerThread的Handler,用于消息分发。

模式配置

从上述代码中ModeStateCallback#onCreated的回调可以看到,在成功创建模式后就可以开始配置了。

比较重要的是预览分辨率和拍照分辨率,可通过以下代码获取设备支持的

// 预览分辨率
List<Size> supportedPreviewSizes = mMode.getModeCharacteristics()
                                        .getSupportedPreviewSizes(SurfaceTexture.class);

// 拍照分辨率        
List<Size> supportedCaptureSizes = mMode.getModeCharacteristics()
                                        .getSupportedCaptureSizes(ImageFormat.JPEG);

因为用的是TextureView,所以传入SurfaceTexture.class。预览分辨率还需要设置回TextureView中保证预览画面正常。ps:分辨率的筛选逻辑比较常规就不多赘述了,这里选一个最大的3:4比例

textureView.getSurfaceTexture()
           .setDefaultBufferSize(mPreviewSize.getWidth(), mPreviewSize.getHeight());

ps:mMode.getModeCharacteristics()可以获取到在该模式下一些支持的参数,除上述的分辨率外还有支持的对焦类型、缩放范围等。可参考:ModeCharacteristics

mMode.getModeConfigBuilder()
     .addPreviewSurface(new Surface(textureView.getSurfaceTexture()))
     .addCaptureImage(pictureSize, ImageFormat.JPEG);
mMode.getModeConfigBuilder().setDataCallback(mActionDataCallback, mHandler);
mMode.getModeConfigBuilder().setStateCallback(mActionStateCallback, mHandler);
mMode.configure();

配置时还需要传入ActionStateCallbackActionDataCallback对象,用于在该模式下的一些操作事件的回调、数据的回调

private final ActionStateCallback mActionStateCallback = new ActionStateCallback() {
    @Override
    public void onPreview(Mode mode, int state, @Nullable PreviewResult result) {
        super.onPreview(mode, state, result);
        // 预览事件回调
    }

    @Override
    public void onTakePicture(Mode mode, int state, @Nullable TakePictureResult result) {
        super.onTakePicture(mode, state, result);
        // 拍照事件回调
    }

    @Override
    public void onFocus(Mode mode, int state, @Nullable FocusResult result) {
        super.onFocus(mode, state, result);
        // 对焦事件回调
    }
};

private final ActionDataCallback mActionDataCallback = new ActionDataCallback() {
    @Override
    public void onImageAvailable(Mode mode, int type, Image image) {
        super.onImageAvailable(mode, type, image);
        // 拍照数据回调
    }

    @Override
    public void onThumbnailAvailable(Mode mode, int type, android.util.Size size, byte[] data) {
        super.onThumbnailAvailable(mode, type, size, data);
    }
};

开始预览

ModeStateCallback#onConfigured回调后即可调用Mode#startPreview开启预览。

// ModeStateCallback
@Override
public void onConfigured(Mode mode) {
    super.onConfigured(mode);
    mMode.startPreview();  // Mode配置成功,可以开始预览
}

这时您的界面上应该就能看到预览画面了。

拍照

Mode#takePicture触发拍照

mMode.takePicture();

ActionStateCallback#onTakePicture会回调拍照相关的事件,包括错误事件

// ActionStateCallback
@Override
public void onTakePicture(Mode mode, int state, @Nullable TakePictureResult result) {
    super.onTakePicture(mode, state, result);
    if (state == TakePictureResult.State.CAPTURE_COMPLETED) {
        // 拍照完成
    } else if (state == TakePictureResult.State.ERROR_CAPTURE_NOT_READY
            || state == TakePictureResult.State.ERROR_FILE_IO
            || state == TakePictureResult.State.ERROR_UNKNOWN
            || state == TakePictureResult.State.ERROR_UNSUPPORTED_OPERATION) {
        // 拍照出错
    }
}

可参考:ActionStateCallback.TakePictureResult.State

拍照成功后在ActionDataCallback#onImageAvailable回调原图的Image对象,数据格式为jpg

// ActionDataCallback
@Override
public void onImageAvailable(Mode mode, int type, Image image) {
    super.onImageAvailable(mode, type, image);
    if (type == Type.TAKE_PICTURE) {
        ByteBuffer buffer = image.getPlanes()[0].getBuffer();
        byte[] bytes = new byte[buffer.remaining()];
        buffer.get(bytes);
        // 转成Bitmap?
        image.close();
    }
}

这样就会拿到jpg的byte数组了。

资源释放

在结束时,需要释放相关资源,当然还包括创建的HandlerThread,避免内存泄漏

if (mMode != null) {
    mMode.release();
}

超广角能力

由于笔者最初是想研究超广角的,所以也来看一下

float[] zooms = mMode.getModeCharacteristics().getSupportedZoom();
mMode.setZoom(zooms[0]);

由于CameraKit已经帮我们抽象了物理摄像头,对于后置摄像头当然也包括那颗超广角摄像头。使用以上代码可以获取到当前模式下所支持的缩放范围,一般是一个长度为2的数组。在华为P40上zooms[0] = 0.6f。设置后即可获得超广角的预览。

一些疑难杂症

支持的分辨率较少

在华为P40上,通过CameraKit获取支持的拍照分辨率极少

  • 通过CameraKit获取

    image.png

  • 通过原生Camera2获取

    image.png

  • 即使是超广角镜头,通过原生Camera2获取

    image.png

CameraKit的实例约束条件

这个其实不太算是问题,只是限制罢了。CameraKit实例化前会判断

  • app是否已经获取了拍照权限(ps:个人认为这个这个判断应该交给调用方判断的。。。)

  • 设备是否支持,支持的范围如下图:

    image.png 笔者有一台华为MatePad11,是高通芯片的,实测发现并不支持,所以该库支持的范围还是比较窄的。

拍照输出的时间很慢

一般使用Camera2拍照平均在500ms可以输出,使用CameraKit最快也要2s。如果使用一些更为专业的功能可能会更长,这个没有细测。 在HUAWEI的社区中也有人提问:CameraKit中拍照速度慢的情况下要5、6秒,太慢了,请问如何优化-华为开发者论坛

笔者的猜测是,从CameraKit导入的一些类来看,其依赖的还是Camera2。推测是在输出到调用方之前,CameraKit会调用一些系统的服务对图像进行处理,就比如超广角的输出是处理过畸变的。还有就是上述说的分辨率支持极少,所能选用的3:4分辨率已经到4096 * 3072,导致这些处理比较耗时。

无法获取预览帧

接入时发现该库并没有很好的提供获取预览帧的方法,只能通过在配置Mode时添加多一个Surface。这里使用ImageReader实现,具体可参考Camera2的做法,大同小异。

previewImageReader = ImageReader.newInstance(
        previewSize.getWidth(),
        previewSize.getHeight(),
        ImageFormat.YUV_420_888,
        2);
previewImageReader.setOnImageAvailableListener(this, mHandler);

mMode.getModeConfigBuilder()
     .addPreviewSurface(new Surface(textureView.getSurfaceTexture()))
     .addPreviewSurface(previewImageReader.getSurface())
     .addCaptureImage(pictureSize, ImageFormat.JPEG);
mMode.getModeConfigBuilder().setDataCallback(mActionDataCallback, mHandler);
mMode.getModeConfigBuilder().setStateCallback(mActionStateCallback, mHandler);
mMode.configure();

这里需要使用YUV_420_888,提高输出效率。还需要注意的是输出的Image转成byte数组的问题。

还有一点,根据社区的一些反馈,并不是所有设备都支持这样同时注册两个预览流,可通过以下方式获取最大的支持数

mMode.getModeCharacteristics().getMaxPreviewSurfaceNumber()

该方法虽然可以稳定获取到预览帧,但是随之而来的是加大了拍照输出的时间。严重的可达到5、6s以上。

另一种思路

这个又有另外一个思路去解决:上述代码只注册一个ImageReader用于获取YUV_420_888的预览帧,再通过GLSurfaceView绘制到视图上,同时将Image转成byte数组作为数据层的回调。具体的实现这里不细说了,推荐一个OPPO的Demo,里面有YUV_420_888通过OpenGL绘制的逻辑,可以参考一下:oppo/CameraUnit

这里需要注意的是:

  • ImageReader#OnImageAvailableListener输出和GLSurfaceView.Renderer#onDrawFrame的绘制在两个线程,所以需要保证同步。
  • 由于Image转成byte数组的过程可能存在耗时,这一块主要来源于大内存的申请和gc,可采用全局变量避免频繁的内存申请。但由于采用了全局变量,但又不能因为这个转换导致绘制的掉帧情况,所以需要一些原子性的变量加以辅助,适当做一些丢帧操作。

以上只是笔者的设想,里面也有更好的优化空间。

最后

以上就是笔者关于华为CameraKit的研究记录。其实都2023年了,在一台鸿蒙手机上做一些Android开发确实有些不太靠谱。在官方文档中最近一次更新是在2021年,目前HUAWEI还是把焦点放在鸿蒙的更新上,华为社区关于该库的问题看上去也没有得到很好的解决。所以以上提到的那些问题可能不会得到很好的解决了。这篇文章也可以当是一篇冷知识看看吧。

image.png