Edited at
OpenCVDay 17

THETAの中でOpenCVを動かす

本記事はOpenCV Advent Calendar 2018の17日目の記事です。

RICOH THETAというカメラの中で、OpenCVを動かしてみました。

Android Studio + NDK (ndk-build)でOpenCVを使ったアプリをビルドしたい、という方にも参考になれば幸いです。


はじめに

こんにちは、リコーの@roohii_3です。

弊社でRICOH THETAという全周囲360度撮れるカメラを出してるのですが、このカメラの最新機種(2018年12月現在)のRICOH THETA Vは、Androidで動いています。

Androidアプリを作る感覚でTHETAをカスタマイズすることもでき、そのカスタマイズ機能を「プラグイン」と呼んでいます。

THETAプラグインでOpenCV+クラウドなどを組み合わせば、何かを認識して撮影・クラウドに送信、のようなIoT的な使い方もできそうです。

ということで、そのことはじめとしてTHETAプラグインでOpenCVを動かしてみました。


開発環境


  • OpenCV Android pack ver. 3.4.4

  • Android Studio ver. 3.2.1

  • JDK ver. 1.8.0_191

  • NDK ver. 18.1.5063045

  • gradle ver. 4.6

  • RICOH THETA V Firmware ver. 2.50.1
    (Android ver. 7.1.2 / API level 25)


事前準備


OpenCV Android pack



  1. OpenCVのReleasesページから"Android pack"の最新版をダウンロード

  2. ダウンロードしたファイルを解凍後、任意の場所に配置

    私は ”OpenCV-android-sdk” → ”OpenCV-3.4.4-android-sdk” にリネームし、以下の場所に置きました。

C:/opencv/OpenCV-3.4.4-android-sdk


Android開発環境


Android Studio

Android Developersより、Android Studioをダウンロード・インストール


Android SDK・Tools


  1. Android Studioのメニューより、”Tools > SDK Manager”を開く

  2. SDK Platforms”タブより、Androidのバージョンに応じて必要なものにチェックを入れる

    SDK_download.png

  3. SDK Tools”タブより、以下のものにチェックが入っていなかったら入れておく


    • Android SDK Build-Tools

    • Android SDK Platform-Tools

    • Android SDK Tools

    • Google USB Driver

    • NDK



  4. "OK"をクリック


JDK

以下より、”Java SE Developer Kit 8”をダウンロード・インストール

https://www.oracle.com/technetwork/java/javase/downloads/jdk8-downloads-2133151.html


RICOH THETA V

THETAを使う場合は、以下のリンクを参考にして開発者モードにしておく

【THETAプラグイン開発】プラグインパートナープログラムに登録してみよう

【THETAプラグイン開発】THETAを開発者モードにする手順


OpenCV for Android

OpenCVの公式ドキュメントにある"Android Development with OpenCV"によると、AndroidでOpenCVを使うにはいくつか方法があるようです。


  1. Javaで使う方法

    1.1. OpenCV Managerを使う方法

    1.2. ライブラリを静的リンクさせる方法


  2. NDKによってC/C++で使う方法

    ※ビルドに、"CMake"を使う方法、"ndk-build"を使う方法の2種類がある模様。


OpenCV公式では「1.1. OpenCV Managerを使う方法」を推奨しているみたいです1 2

しかし、THETAは常時ネットワークに繋がっていないため、OpenCV Managerの特徴を生かせられません。

そのため、1.2.の方法か2.の方法を使うことになります。

今回は、「2. NDKによってC/C++で使う方法」で実装することにします。

ビルドには”ndk-build”を使います。


プロジェクトファイル・NDKビルド環境の準備

1. Android Studioプロジェクトの新規作成

Android Studioを開き、メニューの"File > New Project"を選択します。画面に従って"Empty Activity"を作成します。

"Include C++ support"のチェックは不要です。これにチェックを入れるとCMakeList.txtが作成されるようですが、今回はndk-buildでビルドするのでチェックの必要はありません。

2. ビルドファイルの作成

"app"ディレクトリの下に"jni"ディレクトリを作成し、"Android.mk"と"Application.mk"を作成します。

mk_file.png

それぞれのファイルには、以下のように記入します。

【Android.mk】


  • 7行目のincludeに、OpenCV Android packの中にある"OpenCV.mk"のパスを記入


  • LOCAL_MODULEに、任意の名称(NDKで生成されるライブラリ名)を設定


  • LOCAL_SRC_FILESに、C/C++(Nativeコード)のソース名を設定

    ※Nativeコードが複数ある場合は、空白で区切るようです3


Android.mk

LOCAL_PATH := $(call my-dir)

include $(CLEAR_VARS)

OPENCV_INSTALL_MODULES:=on
OPENCV_LIB_TYPE:=SHARED
include C:\opencv\OpenCV-3.4.4-android-sdk\sdk\native\jni\OpenCV.mk

LOCAL_MODULE := opencvsample
LOCAL_SRC_FILES := sample.cpp
include $(BUILD_SHARED_LIBRARY)


【Application.mk】



  • APP_ABIには、プラットフォームに応じて適当なものを設定


Application.mk

APP_STL := c++_static

APP_CPPFLAGS := -frtti -fexceptions
APP_ABI := armeabi-v7a

3. OpenCVライブラリ(.soファイル)のコピー

"app"ディレクトリ下に"jniLibs"ディレクトリを作成し、OpenCVライブラリ(.soファイル)をコピー&ペーストします。

プラットフォームに応じて、必要なライブラリファイルをディレクトリごとコピペします。

コピー元: C:/(OpenCV Android packがある場所)/sdk/native/libs/armeabi-v7a

コピー先: C:/(プロジェクトファイルがある場所)/app/jniLibs/armeabi-v7a

sofile.png

4. build.gradleの編集

build.gradle(Module:app)を開き、下記のようにNDK設定を追記します。



  • moduleNameには、"Android.mk"で設定したLOCAL_MODULEの名前の先頭に"lib"をつけたものを設定する


  • abiFiltersの設定値は、プラットフォームに応じて適当なものを設定する


build.gradle

android {

...
defaultConfig {
...
ndk {
moduleName "libopencvsample"
abiFilters 'armeabi-v7a'
}
}
}

エディタウィンドウ上部に"Sync Now"が出たら、それをクリック

5. ビルドシステムの設定

プロジェクトツリーで"app"を右クリックし、"Link C++ Project with Gradle"を選択します。

開いたウィンドウのBuild Systemに "ndk-build" 、Project Pathに "Android.mk" のパスを指定し、OKをクリックします。

setbuildsys.png


実装

"drawable"に置いた"lena.jpg"を開き、Nativeコード内でOpenCVを動かして色空間をRGB→BGRに変換、それをImageView上に表示させる、というものを作ってみます。


layout

バージョンを表示させるTextViewと、処理後の画像を表示させるImageViewを配置しています。


activity_main.xml

<?xml version="1.0" encoding="utf-8"?>

<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">

<TextView
android:id="@+id/textView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent" />

<ImageView
android:id="@+id/imageView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"/>

</android.support.constraint.ConstraintLayout>



Javaコード

"Android.mk"でLOCAL_MODULEに設定したライブラリ名の先頭に"lib"を付け、System.loadLibrary()で読み込みます。

Nativeコード関数は、public native String version();のように宣言しておきます。


MainActivity.java

package com.theta360.opencvsample;

import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.widget.ImageView;
import android.widget.TextView;

import java.nio.ByteBuffer;

public class MainActivity extends AppCompatActivity {

// load native library
static {
System.loadLibrary("opencvsample");
}

private TextView mTextView;
private ImageView mImageView;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);

// OpenCV version
mTextView = (TextView) findViewById(R.id.textView);
mTextView.setText("OpenCV version: " + version());

// load the picture from the drawable resource
Bitmap img = BitmapFactory.decodeResource(getResources(), R.drawable.lena);

// get the byte array from the Bitmap instance
ByteBuffer byteBuffer = ByteBuffer.allocate(img.getByteCount());
img.copyPixelsToBuffer(byteBuffer);

// call the process from the native library
byte[] dst = rgba2bgra(img.getWidth(), img.getHeight(), byteBuffer.array());

// set the output image on an ImageView
Bitmap bmp = Bitmap.createBitmap(img.getWidth(), img.getHeight(), Bitmap.Config.ARGB_8888);
bmp.copyPixelsFromBuffer(ByteBuffer.wrap(dst));
mImageView = (ImageView) findViewById(R.id.imageView);
mImageView.setImageBitmap(bmp);
}

// native functions
public native String version();
public native byte[] rgba2bgra(int width, int height, byte[] src);
}



Nativeコード

OpenCVの処理はC/C++で行います。

"app > jni"下に任意の名前でC/C++ファイルを作成します。

"Android.mk"のLOCAL_SRC_FILESには、作成したファイルの名称を設定しておきます。

cppsource.png

Java ↔ Native(C/C++)のやり取りで用いる関数には、特殊なルールがあるようです。

以下のように、定義します。

JNIEXPORT [戻り値の型] JNICALL Java_[最上位ソースディレクトリからのJavaソースの相対パス]_[呼び出し元のJavaのクラス名]_[関数名](JNIEnv *, jobject, [引数], ...))

戻り値や引数の型も特殊のものを使うようです。

詳しくは、JNIの型とデータ構造等を参考にしてください。


sample.cpp

#include <jni.h>

#include <string>
#include <opencv2/core.hpp>
#include <cv.hpp>

extern "C"
{
JNIEXPORT jstring JNICALL
Java_com_theta360_opencvsample_MainActivity_version(
JNIEnv *env,
jobject) {
std::string version = cv::getVersionString();
return env->NewStringUTF(version.c_str());
}

JNIEXPORT jbyteArray
JNICALL Java_com_theta360_opencvsample_MainActivity_rgba2bgra
(
JNIEnv *env,
jobject obj,
jint w,
jint h,
jbyteArray src
) {
// 要素列の取得
// 最後に開放する必要がある
jbyte *p_src = env->GetByteArrayElements(src, NULL);
if (p_src == NULL) {
return NULL;
}

// 配列をcv::Matに変換する
cv::Mat m_src(h, w, CV_8UC4, (u_char *) p_src);
cv::Mat m_dst(h, w, CV_8UC4);

// OpenCV process
cv::cvtColor(m_src, m_dst, CV_RGBA2BGRA);

// 配列をcv::Matから取り出す
u_char *p_dst = m_dst.data;

// 戻り値用に要素を割り当てる
jbyteArray dst = env->NewByteArray(w * h * 4);
if (dst == NULL) {
env->ReleaseByteArrayElements(src, p_src, 0);
return NULL;
}
env->SetByteArrayRegion(dst, 0, w * h * 4, (jbyte *) p_dst);

// release
env->ReleaseByteArrayElements(src, p_src, 0);

return dst;
}
}



実行

実行すると、下記のように表示されます。

※THETAには画面がないので、Vysorを使っています。

screenshot.jpg


おわりに

THETA内でOpenCVの動作確認をしてみただけなので、もうちょっと360度映像の特性を活かしたものを作ってみたいです。

何かできたらまた書こうと思います。

続編書きました↓

THETAの中でOpenCVを動かす【プレビューフレーム取得編】(2019/05/16 追記)

THETAの中でOpenCVを動かす【プレビューフレーム応用編】(2019/06/05 追記)

明日は@n_uehさんの「OpenCV×VUIについて」の記事です。


RICOH THETAプラグインパートナープログラムについて

THETAプラグインに興味を持たれた方がいれば、以下の記事もぜひご覧ください。

RICOH THETAプラグイン開発者コミュニティでは、他にも記事を書いています。

RICOH THETAプラグインについてはこちら。興味を持たれた方はtwitterのフォローとTHETAプラグイン開発コミュニティ(slack)への参加もぜひどうぞ。





  1. ドキュメントに、”Using async initialization is a recommended way for application development. It uses the OpenCV Manager to access OpenCV libraries externally installed in the target system.”とあります。 



  2. OpenCV Managerについては → [Android OpenCV Manager]に詳細が書いてありました。ver.2.4のドキュメントなので情報が古いかもですが。 



  3. https://developer.android.com/ndk/guides/android_mk?hl=ja