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

25 thoughts on “OpenNI 的 User Generator”

  1. heresy 你好,我刚接触kinect,想请教您一下,openNI有没有可以调整kinect摄像头俯仰角的函数(就是可以实现马达驱动摄像头上下动)?谢谢。我看到微软的SDK有这个函数可以做到nui.NuiCamera.ElevationAngle

  2. to heresy我在openNI的主?上看到有?中?件openni complaint hardware binaries,???不?有??功能呢???

  3. to hudsonopenni complaint hardware binaries 是 OpenNI 相容硬體的公版驅動程式,Kinect 無法使用。而也如同上之前的回答,OpenNI 並沒有提供控制鏡頭實體的方法。

  4. to heresy想請教一下,有沒有可以不用Kinect作為OpenNI的硬件做開發的,也就是用一般的CMOSsensor可以嗎?

  5. to levenOpenNI 基本上是要使用深度影像來做處理,而一般的 CMOS 感應器並沒有深度的資訊,所以是無法使用的。而目前 OpenNI 能使用的相容硬體,其實主要是 Asus 的 Xtion Pro。Microsoft Kinect 要使用其實還是需要靠第三方的驅動程式的。

  6. heresy,我是新手,最近刚开始研究openNI,我想问一下,如果我没有kinect设备,但是有kinect录制的深度视频和彩色视频,是否能够通过openNI得到骨架等信息呢?如何通过openNI直接调用现成的视频呢?

  7. 作者你好,最近我在开发kinect的过程中(使用OPENNI做手势识别的项目)发现他的骨架跟踪效果并不是,但是kinect又能很好的识别人身体的所有像素,最近我都在思考如何将这两者结合起来,能够非常准确的找到手的位置,不知道您有什么建议么?

  8. to 夜火流离
    抱歉,你的留言似乎有些缺字?不是很明確。
    不過,如果你是覺得 OpenNI 提供的骨架追蹤準確度不夠高的話,基本上除非自己根據深度影像來自幾實作人體骨架便是、追蹤的演算法,不然應該是沒辦法的。

  9. 弱弱的问一下,OPENNI支持KINECT摄像头的cropping功能吗?也就是裁剪功能,今天我试了怎么不太合适,显示的图像是变小了但是图像不合适。
    XnCropping pCropping;
    pCropping.bEnabled=true;
    pCropping.nXOffset=200;
    pCropping.nYOffset=200;
    pCropping.nXSize=320;
    pCropping.nYSize=240;
    m_image.GetCroppingCap().SetCropping(pCropping);

  10. to 我不认识我

    抱歉,Heresy 這邊也沒有使用 Cropping 的經驗,所以不確定。

  11. 请问可不可以实现在录制的同时实时显示图像?可不可以同时实现两台Kinect的同时录制?

  12. to Tifa
    可以同時顯示加錄製、只要在程式中加上 xn::Player 就好了。

    至於兩台同時錄製 Heresy 就沒試過了。

  13. 大神,你好,请教一个问题:
    OpenNI是如何根据深度数据来分析出场景中使用者信息的(比如;使用者个数、位置等等)

  14. to 00231
    這部分是由 middleware、一般是 PrimeSense 的 NITE 來實作的。而他並沒有公開他的演算法,所以一般人是沒辦法知道他的實作原理的。
    不過基本上,應該也都還是以 Computer Vision 的演算法出發的。

  15. 谢谢大神指导,不过我现在在纠结一个问题:用其他数据来取代Kinect的深度数据,我已经写进DepthMeteData里面了,但是用UserGenerator的时候他好像用的不是我给他的数据,还是用原先的数据,现在正在抓狂中,你能帮我解惑么?不胜感激.如果这里不方便的话,L121014@163.com,期待你的回复~!

  16. 求助Heresy大神,我想用两台Kinect在openni中分别显示他们的骨架结构,不知道这里应该怎么设置啊QQ

  17. to Richard
    目前 PrimeSense NITE 提供的 User Generator 有問題,沒有辦法在單一程式裡面正確地使用兩個 user generator。

  18. 冒昧提問實感抱歉也先感謝您統整出的KINCET資料

    OpenNI 的 User Generator 回應

    目的:找人體輪廓

    嘗試使用 OPENCV2.3.1 IplImage 撰寫

    發現實現出的IplImage 上方有切除的區塊 請問用Qt實現會遇到這問題?

    // get user map
    xn::SceneMetaData mUserMap;
    mUserGenerator.GetUserPixels( 0, mUserMap );
    IplImage* TRYImg=cvCreateImage(cvSize(640,480),IPL_DEPTH_8U,3);//建立 8BIT RGB 圖片
    cvSet(TRYImg,cv::Scalar(0,0,0));

    for(int x=0;xheight; x)
    {
    for(int y=0;ywidth; y)
    {
    int iLabel = mUserMap( y, x );

    if(iLabel==1)
    {
    ((uchar*)(TRYImg->imageData TRYImg->widthStep*x))[y*3 2]=255;
    ((uchar*)(TRYImg->imageData TRYImg->widthStep*x))[y*3 1]=0;
    ((uchar*)(TRYImg->imageData TRYImg->widthStep*x))[y*3]=0;
    }
    }
    }

    cvShowImage(“TRY”,TRYImg);

發佈回覆給「Ray」的留言 取消回覆

發佈留言必須填寫的電子郵件地址不會公開。