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

相关文章

Android实现定时任务的几种方式汇总(附源码)

《Android实现定时任务的几种方式汇总(附源码)》在Android应用中,定时任务(ScheduledTask)的需求几乎无处不在:从定时刷新数据、定时备份、定时推送通知,到夜间静默下载、循环执行... 目录一、项目介绍1. 背景与意义二、相关基础知识与系统约束三、方案一:Handler.postDel

Java 正则表达式URL 匹配与源码全解析

《Java正则表达式URL匹配与源码全解析》在Web应用开发中,我们经常需要对URL进行格式验证,今天我们结合Java的Pattern和Matcher类,深入理解正则表达式在实际应用中... 目录1.正则表达式分解:2. 添加域名匹配 (2)3. 添加路径和查询参数匹配 (3) 4. 最终优化版本5.设计思

深入理解Apache Kafka(分布式流处理平台)

《深入理解ApacheKafka(分布式流处理平台)》ApacheKafka作为现代分布式系统中的核心中间件,为构建高吞吐量、低延迟的数据管道提供了强大支持,本文将深入探讨Kafka的核心概念、架构... 目录引言一、Apache Kafka概述1.1 什么是Kafka?1.2 Kafka的核心概念二、Ka

Java调用C++动态库超详细步骤讲解(附源码)

《Java调用C++动态库超详细步骤讲解(附源码)》C语言因其高效和接近硬件的特性,时常会被用在性能要求较高或者需要直接操作硬件的场合,:本文主要介绍Java调用C++动态库的相关资料,文中通过代... 目录一、直接调用C++库第一步:动态库生成(vs2017+qt5.12.10)第二步:Java调用C++

Python实现无痛修改第三方库源码的方法详解

《Python实现无痛修改第三方库源码的方法详解》很多时候,我们下载的第三方库是不会有需求不满足的情况,但也有极少的情况,第三方库没有兼顾到需求,本文将介绍几个修改源码的操作,大家可以根据需求进行选择... 目录需求不符合模拟示例 1. 修改源文件2. 继承修改3. 猴子补丁4. 追踪局部变量需求不符合很

python+opencv处理颜色之将目标颜色转换实例代码

《python+opencv处理颜色之将目标颜色转换实例代码》OpenCV是一个的跨平台计算机视觉库,可以运行在Linux、Windows和MacOS操作系统上,:本文主要介绍python+ope... 目录下面是代码+ 效果 + 解释转HSV: 关于颜色总是要转HSV的掩膜再标注总结 目标:将红色的部分滤

一文详解SQL Server如何跟踪自动统计信息更新

《一文详解SQLServer如何跟踪自动统计信息更新》SQLServer数据库中,我们都清楚统计信息对于优化器来说非常重要,所以本文就来和大家简单聊一聊SQLServer如何跟踪自动统计信息更新吧... SQL Server数据库中,我们都清楚统计信息对于优化器来说非常重要。一般情况下,我们会开启"自动更新

Spring 中 BeanFactoryPostProcessor 的作用和示例源码分析

《Spring中BeanFactoryPostProcessor的作用和示例源码分析》Spring的BeanFactoryPostProcessor是容器初始化的扩展接口,允许在Bean实例化前... 目录一、概览1. 核心定位2. 核心功能详解3. 关键特性二、Spring 内置的 BeanFactory

深入理解Apache Airflow 调度器(最新推荐)

《深入理解ApacheAirflow调度器(最新推荐)》ApacheAirflow调度器是数据管道管理系统的关键组件,负责编排dag中任务的执行,通过理解调度器的角色和工作方式,正确配置调度器,并... 目录什么是Airflow 调度器?Airflow 调度器工作机制配置Airflow调度器调优及优化建议最

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

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