前のチュートリアル: 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のビルド方法
- 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)
- 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);
}
@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;
}
}
- 覚え書き
- ここでは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) {
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
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
{
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コードを書く代わりに、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();
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のimageラッパーを介して元のOpenGLテクスチャに戻す際に、もう一度画像データをコピーする必要がある。
パフォーマンスに関する注意点
パフォーマンスを比較するため、同じプレビューフレームの変更処理(Laplacian)を、C/C++コード(cv::Laplacian を cv::Mat で呼び出す)、直接のOpenCL呼び出し(入力と出力にOpenCLの images を使用)、およびOpenCVの T-API(cv::Laplacian を cv::UMat で呼び出す)で実行し、720pのカメラ解像度の Sony Xperia Z3 上でFPSを測定した:
- C/C++版 は 3-4 fps を示す
- 直接のOpenCL呼び出し は 25-27 fps を示す
- OpenCV T-API は 11-13 fps を示す(
cl_image から cl_buffer への往復の余分なコピーが原因)