LoginSignup
7
9

More than 5 years have passed since last update.

「私、いくつに見える?」への返答機能をRoBoHoNに実装する(AWS Rekognition連携)

Posted at

モバイル型ロボット電話 RoBoHoN に、AWS Rekognition APIを連携させ、ユーザーからの「私、いくつに見える?」という問いかけに返事をできるように実装しました。

キャプチャ.PNG

環境

Windows 7 SP1 64bit
Android Studio 2.3.1
RoBoHoN_SDK 1.2.0
RoBoHon端末ビルド番号 02.01.00
AWS SDK for Android 2.6.9

AWS Mobile SDK for Android 導入

  • 導入要件

 Android 2.3.3 (API Level 10) or higher

RoBoHoN は Android 5.0.2 (API Level 21 (Lollipop))

  • 参考

[AWS] Set Up the AWS Mobile SDK for Android
http://docs.aws.amazon.com/mobile/sdkforandroid/developerguide/setup.html

  1. Get the AWS Mobile SDK for Android.
  2. Set permissions in your AndroidManifest.xml file.
  3. Obtain AWS credentials using Amazon Cognito.

[qiita] Amazon Rekognitionで犬と唐揚げを見分けるアプリを作ってみた
https://qiita.com/unoemon/items/2bdf933127b6e225d036

  • 追加ライブラリ
compile 'com.amazonaws:aws-android-sdk-core:2.6.9'
compile 'com.amazonaws:aws-android-sdk-rekognition:2.6.9'
compile 'com.amazonaws:aws-android-sdk-cognito:2.6.9'

RoBoHoN実装

RoBoHoN独自の発語用 VoiceUI 、撮影用ライブラリ、そして Rekognition 通信を行き来をする実装を書いていきます。

  • Rekognition 通信部分参考

[AWS] Documentation » Amazon Rekognition » 開発者ガイド » Amazon Rekognition の開始方法 » ステップ 4: API の使用開始 » 演習 2: 顔の検出 (API)
http://docs.aws.amazon.com/ja_jp/rekognition/latest/dg/get-started-exercise-detect-faces.html

  • インターネット疎通とストレージパーミッション
AndroidManifest.xml
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
  • RoBoHonシナリオ関連定義類
ScenarioDefinitions.java
/**
*  Guess my age? シーン、accost、通知設定
*/
public static final String SCN_CALL = PACKAGE + ".guess.call";
public static final String SCN_RES = PACKAGE + ".guess.res";

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


public static final String FUNC_CALL = "guess_call";
public static final String FUNC_RES = "guess_res";


/**
* 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.rekog</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.rekog" />
                    <data key="class_name" value="com.dev.zdev.rekog.MainActivity" />
                </control>
            </action>
        </topic>
    </body>
</hvml>
rekog_call.hvml
<?xml version="1.0" ?>
<hvml version="2.0">
    <head>
        <producer>com.dev.zdev.rekog</producer>
        <description>Guess my age? 撮影呼びかけ</description>
        <scene value="com.dev.zdev.rekog.guess.call" />
        <version value="1.0" />
        <accost priority="75" topic_id="call" word="com.dev.zdev.rekog.guess.call" />
    </head>
    <body>
        <topic id="call" listen="false">
            <action index="1">
                <speech>お顔をよーく見せて……。写真を撮るよ…。<wait ms="300"/></speech>
                <behavior id="assign" type="normal" />
            </action>
            <action index="2">
                <control function="guess_call" target="com.dev.zdev.rekog"/>
            </action>
        </topic>
    </body>
</hvml>
rekog_res.hvml
<?xml version="1.0" ?>
<hvml version="2.0">
    <head>
        <producer>com.dev.zdev.rekog</producer>
        <description>Guess my age? 結果通知</description>
        <scene value="com.dev.zdev.rekog.guess.res" />
        <version value="1.0" />
        <accost priority="75" topic_id="call" word="com.dev.zdev.rekog.guess.res" />
    </head>
    <body>
        <topic id="call" listen="false">
            <action index="1">
                <speech>${memory_p:com.dev.zdev.rekog.res}にみえるみたいだよ</speech>
                <behavior id="assign" type="normal" />
            </action>
            <action index="2">
                <control function="guess_res" target="com.dev.zdev.rekog"/>
            </action>
        </topic>
    </body>
</hvml>
  • MainActivity(各所抜粋)
MainActivity.java

private boolean hascall;

//onCreate ファンクション にカメラ連携起動結果取得用レシーバー登録
        mCameraResultReceiver = new CameraResultReceiver();
        IntentFilter filterCamera = new IntentFilter(ACTION_RESULT_TAKE_PICTURE);
        registerReceiver(mCameraResultReceiver, filterCamera);

//onResume ファンクション にScenn有効化追記
        VoiceUIManagerUtil.enableScene(mVoiceUIManager, ScenarioDefinitions.SCN_CALL);
        VoiceUIManagerUtil.enableScene(mVoiceUIManager, ScenarioDefinitions.SCN_RES);

//onResume ファンクション にScenn有効化後、即時発話(初回のみ)

        if (mVoiceUIManager != null && !hascall) {
            VoiceUIVariableListHelper helper = new VoiceUIVariableListHelper().addAccost(ScenarioDefinitions.ACC_CALL);
            VoiceUIManagerUtil.updateAppInfo(mVoiceUIManager, helper.getVariableList(), true);
        }

//onPause ファンクション にScene無効化
        VoiceUIManagerUtil.disableScene(mVoiceUIManager, ScenarioDefinitions.SCN_CALL);
        VoiceUIManagerUtil.disableScene(mVoiceUIManager, ScenarioDefinitions.SCN_RES);        

//onDestroy ファンクション にカメラ連携起動結果取得用レシーバー破棄
        this.unregisterReceiver(mCameraResultReceiver);

    /**
     * VoiceUIListenerクラスからのコールバックを実装する.
     */
    @Override
    public void onExecCommand(String command, List<VoiceUIVariable> variables) {
        Log.v(TAG, "onExecCommand() : " + command);
        switch (command) {
            case ScenarioDefinitions.FUNC_CALL:
                // 写真呼びかけ状況設定
                hascall =true;
                //写真を撮る
                sendBroadcast(getIntentForPhoto(false));
                break;
            case ScenarioDefinitions.FUNC_RES:
                finish();
                break;
            case ScenarioDefinitions.FUNC_END_APP:
                finish();
                break;
            default:
                break;
        }
    }


    /**
     * カメラ連携の結果を受け取るためのBroadcastレシーバー クラス
     * それぞれの結果毎に処理を行う.
     */
    private class CameraResultReceiver extends BroadcastReceiver {
        @Override
        public void onReceive(Context context, Intent intent) {
            String action = intent.getAction();
            Log.v(TAG, "CameraResultReceiver#onReceive() : " + action);
            switch (action) {
                case ACTION_RESULT_FACE_DETECTION:
                    int result = intent.getIntExtra(FaceDetectionUtil.EXTRA_RESULT_CODE, FaceDetectionUtil.RESULT_CANCELED);
                    break;
                case ACTION_RESULT_TAKE_PICTURE:
                    result = intent.getIntExtra(ShootMediaUtil.EXTRA_RESULT_CODE, ShootMediaUtil.RESULT_CANCELED);
                    if(result == ShootMediaUtil.RESULT_OK) {
                        // 1. 撮影画像ファイルパス取得 
                        final String path = intent.getStringExtra(ShootMediaUtil.EXTRA_PHOTO_TAKEN_PATH);
                        Log.v(TAG, "PICTURE_path : " + path);
                        Thread thread = new Thread(new Runnable() {public void run() {
                            try {
                                // 2. APIリクエスト、レスポンス取得
                                String res = (new GetAge()).inquireAge(path, getApplicationContext());
                                Log.v(TAG, "onExecCommand: RoBoHoN:" + res);
                                int ret = VoiceUIVariableUtil.setVariableData(mVoiceUIManager, ScenarioDefinitions.MEM_P_RES, res);
                                VoiceUIManagerUtil.stopSpeech();
                                // 3. RoBoHon 結果発話
                                if (mVoiceUIManager != null) {
                                    VoiceUIVariableListHelper helper = new VoiceUIVariableListHelper().addAccost(ScenarioDefinitions.ACC_RES);
                                    VoiceUIManagerUtil.updateAppInfo(mVoiceUIManager, helper.getVariableList(), true);
                                }
                            } catch (Exception e) {
                                Log.v(TAG, "onExecCommand: Exception" +  e.getMessage());
                            };
                        }});
                        thread.start();}
                    }
                    break;
                case ACTION_RESULT_REC_MOVIE:
                    result = intent.getIntExtra(ShootMediaUtil.EXTRA_RESULT_CODE, ShootMediaUtil.RESULT_CANCELED);
                    break;
                default:
                    break;
            }
        }
    }
  • AWS Rekognition通信
MainActivity.java
package com.dev.zdev.rekog;

/**
 * Created by zdev on 2017/12/10.
 */

import android.util.Log;
import android.content.Context;

import android.graphics.Bitmap;
import android.graphics.BitmapFactory;

import com.amazonaws.auth.CognitoCachingCredentialsProvider;
import com.amazonaws.regions.Regions;
import com.amazonaws.services.rekognition.AmazonRekognitionClient;

import java.io.ByteArrayOutputStream;
import java.nio.ByteBuffer;
import java.util.List;

import com.amazonaws.services.rekognition.model.DetectFacesRequest;
import com.amazonaws.services.rekognition.model.DetectFacesResult;
import com.amazonaws.services.rekognition.model.FaceDetail;
import com.amazonaws.services.rekognition.model.AgeRange;
import com.amazonaws.services.rekognition.model.Image;

public class GetAge {
    private static final String TAG = GetAge.class.getSimpleName();
    private static int newWidth = 326;
    private static int newHeight = 244;

    private String resMsg = "ゼロさいからひゃくさいの間 ";
    AmazonRekognitionClient amazonRekognitionClient = null;

    private Bitmap resizeImag(String path){
        return Bitmap.createScaledBitmap(BitmapFactory.decodeFile(path), newWidth, newHeight, true);
    };

    private void setAmazonRekognitionClient(Context appcontext){
        // Amazon Cognito 認証情報プロバイダーを初期化します
        CognitoCachingCredentialsProvider credentialsProvider = new CognitoCachingCredentialsProvider(
            appcontext,
            "us-east-1:XXXXXXXXXXXX", // ID プールの ID
            Regions.US_EAST_1 // リージョン
        );
        this.amazonRekognitionClient =  new AmazonRekognitionClient(credentialsProvider);
    };

    public synchronized String inquireAge(String path, Context appcontext) throws Exception {

        try {
            Bitmap img = resizeImag(path);

            if (this.amazonRekognitionClient == null) {
                setAmazonRekognitionClient(appcontext);
            }

            ByteBuffer imageBytes = null;
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            img.compress(Bitmap.CompressFormat.JPEG, 100, baos);
            imageBytes = ByteBuffer.wrap(baos.toByteArray());

            DetectFacesRequest request = new DetectFacesRequest()
                    .withImage(new Image()
                            .withBytes(imageBytes))
                    .withAttributes("ALL");

            DetectFacesResult result = amazonRekognitionClient.detectFaces(request);
            List<FaceDetail> faceDetails = result.getFaceDetails();

            Log.v(TAG, "inquireAge: " + faceDetails.toString());

            for (FaceDetail face: faceDetails) {
                if (request.getAttributes().contains("ALL")) {
                    AgeRange ageRange = face.getAgeRange();
                    Log.v(TAG, "inquireAge: " + ageRange.getLow().toString() + " and " + ageRange.getHigh().toString() + " years old.");
                    this.resMsg = ageRange.getLow().toString() + " さいから " + ageRange.getHigh().toString() + " さいの間 ";
                } else { // non-default attributes have null values.
                    Log.v(TAG, "inquireAge: " + "Here's the default set of attributes:");
                }
            }

            return this.resMsg;

        } catch (Exception e) {
            Log.v(TAG, "inquireAge: exception: " + e.toString());
            return this.resMsg;
        }
    }
}

実行の様子と感想

年末も近いし、色々とひとが集まる機会に使えたらいいな、と、パーティアプリにしたく、実装しました。
(Microsoft「How-Old.net」が流行ったときのように、1~2回/人 試して場が沸いたら上出来!という)

家族で試したところ、下記のようになりました。

  • 30代後半男性: 45 ~ 63 才 (!)の間
  • 30代後半女性: 26 ~ 38 才の間
  • 9才 … 6 ~ 13 才の間
  • 4才 … 4 ~ 7 才の間、4 ~ 9 才の間

私(30代後半)は 「14 ~ 23 才の間」というスコアも叩き出せたので、家族が沸き「ロボホンがひいきしてる!(子供たちの声)」「なんか匙加減して実装してない?」など、”結果をロボホンが発声する”1 という文脈も楽しめました。



  1. ロボホンSDK内規定(0201_SR01MW_Personality_and_Speech_Regulations_V01_00_01)には "ロボホンはユーザに対して誠実です。ユーザを裏切ることはありません(中略) 不確実な情報を話すときは、それとわかる言い回しをする(例:「雨になるよ」→「予報によると、雨になるみたいだよ」" があります。そのため、今回も結果通知の語尾に「~才にみえるよ」でなく「~才にみえるみたいだよ」とつけています。(また、同規定書には "主観を持たない"というくだり("ロボホンはロボットです 。ロボットは 、基本的に プログラムされたとおりに動くものです 。ロボホン自身の主観(好き嫌いや感想など ロボホン自身の主観(好き嫌いや感想など 、人によって感じ方が変わるもの 、人によって感じ方が変わるもの )は 持たないのが基本的考え方です 。")ともあります) 

7
9
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
9