OpenCV 4.12.0
開源計算機視覺
載入中...
搜尋中...
無匹配項
在基於 Android 相機預覽的 CV 應用程式中使用 OpenCL

上一個教程: 如何在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+)
  • githubreleases下載OpenCV原始碼,並按照wiki上的說明進行構建。

它還假設您熟悉Android Java和JNI程式設計基礎。如果您需要上述任何方面的幫助,可以參考我們的Android開發簡介指南。

本教程還假設您擁有一個支援OpenCL的Android裝置。

相關原始碼位於OpenCV示例中的opencv/samples/android/tutorial-4-opencl目錄。

如何構建帶有OpenCL的自定義OpenCV Android SDK

  1. 組裝和配置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)
  2. 構建帶有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);
//NativePart.initCL();
}
@Override
public void surfaceDestroyed(SurfaceHolder holder) {
//NativePart.closeCL();
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) {
// FPS
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);
}

由於CameraCamera2 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;
}
}
std::string String
定義 cvstd.hpp:151

如您所見,CameraCamera2 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
{
haveOpenCL = false;
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);
cv::ocl::attachContext(p.getInfo<CL_PLATFORM_NAME>(), p(), theContext(), devs[0]());
LOGD("OpenCV+OpenCL works OK!");
else
LOGE("Can't init OpenCV with OpenCL TAPI");
haveOpenCL = true;
}
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"); //TODO: may be done once
Laplacian.setArg(0, imgIn);
Laplacian.setArg(1, imgOut);
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();
cv::UMat uIn, uOut, uTmp;
LOGD("loading texture data to OpenCV UMat costs %d ms", getTimeInterval(t));
theQueue.enqueueReleaseGLObjects(&images);
t = getTimeMs();
//cv::blur(uIn, uOut, cv::Size(5, 5));
cv::Laplacian(uIn, uTmp, CV_8U);
cv:multiply(uTmp, 10, uOut);
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);
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] = { (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::Matcv::Laplacian)、直接OpenCL呼叫(使用OpenCL影像作為輸入和輸出)以及OpenCV T-API(呼叫帶有cv::UMatcv::Laplacian)對相同預覽幀修改(拉普拉斯運算元)的FPS表現:

  • C/C++版本顯示 3-4 fps
  • 直接OpenCL呼叫顯示 25-27 fps
  • OpenCV T-API顯示 11-13 fps(由於從cl_imagecl_buffer再返回的額外複製)