画像処理でARマーカーだけを検出したかった

この記事は <a herf="https://qiita.com/advent-calendar/2017/ipu">岩手県立大学 Advent Calendar 2017</a> の23日目の記事です。

#はじめに
<a herf="https://qiita.com/KentaKato/items/6fc794da376a497a005f">9日目の記事</a>と合わせて（？）XRシリーズの第二弾です．
XRを簡単に説明すると，ARやVR，MR，SRと◯Rという言葉がいくつか世の中に出てきたので，総称が必要なのではないか？ということでXR（クロスリアリティ）という言葉が出てきたそうです．
前回がVRの初歩的な実装で，今回はARの初歩的な実装です．
私が普段よく利用しているopenFrameworksをベースに，OpenCVを用いた画像処理でARマーカーの検出まで行っていきます．

が，しかし，タイトルを見て察していただけるように，検出できていません．
正しくはARマーカーだけを正確に検出することができませんでした．
大学4年生は卒論末期で忙しいのですとできなかったことの言い訳をさせてもらいます．

#ARマーカー検出までの流れ
ARマーカー検出までの基本的な流れは<a herf="http://im-lab.net/artoolkit-overview/">こちらのサイト</a>を参考にさせていただきました．
ところどころ自分の知っている知識で保管しているので，細部が異なりますがご容赦ください．

本記事でのARマーカー検出までの流れをざっくり書くと
元画像のグレースケール化→2値化→輪郭抽出→輪郭の近似→四角形ぽい領域を抽出（今回はここまで）→検出したいマーカーと一致するかどうか→一致したものを表示
を想定していました．

#開発環境
MacBook Pro macOS Sierra
openFrameworks 0.9.8

#プログラム
今回addonsとして<a herf="https://github.com/kylemcdonald/ofxCv">ofxCv</a>とofxOpenCvを使用しています．
addonsとはopenFrameworksの拡張機能とでも言えばわかりやすいでしょうか？
以下プログラムです．

```c++:ofApp.h
#pragma once

#include "ofMain.h"
#include "ofxCv.h"
#include "ofxOpenCv.h"

class ofApp : public ofBaseApp{

	public:
		void setup();
		void update();
		void draw();
		
    ofVideoGrabber grabber;
    ofImage markerImage;
    cv::Mat srcImg, srcMarker, mapImg;
    cv::Point2f dstPoint[4];
};
```
```c++:ofApp.cpp
#include "ofApp.h"

//--------------------------------------------------------------
void ofApp::setup(){
    grabber.setDeviceID(0);
    grabber.initGrabber(640, 480);
    
    markerImage.load("markerImage.png");
    srcMarker = ofxCv::toCv(markerImage);
    
    dstPoint[0] = cv::Point2f(0, 0);
    dstPoint[1] = cv::Point2f(srcMarker.cols, 0);
    dstPoint[2] = cv::Point2f(srcMarker.cols, srcMarker.rows);
    dstPoint[3] = cv::Point2f(0, srcMarker.rows);
}

//--------------------------------------------------------------
void ofApp::update(){
    grabber.update();
    
    if(grabber.isFrameNew()){
        cv::Mat processImg;
        srcImg = ofxCv::toCv(grabber).clone();
        processImg = srcImg.clone();
        
        // カメラからの入力画像をグレースケール化します．
        cv::Mat grayImg;
        cv::cvtColor(processImg, grayImg, CV_BGR2GRAY);
        
        // 大津の2値化手法を使ってグレースケール画像を2値化します．
        cv::Mat thresholdImg;
        cv::threshold(grayImg, thresholdImg, 0, 255, cv::THRESH_OTSU);
        
        // 2値化画像から輪郭を抽出します．
        std::vector<std::vector<cv::Point>> contours;
        cv::findContours(thresholdImg, contours, CV_RETR_TREE, CV_CHAIN_APPROX_TC89_KCOS);
        
        for(auto contour = contours.begin(); contour != contours.end(); contour++){
            // 見つかった輪郭を直線近似（正確な言葉ではないですが）を施します．
            std::vector<cv::Point> approxcurve;
            cv::approxPolyDP(cv::Mat(*contour), approxcurve, 5, true);
            double area = cv::contourArea(approxcurve);
            
            // 直線近似を施した輪郭のうち，輪郭線が4つ（四角形の輪郭）かつ領域が一定以上のものをマーカーの候補とします．
            if(approxcurve.size() == 4 && 2000 < area){
                cv::Point2f srcPoint[] = {approxcurve[0], approxcurve[1], approxcurve[2], approxcurve[3]};
                
                // マーカー候補の領域に対し，検出したいマーカー画像のサイズで透視変換を行います．
                cv::Mat map = cv::getPerspectiveTransform(srcPoint, dstPoint);
                cv::warpPerspective(srcImg, mapImg, map, srcMarker.size());
                cv::Mat candidateImg = mapImg.clone();
                cv::cvtColor(mapImg, mapImg, CV_BGR2GRAY);
                
                cv::polylines(srcImg, approxcurve, true, cv::Scalar(0 ,255, 0), 2);
                cv::circle(srcImg, approxcurve[0], 10, cv::Scalar(255, 0, 0));
                cv::circle(srcImg, approxcurve[1], 10, cv::Scalar(0, 255, 0));
                cv::circle(srcImg, approxcurve[2], 10, cv::Scalar(0, 0, 255));
            }
        }
    }
}

//--------------------------------------------------------------
void ofApp::draw(){
    ofxCv::drawMat(srcImg, 0, 0);
        ofxCv::drawMat(mapImg, srcImg.cols, 0);
}
```
#実行結果
左上がカメラ映像からAEマーカー候補の領域に対し線を引いたものです．
カメラ映像の右上隣りにある小さな画像はARマーカー候補の領域の透視変換後の画像になります．
　　
<img width="1280" alt="スクリーンショット 2017-12-23 14.33.10.png" src="https://qiita-image-store.s3.amazonaws.com/0/153217/89c51d9f-d7d3-a78d-9a3d-b469024953fb.png">

<img width="1280" alt="スクリーンショット 2017-12-23 14.33.16.png" src="https://qiita-image-store.s3.amazonaws.com/0/153217/48df79a1-0e94-ffe8-430d-895d3b4845bd.png">

<img width="1280" alt="スクリーンショット 2017-12-23 14.33.22.png" src="https://qiita-image-store.s3.amazonaws.com/0/153217/6fd9f23a-8948-31ab-1c7d-7bb5293dd033.png">

<img width="1280" alt="スクリーンショット 2017-12-23 14.33.35.png" src="https://qiita-image-store.s3.amazonaws.com/0/153217/f3929281-9761-3be2-f6e5-fb969eb9751e.png">
　　
一応ARマーカー候補の領域を検出することはできました．

#なぜARマーカー候補なのか
ARマーカー検出までの流れでも説明した通り，本来ならこのあとに検出したいARマーカーと何かしらの手法を使って一致するかどうかの処理を施す必要があります．
比較手法として，回転した画像同士でも類似度を算出できるmatchShapesを使おうとしましたが，いまいちうまく動きませんでした．
そのため，一見確かにARマーカーが検出できているように思えますが，複数マーカーを同時に検出したときに，それぞれの座標や，ARマーカーごとに特定のオブジェクトを表示するといったことができません．
また，手動で設定しているパラメータがいくつかあるため，ARマーカー以外をARマーカー候補として検出してしまうことがあります．

#おわりに
今回はARマーカー候補の検出までとなってしまいました．
当初の予定ではARマーカーの距離まで出すつもりでしたので，今後も時間を見つけて少しずつ今回のプログラムを改良していきたいと思います．
ARマーカーとして認識できたタイミングと距離計算をできたタイミングでXRシリーズの第3弾，第4弾として記事にできたらなあと思います．

明日のアドベントカレンダーは<a herf = "https://qiita.com/c-ardinal">c-ardinal</a>くんです．

#参考にしたサイト
tetro
http://tenteroring.luna.ddns.vc/tenteroring/tag/%E7%9F%A9%E5%BD%A2%E6%A4%9C%E5%87%BA