Python: アフィン変換

# アフィン変換

opencvにアフィン変換はあるが, ちょいと遊びで自作してみた.
今回は画像を読み込んで, 回転しても範囲外に出ないように縁を付けた後変換するような処理で実装した.


```affin.py
import cv2
import numpy as np

from IPython.display import display, Image


def display_cv_image(image, format='.png'):
    decoded_bytes = cv2.imencode(format, image)[1].tobytes()
    display(Image(data=decoded_bytes))


def add_edge(img):
    H, W = img.shape
    WID = int(np.max(img.shape) * 2**0.5)
    e_img = np.zeros((WID, WID))
    e_img[int((WID-H)/2):int((WID+H)/2),
          int((WID-W)/2):int((WID+W)/2)] = img
    return e_img


def affin(img, a, tx=0, ty=0):
    e_img = add_edge(img)
    WID = np.max(e_img.shape)
    x = np.tile(np.linspace(-1, 1, WID).reshape(1, -1), (WID, 1))
    y = np.tile(np.linspace(-1, 1, WID).reshape(-1, 1), (1, WID))
    p = np.array([[x, y, np.ones(x.shape)]])
    t = np.array([[np.cos(a), -np.sin(a), -tx],
                  [np.sin(a),  np.cos(a), -ty],
                  [        0,          0, 1]])
    dx, dy, _ = np.sum(p * t.reshape(*t.shape, 1, 1), axis=1)
    u = np.clip((dx + 1) * WID / 2, 0, WID-1).astype('i')
    v = np.clip((dy + 1) * WID / 2, 0, WID-1).astype('i')
    return e_img[v, u]
```

今回はJupyter上でOpenCVを用いて画像表示を行うために以下を参考にdisplay_cv_imageを用意した.
http://uphy.hatenablog.com/entry/2016/12/11/110703

```python
def display_cv_image(image, format='.png'):
    decoded_bytes = cv2.imencode(format, image)[1].tobytes()
    display(Image(data=decoded_bytes))
```

回転させても範囲外に出ないように縁をつける処理はこんな感じ.
まぁアフィン変換の本質の部分じゃないので(　´_ゝ｀)ﾌｰﾝってみてもらえれば・・・
（ただ，縁をつけると正方形になるようになっていることはそこそこ大事かも？）

```python
def add_edge(img):
    H, W = img.shape
    WID = int(np.max(img.shape) * 2**0.5)
    e_img = np.zeros((WID, WID))
    e_img[int((WID-H)/2):int((WID+H)/2),
          int((WID-W)/2):int((WID+W)/2)] = img
    return e_img
```

これが実装したアフィン変換

```python
def affin(img, a, tx=0, ty=0):
    e_img = add_edge(img)
    WID = np.max(e_img.shape)
    x = np.tile(np.linspace(-1, 1, WID).reshape(1, -1), (WID, 1))
    y = np.tile(np.linspace(-1, 1, WID).reshape(-1, 1), (1, WID))
    p = np.array([[x, y, np.ones(x.shape)]])
    t = np.array([[np.cos(a), -np.sin(a), -tx],
                  [np.sin(a),  np.cos(a), -ty],
                  [        0,          0, 1]])
    dx, dy, _ = np.sum(p * t.reshape(*t.shape, 1, 1), axis=1)
    u = np.clip((dx + 1) * WID / 2, 0, WID-1).astype('i')
    v = np.clip((dy + 1) * WID / 2, 0, WID-1).astype('i')
    return e_img[v, u]
```

まず縁をつけて幅x高=WIDxWIDの画像にする.

```python
    e_img = add_edge(img)
    WID = np.max(e_img.shape)
```

次に上端から下端まで-1から1に変化するxと左端から右端まで-1から1に変化するyをもつ点のpをWIDxWIDのマップとして作る.
この時pの構造は3×WID×WID　

```python
    x = np.tile(np.linspace(-1, 1, WID).reshape(1, -1), (WID, 1))
    y = np.tile(np.linspace(-1, 1, WID).reshape(-1, 1), (1, WID))
    p = np.array([[x, y, np.ones(x.shape)]])
```

あとはこの各点をアフィン変換で目的の座標に変換する. 簡単化のため今回は引数で回転と並行移動のみあたえる.
（引数でこのtを渡す形式にすれば他の変換もできるようになる）

```python
    t = np.array([[np.cos(a), -np.sin(a), -tx],
                  [np.sin(a),  np.cos(a), -ty],
                  [        0,          0, 1]])
    dx, dy, _ = np.sum(p * t.reshape(*t.shape, 1, 1), axis=1)
```

変換後のdx, dyは画像の座標系ではないので画像の座標系に変換して元画像の対応する点を取ってくると完成！

```python
    u = np.clip((dx + 1) * WID / 2, 0, WID-1).astype('i')
    v = np.clip((dy + 1) * WID / 2, 0, WID-1).astype('i')
    return e_img[v, u]
```

# テスト

テストに用いる画像を読み込む. 今回はlenaさんを用いた.

```python
img = cv2.imread('lena.jpg', 0)
display_cv_image(img, '.png')
```

![lena0.jpg](https://qiita-image-store.s3.amazonaws.com/0/365961/fa99e49c-6e18-dda2-12ab-74266434b3fc.jpeg)



エッジをつける処理はこんな感じ

```python
display_cv_image(add_edge(img), '.png')
```

![e_lena.jpg](https://qiita-image-store.s3.amazonaws.com/0/365961/0bb89f5f-1ed8-65b0-0d74-6f6c648efdce.jpeg)


アフィン変換で45度回転

```python
display_cv_image(affin(img, np.pi/4), '.png')
```

![45_lena.jpg](https://qiita-image-store.s3.amazonaws.com/0/365961/136a8ece-0dfc-9826-e660-19e1bbbba0c7.jpeg)


平行移動も一応実装. 第3引数でx方向, 第4引数でy方向に移動させることができる.
(今回は1.0が画像半分の長さ相当)

```python
display_cv_image(affin(img, 0, 1.0), '.png')
```

![x_lena.jpg](https://qiita-image-store.s3.amazonaws.com/0/365961/3ede5e25-3908-212d-7595-c2247f871495.jpeg)


# まとめ
とりあえず回転と平行移動を簡単に行うaffin変換を実装した.
また, tを変えると拡大縮小や画像をゆがませることもできる（確認済）

# 追記
今回作ったアフィン変換はラドン変換という別の変換行うために特化した仕様なので使い勝手は良くないかも・・・