- 投稿日:2020-01-04T23:32:00+09:00
Xamarin.Forms(Android) から OpenCV(C/C++) を利用する
Xamarin.Forms(Android) から OpenCV(C/C++) を利用する
はじめに
OpenCV公式が公開している Android用ライブラリを利用して、Xamarin.Forms から C/C++ で OpenCV を操作する方法をまとめました。(Xamarin.Forms ですが iOS については何も書いていません)
今回は動作確認用のサンプルとして、単色の黒/白 2つのMat画像を結合して平均輝度値を求めています。
以降は arm64-v8a アーキテクチャに絞って書いていますので、他の環境の方は適宜読み替えてください。
確認環境
- OpenCV 4.2.0
- Windows 10
- Visual Studio Community 2019 16.4.2
- Xamarin.Forms 4.4.0.991265
- Google Pixel 3 (Android 10.0 - API 29)
セットアップ
特に変わったことはしていません。
OpenCVライブラリの取得
OpenCVの公式 から取得して、良い感じの場所に展開しておきます。
今回は "C:\opencv\OpenCV-android-sdk" に置きました。
自作ライブラリの対応
STL設定
デフォルトでは [LLVM libc++ スタティックライブラリ(c++_static)] になっていたので、[共有ライブラリ (c++_shared)] に変えました。
OpenCVインクルード
展開したOpenCVのヘッダフォルダをインクルードします。
C:\opencv\OpenCV-android-sdk\sdk\native\jni\includeOpenCVライブラリ
展開したOpenCVの各ライブラリフォルダをインクルードします。
C:\opencv\OpenCV-android-sdk\sdk\native\libs\arm64-v8a C:\opencv\OpenCV-android-sdk\sdk\native\3rdparty\libs\arm64-v8a C:\opencv\OpenCV-android-sdk\sdk\native\staticlibs\arm64-v8aOpenCVライブラリ名
上記ディレクトリ内の各ライブラリ名を指定します。今回は全てのライブラリを列挙してみました。
ライブラリ名には、libXXXX.a の XXXX の部分のみを記述するようにして下さい。
ちなみに libXXXX.a とフルで書くと、LINK で "cannot find" になります。
VSが気を利かせてくれればハマらずに済んだのですが…opencv_java4 cpufeatures IlmImf ittnotify libjasper libjpeg-turbo libpng libprotobuf libtiff libwebp quirc tbb tegra_hal opencv_calib3d opencv_core opencv_dnn opencv_features2d opencv_flann opencv_highgui opencv_imgcodecs opencv_imgproc opencv_ml opencv_objdetect opencv_photo opencv_stitching opencv_video opencv_videoioソースコード(C++)
デフォルトで作成される [プロジェクト名.h] と [プロジェクト名.cpp] の中身は削除して、以下を追加しました。
サンプルなので、ややこしいことはしていません。
// NativeOpenCv.cpp #include "NativeOpenCv.h" #include <opencv2/core.hpp> #define DllExport extern "C" DllExport double GetMatMeanY(int black_length, int white_length) { int row = 100; // 1. 引数で指定された割合で、単色の黒/白 Mat を作成 cv::Mat brack = cv::Mat::zeros(row, black_length, CV_8UC1); cv::Mat white(row, white_length, CV_8UC1, cv::Scalar(255, 255, 255)); // 2. サイドバイサイドで 2つのMat を結合 cv::Mat merge; hconcat(brack, white, merge); // 3. 結合したMatの平均輝度値を求める(単色なので輝度(Y)と呼べない気もする) return cv::mean(merge)[0]; }自作ライブラリの対応は以上です。
Xamarin(Android)の対応
OpenCVライブラリ
Android側のプロジェクトに libs\arm64-v8a フォルダを作成して、OpenCVの共有ライブラリ(*.so) を追加します。(今回は1つだけでした)
C:\opencv\OpenCV-android-sdk\sdk\native\libs\arm64-v8a\*.so追加したライブラリ(*.so) のプロパティを変更します。
- ビルドアクション:AndroidNativeLirary
- 出力ディレクトリにコピー:新しい場合はコピーする
ソースコード(C#)
とりあえず動作を見るだけなので、デフォルトで作成される MainActivity.cs に自作ライブラリの呼び出し処理(P/Invoke)を追加しました。
public class MainActivity : global::Xamarin.Forms.Platform.Android.FormsAppCompatActivity { // ◆追加:ここから1 [System.Runtime.InteropServices.DllImport("NativeOpenCv")] private static extern double GetMatMeanY(int black_length, int white_length); // ◆追加:ここまで1 protected override void OnCreate(Bundle savedInstanceState) { ~~割愛~~ // ◆追加:ここから2 // 黒画像と白画像の割合を指定して、平均輝度値を求める var y0 = GetMatMeanY(1, 1); // 255 * 1/2 = 127.5 var y1 = GetMatMeanY(2, 1); // 255 * 1/3 = 85.0 var y2 = GetMatMeanY(200, 300); // 255 * 3/5 = 153.0 // ◆追加:ここまで2 } ~~割愛~~ }対応は以上です。
実行しても何も分かりませんが、ブレークポイントを仕掛ければコメント通りの輝度値が取得できるはずです。
サンプル
今回の紹介に使用したリポジトリは以下で公開しています。
https://github.com/hsytkm/XamaFormsOfficialCv
参考
Android プロジェクトへの C / C++ コードの追加
終わりに
2019年冬休みの自分への課題を記事にまとめました。
本当はOpenCVのソースをセルフビルドして利用したかったのですが、CMakeやらLinkやらのエラーが取れないまま休みが終わってしましました…
仕事が始まるとバタバタして熱が冷めてしまいそうですが、時間を見つけてリベンジしたい!
- 投稿日:2020-01-04T22:14:07+09:00
[kotlin] アンドロイドで画像分類をする(Pytorch Mobile)
PyTorch Mobile
去年(2019年)の10月くらいに出た。Tensolflow Liteとかではandroid iosでも機械学習ができたが、やっとpytorch 1.3からモバイル向けが登場した。tensorflow よりpytorch使う側からすると最高だね!
tensorflow Liteと同様にandroid ios で利用できるようになっている。詳細はこちら
PyTorch Mobile公式サイト : https://pytorch.org/mobile/home/今回やること
公式サイトで紹介されているチュートリアルをやる。Kotlinで書く!
resNetの学習済みモデルを使って画像の分類を行う。(推論のみ)github載せてます https://github.com/SY-BETA/PyTorchMobile
こんな感じ ↓
分類する画像と上位二つの分類結果とそのスコアを表示するだけの簡単なもの。(Canis lupusってなんだろ?)必要なもの
- python の実行環境 (自分はjupyter notebookでやった)
- pytorch, torchVision(最新版推奨)
- android studio
こんだけ
ResNetモデルのダウンロード
まずandroid studio で新規プロジェクトを作成する。
そのプロジェクトにassetsフォルダを作成する。(「UI左のapp右クリック-> 新規 -> フォルダ -> assetsフォルダ」 でできる)
作成したらそのプロジェクトのappフォルダと同じ階層で以下のpythonコードを実行するcreateModel.pyimport torch import torchvision # resnetモデルを利用 model = torchvision.models.resnet18(pretrained=True) # 推論modeに model.eval() example = torch.rand(1, 3, 224, 224) traced_script_module = torch.jit.trace(model, example) traced_script_module.save("app/src/main/assets/resnet.pt")うまく実行できると先ほど作ったassetsフォルダに
resnet.pt
というファイルが追加される。assetsフォルダとdrawableフォルダに以下のサンプル画像を
image.jpg
の名前で保存する
実装
依存関係
gradleに以下を追加(2020年1月4日時点)
dependencies { implementation 'org.pytorch:pytorch_android:1.3.0' implementation 'org.pytorch:pytorch_android_torchvision:1.3.0' }android studio でレイアウトを作る
適当にレイアウト作成
縦に画像が1個とテキストが6個あるだけのレイアウトactivity_main.xml<?xml version="1.0" encoding="utf-8"?> <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=".MainActivity"> <TextView android:id="@+id/textView" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Input" android:textSize="30sp" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" /> <ImageView android:id="@+id/imageView" android:layout_width="wrap_content" android:layout_height="230dp" android:scaleType="fitCenter" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@+id/textView" app:srcCompat="@drawable/image" /> <TextView android:id="@+id/textView2" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Result" android:textSize="30sp" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@+id/imageView" /> <TextView android:id="@+id/result1Score" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="32dp" android:text="TextView" android:textSize="18sp" app:layout_constraintBottom_toTopOf="@+id/result1Class" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintHorizontal_bias="0.5" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@+id/textView2" /> <TextView android:id="@+id/result1Class" android:layout_width="250dp" android:layout_height="wrap_content" android:layout_marginStart="40dp" android:layout_marginTop="8dp" android:layout_marginEnd="40dp" android:gravity="center" android:text="TextView" android:textSize="18sp" app:layout_constraintBottom_toTopOf="@+id/result2Score" app:layout_constraintEnd_toEndOf="@+id/result1Score" app:layout_constraintHorizontal_bias="0.5" app:layout_constraintStart_toStartOf="@+id/result1Score" app:layout_constraintTop_toBottomOf="@+id/result1Score" /> <TextView android:id="@+id/result2Score" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="24dp" android:text="TextView" android:textSize="18sp" app:layout_constraintBottom_toTopOf="@+id/result2Class" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintHorizontal_bias="0.5" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@+id/result1Class" app:layout_constraintVertical_bias="0.94" /> <TextView android:id="@+id/result2Class" android:layout_width="250dp" android:layout_height="wrap_content" android:layout_marginStart="40dp" android:layout_marginTop="8dp" android:layout_marginEnd="40dp" android:layout_marginBottom="32dp" android:gravity="center" android:text="TextView" android:textSize="18sp" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="@+id/result2Score" app:layout_constraintHorizontal_bias="0.5" app:layout_constraintStart_toStartOf="@+id/result2Score" app:layout_constraintTop_toBottomOf="@+id/result2Score" /> </androidx.constraintlayout.widget.ConstraintLayout>モデルのロード
先に作成した
resnet.pt
をロードするMainActivity.ktoverride fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) //// assetファイルからパスを取得する関数 fun assetFilePath(context: Context, assetName: String): String { val file = File(context.filesDir, assetName) if (file.exists() && file.length() > 0) { return file.absolutePath } context.assets.open(assetName).use { inputStream -> FileOutputStream(file).use { outputStream -> val buffer = ByteArray(4 * 1024) var read: Int while (inputStream.read(buffer).also { read = it } != -1) { outputStream.write(buffer, 0, read) } outputStream.flush() } return file.absolutePath } } /// モデルと画像をロード /// シリアル化されたモデルをロード val bitmap = BitmapFactory.decodeStream(assets.open("image.jpg")) val module = Module.load(assetFilePath(this, "resnet.pt")) }assetsフォルダから画像やモデルをロードするのは結構面倒な書き方をするので注意
推論
dependenciesに追加したモジュールとresnetを使ってサンプル画像を入力して結果を出力する
MainActivity.kt/// テンソルに変換 val inputTensor = TensorImageUtils.bitmapToFloat32Tensor( bitmap, TensorImageUtils.TORCHVISION_NORM_MEAN_RGB, TensorImageUtils.TORCHVISION_NORM_STD_RGB ) /// 推論とその結果 /// フォワードプロパゲーション val outputTensor = module.forward(IValue.from(inputTensor)).toTensor() val scores = outputTensor.dataAsFloatArray推論結果
上位のscoreを取り出す
MainActivity.kt/// scoreを格納する変数 var maxScore: Float = 0F var maxScoreIdx = -1 var maxSecondScore: Float = 0F var maxSecondScoreIdx = -1 /// scoreが高いものを上から2個とる for (i in scores.indices) { if (scores[i] > maxScore) { maxSecondScore = maxScore maxSecondScoreIdx = maxScoreIdx maxScore = scores[i] maxScoreIdx = i } }分類クラス
分類するクラスの名前
すごく長いので省略 (imageNetの1000クラス分類のアレです)
githubに載せてるのでImageNetClasses.kt
の中身をコピペしてくださいgithub クラス名リスト(ImageNetClasses.kt)
ImageNetClasses.ktclass ImageNetClasses { var IMAGENET_CLASSES = arrayOf( "tench, Tinca tinca", "goldfish, Carassius auratus", //~~~~~~~~~~~~~~略(githubからコピペしてください)~~~~~~~~~~~~~~~~// "toilet tissue, toilet paper, bathroom tissue" ) }結果を表示
インデックスから推論したクラス名を取得し、
最後に推論結果をレイアウトに表示するMainActivity.kt/// インデックスから分類したクラス名を取得 val className = ImageNetClasses().IMAGENET_CLASSES[maxScoreIdx] val className2 = ImageNetClasses().IMAGENET_CLASSES[maxSecondScoreIdx] result1Score.text = "score: $maxScore" result1Class.text = "分類結果:$className" result2Score.text = "score:$maxSecondScore" result2Class.text = "分類結果:$className2"完了!!ビルドすれば冒頭のような画面ができるはず。
いろんな写真入れて遊んでみてください。おわり
ライブラリって便利。画像分類がこんだけでできるとは。
tensorに変換とかが少しひっかかるなって感じだったけど、これでpytorchでもandroid に使えるようになった。
あと余談で、最初pytorchのバージョンが最新じゃなくてモデルのロードのときエラー出て全くできかったところと、assetsフォルダのパスの取得で結構ハマった。
- 投稿日:2020-01-04T21:45:35+09:00
RailsでiOSアプリのURI スキームに対応する
一瞬「あれっ...」ってなったので備忘録
TL; DR
routes.rb
内でダイレクトルーティングで定義すればOKバージョン
- Ruby:2.6.1
- Rails:5.2.3
タイトルにiOSと書いてますがAndroidも対応できます。ただのRoutingの話なので。
やりたいこと
- RailsがWebとモバイルアプリの両方のサーバサイドの役割を担っている
- モバイルアプリ内で、特定の画面に遷移させるURIスキームを使いたい
- メニューなどで表示されていないページへの遷移など
- 上記の例としてInstagramに「運営からのコメント」があり、チャットで操作説明を受ける画面に遷移させたいような目的があったとする
- ヘルパーリンク:
qa_chat
- URIスキーム:
instagram://qa_chat
解決策
routes.rb
routes.rb
で、以下のようなダイレクトルーティングを設定してあげれば良いです。routes.rbdirect :qa_chat do "instagram://qa_chat" endヘルパーリンク
Railsと同一のViewでヘルパーリンクを利用する場合は、以下のように記述します。
sample.erb<%= link_to qa_chat_url %>ちなみにダイレクトルーティングについて、Railsガイドでは以下の部分に記載があります。
(このURIスキームについての記載はありませんが、あくまで参考までに)
小ネタ
外部URLもroutes.rbで変数として定義できる
ダイレクトルーティングといえば、こちらの使い方のほうが一般的です。
よく使う外部URLを変数定義して、Railsアプリケーション内で使うことができます。
サービスLPのURLを記述する時は、個別にではなく、routes.rb
にまとめて記述するほうが管理しやすいです。example.rbdirect :homepage do "http://www.rubyonrails.org" end
- 投稿日:2020-01-04T19:53:33+09:00
Intentのフラグを解析する
AndroidでIntentを受け取ったときにどういうフラグが設定されているかを調べたい時、単純に
intent.flags
を参照するだけだと分かりづらかったので、フラグの名称を出力するために以下のコードを書きました。Intent::class.java.fields.forEach { field -> if (field.name.startsWith("FLAG_")) { val flag = field.get(null) if (flag is Int) { if (flag and intent.flags != 0) { Log.d("MyApp", "Intent Flag ${field.name}") } } } }これで
Intent Flag FLAG_ACTIVITY_NEW_TASK
などがログに出力されます。
- 投稿日:2020-01-04T19:44:49+09:00
FlutterはデフォルトでReleaseビルドにInternetパーミッションを付けない
はい、タイトルのとおりです。
昔は違ったのですが、今のFlutterで
flutter create
して作られるプロジェクトでは、android/app/src/main/AndroidManifest.xml
の中に<uses-permission android:name="android.permission.INTERNET"/>
の指定がありません。そのため、リリースビルドしたアプリではインターネットに接続できず、Image.network
は何も読み込まないしHttpClient
などを使ったインターネット接続は全て失敗します。
android/app/src/debug/AndroidManifest.xml
の中には<uses-permission android:name="android.permission.INTERNET"/>
が記述されているため、デバッグビルドであればインターネット接続ができます。私はこのことに気付くまで1日かかりました……
リリースビルドしたときだけアプリが動かない!!とお悩みの方は、一度これをチェックしてみてはいかがでしょうか。
- 投稿日:2020-01-04T17:57:41+09:00
nox_adb shellでchgrpする
背景
NoxPlayerのIceCreamSandwich環境でUWSCマクロを書こうとしたとき、nox_adb shell環境では最初からls,grep,cp,chmod等のUNIX系コマンドは通る。
そのため、NoxのAndroid環境ではBusyBoxのコマンド群が予め/system/bin/shに含まれていたのではないかと勘違いしていた。しかしながら、ファイル・ディレクトリのグループ情報を変更するときにchgrpしようとすると
nox_adb shell #chgrp gip filename #/system/bin/sh: chgrp: not foundと出てしまい、グループ情報が変更できないことがわかった。そのためchgrpを使う方法を2つ考えた。
解決方法1:BusyBoxを入れる
Nox側でroot取って、ここからアプリをインストール、アプリを開いてBusyBoxをインストール。
画面の指示通りにインストールしたなら、nox_adb側でnox_adb shell #cd /system/xbin #alias busybox="`pwd`/busybox"と打ち込めば、chgrpを含め,vi,tar等も通るようになる。
なお、adb shellに関して公式のドキュメントを見てもls以外の記述はなく、何のコマンドが最初から使えるかわからないがここを見る限り、Androidのバージョンによっては、adb shell環境ではgrep,cp,chmodが最初から使えないのもありそう。
ただし、手持ちのOreo機種ではchgrpコマンドも含め、いずれのコマンドも通ったので、Androidバージョンによって通ったり通らなかったりするコマンドがあるみたい。解決方法2
chownなら問題なく通るため、以下のようにグループ情報を変更する事ができる。
chown uid.gid filename
chgrpするだけならこっちのほうが簡単。
教訓
adb shellするならBusyBoxを最初から入れたほうがいい。
参考
Androidのターミナルを強化する『BusyBox Non-Root』
How to change the group id of a file on rooted android device?
Android Debug Bridge(adb)
- 投稿日:2020-01-04T15:55:55+09:00
51歳からのプログラミング 備忘 android ダイアログに複数のViewを表示
ダイアログに複数のViewを表示するには、Viewを配置したLinearLayoutを、ダイアログにインフレートする。後で使いたい機能なのでちょっと備忘メモ。
構成
1.MainActivity.java
2.MyDialog.java
3.dilog_my.xmlMainActivityprotected void onCreate(final Bundle savedInstanceState){ ... AppCompatDialogFragment dialog = new MyDialog(); dialog.show(getSupportFragmentManager(),null); }MyDialog.javapublic class MyDialog extends AppCompatDialogFragment{ @Override public Dialog onCreateDialog(Bundle savedInstanceState){ AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); LayoutInflater inflater = requireActivity().getLayoutInflater(); // getActivity().getLayoutInflater();でもいけた builder.setView(inflater.inflate(R.layout.dialog_my,null)) .setPositiveButton("OK",new DialogInterface.OnClickListener(){ @Override public void onClick(DialogInterface dialogInterface,int i){ // OKボタンを押した時の処理 } }).setNegativeButton("cancel",new DialogInterface.OnClickListener(){ @Override public void onClick(DialogInterface dialogInterface,int i){ // CANCELボタンを押した時の処理 } }); return builder.create(); } }dialog_my.xml// インフレートさせるレイアウト // EditTextを2つ配置してみる <?xml version="1.0" encodeing="utf-8" ?> <LinearLayout xmls:android="http://schemas.android.com/apk/res/android" android:orientation="vertical" android:layout_width="match_parent" android:layout_height="match_parent"> <EditText android:id="@+id/edit1" android:inputType="numberPassword" android:layout_width="match_parent" android:layout_height="match_parent"/> <EditText android:id="@+id/edit2" android:layout_width="match_parent" android:layout_height="match_parent"/> </LinearLayout>
- 投稿日:2020-01-04T14:34:44+09:00
[Unity] invalid build pathでAndroidビルドが出来ない
事象
Invalid build path: c:/develop/unity/test UnityEngine.GUIUtility:ProcessEvent(Int32, IntPtr) (at C:/buildslave/unity/build/Modules/IMGUI/GUIUtility.cs:179)
Unity 2019.2.13f1
でAndroidビルドをしようとすると、.apk
ファイルの保存ができなかった。ビルドボタン選択時、表示されるダイアログで、(保存先の?)フォルダ選択しかできず。
いつもなら、保存する.apk
ファイルの名前を指定して保存する手順のはず。。。とりあえず、プロジェクトフォルダを選択してみると、このエラー。
いや、なんで?????
解決方法
プロジェクトフォルダ以外のフォルダを指定する。
ただし、プロジェクトフォルダ配下のフォルダはいくつか試してみてダメだったので、それより手前のフォルダを指定する必要があるかも。
今回は適当にフォルダを掘って、
C:\new
を指定してみました。
するとビルドが始まり、success後、指定したフォルダに.apkファイルが出来た。。。いや、なんで?????
ちなみに
環境
Windows10
Unity 2019.2.13f1
AndroidSDK 26.1.1UnityでOculusQuestのビルドを試していたところ発生。
AndroidSDKのバージョンと、Unityのバージョンに互換性がないと動かないらしいけど、2週間ほど前にビルド実績があるのでおそらく関係なし。
Questビルド用に、プロジェクトの設定を色々変えていたのが原因?
分かり次第、追記したい。。
- 投稿日:2020-01-04T13:09:54+09:00
Android で OpenCL を使って画像変換する
Android で OpenCL を試す
の続きです概要
インテルが Android で OpenCL を使うチュートリアルとサンプルコードを公開してます。
今回は、これを試す。[Tutorial: Getting Started with OpenCL™ on Android* OS(https://software.intel.com/en-us/android/articles/opencl-basic-sample-for-android-os)
前提
Android端末に OpenCL ランタイムが必要です。使い方
アプリを起動すると、組み込みの画像が表示される。画面をタッチすると、
タッチした座標を中心とした円が表示され、
どんどん大きくなるアニメーションを行う。
円の内側にある画像を白黒に変換する。
すべての画像が白黒になると、アニメーションは終了する。アプリを作成する
まず、下記を読んでください。
(1) プロジェクトを作成する
(2) OpenCLのライブラリを用意する
(3) app/src/main/cpp フォルダーに C++ のプログラムファイルを配置する
(4) app/src/main/Assets フォルダーにOPenCLのカーネルプログラムファイルを配置する
(5) app/src/main/cpp/res/drawableフォルダーに画像を配置する
(6) レイアウトファイルに ImageView を配置する
activity_main.xml<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" <ImageView android:id="@+id/outputImageView" android:layout_width="fill_parent" android:layout_height="fill_parent" android:src="@drawable/picture" />(7) MainActivity に JNIインターフェイスを記載する
MainActivity.javapublic class MainActivity extends Activity private native void nativeStepOpenCL ( // 詳細 略(8) C++ のプログラムにJNIインターフェイスを記載する
step.cppextern "C" void Java_com_intel_sample_androidbasicocl_MainActivity_nativeStepOpenCL ( // 詳細 略(9) app/src/main/cpp フォルダーに CMakeLists.txt を作成する
CMakeLists.txtadd_library( # Sets the name of the library. step # Sets the library as a shared library. SHARED # Provides a relative path to your source file(s). step.cpp )(10) app/build.gradle を変更する
build.gradleexternalNativeBuild { cmake { path 'src/main/cpp/CMakeLists.txt' } }アプリの動作
Java から OPenCLのカーネルプログラムへ画像を渡す
JNI の引数に Java の Bitmap を使う
MainActivity
```
public void onWindowFocusChanged(boolean hasFocus)
{
// アプリが起動すると
BackgroundThread を開始する
startBackgroundThread();private void startBackgroundThread () {BackgroundThread を開始すると、step() を繰り返し実行する
backgroundThread = new Thread(new Runnable() {
public void run() {
while(!isShuttingDown)
{
step();private void step () {//ネィテブコードを実行する
nativeStepOpenCL(
// 詳細 略
);//ネィテブコードへのインターフェイス
private native void nativeStepOpenCL (
// 詳細 略
```画面にタッチすると、画像を変換する
MainActivity
public boolean onTouchEvent(MotionEvent event)
{
// 画面にタッチすると、座標を取得する
xTouchUI = (int)(event.getX());
yTouchUI = (int)(event.getY());
step.cpp
```
nativeStepOpenCL
(
JNIEnv* env,
jobject thisObject,
jint stepCount,
jint xTouch,
jint yTouch,
jint radius,
jboolean updateInputBitmap,
jobject inputBitmap,
jobj
)
{// Bitmap から画素を取得する
void* inputPixels = 0;
AndroidBitmap_lockPixels(env, inputBitmap, &inputPixels);// Buffer を作成する
clCreateBuffer
(
openCLObjects.context,
CL_MEM_READ_ONLY | CL_MEM_COPY_HOST_PTR,
bufferSize, // Buffer size in bytes.
inputPixels, // Bytes for initialization.
&err
);
```アプリを実行する
analyze apk により下記の2つが同封されていることが確認できる。
- libstep.so
- libOpenCL.so
スクリーンショット
サンプルコードをgithub に公開した。
https://github.com/ohwada/Android_Samples/tree/master/OpenCL4
- 投稿日:2020-01-04T10:22:05+09:00
[kotlin] surfaceViewで簡単お絵描き android アプリを作る
今回やること
surfaceViewを使って簡単お絵描きアプリを作る。
リセットボタンと色を変えられる機能をつける。
こんな感じ ↓
作るファイルは3個(MainActivity.kt, CustomSurfaceView.kt, activity_main.xml)だけ
※以下少し長くなります。レイアウトファイルにsurfaceViewをのせる
android studio のパレットからsurfaceView(widgetsとかにある)をドラッグする。
色変更ボタンとリセットボタンを配置するactivity_main.xml<?xml version="1.0" encoding="utf-8"?> <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=".MainActivity"> <Button android:id="@+id/blackBtn" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="8dp" android:text="くろ" app:layout_constraintEnd_toStartOf="@+id/redBtn" app:layout_constraintHorizontal_bias="0.5" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" /> <Button android:id="@+id/redBtn" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="8dp" android:text="あか" app:layout_constraintEnd_toStartOf="@+id/greenBtn" app:layout_constraintHorizontal_bias="0.5" app:layout_constraintStart_toEndOf="@+id/blackBtn" app:layout_constraintTop_toTopOf="parent" /> <Button android:id="@+id/greenBtn" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="8dp" android:text="みどり" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintHorizontal_bias="0.5" app:layout_constraintStart_toEndOf="@+id/redBtn" app:layout_constraintTop_toTopOf="parent" /> <SurfaceView android:id="@+id/surfaceView" android:layout_width="0dp" android:layout_height="0dp" app:layout_constraintBottom_toTopOf="@+id/resetBtn" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@+id/redBtn" /> <Button android:id="@+id/resetBtn" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginBottom="8dp" android:text="リセット" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" /> </androidx.constraintlayout.widget.ConstraintLayout>CustomSurfaceViewを作る
コンストラクタ(初期値など)
とりあえずコピペでok
CustomSurfaceView.ktclass CustomSurfaceView: SurfaceView, SurfaceHolder.Callback{ private var surfaceHolder: SurfaceHolder? = null private var paint: Paint? = null private var path: Path? = null var color: Int? = null var prevBitmap: Bitmap? = null private var prevCanvas: Canvas? = null private var canvas: Canvas? = null var width: Int? = null var height: Int? = null constructor(context: Context, surfaceView: SurfaceView) : super(context) { surfaceHolder = surfaceView.holder /// display の情報(高さ 横)を取得 val size = Point().also { (context.getSystemService(Context.WINDOW_SERVICE) as WindowManager).defaultDisplay.apply { getSize( it ) } } /// surfaceViewのサイズ width = size.x height = size.y /// 背景を透過させ、一番上に表示 surfaceHolder!!.setFormat(PixelFormat.TRANSPARENT) surfaceView.setZOrderOnTop(true) /// コールバック surfaceHolder!!.addCallback(this) /// ペイント関連の設定 paint = Paint() color = Color.BLACK paint!!.color = color as Int paint!!.style = Paint.Style.STROKE paint!!.strokeCap = Paint.Cap.ROUND paint!!.isAntiAlias = true paint!!.strokeWidth = 15F } }
surfaceView
とSurfaceHolder.Callback
を継承する。
widthとheightに関しては本当はViewTreeObserver
とか使ってsurfaceViewのサイズに合わせた方がいいが今回はこっちでも問題ないので簡単なスクリーンサイズを使用。
setZOrderTop(true)
とsetFormat
がないと線とか何も表示されないので注意。(自分はこれに気付かず2日くらいハマった)データクラス
描画する際のpathと色を保存するデータクラスを作る。
新しくファイル作ってもCustomSurfaceViewに書いてもokCustomSurfaceView.kt//// pathクラスの情報とそのpathの色情報を保存する data class pathInfo( var path: Path, var color: Int )Implement
Implementする。
canvasとbitmapを初期化するメソッドを作るCustomSurfaceView.kt/// surfaceViewが作られたとき override fun surfaceCreated(holder: SurfaceHolder?) { /// bitmap,canvas初期化 initializeBitmap() } override fun surfaceChanged(holder: SurfaceHolder?, format: Int, width: Int, height: Int) { } override fun surfaceDestroyed(holder: SurfaceHolder?) { /// bitmapをリサイクル prevBitmap!!.recycle() } /// bitmapとcanvasの初期化 private fun initializeBitmap() { if (prevBitmap == null) { prevBitmap = Bitmap.createBitmap(width!!, height!!, Bitmap.Config.ARGB_8888) } if (prevCanvas == null) { prevCanvas = Canvas(prevBitmap!!) } prevCanvas!!.drawColor(0, PorterDuff.Mode.CLEAR) }今回BitmapはsurfaceViewがdestroyされたときにリサイクルする。bitmapはそのままにしておくとメモリーリークが発生する危険があるので使わなくなったらリサイクルしておく。
描画メソッド
onTouchリスナーで感知して線を書くメソッドをつくる
CustomSurfaceView.ktprivate fun draw(pathInfo: pathInfo) { canvas = Canvas() /// ロックしてキャンバスを取得 canvas = surfaceHolder!!.lockCanvas() //// キャンバスのクリア canvas!!.drawColor(0, PorterDuff.Mode.CLEAR) /// 前回のビットマップをキャンバスに描画 canvas!!.drawBitmap(prevBitmap!!, 0F, 0F, null) //// pathを描画 paint!!.color = pathInfo.color canvas!!.drawPath(pathInfo.path, paint!!) /// ロックを解除 surfaceHolder!!.unlockCanvasAndPost(canvas) } /// 画面をタッチしたときにアクションごとに関数を呼び出す fun onTouch(event: MotionEvent) : Boolean{ when (event.action) { MotionEvent.ACTION_DOWN -> touchDown(event.x, event.y) MotionEvent.ACTION_MOVE -> touchMove(event.x, event.y) MotionEvent.ACTION_UP -> touchUp(event.x, event.y) } return true } ///// path クラスで描画するポイントを保持 /// ACTION_DOWN 時の処理 private fun touchDown(x: Float, y: Float) { path = Path() path!!.moveTo(x, y) } /// ACTION_MOVE 時の処理 private fun touchMove(x: Float, y: Float) { /// pathクラスとdrawメソッドで線を書く path!!.lineTo(x, y) draw(pathInfo(path!!, color!!)) } /// ACTION_UP 時の処理 private fun touchUp(x: Float, y: Float) { /// pathクラスとdrawメソッドで線を書く path!!.lineTo(x, y) draw(pathInfo(path!!, color!!)) /// 前回のキャンバスを描画 prevCanvas!!.drawPath(path!!, paint!!) }surfaceViewをセット
上記で作ったCustomSurfaceViewをレイアウトのsurfaceViewにセットする
MainActivity.ktoverride fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) /// CustomSurfaceViewのインスタンスを生成しonTouchリスナーをセット val customSurfaceView = CustomSurfaceView(this, surfaceView) surfaceView.setOnTouchListener { v, event -> customSurfaceView.onTouch(event) } }とりあえずここまで来たら黒の線で絵を描けるようになってるはず。
最後にカラーチェンジとリセットを実装する色とリセットを追加
CustomSurfaceViewにメソッドを書き加える。
CustomSurfaceView.kt/// resetメソッド fun reset() { ///初期化とキャンバスクリア initializeBitmap() canvas = surfaceHolder!!.lockCanvas() canvas?.drawColor(0, PorterDuff.Mode.CLEAR) surfaceHolder!!.unlockCanvasAndPost(canvas) } /// color チェンジメソッド fun changeColor(colorSelected: String) { when (colorSelected) { "black" -> color = Color.BLACK "red" -> color = Color.RED "green" -> color = Color.GREEN } paint!!.color = color as Int }最後にMainActivityにリスナーをセットして完成
MainActivity.kt/// カラーチェンジボタンにリスナーをセット /// CustomSurfaceViewのchangeColorメソッドを呼び出す blackBtn.setOnClickListener { customSurfaceView.changeColor("black") } redBtn.setOnClickListener { customSurfaceView.changeColor("red") } greenBtn.setOnClickListener { customSurfaceView.changeColor("green") } /// リセットボタン resetBtn.setOnClickListener { customSurfaceView.reset() }完成!!
おわり
少し長くなったが最後までついてきていただいた方はお絵描きアプリができてるはず。
最初はsurfaceViewの使い方がよくわからず戸惑ったが使ってみるとおもしろいことできそうだなって感じた。
自分のスマホで動くのは楽しくていいね! Android最高。
- 投稿日:2020-01-04T03:46:38+09:00
Fragment内で端末の戻るボタンを押した際の制御を行う�
自分が作っているアプリで端末の戻るボタンを制御する必要があり、Android Developers の公式リファレンスに簡単に制御する方法が載っていたのでその備忘録。
実装
基本的にこちらに書かれているのですが、以下の手順を行ます。
Fragment 内で
activity
からonBackPressedDispatcher
のインスタンスを取得
onBackPressedDispatcher#addCallback
にLifecycleOwner
とOnBackPressedCallback
を渡す。override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) activity?.onBackPressedDispatcher?.addCallback(this, object : OnBackPressedCallback(true) { override fun handleOnBackPressed() { // ここで戻るボタンがタップされた時の処理を行う。 } }) }終わりに
AndroidX が出る前は端末の戻るボタンを制御する際は結構めんどくさかったのですが、今は Fragment からも操作できるようになっていてとても簡単だなと思いました。
相変わらず Android が進化していて開発していてとても楽しいです。☺️