OpenCV 4.11.0
开源计算机视觉
加载中…
搜索中…
无匹配项
在基于Android相机预览的CV应用程序中使用OpenCL

上一教程: 如何在Android设备上运行深度神经网络
下一教程: 在MacOS上安装

原作者Andrey Pavlenko, Alexander Panov
兼容性OpenCV >= 4.9

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

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

  • Android Studio (2022.2.1.+)
  • JDK 17
  • Android SDK
  • Android NDK (25.2.9519653+)
  • githubreleases下载OpenCV源代码,并按照wiki上的说明进行构建。

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

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

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

如何构建带有OpenCL的自定义OpenCV Android SDK

  1. 组装和配置Android OpenCL SDK。示例的JNI部分依赖于标准的Khronos OpenCL头文件,以及OpenCL的C++包装器和libOpenCL.so。标准的OpenCL头文件可以从OpenCV存储库的3rdparty目录或您的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
    libOpenCL.so可能由BSP提供,或者可以从任何具有相关架构的启用OpenCL的Android设备下载。
    cd your_path/ANDROID_OPENCL_SDK && mkdir lib && cd lib
    adb pull /system/vendor/lib64/libOpenCL.so
    libOpenCL.so的系统版本可能有很多平台特定的依赖项。-Wl,--allow-shlib-undefined标志允许在构建过程中忽略未使用的第三方符号。以下CMake行允许将JNI部分链接到标准OpenCL,但不将loadLibrary包含到应用程序包中。系统OpenCL API在运行时使用。
    target_link_libraries(${target} -lOpenCL)
  2. 使用OpenCL构建自定义OpenCV Android SDK。默认情况下,Android OS的OpenCV构建中禁用了OpenCL支持(T-API),但可以在本地重新构建启用OpenCL/T-API的Android版OpenCV:对CMake使用-DWITH_OPENCL=ON选项。您还需要指定Android OpenCL SDK的路径:对CMake使用-DANDROID_OPENCL_SDK=path_to_your_Android_OpenCL_SDK选项。如果您使用build_sdk.py构建OpenCV,请按照wiki上的说明操作。在您的.config.py(例如ndk-18-api-level-21.config.py)中设置这些CMake参数:
    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 OS上成为可能。

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 CameraGLSurfaceView implements CameraGLSurfaceView.CameraTextureListener {
static final String LOGTAG = "MyGLSurfaceView";
受保护的 int procMode = NativePart.PROCESSING_MODE_NO_PROCESSING;
静态 final String[] procModeName = new String[] {"无处理", "CPU", "OpenCL 直接", "OpenCL 通过 OpenCV"};
受保护的 int frameCounter;
受保护的 long lastNanoTime;
TextView mFpsText = null;
公共 MyGLSurfaceView(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
公共 boolean onTouchEvent(MotionEvent e) {
如果(e.getAction() == MotionEvent.ACTION_DOWN)
((Activity)getContext()).openOptionsMenu();
返回 true;
}
@Override
公共 void surfaceCreated(SurfaceHolder holder) {
super.surfaceCreated(holder);
//NativePart.initCL();
}
@Override
公共 void surfaceDestroyed(SurfaceHolder holder) {
//NativePart.closeCL();
super.surfaceDestroyed(holder);
}
公共 void setProcessingMode(int newMode) {
如果(newMode>=0 && newMode
procMode = newMode;
否则
Log.e(LOGTAG, "忽略无效的处理模式: " + newMode);
((Activity) getContext()).runOnUiThread(new Runnable() {
公共 void run() {
Toast.makeText(getContext(), "选择的模式: " + procModeName[procMode], Toast.LENGTH_LONG).show();
}
});
}
@Override
公共 void onCameraViewStarted(int width, int height) {
((Activity) getContext()).runOnUiThread(new Runnable() {
公共 void run() {
Toast.makeText(getContext(), "onCameraViewStarted", Toast.LENGTH_SHORT).show();
}
});
如果 (NativePart.builtWithOpenCL())
NativePart.initCL();
frameCounter = 0;
lastNanoTime = System.nanoTime();
}
@Override
公共 void onCameraViewStopped() {
((Activity) getContext()).runOnUiThread(new Runnable() {
公共 void run() {
Toast.makeText(getContext(), "onCameraViewStopped", Toast.LENGTH_SHORT).show();
}
});
}
@Override
公共 boolean onCameraTexture(int texIn, int texOut, int width, int height) {
// FPS
frameCounter++;
如果(frameCounter >= 30)
{
最终的 int fps = (int) (frameCounter * 1e9 / (System.nanoTime() - lastNanoTime));
Log.i(LOGTAG, "drawFrame() FPS: "+fps);
如果(mFpsText != null) {
Runnable fpsUpdater = new Runnable() {
公共 void run() {
mFpsText.setText("FPS: " + fps);
}
};
new Handler(Looper.getMainLooper()).post(fpsUpdater);
} 否则 {
Log.d(LOGTAG, "mFpsText == null");
mFpsText = (TextView)((Activity) getContext()).findViewById(R.id.fps_text_view);
}
frameCounter = 0;
lastNanoTime = System.nanoTime();
}
如果(procMode == NativePart.PROCESSING_MODE_NO_PROCESSING)
返回 false;
NativePart.processFrame(texIn, texOut, width, height, procMode);
返回 true;
}
}
注意
我们使用了两个渲染器类:一个用于旧版 Camera API,另一个用于现代 Camera2 API。

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

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

由于 Camera 和 Camera2 API 在相机设置和控制方面存在很大差异,因此让我们为两个对应的渲染器创建一个基类

公共抽象类 MyGLRendererBase 实现 GLSurfaceView.Renderer, SurfaceTexture.OnFrameAvailableListener {
受保护的 final String LOGTAG = "MyGLRendererBase";
受保护的 SurfaceTexture mSTex;
受保护的 MyGLSurfaceView mView;
受保护的 boolean mGLInit = false;
受保护的 boolean mTexUpdate = false;
MyGLRendererBase(MyGLSurfaceView view) {
mView = view;
}
受保护的抽象 void openCamera();
受保护的抽象 void closeCamera();
受保护的抽象 void setCameraPreviewSize(int width, int height);
公共 void onResume() {
Log.i(LOGTAG, "onResume");
}
公共 void onPause() {
Log.i(LOGTAG, "onPause");
mGLInit = false;
mTexUpdate = false;
closeCamera();
如果(mSTex != null) {
mSTex.release();
mSTex = null;
NativeGLRenderer.closeGL();
}
}
@Override
公共同步 void onFrameAvailable(SurfaceTexture surfaceTexture) {
//Log.i(LOGTAG, "onFrameAvailable");
mTexUpdate = true;
mView.requestRender();
}
@Override
公共 void onDrawFrame(GL10 gl) {
//Log.i(LOGTAG, "onDrawFrame");
如果 (!mGLInit)
返回;
同步 (this) {
如果 (mTexUpdate) {
mSTex.updateTexImage();
mTexUpdate = false;
}
}
NativeGLRenderer.drawFrame();
}
@Override
公共 void onSurfaceChanged(GL10 gl, int surfaceWidth, int surfaceHeight) {
Log.i(LOGTAG, "onSurfaceChanged(" + surfaceWidth + "x" + surfaceHeight + ")");
NativeGLRenderer.changeSize(surfaceWidth, surfaceHeight);
setCameraPreviewSize(surfaceWidth, surfaceHeight);
}
@Override
公共 void onSurfaceCreated(GL10 gl, EGLConfig config) {
Log.i(LOGTAG, "onSurfaceCreated");
字符串 strGLVersion = GLES20.glGetString(GLES20.GL_VERSION);
如果 (strGLVersion != 空)
Log.i(LOGTAG, "OpenGL ES 版本: " + strGLVersion);
int hTex = NativeGLRenderer.initGL();
mSTex = 新 SurfaceTexture(hTex);
mSTex.setOnFrameAvailableListener(this);
openCamera();
mGLInit = 真;
}
}
std::string 字符串
**定义** cvstd.hpp:151

正如你所看到的,`Camera` 和 `Camera2` API 的继承者应该实现以下抽象方法

受保护的抽象 void openCamera();
受保护的抽象 void closeCamera();
受保护的抽象 void setCameraPreviewSize(int width, int height);

让我们将它们的实现细节留在这个教程之外,请参考 源代码 查看它们。

预览帧修改

OpenGL ES 2.0 初始化的细节也很直接,并且过于冗长,不适合在此引用,但此处重要的是,作为摄像机预览目标的 OpenGL 纹理应该是 `GL_TEXTURE_EXTERNAL_OES` 类型(而不是 `GL_TEXTURE_2D`),它在内部以 *YUV* 格式保存图像数据。这使得无法通过 CL-GL 互操作 ( `cl_khr_gl_sharing` ) 共享它,也无法通过 C/C++ 代码访问它的像素数据。为了克服这个限制,我们必须从这个纹理到另一个常规的 `GL_TEXTURE_2D` 纹理执行 OpenGL 渲染,使用 *帧缓冲区对象* (又名 FBO)。

C/C++ 代码

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

直接 OpenCL 调用

同样,`GL_TEXTURE_2D` 纹理可以与 OpenCL 共享而无需复制,但我们必须以特殊方式创建 OpenCL 上下文。

int initCL()
{
dumpCLinfo();
LOGE("initCL: 开始 initCL");
EGLDisplay mEglDisplay = eglGetCurrentDisplay();
如果 (mEglDisplay == EGL_NO_DISPLAY)
LOGE("initCL: eglGetCurrentDisplay() 返回 'EGL_NO_DISPLAY',错误 = %x", eglGetError());
EGLContext mEglContext = eglGetCurrentContext();
如果 (mEglContext == EGL_NO_CONTEXT)
LOGE("initCL: eglGetCurrentContext() 返回 'EGL_NO_CONTEXT',错误 = %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 };
尝试
{
haveOpenCL = 假;
cl::Platform p = cl::Platform::getDefault();
std::string ext = p.getInfo<CL_PLATFORM_EXTENSIONS>();
如果 (ext.find("cl_khr_gl_sharing") == std::string::npos)
LOGE("警告:PLATFORM 不支持 CL-GL 共享");
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("上下文返回 %d 个设备,取第一个", devs.size());
ext = devs[0].getInfo<CL_DEVICE_EXTENSIONS>();
如果 (ext.find("cl_khr_gl_sharing") == std::string::npos)
LOGE("警告:DEVICE 不支持 CL-GL 共享");
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<CL_PLATFORM_NAME>(), p(), theContext(), devs[0]());
如果 (cv::ocl::useOpenCL())
LOGD("OpenCV+OpenCL 工作正常!");
否则
LOGE("无法使用 OpenCL TAPI 初始化 OpenCV");
haveOpenCL = 真;
}
捕获 (const cl::Error& e)
{
LOGE("cl::Error: %s (%d)", e.what(), e.err());
返回 1;
}
捕获 (const std::exception& e)
{
LOGE("std::exception: %s", e.what());
返回 2;
}
捕获(...)
{
LOGE("OpenCL 信息:初始化 OpenCL 内容时出现未知错误");
返回 3;
}
LOGD("initCL 完成");
如果 (haveOpenCL)
返回 0;
否则
返回 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() 耗时 %d 毫秒", getTimeInterval(t));
t = getTimeMs();
cl::Kernel Laplacian(theProgI2I, "Laplacian"); //TODO: 可能只执行一次
Laplacian.setArg(0, imgIn);
Laplacian.setArg(1, imgOut);
theQueue.finish();
LOGD("Kernel() 耗时 %d 毫秒", getTimeInterval(t));
t = getTimeMs();
theQueue.enqueueNDRangeKernel(Laplacian, cl::NullRange, cl::NDRange(w, h), cl::NullRange);
theQueue.finish();
LOGD("enqueueNDRangeKernel() 耗时 %d 毫秒", getTimeInterval(t));
t = getTimeMs();
theQueue.enqueueReleaseGLObjects(&images);
theQueue.finish();
LOGD("enqueueReleaseGLObjects() 耗时 %d 毫秒", getTimeInterval(t));

OpenCV T-API

但是,与其自己编写 OpenCL 代码,不如使用隐式调用 OpenCL 的 **OpenCV T-API**。您只需要将创建的 OpenCL 上下文传递给 OpenCV(通过 `cv::ocl::attachContext()`),并以某种方式将 OpenGL 纹理与 `cv::UMat` 包装起来。不幸的是,`UMat` 在内部保留 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("将纹理数据加载到OpenCV UMat耗时 %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);
LOGD("OpenCV处理耗时 %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);
LOGD("将结果上传到纹理耗时 %d ms", getTimeInterval(t));
注意
通过OpenCL图像包装器将修改后的图像放回原始OpenGL纹理时,我们必须再进行一次图像数据复制。

性能说明

为了比较性能,我们测量了在Sony Xperia Z3(720p摄像头分辨率)上,通过C/C++代码(调用带有cv::Mat的cv::Laplacian)、直接OpenCL调用(使用OpenCL图像作为输入和输出)以及OpenCV T-API(调用带有cv::UMat的cv::Laplacian)对相同预览帧进行修改(Laplacian)的FPS。

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