Как видно, мы можем получать сразу практически все данные: цветное изображение, данные с камеры глубины + индекс игрока и скелет. Получение скелета мы тоже включим, но трогать пока не будем.
Далее, для каждого типа данных нам необходимо открыть свой поток при при помощи метода NuiImageStreamOpen. Описание этого метода из справки:
HRESULT NuiImageStreamOpen(
NUI_IMAGE_TYPE eImageType,
NUI_IMAGE_RESOLUTION eResolution,
DWORD dwImageFrameFlags_NotUsed,
DWORD dwFrameLimit,
HANDLE hNextFrameEvent,
HANDLE *phStreamHandle
);
Теперь по порядку.
eImageType - тип изображения, который определяет формат данных.
eResolution определяет размеры кадра (об этом далее).
dwImageFrameFlags_NotUsed пока не используется.
dwFrameLimit - количество кадров, которые будут буферизироваться. Оно не может быть больше чем NUI_IMAGE_STREAM_FRAME_LIMIT_MAXIMUM (сейчас это 4). Рекомендуют использовать значение 2.
hNextFrameEvent - событие, которое будет сигнализировать нам о том, что новый кадр сформирован. Очень важный параметр (об этом далее)
phStreamHandle - указатель, в который, в случае успешного выполнения, будет записан идентификатор потока.
Теперь подробнее о разрешении изображения. В документации приведена таблица, которая описывает размеры кадра для каждого из типов данных. Так для NUI_IMAGE_TYPE_DEPTH_AND_PLAYER_INDEX разрешение может быть либо 80x60 либо 320x240. Для NUI_IMAGE_TYPE_DEPTH - уже может быть до 640x480. А для NUI_IMAGE_TYPE_COLOR - либо 640x480 либо 1280x1024. Подробнее об типах данных читайте ниже.
Как я уже сказал - идентификатор события - это очень нужный и полезный параметр. Он позволяет сделать нам хорошее многопоточное приложение. Для того, чтобы не тормозить основной поток, мы из другого потока будем получать сведения о том, что кадр уже готов и просто будем забирать этот кадр, вместо того, чтобы ждать его в основном потоке. Для каждого из типов данных нам понадобится одно событие. Также создадим событие, которое сообщит о завершении работы с кинектом. Обратите внимание, что событие должно быть "manual reset". То есть после вызова SetEvent внутри Kinect SDK будет происходить вызов ResetEvent. Если создать не manual-reset событие, получим скорость обновления данных около 15 кадров в секунду. Да, забыл сказать, что в нормальном режиме имеем около 30-ти кадров в секунду.
Итак, создадим события для каждых типов данных:
_threadStopEvent = CreateEvent(0, false, false, 0);
_kinectImageNextFrameEvent = CreateEvent(0, true, false, 0);
_kinectDepthNextFrameEvent = CreateEvent(0, true, false, 0);
_kinectSkeletonNextFrameEvent = CreateEvent(0, true, false, 0);
И откроем потоки. Следует заметить, что метода закрытия потока не существует - они освобождаются вместе с вызовом NuiShutdown(). Так как мы не гонимся за большим размером выходного изображения - будем использовать 640x480 для цветной картинки
result = NuiImageStreamOpen(NUI_IMAGE_TYPE_COLOR, NUI_IMAGE_RESOLUTION_640x480, 0, 2,
_kinectImageNextFrameEvent, &_kinectImageStream);
Для глубины и индекса игрока будем использовать максимально возможное разрешение - 320x240:
Кстати, для того, чтобы получать индекс игрока вместе с глубиной, нам обязательно нужно использовать флаг NUI_INITIALIZE_FLAG_USES_SKELETON при инициализации. Как я уже говорил, включим слежение за скелетом, но трогать пока не будем:
result = NuiSkeletonTrackingEnable(_kinectSkeletonNextFrameEvent, 0);
Теперь создадим поток, в котором будем обрабатывать события, приходящие от Kinect SDK:
_thread = CreateThread(0, 0, threadFunc, this, 0, 0);
Сразу рассмотрим как это все правильно выключить. Мы сообщаем потоку о том, что работа завершена, после чего завершаем поток и удаляем все события. Также вызываем метод NuiShutdown()
if (_threadStopEvent)
{
SetEvent(_threadStopEvent);
if (_thread)
{
WaitForSingleObject(_thread, INFINITE);
CloseHandle(_thread);
}
CloseHandle(_threadStopEvent);
}
NuiShutdown();
if (_kinectSkeletonNextFrameEvent)
CloseHandle(_kinectSkeletonNextFrameEvent);
if (_kinectDepthNextFrameEvent)
CloseHandle(_kinectDepthNextFrameEvent);
if (_kinectImageNextFrameEvent)
CloseHandle(_kinectImageNextFrameEvent);
Метод, который выполняется в отдельном потоке предельно прост - мы в цикле ждем наступления одного из событий и после чего вызываем соответствующий метод:
enum KinectEvent
{
KinectEvent_Shutdown,
KinectEvent_NextImageFrame,
KinectEvent_NextDepthFrame,
KinectEvent_NextSkeletonFrame,
KinectEvent_max
};
DWORD WINAPI KinectPrivate::threadFunc(void* param)
{
std::cout << "Running kinect thread..." << std::endl;
KinectPrivate* kp = reinterpret_cast<KinectPrivate*>(param);
HANDLE eventList[KinectEvent_max] =
{
kp->_threadStopEvent,
kp->_kinectImageNextFrameEvent,
kp->_kinectDepthNextFrameEvent,
kp->_kinectSkeletonNextFrameEvent,
};
DWORD eventIndex = static_cast<DWORD>(-1);
DWORD numEvents = sizeof(eventList) / sizeof(HANDLE);
bool running = true;
while (running)
{
eventIndex = WaitForMultipleObjects(numEvents, eventList, false, 100);
if (eventIndex == KinectEvent_Shutdown)
{
running = false;
}
else if (eventIndex == KinectEvent_NextImageFrame)
{
kp->didGetNewImageFrame();
}
else if (eventIndex == KinectEvent_NextDepthFrame)
{
kp->didGetNewDepthFrame();
}
else if (eventIndex == KinectEvent_NextSkeletonFrame)
{
kp->didGetNewSkeletonFrame();
}
}
std::cout << "Kinect thread has finished." << std::endl;
return 0;
}
Получение RGB изображения
Итак, мы подобрались к самому интересному. Если у нас получилось все правильно, то каждый раз, когда сформируется новый кадр будет вызван наш метод
didGetNewImageFrame. Внутри него мы должны будем получить новый кадр, получить сырые данные изображения и обязательно "зарелизить" полученный кадр, иначе кадров чере 5-8 все будет очень плохо (программа просто упадет).
Получение кадра происходит путем вызова метода
NuiImageStreamGetNextFrame. Открываем его в справке:
HRESULT NuiImageStreamGetNextFrame(
HANDLE hStream,
DWORD dwMillisecondsToWait,
CONST NUI_IMAGE_FRAME **ppcImageFrame
);
hStream - наш открытый поток
dwMillisecondsToWait - количество миллисекунд, после которого данный метод вернет управление, в случае, если следующего кадра не будет.
ppcImageFrame - указатель на указатель на структуру NUI_IMAGE_FRAME. Данный метод сам создаст нам объект, а потом его надо будет передать в метод NuiImageStreamReleaseFrame для удаления.
Так как мы точно знаем, что следующий кадр уже готов, то мы указываем dwMillisecondsToWait равным нулю:
const NUI_IMAGE_FRAME* frame = 0;
HRESULT result = NuiImageStreamGetNextFrame(_kinectImageStream, 0, &frame);
if (FAILED(result)) return;
В случае успеха у нас будет указатель на структуру NUI_IMAGE_FRAME, в которой много полей (часть из которых не используется), но более всего нам интересен указатель на NuiImageBuffer, который собственно и хранит данные о изображении. У него есть метод, позволяющие узнать размер изображения - это GetLevelDesc и метод LockRect (не забываем потом вызывать UnlockRect!) который возвращает нам маленькую, но такую важную структуру KINECT_LOCKED_RECT. Эта структура и содержит сырые данные изображения:
typedef struct _KINECT_LOCKED_RECT{
INT Pitch;
void * pBits;
} KINECT_LOCKED_RECT;
Небольшое отступление. Как я уже говорил - целью у нас является написать обертку для Kinect SDK. То есть мы не просто так получаем данные изображения. Их потом надо как-то использовать. Так вот, чтобы передать управление наружу я решил использовать паттерн делегирования, и написал вот такой класс:
typedef unsigned short* KinectDepthData;
typedef vec4ub* KinectColorData;
class KinectDelegate
{
public:
virtual void kinectDidGetImageFrame(const vec2i& size, KinectColorData data) = 0;
virtual void kinectDidGetDepthFrame(const vec2i& size, KinectDepthData data) = 0;
};
Наследуя этот класс, мы реализуем обработку данных с камеры. К слову сказать, данные изображения поступают в BGRA формате (для NUI_IMAGE_TYPE_COLOR), при этом A всегда равен нулю, что немного напрягает (может это разерезвированно для последующего использования?), а данные с камеры глубины хранятся в unsigned short. Но об этом дальше.
Итак, цельный метод получения данных с камеры выглядит вот так:
Теперь самое интересное - формат этих данных. Как я уже говорил каждая точка, это unsigned short. Данные построенны следующим образом: первые три бита хранят индекс игрока, следующие 12 бит - значение глубины и самый старший бит не используется. То есть
X -> XXXXXXXXXXXX -> XXX
^ ^ ^
не исп. индекс игрока
|
– глубина
Индекс игрока может быть 0 (если игроков нет), 1 или 2. Получить его можно с помощью побитного умножения:
unsigned short playerIndex = value & 0x7;
Оставшуюся часть сдвигаем вправо на 3 бита:
Теперь у нас есть значение от 0 до 4095. Чтобы запихнуть его в RGBA текстуру (состоящую из байтов) нужно поделить значение на 16 - получим значения в диапазоне от 0 до 255. Теперь можно играться с этим как угодно. Например выводить глубину, а игроков подсвечивать цветами, или оставлять только игроков, итд. Вот например:
На этом все. В следующей части я постараюсь рассказать о получении скелета, а так же о том, как совместить данные с RGB камеры и камеры глубины (да да, все не так просто - камеры разнесены на некоторое расстояние). Спасибо за внимание.
#Kinect, #Xbox
1 августа 2011
Комментарии [11]