THETAプラグインでスリットスキャンを試す

この記事は [RICOH THETA Advent Calendar 2020](https://qiita.com/advent-calendar/2020/theta) の23日目の記事です。
アドベントカレンダーが終わっても、皆さんが作るTHETA関連（特にTHETAプラグイン）記事をまってます！
（過去の空箱を埋めていただくのも大歓迎です！）

## はじめに

リコーの @KA-2 です。

弊社では[RICOH THETA](https://theta360.com/ja/)という全周囲360度撮れるカメラを出しています。
<B>RICOH THETA V</B>や<b>RICOH THETA Z1</b>は、OSに<B>Android</B>を採用しています。Androidアプリを作る感覚でTHETAをカスタマイズすることもでき、そのカスタマイズ機能を「プラグイン」と呼んでいます（詳細は本記事の末尾を参照）。

今回は、「こんな実験してみたよ」くらいの内容です。
動画はサマにならないけど、静止画としたらなんとか“それっぽく”なったかも。

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">◤Qiita記事公開◢<br>ぐるぐる、ねじねじ、ひらひら、ふにゃふにゃする人々。<br>そんな映像効果の実験途中な記事です！<br>RICOH THETAアドベントカレンダー2020の23日目です。<a href="https://t.co/v6M4H25NmP">https://t.co/v6M4H25NmP</a><a href="https://twitter.com/hashtag/THETA?src=hash&amp;ref_src=twsrc%5Etfw">#THETA</a> <a href="https://twitter.com/hashtag/thetaplugin?src=hash&amp;ref_src=twsrc%5Etfw">#thetaplugin</a> <a href="https://twitter.com/hashtag/SlitScan?src=hash&amp;ref_src=twsrc%5Etfw">#SlitScan</a> <a href="https://t.co/bwO5v9KZ7k">pic.twitter.com/bwO5v9KZ7k</a></p>&mdash; THETAプラグイン開発者コミュニティ (@thetaplugin) <a href="https://twitter.com/thetaplugin/status/1341568621659660289?ref_src=twsrc%5Etfw">December 23, 2020</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

一般カメラで「スリットスキャン」などと呼ばれる映像表現手法をTHETAでできるかな？と思い立って試してみた次第です。

## スリットスキャンとは？

Webで検索すると、わかりやすそうな記事が沢山でてきます。
検索結果の上位にでてくる[こちら](https://www.d-pv.jp/column/post-80/)などは例が豊富でわかりやすいです。

一応、自分なりの説明もしておくと、「時間をずらした線で映像を作る」という手法だと解釈しています。「線」＝スリット、「時間をずらす」＝スキャンというこから、「スリットスキャン」と呼ばれているようです。

皆さんの身近なところで判りやすいものでは<b>コピー機などのスキャナー</b>が、相当します。
「時間のズレ」が極小すぎてわからない例では、実はメカ式シャッター（フォーカルプレーンシャッター）という機構をもつカメラにおいて「シャッター速度がとても高速な時」も実はスリットを動かして映像ができあがっています。さらに、メカ式シャッターを持たないデジタルカメラ（特にスマートフォン）においてもこのような現象は起こっていまして、「ローリングシャッターゆがみ」などといわれていた現象を記憶している方もいらっしゃるかもしれません。電車の窓から動画を撮ると電柱が倒れて映ったり、動画の中を横切った車や電車がナナメに歪んで見える現象です。

このような歪みを積極的に起こしている撮影手法/映像表現手法です。

今回の実験では、スキャンする方向を上→下や下→上としましたが、
横方向にスキャンすると、歩行者が針のように映ったり、猫の胴体が無くなったり伸びたり、長い電車が縮んだり、スキャンをゆっくり行うと[ゴーストタウンエフェクト記事](https://qiita.com/Yuuki_S/items/8a31358d1a5bbbb03136)に近しい映像が撮れることもあります。

デジタルカメラがこんなにも進化する前には、フィルムカメラの光をコピー機用のラインスキャナーでスキャンして高解像度のデジタル映像を撮れるカメラを自作した方、なんていうのもお見掛けしたこともあります。


## THETAでどうやったの

<b>[THETAプラグインでライブプリビューを扱いやすくする](https://qiita.com/KA-2/items/cef05a4960663bd2ba2f)</b>のプロジェクトをベースとして、プラグインが連続フレームをうけとると、「横1本だけ新しい映像として採用し、残りは捨てる」ということをひたすら繰り返しおこなっています。1コマ目だけは全部使いますけれども。

ですので、１画面の更新が遅くなります。画像の解像度が高いほど１コマ分の映像ができあがるのに時間を要します。

1024×512pixel 30fpsのライブプリビューですと映像はこんな感じ。1画面できあがるまでおよそ17秒かかるので、カメラの前で 9～10回転くらいすることも余裕です。

![1024_18fps_横ライン上から下スキャンの場合.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/265455/3a19058a-d8de-fc80-fdbe-185374e15d37.jpeg)

この速度ですと、ドアの出入りを表現することがかなり難しく……
640×320 30fps でやっとこんな感じにできました。

![640x320ならなんとか.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/265455/f6915228-1c05-a875-4a10-55d55f491625.jpeg)

Web検索で出てくるミュージックビデオの例では、かなり高速なラインスキャンをしていることがわかります。

## ソースコード

前述のとおり、[こちらのプロジェクト](https://github.com/theta-skunkworks/theta-plugin-extendedpreview)のMainActivityの中のスレッド部分だけ手を加えています。
解像度やフレームレートを変更するには、WebUIのJavaScriptの固定値を変えたりもしますが、そこは[以前の記事](https://qiita.com/KA-2/items/cef05a4960663bd2ba2f)を調べてください。

ラインスキャンの映像を作っているスレッド部分だけ抜粋します。

```MainActivity.java
    //==============================================================
    // OLED Thread
    //==============================================================
    public void drawOledThread() {
        new Thread(new Runnable() {
            @Override
            public void run() {
                int outFps=0;
                long startTime = System.currentTimeMillis();
                Bitmap beforeBmp = null;

                int imgWidth=0;
                int imgHeight=0;
                int posWidth=0;
                int posHeight=0;
                Bitmap bmpSlitScan=null;

                while (mFinished == false) {

                    byte[] jpegFrame = latestLvFrame;
                    if ( jpegFrame != null ) {

                        //JPEG -> Bitmap
                        Bitmap bitmap = BitmapFactory.decodeByteArray(jpegFrame, 0, jpegFrame.length);

                        if ( bmpSlitScan != null ) {
                            //縦スキャン
                            Bitmap bmpSlit = Bitmap.createBitmap(bitmap, 0, posHeight, imgWidth, 1, null, true);
                            Canvas canvasSlitScan = new Canvas(bmpSlitScan);
                            Paint mPaint = new Paint();
                            canvasSlitScan.drawBitmap(bmpSlit, 0, posHeight, mPaint);

                        } else {
                            imgWidth=bitmap.getWidth();
                            imgHeight=bitmap.getHeight();
                            bmpSlitScan = Bitmap.createBitmap(imgWidth, imgHeight, Bitmap.Config.ARGB_8888);
                            Canvas canvasSlitScan = new Canvas(bmpSlitScan);
                            Paint mPaint = new Paint();
                            canvasSlitScan.drawBitmap(bitmap, 0, 0, mPaint);
                        }

                        /*
                        //縦スキャン位置更新 (上→下)
                        posHeight++;
                        if ( posHeight >= imgHeight ) {
                            posHeight=0;
                        }
                         */
                        //縦スキャン位置更新 (下→上)
                        posHeight--;
                        if ( posHeight < 0 ) {
                            posHeight = imgHeight-1;
                        }

                        //set result image
                        ByteArrayOutputStream baos = new ByteArrayOutputStream();
                        bmpSlitScan.compress(Bitmap.CompressFormat.JPEG, 100, baos);
                        latestFrame_Result = baos.toByteArray();

                        outFps++;
                    } else {
                        try {
                            Thread.sleep(33);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }

                    long curTime = System.currentTimeMillis();
                    long diffTime = curTime - startTime;
                    if (diffTime >= 1000 ) {
                        Log.d(TAG, "[OLED]" + String.valueOf(outFps) + "[fps]" );
                        startTime = curTime;
                        outFps =0;
                    }

                }
            }
        }).start();
    }
```

jpegのフレームが受け取れるので、Android標準の画像処理メソッドを利用しやすくするためにbitmapに形式を変換したあと、Canvas や Paint をつかって1ライン更新し、WebUIに編集後映像を表示するために編集後の映像をjpegにしています。

以下のようにすると横スキャンです。

```MainActivity.java
                            //横スキャン
                            Bitmap bmpSlit = Bitmap.createBitmap(bitmap, posWidth, 0, 1, imgHeight, null, true);
                            Canvas canvasSlitScan = new Canvas(bmpSlitScan);
                            Paint mPaint = new Paint();
                            canvasSlitScan.drawBitmap(bmpSlit, posWidth, 0, mPaint);
```

「上から下」「下から上」のスキャン方向切替は、posHeightの更新の仕方で切り替えています。
横方向スキャンをする場合には、posWidthをずらしていってください。


## まとめ

ひとまずは、ライブプリビューで得られる映像の画質で時間をかければ、スリットスキャンの映像表現ができることが判りました。
それなりに使えるようにするには、解像度を落とさねばならず、、、
まだまだ卵の状態ですね、ストア公開はとうめん難しいなーという感触。
（過去映像をバッファしながらなめらかにする、そうするとNDKで実装して、と、かかなり工夫をせねば。。。）

あと、露出パラメータ（絞り値、シャッター速度、ISO感度）やホワイトバランスを固定しないとという注意点も。この実験ではオートなのでコマ間の差異が線でみえてしまってます。

こんなかんじで、いろいろな実験をくりかえしていきます。
みなさんも、多様なことを試してみてください！

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

THETAプラグインをご存じない方は[こちら](https://github.com/ricohapi/theta-api-specs/blob/main/core/products/theta-plugin.md)をご覧ください。
パートナープログラムへの登録方法は[こちら](https://qiita.com/mShiiina/items/55d98f366e650ca42251)にもまとめてあります。
Qiitaの[RICOH THETAプラグイン開発者コミュニティ TOPページ「About」](https://qiita.com/organizations/theta-plugin)に便利な記事リンク集もあります。
興味を持たれた方は[Twitter](https://twitter.com/thetaplugin)のフォローと[THETAプラグイン開発者コミュニティ(Slack)](https://join.slack.com/t/thetadeveloper/shared_invite/enQtNzcxODI2MTk2ODg0LWY5YmU2ZTIxNGYwNzlmNWZjMWMyMWFlMzY3ZmQ3NGU3ZjZiZjI0Njk4MGIxMDU5NDJiMzQ4Y2FkNDEzYmFlNGU)への参加もよろしくおねがいします｡