PyTorch使いのためのPIL入門

最近KerasからPyTorchに全面移行しようとしているのですが、自作データセットを扱う場合、skimageやcv2経由で得られる`numpy.array`では扱いづらいようでしたのでPILを使っていくことにしました。
ただ、必要な情報のみが書かれているPILの記事が容易には見つけられなかったため、書くことにしました。


# どんな人に向けているか

- 最近KerasからPyTorchに乗りかえた(または乗り換えを検討している)
- 落ちているものではなく自作のデータセットを使いたい
- もっぱら`numpy.array`で慣れていて`PIL`の扱いに慣れていない

# tl;dr

PILは便利だけど覚えるのが面倒くさいなら↓で変換できるので使うといいと思うよ

```python
from skimage.io import imread
from PIL import Image

img = Image.fromarray(imread('./img-path.jpg'))
```

# 目標

```python:CustomDataset.py
from torch.utils.data import DataLoader
from torchvision import transforms


transform = transforms.Compose([
    transforms.RandomHorizontalFlip(),
    transforms.ToTensor(),
])

class CustomDataset(torch.utils.data.Dataset):
    
    def __init__(self, img_paths, labels, transform=None):
        self.transform = transform
        self.img_paths = img_paths  # 画像パスのリスト
        self.labels    = labels
        
        
    def __getitem__(self, index):
        img_path = self.img_paths[index]
        label    = self.labels[index]
        
        ######### ここから #########
        with open(img_path, 'rb') as f:
            image = Image.open(f)
            image = image.convert('RGB')
        ######### ここまでの話 #########
        
        if self.transform:
            image = self.transform(image)
        return image, label    

    def __len__(self):
        return len(self.images)

custom_dataset = CustomDataset(img_paths, labels, transform)
custom_loader  = torch.utils.data.DataLoader(dataset=custom_dataset, ~)
```

のPILの部分を簡潔に理解できるようにすること。
なお、回転などはTransform内で処理をすればよいため触れていません。

# PILの実装パターン
まずは以下のモジュールをインポートしておきます。
これ以下の章ではインポートされている前提で進めます。
今回使用した画像は、 **幅が125, 高さが150pixelのRGB画像** です。

```python
from skimage.io import imread  # 比較のため少しだけ使うのでインポートしておく
from PIL import Image

# jpgファイルを読み込んでいますが、PNG, PPM, BMP, TIFF, GIFなども対応している
img = Image.open('./img-path.jpg')
```

## 基本

### 画像の種類を見る
- 画像のサイズは(幅x高さ)
    - つまり `skimage.io.imread`の挙動と逆
- RGB画像でもチャンネル数は表示されない

```python
print(f'img.format: {img.format}')  # img.format: JPEG
print(f'img.size  : {img.size}')    # img.size  : (125, 150)
print(f'img.mode  : {img.mode}')    # img.mode  : RGB

# 個別のアクセスもできます
print(img.width) #=> 125
print(img.height) #=> 150
```

### グレースケールとRGBの変換

```python
img.convert('1')  # ２値化
img.convert('L')  # グレースケール化

# グレースケール化してからRGB化。グレースケールの時と同じ画像が出てくる。
# ただし、 .modeはRGBとなっている。
img.convert('L').convert('RGB').mode #=> RGB
```

### コピーと値の変更
- コピーの方法、挙動は同じ
- `PIL.Image`はインデックスでアクセスできないことに留意する

```python:skimage-copy.py
# skimageを使った場合
img = imread('./img-path.jpg')
tmp = img.copy()
tmp[0][0][0] = 0
print(f'img[0][0][0]: {img[0][0][0]}')  # 232
print(f'tmp[0][0][0]: {tmp[0][0][0]}')  # 0
```

```python:PIL-Image-copy.py
# PIL.Imageを使った場合
img = Image.open('./img-path.jpg')
tmp = img.copy()
tmp.putpixel((0, 0), (255, 255, 255))
print(tmp.getpixel((0, 0)))  # (255, 255, 255)
print(img.getpixel((0, 0)))  # (232, 220, 196)
```


### そのほか

- 色の範囲　 ... `img.getextrema()`
- 画像の保存 ... `img.save(path)`
- リサイズ    ... `img.resize((width, height))`
- 表示      ... jupyter使ってるなら`img`でエンターすると出ると思います。 `.show`メソッドはあるけど別窓で出るので不要な場合が多いと思います
- 画像の融合 ... `image.blend`

## np.arrayと違うところのTIPS
上にも書いてますが、注意点として書いておきます。

- 形状をみるには、`.shape`ではなく`.size`を使う
    - sizeはチャンネル数は表示されない
    - `.mode`を使って画像の種類(RGB, グレースケールなど)を確認する必要有
- PILは`img[0]`みたいにインデックスでアクセスできない。
    - `img.getpixel((100, 100))`のようにする


# 参考
- [Pythonの画像処理ライブラリPillow(PIL)の使い方](https://note.nkmk.me/python-pillow-basic/)
- [公式ドキュメント - Image Module](https://pillow.readthedocs.io/en/stable/reference/Image.html)