前のチュートリアル: ArUcoマーカーの検出
次のチュートリアル: ChArUcoボードの検出
| |
| 原著者 | Sergio Garrido, Alexander Panov |
| 互換性 | OpenCV >= 4.7.0 |
ArUcoボードは、カメラに対して単一の姿勢を提供するという意味で、単一のマーカーのように振る舞うマーカーの集合である。
最も一般的なボードは、すべてのマーカーが同一平面上にあるものである。簡単に印刷できるためである:
ただし、ボードはこの配置に限定されるわけではなく、任意の2Dまたは3Dレイアウトを表現できる。
ボードと独立したマーカーの集合との違いは、ボード内のマーカー間の相対位置があらかじめわかっている点にある。これにより、すべてのマーカーのコーナーを用いて、ボード全体に対するカメラの姿勢を推定できる。
独立したマーカーの集合を使用する場合、環境内でのマーカーの相対位置がわからないため、各マーカーの姿勢を個別に推定することになる。
ボードを使用する主な利点は次のとおりである:
- 姿勢推定がはるかに柔軟になる。姿勢推定の実行に必要なマーカーは一部だけである。したがって、オクルージョンや部分的な見えがある場合でも姿勢を計算できる。
- より多くの点対応 (マーカーのコーナー) が利用されるため、得られる姿勢は通常より正確になる。
ボードの検出
ボードの検出は標準的なマーカー検出と同様である。唯一の違いは姿勢推定のステップにある。実際、マーカーボードを使用するには、ボードの姿勢を推定する前に標準的なマーカー検出を行う必要がある。
ボードの姿勢推定を行うには、以下に samples/cpp/tutorial_code/objectDetection/detect_board.cpp で示すように、solvePnP() 関数を使用する必要がある。
int markersX = parser.get<int>("w");
int markersY = parser.get<int>("h");
float markerLength = parser.get<float>("l");
float markerSeparation = parser.get<float>("s");
bool showRejected = parser.has("r");
bool refindStrategy = parser.has("rs");
int camId = parser.get<int>("ci");
Mat camMatrix, distCoeffs;
readCameraParamsFromCommandLine(parser, camMatrix, distCoeffs);
if(parser.has("v")) {
video = parser.get<
String>(
"v");
}
if(!parser.check()) {
parser.printErrors();
return 0;
}
int waitTime;
if(!video.empty()) {
waitTime = 0;
} else {
waitTime = 10;
}
float axisLength = 0.5f * ((float)min(markersX, markersY) * (markerLength + markerSeparation) +
markerSeparation);
double totalTime = 0;
int totalIterations = 0;
while(inputVideo.
grab()) {
vector<int> ids;
vector<vector<Point2f>> corners, rejected;
detector.detectMarkers(image, corners, ids, rejected);
if(refindStrategy)
detector.refineDetectedMarkers(image, board, corners, ids, rejected, camMatrix,
distCoeffs);
int markersOfBoardDetected = 0;
if(!ids.empty()) {
board.matchImagePoints(corners, ids, objPoints, imgPoints);
cv::solvePnP(objPoints, imgPoints, camMatrix, distCoeffs, rvec, tvec);
markersOfBoardDetected = (int)objPoints.
total() / 4;
}
totalTime += currentTime;
totalIterations++;
if(totalIterations % 30 == 0) {
cout << "Detection Time = " << currentTime * 1000 << " ms "
<< "(Mean = " << 1000 * totalTime / double(totalIterations) << " ms)" << endl;
}
if(!ids.empty())
aruco::drawDetectedMarkers(imageCopy, corners, ids);
if(showRejected && !rejected.empty())
aruco::drawDetectedMarkers(imageCopy, rejected,
noArray(),
Scalar(100, 0, 255));
if(markersOfBoardDetected > 0)
char key = (char)
waitKey(waitTime);
if(key == 27) break;
引数は次のとおりである:
drawFrameAxes() 関数を用いて、得られた姿勢を確認できる。例えば:
Board with axis
そしてこれは、ボードが部分的にオクルージョンされた別の例である:
Board with occlusions
ご覧のとおり、一部のマーカーが検出されていなくても、残りのマーカーからボードの姿勢を推定できる。
サンプル動画:
完全に動作する例は、samples/cpp/tutorial_code/objectDetection/ 内の detect_board.cpp に含まれている。
サンプルは、cv::CommandLineParser を介してコマンドラインから入力を受け取るようになっている。このファイルの場合、例として用いる引数は次のようになる:
-w=5 -h=7 -l=100 -s=10
-v=/path_to_opencv/opencv/doc/tutorials/objdetect/aruco_board_detection/gboriginal.jpg
-c=/path_to_opencv/opencv/samples/cpp/tutorial_code/objectDetection/tutorial_camera_params.yml
-cd=/path_to_opencv/opencv/samples/cpp/tutorial_code/objectDetection/tutorial_dict.yml
detect_board.cpp の引数:
const char* keys =
"{w | | Number of squares in X direction }"
"{h | | Number of squares in Y direction }"
"{l | | Marker side length (in pixels) }"
"{s | | Separation between two consecutive markers in the grid (in pixels)}"
"{d | | dictionary: DICT_4X4_50=0, DICT_4X4_100=1, DICT_4X4_250=2,"
"DICT_4X4_1000=3, DICT_5X5_50=4, DICT_5X5_100=5, DICT_5X5_250=6, DICT_5X5_1000=7, "
"DICT_6X6_50=8, DICT_6X6_100=9, DICT_6X6_250=10, DICT_6X6_1000=11, DICT_7X7_50=12,"
"DICT_7X7_100=13, DICT_7X7_250=14, DICT_7X7_1000=15, DICT_ARUCO_ORIGINAL = 16}"
"{cd | | Input file with custom dictionary }"
"{c | | Output file with calibrated camera parameters }"
"{v | | Input from video or image file, if omitted, input comes from camera }"
"{ci | 0 | Camera id if input doesnt come from video (-v) }"
"{dp | | File of marker detector parameters }"
"{rs | | Apply refind strategy }"
"{r | | show rejected candidates too }";
}
グリッドボード
cv::aruco::Board オブジェクトの作成には、環境内の各マーカーのコーナー位置を指定する必要がある。ただし多くの場合、ボードは同一平面上にグリッド状に配置されたマーカーの集合にすぎないため、簡単に印刷して使用できる。
幸い、arucoモジュールは、この種のマーカーを簡単に作成・印刷するための基本機能を提供している。
cv::aruco::GridBoard クラスは cv::aruco::Board クラスを継承する特殊なクラスであり、次の画像のように、すべてのマーカーが同一平面上にグリッド状に配置されたボードを表す:
Image with aruco board
具体的には、グリッドボードの座標系はボード平面上に位置し、ボードの左下隅を原点とし、Zが外側を向くように配置される。次の画像のとおりである (X:赤, Y:緑, Z:青):
Board with axis
cv::aruco::GridBoard オブジェクトは、以下の引数を用いて定義できる:
- X方向のマーカー数。
- Y方向のマーカー数。
- マーカーの一辺の長さ。
- マーカー間の間隔の長さ。
- マーカーの辞書。
- すべてのマーカーのid (X*Y個のマーカー)。
このオブジェクトは、これらの引数から cv::aruco::GridBoard コンストラクタを用いて簡単に作成できる:
- 第1引数と第2引数は、それぞれX方向とY方向のマーカー数である。
- 第3引数と第4引数は、それぞれマーカーの長さとマーカー間の間隔である。これらは任意の単位で指定できるが、このボードに対して推定される姿勢が同じ単位で測定されることに留意する (一般にはメートルが使われる)。
- 最後に、マーカーの辞書を指定する。
したがって、このボードは5x7=35個のマーカーで構成される。各マーカーのidは、デフォルトでは0から昇順に割り当てられるため、0, 1, 2, ..., 34となる。
グリッドボードを作成したら、おそらくそれを印刷して使用したいだろう。これには2つの方法がある:
- スクリプト
apps/pattern-tools/generate_pattern.py を使用する。キャリブレーションパターンの作成を参照。
cv::aruco::GridBoard::generateImage() 関数を使用する。
cv::aruco::GridBoard::generateImage() 関数は cv::aruco::GridBoard クラスで提供されており、以下のコードを用いて呼び出せる:
board.generateImage(imageSize, boardImage, margins, borderBits);
- 第1引数は出力画像のサイズ (ピクセル単位) である。この場合は600x500ピクセルである。これがボードの寸法に比例していない場合、画像の中央に配置される。
boardImage: ボードが描かれた出力画像。
- 第3引数は(省略可能な)マージン (ピクセル単位) であり、どのマーカーも画像の境界に接しないようにする。この場合のマージンは10である。
- 最後に、
generateImageMarker() 関数と同様に、マーカーの境界のサイズを指定する。デフォルト値は1である。
ボード作成の完全に動作する例は、samples/cpp/tutorial_code/objectDetection/create_board.cpp に含まれている
出力画像は次のようになる:
このサンプルは現在、cv::CommandLineParser を介してコマンドラインから入力を受け取る。このファイルの場合、例のパラメータは次のようになる:
"_output_path_/aboard.png" -w=5 -h=7 -l=100 -s=10 -d=10
マーカー検出の精緻化
ArUcoボードは、マーカーの検出を改善するためにも利用できる。ボードに属するマーカーの一部を検出済みの場合、これらのマーカーとボードのレイアウト情報を用いて、まだ検出されていないマーカーを見つけることができる。
これは cv::aruco::refineDetectedMarkers() 関数を用いて行える。この関数は cv::aruco::ArucoDetector::detectMarkers() を呼び出した後に呼び出す必要がある。
この関数の主な引数は、マーカーが検出された元の画像、ボードオブジェクト、検出されたマーカーのコーナー、検出されたマーカーのid、および棄却されたマーカーのコーナーである。
棄却されたコーナーは cv::aruco::ArucoDetector::detectMarkers() 関数から取得でき、マーカー候補とも呼ばれる。これらの候補は、元の画像内で見つかった正方形の形状であるが、識別ステップを通過できなかったもの (すなわち内部の符号化に誤りが多すぎるもの) であり、そのためマーカーとして認識されなかったものである。
ただし、これらの候補は、画像中の高ノイズ、非常に低い解像度、その他バイナリコードの抽出に影響する関連する問題のために正しく識別されなかった、実際のマーカーである場合もある。cv::aruco::ArucoDetector::refineDetectedMarkers() 関数は、これらの候補とボードの欠落しているマーカーとの間の対応を見つける。この探索は2つの引数に基づく:
- 候補と、検出されなかったマーカーの投影との距離。これらの投影を得るには、ボードのマーカーを少なくとも1つ検出している必要がある。投影は、カメラパラメータ(カメラ行列と歪み係数)が与えられている場合はそれを用いて求められる。与えられていない場合は、投影は局所的なホモグラフィから求められ、平面ボードのみが許容される(すなわち、すべてのマーカーコーナーのZ座標が同一でなければならない)。
refineDetectedMarkers() の minRepDistance 引数は、候補コーナーと投影されたマーカーコーナーとの間の最小ユークリッド距離を決定する(デフォルト値10)。
- 二値符号化。候補が最小距離の条件を満たした場合、それが実際に投影されたマーカーであるかどうかを判定するため、その内部ビットが再度解析される。ただしこの場合、条件はそれほど厳しくなく、許容される誤りビットの数をより大きくできる。これは
errorCorrectionRate 引数で指定する(デフォルト値3.0)。負の値が与えられた場合、内部ビットはまったく解析されず、コーナー距離のみが評価される。
これは cv::aruco::ArucoDetector::refineDetectedMarkers() 関数を使用する例である:
detector.detectMarkers(image, corners, ids, rejected);
if(refindStrategy)
detector.refineDetectedMarkers(image, board, corners, ids, rejected, camMatrix,
distCoeffs);
また、場合によっては、最初に検出されたマーカーの数が少なすぎると(例えばマーカーが1つか2つしかない場合)、検出されなかったマーカーの投影の品質が悪くなり、誤った対応付けを生じることがある点にも注意する必要がある。
より詳細な実装については、モジュールのサンプルを参照すること。