上一个教程 : 使用 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
再返回的额外复制)。