基于Android相機(jī)預(yù)覽的CV應(yīng)用程序中使用OpenCL

2018-08-26 11:15 更新

本指南旨在幫助您在基于Android相機(jī)預(yù)覽的CV應(yīng)用程序中使用OpenCL?。它是為基于Eclipse的ADT工具(現(xiàn)在已不再使用Google)編寫的,但可以輕松地使用Android Studio進(jìn)行復(fù)制。

本教程假設(shè)您已經(jīng)安裝并配置了以下內(nèi)容:

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

它還假定您熟悉Android Java和JNI編程基礎(chǔ)知識。如果您需要上述任何方面的幫助,您可以參考我們的Android開發(fā)入門指南。

本教程還假定您具有啟用OpenCL的Android操作設(shè)備。

相關(guān)的源代碼位于OpenCV / samples / android / tutorial-4-opencl目錄下的OpenCV示例中。

前言

通過OpenCL 使用GPGPU進(jìn)行應(yīng)用程序性能提升是現(xiàn)在非?,F(xiàn)代的趨勢。一些CV算法(例如圖像過濾)在GPU上比在CPU上運行得更快。最近在Android操作系統(tǒng)上成為可能。

用于Android操作設(shè)備的最流行的CV應(yīng)用場景是以預(yù)覽模式啟動相機(jī),將一些CV算法應(yīng)用于每個幀,并顯示該CV算法修改的預(yù)覽幀。

讓我們考慮一下在這種情況下如何使用OpenCL。特別是讓我們嘗試兩種方法:直接調(diào)用OpenCL API和最近推出的OpenCV T-API(也稱為透明API) - 一些OpenCV算法的隱式OpenCL加速。

應(yīng)用結(jié)構(gòu)

啟動Android API 11級(Android 3.0)Camera API允許使用OpenGL紋理作為預(yù)覽框架的目標(biāo)。Android API第21級帶來了一個新的Camera2 API,可以更好地控制相機(jī)設(shè)置和使用模式,特別是允許預(yù)覽框架和OpenGL紋理的多個目標(biāo)。

在OpenGL紋理中使用預(yù)覽框架是一個非常好的使用OpenCL,因為有一個OpenGL-OpenCL互操作性API(cl_khr_gl_sharing),允許與OpenCL功能共享OpenGL紋理數(shù)據(jù)而不復(fù)制(當(dāng)然有一些限制)。

我們?yōu)槲覀兊膽?yīng)用程序創(chuàng)建一個基礎(chǔ),只需配置Android相機(jī)將預(yù)覽幀發(fā)送到OpenGL紋理,并在顯示屏上顯示這些幀,而不進(jìn)行任何處理。

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,另一個用于現(xiàn)代Camera2。

一個最小的Renderer類可以在Java中實現(xiàn)(OpenGL ES 2.0 在Java中可用),但是由于我們將使用OpenCL修改預(yù)覽紋理,所以我們將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在相機(jī)設(shè)置和控制方面有很大不同,我們?yōu)閮蓚€相應(yīng)的渲染器創(chuàng)建一個基類:

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應(yīng)實現(xiàn)以下抽象方法:

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

讓我們將其實現(xiàn)的細(xì)節(jié)放在本教程之外,請參考源代碼查看。

預(yù)覽框架修改

OpenGL ES 2.0初始化的細(xì)節(jié)在這里也很引人矚目,但重要的一點是,作為相機(jī)預(yù)覽目標(biāo)的OpeGL紋理應(yīng)該是類型GL_TEXTURE_EXTERNAL_OES(不是GL_TEXTURE_2D),在內(nèi)部它保存YUV格式的圖像數(shù)據(jù)。這使得無法通過CL-GL interop(cl_khr_gl_sharing)分享它,并通過C / C ++代碼訪問其像素數(shù)據(jù)。為了克服這個限制,我們必須GL_TEXTURE_2D使用FrameBuffer Object(也就是FBO)來執(zhí)行從這個紋理到另一個常規(guī)的OpenGL渲染。

C / C ++代碼

之后,我們可以通過C / C ++ 讀?。◤?fù)制)像素數(shù)據(jù),glReadPixels()并通過修改后將它們寫回紋理glTexSubImage2D()。

直接OpenCL調(diào)用

此外,GL_TEXTURE_2D紋理可以與OpenCL共享而不復(fù)制,但是我們必須用特殊的方式創(chuàng)建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");
}
注意
要構(gòu)建此JNI代碼,您需要從Khronos網(wǎng)站獲取OpenCL 1.2標(biāo)題,并從您運行應(yīng)用程序的設(shè)備下載libOpenCL.so。

然后紋理可以被一個cl::ImageGL對象包裹并通過OpenCL調(diào)用進(jìn)行處理:

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隱式調(diào)用OpenCL 代碼,而不是編寫OpenCL代碼。所有你需要的是將創(chuàng)建的OpenCL上下文傳遞給OpenCV(via cv::ocl::attachContext()),并以某種方式包裝OpenGL紋理cv::UMat。不幸的是在內(nèi)部UMat保留OpenCL 緩沖區(qū),這不能被OpenGL 紋理或OpenCL 映像包裹- 所以我們必須在這里復(fù)制圖像數(shù)據(jù):

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();
  • 注意
    當(dāng)通過OpenCL圖像包裝器將修改后的圖像放置到原始的OpenGL紋理時,我們必須再創(chuàng)建一個圖像數(shù)據(jù)。
  • 注意
    默認(rèn)情況下,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
要使用您自己修改的內(nèi)容,libopencv_java3.so您必須保留在APK中,不要使用OpenCV Manager并手動加載System.loadLibrary("opencv_java3")。

Performance notes

為了比較的性能,我們測量了同一預(yù)覽幀修改的FPS(拉普拉斯)由C / C ++代碼(調(diào)用完成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(由于額外的復(fù)制從cl_image到cl_buffer和背面)
以上內(nèi)容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號