【C++】OpenCVとカメラキャリブレーションで魚眼レンズ歪みを補正する手順とサンプルコード
C++とOpenCVなら魚眼画像を短いコードで補正できます。
チェスボードを写した複数枚の画像からcv::fisheye::calibrate
でカメラ行列Kと歪係数Dを取得し、取得後はcv::fisheye::undistortImage
に渡すだけで平坦化された画像が得られます。
リアルタイム実装も負荷が小さく、映像配信やロボット視覚にすぐ適用できます。
魚眼レンズ歪みの特徴
魚眼レンズは広い視野角を持つ特殊なレンズで、通常のレンズでは捉えきれない広範囲の映像を撮影できます。
しかし、その特性上、画像には独特の歪みが生じます。
魚眼レンズの歪みは主にラジアル歪みとタンジェンシャル歪みに分類され、それぞれの特徴を理解することが補正処理の第一歩となります。
ラジアル歪みとタンジェンシャル歪みの違い
魚眼レンズの歪みは、レンズの形状や光学設計に起因するもので、主に以下の2種類に分けられます。
ラジアル歪み(Radial Distortion)

ラジアル歪みは、画像の中心から放射状に発生する歪みで、魚眼レンズの特徴的な「樽型歪み(バレルディストーション)」がこれに該当します。
画像の中心から離れるほど、直線が曲線に変形し、被写体が膨らんで見える現象です。
ラジアル歪みは、以下のような数式で表されることが多いです。
ここで、
魚眼レンズの場合、これらの係数は大きく、強い歪みを生み出します。
タンジェンシャル歪み(Tangential Distortion)

タンジェンシャル歪みは、レンズとイメージセンサーの位置ずれや傾きによって生じる歪みです。
画像の直線が斜めにずれたり、非対称な歪みが発生したりします。
これは、レンズがセンサーに対して完全に平行でない場合に起こります。
タンジェンシャル歪みは以下の式で表されます。
ここで、
魚眼レンズの補正では、これらの歪みを正確にモデル化し、補正パラメータを推定することが重要です。
OpenCVのfisheye
モジュールでは、特にラジアル歪みを重視したモデルが採用されています。
視野角とイメージサークル
魚眼レンズの最大の特徴は、非常に広い視野角(Field of View, FoV)を持つことです。
一般的なレンズの視野角は60度から90度程度ですが、魚眼レンズは100度以上、場合によっては180度近くの視野角を実現します。
視野角の定義
視野角は、レンズがカバーできる撮影範囲の角度を示します。
水平視野角、垂直視野角、対角視野角の3種類がありますが、魚眼レンズでは特に対角視野角が広いことが特徴です。
イメージサークル(Image Circle)
イメージサークルとは、レンズがセンサー上に投影する円形の像の範囲を指します。
魚眼レンズは円形のイメージサークルを形成し、センサーの四隅まで映像が届くため、画像の四隅に強い歪みが現れます。
このイメージサークルの外側は黒くなることが多く、魚眼レンズ特有の「円形魚眼」画像が得られます。
補正処理では、このイメージサークルの範囲内で歪みを補正し、できるだけ自然な画像に変換することが目標です。
視野角と歪みの関係
視野角が広いほど、画像の周辺部での歪みが大きくなります。
魚眼レンズの補正では、視野角の広さに応じて補正パラメータを調整し、画像の中心から周辺まで均一に補正できるようにします。
魚眼レンズの歪みは、ラジアル歪みとタンジェンシャル歪みの複合的な影響で発生し、広い視野角と円形のイメージサークルが特徴です。
これらの特性を理解することで、OpenCVの魚眼補正機能を効果的に活用できます。
カメラキャリブレーションの流れ
チェスボードパターンの準備
カメラキャリブレーションには、正確な特徴点検出が可能なチェスボードパターンがよく使われます。
パターンの準備段階で適切なサイズや印刷品質を確保することが、キャリブレーションの精度向上につながります。
マス目サイズと余白設定
チェスボードのマス目サイズは、実際の物理的な大きさを正確に把握しておく必要があります。
OpenCVのキャリブレーション関数では、チェスボードの正方形の一辺の長さ(単位はミリメートルやセンチメートル)を入力パラメータとして使用します。
一般的には、マス目の一辺は20mmから30mm程度が扱いやすいです。
小さすぎると撮影時に特徴点がぼやけやすく、大きすぎると撮影範囲に収まりにくくなります。
また、チェスボードの周囲に余白を設けることも重要です。
余白がないと、画像の端でコーナー検出が困難になる場合があります。
余白は数ミリメートル程度で十分ですが、撮影時にパターン全体がフレームに収まるように調整してください。
用紙種類と印刷解像度
チェスボードパターンは、できるだけ高解像度で印刷することが望ましいです。
印刷解像度が低いと、マス目の境界が不鮮明になり、コーナー検出の精度が落ちます。
光沢のある用紙は反射が強くなりやすいため、マット紙や普通紙を選ぶと良いでしょう。
反射によるハイライトが特徴点検出の妨げになることがあります。
また、印刷後にパターンが歪んでいないか、平らな面に貼り付けて確認してください。
曲がったりシワが入ったりすると、キャリブレーション結果に悪影響を与えます。
撮影のコツ
キャリブレーション用のチェスボード画像は、さまざまな角度や距離から撮影することで、より正確なパラメータ推定が可能になります。
角度・距離のバリエーション
チェスボードをカメラに対して平行に撮影するだけでなく、斜めや回転させた状態でも撮影してください。
これにより、カメラの歪みや内部パラメータを多角的に推定できます。
距離も変化させることが重要です。
近距離から遠距離まで複数の距離で撮影し、焦点距離やズームの影響を考慮します。
撮影時は、チェスボードの全体がフレームに収まるようにしつつ、できるだけ多くのコーナーが鮮明に写るように調整してください。
照明条件の確保
均一で十分な明るさの照明環境を用意してください。
影や反射が強いと、チェスボードのコーナー検出が困難になります。
自然光が利用できる場合は、直射日光を避けて柔らかい光を当てると良いでしょう。
人工照明の場合は、複数の光源を使って影を減らす工夫が効果的です。
また、フラッシュ撮影は反射を強めるため避けることをおすすめします。
必要画像枚数の算出
キャリブレーションの精度は、撮影したチェスボード画像の枚数に大きく依存します。
一般的には、10枚から20枚程度の画像が推奨されます。
枚数が少ないと、パラメータ推定の信頼性が低下し、補正結果に誤差が生じやすくなります。
一方で、過剰に多い枚数は処理時間が増加するため、バランスが重要です。
また、撮影した画像はすべて同じ条件でなくても構いません。
角度や距離、照明条件が異なる多様な画像を用意することで、より堅牢なキャリブレーションが可能になります。
画像枚数 | 特徴・効果 |
---|---|
5枚未満 | 精度が低く、誤差が大きくなる可能性 |
10~20枚 | バランスが良く、安定した結果が得られる |
30枚以上 | 精度向上の余地はあるが処理負荷が増加 |
撮影した画像は、キャリブレーション処理の前にコーナー検出が成功しているか確認し、不良画像は除外することが望ましいです。
OpenCV fisheye モジュール概説
fisheye API と従来モデルの比較
OpenCVには魚眼レンズの歪み補正用に専用のfisheye
モジュールが用意されています。
従来のカメラキャリブレーションモデル(cv::calibrateCamera
など)と比較すると、fisheye
モジュールは魚眼レンズ特有の強いラジアル歪みをより正確に扱うために設計されています。
従来モデルは主にピンホールカメラモデルをベースにしており、歪みパラメータはラジアル歪み係数3つ(k1, k2, k3)とタンジェンシャル歪み係数2つ(p1, p2)で表現されます。
しかし、魚眼レンズのような広角レンズではこれらのパラメータだけでは十分に歪みを表現できません。
一方、fisheye
モジュールは4つのラジアル歪みパラメータ(k1, k2, k3, k4)を用い、タンジェンシャル歪みは考慮しません。
これは魚眼レンズの歪みが主にラジアル成分であるためです。
また、fisheye
モデルは非線形な投影モデルを採用しており、より広い視野角に対応可能です。
特徴 | 従来モデル (calibrateCamera) | fisheye モジュール (fisheye::calibrate) |
---|---|---|
歪みパラメータ | k1, k2, k3, p1, p2 | k1, k2, k3, k4 |
タンジェンシャル歪み | 対応あり | 対応なし |
対応視野角 | 約90度まで | 最大180度近く |
モデルの非線形性 | 低い | 高い |
補正精度(魚眼レンズ) | 低い | 高い |
このため、魚眼レンズの補正にはfisheye
モジュールの使用が推奨されます。
使用頻度の高い関数
cv::fisheye::calibrate
魚眼レンズの内部パラメータと歪み係数を推定する関数です。
チェスボードなどのキャリブレーションパターンの3D座標と対応する2D画像座標を入力として受け取り、カメラ行列(K)と歪み係数(D)を計算します。
主な引数は以下の通りです。
objectPoints
:3D空間上のキャリブレーションパターンの点群(通常はチェスボードのコーナーの実座標)imagePoints
:対応する画像上の2D点群imageSize
:画像のサイズK
:出力のカメラ行列(3×3)D
:出力の歪み係数(4×1)rvecs
、tvecs
:各画像の回転ベクトルと並進ベクトルflags
:キャリブレーションのオプション(例:cv::fisheye::CALIB_RECOMPUTE_EXTRINSIC
など)
戻り値はリプロジェクション誤差の平均値で、値が小さいほど精度が高いことを示します。
cv::fisheye::estimateNewCameraMatrixForUndistortRectify
補正後の画像のカメラ行列を計算する関数です。
元のカメラ行列と歪み係数をもとに、補正後の画像の視野角やスケールを調整できます。
主なパラメータは以下の通りです。
K
:元のカメラ行列D
:歪み係数imageSize
:画像サイズR
:回転行列(通常は単位行列)P
:出力の新しいカメラ行列balance
:視野角のバランス(0で画角を最大限維持、1で歪みを完全に除去)newSize
:補正後の画像サイズfov_scale
:視野角のスケール調整
この関数を使うことで、補正後の画像のトリミングや黒帯の発生をコントロールできます。
cv::fisheye::undistortImage
魚眼レンズの歪みを補正して、歪みのない画像を生成する関数です。
キャリブレーションで得られたカメラ行列と歪み係数を使い、入力画像の歪みを除去します。
主な引数は以下の通りです。
distorted
:歪みのある入力画像undistorted
:補正後の出力画像K
:カメラ行列D
:歪み係数Knew
:補正後のカメラ行列(省略可能)new_size
:補正後の画像サイズ(省略可能)
この関数は内部でマッピングを計算し、ピクセル単位で補正を行います。
Knew
を指定しない場合は元のカメラ行列が使われます。
cv::initUndistortRectifyMap と cv::remap
initUndistortRectifyMap
は、歪み補正と画像の整列(レクティファイ)に必要なマッピング行列を生成する関数です。
これにより、補正処理を高速化できます。
主な引数は以下の通りです。
K
:カメラ行列D
:歪み係数R
:回転行列(通常は単位行列)P
:新しいカメラ行列size
:画像サイズm1type
:マップの型(例:CV_32FC1
)map1
,map2
:出力のマップ行列
このマップを使って、remap
関数で画像のピクセルを変換します。
remap
はマップに従って入力画像のピクセルを補正後の位置に再配置し、補正画像を生成します。
この方法は、複数画像の補正や動画ストリームのリアルタイム処理に適しています。
マップを一度計算すれば、繰り返し使えるため処理効率が高いです。
キャリブレーションコード設計
入力画像の管理
キャリブレーション処理の最初のステップは、チェスボードパターンを撮影した複数の画像を適切に管理することです。
画像はファイル名の規則性を持たせて保存し、プログラム内でループ処理しやすい形にしておくと効率的です。
例えば、calib_01.jpg
、calib_02.jpg
のように連番で保存し、OpenCVのcv::imread
で順番に読み込みます。
画像の読み込みに失敗した場合はエラーハンドリングを行い、処理を中断またはスキップするようにします。
また、画像サイズは統一されていることが望ましく、異なる解像度の画像が混在するとキャリブレーションの精度に影響を与える可能性があります。
必要に応じてリサイズ処理を行うことも検討してください。
チェッカーボードコーナー検出
チェスボードのコーナー検出は、キャリブレーションの精度を左右する重要な処理です。
OpenCVのcv::findChessboardCorners
関数を使い、画像内のチェスボードの交点を検出します。
検出時のポイントは以下の通りです。
- チェスボードのサイズ(内側の交点数)を正確に指定すること。例えば、9×6のチェスボードなら
cv::Size(9, 6)
を指定します - 画像をグレースケールに変換してから検出を行うと精度が向上します
- 検出結果をサブピクセル精度に補正するために、
cv::cornerSubPix
を使用します。これにより、コーナー位置の精度が向上し、キャリブレーションの誤差が減少します
検出に成功した場合は、検出したコーナー座標を保存し、失敗した場合はその画像を除外するか再撮影を検討します。
パラメータ推定と保存
チェスボードの3D座標と検出した2Dコーナー座標を用いて、カメラの内部パラメータや歪み係数を推定します。
OpenCVのcv::fisheye::calibrate
関数を使い、以下のパラメータを取得します。
カメラ行列K
カメラ行列K
は、焦点距離や主点座標を含む3×3の行列です。
形式は以下のようになります。
:水平方向・垂直方向の焦点距離(ピクセル単位) :画像の主点(光学中心)の座標
この行列は、画像座標系とカメラ座標系の変換に使われます。
歪み係数D
歪み係数D
は4つの要素を持つベクトルで、魚眼レンズのラジアル歪みを表します。
これらの係数は、画像の歪みを補正するために使われます。
値はキャリブレーションの結果により異なり、強い歪みがある場合は大きな値になることがあります。
回転・平行移動ベクトル
各キャリブレーション画像に対して、カメラの姿勢を表す回転ベクトルrvecs
と平行移動ベクトルtvecs
も推定されます。
- 回転ベクトルはロドリゲスの回転ベクトル形式で表され、3次元空間での回転を示します
- 平行移動ベクトルはカメラの位置を示します
これらは主に外部パラメータとして、カメラとチェスボードの相対位置関係を表現します。
補正処理や3D再構成に利用されることがあります。
以下に、キャリブレーションの基本的なコード例を示します。
チェスボード画像の読み込みからコーナー検出、パラメータ推定までの流れを含みます。
#include <opencv2/opencv.hpp>
#include <opencv2/fisheye.hpp>
#include <iostream>
#include <vector>
int main() {
// チェスボードの内側コーナー数(横×縦)
cv::Size boardSize(9, 6);
// チェスボードの3D座標(Z=0の平面上)
std::vector<cv::Point3f> objectCorners;
for (int i = 0; i < boardSize.height; ++i) {
for (int j = 0; j < boardSize.width; ++j) {
objectCorners.push_back(cv::Point3f(j * 20.0f, i * 20.0f, 0)); // 20mm間隔
}
}
// 3D点群と2D点群の格納用
std::vector<std::vector<cv::Point3f>> objectPoints;
std::vector<std::vector<cv::Point2f>> imagePoints;
// 画像ファイル名のリスト(例)
std::vector<std::string> imageFiles = {
"calib_01.jpg", "calib_02.jpg", "calib_03.jpg", // 必要に応じて追加
};
cv::Size imageSize;
for (const auto& file : imageFiles) {
cv::Mat image = cv::imread(file);
if (image.empty()) {
std::cerr << "画像読み込み失敗: " << file << std::endl;
continue;
}
if (imageSize == cv::Size()) {
imageSize = image.size();
}
cv::Mat gray;
cv::cvtColor(image, gray, cv::COLOR_BGR2GRAY);
std::vector<cv::Point2f> corners;
bool found = cv::findChessboardCorners(gray, boardSize, corners,
cv::CALIB_CB_ADAPTIVE_THRESH | cv::CALIB_CB_NORMALIZE_IMAGE);
if (found) {
cv::cornerSubPix(gray, corners, cv::Size(11, 11), cv::Size(-1, -1),
cv::TermCriteria(cv::TermCriteria::EPS + cv::TermCriteria::COUNT, 30, 0.1));
imagePoints.push_back(corners);
objectPoints.push_back(objectCorners);
// 検出結果を表示
cv::drawChessboardCorners(image, boardSize, corners, found);
cv::imshow("Corners", image);
cv::waitKey(100);
} else {
std::cerr << "チェスボードコーナー検出失敗: " << file << std::endl;
}
}
cv::destroyAllWindows();
if (imagePoints.size() < 5) {
std::cerr << "十分な画像がありません。キャリブレーションを中止します。" << std::endl;
return -1;
}
cv::Mat K = cv::Mat::eye(3, 3, CV_64F);
cv::Mat D = cv::Mat::zeros(4, 1, CV_64F);
std::vector<cv::Mat> rvecs, tvecs;
int flags = cv::fisheye::CALIB_RECOMPUTE_EXTRINSIC |
cv::fisheye::CALIB_CHECK_COND |
cv::fisheye::CALIB_FIX_SKEW;
double rms = cv::fisheye::calibrate(objectPoints, imagePoints, imageSize, K, D, rvecs, tvecs, flags,
cv::TermCriteria(3, 100, 1e-6));
std::cout << "キャリブレーション完了。RMS誤差: " << rms << std::endl;
std::cout << "カメラ行列 K:\n" << K << std::endl;
std::cout << "歪み係数 D:\n" << D.t() << std::endl;
return 0;
}
このコードでは、チェスボードのコーナー検出に成功した画像のみを使ってキャリブレーションを行い、カメラ行列K
と歪み係数D
を推定しています。
rvecs
とtvecs
には各画像の回転・平行移動ベクトルが格納されます。
キャリブレーションの結果は、歪み補正や3D再構成に活用できます。
補正処理実装ポイント
魚眼レンズの歪み補正を実装する際には、補正後の画像品質や処理速度を考慮したパラメータ設定や最適化が重要です。
ここでは、新カメラ行列の生成オプションや視野角調整のパラメータ、そして高速化のためのマップキャッシュ技術について解説します。
新カメラ行列の生成オプション
魚眼レンズの補正では、元のカメラ行列K
と歪み係数D
をもとに、新しいカメラ行列Knew
を生成して補正画像の視野角やトリミングを調整します。
OpenCVのcv::fisheye::estimateNewCameraMatrixForUndistortRectify
関数を使うことで、補正後のカメラ行列を柔軟に設定可能です。
この関数の主なパラメータは以下の通りです。
K
:元のカメラ行列D
:歪み係数imageSize
:入力画像のサイズR
:回転行列(通常は単位行列)balance
:視野角のバランスを調整するパラメータ(0〜1)newSize
:補正後の画像サイズfov_scale
:視野角のスケール調整
balance
は補正後の画像における視野角のトレードオフを制御します。
0に近い値は元の視野角を最大限維持し、黒帯が多く残る傾向があります。
1に近い値は黒帯を減らしつつ画角を狭めるため、トリミングが強くなります。
fov_scale
は視野角のスケールをさらに細かく調整でき、1.0がデフォルトです。
値を小さくすると画角が狭まり、画像の端の歪みが減少します。
これらのパラメータを適切に設定することで、補正後の画像の見た目や利用シーンに合わせた最適な画角を得られます。
balance と fov_scale の調整
balance
とfov_scale
は補正画像のトリミングや黒帯の発生に大きく影響します。
実際のアプリケーションでは、これらの値を調整しながら最適な補正結果を探すことが多いです。
- balance = 0.0
- 視野角を最大限に保持
- 画像の周辺に黒帯が多く発生
- 映像の情報量は多いが、黒帯が気になる場合がある
- balance = 1.0
- 黒帯をほぼ除去
- 視野角が狭くなり、トリミングが強い
- 画面全体が有効画素で埋まる
- fov_scale
- 1.0が標準
- 0.8〜1.2の範囲で微調整可能
- 小さい値は画角を狭め、歪みを減らす
- 大きい値は画角を広げるが黒帯が増える可能性あり
これらのパラメータは、ユーザーインターフェースのスライダーなどでリアルタイムに調整できると便利です。
用途に応じて、視野角の広さと黒帯の有無をバランスよく設定してください。
高速化のためのマップキャッシュ
魚眼補正はピクセル単位での座標変換を伴うため、リアルタイム処理では高速化が重要です。
OpenCVでは、補正用のマッピング行列を事前に計算しキャッシュすることで、処理を効率化できます。
CPU版remap
cv::initUndistortRectifyMap
関数で補正マップを生成し、cv::remap
関数で画像を補正します。
マップは2つの行列map1
, map2
で表され、各ピクセルの変換先座標を格納しています。
この方法の利点は、マップ計算を一度だけ行い、複数の画像や動画フレームに対して繰り返し使えることです。
CPU処理でも十分高速ですが、解像度が高い場合やフレームレートが高い場合は負荷が大きくなります。
以下はCPU版のマップ生成と補正の例です。
cv::Mat map1, map2;
cv::fisheye::initUndistortRectifyMap(K, D, cv::Mat::eye(3,3,CV_64F), Knew, imageSize, CV_16SC2, map1, map2);
cv::remap(distortedImage, undistortedImage, map1, map2, cv::INTER_LINEAR);
GPUアクセラレーション (CUDA/GL)
OpenCVのCUDAモジュールやOpenGL連携を利用すると、GPU上でマップ生成やリマップ処理を高速に実行できます。
特に高解像度の動画ストリーム処理やリアルタイムアプリケーションで効果的です。
CUDA版では、cv::cuda::initUndistortRectifyMap
やcv::cuda::remap
を使い、GPUメモリ上で処理を完結させます。
これによりCPU負荷を大幅に軽減し、フレームレートの向上が期待できます。
ただし、GPU環境のセットアップやOpenCVのCUDA対応ビルドが必要であり、環境依存性が高い点に注意してください。
補正処理の実装では、新カメラ行列の生成パラメータを適切に設定し、balance
やfov_scale
で視野角と黒帯のバランスを調整します。
さらに、マップキャッシュを活用してremap
処理を高速化し、CPUまたはGPU環境に応じた最適な実装を選択することが重要です。
動画ストリームへの適用
魚眼レンズの歪み補正は静止画だけでなく、リアルタイムの動画ストリームにも適用されることが多いです。
ここでは、OpenCVのcv::VideoCapture
を使った動画処理の基本的なループ構造、fpsやレイテンシーの測定方法、そして複数カメラを扱うマルチカメラ対応について解説します。
cv::VideoCapture ループ構造
動画ストリームを扱う際は、cv::VideoCapture
クラスを使ってカメラや動画ファイルからフレームを連続的に取得します。
基本的な処理は以下のようなループ構造になります。
VideoCapture
オブジェクトを初期化し、カメラデバイスや動画ファイルを開きます。- ループ内で
cap.read(frame)
を呼び、フレームを取得。 - 取得したフレームに対して魚眼補正処理を実行。
- 補正後のフレームを表示または保存。
- キー入力を監視し、終了条件を判定。
以下は典型的なコード例です。
#include <opencv2/opencv.hpp>
#include <opencv2/fisheye.hpp>
#include <iostream>
int main() {
cv::VideoCapture cap(0); // カメラデバイス0を開く
if (!cap.isOpened()) {
std::cerr << "カメラを開けませんでした。" << std::endl;
return -1;
}
cv::Mat K, D; // キャリブレーションで得られたカメラ行列と歪み係数を事前に設定
// ここでは例として初期化(実際はキャリブレーション結果を読み込む)
K = (cv::Mat_<double>(3,3) << 300, 0, 320, 0, 300, 240, 0, 0, 1);
D = (cv::Mat_<double>(4,1) << -0.1, 0.01, 0, 0);
cv::Mat map1, map2;
cv::Size imageSize((int)cap.get(cv::CAP_PROP_FRAME_WIDTH), (int)cap.get(cv::CAP_PROP_FRAME_HEIGHT));
cv::fisheye::initUndistortRectifyMap(K, D, cv::Mat::eye(3,3,CV_64F), K, imageSize, CV_16SC2, map1, map2);
cv::Mat frame, undistorted;
while (true) {
if (!cap.read(frame)) {
std::cerr << "フレーム取得失敗" << std::endl;
break;
}
cv::remap(frame, undistorted, map1, map2, cv::INTER_LINEAR);
cv::imshow("Original", frame);
cv::imshow("Undistorted", undistorted);
if (cv::waitKey(1) == 27) { // ESCキーで終了
break;
}
}
cap.release();
cv::destroyAllWindows();
return 0;
}
このループでは、毎フレームremap
関数で補正を行い、オリジナルと補正後の映像を表示しています。
リアルタイム処理のため、マップは事前に計算しておくことが重要です。
fps とレイテンシー測定
動画処理の性能評価には、フレームレート(fps)とレイテンシー(遅延時間)の測定が欠かせません。
- fps(Frames Per Second)
1秒間に処理できるフレーム数を示します。
高fpsは滑らかな映像表示を意味し、リアルタイム性の指標となります。
- レイテンシー
カメラからフレーム取得後、補正処理を経て画面に表示されるまでの遅延時間です。
低レイテンシーは操作性や応答性に直結します。
fpsの計測は、一定時間内に処理したフレーム数をカウントし、秒数で割る方法が一般的です。
レイテンシーは高精度タイマー(例:cv::getTickCount
)を使い、処理開始から終了までの時間を計測します。
以下はfps計測の例です。
int frameCount = 0;
double startTime = (double)cv::getTickCount();
while (true) {
if (!cap.read(frame)) break;
// 補正処理
cv::remap(frame, undistorted, map1, map2, cv::INTER_LINEAR);
frameCount++;
double currentTime = (double)cv::getTickCount();
double elapsed = (currentTime - startTime) / cv::getTickFrequency();
if (elapsed >= 1.0) {
std::cout << "FPS: " << frameCount / elapsed << std::endl;
frameCount = 0;
startTime = currentTime;
}
cv::imshow("Undistorted", undistorted);
if (cv::waitKey(1) == 27) break;
}
レイテンシー測定は、フレーム取得直後と表示直前のタイムスタンプを比較して算出します。
マルチカメラ対応
複数の魚眼カメラを同時に扱う場合は、各カメラのキャリブレーションパラメータを個別に管理し、独立して補正処理を行う必要があります。
ポイントは以下の通りです。
- 複数の
cv::VideoCapture
オブジェクトを用意
カメラIDやデバイスパスを指定して複数のカメラを開きます。
- 各カメラごとにキャリブレーションパラメータを読み込み
カメラ行列K
、歪み係数D
、補正マップmap1
、map2
を個別に保持します。
- フレーム取得と補正を並列または順次処理
CPUリソースやスレッドを活用し、各カメラの映像をリアルタイムで補正します。
- 同期処理の検討
複数カメラ映像を同時に扱う場合、フレームのタイムスタンプを揃えるなど同期処理が必要になることがあります。
以下はマルチカメラ処理の簡単な例です。
std::vector<cv::VideoCapture> caps;
std::vector<cv::Mat> Ks, Ds, map1s, map2s;
std::vector<cv::Size> imageSizes;
// カメラ数
int cameraCount = 2;
// カメラ初期化とパラメータ読み込み(例)
for (int i = 0; i < cameraCount; ++i) {
caps.emplace_back(i);
if (!caps[i].isOpened()) {
std::cerr << "カメラ " << i << " を開けませんでした。" << std::endl;
return -1;
}
// キャリブレーションパラメータを読み込む処理をここに追加
// 例: Ks[i], Ds[i] にセット
cv::Size size((int)caps[i].get(cv::CAP_PROP_FRAME_WIDTH), (int)caps[i].get(cv::CAP_PROP_FRAME_HEIGHT));
imageSizes.push_back(size);
cv::Mat map1, map2;
cv::fisheye::initUndistortRectifyMap(Ks[i], Ds[i], cv::Mat::eye(3,3,CV_64F), Ks[i], size, CV_16SC2, map1, map2);
map1s.push_back(map1);
map2s.push_back(map2);
}
while (true) {
for (int i = 0; i < cameraCount; ++i) {
cv::Mat frame, undistorted;
if (!caps[i].read(frame)) {
std::cerr << "カメラ " << i << " のフレーム取得失敗" << std::endl;
continue;
}
cv::remap(frame, undistorted, map1s[i], map2s[i], cv::INTER_LINEAR);
cv::imshow("Camera " + std::to_string(i), undistorted);
}
if (cv::waitKey(1) == 27) break;
}
マルチカメラ環境では、処理負荷が増大するため、スレッド化やGPUアクセラレーションの活用も検討してください。
検証とデバッグ
魚眼レンズの歪み補正を正確に行うためには、キャリブレーション結果の検証と補正処理のデバッグが欠かせません。
ここでは、リプロジェクションエラーの確認方法、直線性評価の指標、そして補正前後の画像を重ねて比較する手法について詳しく解説します。
リプロジェクションエラーの確認
リプロジェクションエラーは、キャリブレーションの精度を評価する代表的な指標です。
これは、3D空間上の既知の点(チェスボードのコーナーなど)をカメラパラメータを使って2D画像上に投影し、その投影点と実際に検出された2D点との距離の平均値を示します。
具体的には、以下の手順で計算します。
- キャリブレーションで得られたカメラ行列
K
、歪み係数D
、回転ベクトルrvec
、平行移動ベクトルtvec
を用意します。 - 3Dのチェスボードコーナー座標を
cv::projectPoints
関数で2D画像座標に投影します。 - 投影点と実際に検出したコーナー点とのユークリッド距離を計算します。
- 全点の誤差の平均を算出し、リプロジェクションエラーとします。
リプロジェクションエラーが小さいほど、キャリブレーションの精度が高いことを示します。
一般的に、1ピクセル以下の誤差が望ましいとされています。
以下はリプロジェクションエラーを計算するサンプルコードです。
double computeReprojectionError(
const std::vector<std::vector<cv::Point3f>>& objectPoints,
const std::vector<std::vector<cv::Point2f>>& imagePoints,
const std::vector<cv::Mat>& rvecs,
const std::vector<cv::Mat>& tvecs,
const cv::Mat& K,
const cv::Mat& D)
{
double totalError = 0;
size_t totalPoints = 0;
for (size_t i = 0; i < objectPoints.size(); ++i) {
std::vector<cv::Point2f> projectedPoints;
cv::fisheye::projectPoints(objectPoints[i], projectedPoints, rvecs[i], tvecs[i], K, D);
double err = 0;
for (size_t j = 0; j < objectPoints[i].size(); ++j) {
double dx = imagePoints[i][j].x - projectedPoints[j].x;
double dy = imagePoints[i][j].y - projectedPoints[j].y;
err += std::sqrt(dx * dx + dy * dy);
}
totalError += err;
totalPoints += objectPoints[i].size();
}
return totalError / totalPoints;
}
この関数を使い、キャリブレーション後にリプロジェクションエラーを計測し、結果をログに出力することで精度を把握できます。
直線性評価メトリクス
魚眼レンズの歪み補正の効果を視覚的かつ定量的に評価するために、画像内の直線の「直線性」を測定する方法があります。
魚眼レンズの歪みは直線を曲げてしまうため、補正後に直線がどれだけ真っ直ぐに復元されているかが重要です。
直線性評価の手法
- 対象の直線を含む画像を用意
例えば、建物の壁や道路の白線など、明確な直線が写っている画像を使います。
- 直線上の点群を抽出
エッジ検出(Cannyなど)やHough変換で直線の点群を取得します。
- 直線フィッティング
点群に対して最小二乗法などで直線をフィッティングします。
- 点群とフィッティング直線の距離誤差を計算
各点から直線までの垂直距離を求め、その平均や最大値を評価指標とします。
補正前の画像では誤差が大きく、補正後の画像で誤差が小さくなることが理想です。
直線性評価の例
double evaluateLinearity(const std::vector<cv::Point>& points) {
cv::Vec4f line;
cv::fitLine(points, line, cv::DIST_L2, 0, 0.01, 0.01);
double errorSum = 0;
for (const auto& pt : points) {
double distance = std::abs((pt.x - line[2]) * line[1] - (pt.y - line[3]) * line[0]);
errorSum += distance;
}
return errorSum / points.size();
}
この関数で得られる値が小さいほど、点群が直線に近いことを示します。
補正前後のオーバーレイ比較
補正処理の効果を直感的に把握するために、補正前後の画像を重ね合わせて比較する方法があります。
これにより、歪みの変化や補正の正確さを視覚的に確認できます。
オーバーレイ比較の手順
- 補正前の画像と補正後の画像を同じサイズに揃えます。
- 画像の透明度(アルファ値)を調整し、片方の画像を半透明にします。
- 2つの画像を重ねて表示し、歪みの違いを視覚的に確認します。
OpenCVでは、cv::addWeighted
関数を使って簡単にオーバーレイ画像を作成できます。
cv::Mat overlay;
double alpha = 0.5; // 補正前画像の透明度
double beta = 1.0 - alpha; // 補正後画像の透明度
cv::addWeighted(originalImage, alpha, undistortedImage, beta, 0.0, overlay);
cv::imshow("Overlay Comparison", overlay);
cv::waitKey(0);
この方法は、補正前後の差異を一目で把握できるため、デバッグや調整作業に非常に有効です。
これらの検証・デバッグ手法を組み合わせることで、魚眼レンズのキャリブレーションと補正処理の精度を高め、信頼性の高い歪み補正を実現できます。
トラブルシューティング
魚眼レンズのキャリブレーションや歪み補正を行う際には、さまざまな問題が発生することがあります。
ここでは、よくあるトラブルとその対処法について解説します。
コーナー検出に失敗する場合
チェスボードのコーナー検出がうまくいかないと、キャリブレーションが正しく行えません。
以下のポイントを確認してください。
- 画像の解像度と鮮明さ
低解像度やピントが合っていない画像はコーナー検出が困難です。
撮影時に十分な解像度とピントを確保してください。
- 照明条件
影や反射、過度な明るさや暗さは検出精度を下げます。
均一で柔らかい照明環境を整え、反射を避けるためにマットなチェスボードを使用すると良いです。
- チェスボードのサイズ指定ミス
cv::findChessboardCorners
に渡すチェスボードの内側コーナー数(行×列)が実際のパターンと一致しているか確認してください。
間違っていると検出に失敗します。
- 画像の歪みが強すぎる
魚眼レンズの強い歪みでコーナーが大きく変形している場合、検出が難しくなります。
複数の角度や距離から撮影し、歪みの少ない画像も含めると改善します。
- 画像の前処理
グレースケール変換やヒストグラム均一化を行うと検出率が向上することがあります。
これらを試しても検出できない場合は、別のキャリブレーションパターン(例えばアプリケーションで使われるドットパターン)を検討するのも手です。
キャリブレーションが収束しない場合
キャリブレーション処理が収束せず、パラメータが安定しない場合は以下をチェックしてください。
- 入力データの品質
コーナー検出に成功した画像が十分に多く、多様な角度や距離から撮影されているか確認します。
枚数が少なすぎると収束しにくいです。
- パラメータの初期値やフラグ設定
cv::fisheye::calibrate
のフラグ設定が適切か確認します。
例えば、CALIB_FIX_SKEW
やCALIB_RECOMPUTE_EXTRINSIC
などのフラグを調整してみてください。
- チェスボードの物理サイズの誤り
3D座標として指定するチェスボードのマス目サイズが実際のサイズと異なると、パラメータ推定が不安定になります。
正確なサイズを入力してください。
- 画像の解像度の不一致
入力画像のサイズがキャリブレーション時に指定したサイズと異なると問題が生じます。
すべての画像が同じ解像度であることを確認してください。
- 外れ値の存在
コーナー検出に失敗した画像や誤検出が混入していると収束しにくくなります。
検出結果を目視で確認し、異常な画像は除外してください。
これらを見直しても収束しない場合は、キャリブレーション用の画像を再撮影し、条件を改善することが必要です。
補正後に歪みが残る場合
補正処理を行っても画像に歪みが残る場合、以下の原因が考えられます。
- キャリブレーション精度不足
リプロジェクションエラーが大きい場合、パラメータの推定が不十分です。
より多くの高品質な画像で再キャリブレーションを行いましょう。
- パラメータの誤用
補正時に使用するカメラ行列K
や歪み係数D
がキャリブレーション結果と異なっている、または誤っている可能性があります。
正しいパラメータを使っているか確認してください。
- 補正関数のパラメータ設定ミス
cv::fisheye::undistortImage
やinitUndistortRectifyMap
の引数設定が適切か見直します。
特に新カメラ行列Knew
やbalance
パラメータの設定が影響します。
- 視野角のトレードオフ
balance
パラメータを小さく設定すると視野角を広く保てますが、周辺部に黒帯や歪みが残ることがあります。
適切なバランスを探してください。
- 画像の解像度やサイズの不一致
補正時の画像サイズがキャリブレーション時と異なると、補正結果にズレが生じることがあります。
- 魚眼モデルの限界
極端に広角な魚眼レンズや特殊なレンズ設計の場合、OpenCVのfisheyeモデルで完全に補正できないこともあります。
その場合は専用の補正ソフトや別のモデルを検討してください。
これらのトラブルは、原因を一つずつ潰していくことで解決できます。
特にコーナー検出の成功率向上とキャリブレーション画像の品質管理が重要です。
問題が解決しない場合は、ログやエラーメッセージを詳細に確認し、パラメータや処理手順を見直してください。
実用例
魚眼レンズの歪み補正は、さまざまな分野で活用されています。
ここでは、ロボットナビゲーション、パノラマ生成、ドローン映像補正の3つの代表的な実用例について詳しく解説します。
ロボットナビゲーション
ロボットの自律移動や環境認識において、広い視野角を持つ魚眼レンズは非常に有用です。
魚眼カメラを搭載することで、ロボットは周囲の状況を広範囲に把握でき、障害物検知や経路計画の精度が向上します。
しかし、魚眼レンズ特有の歪みがそのままでは正確な距離計測や物体認識を妨げるため、歪み補正が必須です。
OpenCVのfisheyeモジュールを使って補正を行うことで、以下のような効果が得られます。
- 正確な距離推定
歪みのない画像で物体の位置を正確に把握できるため、障害物回避やマッピングの精度が向上します。
- 特徴点抽出の安定化
歪み補正により、特徴点検出やマッチングの誤差が減少し、SLAM(自己位置推定と地図作成)アルゴリズムの性能が向上します。
- 経路計画の信頼性向上
補正画像を使うことで、ロボットの周囲環境を正確にモデル化でき、安全な経路を計算しやすくなります。
実際の実装では、リアルタイムで補正処理を行いながら、カメラからの映像をナビゲーションアルゴリズムに入力します。
GPUアクセラレーションを活用すると、処理遅延を抑えつつ高精度な補正が可能です。
パノラマ生成
魚眼レンズは広い視野角を持つため、複数のカメラ映像や複数枚の画像をつなぎ合わせてパノラマ画像を作成する際に重宝されます。
しかし、魚眼レンズの歪みが補正されていないと、画像のつなぎ目にズレや歪みが生じ、自然なパノラマが作れません。
歪み補正を行うことで、以下のメリットがあります。
- 画像の幾何学的整合性向上
補正画像は直線が直線として表現されるため、画像間のマッチングが容易になります。
- シームレスなつなぎ合わせ
歪みのない画像同士を合成することで、境界部分の違和感や重なりのズレを減らせます。
- 広角パノラマの高品質化
魚眼レンズの広い視野角を活かしつつ、自然な見た目のパノラマを生成可能です。
OpenCVのStitcher
クラスなどと組み合わせて、補正済みの魚眼画像を入力することで、より高品質なパノラマ生成が実現します。
補正処理は事前に行い、パノラマ合成の前処理として組み込むのが一般的です。
ドローン映像補正
ドローンに搭載された魚眼カメラは、空撮や監視、測量など多様な用途で利用されています。
広い視野角により、ドローンの周囲環境を広範囲に捉えられますが、魚眼歪みが映像の解析や視認性を妨げることがあります。
歪み補正を適用することで、以下の効果が得られます。
- 映像の視認性向上
歪みのない映像は対象物の形状や位置を正確に把握しやすく、操縦者や解析システムの判断を助けます。
- 画像解析の精度向上
物体検出や追跡、地形解析などのアルゴリズムは、補正済み映像でより高い精度を発揮します。
- 測量やマッピングの信頼性向上
ドローンによる3Dマッピングや測量では、正確なカメラパラメータと補正が不可欠です。
補正画像を使うことで、誤差を減らし精度の高い地図作成が可能です。
実際の運用では、ドローンのリアルタイム映像ストリームに対してGPUを活用した高速補正を行い、操縦者のモニターや自動解析システムに補正済み映像を提供します。
これらの実用例は、魚眼レンズの広い視野角を活かしつつ、歪み補正によって映像の品質と解析精度を大幅に向上させる典型的なケースです。
OpenCVのfisheyeモジュールを活用することで、これらの分野で高精度かつ効率的な補正処理を実現できます。
パラメータ最適化のヒント
魚眼レンズの歪み補正においては、補正後の画像品質や視野角のバランスを取るためにパラメータの最適化が重要です。
ここでは、スケール調整と画角保持、アスペクト比とクロップ管理に関するポイントを解説します。
スケール調整と画角保持
補正後の画像のスケール調整は、視野角(Field of View, FoV)をどの程度保持するかに直結します。
OpenCVのcv::fisheye::estimateNewCameraMatrixForUndistortRectify
関数で設定できるbalance
やfov_scale
パラメータが主な調整要素です。
- balanceパラメータ
0から1の範囲で設定し、0に近いほど元の視野角を最大限に保持しますが、画像の周辺に黒帯(未補正領域)が多くなります。
1に近いと黒帯は減りますが、視野角が狭まりトリミングが強くなります。
- fov_scaleパラメータ
視野角のスケールを微調整するためのパラメータで、1.0がデフォルトです。
1より小さい値にすると画角が狭まり、歪みが少なくなりますが、情報量も減ります。
逆に1より大きい値は画角を広げますが、黒帯が増える可能性があります。
スケール調整のポイントは、用途に応じて視野角の広さと画像の見た目(黒帯の有無やトリミング量)をバランスよく設定することです。
例えば、ロボットナビゲーションでは視野角を広く保つことが重要ですが、映像の見やすさを優先する場合は黒帯を減らす設定が適しています。
アスペクト比とクロップ管理
補正後の画像は、元画像と異なるアスペクト比やサイズになることがあります。
これを適切に管理しないと、映像の歪みや表示の不自然さが生じます。
- アスペクト比の維持
補正時に画像サイズを変更すると、アスペクト比が変わる場合があります。
estimateNewCameraMatrixForUndistortRectify
のnew_size
パラメータで補正後の画像サイズを指定し、元のアスペクト比に近いサイズを選ぶことが望ましいです。
- クロップ(トリミング)
補正処理では、黒帯を除去するために画像の周辺がトリミングされることがあります。
トリミング量が多すぎると重要な情報が欠落するため、適切なバランスが必要です。
- 黒帯の扱い
黒帯が気になる場合は、balance
パラメータを調整してトリミングを増やすか、黒帯部分をマスク処理して視覚的に目立たなくする方法もあります。
- リサイズとスケーリング
補正後の画像を表示や解析に合わせてリサイズする際は、アスペクト比を維持したスケーリングを行い、画像の歪みを防ぎます。
これらの管理を適切に行うことで、補正後の画像が自然な見た目となり、後続の画像処理や解析に適した状態を保てます。
用途や表示環境に応じて、アスペクト比とクロップの設定を調整してください。
コード拡張アイデア
魚眼レンズの歪み補正コードは基本的な実装だけでなく、ユーザビリティや運用効率を高めるためにさまざまな拡張が考えられます。
ここでは、UIスライダーによるリアルタイム調整、JSON形式での設定保存と再利用、自動リバランススクリプトの3つのアイデアを詳しく解説します。
UI スライダーでリアルタイム調整
補正パラメータのbalance
やfov_scale
は、画像の見た目や視野角に大きく影響します。
これらをリアルタイムに調整できるUIを実装すると、ユーザーが最適な補正設定を直感的に見つけやすくなります。
OpenCVのcv::createTrackbar
を使うと、簡単にスライダーを作成してパラメータを動的に変更可能です。
スライダーの値に応じて補正マップを再計算し、補正画像を即座に更新します。
以下はbalance
パラメータをスライダーで調整する例です。
#include <opencv2/opencv.hpp>
#include <opencv2/fisheye.hpp>
#include <iostream>
cv::Mat distortedImage, undistortedImage;
cv::Mat K, D, map1, map2;
cv::Size imageSize;
int balanceSlider = 50; // 0〜100のスライダー値
void onTrackbar(int, void*) {
double balance = balanceSlider / 100.0;
cv::Mat Knew = cv::fisheye::estimateNewCameraMatrixForUndistortRectify(
K, D, imageSize, cv::Mat::eye(3,3,CV_64F), balance, imageSize, 1.0);
cv::fisheye::initUndistortRectifyMap(K, D, cv::Mat::eye(3,3,CV_64F), Knew,
imageSize, CV_16SC2, map1, map2);
cv::remap(distortedImage, undistortedImage, map1, map2, cv::INTER_LINEAR);
cv::imshow("Undistorted", undistortedImage);
}
int main() {
distortedImage = cv::imread("distorted.jpg");
if (distortedImage.empty()) {
std::cerr << "画像読み込み失敗" << std::endl;
return -1;
}
imageSize = distortedImage.size();
// 例として固定のカメラ行列と歪み係数
K = (cv::Mat_<double>(3,3) << 300, 0, imageSize.width/2, 0, 300, imageSize.height/2, 0, 0, 1);
D = (cv::Mat_<double>(4,1) << -0.1, 0.01, 0, 0);
cv::namedWindow("Undistorted", cv::WINDOW_AUTOSIZE);
cv::createTrackbar("Balance", "Undistorted", &balanceSlider, 100, onTrackbar);
onTrackbar(balanceSlider, nullptr);
cv::waitKey(0);
return 0;
}
このコードでは、スライダーを動かすとbalance
値が変わり、補正画像がリアルタイムに更新されます。
ユーザーは視野角と黒帯のバランスを直感的に調整可能です。
JSON 出力による設定再利用
キャリブレーションや補正パラメータは一度求めたら、再利用や共有がしやすい形式で保存しておくと便利です。
JSON形式は人間にも読みやすく、他のプログラムや言語からも扱いやすいためおすすめです。
パラメータとしては、カメラ行列K
、歪み係数D
、補正用パラメータ(balance
やfov_scale
など)をJSONに保存します。
C++ではnlohmann/json
などのライブラリを使うと簡単にJSONの読み書きが可能です。
例として、パラメータをJSONに保存するコードのイメージを示します。
#include <nlohmann/json.hpp>
#include <fstream>
void saveCalibrationToJson(const cv::Mat& K, const cv::Mat& D, double balance, const std::string& filename) {
nlohmann::json j;
j["K"] = std::vector<double>(K.begin<double>(), K.end<double>());
j["D"] = std::vector<double>(D.begin<double>(), D.end<double>());
j["balance"] = balance;
std::ofstream file(filename);
file << j.dump(4); // インデント4で整形出力
file.close();
}
void loadCalibrationFromJson(cv::Mat& K, cv::Mat& D, double& balance, const std::string& filename) {
std::ifstream file(filename);
nlohmann::json j;
file >> j;
std::vector<double> kVec = j["K"].get<std::vector<double>>();
std::vector<double> dVec = j["D"].get<std::vector<double>>();
balance = j["balance"].get<double>();
K = cv::Mat(kVec).reshape(1, 3);
D = cv::Mat(dVec).reshape(1, 4);
}
このように保存したJSONファイルを読み込むことで、補正パラメータを簡単に再利用でき、複数環境での共有やバージョン管理も容易になります。
自動リバランススクリプト
補正パラメータのbalance
やfov_scale
は手動で調整することも多いですが、画像の黒帯面積や視野角を自動で評価し、最適なパラメータを探索するスクリプトを作成すると効率的です。
自動リバランススクリプトの基本的な流れは以下の通りです。
- 複数の
balance
やfov_scale
の候補値を設定。 - 各候補値で補正マップを生成し、補正画像を作成。
- 補正画像の黒帯(黒いピクセル)の割合や画角の広さを計測。
- 黒帯が少なく、かつ画角が広いパラメータを最適解として選択。
黒帯の割合は、補正画像のピクセル値が黒(例:RGB=0,0,0)に近い画素数をカウントし、全画素数で割ることで算出できます。
以下は黒帯割合を計算する簡単な例です。
double calculateBlackAreaRatio(const cv::Mat& image) {
cv::Mat gray;
if (image.channels() == 3) {
cv::cvtColor(image, gray, cv::COLOR_BGR2GRAY);
} else {
gray = image;
}
int blackPixels = cv::countNonZero(gray < 10); // ほぼ黒とみなす閾値10
return static_cast<double>(blackPixels) / (gray.rows * gray.cols);
}
この関数を使い、複数のパラメータで補正画像を生成して評価し、最適なパラメータを自動的に決定できます。
これらの拡張アイデアを実装することで、魚眼レンズ補正の操作性や運用効率が大幅に向上します。
特にリアルタイム調整UIや設定のJSON管理は、開発や運用の現場で非常に役立つ機能です。
まとめ
本記事では、C++とOpenCVを用いた魚眼レンズの歪み補正について、特徴やキャリブレーションの流れ、OpenCVのfisheyeモジュールの使い方から実装のポイント、動画ストリームへの適用、検証・デバッグ方法、トラブルシューティング、実用例、パラメータ最適化、さらにコード拡張アイデアまで幅広く解説しました。
これにより、魚眼レンズ特有の強い歪みを正確に補正し、リアルタイム処理や多様な応用に対応できる技術が身につきます。