動画のランドマークを元にして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): に解決のキーポイントがありそうな気がしてます。
続きはいつになるか不明です…。