人体姿态估计--OpenPose算法解析

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了人体姿态估计--OpenPose算法解析相关的知识,希望对你有一定的参考价值。

参考技术A

  人体姿态估计是计算机视觉中一个很基础的问题。从名字的角度来看,可以理解为对“人体”的姿态(关键点,比如头,左手,右脚等)的位置估计。
  人体姿态估计可以分为两种思路,
(1)“top-down”,它指先检测人体区域,再检测区域内的人体关键点。

  已有"bottom-up"方法缺点:(1)未利用全局上下文先验信息,也即图片中其他人的身体关键点信息;(2)将关键点对应到不同的人物个体,算法复杂度太高。
  文章改进点:提出“Part Affinity Fields (PAFs)”,每个像素是2D的向量,用于表征位置和方向信息。基于检测出的关节点和关节联通区域,使用greedy inference算法,可以将这些关节点快速对应到不同人物个体。

  损失函数是保证网络能收敛的最重要的关键点,因此作者对两分支的损失函数均采用L2 loss。训练时,每个阶段都会产生loss,避免梯度消失;预测时只使用最后一层的输出。公式表示如下:

其中, 表示branch1 的label图,也称为heatmap; 是branch2 的label图 ,也称为vectormap。另外,考虑到有些训练数据集只标注了图片中部分人物的关节点,因此对损失函数采用了空域加权操作,W表示二值化mask矩阵,当位置p的标签缺失时其值为0,否则值为1。显然,对于未被标记的人物关节点 ,而被标记的人物关节点和非关节点 ,所以未被标记的人物关节点不会影响模型的学习过程,整个CNN网络架构的优化目标函数如下,

   实际上就是使用2D高斯分布建模,求出一张图像上身体j部位的heatmap,记第k个人的第j个关节的heatmap为 , 表示位置信息,则有:

   表示了使用part affinity fields(PAF)建模骨骼区域,对于骨骼区域内的每一个像素,使用2D向量同时表征位置和方向信息,这里的方向指代当前骨骼对应的关节点对的连接方向,对应vectormap。以下图的骨骼区域为例

  经过上述过程,我们已经得到各个关节点的坐标图--heatmap,与关节对连接的vectormap,现在的问题就是如何合理地在推理阶段将各个关节连接成一段骨骼,并将它们组装成一个人?
   关节拼接 :对于任意两个关节点位置 和 ,通过计算PAFs的线性积分来表征骨骼点对的相关性,也即表征了骨骼点对的置信度,公式表示如下, 为了快速计算积分,一般采用均匀采样的方式近似这两个关节点间的相似度,    多人检测 :由于图片中人数不确定,同时伴随遮挡、变形等问题,因此只使用上述计算关节对相似度,只能保证局部最优,因此作者利用greedy relaxation的思想生成全局较优的搭配。具体操作如下:
(1)已知不同关节点的heatmap,也就是不同人的某个关节点的点集;
(2)现在要将不同的点集进行唯一匹配,如:一群表示手肘的点集和手腕的点集,两点集中的点必须存在唯一匹配;
(3) 关节点之间的相关性PAF已知,将关键点作为图的顶点,将关键点之间的相关性PAF看为图的边权,则将多人检测问题转化为二分图匹配问题,并用匈牙利算法求得相连关键点最优匹配。

  由上图可知,COCO数据集总共有18个关键点,17个肢体骨架,但heatmap多了一个背景图,vectormap多了耳朵和肩膀的肢体,为什末要虚构这麽一个肢体呢,因为有时候人体是背对相机的,眼睛这个关键点是不可见的,为了更好的预测耳朵,引入这两个个肢体(也就是关节对:2-16和5-17)。所以总共有19个肢体,应为vectormap为矢量,预测时分为x,y两个分量,所以有19*2=38

  完全参考 https://blog.csdn.net/m0_37477175/article/details/81236115 ,结合2.4节中vectormap( )的计算公式与绿色虚线框内的区域以点集数学公式理解。
  关键是叉乘的几何意义是两个向量所组成的平行四边形的面积,所以 就表示与向量 平行距离为 的区域,也就是骨骼宽度。

后来论文作者对网络结构进行了改进,使得效果更好,速度更快,参考文献【11】。

【1】 Realtime Multi-Person 2D Pose Estimation using Part Affinity Fields
【2】 人体姿态估计的过去、现在和未来
【3】 论文解读-Realtime Multi-Person 2D Pose Estimation using Part Affinity Fields
【4】 Realtime Multi-Person 2D Pose Estimation Using Part Affinity Fields【菜鸟读者】
【5】 知乎:openpose笔记
【6】 openpose论文总结:Realtime Multi-Person 2D Pose Estimation using Part Affinity Fields
【7】 详细介绍匈牙利算法步骤
【8】 Github 项目 - OpenPose 关键点输出格式
【9】 openpose的细节处理
【10】 tf-openpose人体姿态估计标签生成--heatmap--vectormap
【11】 OpenPose: Realtime Multi-Person 2D Pose Estimation using Part Affinity Fields

基于YOLOV7的openpose人体姿态检测识别,FPS可以达到“较高”的效果


前不久yolov7(原yolov4团队)在yolov6(美团)开源不到两周的时间也更新了,
如下图所示,yolov7效果比前面的版本确实牛逼,在精度相同的情况下,速度上面提升了一大截,但是这是在比较好的设备上面;

YOLOv7 的发展方向与当前主流的实时目标检测器不同,研究团队希望它能够同时支持移动 GPU 和从边缘到云端的 GPU 设备。除了架构优化之外,该研究提出的方法还专注于训练过程的优化,将重点放在了一些优化模块和优化方法上。这可能会增加训练成本以提高目标检测的准确性,但不会增加推理成本。我想这才是运用到工业一个巨大的提升。

由于要训练自己的数据集,所以就需要人工标注数据,使用软件为labelme。该工具非常好用,基本可以满足深度学习众多任务的数据标注需求具体使用如下步骤所示:

http://labelme.csail.mit.edu/Release3.0/

安装labelme,数据标注

对于labelme标注工具,它是一个多功能的标注工具:

  • 对图像进行多边形,矩形,圆形,多段线,线段,点形式的标注(可用于目标检测,图像分割,等任务)。
  • 对图像进行进行 flag 形式的标注(可用于图像分类 和 清理 任务)。
  • 视频标注
  • 生成 VOC 格式的数据集(for semantic / instance segmentation)
  • 生成 COCO 格式的数据集(for instance segmentation)
##################
## for Python 2 ##
##################
conda create --name=labelme python=2.7
source activate labelme
# conda install -c conda-forge pyside2
conda install pyqt
pip install labelme
# 如果想安装最新版本,请使用下列命令安装:
# pip install git+https://github.com/wkentaro/labelme.git

##################
## for Python 3 ##
##################
conda create --name=labelme python=3.6
source activate labelme
# conda install -c conda-forge pyside2
# conda install pyqt
pip install pyqt5  # pyqt5 can be installed via pip on python3
pip install labelme

OPENPOSE肢体检测

完整测试代码,只需要修改模型路径和测试视频路径

import torch
import cv2
import numpy as np
import time
import torchvision
from torchvision import transforms


def xyxy2xywh(x):
    # Convert nx4 boxes from [x1, y1, x2, y2] to [x, y, w, h] where xy1=top-left, xy2=bottom-right
    y = x.clone() if isinstance(x, torch.Tensor) else np.copy(x)
    y[:, 0] = (x[:, 0] + x[:, 2]) / 2  # x center
    y[:, 1] = (x[:, 1] + x[:, 3]) / 2  # y center
    y[:, 2] = x[:, 2] - x[:, 0]  # width
    y[:, 3] = x[:, 3] - x[:, 1]  # height
    return y
def xywh2xyxy(x):
    # Convert nx4 boxes from [x, y, w, h] to [x1, y1, x2, y2] where xy1=top-left, xy2=bottom-right
    y = x.clone() if isinstance(x, torch.Tensor) else np.copy(x)
    y[:, 0] = x[:, 0] - x[:, 2] / 2  # top left x
    y[:, 1] = x[:, 1] - x[:, 3] / 2  # top left y
    y[:, 2] = x[:, 0] + x[:, 2] / 2  # bottom right x
    y[:, 3] = x[:, 1] + x[:, 3] / 2  # bottom right y
    return y
def xywhn2xyxy(x, w=640, h=640, padw=0, padh=0):
    # Convert nx4 boxes from [x, y, w, h] normalized to [x1, y1, x2, y2] where xy1=top-left, xy2=bottom-right
    y = x.clone() if isinstance(x, torch.Tensor) else np.copy(x)
    y[:, 0] = w * (x[:, 0] - x[:, 2] / 2) + padw  # top left x
    y[:, 1] = h * (x[:, 1] - x[:, 3] / 2) + padh  # top left y
    y[:, 2] = w * (x[:, 0] + x[:, 2] / 2) + padw  # bottom right x
    y[:, 3] = h * (x[:, 1] + x[:, 3] / 2) + padh  # bottom right y
    return y
def box_iou(box1, box2):
    # https://github.com/pytorch/vision/blob/master/torchvision/ops/boxes.py
    """
    Return intersection-over-union (Jaccard index) of boxes.
    Both sets of boxes are expected to be in (x1, y1, x2, y2) format.
    Arguments:
        box1 (Tensor[N, 4])
        box2 (Tensor[M, 4])
    Returns:
        iou (Tensor[N, M]): the NxM matrix containing the pairwise
            IoU values for every element in boxes1 and boxes2
    """

    def box_area(box):
        # box = 4xn
        return (box[2] - box[0]) * (box[3] - box[1])

    area1 = box_area(box1.T)
    area2 = box_area(box2.T)

    # inter(N,M) = (rb(N,M,2) - lt(N,M,2)).clamp(0).prod(2)
    inter = (torch.min(box1[:, None, 2:], box2[:, 2:]) - torch.max(box1[:, None, :2], box2[:, :2])).clamp(0).prod(2)
    return inter / (area1[:, None] + area2 - inter)  # iou = inter / (area1 + area2 - inter)
def letterbox(img, new_shape=(640, 640), color=(114, 114, 114), auto=True, scaleFill=False, scaleup=True, stride=32):
    # Resize and pad image while meeting stride-multiple constraints
    shape = img.shape[:2]  # current shape [height, width]
    if isinstance(new_shape, int):
        new_shape = (new_shape, new_shape)

    # Scale ratio (new / old)
    r = min(new_shape[0] / shape[0], new_shape[1] / shape[1])
    if not scaleup:  # only scale down, do not scale up (for better test mAP)
        r = min(r, 1.0)

    # Compute padding
    ratio = r, r  # width, height ratios
    new_unpad = int(round(shape[1] * r)), int(round(shape[0] * r))
    dw, dh = new_shape[1] - new_unpad[0], new_shape[0] - new_unpad[1]  # wh padding
    if auto:  # minimum rectangle
        dw, dh = np.mod(dw, stride), np.mod(dh, stride)  # wh padding
    elif scaleFill:  # stretch
        dw, dh = 0.0, 0.0
        new_unpad = (new_shape[1], new_shape[0])
        ratio = new_shape[1] / shape[1], new_shape[0] / shape[0]  # width, height ratios

    dw /= 2  # divide padding into 2 sides
    dh /= 2

    if shape[::-1] != new_unpad:  # resize
        img = cv2.resize(img, new_unpad, interpolation=cv2.INTER_LINEAR)
    top, bottom = int(round(dh - 0.1)), int(round(dh + 0.1))
    left, right = int(round(dw - 0.1)), int(round(dw + 0.1))
    img = cv2.copyMakeBorder(img, top, bottom, left, right, cv2.BORDER_CONSTANT, value=color)  # add border
    return img, ratio, (dw, dh)
def non_max_suppression_kpt(prediction, conf_thres=0.25, iou_thres=0.45, classes=None, agnostic=False, multi_label=False,labels=(), kpt_label=False, nc=None, nkpt=None):
    """Runs Non-Maximum Suppression (NMS) on inference results

    Returns:
         list of detections, on (n,6) tensor per image [xyxy, conf, cls]
    """
    if nc is None:
        nc = prediction.shape[2] - 5  if not kpt_label else prediction.shape[2] - 56 # number of classes
    xc = prediction[..., 4] > conf_thres  # candidates

    # Settings
    min_wh, max_wh = 2, 4096  # (pixels) minimum and maximum box width and height
    max_det = 300  # maximum number of detections per image
    max_nms = 30000  # maximum number of boxes into torchvision.ops.nms()
    time_limit = 10.0  # seconds to quit after
    redundant = True  # require redundant detections
    multi_label &= nc > 1  # multiple labels per box (adds 0.5ms/img)
    merge = False  # use merge-NMS

    t = time.time()
    output = [torch.zeros((0,6), device=prediction.device)] * prediction.shape[0]
    for xi, x in enumerate(prediction):  # image index, image inference
        # Apply constraints
        # x[((x[..., 2:4] < min_wh) | (x[..., 2:4] > max_wh)).any(1), 4] = 0  # width-height
        x = x[xc[xi]]  # confidence

        # Cat apriori labels if autolabelling
        if labels and len(labels[xi]):
            l = labels[xi]
            v = torch.zeros((len(l), nc + 5), device=x.device)
            v[:, :4] = l[:, 1:5]  # box
            v[:, 4] = 1.0  # conf
            v[range(len(l)), l[:, 0].long() + 5] = 1.0  # cls
            x = torch.cat((x, v), 0)

        # If none remain process next image
        if not x.shape[0]:
            continue

        # Compute conf
        x[:, 5:5+nc] *= x[:, 4:5]  # conf = obj_conf * cls_conf

        # Box (center x, center y, width, height) to (x1, y1, x2, y2)
        box = xywh2xyxy(x[:, :4])

        # Detections matrix nx6 (xyxy, conf, cls)
        if multi_label:
            i, j = (x[:, 5:] > conf_thres).nonzero(as_tuple=False).T
            x = torch.cat((box[i], x[i, j + 5, None], j[:, None].float()), 1)
        else:  # best class only
            if not kpt_label:
                conf, j = x[:, 5:].max(1, keepdim=True)
                x = torch.cat((box, conf, j.float()), 1)[conf.view(-1) > conf_thres]
            else:
                kpts = x[:, 6:]
                conf, j = x[:, 5:6].max(1, keepdim=True)
                x = torch.cat((box, conf, j.float(), kpts), 1)[conf.view(-1) > conf_thres]


        # Filter by class
        if classes is not None:
            x = x[(x[:, 5:6] == torch.tensor(classes, device=x.device)).any(1)]

        # Apply finite constraint
        # if not torch.isfinite(x).all():
        #     x = x[torch.isfinite(x).all(1)]

        # Check shape
        n = x.shape[0]  # number of boxes
        if not n:  # no boxes
            continue
        elif n > max_nms:  # excess boxes
            x = x[x[:, 4].argsort(descending=True)[:max_nms]]  # sort by confidence

        # Batched NMS
        c = x[:, 5:6] * (0 if agnostic else max_wh)  # classes
        boxes, scores = x[:, :4] + c, x[:, 4]  # boxes (offset by class), scores
        i = torchvision.ops.nms(boxes, scores, iou_thres)  # NMS
        if i.shape[0] > max_det:  # limit detections
            i = i[:max_det]
        if merge and (1 < n < 3E3):  # Merge NMS (boxes merged using weighted mean)
            # update boxes as boxes(i,4) = weights(i,n) * boxes(n,4)
            iou = box_iou(boxes[i], boxes) > iou_thres  # iou matrix
            weights = iou * scores[None]  # box weights
            x[i, :4] = torch.mm(weights, x[:, :4]).float() / weights.sum(1, keepdim=True)  # merged boxes
            if redundant:
                i = i[iou.sum(1) > 1]  # require redundancy

        output[xi] = x[i]
        if (time.time() - t) > time_limit:
            print(f'WARNING: NMS time limit time_limits exceeded')
            break  # time limit exceeded

    return output
def output_to_keypoint(output):
    # Convert model output to target format [batch_id, class_id, x, y, w, h, conf]
    targets = []
    for i, o in enumerate(output):
        kpts = o[:,6:]
        o = o[:,:6]
        for index, (*box, conf, cls) in enumerate(o.detach().cpu().numpy()):
            targets.append([i, cls, *list(*xyxy2xywh(np.array(box)[None])), conf, *list(kpts.detach().cpu().numpy()[index])])
    return np.array(targets)
def plot_skeleton_kpts(im, kpts, steps, orig_shape=None):
    #Plot the skeleton and keypointsfor coco datatset
    palette = np.array([[255, 128, 0], [255, 153, 51], [255, 178, 102<

以上是关于人体姿态估计--OpenPose算法解析的主要内容,如果未能解决你的问题,请参考以下文章

jetson nano上部署人体姿态估计算法openpose并封装应用详细过程

多目标姿态估计

多目标姿态估计

OpenPose 安装和使用教程

人体姿态估计应用源码文末-行为安全监测系统姿态纠正系统健身评分系统

Python 三维姿态估计+Unity3d 实现 3D 虚拟现实交互游戏