☰
DukeMTMC-VideoReID数据集全解析:从数据加载到评估协议
2026/10/11 9:47:49 网站建设 项目流程

简介:DukeMTMC-VideoReID 是一套面向行人再识别(Video Re-ID)任务的 Python 数据集与代码库,适用于监控场景下跨摄像头行人追踪与身份识别的研究与开发。该数据集源自大型多目标、多摄像头跟踪项目 DukeMTMC,包含 8 个摄像头拍摄的 1444 名行人的 4661 段视频片段,并划分训练、测试、验证集,可帮助评估和对比不同 ReID 算法的性能。压缩包共 43 个文件,其中以 Python 脚本为主(35 个),包含模型定义、训练器、评估指标、数据加载与特征提取等模块,另有 shell 脚本用于快速运行、文本文件提供说明与许可证、一个 pyc 编译文件及少量配置文档,整体仅 38KB,属于轻量但完整的基线代码框架。目前已有 586 人学习。利用包内代码可快速上手行人再识别实验:直接运行 baseline 作为基线模型,或修改参数尝试不同网络结构与损失函数;同时附带日志存储、数据加载和度量学习等模块,便于复现论文结果、调试模型和学习 ReID 完整流程。这套代码与数据集组合为行人检测与跟踪领域的研究者提供了便捷的起点。

1. DukeMTMC-VideoReID:视频行人重识别绕不开的基准数据集

在行人重识别(reID)这个方向摸爬滚打过一段时间后,你会发现 DukeMTMC-VideoReID 是无论如何都绕不过去的一个名字。无论你之前用的是 Market1501 这种单帧数据集,还是想验证自己的视频 reID 模型有多能打,最终都得回到这个标准上来。它把 DukeMTMC 多目标跟踪数据集里的行人轨迹裁剪成了一个个视频片段,每个片段都有明确的身份标签和摄像头编号,直接拿来做视频 reID 的训练和评估。这个 zip 包解压后就是完整的数据集,你不需要自己费劲去原始视频里切帧,省掉了不少脏活。它尤其适合两类人:一类是刚进入 reID 方向、想找一个有挑战性的 demo 来跑通全流程的初学者,另一类是已经做完单帧模型、想看看时序信息到底能带来多少性能提升的算法工程师。

2. 数据集结构先看明白:目录、命名和划分协议

2.1 解压后的目录:images 文件夹里到底放了什么

你下载下来的压缩包解压后,核心大概率是一个名为images的文件夹,所有裁剪好的行人图像都按拍摄顺序堆在里面。每张图的命名是身份ID_摄像头编号_帧号.jpg,比如0002_01_0100.jpg,就代表身份 2 在摄像头 1 下的第 100 帧。这个命名规则是所有后续处理的基础:一旦你把它拆错了,数据处理阶段就会全盘出错。

文件名示例身份ID摄像头编号帧号
0002_01_0100.jpg0002010100
0003_02_0523.jpg0003020523

需要注意,这里的帧号是跟踪轨迹里的帧序号,不是原始视频的绝对时间码。同一个人在同一摄像头下会有一长串连续帧,这串帧就是你模型要处理的“视频序列”。我在拿到数据后第一件事是写个脚本统计一下每个(身份, 摄像头)组合下的帧数,这个数字直接决定后面采样策略怎么设计。

2.2 标注文件:mat 里的 train_list、query_list 和 gallery_list

DukeMTMC-VideoReID 官方给的标注是.mat格式,一般包括train_list.mat、query_list.mat和gallery_list.mat三个文件。用scipy.io.loadmat读出来,你会发现里面是一个 N×1 的 cell 数组,每个 cell 是一个字符串,内容是不带.jpg后缀的图像前缀。

import scipy.io as sio raw = sio.loadmat('train_list.mat')['train_list'] print(raw.shape) # (N, 1) print(raw[0]) # numpy 数组,内容是 '0001_01_0001'

这个raw变量是二维数组,每行是一个 0 维数组,所以要取x[0]才能拿到真正的字符串。我见过不少人在这里直接对raw做循环,结果每个元素都是一个数组,拼接字符串的时候报 type error,浪费了好几分钟。

2.3 官方划分:训练集、查询集、候选集的构造规则

DukeMTMC-VideoReID 的官方划分原则是:训练集和测试集的身份互不重叠,测试集里再拆出查询集和候选集。查询集每个身份会挑出若干视频片段作为 probe,候选集里放所有测试身份的片段作为 gallery。真正做评估时,你的模型需要从 gallery 里找出与某个 query 片段属于同一身份的所有视频序列。

这里经常有一个误解:gallery 里可能包含 query 所对应的那个身份在其它摄像头下的片段,也可能包含同一摄像头下的片段。如果评估时不排除同摄像头,Rank-1 会虚高,因为这个摄像头下的光照、背景几乎一样,匹配太容易了。我通常在构造距离矩阵之前先建一个 mask,把 query 和 gallery 中属于同一身份且同一摄像头的配对直接遮掉,这样得到的指标才是有说服力的。

文件用途典型内容
train_list.mat训练视频序列前缀列表每个前缀对应一帧图像
query_list.mat查询视频序列前缀列表作为 probe
gallery_list.mat候选视频序列前缀列表作为 gallery,与 query 配对评估

实际数字以你下载的 mat 文件为准,算法本身不依赖具体数量,但你要知道自己手里的训练身份大致在一千左右,测试身份也在几百量级,这决定了一个 batch 里能放多少个不同的 P。

3. 把视频序列读进训练管线:数据加载与帧采样

3.1 读取 mat 列表并生成按身份分组的序列索引

这里我一般会把 mat 里的每个前缀拆成(身份, 摄像头, 帧号)三个字段,然后以身份为主键、摄像头为次键,构建一个字典。同一个(身份, 摄像头)下的所有帧按帧号排序后就是一个完整的视频轨迹。

from collections import defaultdict import scipy.io as sio def build_sequences(mat_path, key): raw = sio.loadmat(mat_path)[key] seq_dict = defaultdict(lambda: defaultdict(list)) for row in raw: prefix = str(row[0]) pid, cam, fid = prefix.split('_') seq_dict[pid][cam].append(prefix) # 按帧号排序,保证时间顺序 for pid in seq_dict: for cam in seq_dict[pid]: seq_dict[pid][cam].sort(key=lambda x: int(x.split('_')[2])) return seq_dict

这段代码做了三件事:读取 mat、拆分字段、按帧号排序。sorted key用帧号的整数形式而不是字符串,否则0002和0100会按字典序排错。排序这一步很关键,视频 reID 的序列特征依赖帧之间的时间连续性,乱序的序列会让时序建模完全失效。

3.2 固定长度采样:从长轨迹里抽连续子序列

不同身份的轨迹长度差异很大,短的只有几帧,长的可能有几百帧。训练时为了凑 batch,得把每个轨迹抽成固定长度。常见做法是设一个seq_len = 4或8,从轨迹中随机选一个起始位置,取连续seq_len帧;如果轨迹长度不足,就把最后一帧重复补齐。

import random def sample_frames(seq, seq_len=4): if len(seq) >= seq_len: start = random.randint(0, len(seq) - seq_len) return seq[start:start + seq_len] else: return seq + [seq[-1]] * (seq_len - len(seq))

随机起始采样相当于做了一种时间维度的数据增强,它让模型不依赖固定起点。尾帧重复填充的缺点也很明显:如果某个轨迹只有 2 帧,补出来会有大量重复帧,模型可能学会“这个位置本来就是重复的”这个虚假线索。所以我会给补帧的样本打上一个 mask,或者在 loss 里忽略重复帧部分。

3.3 对接 DataLoader:批量加载序列张量

把上面的逻辑封装成一个 Dataset 类,__getitem__返回一个长度seq_len的图像序列。注意此时图像是单帧读取的,模型输入会多出一个时间维度。

from torch.utils.data import Dataset from PIL import Image import torchvision.transforms as T transform = T.Compose([ T.Resize((256, 128)), T.ToTensor(), T.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]) ]) class DukeVideoDataset(Dataset): def __init__(self, seq_dict, images_dir, seq_len=4): self.items = [] for pid, cam_dict in seq_dict.items(): for cam, seq in cam_dict.items(): self.items.append((pid, cam, seq)) self.images_dir = images_dir self.seq_len = seq_len def __len__(self): return len(self.items) def __getitem__(self, idx): pid, cam, seq = self.items[idx] frames = sample_frames(seq, self.seq_len) imgs = [transform(Image.open(f'{self.images_dir}/{f}.jpg')) for f in frames] # imgs: (seq_len, 3, 256, 128) return torch.stack(imgs), int(pid)

这段代码里有一个容易翻车的点:int(pid)转换。因为 mat 里的身份字符串是'0001'这种带前导零的形式,如果不转 int,同一个身份'0001'和'1'会被当成两个不同的类。训练时分类层的类别数会莫名翻倍,损失值降不下去。我通常会在建索引时就把pid映射成连续的整数,比如pid2label[pid] = len(pid2label)。

4. 模型评估与 baseline:先把评估协议跑通再谈涨点

4.1 评价指标:mAP 和 Rank-1 对视频 reID 意味着什么

视频 reID 的标准评估和单帧 reID 一致,用的还是 mAP 和 Rank-k。区别在于,query 不再是一张图,而是一个视频片段;gallery 也不再是单张图,而是一整段视频序列。你需要把整个序列喂给模型,得到一条序列级特征。

Rank-1表示最相似的候选序列中,第一个就是同身份的比例。mAP则更看重整体排序质量:如果同身份的视频序列在排序列表里排得更靠前,mAP 就更高。这两个指标能直观反映模型对“时序不变性”的建模能力,因为同一个人的姿态、光照在视频里是动态变化的。

4.2 基线方法:瞬时帧 CNN + 平均池化特征

最简单也最稳妥的 baseline 是:用 ImageNet 预训练的 ResNet50 提取每一帧的特征,然后把序列内所有帧的特征做平均池化,得到一条序列特征。这一步能帮你验证数据加载、评估协议是否正确,也是后续所有花哨模型的对标起点。

def extract_sequence_features(model, data_loader): model.eval() all_feats = [] with torch.no_grad(): for imgs, _ in data_loader: # imgs: (batch, seq_len, 3, 256, 128) b, t, c, h, w = imgs.shape imgs = imgs.view(b * t, c, h, w) feat = model(imgs) # (b*t, d) feat = feat.view(b, t, -1) feat = feat.mean(dim=1) # (b, d) all_feats.append(feat) return torch.cat(all_feats, dim=0)

这里我习惯把序列维度展平到 batch 维度里,一次性跑完全部帧。feat.mean(dim=1)就是时间维度的平均池化。如果你是第一次跑这个数据,建议先用这个 baseline 算出指标,如果 Rank-1 和 mAP 都在一个合理范围,说明你的数据管线没问题;如果指标离谱,大概率是序列长度、图片路径或者标签映射出错了。

4.3 多帧特征聚合的几种常见策略对比

平均池化只是最简单的一种。实际项目中我会在平均池化、最大池化、注意力加权之间做对比,来判断时序信息的价值。下面这张表是我在 DukeMTMC-VideoReID 上常用的几种聚合方式:

聚合方式特点适用场景
平均池化稳定、无额外参数作为 baseline,验证数据与协议
最大池化关注最显著的帧,抗遮挡强轨迹中有严重遮挡或检测残缺
时间注意力给每一帧学习权重需要模型自动筛选模糊帧
BiLSTM / GRU捕捉帧间时序依赖姿态变化剧烈,需要运动信息

注意平均池化对模糊帧特别敏感——如果某一帧严重遮挡,它会拉低整条序列的特征质量。你会看到有些人直接用最大池化,就是因为最大池化只保留下响应最强的那一帧,反而在低质量序列里效果更好。这些策略没有绝对对错,我用标准评估脚本把每种都跑一遍,对比结果就出来了。

5. 避坑与常见问题:五个让我卡住过的具体场景

5.1 mat 文件读出来的字符串带 b'' 前缀

现象:使用str(row[0])后,打印出来是"b'0001_01_0001'",路径拼接时找不到文件。

原因:matlab 的字符串在 Python 里被读成了 bytes 类型,直接转 str 会带上b前缀。

解决:用bytes解码,或者直接用 numpy 的str方法。我统一这么写:prefix = bytes(row[0]).decode('utf-8')。

5.2 查询集与候选集存在同摄像头同身份样本

现象:Rank-1 高得离谱,九成多,但换一个摄像头测试就暴跌。

原因:评估时没有排除“同身份+同摄像头”的匹配对,模型靠背景相似就能蒙对。

解决:在生成测试配对时,对每个 query,先把 gallery 中同一个身份且同一个摄像头的序列全部剔除,再计算距离。注意是“且”,只剔除同摄像头但不同身份的是不对的。

5.3 连续帧文件缺失,但 mat 列表里仍然有记录

现象:加载数据时出现 FileNotFoundError,但文件列表明明存在。

原因:官方标注是基于完整视频轨迹生成的,但下载或解压过程中数据可能不完整,或者 GitHub 版本里本身就有漏帧。

解决:构建序列索引时,先检查图像文件是否真实存在,不存在的帧直接丢弃。我一般会加一个os.path.exists的过滤,同时统计一下每个序列的实际长度,避免后面采样时出错。

5.4 序列长度分布悬殊,batch 里出现大量填充帧

现象:训练 loss 下降很快,但验证集指标很差。

原因:短序列占比多,填充帧占了一半,模型学到的是“重复帧特征应该被平均掉”这种假规律。

解决:把序列长度按分位数划分,对少于seq_len的序列使用相邻帧插值而不是简单重复。另一个办法是在 loss 里给填充帧一个极小权重,让模型关注真正的有效帧。

5.5 身份标签映射出错,分类层类别数虚高

现象:模型不收敛,训练损失反复震荡,分类头输出类别数比预期多几十。

原因:身份字符串没有统一转换成 int,'0001'和'1'被当成不同类别。

解决:从 mat 文件加载列表后,先对身份做一次全局排序,再映射为 0 到 N-1 的连续整数。我用一个字典pid_to_label来保证映射稳定,而且每次训练前固定映射表,避免不同 epoch 之间标签错位。

6. 一个进阶技巧:用拼接特征验证视频信息增益

6.1 把单帧特征与平均特征拼起来训练

当你把 baseline 跑通后,下一步最关心的是“视频里的时序信息到底有没有用”。一个低成本验证方法:对每个视频序列,取中间帧的单帧特征、整个序列的平均特征、整个序列的最大特征,三者拼接起来,重新走一遍评估。如果拼接后的 Rank-1 比单独用平均特征有明显提升,说明帧间的互补信息确实存在。

def extract_combined_feature(model, frames): # frames: (seq_len, 3, H, W) feats = model(frames) # (seq_len, d) mid = feats[len(feats)//2] # 中间帧特征 avg = feats.mean(dim=0) # 平均特征 max_f = feats.max(dim=0).values # 最大特征 return torch.cat([mid, avg, max_f])

6.2 验证流程与阈值判断

具体操作是:先在测试集上分别提取平均特征和拼接特征,计算相同的 cosine 距离矩阵,然后比较两者的 Rank-1 和 mAP。如果提升超过 1 个点,说明时序信息值得你做更复杂的模型;如果几乎没变,不妨先把精力放在数据清洗或更强的单帧特征上。

我做这个实验时一般固定同一个随机种子,保证评估协议完全一致,否则两次结果的差异无法归因。后来每拿到一个新数据集,我都强制先跑一遍这个简单特征拼接实验,用 20 分钟确认数据没问题再开始调模型,省下了不知道多少无效训练的时间。希望这个习惯也能帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询