【SORT算法】系列之深度解读

目录

        • SORT算法
        • SORT算法流程解读
        • SORT流程图
        • SORT算法代码解读
          • 一些分析
        • 小结
        • 参考

SORT算法

本文参考sort算法过程直白解读,在此基础上写出自己梳理一遍abewley/sort源代码,如有侵权,请联系删除。

  使用SoRT算法已经有一段时间,发现流程较为清晰,这里简单写篇博客进行总结与数理。效果上面肯定会逊色于DeepSoRT算法,但是目前来看SoRT算法是非深度学习中应用较为广泛的目标跟踪算法之一。需要你能够找到比较合适的Features进行匹配关联。如果你想快速了解SoRT算法,请直接先看SoRT流程图的可视化流程操作。

SORT算法流程解读

  其实解释算法流程是如何运作的,最为直白的方式就是单步调试并且分析每一步的大致结果与流程。这位博主的写的比较具体与详细,传送门sort算法过程直白解读。我在阅读她的博客基础上,通过VS2019进行单步调试abewley/sort源代码进行分析。下面我会贴出调试SoRT算法的流程来直白分析。

  SoRT算法流程整体来看就几个步骤循环执行,当前帧检测结果与上一帧追踪结果再进行KalmanFilter预测结果进行数据关联,对关联后的进行处理。无非有以下几种情况:

  • 关联成功的检测box与追踪box;
  • 未关联成功的检测box;
  • 未关联成功的追踪box;

然后就是对上述几种情况进行如何处理:

  1. 关联成功的检测box与追踪box处理:使用检测的box对追踪结果进行KalmanFilter权重以及参数更新,同时记录关联追踪box的计数次数;
  2. 未关联成功的box处理:对检测的box进行KalmanFilter初始化,并且加入追踪列表中;
  3. 未关联成功的追踪box处理:首先判断该追踪box失去关联的计数次数是否大于最大的阈值max_age,如果大于则从追踪列表删除;否则,就将失去关联计数加1。
  4. 当然,还会存在关联几次min_hits之后在追踪列表显示进行输出;
  5. 至于关联是否成功是通过检测box与追踪box的IoU计算出来,然后组成代价矩阵,通过匈牙利算法找出最大匹配对。最后通过阈值来判断是否关联。

ok, 下面我们来通过调试的每一帧结果进行解释上述SoRT算法的流程。


第1帧

【SORT算法】系列之深度解读_第1张图片

  第一帧检测dets结果如上图可视化,有3个行人目标被检测出来。由于是第一帧,追踪的trks为空。由于trks为空,那么第一帧检测dets与trks的匹配结果为:match = [] unmatched_dets = [0 1 2] unmatched_trks = [],(其中unmatched_dets里面的为dets的索引好)。然后需要使用KalmanFilter对未匹配检测的结果进行初始化,并且加入trks中。结果见下图:

【SORT算法】系列之深度解读_第2张图片

  通过上图可以清晰得出:第1帧检测3个行人,跟踪列表为空,很容易得到检测结果与跟踪结果进行关联匹配,存在3个未检测的匹配检测结果,将这3个检测结果使用KalmanFilter进行初始化加入跟踪列表中。


第2帧

【SORT算法】系列之深度解读_第3张图片

  第2帧检测3个行人,数据见下图dets列表。我们知道第1帧的结果已经进行KalmanFilter初始化并且加入追踪列表里面,所以对当前追踪列表里面进行一次预测,即KalmanFilter的预测阶段,预测的数据见下图predicts列表。

【SORT算法】系列之深度解读_第4张图片

  注意:这里面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对象中对其进行更新。

【SORT算法】系列之深度解读_第5张图片

  如果存在未关联成功的检测结果,那么需要使用KalmanFilter进行初始化,并且加入追踪列表中。

【SORT算法】系列之深度解读_第6张图片

  如果存在未关联成功的追踪结果,有个参数max_age代表最大多少帧未关联上,如果没有超过max_age,那么继续使用KalmanFilter进行跟踪,如果超过max_age,那么将其从跟踪列表删除。

【SORT算法】系列之深度解读_第7张图片

至此,第2帧结束。


第3帧

  第3帧检测dets为3个行人,与第2帧更新后的KalmanFilter权重进行预测,然后进行匹配关联,结果流程与第2帧一致。关联成功3个,未关联成功的检测结果为0,未关联成功的追踪结果为0。只是第3帧有一点与第2帧不同,就是追踪输出3个行人,这是因为追踪列表中的这3个行人已经成功关联3次,大于min_hits,可以作为追踪结果输出。

【SORT算法】系列之深度解读_第8张图片

第4-11帧

第4-11帧关联成功,流程与第3帧一致,所以追踪列表一直输出3个追踪结果。


第12帧

【SORT算法】系列之深度解读_第9张图片

第12帧检测有4个行人,与第11帧的追踪列表3个行人预测的结果,进行IoU计算与匈牙利算法关联匹配。关联结果:

match = [
  [0, 2]
  [1, 1]
  [2, 0]]
unmatched_dets = [3]
unmatched_trks = []

  分别对关联的检测与追踪的结果进行处理:①对match的结果使用当前dets的结果来更新KalmanFilter的权重系数,同时更新追踪列表的min_hitsmax_age参数。②对unmatched_dets使用KalmanFilter进行初始化,同时加入追踪列表中。③对unmatched_trks的目标进行判断,看失去跟踪的次数是否大于max_age,大于则将其从追踪列表中移除,否则失去关联计数+1。


第13帧

【SORT算法】系列之深度解读_第10张图片

  我们知道第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帧

【SORT算法】系列之深度解读_第11张图片

  在第14帧时候,流程与第13帧处理一致。只是这里在第12帧新加入的检测那个KalmanFilter的关联次数在第14帧时候已经达到min_hits次数,因此在追踪列表中进行输出。


第15-18帧

一致稳定检测4个目标,4个追踪结果进行迭代更新KalmanFilter的追踪。

【SORT算法】系列之深度解读_第12张图片

第19帧

【SORT算法】系列之深度解读_第13张图片

  我们从第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帧

【SORT算法】系列之深度解读_第14张图片

第20帧检测4个目标,上一帧19帧预测5个目标(其中有一个在第19帧中未匹配成功,一个是检测刚加入追踪列表),其中检测4个与预测5个关联匹配的结果:

match = [
   [0, 1]
   [1, 0]
   [2, 3]
   [3, 4]]
unmatched_dets = []
unmatched_trks = [3]

第21帧

【SORT算法】系列之深度解读_第15张图片

第21帧检测结果为4个,第20帧预测的结果为4,我们知道第19帧预测结果有5个目标,由于一个追踪目标失去关联,因此从追踪列表中移除,所以在第20帧预测结果只有4个目标 =【3个19帧关联成功 + 19帧一个未关联成功的检测结果KalmanFilter初始化】。我的程序设置max_age为1,即只要关联失败将会从追踪列表中移除。


SORT流程图

说了这么多,无非就以下几个步骤来判断SoRT算法:

【SORT算法】系列之深度解读_第16张图片

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。剩下的就是一些条件限制,例如连续几帧关联上才显示出来,每次关联上后需要对其更新来改变其状态等。

  • 失去匹配关联的追踪目标,存在多少帧率后会从追踪列表中删除;
  • 刚刚进行关联成功的目标,几帧后才可以进入稳定的状态;
  • 计算每个检测结果与追踪列表中的IoU,并不是稳定区分匹配关联成功的要素;
  • 匈牙利匹配算法并不能够提升匹配成功率,只能说提高了匹配概率最大化;
  • 某种意义上匈牙利算法加入SoRT算法的流程并不是完全正确的思想,想想看匈牙利算法目的在于匹配个数最大化而不是匹配的准不准问题,这样就会带来错误匹配提升;
参考

sort算法过程直白解读

你可能感兴趣的:(机器学习,目标跟踪,SoRT)