\u200E
使用关键点检测打造小工具Padoodle,让涂鸦小人跟随真人学跳舞
发布日期:2022-03-16T07:09:25.000+0000 浏览量:184次
Windows自带的画图软件曾陪伴了我小时候最初接触电脑的几年时光,这个简单的小工具对于小时候的我来说神奇,仿佛什么都可以画出来。我画过很多人像,也曾幻想着让这些人像跟着我一起动起来(当时还不知道有flash这种东西)。前段时间,我使用飞桨实现了一个让涂鸦动起来的小项目,今天来和大家分享我小时候的想法——让涂鸦小人跟着人动起来。

MetaAI发布的项目


想让涂鸦小人和我做出一样的动作,首先我们需要一个人体关键点检测模型。飞桨提供人体关键点检 测模型的有 飞桨预训练模型应用工具 PaddleHub 飞桨目标检测套件 PaddleDetection
本项目使用的是 PaddleHub 中的 human_pose_estimation_resnet50_mpii 模型。这个模型相对于 openpose 更快,但是效果稍微差一点,如果对涂鸦小人动作的准确度需求特别高,或者希望模型返回关键点坐标的置信度信息的,可以试试 openpose_body_estimation 模型,或是 PaddleDetection 中的 HRNet 系列模型和最新的 PP-TinyPose

PaddleDetection链接: https://github.com/PaddlePaddle/PaddleDetection
PaddleHub链接: https://github.com/PaddlePaddle/PaddleHub
由于涂鸦中的每一个点都要与骨骼点绑定在一起,所以骨骼点并不能太少,否则与骨骼点不相近的点也会因为一些特殊原因绑定在一起。我们需要对检测出的骨骼点 K 进行一定的扩充,本项目在每两个相邻骨骼点之间计算中点作为扩充的骨骼点,然后重 复这个操作两次。
human_pose_estimation_resnet50_mpii这个模型的训练数据集是mpii,有16个关键点。我们先在“thorax”和“pelvis”两个关键点之间创建一个中心点作为所有关键点的根。然后构建我们整个人体的一个以关键点为结点的树形结构。这个树形结构在后面都会用到。最终,我们扩展后的关键点个数为65,这里将这个扩展的关键点组记为K’。
将关键点检测模型封装一下:
        
          
class estUtil():
     #封装的关键点检测类
     def __init__(self):
         super(estUtil,  self).__init_ _()
         # 使用human_pose_estimation_resnet50_mpii模型
         self. module = hub.Module(name= 'human_pose_estimation_resnet50_mpii')

     def do_est(self, frame):
        res =  self. module.keypoint_detection(images=[frame], use_gpu=True)
         return res[ 0][ 'data']
扩充关键点的方法:
        
          
def complexres(res, FatherAndSon):
#扩充关键点,但仍然要保持逻辑上的关键点的节点顺序
    cres = copy.deepcopy(res)
    for key,pos in res.items():
        father = FatherAndSon[key]
        if father == key:
#当时根节点的时候停止
            continue 
        if key[0] == 'm' or father[0] == 'm':
#子节点第一种命名规则
            midkey = 'm'+key+'_'+father
        else:
            kn = ''
            for t in key.split('_'):
                kn += t[0]
            fn = ''
            for t in father.split('_'):
                fn += t[0]
#子节点第二种命名规则
            midkey = 'm _'+kn+'_'+fn
#计算中点,并把结果按逻辑顺序存到字典中
        midvalue = [ (pos[0] + res[ father][ 0]) / 2, (pos[ 1] + res[ father][ 1])/2]
        FatherAndSon[key] = midkey
        FatherAndSon[midkey] = father
        cres[midkey] = midvalue
    return cres, FatherAndSon

左为扩展前的关键点k,右为扩展后的关键点k’



涂鸦的记录与优化

为了方便交互,我使用OpenCV制作了一个简单的小画板,用户可以选择不同的颜色来绘制涂鸦。这里为了后续绑定关键点与涂鸦,我先将模板的人体关键点画在了画布上,用户就拥有了一套参考坐标,更方便地绘制自己的涂鸦小人。

OpenCV会在用户按下鼠标后不断地采集我们的画笔(鼠标)的当前位置(Mouse_x,Mouse_y)。当用户松开鼠标后,程序则停止记录。将刚才记录的点依次连接起来,就是我们鼠标刚才点击并移动的轨迹。一幅画可以由一笔完成,也可以多笔多个颜色来完成。我们把这里得到的涂鸦小人记作B。
由于OpenCV是按照一定的帧率来采集我们的画笔位置,对于同样长的一条线,如果我们画得慢,则取样的点就会多;如果我们画得快,则取样的点就会少。在后续的使用中,因为要大量的计算取样点和骨骼点的相对关系(通过评估,这部分的时间会远远大于模型的运算时间,成为了流畅运行的瓶颈),所以这里我们要对这些取样点B进行过滤,我这里使用最直观的过滤方法,即当连续的三个点在同一条直线上时,将中间的那个点过滤掉,只保留两个端点。通过这个方法,一个简单的涂鸦小人的点数可以从几千个降低到几十个,项目也能更平滑的运行。这里我们将过滤后的取样点记为B’。
过滤简化皮肤数据的方法:
         
           
def linesFilter():
    global lines
    for line in lines:
        linelen = len(line)
        sindex = 0
        mindex = 1
        while mindex  len(line):
            eindex = mindex + 1
            if eindex >
= len(line):
                break
            d1 = line[ mindex][ 0] - line[ sindex][ 0]
            d2 = line[ mindex][ 1] - line[ sindex][ 1]
            d3 = line[ eindex][ 0] - line[ sindex][ 0]
            d4 = line[ eindex][ 1] - line[ sindex][ 1]
#判断三个点是否在一条直线上
            if abs(d1 *d4-d2*d3)  <= 1e-6:
                line.pop(mindex)
            else:
                sindex += 1
                mindex += 1

def linesCompose():
#防止删除点过多,在每两个点中间插值出一个新的点
    global lines
    tlines = []
    for line in lines:
        tlines.append([line[0]])
        for i in range(1,len(line)):
            l_1 = tlines[-1][-1]
            tlines[-1].append(((l_1[0] + line[i][0]) / 2,(l_1[1] + line[i][1]) / 2))
            tlines[-1].append((line[i]))
    lines = tlines




关键点与涂鸦的绑定

锚点绑定数量:在动画开始之前,还有非常重要的一步,就是要把我们取样后画的皮肤B’和扩充后的关键点组K’进行绑定。通过上面的描述,我们知道皮肤B’其实就是一个个点,这个过程就是皮肤的点和关键点之间的绑定,更专业的词语来说,我们要为皮肤点选择它们的锚点(Anchor),这些锚点都是来源于骨骼关键点。在这个项目中我们每个皮肤点和最多四个关键点绑定,这个数量和我们关键点K’的个数有关,当我们的关键点K’足够稠密的时候,我们的锚点数量是可以少一点。

锚点绑定标准:这里选定锚点的衡量标准为距离,也就是选择皮肤点n最近的m个关键点。这种方法有缺点,譬如在例子中,由于我们选择的是最近的关键点,在我们画的胡须的对应的锚点中,我们肩部的锚点反而比脸部的某些点更近,这就导致胡须会跟随者我们的肩膀来运动。如果想要更精确的匹配锚点,也可以人为干预这个过程,删除某些上述类似的不合理的绑定。
左为我们画好的涂鸦,右为涂鸦和关键点绑定的效果

绑定皮肤数据和骨骼数据:
   
     
def buildskin(lines, colors, cirRads, nodes):
     if lines  is  None  or nodes  is  None  or len(lines) ==  0  or len(nodes) ==  0:
         return []
    skins = []
    print( "doodle node length", len(nodes))
     #将opencv获取的皮肤点列表封装成skinItem类的对象列表
     for lineindex  in range(len(lines)):
        init =  True
        line = lines[lineindex]
        color = colors[lineindex]
        cirRad = cirRads[lineindex]
         for p  in line:
             if init:
                skins.append(skinItem(p[ 0], p[ 1],  True, color, cirRad))
                init =  False
             else:
                skins.append(skinItem(p[ 0], p[ 1],  False, color, cirRad))
     #计算每个skinItem对象最近的四个骨骼点并封装为锚点
     for skin  in skins:
        md = [float( "inf"), float( "inf"), float( "inf"), float( "inf")]
        mn = [ NoneNoneNoneNone]
        mdlen =  0
         for key,node  in nodes.items():
            d = distance(skin.getPos(), node.getPos())
            maxi = judge(md)
             if d < md[maxi]:
                md[maxi] = d
                mn[maxi] = node
                mdlen +=  1

         if mdlen <  4:
            md = md[:mdlen]
            mn = mn[:mdlen]
        ws = dist2weight(md)
         # 分配每个锚点的权重
         for j  in range(len(mn)):
            th = math.atan2(skin.y-mn[j].y, skin.x-mn[j].x)
            r = distance(skin.getPos(), mn[j].getPos())
            w = ws[j]
            skin.appendAnchor(anchorItem(mn[j], th-mn[j].thabs, r, w))
     return skins



涂鸦的更新

在我们做好前一步的初始化后,就可以在之后的每一帧中,计算皮肤点的新位置了。在前一步绑定的时候,我们同时还记录了一些锚点的其他信息:该皮肤点与锚点的距离和角度信息。在得到四个锚点之后,我们还要计算一个初始权重α。这样一来,当我们的关键点的位置变化了之后,我们能根据锚点的新位置计算出一个加权的皮肤点的新位置S’’。我们按照S’’的顺序把皮肤都画出来,就完成了整个项目。


在每帧中根据新的骨骼计算新的皮肤点:
   
     
def calculateSkin(skins, scale):
     for skin  in skins:
        xw =  0
        yw =  0
#根据皮肤点每个锚点的坐标与角度,计算出新的皮肤点的坐标
         for anchor  in skin.getAnchor():
            x = anchor.node.x + math.cos(anchor.th+anchor.node.thabs) * anchor.r * scale
            y = anchor.node.y + math.sin(anchor.th+anchor.node.thabs) * anchor.r * scale
            xw += x * anchor.w
            yw += y * anchor.w
        skin.x = xw
        skin.y = yw
     return skins



目前的一些问题与改进方向

1)问题
human_pose_estimation_resnet50_mpii这个模型很大的一个缺点就是没有关节的置信度的输出,因此我们没有办法对结果进行过滤。如果输入不完整的人体图像,模型仍然会输出16个关键点,其中本应不在图像中的关键点也会存在,这些虚假的结果点会导致皮肤点也画错的现象。除此之外,为了更好的效果,输入的视频最好背景少一点,减少一些影响因素。
2)改进方向
  • 大家可以尝试使用飞桨目标检测套件PaddleDetection中的其他的关键点检测模型,值得注意的是,如果你使用的模型是基于COCO数据集,需要更改doodle文件。
  • 为了能有更流畅的体验,可以尝试把关键点检测模型放到另一个线程里,这样的效果会更好一点。
我在飞桨开发者说专栏也直播进行了本项目的分享,欢迎大家移步B站观看视频分享。

分享视频:
https://www.bilibili.com/video/BV1N34y1y72o?spm_id_from=333.999.0.0
项目链接:
https://aistudio.baidu.com/aistudio/projectdetail/2498845
PaddleDetection:
https://github.com/PaddlePaddle/PaddleDetection
PaddleHub:
https://github.com/PaddlePaddle/PaddleHub
 

相关推荐



关注【飞桨PaddlePaddle】公众号
获取更多技术内容~


觉得内容不错的话,点个“在看”呗