OpenCV 5.0.0
Open Source Computer Vision
読み込み中...
検索中...
見つかりません
🤖 AIによる機械翻訳(非公式) — これは OpenCV 5.0.0 公式リファレンス(英語)を AI (Claude) で自動翻訳したものです。訳に誤りを含む場合があります。正確な情報は 公式英語版(原文) を参照してください。
マルチビューカメラキャリブレーションのチュートリアル

前のチュートリアル: 対話的カメラキャリブレーションアプリケーション
次のチュートリアル: USAC: OpenCVにおけるRandom Sample Consensusの改良

原著者Maksym Ivashechkin, Linfei Pan
互換性OpenCV >= 5.0

構成

このチュートリアルは以下のセクションで構成される:

  • はじめに
  • 概要
  • 実行方法
  • Pythonの例
  • アルゴリズムの詳細
  • メソッドの入力
  • メソッドの出力
  • メソッドの入力
  • 擬似コード
  • PythonサンプルAPI
  • C++サンプルAPI
  • 実践的なデバッグ手法

はじめに

マルチビューキャリブレーションはコンピュータビジョンにおいて非常に重要なタスクである。3D再構成、structure from motion、自動運転などで広く用いられる。キャリブレーション手順は、カメラの内部パラメータと外部パラメータを取得するために行わなければならない、あらゆるビジョンタスクの最初のステップであることが多い。カメラキャリブレーションパラメータの精度はその後のすべての計算と結果に直接影響するため、精密な内部パラメータと外部パラメータを推定することは極めて重要である。

キャリブレーションアルゴリズムは各カメラについて、画像上にキャリブレーションパターン(例: チェッカーボード、ChArUcoなど)が写り検出されている一連の画像を必要とする。さらに、実スケールの結果を得るには、キャリブレーションパターングリッドの隣り合う2点間の3D距離を測定する必要がある。外部パラメータのキャリブレーションでは、画像は異なる視点から取得した同じキャリブレーションパターンを共有していなければならない。セットアップの例を次の図に示す。

さらに、パターングリッドを共有する画像は同じ瞬間に撮影されていなければならない。言い換えると、カメラは同期している必要がある。そうでなければ、外部パラメータのキャリブレーションは失敗する。なお、各パターン点が一意に決定できる場合(例えばChArUcoターゲットを使用する場合、cv::aruco::CharucoBoardを参照)、部分的な観測のみに基づいてキャリブレーションすることも可能である。マルチビューカメラのキャリブレーションではカメラペア間の視野の重なりは通常限られており、同時に完全なパターンを観測することは一般に難しいため、これは推奨される。

内部パラメータのキャリブレーションには、カメラの焦点距離、スキュー、主点の推定が含まれる。これらのパラメータはサイズ3x3の上三角の内部行列にまとめられる。さらに、内部キャリブレーションにはカメラの歪みパラメータを求めることも含まれる。

外部パラメータは2つのカメラ間の相対的な回転と並進を表す。各フレームについて、カメラ\(i\)の絶対的なカメラ姿勢を\(R_i, t_i\)、カメラ\(i\)とカメラ\(j\)間の相対的なカメラ姿勢を\(R_{ij}, t_{ij}\)とする。\(R_1, t_1\)と、任意の\(i\not=1\)についての\(R_{1i}\)が既知であるとすると、その姿勢は次のように計算できる

\[ R_i = R_{1i} R_1\]

\[ t_i = R_{1i} t_1 + t_{1i}\]

2つのカメラ間の相対姿勢は次のように計算できるので

\[ R_{ij} = R_j R_i^\top \]

\[ t_{ij} = -R_{ij} t_i + R_j \]

これは、\(R_{ij}, i\not=1\)の形をした他の任意の相対姿勢が冗長であることを意味する。したがって、\(N\)台のカメラに対して、推定された相対回転と並進を正しく選んだペアの十分な数は\(N-1\)であり、一方で考えられるすべてのペアの外部パラメータ\(N^2 = N * (N-1) / 2\)は、推定されたものから導出できる。内部パラメータのキャリブレーションの詳細はこのチュートリアルキャリブレーションパターンの作成と、その実装cv::calibrateCameraにある。

内部パラメータと外部パラメータのキャリブレーションの後、内部行列、回転行列、並進を組み合わせてカメラの射影行列が求められる。射影行列により、三角測量(3D再構成)、平行化(レクティフィケーション)、エピポーラ幾何の算出などが可能になる。

以下のセクションでは、マルチカメラキャリブレーション全体のパイプラインを構成する個々のアルゴリズムのステップを説明する:

概要

このアルゴリズムは3つの主要なステップから成り、次のように列挙できる:

  1. 各カメラについて内部パラメータ(内部行列と歪み係数)を独立にキャリブレーションする。
  2. ステップ1で得た内部パラメータを用いて、カメラをペアごとに(カメラペアの位置合わせを用いて)キャリブレーションする。
  3. すべてのカメラを同時に用いてグローバル最適化を行い、外部パラメータを精緻化する。

実行方法:

N個のカメラビューがあり、各i番目のビューにはパターン点(例: チェッカーボード)を含むM枚の画像があるものとする。

Pythonの例

Pythonでサンプルコード(opencv/apps/multiview-calibration/multiview_calibration.py)を実行するには、生の画像を使う方法と、用意した点群を使う方法の2つの選択肢がある。最初の選択肢は、各行に1枚の画像へのパスを記したN個のファイル(対応するファイルの特定のカメラの画像)を用意することである。あるフレームでそのカメラに対応する画像がない場合は、その行を空のままにする。例えば、カメラiのファイルは次のようになる(file_i.txt):

/path/to/image_1_of_camera_i
/path/to/image_3_of_camera_i
...
/path/to/image_M_of_camera_i

画像へのパスはfile_i.txtからの相対パスでなければならない。その後、サンプルプログラムは次のようにコマンドラインから実行できる:

$ python3 multiview_calibration.py --pattern_size W,H --pattern_type TYPE --is_fisheye IS_FISHEYE_1,...,IS_FISHEYE_N \
--pattern_distance DIST --filenames /path/to/file_1.txt,...,/path/to/file_N.txt

WHをパターン点のサイズに、TYPEをキャリブレーショングリッドの種類の名前(サポートされているパターン: checkerboard, circles, acircles)に、IS_FISHEYEをカメラの種類(1 - 魚眼、0 - ピンホール)に、DISTをパターン距離(すなわちチェッカーボードの2つのセル間の距離)に置き換える。サンプルスクリプトは指定したパターンの種類に従って画像点を自動的に検出する。デフォルトでは検出は並列に行われるが、このオプションはオフにできる。

Pythonサンプルで使用できる追加の(省略可能な)フラグは次のとおりである:

  • --winsize - コーナー検出のウィンドウサイズを定義するためにH,Wの値を渡す(デフォルトは5,5)。
  • --debug_corners - TrueまたはFalseを渡す。Trueの場合、ユーザーが検出を手動で確認できるように、検出されたコーナー付きのランダムな画像をいくつか表示する(デフォルトはFalse)。
  • --points_json_file - 検出後に画像点とパターン点を保存できるJSONファイルの名前を渡す。後でこのファイルを使ってサンプルコードを実行できる。デフォルト値は''(何も保存されない)。
  • --find_intrinsics_in_python - 0または1を渡す。1の場合、Pythonサンプルが内部パラメータを自動的にキャリブレーションし、再投影誤差を報告する。マルチビューキャリブレーションは外部パラメータについてのみ行われる。このフラグは、キャリブレーション処理を分離し、何が問題なのかをデバッグしやすくすることを目的としている。
  • --path_to_save - 結果をpickleファイルに保存するパス
  • --path_to_visualize - 可視化を実行するために必要な結果のpickleファイルへのパス
  • --visualize - 可視化フラグ(TrueまたはFalse)。Trueの場合は可視化のみを実行するが、path_to_visualizeを指定しなければならない
  • --resize_image_detection - True / False。Trueの場合、コーナー検出を高速化するために画像がリサイズされる
  • --gt_file - グラウンドトゥルースを含むファイルへのパス。例はopencv_extra/testdata/python/hololens_multiview_calibration_images/HololensCapture4/gt.txtにある(現在はプルリクエスト1089内)。形式は次のとおりである
    K_0 (3 x 3)
    distortion_0 (1 row),
    R_0 (3 x 3)
    t_0 (3 x 1)
    ...
    K_n (3 x 3)
    distortion_n (1 row),
    R_n (3 x 3)
    t_n (3 x 1)
    # (Optional, pose for each frame)
    R_f0 (3 x 3)
    t_f1 (3 x 1)
    ...
    R_fm (3 x 3)
    t_fm (3 x 1)

代わりに、画像点、パターン点、およびカメラが魚眼かどうかを示すブール値を含むべきJSONファイルからPythonサンプルを実行することもできる。JSONファイルの例はopencv_extra/testdata/python/multiview_calibration_data.jsonにある(現在はプルリクエスト1001内)。その形式は以下の項目を持つ辞書でなければならない:

  • object_points - パターン(オブジェクト)点のリストのリスト(サイズ NUM_POINTS x 3)。
  • image_points - 画像点のリストのリストのリストのリスト(サイズ NUM_CAMERAS x NUM_FRAMES x NUM_POINTS x 2)。これは固定サイズである点に注意。観測が不完全な場合は、対応する画像点を無効(例えば(-1, -1))に設定する
  • image_sizes - 画像サイズのタプル(width x height)のリスト。
  • is_fisheye - ブール値のリスト(true - 魚眼カメラ、false - それ以外)。省略可能で:
  • Ksdistortions - 内部パラメータ。これらがJSONファイルで提供されている場合、提案手法は内部パラメータを推定しない。Ks(内部行列)はリストのリストのリスト(NUM_CAMERAS x 3 x 3)、distortionsは歪みパラメータのリストのリスト(NUM_CAMERAS x NUM_VALUES)である。
  • images_names - キャリブレーション後の点の可視化用の画像ファイル名のリストのリスト(NUM_CAMERAS x NUM_FRAMES x string)。
$ python3 multiview_calibration.py --json_file /path/to/json

フラグの説明は、サンプルスクリプトをhelpオプション付きで実行することで直接確認できる:

python3 multiview_calibration.py --help

(Blenderで生成された)opencv_extra/testdata/python/由来のmultiview_calibration_imagesデータに対するLinuxターミナルでの期待される出力は次のとおりである:

opencv_extra/testdata/python/real_multiview_calibration_imagesにある実環境のキャリブレーション画像に対する期待される出力は次のとおりである:

opencv_extra/testdata/python/hololens_multiview_calibration_imagesにある実環境のキャリブレーション画像に対する期待される出力は次のとおりである

使用したコマンド

python3 multiview_calibration.py --filenames ../../results/hololens/HololensCapture1/output/cam_0.txt,../../results/hololens/HololensCapture1/output/cam_1.txt,../../results/hololens/HololensCapture1/output/cam_2.txt,../../results/hololens/HololensCapture1/output/cam_3.txt --pattern_size 6,10 --pattern_type charuco --fisheye 0,0,0,0 --pattern_distance 0.108 --board_dict_path ../../results/hololens/charuco_dict.json --gt_file ../../results/hololens/HololensCapture1/output/gt.txt

アルゴリズムの詳細

  1. Intrinsics estimation, and rotation and translation initialization
    1. If the intrinsics are not provided, the calibration procedure starts intrinsics calibration independently for each camera using the OpenCV function cv::calibrateCamera.
      1. The following flags are used for the calibrating pinhole camera and fisheye camera
        • ピンホール: cv::CALIB_ZERO_TANGENT_DIST - 接線方向の歪み係数をゼロにし、魚眼カメラモデルと整合させる。
        • 魚眼: cv::CALIB_RECOMPUTE_EXTRINSIC, cv::CALIB_FIX_SKEW - 魚眼カメラモデルの内部キャリブレーションはあまり安定しておらず、これら2つのパラメータは結果を頑健にするのに役立つことが経験的に分かっている
      2. すべての画像点が共線であるという縮退した設定を避けるため、観測数が4未満の画像やカバレッジが0.5%未満のフレームを無効としてマークする縮退チェックが行われる。
      3. 内部キャリブレーションの出力には、回転ベクトル、並進ベクトル(パターン点のカメラフレームへの変換)、およびフレームごとの誤差も含まれる。各フレームについて、すべてのカメラの中で最も誤差が小さいカメラのインデックスが保存される。
    2. それ以外で、内部パラメータが既知の場合、提案アルゴリズムはperspective-n-point推定(cv::solvePnP, cv::fisheye::solvePnP)を実行して、回転ベクトルと並進ベクトル、およびフレームごとの再投影誤差を推定する。
  2. Initialization of relative camera pose.
    1. If the initial relative poses are not assumed known (CALIB_USE_EXTRINSIC_GUESS flag not set), then the relative camera extrinsics are found by traversing a spanning tree and estimating pairwise relative camera pose.
      1. Miminal spanning tree establishment. Assume that cameras can be represented as nodes of a connected graph. An edge between two cameras is created if there is any concurrent observation over all frames. If the graph does not connect all cameras (i.e., exists a camera that has no overlap with other cameras) then calibration is not possible. Otherwise, the next step consists of finding the maximum spanning tree (MST) of this graph. The MST captures all the best pairwise camera connections. The weight of edges across all frames is a weighted combination of multiple factors:
        • (主要) 両方の画像(カメラ)で検出されたパターン点の数
        • 画像内に投影された点の凸包の面積と画像解像度との比。
        • カメラの光軸間の角度(回転ベクトルから求める)。
        • カメラの光軸とパターンの法線ベクトルとの間の角度(共線でない3つのパターン点から求める)。
      2. 相対カメラ姿勢の初期化。カメラの外部パラメータの初期推定は、カメラペアの位置合わせ(cv::registerCamerasを参照)によって求められる。一般性を失うことなく、0番目のカメラの回転は単位行列に、並進はゼロベクトルに固定され、0番目のノードがMSTのルートとなる。ステレオキャリブレーションの順序は、ルートから始めてMSTを幅優先探索でたどることで選択される。ペアの総数(木の辺の数でもある)はNUM_CAMERAS - 1であり、これは木グラフの性質である。
    2. あるいは、カメラ姿勢の事前知識が提供されている場合、このステップはスキップできる
  3. Global optimization. Given the initial estimate of extrinsics, the aim is to polish results using global optimization (via the Levenberq-Marquardt method, see cv::LevMarq class).
    • 反復の総数を減らすため、内部キャリブレーションによる最初のステップで推定された最も誤差の小さいすべての回転ベクトルと並進ベクトルは、ルートカメラを基準とした相対的なものに変換される。
    • パラメータの総数は (NUM_CAMERAS - 1) x (3 + 3) + NUM_FRAMES x (3 + 3) であり、ここで3は回転ベクトル、3は並進ベクトルを表す。パラメータの最初の部分は外部パラメータ用、2番目の部分はフレームごとの回転ベクトルと並進ベクトル用である。これははじめにの説明図から見て取れる。各フレームについて、カメラ間の相対姿勢が固定されていれば、カメラ姿勢を計算するのに必要なカメラ姿勢は1つだけである。
    • ロバスト関数を追加で適用し、最適化中の外れ値点の影響を緩和する。この関数はガウス関数の導関数の形をしており、$x * exp(-x/s)$である(expの近似により効率的に実装される)。ここでxは二乗ピクセル誤差、sは手動で事前定義したスケールである。この関数を選んだのは、0からyピクセル誤差の区間では増加し、その後は減少するためである。考え方としては、yに達するまでは関数が誤差をわずかに減少させ、誤差が大きすぎる(yより大きい)場合はそのロバスト値が0に制限される、というものである。スケール係数の値は、誤差のロバスト値が10pxになるまでロバスト関数をほぼ線形に増加させ、その後減少させるよう、総当たり評価によって求められた(下の関数のプロットを参照)。値自体は30に等しいが、OpenCVのソースコードで変更できる。

メソッドの入力

提案手法の高レベルの入力は次のとおりである:

  • パターン(オブジェクト)点: (NUM_FRAMES x) NUM_PATTERN_POINTS x 3。点はフレームに沿ったパターン点のコピーを含んでもよい。
  • 画像点: NUM_CAMERAS x NUM_FRAMES x NUM_PATTERN_POINTS x 2。
  • 画像サイズ: NUM_CAMERAS x 2(幅と高さ)。
  • 検出マスク: サイズ NUM_CAMERAS x NUM_FRAMES の行列で、特定のカメラとフレームインデックスについてパターン点が検出されたかどうかを示す。
  • Rs, Ts(省略可能): カメラ0を基準とした(相対的な)回転と並進。ベクトルの数はNUM_CAMERAS-1であり、最初のカメラについては回転ベクトルと並進ベクトルがゼロである。
  • Ks(省略可能): カメラごとの内部行列。
  • 歪み(省略可能)。
  • use_intrinsics_guess: 内部パラメータが与えられているかどうかを示す。
  • Flags_intrinsics: 内部パラメータ推定用のフラグ。
  • use_extrinsic_guess: 外部パラメータが与えられているかどうかを示す。

メソッドの出力

提案手法のハイレベルな出力は以下のとおりである。

  • Rs, Ts: カメラ0を基準とした外部パラメータの(相対)回転ベクトルと並進ベクトル。ベクトルの数は NUM_CAMERAS-1 であり、最初のカメラの回転ベクトルと並進ベクトルはゼロである。
  • 各カメラの内部パラメータ行列。
  • 各カメラの歪み係数。
  • カメラ0を基準とした各フレームのパターンの回転ベクトルと並進ベクトル。回転と並進の組み合わせによりパターン点をカメラ座標空間へ変換でき、それにより内部パラメータを用いて3D点を画像へ投影できる。
  • サイズ NUM_CAMERAS x NUM_FRAMES の再投影誤差の行列
  • 外部パラメータの初期推定に用いられる出力ペア。ペアの数は NUM_CAMERAS-1 である。

擬似コード

この手法の考え方はハイレベルな擬似コードで示せるが、提案手法のC++実装全体は opencv/modules/calib/src/multiview_calibration.cpp ファイルに実装されている。

def mutiviewCalibration (pattern_points, image_points, detection_mask):
for cam_i = 1,…,NUMBER_CAMERAS:
if CALIBRATE_INTRINSICS:
K_i, distortion_i, rvecs_i, tvecs_i = calibrateCamera(pattern_points, image_points[cam_i])
else:
rvecs_i, tvecs_i = solvePnP(pattern_points, image_points[cam_i], K_i, distortion_i)
# Select best rvecs, tvecs based on reprojection errors. Process data:
if CALIBRATE_EXTRINSICS:
pattern_img_area[cam_i][frame] = area(convexHull(image_points[cam_i][frame]))
angle_to_board[cam_i][frame] = arccos(pattern_normal_frame * optical_axis_cam_i)
angle_cam_to_cam[cam_i][cam_j] = arccos(optical_axis_cam_i * optical_axis_cam_j)
graph = maximumSpanningTree(detection_mask, pattern_img_area, angle_to_board, angle_cam_to_cam)
camera_pairs = bread_first_search(graph, root_camera=0)
for pair in camera_pairs:
# find relative rotation, translation from camera i to j
R_ij, t_ij = registerCameras(pattern_points_i, pattern_points_j, image_points[i], image_points[j])
else:
pass
R*, t* = optimizeLevenbergMarquardt(R, t, pattern_points, image_points, K, distortion)

Python サンプルAPI

Pythonでキャリブレーション手順を実行するには、以下の手順に従う(apps/multiview-calibration/multiview_calibration.py のサンプルコードを参照)。

  1. データの準備:

    if pattern_type.lower() == 'checkerboard' or pattern_type.lower() == 'charuco':
    pattern = chessboard_points(grid_size, dist_m)
    elif pattern_type.lower() == 'circles':
    pattern = circles_grid_points(grid_size, dist_m)
    elif pattern_type.lower() == 'acircles':
    pattern = asym_circles_grid_points(grid_size, dist_m)
    else:
    raise NotImplementedError("Pattern type is not implemented!")
    if pattern_type.lower() == 'charuco':
    assert (board_dict_path is not None) and os.path.exists(board_dict_path)
    board_dict = json.load(open(board_dict_path, 'r'))
    else:
    board_dict = None

    検出マスク行列は、検出後の画像点のサイズを確認することで後から構築される。

  2. 画像上のパターン点を検出する:

    if pattern_type.lower() == 'checkerboard':
    ret, corners = cv.findChessboardCorners(
    cv.cvtColor(img_detection, cv.COLOR_BGR2GRAY), grid_size, None
    )
    if ret:
    if scale < 1.0:
    corners /= scale
    corners2 = cv.cornerSubPix(cv.cvtColor(img, cv.COLOR_BGR2GRAY),
    corners, winsize, (-1,-1), criteria)
    elif pattern_type.lower() == 'circles':
    # Workaround: CALIB_CB_CLUSTERING does not allow pattern flip
    ret, corners = cv.findCirclesGrid(
    img_detection, patternSize=grid_size, flags=cv.CALIB_CB_SYMMETRIC_GRID+cv.CALIB_CB_CLUSTERING
    )
    if ret:
    corners2 = corners / scale
    elif pattern_type.lower() == 'acircles':
    # Workaround: CALIB_CB_CLUSTERING does not allow pattern flip
    ret, corners = cv.findCirclesGrid(
    img_detection, patternSize=grid_size, flags=cv.CALIB_CB_ASYMMETRIC_GRID+cv.CALIB_CB_CLUSTERING
    )
    if ret:
    corners2 = corners / scale
    elif pattern_type.lower() == 'charuco':
    dictionary = cv.aruco.getPredefinedDictionary(board_dict["dictionary"])
    size=(grid_size[0] + 1, grid_size[1] + 1),
    squareLength=board_dict["square_size"],
    markerLength=board_dict["marker_size"],
    dictionary=dictionary
    )
    # The found best practice is to refine detected Aruco marker with contour,
    # then refine subpix with the board functions
    detector_params = cv.aruco.DetectorParameters()
    charuco_params = cv.aruco.CharucoParameters()
    charuco_params.tryRefineMarkers = True
    detector_params.cornerRefinementMethod = cv.aruco.CORNER_REFINE_CONTOUR
    refine_params = cv.aruco.RefineParameters()
    detector = cv.aruco.CharucoDetector(board, charuco_params, detector_params, refine_params)
    charucoCorners, charucoIds, _, _ = detector.detectBoard(img_detection)
    corners = np.ones([grid_size[0] * grid_size[1], 1, 2]) * -1
    ret = (not charucoIds is None) and charucoIds.flatten().size > 3
    if ret:
    corners[charucoIds.flatten()] = cv.cornerSubPix(cv.cvtColor(img, cv.COLOR_BGR2GRAY),
    charucoCorners / scale, winsize, (-1,-1), criteria)
    corners2 = corners
    else:
    raise ValueError("Calibration pattern is not supported!")
  3. 検出マスク行列を構築する:

    for i in range(len(image_points)):
    for j in range(len(image_points[0])):
    detection_mask[i,j] = int(len(image_points[i][j]) != 0)
  4. 最後に、キャリブレーション関数を以下のように実行する:

    rmse, Ks, distortions, Rs, Ts, output_pairs, rvecs0, tvecs0, errors_per_frame = \
    cv.calibrateMultiviewExtended(
    objPoints=pattern_points_all,
    imagePoints=image_points,
    imageSize=image_sizes,
    detectionMask=detection_mask,
    models=np.array(models, dtype=np.uint8),
    Rs=None,
    Ts=None,
    Ks=Ks,
    distortions=distortions,
    flagsForIntrinsics=np.array([pinhole_flag if models[x] == cv.CALIB_MODEL_PINHOLE else fisheye_flag for x in range(num_cameras)], dtype=int),
    flags = (cv.CALIB_USE_INTRINSIC_GUESS if useIntrinsics else 0) +
    (cv.CALIB_STEREO_REGISTRATION if use_stereo_init else 0)
    )

C++ サンプルAPI

C++でキャリブレーション手順を実行するには、以下の手順に従う(opencv/samples/cpp/multiview_calibration_sample.cpp のサンプルコードを参照)。

  1. Pythonサンプルと同様にデータを準備する。すなわち、パターンサイズとスケール、魚眼カメラマスク、画像ファイル名を含むファイルを用意し、それらを関数に渡す。

    static void detectPointsAndCalibrate (cv::Size pattern_size, float pattern_distance, const std::string &pattern_type,
    const std::vector<uchar> &models, const std::vector<std::string> &filenames,
    const cv::String* dict_path=nullptr)
  2. データの初期化:

    std::vector<cv::Point3f> board (pattern_size.area());
    const int num_cameras = (int)models.size();
    std::vector<std::vector<cv::Mat>> image_points_all;
    std::vector<cv::Size> image_sizes;
    std::vector<cv::Mat> Ks, distortions, Ts, Rs;
    if (pattern_type == "checkerboard" || pattern_type == "charuco") {
    for (int i = 0; i < pattern_size.height; i++) {
    for (int j = 0; j < pattern_size.width; j++) {
    board[i*pattern_size.width+j] = cv::Point3f((float)j, (float)i, 0.f) * pattern_distance;
    }
    }
    } else if (pattern_type == "circles") {
    for (int i = 0; i < pattern_size.height; i++) {
    for (int j = 0; j < pattern_size.width; j++) {
    board[i*pattern_size.width+j] = cv::Point3f((float)j, (float)i, 0.f) * pattern_distance;
    }
    }
    } else if (pattern_type == "acircles") {
    for (int i = 0; i < pattern_size.height; i++) {
    for (int j = 0; j < pattern_size.width; j++) {
    if (i % 2 == 1) {
    board[i*pattern_size.width+j] = cv::Point3f((j + .5f)*pattern_distance, (i/2 + .5f) * pattern_distance, 0.f);
    } else{
    board[i*pattern_size.width+j] = cv::Point3f(j*pattern_distance, (i/2)*pattern_distance, 0);
    }
    }
    }
    }
    else {
    CV_Error(cv::Error::StsNotImplemented, "pattern_type is not implemented!");
    }
  3. ChArUco検出器のセットアップ: 省略可能。パターンタイプがChArUcoの場合にのみ必要。

    if (pattern_type == "charuco") {
    CV_Assert(dict_path != nullptr);
    CV_Assert(fs.isOpened());
    int dict_int;
    double square_size, marker_size;
    fs["dictionary"] >> dict_int;
    fs["square_size"] >> square_size;
    fs["marker_size"] >> marker_size;
    auto dictionary = cv::aruco::getPredefinedDictionary(dict_int);
    // For charuco board, the size is defined to be the number of box (not inner corner)
    auto charuco_board = cv::aruco::CharucoBoard(
    cv::Size(pattern_size.width+1, pattern_size.height+1),
    static_cast<float>(square_size), static_cast<float>(marker_size), dictionary);
    // It is suggested to use refinement in detecting charuco board
    auto detector_params = cv::aruco::DetectorParameters();
    auto charuco_params = cv::aruco::CharucoParameters();
    charuco_params.tryRefineMarkers = true;
    detector_params.cornerRefinementMethod = cv::aruco::CORNER_REFINE_CONTOUR;
    detector = cv::makePtr<cv::aruco::CharucoDetector>(charuco_board, charuco_params, detector_params);
    }
  4. 画像上のパターン点を検出する:

    int num_frames = -1;
    for (const auto &filename : filenames) {
    std::fstream file(filename);
    CV_Assert(file.is_open());
    std::string img_file;
    std::vector<cv::Mat> image_points_cameras;
    bool save_img_size = true;
    while (std::getline(file, img_file)) {
    if (img_file.empty()){
    image_points_cameras.emplace_back(cv::Mat());
    continue;
    }
    cv::Mat img = cv::imread(img_file), corners;
    if (save_img_size) {
    image_sizes.emplace_back(cv::Size(img.cols, img.rows));
    save_img_size = false;
    }
    bool success = false;
    if (pattern_type == "checkerboard") {
    success = cv::findChessboardCorners(img, pattern_size, corners);
    }
    else if (pattern_type == "circles")
    {
    success = cv::findCirclesGrid(img, pattern_size, corners, cv::CALIB_CB_SYMMETRIC_GRID);
    }
    else if (pattern_type == "acircles")
    {
    success = cv::findCirclesGrid(img, pattern_size, corners, cv::CALIB_CB_ASYMMETRIC_GRID);
    }
    else if (pattern_type == "charuco")
    {
    std::vector<int> ids; cv::Mat corners_sub;
    detector->detectBoard(img, corners_sub, ids);
    corners.create(static_cast<int>(board.size()), 2, CV_32F);
    if (ids.size() < 4)
    success = false;
    else {
    success = true;
    int head = 0;
    for (int i = 0; i < static_cast<int>(board.size()); i++) {
    if (head < static_cast<int>(ids.size()) && ids[head] == i) {
    corners.at<float>(i, 0) = corners_sub.at<float>(head, 0);
    corners.at<float>(i, 1) = corners_sub.at<float>(head, 1);
    head++;
    } else {
    // points outside of frame border are dropped by calibrateMultiview
    corners.at<float>(i, 0) = -1.;
    corners.at<float>(i, 1) = -1.;
    }
    }
    }
    }
    cv::Mat corners2;
    corners.convertTo(corners2, CV_32FC2);
    if (success && corners.rows == pattern_size.area())
    image_points_cameras.emplace_back(corners2);
    else
    image_points_cameras.emplace_back(cv::Mat());
    }
    if (num_frames == -1)
    num_frames = (int)image_points_cameras.size();
    else
    CV_Assert(num_frames == (int)image_points_cameras.size());
    image_points_all.emplace_back(image_points_cameras);
    }
  5. 検出マスク行列を構築する:

    cv::Mat visibility(num_cameras, num_frames, CV_8UC1);
    for (int i = 0; i < num_cameras; i++) {
    for (int j = 0; j < num_frames; j++) {
    visibility.at<unsigned char>(i,j) = image_points_all[i][j].empty() ? 0 : 1;
    }
    }
  6. キャリブレーションの実行:

    const double rmse = calibrateMultiview(objPoints, image_points_all, image_sizes, visibility,
    models, Ks, distortions, Rs, Ts);

実践的なデバッグ手法

  1. Intrinsics calibration
    1. キャリブレーションを行うのに最も適したフラグを選択する。例えば、ピンホールカメラモデルの歪みが顕著でない場合、cv::CALIB_RATIONAL_MODEL を使う必要はないかもしれない。魚眼カメラモデルの場合は、cv::CALIB_RECOMPUTE_EXTRINSICcv::CALIB_FIX_SKEW の使用が推奨される。
    2. カメラの内部パラメータは、点が画像内でより散らばっているほど精度よく推定できる。観測された点のヒートマップを描画するには、以下のコードを使用できる。

      def plotDetection(image_sizes, image_points):
      num_cameras = len(image_sizes)
      num_frames = len(image_points[0])
      for c in range(num_cameras):
      w, h = image_sizes[c]
      w = int(w / 10) + 1
      h = int(h / 10) + 1
      counts = np.zeros([h, w], dtype=np.int32)
      for f in range(num_frames):
      if len(image_points[c][f]):
      pos = np.floor(image_points[c][f] / 10).astype(np.int32)
      counts[pos[:,1], pos[:,0]] += 1
      vmax = np.max(counts)
      plt.figure()
      plt.imshow(counts, cmap='hot', interpolation='nearest',vmax=vmax)
      # Adding colorbar for reference
      plt.colorbar()
      plt.axis("off")
      savefile = "counts" + str(c) + ".png"
      print("Saving: " + savefile)
      plt.savefig(savefile, dpi=300, bbox_inches='tight')
      plt.close()

      左の例はうまく散らばっていないが、右の例はよりよく散らばったパターンを示している。

    3. 結果が妥当であることを確認するために再投影誤差を描画する。
    4. カメラ内部パラメータの真値が利用可能な場合、内部パラメータに対する推定誤差の可視化が提供される。

      x = np.linspace(0, width - 1, width)
      y = np.linspace(0, height - 1, height)
      # Generate the grid using np.meshgrid
      X, Y = np.meshgrid(x, y)
      points = np.concatenate([X[:,:,None], Y[:,:,None]], axis=2).reshape([-1, 1, 2])
      # Undistort the image points with the estimated distortions
      if models[cam] == cv.CALIB_MODEL_FISHEYE:
      points_undist = cv.fisheye.undistortPoints(points, Ks[cam],distortions[cam])
      else:
      points_undist = cv.undistortPoints(points, Ks[cam], distortions[cam])
      pt_norm = np.concatenate([points_undist, np.ones([points_undist.shape[0], 1, 1])], axis=2)
      # Distort the image points with the ground truth distortions
      if models[cam] == cv.CALIB_MODEL_FISHEYE:
      projected = cv.fisheye.projectPoints(pt_norm, np.zeros([3, 1]), np.zeros([3, 1]), Ks_gt[cam], distortions_gt[cam])[0]
      else:
      projected = cv.projectPoints(pt_norm, np.zeros([3, 1]), np.zeros([3, 1]), Ks_gt[cam], distortions_gt[cam])[0]
      errs_pt = np.linalg.norm(projected - points, axis=2)
      errs_pt = errs_pt.reshape([height, width])
      vmax = np.percentile(errs_pt, 95)
      plt.figure()
      plt.imshow(errs_pt, cmap='hot', interpolation='nearest',vmax=vmax)
      # Adding colorbar for reference
      plt.colorbar()
      savefile = "errors" + str(cam) + ".png"
      print("Saving: " + savefile)
      plt.savefig(savefile,dpi=300, bbox_inches='tight')

      結果として得られる可視化は のようになる。

  1. Multiview calibration
    1. apps/multiview-calibration/multiview_calibration.py 内の plotCamerasPosition を使うと、マルチビューキャリブレーションのために構築されたグラフを描画できる。これはカメラの位置、(ランダムなフレームの)チェッカーボード、ステレオキャリブレーションの初期段階で使われたタプルを明示的に示す黒線で結ばれたカメラのペアを表示する。灰色の破線は、最適化でも使われる非全域木の辺を表す。これらの線の太さは共視可能なフレーム数、すなわち接続の強さを示す。グラフの辺が密で太いほど望ましい。 右の木では、カメラ4の接続がかなり限定的であり、強化の余地がある。
    2. 再投影誤差を矢印(ある点から逆投影された点へ)で示す可視化メソッドが提供されている(apps/multiview-calibration/multiview_calibration.py 内の plotProjection を参照)。矢印の色は誤差値を強調する。さらに、タイトルにはそのフレームの平均誤差と、キャリブレーションに使われた他のフレームの中での精度が報告される。