Real-Time Compressive Tracking, Kaihua Zhang, LeiZhang,and Ming-Hsuan Yang
这篇论文由香港理工大学张开华发表在2012年的ECCV之上,论文附有数据对比及代码。
论文主页及源码下载:http://www4.comp.polyu.edu.hk/~cslzhang/CT/CT.htm
跟踪效果:http://v.youku.com/v_show/id_XNDMzODcxNjcy.html
根据作者的描述,这是一种简单高效地基于压缩感知的跟踪算法。首先利用符合压缩感知RIP条件的随机测量矩阵对多尺度图像特征进行降维,然后在降维后的特征上采用简单的朴素贝叶斯分类器进行分类。该跟踪算法非常简单,但是实验结果鲁棒性强,速度大概能到达40帧/秒。
该压缩跟踪算法的流程如下:
输入:第t帧图像
Step 1 :在t-1帧跟踪到的目标位置It-1的周围(也就是满足Dγ={z|||l(z)−lt−1||<γ,与It-1距离小于γ)采样n个图像片,然后对这些图像片进行类Harr特征提取(降维),得到每个图像片的特征向量v。
Step 2:使用公式(4)中分类器H(v)对这些v进行分类,找到最大分类分数的图像片作为当前帧跟踪到的目标,位置为It;
Step 3:采样两个样本集:Dα= {z|||l(z) − lt||< α}和 Dζ ,β= {z|ζ < ||l(z)−lt||<β}其中,α< ζ < β;
Step 4:提取上述两个样本集的特征,通过公式(6)来更新分类器参数。
输出:跟踪到的目标位置It和更新后的分类器参数。
有关这篇文章的详细讲解可以参见博文:
http://blog.csdn.net/zouxy09/article/details/8118360
这里主要讲解该算法的C++代码实现流程。它包含头文件CompressiveTracker.h和源文件CompressiveTracker.cpp。
头文件包含CompressiveTracker类定义。这些变量包括:
/************************************************************************
* File: CompressiveTracker.h
* Brief: C++ demo for paper: Kaihua Zhang, Lei Zhang, Ming-Hsuan Yang,"Real-Time Compressive Tracking," ECCV 2012.
* Version: 1.0
* Author: Yang Xian
* Email: yang_xian521@163.com
* Date: 2012/08/03
* History:
* Revised by Kaihua Zhang on 14/8/2012
* Email: zhkhua@gmail.com
* Homepage: http://www4.comp.polyu.edu.hk/~cskhzhang/
************************************************************************/
#pragma once
#include <opencv2/opencv.hpp>
#include <vector>
using std::vector;
using namespace cv;
//---------------------------------------------------
class CompressiveTracker
{
public:
CompressiveTracker(void); //构造函数
~CompressiveTracker(void); //析构函数
private:
int featureMinNumRect; //
int featureMaxNumRect; //
int featureNum; //每个box的类harr特征个数(即弱分类器个数),文中取为50. 每一个样本有50个类harr特征,每一个类harr特征是由2到3个随机选择的矩形框来构成的,对这些矩形框的灰度加权求和作为这一个类harr特征的特征值。
vector<vector<Rect>> features; // box的特征(向量,大小为featureNum,每个元素代表矩形框)
vector<vector<float>> featuresWeight; //随机测量矩阵R的非零元素(矩阵大小为featureNum x s,s即为文中公式(2)中的s)
int rOuterPositive; //在目标位置附近采集正样本的范围
vector<Rect> samplePositiveBox; // 采集的正样本集(向量,每个元素代表矩形框)
vector<Rect> sampleNegativeBox; //采集的负样本集(向量,每个元素代表矩形框)
int rSearchWindow; //扫描窗口的大小
Mat imageIntegral; //图像的积分图(矩阵)
Mat samplePositiveFeatureValue; // 采集的正样本的类harr特征的值(矩阵)
Mat sampleNegativeFeatureValue; //采集的负样本的类harr特征的值(矩阵)
vector<float> muPositive; //向量(大小为featureNum),描述正样本的高斯分布均值
vector<float> sigmaPositive; // 向量(大小为featureNum),描述正样本的高斯分布方差
vector<float> muNegative; //向量(大小为featureNum),描述负样本的高斯分布均值
vector<float> sigmaNegative; //向量(大小为featureNum),描述负样本的高斯分布方差
float learnRate; //学习速率,控制分类器参数更新的步长
vector<Rect> detectBox; // 向量(每个元素代表矩形框),待检测的box
Mat detectFeatureValue; //
RNG rng; // 随机数
private:
void HaarFeature(Rect& _objectBox, int _numFeature);
void sampleRect(Mat& _image, Rect& _objectBox, float _rInner, float _rOuter, int _maxSampleNum, vector<Rect>& _sampleBox);
void sampleRect(Mat& _image, Rect& _objectBox, float _srw, vector<Rect>& _sampleBox);
void getFeatureValue(Mat& _imageIntegral, vector<Rect>& _sampleBox, Mat& _sampleFeatureValue);
void classifierUpdate(Mat& _sampleFeatureValue, vector<float>& _mu, vector<float>& _sigma, float _learnRate);
void radioClassifier(vector<float>& _muPos, vector<float>& _sigmaPos, vector<float>& _muNeg, vector<float>& _sigmaNeg,
Mat& _sampleFeatureValue, float& _radioMax, int& _radioMaxIndex);
public:
void processFrame(Mat& _frame, Rect& _objectBox);
void init(Mat& _frame, Rect& _objectBox);
};
源文件CompressiveTracker.cpp描述函数体。这些函数包括:
HaarFeature——计算类Harr特征
sampleRect——在上一帧跟踪的目标box的周围采集若干正样本和负样本,来初始化或者更新分类器(step 4)
sampleRect重载——在上一帧跟踪的目标box的周围采集若干box来确定下一帧的目标box(step 1)
getFeatureValue——通过积分图来计算采集到的每一个样本的类harr特征
classifierUpdate——计算新采集到的正负样本的特征的期望和标准差,并用其来更新分类器(公式(6))
radioClassifier——计算分类分数(公式(4))
processFrame——传入上一帧跟踪到的box,来处理新的一帧
init——传入第一帧和要跟踪的目标box(由文件读入或者用户鼠标框选),来初始化分类器
其中前6个是私有函数后2个是公有函数。
先来看初始化函数init
void CompressiveTracker::init(Mat& _frame, Rect& _objectBox)
/* Description: initialize randommeasurement matrix, sample feature value, classifier
Arguments:
-_frame: processing frame
-_objectBox: recent object position
*/
{
// compute feature template
//计算初始目标box的类harr特征(其中包含随机测量矩阵R的非零元素值,该矩阵只在此计算一次,并且存储在featuresWeight中,用于后续帧的计算)
HaarFeature(_objectBox, featureNum);
// compute sample templates
//在初始目标box周围,采集正样本和负样本来初始化我们的分类器
sampleRect(_frame, _objectBox, rOuterPositive, 0, 1000000, samplePositiveBox);
sampleRect(_frame, _objectBox, rSearchWindow*1.5, rOuterPositive+4.0, 100, sampleNegativeBox);
//用integral函数(opencv库函数)计算积分图,用于快速计算类harr特征
integral(_frame, imageIntegral, CV_32F);
//通过上面的积分图,计算我们采样到的正负样本的类harr特征
getFeatureValue(imageIntegral, samplePositiveBox, samplePositiveFeatureValue);
getFeatureValue(imageIntegral, sampleNegativeBox, sampleNegativeFeatureValue);
//通过上面的正负样本的特征来初始化分类器
classifierUpdate(samplePositiveFeatureValue, muPositive, sigmaPositive, learnRate);
classifierUpdate(sampleNegativeFeatureValue, muNegative, sigmaNegative, learnRate);
}
首先来看函数HarrFeature,它是通过积分图来计算采集到的样本的类harr特征。
void CompressiveTracker::HaarFeature(Rect& _objectBox, int _numFeature)
/*Description: compute Haar features
Arguments:
-_objectBox: [x y width height] object rectangle
-_numFeature: total number of features. The default is 50.
*/
{
// _numFeature是一个样本box的类harr特征的个数,这里取为50个
//每一个harr特征由2到3个随机选择的矩形框(vector<Rect>())构成
features = vector<vector<Rect>>(_numFeature, vector<Rect>());
// featuresWeight就是随机测量矩阵中的非零元素值,行指标表示特征,列指标表示矩形框(s)。
featuresWeight = vector<vector<float>>(_numFeature, vector<float>());
// numRect是每个特征对应的矩形框的个数,也即文中公式(2)中的s
int numRect;
Rect rectTemp;
float weightTemp;
for (int i=0; i<_numFeature; i++)
{
// featureMinNumRect = 2, featureMinNumRect = 4
//生成[2,4]内的随机数,并向下取整
numRect = cvFloor(rng.uniform((double)featureMinNumRect, (double)featureMaxNumRect));
//int c = 1;
for (int j=0; j<numRect; j++)
{
// rectemp用于保存得到的矩形框(位置、长宽)
rectTemp.x = cvFloor(rng.uniform(0.0, (double)(_objectBox.width - 3)));
rectTemp.y = cvFloor(rng.uniform(0.0, (double)(_objectBox.height - 3)));
rectTemp.width = cvCeil(rng.uniform(0.0, (double)(_objectBox.width - rectTemp.x - 2)));
rectTemp.height = cvCeil(rng.uniform(0.0, (double)(_objectBox.height - rectTemp.y - 2)));
//这里的矩形框是相对于目标box的相对位置
features[i].push_back(rectTemp);
//weightTemp = (float)pow(-1.0, c);
//pow(-1.0, c)也就是-1的c次方,而c随机地取0或者1,也就是说weightTemp是随机的正或者负。
//随机测量矩阵中,矩阵元素有三种,sqrt(s)、-sqrt(s)和零。为正和为负的概率是相等的,
//这就是为什么是[2,4)均匀采样的原因,就是取0或者1概率一样。
//但是这里为什么是sqrt(s)分之一呢?还有什么时候是0呢?论文中是0的概率不是挺大的吗?
//没有0元素,哪来的稀疏表达和压缩呢?不懂,恳请指导!(当然稀疏表达的另一个好处
// weightTemp
weightTemp = (float)pow(-1.0, cvFloor(rng.uniform(0.0, 2.0))) / sqrt(float(numRect));
//保存每一个矩形框对应的权重
featuresWeight[i].push_back(weightTemp);
}
}
}
然后是采集样本用于分类器参数的更新和目标位置的预测,这分别由函数sampleRect及其重载函数来实现
void CompressiveTracker::sampleRect(Mat& _image, Rect& _objectBox, float _rInner, float _rOuter, int _maxSampleNum, vector<Rect>& _sampleBox)
/* Description: compute the coordinate of positive and negative sample image templates
Arguments:
-_image: processing frame
-_objectBox: recent object position
-_rInner: inner sampling radius
-_rOuter: Outer sampling radius
-_maxSampleNum: maximal number of sampled images
-_sampleBox: Storing the rectangle coordinates of the sampled images.
*/
{
int rowsz = _image.rows - _objectBox.height - 1;
int colsz = _image.cols - _objectBox.width - 1;
//在上一帧跟踪的目标box的周围采集正样本和负样本
// _rInner对应文中Algorithm 1中的alpha,_rOuter对应文中的zeta。
//在小于_rInner距离的区域内采集正样本
//在大于_rOuter距离的区域内采集负样本
float inradsq = _rInner*_rInner;
float outradsq = _rOuter*_rOuter;
int dist;
//限制采集的矩形框的坐标,一是保证其在由_rInner和_rOuter确定的范围内,二是防止其超出整帧图像的范围
int minrow = max(0,(int)_objectBox.y-(int)_rInner);
int maxrow = min((int)rowsz-1,(int)_objectBox.y+(int)_rInner);
int mincol = max(0,(int)_objectBox.x-(int)_rInner);
int maxcol = min((int)colsz-1,(int)_objectBox.x+(int)_rInner);
int i = 0;
// 分子_maxSampleNum是我们需要采集的矩形框的最大个数
//分母是可以采集的矩形框的最大个数
//prob用于对所有采集的矩形框进行取舍判断
float prob = ((float)(_maxSampleNum))/(maxrow-minrow+1)/(maxcol-mincol+1);
int r;
int c;
_sampleBox.clear();// important
Rect rec(0,0,0,0);
for( r=minrow; r<=(int)maxrow; r++ )
for( c=mincol; c<=(int)maxcol; c++ ){
//计算生成的矩形框到目标矩形框的距离
dist = (_objectBox.y-r)*(_objectBox.y-r) + (_objectBox.x-c)*(_objectBox.x-c);
// 第一个条件是保证矩形框个数不超过由_maxSampleNum决定的上限
//如果超过,则随机选择其中的若干个
//后两个条件是保证距离需要在_rInner和_rOuter的范围内
if( rng.uniform(0.,1.) < prob && dist < inradsq && dist >= outradsq ){
rec.x = c;
rec.y = r;
rec.width = _objectBox.width; //目标矩形框的大小始终没变化
rec.height= _objectBox.height;
_sampleBox.push_back(rec);
i++;
}
}
_sampleBox.resize(i);
}
它的重载函数的函数体基本相同,只是参数略有不同,因为该重载函数是用来预测目标的位置,即算法步骤中的step 1,所以只需用到参数rSearchWindow,即这里的_srw。
void CompressiveTracker::sampleRect(Mat& _image, Rect& _objectBox, float _srw, vector<Rect>& _sampleBox)
再来看看函数getFeatureValue是怎么得到每个特征对应的值的
void CompressiveTracker::getFeatureValue(Mat& _imageIntegral, vector<Rect>& _sampleBox, Mat& _sampleFeatureValue)
{
//sampleBox存储采集的样本矩形框的位置
int sampleBoxSize = _sampleBox.size();
// sampleFeatureValue存储样本矩形框对应的特征的值,行指标为特征序号,列指标为样本序号
_sampleFeatureValue.create(featureNum, sampleBoxSize, CV_32F);
float tempValue;
int xMin;
int xMax;
int yMin;
int yMax;
for (int i=0; i<featureNum; i++)
{
for (int j=0; j<sampleBoxSize; j++)
{
tempValue = 0.0f;
for (size_t k=0; k<features[i].size(); k++)
{
// features中的矩形框是相对于目标box的相对位置的,
//所以需要加上box的坐标才是其在整幅图像中的坐标
xMin = _sampleBox[j].x + features[i][k].x;
xMax = _sampleBox[j].x + features[i][k].x + features[i][k].width;
yMin = _sampleBox[j].y + features[i][k].y;
yMax = _sampleBox[j].y + features[i][k].y + features[i][k].height;
//通过积分图来快速计算一个矩形框的所有像素灰度之和
// tempValue就是经过随机测量矩阵加权后的灰度和
//每一个类harr特征是由2到3个矩形框来构成的,对这些矩形框的灰度加权求和
//作为这一个类harr特征的值
tempValue += featuresWeight[i][k] *
(_imageIntegral.at<float>(yMin, xMin) +
_imageIntegral.at<float>(yMax, xMax) -
_imageIntegral.at<float>(yMin, xMax) -
_imageIntegral.at<float>(yMax, xMin));
}
_sampleFeatureValue.at<float>(i,j) = tempValue;
}
}
}
接下来是分类器的更新,由函数classifierUpdate实现
void CompressiveTracker::classifierUpdate(Mat& _sampleFeatureValue, vector<float>& _mu, vector<float>& _sigma, float _learnRate)
{
Scalar muTemp;
Scalar sigmaTemp;
for (int i=0; i<featureNum; i++)
{
//对每个类harr特征,计算其期望和标准差
meanStdDev(_sampleFeatureValue.row(i), muTemp, sigmaTemp);
//文中的公式(6)
_sigma[i] = (float)sqrt( _learnRate*_sigma[i]*_sigma[i] + (1.0f-_learnRate)*sigmaTemp.val[0]*sigmaTemp.val[0]
+ _learnRate*(1.0f-_learnRate)*(_mu[i]-muTemp.val[0])*(_mu[i]-muTemp.val[0])); // equation 6 in paper
_mu[i] = _mu[i]*_learnRate + (1.0f-_learnRate)*muTemp.val[0]; // equation 6 in paper
}
}
计算文中的H(v)即分类器分数,对各个矩形框的分类器分数进行计算,选择分数最大的矩形框作为目标矩形框,由函数radioClassifier实现(作者ratio把写成了radio,呵呵)
void CompressiveTracker::radioClassifier(vector<float>& _muPos, vector<float>& _sigmaPos, vector<float>& _muNeg, vector<float>& _sigmaNeg,
Mat& _sampleFeatureValue, float& _radioMax, int& _radioMaxIndex)
{
float sumRadio;
// FLT_MAX是最大的浮点数的宏定义
_radioMax = -FLT_MAX;
_radioMaxIndex = 0;
float pPos;
float pNeg;
int sampleBoxNum = _sampleFeatureValue.cols; //采样得到的矩形框的个数
for (int j=0; j<sampleBoxNum; j++)
{
sumRadio = 0.0f;
for (int i=0; i<featureNum; i++)
{
pPos = exp( (_sampleFeatureValue.at<float>(i,j)-_muPos[i])*(_sampleFeatureValue.at<float>(i,j)-_muPos[i]) / -(2.0f*_sigmaPos[i]*_sigmaPos[i]+1e-30) ) / (_sigmaPos[i]+1e-30);
pNeg = exp( (_sampleFeatureValue.at<float>(i,j)-_muNeg[i])*(_sampleFeatureValue.at<float>(i,j)-_muNeg[i]) / -(2.0f*_sigmaNeg[i]*_sigmaNeg[i]+1e-30) ) / (_sigmaNeg[i]+1e-30);
sumRadio += log(pPos+1e-30) - log(pNeg+1e-30); // equation 4
}
if (_radioMax < sumRadio)
{
_radioMax = sumRadio;
_radioMaxIndex = j;
}
}
}
最后是processFrame函数,它用于连续处理图像帧
void CompressiveTracker::processFrame(Mat& _frame, Rect& _objectBox)
{
// predict
//在上一帧跟踪到的box周围,采集需要检测的box框
sampleRect(_frame, _objectBox, rSearchWindow, detectBox);
//计算这一帧的积分图
integral(_frame, imageIntegral, CV_32F);
//用积分图来计算上面采集到的每个box的类harr特征
getFeatureValue(imageIntegral, detectBox, detectFeatureValue);
int radioMaxIndex;
float radioMax;
//对上面的每个box进行匹配分类
radioClassifier(muPositive, sigmaPositive, muNegative, sigmaNegative, detectFeatureValue, radioMax, radioMaxIndex);
//分数最高的那个box即为下一帧的目标box
_objectBox = detectBox[radioMaxIndex];
// update
//在新跟踪到的这个目标box的周围,采集正样本和负样本来更新分类器
sampleRect(_frame, _objectBox, rOuterPositive, 0.0, 1000000, samplePositiveBox);
sampleRect(_frame, _objectBox, rSearchWindow*1.5, rOuterPositive+4.0, 100, sampleNegativeBox);
//通过上面的积分图,计算我们采样到的正负样本box的类harr特征
getFeatureValue(imageIntegral, samplePositiveBox, samplePositiveFeatureValue);
getFeatureValue(imageIntegral, sampleNegativeBox, sampleNegativeFeatureValue);
//通过上面的正负样本的特征来更新我们的分类器
classifierUpdate(samplePositiveFeatureValue, muPositive, sigmaPositive, learnRate);
classifierUpdate(sampleNegativeFeatureValue, muNegative, sigmaNegative, learnRate);
}