上一教程: 如何在 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+)
- 从 github 或 版本 下载 OpenCV 源代码,并按照 wiki 上的说明 来构建。
此外还假设您熟悉 Android Java 和 JNI 编程基础知识。如果您在上述任何方面需要帮助,可以参考我们的 Android 开发简介 指南。
本教程还假设您有一个启用了 OpenCL 的 Android 操作设备。
相关源代码位于 OpenCV 示例的 opencv/samples/android/tutorial-4-opencl 目录中。
如何构建带有 OpenCL 的自定义 OpenCV Android SDK
- 组装并配置 Android OpenCL SDK。示例的 JNI 部分依赖于标准的 Khronos OpenCL 头文件、OpenCL 的 C++ 封装和 libOpenCL.so。标准 OpenCL 头文件可以从 OpenCV 代码库的 3rdparty 目录或您的 Linux 发行版包中复制。可以在 Github 上的官方 Khronos 存储库 中找到 C++ 封装。以下列方式将头文件复制到特定的目录中
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)
- 使用 OpenCL 构建自定义 OpenCV Android SDK。对于 Android 操作系统,默认情况下在 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
中设置这些 CMake 参数,例如 ndk-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(又称 Transparent API)——某些 OpenCV 算法的隐式 OpenCL 加速。
应用程序结构
从 Android API 级别 11(Android 3.0)开始,Camera 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 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);
}
@Override
public void surfaceDestroyed(SurfaceHolder holder) {
super.surfaceDestroyed(holder);
}
public void setProcessingMode(int newMode) {
if(newMode>=0 && newMode<procModeName.length)
procMode = newMode;
else
Log.e(LOGTAG, "Ignoring invalid processing mode: " + newMode);
((Activity) getContext()).runOnUiThread(new Runnable() {
public void run() {
Toast.makeText(getContext(), "Selected mode: " + procModeName[procMode], Toast.LENGTH_LONG).show();
}
});
}
@Override
public void onCameraViewStarted(int width, int height) {
((Activity) getContext()).runOnUiThread(new Runnable() {
public void run() {
Toast.makeText(getContext(), "onCameraViewStarted", Toast.LENGTH_SHORT).show();
}
});
if (NativePart.builtWithOpenCL())
NativePart.initCL();
frameCounter = 0;
lastNanoTime = System.nanoTime();
}
@Override
public void onCameraViewStopped() {
((Activity) getContext()).runOnUiThread(new Runnable() {
public void run() {
Toast.makeText(getContext(), "onCameraViewStopped", Toast.LENGTH_SHORT).show();
}
});
}
@Override
public boolean onCameraTexture(int texIn, int texOut, int width, int height) {
frameCounter++;
if(frameCounter >= 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。
可以用 Java 实现一个最小的 Renderer
类(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) {
mTexUpdate = true;
mView.requestRender();
}
@Override
public void onDrawFrame(GL10 gl) {
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;
}
}
std::string String
定义 cvstd.hpp:151
如你所见,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++ 代码访问其像素数据。为了克服此限制,我们必须从该纹理到另一个常规 GL_TEXTURE_2D
纹理执行 OpenGL 渲染,使用 帧缓冲对象(又名 FBO)。
C/C++ 代码
之后,我们可以通过 glReadPixels()
从 C/C++ 中读取(复制)像素数据,并在修改后通过 glTexSubImage2D()
将其写回纹理。
直接 OpenCL 调用
同样地,GL_TEXTURE_2D
纹理可以与 OpenCL 共享而无需复制,但我们必须通过特殊方式创建 OpenCL 环境
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<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]);
cl::Program::Sources src(1, std::make_pair(oclProgI2I, sizeof(oclProgI2I)));
theProgI2I = cl::Program(theContext, src);
theProgI2I.build(devs);
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;
}
void attachContext(const String &platformName, void *platformID, void *context, void *deviceID)
将 OpenCL 环境连接到 OpenCV。
那么该纹理可以通过一个 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");
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()
)及以 cv::UMat
封装 OpenGL 纹理。遗憾的是,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();
LOGD("将纹理数据加载至 OpenCV UMat 消耗 %d 毫秒", getTimeInterval(t));
theQueue.enqueueReleaseGLObjects(&images);
t = getTimeMs();
cv:multiply(uTmp, 10, uOut);
LOGD("OpenCV 处理消耗 %d 毫秒", 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);
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 毫秒", getTimeInterval(t));
void * handle(AccessFlag accessFlags) const
static Queue & getDefault()
@ ACCESS_READ
定义 mat.hpp:65
#define CV_8U
定义 interface.h:73
void convertFromImage(void *cl_mem_image, UMat &dst)
将 OpenCL image2d_t 转换为 UMat。
#define CV_Assert(expr)
运行时检查某个条件,如果失败则抛出异常。
定义 base.hpp:342
void Laplacian(InputArray src, OutputArray dst, int ddepth, int ksize=1, double scale=1, double delta=0, int borderType=BORDER_DEFAULT)
计算图像的拉普拉斯算子。
磁盘文件关联的文件存储“黑匣子”表示。
定义 core.hpp:102
- 注释
- 我们将不得不进行更多图像数据复制,通过 OpenCL 图像包装器将已修改的图像放回原始 OpenGL 纹理。
性能说明
为了比较性能,我们测量了以 C/C++ 代码(使用 cv::Laplacian
加 cv::Mat
)、直接 OpenCL 调用(使用 OpenCL 图像 作为输入和输出)、以及 OpenCV T-API(使用 cv::Laplacian
加 cv::UMat
)对相同的预览帧修改(拉普拉斯算子)所做的修改的 FPS 在具有 720p 摄像头分辨率的 Sony Xperia Z3 上
- C/C++ 版本 显示为 3-4 fps
- 直接 OpenCL 调用 显示为 25-27 fps
- OpenCV T-API 显示为 11-13 fps(由于在
cl_image
和 cl_buffer
之间进行额外的复制)