前回は顔認識を行いました。

Introduction

顔認識を行う際、前処理を行いました。サンプルコードでは、グレイスケール化縮小ヒストグラムの均一化が前処理でした。

グレイスケール化

カラー画像を256階調のグレーな画像に変換します。
通常、RGBの各色 (赤、緑、青)の各輝度を特定の比率で混ぜ合わせ、グレーの256階調に変換します。
変換式は、ITU-R (国際電気通信連合 無線通信部門) 勧告の BT.6010.299 R + 0.587 G + 0.114 BBT.7090.2126 R + 0.7152 G + 0.0722 BSMPTER (米国映画テレビ技術者協会) が定めた規格 240M0.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+512800x600+512 です。
ループ内の計算などがあるので、単純に比較はできませんが、ループ回数だけなら、4分の一で済みます。

上記を検証するためにサンプルプログラムを用意しました。
サンプルプログラムはhttps://github.com/takuya-takeuchi/Demo/tree/master/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/OpenCV2