C++とOpenCVで眼球の動きを検出・追跡する方法をわかりやすく解説
はじめに
この記事では、OpenCVとC++を組み合わせて、Webカメラの映像から眼球の動きを検出し、その位置をリアルタイムに追跡する方法を解説します。顔認識でおなじみのHaarカスケード分類器と、円検出に有効なハフ変換(Hough Transform)を組み合わせることで、瞳の動きを安定的に捉えることができます。
処理の全体フロー
本プログラムは、以下のステップで眼球の検出・追跡を行います。
- 顔の検出: Haarカスケード分類器(haarcascade_frontalface_alt.xml)で映像から顔領域を検出します。
- 目の検出: 検出した顔領域に対して目用のカスケード(haarcascade_eye.xml)を適用し、両目の位置を特定します。
- 左目の選択: 検出された複数の目の中から、x座標が最も小さい(画面上で左側の)目を「左目」として選択します。
- 円候補の抽出: ハフ変換(HoughCircles)により、目の領域内に含まれる円の候補を複数検出します。
- 眼球(瞳孔)の判定: 各円の内部ピクセルの輝度値を合計し、最も暗い円を瞳孔(眼球)とみなします。
- ブレの抑制: 直近数フレームの中心座標の平均を取り、検出結果の揺らぎを平滑化してから描画します。
サンプルコード
以下が、眼球の検出と追跡を行う完全なプログラムです。
#include<iostream>
#include<opencv2/core/core.hpp>
#include<opencv2/highgui/highgui.hpp>
#include<opencv2/imgproc/imgproc.hpp>
#include<opencv2/objdetect/objdetect.hpp>
#include<string>
using namespace cv;
using namespace std;
// 各円候補の内部の輝度値を合計し、最も暗い円(瞳孔)を返す
Vec3f eyeBallDetection(Mat& eye, vector<Vec3f>& circles) {
vector<int> sums(circles.size(), 0);
for (int y = 0; y < eye.rows; y++) {
uchar* data = eye.ptr<uchar>(y);
for (int x = 0; x < eye.cols; x++) {
int pixel_value = static_cast<int>(*data);
for (int i = 0; i < circles.size(); i++) {
Point center((int)round(circles[i][0]), (int)round(circles[i][1]));
int radius = (int)round(circles[i][2]);
if (pow(x - center.x, 2) + pow(y - center.y, 2) < pow(radius, 2)) {
sums[i] = sums[i] + pixel_value;
}
}
++data;
}
}
int smallestSum = 9999999;
int smallestSumIndex = -1;
for (int i = 0; i < circles.size(); i++) {
if (sums[i] < smallestSum) {
smallestSum = sums[i];
smallestSumIndex = i;
}
}
return circles[smallestSumIndex];
}
// x座標が最小(最も左)の目を返す
Rect detectLeftEye(vector<Rect>& eyes) {
int leftEye = 99999999;
int index = 0;
for (int i = 0; i < eyes.size(); i++) {
if (eyes[i].tl().x < leftEye) {
leftEye = eyes[i].tl().x;
index = i;
}
}
return eyes[index];
}
vector<Point> centers;
Point track_Eyeball;
// 直近iterationフレーム分の中心座標の平均を取り、ブレを平滑化する
Point makeStable(vector<Point>& points, int iteration) {
float sum_of_X = 0;
float sum_of_Y = 0;
int count = 0;
int j = max(0, (int)(points.size() - iteration));
int number_of_points = points.size();
for (j; j < number_of_points; j++) {
sum_of_X = sum_of_X + points[j].x;
sum_of_Y = sum_of_Y + points[j].y;
++count;
}
if (count > 0) {
sum_of_X /= count;
sum_of_Y /= count;
}
return Point(sum_of_X, sum_of_Y);
}
void eyeDetection(Mat& frame, CascadeClassifier& faceCascade, CascadeClassifier& eyeCascade) {
Mat grayImage;
cvtColor(frame, grayImage, COLOR_BGR2GRAY);
equalizeHist(grayImage, grayImage);
// 顔の検出
Mat inputImage = grayImage;
vector<Rect> storedFaces;
float scaleFactor = 1.1;
int minimumNeighbour = 2;
Size minImageSize = Size(150, 150);
faceCascade.detectMultiScale(
inputImage, storedFaces, scaleFactor, minimumNeighbour, 0 | CASCADE_SCALE_IMAGE, minImageSize);
if (storedFaces.size() == 0) return;
Mat face = grayImage(storedFaces[0]);
// 検出した顔の周囲に矩形を描画
int x = storedFaces[0].x;
int y = storedFaces[0].y;
int h = y + storedFaces[0].height;
int w = x + storedFaces[0].width;
rectangle(frame, Point(x, y), Point(w, h), Scalar(255, 0, 255), 2, 8, 0);
// 目の検出
Mat faceRegion = face;
vector<Rect> eyes;
float eyeScaleFactor = 1.1;
int eyeMinimumNeighbour = 2;
Size eyeMinImageSize = Size(30, 30);
eyeCascade.detectMultiScale(faceRegion, eyes, eyeScaleFactor, eyeMinimumNeighbour, 0 | CASCADE_SCALE_IMAGE, eyeMinImageSize);
if (eyes.size() != 2) return;
// 検出した目の周囲に矩形を描画
for (Rect& eye : eyes) {
rectangle(frame, storedFaces[0].tl() + eye.tl(), storedFaces[0].tl() + eye.br(), Scalar(0, 255, 0), 2);
}
// 左目の取得
Rect eyeRect = detectLeftEye(eyes);
Mat eye = face(eyeRect);
equalizeHist(eye, eye);
// ハフ変換により目の領域内の円を検出
Mat hough_Circle_Input = eye;
vector<Vec3f> circles;
int method = 3;
int detect_Pixel = 1;
int minimum_Distance = eye.cols / 8;
int threshold = 250;
int minimum_Area = 15;
int minimum_Radius = eye.rows / 8;
int maximum_Radius = eye.rows / 3;
HoughCircles(hough_Circle_Input, circles,
HOUGH_GRADIENT, detect_Pixel, minimum_Distance, threshold, minimum_Area, minimum_Radius, maximum_Radius);
// 眼球を囲む円を描画
if (circles.size() > 0) {
Vec3f eyeball = eyeBallDetection(eye, circles);
Point center(eyeball[0], eyeball[1]);
centers.push_back(center);
center = makeStable(centers, 5);
track_Eyeball = center;
int radius = (int)eyeball[2];
circle(frame, storedFaces[0].tl() + eyeRect.tl() + center, radius, Scalar(0, 0, 255), 2);
circle(eye, center, radius, Scalar(255, 255, 255), 2);
}
cout << "The location of the eyeball is" << track_Eyeball << endl;
imshow("Eye", eye);
}
int main() {
// カスケード分類器の読み込み
CascadeClassifier faceCascade;
faceCascade.load("C:/opencv/sources/data/haarcascades/haarcascade_frontalface_alt.xml");
CascadeClassifier eyeCascade;
eyeCascade.load("C:/opencv/sources/data/haarcascades/haarcascade_eye.xml");
// カメラ映像の取得とeyeDetection関数の呼び出し
VideoCapture cap(0);
Mat frame;
while (1) {
cap >> frame;
eyeDetection(frame, faceCascade, eyeCascade);
imshow("Webcam", frame);
if (waitKey(30) >= 0) break;
}
return 0;
}主要関数の解説
eyeBallDetection()
ハフ変換で得られた各円候補について、円の内部に含まれる全ピクセルの輝度値を合計します。瞳孔は虹彩の中で最も暗い部分であるため、合計値が最小の円を眼球として採用するシンプルかつ効果的な手法です。
detectLeftEye()
目カスケードによって検出された複数の矩形の中から、左上隅のx座標が最も小さいもの(画像上で左側の目)を返します。これにより、常に同じ片方の目だけを追跡対象にできます。
makeStable()
フレームごとの検出結果には微小なブレが伴います。この関数は直近「iteration」フレーム分の中心座標の平均を計算することで、移動平均フィルタのように働き、表示される円のガタつきを抑えます。
eyeDetection()
グレースケール変換とヒストグラム均一化(equalizeHist)で照明条件の影響を軽減した後、顔検出→目検出→ハフ変換→描画までの一連の処理を実行するメインの処理関数です。
調整可能な主なパラメータ
- scaleFactor(1.1): 画像を段階的に縮小しながら走査する際の縮小率。小さくすると精度が上がりますが、処理時間が増加します。
- minimumNeighbour(2): 検出を確定するために必要な近傍矩形の最小数。大きくすると誤検出が減ります。
- HoughCircles の threshold(250): 円検出の感度を決める閾値。環境に応じて調整してください。
- minimum_Radius / maximum_Radius: 検出する円の半径範囲。目のサイズに合わせて設定することで、不要な円の検出を防ぎます。
実行時の注意点
- カスケードXMLファイルのパス(C:/opencv/sources/data/haarcascades/...)は、お使いのOpenCVのインストール環境に合わせて変更してください。
- プログラムは両目が正しく2つ検出された場合のみ処理を続行するため、正面を向いた明るい環境での使用を推奨します。
- コンソールには常時、追跡中の眼球中心座標が出力されます。
実行結果
プログラムを実行すると、カメラ映像内の顔にマゼンタ色の矩形、両目に緑色の矩形が描画され、さらに左目の瞳孔の位置に赤い円がリアルタイムに追従して表示されます。


-
C++とOpenCVで画像のチャンネル数を取得する方法を解説
この記事では、OpenCVを使って画像のチャンネル数を調べる方法について解説します。プログラムを実行すると、コンソール画面に画像のチャンネル数が表示されます。チャンネル数を取得するには、OpenCVが提供するchannels()メソッドを使用します。このメソッドは、Matクラスのオブジェクト(画像行列)に対して呼び出すことで、その画像のチャンネル数を整数値として返します。例えば、カラー画像の場合は通常3チャンネル(BGR)、グレースケール画像の場合は1チャンネルとなります。画像処理を行う際、扱っている画像がカラーかモノクロかを判別するために、チャンネル数の確認は非常に重要な処理の一つです。以下
-
C++とOpenCVで画像を読み込んで表示する方法を徹底解説
この記事では、C++でOpenCVを使用して画像を読み込み、表示する方法について詳しく解説します。OpenCVで画像の読み込みや表示を行うには、以下のような関数やデータ構造が必要になります。 画像処理に必要な基本要素 Mat(マトリクス) Matは関数ではなく、データ構造の一種である変数型です。C++におけるint型やchar型、string型のように、MatはOpenCV独自の変数型であり、画像を格納するための行列(マトリクス)データ構造を作成します。プログラム内で「Mat myImage;」と記述した場合、「myImage」という名前の行列変数を宣言していることになります。 name