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






4.68/5 (19投票s)
此应用程序允许用户以每秒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日:第四个版本