KinectAzureDK编程实战_OpenGL 显示 Kinect 数据

  • 2019 年 10 月 8 日
  • 笔记

前几篇文章,我讲了如何用 OpenCV 和 PCL 操作和可视化 Kinect Azure DK 的 RGB + Depth + IR 图像的 C++ 代码。

目前,这些代码已经部分开源于 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