MediaPipeからBlenderへ - ④ 動きの取り込み(未完)

From MediaPipe to Blender - Capturing the movement (unfinished)

動画のランドマークを元にしてBlenderのアーマチュアが同じ動きをするか試しています。

今のところ、位置はなんとかなりましたが、個々のボーンの角度がまるでダメダメです。ただ、プログラムのどこを修正すればいいかまでは分かりましたので、途中経過をお知らせしますね。

※このページに掲載したコードは、キーフレーム設定の部分を最終版でかなり修正したので、あまり参考になりません…。

まずは元動画の動き

Blenderに参照用動画として元の動画を取り込み再生します。

これは角度の設定が分からず、とりあえず動かしたアーマチュア

動くことは動いたけれども、ベビーダンスには程遠い。

参照用と重ね合わせてみる

ベビーダンスで垂直方向に回転した時、アーマチュアは後方にのけぞっているみたい。

このアニメーションを作ったコード(mp_to_blend2.py)

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

'''
【3分で分かる!】三角関数の基礎知識(定義や性質)をわかりやすく
https://goukaku-suppli.com/archives/37382

オイラー角について
https://el-ement.com/blog/2018/05/19/euler-angles/

三次元座標系で回転を表現するための方法:回転ベクトル, 回転行列, オイラー角, クォータニオン(四元数)
https://kamino.hatenablog.com/entry/rotation_expressions

Quaternionによる3次元の回転変換
https://qiita.com/kenjihiranabe/items/945232fbde58fab45681

Blenderマニュアルより:
https://docs.blender.org/api/htmlI/x1960.html
選択したボーンはチェーンの前のボーンに対して回転し、チェーンの後続のボーンはすべてその回転に従います。
'''

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

# 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 sys
import cv2
import mediapipe as mp
import bpy
#from math import sin, cos, tan, atan
import math
from mathutils import Quaternion, Vector

# パス、ファイル名
PrjDir = "/Path/to/Project/"
vidName = "Input.mp4"
vidIn = PrjDir + vidName
vidOut = PrjDir + "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 startPos(vList):
    '''
    ボーンの名前とボーンのスタート位置
    Pelvis, Thigh_L, Thigh_Rは位置と角度が指定可能
    他は角度のみ
    '''
    Pelvis = vList[33]          # 位置と角度が指定可能
    Stomach = vList[37]
    Chest = vList[36]
    Neck = vList[34]
    Head = vList[35]
    Clavicle_L = vList[34]
    Arm_L = vList[11]
    Forearm_L = vList[13]
    Hand_L = vList[15]
    Clavicle_R = vList[34]
    Arm_R = vList[12]
    Forearm_R = vList[14]
    Hand_R = vList[16]
    Thigh_L = vList[23]         # 位置と角度が指定可能
    Calf_L = vList[25]
    Foot_L = vList[27]
    Thigh_R = vList[24]         # 位置と角度が指定可能
    Calf_R = vList[26]
    Foot_R = vList[28]

    startPosition = (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)
    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")

    return [BornNames, startPosition]

def Angle(p0, p1):
    '''
    p0 = (x, y, z)  # 直前の座標
    p1 = (X, Y, Z)  # 現在の座標
    '''

    # 数学がわからないのでとりあえず
    # ダミーとして単純な計算式を入れておく
    # このファンクションが最大課題!!!
    
    x, y, z = p0[0], p0[1], p0[2]
    X, Y, Z = p1[0], p1[1], p1[2]

    v0 = Vector((x, y, z))*100
    v1 = Vector((X, Y, Z))*100

    qrot = Quaternion(v1 - v0)
    return qrot

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

# ポーズ・オブジェクト
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
for i in lmAll:
    Names = startPos(i)[0]
    PData = startPos(i)[1]

    if FrmNo == 0:
        Prv = PData

    bpy.context.scene.frame_set(FrmNo)
    for j in Names:
        bpy.context.object.data.bones.active = Arm.data.bones[j]
        jIndx = Names.index(j)
        if j in ["Pelvis"]:
            bpy.context.active_pose_bone.location[0] = PData[jIndx][0]
            bpy.context.active_pose_bone.location[1] = PData[jIndx][1]
            bpy.context.active_pose_bone.location[2] = PData[jIndx][2]
            # ぷるぷるした動きを防ぐためにキーフレームは間引きして設定
            if FrmNo % 10 == 0:
                bpy.context.active_pose_bone.keyframe_insert(data_path="location")

        # 接続されてないボーンは位置のキーフレームが設定できる。
        # とりあえず現在の座標から直前の座標を引いてみた
        if j in ["Thigh_L", "Thigh_R"]:
            bpy.context.active_pose_bone.location[0] = PData[jIndx][0] - Prv[jIndx][0]
            bpy.context.active_pose_bone.location[1] = PData[jIndx][1] - Prv[jIndx][1]
            bpy.context.active_pose_bone.location[2] = PData[jIndx][2] - Prv[jIndx][2]
            if FrmNo % 10 == 0:
                bpy.context.active_pose_bone.keyframe_insert(data_path="location")

        # Angle(直前の座標、現在の座標)でクォータニオン値を計算する
        Aw = Angle(Prv[jIndx], PData[jIndx])[0]
        Ax = Angle(Prv[jIndx], PData[jIndx])[1]
        Ay = Angle(Prv[jIndx], PData[jIndx])[2]
        Az = Angle(Prv[jIndx], PData[jIndx])[3]
        bpy.context.active_pose_bone.rotation_mode = 'QUATERNION'
        bpy.context.active_pose_bone.rotation_quaternion[0] = Aw
        bpy.context.active_pose_bone.rotation_quaternion[1] = Ax
        bpy.context.active_pose_bone.rotation_quaternion[2] = Ay
        bpy.context.active_pose_bone.rotation_quaternion[3] = Az
        if FrmNo % 10 == 0:
            bpy.context.active_pose_bone.keyframe_insert(data_path="rotation_quaternion")
    Prv = PData
    FrmNo += 1
bpy.ops.object.posemode_toggle()

中学高校の数学(三角関数やベクトル)に戻ってきっちりやり直そうとしましたが、やればやるほど蟻地獄。なんとなく、def Angle(p0, p1): に解決のキーポイントがありそうな気がしてます。

続きはいつになるか不明です…。



関連記事