OpenNI 的 User Generator


這一篇,來大概講一下 OpenNI 裡面「User Generator」的用法。User generator 的型別是 xn::UserGenerator,在 OpenNI 裡,他是用來偵測場景內的使用者用的 node。透過 User Generator,不但可以偵測到有新的使用者出現、或是使用者離開的事件,同時也可以抓到目前畫面中使用者的數量、位置等等。

實際上,User generator 也可能提供了 Skeleton 和 Pose Detection 這兩個 Capability,可以做到更複雜的工作。之前在《透過 OpenNI / NITE 分析人體骨架》一文中所說明的人體骨架的分析與追蹤,基本上也都是透過 User generator 和他的 Skeleton 和 Pose Detection 這兩個 Capability 來做到的~

而這一篇,就暫時不管 skeleton 和 pose detection,把重點放在 user generator 之前沒提過的功能上吧~

首先,雖然不是本文重點,不過 User Generator 有提供三個 callback event,分別是「新的使用者」(New User)、「使用者消失」(User Exit)、「使用者重新進入」(User Re Enter);透過這三種主動通知的事件,可以拿來做為整個畫面的場景有變化時的主要程式執行的區段。除了人體骨架的分析與追蹤外,在其他許多場合也是很有用的。

而如果去除這三個主動式的事件的話,在程式執行時,也還可以透過 user generator 的函式,來取得一些其他的資料;這些函式包括了:

  • XnUInt16 GetNumberOfUsers()

    取得目前 user generator 所偵測到的使用者數目。

  • GetUsers( XnUserID, XnUInt16 )

    取得目前 user generator 偵測到的使用者的 User ID(XnUserID);所取得的 User ID 是用來識別個別使用者用的,接下來的兩個函式都需要指定 user ID 來取得指定使用者的進一步資料。

  • GetCoM( XnUserID, XnPoint3D& )

    取得指定使用者質心(center of mass)所在的位置。某些狀況下可以以此當作該使用者的主要參考位置。

  • GetUserPixels( XnUserID, SceneMetaData& )

    取得指定使用者的像素資料,如果 User ID 給 0 的話,代表是要取得所有使用者的像素資料。取得回來的資料型別會是 Xn::SceneMetaData,以目前的 NITE 所提供的版本來說,裡面的資料實際上是基於 depth generator 產生的的圖(2D Array),每一個像素的資料型別都是 XnLabel,代表該像素顯示的資料是哪個使用者的。

其中,GetNumberOfUsers()GetUsers() 的用法,在《透過 OpenNI / NITE 分析人體骨架》時就已經提過了,而 GetCoM() 的使用方法相當簡單,所以這邊基本上就簡單帶過:

XnUInt16 nUserNum = m_User.GetNumberOfUsers();
XnUserID* aUserID = new XnUserID[ nUserNum ];
XnStatus eRes = m_User.GetUsers( aUserID, nUserNum );
for( int i = 0; i < nUserNum; ++ i )
{
  cout << "User " << aUserID[i] << " @ ";
  XnPoint3D mPos;
  m_User.GetCoM( aUserID[i], mPos );
  cout << mPos.X << "/" << mPos.Y << "/" << mPos.Z << endl;
}
delete[] aUserID;

基本上,上面這個例子,會先透過 GetNumberOfUsers() 取得使用者的數量,並透過 GetUsers() 來取得目前所有使用者的 User ID。而在取得使用者的 User ID 後,接下來則是透過一個迴圈,依序根據使用者的 User ID,透過 GetCoM() 去取得搭的質心位置(Center of Mass)、並把取得的位置(mPos)輸出到 standard output。

而上面這段程式執行後,應該就會逐行地輸出目前使用者的質心所在位置了~


接下來,則是比較複雜一點的 GetUserPixels()。它的目的,是用來取得目前的畫面裡,指定的使用者所涵蓋的範圍,他需要指定一個 XnUserID 的變數、來指定是要針對哪個使用者作資料的取得,如果是給 0 的話,則會取得所有使用者的資料。而取得的資料則是經過封包、型別為 xn::SceneMetaData 的一張圖。他和 Depth Generator 以及 Image Generator 的 xn::DepthMetaDataxn::ImageMetaData 一樣,都是繼承自 xn::MapMetaData 的資料型別。

以 NITE 提供的 User Generator 來說,所輸出的 xn::SceneMetaData 資料,基本上就是根據 xn::DepthMetaData 做分析計算出來的~這張圖上的每一個點(像素、Pixel),都是一個型別為 XnLabel 的「標籤」,代表這個點是用來呈現哪個使用者的。

而如果把不同的 user 指定成不同的顏色、並畫出來的話,就會變成類似右邊的圖。其中,黑色就是背景,而紅、藍、綠三個色塊,則是代表 User generator 偵測到的不同使用者;在這邊應該也可以發現,並不一定是人才會 User generator 被當作「使用者」,這點是在使用 User generator 可能要注意的地方。

如果要做到這件事,用 Qt 的 QImage 來做的話,程式會像下面這樣子:

// build color table
QColor  aColorTable[4];
aColorTable[0] = QColor::fromRgb(   0,   0,   0 );
aColorTable[1] = QColor::fromRgb( 255,   0,   0 );
aColorTable[2] = QColor::fromRgb(   0, 255,   0 );
aColorTable[3] = QColor::fromRgb(   0,   0, 255 );

// get user map
xn::SceneMetaData mUserMap;
m_User.GetUserPixels( 0, mUserMap );

// Create image
QImage img( mUserMap.FullXRes(), mUserMap.FullYRes(), QImage::Format_ARGB32 );

// apply user color to image
int iLabel;
for( int y = 0; y < img.height(); ++ y )
{
  for( int x = 0; x < img.width(); ++ x )
  {
    iLabel = mUserMap( x, y );
    if( iLabel < 5 )
      img.setPixel( x, y, aColorTable[iLabel].rgba() );
  }
}

這邊的程式,是先建立一個對應不同使用者的色彩表(aColorTable),為了簡化程式,Heresy 這張表的大小只有四,也就是扣除背景外,只能對應到三個使用者。

而接下來第二步,則是透過 GetUserPixels() 來取得 xn::SceneMetaData;Heresy 這邊沒有指定 User ID,而是給 0 當作參數,所以得到的 mUserMap 裡,會包含所有使用者的資料。

接下來,則是要建立一張大小和 mUserMap 一樣大的圖,用來填上不同的顏色。這邊 Heresy 是使用 Qt 裡的 QImage 這個專門用來處理影像的資料型別,如果不是使用 Qt,而是使用其他的圖形環境、或是其他的影像處理套件,也就請改使用對應的資料類別了~

最後,就是依序去讀取 mUserMap 裡的每個像素、根據他的值(代表不同的使用者),來在建立好的 img 上填上不同的顏色了~由於 xn::SceneMetaData 有定義 operator(),所以可以簡單地把 x、y 的座標直傳進去,就可以取得該點的值了;而如果不想這樣做的話,也可以使用他的 Data() 函式,直接取得 XnLabel 的指標做進一步的處理。

當程式執行完這段程式碼後,如果再把 img 這個 QImage 的圖片畫出來,就會得到類似右上方那樣的結果了~而如果想要指定不同的顏色、或更多的顏色,也只要再把上面的程式再稍微調整一下就可以了~


這有什麼用呢?基本上,透過 user generator 的這些資料,可以快速地取得使用者所在畫面中的位置、所佔的像素,進一步的,就是可以只取得這些資料了~

比如說,如果把這邊取得的 xn::SceneMetaData 影像資料當作遮罩(mask)、來對 image generator 取得的資料進行裁切的話,就可以取得只有人的影像了~

而如果再把這張只有人的圖和其他照片、圖片放在一起的話,就可以當作一個合成照片、替換背景的功能了。像右邊的圖,基本上就是以這樣的方法做出來的合成圖~再加上 Kinect 是動態擷取畫面進行處理的,所以這個程式也就可以動態處理這些資料,把人的動作、放到指定的背景塗上了~

這部分 Heresy 的程式大致如下:

QImage img( 640, 480, QImage::Format_ARGB32 );
const XnRGB24Pixel* pImgMap = m_OpenNI.m_ImageMD.RGB24Data();
for( int y = 0; y < img.height(); ++ y )
{
  for( int x = 0; x < img.width(); ++ x )
  {
    if( m_OpenNI.m_SceneData( x, y ) == 0 )
    {
      img.setPixel( x, y, QColor::fromRgb( 0, 0, 0, 0 ).rgba() );
    }
    else
    {
      const XnRGB24Pixel& pVal = pImgMap[ y * img.width() + x ];
      img.setPixel( x, y, 
                QColor::fromRgb(pVal.nRed,pVal.nGreen,pVal.nBlue,255).rgba() );
    }
  }
}

這邊基本的概念,其實和前面一段程式相同,只是這邊填入的顏色不是查表查到的顏色,而是直接使用 image generator 所讀到的顏色了(註三)~而如此建立完影像後,在和背景做合成,就可以完成類似右上方的合成圖了~

實際上,這邊的程式可以簡單地透過修改之前《使用 Qt GraphicsView 顯示 OpenNI 影像資料》的程式碼來完成,Heresy 就不多提了,完整的範例程式,請到 Heresy 的 SkyDrive 上下載;這次有附上編譯好的 Win32 程式,以及必要的 Qt DLL 檔,所以在有安裝好 OpenNI / NITE 的環境下,應該只要有安裝 Visual C++ 2010 可轉散發套件,就可以執行了。

QTKinect.exe 這個程式執行時,可以加上一個參數、指定要當作背景圖的影像檔(支援 PNG、BMP、JPEG),例如執行 QtKinect abc.jpg 的話,程式就會去讀取 abc.jpg 這個檔案,把它當作背景圖;另外,也可以直接把圖檔拖到 QTKinect.exe 的圖示上,透過這個方法來開啟程式,也可以讓他去讀取這張圖檔,來當作背景圖。而如果沒有指定的話,就會使用執行目錄下的「background.jpg」當作背景圖(此照片為新竹大隘、巴斯達隘的祭場)。


附註:

  1. XnUserID 是用 typedef 來定義的,在 Win32 環境下,實際型別應該會是 unsigned int;而相較於此,XnLabel 則是 unsigned short

  2. 雖然 XnUserIDxn::SceneMetaData 裡的 XnLabel 的型別並不相同,不過基本上這邊 XnLabel 的值應該就是對應到 XnUserID

  3. 其中,程式裡的 m_OpenNI.m_ImageMD 是透過 image generator 所得到的 xn::ImageMetaDatam_OpenNI.m_SceneData 則就是 xn::SceneMetaData


OpenNI / Kinect 相關文章目錄
Nokia Qt 相關文章目錄

對「OpenNI 的 User Generator」的想法

  1. HI~Heresy
    請問您有沒有試過,產生兩個User Generator來產生骨架!?
    我宣告兩個User Generator,一個試吃MockGenerator的資料來產生骨架,一個試吃原始Depth的資 料,但這樣子好像會造成再waitandupdatall 其中有一個User generator會當掉,

      • 謝謝您的回覆,

        想再請問您一個問題,如果使用了mockgenerator,這樣子 user-generator就只會去吃mockgenerator的資料嘛!?

        有辦法再去讀取depth generator的資料呢!?

        • 不行。
          在架構上,應該是要針對兩個 Depth Generator 各自建立 User Generator;不過很遺憾,目前做不到。
          如果你是有的時候要切換的話,那只要控制 mock generator 的資料是否要經過修改應該就好了?

          • 嗯嗯 我剛剛就在測試控制mock generator的資料!!

            希望NITE新增這個功能 ~"~

  2. Hi,Heresy
    不知道能否請問,在利用使用openni獲得depth data時,有可能在將資料送到middleware進行skeleton判別前,先將得到的depth data先進行前處理嗎?例如:把所有得到的depth資料都先減掉一背景資料(depth data),或是先進行資料濾波之類的,再進一步進行skeleton判別嗎?

    謝謝你的分享

    • Heresy 沒有很認真去研究過這個問題。
      不過以 OpenNI 的架構來說,要做到這件事,可能是需要去自己寫一個 Depth Generator,然後在內不去讀取本來的 Depth Generator 的資料、並進行必要的處理;最後再讓 User Genenerator 來讀取這個經過修改過的 dpeth generator,以此進行分析。

      不過這也只是 Heresy 的想法,不確定實作上是否可以實現。

      • 今天稍微玩了一下,看來應該可以不用自己寫一個 Depth Generator,而可以透過 mockDepthGenerator 來做到。
        等下禮拜有時間,應該會把這部分的東西整理一下分享出來。

        • Hi,Heresy
          我參考範例程式NiRecordSynthetic,試過使用mockDepthGenerator,把先前錄過的.oni檔,預處理過後再轉存一次,才能重新分析skeleton,直接修改就不知道要從何下手了
          謝謝也期待你的分享

          • 其實基本上使用是一樣的。
            只是在建立 User Generator 的時候,要透過 xn::Query 來作條件的限制,讓他去使用 mockDepthGenerator 而已。

  3. 請問Heresy

    對於UserGenerator的GetUserPixels
    我如果只有取得Depath的資料 這樣GetUserPixels可以讀取
    但是如果Depath 與 RGB 一起讀取的話
    GetUserPixels取出來的資料就很奇怪
    不知道Heresy有沒有遇過這問題?

      • Heresy 自己沒遇過這個問題。
        不過,不知道你的取不到是什麼意思?有檢查過 GetUserPixels() 回傳的狀態嗎?

        • 不好意思
          我已經發現我錯在那了
          不小心再讀取彩色影像時 把user的指標跑完一遍了
          照程再讀取景深資料時 造成 overflow

  4. 大大請問一下,目前openNI有偵測手指的節點嗎(或者微軟的SDK有)?
    另外,可以把深度換算成現實離kinect幾公分的換算嗎 @@?
    麻煩了,謝謝

    • 1. 沒有。目前 OpenNI 或 Kinect for Windows SDK 都沒有針對手指去做偵測。如果有需要,必須自己去寫。

      2. 沒弄錯的話,透過 ConvertProjectiveToRealWorld() 轉換出來的單位就是毫米(millimeter)了。

  5. Heresy大大你好~我使用之前向你請教的SceneMetaData.operator()去讀取偵測到人的部分X.Y座標,
    我是這樣寫的
    g_UserGenerator.GetUserPixels(0,mUserMap);
    for(usermapy=1;usermapy<240;usermapy++) <===kinect的深度圖解析度是320×240所以我這樣帶
    {
    for(usermapx=1;usermapx<320;usermapx++)
    {
    f=mUserMap.operator()(usermapx,usermapy);
    if(f==1) <=========1的部分是人體部分
    {
    //<===中間演算法我省去
    }

    • 我已經算出在這張深度圖我想要的新點位,我想請教大大的是…我有辦法把新點位轉成或者取代OPENNI裡的關節點嗎??

      • 看不懂你的意思?
        基本上,你要使用 OpenNI 計算出來的關節位置,也是要把它讀取出來才能用。
        如果你是要修改某個關節點的資料,那不是直接跳過 OpenNI、然後用自己計算出的關節點位就好了嗎?

        • …抱歉敘述的不是很好,我透過上面的方法取得X.Y值,我能夠透過什麼方法將…例如"XN_SKEL_NECK",這個點座標轉成我取得的X.Y值,可以直接寫等式例如DramLimb裡面joint1.position.x=新座標的.X,我這樣的方法可以嗎?

          • 為何不行呢?

            唯一要注意的是,透過 GetSkeletonJoint() 取得的座標使屬於 real world 座標系統的,而你這樣計算出來的 x, y 以及透過 depth map generaor 取得的 z、是屬於投影座標系統的座標。
            如果要轉換成 real world 座標系統,要另外透過 depth map generator 所提供的 GetSkeletonJoint() 這個函式來做轉換。

          • 大大不好意思…再隔了那麼久再請教你 ,GetSkeletonJoint()是SkeletonCapability裡的嗎?還是透過ConvertProjectiveToRealWorld轉換成realword座標?還有…從上述的方法取得的X.Y值是從DepthGenerator取得Z值?還是從其他地方取得

          • 那投影座標系統的座標是透過SkeletonCapability.GetSkeletonJoint() 來轉換成real world 座標系統?
            還是ConvertProjectiveToRealWorld?

    • call operator 可以直接用,不必這樣寫
      mUserMap.operator()(usermapx,usermapy);
      一般是直接寫成
      mUserMap(usermapx,usermapy);

  6. Heresy大大你好~我想請教你,我使用MetaData掃過整張影像,將人的部分歸為"1″背景部分歸為"0″做區分,然後我將整張影像人的部分自訂座標最左上方一點作為(1,1)座標下一點為(1,2)依此類推下一行作為(2,1)、(2,2)……..,我要在我自訂的座標裡,有辦法對應回去openni的座標嗎?該如何處理…orz?

    • 不知道你到底打算怎樣轉換座標系統。
      不過如果你已經知道兩張圖之間的關係的話,應該就可以推算出來座標轉換的公式吧?
      之後再透過這個公式來做座標系統的轉換不就可以了嗎?

      一般簡單的是只有 shift,如果這樣的情況,只要找到 X 軸的位移量、和 Y 軸的位移量,不就可以自己做座標轉換了嗎?

      • 我沒辦法使用相對位移計算出相對座標…..~我是想問大大,據大大的了解有辦法做這樣的資料轉換嗎?

        • 相對位移只是一個舉例,實際上是看你的座標到底是怎麼對應的。

          另外,請問一下,你有影像處理或電腦視覺的相關經驗嗎?
          如果沒有,建議你最好先去大概了解一下影像處理的基礎,再來寫這邊的程式,這樣才會比較有概念。

  7. 我想請教大大~GetUserPixels這個印出來的值有辦法是使用者每點的X.Y.Z值嗎?
    我測試了好久都沒辦法得到使用者部分各點的X.Y.Z值。

    • GetUserPixels() 取得的資料只是使用者代號的標籤而已,你要取得點位的資料,請搭配透過 Depth Generator 取得的深度突來合併使用。
      本文最後的範例,就是搭配 Image Generator 的 image map 來做使用,基本上概念是一樣的。

      • 我試了還是試不出來….那假如我只要使用者部分的每點X.Y值,我使用
        m_User.GetUserPixels( 0, mUserMap );後輸出mUserMap.FullXRes()….使用者的X值還是讀取不了,大大有範例可以說明參考嗎?

        • 不太確定你的問題在哪裡?
          在這篇文章裡已經說明了,基本上,透過 GetUserPixels() 取得的 xn::SceneMetaData,算是包含 metadata 的一張圖,而 FullXRes() 只是代表這張圖 X 軸的大小,並非座標值。

          在 ( x, y ) 這點的 label 值,則可以透過他的 operator() 來做存取(也就是「iLabel = mUserMap( x, y );」);而在得到 ( x, y ) 點的 label 值後,就可以判斷這個點是哪個 user 了。
          如果是自己要的 user,就再去查詢 depth map 裡的 ( x, y ) 座標的值,不就可以取得他的空間點位資訊了嗎?

          • 我最終的目的是想要將使用者每個Pixels 的(X.Y.Z)值印出來,我使用GetUserPixels() 取得使用者的metadata後,但我如何將使用者每個Pixels的記錄下來(當然我知道如何寫入Text檔,我不曉得如何讀取使用者部分每點的X.Y.Z)?

          • 已經說過了,請參考本文最後的範例,去掃過整張 xn::SceneMetaData。
            同時把 ( x, y ) 的座標,再去讀取 depth map 的值,就可以取得該點的深度了。
            基本上,只要把本文最後的範例裡的 m_OpenNI.m_ImageMD 改成讀出的 xn::DepthMetaData,再把型別、資料處理的部分作對應的修改,就可以了。

            不過如果 xn::DepthMetaData 內的深度值是 Projective 座標系統的,你如果是要真實世界座標系統的點位,請使用 depth generator 的 ConvertProjectiveToRealWorld() 來做轉換(請參考《透過 OpenNI 建立 Kinect 3D Point Cloud》)。

  8. Heresy 我在實驗過程中發現一個BUG,就是在設定了 m_DepthGenerator.SetViewPort( ) 為彩色圖的視口之後,玩家走出場景 和重新進入場景的 回調函數 不起作用了,如果有機會接觸到primesense公司的員工時候,希望反映一下這個問題

  9. 對~目前想知道說他裏頭的演算法是如何寫的> <
    既然沒有來源那我就另尋辦法哩~感謝Heresy喔^ ^

  10. 哈囉~Heresy
    不好意思問你一下喔
    請問有辦法可以知道他的 skeleton 和 pose detection 的函式是怎麼做出來的嗎?
    我目前有自己compile好他給的source
    可是我用VS2010按F12一直找下去
    越看越深……還是無法理解> <
    請問Heresy對這方面有沒有研究過呢?

    • 抱歉,不太確定你的問題?你是想知道 Skeleton 和 Pose Detection 內部的演算法實作嗎?
      這部分實際上是屬於 NITE 的部分,PrimeSense 並沒有把這部分 Open Source、只有釋出編譯好的 binary 檔,所以是沒辦法直接看到它是怎麼實作的。

發表迴響

在下方填入你的資料或按右方圖示以社群網站登入:

WordPress.com 標誌

您的留言將使用 WordPress.com 帳號。 登出 /  變更 )

Google photo

您的留言將使用 Google 帳號。 登出 /  變更 )

Twitter picture

您的留言將使用 Twitter 帳號。 登出 /  變更 )

Facebook照片

您的留言將使用 Facebook 帳號。 登出 /  變更 )

連結到 %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.