LoginSignup
53
25

More than 5 years have passed since last update.

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

Posted at

動機

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

結果

image.png
本人ができたって言ってるからいいと思う

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

コード

やりたい事

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

image.png
そう、シアハートアタックさん

具体的には、

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

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

image.png
こういうこと

開発開始のその前に

環境は以下の通り

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

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

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

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

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

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

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

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

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

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

image.png

OpenCVの画像認識を利用するので、OpenCV 3.4.4 Android Packをダウンロードして解凍

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を入れるのゴイスーなまとめを参照してほしい。

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

device-2019-01-10-135507.png

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

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

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」とかで検索すると、手の映った画像を他角形で囲って認識する方法が出てくる。(こういう奴)
ちなみにコレ、AR_Fukuokaでも勉強した内容。やっぱAR_Fukuoka凄い。

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

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

image.png

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

image.png

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

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

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
ヒストグラムを出してみた結果

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

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

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

image.png

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

image.png

    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

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%を超えたら「手を認識した」とみなして色相の上限・加減を保存する。

    // detect left hand
    if (maxArea >= floor((input.rows * input.cols * 0.2))){
        return true;
    }
    return false;
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を使いこなすと言う
ジャストな記事があるので、それを参考にする。

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に下記参照を追加する。

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関連に関しては後述)

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')

シアハートアタックのモデルをココから探す。
さすがにシアハートアタックはなかったので、青色の車にした。

image.png

Created By Poly by Googleです。

インポートの方法はココを参考にする。英語で書いてあるけど、何となく分かると思う。

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

ARCoreの画面を作る。

<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呼び出し。

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)

    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

流行りに乗れたみたいでよかった

53
25
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
53
25