基于Android相机预览的CV应用程序中使用OpenCL
本指南旨在帮助您在基于Android相机预览的CV应用程序中使用OpenCL™。它是为基于Eclipse的ADT工具(现在已不再使用Google)编写的,但可以轻松地使用Android Studio进行复制。
本教程假设您已经安装并配置了以下内容:
- JDK
- Android SDK和NDK
- 具有ADT和CDT插件的Eclipse IDE
它还假定您熟悉Android Java和JNI编程基础知识。如果您需要上述任何方面的帮助,您可以参考我们的Android开发入门指南。
本教程还假定您具有启用OpenCL的Android操作设备。
相关的源代码位于OpenCV / samples / android / tutorial-4-opencl目录下的OpenCV示例中。
前言
通过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)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 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。
一个最小的Renderer类可以在Java中实现(OpenGL ES 2.0 在Java中可用),但是由于我们将使用OpenCL修改预览纹理,所以我们将OpenGL的东西移动到JNI。这是一个简单的Java包装器我们的JNI的东西:
public class NativeGLRenderer {
static
{
System.loadLibrary("opencv_java3"); // 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和Camera2API应实现以下抽象方法:
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 interop(cl_khr_gl_sharing)分享它,并通过C / C ++代码访问其像素数据。为了克服这个限制,我们必须GL_TEXTURE_2D使用FrameBuffer Object(也就是FBO)来执行从这个纹理到另一个常规的OpenGL渲染。
C / C ++代码
之后,我们可以通过C / C ++ 读取(复制)像素数据,glReadPixels()并通过修改后将它们写回纹理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
但是,您可能希望使用OpenCV T-API隐式调用OpenCL 代码,而不是编写OpenCL代码。所有你需要的是将创建的OpenCL上下文传递给OpenCV(via cv::ocl::attachContext()),并以某种方式包装OpenGL纹理cv::UMat。不幸的是在内部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纹理时,我们必须再创建一个图像数据。
注意 默认情况下,Android OS的OpenCV版本中禁用了OpenCL支持(T-API)(因此在3.0版本的官方软件包中不存在),但是可以在启用OpenCL / T-API的Android上重建本地OpenCV:use -DWITH_OPENCL=YESoption为CMake。
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_java3.so
您必须保留在APK中,不要使用OpenCV Manager并手动加载System.loadLibrary("opencv_java3")
。
Performance notes
为了比较的性能,我们测量了同一预览帧修改的FPS(拉普拉斯)由C / C ++代码(调用完成cv::Laplacian与cv::Mat呼叫(使用的OpenCL),通过直接的OpenCL 图像用于输入和输出),以及由OpenCV的T-API(呼叫要cv::Laplacian与cv::UMat)对索尼的Xperia Z3具备720p摄像头分辨率:
- C / C ++版本显示3-4 fps
- 直接OpenCL呼叫显示25-27 fps
- OpenCV的T-API显示11-13 FPS(由于额外的复制从cl_image到cl_buffer和背面)