OpenCV 4.13.0
Open Source Computer Vision
読み込み中...
検索中...
見つかりません
🤖 AIによる機械翻訳(非公式) — これは OpenCV 4.13.0 公式リファレンス(英語)を AI (Claude) で自動翻訳したものです。訳に誤りを含む場合があります。正確な情報は 公式英語版(原文) を参照してください。
AndroidカメラプレビューベースのアプリケーションでのOpenCLの利用

前のチュートリアル: Androidデバイス上でディープネットワークを実行する方法
次のチュートリアル: MacOSへのインストール

原著者Andrey Pavlenko, Alexander Panov
互換性OpenCV >= 4.9

このガイドは、Androidカメラプレビューをベースとするコンピュータビジョンアプリケーションで OpenCL ™ を利用する手助けをするために作成された。チュートリアルは Android Studio 2022.2.1 向けに書かれている。Ubuntu 22.04 でテストした。

このチュートリアルでは、以下がインストールおよび設定されていることを前提とする:

  • Android Studio (2022.2.1.+)
  • JDK 17
  • Android SDK
  • Android NDK (25.2.9519653+)
  • OpenCVのソースコードを github または リリース からダウンロードし、wikiの手順 に従ってビルドする。

また、Android Java と JNI プログラミングの基礎に習熟していることを前提とする。上記のいずれかについて助けが必要な場合は、Android開発入門 ガイドを参照してほしい。

このチュートリアルでは、OpenCLが有効なAndroid搭載デバイスを所有していることも前提とする。

関連するソースコードは、OpenCVサンプルの opencv/samples/android/tutorial-4-opencl ディレクトリ内にある。

OpenCLを有効にしたカスタムOpenCV Android SDKのビルド方法

  1. Android OpenCL SDKを組み立てて設定する。 サンプルのJNI部分は、標準のKhronos OpenCLヘッダ、OpenCL用のC++ラッパー、および libOpenCL.so に依存している。標準のOpenCLヘッダは、OpenCVリポジトリの3rdpartyディレクトリ、またはLinuxディストリビューションのパッケージからコピーできる。C++ラッパーは Github上の公式Khronosリポジトリ で入手できる。次の方法でヘッダファイルを専用のディレクトリにコピーする:
    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ビルドではデフォルトで無効になっている。しかし、OpenCL/T-APIを有効にしてOpenCVをAndroid向けにローカルで再ビルドすることは可能である:CMakeに -DWITH_OPENCL=ON オプションを使用する。また、Android OpenCL SDKへのパスを指定する必要がある:CMakeに -DANDROID_OPENCL_SDK=path_to_your_Android_OpenCL_SDK オプションを使用する。build_sdk.py を使ってOpenCVをビルドする場合は、wikiの手順 に従う。これらのCMakeパラメータを .config.py(例えば ndk-18-api-level-21.config.py)に設定する:
    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 ..

序文

アプリケーションのパフォーマンス向上のために GPGPU をOpenCL経由で利用することは、今や非常に現代的なトレンドである。一部のコンピュータビジョンアルゴリズム(例えば画像フィルタリング)は、CPU上よりもGPU上ではるかに高速に動作する。最近、これがAndroid OS上でも可能になった。

Android搭載デバイスで最も一般的なコンピュータビジョンアプリケーションのシナリオは、カメラをプレビューモードで起動し、各フレームに何らかのコンピュータビジョンアルゴリズムを適用して、そのアルゴリズムで変更したプレビューフレームを表示するというものである。

このシナリオでOpenCLをどのように利用できるかを考えてみよう。特に、2つの方法を試してみる:OpenCL APIの直接呼び出しと、最近導入されたOpenCV T-API(別名 Transparent 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) が存在し、(もちろんいくつかの制約はあるが)コピーすることなくOpenGLテクスチャのデータをOpenCL関数と共有できるからである。

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;
}
}
覚え書き
ここでは2つのレンダラークラスを使用する:1つはレガシーの Camera API用、もう1つはモダンな 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 APIと Camera2 APIはカメラのセットアップと制御において大きく異なるので、対応する2つのレンダラーのための基底クラスを作成しよう:

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
Definition cvstd.hpp:151

ご覧のとおり、Camera APIと 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++コードからのピクセルデータへのアクセスができない。この制約を回避するには、FrameBuffer Object(別名FBO)を使って、このテクスチャから別の通常の GL_TEXTURE_2D テクスチャへのOpenGLレンダリングを実行する必要がある。

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コードを書く代わりに、OpenCLを暗黙的に呼び出す OpenCV T-API を使いたい場合もあるだろう。必要なのは、作成したOpenCLコンテキストをOpenCVに渡し(cv::ocl::attachContext() を介して)、何らかの方法でOpenGLテクスチャを cv::UMat でラップすることだけである。残念ながら UMat は内部にOpenCLの buffer を保持しており、これはOpenGLの texture にもOpenCLの image にもラップできない。そのため、ここで画像データをコピーする必要がある:

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のimageラッパーを介して元のOpenGLテクスチャに戻す際に、もう一度画像データをコピーする必要がある。

パフォーマンスに関する注意点

パフォーマンスを比較するため、同じプレビューフレームの変更処理(Laplacian)を、C/C++コード(cv::Laplaciancv::Mat で呼び出す)、直接のOpenCL呼び出し(入力と出力にOpenCLの images を使用)、およびOpenCVの T-APIcv::Laplaciancv::UMat で呼び出す)で実行し、720pのカメラ解像度の Sony Xperia Z3 上でFPSを測定した:

  • C/C++版3-4 fps を示す
  • 直接のOpenCL呼び出し25-27 fps を示す
  • OpenCV T-API11-13 fps を示す(cl_image から cl_buffer への往復の余分なコピーが原因)