Vorwort
Codeadresse: AlphaPose-Pytorch-Version
In diesem Artikel wird Bild 1.jpg (854 x 480) als Beispiel verwendet, um die Details des gesamten Vorhersageprozesses zu interpretieren und aufzuzeichnen.
python demo.py --indir examples/demo --outdir examples/res --save_img
1. YOLO
1.1 Bildvorverarbeitung
- cv2 liest das BGR-Bild
img [480,854,3] (h,w,c)
- Skalieren Sie das Bild entsprechend den Parametern
inp_dim=608
, um das Seitenverhältnis beizubehalten[341,608,3]
, und führen Sie eine Auffüllung mit einem Wert von 128 to durch[608,608,3]
- Konvertieren Sie BGR in RGB, Dimensionsverarbeitung, konvertieren Sie in Tensor, konvertieren Sie den Datentyp in Float, teilen Sie den Wert zur Normalisierung durch 255.
[1,3,608,608] (b,c,h,w)
Ausgabe:
img: [1,3,608,608] (yolo输入图像)
orig_img: [480,854,3] (原始BGR图像)
im_name: 'examples/demo/1.jpg'
im_dim_list:[854,480,854,480](原图尺寸, 用于把yolo的输出坐标转换到原图坐标系)
1.2 Argumentation des Yolo-Modells
eingeben:img[1,3,608,608]
Ausgabe:pred[1,22743,85]
1 = Batchgröße \mathrm{1=Batchgröße}1=Batchgröße
22743 = [ (608/32) 2 + (608/16) 2 + (608/8) 2] × 3 22743=[(608/32)^2+(608/16)^2+(608/8) ^2]\times322743=[( 608/32 )2+( 608/16 )2+( 608/8 )2 ]×3
85 = [ x , y , w , h , conf , 80 Klassen ] \mathrm{85=[x,y,w,h,conf,80classes]}85=[ X ,y ,w ,H ,conf ,80Klassen ]
1.3 Ausgabenachbearbeitung
(1) Erste Stufe
- Koordinaten
xywh
drehen sichxyxy
- For
batchsize
-Schleife,[22743,85]
turn[22743,7]
, 7 = [ x , y , x , y , conf , class score , class ] \mathrm{7=[x,y,x,y,conf,class\ score,class]}7=[ X ,y ,X ,y ,conf ,Klassenergebnis , _ class ] , das heißt, die 80 Klassenwerte werden in den Klassenindex mit der höchsten Punktzahl und dessen Wert umgewandelt - Conf ≤ 0.05 entfernen \mathrm{conf\le0.05}conf≤0,05 Begriffe
[37,7]
- Behalten Sie die Elemente, deren Kategorie „Mensch“ ist, und sortieren Sie
conf
sie von oben nach unten, um die Ergebnisse zu erhaltenimg_pred[19,7]
- NMS entfernen doppelte Ziele
[19,7]->[6,7]
- Fügen Sie
batch_idx
hier hinzu, dass die Batchgröße 1 ist, also sind sie alle 0,[6,7]->[6,8]
, 8 = [Batch-IDx, x, y, x, y, conf, Klassenpunktzahl, Klasse] \mathrm{8=[Batch\-IDx, x,y,x,y , conf,class\score,class]}8=[ Stapel -IDX , X ,y ,X ,y ,conf ,Klassenergebnis , _ Klasse ] - Konvertieren Sie die numerischen Koordinaten von
[608,608]
in die ursprünglichen Bildkoordinaten[854,480]
und setzen Sie die Koordinatenclamp
dazwischen[0,w] [0,h]
Ausgabe:
orig_img:[480,854,3] (原始BGR图像)
im_name:'examples/demo/1.jpg'
boxes: [6,4] (x,y,x,y)(原图坐标系)
scores: [6,1] (conf)
NMS-Details
nms_conf=0.6
- Fügen Sie
img_pred
das erste Element in das Ergebnis ein - Berechnen Sie iou mit allen verbleibenden Termen und dem letzten Term im Ergebnis und behalten Sie
iou<nms_conf
den Term als neuen beiimg_pred
- Schleife, bis
img_pred
keine Ziele mehr vorhanden sind - Wenn die Anzahl der Ziele nach nms mehr als 100 beträgt, wird nms
nms_conf-0.05
von Anfang animg_pred
neu gestartet
(2) Zweite Stufe
- Das Originalbild wird
orig_img [480,854,3]
von BGR in RGB konvertiert, dimensional verarbeitet, in Tensor konvertiert, der Datentyp in Float konvertiert und der Wert zur Normalisierung durch 255 geteilt, und wir erhalteninp [3,480,854]
- Verarbeiten Sie drei Kanäle
inp[0].add_(-0.406), inp[1].add_(-0.457), inp[2].add_(-0.480)
- Erweitern Sie
boxes
den Bereich des Zielfelds und speichern Sie die Koordinaten der oberen linken Eckept1
und der unteren rechten Ecke.pt2
- Schneiden Sie
boxes
jedes Ziel aus dem Bild aus, verwenden Sie eine proportionale Skalierung + Nullauffüllung, vereinheitlichen Sie es zu einem[3,320,256]
Bild der Größe und speichern Sie es.inps
Ausgabe:
inps: [6,3,320,256] (检测目标的子图像,作为Alphapose的输入)
orig_img:[480,854,3] (原始BGR图像)
im_name:'examples/demo/1.jpg'
boxes: [6,4] (x1,y1,x2,y2)(yolo原始输出,原图坐标系)
scores: [6,1] (yolo输出conf)
pt1: [6,2] (x1,y1)(yolo输出扩大后坐标,原图坐标系)
pt2: [6,2] (x2,y2)(yolo输出扩大后坐标,原图坐标系)
2. Pose
2.1 Inferenz des Posenmodells
eingeben:inps[6,3,320,256]
Ausgabe:hm[6,17,80,64]
Das heißt, 6 Ziele, jedes Ziel verfügt über eine Heatmap, die 17 Schlüsselpunkten entspricht.
2.2 Nachbearbeitung der Ausgabe
(1) Die erste Stufe: Konvertieren der Wärmekarte in Koordinaten
- Rufen Sie
hm[6,17,80,64]
den Index des Maximalwerts in der Heatmap für jeden Schlüsselpunkt abpreds[6,17,2]
- Da
opt.matching=False
hier eine einfache Nachbearbeitung verwendet wird, lautet der Quellcode wie folgt: Nehmen Sie
als Beispielpreds
einen Index , nehmen Sie die Werte der vier benachbarten oberen, unteren, linken und rechten Positionen in seiner Heatmap heraus und platzieren Sie sie sie[x,y]
jeweils in xx .x和yyVersatz 0,25 0,25in die höhere Richtung auf der y- Achse0,25
bisxxNehmen wir als Beispiel die x- Achse: pleft = hm [ y ] [ x − 1 ] p_\mathrm{left}=\mathrm{hm}[y][x-1]Plinks=hm [ y ] [ x−1 ] ,pright = hm [ y ] [ x + 1 ] p_\mathrm{right}=\mathrm{hm}[y][x+1]Prichtig=hm [ y ] [ x+1],若 p l e f t > p r i g h t p_\mathrm{left}>p_\mathrm{right} pleft>pright则 x + 0.25 x+0.25 x+0.25,若 p l e f t = p r i g h t p_\mathrm{left}=p_\mathrm{right} pleft=pright 则 x x x 保持不变
最后会在所有的坐标值上 + 0.2 +0.2 +0.2
for i in range(preds.size(0)):
for j in range(preds.size(1)):
hm = hms[i][j]
pX, pY = int(round(float(preds[i][j][0]))), int(round(float(preds[i][j][1])))
if 0 < pX < opt.outputResW - 1 and 0 < pY < opt.outputResH - 1:
diff = torch.Tensor((hm[pY][pX + 1] - hm[pY][pX - 1], hm[pY + 1][pX] - hm[pY - 1][pX]))
preds[i][j] += diff.sign() * 0.25
preds += 0.2
- 目前得到的坐标
preds[6,17,2]
是相对于输出分辨率[80,64]
坐标系下的,转换到原图分辨率[480,854]
坐标系下,得到preds_tf[6,17,2]
输出:
preds: [6,17,2] (经过第二步偏移处理后的坐标,相对于热力图坐标系)
preds_tf: [6,17,2] (最终坐标,相对于原图坐标系)
maxval: [6,17,1] (热力图最大值)
(2)第二阶段:pose nms
输入:
ori_bboxs: [6,4] (yolo原始输出,原图坐标系)
ori_bbox_scores:[6,1] (yolo输出conf)
ori_pose_preds: [6,17,2] (对应preds_tf,关键点坐标,原图坐标系)
ori_pose_scores:[6,17,1] (对应maxval,热力图最大值)
- 根据
bboxs
计算每个目标框的w,h
,选择每个目标框中的最大值max(w,h)
并乘上alpha=0.1
构成ref_dists[6]
- 根据
pose_scores
计算每个目标17个关键点得分的均值,得到human_scores[6]
- 开始循环,直到
human_scores
无目标- 选择
human_scores
最高的目标,坐标和得分分别记为pick_preds[17,2], pick_scores[17,1]
全部的坐标和得分记为all_preds[6,17,2], all_scores[6,17,1]
(此处命名方式与源码略有不同,以便于区分) - 计算距离:
final_dist[6]
目标的同类别关键点的距离,距离越近数值越大
score_dists
计算位置距离非常近的同类别关键点的得分距离
point_dist
= e − d / 2.65 =e^{-d/2.65} =e−d/2.65,因为 d ≥ 0 d\ge0 d≥0,所以 0 < p o i n t _ d i s t ≤ 1 0<\mathrm{point\_dist}\le1 0<point_dist≤1。 d d d 越小, p o i n t _ d i s t \mathrm{point\_dist} point_dist 越大,目标本身则最大全为1 - 计算关键点匹配数量:
num_match_keypoints[6]
目标之间同类别关键点中距离较近的数量 - 去除多余目标:目标之间的距离超过阈值 or 目标之间距离相近的关键点数量超过阈值 → 判定为多余的目标。
由于选出的目标本身也在其中,因此目标自身必然在去除的队伍中,如果除了自身还有目标被去除,那么会把额外的目标与自身的索引放在一起得到merge_ids
,这些目标相互之间距离很近,用于后续融合目标。
- 选择
对应第2步
def get_parametric_distance(i, all_preds, all_scores):
pick_preds, pick_scores = all_preds[i], all_scores[i]
'计算坐标位置的欧氏距离 dist[6,17](同类别关键点之间的距离)'
dist = torch.sqrt(torch.sum(torch.pow(pick_preds[np.newaxis, :] - all_preds, 2), dim=2))
'计算dist<=1的点之间的得分距离 score_dists[6,17]'
mask = (dist <= 1)
score_dists = torch.zeros(all_preds.shape[0], 17)
score_dists[mask] = torch.tanh(pick_scores[mask]/delta1) * torch.tanh(all_scores[mask]/delta1) 'delta1=1'
'final_dist[6]'
point_dist = torch.exp((-1) * dist / delta2) 'delta2=2.65'
final_dist = torch.sum(score_dists, dim=1) + mu * torch.sum(point_dist, dim=1) 'mu=1.7'
return final_dist
对应第3步
def PCK_match(pick_pred, all_preds, ref_dist):
dist = torch.sqrt(torch.sum(torch.pow(pick_preds[np.newaxis, :] - all_preds, 2), dim=2))
ref_dist = min(ref_dist, 7)
num_match_keypoints = torch.sum(dist / ref_dist <= 1, dim=1)
return num_match_keypoints
对应第4步
'gamma=22.48, matchThreds=5'
delete_ids = torch.from_numpy(np.arange(human_scores.shape[0]))[(final_dist > gamma) | (num_match_keypoints >= matchThreds)]
输出:
merge_ids: [6,x]
preds_pick: [6,17,2]
scores_pick: [6,17,1]
bbox_scores_pick: [6,1]
'''
这里的输出是从各个输入 orig_xxxx, 例如 ori_bbox_scores 中挑选出来的(nms后的目标)
只是在这个例子中, nms判断并没有重复的目标, 因此和原始输入保持一致
merge_ids 是一个列表, x代表每一项的长度, 本例中x都=1
如果nms判断存在重复目标, 那么会把这些目标在原始输入中的索引记录在 merge_ids 中, 此时x>1
在第三阶段中会把这些目标进行融合
'''
(3)第三阶段:融合与过滤
- 去除17个关键点中最高得分 m a x _ s c o r e < s c o r e T h r e d s = 0.3 \mathrm{max\_score < scoreThreds = 0.3} max_score<scoreThreds=0.3 的目标
- 融合目标,具体看下面代码,简单来说就是把距离比较近的关键点根据得分的高低作为权重,把坐标位置和得分进行加权求和作为融合后的目标
- 去除融合后,17个关键点中最高得分 m a x _ s c o r e < s c o r e T h r e d s = 0.3 \mathrm{max\_score < scoreThreds = 0.3} max_score<scoreThreds=0.3 的目标
- 根据能包含目标所有关键点的矩形框面积来过滤目标,
1.5**2 * (xmax-xmin) * (ymax-ymin) < areaThres=0
,具体为外接矩形长宽都乘1.5后计算面积,由于这里阈值为0,过滤基本无效 - 最后会把所有关键点坐标数值 − 0.3 -0.3 −0.3,并且根据关键点得分和目标框得分生成
proposal_score
,具体见下面代码
此阶段过滤掉了一个目标,最终得到5个目标。
merge_pose, merge_score = p_merge_fast(preds_pick[j], ori_pose_preds[merge_id], ori_pose_scores[merge_id], ref_dists[pick[j]])
def p_merge_fast(ref_pose, cluster_preds, cluster_scores, ref_dist):
'''
Score-weighted pose merging
INPUT: 本博客中文别称
ref_pose: reference pose -- [17, 2] 挑选目标关键点
cluster_preds: redundant poses -- [n, 17, 2] 多余目标关键点(挑选目标本身包含在多余目标中)
cluster_scores: redundant poses score -- [n, 17, 1]
ref_dist: reference scale -- Constant
OUTPUT:
final_pose: merged pose -- [17, 2]
final_score: merged score -- [17]
'''
'计算与多余目标关键点距离 dist[n,17]'
dist = torch.sqrt(torch.sum(
torch.pow(ref_pose[np.newaxis, :] - cluster_preds, 2),
dim=2
))
kp_num = 17
'回顾一下, ref_dist是挑选目标的目标框的 max(h,w)*0.1'
ref_dist = min(ref_dist, 15)
mask = (dist <= ref_dist)
final_pose = torch.zeros(kp_num, 2)
final_score = torch.zeros(kp_num)
if cluster_preds.dim() == 2:
cluster_preds.unsqueeze_(0)
cluster_scores.unsqueeze_(0)
if mask.dim() == 1:
mask.unsqueeze_(0)
# Weighted Merge
'根据pose的得分来决定每个目标所占的比例, 具体为该得分占总得分的比例'
masked_scores = cluster_scores.mul(mask.float().unsqueeze(-1))
normed_scores = masked_scores / torch.sum(masked_scores, dim=0)
'根据计算得到的比例做加权和, 得到最终的pose及其得分'
final_pose = torch.mul(cluster_preds, normed_scores.repeat(1, 1, 2)).sum(dim=0)
final_score = torch.mul(masked_scores, normed_scores).sum(dim=0)
return final_pose, final_score
final_result.append({
'keypoints': merge_pose - 0.3,
'kp_score': merge_score,
'proposal_score': torch.mean(merge_score) + bbox_scores_pick[j] + 1.25 * max(merge_score)
})
keypoints [17,2]
kp_score [17,1]
proposal_score [1]
3. Alphapose 网络结构
3.1 总流程
- SEResnet 作为 backbone 提取特征
- 用
nn.PixelShuffle(2)
提升分辨率 - 经过两个 DUC 模块进一步提升分辨率
- 通过一个卷积得到输出
- 此时获得的输出如图所示
out[6,33,80,64]
有33个关键点,通过out.narrow(1, 0, 17)
获取前17个关键点作为最终的输出hm[6,17,80,64]
3.2 DUC 模块
- 2个DUC模块结构相同,都是先用卷积升维,再用一个
nn.PixelShuffle(2)
提升分辨率 - 图中以 DUC1 模块的参数为例进行绘制
3.3 PixelShuffle 操作
import torch
import torch.nn as nn
input_tensor = torch.arange(1, 17).view(1, 16, 1, 1).float()
pixel_shuffle = nn.PixelShuffle(2)
output_tensor = pixel_shuffle(input_tensor)
print(output_tensor)
>>>
tensor([[[[ 1., 2.],
[ 3., 4.]],
[[ 5., 6.],
[ 7., 8.]],
[[ 9., 10.],
[11., 12.]],
[[13., 14.],
[15., 16.]]]])
3.4 SEResnet 框架
图中省略了 batchsize 维度,主要分为4层,分别相对原图下采样4、8、16、32倍
3.5 SEResnet 细节
仿照代码,把这4个由 Bottleneck_SE
和 Bottleneck
构成的层级记作 l a y e r 1 ∼ 4 \mathrm{layer1\sim4} layer1∼4,图中为 l a y e r 1 \mathrm{layer1} layer1 的数据。
每个层中两种 Bottleneck
都会通过三个卷积层,先把特征维度控制为输出特征维度的 1/4,第二个保持不变,第三个达到输出特征维度,再以第二层为例:
l a y e r 2 : \mathrm{layer2:} layer2:
B o t t l e n e c k _ S E : 256 → 128 → 128 → 512 \mathrm{Bottleneck\_ SE:256\to128\to128\to512} Engpass_SE:256→128→128→512
Engpass: 512 → 128 → 128 → 512 \mathrm{Flaschenhals:512\to128\to128\to512}Engpass:512→128→128→512