本指南旨在幫助您在基于Android相機預(yù)覽的CV應(yīng)用程序中使用OpenCL?。它是為基于Eclipse的ADT工具(現(xiàn)在已不再使用Google)編寫的,但可以輕松地使用Android Studio進(jìn)行復(fù)制。
本教程假設(shè)您已經(jīng)安裝并配置了以下內(nèi)容:
它還假定您熟悉Android Java和JNI編程基礎(chǔ)知識。如果您需要上述任何方面的幫助,您可以參考我們的Android開發(fā)入門指南。
本教程還假定您具有啟用OpenCL的Android操作設(shè)備。
相關(guān)的源代碼位于OpenCV / samples / android / tutorial-4-opencl目錄下的OpenCV示例中。
通過OpenCL 使用GPGPU進(jìn)行應(yīng)用程序性能提升是現(xiàn)在非?,F(xiàn)代的趨勢。一些CV算法(例如圖像過濾)在GPU上比在CPU上運行得更快。最近在Android操作系統(tǒng)上成為可能。
用于Android操作設(shè)備的最流行的CV應(yīng)用場景是以預(yù)覽模式啟動相機,將一些CV算法應(yīng)用于每個幀,并顯示該CV算法修改的預(yù)覽幀。
讓我們考慮一下在這種情況下如何使用OpenCL。特別是讓我們嘗試兩種方法:直接調(diào)用OpenCL API和最近推出的OpenCV T-API(也稱為透明API) - 一些OpenCV算法的隱式OpenCL加速。
啟動Android API 11級(Android 3.0)Camera API允許使用OpenGL紋理作為預(yù)覽框架的目標(biāo)。Android API第21級帶來了一個新的Camera2 API,可以更好地控制相機設(shè)置和使用模式,特別是允許預(yù)覽框架和OpenGL紋理的多個目標(biāo)。
在OpenGL紋理中使用預(yù)覽框架是一個非常好的使用OpenCL,因為有一個OpenGL-OpenCL互操作性API(cl_khr_gl_sharing),允許與OpenCL功能共享OpenGL紋理數(shù)據(jù)而不復(fù)制(當(dāng)然有一些限制)。
我們?yōu)槲覀兊膽?yīng)用程序創(chuàng)建一個基礎(chǔ),只需配置Android相機將預(yù)覽幀發(fā)送到OpenGL紋理,并在顯示屏上顯示這些幀,而不進(jìn)行任何處理。
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,另一個用于現(xiàn)代Camera2。
一個最小的Renderer類可以在Java中實現(xiàn)(OpenGL ES 2.0 在Java中可用),但是由于我們將使用OpenCL修改預(yù)覽紋理,所以我們將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在相機設(shè)置和控制方面有很大不同,我們?yōu)閮蓚€相應(yīng)的渲染器創(chuàng)建一個基類:
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應(yīng)實現(xiàn)以下抽象方法:
protected abstract void openCamera();
protected abstract void closeCamera();
protected abstract void setCameraPreviewSize(int width,int height);
讓我們將其實現(xiàn)的細(xì)節(jié)放在本教程之外,請參考源代碼查看。
OpenGL ES 2.0初始化的細(xì)節(jié)在這里也很引人矚目,但重要的一點是,作為相機預(yù)覽目標(biāo)的OpeGL紋理應(yīng)該是類型GL_TEXTURE_EXTERNAL_OES(不是GL_TEXTURE_2D),在內(nèi)部它保存YUV格式的圖像數(shù)據(jù)。這使得無法通過CL-GL interop(cl_khr_gl_sharing)分享它,并通過C / C ++代碼訪問其像素數(shù)據(jù)。為了克服這個限制,我們必須GL_TEXTURE_2D使用FrameBuffer Object(也就是FBO)來執(zhí)行從這個紋理到另一個常規(guī)的OpenGL渲染。
之后,我們可以通過C / C ++ 讀?。◤?fù)制)像素數(shù)據(jù),glReadPixels()并通過修改后將它們寫回紋理glTexSubImage2D()。
此外,GL_TEXTURE_2D紋理可以與OpenCL共享而不復(fù)制,但是我們必須用特殊的方式創(chuàng)建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");
}
然后紋理可以被一個cl::ImageGL對象包裹并通過OpenCL調(diào)用進(jìn)行處理:
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隱式調(diào)用OpenCL 代碼,而不是編寫OpenCL代碼。所有你需要的是將創(chuàng)建的OpenCL上下文傳遞給OpenCV(via cv::ocl::attachContext()),并以某種方式包裝OpenGL紋理cv::UMat。不幸的是在內(nèi)部UMat保留OpenCL 緩沖區(qū),這不能被OpenGL 紋理或OpenCL 映像包裹- 所以我們必須在這里復(fù)制圖像數(shù)據(jù):
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();
注意
當(dāng)通過OpenCL圖像包裝器將修改后的圖像放置到原始的OpenGL紋理時,我們必須再創(chuàng)建一個圖像數(shù)據(jù)。
注意
默認(rèn)情況下,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")
。為了比較的性能,我們測量了同一預(yù)覽幀修改的FPS(拉普拉斯)由C / C ++代碼(調(diào)用完成cv::Laplacian與cv::Mat呼叫(使用的OpenCL),通過直接的OpenCL 圖像用于輸入和輸出),以及由OpenCV的T-API(呼叫要cv::Laplacian與cv::UMat)對索尼的Xperia Z3具備720p攝像頭分辨率:
更多建議: