日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

卡通驱动项目ThreeDPoseTracker——模型驱动解析

發布時間:2023/12/13 编程问答 31 豆豆
生活随笔 收集整理的這篇文章主要介紹了 卡通驱动项目ThreeDPoseTracker——模型驱动解析 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

前言

之前解析過ThreeDPoseTracker這個項目中的深度學習模型,公眾號有兄弟私信一些問題,我剛好對這個項目實現有興趣,就分析一波源碼,順便把問題解答一下。

這個源碼其實包括很多內容:3D姿態估計,坐標平滑,骨骼驅動,物理仿真等,非常值得分析。

參考博客:
ThreeDPoseTracker源碼

理論與實現

核心代碼是源碼中的VNectModel.cs,主要是用預測出的3D坐標驅動卡通人體模型,包括內容有:

  • 根關節位置
  • 各關節旋轉信息

其核心在于旋轉量的確定,至于根關節位置的確定,感覺涉及到很多亂七八糟的內置參數,就不詳細介紹了,但是會額外提供一個我用到天荒地老的計算方法。

如果下面的理論看不懂,推薦看看我按照源碼復現的一套簡化流程,一千行源碼直接重寫成兩百多行

預備知識——“LookRotation”

源碼中有個至關重要的函數LookRotation(a,b),它的作用是:

  • 使得z軸(藍色)始終精準指向a方向
  • 使得y軸(綠色)始終偏向b方向

為什么一個是精準指向,一個是偏向,因為y和z軸是垂直的,如果a和b不垂直,那么此函數就會保證z與a同方向,y和b大致同向,看圖

正方體為物體,藍色和綠色分別為y和z軸,兩個小球分別為y和z的目標方向。

左圖為標準的指向,中間和右圖為調整了目標方向后,物體的y和z的指向,可以發現,藍色z軸始終指向目標,但是綠色y軸是偏向那個方向,因為是立體圖,看著綠軸偏的很遠,其實是差不多的。

總而言之,藍軸始終是向著oa方向,綠軸向著ob與oa組成的平面中與oa垂直的方向。

驅動問題解讀

如果掃過一眼代碼,會發現有很多重復代碼,無外乎以下幾類:

初始化Init()的時候有:

AddSkeleton(xx,yy) xx.Inverse = Quaternion.Inverse(Quaternion.LookRotation(xx.position - xxx.position, yy)); xx.InverseRotation = xx.Inverse*xx.InitRotation

驅動PoseUpdate的時候有:

xx = TriangleNormal(aa,bb,cc) xx.rotation = Quaternion.LookRotation(yy) * xx.InverseRotation;

會發現初始化和驅動時候貌似是一種反向計算,所以才出現這么多逆(inverse)。

為什么要用LookRotation計算各個關節的旋轉,而非對某根骨骼直接通過Quaternion.FromToRotation(a,b)計算出初始姿態到新的姿態下需要的旋轉矩陣呢?

  • 如果只用FromToRotation計算向量到向量的旋轉,只能控制位置正確,無法控制方向正確,比如根關節到頸部是直著上去的,這時候人也可以側身也可以面向前方,所以對關節必須控制至少兩個方向的旋轉,因此必須使用LookRotation去控制z和y軸朝向。

為什么用了LookRotation還要求這么多逆(inverse)?

  • 同一個模型的不同關節具有不同的初始旋轉量(即InitRotation),而且不同人的同一個關節也可能具有不同的旋轉量,甚至初始的局部坐標軸都不同,比如源碼中提供的兩個模型的膝蓋部分局部坐標系如下

此時如果用LookRotation,不同的人就需要設置對應的規則,比如同一個姿態,左邊的人藍色z軸向后,右邊人藍色z軸向前,搞不好還有其它的情況,此時就無法用基于LookRotation的同一套代碼去驅動這個人了。

如果還是不懂為什么不能用同一套代碼驅動,可以舉個例子:小腿向后收起的時候,左圖的LookRotation必須保證藍軸向上,右圖必須保證藍軸向下,如下圖:

由于坐標軸最終的方向都不同,所以即使是同一個姿勢,對于具有不同坐標系的相同關節也需要針對性LookRotation。

那么為什么源碼里面可以使用LookRotation去玩驅動,很簡單,因為源碼將所有的關節都利用初始姿態做了LookRotation的對齊,得到了一個中間矩陣,即源碼中的xx.InverseRotation,利用這個中間矩陣就能在驅動的時候對齊所有坐標系,達到通用目的。

源碼分析與復現

如何實現上述問題中的坐標系對齊呢?

利用初始姿態下各關節的坐標和旋轉來確定,具體是:
當前關節的lookrotation=初始旋轉InitRotation×對齊矩陣當前關節的lookrotation = 初始旋轉InitRotation \times 對齊矩陣 lookrotation=InitRotation×
所以源碼中的下面類似代碼就是為了求解對齊矩陣:

xx.Inverse = Quaternion.Inverse(Quaternion.LookRotation(xx.position - xxx.position, yy)); xx.InverseRotation = xx.Inverse*xx.InitRotation

看不懂就可以寫成

Quaternion.LookRotation(xx.position - xxx.position, yy) = xx.InitRotation * Quaternion.Inverse(xx.InverseRotation)

這就對應上述公式,其中Quaternion.Inverse(xx.InverseRotation)就對應了對齊矩陣。

因此我在復現時候,以根關節的對齊矩陣為例,把上述代碼改成了

root = animator.GetBoneTransform(HumanBodyBones.Hips); midRoot = Quaternion.Inverse(root.rotation) * Quaternion.LookRotation(forward);

其中forward是按照源碼的要求,指示人體的當前方向。

如何設置LookRotation的方向?

繼續分析源碼,發現對于所有關節都做了

var forward = TriangleNormal(jointPoints[PositionIndex.hip.Int()].Transform.position, jointPoints[PositionIndex.lThighBend.Int()].Transform.position, jointPoints[PositionIndex.rThighBend.Int()].Transform.position); jointPoint.Inverse = GetInverse(jointPoint, jointPoint.Child, forward);jointPoint.InverseRotation = jointPoint.Inverse * jointPoint.InitRotation;

第一行,基于根關節和左右胯關節坐標計算出人體朝向,然后以此作為所有關節的LookRotation的y方向,以及每個關節與其子關節的方向作為z方向,計算出中間矩陣。

注意,在接下來,分別對頭部和手掌單獨又計算了一遍,因為他倆比較特殊

對于頭部,直接求解出頭到鼻子的向量作為LookRotation的z方向,未設置y方向。

var gaze = jointPoints[PositionIndex.Nose.Int()].Transform.position - jointPoints[PositionIndex.head.Int()].Transform.position; // head的方向是head->Nose head.Inverse = Quaternion.Inverse(Quaternion.LookRotation(gaze));

然后計算頭部的中間矩陣

head.Inverse = Quaternion.Inverse(Quaternion.LookRotation(gaze));head.InverseRotation = head.Inverse * head.InitRotation;

對于手腕,直接利用手腕、大拇指、中指的坐標,計算出手掌方向作為LookRotation的y方向,

var lf = TriangleNormal(lHand.Pos3D, jointPoints[PositionIndex.lMid1.Int()].Pos3D, jointPoints[PositionIndex.lThumb2.Int()].Pos3D); // 手掌方向 var rf = TriangleNormal(rHand.Pos3D, jointPoints[PositionIndex.rThumb2.Int()].Pos3D, jointPoints[PositionIndex.rMid1.Int()].Pos3D);

而左手腕以大拇指到中指的方向為z方向,而右手腕以中指到大拇指方向為z方向:

lHand.Inverse = Quaternion.Inverse(Quaternion.LookRotation(jointPoints[PositionIndex.lThumb2.Int()].Transform.position - jointPoints[PositionIndex.lMid1.Int()].Transform.position, lf)); rHand.Inverse = Quaternion.Inverse(Quaternion.LookRotation(jointPoints[PositionIndex.rThumb2.Int()].Transform.position - jointPoints[PositionIndex.rMid1.Int()].Transform.position, rf));

再分別求解出中間矩陣:

lHand.InverseRotation = lHand.Inverse * lHand.InitRotation; rHand.InverseRotation = rHand.Inverse * rHand.InitRotation;

其實完全沒必要區分這么明顯,只需要求解和使用的時候對應好就行了,比如我實現的時候就統一大拇指到中指:

midLhand = Quaternion.Inverse(lhand.rotation) * Quaternion.LookRotation(lthumb2.position - lmid1.position,TriangleNormal(lhand.position, lthumb2.position, lmid1.position)); midRhand = Quaternion.Inverse(rhand.rotation) * Quaternion.LookRotation(rthumb2.position - rmid1.position,TriangleNormal(rhand.position, rthumb2.position, rmid1.position));

也就是說,對于某些特定關節,需要單獨設置用于計算中間變換矩陣的LookRotation信息。推薦看我實現的源碼,分為軀干、頭、手掌三個部分,我實現的源碼就不貼了,文末找。

注意這里計算初始姿態中各關節的LookRotation方法與運行時從深度學習預測的3D關節坐標中計算的LookRotation方案要一模一樣。

如何驅動?

通過
當前關節的lookrotation=初始旋轉InitRotation×對齊矩陣當前關節的lookrotation = 初始旋轉InitRotation \times 對齊矩陣 lookrotation=InitRotation×
得到了每個關節的對齊矩陣,那么這個公式很容易得到每個關節的當前旋轉信息:
當前旋轉Rotation=當前關節的lookrotation×Quaternion.Inverse(對齊矩陣)當前旋轉Rotation = 當前關節的lookrotation \times Quaternion.Inverse(對齊矩陣) Rotation=lookrotation×Quaternion.Inverse()
然后分析源碼,在PoseUpdate()函數中,前面的不用看,是計算根關節坐標的,我們先關注關節旋轉。

注意因為用對齊矩陣是從初始姿態獲取的,所以如何依據初始姿態計算的lookrotation就要按照同樣的方法從深度學習模型預測的3D關節坐標中計算對應的lookrotation參數。

比如人體方向依舊是根、左右胯部的坐標計算:

var forward = TriangleNormal(jointPoints[PositionIndex.hip.Int()].Pos3D, jointPoints[PositionIndex.lThighBend.Int()].Pos3D, jointPoints[PositionIndex.rThighBend.Int()].Pos3D);

而根關節當前的旋轉就是根據上述公式計算得到:

jointPoints[PositionIndex.hip.Int()].Transform.rotation = Quaternion.LookRotation(forward) * jointPoints[PositionIndex.hip.Int()].InverseRotation;

其它關節我不貼源碼了,直接描述:

軀干關節:以身體方向為LookRotation的y方向,以當前關節到其子關節的方向為z方向。

手腕:以手腕、大拇指、中指形成的平面的法線方向為y方向,以拇指到中指的方向為z方向。

比如我隨便貼一下我復現的左臂(肩、肘、腕)實時驅動:

// 左臂 lshoulder.rotation = Quaternion.LookRotation(pred3D[5] - pred3D[6], forward) * Quaternion.Inverse(midLshoulder); lelbow.rotation = Quaternion.LookRotation(pred3D[6] - pred3D[7], forward) * Quaternion.Inverse(midLelbow); lhand.rotation = Quaternion.LookRotation(pred3D[8] - pred3D[9],TriangleNormal(pred3D[7], pred3D[8], pred3D[9]))*Quaternion.Inverse(midLhand);

其中pred3D就是深度學習模型預測的3D關節坐標。

人體位置

上述講解了旋轉的計算方法,關于整個人體的位置,源碼中自有一套方案,但是里面預設了很多固定參數,不是特別想分析,所以用了萬年不變的方法,計算unity人物模型腿的長度和深度學習預測的腿部長度,然后計算比例系數,乘到深度學習預測的根關節位置即可。

float tallShin = (Vector3.Distance(pred3D[16], pred3D[17]) + Vector3.Distance(pred3D[20], pred3D[21]))/2.0f; float tallThigh = (Vector3.Distance(pred3D[15], pred3D[16]) + Vector3.Distance(pred3D[19], pred3D[20]))/2.0f; float tallUnity = (Vector3.Distance(lhip.position, lknee.position) + Vector3.Distance(lknee.position, lfoot.position)) / 2.0f +(Vector3.Distance(rhip.position, rknee.position) + Vector3.Distance(rknee.position, rfoot.position)); root.position = pred3D[24] * (tallUnity/(tallThigh+tallShin));

是不是超級簡單,雖然效果有點偏差,但是后續還是會分析一下源碼中更新人體位置的方案。

復現流程

在VNectModel.cs中的PoseUpdate函數加入以下代碼:

FileStream fs = new FileStream(@"D:\code\Unity\ThreeDExperiment\Assets\Resources\record.txt", FileMode.Append); StreamWriter sw = new StreamWriter(fs); //寫入 foreach(JointPoint jointPoint in jointPoints) {sw.Write(jointPoint.Pos3D.x.ToString() + " " + jointPoint.Pos3D.y.ToString() + " " + jointPoint.Pos3D.z.ToString() + " "); } sw.WriteLine(); sw.Flush(); sw.Close(); fs.Close();

將關鍵點寫入到txt中做復現時候用的3D關鍵點數據

然后按照理論進行復現后效果如下:

紅色為預測的3D坐標,人物模型會做出與紅色骨架一樣的姿勢。

結論

這個感覺還是沒有考慮人體運動的動力學特性,如果跑過源碼,很容易發現個別姿勢會出現奇怪的關節扭曲現象,這就是不考慮動力學的后果,給我自己之前的代碼打一波廣告,那個絕對比這個好,哈哈。

完整的unity實現放在微信公眾號的簡介中描述的github中,有興趣可以去找找。或者在公眾號回復“ThreeDPose",同時文章也同步到微信公眾號中,有疑問或者興趣歡迎公眾號私信。

總結

以上是生活随笔為你收集整理的卡通驱动项目ThreeDPoseTracker——模型驱动解析的全部內容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。