阅读(1348) (13)

基于Android相机预览的CV应用程序中使用OpenCL

2017-08-26 11:03:17 更新

本指南旨在帮助您在基于Android相机预览的CV应用程序中使用OpenCL™。它是为基于Eclipse的ADT工具(现在已不再使用Google)编写的,但可以轻松地使用Android Studio进行复制。

本教程假设您已经安装并配置了以下内容:

  • JDK
  • Android SDK和NDK
  • 具有ADT和CDT插件的Eclipse IDE

它还假定您熟悉Android Java和JNI编程基础知识。如果您需要上述任何方面的帮助,您可以参考我们的Android开发入门指南。

本教程还假定您具有启用OpenCL的Android操作设备。

相关的源代码位于OpenCV / samples / android / tutorial-4-opencl目录下的OpenCV示例中。

前言

通过OpenCL 使用GPGPU进行应用程序性能提升是现在非常现代的趋势。一些CV算法(例如图像过滤)在GPU上比在CPU上运行得更快。最近在Android操作系统上成为可能。

用于Android操作设备的最流行的CV应用场景是以预览模式启动相机,将一些CV算法应用于每个帧,并显示该CV算法修改的预览帧。

让我们考虑一下在这种情况下如何使用OpenCL。特别是让我们尝试两种方法:直接调用OpenCL API和最近推出的OpenCV T-API(也称为透明API) - 一些OpenCV算法的隐式OpenCL加速。

应用结构

启动Android API 11级(Android 3.0)Camera API允许使用OpenGL纹理作为预览框架的目标。Android API第21级带来了一个新的Camera2 API,可以更好地控制相机设置和使用模式,特别是允许预览框架和OpenGL纹理的多个目标。

在OpenGL纹理中使用预览框架是一个非常好的使用OpenCL,因为有一个OpenGL-OpenCL互操作性API(cl_khr_gl_sharing),允许与OpenCL功能共享OpenGL纹理数据而不复制(当然有一些限制)。

我们为我们的应用程序创建一个基础,只需配置Android相机将预览帧发送到OpenGL纹理,并在显示屏上显示这些帧,而不进行任何处理。

Activity为此目的的最小类似如下:

public class Tutorial4Activity extends Activity {
    private MyGLSurfaceView mView;
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        requestWindowFeature(Window.FEATURE_NO_TITLE);
        getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN,
                WindowManager.LayoutParams.FLAG_FULLSCREEN);
        getWindow().setFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON,
                WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
        setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);
        mView = new MyGLSurfaceView(this);
        setContentView(mView);
    }
    @Override
    protected void onPause() {
        mView.onPause();
        super.onPause();
    }
    @Override
    protected void onResume() {
        super.onResume();
        mView.onResume();
    }
}

和一个最小的View类:

public class MyGLSurfaceView extends GLSurfaceView {
    MyGLRendererBase mRenderer;
    public MyGLSurfaceView(Context context) {
        super(context);
        if(android.os.Build.VERSION.SDK_INT >= 21)
            mRenderer = new Camera2Renderer(this);
        else
            mRenderer = new CameraRenderer(this);
        setEGLContextClientVersion(2);
        setRenderer(mRenderer);
        setRenderMode(GLSurfaceView.RENDERMODE_WHEN_DIRTY);
    }
    @Override
    public void surfaceCreated(SurfaceHolder holder) {
        super.surfaceCreated(holder);
    }
    @Override
    public void surfaceDestroyed(SurfaceHolder holder) {
        super.surfaceDestroyed(holder);
    }
    @Override
    public void surfaceChanged(SurfaceHolder holder, int format, int w, int h) {
        super.surfaceChanged(holder, format, w, h);
    }
    @Override
    public void onResume() {
        super.onResume();
        mRenderer.onResume();
    }
    @Override
    public void onPause() {
        mRenderer.onPause();
        super.onPause();
    }
}

注意:我们使用两个渲染器类:一个用于旧版Camera API,另一个用于现代Camera2。

一个最小的Renderer类可以在Java中实现(OpenGL ES 2.0 在Java中可用),但是由于我们将使用OpenCL修改预览纹理,所以我们将OpenGL的东西移动到JNI。这是一个简单的Java包装器我们的JNI的东西:

public class NativeGLRenderer {
    static
    {
        System.loadLibrary("opencv_java3"); // comment this when using OpenCV Manager
        System.loadLibrary("JNIrender");
    }
    public static native int initGL();
    public static native void closeGL();
    public static native void drawFrame();
    public static native void changeSize(int width, int height);
}

由于CameraCamera2API在相机设置和控制方面有很大不同,我们为两个相应的渲染器创建一个基类:

public abstract class MyGLRendererBase implements GLSurfaceView.Renderer, SurfaceTexture.OnFrameAvailableListener {
    protected final String LOGTAG = "MyGLRendererBase";
    protected SurfaceTexture mSTex;
    protected MyGLSurfaceView mView;
    protected boolean mGLInit = false;
    protected boolean mTexUpdate = false;
    MyGLRendererBase(MyGLSurfaceView view) {
        mView = view;
    }
    protected abstract void openCamera();
    protected abstract void closeCamera();
    protected abstract void setCameraPreviewSize(int width, int height);
    public void onResume() {
        Log.i(LOGTAG, "onResume");
    }
    public void onPause() {
        Log.i(LOGTAG, "onPause");
        mGLInit = false;
        mTexUpdate = false;
        closeCamera();
        if(mSTex != null) {
            mSTex.release();
            mSTex = null;
            NativeGLRenderer.closeGL();
        }
    }
    @Override
    public synchronized void onFrameAvailable(SurfaceTexture surfaceTexture) {
        //Log.i(LOGTAG, "onFrameAvailable");
        mTexUpdate = true;
        mView.requestRender();
    }
    @Override
    public void onDrawFrame(GL10 gl) {
        //Log.i(LOGTAG, "onDrawFrame");
        if (!mGLInit)
            return;
        synchronized (this) {
            if (mTexUpdate) {
                mSTex.updateTexImage();
                mTexUpdate = false;
            }
        }
        NativeGLRenderer.drawFrame();
    }
    @Override
    public void onSurfaceChanged(GL10 gl, int surfaceWidth, int surfaceHeight) {
        Log.i(LOGTAG, "onSurfaceChanged("+surfaceWidth+"x"+surfaceHeight+")");
        NativeGLRenderer.changeSize(surfaceWidth, surfaceHeight);
        setCameraPreviewSize(surfaceWidth, surfaceHeight);
    }
    @Override
    public void onSurfaceCreated(GL10 gl, EGLConfig config) {
        Log.i(LOGTAG, "onSurfaceCreated");
        String strGLVersion = GLES20.glGetString(GLES20.GL_VERSION);
        if (strGLVersion != null)
            Log.i(LOGTAG, "OpenGL ES version: " + strGLVersion);
        int hTex = NativeGLRenderer.initGL();
        mSTex = new SurfaceTexture(hTex);
        mSTex.setOnFrameAvailableListener(this);
        openCamera();
        mGLInit = true;
    }
}

您可以看到,继承者Camera和Camera2API应实现以下抽象方法:

protected  abstract  void openCamera();
protected  abstract  void closeCamera();
protected  abstract  void setCameraPreviewSize(int width,int height);

让我们将其实现的细节放在本教程之外,请参考源代码查看。

预览框架修改

OpenGL ES 2.0初始化的细节在这里也很引人瞩目,但重要的一点是,作为相机预览目标的OpeGL纹理应该是类型GL_TEXTURE_EXTERNAL_OES(不是GL_TEXTURE_2D),在内部它保存YUV格式的图像数据。这使得无法通过CL-GL interop(cl_khr_gl_sharing)分享它,并通过C / C ++代码访问其像素数据。为了克服这个限制,我们必须GL_TEXTURE_2D使用FrameBuffer Object(也就是FBO)来执行从这个纹理到另一个常规的OpenGL渲染。

C / C ++代码

之后,我们可以通过C / C ++ 读取(复制)像素数据,glReadPixels()并通过修改后将它们写回纹理glTexSubImage2D()。

直接OpenCL调用

此外,GL_TEXTURE_2D纹理可以与OpenCL共享而不复制,但是我们必须用特殊的方式创建OpenCL上下文:

void initCL()
{
    EGLDisplay mEglDisplay = eglGetCurrentDisplay();
    if (mEglDisplay == EGL_NO_DISPLAY)
        LOGE("initCL: eglGetCurrentDisplay() returned 'EGL_NO_DISPLAY', error = %x", eglGetError());
    EGLContext mEglContext = eglGetCurrentContext();
    if (mEglContext == EGL_NO_CONTEXT)
        LOGE("initCL: eglGetCurrentContext() returned 'EGL_NO_CONTEXT', error = %x", eglGetError());
    cl_context_properties props[] =
    {   CL_GL_CONTEXT_KHR,   (cl_context_properties) mEglContext,
        CL_EGL_DISPLAY_KHR,  (cl_context_properties) mEglDisplay,
        CL_CONTEXT_PLATFORM, 0,
        0 };
    try
    {
        cl::Platform p = cl::Platform::getDefault();
        std::string ext = p.getInfo<CL_PLATFORM_EXTENSIONS>();
        if(ext.find("cl_khr_gl_sharing") == std::string::npos)
            LOGE("Warning: CL-GL sharing isn't supported by PLATFORM");
        props[5] = (cl_context_properties) p();
        theContext = cl::Context(CL_DEVICE_TYPE_GPU, props);
        std::vector<cl::Device> devs = theContext.getInfo<CL_CONTEXT_DEVICES>();
        LOGD("Context returned %d devices, taking the 1st one", devs.size());
        ext = devs[0].getInfo<CL_DEVICE_EXTENSIONS>();
        if(ext.find("cl_khr_gl_sharing") == std::string::npos)
            LOGE("Warning: CL-GL sharing isn't supported by DEVICE");
        theQueue = cl::CommandQueue(theContext, devs[0]);
        // ...
    }
    catch(cl::Error& e)
    {
        LOGE("cl::Error: %s (%d)", e.what(), e.err());
    }
    catch(std::exception& e)
    {
        LOGE("std::exception: %s", e.what());
    }
    catch(...)
    {
        LOGE( "OpenCL info: unknown error while initializing OpenCL stuff" );
    }
    LOGD("initCL completed");
}
注意
要构建此JNI代码,您需要从Khronos网站获取OpenCL 1.2标题,并从您运行应用程序的设备下载libOpenCL.so

然后纹理可以被一个cl::ImageGL对象包裹并通过OpenCL调用进行处理:

cl::ImageGL imgIn (theContext, CL_MEM_READ_ONLY,  GL_TEXTURE_2D, 0, texIn);
cl::ImageGL imgOut(theContext, CL_MEM_WRITE_ONLY, GL_TEXTURE_2D, 0, texOut);
std::vector < cl::Memory > images;
images.push_back(imgIn);
images.push_back(imgOut);
theQueue.enqueueAcquireGLObjects(&images);
theQueue.finish();
cl::Kernel Laplacian = ...
Laplacian.setArg(0, imgIn);
Laplacian.setArg(1, imgOut);
theQueue.finish();
theQueue.enqueueNDRangeKernel(Laplacian, cl::NullRange, cl::NDRange(w, h), cl::NullRange);
theQueue.finish();
theQueue.enqueueReleaseGLObjects(&images);
theQueue.finish();

OpenCV T-API

但是,您可能希望使用OpenCV T-API隐式调用OpenCL 代码,而不是编写OpenCL代码。所有你需要的是将创建的OpenCL上下文传递给OpenCV(via cv::ocl::attachContext()),并以某种方式包装OpenGL纹理cv::UMat。不幸的是在内部UMat保留OpenCL 缓冲区,这不能被OpenGL 纹理或OpenCL 映像包裹- 所以我们必须在这里复制图像数据:

cl::ImageGL imgIn (theContext, CL_MEM_READ_ONLY,  GL_TEXTURE_2D, 0, tex);
std::vector < cl::Memory > images(1, imgIn);
theQueue.enqueueAcquireGLObjects(&images);
theQueue.finish();
cv::UMat uIn, uOut, uTmp;
cv::ocl::convertFromImage(imgIn(), uIn);
theQueue.enqueueReleaseGLObjects(&images);
cv::Laplacian(uIn, uTmp, CV_8U);
cv:multiply(uTmp, 10, uOut);
cv::ocl::finish();
cl::ImageGL imgOut(theContext, CL_MEM_WRITE_ONLY, GL_TEXTURE_2D, 0, tex);
images.clear();
images.push_back(imgOut);
theQueue.enqueueAcquireGLObjects(&images);
cl_mem clBuffer = (cl_mem)uOut.handle(cv::ACCESS_READ);
cl_command_queue q = (cl_command_queue)cv::ocl::Queue::getDefault().ptr();
size_t offset = 0;
size_t origin[3] = { 0, 0, 0 };
size_t region[3] = { w, h, 1 };
CV_Assert(clEnqueueCopyBufferToImage (q, clBuffer, imgOut(), offset, origin, region, 0, NULL, NULL) == CL_SUCCESS);
theQueue.enqueueReleaseGLObjects(&images);
cv::ocl::finish();
  • 注意
    当通过OpenCL图像包装器将修改后的图像放置到原始的OpenGL纹理时,我们必须再创建一个图像数据。
  • 注意
    默认情况下,Android OS的OpenCV版本中禁用了OpenCL支持(T-API)(因此在3.0版本的官方软件包中不存在),但是可以在启用OpenCL / T-API的Android上重建本地OpenCV:use -DWITH_OPENCL=YESoption为CMake。
    
cd opencv-build-android
path/to/cmake.exe -GNinja -DCMAKE_MAKE_PROGRAM="path/to/ninja.exe" -DCMAKE_TOOLCHAIN_FILE=path/to/opencv/platforms/android/android.toolchain.cmake -DANDROID_ABI="armeabi-v7a with NEON" -DCMAKE_BUILD_WITH_INSTALL_RPATH=ON path/to/opencv
path/to/ninja.exe install/strip
要使用您自己修改的内容,libopencv_java3.so您必须保留在APK中,不要使用OpenCV Manager并手动加载System.loadLibrary("opencv_java3")

Performance notes

为了比较的性能,我们测量了同一预览帧修改的FPS(拉普拉斯)由C / C ++代码(调用完成cv::Laplaciancv::Mat呼叫(使用的OpenCL),通过直接的OpenCL 图像用于输入和输出),以及由OpenCV的T-API(呼叫要cv::Laplaciancv::UMat)对索尼的Xperia Z3具备720p摄像头分辨率:

  • C / C ++版本显示3-4 fps
  • 直接OpenCL呼叫显示25-27 fps
  • OpenCV的T-API显示11-13 FPS(由于额外的复制从cl_image到cl_buffer和背面)