こしあん
2019-01-07

keras_preprocessingを使ってお手軽に画像を回転させる方法

Pocket
LINEで送る


Data Augmentationで画像を回転させたいことがあります。画像の回転は一般に「アフィン変換」と呼ばれる操作で、OpenCVやPillowのライブラリを使えば簡単にできるのですが、Numpy配列に対して1から書くとかなりめんどいのです。Kerasが裏で使っているkeras_preprocessingというモジュールを使うと簡単にできることがわかったので紹介します。

また、速度の比較でOpenCVによる回転も見ていきます。

Kerasが裏で使っているkeras_preprocessing

Kerasのソースコード読んでいて気づいたのですが、ImageDataGenerator等で画像を回転させる具体的な操作がKerasのライブラリの中で一切記載されていないのです。どこでやっているんだろうと思ったらこのインポート、

from keras_preprocessing import image

ここでやっています。URLはこちら。回転操作はアフィン変換(affine_transformations.py)にあります。つまり、KerasのData AugmentationはKeras本体ではなく、keras_preprocessingという別のモジュールで実装しているのです。

逆に言えば、裏で使っているkeras_preprocessingというモジュールをインポートすれば、Kerasが使っている個々の変換操作を必要なところだけ使うことができます。これはなかなか便利です。

keras_preprocessingによる画像の回転

例としてこの画像を「cat.jpg」として保存しておきます。可愛いですね。

この猫画像を-90度~90度まで10度刻みで回転するコードは次のとおりです。

from keras_preprocessing.image import apply_affine_transform
from PIL import Image
import matplotlib.pyplot as plt
import numpy as np


def rotation(degree):
    image = np.asarray(Image.open("cat.jpg"), dtype=np.uint8)
    image = apply_affine_transform(image, channel_axis=2, theta=degree, fill_mode="nearest", cval=0.)

    plt.clf()
    plt.imshow(image)
    plt.title("degree = "+str(degree))
    cnt = (degree + 90) // 10
    plt.savefig(f"images/deg_{cnt:02d}.jpg")

for i in np.arange(-90, 100, 10):
    rotation(i)

これだけでOKです。plt.clf()以降は、プロットしたり保存したりする操作なので、回転操作は「apply_affine_transform」のたった1行のみです。結果は次のようになります。

いい感じですね。回転して余ってしまったピクセルに対して補完処理が入っているので、よくありがちな背景が黒くなってしまうということがありません。

ちなみにこれをOpenCVやPillowのライブラリで実装すると若干面倒です(特に背景の補完あたりが)。背景の補完なくてもアフィン変換は数行いります(参考)。なにせライブラリに読み込ませるのが面倒ですよね。

apply_affine_transformのパラメーターについてです。

image = apply_affine_transform(image, channel_axis=2, theta=degree, fill_mode="nearest", cval=0.)
  • image:画像の配列を表します
  • channel_axis:カラーチャンネルの軸。通常は2番目でしょう。
  • theta:回転の度数(notラジアン単位)
  • fill_mode:回転で余ったセルをなにで埋めるか。constant, nearest, reflect, wrapの4種類があり、デフォルトはnearest
  • cval:fill_modeがconstantのときの余ったセルの埋める値

デメリットは遅さ

ただ、keras_preprocessingはお手軽で補完処理も入って綺麗なのですが、若干遅いです。LineProfilerを使ってPillowによる読み込み→回転を50回ループさせてみます。

def rotation(degree):
    for i in range(50):
        image = np.asarray(Image.open("cat.jpg").resize((512, 512), Image.BILINEAR), dtype=np.uint8)
        image = apply_affine_transform(image, channel_axis=2, theta=degree, fill_mode="nearest", cval=0.)
Total time: 5.7427 s
Function: rotation at line 7

Line #      Hits         Time  Per Hit   % Time  Line Contents
==============================================================
     7                                           def rotation(degree):
     8        51        454.0      8.9      0.0      for i in range(50):
     9        50    4190574.0  83811.5     24.1          image = np.asarray(I
e.open("cat.jpg"), dtype=np.uint8)
    10        50   13189224.0 263784.5     75.9          image = apply_affine
ansform(image, channel_axis=2, theta=degree, fill_mode="nearest", cval=0.)

apply_affine_transformだけで画像の読み込みの3倍程度のボトルネックが発生します。これは大きい画像だとちょっと厳しい。

速度を取るならOpenCVによる回転

速さを取るのなら先程のリンクで最速だったOpenCVによるアフィン変換を取るべきでしょう。自分は勘違いしていたのですが、OpenCVって内部的にNumpy配列で管理しているので、専用の型で持っていない、つまりPillowで読み込ませてNumpy配列にして、それを直にOpenCVにわたすということができるのです。

ただしOpenCVの場合はセルが切れたり、余ったセルの補完処理とかがないです。そこは速度を取るか品質を取るか考える必要はあります。ちなみに45度回転すると次のようになります。

このときのコードは次の通りです。

import cv2

def rotation_cv2(degree):
    img = np.asarray(Image.open("cat.jpg"), dtype=np.uint8)
    center = (img.shape[1]//2, img.shape[0]//2)
    affine = cv2.getRotationMatrix2D(center, degree,  1.0)
    img_afn = cv2.warpAffine(img, affine, (img.shape[1], img.shape[0]),flags=cv2.INTER_LINEAR)

    plt.imshow(img_afn)
    plt.show()

ちなみにPillowやpyplotの座標系は(y, x, c)ですが、OpenCVの座標系はなぜか(x, y, c)と縦横が逆で定義されているので、shapeや中点の指定では縦横を気をつけてください。

速度を同様に計測してみます。50回ループさせました。

Total time: 1.46289 s
Function: rotation_cv2 at line 13

Line #      Hits         Time  Per Hit   % Time  Line Contents
==============================================================
    13                                           def rotation_cv2(degree):
    14        51        674.0     13.2      0.0      for i in range(50):
    15        50    4089824.0  81796.5     92.4          img = np.asarray(Image
open("cat.jpg"), dtype=np.uint8)
    16        50        829.0     16.6      0.0          center = (img.shape[1]
/2, img.shape[0]//2)
    17        50       3929.0     78.6      0.1          affine = cv2.getRotati
nMatrix2D(center, degree,  1.0)
    18        50     332164.0   6643.3      7.5          img_afn = cv2.warpAffi
e(img, affine, (img.shape[1], img.shape[0]),flags=cv2.INTER_LINEAR)

OpenCVによるアフィン変換、むちゃくちゃ速いです。keras_preprocessingの場合は読み込み:アフィン変換=1:3ぐらいだったのに、OpenCVの場合は読み込み:アフィン変換=1:0.1ぐらいまで短くなっています。速度を気にする場合はこちらが良いでしょう。

OpenCVによるNumpy配列の回転(変態的手法)

「OpenCVって内部的にNumpy配列扱ってるのならただのNumpy配列もアフィン変換で回転できんじゃね?」という疑問がわきます。試してみたらできました

def rotate_numpy():
    X = np.arange(18, dtype=np.uint8).reshape(3,3,2)
    print(X[:,:,0])
    print(X[:,:,1])

    center = (1,1)
    affine = cv2.getRotationMatrix2D(center, 90,  1.0)
    X_afn = cv2.warpAffine(X, affine, (X.shape[1], X.shape[0]),flags=cv2.INTER_LINEAR)
    print("affine transform")
    print(X_afn.shape)
    print(X_afn[:,:,0])
    print(X_afn[:,:,1])
[[ 0  2  4]
 [ 6  8 10]
 [12 14 16]]
[[ 1  3  5]
 [ 7  9 11]
 [13 15 17]]
affine transform
(3, 3, 2)
[[ 4 10 16]
 [ 2  8 14]
 [ 0  6 12]]
[[ 5 11 17]
 [ 3  9 15]
 [ 1  7 13]]

見事にNumpy配列が回転してます。ただし、画像のように3次元でないと無理(?)、しかもdtype=np.uint8と指定してあげないとダメみたいです。ただ、チャンネル数はいい加減に2とか入れても大丈夫でした。

なかなかこれは変態的な手法ではないのでしょうか。

まとめ

keras_preprocessingから、OpenCVによる回転に脱線してしまいましたが、以下のように使い分けるとよいでしょう。

  • 速度よりも、余ったセルの補完処理したり綺麗に出力したい→keras_preprocessing
  • 品質よりも、とにかく速度がほしい→OpenCV

とのことでした。OpenCVによる一般的なNumpy配列の回転ができたのは驚きでした。

Related Posts

クラス別のData Augmentationって意味ある?を調べてみた... Data Augmentationで精度を上げることにお熱になっていると、「特定の足引っ張っているクラスだけAugmetationかけたらいいんじゃない?」的なことをやりたくなります。これを調べてみました。その結果、全体を一括で処理するケースと、クラス別に局所的な処理をするケースで、精度に大きなギ...
Chainerで画像の前処理やDataAugmentationをしたいときはDatasetMixin... Chainerにはデフォルトでランダムクロップや標準化といった、画像の前処理やDataAugmentation用の関数が用意されていません。別途のChainer CVというライブラリを使う方法もありますが、chainer.dataset.DatasetMixinを継承させて独自のデータ・セットを定...
画像のダウンサンプリングとPSNRの測定... U-Netでどこまでの深さが効いているのかを考えるために、画像をダウンサンプリングし、アップサンプリングするという処理を行いPSNRを見ていきます。その結果、PSNRが15~20程度だと、U-Netの深い層が効かないかもしれないという状況を確認することができました。 きかっけ・考え方 U-Ne...
PyTorch/TorchVisionで複数の入力をモデルに渡したいケース... PyTorch/TorchVisionで入力が複数あり、それぞれの入力に対して同じ前処理(transforms)をかけるケースを考えます。デフォルトのtransformsは複数対応していないのでうまくいきません。しかし、ラッパークラスを作り、それで前処理をラップするといい感じにできたのでその方法を...
PyTorchでOnehotエンコーディングするためのワンライナー... PyTorchでクラスの数字を0,1のベクトルに変形するOnehotベクトルを簡単に書く方法を紹介します。ワンライナーでできます。 TL;DR PyTorchではこれでOnehotエンコーディングができます。 onehot = torch.eye(10) ただし、labelはLongTe...
Pocket
Delicious にシェア

Add a Comment

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です