TLD算法流程说明--episode1

来源:互联网 发布:非结构化数据举例 编辑:程序博客网 时间:2024/04/30 04:41

TLD(Tracking LearningDetector),包括trackingmodelingdetection,其中tracking工作时基于Lucas-Kanade光流法的。modeling学习的过程可以正负反馈,得到较好的学习结果,对于他的学习过程,作者在另一篇文章中又详细介绍了P-Nlearning这种学习算法。detection的部分用的是多个分类器的级联。对于特征的选择,他提出了一种基于LBP特征的2bitBP特征

以下解读完全基于arthurv的程序,中间参考了BeyondEgo和zouxy09的博客,在此也表示感谢,由于个人能力有限,难免有不足纰漏之处,欢迎指正,转载请注明出处,谢谢!

整个算法是按照以下流程进行:

一、   读取参数

通过myParam.yml获取参数,通过文件或者用户鼠标选中初始跟踪区域BoundingBox

二、   用第一帧图像last_grayBoundingBox进行初始化工作,调用tld.init(last_gray,box,bb_file),其中bb_file用于存储后面的跟踪结果。init()具体分为如下几个部分:

1.     buildGrid(frame,box)

这个函数输入是当前帧frame和初始跟踪区域box,主要功能是获取当前帧中的所有扫描窗口,这些窗口有21个尺度,缩放系数是1.2,也即以1为中间尺度,进行101.2倍的缩小和101.2倍的放大。在每个尺度下,扫描窗口的步长为宽高的10%(SHIFT=0.1),从而得到所有的扫描窗口存放在容器grid中,这个grid的每个元素包含6个属性:当前扫描窗口左上角的x坐标、y坐标、宽度w、高度h、与初始跟踪区域box的重叠率overlap、当前扫描窗口所在的尺度sidx。其中重叠率的定义是:两个窗口的交集/两个窗口的并集。与此同时,如果当前窗口的宽度和高度不小于最小窗口的阈值min_win(固定值15,从myParam.yml中获取),就将这个尺寸放到scales容器中,由于有min_win的限制,所以某些特别消的尺度下的扫描窗口尺寸就不会放到scales中,故该容器长度小于等于21

2.     为各种变量或容器分配存储空间,包括积分图iisum、平方积分图iisqsum、存放good_boxes和存放bad_boxes的容器,等等。

3.     getOverlappingBoxes(box,num_closest_init)

这个函数输入是初始跟踪窗口box,以及good_boxes中要放入的扫描窗口的数量closest_init(初始值10是从myParam.yml中获取),主要功能是:

A.     找出与初始跟踪窗口box重叠率最高的扫描窗口best_box

B.     找出与初始跟踪窗口box重叠率超过0.6的,将其当成较好的扫描窗口,可以用于后面产生正样本,放到good_boxes

C.     找出与初始跟踪窗口box重叠率小于0.2的,将其当成较差的扫描窗口,可以用于后面产生负样本,放到bad_boxes

D.     调用getBBHull()函数,获取good_boxes中所有窗口并集的外接矩形

4.     classifier.prepare(scales)

这个函数输入是buildGrid函数中得到的scales容器,主要功能是进行分类器的准备。TLD中的分类器由三个部分组成:方差分类器(用于结合积分平方图以剔除bad_boxes中方差较小的负样本)Fern分类器、最近邻NN分类器。这三个分类器是级联的,也就是说每个扫描窗口必须都通过这些分类器,才能认为是含有前景目标的,才可能是当前帧的跟踪结果。这里的准备主要是针对Fern分类器进行的,所以先说说Fern分类器,以便于说明这个函数做了哪些准备。

Fern翻译是“蕨类”,但从自己的知识构成看,更愿意理解为森林,这个森林有nstructs(值为10)棵树对应nstructs个基本的分类器,每个基本的分类器有structSize(值为13)个节点,每个节点的作用就是根据某种判定规则指定这个节点要往左子树还是往右子树进行搜索,最终搜索到leaf。因此这棵树可以看成深度也是structSize层,每层只有一个节点,这样每棵树其实就是一条从rootleaf的一条路径而没有其它分支。后来再搜索关于蕨类植物的照片,发现用这个词确实更为准确,它就是从根茎部长出多个叶柄(对应多棵树),每个叶柄上有多个关节(对应节点),每个关节上左右生出羽片(没有对应,可以忽略),二每个叶柄上也有一个孢子囊群(对应叶子)。如果还要更形象一点,可以类比为10爪鱼,这个10爪鱼有10个爪,每个爪上有13个关节。

 

           [原创]TLD算法流程说明--episode1           [原创]TLD算法流程说明--episode1

每个节点在进行判断的时候,所依据的规则是:在当前输入的图像片patch中随机去两个点(x1,y1)(x2,y2),计算这两点的像素值f1f2,若f1>f2则返回1,也即往右子树的路径走,反之返回0,往左子树的路径走。而这里对Fern分类器的准备工作就是为features[s][i]申请空间,这里的s表示scales中的尺度索引,取值范围是[0,scales.size()-1]之间的整数,i表示第i个节点,取值范围是[0,129],这里的12910*13-1得到的,对应这个森林中所有树的所有节点。从而features[s][i]表示当前尺度s下第i个节点存储的两个随机点的坐标,其坐标是由当前尺度下的宽高度乘以一个(0,1)之间的随机数得到的,在用每个节点确定路径的时候,就是用此节点的两个随机坐标在输入图像片patch中的像素值进行比较来得到01,每棵树的13个节点就得到一个长度为13的二进制编码x,这样的二进制编码取值有2.^13种可能,而每一个x又对应一个后验概率post=pNum/nNum,其中pNumnNum分别对应正负样本的个数。由于森林(Fern)中有10棵树,从而可以得到10个后验概率,将10个后验概率平均后若结果大于阈值thr_nn(初始经验值是0.65,从myParam.yml中获取,后面会更新),则认为该图像片patch含有跟踪目标。由于x2.^13种可能,所以后验概率post、正样本数目pNum和负样本数目nNum的容器规模也是2.^13,在本函数的最后也就是为其申请空间。

5.     generatePositiveData(frame,num_warps_init)

这个函数的输入是当前帧frame、要进行仿射变换的次数num_warps_init(初始值20是从myParam.yml中获取),主要功能是将good_boxes中的10个扫描窗口进行num_warps_init次仿射变换,这样共得到10*20个窗口,做为正样本数据。可以分为以下几个步骤:

5.1    调用getPattern(frame(best_box),pEx,mean,stdev),对于framebest_box区域,将其缩放为patch_size*patch_size(15*15)大小然后计算均值与方差,将每个元素的像素值减去均值使得得到的patern的均值为0

5.2    调用GaussianBlur(frame,img,Size(9,9),1.5),对整个frame进行高斯平滑以得到img,高斯模板大小为9*9X方向的方差为1.5,同时利用第3步得到的bbhull获取img的该区域。进行仿射变换是用cv::PatchGenerator做为生成器,调用其关于“()”符号的重载运算函数来进行。注意这里感觉并没有用到防身变换后的结果来进行处理,所以可能是代码有误。代码中是用generator(frame,pt,warped,bbhull.size(),rng),其中frame是当前的帧,ptbbhull窗口的中心坐标,warped是进行仿射变换后的结果,bbhull.size()bbhull的宽高度,rng是一个随机数。这个函数是根据bbhull.size()的尺寸和rng生成一个仿射矩阵,对frame进行仿射变换得到结果放到warped中,也即变换前尺寸是原始frame的尺寸,变换后就是bbhull的尺寸了。具体可以参见planardetect.cpp中关于“()”运算符的重载函数。然而其后面并没有对warped进行任何处理,所以感觉有错误。后来查看beyondEgo的博客(http://quandb2007.blog.163.com/blog/static/4187887520135873314523/)看到里面的说明也印证了有别人和自己一样的想法。里面提到了两种修改方法,现罗列如下:

法一:将generator(frame,pt,warped,bbhull.size(),rng) 改成 generator(img,pt,img,frame.size(),rng)

法二:将整个for循环内容修改为:

           for(int i=0;i<num_warps;i++)

                            {

                                        if(i == 0)

                                         {

                                                      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));

                                                       }

                                          }

                                        else

                                          {

                                                     generator(img,pt,warped,bbhull.size(),rng);

                                                    for (int b = 0; b < good_boxes.size(); b++)

                                                     {

                                                                    idx=good_boxes[b];

                                                                    Rect region(grid[idx].x-bbhull.x, grid[idx].y - bbhull.y,grid[idx].width, grid[idx].height);

                                                                    patch = warped(region);

                                                                    classifier.getFeatures(patch, grid[idx].sidx, fern);

                                                                    pX.push_back(make_pair(fern,1));

                                                     }

                                         }

}

对于法一,是把进行仿射变换的“源”从高斯平滑前的frame换成高斯平滑后的img,其结果也存放到img中,也即仿射变换后的尺寸仍然是img.size(),对于法一还好理解。但是对于法二,其仿射变换变成了generator(img,pt,warped,bbhull.size(),rng),后面却用了个rect来限定这个区域,而rect左上角坐标的设置看起来挺费解,感觉就是将这个区域由原来的位置进行一点小移动并没有改变区域的形状。这里的修改还是有待验证和深入理解。

总之,这里是对good_boxes中每个扫描窗口进行20次仿射变换后得到200patch,然后把patch放到getFeature(patch,grid[idx].sidx,fern)函数中,按照第4点中所述的规则得到长度为1301二进制编码放到fern中,由于fern中有10棵树,故其规模为10,每个元素存放一个int型值,这个int型值写成二进制形式就是对应该树的长度为1301编码。之后把这些特征放到正样本数据集pX中,其中makepair(fern,1)中第二个参数”1”表示正样本的类别标签,进而pX中会有200个正样本。

6.     meanStdDev(frame1(best_box),mean,stdev)

统计best_box的均值和标准差,取var为“标准差平方的一半”做为方差分类器的阈值

7.     integral(frame1,iisum,iisqsum)

计算积分图和平方积分图,并调用getVar(best_box, iisum, iisqsum)利用积分图和平方积分图进行快速计算以得到best_box中的方差,取方差的一半做为检测方差vr

8.     generateNegativeData(frame1)

由于在跟踪时需要注意到跟踪的目标是否发生远离或靠近镜头,以及旋转和位移的变化所以加入了仿射变换,但是对于负样本而言,它本身就是负样本,进行仿射变换没有什么意义,所以无需进行。由于在3步中重叠率小于0.2的都放到bad_boxes中,所以其规模很大,这里就先把bad_boxes中的元素顺序打乱,之后把方差大于var*0.5bad_boxes放到pEx中作为负样本,而把方差较小的进行剔除。和第5步一样,也调用getFeature()获取负样本数据并放到nX中。另外,它还对打乱顺序的bad_boxes取了前bad_patches(固定值为100,从myParam.yml中读取)个,通过getPattern()将获取的pattern放到nEx中,作为近邻数据集的训练集用于后面对近邻分类器的训练。

generatePositiveDatagenerateNegativeData的区别在于:前者进行仿射变换,后者没有;前者无需打乱good_boxes,而后者打乱了bad_boxes,目的是为了随机获取前bad_patches个负样本作为后面近邻数据集的负样本训练集(关于近邻数据集的正样本训练集只有一个数据就是best_box

9.     将负样本集nX一分为二成为训练集nX和测试集nXT,将负样本近邻数据集nEx也一分为二成为近邻数据训练集nEx和近邻数据测试集nExT,而正样本集pX和正样本近邻数据集pEx无需划分。

10.    将上面所得的数据集进行如下划分:

10.1   将正样本集pX(good_boxes的区域仿射变换后的200个结果)和负样本集nX(bad_boxes中方差较大的区域选取一半出来)顺序打乱放到ferns_data[]中,用于训练Fern分类器,注意这里对于每个box所存的数据是10棵树分别对这个box的特征:也即10个长度为13的二进制编码。

10.2   将正样本近邻数据集pEx(其实只有一个数据,就是best_box所得到的pattern)和负样本近邻数据集nEx(bad_boxes打乱顺序后前bad_patches/2=50个数据所得到的pattern)放到nn_data[]中,用于训练最近邻分类器。

10.3   负样本集的另一个nXT和负样本近邻数据集的另一半nExT组合起来,作为测试集,用于评价并修改得到最好的分类器阈值。

[原创]TLD算法流程说明--episode1

11.    classifier.trainF(ferns_data,2)

这个函数输入是正负样本集pXnX所存储的ferns_data[],第二个参数2表示bootstrap=2(但在函数中并没有看出其作用)。主要功能是:对每一个样本ferns_data[i],如果样本是正样本标签,先用measure_forest函数,找出该样本box中所有树的所有特征值,对应的后验概率累加值,该累加值如果小于正样本阈值(0.6*nstructs,0.6这个经验值是Fern分类器的阈值,,初始化时从myParam.yml中读取,后面会用测试集来评估修改,找到最优),也就是输入的是正样本,却被分类成负样本了,出现了分类错误,所以就把该样本添加到正样本库使pNum=pNum+1,同时用update函数更新后验概率。对于负样本,同样,如果出现负样本分类错误,就添加到负样本库使nNum=nNum+1update函数有三个参数,第一个参数是该box对应的10棵树fern[],第二个参数要进行更新的是正样本库还是负样本库,1表示更新正样本库的数目,0表示更新负样本库的数目,第三个参数表示要更新的数目,在整个程序中所有的调用该值都是取1,也即每次都是对样本库数目增加1(为何另一边的不用相应地减小1),这也跟上面提到的分错的样本放到对应的库是同样的意思,因为每次只能判断一个样本是否有分错,所以更新的数目也只能是1。而在更新数目的同时,也更新了后验概率值,按照post=pNum/nNum的式子来更新

[原创]TLD算法流程说明--episode1

12.    classifier.trainNN(nn_data)

这个函数的输入是正样本近邻数据集pEx和负样本近邻数据集nEx所存储的nn_data[]。对每一个样本nn_data,如果标签是正样本,通过NNConf(nn_examples[i],isin, conf, dummy)计算输入图像片与在线模型之间的相关相似度conf,如果相关相似度小于0.65,则认为其不含有前景目标,也就是分类错误了,这时候就把它加到正样本库。然后就通过pEx.push_back(nn_examples[i]);将该样本添加到pEx正样本库中;同样,如果出现负样本分类错误,就添加到负样本库。

12.1   NNConf()函数中对于输入的图像片patch,先遍历在线模型中的正样本近邻数据集pEx中的box(第一次其实就是best_box,后面会在线更新),调用matchTemplate()计算匹配度ncc,再由ncc得到相似度nccP,并找出ncc中的最大者maxP;同样的方式也遍历在线模型中的负样本近邻数据集nEx中的box来找出maxN

12.2   接下来会计算1-maxP1-maxN,个人理解为:正样本不相似的最小程度、负样本不相似的最小程度。然后就计算相关相似度rsconf=(1-maxN)/[(1-maxP)+(1-maxN)],也即正负样本不相似的最小情况下,负样本不相似所占比例就定义为相关相似度,如果负样本不相似所占比重越大,那么该patchpatch与负样本不相似的可能性越大,从而相关相似度越高(好像有点拗口,个人是从对偶问题的角度理解的)

12.3   关于保守相似度csconf是这样得到的,在pEx的前半部分数据中,如果有对maxP进行更新,那么把此时的maxP放到csmaxP中,也即csmaxP记录正样本近邻数据集pEx的前半部分数据中与输入图像片的最大相似度,然后csconf=(1-maxN)/[(1-maxN)+(1-csmaxP)],由于csmaxP不超过maxP,所以csconf不超过rsconf,也即在认为当前输入图像片patch与正样本近邻数据集数据相似的问题上,对同个patchrsconf的度量更为“保守”。在第一个进行训练的时候,是否保守没有多大意义,所以在init()中第一次进行trainNN()时,会将rsconf所得到的值丢弃。

12.4   isin中存放三个int型值,初始化全为-1。第一个如果取值为1,则表示NNConf()在计算输入图像片patch与在线模型pEx中的box时发现在线模型中有一个与其相似度超过阈值ncc_thesame(固定值0.95,从myParam.yml中读取),此时会把这个patch也放到在线模型的pEx中,所以第一个取值为1就表示已经把当前输入图像片patch放到pEx中。第二个的取值依赖于第1个的取值,如果第一个取值为-1,那么第二个的取值就是-1,如果第一个的取值是1,那么第二个的取值就是在遍历在线模型时找到的第一个与输入图像片patch相似度超过ncc_thesamebox的索引。第三个意义与第一个接近,不同的地方只在于第一个是对应在线模型的正样本近邻数据集pEx,第三个是对应在线模型的负样本近邻数据集nEx

13.    classifier.evaluateTh(nXT,nExT)

这个函数的输入是负样本数据集的后半部分nXT(存放feature)和负样本近邻数据集(存放pattern)的后半部分nExT,主要功能是将这两个作为测试集用来校正阈值thr_fernthr_nnthr_nn_valid

13.1   更新thr_fern:对nXT中的每个样本,用measure_forest()函数找出10棵树对这个box01码,由01码找到对应的后验概率,将10个后验概率累加后求平均,将平均值与thr_fern(初始值0.6,从myParam.yml中获取)比较,如果超过thr_fern则将thr_fern更新为这个平均值。也即对于nXT中所有的box10棵树都会对其投票得到一个后验概率,将所有后验概率取平均后,比较所有的box,取后验概率均值最大的那个box所对应的平均值放到thr_fern中。

13.2   更新thr_nn:对于负样本近邻数据nExT测试集中的每个样本,用NNConf()函数计算它与在线模型pExnEx中数据的相似度来得到相关相似度conf(与第一次训练NN分类器一样,这里得到的保守相似度也被丢弃了),如果conf大于阈值thr_nn(初始值0.65,从myParam.yml中获取),则更新thr_nnconf

13.3   更新thr_nn_valid:如果更新后的thr_nn大于thr_nn_valid(初始值0.7,从myParam.yml中获取),那么更新thr_nn_valid值为thr_nn

0 0
原创粉丝点击