今回の記事では、OpenCVのimshow & waitKeyの代わりにOpenGLで画像を表示することで、フレームレートの改善やレンダリングのタイミングの同期を行う方法を紹介してみます。
かなり荒っぽい方法ですが、筆者が調べた限りでは、これが最も簡単に画像表示を速くする方法でした。
他にいい方法があるよ!という方はぜひコメントやDMで教えて下さい!
もくじ
スポンサーリンク
0. 検証環境
※ ほかIntel Core i7内蔵GPUのみのラップトップなどいくつかの環境で検証済み
執筆時点ではWindowsでしか検証していませんが、今回使ったOpenGL関連のライブラリはすべてクロスプラットフォームに開発されているので、調整すればLinuxでも動くと思います。
1. 今回の目的
前回の【C++】リアルタイムな処理をパイプライン化してフレームレートを改善してみるに引き続き、自作アプリのフレームレートを改善しようという取り組みです。
筆者は、大学の研究でARアプリを作るため、カメラから取得した画像をOpenCVのフォーマットにし、諸々の画像処理をかけてからディスプレイやプロジェクタに表示させる、という処理をよく書いています。
しかし、これを60FPSや120FPSで動作させようとすると、OpenCVのimshow & waitKey(1)
の実行速度が問題に突き当たります。
OpenCVのhighguiモジュールは、クラスプラットフォームに利用でき、マウス操作やスライダーなどのGUI機能をサポートするとても優秀なツールですが、その代償として若干処理が重いのです。
厳密には、highguiはQtやVTKをバックエンドにビルドできるので、ビルドオプションの設定によって挙動が変わりますし、グラボを挿すかどうかによっても動作速度が変わります。 しかし筆者が雑に何パターンか試したところ、表示サイズを変えなければそれなりのフレームレートが出るものの、resizeやフルスクリーンを挟むとどの環境でも40FPS程度の速度しか出ませんでした。
また、OpenCVでフルスクリーン表示させるとウィンドウの縁が表示されてしまい、端の方の表示が数ピクセルズレてしまう問題が知られています。 これはOpenCVの実装による問題で、手軽な解決方法はないようです。
そこで今回は、OpenCVで処理した画像をimshowではなくOpenGLで高速に表示させる方法を試してみました。
2. 実装
ソースコードはこちらのGitHubリポジトリにアップしています。
以前の記事で紹介したように、本来OpenGLはライスタライズ方式の3Dレンダリングを実行させるためのAPIです。 その一方でGLSLというシェーダーを読み込める仕様になっているので、ただ2D画像を表示させるようなシェーダーを渡してやれば、少ないオーバーヘッドで2D画像を表示させることもできます。
詳細はGitHubを見ていただくとして、ここでは大まかな実装方針について書いてみます。
2.1. 使っているライブラリ
今回のプロジェクトではOpenCVのほかに以下のライブラリを使っています。
2.2. GLSLシェーダー
myshader.vert
, myshader.frag
というファイルが、GLSL形式のバーテックスシェーダーとフラグメントシェーダーです。
普通に3Dシーンをレンダリングするときは、前者で物体の位置やカメラの位置を考慮した変換を行い、後者が光の当たり具合を考慮しながら色を決定するのですが、今回はただ画像を表示するだけなので、与えられた二次元座標にテクスチャを表示させるだけのシェーダーになっています。
2.3. glsl_program_wrapper
OpenGLでGLSLシェーダーを使うためには、ファイルを読み込みコンパイルしてOpenGLにリンクする作業が必要です。
この一連の処理をglsl_program_wrapper.cpp
にまとめました。
2.4. gl_imshow
myshaderとgl_slprogram_wrapperを使ってOpenGLで2次元平面にテクスチャを描画する処理をまとめたのがgl_imshow.cpp
です。
直角三角形を2枚組み合わせて1枚の四角形を作り、そこにテクスチャを貼り付けることで、画像を全画面表示しています。
画像に妙なリサイズや平滑化がかかるのを避けるため、テクスチャの参照モードはNEAREST(最も座標が近いピクセルの色をそのまま使うモード)に設定しています。 もし、画像とディスプレイ上のスクリーンのサイズが違うならLINEAR(色を線形補間して使うモード)にしたほうがいいかもしれません。
現状では、24bit colorのRGB画像しかサポートしていません。 その他の画像以外を投げると色がバグるので、適宜実装を調整してください。
2.5. main
main.cpp
にはmain関数と、キーボード入力時の処理を定義するkeycallbackを書いてあります。
main関数は、お決まりのOpenGL初期化処理のあと、OpenCVで画像を読み込み、OpenGLを通して画像を表示しています。 実行中はimshow呼び出しのフレームレートを計測・表示し続け、ESCキーでアプリが終了します。
OpenGLの初期化は別クラスに分けようかとも思ったのですが、ウィンドウ or フルスクリーンの切り替えや、キー入力の設定は毎回調整したいので、結局main関数にベタ書きしました。
2.6. 注意点
OpenGLでは画像をバッファーに格納する際のアライメントがデフォルトで4になっています。 そのため、画像の横1列分のバイト数が4の倍数でないと、表示される画像がおかしくなるようです(筆者の環境では画像が斜めにズレて色が変になった)。
今回のサンプルコードでは、画像の横幅が1920で4の倍数なので問題なく動きますが、そうでないときはリサイズするかRGBAに変換してデータ数を4の倍数にする必要があります。
一応、OpenGLバッファーのアライメントは1,2,4,8に変更できるようですが、パフォーマンス上の利点があって4にしていると思われるので、あまり変更しない方がいい気がします。
詳細はOpenGL Wikiを参照してください。
3. 成果
レンダリング結果がディスプレイに読み出されるのを待たずにレンダリングさせるためglfwSwapInterval(0);
としてサンプルコードを実行してみたところ、1080x1920の画像を400~700FPSで表示させることができました。
GTXを挿したPCとIntel CPUだけのPCであまり差がなかったのが意外でしたが、主に時間を食っているのは画像のアップロードで、シェーダーの実行自体にはさほど時間がかかっていないということなのかもしれません。
実際にARアプリに使うときは画像処理分の時間が加算されるのでループ周期が遅くなりますが、前回の記事で紹介したパイプライン化を施せば、高フレームレートのゲーミングディスプレイでもフレーム落ちなくimshowできると思われます。
また、glfwSwapInterval(1);
とすれば、ディスプレイにレンダリング結果が転送されるのを待ってからglfwSwapBuffers(window);
の処理が返るようになるので、ディスプレイ表示のタイミングと同期して処理を進めることができます。
最後に、画像が正確に表示されているか確認するため、1ピクセルピッチの縞模様の画像を表示させてみました。
リポジトリのx-pat.png
がその画像です。
Full HDのディスプレイに表示させた結果、縞模様が乱れる箇所は見られませんでした。 よって、ピクセルレベルで正確な画像を表示できていることも確認できました。