上一個教程: 如何在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或releases下載OpenCV原始碼,並按照wiki上的說明進行構建。
它還假設您熟悉Android Java和JNI程式設計基礎。如果您需要上述任何方面的幫助,可以參考我們的Android開發簡介指南。
本教程還假設您擁有一個支援OpenCL的Android裝置。
相關原始碼位於OpenCV示例中的opencv/samples/android/tutorial-4-opencl目錄。
如何構建帶有OpenCL的自定義OpenCV Android SDK
- 組裝和配置Android OpenCL SDK。 示例的JNI部分依賴於標準的Khornos OpenCL標頭檔案、OpenCL的C++包裝器和libOpenCL.so。標準的OpenCL標頭檔案可以從OpenCV倉庫中的3rdparty目錄或您的Linux發行版軟體包中複製。C++包裝器可在官方Khronos Github倉庫中獲取。將標頭檔案按以下方式複製到專用目錄中:
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。 OpenCL支援(T-API)在預設的Android OS版OpenCV中是停用的,但可以透過使用CMake的
-DWITH_OPENCL=ON選項在本地重新構建支援OpenCL/T-API的OpenCV for Android。您還需要指定Android OpenCL SDK的路徑:使用CMake的-DANDROID_OPENCL_SDK=path_to_your_Android_OpenCL_SDK選項。如果您正在使用build_sdk.py構建OpenCV,請遵循wiki上的說明。在您的.config.py(例如ndk-18-api-level-21.config.py)中設定這些CMake引數: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(又稱透明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 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。
最簡單的Renderer類可以用Java實現(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初始化的細節也相當直接和冗長,在此不贅述,但這裡的重點是作為相機預覽目標的OpenGL紋理型別應為GL_TEXTURE_EXTERNAL_OES(而非GL_TEXTURE_2D),它內部以YUV格式儲存影像資料。這使得無法透過CL-GL互操作(cl_khr_gl_sharing)共享它,也無法透過C/C++程式碼訪問其畫素資料。為克服此限制,我們必須使用幀緩衝區物件(aka FBO)將此紋理渲染到另一個常規GL_TEXTURE_2D紋理中。
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
{
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");
}
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;
}
然後紋理可以被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() costs %d ms", getTimeInterval(t));
t = getTimeMs();
cl::Kernel
Laplacian(theProgI2I,
"Laplacian");
theQueue.finish();
LOGD("Kernel() costs %d ms", getTimeInterval(t));
t = getTimeMs();
theQueue.enqueueNDRangeKernel(Laplacian, cl::NullRange, cl::NDRange(w, h), cl::NullRange);
theQueue.finish();
LOGD("enqueueNDRangeKernel() costs %d ms", getTimeInterval(t));
t = getTimeMs();
theQueue.enqueueReleaseGLObjects(&images);
theQueue.finish();
LOGD("enqueueReleaseGLObjects() costs %d ms", getTimeInterval(t));
OpenCV T-API
但是,您可能希望使用隱式呼叫OpenCL的OpenCV T-API,而不是自己編寫OpenCL程式碼。您需要做的就是將建立的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("loading texture data to OpenCV UMat costs %d ms", getTimeInterval(t));
theQueue.enqueueReleaseGLObjects(&images);
t = getTimeMs();
LOGD("OpenCV processing costs %d ms", 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("uploading results to texture costs %d ms", getTimeInterval(t));
- 注意
- 在將修改後的影像透過OpenCL影像包裝器放回原始OpenGL紋理時,我們必須再進行一次影像資料複製。
效能說明
為了比較效能,我們測量了在Sony Xperia Z3手機上,在720p相機解析度下,透過C/C++程式碼(呼叫帶有cv::Mat的cv::Laplacian)、直接OpenCL呼叫(使用OpenCL影像作為輸入和輸出)以及OpenCV T-API(呼叫帶有cv::UMat的cv::Laplacian)對相同預覽幀修改(拉普拉斯運算元)的FPS表現:
- C/C++版本顯示 3-4 fps
- 直接OpenCL呼叫顯示 25-27 fps
- OpenCV T-API顯示 11-13 fps(由於從
cl_image到cl_buffer再返回的額外複製)