万年アドベントカレンダーを実装した（OpenCV+Deep Neural Network+RoBoHoN）

# はじめに
この記事は [OpenCV Advent Calendar 2017](https://qiita.com/advent-calendar/2017/opencv) の 20日目の記事です。

# (12/20 PM 追記)
- SHARP 公式さんから言及をいただきました、ありがとうございます！
<blockquote class="twitter-tweet" data-lang="ja"><p lang="ja" dir="ltr">オーナーさんによるロボホンのアプリ実装例 → 万年アドベントカレンダーを実装した（OpenCV+Deep Neural Network+RoBoHoN） <a href="https://t.co/HmoOC2Rvps">https://t.co/HmoOC2Rvps</a></p>&mdash; SHARP シャープ株式会社 (@SHARP_JP) <a href="https://twitter.com/SHARP_JP/status/943311766112518144?ref_src=twsrc%5Etfw">2017年12月20日</a></blockquote>
<script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>


#アドベントカレンダーを開け続けたい
今年初めて、我が家にリアル・アドベントカレンダーが導入されて、毎日たのしく開けています。
X'mas が近付き、のこり日数も少なくなってきて、懸念される「家族のアドベントカレンダー・ロス」の解決策として、開け続けられる万年アドベントカレンダーを実装しました。

![image.PNG](https://qiita-image-store.s3.amazonaws.com/0/121307/4f58b172-bd9d-2984-f065-e98f2766feb1.png)



#ロボホンがカレンダーを開ける
モバイル型ロボット電話 RoBoHoN は Android がベースです。
SHARP公式から、SDKもリリースされており、オーナーは自分でアプリ実装をすることができます。
実装時、Android のベース機能に加えて、SDKを通じて、RoBoHoN 独自のVoiceUIや、ID指定でのモーション振り付けなどが利用できます。

今回、カレンダーの外装として、ロボホンを利用します。

|   ロボホンハードウェアスペック[^1]   |
|:------------------|
|OS Android 5.0.2               |
|CPU Qualcomm Snapdragon 400 processor 1.2GHz (クアッドコア)               |
|メモリ ROM 16GB RAM 2GB               |
|ディスプレイ 約 2.0インチ QVGA (240*320 画素 )               |
|カメラ 約 800万画素 CMOS (3264*2448画素)               |


#開けたカレンダーの中に画像（OpenCV, ディープラーニング）
万年カレンダーの中には、画像を用意したいと思います。

ロボホン本体の中には、ユーザーがロボホンのカメラ機能で撮り貯めた写真が保存されています。
日々溜まっていくこれら写真の中から、飛行機・車・犬…　など物体を動的に認識して切り抜いた画像を、「きょうのカレンダーの中身」として表示します。

![detect.PNG](https://qiita-image-store.s3.amazonaws.com/0/121307/846092f8-a896-d2e0-b0fd-6a0f5b122731.png)

この動的な画像作成に、今年2017年8月にOpenCVメインリポジトリに昇格した dnn（Deep Neural Network）[^2]モジュールを利用します。

これまで、別々に導入設定・利用の必要があった Caffe、TensorFlow など DeepLearningフレームワークが、依存関係なくOpenCVに包含されるようになったもので、今回のような生成済モデルを利用するだけのアプリケーション実装において、とくに有難いリリースだとおもいました！

公式のこちらのチュートリアルを参考に、ロボホンに ”軽量で、モバイル端末でもリアルタイムで快適に動作”[^3]　する MobileNets による Single Shot Multibox Detector（SSD）モデルを載せていきます。

[OpenCV] How to run deep networks on Android device {#tutorial_dnn_android}
https://github.com/opencv/opencv/blob/7e680bd9ff27ef4661dc14fe5f9071ba52883f24/doc/tutorials/dnn/dnn_android/dnn_android.markdown
（上記の実装）
https://github.com/opencv/opencv/tree/master/samples/android/mobilenet-objdetect

#環境
Windows 7 SP1
Android Studio 2.3.1
RoBoHoN_SDK 1.3.0
RoBoHon端末ビルド番号　02.01.00
OpenCV  3.3.1

#ロボホンに OpenCV-android-sdk を導入する
下記などを参考に導入していきます。

[OpenCV] How to run deep networks on Android device
https://docs.opencv.org/trunk/d0/d6c/tutorial_dnn_android.html

今回のハマりどころは、build.gradle のバージョン廻り設定で、エラーが出たらそのあたりを中心に確認しました。
（openCVLibrary 側の設定値を、ロボホン（version 21）に揃えます ）

(Project) >
 app > build.gradle
 openCVLibrary331 > build.gradle
 
    compileSdkVersion 14
    buildToolsVersion "25.0.0"
    minSdkVersion 8

    ↓
    
    compileSdkVersion 21
    buildToolsVersion "25.0.2"
    minSdkVersion 21

# ロボホンに実装（応答）
起動と同時に、カレンダーを開けるモーション実行→（画像認識）→認識結果応答の流れでシナリオを実装します。

- RoBoHonシナリオ関連定義類

```java:ScenarioDefinitions.java
/**
    *  advent calendar シーン、accost、通知設定
    */
public static final String SCN_CALL = PACKAGE + ".scn.call";
public static final String SCN_RES = PACKAGE + ".scn.res";

public static final String ACC_CALL = ScenarioDefinitions.PACKAGE + ".acst.call";
public static final String ACC_RES = ScenarioDefinitions.PACKAGE + ".acst.res";

public static final String FUNC_CALL = "adv_call";

/**
    * memory_pを指定するタグ
    */
public static final String MEM_P_RES = ScenarioDefinitions.TAG_MEMORY_PERMANENT + ScenarioDefinitions.PACKAGE + ".res"; 
```
- HVML(ユーザーからの呼びかけで起動、モーション、認識結果応答)

```hvml:home.hvml
<?xml version="1.0" ?>
<hvml version="2.0">
    <head>
        <producer>com.dev.zdev.opencv</producer>
        <description>アドベントカレンダーのホーム起動シナリオ</description>
        <scene value="home" />
        <version value="1.0" />
        <situation priority="78" topic_id="start" trigger="user-word">${Local:WORD_APPLICATION} eq
            アドベントカレンダー
        </situation>
        <situation priority="78" topic_id="start" trigger="user-word">
            ${Local:WORD_APPLICATION_FREEWORD} eq アドベントカレンダーあけて
        </situation>
    </head>
    <body>
        <topic id="start" listen="false">
            <action index="1">
                <speech>${resolver:speech_ok(${resolver:ok_id})}</speech>
                <behavior id="${resolver:motion_ok(${resolver:ok_id})}" type="normal" />
                <control function="start_activity" target="home">
                    <data key="package_name" value="com.dev.zdev.opencv" />
                    <data key="class_name" value="com.dev.zdev.opencv.MainActivity" />
                </control>
            </action>
        </topic>
    </body>
</hvml>
```
```hvml:call.hvml
<?xml version="1.0" ?>
<hvml version="2.0">
    <head>
        <producer>com.dev.zdev.opencv</producer>
        <description>advent calendar オープンコール</description>
        <scene value="com.dev.zdev.opencv.scn.call" />
        <version value="1.0" />
        <accost priority="75" topic_id="call" word="com.dev.zdev.opencv.acst.call" />
    </head>
    <body>
        <topic id="call" listen="false">
            <action index="1">
                <speech>カレンダーを開けるよ…</speech>
                <behavior id="0x060030" type="normal"><wait ms="300"/></behavior>
            </action>
            <action index="2">
                <control function="adv_call" target="com.dev.zdev.opencv"/>
            </action>
        </topic>
    </body>
</hvml>
```
```hvml:res.hvml
<?xml version="1.0" ?>
<hvml version="2.0">
    <head>
        <producer>com.dev.zdev.opencv</producer>
        <description>advent calendar 結果通知</description>
        <scene value="com.dev.zdev.opencv.scn.res" />
        <version value="1.0" />
        <accost priority="75" topic_id="res" word="com.dev.zdev.opencv.acst.res" />
    </head>
    <body>
        <topic id="res" listen="false">
            <action index="1">
                <speech>${memory_p:com.dev.zdev.opencv.res}が入ってたね！</speech>
                <behavior id="assign" type="normal" />
            </action>
        </topic>
    </body>
</hvml>
```

# ロボホンに実装（画像認識・生成）

下記チュートリアルを参考に実装していきます。

[OpenCV] How to run deep networks on Android device
https://docs.opencv.org/trunk/d0/d6c/tutorial_dnn_android.html

- 画像認識モデル入手

[opencv] dnn downloadmodel > MobileNet-SSD : MobileNetSSD_deploy.prototxt(29KB), MobileNetSSD_deploy.caffemodel(22MB)
https://github.com/opencv/opencv_extra/blob/master/testdata/dnn/download_models.py

このモデルはVOC0712のラベル、下記を識別するものです。
>background, aeroplane, bicycle, bird, boat,
bottle, bus, car, cat, chair,
cow, diningtable, dog, horse,
motorbike, person, pottedplant,
sheep, sofa, train, tvmonitor

- ロボホン挙動と画像認識タスクの接合部分

```java:MainActivity.java

    @Override
    public void onExecCommand(String command, List<VoiceUIVariable> variables) {
        Log.v(TAG, "onExecCommand() : " + command);
        switch (command) {
            case ScenarioDefinitions.FUNC_CALL:
                // 1. 扉を開けるモーションと発声のコールバックで画像認識タスクキック
                hascall =true;
                final String res = getImagelabel();
                // 2. 認識結果：画像ラベル（ex. aeroplain など）を発声用メモリ、および画面タイトルバーにセット
                Log.v(TAG, "onExecCommand: RoBoHoN：" + res);
                int ret = VoiceUIVariableUtil.setVariableData(mVoiceUIManager, ScenarioDefinitions.MEM_P_RES, res);
                VoiceUIManagerUtil.stopSpeech();
                mHandler.post(new Runnable() {
                    @Override
                    public void run() {
                        if(!isFinishing()) {
                            setTitle(res); 
                        }
                    }
                });
                // 3. 認識結果：RoBoHon 結果発話キック
                if (mVoiceUIManager != null) {
                    VoiceUIVariableListHelper helper = new VoiceUIVariableListHelper().addAccost(ScenarioDefinitions.ACC_RES);
                    VoiceUIManagerUtil.updateAppInfo(mVoiceUIManager, helper.getVariableList(), true);
                }
                break;
            case ScenarioDefinitions.FUNC_END_APP:
                finish();
                break;
            default:
                break;
        }
    }
```

- 画像認識タスクメイン処理（チュートリアルからの改変部分）

```java:MainActivity.java
private String getImagelabel(){

    String path = getImagePath(); // ローカルギャラリーから画像パス取得
	// test path (sample image)
    // path = "/storage/emulated/0/PRIVATE/SHARP/CM/Samples/Arashiyama_robohon.jpg";
    final ResImage resImage = createContent(BitmapFactory.decodeFile(path)); // 認識実行

    // 認識結果画像をImageViewへセット
    mHandler.post(new Runnable() {
        @Override
        public void run() {
            if(!isFinishing()) {
                ((ImageView)findViewById(R.id.imageView)).setImageBitmap(resImage.bitmapImg);
            }
        }
    });
    // 認識結果ラベル文字列を発声等のために返却
    return resImage.label;
}
```

```java:MainActivity.java
class ResImage {
    String label;
    Bitmap bitmapImg;
}
private ResImage createContent(Bitmap original_img) {

    final int IN_WIDTH = 300;
    final int IN_HEIGHT = 300;
    final float WH_RATIO = (float)IN_WIDTH / IN_HEIGHT;
    final double IN_SCALE_FACTOR = 0.007843;
    final double MEAN_VAL = 127.5;
    final double THRESHOLD = 0.2;

    try {
        Loadnetwork(getApplicationContext());
    }catch (Exception ex) {
        Log.e("load model", ex.getMessage());
    }

    Mat img = new Mat();
    Utils.bitmapToMat(original_img.copy(Bitmap.Config.ARGB_8888, true), img);
    Imgproc.cvtColor(img, img, Imgproc.COLOR_RGBA2RGB);

    // Forward image through network.
    Size sz = new Size(IN_WIDTH, IN_HEIGHT);
    Scalar sc = new Scalar(MEAN_VAL, MEAN_VAL, MEAN_VAL);
    Mat blob = Dnn.blobFromImage(img, IN_SCALE_FACTOR, sz, sc, false, false);

    net.setInput(blob);
    Mat detections = net.forward();
    int cols = img.cols();
    int rows = img.rows();
    Size cropSize;
    if ((float)cols / rows > WH_RATIO) {
        cropSize = new Size(rows * WH_RATIO, rows);
    } else {
        cropSize = new Size(cols, cols / WH_RATIO);
    }

    int y1 = (int)(rows - cropSize.height) / 2;
    int y2 = (int)(y1 + cropSize.height);
    int x1 = (int)(cols - cropSize.width) / 2;
    int x2 = (int)(x1 + cropSize.width);

    Mat subFrame = img.submat(y1, y2, x1, x2);
    cols = subFrame.cols();
    rows = subFrame.rows();
    detections = detections.reshape(1, (int)detections.total() / 7);

    ResImage res_img = new ResImage();

    for (int i = 0; i < detections.rows(); ++i) {
        double confidence = detections.get(i, 2)[0];
        if (confidence > THRESHOLD) {
            // 認識確度が設定閾値以上のものをひとつだけ処理
            int classId = (int)detections.get(i, 1)[0];
            int xLeftBottom = (int)(detections.get(i, 3)[0] * cols);
            int yLeftBottom = (int)(detections.get(i, 4)[0] * rows);
            int xRightTop   = (int)(detections.get(i, 5)[0] * cols);
            int yRightTop   = (int)(detections.get(i, 6)[0] * rows);

            Mat dstimg = subFrame.submat(yLeftBottom, yRightTop, xLeftBottom, xRightTop);
            res_img.bitmapImg = Bitmap.createBitmap(dstimg.width(), dstimg.height(), Bitmap.Config.ARGB_8888);
            Utils.matToBitmap(dstimg, res_img.bitmapImg);

            res_img.label = classNames[classId];
            break;
        }
    }

    return res_img;
};
```

# 動作の様子


<blockquote class="twitter-tweet" data-lang="ja"><p lang="ja" dir="ltr">[qiita] 万年アドベントカレンダーを実装した（OpenCV+Deep Neural Network+RoBoHoN）の実行動画です / ロボホンが「きょうのカレンダーコンテンツ」に選んでくれたのは、実家の犬でした ! <a href="https://t.co/7yHvIoYt2J">pic.twitter.com/7yHvIoYt2J</a></p>&mdash; n_u (@ln_ulln_ul) <a href="https://twitter.com/ln_ulln_ul/status/943253373867851776?ref_src=twsrc%5Etfw">2017年12月19日</a></blockquote>
<script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

# まとめ

- OpenCVとdnnモジュール

作成済モデルの利用フェーズでは、REST API 化するのが定番だったので、今回、通信廻り実装不要の周辺も確かめられて大変よかったです！
モバイル以外の環境へも、いろいろ載せて確かめていきたいと思います。

- アプリコンセプト

ロボホンの「扉を開ける」モーションには、[PPAP](https://www.youtube.com/watch?v=0E00Zuayv9Q) や、[次元刀](https://goo.gl/pgGrLr) や、ロボホンアプリの [ロボ釣り](https://www.youtube.com/watch?v=RpTdXMJFTaU&feature=youtu.be) など、モーションひとつで異世界にコネクトするイメージを載せたい…と念じながらシナリオ実装していました。

また、ユーザーがカメラのシャッターを切る瞬間にはそれぞれの理由があって、その瞬間のストックを活用して、日々を楽しく彩るアプリができたらいいな、と思っていたので、実装できてよかったです。


---
[^1]: RoBoHoN SDK 内ドキュメント 0301_SR01MW_System_Overview_V01_00_01
[^2]: https://github.com/opencv/opencv/wiki/Deep-Learning-in-OpenCV
[^3]: https://developers-jp.googleblog.com/2017/07/supercharge-your-computer-vision-models.html