资讯详情 DukeMTMC-VideoReID数据集全解析:从数据加载到评估协议
📅 2026/10/11 9:47:55
简介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.jpg00020101000003_02_0523.jpg0003020523需要注意这里的帧号是跟踪轨迹里的帧序号不是原始视频的绝对时间码。同一个人在同一摄像头下会有一长串连续帧这串帧就是你模型要处理的“视频序列”。我在拿到数据后第一件事是写个脚本统计一下每个(身份, 摄像头)组合下的帧数这个数字直接决定后面采样策略怎么设计。2.2 标注文件mat 里的 train_list、query_list 和 gallery_listDukeMTMC-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查询视频序列前缀列表作为 probegallery_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(keylambda 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_len4): 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_len4): 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(dim1) # (b, d) all_feats.append(feat) return torch.cat(all_feats, dim0)这里我习惯把序列维度展平到 batch 维度里一次性跑完全部帧。feat.mean(dim1)就是时间维度的平均池化。如果你是第一次跑这个数据建议先用这个 baseline 算出指标如果 Rank-1 和 mAP 都在一个合理范围说明你的数据管线没问题如果指标离谱大概率是序列长度、图片路径或者标签映射出错了。4.3 多帧特征聚合的几种常见策略对比平均池化只是最简单的一种。实际项目中我会在平均池化、最大池化、注意力加权之间做对比来判断时序信息的价值。下面这张表是我在 DukeMTMC-VideoReID 上常用的几种聚合方式聚合方式特点适用场景平均池化稳定、无额外参数作为 baseline验证数据与协议最大池化关注最显著的帧抗遮挡强轨迹中有严重遮挡或检测残缺时间注意力给每一帧学习权重需要模型自动筛选模糊帧BiLSTM / GRU捕捉帧间时序依赖姿态变化剧烈需要运动信息注意平均池化对模糊帧特别敏感——如果某一帧严重遮挡它会拉低整条序列的特征质量。你会看到有些人直接用最大池化就是因为最大池化只保留下响应最强的那一帧反而在低质量序列里效果更好。这些策略没有绝对对错我用标准评估脚本把每种都跑一遍对比结果就出来了。5. 避坑与常见问题五个让我卡住过的具体场景5.1 mat 文件读出来的字符串带 b 前缀现象使用str(row[0])后打印出来是b0001_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 身份标签映射出错分类层类别数虚高现象模型不收敛训练损失反复震荡分类头输出类别数比预期多几十。原因身份字符串没有统一转换成 int0001和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(dim0) # 平均特征 max_f feats.max(dim0).values # 最大特征 return torch.cat([mid, avg, max_f])6.2 验证流程与阈值判断具体操作是先在测试集上分别提取平均特征和拼接特征计算相同的 cosine 距离矩阵然后比较两者的 Rank-1 和 mAP。如果提升超过 1 个点说明时序信息值得你做更复杂的模型如果几乎没变不妨先把精力放在数据清洗或更强的单帧特征上。我做这个实验时一般固定同一个随机种子保证评估协议完全一致否则两次结果的差异无法归因。后来每拿到一个新数据集我都强制先跑一遍这个简单特征拼接实验用 20 分钟确认数据没问题再开始调模型省下了不知道多少无效训练的时间。希望这个习惯也能帮到你。本文还有配套的精品资源点击获取