KinectAzureDK编程实战_OpenGL 显示 Kinect 数据
- 2019 年 10 月 8 日
- 笔记
目前,这些代码已经部分开源于 Github 上。
K4aGrabber https://github.com/forestsen/K4aGrabber
这是关于如何用 PCL 打开 Kinect Azure DK,并可视化彩色点云。对应的文章是我半个月前写的《KinectAzureDK编程实战_PCL》。
另外,还有关于如何用 OpenCV 操作和可视化 Kinect raw data 的代码,以及用 OpenGL 原生的显示 raw data 的代码。过段时间这些都会开源。
这篇文章,我就讲如何用 OpenGL 原生的显示 raw data。
只有开发基于 OpenGL 为显示系统的 AR 应用时,才会用到 OpenGL 显示摄像头视频。
五年前,我在知乎上曾经回答过一个问题(目前是最高赞)。在这个问题中,我讲了如何用 OpenGL 和 OpenCV 实现一个 AR 应用。
想用OpenCV做AR该如何入手? https://www.zhihu.com/question/26983174/answer/35328819
实际上,类似于如何用 PCL 的 Visualizer 来可视化 Kinect 的实时点云的做法。我们也需要用多线程的方式显示 Kinect 实时图像。
主程序用于 OpenGL 可视化,声明一个线程用于接收实时图像。但是这两个线程需要 互斥 的操作共享数据。
我们先看完整代码。依赖于 freeglut 的 OpenGL。
PS:注意这部分代码和 Kinect Azure DK 开源的代码不一样,Kinect 使用 imgui 作为可视化环境,依赖于 glfw。我这里用的是 GL 1 版本。后面我也将基于 GL 3 的 Shader 来写。
我们一点一点的讲。
首先看,我们声明的这些变量。
std::thread capturing_thread; std::mutex mtx; GLuint texture_id; int texture_width; int texture_height; int glutwin; unsigned char key; GLuint texture_id; int texture_width; int texture_height; int bufferIndex = 0; k4a::image initTexture; k4a::image colorImageBuffer; std::array<k4a::image, 30> colorBuffer; k4a::image outColorFrame;
关于多线程操作的线程和互斥量。这里引用的是 C++ 标准库 STL 的 <thread>, <mutex> 头文件。
std::thread capturing_thread; std::mutex mtx;
关于 OpenGL 可视化显示需要的纹理相关变量。其中 glutwin 变量表示窗口的序号。用户在键盘按下特定键 key时,就关闭 glutwin 表示的窗口。
GLuint texture_id; int texture_width; int texture_height; int glutwin; unsigned char key;
关于从 Kinect 接收的 raw data。
int bufferIndex = 0; k4a::image initTexture; k4a::image colorImageBuffer; std::array<k4a::image, 30> colorBuffer; k4a::image outColorFrame;
使用一个 buffer 来作为多线程操作的共享数据 buffer。
同一时刻只能有一个线程来操作共享数据 buffer,我把 buffer 设置为一个有 30 个 k4a::image 的静态数组。这样尽量保证线程有数据可以提取。
initTexture 表示用于初始化 OpenGL 窗口环境。
colorImageBuffer 用于表示实时获取的 Kinect 的 raw data。
colorBuffer 用于作为多线程操作的共享数据 buffer。
outColorFrame 用于 OpenGL 每次渲染窗口环境时贴的纹理。
我们再来看 main 函数
const uint32_t deviceCount = k4a::device::get_installed_count(); if (deviceCount == 0) { cout << "no azure kinect devices detected!" << endl; } config = K4A_DEVICE_CONFIG_INIT_DISABLE_ALL; config.camera_fps = K4A_FRAMES_PER_SECOND_30; config.depth_mode = K4A_DEPTH_MODE_NFOV_UNBINNED; config.color_format = K4A_IMAGE_FORMAT_COLOR_BGRA32; config.color_resolution = K4A_COLOR_RESOLUTION_720P; config.synchronized_images_only = true; cout << "Started opening K4A device..." << endl; device = k4a::device::open(K4A_DEVICE_DEFAULT); device.start_cameras(&config); cout << "Finished opening K4A device." << endl; while (1) { if (device.get_capture(&capture, std::chrono::milliseconds(0))) { initTexture = capture.get_color_image(); break; } } texture_width = initTexture.get_width_pixels(); texture_height = initTexture.get_height_pixels();
上面代码是 main 函数代码的上半部分,这部分代码我们在前几篇文章中多次讲过,这里就不赘述了。
需要说一下的是上面有一个 while 循环,这是为了获取当前 Kinect 的第一有效帧 initTexture,用这一帧来初始化 OpenGL 将要显示于整个窗口环境的纹理。
glutInit(&argc, argv); glutInitDisplayMode(GLUT_SINGLE | GLUT_RGB); glutInitWindowSize(texture_width, texture_height); glutInitWindowPosition(100, 100); int glutwin = glutCreateWindow("test"); glutReshapeFunc(reshape); glutDisplayFunc(display); glutKeyboardFunc(keyboard); glutIdleFunc(idle); capturing_thread = std::thread(frameRetriever); capturing_thread.detach(); glutMainLoop(); capturing_thread.join(); device.close();
上面代码是 main 函数代码的下半部分,关于建立 OpenGL 可视化环境。
基于 glut 的 OpenGL 环境的建立,需要几个函数。
reshape 函数:用于窗口环境变化时重置窗口。
display 函数:用于如何窗口显示环境的渲染。
keyboard 函数:用于接收用户按键事件。
idle 函数:用于窗口显示环境的刷新。
重要的是,我在这里把接收 Kinect 的 raw data 的 thread 初始化。这个线程所运行的函数就是 frameRetriever。
最后在用户退出窗口后,即 glutMainLoop() 之后,退出 thread,并关闭 Kinect。
下面讲这几个函数如何实现。
createTexture 函数。
上面代码没啥好说的。每一个 OpenGL 1 的显示纹理函数基本都是这样的流程,glGenTextures 函数生成一个 texture_id,然后 glBindTexture 绑定该 texture_id 到当前的显示环境中,glTexImage2D 函数显示纹理数据的 raw data。
frameRetriever 函数。
这个函数主要就是一个 while 循环接收 Kinect 的 raw data,传给 k4a::image 格式的 colorImageBuffer 。
使用互斥量 mtx 向共享 colorBuffer传输数据。
并不断检测用户按键,如果按下 ”q“ 键,就退出 OpenGL 显示环境。
drawBackground 函数。
这是 OpenGL 可视化 Kinect 图像到窗口环境的代码。这个函数最重要,我们一部分一部分的讲。
首先,根据 bufferIndex 从共享数据 colorBuffer 提取当前帧。需要用互斥量 mtx 来控制,在同一时刻只能由一个线程 存取 共享数据。取出数据后载入 OpenGL 环境中。
if (bufferIndex > 0) { mtx.lock(); outColorFrame = colorBuffer[(bufferIndex - 1) % 30]; mtx.unlock(); if (texture_id != 0) { glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, texture_width, texture_height, 0, GL_BGRA_EXT, GL_UNSIGNED_BYTE, outColorFrame.get_buffer()); } }
然后,确定 OpenGL 窗口的显示环境。这里我们需要确定投影矩阵、模型视图矩阵。
int w = texture_width; int h = texture_height; const GLfloat bgTextureVertices[] = { 0, 0, (float)w, 0, 0, (float)h, (float)w, (float)h }; const GLfloat bgTextureCoords[] = { 1, 0, 1, 1, 0, 0, 0, 1 }; const GLfloat proj[] = { 0, -2.f / w, 0, 0, -2.f / h, 0, 0, 0, 0, 0, 1, 0, 1, 1, 0, 1 };
bgTextureVertices 数组记录的是纹理实际坐标,分别表示四个角的实际坐标。
bgTextureCoords 数组记录的是纹理的 uv 坐标,分别表示四个角的 uv 坐标。
proj 矩阵就是投影矩阵。这个投影矩阵如何计算请参看 《Mastering OpenCV with Practical Computer Vision Projects》 P82。
也可参考我在知乎回答的。
想用OpenCV做AR该如何入手? https://www.zhihu.com/question/26983174/answer/35328819
然后就是把这些数组、矩阵都加载到 OpenGL 环境中。
glMatrixMode(GL_PROJECTION); glLoadMatrixf(proj); glMatrixMode(GL_MODELVIEW); glLoadIdentity(); // Update attribute values. glEnableClientState(GL_VERTEX_ARRAY); glEnableClientState(GL_TEXTURE_COORD_ARRAY); glVertexPointer(2, GL_FLOAT, 0, bgTextureVertices); glTexCoordPointer(2, GL_FLOAT, 0, bgTextureCoords); glColor4f(1, 1, 1, 1); glDrawArrays(GL_TRIANGLE_STRIP, 0, 4); glDisableClientState(GL_VERTEX_ARRAY); glDisableClientState(GL_TEXTURE_COORD_ARRAY); glDisable(GL_TEXTURE_2D);
还剩下几个函数,没有必要详细讲解了。在上面已经提到过。
void display() { drawBackground(); glutSwapBuffers(); } void reshape(int w, int h) { glViewport(0, 0, w, h); glMatrixMode(GL_PROJECTION); glLoadIdentity(); glMatrixMode(GL_MODELVIEW); glLoadIdentity(); createTexture(); } void keyboard(unsigned char button, int x, int y) { key = button; glutPostRedisplay(); } void idle() { glutPostRedisplay(); }
上面的所有代码都是关于如何实时显示 Kinect 的 RGB 图像。那么关于 Depth 和 IR 图像,稍微有点不一样。在 drawBackground 函数中需要先提取当前帧的 raw buffer,然后经过着色函数 ColorizeDepthImage 来把 Depth 图像完美的显示出来。
if (bufferIndex > 0) { mtx.lock(); outDepthFrame = depthBuffer[(bufferIndex - 1) % 30]; mtx.unlock(); if (texture_id != 0) { std::vector<sen::Pixel> depthTextureBuffer; ColorizeDepthImage(outDepthFrame, DepthPixelColorizer::ColorizeBlueToRed, GetDepthModeRange(config.depth_mode), &depthTextureBuffer); glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, texture_width, texture_height, 0, GL_BGRA_EXT, GL_UNSIGNED_BYTE, outDepthFrame.get_buffer()); } }
后面我将写一篇文章,讲我最近实现的如何通过 ArUco 这个二维码标记物,基于 Kinect Azure DK 实现一个 AR 程序。也将去完善五年前在知乎上写的答案。
本文的所有代码都将在 Github 和 Coding 上开源。敬请关注我的 Github 和 Coding 主页。
Github 主页 https://github.com/forestsen
Coding 主页 https://coding.net/u/forestsen