Итак, наконец-то оторвавшись от Rage, продолжим :) Крови сегодня не будет (да, «все врут»), зато будет первый код: мы посмотрим как получать кадры от камеры и сенсора глубины, а также повертим «головой» Kinect вверх-вниз из кода на C# в нашем XNA-приложении. Соответственно, кроме описанных ранее требований, нам также понадобится XNA Game Studio (я использую 4.0, хотя она уже обновилась — разницы в коде для нас быть не должно).
Для самых нетерпеливых:
Первое что нам нужно сделать — добавить в проект (XNA Windows Game) ссылку на библиотеку Kinect, она находится на вкладке .NET под названием Microsoft.Research.Kinect:
Теперь пара слов об именовании — в своих проектах я стараюсь придерживаться следующих принципов:
Также я считаю удобным определять короткие псевдонимы для нестандартных пространств имён, избегая тем самым скопления мало связанных между собой элементов в одном списке в IntelliSence и потенциальных конфликтов в их названиях.
Microsoft.Research.Kinect разделяется на две части — Audio и Nui. Руководствуясь описанным выше принципом подключим нужное нам пространство имён:
Добавим инициализацию устройства с нужными нам режимами в метод Initialize():
Нам понадобятся две ссылки на кадры, которые мы будем отрисовывать, а также массив цветов пикселей, который будет использоваться для формирования этих кадров. Кроме этого, укажем размер кадра и рабочую зону для сенсора глубины (в миллиметрах; здесь указана максимальная зона видимости контроллера):
private Color[] _textureData;
private int _tW = 640;
private int _tH = 480;
private int _distMin = 850;
Создадим экземпляры текстур в методе LoadContent(), чтобы ссылки не были пустыми:
_textureData = new Color[_tW * _tH];
В обработчике события готовности видеокадра полученный PlanarImage преобразуется в Texture2D:
int index = 0;
В обработчике события готовности кадра глубины информация из PlannarImage адаптируется под указанную нами рабочую зону и помещается в Texture2D. Чем ближе объект, тем светлее он будет на кадре. Белый цвет имеют объекты, расположенные слишком близко или слишком далеко, а так же «мёртвые зоны»:
Итак, кадры формируются, теперь остаётся их отрисовать. На полученных кадрах глубины изображение «правильное», тогда как на видеокадрах оно отражено зеркально — мне кажется в данном случае оба изображения должны отражать нас зеркально:
0f , Vector2.Zero, 1f , SpriteEffects.FlipHorizontally, 0);
И, наконец, то, ради чего мы все здесь сегодня собрались — возможность повертеть этой хреновиной программно :) Теперь мы знаем что в «голову» Kinect встроен акселерометр, однако в SDK BETA нам показывают угол наклона сенсора лишь в вертикальной плоскости (±64 градуса). Добавим метод GetLiftAngle(), получающий это значение:
Для самых нетерпеливых:
- исходник плохого примера (XNA 4.0, Kinect for Windows SDK BETA)
- исходник хорошего примера (XNA 4.0, Kinect for Windows SDK BETA)
- обновление примера (XNA 4.0, Kinect for Windows SDK BETA), подробнее здесь
- обновление примера (XNA 4.0, Kinect for Windows SDK BETA 2), подробнее здесь
Первое что нам нужно сделать — добавить в проект (XNA Windows Game) ссылку на библиотеку Kinect, она находится на вкладке .NET под названием Microsoft.Research.Kinect:
Теперь пара слов об именовании — в своих проектах я стараюсь придерживаться следующих принципов:
- все названия оформляются в стиле CamelCase, при этом имена переменных и экземпляров классов — в lowerCamelCase
- названия глобальных переменных начинаются со знака подчёркивания: _global
- названия переданных в качестве параметра переменных начинаются с «p_»: p_param
- названия локальных переменных остаются как есть: local
Также я считаю удобным определять короткие псевдонимы для нестандартных пространств имён, избегая тем самым скопления мало связанных между собой элементов в одном списке в IntelliSence и потенциальных конфликтов в их названиях.
Microsoft.Research.Kinect разделяется на две части — Audio и Nui. Руководствуясь описанным выше принципом подключим нужное нам пространство имён:
using K = Microsoft.Research.Kinect.Nui;
Теперь создадим экземпляр класса Runtime, представляющий сам сенсор Kinect:
private K.Runtime _runtime = new K.Runtime();
Для включения сенсора его надо инициализировать указав режимы работы, которые мы будем использовать. Они задаются комбинацией значений перечисления RuntimeOptions:
private K.Runtime _runtime = new K.Runtime();
Для включения сенсора его надо инициализировать указав режимы работы, которые мы будем использовать. Они задаются комбинацией значений перечисления RuntimeOptions:
- UseColor — RGB камера
- UseDepth — сенсор глубины
- UseDepthAndPlayerIndex — сенсор глубины с определением положения шести человек
- UseSkeletalTracking — отслеживание скелетов двух игроков
Добавим инициализацию устройства с нужными нам режимами в метод Initialize():
protected override void Initialize()
{
_runtime.Initialize(
//K.RuntimeOptions.UseDepthAndPlayerIndex |
{
_runtime.Initialize(
//K.RuntimeOptions.UseDepthAndPlayerIndex |
//K.RuntimeOptions.UseSkeletalTracking |
K.RuntimeOptions.UseDepth |
K.RuntimeOptions.UseColor
K.RuntimeOptions.UseDepth |
K.RuntimeOptions.UseColor
);
base.Initialize();
}
base.Initialize();
}
Также добавим код для корректного завершения работы с Kinect при закрытии приложения:
protected override void UnloadContent()
{
_runtime.Uninitialize();
}
Нам понадобятся две ссылки на кадры, которые мы будем отрисовывать, а также массив цветов пикселей, который будет использоваться для формирования этих кадров. Кроме этого, укажем размер кадра и рабочую зону для сенсора глубины (в миллиметрах; здесь указана максимальная зона видимости контроллера):
private Texture2D _RGB;
private Texture2D _Depth;
private Texture2D _Depth;
private Color[] _textureData;
private int _tW = 640;
private int _tH = 480;
private int _distMin = 850;
private int _distMax = 4000;
private int _distOffset;
Создадим экземпляры текстур в методе LoadContent(), чтобы ссылки не были пустыми:
protected override void LoadContent()
{
_spriteBatch = new SpriteBatch(GraphicsDevice);
{
_spriteBatch = new SpriteBatch(GraphicsDevice);
_RGB = new Texture2D(this.GraphicsDevice, _tW, _tH);
_Depth = new Texture2D(this.GraphicsDevice, _tW, _tH);
_Depth = new Texture2D(this.GraphicsDevice, _tW, _tH);
_textureData = new Color[_tW * _tH];
_distOffset = _distMax - _distMin;
Здесь же откроем потоки изображений от камеры и сенсора глубины, задав для них настройки. VideoStream и DepthStream являются потомками класса ImageStream, у которого есть метод Open(ImageStreamType streamType, int poolSize, ImageResolution resolution, ImageType image). Для нашей задачи 640х480 — оптимальное разрешение для обоих потоков:
_runtime.VideoStream.Open(K.ImageStreamType.Video, 2,
K.ImageResolution.Resolution640x480, K.ImageType.Color);
_runtime.VideoStream.Open(K.ImageStreamType.Video, 2,
K.ImageResolution.Resolution640x480, K.ImageType.Color);
_runtime.DepthStream.Open(K.ImageStreamType.Depth, 2,
K.ImageResolution.Resolution640x480, K.ImageType.Depth);
Также здесь мы подпишемся на события, возникающие когда очередной кадр сформирован:K.ImageResolution.Resolution640x480, K.ImageType.Depth);
_runtime.VideoFrameReady +=
new EventHandler<K.ImageFrameReadyEventArgs>(OnVideoFrameReady);
new EventHandler<K.ImageFrameReadyEventArgs>(OnVideoFrameReady);
_runtime.DepthFrameReady +=
new EventHandler<K.ImageFrameReadyEventArgs>(OnDepthFrameReady);
}
new EventHandler<K.ImageFrameReadyEventArgs>(OnDepthFrameReady);
}
В обработчике события готовности видеокадра полученный PlanarImage преобразуется в Texture2D:
private void OnVideoFrameReady(object sender, K.ImageFrameReadyEventArgs e)
{
K.PlanarImage _image = e.ImageFrame.Image;
_RGB = new Texture2D(this.GraphicsDevice, _tW, _tH);
{
K.PlanarImage _image = e.ImageFrame.Image;
_RGB = new Texture2D(this.GraphicsDevice, _tW, _tH);
int index = 0;
for (int y = 0; y < _image.Height; y++)
{
for (int x = 0; x < _image.Width; x++, index += 4)
{
_textureData[y * _image.Width + x] = new Color(
_image.Bits[index + 2],
_image.Bits[index + 1],
_image.Bits[index + 0]);
}
}
_RGB.SetData(_textureData);
}
В обработчике события готовности кадра глубины информация из PlannarImage адаптируется под указанную нами рабочую зону и помещается в Texture2D. Чем ближе объект, тем светлее он будет на кадре. Белый цвет имеют объекты, расположенные слишком близко или слишком далеко, а так же «мёртвые зоны»:
private void OnDepthFrameReady(object sender, K.ImageFrameReadyEventArgs e)
{
{
K.PlanarImage p = e.ImageFrame.Image;
_Depth = new Texture2D(this.GraphicsDevice, _tW, _tH);
int index = 0;
int n, distance;
byte intensity;
for (int y = 0; y < p.Height; y++)
{
for (int x = 0; x < p.Width; x++, index += 2)
{
n = (y * p.Width + x) * 2;
distance = (p.Bits[n + 0] | p.Bits[n + 1] << 8);
intensity = (byte)(255 - (255 * Math.Max(
distance - _distMin, 0) / (_distOffset)));
_textureData[y * p.Width + x] =
new Color(intensity, intensity, intensity);
}
}
_Depth.SetData(_textureData);
}
Итак, кадры формируются, теперь остаётся их отрисовать. На полученных кадрах глубины изображение «правильное», тогда как на видеокадрах оно отражено зеркально — мне кажется в данном случае оба изображения должны отражать нас зеркально:
protected override void Draw(GameTime p_gameTime)
{
{
GraphicsDevice.Clear(Color.CornflowerBlue);
_spriteBatch.Begin();
_spriteBatch.Draw(_RGB, Vector2.Zero, Color.White);
_spriteBatch.Draw(
_Depth, new Vector2(_tW, 0), null, Color.White,
_spriteBatch.End();
base.Draw(p_gameTime);
} И, наконец, то, ради чего мы все здесь сегодня собрались — возможность повертеть этой хреновиной программно :) Теперь мы знаем что в «голову» Kinect встроен акселерометр, однако в SDK BETA нам показывают угол наклона сенсора лишь в вертикальной плоскости (±64 градуса). Добавим метод GetLiftAngle(), получающий это значение:
private int _liftAngle = 0;
private int GetLiftAngle()
{
_liftAngle = _runtime.NuiCamera.ElevationAngle;
return _liftAngle;
}
Свойству ElevationAngle можно назначать значения от -27 до 27, выход за границы этого диапазона сопровождается ArgumentOutOfRangeException. Значению 0 соответствует горизонтальное положение «головы», даже если основание контроллера стоит под наклоном (если сильно наклонить устройство в руках, назначение ElevationAngle приводит к странным углам поворота и жалобному скрежету моторчика). Добавим код для изменения угла наклона с учётом ограничений, не забыв подключить пространство имён System.Threading и поместить назначение ElevationAngle в отдельный поток (иначе приложение будет подвисать на время поворота):{
_liftAngle = _runtime.NuiCamera.ElevationAngle;
return _liftAngle;
}
private void Lift(int p_angle)
{
if (p_angle == _liftAngle) return;
if (p_angle > 27) p_angle = 27;
else if (p_angle < -27) p_angle = -27;
_liftAngle = p_angle;
{
if (p_angle == _liftAngle) return;
if (p_angle > 27) p_angle = 27;
else if (p_angle < -27) p_angle = -27;
_liftAngle = p_angle;
Thread t = new Thread(new ThreadStart(Lift));
t.Start();
}
private void Lift()
{
_runtime.NuiCamera.ElevationAngle = _liftAngle;
}
Добавим вывод угла наклона сенсора в заголовок окна, а также повесим управление наклоном на стрелки вверх-вниз с отложенным применением при отжатии клавиши:
private KeyboardState _keysState;
private KeyboardState _keysStatePre;
private float _liftDelta = 0;
protected override void Update(GameTime p_gameTime)
private KeyboardState _keysStatePre;
private float _liftDelta = 0;
protected override void Update(GameTime p_gameTime)
{
this.Window.Title = GetLiftAngle().ToString();
_keysStatePre = _keysState;
_keysState = Keyboard.GetState();
if (_keysState.IsKeyDown(Keys.Up))
{
_liftDelta += 0.1f ;
this.Window.Title += " +" + ((int)_liftDelta).ToString();
}
else if (_keysStatePre.IsKeyDown(Keys.Up))
{
Lift(_liftAngle + (int)_liftDelta);
_liftDelta = 0;
}
if (_keysState.IsKeyDown(Keys.Down))
{
_liftDelta -= 0.1f ;
this.Window.Title += " " + ((int)_liftDelta).ToString();
}
else if (_keysStatePre.IsKeyDown(Keys.Down))
{
Lift(_liftAngle + (int)_liftDelta);
_liftDelta = 0;
}
base.Update(p_gameTime);
}
Вот и готов плохой пример по основам работы с Kinect. Плохой он потому что подойдёт только для первого ознакомления, использовать его в реальном проекте в таком виде нельзя и дело даже не в том что весь код, нарушая инкапсуляцию, скинут в один класс Game.
Что если в момент запуска приложения контроллер не будет подключен? А если мы его отключим во время работы? А если передадим в _runtime.Initialize(...) не ту комбинацию параметров? Или попробуем инициализировать Kinect повторно? А если выставим не то разрешение при открытии потоков изображений? Или будем назначать _runtime.NuiCamera.ElevationAngle чаще одного раза в несколько секунд?
Ответы на эти вопросы нам дают окошки возникающих исключений. Поэтому, чтобы не учить плохому, я решил сделать второй, более правильный пример. В нём в той или иной степени решены описанные проблемы и его вполне можно использовать в своих проектах, однако он сложнее для описания в рамках статьи, поэтому я оставляю его изучение для самостоятельной работы заинтересованного читателя.
Что если в момент запуска приложения контроллер не будет подключен? А если мы его отключим во время работы? А если передадим в _runtime.Initialize(...) не ту комбинацию параметров? Или попробуем инициализировать Kinect повторно? А если выставим не то разрешение при открытии потоков изображений? Или будем назначать _runtime.NuiCamera.ElevationAngle чаще одного раза в несколько секунд?
Ответы на эти вопросы нам дают окошки возникающих исключений. Поэтому, чтобы не учить плохому, я решил сделать второй, более правильный пример. В нём в той или иной степени решены описанные проблемы и его вполне можно использовать в своих проектах, однако он сложнее для описания в рамках статьи, поэтому я оставляю его изучение для самостоятельной работы заинтересованного читателя.
Для самых терпеливых:
- исходник плохого примера (XNA 4.0, Kinect for Windows SDK BETA)
- исходник хорошего примера (XNA 4.0, Kinect for Windows SDK BETA)
- обновление примера (XNA 4.0, Kinect for Windows SDK BETA), подробнее здесь
- обновление примера (XNA 4.0, Kinect for Windows SDK BETA 2), подробнее здесь
(продолжение следует)
Жду ваших отзывов и замечаний в комментариях и на сайте xnadev.ru
4 комментария:
Классный урок! Реально классный. Но вот я до этого с XNA дело не имел и мне составило большим трудом пройти этот урок. Вопрос: почему вы не использовали wpf вместе xna?
Вместе с SDK идёт пример SkeletalViewer с похожей функциональностью, он на WPF. XNA - гораздо более игровая платформа и мне интересно именно такое сочетание
А не можете подсказать (заранее извиняюсь за возможную наглость), где есть уроки на русском с английском пока что слаб..
Кажется не встречал. Логика работы с сенсором в любом случае будет схожа, не сложно адаптировать
Отправить комментарий