多目标跟踪(MOT)--DeepSort原理及代码详解

Posted Gthan学算法

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了多目标跟踪(MOT)--DeepSort原理及代码详解相关的知识,希望对你有一定的参考价值。

代码来源
论文链接

DeepSort

1. MOT(Multi-Object Tracking)简介

多目标跟踪是检测视频影像中出现的多个目标(如行人,汽车等)并对每个目标进行轨迹跟踪ID分配。其中视频前后出现的同一目标应分配同一ID,不同目标分配不同ID。

目前,MOT问题的处理框架主要有(1)先检测后跟踪(Tracking by detection),如Sort/DeepSort;(2)检测和跟踪联合,如JDE,CenterTrack等;(3)基于注意力机制,如TransTrack,TrackFormer等

其中处理过程主要可分为四个步骤:
1.按帧数解析输入视频
2.通过目标检测网络获取视频原始帧的目标检测框
3.对检测出的目标框进行特征提取(运动或语义特征)并对前后视频帧进行相似度计算
4.数据关联,匹配目标框和对应轨迹及ID

2. DeepSort前身:Sort(Simple Online And Realtime Tracking)


Sort主要思路:首先通过检测器Detections(如Faster RCNN)检测出每一帧的目标;其次通过卡尔曼滤波预测目标在下一帧的位置,将预测的位置与检测器实际检测的位置做相似度计算(IOU);最后通过匈牙利算法匹配对应轨迹和目标框,匹配结果分为三种:(1)未匹配的轨迹,直接删除,(2)未匹配的目标框,初始化为新的轨迹,(3)匹配成功的轨迹与目标框,通过卡尔曼滤波更新检测框位置,获取最优估计。

Sort存在问题:由于只考虑检测框与预测框的重叠面积(IOU),当两个目标发生遮挡后导致ID-Switch增大(同一目标的ID发生变化);同时未考虑对于遮挡后重新出现目标,进一步导致ID-Switch过大。

3. DeepSort总体框架(流程)

3.1 框架概要

对比Sort的主要改进:利用ReID模型提取外观语义特征,加入外观信息;增加了级联匹配(Matching Cascade)和轨迹确认(Confirmed、Tentative、Deleted)

DeepSort主要模块
(1)目标检测模块:通过目标检测网络,获取输入每一帧图片中的目标框
(2)轨迹跟踪模块:通过卡尔曼滤波进行轨迹预测和更新,获取新的轨迹集合
(3)数据匹配模块:通过级联匹配和IOU匹配将轨迹和目标框关联

DeepSort主要流程:检测器获取视频当前帧中目标框 → \\rightarrow 卡尔曼滤波根据当前帧的轨迹集合预测下一帧轨迹集合 → \\rightarrow 预测轨迹与下一帧检测目标框进行匹配 → \\rightarrow 卡尔曼滤波更新匹配成功的轨迹

3.2 流程分析

整个流程概括如下:
(1)将第一帧检测目标框初始化对应轨迹进行卡尔曼滤波预测下一时刻轨迹,其中初始化轨迹状态为不确定态
(2)将上一时刻确认态轨迹与当前时刻检测目标框进行级联匹配,级联匹配结果中匹配失败轨迹和匹配失败目标框用于后续IOU匹配,匹配成功轨迹和目标框进行卡尔曼滤波预测和更新
(3)将级联匹配结果中匹配失败轨迹和匹配失败目标框以及上一帧不确定态轨迹进行IOU匹配,匹配结果中匹配失败轨迹若仍为不确定态或为确定态但连续匹配失败次数超标则删除该轨迹;匹配失败轨迹为确定态且连续匹配失败次数未超标进行卡尔曼滤波预测;匹配失败目标框则初始化对应轨迹进行卡尔曼滤波预测;匹配成功轨迹和目标框进行卡尔曼滤波预测和更新
(4)重复(2)和(3),直到结束

4. DeepSort各模块讲解

4.1 目标检测模块

目标检测模块通过对应模型检测视频每一帧图片中的目标,其精度直接影响多目标跟踪的最终效果。

4.1.1 目标检测模型概述

目前整理了有关基于深度学习的目标检测模型的发展路线

首先以Anchor-based为基础的Two-stage模型因其检测速度慢的问题逐渐发展为One-stage模型。然而由于Anchor的设计存在超参数,难以覆盖所有尺寸目标;同时为了保证一定的精度,选择了大量的Anchor用来检测占用计算量和内存消耗。

后来以Anchor-free为基础的模型提出以检测关键点(中心点)的方式解决Anchor存在的问题,大大优化了模型超参数的数量。此外大多数目标检测模型需要对预测结果进行非极大值抑制(NMS)后处理,对此提出了NMS-free模型皆在取消NMS后处理,提高检测速度,实现真正意义上的端到端检测。

目前Transformer架构在视觉领域(ViT,Swin Transformer)取得了一系列成功。自此以Transformer为基础的目标检测模型展现出了优越的性能并且基于Transformer的模型能够天然的消除人工设置的Anchor-based以及NMS后处理是一个优良的端到端检测器。

有关目标检测模型解析,后续整理发布

4.1.2 Detection类解析

通过相应目标检测模型将视频中每一帧图片的检测结果(边框坐标及置信度得分)封装于Detection类,此外该类包含了ReID模块提取的外观语义特征以及不同边框坐标形式转换方法,具体代码如下:

class Detection(object):
    def __init__(self, tlwh, confidence, feature):
    	'''
    	tlwh:目标框左上角横纵坐标x, y; 宽w; 高h
    	confindnce:目标类别置信度得分
    	feature:ReID模块提取目标框的外观语义特征
    	'''
        self.tlwh = np.asarray(tlwh, dtype=np.float) 
        self.confidence = float(confidence) 
        self.feature = np.asarray(feature, dtype=np.float32) 

    def to_tlbr(self):
    	'''将目标框坐标tlwh转换为左上角横纵坐标x,y; 右下角横纵坐标x, y'''
        ret = self.tlwh.copy()
        ret[2:] += ret[:2]
        return ret

    def to_xyah(self):
    	'''将目标框坐标tlwh转换为中心点横纵坐标x,y; 宽高比a; 高h'''
        ret = self.tlwh.copy()
        ret[:2] += ret[2:] / 2
        ret[2] /= ret[3]
        return ret

4.2 轨迹跟踪模块

轨迹跟踪模块主要通过Track类和Tracker类完成,具体功能如下:
Track类:存储单个轨迹的状态和信息以及负责单个轨迹的预测和更新
Tracker类:存储所有轨迹(集合)的状态和信息,负责轨迹的初始化以及所有轨迹(集合)的预测和更新同时封装了数据匹配模块

4.2.1 Track类

Track类的核心代码及注释如下:

class TrackState:
	'''
	单个轨迹的三种状态
	'''
    Tentative = 1 #不确定态
    Confirmed = 2 #确定态
    Deleted = 3 #删除态

class Track:
    def __init__(self, mean, covariance, track_id, class_id, conf, n_init, max_age,
                 feature=None):
        '''
        mean:位置、速度状态分布均值向量,维度(8×1)
        convariance:位置、速度状态分布方差矩阵,维度(8×8)
        track_id:轨迹ID
        class_id:轨迹所属类别
        hits:轨迹更新次数(初始化为1),即轨迹与目标连续匹配成功次数
        age:轨迹连续存在的帧数(初始化为1),即轨迹出现到被删除的连续总帧数
        time_since_update:轨迹距离上次更新后的连续帧数(初始化为0),即轨迹与目标连续匹配失败次数
        state:轨迹状态
        features:轨迹所属目标的外观语义特征,轨迹匹配成功时添加当前帧的新外观语义特征
        conf:轨迹所属目标的置信度得分
        _n_init:轨迹状态由不确定态到确定态所需连续匹配成功的次数
        _max_age:轨迹状态由不确定态到删除态所需连续匹配失败的次数
        '''   
        self.mean = mean
        self.covariance = covariance
        self.track_id = track_id
        self.class_id = int(class_id)
        self.hits = 1
        self.age = 1
        self.time_since_update = 0

        self.state = TrackState.Tentative
        self.features = []
        if feature is not None:
            self.features.append(feature) #若不为None,初始化外观语义特征

        self.conf = conf
        self._n_init = n_init
        self._max_age = max_age

    def increment_age(self):
    	'''
    	预测下一帧轨迹时调用
    	'''
        self.age += 1 #轨迹连续存在帧数+1
        self.time_since_update += 1 #轨迹连续匹配失败次数+1

    def predict(self, kf):
    	'''
    	预测下一帧轨迹信息
    	'''
        self.mean, self.covariance = kf.predict(self.mean, self.covariance) #卡尔曼滤波预测下一帧轨迹的状态均值和方差
        self.increment_age() #调用函数,age+1,time_since_update+1

    def update(self, kf, detection, class_id, conf):
    	'''
    	更新匹配成功的轨迹信息
    	'''
        self.conf = conf #更新置信度得分
        self.mean, self.covariance = kf.update(
            self.mean, self.covariance, detection.to_xyah()) #卡尔曼滤波更新轨迹的状态均值和方差
        self.features.append(detection.feature) #添加轨迹对应目标框的外观语义特征
        self.class_id = class_id.int() #更新轨迹所属类别

        self.hits += 1 #轨迹匹配成功次数+1
        self.time_since_update = 0 #匹配成功时,轨迹连续匹配失败次数归0
        if self.state == TrackState.Tentative and self.hits >= self._n_init:
            self.state = TrackState.Confirmed #当连续匹配成功次数达标时轨迹由不确定态转为确定态

    def mark_missed(self):
    	'''
    	将轨迹状态转为删除态
    	'''
        if self.state == TrackState.Tentative:
            self.state = TrackState.Deleted #当级联匹配和IOU匹配后仍为不确定态
        elif self.time_since_update > self._max_age:
            self.state = TrackState.Deleted #当连续匹配失败次数超标

	'''
	该部分还存在一些轨迹坐标转化及状态判定函数,具体可参考代码来源
	'''

Track类是轨迹的一个基本单位,通过mean、covariance存储轨迹的位置和速度信息,利用卡尔曼滤波对轨迹信息进行预测更新;同时定义了轨迹的三种状态(Tentaitve、Confirmed、Deleted)及三种状态之间的转换关系。其中三种状态如下:

Tentaitive态:代表轨迹初始(默认)状态,该状态轨迹更新时连续匹配成功次数(hits)+1,达到指定匹配成功次数(_n_init默认为3)转换为Confirmed态;经过级联匹配和IOU匹配后该轨迹状态仍为Tentaiva,则认为轨迹没有匹配上任何目标,转换为Deleted态。

Confirmed态:代表轨迹匹配成功状态。该状态轨迹预测而不更新时连续匹配失败次数(time_since_update)+1,达到连续匹配失败次数(_max_age默认为70)转换为Deleted态。

Deleted态:代表轨迹失效状态。该轨迹从所有轨迹(集合)中删除。

4.2.2 Tracker类

Tracker类的核心代码及注释如下:
其中只列出了用于轨迹预测、更新及初始化部分,对于其中的数据匹配模块在4.3.2中讲解

class Tracker:
    GATING_THRESHOLD = np.sqrt(kalman_filter.chi2inv95[4]) #门控距离的阈值
    
    def __init__(self, metric, max_iou_distance=0.9, max_age=30, n_init=3, _lambda=0):
    	'''
    	metric:用于关联轨迹与目标框的距离度量函数,是NearestNeighborDistanceMetric类
    	max_iou_distance:IOU匹配中代价矩阵的阈值
    	max_age:轨迹状态由不确定态到删除所需连续匹配失败的次数
    	n_init:轨迹状态由不确定态到确定态所需连续匹配成功的次数
    	_lambda:门控距离矩阵和外观语义特征相似度矩阵之间的权重
    	kf:卡尔曼滤波模块
    	tracks:所有轨迹状态和信息集合
    	_next_id:下一个轨迹ID
    	'''
        self.metric = metric
        self.max_iou_distance = max_iou_distance
        self.max_age = max_age
        self.n_init = n_init
        self._lambda = _lambda

        self.kf = kalman_filter.KalmanFilter()
        self.tracks = []
        self._next_id = 1

    def predict(self):
    	'''
    	预测轨迹集合中所有轨迹的下一帧信息
    	'''
        for track in self.tracks:
            track.predict(self.kf)

    def increment_ages(self):
    	'''
    	当视频帧中未检测到目标时调用
    	因无法进行卡尔曼滤波更新,故不再额外进行卡尔曼滤波预测,但需要更新相应轨迹信息
    	'''
        for track in self.tracks:
            track.increment_age() #增加轨迹存在帧数和匹配失败次数
            track.mark_missed() #判断轨迹是否需要转为删除态

    def update(self, detections, classes, confidences):
    	'''
    	更新轨迹集合中所有轨迹状态和信息
    	'''
        matches, unmatched_tracks, unmatched_detections = \\
            self._match(detections) #进行级联匹配和IOU匹配,得到匹配成功轨迹和目标框、匹配失败轨迹以及匹配失败目标框
		
		#更新轨迹集合的状态和信息
        for track_idx, detection_idx in matches:
            self.tracks[track_idx].update(self.kf, detections[detection_idx], classes[detection_idx], 
            							  confidences[detection_idx]) #更新匹配成功的轨迹信息
        for track_idx in unmatched_tracks:
            self.tracks[track_idx].mark_missed() #判断匹配失败轨迹是否需要转为删除态
        for detection_idx in unmatched_detections:
            self._initiate_track(detections[detection_idx], classes[detection_idx].item(), 
            					 confidences[detection_idx].item()) #对匹配失败的目标框初始化轨迹
        self.tracks = [t for t in self.tracks if not t.is_deleted()] #从轨迹集合中剔除删除态轨迹
		
		#更新度量轨迹和目标框距离中的外观语义特征信息
        active_targets = [t.track_id for t in self.tracks if t.is_confirmed()] #确定态轨迹ID集合
        features, targets = [], []
        for track in self.tracks:
            if not track.is_confirmed():
                continue #选择确认态轨迹
            features += track.features #确认态轨迹的外观语义特征矩阵
            targets += [track.track_id for _ in track.features] #外观语义特征对应的确认态轨迹ID集合
            track.features = [] #清空确定态轨迹的外观语义特征
        self.metric.partial_fit(np.asarray(features), np.asarray(targets), 
        						active_targets) #更新确认态轨迹(同一ID)以往所有时刻的外观语义特征字典
    
    def _initiate_track(self, detection, class_id, conf):
    	'''
    	初始化轨迹状态和信息
    	'''
        mean, covariance = self.kf.initiate(detection.to_xyah()) #卡尔曼滤波初始化轨迹位置和速度信息
        self.tracks.append(Track(
            mean, covariance, self._next_id, class_id, conf, self.n_init, self.max_age,
            detection.feature)) #初始化轨迹加入轨迹集合
        self._next_id += 1 #轨迹数量增加,下一个轨迹ID+1

	'''
	该部分还存在_full_cost_metric以及_match函数代码用于轨迹和目标框的匹配,在4.3.2中讲解
	'''

Tracker类是整个DeepSort框架的核心部分整合所有轨迹的信息和状态逐一通过卡尔曼滤波对轨迹信息进行预测。由于在轨迹集合更新过程中涉及数据匹配模块,Tracker类的update函数封装了级联匹配和IOU匹配,根据对应匹配结果结合卡尔曼滤波更新轨迹集合中所有轨迹的状态和信息并且针对未匹配的目标(以及第一帧目标框)初始化轨迹状态和信息。该部分结合4.3.2级联匹配和IOU匹配以及3.1DeepSort框架图阅读。

4.2.3 卡尔曼滤波

卡尔曼滤波主要包含预测和更新。其中预测是根据系统当前状态对系统的下一步状态做出有根据的预测更新是通过测量值预测值综合计算出系统状态的最优估计值。具体可参考详解卡尔曼滤波原理博客,其中引用表达式:

预测:
x ⃗ k = F k x ⃗ k − 1 + B k u ⃗ k \\vec x_k = F_k \\vec x_k - 1 + B_k\\vec u_k x k=Fkx k1+Bku k
P k = F k P k − 1 F k T + Q k P_k = F_kP_k - 1F_k^T + Q_k Pk=FkPk1FkT+Qk

x ⃗ \\vec x x : 状态向量(均值)
u ⃗ \\vec u u : 外部控制向量
P P P:状态矩阵(协方差)
F F F:状态转移矩阵
B B B: 外部控制矩阵
Q Q Q: 外部干扰矩阵(噪声)

更新:
x ⃗ k ′ = x ⃗ k + K ( z ⃗ k − H k x ⃗ k ) \\vec x'_k = \\vec x_k + K(\\vec z_k - H_k\\vec x_k) x k=#私藏项目实操分享#原理讲解-项目实战 <-; 多目标跟踪算法之DeepSORT

用 YOLO v5+DeepSORT,打造实时多目标跟踪模型

多目标跟踪(MOT/MTT)

#yyds干货盘点#项目实战 <-; DeepSORT算法实现车辆和行人跟踪计数和是否道路违规检测

多目标跟踪 C++ 实现支持deepsort 和 bytetrack

(HOTA)多目标跟踪MOT指标计算方法