首先看骨骼追踪例子代码的结构:
例子代码不是很多,在SkeletonBasics.cpp中,大部分的功能都在kinect SDK中实现,应用开发只需要了解接口,并调用它的接口即可。首先我们找到main函数:
int APIENTRY wWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPWSTR lpCmdLine, int nCmdShow) { CSkeletonBasics application; application.Run(hInstance, nCmdShow); }
这个main函数是不是似曾相识,跟深度图的main函数差不多的,只是构造一个对象,然后调用这个对象的Run接口,构造函数只是初始化成员,所以我们直接看Run的实现:
int CSkeletonBasics::Run(HINSTANCE hInstance, int nCmdShow) { MSG msg = {0}; WNDCLASS wc = {0}; // Dialog custom window class wc.style = CS_HREDRAW | CS_VREDRAW; wc.cbWndExtra = DLGWINDOWEXTRA; wc.hInstance = hInstance; wc.hCursor = LoadCursorW(NULL, IDC_ARROW); wc.hIcon = LoadIconW(hInstance, MAKEINTRESOURCE(IDI_APP)); wc.lpfnWndProc = DefDlgProcW; wc.lpszClassName = L"SkeletonBasicsAppDlgWndClass"; if (!RegisterClassW(&wc)) { return 0; } // Create main application window HWND hWndApp = CreateDialogParamW( hInstance, MAKEINTRESOURCE(IDD_APP), NULL, (DLGPROC)CSkeletonBasics::MessageRouter, reinterpret_cast<LPARAM>(this)); // Show window ShowWindow(hWndApp, nCmdShow); const int eventCount = 1; HANDLE hEvents[eventCount]; // Main message loop while (WM_QUIT != msg.message) { hEvents[0] = m_hNextSkeletonEvent; // Check to see if we have either a message (by passing in QS_ALLEVENTS) // Or a Kinect event (hEvents) // Update() will check for Kinect events individually, in case more than one are signalled DWORD dwEvent = MsgWaitForMultipleObjects(eventCount, hEvents, FALSE, INFINITE, QS_ALLINPUT); // Check if this is an event we're waiting on and not a timeout or message if (WAIT_OBJECT_0 == dwEvent) { Update(); } if (PeekMessageW(&msg, NULL, 0, 0, PM_REMOVE)) { // If a dialog message will be taken care of by the dialog proc if ((hWndApp != NULL) && IsDialogMessageW(hWndApp, &msg)) { continue; } TranslateMessage(&msg); DispatchMessageW(&msg); } } return static_cast<int>(msg.wParam); }
这个Run函数是不是也是很熟悉,跟深度图的Run函数也是差不多,创建对话框,然后进入消息处理死循环,在死循环里处理窗口消息和kinect消息。
接下来和深度图一样,我们也进入到窗口初始化消息里,看整个程序的初始化是怎么样的:
LRESULT CALLBACK CSkeletonBasics::DlgProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) { switch (message) { case WM_INITDIALOG: { // Bind application window handle m_hWnd = hWnd; // Init Direct2D D2D1CreateFactory(D2D1_FACTORY_TYPE_SINGLE_THREADED, &m_pD2DFactory); // Look for a connected Kinect, and create it if found CreateFirstConnected(); } break;
初始化处理和深度图也差不多,D2D1CreateFactory函数创建DirectX对象,然后调用CreateFirstConnected去创建kinect对象,在CreateFirstConnected函数里的处理又有何不同呢?我们一起来看代码:
HRESULT CSkeletonBasics::CreateFirstConnected() { INuiSensor * pNuiSensor; // 获取kinect数量 int iSensorCount = 0; HRESULT hr = NuiGetSensorCount(&iSensorCount); if (FAILED(hr)) { return hr; } // Look at each Kinect sensor 查找一个有效的kinect设备 for (int i = 0; i < iSensorCount; ++i) { // Create the sensor so we can check status, if we can't create it, move on to the next hr = NuiCreateSensorByIndex(i, &pNuiSensor); if (FAILED(hr)) { continue; } // Get the status of the sensor, and if connected, then we can initialize it hr = pNuiSensor->NuiStatus(); if (S_OK == hr) { m_pNuiSensor = pNuiSensor; break; } // This sensor wasn't OK, so release it since we're not using it pNuiSensor->Release(); } if (NULL != m_pNuiSensor) { // 这里的参数和深度图不一样,深度图用的参数是NUI_INITIALIZE_FLAG_USES_DEPTH // 这个意思就是将这个kinect初始化成骨骼跟踪 hr = m_pNuiSensor->NuiInitialize(NUI_INITIALIZE_FLAG_USES_SKELETON); if (SUCCEEDED(hr)) { // 这个Event和深度图一样,是kinectSDK和上层应用程序通信的事件 m_hNextSkeletonEvent = CreateEventW(NULL, TRUE, FALSE, NULL); // 打开一个骨骼跟踪数据,深度图需要的参数比较复杂,这个比较简答 hr = m_pNuiSensor->NuiSkeletonTrackingEnable(m_hNextSkeletonEvent, 0); } } if (NULL == m_pNuiSensor || FAILED(hr)) { SetStatusMessage(L"No ready Kinect found!"); return E_FAIL; } return hr; }
看初始化kinect设备的代码,貌似区别也不是很大,只是初始化参数不同,打开数据流的函数不同,框架性的代码还是差不多的。
当这个m_hNextSkeletonEvent事件被注册到kinect SDK库之后,当有数据来时,Update函数会被调用,Update函数最后调用了ProcessSkeleton函数来处理骨骼数据:
void CSkeletonBasics::ProcessSkeleton() { NUI_SKELETON_FRAME skeletonFrame = {0}; // 获取一帧骨骼数据 HRESULT hr = m_pNuiSensor->NuiSkeletonGetNextFrame(0, &skeletonFrame); if ( FAILED(hr) ) { return; } // 让骨骼数据变得平滑,消除抖动现象 m_pNuiSensor->NuiTransformSmooth(&skeletonFrame, NULL); // 创建m_pRenderTarget对象 hr = EnsureDirect2DResources( ); if ( FAILED(hr) ) { return; } m_pRenderTarget->BeginDraw(); m_pRenderTarget->Clear( ); RECT rct; GetClientRect( GetDlgItem( m_hWnd, IDC_VIDEOVIEW ), &rct); int width = rct.right; int height = rct.bottom; // 最多支持6个人的骨骼跟踪数据 for (int i = 0 ; i < NUI_SKELETON_COUNT; ++i) { NUI_SKELETON_TRACKING_STATE trackingState = skeletonFrame.SkeletonData[i].eTrackingState; if (NUI_SKELETON_TRACKED == trackingState) { // 画骨骼 DrawSkeleton(skeletonFrame.SkeletonData[i], width, height); } else if (NUI_SKELETON_POSITION_ONLY == trackingState) { // 只是跟踪位置的话,那么就画个位置,在人很多时,kinect只跟踪两个人的骨骼,其他人之显示位置 D2D1_ELLIPSE ellipse = D2D1::Ellipse( SkeletonToScreen(skeletonFrame.SkeletonData[i].Position, width, height), g_JointThickness, g_JointThickness ); m_pRenderTarget->DrawEllipse(ellipse, m_pBrushJointTracked); } } hr = m_pRenderTarget->EndDraw(); // Device lost, need to recreate the render target // We'll dispose it now and retry drawing if (D2DERR_RECREATE_TARGET == hr) { hr = S_OK; DiscardDirect2DResources(); } }
在处理骨骼数据函数中,对数据skeletonFrame中保存的六个骨骼跟踪数据循环处理,如果状态是NUI_SKELETON_TRACKED则通过DrawSkeleton画详细骨骼信息,如果是NUI_SKELETON_POSITION_ONLY则只画位置信息,如果不是这两个状态,则啥也不做。接下来我们来看看DrawSkeleton看看骨骼信息怎么画的:
void CSkeletonBasics::DrawSkeleton(const NUI_SKELETON_DATA & skel, int windowWidth, int windowHeight) { int i; // 将关节点转化成屏幕上的坐标点 for (i = 0; i < NUI_SKELETON_POSITION_COUNT; ++i) { m_Points[i] = SkeletonToScreen(skel.SkeletonPositions[i], windowWidth, windowHeight); } // 画骨骼,参数1是骨骼数据,参数2和参数3是关节 DrawBone(skel, NUI_SKELETON_POSITION_HEAD, NUI_SKELETON_POSITION_SHOULDER_CENTER); // 这个是脑袋,从脑袋关节到肩膀中间 DrawBone(skel, NUI_SKELETON_POSITION_SHOULDER_CENTER, NUI_SKELETON_POSITION_SHOULDER_LEFT); // 肩膀中间到左边 DrawBone(skel, NUI_SKELETON_POSITION_SHOULDER_CENTER, NUI_SKELETON_POSITION_SHOULDER_RIGHT); // 肩膀中间到右边,下面的类似 DrawBone(skel, NUI_SKELETON_POSITION_SHOULDER_CENTER, NUI_SKELETON_POSITION_SPINE); DrawBone(skel, NUI_SKELETON_POSITION_SPINE, NUI_SKELETON_POSITION_HIP_CENTER); DrawBone(skel, NUI_SKELETON_POSITION_HIP_CENTER, NUI_SKELETON_POSITION_HIP_LEFT); DrawBone(skel, NUI_SKELETON_POSITION_HIP_CENTER, NUI_SKELETON_POSITION_HIP_RIGHT); // Left Arm DrawBone(skel, NUI_SKELETON_POSITION_SHOULDER_LEFT, NUI_SKELETON_POSITION_ELBOW_LEFT); DrawBone(skel, NUI_SKELETON_POSITION_ELBOW_LEFT, NUI_SKELETON_POSITION_WRIST_LEFT); DrawBone(skel, NUI_SKELETON_POSITION_WRIST_LEFT, NUI_SKELETON_POSITION_HAND_LEFT); // Right Arm DrawBone(skel, NUI_SKELETON_POSITION_SHOULDER_RIGHT, NUI_SKELETON_POSITION_ELBOW_RIGHT); DrawBone(skel, NUI_SKELETON_POSITION_ELBOW_RIGHT, NUI_SKELETON_POSITION_WRIST_RIGHT); DrawBone(skel, NUI_SKELETON_POSITION_WRIST_RIGHT, NUI_SKELETON_POSITION_HAND_RIGHT); // Left Leg DrawBone(skel, NUI_SKELETON_POSITION_HIP_LEFT, NUI_SKELETON_POSITION_KNEE_LEFT); DrawBone(skel, NUI_SKELETON_POSITION_KNEE_LEFT, NUI_SKELETON_POSITION_ANKLE_LEFT); DrawBone(skel, NUI_SKELETON_POSITION_ANKLE_LEFT, NUI_SKELETON_POSITION_FOOT_LEFT); // Right Leg DrawBone(skel, NUI_SKELETON_POSITION_HIP_RIGHT, NUI_SKELETON_POSITION_KNEE_RIGHT); DrawBone(skel, NUI_SKELETON_POSITION_KNEE_RIGHT, NUI_SKELETON_POSITION_ANKLE_RIGHT); DrawBone(skel, NUI_SKELETON_POSITION_ANKLE_RIGHT, NUI_SKELETON_POSITION_FOOT_RIGHT); // 画关节 for (i = 0; i < NUI_SKELETON_POSITION_COUNT; ++i) { D2D1_ELLIPSE ellipse = D2D1::Ellipse( m_Points[i], g_JointThickness, g_JointThickness ); if ( skel.eSkeletonPositionTrackingState[i] == NUI_SKELETON_POSITION_INFERRED ) { m_pRenderTarget->DrawEllipse(ellipse, m_pBrushJointInferred); } else if ( skel.eSkeletonPositionTrackingState[i] == NUI_SKELETON_POSITION_TRACKED ) { m_pRenderTarget->DrawEllipse(ellipse, m_pBrushJointTracked); } } }
在这里绘制比较简单,所以这里绘制并没有像深度图一样使用一个类来做绘制,直接写到函数里了。另外一个这里DirectX的绘制也就只是画画线,如果你熟悉GDI,完全可以通过GDI来绘图,而不用DirectX。