使用飞桨PaddleHub实现皮影戏创作,传承正在消失的艺术

点击左上方蓝字关注我们

【飞桨开发者说】Zohar:是个快秃顶的野生AI训练师,经常捕捉各类AI模型进行训练,励志在各大AI竞赛取得冠军。

项目背景

领略千年皮影戏魅力,传承正在消失的艺术。皮影戏的神奇,在于小小皮影在指尖上飞舞,时而刀光剑影、时而策马扬鞭、时而缠绵悱恻,千军万马是他,单打独斗也是他。皮影戏可谓是闻名中外,它是把光影声色做到极致的一门古老艺术。

先辈门通过手艺演绎着皮影戏,随着人工智能的浪潮,我们同样也可以通过AI方式来实现皮影戏的效果。通过PaddleHub提供的人体骨骼关键点检测预训练模型,我们就可以快速实现皮影戏的效果。

PaddleHub可以便捷地获取PaddlePaddle生态下的预训练模型,完成模型的管理和一键预测。配合使用Fine-tune API,可以基于大规模预训练模型快速完成迁移学习,让预训练模型能更好地服务于用户特定场景的应用。

效果展示

在PaddleHub获取到人体骨骼关键点模型之后,就可以对这些关键点进行连接,从而形成了人体姿态。然后我们将皮影素材映射到人体姿态身上,让皮影跟随人体姿态进行运动,就达到“皮影戏”的效果。下面我们来看一下实现的效果吧:

  • 下面是对单张图片进行转换的效果,左边是含有人体的原始图片,并通过人体骨骼关键点检测后标注出了关键点位置,右边就是我们要实现皮影素材叠加的皮影:

  • 在实现单张图片之后,我们就可以对视频中的每一帧进行处理,随着视频中的人物运动,皮影也会跟随着一起运动,最终达到“皮影戏“的效果!

我在b站上找了一个可爱小姐姐的舞蹈视频进行转换(别光看小姐姐跳舞哦!),效果如下:

具体实现


1. 安装依赖包和模型

首先我们需要通过pip安装PaddlePaddle和PaddleHub,安装完后,我们就可以通过PaddleHub来安装人体骨骼关键点检测模型 human_pose_estimation_resnet50_mpii。

! hub install human_pose_estimation_resnet50_mpii==1.1.1

选取一张含有人体姿态的作为输入,检测人体骨骼关键点信息。

import paddlehub as hub

pose_estimation = hub.Module(name="human_pose_estimation_resnet50_mpii")
pose_estimation.keypoint_detection(paths=['work/imgs/body01.jpg'], visualization=True, output_dir="work/output_pose/")
 ---------------------------------------------------------------------
image saved in work/output_pose/body01.jpg
[{'path': 'work/imgs/body01.jpg',
  'data': OrderedDict([('left_ankle', [192, 679]),
               ('left_knee', [203, 521]),
               ('left_hip', [213, 347]),
               ('right_hip', [291, 347]),
               ('right_knee', [307, 521]),
               ('right_ankle', [307, 672]),
               ('pelvis', [255, 347]),
               ('thorax', [255, 166]),
               ('upper_neck', [255, 120]),
               ('head_top', [255, 22]),
               ('right_wrist', [161, 354]),
               ('right_elbow', [182, 264]),
               ('right_shoulder', [192, 166]),
               ('left_shoulder', [317, 166]),
               ('left_elbow', [333, 271]),
               ('left_wrist', [348, 362])])}]

从输出内容中我们可以看出,检测出来的人体骨骼关键点的信息包含肩膀,肘部,手腕,头顶等坐标点位,将这些信息分别标记到图像的人体中可以得到如下结果:

实现姿态映射

要实现皮影戏的效果,我们首先要解析人体各个骨骼关键点的位置信息,通过这些关键点的信息,计算人体的肢体长度,旋转角度,这样就可以将皮影的素材对应的缩放和旋转,并移动到指定位置,从而达到与人体动作同步的效果。具体的实现思路如下:

1. 首先解析某个部位相关骨骼关键点的,这里以手臂进行举例 :在人体骨骼关键点解析结果中,想要获取手臂在图像中具体坐标位置信息,我们可以通过肩膀(shoulder)和肘部(elbow)关键点得到。

2. 通过两个骨骼关键点可以确认肢体的位置,长度和旋转角度,根据长度就可以对素材进行缩放,根据旋转角度,可以先对素材进行中心旋转,再结合骨骼关键点位置信息,最终得到素材旋转后需要位移的信息,就可以得到最终素材显示的位置。按照这种方法将各个素材图片映射到对应的肢体上,便可以达到人体姿态映射的效果。

3. 除了得到动作映射后的皮影人之外,我们还需要有一个合适的背景,将皮影人合并到背景图像中进行输出。了解实现思路后,我们就可以开始动手实践了:

def get_true_angel(value):
    ''' 转转得到角度值 '''
    return value/np.pi*180

def get_angle(x1, y1, x2, y2):
    '''  计算旋转角度 '''
    dx = abs(x1- x2)
    dy = abs(y1- y2)
    result_angele = 0
    if x1 == x2:
        if y1 > y2:
            result_angele = 180
    else:
        if y1!=y2:
            the_angle = int(get_true_angel(np.arctan(dx/dy)))
        if x1 < x2:
            if y1>y2:
                result_angele = -(180 - the_angle)
            elif y1<y2:
                result_angele = -the_angle
            elif y1==y2:
                result_angele = -90
        elif x1 > x2:
            if y1>y2:
                result_angele = 180 - the_angle
            elif y1<y2:
                result_angele = the_angle
            elif y1==y2:
                result_angele = 90

    if result_angele<0:
        result_angele = 360 + result_angele
    return result_angele

def rotate_bound(image, angle, key_point_y):
    ''' 旋转图像,并取得关节点偏移量 '''
    (h,w) = image.shape[:2]
    #旋转中心
    (cx,cy) = (w/2,h/2)
    # 关键点坐标
    (kx,ky) = cx, key_point_y
    d = abs(ky - cy)
    #设置旋转矩阵
    M = cv2.getRotationMatrix2D((cx,cy), -angle, 1.0)
    cos = np.abs(M[0,0])
    sin = np.abs(M[0,1])
    # 计算图像旋转后的新边界
    nW = int((h*sin)+(w*cos))
    nH = int((h*cos)+(w*sin))
    # 计算旋转后的相对位移
    move_x = nW/2 + np.sin(angle/180*np.pi)*d 
    move_y = nH/2 - np.cos(angle/180*np.pi)*d
    # 调整旋转矩阵的移动距离(t_{x}, t_{y})
    M[0,2] += (nW/2) - cx
    M[1,2] += (nH/2) - cy
    return cv2.warpAffine(image,M,(nW,nH)), int(move_x), int(move_y)

def get_distences(x1, y1, x2, y2):
''' 计算两个坐标点之间的距离 '''
return ((x1-x2)**2 + (y1-y2)**2)**0.5

def append_img_by_sk_points(img, append_img_path, key_point_y, first_point, second_point, append_img_reset_width=None, append_img_max_height_rate=1, middle_flip=False, append_img_max_height=None):
    ''' 将添加的素材图片进行缩放 '''
    append_image = cv2.imdecode(np.fromfile(append_img_path, dtype=np.uint8), cv2.IMREAD_UNCHANGED)
    # 根据长度进行缩放
    sk_height = int(get_distences(first_point[0], first_point[1], second_point[0], second_point[1])*append_img_max_height_rate)
    # 缩放制约
    if append_img_max_height:
        sk_height = min(sk_height, append_img_max_height)

    sk_width = int(sk_height/append_image.shape[0]*append_image.shape[1]) if append_img_reset_width is None else int(append_img_reset_width)
    if sk_width <= 0:
        sk_width = 1
    if sk_height <= 0:
        sk_height = 1

    # 关键点映射
    key_point_y_new = int(key_point_y/append_image.shape[0]*append_image.shape[1])
    # 缩放图片
    append_image = cv2.resize(append_image, (sk_width, sk_height))
    img_height, img_width, _ = img.shape
    # 是否根据骨骼节点位置在 图像中间的左右来控制是否进行 左右翻转图片
    # 主要处理头部的翻转, 默认头部是朝左
    if middle_flip:
        middle_x = int(img_width/2)
        if first_point[0] < middle_x and second_point[0] < middle_x:
            append_image = cv2.flip(append_image, 1)

    # 旋转角度
    angle = get_angle(first_point[0], first_point[1], second_point[0], second_point[1])
    append_image, move_x, move_y = rotate_bound(append_image, angle=angle, key_point_y=key_point_y_new)
    app_img_height, app_img_width, _ = append_image.shape

    zero_x = first_point[0] - move_x
    zero_y = first_point[1] - move_y

    (b, g, r) = cv2.split(append_image) 
    for i in range(0, r.shape[0]):
        for j in range(0, r.shape[1]):
            if 230>r[i][j]>200 and 0<=zero_y+i<img_height and 0<=zero_x+j<img_width:
                img[zero_y+i][zero_x+j] = append_image[i][j]
    return img

def get_combine_img(img_path, pose_estimation, body_img_path_map, backgroup_img_path= 'work/background.jpg'):
    ''' 识别图片中的关节点,并将皮影的肢体进行对应,最后与原图像拼接后输出 '''
    result = pose_estimation.keypoint_detection(paths=[img_path])
    image=cv2.imread(img_path)
    # 背景图片
    backgroup_image = cv2.imread(backgroup_img_path)
    image_flag = cv2.resize(backgroup_image, (image.shape[1], image.shape[0]))
    # 最小宽度
    min_width = int(get_distences(result[0]['data']['head_top'][0], result[0]['data']['head_top'][1], result[0]['data']['upper_neck'][0], result[0]['data']['upper_neck'][1])/3)

    #右大腿
    append_img_reset_width = max(int(get_distences(result[0]['data']['pelvis'][0], result[0]['data']['pelvis'][1],result[0]['data']['left_hip'][0], result[0]['data']['right_hip'][1])*1.6), min_width)
    image_flag = append_img_by_sk_points(image_flag, body_img_path_map['right_hip'], key_point_y=10, first_point=result[0]['data']['right_hip'],second_point=result[0]['data']['right_knee'], append_img_reset_width=append_img_reset_width)

    # 右小腿
    append_img_reset_width = max(int(get_distences(result[0]['data']['pelvis'][0], result[0]['data']['pelvis'][1], result[0]['data']['left_hip'][0], result[0]['data']['right_hip'][1])*1.5), min_width)
    image_flag = append_img_by_sk_points(image_flag, body_img_path_map['right_knee'], key_point_y=10, first_point=result[0]['data']['right_knee'],second_point=result[0]['data']['right_ankle'], append_img_reset_width=append_img_reset_width)

    # 左大腿
    append_img_reset_width = max(int(get_distences(result[0]['data']['pelvis'][0], result[0]['data']['pelvis'][1],result[0]['data']['left_hip'][0], result[0]['data']['left_hip'][1])*1.6), min_width)
    image_flag = append_img_by_sk_points(image_flag, body_img_path_map['left_hip'], key_point_y=0, first_point=result[0]['data']['left_hip'],second_point=result[0]['data']['left_knee'], append_img_reset_width=append_img_reset_width)

    # 左小腿
    append_img_reset_width = max(int(get_distences(result[0]['data']['pelvis'][0], result[0]['data']['pelvis'][1],result[0]['data']['left_hip'][0], result[0]['data']['left_hip'][1])*1.5), min_width)
    image_flag = append_img_by_sk_points(image_flag, body_img_path_map['left_knee'], key_point_y=10, first_point=result[0]['data']['left_knee'],second_point=result[0]['data']['left_ankle'], append_img_reset_width=append_img_reset_width)

    # 右手臂
    image_flag = append_img_by_sk_points(image_flag, body_img_path_map['left_elbow'], key_point_y=25, first_point=result[0]['data']['right_shoulder'],second_point=result[0]['data']['right_elbow'], append_img_max_height_rate=1.2)

    # 右手肘
    append_img_max_height = int(get_distences(result[0]['data']['right_shoulder'][0], result[0]['data']['right_shoulder'][1],result[0]['data']['right_elbow'][0], result[0]['data']['right_elbow'][1])*1.6)
    image_flag = append_img_by_sk_points(image_flag, body_img_path_map['left_wrist'], key_point_y=10, first_point=result[0]['data']['right_elbow'],second_point=result[0]['data']['right_wrist'], append_img_max_height_rate=1.5, append_img_max_height=append_img_max_height)

    # 左手臂
    image_flag = append_img_by_sk_points(image_flag, body_img_path_map['right_elbow'], key_point_y=25, first_point=result[0]['data']['left_shoulder'], second_point=result[0]['data']['left_elbow'],  append_img_max_height_rate=1.2)

    # 左手肘
    append_img_max_height = int(get_distences(result[0]['data']['left_shoulder'][0], result[0]['data']['left_shoulder'][1],result[0]['data']['left_elbow'][0], result[0]['data']['left_elbow'][1])*1.6)
    image_flag = append_img_by_sk_points(image_flag, body_img_path_map['right_wrist'], key_point_y=10, first_point=result[0]['data']['left_elbow'],second_point=result[0]['data']['left_wrist'], append_img_max_height_rate=1.5, append_img_max_height=append_img_max_height)

    # 头
    image_flag = append_img_by_sk_points(image_flag, body_img_path_map['head'], key_point_y=10, first_point=result[0]['data']['head_top'],second_point=result[0]['data']['upper_neck'], append_img_max_height_rate=1.2, middle_flip=True)

    # 身体
    append_img_reset_width = max(int(get_distences(result[0]['data']['left_shoulder'][0], result[0]['data']['left_shoulder'][1],result[0]['data']['right_shoulder'][0], result[0]['data']['right_shoulder'][1])*1.2), min_width*3)
    image_flag = append_img_by_sk_points(image_flag, body_img_path_map['body'], key_point_y=20, first_point=result[0]['data']['upper_neck'],second_point=result[0]['data']['pelvis'], append_img_reset_width=append_img_reset_width, append_img_max_height_rate=1.2)

    result_img =  np.concatenate((image, image_flag), axis=1) 
    return result_img

def img_show_bgr(image,size=8):
    ''' 图片显示 '''
    image=cv2.cvtColor(image,cv2.COLOR_BGR2RGB)
    plt.figure(figsize=(size,size))
    plt.imshow(image)
    plt.axis("off")
plt.show() 

# 素材图片的位置信息
body_img_path_map = {
    "right_hip" : "./work/img_change/jpg/right_hip.jpg",
    "right_knee" : "./work/img_change/jpg/right_knee.jpg",
    "left_hip" : "./work/img_change/jpg/left_hip.jpg",
    "left_knee" : "./work/img_change/jpg/left_knee.jpg",
    "left_elbow" : "./work/img_change/jpg/left_elbow.jpg",
    "left_wrist" : "./work/img_change/jpg/left_wrist.jpg",
    "right_elbow" : "./work/img_change/jpg/right_elbow.jpg",
    "right_wrist" : "./work/img_change/jpg/right_wrist.jpg",
    "head" : "./work/img_change/jpg/head.jpg",
    "body" : "./work/img_change/jpg/body.jpg"
}

pos_img_path = 'work/output_pose/body01.jpg'

result_img =  get_combine_img(pos_img_path, pose_estimation, body_img_path_map)
img_show_bgr(result_img, size=10)

我们将前面的人体图像作为输入,就可以得到我们想要的效果了:

让皮影动起来


想要真正让皮影动起来,我们就需要对视频进行操作,具体实现思路如下:

1. 准备含有人体运动的视频素材;

2. 将视频中每一帧保存成图片;

3. 分析图片中的人体姿势, 并转换为皮影姿势,并保存;

4. 将转换后所有的合并图像到视频,得到最终的结果;

我们来看一下一些实现的效果吧:

由于篇幅有限,后续功能实现大家可到我在AI Studio公开的项目进行查看,欢迎大家Fork点赞和评论。

PS:想看视频教程的同学可以查看B站上教程 :

总结

我们在学习AI知识的同时,弘扬了传统文化,这都得益于PaddleHub的简单易用,使得我们可以快速利用人体骨骼关键点位置信息,通过映射皮影素材完成“皮影戏”的效果,大家一起给飞桨团队和PaddleHub点个star吧!

如在使用过程中有问题,可加入飞桨官方QQ群进行交流:1108045677。

·飞桨PaddleHub项目地址·

GitHub: https://github.com/PaddlePaddle/PaddleHub 

Gitee: https://gitee.com/paddlepaddle/PaddleHub

·飞桨官网地址·

https://www.paddlepaddle.org.cn/


扫描二维码 | 关注我们

微信号 : PaddleOpenSource

END

明日活动