Help us understand the problem. What is going on with this article?

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 

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした