一、训练数据准备
标注完的数据是没法直接用来训练的,还需要制作成训练可以用(或者说训练代码可以直接读取使用)的文本。通常文本保存的内容是训练集图片和标签的名字索引,这样方便对数据进行统一管理和操作。根据下面的代码生成文本数据(train.txt
, valid.txt
,test.txt
)
import math
import numpy as np
import os
def gene_txt():
root_path_1 = '../train_0703/'
root_path = '../train_0628/'
image_path = root_path_1 + 'render/'
label_path = root_path_1 + 'label/'
file_names = os.listdir(image_path)
file_names.sort()
num_file = len(file_names)
train = open(root_path_1 + 'train.txt', 'a+')
valid = open(root_path_1 + 'valid.txt', 'a+')
test = open(root_path_1 + 'test.txt', 'a+')
for i in range(num_file):
if i < num_file*0.8:
file_name = file_names[i][:-4]
new_data = image_path+file_name + '.png'
new_label = label_path+file_name + '.txt'
# train_list.append([new_data, new_label])
train.write(new_data + ' ' + new_label + '\n')
elif i < num_file:
file_name = file_names[i][:-4]
new_data = image_path + file_name + '.png'
new_label = label_path + file_name + '.txt'
valid.write(new_data + ' ' + new_label + '\n')
else:
print('make test file')
file_name = file_names[i][:-4]
new_data = image_path + file_name + '.png'
new_label = label_path + file_name + '.txt'
valid.write(new_data + ' ' + new_label + '\n')
if __name__ == "__main__":
gene_txt()
二、数据读取和处理
-
数据的读取
该部分采用Python多线程数据读取方式,可以参考我之前写的博文(https://blog.csdn.net/kxh123456/article/details/109623897)。该博文详细的介绍了如何利用Python多线程和队列并行处理读取,并且兼顾CPU和GPU的计算效率,从而保证训练过程更快。 -
标签数据处理
近些年的关键点识别任务中,通常不是将关键点的二维数字坐标作为网络输出的损失计算。为了提升关键点的预测效果,当前的关键点识别网络都采用二维高斯热量图(heatmap
)来表达单个关键点,同时用二维的向量表达关键点之间的连接(vectormap
)。下面将实例介绍如何根据真实关键点坐标生成每个关键点的热量图和向量图。 -
生成热量图
def put_heatmap(self, heatmap, plane_idx, center, sigma): """ 功能:利用高斯公式计算单个关节点的热量图 :param heatmap: 存储计算的热量图 :param plane_idx: 关键点索引 :param center: 关键点的坐标(也即是高斯的中心) :param sigma: 高斯公式的参数 :return: 计算的热量图 """ center_x, center_y = center _, height, width = heatmap.shape[:3] th = 4.6052 delta = math.sqrt(th * 2) x0 = int(max(0, center_x - delta * sigma)) y0 = int(max(0, center_y - delta * sigma)) x1 = int(min(width, center_x + delta * sigma)) y1 = int(min(height, center_y + delta * sigma)) for y in range(y0, y1): for x in range(x0, x1): d = (x - center_x) ** 2 + (y - center_y) ** 2 exp = d / 2.0 / sigma / sigma if exp > th: continue heatmap[plane_idx][y][x] = max(heatmap[plane_idx][y][x], math.exp(-exp)) heatmap[plane_idx][y][x] = min(heatmap[plane_idx][y][x], 1.0) return heatmap
def get_heatmap(self, label, sign=True): """ 功能:生成openpose训练用的热量图 :param label: 已标注的真实2D关键点坐标 :param sign: :return: 生成的热量图数据 """ heatmap = np.zeros((self.point_num, self.output_size[0], self.output_size[1]), dtype=np.float32) for idx in range(self.point_num): joints = label[idx] if joints[0] < 0 or joints[1] < 0: continue self.put_heatmap(heatmap, idx, joints, self.sigma) heatmap = heatmap.transpose((1, 2, 0)) heatmap[:, :, -1] = np.clip(1 - np.amax(heatmap, axis=2), 0.0, 1.0) # background return heatmap.astype(np.float16)
-
生成向量连接图
def put_vectormap(self, vectormap, countmap, plane_idx, center_from, center_to, threshold=1): """ 功能: 计算每个两个关节点向量的x,y方向上的map :param vectormap: :param countmap: :param plane_idx: 关节点索引 :param center_from: 向量终点 :param center_to: 向量起点 :param threshold: 向量叉乘的范围限定阈值 :return: """ _, height, width = vectormap.shape[:3] vec_x = center_to[0] - center_from[0] vec_y = center_to[1] - center_from[1] min_x = max(0, int(min(center_from[0], center_to[0]) - threshold)) min_y = max(0, int(min(center_from[1], center_to[1]) - threshold)) max_x = min(width, int(max(center_from[0], center_to[0]) + threshold)) max_y = min(height, int(max(center_from[1], center_to[1]) + threshold)) norm = math.sqrt(vec_x ** 2 + vec_y ** 2) if norm == 0: return vec_x /= norm vec_y /= norm for y in range(min_y, max_y): for x in range(min_x, max_x): bec_x = x - center_from[0] bec_y = y - center_from[1] dist = abs(bec_x * vec_y - bec_y * vec_x) if dist > threshold: continue countmap[plane_idx][y][x] += 1 vectormap[plane_idx * 2 + 0][y][x] = vec_x vectormap[plane_idx * 2 + 1][y][x] = vec_y
def get_vectormap(self, label, sign = True): """ 功能: 生成OpenPose的向量图(PAF标签), 因为每两个关键点的连线有两个方向(x-axis, y-axis), vectormap是heatmap的2倍 :param joint: 已标注的真实2D关键点坐标 :param sign: :return: """ vectormap = np.zeros((len(self.shuffle_ref)*2, self.output_size[0], self.output_size[1]), dtype=np.float32) countmap = np.zeros((len(self.shuffle_ref), self.output_size[0], self.output_size[1]), dtype=np.int16) for plane_idx, (j_idx1, j_idx2) in enumerate(self.shuffle_ref): center_from = label[j_idx1] center_to = label[j_idx2] if center_from[0] < -100 or center_from[1] < -100 or center_to[0] < -100 or center_to[1] < -100: continue self.put_vectormap(vectormap, countmap, plane_idx, center_from, center_to) vectormap = vectormap.transpose((1, 2, 0)) nonzeros = np.nonzero(countmap) for p, y, x in zip(nonzeros[0], nonzeros[1], nonzeros[2]): if countmap[p][y][x] <= 0: continue vectormap[y][x][p * 2 + 0] /= countmap[p][y][x] vectormap[y][x][p * 2 + 1] /= countmap[p][y][x] return vectormap.astype(np.float16)
三、OpenPose算法流程
论文下载: Realtime Multi-Person 2D Pose Estimation using Part Affinity Fields.
开源链接: https://github.com/CMU-Perceptual-Computing-Lab/openpose.
论文翻译: https://blog.csdn.net/kxh123456/article/details/114585736
OpenPose算法的基本流程如下:
- 以 W*H 大小的彩色图像作为输入。
- 经过
VGG
的前10层网络得到一个特征F. - 网络分成两个循环分支:一个分支用于预测置信图S:关键点(人体关节);另一个分支用于预测关节点之间的亲和度L:像素点在骨架中的走向(肢体)。
- 第一个循环分支以特征F作为输入,得到一组S1,L1.
- 第二个分支之后,分别以上一个分支的输出 S t − 1 , L t − 1 S_{t-1}, L_{t-1} St−1,Lt−1和特征F作为输入。
- 网络最终输出S,L(S:
[
W
/
o
u
t
p
u
t
s
t
r
i
d
e
,
H
/
o
u
t
p
u
t
s
t
r
i
d
e
,
J
]
[W/{outputstride},H/outputstride,J]
[W/outputstride,H/outputstride,J]),J 表示预测单个人的关节点数量。
- 损失函数计算 S,L 的预测值与
g
r
o
u
n
d
t
r
u
t
h
(
S
∗
,
L
∗
)
groundtruth(S^*,L^*)
groundtruth(S∗,L∗)之间的L2范数。如果某个关键点标注缺失则不计算该点,
W
(
p
)
=
0
W(p)=0
W(p)=0表示关键点缺失,
W
(
p
)
=
1
W(p)=1
W(p)=1表示关键点存在,损失函数公式如下:
总的损失函数为各项之和:
核心点解析:
-
Confidence Maps for Part Detection. 为了在训练过程中评估 f S f_S fS,我们需要生成真实标签的置信图。每一张置信图表示某个关键点在某个像素位置出现的置信度。单人的置信图生成方式如下,k 表示每个人,j 表示身体某个部位的关键点,p 表示像素位置(也即是实际的标注位置),
在训练阶段,因为最大的峰值更加具有可信度,所以我们取最大的响应值表示预测的位置,具体公式如下:
如果一个像素点存在多个响应,意味这一张图中有多个人,见下图。下图表示关节点 j j j 的置信图,2条高斯曲线表示像素 p p p 对2个人 j j j 关节的响应,出现这种情况时,我们取最大响应值,即图中的虚线。
在测试的阶段,我们通常使用非最大值抑制的方式获得最终的预测位置。 -
Part Affinity Fields for Part Association(PAF). 当检测出多个人体关节点后,本文提出PAF将每个关节点和不同人对应起来。PAF即关节亲和场 L c , k ∗ ( p ) L^*_{c,k}(p) Lc,k∗(p),表示第k个人的 limb c(两两关节的联接)是否存在于某像素点,若存在,其值为 limb c 的单位向量,否则为0,见下式:
如果 v 满足以下两个条件,则判定像素 p 在 limb c上。 其中, x j 1 , k , x j 2 , k x_{j_1,k},x_{j_2,k} xj1,k,xj2,k表示关节点的真实坐标值。 l c , k l_{c,k} lc,k 表示limb的长度。 σ l \sigma_l σl表示 limb 的宽度。具体公式如下所示:
如果多个人的 limb c 有重叠,则取重叠区域像素点的平均单位向量,见下式,最终,一张图对应的PAF为 L c ∗ ( p ) L^*_c(p) Lc∗(p),其维度为W ×H × J × 2,× 2表示单位向量v是二维(X方向和Y方向),公式如下所示:
-
在模型 inference 阶段,我们可以得到关节置信图和关节亲和场的预测值,还需要将各个人的关节点联接起来,本文使用了最大边权的匈牙利算法进行二分图匹配。
为了减少计算量,本文将关节点多分图转化为二分图,然后进行匹配,如下图所示:
-
二分图原理:二分图是图论中的一种特殊模型。设G=(V,E)是一个无向图,如果顶点 V 可分割为两个互不相交的子集(A,B),并且图中的每条边(i,j)所关联的两个顶点 i i i 和 j j j 分别属于这两个不同的顶点集(i in A,j in B),则称图 G 为一个二分图,
二分图匹配:二分图上进行匹配,一个点群中的点只与另一个点群中的点进行唯一匹配,即任意两条边没有公共顶点 。
匈牙利算法:匈牙利算法的目标是找出limb c集合Zc中边权和最大的组合,见下式(12)。
关节拼接:对于任意两个关节点位置 d j 1 d_{j1} dj1和 d j 2 d_{j2} dj2,通过计算PAFs的线性积分来表征骨骼点对的相关性,也即表征了骨骼点对的置信度,公式表示如下
为了快速计算积分,一般采用均匀采样的方式近似这两个关节点间的相似度,
四、工程解析
由于代码量很大,博客的篇幅有限,所以不进行逐个脚本的解析,需要的话留言咨询。