這一篇,來大概講一下 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::DepthMetaData、xn::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」當作背景圖(此照片為新竹大隘、巴斯達隘的祭場)。
附註:
-
XnUserID 是用 typedef 來定義的,在 Win32 環境下,實際型別應該會是 unsigned int;而相較於此,XnLabel 則是 unsigned short。
-
雖然 XnUserID 和 xn::SceneMetaData 裡的 XnLabel 的型別並不相同,不過基本上這邊 XnLabel 的值應該就是對應到 XnUserID。
-
其中,程式裡的 m_OpenNI.m_ImageMD 是透過 image generator 所得到的 xn::ImageMetaData、m_OpenNI.m_SceneData 則就是 xn::SceneMetaData。
版主您好,拜讀您的文章後想請教您一個問題。當某個已被偵測的使用者離開畫面,此時UserGenerator並不會馬上將這位離開畫面的使用者視為Lost User,也就是說該使用者的UserID還是會被保留。若此時又有另一位使用者進入畫面,UserID將會持續往上累加。若這種情況發生非常頻繁,將會使得UserID很快到達10位的上限 (累加的速度高於User被重置的速度)。所以想請問版主是否有解決的方法?感謝版主的幫忙。
讚讚
就 Heresy 所知,這個問題應該沒辦法解。不過基本上, user id 增加應該不會造成什麼問題才對?
讚讚
版主您好,感謝您的回覆。
因為我目前是將Kinect運用在同時會有許多人出現在鏡頭前的場合下。因此,若是有User離開畫面,但UserGenerator並沒有馬上把該名User的User ID釋放掉,那麼就會導致新進入畫面的使用者無法被偵測到。因為根據測試,User ID似乎最大上限是10,超過10的時候就不會再增加了。
讚讚
這種狀況 Heresy 就沒碰過了…
讚讚
當兩個人離太近或靠在一起時 , 或者人物手中拿東西 , 發現似乎被判別成同一個user , 畫出骨架位置 , 發現位置搶來搶去 , 想請問大神有沒有什麼比較好的解決方案 ???
讚讚
這個算是 NITE 演算法上的先天限制,除非是自己想辦修改深度影像、然後再給 NITE 分析,不然大概就是只能自己換別的演算法來處理了。
讚讚