之前在点跟踪技术中介绍了两种不同的跟踪理论。一种是假设每个特征点在前后两帧的亮度是不变的,这就是光流法,但是大部分实际跟踪问题都不能完全满足这个假设。另一种是承认每个点是可变的,但变化是极小的,而且该点周围的一个邻域的变化都是极小的,于是把点跟踪变成块匹配问题。实际上大部分跟踪方法是不提取特征点,而直接针对整个物体所在的图像块的(包含物体的矩形或其他规则形状),提取整个图像块的特征,进行匹配或者其他检测方法。
块跟踪技术中最基础的方法是meanshift方法。
meanshift的应用方法一般是首先提取物体块初始的颜色/亮度直方图,然后根据该直方图在之后的每一帧图像中寻找最相似的出现,即为物体的位置。
为了避免进行诸如滑动窗口之类的穷举式搜索,meanshift采用了一种贪心搜索法。
meanshift并不是专为图像跟踪设计的算法,它是一种概率密度估计算法。
将图像中所有像素点为中心的图像块与目标的相似度用密度函数来表示,目标是从任意位置开始向密度最高的位置移动。根据梯度法的原则,meanshift每次选择沿梯度反方向移动。这个过程当中每次只需要计算上次起始点周围一个范围内(窗口内)若干采样点的密度,不需要计算整个图像所有像素点的密度。
在实际应用中,OpenCV采用的是一种称为Camshift的变体方法。这种方法很难称为“改进”,因为他的效果不见得比meanshift好,但是肯定比meanshift快,而且可以自适应的估计物体的形状大小和方向。
Camshift的关键是反向投影(BackProjection)方法。
BackProjection方法的介绍详见
这种方法只能算是伪直方图匹配法。举个比较极端的例子,学习一个半红半蓝的物体的直方图,再去计算纯红色块、纯蓝色块和半红半蓝块,它们的匹配度是相同的。物体的方向和大小是直接根据反向直方图的零阶、一阶和二阶矩用固定公式算出来的。可见这个方法是多么不靠谱,不过它的计算速度绝对块。真是图像处理无坚不破,唯快不破。
附上OpenCV的Camshift使用方法
#include <opencv2\opencv.hpp>
using namespace std;
using namespace cv;
enum Status {PAUSED, PLAY, TRACKING_INIT, TRACKING};
Status status = PLAY;
bool showHist = false;
bool selectObject = false;
Point origin;
Rect selection;
Size imgSz;
void onMouse(int evt, int x, int y, int, void*) {
if (status == TRACKING || status == TRACKING_INIT)
return;
if (selectObject) {
selection.x = MIN(x, origin.x);
selection.y = MIN(y, origin.y);
selection.width = abs(x - origin.x);
selection.height = abs(y - origin.y);
selection &= Rect(0, 0, imgSz.width, imgSz.height);
}
switch(evt) {
case CV_EVENT_LBUTTONDOWN:
origin = Point(x, y);
selection = Rect(x, y, 0, 0);
selectObject = true;
break;
case CV_EVENT_LBUTTONUP:
selectObject = false;
break;
}
}
int main() {
VideoCapture cap(0);
if (!cap.isOpened()) {
cout << "open video error" << endl;
exit(-1);
}
namedWindow("disp");
setMouseCallback("disp", onMouse, 0);
Mat frame, disp, hsv, mask, hue, hist, histimg = Mat::zeros(200, 320, CV_8UC3), backproj;
int smin = 30;
int vmin = 10;
int vmax = 256;
int hsize = 16;
float hranges[] = {0,180};
const float* phranges = hranges;
int iFrame = 0;
while (1) {
if (status != PAUSED) {
cap >> frame;
if (frame.empty())
break;
}
//get initial information
if (iFrame == 0) {
imgSz.width = frame.cols;
imgSz.height = frame.rows;
}
cvtColor(frame, hsv, CV_BGR2HSV);
//ignore dark regions
inRange(hsv, Scalar(0, smin, vmin), Scalar(180, 256, vmax), mask);
//only hue information is used
int ch[] = {0, 0};
hue.create(hsv.size(), hsv.depth());
mixChannels(&hsv, 1, &hue, 1, ch, 1);
if (status != TRACKING) {
//calculate histogram of the object
Mat roi(hue, selection), maskroi(mask, selection);
calcHist(&roi, 1, 0, maskroi, hist, 1, &hsize, &phranges);
normalize(hist, hist, 0, 255, CV_MINMAX);
histimg = Scalar::all(0);
int binW = histimg.cols / hsize;
Mat buf(1, hsize, CV_8UC3);
for( int i = 0; i < hsize; i++ )
buf.at<Vec3b>(i) = Vec3b(saturate_cast<uchar>(i*180./hsize), 255, 255);
cvtColor(buf, buf, CV_HSV2BGR);
for( int i = 0; i < hsize; i++ )
{
int val = saturate_cast<int>(hist.at<float>(i)*histimg.rows/255);
rectangle( histimg, Point(i*binW,histimg.rows),
Point((i+1)*binW,histimg.rows - val),
Scalar(buf.at<Vec3b>(i)), -1, 8 );
}
if (status == TRACKING_INIT)
status = TRACKING;
} else {
//track
calcBackProject(&hue, 1, 0, hist, backproj, &phranges);
backproj &= mask;
RotatedRect trackBox = CamShift(backproj, selection,
TermCriteria( CV_TERMCRIT_EPS | CV_TERMCRIT_ITER, 10, 1 ));
ellipse(frame, trackBox, Scalar(255, 255, 0), 2);
}
/* draw */
if (status == PAUSED || status == PLAY) {
//user labeling
frame.copyTo(disp);
rectangle(disp, selection, Scalar(255, 255, 0), 2);
imshow("disp", disp);
} else if (status == TRACKING) {
imshow("disp", frame);
}
if (showHist) {
imshow("hist", histimg);
}
iFrame++;
char key = waitKey(30);
if (key == 27)
break;
switch (key) {
case ' ':
status = TRACKING_INIT;
break;
case 'p':
if (status == PAUSED)
status = PLAY;
else
status = PAUSED;
break;
case 'u':
status = PLAY;
selection = Rect(0, 0, 0, 0);
break;
case 'h':
showHist = !showHist;
if( !showHist )
destroyWindow("hist");
else
namedWindow("hist",1);
break;
}
}
}