以下链接是个人关于fast-reid(BoT行人重识别) 所有见解,如有错误欢迎大家指出,我会第一时间纠正。有兴趣的朋友可以加微信:a944284742相互讨论技术。若是帮助到了你什么,一定要记得点赞!因为这是对我最大的鼓励。
行人重识别02-00:fast-reid(BoT)-目录-史上最新无死角讲解
在上篇博客中提到 fastreid\evaluation\evaluator.py 中函数 inference_on_dataset 由两个比较重要的地方:
# 把提出的特征,以及标签添加到evaluator.pids,evaluator.camids,evaluator.features之中
evaluator.process(inputs, outputs)
# 对结果进行评估
results = evaluator.evaluate()
其中都涉及到了 evaluator ,那么我们现在就来看看,其到底是何方神圣。在tools\train_net.py文件中可以看到:
# 继承于DefaultTrainer创建一个训练类
class Trainer(DefaultTrainer):
@classmethod # 重写评估函数
def build_evaluator(cls, cfg, num_query, output_folder=None):
"""
:param cfg: 配置参数
:param num_query: 测试数据集的数目
:param output_folder: 输出目录
:return:
"""
if output_folder is None:
output_folder = os.path.join(cfg.OUTPUT_DIR, "inference")
return ReidEvaluator(cfg, num_query)
这里的 ReidEvaluator,就是我们想要了解的东西,上面的 evaluator 就是其创建出来的类对象。那么我们就具体去看看其实现了那些东西。
ReidEvaluator的注释如下:
class ReidEvaluator(DatasetEvaluator):
def __init__(self, cfg, num_query, output_dir=None):
# 获得配置信息
self.cfg = cfg
# 获得需要查询图片的数目
self._num_query = num_query
# 获得数目目录
self._output_dir = output_dir
# 用于存储提取的特征
self.features = []
# 用于存储身份id
self.pids = []
# 用于存储图片对应的摄像头id
self.camids = []
def reset(self):
# 把身份特征,身份id,图片对应的摄像头id全部清零
self.features = []
self.pids = []
self.camids = []
def process(self, inputs, outputs):
# 获得query图片的身份id,以及摄像头ID,outputs(图片提出出来的features)。因为使用的是extend函数,
# 最后会把query中的所有图片信息都分别添加到 self.pids,self.camids,self.features之中
self.pids.extend(inputs["targets"])
self.camids.extend(inputs["camid"])
self.features.append(outputs.cpu())
@staticmethod
def cal_dist(metric: str, query_feat: torch.tensor, gallery_feat: torch.tensor):
assert metric in ["cosine", "euclidean"], "must choose from [cosine, euclidean], but got {}".format(metric)
if metric == "cosine":
dist = 1 - torch.mm(query_feat, gallery_feat.t())
else:
m, n = query_feat.size(0), gallery_feat.size(0)
xx = torch.pow(query_feat, 2).sum(1, keepdim=True).expand(m, n)
yy = torch.pow(gallery_feat, 2).sum(1, keepdim=True).expand(n, m).t()
dist = xx + yy
dist.addmm_(query_feat, gallery_feat.t(), beta=1, alpha=-2)
dist = dist.clamp(min=1e-12).sqrt() # for numerical stability
return dist.cpu().numpy()
def evaluate(self):
"""
对提取到的特征进行评估
"""
# 使用分布式进行计算
if comm.get_world_size() > 1:
comm.synchronize()
features = comm.gather(self.features)
features = sum(features, [])
pids = comm.gather(self.pids)
pids = sum(pids, [])
camids = comm.gather(self.camids)
camids = sum(camids, [])
if not comm.is_main_process():
return {}
# 非分布式,单台机器进行计算
else:
features = self.features
pids = self.pids
camids = self.camids
# 把所有特征垂直连接起来,
features = torch.cat(features, dim=0)
# query feature, person ids and camera ids
# query 和 bounding_box_test 中的数据是串接起来的,现在把他们独立分开来
# query 表示需要被查询的数据
query_features = features[:self._num_query]
query_pids = np.asarray(pids[:self._num_query])
query_camids = np.asarray(camids[:self._num_query])
# gallery features, person ids and camera ids
# gallery表示 bounding_box_test
gallery_features = features[self._num_query:]
gallery_pids = np.asarray(pids[self._num_query:])
gallery_camids = np.asarray(camids[self._num_query:])
# 用于保存结果
self._results = OrderedDict()
# 如果设置了AQE,则进行aqe处理:将检索到的 top k最近邻与原始查询合并,并执行另一次检索
if self.cfg.TEST.AQE.ENABLED:
logger.info("Test with AQE setting")
qe_time = self.cfg.TEST.AQE.QE_TIME
qe_k = self.cfg.TEST.AQE.QE_K
alpha = self.cfg.TEST.AQE.ALPHA
query_features, gallery_features = aqe(query_features, gallery_features, qe_time, qe_k, alpha)
# 如果设置了评估标准为 cosine 则对 query_features 与 gallery_features 进行正则化处理
if self.cfg.TEST.METRIC == "cosine":
query_features = F.normalize(query_features, dim=1)
gallery_features = F.normalize(gallery_features, dim=1)
# 计算欧式距离或者余弦距离:
# [3368,2048](query提取的特征向量), [15913,2048](bounding_box_test提取的特征向量)-->[3368,15913]
dist = self.cal_dist(self.cfg.TEST.METRIC, query_features, gallery_features)
# 如果使能了RERANK,默认为False
if self.cfg.TEST.RERANK.ENABLED:
logger.info("Test with rerank setting")
k1 = self.cfg.TEST.RERANK.K1
k2 = self.cfg.TEST.RERANK.K2
lambda_value = self.cfg.TEST.RERANK.LAMBDA
q_q_dist = self.cal_dist(self.cfg.TEST.METRIC, query_features, query_features)
g_g_dist = self.cal_dist(self.cfg.TEST.METRIC, gallery_features, gallery_features)
re_dist = re_ranking(dist, q_q_dist, g_g_dist, k1, k2, lambda_value)
# 把query_features,gallery_features转换为numpy到cpu上
query_features = query_features.numpy()
gallery_features = gallery_features.numpy()
cmc, all_AP, all_INP = evaluate_rank(re_dist, query_features, gallery_features,
query_pids, gallery_pids, query_camids,
gallery_camids, use_distmat=True)
else:
# 把query_features,gallery_features转换为numpy到cpu上
query_features = query_features.numpy()
gallery_features = gallery_features.numpy()
# 进行rank评估:dist表示测试出来的欧式距离,或者余弦相似度
# query_features表示query图片对应的特征向量,gallery_features表示bounding_box_test中图像对应的特征向量
# query_pids 表示query图片对应的身份ID, gallery_pids 表示 bounding_box_test中图像对应的身份ID
# query_camids 表示query图片对应的摄像头id,gallery_camids 表示 bounding_box_test中图像对应的摄像头id
cmc, all_AP, all_INP = evaluate_rank(dist, query_features, gallery_features,
query_pids, gallery_pids, query_camids, gallery_camids,
use_distmat=False)
mAP = np.mean(all_AP)
mINP = np.mean(all_INP)
for r in [1, 5, 10]:
self._results['Rank-{}'.format(r)] = cmc[r - 1]
self._results['mAP'] = mAP
self._results['mINP'] = mINP
# 如果是能了ROC,则进行ROC评估
if self.cfg.TEST.ROC_ENABLED:
scores, labels = evaluate_roc(dist, query_features, gallery_features,
query_pids, gallery_pids, query_camids, gallery_camids)
fprs, tprs, thres = metrics.roc_curve(labels, scores)
for fpr in [1e-4, 1e-3, 1e-2]:
ind = np.argmin(np.abs(fprs - fpr))
self._results["TPR@FPR={:.0e}".format(fpr)] = tprs[ind]
return copy.deepcopy(self._results)
其上的代码,主要进行了两种评估方式:
# rank评估
cmc, all_AP, all_INP = evaluate_rank(re_dist, query_features,gallery_features,query_pids, gallery_pids,query_camids,gallery_camids, use_distmat=True)
# roc评估
scores, labels = evaluate_roc(dist, query_features, gallery_features,query_pids, gallery_pids, query_camids, gallery_camids)
本人只对 rank评估方式进行讲解,对于 roc 的评估,有需要了解的同志,可以自己琢磨一下代码。
在对代码进行了解之前,我们需要了解一下cmc评估的概念,这是我参考的一篇博客:
https://blog.csdn.net/weixin_40446557/article/details/83106995
对于 evaluate_rank 这个函数,我们把参数设置 use_cython=False 之后,其最终调用到的是 fastreid\evaluation\rank.py 中的 def eval_market1501() 函数:
def eval_market1501(distmat, q_feats, g_feats, q_pids, g_pids, q_camids, g_camids, max_rank, use_distmat):
"""Evaluation with market1501 metric
Key: for each query identity, its gallery images from the same camera view are discarded.
"""
# 获得query,gallery 图片(特征)的数目
num_q, num_g = distmat.shape
# 获得特征向量的维度
dim = q_feats.shape[1]
# 为faiss指定每个特征向量的维度
index = faiss.IndexFlatL2(dim)
# 把g_feats添加到这个容器之中
index.add(g_feats)
# 如果gallery的数目小于max_rank则报错
if num_g < max_rank:
max_rank = num_g
print('Note: number of gallery samples is quite small, got {}'.format(num_g))
# 如果使用distmat方式,则对distmat(每一列)进行排序,获得排序之后的索引
if use_distmat:
indices = np.argsort(distmat, axis=1)
# 如果不使用distmat方式,则通过 index.search 进行搜索
else:
_, indices = index.search(q_feats, k=num_g)
# 进行匹配,如果g_pids[indices]等于q_pids[:, np.newaxis]身份ID,则被置1。
# matches[3368,15913],排列之后的结果类似如下:
#
matches = (g_pids[indices] == q_pids[:, np.newaxis]).astype(np.int32)
# compute cmc curve for each query,计算每个查询的cmc曲线
all_cmc = []
all_AP = [] # 记录 query 每张图像的AP
all_INP = []
num_valid_q = 0. # number of valid query,记录有效的query数量
# 循环处理 query 每张图片的结果
for q_idx in range(num_q):
# get query pid and camid,获得query的 pid 以及 camid
q_pid = q_pids[q_idx]
q_camid = q_camids[q_idx]
# remove gallery samples that have the same pid and camid with query
# 取出当前第q_idx张图片,在gallery中查询过后的排序结果
# [3368,15913]-->[15913,]
order = indices[q_idx]
# 删除与查询具有相同pid和camid的gallery样本,也就是删除query和gallery中相同图片的结果
remove = (g_pids[order] == q_pid) & (g_camids[order] == q_camid)
keep = np.invert(remove)
# compute cmc curve,二进制向量,值为1的位置是正确的匹配
raw_cmc = matches[q_idx][keep] # binary vector, positions with value 1 are correct matches
if not np.any(raw_cmc):
# 当查询标识未出现在图库中时,此条件为真
# this condition is true when query identity does not appear in gallery
continue
# 计算一行中的累加值,如一个数组为[0,0,1,1,0,2,0]
# 通过cumsum得到[0,0,1,2,2,4,4]
cmc = raw_cmc.cumsum()
# 获得raw_cmc中值为 1 的坐标
pos_idx = np.where(raw_cmc == 1)
# 选择最大的一个坐标
max_pos_idx = np.max(pos_idx)
# 计算inp,该值约大表示预测得约准
inp = cmc[max_pos_idx] / (max_pos_idx + 1.0)
all_INP.append(inp)
# cmc > 1的位置,表示都预测正确了
cmc[cmc > 1] = 1
# 根据max_rank,添cmc到all_cmc之中
all_cmc.append(cmc[:max_rank])
num_valid_q += 1.
# compute average precision,计算平均精度
# reference: https://en.wikipedia.org/wiki/Evaluation_measures_(information_retrieval)#Average_precision
num_rel = raw_cmc.sum()
tmp_cmc = raw_cmc.cumsum()
tmp_cmc = [x / (i + 1.) for i, x in enumerate(tmp_cmc)]
tmp_cmc = np.asarray(tmp_cmc) * raw_cmc
AP = tmp_cmc.sum() / num_rel
all_AP.append(AP)
# 所有查询身份没有出现在图库(gallery)中则报错
assert num_valid_q > 0, 'Error: all query identities do not appear in gallery'
# 计算平均cmc精度
all_cmc = np.asarray(all_cmc).astype(np.float32)
all_cmc = all_cmc.sum(0) / num_valid_q
return all_cmc, all_AP, all_INP
到这里,我们对于模型的评估已经有了一个大致的了解,那么在后面我会带大家去训练自己的数据。