最近,完成了基於機器學習的Kinect姿勢識別的功能。但是,光做一做相應的姿勢,收集到識別的數據,總感覺差強人意,少了點什麼東西。而且有時候會因爲距離kinect的距離遠近發生誤判的情況,Scene界面什麼也沒有,只好憑感覺來調整位置。還有就是,面對每秒上百條數據結果,還真是看不過來。於是,我就萌生了一個想法,能不能把收集到的數據展示成爲一個類似於實時監控波形圖,這樣的話,我就能夠在Unity上更方便的處理姿勢檢測的數據啦。
好啦,我們先看一下效果圖~
嗯,接下來,我們來講解一下具體的製作過程。
首先是,這個波形圖的建立,我們這裏使用動態紋理的方式生成。
創建一個新的腳本,我這裏命名爲HistogramTexture。
哦,對了,在編寫本腳本之前,請先在您的項目中增加LMNRY/SetProperty(https://github.com/LMNRY/SetProperty/tree/master/Scripts)這個插件,他可以幫助我們可以在編輯模式下調試的時候,隨意概念腳本的屬性並顯示。
下面是主要程序紋理的代碼。
我們可以在類名前面加一個標誌[ExecuteInEditMode],這表明我們的代碼是可以在編輯模式下使用的。編輯模式下的update函數不會一直調用的,而是當scene界面發生變化的時候纔會執行。
切記,在本例中,這個模式我們可以當做最初畫程序紋理調試用,當真正運行程序的時候,要把[ExecuteInEditMode]註釋掉。
public Material material = null;
public bool iscanvas = false;
private Texture2D m_generatedTexture = null;
private int base_width;
private Queue heights = new Queue();
private float height = 0f;
#region Material properties
[SerializeField,SetProperty("textureWidth")]
private int m_textureWidth = 100;
public int textureWidth{
get{
return m_textureWidth;
}
set{
m_textureWidth = value;
_UpdateMaterial ();
}
}
[SerializeField,SetProperty("rectangleColor")]
private Color m_rectangaleColor = Color.yellow;
public Color rectangleColor{
get{
return m_rectangaleColor;
}
set{
m_rectangaleColor = value;
_UpdateMaterial ();
}
}
[SerializeField,SetProperty("Count")]
private int m_count = 30;
public int Count{
get{
return m_count;
}
set{
m_count = value;
_UpdateMaterial ();
}
}
#endregion
這裏主要就是聲明一下腳本需要的變量。其中的SerializeField就是表明這個私有變量可以被暴露到inspector上面。那個SetProperty就是我剛纔要大家去下載的小插件提供的標誌符。
void Start () {
if (iscanvas) {
} else {
if (material == null) {
Renderer renderer = gameObject.GetComponent<Renderer> ();
if (renderer == null) {
Debug.LogWarning ("Cannot find a renderer.");
return;
}
material = renderer.sharedMaterial;
}
}
_UpdateMaterial ();
}
這段代碼就是一些判斷,是否這個程序紋理是用在一個3維的物體上呢,還是花在Unity 的UI Canvas上面,還有就是判斷如果是3D物體的話,那麼這個物體對應的material有沒有被提供。
// Update is called once per frame
void _UpdateMaterial () {
if (material != null||iscanvas) {
base_width = (int)Mathf.Ceil(textureWidth / (float)m_count);
// Debug.Log ("base_with is "+base_width);
heights.Clear ();
for (int i = 0; i < m_count; i++) {
heights.Enqueue (1);
}
m_generatedTexture = _GenerateproduceTexture ();
if (iscanvas) {
GameObject.Find ("Diagram").GetComponent<RawImage> ().texture = m_generatedTexture;
} else {
material.SetTexture ("_MainTex", m_generatedTexture);
}
}
}
這裏呢,就是調用構造紋理的函數,並且做一些基本參數的賦值,我這裏是決定把我的波形圖的寬度分成30個單位。當然,你也可以設置成爲其他的份數。我這裏選擇一個隊列作爲存儲每次動作檢測的結果,因爲隊列是先進先出嘛,所以,這樣我們畫出來的波形圖有一種向前推移的視覺效果。因爲C#提供的Queue類沒有給出一個初始化的值,所以,我這裏就先向隊列裏面塞了30個數值,就相當於初始化了。還有需要注意的是,因爲波形圖的每一條的寬度是我們根據圖片的大小以及份數計算生成的,所以,這個數值不可避免的可能是一個分數,在這裏,我需要給他做向上取整,爲什麼是向上取整呢?我們看一下下面的代碼。
private Texture2D _GenerateproduceTexture(){
Texture2D proceduralTexture = new Texture2D (textureWidth,textureWidth);
//draw histogram
object[] hs = heights.ToArray();
for (int w = 0; w < textureWidth; w++) {
int it = (int)Mathf.Floor(w / base_width);
int distogram = int.Parse (hs [it].ToString ());
// Debug.Log ("distogram is "+distogram);
for (int h = 0; h < distogram; h++) {
Color pixel = m_rectangaleColor;
proceduralTexture.SetPixel (w,h,pixel);
}
}
proceduralTexture.Apply ();
return proceduralTexture;
}
本段代碼主要是根據隊列提供的檢測結果來畫出我們的波形圖,就是遍歷所有的貼圖像素點來賦值,即針對當前位置的橫座標畫出這個橫座標對應的縱座標是多少,而這個縱座標就是存儲在我們的隊列之中的。我們的任務就是給定一個橫座標的位置點,我們可以計算出這一條的高度是多少。我的計算方法是這個樣的int it = (int)Mathf.Floor(w / base_width);int distogram = int.Parse (hs [it].ToString ());所以呢,如果base_width向下取整的話,會導致it>份數,數組越界的。
public void setHeight(float value){
maxScore = value > maxScore ? value:maxScore;
height = (int)(value*100);
heights.Dequeue ();
heights.Enqueue (height);
m_generatedTexture = _GenerateproduceTexture ();
Debug.Log ("maxScore is "+maxScore);
if (iscanvas) {
GameObject.Find ("Diagram").GetComponent<RawImage> ().texture = m_generatedTexture;
} else {
material.SetTexture ("_MainTex", m_generatedTexture);
}
height = 0f;
}
這裏就是處理來自檢測器的數據,完成繪圖的操作。
關於如何設置檢測器,可以參考我的另外一篇博文:《 Unity5 利用Kinect Studio 和Gesture Builder建立自定義姿勢分類器》(http://blog.csdn.net/nijiayy/article/details/68926979)
如果你選擇在一個plane上面顯示波形的話,切記要你的plane面相直射光,因爲我們的material 知識默認的shader,所以不會自發光,如果你的plane背對光源的話,那麼無論怎麼調整plane都是一片黑…
如果選擇Canvas的顯示的話,要選擇Raw Image 物體放在Canvas下面。
至此,這個波形圖就算是完成了,下面我們來顯示一下Kinect檢測到我們骨骼的位置。
首先,我們的層級結構是這樣的:
我們需要創建一個新的Layer 叫做“KinectBody”
Canvas :包含Diagram和BodyView兩個RawImage類型的物體
BodyManager: 空物體,包含函數BodySourceMnaager(Kinect 庫函數)
GestureDetector:空物體,包含函數GestureSourceManager和GestureController(具體可見我上文提過的博文)
CreateDiagram:空物體,包含HistogramTexture函數
BodyView:空物體,包含BodyOnCanvas函數(基於BodySourceView改動)
DetecBodyCamera:用於專門檢測Kinect 骨骼的攝像機,只看見“KinectBody”層,深度要低於主攝像頭。
BackGround:相當於骨骼圖的背景,KinectBody 層
我們需要根據BodySourceView這個kinect的類庫做一個小小的改動,就是移動他的位置,默認的這個BodySourceView就把我們的Kinect 骨骼放在了世界座標(0,0,0)的位置,所以呢,有時候我們需要移動它。而且,需要把所有創建的骨骼全部放在“KinectBody”層。避免影響主攝像頭。
在BodyOnCanvas中,把DetecBodyCamera作爲提供顯示的位置。
創建一個Render Texture,然後把它拖入DetecBodyCamera如圖所示:
這樣,這個diagram材質的內容就是DetecBodyCamera所採集到的場景了,然後把它拖入BodyView的Raw Image 組件的Texture參數即可。
調整一下攝像機位置,滿意就好~
既然HistogramTexture腳本其實是多用的,既可以用於渲染Canvas 的UI又可以改變3維世界中Plane的material,我們雖然提供了一個iscanvas參數來判斷,但是,對於使用canvas顯示的話,顯然,我們不需要給腳本提供一個material,但是目前這個腳本的inspector無論如何都是public以及需要序列化的參數全部顯示的:
所以,來想想可以不可以當iscanvas勾選的時候,material參數不暴露出來,而當iscanvas沒有勾選的時候,material就暴露出來了,就向這樣:
這樣也避免了使用腳本時候的誤判~
既然大家都說Unity是一個可以自編輯的編輯器,那麼我們就來說說如何b編輯Inspector的顯示吧。
既然,加入SetProperty 這個插件的時候,已經有了Editor這個文件夾,(這個控制編輯器的腳本沒必要一定放在Editor文件夾下面,但是爲了文件結構的清晰,我建議大家放在Editor文件夾下面)那麼我們就建立在這個文件夾下面建立一個HistogramTexture對應的自定義的設置面板。
using UnityEditor;
using UnityEngine;
using System;
[CustomEditor(typeof(HistogramTexture))]
public class HistogramTextureEditor : Editor {
override public void OnInspectorGUI(){
var myscript = target as HistogramTexture;
myscript.iscanvas = EditorGUILayout.Toggle ("Is Canvas",myscript.iscanvas);
if (!myscript.iscanvas) {
myscript.material = (Material)EditorGUILayout.ObjectField("material",myscript.material,typeof(Material));
}
myscript.textureWidth = EditorGUILayout.IntField ("Texture Width",myscript.textureWidth);
myscript.rectangleColor = EditorGUILayout.ColorField ("Rectangle Color",myscript.rectangleColor);
myscript.Count = EditorGUILayout.IntField ("Count",myscript.Count);
}
}
注意這個腳本的命名一定是”腳本名”+Editor,這個腳本名就是你打算要自定義Inspector面板的那個腳本。
這個腳本還是比較簡單的,就說一下關於material的對象,因爲你發現對於float類型的參數,int類型的參數,都有對應的field,那麼material ,texture,gameobject這些該怎麼辦呢?答案是quan’bu’y’og全部用全部用全部用全部用全部用全部用全部用全部用全部用ObjectFiled,然後在裏面加一個類型的Options,例如我這裏需要填寫進去一個material,那麼就這麼寫:
myscript.material = (Material)EditorGUILayout.ObjectField(“material”,myscript.material,typeof(Material));
好啦,這樣你的函數的Inspector面板也顯得很有趣啦~
如果對這個小例子感興趣的話,可以下載玩一玩~
https://github.com/ArlexDu/UnityKinectMLTest