65.9K
CodeProject 正在变化。 阅读更多。
Home

多线程 Kinect 流保存器应用程序

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.68/5 (19投票s)

2013年9月12日

CPOL

6分钟阅读

viewsIcon

34372

此应用程序允许用户以每秒30帧的速度录制并将Kinect流存储到文件夹中。

引言

本文旨在为希望开发基于Microsoft Kinect传感器的无标记运动捕捉系统的研究人员提供帮助。本文将重点介绍C++编程的实践技巧,同时提供C++示例代码,演示如何将Kinect流(骨骼、彩色和深度流)录制并存储到指定目录中。

Kinect流保存应用程序基于KinectExplorer-D2D(C++版)示例代码,并与SDK 1.7兼容。对示例代码的修改将允许用户以每秒30帧(FPS)的速度实时显示和存储Kinect流。因此,录制任务不会降低处理任务(数据流传输和显示)的速度。

                                                                   

在本文中,您可以找到分步说明,了解如何将流数据(彩色、深度、骨骼)存储到动态FIFO缓冲区,以及如何将FIFO缓冲区中的数据单独写入如上所述的输出文件。磁盘写入在单独的
线程中完成,以避免在数据收集过程中帧处理速度下降。

请注意,本文中解释的所有C++示例代码都应通过Visual Studio构建和运行。

必备组件

作为起点,本文假设您

  • 对创建Windows应用程序所需的C++库、示例和文档有相当的了解,
  • 能够掌握在Visual Studio中构建和运行C++代码的基础知识。

要求

  • Microsoft Visual Studio
  • Windows版Kinect SDK
  • 应用程序“Kinect Explorer - D2D C++”的源代码

使用代码

创建线程

实时录制Kinect流的主要问题之一是避免在数据收集过程中帧处理速度下降。保持帧率在30 FPS的唯一解决方案是创建一个新线程来存储流内容。这样,将有一个线程执行处理任务,包括初始化Kinect传感器、注释数据流和将流存储到动态缓冲区;另一个线程将Kinect流写入磁盘上的输出文件。

第一步,我们可以使用CreateThread函数创建一个线程。新线程必须在应用程序主循环中创建。

下面是一个简单的例子,演示如何创建一个新线程来执行已定义的函数SaveThread。如果使用Kinect Explorer应用程序,我们可以在“KinectWindow.cpp”中定义的MessageLoop函数中创建新线程。

我们还需要创建一个事件来停止新线程,这与创建新线程同步。

WPARAM KinectWindow::MessageLoop()
{
    {...} 
    
    // Create a new thread to save the streams
    e_hSaveThread = CreateThread (NULL, 0, SaveThread, this, 0, NULL );   

    // Create an event to stop the savethread
    e_hStopSaveThread = CreateEventW (nullptr, TRUE, FALSE, nullptr);     
 
    {...}
    
    WaitForSingleObject(e_hSaveThread, INFINITE);
    CloseHandle(e_hSaveThread);
 
    return msg.wParam;
}
void KinectWindow::OnClose(HWND hWnd, WPARAM wParam)
{
    ...
 
    // Stop  thread
    if (INVALID_HANDLE_VALUE != e_hSaveThread)
    {
        SetEvent(e_hStopSaveThread);
    }

 
    ...
}

以下是指示如何定义SaveThread函数的代码。第一个函数返回线程的句柄,第二个函数可以处理存储过程。

ORD WINAPI KinectWindow::SaveThread(LPVOID lpParam) 
{
    KinectWindow *pthis = (KinectWindow *)lpParam;
    return pthis->SaveThread( );
} 

WORD WINAPI KinectWindow::SaveThread()
{
    bool SaveProcessing = true;
    
        
    while(SaveProcessing)
    {
	// stop event was signalled;
	if ( WAIT_OBJECT_0 == WaitForSingleObject(e_hStopSaveThread,1))
	{
	    SaveProcessing = false;
            break;
	}	
       ...    
     }
 return 0;
}

创建第二个线程后,我们可以继续存储Kinect流。

将骨骼流保存到.csv文件

由于应用程序是多线程的,应该有一个FIFO(先进先出)缓冲区用于保存流。这样,Kinect流将由主线程(Kinect Explorer - D2D中的ProcessThread函数)推入FIFO缓冲区,第二个线程(SaveThread函数)将从FIFO中移除最后的数据流。移除的数据流可以以任何格式保存到文件中。

在C++中,Deque或双端队列是具有动态大小的序列容器,可以在两端(其前端或后端)进行扩展或收缩。在这里,我们将使用deque来实现我们的FIFO缓冲区,用于保存流。

 

以下是一个简单的例子,演示如何声明一个动态FIFO缓冲区来存储骨骼流。

struct    SkeletonStream // Dynamic Buufer to store Skeleton streams
{  
    deque <NUI_SKELETON_DATA> SkeletonJointPosition[NUI_SKELETON_COUNT];
    deque <DWORD>             dwframenumber;
    deque <LARGE_INTEGER>     FrameTime;
};
SkeletonStream              SkeletonBuffer;     

在这里,您可以看到更多关于如何将NUI_SKELETON_FRAME传递给名为BufferSkeletonStream的函数,该函数负责缓冲流的示例。

下一个重要步骤是确保多线程应用程序之间共享缓冲区没有任何问题。换句话说,线程需要同步。临界区对象提供了两个线程之间使用FIFO缓冲区的同步。这样,消费线程可以调用EnterCriticalSection函数(可在MSDN Kernel32.lib中找到)来请求临界区的所有权。完成对FIFO缓冲区的操作后,它可以调用LeaveCrtiticalSection函数来释放临界区的所有权。如果临界区对象当前被另一个线程拥有,EnterCriticalSection会无限期等待所有权。

注意:您需要将以下代码添加到“NuiSkeletonStream.cpp”中定义的ProcessSkeleton函数中。

void NuiSkeletonStream::ProcessSkeleton()
{
    ...
 
    // Set skeleton data to stream viewers
    AssignSkeletonFrameToStreamViewers(&m_skeletonFrame);
 
    // save the Skeleton stream
    NUI_SKELETON_FRAME* nui_skeletonFrame = &m_skeletonFrame;
    BufferSkeletonStream(nui_skeletonFrame);
 
    UpdateTrackedSkeletons();
} 
/// Push back the Skeleton streams into a buffer
void NuiSkeletonStream::BufferSkeletonStream(const NUI_SKELETON_FRAME* pFrame)
{
    nui_skeleton_frame = pFrame;
    
    // Request ownership of the critical section.
    EnterCriticalSection(&CriticalSection_Skeleton);
 
    for (int i = 0 ; i < NUI_SKELETON_COUNT ; i++)
    {
        SkeletonBuffer.SkeletonJointPosition[i].push_back(
           nui_skeleton_frame->SkeletonData[i]);
    }
    SkeletonBuffer.FrameTime.push_back(nui_skeleton_frame->liTimeStamp);
    SkeletonBuffer.dwframenumber.push_back(nui_skeleton_frame->dwFrameNumber);
    
    // Release ownership of the critical section.
    LeaveCriticalSection(&CriticalSection_Skeleton);
}  

注意:一旦我们定义了BufferSkeletonStream函数,我们需要将以下代码添加到已经在“KinectWindow.cpp”中定义的SaveThread函数中。该代码将把包含20个关节位置的骨骼流作为.csv文件存储到文件夹中。

  DWORD WINAPI KinectWindow::SaveThread(LPVOID lpParam) 
{
    KinectWindow *pthis = (KinectWindow *)lpParam;
    return pthis->SaveThread( );
}
 
WORD WINAPI KinectWindow::SaveThread()
{
    bool SaveProcessing = true;
    
        
    while(SaveProcessing)
    {
	// stop event was signalled;
	if ( WAIT_OBJECT_0 == WaitForSingleObject(e_hStopSaveThread,1))
	{
	    SaveProcessing = false;
            break;
	}
           
        // save the skeleton stream    
        SaveSkeletonStream;        
        }
 return 0;
}
/ Save Skeleton streams as .csv files
void KinectWindow:: SaveSkeletonStream
{
    bool EnSave = false;        
 
    NUI_SKELETON_DATA TempSkeletonBuffer[NUI_SKELETON_COUNT];
    DWORD TempframeNumber;
    LARGE_INTEGER TempFrameTime;
 
    // Request ownership of the critical section.
    EnterCriticalSection(&CriticalSection_Skeleton);
 
    if (!SkeletonBuffer.dwframenumber.empty())
    {
        for (int i = 0 ; i < NUI_SKELETON_COUNT ; i++)
        {
            TempSkeletonBuffer[i] = SkeletonBuffer.SkeletonJointPosition[i].front();          
            SkeletonBuffer.SkeletonJointPosition[i].pop_front(); 
            SkeletonBuffer.SkeletonJointPosition[i].shrink_to_fit();
        }
        TempframeNumber = SkeletonBuffer.dwframenumber.front();
        TempFrameTime = SkeletonBuffer.FrameTime.front();
        
        SkeletonBuffer.dwframenumber.pop_front(); 
        SkeletonBuffer.FrameTime.pop_front(); 
        SkeletonBuffer.dwframenumber.shrink_to_fit();
        SkeletonBuffer.FrameTime.shrink_to_fit(); 
        
        EnSave = true;
    }
 
    // Release ownership of the critical section.
    LeaveCriticalSection(&CriticalSection_Skeleton);
 
    if (EnSave)
    {
        if (SkeletonJoint)
        {
           for( int j = 0 ; j < NUI_SKELETON_POSITION_COUNT ; j++ ) {                                           
                 SkeletonJoint << TempSkeletonBuffer[i].SkeletonPositions[j].x<<","<<
                                  TempSkeletonBuffer[i].SkeletonPositions[j].y<<","<<
                                  TempSkeletonBuffer[i].SkeletonPositions[j].z<<","<<
                                  fixed<< 
                                  TempSkeletonBuffer[i].eSkeletonPositionTrackingState[j]<< 
                                  endl;}    
         }                          
         SkeletonTime << fixed <<  TempframeNumber 
                      <<","<< TempFrameTime.QuadPart <<endl;
        
        EnSave = false;
    }
    else if (EnStop_Skel)
    {
        EnStop_Skel = false;
        SkeletonJoint.close();        
        SkeletonTime.close();
        SkeletonBuffer.dwframenumber.clear();
        SkeletonBuffer.FrameTime.clear();
        for( int i = 0 ; i < NUI_SKELETON_COUNT ; i++ )
        {
            SkeletonBuffer.SkeletonJointPosition[i].clear();
        }        
    } 
}

 

将彩色流保存到.bitmap文件

同样,为了保存彩色流,我们需要声明一个动态FIFO缓冲区。唯一的区别是应该有一个动态向量(或deque)来存储彩色流(一个640 x 480的向量),这使得图像像素的插入和删除更加复杂。shared_ptr为程序员提供了不同的复杂性权衡,应相应使用。使用shared_ptr,内存泄漏的风险始终最小,因为析构函数总是确保分配的内存被删除。

struct ColorStream // Dynamic Buufer to store color streams
{
    deque <shared_ptr<vector <BYTE> > >        ColorImageBuffer;
    deque <DWORD >                    c_width;
    deque <DWORD >                    c_height;    
};
ColorStream                        ColorBuffer;  

在这里,您可以看到更多关于如何将NuiImageBuffer传递给名为BufferColorStream的函数,该函数缓冲彩色流的示例。

注意:我们需要将以下代码添加到NuiColorStream.cpp中定义的ProcessColor函数中。

void NuiColorStream::ProcessColor()
{
    HRESULT hr;
    NUI_IMAGE_FRAME imageFrame;
 
    ...
 
    // Make sure we've received valid data
    if (lockedRect.Pitch != 0)
    {
        ...
        
    if (m_pStreamViewer)
        {
            // Set image data to viewer
            m_pStreamViewer->SetImage(&m_imageBuffer);
        }
 
    // save the color stream
    NuiImageBuffer* nui_Color = &m_imageBuffer;
    BufferColorStream(m_pColorStream->nui_Color);
    
    }
 
    // Unlock frame data
    pTexture->UnlockRect(0);
 
ReleaseFrame:
    m_pNuiSensor->NuiImageStreamReleaseFrame(m_hStreamHandle, &imageFrame);
}
void NuiColorStream::BufferColorStream(const NuiImageBuffer* pImageiTimeStamp)
{
    const NuiImageBuffer*    nui_Buffer; // Pointer to Image Buffer
    nui_Buffer = pImage;
    shared_ptr<vector <BYTE>> Buffer(new vector<BYTE>(nui_Buffer->GetBufferSize()));
	copy ( nui_Buffer->GetBuffer(), 
	       nui_Buffer->GetBuffer() + nui_Buffer->GetBufferSize(), 
	       Buffer->begin());	
    
    // Request ownership of the critical section.
    EnterCriticalSection(&CriticalSection_Color);
 
    ColorBuffer.height.push_back(nui_Buffer->GetHeight());
    ColorBuffer.width.push_back(nui_Buffer->GetWidth());    
    ColorBuffer.ImageBuffer.push_back(Buffer);
    
    // Release ownership of the critical section.
    LeaveCriticalSection(&CriticalSection_Color);
}

注意:一旦我们定义了BufferColorStream函数,我们需要将以下代码添加到已经在“KinectWindow.cpp”中定义的SaveThread函数中。该代码将把包含彩色像素的彩色流以位图格式写入磁盘。

    DWORD WINAPI KinectWindow::SaveThread(LPVOID lpParam) 
{
    KinectWindow *pthis = (KinectWindow *)lpParam;
    return pthis->SaveThread( );
}
 
WORD WINAPI KinectWindow::SaveThread()
{
    bool SaveProcessing = true;
    
        
    while(SaveProcessing)
    {
	// stop event was signalled;
	if ( WAIT_OBJECT_0 == WaitForSingleObject(e_hStopSaveThread,1))
	{
	    SaveProcessing = false;
            break;
	}
    
        // save the skeleton stream    
        SaveSkeletonStream;
 
        // save the color stream
        SaveColorStream;    
        }
 return 0;
}

 

// Save Color images as Bitmaps
void KinectWindow::SaveColorStream
{
    bool EnSave = false;    
               
    shared_ptr<vector <BYTE>>  TempColorBuffer
			       (new vector<BYTE>(ColorBuffer.ImageBuffer.size()));
    DWORD                TempHeight;
    DWORD                TempWidth;
 
    // Request ownership of the critical section.
    EnterCriticalSection(&CriticalSection_Color);
 
    if (! ColorBuffer.ImageBuffer.empty())
    {            
        TempColorBuffer = ColorBuffer.ImageBuffer.front();
        TempHeight =      ColorBuffer.height.front();
        TempWidth =       ColorBuffer.width.front();
        Pop_FrontColor()
        EnSave = true;
    }        
 
    // Release ownership of the critical section.
    LeaveCriticalSection(&CriticalSection_Color);
 
    if (EnSave)
    {
        WCHAR c_screenshotPath[MAX_PATH];
	StringCchPrintfW(c_screenshotPath,
	                _countof(c_screenshotPath),
	                L"%s\\%ld_Color.bmp",
	                foldername,c_Counter);	
			
	HRESULT hr = SaveBitmapToFile(TempColorBuffer->data(),
				      TempWidth,
				      TempHeight,  
				      32, 
				      c_screenshotPath);            
        c_Counter++;
        EnSave = false;            
    } else if (EnStop_Color) c_Counter = 0;
}
void KinectWindow:: Pop_FrontColor()
{
	ColorBuffer.ImageBuffer.pop_front();			
	ColorBuffer.height.pop_front();			
	ColorBuffer.width.pop_front();				
	ColorBuffer.FrameNumber.pop_front();				
	ColorBuffer.FrameTime.pop_front();

	ColorBuffer.ImageBuffer.shrink_to_fit();			
	ColorBuffer.height.shrink_to_fit();
	ColorBuffer.width.shrink_to_fit();
	ColorBuffer.FrameNumber.shrink_to_fit();
	ColorBuffer.FrameTime.shrink_to_fit();
} 
// Save Bitmap images to a file
HRESULT StreamSaver::SaveBitmapToFile(BYTE* pBitmapBits, 
	                              LONG lWidth, 
				      LONG lHeight, 
				      WORD wBitsPerPixel, 
				      LPCWSTR lpszFilePath)
{ 
    DWORD dwByteCount = lWidth * lHeight * (wBitsPerPixel / 8);
 
    BITMAPINFOHEADER bmpInfoHeader = {0};
 
    bmpInfoHeader.biSize        = sizeof(BITMAPINFOHEADER);  // Size of the header
    bmpInfoHeader.biBitCount    = wBitsPerPixel;             // Bit count
    bmpInfoHeader.biCompression = BI_RGB;                    // Standard RGB
    bmpInfoHeader.biWidth       = lWidth;                    // Width in pixels
    bmpInfoHeader.biHeight      = -lHeight;                  // Height in pixels
    bmpInfoHeader.biPlanes      = 1;                         // Default
    bmpInfoHeader.biSizeImage   = dwByteCount;               // Image size in bytes

    BITMAPFILEHEADER bfh = {0};
 
    bfh.bfType    = 0x4D42;                                           
    bfh.bfOffBits = bmpInfoHeader.biSize + 
		    sizeof(BITMAPFILEHEADER);       // Offset to the start of pixel data 
    bfh.bfSize    = bfh.bfOffBits +  
		    bmpInfoHeader.biSizeImage;      // Size of image + headers

    // Create the file on disk to write to
    HANDLE hFile = CreateFileW(lpszFilePath, 
				GENERIC_WRITE, 
				0, 
				NULL, 
				CREATE_ALWAYS, 
				FILE_ATTRIBUTE_NORMAL, 
				NULL);

    // Return if error opening file
    if (NULL == hFile) 
    {
        return E_ACCESSDENIED;
    }
 
    DWORD dwBytesWritten = 0;
    
    // Write the bitmap file header
    if ( !WriteFile(hFile, &bfh, sizeof(bfh), &dwBytesWritten, NULL) )
    {
        CloseHandle(hFile);
        return E_FAIL;
    }
    
    // Write the bitmap info header
    if ( !WriteFile(hFile, &bmpInfoHeader, sizeof(bmpInfoHeader), &dwBytesWritten, NULL) )
    {
        CloseHandle(hFile);
        return E_FAIL;
    }
    
    // Write the Color Data
    if ( !WriteFile(hFile, pBitmapBits, bmpInfoHeader.biSizeImage, &dwBytesWritten, NULL) )
    {
        CloseHandle(hFile);
        return E_FAIL;
    }   
    
    // Close the file
    CloseHandle(hFile);
  return S_OK;
}

保存Depth流与Color流非常相似。

关注点

  • 使用临界区对象在线程之间进行同步
  • 二维动态向量

 


使用条款

如果您的研究受益于IATSL提供的Kinect Stream Saver应用程序,
请考虑以下使用条款

- 请在任何关于您的作品的出版物中引用以下论文

* Elham Dolatabadi, Babak Taati, Gemma S. Parra-Dominguez, Alex Mihailidis,
“一种无标记运动追踪方法,用于理解步态和平衡的变化:案例研究”,
北美康复工程与辅助技术协会(RESNA 2013年年会)。学生科学论文竞赛获奖者

- 请在任何关于您的作品的出版物中致谢此应用程序。感谢您的支持。

致谢文本示例
我们要感谢智能辅助技术与系统实验室提供的应用程序,该应用程序促进了这项研究,包括由Elham Dolatabadi开发的Kinect Stream Saver应用程序。


 

源代码

您还可以从以下网址下载Kinect Stream Saver应用程序的源代码

http://kinectstreamsaver.codeplex.com/


历史   

  • 2013年9月11日:第一个版本。
  • 2013年9月12日:第二个版本
  • 2013年9月30日:第三个版本
  • 2013年10月12日:第四个版本
© . All rights reserved.