tldInit.m
function tld = tldInit(opt,tld)
lk(0);
if ~isempty(tld);
handle = tld.handle;
tld = opt;
tld.handle = handle;
else
tld = opt;
end
% INITIALIZE DETECTOR =====================================================
% Scanning grid
% tld.grid:一个6Xn(n表示不同有效尺度下共有多少个的网格)的矩阵,前四行组成列向量表示gridbox的4个顶点,5表示索引,6表示横向的分布点数
% tld.scales:有效尺度下gridbox的高和宽,为2Xm矩阵(m表示有效的尺度的个数)
[tld.grid tld.scales] = bb_scan(tld.source.bb,size(tld.source.im0.input),tld.model.min_win);
% Features
tld.nGrid = size(tld.grid,2);
% tld.model.num_trees:有10棵分类树
% tld.model.num_features:每棵树里有13个特性,这里是会有13个点对的比较
% tld.features:4*13X10的矩阵,为fern分类器选取的随机点对,4表示一个点对的四个坐标值
tld.features = tldGenerateFeatures(tld.model.num_trees,tld.model.num_features,0);
% Initialize Detector
fern(0); % cleanup
fern(1,tld.source.im0.input,tld.grid,tld.features,tld.scales); % allocate structures
% Temporal structures
tld.tmp.conf = zeros(1,tld.nGrid); % conf是网格属于正样本的概率
tld.tmp.patt = zeros(tld.model.num_trees,tld.nGrid); % 每个网格的patten值,即局部二值特征,为10×n的矩阵,n为全部网格数量
% RESULTS =================================================================
% Initialize Trajectory
tld.img = cell(1,length(tld.source.idx));
tld.snapshot= cell(1,length(tld.source.idx));
tld.dt = cell(1,length(tld.source.idx));
tld.bb = nan(4,length(tld.source.idx));
tld.center = nan(2,length(tld.source.idx));
tld.conf = nan(1,length(tld.source.idx));
tld.valid = nan(1,length(tld.source.idx)); % 每一帧输出的预测BB是否有效?如果无效,则不运行学习模块
tld.size = nan(1,length(tld.source.idx));
tld.trackerfailure = nan(1,length(tld.source.idx)); % 这个好像没啥用
tld.draw = zeros(2,0);
tld.pts = zeros(2,0);
% Fill first fields
tld.img{1} = tld.source.im0;
tld.bb(:,1) = tld.source.bb;
tld.center(:,1) = bb_center(tld.bb(:,1));
tld.conf(1) = 1;
tld.valid(1)= 1;
tld.size(1) = 1;
% TRAIN DETECTOR ==========================================================
% Initialize structures
tld.imgsize = size(tld.source.im0.input);
tld.X = cell(1,length(tld.source.idx)); % training data for fern
tld.Y = cell(1,length(tld.source.idx));
tld.pEx = cell(1,length(tld.source.idx)); % training data for NN
tld.nEx = cell(1,length(tld.source.idx));
% tld.source.bb:用户目标标定框
% tld.grid: 生成的gridbox信息矩阵
% overlap:一维行向量,记录各个gridbox与初始BB的重叠率
overlap = bb_overlap(tld.source.bb,tld.grid); % bottleneck
% Target (display only)
tld.target = img_patch(tld.img{1}.input,tld.bb(:,1));
% Generate Positive Examples
[pX,pEx,bbP] = tldGeneratePositiveData(tld,overlap,tld.img{1},tld.p_par_init);
pY = ones(1,size(pX,2)); % 正样本标签,后面会看到
% disp(['# P patterns: ' num2str(size(pX,2))]);
% disp(['# P patches : ' num2str(size(pEx,2))]);
% Correct initial bbox
tld.bb(:,1) = bbP(1:4,:); % 最靠近初始BB的gridbox
% Variance threshold
tld.var = var(pEx(:,1))/2; % 方差分类器所要用到的方差值
% disp(['Variance : ' num2str(tld.var)]);
% Generate Negative Examples
% 假设归一化后的patch面积为s,则nEx为s×100的矩阵,每一列代表一个图像patch,应该懂吧?
% 因为tldGenerateNegativeData将patch变成了一维的列向量
% 相当于最终会生成100个负样本图像patch存储在nEx中,然后nX为所有负样本的局部特征,为一个10×(负样本数量)的矩阵
[nX,nEx] = tldGenerateNegativeData(tld,overlap,tld.img{1});
% disp(['# N patterns: ' num2str(size(nX,2))]);
% disp(['# N patches : ' num2str(size(nEx,2))]);
% Split Negative Data to Training set and Validation set
% nx1和nx2各占50%的nx,而nEx1和nEx2同样也各占50%的nEx
[nX1,nX2,nEx1,nEx2] = tldSplitNegativeData(nX,nEx);
nY1 = zeros(1,size(nX1,2));
% Generate Apriori Negative Examples
%[anX,anEx] = tldGenerateAprioriData(tld);
%anY = zeros(1,size(anX,2));
% disp(['# apriori N patterns: ' num2str(size(anX,2))]);
% disp(['# apriori N patches : ' num2str(size(anEx,2))]);
% 存储正负样本patch
tld.pEx{1} = pEx; % save positive patches for later
tld.nEx{1} = nEx; % save negative patches for later
% tld.X相当于训练的输入懂吗?也就是已知标签的正负样本的比特特征
% 其实训练就可以理解为,根据正负样本的比特特征,以及其对应的标签,进行训练
tld.X{1} = [pX nX1];
tld.Y{1} = [pY nY1];
% 打乱训练样本
idx = randperm(size(tld.X{1},2));
tld.X{1} = tld.X{1}(:,idx);
tld.Y{1} = tld.Y{1}(:,idx);
% Train using training set ------------------------------------------------
% 对Fern分类器进行更新
% 可以看到,训练的其实就是蕨分类器
% 看到这里终于也理解了论文的原理,学习模块中提到的那个分类器其实就是fern分类器
bootstrap = 2; % 详见fern.cpp,为总循环遍历的次数
fern(2,tld.X{1},tld.Y{1},tld.model.thr_fern,bootstrap);
% Nearest Neightbour
tld.pex = [];
tld.nex = [];
% 训练最近邻分类器,其实就是将正负样本图像patch添加至目标模型中,直至其能区分现有的正负样本
% 在初始化阶段,正样本patch数量为1,负样本patch数量为50
tld = tldTrainNN(pEx,nEx1,tld);
tld.model.num_init = size(tld.pex,2);
% Estimate thresholds on validation set ----------------------------------
% Fern
% 得到fern分类器关于nX2的得分,然后取得分的最大值和原来设置的fern分类器阈值(0.5)比较
% 意为:假如对于负样本,fern分类器能够得到比0.5更高的分数,则意味着分类边界需要往上调
% 如果没有,则最低保持在0.5
conf_fern = fern(3,nX2);
tld.model.thr_fern = max(max(conf_fern)/tld.model.num_trees,tld.model.thr_fern);
% Nearest neighbor
% 这里也是,得到最近邻分类器对nEx2的得分
conf_nn = tldNN(nEx2,tld);
% 修正初始设置的阈值
tld.model.thr_nn = max(tld.model.thr_nn,max(conf_nn));
tld.model.thr_nn_valid = max(tld.model.thr_nn_valid,tld.model.thr_nn);
tldInit中比较重要的几个点:
1、什么是grid和scales?
grid其实就是一个6×n的矩阵,n代表生成的全部网格数量,不懂的可以搜一下窗口扫描法。每一列都有6行,前面4行代表每一个网格的坐标,第5行是一个索引,没有特别的意义,代表的是第几个有效尺度(后面会解释),第6行则是代表该索引下同一行会有多少个网格。
至于scales,则是一个2×m的矩阵,代表有效尺度下grid的高和宽,m代表的是有效尺度的总个数,可以通过设置断点查看变量值发现,grid中最后1列第5行的值,就是m的值。
那么什么是有效尺度呢?记得TLD源码中有一个参数叫min_win吗?这个参数限制了最小矩形框的大小,也就是说,如果设置的初始目标的尺寸小于min_win,那么跟踪就不会进行,同样的,网格是基于目标的尺寸生成的(参考bb_scan.m),如果生成的网格尺寸小于min_win,则同样也不考虑该网格,也就是说,这个尺寸(或者说尺度)的网格是一个无效值。所以其实gird中第5行的值没啥特别的意义。
2、tld.features有什么用?
注释没写很清楚,其实tld.features是一个结构体,包含两个属性tld.features.x和tld.features.type,tld.features.type是一个固定值"forest",而tld.features.x才是特征点矩阵,其为一个4*13×10的矩阵。对于每棵分类树,也就是矩阵中的每一列,都有52个值,从上到下代表了13个点对的坐标。
肯定有同学想到了,那么多个grid,每个grid的位置都不同,怎么能用一个52×10的矩阵表示所有grid中每对特征点的坐标呢?所以,这里的坐标是相对坐标!!!
还记得grid是一个6×n的矩阵吧?单看这个矩阵中的任意一列,也就是随机找一个grid,前面四行代表这个grid的绝对坐标, 这个坐标再加上features的相对坐标,是不是就能得到对于每一个grid中,用于比较的点对的最终坐标了呢?
按照这个思路继续下去,每个gird的大小不同,也就是其尺度不同,因此,对于不同的尺度,特征点对需要偏移不同的距离,从而适应所有的尺度。
在fern.cpp中,有一个变量叫做OFF,OFF指向一个大小为260m(m个尺度×10棵树×13对特征)的一维数组,从这个一维数组能够得到每棵树中的每对特征点,在每个尺度下的网格中的位置偏移量。
看到这里思路是不是顿时清晰起来了!
基于tld.features,以及fern.cpp中的OFF,就能够得到每一个尺度grid中,特征点对的坐标。然后基于这个坐标,执行像素值比对,就能得到各个grid的局部二值特征,进而用于fern分类器判断。
3、pX、nX与pEx、nEx的区别是什么?
首先以正样本为例,从[pX,pEx,bbP] = tldGeneratePositiveData(tld,overlap,tld.img{1},tld.p_par_init)看出pX和pEx均由tldGeneratePositiveData生成。再看tldGeneratePositiveData,发现pEx其实就是正样本的图像patch,其经过tldGetPattern之后,变成一个225行的矩阵,这个矩阵的每一列都代表一个归一化后的正样本图像patch,为什么是225?因为patchsize是[15 15](论文中说的,将图像patch归一化到15×15大小)。
那既然已经有了pEx作为图像patch,pX又是什么呢?观察tldGeneratePositiveData中的pX = [pX fern(5,im1,idxP,0)],发现pX由fern中的第5个分支代码生成。进入fern.cpp之后可以发,pX其实就是正样本在fern分类器中每一棵分类树对该样本grid的局部二值特征,所以在tldInit中,其为一个10×200的矩阵,矩阵的第i行第j列代表第j个正样本在第i棵树下的局部二值特征(参照TLD原文,初始化阶段生成200个正样本)。
生成pEx作为正样本的图像patch能理解,因为这里是论文中提到过的:在最近邻分类器中,正样本的图像patch需要用于和未知的图像patch计算相似度,进而进行分类。那pX又是做什么的呢?
仔细阅读tldInit,才明白,pEx是输入至最近邻分类器(NN)中计算相似度的,而pX则是输入至fern分类器中,计算得到fern分类器输出的置信度值的。
懂了吗?TLD检测器有三个模块:方差分类器、蕨分类器(fern)以及最近邻分类器,方差分类器在fern.cpp中实现,直接基于灰度方差进行判断。fern分类器则是在fern(4)分支中实现,其基于每个gird的局部二值特征来得到输出的置信度值。而最近邻分类器则在tldNN.m中实现,其输出一个基于像素灰度值得到的相似度值,并与分类阈值进行比较。
4、每次调用fern.cpp,到底干了什么?
这里结合我写的TLD matlab版源码理解Part2(随笔)和 博客 来理解。
- 首先是fern(0),这里相当于清除了fern分类器中所有的变量。
- 然后是fern(1,tld.source.im0.input,tld.grid,tld.features,tld.scales),这里相当于基于前面提到的grid、scales以及features,来分配fern分类器所需的内存空间和结构,并将其初始化。这里很重要,还记得上面提到的OFF吗?就是在这一段生成的。同时还生成了一些其它比较关键的数据结构,并对这些数据结构初始化。
- fern(2,tld.X{1},tld.Y{1},tld.model.thr_fern,bootstrap)则是在得到了正负样本的局部二值特征后,对fern中的随机森林进行具体的初始化。
- fern(3,nX2)则是简单地计算负样本二值特征在fern分类器中的置信度值,并直接输出,用于判断是否修改fern分类阈值。
- 比较关键的是tldGeneratePositiveData.m和tldGenerateNegativeData.m中对fern的调用,分别为pX = [pX fern(5,im1,idxP,0)]和[nX,status] = fern(5,img,idxN,tld.var/2),这里的作用其实就是计算正负样本grid的局部二值特征。
bb_scan.m
function [bb_out,sca] = bb_scan(bb, imsize,min_win)
SHIFT = 0.1; % 当滑动窗口时,通常需要定义一个步长就,这里将步长系数定义为0.1,也就是说,SHIFT×宽度=实际的步长
SCALE = 1.2.^[-10:10]; % 窗口尺寸缩放,从1.2^-10到1.2^10,为一个1×21的向量
MINBB = min_win;% min_win参数
% Chack if input bbox is smaller than minimum
% 如果初始bounding box的长或宽小于min_win,则直接不扫描,也就代表不进行后续的一系列跟踪操作
if min(bb_size(bb)) < MINBB
bb_out = [];
sca = [];
return;
end
% 基于初始BB大小和不同的尺度,生成不同大小的长和宽,bbW和bbH均为1×21的一维向量
bbW = round(bb_width(bb) .* SCALE);
bbH = round(bb_height(bb) .* SCALE);
% min(bbH,bbH)=bbH,所以这里就是bbH乘上步长系数,从而得到真正的移动步长
bbSHH = SHIFT * min(bbH,bbH);
bbSHW = SHIFT * min(bbH,bbW);
% 存储坐标的最小最大值
bbF = [2 2 imsize(2) imsize(1)]';
bbs = {};
sca = [];
idx = 1;
for i = 1:length(SCALE)
% 不考虑尺寸小于min_win的网格,也就是所谓的无效尺度
% 通过了这里的,就可以继续后面的代码,也就是相对应的有效尺度
if bbW(i) < MINBB || bbH(i) < MINBB, continue; end
% 基于有效尺度,生成在该尺度下,各网格坐标中的xmin值
left = round(bbF(1):bbSHW(i):bbF(3)-bbW(i)-1);
% 基于有效尺度,生成在该尺度下,各网格坐标中的ymin值
top = round(bbF(2):bbSHH(i):bbF(4)-bbH(i)-1);
% ntuples就是将left和top两两组合,假如left有255个,top也有255个,最终就会生成一个2×65025的矩阵,懂了吗?255×255=65025,不懂的去搜ntuples函数
% 这里是top在前,所以最终生成的矩阵中,第一行代表ymin,第二行代表xmin
grid = ntuples(top,left);
if isempty(grid), continue; end
% 每一个网格的坐标是如下所示生成的:
% 首先,第一行为grid中的第二行,也就是xmin,第二行为ymin
% 第三行和第四行则为xmin和ymin加上该尺度下的对应长宽值之后的xmax和ymax
% 第五行定义的就是有效尺度了,idx从1开始计数,只有当执行一次完整的循环时才加一,因此,idx就代表是第几个有效尺度,这里直接用idx乘上grid的列数,从而生成一行值全为idx的一维向量
% 第六行代表的是同一行有多少个网格,能看懂吧?left的长度就能够衡量一行有多少个网格,然后乘以gird的列数,从而生成一行值全为length(left)的一维向量
% 所以,只要尺度是一样的,最终生成的tld.grid中的第5行和第6行的值就是一样的。
bbs{end+1} = [grid(2,:); ...
grid(1,:); ...
grid(2,:)+bbW(i)-1; ...
grid(1,:)+bbH(i)-1; ...
idx*ones(1,size(grid,2));
length(left)*ones(1,size(grid,2));];
% sca存储的则是该尺度下对应的高和宽,能理解吧,就是前面说的tld.scales
sca = [sca [bbH(i); bbW(i)]];
idx = idx + 1;
end
bb_out = [];
for i = 1:length(bbs)
bb_out = [bb_out bbs{i}];
end
% for i = 1:length(bbs)
%
% if i-1 > 0
% idx = bb_overlap(bbs{i},bbs{i-1},2);
% bbs{i}(7,:) = idx;
% end
%
% if i+1 <= length(bbs)
% idx = bb_overlap(bbs{i},bbs{i+1},2);
% bbs{i}(8,:) = idx;
% end
%
% end
% disp(['MINBB: ' num2str(MINBB) ', bb: ' num2str(size(bbs,2))]);
% end
tldGenerateFeatures.m
function f = tldGenerateFeatures(nTREES, nFEAT, show)
SHI = 1/5;
SCA = 1;
OFF = SHI;
% 0:SHI:1即为[0, 0.2, 0.4, 0.6, 0.8, 1],1×6的向量
% ntuples将两个1×6的向量两两组合,生成一个2X36的矩阵
% repmat则将ntuples生成的2×36矩阵重复,最终得到一个4×36的矩阵
x = repmat(ntuples(0:SHI:1,0:SHI:1),2,1);
x = [x x + SHI/2]; % 前者为4X36矩阵,后者为矩阵偏移0.1后的结果,最终的x为一个4×72矩阵
k = size(x,2); % k = 36*2 = 72
r = x; r(3,:) = r(3,:) + (SCA*rand(1,k)+OFF);% x的第3行加上随机的1X72矩阵(0~1)再加上0.2
l = x; l(3,:) = l(3,:) - (SCA*rand(1,k)+OFF);% x的第3行减去随机的1X72矩阵(0~1)再减去0.2
t = x; t(4,:) = t(4,:) - (SCA*rand(1,k)+OFF);% x的第4行减去随机的1X72矩阵(0~1)再减去0.2
b = x; b(4,:) = b(4,:) + (SCA*rand(1,k)+OFF);% x的第4行加上随机的1X72矩阵(0~1)再加上0.2
x = [r l t b];% 4×288矩阵
% x([1 2],:) < 1 & x([1 2],:) > 0 表示选取x的第1行和第2行,然后判断选定行中的元素是否大于0且小于1
% all(..., 1) 是对生成的逻辑矩阵进行列方向上的逻辑与(AND)操作
% 即在上一步的基础上,再对每一列进行AND操作,生成一个逻辑向量
% 也就是说,对于x的第一行和第二行组成的矩阵,只有每一列中的2个元素都在(0,1)区间内,最终生成的idx中的对应位置的逻辑值才为1
% idx为1×288的一维向量
idx = all(x([1 2],:) < 1 & x([1 2],:) > 0,1);
x = x(:,idx);% 挑选满足条件的所有x的列
x(x > 1) = 1;% x元素中大于1的则用1代替此元素
x(x < 0) = 0;% x元素中小于0的则用0代替此元素
numF = size(x,2); % 看看现在还剩下多少列
% randperm(numF)从1~numF的数字序列随机打乱,这里是指把X的列打乱
x = x(:,randperm(numF));
% 取x的前nFEAT×nTREES=130列,x目前为4x130矩阵
x = x(:,1:nFEAT*nTREES);
% 把 x reshape成52×10的矩阵
x = reshape(x,4*nFEAT,nTREES);
% tld.features为一个结构体,里面包含了x和一个type值
f.x = x;
f.type = 'forest';
% 如果show为1,则可视化特征
if nargin == 3
if show
for i = 1:nTREES
F = 1+99*reshape(f.x(:,i),4,[]);
img = zeros(100,100);
imshow(img);
line(F([1 3],:),F([2 4],:),'linewidth',1,'color','w');
pause(.05);
end
end
end
tldGeneratePositiveData.m
这个代码其实主要传回了一个px和pEx,其中pX代表正样本的模式特征,对于初始化阶段,生成的pX是一个10×200的矩阵,因为有10棵树你懂吧?这个矩阵中的第i行第j列代表仿射变换后的正样本j在第i棵决策树上的特征。
然后pEx就是基于patchsize归一化后的图像块,但是代码中将他reshape成了一个一维向量。
参考博客的解释
pEx: 目标patch(像素块)经过imsize 到patchsize 即 15*15,然后再减去均值,变成一个列向量。
px : 是模式pattern 的值,每一列是10个值,每个值是13个二进制数的10进制,表示一个局部二值模式,每个框对应一列。
function [pX,pEx,bbP0] = tldGeneratePositiveData(tld,overlap,im0,p_par)
% 输入:
% overlap:一维行向量,记录各个网格与目标BB的重叠率
% tld.p_par_init:opt.p_par_init= struct('num_closest',10,'num_warps',20,'noise',5,'angle',20,'shift',0.02,'scale',0.02);
% 输出:
% pX:10 X length(idxP)*20 (length(idxP)<=10,20为'num_warps',20)的矩阵列向量表示一个gridbox的10棵树上的13位有效的code
% pEx:225X1的列向量,各元素值为原像素值减去像素均值
% bbP:最靠近BBOX的10个网格,每一列的列向量表示该gird box的4个顶点
pX = [];
pEx = [];%zeros(prod(tld.patchsize),numWarps);
% Get closest bbox
[~,idxP] = max(overlap);
% 得到距离目标BB最近的网格的坐标
bbP0 = tld.grid(1:4,idxP);
% Get overlapping bboxes
idxP = find(overlap > 0.6);
if length(idxP) > p_par.num_closest % 如果交并比在0.6以上的网格数量大于num_closest,也就是10
[~,sIdx] = sort(overlap(idxP),'descend'); % 则选取交并比最大的10个网格
idxP = idxP(sIdx(1:p_par.num_closest));
end
% 得到满足条件的网格
bbP = tld.grid(:,idxP);
% 如果没有满足条件的网格,则直接返回
if isempty(bbP), return; end
% Get hull
bbH = bb_hull(bbP); % 得到能包围所有bbp中boxes的最小矩形,bb_hull非常简单
cols = bbH(1):bbH(3);
rows = bbH(2):bbH(4);
im1 = im0;
% 返回一个15*15×length(bbP0)的一维向量
% 其实就是先基于bbP0提取得到多个图像patch
% 然后将其归一化到[15 15]的大小,然后直接将二维图像矩阵转换为一维向量,详细可以看代码,比较简单
pEx = tldGetPattern(im1,bbP0,tld.model.patchsize);
if tld.model.fliplr
pEx = [pEx tldGetPattern(im1,bbP0,tld.model.patchsize,1)];
end
% 进行20次仿射变换
for i = 1:p_par.num_warps
if i > 1
randomize = rand; % Sets the internal randomizer to the same state
%patch_input = img_patch(im0.input,bbH,randomize,p_par);
% 返回将画面进行仿射变换后的patch
patch_blur = img_patch(im0.blur,bbH,randomize,p_par);
% 把仿射变换后的图像放到原图像对应的位置
% 这里很重要,保证在C调用里的偏移的起始地址可以是一样的
im1.blur(rows,cols) = patch_blur;
%im1.input(rows,cols) = patch_input;
end
% Measures on blured image
% 单次返回10Xlength(idxP)的矩阵,列向量表示一个gridbox的10棵树上的13位code
% 对于初始化阶段,最终能生成一个10Xlength(idxP)*20的矩阵
pX = [pX fern(5,im1,idxP,0)];
% Measures on input image
%pEx(:,i) = tldGetPattern(im1,bbP0,tld.model.patchsize);
%pEx = [pEx tldGetPattern(im1,tld.grid(1:4,idxP),tld.model.patchsize)];
end
tldGenerateNegativeData
function [nX,nEx] = tldGenerateNegativeData(tld,overlap,img)
% Measure patterns on all bboxes that are far from initial bbox
idxN = find(overlap<tld.n_par.overlap);
% nx为10行idxn列的矩阵,每一列代表着overlap小于0.2的网格在每棵树下的比特特征
% 注意,只有status为1的列,才有比特特征值
% status为1的列是通过了方差检测的网格
[nX,status] = fern(5,img,idxN,tld.var/2);
idxN = idxN(status==1); % bboxes far and with big variance
nX = nX(:,status==1);
% Randomly select 'num_patches' bboxes and measure patches
idx = randvalues(1:length(idxN),tld.n_par.num_patches);
bb = tld.grid(:,idxN(idx));
% 得到100个随机选取的负样本的patch,注意这里的patch被转换成了一维向量,具体参考tldGetPattern
nEx = tldGetPattern(img,bb,tld.model.patchsize);
tldGetPattern
function pattern = tldGetPattern(img,bb,patchsize,flip)
% get patch under bounding box (bb), normalize it size, reshape to a column
% vector and normalize to zero mean and unit variance (ZMUV)
% initialize output variable
% 获取bb的数量
nBB = size(bb,2);
% 上面的英文注释说要将图像patch reshape成一维向量,所以作为输出的pattern按照下面这行代码定义
pattern = zeros(prod(patchsize),nBB);
if ~exist('flip','var')
flip= 0;
end
% for every bounding box
for i = 1:nBB
% sample patch
% 由BB得到图像patch,就是从图像中提取像素
patch = img_patch(img.input,bb(:,i));
% flip if needed
if flip
patch = fliplr(patch);
end
% normalize size to 'patchsize' and nomalize intensities to ZMUV
% tldPatch2Parrtern这个代码很简单,这里就是将图像patch先reshape到指定的patchsize
% 然后再转换为一维向量,然后减去像素值的均值,然后输出
pattern(:,i) = tldPatch2Pattern(patch,patchsize);
end