TLD(Tracking LearningDetector),包括tracking、modeling、detection,其中tracking工作时基于Lucas-Kanade光流法的。modeling学习的过程可以正负反馈,得到较好的学习结果,对于他的学习过程,作者在另一篇文章中又详细介绍了P-Nlearning这种学习算法。detection的部分用的是多个分类器的级联。对于特征的选择,他提出了一种基于LBP特征的2bitBP特征。
以下解读完全基于arthurv的程序,中间参考了BeyondEgo和zouxy09的博客,在此也表示感谢,由于个人能力有限,难免有不足纰漏之处,欢迎指正,转载请注明出处,谢谢!
整个算法是按照以下流程进行:
一、
通过myParam.yml获取参数,通过文件或者用户鼠标选中初始跟踪区域BoundingBox。
二、
1.
这个函数输入是当前帧frame和初始跟踪区域box,主要功能是获取当前帧中的所有扫描窗口,这些窗口有21个尺度,缩放系数是1.2,也即以1为中间尺度,进行10次1.2倍的缩小和10次1.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.
3.
这个函数输入是初始跟踪窗口box,以及good_boxes中要放入的扫描窗口的数量closest_init(初始值10是从myParam.yml中获取),主要功能是:
A.
B.
C.
D.
4.
这个函数输入是buildGrid函数中得到的scales容器,主要功能是进行分类器的准备。TLD中的分类器由三个部分组成:方差分类器(用于结合积分平方图以剔除bad_boxes中方差较小的负样本)、Fern分类器、最近邻NN分类器。这三个分类器是级联的,也就是说每个扫描窗口必须都通过这些分类器,才能认为是含有前景目标的,才可能是当前帧的跟踪结果。这里的准备主要是针对Fern分类器进行的,所以先说说Fern分类器,以便于说明这个函数做了哪些准备。
Fern翻译是“蕨类”,但从自己的知识构成看,更愿意理解为森林,这个森林有nstructs(值为10)棵树对应nstructs个基本的分类器,每个基本的分类器有structSize(值为13)个节点,每个节点的作用就是根据某种判定规则指定这个节点要往左子树还是往右子树进行搜索,最终搜索到leaf。因此这棵树可以看成深度也是structSize层,每层只有一个节点,这样每棵树其实就是一条从root到leaf的一条路径而没有其它分支。后来再搜索关于蕨类植物的照片,发现用这个词确实更为准确,它就是从根茎部长出多个叶柄(对应多棵树),每个叶柄上有多个关节(对应节点),每个关节上左右生出羽片(没有对应,可以忽略),二每个叶柄上也有一个孢子囊群(对应叶子)。如果还要更形象一点,可以类比为10爪鱼,这个10爪鱼有10个爪,每个爪上有13个关节。
每个节点在进行判断的时候,所依据的规则是:在当前输入的图像片patch中随机去两个点(x1,y1)和(x2,y2),计算这两点的像素值f1和f2,若f1>f2则返回1,也即往右子树的路径走,反之返回0,往左子树的路径走。而这里对Fern分类器的准备工作就是为features[s][i]申请空间,这里的s表示scales中的尺度索引,取值范围是[0,scales.size()-1]之间的整数,i表示第i个节点,取值范围是[0,129],这里的129是10*13-1得到的,对应这个森林中所有树的所有节点。从而features[s][i]表示当前尺度s下第i个节点存储的两个随机点的坐标,其坐标是由当前尺度下的宽高度乘以一个(0,1)之间的随机数得到的,在用每个节点确定路径的时候,就是用此节点的两个随机坐标在输入图像片patch中的像素值进行比较来得到0或1,每棵树的13个节点就得到一个长度为13的二进制编码x,这样的二进制编码取值有2.^13种可能,而每一个x又对应一个后验概率post=pNum/nNum,其中pNum和nNum分别对应正负样本的个数。由于森林(Fern)中有10棵树,从而可以得到10个后验概率,将10个后验概率平均后若结果大于阈值thr_nn(初始经验值是0.65,从myParam.yml中获取,后面会更新),则认为该图像片patch含有跟踪目标。由于x有2.^13种可能,所以后验概率post、正样本数目pNum和负样本数目nNum的容器规模也是2.^13,在本函数的最后也就是为其申请空间。
5.
这个函数的输入是当前帧frame、要进行仿射变换的次数num_warps_init(初始值20是从myParam.yml中获取),主要功能是将good_boxes中的10个扫描窗口进行num_warps_init次仿射变换,这样共得到10*20个窗口,做为正样本数据。可以分为以下几个步骤:
5.1
5.2
法一:将generator(frame,pt,warped,bbhull.size(),rng) 改成 generator(img,pt,img,frame.size(),rng)
法二:将整个for循环内容修改为:
}
对于法一,是把进行仿射变换的“源”从高斯平滑前的frame换成高斯平滑后的img,其结果也存放到img中,也即仿射变换后的尺寸仍然是img.size(),对于法一还好理解。但是对于法二,其仿射变换变成了generator(img,pt,warped,bbhull.size(),rng),后面却用了个rect来限定这个区域,而rect左上角坐标的设置看起来挺费解,感觉就是将这个区域由原来的位置进行一点小移动并没有改变区域的形状。这里的修改还是有待验证和深入理解。
总之,这里是对good_boxes中每个扫描窗口进行20次仿射变换后得到200个patch,然后把patch放到getFeature(patch,grid[idx].sidx,fern)函数中,按照第4点中所述的规则得到长度为13的01二进制编码放到fern中,由于fern中有10棵树,故其规模为10,每个元素存放一个int型值,这个int型值写成二进制形式就是对应该树的长度为13的01编码。之后把这些特征放到正样本数据集pX中,其中makepair(fern,1)中第二个参数”1”表示正样本的类别标签,进而pX中会有200个正样本。
6.
统计best_box的均值和标准差,取var为“标准差平方的一半”做为方差分类器的阈值
7.
计算积分图和平方积分图,并调用getVar(best_box,
8.
由于在跟踪时需要注意到跟踪的目标是否发生远离或靠近镜头,以及旋转和位移的变化所以加入了仿射变换,但是对于负样本而言,它本身就是负样本,进行仿射变换没有什么意义,所以无需进行。由于在3步中重叠率小于0.2的都放到bad_boxes中,所以其规模很大,这里就先把bad_boxes中的元素顺序打乱,之后把方差大于var*0.5的bad_boxes放到pEx中作为负样本,而把方差较小的进行剔除。和第5步一样,也调用getFeature()获取负样本数据并放到nX中。另外,它还对打乱顺序的bad_boxes取了前bad_patches(固定值为100,从myParam.yml中读取)个,通过getPattern()将获取的pattern放到nEx中,作为近邻数据集的训练集用于后面对近邻分类器的训练。
generatePositiveData和generateNegativeData的区别在于:前者进行仿射变换,后者没有;前者无需打乱good_boxes,而后者打乱了bad_boxes,目的是为了随机获取前bad_patches个负样本作为后面近邻数据集的负样本训练集(关于近邻数据集的正样本训练集只有一个数据就是best_box)
9.
10.
10.1
10.2
10.3
11.
这个函数输入是正负样本集pX和nX所存储的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+1。update函数有三个参数,第一个参数是该box对应的10棵树fern[],第二个参数要进行更新的是正样本库还是负样本库,1表示更新正样本库的数目,0表示更新负样本库的数目,第三个参数表示要更新的数目,在整个程序中所有的调用该值都是取1,也即每次都是对样本库数目增加1(为何另一边的不用相应地减小1?),这也跟上面提到的分错的样本放到对应的库是同样的意思,因为每次只能判断一个样本是否有分错,所以更新的数目也只能是1。而在更新数目的同时,也更新了后验概率值,按照post=pNum/nNum的式子来更新
12.
这个函数的输入是正样本近邻数据集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
12.2
12.3
12.4
13.
这个函数的输入是负样本数据集的后半部分nXT(存放feature)和负样本近邻数据集(存放pattern)的后半部分nExT,主要功能是将这两个作为测试集用来校正阈值thr_fern、thr_nn、thr_nn_valid。
13.1
13.2
13.3