【python】自動でトリミング画像をシームレスに結合させる

##やりたいこと
トリミングされた二つの画像を、つなぎ目が自然になるように**自動で**結合させます。
![画像結合_イメージ_re.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/642673/0a079578-a9e4-6e77-731c-5c0ed3fe011d.jpeg)
##アイデア
上側画像と下側画像の端部を起点として、徐々に領域を狭めながら、比較をしていきます。
最も一致度の高かった領域を共通領域として画像結合させます。
![画像結合_アルゴイメージ_re.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/642673/0acc1c3b-4cc1-aac8-3946-2f375bdee20c.jpeg)
##前提
- pillow形式で読み込み
- 垂直方向で結合
- 横方向位置はあらかじめ揃っている
- 横幅も揃っている
- python3.8

##流れ（勉強したこと）
１．画像のトリミング
２．画像を比較する
３．画像を連結する
４．計算量を減らすために

####１．画像のトリミング
今回はpillow形式のまま、`Image.crop()`を使用してトリミングを行ないました。

```
Image.crop((トリミング左端のx座標, 上端のy座標, 右端のx座標, 下端のy座標))
```
引数はタプルで指定する必要があり、**カッコ"("")"が二重になっている**ので、注意が必要です。計算量を少しでも少なくするため、上側と下側のうち、より小さいほうを比較開始領域とし、1pxずつ小さくしていっています。（例コードでは上側の画像が大きい場合、変数dif_hにその高さの差分を格納し、トリミング上端座標の指定に用いています）

####２．画像を比較する
トリミングした画像をopencv形式に変換し、`cv2.matchTemplate()`を用いて画像の比較しています。

```
cv2.matchTemplate(比較する画像1, 比較する画像2, マッチングのモード)
```
マッチングのモードは色々あるようですが、今回の用途の場合何でもOK（例外あり）だと思うので、スタンダードっぽい`cv2.TM_CCOEFF_NORMED`を用いました。
matchTemplateのアウトプットはマッチング度合いを明暗で示すグレースケール画像（らしい）ので、さらに`cv2.minMaxLoc()`を用いて最大値=この画像のマッチング評価結果を取得しています。

```
cv2.minMaxLoc(評価するグレースケール画像)[1]
```
minmaxLocの返り値は、2つ目に最大値が格納されているので、[1]で最大値を取り出しています。（画像テンプレートマッチングは[こちらのページ](http://labs.eecs.tottori-u.ac.jp/sd/Member/oyamada/OpenCV/html/py_tutorials/py_imgproc/py_template_matching/py_template_matching.html)等で詳細に説明がされています）

上記を、領域を変化させながらfor文で繰り返し、マッチング評価結果が最大となる場所を探し出します。

####３．画像を連結する
こちらのページから引用させていただきました。
https://note.nkmk.me/python-pillow-concat-images/

```python
def get_concat_v(im1, im2):
    dst = Image.new('RGB', (im1.width, im1.height + im2.height))
    dst.paste(im1, (0, 0))
    dst.paste(im2, (0, im1.height))
    return dst
```
pillow形式の画像において、`Image.new()`で領域を作成し、`Image.paste()`で連結させる画像を貼り付けています。

####４．計算量を減らすために
やってみると分かるのですが、**画像サイズが大きければ大きいほど滅茶苦茶時間がかかります**。
今回は、グレースケール化したり、上側画像と下側画像でより小さいほうを比較開始領域サイズとしたりしています。
今回採用していませんが、リサイズして画像比較したり、比較する領域を制限したり、一定の評価値を上回ったら比較を終了するなどして、計算量を減らせると思います。

##完成したコード

```python
import numpy as np
from PIL import Image
import cv2

# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 
def auto_img_concat_v(upper_img, under_img):

    max_res_num = 0  # マッチング評価結果の最大値を保存
    over_pixel = 0  # マッチング評価最大時の重なりピクセル数を保存

    img_w = upper_img.width
    upper_h = upper_img.height
    under_h = under_img.height

    compare_height = min(upper_h, under_h)  # 比較するピクセル数（高さ）

    # 上画像の高さが高い場合、差分を取得（そうでない場合は0）
    dif_h = (upper_h - under_h) if upper_h > under_h else 0

    for i in range(1, compare_height):

        search_img = upper_img.crop((0, i + dif_h, img_w, upper_h))  # 上画像のトリミング
        target_img = under_img.crop((0, 0, img_w, compare_height - i))  # 下画像のトリミング 

        search_np = np.array(search_img, dtype=np.uint8)  # 画像を配列へ変換（上画像）
        cv_search_img = cv2.cvtColor(search_np, cv2.COLOR_RGB2GRAY)  #グレースケール化（上画像）

        target_np = np.array(target_img, dtype=np.uint8)  # 　　　〃　　　（下画像）
        cv_target_img = cv2.cvtColor(target_np, cv2.COLOR_RGB2GRAY)  # 　　　〃　　　（下画像）

        res = cv2.matchTemplate(
            cv_search_img, cv_target_img, cv2.TM_CCOEFF_NORMED)  # マッチング評価（出力は類似度を表すグレースケール画像）
        res_num = cv2.minMaxLoc(res)[1]  # マッチング評価を数値で取得
        print(res_num, "\n", i)

        if max_res_num < res_num:  # マッチング評価結果の最大値を取得
            max_res_num = res_num
            over_pixel = target_img.height  # マッチング評価値最大時の重なりピクセル数を取得

    print("\n", max_res_num, "\n", over_pixel)

    if max_res_num > 0.98:  # 評価値が一定以上なら結合処理
        result_img = get_concat_v(upper_img.crop(
            (0, 0, img_w, upper_h - over_pixel)), under_img)  # 画像結合
        return result_img
    else:
        print("画像結合に失敗しました")
        
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 
# 画像を連結させる関数（垂直方向）
def get_concat_v(im1, im2):
    dst = Image.new('RGB', (im1.width, im1.height + im2.height))
    dst.paste(im1, (0, 0))
    dst.paste(im2, (0, im1.height))
    return dst

# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 
if __name__ == "__main__":
    upper = Image.open(r"C:\Users\aaa\Desktop\ダンボー_upper.jpg")
    under = Image.open(r"C:\Users\aaa\Desktop\ダンボー_under.jpg")
    concat_img = auto_img_concat_v(upper, under)

    if concat_img:
        concat_img.show()
```
####結果
![結果re.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/642673/815c82ad-aab8-c6a6-7601-185ae87b1327.jpeg)
上手く結合できました。

##今後の予定
計算量が多いことや、横方向が一致していない画像に対応していない等の課題はありますが、
とりあえずやりたいことは出来ました。
今後は、pyautogと組み合わせて、スクロールした画面を自動連結するツールを作りたいです。