OpenPose(tensorflow)工程详解系列(二)—— 详细解读以及工程梳理

一、训练数据准备

标注完的数据是没法直接用来训练的,还需要制作成训练可以用(或者说训练代码可以直接读取使用)的文本。通常文本保存的内容是训练集图片和标签的名字索引,这样方便对数据进行统一管理和操作。根据下面的代码生成文本数据(train.txtvalid.txttest.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()

二、数据读取和处理

  1. 数据的读取
    该部分采用Python多线程数据读取方式,可以参考我之前写的博文(https://blog.csdn.net/kxh123456/article/details/109623897)。该博文详细的介绍了如何利用Python多线程和队列并行处理读取,并且兼顾CPU和GPU的计算效率,从而保证训练过程更快。

  2. 标签数据处理
    近些年的关键点识别任务中,通常不是将关键点的二维数字坐标作为网络输出的损失计算。为了提升关键点的预测效果,当前的关键点识别网络都采用二维高斯热量图(heatmap)来表达单个关键点,同时用二维的向量表达关键点之间的连接(vectormap)。下面将实例介绍如何根据真实关键点坐标生成每个关键点的热量图和向量图。

  3. 生成热量图

    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)
    
  4. 生成向量连接图

    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算法的基本流程如下:
  1. W*H 大小的彩色图像作为输入。
  2. 经过VGG的前10层网络得到一个特征F.
  3. 网络分成两个循环分支:一个分支用于预测置信图S:关键点(人体关节);另一个分支用于预测关节点之间的亲和度L:像素点在骨架中的走向(肢体)。
  4. 第一个循环分支以特征F作为输入,得到一组S1,L1.
  5. 第二个分支之后,分别以上一个分支的输出 S t − 1 , L t − 1 S_{t-1}, L_{t-1} St1,Lt1特征F作为输入。
  6. 网络最终输出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/outputstrideH/outputstrideJ]),J 表示预测单个人的关节点数量。
    在这里插入图片描述
  7. 损失函数计算 S,L 的预测值与 g r o u n d t r u t h ( S ∗ , L ∗ ) groundtruth(S^*,L^*) groundtruth(SL)之间的L2范数。如果某个关键点标注缺失则不计算该点, W ( p ) = 0 W(p)=0 W(p)=0表示关键点缺失, W ( p ) = 1 W(p)=1 W(p)=1表示关键点存在,损失函数公式如下:
    在这里插入图片描述
    总的损失函数为各项之和:
    在这里插入图片描述

核心点解析:

  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 关节的响应,出现这种情况时,我们取最大响应值,即图中的虚线。
    在这里插入图片描述
    在测试的阶段,我们通常使用非最大值抑制的方式获得最终的预测位置。

  2. 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 满足以下两个条件,则判定像素 plimb c上。 其中, x j 1 , k , x j 2 , k x_{j_1,k},x_{j_2,k} xj1,kxj2,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方向),公式如下所示:
    在这里插入图片描述

  3. 在模型 inference 阶段,我们可以得到关节置信图和关节亲和场的预测值,还需要将各个人的关节点联接起来,本文使用了最大边权的匈牙利算法进行二分图匹配。
    为了减少计算量,本文将关节点多分图转化为二分图,然后进行匹配,如下图所示:
    在这里插入图片描述

  4. 二分图原理:二分图是图论中的一种特殊模型。设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的线性积分来表征骨骼点对的相关性,也即表征了骨骼点对的置信度,公式表示如下
    在这里插入图片描述
    为了快速计算积分,一般采用均匀采样的方式近似这两个关节点间的相似度,
    在这里插入图片描述

四、工程解析

由于代码量很大,博客的篇幅有限,所以不进行逐个脚本的解析,需要的话留言咨询。

五、参考资源

  1. https://blog.csdn.net/htt789/article/details/80283370
  2. https://zhuanlan.zhihu.com/p/48507352
  3. 二分图详解
  4. 匈牙利算法
  5. https://blog.csdn.net/magic_ll/category_8260264.html
  • 15
    点赞
  • 96
    收藏
    觉得还不错? 一键收藏
  • 25
    评论
评论 25
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值