Mean-shift Trackingを実装してみた
勉強のためにMean-shift Trackingを実装してみました
ソースコード
以下に公開してあります
github.com
参考
実装の際に参考にしたのはOpenCVのCamShiftのサンプルコードと
こちらのintelの論文です
http://opencv.jp/opencv-1.0.0_org/docs/papers/camshift.pdf
URLから分かるようにCamShiftのオリジナルっぽいですね
アルゴリズム
アルゴリズムを簡単に紹介します
追跡ウィンドウの更新
枚フレーム以下の処理を行います
- 追跡対象の2次元確率分布を計算
- 追跡ウィンドウ内の確率分布の重心を求める
- ウィンドウの中心が重心の位置に来るようウィンドウを更新
- 収束するまで2、3を繰り返す
2次元確率分布は要するに「追跡対象らしさマップ」みたいなものです
最初に計算したヒストグラムを2次元に逆投影(back projection)することで求められます
2、3がMean-shiftの本処理です
確率分布のピークに向かってウインドウを移動させます
さらにウィンドウのサイズや傾きを計算する処理が加わると
CamShiftと呼ばれる手法になります
あとはこことかも参考になりました
OpenCV: Meanshift and Camshift
ソースの解説
MeanShiftTrackerクラス
start()で追跡の開始、update()で追跡ウィンドウの更新を行います
勉強用ということでメンバは公開にしました
class MeanShiftTracker { public: MeanShiftTracker(); void start(const cv::Mat& img, const cv::Rect& window); int update(const cv::Mat& img, cv::Rect& window); int vmin_, vmax_, smin_; cv::Mat hist_, backProject_; };
追跡の開始
ウィンドウ内の色ヒストグラムを計算します
純粋に色だけに着目したい(輝度や彩度の影響を無視したい)ので
RGBをHSVに変換しそのHue成分だけを使用します
また輝度や彩度が低い画素では色の表現力も弱くなるので
そのような画素もマスクによって計算から除外します
void MeanShiftTracker::start(const cv::Mat& img, const cv::Rect& window) { CV_Assert(img.type() == CV_8UC3); // ROIの設定 cv::Mat roi = img(window); // HSVに変換 cv::Mat hsv; cv::cvtColor(roi, hsv, cv::COLOR_BGR2HSV); // マスクの作成 cv::Mat mask; cv::inRange(hsv, cv::Scalar(0, smin_, vmin_), cv::Scalar(180, 256, vmax_), mask); // Hue成分の抽出 cv::Mat hue(hsv.size(), hsv.depth()); int fromTo[2] = { 0, 0 }; cv::mixChannels({ hsv }, { hue }, fromTo, 1); // ヒストグラムの計算 int histSize = 64; float range[] = { 0, 180 }; calcHist(hue, mask, hist_, histSize, range); }
ヒストグラムの計算では値がrangeの範囲内にある画素を
0≦x<1となるよう正規化し、histSizeをかけることでビンを算出しています
void calcHist(const cv::Mat& img, const cv::Mat& mask, cv::Mat& hist, int histSize, const float *range) { CV_Assert(img.type() == CV_8U); CV_Assert(mask.type() == CV_8U); CV_Assert(mask.size() == img.size()); hist.create(cv::Size(1, histSize), CV_32F); hist = cv::Scalar::all(0); float minv = range[0]; float maxv = range[1]; for (int i = 0; i < img.rows; ++i) { for (int j = 0; j < img.cols; ++j) { float v = img.at<uchar>(i, j); if (v < minv || v >= maxv) continue; float nv = (v - minv) / (maxv - minv); int bin = static_cast<int>(histSize * nv); CV_Assert(bin < histSize); if (mask.at<uchar>(i, j)) hist.at<float>(bin) += 1.0f; } } }
追跡ウィンドウの更新
全体の流れはこんな感じ
前半部分は開始処理とほとんど同じですね
int MeanShiftTracker::update(const cv::Mat& img, cv::Rect& window) { // HSVに変換 cv::Mat hsv; cv::cvtColor(img, hsv, cv::COLOR_BGR2HSV); // マスクの作成 cv::Mat mask; cv::inRange(hsv, cv::Scalar(0, smin_, vmin_), cv::Scalar(180, 256, vmax_), mask); // Hue成分の抽出 cv::Mat hue(hsv.size(), hsv.depth()); int fromTo[2] = { 0, 0 }; cv::mixChannels({ hsv }, { hue }, fromTo, 1); // ヒストグラムの逆投影 float range[] = { 0, 180 }; calcBackProject(hue, hist_, backProject_, range); // 逆投影画像のマスキング cv::bitwise_and(backProject_, mask, backProject_); // ウィンドウの更新 return meanShift(backProject_, window); }
追跡対象の2次元確率分布を求めるため、ヒストグラムの逆投影を行います
各画素に対しヒストグラムを計算した時と同じ方法でビンを計算し
ビンに対応するヒストグラムの値(度数)を逆投影画像に格納します
追跡対象に近い色を持つならば、大きな値になるはずです
見やすいように値を0~255に正規化してあります
void calcBackProject(const cv::Mat& img, const cv::Mat& hist, cv::Mat& backProject, const float *range) { CV_Assert(img.type() == CV_8U); CV_Assert(hist.type() == CV_32F); backProject.create(img.size(), CV_8U); backProject = cv::Scalar::all(0); cv::Mat _hist = hist; cv::normalize(_hist, _hist, 0, 255, cv::NORM_MINMAX); int histSize = hist.rows; float minv = range[0]; float maxv = range[1]; for (int i = 0; i < img.rows; ++i) { for (int j = 0; j < img.cols; ++j) { float v = img.at<uchar>(i, j); if (v < minv || v >= maxv) continue; float nv = (v - minv) / (maxv - minv); int bin = static_cast<int>(histSize * nv); CV_Assert(bin < histSize); backProject.at<uchar>(i, j) = cv::saturate_cast<uchar>(_hist.at<float>(bin)); } } }
追跡ウィンドウ内の確率分布の重心を求めます
分布の0次モーメントを
1次モーメントを
とすると、重心は以下のように求まります
ウィンドウの中心が重心の位置に来るようウィンドウ位置を更新します
反復が既定回数に達するか、ウィンドウの移動量が小さくなるまで繰り返します
int meanShift(const cv::Mat& probImage, cv::Rect& window) { CV_Assert(probImage.type() == CV_8U); int maxiter = 20; for (int iter = 0; iter < maxiter; ++iter) { // 重心の計算 unsigned int mz = 0, mx = 0, my = 0; for (int i = 0; i < window.height; ++i) { for (int j = 0; j < window.width; ++j) { int y = i + window.y; int x = j + window.x; mz += probImage.at<uchar>(y, x); mx += x * probImage.at<uchar>(y, x); my += y * probImage.at<uchar>(y, x); } } if (mz == 0) return 0; int cx = mx / mz; int cy = my / mz; // ウィンドウの位置を更新 int winx = cx - window.width/2; int winy = cy - window.height/2; winx = std::min(probImage.cols - 1 - window.width, std::max(0, winx)); winy = std::min(probImage.rows - 1 - window.height, std::max(0, winy)); // 移動量が小さい場合は終了 if (abs(winx - window.x) < 1 && abs(winy - window.y) < 1) break; window.x = winx; window.y = winy; } return 1; }
サンプル
入力データはこちらを利用させていただきました
David Ross - Incremental Visual Tracking
OpenCVのCamShiftのデモと同様に
マウスのドラッグで追跡対象を指定できるようになってます
キーボードの'b'を押すと逆投影画像に表示が切り替わります
MeanShiftTracker Demo
最初は追跡に成功していますが
途中でウィンドウが背景の方に外れてしまいました
おわりに
以前の記事ではOpenCVのサンプルの紹介で終わってしまったので
今回は理解のために自分で中身を実装してみました
WindowsにOpenCV 3 + Visual Studio 2015の環境を構築する
WindowsにOpenCVとVisual Studioをインストールして画像処理を始めましょう
OpenCVとVisual Studioのバージョンは2016年7月時点で最新のものを選びました
- やること
- OpenCV 3.1のインストール・設定
- Visual Studio 2015のインストール・設定
- CMake 3.5.2のインストール・設定
- CMakeLists.txtとソースの準備
- プロジェクトの作成・ビルド・実行
- おわりに
やること
- WindowsにOpenCV 3 + Visual Studio 2015の環境を構築
- OpenCVを使った簡単なプログラムのビルド・実行
OpenCV 3.1のインストール・設定
まずOpenCVをインストールします
- DOWNLOADS | OpenCVからOpenCV for Windows VERSION3.1をダウンロード
- opencv-3.1.0.exe実行
- ファイルを適当なディレクトリに展開
ここではDドライブ直下に展開したとして説明を続けます
D:\opencv
次に環境変数を設定します
OPENCV_DIRでCMakeにOpenCVのインストール場所を教えていますが
CMakeを使わないのであれば設定は不要です
Visual Studio 2015のインストール・設定
結構時間がかかりました(ファイルサイズ的に)
- Downloads | Visual Studio Official Siteから無償のVisual Studio Communityをダウンロード
- vs_community_ENU.exeを実行してインストール
インストール完了と思いきや、C++の開発ツールがまだ入っていないので入れます
- Visual Studioを立ち上げる
- New Projectからプロジェクト作成画面を表示
- Visual C++ -> Install|Visual C++| Install Visual C++ 2015 Tools for Windows Desktopを選択
- OKを押してインストール
- 参考:C++/Visual Studio 2015 プログラミング
CMake 3.5.2のインストール・設定
Visual Studioのプロジェクト作成ごとにOpenCVのincludeやライブラリの設定をするのは面倒です
CMakeを使うと自動で設定してくれますし、再利用がきくのでおススメです
CMakeLists.txtとソースの準備
いよいよプログラムの作成に入ります
プロジェクト用ディレクトリを作成し、CMakeLists.txtとソースコードを置きます
HelloCV |- CMakeLists.txt |- main.cpp
CMakeLists.txtの中身はこんな感じです
これでOpenCVが使えるプロジェクトを作成することができます
cmake_minimum_required(VERSION 2.8) find_package(OpenCV REQUIRED) include_directories(${OpenCV_INCLUDE_DIRS}) file(GLOB srcs ./*.cpp ./*.h*) add_executable(HelloCV ${srcs}) target_link_libraries(HelloCV ${OpenCV_LIBS})
main.cppの中身はこんな感じです
画像に「Hello OpenCV World.」というテキストを描画して表示するプログラムです
#include <opencv2\opencv.hpp> int main() { cv::Mat img = cv::Mat::zeros(cv::Size(512, 256), CV_8UC3); cv::putText(img, "Hello OpenCV World.", cv::Point(100, 100), 0, 1, cv::Scalar(255, 255, 255)); cv::imshow("HelloCV", img); cv::waitKey(0); return 0; }
プロジェクトの作成・ビルド・実行
コマンドラインでプロジェクトを作成しますので、まずはコンソールを立ち上げましょう
コマンドプロンプトでも良いですが、さすがにつらいという方にはコンソールエミュレータを入れることをおすすめします(私は現在cmder | Console Emulatorを使用しています)
- プロジェクトのディレクトリに移動
$ls CMakeLists.txt main.cpp
- プロジェクトの作成
$mkdir build $cd build $cmake .. -G "Visual Studio 14 Win64"
成功するとbuild以下にProject.slnというソリューションファイルができてるはずです
これをダブルクリックするとVisual Studio 2015が起動します
- プロジェクトのビルド
cmake --build . --config Release
コマンドラインでビルドできて便利!
この方法はこちらのブログを参考にさせていただきました
CMakeプロジェクトをVisual Studioでビルドするには - kumar8600の日記
- 実行
./Release/HelloCV.exe
無事動きました
おわりに
WindowsにOpenCV 3 + Visual Studio 2015の環境を構築する手順を紹介しました
手軽な方法を選んだつもりですが、何だかんだやること多いですね
ちなみにVisual Studio 2015でソースを表示してみましたが、文字が柔らかくなった印象です
Visual Studio 2013よりもいいかも…!
Google Code Jamに参加してみた
Googleが開催するプログラミングコンテスト「Google Code Jam」の予選ラウンドに参加しました
全4問のうち、正解率の高かった2問は何とか解くことができたのですが
3問目で解答が思いつかず降参
プログラミングコンテスト、興味はあって取り組むようにしてるのですが
問題を読んでるだけで頭が痛くなってきます…
向いてないのかな…
字下げスタイルとわたし
「字下げスタイル?コーディング作法って何?」
って感じで学生の頃はこんなコードを書いてました
void func(float *a,float *b,int n){ for(int i=0; i<n; i++){ b[i] = a[i] ; } }
いま思うと、いろいろと気になるところがある
- 変数名適当
- 基本的にスペースが無い
- なのに何故かセミコロンの前だけスペース!?
「会社に入って」
他人とコードを共有したり、参考書を読むようになってからは、ちょっとましになった思います
- 関数名とか、変数名を適当にしない
- コメント書く
- 既存のスタイルに従う(K&Rスタイルとか)
// n要素コピーする void copy(const float *src, float *dst, int n) { for (int i = 0; i < n; i++) { dst[i] = src[i]; } }
最近は
なんとなくBSDスタイルいいんじゃね?と思い始めてます
全て中括弧を次の行に置く一貫性と、if-else句の見易さが良いです
コードが長くなりがちだけど
void copy(const float *src, float *dst, int n) { for (int i = 0; i < n; i++) { dst[i] = src[i]; } if (hoge) { hogehoge(); } else { hogehogehoge(); } }
皆さんはどのスタイルを使いますか
OpenCVのMean-shift Trackingを試してみた
OpenCVで使える、色ベースのトラッキング「meanShift」と「CamShift」を紹介します!
何ができるの?
追跡したい対象の領域をはじめに指定することで、
その色情報(ヒストグラム)をもとに、対象を追跡できます
探索窓が固定なMeanShiftに対し、CamShiftは物体サイズに合わせて窓サイズを調整してくれます
試してみよう
OpenCVにCamShiftのサンプルコードがあります
(OpenCVのインストール先)/sources/samples/cpp/camshiftdemo.cpp
実行例:YouTubeから拾った動画で失礼します
OpenCV + Camshift Demonstration - YouTube
meanShiftも試してみたい
camshiftdemo.cppを少し修正すると、meanShiftも試すことができます
CamShift()の呼び出し箇所
cv::RotatedRect trackBox = cv::CamShift(backproj, trackWindow, cv::TermCriteria(cv::TermCriteria::EPS | cv::TermCriteria::COUNT, 10, 1));
を以下のように修正します
cv::meanShift(backproj, trackWindow, cv::TermCriteria(cv::TermCriteria::EPS | cv::TermCriteria::COUNT, 10, 1));
また、すぐ下の領域を描画する箇所
cv::ellipse(_image, trackBox, cv::Scalar(0, 0, 255), 3, cv::LINE_AA);
を以下のように修正します
cv::rectangle(_image, trackWindow.tl(), trackWindow.br(), cv::Scalar(0,0,255), 3, cv::LINE_AA);
終わりに
今回は「meanShift」と「CamShift」の表面だけ紹介しましたが
今後、追跡の仕組みについても解説したいと思います
社会人プログラマーになって約1年がたった
わかったのはコンピュータの世界が果てしないということ
たまに勉強すべきことが多すぎて呆然とする