相关文章推荐
无邪的黄花菜  ·  Disabling the MD5 ...·  8 月前    · 
忧郁的大葱  ·  Camunda-业务任务 - 掘金·  1 年前    · 
瘦瘦的打火机  ·  PHP ...·  1 年前    · 
1,806

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

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

CameraKit - 相机能力接入准备

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

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获取

    通过原生Camera2获取

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

    CameraKit的实例约束条件

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

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

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

    笔者有一台华为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,
    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还是把焦点放在鸿蒙的更新上,华为社区关于该库的问题看上去也没有得到很好的解决。所以以上提到的那些问题可能不会得到很好的解决了。这篇文章也可以当是一篇冷知识看看吧。

    Cy13er
    粉丝