轉載請注明出處:
https://www.cnblogs.com/darkknightzh/p/12150171.html
論文
RMPE: Regional Multi-Person Pose Estimation
https://arxiv.org/abs/1612.00137
官方代碼:
https://github.com/MVIG-SJTU/AlphaPose
官方pytorch代碼:
https://github.com/MVIG-SJTU/AlphaPose/tree/pytorch
1. 簡介
該論文指出,定位和識別中不可避免的會出現錯誤,這些錯誤會引起單人姿態估計(single-person pose estimator,SPPE)的錯誤,特別是完全依賴人體檢測的姿態估計算法。因而該論文提出了區域姿態估計(Regional Multi-Person Pose Estimation,RMPE)框架。主要包括symmetric spatial transformer network (SSTN)、Parametric Pose Non- Maximum-Suppression (NMS), 和Pose-Guided Proposals Generator (PGPG)。並且使用symmetric spatial transformer network (SSTN)、deep proposals generator (DPG) 、parametric pose nonmaximum suppression (p-NMS) 三個技術來解決野外場景下多人姿態估計問題。
2. 之前算法的問題
2.1檢測框定位錯誤
如下圖所示。紅框為真實框,黃框為檢測到的框(IoU>0.5)。由於定位錯誤,黃框得到的熱圖無法檢測到關節點
解決方法:增大訓練時的框(框增大0.2-0.3倍)
2.2 檢測框冗余
如下圖所示。同一個人可能檢測到多個框。
解決方法:使用p-NMS來解決人體檢測框不准確時的姿態估計問題。
3. 網絡結構
3.1 總體結構
總體網絡結構如下圖:
Symmetric STN=STN+SPPE+SDTN
STN:空間變換網絡,對於不准確的輸入,得到准確的人的框。輸入候選區域,用於獲取高質量的候選區域。
SPPE:得到估計的姿態。
SDTN:空間逆變換網絡,將估計的姿態映射回原始的圖像坐標。
Pose-NMS:消除額外的估計到的姿態
Parallel SPPE:訓練階段作為額外的正則項,避免陷入局部最優,並進一步提升SSTN的效果。包含相同的STN及SPPE(所有參數均被凍結),無SDTN。測試階段無此模塊。
PGPG(Pose-guided Proposals Generator):通過PGPG網絡得到訓練圖像,用來訓練SSTN+SPPE模塊。
3.2 SSTN
SSTN如下圖所示。不准確的輸入(下圖左側input)經過STN+SPPE+SDTN,先姿態估計,把估計結果映射到原圖,以此來調整原本的框,使框變的精准。其中中間黑色虛線的框認為是准確的輸入(即中心化的輸入,將姿態對齊到圖像中心)。
3.3 STN和SDTN
STN為2D的仿射變換,定義如下:
SDTN定義如下:
其中為變換后坐標,
為變換前坐標。${{\theta }_{1}}$,${{\theta }_{2}}$,${{\theta }_{3}}$,${{\gamma }_{1}}$,${{\gamma }_{2}}$,${{\gamma }_{3}}$為變換參數關系如下:
(使用SDTN進行反向傳播的公式請見論文)
3.4 Parallel SPPE(PSPPE)
PSPPE模塊和原始的SPPE共享相同的STN參數,但是無SDTN模塊。此分支的人體姿態已經中心化,和中心化后的真知標簽直接比較。訓練階段,PSPPE所有層的參數均被凍結,目的是反傳中心化的姿態誤差到STN模塊。因而若STN得到的姿態未中心化,會產生較大的誤差,使得STN集中於正確的區域。
可以講PSPPE作為訓練階段額外的正則項。
3.5 P-NMS
定義:令第i個姿態由m個關節點組成,定義為$\left\{ \left\langle k_{i}^{1},c_{i}^{1} \right\rangle ,\cdots ,\left\langle k_{i}^{m},c_{i}^{m} \right\rangle \right\}$,其中k為location,c為socre。
消除過程:score最高的姿態作為基准,重復消除接近基准姿態的姿態,直到剩下單一的姿態。
消除准則:消除標准用於重復消除剩余姿態,為:
$f({{P}_{i}},{{P}_{j}}|\Lambda ,\eta )=\mathbf{1}(d({{P}_{i}},{{P}_{j}}|\Lambda ,\lambda )\le \eta )$
其中,距離函數$d(\centerdot )$包括姿態距離和空間距離,若$d(\centerdot )$不大於$\eta $,則上面$f(\centerdot )$的輸出為1,表明由於${{P}_{i}}$和基准姿態${{P}_{j}}$過於相似,因而${{P}_{i}}$需要被消除。其定義如下:
$d({{P}_{i}},{{P}_{j}}|\Lambda )\text{=}{{K}_{Sim}}({{P}_{i}},{{P}_{j}}|{{\sigma }_{1}})+\lambda {{H}_{sim}}({{P}_{i}},{{P}_{j}}|{{\sigma }_{2}})$
其中,$\Lambda =\{{{\sigma }_{1}},{{\sigma }_{2}},\lambda \}$。
姿態距離用於消除和其他姿態太近且太相似的姿態,假定${{P}_{i}}$的bbox是${{B}_{i}}$,其定義為如下的soft matching公式(不同特征之間score的相似度):
其中$B(k_{i}^{n})$為中心在$k_{i}^{n}$的box,並且每個坐標$B(k_{i}^{n})$為原始坐標${{B}_{i}}$的1/10。
如下圖所示。其中藍框為關節點${{P}_{i}}$的框,各黑點為藍框${{P}_{i}}$各個關節點位置$k_{i}^{n}$(為了方便,只顯示了4個),各紅框為寬高為藍框1/10的子框,其中心為相應的關節點$k_{i}^{n}$,三角為姿態${{P}_{j}}$在紅框內的關節點$k_{j}^{n}$,五星為姿態${{P}_{j}}$在紅框外關節點$k_{j}^{n}$。進行消除時,對三角使用上式的if進行消除,因該點在子框內;對五星使用otherwise,因該點在子框外(左上角既有三角,又有五星。實際上對於一個檢測到的姿態${{P}_{j}}$,是不會出現這種情況的,因為一個姿態的某個特定關節點只有一個,不會出現三角和五星兩個關節點。此處只是顯示使用)。
空間距離用於衡量不同特征之間空間距離的相似度,令$k_{i}^{n}$和$k_{j}^{n}$為不同特征中心,其定義如下:
${{H}_{sim}}({{P}_{i}},{{P}_{j}}|{{\sigma }_{2}})=\sum\limits_{n}{\exp [-\frac{{{(k_{i}^{n}-k_{j}^{n})}^{2}}}{{{\sigma }_{2}}}]}$
$\lambda $為平衡姿態距離和空間距離的權重。$\eta $為閾值。上式共四個參數${{\sigma }_{1}}$,${{\sigma }_{2}}$,$\lambda $,$\eta $,論文中說交替固定2個,訓練另外兩個。但是pytorch代碼中全部固定了。
3.6 PGPG
步驟:
1 歸一化姿態,使得所有軀干有歸一化長度。
2 使用kmeans聚類對齊的姿態,並且聚類得到的中心形成atomic poses。
3 對有相同atomic poses的人,計算gt bbox和detected bbox的偏移。
4 偏移使用gt bbox進行歸一化。
5 此時,偏移作為頻率的分布,且固定數據為高斯混合分布。對於不同的atomic poses,有不同的高斯混合分布的參數。
注:沒看此部分對應的代碼
4. 代碼
4.1 前向推斷
網絡前向推斷使用InferenNet_fast函數,其中輸入圖像x為通過yolo V3檢測到的單張人體。
輸出為熱圖。out.narrow原因是,訓練時使用了COCO和MPII,因而特征維數維33,前17層為COCO特征。代碼中只測試COCO上性能,因而只取前17層熱圖。

1 class InferenNet_fast(nn.Module): 2 def __init__(self, kernel_size, dataset): 3 super(InferenNet_fast, self).__init__() 4 5 model = createModel().cuda() 6 print('Loading pose model from {}'.format('./models/sppe/duc_se.pth')) 7 model.load_state_dict(torch.load('./models/sppe/duc_se.pth')) 8 model.eval() 9 self.pyranet = model # 圖像得到33維熱圖 10 self.dataset = dataset 11 12 def forward(self, x): 13 out = self.pyranet(x) # 得到b*33*h*w的矩陣 14 # https://github.com/MVIG-SJTU/AlphaPose/issues/187#issuecomment-441416429 指出,代碼聯合訓練COCO和MPII,前17個為COCO,后16個為MPII,故此處取前17層 15 out = out.narrow(1, 0, 17) # data = tensor:narrow(dim, index, size)取出tensor中第dim維上索引從index開始到index+size-1的所有元素存放在data中 16 17 return out # 圖像得到33維熱圖,取出channel上0—16維特征 18 19 20 def createModel(): 21 return FastPose() 22 23 24 class FastPose(nn.Module): 25 DIM = 128 26 27 def __init__(self): 28 super(FastPose, self).__init__() 29 self.preact = SEResnet('resnet101') # 101層SE_ResNet 30 self.suffle1 = nn.PixelShuffle(2) #將Input: (N, C∗upscale_factor * upscale_factor2, H, W)轉換成輸出Output: (N, C, H∗upscale_factor, W∗upscale_factor),此處upscale_factor=2 31 self.duc1 = DUC(512, 1024, upscale_factor=2) # conv+BN+ReLU+PixelShuffle, PixelShuffle將1024維降低到256維 32 self.duc2 = DUC(256, 512, upscale_factor=2) # conv+BN+ReLU+PixelShuffle, PixelShuffle將512維降低到128維 33 self.conv_out = nn.Conv2d(self.DIM, opt.nClasses, kernel_size=3, stride=1, padding=1) # 128維降低到33維 34 35 def forward(self, x: Variable): 36 out = self.preact(x) 37 out = self.suffle1(out) 38 out = self.duc1(out) 39 out = self.duc2(out) 40 41 out = self.conv_out(out) 42 return out 43 44 45 class DUC(nn.Module): 46 ''' 47 INPUT: inplanes, planes, upscale_factor 48 OUTPUT: (planes // 4)* ht * wd 49 ''' 50 def __init__(self, inplanes, planes, upscale_factor=2): 51 super(DUC, self).__init__() 52 self.conv = nn.Conv2d(inplanes, planes, kernel_size=3, padding=1, bias=False) 53 self.bn = nn.BatchNorm2d(planes) 54 self.relu = nn.ReLU() 55 56 self.pixel_shuffle = nn.PixelShuffle(upscale_factor) #將Input: (N, C∗upscale_factor * upscale_factor2, H, W)轉換成輸出Output: (N, C, H∗upscale_factor, W∗upscale_factor) 57 58 def forward(self, x): 59 x = self.conv(x) 60 x = self.bn(x) 61 x = self.relu(x) 62 x = self.pixel_shuffle(x) 63 return x
4.2 預測
預測代碼如下:

1 def getPrediction(hms, pt1, pt2, inpH, inpW, resH, resW): # 由於對人體檢測后裁剪的圖像進行預測,后6個參數為裁剪圖像的相關信息 2 '''Get keypoint location from heatmaps''' 3 assert hms.dim() == 4, 'Score maps should be 4-dim' 4 # 每個通道最大值作為關節點,因為是自頂向下,前提就是每張圖只有一個人,因而每個通道只有一個關節點 5 maxval, idx = torch.max(hms.view(hms.size(0), hms.size(1), -1), 2) # hms.size(0)為batchsize,hms.size(1)為channels,熱圖中h*w變成一維后的最大值及索引 6 7 maxval = maxval.view(hms.size(0), hms.size(1), 1) # b*c*1的矩陣 8 idx = idx.view(hms.size(0), hms.size(1), 1) + 1 # b*c*1的矩陣,+1是用於防止計算xy坐標時錯誤 9 10 preds = idx.repeat(1, 1, 2).float() # b*c*2的矩陣,將第2維重復一遍 11 12 preds[:, :, 0] = (preds[:, :, 0] - 1) % hms.size(3) # 得到x坐標 13 preds[:, :, 1] = torch.floor((preds[:, :, 1] - 1) / hms.size(3)) # 得到y坐標 14 15 pred_mask = maxval.gt(0).repeat(1, 1, 2).float() # 最大值中大於0的第2維重復一遍 16 preds *= pred_mask # 去掉maxval小於0對應的坐標 17 18 # Very simple post-processing step to improve performance at tight PCK thresholds 19 for i in range(preds.size(0)): # 遍歷batchsize中每個輸入的預測 20 for j in range(preds.size(1)): # 遍歷每個channels 21 hm = hms[i][j] # 當前熱圖 22 pX, pY = int(round(float(preds[i][j][0]))), int(round(float(preds[i][j][1]))) # 當前坐標 23 # 得到熱圖每個關節點的坐標后,進一步結合上下左右四個點,優化坐標(論文中沒有提到) 24 if 0 < pX < opt.outputResW - 1 and 0 < pY < opt.outputResH - 1: # 當前坐標在特征圖內 25 diff = torch.Tensor((hm[pY][pX + 1] - hm[pY][pX - 1], hm[pY + 1][pX] - hm[pY - 1][pX])) # 當前熱圖點右側減左側值,當前點熱圖下邊減上邊值 26 preds[i][j] += diff.sign() * 0.25 # diff.sign()得到diff每個元素的正負;此處將preds進行偏移 27 preds += 0.2 # preds進一步偏移?? 28 29 preds_tf = torch.zeros(preds.size()) 30 preds_tf = transformBoxInvert_batch(preds, pt1, pt2, inpH, inpW, resH, resW) # 熱圖中關節點坐標映射回原始圖像上的坐標 31 32 return preds, preds_tf, maxval # 返回關節點在原始圖像裁剪后圖像上的坐標,在原始圖像上的坐標,熱圖最大值
4.3 P-NMS
p _poseNMS.py配置參數如下(固定的參數,並未體現出通過訓練得到):

1 delta1 = 1 2 mu = 1.7 3 delta2 = 2.65 4 gamma = 22.48 5 scoreThreds = 0.3 6 matchThreds = 5 7 areaThres = 0#40 * 40.5 8 alpha = 0.1 9 10 pose_nms如下: 11 def pose_nms(bboxes, bbox_scores, pose_preds, pose_scores): 12 ''' 13 Parametric Pose NMS algorithm 14 bboxes: bbox locations list (n, 4) 15 bbox_scores: bbox scores list (n,) # 各個框為人的score 16 pose_preds: pose locations list (n, 17, 2) 各關節點的坐標 17 pose_scores: pose scores list (n, 17, 1) 各個關節點的score 18 ''' 19 #global ori_pose_preds, ori_pose_scores, ref_dists 20 21 pose_scores[pose_scores == 0] = 1e-5 22 final_result = [] 23 24 ori_bbox_scores = bbox_scores.clone() # 各個框為人的score,下面要刪除,此處先備份 25 ori_pose_preds = pose_preds.clone() # 各關節點的坐標,下面要刪除,此處先備份 26 ori_pose_scores = pose_scores.clone() # 各個關節點的score,下面要刪除,此處先備份 [n, 17, 1] 27 28 xmax = bboxes[:, 2] # 檢測到的人在原始圖像上的坐標 29 xmin = bboxes[:, 0] 30 ymax = bboxes[:, 3] 31 ymin = bboxes[:, 1] 32 33 widths = xmax - xmin # 檢測到的人的寬高 34 heights = ymax - ymin 35 ref_dists = alpha * np.maximum(widths, heights) # alpha=0.1,為論文中的1/10,此處為NMS中當前batch各個人子框的閾值[n,] 36 37 nsamples = bboxes.shape[0] 38 human_scores = pose_scores.mean(dim=1) # 當前batch各個人姿態的均值 [n, 1] 39 human_ids = np.arange(nsamples) 40 pick = [] # Do pPose-NMS 41 merge_ids = [] 42 while(human_scores.shape[0] != 0): 43 pick_id = torch.argmax(human_scores) # Pick the one with highest score 找出分值最高的姿態的索引 44 pick.append(human_ids[pick_id]) # 由於后面要delete array的部分值,因而此處保存索引 45 # num_visPart = torch.sum(pose_scores[pick_id] > 0.2) 46 47 ref_dist = ref_dists[human_ids[pick_id]] # Get numbers of match keypoints by calling PCK_match 當前人NMS子框的閾值 48 simi = get_parametric_distance(pick_id, pose_preds, pose_scores, ref_dist) # 公式(10)的距離,[n],由於每次均會刪除id,因而n遞減 49 num_match_keypoints = PCK_match(pose_preds[pick_id], pose_preds, ref_dist) # 返回滿足條件的點的數量,[n],由於每次均會刪除id,因而n遞減 50 51 # Delete humans who have more than matchThreds keypoints overlap and high similarity # gamma = 22.48,matchThreds = 5, 52 delete_ids = torch.from_numpy(np.arange(human_scores.shape[0]))[(simi > gamma) | (num_match_keypoints >= matchThreds)] # 迭代刪除的索引 53 54 if delete_ids.shape[0] == 0: 55 delete_ids = pick_id 56 #else: 57 # delete_ids = torch.from_numpy(delete_ids) 58 59 merge_ids.append(human_ids[delete_ids]) # 每次篩選出來的人的索引,如果沒有近距離的人,merge_ids==pick 60 pose_preds = np.delete(pose_preds, delete_ids, axis=0) 61 pose_scores = np.delete(pose_scores, delete_ids, axis=0) 62 human_ids = np.delete(human_ids, delete_ids) 63 human_scores = np.delete(human_scores, delete_ids, axis=0) 64 bbox_scores = np.delete(bbox_scores, delete_ids, axis=0) 65 66 assert len(merge_ids) == len(pick) 67 preds_pick = ori_pose_preds[pick] # 根據pick重新映射后的不同人各關節點的坐標 68 scores_pick = ori_pose_scores[pick] 69 bbox_scores_pick = ori_bbox_scores[pick] 70 #final_result = pool.map(filter_result, zip(scores_pick, merge_ids, preds_pick, pick, bbox_scores_pick)) 71 #final_result = [item for item in final_result if item is not None] 72 73 for j in range(len(pick)): # 人的數量。此處是當人體檢測器檢測的不好,同一個人檢測到了2個以上的框,這些框比較接近的情況 74 ids = np.arange(17) 75 max_score = torch.max(scores_pick[j, ids, 0]) 76 77 if max_score < scoreThreds: 78 continue 79 80 merge_id = merge_ids[j] # Merge poses 81 # 返回冗余關節點位置和這些關節點對應的score。無冗余姿態的情況下,merge_pose==preds_pick[j]==ori_pose_preds[merge_id],merge_score==ori_pose_scores[merge_id] 82 merge_pose, merge_score = p_merge_fast(preds_pick[j], ori_pose_preds[merge_id], ori_pose_scores[merge_id], ref_dists[pick[j]]) 83 84 max_score = torch.max(merge_score[ids]) 85 if max_score < scoreThreds: 86 continue 87 88 xmax = max(merge_pose[:, 0]) 89 xmin = min(merge_pose[:, 0]) 90 ymax = max(merge_pose[:, 1]) 91 ymin = min(merge_pose[:, 1]) 92 93 if (1.5 ** 2 * (xmax - xmin) * (ymax - ymin) < areaThres): 94 continue 95 96 final_result.append({ 97 'keypoints': merge_pose - 0.3, 98 'kp_score': merge_score, 99 'proposal_score': torch.mean(merge_score) + bbox_scores_pick[j] + 1.25 * max(merge_score) 100 }) 101 102 return final_result 103 104 105 106 def PCK_match(pick_pred, all_preds, ref_dist): 107 dist = torch.sqrt(torch.sum(torch.pow(pick_pred[np.newaxis, :] - all_preds, 2), dim=2 )) # 當前點和其他所有點的距離 [n, 17] 108 ref_dist = min(ref_dist, 7) 109 num_match_keypoints = torch.sum(dist / ref_dist <= 1, dim=1) # 得到滿足條件的點的數量 [n] 110 return num_match_keypoints # 返回滿足條件的點的數量 111 112 113 114 def get_parametric_distance(i, all_preds, keypoint_scores, ref_dist): 115 pick_preds = all_preds[i] # 當前預測關節點的坐標 116 pred_scores = keypoint_scores[i] # 當前預測關節點的分值 117 dist = torch.sqrt(torch.sum(torch.pow(pick_preds[np.newaxis, :] - all_preds, 2), dim=2)) # 當前人關節點和所有人關節點的距離 [n, 17] 118 mask = (dist <= 1) # 當前人關節點和所有人關節點的mask,此處指如果兩套關節點距離太小(因是二維矩陣,不會出現某人部分關節點mask=1),則mask=1,一般來說,只是本人關節點mask=1 [n, 17] 119 120 score_dists = torch.zeros(all_preds.shape[0], 17) # Define a keypoints distance 121 keypoint_scores.squeeze_() 122 if keypoint_scores.dim() == 1: 123 keypoint_scores.unsqueeze_(0) # 增加維度 124 if pred_scores.dim() == 1: 125 pred_scores.unsqueeze_(1) # 增加維度 126 pred_scores = pred_scores.repeat(1, all_preds.shape[0]).transpose(0, 1) # The predicted scores are repeated up to do broadcast。 [n, 1] 127 128 # 由於broadcast,pred_scores!=keypoint_scores,但是pred_scores[mask] == keypoint_scores[mask] 129 score_dists[mask] = torch.tanh(pred_scores[mask] / delta1) * torch.tanh(keypoint_scores[mask] / delta1) # delta1 = 1,當前點和近距離點的score的相似度,公式(8) 130 131 point_dist = torch.exp((-1) * dist / delta2) # delta2 = 2.65,當前點和近距離點的距離的相似度,公式(9) 132 final_dist = torch.sum(score_dists, dim=1) + mu * torch.sum(point_dist, dim=1) # mu = 1.7,最終的距離 [n] 133 134 return final_dist # 返回最終的距離 135 136 137 # 如果人體檢測器效果很好,無冗余檢測,則此函數無效 138 def p_merge_fast(ref_pose, cluster_preds, cluster_scores, ref_dist): 139 ''' 140 Score-weighted pose merging 141 INPUT: 142 ref_pose: reference pose -- [17, 2] ref_pose # 根據pick重新映射后的當前人各關節點的坐標 143 cluster_preds: redundant poses -- [n, 17, 2] cluster_preds # 篩選出來的當前人各關節點的坐標 144 cluster_scores: redundant poses score -- [n, 17, 1] cluster_scores # 篩選出來的當前人各個關節點的score 145 ref_dist: reference scale -- Constant ref_dist # 根據pick重新映射后當前人NMS子框的閾值 146 OUTPUT: 147 final_pose: merged pose -- [17, 2] 148 final_score: merged score -- [17] 149 ''' 150 # 無冗余姿態的情況下,ref_pose==cluster_preds==final_pose,dist=[[0....0]] 17個 151 dist = torch.sqrt(torch.sum(torch.pow(ref_pose[np.newaxis, :] - cluster_preds, 2), dim=2)) 152 153 kp_num = 17 154 ref_dist = min(ref_dist, 15) 155 156 mask = (dist <= ref_dist) 157 final_pose = torch.zeros(kp_num, 2) 158 final_score = torch.zeros(kp_num) 159 160 if cluster_preds.dim() == 2: 161 cluster_preds.unsqueeze_(0) # [17,2] ==> [1, 17, 2] 162 cluster_scores.unsqueeze_(0) # [17,1] ==> [1, 17, 1] 163 if mask.dim() == 1: 164 mask.unsqueeze_(0) # [1,17] ==> [1, 17] 不變 165 166 # Weighted Merge 167 masked_scores = cluster_scores.mul(mask.float().unsqueeze(-1)) # [1, 17, 1] 冗余score乘以mask,並進行歸一化 168 normed_scores = masked_scores / torch.sum(masked_scores, dim=0) # [1, 17, 1] 的全1矩陣 169 170 # 冗余關節點位置乘歸一化分數,得到冗余關節點位置。無冗余姿態的情況下,無冗余姿態的情況下,ref_pose==cluster_preds==final_pose 171 final_pose = torch.mul(cluster_preds, normed_scores.repeat(1, 1, 2)).sum(dim=0) 172 # 歸一化之前的冗余關節點分數 final_score==cluster_scores==masked_scores 173 final_score = torch.mul(masked_scores, normed_scores).sum(dim=0) 174 175 return final_pose, final_score # 返回冗余關節點位置和這些關節點對應的score