不知道從什麼時候開始,3D動畫就熱起來了,但是很多經典動畫3D化後就變味了,人物的肢體動作看上去僵硬了不少。並且,傳統3D靠一幀一幀製作,費時費力。一位日本中二少年自學了機器學習後,就給自己做了個酷炫的模型,可以把自己的動作實時變成流暢的3D人物動作,而且整個過程非常簡單易操作。這個推特名為幸彥青柳(Yukihiko Aoyagi)的日本小哥將3D姿態估計與3D開發平臺和一些渲染引擎(比如Unity)相結合,於此更夠跟準確地跟蹤3D空間中的人體運動。上面的動圖就是針對動作的實時估計和生成。不過可惜的是,這個專案目前還只支援單人動作,不能實現雙人對打。青柳君嘗試過多種實現方式,包括WindowsML,ML.Net,Onnx Runtime等,但最終選擇了OpenCVSharp,也就是OpenCV模型匯入功能,在Unity中載入和執行Onnx,因為OpenCVSharp在Unity和.Net環境中可以用相同的方式處理,影像也不會被轉換為Mat格式。儘管看上去處理起來很容易,但目前還缺少相關資料,青柳君特意總結了他的這次嘗試,將文章公佈在了Qiita上。// Properties for onnx and estimation
private Net Onnx;
private Mat[] outputs = new Mat[4];
private const int inputImageSize = 224;
private const int JointNum = 24;
private const int HeatMapCol = 14;
private const int HeatMapCol_Squared = 14 * 14;
private const int HeatMapCol_Cube = 14 * 14 * 14;
char[] heatMap2Dbuf = new char[JointNum * HeatMapCol_Squared * 4];
float[] heatMap2D = new float[JointNum * HeatMapCol_Squared];
char[] offset2Dbuf = new char[JointNum * HeatMapCol_Squared * 2 * 4];
float[] offset2D = new float[JointNum * HeatMapCol_Squared * 2];
char[] heatMap3Dbuf = new char[JointNum * HeatMapCol_Cube * 4];
float[] heatMap3D = new float[JointNum * HeatMapCol_Cube];
char[] offset3Dbuf = new char[JointNum * HeatMapCol_Cube * 3 * 4];
float[] offset3D = new float[JointNum * HeatMapCol_Cube * 3];
public void InitONNX()
{
Onnx = Net.ReadNetFromONNX(Application.dataPath + @"\MobileNet3D2.onnx");
for (var i = 0; i < 4; i++) outputs[i] = new Mat();
}
/// <summary>
/// Predict
/// </summary>
/// <param name="img"></param>
public void Predict(Mat img)
{
var blob = CvDnn.BlobFromImage(img, 1.0 / 255.0, new OpenCvSharp.Size(inputImageSize, inputImageSize), 0.0, false, false);
Onnx.SetInput(blob);
Onnx.Forward(outputs, new string[] { "369", "373", "361", "365" });
// copy 2D outputs
Marshal.Copy(outputs[2].Data, heatMap2Dbuf, 0, heatMap2Dbuf.Length);
Buffer.BlockCopy(heatMap2Dbuf, 0, heatMap2D, 0, heatMap2Dbuf.Length);
Marshal.Copy(outputs[3].Data, offset2Dbuf, 0, offset2Dbuf.Length);
Buffer.BlockCopy(offset2Dbuf, 0, offset2D, 0, offset2Dbuf.Length);
for (var j = 0; j < JointNum; j++)
{
var maxXIndex = 0;
var maxYIndex = 0;
jointPoints[j].score2D = 0.0f;
for (var y = 0; y < HeatMapCol; y++)
{
for (var x = 0; x < HeatMapCol; x++)
{
var l = new List<int>();
var v = heatMap2D[(HeatMapCol_Squared) * j + HeatMapCol * y + x];
if (v > jointPoints[j].score2D)
{
jointPoints[j].score2D = v;
maxXIndex = x;
maxYIndex = y;
}
}
}
jointPoints[j].Pos2D.x = (offset2D[HeatMapCol_Squared * j + HeatMapCol * maxYIndex + maxXIndex] + maxXIndex / (float)HeatMapCol) * (float)inputImageSize;
jointPoints[j].Pos2D.y = (offset2D[HeatMapCol_Squared * (j + JointNum) + HeatMapCol * maxYIndex + maxXIndex] + maxYIndex / (float)HeatMapCol) * (float)inputImageSize;
}
// copy 3D outputs
Marshal.Copy(outputs[0].Data, heatMap3Dbuf, 0, heatMap3Dbuf.Length);
Buffer.BlockCopy(heatMap3Dbuf, 0, heatMap3D, 0, heatMap3Dbuf.Length);
Marshal.Copy(outputs[1].Data, offset3Dbuf, 0, offset3Dbuf.Length);
Buffer.BlockCopy(offset3Dbuf, 0, offset3D, 0, offset3Dbuf.Length);
for (var j = 0; j < JointNum; j++)
{
var maxXIndex = 0;
var maxYIndex = 0;
var maxZIndex = 0;
jointPoints[j].score3D = 0.0f;
for (var z = 0; z < HeatMapCol; z++)
{
for (var y = 0; y < HeatMapCol; y++)
{
for (var x = 0; x < HeatMapCol; x++)
{
float v = heatMap3D[HeatMapCol_Cube * j + HeatMapCol_Squared * z + HeatMapCol * y + x];
if (v > jointPoints[j].score3D)
{
jointPoints[j].score3D = v;
maxXIndex = x;
maxYIndex = y;
maxZIndex = z;
}
}
}
}
jointPoints[j].Now3D.x = (offset3D[HeatMapCol_Cube * j + HeatMapCol_Squared * maxZIndex + HeatMapCol * maxYIndex + maxXIndex] + (float)maxXIndex / (float)HeatMapCol) * (float)inputImageSize;
jointPoints[j].Now3D.y = (float)inputImageSize - (offset3D[HeatMapCol_Cube * (j + JointNum) + HeatMapCol_Squared * maxZIndex + HeatMapCol * maxYIndex + maxXIndex] + (float)maxYIndex / (float)HeatMapCol) * (float)inputImageSize;
jointPoints[j].Now3D.z = (offset3D[HeatMapCol_Cube * (j + JointNum * 2) + HeatMapCol_Squared * maxZIndex + HeatMapCol * maxYIndex + maxXIndex] + (float)(maxZIndex - 7) / (float)HeatMapCol) * (float)inputImageSize;
}
}
模型輸入224x224的影像,輸出的關節數為24個,熱圖(Heatmap)為14x14。2D熱圖格式是24x14x14,3D的是24x14x14x14。將其作為與熱圖的座標偏移值,輸出的2D(x,y)變為2x24x14x14,3D(x,y,z)變為3x24x14x14x14。public void InitONNX()
{
Onnx = Net.ReadNetFromONNX(Application.dataPath + @"\MobileNet3D2.onnx");
for (var i = 0; i < 4; i++) outputs[i] = new Mat();
}
由於OpenCV的輸出是透過Mat物件返回的,需要準備四個陣列。public void Predict(Mat img)
{
var blob = CvDnn.BlobFromImage(img, 1.0 / 255.0, new OpenCvSharp.Size(inputImageSize, inputImageSize), 0.0, false, false);
Onnx.SetInput(blob);
Onnx.Forward(outputs, new string[] { "369", "373", "361", "365" });
// copy 2D outputs
Marshal.Copy(outputs[2].Data, heatMap2Dbuf, 0, heatMap2Dbuf.Length);
Buffer.BlockCopy(heatMap2Dbuf, 0, heatMap2D, 0, heatMap2Dbuf.Length);
Marshal.Copy(outputs[3].Data, offset2Dbuf, 0, offset2Dbuf.Length);
Buffer.BlockCopy(offset2Dbuf, 0, offset2D, 0, offset2Dbuf.Length);
Predict方法引數的Mat物件是正常的CV_8UC3 Mat影像資料,需要將其轉換為Blob Mat才能傳遞給Onnx,這個過程利用BlobFromImage就能完成。在Output中,“369”和“373”是3D,“361”和“365”是2D。但如果是Mat物件,處理起來就稍微複雜一些,因為還需要將其轉換為float陣列。由於3D是一個相當大的迴圈,最好再做一些改進,但是由於它現在移動得足夠快,保持原樣也是可以的。去年的日本黃金週,青柳君第一次接觸機器學習,也一直在3D姿勢估計這塊有所鑽研。今年3月份,他在iOS上實現了3D姿勢估計。據本人推特發言稱,他用了一天時間學習,然後做出了這個模型。根據青柳君本人介紹,iOS專案的學習環境是Windows10/PyTorch0.4,執行環境是iPhone XS Max,至於選擇iPhone XS Max的原因,青柳君說,iPhone XS Max的A12處理器功能非常強大。青柳君準備了2D和3D的資料集,2D資料集是利茲運動姿勢資料集,利茲運動姿勢擴充套件訓練資料集、MPII人類姿勢資料集、Microsoft COCO;而3D資料集是原始資料集。在此之前他還做了很多準備,包括從AssetStore購買的資料等,當然還有Unity。然後就可以利用Unity建立3D角色動畫了,建立角色影像和座標,包括肩膀、肘部、手腕、拇指、中指、腳、膝蓋、腳踝、腳趾、耳朵、眼睛、鼻子,以輸出身體的中心位置,即肚臍。該資料集由於許可原因結果變得十分複雜,導致釋出失敗。由於這是CG,因此可以隨意更改角色的紋理和姿勢。最初,他希望更改每個時期資料集的內容,以提高泛化效能,但沒有效果,為此大約有100,000個副本用於學習。即使是用3D版本的影像,也可以照原樣學習,最後可以獲得相似的影像,但是無法獲得預期的效能。將透過PyTorch學習得到的模型匯出到Onnx,用coremltools轉換為CoreML模型,此時就算是估計到了相同的影像,結果也會有所不同,所以準確度未知。將模型匯入Mac,使用XCode的iPhone版本,透過實時捕獲後方攝像機影像執行3D估計。XS Max能以大約40fps的速度執行,但是,一段時間,手機會變熱,速記也會下降至約30fps。如果僅用於學習2D模型,其執行速度會接近100fps。由於這是個3D專案,顯示時無法從攝像機看到的部分,判斷熱圖的閾值已降低到幾乎為零。例如,如果手臂正常可見,熱圖的最大部分為0.5或更高(最大值為1.0);如果看不到手臂,將得到0.2或0.1的值,閾值降低。但就結果而言,無論身在何處,系統都可以判斷為有人。上週,Adobe也釋出了一款用於視覺效果和動態圖形軟體After Effects,該軟體的AI功能能夠自動跟蹤人體運動並將其應用於動畫。簡單地說,就是能夠把現實人物的動作直接轉換成為動畫。Adobe研究科學家Jimei Yang在演示中說,這一功能利用了Adobe的人工智慧平臺Sensei,該平臺用超過10000張影像進行了訓練,從而能夠識別人體的關鍵點。據瞭解,人體跟蹤器在源影片中能夠檢測到人體的運動,胳膊、軀幹和腿部的18個關節點將生成相關跟蹤點,然後將跟蹤點轉移到動畫角色上,利用該功能,快速建立2D人物動畫根本不在話下!當然,對於姿勢估計的實現還遠遠不止現在的程度,未來希望不僅是青柳君和Adobe,有更多人都參與到這個領域的研究和學習中來,促進相關領域的發展。