- 投稿日:2021-01-10T22:36:07+09:00
getPackageManagerでインストールアプリ一覧が表示されない(SDK30・Android11)
これまでgetPackageManager()#getInstalledPackages()を用いれば簡単に端末内のインストールアプリ情報を取得することができました。
しかし、Android 11(SDK30)からは単に下記のようなコードを記載するだけではシステムアプリ情報しか取得できなくなりました。例)デバイス内のインストールアプリをLog出力する
MainActivity.javaPackageManager pm = getApplicationContext().getPackageManager(); List<PackageInfo> appInfoList = pm.getInstalledPackages(0); for(int i =0;i<appInfoList.size();i++){ Log.d("MainActivity",appInfoList.get(i).applicationInfo.loadLabel(pm).toString()); }何がダメだったか
Android 11(SDK30)からはセキュリティ的に厳しくなっており、外部パッケージへのアクセスについてはManifestに明示する必要があります。
解決策
AndroidManifest.xmlにパーミッションの記述を書いてしまう。
AndroidManifest.xml<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES"/>…全部許可してしまうので非推奨なやり方だと思われます。
本来は<queries>
で囲って個別に明示するのが正しい方法なのかも。参考
- 投稿日:2021-01-10T22:36:07+09:00
getPackageManagerでインストールアプリ一覧が表示されない(Android11・APIレベル30)
これまでgetPackageManager()#getInstalledPackages()を用いれば簡単に端末内のインストールアプリ情報を取得することができました。
しかし、Android 11(APIレベル30)からは単に下記のようなコードを記載するだけではシステムアプリ情報しか取得できなくなりました。例)デバイス内のインストールアプリをLog出力する
MainActivity.javaPackageManager pm = getApplicationContext().getPackageManager(); List<PackageInfo> appInfoList = pm.getInstalledPackages(0); for(int i =0;i<appInfoList.size();i++){ Log.d("MainActivity",appInfoList.get(i).applicationInfo.loadLabel(pm).toString()); }何がダメだったか
Android 11(APIレベル30)からはセキュリティ的に厳しくなっており、外部パッケージへのアクセスについてはManifestに明示する必要があります。
解決策
AndroidManifest.xmlにパーミッションの記述を書いてしまう。
AndroidManifest.xml<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES"/>…全部許可してしまうので非推奨なやり方だと思われます。
本来は<queries>
で囲って個別に明示するのが正しい方法なのかも。参考
- 投稿日:2021-01-10T21:11:33+09:00
ConstraintLayoutの高級属性を使いましょう
現在プロジェクトを新たに生成する時、ConstraintLayoutが自動で作られました。
今回は開発の実用例によって説明します。GuideLine
- 補助線の方向を設置する ログイン画面でよく使われたレイアウトです。 android:orientation="vertical"
layout_constraintGuide_begin 左側と上の距離
layout_constraintGuide_end 右側と下の距離
layout_constraintGuide_percent パーセント%で設定<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent" android:layout_height="match_parent"> <androidx.constraintlayout.widget.Guideline android:id="@+id/guideline" android:layout_width="wrap_content" android:layout_height="wrap_content" android:orientation="vertical"//縦のラインを設定 app:layout_constraintGuide_begin="120dp" //縦のラインを設定する場合は、左から120dp /> <TextView android:id="@+id/tv_username" android:layout_width="wrap_content" android:layout_height="60dp" android:layout_marginTop="72dp" android:gravity="center_vertical" android:text="用户名" app:layout_constraintEnd_toStartOf="@+id/guideline" app:layout_constraintTop_toTopOf="parent" /> <TextView android:id="@+id/tv_password" android:layout_width="wrap_content" android:layout_height="60dp" android:gravity="center_vertical" android:text="密码" app:layout_constraintEnd_toStartOf="@+id/guideline" app:layout_constraintTop_toBottomOf="@+id/tv_username" /> <EditText android:id="@+id/et_username" android:layout_width="200dp" android:layout_height="wrap_content" app:layout_constraintBottom_toBottomOf="@+id/tv_username" app:layout_constraintStart_toStartOf="@+id/guideline" /> <EditText android:layout_width="200dp" android:layout_height="wrap_content" app:layout_constraintBottom_toBottomOf="@+id/tv_password" app:layout_constraintStart_toEndOf="@+id/tv_password" /> </androidx.constraintlayout.widget.ConstraintLayout>Group
1.constraint_referenced_idsの属性を通じて、レイアウトのネストを減らせます。
<androidx.constraintlayout.widget.ConstraintLayout 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="com.hencoder.Helpers"> <Button app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" android:id="@+id/button" android:text="group" android:layout_width="wrap_content" android:layout_height="wrap_content"/> <androidx.constraintlayout.widget.Group android:id="@+id/group" android:layout_width="wrap_content" android:layout_height="wrap_content" //コントロールのグループに対してsetVisibilityを同一に設定できます app:constraint_referenced_ids="view,view1,view7,view8" /> <ImageView android:id="@+id/view1" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="48dp" android:src="@mipmap/ic_launcher" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" /> <ImageView android:id="@+id/view" android:layout_width="wrap_content" android:layout_height="wrap_content" android:src="@mipmap/ic_launcher" app:layout_constraintStart_toEndOf="@+id/view1" app:layout_constraintTop_toTopOf="@+id/view1" /> <ImageView android:id="@+id/view7" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginStart="80dp" android:layout_marginTop="72dp" android:src="@mipmap/ic_launcher" app:layout_constraintStart_toEndOf="@+id/view1" app:layout_constraintTop_toTopOf="@+id/view1" /> <ImageView android:id="@+id/view8" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginStart="8dp" android:layout_marginTop="72dp" android:src="@mipmap/ic_launcher" app:layout_constraintStart_toEndOf="@+id/view1" app:layout_constraintTop_toTopOf="@+id/view1" /> </androidx.constraintlayout.widget.ConstraintLayout>ConstraintSet
setContentIdを使用して、指定したコントロールをプレースホルダーの位置に配置します。
<androidx.constraintlayout.widget.ConstraintLayout 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:id="@+id/constraintLayout" android:layout_width="match_parent" android:layout_height="match_parent" tools:context="com.hencoder.PlaceHolder"> <androidx.constraintlayout.widget.Placeholder android:id="@+id/placeholder" android:layout_width="48dp" android:layout_height="48dp" android:layout_marginTop="16dp" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" /> <androidx.appcompat.widget.AppCompatImageView android:id="@+id/favorite" android:layout_width="48dp" android:layout_height="48dp" android:onClick="onClick" android:src="@drawable/ic_favorite_black_24dp" android:tint="#E64A19" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toStartOf="@id/mail" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" /> <androidx.appcompat.widget.AppCompatImageView android:id="@+id/mail" android:layout_width="48dp" android:layout_height="48dp" android:onClick="onClick" android:src="@drawable/ic_mail_black_24dp" android:tint="#512DA8" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toStartOf="@id/save" app:layout_constraintStart_toEndOf="@id/favorite" app:layout_constraintTop_toTopOf="parent" /> <androidx.appcompat.widget.AppCompatImageView android:id="@+id/save" android:layout_width="48dp" android:layout_height="48dp" android:onClick="onClick" android:src="@drawable/ic_save_black_24dp" android:tint="#D32F2F" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toStartOf="@id/play" app:layout_constraintStart_toEndOf="@id/mail" app:layout_constraintTop_toTopOf="parent" /> <androidx.appcompat.widget.AppCompatImageView android:id="@+id/play" android:layout_width="48dp" android:layout_height="48dp" android:onClick="onClick" android:src="@drawable/ic_play_circle_filled_black_24dp" android:tint="#FFA000" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toEndOf="@+id/save" app:layout_constraintTop_toTopOf="parent" /> </androidx.constraintlayout.widget.ConstraintLayout>画像を押すと位置を交換できます
class PlaceHolder : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_place_holder) } fun onClick(view: View) { findViewById<Placeholder>(R.id.placeholder).setContentId(view.id) } }Flow
wrapMode
chain aligned none(デフォルト)
<androidx.constraintlayout.widget.ConstraintLayout 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="com.hencoder.FlowActivity"> <androidx.constraintlayout.helper.widget.Flow android:id="@+id/flow" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_marginStart="16dp" android:layout_marginTop="16dp" android:background="@color/colorAccent" android:orientation="horizontal" app:flow_wrapMode="chain" //モード app:flow_verticalGap="16dp" app:flow_horizontalGap="16dp" app:constraint_referenced_ids="view1,view2,view3,view4,view5,view6" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" /> <View android:id="@+id/view1" android:layout_width="80dp" android:layout_height="80dp" android:background="@color/colorPrimaryDark" /> <View android:id="@+id/view2" android:layout_width="80dp" android:layout_height="80dp" android:background="@color/colorPrimaryDark" /> <View android:id="@+id/view3" android:layout_width="80dp" android:layout_height="80dp" android:layout_margin="8dp" android:background="@color/colorPrimaryDark" /> <View android:id="@+id/view4" android:layout_width="80dp" android:layout_height="80dp" android:background="@color/colorPrimaryDark" /> <View android:id="@+id/view5" android:layout_width="80dp" android:layout_height="80dp" android:background="@color/colorPrimaryDark" /> <View android:id="@+id/view6" android:layout_width="80dp" android:layout_height="80dp" android:background="@color/colorPrimaryDark" /> </androidx.constraintlayout.widget.ConstraintLayout>この開発する時よく使う四つ属性を紹介しました。実はどうやってこの属性を活用するのが大事だと思ってます。
- 投稿日:2021-01-10T20:23:58+09:00
AndroidスマホをNFCタグにかざして家電を操作する方法
iPhone(iOS13以降)ではショートカットを利用してNFCタグにスマホをかざして家電を操作する方法がありますが、Androidスマホでも同じことができないか色々調べて試してみようと思いました。
そこで今回はNature Remoを利用してNFCタグにAndroidスマホをかざして家電を操作する方法についてまとめました準備するもの
・PC
・NFC対応Androidスマホ
・Nature Remo(旧版でもminiでもOK)
https://www.amazon.co.jp/Nature-%E3%82%B9%E3%83%9E%E3%83%BC%E3%83%88%E3%83%AA%E3%83%A2%E3%82%B3%E3%83%B3-Remo-%E3%83%8D%E3%82%A4%E3%83%81%E3%83%A3%E3%83%BC%E3%83%AA%E3%83%A2-Remo-1W3/dp/B08BLSLWH4
・NFCタグ(対応しているものをお使いください、以下は例です)
https://www.amazon.co.jp/gp/product/B079JTYTWV/
・Nature Remoアプリ
https://play.google.com/store/apps/details?id=global.nature.remo&hl=ja&gl=US
・NFC Tools Pro Editionアプリ(HTTP POSTするために有料版を使用)
https://play.google.com/store/apps/details?id=com.wakdev.nfctools.pro&hl=ja&gl=US方法
Nature RemoのCloud APIを使用します。それには、「アクセストークン」と「Signal ID」が必要となります。
アクセストークンの発行
まずは以下のリンク先から、Nature Remoのアプリで登録したアカウントを使いログインをして、「home.nature.global」からのアクセスを許可します。
アクセスを許可すると以下の画面へ移行するので「Go」をクリックします。
すると、以下のようなNature Remoのアクセストークンを管理できるページへ移動するので、ページ内の左下あたりにある「Generate access token」をクリックします。
「Generate access token」をクリックすると、以下の画面のように新しいアクセストークンが表示され、有効なアクセストークンのリストに追加されます。
また、表示されているアクセストークンの下にある「Copy」をクリックするとアクセストークンがクリップボードにコピーされます。
上記のアクセストークンが表示されているページは、更新するとアクセストーク表示が消えてしまい、再表示することはできません。
そのため、アクセストークンがわからなくなった場合には、もう一度「Generate access token」をクリックして新しいアクセストークンを発行する必要があります。
Signal IDについて
Nature Remoアプリでリモコンのボタンを一つづつ登録すると、ボタン毎にSignal IDが振られます。そのSignal IDは、Cloud APIでリモコン送信を行う際に利用します。
Signal IDなどのリモコン情報を取得する方法
Cloud APIを使いSignal IDを含むリモコン情報を取得するには、まず、Nature Remoのアプリを使い赤外線リモコンを登録します。
今回の例では、アプリで「新しい家電を追加する」ボタンから「電気」を追加して名前を「電気」とします。
そして、追加した家電内に照明をコントロールするリモコンのON、OFFの赤外線ボタンを、それぞれ「オン」と「オフ」という名前で登録します。
Signal IDを含むリモコン情報を取得するには、コマンドライン(Macのターミナル・Windows のコマンドプロンプトなど)で以下のcurlコマンドを実行します。
XXXXXXXXの箇所には自身で発行したアクセストークンの値を指定します。
curl -X GET "https://api.nature.global/1/appliances" -H "accept: application/json" -H "Authorization: Bearer XXXXXXXX"上記のコマンドを実行すると、次のようなJSONデータが取得できます。
[ { "id": "XXXXXXXX", "device": { // 中略(Nature Remoのデバイスのデータ) }, "model": null, "nickname": "電気", "image": "ico_light", "type": "IR", "settings": null, "aircon": null, "signals": [ { "id": "XXXXXXXX", "name": "オン", "image": "ico_on" }, { "id": "XXXXXXXX", "name": "オフ", "image": "ico_off" } ] } ]※jsonが整形されていない場合は以下のようなツールを使用するかHomebrewでjqを導入して整形してみてください
http://tools.m-bsys.com/development_tooles/json-beautifier.php
https://qiita.com/tomitz/items/f48a6bfae1123cadc69aNature Remoアプリでリモコンのボタンを一つづつ登録した場合、JSONデータ内のキー"type"の値が"IR"となります。
そして、 キー"signals"の値にリモコンボタンの情報がオブジェクトの配列で取得できます。
今回の例では、照明リモコンのボタン「オン」「オフ」を登録したため、その2つのリモコンボタンが、キー"signals"の値にオブジェクトの配列として取得されています。
キー"id"の値が割り振られたSignal IDとなります。また、キー"name"と"image"の値 は、アプリで赤外線ボタンを登録した際の「ボタン名」と「アイコン名」となります。
なお、JSONデータの先頭にあるキー"id"の値はAppliance IDとなり、アプリで登録した家電毎に割り振られるIDとなります。
Nature RemoのCloud APIを利用し赤外線信号を送信するタスクを作成
以下、NFC Tools Pro EditionでCloud APIを使用し、Nature Remoから赤外線信号を送信するタスクの作成手順となります。
「HTTP POST」をタップします
※有料版でなければ使用できないようなので注意
POSTパラメータの横の「+」をタップします
「リクエスト」に「https://api.nature.global/1/signals/XXXXXXXX/send 」と入力します
XXXXXXXXの箇所は、操作したい赤外線送信ボタンのSignal IDを指定します「名前」に「access_token」、「値」に発行した自身のアクセストークンを入力します
「書く」をタップしてNFCタグにスマホをかざして情報を書き込みます
アプリを閉じてもう一度NFCタグにスマホをかざしてみて電気がオンorオフできたら成功です
(´ - `).。oO(安っぽい。。w)
最後に
Nature Remoを利用してNFCタグにAndroidスマホをかざして家電を操作する方法についてまとめました。
あくまでこれは一つの方法であって、やり方は色々あると思うので参考になれば幸いです。参考
https://blog-and-destroy.com/12244
https://support.nature.global/hc/ja/articles/900001736023--iOS13%E4%BB%A5%E9%99%8D-%E3%82%B7%E3%83%A7%E3%83%BC%E3%83%88%E3%82%AB%E3%83%83%E3%83%88%E3%82%92%E4%BD%BF%E7%94%A8%E3%81%97%E3%81%A6-Siri%E3%81%8B%E3%82%89Nature-Remo%E3%82%92%E6%93%8D%E4%BD%9C%E3%81%99%E3%82%8B
- 投稿日:2021-01-10T20:19:25+09:00
【Android】公式に記載されている Glide のセットアップ方法は間違っている
あらすじ
Glideの README の「Download」を見ると、Gradle のセットアップ方法が以下のように記載されています。
app/build.gradledependencies { implementation 'com.github.bumptech.glide:glide:4.11.0' annotationProcessor 'com.github.bumptech.glide:compiler:4.11.0' }
./gradlew build
コマンドでビルドをしていた時に、Glide で以下のような警告が出ていることに気付きました。app: 'annotationProcessor' dependencies won't be recognized as kapt annotation processors. Please change the configuration name to 'kapt' for these artifacts: 'com.github.bumptech.glide:compiler:4.11.0'.解決方法
警告文の通り、
app/build.gradle
でannotationProcessor
となっている箇所を、kapt
に変更してください。app/build.gradledependencies { def glide_version = "4.11.0" implementation "com.github.bumptech.glide:glide:$glide_version" kapt "com.github.bumptech.glide:compiler:$glide_version" }kapt について
kotlin-annotation-processing tools
の略で、Kotlin でもアノテーションを使えるようにするためのツールのようです。
下記の記事が参考になりました。
- 投稿日:2021-01-10T20:08:49+09:00
rooted+MagiskなAndroidのアップデート
rooted+MagiskなAndroid端末にアップデートをあてるには、いくつか作業があります。
今回は下記前提条件のもとでAndroid端末にアプデをあてたいと思います。■前提条件
- bootloader unlock済かつmagiskを導入済
- Clockworkmod recovery等のカスタムRecoveryは導入していない
- Androidセキュリティアップデートを2020年12月5日から2021年1月5日へアップデート
- 端末はPixel 3 XL
- Androidは11(APIは30)
- Magiskは21.2
■作業
1 必要なファイルの用意
すべてFactory Images for Nexus and Pixel Devicesで揃います。
(1)Full OTA Image(zip)
Factory Images for Nexus and Pixel Devicesの左ペインから「Full OTA Images」へアクセスし、必要なOTAファイルをダウンロードします。
今回はPixel 3 XLに2021年1月15日のアップデートをあてるので、「"crosshatch" for Pixel 3 XL」の11.0.0 (RQ1A.210105.003, Jan 2021)をダウンロードします。
ダウンロードしたzipファイルはadbのパスが通っているところに置いておきます。
(2)(アップデート前のバージョンの)boot.img
アプデをあてる前に、弄っていないboot.imgを先に当てておく必要があります。
(Magiskを適用する際に、 patchedなboot.imgをあてた状態になっているため)
後述しますが、前回アップデート適用時に保存しておいたものを使ってください。
「保存し忘れた!」という人は下(3)を参考に、現在バージョンのboot.imgを用意してください。
(今回の例ならば、Factory Imageの11.0.0 (RQ1A.201205.003, Dec 2020, All carriers except Verizon)を使用します。)(3)(アップデート後の)boot.img
Factory Images for Nexus and Pixel Devicesの左ペインから「Factory Images」へアクセスし、OTAと同じバージョンのFactory Image(今回は11.0.0 (RQ1A.210105.003, Jan 2021))をダウンロードします。(今回は2021年1月15日版)
zipファイルをダウンロートしたら、任意の場所に解凍します。
解凍したファイルの中にあるimage-crosshatch-rq1a.210105.003.zipを更に解凍します。
(下のスクショだとimage-crossha....003.zipとなっているzip)
更に解凍したファイルの中にあるboot.imgが、今回必要な現在バージョンのboot.imgになります。
このboot.imgは、次回アップデート時(今回の例では2021年2月5日前後のアップデート)に必要になるので、どこかに保存しておくと良いでしょう。(保存し忘れてもダウンロードできます。)2 (現在バージョンの)boot.imgをあてる
ターミナル(コマンドプロンプト)から、現在バージョンのboot.imgをあてます。(便宜上、1205boot.imgというファイル名にしています。)
adb reboot bootloader fastboot flash boot 1205boot.img fastboot reboot3 OTAをあてる
ターミナル(コマンドプロンプト)から、recoveryに入ります。
adb reboot recovery画面中央に腹が開いたドロイド君と「No command」と表示が出たら、電源ボタンを押しながら音量上ボタン(Volume+)を一度押すと、Android Recoveryに入ることができます。
「Apply update from ADB」を選択(音量ボタンで上下・電源ボタンで決定)すると、adbからの入力を受け付けるようになるので、ターミナル(コマンドプロンプト)からOTAをあてます。(便宜上、0105ota.zipというファイル名にしています。)あてた後、Recoveryの「Reboot system now」を選択して再起動します。
adb sideload 0105ota.zip4 Magiskでpatchedなboot.imgを作成する。
ここまででOTAはあてられましたが、root権限が消失しているので、復活させます。
アップデート後のboot.imgを、端末内に配置します。(ファイル名は0105boot.imgとしています。)
Download内に置くと便利です。Magiskからインストール→パッチするファイルの選択→Let's Goでpatchedなboot.imgがDownloadフォルダ内に生成されます。(magisk_patched_A4gr0.img)
このmagisk_patched_A4gr0.imgをAndroid端末からPCのadbパスが通っているフォルダに置いておきます。
5 patchedなboot.imgをあてる
magisk_patched_A4gr0.imgを、ターミナル(コマンドプロンプト)からあてます。
(ファイル名を便宜上0105boot_patched.imgとしています)adb reboot bootloader fastboot flash boot 0105boot_patched.img fastboot rebootこれで終了です。
参考:How to sideload the Android 9 Pie OTA on Pixel, Pixel XL, Pixel 2, Pixel 2 XL - 9To5 Google
- 投稿日:2021-01-10T18:09:41+09:00
TensorFlowとCameraXでリアルタイム物体検知Androidアプリ
今回やること
CameraX
とTensorflow lite
を使ってリアルタイムに物体検知するアプリをcameraXの画像解析ユースケースを使ってサクッと作っていきます。
(注: CameraXの実装は1.0.0-rc01
のものです。)
GitHubリポジトリを今記事最下部に載せてますので適宜参照してください。
ちょっと長めなのでとりあえず試したい方はリポジトリを見てください。バウンディングボックスとスコアを表示するものです
モデルの用意
物体検知に使用する訓練済みモデルを探してとってきます。
今回はTensorFlow Hub のssd_mobileNet_v1を使用します。tfliteモデル
をダウンロードします。
ssd_mobileNet_v1
はこんな感じのモデルです。
input shape 300 x 300 color channel 3
output sahpe location [1, 10, 4] バウンディングボックス category [1, 10] カテゴリラベルのインデックス (91クラスのcoco_datasetで学習したモデルです) score [1, 10] 検出結果のスコア number of detection [1] 検出した物体の数(今回のモデルは10で一定) TensorFlow Hubにはほかにも色々訓練済みモデルがあるので好きなものを選んでください。
ただ、input size
が大きいものはパラメータ数が多くAndroidだと推論に時間がかかるので注意が必要です。
また、場合によってはtfliteモデルを自分でエクスポートする必要がある場合もあります。今回はそのままモデルを使いますが、
Tensorflow API
とか使って転移学習させるのも面白そうですね。Android Studio で実装
gradle
Tensorflow lite APIとCameraX、カメラ権限用にpermission dispatcherの依存関係を追加します。
build.gradle// permissionDispatcher implementation "org.permissionsdispatcher:permissionsdispatcher:4.7.0" kapt "org.permissionsdispatcher:permissionsdispatcher-processor:4.7.0" // cameraX def camerax_version = "1.0.0-rc01" implementation "androidx.camera:camera-core:${camerax_version}" implementation "androidx.camera:camera-camera2:$camerax_version" implementation "androidx.camera:camera-lifecycle:$camerax_version" implementation "androidx.camera:camera-view:1.0.0-alpha20" // tensorflow lite implementation 'org.tensorflow:tensorflow-lite:2.2.0' implementation 'org.tensorflow:tensorflow-lite-support:0.0.0-nightly'assetsフォルダの用意
先ほどダウンロードした
.tflite
モデルをAndroid Studioのassetsフォルダに入れます。(assetsはプロジェクト右クリック「New -> Folder -> Assets Folder」で作れます)
検出結果のインデックスをラベルにマッピングするために正解ラベルも用意しておきます。
自分のリポですがこちらからcoco_dataset
のラベルをDLして同様にassetsフォルダにtxtファイルを入れてください。これでAndroid Studioのassetsフォルダには
ssd_mobile_net_v1.tflite
とcoco_dataset_labels.txt
の2つが入っている状態になったと思います。
CameraXの実装
(注: CameraXの実装は
1.0.0-rc01
のものです。)
基本的にはこちらの公式チュートリアルのままやっていくだけです。マニフェストにカメラ権限を追加
AndroidManifest.xml<uses-permission android:name="android.permission.CAMERA" />レイアウトファイルの定義
カメラビューとsurfaceView
を定義します。
バウンディングボックスなどリアルタイムに描写するのでView
ではなくsurfaceView
を使用してビューに検出結果を反映させます。activity_main.xml<androidx.constraintlayout.widget.ConstraintLayout //省略// > <androidx.camera.view.PreviewView android:id="@+id/cameraView" android:layout_width="0dp" android:layout_height="0dp" //省略// /> <SurfaceView android:id="@+id/resultView" android:layout_width="0dp" android:layout_height="0dp" //省略// /> </androidx.constraintlayout.widget.ConstraintLayout>MainActivityにcameraXの実装。後からpermissionDispatcherを追加します。
この辺はチュートリアルと一緒なので最新のチュートリアルを参考にしたほうがいいかもしれません。MainActivity.ktprivate lateinit var cameraExecutor: ExecutorService override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) cameraExecutor = Executors.newSingleThreadExecutor() setupCamera() } fun setupCamera() { val cameraProviderFuture = ProcessCameraProvider.getInstance(this) cameraProviderFuture.addListener({ val cameraProvider: ProcessCameraProvider = cameraProviderFuture.get() // プレビューユースケース val preview = Preview.Builder() .build() .also { it.setSurfaceProvider(cameraView.surfaceProvider) } // 背面カメラを使用 val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA // 画像解析(今回は物体検知)のユースケース val imageAnalyzer = ImageAnalysis.Builder() .setTargetRotation(cameraView.display.rotation) .setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST) // 最新のcameraのプレビュー画像だけをを流す .build() // TODO ここに物体検知 画像解析ユースケースのImageAnalyzerを実装 try { cameraProvider.unbindAll() // 各ユースケースをcameraXにバインドする cameraProvider.bindToLifecycle(this, cameraSelector, preview, imageAnalyzer) } catch (exc: Exception) { Log.e("ERROR: Camera", "Use case binding failed", exc) } }, ContextCompat.getMainExecutor(this)) } override fun onDestroy() { super.onDestroy() cameraExecutor.shutdown() }とりあえずここまで来たら設定から手動でカメラ権限を許可すればカメラプレビューが見れるはずです。ただ、
surfaceView
はデフォルトでは黒なので画面が黒くなっている場合はいったんsurfaceView
をコメントアウトして確認してください。permission dispatcherの実装
カメラ権限リクエスト用にpermission disptcherを実装します。(手動で権限許可するから別にいいというかたは飛ばしてください)
MainActivity.kt@RuntimePermissions class MainActivity : AppCompatActivity() { // 略 @NeedsPermission(Manifest.permission.CAMERA) fun setupCamera() {...} }各アノテーションを対象クラスとメソッドに追加していったんビルドします。
パーミッションリクエスト用の関数が自動生成されます。先ほどの
setupCamera
メソッドを以下のように変更し、権限リクエスト結果からコールされるようにします。
なお、今回は拒否された時などの処理に関しては実装しません。MainActivity.ktoverride fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) cameraExecutor = Executors.newSingleThreadExecutor() //setupCamera() 削除 // permissionDispatcherでsetUpCamera()メソッドをコール setupCameraWithPermissionCheck() } override fun onRequestPermissionsResult( requestCode: Int, permissions: Array<String>, grantResults: IntArray ) { super.onRequestPermissionsResult(requestCode, permissions, grantResults) onRequestPermissionsResult(requestCode, grantResults) }これでカメラのプレビュー関連については実装完了です。
続いて、画像解析ユースケースやモデル読み込み、結果の表示などを実装します。モデル読み込み関数の実装
tflite
モデルの読み込みや正解ラベルをassetsから読み込む関数をMainActivityに実装します。
特に難しいこともしていないのでコピペでokです。MainActivity.ktcompanion object { private const val MODEL_FILE_NAME = "ssd_mobilenet_v1.tflite" private const val LABEL_FILE_NAME = "coco_dataset_labels.txt" } // tfliteモデルを扱うためのラッパーを含んだinterpreter private val interpreter: Interpreter by lazy { Interpreter(loadModel()) } // モデルの正解ラベルリスト private val labels: List<String> by lazy { loadLabels() } // tfliteモデルをassetsから読み込む private fun loadModel(fileName: String = MainActivity.MODEL_FILE_NAME): ByteBuffer { lateinit var modelBuffer: ByteBuffer var file: AssetFileDescriptor? = null try { file = assets.openFd(fileName) val inputStream = FileInputStream(file.fileDescriptor) val fileChannel = inputStream.channel modelBuffer = fileChannel.map(FileChannel.MapMode.READ_ONLY, file.startOffset, file.declaredLength) } catch (e: Exception) { Toast.makeText(this, "モデルファイル読み込みエラー", Toast.LENGTH_SHORT).show() finish() } finally { file?.close() } return modelBuffer } // モデルの正解ラベルデータをassetsから取得 private fun loadLabels(fileName: String = MainActivity.LABEL_FILE_NAME): List<String> { var labels = listOf<String>() var inputStream: InputStream? = null try { inputStream = assets.open(fileName) val reader = BufferedReader(InputStreamReader(inputStream)) labels = reader.readLines() } catch (e: Exception) { Toast.makeText(this, "txtファイル読み込みエラー", Toast.LENGTH_SHORT).show() finish() } finally { inputStream?.close() } return labels }画像解析ユースケースの実装
メインの物体検知の推論パイプラインを実装していきます。
CameraXの画像解析ユースケースを利用することでより手軽に実装できるようになりました。(数行で実装できるというわけではないですが。。。)
チュートリアルでは画素値の平均をとったりしています。cameraXで用意されている
ImageAnalysis.Analyzer
を実装しカメラのプレビューを受け取り、解析結果を返すようなObjectDetectorクラス
を作ります。
typealias
でコールバックとして解析結果を受け取れるように定義します。ObjectDetector.kttypealias ObjectDetectorCallback = (image: List<DetectionObject>) -> Unit /** * CameraXの物体検知の画像解析ユースケース * @param yuvToRgbConverter カメラ画像のImageバッファYUV_420_888からRGB形式に変換する * @param interpreter tfliteモデルを操作するライブラリ * @param labels 正解ラベルのリスト * @param resultViewSize 結果を表示するsurfaceViewのサイズ * @param listener コールバックで解析結果のリストを受け取る */ class ObjectDetector( private val yuvToRgbConverter: YuvToRgbConverter, private val interpreter: Interpreter, private val labels: List<String>, private val resultViewSize: Size, private val listener: ObjectDetectorCallback ) : ImageAnalysis.Analyzer { override fun analyze(image: ImageProxy) { //TODO 推論コードの実装 } } /** * 検出結果を入れるクラス */ data class DetectionObject( val score: Float, val label: String, val boundingBox: RectF )MainActivityの「TODO ここに物体検知 画像解析ユースケースのImageAnalyzerを実装」の部分を以下のように書き換えます。
MainActivity.kt// 画像解析(今回は物体検知)のユースケース val imageAnalyzer = ImageAnalysis.Builder() .setTargetRotation(cameraView.display.rotation) .setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST) // 最新のcameraのプレビュー画像だけをを流す .build() .also { it.setAnalyzer( cameraExecutor, ObjectDetector( yuvToRgbConverter, interpreter, labels, Size(resultView.width, resultView.height) ) { detectedObjectList -> // TODO 検出結果の表示 } ) }各コンストラクタ変数についてはコメントを参照してください。
ここでYuvToRgbConverterがエラーになっていると思いますが今から説明しますので大丈夫です。
ImageAnalysis.Analyzer
インターフェースのanalyze
メソッドを実装していくのですが、ここでanalyze
メソッドの引数にImageProxy
という型でカメラのプレビュー画像が流れてきます。
このImageProxy
をbitmapやtensorに変換しないと推論とかができないのですが、これがちょっと面倒なんです。。。
ImageProxy
の中にはandroid.Media.Image
が入っており画像ピクセルデータを一つもしくは複数のPlane
としてグルーピングして保存しています。アンドロイドのカメラではYUV_420_888
という形式でImage
が生成されるのでこれをRGB bitmapに変換するコンバーターを作る必要があります。確か、pytorch mobileにはコンバーターが用意されていた気がしますが、tensorflowにはありませんでした。リポジトリあさってたらcameraXのサンプルにソースがあったので今回はそれを使用します。(自分で実装するのもありですが)
ということで
この公式サンプルのコンバータをコピーしてYuvToRgbConverter
クラスを作って、MainActivityにそのインスタンスを以下のように追加してください。MainActivity.kt// カメラのYUV画像をRGBに変換するコンバータ private val yuvToRgbConverter: YuvToRgbConverter by lazy { YuvToRgbConverter(this) }モデル関連の変数定義
モデルのinput画像サイズや結果を受け取るための変数を先ほどの
ObjectDetector
クラスに定義します。使用するモデルのshape
に合わせる必要があります。ObjectDetector.ktcompanion object { // モデルのinputとoutputサイズ private const val IMG_SIZE_X = 300 private const val IMG_SIZE_Y = 300 private const val MAX_DETECTION_NUM = 10 // 今回使うtfliteモデルは量子化済みなのでnormalize関連は127.5fではなく以下の通り private const val NORMALIZE_MEAN = 0f private const val NORMALIZE_STD = 1f // 検出結果のスコアしきい値 private const val SCORE_THRESHOLD = 0.5f } private var imageRotationDegrees: Int = 0 private val tfImageProcessor by lazy { ImageProcessor.Builder() .add(ResizeOp(IMG_SIZE_X, IMG_SIZE_Y, ResizeOp.ResizeMethod.BILINEAR)) // モデルのinputに合うように画像のリサイズ .add(Rot90Op(-imageRotationDegrees / 90)) // 流れてくるImageProxyは90度回転しているのでその補正 .add(NormalizeOp(NORMALIZE_MEAN, NORMALIZE_STD)) // normalization関連 .build() } private val tfImageBuffer = TensorImage(DataType.UINT8) // 検出結果のバウンディングボックス [1:10:4] // バウンディングボックスは [top, left, bottom, right] の形 private val outputBoundingBoxes: Array<Array<FloatArray>> = arrayOf( Array(MAX_DETECTION_NUM) { FloatArray(4) } ) // 検出結果のクラスラベルインデックス [1:10] private val outputLabels: Array<FloatArray> = arrayOf( FloatArray(MAX_DETECTION_NUM) ) // 検出結果の各スコア [1:10] private val outputScores: Array<FloatArray> = arrayOf( FloatArray(MAX_DETECTION_NUM) ) // 検出した物体の数(今回はtflite変換時に設定されているので 10 (一定)) private val outputDetectionNum: FloatArray = FloatArray(1) // 検出結果を受け取るためにmapにまとめる private val outputMap = mapOf( 0 to outputBoundingBoxes, 1 to outputLabels, 2 to outputScores, 3 to outputDetectionNum )なんだか変数ばっかりで見づらいですが全部必要です。
画像の前処理はtensorflow lite ライブラリのImageProcessor
を使用して行います。
各変数の説明はコメントを参照してください。基本的にここで示したモデルinfoをkotlinで定義しています。推論コードの実装
続いてinterpreterを使ってモデルで推論します。
ObjectDetector.kt// 画像をYUV -> RGB bitmap -> tensorflowImage -> tensorflowBufferに変換して推論し結果をリストとして出力 private fun detect(targetImage: Image): List<DetectionObject> { val targetBitmap = Bitmap.createBitmap(targetImage.width, targetImage.height, Bitmap.Config.ARGB_8888) yuvToRgbConverter.yuvToRgb(targetImage, targetBitmap) // rgbに変換 tfImageBuffer.load(targetBitmap) val tensorImage = tfImageProcessor.process(tfImageBuffer) //tfliteモデルで推論の実行 interpreter.runForMultipleInputsOutputs(arrayOf(tensorImage.buffer), outputMap) // 推論結果を整形してリストにして返す val detectedObjectList = arrayListOf<DetectionObject>() loop@ for (i in 0 until outputDetectionNum[0].toInt()) { val score = outputScores[0][i] val label = labels[outputLabels[0][i].toInt()] val boundingBox = RectF( outputBoundingBoxes[0][i][1] * resultViewSize.width, outputBoundingBoxes[0][i][0] * resultViewSize.height, outputBoundingBoxes[0][i][3] * resultViewSize.width, outputBoundingBoxes[0][i][2] * resultViewSize.height ) // しきい値よりも大きいもののみ追加 if (score >= ObjectDetector.SCORE_THRESHOLD) { detectedObjectList.add( DetectionObject( score = score, label = label, boundingBox = boundingBox ) ) } else { // 検出結果はスコアの高い順にソートされたものが入っているので、しきい値を下回ったらループ終了 break@loop } } return detectedObjectList.take(4) }まずcameraXの画像をYUV -> RGB bitmap -> tensorflowImage -> tensorflowBufferと変換していき
interpreter
を使って推論します。引数に入れたoutputMap
に推論結果が格納されるので定義した各output変数から結果を整形してリストとして返すようなdetect
関数を作成します。続いて
analyze
関数からこのdetect
関数をコールするようにしてObjectDetector
クラスは完成です。ObjectDetector.kt// cameraXから流れてくるプレビューのimageを物体検知モデルに入れて推論する @SuppressLint("UnsafeExperimentalUsageError") override fun analyze(image: ImageProxy) { if (image.image == null) return imageRotationDegrees = image.imageInfo.rotationDegrees val detectedObjectList = detect(image.image!!) listener(detectedObjectList) //コールバックで検出結果を受け取る image.close() }
image.close()
は必ず呼ぶ必要があるので注意してください。android.Media.Image
はシステムリソースを食うので開放する必要があります。ここまで実装出来たらが推論パイプラインの実装は完了です。
最後に検出結果の表示を実装します。検出結果の表示を実装
viewの描画がリアルタイムに行われるので
View
ではなくsurfaceView
を使ってバウンディングボックスなどの表示を実装します。
初期化処理をOverlaySurfaceView
クラスを作って適当に書いていきます。
コールバックやsurfaceViewとは?みたいなのはほかの方の記事でたくさん書かれているので割愛します。OverlaySurfaceView.ktclass OverlaySurfaceView(surfaceView: SurfaceView) : SurfaceView(surfaceView.context), SurfaceHolder.Callback { init { surfaceView.holder.addCallback(this) surfaceView.setZOrderOnTop(true) } private var surfaceHolder = surfaceView.holder private val paint = Paint() private val pathColorList = listOf(Color.RED, Color.GREEN, Color.CYAN, Color.BLUE) override fun surfaceCreated(holder: SurfaceHolder) { // surfaceViewを透過させる surfaceHolder.setFormat(PixelFormat.TRANSPARENT) } override fun surfaceChanged(holder: SurfaceHolder, format: Int, width: Int, height: Int) { } override fun surfaceDestroyed(holder: SurfaceHolder) { } }これにバウンディングボックスを表示する
draw
関数を作っていきます。OverlaySurfaceView.ktfun draw(detectedObjectList: List<DetectionObject>) { // surfaceHolder経由でキャンバス取得(画面がactiveでない時にもdrawされてしまいexception発生の可能性があるのでnullableにして以下扱ってます) val canvas: Canvas? = surfaceHolder.lockCanvas() // 前に描画していたものをクリア canvas?.drawColor(0, PorterDuff.Mode.CLEAR) detectedObjectList.mapIndexed { i, detectionObject -> // バウンディングボックスの表示 paint.apply { color = pathColorList[i] style = Paint.Style.STROKE strokeWidth = 7f isAntiAlias = false } canvas?.drawRect(detectionObject.boundingBox, paint) // ラベルとスコアの表示 paint.apply { style = Paint.Style.FILL isAntiAlias = true textSize = 77f } canvas?.drawText( detectionObject.label + " " + "%,.2f".format(detectionObject.score * 100) + "%", detectionObject.boundingBox.left, detectionObject.boundingBox.top - 5f, paint ) } surfaceHolder.unlockCanvasAndPost(canvas ?: return) }surfaceHolder経由で取得するcanvasですが、viewがリークする可能性があるのでnullableで扱ってます。
canvas
を使ってバウンディングボックス(Rect)と文字を表示しているだけです。あとは、SurfaceViewのコールバックなどをセットするだけです。
MainActity.ktprivate lateinit var overlaySurfaceView: OverlaySurfaceView override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) overlaySurfaceView = OverlaySurfaceView(resultView) // 略 }MainActivityの画像解析ユースケースのコールバック「TODO 検出結果の表示」の部分を以下のように変更します。
MainActivity.kt// 画像解析(今回は物体検知)のユースケース val imageAnalyzer = ImageAnalysis.Builder() .setTargetRotation(cameraView.display.rotation) .setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST) // 最新のcameraのプレビュー画像だけをを流す .build() .also { it.setAnalyzer( cameraExecutor, ObjectDetector( yuvToRgbConverter, interpreter, labels, Size(resultView.width, resultView.height) ) { detectedObjectList -> // 解析結果の表示 overlaySurfaceView.draw(detectedObjectList) } ) }これで完成です!
いい感じに実装出来ましたか?おわり
cameraXもrcになってもうそろそろかっってみんな思ってるんじゃないでしょうか。ユースケースが色々用意されていてそれに則って実装するとやりやすくて拡張性があるのが魅力ですね。個人的にはもうプロダクトにバンバン投入していってもいいんじゃないかって思ってたり。。
- 投稿日:2021-01-10T00:56:01+09:00
【Android/Kotlin】最小構成でLiveDataを使う
環境
- Mac
- AndroidStudio4.1
- Kotlin
LiveDataとは
データの変更をリアルタイムで検知して、それをトリガーとしてなんらかの処理を行う
LiveData
に検知したいデータを格納し、
Observer
に変更された時の処理を記述する結論
コード解説
今回のコードの動き
事前準備
- LiveDataを初期化
- Observerを定義
動かす
- 背景をタップする
LiveData
の中身を変更するObserver
が変更を検知し、text_view
にsampleLiveData
の値を入れるコード
MainActivity.ktclass MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) // LiveDataを初期化 val sampleLiveData: MutableLiveData<LocalDateTime> by lazy { MutableLiveData<LocalDateTime>() } // Observerを定義 // sampleLiveData の値が変化したら text_view.text を変更 sampleLiveData.observe(this, Observer { value -> // LiveDataが変化したらここの内容が実行される // sampleLiveData.valueの値が変数valueに入っている value?.let { text_view.text = it.toString() } }) // 背景タップでLiveDataに現在時刻を入れる constraint.setOnClickListener { sampleLiveData.value = LocalDateTime.now() } } }参考にした記事