Day 33:目标跟踪基础 — SORT与DeepSORT

今日目标 :掌握目标跟踪的核心组件(卡尔曼滤波/匈牙利匹配/ReID),理解SORT到DeepSORT的进化,从零实现简单跟踪器

预计阅读 :10分钟 | 动手操作:40分钟


一、检测 vs 跟踪:从"找到"到"追踪"

复制代码
目标检测:每一帧独立检测,找到"有什么"和"在哪里"
  → 帧与帧之间没有关联,同一个物体在不同帧被当作新物体

目标跟踪:关联帧间的检测结果,给每个物体分配ID
  → 同一个人从进入画面到离开,ID始终不变
  → 用于计数、轨迹分析、行为识别

Tracking-by-Detection 范式:
  Step 1: 检测器找到每一帧的所有物体
  Step 2: 跟踪器关联帧间检测结果
  Step 3: 维护每个物体的轨迹(轨迹 = 同一ID的检测序列)

端侧AI最常用的跟踪场景:
  - 行人/车辆跟踪计数
  - 异常行为检测
  - 客流统计
  - 多目标轨迹分析
python 复制代码
import torch
import torch.nn as nn
import numpy as np
from scipy.optimize import linear_sum_assignment

torch.manual_seed(42)

二、核心组件

2.1 卡尔曼滤波:预测运动轨迹

python 复制代码
class KalmanFilter:
    """
    卡尔曼滤波器:跟踪器的"大脑"
    
    核心思想:用运动模型预测物体下一帧的位置
    如果检测和预测接近 → 匹配成功 → 用检测更新预测
    如果检测丢失 → 继续用预测位置作为估计
    
    状态向量: [x, y, a, h, vx, vy, va, vh]
      x, y: 框中心坐标
      a: 宽高比 (aspect ratio)
      h: 高度
      vx, vy, va, vh: 对应的速度
    
    观测向量: [x, y, a, h] (检测器输出的框)
    
    两个阶段:
    1. 预测 (Predict): 根据上一帧状态,预测当前帧位置
    2. 更新 (Update): 用检测结果修正预测
    """
    def __init__(self):
        # 状态转移矩阵 (8×8)
        self.F = np.eye(8)
        # 位置 = 上一帧位置 + 速度 × dt
        for i in range(4):
            self.F[i, i+4] = 1.0
        
        # 观测矩阵 (4×8)
        # 我们只能观测到位置,观测不到速度
        self.H = np.eye(4, 8)
        
        # 过程噪声协方差
        self.Q = np.eye(8)
        self.Q[4:, 4:] *= 0.01  # 速度噪声更小
        
        # 观测噪声协方差
        self.R = np.eye(4) * 0.1
        
        # 状态协方差矩阵
        self.P = np.eye(8) * 10.0
        
        # 状态向量
        self.x = None
    
    def init(self, measurement):
        """初始化卡尔曼滤波器"""
        # measurement: [x, y, a, h]
        self.x = np.zeros(8)
        self.x[:4] = measurement
        # 速度初始化为0
        self.x[4:] = 0
    
    def predict(self):
        """预测阶段:根据运动模型预测下一帧位置"""
        if self.x is None:
            return None
        
        # 状态预测: x̂ = F × x
        self.x = self.F @ self.x
        
        # 协方差预测: P = F × P × F^T + Q
        self.P = self.F @ self.P @ self.F.T + self.Q
        
        return self.x[:4]  # 返回预测的位置
    
    def update(self, measurement):
        """更新阶段:用检测结果修正预测"""
        if self.x is None:
            self.init(measurement)
            return
        
        # 卡尔曼增益: K = P × H^T × (H × P × H^T + R)^{-1}
        S = self.H @ self.P @ self.H.T + self.R
        K = self.P @ self.H.T @ np.linalg.inv(S)
        
        # 测量残差: y = z - H × x
        y = measurement - self.H @ self.x
        
        # 状态更新: x = x + K × y
        self.x = self.x + K @ y
        
        # 协方差更新: P = (I - K × H) × P
        self.P = (np.eye(8) - K @ self.H) @ self.P
    
    @staticmethod
    def demo():
        """演示卡尔曼滤波的工作原理"""
        kf = KalmanFilter()
        
        # 模拟一个物体的运动(带噪声的检测)
        true_positions = []
        noisy_detections = []
        
        for t in range(20):
            # 真实位置(匀速直线运动)
            true_x = 100 + 5 * t
            true_y = 200 + 2 * t
            true_positions.append([true_x, true_y])
            
            # 模拟检测噪声
            noisy_x = true_x + np.random.randn() * 3
            noisy_y = true_y + np.random.randn() * 3
            noisy_detections.append([noisy_x, noisy_y])
        
        # 卡尔曼滤波跟踪
        filtered_positions = []
        kf.init([noisy_detections[0][0], noisy_detections[0][1], 1.0, 100.0])
        
        for det in noisy_detections:
            pred = kf.predict()
            filtered_positions.append(pred[:2])
            kf.update([det[0], det[1], 1.0, 100.0])
        
        print("卡尔曼滤波演示:")
        print(f"  真实位置: 从({true_positions[0][0]},{true_positions[0][1]}) "
              f"到({true_positions[-1][0]},{true_positions[-1][1]})")
        print(f"  检测噪声: ±3像素")
        print(f"  滤波后: 平滑了噪声,跟踪了运动")
        print(f"\n  核心公式:")
        print(f"  预测: x̂ = F·x (状态向前推一步)")
        print(f"  更新: x = x̂ + K·(z - H·x̂) (用检测修正预测)")

KalmanFilter.demo()

2.2 匈牙利算法:最优匹配

python 复制代码
def hungarian_matching_demo():
    """
    匈牙利算法:解决二分图的最优匹配问题
    
    在跟踪中:
    左侧:当前帧的检测框
    右侧:上一帧的跟踪框
    代价矩阵:检测框和跟踪框之间的IoU距离(或外观距离)
    输出:最优的一一匹配
    
    为什么需要匈牙利算法?
    不能简单地"谁最近就匹配谁"
    需要考虑全局最优:
      Track1离Det1近,Track2也离Det1近
      → Track1匹配Det1,Track2匹配Det2(全局最优)
      → 而不是Track1和Track2都抢Det1
    """
    # 模拟代价矩阵:4个检测 × 4个跟踪
    # 代价 = 1 - IoU(越小越好)
    cost_matrix = np.array([
        [0.1, 0.8, 0.9, 0.7],  # Det1: 和Track1最匹配
        [0.7, 0.2, 0.8, 0.9],  # Det2: 和Track2最匹配
        [0.8, 0.9, 0.3, 0.8],  # Det3: 和Track3最匹配
        [0.9, 0.7, 0.8, 0.4],  # Det4: 和Track4最匹配
    ])
    
    # 匈牙利算法求解
    row_ind, col_ind = linear_sum_assignment(cost_matrix)
    
    print("匈牙利算法匹配演示:")
    print("代价矩阵:")
    for i, row in enumerate(cost_matrix):
        print(f"  Det{i+1}: {row}")
    
    print("\n最优匹配:")
    total_cost = 0
    for r, c in zip(row_ind, col_ind):
        print(f"  Det{r+1} → Track{c+1} (代价: {cost_matrix[r,c]:.2f})")
        total_cost += cost_matrix[r, c]
    print(f"  总代价: {total_cost:.2f}")


hungarian_matching_demo()

2.3 SORT:简单在线实时跟踪

python 复制代码
class SORTTracker:
    """
    SORT: Simple Online and Realtime Tracking
    论文:https://arxiv.org/abs/1602.00763
    
    核心思想:卡尔曼滤波 + 匈牙利匹配
    
    流程:
    1. 检测器输出当前帧的所有检测框
    2. 对每个跟踪器,用卡尔曼滤波预测下一帧位置
    3. 计算检测框和预测框的IoU,构建代价矩阵
    4. 匈牙利算法匹配
    5. 匹配成功的:用检测更新卡尔曼滤波
    6. 匹配失败的检测:创建新跟踪器
    7. 匹配失败的跟踪器:如果连续N帧未匹配,删除
    
    SORT的优缺点:
    ✅ 简单、快速(260 FPS)
    ✅ 纯运动模型,容易部署
    ❌ 遮挡后容易ID Switch
    ❌ 没有外观信息,重新出现后无法恢复ID
    """
    def __init__(self, max_age=3, min_hits=3, iou_threshold=0.3):
        self.max_age = max_age      # 最大丢失帧数
        self.min_hits = min_hits    # 最少确认帧数
        self.iou_threshold = iou_threshold
        self.trackers = []          # 活跃的跟踪器
        self.frame_count = 0
        self.next_id = 0
    
    def _iou_batch(self, boxes1, boxes2):
        """批量计算IoU矩阵"""
        if len(boxes1) == 0 or len(boxes2) == 0:
            return np.zeros((len(boxes1), len(boxes2)))
        
        # boxes: [x1, y1, x2, y2]
        lt = np.maximum(boxes1[:, None, :2], boxes2[:, :2])
        rb = np.minimum(boxes1[:, None, 2:], boxes2[:, 2:])
        wh = np.maximum(0, rb - lt)
        inter = wh[:, :, 0] * wh[:, :, 1]
        
        area1 = (boxes1[:, 2] - boxes1[:, 0]) * (boxes1[:, 3] - boxes1[:, 1])
        area2 = (boxes2[:, 2] - boxes2[:, 0]) * (boxes2[:, 3] - boxes2[:, 1])
        union = area1[:, None] + area2 - inter
        
        return inter / (union + 1e-8)
    
    def update(self, detections):
        """
        更新跟踪器
        
        detections: [(x1, y1, x2, y2, score), ...]
        """
        self.frame_count += 1
        
        # 转换为numpy数组
        dets = np.array([[d[0], d[1], d[2], d[3]] for d in detections])
        
        # 预测所有跟踪器的下一帧位置
        predicted = []
        for tracker in self.trackers:
            pred = tracker['kf'].predict()
            if pred is not None:
                # 将[x, y, a, h]转为[x1, y1, x2, y2]
                x, y, a, h = pred
                w = a * h
                predicted.append([x - w/2, y - h/2, x + w/2, y + h/2])
            else:
                predicted.append([0, 0, 0, 0])
        predicted = np.array(predicted) if predicted else np.empty((0, 4))
        
        # IoU匹配
        matched, unmatched_dets, unmatched_trks = [], [], []
        
        if len(dets) > 0 and len(predicted) > 0:
            iou_matrix = self._iou_batch(dets, predicted)
            
            # 匈牙利匹配
            cost = 1 - iou_matrix
            row_ind, col_ind = linear_sum_assignment(cost)
            
            for r, c in zip(row_ind, col_ind):
                if iou_matrix[r, c] >= self.iou_threshold:
                    matched.append((r, c))
                else:
                    unmatched_dets.append(r)
                    unmatched_trks.append(c)
            
            unmatched_dets.extend([i for i in range(len(dets)) if i not in row_ind])
            unmatched_trks.extend([i for i in range(len(predicted)) if i not in col_ind])
        else:
            unmatched_dets = list(range(len(dets)))
            unmatched_trks = list(range(len(predicted)))
        
        # 更新匹配的跟踪器
        for d_idx, t_idx in matched:
            det = dets[d_idx]
            x1, y1, x2, y2 = det
            w = x2 - x1
            h = y2 - y1
            x = (x1 + x2) / 2
            y = (y1 + y2) / 2
            a = w / h if h > 0 else 1.0
            
            self.trackers[t_idx]['kf'].update([x, y, a, h])
            self.trackers[t_idx]['hits'] += 1
            self.trackers[t_idx]['age'] = 0
            self.trackers[t_idx]['bbox'] = det
        
        # 创建新的跟踪器(未匹配的检测)
        for d_idx in unmatched_dets:
            det = dets[d_idx]
            x1, y1, x2, y2 = det
            w = x2 - x1
            h = y2 - y1
            x = (x1 + x2) / 2
            y = (y1 + y2) / 2
            a = w / h if h > 0 else 1.0
            
            kf = KalmanFilter()
            kf.init([x, y, a, h])
            
            self.trackers.append({
                'kf': kf,
                'id': self.next_id,
                'hits': 1,
                'age': 0,
                'bbox': det,
            })
            self.next_id += 1
        
        # 更新未匹配跟踪器的age
        for t_idx in unmatched_trks:
            self.trackers[t_idx]['age'] += 1
        
        # 删除过期的跟踪器
        self.trackers = [t for t in self.trackers if t['age'] <= self.max_age]
        
        # 返回确认的跟踪结果
        results = []
        for t in self.trackers:
            if t['hits'] >= self.min_hits:
                results.append({
                    'id': t['id'],
                    'bbox': t['bbox'],
                })
        
        return results


def demo_sort():
    """演示SORT跟踪器"""
    tracker = SORTTracker(max_age=3, min_hits=2, iou_threshold=0.3)
    
    # 模拟3帧数据
    frames = [
        # 帧1: 两个检测
        [(100, 100, 200, 200, 0.9), (300, 300, 400, 400, 0.85)],
        # 帧2: 物体移动了
        [(105, 105, 205, 205, 0.9), (305, 305, 405, 405, 0.85)],
        # 帧3: 物体继续移动
        [(110, 110, 210, 210, 0.9), (310, 310, 410, 410, 0.85)],
    ]
    
    print("SORT跟踪演示:")
    for i, dets in enumerate(frames):
        results = tracker.update(dets)
        ids = [r['id'] for r in results]
        print(f"  帧{i+1}: 检测{len(dets)}个 → 跟踪ID: {ids}")

demo_sort()

三、DeepSORT:加入外观特征

python 复制代码
"""
DeepSORT: Simple Online and Realtime Tracking with a Deep Association Metric
论文:https://arxiv.org/abs/1703.07402

SORT的问题:
  遮挡后ID Switch严重(物体被遮挡后重新出现,被当作新物体)
  原因:纯靠运动模型,没有外观信息

DeepSORT的改进:
  1. 加入ReID外观特征(CNN提取的embedding)
  2. 级联匹配策略(先匹配活跃的,再匹配新出现的)
  3. 运动+外观联合代价

DeepSORT的核心公式:
  代价 = λ × 运动代价 + (1-λ) × 外观代价
  
  运动代价 = 马氏距离 (Mahalanobis Distance)
    衡量检测框和预测框的"运动一致性"
    
  外观代价 = 余弦距离 (Cosine Distance)
    衡量两个物体的"长得像不像"
    用ReID网络提取的特征向量计算余弦相似度
"""

class DeepSORTConcept:
    def __init__(self):
        self.components = {
            '卡尔曼滤波': '预测运动轨迹(和SORT相同)',
            'ReID网络': '提取外观特征向量(128维或512维)',
            '级联匹配': '分层次匹配,优先匹配活跃轨迹',
            '特征库': '保存最近100帧的外观特征',
        }
    
    def describe(self):
        print("DeepSORT = SORT + ReID + 级联匹配")
        print("\n核心组件:")
        for k, v in self.components.items():
            print(f"  {k}: {v}")
        
        print("\n匹配代价:")
        print("  运动代价: 马氏距离 d_mahalanobis")
        print("  外观代价: 1 - cos_sim(embedding1, embedding2)")
        print("  联合代价: c = λ·d_m + (1-λ)·d_app")
        
        print("\n级联匹配:")
        print("  第1轮: 匹配age=0的轨迹(最活跃)")
        print("  第2轮: 匹配age=1的轨迹")
        print("  ...")
        print("  优先匹配活跃轨迹,避免被旧轨迹抢走")

DeepSORTConcept().describe()

四、动手实践:简易ReID特征提取

python 复制代码
class SimpleReID:
    """
    简易ReID特征提取器
    
    实际项目中使用专门的ReID网络(如OSNet、ResNet-50)
    这里演示概念
    """
    def __init__(self):
        # 模拟:用颜色直方图作为简单的外观特征
        pass
    
    @staticmethod
    def extract_features(detections, frame):
        """
        提取检测框的外观特征
        
        实际中:crop检测框 → ReID网络 → embedding向量
        这里:简化为随机特征(演示概念)
        """
        features = []
        for det in detections:
            # 实际:用CNN提取特征
            # feature = reid_model(crop_image)
            feature = np.random.randn(128)  # 128维特征向量
            feature = feature / np.linalg.norm(feature)  # 归一化
            features.append(feature)
        return np.array(features)
    
    @staticmethod
    def cosine_similarity(feat1, feat2):
        """余弦相似度"""
        return np.dot(feat1, feat2) / (np.linalg.norm(feat1) * np.linalg.norm(feat2) + 1e-8)


print("ReID特征提取:")
print("  实际网络: OSNet / ResNet-50 / MobileNet")
print("  特征维度: 128维或512维")
print("  距离度量: 余弦相似度")
print("  端侧推荐: OSNet (轻量级ReID专用网络)")

五、端侧AI跟踪方案

python 复制代码
def edge_tracking_guide():
    """
    端侧AI跟踪方案选型
    
    核心考虑:
    1. 检测器速度(YOLO是关键)
    2. 跟踪器速度(SORT足够快)
    3. ReID速度(DeepSORT的瓶颈)
    """
    guide = {
        'SORT (纯运动)': {
            '速度': '极快(260 FPS跟踪器)',
            '精度': '一般(遮挡后ID Switch多)',
            '适用': '简单场景、遮挡少、不计ID',
            '部署': '几乎零成本,只需卡尔曼滤波',
        },
        'DeepSORT (运动+外观)': {
            '速度': '快(外加ReID推理时间)',
            '精度': '较好(遮挡后恢复ID)',
            '适用': '需要稳定ID、中等遮挡',
            '部署': '需要额外部署ReID网络',
        },
        'ByteTrack (数据关联改进)': {
            '速度': '快(利用低分检测框)',
            '精度': '很好(遮挡恢复能力强)',
            '适用': '端侧AI推荐!最新SOTA',
            '部署': '不需要ReID,纯数据关联',
        },
        'BoT-SORT (ByteTrack改进)': {
            '速度': '快',
            '精度': '更好(加入相机运动补偿)',
            '适用': '运动相机场景',
            '部署': '需要相机运动估计',
        },
    }
    
    print("端侧AI跟踪方案选型:")
    for name, info in guide.items():
        print(f"\n  [{name}]")
        for k, v in info.items():
            print(f"    {k}: {v}")
    
    print("\n\n端侧AI推荐:")
    print("  简单场景: SORT → 极快,部署简单")
    print("  需要稳定ID: ByteTrack → 不需要额外ReID网络")
    print("  精度优先: DeepSORT + OSNet → ReID加外观信息")

edge_tracking_guide()

六、常见坑点

坑1:卡尔曼滤波的状态表示

python 复制代码
# SORT/DeepSORT的状态:
# [x, y, a, h, vx, vy, va, vh]
# a = w/h (宽高比),不是宽度!

# ❌ 错误:把a当成宽度
# x, y, w, h, vx, vy, vw, vh

# ✅ 正确:a是宽高比
# 从检测框转换:
# x = (x1 + x2) / 2
# y = (y1 + y2) / 2
# a = (x2 - x1) / (y2 - y1)
# h = y2 - y1

坑2:IoU匹配的阈值调不对

python 复制代码
# IoU阈值太高 → 检测和跟踪匹配不上 → 大量ID Switch
# IoU阈值太低 → 错误的匹配 → 跟踪漂移
# 经验值:0.3-0.5 是常用范围
# 快速运动场景:降低阈值到0.2
# 慢速运动场景:提高阈值到0.5

坑3:不要过早创建新ID

python 复制代码
# 检测器偶发的误检 → 如果立即创建新ID → 大量虚假ID
# 解决方案:min_hits 参数
# 新检测框需要连续出现 min_hits 帧才确认为新ID
# 端侧推荐:min_hits=3

坑4:DeepSORT的特征库管理

python 复制代码
# 特征库保存最近N帧的外观特征
# N太小 → 遮挡后无法恢复
# N太大 → 内存占用大,外观变化后匹配不准
# 推荐:N=100(保存最近100帧的外观特征)
# 端侧优化:N=30-50(节省内存)

七、今日作业

  1. 手写卡尔曼滤波:实现卡尔曼滤波的predict和update,用模拟数据验证
  2. 匈牙利匹配:手写匈牙利匹配(或用scipy),理解代价矩阵的构建
  3. SORT跟踪器:实现SORT跟踪器,用模拟数据测试跟踪效果
  4. 打卡 :评论区发你的跟踪结果,格式:"Day 33/100 打卡:目标跟踪已掌握!"

今日小结

复制代码
今天你学会了:
✅ 检测 vs 跟踪:关联帧间检测,分配ID
✅ Tracking-by-Detection范式
✅ 卡尔曼滤波:预测运动轨迹 + 检测修正
✅ 匈牙利算法:全局最优匹配
✅ SORT:卡尔曼+匈牙利,260FPS
✅ DeepSORT:SORT+ReID+级联匹配
✅ 运动代价(马氏距离) + 外观代价(余弦距离)
✅ ReID特征提取概念
✅ 端侧AI跟踪方案选型
✅ 4个经典坑点

明日预告

Day 34:Transformer与ViT

自注意力机制、多头注意力、位置编码、Vision Transformer


🔥 关注我,每天解锁一个端侧AI技能!

微信公众号:xxx | 小红书:xxx | CSDN:xxx

评论区打卡,一起坚持100天!


附:小红书图文版

封面标题建议:目标跟踪入门 | SORT/DeepSORT一文搞懂 🎯

P1 --- 封面

标题:目标跟踪

副标题:卡尔曼滤波 / 匈牙利匹配 / SORT / DeepSORT

关键词:目标跟踪 / SORT / DeepSORT / 卡尔曼滤波

P2 --- 检测 vs 跟踪

检测:每帧独立,找到"有什么"

跟踪:关联帧间,给每个物体分配ID

Tracking-by-Detection = 检测器 + 跟踪器

P3 --- 卡尔曼滤波

预测:根据运动模型猜下一帧位置

更新:用检测结果修正预测

预测 + 观测 = 最优估计

跟踪器的"大脑"

P4 --- 匈牙利匹配

左:当前帧检测框

右:上一帧跟踪框

代价:1 - IoU

求全局最优匹配(不是贪心!)

P5 --- SORT → DeepSORT

SORT: 卡尔曼+匈牙利 (260FPS)

DeepSORT: SORT+ReID+级联匹配

ByteTrack: 不需要ReID,纯数据关联

端侧推荐:ByteTrack!

P6 --- 今日作业

手写卡尔曼滤波 + SORT跟踪器

评论区打卡 Day 33/100

标签:#目标跟踪 #SORT #DeepSORT #卡尔曼滤波


CSDN发布提示:CSDN版本建议在卡尔曼滤波部分放预测-更新循环图,在匈牙利匹配部分放二分图匹配示意图,在SORT部分放跟踪流程图,在DeepSORT部分放运动+外观联合代价的示意图。

相关推荐
您^_^13 分钟前
DeepSeek-Harness 升级排障完全指南:三类本地残留逐个拆解
人工智能·windows·个人开发·deepseekharness·deepseekv4pro
旖旎夜光19 分钟前
【AI入门】大模型介绍全解析:从模型、LLM 到提示词与嵌入
人工智能·笔记·python·学习·ai编程
牧羊人.33320 分钟前
计算机视觉基础 第13章 |背景建模与运动目标检测
图像处理·人工智能·目标检测·计算机视觉·目标跟踪
卷无止境20 分钟前
SIE:当一个推理引擎决定把100多个模型装进一个集群里
人工智能·python
阳明山水23 分钟前
概念漂移分类与自适应策略解析
人工智能·深度学习·算法·机器学习·架构
狂师24 分钟前
AI 测试提效 | 别搞万能 Skill,推荐5 个 Agent Skill 串起 UI 自动化执行到报告生成全流程
人工智能·agent·测试
动物园猫26 分钟前
超市空货架目标检测数据集:1,500张图像 | 目标检测
人工智能·目标检测·计算机视觉
卷无止境27 分钟前
Orca:当五个AI程序员同时给你打工
人工智能·python
晴天1631 分钟前
操作系统开发入门-Day34
人工智能