MediaPipeからBlenderへ - ③ 全身

From MediaPipe to Blender - Pose

前回の手オブジェクトに続き、今日はポーズ・データをBlenderに取り込んでアーマチュアを作ります。

mediaPipeの頂点番号

オリジナル0〜32に33以降を追加(図の緑の部分)vertsCalcZdepth(h, w, lm)で作成

mp_to_blend.pyにポーズ検知のパートを追加

# mp_to_blend.py

# ========================================================================
# 準備
# ========================================================================

# bpyを外部環境でインポートする時のメッセージを防ぐ
import os, ssl
if not os.path.exists("/run/user/1000/gvfs"):
    os.mkdir("/run/user/1000/gvfs")

# Blender内部でmediapipeをインポートする時[SSL: CERTIFICATE_VERIFY_FAILED]を回避する
ssl._create_default_https_context = ssl._create_unverified_context

# ...................................................................

# 必要なモジュールを読み込む
import cv2
import mediapipe as mp
import bpy

# パス、ファイル名
PrjDir = "/Path/to/Project/"
imgName = "Input.webp"  # png, jpegも可
imgIn = PrjDir + imgName

# ...................................................................

# MediaPipeのオブジェクト
mp_face_mesh = mp.solutions.face_mesh
mp_hands = mp.solutions.hands
mp_pose = mp.solutions.pose

# 顔の各部を形成する頂点セットが予め用意されている
Face_Dict = {   "CONTOURS":mp_face_mesh.FACEMESH_CONTOURS,
                "LEFT_EYE":mp_face_mesh.FACEMESH_LEFT_EYE,
                "RIGHT_EYE":mp_face_mesh.FACEMESH_RIGHT_EYE,
                "LIPS":mp_face_mesh.FACEMESH_LIPS,
                "LEFT_EYEBROW":mp_face_mesh.FACEMESH_LEFT_EYEBROW,
                "RIGHT_EYEBROW":mp_face_mesh.FACEMESH_RIGHT_EYEBROW,
                "TESSELATION":mp_face_mesh.FACEMESH_TESSELATION,
                "FACE_OVAL":mp_face_mesh.FACEMESH_FACE_OVAL,
                "IRISES":mp_face_mesh.FACEMESH_IRISES,
                "LEFT_IRIS":mp_face_mesh.FACEMESH_LEFT_IRIS,
                "RIGHT_IRIS":mp_face_mesh.FACEMESH_RIGHT_IRIS,
                "NUM_LANDMARKS":mp_face_mesh.FACEMESH_NUM_LANDMARKS,
                "NUM_LANDMARKS_WITH_IRISES":mp_face_mesh.FACEMESH_NUM_LANDMARKS_WITH_IRISES
            }

hand_conn = mp_hands.HAND_CONNECTIONS

# pose_conn = mp_pose.POSE_CONNECTIONS    # Blender用コネクションリストを別途作るので要らない

# ...................................................................

# 画像オブジェクト
img = cv2.imread(imgIn)
h = img.shape[0]
w = img.shape[1]


# ========================================================================
# サブ
# ========================================================================

def vertsCalc(h, w, lm):
    '''
    画像の高さ・幅とMediaPipeのランドマークデータから
    各頂点の座標を求める
    '''
    vList = []
    for i in lm:
        V = str(i).split("\n")
        Vx = float(V[0].split(": ")[1]) * w * 0.001
        Vy = float(V[1].split(": ")[1]) * h * 0.001
        Vz = float(V[2].split(": ")[1]) * w * 0.001
        #vList.append((Vx, Vy, Vz))
        vList.append((Vx, Vz, Vy*-1))   # Blenderではzが上、-yが前
    return vList


def vertsCalcZdepth(h, w, lm):
    '''
    MediaPipeのデータに33以降を追加
    Pose用頂点計算:  1) 奥行きに定数を乗算して浅くする
                    2) 既存の線分の中点を計算し、体の中心線が引けるようにする
    '''
    z_depth = 0.15
    vList, vAdd = [], []
    Counter = 0
    for i in lm:
        V = str(i).split("\n")
        Vx = float(V[0].split(": ")[1]) * w * 0.001
        Vy = float(V[1].split(": ")[1]) * h * 0.001
        Vz = float(V[2].split(": ")[1]) * w * 0.001 * z_depth
        vList.append((Vx, Vz, Vy*-1))   # zが上、-yが前
        if Counter in [9,10,11,12,23,24]:
            vAdd.append((Vx,Vy,Vz))
        Counter += 1
    # 33〜35追加
    # 33=23,24の中点 34=11,12の中点 35=9,10の中点
    V33x = (vAdd[4][0] + vAdd[5][0]) / 2
    V33y = (vAdd[4][1] + vAdd[5][1]) / 2
    V33z = (vAdd[4][2] + vAdd[5][2]) / 2
    V33 = (V33x, V33z, V33y*-1)
    V34x = (vAdd[2][0] + vAdd[3][0]) / 2
    V34y = (vAdd[2][1] + vAdd[3][1]) / 2
    V34z = (vAdd[2][2] + vAdd[3][2]) / 2
    V34 = (V34x, V34z, V34y*-1)
    V35x = (vAdd[0][0] + vAdd[1][0]) / 2
    V35y = (vAdd[0][1] + vAdd[1][1]) / 2
    V35z = (vAdd[0][2] + vAdd[1][2]) / 2
    V35 = (V35x, V35z, V35y*-1)
    vList.append(V33)
    vList.append(V34)
    vList.append(V35)
    # 36, 37 (Stomach, Chest)
    V36x = (V33x + V34x) / 2
    V36y = (V33y + V34y) / 2
    V36z = (V33z + V34z) / 2
    V36 = (V36x, V36z, V36y*-1)
    vList.append(V36)
    V37x = (V33x + V36x) / 2
    V37y = (V33y + V36y) / 2
    V37z = (V33z + V36z) / 2
    V37 = (V37x, V37z, V37y*-1)
    vList.append(V37)
    return vList


def edgeData(mpConn):
    '''
    MediaPipeで用意されているコネクションの頂点ペア
    '''
    pairList = []
    for i in mpConn:
        pairList.append(i)
    return pairList


def edgeDataPose():
    '''
    PoseランドマークからArmatureを作るための頂点ペア
    '''
    Pelvis = (33, 37)
    Stomach = (37, 36)
    Chest = (36, 34)
    Neck = (34, 35)
    Head = (35, 0)
    Clavicle_L = (34, 11)
    Arm_L = (11, 13)
    Forearm_L = (13, 15)
    Hand_L = (15, 19)
    Clavicle_R = (34, 12)
    Arm_R = (12, 14)
    Forearm_R = (14, 16)
    Hand_R = (16, 20)
    P_L =  (33, 23)
    Thigh_L = (23, 25)
    Calf_L = (25, 27)
    Foot_L = (27, 31)
    P_R =  (33, 24)
    Thigh_R = (24, 26)
    Calf_R = (26, 28)
    Foot_R = (28, 32)
    pairList = [Pelvis, Stomach, Chest, Neck, Head,
                Clavicle_L, Arm_L, Forearm_L, Hand_L,
                Clavicle_R, Arm_R, Forearm_R, Hand_R,
                P_L, Thigh_L, Calf_L, Foot_L,
                P_R, Thigh_R, Calf_R, Foot_R]
    return pairList


# ========================================================================
# メイン
# ========================================================================

# 顔 ................................................................
with mp_face_mesh.FaceMesh(
        static_image_mode = True,
        max_num_faces = 1,
        refine_landmarks = True,
        min_detection_confidence = 0.5 ) as Face:
    results = Face.process(cv2.cvtColor(img, cv2.COLOR_BGR2RGB))
lm_detect = bool(results.multi_face_landmarks)
if lm_detect:
    Face_verts = vertsCalc(h, w, results.multi_face_landmarks[0].landmark)
    Face_edges = edgeData(Face_Dict["TESSELATION"])
    FaceMesh = bpy.data.meshes.new("face")
    FaceMesh.from_pydata(Face_verts, Face_edges, [])
    FaceMesh.validate()
    FaceMesh.update()
    ob = bpy.data.objects.new("Face", FaceMesh)
    scene = bpy.context.scene
    scene.collection.objects.link(ob)

# 手 ................................................................
with mp_hands.Hands(
        static_image_mode = True,
        max_num_hands = 2,
        min_detection_confidence = 0.5) as Hands:
    results = Hands.process(cv2.cvtColor(img, cv2.COLOR_BGR2RGB))
lm_detect = bool(results.multi_hand_landmarks)
if lm_detect:
    # 両手を認識した場合、オブジェクトを2つ作成する(手0・手1)
    for hand_No, hand_landmarks in enumerate(results.multi_hand_landmarks):
        Hand_verts = vertsCalc(h, w, results.multi_hand_landmarks[hand_No].landmark)
        Hand_edges = edgeData(hand_conn)
        HandMesh = bpy.data.meshes.new("hand"+str(hand_No))
        HandMesh.from_pydata(Hand_verts, Hand_edges, [])
        HandMesh.validate()
        HandMesh.update()
        ob = bpy.data.objects.new("Hand"+str(hand_No), HandMesh)
        scene = bpy.context.scene
        scene.collection.objects.link(ob)

# 全身 ..............................................................
with mp_pose.Pose(
        static_image_mode = True,
        model_complexity = 2,
        min_detection_confidence = 0.5) as Pose:
    results = Pose.process(cv2.cvtColor(img, cv2.COLOR_BGR2RGB))
lm_detect = bool(results.pose_landmarks)
if lm_detect:
    # メッシュデータ
    Pose_verts = vertsCalcZdepth(h, w, results.pose_landmarks.landmark)
    Pose_edges = edgeDataPose()
    PoseMesh = bpy.data.meshes.new("pose")
    PoseMesh.from_pydata(Pose_verts, Pose_edges, [])
    PoseMesh.validate()
    PoseMesh.update()

    # オブジェクトに収納
    ob = bpy.data.objects.new("Pose", PoseMesh)
    scene = bpy.context.scene
    scene.collection.objects.link(ob)

    # PoseオブジェクトからSkinモディファイア経由でArmatureを作る
    ob.select_set(True)
    bpy.context.view_layer.objects.active = ob
    bpy.ops.object.editmode_toggle()
    bpy.ops.mesh.select_all(action="DESELECT")

    # Pelvisの始点をルートに指定
    import bmesh
    bm = bmesh.from_edit_mesh(ob.data)
    bm.verts.ensure_lookup_table()
    bm.verts[33].select = True
    bpy.ops.object.skin_root_mark()
    bpy.ops.object.editmode_toggle()

    # Skinモディファイアを適用 → Armatureを作成
    bpy.ops.object.modifier_add(type='SKIN')
    bpy.ops.object.skin_armature_create(modifier="Skin")

    # 変形したPoseオブジェクトを削除
    bpy.data.objects.remove(ob)

    # Pelvis(Bone.00)から奥に向かってできたボーン('Bone')と
    # P_L, P_R('Bone.13','Bone.17')を削除
    Arm = bpy.data.objects['Armature']
    bpy.ops.object.editmode_toggle()
    bpy.ops.armature.select_all(action='DESELECT')
    Arm.data.edit_bones.remove(Arm.data.edit_bones['Bone'])
    Arm.data.edit_bones.remove(Arm.data.edit_bones['Bone.13'])
    Arm.data.edit_bones.remove(Arm.data.edit_bones['Bone.17'])

    # Thigh_L, Thigh_R(Bone.14, Bone.18)をPelvis(Bone.00)の子に設定
    Arm.data.edit_bones['Bone.14'].parent = Arm.data.edit_bones['Bone.00']
    Arm.data.edit_bones['Bone.18'].parent = Arm.data.edit_bones['Bone.00']

    # 各ボーンの名前を番号からわかり易いものに変える
    # (CascadeurのQUICK RIGGING TOOLに合わせて名前付け)
    Arm.data.edit_bones['Bone.00'].name = 'Pelvis'
    Arm.data.edit_bones['Bone.01'].name = 'Stomach'
    Arm.data.edit_bones['Bone.02'].name = 'Chest'
    Arm.data.edit_bones['Bone.03'].name = 'Neck'
    Arm.data.edit_bones['Bone.04'].name = 'Head'
    Arm.data.edit_bones['Bone.05'].name = 'Clavicle_L'
    Arm.data.edit_bones['Bone.06'].name = 'Arm_L'
    Arm.data.edit_bones['Bone.07'].name = 'Forearm_L'
    Arm.data.edit_bones['Bone.08'].name = 'Hand_L'
    Arm.data.edit_bones['Bone.09'].name = 'Clavicle_R'
    Arm.data.edit_bones['Bone.10'].name = 'Arm_R'
    Arm.data.edit_bones['Bone.11'].name = 'Forearm_R'
    Arm.data.edit_bones['Bone.12'].name = 'Hand_R'
    Arm.data.edit_bones['Bone.14'].name = 'Thigh_L'
    Arm.data.edit_bones['Bone.15'].name = 'Calf_L'
    Arm.data.edit_bones['Bone.16'].name = 'Foot_L'
    Arm.data.edit_bones['Bone.18'].name = 'Thigh_R'
    Arm.data.edit_bones['Bone.19'].name = 'Calf_R'
    Arm.data.edit_bones['Bone.20'].name = 'Foot_R'

    # オブジェクト・モードに戻る
    bpy.ops.object.editmode_toggle()
    # ボーンの形状を八面体へ
    bpy.context.object.data.display_type = 'OCTAHEDRAL'
    # 原点をジオメトリに移動
    bpy.ops.object.origin_set(type='ORIGIN_GEOMETRY', center='BOUNDS')

確認

Pelvisがルートボーンになっているアーマチュアができた。

次回は難関アニメーションの取り込みです。この記事を書いている時点(2023.02.25)ではオイラー角やクォータニオン、Blenderの骨の動く仕組みがよくわかっていないので、完成は遠いです…。

でも悪戦苦闘を記録しておくのも何かの足しになると思うので、出来上がっているところまでのコードと動作結果を載せたいと思います。



関連記事