※改善版の記事を公開中です。ぜひ合わせてお読みください。
【改善版】THETA プラグインで QR コードを読む - Qiita
はじめに
こんにちは、リコーの @shrhdk_ です。
リコーでは RICOH THETA という全周囲360度撮れるカメラをつくっています。
実は、最近の THETA V や THETA Z1 といった機種は、OS に Android を採用していて、本体内部で Android アプリを動かすことができます。
THETA 向けに開発した Android アプリのことを THETA プラグインと呼んでいて、開発したプラグインは 公式プラグインストア にて配布することができます。
今回は、QR コードを読み取るプラグインを作ってみたので、作り方を紹介します。
RICOH THETA プラグインパートナープログラムについて
THETA プラグインをご存じない方は こちら をご覧ください。
興味を持たれた方は Twitter のフォローと THETAプラグイン開発コミュニティ (Slack) への参加もよろしくお願いします。
今回作ったプラグイン
今回作ったプラグインは、実用性はありません。以下のような動きをします。
- 起動するとすぐにスキャンが開始される
- 色名の QR コードをスキャンする
- シャッターボタンの反対側のレンズにむける
- 対応する色名は
RED
,GREEN
,BLUE
の3種類
- スキャン結果が表示される
- THETA V では Wi-Fi LED がスキャンした色で点灯する
- THETA Z1 では OLED にスキャンした色名が表示される
- シャッターボタンを押すとスキャンが再開される
ビルド方法
ソースコードは以下のリポジトリで公開しています。
ソースコードをダウンロードして、Android Studio でインポートしてビルドしてください。
ライブラリ
THETA Plug-in Library
THETA プラグイン特有の機能を利用するために、プロジェクトに SDK を取り込む必要があります。
プロジェクトルート/build.gradle
に以下の行を追加して、
allprojects {
repositories {
google()
jcenter()
+ maven { url 'https://github.com/ricohapi/theta-plugin-library/raw/master/repository' } // この行を追加
}
}
プロジェクトルート/app/build.gradle
に以下の行を追加します。
dependencies {
...
+ implementation 'com.theta360:pluginlibrary:2.0.0' // この行を追加
...
}
ZXing
QR コードのデコードには ZXing というライブラリを使います。「ゼブラ・クロッシング」と読むらしいです。洒落てますね。
このライブラリは Android に対応しているので、Android ベースの THETA プラグイン開発にも使えます。
プロジェクトルート/app/build.gradle
に以下の行を追加して、プロジェクトに取り込みます。
dependencies {
...
+ implementation 'com.google.zxing:javase:3.3.3' // この行を追加
+ implementation 'com.google.zxing:android-core:3.3.0' // この行を追加
...
}
処理の流れ
大まかな処理の流れは以下のようになります。
- カメラ初期化
- キャプチャ開始
- コールバックで画像データ取得
- 画像変換
- ZXing でデコード (できなかったら 3 へ戻る)
- キャプチャ停止
- 結果表示
それぞれ順番に解説していきます。プロジェクト全体は以下のリポジトリで公開しています。
0. THETA Plug-in Library の利用
THETA Plug-in Library の API は com.theta360.pluginlibrary.activity.PluginActivity
を継承して使います。
以後、断りなく出てくるメソッドは PluginActivity
のメソッドだと考えてください。
class MainActivity : PluginActivity(), Camera.PreviewCallback {
...
}
1. カメラ初期化 (と後処理)
カメラ初期化は onResume でやります。
SurfaceTexture の生成と設定、カメラの設定をしています。
THETA のカメラ API は Android Camera API v1 をベースにいくつかの独自パラメータを追加したものとなっています。
あわせて、ボタンのイベントハンドラの登録と、キャプチャ開始のメソッドも呼んでいます。
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
に表示しています。
最後に開始音を鳴らします。
private fun start() {
mCamera?.apply {
setPreviewCallback(this@MainActivity) // MainActivity が Camera.Callback を実装している
startPreview()
notificationLedHide(LedTarget.LED3) // for V
showOledTextMiddle("Scanning") // for Z1
notificationAudioMovStart() // 開始音を鳴らす
}
}
showOledTextMiddle
メソッドは次のような実装で、ブロードキャストインテントを投げています。
private fun showOledTextMiddle(text: String) {
sendBroadcast(Intent("com.theta360.plugin.ACTION_OLED_TEXT_SHOW").apply {
putExtra("text-middle", text)
})
}
このあたりの API の仕様は以下のページで解説されています。
3. コールバックで画像データ取得
Camera#startPreview
を呼ぶと、Camera#setPreviewCallback
メソッドで登録したオブジェクトのメソッドがフレーム毎に呼ばれます。
override fun onPreviewFrame(data: ByteArray?, camera: Camera?) {
// 毎フレーム呼ばれる
// data にフレームデータが入っている
}
4. 画像変換
まず、取得した画像をトリミングしつつ、ZXing で処理可能な形式に変換します。
data には Equirectangular 形式で大きさは 1920x960 の画像データが入っています。RGB 形式ではなく、YUV の YV12 形式となっています。
Equirectangular 形式は画像端の歪みが大きいため、中央部分をくり抜いて使います。本来は射影方式を変換して歪みを取り除くべきですが、今回は簡易的に処理しています。
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 コードのデコードに成功したら、カメラのキャプチャを停止します。(シャッターボタン押下で再開)
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
でキャプチャを停止して、終了音を鳴らします。
private fun stop() {
mCamera?.apply {
stopPreview() // 停止
notificationAudioMovStop() // 終了音を鳴らす
}
}
7. 結果表示
THETA V の場合は Wi-Fi LED の点灯色を変更し、THETA Z1 では OLED の色名を表示します。
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 のフォローと THETAプラグイン開発コミュニティ (Slack) への参加もよろしくお願いします。