如何將 MediaPipe 的骨架轉換到 Unity 人體骨架?

前言

MediaPipe 是由 Google 所維護與開發的 ML Solution,提供多個影像辨識的 AI 模型,來進行人體骨架的偵測。其中包括多個人體部位1的偵測(圖一),諸如臉部、手部、軀幹、頭髮⋯⋯等等。

圖一
圖一

人體軀幹辨識的結果可以有需多應用,比如說手部的骨架可以協助我們進行手勢操作,我們也可以透過頭髮區域的辨識來改變髮色⋯⋯等。

另一個常見的應用,莫過於透過 Holistic2 的解決方案來進行全身的骨架偵測。由於該解決方案包含臉部與身體骨架的資訊,使我們得以取代傳統的動作捕捉系統(比如 Motion Capture 裝置),對於 Vtuber 等較不需精細動作的應用相當的有幫助!

一旦我們得到 MediaPipe 的骨架資訊後,經過轉換,我們可以將其套用到個人的人體模型 (比如 VRM 模型)。經由 Unity 或是 Autodesk 等建模軟體或遊戲引擎的支援,能對骨架進行即時的開發與修改。

然而,儘管在 Youtube 等影音平台上,我們看到有些許 Demo 影片展示了這之間的轉換,卻因為商業軟體的緣故,並沒有開發相關的程式碼與作法。

因此,在這篇文章中,我們將會說明我們是如何將 MediaPipe 的人體骨架轉換到 Unity 的人體骨架上,並且將轉換的程式碼開源,希望能藉社群之力,將我們的轉換做得更完整。

GanniPiece/MetU: A mapping from Mediapipe skeleton to Unity humanoid skeleton. (github.com)

若您對 Unity 與 MediaPipe 的座標系轉換,或是三維角度的旋轉已有概念,可以直接跳至作法繼續。

坐標系轉換

MediaPipe 中的坐標系 是所謂的右手座標系3,而 Unity 引擎中則是左手座標系4。除此之外,在 MediaPipe 中的原點是由右上開始,而 Unity 則是由左下開始。 我們可以從以下的圖進行兩個座標系的比較:

image_1673276190409_0.png
圖二

左邊的是 Mediapipe 的座標系,而右邊則是 Unity 的世界座標系。

注意:Mediapipe 中 world pose landmark 與 pose landmark 兩者雖皆為右手座標系,但定義上有些許不同。前者是以人的 hip 為坐標中心,後者則是以畫面左上角作為原點。

三維物體旋轉

在三維空間中以動態歐拉角計算物體的旋轉角度,很難不遇到環價鎖定 (萬象鎖) 5 的問題。當然,若我們以靜態來計算,便不會遇到此問題。

什麼是靜態又什麼是動態歐拉角呢?

我們先從動態開始:想像我們有個三個旋轉矩陣 $R_x$ , $R_y$ , $R_z$ ,分別紀錄了三軸上的旋轉。這也代表了以下式子的關係: $$Rp = R_xR_yR_zp = (R_xR_y)(R_zp)$$ 點 $p$ 的旋轉,其實就是由 $R_x$ , $R_y$ , 以及 $R_z$ 三軸上的旋轉所組成。然則,對於 x, y 軸上的旋轉來說,點 $p$ 實際已是經過 $z$ 軸的旋轉了。這會造成什麼現象呢?就是當今天沿著 $y$ 軸旋轉的大小是 $90$ 度時,所有 $z$ 軸的旋轉皆會與 $y$ 軸重疊,如此一來,我們就會失去其中一維的資訊。

https://upload.wikimedia.org/wikipedia/commons/4/49/Gimbal_Lock_Plane.gif
圖三5

我們以維基百科的這張圖來作為說明。紫綠藍分別代表了飛機的三軸旋轉,當今天各軸的經過旋轉後,使得兩者甚至三者的維度資訊消失,之後的對於途中內圈兩者的旋轉,皆是相同的結果。

你說,那我們就透過一個不會變的座標系來進行旋轉這件事不就解決了嗎?

沒錯,這也是為什麼我們避免使用動態歐拉角來描述三圍的旋轉,而以一個統一的座標系,比如說世界座標來紀錄旋轉。如此一來,就可以避免掉三維空間旋轉的萬象鎖問題。在 Unity 中,要計算這個角度我們可以透過以下的方法完成:

1using UnityEngine;
2Vector3.FromToRotation(fromDirection_a, toDirection_b);

FromToRotation 提供我們一個從 a 向量至 b 向量間的旋轉角度,該方法回傳的是一個 Quaternion 的值。假如我們想要獲得的是歐拉角的話,只要加上 .eulerAngles 即可取用。

作法

有了上述兩個先備知識後,就可以來看我們是如何將兩方的座標進行對應了。主要的步驟可以分成兩個大方向:

  1. 轉換座標軸
  2. 設定角度

我們從上面坐標系轉換的圖中看到,Unity 世界座標與 MediaPipe world pose landmark 間的座標系都差了一個方向,因此我們在獲得 MediaPipe 上各個座標位置後,需要將所有座標點乘上一個負號,來轉換到 Unity 的世界座標上。

$$P_{Unity}(x,y,z) = -\alpha P_{MediaPipe} (x,y,z) = \alpha P_{MediaPipe} (-x, -y, -z)$$ , $\alpha$ 是一個大於零的常數,是兩邊座標系單位長度的比例。

舉例來說,我們看到圖片中 Unity 世界座標 y 軸(綠色箭頭)的負方向,對應到的是 MediaPipe World Pose Landmark 的正方向,x 與 z 軸上的情況亦是如此。因此,假如在 MediaPipe 上的座標點 $(-1, -1, -1)$ ,在 Unity 上就會變成 $\alpha (1, 1, 1)$ 。

進行完座標轉換後,我們要計算 Unity 中骨架向量,與 MediaPipe 中對應的骨架向量的旋轉角度,再對其進行旋轉,即可將 Unity 中的人物模型移至指定動作。舉個例子來說,假如我們要設定上手臂 (upperArm) 的旋轉角度,我們可以將 MediaPipe 中對應的向量(肩膀 — 手肘)的向量算出,接著再計算出 Unity 中人物模型當前 肩膀-手肘 的向量,透過 FromToRotation 算出兩者間的旋轉角度,將 Unity 中的向量旋轉至指定的 MediaPipe 向量,即可進行轉換。

我們可以將上述的步驟統整為以下的流程(對於每一個關節點):

1MappingBoneFromMediaPipeToUnity (mediaPipeVec, unityVec, target):
2var mediaPipeVecInUnity = -mediaPipeVec
3var rotation = FromToRotation(unityVec, mediaPipeVecInUnity)
4target.Rotate(rotation, Space.World)

小結

在這篇文章中,我們提到了一個即時的骨架偵測解決方案——MediaPipe,該機器學習的解決方案協助我們快速的獲得骨架資訊。然而,網路上缺乏將其骨架對應到 Unity 人物骨架的方法,因此,我們在此篇文章中提供一個簡單易懂的作法。我們同時將轉換的原始碼公開,有興趣的讀者可以前往參考。

參考資料