画像からのパッチ抽出について (OpenCV, PyTorch, Tensorflow)

この記事では、画像からパッチ（小さな画像の断片）を抽出する方法について、OpenCV、PyTorch、TensorFlowを用いた実装例とともに説明します。

## パッチ抽出とは？
パッチ抽出とは、大きな画像から小さな画像の断片を取り出すプロセスです。このプロセスは、画像解析や機械学習の前処理でよく利用されます。

* **カーネルサイズ**: パッチのサイズを指定します。例えば、(3,3)のカーネルサイズは、3x3ピクセルのパッチを抽出することを意味します。
* **ストライド**: パッチを抽出する際のステップサイズです。ストライドが(1,1)であれば、1ピクセルごとにパッチを抽出します。
* **パディング**: 画像の端に余白を加えることで、画像のサイズを調整します。パディングを利用すると、画像の端にある情報もパッチとして抽出することが可能になります。

## パッチ抽出の応用
パッチ抽出は、画像の局所的な特徴を捉えるために用いられます。例えば、画像内の特定のオブジェクトを検出する場合や、画像を細かい部分に分割して分析する場合などに有効です。また、コンピュータビジョンにおける畳み込みニューラルネットワーク（CNN）では、パッチ抽出の概念が核となる操作の一つとして利用されています。

## パッチ抽出のビジュアル例
以下に、カーネルサイズ(3, 3)、ストライド(1, 1)、パディングなしの設定でパッチが抽出される様子を示す図を用意しました。この図は、画像からどのようにしてパッチが切り出されるかを視覚的に理解するのに役立ちます。


![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/208980/b122a32d-bfe1-46cc-214c-4e06e2c1aa14.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/208980/dfdae07f-bdb1-d424-2104-43a67d17fa67.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/208980/236d1e45-64ec-5bd0-d6fe-d0b538a18152.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/208980/50d5a13a-ebcd-aa0f-b0f2-a047baa464d1.png)

## Lenna画像での例
Lenna画像を用いて、異なるパッチサイズとストライドでパッチを抽出する例を示します。

### 大きなパッチの抽出
* パッチサイズ: (128, 128)
* ストライド: (64, 64)
* パディング: なし

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/208980/8256b325-a8bf-4e18-9301-b28ba0b7e0fd.png)

### 小さなパッチの抽出
* パッチサイズ: (3, 3)
* ストライド: (1, 1)
* パディング: なし

```
Orignal torch.Size([1, 3, 512, 512])
Patched Image torch.Size([1, 3, 7, 7, 128, 128])
* 912 = 128 * 7 + 16
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/208980/fd8f66d8-286c-5f26-41a6-1427eff0b61e.png)

最初は差がわかりにくいかもしれませんが、拡大してみるとパッチ間の違いがはっきりとわかります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/208980/b2134834-4031-acd0-d2eb-cc43e6aed609.png)

## 実装方法

### OpenCVでの実装
OpenCVでは、純粋なforループを使用して画像からパッチを抽出します。
```python
import cv2
import numpy as np

import numpy as np
import cv2


def extract_and_tile_patches(image, kernel_size=(3, 3), stride=(1, 1), padding=(0, 0)):
    # 画像にパディングを追加
    padded_image = cv2.copyMakeBorder(image, padding[0], padding[0], padding[1], padding[1], cv2.BORDER_CONSTANT,
                                      value=0)

    # パディングを加えた画像の次元を取得
    padded_height, padded_width = padded_image.shape[:2]

    # パッチを抽出する
    patches = []
    for y in range(0, padded_height - kernel_size[1] + 1, stride[1]):
        for x in range(0, padded_width - kernel_size[0] + 1, stride[0]):
            patch = padded_image[y:y + kernel_size[1], x:x + kernel_size[0]]
            patches.append(patch)

    # パッチの数を計算
    num_patches_x = (padded_width - kernel_size[0]) // stride[0] + 1
    num_patches_y = (padded_height - kernel_size[1]) // stride[1] + 1

    # 新しい画像のサイズを計算
    tiled_image_height = num_patches_y * kernel_size[1]
    tiled_image_width = num_patches_x * kernel_size[0]

    # 新しい画像を作成
    tiled_image = np.zeros((tiled_image_height, tiled_image_width, 3), dtype=np.uint8)

    # パッチを新しい画像に配置
    patch_idx = 0
    for y in range(0, tiled_image_height, kernel_size[1]):
        for x in range(0, tiled_image_width, kernel_size[0]):
            tiled_image[y:y + kernel_size[1], x:x + kernel_size[0]] = patches[patch_idx]
            patch_idx += 1

    return tiled_image


# 元の画像を読み込む
image = cv2.imread('./image/lena.png')  # 画像のパスを適宜変更してください

# パッチを抽出してタイル状に配置
tiled_image = extract_and_tile_patches(image,kernel_size=(128,128), stride=(64,64), padding=(0,0))

# 結果を表示
cv2.imshow('Tiled Patches', tiled_image)
cv2.waitKey(0)
cv2.destroyAllWindows()
cv2.imwrite("./image/tiled_patches_lena-opencv.png", tiled_image)
```

### Pytorch

PyTorchでは、unfoldメソッドを使用して効率的にパッチを抽出できます。

```python
import torch
import torchvision
from PIL import Image
import numpy as np
import matplotlib.pyplot as plt

def extract_and_tile_patches(image_path, patch_size=3, stride=1, padding=0):
    # 画像をテンソルとして読み込む
    with Image.open(image_path) as img:
        original = np.expand_dims(np.asarray(img, np.float32).transpose([2, 0, 1]), axis=0) / 255.0
    feat = torch.as_tensor(original)

    # Unfoldレイヤーの作成
    unfolder = torch.nn.Unfold(kernel_size=patch_size, stride=stride, padding=padding)

    # Unfold操作でパッチを抽出
    feat_unfolded = unfolder(feat)

    # Reshape操作でパッチの形状を整える
    feat_reshaped = feat_unfolded.reshape(feat.size(0), feat.size(1), patch_size, patch_size, -1)

    # Permute操作でテンソルの次元を並び替える
    feat_permuted = feat_reshaped.permute(0, 4, 1, 2, 3)

    # パッチの数を取得
    num_patches = feat_permuted.size(1)

    # タイル状の画像を初期化
    grid_size = int(np.ceil(np.sqrt(num_patches)))  # グリッドサイズの計算
    tiled_image = torch.zeros((feat_permuted.size(2), grid_size * patch_size, grid_size * patch_size))

    # パッチをタイル状に配置
    for i in range(num_patches):
        row = i // grid_size
        col = i % grid_size
        tiled_image[:, row*patch_size:(row+1)*patch_size, col*patch_size:(col+1)*patch_size] = feat_permuted[0, i]

    # チャンネルを最後に移動してNumPy配列に変換
    tiled_image_np = tiled_image.permute(1, 2, 0).numpy()

    return tiled_image_np

# 関数を使用して画像からパッチを抽出し、タイル状に配置
image_path = "./image/lena.png"  # 画像のパスを指定
tiled_image_np = extract_and_tile_patches(image_path, patch_size=128, stride=64)

# 結果を表示
plt.figure(figsize=(10, 10))
plt.imshow(tiled_image_np)
plt.axis('off')
plt.show()

```

### Tensorflow

TensorFlowでは、tf.image.extract_patches関数を利用してパッチを抽出します。

```python
import tensorflow as tf
import numpy as np
import matplotlib.pyplot as plt
from PIL import Image

def extract_and_tile_patches(image_path, patch_size=128, stride=64):
    # 画像をテンソルとして読み込む
    img = Image.open(image_path)
    img_array = np.array(img, dtype=np.float32) / 255.0
    img_tensor = tf.convert_to_tensor(img_array)
    img_tensor = tf.expand_dims(img_tensor, axis=0)  # バッチ次元を追加

    # パッチを抽出
    patches = tf.image.extract_patches(
        images=img_tensor,
        sizes=[1, patch_size, patch_size, 1],
        strides=[1, stride, stride, 1],
        rates=[1, 1, 1, 1],
        padding='VALID'
    )

    # パッチの形状を変更して、各パッチを行に配置
    patch_dim = patches.shape[-1]
    patches_reshaped = tf.reshape(patches, [-1, patch_size, patch_size, 3])

    # パッチ数を取得
    num_patches = patches_reshaped.shape[0]

    # タイル状に並べた画像を作成
    num_patches_side = int(np.ceil(np.sqrt(num_patches)))
    tiled_image = tf.reshape(patches_reshaped, [1, num_patches_side, num_patches_side, patch_size, patch_size, 3])
    tiled_image = tf.transpose(tiled_image, [0, 1, 3, 2, 4, 5])
    tiled_image = tf.reshape(tiled_image, [1, num_patches_side * patch_size, num_patches_side * patch_size, 3])

    return tiled_image[0]

# 関数を使用して画像からパッチを抽出し、タイル状に配置
image_path = "./image/lena.png"  # 画像のパスを指定
tiled_image = extract_and_tile_patches(image_path)

# 結果を表示
plt.figure(figsize=(10, 10))
plt.imshow(tiled_image)
plt.axis('off')
plt.show()

```
## まとめ
この記事が画像からのパッチ抽出についての理解を深める助けとなれば幸いです。各ライブラリでの実装方法については、上記のサンプルコードを参考にしてください。