//****************Edit 02/08/2015*******************************
在KinectSDK2.0發佈後,好多人反應找不到KinectSDK 1.8的下載頁面,現將鏈接粘在此處
KinectSDK-v1.8-Setup.exe KinectDeveloperToolkit-v1.8-Setup.exe
Kinect MSDN Blog——關於Kinect最新官方新聞
http://blogs.msdn.com/b/kinectforwindows/
//******************************************************************
Kinect SDK 讀取彩色、深度、骨骼信息並用OpenCV顯示
一、原理說明
對於原理相信大家都明白大致的情況,因此,在此只說比較特別的部分。
1.1 深度流數據:
深度數據流所提供的圖像幀中,每一個像素點代表的是在深度感應器的視野中,該特定的(x, y)座標處物體到離攝像頭平面最近的物體到該平面的距離,
注意是平面到平面距離,並不是到攝像機的斜線距離(以毫米爲單位)。
Kinect中深度值最大爲4096mm,0值通常表示深度值不能確定,一般應該將0值過濾掉。微軟建議在開發中使用1220mm~3810mm範圍內的值。
在進行其他深度圖像處理之前,應該使用閾值方法過濾深度數據至1220mm-3810mm這一範圍內。
下圖顯示了Kinect Sensor的感知範圍,其中的default range對Xbox 360和Kinect for Windows都適用,而near range僅對後者適用:
深度數據的存儲:
Kinect的深度圖像數據含有兩種格式,兩種格式都是用兩個字節來保存一個像素的深度值,而兩方式的差別在於:
(1)唯一表示深度值:那麼像素的低12位表示一個深度值,高4位未使用;
(2)既表示深度值又含有遊戲者ID:Kinect SDK具有分析深度數據和探測人體或者遊戲者輪廓的功能,它一次能夠識別多達6個遊戲者。SDK爲每一個追蹤到的遊戲者編號作爲索引。
而這個方式中,像素值的高13位保存了深度值,低三位保存用戶序號,7 (0000 0111)這個位掩碼能夠幫助我們從深度數據中獲取到遊戲者索引值。對於這種情況的處理如下:
USHORTrealDepth = (depthID & 0xfff8) >> 3; //提取距離信息,高13位
USHORTplayer = depthID & 0x07 ; //提取ID信息,低3位
SDK提供了專門的函數:
SHORT realDepth= NuiDepthPixelToDepth(depth);
USHORT playerIndex = NuiDepthPixelToPlayerIndex(depth);//獲得深度圖該點像素位置對應的 UserID;
在對OpenCV進行賦值時需要將其轉化到[0,255]
BYTE b = 255 - static_cast<BYTE>(256 * realDepth / 0x0fff);
1.2 Skeleton信息
玩家的各關節點位置用(x, y, z)座標表示。與深度圖像空間座標不同的是,這些座標單位是米。座標軸x,y, z是深度感應器實體的空間x, y, z座標軸。這個座標系是右手螺旋的,Kinect感應器處於原點上,z座標軸則與Kinect感應的朝向一致。y軸正半軸向上延伸,x軸正半軸(從Kinect感應器的視角來看)向左延伸,如下圖所示。
Kinect放置的位置會影響生成的圖像。例如,Kinect可能被放置在非水平的表面上或者有可能在垂直方向上進行了旋轉調整來優化視野範圍。在這種情況下,y軸就往往不是相對地面垂直的,或者不與重力方向平行。最終得到的圖像中,儘管人筆直地站立,在圖像中也會顯示出事傾斜的。
Kinect最多可以跟蹤兩個骨骼,可以最多檢測六個人。站立模式可以跟蹤20個關節點,坐姿模式的話,可以跟蹤10個關節點。
NUI骨骼跟蹤分主動和被動兩種模式,提供最多兩副完整的骨骼跟蹤數據。主動模式下需要調用相關幀讀取函數獲得用戶骨骼數據,而被動模式下還支持額外最多四人的骨骼跟蹤,但是在該模式下僅包含了用戶的位置信息,不包括詳細的骨骼數據。也就是說,假如Kinect面前站着六個人,Kinect能告訴你這六個人具體站在什麼位置,但只能提供其中兩個人的關節點的數據(這兩個人屬於主動模式),也就是他們的手啊,頭啊等等的位置都能告訴你,而其他的人,Kinect只能提供位置信息,也就是你站在哪,Kinect告訴你,但是你的手啊,頭啊等具體在什麼位置,它就沒法告訴你了(這四個人屬於被動模式)。
對於所有獲取的骨骼數據,其至少包含以下信息:
1)、相關骨骼的跟蹤狀態,被動模式時僅包括位置數據(用戶所在位置),主動模式包括完整的骨骼數據(用戶20個關節點的空間位置信息)。
2)、唯一的骨骼跟蹤ID,用於分配給視野中的每個用戶(和之前說的深度數據中的ID是一個東西,用以區分現在這個骨骼數據是哪個用戶的)。
3)、用戶質心位置,該值僅在被動模式下可用(就是標示用戶所在位置的,當無法檢測出Skeleton數據時,就應該顯示用戶質心位置,因爲質心是根據深度圖的UserID計算出來的,並不受骨骼信息影響)。
二、流程解析
作爲一名KINECT程序員,你需要記得的是,微軟SDK中提供的運行環境在處理KINECT傳輸數據時,是遵循一條3步驟的運行管線的。
第一階段:只處理彩色和深度數據
第二階段:處理用戶索引並根據用戶索引將顏色信息追加到深度圖中。(參考 NUI_INITIALIZE_FLAG_USES_DEPTH_AND_PLAYER_INDEX)
第三階段:處理骨骼追蹤數據
1. 基本設置
1.1 在vs2010項目中,需要設置C++目錄
包含目錄中加入 $(KINECTSDK10_DIR)\inc;
庫目錄中加入 $(KINECTSDK10_DIR)\lib\x86
(注意安裝Kinect SDK以後,路徑不一樣了,環境變量名變成KINECTSDK10_DIR)
MSRKINECTSDK是環境變量,正確安裝MS KINECT FRO WINDOWS SDK 後,會在計算機中的環境變量中看到。1.2 添加特定庫
除了指定目錄外,你還需要在鏈接器中設置附加依賴項,填入KinectNUI.lib
1.3 頭文件
爲了使用NUI中的API,首先我們要包含 NuiApi.h,切記,在這之前,要保證你已經包含了windows.h
#include <Windows.h>
#include "NuiApi.h"
切記,在這之前,要保證你已經包含了windows.h否則 Nuiapi中很多根據windows平臺定義的數據類型及宏都不生效。1.4 其他設置
如果要使用Kinect SDK 1.7+中的Inteaction部分控件,除了以上提到的基本設置需要額外加入Kinect toolkit文件夾下的相應的.h、.lib以及dll文件。
PS:
其實最簡單的方法就是把Kinect Toolkits中的類似sample Install到目錄,一看就知道怎麼做的了、
2. Nui初始化
接下來,任何想使用微軟提供的API來操作KINECT,都必須在所有操作之前,調用NUI的初始化函數
HRESULT NuiInitialize(DWORD dwFlags);DWORD dwFlags參數:
NUI_INITIALIZE_FLAG_USES_DEPTH_AND_PLAYER_INDEX 使用NUI中的帶用戶信息的深度圖數據
NUI_INITIALIZE_FLAG_USES_COLOR 使用NUI中的彩色圖數據
NUI_INITIALIZE_FLAG_USES_SKELETON 使用NUI中的骨骼追蹤數據
NUI_INITIALIZE_FLAG_USES_DEPTH 僅僅使用深度圖數據(如果你自己有良好的場景分析或物體識別算法,那麼你應該用這個)
以上4個標誌位,你可以使用一個,也可以用 | 操作符將它們組合在一起。例如:
HRESULT hr = NuiInitialize(NUI_INITIALIZE_FLAG_USES_COLOR);//只使用彩色圖
HRESULT hr = NuiInitialize(NUI_INITIALIZE_FLAG_USES_DEPTH_AND_PLAYER_INDEX | NUI_INITIALIZE_FLAG_USES_SKELETON | NUI_INITIALIZE_FLAG_USES_COLOR);//使用帶用戶信息的深度圖/使用用戶骨骼框架/使用彩色圖
NuiInitialize就是應用程序用通過傳遞給dwFlags參數具體值,來初始化Kinect SDK處理管線中必須的階段。因此,我們總是先在標誌位中指定圖像類型,纔可以在接下來的環節中去調用NuiImageStreamOpen之類的函數。如果你初始化的時候沒指定NUI_INITIALIZE_FLAG_USES_COLOR,那你以後就別指望NuiImageStreamOpen能打開彩色數據了,它肯定會調用失敗,因爲沒初始化嘛。
初始化以後,在我們繼續其他深入獲取NUI設備的數據之前,先了解一下如何關閉你的程序與NUI之間的聯繫。
VOID NuiShutdown();
關於這個函數,沒什麼可說的,你的程序退出時,都應該調用一下。甚至於,你的程序暫時不使用KINECT了,就放開對設備的控制權,好讓其他程序可以訪問KINECT。
友情提示使用OpenGL的程序員們,如果你們是在使用glut庫,那麼不要在glMainLoop()後面調用NuiShutdown(),因爲它不會執行,你應該在窗口關閉以及任意你執行了退出代碼的時刻調用它。
注意:
一個應用程序對一個KINECT設備,必須要調用此函數一次,並且也只能調用一次。如果在這之後又調用一次初始化,勢必會引起邏輯錯誤(即使是2個不同程序)。比如你運行一個SDK的例子,在沒關閉它的前提下,再運行一個,那麼後運行的就無法初始化成功,但不會影響之前的程序繼續運行。
如果你的程序想使用多臺KINECT,那麼請使用INuiInstance接口來初始化你的設備。
3. CreateNextFrame Event(是否讀取下一幀的控制信號)
一個用來手動重置信號是否可用的事件句柄(event),該信號用來控制KINECT是否可以開始讀取下一幀數據。
HANDLE m_hNextVideoFrameEvent = CreateEvent( NULL, TRUE, FALSE, NULL );
HANDLE m_hNextDepthFrameEvent = CreateEvent( NULL, TRUE, FALSE, NULL );
HANDLE m_hNextSkeletonEvent = CreateEvent( NULL, TRUE, FALSE, NULL );
HANDLE m_hEvNuiProcessStop = CreateEvent(NULL,TRUE,FALSE,NULL);//用於結束的事件對象;
也就是說在這裏指定一個句柄後,隨着程序往後繼續推進,當你在任何時候想要控制kinect讀取下一幀數據時,
都應該先使用WaitForSingleObject(handleEvent, 0)判斷一下該句柄,是否有數據可拿。
CreateEvent()創建一個windows事件對象,創建成功則返回事件的句柄。事件有兩個狀態,有信號和沒有信號!上面說到了。就是拿來等待新數據的。
CreateEvent函數需要4個參數:
設定爲NULL的安全描述符;
一個設定爲true的布爾值,因爲應用程序將重置事件消息;
一個未指定的事件消息初始狀態的布爾值;
一個空字符串,因爲事件未命名
4. Open Stream(For color & depth stream)
打開對NUI設備的訪問通道,只針對彩色數據流和深度數據流。
使用這個函數來打開kinect彩色或者深度圖的訪問通道,當然,其內部原理是通過"流"來實現的,因此,你也可以把這個函數理解爲,創建一個訪問彩色或者深度圖的數據流。似乎從很久遠的時候開始,微軟就在windows中開始使用流來訪問所有硬件設備了。
HRESULT NuiImageStreamOpen (NUI_IMAGE_TYPE eImageType,
NUI_IMAGE_RESOLUTION eResolution,
DWORD dwImageFrameFlags_NotUsed,
DWORD dwFrameLimit,
HANDLE hNextFrameEvent,
HANDLE *phStreamHandle);
參數:
NUI_IMAGE_TYPE eImageType [in]
這是一個NUI_IMAGE_TYPE 枚舉類型的值(對應NuiInitialize中的標誌位),用來詳細指定你要創建的流類型。比如你要打開彩色圖,就使用NUI_IMAGE_TYPE_COLOR。
要打開深度圖,就使用
NUI_IMAGE_TYPE_DEPTH,
NUI_IMAGE_TYPE_DEPTH_AND_PLAYER_INDEX。能打開的圖像類型,必須是你在初始化的時候指定過的。
NUI_IMAGE_RESOLUTION eResolution[in]
這是一個NUI_IMAGE_RESOLUTION 枚舉類型的值,用來指定你要以什麼分辨率來打開eImageType(參數1)中指定的圖像類別。
彩色圖NUI_IMAGE_TYPE_COLOR,支持2種分辨率:NUI_IMAGE_RESOLUTION_1280x960,NUI_IMAGE_RESOLUTION_640x480
深度圖NUI_IMAGE_TYPE_DEPTH,支持3種分辨率:NUI_IMAGE_RESOLUTION_640x480, NUI_IMAGE_RESOLUTION_320x240, NUI_IMAGE_RESOLUTION_80x60
DWORD dwImageFrameFlags_NotUsed [in]
一點用沒有,你隨便給個整數就行了。以後的版本里不知道它會不會有意義。
DWORD dwFrameLimit[in]
指定NUI運行時環境將要爲你所打開的圖像類型建立幾個緩衝。最大值是NUI_IMAGE_STREAM_FRAME_LIMIT_MAXIMUM(當前版本爲 4).對於大多數啊程序來說,2就足夠了。
HANDLE hNextFrameEvent [in, optional]
就是之前建立的一個用來手動重置信號是否可用的事件句柄(event),該信號用來控制KINECT是否可以開始讀取下一幀數據。
也就是說在這裏指定一個句柄後,隨着程序往後繼續推進,當你在任何時候想要控制kinect讀取下一幀數據時,都應該先使用WaitForSingleObject判斷一下該句柄。
HANDLE phStreamHandle[out] (就是通過這個讀取數據)
指定一個句柄的地址。函數成功執行後,將會創建對應的數據訪問通道(流),並且讓該句柄保存這個通道的地址。也就是說,如果現在創建成功了。
那麼以後你想讀取數據,就要通過這個句柄了。
5. Skeleton Flag Enable(For skeleton stream)
設置Skeleton Stream跟蹤標誌位
HRESULTNuiSkeletonTrackingEnable( m_hNextSkeletonEvent, Flag );
Flag:
近景: NUI_SKELETON_TRACKING_FLAG_ENABLE_IN_NEAR_RANGE
坐姿: NUI_SKELETON_TRACKING_FLAG_ENABLE_SEATED_SUPPORT; 只有頭肩手臂10個節點;
站姿:flag&~(NUI_SKELETON_TRACKING_FLAG_ENABLE_SEATED_SUPPORT); 有標準的20個節點;
默認值0:站姿和非近景數據位分佈如上圖。
6.等待新的數據,等到後觸發
WaitForSingleObject(nextColorFrameEvent, INFINITE)==0
WAIT_OBJECT_0 == WaitForSingleObject(m_hEvNuiProcessStop, 0)
WAIT_OBJECT_0 == WaitForSingleObject(m_hNextVideoFrameEvent, 0)
WAIT_OBJECT_0 == WaitForSingleObject(m_hNextDepthFrameEvent, 0)
WAIT_OBJECT_0 == WaitForSingleObject(m_hNextSkeletonEvent, 0)
程序運行堵塞在這裏,這個事件有信號,就是說有數據,那麼程序往下執行,如果沒有數據,就會等待。函數第二個參數表示你願意等多久,具體的數據的話就表示你願意等多少毫秒,還不來,我就不要了,繼續往下走。如果是INFINITE的話,就表示無限等待新數據,直到海枯石爛,一定等到爲止。等到有信號後就返回0 。
7. 從流數據獲得Frame數據
7.1 For Color & Depth Sream
7.1.1 NuiImageStreamGetNextFrame
HRESULT NuiImageStreamGetNextFrame (HANDLE hStream,
DWORD dwMillisecondsToWait,
CONST NUI_IMAGE_FRAME **ppcImageFrame);
eg:
const NUI_IMAGE_FRAME * pImageFrame = NULL;
HRESULT hr = NuiImageStreamGetNextFrame( h, 0, &pImageFrame );
參數:
HANDLE hStream [in]
前面NuiImageStreamOpen打開數據流時的輸出參數,就是流句柄,彩色數據對應彩色流句柄,深度數據對應深度流句柄。
DWORD dwMillisecondsToWait [in]
延遲時間,以微秒爲單位的整數。當運行環境在讀取之前,會先等待這個時間。
CONST NUI_IMAGE_FRAME **ppcImageFrame [out] 出參,
指定一個 NUI_IMAGE_FRAME 結構的指針,當讀取成功後,該函數會將讀取到的數據地址返回,保存在此參數中。pImageFrame包含了很多有用信息,包括:圖像類型,分辨率,圖像緩衝區,時間戳等等。
返回值同樣是S_OK表示成功
7.1.2 INuiFrameTexture接口
INuiFrameTexture * pTexture = pImageFrame->pFrameTexture;
一個容納圖像幀數據的對象,類似於Direct3D紋理,但是隻有一層(不支持mip-maping)。
其公有方法包含以下:
AddRef---增加一個對象上接口的引用數目;該方法在每複製一個指向該對象上接口的指針時都要調用一次;
BufferLen---獲得緩衝區的字節長度;
GetLevelDesc---獲得緩衝區的描述;
LockRect---給緩衝區上鎖;
Pitch---返回一行的字節數;
QueryInterface---獲取指向對象所支持的接口的指針,該方法對其所返回的指針調用AddRef函數;
Release---減少一個對象上接口的引用計數;
UnlockRect---對緩衝區解鎖;
7.1.3 提取數據幀到LockedRect並鎖定數據
pTexture->LockRect(0, &LockedRect, NULL, 0);
提取數據幀到LockedRect,它包括兩個數據對象:pitch每行字節數,pBits第一個字節地址。另外,其還鎖定數據,這樣當我們讀數據的時候,kinect就不會去修改它。
好了,現在真正保存圖像的對象LockedRect我們已經有了,並且也將圖像信息寫入這個對象了。
7.1.4 將數據轉換爲OpenCV的Mat格式
然後將其保存圖像的對象LockedRect的格式,轉化爲OpenCV的Mat格式。
BYTE* pBuffer
= (BYTE*) LockedRect.pBits;
Mat
colorImg(COLOR_HIGHT,COLOR_WIDTH,CV_8UC4,pBuffer);
Mat depthImg(DEPTH_HIGHT,DEPTH_WIDTH,CV_16U,pBuffer);
對於INuiFrameTexture * pTexture = pImageFrame->pFrameTexture;獲得的數據彩色數據和深度數據的規格是不一樣的!!!
彩色數據:單位數據是32位,對應BGRA
深度數據:單位數據是16位。
深度數據的取值還有另一種方式:
INuiFrameTexture* pDepthImagePixelFrame;
BOOL nearMode = TRUE;
m_pNuiSensor->NuiImageFrameGetDepthImagePixelFrameTexture(m_pDepthStreamHandle, &pImageFrame, &nearMode, &pDepthImagePixelFrame);
INuiFrameTexture * pTexture = pDepthImagePixelFrame;
NUI_LOCKED_RECT LockedRect;
pTexture->LockRect(0, &LockedRect, NULL, 0 );
通過這種方式獲得的深度數據跟彩色數據的規格一致都是32bit一組,但是16~35位爲空,不包含數據,數據依然保存在低16位,只不過加了兩個通道。
7.2 For Skeleton Stream
HRESULT NuiSkeletonGetNextFrame(
DWORD dwMillisecondsToWait,
NUI_SKELETON_FRAME *SkeletonFrame);
eg:
NUI_SKELETON_FRAME SkeletonFrame;
HRESULT hr = NuiSkeletonGetNextFrame( 0, &SkeletonFrame );
在獲得了 Depth Sream的Frame的數據之後,才能獲得skeleton數據。這屬於三步走管線的第三步。
骨骼幀數據保存在NUI_SKELETON_FRAME結構體中,我們首先來分析下這個最重要的結構體:
typedef struct _NUI_SKELETON_FRAME {
LARGE_INTEGER liTimeStamp;
DWORD dwFrameNumber;
DWORD dwFlags; //沒用到
Vector4 vFloorClipPlane;
Vector4 vNormalToGravity;
NUI_SKELETON_DATA SkeletonData[NUI_SKELETON_COUNT];
} NUI_SKELETON_FRAME;
LARGE_INTEGER liTimeStamp & DWORD dwFrameNumber (時間標記字段):
SkeletonFrame的dwFrameNumber和liTimestamp字段表示當前記錄中的幀序列信息。FrameNumber是深度數據幀中的用來產生骨骼數據幀的幀編號。幀編號通常是不連續的,但是之後的幀編號一定比之前的要大。骨骼追蹤引擎在追蹤過程中可能會忽略某一幀深度數據,這跟應用程序的性能和每秒產生的幀數有關。例如,在基於事件獲取骨骼幀信息中,如果事件中處理幀數據的時間過長就會導致這一幀數據還沒有處理完就產生了新的數據,那麼這些新的數據就有可能被忽略了。如果採用查詢模型獲取幀數據,那麼取決於應用程序設置的骨骼引擎產生數據的頻率,即取決於深度影像數據產生骨骼數據的頻率。
Timestap字段記錄自Kinect傳感器初始化(調用NuiInitialize函數)以來經過的累計毫秒時間。不用擔心FrameNumber或者Timestamp字段會超出上限。FrameNumber是一個32位的整型,Timestamp是64位整型。如果應用程序以每秒30幀的速度產生數據,應用程序需要運行2.25年纔會達到FrameNumber的限,此時Timestamp離上限還很遠。另外在Kinect傳感器每一次初始化時,這兩個字段都會初始化爲0。可以認爲FrameNumber和Timestamp這兩個值是唯一的。
這兩個字段在分析處理幀序列數據時很重要,比如進行關節點值的平滑,手勢識別操作等。在多數情況下,我們通常會處理幀時間序列數據,這兩個字段就顯得很有用。目前SDK中並沒有包含手勢識別引擎。在未來SDK中加入手勢引擎之前,我們需要自己編寫算法來對幀時間序列進行處理來識別手勢,這樣就會大量依賴這兩個字段。
最重要的要數這個成員了。首先,Kinect可以檢測到6個骨骼,在NuiSensor.h中有宏定義:#define NUI_SKELETON_COUNT ( 6 )。所以SkeletonData[NUI_SKELETON_COUNT]定義了六個骨骼的數據。骨骼數據是通過一個NUI_SKELETON_DATA類型的結構體保存的:
typedef struct _NUI_SKELETON_DATA {
NUI_SKELETON_TRACKING_STATE eTrackingState;
DWORD dwTrackingID;
DWORD dwEnrollmentIndex;
DWORDdwUserIndex;
Vector4 Position;
Vector4 SkeletonPositions[20];
NUI_SKELETON_POSITION_TRACKING_STATE eSkeletonPositionTrackingState[20];
DWORD dwQualityFlags;
} NUI_SKELETON_DATA;
其中:
eTrackingState:
eTrackingState字段表示當前的骨骼數據的狀態,是一個枚舉類型。
typedef enum NUI_SKELETON_TRACKING_STATE
{
NUI_SKELETON_NOT_TRACKED = 0,
NUI_SKELETON_POSITION_ONLY,
NUI_SKELETON_TRACKED
} NUI_SKELETON_TRACKING_STATE;
下表展示了SkeletonTrackingState枚舉的可能值的含義:
NUI_SKELETON_NOT_TRACKED 表示骨架沒有被跟蹤到,這個狀態下,骨骼數據的Position字段和相關的關節點數據中的每一個位置點值都是0 。
NUI_SKELETON_POSITION_ONLY 檢測到了骨骼對象,但是跟蹤沒有激活,也就是說骨骼數據的Position字段有值,但是相關的關節點數據中的每一個位置點值都是0(對應被動模式,被動模式只提供骨骼的位置,不提供關節點的位置)。
NUI_SKELETON_TRACKED 所有骨骼點的位置都被跟蹤,骨骼數據的Position字段和相關的關節點數據中的每一個位置點值都非零(對應主動模式,骨骼位置和關節點位置都提供)。
dwTrackingID:
骨骼追蹤引擎對於每一個追蹤到的遊戲者的骨骼信息都有一個唯一編號。這個值是整型,他會隨着新的追蹤到的遊戲者的產生添加增長。另外,這個編號的產生是不確定的。如果骨骼追蹤引擎失去了對遊戲者的追蹤,比如說遊戲者離開了Kinect的視野,那麼這個對應的唯一編號就會過期。當Kinect追蹤到了一個新的遊戲者,他會爲其分配一個新的唯一編號。編號值爲0表示這個骨骼信息不是遊戲者的。
Position:
Position是一個Vector4類型的字段,代表所有骨骼的中間點。身體的中間點和脊柱關節的位置相當。該字段提供了一個最快且最簡單的所有視野範圍內的遊戲者位置的信息,而不管其是否在追蹤狀態中。在一些應用中,如果不用關心骨骼中具體的關節點的位置信息,那麼該字段對於確定遊戲者的位置狀態已經足夠。該字段對於手動選擇要追蹤的遊戲者也是一個參考。例如,應用程序可能需要追蹤距離Kinect最近的且處於追蹤狀態的遊戲者,那麼該字段就可以用來過濾掉其他的遊戲者。
Vector4類型是一個空間座標的類型:
Vector4typedef struct _Vector4 {
FLOAT x; //X coordinate
FLOAT y; //Y coordinate
FLOAT z; //Z coordinate
FLOAT w; //W coordinate
} Vector4;
SkeletonPositions[20]:
這個數組記錄的是主動模式下骨骼的20個關節點對應的空間位置信息。每一個關節點都有類型爲Vector4的位置屬性,他通過X,Y,Z三個值來描述關節點的位置。X,Y值是相對於骨骼平面空間的位置,他和深度影像,彩色影像的空間座標系不一樣。KinectSnesor對象有一系列的座標轉換方法,可以將骨骼座標點轉換到對應的深度數據影像中去。
SkeletonPositionTrackingState[20]:
上面說到骨骼有跟蹤的好與不好,那麼關節點也是跟蹤和分析的,那也有好與不好之分吧。而SkeletonPositionTrackingState屬性就是標示對應的每一個關節點的跟蹤狀態的:
typedef enum _NUI_SKELETON_POSITION_TRACKING_STATE
{
NUI_SKELETON_POSITION_NOT_TRACKED = 0,
NUI_SKELETON_POSITION_INFERRED,
NUI_SKELETON_POSITION_TRACKED
} NUI_SKELETON_POSITION_TRACKING_STATE;
各個狀態表示的含義:
NUI_SKELETON_POSITION_NOT_TRACKED 關節點位置沒有跟蹤到,而且也不能通過一些信息推斷出來,所以關節點的Position值都爲0 。
NUI_SKELETON_POSITION_INFERRED 骨骼的關節點位置可以由上一幀數據、已經跟蹤到的其他點位置和骨架的幾何假設這三個信息推測出來。
NUI_SKELETON_POSITION_TRACKED 關節點被成功跟蹤,它的位置是基於當前幀的計算得到的。
NUI_SKELETON_POSITION_INDEX:
另外,SkeletonPositions[20]這個數組是保存20個關節點的位置的,那麼哪個數組元素對應哪個關節點呢?實際上,這個數組的保存是有順序的,然後,我們可以通過下面的枚舉值做爲這個數組的下標來訪問相應的關節點位置信息:
typedef enum _NUI_SKELETON_POSITION_INDEX
{
NUI_SKELETON_POSITION_HIP_CENTER = 0,
NUI_SKELETON_POSITION_SPINE,
NUI_SKELETON_POSITION_SHOULDER_CENTER,
NUI_SKELETON_POSITION_HEAD,
NUI_SKELETON_POSITION_SHOULDER_LEFT,
NUI_SKELETON_POSITION_ELBOW_LEFT,
NUI_SKELETON_POSITION_WRIST_LEFT,
NUI_SKELETON_POSITION_HAND_LEFT,
NUI_SKELETON_POSITION_SHOULDER_RIGHT,
NUI_SKELETON_POSITION_ELBOW_RIGHT,
NUI_SKELETON_POSITION_WRIST_RIGHT,
NUI_SKELETON_POSITION_HAND_RIGHT,
NUI_SKELETON_POSITION_HIP_LEFT,
NUI_SKELETON_POSITION_KNEE_LEFT,
NUI_SKELETON_POSITION_ANKLE_LEFT,
NUI_SKELETON_POSITION_FOOT_LEFT,
NUI_SKELETON_POSITION_HIP_RIGHT,
NUI_SKELETON_POSITION_KNEE_RIGHT,
NUI_SKELETON_POSITION_ANKLE_RIGHT,
NUI_SKELETON_POSITION_FOOT_RIGHT,
NUI_SKELETON_POSITION_COUNT
} NUI_SKELETON_POSITION_INDEX;
好了,感覺把這些說完,代碼裏面的東西就很容易讀懂了。所以也沒必要贅述了。但是還需要提到的幾點是:
平滑化:
NuiTransformSmooth(&skeletonFrame,NULL);
在骨骼跟蹤過程中,有些情況會導致骨骼運動呈現出跳躍式的變化。例如遊戲者的動作不夠連貫,Kinect硬件的性能等等。骨骼關節點的相對位置可能在幀與幀之間變動很大,這回對應用程序產生一些負面的影響。例如會影響用戶體驗和給控製造成意外等。
而這個函數就是解決這個問題的,它對骨骼數據進行平滑,通過將骨骼關節點的座標標準化來減少幀與幀之間的關節點位置差異。
HRESULT NuiTransformSmooth(
NUI_SKELETON_FRAME *pSkeletonFrame,
const NUI_TRANSFORM_SMOOTH_PARAMETERS *pSmoothingParams
)
這個函數可以傳入一個NUI_TRANSFORM_SMOOTH_PARAMETERS類型的參數:
typedef struct_NUI_TRANSFORM_SMOOTH_PARAMETERS {
FLOAT fSmoothing;
FLOAT fCorrection;
FLOAT fPrediction;
FLOAT fJitterRadius;
FLOAT fMaxDeviationRadius;
} NUI_TRANSFORM_SMOOTH_PARAMETERS;
NUI_TRANSFORM_SMOOTH_PARAMETERS這個結構定義了一些屬性:
fSmoothing:平滑值(Smoothing)屬性,設置處理骨骼數據幀時的平滑量,接受一個0-1的浮點值,值越大,平滑的越多。0表示不進行平滑
fCorrection:修正值(Correction)屬性,接受一個從0-1的浮點型。值越小,修正越多。
fJitterRadius:抖動半徑(JitterRadius)屬性,設置修正的半徑,如果關節點“抖動”超過了設置的這個半徑,將會被糾正到這個半徑之內。該屬性爲浮點型,單位爲米。
fMaxDeviationRadius:最大偏離半徑(MaxDeviationRadius)屬性,用來和抖動半徑一起來設置抖動半徑的最大邊界。任何超過這一半徑的點都不會認爲是抖動產生的,而被認定爲是一個新的點。該屬性爲浮點型,單位爲米。
fPrediction:預測幀大小(Prediction)屬性,返回用來進行平滑需要的骨骼幀的數目。
空間座標轉換:
NuiTransformSkeletonToDepthImage(pSkel->SkeletonPositions[i], &fx, &fy );
由於深度圖像數據和彩色圖像數據來自於不同的攝像頭,而這兩個攝像頭所在的位置不一樣,而且視場角等也不也一樣,所以產生的圖像也會有差別(就好像你兩個眼睛看到的東西都不一樣,這樣大腦才能通過他們合成三維場景理解),也就是說這兩幅圖像上的像素點並不嚴格一一對應。例如深度圖像中,你在圖像的位置可能是(x1,y1),但在彩色圖像中,你在圖像的位置是(x2,y2),而兩個座標一般都是不相等的。另外,骨骼數據的座標又和深度圖像和彩色圖像的不一樣。所以就存在了彩色座標空間、深度座標空間、骨骼座標空間和你的UI座標空間四個不同的座標空間了。那麼他們各個空間之前就需要交互,例如我在骨骼空間找到這個人在(x1,y1)座標上,那麼對應的深度圖像,這個人在什麼座標呢?另外,我們需要把骨骼關節點的位置等畫在我們的UI窗口上,那麼它們的對應關係又是什麼呢?
微軟SDK提供了一系列方法來幫助我們進行這幾個空間座標系的轉換。例如:
voidNuiTransformSkeletonToDepthImage(
Vector4 vPoint, //骨骼空間中某點座標
FLOAT *pfDepthX, //對應的深度空間中的座標
FLOAT *pfDepthY
)//將骨骼座標轉換到深度圖像座標上去。
HRESULTNuiImageGetColorPixelCoordinatesFromDepthPixel(
NUI_IMAGE_RESOLUTION eColorResolution,
const NUI_IMAGE_VIEW_AREA *pcViewArea,
LONG lDepthX,
LONG lDepthY,
USHORT usDepthValue,
LONG *plColorX,
LONG *plColorY
)//獲取在深度圖中具體座標位置的像素在相應彩色空間中的像素的座標。
Vector4NuiTransformDepthImageToSkeleton(
LONG lDepthX,
LONG lDepthY,
USHORT usDepthValue
)//傳入深度圖像座標,返回骨骼空間座標
三、代碼及註釋
#include <iostream>
#include <opencv/cv.h>
#include <opencv/highgui.h>
using namespace std;
using namespace cv;
//KKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKK
#include "windows.h"
#include "NuiApi.h"
//KKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKK
//----各種內核事件和句柄-----------------------------------------------------------------
HANDLE m_hNextVideoFrameEvent;
HANDLE m_hNextDepthFrameEvent;
HANDLE m_hNextSkeletonEvent;
HANDLE m_pVideoStreamHandle;
HANDLE m_pDepthStreamHandle;
HANDLE m_hEvNuiProcessStop;//用於結束的事件對象;
//---圖像大小等參數--------------------------------------------
#define COLOR_WIDTH 640
#define COLOR_HIGHT 480
#define DEPTH_WIDTH 640
#define DEPTH_HIGHT 480
#define SKELETON_WIDTH 640
#define SKELETON_HIGHT 480
#define CHANNEL 3
#define WIDTH 80
#define HEIGHT 40
//---Image stream 分辨率-------------------------------
NUI_IMAGE_RESOLUTION colorResolution = NUI_IMAGE_RESOLUTION_640x480; //設置圖像分辨率
NUI_IMAGE_RESOLUTION depthResolution = NUI_IMAGE_RESOLUTION_640x480;
//---顯示圖像------------------------------------------;
Mat depthRGB(DEPTH_HIGHT,DEPTH_WIDTH,CV_8UC3,Scalar(0,0,0));
//---顏色------------------------------
const Scalar SKELETON_COLORS[NUI_SKELETON_COUNT] =
{
Scalar(0, 0, 255), // Blue
Scalar(0, 255, 0), // Green
Scalar(255, 255, 64), // Yellow
Scalar(64, 255, 255), // Light blue
Scalar(255, 64, 255), // Purple
Scalar(255, 128, 128) // Pink
};
//深度信息轉換爲彩色信息,顯示人體所在區域;
HRESULT DepthToRGB(USHORT depth, uchar& red, uchar& green, uchar& blue)
{
SHORT realDepth = NuiDepthPixelToDepth(depth);
USHORT playerIndex = NuiDepthPixelToPlayerIndex(depth);//獲得深度圖該點像素位置對應的 UserID;
// Convert depth info into an intensity for display
BYTE b = 255 - static_cast<BYTE>(256 * realDepth / 0x0fff);
switch(playerIndex)
{
//場景中沒有對象時,只繪製灰度圖,灰度值[0,128];
case 0:
red = b/2;
green = b/2;
blue = b/2;
break;
case 1:
red = b;
green = 0;
blue = 0;
break;
case 2:
red = 0;
green = b;
blue = 0;
break;
case 3:
red = b/4;
green = b;
blue = b;
break;
case 4:
red = b;
green = b;
blue = b/4;
break;
case 5:
red = b;
green = b/4;
blue = b;
break;
case 6:
red = b/2;
green = b/2;
blue = b;
break;
case 7:
red = 255 - b/2;
green = 255 - b/2;
blue = 255 - b/2;
break;
default:
red = 0;
green = 0;
blue = 0;
break;
}
return S_OK;
}
//獲取彩色圖像數據,並進行顯示
int DrawColor(HANDLE h)
{
const NUI_IMAGE_FRAME * pImageFrame = NULL;
HRESULT hr = NuiImageStreamGetNextFrame( h, 0, &pImageFrame );
if(FAILED( hr ) )
{
cout<<"GetColor Image Frame Failed"<<endl;
return-1;
}
INuiFrameTexture* pTexture = pImageFrame->pFrameTexture;
NUI_LOCKED_RECT LockedRect;
pTexture->LockRect(0, &LockedRect, NULL, 0 );
if(LockedRect.Pitch != 0 )
{
BYTE* pBuffer = (BYTE*) LockedRect.pBits;
//OpenCV顯示彩色視頻
if(true)
{
Mat temp(COLOR_HIGHT,COLOR_WIDTH,CV_8UC4,pBuffer);
imshow("Color",temp);
}
int c = waitKey(1);//按下ESC結束
//如果在視頻界面按下ESC,q,Q都會導致整個程序退出
if(c == 27 || c == 'q' || c == 'Q' )
{
SetEvent(m_hEvNuiProcessStop);
}
}
NuiImageStreamReleaseFrame(h, pImageFrame );
return 0;
}
//獲取深度圖像數據,並進行顯示
int DrawDepth(HANDLE h)
{
const NUI_IMAGE_FRAME * pImageFrame = NULL;
HRESULT hr = NuiImageStreamGetNextFrame( h, 0, &pImageFrame );
if(FAILED( hr ) )
{
cout<<"GetDepth Image Frame Failed"<<endl;
return-1;
}
INuiFrameTexture* pTexture = pImageFrame->pFrameTexture;
NUI_LOCKED_RECT LockedRect;
pTexture->LockRect(0, &LockedRect, NULL, 0 );
if(LockedRect.Pitch != 0 )
{
BYTE* pBuff = (BYTE*) LockedRect.pBits;
//OpenCV顯示深度視頻
if(true)
{
Mat depthTmp(DEPTH_HIGHT,DEPTH_WIDTH,CV_16U,pBuff);
//imshow("Depth",depthTmp);
depthRGB.setTo(0);
//顯示骨骼人體區域信息;
for (int y=0; y<DEPTH_HIGHT; y++)
{
const USHORT* p_depthTmp = depthTmp.ptr<USHORT>(y);
uchar* p_depthRGB = depthRGB.ptr<uchar>(y);
for (int x=0; x<DEPTH_WIDTH; x++)
{
USHORT depthValue = p_depthTmp[x];
if (depthValue != 63355)
{
uchar redValue,greenValue,blueValue;
DepthToRGB(depthValue,redValue,greenValue,blueValue);
p_depthRGB[3*x] = blueValue;
p_depthRGB[3*x+1] = greenValue;
p_depthRGB[3*x+2] = redValue;
}
else
{
p_depthRGB[3*x]=0;
p_depthRGB[3*x+1]=0;
p_depthRGB[3*x+2]=0;
}
}
}
//imshow("DepthRGB",depthRGB);
}
int c = waitKey(1);//按下ESC結束
if(c == 27 || c == 'q' || c == 'Q' )
{
SetEvent(m_hEvNuiProcessStop);
}
}
NuiImageStreamReleaseFrame(h, pImageFrame );
return 0;
}
//獲取骨骼數據,並在深度圖上進行顯示
int DrawSkeleton()
{
NUI_SKELETON_FRAME SkeletonFrame;
cv::Point pt[20];
HRESULT hr = NuiSkeletonGetNextFrame( 0, &SkeletonFrame );
if(FAILED( hr ) )
{
cout<<"GetSkeleton Image Frame Failed"<<endl;
return -1;
}
bool bFoundSkeleton = false;
for(int i = 0 ; i < NUI_SKELETON_COUNT ; i++ )
{
if(SkeletonFrame.SkeletonData[i].eTrackingState == NUI_SKELETON_TRACKED )
{
bFoundSkeleton= true;
}
}
//Has skeletons!
if(bFoundSkeleton )
{
NuiTransformSmooth(&SkeletonFrame,NULL);
//cout<<"skeleton num:"<<NUI_SKELETON_COUNT<<endl;
for(int i = 0 ; i < NUI_SKELETON_COUNT ; i++ )
{
NUI_SKELETON_TRACKING_STATE trackingState = SkeletonFrame.SkeletonData[i].eTrackingState;
if(trackingState == NUI_SKELETON_TRACKED ) //骨骼位置跟蹤成功,則直接定位;
{
//NUI_SKELETON_DATA *pSkel = &(SkeletonFrame.SkeletonData[i]);
NUI_SKELETON_DATA SkelData = SkeletonFrame.SkeletonData[i];
Point jointPositions[NUI_SKELETON_POSITION_COUNT];
for (int j = 0; j < NUI_SKELETON_POSITION_COUNT; ++j)
{
LONG x, y;
USHORT depth;
cout<<j<<" :("<<SkelData.SkeletonPositions[j].x<<","<<SkelData.SkeletonPositions[j].y<<") ";
NuiTransformSkeletonToDepthImage(SkelData.SkeletonPositions[j], &x, &y, &depth, depthResolution);
//circle(depthRGB, Point(x,y), 5, Scalar(255,255,255), -1, CV_AA);
jointPositions[j] = Point(x, y);
}
for (int j = 0; j < NUI_SKELETON_POSITION_COUNT; ++j)
{
if (SkelData.eSkeletonPositionTrackingState[j] == NUI_SKELETON_POSITION_TRACKED)
{
circle(depthRGB,jointPositions[j],5,SKELETON_COLORS[i],-1,CV_AA);
circle(depthRGB,jointPositions[j],6,Scalar(0,0,0),1,CV_AA);
}
else if (SkelData.eSkeletonPositionTrackingState[j] == NUI_SKELETON_POSITION_INFERRED)
{
circle(depthRGB, jointPositions[j], 5, Scalar(255,255,255), -1, CV_AA);
}
}
cout<<endl;
}
else if(trackingState == NUI_SKELETON_POSITION_INFERRED) //如果骨骼位置跟蹤未成功,通過推測定位骨骼位置;
{
LONG x, y;
USHORT depth=0;
NuiTransformSkeletonToDepthImage(SkeletonFrame.SkeletonData[i].Position,&x, &y,&depth,depthResolution);
cout<<SkeletonFrame.SkeletonData[i].Position.x<<";"<<SkeletonFrame.SkeletonData[i].Position.y<<endl;
circle(depthRGB, Point(x, y), 7, CV_RGB(0,0,0), CV_FILLED);
}
}
}
imshow("SkeletonDepth",depthRGB);
int c = waitKey(1);//按下ESC結束
if(c == 27 || c == 'q' || c == 'Q' )
{
SetEvent(m_hEvNuiProcessStop);
}
return 0;
}
//kinect讀取數據流線程;
DWORD WINAPI KinectDataThread(LPVOID pParam)
{
HANDLE hEvents[4] = {m_hEvNuiProcessStop,m_hNextVideoFrameEvent,
m_hNextDepthFrameEvent,m_hNextSkeletonEvent};
while(1)
{
int nEventIdx;
nEventIdx=WaitForMultipleObjects(sizeof(hEvents)/sizeof(hEvents[0]),
hEvents,FALSE,100);
if(WAIT_OBJECT_0 == WaitForSingleObject(m_hEvNuiProcessStop, 0))
{
break;
}
//Process signal events
if(WAIT_OBJECT_0 == WaitForSingleObject(m_hNextVideoFrameEvent, 0))
{
DrawColor(m_pVideoStreamHandle);
}
if(WAIT_OBJECT_0 == WaitForSingleObject(m_hNextDepthFrameEvent, 0))
{
DrawDepth(m_pDepthStreamHandle);
}
if(WAIT_OBJECT_0 == WaitForSingleObject(m_hNextSkeletonEvent, 0))
{
DrawSkeleton();
}
}
CloseHandle(m_hEvNuiProcessStop);
m_hEvNuiProcessStop= NULL;
CloseHandle(m_hNextSkeletonEvent );
CloseHandle(m_hNextDepthFrameEvent );
CloseHandle(m_hNextVideoFrameEvent );
return 0;
}
int main(int argc,char * argv[])
{
//初始化NUI
HRESULT hr =NuiInitialize(NUI_INITIALIZE_FLAG_USES_DEPTH_AND_PLAYER_INDEX|NUI_INITIALIZE_FLAG_USES_COLOR|NUI_INITIALIZE_FLAG_USES_SKELETON);
if(hr != S_OK )
{
cout<<"Nui Initialize Failed"<<endl;
return hr;
}
//打開KINECT設備的彩色圖信息通道
m_hNextVideoFrameEvent = CreateEvent( NULL, TRUE, FALSE, NULL );
m_pVideoStreamHandle = NULL;
hr= NuiImageStreamOpen(NUI_IMAGE_TYPE_COLOR,colorResolution, 0,
2,m_hNextVideoFrameEvent, &m_pVideoStreamHandle);
if(FAILED( hr ) )
{
cout<<"Could not open image stream video"<<endl;
return hr;
}
m_hNextDepthFrameEvent = CreateEvent( NULL, TRUE, FALSE, NULL );
m_pDepthStreamHandle = NULL;
hr= NuiImageStreamOpen(NUI_IMAGE_TYPE_DEPTH_AND_PLAYER_INDEX,depthResolution, 0,
2, m_hNextDepthFrameEvent,&m_pDepthStreamHandle);
if(FAILED( hr ) )
{
cout<<"Could not open depth stream video"<<endl;
return hr;
}
m_hNextSkeletonEvent = CreateEvent( NULL, TRUE, FALSE, NULL );
//骨骼跟蹤事件flag設置:
//坐姿 flag|NUI_SKELETON_TRACKING_FLAG_ENABLE_SEATED_SUPPORT;只有頭肩手臂10個節點;
//站姿 flag&~(NUI_SKELETON_TRACKING_FLAG_ENABLE_SEATED_SUPPORT);有20個節點;
hr= NuiSkeletonTrackingEnable( m_hNextSkeletonEvent,
NUI_SKELETON_TRACKING_FLAG_ENABLE_IN_NEAR_RANGE/*&(~NUI_SKELETON_TRACKING_FLAG_ENABLE_SEATED_SUPPORT)*/);
if(FAILED( hr ) )
{
cout<<"Could not open skeleton stream video"<<endl;
return hr;
}
m_hEvNuiProcessStop = CreateEvent(NULL,TRUE,FALSE,NULL);//用於結束的事件對象;
//開啓一個線程---用於讀取彩色、深度、骨骼數據;
HANDLE m_hProcesss = CreateThread(NULL, 0, KinectDataThread, 0, 0, 0);
///////////////////////////////////////////////
while(m_hEvNuiProcessStop!=NULL)
{
WaitForSingleObject(m_hProcesss,INFINITE);
CloseHandle(m_hProcesss);
m_hProcesss= NULL;
}
//Clean up.
NuiShutdown();
return 0;
}
顯示圖像(坐姿,因此只有上肢10個骨骼點):