DeepSORT算法代码解析(全)

21 篇文章 5 订阅
订阅专栏

Deep SORT多目标跟踪算法代码解析

Deep SORT是多目标跟踪(Multi-Object Tracking)中常用到的一种算法,是一个Detection Based Tracking的方法。这个算法工业界关注度非常高,在知乎上有很多文章都是使用了Deep SORT进行工程部署。笔者将参考前辈的博客,结合自己的实践(理论&代码)对Deep SORT算法进行代码层面的解析。

在之前笔者写的一篇 Deep SORT论文阅读总结中,总结了DeepSORT论文中提到的核心观点,如果对Deep SORT不是很熟悉,可以先理解一下,然后再来看解读代码的部分。

1. MOT主要步骤

在《DEEP LEARNING IN VIDEO MULTI-OBJECT TRACKING: A SURVEY》这篇基于深度学习的多目标跟踪的综述中,描述了MOT问题中四个主要步骤:

多目标跟踪众多的主要步骤

  • 给定视频原始帧。
  • 运行目标检测器如Faster R-CNN、YOLOv3、SSD等进行检测,获取目标检测框。
  • 将所有目标框中对应的目标抠出来,进行特征提取(包括表观特征或者运动特征)。
  • 进行相似度计算,计算前后两帧目标之间的匹配程度(前后属于同一个目标的之间的距离比较小,不同目标的距离比较大)
  • 数据关联,为每个对象分配目标的ID。

以上就是四个核心步骤,其中核心是检测,SORT论文的摘要中提到,仅仅换一个更好的检测器,就可以将目标跟踪表现提升18.9%。

2. SORT

Deep SORT算法的前身是SORT, 全称是Simple Online and Realtime Tracking。简单介绍一下,SORT最大特点是基于Faster R-CNN的目标检测方法,并利用卡尔曼滤波算法+匈牙利算法,极大提高了多目标跟踪的速度,同时达到了SOTA的准确率。

这个算法确实是在实际应用中使用较为广泛的一个算法,核心就是两个算法:卡尔曼滤波匈牙利算法

卡尔曼滤波算法分为两个过程,预测和更新。该算法将目标的运动状态定义为8个正态分布的向量。

预测:当目标经过移动,通过上一帧的目标框和速度等参数,预测出当前帧的目标框位置和速度等参数。

更新:预测值和观测值,两个正态分布的状态进行线性加权,得到目前系统预测的状态。

**匈牙利算法:**解决的是一个分配问题,在MOT主要步骤中的计算相似度的,得到了前后两帧的相似度矩阵。匈牙利算法就是通过求解这个相似度矩阵,从而解决前后两帧真正匹配的目标。这部分sklearn库有对应的函数linear_assignment来进行求解。

SORT算法中是通过前后两帧IOU来构建相似度矩阵,所以SORT计算速度非常快。

下图是一张SORT核心算法流程图:

Harlek提供的SORT解析图

Detections是通过目标检测器得到的目标框,Tracks是一段轨迹。核心是匹配的过程与卡尔曼滤波的预测和更新过程。

流程如下:目标检测器得到目标框Detections,同时卡尔曼滤波器预测当前的帧的Tracks, 然后将Detections和Tracks进行IOU匹配,最终得到的结果分为:

  • Unmatched Tracks,这部分被认为是失配,Detection和Track无法匹配,如果失配持续了 T l o s t T_{lost} Tlost次,该目标ID将从图片中删除。
  • Unmatched Detections, 这部分说明没有任意一个Track能匹配Detection, 所以要为这个detection分配一个新的track。
  • Matched Track,这部分说明得到了匹配。

卡尔曼滤波可以根据Tracks状态预测下一帧的目标框状态。

卡尔曼滤波更新是对观测值(匹配上的Track)和估计值更新所有track的状态。

3. Deep SORT

DeepSort中最大的特点是加入外观信息,借用了ReID领域模型来提取特征,减少了ID switch的次数。整体流程图如下:

图片来自知乎Harlek

可以看出,Deep SORT算法在SORT算法的基础上增加了级联匹配(Matching Cascade)+新轨迹的确认(confirmed)。总体流程就是:

  • 卡尔曼滤波器预测轨迹Tracks
  • 使用匈牙利算法将预测得到的轨迹Tracks和当前帧中的detections进行匹配(级联匹配和IOU匹配)
  • 卡尔曼滤波更新。

其中上图中的级联匹配展开如下:

图片来自知乎Harlek

上图非常清晰地解释了如何进行级联匹配,上图由虚线划分为两部分:

上半部分中计算相似度矩阵的方法使用到了外观模型(ReID)和运动模型(马氏距离)来计算相似度,得到代价矩阵,另外一个则是门控矩阵,用于限制代价矩阵中过大的值。

下半部分中是是级联匹配的数据关联步骤,匹配过程是一个循环(max age个迭代,默认为70),也就是从missing age=0到missing age=70的轨迹和Detections进行匹配,没有丢失过的轨迹优先匹配,丢失较为久远的就靠后匹配。通过这部分处理,可以重新将被遮挡目标找回,降低被遮挡然后再出现的目标发生的ID Switch次数。

将Detection和Track进行匹配,所以出现几种情况

  1. Detection和Track匹配,也就是Matched Tracks。普通连续跟踪的目标都属于这种情况,前后两帧都有目标,能够匹配上。
  2. Detection没有找到匹配的Track,也就是Unmatched Detections。图像中突然出现新的目标的时候,Detection无法在之前的Track找到匹配的目标。
  3. Track没有找到匹配的Detection,也就是Unmatched Tracks。连续追踪的目标超出图像区域,Track无法与当前任意一个Detection匹配。
  4. 以上没有涉及一种特殊的情况,就是两个目标遮挡的情况。刚刚被遮挡的目标的Track也无法匹配Detection,目标暂时从图像中消失。之后被遮挡目标再次出现的时候,应该尽量让被遮挡目标分配的ID不发生变动,减少ID Switch出现的次数,这就需要用到级联匹配了。

4. Deep SORT代码解析

论文中提供的代码是如下地址: https://github.com/nwojke/deep_sort

Github库中Deep_sort文件结构

上图是Github库中有关Deep SORT的核心代码,不包括Faster R-CNN检测部分,所以主要将讲解这部分的几个文件,笔者也对其中核心代码进行了部分注释,地址在: https://github.com/pprp/deep_sort_yolov3_pytorch , 将其中的目标检测器换成了U版的yolov3, 将deep_sort文件中的核心进行了调用。

4.1 类图

下图是笔者总结的这几个类调用的类图(不是特别严谨,但是能大概展示各个模块的关系):

Deep Sort类图

DeepSort是核心类,调用其他模块,大体上可以分为三个模块:

  • ReID模块,用于提取表观特征,原论文中是生成了128维的embedding。
  • Track模块,轨迹类,用于保存一个Track的状态信息,是一个基本单位。
  • Tracker模块,Tracker模块掌握最核心的算法,卡尔曼滤波匈牙利算法都是通过调用这个模块来完成的。

DeepSort类对外接口非常简单:

self.deepsort = DeepSort(args.deepsort_checkpoint)#实例化
outputs = self.deepsort.update(bbox_xcycwh, cls_conf, im)#通过接收目标检测结果进行更新

在外部调用的时候只需要以上两步即可,非常简单。

通过类图,对整体模块有了框架上理解,下面深入理解一下这些模块。

4.2 核心模块

Detection类
class Detection(object):
    """
    This class represents a bounding box detection in a single image.
	"""
    def __init__(self, tlwh, confidence, feature):
        self.tlwh = np.asarray(tlwh, dtype=np.float)
        self.confidence = float(confidence)
        self.feature = np.asarray(feature, dtype=np.float32)
    def to_tlbr(self):
        """Convert bounding box to format `(min x, min y, max x, max y)`, i.e.,
        `(top left, bottom right)`.
        """
        ret = self.tlwh.copy()
        ret[2:] += ret[:2]
        return ret
    def to_xyah(self):
        """Convert bounding box to format `(center x, center y, aspect ratio,
        height)`, where the aspect ratio is `width / height`.
        """
        ret = self.tlwh.copy()
        ret[:2] += ret[2:] / 2
        ret[2] /= ret[3]
        return ret

Detection类用于保存通过目标检测器得到的一个检测框,包含top left坐标+框的宽和高,以及该bbox的置信度还有通过reid获取得到的对应的embedding。除此以外提供了不同bbox位置格式的转换方法:

  • tlwh: 代表左上角坐标+宽高
  • tlbr: 代表左上角坐标+右下角坐标
  • xyah: 代表中心坐标+宽高比+高
Track类
class Track:
    # 一个轨迹的信息,包含(x,y,a,h) & v
    """
    A single target track with state space `(x, y, a, h)` and associated
    velocities, where `(x, y)` is the center of the bounding box, `a` is the
    aspect ratio and `h` is the height.
    """

    def __init__(self, mean, covariance, track_id, n_init, max_age,
                 feature=None):
        # max age是一个存活期限,默认为70帧,在
        self.mean = mean
        self.covariance = covariance
        self.track_id = track_id
        self.hits = 1 
        # hits和n_init进行比较
        # hits每次update的时候进行一次更新(只有match的时候才进行update)
        # hits代表匹配上了多少次,匹配次数超过n_init就会设置为confirmed状态
        self.age = 1 # 没有用到,和time_since_update功能重复
        self.time_since_update = 0
        # 每次调用predict函数的时候就会+1
        # 每次调用update函数的时候就会设置为0

        self.state = TrackState.Tentative
        self.features = []
        # 每个track对应多个features, 每次更新都将最新的feature添加到列表中
        if feature is not None:
            self.features.append(feature)

        self._n_init = n_init  # 如果连续n_init帧都没有出现匹配,设置为deleted状态
        self._max_age = max_age  # 上限

Track类主要存储的是轨迹信息,mean和covariance是保存的框的位置和速度信息,track_id代表分配给这个轨迹的ID。state代表框的状态,有三种:

  • Tentative: 不确定态,这种状态会在初始化一个Track的时候分配,并且只有在连续匹配上n_init帧才会转变为确定态。如果在处于不确定态的情况下没有匹配上任何detection,那将转变为删除态。
  • Confirmed: 确定态,代表该Track确实处于匹配状态。如果当前Track属于确定态,但是失配连续达到max age次数的时候,就会被转变为删除态。
  • Deleted: 删除态,说明该Track已经失效。

状态转换图

max_age代表一个Track存活期限,他需要和time_since_update变量进行比对。time_since_update是每次轨迹调用update函数的时候就会+1,每次调用predict的时候就会重置为0,也就是说如果一个轨迹长时间没有update(没有匹配上)的时候,就会不断增加,直到time_since_update超过max age(默认70),将这个Track从Tracker中的列表删除。

hits代表连续确认多少次,用在从不确定态转为确定态的时候。每次Track进行update的时候,hits就会+1, 如果hits>n_init(默认为3),也就是连续三帧的该轨迹都得到了匹配,这时候才将不确定态转为确定态。

需要说明的是每个轨迹还有一个重要的变量,features列表,存储该轨迹在不同帧对应位置通过ReID提取到的特征。为何要保存这个列表,而不是将其更新为当前最新的特征呢?这是为了解决目标被遮挡后再次出现的问题,需要从以往帧对应的特征进行匹配。另外,如果特征过多会严重拖慢计算速度,所以有一个参数budget用来控制特征列表的长度,取最新的budget个features,将旧的删除掉。

ReID特征提取部分

ReID网络是独立于目标检测和跟踪器的模块,功能是提取对应bounding box中的feature,得到一个固定维度的embedding作为该bbox的代表,供计算相似度时使用。

class Extractor(object):
    def __init__(self, model_name, model_path, use_cuda=True):
        self.net = build_model(name=model_name,
                               num_classes=96)
        self.device = "cuda" if torch.cuda.is_available(
        ) and use_cuda else "cpu"
        state_dict = torch.load(model_path)['net_dict']
        self.net.load_state_dict(state_dict)
        print("Loading weights from {}... Done!".format(model_path))
        self.net.to(self.device)
        self.size = (128,128)
        self.norm = transforms.Compose([
            transforms.ToTensor(),
            transforms.Normalize([0.3568, 0.3141, 0.2781],
                                 [0.1752, 0.1857, 0.1879])
        ])

    def _preprocess(self, im_crops):
        """
        TODO:
            1. to float with scale from 0 to 1
            2. resize to (64, 128) as Market1501 dataset did
            3. concatenate to a numpy array
            3. to torch Tensor
            4. normalize
        """
        def _resize(im, size):
            return cv2.resize(im.astype(np.float32) / 255., size)

        im_batch = torch.cat([
            self.norm(_resize(im, self.size)).unsqueeze(0) for im in im_crops
        ],dim=0).float()
        return im_batch

    def __call__(self, im_crops):
        im_batch = self._preprocess(im_crops)
        with torch.no_grad():
            im_batch = im_batch.to(self.device)
            features = self.net(im_batch)
        return features.cpu().numpy()

模型训练是按照传统ReID的方法进行,使用Extractor类的时候输入为一个list的图片,得到图片对应的特征。

NearestNeighborDistanceMetric类

这个类中用到了两个计算距离的函数:

  1. 计算欧氏距离
def _pdist(a, b):
    # 用于计算成对的平方距离
    # a NxM 代表N个对象,每个对象有M个数值作为embedding进行比较
    # b LxM 代表L个对象,每个对象有M个数值作为embedding进行比较
    # 返回的是NxL的矩阵,比如dist[i][j]代表a[i]和b[j]之间的平方和距离
    # 实现见:https://blog.csdn.net/frankzd/article/details/80251042
    a, b = np.asarray(a), np.asarray(b)  # 拷贝一份数据
    if len(a) == 0 or len(b) == 0:
        return np.zeros((len(a), len(b)))
    a2, b2 = np.square(a).sum(axis=1), np.square(
        b).sum(axis=1)  # 求每个embedding的平方和
    # sum(N) + sum(L) -2 x [NxM]x[MxL] = [NxL]
    r2 = -2. * np.dot(a, b.T) + a2[:, None] + b2[None, :]
    r2 = np.clip(r2, 0., float(np.inf))
    return r2

图源自csdn博客

  1. 计算余弦距离
def _cosine_distance(a, b, data_is_normalized=False):
    # a和b之间的余弦距离
    # a : [NxM] b : [LxM]
    # 余弦距离 = 1 - 余弦相似度
    # https://blog.csdn.net/u013749540/article/details/51813922
    if not data_is_normalized:
        # 需要将余弦相似度转化成类似欧氏距离的余弦距离。
        a = np.asarray(a) / np.linalg.norm(a, axis=1, keepdims=True)
        #  np.linalg.norm 操作是求向量的范式,默认是L2范式,等同于求向量的欧式距离。
        b = np.asarray(b) / np.linalg.norm(b, axis=1, keepdims=True)
    return 1. - np.dot(a, b.T)

图源csdn博客

以上代码对应公式,注意余弦距离=1-余弦相似度

最近邻距离度量类
class NearestNeighborDistanceMetric(object):
    # 对于每个目标,返回一个最近的距离
    def __init__(self, metric, matching_threshold, budget=None):
        # 默认matching_threshold = 0.2 budge = 100
        if metric == "euclidean":
            # 使用最近邻欧氏距离
            self._metric = _nn_euclidean_distance
        elif metric == "cosine":
            # 使用最近邻余弦距离
            self._metric = _nn_cosine_distance
        else:
            raise ValueError("Invalid metric; must be either 'euclidean' or 'cosine'")

        self.matching_threshold = matching_threshold
        # 在级联匹配的函数中调用
        self.budget = budget
        # budge 预算,控制feature的多少
        self.samples = {}
        # samples是一个字典{id->feature list}

    def partial_fit(self, features, targets, active_targets):
        # 作用:部分拟合,用新的数据更新测量距离
        # 调用:在特征集更新模块部分调用,tracker.update()中
        for feature, target in zip(features, targets):
            self.samples.setdefault(target, []).append(feature)
            # 对应目标下添加新的feature,更新feature集合
            # 目标id  :  feature list
            if self.budget is not None:
                self.samples[target] = self.samples[target][-self.budget:]
            # 设置预算,每个类最多多少个目标,超过直接忽略

        # 筛选激活的目标
        self.samples = {k: self.samples[k] for k in active_targets}

    def distance(self, features, targets):
        # 作用:比较feature和targets之间的距离,返回一个代价矩阵
        # 调用:在匹配阶段,将distance封装为gated_metric,
        #       进行外观信息(reid得到的深度特征)+
        #       运动信息(马氏距离用于度量两个分布相似程度)
        cost_matrix = np.zeros((len(targets), len(features)))
        for i, target in enumerate(targets):
            cost_matrix[i, :] = self._metric(self.samples[target], features)
        return cost_matrix
Tracker类

Tracker类是最核心的类,Tracker中保存了所有的轨迹信息,负责初始化第一帧的轨迹、卡尔曼滤波的预测和更新、负责级联匹配、IOU匹配等等核心工作。

class Tracker:
    # 是一个多目标tracker,保存了很多个track轨迹
    # 负责调用卡尔曼滤波来预测track的新状态+进行匹配工作+初始化第一帧
    # Tracker调用update或predict的时候,其中的每个track也会各自调用自己的update或predict
    """
    This is the multi-target tracker.
    """

    def __init__(self, metric, max_iou_distance=0.7, max_age=70, n_init=3):
        # 调用的时候,后边的参数全部是默认的
        self.metric = metric 
        # metric是一个类,用于计算距离(余弦距离或马氏距离)
        self.max_iou_distance = max_iou_distance
        # 最大iou,iou匹配的时候使用
        self.max_age = max_age
        # 直接指定级联匹配的cascade_depth参数
        self.n_init = n_init
        # n_init代表需要n_init次数的update才会将track状态设置为confirmed

        self.kf = kalman_filter.KalmanFilter()# 卡尔曼滤波器
        self.tracks = [] # 保存一系列轨迹
        self._next_id = 1 # 下一个分配的轨迹id
	def predict(self):
        # 遍历每个track都进行一次预测
        """Propagate track state distributions one time step forward.

        This function should be called once every time step, before `update`.
        """
        for track in self.tracks:
            track.predict(self.kf)

然后来看最核心的update函数和match函数,可以对照下面的流程图一起看:

update函数

def update(self, detections):
    # 进行测量的更新和轨迹管理
    """Perform measurement update and track management.

    Parameters
    ----------
    detections : List[deep_sort.detection.Detection]
        A list of detections at the current time step.

    """
    # Run matching cascade.
    matches, unmatched_tracks, unmatched_detections = \
        self._match(detections)

    # Update track set.
    # 1. 针对匹配上的结果
    for track_idx, detection_idx in matches:
        # track更新对应的detection
        self.tracks[track_idx].update(self.kf, detections[detection_idx])

    # 2. 针对未匹配的tracker,调用mark_missed标记
    # track失配,若待定则删除,若update时间很久也删除
    # max age是一个存活期限,默认为70帧
    for track_idx in unmatched_tracks:
        self.tracks[track_idx].mark_missed()

    # 3. 针对未匹配的detection, detection失配,进行初始化
    for detection_idx in unmatched_detections:
        self._initiate_track(detections[detection_idx])

    # 得到最新的tracks列表,保存的是标记为confirmed和Tentative的track
    self.tracks = [t for t in self.tracks if not t.is_deleted()]

    # Update distance metric.
    active_targets = [t.track_id for t in self.tracks if t.is_confirmed()]
    # 获取所有confirmed状态的track id
    features, targets = [], []
    for track in self.tracks:
        if not track.is_confirmed():
            continue
        features += track.features  # 将tracks列表拼接到features列表
        # 获取每个feature对应的track id
        targets += [track.track_id for _ in track.features]
        track.features = []

    # 距离度量中的 特征集更新
    self.metric.partial_fit(np.asarray(features), np.asarray(targets),
                            active_targets)

match函数:

def _match(self, detections):
    # 主要功能是进行匹配,找到匹配的,未匹配的部分
    def gated_metric(tracks, dets, track_indices, detection_indices):
        # 功能: 用于计算track和detection之间的距离,代价函数
        #        需要使用在KM算法之前
        # 调用:
        # cost_matrix = distance_metric(tracks, detections,
        #                  track_indices, detection_indices)
        features = np.array([dets[i].feature for i in detection_indices])
        targets = np.array([tracks[i].track_id for i in track_indices])

        # 1. 通过最近邻计算出代价矩阵 cosine distance
        cost_matrix = self.metric.distance(features, targets)
        # 2. 计算马氏距离,得到新的状态矩阵
        cost_matrix = linear_assignment.gate_cost_matrix(
            self.kf, cost_matrix, tracks, dets, track_indices,
            detection_indices)
        return cost_matrix

    # Split track set into confirmed and unconfirmed tracks.
    # 划分不同轨迹的状态
    confirmed_tracks = [
        i for i, t in enumerate(self.tracks) if t.is_confirmed()
    ]
    unconfirmed_tracks = [
        i for i, t in enumerate(self.tracks) if not t.is_confirmed()
    ]

    # 进行级联匹配,得到匹配的track、不匹配的track、不匹配的detection
    '''
    !!!!!!!!!!!
    级联匹配
    !!!!!!!!!!!
    '''
    # gated_metric->cosine distance
    # 仅仅对确定态的轨迹进行级联匹配
    matches_a, unmatched_tracks_a, unmatched_detections = \
        linear_assignment.matching_cascade(
            gated_metric,
            self.metric.matching_threshold,
            self.max_age,
            self.tracks,
            detections,
            confirmed_tracks)

    # 将所有状态为未确定态的轨迹和刚刚没有匹配上的轨迹组合为iou_track_candidates,
    # 进行IoU的匹配
    iou_track_candidates = unconfirmed_tracks + [
        k for k in unmatched_tracks_a
        if self.tracks[k].time_since_update == 1  # 刚刚没有匹配上
    ]
    # 未匹配
    unmatched_tracks_a = [
        k for k in unmatched_tracks_a
        if self.tracks[k].time_since_update != 1  # 已经很久没有匹配上
    ]

    '''
    !!!!!!!!!!!
    IOU 匹配
    对级联匹配中还没有匹配成功的目标再进行IoU匹配
    !!!!!!!!!!!
    '''
    # 虽然和级联匹配中使用的都是min_cost_matching作为核心,
    # 这里使用的metric是iou cost和以上不同
    matches_b, unmatched_tracks_b, unmatched_detections = \
        linear_assignment.min_cost_matching(
            iou_matching.iou_cost,
            self.max_iou_distance,
            self.tracks,
            detections,
            iou_track_candidates,
            unmatched_detections)

    matches = matches_a + matches_b  # 组合两部分match得到的结果

    unmatched_tracks = list(set(unmatched_tracks_a + unmatched_tracks_b))
    return matches, unmatched_tracks, unmatched_detections

以上两部分结合注释和以下流程图可以更容易理解。

图片来自知乎Harlek

级联匹配

下边是论文中给出的级联匹配的伪代码:

论文中的级联匹配的伪代码

以下代码是伪代码对应的实现

# 1. 分配track_indices和detection_indices
if track_indices is None:
    track_indices = list(range(len(tracks)))

if detection_indices is None:
    detection_indices = list(range(len(detections)))

unmatched_detections = detection_indices

matches = []
# cascade depth = max age 默认为70
for level in range(cascade_depth):
    if len(unmatched_detections) == 0:  # No detections left
        break

    track_indices_l = [
        k for k in track_indices
        if tracks[k].time_since_update == 1 + level
    ]
    if len(track_indices_l) == 0:  # Nothing to match at this level
        continue

    # 2. 级联匹配核心内容就是这个函数
    matches_l, _, unmatched_detections = \
        min_cost_matching(  # max_distance=0.2
            distance_metric, max_distance, tracks, detections,
            track_indices_l, unmatched_detections)
    matches += matches_l
unmatched_tracks = list(set(track_indices) - set(k for k, _ in matches))
门控矩阵

门控矩阵的作用就是通过计算卡尔曼滤波的状态分布和测量值之间的距离对代价矩阵进行限制。

代价矩阵中的距离是Track和Detection之间的表观相似度,假如一个轨迹要去匹配两个表观特征非常相似的Detection,这样就很容易出错,但是这个时候分别让两个Detection计算与这个轨迹的马氏距离,并使用一个阈值gating_threshold进行限制,所以就可以将马氏距离较远的那个Detection区分开,可以降低错误的匹配。

def gate_cost_matrix(
        kf, cost_matrix, tracks, detections, track_indices, detection_indices,
        gated_cost=INFTY_COST, only_position=False):
    # 根据通过卡尔曼滤波获得的状态分布,使成本矩阵中的不可行条目无效。
    gating_dim = 2 if only_position else 4
    gating_threshold = kalman_filter.chi2inv95[gating_dim]  # 9.4877

    measurements = np.asarray([detections[i].to_xyah()
                               for i in detection_indices])
    for row, track_idx in enumerate(track_indices):
        track = tracks[track_idx]
        gating_distance = kf.gating_distance(
            track.mean, track.covariance, measurements, only_position)
        cost_matrix[row, gating_distance >
                    gating_threshold] = gated_cost  # 设置为inf
    return cost_matrix
卡尔曼滤波器

在Deep SORT中,需要估计Track的以下状态:

  • 均值:用8维向量(x, y, a, h, vx, vy, va, vh)表示。(x,y)是框的中心坐标,宽高比是a, 高度h以及对应的速度,所有的速度都将初始化为0。
  • 协方差:表示目标位置信息的不确定程度,用8x8的对角矩阵来表示,矩阵对应的值越大,代表不确定程度越高。

下图代表卡尔曼滤波器主要过程:

DeepSORT: Deep Learning to Track Custom Objects in a Video

  1. 卡尔曼滤波首先根据当前帧(time=t)的状态进行预测,得到预测下一帧的状态(time=t+1)
  2. 得到测量结果,在Deep SORT中对应的测量就是Detection,即目标检测器提供的检测框。
  3. 将预测结果和测量结果进行更新

下面这部分主要参考: https://zhuanlan.zhihu.com/p/90835266

如果对卡尔曼滤波算法有较为深入的了解,可以结合卡尔曼滤波算法和代码进行理解。

预测分两个公式:

第一个公式:

x ′ = F x x'=Fx x=Fx

其中F是状态转移矩阵,如下图:

图源知乎@求索

第二个公式:

P ′ = F P F T + Q P'=FPF^T+Q P=FPFT+Q

P是当前帧(time=t)的协方差,Q是卡尔曼滤波器的运动估计误差,代表不确定程度。

def predict(self, mean, covariance):
    # 相当于得到t时刻估计值
    # Q 预测过程中噪声协方差
    std_pos = [
        self._std_weight_position * mean[3],
        self._std_weight_position * mean[3],
        1e-2,
        self._std_weight_position * mean[3]]

    std_vel = [
        self._std_weight_velocity * mean[3],
        self._std_weight_velocity * mean[3],
        1e-5,
        self._std_weight_velocity * mean[3]]

    # np.r_ 按列连接两个矩阵
    # 初始化噪声矩阵Q
    motion_cov = np.diag(np.square(np.r_[std_pos, std_vel]))

    # x' = Fx
    mean = np.dot(self._motion_mat, mean)

    # P' = FPF^T+Q
    covariance = np.linalg.multi_dot((
        self._motion_mat, covariance, self._motion_mat.T)) + motion_cov

    return mean, covariance

更新的公式
y = z − H x ′ y=z-Hx' \\ y=zHx

S = H P ′ H T + R S=HP'H^T+R \\ S=HPHT+R

K = P ′ H T S − 1 K=P'H^TS^{-1} \\ K=PHTS1

x = x ′ + K y x=x'+Ky \\ x=x+Ky

P = ( I − K H ) P ′ P=(I-KH)P' P=(IKH)P

def project(self, mean, covariance):
    # R 测量过程中噪声的协方差
    std = [
        self._std_weight_position * mean[3],
        self._std_weight_position * mean[3],
        1e-1,
        self._std_weight_position * mean[3]]

    # 初始化噪声矩阵R
    innovation_cov = np.diag(np.square(std))

    # 将均值向量映射到检测空间,即Hx'
    mean = np.dot(self._update_mat, mean)

    # 将协方差矩阵映射到检测空间,即HP'H^T
    covariance = np.linalg.multi_dot((
        self._update_mat, covariance, self._update_mat.T))

    return mean, covariance + innovation_cov

def update(self, mean, covariance, measurement):
    # 通过估计值和观测值估计最新结果

    # 将均值和协方差映射到检测空间,得到 Hx' 和 S
    projected_mean, projected_cov = self.project(mean, covariance)

    # 矩阵分解
    chol_factor, lower = scipy.linalg.cho_factor(
        projected_cov, lower=True, check_finite=False)

    # 计算卡尔曼增益K
    kalman_gain = scipy.linalg.cho_solve(
        (chol_factor, lower), np.dot(covariance, self._update_mat.T).T,
        check_finite=False).T

    # z - Hx'
    innovation = measurement - projected_mean

    # x = x' + Ky
    new_mean = mean + np.dot(innovation, kalman_gain.T)

    # P = (I - KH)P'
    new_covariance = covariance - np.linalg.multi_dot((
        kalman_gain, projected_cov, kalman_gain.T))
    return new_mean, new_covariance

y = z − H x ′ y=z-Hx' y=zHx

这个公式中,z是Detection的mean,不包含变化值,状态为[cx,cy,a,h]。H是测量矩阵,将Track的均值向量 x ′ x' x映射到检测空间。计算的y是Detection和Track的均值误差。
S = H P ′ H T + R S=HP'H^T+R S=HPHT+R
R是目标检测器的噪声矩阵,是一个4x4的对角矩阵。 对角线上的值分别为中心点两个坐标以及宽高的噪声。
K = P ′ H T S − 1 K=P'H^TS^{-1} K=PHTS1
计算的是卡尔曼增益,是作用于衡量估计误差的权重。
x = x ′ + K y x=x'+Ky x=x+Ky
更新后的均值向量x。
P = ( I − K H ) P ′ P=(I-KH)P' P=(IKH)P
更新后的协方差矩阵。

卡尔曼滤波笔者理解也不是很深入,没有推导过公式,对这部分感兴趣的推荐几个博客:

  1. 卡尔曼滤波+python写的demo: https://zhuanlan.zhihu.com/p/113685503?utm_source=wechat_session&utm_medium=social&utm_oi=801414067897135104
  2. 详解+推导: https://blog.csdn.net/honyniu/article/details/88697520

5. 流程解析

流程部分主要按照以下流程图来走一遍:

知乎@猫弟总结的deep sort流程图

感谢知乎@猫弟总结的流程图,讲解非常地清晰,如果单纯看代码,非常容易混淆。比如说代价矩阵的计算这部分,连续套了三个函数,才被真正调用。上图将整体流程总结地非常棒。笔者将参考以上流程结合代码进行梳理:

  1. 分析detector类中的Deep SORT调用:
class Detector(object):
    def __init__(self, args):
        self.args = args
        if args.display:
            cv2.namedWindow("test", cv2.WINDOW_NORMAL)
            cv2.resizeWindow("test", args.display_width, args.display_height)

        device = torch.device(
            'cuda') if torch.cuda.is_available() else torch.device('cpu')

        self.vdo = cv2.VideoCapture()
        self.yolo3 = InferYOLOv3(args.yolo_cfg,
                                 args.img_size,
                                 args.yolo_weights,
                                 args.data_cfg,
                                 device,
                                 conf_thres=args.conf_thresh,
                                 nms_thres=args.nms_thresh)
        self.deepsort = DeepSort(args.deepsort_checkpoint)

初始化DeepSORT对象,更新部分接收目标检测得到的框的位置,置信度和图片:

outputs = self.deepsort.update(bbox_xcycwh, cls_conf, im)
  1. 顺着DeepSORT类的update函数看
class DeepSort(object):
    def __init__(self, model_path, max_dist=0.2):
        self.min_confidence = 0.3
        # yolov3中检测结果置信度阈值,筛选置信度小于0.3的detection。

        self.nms_max_overlap = 1.0
        # 非极大抑制阈值,设置为1代表不进行抑制

        # 用于提取图片的embedding,返回的是一个batch图片对应的特征
        self.extractor = Extractor("resnet18",
                                   model_path,
                                   use_cuda=True)

        max_cosine_distance = max_dist
        # 用在级联匹配的地方,如果大于改阈值,就直接忽略
        nn_budget = 100
        # 预算,每个类别最多的样本个数,如果超过,删除旧的

        # 第一个参数可选'cosine' or 'euclidean'
        metric = NearestNeighborDistanceMetric("cosine",
                                               max_cosine_distance,
                                               nn_budget)
        self.tracker = Tracker(metric)

    def update(self, bbox_xywh, confidences, ori_img):
        self.height, self.width = ori_img.shape[:2]
        # generate detections
        features = self._get_features(bbox_xywh, ori_img)
        # 从原图中crop bbox对应图片并计算得到embedding
        bbox_tlwh = self._xywh_to_tlwh(bbox_xywh)

        detections = [
            Detection(bbox_tlwh[i], conf, features[i])
            for i, conf in enumerate(confidences) if conf > self.min_confidence
        ]  # 筛选小于min_confidence的目标,并构造一个Detection对象构成的列表
        # Detection是一个存储图中一个bbox结果
        # 需要:1. bbox(tlwh形式) 2. 对应置信度 3. 对应embedding

        # run on non-maximum supression
        boxes = np.array([d.tlwh for d in detections])
        scores = np.array([d.confidence for d in detections])

        # 使用非极大抑制
        # 默认nms_thres=1的时候开启也没有用,实际上并没有进行非极大抑制
        indices = non_max_suppression(boxes, self.nms_max_overlap, scores)
        detections = [detections[i] for i in indices]

        # update tracker
        # tracker给出一个预测结果,然后将detection传入,进行卡尔曼滤波操作
        self.tracker.predict()
        self.tracker.update(detections)

        # output bbox identities
        # 存储结果以及可视化
        outputs = []
        for track in self.tracker.tracks:
            if not track.is_confirmed() or track.time_since_update > 1:
                continue
            box = track.to_tlwh()
            x1, y1, x2, y2 = self._tlwh_to_xyxy(box)
            track_id = track.track_id
            outputs.append(np.array([x1, y1, x2, y2, track_id], dtype=np.int))

        if len(outputs) > 0:
            outputs = np.stack(outputs, axis=0)
        return np.array(outputs)

从这里开始对照以上流程图会更加清晰。在Deep SORT初始化的过程中有一个核心metric,NearestNeighborDistanceMetric类会在匹配和特征集更新的时候用到。

梳理DeepSORT的update流程:

  • 根据传入的参数(bbox_xywh, conf, img)使用ReID模型提取对应bbox的表观特征。

  • 构建detections的列表,列表中的内容就是Detection类,在此处限制了bbox的最小置信度。

  • 使用非极大抑制算法,由于默认nms_thres=1,实际上并没有用。

  • Tracker类进行一次预测,然后将detections传入,进行更新。

  • 最后将Tracker中保存的轨迹中状态属于确认态的轨迹返回。

以上核心在Tracker的predict和update函数,接着梳理。

  1. Tracker的predict函数

Tracker是一个多目标跟踪器,保存了很多个track轨迹,负责调用卡尔曼滤波来预测track的新状态+进行匹配工作+初始化第一帧。Tracker调用update或predict的时候,其中的每个track也会各自调用自己的update或predict

class Tracker:
    def __init__(self, metric, max_iou_distance=0.7, max_age=70, n_init=3):
        # 调用的时候,后边的参数全部是默认的
        self.metric = metric
        self.max_iou_distance = max_iou_distance
        # 最大iou,iou匹配的时候使用
        self.max_age = max_age
        # 直接指定级联匹配的cascade_depth参数
        self.n_init = n_init
        # n_init代表需要n_init次数的update才会将track状态设置为confirmed

        self.kf = kalman_filter.KalmanFilter()  # 卡尔曼滤波器
        self.tracks = []  # 保存一系列轨迹
        self._next_id = 1  # 下一个分配的轨迹id

    def predict(self):
        # 遍历每个track都进行一次预测
        """Propagate track state distributions one time step forward.
        This function should be called once every time step, before `update`.
        """
        for track in self.tracks:
            track.predict(self.kf)

predict主要是对轨迹列表中所有的轨迹使用卡尔曼滤波算法进行状态的预测。

  1. Tracker的更新

Tracker的更新属于最核心的部分。

    def update(self, detections):
        # 进行测量的更新和轨迹管理
        """Perform measurement update and track management.

        Parameters
        ----------
        detections : List[deep_sort.detection.Detection]
            A list of detections at the current time step.

        """
        # Run matching cascade.
        matches, unmatched_tracks, unmatched_detections = \
            self._match(detections)

        # Update track set.
        # 1. 针对匹配上的结果
        for track_idx, detection_idx in matches:
            # track更新对应的detection
            self.tracks[track_idx].update(self.kf, detections[detection_idx])

        # 2. 针对未匹配的tracker,调用mark_missed标记
        # track失配,若待定则删除,若update时间很久也删除
        # max age是一个存活期限,默认为70帧
        for track_idx in unmatched_tracks:
            self.tracks[track_idx].mark_missed()

        # 3. 针对未匹配的detection, detection失配,进行初始化
        for detection_idx in unmatched_detections:
            self._initiate_track(detections[detection_idx])

        # 得到最新的tracks列表,保存的是标记为confirmed和Tentative的track
        self.tracks = [t for t in self.tracks if not t.is_deleted()]

        # Update distance metric.
        active_targets = [t.track_id for t in self.tracks if t.is_confirmed()]
        # 获取所有confirmed状态的track id
        features, targets = [], []
        for track in self.tracks:
            if not track.is_confirmed():
                continue
            features += track.features  # 将tracks列表拼接到features列表
            # 获取每个feature对应的track id
            targets += [track.track_id for _ in track.features]
            track.features = []

        # 距离度量中的 特征集更新
        self.metric.partial_fit(np.asarray(features), np.asarray(targets),active_targets)

这部分注释已经很详细了,主要是一些后处理代码,需要关注的是对匹配上的,未匹配的Detection,未匹配的Track三者进行的处理以及最后进行特征集更新部分,可以对照流程图梳理。

Tracker的update函数的核心函数是match函数,描述如何进行匹配的流程:

def _match(self, detections):
    # 主要功能是进行匹配,找到匹配的,未匹配的部分
    def gated_metric(tracks, dets, track_indices, detection_indices):
        # 功能: 用于计算track和detection之间的距离,代价函数
        #        需要使用在KM算法之前
        # 调用:
        # cost_matrix = distance_metric(tracks, detections,
        #                  track_indices, detection_indices)
        features = np.array([dets[i].feature for i in detection_indices])
        targets = np.array([tracks[i].track_id for i in track_indices])

        # 1. 通过最近邻计算出代价矩阵 cosine distance
        cost_matrix = self.metric.distance(features, targets)

        # 2. 计算马氏距离,得到新的状态矩阵
        cost_matrix = linear_assignment.gate_cost_matrix(
            self.kf, cost_matrix, tracks, dets, track_indices,
            detection_indices)
        return cost_matrix

    # Split track set into confirmed and unconfirmed tracks.
    # 划分不同轨迹的状态
    confirmed_tracks = [
        i for i, t in enumerate(self.tracks) if t.is_confirmed()
    ]
    unconfirmed_tracks = [
        i for i, t in enumerate(self.tracks) if not t.is_confirmed()
    ]

    # 进行级联匹配,得到匹配的track、不匹配的track、不匹配的detection
    '''
    !!!!!!!!!!!
    级联匹配
    !!!!!!!!!!!
    '''
    # gated_metric->cosine distance
    # 仅仅对确定态的轨迹进行级联匹配
    matches_a, unmatched_tracks_a, unmatched_detections = \
        linear_assignment.matching_cascade(
            gated_metric,
            self.metric.matching_threshold,
            self.max_age,
            self.tracks,
            detections,
            confirmed_tracks)

    # 将所有状态为未确定态的轨迹和刚刚没有匹配上的轨迹组合为iou_track_candidates,
    # 进行IoU的匹配
    iou_track_candidates = unconfirmed_tracks + [
        k for k in unmatched_tracks_a
        if self.tracks[k].time_since_update == 1  # 刚刚没有匹配上
    ]
    # 未匹配
    unmatched_tracks_a = [
        k for k in unmatched_tracks_a
        if self.tracks[k].time_since_update != 1  # 已经很久没有匹配上
    ]

    '''
    !!!!!!!!!!!
    IOU 匹配
    对级联匹配中还没有匹配成功的目标再进行IoU匹配
    !!!!!!!!!!!
    '''
    # 虽然和级联匹配中使用的都是min_cost_matching作为核心,
    # 这里使用的metric是iou cost和以上不同
    matches_b, unmatched_tracks_b, unmatched_detections = \
        linear_assignment.min_cost_matching(
            iou_matching.iou_cost,
            self.max_iou_distance,
            self.tracks,
            detections,
            iou_track_candidates,
            unmatched_detections)

    matches = matches_a + matches_b  # 组合两部分match得到的结果

    unmatched_tracks = list(set(unmatched_tracks_a + unmatched_tracks_b))
    return matches, unmatched_tracks, unmatched_detections

对照下图来看会顺畅很多:

图片来自知乎Harlek

可以看到,匹配函数的核心是级联匹配+IOU匹配,先来看看级联匹配:

调用在这里:

matches_a, unmatched_tracks_a, unmatched_detections = \
    linear_assignment.matching_cascade(
        gated_metric,
        self.metric.matching_threshold,
        self.max_age,
        self.tracks,
        detections,
        confirmed_tracks)

级联匹配函数展开:

def matching_cascade(
        distance_metric, max_distance, cascade_depth, tracks, detections,
        track_indices=None, detection_indices=None):
    # 级联匹配

    # 1. 分配track_indices和detection_indices
    if track_indices is None:
        track_indices = list(range(len(tracks)))

    if detection_indices is None:
        detection_indices = list(range(len(detections)))

    unmatched_detections = detection_indices

    matches = []
    # cascade depth = max age 默认为70
    for level in range(cascade_depth):
        if len(unmatched_detections) == 0:  # No detections left
            break

        track_indices_l = [
            k for k in track_indices
            if tracks[k].time_since_update == 1 + level
        ]
        if len(track_indices_l) == 0:  # Nothing to match at this level
            continue

        # 2. 级联匹配核心内容就是这个函数
        matches_l, _, unmatched_detections = \
            min_cost_matching(  # max_distance=0.2
                distance_metric, max_distance, tracks, detections,
                track_indices_l, unmatched_detections)
        matches += matches_l
    unmatched_tracks = list(set(track_indices) - set(k for k, _ in matches))
    return matches, unmatched_tracks, unmatched_detections

可以看到和伪代码是一致的,文章上半部分也有提到这部分代码。这部分代码中还有一个核心的函数min_cost_matching,这个函数可以接收不同的distance_metric,在级联匹配和IoU匹配中都有用到。

min_cost_matching函数:

def min_cost_matching(
        distance_metric, max_distance, tracks, detections, track_indices=None,
        detection_indices=None):
  
    if track_indices is None:
        track_indices = np.arange(len(tracks))
    if detection_indices is None:
        detection_indices = np.arange(len(detections))

    if len(detection_indices) == 0 or len(track_indices) == 0:
        return [], track_indices, detection_indices  # Nothing to match.
    # -----------------------------------------
    # Gated_distance——>
    #       1. cosine distance
    #       2. 马氏距离
    # 得到代价矩阵
    # -----------------------------------------
    # iou_cost——>
    #       仅仅计算track和detection之间的iou距离
    # -----------------------------------------
    cost_matrix = distance_metric(
        tracks, detections, track_indices, detection_indices)
    # -----------------------------------------
    # gated_distance中设置距离中最高上限,
    # 这里最远距离实际是在deep sort类中的max_dist参数设置的
    # 默认max_dist=0.2, 距离越小越好
    # -----------------------------------------
    # iou_cost情况下,max_distance的设置对应tracker中的max_iou_distance,
    # 默认值为max_iou_distance=0.7
    # 注意结果是1-iou,所以越小越好
    # -----------------------------------------
    cost_matrix[cost_matrix > max_distance] = max_distance + 1e-5

    # 匈牙利算法或者KM算法
    row_indices, col_indices = linear_assignment(cost_matrix)

    matches, unmatched_tracks, unmatched_detections = [], [], []

    # 这几个for循环用于对匹配结果进行筛选,得到匹配和未匹配的结果
    for col, detection_idx in enumerate(detection_indices):
        if col not in col_indices:
            unmatched_detections.append(detection_idx)

    for row, track_idx in enumerate(track_indices):
        if row not in row_indices:
            unmatched_tracks.append(track_idx)

    for row, col in zip(row_indices, col_indices):
        track_idx = track_indices[row]
        detection_idx = detection_indices[col]
        if cost_matrix[row, col] > max_distance:
            unmatched_tracks.append(track_idx)
            unmatched_detections.append(detection_idx)
        else:
            matches.append((track_idx, detection_idx))
    # 得到匹配,未匹配轨迹,未匹配检测
    return matches, unmatched_tracks, unmatched_detections

注释中提到distance_metric是有两个的:

  • 第一个是级联匹配中传入的distance_metric是gated_metric, 其内部核心是计算的表观特征的级联匹配。
def gated_metric(tracks, dets, track_indices, detection_indices):
    # 功能: 用于计算track和detection之间的距离,代价函数
    #        需要使用在KM算法之前
    # 调用:
    # cost_matrix = distance_metric(tracks, detections,
    #                  track_indices, detection_indices)
    features = np.array([dets[i].feature for i in detection_indices])
    targets = np.array([tracks[i].track_id for i in track_indices])

    # 1. 通过最近邻计算出代价矩阵 cosine distance
    cost_matrix = self.metric.distance(features, targets)

    # 2. 计算马氏距离,得到新的状态矩阵
    cost_matrix = linear_assignment.gate_cost_matrix(
        self.kf, cost_matrix, tracks, dets, track_indices,
        detection_indices)
    return cost_matrix

对应下图进行理解(下图上半部分就是对应的gated_metric函数):

图片来自知乎Harlek

  • 第二个是IOU匹配中的iou_matching.iou_cost:
# 虽然和级联匹配中使用的都是min_cost_matching作为核心,
# 这里使用的metric是iou cost和以上不同
matches_b, unmatched_tracks_b, unmatched_detections = \
    linear_assignment.min_cost_matching(
        iou_matching.iou_cost,
        self.max_iou_distance,
        self.tracks,
        detections,
        iou_track_candidates,
        unmatched_detections)

iou_cost代价很容易理解,用于计算Track和Detection之间的IOU距离矩阵。

def iou_cost(tracks, detections, track_indices=None,
             detection_indices=None):
    # 计算track和detection之间的iou距离矩阵

    if track_indices is None:
        track_indices = np.arange(len(tracks))
    if detection_indices is None:
        detection_indices = np.arange(len(detections))

    cost_matrix = np.zeros((len(track_indices), len(detection_indices)))
    for row, track_idx in enumerate(track_indices):
        if tracks[track_idx].time_since_update > 1:
            cost_matrix[row, :] = linear_assignment.INFTY_COST
            continue

        bbox = tracks[track_idx].to_tlwh()
        candidates = np.asarray(
            [detections[i].tlwh for i in detection_indices])
        cost_matrix[row, :] = 1. - iou(bbox, candidates)
    return cost_matrix

6. 总结

以上就是Deep SORT算法代码部分的解析,核心在于类图和流程图,理解Deep SORT实现的过程。

如果第一次接触到多目标跟踪算法领域的,可以到知乎上看这篇文章以及其系列,对新手非常友好: https://zhuanlan.zhihu.com/p/62827974

笔者也收集了一些多目标跟踪领域中认可度比较高、常见的库,在这里分享给大家:

  • SORT官方代码: https://github.com/abewley/sort

  • DeepSORT官方代码: https://github.com/nwojke/deep_sort

  • 奇点大佬keras实现DeepSORT: https://github.com/Qidian213/deep_sort_yolov3

  • CenterNet作检测器的DeepSORT: https://github.com/xingyizhou/CenterTrack 和 https://github.com/kimyoon-young/centerNet-deep-sort

  • JDE Github地址: https://github.com/Zhongdao/Towards-Realtime-MOT

  • FairMOT Github地址: https://github.com/ifzhang/FairMOT

  • 笔者修改的代码: https://github.com/pprp/deep_sort_yolov3_pytorch

笔者也是最近一段时间接触目标跟踪领域,数学水平非常有限(卡尔曼滤波只能肤浅了解大概过程,但是还不会推导)。本文目标就是帮助新入门多目标跟踪的新人快速了解Deep SORT流程,由于自身水平有限,也欢迎大佬对文中不足之处进行指点一二。

7. 参考

https://arxiv.org/abs/1703.07402

https://github.com/pprp/deep_sort_yolov3_pytorch

https://www.cnblogs.com/yanwei-li/p/8643446.html

https://zhuanlan.zhihu.com/p/97449724

https://zhuanlan.zhihu.com/p/80764724

https://zhuanlan.zhihu.com/p/90835266

https://zhuanlan.zhihu.com/p/113685503

Deep-Sort多目标追踪算法代码解析
qq_33287871的博客
05-07 1万+
Deep SORT是多目标跟踪(Multi-Object Tracking)中常用到的一种算法,是一个Detection Based Tracking的方法。这个算法工业界关注度非常高,在知乎上有很多文章都是使用了Deep SORT进行工程部署。 1. MOT主要步骤 在《DEEP LEARNING IN VIDEO MULTI-OBJECT TRACKING: A SURVEY》这篇基于深度学习...
目标跟踪算法 | DeepSort
黎国溥
02-09 3万+
前言 论文名称:(ICIP2017)Single-Simple Online and Realtime Tracking with a Deep Association Metric 论文地址:https://arxiv.org/abs/1703.07402 开源地址:https://github.com/nwojke/deep_sort 一、多目标跟踪的工作流程(常规) (1)给定视频的原始帧; (2)运行对象检测器以获得对象的边界框; (3)对于每个检测到的物体,计算出不同的特征.
目标跟踪(MOT)--DeepSort原理及代码详解
Gthan
06-07 9598
对多目标跟踪(MOT)进行简要概述同时对其中DeepSort算法的总体框架、流程及各模块的实现原理、方法和代码复现进行了详细的讲解
一文详解DeepSort多目标追踪算法——原理篇
最新发布
AAI666666的博客
02-22 3040
今儿给小伙伴带来一项在计算机视觉中相对较新的技术——目标追踪。说起目标追踪,大家应该都不陌生叭,它可用于跟踪监控摄像头中的人、车辆、行李或其他物体,这在大型商场、机场、银行、交通枢纽和街头巷尾的cctv摄像头中广泛使用。
ssd_deepsort.zip
04-20
网上大多代码为yolo3+deepsort,我自己稍微改了一下,改成mxnet的ssd+deepsort,通过学习deepsort才知道原来检测与deepsort就像两块积木一样,拼接在一起就好了
基于track.py文件修改的DeepSORT的ID switch后处理代码,另在nn_matching.py文件中进行了粗略修
07-17
def distance(self, features, targets): cost_matrix = np.zeros((len(targets), len(features))) key_list = list(self.samples.keys()) for i, target_k in enumerate(targets): target = key_list[i] cost_matrix[i, :] = self._metric(self.samples[target], features) return cost_matrix
DeepSORT算法流程分析.md
05-29
根据Deep SORT代码进行算法流程分析,通过列举了前4 帧的跟踪流程,对每一帧各种结果的可能性进行了分析,便于研究多目标跟踪方向的道友们更好的理解代码流程。本人也是初学者,若有解释不到位或者借鉴不当之处,欢迎联系指正!
deep_sort_pytorch:使用Deepsort和yolov3与pytorch进行MOT跟踪
04-28
使用PyTorch进行深度排序 更新(1-1-2020) 变化 修正错误 重构代码 通过在gpu上添加nms来进行准确检测 最新更新(07-22) 变化 错误修复(感谢@ JieChen91和@ yingsen1进行错误报告)。 使用批处理为每个帧提取特征,这会导致速度提速。 代码改进。 进一步的改进方向 在特定数据集而不是官方数据集上训练检测器。 在pedestrain数据集上重新训练REID模型以获得更好的性能。 将YOLOv3检测器替换为高级检测器。 欢迎对此存储库做出任何贡献! 介绍 这是MOT跟踪算法深度排序的一种实现。 深度排序与排序基本相同,但深度CNN模型添加了CNN模型以提取受检测器限制的人体部位图像中的特征。 这个CNN模型确实是一个RE-ID模型, 使用的检测器是FasterRCNN,原始源代码是 。 但是,在原始代码中,CNN模型是使用tensorf
跟踪算法-Deep sort简介
qq_44936246的博客
10-15 1万+
目录跟踪的基本思想跟踪框与检测框卡尔曼滤波算法---预测匈牙利算法----匹配 对于目标跟踪,前提是能够对单张图片中的车辆进行检测,从而知道图片中车辆的位置,根据连续的图像中目标位置的轨迹预测,从而来实现跟踪。 跟踪的基本思想 如下图所示,设T1和T2是视频中连续的两帧图像, 如要在T2帧中跟踪T1中的红色框中的车辆,首先,在T2中进行车辆检测,检测到了三辆车,如黄色框所示;然后需要解决的问题是,要在T1中红色框和T2中黄色框之间建立关联,根据关联关系,确定T2中检测到的车哪辆是T1中的跟踪结果,并用该检测
YOLOv5+DeepSORT目标跟踪与计数精讲
05-10
本课程使用YOLOv5和DeepSORT对视频中的行人、车辆做多目标跟踪和计数,开展YOLOv5目标检测和DeepSORT目标跟踪强强联手的应用。 课程分别在Windows和Ubuntu系统上做项目演示,并对DeepSORT原理和代码做详细解读(使用PyCharm单步调试讲解)。 课程包括:基础篇、实践篇、原理篇和代码解析篇。Ÿ  基础篇包括多目标跟踪任务介绍、数据集和评估指标;Ÿ  实践篇包括Win10和Ubuntu系统上的YOLOv5+DeepSORT的多目标跟踪和计数具体的实践操作步骤演示,特别是对行人、车辆的ReID数据集讲解了训练方法;Ÿ  原理篇中讲解了马氏距离、匈牙利算法、卡尔曼滤波器的原理,并解读了SORTDeepSORT论文;Ÿ  代码解析篇中使用PyCharm单步调试对DeepSORT代码逐个文件进行讲解。课程提供注释后的代码
Deepsort 算法的介绍
weixin_52002919的博客
12-08 1万+
Deep-Sort目标跟踪算法原理和代码解析 deepsort是基于目标检测的多目标跟踪算法(Mutil-object Tracking),目标检测算法的优劣影响该算法跟踪的效果。 1.MOT算法的主要步骤 给定视频的初始帧 运行目标检测算法,例如YOLO、Faster R-CNN 、SSD等算法对视频每帧进行检测,获得检测边界框 根据检测边界框对图片进行裁剪获得检测目标,再依次对目标进行特征提取(表观特征或运动特征) 根据提取的特征,计算前后两帧的相似度矩阵(cost_metrix) 数据关联,为每
基于YOLOV5-7.0+DeepSort的目标追踪算法
10-30
基于DeepSORT算法和YOLOv5 7.0版本的目标跟踪实现。DeepSORT是一种强大的多目标跟踪算法,结合YOLOv5 7.0版本的目标检测能力,可以实现高效准确的实时目标跟踪。 基于 YOLOV5 和 DeepSort 的目标追踪算法是一种结合了目标检测和运动预测的方法,用于在视频中实现多目标跟踪。 YOLOV5 是一种目标检测算法,它能够从视频帧中检测出目标对象,并给出其位置信息。具体来说,YOLOV5 通过将视频分解成多幅图像并逐帧执行,能够识别出每帧中的目标对象,并为其分配标签。 DeepSort 是基于 SORT目标跟踪算法的改进版。它从 SORT 演变而来,使用卡尔曼滤波器预测所检测对象的运动轨迹,并使用匈牙利算法将它们与新的检测目标相匹配。DeepSort 还整合了外观信息,从而提高 SORT 的性能,这使得在遇到较长时间的遮挡时,也能够正常跟踪目标,并有效减少 ID 转换的发生次数。 在基于 YOLOV5 和 DeepSort 的目标追踪算法中,首先使用 YOLOV5 对视频帧进行目标检测,然后使用 DeepSort 对检测到的目标进行跟踪。具体步
yolov5-deepsort算法WiderPerson密集行人检测和跟踪+训练权重
04-22
1、yolov5-deepsort算法WiderPerson密集行人检测和跟踪,包含YOLOv5训练好的WiderPerson数据集权重以及各种训练曲线 2、可以生成目标运动轨迹 3、pytorch框架,python代码 4、结果参考:https://blog.csdn.net/zhiqingAI/article/details/124230743
DEEP SORT目标跟踪算法论文
09-07
DEEP SORT目标跟踪算法论文
人脸跟踪:deepsort代码解读
BigCowPeking
08-30 1万+
代码流程图 deepsort代码解读 deep_sort代码(此处)处理流程解析:  按视频帧顺序处理,每一帧的处理流程如下: 读取当前帧目标检测框的位置及各检测框图像块的深度特征(此处在处理实际使用时需要自己来提取); 根据置信度对检测框进行过滤,即对置信度不足够高的检测框及特征予以删除; 对检测框进行非最大值抑制,消除一个目标身上多个框的情况; 预测:使用kalman滤波预...
复现deepsort出现AttributeError: ‘Upsample‘ object has no attribute ‘recompute_scale_factor‘
qq_44832009的博客
03-06 233
AttributeError: ‘Upsample‘ object has no attribute ‘recompute_scale_factor‘
DeepSORT(工作流程)
热门推荐
weixin_41761357的博客
07-15 4万+
DeepSORT是针对多目标跟踪的跟踪算法,传统的单目标跟踪算法直接用于多目标跟踪的话,理论上似乎可行,但是实际应用中会发现,单纯的套用单目标跟踪算法用于多个目标进行逐个跟踪的结果并不理想。至于有多不理想,把KCF用于多目标跟踪,结果就是KCF的帧率变低(初始帧框完后,后继框目标的速度极慢);速度极快的mosse用于多目标跟踪,精度降低(框很多背景而不是目标) 此时对多目标跟踪,常见的跟踪策略就是track+ ...
Deepsort从入门到精通
qq_53545309的博客
11-10 1829
在目标检测领域,(Simple Online and Realtime Tracking)算法和(Deep Learning for Multi-Object Tracking)算法是两种常用的目标追踪算法,它们通常与目标检测器结合使用,用于在视频中跟踪和识别目标。: SORT 算法是一种简单高效的多目标跟踪算法,其主要思想是通过关联检测框和已知轨迹来进行目标追踪。SORT 算法首先利用目标检测器检测出目标,并根据检测框的位置、大小等信息建立轨迹和检测框之间的关联。
DeepSort目标跟踪算法
pengxiang1998的博客
12-05 6028
DeepSort目标跟踪算法是在Sort算法基础上改进的。 Sort算法的核心便是卡尔曼滤波与匈牙利匹配算法 卡尔曼滤波是一种通过运动特征来预测目标运动轨迹的算法 其核心为五个公式,包含两个过程: 其分为先验估计(预测) 其中Xt-表示预测的位置状态,包含位置速度等信息,F为状态转移矩阵,描述前一帧如何影响该帧,ut-1为控制量,可认为是加速度B为控制矩阵,表示如何控制ut-1作用于当前状态。P-为当前帧的预测协方差矩阵,是描述变化关系的,要知道,变量之间是有联系而非独立的,比
deepsort算法解析
07-28
DeepSORT算法SORT算法的改进版本,其最大的特点是加入了外观信息,通过借用ReID领域模型来提取特征,从而减少了ID切换的次数。DeepSORT算法的流程如下: 1. 使用目标检测方法获取每一帧的目标检测框(detections)。 2. 使用卡尔曼滤波器对前一帧的轨迹(tracks)中的每个轨迹进行预测,得到当前帧轨迹的均值和协方差。 3. 将目标检测框和轨迹进行IOU匹配,得到匹配的轨迹(matched tracks)、未匹配的检测框(unmatched detections)和未匹配的轨迹(unmatched tracks)。 4. 使用卡尔曼滤波器更新匹配的轨迹的状态。 5. 对于未匹配的检测框,将其初始化为新的轨迹。 6. 对于未匹配的轨迹,将其删除。 7. 通过级联匹配和IOU匹配,使用匈牙利算法将预测得到的轨迹和当前帧中的检测框进行匹配。 8. 使用卡尔曼滤波器更新匹配的轨迹的状态。 DeepSORT算法代码可以在以下地址找到:\[2\]。该代码主要解析DeepSORT的核心部分,不包括目标检测部分。 总结来说,DeepSORT算法SORT算法的基础上加入了外观信息,通过使用ReID模型提取特征来减少ID切换的次数,从而提高了多目标跟踪的准确性和稳定性。 #### 引用[.reference_title] - *1* *2* [Deepsort 算法的介绍](https://blog.csdn.net/weixin_52002919/article/details/123954823)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^control_2,239^v3^insert_chatgpt"}} ] [.reference_item] - *3* [目标跟踪——Deep Sort算法原理浅析](https://blog.csdn.net/JulyLi2019/article/details/123992423)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^control_2,239^v3^insert_chatgpt"}} ] [.reference_item] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
写文章

热门文章

  • ISP(Image Singal Process)算法 5234
  • DeepSORT算法代码解析(全) 3451
  • YOLOv8的改进 2706
  • 常见的ISP算法及其实现 1841
  • 相机标定中的4个坐标系及其推导过程 1597

最新评论

  • DeepSORT算法代码解析(全)

    一只程序猿林: 为什么更新的特征列表要清空

  • YOLOv8的改进

    m0_54773696: 请问代码在哪里找到的?

  • DeepSORT算法代码解析(全)

    m0_51272092: 必须点个赞,真的太用心了!

  • 大恒工业相机实例使用

    未来超低端科技研究所: 你指的系统级驱动是什么?

  • DeepSORT算法代码解析(全)

    weixin_45933816: 太棒了,一下疏通了,赞

您愿意向朋友推荐“博客详情页”吗?

  • 强烈不推荐
  • 不推荐
  • 一般般
  • 推荐
  • 强烈推荐
提交

最新文章

  • 多线程编程10例
  • 超强面经——目标检测篇
  • 利用mAP计算目标检测精确度
2023年110篇
2022年5篇

目录

目录

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

和风细动帘帷暖

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或 充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值

深圳SEO优化公司株洲百度网站优化哪家好随州SEO按效果付费价格抚顺网站改版价格昌吉网站定制报价襄樊网页制作价格吕梁seo优化推荐邯郸优化多少钱荆州seo报价宝鸡seo推荐柳州网站搜索优化公司烟台网站优化排名公司永新百度关键词包年推广哪家好珠海百搜标王推荐成都网站定制哪家好天津网站推广工具哪家好清远阿里店铺托管价格河池建设网站推荐银川seo网站优化价格湛江建站价格铁岭关键词排名包年推广大鹏网站定制推荐玉溪外贸网站建设报价坪山网站推广方案多少钱海北阿里店铺托管通辽网站推广方案公司垦利企业网站设计哪家好天水外贸网站制作报价金昌高端网站设计推荐海西网站搭建哪家好晋城网络广告推广公司歼20紧急升空逼退外机英媒称团队夜以继日筹划王妃复出草木蔓发 春山在望成都发生巨响 当地回应60岁老人炒菠菜未焯水致肾病恶化男子涉嫌走私被判11年却一天牢没坐劳斯莱斯右转逼停直行车网传落水者说“没让你救”系谣言广东通报13岁男孩性侵女童不予立案贵州小伙回应在美国卖三蹦子火了淀粉肠小王子日销售额涨超10倍有个姐真把千机伞做出来了近3万元金手镯仅含足金十克呼北高速交通事故已致14人死亡杨洋拄拐现身医院国产伟哥去年销售近13亿男子给前妻转账 现任妻子起诉要回新基金只募集到26元还是员工自购男孩疑遭霸凌 家长讨说法被踢出群充个话费竟沦为间接洗钱工具新的一天从800个哈欠开始单亲妈妈陷入热恋 14岁儿子报警#春分立蛋大挑战#中国投资客涌入日本东京买房两大学生合买彩票中奖一人不认账新加坡主帅:唯一目标击败中国队月嫂回应掌掴婴儿是在赶虫子19岁小伙救下5人后溺亡 多方发声清明节放假3天调休1天张家界的山上“长”满了韩国人?开封王婆为何火了主播靠辱骂母亲走红被批捕封号代拍被何赛飞拿着魔杖追着打阿根廷将发行1万与2万面值的纸币库克现身上海为江西彩礼“减负”的“试婚人”因自嘲式简历走红的教授更新简介殡仪馆花卉高于市场价3倍还重复用网友称在豆瓣酱里吃出老鼠头315晚会后胖东来又人满为患了网友建议重庆地铁不准乘客携带菜筐特朗普谈“凯特王妃P图照”罗斯否认插足凯特王妃婚姻青海通报栏杆断裂小学生跌落住进ICU恒大被罚41.75亿到底怎么缴湖南一县政协主席疑涉刑案被控制茶百道就改标签日期致歉王树国3次鞠躬告别西交大师生张立群任西安交通大学校长杨倩无缘巴黎奥运

深圳SEO优化公司 XML地图 TXT地图 虚拟主机 SEO 网站制作 网站优化