Distance Measurement (C++)
単眼のカメラで、既知の大きさと色のカラーボールの距離を求めます。
カメラの内部パラメータはOpenCVの機能は使わず、簡単な実験で求めてみます。
1. はじめに
OpenCVを使い、単眼カメラで既知の物体(カラーボール)の距離を計測する方法を解説します。
今回は以下のような手順で物体の距離を計測します。
-
カメラの内部パラメータを求める。
-
単眼カメラを使って物体の距離を計測する。
-
画像から特定の色の画素を抽出する。
-
抽出した画像から円を検出する。
-
抽出した円の直径からカラーボールの距離を算出する。
-
4. カラーボールの距離計測
ボールの位置を計算するためには、画像上でのボールの大きさdが必要となります。
今回の特定の色のボールの距離を計測するので、 特定の色を抽出した後に、円検出を行いボールの大きさを求めます。
以下のball_detection.cppを作成してください。
■Windows OSの場合
ソリューションを作成したらここを参考に設定を行い、追加の依存ファイルに次の3つライブラリを追加してください。
*246は自分のOpenCVのバージョンに合わせて変更してください。
■ソリューションの構成がDebugの場合
・opencv_core246d.lib
・opencv_highgui246d.lib
・opencv_imgproc246d.lib
■ソリューションの構成がReleaseの場合
・opencv_core246.lib
・opencv_highgui246.lib
・opencv_imgproc246.lib
プロジェクトの設定が終わったら、以下のコードを記述します。
-
#include <opencv2/core/core.hpp>
-
#include <opencv2/highgui/highgui.hpp>
-
#include <opencv2/imgproc/imgproc.hpp>
-
#include <iostream>
-
-
#include "image_processing.h"
-
-
const unsigned short width = 640;
-
const unsigned short height = 480;
-
const double f_per_s = 636;
-
const double ball_radius = 10;
-
-
int main(int argc, char *argv[])
-
{
-
cv::VideoCapture cap(2);
-
cap.set(CV_CAP_PROP_FRAME_WIDTH, width);
-
cap.set(CV_CAP_PROP_FRAME_HEIGHT, height);
-
-
if(!cap.isOpened())
-
{
-
return -1;
-
}
-
-
cv::namedWindow("Capture", CV_WINDOW_AUTOSIZE|CV_WINDOW_FREERATIO);
-
-
hsv_threshold threshold = {100, 110, 200, 50};
-
-
cv::Mat frame;
-
cv::Mat binary;
-
-
float radius = 0.0f;
-
double z, x, y = 0.0;
-
cv::vector<cv::Vec3f> circles;
-
-
while(1)
-
{
-
cap >> frame;
-
-
binary = extract_color(frame, threshold);
-
-
cv::HoughCircles(binary, circles, CV_HOUGH_GRADIENT, 1, binary.rows/8, 200, 10, 0, 0);
-
-
if(circles.size() > 0)
-
{
-
cv::Point2f center(circles[0][0], circles[0][1]);
-
radius = circles[0][2];
-
-
z = ball_radius / radius * f_per_s;
-
y = (center.x - width / 2) * z / f_per_s;
-
x = (center.y - height / 2) * z / f_per_s;
-
-
std::cout << "Z: " << z << " X: " << x << " Y: " << y << std::endl;
-
-
cv::circle(frame, center, 3, cv::Scalar(0, 255, 0), -1, 8, 0);
-
cv::circle(frame, center, radius, cv::Scalar(0, 0, 255), 3, 8, 0);
-
}
-
-
cv::imshow("Capture", frame);
-
-
if(cv::waitKey(30) == 27)
-
{
-
cv::destroyAllWindows();
-
break;
-
}
-
}
-
-
return 0;
-
}
このプログラムはWebカメラから画像を取得し画像から特定の色を抽出し、円検出を行います。
色抽出に用いているextract_color関数についてはここをご覧ください。
-
6行目
extract_color関数や、hsv_threshold構造体を使用するためにimage_processing.hをインクルードします。
-
10行目
f/sの値を定義します。
-
11行目
検出する物体の半径を定義します。
-
26行目
カラーボールを色抽出するために、HSV色空間でのしきい値を設定します。
使用するカラーボールに合わせて設定してください。
-
31行目
検出したボールの半径を格納する変数を定義します。
-
32行目
計算したXYZ座標の位置を格納する変数を定義します。
-
33行目
今回使用するcv::HoughCircles関数は複数の円を検出します。
そこで、複数の円の情報を格納するためcv::vector<cv::Vec3f>型の変数を定義します。
-
39行目
extract_color関数を使って、キャプチャしたカメラ画像から特定の色を抽出します。
処理済みの画像 extract_color(画像, しきい値)
-
41行目
cv::HoughCircles(画像,
検出した円を出力するベクター,
方法, *現在、CV_HOUGH_GRADIENTのみ実装
画像分解能と投票分解能の比の逆数,
cv::Canny()関数に渡されるしきい値の大きい方の値, *小さい方は大きい方の半分になる
投票数のしきい値,
円の半径の最小値,
円の半径の最大値)
ハフ変換を用いてグレースケール画像から円を検出します。
投票数が多い順に検出した円をベクターに出力します。
-
43-56行目
円が1個以上検出されたら、座標の計算を行います。
今回は、最も投票数の多かった円のみ使用します。
最も得票数の多い円の各情報は以下のようにベクターの0番目に格納されています。
・0番目の円のu座標: circles[0][0]
・0番目の円のv座標: circles[0][1]
・0番目の円の半径: circles[0][2]
・48-50行目
式(2),(4),(5)に基本的に準じて、検出した円の半径からボールの位置を求めています。
実装上の工夫として、cv::HoughCircles関数は半径を返すので、半径を用いて計算しています。
また、ボールの中心座標であるcenterは画像の左上を原点としたuv座標系の値なので、
画像中心を原点としたxy座標系に変換する必要があります。
図4.1のように、画像中心からの位置(x,y)は、検出した円の中心座標から、画像の中心座標を引けば求めることができます。
5. 動作確認
作成したプログラムを実行して下さい。
下図のように、カラーボールが検出され、その座標がコンソール上にその座標が表示されれば完成です。
ぴったりではありませんが、特定の色の円が検出できていることがわかります。
いろいろな場所に置いてボールの位置が正しく検出できているか確認してみてください。
筆者の環境では円検出の揺らぎのため、たまに大きく外します が、おおよそZが ±5 cm程度で、X,Yは±2 cm程度の精度でした。
・54-55行目
検出した円の中心と、円の半径に従い、円を描写します。
6. おわりに
OpenCVを使って特定の色の円の位置を求めることができました。
画像処理では難しい数式が頻出しますが、条件を絞れば、今回のように中学生程度の数学で問題を解くことができます。
興味があれば参考文献やネットで、カメラキャリブレーションや3次元計測について調べてみてください。
OpenCVを使えば精度のよいカメラキャリブレーションを簡単に行うことができます。
参考文献
-
OpenCV2プログラミングブック制作チーム(2011)『OpenCV 2 プログラミングブック』マイナビ
-
徐 剛, 辻 三郎(1998)『3次元ビジョン』共立出版
-
「OpenCV 2.2 C++ リファレンス」<http://opencv.jp/opencv-2svn/cpp/index.html#>(2014/8/14アクセス)

5.1 カラーボールの検出結果
2. 単眼カメラを用いた物体の距離算出方法
単眼カメラを使って既知の物体の距離を算出する方法を説明します。
*今回の手法は小さい物体を十分遠い位置から計測すること仮定のもと、いくつかの近似を行っています。
まずは、物体の大きさとその距離によって、撮影された画像がどのように変化するのか考えます。
図2.1は物体とカメラの撮像面に映る像を真横から見たもので、焦点を原点とした物体と像の位置を表す図です。
これはピンホールカメラモデルと呼ばれるもので、中学校の理科で習う凸レンズとロウソクの問題にそっくりです。
この図2.1はカメラの焦点位置から距離 mmにある、直径 mmの物体をカメラで写すと、
焦点から焦点距離 mm離れた撮像面に、画像中心から高さ mmの像が映るということを表しています。
撮像面に映る像の大きさは図2.2のように、撮像素子1 pixelあたりの大きさs mmと像の画素数d pixelの掛け算で求めることができます。
図2.1の撮像面に写った像と焦点が作る三角形と、物体と焦点が作る三角形は相似になるため、以下のような式が導けます。
ds / f = D / Z -(1)
式(1)を以下のように変形します。
Z = (D / d) × (f / s) -(2)
式(2)の焦点距離と1画素の大きさはカメラに固有の値となります。
物体の大きさは既知とするので、f/sの値を事前に求めておくことで、像の画素数のみで物体の距離を求めることができます。
次に図2.3のように、物体の軸方向の位置を求めます。

2.1 物体の距離と像の 大きさの関係

2.2 撮像素子に映る物体のイメージ

2.3 物体の座標と像の関係
物体と像の中心座標が焦点となす三角形の相似に着目し以下の様な式が導けます。
xs / f = X / Z -(3)
式(3)を以下のように変形します。
X = xZ × (s / f) -(4)
式(4)からわかるように、f/sの値を事前に求めておけば、Zの値からXの値を計算することができます。
同様にしてYは以下のように求められます。
Y = yZ × (s / f) -(5)
求めた式(2),(4),(5)を用いて、焦点位置から見たボールの座標を計算します。
3. カメラの内部パラメータを求める
カメラの焦点位置Cを原点としたXYZ座標系での物体の位置を求めるには、
焦点距離fと撮像素子1 pixelあたりの大きさsを求める必要があることがわかりました。
このようなカメラに固有の値を、カメラの内部パラメータと呼びます。
まずはどのようにして、f/sを求めるのか説明します。
式(1)をf/sに注目して以下のように変形します。
f / s = dZ / D -(6)
式(6)より、大きさのわかっている物体を、ある位置Zに置いたときに画像上での画素数dを求めれば、f / sを計算できるとことがわかります。
今回は大まかな位置がわかれば良いことにし、簡単な実験でパラメータを求めてみましょう。
それでは実験方法を説明します。
まずは、図3.1のようにテーブルのような広い平面にWebカメラとボール、メジャーやスケールを配置します。
メジャーの原点はできる限り、カメラのレンズ部分に一致させてください。

3.1 実験環境の例
これでボールの位置を計測する準備ができたので、次は画像上にボールを映します。
OpenCVを使ってWebカメラの画像をキャプチャしましょう。
以下のsimple_calibration.cppを作成してください。
■Windows OSの場合
ソリューションを作成したらここを参考に設定を行い、追加の依存ファイルに次のライブラリを追加してください。
*246は自分のOpenCVのバージョンに合わせて変更してください。
■ソリューションの構成がDebugの場合
・opencv_core246d.lib
・opencv_highgui246d.lib
■ソリューションの構成がReleaseの場合
・opencv_core246.lib
・opencv_highgui246.lib
プロジェクトの設定が終わったら、以下のコードを記述します。
-
#include <opencv2/core/core.hpp>
-
#include <opencv2/highgui/highgui.hpp>
-
-
const unsigned short width = 640;
-
const unsigned short height = 480;
-
const int radius = 60;
-
-
int main(int argc, char *argv[])
-
{
-
cv::VideoCapture cap(0);
-
cap.set(CV_CAP_PROP_FRAME_WIDTH, width);
-
cap.set(CV_CAP_PROP_FRAME_HEIGHT, height);
-
-
if(!cap.isOpened())
-
{
-
return -1;
-
}
-
-
cv::namedWindow("Calibration", CV_WINDOW_AUTOSIZE|CV_WINDOW_FREERATIO);
-
cv::Mat frame;
-
-
while(1)
-
{
-
cap >> frame;
-
-
cv::circle(frame, cv::Point(width / 2, height / 2), radius, cv::Scalar(255, 0, 0), 1, 8, 0);
-
-
cv::imshow("Calibration", frame);
-
-
if(cv::waitKey(30) >= 0)
-
{
-
cv::destroyWindow("Calibration");
-
break;
-
}
-
}
-
return 0;
-
}
このプログラムは基本的はWebカメラから画像を取得し、画像中心に円を描画しているだけの単純なプログラムです。
Webカメラからの画像の取得に関してここをご覧ください。
-
1-2行目
OpenCVのヘッダファイルをインクルードします。
-
6行目
描画する円の半径を指定します。
指定する半径は使用するボールや環境に合わせて適せん変更してください。
-
26行目
画像中心にradiusで指定した半径の円を描画します。
このプログラムを実行すると画像の中心に円が描画されます。
図3.2のように描画された描画された円の中にぴったりと収まる位置にボールを移動して、そのときのカメラからの距離を計測してください。
特定の位置のときの画素数を求めるのではなく、特定の画素数のときの位置を求めています。

3.2 ボールと画像中の位置合わせ
私の環境で直径Dが20 mmの木製のボールを使って3回計測したところ、Zの平均は106 mmとなりました。
Zが求まったら、計測結果を式(6)に代入し、f/sを計算してください。
f / s = (60 ×2 ×106) / 20
= 636
これでカメラの内部パラメータf/sを求めることができました。
この値はカメラごとに異なりますが、一度求めればレンズと撮像素子の位置が変わらない限り有効です。
今回求めたパラメータはカメラの内部パラメータの一部で、他には撮像素子の座標軸のなす角度や、光軸の位置などがあります。
4.1 uv座標とxy座標の変換
