コードの性能評価と改善方法

目的

画像処理では、単位時間あたり膨大な量の計算が必要となるので,正しい答えを得るだけではなく、高速に計算する必要がある.そこでこの章では

  • 自分が書いたコードの性能を計測する方法を学ぶ
  • 自分が書いたコードを改善する秘訣を学ぶ
  • 次の関数の使い方を学ぶ : cv2.getTickCount, cv2.getTickFrequency

処理時間の計測のため関数・モジュールとして、OpenCVが用意しているもの以外にPython自体には time モジュールがある。また, profile モジュールを使えば,コード内のそれぞれの関数が何回実行されたか,何回呼び出されたかの詳細なレポートを取得できる.さらに,IPythonを使えば,もっと易しい形でこれらの機能を使うことができる.ここでは、これらの内で重要なものを紹介する.詳しくは 補足資料 に載っている資料を参照すること.

OpenCVの機能を使って性能を計測

cv2.getTickCount 関数は(コンピュータがON状態になったというような)「参照イベント」後からこの関数が呼ばれるまでの経過時間をクロック数で返す.つまり,ある関数を実行する前と後でこの関数を呼び、差を取れば,その関数の処理時間をクロック数で知ることができる.

cv2.getTickFrequency 関数は、1秒あたりのクロック数(クロック周波数)を返す関数である.したがって、ある関数の処理時間を秒単位で知るには,以下のようにする:

In [2]:
import cv2
e1 = cv2.getTickCount()
# ここに処理時間を測りたい関数呼び出しを書く
e2 = cv2.getTickCount()
time = (e2 - e1)/ cv2.getTickFrequency()
print(time)
4.0043e-05

次の例ではカーネルサイズを5から49と変化させながら中央値フィルタを適用した時にかかる処理速度を表示する. (結果がどのようなものかはここでは重要ではないので、気にしないこと):

In [5]:
img1 = cv2.imread('messi5.jpg')

e1 = cv2.getTickCount()
for i in range(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
0.494372642

Note: Pythonのtime モジュールを使ってもできる。そのためにはcv2.getTickCountの代わりに time.time() 関数を使う.

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

多くのOpenCVの関数はSSE2やAVX(コンピュータの並列化の仕組み、ベクトル演算の高速化が可能)による最適化を取り入れているが,中には最適化されていない関数もある.もしもシステムで最適化機能をサポートしているのであれば、それを利用しない手はない(今のプロセッサであれば、その大半はサポートしているはず)。最適化機能はデフォルトではコンパイル時に有効化される.そして有効化されていればOpenCVは最適化されたコードを実行し,そうでなければ最適化されていないコードを実行することになる.最適化が有効になっているか無効なのかは cv2.useOptimized(onoff) 関数を使って調べられ、cv2.setUseOptimized() 関数を使えば有効・無効を切り替えられる.そこで,以下のコードを見てみよう.

In [20]:
# check if optimization is enabled
import cv2

img = cv2.imread('messi5.jpg')
cv2.useOptimized()
Out[20]:
False
In [21]:
%timeit res = cv2.medianBlur(img,49)
76.1 ms ± 5.31 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)
In [22]:
cv2.setUseOptimized(True)
In [23]:
cv2.useOptimized()
Out[23]:
True
In [24]:
%timeit res = cv2.medianBlur(img,49)
34.3 ms ± 536 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)

最適化が有効になっていると,中央値フィルタの処理速度は無効時に比べおよそ2倍ほど速くなる.関数の実装を見れば中央値フィルタがSIMD最適化されていることが分かる.この例が示すように,ライブラリの中身を変更すること無く、コードの先頭からコード最適化を有効にできるす(繰り返しになるが,デフォルトではコードの最適化が有効になっている).

IPythonの機能を使った性能の計測

注: Jupyter notebookはIPythonから派生したもの

プログラミングをしていると,二つの似たような処理をするコードの性能を比較することがある.IPythonはこの目的のため %timeit というマジック・コマンドを用意している.このコマンドでは,処理速度計測を高精度に行うため,コードを数回実行する.ただし,この機能は主に1行の命令を計測する作業に向いている.

例えば,以下に示す加算処理の内,どの処理が良い処理だと考えるだろうか: x = 5; y = x**2x = 5; y = x*xx = np.uint8([5]); y = x*xy = np.square(x) 。 IPython上で %timeitコマンドを使えば比較が容易にできます.

In [25]:
x = 5
In [26]:
%timeit y=x**2
282 ns ± 2.25 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)
In [27]:
%timeit y=x*x
84.8 ns ± 1.16 ns per loop (mean ± std. dev. of 7 runs, 10000000 loops each)
In [29]:
import numpy as np
z = np.uint8([5])
%timeit y=z*z
609 ns ± 8.68 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)
In [30]:
%timeit y=np.square(z)
673 ns ± 5 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)

この結果を見ると, x = 5 ; y = x*x の処理が最も速く,Numpyに比べて20倍ほど高速であることが分かる.配列の作成も処理速度に含めると100倍速い.すごくないだろうか? (Numpyの開発チームは,この計算に対して処理の高速化を行っている)

Note: Pythonのスカラー計算はNumpyのスカラー計算より高速なので,1個か2個の要素だけを含む処理であればPythonのスカラー計算を使うほうがNumpyの配列を使って計算するより高速に処理ができる.だから配列のサイズが大きくなった時にNumpyを使うと良いだろう.

もう一つ例を示す.この例では同じ画像に対して cv2.countNonZero(src) 関数と np.count_nonzero(src) 関数を適用した時の性能を比較する.

In [33]:
import cv2
img = cv2.imread('messi5.jpg',0) # gray scale
print(img.shape)
%timeit z = cv2.countNonZero(img)
(342, 548)
17.3 µs ± 105 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)
In [34]:
%timeit z = np.count_nonzero(img)
359 µs ± 2.35 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)

結果が示すように,OpenCVの関数の方が25倍ほど高速である.

Note: 一般的にOpenCVの関数はNumpyの関数より高速に実行されるため,同一の処理であればOpenCVを使った方が良いだろう.ただし例外がある.コピーではなくデータの値を見る時はNumpyの方が高速に処理ができる.

IPythonの更なるマジックコマンド

上で紹介した %timeit以外にも,性能計測やプロファイリング,メモリ計測などに便利なマジックコマンドがある.これらのマジックコマンドについては公式ドキュメントによくまとめられているので,興味がある人は,ドキュメントに目を通してみよう.

パフォーマンスの最適化技術

PythonとNumpyの最大限の性能を引き出すための技術が幾つかある.ここでは関連性のある技術のみ紹介する.ここでの大事な点は,アルゴリズムを実装する時は,まず初めに単純な実装を試してみるということである.処理のボトルネックを見つけたりコードを最適化したりするのは,アルゴリズムが正しく動作することを確認してからにする.

  1. Pythonを使うのであれば,可能な限りループの使用を避けよう.特に二重/三重ループやそれ以上の多重ループは、処理速度が極端に遅くなってしまうため避けるべきである.
  2. アルゴリズムやコードはできる限りベクトル化しよう.なぜなら,NumpyとOpenCVはvectorの処理に対して性能を発揮するように最適化されているからである.
  3. キャッシュ・コヒーレンス(cache coherence)、つまりデータがメモリ/キャッシュ上でどのように配置されるか考えて処理をする.
  4. 不要な配列のコピーはせずに,データ参照を試せ.配列のコピーは重い処理の一つ.

上記の点を全て意識したとしても、あなたの書いたコードの処理速度が遅い,もしくは大きなループの使用が避けられないのであれば,ループ処理を高速に行うCythonのようなライブラリの使用を検討したほうが良いだろう.

補足資料

  1. Pythonの最適化技術(英語)
  2. Scipy講義資料(英語) - Advanced Numpy

目次

  • 最初に戻る
  • 一つ上: 基本の処理
    画像に対する基本の処理を学ぶ: 画素値の編集,幾何変換,コードの最適化(code optimization),数学関数など
  • 前の学習項目 画像の算術演算 imageArithmetics.ipynb
    画像の算術演算を学ぶ.
  • 次の学習項目
    OpenCVを使った画像処理 : OpenCVが提供する様々な画像処理の関数について学ぶ
    色空間の変換 colorSpaces.ipynb
    画像を別の色空間へ変換する方法を学ぶ. また,動画中で特定の色を持つ物体の追跡方法を学ぶ