MediaPipeからBlenderへ - ⑥ アーマチュアが動いた!

From MediaPipe to Blender - Armature moved!

1月21日に顔メッシュの記事を載せてからほぼ2ヶ月、どうやら「画像や動画の座標データからBlenderオブジェクトを作成する」という目標を達成したようなので、仕上げの記事をまとめます。

MediaPipeからBlenderへ:これまでの記事

タイトル概略リンク
① 顔メッシュMediaPipeを使うための準備、顔ランドマークからBlenderメッシュを作るところまでをPythonスクリプトで実行。
② 手手のランドマークからBlenderメッシュを作る。①のPythonスクリプトに手の検知の部分を追加。Blender内の操作で、辺だけのメッシュにスキン・モディファイアをかけ厚みのあるオブジェクトが作成できることを確認した。
③ 全身ポーズのランドマークからBlenderメッシュを作る。②のPythonスクリプトに全身検知を追加。スキン・モディファイア経由でアーマチュアを作成。スクリプトが長くなるので、この先は別スクリプトで継続。
④ 動きの取り込み(未完)ルートのポーズボーンに位置データを設定してキーフレームを作成するところまでできた。個々のボーンの回転設定がまるでわからないので挫折しそうになる。掲載したコードは、キーフレーム設定の部分を最終版でかなり修正したので、あまり参考になりません...。
⑤ ボーンの回転をスクリプトでポーズボーンの回転をスクリプトで行うのに苦労したので、忘れないうちにメモ。

修正したコードを実行した結果

まずは元動画とアーマチュアを重ねて表示した動画。ダンスが進むにつれてルートボーンの位置がだんだん高くなりますが、気にしないことにします。

元動画を非表示にした動画。アーマチュアがちゃんと回転しているのがわかります。(後日、メッシュオブジェクトと結合して動かしてみたところ、Pelvisが垂直軸回りで回転していないことが分かりました。今後の課題です。)

mp_to_blend.pyに少し追加

必要かどうかわかりませんが、clavicle(鎖骨)とArm(上腕)を切り離す処理を追加しました。下から5行目あたり。親ボーンと接続していなければキーフレームで位置を設定できるみたいなので。

    # 名前の変更を決定するためオブジェクト・モードに戻る
    bpy.ops.object.editmode_toggle()

    # ClavicleとArmのつながりを解除
    bpy.context.object.data.bones.active = Arm.data.bones['Arm_L']
    bpy.ops.object.editmode_toggle()
    bpy.ops.armature.parent_clear(type='DISCONNECT')
    bpy.ops.object.editmode_toggle()
    bpy.context.object.data.bones.active = Arm.data.bones['Arm_R']
    bpy.ops.object.editmode_toggle()
    bpy.ops.armature.parent_clear(type='DISCONNECT')
    bpy.ops.object.editmode_toggle()

    # ボーンの形状を八面体へ
    bpy.context.object.data.display_type = 'OCTAHEDRAL'

元動画の動きを再現するスクリプト

③のスクリプトで作った"Armature"があることが前提のスクリプトです。

# mp_to_blend2.py
# part 2 ... 動画

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

# 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
from mathutils import Quaternion, Vector, Matrix

# パス、ファイル名
PrjDir = "/Path/to/Project/"
vidName = "Input.mp4"
vidIn = PrjDir + "vid/" + vidName
vidOut = PrjDir + "vid/Output." + vidName.split(".")[1]

# 動画は1回作ればよい
if os.path.exists(vidOut):
    vidOutExist = True
else:
    vidOutExist = False

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

# MediaPipeのオブジェクト
mp_pose = mp.solutions.pose
mp_drawing = mp.solutions.drawing_utils
mp_drawing_styles = mp.solutions.drawing_styles

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

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 Bones(vList):
    '''
    ボーンの名前、ボーンのスタート位置、子ボーンのスタート位置
    rotation_differenceと行列の掛け算で回転させる
    '''
    # LocationZahyo
    Pelvis = Vector(vList[33])
    Stomach = Vector(vList[37])
    Chest = Vector(vList[36])
    Neck = Vector(vList[34])
    Head = Vector(vList[35])
    Clavicle_L = Vector(vList[34])
    Arm_L = Vector(vList[11])
    Forearm_L = Vector(vList[13])
    Hand_L = Vector(vList[15])
    Clavicle_R = Vector(vList[34])
    Arm_R = Vector(vList[12])
    Forearm_R = Vector(vList[14])
    Hand_R = Vector(vList[16])
    Thigh_L = Vector(vList[23])
    Calf_L = Vector(vList[25])
    Foot_L = Vector(vList[27])
    Thigh_R = Vector(vList[24])
    Calf_R = Vector(vList[26])
    Foot_R = Vector(vList[28])

    BornNames = ("Pelvis", "Stomach", "Chest", "Neck", "Head",
                 "Clavicle_L", "Arm_L", "Forearm_L", "Hand_L",
                 "Clavicle_R", "Arm_R", "Forearm_R", "Hand_R",
                 "Thigh_L", "Calf_L", "Foot_L",
                 "Thigh_R", "Calf_R", "Foot_R")

    # 画像の幅と高さからMediaPipeが計算した数値。ポーズモードでは直接は使えない。
    LocVec = (Pelvis, Stomach, Chest, Neck, Head,
              Clavicle_L, Arm_L, Forearm_L, Hand_L,
              Clavicle_R, Arm_R, Forearm_R, Hand_R,
              Thigh_L, Calf_L, Foot_L,
              Thigh_R, Calf_R, Foot_R)

    # ボーンのテールの位置(=子のスタート位置)
    Pel_tail = Stomach
    Sto_tail = Chest
    Che_tail = Neck
    Nek_tail = Head
    Hed_tail = Vector(vList[0])
    ClaL_tail = Arm_L
    AmL_tail = Forearm_L
    FAmL_tail = Hand_L
    HndL_tail = Vector(vList[19])
    ClaR_tail = Arm_R
    AmR_tail = Forearm_R
    FAmR_tail = Hand_R
    HndR_tail = Vector(vList[20])
    ThiL_tail = Calf_L
    CalL_tail = Foot_L
    FtL_tail = Vector(vList[31])
    ThiR_tail = Calf_R
    CalR_tail = Foot_R
    FtR_tail = Vector(vList[32])

    TailV = (Pel_tail, Sto_tail, Che_tail, Nek_tail, Hed_tail,
             ClaL_tail, AmL_tail, FAmL_tail, HndL_tail,
             ClaR_tail, AmR_tail, FAmR_tail, HndR_tail,
             ThiL_tail, CalL_tail, FtL_tail,
             ThiR_tail, CalR_tail, FtR_tail)

    return [BornNames, LocVec, TailV]


def rotMat(pb, prvHead, prvTail, crntHead, crntTail):
    pb = bpy.context.active_pose_bone

    prvVec = prvTail - prvHead
    targetVec = crntTail - crntHead
    Q = prvVec.rotation_difference(targetVec)

    # 3個のマトリクス(行列)の掛け算
    M = (
        Matrix.Translation(pb.head) @
        Q.to_matrix().to_4x4() @
        Matrix.Translation(-pb.head)
        )

    # 元の行列にMを掛けたものを代入
    pb.matrix = M @ pb.matrix


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

# ポーズ・オブジェクト
pose = mp_pose.Pose(min_detection_confidence=0.5,
                    min_tracking_confidence=0.5)

# 動画を開く
cap = cv2.VideoCapture(vidIn)
if cap.isOpened() == False:
    print("入力動画だめぽ")
    raise TypeError
w = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
h = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
vLen = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
fps = int(cap.get(cv2.CAP_PROP_FPS))

# ランドマーク付き動画がなければ書き出す
if not vidOutExist:
    out = cv2.VideoWriter(vidOut, cv2.VideoWriter_fourcc('m', 'p', '4', 'v'),
                          fps, (w, h))

lmAll = []      # 頂点データの入れ物。Blender用
while cap.isOpened():
    ret, image = cap.read()
    if not ret:
        break
    image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
    image.flags.writeable = False
    results = pose.process(image)
    image.flags.writeable = True
    image = cv2.cvtColor(image, cv2.COLOR_RGB2BGR)
    if not vidOutExist:
        mp_drawing.draw_landmarks(image,
                                  results.pose_landmarks,
                                  mp_pose.POSE_CONNECTIONS)
        out.write(image)

    # 頂点データをアペンド
    Pose_verts = vertsCalcZdepth(h, w,
                                 results.pose_landmarks.landmark)
    lmAll.append(Pose_verts)
pose.close()
cap.release()
if not vidOutExist:
    out.release()


# ...................................................................
# Blener内での処理
try:
    Arm = bpy.data.objects["Armature"]      # 存在しない場合は終了
except KeyError:
    print("Armatureがないの")
    exit()

Arm.select_set(True)
bpy.ops.object.posemode_toggle()
#bpy.context.object.data.show_axes = True
#bpy.context.object.data.show_names = True

FrmNo = 0
# 1フレームごとの全身のランドマークの座標 x 動画のフレーム数
# Bones(vList) は3種類の情報を返す [BornNames, LocVec, Vecs]
for i in lmAll:
    Names = Bones(i)[0]
    LocVec = Bones(i)[1]
    TailVec = Bones(i)[2]

    if FrmNo == 0:
        PrvLoc = LocVec
        PrvTail = TailVec

    bpy.context.scene.frame_set(FrmNo)
    for j in Names:
        bpy.context.object.data.bones.active = Arm.data.bones[j]

        bpy.context.scene.transform_orientation_slots[0].type = "GLOBAL"
        jIndx = Names.index(j)

        LocDiff = LocVec[jIndx] - PrvLoc[jIndx]
        if j in ["Pelvis", "Arm_L", "Arm_R", "Thigh_L", "Thigh_R"]:
            # 位置
            if j == "Pelvis":
                bpy.context.active_pose_bone.location = LocVec[jIndx]
            else:
                bpy.context.active_pose_bone.location = LocDiff
            bpy.context.active_pose_bone.keyframe_insert(data_path="location")

        # 回転
        bpy.context.scene.transform_orientation_slots[0].type = "GLOBAL"
        bpy.context.active_pose_bone.rotation_mode = 'QUATERNION'
        bpy.context.active_bone.use_inherit_rotation = False
        # 回転行列を適用する
        # rotMat(pb, prvHead, prvTail, crntHead, crntTail):
        rotMat(Arm.data.bones[j], PrvLoc[jIndx], PrvTail[jIndx], LocVec[jIndx], TailVec[jIndx])
        bpy.context.active_pose_bone.keyframe_insert(data_path="rotation_quaternion")

    PrvLoc = LocVec
    PrvTail = TailVec
    FrmNo += 1
bpy.ops.object.posemode_toggle()

これからやりたいこと

似顔絵モデルの作成やアニメ付けが格段に容易になったので、伝えたいメッセージのコンテンツ作りも楽になると思います。いろいろなソースからアニメーション・データを取り出し、ライブラリに収納して使いまわしができるようにしたいです。



関連記事