TLD 目标跟踪源码理解

2023-12-23 03:40
文章标签 源码 目标 理解 跟踪 tld

本文主要是介绍TLD 目标跟踪源码理解,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

最近在做目标跟踪的相关程序的加速,整合借这个机会整理下TLD的源码理解,由于本人C++功底有限,难免会出现表述和理解错误,请大家批评指正
本帖子 长期更新 未完部分耐心等待

TLD源码解读

主函数部分

主要涉及两个函数:

  • RTLTrackerInit
tracker.RTLTrackerInit(last_gray, box);

其中 last gray 是frame第一帧经过灰度变换的图像,代码如下:

capture >> frame;
cvtColor(frame, last_gray, CV_RGB2GRAY);

Box 是我们标定的目标位置,TLD是单目标跟踪所以只能实现标记一个框

Rect box(248,198,27,14);
  • RTLTrackerTracking
tracker.RTLTrackerTracking(last_gray, current_gray, pbox, status, true);

其中 pbox 是 box 调用 BoundingBox 的构造函数得到的

class BoundingBox : public cv::Rect
{
public:BoundingBox( ){}BoundingBox( cv::Rect r ) : cv::Rect( r ){}/* 和指定的box的重叠度 */float overlap;/* 使用的缩放组合的id( 0~21 ) */int scaleId;
};

status函数表示是否跟踪成功,如果跟踪成功则画框

 if (status) {rectangle(frame, pbox, Scalar(0, 255, 0), 2, 8, 0);detections++;}

TrackerInit函数解读

TrackerInit类

void RtLtTracker::RTLTrackerInit( const Mat &frame, const Rect userBox )
{/* 获得所有的扫描窗口 */boxOperator.getAllScanWindowOfFrame( frame, userBox );/* 将这些窗口分类为好坏窗口,并得到好窗口的边界 */boxOperator.classifyScanningWindows( userBox, numClosestInit );/* 分配各种空间 *//*struct TempStruct {std::vector<std::vector<int> > patt;std::vector<float> conf;};*/tmp.conf = vector<float>( boxOperator.allScanningWindows.size( ) );tmp.patt = vector<vector<int> >( boxOperator.allScanningWindows.size( ), vector<int>( 10, 0 ) );classifier.positiveNNSample.create( classifier.patchSize, classifier.patchSize, CV_64F );/***********************************************************\* 源代码有保留上一帧信息的操作,应该是用来做BF跟踪器跟丢自检,这里先不加  *\***********************************************************//****????这里有个疑问是为什么不用初始rect作为bestWindows,而还是要用和Rect重合度最好的windows作为             bestWindows   因为这本来就是初始化阶段啊****/lastbox=boxOperator.bestWindow;lastconf=1;lastvalid=true;classifier.prepare( boxOperator.allScaledSizes );/* 生成数据 */patchGenerator = PatchGenerator(0, 0, noiseInit, true, 1 - scaleInit, 1 + scaleInit,-angleInit*CV_PI / 180, angleInit*CV_PI / 180,-angleInit*CV_PI / 180, angleInit*CV_PI / 180 );boxOperator.iisum.create( frame.rows + 1, frame.cols + 1, CV_32F );boxOperator.iisqsum.create( frame.rows + 1, frame.cols + 1, CV_64F );integral( frame, boxOperator.iisum, boxOperator.iisqsum );Scalar stdev, mean;meanStdDev( frame( boxOperator.bestWindow ), mean, stdev );classifier.varClassifierTh = powf( stdev.val[0], 2 ) * 0.5f;classifier.generatePositiveData(frame, classifier.numWrapsInit, boxOperator, patchGenerator );classifier.generateNegativeData(frame, boxOperator);/* 处理数据并训练分类器 */classifier.makeTrainAndTestDataThenTrain( );
}

对上述为啥不用userBox 而是 用 BestWindow 的疑问的解答:

这里写图片描述

初始化阶段的userBox和bestWindow本质是差不多的

下面我们逐句分析Tracker的代码:

  • getAllScanWindowOfFrame

    输入1: frame 代表第一帧
    输入2:userBox代表初始的矩阵框 Rect


void BoxOperator::getAllScanWindowOfFrame( const Mat &frame, const Rect &userBox )
{// 所有的缩放尺度,由尺度缩放系数构造,为[1.2^-10, 1.2^10],一共21种缩放尺度const float SCALES[] ={0.16151f, 0.19381f, 0.23257f, 0.27908f, 0.33490f,0.40188f, 0.48225f, 0.57870f, 0.69444f, 0.83333f,1.00000f, 1.20000f, 1.44000f, 1.72800f, 2.07360f,2.48832f, 2.98598f, 3.58318f, 4.29982f, 5.15978f,6.19174f };Size scaledSize;BoundingBox bbox;int cnt = 0;// 对于每一种缩放尺度for ( int s = 0; s < 21; s++ ){/*********************代表缩放后的宽度************************/int scaledWidth  = (int)roundf( userBox.width * SCALES[s] );/*********************代表缩放后的高度************************/int scaledHeight = (int)roundf( userBox.height * SCALES[s] );/****************变换权值:宽度和高度较小值*********************/int shiftWeight  = min( scaledWidth, scaledHeight );/*    ***********筛选出不合适的padding size 跳过****************1) 不满足最小窗尺寸:minWindowSize = 152) 缩放后的宽度比原图像帧的宽度还大3) 缩放后的高度比原图像帧的长度还大**********************************************************/if ( shiftWeight < minWindowSize ||scaledWidth > frame.cols ||scaledHeight > frame.rows ){continue;}/************* 保留合适的scaledSize **********/scaledSize.width  = scaledWidth;scaledSize.height = scaledHeight;/***********保存所有的ScaledSize*************/allScaledSizes.push_back( scaledSize );/***************设置扫描步长******************/int step = (int)roundf( shiftWeight * scanningShift );for ( int y=1; y < frame.rows - scaledHeight; y += step ){for ( int x=1; x < frame.cols - scaledWidth; x += step ){bbox.x = x;bbox.y = y;bbox.width   = scaledWidth;bbox.height  = scaledHeight;// 保存与初始输入窗口box的重叠度bbox.overlap = bbOverlap( bbox, BoundingBox( userBox ) );// 记录扫描窗口的idbbox.scaleId = cnt;// 把所有的窗口集中到一个容器内allScanningWindows.push_back( bbox );}}cnt++;}
}
  • classifyScanningWindows

    输入1:

/* 在初始化分类器时需要保留的好窗口数量 */int numClosestInit          = 10;
 输入2:userBox 代表初始的矩阵框 Rect 
void BoxOperator::classifyScanningWindows( const Rect &userBox, int closestNum )
{float maxOverlap = 0;for ( int i = 0; i < allScanningWindows.size( ); i++ ){if ( allScanningWindows[i].overlap > maxOverlap ){bestWindow = allScanningWindows[i];maxOverlap = bestWindow.overlap;}/**********goodOverlapth代表好窗口重叠度  = 0.6f ************/if ( allScanningWindows[i].overlap > goodOverlapTh ){goodBoxIndexes.push_back( i );}/**********badOverlapth代表坏窗口重叠度  = 0.2f ************/else if ( allScanningWindows[i].overlap < badOverlapTh ){badBoxIndexes.push_back( i );}}/*  如果筛选出来的好窗口数量大于给定数量,那么选取重合度大的留下来 在不要求排序的前提下选取给定条件下前N大的数 *///  !!!!这段代码可以用作欣赏!!!!if ( goodBoxIndexes.size( ) > closestNum ){nth_element(goodBoxIndexes.begin( ),goodBoxIndexes.begin( ) + closestNum,goodBoxIndexes.end( ),/*struct OComparator{OComparator( const std::vector<BoundingBox>& _grid ):grid( _grid ){}std::vector<BoundingBox> grid;bool operator()( int idx1, int idx2 ){return grid[idx1].overlap > grid[idx2].overlap;}};*/OComparator( allScanningWindows ) );goodBoxIndexes.resize( closestNum );}getGoodBoxHull( );
}

代码段的最后其中涉及到getGoodBoxHull()

void BoxOperator::getGoodBoxHull( )
{int x1=INT_MAX, x2=0;int y1=INT_MAX, y2=0;int idx;/***这段代码的目的是:保证 x1 和y1 在 int 可以表示的范围内保证 x2 和 y2 是大于0的数***/for ( int i=0; i < goodBoxIndexes.size( ); i++ ){idx= goodBoxIndexes[i];x1 = min( allScanningWindows[idx].x, x1 );y1 = min( allScanningWindows[idx].y, y1 );x2 = max( allScanningWindows[idx].x + allScanningWindows[idx].width, x2 );y2 = max( allScanningWindows[idx].y + allScanningWindows[idx].height, y2 );}goodBoxHull.x      = x1;goodBoxHull.y      = y1;goodBoxHull.width  = x2 - x1;goodBoxHull.height = y2 - y1;
}
  • tmp结构分配空间
tmp.conf = vector<float>( boxOperator.allScanningWindows.size( ) );
tmp.patt = vector<vector<int> >( boxOperator.allScanningWindows.size( ), vector<int>( 10, 0 ) );

其中tmp的TempStruct为

struct TempStruct {std::vector<std::vector<int> > patt;std::vector<float> conf;
};

正样本的pathsize为 15*15

classifier.positiveNNSample.create( classifier.patchSize, classifier.patchSize, CV_64F );
  • classifier.prepare(boxOperator.allScaledSizes)
    关于随机厥的详细用法可以参考:http://www.cnblogs.com/nsnow/p/4670640.html
    输入:
    boxOperator.allScaledSizes是在getAllScanWindowOfFrame函数中体现的
/***********保存所有的ScaledSize*************/allScaledSizes.push_back( scaledSize );

这行语句代表所有符合要求的21种尺度下的方框

void FernNNClassifier::prepare( const vector<Size> &scales )
{acum = 0;/* 初始化测试特征的位置 */// 随机森林中树的总数为 10// 每棵树的特征总数为 13 ,每棵树的判断节点个数,树上每一个特征作为一个决策点 int totalFeatures = totFerns * featureNumPerFern;fernsFeatures = vector<vector<Feature> >( scales.size( ), vector<Feature>( totalFeatures ) );RNG& rng = theRNG( );float x1f, x2f, y1f, y2f;int x1, x2, y1, y2;for ( int i=0; i < totalFeatures; i++ ){x1f = (float)rng;y1f = (float)rng;x2f = (float)rng;y2f = (float)rng;/* 利用随机数,随机定位出两个像素点,并作为特征 *///  其实就是2bit BP特征 随机找两个点比较亮度//  这篇博客里有写 http://www.cnblogs.com/nsnow/p/4670640.htmlfor ( int s=0; s < scales.size( ); s++ ){x1 = x1f * scales[s].width;y1 = y1f * scales[s].height;x2 = x2f * scales[s].width;y2 = y2f * scales[s].height;fernsFeatures[s][i] = Feature( x1, y1, x2, y2 );}}fernNegativeTh = 0.5f * totFerns;/* 初始化后验概率 */for ( int i = 0; i < totFerns; i++ ){posteriors.push_back( vector<float>( powf( 2.0f, featureNumPerFern ), 0 ) );positiveCounter.push_back( vector<int>( pow( 2.0, featureNumPerFern ), 0 ) );negativeCounter.push_back( vector<int>( pow( 2.0, featureNumPerFern ), 0 ) );}
}

在这里继续查看Feature类的定义

struct Feature{uchar x1, y1, x2, y2;Feature( ) : x1( 0 ), y1( 0 ), x2( 0 ), y2( 0 ) {}Feature( int _x1, int _y1, int _x2, int _y2 ): x1( (uchar)_x1 ), y1( (uchar)_y1 ), x2( (uchar)_x2 ), y2( (uchar)_y2 ){}bool operator ()( const cv::Mat& patch ) const{return patch.at<uchar>( y1, x1 ) > patch.at<uchar>( y2, x2 );}};

关于RNG的用法可以参考博客 :http://blog.csdn.net/yang_xian521/article/details/6931385
重点是:就是要写成 rng.uniform(0.f, 1.f); 而不能写成rng.uniform( 0 , 1),因为输入为int型参数,会调用uniform(int,int),只能产生0。请大家注意使用

疑点是:为什么初始化成2的13次幂:

/************************* 初始化后验概率********************************/
//一共totFerns bit的特征 所以可能的情形有 2^totFerns 个 即0 到 2^totFerns - 1个
//http://www.cnblogs.com/nsnow/p/4670640.html中的作图 坐标范围有误 但是原理正确for ( int i = 0; i < totFerns; i++ ){posteriors.push_back( vector<float>( powf( 2.0f, featureNumPerFern ), 0 ) );positiveCounter.push_back( vector<int>( pow( 2.0, featureNumPerFern ), 0 ) );negativeCounter.push_back( vector<int>( pow( 2.0, featureNumPerFern ), 0 ) );}
  • patchGenerator
    现在我也不懂为什么参数要这么设置,应该和接下来的仿射变换生成样本有关,先放在这,以后补上
    patchGenerator = PatchGenerator(0, 0, noiseInit, true, 1 - scaleInit, 1 + scaleInit,-angleInit*CV_PI / 180, angleInit*CV_PI / 180,-angleInit*CV_PI / 180, angleInit*CV_PI / 180 );
  • integral
    这个部分是用于计算图像帧的积分图
    具体细节参考博客:http://blog.csdn.net/iracer/article/details/49029239
    其中参数的含义 和 积分图的意义 如下图所示
    这里写图片描述
boxOperator.iisum.create( frame.rows + 1, frame.cols + 1, CV_32F );
boxOperator.iisqsum.create( frame.rows + 1, frame.cols + 1, CV_64F );
integral( frame, boxOperator.iisum, boxOperator.iisqsum );
  • meanStdDev
meanStdDev( frame( boxOperator.bestWindow ), mean, stdev );

这里写图片描述

  • varClassifierTh
    方差分类器的方差阈值
classifier.varClassifierTh = powf( stdev.val[0], 2 ) * 0.5f;
  • generatePositiveData
void FernNNClassifier::generatePositiveData( const Mat& frame, int numWarps, const BoxOperator &bop, PatchGenerator patchGenerator )
{Scalar mean, stdev;/** 将frame图像bestBox区域的图像片归一化为均值为0的15*15大小的patch,* 存于positiveNNSample(用于最近邻分类器的正样本)中(最近邻的box的Pattern),* 该正样本只有一个。*/NormalOperation::getPattern( frame( bop.bestWindow ), positiveNNSample, patchSize, mean, stdev );/************************\* 做仿射变换,并提取Fern特征 *\************************/Mat img, wraped;/* 利用高斯滤波平滑图像 */GaussianBlur( frame, img, Size( 9, 9 ), 1.5 );wraped = img( bop.goodBoxHull );RNG& rng = theRNG( );/* 获取好窗口边界框的中心点 */Point2f hullCenter(bop.goodBoxHull.x + ( bop.goodBoxHull.width - 1 ) * 0.5f,bop.goodBoxHull.y + ( bop.goodBoxHull.height - 1 )* 0.5f );vector<int> fern( totFerns );positiveFernSamples.clear( );Mat patch;int idx;for ( int i=0; i < numWarps; i++ ){if ( i > 0 ){/* 对图像进行仿射变换 */patchGenerator( frame, hullCenter, wraped, bop.goodBoxHull.size( ), rng );}for ( int b = 0; b < bop.goodBoxIndexes.size( ); b++ ){idx   = bop.goodBoxIndexes[b];patch = img( bop.allScanningWindows[idx] );/* 获得输入patch的13位二进制码特征 */getFernsFeatures(patch, bop.allScanningWindows[idx].scaleId,fern );/* 标记为正样本 */positiveFernSamples.push_back( make_pair( fern, 1 ) );}}}

仿射变换介绍:具体详细解释见 http://blog.csdn.net/carson2005/article/details/7540936
这里写图片描述

获取13bit特征详见博客:http://www.cnblogs.com/nsnow/p/4670640.html

  • generateNegativeData
void FernNNClassifier::generateNegativeData( const Mat &frame, BoxOperator &bop )
{random_shuffle( bop.badBoxIndexes.begin( ), bop.badBoxIndexes.end( ) );/************************************************************\* 利用积分图,计算每个badBox对应patch的方差,然后选取那些方差大的patch *\************************************************************/int idx;vector<int> fern( totFerns );Mat patch;/* 将方差大于varClassifierTh/2的提取特征,标记为负样本0,* 放入集合分类器负样本集合negativeFernSamples中*/for ( int j = 0; j < bop.badBoxIndexes.size( ); j++ ){idx = bop.badBoxIndexes[j];if ( bop.getVar( bop.allScanningWindows[idx], bop.iisum, bop.iisqsum ) < varClassifierTh * 0.5f ){continue;}patch = frame( bop.allScanningWindows[idx] );getFernsFeatures( patch, bop.allScanningWindows[idx].scaleId, fern );negativeFernSamples.push_back( make_pair( fern, 0 ) );}/* 取 bad_patches 个归一化以后作为NN分类器的负样本 */Scalar tmp1, tmp2;negativeNNSamples = vector<Mat>( badPatchs );for ( int i = 0; i < badPatchs; i++ ){idx = bop.badBoxIndexes[i];patch = frame( bop.allScanningWindows[idx] );NormalOperation::getPattern( patch, negativeNNSamples[i], patchSize, tmp1, tmp2 );}
}

这篇关于TLD 目标跟踪源码理解的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/526583

相关文章

Go中sync.Once源码的深度讲解

《Go中sync.Once源码的深度讲解》sync.Once是Go语言标准库中的一个同步原语,用于确保某个操作只执行一次,本文将从源码出发为大家详细介绍一下sync.Once的具体使用,x希望对大家有... 目录概念简单示例源码解读总结概念sync.Once是Go语言标准库中的一个同步原语,用于确保某个操

一文带你理解Python中import机制与importlib的妙用

《一文带你理解Python中import机制与importlib的妙用》在Python编程的世界里,import语句是开发者最常用的工具之一,它就像一把钥匙,打开了通往各种功能和库的大门,下面就跟随小... 目录一、python import机制概述1.1 import语句的基本用法1.2 模块缓存机制1.

深入理解C语言的void*

《深入理解C语言的void*》本文主要介绍了C语言的void*,包括它的任意性、编译器对void*的类型检查以及需要显式类型转换的规则,具有一定的参考价值,感兴趣的可以了解一下... 目录一、void* 的类型任意性二、编译器对 void* 的类型检查三、需要显式类型转换占用的字节四、总结一、void* 的

深入理解Redis大key的危害及解决方案

《深入理解Redis大key的危害及解决方案》本文主要介绍了深入理解Redis大key的危害及解决方案,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着... 目录一、背景二、什么是大key三、大key评价标准四、大key 产生的原因与场景五、大key影响与危

深入理解C++ 空类大小

《深入理解C++空类大小》本文主要介绍了C++空类大小,规定空类大小为1字节,主要是为了保证对象的唯一性和可区分性,满足数组元素地址连续的要求,下面就来了解一下... 目录1. 保证对象的唯一性和可区分性2. 满足数组元素地址连续的要求3. 与C++的对象模型和内存管理机制相适配查看类对象内存在C++中,规

Java汇编源码如何查看环境搭建

《Java汇编源码如何查看环境搭建》:本文主要介绍如何在IntelliJIDEA开发环境中搭建字节码和汇编环境,以便更好地进行代码调优和JVM学习,首先,介绍了如何配置IntelliJIDEA以方... 目录一、简介二、在IDEA开发环境中搭建汇编环境2.1 在IDEA中搭建字节码查看环境2.1.1 搭建步

如何用Java结合经纬度位置计算目标点的日出日落时间详解

《如何用Java结合经纬度位置计算目标点的日出日落时间详解》这篇文章主详细讲解了如何基于目标点的经纬度计算日出日落时间,提供了在线API和Java库两种计算方法,并通过实际案例展示了其应用,需要的朋友... 目录前言一、应用示例1、天安门升旗时间2、湖南省日出日落信息二、Java日出日落计算1、在线API2

认识、理解、分类——acm之搜索

普通搜索方法有两种:1、广度优先搜索;2、深度优先搜索; 更多搜索方法: 3、双向广度优先搜索; 4、启发式搜索(包括A*算法等); 搜索通常会用到的知识点:状态压缩(位压缩,利用hash思想压缩)。

JAVA智听未来一站式有声阅读平台听书系统小程序源码

智听未来,一站式有声阅读平台听书系统 🌟&nbsp;开篇:遇见未来,从“智听”开始 在这个快节奏的时代,你是否渴望在忙碌的间隙,找到一片属于自己的宁静角落?是否梦想着能随时随地,沉浸在知识的海洋,或是故事的奇幻世界里?今天,就让我带你一起探索“智听未来”——这一站式有声阅读平台听书系统,它正悄悄改变着我们的阅读方式,让未来触手可及! 📚&nbsp;第一站:海量资源,应有尽有 走进“智听

烟火目标检测数据集 7800张 烟火检测 带标注 voc yolo

一个包含7800张带标注图像的数据集,专门用于烟火目标检测,是一个非常有价值的资源,尤其对于那些致力于公共安全、事件管理和烟花表演监控等领域的人士而言。下面是对此数据集的一个详细介绍: 数据集名称:烟火目标检测数据集 数据集规模: 图片数量:7800张类别:主要包含烟火类目标,可能还包括其他相关类别,如烟火发射装置、背景等。格式:图像文件通常为JPEG或PNG格式;标注文件可能为X