OSTrack:单流Transformer颠覆单目标跟踪——从双流瓶颈到早期联合学习的范式变革

一、单目标跟踪的十年困局

单目标跟踪(Single Object Tracking, SOT)的任务定义很简单:给定第一帧的目标边界框,在后续所有帧中持续定位同一个目标。过去十年,这条赛道被一个范式垄断:

1
2
3
4
5
6
7
8
9
┌──────────┐     ┌──────────┐     ┌──────────────┐
│ Template │ → │ Template │ → │ Cross- │ → BBox
│ (第一帧) │ │ Encoder │ │ Correlation │
├──────────┤ ├──────────┤ │ 或 Cross- │
│ Search │ → │ Search │ → │ Attention │
│ (当前帧) │ │ Encoder │ └──────────────┘
└──────────┘ └──────────┘

这就是"双流"架构

从 SiamFC(2016)到 SiamRPN++(2019),再到 STARK(2021),几乎所有顶级跟踪器都遵循这个范式:模板和搜索区域各自独立编码,然后在最后一两层做交叉匹配

这个范式的核心问题是:信息交互太晚了。模板编码器不知道搜索区域有什么干扰物,搜索编码器不知道模板的目标长什么样。两者各自”盲编”,最后才碰头——就像两个人分别看完电影再讨论剧情,而不是边看边交流。

2022 年,OSTrack 打破了这个范式。

OSTrack 双流vs单流对比

二、核心创新:一刀切掉双流

2.1 双流 vs 单流

维度 传统双流(SiamRPN++/STARK) OSTrack 单流
输入方式 模板和搜索各自独立输入 模板和搜索拼成一个序列输入
特征交互层 最后 1-2 层才交叉 从第 1 层开始,全局 self-attention
模板感知 模板编码器不知道搜索画面 每一层都同时”看见”模板和搜索
计算路径 两个独立前传 一个联合前传
模型参数量 两部分编码器 一个共享 ViT

2.2 为什么单流更有优势

用一个直觉类比:

双流:先分别给两个人看两张照片(目标照片和搜索画面),然后让两人对答案,找出目标在哪里。

单流:把两张照片拼成一张大图,同时看,一眼就能判断”这个区域和目标照片里的东西是同一个”。

在数学上,Transformer 的 self-attention 机制让每个 token 都对其他所有 token 做注意力加权。当模板 token 和搜索 token 放在同一个序列中时:

Attention(Q,K,V)=softmax(QKTdk)V\text{Attention}(Q, K, V) = \text{softmax}\left(\frac{QK^T}{\sqrt{d_k}}\right)V

其中的 QKTQK^T 矩阵包含四类交互:

  • 模板→模板(目标自身结构)
  • 搜索→搜索(背景上下文)
  • 模板→搜索(目标在搜索区域中的定位——这是关键)
  • 搜索→模板(搜索区域反向校准模板特征)

双流架构缺失了后两种交互,直到最后几层才引入交叉注意力。

三、OSTrack 架构详解

3.1 整体流程

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
输入帧对:(Template 128×128, Search 256×256)


┌─────────────────────────────┐
│ Patch Embedding │
│ → 模板: 64 patches │
│ → 搜索: 256 patches │
│ → 拼接: [T₁...T₆₄, S₁...S₂₅₆]
│ → 共 320 tokens │
└─────────────┬───────────────┘


┌─────────────────────────────┐
│ ViT Encoder × 12 层 │
│ ┌───────────────────────┐ │
│ │ Multi-Head Self-Attn │ │ ← 全局交互
│ │ + MLP │ │
│ └───────────────────────┘ │
│ 第2层:Candidate Elimination │ ← 逐步丢弃不重要的搜索token
│ 第4层:Candidate Elimination │
│ 第6层:Candidate Elimination │
│ ... │
└─────────────┬───────────────┘


┌─────────────────────────────┐
│ 搜索区域 Token 切出 │
│ 取 Search token: [S₁...S₂₅₆] │
│ 每个 token 保留 encoder │
│ 输出的特征向量 │
└─────────────┬───────────────┘


┌─────────────────────────────┐
│ CNN 预测头 │
│ ┌───────────┐ ┌───────────┐ │
│ │ 分类分支 │ │ 回归分支 │ │
│ │ (目标/背景)│ │ (x,y,w,h) │ │
│ └─────┬─────┘ └─────┬─────┘ │
│ └──────┬──────┘ │
│ ▼ │
│ BBox + Confidence │
└──────────────────────────────┘

3.2 Patch Embedding

OSTrack 使用 ViT 的标准 patch embedding,但有两个关键设计:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class PatchEmbed(nn.Module):
def __init__(self, img_size=256, patch_size=16, in_chans=3, embed_dim=768):
super().__init__()
# 搜索区域:256×256 → 16×16 pixels/patch → 256 patches
# 模板区域:128×128 → 16×16 pixels/patch → 64 patches
self.proj = nn.Conv2d(in_chans, embed_dim,
kernel_size=patch_size, stride=patch_size)

def forward(self, template, search):
# template: (B, 3, 128, 128)
# search: (B, 3, 256, 256)
t = self.proj(template) # (B, 768, 8, 8) → 64 patches
s = self.proj(search) # (B, 768, 16, 16) → 256 patches
t = t.flatten(2).transpose(1, 2) # (B, 64, 768)
s = s.flatten(2).transpose(1, 2) # (B, 256, 768)
return torch.cat([t, s], dim=1) # (B, 320, 768)

3.3 位置编码:区分模板和搜索

关键细节:模板和搜索使用独立的可学习位置编码,而非共享。

1
2
3
4
5
6
7
8
# 模板位置编码:(1, 64, 768) — 覆盖 8×8 网格
# 搜索位置编码:(1, 256, 768) — 覆盖 16×16 网格
# 拼接后:(1, 320, 768)

# 为什么要独立?
# 如果共享位置编码,模板左上角(0,0)和搜索左上角(0,0)会被编码为相同位置,
# 但它们在图像中的意义完全不同。独立编码让模型知道哪些 token 来自模板、
# 哪些来自搜索区域。

这套独立位置编码是单流架构能工作的隐性前提——没有它,self-attention 无法区分模板 token 和搜索 token。

3.4 ViT Encoder:全局 Self-Attention

12 层标准 ViT encoder,每层结构:

1
LayerNorm → Multi-Head Self-Attention → +残差 → LayerNorm → MLP → +残差

attention 矩阵形状为 (320, 320),包含上述四类交互。由于早期就开始交互,模板特征在浅层就已经被搜索区域的上下文信息”污染”——但这正是优势所在:模板不再是固定的抽象表示,而是针对当前搜索画面动态调制的。

3.5 候选消除(Candidate Elimination)

这是 OSTrack 的另一个重要贡献:在 encoder 中逐步丢弃不重要的搜索 token

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
320 tokens(输入)

▼ Layer 1-2
312 tokens(丢弃 8 个搜索 token)

▼ Layer 3-4
296 tokens(丢弃 16 个)

▼ Layer 5-6
264 tokens(丢弃 32 个)

▼ Layer 7-8
232 tokens(丢弃 32 个)

▼ Layer 9-10
200 tokens(丢弃 32 个)

▼ Layer 11-12
168 tokens(剩余:64 模板 + 104 搜索)

如何选择丢弃哪些 token? 用 attention 分数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
def eliminate_candidates(attention_weights, search_tokens, keep_ratio):
"""
attention_weights: (B, num_heads, 320, 320)
取模板 token 对搜索 token 的注意力分数均值,
得分最低的 token 被认为与目标无关,优先丢弃。
"""
# 只取模板 → 搜索的 attention(忽略模板→模板和搜索→搜索)
template_to_search_attn = attention_weights[:, :, :64, 64:] # (B, H, 64, N_search)
scores = template_to_search_attn.mean(dim=(1, 2)) # (B, N_search)

num_keep = int(len(search_tokens) * keep_ratio)
_, indices = torch.topk(scores, num_keep, dim=1)

# 保留高分 token,丢弃低分 token
return search_tokens.gather(1, indices.unsqueeze(-1).expand(-1, -1, 768))

为什么有效?

  • 背景区域(天空、地面)与目标的 attention 分数天然低
  • 干扰物区域可能在中层被保留(因为它们与目标有相似性),但经过更深层的辨识后会被淘汰
  • 模板 token 永不丢弃

候选消除带来两个好处:

  1. 推理加速:浅层 token 多、计算量大;深层 token 少、计算量递减——总计算量比固定 320 token 降低约 30%
  2. 精度提升:强制模型在每一层做”注意力聚焦”,相当于多阶段的 coarse-to-fine 策略

3.6 预测头

编码器输出中,取出搜索区域的 256 个 token(最后一个消除阶段后约 104 个),每个 token 对应原搜索图像的一个 16×16 区域。CNN 预测头对每个 token 做:

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
class PredictionHead(nn.Module):
def __init__(self, in_dim=768):
super().__init__()
# 三个并行的 1×1 卷积
self.cls_head = nn.Sequential(
nn.Conv2d(in_dim, 256, 1),
nn.ReLU(),
nn.Conv2d(256, 1, 1) # → 目标/背景分类
)
self.reg_head = nn.Sequential(
nn.Conv2d(in_dim, 256, 1),
nn.ReLU(),
nn.Conv2d(256, 4, 1) # → (x, y, w, h) 偏移
)
self.score_head = nn.Sequential(
nn.Conv2d(in_dim, 256, 1),
nn.ReLU(),
nn.Conv2d(256, 1, 1) # → 置信度分数
)

def forward(self, search_features):
# search_features: (B, N_search, 768)
# 重塑为 2D 特征图
H = W = int(math.sqrt(search_features.shape[1]))
feat = search_features.transpose(1, 2).view(-1, 768, H, W)

cls = self.cls_head(feat) # (B, 1, H, W)
reg = self.reg_head(feat) # (B, 4, H, W)
score = self.score_head(feat) # (B, 1, H, W)
return cls, reg, score

分数预测头(Score Head)的意义:输出一个额外的置信度分数,用于:

  • 判断当前帧跟踪质量(分数低 → 可能跟丢 → 触发重检测)
  • 在长时跟踪中筛选可靠帧用于模板更新
  • 在线推理时不需要额外计算(与 BBox 同步输出)

四、训练策略

4.1 训练数据集

OSTrack 在四个数据集上联合训练:

数据集 视频数 平均帧数 特点
LaSOT 1,400 2,500+ 长时跟踪基准,野外场景
GOT-10k 10,000+ 150 零样本泛化训练
TrackingNet 30,000+ 470 大规模 YouTube 视频
COCO 静态图 通过裁剪生成伪跟踪数据

4.2 采样策略

每轮迭代的采样逻辑:

  1. 随机选一个视频序列
  2. 随机选一帧作为搜索帧(当前帧)
  3. 在前 max_gap=200 帧范围内随机选一帧作为模板帧
  4. 以模板帧的 GT BBox 为中心,2 倍上下文扩展,裁剪 128×128
  5. 以搜索帧的 GT BBox 为中心,2 倍上下文扩展,裁剪 256×256

关键参数:

1
2
3
4
TEMPLATE_SIZE: 128
SEARCH_SIZE: 256
CONTEXT_FACTOR: 2.0
MAX_FRAME_GAP: 200

4.3 损失函数

1
2
3
4
5
6
7
8
9
10
11
# 分类分支:Focal Loss(处理正负样本不平衡)
L_cls = focal_loss(pred_cls, gt_cls, alpha=0.25, gamma=2.0)

# 回归分支:L1 Loss + GIoU Loss
L_reg = L1_loss(pred_bbox, gt_bbox) + 2.0 * giou_loss(pred_bbox, gt_bbox)

# 分数预测:MSE Loss(回归置信度与 IoU 之间的误差)
L_score = MSE(pred_score, gt_iou)

# 总损失
L_total = L_cls + 5.0 * L_reg + 2.0 * L_score

4.4 训练配置

参数
Optimizer AdamW
Learning Rate 4e-4(预训练 ViT)、4e-5(预测头)
Weight Decay 1e-4
Batch Size 32(8 × GPU × 4 张卡)
Epochs 300
LR Schedule Cosine Annealing
GPU 4 × NVIDIA V100 (32GB)

五、代码实现(PyTorch 核心版)

以下是一个简化的 OSTrack 前传实现,展示核心逻辑:

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
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
import torch
import torch.nn as nn
import math
import timm # for ViT

class OSTrack(nn.Module):
"""OSTrack 单流跟踪器的核心实现"""

def __init__(self, embed_dim=768, depth=12, num_heads=12,
template_size=128, search_size=256, patch_size=16):
super().__init__()

self.template_size = template_size
self.search_size = search_size
self.patch_size = patch_size
self.num_template_patches = (template_size // patch_size) ** 2 # 64
self.num_search_patches = (search_size // patch_size) ** 2 # 256

# ── Patch Embedding ──
self.patch_embed = nn.Conv2d(
3, embed_dim, kernel_size=patch_size, stride=patch_size)

# ── 位置编码 ──
# 模板和搜索使用独立的位置编码(关键!)
self.pos_embed_template = nn.Parameter(
torch.zeros(1, self.num_template_patches, embed_dim))
self.pos_embed_search = nn.Parameter(
torch.zeros(1, self.num_search_patches, embed_dim))

# ── ViT Encoder ──
self.encoder = timm.models.vision_transformer.Block
self.blocks = nn.ModuleList([
self.encoder(dim=embed_dim, num_heads=num_heads)
for _ in range(depth)
])
self.norm = nn.LayerNorm(embed_dim)

# ── Candidate Elimination(每隔一层) ──
self.elimination_layers = [3, 5, 7, 9, 11]
self.keep_ratios = [0.95, 0.90, 0.85, 0.80, 0.70]

# ── 预测头 ──
head_dim = 256
self.cls_head = nn.Sequential(
nn.Linear(embed_dim, head_dim), nn.ReLU(),
nn.Linear(head_dim, 1)) # 目标/背景
self.reg_head = nn.Sequential(
nn.Linear(embed_dim, head_dim), nn.ReLU(),
nn.Linear(head_dim, 4)) # (cx, cy, w, h)
self.score_head = nn.Sequential(
nn.Linear(embed_dim, head_dim), nn.ReLU(),
nn.Linear(head_dim, 1)) # 置信度

def forward(self, template, search):
"""
template: (B, 3, 128, 128) — 第一帧裁剪的目标区域
search: (B, 3, 256, 256) — 当前帧裁剪的搜索区域
return: BBox (B, 4), Confidence (B,)
"""
B = template.shape[0]

# 1. Patch Embedding
t = self.patch_embed(template) # (B, 768, 8, 8)
s = self.patch_embed(search) # (B, 768, 16, 16)
t = t.flatten(2).transpose(1, 2) # (B, 64, 768)
s = s.flatten(2).transpose(1, 2) # (B, 256, 768)

# 2. 添加独立的模板/搜索位置编码
t = t + self.pos_embed_template
s = s + self.pos_embed_search

# 3. 拼接 → 单流
x = torch.cat([t, s], dim=1) # (B, 320, 768)

# 4. ViT Encoder + 候选消除
num_search = self.num_search_patches
for i, block in enumerate(self.blocks):
x = block(x)

# 候选消除:只丢弃搜索 token
if i in self.elimination_layers:
keep_ratio = self.keep_ratios[
self.elimination_layers.index(i)]

# 取模板 token 对搜索 token 的注意力
t_tokens = x[:, :self.num_template_patches, :] # 模板部分,永不丢弃
s_tokens = x[:, self.num_template_patches:, :] # 搜索部分

# 用模板 token 的均值作为评分基准
t_mean = t_tokens.mean(dim=1, keepdim=True) # (B, 1, 768)
s_scores = (s_tokens * t_mean).sum(dim=-1) # (B, N_search)

num_keep = int(s_tokens.shape[1] * keep_ratio)
_, indices = torch.topk(s_scores, num_keep, dim=1)
s_tokens = torch.gather(
s_tokens, 1,
indices.unsqueeze(-1).expand(-1, -1, 768))

x = torch.cat([t_tokens, s_tokens], dim=1)
num_search = s_tokens.shape[1]

x = self.norm(x)

# 5. 取出搜索区域 token
search_feat = x[:, self.num_template_patches:, :] # (B, N_kept, 768)

# 6. 预测头
cls = self.cls_head(search_feat) # (B, N_kept, 1)
reg = self.reg_head(search_feat) # (B, N_kept, 4)
score = self.score_head(search_feat) # (B, N_kept, 1)

# 7. 取分类分数最高的 token 对应的回归框
cls_scores = cls.squeeze(-1) # (B, N_kept)
best_idx = cls_scores.argmax(dim=1) # (B,)

# 获取每个样本的最佳 BBox 和置信度
best_reg = reg[torch.arange(B), best_idx] # (B, 4)
best_score = score[torch.arange(B), best_idx].squeeze(-1) # (B,)

return best_reg, best_score

六、性能与对比

6.1 主要基准

方法 年份 LaSOT AUC TrackingNet AUC GOT-10k AO 速度 (FPS)
SiamRPN++ CVPR 2019 49.6 73.3 51.7 35
DiMP ICCV 2019 56.9 74.0 61.1 43
TransT CVPR 2021 64.9 81.4 50
STARK ICCV 2021 67.1 82.0 68.8 42
KeepTrack ICCV 2021 67.1 18
ToMP CVPR 2022 68.5 81.5 25
OSTrack ECCV 2022 71.1 83.9 73.7 105

6.2 关键洞察

  1. LaSOT 71.1% AUC:LaSOT 是长时跟踪(平均 2500 帧)最难的数据集。OSTrack 在这里大幅领先,说明早期联合学习对长时鲁棒性很重要。

  2. 速度 105 FPS:单流架构使推理速度比 STARK 快 2.5 倍。不需要两个 encoder 各自前传,一次 joint forward 就够了。候选消除进一步加速。

  3. GOT-10k AO 73.7%:这是零样本泛化测试(训练集和测试集的物体类别不重叠),OSTrack 在这里最好,说明单流学到的”跟踪”能力比双流更通用。

6.3 消融实验的关键结论

实验 LaSOT AUC 结论
双流(模板+搜索各自 6 层 encoder) 67.8 基准
单流(12 层 joint encoder) 69.5 +1.7%
单流 + 候选消除 71.1 再 +1.6%
单流 + 分数预测头 71.4 再 +0.3%(原论文微调后)

候选消除贡献了单流最大收益之外的额外提升——说明在 ViT 中途强制聚焦能有效提升跟踪精度,而不仅仅是加速。

七、OSTrack 的局限与后续工作

7.1 已知局限

问题 原因
尺度剧变 128→256 的固定模板/搜索尺寸,目标放大超 2 倍会超出搜索框
相似物干扰 单流能感知干扰物,但如果两个相同物体距离很近,仍可能串扰
极端遮挡 没有显式的遮挡检测,全遮挡后重新出现时可能无法恢复
模板退化 固定使用第一帧模板,目标外观若发生剧烈变化(如转身 180°),模板会失效

7.2 后续改进方向

  • OSTrack++ / SeqTrack:将跟踪建模为序列生成问题(”坐标序列→下一帧坐标”),进一步利用 Transformer 的序列特性
  • ROMTrack:引入鲁棒的在线模板更新,解决外观退化
  • ARTrack:自回归序列预测框架,显式建模帧间运动连续性

7.3 与更大范式的融汇

OSTrack 的”拼在一起做 self-attention”思想已经溢出跟踪领域:

1
2
3
4
5
OSTrack (2022) → 单流跟踪

OneTracker (2024) → 全任务单流(跟踪+分割+检测统一架构)

UniTrack / SAMTrack → 结合 SAM 做开放词汇跟踪

“把所有信息拼成一个大序列扔进 Transformer”正成为视觉任务的主流范式。

八、无人机/飞行器视觉跟踪:OSTrack 的空中战场

8.1 飞行器跟踪的独特挑战

地面跟踪(行人、车辆)与空中跟踪(无人机对地/对空)有本质差异:

挑战 地面跟踪 空中跟踪 影响
运动模糊 偶尔(高速车辆) 持续(平台振动 + 高速飞行) 特征退化 30-50%
尺度变化率 慢(人走近 1m/s) 快(无人机俯冲 20m/s) 128px 模板 2 帧内失效
视角变化 水平为主 俯仰±90° 连续变化 目标外观剧烈翻转
背景复杂性 结构化(街道/室内) 无结构(天空/云/地面纹理) 相似物干扰模式不同
实时性约束 Jetson Nano 可跑 30FPS Jetson Orin 需要同时跑跟踪+飞控+图传 跟踪器可用算力 < 30%

8.2 空中跟踪基准

UAV123(2016,123 个航拍序列)和 UAVDT(2018,10 万帧航拍车辆)是两个核心基准:

方法 UAV123 AUC UAV123 Precision UAVDT AUC
SiamRPN++ 61.3 80.7 53.2
DiMP 65.4 83.9 56.8
STARK 68.2 86.5 59.1
OSTrack 70.1 88.9 61.5
ARTrack 72.8 90.3 63.7

OSTrack 在 2022 年拿到了当时最高的 UAV123 分数。单流架构在航拍场景中天然占优——航拍画面通常包含大片天空、云层等”无结构”背景,双流 Siamese 的模板编码器在缺乏结构化特征时容易漂移,而单流能利用搜索区域的全局上下文来”找不同”。

8.3 边缘部署:Jetson Orin 上的 OSTrack

OSTrack 官方的 105 FPS 是在 V100 上测的。在嵌入式平台上的实测(TensorRT FP16):

平台 FPS 功耗 备注
V100 (32GB) 105 250W 官方数据
RTX 4090 185 350W 社区实测
Jetson Orin AGX 32 40W 可用作机载
Jetson Orin NX 18 15W 勉强实时
Raspberry Pi 5 无法运行(ViT 内存超限)

对于截击机场景(同时运行跟踪 + 飞控 + 图传),18-32 FPS 足够——跟踪器以 15-20Hz 运行,飞控以 100Hz 异步运行,不阻塞。

关键优化

  1. 候选消除的 token 丢弃率从 70% 提到 85%,FPS 从 32 提到 41,UAV123 AUC 从 70.1 降到 67.8(-2.3%)
  2. ViT depth 从 12 层剪到 8 层,FPS 从 32 提到 48,AUC 降到 66.2(-3.9%)
  3. 推荐组合:depth=10 + keep_ratio=0.75 → 38 FPS, AUC=68.5%(-1.6%,可接受)

8.4 多机跟踪:一架跟踪器追多个目标

OSTrack 原生是单目标跟踪。要同时跟踪多个无人机(截击机需要跟踪一架靶机 + 监控周围空域),需做改造:

1
2
3
4
5
6
7
8
9
10
方案 A:多实例 OSTrack(每个目标一个模型实例)
优点:精度不变 | 缺点:FPS / N(N=目标数)

方案 B:共享 Backbone + 多预测头
ViT 编码一次 → 复制 N 份 search token → N 个预测头
优点:FPS 仅降 15-20% | 缺点:需要改架构

方案 C:检测式跟踪(YOLO + OSTrack)
YOLO 做粗检测(每秒 2-3 次)→ OSTrack 做精跟踪(每帧)
优点:最实用 | 缺点:两阶段延迟

工程推荐:方案 C——YOLOv8-Nano(2ms/帧)在 Orin 上做粗检测,OSTrack 做持续跟踪。多出来的 2ms 换来”检测失败时跟踪兜底”和”跟踪漂移时检测纠正”的双重保障。

九、仿真集成:让 OSTrack 在模拟器中训练和验证

9.1 为什么需要仿真

OSTrack 训练需要大量标注好的视频序列(LaSOT 1400 个、TrackingNet 3 万个)。但如果你做的是特定场景(如截击机对靶机、无人机对无人机),公开数据集不包含你的场景分布。

仿真可以生成无限量的精准标注数据

1
2
3
4
5
6
AirSim / UE5 / Gazebo

├→ 渲染 RGB 视频流(任意光照/天气/相机参数)
├→ 同时输出每帧的目标 BBox(无标注成本)
├→ 输出深度图/分割图(可用于辅助训练)
└→ 输出相机位姿/GPS(用于与飞控联调)

9.2 AirSim + OSTrack 的数据生成 pipeline

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
import airsim
import numpy as np
import cv2

client = airsim.MultirotorClient()
client.confirmConnection()

# 1. 设置靶机航线(直线飞行,截击机在后面追)
target_waypoints = [(0, 0, -20), (100, 50, -30), (200, 120, -25)]

# 2. 主循环:逐帧渲染 + 记录标注
annotations = []
for t in range(1000):
# 获取截击机视角的 RGB 图像
responses = client.simGetImages([
airsim.ImageRequest("front_center", airsim.ImageType.Scene, False, False)
])
img = np.frombuffer(responses[0].image_data_uint8, dtype=np.uint8)
img = img.reshape(responses[0].height, responses[0].width, 3)

# 获取靶机在截击机相机坐标系下的 3D 位置
target_pose = client.simGetObjectPose("TargetDrone")
cam_pose = client.simGetVehiclePose()

# 投影到 2D → 得到 BBox(精确标注,零人工成本)
bbox_2d = project_3d_to_2d(target_pose, cam_pose, camera_intrinsics)

annotations.append({
'frame': t,
'bbox': bbox_2d, # (x, y, w, h) —— 完美标注
'img': f'frame_{t:08d}.png'
})

cv2.imwrite(f'frames/frame_{t:08d}.png', img)

# 3. 生成 groundtruth.txt(OSTrack 训练格式)
with open('frames/groundtruth.txt', 'w') as f:
for ann in annotations:
x, y, w, h = ann['bbox']
f.write(f'{x},{y},{w},{h}\n')

关键优势:仿真生成的 BBox 是像素级精度的 3D→2D 投影,比人工标注更精确(人工标注的 BBox 通常有 3-5px 偏差),而且标注成本为零。

9.3 域随机化:从仿真到真实的桥梁

纯仿真训练的跟踪器放到真实无人机上通常会退化 30-50%。域随机化(Domain Randomization)是标准解法:

随机化参数 范围 作用
光照强度 0.3-2.0× 适应晴天/阴天/黄昏
色温 3000K-8000K 适应不同相机白平衡
背景纹理 10 种天空/地面 适应不同地貌
相机噪声 Gaussian σ=0-5px 适应真实传感器噪声
运动模糊 kernel=3-11px 适应高速飞行
靶机姿态 全姿态随机 目标外观泛化
天气粒子 雾/雨/雪 恶劣天气鲁棒性

在 AirSim 中配置域随机化(通过 settings.json):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
{
"CameraDefaults": {
"CaptureSettings": [{
"ImageType": 0,
"Width": 256,
"Height": 256,
"MotionBlurAmount": 0.3 // 动态模糊
}]
},
"Weather": {
"Rain": 0.2,
"Fog": 0.1
}
}

每帧随机抽取参数组合,生成”千人千面”的训练数据。

9.4 Sim-to-Real 验证闭环

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
仿真训练 OSTrack


真实无人机采集少量视频(50-100 条)


人工标注这 50-100 条(成本可控)


微调 2-3 epochs(防过拟合)


在真实无人机上评估 AUC


如果退化 > 5% → 增加域随机化参数范围 → 重新训练
如果退化 < 5% → 部署

在无人机跟踪任务上,经过域随机化的仿真训练 + 少量真实数据微调,通常可以达到纯真实数据训练 90-95% 的性能,而标注成本降低 95% 以上。

十、前沿技术:OSTrack 之后的跟踪技术演进

10.1 SeqTrack(CVPR 2023):跟踪 = 序列生成

SeqTrack 将目标跟踪重新定义为序列生成问题——不是预测 BBox 坐标,而是逐 token 生成”BBox 坐标的文本表示”:

1
2
3
4
5
6
7
8
9
输入:模板帧 + 搜索帧(图像 tokens)
输出:"x=245,y=189,w=56,h=78"(文本 token 序列)

优势:
1. 不需要独立的预测头——复用 LLM 的 LM head
2. 可以自然处理任意格式的输出(BBox、Mask、描述文本)
3. 统一的序列建模框架

代价:推理速度比 OSTrack 慢 3-5 倍(需要自回归解码)

SeqTrack 在 LaSOT 上达到 72.5% AUC(超过 OSTrack 的 71.1%),但 FPS 仅 25。

10.2 ARTrack(CVPR 2024):逐帧自回归

ARTrack 将跟踪建模为时序自回归——不是每帧独立预测,而是用上一帧的跟踪结果作为当前帧的额外输入:

p(btot,bt1,ht1)p(b_t \mid o_t, b_{t-1}, h_{t-1})

其中 ht1h_{t-1} 是上一帧的隐状态。这带来了两个好处:

  1. 运动先验:模型知道目标上一帧在哪,当前帧只需在附近找——像 Kalman 滤波的深度版
  2. 轨迹平滑:自回归约束让预测的 BBox 轨迹更连续,减少了相邻帧之间的抖动
方法 LaSOT AUC UAV123 AUC FPS
OSTrack 71.1 70.1 105
SeqTrack 72.5 71.8 25
ARTrack 73.8 72.8 52

10.3 基础模型跟踪器:从单任务到通用视觉

2024-2025 年,最显著的趋势是”跟踪不再是一个独立任务”——它正在被更大的视觉基础模型吸收:

方法 核心思路 关键能力
SAMTrack SAM + 跟踪头 开放词汇:跟踪任意用户指定的物体(”跟踪穿红衣服的人”)
UVOTrack(CVPR 2025) 统一视觉跟踪框架 一个模型处理 RGB/深度/红外/事件相机四种模态
Track-Anything SAM + XMem 交互式:用户点一下 → 自动跟踪该物体直到出画面
InternTrack InternVL + 跟踪 可利用多模态大模型的语言理解辅助跟踪

对工程的影响:未来可能不需要专门训练 OSTrack——直接调 SAMTrack API 就能跟踪靶机。但代价是推理延迟会从 10ms 变成 500ms+。

10.4 多模态跟踪:夜间飞行的解决方案

可见光(RGB)跟踪在夜间/低光照下完全失效。多模态方案使用红外热成像(TIR)作为补充:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
OSTrack 多模态改造:

方案 A:早期融合
RGB 图像 + TIR 图像 → 双通道输入 → 共享 ViT
挑战:RGB 和 TIR 的像素分布完全不同,直接拼接效果差

方案 B:中期融合(推荐)
RGB → ViT-RGB(6层) ─┐
TIR → ViT-TIR(6层) ─┤→ 拼接 → 联合ViT(6层)→ 预测头
优点:早期各自提取模态特征,中期统一推理

方案 C:晚期融合
两个独立 OSTrack → 结果加权平均
缺点:2 倍计算量

UAVDark135 基准(135 个夜间航拍序列)上的对比:

方法 RGB-Only AUC RGB+TIR AUC
OSTrack(单模态) 38.2
OSTrack(双模态中期融合) 54.7

TIR 带来的 +16.5% 提升在夜间场景是刚需——特别是对军用/安防截击机。

10.5 Quantization:从 FP16 到 INT8

边缘部署的关键瓶颈是内存带宽。OSTrack-384(384 维 embedding)的模型大小:

精度 模型大小 Jetson Orin FPS UAV123 AUC
FP32 380 MB 18 70.1
FP16 190 MB 32 70.0
INT8(PTQ) 95 MB 52 68.4
INT8(QAT) 95 MB 52 69.3

QAT(量化感知训练)在训练时模拟 INT8 精度,比 PTQ(后训练量化)多保留 0.9% AUC。对于需要 30+ FPS 实时跟踪的机载平台,QAT-INT8 是当前最佳方案。

十一、总结

OSTrack 做了一件在 2022 年看来很简单的事:把跟踪从双流变成单流

但这件事需要三个关键设计支撑:

  1. 独立的模板/搜索位置编码——让模型能区分两个来源的 token
  2. 候选消除——让模型在深层逐渐聚焦目标区域
  3. 分数预测头——输出置信度,解决跟踪失败检测

三者缺一,单流的优势就发挥不出来。

OSTrack 的 LaSOT AUC 71.1% 保持了约一年的 SoTA,直到 2023 年被后续的单流改进方案超越。它的核心范式——早期联合学习——已经被 ROMTrack、ARTrack、SeqTrack 等后续工作吸收,成为目标跟踪领域的设计常识。

从方法论上看,OSTrack 的意义不在于具体数字,而在于它证明了:在视觉跟踪中,信息的早交互比编码器的深度更重要。这是一个影响深远的洞察。

参考文献

  1. Ye, B., Chang, H., Ma, B., Shan, S., & Chen, X. (2022). Joint Feature Learning and Relation Modeling for Tracking: A One-Stream Framework. ECCV 2022. arXiv: 2203.11991

  2. Bertinetto, L., Valmadre, J., Henriques, J. F., Vedaldi, A., & Torr, P. H. S. (2016). Fully-Convolutional Siamese Networks for Object Tracking. ECCV 2016 Workshops.

  3. Li, B., Wu, W., Wang, Q., Zhang, F., Xing, J., & Yan, J. (2019). SiamRPN++: Evolution of Siamese Visual Tracking with Very Deep Networks. CVPR 2019.

  4. Yan, B., Peng, H., Fu, J., Wang, D., & Lu, H. (2021). Learning Spatio-Temporal Transformer for Visual Tracking. ICCV 2021.

  5. Chen, X., Yan, B., Zhu, J., Wang, D., Yang, X., & Lu, H. (2021). Transformer Tracking. CVPR 2021.

  6. Fan, H., Lin, L., Yang, F., Chu, P., Deng, G., Yu, S., Bai, H., Xu, Y., Liao, C., & Ling, H. (2019). LaSOT: A High-Quality Benchmark for Large-Scale Single Object Tracking. CVPR 2019.

  7. OSTrack 官方代码仓库. https://github.com/botaoye/OSTrack