Android视觉应用性能优化

计算机视觉在移动端的应用,典型的如手势识别,人脸识别,流程都差不多,都是利用移动端的相机采集数据,丢给算法层,根据识别的结果来做一些业务,中间可能还要做一些图形的渲染。

本文主要讨论这中间涉及到的一些问题以及优化的思路,算是对过往工作的总结吧。大致问题有以下五点:

一,相机采集数据的格式是NV21,而算法层所需的格式是RGB,这中间需要转换,对于每秒30帧,每帧1920*1080的图像转换的耗时还是不可忽视的。

二,算法层耗时在20ms-80ms不定,这样平均下来算法输出帧率大概在15~20,而相机的原始帧率是30,这会造成明显的延时,所以如何提升算法的输出帧率是个问题。

三,整个流程是异步的,相机数据是异步的,RGB转换是异步的,算法是异步的,渲染也是异步的,如何设计整个系统让数据流稳定地跑起来,要注意线程同步的问题。

四,设计到大量的数据读写,要尽可能减少数据拷贝,尽可能复用对象

五,图形渲染,除了相机每秒30帧的预览数据,还有额外关于人脸和手的渲染效果

先说说第一点,RGB转换用Java实现肯定性能是有问题的,后来改用C++实现了,对于1280*720的帧转换平均要10ms,1920*1080可能要20ms左右了,Neon实现可能效果会好不少,不过没试过。我这里是直接采用GPU来做转换的,可以参考我的Github项目Android-Camera,其中对GPU的RGB转换的几种方案做了实验和对比,性能最好的是采用PBuffer和PBO的方式,对于1280*720转换降到了2ms,1920*1280在6ms左右,这样差不多可以接受了,而且也没有占用CPU。这里转换生成的RGB可以丢给算法层,也可以保存为本地图像。

再来说说第二点,算法层耗时较多会拖慢输出帧率,通常是在视野内没有聚焦时的全量扫描会非常耗时,当跟踪到手或脸之后就会快多了。除了算法层优化之外,移动端也可以进一步优化以提高输出帧率。常规流程是相机的帧数据来之后,先判断当前算法层是否Busy,如果Busy则丢弃当前相机帧,如果Free则将相机帧做RGB转换,然后丢给算法层。这样逻辑上实现相对简单,但是仔细分析后会发现算法层其实有一部分时间是白白浪费的,极端的情况是相机上一帧刚过,算法层就返回了,结果要等到下一帧数据来时才能开始下一次计算,这就白白浪费了30ms。解决的方式是采用双缓冲,开辟两个buffer,一个buffer用于算法层计算,另一个用于相机帧写入,当算法层返回时,切换到另一个buffer继续算,而无需额外等待,这样吞吐量就提升了。效果还是很明显的,基本能到25帧左右。关键代码如下:

public class DoubleBuffer {

    private FrameBuffer[] mBuffers;

    /**
     * 另一个buffer是否准备好了
     */
    private volatile boolean mReady;

    /**
     * 当前正在占用的buffer
     */
    private volatile int mActive = 0;

    public DoubleBuffer() {
        mBuffers = new FrameBuffer[2];
        mBuffers[0] = new FrameBuffer();
        mBuffers[1] = new FrameBuffer();
    }

    public FrameBuffer get() {
        synchronized (mBuffers) {
            while (!mReady) {
                try {
                    mBuffers.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            mActive = 1 - mActive;
            mReady = false;
            return mBuffers[mActive];
        }
    }

    public void put(final ByteBuffer yuv, final ByteBuffer rgba) {
        synchronized (mBuffers) {
            int idx = 1 - mActive;
            mBuffers[idx].put(yuv, rgba);
            mReady = true;
            mBuffers.notifyAll();
        }
    }
}

再来说说第三点,首先相机帧数据过来要先丢给GPU做RGB转换,GPU在单独一个线程将NV21渲染成纹理,然后读出RGB像素数据丢到Double Buffer中,这个是不能做耗时操作阻塞的,如果有截图的需要,可以在这里保存RGB图像到文件,注意另开一个线程。而算法层由于耗时较多,所以也要单独放在一个线程,此外图像渲染也是单独一个线程。算法层所在的线程要不断地从Double Buffer中取数据运算,识别出手和人脸,然后给结果保存下来,跟随接下来的相机帧一起渲染,如给手和人脸框出来,这个理论上是有延迟的,尤其当手移动很快时,框会跟不上手的移动。不过如果算法层的输出帧率能到25以上,问题就不大了。

再来说说第四点,要减少数据拷贝,尽可能复用对象避免频繁GC。减少数据拷贝尤其是在Java层和算法层之间传递数据时。图像相关的操作都要小心翼翼,一方面要尽可能复用缓冲,同时要小心内存泄露,因为一帧的缓冲就是8M,

最后说说第五点,要实现相同的效果有几种方案,可以将相机预览输出到TextureView,然后在canvas上画人脸和手框,但是性能堪忧。也可以做几层Surface View,底层是相机预览,上层Surface是做人脸和手框,还可以再叠加一层Surface做其它动画。不过这样实在是太费事了,麻烦不说,每个Surface渲染都要单独开线程,并且如果需要同时录制相机预览和人脸框就没法做,因为是分开在两个Surface中渲染的,录制通常是围绕一个Surface的数据进行的。所以比较好的方案是采用一层Surface,然后所有东西都绘制在一起,只用一个渲染线程,录制视频也方便。另外还要说说GLSurfaceView和SurfaceView,这两个其实是一回事,GLSurfaceView是继承自SurfaceView,只不过内部自动开了个渲染线程,并且自己管理Egl上下文。而如果我们用SurfaceView的话这些都得自己来处理了,虽然麻烦些,但是灵活性很好。比如我们可以给EglContext共享给另一个渲染线程,这样就可以共享纹理,而无需重复渲染,但是GLSurface中EglContext是没有开放出来的。

你可能感兴趣的:(Android,性能优化)