Python3 ExifのOrientation属性による画像の回転と縮小(PIL使用)

画像認識システムの落とし穴となる「ExifのOrientation属性」とは?

元記事
The dumb reason your fancy Computer Vision app isn’t working: Exif Orientation

Pythonの画像ライブラリ、ExifのOrientation属性が反映されないものが多いから
AIでの画像認識時は気をつけてねとのこと。

記事中で紹介されているExifのOrientation属性が付与された画像があるリポジトリ。
exif-orientation-examples

ダウンロードしてlabelImgで表示してみます。
Ubuntu 18.04にlabelImgをインストールして、学習用画像のラベル付を行う

左がlabelImg、右側は画像ビューワーで同じ画像を表示しています。
確かにExif情報が反映されていませんね。

a12_01.png



画像の回転リサイズ



指定ディレクトリにある画像すべてのOrientationを反映。
同時に画像のリサイズを行いたい場合はこんな処理になりました。

元画像:images
変換後:convertedに出力


  1. import PIL.Image
  2. import PIL.ImageOps
  3. import numpy as np
  4. import os
  5. def exif_transpose(img):
  6.     if not img:
  7.         return img
  8.     exif_orientation_tag = 274
  9.     # Check for EXIF data (only present on some files)
  10.     if hasattr(img, "_getexif") and isinstance(img._getexif(), dict) and exif_orientation_tag in img._getexif():
  11.         exif_data = img._getexif()
  12.         orientation = exif_data[exif_orientation_tag]
  13.         # Handle EXIF Orientation
  14.         if orientation == 1:
  15.             # Normal image - nothing to do!
  16.             pass
  17.         elif orientation == 2:
  18.             # Mirrored left to right
  19.             img = img.transpose(PIL.Image.FLIP_LEFT_RIGHT)
  20.         elif orientation == 3:
  21.             # Rotated 180 degrees
  22.             img = img.rotate(180)
  23.         elif orientation == 4:
  24.             # Mirrored top to bottom
  25.             img = img.rotate(180).transpose(PIL.Image.FLIP_LEFT_RIGHT)
  26.         elif orientation == 5:
  27.             # Mirrored along top-left diagonal
  28.             img = img.rotate(-90, expand=True).transpose(PIL.Image.FLIP_LEFT_RIGHT)
  29.         elif orientation == 6:
  30.             # Rotated 90 degrees
  31.             img = img.rotate(-90, expand=True)
  32.         elif orientation == 7:
  33.             # Mirrored along top-right diagonal
  34.             img = img.rotate(90, expand=True).transpose(PIL.Image.FLIP_LEFT_RIGHT)
  35.         elif orientation == 8:
  36.             # Rotated 270 degrees
  37.             img = img.rotate(90, expand=True)
  38.     return img
  39. def load_image_file(file, mode='RGB'):
  40.     # Load the image with PIL
  41.     img = PIL.Image.open(file)
  42.     if hasattr(PIL.ImageOps, 'exif_transpose'):
  43.         # Very recent versions of PIL can do exit transpose internally
  44.         img = PIL.ImageOps.exif_transpose(img)
  45.     else:
  46.         # Otherwise, do the exif transpose ourselves
  47.         img = exif_transpose(img)
  48.     img = img.convert(mode)
  49.     return img
  50. def main():
  51.     for file in os.listdir('images'):
  52.         # exif Orientation
  53.         img = load_image_file(os.path.join('images', file))
  54.         # 1/2にリサイズ
  55.         width, height = img.size
  56.         img = img.resize((int(width / 2), int(height / 2)))
  57.         # 結果を保存
  58.         img.save(os.path.join('converted', file))
  59.     
  60. if __name__ == '__main__':
  61.     main()



画像の向き変換と縮小が一括で行えました。

a12_02.png

Ubuntu 18.04にlabelImgをインストールして、学習用画像のラベル付を行う

PyTorch 1.3.1 + YOLOV3で自分が指定したオブジェクトを検出しようと思います。
その前に、学習用の正解データを作成する必要があります。
こういったツールは「アノテーションツール(正解入力ツール)」と呼ばれているようです。


labelImg



YOLO関連のドキュメントを見ていると、labelImgを使用した例がよく出てきます。
https://github.com/tzutalin/labelImg

Ubuntu 18.04にインストールして使い方を見てみます。
インストールはpipで行いました。


$ pip3 install labelImg



インストール後は、labelImgで起動します。


$ labelImg



a11_01.png



使い方



「Open Dir」で画像が保存してあるディレクトリを開きます。
「Change Save Dir」でラベル付けした情報(textデータ)を保存するパスを指定。
忘れずに「PascalVOC」となっている箇所をクリックして
「YOLO」形式に変更しておきます。

a11_02.png

「Creaet RectBox」をクリック。
マウスで対象となる範囲を選択します。

a11_03.png

設定するラベルの名前を入力

a11_04.png

これでラベル付けできました。



知っておいたほうが良い設定とショートカット



ラベルを設定する操作を画像数分行うことになるので、快適に操作したいです。
Ctrl + Sでラベル情報の保存ですが、毎回押してられないので
メニューの[View] - [Auto Save Mode]にチェックを付けておきます。
これで自動的に保存されます。

a11_05.png

ラベルは1つの画像にまとめて複数設定するのではなく、
今回は「A」、二週目は「B」と付けてラベル数分周回したほうが
探すオブジェクトを固定できるので間違いがないと思います。

「Use default label」にチェックを付けてこの周回で設定するラベル名を入力しておきます。

a11_06.png

使うショートカットは
w:矩形ボックスを作成する
d:次の画像
a:前の画像

ラベルを付け終わったらdとwを押す。
ラベルデータは自動保存、次の画像に切り替わり(d)、矩形選択モード(w)になります。
これで連続してラベルを設定できます。

a11_07.png



設定したラベル情報



設定したラベルのリストは、「Change Save Dir」で指定したディレクトリに
「classes.txt」というファイル名で出力されます。

学習、解析結果表示時に必要となります。


PyTorch 1.3.1 + YOLOV3による物体検出

PyTorch 1.3.1で物体検出してみます。
最近はYOLOv3というモデルが優秀らしいので、こちらを使用してみます。

PyTorch-YOLOv3



こちらを使用してみました。
https://github.com/eriklindernoren/PyTorch-YOLOv3

ドキュメントの手順通りチェックアウトとweightsの取得を行います。


$ git clone https://github.com/eriklindernoren/PyTorch-YOLOv3
$ cd PyTorch-YOLOv3/
$ sudo pip3 install -r requirements.txt
$ cd weights/
$ bash download_weights.sh





detect



物体検出を試してみます。
サンプルの画像がdata/sampleに同梱されているので、そのまま実行可能です。


$ python3 detect.py



実行するとoutputというディレクトリが作成され、
その中に解析結果が保存されます。

解析対象の1例(よく見かける画像)

a10_01.jpg

ちゃんと解析できていますね。

a10_02.png


次は検出したいオブジェクトを自分で指定する方法を調べていきます。

PyTorch 1.3 転移学習 (Transfer Learning) によるロシア語の画像分類

PyTorch 1.3の転移学習チュートリアル
TRANSFER LEARNING FOR COMPUTER VISION TUTORIAL

既に学習済のモデルを利用して、別のデータの学習を行う仕組みです。
これを利用してロシア語分類の性能を挙げられるのか試してみました。

PyTorch ロシア語の画像分類サンプル
この時試したものは精度は73%


MNIST


本当は英語の手書き画像の分類済モデルを使用するのが良さそうですが、
お試しで手書きの数字MINSTデータを流用することにしました。
Ubuntu 18.04にPyTorch 1.0をインストールし、MNISTの手書き分類を実行する

こんなプログラムを実行


  1. import torch
  2. import torch.nn as nn
  3. import torch.nn.functional as F
  4. import torch.optim as optim
  5. from torchvision import datasets, transforms
  6. class Net(nn.Module):
  7.     def __init__(self):
  8.         super(Net, self).__init__()
  9.         self.conv1 = nn.Conv2d(1, 20, 5, 1)
  10.         self.conv2 = nn.Conv2d(20, 50, 5, 1)
  11.         self.fc1 = nn.Linear(4*4*50, 500)
  12.         self.fc2 = nn.Linear(500, 10)
  13.         
  14.     def forward(self, x):
  15.         x = F.relu(self.conv1(x))
  16.         x = F.max_pool2d(x, 2, 2)
  17.         x = F.relu(self.conv2(x))
  18.         x = F.max_pool2d(x, 2, 2)
  19.         x = x.view(-1, 4*4*50)
  20.         x = F.relu(self.fc1(x))
  21.         x = self.fc2(x)
  22.         return F.log_softmax(x, dim=1)
  23.     
  24. def train(model, device, train_loader, optimizer, epoch):
  25.     model.train()
  26.     for batch_idx, (data, target) in enumerate(train_loader):
  27.         data, target = data.to(device), target.to(device)
  28.         optimizer.zero_grad()
  29.         output = model(data)
  30.         loss = F.nll_loss(output, target)
  31.         loss.backward()
  32.         optimizer.step()
  33.         if batch_idx % 10 == 0:
  34.             print('Train Epoch: {} [{}/{} ({:.0f}%)]\tLoss: {:.6f}'.format(
  35.                 epoch, batch_idx * len(data), len(train_loader.dataset),
  36.                 100. * batch_idx / len(train_loader), loss.item()))
  37. def test(model, device, test_loader):
  38.     model.eval()
  39.     test_loss = 0
  40.     correct = 0
  41.     with torch.no_grad():
  42.         for data, target in test_loader:
  43.             data, target = data.to(device), target.to(device)
  44.             output = model(data)
  45.             test_loss += F.nll_loss(output, target, reduction='sum').item() # sum up batch loss
  46.             pred = output.argmax(dim=1, keepdim=True) # get the index of the max log-probability
  47.             correct += pred.eq(target.view_as(pred)).sum().item()
  48.     test_loss /= len(test_loader.dataset)
  49.     print('Test set: Average loss: {:.4f}, Accuracy: {}/{} ({:.0f}%)'.format(
  50.         test_loss, correct, len(test_loader.dataset),
  51.         100. * correct / len(test_loader.dataset)))
  52. def main():
  53.     
  54.     torch.manual_seed(1)
  55.     device = torch.device('cuda') # or cpu
  56.     # cuda利用の場合のオプション
  57.     kwargs = {'num_workers': 1, 'pin_memory': True}
  58.     train_loader = torch.utils.data.DataLoader(
  59.         datasets.MNIST('../data', train=True, download=True,
  60.                      transform=transforms.Compose([
  61.                          transforms.ToTensor(),
  62.                          transforms.Normalize((0.1307,), (0.3081,))
  63.                      ])),
  64.         batch_size=64, shuffle=True, **kwargs)
  65.     test_loader = torch.utils.data.DataLoader(
  66.         datasets.MNIST('../data', train=False, transform=transforms.Compose([
  67.                          transforms.ToTensor(),
  68.                          transforms.Normalize((0.1307,), (0.3081,))
  69.                      ])),
  70.         batch_size=1000, shuffle=True, **kwargs)
  71.     
  72.     model = Net().to(device)
  73.     optimizer = optim.SGD(model.parameters(), lr=0.01, momentum=0.5)
  74.     
  75.     for epoch in range(1, 10 + 1):
  76.         train(model, device, train_loader, optimizer, epoch)
  77.         test(model, device, test_loader)
  78.     
  79.     torch.save(model.state_dict(), 'mnist_cnn.pt')
  80.     
  81.         
  82. if __name__ == '__main__':
  83.     main()



学習結果である「mnist_cnn.pt」を作成します。



TRANSFER LEARNING



ポイントは、最後の10分類


  1. self.fc2 = nn.Linear(500, 10)



こちらを学習済モデルロード後に33クラスへ分類するよう変更する点でしょうか。


  1. # 保存しておいたモデルをロード
  2. model = Net()
  3. model.load_state_dict(torch.load('mnist_cnn.pt'))
  4. # 10クラスに分類していた箇所を33クラスに分類するよう変更
  5. model.fc2 = nn.Linear(500, 33)




見様見真似で書いたコードがこちらになります。


  1. import torch
  2. import torch.nn as nn
  3. import torch.nn.functional as F
  4. import torch.optim as optim
  5. from torch.utils.data import Dataset
  6. from sklearn.model_selection import train_test_split
  7. from torchvision import datasets, transforms
  8. import cv2
  9. import copy
  10. class MyDataset(Dataset):
  11.     def __init__(self, transform=None):
  12.         # csvデータの読み出し
  13.         image_dataframe = []
  14.         with open('data/letters2.csv', 'r') as f:
  15.             f.readline()
  16.             for line in f:
  17.                 row = line.strip().split(',')
  18.                 # 1:label, 2:file
  19.                 image_dataframe.append([row[1], row[2]])
  20.         self.image_dataframe = image_dataframe
  21.         # 入力データへの加工
  22.         self.transform = transform
  23.     # データのサイズ
  24.     def __len__(self):
  25.         return len(self.image_dataframe)
  26.     # データとラベルの取得
  27.     def __getitem__(self, idx):
  28.         #dataframeから画像へのパスとラベルを読み出す
  29.         label = self.image_dataframe[idx][0]
  30.         image_file = 'data/letters2/' + self.image_dataframe[idx][1]
  31.         image = cv2.imread(image_file, cv2.IMREAD_GRAYSCALE)
  32.         # 稀に31x32の画像サイズが含まれているので補正
  33.         image = cv2.resize(image, (30, 30))
  34.         
  35.         if self.transform:
  36.             image = self.transform(image)
  37.         return image, int(label) - 1
  38. class Net(nn.Module):
  39.     def __init__(self):
  40.         super(Net, self).__init__()
  41.         self.conv1 = nn.Conv2d(1, 20, 5, 1)
  42.         self.conv2 = nn.Conv2d(20, 50, 5, 1)
  43.         self.fc1 = nn.Linear(4*4*50, 500)
  44.         self.fc2 = nn.Linear(500, 10)
  45.     def forward(self, x):
  46.         x = F.relu(self.conv1(x))
  47.         x = F.max_pool2d(x, 2, 2)
  48.         x = F.relu(self.conv2(x))
  49.         x = F.max_pool2d(x, 2, 2)
  50.         x = x.view(-1, 4*4*50)
  51.         x = F.relu(self.fc1(x))
  52.         x = self.fc2(x)
  53.         return F.log_softmax(x, dim=1)
  54.     
  55. # 学習
  56. def train(model, device, train_loader, optimizer, epoch):
  57.     model.train()
  58.     for batch_idx, (data, target) in enumerate(train_loader):
  59.         data, target = data.to(device), target.to(device)
  60.         
  61.         optimizer.zero_grad()
  62.         output = model(data)
  63.         loss = F.nll_loss(output, target)
  64.         loss.backward()
  65.         optimizer.step()
  66.         if batch_idx % 10 == 0:
  67.             print('Train Epoch: {} [{}/{} ({:.0f}%)]\tLoss: {:.6f}'.format(
  68.                 epoch, batch_idx * len(data), len(train_loader.dataset),
  69.                 100. * batch_idx / len(train_loader), loss.item()))
  70. # テスト
  71. def test(model, device, test_loader):
  72.     model.eval()
  73.     test_loss = 0
  74.     correct = 0
  75.     with torch.no_grad():
  76.         for data, target in test_loader:
  77.             data, target = data.to(device), target.to(device)
  78.             output = model(data)
  79.             test_loss += F.nll_loss(output, target, reduction='sum').item() # sum up batch loss
  80.             pred = output.argmax(dim=1, keepdim=True) # get the index of the max log-probability
  81.             correct += pred.eq(target.view_as(pred)).sum().item()
  82.     test_loss /= len(test_loader.dataset)
  83.     score = 100. * correct / len(test_loader.dataset)
  84.     print('Test set: Average loss: {:.4f}, Accuracy: {}/{} ({:.0f}%)'.format(
  85.         test_loss, correct, len(test_loader.dataset),
  86.         score))
  87.     return score
  88.         
  89. def main():
  90.     
  91.     torch.manual_seed(1)
  92.     device = torch.device('cuda') # or cuda
  93.     # 作成したデータセットを呼び出し
  94.     img_dataset = MyDataset(transform=transforms.Compose([
  95.         transforms.ToTensor(),
  96.         transforms.Normalize((0.1307,), (0.3081,))
  97.     ]))
  98.     # 訓練とテストデータに分割
  99.     train_data, test_data = train_test_split(img_dataset, test_size=0.2)
  100.     train_loader = torch.utils.data.DataLoader(train_data, batch_size=64, shuffle=True)
  101.     test_loader = torch.utils.data.DataLoader(test_data, batch_size=64, shuffle=True)
  102.     # モデルを作成し学習開始
  103.     #model = Net().to(device)
  104.     # 保存しておいたモデルをロード
  105.     model = Net()
  106.     model.load_state_dict(torch.load('mnist_cnn.pt'))
  107.     # 10クラスに分類していた箇所を33クラスに分類するよう変更
  108.     model.fc2 = nn.Linear(500, 33)
  109.     model.to(device)
  110.     optimizer = optim.SGD(model.parameters(), lr=0.01, momentum=0.5)
  111.     epochs = 100
  112.     best_model = None
  113.     best_score = 0
  114.     for epoch in range(1, epochs + 1):
  115.         train(model, device, train_loader, optimizer, epoch)
  116.         # 一番よかったスコアを保存
  117.         score = test( model, device, test_loader)
  118.         if best_score < score:
  119.             best_model = copy.deepcopy(model.state_dict())
  120.             best_score = score
  121.     # モデルの保存
  122.     torch.save(best_model, 'russian.pt')
  123.     print('best socore: %.02f' % best_score)
  124.         
  125. if __name__ == '__main__':
  126.     main()



実行結果

...
Train Epoch: 99 [4480/4752 (93%)]    Loss: 0.011601
Test set: Average loss: 0.9795, Accuracy: 963/1188 (81%)
Train Epoch: 100 [0/4752 (0%)]    Loss: 0.024016
Train Epoch: 100 [640/4752 (13%)]    Loss: 0.014254
Train Epoch: 100 [1280/4752 (27%)]    Loss: 0.017021
Train Epoch: 100 [1920/4752 (40%)]    Loss: 0.010790
Train Epoch: 100 [2560/4752 (53%)]    Loss: 0.021102
Train Epoch: 100 [3200/4752 (67%)]    Loss: 0.015110
Train Epoch: 100 [3840/4752 (80%)]    Loss: 0.007911
Train Epoch: 100 [4480/4752 (93%)]    Loss: 0.012257
Test set: Average loss: 0.9657, Accuracy: 962/1188 (81%)
best socore: 81.57



本当に効果があったかは疑問ですが、81%とちょっとだけ精度が上がりました。


PyTorch 学習結果に単一の画像を渡して推測させる

前回、ロシア語の分類を行ってみました。
PyTorch ロシア語の画像分類サンプル

今回は出力した結果ファイルを利用して画像の判定を行ってみようと思います。


単独の画像を渡す



データセットの最初の値を渡して結果を出力するサンプルはよく見かけますが、
素の画像ファイルを渡す方法がわからず苦労しました。

ポイントは
・引数はtorch.tensor
・バッチ処理を想定しているので、単一の画像であっても配列で渡す必要がある

渡すデータをprintしてみて


tensor([[[0.9988, 0.9817, 0.9817, ..., 0.8961, 0.8961, 0.8961],



のように[[[で閉じられているtensorは渡せません。


tensor([[[[0.9988, 0.9817, 0.9817, ..., 0.8961, 0.8961, 0.8961],



のように[[[[で配列が始まっていればOKです。



解析結果を取得するサンプル



当然ながら、読み込んだ画像の前処理は使用したDatasetと揃えます。


  1. import torch
  2. import torch.nn as nn
  3. import torch.nn.functional as F
  4. from torchvision import transforms
  5. import cv2
  6. class Net(nn.Module):
  7.     # 解析モデルの作成
  8.     # 今回は適当に2つの値から4つのクラス分類を行う
  9.     def __init__(self):
  10.         super(Net, self).__init__()
  11.         self.conv1 = nn.Conv2d(1, 32, 3, 1)
  12.         self.conv2 = nn.Conv2d(32, 64, 3, 1)
  13.         self.dropout1 = nn.Dropout2d(0.25)
  14.         self.dropout2 = nn.Dropout2d(0.5)
  15.         self.fc1 = nn.Linear(12544, 128)
  16.         self.fc2 = nn.Linear(128, 33)
  17.         
  18.     def forward(self, x):
  19.         x = self.conv1(x)
  20.         x = F.relu(x)
  21.         x = self.conv2(x)
  22.         x = F.max_pool2d(x, 2)
  23.         x = self.dropout1(x)
  24.         x = torch.flatten(x, 1)
  25.         x = self.fc1(x)
  26.         x = F.relu(x)
  27.         x = self.dropout2(x)
  28.         x = self.fc2(x)
  29.         output = F.log_softmax(x, dim=1)
  30.         return output
  31. def read(image_file, transform):
  32.     image = cv2.imread(image_file, cv2.IMREAD_GRAYSCALE)
  33.     # 稀に31x32の画像サイズが含まれているので補正
  34.     image = cv2.resize(image, (32, 32))
  35.     image =transform(image)
  36.     
  37.     return image
  38.     
  39. def main():
  40.     device = torch.device('cpu')
  41.     # 保存しておいたモデルをロード
  42.     model = Net()
  43.     model.load_state_dict(torch.load('russian.pt'))
  44.     
  45.     # 推論モードに切り替え
  46.     model.eval()
  47.     
  48.     # file = '01_51.png'
  49.     # file = '07_192.png'
  50.     file = '25_73.png'
  51.     data = read('data/letters2/'+file,transform=transforms.Compose([
  52.         transforms.ToTensor(),
  53.         transforms.Normalize([0.485, ], [0.229, ])
  54.     ]))
  55.     print(data.size())
  56.     data = data.view(1, 1, 32, 32)
  57.     print(data.size())
  58.     output = model(data.to(device))
  59.     pred = output.argmax(dim=1, keepdim=True)
  60.     print(pred[0].item())
  61.         
  62. if __name__ == '__main__':
  63.     main()




実行結果

24



25_73.pngを解析した結果なので、ラベルは1引いた24。
ちゃんと正解データが出力できているようです。

プロフィール

Author:symfo
blog形式だと探しにくいので、まとめサイト作成中です。
Symfoware まとめ

PR




検索フォーム

月別アーカイブ