魚眼カメラでQRコードを読み込んでみようとした

# はじめに
Paypayや楽天ペイなどの普及でより身近になった感じがあるQRコードですが、ふとした時に「これって魚眼カメラでも読み取れるのかな？」と思ったので、実際にやってみました。

# やったこと
Jatson NanoにインタニヤのラズベリーパイVR220カメラを接続し、スマホ(iPhone XS)画面に表示したQRコードを読み取るプログラムを開発ました。
ソースコードは[Github](https://github.com/noueezy/fisheye_qr)にアップしています。
ざっくりとした仕組みを説明すると、

1. 撮影
1. QRコードの位置を検出
1. QRコード周辺の魚眼歪みの除去
1. QRコード読み取り

という処理の流れになっています。
撮影・検出・歪み除去の様子は下記Youtubeで見れます。(画像クリックでYoutubeへジャンプ)

[![qr_fisheye](http://img.youtube.com/vi/JuxGfsaKdkU/0.jpg)](http://www.youtube.com/watch?v=JuxGfsaKdkU)

以下、仕組みの簡単な説明を記述します。

# 撮影
Jetson NanoでRaspberry Piのカメラを動作させるにはgstreamerを使います。[こちらの記事](https://qiita.com/tsutof/items/2d2248ec098c1b8d3e32)を参考に撮影してみました。

とりあえず何も考えずに撮影した結果はこちら↓
![20210201225322.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/22812/090e622c-2daf-7917-c21f-fc4f82c77bde.jpeg)
上の画像のようにiPhoneの画面が明るすぎてQRコードがまともに写っていませんでした
（※プライバシーを考慮してぼかしを入れています）
露出が合っていないようです。

これは魚眼カメラあるあるだと思うのですが、露出をオートで撮影すると、画角が広すぎて画面内にいろんな明るさのものが映り込んでしまうため、狙ったところが狙った明るさにならないことがあります。特にスマホの画面のような明るさが強いものだと簡単に白飛びしてしまいます。
今回はQRコードを読み取ることが目的なので、gstreamerの露出やゲインを抑えて暗めに撮影してスマホ画面がきれいに撮影できるようにしてみました。
gstreamerの露出設定部分は[ここ](https://github.com/noueezy/fisheye_qr/blob/31def273dad1cc656ab745237d0a5e7b120d1f4a/nano_camera.py#L7)になります。


これでiPhoneに表示したQRコードを撮影するとこのようになります。
明るさを抑えてQRコードのみ浮き上がっている状態で撮影することができました。
![20210211165616_capture.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/22812/a4ab4b61-fe37-7afd-af70-c7b974b5bca5.jpeg)

撮影ができたらすぐさまQRコードを読み取りたいところですが、QRコードがカメラの真正面ならともかく、画角の外側に写るとQRコードの形状が歪んでしまうので読み取りができません。
せっかく魚眼で撮影するからには画角の外側のQRコードも読み取りたいところです。
今回はQRコードの箇所を狙い撃ちで歪み除去したいと思います。

# QRコードの位置を検出
QRコードの歪みを取るために、まず画像中のどこにQRコードが写っているかを検出する必要があります。
機械学習などで検出する方法を考えましたがデータを集めるのが面倒だったので、ルールベースで検出します。

今回は幸か不幸か撮影画像は画面（QRコード）のみが明るく、他の部分は暗い画像になっています。
この画像の特徴を使って、次のようにQRコードの画像上の位置を検出することにしました。
1. 2値化処理で明るい部分とそれ以外を分ける
2. 明るい部分の輪郭を検出
3. 検出した輪郭から重心の位置を算出
この部分のコードは[これ](https://github.com/noueezy/fisheye_qr/blob/main/centroid.py)になります。

QRコードの画像上の位置を求めたら、カメラに対してどの方向にそれがあるかを算出します。
なお、この処理にはOpenCVのomnidirモジュールのキャリブレーションパラメータを使用するので、予めパラメータを算出しておく必要があります。
キャリブレーションについては[過去の記事](https://qiita.com/noueezy/items/4d719ba9935b3a989315)をご参照ください。
OpenCVのomnidirモジュールのカメラモデルでは、魚眼の撮影画像を「カメラ中心を原点とした半径1の天球（単位球）の球面に投影したもの」として扱うことができます。
したがって、画像上の重心の座標(u,v)を天球面の座標(x,y,z)に変換し、さらにそれを極座標(r,θ,φ)に変換すると、カメラに対するQRコードの方向が算出できます。
重心を単位球面の座標に変換するコードは[ここ](https://github.com/noueezy/fisheye_qr/blob/31def273dad1cc656ab745237d0a5e7b120d1f4a/unit_sphere.py#L35)、球面の座標を極座標に変換するコードは[ここ](https://github.com/noueezy/fisheye_qr/blob/31def273dad1cc656ab745237d0a5e7b120d1f4a/fisheye_qr.py#L40)になります。

# QRコード周辺の魚眼歪みの除去
算出した極座標の角度成分θ・φから回転行列を算出し、キャリブレーションパラメータとともにOpenCVのomnidir::UndistortImageへ渡すと、歪みが除去された画像を取得できます。

歪み除去の範囲
![20210211165616_crop.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/22812/e001747e-ad01-15a2-ca42-29cb6d94d23d.jpeg)

歪み除去済みの画像
![20210211165616_undistort.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/22812/d8321879-e7c7-3f27-8c2b-c0ebb46c6370.jpeg)

歪み除去の部分のコードは[ここ](https://github.com/noueezy/fisheye_qr/blob/31def273dad1cc656ab745237d0a5e7b120d1f4a/fisheye_qr.py#L46)になります。

# QRコード読み取り
QRコードの歪みが取れたら、QR読み取りモジュールに画像を渡して、QRコードから情報を取得します。
今回は[pyzbarを使ってQRコードを読み取っています](https://github.com/noueezy/fisheye_qr/blob/31def273dad1cc656ab745237d0a5e7b120d1f4a/fisheye_qr.py#L49)。

撮影から読み取りまでの手順は以上です。

# 終わりに
魚眼カメラでQRコードを読み込んでみました。
あえてQRコードの読み取りに魚眼カメラを使う利点はなさそうですが(汗)、処理の手順の一つ一つは魚眼カメラを使うアプリケーションの開発に役に立ちそうな要素を含んでいると思います。