7 июля 2011 в 13:01

Работа с Microsoft Kinect в приложениях на C++

C++*

Введение


Совсем недавно Microsoft выпустили beta-версию инструментария для работы с Kinect – Microsoft Research Kinect SDK. В инструментарии доступны заголовочные файлы, библиотека, а также примеры использования в приложениях на C++. Но наличие самого SDK не решает проблему с отсутствием доходчивых примеров и документации. Заметно, что Microsoft больше ориентируется на .NET разработчиков, поэтому, например, на официальном форуме подавляющее большинство топиков связаны с C#, а гуглопоиск при попытке найти какое-либо описание API для Kinect выдает всего несколько ссылок, и те — на официальную документацию.

В этой статье рассматриваются варианты использования Microsoft Kinect, а также упомянутого выше программного инструментария в C++ приложениях и в связке с библиотекой wxWidgets.



Начало работы


Для того, чтобы начать, необходимо, как минимум, скачать средства разработки. Сделать это можно на страничке Microsoft Research Kinect SDK.
Также нам необходима будет библиотека wxWidgets. Скачать ее (v2.9.1) можно с официального сайта или из SVN-репозитория (я предпочитаю SVN HEAD – в нем часто появляется много новых полезных вещей, которых нет в официальном релизе, но часто появляются и баги).
Для тех, кто предпочитает «светлую» сторону и жаждет разрабатывать приложения только с использованием бесплатных инструментов, есть смысл скачать Visual C++ 2010 Express, а также Microsoft Windows SDK for Windows 7 (или более поздний), без него собрать wxWidgets в Visual C++ Express, скорее всего, не получится.

Собираем wxWidgets


Процесс установки Visual Studio и Kinect SDK мы рассматривать не будем, там все сводится к нескольким нажатиям кнопки Next в мастере установки, а вот процесс сборки wxWidgets мы рассмотрим более подробно, т.к. от него зависят дальнейшие шаги разработки приложения.
После того, как исходный код wxWidgets загружен и распакован в отдельную папку, необходимо добавить переменную окружения WXWIN, в значение которой прописать путь к папке с исходными кодами wxWidgets.
При использовании исходных кодов из SVN, необходимо скопировать файл %WXWIN%/include/msw/setup0.h в %WXWIN%/include/msw/setup.h.
По умолчанию в решении wxWidgets есть несколько доступных конфигураций (Рис. 1):
  • Debug
  • Release
  • DLL Debug
  • DLL Release

Первые две конфигурации позволяют собрать wxWidgets в виде статических библиотек, последние – в виде нескольких динамически подгружаемых модулей.



Сборка статических библиотек


Перед сборкой статических библиотек (конфигурации Debug и Release) необходимо в свойствах всех проектов в решении установить для параметра C/C++ -> Code Generation -> Runtime Library значение Multi-Threaded Debug и Multi-Threaded соответственно (Рис. 2).



Установка таких параметров компиляции позволит избавиться от необходимости установки Visual C++ Redistributable на машинах конечных пользователей вместе с нашим приложением. После установки параметров компиляции можно собирать решение. В результате в подкаталоге lib/vc_lib должны создаться несколько .lib файлов, которые впоследствии будут использоваться в нашем приложении.

Сборка динамических библиотек


Для сборки динамических библиотек ничего в настройках компилятора менять не нужно. Но есть другая проблема – в решении не проставлены зависимости, поэтому процесс сборки нужно будет перезапускать несколько раз, т.к. при линковке некоторых библиотек будут возникать ошибки. После сборки в подкаталоге lib/vc_dll должны создаться несколько .DLL и .LIB файлов.
Хотелось бы отметить, что необходимо собирать (Debug) и отладочную и оптимизированную (Release) версии библиотек.

Создаем тестовое приложение


Итак, на данный момент у нас есть:
  • Visual Studio 2010
  • Microsoft Research Kinect SDK
  • Собранные библиотеки wxWidgets
    • Статические
    • Динамические

Можно приступать к созданию приложения.
В тестовом приложении у нас будет:
  • Класс приложения (производный от wxApp)
  • Класс главной формы (производный от wxFrame)
  • Класс канвы (элемент управления, производный от wxWindow, на котором будет отображаться изображение с сенсора Kinect)

KinectTestApp.h
#ifndef _KINECTTESTAPP_H_
#define _KINECTTESTAPP_H_

#include "wx/image.h"
#include "KinectTestMainFrame.h"

class KinectTestApp: public wxApp
{    
    DECLARE_CLASS( KinectTestApp )
    DECLARE_EVENT_TABLE()
public:
    KinectTestApp();
    void Init();
    virtual bool OnInit();
    virtual int OnExit();
};

DECLARE_APP(KinectTestApp)

#endif


KinectTestApp.cpp
...
bool KinectTestApp::OnInit()
{    
#if wxUSE_LIBPNG
    wxImage::AddHandler(new wxPNGHandler);
#endif
#if wxUSE_LIBJPEG
    wxImage::AddHandler(new wxJPEGHandler);
#endif
    KinectTestMainFrame* mainWindow = new KinectTestMainFrame( NULL );
    mainWindow->Show(true);
    return true;
}


KinectTestMainFrame.h
class KinectTestMainFrame: public wxFrame, public wxThreadHelper
{    
    DECLARE_CLASS( KinectTestMainFrame )
    DECLARE_EVENT_TABLE()
public:
    KinectTestMainFrame();
    KinectTestMainFrame( wxWindow* parent,
        wxWindowID id = SYMBOL_KINECTTESTMAINFRAME_IDNAME,
        const wxString& caption = SYMBOL_KINECTTESTMAINFRAME_TITLE,
        const wxPoint& pos = SYMBOL_KINECTTESTMAINFRAME_POSITION,
        const wxSize& size = SYMBOL_KINECTTESTMAINFRAME_SIZE,
        long style = SYMBOL_KINECTTESTMAINFRAME_STYLE );
    bool Create( wxWindow* parent,
        wxWindowID id = SYMBOL_KINECTTESTMAINFRAME_IDNAME,
        const wxString& caption = SYMBOL_KINECTTESTMAINFRAME_TITLE,
        const wxPoint& pos = SYMBOL_KINECTTESTMAINFRAME_POSITION,
        const wxSize& size = SYMBOL_KINECTTESTMAINFRAME_SIZE,
        long style = SYMBOL_KINECTTESTMAINFRAME_STYLE );
    ~KinectTestMainFrame();
    void Init();
    void CreateControls();
    wxBitmap GetBitmapResource( const wxString& name );
    wxIcon GetIconResource( const wxString& name );
    virtual wxThread::ExitCode Entry();

    wxGridBagSizer* m_MainSizer;
    wxListBox* m_DeviceListBox;
    KinectCanvas* m_Canvas;
};

#endif


KinectTestMainFrame.cpp
...
void KinectTestMainFrame::CreateControls()
{    
    KinectTestMainFrame* itemFrame1 = this;

    m_MainSizer = new wxGridBagSizer(0, 0);
    m_MainSizer->SetEmptyCellSize(wxSize(10, 20));
    itemFrame1->SetSizer(m_MainSizer);

    wxArrayString m_DeviceListBoxStrings;
    m_DeviceListBox = new wxListBox( itemFrame1,
    ID_DEVICE_LISTBOX, wxDefaultPosition,
    wxDefaultSize, m_DeviceListBoxStrings,
    wxLB_SINGLE );
    m_MainSizer->Add(m_DeviceListBox,
    wxGBPosition(0, 0), wxGBSpan(1, 1),
    wxGROW|wxGROW|wxALL, 5);

    m_Canvas = new KinectCanvas( itemFrame1,
    ID_KINECT_CANVAS, wxDefaultPosition,
    wxSize(320, 240), wxSIMPLE_BORDER );
    m_MainSizer->Add(m_Canvas, wxGBPosition(0, 1),
    wxGBSpan(1, 1), wxALIGN_CENTER_HORIZONTAL|
    wxALIGN_CENTER_VERTICAL|wxALL, 5);
    m_MainSizer->AddGrowableCol(1);
    m_MainSizer->AddGrowableRow(0);
}
...
wxThread::ExitCode KinectTestMainFrame::Entry()
{
    return NULL;
}


KinectCanvas.h
...
class KinectCanvas: public wxWindow
{    
    DECLARE_DYNAMIC_CLASS( KinectCanvas )
    DECLARE_EVENT_TABLE()

public:
    KinectCanvas();
    KinectCanvas(wxWindow* parent,
    wxWindowID id = ID_KINECTCANVAS,
    const wxPoint& pos = wxDefaultPosition,
    const wxSize& size = wxSize(100, 100),
    long style = wxSIMPLE_BORDER);

    bool Create(wxWindow* parent,
    wxWindowID id = ID_KINECTCANVAS,
    const wxPoint& pos = wxDefaultPosition,
    const wxSize& size = wxSize(100, 100),
    long style = wxSIMPLE_BORDER);
    ~KinectCanvas();

    void Init();
    void CreateControls();
    void OnPaint( wxPaintEvent& event );

    wxImage * GetCurrentImage() const { return m_CurrentImage ; }
    void SetCurrentImage(wxImage * value) { m_CurrentImage = value ; }

    wxBitmap GetBitmapResource( const wxString& name );
    wxIcon GetIconResource( const wxString& name );

    wxImage * m_CurrentImage;
};

#endif



KinectCanvas.cpp
...
IMPLEMENT_DYNAMIC_CLASS( KinectCanvas, wxWindow )
BEGIN_EVENT_TABLE( KinectCanvas, wxWindow )
    EVT_PAINT( KinectCanvas::OnPaint )
END_EVENT_TABLE()
...
void KinectCanvas::OnPaint( wxPaintEvent& event )
{
    wxAutoBufferedPaintDC dc(this);
    if(!m_CurrentImage)
    {
        dc.SetBackground(wxBrush(GetBackgroundColour(), wxSOLID));
        dc.Clear();
        dc.DrawLabel(_("No image"), wxRect(dc.GetSize()),
        wxALIGN_CENTER_HORIZONTAL|wxALIGN_CENTER_VERTICAL);
    }
    else
    {
        wxBitmap bmp(*m_CurrentImage);
        dc.DrawBitmap(bmp,
        (dc.GetSize().GetWidth()-bmp.GetWidth())/2,
        (dc.GetSize().GetHeight()-bmp.GetHeight())/2);
    }
}



В исходном коде, приведенном выше, есть несколько пустых методов, а также несколько методов, использование которых не очевидно (например, методы GetIconResource(), GetBitmapResource(), Init()). Все это потому, что для создания каркаса приложения был использован дизайнер форм DialogBlocks. Это платный инструмент, но функционала пробной версии вполне достаточно для создания нашего приложения.
Перед тем, как пытаться собрать приложение, необходимо изменить параметры проекта таким образом, чтобы они соответствовали параметрам сборки wxWidgets. Это значит, что если мы хотим использовать статические библиотеки wxWidgets, нам в свойствах проекта в параметре C/C++ -> Code Generation -> Runtime Library нужно установить такие же значения для конфигурации Debug и Release. Если же нам необходимо использовать динамические библиотеки wxWidgets, то в настройках проекта в параметре C/C++ -> Preprocessor -> Preprocessor Definitions необходимо добавить макрос WXUSINGDLL. Этот макрос, также, используется при сборке динамических библиотек wxWidgets, и в итоге настройки нашего проекта и wxWidgets будут совпадать (Рис. 3).



Также, для отладочной версии приложения необходимо в настройках компилятора ресурсов в директивы препроцессора добавить макрос wxUSE_NO_MANIFEST=1. Это нужно для того, чтобы не было конфликтов манифеста, указанного в ресурсном файле wxWidgets (%WXWIN%/include/msw/wx.rc) и манифеста, который Visual Studio добавляет в приложение автоматически.
После выполнения указанных выше действий можно собирать приложение. В результате у нас получится что-то вроде этого (Рис. 4):



Использование Microsoft Research Kinect SDK


После установки Kinect SDK, в системе появится переменная окружения %MSRKINECTSDK%, будет содержать путь к папке, в который был установлен SDK. В этой папке есть подкаталог inc – с заголовочными файлами, и lib – с библиотекой. Путь к заголовочным файлам нужно добавить в настройки компилятора в нашем тестовом приложении, путь к библиотеке – в настройки компоновщика.

Получение списка устройств


На данный момент у нас есть все собранные зависимости и шаблон приложения. Теперь можно приступить к написанию кода, непосредственно использующего Kinect SDK.
В Kinect SDK есть функционал, который позволяет работать с несколькими устройствами Kinect, подключенным к одному компьютеру. Это более универсальное решение при разработке приложения т.к. заранее неизвестно, сколько устройств нам понадобится. Поэтому для нас будет более предпочтительным использование именно этого API.
Для получений списка устройств используется функция MSR_NuiGetDeviceCount(), которая в качестве параметра принимает указатель на целочисленную переменную, в которую, при успешном выполнении, будет записано количество доступных сенсоров:

NUIAPI HRESULT MSR_NuiGetDeviceCount(
  int * pCount
);

Каждое устройство Kinect имеет свой уникальный идентификатор, получить который можно с помощью метода INuiInstance::MSR_NuiGetPropsBlob(). Этот метод в качестве параметров принимает:
  • Идентификатор свойства (в beta-версии SDK может иметь только одно значение — INDEX_UNIQUE_DEVICE_NAME)
  • Указатель на переменную, в которую будет записан результат
  • Объем памяти, доступный для записи результата (например, длина строки). В beta-версии SDK этот параметр не используется.

virtual bool MSR_NuiGetPropsBlob(
  MsrNui::NUI_PROPSINDEX Index,
  void * pBlob,
  DWORD * pdwInOutSize
);


Вооружившись только что полученными знаниями, мы можем реализовать получение списка устройств в нашем приложении.

wxKinectHelper.h
#pragma once

#include <vector>

interface INuiInstance;

class KinectHelper
{
protected:
    typedef std::pair<INuiInstance *, HANDLE> InstanceInfo;
    typedef std::vector<InstanceInfo> InstanceVector;
public:
    KinectHelper();
    virtual ~KinectHelper();

    size_t GetDeviceCount();
    wxString GetDeviceName(size_t index);

    bool IsDeviceOK(size_t deviceIndex);
protected:
    InstanceVector m_Instances;

    void Finalize();
    InstanceInfo * GetInstanceByIndex(size_t index);
};

wxKinectHelper.cpp
#include <wx/wx.h>
#include "msr_nuiapi.h"
#include "KinectHelper.h"

KinectHelper::KinectHelper()
{
}

KinectHelper::~KinectHelper()
{
    Finalize();
}

size_t KinectHelper::GetDeviceCount()
{
    int result(0);
    if(FAILED(MSR_NUIGetDeviceCount(&result))) return 0;
    return (size_t)result;
}

KinectHelper::InstanceInfo * KinectHelper::GetInstanceByIndex(size_t index)
{
    INuiInstance * instance = NULL;
    for(InstanceVector::iterator i = m_Instances.begin();
    i != m_Instances.end(); i++)
    {
        instance = (*i).first;
        if(instance->InstanceIndex() == (int)index) return &(*i);
    }
    if(!instance)
    {
        if(!FAILED(MSR_NuiCreateInstanceByIndex((int)index, &instance)))
        {
            InstanceInfo info;
            info.first = instance;
            info.second = NULL;
            m_Instances.push_back(info);
            return &(m_Instances.at(m_Instances.size()-1));
        }
    }
    return NULL;
}

void KinectHelper::Finalize()
{
    for(InstanceVector::const_iterator i = m_Instances.begin();
    i != m_Instances.end(); i++)
    {
        if((*i).first && (*i).second)
        {
            (*i).first->NuiShutdown();
            MSR_NuiDestroyInstance((*i).first);
        }
    }
}

wxString KinectHelper::GetDeviceName(size_t index)
{
    BSTR result;
    DWORD size;
    InstanceInfo * info = GetInstanceByIndex(index);
    if(info != NULL)
    {
        INuiInstance * instance = info->first;
        if(instance != NULL && instance->MSR_NuiGetPropsBlob(
    MsrNui::INDEX_UNIQUE_DEVICE_NAME, &result, &size))
        {
            wxString name = result;
            SysFreeString(result);
            return name;
        }
    }
    return wxT("Unknown Kinect Sensor");
}

bool KinectHelper::IsDeviceOK(size_t deviceIndex)
{
    return GetInstanceByIndex(deviceIndex) != NULL;
}

Структура InstanceInfo содержит указатель на экземпляр INuiInstance, с помощью которого мы можем получить имя устройства, а также дескриптор потока, в котором происходит захват изображения (будет рассмотрен далее).
Класс wxKinectHelper содержит вектор структур InstanceInfo и методы для получения количества устройств, а также имени каждого устройства. В деструкторе класса wxKinectHelper вызывается метод Finalize(), который закрывает все открытые потоки захвата изображений, а затем удаляет все экземпляры INuiInstance.
Теперь необходимо добавить функционал получения списка устройств в наше приложение.

wxKinectHelperMainFrame.h
...
class KinectTestMainFrame: public wxFrame, public wxThreadHelper
{
    ...
    void ShowDevices();
    ...
    KinectHelper * m_KinectHelper;
}
...

wxKinectHelperMainFrame.cpp
...
void KinectTestMainFrame::ShowDevices()
{
    size_t count = m_KinectHelper->GetDeviceCount();
    m_DeviceListBox->Clear();
    for(size_t i = 0; i < count; ++i)
    {
        int item = m_DeviceListBox->Append(
            m_KinectHelper->GetDeviceName(i));
        m_DeviceListBox->SetClientData(item, (void*)i);
    }
}

В результате после запуска приложения мы получим список доступных устройств Kinect (Рис. 5):



Получение изображения с Kinect


Перед тем, как начать захват изображения с устройства, надо его инициализировать. Делается это с помощью метода INuiInstance::NuiInitialize(), который в качестве параметра принимает битовую маску, описывающую список подсистем устройства, которые мы планируем использовать (сенсор глубины, камеру или поиск игроков на видео).
HRESULT NuiInitialize(
  DWORD dwFlags,
);

Для того, чтобы получать изображение с Kinect необходимо инициализировать поток захвата изображения. Для этих целей используется метод INuiInstance:: NuiImageStreamOpen(), который в качестве параметров принимает:
  • Тип изображения (цветное изображение, буфер глубины и т.д.)
  • Разрешение (от 80x60 до 1280x1024)
  • Флаги обработки изображения (в beta-версии SDK не используется)
  • Количество кэшируемых кадров (максимальное значение NUI_IMAGE_STREAM_FRAME_LIMIT_MAXIMUM на данный момент равно 4)
  • Дескриптор события, которое будет возникать при получении нового кадра (необязательный параметр, но на деле оказалось, что если передать NULL, то поток захвата может и не запуститься)
  • Указатель на переменную, в которую будет записан дескриптор потока захвата изображения при успешном завершении работы функции.

Для того чтобы остановить захват изображений с устройства, необходимо вызвать метод INuiInstance::NuiShutdown(), а после завершения работы с экземпляром INuiInstance нужно освободить память с помощью функции MSR_NuiDestroyInstance(), в параметр которой передать указатель на объект INuiInstance.

Получение буфера глубины (Depth Buffer)


Для того, чтобы начать получение буфера глубины, необходимо вызвать метод INuiInstance:: NuiImageStreamOpen() и в качестве первого параметра передать значение, содержащее флаг NUI_IMAGE_TYPE_DEPTH_AND_PLAYER_INDEX или NUI_IMAGE_TYPE_DEPTH. Наиболее подходящий для последующей обработки буфер у меня получался с использованием флага NUI_IMAGE_TYPE_DEPTH_AND_PLAYER_INDEX. В исходном коде подобный вызов будет выглядеть таким образом:
if(FAILED(info->first->NuiImageStreamOpen(NUI_IMAGE_TYPE_DEPTH_AND_PLAYER_INDEX,
            NUI_IMAGE_RESOLUTION_320x240, 0,
            3,
            hDepthFrameEvent,
            &info->second))) { /* Handle error here */}

В результате описанного выше вызова, в переменной info->second будет дескриптор потока захвата изображения. Дескриптор события hDepthFrameEvent можно создать с помощью функции CreateEvent().

Когда новое изображение будет доступно, будет вызвано событие hDepthFrameEvent. Ожидание этого события можно реализовать посредством функции WaitForMultipleObjects() или WaitForSingleObject().

Получить же сам буфер из устройства можно с помощью метода NuiImageStreamGetNextFrame(), которому в качестве параметров нужно передать:
  • Дескриптор потока захвата
  • Период ожидания буфера в миллисекундах
  • Указатель на структуру NUI_IMAGE_FRAME, в которую будет записана информация о полученном буфере

virtual HRESULT NuiImageStreamGetNextFrame(
  _In_ HANDLE hStream,
  _In_ DWORD dwMillisecondsToWait,
  _Deref_out_ CONST NUI_IMAGE_FRAME **ppcImageFrame
);

В полученном экземпляре NUI_IMAGE_FRAME нас, на данный момент, больше всего интересует поле NuiImageBuffer *pFrameTexture.
Для работы непосредственно с данными буфера, необходимо вызвать метод LockRect(). У метода LockRect() четыре параметра, из которых в beta-версии API используется два.
В качестве первого параметра нужно передать 0, в качестве второго – указатель на структуру KINECT_LOCKED_RECT, в которую, после успешного завершения работы функции, будут записаны данные для работы с буфером. В качестве третьего параметра передаем NULL, в качестве четвертого – 0.
STDMETHODIMP LockRect(
    UINT Level,
    KINECT_LOCKED_RECT* pLockedRect,
    CONST RECT* pRectUsuallyNull,
    DWORD Flags
);

Далее в структуре KINECT_LOCKED_RECT нас интересует поле pBits, которое содержит непосредственно данные глубины. Для каждого пиксела изображения в буфере отводится 2 байта. Судя по FAQ на официальном форуме, формат данных следующий:
• При использовании флага NUI_INITIALIZE_FLAG_USES_DEPTH_AND_PLAYER_INDEX, 12 младших бит отводится для значения глубины и оставшиеся 3 бита – для индекса игрока, старший бит не используется.
• При использовании флага NUI_INITIALIZE_FLAG_USES_DEPTH, 12 младших бит отводится для значения глубины, остальные не используются.
Для получения изображения в оттенках серого нам необходимо нормализовать значение глубины таким образом, чтобы получить значение в диапазоне от 0 до 255. Сделать это можно вот так:
USHORT RealDepth = (s & 0xfff8) >> 3;
BYTE l = 255 - (BYTE)(256*RealDepth/0x0fff);
RGBQUAD q;
q.rgbRed = q.rgbBlue = q.rgbGreen = l;
return q;

Для того, чтобы завершить работу с полученным изображением, необходимо освободить память, выделенную под буфер. Сделать это можно с помощью метода NuiImageStreamReleaseFrame(), который в качестве параметров принимает дескриптор потока и указатель на экземпляр NUI_IMAGE_FRAME.

Давайте подытожим то, что мы имеем на данный момент:
  • Для того, чтобы начать захват, надо инициализировать устройство методом NuiInitialize().
  • Затем надо стартовать поток захвата с помощью метода NuiImageStreamOpen().
  • При получении нового изображения вызывается событие, дескриптор которого мы передали в NuiImageStreamOpen().
  • После вызова события можно получить фрейм с помощью метода NuiImageStreamGetNextFrame().
  • Затем выполнить захват буфера с помощью метода NuiImageBuffer::LockRect().
  • После этого пройтись по буферу и получить цвет каждого пиксела, нормализовав значение глубины.
  • Освободить буфер с помощью метода NuiImageStreamReleaseFrame().
  • Для того, чтобы остановить захват изображений с устройства, надо деинициализировать его методом NuiShutdown().

Теперь посмотрим, как можно применить все это на практике:
wxKinectHelper.h
class KinectHelper
{
    ...    
    const wxSize & GetFrameSize();
    BYTE * CreateDataBuffer();
    void FreeDataBuffer(BYTE * data);
    size_t GetDataBufferLength();

    bool StartGrabbing(size_t deviceIndex, HANDLE hDepthFrameEvent);
    bool ReadKinectFrame(size_t deviceIndex, BYTE * data);

    bool IsDeviceOK(size_t deviceIndex);
    bool IsGrabbingStarted(size_t deviceIndex);

    static RGBQUAD Nui_ShortToQuad_Depth( USHORT s );
protected:
    InstanceVector m_Instances;
    wxSize m_FrameSize;
    ...
};

wxKinectHelper.cpp
...
void ReadLockedRect(KINECT_LOCKED_RECT & LockedRect, int w, int h, BYTE * data)
{
    if( LockedRect.Pitch != 0 )
    {
        BYTE * pBuffer = (BYTE*) LockedRect.pBits;

        // draw the bits to the bitmap
        USHORT * pBufferRun = (USHORT*) pBuffer;
        for( int y = 0 ; y < h ; y++ )
        {
            for( int x = 0 ; x < w ; x++ )
            {
                RGBQUAD quad = KinectHelper::Nui_ShortToQuad_Depth( *pBufferRun );
                pBufferRun++;
                int offset = (w * y + x) * 3;
                data[offset + 0] = quad.rgbRed;
                data[offset + 1] = quad.rgbGreen;
                data[offset + 2] = quad.rgbBlue;
            }
        }
    }
}
...
BYTE * KinectHelper::CreateDataBuffer()
{
    size_t length = GetDataBufferLength();
    BYTE * result = (BYTE*)CoTaskMemAlloc(length);
    memset(result, 0, length);
    return result;
}

size_t KinectHelper::GetDataBufferLength()
{
    return m_FrameSize.GetWidth() * m_FrameSize.GetHeight() * 3;
}

void KinectHelper::FreeDataBuffer(BYTE * data)
{
    CoTaskMemFree((LPVOID)data);
}

void KinectHelper::Finalize()
{
    for(InstanceVector::const_iterator i = m_Instances.begin();
    i != m_Instances.end(); i++)
    {
        if((*i).first && (*i).second)
        {
            (*i).first->NuiShutdown();
            MSR_NuiDestroyInstance((*i).first);
        }
    }
}

bool KinectHelper::StartGrabbing(size_t deviceIndex, HANDLE hDepthFrameEvent)
{
    do
    {
        InstanceInfo * info = GetInstanceByIndex(deviceIndex);
        if(!info || !info->first) break;
        if(FAILED(info->first->NuiInitialize(
    NUI_INITIALIZE_FLAG_USES_DEPTH_AND_PLAYER_INDEX))) break;
        if(FAILED(info->first->NuiImageStreamOpen(
    NUI_IMAGE_TYPE_DEPTH_AND_PLAYER_INDEX,
            NUI_IMAGE_RESOLUTION_320x240, 0,
            3,
            hDepthFrameEvent,
            &info->second))) break;
    }
    while(false);
    return false;
}

bool KinectHelper::IsDeviceOK(size_t deviceIndex)
{
    return GetInstanceByIndex(deviceIndex) != NULL;
}

bool KinectHelper::IsGrabbingStarted(size_t deviceIndex)
{
    InstanceInfo * info = GetInstanceByIndex(deviceIndex);
    return (info != NULL && info->first != NULL && info->second != NULL);
}

bool KinectHelper::ReadKinectFrame(size_t deviceIndex, BYTE * data)
{
    do
    {
        if(deviceIndex < 0) break;
        InstanceInfo * info = GetInstanceByIndex((size_t)deviceIndex);
        if(!info || !info->second) break;
        const NUI_IMAGE_FRAME * pImageFrame;
        if(FAILED(NuiImageStreamGetNextFrame(
    info->second, 200, &pImageFrame))) break;
        NuiImageBuffer * pTexture = pImageFrame->pFrameTexture;
        KINECT_LOCKED_RECT LockedRect;
        pTexture->LockRect( 0, &LockedRect, NULL, 0 );
        ReadLockedRect(LockedRect, m_FrameSize.GetWidth(),
    m_FrameSize.GetHeight(), data);
        NuiImageStreamReleaseFrame(info->second, pImageFrame);
        return true;
    }
    while(false);
    return false;
}

RGBQUAD KinectHelper::Nui_ShortToQuad_Depth( USHORT s )
{
    USHORT RealDepth = (s & 0xfff8) >> 3;
    BYTE l = 255 - (BYTE)(256*RealDepth/0x0fff);
    RGBQUAD q;
    q.rgbRed = q.rgbBlue = q.rgbGreen = l;
    return q;
}

KinectTestMainFrame.h
class KinectTestMainFrame: public wxFrame, public wxThreadHelper
{
    ...
    void OnDEVICELISTBOXSelected( wxCommandEvent& event );
    ...
    void ShowDevices();
    void StopGrabbing();
    HANDLE m_NewDepthFrameEvent;
    KinectHelper * m_KinectHelper;
    BYTE * m_pDepthBuffer;
    wxImage * m_CurrentImage;
    int m_SelectedDeviceIndex;
};

KinectTestMainFrame.cpp
...
BEGIN_EVENT_TABLE( KinectTestMainFrame, wxFrame )
    EVT_LISTBOX( ID_DEVICE_LISTBOX, KinectTestMainFrame::OnDEVICELISTBOXSelected )
END_EVENT_TABLE()
...
void KinectTestMainFrame::Init()
{
    m_NewDepthFrameEvent = CreateEvent( NULL, TRUE, FALSE, NULL );
    m_KinectHelper = new KinectHelper;
    m_pDepthBuffer = m_KinectHelper->CreateDataBuffer();
    m_CurrentImage = new wxImage(
        m_KinectHelper->GetFrameSize().GetWidth(),
        m_KinectHelper->GetFrameSize().GetHeight(),
        m_pDepthBuffer, true);
    m_SelectedDeviceIndex = -1;
    m_MainSizer = NULL;
    m_DeviceListBox = NULL;
    m_Canvas = NULL;
}
...
KinectTestMainFrame::~KinectTestMainFrame()
{
    StopGrabbing();
    wxDELETE(m_CurrentImage);
    m_KinectHelper->FreeDataBuffer(m_pDepthBuffer);
    wxDELETE(m_KinectHelper);
}
...
wxThread::ExitCode KinectTestMainFrame::Entry()
{
    while(!GetThread()->TestDestroy())
    {
        int mEventIndex = WaitForMultipleObjects(
            1, &m_NewDepthFrameEvent, FALSE, 100);
        switch(mEventIndex)
        {
        case 0:
            {
                wxCriticalSectionLocker lock(m_CS);
                m_KinectHelper->ReadKinectFrame(
                    m_SelectedDeviceIndex, m_pDepthBuffer);
                m_Canvas->Refresh();
            }
            break;
        default:
            break;
        }
    }
    return NULL;
}
...
void KinectTestMainFrame::OnDEVICELISTBOXSelected( wxCommandEvent& event )
{
    do
    {
        StopGrabbing();
        size_t deviceIndex =
            (size_t)m_DeviceListBox->GetClientData(event.GetInt());
        if(deviceIndex < 0 || deviceIndex >
            m_KinectHelper->GetDeviceCount()) break;
        m_SelectedDeviceIndex = deviceIndex;
        if(!m_KinectHelper->IsDeviceOK(deviceIndex)) break;
        if(!m_KinectHelper->IsGrabbingStarted(deviceIndex))
        {
            m_KinectHelper->StartGrabbing(
                deviceIndex, m_NewDepthFrameEvent);
            if(CreateThread() != wxTHREAD_NO_ERROR) break;
            m_Canvas->SetCurrentImage(m_CurrentImage);
            GetThread()->Run();
        }
    }
    while(false);
}

void KinectTestMainFrame::StopGrabbing()
{
    if(GetThread())
    {
        if(GetThread()->IsAlive())
        {
            GetThread()->Delete();
        }
        if(m_kind == wxTHREAD_JOINABLE)
        {
            if(GetThread()->IsAlive())
            {
                GetThread()->Wait();
            }
            wxDELETE(m_thread);
        }
        else
        {
            m_thread = NULL;
        }
    }
}

При старте приложения Объект wxKinectHelper выделяет память для буфера глубины, в соответствии с разрешением (320x240x24). Затем выделенная область памяти передается в качестве RGB-буфера объекту m_CurrentImage.
При выделении устройства в списке доступных устройств запускается поток захвата изображения с устройства, а объект m_CurrentImage ассоциируется с канвой.
В методе Entry() происходит ожидание нового изображения с устройства. Когда изображение доступно, RGB-буфер заполняется новыми значениями, а затем вызывается перерисовка канвы.
В результате, после запуска приложения и нажатия на имя устройства в списке, мы получим что-то вроде этого (Рис. 6):



Получение цветного изображения с камеры


Для получения изображения с камеры устройства необходимо указать флаг NUI_INITIALIZE_FLAG_USES_COLOR при вызове метода NuiInitialize(), а также указать разрешение не ниже 640x480 при вызове метода NuiImageStreamOpen().
В результате код будет выглядеть приблизительно так:
if(FAILED(info->first->NuiInitialize(
    NUI_INITIALIZE_FLAG_USES_DEPTH_AND_PLAYER_INDEX|
    NUI_INITIALIZE_FLAG_USES_COLOR))) break;
if(FAILED(info->first->NuiImageStreamOpen(NUI_IMAGE_TYPE_COLOR,
    NUI_IMAGE_RESOLUTION_640x480, 0,
    3,
    hDepthFrameEvent,
    &info->second))) break;

Соответственно, данные в структуре KINECT_LOCKED_RECT содержатся в формате RGBA (для доступа к данным вполне подходит структура RGBQUAD, доступная в SDK). Таким образом, код для получения RGB-буфера будет выглядеть подобным образом:
if( LockedRect.Pitch != 0 )
{
    BYTE * pBuffer = (BYTE*) LockedRect.pBits;
    for( int y = 0 ; y < h ; y++ )
    {
        for( int x = 0 ; x < w ; x++ )
        {
            RGBQUAD * quad = ((RGBQUAD*)pBuffer) + x;
            int offset = (w * y + x) * 3;
            data[offset + 0] = quad->rgbRed;
            data[offset + 1] = quad->rgbGreen;
            data[offset + 2] = quad->rgbBlue;
        }
        pBuffer += LockedRect.Pitch;
    }
}

Отслеживание положения игроков (Skeleton Tracking)


Алгоритм получения и отображения сегментов «скелета» игроков отличается от получения обычных изображений.

Для того, чтобы включить возможность получения сегментов скелета, необходимо методу NuiInitialize() передать флаг, содержащий значение NUI_INITIALIZE_FLAG_USES_SKELETON, а затем вызвать метод NuiSkeletonTrackingEnable(), которому в качестве первого параметра передать дескриптор события, которое будет вызвано при получении новой порции данных с сегментами, а в качестве второго параметра – набор флагов (beta-версия SDK игнорирует этот параметр, поэтому можно передавать 0).

Для завершения потока получения сегментов скелета, необходимо вызвать метод NuiSkeletonTrackingDisable().
В коде это будет выглядеть таким образом:
if(FAILED(info->first->NuiSkeletonTrackingEnable(hSkeletonFrameEvent, 0)))
    { /* error */ };

Получить буфер данных, содержащий информацию о положении игроков можно с помощью метода NuiSkeletonGetNextFrame(), который в качестве параметров принимает:
  • Период ожидания буфера (в миллисекундах)
  • Указатель на структуру NUI_SKELETON_FRAME, которая, в случае успешного завершения работы функции, будет содержать указатель на буфер данных.

После вызова метода NuiSkeletonGetNextFrame() мы получаем экземпляр структуры NUI_SKELETON_FRAME. Давайте рассмотрим ее более подробно.
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;

  • liTimeStamp – дата\время получения буфера глубины, из которого были получены сегменты скелета.
  • dwFlag – битовая маска, содержащая флаги.
  • vFloorClipPlane –координаты пола (вычисленные внутри библиотеки), которые были использованы для отсечения всего, что ниже пола.
  • vNormalToGravity – вектор нормали.
  • dwFrameNumber – номер кадра.
  • SkeletonData – массив структур NUI_SKELETON_DATA, каждая из которых содержит данные о секгментах скелета одного игрока.

Как видно из описания структуры NUI_SKELETON_FRAME, поддерживается ограниченное количество игроков (в текущей версии SDK значение NUI_SKELETON_COUNT равно 6).

Теперь рассмотрим структуру NUI_SKELETON_DATA:
struct _NUI_SKELETON_DATA {
    NUI_SKELETON_TRACKING_STATE eTrackingState;
    DWORD dwTrackingID;
    DWORD dwEnrollmentIndex;
    DWORD dwUserIndex;
    Vector4 Position;
    Vector4 SkeletonPositions[NUI_SKELETON_POSITION_COUNT];
    NUI_SKELETON_POSITION_TRACKING_STATE
        eSkeletonPositionTrackingState[NUI_SKELETON_POSITION_COUNT];
    DWORD dwQualityFlags;
} NUI_SKELETON_DATA;

  • eTrackingState – значение из перечисления NUI_SKELETON_TRACKING_STATE. Может указывать на то, что игрок не найден, найдены только координаты игрока (без сегментов скелета) или же что найдены координаты и сегменты скелета.
  • dwEnrollmentIndex – судя по документации (стр. 20), не используется в текущей версии.
  • dwUserIndex – в текущей версии SDK всегда равно XUSER_INDEX_NONE.
  • dwTrackingID – номер отслеживаемого игрока.
  • Position – координаты игрока.
  • SkeletonPositions – список координат сочленений сегментов скелета
  • eSkeletonPositionTrackingState – список флагов, которые указывают, найдены ли сочленения сегментов скелета.

Как видно из описания структуры NUI_SKELETON_DATA, количество поддерживаемых сочленений сегментов ограничено числом, равным NUI_SKELETON_POSITION_COUNT.
Теперь рассмотрим саму реализацию получения координат игроков, с использованием описанного выше API:

KinectHelper.h
...
struct KinectStreams
{
    HANDLE hDepth;
    HANDLE hColor;
    HANDLE hSkeleton;
    KinectStreams() : hDepth(NULL), hColor(NULL), hSkeleton(NULL) {}
};
...

KinectHelper.cpp
void KinectHelper::Finalize()
{
    for(InstanceVector::const_iterator i = m_Instances.begin();
        i != m_Instances.end(); i++)
    {
        if((*i).first)
        {
            ...
            if((*i).second.hSkeleton != NULL)
            {
                (*i).first->NuiSkeletonTrackingDisable();
            }
            MSR_NuiDestroyInstance((*i).first);
        }
    }
}

bool KinectHelper::StartGrabbing(size_t deviceIndex,
        HANDLE hDepthFrameEvent,
        HANDLE hColorFrameEvent,
        HANDLE hSkeletonFrameEvent)
{
    do
    {
        if(hDepthFrameEvent == NULL &&
            hColorFrameEvent == NULL &&
            hSkeletonFrameEvent == NULL) break;
        InstanceInfo * info = GetInstanceByIndex(deviceIndex);
        if(!info || !info->first) break;
        if(FAILED(info->first->NuiInitialize(
            NUI_INITIALIZE_FLAG_USES_DEPTH_AND_PLAYER_INDEX |
            NUI_INITIALIZE_FLAG_USES_COLOR |
            NUI_INITIALIZE_FLAG_USES_SKELETON))) break;
        ...
        if(hSkeletonFrameEvent != NULL)
        {
            if(FAILED(info->first->NuiSkeletonTrackingEnable(
                hSkeletonFrameEvent, 0))) break;
            info->second.hSkeleton = hSkeletonFrameEvent;
        }
    }
    while(false);
    return false;
}

void * KinectHelper::ReadSkeletonFrame(size_t deviceIndex)
{
    do
    {
        if(deviceIndex < 0) break;
        InstanceInfo * info = GetInstanceByIndex((size_t)deviceIndex);
        if(!info || !info->second.hColor) break;
        NUI_SKELETON_FRAME * frame = new NUI_SKELETON_FRAME;
        if(FAILED(info->first->NuiSkeletonGetNextFrame(200, frame))) break;
        return frame;
    }
    while(false);
    return NULL;
}

Отрисовка скелета игрока

На данный момент у нас есть информация о том, как:
  • Инициализировать устройство для получения положения игроков
  • Стартовать поток захвата положения игроков
  • Получить буфер, содержащий координаты сегментов скелетов игроков
  • Добраться до нужных данных в буфере
  • Остановить поток получения положения игроков.

Теперь надо полученные данные как-то показать в приложении.
Перед тем, как что-то делать с данными из NUI_SKELETON_FRAME, нужно отправить их на предобработку. Предобработку выполняет метод NuiTransformSmooth() – он отфильтровывает координаты сегментов для того, чтобы избежать подергиваний и резких движений. В качестве параметров метод NuiTransformSmooth() принимает указатель на структуру NUI_SKELETON_FRAME и, опционально, указатель на объект NUI_TRANSFORM_SMOOTH_PARAMETERS, содержащий параметры предобработки.
HRESULT NuiTransformSmooth(
    NUI_SKELETON_FRAME *pSkeletonFrame,
    CONST NUI_TRANSFORM_SMOOTH_PARAMETERS *pSmoothingParams
);

Для того, чтобы отобразить сегменты скелета, необходимо преобразовать их координаты в координаты изображения. Сделать это можно с помощью метода NuiTransformSkeletonToDepthImageF(), который в качестве параметров принимает:
  • Координаты точки сочленения сегментов скелета в виде структуры Vector4.
  • Указатель на переменную, в которую будет записана X-координата.
  • Указатель на переменную, в которую будет записана Y-координата.

VOID NuiTransformSkeletonToDepthImageF(
  Vector4 vPoint,
  _Out_ FLOAT *pfDepthX,
  _Out_ FLOAT *pfDepthY
);

После получения координат всех точек сочленения, можно их отобразить на канве с помощью обычных графических примитивов.

Вот как выглядит на практике код для отображения сегментов скелета:
SkeletonPainter.h
#pragma once

#include <wx/wx.h>

class SkeletonPainterImpl;

class SkeletonPainter
{
public:
    SkeletonPainter();
    ~SkeletonPainter();
    void DrawSkeleton(wxDC & dc, void * data);
private:
    SkeletonPainterImpl * m_Impl;
};

SkeletonPainter.cpp
#include "SkeletonPainter.h"
#if defined(__WXMSW__)
#include "SkeletonPainterImplMSW.h"
#endif

SkeletonPainter::SkeletonPainter()
{
#if defined(__WXMSW__)
    m_Impl = new SkeletonPainterImplMSW;
#else
    m_Impl = NULL;
#endif
}

SkeletonPainter::~SkeletonPainter()
{
    wxDELETE(m_Impl);
}

void SkeletonPainter::DrawSkeleton(wxDC & dc, void * data)
{
    if(m_Impl)
    {
        m_Impl->DrawSkeleton(dc, data);
    }
}

SkeletonPainterImpl.h
#pragma once

#include <wx/wx.h>

class SkeletonPainterImpl
{
public:
    virtual ~SkeletonPainterImpl() {}
    virtual void DrawSkeleton(wxDC & dc, void * data) = 0;
};

SkeletonPainterImplMSW.h
#pragma once

#include "SkeletonPainterImpl.h"
#include "msr_nuiapi.h"

class SkeletonPainterImplMSW : public SkeletonPainterImpl
{
public:
    ~SkeletonPainterImplMSW();
    void DrawSkeleton(wxDC & dc, void * data);
private:
    void Nui_DrawSkeleton(wxDC & dc, NUI_SKELETON_DATA * data, size_t index);
    void Nui_DrawSkeletonSegment(wxDC & dc, wxPoint * points, int numJoints, ... );
    
    static wxPen m_SkeletonPen[6];
};

SkeletonPainterImplMSW.cpp
#include "SkeletonPainterImplMSW.h"

wxPen SkeletonPainterImplMSW::m_SkeletonPen[6] =
{
    wxPen(wxColor(255, 0, 0), wxSOLID),
...
};

SkeletonPainterImplMSW::~SkeletonPainterImplMSW()
{
}

void SkeletonPainterImplMSW::DrawSkeleton(wxDC & dc, void * data)
{
    do
    {
        NUI_SKELETON_FRAME * frame =
            reinterpret_cast<NUI_SKELETON_FRAME*>(data);
        if(!frame) break;
        int skeletonCount(0);
        for( int i = 0 ; i < NUI_SKELETON_COUNT ; i++ )
        {
            if( frame->SkeletonData[i].eTrackingState ==
                NUI_SKELETON_TRACKED )
            {
                skeletonCount++;
            }
        }
        if(!skeletonCount) break;
        NuiTransformSmooth(frame, NULL);
        for(size_t i = 0 ; i < NUI_SKELETON_COUNT ; i++ )
        {
            if(frame->SkeletonData[i].eTrackingState ==
                NUI_SKELETON_TRACKED)
            {
                Nui_DrawSkeleton(dc, &frame->SkeletonData[i], i );
            }
        }
    }
    while(false);
}

void SkeletonPainterImplMSW::Nui_DrawSkeleton(wxDC & dc,
        NUI_SKELETON_DATA * data, size_t index)
{
    wxPoint points[NUI_SKELETON_POSITION_COUNT];
    float fx(0), fy(0);
    wxSize imageSize = dc.GetSize();
    for (size_t i = 0; i < NUI_SKELETON_POSITION_COUNT; i++)
    {
        NuiTransformSkeletonToDepthImageF(
            data->SkeletonPositions[i], &fx, &fy);
        points[i].x = (int) ( fx * imageSize.GetWidth() + 0.5f );
        points[i].y = (int) ( fy * imageSize.GetHeight() + 0.5f );
    }

    Nui_DrawSkeletonSegment(dc,points,4,
        NUI_SKELETON_POSITION_HIP_CENTER,
        NUI_SKELETON_POSITION_SPINE,
        NUI_SKELETON_POSITION_SHOULDER_CENTER,
        NUI_SKELETON_POSITION_HEAD);
    Nui_DrawSkeletonSegment(dc,points,5,
        NUI_SKELETON_POSITION_SHOULDER_CENTER,
        NUI_SKELETON_POSITION_SHOULDER_LEFT,
        NUI_SKELETON_POSITION_ELBOW_LEFT,
        NUI_SKELETON_POSITION_WRIST_LEFT,
        NUI_SKELETON_POSITION_HAND_LEFT);
    Nui_DrawSkeletonSegment(dc,points,5,
        NUI_SKELETON_POSITION_SHOULDER_CENTER,
        NUI_SKELETON_POSITION_SHOULDER_RIGHT,
        NUI_SKELETON_POSITION_ELBOW_RIGHT,
        NUI_SKELETON_POSITION_WRIST_RIGHT,
        NUI_SKELETON_POSITION_HAND_RIGHT);
    Nui_DrawSkeletonSegment(dc,points,5,
        NUI_SKELETON_POSITION_HIP_CENTER,
        NUI_SKELETON_POSITION_HIP_LEFT,
        NUI_SKELETON_POSITION_KNEE_LEFT,
        NUI_SKELETON_POSITION_ANKLE_LEFT,
        NUI_SKELETON_POSITION_FOOT_LEFT);
    Nui_DrawSkeletonSegment(dc,points,5,
        NUI_SKELETON_POSITION_HIP_CENTER,
        NUI_SKELETON_POSITION_HIP_RIGHT,
        NUI_SKELETON_POSITION_KNEE_RIGHT,
        NUI_SKELETON_POSITION_ANKLE_RIGHT,
        NUI_SKELETON_POSITION_FOOT_RIGHT);
}

void SkeletonPainterImplMSW::Nui_DrawSkeletonSegment(wxDC & dc,
        wxPoint * points, int numJoints, ...)
{
    va_list vl;
    va_start(vl,numJoints);
    wxPoint segmentPositions[NUI_SKELETON_POSITION_COUNT];

    for (int iJoint = 0; iJoint < numJoints; iJoint++)
    {
        NUI_SKELETON_POSITION_INDEX jointIndex =
            va_arg(vl,NUI_SKELETON_POSITION_INDEX);
        segmentPositions[iJoint].x = points[jointIndex].x;
        segmentPositions[iJoint].y = points[jointIndex].y;
    }

    dc.SetPen(*wxBLUE_PEN);
    dc.DrawLines(numJoints, segmentPositions);

    va_end(vl);
}

Использование класса SkeletonPainter в приложении будет выглядеть подобным образом:

KinectTestMainFrame.h
...
class KinectTestMainFrame: public wxFrame, public wxThreadHelper
{    
    ...
    HANDLE m_NewSkeletonFrameEvent;
    wxImage m_SkeletonImage;
    ...
};
...

KinectTestMainFrame.cpp
...
wxThread::ExitCode KinectTestMainFrame::Entry()
{
    HANDLE eventHandles[3];
    eventHandles[0] = m_NewDepthFrameEvent;
    eventHandles[1] = m_NewColorFrameEvent;
    eventHandles[2] = m_NewSkeletonFrameEvent;
    SkeletonPainter painter;
    while(!GetThread()->TestDestroy())
    {
        int mEventIndex = WaitForMultipleObjects(
            _countof(eventHandles), eventHandles, FALSE, 100);
        switch(mEventIndex)
        {
        ...
        case 2:
            {
                void * frame = m_KinectHelper->ReadSkeletonFrame(
                    m_SelectedDeviceIndex);
                if(frame)
                {
                    wxBitmap bmp(
                        m_SkeletonImage.GetWidth(),
                        m_SkeletonImage.GetHeight());
                    wxMemoryDC dc(bmp);
                    painter.DrawSkeleton(dc, frame);
                    m_KinectHelper->ReleaseSkeletonFrame(frame);
                    dc.SelectObject(wxNullBitmap);
                    m_SkeletonImage = bmp.ConvertToImage();
                    m_SkeletonCanvas->Refresh();
                }
            }
            break;
        default:
            break;
        }
    }
    return NULL;
}

В результате описанных выше действий у нас должен получиться приблизительно такой результат (Рис. 7):



Избавляемся от платформо-зависимого кода в приложении



Описанный выше пример хорош всем, кроме того, что в проекте для разработки интерфейса пользователя, используется кросс-платформенная библиотека, а часть GUI-кода написана с использованием API, которое специфично только для Windows.

Для работы с Kinect существует несколько сторонних библиотек, например libfreenect или OpenNI, но уже на данном этапе у нас сложилась ситуация, что код приложения завязан на использовании SDK от Microsoft.

Для того, чтобы решить это досадное недоразумение можно вынести код, связанный с получением изображений с устройства в отдельный класс граббера, а функционал класса KinectHelper ограничить получением списка устройств и созданием экземпляров граббера:

KinectGrabberBase.h
#pragma once

#include <wx/wx.h>

class KinectGrabberBase
{
public:
    KinectGrabberBase(wxEvtHandler * handler);
    virtual ~KinectGrabberBase();

    virtual bool GrabDepthFrame(unsigned char * data) = 0;
    virtual bool GrabColorFrame(unsigned char * data) = 0;
    virtual void * GrabSkeletonFrame() = 0;

    virtual bool Start() = 0;
    virtual bool Stop() = 0;
    virtual bool IsStarted() = 0;

    const wxSize & GetDepthFrameSize();
    const wxSize & GetColorFrameSize();
protected:
    wxSize m_DepthFrameSize;
    wxSize m_ColorFrameSize;
    wxEvtHandler * m_Handler;
};

BEGIN_DECLARE_EVENT_TYPES()

DECLARE_LOCAL_EVENT_TYPE(KINECT_DEPTH_FRAME_RECEIVED, -1)
DECLARE_LOCAL_EVENT_TYPE(KINECT_COLOR_FRAME_RECEIVED, -1)
DECLARE_LOCAL_EVENT_TYPE(KINECT_SKELETON_FRAME_RECEIVED, -1)

END_DECLARE_EVENT_TYPES()

KinectGrabberBase.cpp
#include "KinectGrabberBase.h"

DEFINE_EVENT_TYPE(KINECT_DEPTH_FRAME_RECEIVED)
DEFINE_EVENT_TYPE(KINECT_COLOR_FRAME_RECEIVED)
DEFINE_EVENT_TYPE(KINECT_SKELETON_FRAME_RECEIVED)
...

KinectGrabberMSW.h
#pragma once

#include "KinectGrabberBase.h"
#include "MSR_NuiApi.h"

class KinectGrabberMSW : public KinectGrabberBase, public wxThreadHelper
{
...
private:
    virtual wxThread::ExitCode Entry();
    BYTE * CreateDepthDataBuffer();
    BYTE * CreateColorDataBuffer();
    size_t GetDepthDataBufferLength();
    size_t GetColorDataBufferLength();
    void FreeDataBuffer(BYTE * data);
    bool ReadDepthFrame();
    bool ReadColorFrame();
    bool ReadSkeletonFrame();

    void ReadDepthLockedRect(KINECT_LOCKED_RECT & LockedRect,
        int w, int h, BYTE * data);
    void ReadColorLockedRect(KINECT_LOCKED_RECT & LockedRect,
        int w, int h, BYTE * data);
    static RGBQUAD Nui_ShortToQuad_Depth( USHORT s );
    void ResetEvents();
    void StopThread();
    bool CopyLocalBuffer(BYTE * src, BYTE * dst, size_t count);
    HANDLE m_NewDepthFrameEvent;
    HANDLE m_NewColorFrameEvent;
    HANDLE m_NewSkeletonFrameEvent;
    HANDLE m_DepthStreamHandle;
    HANDLE m_ColorStreamHandle;
    BYTE * m_DepthBuffer;
    BYTE * m_ColorBuffer;
    INuiInstance * m_Instance;
    size_t m_DeviceIndex;
    NUI_SKELETON_FRAME m_SkeletonFrame;
};

KinectGrabberMSW.cpp
#include "KinectGrabberMSW.h"

KinectGrabberMSW::KinectGrabberMSW(wxEvtHandler * handler, size_t deviceIndex)
    : KinectGrabberBase(handler), m_DeviceIndex(deviceIndex), m_Instance(NULL)
{
    m_DepthBuffer = CreateDepthDataBuffer();
    m_ColorBuffer = CreateColorDataBuffer();
    ResetEvents();
    do
    {
        if(FAILED(MSR_NuiCreateInstanceByIndex((int)m_DeviceIndex, &m_Instance))) break;
        if(FAILED(m_Instance->NuiInitialize(
            NUI_INITIALIZE_FLAG_USES_DEPTH_AND_PLAYER_INDEX |
            NUI_INITIALIZE_FLAG_USES_COLOR |
            NUI_INITIALIZE_FLAG_USES_SKELETON))) break;
    }
    while(false);
}
...
void * KinectGrabberMSW::GrabSkeletonFrame()
{
    do
    {
        if(!GetThread() || !GetThread()->IsAlive() ||
            !m_Instance || !m_NewSkeletonFrameEvent) break;
        return &m_SkeletonFrame;
    }
    while(false);
    return NULL;
}

bool KinectGrabberMSW::Start()
{
    do
    {
        if(!m_Instance) break;
        if(GetThread() && GetThread()->IsAlive()) break;

        if(CreateThread() != wxTHREAD_NO_ERROR) break;

        m_NewDepthFrameEvent = CreateEvent( NULL, TRUE, FALSE, NULL );
        m_NewColorFrameEvent = CreateEvent( NULL, TRUE, FALSE, NULL );
        m_NewSkeletonFrameEvent = CreateEvent( NULL, TRUE, FALSE, NULL );

        if(FAILED(m_Instance->NuiImageStreamOpen(
                NUI_IMAGE_TYPE_DEPTH_AND_PLAYER_INDEX,
                NUI_IMAGE_RESOLUTION_320x240, 0,
                3,
                m_NewDepthFrameEvent,
                &m_DepthStreamHandle))) break;
        if(FAILED(m_Instance->NuiImageStreamOpen(NUI_IMAGE_TYPE_COLOR,
                NUI_IMAGE_RESOLUTION_640x480, 0,
                4,
                m_NewColorFrameEvent,
                &m_ColorStreamHandle))) break;
        if(FAILED(m_Instance->NuiSkeletonTrackingEnable(
            m_NewSkeletonFrameEvent, 0))) break;

        GetThread()->Run();

        return true;
    }
    while(false);
    return false;
}
...
wxThread::ExitCode KinectGrabberMSW::Entry()
{
    HANDLE eventHandles[3];
    eventHandles[0] = m_NewDepthFrameEvent;
    eventHandles[1] = m_NewColorFrameEvent;
    eventHandles[2] = m_NewSkeletonFrameEvent;
    while(!GetThread()->TestDestroy())
    {
        int mEventIndex = WaitForMultipleObjects(
            _countof(eventHandles), eventHandles, FALSE, 100);
        switch(mEventIndex)
        {
        case 0: ReadDepthFrame(); break;
        case 1: ReadColorFrame(); break;
        case 2: ReadSkeletonFrame(); break;
        default:
            break;
        }
    }
    return NULL;
}
...
void KinectGrabberMSW::StopThread()
{
    if(GetThread())
    {
        if(GetThread()->IsAlive())
        {
            GetThread()->Delete();
        }
        if(m_kind == wxTHREAD_JOINABLE)
        {
            if(GetThread()->IsAlive())
            {
                GetThread()->Wait();
            }
            wxDELETE(m_thread);
        }
        else
        {
            m_thread = NULL;
        }
    }
    wxYield();
}

bool KinectGrabberMSW::ReadDepthFrame()
{
    do
    {
        if(m_DeviceIndex < 0 || !m_Instance) break;
        const NUI_IMAGE_FRAME * pImageFrame;
        if(FAILED(NuiImageStreamGetNextFrame(
            m_DepthStreamHandle, 200, &pImageFrame))) break;
        NuiImageBuffer * pTexture = pImageFrame->pFrameTexture;
        KINECT_LOCKED_RECT LockedRect;
        pTexture->LockRect( 0, &LockedRect, NULL, 0 );
        ReadDepthLockedRect(LockedRect,
            m_DepthFrameSize.GetWidth(),
            m_DepthFrameSize.GetHeight(),
            m_DepthBuffer);
        NuiImageStreamReleaseFrame(m_DepthStreamHandle, pImageFrame);
        if(m_Handler)
        {
            wxCommandEvent e(KINECT_DEPTH_FRAME_RECEIVED, wxID_ANY);
            e.SetInt(m_DeviceIndex);
            m_Handler->AddPendingEvent(e);
        }
        return true;
    }
    while(false);
    return false;
}

bool KinectGrabberMSW::ReadColorFrame()
{
    do
    {
        if(m_DeviceIndex < 0 || !m_Instance) break;
        const NUI_IMAGE_FRAME * pImageFrame;
        if(FAILED(NuiImageStreamGetNextFrame(
            m_ColorStreamHandle, 200, &pImageFrame))) break;
        NuiImageBuffer * pTexture = pImageFrame->pFrameTexture;
        KINECT_LOCKED_RECT LockedRect;
        pTexture->LockRect( 0, &LockedRect, NULL, 0 );
        ReadColorLockedRect(LockedRect,
            m_ColorFrameSize.GetWidth(),
            m_ColorFrameSize.GetHeight(),
            m_ColorBuffer);
        NuiImageStreamReleaseFrame(m_ColorStreamHandle, pImageFrame);
        if(m_Handler)
        {
            wxCommandEvent e(KINECT_COLOR_FRAME_RECEIVED, wxID_ANY);
            e.SetInt(m_DeviceIndex);
            m_Handler->AddPendingEvent(e);
        }
        return true;
    }
    while(false);
    return false;
}

bool KinectGrabberMSW::ReadSkeletonFrame()
{
    do
    {
        if(m_DeviceIndex < 0 || !m_Instance) break;
        if(FAILED(m_Instance->NuiSkeletonGetNextFrame(200, &m_SkeletonFrame))) break;
        if(m_Handler)
        {
            wxCommandEvent e(KINECT_SKELETON_FRAME_RECEIVED, wxID_ANY);
            e.SetInt(m_DeviceIndex);
            m_Handler->AddPendingEvent(e);
        }
        return true;
    }
    while(false);
    return false;
}

void KinectGrabberMSW::ReadDepthLockedRect(KINECT_LOCKED_RECT & LockedRect, int w, int h, BYTE * data)
{
    if( LockedRect.Pitch != 0 )
    {
        BYTE * pBuffer = (BYTE*) LockedRect.pBits;
        USHORT * pBufferRun = (USHORT*) pBuffer;
        for( int y = 0 ; y < h ; y++ )
        {
            for( int x = 0 ; x < w ; x++ )
            {
                RGBQUAD quad = KinectGrabberMSW::Nui_ShortToQuad_Depth( *pBufferRun );
                pBufferRun++;
                int offset = (w * y + x) * 3;
                data[offset + 0] = quad.rgbRed;
                data[offset + 1] = quad.rgbGreen;
                data[offset + 2] = quad.rgbBlue;
            }
        }
    }
}

void KinectGrabberMSW::ReadColorLockedRect(KINECT_LOCKED_RECT & LockedRect, int w, int h, BYTE * data)
{
    if( LockedRect.Pitch != 0 )
    {
        BYTE * pBuffer = (BYTE*) LockedRect.pBits;
        for( int y = 0 ; y < h ; y++ )
        {
            for( int x = 0 ; x < w ; x++ )
            {
                RGBQUAD * quad = ((RGBQUAD*)pBuffer) + x;
                int offset = (w * y + x) * 3;
                data[offset + 0] = quad->rgbRed;
                data[offset + 1] = quad->rgbGreen;
                data[offset + 2] = quad->rgbBlue;
            }
            pBuffer += LockedRect.Pitch;
        }
    }
}
...

KinectHelper.h
#pragma once

class KinectGrabberBase;

class KinectHelper
{
public:
    KinectHelper();
    ~KinectHelper();

    size_t GetDeviceCount();
    wxString GetDeviceName(size_t index);
    KinectGrabberBase * CreateGrabber(wxEvtHandler * handler, size_t index);
};

KinectHelper.cpp
...
wxString KinectHelper::GetDeviceName(size_t index)
{
    BSTR result;
    DWORD size;
    INuiInstance * instance(NULL);
    wxString name = wxT("Unknown Kinect Sensor");
    if(!FAILED(MSR_NuiCreateInstanceByIndex(index, &instance)))
    {
        if(instance != NULL)
        {
            if(instance->MSR_NuiGetPropsBlob(
                MsrNui::INDEX_UNIQUE_DEVICE_NAME,
                &result, &size))
            {
                name = result;
                SysFreeString(result);
            }
            MSR_NuiDestroyInstance(instance);
        }
    }
    return name;
}

KinectGrabberBase * KinectHelper::CreateGrabber(wxEvtHandler * handler, size_t index)
{
#if defined(__WXMSW__)
    return new KinectGrabberMSW(handler, index);
#else
    return NULL;
#endif
}
...

Выделение памяти для RGB-буферов, а также код для обеспечения захвата изображений в отдельном потоке, можно убрать из класса формы. Теперь класс формы будет выглядеть так:

KinectTestMainFrame.h
class KinectTestMainFrame: public wxFrame
{
    ...
    void OnDepthFrame(wxCommandEvent & event);
    void OnColorFrame(wxCommandEvent & event);
    void OnSkeletonFrame(wxCommandEvent & event);
    ...
    wxImage m_CurrentImage;
    int m_SelectedDeviceIndex;
    wxImage m_ColorImage;
    wxImage m_SkeletonImage;
    KinectGrabberBase * m_Grabber;
    ...
};

KinectTestMainFrame.cpp
...
BEGIN_EVENT_TABLE( KinectTestMainFrame, wxFrame )
    ...
    EVT_COMMAND (wxID_ANY, KINECT_DEPTH_FRAME_RECEIVED, \
        KinectTestMainFrame::OnDepthFrame)
    EVT_COMMAND (wxID_ANY, KINECT_COLOR_FRAME_RECEIVED, \
        KinectTestMainFrame::OnColorFrame)
    EVT_COMMAND (wxID_ANY, KINECT_SKELETON_FRAME_RECEIVED, \
        KinectTestMainFrame::OnSkeletonFrame)
END_EVENT_TABLE()
...
void KinectTestMainFrame::OnDEVICELISTBOXSelected( wxCommandEvent& event )
{
    do
    {
        size_t deviceIndex =
            (size_t)m_DeviceListBox->GetClientData(event.GetInt());
        if(deviceIndex < 0 ||
            deviceIndex > m_KinectHelper->GetDeviceCount()) break;
        m_SelectedDeviceIndex = deviceIndex;
        StartGrabbing();
    }
    while(false);
}

void KinectTestMainFrame::StartGrabbing()
{
    StopGrabbing();
    m_Grabber = m_KinectHelper->CreateGrabber(this, m_SelectedDeviceIndex);
    m_CurrentImage = wxImage(
        m_Grabber->GetDepthFrameSize().GetWidth(),
        m_Grabber->GetDepthFrameSize().GetHeight());
    m_ColorImage = wxImage(
        m_Grabber->GetColorFrameSize().GetWidth(),
        m_Grabber->GetColorFrameSize().GetHeight());
    m_SkeletonImage = wxImage(
        m_Grabber->GetDepthFrameSize().GetWidth(),
        m_Grabber->GetDepthFrameSize().GetHeight());
    m_DepthCanvas->SetCurrentImage(&m_CurrentImage);
    m_ColorCanvas->SetCurrentImage(&m_ColorImage);
    m_SkeletonCanvas->SetCurrentImage(&m_SkeletonImage);
    if(!m_Grabber->Start())
    {
        StopGrabbing();
    }
}
...
void KinectTestMainFrame::OnDepthFrame(wxCommandEvent & event)
{
    do
    {
        if(!m_Grabber) break;
        m_Grabber->GrabDepthFrame(m_CurrentImage.GetData());
        m_DepthCanvas->Refresh();
    }
    while(false);
}

void KinectTestMainFrame::OnColorFrame(wxCommandEvent & event)
{
    do
    {
        if(!m_Grabber) break;
        m_Grabber->GrabColorFrame(m_ColorImage.GetData());
        m_ColorCanvas->Refresh();
    }
    while(false);
}

void KinectTestMainFrame::OnSkeletonFrame(wxCommandEvent & event)
{
    do
    {
        if(!m_Grabber) break;
        SkeletonPainter painter;
        wxBitmap bmp(m_SkeletonImage.GetWidth(), m_SkeletonImage.GetHeight());
        wxMemoryDC mdc(bmp);
        painter.DrawSkeleton(mdc, m_Grabber->GrabSkeletonFrame());
        mdc.SelectObject(wxNullBitmap);
        m_SkeletonImage = bmp.ConvertToImage();
        m_SkeletonCanvas->Refresh();
    }
    while(false);
}

Как видно из кода, класс граббера, при получении нового кадра, отправляет уведомление объекту wxEvtHandler (а класс wxFrame в wxWidgets является производным от wxEvtHandler). У формы есть обработчики событий, которые вызываются при получении уведомлений от граббера.

Причина, по которой метод KinectGrabberBase::GrabSkeletonFrame() возвращает void*, тоже довольно проста – если делать реализации захвата изображений с использованием различных SDK (в том числе и неофициальных), то не факт что у всех этих SDK информация о положении игроков будет приходить в виде одинаковых структур данных. В любом случае координаты нужно отправлять на пост-обработку. В таком случае код, который получит указатель из граббера, будет сам знать, к какому типа данных его нужно преобразовать. Графическому интерфейсу о внутреннем устройстве граббера знать совсем не обязательно.

В завершение


В завершение хотелось бы отметить что, хотя SDK от Microsoft находится в состоянии beta, но оно вполне пригодно к использованию, хотя функционал управления Kinect реализован не полностью (libfreenect, Например, позволяет управлять светодиодами на устройстве, а официальный SDK, судя по документации, этого не умеет). Библиотека работает на удивление стабильно. Заметно, что разработчики позаботились о том, чтобы избежать утечек памяти. Если, например, забыть закрыть поток при выходе, то отладчик Visual Studio не будет сообщать об утечках памяти, скорее всего все корректно завершается и удаляется из памяти при выгрузке библиотеки.
Исходный код тестового приложения и библиотеки можно найти на Google Code – wxKinectHelper.
Рассчитываю на развитие проекта и добавление новых реализаций грабберов. В данный момент пытаюсь приручить libfreenect. Из коробки и без предварительных шаманств удалось запустить все кроме получения изображений – LED индикаторами мигать и жужжать моторчиком получается отлично. То же пытаюсь сделать и с OpenNI.

Полезные ссылки


Microsoft Research Kinect SDK

Видео по разработке с Kinect SDK



Альтернативные SDK



Интересные проекты, использующие Kinect SDK



На правах ЗЫ


Если Вы планируете или уже начали разработку C++ проекта с использованием Kinect SDK (официального или альтернативного) или проекта с использованием алгоритмов компьютерного зрения, и Вам нужна помощь в разработке — готов рассмотреть различные варианты сотрудничества.
+50
8576
56
t_rex 36,0 G+

Комментарии (7)

+6
moadib #
Плюсую, но мне лично кажется, что не надо было простыни кода выкладывать. Показать основные моменты(работа с api, изображением) и ссылку на архив с исходниками, или репозиторий :) А то статья какая-то неоправданно большая получилась.
+1
t_rex #
Я думаю о том чтобы убрать чуток, т.к. уже несколько таких отзывов получил.
Но в данном случае смысл был показать два варианта использования (в SVN сейчас только последний) — первый, когда instance живет на протяжении всей работы приложения (создается хелпером и удаляется тоже хелпером, в граббинг может останавливаться и заканчиваться сколько угодно раз), а второй — когда инстанс создается только на время граббинга.
0
tangro #
Скажите, а зачем было пол-статьи долбаться с компиляцией и подключением wxWidgets? Ради одного листбокса и одного ивента?
+3
t_rex #
Опыт показывает, что если в примере используется что-то отличное от стандартных библиотек, которые идут с Visual Studio, то лучше потратить время и объяснить как эти дополнительные инструменты собираются, чем потом по 10 раз отвечать всем, как запустить сорцы из примера. Поэтому решил выделить на это отдельную часть статьи.

И да, планируется продолжение темы с Kinect, поэтому часть, связанная со сборкой wxWidgets лишней не будет.
+2
t_rex #
По поводу выбора GUI-библиотеки для примера: тот же OpenNI работает и в Linux. Если изначально завязат разработку на WinAPI или MFC, то возможности собрать все по-простому и запустить в Linux не будет.
Если тема разработки с Kinect Вам интересна, то следите за развитием wxKinectHelper проекта на гуглокоде. В ближайшее время рассчитываю зарелизить кросс-платформенное решение ;)
0
XaocCPS #
про вас написали на Channel9 channel9.msdn.com/coding4fun/kinect/C-Kinect-SDK-Usage-Tutorial
0
t_rex #
Видел, но там как-то совсем не смешно получилось. У других проектов на о Kinect на Channel9 более интересные анонсы :)

Только зарегистрированные пользователи могут оставлять комментарии. Войдите, пожалуйста.