keras_preprocessingを使ってお手軽に画像を回転させる方法
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配列の回転ができたのは驚きでした。
Shikoan's ML Blogの中の人が運営しているサークル「じゅ~しぃ~すくりぷと」の本のご案内
技術書コーナー
北海道の駅巡りコーナー