OpenCV を使ったビデオオブジェクトトラッキング

C++Beginner
オンラインで実践に進む

はじめに

この実験では、OpenCV を使ってビデオオブジェクトトラッキングを実装します。
このプロジェクトを学ぶ前に、「C++ で太陽系を作る」コースを終了しておく必要があります。

学ぶこと

  • C++ の基本
  • g++ の基本
  • 画像表現
  • OpenCV の応用
  • ミーンシフトとカムシフトアルゴリズム

最終結果

この実験では、太陽系の惑星を追跡できるプログラムを実装します。(次の画像では、黄色い軌道から木星を選択しています。追跡されたオブジェクトは赤い食でマークされています):

画像の説明

このプロジェクトを書く前に、「C++ で太陽系を作る」コースを終了しておく必要があります。

ビデオファイルを作成する

LabEx 環境では、カメラ環境をサポートしていません。したがって、プロジェクト用にビデオファイルを作成する必要があります。

ビデオ録画ツールをインストールしましょう:

sudo apt-get update && sudo apt-get install gtk-recordmydesktop

インストール後、アプリケーションメニューから録画ソフトウェアを見つけることができます:

画像の説明

次に、太陽系プログラム ./solarsystem を実行し、RecordMyDesktop を使ってデスクトップ画面を録画し(10〜30 秒で大丈夫)、~/Code/camshiftvideo という名前で保存します:

画像の説明

録画を終了したいときは、画面右下の 停止 ボタンをクリックします。すると、video.ogv ファイルが生成されます:

画像の説明

デジタル画像の基礎

OpenCV はオープンソースのクロスプラットフォームコンピュータビジョンライブラリです。OpenGL の画像レンダリングとは異なり、OpenCV は画像処理やコンピュータビジョンにおける多くの一般的なアルゴリズムを実装しています。OpenCV を学ぶ前に、コンピュータ内の画像やビデオのいくつかの基本概念を理解する必要があります。

まず、コンピュータ内で画像がどのように表現されているかを理解する必要があります。画像を保存する一般的な方法には 2 つあります。1 つはベクトルマップ、もう 1 つはピクセルマップです。

ベクトルマップでは、画像は数学的に線で結ばれた一連の点として定義されます。ベクトルマップファイル内のグラフィック要素はオブジェクトと呼ばれます。すべてのオブジェクトは独立したエンティティであり、色、形状、輪郭、サイズ、画面位置などのプロパティを持っています。

もっと一般的なのはピクセルマップです。たとえば、画像のサイズはよく 1024×768 です。これは、画像が水平方向に 1024 ピクセル、垂直方向に 768 ピクセルを持っていることを意味します。

ピクセルはピクセルマップの基本単位です。通常、ピクセルは 3 原色(赤、緑、青)の混合です。コンピュータの本質は数の認識であるため、通常、0 から 255 までの明るさで原色を表現します。言い換えると、赤色については、0は最も暗い、つまりを意味し、255は最も明るい、つまり純赤色を意味します。

したがって、ピクセルは 3 つ組 (R,G,B) として表現でき、(255,255,255) と表現でき、(0,0,0) です。このような画像をRGB 色空間の画像と呼びます。RGB は画像の 3 つのチャネルになります。RGB 色空間以外にも、HSVYCrCb など、他にも多くの色空間があります。

ピクセルがピクセルマップに対するものと同じように、画像はビデオの基本単位です。ビデオは一連の画像で構成されており、それぞれの画像をフレームと呼びます。通常、ビデオのフレームレートとは、このビデオが 1 秒間にこれだけのフレーム画像を含んでいることを意味します。たとえば、フレームレートが 25 の場合、このビデオは 1 秒間に 25 フレームを再生します。

1 秒間に 1000 ミリ秒があり、フレームレートが rate であるとすると、フレーム画像間の時間間隔は 1000/rate になります。

画像のカラーヒストグラム

カラーヒストグラムは画像を記述するためのツールです。通常のヒストグラムと似ていますが、カラーヒストグラムは特定の画像から計算する必要があります。

画像がRGB 色空間にある場合、R チャネル の各色の出現回数を数えることができます。これにより、256 の長さの配列(色確率ルックアップテーブル)を取得できます。画像のピクセルの総数(幅×高さ)ですべての値を同時に割り、得られたシーケンスをヒストグラムに変換します。結果はR チャネル のカラーヒストグラムになります。同様に、G チャネルB チャネル のヒストグラムを作成できます。

ヒストグラムのバックプロジェクション

RGB 色空間では、ヒストグラムが照明の変化に敏感であることが証明されています。この変化がトラッキング効果に与える影響を軽減するために、ヒストグラムのバックプロジェクションが必要です。このプロセスは 3 つのステップに分かれます。

  1. まず、画像を RGB 空間から HSV 空間に変換します。
  2. 次に、H チャネルのヒストグラムを計算します。
  3. 画像の各ピクセルの値を、色確率ルックアップテーブルの対応する確率で置き換えて、色確率分布マップを取得します。

このプロセスはバックプロジェクションと呼ばれ、色確率分布マップはグレースケール画像です。

OpenCV の基礎

まず OpenCV をインストールする必要があります:

sudo apt update
sudo apt-get install libopencv-dev

C++ の基本的な構文は既に知っていると仮定します。ほとんどのプログラムでは、ヘッダーファイル #include <iostream>using namespace std; または std::cout が使用されます。OpenCV にも独自の名前空間があります。

OpenCV を使用するには、次のヘッダーファイルを含めるだけです:

#include <opencv2/opencv.hpp>

そして:

using namespace cv;

で OpenCV 名前空間を有効にします(またはすべての API に直接 cv:: 接頭辞を使用します)。

これが初めて OpenCV を使用する場合、OpenCV のインターフェイスに慣れていないかもしれません。そのため、OpenCV API を学ぶ際には cv:: 接頭辞を使用することをお勧めします。

録画したビデオを読み込む最初のプログラムを書いてみましょう:

//
// main.cpp
//
#include <opencv2/opencv.hpp> // OpenCV ヘッダーファイル

int main() {

    // ビデオキャプチャオブジェクトを作成
    // OpenCV は VideoCapture オブジェクトを提供しており、
    // ファイルからのビデオ読み取りはカメラからの読み取りと同じように扱われます。
    // 入力パラメータがファイルパスの場合、ビデオファイルを読み取ります。
    // カメラの識別番号(通常は 0)の場合、カメラを読み取ります。
    cv::VideoCapture video("video.ogv"); // ファイルから読み取り
    // cv::VideoCapture video(0);        // カメラから読み取り

    // 読み取った画像フレーム用のコンテナ、OpenCV の Mat オブジェクト
    // OpenCV のキークラスは Mat で、これは行列を意味します。
    // OpenCV は行列を使って画像を記述します。
    cv::Mat frame;
    while(true) {

        // ビデオデータを frame に書き込む、>>は OpenCV によってオーバーライドされます。
        video >> frame;

        // フレームがない場合、ループを終了する
        if(frame.empty()) break;

        // 現在のフレームを表示する
        cv::imshow("test", frame);

        // ビデオのフレームレートは 15 なので、スムーズに再生するために 1000/15 待つ必要があります。
        // waitKey(int delay) は OpenCV の待機関数です。
        // この時点で、プログラムはキーボード入力を待つために `delay` ミリ秒待ちます。
        int key = cv::waitKey(1000/15);

        // キーボードの ESC ボタンを押したときにループを終了する
        if (key == 27) break;
    }
    // メモリを解放する
    cv::destroyAllWindows();
    video.release();
    return 0;

}

この main.cpp ファイルを ~/Code/camshift にある video.ogv と同じフォルダに置き、プログラムをコンパイルします:

g++ main.cpp `pkg-config opencv --libs --cflags opencv` -o  main

プログラムを実行すると、ビデオが再生されるのが見えます:

./main
画像の説明

注意

次のエラーが表示される場合があります:

libdc1394 error: Failed to initialize libdc1394

これは OpenCV のバグであり、実行には影響しません。

この問題を解消したい場合は、プログラムを実行する前に次のコードを実行できます:

sudo ln /dev/null /dev/raw1394

ミーンシフトとキャムシフトアルゴリズム

  • ミーンシフト
  • カムシフト
  • トラッキング対象を選択するためにマウスコールバックイベントを設定する
  • ビデオストリームから画像を読み取る
  • カムシフトを実装する

ミーンシフト

ミーンシフトアルゴリズムとカムシフトアルゴリズムは、オブジェクトトラッキングの 2 つの古典的なアルゴリズムです。カムシフトはミーンシフトに基づいています。それらの数学的解釈は複雑ですが、基本的な考え方は比較的単純です。ですから、それらの数学的事実を飛ばし、まずミーンシフトアルゴリズムを紹介します。

画面上に赤い点の集合があると仮定します。青い円(ウィンドウ)は、最も密度の高い領域(または点の数が最も多い場所)にある点に移動する必要があります:

画像の説明

上の画像のように、青い円を C1 とし、円の中心を C1_o とします。しかし、この円の重心は C1_r で、青い実線の円として示されています。

C1_oC1_r が重ならない場合、円 C1 を繰り返して円 C1_r の中心に移動させます。最終的には、最も密度の高い円 C2 の上に留まります。

画像処理では、通常、画像のバックプロジェクトヒストグラムを使用します。トラッキング対象が移動するとき、この移動プロセスはバックプロジェクトヒストグラムによって反映されることは明らかです。したがって、ミーンシフトアルゴリズムは最終的に選択したウィンドウを移動対象の位置に移動させます。(アルゴリズムは最終的に収束することが証明されています。)

キャムシフト

前の説明の後、ミーンシフトアルゴリズムは常に固定されたウィンドウサイズを追跡することがわかりました。これは私たちのニーズに合っていません。なぜなら、ビデオでは、対象オブジェクトが必ずしも大きくないからです。

そこで、この問題を改善するためにカムシフトが作られました。これは、カムシフトの連続的な適応型ミーンシフトからもわかります。

その基本的な考え方は、まずミーンシフトアルゴリズムを適用することです。ミーンシフトの結果が収束すると、カムシフトはウィンドウサイズを更新し、ウィンドウに合う方向楕円を計算し、その後、楕円を新しいウィンドウとして適用してミーンシフトアルゴリズムを適用します。

OpenCV は、カムシフトアルゴリズムに対する汎用的なインターフェイスを提供しています:

RotatedRect CamShift(InputArray probImage, Rect& window, TermCriteria criteria)

最初のパラメータである probImage は、対象ヒストグラムのバックプロジェクションです。2 番目のパラメータである window は、カムシフトアルゴリズムの検索ウィンドウです。3 番目のパラメータである criteria は、アルゴリズムの終了条件です。

分析

カムシフトアルゴリズムの基本的な考え方を理解した後、このコードの実装は主に以下のいくつかのステップに分かれることがわかります:

  1. トラッキング対象を選択するためにマウスコールバックイベントを設定する。
  2. ビデオストリームから画像を読み取る。
  3. カムシフトプロセスを実装する。

以下では、main.cpp のコードを続けて修正します。

マウスコールバック関数による追跡対象の選択

OpenCV は OpenGL とは異なります。マウスコールバック関数には 5 つのパラメータが指定されています。最初の 3 つが最も必要となるものです:event の値を通じて、マウスの左ボタンが押されたイベント (CV_EVENT_LBUTTONDOWN)、マウスの左ボタンが離されたイベント (CV_EVENT_LBUTTONUP) などを取得できます:

bool selectObject = false; // オブジェクトが選択されているかどうかを表す
int trackObject = 0;       // 1 はトラッキング対象があることを意味し、0 は対象がないことを意味し、-1 はカムシフト特性が計算されていないことを意味します
cv::Rect selection;        // マウスによって選択された領域を保存する
cv::Mat image;             // ビデオからのキャッシュ画像

// OpenCV からのマウスのコールバック関数:
// void onMouse(int event, int x, int y, int flag, void *param)
// 4 番目のパラメータ `flag` は追加の状態を表し、
// param はユーザーパラメータを意味しますが、これらは必要ないので、名前は付けません。
void onMouse( int event, int x, int y, int, void* ) {
    static cv::Point origin;
    if(selectObject) {
        // 選択された高さと幅、左上隅の位置を決定する
        selection.x = MIN(x, origin.x);
        selection.y = MIN(y, origin.y);
        selection.width = std::abs(x - origin.x);
        selection.height = std::abs(y - origin.y);

        // &は cv::Rect によって上書きされます
        // 2 つの領域の交差を意味します
        // ここでの主な目的は、選択領域外の領域を処理することです
        selection &= cv::Rect(0, 0, image.cols, image.rows);
    }

    switch(event) {
            // 左ボタンが押されたときの処理
        case CV_EVENT_LBUTTONDOWN:
            origin = cv::Point(x, y);
            selection = cv::Rect(x, y, 0, 0);
            selectObject = true;
            break;
            // 左ボタンが離されたときの処理
        case CV_EVENT_LBUTTONUP:
            selectObject = false;
            if( selection.width > 0 && selection.height > 0 )
                trackObject = -1; // トラッキング対象のカムシフト特性が計算されていない
            break;
    }
}

ビデオストリーミングから画像を読み込む

ビデオストリームを読み取る構造を実装しました。もう少し詳細を書きましょう:

int main() {
    cv::VideoCapture video("video.ogv");
    cv::namedWindow("CamShift at LabEx");

    // 1. マウスイベントコールバックを登録する
    cv::setMouseCallback("CamShift at LabEx", onMouse, NULL);

    cv::Mat frame;

    // 2. ビデオから画像を読み取る
    while(true) {
        video >> frame;
        if(frame.empty()) break;

        // キャッシュ用に frame の画像をグローバル変数 image に書き込む
        frame.copyTo(image);

        // オブジェクトを選択している場合、四角形を描画する
        if( selectObject && selection.width > 0 && selection.height > 0 ) {
            cv::Mat roi(image, selection);
            bitwise_not(roi, roi);
        }
        imshow("CamShift at LabEx", image);
        int key = cv::waitKey(1000/15.0);
        if(key == 27) break;
    }
    // 割り当てられたメモリを解放する
    cv::destroyAllWindows();
    video.release();
    return 0;
}

注記:

ROI(関心領域): 画像処理において、処理対象となる任意の領域を関心領域、すなわち ROI と呼ぶことができます。

OpenCV を使ってキャムシフトを実装する

トラッキング対象を計算するためのバックプロジェクションヒストグラムには、cvtColor関数を使用する必要があります。これは、RGB 色空間の元の画像を HSV 色空間に変換することができます。ヒストグラムの計算は、初期ターゲットを選択した後でなければなりません。したがって:

int main() {
    cv::VideoCapture video("video.ogv");
    cv::namedWindow("CamShift at LabEx");
    cv::setMouseCallback("CamShift at LabEx", onMouse, NULL);

    cv::Mat frame;
    cv::Mat hsv, hue, mask, hist, backproj;
    cv::Rect trackWindow;             // トラッキングウィンドウ
    int hsize = 16;                   // ヒストグラム用
    float hranges[] = {0,180};        // ヒストグラム用
    const float* phranges = hranges;  // ヒストグラム用

    while(true) {
        video >> frame;
        if(frame.empty()) break;
        frame.copyTo(image);

        // HSV 空間に変換する
        cv::cvtColor(image, hsv, cv::COLOR_BGR2HSV);
        // オブジェクトがある場合の処理
        if(trackObject) {

            // H: 0~180、S: 30~256、V: 10~256 のみを処理し、それ以外をフィルタリングして残りの部分を mask にコピーする
            cv::inRange(hsv, cv::Scalar(0, 30, 10), cv::Scalar(180, 256, 10), mask);
            // hsv からチャンネル h を分離する
            int ch[] = {0, 0};
            hue.create(hsv.size(), hsv.depth());
            cv::mixChannels(&hsv, 1, &hue, 1, ch, 1);

            // トラッキング対象が計算されていない場合の特性抽出
            if( trackObject < 0 ) {

                // チャンネル h と mask ROI を設定する
                cv::Mat roi(hue, selection), maskroi(mask, selection);
                // ROI ヒストグラムを計算する
                calcHist(&roi, 1, 0, maskroi, hist, 1, &hsize, &phranges);
                // ヒストグラムの正規化
                normalize(hist, hist, 0, 255, CV_MINMAX);

                // トラッキング対象を設定する
                trackWindow = selection;

                // トラッキング対象が計算されたことを示す
                trackObject = 1;
            }
            // ヒストグラムのバックプロジェクション
            calcBackProject(&hue, 1, 0, hist, backproj, &phranges);
            // 共通領域を取得する
            backproj &= mask;
            // カムシフトアルゴリズムを呼び出す
            cv::RotatedRect trackBox = CamShift(backproj, trackWindow, cv::TermCriteria( CV_TERMCRIT_EPS | CV_TERMCRIT_ITER, 10, 1 ));
            // 描画用の領域が小さすぎる場合の処理
            if( trackWindow.area() <= 1 ) {
                int cols = backproj.cols, rows = backproj.rows, r = (MIN(cols, rows) + 5)/6;
                trackWindow = cv::Rect(trackWindow.x - r, trackWindow.y - r,
                                       trackWindow.x + r, trackWindow.y + r) & cv::Rect(0, 0, cols, rows);
            }
            // トラッキング領域を描画する
            ellipse( image, trackBox, cv::Scalar(0,0,255), 3, CV_AA );

        }


        if( selectObject && selection.width > 0 && selection.height > 0 ) {
            cv::Mat roi(image, selection);
            bitwise_not(roi, roi);
        }
        imshow("CamShift at LabEx", image);
        int key = cv::waitKey(1000/15.0);
        if(key == 27) break;
    }
    cv::destroyAllWindows();
    video.release();
    return 0;
}

概要

このプロジェクトで書いたコードをすべて以下に示します:

#include <opencv2/opencv.hpp>

bool selectObject = false; // オブジェクトが選択されているかどうかを表す
int trackObject = 0;       // 1 はトラッキング対象があることを意味し、0 は対象がないことを意味し、-1 はカムシフト特性が計算されていないことを意味します
cv::Rect selection;        // マウスによって選択された領域を保存する
cv::Mat image;             // ビデオからのキャッシュ画像

// OpenCV からのマウスのコールバック関数:
// void onMouse(int event, int x, int y, int flag, void *param)
// 4 番目のパラメータ `flag` は追加の状態を表し、
// param はユーザーパラメータを意味しますが、これらは必要ないので、名前は付けません。
void onMouse( int event, int x, int y, int, void* ) {
    static cv::Point origin;
    if(selectObject) {
        // 選択された高さと幅、左上隅の位置を決定する
        selection.x = MIN(x, origin.x);
        selection.y = MIN(y, origin.y);
        selection.width = std::abs(x - origin.x);
        selection.height = std::abs(y - origin.y);

        // &は cv::Rect によって上書きされます
        // 2 つの領域の交差を意味します
        // ここでの主な目的は、選択領域外の領域を処理することです
        selection &= cv::Rect(0, 0, image.cols, image.rows);
    }

    switch(event) {
            // 左ボタンが押されたときの処理
        case CV_EVENT_LBUTTONDOWN:
            origin = cv::Point(x, y);
            selection = cv::Rect(x, y, 0, 0);
            selectObject = true;
            break;
            // 左ボタンが離されたときの処理
        case CV_EVENT_LBUTTONUP:
            selectObject = false;
            if( selection.width > 0 && selection.height > 0 )
                trackObject = -1; // トラッキング対象のカムシフト特性が計算されていない
            break;
    }
}

int main( int argc, const char** argv ) {
    cv::VideoCapture video("video.ogv");
    cv::namedWindow("CamShift at LabEx");
    cv::setMouseCallback("CamShift at LabEx", onMouse, NULL);

    cv::Mat frame, hsv, hue, mask, hist, backproj;
    cv::Rect trackWindow;             // トラッキングウィンドウ
    int hsize = 16;                   // ヒストグラム用
    float hranges[] = {0,180};        // ヒストグラム用
    const float* phranges = hranges;  // ヒストグラム用

    while(true) {
        video >> frame;
        if(frame.empty()) break;
        frame.copyTo(image);

        // HSV 空間に変換する
        cv::cvtColor(image, hsv, cv::COLOR_BGR2HSV);
        // オブジェクトがある場合の処理
        if(trackObject) {

            // H: 0~180、S: 30~256、V: 10~256 のみを処理し、それ以外をフィルタリングして残りの部分を mask にコピーする
            cv::inRange(hsv, cv::Scalar(0, 30, 10), cv::Scalar(180, 256, 256), mask);
            // hsv からチャンネル h を分離する
            int ch[] = {0, 0};
            hue.create(hsv.size(), hsv.depth());
            cv::mixChannels(&hsv, 1, &hue, 1, ch, 1);

            // トラッキング対象が計算されていない場合の特性抽出
            if( trackObject < 0 ) {

                // チャンネル h と mask ROI を設定する
                cv::Mat roi(hue, selection), maskroi(mask, selection);
                // ROI ヒストグラムを計算する
                calcHist(&roi, 1, 0, maskroi, hist, 1, &hsize, &phranges);
                // ヒストグラムの正規化
                normalize(hist, hist, 0, 255, CV_MINMAX);

                // トラッキング対象を設定する
                trackWindow = selection;

                // トラッキング対象が計算されたことを示す
                trackObject = 1;
            }
            // ヒストグラムのバックプロジェクション
            calcBackProject(&hue, 1, 0, hist, backproj, &phranges);
            // 共通領域を取得する
            backproj &= mask;
            // カムシフトアルゴリズムを呼び出す
            cv::RotatedRect trackBox = CamShift(backproj, trackWindow, cv::TermCriteria( CV_TERMCRIT_EPS | CV_TERMCRIT_ITER, 10, 1 ));
            // 描画用の領域が小さすぎる場合の処理
            if( trackWindow.area() <= 1 ) {
                int cols = backproj.cols, rows = backproj.rows, r = (MIN(cols, rows) + 5)/6;
                trackWindow = cv::Rect(trackWindow.x - r, trackWindow.y - r,
                                       trackWindow.x + r, trackWindow.y + r) & cv::Rect(0, 0, cols, rows);
            }
            // トラッキング領域を描画する
            ellipse( image, trackBox, cv::Scalar(0,0,255), 3, CV_AA );

        }


        if( selectObject && selection.width > 0 && selection.height > 0 ) {
            cv::Mat roi(image, selection);
            bitwise_not(roi, roi);
        }
        imshow("CamShift at LabEx", image);
        int key = cv::waitKey(1000/15.0);
        if(key == 27) break;
    }
    cv::destroyAllWindows();
    video.release();
    return 0;
}

main.cppを再コンパイルしましょう:

g++ main.cpp $(pkg-config opencv --libs --cflags opencv) -o main

そして実行します:

./main

これで、プログラム内でオブジェクトを選択してトラッキングを行うことができます:

画像の説明

上の画像では、木星を選択し、トラッキングウィンドウは赤い楕円です。