動機
2019年になったし、もうそろそろ左手から自動追尾弾ぐらいだせないといけない。
結果
パーツ的に役立つかもなので一応コード上げる
やりたい事
左手 + 自動追尾と言えばこの方
具体的には、
- 左手を認識している状態で
- 「シアハートアタック」とか「第二の爆弾」とか言うと、
- シアハートアタックが自動追尾し始める。
箇条書きにしたけど、どうしても説明がシアハートアタックありきになってしまった。
分からないとアレなので絵も描いた。
開発開始のその前に
環境は以下の通り
- 実行環境
- 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++サポートあり。
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。
補足だが、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凄い。
では今回どうするかと言うと、もっとざっくり攻める。要は「手として認識しない」。
何言ってんだコイツと思われるだろうからざっと図解する。
左手をかざすアプリなのだから、スマホの画面上も左側に限定する。
そして、肌色の領域がある程度大きければ、「手をかざした」とみなす。
何ならヒストグラムを出してみればわかるハズ。
コードはこんな感じでヒストグラム出す
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;
}
これでイケるはずだ!と言う夢を見たので、よくある手段で手を認識する。
まずは色を範囲指定して手っぽい部分を大体取れるようにする。
こんな感じでコントロールを配置して、
ざっくりHSVの範囲指定とerode,dilateでまとまりをつくる
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でまとまりをくくる
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')
シアハートアタックのモデルをココから探す。
さすがにシアハートアタックはなかったので、青色の車にした。
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のシアハートアタックが出てきて「元ネタこれだったんだ」って思った。
流行りに乗れたみたいでよかった