OpenCV
MachineLearning
DeepLearning
Caffe
RoBoHoN
OpenCVDay 20

万年アドベントカレンダーを実装した(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 や、次元刀 や、ロボホンアプリの ロボ釣り など、モーションひとつで異世界にコネクトするイメージを載せたい…と念じながらシナリオ実装していました。

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