Python
DeepLearning
MMD
TensorFlow

VMD-Lifting がやっていること

ニューラルネットワーク等を用いて写真や動画に写っている人のポーズを推定する技術(Pose Estimation)は近年盛んに研究されており、OpenPose や VNect など多くの論文が発表されています。Denis Tome' らによる Lifting from the Deep ( https://github.com/DenisTome/Lifting-from-the-Deep-release )もその中の一つであり、写真に写った人のポーズを推定します。

この Lifting from the Deep で推定したポーズを MikuMikuDance (MMD) で用いられるVMD形式で出力するプログラムである VMD-Lifting ( https://github.com/errno-mmd/VMD-Lifting )を作りました。VMD-Lifting は、Lifting from the Deepにより推定された関節(joint)の位置を元に、ボーンの角変位を計算し、それをVMDフォーマットのファイルに出力します。

出力結果の例を下図に示します。左が元の写真、中央がLifting from the Deepにより推定された関節の位置、右がVMD-Liftingの出力したVMDファイルをMMDに読ませた結果です。(写真は https://www.pexels.com/ および http://stokpic.com より)

VMD-Lifting出力結果の例

この記事では、VMD-Liftingがどうやって関節の位置からボーンの角変位を計算しているか、について解説します。ポーズ推定そのものについては触れません(私には説明できません)ので、ポーズ推定の仕組みについて知りたい方は Lifting from the Deep の論文を参照してください ( http://www0.cs.ucl.ac.uk/staff/D.Tome/papers/LiftingFromTheDeep.html )。

まず、Lifting from the Deep によるポーズ推定で得られる関節の位置のデータ形式を見てみましょう。
application/vmdlifting.py の45行目、

  pose_2d, visibility, pose_3d = pose_estimator.estimate(image)

ここで、推定された3次元の関節の位置が変数 pose_3d に格納されます。
pose_3d のデータ形式は、 packages/lifting/_pose_estimator.py の111行目に書かれているとおり、

 num_ppl x 3 x num_joints

ここで num_ppl は検出された人の人数、num_jointsは関節の数です。
現時点(2018/1/27)では num_joints = 17 で、各関節の番号は次のとおりです。

0 腰
1 右脚付け根
2 右ひざ
3 右足首
4 左脚付け根
5 左ひざ
6 左足首
7 胴体の中心
8 首の付け根
9 あご
10 頭頂
11 左肩
12 左ひじ
13 左手首
14 右肩
15 右ひじ
16 右手首

pos2vmd.py の192行目から始まるconvert_position()関数で、このデータを PyQt5 の3次元ベクトル QVector3D の配列に移しています。ただし、Lifting from the Deep の座標系では z軸が垂直上向きなので、MMDの座標系に合わせるために y と z を入れ替えています。

def convert_position(pose_3d):
    positions = []
    for pose in pose_3d:
        for j in range(pose.shape[1]):
            q = QVector3D(pose[0, j], pose[2, j], pose[1, j])
            positions.append(q)
    return positions

これで関節の位置情報が得られたので、次にボーンの方向(orientation)を求めたいと思います。その処理は pos2vmd.py の positions_to_frames() 関数で行っています。

例えば上半身ボーンは、胴体の中心から首の付け根の方を向いていますので、上半身ボーンの向き(direction)は、首の付け根の位置から胴体の中心の位置を引くことで求められます。

   direction = pos[8] - pos[7]

しかし、この情報だけではボーンの方向(orientation)を決めることはできません。どういう事かというと、例えば下図の A と Bの上半身ボーンは始点と終点の位置が同じなので、向き(direction)は同一ですが、ひねりが異なります。

ボーンの向きは同じでひねりが異なる例

AとBでは違うポーズになってしまいますので、ボーンのひねりを何らかの方法で求める必要があります。とはいえ手に入る情報は関節の位置情報だけなので、それを使って何とかするしかありません。

まず上半身については、上半身ボーンの向きを表すベクトル v1 と、体の背面方向のベクトル v2と、
左肩から右肩の方を向くベクトル v3 が互いに直交するものとして、v1 と v3 の外積で v2 の向きを求めます。

   up = QVector3D.crossProduct(direction, (pos[14] - pos[11])).normalized()

外積でv2の向きを求める

MMDの初期状態では v1 は垂直上向き(0, 1, 0)であり、v2 は(0, 0, 1)です。
この初期状態からの回転を計算します。

  upper_body_orientation = QQuaternion.fromDirection(direction, up)
  initial = QQuaternion.fromDirection(QVector3D(0, 1, 0), QVector3D(0, 0, 1))
  bf.rotation = upper_body_orientation * initial.inverted()

同様に、下半身ボーンは胴体の中心と腰の位置情報に加えて、右脚付け根と左脚付け根の位置情報を使うことで、ボーンの回転を求めています。

腕とひじのボーンについては、ひじが一方向にしか曲がらない、という条件を入れることで方向を決めています。足とひざのボーンについても同様です。(詳細はソースコードを読んでみてください)
knee.png
各ボーンの回転が求まったら、VMDフォーマットでファイルに出力します。
足のIKは切っておく必要があるので、その分のIK on/off フレーム情報もVMDファイルに追加しています。