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. 版主您好,拜讀您的文章後想請教您一個問題。當某個已被偵測的使用者離開畫面,此時UserGenerator並不會馬上將這位離開畫面的使用者視為Lost User,也就是說該使用者的UserID還是會被保留。若此時又有另一位使用者進入畫面,UserID將會持續往上累加。若這種情況發生非常頻繁,將會使得UserID很快到達10位的上限 (累加的速度高於User被重置的速度)。所以想請問版主是否有解決的方法?感謝版主的幫忙。

      • 版主您好,感謝您的回覆。
        因為我目前是將Kinect運用在同時會有許多人出現在鏡頭前的場合下。因此,若是有User離開畫面,但UserGenerator並沒有馬上把該名User的User ID釋放掉,那麼就會導致新進入畫面的使用者無法被偵測到。因為根據測試,User ID似乎最大上限是10,超過10的時候就不會再增加了。

  2. 當兩個人離太近或靠在一起時 , 或者人物手中拿東西 , 發現似乎被判別成同一個user , 畫出骨架位置 , 發現位置搶來搶去 , 想請問大神有沒有什麼比較好的解決方案 ???

    • 這個算是 NITE 演算法上的先天限制,除非是自己想辦修改深度影像、然後再給 NITE 分析,不然大概就是只能自己換別的演算法來處理了。

發表迴響

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

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.