ばやしこのブログ

プログラミング初心者がメモとかを書きます。基礎的なこともメモとして。偏食系のアニオタ。

Androidアプリのアイコンが変

先日、スマートフォンを新しいものに買い替えた。
SonyXperia XZ1でOSはAndroid 8.0。
ずっとAQUOS系を使ってたのでなかなか新鮮。

というわけで自分の作ったアプリをインストールしてみたのだがアイコンが変。
どうもデフォルトのアイコンが使われているっぽい。

これはAndroid7.1から追加された機能でRound Icon、つまり丸形アイコンが定義できるようになったことが原因らしい。
要するにround iconを設定してなかったことが原因らしい。

 <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">

こんな感じでandroid:roundIconを設定してはいたが画像ファイルの方をデフォルトから弄ってなかったのが原因っぽい。
ちゃんとこれ用にアイコンを作ろうかと思ったけどAndroid8.0からはAdaptive Iconになっているらしく面倒なのでとりあえず今までのアイコンを使いまわすことにした。

 <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">

行儀はよくないけどとりあえずこれで解決。
round_icon使う場面でも元のアイコンを使うようにしてるだけだから解決と言えるかは微妙だけど。

NFC機能がないと動かないアプリだったからエミュレータで動かしていなかったけど、ランチャーアイコンで違いがあるのは盲点だった。

MySQLの死と監視スクリプト

MySQLサーバーがいつの間にか落ちてた.理由はわからん.

というわけでWEBアプリとスマホアプリ用APIを動かしているサーバーのMySQLが落ちていた.
アプリ使ってくれている人からの問い合わせで気づいた.
最近忙しくて放置気味だったから全く気付かなかった.
ありがたいとともに申し訳ない.


ログ見てもそれらしい記述がなくて原因は不明なので,落ちたときにすぐ気づける体制を整えることにした.
本当は原因突き止めるべきなんだろうけど.

以下のスクリプトをcronで定期実行することによって今後MySQLが落ちたときはメールで通知が行くようにした.

# coding: utf-8                                                                                                                                        
import MySQLdb
import os
import traceback

#MySQL起動確認                                                                                                                                         
try :
    connection = MySQLdb.connect(db='DB_NAME',user='USER',charset='utf8mb4')
except :
    message = traceback.format_exc()
    f = open("error.mail","w")
    f.write("MySQLサーバーに接続できません\nTraceback:\n "+message)
    f.close()
    os.system('cat error.mail | mail -s "サーバーエラー" -r rabbit@vayacico.com ????????????@softbank.ne.jp')
    os._exit(0)

MySQLに接続して例外吐いたらメールを送るシンプルなスクリプト
本当はGmailに通知したかったけど迷惑メールチェックで弾かれるのか届かないので迷惑メールチェック弱そうなソフトバンクで受け取ることにした.
今後はAPI叩いて正当性確認する処理も追加したい.

Androidアプリ内のSQLiteデータベースファイルをパソコンに持ってくる

毎回引っかかてる気がしたのでいい加減覚えるためにメモ
ググってよく見つかる方法だとなぜかうまくいかないし

環境に依存してそうなので以下に環境を載せておく

  • 502sh(Android5.1.1)
  • Windows10

以下がpullするまでのコマンド

C:\Users\vayacico>adb shell
shell@SG502SH:/ $ run-as com.vayacico.suicakeeper2
shell@SG502SH:/data/data/com.vayacico.suicakeeper2 $ chmod 777 databases/dbname.db
shell@SG502SH:/data/data/com.vayacico.suicakeeper2 $ exit
shell@SG502SH:/ $ cp /data/data/com.vayacico.suicakeeper/databases/user.db /sdcard/
shell@SG502SH:/ $ exit
C:\Users\vayacico>adb pull /sdcard/user.db
/sdcard/user.db: 1 file pulled. 1.8 MB/s (28672 bytes in 0.015s)

C:\Users\vayacico>

ググって見つかる多くの方法だとrun-as内で直接SD直下にコピーできてるっぽいんだけど自分の環境でやるとPermission deniedされる.
というわけで脳死でchmod 777して通常ユーザーから見れるようにしてからコピーした.

バージョン間の問題かな?

Androidで位置情報の取得

アプリに位置情報を利用した機能を実装しようと思い、Androidでの現在位置の取得について調べた.
ブログやら記事やら見てもどれが最新なのかいまいちわからなかったので今回は公式のトレーニングに従って実装してみた.

developer.android.com

この方法はGoogle Play Servicesを利用した方法でサービスが保持している直近の位置情報を取得することができる.
保持している情報というわけで過去の情報となるわけだがほとんどの場合で現在地と一致するらしい.

準備

Google Play Servicesを利用するとのことなのでGoogle Play Servicesのコンポーネントをプロジェクトに導入する必要がある.
Set Up Google Play Services  |  Google APIs for Android  |  Google Developers
導入の方法に関してはここを参考にした.
Google Play Servicesで1つのライブラリになっているのかと思いきやサービスごとに分かれているらしい.
今回は位置情報サービスにアクセスするのでそれっぽいGoogle Location and Activity Recognitionを選んで導入.
appの方のbuild.gradleのdependenciesに以下を追記.
バージョンに関しては上記のサイトのリストが最新バージョンになっている(らしい)のでこれをコピペした.

    implementation 'com.google.android.gms:play-services-location:11.8.0'

追記後にsyncを行うことでGoogle Location and Activity Recognitionのモジュールが導入される.

権限

位置情報にアクセスするためには権限が居るのでAndroidManifest.xmlにその旨を記述する.
権限には以下の2種類がある.

  • ACCESS_COARSE_LOCATION
    • 都市レベル(?)の精度で取得する(たぶん市町村レベル)
  • ACCESS_FINE_LOCATION
    • 高精度で位置情報を取得

ここで選択した精度がAPIによって返される位置情報の精度を決定する…らしいのだが高精度の位置情報を取得しようとしてACCESS_FINE_LOCATIONだけを書いて実行したらいくら待ってもコールバックが呼ばれない事態に陥った.
試しにACCESS_COARSE_LOCATIONとACCESS_FINE_LOCATIONを併記したら高精度で位置情報を取得できたので併記する必要があるらしい.
というわけで高精度で位置情報を取得する場合は以下のようになった.
市町村レベルでよければACCESS_FINE_LOCATIONの方を消せばよい.

<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />

実装

まずはこれで初期化を行う.

FusedLocationProviderClient mFusedLocationProviderClient = LocationServices.getFusedLocationProviderClient(this);

そして位置情報の取得を行う.
結果はコールバックで帰ってくるので登録を行う.

mFusedLocationProviderClient.getLastLocation().addOnSuccessListener(new OnSuccessListener<Location>() {
    @Override
    public void onSuccess(Location location) {
        if(location==null){
            Log.d("Result","location is null");
        }else{
             Log.d("Result","("+location.getLongitude()+","+location.getLatitude()+")");
        }
    }
});

この例では緯度と経度をログに出力している.
また,環境によってはnullが帰ってくることもあるらしいのでチェックを行う.

ここまでのコードで位置情報の取得はできるのだがパーミッションのチェックを行わないとAndroid Studioから怒られるので(ビルドもできない?)getLastLocation()を呼ぶ前に以下のようなコードを挿入する必要がある.

if(ActivityCompat.checkSelfPermission(getApplicationContext(), Manifest.permission.ACCESS_COARSE_LOCATION)!= PackageManager.PERMISSION_GRANTED){
    return;
}

これで位置情報を取得できる.

以下が実装した例.
github.com

Last Locationということもあり待ち時間なく取得できる.
位置情報の取得というと時間がかかるイメージがあったけどこれなら使いやすいかも.

追記

自分の持ってる端末のバージョンがAndroid5.1だから問題なかったけどパーミッションまわりで許可を求めるダイアログが必要らしい.

シンボリックリンクを利用してOnedriveを利用

自分の所属している大学院にもやっとOffice365が導入されて便利に使わせてもらっているのだが不満がひとつだけある.

f:id:vayacico:20171228113251p:plain

そう、Onedriveのディレクトリ名に大学名が入ることである.
別に大学名がダサいのが嫌というわけでもなく(ダサいとは思っているが)ディレクトリ名に日本語とスペースが入ると困るという実利的な問題である.
ファイルを置いておく分には問題ないのだがソースコードを置こうとするとちょっとだけ問題がでてきた.
各種ツールがうまく動かないのである.
たとえばAndroid Studioだとこんな感じに怒られる.

 Your project path contains non-ASCII characters. This will most likely cause the build to fail on Windows. Please move your project to a different  directory. 

そもそもスペース含むパスは色々と不便だ.
素直にローカル保存にしておけばいいのだがノーパソしか持っていない自分としてはクラウドに預けてないと不安である.

ディレクトリ名を変更する方法を調べてみてもOffice365の管理者に頼む以外の方法が見つからなかったので実体はPC内の行儀の良い場所においてシンボリックリンクをOneDrive内に作成することにした.

というわけで以下のコマンドでシンボリックリンク作成した.

C:\Users\vayacico>mklink /D "C:\Users\vayacico\OneDrive - 埼玉大学\private\project" "C:\Users\vayacico\Documents\project"
C:\Users\vayacico\OneDrive - 埼玉大学\private\project <<===>> C:\Users\vayacico\Documents\project のシンボリック リンクが作成され ました

\Dオプションでディレクトリのシンボリックリンクを作るということになるらしい

シンボリックリンクでちゃんと中身をアップロードしてくれるか不安だったがブラウザから確認したところ実体もちゃんとアップされていたのでこれで問題ないらしい.

ひとまずこれでノーパソに何かがあっても致命傷を負わない環境ができた.
ノーパソに何かがあった時点で何もできなくなる環境は変わらないが.

Webアプリが動いてるサーバーへwordpressの導入

さくらのVPSを借りてWebアプリやスマホアプリのためのAPIを動かしているがトップページには申し分程度のHTMLファイルしか置いてなかったのでwordpressを利用してリッチにしてみた.
別に向こうでブログ書く気はないけど.

今回私が求めた条件は以下の通り.

  • 既存のWebサービスに影響を及ぼさないこと
  • vayacico.comにアクセスしたらWordPressのトップページが表示されること
  • ルートディレクトリを汚さないこと

この条件を満たすためにルートディレクトリのサブディレクトリにwordpressを展開してApachemod_rewriteモジュールでURLを書き換えることにした.

まずは通常通りwordpressのインストールする.

インストールといってもファイルを持ってきて置くだけらしい.
どの段階までをインストールというのにかに依るけど.

$ wget https://ja.wordpress.org/wordpress-4.8.2-ja.zip
$ unzip wordpress-4.8.2-ja.zip 
$ sudo cp -r wordpress /var/www/html/

続いてデータベースの作成.
MySQLは既に導入済みだったのでユーザーとデータベースを作るだけ.

mysql> CREATE USER user IDENTIFIED BY 'password';
mysql> CREATE DATABASE wordpress DEFAULT CHARACTER SET utf8 COLLATE utf8_unicode_ci;
mysql> GRANT ALL ON user.* to wordpress_user;

とりあえずこれで/wordpress/以下で動かせるようになる.

自分は先にWordPressの初期設定をやってしまったので後でひと手間かかった.
先にmod_rewriteの設定をやった方が楽だったのかもしれないかも.

アクセスするとこんな感じの設定画面になるのであとは従っていけば完了する.
f:id:vayacico:20171029163510p:plain

ファイルに書き込めないだの言われたのでEmacsから手動で作成した.
f:id:vayacico:20171029163654p:plain

あとは従ってやればOKだった.

続いてmod_rewriteの設定を行う.

前にも散々弄った形跡があるので(なにをしたかったのかは忘れた)これだけで動くのかわからないが自分は以下の設定でいい感じに動いてくれた.

<IfModule mod_rewrite.c>
        RewriteEngine On
        RewriteRule ^/api/(.*)$ /api/$1 [L]
        RewriteRule ^/twitgallery/(.*)$ /twitgallery/$1 [L]
        RewriteRule ^/(.*)$ /wordpress/$1 [L]
</IfModule>

/api/以下でスマホアプリ用のAPIが動いてるので,要求URIが/api/~だったら/api/~に書き換えて(書き換えてないけど)判定を終了する.
同じように/twitgallery/以下でもWebアプリが走っているのでこれも書き換えて(書き換えてないけど)判定を終了.
そして最後のルールで/~を全て/wordpress/~に書き換える.

こうすることでAPIやWebアプリだった場合以外を全部WordPressに飛ばすことができた.


これで終わり…だと思っていたのだがアクセスしてみるとCSSが効いてないデザインが崩れた見た目になる,
どうやらWordPress内でURI情報を保持して使っているらしい.
デベロッパーコンソールで見てみるとどうもドメインも含む完全なURLでCSSやJSにアクセスしてるっぽかった.

設定画面から書き換えできるらしいので一度mod_rewriteの設定を元に戻し管理画面に入る.

[設定]->[一般]にそれっぽい設定があったので書き換える.

f:id:vayacico:20171029165531p:plain

変更を保存するとその場で切り替わるらしく404になるが設定は正常に完了している.

そしてmod_rewriteの設定をもう一度変えることで全部まるっと思った通りに動いた.

f:id:vayacico:20171029170047p:plain

内容はまだ全く作っていないがとりあえずこれで完了.
思いの外簡単であった.
セキュリティがなんとなく怖くもあるが.

ログインページをローカルアクセスオンリーにしてsshポートフォワーディングからのみアクセスできるようにしたかったけど上手くいかない.
なんで相対パスで偏移してくれないのか….
ここら辺はまた今度やろう.

TensorFlowの画像分類を利用したAndroidアプリを作ってみた

TensorFlowのAndroid実装を利用したアプリを作ってみたので流れをまとめる.
ちなみにディープラーニングのコードには一切触れてない入門書読んだだけのディープラーニングにわかなのでご了承を.
何か頓珍漢なこと言ってたら教えてください.

環境

モデルの準備

モデルが無ければ何も始まらないのでまずはモデルを用意する.
用意するといっても自分で1から作るのはしんどいといので既存のモデルを転移学習することにした.
自分でモデル設計するスキルも1から学習させられるようなスペックのパソコンも持ってないし.

既存のモデルと学習済みの重みを元に新しいモデルを作ることをFine-tuning(転移学習)というらしく,これにより少ない学習データと学習時間でそれなりの結果が得られる.
Fine-tuningは以下のチュートリアルに従うことによって簡単にできる.

How to Retrain an Image Classifier for New Categories  |  TensorFlow

まずはTensorflow本体をGitから持ってくる.

$ git clone https://github.com/tensorflow/tensorflow

次にビルドを行う.
初回のビルドはそれなりに時間がかかる.

$ cd tensorflow/
$ ./configure
$ bazel build tensorflow/examples/image_retraining:retrain

途中でライブラリが足りないだの何だの言われるので適宜持ってくる.
ノーパソで1時間ちょいかかった.

これでFine-tuningを行う準備は完了.
正直拍子抜けするほど簡単であるがこれだけでFine-tuningを行うことができるらしい.
任意のモデル使ったり細かいカスタマイズとかは出来ないけど.

データセットはラベル名をディレクトリ名として適当な場所に置く.
自分は大体以下の感じになった.

.
├── ajiazou
│   ├── 000000274001L.jpg
│   ├── 001(2).jpg
│   ├── 004.jpg
│   ├── 008.jpg
│   ├── ...
│ 
├── amerikabaku
│   ├── 000000274001L.jpg
│   ├── 001(2).jpg
│   ├── 004.jpg
│   ├── 008.jpg
│   ├── ...
│  

今回は画像検索からもってきた動物画像を学習データとして使った.
動物の種類は46種.
それぞれ100-200枚程度集められた.

そしてスクリプトを実行する.
image_dirに学習データを置いてあるディレクトリを指定する.

$ bazel-bin/tensorflow/examples/image_retraining/retrain --image_dir=~/dataset/ueno/

ここでもライブラリがないだの何だの言われるので適当にpipでインストールする.
後は待っているだけで自動的にモデルをダウンロードしてきてFine-tuningが行われる.
画像でないファイルが含まれると容赦なく落ちるので事前に確認しておく.
デフォルトでダウンロードされるモデルはGoogleのInception v3となってる.
オプションで指定することによりMobileNetという軽量モデルを使うことも可能である.
ただ試してみた感じ汎化性が微妙っぽい.
あまりに結果が悪すぎるので使い方が間違ってる可能性もあるけど.
モデルの指定の他にトレーニング回数(デフォルトで4000)や学習時の画像加工(反転,拡大,明るさ)の指定もできる.
ちなみにモデルのライセンスはApache2.0なのでいろいろと安心.
ちなみにTensoBordも使えるらしいので手軽にディープラーニングやってる感を味わえる.

途中経過はコンソールにも表示されるので見てて楽しい.
すぐに飽きたけど.

f:id:vayacico:20171021013959p:plain

学習されたモデルは特に指定してなければ/tmp/output_graph.pbとしてラベルが書かれたoutput_label.txtとペアで保存される.

PC上で使う分にはこれで終了なのだがAndroidで使うためにはもう一手間必要となる.
AndroidのTensorflowで扱うためには無駄のないモデルである必要があるらしく,つまりは学習のためのみに使われるドロップアウト層や結果に関与しない層を取り除く必要があるとのこと.
聞くと面倒そうだがこれもコマンド一発で行うことができる.

$ bazel build tensorflow/python/tools:optimize_for_inference 
$ bazel-bin/tensorflow/python/tools/optimize_for_inference --input="/tmp/output_graph.pb" --input_names="Mul" --output_names="final_result"

input_namesとoutput_namesはそれぞれ入力層と出力層の名前らしい.
正直よくわかってないけどinception v3とMobileNetは共通してこれでできるので問題ない.

これでAndroidで使うためのモデルの準備が完了した.
出力されたモデルファイルとラベルファイルをAndroidアプリのassetに持っていく.

Androidアプリの作成

続いてAndroidサイドの実装を行う.
tensorflow/tensorflow/examples/android at master · tensorflow/tensorflow · GitHub
ここに書いてある通り,gradleに書くだけで自動的にダウンロードしてきてくれて使える状態になる.

allprojects {
    repositories {
        jcenter()
    }
}

dependencies {
    compile 'org.tensorflow:tensorflow-android:+'
}

ただnightly-android [Jenkins]から最新のビルドを持ってきて使った方が早かったので自分はこっちを使った.
数時間おきに安定ビルドが行われているちょっとよくわからない世界である.

これでAndroidでTensorflowを使えるようになったわけだけど,自分で画像の形式変換や結果の解析のコードを書くのは面倒なのでTensorflowのデモアプリに含まれるクラスを持ってきて便利に使わせてもらう.
使わせてもらうクラスは以下の2つ.


簡単に言うとTensorflowを簡単に使うためのクラスと画像分類を簡単に使うためのクラス.
ここまでくると後は簡単.

まずは各変数の定義

int INPUT_SIZE = 299;
int IMAGE_MEAN = 128;
float IMAGE_STD = 128;
String INPUT_NAME = "Mul";
String OUTPUT_NAME = "final_result";
String MODEL_FILE = "file:///android_asset/optimized_graph.pb";
String LABEL_FILE = "file:///android_asset/output_labels.txt";

MODEL_FILEとLABEL_FILEでassetsに置いたモデルファイルとラベルファイルのパスを指定.
INPUT_SIZEは入力サイズでinception_v3を使う場合は299となる.
MobileNetを使う場合は指定したモデル名に書いてあるサイズを指定(MobileNet_v1_1.0_???)する.

次に初期化.

Classifier classifier = TensorFlowImageClassifier.create(getAssets(), MODEL_FILE, LABEL_FILE, INPUT_SIZE, IMAGE_MEAN, IMAGE_STD, INPUT_NAME, OUTPUT_NAME);

そしてこれで識別の実行.
それなりに時間がかかるので自分はAsyncTaskで実行させた.
スレッドで実行させると速度がガクッと下がるのが謎.
軽く調べた感じネイティブ実装でCPUに割り振るときのうんたらかんたらとか書いてあったがよく分からなかった.

List<Classifier.Recognition> results = classifier.recognizeImage(bitmap);

与えるBitmapはARGB形式(Bitmap.Config.ARGB_8888)でサイズがINPUT_SIZE x INPUT_SIZEである必要がある.
デモアプリに含まれるクラスでサイズの変換できる.
tensorflow/ImageUtils.java at master · tensorflow/tensorflow · GitHub

これでresultにラベル名と信頼度のリストが格納され無事分類が完了する.

実装したアプリ

以上の流れで実装してみたアプリを動物園で試してみた結果がこちら.
UI自体はWebViewで作った.


転移学習でラッキーさんに動物を分類してもらった


うまくいく動物相手だとわりと安定して正しく識別してくれる.
動画だとそれなりにいい感じに動いてるけど実際はダメダメ.
失敗動画はそんなに録画してないだけ.
撮影環境にかなりシビアである.
原因としては画像検索で得られる画像が良くも悪くも綺麗すぎるのが考えられる.
やはりプロが一眼で撮ったであろう野生生物とスマホのカメラで撮った動物園の動物とでは条件が違いすぎたのかもしれない.

ここでも述べられている通りデータの特徴の違いが大きく影響したのだと考えられる.
まぁ当たり前ではあるが.
https://qiita.com/nonbiri15/items/b29fe079d359d531bf85qiita.com

動物園でデータ集めればもうちょいいい結果になるのかもしれない.

まとめ

最終的なアプリとしてはちょっと悔しい結果ではあったが,専門知識を持ち合わせていない自分がここまで簡単にできるようになるとはディープラーニングもずいぶん裾野が広がってきたように感じる.
最先端っぽいことが簡単に実装できちゃうとやっぱり楽しい.
最先端の人たちにとっては画像分類なんかもう古いみたいになってるのかもしれないけど.
ググるとなんか凄いことばっかりやってるし.
まぁ順調に自分が扱えるところまで下りてきてると考える夢が広がる.