日本中二少年教你用姿勢估計把自己變成3D人物,動作實時同步,iOS上也能實現

大資料文摘發表於2019-11-11
不知道從什麼時候開始,3D動畫就熱起來了,但是很多經典動畫3D化後就變味了,人物的肢體動作看上去僵硬了不少。並且,傳統3D靠一幀一幀製作,費時費力。

現在,你就擁有一個拯救3D動畫的機會!

一位日本中二少年自學了機器學習後,就給自己做了個酷炫的模型,可以把自己的動作實時變成流暢的3D人物動作,而且整個過程非常簡單易操作。

話不多說先看效果圖:
日本中二少年教你用姿勢估計把自己變成3D人物,動作實時同步,iOS上也能實現
這個推特名為幸彥青柳(Yukihiko Aoyagi)的日本小哥將3D姿態估計與3D開發平臺和一些渲染引擎(比如Unity)相結合,於此更夠跟準確地跟蹤3D空間中的人體運動。上面的動圖就是針對動作的實時估計和生成。

不過可惜的是,這個專案目前還只支援單人動作,不能實現雙人對打。
日本中二少年教你用姿勢估計把自己變成3D人物,動作實時同步,iOS上也能實現
專案已經在GitHub上開源:https://github.com/yukihiko/ThreeDPoseUnitySample?source=post_page-----e74d7d347c2

趁著它還沒刷爆朋友圈,趕緊上手試一試!

用3D姿勢估計的Onnx模型移動Unity

青柳君嘗試過多種實現方式,包括WindowsML,ML.Net,Onnx Runtime等,但最終選擇了OpenCVSharp,也就是OpenCV模型匯入功能,在Unity中載入和執行Onnx,因為OpenCVSharp在Unity和.Net環境中可以用相同的方式處理,影像也不會被轉換為Mat格式。

儘管看上去處理起來很容易,但目前還缺少相關資料,青柳君特意總結了他的這次嘗試,將文章公佈在了Qiita上。

相關連結:https://qiita.com/yukihiko_a/items/386e3a86a5e523757707

有關Onnx的程式碼部分如下:
// 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。

日本中二少年教你用姿勢估計把自己變成3D人物,動作實時同步,iOS上也能實現

public void InitONNX()
    {
        Onnx = Net.ReadNetFromONNX(Application.dataPath + @"\MobileNet3D2.onnx");
        for (var i = 0; i < 4; i++) outputs[i] = new Mat();
    }
首先,使用InitONNX()讀取Onnx檔案。

由於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是一個相當大的迴圈,最好再做一些改進,但是由於它現在移動得足夠快,保持原樣也是可以的。

在iOS上也能實現3D姿勢估計


去年的日本黃金週,青柳君第一次接觸機器學習,也一直在3D姿勢估計這塊有所鑽研。

今年3月份,他在iOS上實現了3D姿勢估計。據本人推特發言稱,他用了一天時間學習,然後做出了這個模型。
日本中二少年教你用姿勢估計把自己變成3D人物,動作實時同步,iOS上也能實現

根據青柳君本人介紹,iOS專案的學習環境是Windows10/PyTorch0.4,執行環境是iPhone XS Max,至於選擇iPhone XS Max的原因,青柳君說,iPhone XS Max的A12處理器功能非常強大。

還是先看看效果如何:

日本中二少年教你用姿勢估計把自己變成3D人物,動作實時同步,iOS上也能實現  
青柳君準備了2D和3D的資料集,2D資料集是利茲運動姿勢資料集,利茲運動姿勢擴充套件訓練資料集、MPII人類姿勢資料集、Microsoft COCO;而3D資料集是原始資料集。

在此之前他還做了很多準備,包括從AssetStore購買的資料等,當然還有Unity。

然後就可以利用Unity建立3D角色動畫了,建立角色影像和座標,包括肩膀、肘部、手腕、拇指、中指、腳、膝蓋、腳踝、腳趾、耳朵、眼睛、鼻子,以輸出身體的中心位置,即肚臍。

該資料集由於許可原因結果變得十分複雜,導致釋出失敗。
日本中二少年教你用姿勢估計把自己變成3D人物,動作實時同步,iOS上也能實現

由於這是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釋出最新動作追蹤軟體

上週,Adobe也釋出了一款用於視覺效果和動態圖形軟體After Effects,該軟體的AI功能能夠自動跟蹤人體運動並將其應用於動畫。

簡單地說,就是能夠把現實人物的動作直接轉換成為動畫。
日本中二少年教你用姿勢估計把自己變成3D人物,動作實時同步,iOS上也能實現

與青柳君的機器學習專案的效果相差無幾!

Adobe研究科學家Jimei Yang在演示中說,這一功能利用了Adobe的人工智慧平臺Sensei,該平臺用超過10000張影像進行了訓練,從而能夠識別人體的關鍵點。

據瞭解,人體跟蹤器在源影片中能夠檢測到人體的運動,胳膊、軀幹和腿部的18個關節點將生成相關跟蹤點,然後將跟蹤點轉移到動畫角色上,利用該功能,快速建立2D人物動畫根本不在話下!

怎麼樣,有沒有覺得開啟了新世界的大門?

當然,對於姿勢估計的實現還遠遠不止現在的程度,未來希望不僅是青柳君和Adobe,有更多人都參與到這個領域的研究和學習中來,促進相關領域的發展。

相關文章