Kinect 中文教程¶
作者: | Edward Zhang |
---|---|
译者: | Wu Xin |
授权: | 该(翻译)文档遵循 CC BY-SA 4.0 许可协议。 |
本教程面向 Kinect 的初学者,使用 C++ 语言构建了 Kinect 的一些入门案例,并使用 OpenGL (GLUT 或 SDL) 来实现可视化。教程分为相互平行的两部分,分别是:
- 基于 Kinect v1.8 SDK 的基础操作,适用于基于结构光的 Kinect 传感器,包括 XBox 360 Kinect 和 Kinect for Windows。

- 基于 Kinect v2.0 SDK 的基础操作,适用于采用 Time of Flight (TOF) 深度感知方式的 Kinect 传感器,即 XBox One Kinect。

基本原则¶
在把玩过一阵子 Kinect 之后,作者发现很多资料都残缺不全。作为一份(给后来者)的参考,本教程应运而生。以下是本教程的指导原则:
- 如无必要,勿费口舌 - 本教程主要侧重于如何使用各种 API。对于其余的窗口创建和显示部分,将会在简单概括后略过。如果你想要深入学习 OpenGL、C++ 等知识,可以去寻找这些方面更加专业的教程。这里不会同时精讲三种 API 和一种语言,这只会让人更加糊涂。
- 精炼代码,主次分明 - 教程中的代码刚好能够让它工作,其余部分基本会忽略。为了避免头文件过多带来的混乱,一部分代码结构会被牺牲,这样你可以专注于相关的功能。
注解
译者注:该教程是在原有英文教程的基础上,由译者个人维护的非官方中文教程。点击此处可以查看原版英文教程。
准备工作¶
注解
这一部分教程是针对第一代 Kinect 的。你的 Kinect 是第二代的吗?来看 Kinect v2.0 教程吧!
本系列教程适用于希望使用微软 Kinect SDK 的 C++ 程序员。教程将尽量精简 Windows 代码。我们将使用 OpenGL 提供图形化操作。
目标: | 本章将使用正确的配置初始化一个 Visual Studio 项目。确保你拥有 Kinect 编程所需要的所有组件。 |
---|
注解
译者注:在本章开始之前,确保 Kinect SDK 已顺利安装,可参考附录。
环境需求¶
- Windows(这毕竟是Windows SDK嘛~)
- 一台 Kinect(最好是 Kinect for PC)
- Visual Studio(选择一个较新的版本)
- C/C++ 语言编程经验
- 熟悉(或愿意学习)OpenGL
本教程将兼容 GLUT 和 SDL。
安装 GLUT¶
你可以选择使用 GLUT 或 SDL 作为 OpenGL 的接口。在本节中将展示在系统中安装 GLUT 的步骤,你也可以选择跳过本节,在安装 SDL 一节中查看 SDL 的安装流程。
大多数 windows 系统并不会自带 GLUT。GLUT 是一套在 OpenGL 中用于窗口化和处理事件的代码库,它非常陈旧了,但使用依然很广泛。
- 去这个网站下载并解压 MSVC freeglut 的二进制文件。
- 将刚刚解压的
include/
目录和lib/
目录中的内容复制到合适的 Windows SDK 目录中,如: - Visual Studio 2010 中:
C:/Program Files/Microsoft SDKs/Windows/v7.0A/Include/
和C:/Program Files/Microsoft SDKs/Windows/v7.0A/Lib/
- Visual Studio 2012以上:
C:/Program Files/Windows Kits (x86)/8.1/Include/um/
和C:/Program Files (x86)/Windows Kits/8.1/Lib/winv6.3/um/
- Visual Studio 2010 中:
- 将刚刚解压的
- 复制
bin/x64/freeglut.dll
到C:/Windows/System32
、bin/x86/freeglut.dll
到C:/Windows/SysWOW64
。如果你的系统是 32 位的,只需要把bin/x86/freeglut.dll
复制到C:/Windows/System32
。
注解
译者注:步骤1和2,如果不想污染自己的系统环境,也可以在解压后不去复制这些文件,稍后在 Visual Studio 项目中配置对应地址即可;步骤3,也可以选择与自己系统对应的 .dll 文件,稍后复制到项目的运行目录中。
安装 SDL¶
你可以选择使用 GLUT 或 SDL 作为 OpenGL 的接口。在本节中将展示在系统中安装 SDL 的步骤,如果在上一节已选择安装 GLUT,请跳过本节。
去 SDL 官网下载下载最新版本的 Visual C++ SDL 开发库。在本教程编写期间 SDL 版本稳定在 1.2.15 。
注解
译者注 :SDL 开发库对应官网中的 Development Libraries,不是 Runtime Binaries。
将下载到的压缩文件解压至一个合适的位置(如:C:\
)。
人生苦短,做完下面两件事,会让我们的生活会更加惬意:
- 复制
SDL.dll
到我们的系统目录中。打开C:\SDL-1.2.15\lib\x64
(32 位系统下为/x86
),复制SDL.dll
。如果你的系统是 64 位的,就将其粘贴到C:\Windows\SysWOW64
中;否则,就将它粘贴到C:\Windows\System32
。 - 将 SDL 文件夹添加为一项环境变量。打开系统的环境变量,新建一个变量,其中变量名为
SDL_DIR
、变量值为 SDL 文件夹所在地址,如:C:\SDL-1.2.15
。
注解
译者注 :同样,.dll 文件也可以不必复制到系统目录,只需稍后复制到项目的运行目录中。
新建 Kinect 项目¶
打开 Visual Studio,并新建一个 C++ 空项目。

为了配置构建规则(即 includes 和 libs),我们需要使用属性管理器。将不同组件的配置保存在不同的配置文件中,可以方便之后重复使用。
属性管理器可以在视图菜单中打开。

为了模块化配置,我们创建一个属性表用于 OpenGL 信息,另一个用于 Kinect SDK 信息。在属性管理器中,右键项目名称,选择添加新项目属性表,命名为 OpenGL;再新建一个属性表,命名为 KinectSDK。你将会在 “Debug” 和 “Release” 目录下看到以它们命名的新属性表。属性表是包含构建配置数据的、后缀为 .props 的文件。


首先配置Kinect 属性。右键 Kinect 属性表,点击“属性”。
- 在
C/C++ > 常规 > 附加包含目录
中,添加$(KINECTSDK10_DIR)/inc
。 - 在
链接器 > 常规 > 附加库目录
中,添加$(KINECTSDK10_DIR)/lib/amd64
(32 位系统下为/x86
)。 - 在
链接器 > 输入 > 附加依赖项
中,添加kinect10.lib
。
注意,要添加 include 和 library 目录,你可以直接在对应输入框中键入,也可以单击下拉框,选择<编辑>,然后在弹出的对话框中输入位置。这种方法可以在管理多个目录的时候轻松一些,并且让你浏览目录更加清晰准确(如果不使用KINECTSDK10_DIR
这样的环境变量)。
注解
译者注 :包含文件和库文件也可以直接添加在属性表的VC++目录 > 包含目录
和VC++目录 > 库目录
中。
注解
译者注 :一个空项目的属性表中可能会不显示C/C++ > 常规 > 附加包含目录
,在项目中先添加一个文件即可解决。
接下来,配置 OpenGL 属性表。
GLUT 方式¶
如果在前面选择了 GLUT 作为 OpenGL 的接口,需要依据本节步骤配置 OpenGL 属性表。
右键 OpenGL 属性表,点击“属性”,在链接器 > 输入 > 附加依赖项
中,添加以下内容:
opengl32.lib
glu32.lib
freeglut.lib

注解
译者注 :如果在安装 GLUT 时没有将对应包含文件和库文件复制到系统目录下,还需要将它们的地址添加到该属性表对应的包含目录和库目录中。
SDL 方式¶
如果在前面选择了 SDL 作为 OpenGL 的接口,需要依据本节步骤配置 OpenGL 属性表。
右键 OpenGL 属性表,点击“属性”。
- 在
C/C++ > 常规 > 附加包含目录
中,添加$(SDL_DIR)\include
。 - 在
链接器 > 常规 > 附加库目录
中,添加$(SDL_DIR)\lib\x64
(32 位系统下为/x86
)。 - 在
链接器 > 输入 > 附加依赖项
中,添加以下内容:
opengl32.lib
glu32.lib
SDL.lib
SDLmain.lib

至此,我们的项目就准备好了。
Kinect 基础¶
目标: | 学习如何初始化一台 Kinect,并从中获取 RGB 类型的数据。 |
---|---|
源码: | 点此查看 1_Basics.zip |
概述¶
我们有两段与 Kinect 相关的代码,下文将详细讨论这些内容,然后对展示部分的代码做一个简要介绍。
包含文件,常量和全局变量¶
包含文件¶
大多数的文件名就解释清楚了它自己。使用 Kinect 基本都会用到三个头文件:NuiApi.h
,NuiImageCamera.h
和NuiSensor.h
。
为了让 Kinect 的包含文件正常工作,你还需要在前面添加Ole2.h
和Windows.h
。同时也不要忘了添加可视化相关的包含文件。
// GLUT
#include <Windows.h>
#include <Ole2.h>
#include <gl/GL.h>
#include <gl/GLU.h>
#include <gl/glut.h>
#include <NuiApi.h>
#include <NuiImageCamera.h>
#include <NuiSensor.h>
// SDL
#include <Windows.h>
#include <Ole2.h>
#include <SDL_opengl.h>
#include <SDL.h>
#include <NuiApi.h>
#include <NuiImageCamera.h>
#include <NuiSensor.h>
常量和全局变量¶
我们把窗口的宽度和高度定义为 640*480,这恰好也是 Kinect 摄像头输入的图像尺寸。
注意,data
数组将会保存一份我们从 Kinect 中获取的图像的副本,以便我们将其作为纹理 (texture)。有经验的 OpenGL 用户也可以使用帧缓存对象 (Frame Buffer Object) 来代替它。
#define width 640
#define height 480
// OpenGL Variables
GLuint textureId; // ID of the texture to contain Kinect RGB Data
GLubyte data[width*height*4]; // BGRA array containing the texture data
// Kinect variables
HANDLE rgbStream; // The identifier of the Kinect's RGB Camera
INuiSensor* sensor; // The kinect sensor
Kinect 代码¶
Kinect 初始化¶
下面是我们的第一段与 Kinect 相关的代码。initKinect()
函数的作用是初始化一个 Kinect 设备。它包含了两个部分:首先,我们需要找到一个已连接到电脑的 Kinect 传感器,然后将其初始化并准备从中读取数据。
bool initKinect() {
// Get a working kinect sensor
int numSensors;
if (NuiGetSensorCount(&numSensors) < 0 || numSensors < 1) return false;
if (NuiCreateSensorByIndex(0, &sensor) < 0) return false;
// Initialize sensor
sensor->NuiInitialize(NUI_INITIALIZE_FLAG_USES_DEPTH | NUI_INITIALIZE_FLAG_USES_COLOR);
sensor->NuiImageStreamOpen(
NUI_IMAGE_TYPE_COLOR, // Depth camera or rgb camera?
NUI_IMAGE_RESOLUTION_640x480, // Image resolution
0, // Image stream flags, e.g. near mode
2, // Number of frames to buffer
NULL, // Event handle
&rgbStream);
return sensor;
}
以下是一些注意事项:
- 通常情况下,我们需要对上面所有函数的返回值都倍加小心,并且考虑到存在不止一个 Kinect 传感器的情况;但是为了简单起见,我们在这里只是尝试使用第一个连接到的 Kinect 传感器。
NuiInitialize()
方法接受一组标志 (flags),来指定我们感兴趣的传感器特性。在本例中我们选择颜色和深度相机输入;此外,还有音频输入和骨骼关节点输入等选项。更多细节请参考官方 API。NuiImageStreamOpen()
方法有一些迷惑性。它初始化了一个句柄 (HANDLE
),我们稍后可以用它来获取图像帧。这个函数可以用来设置 RGB 彩色图像流或深度图像流,这取决于函数的第一个参数。现在你可以暂时忽略第 3 和第 5 个参数。设置分辨率为 640*480,缓冲区大小为一个整数。最后一个参数是一个指向句柄的指针,我们将用它来获取图像帧。更多细节请参考官方文档。
注解
译者注:目前微软已将旧版 Kinect SDK (v1) 文档完全移除,只剩下 Kinect for Windows SDK 2.0 版本,所以上面提供的两个官网链接其实都是打不开的。
这里提供一个聊胜于无的 SDK v1 文档查阅途径,即通过网页快照存档网站 Wayback Machine 来查阅官方文档的历史快照。
例如,上面提到的打不开的两个页面,在网页快照中分别如下:
NuiInitialize()
方法官方 API 的网页快照NuiImageStreamOpen()
方法官方文档的网页快照
几个时间节点:
- 微软官网最后发布的一代 Kinect SDK 版本号为 1.8.0.595,发布日期为 2013 年 9 月 13 日,在此之后文档更新应基本停止。
- 2014 年 10 月微软发布第二代 Kinect for Windows。
- 微软官网大约在 17 年至 18 年前后彻底移除了 Kinect SDK v1 文档的存档,网页快照不再记录。
另外还需注意,网页快照的抓取时间是有一定随机性的,有时一些页面可以查看,但另一些页面可能会报错,对于报错的情况,可以换一个时间节点查看。
从 Kinect 中获取 RGB 帧¶
要从传感器获取帧,我们必须抓取并锁定它,这样在读取时它就不会损坏。
void getKinectData(GLubyte* dest) {
NUI_IMAGE_FRAME imageFrame;
NUI_LOCKED_RECT LockedRect;
if (sensor->NuiImageStreamGetNextFrame(rgbStream, 0, &imageFrame) < 0) return;
INuiFrameTexture* texture = imageFrame.pFrameTexture;
texture->LockRect(0, &LockedRect, NULL, 0);
在这段代码中有三种类型:NUI_IMAGE_FRAME
是一个包含了所有关于该帧的元数据的结构——编号、分辨率等;NUI_LOCKED_RECT
包含一个指向实际数据的指针;INuiFrameTexture
管理帧数据。首先,我们从前面初始化的句柄中获取一个NUI_IMAGE_FRAME
,然后我们得到一个INuiFrameTexture
,这样我们就可以使用NUI_LOCKED_RECT
从它里面获取像素数据。
现在,我们可以将数据复制到我们自己设定的内存位置。LockedRect
的Pitch
方法返回的是帧的每一行中有多少字节;对该值进行简单的检查可以确保该帧不是空的。
if (LockedRect.Pitch != 0)
{
const BYTE* curr = (const BYTE*) LockedRect.pBits;
const BYTE* dataEnd = curr + (width*height)*4;
while (curr < dataEnd) {
*dest++ = *curr++;
}
}
Kinect 数据是 BGRA 格式的,所以我们可以直接将其复制到我们的缓冲区中,作为 OpenGL 纹理使用。
最后,我们必须释放数据帧,这样 Kinect 才能再次使用它。
texture->UnlockRect(0);
sensor->NuiImageStreamReleaseFrame(rgbStream, &imageFrame);
}
以下是一些注意事项:
- 与前面相同,我们仍然没有检查所有的返回代码。在你的程序中,安全起见可以进一步完善这一部分。
- 如果你调用这个更新函数太快了,那么 Kinect 的更新速率可能会跟不上。在这种情况下,
NuiImageStreamGetNextFrame()
将会返回一个负值。NuiImageStreamGetNextFrame()
的第二个参数指定了在返回失败之前等待新帧的时长(单位为毫秒)。 - 需要重申的是,工作流程遵循以下顺序:
- 获取一个帧:
sensor->NuiImageStreamGetNextFrame()
。第一个参数是我们前面初始化的句柄,最后一个参数是指向将要接收帧数据的NUI_IMAGE_FRAME
结构的指针。第二个参数允许你设定一定时间等待一个新的帧(见上文)。 - 锁定像素数据:
imageFrame.pFrameTexture->LockRect()
。第二个参数是指向NUI_LOCKED_RECT
结构的指针;除此之外,所有的参数必须是 0 或 NULL。 - (借助
LockedRect.pBits
)复制数据。 - 解锁像素数据:
imageFrame.pFrameTexture->UnlockRect()
。参数必须为 0。 - 释放帧:
sensor->NuiImageStreamReleaseFrame()
。第一个参数是图像流的句柄,第二个参数是指向我们要释放的图像帧的指针。
- 获取一个帧:
以上就是所有 Kinect 部分的代码!剩下的就是如何把它搬上屏幕。
窗口化,事件处理和主循环¶
本节将会解释与 GLUT——或 SDL——相关的代码,包括了窗口初始化、事件处理和主更新循环。
具体的初始化代码取决于使用那种实现方式 (GLUT 或 SDL)。它只是使用适当的 API 初始化一个窗口,失败时返回 false。GLUT 版本的实现还会通过指定draw()
函数在每次循环迭代中被调用来设置主循环。
主循环在execute()
函数中启动。在 GLUT中,循环是在后台处理的,所以我们需要做的就是调用glutMainLoop()
函数。在 SDL 中,我们编写自己的循环。在每个循环中,我们在屏幕上绘制新的帧,这个处理是在drawKinect()
函数中完成的。
如果你希望通过 GLUT 或 SDL 进行更复杂的窗口和循环管理,或学习更多关于这些函数的知识,网络上也有很多其它的参考资料。
GLUT
void draw() {
drawKinectData();
glutSwapBuffers();
}
void execute() {
glutMainLoop();
}
bool init(int argc, char* argv[]) {
glutInit(&argc, argv);
glutInitDisplayMode(GLUT_DEPTH | GLUT_DOUBLE | GLUT_RGBA);
glutInitWindowSize(width,height);
glutCreateWindow("Kinect SDK Tutorial");
glutDisplayFunc(draw);
glutIdleFunc(draw);
return true;
}
SDL
void execute() {
SDL_Event ev;
bool running = true;
while (running) {
while (SDL_PollEvent(&ev)) {
if (ev.type == SDL_QUIT) running = false;
}
drawKinectData();
SDL_GL_SwapBuffers();
}
}
bool init(int argc, char* argv[]) {
SDL_Init(SDL_INIT_EVERYTHING);
SDL_Surface* screen =
SDL_SetVideoMode(width, height, 32, SDL_HWSURFACE | SDL_GL_DOUBLEBUFFER | SDL_OPENGL);
return screen;
}
通过 OpenGL 显示¶
初始化¶
代码中描述了三个步骤——设置纹理以包含图像帧,准备 OpenGL 来绘制纹理,以及设置摄像机视点(对 2D 图像使用正投影)。
// Initialize textures
glGenTextures(1, &textureId);
glBindTexture(GL_TEXTURE_2D, textureId);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, width, height,
0, GL_BGRA, GL_UNSIGNED_BYTE, (GLvoid*) data);
glBindTexture(GL_TEXTURE_2D, 0);
// OpenGL setup
glClearColor(0,0,0,0);
glClearDepth(1.0f);
glEnable(GL_TEXTURE_2D);
// Camera setup
glViewport(0, 0, width, height);
glMatrixMode(GL_PROJECTION);
glLoadIdentity();
glOrtho(0, width, height, 0, 1, -1);
glMatrixMode(GL_MODELVIEW);
glLoadIdentity();
显然,我们应该用一个函数把上面的片段包起来,这里为了方便直接把它塞进了main()
函数中。
int main(int argc, char* argv[]) {
if (!init(argc, argv)) return 1;
if (!initKinect()) return 1;
/* ...OpenGL texture and camera initialization... */
// Main loop
execute();
return 0;
}
将图像帧画到屏幕上¶
这部分是很常规的代码。首先将 Kinect 数据复制到我们的缓存区中,然后指定我们的纹理使用这个缓冲区。
void drawKinectData() {
glBindTexture(GL_TEXTURE_2D, textureId);
getKinectData(data);
glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, width, height, GL_BGRA, GL_UNSIGNED_BYTE, (GLvoid*)data);
然后,绘制一个以图像帧为纹理的方框。
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
glBegin(GL_QUADS);
glTexCoord2f(0.0f, 0.0f);
glVertex3f(0, 0, 0);
glTexCoord2f(1.0f, 0.0f);
glVertex3f(width, 0, 0);
glTexCoord2f(1.0f, 1.0f);
glVertex3f(width, height, 0.0f);
glTexCoord2f(0.0f, 1.0f);
glVertex3f(0, height, 0.0f);
glEnd();
}
结束!构建并运行,确保你的 Kinect 已经插入。你应该会看到一个包含 Kinect 所拍摄内容的视频流窗口。
Kinect 深度数据¶
目标: | 学习如何从一台 Kinect 中获取深度数据,并了解数据的格式是什么样的。 |
---|---|
源码: | 点此查看 2_Depth.zip |
概述¶
本章教程非常短——与前一章获取 RGB 数据的代码相比只有两处重要的改动。
另外,为了方便将注意力集中在 Kinect 的细节上,我们重构了 SDL 和 GLUT 组件,将它们放置在了不同的文件中。
Kinect 代码¶
Kinect 初始化¶
要从 Kinect 中获取深度数据,只需要改变一下NuiImageStreamOpen()
中的参数。第一个参数是NUI_IMAGE_TYPE_DEPTH
,它告诉 Kinect 我们需要深度图像而不是 RGB 图像。(为了逻辑上的清晰,我们也对应修改了句柄的名称,来反映这一点。)
在使用 Kinect for Windows 时,与 XBox Kinect 相比还多了一个近距离模式。顾名思义,近距离模式允许 Kinect 设备在更近的距离下工作(约 50cm 到 200cm),而没有近距离模式的设备采集距离一般为 80cm 到 400cm。
要激活近距离模式,将第三个参数设置为NUI_IMAGE_STREAM_FLAG_ENABLE_NEAR_MODE
即可。
// NEW VARIABLE
HANDLE depthStream;
bool initKinect() {
// Get a working kinect sensor
int numSensors;
if (NuiGetSensorCount(&numSensors) < 0 || numSensors < 1) return false;
if (NuiCreateSensorByIndex(0, &sensor) < 0) return false;
// Initialize sensor
sensor->NuiInitialize(NUI_INITIALIZE_FLAG_USES_DEPTH | NUI_INITIALIZE_FLAG_USES_COLOR);
// --------------- START CHANGED CODE -----------------
sensor->NuiImageStreamOpen(
NUI_IMAGE_TYPE_DEPTH, // Depth camera or rgb camera?
NUI_IMAGE_RESOLUTION_640x480, // Image resolution
NUI_IMAGE_STREAM_FLAG_ENABLE_NEAR_MODE, // Image stream flags, e.g. near mode
2, // Number of frames to buffer
NULL, // Event handle
&depthStream);
// --------------- END CHANGED CODE -----------------
return sensor;
}
有关近距离模式的更多信息,请参阅此官方博客文章。
从 Kinect 中获取深度数据帧¶
我们将以灰度图像的形式显示 Kinect 的深度图像。每个像素的值将是该像素点到 Kinect 的距离 (mm) 除以 256 的余数。
这里唯一要注意的事情是NuiDepthPixelToDepth()
函数——实际深度图中的每个像素都含有深度信息和玩家信息(即哪个玩家是与这个像素关联的)。调用此函数返回的是该像素处的深度 (mm)。
深度数据是 16 位的,所以我们使用 USHORT
类型来读取它。
const USHORT* curr = (const USHORT*) LockedRect.pBits;
const USHORT* dataEnd = curr + (width*height);
while (curr < dataEnd) {
// Get depth in millimeters
USHORT depth = NuiDepthPixelToDepth(*curr++);
// Draw a grayscale image of the depth:
// B,G,R are all set to depth%256, alpha set to 1.
for (int i = 0; i < 3; ++i)
*dest++ = (BYTE) depth%256;
*dest++ = 0xff;
}
以上就是所有 Kinect 部分的代码!剩下的就是如何把它搬上屏幕。
显示框架¶
这里重构了代码,将 GLUT 和 SDL 部分打包进了init()
函数、draw()
函数和execute()
函数中。
要使用它们,只需要在main.cpp
的最上方包含glut.h
或sdl.h
。确保项目属性中有正确的包含文件路径和链接文件路径。
结束!构建并运行,确保你的 Kinect 已经插入。你应该会看到一个包含 Kinect 所拍摄内容的灰度视频流窗口。
Kinect 点云¶
目标: | 学习如何对齐彩色和深度图像,以获得彩色点云。 |
---|---|
源码: | 点此查看 3_PointCloud.zip |
概述¶
在本章教程中,我们想采取一些新步骤。最有趣的部分就是,我们现在正在处理 3D 数据!但是,创建一个交互式系统的工作量对我们的教程来说有些多了,所以我们只是创建一个简单地旋转点云。本章教程分为三部分:首先,我们简单讨论一下为什么点云可能要比你想像的更难;其次,我们将展示如何使用 Kinect SDK 获取正确的数据;最后,我们将会展示一些可以降低图像展示难度的 OpenGL技巧。
深度和 RGB 坐标系¶
对齐¶
最简单的生成点云的方法是直接重叠深度和彩色图像,使深度像素 (x, y) 与图像像素 (x, y) 一一匹配。然而,这样生成的深度图像质量非常低,对象的边界和颜色无法对齐。这是因为 RGB 相机和深度相机位于 Kinect 上面的不同位置;显然,它们看到的东西并不一致!通常,我们需要对两个相机进行某种对齐操作(正式术语是「注册」, registration),来从一个坐标空间映射到另一个坐标空间。幸运的是,微软粑粑已经帮我们做好这件事了,我们唯一需要做的就是调用正确的函数。
直接重叠 RGB 和深度

“注册”后的 RGB 和深度

要注意,计算机视觉和机器人学方面的研究人员并不喜欢这个内置注册函数的输出质量,所以他们经常用 OpenCV 之类的工具手动进行校正。
Kinect 代码¶
以下很多代码仅仅是对前面两章教程的综合。
Kinect 初始化¶
初始化部分没有新内容。我们需要两个图像流,一个用于深度图像,一个用于彩色图像。
HANDLE rgbStream;
HANDLE depthStream;
INuiSensor* sensor;
bool initKinect() {
// Get a working kinect sensor
int numSensors;
if (NuiGetSensorCount(&numSensors) < 0 || numSensors < 1) return false;
if (NuiCreateSensorByIndex(0, &sensor) < 0) return false;
// Initialize sensor
sensor->NuiInitialize(NUI_INITIALIZE_FLAG_USES_DEPTH | NUI_INITIALIZE_FLAG_USES_COLOR);
sensor->NuiImageStreamOpen(
NUI_IMAGE_TYPE_DEPTH, // Depth camera or rgb camera?
NUI_IMAGE_RESOLUTION_640x480, // Image resolution
0, // Image stream flags, e.g. near mode
2, // Number of frames to buffer
NULL, // Event handle
&depthStream);
sensor->NuiImageStreamOpen(
NUI_IMAGE_TYPE_COLOR, // Depth camera or rgb camera?
NUI_IMAGE_RESOLUTION_640x480, // Image resolution
0, // Image stream flags, e.g. near mode
2, // Number of frames to buffer
NULL, // Event handle
&rgbStream);
return sensor;
}
从 Kinect 中获取深度数据¶
Kinect SDK 提供了一个函数,告诉你 RGB 图像的哪个像素对应深度图像中的特定点。我们将把这个信息保存在另一个全局数组depthToRgbMap
中。特别地,我们按照每一个深度像素的顺序,将对应的彩色像素的行和列(即 x 和 y 坐标)也保存下来。
现在,我们开始处理 3D 数据。我们将深度图像帧想像成空间中的一束点,而不是一个 640*480 的图像。所以在函数getDepthData()
中,我们将用每个点的坐标(而不是每个像素的深度)来填充我们的缓存区。这意味着对于float
类型的坐标来说,我们需要填充的缓存区需要达到width*height*3*sizeof(float)
的大小。
// Global Variables
long depthToRgbMap[width*height*2];
// ...
void getDepthData(GLubyte* dest) {
// ...
const USHORT* curr = (const USHORT*) LockedRect.pBits;
float* fdest = (float*) dest;
long* depth2rgb = (long*) depthToRgbMap;
for (int j = 0; j < height; ++j) {
for (int i = 0; i < width; ++i) {
// Get depth of pixel in millimeters
USHORT depth = NuiDepthPixelToDepth(*curr);
// Store coordinates of the point corresponding to this pixel
Vector4 pos = NuiTransformDepthImageToSkeleton(i,j,*curr);
*fdest++ = pos.x/pos.w;
*fdest++ = pos.y/pos.w;
*fdest++ = pos.z/pos.w;
// Store the index into the color array corresponding to this pixel
NuiImageGetColorPixelCoordinatesFromDepthPixelAtResolution(
NUI_IMAGE_RESOLUTION_640x480, // color frame resolution
NUI_IMAGE_RESOLUTION_640x480, // depth frame resolution
NULL, // pan/zoom of color image (IGNORE THIS)
i, j, *curr, // Column, row, and depth in depth image
depth2rgb, depth2rgb+1 // Output: column and row (x,y) in the color image
);
depth2rgb += 2;
*curr++;
}
}
// ...
这里有很多东西需要解释!
Vector4
是微软在齐次坐标系下的的 3D 点类型。如果你的线性代数生疏了,不用担心齐次坐标——只要把它当作一个具有 x、y、z 坐标的三维点即可。在这个页面可以找到一个简短的说明。NuiTransformDepthImageToSkeleton()
返回某一特定深度像素的 3D 坐标,坐标系是上面提到的 Kinect 坐标系。这个函数还有一个版本,可以接受一个附加的分辨率参数。NuiImageGetColorPixelCoordinatesFromDepthPixelAtResolution()
接受深度像素(深度图像中的行、列和深度),输出彩色图像中的行和列。API 参考页面见此。
注解
译者注:同样地,上面的 API 页面已经失效,替代的网页快照见此。
从 Kinect 中获取彩色数据¶
现在,我们考虑的是点而不是矩形网格,我们希望我们的彩色输出与特定的深度点相关联。特殊地,类似于getDepthData()
函数,我们的getRgbData()
函数的输入需要一个大小为width*height*3*sizeof(float)
的缓存区来存储点云中每个点的红、绿、蓝色彩值。
void getRgbData(GLubyte* dest) {
// ...
const BYTE* start = (const BYTE*) LockedRect.pBits;
float* fdest = (float*) dest;
long* depth2rgb = (long*) depthToRgbMap;
for (int j = 0; j < height; ++j) {
for (int i = 0; i < width; ++i) {
// Determine color pixel for depth pixel i,j
long x = *depth2rgb++;
long y = *depth2rgb++;
// If out of bounds, then do not color this pixel
if (x < 0 || y < 0 || x > width || y > height) {
for (int n = 0; n < 3; ++n) *fdest++ = 0.f;
}
else {
// Determine rgb color for depth pixel (i,j) from color pixel (x,y)
const BYTE* color = start + (x+width*y)*4;
for (int n = 0; n < 3; ++n) *fdest++ = color[2-n]/255.f;
}
}
}
// ...
在最后几行代码中有一些有趣的数学运算,我们来通读一下。首先,彩色图像帧采用 BGRA 格式,每个通道一字节,逐行排列。所以像素 (x, y) 的线性指数是x + width*y
。然后,我们想要的 4 字节块是start + linearindex*4
。最后,我们想要把按字节取值 (0-255) 的 BGRA 格式转换为按浮点数取值 (0.0-1.0) 的 RGB 格式,所以我们对字节的顺序取反,并除以 255:color[2-n]/255.f
。
OpenGL 显示¶
我们要用数组缓存 (array buffers) 来显示我们的点云。什么是数组缓存?他们允许你通过调用一个函数来替换一系列的glBegin()
、glColor()
、glVertex()
、glEnd()
调用。另外,数组缓存存储在 GPU 里面,因此显示的时候效率会更高。不过,它们也确实使代码变得更复杂了。想要跳过数组缓存吗?来这里。
要使用数组缓存,我们需要引入 OpenGL 的扩展。为了简化这一过程,我们选择使用 GLEW。
安装 GLEW¶
- 去这个网站下载并解压 GLEW 的二进制文件。
- 复制解压文件夹中的
include/
和Lib/
目录,到合适的 Windows SDK 目录中,如: - Visual Studio 2010 中:
C:/Program Files/Microsoft SDKs/Windows/v7.0A/Include/
和C:/Program Files/Microsoft SDKs/Windows/v7.0A/Lib/
- Visual Studio 2012以上:
C:/Program Files/Windows Kits (x86)/8.1/Include/um/
和C:/Program Files (x86)/Windows Kits/8.1/Lib/winv6.3/um/
- Visual Studio 2010 中:
- 复制解压文件夹中的
- 复制
bin/x64/glew32.dll
到C:/Windows/System32
、bin/x86/glew32.dll
到C:/Windows/SysWOW64
。如果你的系统是 32 位的,只需要把bin/x86/glew32.dll
复制到C:/Windows/System32
。
将glew32.lib
添加至 OpenGL 或 SDL 属性表的链接器 > 输入 > 附加依赖项
中。
注解
译者注:与第一章提到的相同,步骤1和2,如果不想污染自己的系统环境,也可以在解压后不去复制这些文件,稍后在 Visual Studio 项目中配置对应地址即可;步骤3,也可以选择与自己系统对应的 .dll 文件,稍后复制到项目的运行目录中。
OpenGL 代码¶
既然是处理 3D 数据,我们还需要注意相机设置。我们使用gluPerspective()
和gluLookAt()
函数来为我们解决这个问题。
// Global variables:
GLuint vboId; // Vertex buffer ID
GLuint cboId; // Color buffer ID
// ...
// OpenGL setup
glClearColor(0,0,0,0);
glClearDepth(1.0f);
// Set up array buffers
const int dataSize = width*height * 3 * 4;
glGenBuffers(1, &vboId);
glBindBuffer(GL_ARRAY_BUFFER, vboId);
glBufferData(GL_ARRAY_BUFFER, dataSize, 0, GL_DYNAMIC_DRAW);
glGenBuffers(1, &cboId);
glBindBuffer(GL_ARRAY_BUFFER, cboId);
glBufferData(GL_ARRAY_BUFFER, dataSize, 0, GL_DYNAMIC_DRAW);
// Camera setup
glViewport(0, 0, width, height);
glMatrixMode(GL_PROJECTION);
glLoadIdentity();
gluPerspective(45, width /(GLdouble) height, 0.1, 1000);
glMatrixMode(GL_MODELVIEW);
glLoadIdentity();
gluLookAt(0,0,0,0,0,1,0,1,0);
出于显示的目的,我们没有将它写成一个完整的互动界面,只是用一个“旋转”的摄像头,围绕 Kinect 前方 3 米的点旋转。详细信息请参阅代码。
融会贯通¶
我们写好了getDepthData()
和getRgbData()
,但是该怎么用呢?我们所做的就是在 GPU 中分配一些内存,然后用我们的函数去把点云数据复制到那里。
void getKinectData() {
const int dataSize = width*height*3*sizeof(float);
GLubyte* ptr;
glBindBuffer(GL_ARRAY_BUFFER, vboId);
ptr = (GLubyte*) glMapBuffer(GL_ARRAY_BUFFER, GL_WRITE_ONLY);
if (ptr) {
getDepthData(ptr);
}
glUnmapBuffer(GL_ARRAY_BUFFER);
glBindBuffer(GL_ARRAY_BUFFER, cboId);
ptr = (GLubyte*) glMapBuffer(GL_ARRAY_BUFFER, GL_WRITE_ONLY);
if (ptr) {
getRgbData(ptr);
}
glUnmapBuffer(GL_ARRAY_BUFFER);
}
现在我们想要用glDrawArrays()
函数来绘制我们的点云。
void drawKinectData() {
getKinectData();
rotateCamera();
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
glEnableClientState(GL_VERTEX_ARRAY);
glEnableClientState(GL_COLOR_ARRAY);
glBindBuffer(GL_ARRAY_BUFFER, vboId);
glVertexPointer(3, GL_FLOAT, 0, NULL);
glBindBuffer(GL_ARRAY_BUFFER, cboId);
glColorPointer(3, GL_FLOAT, 0, NULL);
glPointSize(1.f);
glDrawArrays(GL_POINTS, 0, width*height);
glDisableClientState(GL_VERTEX_ARRAY);
glDisableClientState(GL_COLOR_ARRAY);
}
注意,我们也可以用下面的代码替换掉所有的数组缓存代码:
// Global Variables
float colorarray[width*height*3];
float vertexarray[width*height*3];
//...
void getKinectData() {
getDepthData((*GLubyte*) vertexarray);
getRgbData((GLubyte*) colorarray);
}
void drawKinectData() {
getKinectData();
rotateCamera();
glBegin(GL_POINTS);
for (int i = 0; i < width*height; ++i) {
glColor3f(colorarray[i*3], colorarray[i*3+1], colorarray[i*3+2]);
glVertex3f(vertexarray[i*3], vertexarray[i*3+1], vertexarray[i*3+2]);
}
glEnd();
}
结束!构建并运行,确保你的 Kinect 已经插入。你应该会看到一个包含 Kinect 所拍摄的旋转的彩色点云的(视频流)窗口。

Kinect 骨骼追踪¶
目标: | 学习如何从 Kinect 获取骨骼 (skeleton) 跟踪数据,尤其是关节点 (joints) 位置。 |
---|---|
源码: | 点此查看 4_SkeletalTracking.zip |
概述¶
本章教程相当简单,展示了如何获取在 Kinect 视角下的基本人体信息。我们将会展示如何提取身体关节的 3D 位置,这样的信息经过进一步处理,可以实现从简单的绘制骨骼、到复杂的姿势识别在内的各种事情。
为此,我们将从我们在上一章中创建的框架开始,并添加一些内容。
Kinect 代码¶
全局变量¶
我们将维护一个全局变量,来保存最近捕捉到的身体的所有关节点。
// Body tracking variables
Vector4 skeletonPosition[NUI_SKELETON_POSITION_COUNT];
总共有NUI_SKELETON_POSITION_COUNT = 20
个关节点会被 Kinect 追踪,具体可以看这里。每个关节点用一个Vector4
类型来描述其在相机坐标系下的 3D 位置。例如,要看右肘的位置,可以使用skeletonPosition[NUI_SKELETON_POSITION_ELBOW_RIGHT]
。
注解
译者注:上面的链接已失效,替代的网页快照可以看这里。
Kinect 初始化¶
我们在 Kinect 初始化中添加了两个新的部分。首先,我们在初始化 Kinect 时请求了骨骼追踪数据,然后,我们启用了追踪。
默认情况下,Kinect 希望人们能够面对传感器站立,以便更好地跟踪。在函数NuiSkeletonTrackingEnable()
中,通过设置第二个参数,也可以指示设备跟踪对面坐着的人(比如坐在沙发上)。
从 Kinect 中获取关节数据¶
获取骨骼数据要比获取彩色数据和深度数据更简单——它没有锁定或其它繁琐的操作。我们只需从传感器获取一串NUI_SKELETON_FRAME
类型的数据。关节点追踪的噪声可能会很大,所以为了降低关节点位置随时间的抖动,我们还调用了一个内置的平滑函数。
void getSkeletalData() {
NUI_SKELETON_FRAME skeletonFrame = {0};
if (sensor->NuiSkeletonGetNextFrame(0, &skeletonFrame) >= 0) {
sensor->NuiTransformSmooth(&skeletonFrame, NULL);
// Process skeletal frame (see below)...
}
}
Kinect 可以同时追踪最多NUI_SKELETON_COUNT
个用户(在 SDK 中,NUI_SKELETON_COUNT == 6
)。骨骼数据结构NUI_SKELETON_DATA
可以在帧的SkeletonData
数组字段中访问。注意,每个骨骼不一定指向 Kinect 可以看到的实际的人,第一个被跟踪的身体也不一定是数组中的第一个元素。因此,我们需要检查数组中的每个元素是否是被跟踪的身体。通过检查骨骼的追踪状态来做这件事。
// Loop over all sensed skeletons
for (int z = 0; z < NUI_SKELETON_COUNT; ++z) {
const NUI_SKELETON_DATA& skeleton = skeletonFrame.SkeletonData[z];
// Check the state of the skeleton
if (skeleton.eTrackingState == NUI_SKELETON_TRACKED) {
// Get skeleton data (see below)...
}
}
一旦我们有了一个有效的跟踪骨骼,我们就可以将所有的关节点数据复制到我们的关节点位置数组中。鉴于 Kinect 可能会跟丢一部分关节点(比如用户的手臂藏到了背后,或遇到其它遮挡),我们还单独检查每个关节的追踪状态。我们使用Vector4
关节位置的 w 坐标来记录它是否是一个有效的跟踪关节。
// For the first tracked skeleton
{
// Copy the joint positions into our array
for (int i = 0; i < NUI_SKELETON_POSITION_COUNT; ++i) {
skeletonPosition[i] = skeleton.SkeletonPositions[i];
if (skeleton.eSkeletonPositionTrackingState[i] == NUI_SKELETON_POSITION_NOT_TRACKED) {
skeletonPosition[i].w = 0;
}
}
return; // Only take the data for one skeleton
}
OpenGL 显示¶
我们用关节点数组中的坐标绘制一些简单的线条,来显示人体的上肢。也就是说,我们要从右肩到右肘画一条线,然后从右肘到右手腕画一条线;左边也是一样的道理。当然,只有在 Kinect 检测到人的情况下才需要画线,所以我们要先检查向量的 w 坐标是否有效。
void drawKinectData() {
// ...
const Vector4& lh = skeletonPosition[NUI_SKELETON_POSITION_HAND_LEFT];
const Vector4& le = skeletonPosition[NUI_SKELETON_POSITION_ELBOW_LEFT];
const Vector4& ls = skeletonPosition[NUI_SKELETON_POSITION_SHOULDER_LEFT];
const Vector4& rh = skeletonPosition[NUI_SKELETON_POSITION_HAND_RIGHT];
const Vector4& re = skeletonPosition[NUI_SKELETON_POSITION_ELBOW_RIGHT];
const Vector4& rs = skeletonPosition[NUI_SKELETON_POSITION_SHOULDER_RIGHT];
glBegin(GL_LINES);
glColor3f(1.f, 0.f, 0.f);
if (lh.w > 0 && le.w > 0 && ls.w > 0) {
// lower left arm
glVertex3f(lh.x, lh.y, lh.z);
glVertex3f(le.x, le.y, le.z);
// upper left arm
glVertex3f(le.x, le.y, le.z);
glVertex3f(ls.x, ls.y, ls.z);
}
if (rh.w > 0 && re.w > 0 && rs.w > 0) {
// lower right arm
glVertex3f(rh.x, rh.y, rh.z);
glVertex3f(re.x, re.y, re.z);
// upper right arm
glVertex3f(re.x, re.y, re.z);
glVertex3f(rs.x, rs.y, rs.z);
}
glEnd();
}
结束!构建并运行,确保你的 Kinect 已经插入。你应该会看到一个包含 Kinect 所拍摄的旋转的彩色点云的(视频流)窗口,当 Kinect 捕捉到人体时,则绘制红线来展示这个人的上肢姿态。
准备工作¶
注解
这一部分教程是针对第二代 Kinect 的。你的 Kinect 是第一代的吗?来看 Kinect v1.8 教程吧!
本系列教程适用于希望使用微软 Kinect SDK 的 C++ 程序员。教程将尽量精简 Windows 代码。我们将使用 OpenGL 提供图形化操作。
目标: | 本章将使用正确的配置初始化一个 Visual Studio 项目。确保你拥有 Kinect 编程所需要的所有组件。 |
---|
注解
译者注:在本章开始之前,确保 Kinect SDK 已顺利安装,可参考附录。
环境需求¶
- Windows(这毕竟是Windows SDK嘛~)
- 一台 Kinect(最好是 Kinect for PC)
- Visual Studio(选择一个较新的版本)
- C/C++ 语言编程经验
- 熟悉(或愿意学习)OpenGL
本教程将兼容 GLUT 和 SDL。
安装 GLUT¶
你可以选择使用 GLUT 或 SDL 作为 OpenGL 的接口。在本节中将展示在系统中安装 GLUT 的步骤,你也可以选择跳过本节,在安装 SDL 一节中查看 SDL 的安装流程。
大多数 windows 系统并不会自带 GLUT。GLUT 是一套在 OpenGL 中用于窗口化和处理事件的代码库,它非常陈旧了,但使用依然很广泛。
- 去这个网站下载并解压 MSVC freeglut 的二进制文件。
- 将刚刚解压的
include/
目录和lib/
目录中的内容复制到合适的 Windows SDK 目录中,如: - Visual Studio 2010 中:
C:/Program Files/Microsoft SDKs/Windows/v7.0A/Include/
和C:/Program Files/Microsoft SDKs/Windows/v7.0A/Lib/
- Visual Studio 2012以上:
C:/Program Files/Windows Kits (x86)/8.1/Include/um/
和C:/Program Files (x86)/Windows Kits/8.1/Lib/winv6.3/um/
- Visual Studio 2010 中:
- 将刚刚解压的
- 复制
bin/x64/freeglut.dll
到C:/Windows/System32
、bin/x86/freeglut.dll
到C:/Windows/SysWOW64
。如果你的系统是 32 位的,只需要把bin/x86/freeglut.dll
复制到C:/Windows/System32
。
注解
译者注:步骤1和2,如果不想污染自己的系统环境,也可以在解压后不去复制这些文件,稍后在 Visual Studio 项目中配置对应地址即可;步骤3,也可以选择与自己系统对应的 .dll 文件,稍后复制到项目的运行目录中。
安装 SDL¶
你可以选择使用 GLUT 或 SDL 作为 OpenGL 的接口。在本节中将展示在系统中安装 SDL 的步骤,如果在上一节已选择安装 GLUT,请跳过本节。
去 SDL 官网下载下载最新版本的 Visual C++ SDL 开发库。在本教程编写期间 SDL 版本稳定在 1.2.15 。
注解
译者注 :SDL 开发库对应官网中的 Development Libraries,不是 Runtime Binaries。
将下载到的压缩文件解压至一个合适的位置(如:C:\
)。
人生苦短,做完下面两件事,会让我们的生活会更加惬意:
- 复制
SDL.dll
到我们的系统目录中。打开C:\SDL-1.2.15\lib\x64
(32 位系统下为/x86
),复制SDL.dll
。如果你的系统是 64 位的,就将其粘贴到C:\Windows\SysWOW64
中;否则,就将它粘贴到C:\Windows\System32
。 - 将 SDL 文件夹添加为一项环境变量。打开系统的环境变量,新建一个变量,其中变量名为
SDL_DIR
、变量值为 SDL 文件夹所在地址,如:C:\SDL-1.2.15
。
注解
译者注 :同样,.dll 文件也可以不必复制到系统目录,只需稍后复制到项目的运行目录中。
新建 Kinect 项目¶
打开 Visual Studio,并新建一个 C++ 空项目。

为了配置构建规则(即 includes 和 libs),我们需要使用属性管理器。将不同组件的配置保存在不同的配置文件中,可以方便之后重复使用。
属性管理器可以在视图菜单中打开。

为了模块化配置,我们创建一个属性表用于 OpenGL 信息,另一个用于 Kinect SDK 信息。在属性管理器中,右键项目名称,选择添加新项目属性表,命名为 OpenGL;再新建一个属性表,命名为 KinectSDK。你将会在 “Debug” 和 “Release” 目录下看到以它们命名的新属性表。属性表是包含构建配置数据的、后缀为 .props 的文件。


首先配置Kinect 属性。右键 Kinect 属性表,点击“属性”。
- 在
C/C++ > 常规 > 附加包含目录
中,添加$(KINECTSDK20_DIR)/inc
。 - 在
链接器 > 常规 > 附加库目录
中,添加$(KINECTSDK20_DIR)/lib/amd64
(32 位系统下为/x86
)。 - 在
链接器 > 输入 > 附加依赖项
中,添加kinect20.lib
。
注意,要添加 include 和 library 目录,你可以直接在对应输入框中键入,也可以单击下拉框,选择<编辑>,然后在弹出的对话框中输入位置。这种方法可以在管理多个目录的时候轻松一些,并且让你浏览目录更加清晰准确(如果不使用KINECTSDK20_DIR
这样的环境变量)。
注解
译者注 :包含文件和库文件也可以直接添加在属性表的VC++目录 > 包含目录
和VC++目录 > 库目录
中。
注解
译者注 :注意检查 Kinect SDK 的包含文件和库文件目录是否正确,最好在文件夹中打开确认一下。
接下来,配置 OpenGL 属性表。
GLUT 方式¶
如果在前面选择了 GLUT 作为 OpenGL 的接口,需要依据本节步骤配置 OpenGL 属性表。
右键 OpenGL 属性表,点击“属性”,在链接器 > 输入 > 附加依赖项
中,添加以下内容:
opengl32.lib
glu32.lib
freeglut.lib

注解
译者注 :如果在安装 GLUT 时没有将对应包含文件和库文件复制到系统目录下,还需要将它们的地址添加到该属性表对应的包含目录和库目录中。
SDL 方式¶
如果在前面选择了 SDL 作为 OpenGL 的接口,需要依据本节步骤配置 OpenGL 属性表。
右键 OpenGL 属性表,点击“属性”。
- 在
C/C++ > 常规 > 附加包含目录
中,添加$(SDL_DIR)\include
。 - 在
链接器 > 常规 > 附加库目录
中,添加$(SDL_DIR)\lib\x64
(32 位系统下为/x86
)。 - 在
链接器 > 输入 > 附加依赖项
中,添加以下内容:
opengl32.lib
glu32.lib
SDL.lib
SDLmain.lib

至此,我们的项目就准备好了。
Kinect 基础¶
目标: | 学习如何初始化一台 Kinect,并从中获取 RGB 类型的数据。 |
---|---|
源码: | 点此查看 1_Basics.zip |
概述¶
我们有两段与 Kinect 相关的代码,下文将详细讨论这些内容,然后对展示部分的代码做一个简要介绍。
包含文件,常量和全局变量¶
包含文件¶
文件名基本已经解释清楚了它自己:Kinect.h
,即为 Kinect 的主要头文件。
为了让 Kinect 的包含文件正常工作,你还需要在前面添加Ole2.h
和Windows.h
。同时也不要忘了添加可视化相关的包含文件。
// GLUT
#include <Windows.h>
#include <Ole2.h>
#include <gl/GL.h>
#include <gl/GLU.h>
#include <gl/glut.h>
#include <Kinect.h>
// SDL
#include <Windows.h>
#include <Ole2.h>
#include <SDL_opengl.h>
#include <SDL.h>
#include <Kinect.h>
常量和全局变量¶
我们把窗口的宽度和高度定义为 1920*1080,这恰好也是 Kinect 摄像头输入的图像尺寸。
注意,data
数组将会保存一份我们从 Kinect 中获取的图像的副本,以便我们将其作为纹理 (texture)。有经验的 OpenGL 用户也可以使用帧缓存对象 (Frame Buffer Object) 来代替它。
#define width 1920
#define height 1080
// OpenGL Variables
GLuint textureId; // ID of the texture to contain Kinect RGB Data
GLubyte data[width*height*4]; // BGRA array containing the texture data
// Kinect variables
IKinectSensor* sensor; // Kinect sensor
IColorFrameReader* reader; // Kinect color data source
Kinect 代码¶
Kinect 初始化¶
下面是我们的第一段与 Kinect 相关的代码。initKinect()
函数的作用是初始化一个 Kinect 设备。它包含了两个部分:首先,我们需要找到一个已连接到电脑的 Kinect 传感器,然后将其初始化并准备从中读取数据。
bool initKinect() {
if (FAILED(GetDefaultKinectSensor(&sensor))) {
return false;
}
if (sensor) {
sensor->Open();
IColorFrameSource* framesource = NULL;
sensor->get_ColorFrameSource(&framesource);
framesource->OpenReader(&reader);
if (framesource) {
framesource->Release();
framesource = NULL;
}
return true;
} else {
return false;
}
}
以下是一些注意事项:
- 通常情况下,我们需要对上面所有函数的返回值都倍加小心;但是为了简单起见,我们在这里忽略了一些内容。
- 注意数据流请求的一般模式:
- 编写一个适当类型的数据帧输入源 (framesource),包括彩色 (Color)、深度 (Depth)、肢体 (Body) 等。
- 通过传感器接口来请求数据帧输入源。
- 从数据帧输入源中打开读取器。
- 安全地释放数据帧输入源。
- 从读取器中请求数据。
从 Kinect 中获取 RGB 帧¶
void getKinectData(GLubyte* dest) {
IColorFrame* frame = NULL;
if (SUCCEEDED(reader->AcquireLatestFrame(&frame))) {
frame->CopyConvertedFrameDataToArray(width*height*4, data, ColorImageFormat_Bgra);
}
if (frame) frame->Release();
}
这个函数非常简单。我们从数据源中轮询 (poll for) 一个帧,如果有可用的帧,我们就将它以适当的格式复制到纹理 (texture) 数组中。不要忘记在这之后还要释放掉数据帧。
注意,原始彩色帧可能是 YUV 或其它类似的格式,因此需要一些处理将其转换为可用的 RGB/BGR 格式。你也可以使用frame->AccessUnderlyingBuffer()
访问原始数据,我们将在下一章节的教程中使用它。
数据帧的元数据也是可以访问的。下面是一些你可能感兴趣的事情:
- 相机设置,如曝光 (exposure) 和增益 (gain),可以通过
IColorCameraSettings
来访问:
IColorCameraSettings* camerasettings;
frame->get_ColorCameraSettings(&camerasettings);
float gain;
TIMESPAN exposure;
camerasettings->get_Gain(&gain);
camerasettings->get_ExposureTime(&exposure);
// ...etc.
- 维度和视野范围可以通过
IFrameDescription
访问:
IFrameDescription* description;
frame->get_FrameDescription(&description);
int height, width;
float xfov, yfov;
description->get_Height(&height);
description->get_Width(&width);
description->get_HorizontalFieldOfView(&xfov);
description->get_VerticalFieldOfView(&yfov);
// ...etc.
这也适用于深度数据帧和红外数据帧。
更多细节可以访问这个页面。
以上就是所有 Kinect 部分的代码!剩下的就是如何把它搬上屏幕。
窗口化,事件处理和主循环¶
本节将会解释与 GLUT——或 SDL——相关的代码,包括了窗口初始化、事件处理和主更新循环。
具体的初始化代码取决于使用那种实现方式 (GLUT 或 SDL)。它只是使用适当的 API 初始化一个窗口,失败时返回 false。GLUT 版本的实现还会通过指定draw()
函数在每次循环迭代中被调用来设置主循环。
主循环在execute()
函数中启动。在 GLUT中,循环是在后台处理的,所以我们需要做的就是调用glutMainLoop()
函数。在 SDL 中,我们编写自己的循环。在每个循环中,我们在屏幕上绘制新的帧,这个处理是在drawKinect()
函数中完成的。
如果你希望通过 GLUT 或 SDL 进行更复杂的窗口和循环管理,或学习更多关于这些函数的知识,网络上也有很多其它的参考资料。
GLUT
void draw() {
drawKinectData();
glutSwapBuffers();
}
void execute() {
glutMainLoop();
}
bool init(int argc, char* argv[]) {
glutInit(&argc, argv);
glutInitDisplayMode(GLUT_DEPTH | GLUT_DOUBLE | GLUT_RGBA);
glutInitWindowSize(width,height);
glutCreateWindow("Kinect SDK Tutorial");
glutDisplayFunc(draw);
glutIdleFunc(draw);
return true;
}
SDL
void execute() {
SDL_Event ev;
bool running = true;
while (running) {
while (SDL_PollEvent(&ev)) {
if (ev.type == SDL_QUIT) running = false;
}
drawKinectData();
SDL_GL_SwapBuffers();
}
}
bool init(int argc, char* argv[]) {
SDL_Init(SDL_INIT_EVERYTHING);
SDL_Surface* screen =
SDL_SetVideoMode(width, height, 32, SDL_HWSURFACE | SDL_GL_DOUBLEBUFFER | SDL_OPENGL);
return screen;
}
通过 OpenGL 显示¶
初始化¶
代码中描述了三个步骤——设置纹理以包含图像帧,准备 OpenGL 来绘制纹理,以及设置摄像机视点(对 2D 图像使用正投影)。
// Initialize textures
glGenTextures(1, &textureId);
glBindTexture(GL_TEXTURE_2D, textureId);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, width, height,
0, GL_BGRA, GL_UNSIGNED_BYTE, (GLvoid*) data);
glBindTexture(GL_TEXTURE_2D, 0);
// OpenGL setup
glClearColor(0,0,0,0);
glClearDepth(1.0f);
glEnable(GL_TEXTURE_2D);
// Camera setup
glViewport(0, 0, width, height);
glMatrixMode(GL_PROJECTION);
glLoadIdentity();
glOrtho(0, width, height, 0, 1, -1);
glMatrixMode(GL_MODELVIEW);
glLoadIdentity();
显然,我们应该用一个函数把上面的片段包起来,这里为了方便直接把它塞进了main()
函数中。
int main(int argc, char* argv[]) {
if (!init(argc, argv)) return 1;
if (!initKinect()) return 1;
/* ...OpenGL texture and camera initialization... */
// Main loop
execute();
return 0;
}
将图像帧画到屏幕上¶
这部分是很常规的代码。首先将 Kinect 数据复制到我们的缓存区中,然后指定我们的纹理使用这个缓冲区。
void drawKinectData() {
glBindTexture(GL_TEXTURE_2D, textureId);
getKinectData(data);
glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, width, height, GL_BGRA, GL_UNSIGNED_BYTE, (GLvoid*)data);
然后,绘制一个以图像帧为纹理的方框。
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
glBegin(GL_QUADS);
glTexCoord2f(0.0f, 0.0f);
glVertex3f(0, 0, 0);
glTexCoord2f(1.0f, 0.0f);
glVertex3f(width, 0, 0);
glTexCoord2f(1.0f, 1.0f);
glVertex3f(width, height, 0.0f);
glTexCoord2f(0.0f, 1.0f);
glVertex3f(0, height, 0.0f);
glEnd();
}
结束!构建并运行,确保你的 Kinect 已经插入。你应该会看到一个包含 Kinect 所拍摄内容的视频流窗口。
Kinect 深度数据¶
目标: | 学习如何从一台 Kinect 中获取深度数据,并了解数据的格式是什么样的。 |
---|---|
源码: | 点此查看 2_Depth.zip |
概述¶
本章教程非常短——与前一章获取 RGB 数据的代码相比只有两处重要的改动。
另外,为了方便将注意力集中在 Kinect 的细节上,我们重构了 SDL 和 GLUT 组件,将它们放置在了不同的文件中。
Kinect 代码¶
Kinect 初始化¶
要从 Kinect 中获取深度数据,只需要改变一下数据帧输入源、数据帧读取器和数据帧的类型。同时还要注意,深度相机的分辨率是512*424,与彩色相机 (1920*1080) 是不同的。
IDepthFrameReader* reader; // Kinect depth data source
bool initKinect() {
if (FAILED(GetDefaultKinectSensor(&sensor))) {
return false;
}
if (sensor) {
sensor->Open();
IDepthFrameSource* framesource = NULL;
sensor->get_DepthFrameSource(&framesource);
framesource->OpenReader(&reader);
if (framesource) {
framesource->Release();
framesource = NULL;
}
return true;
} else {
return false;
}
}
从 Kinect 中获取深度数据帧¶
我们将以灰度图像的形式显示 Kinect 的深度图像。每个像素的值将是该像素点到 Kinect 的距离 (mm) 除以 256 的余数。
void getKinectData(GLubyte* dest) {
IDepthFrame* frame = NULL;
if (SUCCEEDED(reader->AcquireLatestFrame(&frame))) {
unsigned int sz;
unsigned short* buf;
frame->AccessUnderlyingBuffer(&sz, &buf);
const unsigned short* curr = (const unsigned short*)buf;
const unsigned short* dataEnd = curr + (width*height);
while (curr < dataEnd) {
// Get depth in millimeters
unsigned short depth = (*curr++);
// Draw a grayscale image of the depth:
// B,G,R are all set to depth%256, alpha set to 1.
for (int i = 0; i < 3; ++i)
*dest++ = (BYTE)depth % 256;
*dest++ = 0xff;
}
}
if (frame) frame->Release();
}
注意,与彩色帧不同,我们只想访问来自帧的原始数据。为此,我们使用frame->AccessUnderlyingBuffer
来获得指向数据的指针。
以上就是所有 Kinect 部分的代码!剩下的就是如何把它搬上屏幕。
显示框架¶
这里重构了代码,将 GLUT 和 SDL 部分打包进了init()
函数、draw()
函数和execute()
函数中。
要使用它们,只需要在main.cpp
的最上方包含glut.h
或sdl.h
。确保项目属性中有正确的包含文件路径和链接文件路径。
结束!构建并运行,确保你的 Kinect 已经插入。你应该会看到一个包含 Kinect 所拍摄内容的灰度视频流窗口。
Kinect 点云¶
目标: | 学习如何对齐彩色和深度图像,以获得彩色点云。 |
---|---|
源码: | 点此查看 3_PointCloud.zip |
概述¶
在本章教程中,我们想采取一些新步骤。最有趣的部分就是,我们现在正在处理 3D 数据!但是,创建一个交互式系统的工作量对我们的教程来说有些多了,所以我们只是创建一个简单地旋转点云。本章教程分为三部分:首先,我们简单讨论一下为什么点云可能要比你想像的更难;其次,我们将展示如何使用 Kinect SDK 获取正确的数据;最后,我们将会展示一些可以降低图像展示难度的 OpenGL技巧。
深度和 RGB 坐标系¶
对齐¶
最简单的生成点云的方法是直接重叠深度和彩色图像,使深度像素 (x, y) 与图像像素 (x, y) 一一匹配。然而,这样生成的深度图像质量非常低,对象的边界和颜色无法对齐。这是因为 RGB 相机和深度相机位于 Kinect 上面的不同位置;显然,它们看到的东西并不一致!通常,我们需要对两个相机进行某种对齐操作(正式术语是「注册」, registration),这样我们就可以知道哪个彩色像素是与哪个深度像素相对应了。幸运的是,微软粑粑已经帮我们做好这件事了,我们唯一需要做的就是调用正确的函数。
直接重叠 RGB 和深度

“注册”后的 RGB 和深度

要注意,计算机视觉和机器人学方面的研究人员并不喜欢这个内置注册函数的输出质量,所以他们经常用 OpenCV 之类的工具手动进行校正。
Kinect 代码¶
我们需要处理两个新问题,相应地也有两个新的接口:
- 在此之前,我们每次只需要处理一个数据流。然而,现在我们要同时使用彩色和深度数据(可能还有其他类型的数据,比如红外或身体跟踪数据),我们希望确保正在处理的数据是同步的。否则,彩色图像和深度数据将会对应不起来!为了解决这个问题,我们使用
MultiSourceFrameReader
,它可以提供多个同步的数据帧输入源。 - 为了处理上面提到的不同坐标系统,SDK 提供了一个可以完成所有转换工作的对象:
ICoordinateMapper
。
Kinect 初始化¶
IKinectSensor* sensor; // Kinect sensor
IMultiSourceFrameReader* reader; // Kinect data source
ICoordinateMapper* mapper; // Converts between depth, color, and 3d coordinates
bool initKinect() {
if (FAILED(GetDefaultKinectSensor(&sensor))) {
return false;
}
if (sensor) {
sensor->get_CoordinateMapper(&mapper);
sensor->Open();
sensor->OpenMultiSourceFrameReader(
FrameSourceTypes::FrameSourceTypes_Depth | FrameSourceTypes::FrameSourceTypes_Color,
&reader);
return reader;
} else {
return false;
}
}
我们通过指定在帧中需要哪些数据来打开MultiSourceFrameReader()
。我们还可以请求红外数据、身体跟踪数据和音频数据。我们不需要从帧读取器中单独处理帧输入源。
我们还在这里初始化了ICoordinateMapper
。
从 MultiSourceFrame 中获取深度数据¶
无论是深度数据还是颜色数据,我们都可以从MultiSourceFrame
中得到熟悉的IDepthFrame
和IColorFrame
。下面是深度数据对应的代码:
void getKinectData() {
IMultiSourceFrame* frame = NULL;
reader->AcquireLatestFrame(&frame);
// ...
getDepthData(frame, dest);
// ...
getColorData(frame, dest;
}
void getDepthData(IMultiSourceFrame* frame, GLubyte* dest) {
IDepthFrame* depthframe;
IDepthFrameReference* frameref = NULL;
frame->get_DepthFrameReference(&frameref);
frameref->AcquireFrame(&depthframe);
if (frameref) frameref->Release();
if (!depthframe) return;
// Process depth frame data...
if (depthframe) depthframe->Release();
}
这段代码基本与彩色数据部分相同,只要用 “color” 替换所有出现的 “depth”。
使用坐标系映射接口¶
我们在这个程序中处理了三个不同的坐标空间:
- 点云坐标所在的 3D (XYZ) 空间,用于显示。
- 1920*1080 彩色图像中,像素坐标的二维(列、行)空间。
- 512*424 深度图像中,像素坐标的二维(列、行)空间。
我们将为深度图像中的每个像素显示一个 3D 点。每个点都有对应的 RGB 颜色和 XYZ 坐标。因此,我们将使用CoordinateMapper
获取深度像素坐标与三维点坐标之间的查找表映射,以及深度像素坐标与彩色图像中相应像素之间的另一个映射。
// Global Variables
/*
For reference:
struct ColorSpacePoint { float X, Y; };
struct CameraSpacePoint { float X, Y, Z; };
*/
ColorSpacePoint depth2rgb[width*height]; // Maps depth pixels to rgb pixels
CameraSpacePoint depth2xyz[width*height]; // Maps depth pixels to 3d coordinates
// ...
void getDepthData(IMultiSourceFrame* frame, GLubyte* dest) {
IDepthFrame* depthframe;
// Populate depthframe from MultiSourceFrame...
// Get data from frame
unsigned int sz;
unsigned short* buf;
depthframe->AccessUnderlyingBuffer(&sz, &buf);
mapper->MapDepthFrameToCameraSpace(
width*height, buf, // Depth frame data and size of depth frame
width*height, depth2xyz); // Output CameraSpacePoint array and size
mapper->MapDepthFrameToColorSpace(
width*height, buf, // Depth frame data and size of depth frame
width*height, depth2rgb); // Output ColorSpacePoint array and size
}
为了得到这些映射,我们调用适当的ICoordinateMapper
函数。你可以在ICoordinateMapper文档中找到其他可能的映射。请注意,大多数映射函数都需要深度帧作为输入数组(即使是名称中没有以 “DepthFrame” 开头的映射函数)。
从 Kinect 中获取深度数据¶
现在我们处理 3D 数据。我们想把深度图像帧想像成空间中的一束点,而不是一个 512*424 的图像。因此在我们的getDepthData()
函数中,我们将用每个点的坐标(而不是每个像素的深度)填充我们的缓冲区。这意味着对于float
类型的坐标来说,我们需要填充的缓存区需要达到width*height*3*sizeof(float)
的大小。
这里,我们只使用CoordinateMapper
的depth2xyz
映射。
void getDepthData(IMultiSourceFrame* frame, GLubyte* dest) {
// Populate depth2xyz map...
float* fdest = (float*)dest;
for (int i = 0; i < width*height i++) {
*fdest++ = depth2xyz[i].X;
*fdest++ = depth2xyz[i].Y;
*fdest++ = depth2xyz[i].Z;
}
/* We use the fdest pointer for conciseness. Equivalently, we could use
for (int i = 0; i < width*height; i++) {
fdest[3*i+0] = depth2xyz[i].X;
fdest[3*i+1] = depth2xyz[i].Y;
fdest[3*i+2] = depth2xyz[i].Z;
}
*/
}
从 Kinect 中获取彩色数据¶
现在,我们考虑的是点而不是矩形网格,我们希望我们的彩色输出与特定的深度点相关联。特殊地,类似于getDepthData()
函数,我们的getRgbData()
函数的输入需要一个大小为width*height*3*sizeof(float)
的缓存区来存储点云中每个点的红、绿、蓝色彩值。
void getRgbData(IMultiSourceFrame* frame, GLubyte* dest) {
IColorFrame* colorframe;
// Populate colorframe...
colorframe->CopyConvertedFrameDataToArray(colorwidth*colorheight*4, rgbimage, ColorImageFormat_Rgba);
// Write color array for vertices
float* fdest = (float*)dest;
for (int i = 0; i < width*height; i++) {
ColorSpacePoint p = depth2rgb[i];
// Check if color pixel coordinates are in bounds
if (p.X < 0 || p.Y < 0 || p.X > colorwidth || p.Y > colorheight) {
*fdest++ = 0;
*fdest++ = 0;
*fdest++ = 0;
}
else {
int idx = (int)p.X + colorwidth*(int)p.Y;
*fdest++ = rgbimage[4*idx + 0]/255.;
*fdest++ = rgbimage[4*idx + 1]/255.;
*fdest++ = rgbimage[4*idx + 2]/255.;
}
// Don't copy alpha channel
}
/* We use fdest pointer for conciseness; Equivalently, we could use
for (int i = 0; i < width*height; i++) {
fdest[3*i+0] = ...
fdest[3*i+1] = ...
fdest[3*i+2] = ...
}
*/
}
在这个块中,我们遍历深度图像的像素,使用从CoordinateMapper
获取的depth2xyz
映射查找彩色图像中的对应坐标。我们检查每个深度像素是否确实映射到了 RGB 图像中的某个像素上面,如果没有,那么我们就手动给这个点分配为黑色。
在最后几行代码中有一些有趣的数学运算,我们来通读一下。首先,彩色图像帧采用 BGRA 格式,每个通道一字节,逐行排列。所以像素 (x, y) 的线性指数是x + width*y
。X和Y可以是浮点数,所以在使用它们作为数组索引之前,我们先将它们向下取整为int
类型。然后,我们想要的 4 字节块位于linearindex*4
。最后,我们想要把按字节取值 (0-255) 的 BGRA 格式转换为按浮点数取值 (0.0-1.0) 的 RGB 格式,所以我们取前三个通道,并除以 255:rgbimage[4*linearindex + channel]/255.f
。
OpenGL 显示¶
我们要用数组缓存 (array buffers) 来显示我们的点云。什么是数组缓存?他们允许你通过调用一个函数来替换一系列的glBegin()
、glColor()
、glVertex()
、glEnd()
调用。另外,数组缓存存储在 GPU 里面,因此显示的时候效率会更高。不过,它们也确实使代码变得更复杂了。想要跳过数组缓存吗?来这里。
要使用数组缓存,我们需要引入 OpenGL 的扩展。为了简化这一过程,我们选择使用 GLEW。
安装 GLEW¶
- 去这个网站下载并解压 GLEW 的二进制文件。
- 复制解压文件夹中的
include/
和Lib/
目录,到合适的 Windows SDK 目录中,如: - Visual Studio 2010 中:
C:/Program Files/Microsoft SDKs/Windows/v7.0A/Include/
和C:/Program Files/Microsoft SDKs/Windows/v7.0A/Lib/
- Visual Studio 2012以上:
C:/Program Files/Windows Kits (x86)/8.1/Include/um/
和C:/Program Files (x86)/Windows Kits/8.1/Lib/winv6.3/um/
- Visual Studio 2010 中:
- 复制解压文件夹中的
- 复制
bin/x64/glew32.dll
到C:/Windows/System32
、bin/x86/glew32.dll
到C:/Windows/SysWOW64
。如果你的系统是 32 位的,只需要把bin/x86/glew32.dll
复制到C:/Windows/System32
。
将glew32.lib
添加至 OpenGL 或 SDL 属性表的链接器 > 输入 > 附加依赖项
中。
注解
译者注:与第一章提到的相同,步骤1和2,如果不想污染自己的系统环境,也可以在解压后不去复制这些文件,稍后在 Visual Studio 项目中配置对应地址即可;步骤3,也可以选择与自己系统对应的 .dll 文件,稍后复制到项目的运行目录中。
OpenGL 代码¶
既然是处理 3D 数据,我们还需要注意相机设置。我们使用gluPerspective()
和gluLookAt()
函数来为我们解决这个问题。
// Global variables:
GLuint vboId; // Vertex buffer ID
GLuint cboId; // Color buffer ID
// ...
// OpenGL setup
glClearColor(0,0,0,0);
glClearDepth(1.0f);
// Set up array buffers
const int dataSize = width*height * 3 * 4;
glGenBuffers(1, &vboId);
glBindBuffer(GL_ARRAY_BUFFER, vboId);
glBufferData(GL_ARRAY_BUFFER, dataSize, 0, GL_DYNAMIC_DRAW);
glGenBuffers(1, &cboId);
glBindBuffer(GL_ARRAY_BUFFER, cboId);
glBufferData(GL_ARRAY_BUFFER, dataSize, 0, GL_DYNAMIC_DRAW);
// Camera setup
glViewport(0, 0, width, height);
glMatrixMode(GL_PROJECTION);
glLoadIdentity();
gluPerspective(45, width /(GLdouble) height, 0.1, 1000);
glMatrixMode(GL_MODELVIEW);
glLoadIdentity();
gluLookAt(0,0,0,0,0,1,0,1,0);
出于显示的目的,我们没有将它写成一个完整的互动界面,只是用一个“旋转”的摄像头,围绕 Kinect 前方 3 米的点旋转。详细信息请参阅代码。
融会贯通¶
我们写好了getDepthData()
和getRgbData()
,但是该怎么用呢?我们希望将适当的数据从MultiSourceFrame
中复制到 GPU 上。
void getKinectData() {
IMultiSourceFrame* frame = NULL;
if (SUCCEEDED(reader->AcquireLatestFrame(&frame))) {
GLubyte* ptr;
glBindBuffer(GL_ARRAY_BUFFER, vboId);
ptr = (GLubyte*)glMapBuffer(GL_ARRAY_BUFFER, GL_WRITE_ONLY);
if (ptr) {
getDepthData(frame, ptr);
}
glUnmapBuffer(GL_ARRAY_BUFFER);
glBindBuffer(GL_ARRAY_BUFFER, cboId);
ptr = (GLubyte*)glMapBuffer(GL_ARRAY_BUFFER, GL_WRITE_ONLY);
if (ptr) {
getRgbData(frame, ptr);
}
glUnmapBuffer(GL_ARRAY_BUFFER);
}
if (frame) frame->Release();
}
现在我们想要用glDrawArrays()
函数来绘制我们的点云。
void drawKinectData() {
getKinectData();
rotateCamera();
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
glEnableClientState(GL_VERTEX_ARRAY);
glEnableClientState(GL_COLOR_ARRAY);
glBindBuffer(GL_ARRAY_BUFFER, vboId);
glVertexPointer(3, GL_FLOAT, 0, NULL);
glBindBuffer(GL_ARRAY_BUFFER, cboId);
glColorPointer(3, GL_FLOAT, 0, NULL);
glPointSize(1.f);
glDrawArrays(GL_POINTS, 0, width*height);
glDisableClientState(GL_VERTEX_ARRAY);
glDisableClientState(GL_COLOR_ARRAY);
}
注意,我们也可以用下面的代码替换掉所有的数组缓存代码:
// Global Variables
float colorarray[width*height*3];
float vertexarray[width*height*3];
//...
void getKinectData() {
getDepthData((*GLubyte*) vertexarray);
getRgbData((GLubyte*) colorarray);
}
void drawKinectData() {
getKinectData();
rotateCamera();
glBegin(GL_POINTS);
for (int i = 0; i < width*height; ++i) {
glColor3f(colorarray[i*3], colorarray[i*3+1], colorarray[i*3+2]);
glVertex3f(vertexarray[i*3], vertexarray[i*3+1], vertexarray[i*3+2]);
}
glEnd();
}
结束!构建并运行,确保你的 Kinect 已经插入。你应该会看到一个包含 Kinect 所拍摄的旋转的彩色点云的(视频流)窗口。

Kinect 骨骼追踪¶
目标: | 学习如何从 Kinect 获取骨骼 (skeleton) 跟踪数据,尤其是关节点 (joints) 位置。 |
---|---|
源码: | 点此查看 4_SkeletalTracking.zip |
概述¶
本章教程相当简单,展示了如何获取在 Kinect 视角下的基本人体信息。我们将会展示如何提取身体关节的 3D 位置,这样的信息经过进一步处理,可以实现从简单的绘制骨骼、到复杂的姿势识别在内的各种事情。
为此,我们将从我们在上一章中创建的框架开始,并添加一些内容。
Kinect 代码¶
全局变量¶
我们将维护一个布尔值作为全局变量,它告诉我们是否看到了一个物体(从而判断是否要绘制手臂);还要维护一个数组,保存上次看到的物体中所有关节点。
// Body tracking variables
BOOLEAN tracked; // Whether we see a body
Joint joints[JointType_Count]; // List of joints in the tracked body
关节点数组的每一个元素都是一个Joint
结构体,与被跟踪的人体的每个关节点相对应。这个文档列出了被 Kinect 跟踪的JointType_Count = 25
个关节点。例如,通过查看joints[JointType_ElbowRight].Position
,我们可以获得用户右肘的位置。
Kinect 初始化¶
这里唯一的新功能是:当我们打开MultiSourceFrameReader
时,我们发出人体跟踪数据的请求。
默认情况下,Kinect 希望人们能够面对传感器站立,以便更好地跟踪。在函数NuiSkeletonTrackingEnable()
中,通过设置第二个参数,也可以指示设备跟踪对面坐着的人(比如坐在沙发上)。
从 Kinect 中获取关节数据¶
在我们的新getBodyData()
函数中,我们仍然像以前一样从MultiSourceFrame
中请求帧。
void getBodyData(IMultiSourceFrame* frame) {
IBodyFrame* bodyframe;
IBodyFrameReference* frameref = NULL;
frame->get_BodyFrameReference(&frameref);
frameref->AcquireFrame(&bodyframe);
if (frameref) frameref->Release();
if (!bodyframe) return;
// ------ NEW CODE ------
IBody* body[BODY_COUNT];
bodyframe->GetAndRefreshBodyData(BODY_COUNT, body);
for (int i = 0; i < BODY_COUNT; i++) {
body[i]->get_IsTracked(&tracked);
if (tracked) {
body[i]->GetJoints(JointType_Count, joints);
break;
}
}
// ------ END NEW CODE ------
if (bodyframe) bodyframe->Release();
}
Kinect 可以同时追踪最多BODY_COUNT
个用户(在 SDK 中,BODY_COUNT == 6
)。我们使用IBodyFrame::GetAndRefreshBodyData()
函数填充IBody
指针数组。注意,每个IBody
不一定指向 Kinect 可以看到的一个实际的人,第一个被跟踪的人体也不一定是数组的第一个元素。因此,我们需要对每一个元素检查它是否是一个被跟踪的人体。
为了简单起见,我们每次只处理这个应用程序中的一个人。因此,我们检查返回的每一个人体是否都指向一个被跟踪的人,并在找到一个后跳出循环。我们使用IBody::GetJoints()
函数,用人体中所有的关节点位置填充joints
数组。我们还将跟踪状态保存在全局跟踪变量中。
注意:调用IBodyFrame::GetAndRefreshBodyData()
时始终以BODY_COUNT
作为第一个参数;调用IBody::GetJoint
时始终以JointType_Count
作为第一个参数。
OpenGL 显示¶
我们用关节点数组中的坐标绘制一些简单的线条,来显示人体的上肢。也就是说,我们要从右肩到右肘画一条线,然后从右肘到右手腕画一条线;左边也是一样的道理。当然,只有在 Kinect 检测到人的情况下才需要画线,所以我们要先检查全局布尔变量tracked
。
在Joint
结构体中的Position
域里面,关节点坐标以 3D CameraSpacePoint
的形式表示。
void drawKinectData() {
// ...
if (tracked) {
// Draw some arms
const CameraSpacePoint& lh = joints[JointType_WristLeft].Position;
const CameraSpacePoint& le = joints[JointType_ElbowLeft].Position;;
const CameraSpacePoint& ls = joints[JointType_ShoulderLeft].Position;;
const CameraSpacePoint& rh = joints[JointType_WristRight].Position;;
const CameraSpacePoint& re = joints[JointType_ElbowRight].Position;;
const CameraSpacePoint& rs = joints[JointType_ShoulderRight].Position;;
glBegin(GL_LINES);
glColor3f(1.f, 0.f, 0.f);
// lower left arm
glVertex3f(lh.X, lh.Y, lh.Z);
glVertex3f(le.X, le.Y, le.Z);
// upper left arm
glVertex3f(le.X, le.Y, le.Z);
glVertex3f(ls.X, ls.Y, ls.Z);
// lower right arm
glVertex3f(rh.X, rh.Y, rh.Z);
glVertex3f(re.X, re.Y, re.Z);
// upper right arm
glVertex3f(re.X, re.Y, re.Z);
glVertex3f(rs.X, rs.Y, rs.Z);
glEnd();
}
}
结束!构建并运行,确保你的 Kinect 已经插入。你应该会看到一个包含 Kinect 所拍摄的旋转的彩色点云的(视频流)窗口,当 Kinect 捕捉到人体时,则绘制红线来展示这个人的上肢姿态。
Kinect 环境配置¶
Kinect v1.8 SDK¶
- 微软官方 SDK 下载页面:Kinect for Windows SDK v1.8 (222MB)
- 微软官方 ToolKit 下载页面:Kinect for Windows Developer Toolkit v1.8 (384MB)
在上面两个页面下载并安装 Kinect SDK 和 ToolKit 的安装包,完成后连接 Kinect 设备,检查设备管理器,能够看到 Kinect 设备即为安装成功。

注解
安装过程中 Kinect 设备不要连接电脑。
另外,查看系统环境变量,会发现 Kinect SDK 所在路径也被添加到了系统变量,在配置 Visual Studio 项目时可以直接引用。

开发包附带了一些软件,其中 Developer Toolkit Browser v1.8 中提供了一些资料和例程,可以运行玩一玩。


Kinect v2.0 SDK¶
微软官方 SDK 下载页面:Kinect for Windows SDK 2.0 (276MB)
在上面的页面下载并安装 Kinect SDK 的安装包,完成后连接 Kinect 设备,检查设备管理器,能够看到 Kinect 设备即为安装成功。
注解
安装过程中 Kinect 设备不要连接电脑。
查看系统环境变量,会发现 Kinect SDK 所在路径也被添加到了系统变量,在配置 Visual Studio 项目时可以直接引用。
开发包附带了一些软件,其中 SDK Browser v2.0 中提供了一个自检程序 Kinect Configuration Verifier,可以检查 Kinect 是否连接成功。除此之外还提供了一些资料和例程,可以运行玩一玩。
Kinect 开发文档¶
Kinect v1 开发文档¶
目前微软官网已彻底删除了 Kinect for Windows SDK v1 的文档资料。替代办法是通过网页快照存档网站 Wayback Machine 来查阅官方文档的历史快照。
几个时间节点:
- 微软官网最后发布的一代 Kinect SDK 版本号为 1.8.0.595,发布日期为 2013 年 9 月 13 日,在此之后文档更新应基本停止。
- 2014 年 10 月微软发布第二代 Kinect for Windows。
- 微软官网大约在 17 年至 18 年前后彻底撤掉了 Kinect SDK v1 文档的存档,网页快照不再记录。
另外,网页快照的抓取时间是有一定随机性的,有时一些页面可以查看,但另一些页面可能会报错,对于报错的情况,可以换一个时间节点查看。
Kinect 型号比较¶
参考博客¶
参考论文¶
- Zennaro S, Munaro M, Milani S, et al. Performance evaluation of the 1st and 2nd generation Kinect for multimedia applications[C]//2015 IEEE International Conference on Multimedia and Expo (ICME). IEEE, 2015: 1-6.
- Yang L, Zhang L, Dong H, et al. Evaluating and improving the depth accuracy of Kinect for Windows v2[J]. IEEE Sensors Journal, 2015, 15(8): 4275-4285.
注解
译者注:附录部分是译者整理的相关资料,作为补充和参考。