标签:tld 目标跟踪
和大多数人一样,喜欢上TLD算法是从Kalal的一个视频开始的,一个令人心潮澎湃的视频。不得不说这个视频做的是相当的好,背景音乐更是恰到好处,当然别人的算法过硬才是关键,确实技高一筹啊,美中不足的可能就是Kalal憋足的发音了,哈哈,开玩笑啦。研究这个算法的现状是资源非常多,有很多博客,C++,matlab的源码都有,那我为什么还要写这个文章呢?原因是在阅读C++版本的代码时发现了bug,但目前据我所知,网络上没有一个人将其解释清楚,也有可能是我“搜商”不行哦,呵呵,也可能其他博主忙着别的事情,事情都有轻重缓急的嘛,这里我就来对其中的问题解释解释。才疏学浅,不对的地方请批评指正啊。
众所周知,TLD是Tracking-Learning-Detection的缩写,固然是Tracking,Learning,Detection三个部分,个人推荐阅读Kalal的博士论文,别的不说Related work综述部分的内容是非常不错的,对于想了解这三大领域的人来说是不错的选择。除此外Kalal在Tracking,Learning,Detection的三个研究成果也是非常不错的内容,TLD就不用多说了。
这里用一句话对TLD算法做出评价:不同于传统的单一的跟踪算法,frameto frame的tracking抑或offline trained的detection,TLD将这两种方法结合并提出了一种P-Nlearning的在线学习方法,有效的弥补了frame toframe tracking的eventual failure的缺点和使得需要大量先验样本的offline training的detection变为用少量样本半监督学习的online learning,算法simple,elegant而且effective。
Tracking
对于Tracking这里不做过多的讨论,要讨论的话可以用另外一篇文章来说了。TLD采用的是LK光流跟踪算法,opencv对其有实现。其主要思想是frame toframe的点跟踪。想了解更多,看相关论文是最好的选择。
Detection
TLD中Detection的模型如下图所示。这个模型是一个三层过滤的过程。
第一层主要是根据图像数据的方差来过滤的,如果sliding window过程产生的patch的方差比第一个初始化的目标图像patch的方差的50%(还可以是其他的阈值,论文中是这个阈值)还小,则认为该patch内不可能有目标,直接滤掉它,只将通过第一层的patch进行第二层的过滤。
第二层是一个集成分类器,也就是有多个弱分类器构成,集成分类器的分类结果由弱分类器的结果共同决定。训练这个集成分类器的特征表达方法是对一个图像patch上随机选择13个像素对,这些像素对的比较可以构成一个长度为13的二值序列,这个二值序列进而可以转化为一个整数值,那么这个整数值就是这个图像patch的特征。特征空间的大小就是2的13次方了。对于每个弱分类器,需要记录各个特征模式下的正样本统计值和负样本统计值(这些是先验的,也就是通过先验样本进行训练)。如果给定一个patch并计算了其特征值,那么该patch在一个弱分类器下的后验概率就是该特征模式下的(正样本统计值)/(正样本统计值+负样本统计值),对于每个弱分类器都进行相同的计算,然后对这些弱分类器产生的后验概率求均值,如果均值大于50%,这认为该patch可以通过第二层过滤,如果小于或等于50%则认为不通过。
第三层是一个最近邻分类器,该分类器中保存了已确认是正样本或负样本的图像patch作为其知识库,它们都是固定大小的(15*15)。将通过第二层的patch和分类器中的样本进行一一比较选择最相似的样本的类别作为该patch的分类输出,如果输出时正样本类别这认为这些patch可能是目标,让他们通过第三层。
对于如何初始化Detector,也就是根据第一个手动选中的目标patch,对其及其邻域进行采样,进而初始化detector的第二层和第三层,会在后面讨论bug的时候说明。
Learning
通过这三层过滤后的patch实际上已经很少了,然后将它们和Tracking的结果patch一起进行integration,最终的结果作为系统的跟踪结果,这个结果就已经被确认为目标了。这里有个问题,我们如何衡量这个跟踪结果呢,也就是这个结果会不会和Detector现有的知识存在偏差呢,如果有偏差我们又该怎样记住这个偏差,以防Detector以后不认识这样的偏差呢?说到这里,是不是感觉算法的本质就是人类自己在解决一个放在眼前的问题呢?确实是如此的。这里就是TLD算法的精髓所在了,P-N Leaning,这里也说明了P-N Learning只会在Tracking和Detection都有效的情况下才会运行。
Learning分为两个部分,一个部分是对Detector中的集成分类器的更新,另一个部分是对最近邻分类器的更新。P-N learning就属于第二个部分。
对于第一个部分,这里更新的主要的目的是增强集成分类器的鲁棒性和一般性,对于integration的结果,也就是一个boundingbox,把其邻域中比较接近其本身的(overlap超过某个阈值)bounding box被看作正样本,而那些远离其本身的(overlap小于某个阈值)boundingbox被看作负样本。用这些采得的样本去更新集成分类器,这里涉及一个采样过程,这个过程后面讨论一个bug的时候会介绍。
对于第二个部分,这里涉及P expert和N expert:
P expert主要衡量现有的最近邻分类器所持有的知识是否能够正确的辨认出integration的结果,从另一个角度来说是衡量了tracking和detection的结果之间是否有很大的差异,因为integration的结果是由tracking和detection的结果共同决定的,如果差异很大的最近邻分类器就要学习这种差异。简单来讲就是看对于这个integration的结果,我们已经确定其是目标了,那么再对其进行最近邻分类,如果结果表示其为一个负样本类,那么就说明最近邻分类器是不认识这个目标样本的,应该将其加入最近邻分类器的知识库;这里衡量的度量标准是Conservative Similarity,较大的值表示是目标的可能性越大,如果integration的结果的ConservativeSimilarity小于某个阈值,也就是说最近邻分类器所持有的知识不足以认出这个目标patch,则应该将该结果作为正样本加入最近邻分类器的知识库。
N expert主要衡量现有的最近邻分类器所持有的知识是否能够正确的辨认出在integration的结果邻域中远离其的(overlap小于某个阈值)bounding box。简单来讲对于这些远离integration结果的bounding box我们已经知道其是负样本了,如果对其进行最近邻分类,结果表示其是正样本,那么就说明最近邻分类器是不认识这个负样本的,应该将这个负样本加入最近邻分类器的知识库;这里衡量的度量标准是Relative Similarity,较大的值表示是目标的可能性越大,如果integration的结果的RelativeSimilarity大于某个阈值,也就是说最近邻分类器所持有的知识不足以认出该负样本,则应将该负样本加入最近邻分类器的知识库。对于Relative Similarity和ConservativeSimilarity的区别和计算方式可以参看Kala的相关论文。
P-N Learning在C++版本的TLD算法实现中的代码段如下:
/** * @brief update the NN classifer, i.e. pEx and nEx. * given a positive example and several negative examples, * if each example‘s relative similarity satisfies the thresholds * thr_nnP and thr_nnN, then use this example to update pEx or nEx * * @param nn_examples a positive example and several negative examples **/ void FerNNClassifier::trainNN(const vector<cv::Mat>& nn_examples){//some inprovement could be made here float conf,dummy; vector<int> y(nn_examples.size(),0); y[0]=1; vector<int> isin; for (int i=0;i<nn_examples.size();i++){ // For each example NNConf(nn_examples[i],isin,conf,dummy); // Measure Relative similarity //P expert: if the conservative similarity of the integration fromtracking and detection //is below thr_nnp and that means that the results of tracker and detector dipicted as negative, //however it is highly recomended as positive. // So detector needs to learn this. if (y[i]==1 && dummy/*conf*/<=thr_nnP){ // if y(i) == 1 && conf1 <= tld.model.thr_nn % 0.65 if (isin[1]<0){ // if isnan(isin(2)) pEx = vector<Mat>(1,nn_examples[i]); // tld.pex = x(:,i); continue; // continue; } // end //pEx.insert(pEx.begin()+isin[1],nn_examples[i]); // tld.pex = [tld.pex(:,1:isin(2)) x(:,i) tld.pex(:,isin(2)+1:end)]; % add to model pEx.push_back(nn_examples[i]); } // end //N expert: if the relative similarity of bounding boxes which are far from integration result bounding box //(overlap < 0.2) is above thr_nnN and that means these boxes shold be background however are more //than 50% confident that they depict the object. //So detector needs to learn this. if(y[i]==0 && conf>thr_nnN) // if y(i) == 0 && conf1 > 0.5 nEx.push_back(nn_examples[i]); // tld.nex = [tld.nex x(:,i)]; } // end acum++; printf("%d. Trained NN examples: %d positive %d negative\n",acum,(int)pEx.size(),(int)nEx.size()); } // end
Bug讨论
前面已经提到过了,在学习采样样本的时候的采样过程中,C++版本的源码是有错误的,有人也提到过了,但是没有把这个问题讲清楚。先给出我修改后的代码,再分析为什么这样修改。
/** * @breif Generate Positive data * * @Inputs: * - good_boxes (bbP) * - best_box (bbP0) * - frame (im0) * * @Outputs: * - Positive fern features (pX) * - Positive NN examples (pEx) **/ void TLD::generatePositiveData(const Mat& frame, int num_warps){ //Scalar struct restore RGB value Scalar mean; Scalar stdev; //frame(best_box) //Mat frame2 = frame(Rect(2,2,9,9));//share the same data //Mat frame2 = frame(Rect(2,2,9,9)).clone();//use the copy data getPattern(frame(best_box),pEx,mean,stdev); //Get Fern features on warped patches Mat img; Mat warped; GaussianBlur(frame,img,Size(9,9),1.5); RNG& rng = theRNG(); Point2f pt(bbhull.x+(bbhull.width-1)*0.5f,bbhull.y+(bbhull.height-1)*0.5f); vector<int> fern(classifier.getNumStructs()); pX.clear(); Mat img1 = img; Mat patch; if (pX.capacity()<num_warps*good_boxes.size()) pX.reserve(num_warps*good_boxes.size()); for (int i=0;i<num_warps;i++){ if (i>0) { generator(img1,pt,warped,bbhull.size(),rng); int p=0; for(int i=bbhull.y; i<bbhull.y+bbhull.height; i++) { int q=0; for(int j=bbhull.x; j<bbhull.x+bbhull.width; j++) { img.at<uchar>(i,j) = warped.at<uchar>(p,q); q++; } p++; } } cv::namedWindow("ImgLearning"); cv::imshow("ImgLearning", img); int idx; for (int b=0;b<good_boxes.size();b++){ idx=good_boxes[b]; patch = img(grid[idx]); classifier.getFeatures(patch,grid[idx].sidx,fern); pX.push_back(make_pair(fern,1)); } } printf("Positive examples generated: ferns:%d NN:1\n",(int)pX.size()); }
之所以没把源代码中的bug讲清楚,主要的原因还是不清楚generator()这个函数是怎么用的。opencv的reference文档实际上做的不太好啊,只给出了接口部分,并没有给出如何使用该接口,如果有像标准C++这样的reference文档就好了。使用文档不好找,怎么办呢?只好对它进行黑盒测试了,看看它的反应来猜测怎么用它了。下面给出一个测试代码的例子。
//test PatchGenerator PatchGenerator generator=PatchGenerator (0,0,5,true,1-0.02,1+0.02, -90*CV_PI/180,90*CV_PI/180, -90*CV_PI/180,90*CV_PI/180); RNG& rng = theRNG(); Mat src = imread("psu.jpg", CV_LOAD_IMAGE_GRAYSCALE); Mat img; GaussianBlur(src, img, Size(9,9),1.5); cv::namedWindow("SourceBL"); cv::imshow("SourceBL", img); Rect hull(60,20,60,80); Mat dst;//=img(hull).clone(); Point2f pt(hull.x+(hull.width-1)*0.5f,hull.y+(hull.height-1)*0.5f); Point2f pt1(0,0); for(int i=0; i<1; i++) { //img source //pt tranform area center //return tranform patch //return patch size //rng random affine condition generator(src,pt,dst,hull.size(),rng); string name; stringstream temp; temp << i+1; temp >> name; cv::namedWindow(name); cv::imshow(name, dst); } cv::namedWindow("Result"); cv::imshow("Result", img); waitKey();
测试结果表明generator(img1,pt,warped,bbhull.size(),rng); img1是输入的要对其进行变换的图像,pt是待变换区域的中心点,warped是变换后返回的区域, bbhull.size()是变换区域的大小,mg是一个随机数。注意这里不能对img1整幅图像进行变换,因为采样区域是目标位置及其邻域决定的,我们只需要目标区域及其领域变换即可。得到变换后的区域后,再把这个变换区域赋值给img1对应的区域,然后根据img1进行后续的采样。下面的代码可以查看变换后的结果。
cv::namedWindow("ImgLearning");
cv::imshow("ImgLearning", img);
本文出自 “Remys” 博客,谢绝转载!
标签:tld 目标跟踪
原文地址:http://remyspot.blog.51cto.com/8218746/1631943