OpenCV介绍:Java&Android——在基于 Android 摄像头预览的 CV 应用程序中使用 OpenCL OpenCV v4.8.0

上一个教程使用 OpenCV 进行 Android 开发

下一个教程 : 在 MacOS 中安装

原作者Andrey Pavlenko
兼容性OpenCV >= 3.0

警告
本教程已废弃。

本指南旨在帮助您在基于 Android 摄像头预览的 CV 应用程序中使用 OpenCL ™。它是为基于 Eclipse 的 ADT 工具编写的(现在已被 Google 弃用),但可以很容易地在 Android Studio 中重现。

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

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

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

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

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

前言

通过 OpenCL 使用 GPGPU 来提高应用程序性能已成为现代趋势。一些 CV 算法(如图像过滤)在 GPU 上的运行速度要比在 CPU 上快得多。最近,这在安卓操作系统上已成为可能。

安卓设备上最流行的 CV 应用场景是在预览模式下启动相机,对每一帧应用某些 CV 算法,并显示经该 CV 算法修改的预览帧。

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

应用程序结构

从 Android API 第 11 级(Android 3.0)开始,相机 API 允许使用 OpenGL 纹理作为预览帧的目标。Android API 第 21 级带来了新的 Camera2 API,该 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。

一个最小的渲染器类可以用 Java 实现(OpenGL ES 2.0 可用 Java),但由于我们要用 OpenCL 来修改预览纹理,所以我们要把 OpenGL 的东西移到 JNI 中。下面是一个简单的 JNI Java 封装:

public class NativeGLRenderer {
    static
    {
        System.loadLibrary("opencv_java4"); // 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);
}

由于 Camera 和 Camera2 API 在相机设置和控制方面有很大不同,因此让我们为这两个相应的呈现器创建一个基类:

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 和 Camera2 API 的继承者应实现以下抽象方法:

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 互操作(cl_khr_gl_sharing)共享图像数据,也无法通过 C/C++ 代码访问像素数据。为了克服这一限制,我们必须使用 FrameBuffer Object(又称 FBO)将该纹理渲染为另一个常规的 GL_TEXTURE_2D

C/C++ 代码

然后,我们可以通过 glReadPixels() 从 C/C++ 读取(复制)像素数据,并通过 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

但与其自己编写 OpenCL 代码,不如使用隐式调用 OpenCL 的 OpenCV T-API。您只需将创建的 OpenCL 上下文传递给 OpenCV(通过 cv::ocl::attachContext()),并以某种方式用 cv::UMat 封装 OpenGL 纹理。不幸的是,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 纹理时,我们必须再复制一份图像数据。

注意事项
默认情况下,OpenCL 支持(T-API)在为 Android 操作系统构建的 OpenCV 中是禁用的(因此从 3.0
版开始,官方软件包中就没有了),但可以在本地重建启用了 OpenCL/T-API 的 OpenCV for Android:在 CMake
中使用 -DWITH_OPENCL=YES 选项。
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_java4.so,必须将其保留在 APK 中,而不是使用 OpenCV
管理器,并通过 System.loadLibrary("opencv_java4") 手动加载。

性能说明

为了比较性能,我们在索尼 Xperia Z3(摄像头分辨率为 720p)上测量了通过 C/C++ 代码(使用 cv::Mat调用 cv::Laplacian)、直接调用 OpenCL(使用 OpenCL 图像作为输入和输出)和 OpenCV T-API(使用 cv::UMat调用 cv::Laplacian)完成的相同预览帧修改(拉普拉斯)的 FPS:

  • C/C++ 版本显示 3-4 帧/秒
  • 直接调用 OpenCL 显示为 25-27 帧/秒
  • OpenCV T-API 显示 11-13 帧/秒(由于从 cl_image 复制到 cl_buffer 再返回的额外复制)。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值