基于拓扑图的行人重识别项目实战——Learning Relation and Topology for Occluded Person Re-Identification
数据集与代码链接见文末
1.数据集
这里使用的数据集是Duke数据集,然而,并非直接采用原始数据集,而是经过处理后生成的遮挡式数据集。

2.整体流程
首先,在第一阶段,我们利用一个预训练好的CNN Backbone提取特征图。接着,通过调用一个预训练的姿态估计算法,生成各关键点的热度图(即以关键点为中心的概率密度图)。将提取到的特征图与各关键点的热度图进行乘法操作,从而提取出各关键点的局部特征。对所有局部特征执行平均池化操作,最终获得全局特征。为了进一步优化第一阶段的局部特征和全局特征,需要引入多个损失函数项。这些关键点并非孤立存在,而是具有特定的空间关系。因此,在第二阶段,我们引入关键点之间的关系图,通过图卷积网络更新各关键点的特征表示。同时,通过比较各关键点的local-global特征差异,学习构建邻接矩阵A。通过图卷积网络,可以动态地调整各关键点特征之间的关系,最终将处理后的特征与输入的局部特征进行融合。
第三阶段仍聚焦于行人重识别的匹配问题,采用图匹配技术,其为何不采用其他行人重识别的距离匹配方法呢?这是因为这主要归因于引入遮挡区域的特征会导致对遮挡区域特征的引入。图匹配通过构建一个相似度矩阵,结合相似度匹配机制,能够有效控制匹配过程,从而筛选出遮挡区域的特征。在匹配流程中,尽管存在遮挡,但通过构建相似度矩阵,可以有效地进行匹配。具体而言,该方法会构建AP和AN,并采用三元组损失进行优化,同时在这一过程中还增加了验证损失作为额外的损失项。

3.第一个阶段
第一个阶段流程如下:
首先,采用ResNet作为CNN backbone,提取出特征图。接着,分别通过预训练的关键点识别网络,生成关键点的热度图(不更新模型参数)。基于关键点的热度图和特征图,提取局部与全局特征。将特征输入分类器,计算其属于某个人的概率,同时求取分类损失和三元组损失。

(1)CNN Backbone
就是resnet网络
class Encoder(nn.Module):
def __init__(self, class_num):
super(Encoder, self).__init__()
self.class_num = class_num
# backbone and optimize its architecture
resnet = torchvision.models.resnet50(pretrained=True)
resnet.layer4[0].conv2.stride = (1, 1)
resnet.layer4[0].downsample[0].stride = (1, 1)
# cnn backbone
self.resnet_conv = nn.Sequential(
resnet.conv1, resnet.bn1, resnet.maxpool, # no relu
resnet.layer1, resnet.layer2, resnet.layer3, resnet.layer4)
# self.gap = nn.AdaptiveAvgPool2d(1)
def forward(self, x):
feature_map = self.resnet_conv(x)
return feature_map
AI助手
(2)关键点识别网络
关键点识别模型输出的是人体关键点位置的热力图,其中权重参数已通过预训练获得,并不再进行参数更新。

# 关键点识别网络
class ScoremapComputer(nn.Module):
def __init__(self, norm_scale):
super(ScoremapComputer, self).__init__()
# init skeleton model
self.keypoints_predictor = get_pose_net(pose_config, False)
self.keypoints_predictor.load_state_dict(torch.load(pose_config.TEST.MODEL_FILE))
# self.heatmap_processor = HeatmapProcessor(normalize_heatmap=True, group_mode='sum', gaussion_smooth=None)
self.heatmap_processor = HeatmapProcessor2(normalize_heatmap=True, group_mode='sum', norm_scale=norm_scale)
def forward(self, x):
heatmap = self.keypoints_predictor(x) # before normalization
scoremap, keypoints_confidence, keypoints_location = self.heatmap_processor(heatmap) # after normalization
return scoremap.detach(), keypoints_confidence.detach(), keypoints_location.detach()
AI助手
(3) 局部与全局特征提取
遍历每一个关键点,通过将每个关键点的热度图与特征图进行乘法运算,提取出局部特征。随后,对所有关键点的局部特征进行全局平均池化操作,最终获得全局特征表示。
# 基于特征图和关键点热度图得到局部与全局特征
def compute_local_features(config, feature_maps, score_maps, keypoints_confidence):
'''
the last one is global feature
:param config:
:param feature_maps:
:param score_maps:
:param keypoints_confidence:
:return:
'''
fbs, fc, fh, fw = feature_maps.shape
sbs, sc, sh, sw = score_maps.shape
assert fbs == sbs and fh == sh and fw == sw
# get feature_vector_list
feature_vector_list = []
for i in range(sc + 1): # 遍历每一个关节点
if i < sc: # skeleton-based local feature vectors
score_map_i = score_maps[:, i, :, :].unsqueeze(1).repeat([1, fc, 1, 1])#16 2048 16 8 调整维度,方便后续操作
#print(score_map_i.shape) 热度图直接乘以特征图得到局部特征,由于每次得到每一个关键点的局部损失,对第二个维度进行sum操作
feature_vector_i = torch.sum(score_map_i * feature_maps, [2, 3])
#print(score_map_i.shape)
feature_vector_list.append(feature_vector_i)
else: # global feature vectors 最后,对每一个关键点的局部特征进行全局平均池化得到全局特征
feature_vector_i = (
F.adaptive_avg_pool2d(feature_maps, 1) + F.adaptive_max_pool2d(feature_maps, 1)).squeeze()
feature_vector_list.append(feature_vector_i)
keypoints_confidence = torch.cat([keypoints_confidence, torch.ones([fbs, 1]).cuda()], dim=1)
# compute keypoints confidence 关键点的置信度
keypoints_confidence[:, sc:] = F.normalize(
keypoints_confidence[:, sc:], 1, 1) * config.weight_global_feature # global feature score_confidence
keypoints_confidence[:, :sc] = F.normalize(keypoints_confidence[:, :sc], 1,
1) * config.weight_global_feature # partial feature score_confidence
return feature_vector_list, keypoints_confidence
AI助手
(4)分类器
由于该算法划分为多个阶段,每个阶段的网络训练均具有较高的复杂度。因此,在此处,针对每个阶段,均计算相应的损失。在第一个阶段,计算分类损失(基于预测个体的概率)以及行人重识别的三元组损失。此处的损失通过监督机制,对每个关键点的局部特征与全局特征进行相应的监督。
# 第一个阶段的分类网络
class BNClassifiers(nn.Module):
def __init__(self, in_dim, class_num, branch_num):
super(BNClassifiers, self).__init__()
self.in_dim = in_dim
self.class_num = class_num
self.branch_num = branch_num
for i in range(self.branch_num):
setattr(self, 'classifier_{}'.format(i), BNClassifier(self.in_dim, self.class_num))
def __call__(self, feature_vector_list):
assert len(feature_vector_list) == self.branch_num
# bnneck for each sub_branch_feature
bned_feature_vector_list, cls_score_list = [], []
for i in range(self.branch_num): # 对每一个关键点的局部特征以及全局特征进行操作
feature_vector_i = feature_vector_list[i]
classifier_i = getattr(self, 'classifier_{}'.format(i))
# 分类器,首先对特征进行归一化,然后预测属于某个人的概率
bned_feature_vector_i, cls_score_i = classifier_i(feature_vector_i)
bned_feature_vector_list.append(bned_feature_vector_i)
cls_score_list.append(cls_score_i)
return bned_feature_vector_list, cls_score_list
AI助手
4.第二个阶段
在人体运动过程中,关键点的变化往往具有内在联系。例如,头部关键点和腿部关键点的变化通常表现出差异性。此外,部分关键点的特征因遮挡而呈现无效信息,因此,我们可以将关键点进行分组处理。在处理非结构化数据方面,图卷积神经网络(GCN)表现尤为突出。通过图卷积(GCN),我们可以重构关键点的特征信息。

具体实现方式:
在第一个阶段,我们成功提取了关键点的局部特征向量和整体特征向量,当局部特征与全局特征之间的差异越大时,关键点就越容易成为离群点,从而可能受到遮挡。因此,我们可以利用这种差异特征来构建邻接矩阵。
首先,通过减去关键点的局部特征,从全局特征中获得差异化的特征。接着,基于差异化的特征,推导出关键点的邻接矩阵。随后,通过将邻接矩阵与关键点的局部特征相乘,获得关系特征。之后,将关系特征与局部特征结合,生成考虑关系的融合特征。最后,将融合特征与全局特征进行拼接,最终输出。

(1)图卷积
图卷积是这个模块的核心,图卷积的公式为:

即A是邻接矩阵,X是特征,W是权重,可以使用FC层实现
这个模块的重点是邻接矩阵的学习与更新,其核心步骤为:
为邻接矩阵初始化参数,这属于先验信息。尽管这里的邻接矩阵具有可学习性,但未定义的边不会被纳入模型中。
# GCN 邻接矩阵,先验;注意,虽然这里的邻接矩阵是可学习的,但是这里没有定义的边不会被考虑
self.linked_edges = \
[[13, 0], [13, 1], [13, 2], [13, 3], [13, 4], [13, 5], [13, 6], [13, 7], [13, 8], [13, 9], [13, 10],
[13, 11], [13, 12], # global
[0, 1], [0, 2], # head
[1, 2], [1, 7], [2, 8], [7, 8], [1, 8], [2, 7], # body
[1, 3], [3, 5], [2, 4], [4, 6], [7, 9], [9, 11], [8, 10], [10, 12], # libs
# [3,4],[5,6],[9,10],[11,12], # semmetric libs links
]
self.adj = generate_adj(self.branch_num, self.linked_edges, self_connect=0.0).to(self.device)
self.gcn = GraphConvNet(self.adj, 2048, 2048, 2048, self.config.gcn_scale).to(self.device)
AI助手
掌握邻接矩阵的概念,理解邻接矩阵的学习依据在于其节点的局部特征与全局特征之间的差异,通过全连接层和Sigmoid激活函数,依据这些差异进行更新。
def learn_adj(self, inputs, adj):
# inputs [bs, k(node_num), c]
bs, k, c = inputs.shape
# 调整维度,全局特征与局部特征做减法,得到全局特征与各关键点之间的差异
global_features = inputs[:, k - 1, :].unsqueeze(1).repeat([1, k, 1]) # [bs,k,2048]
distances = torch.abs(inputs - global_features) # [bs, k, 2048],全局特征与各关键点之间的差异
# bottom triangle
distances_gap = []
position_list = []
for i, j in itertools.product(list(range(k)), list(range(k))):#14
if i < j and (i != k - 1 and j != k - 1) and adj[i, j] > 0:
# 对于有边的关键点,计算关键点之间的差异,若两个关键点与全局之间层差异都很小,说明它们关系越紧密
distances_gap.append(distances[:, i, :].unsqueeze(1) - distances[:, j, :].unsqueeze(1))
position_list.append([i, j])
distances_gap = 15 * torch.cat(distances_gap, dim=1) # [bs, edge_number, 2048]
#print(distances_gap.shape) 全连接层+sigmoid
adj_tmp = self.sigmoid(self.scale * self.fc_direct(
self.bn_direct(distances_gap.transpose(1, 2)).transpose(1, 2))).squeeze() # [bs, edge_number]
#print(adj_tmp.shape)#16 16 之前定义好的16个边
# re-assign 更新领接矩阵
adj2 = torch.ones([bs, k, k]).cuda()
for indx, (i, j) in enumerate(position_list):
adj2[:, i, j] = adj_tmp[:, indx]
adj2[:, j, i] = (1 - adj_tmp[:, indx])
#print(adj2.shape)# 16 14 14
# mask掉预先定义没有边的地方
mask = adj.unsqueeze(0).repeat([bs, 1, 1])
#print(mask.shape)#16 14 14
new_adj = adj2 * mask
#print(new_adj.shape)
new_adj = F.normalize(new_adj, p=1, dim=2)
return new_adj
AI助手
- 完成FC层、残差模型等后续图卷积的运算
# 学习领接矩阵的图卷积操作
class AdaptDirGraphGonvLayer(nn.Module):
def __init__(self, in_dim, out_dim, adj, scale):
super(AdaptDirGraphGonvLayer, self).__init__()
# parameters
self.in_dim = in_dim
self.out_dim = out_dim
self.adj = adj
self.scale = scale
self.weight = nn.Parameter(torch.Tensor(in_dim, out_dim))
self.reset_parameters()
self.out = 0
# layers for adj
self.fc_direct = nn.Linear(in_dim, 1, bias=False)
self.bn_direct = nn.BatchNorm1d(in_dim)
self.sigmoid = nn.Sigmoid()
# layers for feature
self.fc_original_feature = nn.Linear(in_dim, out_dim, bias=False)
self.fc_merged_feature = nn.Linear(in_dim, out_dim, bias=False)
self.relu = nn.ReLU()
def reset_parameters(self):
stdv = 1. / math.sqrt(self.weight.size(1))
self.weight.data.uniform_(-stdv, stdv)
def forward(self, inputs):
# learn adj 学习领接矩阵
adj2 = self.learn_adj(inputs, self.adj)
#print(inputs.shape)#16 14 2048
#print(adj2.shape)
# merge feature
merged_inputs = torch.matmul(adj2, inputs)# b 14 2048
#print(merged_inputs.shape)
# 后续残差以及全连接层模块
outputs1 = self.fc_merged_feature(merged_inputs)
#print(outputs1.shape)
# embed original feature
outputs2 = self.fc_original_feature(inputs)
#print(outputs2.shape)
outputs = self.relu(outputs1) + outputs2
#print(outputs.shape)
return outputs
# 邻接矩阵学习函数
def learn_adj(self, inputs, adj):
# inputs [bs, k(node_num), c]
bs, k, c = inputs.shape
# 调整维度,全局特征与局部特征做减法,得到全局特征与各关键点之间的差异
global_features = inputs[:, k - 1, :].unsqueeze(1).repeat([1, k, 1]) # [bs,k,2048]
distances = torch.abs(inputs - global_features) # [bs, k, 2048],全局特征与各关键点之间的差异
# bottom triangle
distances_gap = []
position_list = []
for i, j in itertools.product(list(range(k)), list(range(k))):#14
if i < j and (i != k - 1 and j != k - 1) and adj[i, j] > 0:
# 对于有边的关键点,计算关键点之间的差异,若两个关键点与全局之间层差异都很小,说明它们关系越紧密
distances_gap.append(distances[:, i, :].unsqueeze(1) - distances[:, j, :].unsqueeze(1))
position_list.append([i, j])
distances_gap = 15 * torch.cat(distances_gap, dim=1) # [bs, edge_number, 2048]
#print(distances_gap.shape) 全连接层+sigmoid
adj_tmp = self.sigmoid(self.scale * self.fc_direct(
self.bn_direct(distances_gap.transpose(1, 2)).transpose(1, 2))).squeeze() # [bs, edge_number]
#print(adj_tmp.shape)#16 16 之前定义好的16个边
# re-assign 更新领接矩阵
adj2 = torch.ones([bs, k, k]).cuda()
for indx, (i, j) in enumerate(position_list):
adj2[:, i, j] = adj_tmp[:, indx]
adj2[:, j, i] = (1 - adj_tmp[:, indx])
#print(adj2.shape)# 16 14 14
# mask掉预先定义没有边的地方
mask = adj.unsqueeze(0).repeat([bs, 1, 1])
#print(mask.shape)#16 14 14
new_adj = adj2 * mask
#print(new_adj.shape)
new_adj = F.normalize(new_adj, p=1, dim=2)
return new_adj
AI助手
在第二个阶段,也需要采用分类器来计算三元组损失和分类损失。这些损失的计算方式与第一阶段完全一致。
5.第三个阶段
第三阶段仍聚焦于行人重识别的匹配问题,但采用了图匹配方法。为何不采用其他基于距离的行人重识别方法呢?因为这可能导致遮挡区域的特征被引入。图匹配通过构建一个相似度矩阵,能够有效筛选出遮挡区域的特征。在匹配流程中,继续构建正样本对(AP)和负样本对(AN),并采用三元组损失函数。同时,我们引入了验证损失函数。

其主要路程为:
首先,基于全局特征计算得到一个相似度矩阵,并进行排序处理,将相似度较高的样本配对为正样本,相似度较低的样本配对为负样本。
# 得到AP和AN
def mining_hard_pairs(feature_vector_list, pids):
'''
use global feature (the last one) to mining hard positive and negative pairs
cosine distance is used to measure similarity
:param feature_vector_list:
:param pids:
:return:
'''
global_feature_vectors = feature_vector_list[-1] # 取出全局特征
# 计算相似度矩阵
dist_matrix = cosine_dist(global_feature_vectors, global_feature_vectors)#16 16
label_matrix = label2similarity(pids, pids).float()# 16 16 对应的标签矩阵
# 按照相似度进行排序
_, sorted_mat_distance_index = torch.sort(dist_matrix + (9999999.) * (1 - label_matrix), dim=1, descending=False)
hard_p_index = sorted_mat_distance_index[:, 0]
_, sorted_mat_distance_index = torch.sort(dist_matrix + (-9999999.) * (label_matrix), dim=1, descending=True)
hard_n_index = sorted_mat_distance_index[:, 0]
new_feature_vector_list = []
p_feature_vector_list = []
n_feature_vector_list = []
# 每个特征,相似度最高的特征为正配对(P特征),相似度最低的为负配对(N特征)
for feature_vector in feature_vector_list:
# print(feature_vector.shape)#16 2048
feature_vector = copy.copy(feature_vector)
new_feature_vector_list.append(feature_vector.detach())
feature_vector = copy.copy(feature_vector.detach())
p_feature_vector_list.append(feature_vector[hard_p_index, :])
# print(feature_vector[hard_p_index, :].shape)#16 2048
feature_vector = copy.copy(feature_vector.detach())
n_feature_vector_list.append(feature_vector[hard_n_index, :])
# print(feature_vector[hard_n_index, :].shape)
return new_feature_vector_list, p_feature_vector_list, n_feature_vector_list
AI助手
然后,我们依次对正样本配对和负样本配对进行图匹配操作。针对二阶段得到的融合特征e1和e2,我们将其与全局特征进行连接。随后,分别对两张图片的特征进行全连接处理,通过图匹配计算得到图相似度矩阵。接着,我们将该相似度矩阵与融合特征e1和e2相乘,这一过程充分考虑了两张图片之间的图相似度匹配关系,并将其结果与原特征进行连接。最后,通过全连接层完成特征预测输出。这种方法的核心思想是实现特征间的跨模态匹配。
class GMNet(nn.Module):
def __init__(self):
super(GMNet, self).__init__()
self.BS_ITER_NUM = 20
self.BS_EPSILON = 1e-10
self.FEATURE_CHANNEL = 2048
self.GNN_FEAT = 1024
self.GNN_LAYER = 2
self.VOT_ALPHA = 200.0
self.bi_stochastic = Sinkhorn(max_iter=self.BS_ITER_NUM, epsilon=self.BS_EPSILON)
self.voting_layer = Voting(self.VOT_ALPHA)
for i in range(self.GNN_LAYER):
gnn_layer = Siamese_Gconv(self.FEATURE_CHANNEL, self.GNN_FEAT) if i == 0 else Siamese_Gconv(self.GNN_FEAT, self.GNN_FEAT)
self.add_module('gnn_layer_{}'.format(i), gnn_layer)
self.add_module('affinity_{}'.format(i), Affinity(self.GNN_FEAT))
if i == self.GNN_LAYER - 2: # only second last layer will have cross-graph module
self.add_module('cross_graph_{}'.format(i), nn.Linear(self.GNN_FEAT * 2, self.GNN_FEAT))
def forward(self, emb1_list, emb2_list, adj):
if type(emb1_list).__name__ == type(emb2_list).__name__ == 'list':
emb1 = torch.cat([emb1.unsqueeze(1) for emb1 in emb1_list], dim=1)# b 14 2048
emb2 = torch.cat([emb2.unsqueeze(1) for emb2 in emb2_list], dim=1)
else:
emb1 = emb1_list
emb2 = emb2_list
#print(emb1.shape)
#print(emb2.shape)
org_emb1 = emb1
org_emb2 = emb2
ns_src = (torch.ones([emb1.shape[0]]) * 14).int()#16
ns_tgt = (torch.ones([emb2.shape[0]]) * 14).int()#16
#print(ns_src.shape)
#print(ns_tgt.shape)
# 多层图匹配的过程
for i in range(self.GNN_LAYER):
# 图匹配的过程
gnn_layer = getattr(self, 'gnn_layer_{}'.format(i))
emb1, emb2 = gnn_layer([adj, emb1], [adj, emb2])
#print(emb1.shape) # 16 14 1024
affinity = getattr(self, 'affinity_{}'.format(i))
s = affinity(emb1, emb2)
#print(s.shape)# 16 14 14
s = self.voting_layer(s, ns_src, ns_tgt)
#print(s.shape)# 16 14 14
s = self.bi_stochastic(s, ns_src, ns_tgt)
#print(s.shape)# 16 14 14
# 后续的交叉融合模块,交叉融合之后,继续执行图匹配,得到最终的相似度矩阵
if i == self.GNN_LAYER - 2:
emb1_before_cross, emb2_before_cross = emb1, emb2
#print(emb1_before_cross.shape)
cross_graph = getattr(self, 'cross_graph_{}'.format(i))
emb1 = cross_graph(torch.cat((emb1_before_cross, torch.bmm(s, emb2_before_cross)), dim=-1))
#print(emb1.shape)
emb2 = cross_graph(torch.cat((emb2_before_cross, torch.bmm(s.transpose(1, 2), emb1_before_cross)), dim=-1))
#print(emb2.shape)
# 交叉融合输出的特征
fin_emb1 = org_emb1 + torch.bmm(s, org_emb2)
#print(fin_emb1.shape)
fin_emb2 = org_emb2 + torch.bmm(s.transpose(1,2), org_emb1)
#print(fin_emb1.shape)
return s, fin_emb1, fin_emb2
AI助手
链接:https://pan.baidu.com/s/1vJxnk5QRb2mHrPdvTc203w?pwd=2ou4
提取码:2ou4
