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

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

More than 1 year has passed since last update.

はじめに

この記事は OpenCV Advent Calendar 2017 の 20日目の記事です。

(12/20 PM 追記)

  • SHARP 公式さんから言及をいただきました、ありがとうございます!

アドベントカレンダーを開け続けたい

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

image.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

この動的な画像作成に、今年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シナリオ関連定義類
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(ユーザーからの呼びかけで起動、モーション、認識結果応答)
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>
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>
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

  • ロボホン挙動と画像認識タスクの接合部分
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;
        }
    }
  • 画像認識タスクメイン処理(チュートリアルからの改変部分)
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;
}
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;
};

動作の様子

まとめ

  • OpenCVとdnnモジュール

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

  • アプリコンセプト

ロボホンの「扉を開ける」モーションには、PPAP や、次元刀 や、ロボホンアプリの ロボ釣り など、モーションひとつで異世界にコネクトするイメージを載せたい…と念じながらシナリオ実装していました。

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


n_ueh
よろしくおねがいします! / https://twitter.com/ln_ulln_ul
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
ユーザーは見つかりませんでした