20200701のAndroidに関する記事は7件です。

In-app Billing APIのPurchaseの形式を調べた話

Play Billing Libraryが3.xに

Play Billing Libraryが3.xがリリースされました。
Play Billing Libraryを利用することでAIDLファイルとサービスクラスを作成する手間なくIn-app Billing APIを利用できるようになり、非常に便利になりました。

ところで、In-app Billing APIで購入をした際のレシートの形式が知りたい

Androidでアプリ内課金の実装をする場合、サーバーにレシートを送るAPIが必要です。
APIの実装のためにレシートの形式を把握する必要があり、実際にコードを書いて調べたので備忘録的にまとめておきます。

確認用のコード

今回の確認用のコードですが、以下のように BillingClient.launchBillingFlow()を実行して

val params = BillingFlowParams.newBuilder()
                        .setSkuDetails(details)
                        .build()
billingClient.launchBillingFlow(activity, params)

onPurchasesUpdated で受け取った Purchase の構造をログに出力しました。

    override fun onPurchasesUpdated(billingResult: BillingResult, purchases: MutableList<Purchase>?) {
        when {
            billingResult.responseCode == BillingClient.BillingResponseCode.OK && !purchases.isNullOrEmpty() -> {
                // ここでPurchaseをログに出力
            }
            billingResult.responseCode == BillingClient.BillingResponseCode.USER_CANCELED -> {
                // user cancelled purchase
            }
            else -> {
                // error
            }
        }
    }

予約済みのproductIdを使った静的なレスポンス

In-app Billing APIにはテスト用に予約済みのアイテムIDとそれに対応する静的なレスポンスが設定されています。
構造はプロダクションと同じですが、各パラメータの中身についてはリアルな感じではないです。

{
    "packageName": "your.package.name",
    "acknowledged": false,
    "orderId": "transactionId.android.test.purchased",
    "productId": "android.test.purchased",
    "developerPayload": "",
    "purchaseTime": 0,
    "purchaseState": 0,
    "purchaseToken": "inapp:your.package.name:android.test.purchased"
}

予約済みのアイテムIDの詳細は公式のドキュメントを参照してください:pray:

完全に余談ですが、テスト用の購入は1つのproductIdにつき1度実行すると、それ以降は ITEM_ALREADY_OWNED というエラーが発生します。これが起きないようにするためには、以下のコマンドを使ってGoogle Play Storeのキャッシュを削除して再度実行することが出来ます。

$ adb shell pm clear com.android.vending

実際のproductIdを使ったレスポンス

静的なレスポンスの内容がリアルな感じではなかったので、実際のproductIdをGoogle Play Consoleから登録して試してみます。(元のデータに変更を加えて記載しています。)

Consumableの場合

{
    "orderId": "GPA.0000-1234-5678-90000",
    "packageName": "your.package.name",
    "productId": "consumable",
    "purchaseTime": 1593597175681,
    "purchaseState": 0,
    "purchaseToken": "ocbmpajaaiigkcbmhdkoabcg.BO-J1OxQL832hkKZFhEEasaY8uJMeI7HQg2XLV5BVFAZK5e3lcchnuw4749PMTsA6PeBX-2PN7Sq5gpyl7qJYY1B2Oixfxfrck1oxbRFzO3cwz1NKIbqGB6-SR83wyNO29xVprXpl5Yw",
    "acknowledged": false
}

静的なレスポンスに存在した developerPayloadobfuscatedAccountId/ obfuscatedProfileId に変更されたようです。 launchBillingFlow を呼び出す際に使用する BillingFlowParams に追加することで、同じものがレスポンスにも追加されます。詳しくはこちらを参照してください。

Subscriptionの場合

subscriptionの場合は autoRenewing が追加されるようですね。

{
    "orderId": "GPA.3321-2766-9706-73593",
    "packageName": "your.package.name",
    "productId": "subscription",
    "purchaseTime": 1593599361267,
    "purchaseState": 0,
    "purchaseToken": "hkihoaiukokbicfiahkdkidk.BO-J1OzeRClCsDLfCASQmrTLKUHKz09R70QdDOZoiOtb4gxrlzo-v1GHxzuFOvCMPAtFFJQjV6tWrbQRjDO-feiG_Ue8Z38U-F9UIB5wYFbDrFA4MCUm4A3SgjiT7jYgFS4xX7vrdCBK",
    "autoRenewing": true,
    "acknowledged": false
}

おわりに

こちらに書いたものは、現時点でこうだったという記録でしかないので、変更される可能性があります。
内容の不備等ありましたら、コメントで教えていただけると幸いです:bow:

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

Android の実行環境

配属された新人のトレーナーになったので Android のコーチングをしているわけですが
上手く言語化できない部分があったので自分なりに纏めました。

間違い等コメントいただけると嬉しいです?

Java

  • クラスベースのオブジェクト指向型プログラミング言語
    • 処理速度が速い
    • プラットフォームに依存しない
    • オブジェクト指向

JVM

Java Virtual Machine

  • Java アプリケーションを動かすためのソフトウェア
    • JVM が各 OS 向けに Java クラスファイルをコンバートする事でアプリケーションが動作する

Java プログラム (.java) をコンパイルすると Java クラスファイル (.class) が出来上がる。
Java クラスファイルを JVM が各 OS 向けにバイトコードに変換してアプリケーションを動かしている。

jvm_flow.png

Java の特徴である プラットフォームに依存しない とはこのため。
デメリットとしては、動作環境のセットアップのハードルが他より少し高い。

JRE

Java Runtime Environment

  • Java アプリケーションの実行環境
    • JVM + 対応した API

OS に JRE をインストールすることで Java アプリケーションを実行する事ができる。
ただしコンパイラ等がないので開発はできない。
現在は JRE 単独でインストールできず、JDK をインストールする必要がある。

JDK

Java Development Kit

  • Java でプログラミングするための開発ツール
    • JRE + コンパイラやデバッガーのプログラム等

jvm.png

Android の実行環境

  • Android アプリは .apk ファイル に含まれている .dex ファイルで動作する

Android アプリはコンパイル時に .class ファイル内のバイトコードから .dex ファイルを生成する。
ART (旧 Dalvik) は生成された .dex ファイルを読み込んでアプリを動作させる。
そのため Java 言語で開発が可能だし、Android は JVM で動いているわけではない

art_flow.png

ART と Dalvik

  • ART と Dalvik ではコンパイル方式が違う
Runtime Compiler いつコンパイルされるか
ART AOT (A Head Of Time) インストール時
Dalvik JIT (Just In Time) プログラム実行時

AOT だとプログラム実行時のオーバーヘッドがなくなることで、動作の高速化や省電力化に繋がる。
Android 4.4 から ART にリプレイスされていった。

Kotlin

  • JVM 上で動作するオブジェクト指向言語
    • JDK が必要
    • Java <-> Kotlin の相互運用が可能
    • 実用的で Java より書きやすいけど JVM で動くので OS に依存しない ??
    • Android 用の extension が提供されている

GitHub - JetBrains/kotlin: The Kotlin Programming Language

kotlin-stdlib

  • Kotlin を使うためのコアライブラリ
    • stdlib-jdk は extension

kotlin-stdlib-jre の Deprecate

  • Java 9 モジュールシステムをサポートするために stdlib-jdk に移行された
    • Kotlin 1.3 から使用も禁止された

What's New in Kotlin 1.2 - Kotlin Programming Language

The Kotlin standard library is now fully compatible with the Java 9 module system, which forbids split packages (multiple jar files declaring classes in the same package). In order to support that, new artifacts kotlin-stdlib-jdk7 and kotlin-stdlib-jdk8 are introduced, which replace the old kotlin-stdlib-jre7 and kotlin-stdlib-jre8.

以下の様なライブラリのユースケースが解説されていました。

Library 使用パターン
stdlib minSdkVersion < 21
stdlib-jdk7 minSdkVersion >= 21
stdlib-jdk8 minSdkVersion >= 24

Androidの開発でkotlin-stdlib, kotlin-stdlib-jre7, kotlin-stdlib-jdk7, kotlin-stdlib-jdk8どれ使えばいいの問題 - Qiita

参考

Java SEとJDK、JRE、JVMの違いに関する解説 | Java入門

Android ランタイム(ART)と Dalvik  |  Android オープンソース プロジェクト

VMの歩む道。 Dalvik、ART、そしてJava VM

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

既存アプリを一部Flutter化する(Add-to-app)

Flutterには既存のNativeアプリの一部分をFlutter化するソリューションであるAdd-to-appが存在するので試してみる。

Androidでは特定のActivityをFlutter化出来たり、特定のFragmentをFlutter化出来たりと結構柔軟に対応できそうである。
iOSに関しては特定のViewControllerをFlutter化可能である。

Flutter Module(Flutter化された画面というか普通にFlutterの部分)を作成して、Android、iOSそれぞれから呼び出すまでの手順を示す。

制限

Flutter v1.12 Add-to-appには下記制限が存在する。

  • 複数のFlutterインスタンスを動作させたり、Viewの一部で動作させると挙動がおかしくなる可能性がある。
  • バックグラウンド動作モードは試作中
  • 複数のFlutterライブラリをアプリに同封するのはサポートされていない
  • add-to-appで使用するFlutterPluginは最新の「new Android plugin API」に対応してないといけない
    • Flutter Activityが常に存在しているなどを想定した動作を実装していると挙動がおかしくなる可能性がある。
  • AndroidXアプリしかサポートしていない

Androidプロジェクトへの組み込み、実行

Androidプロジェクト上でFlutterModuleを作成、実行。その後同ModuleをiOSに組み込むという手順で実施してみる。

Flutter Module作成

公式に記載されている「Using Android Studio」の章通りに実行すれば可能。
FlutterModuleのプロジェクトがAndroidアプリとは別のディレクトリに作成されて、AndroidStudio上で上手に内包される感じになる。
後にこのFlutterModuleのプロジェクトをiOSのプロジェクトにも取り込む。

Flutter Moduleの実行方法

公式通りに実行すれば問題ない。

実行方法を大きく分けるとFlutterActivityを生成して単一画面上で実行する方法と、FlutterFragmentを生成して臨機応変な場面で実行する方法に別れる。
基本的にはシンプルなFlutterActivityでの実行を推奨している。
※iOSはViewControllerでの実行しかサポートしていないので、単一画面上でしか実行出来ないものと思われる(すいませんここはiOSそこまで詳しくないです)。となると同一FlutterModuleをAndroidでもiOSでも流用すると考えるとFlutterActivityでの実行を選択することになると思われる。

  • FlutterActivity
    • 普通にActivityを起動する感じ
    • 初期Rootを指定できる
    • FlutterEngineが起動する時間があるので、予めEngineを起動してスタートする事も可能
  • FlutterFragment
    • ActivityのFragment版
    • 基本はシンプルなActivityを使用することを推奨している
    • いろんなActivityのコールバックをFlutterFragmentに通知する必要がある

Pluginのマネジメント

firebase_crashlyticsプラグインみたいにAndroidアプリのGradleの編集を要求するようなプラグインを使用する場合には別途対応が必要である。

iOSプロジェクトへの組み込み、実行

上記で作成したFlutter ModuleをiOSのプロジェクトに組み込んで起動するまでの手順を示す。

iOSプロジェクト作成

Flutter Moduleは作成済みなので公式の「Embed the Flutter module in your existing application」の章からCocoapodsを利用した組み込みを実施する。

  1. Xcodeで新規のStoryboardプロジェクトを作成
  2. CocoaPodsを適用
$ cd プロジェクトパス
$ pod init
$ pod install
Please close any current Xcode sessions and use `Flutter_test.xcworkspace` for this project from now on.
Pod installation complete! There are 0 dependencies from the Podfile and 0 total pods installed.
  1. Flutterへの依存をPodfileに記載

image.png

flutter_application_pathに設定するのがAndroidStudioから作成したFlutte Moduleへのパスである。

4. 依存を解消

$ pod install

Flutter Moduleの実行方法

公式に書いてあるとおりに実行すればFlutterが走るViewControllerが起動する。
Androidと同様FlutterEngineを予め起動することが可能である。

参考

https://flutter.dev/docs/development/add-to-app
https://qiita.com/imamurh/items/43966e33e100956e0998

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

Bluetooth AudioデバイスからTHETAを操る

はじめに

リコーの @mShiiina です。

弊社ではRICOH THETAという全周囲360度撮れるカメラを出しています。
RICOH THETA VRICOH THETA Z1は、OSにAndroidを採用しています。Androidアプリを作る感覚でTHETAをカスタマイズすることもでき、そのカスタマイズ機能を「プラグイン」と呼んでいます(詳細は本記事の末尾を参照)。

2020年6月のFWアップデートにより、プラグインでBluetooth Classicデバイスを使用できるようになりました。

今回は、Bluetoothイヤホン、Bluetoothオーディオを使用してリモート撮影ができるプラグインを作ってみました。動画はこんな感じです。

以下のBluetoothAudioデバイスとTHETAを接続し、THETAをリモートで操作する方法をご紹介します。
・Bluetoothイヤホン:Anker Soundcore Liberty Air
・Bluetoothイヤホン:JPRiDE TWS-520
・Bluetoothオーディオ:Anker Soundcore Icon Mini

上記以外にも、いくつか動作確認をして末尾の章に一覧を掲載しました。その章をみて頂けるとわかるのですが、相性(接続可/不可)や使い勝手はオーディオ機器によってマチマチと思われます。私たちが全ての機器を試すことは不可能でして、、、かならずしもお持ちのイヤホンで使えるとは限らない点はご容赦ください。(とはいえ、AppleのAir Pods Proですら動いたし、大抵は大丈夫はなず!)

BluetoothAudioデバイスの操作に対してTHETAの以下の振る舞いを割り当て可能としました。
1. 静止画撮影
2. 露出補正の設定
3. 通常通知音の音量設定

露出補正の設定時は、こちらの記事にならって、音声を再生し設定値を読み上げています。

使用するには、以下バージョン以降のファームウェアが必要です。最新ファームウェアへのアップデートをお忘れなく!
・THETA V : 3.40.1
・THETA Z1 : 1.50.1

Bluetoothプロファイル

本プラグインは以下のプロファイルに対応しているBluetoothデバイスを使用できます。
・Headset Serviceプロファイル (HSP) ※マイクは使用できません。
・Audio/Video Remote Controlプロファイル (AVRCP)

Bluetoothイヤホンの接続、操作

Anker Soundcore Liberty AirとJPRiDE TWS-520を使用しました。

Anker Soundcore Liberty Air

Amazonで7000円程度で購入できます。

対応プロファイル:HSP、AVRCP

1.jpeg

JPRiDE TWS-520

amazonで5500円程度で購入できます。

対応プロファイル:HSP、AVRCP

2.jpeg

接続手順:
1.Soundcore Liberty Air または TWS-520 をペアリングモードにする
2.プラグインを立ち上げる(最長12秒間Bluetooth Classicデバイスを検索する)
3.接続されるまで待機する

イヤホン操作によるTHETA動作割り当て:
Bluetoothイヤホンには、タッチ式のマルチファンクションボタンがついており、接続デバイスを操作できます。今回は以下のTHETA動作を割り当てました。

イヤホン操作 THETA動作
再生 静止画撮影
一時停止 静止画撮影
曲送り 露出補正を1ステップアップ
曲戻し 露出補正を1ステップダウン

Bluetoothオーディオの接続、操作

Anker Soundcore Icon Mini を使用しました。

Amazonで3000円以下で購入できます。
対応プロファイル:Audio/Video Remote Controlプロファイル (AVRCP)

3.jpeg

オーディオ操作によるTHETA動作割り当て:

Bluetoothオーディオには、ボタンがついており、接続デバイスを操作できます。今回は以下のTHETA動作を割り当てました。

オーディオ操作 THETA動作
再生 静止画撮影
一時停止 静止画撮影
曲送り 露出補正を1ステップアップ
曲戻し 露出補正を1ステップダウン
音量アップ 通常通知音の音量1ステップアップ
音量ダウン 通常通知音の音量1ステップダウン

接続手順:
1.Soundcore Icon Miniをペアリングモードにする
2.プラグインを立ち上げる(最長12秒間Bluetooth Classicデバイスを検索する)
3.接続されるまで待機する

THETA本体の操作

THETA操作によるTHETA動作割り当て:

THETA操作 THETA動作
シャッターボタン短押し 静止画撮影
無線ボタン短押し 通常通知音の音量1ステップアップ
モードボタン短押し 通常通知音の音量1ステップダウン

LED表示

THETA VではBluetoothデバイスの検索、接続、切断状態をLED3に表示します。

LED3状態 内容
黄色点灯 Bluetoothデバイス検索中
青色点灯 Bluetoothデバイス検索終了 or Bluetoothデバイス切断
白色点灯 Bluetoothデバイス接続済み

OLED表示

THETA Z1ではBluetoothデバイスの検索、接続、切断状態をOLEDの3段目に表示します。

OLED3段目状態 内容
DISCOVERY STARTED Bluetoothデバイス検索中
DISCOVERY FINISHED Bluetoothデバイス検索終了
CONNECTED Bluetoothデバイス接続済み
DISCONNECTED Bluetoothデバイス切断

webUIのKeyCode表示

webUIでプラグインが受け取ったKeyCodeを表示します。

4.png

制約

新しく追加されたWebAPIのOptions:BluetoothRoleがCentral、Central_Peripheralの場合(=通常の動作でリモコン機能をONとしている場合)は、使用できません。THETA側でBluetooth Classicの検索処理の実行/停止/接続処理を繰り返しているためです。

プラグイン起動時の退避と終了時の復旧も可能ですが、今回は実装を省略しています。後半の「THETAアプリ側のリモコン検索機能の抑制」のところで退避と復旧の方法について触れていますのでお好みで実装を追加してください。

プログラムの説明

ソースコードはこちらです。

全体説明

THETAプラグインSDKをベースに
~\app\src\main\java\com\theta360\pluginapplication 配下の以下ファイルとMainActivityに手を加えました。

ファイル説明

ファイル 説明
MainActivity.java 各種タスクの起動とキー割り当て操作を行います
bluetooth/BluetoothClientService.java Bluetooth Classicデバイスの検索、接続、切断を行います
bluetooth/BluetoothDeviceReceiver.java Bluetooth関連Intentのブロードキャストレシーバーです
bluetooth/MediaReceiver.java AVRCPプロファイルの音量変更Intentのブロードキャストレシーバーです
task/ChangeCaptureModeTask.java
task/ChangeEvTask.java
task/ChangeEvTask.java
task/ShutterButtonTask.java
task/SoundManagerTask.java
こちらにならって作成したファイルです
EnableBluetoothClassicTask.java Bluetooth Classicの有効/無効を切り替えます
task/ChangeVolumeTask.java 音量を変更します

マニフェスト ファイル

本プラグインでは、AndroidManifest.xmlにパーミッションとサービスを追加しています。

パーミッション

Bluetooth Classicデバイスを使用するためには、以下のパーミッションを定義しています。

AndroidMnifestfile.xml
    <uses-permission android:name="android.permission.BLUETOOTH" />
    <uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
    <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />

開発者としてプログラムを実行するときには、VysorからSettings->App->HEADSET Remoteの「Location」のパーミッションを手動で有効にしてください。
本プラグインは公式プラグインストア での公開を予定しています。
公式プラグインストアからインストールした場合は、インストール時にパーミッションが有効になるため、手動での設定は必要ありません。

サービス

Bluetooth Classicデバイスを扱うサービスを追加しています。

AndroidMnifestfile.xml
        <service android:name="com.theta360.pluginapplication.bluetooth.BluetoothClientService" />

メイン処理(MainActivity.java)

既存の雛形部分(キーコード解釈部)
基本的には、受け取ったkeyCodeに対応するアクションを以下コードで実行するだけです。

MainActivity.java
execKeyProcess(keyCode2KeyProcess(keyCode));

Bluetooth Classic有効/無効切り替え

Bluetooth Classicを有効にするために、プラグイン起動時にまずEnableBluetoothClassicTaskを生成します。

MainActivity.java
        new EnableBluetoothClassicTask(getApplicationContext(),EnableBluetoothClassicTask).execute();

タスク内でWebAPI Options _bluetoothClassicEnableをtrueに設定し、Bluetooth Classicを有効にします。

EnableBluetoothClassicTask.java
        HttpConnector camera = new HttpConnector("127.0.0.1:8080");
        String errorMessage = camera
                .setOption("_bluetoothClassicEnable", Boolean.toString(Boolean.TRUE));

Bluetoothモジュールの電源WebAPI Options _bluetooth_power がOFFの場合は、ONに変更して完了です。

EnableBluetoothClassicTask.java
       String bluetoothPower = camera.getOption("_bluetoothPower");
        if (bluetoothPower.equals("OFF")) {
            errorMessage = camera.setOption("_bluetoothPower", "ON");
            if (errorMessage != null) { // パラメータの設定に失敗した場合はエラーメッセージを表示
                return "NG";
            }
        }

Bluetooth Classicの有効/無効の変更を反映させるには、Bluetoothモジュールの電源OFF/ONを行う必要があります。
_bluetoothClassicPowerがONの状態で_bluetoothClassicEnableを変更するとTHETAアプリ側はBluetoothモジュールの電源OFF/ONを自動で行います。

Bluetooth Classicデバイスの検索、接続

Bluetooth Classicを有効にした後は、Bluetooth Classicデバイスの検索、接続を行います。こちらは、AndroidのBluetoothのAPIを使用します。

ざっくりとした流れは以下になります。
1. サービスを生成する
2. 検索を開始する
3. 発見したデバイスをペアリングする
4. ペアリングしたデバイスを接続する

まずサービスを生成します。

MainActivity.java
    private EnableBluetoothClassicTask.Callback mEnableBluetoothClassicTask = new EnableBluetoothClassicTask.Callback() {
        @Override
        public void onEnableBluetoothClassic(String result) {
            getApplicationContext().startService(new Intent(getApplicationContext(), BluetoothClientService.class));
        }
    };

次にBluetooth Classicデバイスの検索を開始します。
こちらは、Androidの仕様で開始から12秒後に停止します。検索中の場合は、一度停止してから開始します。

BluetoothClientService.java
    private void startClassicScan() {
        if (mBluetoothAdapter.isDiscovering()) {
            mBluetoothAdapter.cancelDiscovery();
        }
        boolean isStartDiscovery = mBluetoothAdapter.startDiscovery();
        Log.d(TAG, "startClassicScan :" + isStartDiscovery);
    }

接続対象のデバイスを発見した場合、ペアリングを行います。
接続対象のデバイスは、CoDで判断します。
CoDは、BluetoothSIGで定められたデバイスの用途を識別する値です。

Device Type Major Device Class Minor Device Class field
Bluetoothオーディオ Audio/Video Wearable Headset Device 1028
Bluetoothイヤホン Audio/Video Headphones 1048
BluetoothClientService.java
        @Override
        public void onFound(BluetoothDevice bluetoothDevice,
                BluetoothClass bluetoothClass,
                int rssi) {
            String name = bluetoothDevice.getName();
            Log.d(TAG, "name" + name);
            if (name != null) {
                int type = bluetoothDevice.getType();
                Log.d(TAG, "type" + String.valueOf(type));
                if (type == bluetoothDevice.DEVICE_TYPE_CLASSIC || type == bluetoothDevice.DEVICE_TYPE_DUAL) {
                    int classNo = bluetoothClass.getDeviceClass();
                    Log.d(TAG, "class" + classNo);
                    if ((classNo == COD_AUDIO_VIDEO_HEADPHONES) || (classNo
                            == COD_AUDIO_VIDEO_WEARABLE_HEADSET_DEVICE)) {
                        stopClassicScan();
                        bluetoothDevice.createBond();
                    }
                }
            }
        }

※ 「DEVICE_TYPE_DUAL」もペアリング対象にすることでApple Air Pods Proに対応できました。

ペアリング完了後に接続します。
bondStateがBOND_BONDEDになるとペアリング完了です。

BluetoothClientService.java
        public void onBondStateChanged(BluetoothDevice bluetoothDevice, int bondState) {
            Log.d(TAG, "onBondStateChanged");
            if (bondState == BluetoothDevice.BOND_BONDED) {
                connect(bluetoothDevice);
            }
        }

getProfileProxyを使用してプロファイル指定し、サービスリスナーと関連付けます。
プロファイルは、 BluetoothProfile.HEADSETを使用します。

BluetoothClientService.java
    private void connect(BluetoothDevice device) {
        int state = mBluetoothAdapter.getProfileConnectionState(BluetoothProfile.HEADSET);
        if (state == BluetoothProfile.STATE_DISCONNECTED) {
            int type = device.getType();
            if (type == BluetoothDevice.DEVICE_TYPE_CLASSIC || type == BluetoothDevice.DEVICE_TYPE_DUAL) {
                int classNo = device.getBluetoothClass().getDeviceClass();
                Log.d(TAG, "class" + classNo);
                boolean isGetProfile = false;
                if ((classNo == COD_AUDIO_VIDEO_HEADPHONES) || (classNo
                        == COD_AUDIO_VIDEO_WEARABLE_HEADSET_DEVICE)) {
                    {
                        isGetProfile = mBluetoothAdapter
                                .getProfileProxy(mContext, mServiceListener,
                                        BluetoothProfile.HEADSET);
                    }
                }

                Log.d(TAG, "isGetProfile :" + isGetProfile);
                if (isGetProfile) {
                    mBluetoothDevice = device;
                }
            }
        }
    }

※ 「DEVICE_TYPE_DUAL」もペアリング対象にすることでApple Air Pods Proに対応できました。

onServiceConnected()内でBluetoothHeadsetクラスのconnect()を行います。

BluetoothClientService.java
        public void onServiceConnected(int i, BluetoothProfile bluetoothProfile) {
            try {
                if (i == BluetoothProfile.HEADSET) {
                    Class bluetoothHeadset = Class
                            .forName("android.bluetooth.BluetoothHeadset");
                    Object object = bluetoothHeadset.cast(bluetoothProfile);
                    Method getConnectionState = bluetoothHeadset
                            .getDeclaredMethod("getConnectionState", BluetoothDevice.class);
                    int connectionState = (int) getConnectionState
                            .invoke(object, mBluetoothDevice);
                    if (connectionState == BluetoothProfile.STATE_DISCONNECTED) {
                        Method connect = bluetoothHeadset
                                .getDeclaredMethod("connect", BluetoothDevice.class);
                        boolean isConnected = (boolean) connect
                                .invoke(object, mBluetoothDevice);
                        Log.d(TAG, "isConnected : " + isConnected);
                        if (isConnected) {
                            mProfile = bluetoothProfile;
                        }
                    }
                }
            } catch (ClassNotFoundException | NoSuchMethodException | IllegalAccessException |
                    InvocationTargetException e) {
                e.printStackTrace();
            }
        }

BluetoothAdapter.EXTRA_CONNECTION_STATEで取得した状態がBluetoothAdapter.STATE_CONNECTEDになれば接続完了です。

BluetoothClientService.java
        public void onConnectionStateChanged(int connectionState) {
            Log.d(TAG, "onConnectionStateChanged");
            if (Build.MODEL.equals("RICOH THETA Z1")) {
                Intent intent = new Intent(Constants.ACTION_OLED_TEXT_SHOW);
                if (connectionState == BluetoothAdapter.STATE_DISCONNECTED) {
                    intent.putExtra(Constants.TEXT_BOTTOM, "disconnected");
                } else if (connectionState == BluetoothAdapter.STATE_CONNECTED) {
                    intent.putExtra(Constants.TEXT_BOTTOM, "connected");
                }
                sendBroadcast(intent);
            } else {
                //LED3点灯
                Intent intentLedShow = new Intent("com.theta360.plugin.ACTION_LED_SHOW");
                if (connectionState == BluetoothAdapter.STATE_DISCONNECTED) {
                    intentLedShow.putExtra("color", LedColor.BLUE.toString());
                } else if (connectionState == BluetoothAdapter.STATE_CONNECTED) {
                    intentLedShow.putExtra("color", LedColor.WHITE.toString());
                }
                intentLedShow.putExtra("target", LedTarget.LED3.toString());
                mContext.sendBroadcast(intentLedShow);
            }
        }

音量調整

本プラグインで扱う音量データは2種類です。
1.ストリームボリューム(AndroidのAudioManagerのパラメータ、StreamTypeはSTREAM_MUSICを使用)
Androidが管理しています。実際に出力する音量です。
2.シャッターボリューム(WebAPIのOptions)
THETAアプリが管理しています。実際に出力する音量ではありません。実際に出力する音量を変更するには、ストリームボリュームの変更が必要です。
プラグインからIntentを使用してTHETAアプリで音声出力する際は、シャッターボリュームからストリームボリュームの値を算出し、ストリームボリュームを設定してから音声を出力しています。
つまり、THETAアプリで音声再生する場合はストリームボリュームはIntentを投げた時点のシャッターボリュームの音量に調整されます。
シャッターボリュームは、 WebAPI Options _shutter_volume で変更することができます。

本プラグインでは、2種類の操作で音量変更ができます。
・Bluetoothオーディオ操作(AVRCP)
・THETA本体ボタン操作
音量変更は、ChangeVolumeTask.javaで行っています。

Bluetoothオーディオ操作で音量変更した場合、Android側でストリームボリュームが変更され、プラグインにはストリームボリュームが変更されたことを表すIntent:"android.media.VOLUME_CHANGED_ACTION"が通知されます。
この時点では、シャッターボリュームは変更されていないため、プラグインで変更しています。ChangeVolumeTaskにストリームボリュームとACTION_TYPE_SET_VOLを渡してタスクを生成しています。

MainActivity.java
    private MediaReceiver.Callback mMediaReceiverCallback = new MediaReceiver.Callback() {
        @Override
        public void onChangeVolume(int stream_type, int prev_vol, int vol) {
            if (stream_type == AudioManager.STREAM_MUSIC) {
                if (vol != prev_vol) {
                    new ChangeVolumeTask(getApplicationContext(), vol,
                            ChangeVolumeTask.ACTION_TYPE_SET_VOL).execute();
                }
            }
        }
    };

THETA本体ボタン操作で音量変更した場合、その時点のシャッターボリュームからストリームボリュームを算出し、1ステップ増減させた音量をシャッターボリュームとストリームボリュームにそれぞれ設定します。

MainActivity.java
           case SET_VOL_PLUS:
                new ChangeVolumeTask(getApplicationContext(), 0,
                        ChangeVolumeTask.ACTION_TYPE_UP_VOL).execute();
                break;
            case SET_VOL_MINUS:
                new ChangeVolumeTask(getApplicationContext(), 0,
                        ChangeVolumeTask.ACTION_TYPE_DOWN_VOL).execute();
                break;

音量変更時の通知音声はこちらにならって、SoundManagerTaskで行っています。プラグインでAudioManagerを使用し、音声を再生します。
接続したBluetoothデバイスから音声出力するために再生時のStreamTypeはSTREAM_MUSICにしています。

THETAアプリ側のリモコン検索機能の抑制

本プラグインでは実装していませんが、プラグインでBluetooth Client機能を使用する場合は、THETAアプリ側のBluetoothリモコン機能をOFFにする必要があります。
Bluetoothリモコン機能はWebAPI Options:_bluetoothRoleで切り替えることができます。

_bluetoothRole Bluetoothリモコン機能
Peripheral OFF
Central ON
Central_Peripheral ON

例えば、プラグイン起動時にonCreate()でOptions:_bluetoothRoleを取得し、onPause()で取得済みのRoleに戻せば、プラグイン起動中のみTHETA本体のBluetoothリモコン機能をOFFすることが可能です。

動作確認したBluetoothオーディオ機器

身近な方々に協力して頂き、もう少し多くのBluetoothオーディオ機器で動作確認してみました。
一覧にして結果をまとめておきます。

オーディオ機器 再生/一時停止
(撮影)
曲送り
(露出+)
曲戻し
(露出-)
Vol+ Vol-
Anker
Icon Mini
(スピーカー)
Anker
Liberty Air
(イヤホン)

操作しにくい

操作しにくい
N/A N/A
Anker
Zolo Liberty
(イヤホン)
×
不安定
×
不安定
N/A N/A
JPRiDE
TWS-520
(イヤホン)
N/A N/A
VANKYO
X100
(イヤホン)

操作しにくい

操作しにくい
Apple
Air Pods Pro
(イヤホン)
N/A N/A
  • N/Aはイヤホン側に機能がありません
  • JPRiDE TWS-520のマニュアルには1回タップで音量調節できる主旨の説明がありますが、実際にはスマートフォン(iOS,Androidどちらも)でも認識できなかったのでN/A扱いにしました。
  • Anker Liberty Airは、タッチ認識位置とタッチ感度の都合、イヤホン操作自体が難かったです。
  • VANKYO X100の音量操作はロングタップです。タップ中に連続してキーコードが送出されるため狙いの音量とするのが難しいです。とはいえ、他の操作は行いやすく、Amazonのタイムセールで3000円程度で購入できることもあります。THETA専用に購入してしまうのもアリかと。おすすめです。
  • Apple Air Pods Proは、Android OSの機器とペアリングする際、ケースのボタンを押してペアリング状態にする必要があります。ご注意ください。

まとめ

今回の事例ではBluetoothデバイスによるリモート撮影をご紹介しました。Bluetoothイヤホンを使ったお手軽なリモート操作をぜひ試してください。
イヤホンの音声アシスタント起動も試しましたが、通知を拾えたり、拾えなかったりと挙動がいまいち掴めませんでした。
今回のハマリポイントは、音量変更時の音声再生でした。プラグインでの音量変更時の通知音をTHETA側にIntentで依頼するとストリームボリューム音量の変更イベント通知が止まらない現象が発生しました。
原因は、AVRCPの音量変更イベントを拾うために、ストリームボリューム音量変化時にシャッターボリューム音量をストリームボリューム音量へ調整する処理を行っていたためです。
THETA側での音声再生時は、ストリームボリューム音量をシャッターボリューム音量へ調整してから、音声再生を行います。
そのため、プラグイン側でシャッターボリューム音量の変更、THETA側でストリームボリューム音量の変更を時間差で延々と調整し続けていました。
対策として、プラグイン側でAudioManegerを用意し、音声再生を行いました。これにより、ストリームボリュームの調整とシャッターボリュームの音量調整の同期がとれ問題を解消できました。

RICOH THETAプラグインパートナープログラムについて

THETAプラグインをご存じない方はこちらをご覧ください。
パートナープログラムへの登録方法はこちらにもまとめてあります。
QiitaのRICOH THETAプラグイン開発者コミュニティ TOPページ「About」に便利な記事リンク集もあります。
興味を持たれた方はTwitterのフォローとTHETAプラグイン開発者コミュニティ(Slack)への参加もよろしくおねがいします。

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

Huu プライバシーポリシー

""",HuuLooのプライバシーポリシーをお読みいただき、ありがとうございます。""",
"""お客様には、当社を信頼して情報を提供していただいていることにお礼を申し上げます。当社が収集する情報の種類、収集の理由、収集した情報の利用方法、お客様情報に関する選択について理解を深めていただくことがまず重要であると考えます。本ポリシーは、当社のプライバシー対策を、法律用語や専門用語をなるべく控えて、平易な言葉で説明しております。""",
"""発効日:2020年4月30日""",
"""1.本プライバシーポリシーの適用範囲""",
"""2.当社が収集する情報""",
"""3.情報の使用目的""",
"""4.情報共有の方法""",
"""5.越境データ移転""",
"""6.お客様が有する権利""",
"""7.お客様の個人情報の保護方法""",
"""8.当社におけるお客様情報の保有期間""",
"""9.児童のプライバシー""",
"""10.プライバシーポリシーの変更""",
"""11.問い合わせ先""",
"""1. 本プライバシーポリシーの適用範囲""",
"""本プライバシーポリシーは、HuuLooが運営するウェブサイト、アプリケーション、イベント、その他のサービスにおいて適用されます。簡略化するため、本プライバシーポリシーではこれらのすべてを当社の「サービス」と総称します。さらにできるだけ正確に理解していただくため、適用サービスすべてに本プライバシーポリシーへのリンクを追加しました。""",
"""サービスの中には、独自のプライバシーポリシーを必要とするものもあります。独自のプライバシーポリシーを有するサービスには、本プライバシーポリシーに代わってそのポリシーが適用されます。""",
"""本サービスにおいて、外部のサイトへのリンクが貼られることがあります。当社はリンク先のウェブサイトにおける個人情報の取り扱いについては責任を負いかねますので、各リンク先のサイトのご利用に際してはそれぞれのプライバシーポリシーを一読される事を推奨します。""",
"""2. 当社が収集する情報""",
"""お客様が良縁に巡り合うお手伝いをさせていただくためには、基本プロフィールや相手のタイプに関するご希望など、お客様について何らかの情報を得る必要があることは言うまでもありません。また、当社では、お客様が当社サービスをご利用の際に生成される、アクセスログ情報や第三者からの情報(お客様がソーシャルメディア・アカウントを通して当社サービスにアクセスしたり、情報をアップロードした場合など)も収集しております。詳細は以下をご覧ください。""",
"""お客様から提供される情報""",
"""お客様が当社のサービスを利用されることにより、一定の情報が当社に提供されることになります。具体的には以下の情報です。""",
"""•アカウント作成時に、お客様のログイン認証情報はもとより、いくつかの基本データ(サービスが機能するうえで必要な性別、生年月日および居住地、登録を有効にするうえで必要な公的書類、顔写真など)が当社に提供されます。""",
"""•顔認証システムによりお客様の本人確認を行う際、お客様の外観に関する情報が当社に提供されます。""",
"""•お客様のプロフィール登録が完了すると、お客様の性格、ライフスタイル、ご関心事項などに加え、写真やビデオなどのコンテンツ情報も当社と共有されます。当社がお客様のカメラや写真アルバムにアクセスする許可を得て、写真やビデオなどの一定のコンテンツを追加する場合もあります。お客様が当社への提供を承諾された情報の中に、法域によって「特殊」または「機密」と見なされるものが含まれている場合があります。この種の情報を提供していただく際は、当該情報に関する当社の取扱いを承諾していただくことになります。""",
"""•お客様が有料サービス契約や購入を(iOSやAndroidなどのプラットフォームを介さずに)当社からの直接行われる場合、お客様のデビットカードやクレジットカードの番号、その他の財務情報が当社もしくは当社の決済サービスプロバイダーに提供されることになります。""",
"""•お客様が調査やフォーカスグループに参加されると、当社の製品・サービスに対するお客様のご意見や推薦の言葉、当社の質問に対する回答が提供されます。""",
"""•お客様が当社のプロモーションやイベント、コンテストに参加されると、登録や参加にあたってお客様が使用された情報が収集されます。""",
"""•お客様が当社の顧客サポートチームに連絡される場合、その過程でお客様が提供する情報が収集されます。当社はこうした会話を、訓練やサービス品質向上の目的でモニターまたは録音する場合があります。""",
"""•お客様が他者との通信や他者の情報の処理を当社に依頼される場合(お客様の友人へのメール送信を当社に依頼される場合など)、お客様がその際に当社に提供する他者に関する情報が収集されます。""",
"""•もちろん、当社は、お客様が公開されるコンテンツやお客様が他の利用者と行われるチャットについても、サービス運営の一環として処理させていただきます。かかる処理には、お客様どうしでコミュニケーションをとるための処理および詐欺等の違法な目的での当社サービスの利用その他それに類する事案の防止および健全なサービスを提供する目的での処理が含まれます。""",
"""他者から提供される情報""",
"""当社が受け取るお客様情報は、お客様から直接提供されるもの以外に、他者から取得するものもあります。具体的には以下の情報です。""",
"""•他の利用者""",
"""他の利用者が当社のサービスを利用することによって、お客様に関する情報が提供される場合があります。例えば、他の利用者がお客様に関して当社に連絡を行った場合などがこれにあたります。""",
"""•ソーシャルメディア""",
"""お客様は、ご自身のアカウントの作成やログインまたはプロフィールの入力を行うために、ご自身のソーシャルメディアアカウント(例えば、Facebook、Instagramなど)から、お客様がHuuLooに取り込むことを要求したデータをHuuLooに反映することのできる機能を提供される場合があります。お客様は、当社サービスの設定画面にて、いつでもプロフィール情報を編集することができます。""",
"""•お客様の認証機能を提供する他の事業者""",
"""お客様は、ご自身が利用なさっている、他の事業者が提供しているお客様の識別子(例:Apple ID)を、当社のサービスのログインのために利用できる場合があります。お客様がかかる機能の利用を選択された場合、当社はお客様を識別するための当該識別子、お客様の電子メールアドレスその他当該事業者がお客様を識別するために利用するお客様の情報を、お客様又は当該他の事業者から取得します。""",
"""•他のパートナー企業""",
"""当社は、お客様に関する情報を当社のパートナー企業から受け取ることもあります。例えば、パートナー企業のウェブサイトやプラットフォーム上に掲載されている HuuLooの広告にお客様が反応すれば、その情報が当社に渡されることになります。""",
"""お客様が当社サービスを利用される際に収集される情報""",
"""お客様が当社のサービスを利用される際、当社もしくは第三者によるクッキーやウェブビーコン等の技術により、使用した機能やその方法、アクセスに使用したデバイスに関する情報が収集されます。詳細は以下をご覧ください。""",
"""なお、クッキーの受け入れをご希望されない場合は、ブラウザの設定でクッキーを拒否することができますが、クッキーを拒否した場合には、本サイトのサービスが一部ご利用できない場合がございます。""",
"""•利用情報""",
"""当社サービス上でのお客様の行動情報が収集されます。具体的には、利用状況(ログイン日時、使用している機能、検索、クリック、閲覧ページ、参照ウェブページのアドレス、クリックした広告など)や他の利用者との接触状況(通信・接触した利用者、接触日時、送受信メッセージ件数など)です。""",
"""•デバイス情報""",
"""お客様が当社サービスにアクセスされると、その際に使用されたデバイスからデバイス情報が収集されます。具体的には以下のとおりです:""",
"""Pアドレス、デバイスのIDや種類、デバイスごとの設定やアプリケーションの設定/特性、アプリケーションのクラッシュ、広告ID(GoogleのAAIDやアップルのIDFAなど。どちらもランダムに生成される数列で、自分のデバイスの設定からリセットできます)、ブラウザの種類/バージョン/言語、オペレーティング・システム、タイムゾーン、お客様のデバイスやブラウザを区別するクッキーなどの技術に紐づけられた識別子(IMEI/UDID、MACアドレスなど)といったハードウェアやソフトウェアに関する情報。""",
"""•サービスプロバイダーや信号強度など、お客様のワイヤレス/モバイルによるネットワーク接続に関する情報。""",
"""•お客様が同意されたその他の情報""",
"""当社はお客様の承諾を条件に、お客様が利用中のサービスやデバイスに応じて、GPS、Bluetooth、Wi-Fi接続など各種手段を使い分けてお客様の正確な地理位置情報(緯度と経度)を収集することができます。地理位置情報の収集は、お客様からの明示的な許可があれば、サービスの利用時以外でもバックグラウンドで行われる可能性があります。ただし、お客様が拒否されれば、当社は地理位置情報の収集を行いません。 同様にお客様の同意を条件に、当社はお客様の写真やビデオを収集する場合があります(お客様がサービス上での写真、ビデオ、ストリーミングの公開を希望される場合など)。""",
"""3. 情報の利用目的""",
"""当社がお客様の情報を使用する主な目的は、当社のサービスの提供と改善にあります。加えて、お客様の安全を守り、お客様が興味を抱く可能性のある広告をお届けするためでもあります。当社がお客様の情報を使用するさまざまな目的については、このあとの実例を交えた詳細な説明を引き続きお読みください。""",
"""お客様のアカウント管理とお客様への当社サービスの提供""",
"""•お客様のアカウントを作成・管理する(本人確認、登録時の年齢確認)。""",
"""•お客様にサポートを提供し、要求に応える(お客様からのお問い合わせ時の本人確認、その他お問い合わせへの対応を含むがこれらに限られない)。""",
"""•決済等、お客様の取引に必要な処理を完了させる。""",
"""•承ったサービスのご希望についての管理や請求処理等、当社のサービスについて、お客様に連絡する。""",
"""お客様同士の出会いのサポート""",
"""•あなたのプロフィールや当サービスでのアクティビティ、好みなどを基に、あなたが出会いたいと思うようなユーザーを表示し、同じようにあなたを他のユーザーに表示する。""",
"""•利用者のプロフィールを互いに紹介する。""",
"""お客様に合った提案や広告の提供""",
"""•購入キャンペーン、くじ、コンテスト、ディスカウント、その他の提案を管理する(当該くじ等の広告、応募、商品の発送を含むがこれらに限られない)。""",
"""•当社のサービスやその他のサイト上でお客様の興味に合わせてカスタマイズしたコンテンツや広告を開発・表示・追跡する。""",
"""•お客様が興味を抱くと思われる製品やサービス(他社製品/サービスを含む)をメール、電話、ソーシャルメディア、モバイル機器で案内する。""",
"""•サービス内またはウェブサイト上で、ご利用の体験談や感想を紹介する。""",
"""当社サービスの改善と新サービスの開発""",
"""•フォーカスグループや調査(アンケートの実施・分析を含むがこれに限られない)を管理する。""",
"""•利用者の行動を調査・分析し、サービスやコンテンツを改善する(当社は特定の機能について、利用者がどのようにサービスを利用しているように見えるかを踏まえてイメージチェンジや大幅な変更を行う用意があります)。""",
"""•新たな機能やサービスを開発する(当社は利用者からの要求に基づいて、興味・関心に基づく新機能を設ける決定をする場合があります)。""",
"""詐欺やその他の違法または権限のない行動の防止・発見・対処""",
"""•健全なサービスの提供を促進するため、プラットフォーム上やその外での違法行為、不正行為、利用規約への違反行為およびその疑惑についての監視・対策を行い、かつ、実際に進行中の当該行為へ対処する。""",
"""•こうした行動についての理解を深め、より的確な対策を講ずるためのデータ分析を行う。""",
"""•不正行為関連のデータを保存し、再発防止を図る。""",
"""法令遵守の徹底""",
"""•法的義務を遵守する。""",
"""•法執行を促進する。""",
"""•利用規約に基づき、当社の権利を行使する。""",
"""以下の法的根拠に基づく上記お客様情報の処理""",
"""•お客様への当社サービスの提供:当社がお客様情報を処理するのは大概、お客様との契約を実行するためです。例えば、お客様が良縁を築く目的で当社のサービスの利用を開始するとき、当社はお客様情報をもとにアカウントやプロフィールを維持して他の利用者が閲覧できるようにし、お客様を他の利用者に推薦します。""",
"""•同意:本ポリシーに同意することにより、お客様は、当社が本プライバシーポリシーに基づいてお客様情報を処理することに同意します。また、当社はある特定の目的でお客様情報を使用する際、お客様の同意を求めることがあります。お客様による同意の取り消しはいつでも可能です。本プライバシーポリシーの末尾に記載のお問い合わせ窓口にご連絡ください。""",
"""•適用法令の遵守:当社は適用法令を遵守するために個人情報を処理することが必要になる場合があります。例えば、当社は、当社の会計税務上の義務に従って取引に関するデータを保持します。当社は、個人情報の利用目的を相当の関連性を有すると合理的に認められる範囲内において変更することがあり、変更した場合には利用者に通知または公表します。また、当初の目的以外の目的で使用する場合や、第三者に提供する場合は、事前に同意を取得の上行います。""",
"""4. 情報共有の方法""",
"""当社の使命は、お客様が良縁に恵まれるお手伝いをすることです。したがって、利用者情報の主な共有相手は、自ずと他の利用者になります。当社はまた利用者情報の一部を、場合によっては司法当局とも共有します。お客様情報の他者との共有について詳細は、以下をお読みください。""",
"""•他の利用者との共有""",
"""お客様がサービス上で自発的に開示された情報(お客様の公開プロフィールを含む)については、他の利用者と共有されることになります。お客様情報が共有されたあとは、お客様ご自身も当社も他者の行動をコントロールすることはできません。情報の管理には十分注意されるとともに、公開されても問題ないコンテンツに限って共有を行うようにしてください。 お客様のプロフィールの全部または一部について、またはお客様ご自身についての一定のコンテンツや情報について共有対象を制限することが設定上可能である場合、それらはお客様の設定に従って表示されます。""",
"""•当社のサービスプロバイダーおよびパートナー企業との共有""",
"""当社はサービスの運営や改善に、サードパーティー企業を活用しています。データのホスティングやメンテナンス、分析、顧客サポート、マーケティング、広告、支払処理、警備など、各種業務においてこうした企業の協力を得ています。 当社はまた、当社サービスの配給や広告の補助を担うパートナー企業とも情報を共有する場合があります。例えば、広告のパートナー企業であれば、お客様の限定的な情報を、ハッシュ化され人間にとって解読不能な形式で提供する場合があります。 サービスプロバイダーの採用やパートナー企業との提携にあたっては、当社は事前に厳格な審査プロセスを実施します。当社のサービスプロバイダーおよびパートナー企業はいずれも、厳格な守秘義務への同意を求められます。""",
"""•事業の承継に伴う移転""",
"""当社は、合併、売却、買収、会社分割、事業再編、組織再編、解散、倒産、その他の事業の承継に全面的または部分的に関与する場合、お客様の情報を移転する場合があります。""",
"""•法的義務に基づく共有""",
"""以下のように合理的に必要と認められる場合、当社はお客様情報を開示する場合があります。(i)裁判所命令、召喚または家宅捜索、政府/司法当局による調査またはその他の法的要求事項などの法的手続きに従う場合、(ii)犯罪の防止または発見に協力する場合(その都度適用法の規定に基づく)、または(iii)人の安全を保護する場合。""",
"""•法的権利を行使するための共有""",
"""当社はまた、以下のような場合にも情報を共有する可能性があります。(i)開示によって現実の訴訟や予想される訴訟において当社の責任が軽減される場合、(ii)当社の法的権利、当社の利用者、パートナー企業、その他の利害関係者の法的権利を保護するうえで必要な場合、(iii)お客様との契約上の権利を実行する場合、(iv)違法行為、不正疑惑、その他の犯罪行為に関して調査、防止、その他の措置を講ずる場合。""",
"""•お客様の同意または要請に基づく共有""",
"""当社はお客様情報をサードパーティー企業と共有するにあたり、お客様の同意を求める場合があります。そのような場合、当社は情報を共有する理由を必ず明示します。""",
"""当社は上記の状況のいずれにおいても、非個人情報(個人を特定できない形式のデバイス情報、一般的な人口動態データや行動データ、地理位置情報など、それ自体個人を特定しない情報)のみならず、ハッシュ化され人間にとって解読不能な形式での個人情報についても、これを使用および共有する場合があります。また、当社のサービスおよびサードパーティーのウェブサイトやアプリケーション上でのターゲティング広告の開発・配給、およびお客様が閲覧する広告の分析・報告を目的として、これらの情報をサードパーティー企業(主に広告会社)と共有する場合があります。さらにこれらの情報を、別の非個人情報や、他の情報源から収集され、ハッシュ化され人間にとって解読不能な形式での個人情報と結合することもあります。""",
"""5. 越境データ移転""",
"""セクション5で述べた情報の共有は時として、例えば米国、その他の法域への越境データ移転を伴う場合があります。例えば、日本に居住する利用者を対象に含むサービスの場合、その利用者の個人情報が日本以外の国に移転されることになります。本プライバシーポリシーに同意する場合、お客様は、かかる国境を越えたデータ移転に同意するものとします。当社は、適切な契約上の措置を通じて、これらの移転を保護します。""",
"""6. お客様が有する権利""",
"""•サービスに関するアクセス/アップデート・ツール:ツールおよびアカウント設定において、お客様から当社に提供された情報や、サービスのなかでお客様のアカウントに直接紐づけられた情報にアクセスしたり、その修正や削除を行ったりすることができます。このツールや設定に関してご不明な点があれば、下記のお問い合わせ窓口までお問い合わせください。""",
"""•デバイスでの許可:モバイルプラットフォームは、電話帳、写真や位置サービス、プッシュ通知など、特定の種類のデバイスデータの収集や通知についてパーミッションを使用する仕組みを取っています。お客様は該当する情報の収集や通知の表示に対する同意・拒否について、デバイス上で設定を変更することができます。ただしこのような操作により、一部のサービスで機能の一部が使用不能になる可能性がありますので、予めご承知おきください。""",
"""•削除:お客様によるアカウントの削除は、退会申請フォームから、または、下記のお問い合わせ窓口ににご連絡いただくことにより、行うことができます。 お客様には、ご自身のプライバシー権について十分な知識をお持ちいただきたいと考えます。重要な点を以下に整理します。""",
"""•お客様情報の確認:該当するプライバシー法は、当社が保存するお客様の個人情報をお客様ご自身で確認する権利を認めている場合があります(この権利は、法域によってアクセス権、データ携行権などとも呼ばれます)。お客様は、こうした要求を、下記のお問い合わせ窓口に提出することにより、お客様の個人情報のコピーの提供を求めることが可能です。""",
"""•お客様情報の更新:当社が保有しているお客様の情報が不正確であるか、または当社が当該情報を使用する権利を喪失しているとお客様が判断され、その修正や削除を希望される場合や、その処理を拒否される場合は、下記のお問い合わせ窓口までご連絡ください。""",
"""お客様の保護および当社のすべての利用者の保護の観点から、当社は上記要求に回答するにあたり、お客様の身元確認書類の提出をお願いする場合があります。""",
"""当社がお客様がご本人であることを確認できない場合、お客様のこうした要求が違法である場合や、別の利用者の企業秘密、知的財産権またはプライバシーを侵害する恐れがある場合など、当社がこうした要求に応じられない場合があることをご承知おきください。当社のサービスを通じて別の利用者から受け取った何らかのメッセージのコピーなど、別の利用者に関する情報を受け取ることをお客様が希望される場合、当該情報を開示するためには、その別の利用者が当社に連絡して(セクション12をご覧ください)承諾書を発行する必要があります。また、当社は、要求に応じる前に、その別の利用者に対して身分証明書の提供を求める場合があります。""",
"""当社はまた、個人情報の処理を拒否する一定の要求に応じられない場合があります。これは大概、当社からお客様へのサービス提供が不可能になる場合であり、例えばお客様の生年月日を把握していなければ、当社はサービスを提供できなくなります。""",
"""お客様は、アプリケーションによる情報収集をすべて停止させることができます。そのためには、お客様のデバイスにおける標準のアンインストール作業を行う必要があります。お客様のモバイル機器からアプリケーションをアンインストールしても、お客様のデバイスに紐づけられた固有識別子はそのまま残ります。したがって、そのモバイル機器にアプリケーションを再インストールすると、この識別子をお客様の過去の取引や行動に再び紐づけることができます。""",
"""7. お客様の個人情報の保護方法""",
"""お客様の個人情報への不正アクセス、その改ざん、漏えいまたは破棄からお客様を保護するため、当社は可能な限りの努力を払っています。""",
"""当社は物理的、技術的、組織的な安全策を更新するため、システムの脆弱性や攻撃を定期的に監視するとともに、情報の収集、保存、処理業務を定期的にチェックしています。""",
"""セキュリティ違反の疑いがあるか、違反が実際に発見された場合、当社はサービスの全部または一部について、お客様による使用を予告なしに中断する場合があります。お客様のアカウントまたは情報がもはや安全でないと思われるときは、至急下記お問い合わせ窓口から当社にご連絡くださるようお願いいたします。""",
"""当社のシステムおよびお客様情報を不正アクセス、窃盗、消失から守るため、当社はバグ報奨金プログラムを導入しました。当社のバグ報奨金プログラムに関する詳しい情報は、下記のお問い合わせ窓口にご連絡ください。""",
"""当社は、 従業者に対し、セキュリティや個人情報の適切な取り扱いに関する必要な教育、啓発活動を行なっています。""",
"""8. 当社におけるお客様情報の保有期間""",
"""当社によるお客様情報の保有は、正当な事業上の目的(セクション4に記載の通り)で当社が必要とする期間に限り、また、適用法が許す範囲内で行います。""",
"""9. 児童のプライバシー""",
"""当社のサービスは、18歳以上の利用者に制限されています。当社のプラットフォームでは18歳未満の利用者を許可せず、18歳未満と知りながら個人情報を収集することはありません。利用者が18歳未満と疑われるときは、サービス中で提供されるレポートシステムをご利用ください。""",
"""10. プライバシーポリシーの変更""",
"""当社は、お客様が良縁と出会うお手伝いをする斬新な方法を常に探し求めているため、本ポリシーはいずれ変更される可能性があります。重大な変更がなされるときは、お客様が確認の時間を持てるように、事前に通知させていただく所存です。""",
"""11. 問い合わせ先""",
"""本プライバシーポリシーに関してご不明な点があれば、以下の当社窓口までお問い合わせください。""",
"""E-mail:huuloowo@gmail.com"""

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

NDKでOpenCVを使った画像処理Androidアプリのひな形 (とマルチプラットフォームに向けた設計)

はじめに

先日、CameraXとOpenCVを使った画像処理Androidアプリのひな形という記事を投稿しました。これは、カメラ入力、画像処理、画面出力のすべてをJavaコードで行いました。
今回は、画像処理をNDK(JNI)側で処理してみます。

これによって、上手く設計すれば様々なプラットフォームで共通のロジックコードを使用することが出来ます。これについては後半で少し触れます。

例えば、↓の動画はディープラーニング処理(Pose NetとSemantic Segmentation (DeepLab)) を行う処理と結果の描画処理をライブラリ化し、Androidアプリ、Windowsアプリ、Linuxアプリ(x64, armv7, aarch64)から呼べるようにしています。

環境

  • Host
    • Windows 10 64-bit
    • Android Studio 4.0
    • Android SDK API Level 30
      • 多少低くても大丈夫なはず
    • Android NDK Version 21.3.6528147
    • opencv-4.3.0-android-sdk.zip
  • Target
    • Galaxy S7 (Android 8.0.0)

プロジェクトの用意

プロジェクトの用意

まずは、 CameraXとOpenCVを使った画像処理Androidアプリのひな形 と同様にプロジェクトを作成します。
コードや設定も同じように行ってください。

1点だけ違いがあります。テンプレートとしてEmpty Activity ではなく、Native C++ を選択する必要があります。

01.jpg

Android NDK側でOpenCVを使う

Android NDK(C/C++)側でOpenCVを使うための設定をします。
app/src/main/cpp/CMakeLists.txt を以下のように編集します。
OpenCV_DIR には、OpenCVの場所を指定します。Java用にOpenCVをimportした際に、OpenCV/sdkの中身がプロジェクト内にコピーされます。その場所を相対パスで指定するのが楽だと思います。あるいは、オリジナルのOpencVの場所を指定します。この場合、環境に応じてパスは変更してください。

app/src/main/cpp/CMakeLists.txt
# For more information about using CMake with Android Studio, read the
# documentation: https://d.android.com/studio/projects/add-native-code.html

# Sets the minimum version of CMake required to build the native library.

cmake_minimum_required(VERSION 3.4.1)

# Creates and names a library, sets it as either STATIC
# or SHARED, and provides the relative paths to its source code.
# You can define multiple libraries, and CMake builds them for you.
# Gradle automatically packages shared libraries with your APK.

add_library( # Sets the name of the library.
             native-lib

             # Sets the library as a shared library.
             SHARED

             # Provides a relative path to your source file(s).
             native-lib.cpp )

# ↓↓↓ 追加 ↓↓↓
#set(OpenCV_DIR "D:/devel/opencv-4.3.0-android-sdk/OpenCV-android-sdk/sdk/native/jni")
set(OpenCV_DIR "${CMAKE_CURRENT_LIST_DIR}/../../../../sdk/native/jni")
find_package(OpenCV REQUIRED)
if(OpenCV_FOUND)
    target_include_directories(native-lib PUBLIC ${OpenCV_INCLUDE_DIRS})
    target_link_libraries(native-lib ${OpenCV_LIBS})
endif()
# ↑↑↑ 追加 ↑↑↑


# Searches for a specified prebuilt library and stores the path as a
# variable. Because CMake includes system libraries in the search path by
# default, you only need to specify the name of the public NDK library
# you want to add. CMake verifies that the library exists before
# completing its build.

find_library( # Sets the name of the path variable.
              log-lib

              # Specifies the name of the NDK library that
              # you want CMake to locate.
              log )

# Specifies libraries CMake should link to your target library. You
# can link multiple libraries, such as libraries you define in this
# build script, prebuilt third-party libraries, or system libraries.

target_link_libraries( # Specifies the target library.
                       native-lib

                       # Links the target library to the log library
                       # included in the NDK.
                       ${log-lib} )

一度、メニューバー -> Build -> Reflesh Linked C++ Projects をします。

NDK(C++)側の実装

インターフェイスの変更

今回は、Java側からcv::MatをJNIに渡して、JNI側で画像処理をして受け取ったcv::Matを書き換えることにします。
実際にはcv::Matを直接渡すのではなくて、ポインタをlong型で渡して、キャストして使います。

MainActivity.java に自動的に作られているJNI関数の宣言を以下のように変更します。

MainActivity.java
public native String stringFromJNI();

public native int processImage(long objMatSrc, long objMatDst);

NDK(C++)側の実装

app/src/main/cpp/native-lib.cpp に自動的に作られているサンプルコードを変更します。変更内容は以下の通りです。

  • 関数名と戻り値を変更して、processImage にする
  • 引数にjlong objMatSrcjlong objMatDst を追加
  • objMatcv::Mat* にキャストして、所定の処理を行う
native-lib.cpp
#include <jni.h>
#include <string>

#include <opencv2/opencv.hpp>

extern "C" JNIEXPORT jint JNICALL
Java_com_example_samplecameraxandopencvndk_MainActivity_processImage(
        JNIEnv* env,
        jobject, /* this */
        jlong   objMatSrc,
        jlong   objMatDst) {

    cv::Mat* matSrc = (cv::Mat*) objMatSrc;
    cv::Mat* matDst = (cv::Mat*) objMatDst;

    static cv::Mat *matPrevious = NULL;
    if (matPrevious == NULL) {
        /* lazy initialization */
        matPrevious = new cv::Mat(matSrc->rows, matSrc->cols, matSrc->type());
    }
    cv::absdiff(*matSrc, *matPrevious, *matDst);
    *matPrevious = *matSrc;
    return 0;
}

呼び出し側のコードを変更する

元々、Java側のMyImageAnalyzer::analyze 関数内で、画像処理を行っていました。
今回はこの処理をNDK側で行うので、先ほど作成した関数を呼ぶようにします。

MainActivity.java
        @Override
        public void analyze(@NonNull ImageProxy image) {
            /* Create cv::mat(RGB888) from image(NV21) */
            Mat matOrg = getMatFromImage(image);

            /* Fix image rotation (it looks image in PreviewView is automatically fixed by CameraX???) */
            Mat mat = fixMatRotation(matOrg);

            Log.i(TAG, "[analyze] width = " + image.getWidth() + ", height = " + image.getHeight() + "Rotation = " + previewView.getDisplay().getRotation());
            Log.i(TAG, "[analyze] mat width = " + matOrg.cols() + ", mat height = " + matOrg.rows());

            /* Do some image processing */
            /* ★★★ 変更点 ★★★ */
            Mat matOutput = new Mat(mat.rows(), mat.cols(), mat.type());
            processImage(mat.getNativeObjAddr(), matOutput.getNativeObjAddr());
//            if (matPrevious == null) matPrevious = mat;
//            Core.absdiff(mat, matPrevious, matOutput);
//            matPrevious = mat;

            /* Draw something for test */
            Imgproc.rectangle(matOutput, new Rect(10, 10, 100, 100), new Scalar(255, 0, 0));
            Imgproc.putText(matOutput, "leftTop", new Point(10, 10), 1, 1, new Scalar(255, 0, 0));

            /* Convert cv::mat to bitmap for drawing */
            Bitmap bitmap = Bitmap.createBitmap(matOutput.cols(), matOutput.rows(),Bitmap.Config.ARGB_8888);
            Utils.matToBitmap(matOutput, bitmap);

            /* Display the result onto ImageView */
            runOnUiThread(new Runnable() {
                @Override
                public void run() {
                    imageView.setImageBitmap(bitmap);
                }
            });

            /* Close the image otherwise, this function is not called next time */
            image.close();
        }

こんな感じで、NDK側でもOpenCVを使うことが出来ます。
本題はここまででお終いです。

ここからはおまけです。オレオレ設計について語ります。

マルチプラットフォームに対応した設計について

一般的な画像処理システム

02.jpg

一般的な画像処理システムのデータフローを上に示します。

  • Input
    • カメラ入力など、画像の入力処理を行います
  • Image Processor
    • 画像処理を行います。例えば、ノイズ除去や、物体検知処理などです
    • 所定のフォーマットの画像(Bitmap, YUVなど)を受け取って、処理結果を返します
      • 例えば、ノイズ除去処理だったら、ノイズ除去後の画像を出力する
      • 例えば、物体検知処理だったら、物体検知結果(座標と識別名)を出力する
  • Renderer
    • 画像処理結果を基に、出力画像を生成します
      • 例えば、バウンディングボックスの描画
  • View
    • 出力画像を画面に出力します
    • (Display という名前の方が良かったかも)

環境依存部、仕様依存部を綺麗に分けてみる

環境依存、非依存という観点で、処理を分類してみます。

  • Input
    • カメラ制御、あるいは動画ファイル読み込みなどは環境に依存して処理が変わる(同じコードを使えない)ため、環境依存
    • OpenCVを使えばある程度共通化できるが、Androidとその他では共通化できない。そもそも言語が違う。
  • Image Processor
    • 純粋な画像処理であれば、環境非依存
  • Renderer
    • 所定のフォーマットの画像に描画するだけであれば環境非依存
  • View
    • 出力端末に依って処理が変わる(同じコードを使えない)ため、環境依存

上記のように分類をすると、純粋な画像処理ロジックだけを持つImage Processor部だけをライブラリとするのがよさそうです。
Renderer部は環境依存ではないのですが、どういう風に描画をするかは仕様に依存します。そのため、ライブラリに入れるのは抵抗があります。アプリケーション側で自由に変更できるべきです。

image.png

とはいえ、描画処理は面倒なので横着する

あるべき姿としては、↑の図のように、Image Processor部だけをライブラリ化すべきです。
Image Processorが行う処理がノイズ除去のように、入出力どちらも画像だけの場合にはRendererはそもそも不要なので問題はありません。
Image Processorが行う処理が物体検知処理のように、結果をデータ(座標など)として出力する場合、Rendererがバウンディングボックス等を描画します。この時、どのように出力画面を作るかは、仕様(商品仕様だったり、お客様の要望だったり、上司の気分)に依存します。そのため、アプリケーション側で自由に変更できるように、ライブラリの外に追い出しました。

が、検知結果などの描画処理も結構面倒で、各環境ごとに実装するのは手間がかかります。

ということで、普段手軽に試すだけであれば、下の図のように、Image Processor部とRenderer部をまとめてライブラリ化して、画像を入力して画像を出力するだけにしておくのがお手軽だと思います。
これによって、環境依存部はカメラ入力と画面の出力のみを担当して、それ以外の環境非依存部(画像処理と描画処理)を全てライブラリ化できます。そしてこのライブラリ化部分をCで書いておけば、AndroidアプリからはNDK(JNI)として呼び出せるし、他の環境(WindowsやLinux)でもそのまま使うことが出来ます。

image.png

OpenCVは?

ここまで、OpenCVは当然使えるものとして考えてきました。便利だから使っているだけで、無くても大丈夫です。
Android(Java)側とNDK(JNI)側のインターフェイスとして使っている所は別にバイト列にしてもOKです。ポインタと、width、height、channel情報があればやり取り可能です。
Android(Java)側でもNV21とRGBの変換に使っているくらいなので、頑張れば実装可能です。NDK側でもRGB画像をそのまま独自、あるいは、別のライブラリ(Deep Learning処理等)を使って画像処理するのであればOpenCVは不要です。

サンプルプロジェクト

https://github.com/iwatake2222/play_with_mnn

冒頭の動画に使ったサンプルプロジェクトです。

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

NDKでOpenCVを使った画像処理向けAndroidアプリのひな形 (とマルチプラットフォームに向けた設計)

はじめに

先日、CameraXとOpenCVを使った画像処理向けAndroidアプリのひな形という記事を投稿しました。これは、カメラ入力、画像処理、画面出力のすべてをJavaコードで行いました。
今回は、画像処理をNDK(C++)側で処理してみます。

これによって、上手く設計すれば様々なプラットフォームで共通のロジックコードを使用することが出来ます。これについては後半で少し触れます。

例えば、↓の動画はディープラーニング処理(Pose NetとSemantic Segmentation (DeepLab)) を行う処理と結果の描画処理をライブラリ化し、Androidアプリ、Windowsアプリ、Linuxアプリ(x64, armv7, aarch64)から呼べるようにしています。

環境

  • Host
    • Windows 10 64-bit
    • Android Studio 4.0
    • Android SDK API Level 30
      • 多少低くても大丈夫なはず
    • Android NDK Version 21.3.6528147
    • opencv-4.3.0-android-sdk.zip
  • Target
    • Galaxy S7 (Android 8.0.0)

プロジェクトの用意

プロジェクトの用意

まずは、 CameraXとOpenCVを使った画像処理向けAndroidアプリのひな形 と同様にプロジェクトを作成します。
コードや設定も同じように行ってください。

1点だけ違いがあります。テンプレートとしてEmpty Activity ではなく、Native C++ を選択する必要があります。

01.jpg

Android NDK側でOpenCVを使う

Android NDK(C/C++)側でOpenCVを使うための設定をします。
app/src/main/cpp/CMakeLists.txt を以下のように編集します。
OpenCV_DIR には、OpenCVの場所を指定します。Java用にOpenCVをimportした際に、OpenCV/sdkの中身がプロジェクト内にコピーされます。その場所を相対パスで指定するのが楽だと思います。あるいは、オリジナルのOpencVの場所を指定します。この場合、環境に応じてパスは変更してください。

app/src/main/cpp/CMakeLists.txt
# For more information about using CMake with Android Studio, read the
# documentation: https://d.android.com/studio/projects/add-native-code.html

# Sets the minimum version of CMake required to build the native library.

cmake_minimum_required(VERSION 3.4.1)

# Creates and names a library, sets it as either STATIC
# or SHARED, and provides the relative paths to its source code.
# You can define multiple libraries, and CMake builds them for you.
# Gradle automatically packages shared libraries with your APK.

add_library( # Sets the name of the library.
             native-lib

             # Sets the library as a shared library.
             SHARED

             # Provides a relative path to your source file(s).
             native-lib.cpp )

# ↓↓↓ 追加 ↓↓↓
#set(OpenCV_DIR "D:/devel/opencv-4.3.0-android-sdk/OpenCV-android-sdk/sdk/native/jni")
set(OpenCV_DIR "${CMAKE_CURRENT_LIST_DIR}/../../../../sdk/native/jni")
find_package(OpenCV REQUIRED)
if(OpenCV_FOUND)
    target_include_directories(native-lib PUBLIC ${OpenCV_INCLUDE_DIRS})
    target_link_libraries(native-lib ${OpenCV_LIBS})
endif()
# ↑↑↑ 追加 ↑↑↑


# Searches for a specified prebuilt library and stores the path as a
# variable. Because CMake includes system libraries in the search path by
# default, you only need to specify the name of the public NDK library
# you want to add. CMake verifies that the library exists before
# completing its build.

find_library( # Sets the name of the path variable.
              log-lib

              # Specifies the name of the NDK library that
              # you want CMake to locate.
              log )

# Specifies libraries CMake should link to your target library. You
# can link multiple libraries, such as libraries you define in this
# build script, prebuilt third-party libraries, or system libraries.

target_link_libraries( # Specifies the target library.
                       native-lib

                       # Links the target library to the log library
                       # included in the NDK.
                       ${log-lib} )

一度、メニューバー -> Build -> Reflesh Linked C++ Projects をします。

NDK(C++)側の実装

インターフェイスの変更

今回は、Java側からcv::MatをJNIに渡して、JNI側で画像処理をして受け取ったcv::Matを書き換えることにします。
実際にはcv::Matを直接渡すのではなくて、ポインタをlong型で渡して、キャストして使います。

MainActivity.java に自動的に作られているJNI関数の宣言を以下のように変更します。

MainActivity.java
public native String stringFromJNI();

public native int processImage(long objMatSrc, long objMatDst);

NDK(C++)側の実装

app/src/main/cpp/native-lib.cpp に自動的に作られているサンプルコードを変更します。変更内容は以下の通りです。

  • 関数名と戻り値を変更して、processImage にする
  • 引数にjlong objMatSrcjlong objMatDst を追加
  • objMatcv::Mat* にキャストして、所定の処理を行う
native-lib.cpp
#include <jni.h>
#include <string>

#include <opencv2/opencv.hpp>

extern "C" JNIEXPORT jint JNICALL
Java_com_example_samplecameraxandopencvndk_MainActivity_processImage(
        JNIEnv* env,
        jobject, /* this */
        jlong   objMatSrc,
        jlong   objMatDst) {

    cv::Mat* matSrc = (cv::Mat*) objMatSrc;
    cv::Mat* matDst = (cv::Mat*) objMatDst;

    static cv::Mat *matPrevious = NULL;
    if (matPrevious == NULL) {
        /* lazy initialization */
        matPrevious = new cv::Mat(matSrc->rows, matSrc->cols, matSrc->type());
    }
    cv::absdiff(*matSrc, *matPrevious, *matDst);
    *matPrevious = *matSrc;
    return 0;
}

呼び出し側のコードを変更する

元々、Java側のMyImageAnalyzer::analyze 関数内で、画像処理を行っていました。
今回はこの処理をNDK側で行うので、先ほど作成した関数を呼ぶようにします。

MainActivity.java
        @Override
        public void analyze(@NonNull ImageProxy image) {
            /* Create cv::mat(RGB888) from image(NV21) */
            Mat matOrg = getMatFromImage(image);

            /* Fix image rotation (it looks image in PreviewView is automatically fixed by CameraX???) */
            Mat mat = fixMatRotation(matOrg);

            Log.i(TAG, "[analyze] width = " + image.getWidth() + ", height = " + image.getHeight() + "Rotation = " + previewView.getDisplay().getRotation());
            Log.i(TAG, "[analyze] mat width = " + matOrg.cols() + ", mat height = " + matOrg.rows());

            /* Do some image processing */
            /* ★★★ 変更点 ★★★ */
            Mat matOutput = new Mat(mat.rows(), mat.cols(), mat.type());
            processImage(mat.getNativeObjAddr(), matOutput.getNativeObjAddr());
//            if (matPrevious == null) matPrevious = mat;
//            Core.absdiff(mat, matPrevious, matOutput);
//            matPrevious = mat;

            /* Draw something for test */
            Imgproc.rectangle(matOutput, new Rect(10, 10, 100, 100), new Scalar(255, 0, 0));
            Imgproc.putText(matOutput, "leftTop", new Point(10, 10), 1, 1, new Scalar(255, 0, 0));

            /* Convert cv::mat to bitmap for drawing */
            Bitmap bitmap = Bitmap.createBitmap(matOutput.cols(), matOutput.rows(),Bitmap.Config.ARGB_8888);
            Utils.matToBitmap(matOutput, bitmap);

            /* Display the result onto ImageView */
            runOnUiThread(new Runnable() {
                @Override
                public void run() {
                    imageView.setImageBitmap(bitmap);
                }
            });

            /* Close the image otherwise, this function is not called next time */
            image.close();
        }

こんな感じで、NDK側でもOpenCVを使うことが出来ます。
本題はここまででお終いです。

ここからはおまけです。オレオレ設計について語ります。
無視してください。

マルチプラットフォームに対応した設計について

一般的な画像処理システム

02.jpg

一般的な画像処理システムのデータフローを上に示します。

  • Input
    • カメラ入力など、画像の入力処理を行います
  • Image Processor
    • 画像処理を行います。例えば、ノイズ除去や、物体検知処理などです
    • 所定のフォーマットの画像(Bitmap, YUVなど)を受け取って、処理結果を返します
      • 例えば、ノイズ除去処理だったら、ノイズ除去後の画像を出力する
      • 例えば、物体検知処理だったら、物体検知結果(座標と識別名)を出力する
  • Renderer
    • 画像処理結果を基に、出力画像を生成します
      • 例えば、バウンディングボックスの描画
  • View
    • 出力画像を画面に出力します
    • (Display という名前の方が良かったかも)

環境依存部、仕様依存部を綺麗に分けてみる

環境依存、非依存という観点で、処理を分類してみます。

  • Input
    • カメラ制御、あるいは動画ファイル読み込みなどは環境に依存して処理が変わる(同じコードを使えない)ため、環境依存
    • OpenCVを使えばある程度共通化できるが、Androidとその他では共通化できない。そもそも言語が違う。
  • Image Processor
    • 純粋な画像処理であれば、環境非依存
  • Renderer
    • 所定のフォーマットの画像に描画するだけであれば環境非依存
    • どういう風に描画したいか? に影響されるので仕様依存
  • View
    • 出力端末に依って処理が変わる(同じコードを使えない)ため、環境依存

上記のように分類をすると、純粋な画像処理ロジックだけを持つImage Processor部だけをライブラリとするのがよさそうです。
Renderer部は環境依存ではないのですが、どういう風に描画をするかは仕様に依存します。そのため、ライブラリに入れるのは抵抗があります。アプリケーション側で自由に変更できるべきです。

image.png

とはいえ、描画処理は面倒なので横着する

あるべき姿としては、↑の図のように、Image Processor部だけをライブラリ化すべきです。
Image Processorが行う処理がノイズ除去のように、入出力どちらも画像だけの場合にはRendererはそもそも不要なので問題はありません。
Image Processorが行う処理が物体検知処理のように、結果をデータ(座標など)として出力する場合、Rendererがバウンディングボックス等を描画します。この時、どのように出力画面を作るかは、仕様(商品仕様だったり、お客様の要望だったり、上司の気分)に依存します。そのため、アプリケーション側で自由に変更できるように、ライブラリの外に追い出しました。

が、検知結果などの描画処理も結構面倒で、各環境ごとに実装するのは手間がかかります。

ということで、普段手軽に試すだけであれば、下の図のように、Image Processor部とRenderer部をまとめてライブラリ化して、画像を入力して画像を出力するだけにしておくのがお手軽だと思います。
これによって、環境依存部はカメラ入力と画面の出力のみを担当して、それ以外の環境非依存部(画像処理と描画処理)を全てライブラリ化できます。そしてこのライブラリ化部分をCで書いておけば、AndroidアプリからはNDK(JNI)として呼び出せるし、他の環境(WindowsやLinux)でもそのまま使うことが出来ます。
(ちなみに、ここまで「ライブラリ」という言葉を使ってしまっていますが、同一バイナリファイルを使いまわせるわけではありません。実際にはcmake上でのライブラリ(add_library)として扱うのが便利です。)

image.png

OpenCVは?

ここまで、OpenCVは当然使えるものとして考えてきました。便利だから使っているだけで、無くても大丈夫です。
Android(Java)側とNDK(JNI)側のインターフェイスとして使っている所は別にバイト列にしてもOKです。ポインタと、width、height、channel情報があればやり取り可能です。
Android(Java)側でもNV21とRGBの変換に使っているくらいなので、頑張れば実装可能です。NDK側でもRGB画像をそのまま独自、あるいは、別のライブラリ(Deep Learning処理等)を使って画像処理するのであればOpenCVは不要です。

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