OpenCV Stitching Module によるパノラマ画像合成の解説【ざっくり】

みなさんはパノラマ写真というものを撮ったことはあるでしょうか？最近のスマートフォンであれば、カメラアプリにほぼ間違いなくパノラマ撮影モードなる機能が搭載されており、誰でも一度くらいはやったことがあると思います。本体をゆっくり回転させながら撮影すると 180 度視界が開けたような横長の写真がとれるアレです。
![0_pano.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/631252/08670eba-460d-1e9e-dd08-21a64d6939a3.jpeg)
今ではすかっりポップになったパノラマ画像合成ですが、その裏側にはコンピュータビジョン分野の様々な話題がつまっています。多方面の研究の成果がまとまってひとつのアプリケーションとして結実したという感じで、この技術の詳細を知ることは駆け出しのグラフィックス技術者にとって非常に有用であると考えています。

そんなわけでこれからパノラマ画像合成アルゴリズムについて、今さらながらまとめていきたいと思います。こちらの技術自体は古いものですが、古き良きを知ることもエンジニアにとって重要です。最新の技術を身につけるためのベースにもなることでしょう。参考になるコードとして、OpenCV の stitching というモジュールが全部入りで非常にまとまっているので、こちらのコードリーディングをしながら進めて参ります。

とはいえ著者は専門家ではないので、わからないことはわからないと言います。間違っているところもあると思います。初心者の方はだまされないように常に疑いの目を持ちつつ読んでください。中級者の方はもっと格式の高い文献を当たってください。上級者の方はマサカリを構えながら読み、間違っているところを指摘していただけると助かります。

## OpenCV stitching モジュールについて

### 基本的な機能
stitch というのは「縫う」という意味で、バラバラの画像を縫い合わせてひとつの大きな画像に仕上げるというイメージです。ですので入力は下記のような複数の画像ファイルになります。
![0_input.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/631252/671a7e0e-06f1-5ee1-1c1e-c44ec00ce9bc.png)
大前提として、ひとつひとつの画像は景色が少しずつ重なるように撮影しなくてはなりません。画像の縫い合わせをやるわけなので、画像の重なった部分が手がかりとなるわけです。出力結果は下記のようになります。
![0_out.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/631252/54aaee29-46e0-289e-4556-f07eb7da0ed9.png)
### モードと制約
stitching モジュールには下記の２つのモードがあり、撮影の仕方によって適したモードを選ぶ必要があります。

|モード|説明|
|---|---|
|パノラマモード|遠くの景色のような画像の合成に向いています。<br>（球面投影）|
|スキャンモード|コピー機でスキャンしたような画像の合成に向いています。<br>（アフィン投影）|

今回はデフォルトのモードであるパノラマモードのみ解説を行います。どちらのモードであっても基本的なアルゴリズムの流れは同じです。どちらかといえばパノラマモードの方が難しいので、こちらが理解できればスキャンモードの方もスッと理解できるでしょう。  

ちなみに OpenCV の stitching モジュールは、どんな画像でも完璧に合成してくれるわけではありません。モードによって撮影方法にある程度の制約があり、それを守って正しいマナーで撮影しないと上手く合成できないのです。公式のドキュメントには、この辺の撮影方法に関する制約みたいな情報は見当たらないのですが、内部のアルゴリズムを理解していると上手く合成するための撮影方法がわかるというわけです。

後で詳しく解説しますが、パノラマモードでは下記のように撮影した画像を球面上に張り付けるような座標変換を行うため、これがそのまま最適な撮影条件となります。
![0_sphereMap.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/631252/c505f8a1-faaa-650c-c9e4-70627d3fb735.png)
撮影者は球の中心にいるイメージで、そこから一歩も動いてはならず、カメラの向きだけを変えて撮影します。そんな条件で撮影された画像であれば、上手く球面上にマッピングできて、良い感じに画像同士がつながります。というわけで撮影時の制約は下記のようになります。

- その場から動かずに、カメラを構える角度だけ変えて撮影する。
- カメラを横にスライドさせて動かすのではなく、体を軸として向きを変える。
- 近くのモノではなく、なるべく遠くの景色を撮る。


## コードリーディング

### コードリーディングに必要な知識

本気でコードを読み解こうとすると下記のような知識が前提として必要になりますが、ざっくり図を眺めるだけでも何かしら分かった気になると思います。必須ではありませんので気楽に読んでください。

- C++ がまあまあ読める
- 画像処理の基本的な知識がある
- OpenCV の基本的な使い方がわかる
- 高校レベルからちょっとはみ出た数学（線形代数の初歩的なところ）がわかる

### 全体の流れ

まずはざっくり全体の流れを示して、処理のイメージをつかんでもらいましょう。が、その前にモジュールの使い方です。stitching モジュール自体の使い方はかなりシンプルで、下記のように入力する画像を読み込んで配列にして、stitch() という関数に突っ込んで終わりです。これだけでパノラマ画像が生成できます。

``` C++
#include "opencv2/opencv.hpp"
#include "opencv2/stitching.hpp"
#include <vector>

int main() 
{
    using namespace cv;

    char* imageList[] = {
        "image0.jpg",
        "image1.jpg",
        "image2.jpg",
        "image3.jpg",
        "image4.jpg",        
    };

    std::vector<Mat> imgArray;
    for (int i = 0; i < 5; ++i) {
        Mat img = imread(imageList[i], IMREAD_UNCHANGED);
        imgArray.push_back(img);
    }

    Mat pano;
    Stitcher::Mode mode = Stitcher::PANORAMA;
    Ptr<cv::Stitcher> stitcher = Stitcher::create(mode);
    stitcher->stitch(imgArray, pano);
    imwrite("Panorama.png", pano);

    return EXIT_SUCCESS;
}
```
利用するだけならものすごく簡単なのですが、この stitch() 関数の裏側では実に様々なことが行われています。さっと中身を見てみると大まかには下記のような流れになっております。


``` C++
Stitcher::stitch()
 |  
 |- Stitcher::estimateTransform()
 |   |
 |   |- Stitcher::matchImages()
 |   | 
 |   |- Stitcher::estimateCameraParams()
 |
 |- Stitcher::composePanaoram()
```

大きく二つのパートに分かれており、前半は estimateTransform() を、後半は composePanaoram() を実行しています。前半パートはそこからさらに Stitcher::matchImages() と estimateCameraParams() に分割されます。関数名からどんなことをやっているかが軽く察しは付きますね。こちらの記事では上記の処理の流れをもうちょっとだけ細かく分解してざっと概要を説明し、各工程で行っている細かい内容は小分けにして別の記事にまとめていきたいと思います。

### matchImages()　～ 画像のマッチング ～ 

入力となる画像達は配列として渡ってきますが、画像の順番はバラバラで完全にランダムなため、どれとどれが重なった領域を持つのか全くわかっていません。人間の目には明らかでもコンピュータに渡る情報はただの数字の羅列にすぎないのです。というわけで、まずはつながっている画像のペアを探るところからはじまります。

最初に画像の中から特徴点を抽出していきます。「特徴点」とは読んで字のごとく特徴を持った点で、他の点と区別できそうな点（ピクセル）のことを指します。
![1_detectFeatures.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/631252/2f6f8ab2-fc86-323d-2b55-e292bf0588c4.png)
続いては各画像で検出された特徴点を見比べていきます。もし、ある画像に写った特徴点が別の画像にも写り込んでいたら、それらの画像は隣り合っているとみなすことが出来ます。逆に特徴点を見比べても一致するものがなければ、それらの画像は離れて撮影されたものと考えることが出来ます。こんな感じで重なりを持つ画像のペアを探していきます。
![2_inlierOnly.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/631252/d5f9f2de-7559-db5a-987a-4a848256905e.png)
とりあえずまずは純粋な特徴点の比較によって画像ペアを見つけ出しますが、アルゴリズム的な限界もあって、最初の段階では結構な数のペアが間違って同じ点だとみなされてしまいます。というわけで、プラスアルファの情報としてカメラの幾何的な関係性も利用することで誤った対応を取り除き、より精度の高いマッチングを実現します。
![2_sphereMap.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/631252/96d3f5b3-0596-f1a9-0e5c-222552d5f86d.png)

### estimateCameraParams()　～ カメラパラメータ推定 ～

matchImages() では、入力された画像のうち、どれとどれが隣り合った画像であるかがわかりましたが、そこまで分かると画像間の幾何的な関係性も計算できます。二つの画像間の特徴点の移動を比べれば、撮影した時に「カメラをどれだけ回したか」という回転 R の情報が分かってしまうのです。
![3_rotate.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/631252/07fca75d-14b0-33f5-e5b4-5cfb8aa7cbc2.png)
またマッチングした全ての画像ペアを列挙すると、下図のようなグラフ構造が出来上がります。
![3_graph.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/631252/4098213f-d402-4403-1cba-2e398e4ff003.png)
ここで、どこか基準となる画像をひとつ決めてグラフのエッジを断ち切れば、ツリー構造へと変換できます。
![3_tree.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/631252/0b13c2b8-0a84-62f7-d1f9-164ebc1e23a5.png)
さらにツリーのルートからエッジをたどるように回転させていけば、各画像のグローバルな回転を求めることができます。
![3_globalRotation.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/631252/d59a065b-c4e0-7418-25bc-7970d45d638d.png)
こうやって求めた回転には少しずつ誤差が含まれるものなので、バンドル調整という手法で誤差を調整します。

### composePanaoram()　～ パノラマ画像合成 ～

全ての画像がどんなアングルで撮影されたか（ カメラの回転 R ）が分かったので、いよいよ画像をつなぎ合わせていきます。まずは画像をひとつひとつ球面に張り付けていくことで、合成された結果の画像がどれくらいの大きさになるかがわかります。同時に入力した画像が最終的な出力画像のどの部分を担うのかも計算されます。
![5_mapping.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/631252/2652c8b4-b168-9050-4330-adf578f5e8d5.png)
つづいて画像同士が重なっている部分に着目し、画像を上手くつなぎ合わせるための縫い目を探します。グラフカットという手法で、２つの画像が自然につながる縫い目を探します。
![7_seam.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/631252/bbae3ac5-dd9c-a663-0609-0d1e35df08dd.png)
最後に画像をブレンドします。縫い目を境界線にして、入力画像のピクセルの値をそのまま出力画像の座標変換した位置にコピーすればとりあえずは完成します。実際の stitch モジュールでは単純なコピーではなく、マルチバンドブレンドと呼ばれる少し賢い手法で実装されています。ざっくり言うと画像の周波数ごとに重みづけを行った値を足し合わせるという方法です。
![8_blend.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/631252/1ca9a31c-8e60-b296-91a0-827a08f235ef.png)
以上がざっくりとしたパノラマ画像合成の流れです。自然に画像が合成されるように様々な工夫がなされているわけです。

## 本編の目次

本編の内容は下記のとおりです。

- 特徴点を抽出する
https://qiita.com/itoshogo3/items/0b0869ae3a119964f036

- 隣り合う画像のペアを見つける
https://qiita.com/itoshogo3/items/fa65ff1b59f0cffda26c

- カメラの回転を計算する
https://qiita.com/itoshogo3/items/5fd652b72b8c424f076b

- 誤差を調整する
https://qiita.com/itoshogo3/items/ef86fa8db4d4054139af

- 球面にマッピングする
https://qiita.com/itoshogo3/items/61848b8accd35f478344

- 露出を補正する（予定）

- 縫い目を探す
https://qiita.com/itoshogo3/items/8a1fb067ac4a3ac28a3f

- 画像をブレンドする
https://qiita.com/itoshogo3/items/35c6b91dbbf1d939f8d3

## おまけ

本編で深く扱わなかったトピックについて、さらに突っ込んでお話しするかもしれません。

- Stitch モジュールで使われる OpenCV 基本画像処理機能（予定）

- 回転行列の話（予定）

- レベンバーグマーカート法の話（予定）

# 参考リンク一覧

- OpenCV 本家
https://opencv.org/

- チュートリアル
https://docs.opencv.org/master/d8/d19/tutorial_stitcher.html

- Images stitching 詳細説明
https://docs.opencv.org/master/d1/d46/group__stitching.html

- stitching パイプライン
http://matthewalunbrown.com/papers/ijcv2007.pdf

- カメラモデル
http://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.217.3639&rep=rep1&type=pdf

- グラフカット
https://www.csd.uwo.ca/~yboykov/Papers/pami04.pdf

- グラフカットのコスト関数
https://dl.acm.org/doi/pdf/10.1145/882262.882264