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