OpenGLでカメラ画像を表示
OpenCVは好きなんだけど標準の描画GUIであるHighGUIはWindowsマシンでは60fps以上出ない(過去記事参照)ので,私は最近では,画像処理だけOpenCVでやらせて描画ウィンドウはGLFWを使ってOpenGLで描画させる,という方法を採用しています.この方が圧倒的に速いだけでなく,簡単に3DモデルをオーバーラップできたりとARとの親和性が良く,非常に便利です.HighGUIの貧弱なトラックバーなんかもOpenGL用GUIライブラリで改良できますし,シェーダ言語GLSLを上手く使えばCUDAやOpenCLなんて使わなくても並列化画像処理まで実装できます(これはちょっと難しいけど).
ただ問題は,OpenGLで画像を扱うのがOpenCVに比べてちょっと難しいという事です.OpenGLはかなりプリミティブなところを触るAPIなので,ウィンドウに画像を描画するだけでも一苦労です.そこで今回は,GLFWのウィンドウに簡単にOpenCVの画像を描画できるクラスを作りました.
OpenCV画像のOpenGL描画用クラス
ソースコード
/******************************************************** OpenGL Image with OpenCV GLFWでOpenCVのcv::Matを背景描画するためのクラス How to Use: 1. メインループに入る前にGLImageを生成 2. 描画したいGLFWwindowを与えてGLImageを初期化 3. メインループ内で次の様に書く(ex. mainWindowの背景にframeImgを描画) // Change Current Window glfwMakeContextCurrent(mainWindow); // Clear Buffer Bits glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); // Draw Image glImg.draw(frameImg); // <- Only rendering // Clear Depth Bits (so you can overwride CG on frameImg) glClear(GL_DEPTH_BUFFER_BIT); // Draw your CG // End Draw glfwSwapBuffers(mainWindow); Change 20160119: ・コンストラクタで初期化できるようにした ・コメント大幅追加 ・GLSLをインライン化して外部ファイルを不要にした *********************************************************/ #pragma once #pragma comment(lib, "opengl32.lib") #pragma comment(lib, "glew32.lib") #pragma comment(lib, "glfw3.lib") #include <stdio.h> #include <stdlib.h> #include <GL\glew.h> #include <GL/glfw3.h> #include "Shader.h" #include <opencv2\opencv.hpp> class GLImage { private: GLFWwindow *imgWindow; GLuint vao; // 頂点配列オブジェクト GLuint vbo; // 頂点バッファオブジェクト GLuint image; // テクスチャオブジェクト GLuint imageLoc;// オブジェクトの場所 Shader s; // シェーダ // バーテックスシェーダ const char *vertexSource = "#version 330 core \n" "layout(location = 0) in vec4 pv;\n" "void main(void)\n" "{\n" " gl_Position = pv;\n" "}\n"; // フラグメントシェーダ const char *fragmentSource = "#version 330 core \n" "uniform sampler2DRect image;\n" "layout(location = 0) out vec4 fc;\n" "void main(void)\n" "{\n" " fc = texture(image, gl_FragCoord.xy);\n" "}\n"; int vertices; public: GLImage() { } GLImage(GLFWwindow *window) { init(window); } void init(GLFWwindow *window) { int w, h; glfwMakeContextCurrent(window); glfwGetWindowSize(window, &w, &h); imgWindow = window; // 頂点配列オブジェクト glGenVertexArrays(1, &vao); glBindVertexArray(vao); // 頂点バッファオブジェクト glGenBuffers(1, &vbo); glBindBuffer(GL_ARRAY_BUFFER, vbo); // [-1, 1] の正方形 static const GLfloat position[][2] = { { -1.0f, -1.0f }, { 1.0f, -1.0f }, { 1.0f, 1.0f }, { -1.0f, 1.0f } }; vertices = sizeof(position) / sizeof (position[0]); glBufferData(GL_ARRAY_BUFFER, sizeof(position), position, GL_STATIC_DRAW); glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 0, 0); glEnableVertexAttribArray(0); // テクスチャ glGenTextures(1, &image); glBindTexture(GL_TEXTURE_RECTANGLE, image); glTexImage2D(GL_TEXTURE_RECTANGLE, 0, GL_RGB, w, h, 0, GL_BGR, GL_UNSIGNED_BYTE, NULL); glTexParameteri(GL_TEXTURE_RECTANGLE, GL_TEXTURE_MAG_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_RECTANGLE, GL_TEXTURE_MIN_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_RECTANGLE, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_BORDER); glTexParameteri(GL_TEXTURE_RECTANGLE, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_BORDER); // シェーダのロード s.initInlineGLSL(vertexSource, fragmentSource); imageLoc = glGetUniformLocation(s.program, "image"); } void draw(cv::Mat frame) { glfwMakeContextCurrent(imgWindow); glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); // 切り出した画像をテクスチャに転送する cv::flip(frame, frame, 0); glBindTexture(GL_TEXTURE_RECTANGLE, image); glTexSubImage2D(GL_TEXTURE_RECTANGLE, 0, 0, 0, frame.cols, frame.rows, GL_BGR, GL_UNSIGNED_BYTE, frame.data); // シェーダプログラムの使用開始 s.enable(); // uniform サンプラの指定 glUniform1i(imageLoc, 0); // テクスチャユニットとテクスチャの指定 glActiveTexture(GL_TEXTURE0); glBindTexture(GL_TEXTURE_RECTANGLE, image); // 描画に使う頂点配列オブジェクトの指定 glBindVertexArray(vao); // 図形の描画 glDrawArrays(GL_TRIANGLE_FAN, 0, vertices); // 頂点配列オブジェクトの指定解除 glBindVertexArray(0); // シェーダプログラムの使用終了 s.disable(); } };
中身の解説
中でやっていることは,インラインのシェーダをコンパイルさせ,その後ビューポートと同じ大きさのポリゴンを用意し,OpenCV画像データをテクスチャとしてポリゴンと一緒にシェーダに転送する,という流れです.注意すべきところは,OpenCVのY軸とOpenGLのY軸は向きが逆だったり,BGR順じゃなくてRGB順だったりと,OpenCVとOpenGLで画像データの扱いが多少異なるということです.このクラスではそこを気にしなくても良いようになっていて,OpenCVのBGR順のカラー画像をHighGUIと同様にそのまま出力できるようになってます.
なお,この画像描画クラスではOpenGL1.1の固定シェーダ機能を使っておらず,プログラマブルシェーダを利用しています.通常ではWindows環境はOpenGL1.1の機能しか使えないように制約しているので,GLFWとOpenGL, OpenCVの他に,その制約を取っ払うためのライブラリであるGLEWも使用しています.WindowsでもモダンなOpenGLをプログラミングできるようにするためのライブラリにはglewとgl3wという2つのライブラリがあるようですが,どちらを使っても構わないと思います.
また,ここで用いているShader.h
というのは私が作ったシェーダコンパイル用クラスです.中身はこちら.
/******************************************************** Shader Class GLSLシェーダを管理するライブラリ 参考: Modern OpenGL Tutorial http://www.opengl-tutorial.org/ 床井研究室 http://marina.sys.wakayama-u.ac.jp/~tokoi/?date=20090827 Change 20160119: シェーダのソースコード文字列を直接読む仕様を追加 *********************************************************/ #pragma once #include "OpenGLHeader.h" class Shader { protected: GLuint vertexShader, fragmentShader; // シェーダオブジェクト const char *vertexFileName, *fragmentFileName; // シェーダファイル名 void readShaderCompile(GLuint shader, const char *file); // .shaderのコンパイル void readInlineShaderCompile(GLuint shader, const char *source); // シェーダのインラインソースをコンパイル void link(GLuint prog); // コンパイルしたshaderをリンクする public: GLuint program; // シェーダプログラム Shader(); // コンストラクタ ~Shader(); // デストラクタ Shader &operator=(Shader &_s) // コピーコンストラクタ { initGLSL(_s.vertexFileName, _s.fragmentFileName); return *this; } // 初期化 // フラグメントシェーダーの有無で分ける void initGLSL(const char *vertexFile); void initGLSL(const char *vertexFile, const char *fragmentFile); void initInlineGLSL(const char *vertexSource, const char *fragmentSource); // 有効化 void enable(){ glUseProgram(program); } void disable(){ glUseProgram(0); } };
/******************************************************** Shader Class GLSLシェーダを管理するライブラリ 参考: Modern OpenGL Tutorial http://www.opengl-tutorial.org/ 床井研究室 http://marina.sys.wakayama-u.ac.jp/~tokoi/?date=20090827 Change 20160119: シェーダのソースコード文字列を直接読む仕様を追加 *********************************************************/ #include "Shader.h" Shader::Shader() { } Shader::~Shader() { } void Shader::readShaderCompile(GLuint shader, const char *file) { FILE *fp; char *buf; GLsizei size, len; GLint compiled; // ファイルを開く fopen_s(&fp, file, "rb"); if (!fp) printf("ファイルを開くことができません %s\n", file); //ファイルの末尾に移動し現在位置を得る fseek(fp, 0, SEEK_END); size = ftell(fp);//ファイルサイズを取得 // ファイルサイズのメモリを確保 buf = (GLchar *)malloc(size); if (buf == NULL){ printf("メモリが確保できませんでした \n"); } // ファイルを先頭から読み込む fseek(fp, 0, SEEK_SET); fread(buf, 1, size, fp); //シェーダオブジェクトにプログラムをセット glShaderSource(shader, 1, (const GLchar **)&buf, &size); //シェーダ読み込み領域の解放 free(buf); fclose(fp); // シェーダのコンパイル glCompileShader(shader); glGetShaderiv(shader, GL_COMPILE_STATUS, &compiled); if (compiled == GL_FALSE){ printf("コンパイルできませんでした!!: %s \n ", file); glGetShaderiv(shader, GL_INFO_LOG_LENGTH, &size); if (size > 0){ buf = (char *)malloc(size); glGetShaderInfoLog(shader, size, &len, buf); printf(buf); free(buf); } } } void Shader::readInlineShaderCompile(GLuint shader, const char *source) { char *infolog; GLsizei size, infolen; GLint compiled; //printf(source); size = strlen(source); // シェーダオブジェクトにプログラムをセット glShaderSource(shader, 1, (const GLchar**)&source, &size); // シェーダのコンパイル glCompileShader(shader); glGetShaderiv(shader, GL_COMPILE_STATUS, &compiled); if (compiled == GL_FALSE){ printf("コンパイルできませんでした!!: InlineShader \n "); glGetShaderiv(shader, GL_INFO_LOG_LENGTH, &size); if (size > 0){ infolog = (char *)malloc(size); glGetShaderInfoLog(shader, size, &infolen, infolog); printf(infolog); free(infolog); } } } void Shader::link(GLuint prog) { GLsizei size, len; GLint linked; char *infoLog; glLinkProgram(prog); glGetProgramiv(prog, GL_LINK_STATUS, &linked); if (linked == GL_FALSE){ printf("リンクできませんでした!! \n"); glGetProgramiv(prog, GL_INFO_LOG_LENGTH, &size); if (size > 0){ infoLog = (char *)malloc(size); glGetProgramInfoLog(prog, size, &len, infoLog); printf(infoLog); free(infoLog); } } } void Shader::initGLSL(const char *vertexFile) { // GPU,OpenGL情報 printf("VENDOR= %s \n", glGetString(GL_VENDOR)); printf("GPU= %s \n", glGetString(GL_RENDERER)); printf("OpenGL= %s \n", glGetString(GL_VERSION)); printf("GLSL= %s \n", glGetString(GL_SHADING_LANGUAGE_VERSION)); //シェーダーオブジェクトの作成 vertexShader = glCreateShader(GL_VERTEX_SHADER); //シェーダーの読み込みとコンパイル readShaderCompile(vertexShader, vertexFile); // シェーダプログラムの作成 program = glCreateProgram(); // シェーダオブジェクトをシェーダプログラムに関連付ける glAttachShader(program, vertexShader); // シェーダオブジェクトの削除 glDeleteShader(vertexShader); // シェーダプログラムのリンク link(program); } void Shader::initGLSL(const char *vertexFile, const char *fragmentFile) { //glewの初期化 GLenum err = glewInit(); if (err != GLEW_OK){ printf("Error: %s\n", glewGetErrorString(err)); } memcpy(&vertexFileName, &vertexFile, sizeof(vertexFile) / sizeof(char)); memcpy(&fragmentFileName, &fragmentFile, sizeof(fragmentFile) / sizeof(char)); // GPU,OpenGL情報 printf("VENDOR= %s \n", glGetString(GL_VENDOR)); printf("GPU= %s \n", glGetString(GL_RENDERER)); printf("OpenGL= %s \n", glGetString(GL_VERSION)); printf("GLSL= %s \n", glGetString(GL_SHADING_LANGUAGE_VERSION)); //シェーダーオブジェクトの作成 vertexShader = glCreateShader(GL_VERTEX_SHADER); fragmentShader = glCreateShader(GL_FRAGMENT_SHADER); //シェーダーの読み込みとコンパイル readShaderCompile(vertexShader, vertexFile); readShaderCompile(fragmentShader, fragmentFile); // プログラムオブジェクトの作成 program = glCreateProgram(); // シェーダオブジェクトをシェーダプログラムに関連付ける glAttachShader(program, vertexShader); glAttachShader(program, fragmentShader); // シェーダオブジェクトの削除 glDeleteShader(vertexShader); glDeleteShader(fragmentShader); // シェーダプログラムのリンク link(program); } void Shader::initInlineGLSL(const char *vertexSource, const char *fragmentSource) { //glewの初期化 GLenum err = glewInit(); if (err != GLEW_OK){ printf("Error: %s\n", glewGetErrorString(err)); } // 擬似的にシェーダファイル名を作成 char vertexFile[] = "InlineVertex", fragmentFile[] = "InlineFragment"; memcpy(&vertexFileName, &vertexFile, sizeof(vertexFile) / sizeof(char)); memcpy(&fragmentFileName, &fragmentFile, sizeof(fragmentFile) / sizeof(char)); // GPU,OpenGL情報 printf("VENDOR= %s \n", glGetString(GL_VENDOR)); printf("GPU= %s \n", glGetString(GL_RENDERER)); printf("OpenGL= %s \n", glGetString(GL_VERSION)); printf("GLSL= %s \n", glGetString(GL_SHADING_LANGUAGE_VERSION)); //シェーダーオブジェクトの作成 vertexShader = glCreateShader(GL_VERTEX_SHADER); fragmentShader = glCreateShader(GL_FRAGMENT_SHADER); //シェーダーの読み込みとコンパイル readInlineShaderCompile(vertexShader, vertexSource); readInlineShaderCompile(fragmentShader, fragmentSource); // プログラムオブジェクトの作成 program = glCreateProgram(); // シェーダオブジェクトをシェーダプログラムに関連付ける glAttachShader(program, vertexShader); glAttachShader(program, fragmentShader); // シェーダオブジェクトの削除 glDeleteShader(vertexShader); glDeleteShader(fragmentShader); // シェーダプログラムのリンク link(program); }
使用方法
コメント文に書いてある通りですが,使用する疑似コードは次の通りです.疑似コードなのでこのままコピペしても動きません(笑).
#include "GLImage.h" GLImage glImg; int main(void) { setup(); //mainWindowの用意 // Initialize GLImage and set context glImg.init(mainWindow); cv::VideoCapture cap(0); while(1){ // get frame Image cv::Mat frameImg; cap >> frameImg; // Change Current Window glfwMakeContextCurrent(mainWindow); // Clear Buffer Bits glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); // Draw Image glImg.draw(frameImg); // <- Only rendering // Clear Depth Bits (so you can overwride CG on frameImg) glClear(GL_DEPTH_BUFFER_BIT); // Draw your CG draw(); // End Draw glfwSwapBuffers(mainWindow); } }
大事なところは,ARでCGを描画する場合はglImg.draw()
の直後にglClear(GL_DEPTH_BUFFER_BIT)
を書き加えてデプスバッファを消去してやる,という事です.GLImageはビューポートと同じ大きさのポリゴンにテクスチャを貼りつけるという作業をするため,glImg.draw()
を実行するとデプスバッファが平面に上書きされてしまいます.GLImageで描いた画像を背景画像にするには,デプスバッファを一旦消去してからCGを描画して,最後にglfwSwapBuffers()
を実行します.
出力結果
こんな感じです(めちゃめちゃ暗い…).画像ではGUIも書き込まれていますが,カメラ画像をこのように映すことができます.なお,GUIにはimgui(ocornut/imguiの方)を使用しています.