Introduction

OpenCVSharpにPull Requestを投げました。
内容は、OpenCVの拡張モジュールに含まれる、Trackingと呼ばれる物体追跡用モジュールのOpenCVSharp対応です。

そんな物体追跡をC++で試してみたメモになります。

What is Tracking?

詳しい理論などは、

がわかりやすいです。
ともかく

  1. 画像上の任意矩形を初期位置を設定
  2. 矩形内の特徴を学習
  3. 矩形周辺を探索し移動量推定
  4. 2.から繰り返し

という流れです。

物体追跡を使えば、現実世界において、通過する車両の数を計算したり出来そうですね。

How to use?

Trackingの使い方は非常にシンプルです。

  1. インスタンスの生成
    • cv::Ptr Tracker::create(const cv::String& trackerType)
  2. インスタンスの初期化
    • bool Tracker::init(const cv::Mat& image, const cv::Rect2d& boundingBox)
  3. 更新
    • bool Tracker::update(const cv::Mat& image, cv::Rect2d& boundingBox)
  4. 3を繰り返す

以上です。

Algorithm

物体追跡にはいくつかのアルゴリズムが用意されており、先述のインスタンスの生成の際に、文字列としてアルゴリズムを指定します。
OpenCV3.2で利用できるアルゴリズムは

の6つになります。
ただし、GOTURN は、Caffeを利用しており、入力として、goturn.prototxtgoturn.caffemodel を実行フォルダに展開する必要があります。
さもなくば、

OpenCV Error: Unspecified error (FAILED: fs.is_open(). Can’t open “goturn.prototxt”) in cv::dnn::ReadProtoFromTextFile, file D:\Works\Lib\OpenCV\src\opencv_contrib-3.2.0\modules\dnn\src\caffe\caffe_io.cpp, line 1077 D:\Works\Lib\OpenCV\src\opencv_contrib-3.2.0\modules\dnn\src\caffe\caffe_io.cpp:1077: error: (-2) FAILED: fs.is_open(). Can’t open “goturn.prototxt” in function cv::dnn::ReadProtoFromTextFile

OpenCV Error: Unspecified error (FAILED: fs.is_open(). Can’t open “goturn.caffemodel”) in cv::dnn::ReadProtoFromBinaryFile, file D:\Works\Lib\OpenCV\src\opencv_contrib-3.2.0\modules\dnn\src\caffe\caffe_io.cpp, line 1086 D:\Works\Lib\OpenCV\src\opencv_contrib-3.2.0\modules\dnn\src\caffe\caffe_io.cpp:1086: error: (-2) FAILED: fs.is_open(). Can’t open “goturn.caffemodel” in function cv::dnn::ReadProtoFromBinaryFile

というエラーが発生します。
入手先は下記の2箇所になります。

  1. Train you own GOTURN model using
  2. Download pretrained caffemodel

ただ、上記2つを見てもそれらしいのはありませんし、protoxtxtやcaffemodelの名前を変更して実行してもエラーになったりして試すことが出来ませんでした。

OpenCV Error: Requested object was not found (Requested blob “.data1” not found) in cv::dnn::Net::setBlob, file D:\Works\Lib\OpenCV\src\opencv_contrib-.2.0\modules\dnn\src\dnn.cpp, line 516

とりあえず、今回は、GOTURN以外を試しました。

Source

下記がサンプルです。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
#include "stdafx.h"
#include <opencv2/core.hpp>
#include <opencv2/highgui.hpp>
#include <opencv2/opencv.hpp>
#include <opencv2/tracking.hpp>

int main(int argc, const char * argv[])
{
// https://www.youtube.com/watch?v=QAYX9MZ77w8
auto path = "B:\\poochy_yoshis_woolly_world.mp4";
cv::VideoCapture vc(path);
if (!vc.isOpened())
return -1;

cv::Mat img;
vc >> img;
if (img.empty())
return -1;

// 横方向の画像数
const int cols = 3;

const int cnt = 5;
cv::String trakerTypes[cnt];
cv::Rect2d rois[cnt];
cv::Ptr<cv::Tracker> trackers[cnt];

// Trackerの設定
trakerTypes[0] = cv::String("KCF");
trakerTypes[1] = cv::String("MIL");
trakerTypes[2] = cv::String("MEDIANFLOW");
trakerTypes[3] = cv::String("TLD");
trakerTypes[4] = cv::String("BOOSTING");
//trakerTypes[5] = cv::String("GOTURN");

for(auto index = 0; index < cnt; index++)
trackers[index] = cv::Tracker::create(trakerTypes[index]);

// 縮小後のサイズ
auto ratio = 0.5;
const auto iw = img.cols * ratio;
const auto ih = img.rows * ratio;
const cv::Size size(iw, ih);

// 初期位置
const int x = 80;
const int y = 60;
const int w = 200;
const int h = 220;
cv::Rect2d roi(x, y, w, h);

for (auto index = 0; index < cnt; index++)
rois[index] = roi;

// 初回フレームの設定
for (auto index = 0; index < cnt; index++)
trackers[index]->init(img, rois[index]);

cv::Mat combined_img(cv::Size(iw * cols, ceil(cnt / static_cast<double>(cols)) * ih ), CV_8UC3);
cv::namedWindow("preview");

while (!img.empty())
{
cv::Rect roi_rect;
roi_rect.width = iw;
roi_rect.height = ih;
for (auto index = 0; index < cnt; index++)
{
cv::Mat cloned = img.clone();
roi_rect.x = iw * (index % cols);
roi_rect.y = ih * (index / cols);

cv::Point p1(rois[index].x, rois[index].y);
cv::Point p2(p1.x + rois[index].width, p1.y + rois[index].height);
cv::rectangle(cloned, p1, p2, CvScalar(255, 0, 0), 3);

cv::putText(cloned, trakerTypes[index], cv::Point(20, 40), cv::FONT_HERSHEY_PLAIN, 2, CvScalar(0, 0, 255), 2);

cv::Mat roi(combined_img, roi_rect);

cv::Mat resized;
cv::resize(cloned, resized, size);
resized.copyTo(roi);
}

cv::imshow("preview", combined_img);

// ESC key check
auto key = static_cast<char>(cv::waitKey(30));
if (key == 27)
break;

vc >> img;
if (img.empty() ||
vc.get(CV_CAP_PROP_POS_AVI_RATIO) == 1)
break;

for (auto index = 0; index < cnt; index++)
trackers[index]->update(img, rois[index]);
}
}

これを動かすと、次のようになります。
サンプルとして、ニンテンドー3DS「ポチと! ヨッシー ウールワールドポチと! ヨッシー ウールワールド」 の任天堂Youtube公式チャンネルからポチと! ヨッシー ウールワールド ゲームで見られるアニメ 「かけっこ編」を使っています。
映像中の青ヨッシーを追跡します。
5fpsで録画した上、かなりフレーム数を間引いてGIFアニメにしていますが、アルゴリズム毎の性能はわかると思います。

評価としては、下記の感じです。

  • MIL
    • やや安定。途中で緑ヨッシーにつられてしまっている。
  • TLD
    • なんかランダムみたいな感じ
  • BOOSTING
    • そこそこ安定。ただし、一度青ヨッシーが枠から外れたため、途中で緑ヨッシーを追跡している。
  • KCF
    • 一番安定。ただし、枠外に外れてしまうと追跡できなくなる
  • MEDIANFLOW
    • まったく動いていない

画像を縮小して高速化も試しています。
まだまだ工夫の余地はあると思いますが、それなりに目的は達成できています。

Conclusion

いくつか問題はありますが、物体追跡は色々楽しめそうな技術ですね。
画像全体の探索はコストがかかりますが、一度見つかった物体の探索は、物体追跡が一番低コストかもしれません。