前回は顔認識を行いました。
Introduction
顔認識を行う際、前処理を行いました。サンプルコードでは、グレイスケール化、縮小、ヒストグラムの均一化が前処理でした。
グレイスケール化
カラー画像を256階調のグレーな画像に変換します。
通常、RGBの各色 (赤、緑、青)の各輝度を特定の比率で混ぜ合わせ、グレーの256階調に変換します。
変換式は、ITU-R (国際電気通信連合 無線通信部門) 勧告の BT.601 の 0.299 R + 0.587 G + 0.114 B、BT.709 の 0.2126 R + 0.7152 G + 0.0722 B、SMPTER (米国映画テレビ技術者協会) が定めた規格 240M の 0.212 R + 0.701 G + 0.087 B 等があります。
縮小
画像のサイズを小さくします。
単純に小さくすると言っても、縦横の比率を維持したり、そうでない方法もあります。
重要なのは、縮小の場合、消失する画素の情報をどのように、残される画素に含めるか、つまり補間するかということがあります。
単純なのは、消失する画素は完全に無視する方法です。
このあたりは、System.Drawing.Drawing2D.InterpolationMode 列挙体 を参考にしてください。
ヒストグラムの均一化
ヒストグラムの累積 (輝度値0から画素数を累積したもの) のグラフの傾きが一定になるように変換する処理です。コントラストの改善や明るさの偏りを修正することが可能です。
ヒストグラムの説明は省きますが、簡単に言うと、画像全体で0-255の輝度がそれぞれ何回登場するか、というデータと思ってくれれば結構です。
まずはこれらが何を意味するのか、どういう計算になるのかを想像してください。
Explanation
本題に入ります。
OpenCV、というよりも世の中にある画像処理ライブラリは、機能毎にメソッドや関数を提供しています。
でないと使い勝手が悪いからです。汎用性を重視しています。
全ての物事において、そうだとは言いませんが、汎用性を重視していると言うことは、性能を犠牲にしていることを意味します。
次の説明は極端ですが、とあるライブラリでこんなメソッドがあるとします。
- Bitmap GrayScaleScale(Bitmap, double, double)
- Bitmap GrayScaleScaleEqulizer(Bitmap, double, double)
- Bitmap GrayScaleEqulizer(Bitmap)
- Bitmap ScaleGrayScale(Bitmap, double, double)
- Bitmap EqulizerGrayScaleScale(Bitmap, double, double)
単純に順番を変えたり、一部を省略しただけですが、字面を見ればなんとなく言いたいことはわかります。
正直、組み合わせ毎にメソッドを用意していたら、とんでもないことになります。こんな仕様を見たら投げたくなります。
ですが、これらは、一回の呼び出しで全てを実行してくれる、という点にメリットがあります。
たかが、コード1行の違い、と思うかもしれませんが、ここで言う、一回の呼び出しで全てを実行してくれるは、コーディングの記述量を意味しません。
前にも書いたように、汎用性によって性能を犠牲にしています。つまり汎用性の犠牲は性能の改善に繋がります。
画像処理の世界では、これは非常に重要です。
実験
ちょっとサンプルを出します。
800x600の24bit画像があります。
これに、グレイスケール、縮小、ヒストグラムの均一化を順番に行うと、ループ回数はどの程度になるでしょう?縮小率は0.5、縮小方法は NearestNeighbor とする。
(注:24bit画像なので、1画素にRGBの3要素にアクセスする、というのは1回にカウントします)
普通に考えると800x600+800x0.5x600x0.5+(800x0.5x600x0.5)x256x2になります。
グレイスケール化
800x600 です。
全ての画素に対して処理を行います。
縮小
800x0.5x600x0.5 です。
全ての画素に対して処理を実施せず、縮小率0.5なので、2画素に1回画素へのアクセスが発生します。
ヒストグラムの均一化
800x600 です。
まず、画像全体のヒストグラムの計算で、800x0.5x600x0.5 を消費します。
次に、取得したヒストグラムを使って、累積値とその比率を求めます。累積に256、その累積値を使ってヒストグラムの修正に256です。
最後に、画像全体の画素を補正します。
ですので、**(800x0.5x600x0.5)x2+256x2** で 800x600+512 になります。
合計 800x600x2.5+512 です。
順番に処理を行うだけで、これだけ時間 (ループ処理が発生します) がかかります。
ですが、これを1回でまとめるとどうなるでしょう。
まず、今回は縮小がNearestNeighborなので、欠落する画素は無視できます。
すなわち、グレースケール化の前に縮小を行っても、結果は変わりません。
次に、グレースケール化ですが、縮小の時点で、欠落しない画素はわかりきっているので、この時点でグレースケール化を適用できます。
つまり、縮小とグレースケール化は同一のループで処理できます。800x0.5x600x0.5で済みます。
次はヒストグラムの均一化ですが、ヒストグラムの計算自体は、前のループで同時に計算できます。つまり、画素をグレースケール化できた時点でヒストグラムの計算が可能です。
最後のヒストグラムに基づいた補正自体は、ヒストグラム自体が計算できていないと実行できないため、800x0.5x600x0.5+512必要です。
よって、合計で 800x0.5x600x0.5+800x0.5x600x0.5+512 の 800x600+512 です。
ループ内の計算などがあるので、単純に比較はできませんが、ループ回数だけなら、4分の一で済みます。
上記を検証するためにサンプルプログラムを用意しました。
サンプルプログラムはhttps://github.com/takuya-takeuchi/Demo/tree/master/ComputerVision/OpenCV/C#/01_OpenCV2。
サンプルで使用した画像は
https://commons.wikimedia.org/wiki/File:Landscape_of_Shadegan.jpg
です。
ロジックはグレイスケール化、縮小、ヒストグラムの均一化を実行しますが、
- Sequential => 順番に関数を実行。ループは関数毎になる。
- OpenCV => 前回の顔認識の前処理と同じ。ただし縮小がNearestNeighbor。
- Optimized => 可能な限りループをまとめている方法。
- Optimized (Parallel) => Optimized で、かつ System.Threading.Tasks.Parallel
- Optimized (C++/CLI) => Optimized を C++/CLI に移植。
のパターンで速度を計測しました。
実験結果
計測結果は
種別/ループ数 | 100 | 1000 | 10000 |
---|---|---|---|
Sequential | 228.6666667 ms | 2270 ms | 22841.66667 ms |
OpenCV | 86.66666667 ms | 853.6666667 ms | 8483.333333 ms |
Optimized | 128.6666667 ms | 961.6666667 ms | 9478 ms |
Optimized (Parallel) | 88 ms | 979.3333333 ms | 9556 ms |
Optimized (C++/CLI) | 69.33333333 ms | 582 ms | 5680.333333 ms |
となりました。
Seqentialはやはり遅いですね。全体的にOptimizedと比較して、2.0-2.5倍くらいの差です。ループ回数分の差だと言えます。
OpenCVのコアがNativeなので、Optimizedでもパフォーマンスに差が出るのは予想通りですが、それなりに善戦したと思います。
C++/CLIを使えばOpenCVを超えることも十分可能です。まぁそこまでするなら、最初からC++/CLIで全部コーディングするべきだと思いますが。
並列化は今回はあまり意味をなしませんでしたね。もっと大きな画像なら有意な差が出ると思うのですが。
Conclusion
OpenCVは手軽に結果を確認できるあたりが良いです。
ですが、本気でパフォーマンスを追求するなら、OpenCVではなく、自分でロジックを実装することも必要です。
WPFやUWPでリッチなUIを実装し、高度な画像処理部はOpenCVまたは自分で実装するということになるでしょう。MFC?知らねぇなぁ。
Source Code
https://github.com/takuya-takeuchi/Demo/tree/master/ComputerVision/OpenCV/C#/01_OpenCV2