OpenCV-Python Tutorials (2) ~Core Operations~ 後半

前回始めたOpenCV-Python Tutorialsの記事,Core Operationsの続きです.

公式:Core Operations — OpenCV-Python Tutorials 1 documentation

パフォーマンスの評価と改善のテクニック

目標

画像処理では,1秒ごとに大量の演算処理を扱うため,自分が書くコードは正しい解決法を提供するだけでなく最速の方法である必要がある.この章では,次のようなことを学ぶ.

  • コードのパフォーマンスの評価
  • コードのパフォーマンスを改善するいくつかの秘訣
  • cv2.getTickCountcv2.getTickFrequencyなどの関数が登場する

OpenCVから離れて,Pythonもまた実行時間の計測に役立つtimeというモジュールを提供している.他のprofileというモジュールも,コードのそれぞれの関数にどれぐらい時間がかかったかといったようなコードの詳細を知るのに役立つ.しかしIPythonを使っている場合は,それらの機能全てはユーザーフレンドリーなやり方によって統合されている.これから,いくつかの重要なものを見ていく.そして,詳細は追加資料の章のリンクで確認しよう.

OpenCVでのパフォーマンスの計測

cv2.getTickCount関数はイベント参照(機械のスイッチがONになったような時)の後からこの関数が呼ばれた時までのクロックサイクル数を返す.従って,もし関数の実行の前と後に呼び出せば,関数の実行に使われたクロック数がわかる.

cv2.getTickFrequency関数はクロックサイクルの頻度,または1秒あたりのクロックサイクル数を返す.従って,実行の秒数を知るには,次のようにできる.

e1 = cv2.getTickCount()
# 計測したいコード
e2 = cv2.getTickCount()
time = (e2 - e1)/ cv2.getTickFrequency()

以下の例でデモンストレーションを示そう.以下の例では,5から49の範囲でのカーネルメディアンフィルタ(ノイズ除去)を用いた.(結果がどのようになるかは気にしなくていい.そこはゴールではない.)

img1 = cv2.imread('messi5.jpg')

e1 = cv2.getTickCount()
for i in xrange(5,49,2):
    img1 = cv2.medianBlur(img1,i)
e2 = cv2.getTickCount()
t = (e2 - e1)/cv2.getTickFrequency()
print t

# Result I got is 0.521107655 seconds
メモ

timeモジュールでも同じことができる.cv2.getTickCountの代わりにtime.time()関数を使う.そうすると2つ違いが取れる.

OpenCVでのデフォルトの最適化

OpenCVの関数の多くはSSE2やAVXなどを使って最適化されている.これは最適化されていないコードも含んでいる.だからもし我々のシステムがこれらの機能をサポートすれば,それを利用するべきだ(ほとんどすべての最近のプロセッサーはこれらをサポートしている).これはコンパイルの間にデフォルトでできる.従って,OpenCVは可能であれば最適化されたコードを実行し,そうでなければ最適化されていないコードを実行する.cv2.useOptimized()を使ってそれが可能/不可能かを確認でき,cv2.setUseOptimized()を使って可能/不可能にするかを設定できる.簡単な例を見てみよう.

In [1]: import cv2

In [2]: img = cv2.imread('messi.jpg')

# 最適化がされているかを確認
In [3]: cv2.useOptimized()
Out[3]: True

In [4]: %timeit res = cv2.medianBlur(img,49)
10 loops, best of 3: 22.3 ms per loop

# 最適化を不可能に
In [5]: cv2.setUseOptimized(False)

In [6]: cv2.useOptimized()
Out[6]: False

In [7]: %timeit res = cv2.medianBlur(img,49)
10 loops, best of 3: 59.1 ms per loop

最適されたメディアンフィルタリングは最適化されていないものより2倍以上早い.もしこのソースを確認したら,メディアンフィルタリングはSIMD最適化なのを見ることができる.なので,最適化を可能にするためにコードの最初でこれを使える.(デフォルトでは可能になっていることをお忘れなく)

IPythonでのパフォーマンスの計測

二つの似たような処理のパフォーマンスを比べる必要が時々あるだろう.IPythonはそれをするために%timeitというマジッックコマンドを提供してくれる.これはより正確な結果を得るために数回コードを実行してくれる.もう一度,一行のコードを計測するのに適している.

例えば,次のコードのどの処理がより良いか知っているだろうか,x = 5; y = x**2x = 5; y = x*xx = np.unit8([5]); y = x*xy = np.square(x)?IPythonのシェルで%timeitを使って確かめてみよう.

In [8]: x = 5

In [9]: %timeit y = x**2
The slowest run took 4.16 times longer than the fastest. This could mean that an intermediate result is being cached 
1000000 loops, best of 3: 325 ns per loop

In [10]: %timeit y = x*x
10000000 loops, best of 3: 51.2 ns per loop

In [12]: import numpy as np

In [13]: z = np.uint8([5])

In [14]: %timeit y = z*z
The slowest run took 2896.08 times longer than the fastest. This could mean that an intermediate result is being cached 
1000000 loops, best of 3: 415 ns per loop

In [15]: %timeit y = np.square(z)
The slowest run took 56.59 times longer than the fastest. This could mean that an intermediate result is being cached 
1000000 loops, best of 3: 423 ns per loop

x = 5; y = x*xが最も早く,Numpyと比べて約8倍速いことがわかる.(※ここは原文では20倍となっていたが,自分でやった実行結果に合わせた) 配列の作成を考えた場合も,100倍以上早くなるだろう.クールじゃね?

メモ

Pythonのscalar処理はNumpyのscalar処理よりも速い.今のところ,1,2の要素を含んだ処理ではNumpyのarray処理よりPythonのscalar処理の方がよい.arrayのサイズが少し大きくなった場合にNumpyは有効だ.

もう1つの例を試してみよう.今回は,同じ画像に対してのcv2.countNonZero()np.count_nonzero()のパフォーマンスを比較する.

In [35]: img2 = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)

In [36]: %timeit z = cv2.countNonZero(grayImg)
The slowest run took 13.75 times longer than the fastest. This could mean that an intermediate result is being cached 
100000 loops, best of 3: 9.11 µs per loop

In [37]: %timeit z = np.count_nonzero(grayImg)
1000 loops, best of 3: 348 µs per loop

OpenCVの関数がNumpyより40倍近く速いことがわかる.

メモ

通常,OpenCVの関数はNumpyの関数より速い.今のところ同じ処理では,OpenCVの方がすぐれている.しかし,例外がありうる.Numpyがコピーの代わりにビューを扱っている場合に限っては例外がありうる.

さらなる他のIPythonの magic command

パフォーマンス,プロファイリング,ラインプロファイリング,メモリ管理などを計るための他のmagic commandsがいくつかある.それらはすべてドキュメントにうまくまとめられているので,リンクだけを置いておく.意欲的な読み手はぜひ取り組んでみよう.

最適化のテクニック

PythonとNumpyのパフォーマンスを最大限に活用するテクニックやコーディング手法がいくつかある.ここでは関連のあるものだけ紹介して,ソースを改善するためのリンクを置いておく.いったん動かしてプロファイリングし,ボトルネックを探して最適化する.

  1. Pythonの中ではできるだけループを使うのを避ける.2重/3重のループは特に避ける.これらはそもそも遅い.

  2. アルゴリズム/コードを最大限に拡張したものへベクトル化する.NumpyとOpenCVはベクトル処理に最適化されている.

  3. キャッシュ・コヒーレンスを活用する

  4. arrayのコピーは必要でない限り絶対に作らない.その代わりにviewsを使うようにしよう.arrayのコピーはコストがかかる処理だ.

追加資料

  1. Python Optimization Techniques

  2. Scipy Lecture Notes - Advanced Numpy

    公式で3つ目に載ってたリンクが死んでたので代わりにIPythonのリンクを載せておく

  3. Configuration and customization — IPython 1.2.1: An Afternoon Hack documentation

おまけ

何回かエラーが出て手こずったので書いておく.

In [16]: %timeit z = cv2.countNonZero(img)
OpenCV Error: Assertion failed (cn == 1) in countNonZero, file /tmp/opencv320150927-47164-e3j47b/opencv-3.0.0/modules/core/src/stat.cpp, line 1297

ググってみたら,1チャンネルの画像でないといけないので,カラー画像だとダメらしい.

c++ - countNonZero function gives an assertion error in openCV - Stack Overflow

じゃあ,グレースケールの画像を使えばいいのか,と思ってやってみたらまたエラーが出た.

In [21]: img2 = cv2.imread('gray.png')

In [22]: %timeit z = cv2.countNonZero(img2)
OpenCV Error: Assertion failed (cn == 1) in countNonZero, file /tmp/opencv320150927-47164-e3j47b/opencv-3.0.0/modules/core/src/stat.cpp, line 1297

チャンネル数を見てみる.

In [24]: img.shape
Out[24]: (342, 548, 3)

In [25]: img2.shape
Out[25]: (2448, 3264, 3)

どっちも3チャンネルの画像っぽい.

In [27]: grayImg = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)

In [28]: grayImg.shape
Out[28]: (342, 548)

グレースケールに変換したらできた.

読み込みの時に第2引数に0を指定するとグレースケールで読み込むことができるらしい.ってのを見つけたのでやってみた.

【シリーズ】「pythonとOpenCVを用いたCVプログラミング 」第8回:... | DERiVE コンピュータビジョン ブログ

In [32]: img3 = cv2.imread('messi.jpg',0)

In [33]: img2.shape
Out[33]: (2448, 3264, 3)

ふぁ?なんで3チャンネルのままなんや… このへんよくわからないんでまた調べます…