- 投稿日:2019-05-23T21:47:43+09:00
Cloud Vison APIを使って、笑ってはいけないのアレができるAndroidアプリを作ってみた
だいぶ昔ですが、Googleの「Cloud Vison API」を使って、笑ってはいけないのアレができるAndroidアプリを作ってみたので、今更ながら紹介します。
概要
年末恒例、「笑ってはいけないシリーズ」でおなじみ、笑うと「デデーン」という効果音とともに「○○ OUT」の字幕が出てくるシーンを忠実に再現したアプリです。
使い方は簡単で、カメラで撮影するか、ギャラリーから画像を選択するだけです。
選択した画像について、笑っているかどうかの判定処理が行われ、笑っていると判断された場合、「デデーン」という効果音とともに「OUT」の字幕が表示されます。アニメーションGIFだと音が出ないため、音ありバージョンをYoutubeにアップロードしました。
https://www.youtube.com/watch?v=DZPbPsgpHZw※Google Playにはリリースしていません。楽しみたい方はGitHubにアップしてますので、そちらをクローンしてセットアップしてください。
開発した経緯
面白そうだったからです笑
開発環境
実装について
まず、Cloud Vision APIを使えるようにするための設定が必要です。
手順に関しては以下の公式ドキュメントや、参考URLを見てください。
Cloud Vision API ドキュメントまた、ソースコードは以下にあるGoogleのサンプルファイルをベースに作成しました。
https://github.com/GoogleCloudPlatform/cloud-vision/tree/master/android/CloudVisionベースの部分は基本的に同じなので解説は省きます。
キモとなる、画像から笑っているかどうかの判定処理の部分について解説します。
APIを呼び出す部分となるため、以下の通りAsyncTaskで非同期処理を行うようにしました。CallCloudVisionAsyncTask.javapackage jp.hiesiea.app.smiledetector; import android.app.ProgressDialog; import android.content.Context; import android.content.res.AssetFileDescriptor; import android.graphics.Bitmap; import android.media.MediaPlayer; import android.os.AsyncTask; import android.util.Log; import android.view.animation.Animation; import android.view.animation.AnimationUtils; import android.widget.TextView; import com.google.api.client.extensions.android.http.AndroidHttp; import com.google.api.client.googleapis.json.GoogleJsonResponseException; import com.google.api.client.http.HttpTransport; import com.google.api.client.json.JsonFactory; import com.google.api.client.json.gson.GsonFactory; import com.google.api.services.vision.v1.Vision; import com.google.api.services.vision.v1.VisionRequestInitializer; import com.google.api.services.vision.v1.model.AnnotateImageRequest; import com.google.api.services.vision.v1.model.BatchAnnotateImagesRequest; import com.google.api.services.vision.v1.model.BatchAnnotateImagesResponse; import com.google.api.services.vision.v1.model.FaceAnnotation; import com.google.api.services.vision.v1.model.Feature; import com.google.api.services.vision.v1.model.Image; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.util.ArrayList; import java.util.List; import jp.hiesiea.app.smiledetecrot.R; public class CallCloudVisionAsyncTask extends AsyncTask<Void, Void, String> { private static final String TAG = CallCloudVisionAsyncTask.class.getSimpleName(); private static final String SOUND_FILE_NAME = "deden.mp3"; private static final String TYPE_FACE_DETECTION = "FACE_DETECTION"; private static final String RESULT_VERY_LIKELY = "VERY_LIKELY"; private static final String RESULT_LIKELY = "LIKELY"; private VisionRequestInitializer mVisionRequestInitializer; private Bitmap mBitmap; private Context mContext; private TextView mTextView; private ProgressDialog mProgressDialog; private Animation mAnimation; private MediaPlayer mMediaPlayer; private MediaPlayer.OnCompletionListener mOnCompletionListener = new MediaPlayer.OnCompletionListener() { @Override public void onCompletion(MediaPlayer mediaPlayer) { mediaPlayer.stop(); mediaPlayer.reset(); mediaPlayer.release(); } }; private Animation.AnimationListener mAnimationListener = new Animation.AnimationListener() { @Override public void onAnimationStart(Animation animation) { } @Override public void onAnimationEnd(Animation animation) { mTextView.setText(""); animation.reset(); } @Override public void onAnimationRepeat(Animation animation) { } }; /** * CloudVisionAPI用の非同期処理呼び出し * @param visionRequestInitializer * @param bitmap * @param textView * @param context */ public CallCloudVisionAsyncTask( VisionRequestInitializer visionRequestInitializer, Bitmap bitmap, TextView textView, Context context) { mVisionRequestInitializer = visionRequestInitializer; mBitmap = bitmap; mTextView = textView; mContext = context; mProgressDialog = new ProgressDialog(context); mAnimation = AnimationUtils.loadAnimation(context, R.anim.translate_animation); mAnimation.setAnimationListener(mAnimationListener); setUpMediaPlayer(); } @Override protected void onPreExecute() { super.onPreExecute(); setUpProgressDialog(); } @Override protected String doInBackground(Void... voids) { try { HttpTransport httpTransport = AndroidHttp.newCompatibleTransport(); JsonFactory jsonFactory = GsonFactory.getDefaultInstance(); Vision.Builder builder = new Vision.Builder(httpTransport, jsonFactory, null); builder.setVisionRequestInitializer(mVisionRequestInitializer); BatchAnnotateImagesRequest batchAnnotateImagesRequest = new BatchAnnotateImagesRequest(); batchAnnotateImagesRequest.setRequests(new ArrayList<AnnotateImageRequest>() {{ AnnotateImageRequest annotateImageRequest = new AnnotateImageRequest(); // Add the image Image base64EncodedImage = new Image(); // Convert the bitmap to a JPEG // Just in case it's a format that Android understands but Cloud Vision ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); mBitmap.compress(Bitmap.CompressFormat.JPEG, 90, byteArrayOutputStream); byte[] imageBytes = byteArrayOutputStream.toByteArray(); // Base64 encode the JPEG base64EncodedImage.encodeContent(imageBytes); annotateImageRequest.setImage(base64EncodedImage); // add the features we want annotateImageRequest.setFeatures(new ArrayList<Feature>() {{ Feature labelDetection = new Feature(); labelDetection.setType(TYPE_FACE_DETECTION); add(labelDetection); }}); // Add the list of one thing to the request add(annotateImageRequest); }}); Vision vision = builder.build(); Vision.Images.Annotate annotateRequest = vision.images().annotate(batchAnnotateImagesRequest); // Due to a bug: requests to Vision API containing large images fail when GZipped. annotateRequest.setDisableGZipContent(true); Log.d(TAG, "created Cloud Vision request object, sending request"); BatchAnnotateImagesResponse response = annotateRequest.execute(); if (checkSmile(response)) { mMediaPlayer.start(); return mContext.getResources().getString(R.string.image_smile_out); } return mContext.getResources().getString(R.string.image_smile_safe); } catch (GoogleJsonResponseException e) { Log.d(TAG, "failed to make API request because " + e.getContent()); } catch (IOException e) { Log.d(TAG, "failed to make API request because of other IOException " + e.getMessage()); } return "Cloud Vision API request failed. Check logs for details."; } @Override protected void onPostExecute(String result) { super.onPostExecute(result); mTextView.setText(result); mTextView.startAnimation(mAnimation); mProgressDialog.dismiss(); } /** * 笑顔判定 * @param response * @return */ private boolean checkSmile(BatchAnnotateImagesResponse response) { List<FaceAnnotation> faceAnnotations = response.getResponses().get(0).getFaceAnnotations(); if (faceAnnotations == null) { return false; } for (FaceAnnotation faceAnnotation : faceAnnotations) { printLog(faceAnnotation); if (faceAnnotation.getJoyLikelihood().equals(RESULT_VERY_LIKELY) || faceAnnotation.getJoyLikelihood().equals(RESULT_LIKELY)) { return true; } } return false; } /** * プログレスダイアログの設定および表示 */ private void setUpProgressDialog() { mProgressDialog.setMessage(mContext.getResources().getString(R.string.loading_message)); mProgressDialog.setProgressStyle(ProgressDialog.STYLE_SPINNER); mProgressDialog.setCancelable(false); mProgressDialog.show(); } /** * 音楽ファイルを再生するための設定 */ private void setUpMediaPlayer() { mMediaPlayer = new MediaPlayer(); mMediaPlayer.setOnCompletionListener(mOnCompletionListener); try (AssetFileDescriptor assetFileDescriptor = mContext.getAssets().openFd(SOUND_FILE_NAME)) { mMediaPlayer.setDataSource(assetFileDescriptor.getFileDescriptor(), assetFileDescriptor.getStartOffset(), assetFileDescriptor.getLength()); mMediaPlayer.prepare(); } catch (IOException e) { Log.e(TAG, e.getMessage()); } } /** * 各パラメータのログ出力 * @param faceAnnotation */ private void printLog(FaceAnnotation faceAnnotation) { Log.d(TAG, "getAngerLikelihood : " + faceAnnotation.getAngerLikelihood()); Log.d(TAG, "getBlurredLikelihood : " + faceAnnotation.getBlurredLikelihood()); Log.d(TAG, "getHeadwearLikelihood : " + faceAnnotation.getHeadwearLikelihood()); Log.d(TAG, "getJoyLikelihood : " + faceAnnotation.getJoyLikelihood()); Log.d(TAG, "getSorrowLikelihood : " + faceAnnotation.getSorrowLikelihood()); Log.d(TAG, "getSurpriseLikelihood : " + faceAnnotation.getSurpriseLikelihood()); Log.d(TAG, "getUnderExposedLikelihood : " + faceAnnotation.getUnderExposedLikelihood()); } }笑っているかどうかの判定処理は、checkSmileメソッドで行なっています。
FaceAnnotation#getJoyLikelihood()から、喜びの度合いを取得できます。
そして、喜びの度合いが「VERY_LIKELY(非常に高い)」、または「LIKELY(高い)」の場合に、笑っているものとみなしています。
FaceAnnotationに関しては、以下の公式ドキュメントを参照してください。https://cloud.google.com/vision/docs/detecting-faces?hl=ja#vision-face-detection-java
GitHub
https://github.com/hiesiea/SmileDetector
参考URL
- 投稿日:2019-05-23T13:49:42+09:00
TWA(Trusted Web Activity) アプリを aab(Android App Bundle) でリリースするときは気をつけてください!
はじめに
TWA は、 PWA サイトのアプリ版を簡単に作れる便利なやつです。
TWA の仕組みとして、 PWA サイト側に 「信頼して良いか」 を判断するための設定ファイル
.well-known/assetlinks.json
を配置する必要があります。今回その設定ファイルの値を間違っていたせいでハマったので、ぼくと同じ人が現れないように記事にしておきます。
誰かの助けになれば幸いです ?気をつけること
.well-known/assetlinks.json
のsha256_cert_fingerprints
の値どうするのか
Google Play Console - リリース管理 - アプリの署名 で、上記画像の該当箇所を確認してください。
これを間違えると、URLバーが永遠に出続けてしまいます。。似たような見た目で 「アップロード証明書」 という項目がありますがソチラではありません!
(aab ファイルでリリースしていない場合は画像のような画面は表示されないようです。)(おまけ) apk でリリースする場合
keytool -list -v -keystore Keystoreファイルパス -alias エイリアス名 -storepass パスワード -keypass パスワード上記コマンドで出力される
SHA256
の値を設定すれば良いです。
- 投稿日:2019-05-23T10:35:12+09:00
Google Playを使ったテストに関して[アルファ版、ベータ版、リリース版]
前置き
この記事では、Google Play Consoleを利用したテストを行う際に選択する必要があるアルファ版、ベータ版、リリース版といったアプリバージョンの違いや、内部テスト、クローズドテスト、オープンテストといったテスト方法の違いについてまとめています。
初めてテストを含めてアプリをリリースする方や、実際にリリースは行わないが、プロセスを確認しておきたい方などの参考になれば幸いです。
アプリリリース方法やテストの設定方法等の具体的な手順は、下記にあります他の方々の記事に非常に分かりやすく詳細に記載いただいていますのでそちらをご参照ください。Google Play での Android アプリの配信方法(インストールとアップデート)を試してみる
Google playでのベータ版配布機能についてAndroidアプリのバージョンについて(アルファ版、ベータ版、リリース版)
Google Play上にアプリをリリースするにはGoogle Play Consoleを利用します。Google Play Consoleでは下記の3つのバージョンを選択することができます。リリース版に移行するまでにテストを行い、品質を改善することが目的です。
アルファ版
アプリの安定性が最も低い試験的なバージョン。テストのグループは人数を少なくする。
ベータ版
アプリのリリース版に近い安定したバージョン。テストのグループは人数を多くする。
リリース版
最も安定したバージョン。テストは完了していることが前提です。
出典:アプリのベータ版テストを実施し、初期段階の貴重なフィードバックを得る/適切なテストの種類を選択します。テストについて(内部テスト、クローズドテスト、オープンテスト)
アルファ版、ベータ版のテストは、テスターの要件に沿ってテスト方法を下記の3つから選択することができます。
内部テスト
- 開発者自身や品質保証部門など、限られた少数のテスターによるテストを行う場合に選択します。
- 限られた少数のテスターのみがGoogle Playストアからアプリをインストールできます。
- テスターはGoogleアカウントかG Suiteアカウントによって登録されます
- テスターは、有料アプリであっても無料でインストールできます
有料アプリ: オープンテスト版またはクローズド テスト版を使用して有料アプリをテストする場合でも、テスターはアプリを購入する必要があります。内部テスト版を使用して有料アプリをテストする場合、テスターは無料でアプリをインストールできます。
出典:オープンテスト版、クローズド テスト版、内部テスト版をセットアップする
他のテストと大きく違う点は、リリースからテスターが利用できるようになるまでの時間の短さです。数分でテスターをGoogle Playストアからアプリをインストールできるようになります。
他のテストの場合だと、数十分ないし、数時間待つ必要があります。クローズドテスト
- 社員や信頼できるユーザーなどの小規模なグループでテストを行う場合に選択します。
- 内部テストより多くの限定されたテスターが、Google Playストアからアプリをインストールできます。
- テスターはGoogleアカウントかG Suiteアカウントによって登録されます。
- テスターは、有料アプリは購入する必要があります
なお、内部テスト、クローズドテストにおいてテスターに登録されていないGoogleアカウントからのアクセスしてもアプリをインストールすることはできません。
オープンテスト
- 大規模なグループでテストを行う場合に選択します。
- クローズドテストより多くの不特定多数のテスターが、Google Playストアからアプリをインストールできます。
- テスターは、有料アプリは購入する必要があります。
他のテストと大きく違う点はテスターの範囲です。内部テスト、クローズドテストテストはGoogleアカウントまたは、G Suiteアカウントにより明示的にテスターを登録します。対してオープンテストはテスターの最大数を設定するのみで明示的にテスターを指定することはありません。
URLを知るすべてのユーザーがテスターとしてテストを実施することができます。
各バージョンが選択できるテストの違い
アルファ版、ベータ版で利用できるテストが異なります。
なお、リリース版はテストが完了していることが前提となります。
内部テスト クローズドテスト オープンテスト アルファ版 ○ ○ ○→✕ ベータ版 ○ ○→✕ ○ 赤字の箇所は以前は選択可能でしたが、2019年現在では選択できなくなっています。前述のアルファ版、ベータ版の位置づけに基づいた変更だと思われます。下記 Android Developers中の記載です。
オープン アルファ版やクローズド ベータ版のテストを作成することはできなくなりました。すでに開始されている既存のオープン アルファ版やクローズド ベータ版テストには引き続きアクセスできます。
出典:オープンテスト版、クローズド テスト版、内部テスト版をセットアップする各テストにおけるテスター範囲の違い
トラック1 リスト2 テスター 内部テスト 1件/アプリ 1件/トラック 100人/リスト クローズドテスト 50件/アプリ 50件/トラック 2000人/リスト オープンテスト 1件/アプリ - 1000人以上 オープンテストにおいて、テスターを明示的に登録しないため、リストはありません。
またリストには個人を表すGoogleアカウントを登録または、複数人を表すGoogleグループを登録することも可能です。
Googleグループ内の人数に制限はありません。Google グループや Google+ コミュニティでテスターを管理したりできます。Google グループや Google+ コミュニティに人数制限はありません。
出典:開発チーム向けに追加のクローズド テスト版トラックを作成する
なお、以前は、Google+コミュニティもGoogleグループ同様にリストに登録可能でしたが、2019年現在では選択できなくなっています。これはGoogle+が終了することに基づいていると思われます。
注: 2019 年に予定されている Google+ の終了に伴い、クローズド テスト版のトラックに Google+ コミュニティを追加できなくなりました。Google+ コミュニティを使用してテスターを管理するようにすでに設定している場合は、移行期間中も引き続きご利用いただけます。Google+ コミュニティのテスターを Google グループに移行するか、代わりにメールアドレスでテスターを管理することをおすすめします。
出典:クローズド テスト版: Google グループでテスターを管理する
テスト・リリースを実際に行う前の概要理解に役立てば幸いです。
参考文献
オープンテスト版、クローズド テスト版、内部テスト版をセットアップする
アプリのベータ版テストを実施し、初期段階の貴重なフィードバックを得る
- 投稿日:2019-05-23T01:26:21+09:00
Linux/Ubuntu Androidアプリを実機で起動
はじめに
- Pixel 3aを買ったのでAndroidアプリを実機で起動したかったけどエラーになった
- 初めての作業だったのでメモっておく
- ほぼここに書いてある https://developer.android.com/studio/run/device
Andoird端末を開発者モード、USBデバッグオンにする
- 設定 -> 端末情報 -> ビルド番号 を7回タップして開発者モードを表示させる - 設定 -> システム -> 開発者向けオプション -> USBデバッグ をオンにする - USBデバッグ許可のダイアログが出たら許可するダイアログがでなければUSB抜き差しすれば良さそう
ルートで以下ファイルを作成する
$ sudo touch /etc/udev/rules.d/51-android.rules # 以下を記述 SUBSYSTEM=="usb", ATTR{idVendor}=="18d1", MODE="0666", GROUP="plugdev" $ chmod a+r /etc/udev/rules.d/51-android.rules18d1部分はGoogleのベンダーID、上に貼ったページに書いてある
Android端末がUSBに繋がっていればlsusbでも確認できる
18d1:4ee7
この部分の:
区切り左側の値$ lsusb Bus 002 Device 001: ID 1d6b:0003 Linux Foundation 3.0 root hub Bus 001 Device 002: ID 1038:137c SteelSeries ApS Bus 001 Device 005: ID 0853:0110 Topre Corporation Bus 001 Device 004: ID 0a12:0001 Cambridge Silicon Radio, Ltd Bluetooth Dongle (HCI mode) Bus 001 Device 003: ID 18d1:4ee7 Google Inc. Bus 001 Device 001: ID 1d6b:0002 Linux Foundation 2.0 root hubAndroid StudioでDevice選択が対象の端末になっていればRunボタンをクリックで実機でアプリ起動するはず
Android Studioの再起動などがいるかも
- 投稿日:2019-05-23T00:04:19+09:00
CameraXのCodelab試してみた
概要
I/Oで発表があったCameraX。Codelabsで公開されていたものを試してみました。ちょっとハマったとこがあったので、メモがてら書いておきます。
Getting Started with CameraXというのも、I/O Extended 2019 Tokyo@GDG にてCodelabsの課題が上がっていたからです笑
当日はセッションに興味がありCodelabには参加しませんでしたが、また機会があれば参加してみたい。Codelabの流れ
実際のCodelabをみてもらえたらいいと思うんですが、簡単に流れを書いておきます。Codelabのセクションとはちょっと変えてます。
- CameraXをgradleに追加
- view finder layoutを準備
- Cameraのパーミッションを付与
- Permission許可&チェックのコードを記述
- view finder(use case)を実装
- image capture(use case)を実装
- image analysis(use case)を実装
ポイント
use caseを作る。
- Preview(view finder): プレビュー
- ImageCaputure: 画像保存
- ImageAnalysis: 画像解析
作ったuse caseを、
CameraX.bindToLifecycle(this, preview, imageCapture, analyzerUseCase)
でlifecycleOwner(Activity)にbindするハマったところ
プレビューが縦に潰れる。。
before.ktval previewConfig = PreviewConfig.Builder().apply { setTargetAspectRatio(Rational(1, 1)) setCaptureMode(ImageCapture.CaptureMode.MIN_LATENCY) }.build()アスペクトをが1:1で設定しているのに、プレビューが縦に崩れる。。。
結局崩れるのはなぜかわからなかったんですが、公式のサンプル
の通りに実際のViewのディスプレイサイズを取得し設定したら治った。after.ktval metrics = DisplayMetrics().also { viewFinder.display.getRealMetrics(it) } val screenAspectRatio = Rational(metrics.widthPixels, metrics.heightPixels) val screenSize = Size(metrics.widthPixels, metrics.heightPixels) val previewConfig = PreviewConfig.Builder().apply { setTargetAspectRatio(screenAspectRatio) setTargetResolution(screenSize) }.build()Capture時に保存された画像は正常だったところを見ると、Viewに反映される際になにか問題があるのかも。時間が許せばもっと探ってみようかな。。
所感
lifecycleにbindでき、自分で面倒を見ないでいいのがすごく楽だと感じました。
もうすこしドキュメント見て、触ってみようと思えました。
- 投稿日:2019-05-23T00:04:19+09:00
CameraXのCodelabs試してみた
概要
I/Oで発表があったCameraX。Codelabsで公開されていたものを試してみました。ちょっとハマったとこがあったので、メモがてら書いておきます。
Getting Started with CameraXというのも、I/O Extended 2019 Tokyo@GDG にてCodelabsの課題が上がっていたからです笑
当日はセッションに興味がありCodelabsには参加しませんでしたが、また機会があれば参加してみたい。実装の流れ
実際のCodelabsをみてもらえたらいいと思うんですが、簡単に流れを書いておきます。Codelabsのセクションとはちょっと変えてます。
- CameraXをgradleに追加
- view finder layoutを準備
- Cameraのパーミッションを付与
- Permission許可&チェックのコードを記述
- view finder(use case)を実装
- image capture(use case)を実装
- image analysis(use case)を実装
ポイント
use caseを作る。
- Preview(view finder): プレビュー
- ImageCaputure: 画像保存
- ImageAnalysis: 画像解析
作ったuse caseを、
CameraX.bindToLifecycle(this, preview, imageCapture, analyzerUseCase)
でlifecycleOwner(Activity)にbindするハマったところ
プレビューが縦に潰れる。。
before.ktval previewConfig = PreviewConfig.Builder().apply { setTargetAspectRatio(Rational(1, 1)) setCaptureMode(ImageCapture.CaptureMode.MIN_LATENCY) }.build()アスペクトをが1:1で設定しているのに、プレビューが縦に崩れる。。。
結局崩れるのはなぜかわからなかったんですが、公式のサンプル
の通りに実際のViewのディスプレイサイズを取得し設定したら治った。after.ktval metrics = DisplayMetrics().also { viewFinder.display.getRealMetrics(it) } val screenAspectRatio = Rational(metrics.widthPixels, metrics.heightPixels) val screenSize = Size(metrics.widthPixels, metrics.heightPixels) val previewConfig = PreviewConfig.Builder().apply { setTargetAspectRatio(screenAspectRatio) setTargetResolution(screenSize) }.build()Capture時に保存された画像は正常だったところを見ると、Viewに反映される際になにか問題があるのかも。時間が許せばもっと探ってみようかな。。
所感
lifecycleにbindでき、自分で面倒を見ないでいいのがすごく楽だと感じました。
もうすこしドキュメント見て、触ってみようと思えました。