charucoキャリブレーションボード

# はじめに
OpenCVでカメラキャリブレーションをする場合、ターゲットとして[チェスボード画像](https://github.com/opencv/opencv/blob/master/doc/pattern.png)や[サークルグリッド画像](https://github.com/opencv/opencv/blob/master/doc/acircles_pattern.png)を使う場合が多いと思います。
キャリブレーションについて調査中、[チェスボード画像の白いマスの部分に何かQRコード風の模様が付いているものを見かけることがありました。](https://docs.opencv.org/4.1.2/charucodefinition.png)
調べてみると、Opencv contribのARマーカー検出等のモジュールであるarucoモジュールの中の機能の一つ、charucoモジュールとのことだったので、試しに使ってみることにしました。

# 参考
ソースコードなどはOpenCVのリファレンスを参考にしています。
https://docs.opencv.org/4.1.2/df/d4a/tutorial_charuco_detection.html

# ボード画像の作成
まずはじめに、キャリブレーションに使うボードの画像を作る必要がありますが、その画像を作成する機能もcharucoモジュールに含まれています。
次のコードで、キャリブレーション画像を作成できます。

```cpp:main.cpp
#include <opencv2/opencv.hpp>
#include <opencv2/aruco/charuco.hpp>

int main(int argc, const char * argv[]) {
    //マーカ辞書作成 6x6マスのマーカを250種類生成
    cv::Ptr<cv::aruco::Dictionary> dictionary = cv::aruco::getPredefinedDictionary(cv::aruco::DICT_6X6_250);
    
    //charucoボード生成 10x7マスのチェスボード、グリッドのサイズ0.04f、グリッド内マーカのサイズ0.02f
    cv::Ptr<cv::aruco::CharucoBoard> board = cv::aruco::CharucoBoard::create(10, 7, 0.04f, 0.02f, dictionary);
    cv::Mat boardImage;
    board->draw(cv::Size(1920, 1080), boardImage, 10, 1);
    cv::imwrite("BoardImage.jpg", boardImage);
}
```

↓のような画像が出力できます。
![BoardImage.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/22812/7f1c78e3-d1eb-4616-ac1c-a2288171af66.jpeg)

この画像を紙などに印刷してキャリブレーション用の撮影を行います。
今回は面倒だったのでノートPCにモニターに写した状態で撮影してみます。

# 検出
カメラはiPhone XSのカメラを使いました。

検出のソースコードは次のとおりです。

```cpp:main.cpp
#include <iostream>
#include <opencv2/opencv.hpp>
#include <opencv2/aruco/charuco.hpp>

int detectCharucoBoard(std::string srcPath, std::string dstPath)
{
    cv::Mat image = cv::imread(srcPath);
    cv::Mat imageCopy = image.clone();
    
    //マーカ辞書作成 6x6マスのマーカを250種類生成
    cv::Ptr<cv::aruco::Dictionary> dictionary = cv::aruco::getPredefinedDictionary(cv::aruco::DICT_6X6_250);
    
    //charucoボード生成 10x7マスのチェスボード、グリッドのサイズ0.04f、グリッド内マーカのサイズ0.02f
    cv::Ptr<cv::aruco::CharucoBoard> board = cv::aruco::CharucoBoard::create(10, 7, 0.04f, 0.02f, dictionary);
    
    //マーカー検出時メソッドを指定
    cv::Ptr<cv::aruco::DetectorParameters> params = cv::aruco::DetectorParameters::create();
    params->cornerRefinementMethod = cv::aruco::CORNER_REFINE_NONE;
    
    //マーカー検出
    std::vector<int> markerIds;
    std::vector<std::vector<cv::Point2f> > markerCorners;
    cv::aruco::detectMarkers(image, board->dictionary, markerCorners, markerIds, params);
    
    if (markerIds.size() > 0) {
        
        //マーカー位置を描画
        cv::aruco::drawDetectedMarkers(imageCopy, markerCorners, markerIds);
        
        //マーカーの座標をもとに、チェスボード画像の交点を検出
        std::vector<cv::Point2f> charucoCorners;
        std::vector<int> charucoIds;
        cv::aruco::interpolateCornersCharuco(markerCorners, markerIds, image, board, charucoCorners, charucoIds);
        if (charucoIds.size() > 0)
        {
            //チェスボード交点位置を描画
            cv::aruco::drawDetectedCornersCharuco(imageCopy, charucoCorners, charucoIds, cv::Scalar(0, 0, 255));
        }
    }
    
    cv::imwrite(dstPath, imageCopy);
    
    return 0;
}


int main(int argc, const char * argv[]) {
    
    detectCharucoBoard("./IMG_5410.jpeg", "./IMG_5410_detect.jpg");
    detectCharucoBoard("./IMG_5411.jpeg", "./IMG_5411_detect.jpg");
    detectCharucoBoard("./IMG_5412.jpeg", "./IMG_5412_detect.jpg");
    
    return 0;
}

```

画像を見てみましょう。
この画像が…
![IMG_5410.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/22812/2dc6a2f8-6f47-e416-2cfe-f1854522b872.jpeg)

こうなります。マーカー(青いid)もチェスボード交点(赤いid)も両方検出できているようです。
![IMG_5410_detect.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/22812/4cf8278c-4099-32b3-09b1-26f2cdb188d3.jpeg)


これだけなら、別に普通のチェスボードでも良くない？という感じがします。

ただし、画像を見てもらうとわかるように、charucoモジュールで検出されるチェスボードの交点には、それぞれ別々のid番号が振ってあります。おそらく、この交点idは、交点に対して最も近いマーカのidをもとに割り振っているものと思われます。これによって通常のチェスボードとは若干異なる撮影ができるようになります。それは…
![IMG_5411.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/22812/f4858a4c-8ab7-f2bf-a15f-07416a4042ac.jpeg)
![IMG_5411_detect.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/22812/b05a85bc-f13c-0ab3-0168-925d2a412b26.jpeg)
このように、ボード全体が写っていなくても、チェスボード交点が検出できるようになります。
通常のチェスボード交点検出関数cv::findChessboardCornersだと、指定したマスのチェスボード画像全体が写っていないと検出ができません。なのでカメラをボードに近づけたり、ボードを画角の端側で撮影しようとすると、ボードが画角外にはみ出してボードが検出できなくなることがありました。そのせいで通常のチェスボードだとキャリブレーション用の撮影自体が結構面倒だったり難しかったりすることが多いのですが、これを使えば効率よく撮影できそうですし、チェスボードの一部でも良いこと、全体を写さなくていいことを利用して色々と応用できそうです。

ちなみにもっとアップで撮影してもOKでした。
![IMG_5412.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/22812/ca1217ce-4eea-5a07-66d7-8234ad13f01d.jpeg)
![IMG_5412_detect.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/22812/412418ee-738c-e3e3-d5ab-b8d8e29cbf1e.jpeg)

# まとめ
charucoのボード検出をやってみました。
従来のチェスボードとは仕様が異なる検出ができたので、どう応用するかを考えてみたいと思います。