基于MeanShift的视频目标跟踪算法及代码实现

时间:2022-09-27 10:13:54

本文详细出处见实验室一个师兄的博客,链接如下http://blog.csdn.net/jinshengtao/article/details/30258833

由于最近在做视频跟踪的小项目,这里对MeanShift的视频目标跟踪算法进行一下小结。

(一)MeanShift算法简介

 MeanShift算法正是属于核密度估计法,它不需要任何先验知识而完全依靠特征空间中样本点的计算其密度函数值。对于一组采样数据,直方图法通常把数据的值域分成若干相等的区间,数据按区间分成若干组,每组数据的个数与总参数个数的比率就是每个单元的概率值;核密度估计法的原理相似于直方图法,只是多了一个用于平滑数据的核函数。采用核函数估计法,在采样充分的情况下,能够渐进地收敛于任意的密度函数,即可以对服从任何分布的数据进行密度估计。

     然后谈谈MeanShift的基本思想及物理含义:

基于MeanShift的视频目标跟踪算法及代码实现

基于MeanShift的视频目标跟踪算法及代码实现

基于MeanShift的视频目标跟踪算法及代码实现

    此外,从公式1中可以看到,只要是落入Sh的采样点,无论其离中心x的远近,对最终的Mh(x)计算的贡献是一样的。然而在现实跟踪过程中,当跟踪目标出现遮挡等影响时,由于外层的像素值容易受遮挡或背景的影响,所以目标模型中心附近的像素比靠外的像素更可靠。因此,对于所有采样点,每个样本点的重要性应该是不同的,离中心点越远,其权值应该越小。故引入核函数和权重系数来提高跟踪算法的鲁棒性并增加搜索跟踪能力。

      接下来,谈谈核函数:

基于MeanShift的视频目标跟踪算法及代码实现

基于MeanShift的视频目标跟踪算法及代码实现

    核函数也叫窗口函数,在核估计中起到平滑的作用。常用的核函数有:Uniform,Epannechnikov,Gaussian等。本文算法只用到了Epannechnikov,它数序定义如下:

基于MeanShift的视频目标跟踪算法及代码实现




(二).基于MeanShift的目标跟踪算法

     基于均值漂移的目标跟踪算法通过分别计算目标区域和候选区域内像素的特征值概率得到关于目标模型和候选模型的描述,然后利用相似函数度量初始帧目标模型和当前帧的候选模版的相似性,选择使相似函数最大的候选模型并得到关于目标模型的Meanshift向量,这个向量正是目标由初始位置向正确位置移动的向量。由于均值漂移算法的快速收敛性,通过不断迭代计算Meanshift向量,算法最终将收敛到目标的真实位置,达到跟踪的目的。

     下面通过图示直观的说明MeanShift跟踪算法的基本原理。如下图所示:目标跟踪开始于数据点xi0(空心圆点xi0,xi1,…,xiN表示的是中心点,上标表示的是的迭代次数,周围的黑色圆点表示不断移动中的窗口样本点,虚线圆圈代表的是密度估计窗口的大小)。箭头表示样本点相对于核函数中心点的漂移向量,平均的漂移向量会指向样本点最密集的方向,也就是梯度方向。因为 Meanshift 算法是收敛的,因此在当前帧中通过反复迭代搜索特征空间中样本点最密集的区域,搜索点沿着样本点密度增加的方向“漂移”到局部密度极大点点xiN,也就是被认为的目标位置,从而达到跟踪的目的,MeanShift 跟踪过程结束。

基于MeanShift的视频目标跟踪算法及代码实现

 

 

基于MeanShift的视频目标跟踪算法及代码实现

基于MeanShift的视频目标跟踪算法及代码实现

基于MeanShift的视频目标跟踪算法及代码实现

基于MeanShift的视频目标跟踪算法及代码实现

基于MeanShift的视频目标跟踪算法及代码实现

(三)运动目标的实现过程【具体算法】:

 运用传统的MeanShift算法进行目标跟踪。本文通过手动选取的方式,用鼠标画矩形框选定跟踪的区域,即(六)中代码实现中的P停止,截取目标。然后计算核函数加权下的选定跟踪区域矩形框的直方图分布,即公式2.11,代码中的hist1[..]。用同样的方法计算第N帧对应矩形框的直方图分布,即公式,代码中的hist2[..]。以两个目标模板分布的相似性最大为原则,使得搜索窗口沿着密度增加最大的方向移动,不断更新中心点的坐标位置,在帧上画矩形框,这样视觉上的效果就是矩形框随人而动,就达到了跟踪的效果。

(四)代码实现:

说明:

1.       RGB颜色空间刨分,采用16*16*16的直方图

2.       目标模型和候选模型的概率密度计算公式参照上文

3.       opencv版本运行:按P停止,截取目标,再按P,进行单目标跟踪

4.       MeanShift_Tracking函数中的权重是根据到目标中心点的距离算的,没有完全按照这个核函数,但这样是可以            的,而且简单。因为前面已经乘过权重了(直方图的计算时把权重融合进去了),也就是g(x)这里不要认为             是错误。

具体代码如下:


#include "cv.h"
#include "highgui.h"
#define  u_char unsigned char
#define  DIST 0.5
#define  NUM 20                 //迭代次数NUM  设为20
//全局变量
bool pause = false;
bool is_tracking = false;
//通过矩形左上角坐标和矩形的宽和高来确定一个矩形区域
CvRect drawing_box;  //drawing_box是CvRect结构体变量
IplImage *current;       //定义一个结构体指针current,全局的,表示当前帧
double *hist1, *hist2;
double *m_wei;    //声明一个双精度型指针变量 //权值矩阵
double C = 0.0;   //归一化系数


void init_target(double *hist1, double *m_wei, IplImage *current)
{
IplImage *pic_hist = 0;   //直方图图像的结构体指针
int t_h, t_w, t_x, t_y;
double h, dist;
int i, j;
int q_r, q_g, q_b, q_temp;

t_h = drawing_box.height;  //t_h              矩形高
t_w = drawing_box.width;  //t_w              矩形宽
t_x = drawing_box.x;          //t_x    t_y     矩形的左上角坐标
t_y = drawing_box.y;

//h=(t_w/2)^2+(t_h/2)^2
h = pow(((double)t_w)/2,2) + pow(((double)t_h)/2,2);  //带宽
//  cvSize表示矩阵框大小,以像素为精度
pic_hist = cvCreateImage(cvSize(300,200),IPL_DEPTH_8U,3);     //生成直方图图像,函数 cvCreateImage 创建头并分配数据
                                                                                            //CvSize矩形框大小,以像素为精度
//初始化权值矩阵和目标直方图
for (i = 0;i < t_w*t_h;i++)
{                                      //m_wei =  (double *)malloc(sizeof(double)*drawing_box.height*drawing_box.width);
m_wei[i] = 0.0;          //权值矩阵中所有值先初始化为0.0,double类型
}


for (i=0;i<4096;i++)
{
hist1[i] = 0.0;                //hist1 = (double *)malloc(sizeof(double)*16*16*16);
                                     //hist1中所有值全为0.0
}
//求权值矩阵中存储的具体每个格子的值
for (i = 0;i < t_h; i++)
{
for (j = 0;j < t_w; j++)
{   
//dist=(i-t_h/2)^2+(j-t_w/2)^2
dist = pow(i - (double)t_h/2,2) + pow(j - (double)t_w/2,2);
//权值矩阵每个格子对应的权值  1-[(i-t_h/2)^2+(j-t_w/2)^2]/(t_w/2)^2+(t_h/2)^2
m_wei[i * t_w + j] = 1 - dist / h;   //Epannechnikov核函数
                                                    //m_wei[0].......一直到m_wei[ (t_h-1)*t_w+t_w-1]
//printf("%f\n",m_wei[i * t_w + j]);
C += m_wei[i * t_w + j] ;                        //将所有的权值加起来等于C
}
}


//计算目标权值直方
for (i = t_y;i < t_y + t_h; i++)
{
for (j = t_x;j < t_x + t_w; j++)
{
//rgb颜色空间量化为16*16*16 bins
// RGB颜色空间刨分,采用16*16*16的直方图
//q_r是r分量的像素值/16
q_r = ((u_char)current->imageData[i * current->widthStep + j * 3 + 2]) / 16;
q_g = ((u_char)current->imageData[i * current->widthStep + j * 3 + 1]) / 16;
q_b = ((u_char)current->imageData[i * current->widthStep + j * 3 + 0]) / 16;
q_temp = q_r * 256 + q_g * 16 + q_b;//设置每个像素点红色、绿色、蓝色分量所占比重  
hist1[q_temp] =  hist1[q_temp] +  m_wei[(i - t_y) * t_w + (j - t_x)] ;//计算直方图统计中每个像素点占的权重
}
}


//归一化直方图
for (i=0;i<4096;i++)
{
hist1[i] = hist1[i] / C;                                       //每个值/总的权值
//printf("%f\n",hist1[i]);
}


//生成目标直方图
double temp_max=0.0;


for (i = 0;i < 4096;i++) //求直方图最大值
{
//printf("%f\n",val_hist[i]);
if (temp_max < hist1[i])
{
temp_max = hist1[i];                //得出直方图最大值
}
}
//画直方图
CvPoint p1,p2;                                                               //定义两个CvPoint结构体变量P1, P2
double bin_width=(double)pic_hist->width/4096;                //直方图柱子单位宽度
double bin_unith=(double)pic_hist->height/temp_max;      //直方图单位高度


for (i = 0;i < 4096; i++)
{
p1.x = i * bin_width;         
p1.y = pic_hist->height;
p2.x = (i + 1)*bin_width;            
p2.y = pic_hist->height - hist1[i] * bin_unith;//屏幕上的坐标是向右,y向下  而我们这里人为的人为是x右 y上 所以要相减
//printf("%d,%d,%d,%d\n",p1.x,p1.y,p2.x,p2.y);
//cvRectangle()函数说明:
//参数1:图像.参数2:矩形的一个顶点 参数3:矩形对角线上的另一个顶点
//参数4:线条颜色 (RGB) 参数5:组成矩形的线条的粗细程度 缺省为1
//参数6:线条的类型 缺省为8     参数7:坐标点的小数点位数  缺省为0
//cvScalar一般用来存放像素值,最多可以存放4个通道的
//存放单通道图像中像素:cvScalar(255);
           //存放三通道图像中像素:cvScalar(255,255,255);
cvRectangle(pic_hist,p1,p2,cvScalar(0,255,0),-1,8,0);
}
cvSaveImage("hist1.jpg",pic_hist);  //参数1:文件名   参数2:要保存的图像
cvReleaseImage(&pic_hist);
}
void MeanShift_Tracking(IplImage *current)
{
int num = 0, i = 0, j = 0;
int t_w = 0, t_h = 0, t_x = 0, t_y = 0;
double *w = 0, *hist2 = 0;
double sum_w = 0, x1 = 0, x2 = 0,y1 = 2.0, y2 = 2.0;    //定义并且初始化防止已经存在的值影响下面的计算
int q_r, q_g, q_b;
int *q_temp;
IplImage *pic_hist = 0;


t_w = drawing_box.width;
t_h = drawing_box.height;

pic_hist = cvCreateImage(cvSize(300,200),IPL_DEPTH_8U,3);     //生成直方图图像
hist2 = (double *)malloc(sizeof(double)*4096);
w = (double *)malloc(sizeof(double)*4096);
q_temp = (int *)malloc(sizeof(int)*t_w*t_h);                           

while ((pow(y2,2) + pow(y1,2) > 0.5)&& (num < NUM))    //迭代的条件  不满足这两个条件时迭代结束
{
num++;                             //num的初值为0
t_x = drawing_box.x;         //x,y坐标
t_y = drawing_box.y;                                                                    //sizeof运算符,它返回变量或类型的字节长度
//将已开辟内存空间 q_temp 的首 sizeof(int)*t_w*t_h个字节的值设为值 0
memset(q_temp,0,sizeof(int)*t_w*t_h);
for (i = 0;i<4096;i++)
{
w[i] = 0.0;
hist2[i] = 0.0;                           //初始化
}


for (i = t_y;i < t_h + t_y;i++)
{
for (j = t_x;j < t_w + t_x;j++)
{
//rgb颜色空间量化为16*16*16 bins
q_r = ((u_char)current->imageData[i * current->widthStep + j * 3 + 2]) / 16;
q_g = ((u_char)current->imageData[i * current->widthStep + j * 3 + 1]) / 16;
q_b = ((u_char)current->imageData[i * current->widthStep + j * 3 + 0]) / 16;
q_temp[(i - t_y) *t_w + j - t_x] = q_r * 256 + q_g * 16 + q_b;
hist2[q_temp[(i - t_y) *t_w + j - t_x]] =  hist2[q_temp[(i - t_y) *t_w + j - t_x]] +  m_wei[(i - t_y) * t_w + j - t_x] ;
}
}


//归一化直方图
for (i=0;i<4096;i++)
{
hist2[i] = hist2[i] / C;
//printf("%f\n",hist2[i]);
}
//生成目标直方图
double temp_max=0.0;


for (i=0;i<4096;i++) //求直方图最大值,为了归一化
{
if (temp_max < hist2[i])
{
temp_max = hist2[i];
}
}
//画直方图
CvPoint p1,p2;
double bin_width=(double)pic_hist->width/(4368);                 //????????
double bin_unith=(double)pic_hist->height/temp_max;


for (i = 0;i < 4096; i++)
{
p1.x = i * bin_width;
p1.y = pic_hist->height;
p2.x = (i + 1)*bin_width;
p2.y = pic_hist->height - hist2[i] * bin_unith;

cvRectangle(pic_hist,p1,p2,cvScalar(0,255,0),-1,8,0);
}
cvSaveImage("hist2.jpg",pic_hist);     //参数1:文件名   参数2:要保存的图像

for (i = 0;i < 4096;i++)
{
if (hist2[i] != 0)
{
w[i] = sqrt(hist1[i]/hist2[i]);     //开平方根
}else
{
w[i] = 0;
}
}

sum_w = 0.0;                           //sum_w用来计算w的总和,初始化为0
x1 = 0.0;
x2 = 0.0;


for (i = 0;i < t_h; i++)
{
for (j = 0;j < t_w; j++)
{
//printf("%d\n",q_temp[i * t_w + j]);
sum_w = sum_w + w[q_temp[i * t_w + j]];        //sum_w用来计算w的总和
x1 = x1 + w[q_temp[i * t_w + j]] * (i - t_h/2);   //x1用来计算所有w[]* (i - t_h/2)的和
x2 = x2 + w[q_temp[i * t_w + j]] * (j - t_w/2);  //x2用来计算所有w[]* (i - t_w/2)的和
}
}
y1 = x1 / sum_w;
y2 = x2 / sum_w;

//中心点位置更新
drawing_box.x += y2;
drawing_box.y += y1;


//printf("%d,%d\n",drawing_box.x,drawing_box.y);
}
free(hist2);
free(w);
free(q_temp);
//显示跟踪结果               cvPoint二维坐标系下的点  参数1:x坐标   参数2:y坐标
cvRectangle(current,cvPoint(drawing_box.x,drawing_box.y),cvPoint(drawing_box.x+drawing_box.width,drawing_box.y+drawing_box.height),CV_RGB(255,0,0),2);
cvShowImage("Meanshift",current);
//cvSaveImage("result.jpg",current);
cvReleaseImage(&pic_hist);
}


void onMouse( int event, int x, int y, int flags, void *param )
{
if (pause)//如果pause为真时,进入if语句。
{
switch(event)
{
case CV_EVENT_LBUTTONDOWN:  //鼠标按下
//the left up point of the rect
drawing_box.x=x;   //左上角的x,y坐标赋给drawing_box.x和drawing_box.y
drawing_box.y=y;
break;
case CV_EVENT_LBUTTONUP:   //鼠标弹起
//finish drawing the rect (use color green for finish)
drawing_box.width=x-drawing_box.x; //矩形宽度=当前点X-左上角点X
drawing_box.height=y-drawing_box.y;//矩形高度=当前点Y-左上角点Y
//cvRectangle()函数说明:
//参数1:图像.参数2:矩形的一个顶点 参数3:矩形对角线上的另一个顶点
//参数4:线条颜色 (RGB) 参数5:组成矩形的线条的粗细程度 缺省为1
//参数6:线条的类型 缺省为8     参数7:坐标点的小数点位数  缺省为0
cvRectangle(current,cvPoint(drawing_box.x,drawing_box.y),cvPoint(drawing_box.x+drawing_box.width,drawing_box.y+drawing_box.height),CV_RGB(255,0,0),2);
cvShowImage("Meanshift",current);   //在Meanshift窗口显示画完矩形框的当前帧

//目标初始化
// RGB颜色空间刨分,采用16*16*16的直方图
//sizeof(double)计算一个double类型的数据占用的内存字节数
//malloc()动态随机分配内存空间的方法,如果分配成功则返回指向被分配内存的指针,否则返回空指针NULL
//使指针hist1指向一个double类型的动态存储单元,单元大小为16x16x16。
hist1 = (double *)malloc(sizeof(double)*16*16*16);
//使用运算符new动态分配一个double型存储区,将首地址赋给该指针变量
m_wei =  (double *)malloc(sizeof(double)*drawing_box.height*drawing_box.width);

//调用初始化目标函数init_target()
init_target(hist1, m_wei, current);
is_tracking = true;    //is_tracking变为真
break;
}
return;
}
}
int  main()
{
//cvCreateFileCapture()确定要读入的AVI文件,返回一个指向CvCapture结构的指针,这个结构包含了所有关于AVI文件的信息,
//也包含状态信息
CvCapture *capture=cvCreateFileCapture("F:\\新建文件夹\\cs.AVI");
//cvQueryFrame()的参数为CvCapture结构的指针,用来将下一帧视频载入内存(实际是填充或更新CvCapture结构),
//返回一个对应当前帧的指针
current = cvQueryFrame(capture);
char res[20];
int nframe = 0;


while (1)
{
/* sprintf(res,"result%d.jpg",nframe);    //主要功能是把格式化的数据写入某个 字符串中
cvSaveImage(res,current);
nframe++;*/
//is_tracking 初始值为false;
//当is_tracking为真时,即用ononMouse()函数画完矩形后,执行函数MeanShift_Tracking()
if(is_tracking)
{
MeanShift_Tracking(current);
}
//在显示视频时这个函数是有用的,用于设置在显示完一帧图像后程序等待"delay"ms再显示下一帧视频
//如果程序想响应某个按键,可利用if(cvWaitKey(1)==Keyvalue);
int c=cvWaitKey(1);//使程序暂停,等待用户触发一按键操作,不运行其他代码,直到键盘值为key的响应之后
//暂停,暂停时执行cvSetMouseCallback(),回调onMouse()函数
if(c == 'p') 
{
pause = true;         //pause初始值为false
//参数1:窗口的名字,参数2:指定窗口里每次鼠标时间发生的时候,被调用的函数指针,比如onMouse()
//这个函数的原型应该为void Foo(int event, int x, int y, int flags, void* param);
//参数3:用户定义的传递到cvSetMouseCallback 函数调用的参数,可设为NULL
cvSetMouseCallback( "Meanshift", onMouse, 0 );
}
//当pause的值为true,执行下面while循环
while(pause){
if(cvWaitKey(0) == 'p')//cvWaitKey(0),即该程序停在显示函数处,不运行其他代码,无限制地等待按键事件
pause = false;          //等按下P键就继续,pause设为false
}
cvShowImage("Meanshift",current);//在Meanshift窗口中显示当前帧
current = cvQueryFrame(capture); //抓取一帧
}
//窗口属性标志。可以选择CV_WINDOW_AUTOSIZE(1)和0两种值。
//CV_WINDOW_AUTOSIZE这个标志被设置后, 如果用户不能手动改变窗口大小,窗口大小会自动调整以适合被显示图像
//0表示以固定的窗口尺寸显示图像。
cvNamedWindow("Meanshift",1);
cvReleaseCapture(&capture);
cvDestroyWindow("Meanshift");
return 0;
}