のんびりしているエンジニアの日記

ソフトウェアなどのエンジニア的な何かを書きます。

画像のDataAugmentationで最速のライブラリを探してみた

Sponsored Links

皆さんこんにちは
お元気ですか。年末に向けて燃え尽きようとしています。

OpenCVアドベントカレンダー第22日です。

qiita.com

今回、PythonのDataAugmentationで最速の方法を探してみます。
画像処理のDeepLearningでは、頻繁にDataAugmentationが使われています。

このData Augmentationは画像に結果がない程度に変換を加え、
ニューラルネットワークに入力する手法です。

以前、こんな資料を作りました。

www.slideshare.net

DataAugmentationにも様々な手法がありますが、次の処理する場合が非常に多いです。

  1. 画像をファイルから取得する。
  2. リサイズ
  3. アフィン変換
  4. 左右反転

この処理ですが、入力時に一定の確率で実行されます。
そのため、この処理が遅い場合には性能のボトルネックになります。
そのため、実装を高速化することは学習時間の短縮に非常に有用となります。

GPUが関わる処理で学習を高速化したい場合はCPU部も高速化することが必要でしょう。

速度を計測するにあたり、使用したライブラリをご紹介

OpenCV

当記事のアドベントカレンダーの掲載先となるフレームワーク
かつ、最も画像処理で有名なライブラリである言わずもがなOpenCVです。
Wikipediaの概要では、次のように記載されています。

画像処理・画像解析および機械学習等の機能を持つC/C++JavaPythonMATLAB用ライブラリ

opencv.org

Scikit-image

私個人はこれを推しています。
Pythonで完結するため、非常にインストールが楽です。
こちらも相当の人が使っており、サンプルも非常に多いです。

scikit-image: Image processing in Python — scikit-image

Pillow

このライブラリのサンプルを見たことある人はかなりいるのではないでしょうか?
Pythonの画像処理ライブラリで、かつて存在したPython Imaging Library (PIL)の fork プロジェクトです。

普段、Scikit-imageを利用している私は殆ど使ったことがありませんが、今回試しに使ってみました。

python-pillow.org

ぶっちゃけ全部画像処理ライブラリとしかいっていない

計測

では、早速計測を行います。
本手法における計測はtimeitを利用して実施します。

対象となる画像は皆さん大好きLennaさん(高225x幅225)です。
f:id:tereka:20171004005907j:plain

※ライブラリによって実装方法が異なるので、結果が多少ことなることがあるかもしれません。
 その点は確認して、利用してください。

画像読み込み

画像読み込みとすると非常に曖昧のため、今回は次の定義とします。

  1. 画像をファイルから読み込む
  2. 0-1へ画像を正規化
  3. float32に変換する
OpenCV

ソースコード

%timeit cv2.imread("./lenna.jpeg").astype(np.float32) / 255.0
2.06 ms ± 39.3 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
Scikit-Image

ソースコード

import skimage.io
%timeit skimage.io.imread("./lenna.jpeg")
2.1 ms ± 40.6 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
PIL

ソースコード

from PIL import Image
def load_img(path):
    with open(path, 'rb') as f:
        with Image.open(f) as img:
            return img.convert('RGB')

%timeit load_img("./lenna.jpeg")
2.02 ms ± 61.5 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

画像のリサイズ

続いて画像のリサイズです。
ニューラルネットワークの入力は固定長になる場合が多いので、基本的にはどこかでリサイズ処理が必要になり、
ImageNetを利用する場合は必須に近い技術となるでしょう。
そのため、この部分の速度を改善できるとDataAugmentationに非常に有用な手法となります。

f:id:tereka:20171221233331j:plain

OpenCV

ソースコード

%timeit cv2.resize(img,(128,128))
271 µs ± 4.26 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
Scikit-image

ソースコード

%timeit skimage.transform.resize(img, (128,128))
4.32 ms ± 142 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
PIL

ソースコード

%timeit p_img.resize((128,128))
23.2 µs ± 850 ns per loop (mean ± std. dev. of 7 runs, 10000 loops each)

アフィン変換を行う

アフィン変換を実施します。
回転、平行移動、スケール変化、引き伸ばしをこのアフィン変換で実現できます。
DataAugmentationにおいても非常にメジャーな技術と言えるでしょう。
時間の都合もあり、条件が若干ばらばらですが、アフィン変換の計測をしました。

全て微妙に違う画像ですが、こんな感じで変換を行っています。

f:id:tereka:20171222005403j:plain

OpenCV
import cv2
def transform_opencv(img):
    size = int(img.shape[0] / 2)
    affine = cv2.getRotationMatrix2D((size, size), 45,  0.8)
    affine[0][2] += 10
    affine[1][2] += 15
    img_afn = cv2.warpAffine(img, affine, img.shape[0:2],flags=cv2.INTER_LINEAR)

%timeit transform_opencv(img)
771 µs ± 139 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
Scikit-Image
def tranform_skimage(img):
    rotation = 45
    rotation = np.deg2rad(rotation)
    tform = AffineTransform(scale=(1.3, 1.3), rotation=rotation,
                            shear=1.0,
                            translation=(3, 3))
    warped_img = warp(img, tform)

%timeit tranform_skimage(img)
4.69 ms ± 72.9 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
PIL
from PIL import Image

def load_img(path):
    with open(path, 'rb') as f:
        with Image.open(f) as img:
            return img.convert('RGB')

def ScaleRotateTranslate(image, angle, center = None, new_center = None, scale = None,expand=False):
    if center is None:
        return image.rotate(angle)
    angle = -angle/180.0*math.pi
    nx,ny = x,y = center
    sx=sy=1.0
    if new_center:
        (nx,ny) = new_center
    if scale:
        (sx,sy) = scale
    cosine = math.cos(angle)
    sine = math.sin(angle)
    a = cosine/sx
    b = sine/sx
    c = x-nx*a-ny*b
    d = -sine/sy
    e = cosine/sy
    f = y-nx*d-ny*e
    return image.transform(image.size, Image.AFFINE, (a,b,c,d,e,f), resample=Image.BICUBIC)

def scale():
    image =load_img("./lenna.jpeg")
    transformed = ScaleRotateTranslate(image, 15, None, (0, 0), scale=0.9)

%timeit scale()
3.03 ms ± 83 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

左右反転

左右反転を行います。処理後の画像は次の通り

f:id:tereka:20171221232633j:plain

OpenCV
%timeit cv2.flip(img,0)
13 µs ± 162 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)
Numpyの方式
%timeit img[:, ::-1]
404 ns ± 12.3 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)
PIL
%timeit img.transpose(p_img.FLIP_LEFT_RIGHT)
56.5 µs ± 4.97 µs per loop (mean ± std. dev. of 7 runs, 10000 loops each)

まとめ

OpenCV強いと書こうと思ったらPILの方が速い場面があった。
OpenCVの方が使いやすいのですが・・・

ライブラリ 画像の読み込み リサイズ アフィン変換 フリップ
OpenCV 2.06ms 271 µs 771 µs 13 µs
Scikit-image 2.1 ms 4.32 ms 4.69 ms なし(numpyで実施)
PIL 2.02ms 23.2 µs 3.03 ms 56.5 µs
Numpy(一部) なし なし なし 404 ns

最後に

OpenCVが想像以上にScikit-imageより速いがPILの方が速い。そのため、もしこの処理で困っているならば
OpenCV、もしくは、PILを利用すると高速化できそうです。

もちろん、状況次第によって変化するかもしれませんが、
この結果を参考に良い実装をしてみてください。