THETA プラグインで QR コードを読む

※改善版の記事を公開中です。ぜひ合わせてお読みください。
[【改善版】THETA プラグインで QR コードを読む - Qiita](https://qiita.com/shrhdk/items/7e6ce8ca8ae7f9fc9937)

![QR コードスキャナ](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/299819/9d801c1c-e750-4a52-483d-8fe3c211ae8f.png)

## はじめに

こんにちは、リコーの @shrhdk_ です。

リコーでは RICOH THETA という全周囲360度撮れるカメラをつくっています。

実は、最近の THETA V や THETA Z1 といった機種は、OS に Android を採用していて、本体内部で Android アプリを動かすことができます。

THETA 向けに開発した Android アプリのことを THETA プラグインと呼んでいて、開発したプラグインは [公式プラグインストア](https://pluginstore.theta360.com/) にて配布することができます。

今回は、QR コードを読み取るプラグインを作ってみたので、作り方を紹介します。

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

THETA プラグインをご存じない方は [こちら](https://api.ricoh/products/theta-plugin/) をご覧ください。

興味を持たれた方は [Twitter](https://twitter.com/thetaplugin) のフォローと [THETAプラグイン開発コミュニティ (Slack)](https://join.slack.com/t/thetadeveloper/shared_invite/enQtNzcxODI2MTk2ODg0LWY5YmU2ZTIxNGYwNzlmNWZjMWMyMWFlMzY3ZmQ3NGU3ZjZiZjI0Njk4MGIxMDU5NDJiMzQ4Y2FkNDEzYmFlNGU) への参加もよろしくお願いします｡

## 今回作ったプラグイン

今回作ったプラグインは、実用性はありません。以下のような動きをします。

1. 起動するとすぐにスキャンが開始される
2. 色名の QR コードをスキャンする
    * シャッターボタンの反対側のレンズにむける
    * 対応する色名は `RED`, `GREEN`, `BLUE` の3種類
3. スキャン結果が表示される
    * THETA V では Wi-Fi LED がスキャンした色で点灯する
    * THETA Z1 では OLED にスキャンした色名が表示される
4. シャッターボタンを押すとスキャンが再開される

## ビルド方法

ソースコードは以下のリポジトリで公開しています。

https://github.com/theta-skunkworks/theta-plug-in-qr-code-scanner

ソースコードをダウンロードして、Android Studio でインポートしてビルドしてください。

## ライブラリ

### THETA Plug-in Library

THETA プラグイン特有の機能を利用するために、プロジェクトに SDK を取り込む必要があります。

`プロジェクトルート/build.gradle` に以下の行を追加して、

```diff_gradle:/build.gradle
allprojects {
    repositories {
        google()
        jcenter()
+       maven { url 'https://github.com/ricohapi/theta-plugin-library/raw/master/repository' } // この行を追加
    }
}
```

`プロジェクトルート/app/build.gradle` に以下の行を追加します。

```diff_gradle:/app/build.gradle
dependencies {
    ...
+   implementation 'com.theta360:pluginlibrary:2.0.0' // この行を追加
    ...
}
```

### ZXing

QR コードのデコードには [ZXing](https://github.com/zxing/zxing) というライブラリを使います。「ゼブラ・クロッシング」と読むらしいです。洒落てますね。

このライブラリは Android に対応しているので、Android ベースの THETA プラグイン開発にも使えます。

`プロジェクトルート/app/build.gradle` に以下の行を追加して、プロジェクトに取り込みます。

```diff_gradle:/app/build.gradle
dependencies {
    ...
+   implementation 'com.google.zxing:javase:3.3.3'       // この行を追加
+   implementation 'com.google.zxing:android-core:3.3.0' // この行を追加
    ...
}
```

## 処理の流れ

大まかな処理の流れは以下のようになります。

1. カメラ初期化
2. キャプチャ開始
3. コールバックで画像データ取得
4. 画像変換
5. ZXing でデコード (できなかったら 3 へ戻る)
6. キャプチャ停止
7. 結果表示

それぞれ順番に解説していきます。プロジェクト全体は以下のリポジトリで公開しています。

https://github.com/theta-skunkworks/theta-plug-in-qr-code-scanner

### 0. THETA Plug-in Library の利用

THETA Plug-in Library の API は `com.theta360.pluginlibrary.activity.PluginActivity` を継承して使います。

以後、断りなく出てくるメソッドは `PluginActivity` のメソッドだと考えてください。

```kotlin:MainActivity.kt
class MainActivity : PluginActivity(), Camera.PreviewCallback {
    ...
}
```

### 1. カメラ初期化 (と後処理)

カメラ初期化は onResume でやります。

SurfaceTexture の生成と設定、カメラの設定をしています。

[THETA のカメラ API](https://api.ricoh/docs/theta-plugin-reference/camera-api/) は Android Camera API v1 をベースにいくつかの独自パラメータを追加したものとなっています。

あわせて、ボタンのイベントハンドラの登録と、キャプチャ開始のメソッドも呼んでいます。

```kotlin:MainActivity.kt
private var mTexture: SurfaceTexture? = null
private var mCamera: Camera? = null

override fun onResume() {
    super.onResume()

    notificationCameraClose() // THETA 本体アプリにカメラを開放してもらう

    mTexture = SurfaceTexture(10)
    mCamera = Camera.open().apply {
        parameters = parameters.apply {
            setPreviewSize(1920, 960) 
            set("RIC_SHOOTING_MODE", "RicMoviePreview1920") // 1920x960 (THETA 独自パラメータ)
            set("RIC_PROC_STITCHING", "RicStaticStitching") // Equirectangular 形式にする (THETA 独自パラメータ)
            previewFrameRate = 5
            previewFormat = ImageFormat.YV12
        }
        setPreviewTexture(mTexture)
    }
    
    // ボタンのイベントハンドラの登録
    setKeyCallback(object : KeyCallback {
        override fun onKeyDown(keyCode: Int, keyEvent: KeyEvent?) {
            if (keyCode == KeyReceiver.KEYCODE_CAMERA) { // シャッターボタンが押された
                start() // キャプチャ再開
            }
        }
        // ...
    })

    start() // キャプチャ開始
}
```

### 2. キャプチャ開始

キャプチャ開始のメソッドは次のようになっています。

まず、フレームデータのコールバックを登録して、`Camera#startPreview` でキャプチャを開始します。

キャプチャ開始後に V では Wi-Fi LED を消灯し、Z1 では OLED に `Scanning` に表示しています。

最後に開始音を鳴らします。

```kotlin:MainActivity.kt
private fun start() {
    mCamera?.apply {
        setPreviewCallback(this@MainActivity) // MainActivity が Camera.Callback を実装している
        startPreview()

        notificationLedHide(LedTarget.LED3) // for V
        showOledTextMiddle("Scanning")      // for Z1
        notificationAudioMovStart()         // 開始音を鳴らす
    }
}
```

`showOledTextMiddle` メソッドは次のような実装で、ブロードキャストインテントを投げています。

```kotlin:MainActivity.kt
private fun showOledTextMiddle(text: String) {
    sendBroadcast(Intent("com.theta360.plugin.ACTION_OLED_TEXT_SHOW").apply {
        putExtra("text-middle", text)
    })
}
```

このあたりの API の仕様は以下のページで解説されています。

https://api.ricoh/docs/theta-plugin-reference/broadcast-intent/#displaying-characters

### 3. コールバックで画像データ取得

`Camera#startPreview` を呼ぶと、`Camera#setPreviewCallback` メソッドで登録したオブジェクトのメソッドがフレーム毎に呼ばれます。

```kotlin:MainActivity.kt
override fun onPreviewFrame(data: ByteArray?, camera: Camera?) {
    // 毎フレーム呼ばれる
    // data にフレームデータが入っている
}
```

### 4. 画像変換

まず、取得した画像をトリミングしつつ、ZXing で処理可能な形式に変換します。

data には Equirectangular 形式で大きさは 1920x960 の画像データが入っています。RGB 形式ではなく、YUV の YV12 形式となっています。

Equirectangular 形式は画像端の歪みが大きいため、中央部分をくり抜いて使います。本来は射影方式を変換して歪みを取り除くべきですが、今回は簡易的に処理しています。

```kotlin:MainActivity.kt
override fun onPreviewFrame(data: ByteArray?, camera: Camera?) {
    val width = 1920 / 5
    val height = 960 / 3
    val left = width * 2
    val top = height * 1
    val src = PlanarYUVLuminanceSource(data, 1920, 960, left, top, width, height, false)
    val bmp = BinaryBitmap(HybridBinarizer(src))
    
    // ...
    // ... ZXing でデコード
    // ...
}
```

### 5. ZXing でデコード

`QRCodeReader` クラスを使って、QR コード画像をデコードします。

見つからなかったり、符号訂正できなかったりした場合は例外が発生するので、キャッチしてログを出しています。

QR コードのデコードに成功したら、カメラのキャプチャを停止します。(シャッターボタン押下で再開)

```kotlin:MainActivity.kt
private val mReader: Reader = QRCodeReader()

override fun onPreviewFrame(data: ByteArray?, camera: Camera?) {
    // ...
    // ... 画像変換処理
    // ...    

    val result: Result
    try {
        result = mReader.decode(bmp)
    } catch (e: Exception) {
        Log.d(TAG, "NOT FOUND")
        return
    }
    Log.d(TAG, "FOUND ${result.text}")

    stop() // キャプチャを停止

    // ...
    // ... 結果表示
    // ...
}
```

### 6. キャプチャ停止

`Camera#stopPreview` でキャプチャを停止して、終了音を鳴らします。

```kotlin:MainActivity.kt
private fun stop() {
    mCamera?.apply {
        stopPreview() // 停止
        notificationAudioMovStop() // 終了音を鳴らす
    }
}
```

### 7. 結果表示

THETA V の場合は Wi-Fi LED の点灯色を変更し、THETA Z1 では OLED の色名を表示します。

```kotlin:MainActivity.kt
override fun onPreviewFrame(data: ByteArray?, camera: Camera?) {
    // ...
    // ... ZXing でデコード
    // ...

    when (result.text) { // for V
        "RED" -> notificationLed3Show(LedColor.RED)
        "GREEN" -> notificationLed3Show(LedColor.GREEN)
        "BLUE" -> notificationLed3Show(LedColor.BLUE)
    }
    showOledTextMiddle(result.text) // for Z1
}
```

## まとめ

THETA プラグインで QR コードを読み込む方法を紹介しました。

今回は数バイト程度の情報を QR コードで読み取れました。もう少し多くの情報量を読めるかもしれませんが試せていません。興味を持たれた方がお試しくれるとうれしいです。

QR コードは Bluetooth や Wi-Fi よりも手軽にデータを受け渡せるので、プラグインの可能性が広がるのではないでしょうか。

興味を持たれた方は [Twitter](https://twitter.com/thetaplugin) のフォローと [THETAプラグイン開発コミュニティ (Slack)](https://join.slack.com/t/thetadeveloper/shared_invite/enQtNzcxODI2MTk2ODg0LWY5YmU2ZTIxNGYwNzlmNWZjMWMyMWFlMzY3ZmQ3NGU3ZjZiZjI0Njk4MGIxMDU5NDJiMzQ4Y2FkNDEzYmFlNGU) への参加もよろしくお願いします｡