チェッカーボードを用いたカメラキャリブレーションプログラム(OpenCVとC++による実装)

今日はOpenCVを用いたカメラキャリブレーションをしてみたいと思います。

カメラキャリブレーションとは

カメラで撮影した画像というのは、実際の空間を正確に映し出しているとは限りません。これはどういうことかというと、例えばカメラにはレンズが入っているため、カメラで撮影した画像は実際の空間よりも歪んでいたり、色も実際の空間とはズレていたりします。
このようなカメラで撮影した画像を、実際の空間に落とし込むためにはそのズレを補正してあげる必要があります。

これを「キャリブレーション」と呼びます。

カメラキャリブレーションとは、実は非常に幅広く、わかりにくい言葉のように思います。何故なら幾何的情報に関するキャリブレーションや、光学的情報に関するキャリブレーションなど、様々なキャリブレーションが存在しているため、一言に「キャリブレーション」と言った場合に、どの部分のキャリブレーションを指すのかわからないことが多いためです。

今回は、1枚の撮影した画像に対して、カメラの内部パラメータを算出し、内部パラメータによるキャリブレーションを行ってみます。

今回の環境

・OS : Windows10(64bit)
・Visual Studio 2015 Community(導入済)
・OpenCV 2.4.11(環境構築済とする)

導入手順

OpenCVについては、今回は環境構築済としました。2.4.11なのでNuGetで入れても良いと思います。

OpenCVのカメラキャリブレーションでは、Zhangのカメラキャリブレーション手法という、キャリブレーションで最も有名(?)な論文の手法を用いています。

論文のタイトルは「A flexible new technique for camera calibration」です。

この手法は、チェッカーボードを複数の角度から撮影し、その画像を用いることによってキャリブレーションを実現する手法で、非常に簡便なため、カメラキャリブレーションのために非常によく使われています。

では、実際にキャリブレーションを行ってみましょう。

(1) キャリブレーションボードの作成

前述の通り、Zhangのカメラキャリブレーションにはキャリブレーション用のチェッカーボードが必要です。

ネットで検索すると、たぶんいろいろ出てくるので別に自分で作る必要はないのですが、今回は経験と思い、自分でPowerPointでチェッカーボードを作ってみました。

一応、画像化したものを掲載しておきます。

チェッカーボードのマスの数は7×10で、マスのサイズは25mmとしました。

これを印刷し、硬い板などに張り付けることでキャリブレーションボードを作ります。

ちょっと不格好ですが、取りあえずできました。糊で貼ったためか若干ヨレたりしてる気がするので、厳密にやりたいときにはしっかりとした板に張った方がよいと思います。

(2) キャリブレーションボードの撮影

次に、キャリブレーションボードの撮影を行っていきます。今回、自宅にカメラがなかったため、スマホのカメラで撮りました。

数十枚あると良いという話もありますが、今回は試しということで15枚撮影してみました。以下に撮影した例を掲載します。

      

いろんな角度のものがあるとよいと思われます。

(3) OpenCVを用いたキャリブレーションプログラムの実装

さて、いよいよコードを書いていきます。

今回は、OpenCVを用いたコードが既にインターネットに公開されておりますので、そちらを参考にさせていただきました。

引用:opencv.jp – OpenCV: カメラキャリブレーション(Camera Calibration)サンプルコード –

OpenCV2.4.11だとライブラリの構成とかが違って微妙に動かなかったため、そのあたりはコードを変更しています。

また、キャリブレーションに使うチェッカーボード画像を「calib_image」フォルダに、歪みを補正する画像を「before_image/test_before.jpg」、歪み補正後の画像(自動で保存される)の名前を「after_image/test_after.jpg」としています。ファイル名は随時変更してください。以下に一応プログラムを示します。

#pragma warning(disable:4996)

#include 
#include <opencv/cv.h>
#include <opencv/highgui.h>

#define IMAGE_NUM  (14)         /* 画像数 */
#define PAT_ROW    (6)          /* パターンの行数 */
#define PAT_COL    (9)         /* パターンの列数 */
#define PAT_SIZE   (PAT_ROW*PAT_COL)
#define ALL_POINTS (IMAGE_NUM*PAT_SIZE)
#define CHESS_SIZE (25.0)       /* パターン1マスの1辺サイズ[mm] */

int
main(int argc, char *argv[])
{
	int i, j, k;
	int corner_count, found;
	int p_count[IMAGE_NUM];
	IplImage *src_img[IMAGE_NUM];
	IplImage *before_img, *after_img;
	CvSize pattern_size = cvSize(PAT_COL, PAT_ROW);
	CvPoint3D32f objects[ALL_POINTS];
	CvPoint2D32f *corners = (CvPoint2D32f *)cvAlloc(sizeof(CvPoint2D32f) * ALL_POINTS);
	CvMat object_points;
	CvMat image_points;
	CvMat point_counts;
	CvMat *intrinsic = cvCreateMat(3, 3, CV_32FC1);
	CvMat *rotation = cvCreateMat(1, 3, CV_32FC1);
	CvMat *translation = cvCreateMat(1, 3, CV_32FC1);
	CvMat *distortion = cvCreateMat(1, 4, CV_32FC1);

	// (1)キャリブレーション画像の読み込み
	for (i = 0; i < IMAGE_NUM; i++) {
		char buf[32];
		sprintf(buf, "calib_image/CheckerBoard%02d.jpg", i);
		if ((src_img[i] = cvLoadImage(buf, CV_LOAD_IMAGE_COLOR)) == NULL) {
			fprintf(stderr, "cannot load image file : %s\n", buf);
		}
	}

	// (2)3次元空間座標の設定
	for (i = 0; i < IMAGE_NUM; i++) {
		for (j = 0; j < PAT_ROW; j++) {
			for (k = 0; k < PAT_COL; k++) {
				objects[i * PAT_SIZE + j * PAT_COL + k].x = j * CHESS_SIZE;
				objects[i * PAT_SIZE + j * PAT_COL + k].y = k * CHESS_SIZE;
				objects[i * PAT_SIZE + j * PAT_COL + k].z = 0.0;
			}
		}
	}
	cvInitMatHeader(&object_points, ALL_POINTS, 3, CV_32FC1, objects);

	// (3)チェスボード(キャリブレーションパターン)のコーナー検出
	int found_num = 0;
	cvNamedWindow("Calibration", CV_WINDOW_AUTOSIZE);
	for (i = 0; i < IMAGE_NUM; i++) {
		found = cvFindChessboardCorners(src_img[i], pattern_size, &corners[i * PAT_SIZE], &corner_count);
		fprintf(stderr, "%02d...", i);
		if (found) {
			fprintf(stderr, "ok\n");
			found_num++;
		}
		else {
			fprintf(stderr, "fail\n");
		}
		// (4)コーナー位置をサブピクセル精度に修正,描画
		IplImage *src_gray = cvCreateImage(cvGetSize(src_img[i]), IPL_DEPTH_8U, 1);
		cvCvtColor(src_img[i], src_gray, CV_BGR2GRAY);
		cvFindCornerSubPix(src_gray, &corners[i * PAT_SIZE], corner_count,
			cvSize(3, 3), cvSize(-1, -1), cvTermCriteria(CV_TERMCRIT_ITER | CV_TERMCRIT_EPS, 20, 0.03));
		cvDrawChessboardCorners(src_img[i], pattern_size, &corners[i * PAT_SIZE], corner_count, found);
		p_count[i] = corner_count;
		cvShowImage("Calibration", src_img[i]);
		cvWaitKey(0);
	}
	cvDestroyWindow("Calibration");

	if (found_num != IMAGE_NUM)
		return -1;
	cvInitMatHeader(&image_points, ALL_POINTS, 1, CV_32FC2, corners);
	cvInitMatHeader(&point_counts, IMAGE_NUM, 1, CV_32SC1, p_count);

	// (5)内部パラメータ,歪み係数の推定
	cvCalibrateCamera2(&object_points, &image_points, &point_counts, cvSize(640, 480), intrinsic, distortion);

	// (6)外部パラメータの推定
	CvMat sub_image_points, sub_object_points;
	int base = 0;
	cvGetRows(&image_points, &sub_image_points, base * PAT_SIZE, (base + 1) * PAT_SIZE);
	cvGetRows(&object_points, &sub_object_points, base * PAT_SIZE, (base + 1) * PAT_SIZE);
	cvFindExtrinsicCameraParams2(&sub_object_points, &sub_image_points, intrinsic, distortion, rotation, translation);

	// (7)XMLファイルへの書き出し
	CvFileStorage *fs;
	fs = cvOpenFileStorage("./camera.xml", 0, CV_STORAGE_WRITE);
	cvWrite(fs, "intrinsic", intrinsic);
	cvWrite(fs, "rotation", rotation);
	cvWrite(fs, "translation", translation);
	cvWrite(fs, "distortion", distortion);
	cvReleaseFileStorage(&fs);

	for (i = 0; i < IMAGE_NUM; i++) {
		cvReleaseImage(&src_img[i]);
	}

	// (8)歪み補正
	before_img = cvLoadImage("before_image/test_before.jpg", CV_LOAD_IMAGE_COLOR);
	if (!before_img) {
		fprintf(stderr, "Error!\n");
		return 0;
	}
	after_img = cvCloneImage(before_img);
	cvUndistort2(before_img, after_img, intrinsic, distortion);

	// (9)画像の表示 
	cvNamedWindow("Distortion", CV_WINDOW_AUTOSIZE);
	cvShowImage("Distortion", before_img);
	cvNamedWindow("UnDistortion", CV_WINDOW_AUTOSIZE);
	cvShowImage("UnDistortion", after_img);
	cvWaitKey(0);

	cvDestroyWindow("Distortion");
	cvDestroyWindow("UnDistortion");

	// (10)画像の保存
	cvSaveImage("after_image/test_after.jpg", after_img);

	return 0;
}

(4) プログラムビルド、実行

上記のプログラムを実行してみました。

私は最初、

#define PAT_ROW    (7)          /* パターンの行数 */
#define PAT_COL    (10)         /* パターンの列数 */

にしていて嵌りました。

チェッカーボードの白黒のタイルの数ではなく、上にはタイル同士が交差する部分の数を入れないといけないのですね(当たり前ですが……)。

ちゃんと

#define PAT_ROW    (6)          /* パターンの行数 */
#define PAT_COL    (9)         /* パターンの列数 */

としてキャリブレーションすると、以下のような画像が表示されました。

15枚試したのですが、1枚だけ失敗してしまいました。

少々角度がキツかったのか、上手くコーナーを認識できなかったようです。

上記プログラムは1枚でも失敗するとプログラムが終了する仕様となっていますので、その画像を除いて14枚としました。

全ての画像でコーナー検出が上手くいくと、「camera.xml」ファイルが吐き出されます。その中身にキャリブレーションで得たパラメータが入っています。

<?xml version="1.0"?>
<opencv_storage>
<intrinsic type_id="opencv-matrix">
 <rows>3</rows>
 <cols>3</cols>
 <dt>f</dt>
 <data>
 6.18665894e+02 0. 3.62660736e+02 0. 6.13982422e+02 2.58364075e+02 0.
 0. 1.</data></intrinsic>
<rotation type_id="opencv-matrix">
 <rows>1</rows>
 <cols>3</cols>
 <dt>f</dt>
 <data>
 -2.15892005e+00 2.15738225e+00 -1.42868593e-01</data></rotation>
<translation type_id="opencv-matrix">
 <rows>1</rows>
 <cols>3</cols>
 <dt>f</dt>
 <data>
 8.82337189e+01 7.57496872e+01 3.84934601e+02</data></translation>
<distortion type_id="opencv-matrix">
 <rows>1</rows>
 <cols>4</cols>
 <dt>f</dt>
 <data>
 6.87919557e-02 6.81028590e-02 -2.52288370e-03 1.07023362e-02</data></distortion>
</opencv_storage>

intrinsicがカメラの内部パラメータ(カメラマトリックス)、distortionが歪係数ベクトルで、rotationとtranslationは外部パラメータなので、今回はIntrinsicとdistortionのみを使います。

これを「cvUndistort2」関数に投げることで、対象画像の歪みを補正することができます。

キャリブレーションを行った画像と入力画像の解像度が異なる場合は、歪み係数はそのままの値で問題ないですが、intrinsicには調整が必要な点には注意してください。

極力、同じ解像度でキャリブレーションした方が楽だと思います。

(5) 結果

このパラメータを使って、キャリブレーションを行ってみました。

【実施前】

【実施後】

今一つ上手くいっているのかわかりませんが、確かに画像は補正されているような気はします。

まとめ

今回はOpenCVで内部カメラパラメータを算出し、カメラキャリブレーションをしてみました。非常に簡単にキャリブレーションができるので、是非ご活用ください。

スポンサーリンク

シェアする

フォローする