Openpose推断阶段原理
前言
之前出過一個關于openpose配置的博客,不過那個代碼雖然寫的很好,而且是官方的,但是分析起來很困難,然后再opencv相關博客中找到了比較清晰的實現,這里分析一波openpose的推斷過程。
國際慣例,參考博客:
opencv官方文檔,只有單人
大佬的實現,包括多人
解讀
直接使用opencv的dnn庫調用openpose的caffe模型,然后對輸出進行后處理。重點是代表關節連接親密度的親和場的解析。
網絡輸出解析
推斷階段的模型結構(pose/coco)戳openpose官網,點這里跳轉,可以使用netscope可視化。
最后一層的結構如下:
layer {name: "concat_stage7"type: "Concat"bottom: "Mconv7_stage6_L2"bottom: "Mconv7_stage6_L1"# top: "concat_stage7"top: "net_output"concat_param {axis: 1}可以發現拼接了兩個層:
-
Mconv7_stage6_L2
layer {name: "Mconv7_stage6_L2"type: "Convolution"bottom: "Mconv6_stage6_L2"top: "Mconv7_stage6_L2"param {lr_mult: 4.0decay_mult: 1}param {lr_mult: 8.0decay_mult: 0}convolution_param {num_output: 19pad: 0kernel_size: 1weight_filler {type: "gaussian"std: 0.01}bias_filler {type: "constant"}} } -
Mconv7_stage6_L1
layer {name: "Mconv7_stage6_L1"type: "Convolution"bottom: "Mconv6_stage6_L1"top: "Mconv7_stage6_L1"param {lr_mult: 4.0decay_mult: 1}param {lr_mult: 8.0decay_mult: 0}convolution_param {num_output: 38pad: 0kernel_size: 1weight_filler {type: "gaussian"std: 0.01}bias_filler {type: "constant"}} }
可以發現,被拼接的兩個層分別具有19和38個特征圖。對照的網絡結構圖
兩個stage,每個stage有兩個branch:第一個branch輸出191919個特征圖,分別代表18個人體關鍵點和背景;第二個branch有38個特征圖,代表文章所提出來親和場(Part Affinity Fileds,PAF),代表關節與關節之前的聯系。
代碼里面對應關系:
keypointsMapping = ['Nose', 'Neck', 'R-Sho', 'R-Elb', 'R-Wr', 'L-Sho', 'L-Elb', 'L-Wr', 'R-Hip', 'R-Knee', 'R-Ank', 'L-Hip', 'L-Knee', 'L-Ank', 'R-Eye', 'L-Eye', 'R-Ear', 'L-Ear']POSE_PAIRS = [[1,2], [1,5], [2,3], [3,4], [5,6], [6,7],[1,8], [8,9], [9,10], [1,11], [11,12], [12,13],[1,0], [0,14], [14,16], [0,15], [15,17],[2,17], [5,16] ]# index of pafs correspoding to the POSE_PAIRS # e.g for POSE_PAIR(1,2), the PAFs are located at indices (31,32) of output, Similarly, (1,5) -> (39,40) and so on. mapIdx = [[31,32], [39,40], [33,34], [35,36], [41,42], [43,44], [19,20], [21,22], [23,24], [25,26], [27,28], [29,30], [47,48], [49,50], [53,54], [51,52], [55,56], [37,38], [45,46]]POSE_PAIRS分別代表keypointsMapping里面同一根骨骼兩端的兩個人體關節(關鍵點)。
mapIdx:代表與POSE_PAIRS對應的親和場特征圖索引
【注】這里很容易出現疑問,為什么同一個關節,在向量場里面有不同的特征圖索引呢?比如[1,2],[1,5]里面的關節1,在PAF特征圖里面是索引31,39。這是因為一個關節可以被其它多個關節連接,而一個向量場PAF特征圖只指向一個關節到另一個關節的鏈接,無法指向其它所有關節的鏈接。后面會可視化解釋。
這里貼一下coco的人體人體關鍵點
調用模型
直接用opencv的dnn.readNetFromCaffe來調用模型
protoFile = './models/pose/coco/pose_deploy_linevec.prototxt' weightsFile = './models/pose/coco/pose_iter_440000.caffemodel' net = cv2.dnn.readNetFromCaffe(protoFile,weightsFile)然后輸入一張圖:
img = cv2.imread('./examples/media/COCO_val2014_000000000328.jpg') frameWidth = img.shape[1] frameHeight = img.shape[0] inHeight = 368 inWidth = int((inHeight/frameHeight)*frameWidth) inBlob = cv2.dnn.blobFromImage(img,1.0/255.0,(inWidth,inHeight),(0,0,0),swapRB=False,crop=False) net.setInput(inBlob) output=net.forward() print(output.shape)#(1, 57, 46, 60)可以發現輸出的特征圖個數和前面分析的相同。接下來隨便可視化看看:
#可視化 plt.figure(figsize=[20,20]) plt.subplot(141) plt.imshow(cv2.cvtColor(img, cv2.COLOR_BGR2RGB)) plt.imshow(cv2.resize(output[0, 10, :, :], (frameWidth, frameHeight)), alpha=0.6) plt.axis("off") plt.subplot(142) plt.imshow(cv2.cvtColor(img, cv2.COLOR_BGR2RGB)) plt.imshow(cv2.resize(output[0, 18, :, :], (frameWidth, frameHeight)), alpha=0.6) plt.axis("off") plt.subplot(143) plt.imshow(cv2.cvtColor(img, cv2.COLOR_BGR2RGB)) plt.imshow(cv2.resize(output[0, 31, :, :], (frameWidth, frameHeight)), alpha=0.6) plt.axis("off") plt.subplot(144) plt.imshow(cv2.cvtColor(img, cv2.COLOR_BGR2RGB)) plt.imshow(cv2.resize(output[0, 39, :, :], (frameWidth, frameHeight)), alpha=0.6) plt.axis("off")從左到右,分別是:第10個關節點的特征圖,背景特征圖,(1,2)(1,2)(1,2)關節的關節111的親和場PAF特征圖,(1,5)(1,5)(1,5)關節的關節111的親和場PAF特征圖。
提取關鍵點
接下來就可以利用前面的18個特征圖把肢體關鍵點提取出來。
對于某個關節的特征圖,調用
def getKeypoints(probMap,threshold=0.1):mapSmooth = cv2.GaussianBlur(probMap,(3,3),0,0)mapMask = np.uint8(mapSmooth>threshold)keypoints = []_,contours,_ = cv2.findContours(mapMask,cv2.RETR_TREE,cv2.CHAIN_APPROX_SIMPLE)for cnt in contours:blobMask = np.zeros(mapMask.shape)blobMask = cv2.fillConvexPoly(blobMask,cnt,1)maskProbMap = mapSmooth*blobMask_,maxVal,_,maxLoc = cv2.minMaxLoc(maskProbMap)keypoints.append(maxLoc+(probMap[maxLoc[1],maxLoc[0]],))#位置和置信度return keypoints就可以將當前關節的位置和對應的置信度提取出來。
提取所有的關節的位置和置信度,就相當于把每個關節的特征圖遍歷一遍:
nPoints = 18 def get_joint_kps(output):detected_keypoints = []keypoints_list = np.zeros((0,3))keypoints_id = 0threshold = 0.1for part in range(nPoints):probMap=output[0,part,:,:]probMap = cv2.resize(probMap,(img.shape[1],img.shape[0]))keypoints = getKeypoints(probMap,threshold)keypoints_with_id = []for i in range(len(keypoints)):keypoints_with_id.append(keypoints[i]+(keypoints_id,)) #所有人的18個關節位置、置信度、idkeypoints_list = np.vstack([keypoints_list,keypoints[i]])keypoints_id += 1detected_keypoints.append(keypoints_with_id)return detected_keypoints,keypoints_list調用方法也很簡單:
detected_keypoints,keypoints_list = get_joint_kps(output)簡單地看一下輸出:
detected_keypoints ''' [[(325, 165, 0.84138775, 0),(442, 143, 0.8589974, 1),(196, 133, 0.8166057, 2)],[(473, 176, 0.7320131, 3),(337, 165, 0.73004884, 4),(197, 133, 0.8598474, 5)],[(420, 176, 0.6951778, 6),(293, 154, 0.76514935, 7),(154, 133, 0.7135527, 8)],[(420, 261, 0.7520779, 9),(262, 218, 0.4267502, 10),(134, 197, 0.7333843, 11)],[(314, 251, 0.23509319, 12),(165, 228, 0.59333, 13),(453, 196, 0.6519662, 14)],[(388, 176, 0.62505144, 15),(518, 176, 0.6421095, 16),(240, 134, 0.6540677, 17)],[(549, 262, 0.73827094, 18),(389, 251, 0.71131617, 19),(240, 207, 0.6886268, 20)],[(495, 293, 0.62819993, 21),(357, 252, 0.7374373, 22),(207, 219, 0.560498, 23)],[(442, 282, 0.6578402, 24),(293, 252, 0.52459615, 25),(165, 228, 0.5512052, 26)],[(410, 283, 0.7377036, 27),(261, 272, 0.69384813, 28),(123, 239, 0.6885635, 29)],[(378, 431, 0.70034677, 30),(251, 411, 0.59873545, 31),(101, 356, 0.6479251, 32)],[(505, 283, 0.54467636, 33),(356, 271, 0.50983644, 34),(208, 229, 0.57463825, 35)],[(569, 314, 0.7413026, 36),(324, 293, 0.774911, 37),(228, 250, 0.7241578, 38)],[(538, 455, 0.58486414, 39),(282, 400, 0.46120968, 40),(207, 369, 0.56457037, 41)],[(314, 154, 0.8159541, 42),(433, 133, 0.72613055, 43),(186, 122, 0.80552864, 44)],[(335, 155, 0.8006719, 45),(453, 133, 0.8574599, 46),(206, 122, 0.80626, 47)],[(304, 133, 0.10505505, 48), (166, 111, 0.5242959, 49)],[(485, 144, 0.76806116, 50),(357, 143, 0.738666, 51),(218, 112, 0.73427236, 52)]]'''可以看出來,這個數據是被分組的,總共18個組,分別代表18個關節,每組涵蓋了當前圖像所有人的這個關節的坐標和置信度,以及當前數據的編號,依次往下排,主要是為了索引keypoints_list里面的數據。這個keypoints_list里面是將所有的關鍵點的坐標和置信度不分組地塞到一起,所以維度是(53,3)(53,3)(53,3)
把關鍵點可視化瞅瞅唄:
#可視化關鍵點 img_show = img.copy() for i in range(nPoints):for j in range(len(detected_keypoints[i])):cv2.circle(img_show,detected_keypoints[i][j][0:2],3,[0,255,0],-1,cv2.LINE_AA) plt.figure(figsize=[8,8]) plt.imshow(img_show) plt.axis('off')區分關鍵點
上面提取了所有的關鍵點,但是沒有計算哪個關鍵點屬于哪個人,此時就需要根據親和場計算各關鍵點之間的聯系。比如第一個人大臂到三個人各自的小臂關鍵點的親和場肯定不同,它只到屬于自己的小臂關鍵點親和場特征比較明顯。知道這個道理,接下來分析一波。
下面就是找到與當前關鍵點最可能連接的肢體關鍵點是哪個。
根據mapIdx里面定義的親和場索引:
先找到親和場特征圖
pafA = output[0,mapIdx[k][0],:,:] #第k組連接關節的第一個關節PAF pafB = output[0,mapIdx[k][1],:,:] #第k組連接關節的第二個關節PAF pafA = cv2.resize(pafA,(frameWidth,frameHeight)) pafB = cv2.resize(pafB,(frameWidth,frameHeight))再找到對應的肢體關鍵點索引:
#找到這兩個關節的位置 candA = detected_keypoints[POSE_PAIRS[k][0]] #找到第一個關節的位置(所有人) candB = detected_keypoints[POSE_PAIRS[k][1]] #找到第二個關節的位置(所有人)再看看親和場怎么算的,先看論文的圖
就是直接把兩個關鍵點連起來,中間做一條線,計算親和場上這條線上的值。
公式表達為
先計算dj2?dj1∥dj2?dj1∥2\frac{d_{j_2}-d_{j_1}}{\parallel d_{j_2}-d_{j_1} \parallel}_2∥dj2???dj1??∥dj2???dj1???2?
畫一條直線過去,得到PAF上每個點的值
n_interp_samples = 10 interp_coord = list(zip(np.linspace(candA[i][0],candB[j][0],num=n_interp_samples),np.linspace(candA[i][1],candB[j][1],num=n_interp_samples))) paf_interp = [] for k in range(len(interp_coord)):paf_interp.append([pafA[int(round(interp_coord[k][1])),int(round(interp_coord[k][0]))], pafB[int(round(interp_coord[k][1])),int(round(interp_coord[k][0]))]])計算PAF得分和平均得分
paf_scores = np.dot(paf_interp,d_ij) avg_paf_score = sum(paf_scores)/len(paf_scores)使用PAF分數,進行閾值篩選
paf_score_th = 0.1 conf_th = 0.7 if(len(np.where(paf_scores>paf_score_th)[0])/n_interp_samples)>conf_th:if(avg_paf_score>maxScore):max_j = jmaxScore = avg_paf_scorefound = 1找到關鍵點以后,就可以把當前關鍵點對兒的索引和得分保存
valid_pair = np.append(valid_pair,[[candA[i][3],candB[max_j][3],maxScore]],axis=0) #被連接的肢體的關鍵點索引這里的整塊代碼寫成函數如下,主要是額外加了一些得分不夠和沒有關鍵點的情況
def get_valid_pairs(output,detected_keypoints):valid_pairs = []invalid_pairs = []n_interp_samples = 10paf_score_th = 0.1conf_th = 0.7for k in range(len(mapIdx)):#兩個可能連接的關節pafA = output[0,mapIdx[k][0],:,:] #第k組連接關節的第一個關節PAFpafB = output[0,mapIdx[k][1],:,:] #第k組連接關節的第二個關節PAFpafA = cv2.resize(pafA,(frameWidth,frameHeight))pafB = cv2.resize(pafB,(frameWidth,frameHeight))#找到這兩個關節的位置candA = detected_keypoints[POSE_PAIRS[k][0]] #找到第一個關節的位置(所有人)candB = detected_keypoints[POSE_PAIRS[k][1]] #找到第二個關節的位置(所有人)nA = len(candA)nB = len(candB)#使用公式計算親和場的得分if(nA!=0 and nB!=0): #如果有這兩個關節valid_pair = np.zeros((0,3))for i in range(nA): #對于第一個關節的所有人遍歷max_j = -1maxScore = -1found = 0for j in range(nB): #第二個關節的所有人遍歷d_ij = np.subtract(candB[j][:2],candA[i][:2])norm = np.linalg.norm(d_ij)if(norm):d_ij = d_ij/norm #公式(10的d部分)else:continueinterp_coord = list(zip(np.linspace(candA[i][0],candB[j][0],num=n_interp_samples),np.linspace(candA[i][1],candB[j][1],num=n_interp_samples)))paf_interp = []for k in range(len(interp_coord)):paf_interp.append([pafA[int(round(interp_coord[k][1])),int(round(interp_coord[k][0]))],pafB[int(round(interp_coord[k][1])),int(round(interp_coord[k][0]))]])paf_scores = np.dot(paf_interp,d_ij)avg_paf_score = sum(paf_scores)/len(paf_scores)if(len(np.where(paf_scores>paf_score_th)[0])/n_interp_samples)>conf_th:if(avg_paf_score>maxScore):max_j = jmaxScore = avg_paf_scorefound = 1if found:valid_pair = np.append(valid_pair,[[candA[i][3],candB[max_j][3],maxScore]],axis=0) #被連接的肢體的關鍵點索引valid_pairs.append(valid_pair)else:#如果關節被遮擋等原因,導致不存在invalid_pairs.append(k)valid_pairs.append([]) return valid_pairs,invalid_pairs稍微看一下這個函數的調用方法和返回的數據結構
#valid_pairs存儲可成對的關節索引,所有人的每個關節成一組,比如3個人的第一個關節,組成一個3*3的矩陣 valid_pairs,invalid_pairs = get_valid_pairs(output,detected_keypoints) ''' [array([[3. , 6. , 0.9164666 ],[4. , 7. , 0.85875524],[5. , 8. , 0.88577998]]), array([[ 3. , 16. , 0.90284936],[ 4. , 15. , 0.77933996],[ 5. , 17. , 0.80140835]]), array([[ 6. , 9. , 0.89909419],[ 7. , 10. , 0.52857684],[ 8. , 11. , 0.71177599]]), array([[ 9. , 14. , 0.8581396 ],[10. , 12. , 0.38934133],[11. , 13. , 0.8797749 ]]), array([[15. , 19. , 0.81766873],[16. , 18. , 0.78573793],[17. , 20. , 0.67746843]]), array([[18. , 21. , 0.63336505],[19. , 22. , 0.88562933],[20. , 23. , 0.7300858 ]]), array([[ 3. , 24. , 0.7975674 ],[ 4. , 25. , 0.53436182],[ 5. , 26. , 0.79336061]]), array([[24. , 27. , 0.80693887],[25. , 28. , 0.59622135],[26. , 29. , 0.80041958]]), array([[27. , 30. , 0.78664207],[28. , 31. , 0.73021965],[29. , 32. , 0.6312245 ]]), array([[ 3. , 33. , 0.90471435],[ 4. , 34. , 0.75671906],[ 5. , 35. , 0.75167511]]), array([[33. , 36. , 0.68868005],[34. , 37. , 0.86412876],[35. , 38. , 0.71096365]]), array([[36. , 39. , 0.82994086],[37. , 40. , 0.86046369],[38. , 41. , 0.9100325 ]]), array([[3. , 1. , 0.96472907],[4. , 0. , 0.97379622],[5. , 2. , 0.42410478]]), array([[ 0. , 42. , 0.8114687 ],[ 1. , 43. , 0.72544987],[ 2. , 44. , 0.90721482]]), array([[44. , 49. , 0.65025106]]), array([[ 0. , 45. , 0.7345252 ],[ 1. , 46. , 0.74511886],[ 2. , 47. , 0.83590513]]), array([[45. , 51. , 0.72804518],[46. , 50. , 0.90572883],[47. , 52. , 0.66244994]]), array([], shape=(0, 3), dtype=float64), array([], shape=(0, 3), dtype=float64)] '''同樣是被分組了,總共有mapIdx對應19種連接方法,因為考慮到多人情況,所以每個連接方法又對應多條連接線。我們把這些邊全連起來看看:
img_show = img.copy() for pair in valid_pairs:for i in range(pair.shape[0]):conA = keypoints_list[int(pair[i][0])].astype(int)conB = keypoints_list[int(pair[i][1])].astype(int)cv2.line(img_show, (conA[0], conA[1]), (conB[0], conB[1]), colors[i], 3, cv2.LINE_AA)plt.imshow(img_show) plt.axis('off')看著基本沒連錯,第一個人的肩膀不會連到第二個人的胳膊肘,其它關鍵點一樣。
分開存儲
原理很簡單,就是把有連接的邊放到一個集合里面
# 根據獲得的能被連接的關鍵點對,把坐標也對應好 def getPersonwiseKeyPoints(valid_pairs,invalid_pairs,keypoints_list):personwiseKeypoints = -1 * np.ones((0,19))for k in range(len(mapIdx)): #遍歷有效的關節連接if(k not in invalid_pairs): #當前關節存在partAs = valid_pairs[k][:,0] #所有人第一個關節索引partBs = valid_pairs[k][:,1] #所有人第二個關節索引indexA,indexB = np.array(POSE_PAIRS[k]) #對應肢體的關鍵點索引for i in range(len(valid_pairs[k])): #當前關節有多少個數據點(人)found = 0person_idx = -1for j in range(len(personwiseKeypoints)):#遍歷人 if(personwiseKeypoints[j][indexA]==partAs[i]):person_idx = jfound=1breakif(found):personwiseKeypoints[person_idx][indexB] = partBs[i]personwiseKeypoints[person_idx][-1] += keypoints_list[partBs[i].astype(int),2]+valid_pairs[k][i][2]elif not found and k<17:row = -1*np.ones(19)row[indexA] = partAs[i]row[indexB] = partBs[i]row[-1] = sum(keypoints_list[valid_pairs[k][i,:2].astype(int),2]) + valid_pairs[k][i][2]personwiseKeypoints = np.vstack([personwiseKeypoints,row])return personwiseKeypoints這塊代碼自己實現也行,反正能連接的邊都在上一步知道了。這里只需要先執行后面的not found,構建幾個personwiseKeypoints,然后再執行上面的found不斷把上一個節點能連的下一個節點塞到對應位置。
輸出:
personwiseKeypoints = getPersonwiseKeyPoints(valid_pairs,invalid_pairs,keypoints_list) print(personwiseKeypoints) ''' [[ 1. 3. 6. 9. 14. 16.18. 21. 24. 27. 30. 33.36. 39. 43. 46. -1. 50.25.16836102][ 0. 4. 7. 10. 12. 15.19. 22. 25. 28. 31. 34.37. 40. 42. 45. -1. 51.22.83992412][ 2. 5. 8. 11. 13. 17.20. 23. 26. 29. 32. 35.38. 41. 44. 47. 49. 52.25.00522498]]'''從結果上來看是三個人,可視化看看
for i in range(17):for n in range(len(personwiseKeypoints)):index = personwiseKeypoints[n][np.array(POSE_PAIRS[i])]if -1 in index:continueB = np.int32(keypoints_list[index.astype(int), 0])A = np.int32(keypoints_list[index.astype(int), 1])cv2.line(img_show, (B[0], A[0]), (B[1], A[1]), colors[i], 3, cv2.LINE_AA)plt.figure(figsize=[15,15]) plt.imshow(img_show[:,:,[2,1,0]])后記
本博客對應代碼:
鏈接:https://pan.baidu.com/s/1ywFPXyTr-9vWbQnUIdjT2g
提取碼:ajcl
后面有機會再解讀一下openpose的網絡搭建理論吧。
有興趣的可以先看看:
《Convolutional Pose Machines》
《OpenPose: Realtime Multi-Person 2D Pose Estimation using Part Affinity Fields》
Stacked Hourglass Networks for Human Pose Estimation
本文以同步到微信公眾號中,代碼也在公眾號簡介的GitHub中,有興趣可以關注一波:
總結
以上是生活随笔為你收集整理的Openpose推断阶段原理的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 聊聊PAAS
- 下一篇: Bing地图切片原理