本文参考sort算法过程直白解读,在此基础上写出自己梳理一遍abewley/sort源代码,如有侵权,请联系删除。
使用SoRT
算法已经有一段时间,发现流程较为清晰,这里简单写篇博客进行总结与数理。效果上面肯定会逊色于DeepSoRT
算法,但是目前来看SoRT
算法是非深度学习中应用较为广泛的目标跟踪算法之一。需要你能够找到比较合适的Features
进行匹配关联。如果你想快速了解SoRT
算法,请直接先看SoRT
流程图的可视化流程操作。
其实解释算法流程是如何运作的,最为直白的方式就是单步调试并且分析每一步的大致结果与流程。这位博主的写的比较具体与详细,传送门sort算法过程直白解读。我在阅读她的博客基础上,通过VS2019
进行单步调试abewley/sort源代码进行分析。下面我会贴出调试SoRT
算法的流程来直白分析。
SoRT
算法流程整体来看就几个步骤循环执行,当前帧检测结果与上一帧追踪结果再进行KalmanFilter
预测结果进行数据关联,对关联后的进行处理。无非有以下几种情况:
然后就是对上述几种情况进行如何处理:
max_age
,如果大于则从追踪列表删除;否则,就将失去关联计数加1。min_hits
之后在追踪列表显示进行输出;ok, 下面我们来通过调试的每一帧结果进行解释上述SoRT
算法的流程。
第1帧
第一帧检测dets
结果如上图可视化,有3个行人目标被检测出来。由于是第一帧,追踪的trks
为空。由于trks
为空,那么第一帧检测dets与trks的匹配结果为:match = [] unmatched_dets = [0 1 2] unmatched_trks = []
,(其中unmatched_dets
里面的为dets
的索引好)。然后需要使用KalmanFilter
对未匹配检测的结果进行初始化,并且加入trks
中。结果见下图:
通过上图可以清晰得出:第1帧检测3个行人,跟踪列表为空,很容易得到检测结果与跟踪结果进行关联匹配,存在3个未检测的匹配检测结果,将这3个检测结果使用KalmanFilter
进行初始化加入跟踪列表中。
第2帧
第2帧检测3个行人,数据见下图dets
列表。我们知道第1帧的结果已经进行KalmanFilter
初始化并且加入追踪列表里面,所以对当前追踪列表里面进行一次预测,即KalmanFilter
的预测阶段,预测的数据见下图predicts
列表。
注意:这里面predicts
列表数据与dets
里面数据已经不同了,接下来需要进行计算IoU
匹配,然后进行匈牙利算法进行关联。
将当前帧dets
与前一帧追踪列表predicts
列表逐一计算IoU
,然后使用匈牙利算法进行最大化关联匹配。最后得到第2帧的关联结果:match
3个检测结果与3个最终列表关联成功,unmatched_dets
未关联成功的检测为0,unmatched_trks
未关联成功的追踪为0。
// match里面数字索引列,第一位是检测列表的索引,第二位为追踪预测列表的索引;
match = [
[0, 2]
[1, 0]
[2, 1] ]
unmatched_dets = []
unmatched_trks = []
在进行过匈牙利匹配关联成功之后,match
成功的需要进行对追踪列表更新。那么如何进行更新?一句话:就是当然帧检测结果来更新预测列表上对应的关联成功的,来对其KalmanFilter
的相关参数进行更新。
举例:上述match
成功的索引,检测结果dets[0]
与预测结果predicts[2]
成功关联,那么就用dets[0]
的box
信息输入predicts[0]
所对应的KalmanFilter
对象中对其进行更新。
如果存在未关联成功的检测结果,那么需要使用KalmanFilter
进行初始化,并且加入追踪列表中。
如果存在未关联成功的追踪结果,有个参数max_age
代表最大多少帧未关联上,如果没有超过max_age
,那么继续使用KalmanFilter
进行跟踪,如果超过max_age
,那么将其从跟踪列表删除。
至此,第2帧结束。
第3帧
第3帧检测dets
为3个行人,与第2帧更新后的KalmanFilter
权重进行预测,然后进行匹配关联,结果流程与第2帧一致。关联成功3个,未关联成功的检测结果为0,未关联成功的追踪结果为0。只是第3帧有一点与第2帧不同,就是追踪输出3个行人,这是因为追踪列表中的这3个行人已经成功关联3次,大于min_hits
,可以作为追踪结果输出。
第4-11帧
第4-11帧关联成功,流程与第3帧一致,所以追踪列表一直输出3个追踪结果。
第12帧
第12帧检测有4个行人,与第11帧的追踪列表3个行人预测的结果,进行IoU计算与匈牙利算法关联匹配。关联结果:
match = [
[0, 2]
[1, 1]
[2, 0]]
unmatched_dets = [3]
unmatched_trks = []
分别对关联的检测与追踪的结果进行处理:①对match
的结果使用当前dets
的结果来更新KalmanFilter
的权重系数,同时更新追踪列表的min_hits
与max_age
参数。②对unmatched_dets
使用KalmanFilter
进行初始化,同时加入追踪列表中。③对unmatched_trks
的目标进行判断,看失去跟踪的次数是否大于max_age
,大于则将其从追踪列表中移除,否则失去关联计数+1。
第13帧
我们知道第12帧检测有4个目标,匹配成功3个,还有1个进行KalmanFilter初始化后并且加入追踪列表中,所以在第12帧中的预测第13帧的结果有4个预测,请看上图。第13帧中检测的结果为4个,与预测的4个进行关联匹配。
match = [
[0, 1]
[1, 0]
[2, 2]
[3, 3]]
unmatched_dets = []
unmatched_trks = []
第13帧中匹配成功4个关联都会在追踪列表中,但是第13帧中输出的追踪结果只存在3个。原因在于刚刚在12帧中1个检测的结果刚刚加入追踪列表且关联成功次数没有超过min_hits
,当超过时候会从追踪列表输出。
第14帧
在第14帧时候,流程与第13帧处理一致。只是这里在第12帧新加入的检测那个KalmanFilter
的关联次数在第14帧时候已经达到min_hits
次数,因此在追踪列表中进行输出。
第15-18帧
一致稳定检测4个目标,4个追踪结果进行迭代更新KalmanFilter
的追踪。
第19帧
我们从第18帧可以看到,18帧的输出追踪有5个。我们在第19帧看到18帧预测的结果为5个,同时第19帧检测的结果也为4个,按照计算IoU后的代价矩阵和匈牙利最大化关联匹配,当前帧的输出应该为4个追踪目标,为什么输出变成了3个?
match = [
[0, 1]
[1, 0]
[2, 3]]
unmatched_dets = [3]
unmatched_trks = [2, 4]
原因在于:来看关联目标的IoU
是否满足阈值,【dets=[3] trks=[4]
为索引号】虽然关联成功,但是IoU
的阈值不满足,也会从追踪列表中移除。因为设置IoU
的阈值能够有效抑制错误关联的目标,提高追踪的效果。同时需要对unmatched_dets
添加KalmanFilter
进行初始化。另外trks=[2]
的索引没有关联成功,所以并不在追踪列表中打印输出出来。
第20帧
第20帧检测4个目标,上一帧19帧预测5个目标(其中有一个在第19帧中未匹配成功,一个是检测刚加入追踪列表),其中检测4个与预测5个关联匹配的结果:
match = [
[0, 1]
[1, 0]
[2, 3]
[3, 4]]
unmatched_dets = []
unmatched_trks = [3]
第21帧
第21帧检测结果为4个,第20帧预测的结果为4,我们知道第19帧预测结果有5个目标,由于一个追踪目标失去关联,因此从追踪列表中移除,所以在第20帧预测结果只有4个目标 =【3个19帧关联成功 + 19帧一个未关联成功的检测结果KalmanFilter初始化】。我的程序设置max_age为1,即只要关联失败将会从追踪列表中移除。
说了这么多,无非就以下几个步骤来判断SoRT算法:
以下为SORT算法的测试代码部分,代码来源abewley/sort源代码,我在流程中添加一些注释进行解释整个流程。这里测试检测的结果是从文件夹下面对应的每一帧的txt
读取保存在detectionFile
中的,这里的每一个txt
对应读取的图像.。最后将追踪的结果保存在txt
文件夹中并且可视化出来。
void TestSORT(std::string seqName, bool display)
{
std::cout << "Processing " << seqName << "..." << std::endl;
// 0. randomly generate colors, only for display
cv::RNG rng(0xFFFFFFFF);
cv::Scalar_<int> randColor[CNUM];
for (int i = 0; i < CNUM; i++)
rng.fill(randColor[i], cv::RNG::UNIFORM, 0, 256);
std::string imgPath = "D:/Data/2DMOT2015/train/" + seqName + "/img1/";
if (display)
if (_access(imgPath.c_str(), 0) == -1)
{
std::cerr << "Image path not found!" << std::endl;
display = false;
}
// 1. read detection file
std::ifstream detectionFile;
std::string detFileName = "data/" + seqName + "/det.txt";
detectionFile.open(detFileName);
if (!detectionFile.is_open())
{
std::cerr << "Error: can not find file " << detFileName << std::endl;
return;
}
std::string detLine;
std::istringstream ss;
std::vector<TrackingBox> detData;
char ch;
float tpx, tpy, tpw, tph;
while ( getline(detectionFile, detLine) )
{
TrackingBox tb;
ss.str(detLine);
ss >> tb.frame >> ch >> tb.id >> ch;
ss >> tpx >> ch >> tpy >> ch >> tpw >> ch >> tph;
ss.str("");
tb.box = cv::Rect_<float>(cv::Point_<float>(tpx, tpy), cv::Point_<float>(tpx + tpw, tpy + tph));
detData.push_back(tb);
}
detectionFile.close();
// 2. group detData by frame
int maxFrame = 0;
for (auto tb : detData) // find max frame number
{
if (maxFrame < tb.frame)
maxFrame = tb.frame;
}
std::vector<std::vector<TrackingBox>> detFrameData;
std::vector<TrackingBox> tempVec;
for (int fi = 0; fi < maxFrame; fi++)
{
for (auto tb : detData)
if (tb.frame == fi + 1) // frame num starts from 1
tempVec.push_back(tb);
detFrameData.push_back(tempVec);
tempVec.clear();
}
// 3. update across frames
int frame_count = 0; // 帧率统计
int max_age = 3; // 在追踪列表中有几帧没有关联匹配成功则就变为不可靠
int min_hits = 3; // 表示连续3帧以上都匹配成功,则作为追踪结果输出
double iouThreshold = 0.3; // IoU匹配的阈值
std::vector<KalmanTracker> trackers; // 追踪存储
KalmanTracker::kf_count = 0; // tracking id relies on this, so we have to reset it in each seq.
// variables used in the for-loop
std::vector<cv::Rect_<float>> predictedBoxes; // 卡尔曼滤波预测的信息
std::vector<std::vector<double>> iouMatrix; // det与trk的IoU代价矩阵
std::vector<int> assignment; //
std::set<int> unmatchedDetections; // 未匹配成功的检测结果,给予添加追踪信息
std::set<int> unmatchedTrajectories; // 未匹配成功的追踪结果,如果未匹配此时大于max_age则不可靠,直接删除;否则,计数加+1
std::set<int> allItems;
std::set<int> matchedItems;
std::vector<cv::Point> matchedPairs;
// 关联匹配的对 其中里面的cv::Point代表 x: 追踪障碍物列表索引 y : 检测结果的障碍物索引
std::vector<TrackingBox> frameTrackingResult;
unsigned int trkNum = 0; // 追踪列表个数
unsigned int detNum = 0; // 检测结果个数
double cycle_time = 0.0;
int64 start_time = 0;
// prepare result file.
std::ofstream resultsFile;
std::string resFileName = "output/" + seqName + ".txt";
resultsFile.open(resFileName);
if (!resultsFile.is_open())
{
std::cerr << "Error: can not create file " << resFileName << std::endl;
return;
}
//
// main loop
for (int fi = 0; fi < maxFrame; fi++)
{
total_frames++;
frame_count++;
std::cout << "当前是第 " << frame_count << " 帧." << std::endl;
// I used to count running time using clock(), but found it seems to conflict with cv::waitkey(),
// when they both exists, clock() can not get right result. Now I use cv::getTickCount() instead.
start_time = cv::getTickCount();
if (trackers.size() == 0) // the first frame met
{ // 第一帧,只有检测结果,追踪结果为空. 将检测结果初始化添加追踪信息
// 或者是追踪结果为空的情况下.
// initialize kalman trackers using first detections.
for (unsigned int i = 0; i < detFrameData[fi].size(); i++)
{ // 将第一帧的检测结果添加KalmanFilter跟踪信息.
KalmanTracker trk = KalmanTracker(detFrameData[fi][i].box);
trackers.push_back(trk);
}
// output the first frame detections
for (unsigned int id = 0; id < detFrameData[fi].size(); id++)
{ // 写入txt文件中.
TrackingBox tb = detFrameData[fi][id];
resultsFile << tb.frame << "," << id + 1 << "," << tb.box.x << "," << tb.box.y << ","
<< tb.box.width << "," << tb.box.height << ",1,-1,-1,-1" << std::endl;
}
continue;
}
///
// 3.1. get predicted locations from existing trackers.
predictedBoxes.clear();
// 追踪列表不为空的情况下,对其进行KalmanFilter预测
for (auto it = trackers.begin(); it != trackers.end();)
{ // 将追踪列表中的所有障碍物进行预测更新.
cv::Rect_<float> pBox = (*it).predict();
if (pBox.x >= 0 && pBox.y >= 0) // 判断预测是否有效
{ // 这里是图像的特殊来判断是否预测有效.
predictedBoxes.push_back(pBox);
it++;
}
else
{
it = trackers.erase(it);
std::cerr << "删除追踪列表中失效的Box : " << frame_count << std::endl;
}
}
///
// 对检测的结果与上一帧追踪列表中预测的当前帧所处位置进行IoU关联
// 3.2. associate detections to tracked object (both represented as bounding boxes)
// dets : detFrameData[fi]
trkNum = predictedBoxes.size();
detNum = detFrameData[fi].size();
iouMatrix.clear();
iouMatrix.resize(trkNum, std::vector<double>(detNum, 0));
for (unsigned int i = 0; i < trkNum; i++) // compute iou matrix as a distance matrix
{
for (unsigned int j = 0; j < detNum; j++)
{
// use 1-iou because the hungarian algorithm computes a minimum-cost assignment.
// 这里如果可以获取比较强的features,那么就能够直接来判断是否匹配成功,以此设定阈值.
iouMatrix[i][j] = 1 - features_simlarity::GetIOU(predictedBoxes[i], detFrameData[fi][j].box);
}
}
// solve the assignment problem using hungarian algorithm.
// the resulting assignment is [track(prediction) : detection], with len=preNum
// 匈牙利算法进行关联匹配
HungarianAlgorithm HungAlgo;
assignment.clear();
HungAlgo.Solve(iouMatrix, assignment);
// find matches, unmatched_detections and unmatched_predictions
unmatchedTrajectories.clear();
unmatchedDetections.clear();
allItems.clear();
matchedItems.clear();
// 如果检测的个数与追踪列表个数一致,什么都不操作?
if (detNum > trkNum) // there are unmatched detections
{ // 检测结果多于追踪的个数,存在未匹配的检测
for (unsigned int n = 0; n < detNum; n++)
allItems.insert(n);
for (unsigned int i = 0; i < trkNum; ++i)
matchedItems.insert(assignment[i]);
// set_diference做两个集合的差集,剩余下来的就是未匹配的检测结果.
std::set_difference(allItems.begin(), allItems.end(),
matchedItems.begin(), matchedItems.end(),
std::insert_iterator<std::set<int>>(unmatchedDetections, unmatchedDetections.begin()));
}
else if (detNum < trkNum) // there are unmatched trajectory/predictions
{ // 检测结果少于追踪结果的个数,未匹配的追踪的结果
for (unsigned int i = 0; i < trkNum; ++i)
if (assignment[i] == -1) // unassigned label will be set as -1 in the assignment algorithm
unmatchedTrajectories.insert(i);
}
else
{
// 检测结果个数与追踪结果个数相同时候,匈牙利最大匹配策略会对其进行一一对应;
; // do Nothing.
}
// filter out matched with low IOU
matchedPairs.clear();
for (unsigned int i = 0; i < trkNum; ++i)
{
if (assignment[i] == -1) // pass over invalid values
continue;
if (1 - iouMatrix[i][assignment[i]] < iouThreshold)
{
unmatchedTrajectories.insert(i);
unmatchedDetections.insert(assignment[i]); // 匹配阈值小于参数阈值
}
else
{ // 匹配对的序列索引 其中i代表是追踪列表的障碍物
// assignment[i]为匹配上检测列表障碍物索引
matchedPairs.push_back(cv::Point(i, assignment[i]));
}
}
///
// 3.3. updating trackers
// update matched trackers with assigned detections.
// each prediction is corresponding to a tracker
// 对追踪结果列表关联成功的检测结果,使用检测结果进行更新.
int detIdx, trkIdx;
for (unsigned int i = 0; i < matchedPairs.size(); i++)
{
trkIdx = matchedPairs[i].x; // 追踪列表的索引
detIdx = matchedPairs[i].y; // 检测列表的索引
trackers[trkIdx].update(detFrameData[fi][detIdx].box); // 使用检测结果的信息来更新追踪结果
}
// create and initialise new trackers for unmatched detections
// 对未匹配成功的检测结果,给予添加KalmanFilter进行追踪.
for (auto umd : unmatchedDetections)
{
KalmanTracker tracker = KalmanTracker(detFrameData[fi][umd].box);
trackers.push_back(tracker);
}
// get trackers' output
// 获取追踪的结果.
frameTrackingResult.clear();
for (auto it = trackers.begin(); it != trackers.end();)
{
// 这里追踪列表 m_time_since_update 必须小于1代表已经更新
// min_hits 代表最低多少次关联成功 || 帧率前多少帧内
if (((*it).m_time_since_update < 1) &&
((*it).m_hit_streak >= min_hits || frame_count <= min_hits))
{
TrackingBox res;
res.box = (*it).get_state();
res.id = (*it).m_id + 1;
res.frame = frame_count;
frameTrackingResult.push_back(res);
it++;
}
else
it++;
// remove dead tracklet
// 移除已经超过max_age帧没有更新的追踪列表中的障碍物.
if (it != trackers.end() && (*it).m_time_since_update > max_age)
it = trackers.erase(it);
}
cycle_time = (double)(cv::getTickCount() - start_time);
total_time += cycle_time / cv::getTickFrequency();
for (auto tb : frameTrackingResult)
resultsFile << tb.frame << "," << tb.id << "," << tb.box.x << "," << tb.box.y << ","
<< tb.box.width << "," << tb.box.height << ",1,-1,-1,-1" << std::endl;
// 显示结果
if (display) // read image, draw results and show them
{
std::ostringstream oss;
oss << imgPath << std::setw(6) << std::setfill('0') << fi + 1;
cv::Mat img = cv::imread(oss.str() + ".jpg");
if (img.empty())
continue;
for (auto tb : frameTrackingResult)
{
cv::rectangle(img, tb.box, randColor[tb.id % CNUM], 2, 8, 0);
cv::putText(img, std::to_string(tb.id), cv::Point(tb.box.x, tb.box.y), cv::FONT_HERSHEY_COMPLEX, 0.5, (255, 255, 255), 0.5, 8);
}
cv::imshow(seqName, img);
cv::waitKey(400);
}
}
resultsFile.close();
if (display)
cv::destroyAllWindows();
}
是否你会觉得奇怪,按道理第一帧只该将检测的结果3个行人被检测到,按照关联min_hits=3
次以上才可以加入追踪列表显示,只因在代码中加入这个判断使其在算法开始执行时候显示。请从下面源码分析中找到对应的行。
if (((*it).m_time_since_update < 1) &&
((*it).m_hit_streak >= min_hits || frame_count <= min_hits))
如果总结SORT
算法的流程:前一帧追踪结果使用KalmanFilter
预测数据与当前帧的检测结果进行IoU
计算与匈牙利匹配关联,得到匹配的结果、未匹配检测的结果、未匹配追踪的结果。然后分别对匹配的结果:使用当前检测结果对KalmanFilter
进行状态更新;对未匹配检测的结果进行KalmanFilter
初始化;对未匹配追踪的结果进行判断有多少帧没有关联上,如果超过阈值则将其从追踪列表删除,否则计数加1。剩下的就是一些条件限制,例如连续几帧关联上才显示出来,每次关联上后需要对其更新来改变其状态等。
sort算法过程直白解读