简介基于ViT的车辆重识别系统项目源码面向计算机视觉方向开发者与车辆识别研究者解决跨摄像头场景下车辆身份匹配与检索问题。资源将Vision Transformer大模型迁移至纯视觉下游任务完整覆盖车辆检测、多目标跟踪、特征提取与向量检索等核心流程并在VeRi-776数据集上达到mAP 84.5、Rank-1 97.3的指标同时对比VehicleID与VeRi-776不同训练数据下的性能差异便于按场景选择模型。包内含3个文件包括inscode工程入口、html交互页面与gitignore配置压缩包仅7KB轻量易部署。已有108人学习。读者可从中掌握ViT模型在车辆ReID任务中的优化细节如ONNX转换、GUI设计及多线程处理思路为复现实验或进一步研发智能交通、安防监控类应用提供可参考的源码框架。1. 基于ViT的车辆重识别换了个摄像头同一辆车为什么像另一辆做园区车辆管理或卡口过车分析的开发者大概都遇到过这种场景一辆黑色SUV从入口正面抓拍系统识别出“车辆A”十分钟后它出现在出口换成侧面俯拍角度算法直接判成了另一辆车——因为在卷积网络眼里车灯、轮毂、前脸这些局部特征全对不上了。这就是车辆重识别Vehicle ReID要解决的问题不依赖车牌只靠车身外观让系统在跨摄像头、跨时间、跨视角的图片里认出“这是同一辆车”。传统CNN在这个任务上存在天然的视角短板而基于ViTVision Transformer的方案因为能把整辆车当作全局序列来建模正在成为这条技术路线的主流。这篇文章适合已经有PyTorch基础、想从行人ReID迁移到车辆方向或者正准备自建车辆检索系统的开发者。我会按一套不花哨、能落地的实现路径讲清楚为什么选ViT、数据怎么组织、训练参数怎么设、以及最容易翻车的五个坑。2. 为什么不是CNN车辆重识别里的视角剧变与局部失效2.1 CNN做车辆比对的三个死穴遮挡、视角、同款车行人重识别里CNN表现不错是因为行人不是刚体衣服纹理、背包、体态这些局部线索在大多数摄像头视角下能保持稳定。但车辆是刚体一面是车头、一面是车尾、一面是侧面视觉差异比行人跨视角大得多。CNN基于局部感受野做特征提取默认依赖的是“哪些局部区域长得像”这直接导致三个死穴第一遮挡。车头被绿化带挡掉一半车灯特征丢失CNN很容易把特征注意力浪费在树叶上。第二视角剧变。正面图的进气格栅和背面图的尾灯在像素层面没有任何可对齐的局部结构卷积核扫到的东西完全变了。第三同款车。同品牌同颜色同配置的批量车辆局部纹理几乎一样CNN会把这辆和另一辆拉到极近的特征距离——而这恰恰是重识别最致命的错误。所以在车辆ReID里评判一个骨干网络好不好关键不是它在ImageNet分类上多准而是它能不能在丢掉局部对齐的情况下仍然抓住“这是同一辆车”的全局证据。CNN的归纳偏置在这里反而成了限制卷积的局部感受野决定了它天生更信任小块纹理而不是跨区域的整体比例关系。这也是为什么早期车辆ReID方案普遍效果不稳换一个数据集指标就大幅波动——本质是模型学到的判别依据不具备跨视角鲁棒性。2.2 ViT的全局关联把一辆车当一篇文章读ViT的做法是把一张224×224的图切成196个16×16的patch每个patch展平后映射成一个token然后像处理句子一样送入Transformer编码器。self-attention让每个token在每一层都能和全图所有其他token计算关联权重于是模型天然具备建模“车头与车尾的几何关系”“车窗位置与车身轮廓的比例”这类全局语义的能力。对车辆重识别来说这意味着即使图像的局部区域变化剧烈比如正面图到侧面图注意力机制仍然可以追踪到“这辆车的轴距比例、车窗与车身的相对位置、整体轮廓线条”这些跨视角稳定的特征。另一个关键优势是ViT对遮挡的容忍度。CNN遇到遮挡时特征图上被遮挡区域直接“坏掉”而Transformer可以通过注意力权重主动压低遮挡patch的贡献从其他可见patch推断整体结构。实践中你会发现ViT提取的车辆特征在query图和gallery图视角差异大时依然能保持较高的余弦相似度而CNN特征往往断崖式下跌。当然ViT也有代价参数量大、训练对学习率更敏感、推理速度比同规格CNN慢。但这些代价在“识别准确率优先、推理可优化”的车辆ReID场景里是可接受的。2.3 先用一行代码感受ViT特征timm加载与推理在动手训练之前我强烈建议你先做一次“手感测试”用timm加载一个预训练ViT直接抽取两张车辆图像的特征算一下余弦相似度。这一步能让你直观理解ViT输出的是什么也能提前发现预处理上的问题。import timm import torch from PIL import Image from torchvision import transforms # 加载 ViT-Base/16去掉分类头输出 768 维全局特征 model timm.create_model(vit_base_patch16_224, pretrainedTrue, num_classes0) model.eval() tf transforms.Compose([ transforms.Resize((224, 224)), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]), ]) def get_feat(img_path): img Image.open(img_path).convert(RGB) x tf(img).unsqueeze(0) # 加 batch 维 with torch.no_grad(): feat model(x) # 形状 [1, 768] return torch.nn.functional.normalize(feat, dim1) # 同一辆车两个视角 vs 两辆不同车 feat_a get_feat(car_front.jpg) feat_b get_feat(car_side.jpg) feat_c get_feat(other_car.jpg) print(same car cosine:, (feat_a * feat_b).sum().item()) print(diff car cosine:, (feat_a * feat_c).sum().item())代码逻辑num_classes0让timm只返回特征、不返回分类概率normalize做L2归一化后面所有训练和检索都保持一致。参数说明vit_base_patch16_224表示ViT-Base、patch size 16、输入分辨率224输出维度768。如果同样的两张同车图片算出来的相似度反而不如不同车优先检查预处理参数是否和预训练要求一致——这是最常见的手滑跟你模型好坏没关系。这一小段代码也是后面整套系统的地基训练、底库检索、评估全部复用同一个特征提取逻辑。3. 从零搭车辆重识别系统环境、数据组织与最小训练代码3.1 环境与硬件一张消费级显卡就能起步车辆ReID的训练对硬件要求没有想象中高。ViT-Base加上224分辨率输入单卡显存8GB以上就能跑得动显存小就把batch size调小一点或者把分辨率降到160代价是精度会有几个点的损失。我比较推荐用conda创建一个独立环境把依赖一次性装干净避免后面被乱七八糟的版本问题打断。下面这份环境清单是我在多个项目上验证过的组合不需要追求最新版本稳定优先。组件推荐选型说明Python3.8conda 环境隔离PyTorch2.x训练主框架动态图灵活timm最新稳定版加载 ViT 预训练权重torchvision与 PyTorch 匹配数据增强和图像变换faiss-cpu最新稳定版评估和部署阶段检索用装好环境后先用第2.3节那段特征提取代码跑通一遍确认timm能正常下载预训练权重、显卡驱动没问题再进入数据集准备。这一步虽然简单但能帮你把“环境问题”和“算法问题”两个变量分开——否则后面训练报错时你根本分不清是环境坏了还是代码坏了。3.2 数据组织按“车ID摄像头ID”划分而不是按文件夹划分车辆重识别的数据组织方式直接决定评估指标的真假。常见的做法是准备三个目录train用于训练query是待检索的车辆图片gallery是底库图片。query里的每辆车要在gallery里存在同ID的其他摄像头抓拍图这样评估时才能算“跨摄像头识别率”。目录结构大致如下data/ ├── train/ │ ├── 0001_c1_001.jpg │ ├── 0001_c2_002.jpg │ ├── 0002_c1_003.jpg │ └── ... ├── query/ │ ├── 0012_c3_004.jpg │ └── ... └── gallery/ ├── 0012_c1_005.jpg └── ...注意图片命名里的约定前缀是车辆ID中间是摄像头ID后缀是帧序号。车辆ID是你要预测的目标摄像头ID则用于后续的评估划分。这里有个很容易踩的坑如果训练集和测试集来自同一个摄像头的邻近帧模型其实在“记场景”而不是“认车辆”指标会虚高到没法看。我一般会在数据准备阶段就按摄像头ID做一份映射表确保train里出现过的摄像头画面不会同时出现在gallery里。做到这一点后面评估出来的mAP才有参考价值。至于标注文件直接用“图片名,车辆ID”的纯文本即可不需要搞复杂的数据库。3.3 最小可跑训练脚本让ViT学会区分“车ID”有了数据和预训练ViT接下来要做的就是把分类头接到ViT特征上让模型学会区分训练集里的每一辆车。下面是一个极简但完整的模型定义和数据加载代码去掉了一切多余封装方便你跑通后再逐步加功能import torch import torch.nn as nn from torch.utils.data import Dataset from torchvision import transforms from PIL import Image import timm class VehicleDataset(Dataset): def __init__(self, img_dir, ann_file): self.samples [] with open(ann_file, r) as f: for line in f: img_name, vid line.strip().split(,) self.samples.append((f{img_dir}/{img_name}, int(vid))) self.tf transforms.Compose([ transforms.Resize((224, 224)), transforms.RandomHorizontalFlip(), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]), ]) def __len__(self): return len(self.samples) def __getitem__(self, idx): path, vid self.samples[idx] img Image.open(path).convert(RGB) return self.tf(img), vid class ViTReID(nn.Module): def __init__(self, n_classes, backbonevit_base_patch16_224): super().__init__() # 预训练 ViT 作骨干输出 768 维特征 self.backbone timm.create_model(backbone, pretrainedTrue, num_classes0) self.fc nn.Linear(768, n_classes) # 车辆 ID 分类头 def forward(self, x, return_featFalse): feat self.backbone(x) logits self.fc(feat) if return_feat: return logits, feat return logits # 训练集有 100 个车辆 ID按实际数据修改 model ViTReID(n_classes100)代码逻辑VehicleDataset从纯文本标注里读图返回224×224的图像和车辆IDViTReID把预训练ViT当作特征提取器后面接一个线性分类头。注意一个关键设计训练时用logits算分类损失但真正用于重识别检索的是feat不是分类输出。参数说明n_classes必须等于训练集里不同的车辆ID数量不是图片数量RandomHorizontalFlip对车辆ReID是安全的数据增强车辆左右翻转不影响身份但不要加RandomRotation那会破坏车辆姿态的语义。如果你的显卡显存小于8GB把Resize改成(160, 160)同时vit_base_patch16_224要换成vit_base_patch16_224对应的timm.create_model参数里改img_size吗不必ViT的位置编码是预训练好的强行改输入分辨率会导致位置编码失配。更简单的是就用224分辨率、batch size调到16甚至8。跑通之后再考虑提分辨率。4. 训练配置与损失函数让全局特征学会区分“同一辆车”4.1 损失函数ID分类 三元组损失的组合逻辑车辆重识别训练的核心是让模型输出的特征满足两个性质同一个车辆ID的图片特征要尽量近不同车辆ID的特征要尽量远。单用分类交叉熵损失模型会偏向“记住类别”学出来的特征往往判别性不够单用三元组损失训练又容易震荡不收敛。所以实践中普遍的做法是把两者组合起来分类头负责稳定收敛三元组损失负责拉大特征间距。下面是基于Batch Hard策略的三元组损失实现以及它在整个训练循环里的用法import torch import torch.nn.functional as F def batch_hard_triplet(feat, label, margin0.3): # feat: [N, D] 已做 L2 归一化label: [N] dist torch.cdist(feat, feat, p2) # 两两欧氏距离矩阵 same (label[:, None] label[None, :]).float() # 同车为 1 pos_dist dist * same # 只保留正样本对 neg_dist dist (same * 1e6) # 盖掉正样本对 # 对每个 anchor 取最远的正样本和最近的负样本 hardest_pos pos_dist.max(dim1).values hardest_neg neg_dist.min(dim1).values loss F.relu(hardest_pos - hardest_neg margin).mean() return loss代码逻辑cdist算出batch内所有样本两两距离same矩阵标记哪些样本对属于同一辆车对每个anchor找“最不像”的同类hardest positive和“最像”的异类hardest negative然后要求前者比后者近至少margin。参数说明margin0.3是常用起点。如果损失一直为0说明样本对太容易区分或者margin过大降到0.1试试如果损失降不下去说明margin太小或batch内每个ID的样本数太少。还有一个隐性要求每个ID在同一个batch里至少要有2张图所以dataloader的采样器要按ID分组采样不能纯随机——这往往是新手写三元组损失不收敛的根源。4.2 三个必调参数patch size、学习率、re-ranking开关第一个参数是patch size。ViT默认是16×16的patch划分这个值是预训练时定死的不要轻易改。在车辆重识别场景里更值得调的是输入分辨率224是起步显存够就上320或384细粒度特征车窗轮廓、后视镜形状会有可见提升。patch改成32会丢失太多空间细节在车辆这种细粒度任务上基本没人用。第二个参数是学习率。预训练骨干和随机初始化的分类头对学习率的耐受度完全不同常见做法是把它们拆成两组参数分别设置骨干3e-5到1e-4分类头1e-3。这里的经验是骨干学习率一旦超过1e-4预训练特征很容易被冲垮训练曲线表现为loss先降后暴升——这时候没有后悔药只能重新加载预训练权重再来。第三个参数容易被忽略re-ranking。它指的是检索出Top-K候选后利用候选列表之间的相互邻近关系做二次重排能显著提升mAP。但新手在评估时要特别注意相关论文的指标是否开了re-ranking——因为开与不开mAP可能差5到10个点。训练阶段不需要re-ranking部署阶段如果对延迟不敏感可以考虑但如果你要做实时检索建议直接关掉把精力放在特征本身的判别力上。这是我在项目里最常说的一句话先别急着用re-ranking刷指标想清楚你的系统到底哪一步是瓶颈。4.3 训练循环与学习率调度别让预训练权重被冲垮下面是一段最小可用的训练循环重点在于参数字典的分组和损失组合其他细节日志、模型保存按你自己的工程习惯补全即可optimizer torch.optim.AdamW([ {params: model.backbone.parameters(), lr: 3e-5}, # 骨干小步微调 {params: model.fc.parameters(), lr: 1e-3}, # 分类头正常训练 ]) scheduler torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max60) for epoch in range(60): model.train() for x, label in dataloader: x, label x.cuda(), label.cuda() logits, feat model(x, return_featTrue) feat F.normalize(feat, dim1) ce F.cross_entropy(logits, label) trip batch_hard_triplet(feat, label, margin0.3) loss ce 1.0 * trip optimizer.zero_grad() loss.backward() optimizer.step() scheduler.step()代码逻辑return_featTrue同时拿到分类logits和特征向量feat在进三元组损失前做L2归一化保证距离度量的一致性和稳定性损失是两个loss直接相加权重都是1.0。参数说明T_max60要和总epoch数一致余弦退火会把学习率从初始值平滑降到0如果你只打算训40轮就把两个数值改成40。三元组损失的权重不是固定的如果你的数据集每个ID样本很少少于4张三元组损失容易抖动可以把权重降到0.5用分类损失稳住大局。训练曲线的判读也值得单独说分类头的loss下降会很快骨干的loss下降很慢甚至先涨后降这是正常的。真正需要警惕的是验证集mAP在某个epoch后持续下降或者骨干loss出现陡增——前者说明过拟合开始后者说明学习率太大。还有一种情况是损失一直在降但检索指标纹丝不动这通常意味着特征被分类头“带偏”了模型学会了区分ID却没有学会通用的车辆外观表征。遇到这种情况把分类损失的权重降下来、三元组损失提上去往往能救回来。5. 车辆重识别训练与评估的五大翻车现场排查清单5.1 同一辆车不同视角被判成不同车现象query里正面拍的车辆在gallery里同一辆车的侧面照排到了第50名开外Top-10全是其他车。原因训练数据里同一个车辆ID的图片大量来自同一个摄像头模型学到的其实是“摄像头视角车辆外观”的联合特征换个摄像头就失效。解决数据准备阶段先按摄像头ID统计每个车辆ID的样本覆盖情况确保训练集里每个ID至少包含2个以上摄像头的图像增强里加RandomErasing模拟遮挡如果样本实在不够把三元组损失的权重提高到1.5让模型更强调跨样本的判别性。这个坑在公开数据集上不明显因为数据集已经刻意做了视角平衡但自建数据几乎必踩。5.2 指标虚高gallery里有训练见过的摄像头现象训练集上mAP到0.95实测新场景只有0.4。原因数据划分时没做摄像头隔离gallery里混入了训练集的摄像头画面模型靠“认背景”混到了高分。解决划分数据时坚持“摄像头级隔离”——同一个摄像头的图像只能出现在train、query、gallery的其中一个集合里评估阶段按摄像头维度拆开看指标如果单摄像头场景指标远高于跨摄像头基本可以判定数据划分有问题。另一个连带检查项是去重同一辆车在同一天同一摄像头下的连续帧视觉上几乎相同如果同时出现在train和gallery会制造“送分题”必须按时间间隔抽帧去重。5.3 ViT不收敛或越训越差预训练权重被毁现象第一个epoch loss正常下降第10个epoch开始loss陡增特征向量之间几乎无法区分。原因最常见的是骨干学习率设成了和分类头一样的1e-3。Transformer和CNN的微调耐受度不一样ViT的预训练权重对学习率非常敏感大步长更新会直接把学到的特征分布打乱。解决骨干学习率从3e-5起步最多到1e-4外加前5个epoch做线性warmup让学习率从0平滑升到目标值。如果已经发生了loss暴增不要试图调低学习率救回来直接重新加载预训练权重重新训练——这是省时间的最优解。5.4 同品牌同色车辆互相误判现象检索白色同款SUV时gallery里排前几名的经常是另一辆同款车真正目标排到后面。原因全局特征被颜色和车身轮廓主导模型没有学到细粒度的个体差异。解决提高输入分辨率到320或384细小的划痕、贴纸、天线位置会进入特征在数据增强里加ColorJitter和RandomGrayscale强迫模型不依赖颜色更进一步的做法是在ViT特征基础上拼接一个局部特征分支比如把最后几层注意力图按车辆部件车窗、车灯、保险杠做分区加权。这个坑没有根治方案只能做缓解。车辆重识别本来就比行人难在同类车之间的个体差异太小合理预期是同款车误排进Top-10可以接受但必须排在同ID的后面。5.5 模型评估mAP高推理排队3秒一张现象离线评估指标不错一上真实摄像头单张图推理加上检索耗时严重超标。原因ViT的自注意力计算量大224分辨率的单张推理在普通GPU上约30到50毫秒但真正的瓶颈在检索——gallery如果有10万张图暴力计算余弦相似度在CPU上要做10万次向量乘法耗时随底库线性增长。解决模型推理阶段导出ONNX并用TensorRT加速检索阶段用FAISS的IVFPQ索引代替暴力索引把检索耗时从线性降到亚线性底库特征提前预计算好不要在线提取。评估指标和系统延迟是两回事mAP高只能说明“离线排序正确”不能说明“线上能用”上线前必须做压测。6. 进阶用FAISS做百万级车辆检索的落地验证6.1 把特征变成可检索的底库训练完成后你得到的是一个能输出768维特征向量的模型。接下来要把所有gallery图像的特征预先抽取出来建成索引。FAISS是这一步的标配工具下面的代码演示了最小可用的建库和检索流程import faiss import numpy as np # gallery_feats 形状 [M, 768]M 为底库图片数 faiss.normalize_L2(gallery_feats) # 先归一化内积等价余弦相似度 index faiss.IndexFlatIP(768) # 内积索引简单精确 index.add(gallery_feats) # query_feat 形状 [1, 768]同样归一化 D, I index.search(query_feat, k10) # 返回距离和索引代码逻辑normalize_L2逐行归一化让内积结果等于余弦相似度IndexFlatIP是暴力索引M在几万规模内精度最高。参数说明k10是检索返回数量评测rank-1时k至少取10实际应用你只需要关心前三名。当底库超过几十万张时把IndexFlatIP换成IndexIVFPQ先聚类再量化检索速度提升一个数量级代价是召回率下降几个点。注意query和gallery抽取特征时必须使用完全相同的预处理参数和模型权重否则检索结果没有意义。6.2 三个指标看系统能不能上线车辆重识别系统的评估我只看三个指标mAP、rank-1、单摄像头与跨摄像头的差值。mAP衡量整体排序质量rank-1衡量“第一张就命中”的概率。单独看rank-1会有误导性因为如果同款车混在gallery里rank-1可能是同款但不是同ID。跨摄像头差值是最容易被忽略的把query按摄像头分组分别计算每个组的rank-1如果不同摄像头之间的指标波动超过10个百分点说明模型对视角和场景的鲁棒性还不够上线后会表现得很不稳定。按这个标准验证一遍模型能不能进生产环境基本就有数了——不需要等部署后再来补救。最后说一个我自己的习惯每换一个数据集第一件事不是刷mAP而是随机挑20张query做Top-10可视化用眼睛扫一遍。指标会骗人可视化不会——如果你看到的结果大多是正确ID但混着同款车说明模型已经可用剩下的交给后处理过滤如果看到大量错乱那先回去查数据划分。这个习惯帮我少走了很多弯路。希望帮到你。本文还有配套的精品资源点击获取