Unity3D Kinect 實時顯示對象的頭部圖像

最近在做一個Unity+Kinect 的項目,因爲涉及一些姿勢的識別,所以要鎖定一個識別的骨架,但是用戶怎麼知道我鎖定的骨架是誰呢?於是想到一個方法,那就是把當前的鎖定的骨架的對象的頭部圖片展示出來,那麼這樣用戶就知道當前檢測的是誰啦~
話不多說,先展示一下demo,最終的一個展示效果~
這裏寫圖片描述
有了這個想法,那麼我們就來開始動手做吧,查找了一些資料,但是一輸入關鍵詞頭部或者face什麼的都是一些Unity的面部檢測的相關內容,但是我只是想把頭部的彩色圖像從ColorView中摳出來而已啊,而且,我的項目最終是需要戴Oculus的,所以面部檢測的話,估計最後帶上那麼大的Oculus也識別不出來。於是乎,突然想到Unity的開發者包裏面有一個GreenScreen的項目,但是那個導入貌似有問題…於是乎去解決一下問題,然後再來研究一些那個項目是幹嘛的…
對於GreenScreen的導入之後的錯誤,我們只要更改一下Shader的一處變量就好啦~
這裏寫圖片描述
然後,我們運行這個程序,你會發現它其實是把檢測到的人的部分顯示在屏幕上,而其他的部分全部用綠色填充,這也就是爲什麼叫做GreenScreen吧…
我們來分析一下這個代碼:CoordinateMapperManager和CoordinateMapperView。
瀏覽一遍代碼之後,它的實現流程是這樣的,利用Kinect的MultiSourceFrameReader讀取Kinect的彩色圖像,深度圖像和BodyIdex,注意,這個BodyIndex不是Body,它不包含骨骼的信息,它是一個和kinect 深度圖像等大的一個數組,如果你用texture展示出來它的話,你會得到一個身體的輪廓【如果你想查看一下這個輪廓的話,可以在CoordinateMapperManager裏面更改一個texture的聲明:m_pColorRGBX = new Texture2D(cDepthWidth,cDeapthHeight,TextureFormat.BC4,false) ,然後在ProcessFrame函數裏面更改一下材質的填充語句爲m_pColorRGBX.LoadRawTextureData(pBodyIndexBuffer),然後運行的話,你就應該可以看到一個紅色的人的輪廓圖像】
額,說的有點多,我就不在這裏分析代碼了,就是說一下實現,他這個例子的實現過程是:採集到圖像的數據,然後把深度圖像的數據以及BodyIndex的數據作爲類似掩碼的一個功能,在Shader裏面片元着色器渲染的時候,判斷當前的像素時候是需要渲染的狀態,是的話,就渲染,不是的話就渲染成綠色,大體上講有點類似於PS的模板的功能。
這裏寫圖片描述
這裏是例子Shader的片元着色器的部分代碼:
這裏寫圖片描述
所以呢,我也就想到了,那我也做一個“模板”啊,於是呢,在做之前,我們要想一想具體的過程是怎麼樣的?
我這裏的流程是:
1. 選取Kinect最近骨架的Head和SpineShoulder兩個節點,作爲計算展示的區域。
2. 把骨骼的Camera座標映射到彩色圖片的座標
3. 計算出末班數組的內容(這裏0表示不渲染,1表示渲染)
4. Shader片元着色器渲染材質
嗯,好下面就開始編寫代碼創建一個與BodySourceView類似的MineBodySourceView腳本,首先根據判斷當前檢測到的骨架的遠近,拿出來距離最近的骨架的bodytrackid。

        //get the nearest body
        float near_Z = 5;
        foreach (var body in data) {
            if (body == null)
            {
                continue;
            }
            if (body.IsTracked) {
                if (body.Joints [Kinect.JointType.SpineMid].Position.Z < near_Z) {
                    nearest_body = body.TrackingId;
                    near_Z = body.Joints [Kinect.JointType.SpineMid].Position.Z;
                }
            }
        }

然後,根據對應骨架的頭部和肩膀中部的座標,計算顯示區域的大小。

    private void RefreshBodyObject(Kinect.Body body, GameObject bodyObject)
    {
        for (Kinect.JointType jt = Kinect.JointType.SpineBase; jt <= Kinect.JointType.ThumbRight; jt++)
        {
            Kinect.Joint sourceJoint = body.Joints[jt];
            Kinect.Joint? targetJoint = null;

            if(_BoneMap.ContainsKey(jt))
            {
                targetJoint = body.Joints[_BoneMap[jt]];
            }

            Transform jointObj = bodyObject.transform.FindChild(jt.ToString());
            jointObj.localPosition = GetVector3FromJoint(sourceJoint);
            if (body.TrackingId == nearest_body && jt == Kinect.JointType.Head) {
                headposition.X = jointObj.position.x;
                headposition.Y = jointObj.position.y;
                headposition.Z = jointObj.position.z;
                camerapoints [0] = headposition;
            }
            if (body.TrackingId == nearest_body && jt == Kinect.JointType.SpineShoulder) {
                neckposition.X = jointObj.position.x;
                neckposition.Y = jointObj.position.y;
                neckposition.Z = jointObj.position.z;
                camerapoints [1] = neckposition;
            }
            LineRenderer lr = jointObj.GetComponent<LineRenderer>();
            if(targetJoint.HasValue)
            {
                lr.SetPosition(0, jointObj.localPosition);
                lr.SetPosition(1, GetVector3FromJoint(targetJoint.Value));
                lr.SetColors(GetColorForState (sourceJoint.TrackingState), GetColorForState(targetJoint.Value.TrackingState));
            }
            else
            {
                lr.enabled = false;
            }
        }
    }

然後我們需要在HeadShowManager裏面獲得這個camerapoints的數據,創建一個MapCameraToImage函數:

    private void MapCameraToImage(){
        CameraSpacePoint[] CameraPoints = GameObject.Find ("BodyView").GetComponent<MineBodySourceView> ().getCameraPoints ();
        if (CameraPoints.Length > 0 && CameraPoints[0].X != 0 &&CameraPoints[0].Y != 0&&CameraPoints[0].Z != 0) {
//          Debug.Log ("head camera position is " + CameraPoints[0].X + ","+CameraPoints[0].Y+","+CameraPoints[0].Z);
            head = m_pCoordinateMapper.MapCameraPointToColorSpace (CameraPoints[0]);
            neck = m_pCoordinateMapper.MapCameraPointToColorSpace (CameraPoints[1]);
//          Debug.Log ("head image position is " + head.X + ","+head.Y);
            int distance = (int)Mathf.Ceil(Mathf.Abs ((head.X - neck.X) + (head.Y - neck.Y)));
//          Debug.Log ("distance is " + distance );
            int TX = (int)Mathf.Ceil(head.X+distance);
            int TY = (int)Mathf.Ceil(head.Y+distance);
            int BX = (int)Mathf.Ceil(head.X-distance);
            int BY = (int)Mathf.Ceil(head.Y-distance);
            _Key [0] = BX;
            _Key [1] = TX;
            _Key [2] = BY;
            _Key [3] = TY;
//              Debug.Log ("Top Right is " + TX+","+TY );
//              Debug.Log ("Bottom Left is " + BX+","+BY );
            for (int i = 0; i < cColorWidth * cColorHeight ; i++) {
                int X = i % cColorWidth;
                int Y = i / cColorWidth; 
                if (X > BX && X < TX && Y > BY && Y < TY) {
                    _Mask [i] = 1;
                } else {
                    _Mask [i] = 0;
                }
            }

        }
    }

注意上文把Camera空間座標映射到彩色圖片座標的是MapCameraPointToColorSpace函數。
因爲Kinect獲得的彩色圖片的分辨率是1920*1080,所以我們的末班也要是這個大小的數組,於是上文中,我們給一個1920*1080 的數組添加數字,根據我們獲得的head以及spineshould的數據,可以確定一個展示的區域(我這裏是以head爲中心,2倍的head+spineshoulder的正方形)。
下面呢,就是把這個模板載入shader啦。
我們構造HeadColorShow腳本Start裏面先建立一個computeBuffer用於給Shader傳值:

            mask = ColorSourceManager.GetComponent<HeadShowManager> ().GetMask ();
    //  Debug.Log ("mask length is "+mask.Length);
        if (mask != null) {
            maskbuffer = new ComputeBuffer (mask.Length, sizeof(int));
            gameObject.GetComponent<Renderer> ().material.SetBuffer ("_Mask", maskbuffer);
        }

然後再update裏面,更新每一幀的“模板”的值:

        int[] buffer = new int[1920 * 1080];
        for (int i = 0; i < mask.Length; i++)
        {
            buffer[i] = (int)mask[i];
        }
        maskbuffer.SetData(buffer);
        buffer = null;

最後呢,我們需要根據GreenScreen裏面的shader更改一個我們自己的shader:
其實其他地方不需要做太多的更改,只需要更改一下片元着色器就好啦~

float4 frag (ps_input i, in uint id : SV_InstanceID) : COLOR
{
    float4 o;

    int colorWidth = (int)(i.tex.x * (float)1920);
    int colorHeight = (int)(i.tex.y * (float)1080);
    int colorIndex = (int)(colorWidth + colorHeight * (float)1920);

    o = float4(0, 0, 0, 1);
    if (_Mask[colorIndex] == 1)
    {
        o = _MainTex.Sample(sampler_MainTex, i.tex);
    }

    return o;
}

哦,對了,在setbuffer那裏需要填寫一個name,那個name必須和shader裏面聲明的變量名稱一樣,如我這裏:

HeadColorShow.cs:
    gameObject.GetComponent<Renderer> ().material.SetBuffer ("_Mask", maskbuffer);
trackhead.shader:
    StructuredBuffer<int> _Mask;

看到了嗎,你只有在setbuffer哪裏聲明我們的數據放在“_Mask”的變量裏面,那麼,在shader裏面我們纔可以使用_Mask變量。
到此爲止,我們已經完成了一個類似頭部追蹤的demo。
這裏寫圖片描述
但是,更多的時候,我們需要只顯示我們頭部的圖像,其他地方既然已經被模板過濾了,那麼我們就不想顯示了。
那怎麼解決這個問題呢?
如果你熟悉Shader的話,其實,shader在渲染材質的紋理的時候,是每個像素每個像素去採樣的,然後再畫在紋理上。所以呢,我們只要保證shader的採樣範圍恰好就是我們展示的範圍,那麼所渲染的紋理就是我們期望的啦。
我們可以使用數據縮放來把一個大的集合映射到一個小的範圍:
比如吧[1,100]的數據縮放到[50,90]的範圍,我們可以根據二元一次的方程,很容易的求出來這個映射的公式:
這裏寫圖片描述
所以就可以得到這個轉換公式是Y= (90 - 50)/100 * X + 50;
同理,我們可以分析一下我麼像素點的橫向映射:原圖的寬度是1920即X的原本變化範圍是[0,1920],而顯示的X的範圍是:
[head.X - distance(head,spineshoulder),head.X+distance(head,spaneshoulder)]
所以呢,關於X的映射就可以輕鬆的算出來啦。同理可得Y的映射公式。
使用這種方法,我們反而不需要傳遞一個很大的數組到shader,而是傳遞四個值就可以啦,也就是X,Y對應的映射函數的參數。
所以呢,在HeadColorView.cs裏面的Update函數做如下更改:

        computekeys[0] = (float)(keys[1]-keys[0])/1920;
        computekeys [1] = (float)keys [0] / 1920;
        computekeys [2] = (float)(keys [3] - keys [2]) / 1080;
        computekeys [3] = (float)keys [2] / 1080;
        if (computekeys [0] < 0 || computekeys [1] < 0 || computekeys [2] < 0 || computekeys [3] < 0) {
            computekeys [0] = 0f;
            computekeys [1] = 0f;
            computekeys [2] = 0f;
            computekeys [3] = 0f;
        }
    //  Debug.Log ("area is "+ computekeys[0]+","+computekeys[1]+","+computekeys[2]+","+computekeys[3]);
        keybuffer.SetData(computekeys);

這裏要注意的是,在Start裏面生命buffer的時候要設置成float類型,而且,對應的ShowHead.shader的參數也要是float類型。

HeadColorView.cs:
        keys = ColorSourceManager.GetComponent<HeadShowManager> ().GetKey();
        if (keys != null) {
            keybuffer = new ComputeBuffer (keys.Length, sizeof(float));
            gameObject.GetComponent<Renderer> ().material.SetBuffer ("key", keybuffer);
            //gameObject.GetComponent<Renderer> ().material.SetFloatArray("key",computekeys);
        }
ShowHead.shader:
    StructuredBuffer<float> key;

對於,只顯示頭部圖畫的話,我們只要實時的在shader的定點着色器裏面更改採樣區域就好啦~

ps_input vert (vs_input v)
{
    ps_input o;
    o.pos = mul (UNITY_MATRIX_MVP, v.pos);
    o.tex = v.tex;
    // Flip x texture coordinate to mimic mirror.
    o.tex.x = key[0]* (1-v.tex.x) + key[1];
    o.tex.y = key[2] * v.tex.y+ key[3];
    return o;
}

那麼,你就可以獲得,如下的效果啦:
這裏寫圖片描述
如果,你想兩個效果都實現的話,需要注意的是,我們只需要而且必須使用一個ColorManger,經過我的實驗,我發現,我發同時調用兩個_Sensor.ColorFrameSource.OpenReader(),所以,我們只要把兩個不同的功能合成一個ColorManager即可。
如果大家需要源碼和原工程玩玩的話,可以從這裏下載:
https://github.com/ArlexDu/UnityKinectMLTest

=======================================================
更新:
最近把我這些腳本轉移到一個項目裏面時候,發現原本展示頭部的紋理一直是空的,後來發現,原來在HeadColorView.cs的start函數執行的比HeadShowManager.cs的Start早,於是導致獲得的texture一直是空的。
所以這就涉及到一個unity鍾腳本執行順序的問題,這裏有一個文章很好的說明了這個問題:http://www.jb51.net/article/57309.htm
所以,我們需要自定義腳本的執行順徐,把我們的manager腳本放在其他腳本執行之前,就可以啦~
這裏寫圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章