天天看点

Improved Dense Trajectory算法源代码的安装与详细分析

IDT算法是人体动作视频分类中性能最好的传统方法,在13年由LEAR实验室发表,前身是DT算法,IDT算法主要对相机运动进行了校正。DT算法文章:DT,IDT算法文章:IDT。

LEAR实验室提供了linux环境下的C++代码,可以在idt下载。官方提供的IDT代码只包含了提取特征的部分,而IDT算法中还需要用Fisher Vector编码特征,再通过SVM分类,这部分后面再学习。由于项目需要用到IDT提特征,所以仔细的学习了一下IDT的代码。

文章目录

    • 1 安装
      • 1.1 opencv2与ffmpeg的安装
      • 1.2 idt算法的运行
    • 2 IDT代码结构
      • 2.1 DenseTrackStab.cpp
        • 2.1.1 读取帧前的一些初始化部分
        • 2.1.2 循环读取视频帧(第一帧的部分)
        • 2.1.3 循环读取视频帧(其余帧的部分)
        • 2.1.4 逐尺度计算Track (算法核心部分)
        • 2.1.5 输出满足条件的Track
        • 2.1.6 密集采样新的特征点
      • 2.2 一些数据类型和函数
        • 2.2.1 Track类
        • 2.2.2 InitPry函数
        • 2.2.3 DescMat结构体和InitDescMat函数
        • 2.2.4 判断轨迹是否合法
      • 2.3 计算描述符的过程
        • 2.3.1 GetDesc函数(在空间上统计描述符信息)
        • 2.3.2 PrintDesc函数(在时间上统计描述符信息)
    • 3 Fisher Vector on IDT
    • 4 线性SVM

1 安装

IDT算法需要在linux环境下运行,建议在ubuntu 16.04中配置,我用ubuntu 20安装opencv2始终成功不了。

IDT主要用到两个库,opencv2和ffmpeg,ffmpeg版本无所谓,opencv貌似只能用2,我安装的是2.4.13,建议先装opencv2,再装ffmpeg

1.1 opencv2与ffmpeg的安装

Ubuntu16.04下安装OpenCV2.4.13

ffmpeg安装:

sudo add-apt-repository ppa:kirillshkrogalev/ffmpeg-next 
sudo apt-get update 
sudo apt-get install ffmpeg
           

查看是否成功安装ffmpeg:

ffmpeg -version
           

1.2 idt算法的运行

从官网下载最新版本的idt代码,首先解压

tar zxvf improved_trajectory_release.tar.gz
           

进入解压的文件夹

cd improved_trajectory_release
           

编译

sudo make
           

然后会生成一个release文件夹,两个cpp编译后的文件在其中,先将test_sequences中的视频文件和特征文件复制到release文件夹中,然后进入release文件夹

cd release
           

先用Video.cpp检验环境是否安装成功,

./Video person01_boxing_d1_uncomp.avi
           

然后,试着提取视频的idt特征,并保存为gz压缩文件,如果想看可视化效果,需要将DenseTrackStab.cpp开头的show_track设置为1,然后重新make一下

./DenseTrackStab person01_boxing_d1_uncomp.avi | gzip > out.features.gz
           

后面还可以和官方提供的特征文件对比,可以参照readme文件的步骤。

2 IDT代码结构

解压得到的IDT文件夹中有2个cpp文件和4个头文件,其中:

  • DenseTrackStab.cpp为主程序
  • Video.cpp用于测试环境是否安装成功
  • DenseTrackStab.h定义了一些参数和数据结构
  • Initialize.h定义了初始化相关的一些函数
  • Descriptors.h定义了一些计算轨迹或描述符时所需的函数
  • OpticalFlow.h定义了光流计算的内容,没有详细了解

一些主程序中用到的参数和数据结构定义于DenSeTrackStab.h中,这一块以及其他头文件的详细注释,可以直接下载我详细注释过的代码:https://pan.baidu.com/s/1nhlUQaNOIZVISZc-ZEHUfQ,提取码goef。

2.1 DenseTrackStab.cpp

2.1.1 读取帧前的一些初始化部分

Initialize.h中定义了解析命令行参数的函数arg_parse,当命令行参数中包含了视频中提取IDT特征的范围(start_frame, end_frame)返回true。

TrackInfo trackInfo;
DescInfo hogInfo, hofInfo, mbhInfo;
//初始化
InitTrackInfo(&trackInfo, track_length, init_gap);
InitDescInfo(&hogInfo, 8, false, patch_size, nxy_cell, nt_cell);
InitDescInfo(&hofInfo, 9, true, patch_size, nxy_cell, nt_cell);
InitDescInfo(&mbhInfo, 8, false, patch_size, nxy_cell, nt_cell);
           

这里TrackInfo结构体存储了轨迹最大长度、特征点采样间隔两个信息,而DescInfo存储了特征描述符的基本信息,包括nBins(描述符的方向数,HOF为9,其他为8)、计算描述符的时空网格的大小、一个描述符的维度、计算描述符的空间窗口大小。(这些变量存储的是轨迹和描述符的基本信息,而不是详细值)。

SeqInfo seqInfo;
InitSeqInfo(&seqInfo, video);
           

SeqInfo存储了输入视频的信息,包括帧长度、分辨率。

std::vector<Frame> bb_list;
	if(bb_file) {
		LoadBoundBox(bb_file, bb_list);
		assert(bb_list.size() == seqInfo.length); // bb_file中帧数要等于输入视频的帧数
	}
           

bb_file是可选项,该文件中是对应输入视频每一帧中,存在的人体的边界框信息,每一项的格式形如:

frame_id  a1 a2 a3 a4 a5  b1 b2 b3 b4 b5 ...
           

分别为帧序号,边界框的左上顶点坐标、右下顶点坐标和置信度(一帧可能有多个人体边界框)

if(flag)
	seqInfo.length = end_frame - start_frame + 1;
           

如果定义了视频中提取特征的范围,则需要更新seqInfo。

SurfFeatureDetector detector_surf(200);
SurfDescriptorExtractor extractor_surf(true, true);
           

定义SURF关键点检测器和SURF描述符提取器。

存储上一帧光流角点的容器、存储当前帧光流角点的容器(用于消除相机运动)

存储上一帧SURF特征点、存储当前帧SURF特征点(也是用于消除相机运动)

上面用了两种方法计算帧间匹配的特征点,这里将两种特征点合并,用于计算单应性矩阵,从而消除当前帧的相机运动,得到校正后的图像。

std::vector<KeyPoint> prev_kpts_surf, kpts_surf;
Mat prev_desc_surf, desc_surf;
           

存储上一帧和当前帧的 SURF特征点和SURF描述符,这两个值共同计算pts_surf

human_mask是根据bb_list计算出的人体遮罩矩阵,用0标记人体位置,在后面检测SURF特征和光流角点时避开人体区域。

存储绘制了轨迹的图像、上一帧灰度图、当前帧灰度图

std::vector<float> fscales(0);
std::vector<Size> sizes(0);
           

尺度金字塔的基本信息,分别含有尺度、对应分辨率信息。分辨率=原分辨率 / 当前尺度。

前一帧灰度图金字塔、当前帧灰度图金字塔、当前帧光流金字塔、当前帧校正了相机运动的光流金字塔。

前一阵多项式展开金字塔、当前帧多项式展开金字塔、当前帧校正了相机运动的多项式展开金字塔 (poly是用于计算光流的)

存储不同尺度下检测的IDT特征(Track类存储一个IDT特征,包括了轨迹和描述符信息,当Track的长度达到L=15,就是一个完整的轨迹了,然后将其从容器中删除。Track类详细解释参见2.2.1)

2.1.2 循环读取视频帧(第一帧的部分)

现在开进入while循环读取视频帧,但是第一帧比较特殊,需要根据第一帧的信息进行一些额外的初始化操作。

image.create(frame.size(), CV_8UC3);
grey.create(frame.size(), CV_8UC1);
prev_grey.create(frame.size(), CV_8UC1);
           

初始化image、gray、prev_gray。

根据图像的分辨率,初始化金字塔的基本信息(如果图像分辨率比较好,则无法创建最大层数的金字塔),该函数详细实现参见2.2.2

BuildPry(sizes, CV_8UC1, prev_grey_pyr);
BuildPry(sizes, CV_8UC1, grey_pyr);
BuildPry(sizes, CV_32FC2, flow_pyr);
BuildPry(sizes, CV_32FC2, flow_warp_pyr);
BuildPry(sizes, CV_32FC(5), prev_poly_pyr);
BuildPry(sizes, CV_32FC(5), poly_pyr);
BuildPry(sizes, CV_32FC(5), poly_warp_pyr);
           

根据建立的金字塔的基本信息,初始化上述的各种金字塔,这些金字塔分别存储不同尺度下的信息。

for(int iScale = 0; iScale < scale_num; iScale++) {
		//根据prev_gray创建前一帧灰度图金字塔(因为当前是视频的第一帧,所以只需要将对应信息保存在prev_xxx中就行)
		if(iScale == 0)
			prev_grey.copyTo(prev_grey_pyr[0]);
		else
			resize(prev_grey_pyr[iScale-1], prev_grey_pyr[iScale], prev_grey_pyr[iScale].size(), 0, 0, INTER_LINEAR);
		// dense sampling feature points
		// 在该尺度吓密集采样特征点,保存在points中
		std::vector<Point2f> points(0);
		DenseSample(prev_grey_pyr[iScale], points, quality, min_distance);
		// save the feature points
		// 保存特征点
		std::list<Track>& tracks = xyScaleTracks[iScale];
		for(i = 0; i < points.size(); i++)
			// 这里在第一帧为每个特征点新建一个Track,是为了初始化Track(points[i]为Track中trajectory的起点)
			tracks.push_back(Track(points[i], trackInfo, hogInfo, hofInfo, mbhInfo));
			}
           

根据第一帧图像,使用双线性插值法得到灰度图金字塔,然后在每一个尺度下的灰度图上密集采样特征点,然后以每一个特征点为起点,创建一个Track对象,用于跟踪该特征点的轨迹和轨迹中的描述符信息。

计算所有尺度下灰度图中的多项式开展信息。

human_mask = Mat::ones(frame.size(), CV_8UC1);
	// 如果读取了human bounding box文件的话,用bounding_box来获取每一帧中人类的mask
	if(bb_file)
		InitMaskWithBox(human_mask, bb_list[frame_num].BBs);
           

根据bb_list信息,获取当前帧下的人体遮罩矩阵,用于在检测SURF关键点时避开人体区域。

detector_surf.detect(prev_grey, prev_kpts_surf, human_mask);
extractor_surf.compute(prev_grey, prev_kpts_surf, prev_desc_surf);
           

检测SURF关键点和SURF描述符信息,同时避开人体区域(SURF特征仅仅在原尺度下进行)

2.1.3 循环读取视频帧(其余帧的部分)

if(bb_file)
	InitMaskWithBox(human_mask, bb_list[frame_num].BBs);
detector_surf.detect(grey, kpts_surf, human_mask);
extractor_surf.compute(grey, kpts_surf, desc_surf);
           

获取人体遮罩矩阵,在原尺度下检测SURF特征。

现在是有前一帧的SURF特征信息,根据两连续帧的SURF特征信息,计算两帧间的SURF匹配点。

my::FarnebackPolyExpPyr(grey, poly_pyr, fscales, 7, 1.5);
my::calcOpticalFlowFarneback(prev_poly_pyr, poly_pyr, flow_pyr, 10, 2);
           

在所有尺度上计算该帧的光流信息(这里光流信息用于计算帧间的光流角点匹配点)。

计算两连续帧的光流角点匹配点。

合并两类匹配点。

Mat H = Mat::eye(3, 3, CV_64FC1);	
// 如果当前帧中总匹配点大于50个,计算homography矩阵
if(pts_all.size() > 50) {
	// match_mask为掩码矩阵,findHomography可选输出该矩阵,这个矩阵用来筛选计算的homography
	std::vector<unsigned char> match_mask;
	// 计算多个二维点对之间的最优单映射变换矩阵
	Mat temp = findHomography(prev_pts_all, pts_all, RANSAC, 1, match_mask);
	if(countNonZero(Mat(match_mask)) > 25)
		  H = temp;
} 
           

根据合并的帧间匹配点,计算这些二维点对之间的最优单映射变换矩阵(即单应性homography)。

后面则是算法的核心部分,逐尺度计算轨迹和轨迹间的描述符信息

Mat H_inv = H.inv();
Mat grey_warp = Mat::zeros(grey.size(), CV_8UC1);
MyWarpPerspective(prev_grey, grey, grey_warp, H_inv); 
           

根据单应性矩阵H的逆,计算校正了相机运动了灰度图grey_wrap。

my::FarnebackPolyExpPyr(grey_warp, poly_warp_pyr, fscales, 7, 1.5);
my::calcOpticalFlowFarneback(prev_poly_pyr, poly_warp_pyr, flow_warp_pyr, 10, 2);
           

根据校正了相机运动的灰度图,重新计算多项式展开金字塔和光流金字塔(这次的光流信息就是用来计算轨迹了)

2.1.4 逐尺度计算Track (算法核心部分)

这个循环中逐个计算当前帧不同尺度下的轨迹、描述符

DescMat* hogMat = InitDescMat(height+1, width+1, hogInfo.nBins);
HogComp(prev_grey_pyr[iScale], hogMat->desc, hogInfo);
DescMat* hofMat = InitDescMat(height+1, width+1, hofInfo.nBins);
HofComp(flow_warp_pyr[iScale], hofMat->desc, hofInfo);
DescMat* mbhMatX = InitDescMat(height+1, width+1, mbhInfo.nBins);
DescMat* mbhMatY = InitDescMat(height+1, width+1, mbhInfo.nBins);
MbhComp(flow_warp_pyr[iScale], mbhMatX->desc, mbhMatY->desc, mbhInfo);
           

计算当前尺度下的对于每一种描述符的积分直方图信息,积分直方图用于计算描述符信息,这里具体怎么算的就不了解了,但是需要看一下存储描述符积分直方图的结构体,和结构体的初始化过程,参见2.2.3。

xyScaleTracks中存储不同尺度下当前正在追踪的轨迹,可以理解为一个池子,里面存放的是不同特征点的轨迹及其周围的描述符信息,当池子中的Track达到最大长度,就将其取出来保存或舍弃。然后将采样的新特征点的Track加进池子中,这样就实现了逐帧逐尺度计算IDT特征的功能。

下面进入了内循环,迭代所有的Track池中的Track,计算当前帧Track的新位置和描述符信息。

int index = iTrack->index; 
Point2f prev_point = iTrack->point[index];
           

计算当前Track的序号,即Track的末尾的特征点的序号,然后再获取这个特征点的位置。

Point2f point;
point.x = prev_point.x + flow_pyr[iScale].ptr<float>(y)[2*x];
point.y = prev_point.y + flow_pyr[iScale].ptr<float>(y)[2*x+1];
           

然后根据光流金字塔计算轨迹在当前帧的新位置。

iTrack->disp[index].x = flow_warp_pyr[iScale].ptr<float>(y)[2*x];
iTrack->disp[index].y = flow_warp_pyr[iScale].ptr<float>(y)[2*x+1];
           

根据校正光流金字塔计算轨迹在当前帧的displacement(根据校正光流所计算的轨迹在当前帧的新位置)

RectInfo rect;
GetRect(prev_point, rect, width, height, hogInfo);
           

描述符是沿着轨迹计算的,对于每一帧,在特征点周围的32×32大小的窗口周围计算,rect则记录了当前帧特征点周围的矩形框位置的信息。

GetDesc(hogMat, rect, hogInfo, iTrack->hog, index);
GetDesc(hofMat, rect, hofInfo, iTrack->hof, index);
GetDesc(mbhMatX, rect, mbhInfo, iTrack->mbhX, index);
GetDesc(mbhMatY, rect, mbhInfo, iTrack->mbhY, index);
           

在特征点周围窗口,根据当前尺度灰度图积分直方图信息计算对应的描述符特征信息。

将当前帧特征点添加到轨迹中,且更新index(此前,描述符信息已经添加进iTrack中对应容器了)。

2.1.5 输出满足条件的Track

目前还在核心算法循环中,当计算了当前迭代的iTrack末尾的特征点和描述符后,判断iTrack当前的轨迹是否达到最大长度15,若达到,则继续判定轨迹是否合法

std::vector<Point2f> trajectory(trackInfo.length+1);
		for(int i = 0; i <= trackInfo.length; ++i)
				trajectory[i] = iTrack->point[i]*fscales[iScale];
           

由于不同尺度,分别要计算一遍Track特征,但是最后输出的IDT特征是基于原尺度的,所以,先将当前iTrack的轨迹坐标还原到原尺度。

std::vector<Point2f> displacement(trackInfo.length);
		for (int i = 0; i < trackInfo.length; ++i)
				displacement[i] = iTrack->disp[i]*fscales[iScale];
           

对应将displacement还原到原尺度。

分别存储轨迹的横坐标均值,纵坐标均值,横坐标标准差,纵坐标标准差,轨迹总欧式长度。

判断轨迹是否合法,该部分参见2.2.4

若轨迹合法,则输出一个完整的Track,一个完整Track包含了436个浮点数,分别为:

帧数、横坐标均值、纵坐标均值、横坐标标准差、纵坐标标准差、轨迹总欧式距离、当前尺度、

printf("%f\t", std::min<float>(std::max<float>(mean_x/float(seqInfo.width), 0), 0.999));
printf("%f\t", std::min<float>(std::max<float>(mean_y/float(seqInfo.height), 0), 0.999));
printf("%f\t", std::min<float>(std::max<float>((frame_num - trackInfo.length/2.0 - start_frame)/float(seqInfo.length), 0), 0.999));
           

横坐标均值相对于图像宽度的位置、纵坐标均值相对于图像高度的位置、整条轨迹相对于视频序列的时间位置。

2.1.6 密集采样新的特征点

现在已经计算了当前帧当前尺度下的轨迹信息和描述符信息,需要在当前尺度下密集采样新的特征点

std::vector<Point2f> points(0);
for(std::list<Track>::iterator iTrack = tracks.begin(); iTrack != tracks.end(); iTrack++)
		points.push_back(iTrack->point[iTrack->index]);
           

首先,获取当前帧当前尺度所有轨迹末尾的特征点,在密集采样新的特征点时,需要避开已有特征点的采样窗口。

DenseSample(grey_pyr[iScale], points, quality, min_distance);
for(i = 0; i < points.size(); i++)
		tracks.push_back(Track(points[i], trackInfo, hogInfo, hofInfo, mbhInfo));
           

密集采样新的特征点,并以这些特征点为起点,创建对应的Track。

2.2 一些数据类型和函数

2.2.1 Track类

class Track
{
public:
    std::vector<Point2f> point;
    std::vector<Point2f> disp; //好像是存储根据校正了camera motion之后的光流计算的轨迹位置
    std::vector<float> hog;
    std::vector<float> hof;
    std::vector<float> mbhX;
    std::vector<float> mbhY;
    int index;                  //记录这段idt特征的trajectory中末尾特征点的序号(从0到trackInfo.length - 1)
  
    /*
    @function 创建一段idt特征时的初始化操作
    @param point_ 轨迹的初始点的位置 -> 存储到point容器的首部
    @param trackInfo 所要存储轨迹的基本信息 -> 用于创建容器point、disp时指定容量
    @param hogInfo 这段Track中所需存储的HOG描述符的基本信息 -> 用于创建容器hog时指定容量(dim x trackInfo.length)
    @param hofInfo 类似hogInfo
    @param mbhInfo 类似hogInfo,注意mbh包含了水平和垂直信息,它们分开分别存储于mbhX和mbhY
    */
    Track(const Point2f& point_, const TrackInfo& trackInfo, const DescInfo& hogInfo,
          const DescInfo& hofInfo, const DescInfo& mbhInfo)
        : point(trackInfo.length+1), disp(trackInfo.length), hog(hogInfo.dim*trackInfo.length),
          hof(hofInfo.dim*trackInfo.length), mbhX(mbhInfo.dim*trackInfo.length), mbhY(mbhInfo.dim*trackInfo.length)
    {
        index = 0;
        point[0] = point_;
    }

    /*
    @function 向一段idt特征中的轨迹添加新的特征点
    @ param point_ 需要添加的特征点
    */
    void addPoint(const Point2f& point_)
    {
        index++;            
        point[index] = point_;
    }
};
           

IDT中在某一帧的某一尺度上密集采样了若干特征点后,将会以这个特征点为起点,生成一个Track变量,然后开始跟踪这个特征点的轨迹,并计算这个特征点周围的描述符信息,当Track.index达到了轨迹的最大长度后并且检测合法后,就输出这个Track并将其从当前追踪的Track列表中删除。这样就保证了列表中的Track一直都是未追踪完的。这样才采样新的特征点时,可以直接判断当前帧当前尺度下有哪些特征点处于一段正在追踪的轨迹中,从而可以采样除了这些特征点之外的新的特征点。

2.2.2 InitPry函数

void InitPry(const Mat& frame, std::vector<float>& scales, std::vector<Size>& sizes)
{
	int rows = frame.rows, cols = frame.cols;
	//min_size用宽度和高度的较小值表示图像的size
	float min_size = std::min<int>(rows, cols);

	int nlayers = 0;
	//当min_size不小于patch_size,按scale_stride递减min_size,并记录递减的次数
	while(min_size >= patch_size) {
		min_size /= scale_stride;
		nlayers++;
	}

	// 如果一次也没有递减,即min_size本来就小于patch_size,n_player=1表示有一个尺度
	if(nlayers == 0) nlayers = 1; // at least 1 scale 

	// 根据输入图像分辨率计算一共有多少个尺度,应不大于设定的最大尺度数量,即8
	scale_num = std::min<int>(scale_num, nlayers);

	// 根据scale_num将scales、sizes容器容量重定义
	scales.resize(scale_num);
	sizes.resize(scale_num);

	//分别输入所有尺度大小和该尺度大小下的图像分辨率大小
	scales[0] = 1.;
	sizes[0] = Size(cols, rows);
	for(int i = 1; i < scale_num; i++) {
		scales[i] = scales[i-1] * scale_stride;
		sizes[i] = Size(cvRound(cols/scales[i]), cvRound(rows/scales[i]));
	}
}
           

IDT算法在多个尺度上密集采样特征点并计算轨迹,DenseTrackStab.h中定义了最大尺度数量scale_num为8,但是在跟踪特征点时,需要在特征点周围的窗口(32×32)计算描述符信息,所以当图像分辨率不够大时,就不能降低图像分辨率获得新的尺度。

当确定了尺度的数量时,保存所有的尺度,scale_stride=x,则尺度金字塔为:1、x、x²、x³…

尺度金字塔分辨率为(原图分辨率为r):r、r/x、r/x²、r/x³、…

2.2.3 DescMat结构体和InitDescMat函数

```cpp typedef struct { int height; //高度和宽度一般是尺度金字塔中某尺度下灰度图的高度和宽度 int width; int nBins; float* desc; // 一共有height * width * nBins个float空间 }DescMat; ``` DescMat存储用于计算某个尺度下用于计算某种描述符的积分直方图信息,这里主要用nBins区分描述符,但其实除了hof的nBins为9,其他都为8。积分直方图的维度为当前尺度灰度图的大小,然后每个像素中包含了nBins个float分量。

DescMat* InitDescMat(int height, int width, int nBins)
{
	DescMat* descMat = (DescMat*)malloc(sizeof(DescMat));
	descMat->height = height;
	descMat->width = width;
	descMat->nBins = nBins;

	long size = height*width*nBins; // 申请的float空间的总数,也就是后面要存储size个float
	descMat->desc = (float*)malloc(size*sizeof(float));
	memset(descMat->desc, 0, size*sizeof(float));
	return descMat;
}
           

初始化时,需要动态申请空间,可以看见申请了size个float空间,size=当前尺度灰度图的高度 × 宽度 × nBins。

2.2.4 判断轨迹是否合法

从两个方面判断轨迹是否合法:从轨迹的形态、判断轨迹是否由于相机位移产生,这通过两个函数实现:

bool IsValid(std::vector<Point2f>& track, float& mean_x, float& mean_y, float& var_x, float& var_y, float& length)
{
	int size = track.size();
	float norm = 1./size;
	for(int i = 0; i < size; i++) {
		mean_x += track[i].x;
		mean_y += track[i].y;
	}
	mean_x *= norm;
	mean_y *= norm;
	for(int i = 0; i < size; i++) {
		float temp_x = track[i].x - mean_x;
		float temp_y = track[i].y - mean_y;
		var_x += temp_x*temp_x;
		var_y += temp_y*temp_y;
	}
	var_x *= norm;
	var_y *= norm;
	var_x = sqrt(var_x);
	var_y = sqrt(var_y);
	// remove static trajectory
	// 轨迹x,y坐标标准差均小于最小标准差,认为是静态轨迹,将其去除
	if(var_x < min_var && var_y < min_var)
		return false;
	// remove random trajectory
	// 轨迹x,y坐标标准差任一大于最大标准差,认为是随机轨迹,没有意义
	if( var_x > max_var || var_y > max_var )
		return false;
	// 求出整段轨迹中的相邻帧特征点的最大距离,并顺便算出总距离(后面用于标准化)
	float cur_max = 0;
	for(int i = 0; i < size-1; i++) {
		track[i] = track[i+1] - track[i];
		float temp = sqrt(track[i].x*track[i].x + track[i].y*track[i].y);
		length += temp;
		if(temp > cur_max)
			cur_max = temp;
	}

	// 如果轨迹中存在两相邻帧,它们特征点的距离大于max_dis,且大于了总距离的70%,认为轨迹存在突变大位移,不合适
	if(cur_max > max_dis && cur_max > length*0.7)
		return false;
	track.pop_back();
	norm = 1./length;
	// normalize the trajectory
	// 满足了以上三种条件,轨迹合法,返回true,并用总距离标准化轨迹
	for(int i = 0; i < size-1; i++)
		track[i] *= norm;

	return true;
}
           

该函数从三个方面判断轨迹形态:①横纵坐标标准差不能过低,否则认定为静态轨迹;②横纵坐标标准差不能过高,否则认定为是随机轨迹;③轨迹的连续帧间欧式距离不能过大,否则认为轨迹存在突变大位移。如果满足三个条件,则将轨迹坐标用总欧式距离标准化。

bool IsCameraMotion(std::vector<Point2f>& disp)
{
	float disp_max = 0;
	float disp_sum = 0;
	for(int i = 0; i < disp.size(); ++i) {
		float x = disp[i].x;
		float y = disp[i].y;
		float temp = sqrt(x*x + y*y);

		disp_sum += temp;
		if(disp_max < temp)
			disp_max = temp;
	}

	if(disp_max <= 1)
		return false;

	float disp_norm = 1./disp_sum;
	for (int i = 0; i < disp.size(); ++i)
		disp[i] *= disp_norm;

	return true;
}
           

判断displacement中特征点横纵坐标平方和的最大值,如果最大值小于等于1,则认为轨迹不合法,否则,将displacement归一化。

2.3 计算描述符的过程

Improved Dense Trajectory算法源代码的安装与详细分析

每一种描述符信息是沿着轨迹一帧一帧计算的,在每一帧以特征点为中心的32×32窗口内计算。从图上可以看出,计算描述符信息并没有用到窗口范围积分直方图的全部信息,而是将一个窗口分为nxCell × nyCell大小(2×2),这一部分主要在函数GetDesc体现,参见2.3.1。在时间上统计描述符信息的过程在函数PrintDesc体现,参见2.3.2

2.3.1 GetDesc函数(在空间上统计描述符信息)

void GetDesc(const DescMat* descMat, RectInfo& rect, DescInfo descInfo, std::vector<float>& desc, const int index)
{
	int dim = descInfo.dim;       // 8or 9 x 2 x 2 = 32or36
	int nBins = descInfo.nBins;   // 8or9
	int height = descMat->height; //当前尺度下图像的高度
	int width = descMat->width;   //当前尺度下图像的宽度
 
	int xStride = rect.width/descInfo.nxCells;  // 32 / 2 = 16
	int yStride = rect.height/descInfo.nyCells; // 32 / 2 = 16
	int xStep = xStride*nBins;                  // 16 x 8or9
	int yStep = yStride*width*nBins;            // 16 x width x 8or9

	// iterate over different cells 迭代不同的cells,这个部分在page2画图演示了
	int iDesc = 0;
	std::vector<float> vec(dim); // dim = 32 or 36

	//循环一共进行 nxCells x nyCells = 4次
	for(int xPos = rect.x, x = 0; x < descInfo.nxCells; xPos += xStride, x++)
	for(int yPos = rect.y, y = 0; y < descInfo.nyCells; yPos += yStride, y++) {
		// get the positions in the integral histogram 获取当前cell在积分直方图desc中的位置
		const float* top_left = descMat->desc + (yPos*width + xPos)*nBins; //这是一个指针的位置,指向了积分直方图desc的 (xPos,yPos)位置的相对内存位置
		const float* top_right = top_left + xStep;   //当前cell的右上角在desc中的内存位置
		const float* bottom_left = top_left + yStep;  //当前cell的左下角在desc中的内存位置
		const float* bottom_right = bottom_left + xStep; //当前cell的右下角在desc中的位置

		for(int i = 0; i < nBins; i++) {
			// 以nBins为单位,计算cell的四个顶点在各个bin的积分值的和
			float sum = bottom_right[i] + top_left[i] - bottom_left[i] - top_right[i];
			// max(sum,0) + epsilon 存储在vec中(每次vec存了nBins个数)
			vec[iDesc++] = std::max<float>(sum, 0) + epsilon;
		}
	}
	// 目前在特征点窗口内计算的描述符信息就存储在vec中
	// 对vec进行标准化
	float norm = 0;
	for(int i = 0; i < dim; i++)
		norm += vec[i];
	if(norm > 0) norm = 1./norm;

	// 找到iTrack中存储描述符信息容器的当前序号
	int pos = index*dim;
	for(int i = 0; i < dim; i++)
		// 将标准化过的描述符的值加到iTrack中对应容器中
		desc[pos++] = sqrt(vec[i]*norm);
}
           

首先,根据不同尺度的原图灰度图,为不同的描述符计算了积分直方图信息,积分直方图的维度为(当前尺度灰度图的宽度 × 高度 × nBins)。

然后,在特征点周围的32 × 32窗口内,划分出nxCell × nyCell个网格:

Improved Dense Trajectory算法源代码的安装与详细分析

然后找到四个网格的顶点位置在积分直方图上对应的位置上的值(每个值有nBins的float分量)。

然后计算每个网格的特征值sum=bottom_right + top_left - bottom_left - top_right(一个sum有nBins个float分量)

这样就得到了一个nxCell × nyCell × nBins维度的float向量,最后用其总和标准化,就得到了当前特征点周围的描述符信息。

2.3.2 PrintDesc函数(在时间上统计描述符信息)

当轨迹达到最大长度(15)时,不仅要输出轨迹的相关信息,还要输出轨迹上对应的四种描述符信息(HOF、HOG、MBHx、MBHy)。一个Track上的一种描述符信息的维度为(nxCell × nyCell × nBins × 15),而输出后的一个描述符的维度为(nxCell × nyCell × nBins × ntCells(3))。

void PrintDesc(std::vector<float>& desc, DescInfo& descInfo, TrackInfo& trackInfo)
{
	int tStride = cvFloor(trackInfo.length/descInfo.ntCells); // 输出描述符信息的时间步长 = 15 / 3 =5
	float norm = 1./float(tStride); // 1 / 5
	int dim = descInfo.dim; // 32or36
	int pos = 0;
	for(int i = 0; i < descInfo.ntCells; i++) {  // 3次循环,每次输出一个32or36维向量
		std::vector<float> vec(dim);
		for(int t = 0; t < tStride; t++)   // 5次循环,将5 x 32or36维向量降维到32or36
			for(int j = 0; j < dim; j++)
				vec[j] += desc[pos++];
		for(int j = 0; j < dim; j++)       // 前面只是将5个向量对位想加,现在用5标准化以下,即通过均值方法降维
			printf("%.7f\t", vec[j]*norm);
	}
}
           

时间方向上,将描述符向量分为了三个部分,每个部分包含了5帧的描述符信息,然后求这5帧描述符信息的均值(每一帧描述符上对应位置之和/5)。

3 Fisher Vector on IDT

4 线性SVM

继续阅读