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

您所在的位置:网站首页 安卓opencl测试程序 在基于Android相机预览的CV应用程序中使用 OpenCL(15)

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

2024-07-12 10:16| 来源: 网络整理| 查看: 265

 查看:OpenCV系列文章目录(持续更新中......) 上一篇:OpenCV4.9.0在Android 开发简介(14) 下一篇:在 MacOS 中安装(16)

本指南旨在帮助您在基于 Android 相机预览的 CV 应用程序中使用 OpenCL ™。教程是为 Android Studio 2022.2.1 编写的。它已使用 Ubuntu 22.04 进行了测试。

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

Android Studio (2022.2.1.+)JDK 17Android SDKAndroid NDK (25.2.9519653+)从 github 或发布版下载 OpenCV 源代码,并按照 wiki 上的指令构建。

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

本教程还假设您有一个启用了 OpenCL 的 Android 操作设备。

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

如何使用 OpenCL 构建自定义 OpenCV Android SDK 组装和配置 Android OpenCL SDK。示例的 JNI 部分依赖于标准的 Khornos OpenCL 标头,以及 OpenCL 和 libOpenCL.so 的C++包装器。标准的 OpenCL 标头可以从 OpenCV 存储库中的第三方目录或您的 Linux 分发包中复制。C++ 包装器可在 Github 上的官方 Khronos 存储库中找到。按以下方式将头文件复制到教学目录: cd your_path/ && mkdir ANDROID_OPENCL_SDK && mkdir ANDROID_OPENCL_SDK/include && cd ANDROID_OPENCL_SDK/include cp -r path_to_opencv/opencv/3rdparty/include/opencl/1.2/CL . && cd CL wget https://github.com/KhronosGroup/OpenCL-CLHPP/raw/main/include/CL/opencl.hpp wget https://github.com/KhronosGroup/OpenCL-CLHPP/raw/main/include/CL/cl2.hpp

wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==

libOpenCL.so 可以随 BSP 一起提供,也可以从任何具有相关架构的 OpenCL-cabaple Android 设备下载 cd your_path/ANDROID_OPENCL_SDK && mkdir lib && cd lib adb pull /system/vendor/lib64/libOpenCL.so

wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==

 libOpenCL.so 的系统版本可能有很多特定于平台的依赖关系。-Wl,--allow-shlib-undefined 标志允许忽略在构建过程中未使用的第三方符号。以下 CMake 行允许将 JNI 部件链接到标准 OpenCL,但不能将 loadLibrary 包含在应用程序包中。系统 OpenCL API 用于运行时。

target_link_libraries(${target} -lOpenCL)

wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==

使用 OpenCL 构建自定义 OpenCV Android SDK。默认情况下,OpenCL 支持 (T-API) 在 Android 操作系统的 OpenCV 构建中处于禁用状态。但可以在启用 OpenCL/T-API 的情况下在本地重建适用于 Android 的 OpenCV:CMake 的 use 选项。您还需要为 CMake 指定 Android OpenCL SDK: use 选项的路径。如果您正在使用 OpenCV 构建 OpenCV,请按照 wiki 上的说明进行操作。在 中设置这些 CMake 参数,例如:-DWITH_OPENCL=ON-DANDROID_OPENCL_SDK=path_to_your_Android_OpenCL_SDKbuild_sdk.py.config.pyndk-18-api-level-21.config.py

ABI("3", "arm64-v8a", None, 21, cmake_vars=dict('WITH_OPENCL': 'ON', 'ANDROID_OPENCL_SDK': 'path_to_your_Android_OpenCL_SDK'))

如果您使用 cmake/ninja 构建 OpenCV,请使用以下 bash 脚本(设置您的NDK_VERSION和路径,而不是路径示例):

cd path_to_opencv && mkdir build && cd build export NDK_VERSION=25.2.9519653 export ANDROID_SDK=/home/user/Android/Sdk/ export ANDROID_OPENCL_SDK=/path_to_ANDROID_OPENCL_SDK/ export ANDROID_HOME=$ANDROID_SDK export ANDROID_NDK_HOME=$ANDROID_SDK/ndk/$NDK_VERSION/ cmake -GNinja -DCMAKE_TOOLCHAIN_FILE=$ANDROID_NDK_HOME/build/cmake/android.toolchain.cmake -DANDROID_STL=c++_shared -DANDROID_NATIVE_API_LEVEL=24 -DANDROID_SDK=$ANDROID_SDK -DANDROID_NDK=$ANDROID_NDK_HOME -DBUILD_JAVA=ON -DANDROID_HOME=$ANDROID_SDK -DBUILD_ANDROID_EXAMPLES=ON -DINSTALL_ANDROID_EXAMPLES=ON -DANDROID_ABI=arm64-v8a -DWITH_OPENCL=ON -DANDROID_OPENCL_SDK=$ANDROID_OPENCL_SDK .. 前言

现在,通过 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) 相机 API 允许使用 OpenGL 纹理作为预览帧的目标。Android API 级别 21 带来了一个新的 Camera2 API,它提供了对相机设置和使用模式的更多控制,它允许预览帧的多个目标,特别是 OpenGL 纹理。

在 OpenGL 纹理中拥有预览帧对于使用 OpenCL 来说很划算,因为有一个 OpenGL-OpenCL 互操作性 API (cl_khr_gl_sharing),允许与 OpenCL 函数共享 OpenGL 纹理数据而无需复制(当然有一些限制)。

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

用于此目的的最小类Activity如下所示: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 CameraGLSurfaceView implements CameraGLSurfaceView.CameraTextureListener { static final String LOGTAG = "MyGLSurfaceView"; protected int procMode = NativePart.PROCESSING_MODE_NO_PROCESSING; static final String[] procModeName = new String[] {"No Processing", "CPU", "OpenCL Direct", "OpenCL via OpenCV"}; protected int frameCounter; protected long lastNanoTime; TextView mFpsText = null; public MyGLSurfaceView(Context context, AttributeSet attrs) { super(context, attrs); } @Override public boolean onTouchEvent(MotionEvent e) { if(e.getAction() == MotionEvent.ACTION_DOWN) ((Activity)getContext()).openOptionsMenu(); return true; } @Override public void surfaceCreated(SurfaceHolder holder) { super.surfaceCreated(holder); //NativePart.initCL(); } @Override public void surfaceDestroyed(SurfaceHolder holder) { //NativePart.closeCL(); super.surfaceDestroyed(holder); } public void setProcessingMode(int newMode) { if(newMode>=0 && newMode= 30) { final int fps = (int) (frameCounter * 1e9 / (System.nanoTime() - lastNanoTime)); Log.i(LOGTAG, "drawFrame() FPS: "+fps); if(mFpsText != null) { Runnable fpsUpdater = new Runnable() { public void run() { mFpsText.setText("FPS: " + fps); } }; new Handler(Looper.getMainLooper()).post(fpsUpdater); } else { Log.d(LOGTAG, "mFpsText == null"); mFpsText = (TextView)((Activity) getContext()).findViewById(R.id.fps_text_view); } frameCounter = 0; lastNanoTime = System.nanoTime(); } if(procMode == NativePart.PROCESSING_MODE_NO_PROCESSING) return false; NativePart.processFrame(texIn, texOut, width, height, procMode); return true; } }

注意

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

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

public class NativePart { static { System.loadLibrary("opencv_java4"); System.loadLibrary("JNIpart"); } public static final int PROCESSING_MODE_NO_PROCESSING = 0; public static final int PROCESSING_MODE_CPU = 1; public static final int PROCESSING_MODE_OCL_DIRECT = 2; public static final int PROCESSING_MODE_OCL_OCV = 3; public static native boolean builtWithOpenCL(); public static native int initCL(); public static native void closeCL(); public static native void processFrame(int tex1, int tex2, int w, int h, int mode); }

由于 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  APIs的继承者应实现以下抽象方法:

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

让我们把它们实现的细节留给本教程之外,请参考源代码查看它们。

预览帧修改

OpenGL ES 2.0 初始化的细节也相当简单明了,这里要引用的嘈杂,但这里重要的一点是,作为相机预览目标的 OpeGL 纹理应该是类型(不是),在内部它以 YUV 格式保存图片数据。这使得无法通过 CL-GL 互操作 () 共享它并通过 C/C++ 代码访问其像素数据。为了克服这个限制,我们必须使用 FrameBuffer 对象(又名 FBO)执行从这个纹理到另一个常规纹理的 OpenGL 渲染

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

C/C++ code

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

直接 OpenCL 调用

此外,该纹理可以在不复制的情况下与 OpenCL 共享,但我们必须以特殊方式创建 OpenCL context如下:

​ int initCL() { dumpCLinfo(); LOGE("initCL: start 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 { haveOpenCL = false; cl::Platform p = cl::Platform::getDefault(); std::string ext = p.getInfo(); 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 devs = theContext.getInfo(); LOGD("Context returned %d devices, taking the 1st one", devs.size()); ext = devs[0].getInfo(); 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]); cl::Program::Sources src(1, std::make_pair(oclProgI2I, sizeof(oclProgI2I))); theProgI2I = cl::Program(theContext, src); theProgI2I.build(devs); cv::ocl::attachContext(p.getInfo(), p(), theContext(), devs[0]()); if( cv::ocl::useOpenCL() ) LOGD("OpenCV+OpenCL works OK!"); else LOGE("Can't init OpenCV with OpenCL TAPI"); haveOpenCL = true; } catch(const cl::Error& e){ LOGE("cl::Error: %s (%d)", e.what(), e.err()); return 1; } catch(const std::exception& e) { LOGE("std::exception: %s", e.what()); return 2; } catch(...) { LOGE( "OpenCL info: unknown error while initializing OpenCL stuff" ); return 3; } LOGD("initCL completed"); if (haveOpenCL) return 0; else return 4; }

然后,纹理可以被对象包装 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); int64_t t = getTimeMs(); theQueue.enqueueAcquireGLObjects(&images); theQueue.finish(); LOGD("enqueueAcquireGLObjects() costs %d ms", getTimeInterval(t)); t = getTimeMs(); cl::Kernel Laplacian(theProgI2I, "Laplacian"); //TODO: may be done once Laplacian.setArg(0, imgIn); Laplacian.setArg(1, imgOut); theQueue.finish(); LOGD("Kernel() costs %d ms", getTimeInterval(t)); t = getTimeMs(); theQueue.enqueueNDRangeKernel(Laplacian, cl::NullRange, cl::NDRange(w, h), cl::NullRange); theQueue.finish(); LOGD("enqueueNDRangeKernel() costs %d ms", getTimeInterval(t)); t = getTimeMs(); theQueue.enqueueReleaseGLObjects(&images); theQueue.finish(); LOGD("enqueueReleaseGLObjects() costs %d ms", getTimeInterval(t)); OpenCV T-API

但是,与其自己编写 OpenCL 代码,不如使用隐式调用 OpenCL 的 OpenCV T-API。您只需要将创建的 OpenCL 上下文传递给 OpenCV(通过cv::ocl::attachContext() ),并以某种  cv::UMat.方式将 OpenGL 纹理包装起来。不幸的是,OpenCL 缓冲区在内部保留,它不能包装在 OpenGL 纹理或 OpenCL 图像上 - 因此我们必须在此处复制图像数据:

​int64_t t = getTimeMs(); cl::ImageGL imgIn (theContext, CL_MEM_READ_ONLY, GL_TEXTURE_2D, 0, texIn); std::vector < cl::Memory > images(1, imgIn); theQueue.enqueueAcquireGLObjects(&images); theQueue.finish(); cv::UMat uIn, uOut, uTmp; cv::ocl::convertFromImage(imgIn(), uIn); LOGD("loading texture data to OpenCV UMat costs %d ms", getTimeInterval(t)); theQueue.enqueueReleaseGLObjects(&images); t = getTimeMs(); //cv::blur(uIn, uOut, cv::Size(5, 5)); cv::Laplacian(uIn, uTmp, CV_8U); cv:multiply(uTmp, 10, uOut); cv::ocl::finish(); LOGD("OpenCV processing costs %d ms", getTimeInterval(t)); t = getTimeMs(); cl::ImageGL imgOut(theContext, CL_MEM_WRITE_ONLY, GL_TEXTURE_2D, 0, texOut); 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] = { (size_t)w, (size_t)h, 1 }; CV_Assert(clEnqueueCopyBufferToImage (q, clBuffer, imgOut(), offset, origin, region, 0, NULL, NULL) == CL_SUCCESS); theQueue.enqueueReleaseGLObjects(&images); cv::ocl::finish(); LOGD("uploading results to texture costs %d ms", getTimeInterval(t));

注意

当通过 OpenCL 图像包装器将修改后的图像放回原始 OpenGL 纹理时,我们必须再制作一个图像数据副本。

性能说明

为了比较在具有720p相机分辨率的Sony Xperia Z3上,通过C / C++代码(调用cv::Laplacian与cv::Mat),直接OpenCL调用(使用OpenCL图像进行输入和输出)和OpenCV T-API(调用cv::Laplacian与cv::UMat)完成的相同预览帧修改(Laplacian)的FPS:

C/C++ 版本显示 3-4 fps直接 OpenCL 调用显示 25-27 fpsOpenCV T-API 显示 11-13 fps(由于额外的来回复制)cl_imagecl_buffer

参考文献:

1、《Use OpenCL in Android camera preview based CV application》  Andrey Pavlenko, Alexander Panov



【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3