こんにちは。
今回は、第2回に引き続き、モデル構築前のデータ処理に関して解説していきます。
第2回の記事はこちらです。
>>【LINEbot】Pythonで女優識別botを作ろう Part2【画像収集編】
画像認識モデルを構築する際に、場合によってはモデル構築時に画像を読み込んでいくことが多いかもしれませんが、今回はあらかじめ別のファイルで画像を入力用のデータに加工していきます。
今回の内容
- 前処理を施す
- 画像を入力用に変換する処理
では、さっそく見ていきましょう!
画像に手を加える前に、、
さっそく画像に対して、処理を加えていきたいのですが、前回のスクレイピングで収集した画像には、余分な画像がそこそこ含まれているので、こういった画像を削除していきます。
この処理の自動化についても考えてみたのですが、今の僕の技術では良い案が思いつかなかったため、手作業で行います。
今回、削除の対象となるものは以下のような画像です。
- 2人以上の人が写っている画像
- 顔がほとんど見えない画像
- 全く別の人の画像
「橋本環奈」で検索して集めた画像での一例
2人以上の人が写っている
顔がほとんど見えない(これは微妙だけど念のため削除)
全く別の人
正直言って、この作業が一番面倒です。。。
OpenCVで顔検出させて自動削除することも試みたのですが、精度が悪かったのでダメでした。
(何か代案がある方は教えてください!)
では、余分な画像を削除し終わったので、次の章では実際に画像を入力用に変換していきます。
入力用に変換
この章では画像に複数の前処理を施し、モデル入力用に変換していきます。
ここで紹介するコードは、モデル学習コードと切り離しているものですが、モデル学習用コードの中に記述してもらっても構いません。
(場合によっては入力用のデータを保存する手間が省けます。)
確認ですが、各女優の画像を格納しているフォルダはこのような構造になっているという前提です。
1 2 3 4 5 |
ーimages \ |-Hashimoto_Kanna |-Ishihara_Satomi |- … |
入力用データを作成するコードはこちらです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 |
# ライブラリのインポート import pickle import os import numpy as np from glob import glob from PIL import Image, ImageFilter import torch from torchvision import transforms # 中央上部の正方形を切り取る(前処理) class CenterTopCrop: def __init__(self, size): self.size = size def __call__(self, img): img = np.array(img) shape = img.shape h, w = shape[0], shape[1] padding_side = int((w - self.size) / 2) img = img[:self.size, padding_side:w-padding_side] if img.shape[1] == 225: img = img[:, 1:] return Image.fromarray(np.uint8(img)) # ぼかしを加える(前処理) class Gaussian: def __init__(self, alpha): self.alpha = alpha def __call__(self, img): img = img.filter(ImageFilter.GaussianBlur(self.alpha)) return img # 画像フォルダからデータセットを作成する関数 def create_datasets(PATH, transform): """ INPUT: PATH: 女優ごとに画像をまとめたフォルダのパス transform: 画像に適用する前処理 RETURN: (img_data, label) img_data: tensor([amount, channel, height, width]) label: tensor([label]) """ # データラベル data_labels = ("Hashimoto_Kanna", "Ishihara_Satomi", "Hukada_Kyoko", "Aragaki_Yui", "Honda_Tubasa") img_list = torch.empty((0, 3, 224, 224)) # 入力する画像データを格納するテンソル label_list = [] # 画像のラベルを格納するリスト # imagesフォルダ内のすべてのフォルダを展開 for img_dir in glob(PATH + os.sep + "*"): dir_cate = img_dir.split(os.sep)[-1] # フォルダ名からカテゴリ名を取得 label = data_labels.index(dir_cate) # data_labelsからカテゴリのラベルを取得 print(dir_cate) # 各フォルダ内のすべてのファイルを展開 for file in glob(img_dir + os.sep + "*"): img = Image.open(file) # 画像を読み込む transformed = transform(img) # 前処理を施す label = torch.tensor(label, dtype=torch.int64) # 上で作成したラベルをTensor型に変換する transformed.unsqueeze_(0) # 次元の追加 img_list = torch.cat([img_list, transformed], dim=0) # データを連結していく label_list = np.append(label_list, label) # データを連結していく label_list = torch.tensor(label_list, dtype=torch.int64) # ラベルリストをテンソルに変換 datasets = (img_list, label_list) # タプルでまとめる return datasets def main(): resize = 224 std = (0.281, 0.275, 0.274) mean = (0.591, 0.520, 0.491) transform = transforms.Compose([ transforms.Resize(resize), # リサイズ CenterTopCrop(resize), # リサイズした画像の中央の正方形を切り取る Gaussian(1.1), # ぼかしを加える transforms.ToTensor(), # Tensor型に変換 transforms.Normalize(mean=mean, std=std) # 標準化 ]) # 画像ファイルからtensor型のデータセットを作成 PATH = os.path.join(os.getcwd(), "images") datasets = create_datasets(PATH, transform) # 作成したデータセットをpickle化して保存 with open("./datasets.pl", "wb") as f: pickle.dump(datasets, f) if __name__ == "__main__": main() |
なるべくコメントを入れるようにしたので、少し読んでもらえれば理解できるかと思います。
では、簡単に解説していきます。
13~26行目の CenterTopCrop クラスは、自作した前処理のクラスになります。
こちらが該当コードです。
13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
class CenterTopCrop: def __init__(self, size): self.size = size def __call__(self, img): img = np.array(img) # numpy配列に変換 shape = img.shape h, w = shape[0], shape[1] # 縦と横のサイズ padding_side = int((w - self.size) / 2) img = img[:self.size, padding_side:w-padding_side] # 該当範囲を切り取る if img.shape[1] == 225: # 横幅が奇数だった際に1ピクセルずらす img = img[:, 1:] return Image.fromarray(np.uint8(img)) |
この前処理では、コメントに書いてあるように、画像の中央上部を指定したサイズの正方形で切り取ります。
実は、この前処理はある仮設に基づいて施しています。
それは、画像内にある顔の位置は中央上部付近にあるだろう、という仮説です。
先ほども述べたように、OpenCVで顔付近だけを検出して切り取ると、ちょっとでも横を向いているだけで顔を検出できなくなったため、大体この辺に顔があるだろ、という範囲を切り取っています。
逆に言うと、極端に画像の端の方に顔があると、モデル学習時にノイズとなる可能性があるので削除した方がいいです。
40~75行目までの関数 create_datasets では、画像フォルダを展開し、入力用データにまとめる処理を行っています。
40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 |
def create_datasets(PATH, transform): """ INPUT: PATH: 女優ごとに画像をまとめたフォルダのパス transform: 画像に適用する前処理 RETURN: (img_data, label) img_data: tensor([amount, channel, height, width]) label: tensor([label]) """ # データラベル data_labels = ("Hashimoto_Kanna", "Ishihara_Satomi", "Hukada_Kyoko", "Aragaki_Yui", "Honda_Tubasa") img_list = torch.empty((0, 3, 224, 224)) # 入力する画像データを格納するテンソル label_list = [] # 画像のラベルを格納するリスト # imagesフォルダ内のすべてのフォルダを展開 for img_dir in glob(PATH + os.sep + "*"): dir_cate = img_dir.split(os.sep)[-1] # フォルダ名からカテゴリ名を取得 label = data_labels.index(dir_cate) # data_labelsからカテゴリのラベルを取得 print(dir_cate) # 各フォルダ内のすべてのファイルを展開 for file in glob(img_dir + os.sep + "*"): img = Image.open(file) # 画像を読み込む transformed = transform(img) # 前処理を施す label = torch.tensor(label, dtype=torch.int64) # 上で作成したラベルをTensor型に変換する transformed.unsqueeze_(0) # 次元の追加 img_list = torch.cat([img_list, transformed], dim=0) # データを連結していく label_list = np.append(label_list, label) # データを連結していく label_list = torch.tensor(label_list, dtype=torch.int64) # ラベルリストをテンソルに変換 datasets = (img_list, label_list) # タプルでまとめる return datasets |
引数には、上の方で確認したimagesフォルダのパスと、画像に施す前処理を渡します。
この関数の中では、各女優のフォルダを展開し、すべての画像に対して前処理をかけています。
前処理を終えたデータを全て積み重ね、ラベルとともにタプルでまとめます。
また、各女優に与えるラベルは、関数の冒頭で定義した data_labels に基づいて付与します。
返り値として設定されている datasets は最終的に以下のような構造となっています。
datasets[0] にすべての画像データが格納されており、 datasets[1] にはすべての画像のラベルが格納されている。 datasets[0][i] の画像データと datasets[1][i] のラベルはそれぞれ i で対応している。
返り値として帰ってくる値は、モデル学習時に簡単に使用できる形でまとめられているので、この返り値をそのままpickle化して保存します。
pickle化は、96,97行目のこの部分で行っています。
96 97 |
with open("./datasets.pl", "wb") as f: pickle.dump(datasets, f) |
おわりに
今回は、画像データをPyTorchへの入力用データに変換する処理を解説してみました。
少し長いコードなので難しく見えますが、細かく見ていくと単純なことのみを行っているので、何度も読み直してみてください。
次回は、最後の方で保存したデータを使って、モデルを構築していきます。
では、今回はここまでとします。
お疲れさまでした。
追記:第4回を更新しました。