1 Use OpenCL in Android camera preview based CV application {#tutorial_android_ocl_intro}
2 =====================================
4 This guide was designed to help you in use of [OpenCL ™](https://www.khronos.org/opencl/) in Android camera preview based CV application.
5 It was written for [Eclipse-based ADT tools](http://developer.android.com/tools/help/adt.html)
6 (deprecated by Google now), but it easily can be reproduced with [Android Studio](http://developer.android.com/tools/studio/index.html).
8 This tutorial assumes you have the following installed and configured:
12 - Eclipse IDE with ADT and CDT plugins
14 It also assumes that you are familiar with Android Java and JNI programming basics.
15 If you need help with anything of the above, you may refer to our @ref tutorial_android_dev_intro guide.
17 This tutorial also assumes you have an Android operated device with OpenCL enabled.
19 The related source code is located within OpenCV samples at
20 [opencv/samples/android/tutorial-4-opencl](https://github.com/opencv/opencv/tree/3.4/samples/android/tutorial-4-opencl/) directory.
25 Using [GPGPU](https://en.wikipedia.org/wiki/General-purpose_computing_on_graphics_processing_units)
26 via OpenCL for applications performance enhancements is quite a modern trend now.
27 Some CV algo-s (e.g. image filtering) run much faster on a GPU than on a CPU.
28 Recently it has become possible on Android OS.
30 The most popular CV application scenario for an Android operated device is starting camera in preview mode, applying some CV algo to every frame
31 and displaying the preview frames modified by that CV algo.
33 Let's consider how we can use OpenCL in this scenario. In particular let's try two ways: direct calls to OpenCL API and recently introduced OpenCV T-API
34 (aka [Transparent API](https://docs.google.com/presentation/d/1qoa29N_B-s297-fp0-b3rBirvpzJQp8dCtllLQ4DVCY/present)) - implicit OpenCL accelerations of some OpenCV algo-s.
39 Starting Android API level 11 (Android 3.0) [Camera API](http://developer.android.com/reference/android/hardware/Camera.html)
40 allows use of OpenGL texture as a target for preview frames.
41 Android API level 21 brings a new [Camera2 API](http://developer.android.com/reference/android/hardware/camera2/package-summary.html)
42 that provides much more control over the camera settings and usage modes,
43 it allows several targets for preview frames and OpenGL texture in particular.
45 Having a preview frame in an OpenGL texture is a good deal for using OpenCL because there is an
46 [OpenGL-OpenCL Interoperability API (cl_khr_gl_sharing)](https://www.khronos.org/registry/cl/sdk/1.2/docs/man/xhtml/cl_khr_gl_sharing.html),
47 allowing sharing OpenGL texture data with OpenCL functions without copying (with some restrictions of course).
49 Let's create a base for our application that just configures Android camera to send preview frames to OpenGL texture and displays these frames
50 on display without any processing.
52 A minimal `Activity` class for that purposes looks like following:
55 public class Tutorial4Activity extends Activity {
57 private MyGLSurfaceView mView;
60 public void onCreate(Bundle savedInstanceState) {
61 super.onCreate(savedInstanceState);
62 requestWindowFeature(Window.FEATURE_NO_TITLE);
63 getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN,
64 WindowManager.LayoutParams.FLAG_FULLSCREEN);
65 getWindow().setFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON,
66 WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
67 setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);
69 mView = new MyGLSurfaceView(this);
70 setContentView(mView);
74 protected void onPause() {
80 protected void onResume() {
87 And a minimal `View` class respectively:
90 public class MyGLSurfaceView extends GLSurfaceView {
92 MyGLRendererBase mRenderer;
94 public MyGLSurfaceView(Context context) {
97 if(android.os.Build.VERSION.SDK_INT >= 21)
98 mRenderer = new Camera2Renderer(this);
100 mRenderer = new CameraRenderer(this);
102 setEGLContextClientVersion(2);
103 setRenderer(mRenderer);
104 setRenderMode(GLSurfaceView.RENDERMODE_WHEN_DIRTY);
108 public void surfaceCreated(SurfaceHolder holder) {
109 super.surfaceCreated(holder);
113 public void surfaceDestroyed(SurfaceHolder holder) {
114 super.surfaceDestroyed(holder);
118 public void surfaceChanged(SurfaceHolder holder, int format, int w, int h) {
119 super.surfaceChanged(holder, format, w, h);
123 public void onResume() {
125 mRenderer.onResume();
129 public void onPause() {
136 __Note__: we use two renderer classes: one for legacy [Camera](http://developer.android.com/reference/android/hardware/Camera.html) API
137 and another for modern [Camera2](http://developer.android.com/reference/android/hardware/camera2/package-summary.html).
139 A minimal `Renderer` class can be implemented in Java (OpenGL ES 2.0 [available](http://developer.android.com/reference/android/opengl/GLES20.html) in Java),
140 but since we are going to modify the preview texture with OpenCL let's move OpenGL stuff to JNI.
141 Here is a simple Java wrapper for our JNI stuff:
144 public class NativeGLRenderer {
147 System.loadLibrary("opencv_java3"); // comment this when using OpenCV Manager
148 System.loadLibrary("JNIrender");
151 public static native int initGL();
152 public static native void closeGL();
153 public static native void drawFrame();
154 public static native void changeSize(int width, int height);
158 Since `Camera` and `Camera2` APIs differ significantly in camera setup and control, let's create a base class for the two corresponding renderers:
161 public abstract class MyGLRendererBase implements GLSurfaceView.Renderer, SurfaceTexture.OnFrameAvailableListener {
162 protected final String LOGTAG = "MyGLRendererBase";
164 protected SurfaceTexture mSTex;
165 protected MyGLSurfaceView mView;
167 protected boolean mGLInit = false;
168 protected boolean mTexUpdate = false;
170 MyGLRendererBase(MyGLSurfaceView view) {
174 protected abstract void openCamera();
175 protected abstract void closeCamera();
176 protected abstract void setCameraPreviewSize(int width, int height);
178 public void onResume() {
179 Log.i(LOGTAG, "onResume");
182 public void onPause() {
183 Log.i(LOGTAG, "onPause");
190 NativeGLRenderer.closeGL();
195 public synchronized void onFrameAvailable(SurfaceTexture surfaceTexture) {
196 //Log.i(LOGTAG, "onFrameAvailable");
198 mView.requestRender();
202 public void onDrawFrame(GL10 gl) {
203 //Log.i(LOGTAG, "onDrawFrame");
207 synchronized (this) {
209 mSTex.updateTexImage();
213 NativeGLRenderer.drawFrame();
217 public void onSurfaceChanged(GL10 gl, int surfaceWidth, int surfaceHeight) {
218 Log.i(LOGTAG, "onSurfaceChanged("+surfaceWidth+"x"+surfaceHeight+")");
219 NativeGLRenderer.changeSize(surfaceWidth, surfaceHeight);
220 setCameraPreviewSize(surfaceWidth, surfaceHeight);
224 public void onSurfaceCreated(GL10 gl, EGLConfig config) {
225 Log.i(LOGTAG, "onSurfaceCreated");
226 String strGLVersion = GLES20.glGetString(GLES20.GL_VERSION);
227 if (strGLVersion != null)
228 Log.i(LOGTAG, "OpenGL ES version: " + strGLVersion);
230 int hTex = NativeGLRenderer.initGL();
231 mSTex = new SurfaceTexture(hTex);
232 mSTex.setOnFrameAvailableListener(this);
239 As you can see, inheritors for `Camera` and `Camera2` APIs should implement the following abstract methods:
241 protected abstract void openCamera();
242 protected abstract void closeCamera();
243 protected abstract void setCameraPreviewSize(int width, int height);
246 Let's leave the details of their implementation beyond of this tutorial, please refer the
247 [source code](https://github.com/opencv/opencv/tree/3.4/samples/android/tutorial-4-opencl/) to see them.
249 Preview Frames modification
250 ---------------------------
252 The details OpenGL ES 2.0 initialization are also quite straightforward and noisy to be quoted here,
253 but the important point here is that the OpeGL texture to be the target for camera preview should be of type `GL_TEXTURE_EXTERNAL_OES`
254 (not `GL_TEXTURE_2D`), internally it keeps picture data in _YUV_ format.
255 That makes unable sharing it via CL-GL interop (`cl_khr_gl_sharing`) and accessing its pixel data via C/C++ code.
256 To overcome this restriction we have to perform an OpenGL rendering from this texture to another regular `GL_TEXTURE_2D` one
257 using _FrameBuffer Object_ (aka FBO).
261 After that we can read (_copy_) pixel data from C/C++ via `glReadPixels()` and write them back to texture after modification via `glTexSubImage2D()`.
263 ### Direct OpenCL calls
265 Also that `GL_TEXTURE_2D` texture can be shared with OpenCL without copying, but we have to create OpenCL context with special way for that:
270 EGLDisplay mEglDisplay = eglGetCurrentDisplay();
271 if (mEglDisplay == EGL_NO_DISPLAY)
272 LOGE("initCL: eglGetCurrentDisplay() returned 'EGL_NO_DISPLAY', error = %x", eglGetError());
274 EGLContext mEglContext = eglGetCurrentContext();
275 if (mEglContext == EGL_NO_CONTEXT)
276 LOGE("initCL: eglGetCurrentContext() returned 'EGL_NO_CONTEXT', error = %x", eglGetError());
278 cl_context_properties props[] =
279 { CL_GL_CONTEXT_KHR, (cl_context_properties) mEglContext,
280 CL_EGL_DISPLAY_KHR, (cl_context_properties) mEglDisplay,
281 CL_CONTEXT_PLATFORM, 0,
286 cl::Platform p = cl::Platform::getDefault();
287 std::string ext = p.getInfo<CL_PLATFORM_EXTENSIONS>();
288 if(ext.find("cl_khr_gl_sharing") == std::string::npos)
289 LOGE("Warning: CL-GL sharing isn't supported by PLATFORM");
290 props[5] = (cl_context_properties) p();
292 theContext = cl::Context(CL_DEVICE_TYPE_GPU, props);
293 std::vector<cl::Device> devs = theContext.getInfo<CL_CONTEXT_DEVICES>();
294 LOGD("Context returned %d devices, taking the 1st one", devs.size());
295 ext = devs[0].getInfo<CL_DEVICE_EXTENSIONS>();
296 if(ext.find("cl_khr_gl_sharing") == std::string::npos)
297 LOGE("Warning: CL-GL sharing isn't supported by DEVICE");
299 theQueue = cl::CommandQueue(theContext, devs[0]);
305 LOGE("cl::Error: %s (%d)", e.what(), e.err());
307 catch(std::exception& e)
309 LOGE("std::exception: %s", e.what());
313 LOGE( "OpenCL info: unknown error while initializing OpenCL stuff" );
315 LOGD("initCL completed");
319 @note To build this JNI code you need __OpenCL 1.2__ headers from [Khronos web site](https://www.khronos.org/registry/cl/api/1.2/) and
320 the __libOpenCL.so__ downloaded from the device you'll run the application.
322 Then the texture can be wrapped by a `cl::ImageGL` object and processed via OpenCL calls:
324 cl::ImageGL imgIn (theContext, CL_MEM_READ_ONLY, GL_TEXTURE_2D, 0, texIn);
325 cl::ImageGL imgOut(theContext, CL_MEM_WRITE_ONLY, GL_TEXTURE_2D, 0, texOut);
327 std::vector < cl::Memory > images;
328 images.push_back(imgIn);
329 images.push_back(imgOut);
330 theQueue.enqueueAcquireGLObjects(&images);
333 cl::Kernel Laplacian = ...
334 Laplacian.setArg(0, imgIn);
335 Laplacian.setArg(1, imgOut);
338 theQueue.enqueueNDRangeKernel(Laplacian, cl::NullRange, cl::NDRange(w, h), cl::NullRange);
341 theQueue.enqueueReleaseGLObjects(&images);
347 But instead of writing OpenCL code by yourselves you may want to use __OpenCV T-API__ that calls OpenCL implicitly.
348 All that you need is to pass the created OpenCL context to OpenCV (via `cv::ocl::attachContext()`) and somehow wrap OpenGL texture with `cv::UMat`.
349 Unfortunately `UMat` keeps OpenCL _buffer_ internally, that can't be wrapped over either OpenGL _texture_ or OpenCL _image_ - so we have to copy image data here:
351 cl::ImageGL imgIn (theContext, CL_MEM_READ_ONLY, GL_TEXTURE_2D, 0, tex);
352 std::vector < cl::Memory > images(1, imgIn);
353 theQueue.enqueueAcquireGLObjects(&images);
356 cv::UMat uIn, uOut, uTmp;
357 cv::ocl::convertFromImage(imgIn(), uIn);
358 theQueue.enqueueReleaseGLObjects(&images);
360 cv::Laplacian(uIn, uTmp, CV_8U);
361 cv:multiply(uTmp, 10, uOut);
364 cl::ImageGL imgOut(theContext, CL_MEM_WRITE_ONLY, GL_TEXTURE_2D, 0, tex);
366 images.push_back(imgOut);
367 theQueue.enqueueAcquireGLObjects(&images);
368 cl_mem clBuffer = (cl_mem)uOut.handle(cv::ACCESS_READ);
369 cl_command_queue q = (cl_command_queue)cv::ocl::Queue::getDefault().ptr();
371 size_t origin[3] = { 0, 0, 0 };
372 size_t region[3] = { w, h, 1 };
373 CV_Assert(clEnqueueCopyBufferToImage (q, clBuffer, imgOut(), offset, origin, region, 0, NULL, NULL) == CL_SUCCESS);
374 theQueue.enqueueReleaseGLObjects(&images);
378 - @note We have to make one more image data copy when placing back the modified image to the original OpenGL texture via OpenCL image wrapper.
379 - @note By default the OpenCL support (T-API) is disabled in OpenCV builds for Android OS (so it's absent in official packages as of version 3.0),
380 but it's possible to rebuild locally OpenCV for Android with OpenCL/T-API enabled: use `-DWITH_OPENCL=YES` option for CMake.
382 cd opencv-build-android
383 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
384 path/to/ninja.exe install/strip
386 To use your own modified `libopencv_java3.so` you have to keep inside your APK, not to use OpenCV Manager and load it manually via `System.loadLibrary("opencv_java3")`.
391 To compare the performance we measured FPS of the same preview frames modification (_Laplacian_) done by C/C++ code (call to `cv::Laplacian` with `cv::Mat`),
392 by direct OpenCL calls (using OpenCL _images_ for input and output), and by OpenCV _T-API_ (call to `cv::Laplacian` with `cv::UMat`) on _Sony Xperia Z3_ with 720p camera resolution:
393 * __C/C++ version__ shows __3-4 fps__
394 * __direct OpenCL calls__ shows __25-27 fps__
395 * __OpenCV T-API__ shows __11-13 fps__ (due to extra copying from `cl_image` to `cl_buffer` and back)