A certain engineer "COMPLEX"

.NETで画像処理を試してみる OpenCVSharp編 第2回

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

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回にカウントします)

普通に考えると800*600+800*0.5*600*0.5+(800*0.5*600*0.5)*256*2になります。

グレイスケール化

800*600 です。
全ての画素に対して処理を行います。

縮小

800*0.5*600*0.5 です。
全ての画素に対して処理を実施せず、縮小率0.5なので、2画素に1回画素へのアクセスが発生します。

ヒストグラムの均一化

800*600 です。
まず、画像全体のヒストグラムの計算で、800*0.5*600*0.5 を消費します。
次に、取得したヒストグラムを使って、累積値とその比率を求めます。累積に256、その累積値を使ってヒストグラムの修正に256です。
最後に、画像全体の画素を補正します。
ですので、(800*0.5*600*0.5)*2+256*2800*600+512 になります。

合計 800*600*2.5+512 です。

順番に処理を行うだけで、これだけ時間 (ループ処理が発生します) がかかります。
ですが、これを1回でまとめるとどうなるでしょう。

まず、今回は縮小がNearestNeighborなので、欠落する画素は無視できます。
すなわち、グレースケール化の前に縮小を行っても、結果は変わりません。
次に、グレースケール化ですが、縮小の時点で、欠落しない画素はわかりきっているので、この時点でグレースケール化を適用できます。
つまり、縮小とグレースケール化は同一のループで処理できます。800*0.5*600*0.5で済みます。
次はヒストグラムの均一化ですが、ヒストグラムの計算自体は、前のループで同時に計算できます。つまり、画素をグレースケール化できた時点でヒストグラムの計算が可能です。
最後のヒストグラムに基づいた補正自体は、ヒストグラム自体が計算できていないと実行できないため、800*0.5*600*0.5+512必要です。

よって、合計で 800*0.5*600*0.5++800*0.5*600*0.5+512800*600+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 に移植。

のパターンで速度を計測しました。

実験結果


計測結果は

種別/ループ数100100010000
Sequential228.6666667 ms2270 ms
22841.66667 ms
OpenCV86.66666667 ms853.6666667 ms8483.333333 ms
Optimized128.6666667 ms961.6666667 ms9478 ms
Optimized (Parallel)88 ms979.3333333 ms9556 ms
Optimized (C++/CLI)69.33333333 ms582 ms5680.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

コメントを残す

メールアドレスが公開されることはありません。

%d人のブロガーが「いいね」をつけました。