20200104のAndroidに関する記事は11件です。

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)

セットアップ

特に変わったことはしていません。

VisualStudioプロジェクト作成 (折りたたみ)

Xamarin.Formsプロジェクト

モバイルアプリ(Xamarin.Forms) から、"XamaFormsOfficialCv" として作成しました。

xamarin_project.png

NativeLibraryプロジェクト

ダイナミック共有ライブラリ(Android) から、"NativeOpenCv" として作成しました。

native_project.png

先に作成したXamaFormsOfficialCv.Android プロジェクトの [参照の追加] より NativeOpenCv を参照します。

この時点で一旦、動作確認しておくと安全です。

OpenCVライブラリの取得

OpenCVの公式 から取得して、良い感じの場所に展開しておきます。

今回は "C:\opencv\OpenCV-android-sdk" に置きました。

opencv_release.png

自作ライブラリの対応

STL設定

デフォルトでは [LLVM libc++ スタティックライブラリ(c++_static)] になっていたので、[共有ライブラリ (c++_shared)] に変えました。

lib_setting_stl.png

OpenCVインクルード

展開したOpenCVのヘッダフォルダをインクルードします。

C:\opencv\OpenCV-android-sdk\sdk\native\jni\include

include_opencv.png

OpenCVライブラリ

展開した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-v8a

add_opencv.png

OpenCVライブラリ名

上記ディレクトリ内の各ライブラリ名を指定します。今回は全てのライブラリを列挙してみました。

ライブラリ名には、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

add_opencv_lib.png

ソースコード(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

lib_path.png

追加したライブラリ(*.so) のプロパティを変更します。

  • ビルドアクション:AndroidNativeLirary
  • 出力ディレクトリにコピー:新しい場合はコピーする

lib_setting.png

ソースコード(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

参考

OpenCV Android

Android C++ ライブラリ サポート

Android プロジェクトへの C / C++ コードの追加

終わりに

2019年冬休みの自分への課題を記事にまとめました。

本当はOpenCVのソースをセルフビルドして利用したかったのですが、CMakeやらLinkやらのエラーが取れないまま休みが終わってしましました…

仕事が始まるとバタバタして熱が冷めてしまいそうですが、時間を見つけてリベンジしたい!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[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.py
import 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の名前で保存する
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.kt
 override 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.kt
class 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フォルダのパスの取得で結構ハマった。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

RailsでiOSアプリのURI スキームに対応する

image.png

一瞬「あれっ...」ってなったので備忘録

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.rb
direct :qa_chat do
    "instagram://qa_chat"
end

ヘルパーリンク

Railsと同一のViewでヘルパーリンクを利用する場合は、以下のように記述します。

sample.erb
<%= link_to qa_chat_url %>

ちなみにダイレクトルーティングについて、Railsガイドでは以下の部分に記載があります。
(このURIスキームについての記載はありませんが、あくまで参考までに)
image.png

小ネタ

外部URLもroutes.rbで変数として定義できる

ダイレクトルーティングといえば、こちらの使い方のほうが一般的です。

よく使う外部URLを変数定義して、Railsアプリケーション内で使うことができます。
サービスLPのURLを記述する時は、個別にではなく、routes.rbにまとめて記述するほうが管理しやすいです。

example.rb
direct :homepage do
    "http://www.rubyonrails.org"
end
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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などがログに出力されます。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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日かかりました……
リリースビルドしたときだけアプリが動かない!!

とお悩みの方は、一度これをチェックしてみてはいかがでしょうか。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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)

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

51歳からのプログラミング 備忘 android ダイアログに複数のViewを表示

ダイアログに複数のViewを表示するには、Viewを配置したLinearLayoutを、ダイアログにインフレートする。後で使いたい機能なのでちょっと備忘メモ。

構成
1.MainActivity.java
2.MyDialog.java
3.dilog_my.xml

MainActivity
protected void onCreate(final Bundle savedInstanceState){
  ...
  AppCompatDialogFragment dialog = new MyDialog();
  dialog.show(getSupportFragmentManager(),null);
}
MyDialog.java
public 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>

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[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ファイルの名前を指定して保存する手順のはず。。。

いつものビルドパターン
3.png

今回のビルドパターン
5.png

とりあえず、プロジェクトフォルダを選択してみると、このエラー。

いや、なんで?????

解決方法

プロジェクトフォルダ以外のフォルダを指定する。

ただし、プロジェクトフォルダ配下のフォルダはいくつか試してみてダメだったので、それより手前のフォルダを指定する必要があるかも。

今回は適当にフォルダを掘って、C:\newを指定してみました。
するとビルドが始まり、success後、指定したフォルダに.apkファイルが出来た。。。

いや、なんで?????

ちなみに

環境
Windows10
Unity 2019.2.13f1
AndroidSDK 26.1.1

UnityでOculusQuestのビルドを試していたところ発生。

AndroidSDKのバージョンと、Unityのバージョンに互換性がないと動かないらしいけど、2週間ほど前にビルド実績があるのでおそらく関係なし。

Questビルド用に、プロジェクトの設定を色々変えていたのが原因?
分かり次第、追記したい。。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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 ランタイムが必要です。

使い方
アプリを起動すると、組み込みの画像が表示される。

画面をタッチすると、
タッチした座標を中心とした円が表示され、
どんどん大きくなるアニメーションを行う。
円の内側にある画像を白黒に変換する。
すべての画像が白黒になると、アニメーションは終了する。

アプリを作成する

まず、下記を読んでください。

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.java
public class MainActivity extends Activity

    private native void nativeStepOpenCL (
// 詳細 略

(8) C++ のプログラムにJNIインターフェイスを記載する

step.cpp
extern "C" void Java_com_intel_sample_androidbasicocl_MainActivity_nativeStepOpenCL
(
// 詳細 略

(9) app/src/main/cpp フォルダーに CMakeLists.txt を作成する

CMakeLists.txt
add_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.gradle
    externalNativeBuild {
        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
);
```

NDK eference : Bitmap

OpenCL clCreateBuffer

アプリを実行する

analyze apk により下記の2つが同封されていることが確認できる。
- libstep.so
- libOpenCL.so
opencl4_analyze_apk.png

スクリーンショット

画面にタッチし、画像変換する例
opencl4_touch.png

サンプルコードをgithub に公開した。
https://github.com/ohwada/Android_Samples/tree/master/OpenCL4

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[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>

キャプチャ.PNG

CustomSurfaceViewを作る

コンストラクタ(初期値など)

とりあえずコピペでok

CustomSurfaceView.kt
class 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
    }
}

surfaceViewSurfaceHolder.Callbackを継承する。
widthとheightに関しては本当はViewTreeObserverとか使ってsurfaceViewのサイズに合わせた方がいいが今回はこっちでも問題ないので簡単なスクリーンサイズを使用。
setZOrderTop(true)setFormatがないと線とか何も表示されないので注意。(自分はこれに気付かず2日くらいハマった)

データクラス

描画する際のpathと色を保存するデータクラスを作る。
新しくファイル作ってもCustomSurfaceViewに書いてもok

CustomSurfaceView.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.kt
    private 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.kt
    override 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最高。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Fragment内で端末の戻るボタンを押した際の制御を行う�

自分が作っているアプリで端末の戻るボタンを制御する必要があり、Android Developers の公式リファレンスに簡単に制御する方法が載っていたのでその備忘録。

実装

基本的にこちらに書かれているのですが、以下の手順を行ます。

  1. Fragment 内で activity から onBackPressedDispatcher のインスタンスを取得

  2. onBackPressedDispatcher#addCallbackLifecycleOwnerOnBackPressedCallback を渡す。

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        activity?.onBackPressedDispatcher?.addCallback(this, object : OnBackPressedCallback(true) {
            override fun handleOnBackPressed() {
                // ここで戻るボタンがタップされた時の処理を行う。
            }
        })
    }

終わりに

AndroidX が出る前は端末の戻るボタンを制御する際は結構めんどくさかったのですが、今は Fragment からも操作できるようになっていてとても簡単だなと思いました。
相変わらず Android が進化していて開発していてとても楽しいです。☺️

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む