• 基于YOLOv8与DeepSORT实现多目标跟踪——算法与源码解析


    一、概述

    "目标跟踪 (Object Tracking)"是机器视觉领域中的一个重要研究领域。根据跟踪的目标数量,可以将其分为两大类:单目标跟踪 (Single Object Tracking,简称 SOT) 和多目标跟踪 (Multi Object Tracking,简称 MOT)。

    多目标跟踪往往面临一些挑战,例如需要同时跟踪多个目标、目标可能频繁遮挡,这些因素使得目标跟丢成为一个常见问题。为了解决这些问题,可以借助跟踪器 DeepSORT 以及检测器 YOLO v8,从而构建一个高性能的实时多目标跟踪模型。

    二、算法与项目流程

    在深度学习领域中,目标跟踪是一项任务,旨在使用对象在空间和时间上的特征来预测它们在整个视频序列中的位置。从技术上讲,目标跟踪包括获取初始的目标检测集,为每个目标分配唯一的标识(ID),并在整个视频帧序列中跟踪它们,同时保持它们的标识不变。

    1.目标检测与目标追踪

    目标检测
    在计算机视觉任务中,目标检测是在图像和视频(一系列的图像)中扫描和搜寻目标并给出目标所在图像的位置坐标(边界框):
    在这里插入图片描述

    在对视频进行检测时,也是把视频拆分成一帧帧来进行处理,这处理的过程中,检测所获取目标在帧与帧之间是无法进行关联的,假设,要对视频进行行人检测时,第一帧检测到了行人,第二帧也检测到同一个行人,但这两帧的这个行人目标是无法进行关联的,假设当前视频中有五个行人,目标检测只能检测到当前视频每一帧都有这个几个目标,并不能确定这几个目标中哪个是目标A,哪个是目标B,那个目标C等等。
    视频示例:

    行人检测

    目标追踪
    从上面的示例可以看出,当使用对象检测器时,只会得到一个包含对象位置信息的输出数组。这个输出数组可能包含多个坐标,每个坐标代表一个检测到的对象的位置。然而,这个输出数组通常不提供有关对象之间的关联信息,因此在查看输出数组时,你无法知道哪个坐标对应于哪个检测框。

    与此不同,目标跟踪器在处理检测结果时为每个对象分配一个唯一的标识符(ID),并且会在整个帧序列中保持该ID不变,直到该对象的生命周期结束。这意味着,无论对象在视频中如何移动或遮挡,跟踪器都能够将相同的ID与该对象关联起来,以维护对象的一致性标识。这种ID分配使得跟踪器能够区分不同的对象,并跟踪它们的轨迹,而不仅仅是提供它们的位置信息。
    视频示例:

    运动目标追踪

    目标检测和目标跟踪的区别
    目标检测:

    • 目标检测任务要求同时完成对象的定位(即确定对象的边界框位置)和分类(即确定对象的类别)。这意味着目标检测算法必须不仅能够确定对象是否存在,还要知道它是什么。
    • 目标检测通常用于识别和定位图像或视频帧中的对象,通常需要明确的目标类别信息。

    目标跟踪:

    • 目标跟踪任务更关注对象在帧与帧之间的连续性,通常更注重对象的运动特征,而不要求进行目标的分类。
    • 目标跟踪可以不涉及目标的类别,它的主要目标是维护对象的位置和轨迹,以实现在视频序列中的跟踪。

    2.目标追踪的类型

    目标跟踪器根据不同的分类标准可以分为多种类型:

    1. 单目标跟踪(Single Object Tracking,SOT):

      • 这类跟踪器专注于跟踪单个目标,即使在帧中存在多个其他对象。它们通常在第一帧中初始化目标的位置,然后在整个帧序列中跟踪它。一些传统的SOT方法包括CSRT、KCF等,但现在基于深度学习的跟踪器如GOTURN和SiamRPN表现更准确。
    2. 多目标跟踪(Multi Object Tracking,MOT):

      • 这些跟踪器具有能力同时跟踪多个目标,甚至可以跟踪不同类别的目标,并保持高速性能。它们通常在大规模数据集上进行训练,因此具有更高的准确性。一些强大的MOT算法包括DeepSORT、JDE和CenterTrack。
    3. 通过检测跟踪(Detection-Based Tracking):

      • 这种类型的跟踪算法首先使用目标检测器检测帧中的对象,然后跨帧执行数据关联以生成目标的轨迹,实现目标的跟踪。它们能够同时跟踪多个对象,并具备处理新对象引入的能力。即使目标检测失败,这些算法也有助于保持目标的跟踪。
    4. 无检测跟踪(Detection-Free Tracking):

      • 这种类型的跟踪算法需要手动初始化目标的位置,然后在后续帧中跟踪这些目标。这种方法通常使用传统的计算机视觉算法,而不依赖于目标检测。它们在一些特定应用中仍然有用。

    不同类型的目标跟踪器适用于不同的应用场景,选择适当的跟踪器取决于任务要求、对象数量和性能要求。

    3.目标跟踪的应用

    1. 交通监控:

      • 交通流量管理:目标跟踪可用于监控道路上的车辆,帮助交通管理部门更好地理解交通流量,进行交通优化和拥堵监测。
      • 违规行为检测:跟踪器可以检测和记录交通违规行为,如闯红灯、违规变道等,以提供证据用于执法。
    2. 体育运动/赛事:

      • 运动员跟踪:跟踪器可用于追踪运动员在比赛中的位置和移动,为教练和观众提供实时数据和分析。
      • 比赛统计:通过跟踪球员和球的位置,可以自动记录得分、犯规和其他比赛统计数据,减轻人工记录的工作量。
    3. 多摄像头监控:

      • 重新识别:核心思想是在多个摄像头之间重新识别相同的对象。如果一个对象在一个带有独特ID的摄像头中被跟踪,然后离开画面并在另一台摄像头中再次出现,跟踪器能够保持相同的ID,帮助重新识别对象。
      • 入侵检测:跟踪器可以用于监控区域,以检测不明对象的出现或不寻常的行为,从而帮助提高安全性。
    4. 智能监控和安全:

      • 安防监控:跟踪器可用于监控大型场所,如机场、商场和公共交通站,以监测潜在的威胁或异常行为。
      • 人脸跟踪:用于跟踪和记录人员的位置,特别是在人脸识别系统中,以进行出入记录或提供定位服务。
    5. 无人机和自动驾驶:

      • 无人机导航:目标跟踪可用于协助无人机导航和跟踪地面目标,例如搜索和救援、农业监测等。
      • 自动驾驶车辆:跟踪技术可以用于自动驾驶车辆中,以帮助车辆理解和预测其他交通参与者的行为。

    4. 算法整合

    在实际应用中,结合目标检测和目标跟踪可以实现更高效的处理方式。可以使用目标检测来定期锁定目标,然后使用目标跟踪来跟踪目标的位置,这样可以降低计算成本,同时可以定期进行目标分类,从而减轻系统的负担。这种方法可以有效提高处理速度,并在实际场景中得到广泛应用。

    1. 目标检测:

      • 使用目标检测算法在每一帧中检测出目标的位置和边界框。这一步可以提供当前帧中的目标位置信息。
    2. 特征提取:

      • 对每个检测到的目标,使用深度学习模型(如卷积神经网络,CNN)提取目标的特征表示。这些特征表示捕捉了目标的外观和特征,通常是高维向量。
    3. 目标匹配:

      • 将每个检测到的目标与先前帧中已跟踪的目标进行匹配,以确定目标的身份和轨迹。匹配过程通常考虑多个因素,包括目标的特征相似度、运动一致性、外观相似度等。
    4. 外观特征描述符:

      • 在目标匹配中,使用强大的外观特征描述符,如DeepSORT中所使用的,可以更准确地区分不同目标之间的相似度。这些描述符帮助算法区分不同目标,即使它们具有相似的外观特征。
    5. 处理复杂情况:

      • 目标跟踪还需要处理各种复杂情况,如目标的消失和重新出现、遮挡、变形等。算法需要具备鲁棒性,以支持长期目标跟踪,并能够应对这些挑战。

    三、DeepSORT算法

    DeepSORT是一种计算机视觉目标跟踪算法,旨在为每个对象分配唯一的ID并跟踪它们。它是SORT(Simple Online and Realtime Tracking,简单在线实时跟踪)算法的扩展和改进版本。SORT是一种轻量级目标跟踪算法,用于处理实时视频流中的目标跟踪问题。DeepSORT引入了深度学习技术,以加强SORT的性能,并特别关注在多个帧之间跟踪目标的一致性。

    1. SORT目标追踪

    SORT 是一种对象跟踪方法,其中使用卡尔曼滤波器和匈牙利算法等基本方法来跟踪对象,并声称比许多在线跟踪器更好。SORT 由以下 4 个关键组件组成:

    1. 检测:首先,在跟踪流程的第一步,目标检测器被用来检测当前帧中需要跟踪的目标对象。常用的目标检测器包括Faster R-CNN、YOLO等。

    2. 估计:在估计阶段,检测结果从当前帧传播到下一帧,使用恒速模型来估计下一帧中目标的位置。当检测结果与已知的目标相关联时,检测到的边界框信息用于更新目标的状态,包括速度分量,这是通过卡尔曼滤波器框架来实现的。

    3. 数据关联:在数据关联步骤中,目标的边界框信息与检测结果结合,从而形成一个成本矩阵,该矩阵计算每个检测与已知目标的所有预测边界框之间的交并比(IOU)距离。然后,使用匈牙利算法来优化分配,以确保正确地将检测结果与目标关联起来。这个技术有助于解决遮挡问题并保持目标的唯一身份。

    4. 管理目标ID的创建与删除:跟踪模块负责创建和销毁目标的唯一身份(ID)。如果检测结果与目标的IOU小于某个预定义的阈值(通常称为IOUmin),则不会将检测结果与目标相关联,这表示目标未被跟踪。此外,如果在连续TLost帧中没有检测到目标,跟踪将终止该目标的轨迹,其中TLost是一个可配置的参数。如果目标重新出现,跟踪将在新的身份下恢复。

    2. DeepSORT算法

    SORT在目标跟踪的精度和准确性方面表现出色,但它在生成大量不断切换的ID轨道和遇到遮挡情况时表现不佳。这是因为SORT使用关联矩阵来进行数据关联,而这种方法在复杂场景下存在一些限制。

    DeepSORT是一种进化的跟踪算法,它改进了数据关联的方式,引入了更好的关联度量。DeepSORT可被定义为一种跟踪算法,它不仅仅基于目标的速度和运动来进行跟踪,还结合了目标的外观特征。

    为了实现这些改进,DeepSORT首先在实际跟踪之前离线训练了一个具有出色区分性的特征嵌入网络。这个网络在大规模人员重新识别数据集上进行了训练,以确保在目标跟踪的上下文中具有高度区分性的特征。

    在DeepSORT中,余弦距离度量方法用于训练深度关联度量模型。根据DeepSORT的论文,余弦距离考虑了目标的外观信息,这在处理遮挡和运动估计失败的情况下尤为有用。这意味着余弦距离是一种度量方法,有助于在目标长时间遮挡或其他复杂情况下准确恢复目标的身份。

    四、Yolov8目标检测

    1.算法简介

    YOLOv8是一种基于图像全局信息进行预测并且它是一种端到端的目标检测系统,最初的YOLO模型由Joseph Redmon和Ali Farhadi于2015年提出,并随后进行了多次改进和迭代,产生了一系列不同版本的YOLO模型,如YOLOv2、YOLOv3、YOLOv4,YOLOv5等。这些更新和迭代旨在提高模型的性能、精度和速度,使其在实际应用中更具竞争力。

    YOLOv8的核心思想是将图像划分为网格,并在每个网格单元中预测物体的边界框和类别。这种设计使得YOLO非常适合实时目标检测应用,因为它可以在较短的时间内完成目标检测任务。

    2. 算法源码与部署

    关于YOLOv8目标检测的相关内容可以看之前的博客:YoloV8目标检测与实例分割——目标检测onnx模型推理

    五、目标检测与追踪源码解析

    1.检测

    在每一帧中,目标检测器识别并提取出边界框(bbox),这些边界框表示在当前帧中检测到的目标物体。

     def detect(self,cv_src):
            boxes, scores, class_ids = self.detector(cv_src)
    
            pred_boxes = []
            for i in range(len(boxes)):
                x1,y1 = int(boxes[i][0]),int(boxes[i][1])
                x2,y2 = int(boxes[i][2]),int(boxes[i][3])
                lbl = class_names[class_ids[i]]
                # print(class_ids[i])
    
                # if lbl in ['person','sack','elec','bag','box','caron']:
                #     continue
    
                pred_boxes.append((x1,y1,x2,y2,lbl,class_ids[i]))
    
            return cv_src,pred_boxes
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    2. 生成detections

    从这些检测到的边界框中,生成称为"detections"的目标检测结果。每个detection通常包含有关目标的信息,如边界框坐标和可信度分数。

    #  deep_sort.py
    def update(self, bbox_xywh, confidences, ori_img):
        self.height, self.width = ori_img.shape[:2]
        # 提取每个bbox的feature
        features = self._get_features(bbox_xywh, ori_img)
        # [cx,cy,w,h] -> [x1,y1,w,h]
        bbox_tlwh = self._xywh_to_tlwh(bbox_xywh)
        # 过滤掉置信度小于self.min_confidence的bbox,生成detections
        detections = [Detection(bbox_tlwh[i], conf, features[i]) for i,conf in enumerate(confidences) if conf > self.min_confidence]
        # NMS (这里self.nms_max_overlap的值为1,即保留了所有的detections)
        boxes = np.array([d.tlwh for d in detections])
        scores = np.array([d.confidence for d in detections])
        indices = non_max_suppression(boxes, self.nms_max_overlap, scores)
        detections = [detections[i] for i in indices]
        ...
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    3. 卡尔曼滤波预测

    对于已知的跟踪对象(“tracks”),在下一帧中进行卡尔曼滤波预测,以估计其新的位置和速度。

    #  track.py
    def predict(self, kf):
        """Propagate the state distribution to the current time step using a 
           Kalman filter prediction step.
        Parameters
        ----------
        kf: The Kalman filter.
        """
        self.mean, self.covariance = kf.predict(self.mean, self.covariance)  # 预测
        self.age += 1  # 该track自出现以来的总帧数加1
        self.time_since_update += 1  # 该track自最近一次更新以来的总帧数加1
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    4.使用匈牙利算法将预测后的tracks和当前帧中的detections进行匹配

    这是DeepSORT中的核心步骤。DeepSORT使用匈牙利算法来将预测的tracks和当前帧的detections进行匹配。这个匹配可以采用两种级联方法:首先,通过计算马氏距离来估算预测对象与检测对象之间的关联,如果马氏距离小于指定的阈值,则将它们匹配为同一目标。其次,DeepSORT还使用外观特征余弦距离度量,通过一个重识别模型获得不同物体的特征向量,然后构建余弦距离代价函数,以计算预测对象与检测对象的相似度。这两个代价函数的结果都趋向于小,如果边界框接近且特征相似,则将它们匹配为同一目标。

    #  tracker.py
    def _match(self, detections):
        def gated_metric(racks, dets, track_indices, detection_indices):
            """
            基于外观信息和马氏距离,计算卡尔曼滤波预测的tracks和当前时刻检测到的detections的代价矩阵
            """
            features = np.array([dets[i].feature for i in detection_indices])
            targets = np.array([tracks[i].track_id for i in track_indices]
     # 基于外观信息,计算tracks和detections的余弦距离代价矩阵
            cost_matrix = self.metric.distance(features, targets)
     # 基于马氏距离,过滤掉代价矩阵中一些不合适的项 (将其设置为一个较大的值)
            cost_matrix = linear_assignment.gate_cost_matrix(self.kf, cost_matrix, tracks, 
                          dets, track_indices, detection_indices)
            return cost_matrix
    
        # 区分开confirmed tracks和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()]
    
        # 对confirmd tracks进行级联匹配
        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)
    
        # 对级联匹配中未匹配的tracks和unconfirmed tracks中time_since_update为1的tracks进行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]
        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)
     
        # 整合所有的匹配对和未匹配的tracks
        matches = matches_a + matches_b
        unmatched_tracks = list(set(unmatched_tracks_a + unmatched_tracks_b))
        
        return matches, unmatched_tracks, unmatched_detections
    
    
    # 级联匹配源码  linear_assignment.py
    def matching_cascade(distance_metric, max_distance, cascade_depth, tracks, detections, 
                         track_indices=None, detection_indices=None):
        ...
        unmatched_detections = detection_indice
        matches = []
        # 由小到大依次对每个level的tracks做匹配
        for level in range(cascade_depth):
     # 如果没有detections,退出循环
            if len(unmatched_detections) == 0:  
                break
     # 当前level的所有tracks索引
            track_indices_l = [k for k in track_indices if 
                               tracks[k].time_since_update == 1 + level]
     # 如果当前level没有track,继续
            if len(track_indices_l) == 0: 
                continue
      
     # 匈牙利匹配
            matches_l, _, unmatched_detections = min_cost_matching(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
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67

    5. 卡尔曼滤波更新

    匹配后,DeepSORT使用检测到的detections来更新每个已知的跟踪对象的状态,例如位置和速度。这有助于保持跟踪对象的准确性和连续性。

    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.
        """
        # 得到匹配对、未匹配的tracks、未匹配的dectections
        matches, unmatched_tracks, unmatched_detections = self._match(detections)
    
        # 对于每个匹配成功的track,用其对应的detection进行更新
        for track_idx, detection_idx in matches:
            self.tracks[track_idx].update(self.kf, detections[detection_idx])
        
    	# 对于未匹配的成功的track,将其标记为丢失
    	for track_idx in unmatched_tracks:
            self.tracks[track_idx].mark_missed()
    	
        # 对于未匹配成功的detection,初始化为新的track
        for detection_idx in unmatched_detections:
            self._initiate_track(detections[detection_idx])
        
    	...
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    6.检测结果

    目标追踪

  • 相关阅读:
    【jvm优化超详细】常见的JVM调优场景
    20.Redis之缓存
    二十三种设计模式全面解析-组合模式与装饰器模式的结合:实现动态功能扩展
    进大厂必须要会的单元测试
    消息中间件概述
    AndroidStudio使用superSUAPK ROOT(失败)
    如果再来一次,你还会选择互联网么?
    SpringBoot中Bean无法加载的原因,以及Bean的扫描方式
    扩展 Calcite 中的 SQL 解析语法
    springBoot -md
  • 原文地址:https://blog.csdn.net/matt45m/article/details/134237238