2019年だし左手から爆弾出す。

# 動機

2019年になったし、もうそろそろ左手から自動追尾弾ぐらいだせないといけない。

# 結果

![image.png](https://qiita-image-store.s3.amazonaws.com/0/42445/82493a37-b9ee-7e36-d811-017d0ffd6f10.png)
<font color="red">本人ができたって言ってるからいいと思う</font>

パーツ的に役立つかもなので一応コード上げる

[コード](https://github.com/miyatama/siahato)

# やりたい事

左手 + 自動追尾と言えばこの方

![image.png](https://qiita-image-store.s3.amazonaws.com/0/42445/31a21b4f-990e-3590-e825-dddbe877b208.png)
<font color="red">そう、シアハートアタックさん</font>

具体的には、

 + 左手を認識している状態で
 + 「シアハートアタック」とか「第二の爆弾」とか言うと、
 + シアハートアタックが自動追尾し始める。

箇条書きにしたけど、どうしても説明がシアハートアタックありきになってしまった。
分からないとアレなので絵も描いた。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/42445/329c6123-581a-32a8-23c6-3f2ef8e4ff9d.png)
<font color="red">こういうこと</font>


# 開発開始のその前に

環境は以下の通り

 + 実行環境
     + Android 8.1.0
 + 開発環境
     + Android Studio 3.2.1
     + OpenCV 3.4.4
     + ARCore SDK for Android v1.6.0

もう少しシステマチックに考える。

 + スマホのカメラで左手を認識する(OpenCV)
 + 「シアハートアタック」を認識する(STT)
 + シアハートアタックを出す(ARCore)
 + 遊ぶ

各工程で妥協できる所は妥協する。

大切な事なのでもう一度書こう。

<B>各工程で妥協する。(つよい決意)</B>

で、先に妥協した所をまとめると

 + スマホのカメラは使うが手として認識はしない
 + 「シアハートアタック」以外でも「シアハートアタック」を認識する
 + 出るのは出るが、爆弾じゃない
 + 追跡もしない
 + 左手から出ない

# スマホのカメラで左手を認識する

まずはAndroid Stutdioから新規プロジェクトを作る。とりあえずC++サポートあり。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/42445/1b018347-965a-cbba-ddd1-3bd63adc5c2c.png)

OpenCVの画像認識を利用するので、[OpenCV 3.4.4 Android Pack](https://sourceforge.net/projects/opencvlibrary/files/3.4.4/opencv-3.4.4-android-sdk.zip/download)をダウンロードして解凍

Android StudioのFile -> New -> Import Moduleから、回答したopencv-3.4.4-android-sdk/sdk/javaを選択。

一発目で素直にビルドは通らないので、下記点を修正。

 + openCVLibrary344/manifest/AndroidManifest.xml
    + targetVersion削除
    + minSdkVersion削除
 + openCVLibrary344/build.gradle
    + targetVersion削除
    + minSdkVersion削除
    + compileSdkVersionを27へ変更(appのbuild.gradleと合わせる)

プロジェクトの参照等は[AndroidStudioにOpenCVを入れる](https://qiita.com/sourcekatu/items/c5fe7850b2731d7e14b2)のゴイスーなまとめを参照してほしい。

こんな感じに表示できればとりあえずOK。

![device-2019-01-10-135507.png](https://qiita-image-store.s3.amazonaws.com/0/42445/ef5f5b78-e7df-8c32-06cc-3ac9b089682b.png)

補足だが、portlateだと全画面表示がめんどうなので、landscape固定にした。

あとは、rgbaで取得されるので、gbraに変換したぐらい。

```cpp
JNIEXPORT jboolean JNICALL
Java_com_example_miyatama_siahato_MainActivity_detectLeftHand(
        JNIEnv* env,
        jobject instance,
        jlong matAddr) {
    Mat &input = *(Mat *) matAddr;
    Mat rgbs[4];
    vector<Mat> channels;
    cv::split(input, rgbs);
    channels.push_back(rgbs[2]);
    channels.push_back(rgbs[1]);
    channels.push_back(rgbs[0]);
    channels.push_back(rgbs[3]);
    cv::merge(channels, input);

    return true;
}
```

では、NDKにMatを引き渡せるようにいなったので、いよいよカメラで手を認識してゆく。
「opencv hand recognition」とかで検索すると、手の映った画像を他角形で囲って認識する方法が出てくる。([こういう奴](https://medium.com/@muehler.v/simple-hand-gesture-recognition-using-opencv-and-javascript-eb3d6ced28a0))
ちなみにコレ、AR_Fukuokaでも勉強した内容。やっぱAR_Fukuoka凄い。

では今回どうするかと言うと、もっとざっくり攻める。要は「手として認識しない」。
何言ってんだコイツと思われるだろうからざっと図解する。

左手をかざすアプリなのだから、スマホの画面上も左側に限定する。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/42445/5d164289-1c68-a934-12ee-64e6b16e7682.png)

そして、肌色の領域がある程度大きければ、「手をかざした」とみなす。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/42445/181e44e6-0fc8-d413-c95f-4a4fb7c06bcc.png)

何ならヒストグラムを出してみればわかるハズ。

コードはこんな感じでヒストグラム出す

```cpp
Java_com_example_miyatama_siahato_MainActivity_detectLeftHand(
        JNIEnv* env,
        jobject instance,
        jlong matAddr) {
    Mat &input = *(Mat *) matAddr;
    Mat rgbs[4];
    vector<Mat> channels;
    cv::split(input, rgbs);
    channels.push_back(rgbs[2]);
    channels.push_back(rgbs[1]);
    channels.push_back(rgbs[0]);
    cv::merge(channels, input);

    int width = floor((float)input.cols* 0.7);
    Mat detectRect(input, cv::Rect(0, 0, width, input.rows));

    Mat hsvMat;
    cv::cvtColor(detectRect, hsvMat, cv::COLOR_BGR2HSV);
    Mat hsvs[3];
    cv::split(hsvMat, hsvs);
    int hist_size = 256;
    int hist[hist_size];
    for (int i = 0; i < hist_size; i++){
        hist[i] = 0;
    }
     for (int i = 0; i < hsvs[0].cols; i++){
        for (int j = 0; j < hsvs[0].rows; j++){
            hist[hsvs[0].data[i * hsvs[0].rows + j]]++;
        }
    }

    int histLength = 2;
    cv::rectangle(
        input,
        cv::Point(0, 0),
        cv::Point(histLength * 256, 255),
        cv::Scalar(0, 0, 0),
        -1,
        8,
        0);
    for (int i = 0; i < hist_size ; i++){
        cv::rectangle(
            input,
            cv::Point(i * histLength, input.rows - hist[i]),
            cv::Point(i * histLength + histLength, input.rows),
            cv::Scalar(255, 0, 0),
            -1,
            8,
            0);
    }

    return true;
}
```

![image.png](https://qiita-image-store.s3.amazonaws.com/0/42445/c8243b3c-869d-a537-67d1-a01f1d3e0e81.png)
<font color="red">ヒストグラムを出してみた結果</font>

これでイケるはずだ！と言う夢を見たので、よくある手段で手を認識する。

まずは色を範囲指定して手っぽい部分を大体取れるようにする。

こんな感じでコントロールを配置して、

![image.png](https://qiita-image-store.s3.amazonaws.com/0/42445/9d159cbf-cbf0-12dc-8091-0a15b2d6cd9c.png)

ざっくりHSVの範囲指定とerode,dilateでまとまりをつくる

![image.png](https://qiita-image-store.s3.amazonaws.com/0/42445/48177020-ab00-3a9f-b92a-e4b7aecd585a.png)

```cpp
    Mat hsv;
    Mat gray = Mat::zeros(input.rows, input.cols, CV_8UC1);
    cv::cvtColor(input, hsv, cv::COLOR_BGR2HSV, 3);
    auto upper = cv::Scalar(thresholdUpper, 255, 255);
    auto lower = cv::Scalar(thresholdLower, 0, 0);
    cv::inRange(hsv, lower, upper, gray);
    cv::erode(gray, gray, Mat(), cv::Point(-1, -1), 5);
    cv::dilate(gray, gray, Mat(), cv::Point(-1, -1), 5);
    Mat buf;
    hsv.copyTo(buf, gray);
    gray.release();
    hsv.release();

    cv::cvtColor(buf, input, cv::COLOR_HSV2BGR);
    buf.release();
```

カクつくので画像サイズを落とした上でfindContourでまとまりをくくる

![image.png](https://qiita-image-store.s3.amazonaws.com/0/42445/ace76ecf-fae1-c531-518e-5e2446937cbe.png)

```cpp
Mat rgbs[4];
vector<Mat> channels;
cv::split(input, rgbs);
channels.push_back(rgbs[2]);
channels.push_back(rgbs[1]);
channels.push_back(rgbs[0]);
cv::merge(channels, input);
rgbs[0].release();
rgbs[1].release();
rgbs[2].release();
rgbs[3].release();

Mat resizedInput;
if (input.cols <= 128){
    input.copyTo(resizedInput);
} else{
    float inputResizedMet = 128.0 / (float)input.cols;
    cv::resize(input, resizedInput, cv::Size(128, ceil(input.rows * inputResizedMet)));
}

Mat hsv;
Mat gray = Mat::zeros(resizedInput.rows, resizedInput.cols, CV_8UC1);
cv::cvtColor(resizedInput, hsv, cv::COLOR_BGR2HSV, 3);
auto upper = cv::Scalar(thresholdUpper, 255, 255);
auto lower = cv::Scalar(thresholdLower, 0, 0);
cv::inRange(hsv, lower, upper, gray);
cv::erode(gray, gray, Mat(), cv::Point(-1, -1), 5);
cv::dilate(gray, gray, Mat(), cv::Point(-1, -1), 5);

vector< vector<cv::Point> > contours;
vector< cv::Vec4i> hierarchy;
cv::findContours(gray, contours, hierarchy, cv::RETR_LIST,cv::CHAIN_APPROX_SIMPLE);

int maximumContourIdx = -1;
int maxArea = 0;
for (int i = 0;i  < contours.size();i++){
    vector<cv::Point> p = contours.at(i);
    int minX, minY = 65536;
    int maxX, maxY = 0;
    for(int j = 0; j < p.size(); j++){
        if (minX > p.at(j).x ) {
            minX = p.at(j).x;
        }
        if (minY > p.at(j).y ) {
            minY = p.at(j).y;
        }
        if (maxX < p.at(j).x ) {
            maxX = p.at(j).x;
        }
        if (maxY < p.at(j).y ) {
            maxY = p.at(j).y;
        }
    }

    int area = (maxX - minX) * (maxY - minY);
    if (maxArea < area) {
        maxArea = area;
        maximumContourIdx = i;
    }
}

if (maximumContourIdx >= 0){
        for (int i = 0; i < (contours.at(maximumContourIdx).size() - 1); i++){
    	    int startX = contours.at(maximumContourIdx).at(i).x;
    	    int startY = contours.at(maximumContourIdx).at(i).y;
    	    int endX = contours.at(maximumContourIdx).at(i + 1).x;
    	    int endY = contours.at(maximumContourIdx).at(i + 1).y;
    	    cv::line(
    	        resizedInput,
    	        cv::Point(startX, startY),
    	        cv::Point(endX, endY),
    	        cv::Scalar(255, 0, 0),
    	        1,
    	        4);
        }
    	int lastIndex = contours.at(maximumContourIdx).size() - 1;
    	int startX = contours.at(maximumContourIdx).at(0).x;
    	int startY = contours.at(maximumContourIdx).at(0).y;
    	int endX = contours.at(maximumContourIdx).at(lastIndex).x;
    	int endY = contours.at(maximumContourIdx).at(lastIndex).y;
    	cv::line(
    	    resizedInput,
    	    cv::Point(startX, startY),
    	    cv::Point(endX, endY),
    	    cv::Scalar(255, 0, 0),
    	    2,
    	    4);
}
gray.release();
hsv.release();

cv::resize(resizedInput, input, cv::Size(input.cols, input.rows));
resizedInput.release();
```

ざっくり手を囲む矩形が画面の20%を超えたら「手を認識した」とみなして色相の上限・加減を保存する。

```cpp
    // detect left hand
    if (maxArea >= floor((input.rows * input.cols * 0.2))){
        return true;
    }
    return false;
```

```kotlin
var detected = detectLeftHand(addr, colorThresholdUpper, colorThresholdLower)
if (detected) {
    val sp = getSharedPreferences("siahato_data", Context.MODE_PRIVATE)
    val editor = sp.edit()
    editor.putInt("color_threshold_upper", colorThresholdUpper)
    editor.putInt("color_threshold_lower", colorThresholdLower)
    editor.apply()
    state = SiahatoState.STAND_ON;
}
```

# 「シアハートアタック」を認識する

左手(?)が認識できたので「シアハートアタック」という言葉の認識に入る。
シアハートアタックの認識:一定時間音声を保存して``シアハートアタック``という言葉があったらシアハートアタックとする。
今回は素直にIntent投げてテキストを取ることにした。
音声ファイルに落としてSTTと言う手もある事はある。画面遷移とか考えるとこっちが楽なのだが、面倒だった。
気が乗ったらやる。
[Android Speech Recognizerを使いこなす](https://qiita.com/KAKKA/items/d15cb9187fd1bcca8770)と言う
ジャストな記事があるので、それを参考にする。

```kotlin
val intent = Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH)
intent.putExtra(RecognizerIntent.EXTRA_LANGUAGE_MODEL, RecognizerIntent.LANGUAGE_MODEL_FREE_FORM)
intent.putExtra(RecognizerIntent.EXTRA_PREFER_OFFLINE, true)
intent.putExtra(RecognizerIntent.EXTRA_CALLING_PACKAGE, packageName)
val recognizer = SpeechRecognizer.createSpeechRecognizer(applicationContext)
recognizer.setRecognitionListener(object: RecognitionListener {
    override fun onReadyForSpeech(p0: Bundle?) { }
    override fun onRmsChanged(p0: Float) { }
    override fun onBufferReceived(p0: ByteArray?) { }
    override fun onPartialResults(p0: Bundle?) { }
    override fun onEvent(p0: Int, p1: Bundle?) { }
    override fun onBeginningOfSpeech() { }
    override fun onEndOfSpeech() { }
    override fun onError(p0: Int) { }
    override fun onResults(results: Bundle?) {
        if (results == null) {
            return
        }
        val texts = results!!.getStringArrayList(android.speech.SpeechRecognizer.RESULTS_RECOGNITION)
        var detectKeyword = false
        for (text in texts) {
            Log.w(TAG, "recognize: $text")
            if (text == "シエアハートアタック" || text == "シアハートアタック" || text == "第二の爆弾"){
                detectKeyword = true
            }
        }
        if (!detectKeyword){
            // siahato!!
        }
    }
})
recognizer.startListening(intent)
```




# シアハートアタックを出す

さて、いよいよARCoreの出番。
まずはプロジェクトのgradleに下記参照を追加する。

```groovy
buildscript {
    repositories {
        google()
        jcenter()
        mavenLocal()
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:3.2.1'
        // この参照を追加
        classpath 'com.google.ar.sceneform:plugin:1.6.0'
    }
}
```

appのgradleにも追加する(car_02関連に関しては後述)

```kotlin
dependencies {
    implementation fileTree(include: ['*.jar'], dir: 'libs')
    implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
    implementation 'com.android.support:appcompat-v7:27.1.1'
    implementation 'com.android.support.constraint:constraint-layout:1.1.3'
    implementation "com.quickbirdstudios:yuvtomat:0.1.0"
    implementation "com.google.ar.sceneform:core:1.6.0"
    implementation "com.google.ar.sceneform.ux:sceneform-ux:1.6.0"

    testImplementation 'junit:junit:4.12'
    androidTestImplementation 'com.android.support.test:runner:1.0.2'
    androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
    implementation project(':openCVLibrary344')
}

apply plugin: 'com.google.ar.sceneform.plugin'


sceneform.asset('sampledata/models/car_02.obj',
        'default',
        'sampledata/models/car_02.sfa',
        'src/main/assets/car_02')

```

シアハートアタックのモデルを[ココ](https://poly.google.com/)から探す。
さすがにシアハートアタックはなかったので、青色の車にした。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/42445/d01b65cb-bb58-f470-b088-2d1656ad053e.png)

<font color="red">Created By Poly by Googleです。</font>

インポートの方法は[ココ](https://developers.google.com/ar/develop/java/sceneform/import-assets)を参考にする。英語で書いてあるけど、何となく分かると思う。

car_02と言う名前で作ればappのgradleに修正はいらないハズ。

ARCoreの画面を作る。

```xml
<RelativeLayout 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=".ShiahatoActivity">

    <com.google.ar.sceneform.ArSceneView
            android:id="@+id/ar_scene_view"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:layout_gravity="top"/>

    <View
            android:id="@+id/standOnView"
            android:background="@color/standOverap"
            android:layout_width="match_parent"
            android:layout_height="match_parent"/>

    <Button android:id="@+id/nextStage"
            android:text="Debug"
            android:layout_width="80dip"
            android:layout_height="80dip"
            android:layout_marginBottom="10dip"
            android:layout_marginLeft="10dip"
            android:layout_alignParentLeft="true"
            android:layout_alignParentBottom="true"/>
</RelativeLayout>
```

カメラ画像をこんな感じで取って、手の認識に回す。手の認識ができたらSTT呼び出し。

```kotlin
var bitmap = createBitmap(arSceneView.width, arSceneView.height, Bitmap.Config.ARGB_8888)

PixelCopy.request(arSceneView, bitmap, {
    if (it == PixelCopy.SUCCESS) {
        var mat = Mat(bitmap.width, bitmap.height, CvType.CV_8UC4)
        Utils.bitmapToMat(bitmap, mat)
        val matAddress = mat.nativeObjAddr

        val sp = getSharedPreferences("siahato_data", Context.MODE_PRIVATE)
        val colorThresholdUpper = sp.getInt("color_threshold_upper", 0)
        val colorThresholdLower = sp.getInt("color_threshold_lower", 0)
        val detected = detectLeftHand(matAddress, colorThresholdUpper, colorThresholdLower)
        if (detected) {
            val message = Message.obtain()
            message.what = DETECT_LEFT_HAND
            handler.sendMessage(message)
        }
    }else{
        e(TAG, "copy bitmap error")
    }
},
handler)
```


いざ、シアハートアタック。(SiahatoはRendarable設定しただけのNode)

```kotlin
    private fun trySiahato(frame: Frame){
        if (frame.camera.trackingState == TrackingState.TRACKING) {
            val posX = arSceneView.width / 2.0
            val posY = arSceneView.height / 2.0
            frame.hitTest(posX.toFloat(), posY.toFloat()).forEach { it ->
                val trackable = it.trackable
                when {
                    trackable is Plane ->{
                       if (trackable.isPoseInPolygon(it.hitPose)){
                           val anchor = it.createAnchor()
                           val anchorNode = AnchorNode(anchor)
                           anchorNode.setParent(arSceneView.scene)
                           val siahato = createSiahato()
                           anchorNode.addChild(siahato)
                           return@forEach
                       }
                    }
                }
            }
        }
    }

    private fun createSiahato(): Node{
        val base = Node()

        val siahato = Siahato(this, 0.1f, siahatoRenderable)
        siahato.setParent(base)
        siahato.localPosition= Vector3(0.0f, 0.0f, 0.2f)

        return base
    }

```


# 振り返り

Google Assistantに「OKグーグル、シアハートアタックを出して」っていったら、
QUEENのシアハートアタックが出てきて「元ネタこれだったんだ」って思った。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/42445/237e59ac-d564-f831-c48b-dfe241c928d7.png)

<font color="red">流行りに乗れたみたいでよかった</font>