opencvを用いて二枚の画像を合成し画角の大きな画像を生成する

これは岩手県立大学アドベントカレンダー22日目の記事です。

# はじめに
　岩手県立大学ソフトウェア情報学研究科修士2年のnyagato_00です．自分の研究の中で，複数台のカメラを用いて撮影範囲を拡張する必要があり，もっとも簡単な２台のカメラから撮影した画像を用いて画像合成を行う方法について解説します．
　自分自身はC++を使う上で便利なオープンソースフレームワークであるopenframeworksを利用してコードを書いています．ベースがC++であるため，豊富なライブラリを使えるだけでなく，アドオンを追加してインクルードするだけでより拡張性の高いソフトの開発が行えます．
　
# OpenCVの単独での利用ではなく，openframeworksを用いて利用するのか
　OpenCVは，こちらもC/C++ Java, Python，MATLABなどで利用することが出来ます．すなわち，openframeworksをわざわざ利用しなくてもC++のネイティブなコードでも記述することが可能です．しかしながら，OpenCVのGUIは少し使いづらくC++には他の便利なGUIライブラリがあります．これらを複合的に利用する際に便利なフレームワークがopenframeworksであるため，私はこの環境を利用しています．
　※所属講座がこのフレームワークを使うことを推奨しているため他の方とのやり取りが楽であるという理由もあります．

# カメラの画角
　カメラの画角とは，一般的にはレンズに対する焦点距離によって決まります．また，レンズが広角レンズであるか望遠レンズであるかによっても変わります．単純に撮影範囲を拡張するだけであれば広角レンズを用いることで解決します．しかりながら，広角レンズを利用する場合は，画像に歪みが発生してしまいます．適切なレンズキャリブレーションを行うことである程度の歪みの補正を行うことは可能ですが，限界はあります．私も広範囲を撮影する必要があるのですが，この歪みが発生してしまうと画像上から算出した特徴点の座標が実際の座標よりもずれてしまい誤差の原因となります．そこで画像の歪みが小さい対角線画角60°のレンズを用いるカメラを2台並行に並べ撮影を行い，2台のカメラからの画像を合成し画角の大きな画像を作成します．

# 注意事項
 OpenCV2.4では，SURFやSIFTなどの利用を制限しており，OpenCVを単純にインクルードするたけでは利用できない．

```ofApp.h
#include "ofMain.h"
#include "ofxCv.h"
#include "ofxOpenCv.h"
#include <opencv2/nonfree/features2d.hpp> // ← 手動でnonfreeをインクルード
#include <opencv2/legacy/legacy.hpp>      // ← 手動でlegacyをインクルード
```

# 2枚の画像の特徴量の算出
　画像の特徴量を算出方法には幾つかの種類があるが，今回はSURF(Speeded Up Robust Features)を用いて特徴量の算出を行う．
　SURFはSIFTに比べ高速であり，特徴量の算出精度もSIFTより少し劣る程度であるため，採用した．
 こちらの2枚の画像から画像の合成を行う．
![5.jpg](https://qiita-image-store.s3.amazonaws.com/0/50627/787dd8ef-cc41-6974-c6f8-883f4254b5f6.jpeg)

![6.jpg](https://qiita-image-store.s3.amazonaws.com/0/50627/5cb32630-0f95-594f-cbe6-39189a56d063.jpeg)

```ofApp.cpp
// SURT特徴器を使う場合
cv::SurfFeatureDetector surf;
// SURTの特徴量を計算するためのディスクリプター
cv::SurfDescriptorExtractor surfDesc;

//画像毎のkey pointsを格納するための配列
std::vector<cv::KeyPoint> keyPoints[2];
//画像毎の特徴量を格納するための配列
cv::Mat descriptors[2];

// それぞれの入力画像に対してdetect関数を用いてkey pointsを算出
surf.detect(gray1, keyPoints[0]);
surf.detect(gray2, keyPoints[1]);

// それぞれの入力画像に対してcompute関数を用いて特徴量を算出
surfDesc.compute(gray1, keyPoints[0], descriptors[0]);
surfDesc.compute(gray2, keyPoints[1], descriptors[1]);

```

前述の通り，入力画像からそれぞれの特徴量が算出できる．2枚の画像に対して同一の特徴量を求め，双方の画像から似ているところ算出する．

```ofApp.cpp
// 2枚の画像からマッチングする特徴点を格納する配列
std::vector<cv::DMatch> matches;
// 1枚目の画像の各特徴点に対して，2枚目の画像から最も近い特徴点を見つける
cv::BruteForceMatcher<cv::L2<float>> matcher;
matcher.match(descriptors[0], descriptors[1], matches);

// 2枚目の画像に対してマッチングした結果を入力
std::vector<cv::Vec2f> getPoints1(matches.size());
// 1枚目の画像に対してマッチングした結果を入力
std::vector<cv::Vec2f> getPoints2(matches.size());
    
for (size_t i = 0; i < matches.size(); i++) {
    getPoints1[i][0] = keyPoints[0][matches[i].queryIdx].pt.x;
    getPoints1[i][1] = keyPoints[0][matches[i].queryIdx].pt.y;
        
    getPoints2[i][0] = keyPoints[1][matches[i].trainIdx].pt.x;
    getPoints2[i][1] = keyPoints[1][matches[i].trainIdx].pt.y;
        
}
```

2枚の画像から得られたマッチングした特徴点を画像に描画する

```ofApp.cpp
// マッチング結果を描画するためのMat型変数を定義
cv::Mat matchedImg;

// drawMatches関数を用いて入力画像に対するマッチングした結果を描画
cv::drawMatches(CamMat1, keyPoints[0], CamMat2, keyPoints[1], matches, matchedImg);
```

![matchedImg_r_of.png](https://qiita-image-store.s3.amazonaws.com/0/50627/846ed97e-a4cd-d810-045d-38e011faf3b3.png)
このようにkey pointsは円で，2つのkey pointを結ぶ線分を画像に重ねて描画することができる．

# ホモグラフィ(射影変換)を用いて画像の合成
　それぞれの画像の特徴点を行列として変数に格納している．2つの行列を用いて射影変換行列(ホモグラフィ行列)を算出する．

```ofApp.cpp
// findHomography関数を用いて射影変換行列を算出する．
cv::Mat homographyImg = cv::findHomography(getPoints1, getPoints2, CV_RANSAC);

//  画像に対して透視変換を行う
cv::warpPerspective(CamMat1, result, homographyImg, cv::Size(static_cast<int>(CamMat1.cols * 1.5), static_cast<int>(CamMat1.rows * 1.1)));

// 2枚目の画像の画素をatメソッドを用いてresultへ代入
for (int y = 0; y < CamMat2.rows; y++){
    for (int x = 0; x < CamMat2.cols; x++){
        result.at<cv::Vec3b>(y, x) = CamMat2.at<cv::Vec3b>(y, x);
    }
}
```

![result_of.png](https://qiita-image-store.s3.amazonaws.com/0/50627/db1432d2-9670-273c-b1c3-6dd4ce0938ca.png)
このように，2枚の画像を合成しパノラマな画像を生成することができる．

今回はもっとも簡単な画像の合成を行ったが，動画像で処理を行うためにはもう少しコードを改修する必要がる．

以下に全コードを示す．冗長な箇所があると思いますのでコメント頂けると幸いです．
また，変数間のやり取りをポインタを利用することでより高速化出来ると思います．

```ofApp.h
#pragma once

#include "ofMain.h"
#include "ofxCv.h"
#include "ofxOpenCv.h"
#include <opencv2/nonfree/features2d.hpp>
#include <opencv2/legacy/legacy.hpp>

class ofApp : public ofBaseApp{

	public:
		void setup();
		void update();
		void draw();

		void keyPressed(int key);
		void keyReleased(int key);
		void mouseMoved(int x, int y );
		void mouseDragged(int x, int y, int button);
		void mousePressed(int x, int y, int button);
		void mouseReleased(int x, int y, int button);
		void mouseEntered(int x, int y);
		void mouseExited(int x, int y);
		void windowResized(int w, int h);
		void dragEvent(ofDragInfo dragInfo);
		void gotMessage(ofMessage msg);
		
   
                ofVideoGrabber  Cam1;
                ofVideoGrabber  Cam2;
                ofImage         img1;
                ofImage         img2;
                cv::Mat         CamMat1,
                                CamMat2;
                cv::Mat         matchedImg_r;
                cv::Mat         result;
    
};
```

```ofApp.cpp
#include "ofApp.h"

//--------------------------------------------------------------
void ofApp::setup(){
    img1.load("6.jpg");
    img2.load("5.jpg");
    
    CamMat2 = ofxCv::toCv(img2);
    CamMat1 = ofxCv::toCv(img1);
    cv::Mat gray1, gray2;
    
    cv::cvtColor(CamMat1, gray1, CV_BGR2GRAY);
    cv::cvtColor(CamMat2, gray2, CV_BGR2GRAY);
    
    
    cv::SurfFeatureDetector surf;
    cv::SurfDescriptorExtractor surfDesc;
    
    std::vector<cv::KeyPoint> keyPoints[2];
    cv::Mat descriptors[2];
    
    surf.detect(gray1, keyPoints[0]);
    surf.detect(gray2, keyPoints[1]);
    
    surfDesc.compute(gray1, keyPoints[0], descriptors[0]);
    surfDesc.compute(gray2, keyPoints[1], descriptors[1]);
    
    std::vector<cv::DMatch> matches;
    cv::BruteForceMatcher<cv::L2<float>> matcher;
    matcher.match(descriptors[0], descriptors[1], matches);
    
    std::vector<cv::Vec2f> getPoints1(matches.size());
    std::vector<cv::Vec2f> getPoints2(matches.size());
    
    for (size_t i = 0; i < matches.size(); i++) {
        getPoints1[i][0] = keyPoints[0][matches[i].queryIdx].pt.x;
        getPoints1[i][1] = keyPoints[0][matches[i].queryIdx].pt.y;
        
        getPoints2[i][0] = keyPoints[1][matches[i].trainIdx].pt.x;
        getPoints2[i][1] = keyPoints[1][matches[i].trainIdx].pt.y;
        
    }
    
    cv::Mat matchedImg;
    cv::drawMatches(CamMat1, keyPoints[0], CamMat2, keyPoints[1], matches, matchedImg);
    
    cv::Mat homographyImg = cv::findHomography(getPoints1, getPoints2, CV_RANSAC);

    
    cv::warpPerspective(CamMat1, result, homographyImg, cv::Size(static_cast<int>(CamMat1.cols * 1.5), static_cast<int>(CamMat1.rows * 1.1)));
    for (int y = 0; y < CamMat2.rows; y++){
        for (int x = 0; x < CamMat2.cols; x++){
            result.at<cv::Vec3b>(y, x) = CamMat2.at<cv::Vec3b>(y, x);
        }
    }
}

//--------------------------------------------------------------
void ofApp::update(){
    // 今回は利用しない
}

//--------------------------------------------------------------
void ofApp::draw(){
    ofxCv::drawMat(CamMat1, 0, 0, CamMat1.cols/2, CamMat1.rows/2);
    ofxCv::drawMat(CamMat2, CamMat1.cols/2, 0, CamMat2.cols/2, CamMat2.rows/2);
    ofxCv::drawMat(matchedImg_r, 0, CamMat1.rows/2);
}

// 以下省略
```


明日の@fkskさんに繋ぎます．