THETA でインスタントカメラを作る

**【告知】THETA インスタントカメラ、Maker Faire Tokyo 2019 で展示します。お気軽に遊びにきてください。**

[THETAプラグイン開発者コミュニティ \| Maker Faire Tokyo 2019 \| Make: Japan](https://makezine.jp/event/makers-mft2019/m0169/)z

## はじめに

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

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

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

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

今回は、THETA をインスタントカメラにしてしまうプラグインを作ったので紹介します。

![動作](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/299819/eb409861-5453-8355-edfb-6c6a08c46c23.gif)

![印刷結果](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/299819/2d7b4b67-6d8b-1709-ccd0-ead66d7ae40e.jpeg)

※このプラグインはストア配布はしていません。ソースコードと設計情報を GitHub で公開しているので、開発者の方はお試しいただけます。

公開中 → https://github.com/theta-skunkworks/theta-plugin-instant-print

## つかいかた

使い方はとてもかんたんです。THETA 本体のボタンを押すと、5秒のセルフタイマーで写真を撮影します。
そして、撮影した写真を THETA に接続した感熱紙プリンタから白黒のパノラマ写真として印刷します。

感熱紙プリンタはレシートプリンタのことです。レシート用の感熱ロール紙が使えるので、印刷コストが激安です！

撮影の制御も、印刷の制御もすべて THETA 単体で実現しています。

## ハードウェア構成

ハードウェア構成もかんたんです。感熱紙プリンタは **ナダ電子株式会社様の [AS-289R2](http://www.nada.co.jp/as289r2/)** を使っています。スイッチサイエンス様で買えます。

このプリンタの I/F は UART なので、USB-OTG ケーブルと秋月電子の [FT234X 超小型USBシリアル変換モジュール](http://akizukidenshi.com/catalog/g/gM-08461/) を組み合わせて THETA と接続しています。

あとは AC アダプタ等の電源系です。感熱紙プリンタは消費電力が大きいので、強めの AC アダプタを使っています。

![ハードウェア構成図](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/299819/9072f115-8db4-6de4-a09c-94bd04e2d1d2.png)

![ハードウェア](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/299819/a4c31507-da7f-c007-ce32-1c8dcb5ed72c.jpeg)

AS-289R2 のベース基板は、昨年の MFT にてナダ電子さんが配布していたものを、MFT 後に弊社社員が Twitter でいただいたものです。

### 回路図

USBシリアル変換モジュールのTXDをプリンタ側のRXD1に接続し、お互いのGNDを接続します。

今回は、大きなビットマップイメージを印刷するので JP6 のジャンパをショートさせて、38,400bps で動作させます。

![回路図](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/299819/0690954a-0831-09e0-1509-accdf1304fed.png)

## ビルド方法

プロジェクトをクローンしてから、OpenCV のライブラリをプロジェクトに追加してください。

[OpenCV](https://opencv.org/) の公式サイトから、v4.1.1 の Android 版をダウンロードして解凍すると、`sdk` というディレクトリが入っています。これをクローンしたプロジェクトのルートにコピーして、ディレクトリ名を `opencv` に変更します。

追加できたら、Android Studio でビルドするか、Gradle でビルドしてください。

```sh
$ git clone https://github.com/theta-skunkworks/theta-plugin-instant-print
$ cd theta-plugin-instant-print
  ~~ OpenCV を追加 ~~
$ ./gradlew build
```

プロジェクト: https://github.com/theta-skunkworks/theta-plugin-instant-print

## ソフトウェアの構成と処理の流れ

ソフトウェアは大まかに次のような流れで処理しています。

1. ボタン入力
2. 撮影
3. 画像処理 (印刷に適した画像に変換)
4. 印刷

構成図にすると次のようになります。緑色の領域がプラグインです。

![ソフトウェア構成図](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/299819/146dde7f-e179-d795-ca28-23d2cca6edcf.png)

順番に説明していきます。

### ボタン入力

ボタンの入力を受け付けるために、THETA Plug-in Library の `PluginActivity#setCallback` メソッドを読んで、リスナーを登録しています。ボタン入力については [【THETAプラグイン開発】THETAプラグインでHello World \- Qiita](https://qiita.com/meronpan/items/fde7d66bed7f31f9e193) で詳しく解説されています。

```kotlin
class MainActivity: PluginActivity() { 
    ...
    override fun onResume() {
        super.onResume()
        ...
        setKeyCallback(object : KeyCallback {
            override fun onKeyDown(code: Int, event: KeyEvent) {
                // ボタンを押すとこのメソッドが呼ばれる
                // ここで撮影処理を開始
            }
            ...
        })
    }
    ...
}
```

内部的には、ボタン押下時に送出されるブロードキャストインテントを受信して、リスナーのメソッドを呼び出しています。ブロードキャストインテントの仕様は以下のページで説明されています。

[Docs/THETA Plug-in Broadcast Intent - Receiving Button Events](https://api.ricoh/docs/theta-plugin-reference/broadcast-intent/#receiving-button-events)

ボタン入力イベントが発生すると、次に撮影処理を実行します。

### 撮影

撮影処理では、カメラパラメータを設定して撮影を実行します。これは [THETA API v2.1] を利用して実現しています。THETA API をかんたんに使うためのライブラリとして、[THETA Web API Client](https://github.com/theta4j/theta-web-api) を利用しています。([解説記事](https://qiita.com/shrhdk/items/5b9bca30db34822ca7af))

まず、撮影モードを静止画にします。THETA が動画モードの状態でプラグインが起動されても、撮影モードが自動的に静止画モードになることはありません。明示的に指定する必要があります。

次に、動作音量を最大にします。このプラグインはみんなでTHETAを取り囲んで、記念撮影をするような使い方を想定しているので、動作音量が皆に聞こえるように大きくします。あわせて、セルフタイマーを5秒に設定しています。

最後に撮影を実行して、その結果を取得しています。

```kotlin
import org.theta4j.webapi.Options.*;
...
executor.submit {
    theta.setOption(CAPTURE_MODE, CaptureMode.IMAGE) // 静止画モードに設定
    val opts = OptionSet.Builder()
        .put(SHUTTER_VOLUME, 100)                    // 動作音量を最大に
        .put(EXPOSURE_DELAY, 5)                      // セルフタイマー5秒
        .build()
    theta.setOptions(opts)                           // 設定
    val res = waitForDone(theta.takePicture())       // 撮影して、完了まで待つ
    // res.result に撮影結果の画像ファイルのパスが入っている
}
```

`theta.takePicture` は同期的に完了しないので、実行直後は撮影結果の画像ファイルも存在しません。ここでは、`waitForDone` というヘルパー関数を使って、撮影終了をポーリングで待っています。`waitForDone` 関数は以下のような定義になっています。

```kotlin
private fun <R> waitForDone(response: CommandResponse<R>): CommandResponse<R> {
    var res = response
    while (res.state != CommandState.DONE) { // 完了ならループ終了
        res = theta.commandStatus(res)       // コマンドの実行状態を問い合わせ
        Thread.sleep(100)                    // 100ms 間隔
    }
    return res
}
```

### 画像処理

画像処理には OpenCV を利用しています。OpenCV の導入方法についてはこちらの記事が詳しいです。

[THETAの中でOpenCVを動かす - Qiita](https://qiita.com/roohii_3/items/63a546169981f41967df)

画像処理は `ImgConv.java` と `imgconv.cpp` に実装しています。

記事では OpenCV 3.4.4 を使っていますが、今回は4系を使ってみました。

次のような流れで画像を処理しています。2値化の処理では全ピクセルに順次アクセスするのですが、このような処理が Java では非常に遅かったため 6,7,8 は C++ で実装しています。

1. ファイルから Bitmap オブジェクトを生成 (`MainActivity.kt`)
2. パノラマ画像の中心調整 (`ImgConv.java`)
3. 縮小 (`ImgConv.java`)
4. クロップ (`ImgConv.java`)
5. 回転 (`ImgConv.java`)
6. グレースケール化 (`imgconv.cpp`)
7. 2値化 (`imgconv.cpp`)
8. 印刷データに変換 (`imgconv.cpp`)

#### パノラマ画像の中心調整

THETA では、撮影ボタン側の被写体がパノラマ画像(Equirectangular画像)の両端に現れます。

今回のシステムでは、ボタンを押した人を中心に配置したいので、画像の中心の調整をしています。

処理はかんたんで、画像を左右に分割して、入れ替えているだけです。

![パノラマ画像の中心調整](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/299819/344c356b-e419-ff87-6174-13b1f1e1fa59.png)

#### 縮小・クロップ・回転

感熱紙プリンタ (AS-289R2) は幅384ピクセルの縦に長い画像を印刷できます。

THETAの撮影画像は比率2:1の横長画像ですので、これを以下の手順で比率1:4、幅384ピクセルの縦長画像に変換しています。

1. 1536x762 に縮小 (比率2:1)
2. 上下をクロップして 1536x384 に変形 (比率4:1)
3. 回転して 384x1536 に変形 (比率1:4)

![画像の変換](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/299819/3d4732b3-2124-42ff-7604-e127b10dd32b.png)

#### グレースケール化

グレースケール化は C++ から OpenCV を使って実装しています。単純に `cv::cvtColor` を呼んでいます。

```cpp
cv::Mat m_src(height, width, CV_8UC4, (u_char *) p_src); // p_src は Java から受け取った RGBA のバイト列
cv::Mat m_gray(height, width, CV_8U);
cv::cvtColor(m_src, m_gray, cv::COLOR_RGBA2GRAY); // m_src がグレースケール化されて m_gray に入る
```

#### 2値化

画像の2値化には、[フロイド-シュタインバーグ・ディザリング](https://ja.wikipedia.org/wiki/%E3%83%95%E3%83%AD%E3%82%A4%E3%83%89-%E3%82%B9%E3%82%BF%E3%82%A4%E3%83%B3%E3%83%90%E3%83%BC%E3%82%B0%E3%83%BB%E3%83%87%E3%82%A3%E3%82%B6%E3%83%AA%E3%83%B3%E3%82%B0) というアルゴリズムを実装しました。384ピクセルの小さな解像度でも比較的自然に2値化ができました。

#### 印刷データに変換

印刷データは1ピクセルが1ビットに対応するバイト列です。幅384ピクセルなので、384ピクセル/8ビットで、1行あたり48バイトです。

バイト0のビット7が画像左上に対応し、バイト47のビット0が画像右上に対応します。バイト48のビット7は画像2行目の右端です。

ビットが1であれば黒く、0であれば白くなります。

![印刷データ](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/299819/24bb5eb1-a5e8-6d46-1fcc-e3e7af1e41a2.png)

### 印刷

印刷関連の処理は `Printer.java` に実装しています。

#### ライブラリ

シリアル通信 (UART) で印刷データをプリンタに送信して印刷します。

Android でシリアル通信を実現するライブラリとして、[mik3y/usb-serial-for-android](https://github.com/mik3y/usb-serial-for-android) を利用しています。

この THETA プラグインでこのライブラリの使う方法は以下の記事が詳しいです。USB 機器を利用する際の権限周りの設定についても詳しく説明されています。

[THETAプラグインでGPS/GNSSレシーバーと連携する【USB Hostシリアル通信】 \- Qiita](https://qiita.com/KA-2/items/711c76fa86e16a5e83c3)

上の記事ではオリジナルのライブラリを自力でビルドしていますが、今回はビルド済みバイナリが配布されているフォーク([kai-morich/usb-serial-for-android](https://github.com/kai-morich/usb-serial-for-android))を利用しています。

#### プリンタ印刷コマンド

プリンタの利用方法は公式ページで配布されている制御コマンド表に書かれています。

[FAQ \| AS\-289R2プリンタシールド \| ナダ電子](http://www.nada.co.jp/as289r2/faq.html)

ビットマップイメージを印刷する場合は、以下のフォーマットでバイト列を送信します。`1c 2a 65` は定数で16進数表記です。画像サイズの指定はバイト数ではなく行数(高さ)なので注意が必要です。

```
1c 2a 65 <画像行数(16bit)> <印刷データのバイト列>
```

#### データ送信

実際のデータ送信は [UsbSerialPort#write](https://github.com/kai-morich/usb-serial-for-android/blob/v1.7.0/usbSerialForAndroid/src/main/java/com/hoho/android/usbserial/driver/UsbSerialPort.java#L134) メソッドを使います。第1引数にバイト列を、第2引数にタイムアウト値をミリ秒で指定します。

```java
mPort.write(data, 1000);
```

`data` に長さ 16,384バイト以上のデータを指定すると、溢れた分は捨てられるので注意が必要です。

今回の画像データは 73,728バイト (1536ピクセル * 384ピクセル / 8ビット) ですので、分割して送信しています。

指定したタイムアウト時間までにすべてのデータ送信が完了しないと、タイムアウトエラーが発生します。AS-289R2 は 38,400bps で通信するので、16,384バイトを送信するには約3.4秒かかります。タイムアウト値は少なくともそれ以上を指定する必要があります。

## まとめ

外付けのプリンタデバイスと連携する THETA プラグインを作成しました。THETA プラグインシステムでは、撮影・画像変換・プリンタ制御といった様々な処理を THETA 単体で実現することができます。