Openpose推斷階段原理

前言

之前出過一個關於openpose配置的博客,不過那個代碼雖然寫的很好,而且是官方的,但是分析起來很困難,然後再opencv相關博客中找到了比較清晰的實現,這裏分析一波openpose的推斷過程。

國際慣例,參考博客:

opencv官方文檔,只有單人

大佬的實現,包括多人

解讀

直接使用opencvdnn庫調用openposecaffe模型,然後對輸出進行後處理。重點是代表關節連接親密度的親和場的解析。

網絡輸出解析

推斷階段的模型結構(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.0
        decay_mult: 1
      }
      param {
        lr_mult: 8.0
        decay_mult: 0
      }
      convolution_param {
        num_output: 19
        pad: 0
        kernel_size: 1
        weight_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.0
        decay_mult: 1
      }
      param {
        lr_mult: 8.0
        decay_mult: 0
      }
      convolution_param {
        num_output: 38
        pad: 0
        kernel_size: 1
        weight_filler {
          type: "gaussian"
          std: 0.01
        }
        bias_filler {
          type: "constant"
        }
      }
    }
    

可以發現,被拼接的兩個層分別具有19和38個特徵圖。對照的網絡結構圖

在這裏插入圖片描述
兩個stage,每個stage有兩個branch:第一個branch輸出1919個特徵圖,分別代表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的人體人體關鍵點

在這裏插入圖片描述

調用模型

直接用opencvdnn.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)關節的關節11的親和場PAF特徵圖,(1,5)(1,5)關節的關節11的親和場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 = 0
    threshold = 0.1
    for 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個關節位置、置信度、id
            keypoints_list = np.vstack([keypoints_list,keypoints[i]])
            keypoints_id += 1
        detected_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)

把關鍵點可視化瞅瞅唄:

#可視化關鍵點
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]] #找到第二個關節的位置(所有人)

再看看親和場怎麼算的,先看論文的圖

在這裏插入圖片描述

就是直接把兩個關鍵點連起來,中間做一條線,計算親和場上這條線上的值。

公式表達爲
在這裏插入圖片描述
先計算dj2dj1dj2dj12\frac{d_{j_2}-d_{j_1}}{\parallel d_{j_2}-d_{j_1} \parallel}_2

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:
	continue

畫一條直線過去,得到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 = j
		maxScore = avg_paf_score
		found = 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 = 10
    paf_score_th = 0.1
    conf_th = 0.7
    for k in range(len(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]] #找到第二個關節的位置(所有人)
        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 = -1
                maxScore = -1
                found = 0
                for 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:
                        continue
                    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_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 = j
                            maxScore = avg_paf_score
                            found = 1
                if 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 = 0
                person_idx = -1
                for j in range(len(personwiseKeypoints)):#遍歷人                    
                    if(personwiseKeypoints[j][indexA]==partAs[i]):
                        person_idx = j
                        found=1
                        break
                if(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:
            continue
        B = 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中,有興趣可以關注一波:
在這裏插入圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章