OpenCVを使ったピサのヴァーティカル塔  ～Vertical Tower of Pisa～

#概要
最近授業で画像処理を学び，行列変形で平面を変形させる技術を学びました

すると不意にピサの斜塔を垂直にしたい衝動にかられたのでPythonとOpenCVの練習がてらいざ作ってみようと思います

まぁほとんど理解できてないのでOpenCVの便利な関数達におんぶにだっこなんですけどね

それでは，Let's Vertical！


#環境
windows10
Python 3.7.7
OpenCV 3.4.2
Pillow 7.1.2
numpy 1.18.4



#実装
使用するピサの斜塔はこちら
![a.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/518001/58e44607-79ba-4585-2405-3e14ae5cfb54.jpeg)


全体的に白が強いので今回はエッジではなく色調で輪郭を検出してみる
（エッジでうまくいかなかった背景は言うまでもない）


```python:pisa
# 白色の検出
def detect_white_color(img):
    # HSV色空間に変換
    hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)

    # 白色のHSVの値域
    hsv_min = np.array([0,0,100])
    hsv_max = np.array([180,45,255])
    mask = cv2.inRange(hsv, hsv_min, hsv_max)

    # マスキング処理
    masked_img = cv2.bitwise_and(img, img, mask=mask)

    return mask, masked_img
```
![a.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/518001/817283a4-9097-592d-6d45-b3416fb1a720.jpeg) ![a.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/518001/37eef11a-8989-54b1-7429-14108e0d99ee.jpeg)



左がmaskで， 右がmasked_img

次に，二値化して矩形の検出


```python:pisa
imgray = cv2.cvtColor(blurred_img,cv2.COLOR_BGR2GRAY)
ret,thresh = cv2.threshold(imgray,115,255,0)
im, contours, hierarchy = cv2.findContours(thresh,cv2.RETR_TREE,cv2.CHAIN_APPROX_SIMPLE)

for cnt in contours:
    rect = cv2.minAreaRect(cnt)
    box = cv2.boxPoints(rect)
    box = np.int0(box)
    cv2.drawContours(img, [box], 0, (0,255,0), 2)

```
まずrectにminAreaRect(contours)を実行
minAreaRect()はBox2Dの構造をもつタプルを返す
尚，Box2Dの構造は(左上の点(x,y)，横と縦のサイズ(width, height)，回転角)

長方形を描画するためには，これらの情報を4つの座標に変換する必要があるので，それができるboxPoints()を実行す

最後に描画するという流れになっている
([参照リファレンス](http://labs.eecs.tottori-u.ac.jp/sd/Member/oyamada/OpenCV/html/py_tutorials/py_imgproc/py_contours/py_contour_features/py_contour_features.html#b))




![a.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/518001/f505a6b7-5065-7edd-a304-cdb5759c60cb.jpeg)


うまくいってそうだけど他の写真にもうまく対応できるように念のためにガウシアンフィルターでぼかしておく!


```python:pisa
#ガウシアンフィルターでぼかす
blurred_img = cv2.GaussianBlur(white_masked_img,(5,5),10)
```

![a.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/518001/f1835f29-697a-443f-842b-d8f3cec49389.jpeg)



かなりエッジが落ち着いてくれましたね

一番大きいrectだけでいいんでピックアップします

```python:pisa
#面積が最大の長方形のインデックスを返す関数
def detect_max_rect(img,contours):
    #最大面積をとるcontoursのインデックス
    max_idx = 0
    
    for i in range(len(contours)):
        if cv2.contourArea(contours[max_idx]) < cv2.contourArea(contours[i]):
            max_idx = i
    
    return max_idx

```
[contourArea(contours)](http://labs.eecs.tottori-u.ac.jp/sd/Member/oyamada/OpenCV/html/py_tutorials/py_imgproc/py_contours/py_contour_features/py_contour_features.html#contour-area)でコーナーに囲まれている四角形の面積を取得することができる

これを用いて面積が最大になる座標のインデックスを返す関数を定義する

そしてminAreaRect(max_idx)とすることで最大面積の四角形のみ抽出することができる
![pisa_rect.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/518001/d1c62beb-0b73-a076-e408-b4a61a8d032c.jpeg)

傾いてるね！

##Let's Vertical

フローチャートとか書かずにやっているのでグダグダですみません

###回転矩形のトリミング

まずは後々必要そうな四角領域をトリミングする関数を作成します

```python:pisa

#切り出し関数
def crop_rect(img, rect):
    center, size, angle = rect
    center = tuple(map(int, center))  # float -> int
    size = tuple(map(int, size))  # float -> int
    h, w = img.shape[:2]  # 画像の高さ、幅

    #rectの高さと幅を取得
    height,width=rect[1]
    height,width=int(height),int(width)
    
    #angleは水平方向の線とのなす角を示すので，+90している
    if angle<0:
        angle+=90
    
    # affine行列を用いて画像を回転させる
    M = cv2.getRotationMatrix2D(center, angle, 1)
    rotated = cv2.warpAffine(img, M, (w, h))

    # 切り抜く
    cropped = cv2.getRectSubPix(rotated, (w,h), center)

    return cropped

```

詳細はコメントに書いておきました
実行結果の画像は以下

![pisa_rect_crop.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/518001/8302a938-8b46-ee8b-00b4-07125c6860fa.jpeg)

不自然で面白いですね

###おったてる
angleが得られているのでもう簡単ですね

画像を回転させる関数を作成します


```python:pisa

#回転させる関数
def rotate_img(img,rect):
    #高さ，幅から画面の中心を計算
    _height = img.shape[0]                         
    _width = img.shape[1]                       
    _center = (int(_width/2), int(_height/2))
    
    _angle=rect[2]
    #angleが水平方向の線とのなす角を示すので，＋９０している
    if _angle<0:
        _angle+=90
    
    # affine行列を用いて画像を回転させる
    _M = cv2.getRotationMatrix2D(_center, _angle, 1)
    _rotated_img = cv2.warpAffine(img, _M, (_width, _height))
    
    return _rotated_img

```

画像の中心とってそこを軸にピサの角度分だけ回転させます

![pisa_rect_rotate.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/518001/5b7f766e-146c-63fa-13e6-b747c7fff1de.jpeg)

ハイ完璧！！！

<br/>
<br/>
<br/>
<br/>
<br/>
<br/>

．．．冗談です，すみません
さっきの関数はどっか捨てておいてください

方針としては元画像からくりぬいたところを画像補完して同じ座標に先ほどのトリムした画像を貼り付けようと思います

###画像補完

```python:pisa
#画像補完
def inpaint_img(img):
    
    _mask = cv2.imread('img/pisa1.jpg')

    #白で塗りつぶす
    cv2.drawContours(_mask, [box], 0, (255,255,255), -1)
    _maskGray = cv2.cvtColor(_mask,cv2.COLOR_BGR2GRAY)
    ret,_thresh = cv2.threshold(_maskGray,254,255,0)
    
    #画像補正
    dst = cv2.inpaint(img, _thresh, 3, cv2.INPAINT_TELEA)
    
    cv2.imwrite('img/pisa_rect_inpaint.jpg', dst)


```

[inpaint](http://labs.eecs.tottori-u.ac.jp/sd/Member/oyamada/OpenCV/html/py_tutorials/py_photo/py_inpainting/py_inpainting.html)は入力画像とそれと同じサイズのマスク画像が必要です
このマスク画像中の非ゼロの値を持つ画素が修復するべき場所を表しています
すなわち，修復したい箇所を白色にして，残りは黒色に二値化処理することで補完することができます

アルゴリズムはINPAINT_TELEAとINPAINT_NSの2種類あり，今回は両方とも実行してみました

![a.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/518001/665bb878-a556-2379-8768-29d9e4ff467b.jpeg)　![a.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/518001/f47c8f6b-92f1-55a3-5a7e-0de5b610d45a.jpeg)



左がINPAINT_TELEA，右がINPAINT_NS

NSのほうが空の部分が少しだけきれいな気がするので今回はこっちを採用

###おったてる(Take2)

画像の貼り付けはOpenCVだけだとめんどくさそうなのでPillowライブラリを使う


```python:pisa
#トリミングしたピサを補完した背景画像に貼り付ける
def paste_pisa(rect):
    #x,yにトリムしたピサのcenter座標が入る
    x,y=rect[0]
    
    #左上に持っていきたいので，半分の幅wと高さhをx,yから引く
    _w,_h=rect[1]
    #print(h,w)
    x -= _h/2
    y -= _w/2
    
    x,y=int(x),int(y)
    
    source_img = Image.open('img/pisa_rect_crop.jpg')
    canvas_img = Image.open('img/pisa_rect_inpaint.jpg')
    
    #inpaintした画像にトリムしたピサをペースト
    canvas_img.paste(source_img, (x,y))
    #画像を保存
    canvas_img.save('img/vertical_pisa.jpg')


```


いざ実行

![vertical_pisa.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/518001/07c69f12-a046-f841-ec65-d38454a7880f.jpeg)

**ぴ，ぴ，ピサが立った！！！**

#乱立させてみる
![1.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/518001/7ea93cc6-3507-7d8f-945f-c33f5ede5b12.jpeg)　　![2.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/518001/7698302d-594b-5427-e8fb-3121f8e2f932.jpeg)
![3.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/518001/f37cffb5-4e80-f332-0f2d-777d64c2836c.jpeg)　![4.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/518001/e58cbceb-aa89-0088-e9ac-247f6bd0439c.jpeg)
![10.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/518001/cb6a6a3e-7462-b47f-fd83-26ee7db81c36.png)　![5.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/518001/b6566053-9550-65eb-6a47-6c70cf414696.jpeg)
![7.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/518001/5fee5ee3-6852-16de-c84e-bc8e83513cea.jpeg)

#さいごに
補完部分が見えていて荒くなっていたり，あまりヴァーティカルってないのがありますが全体的にはうまく言った気がします

個人的には学習とか一切なし，アルゴリズムだけのinpaint関数の精度に驚きました
またpythonの動的型付けも便利すぎて絶句．．．正直型理解してないけど進めたところとかあるんでゆとり仕様に感服です


今回は色で領域を検出しているので次はエッジでできるようになれば自由度が広がる気がしますね

みんなも，**Let' vertical！**

#おまけ
##**ヴァーティカルマイケル**


![m.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/518001/0cdf28fb-1ae6-d560-c09a-97f536bc1077.jpeg)


これでノーマルグラビティ


#参考資料


minAreaRect関数
https://www.it-swarm.dev/ja/python/minarearect-opencv%E3%81%AB%E3%82%88%E3%81%A3%E3%81%A6%E8%BF%94%E3%81%95%E3%82%8C%E3%82%8B%E5%9B%9B%E8%A7%92%E5%BD%A2%E3%81%AE%E3%83%88%E3%83%AA%E3%83%9F%E3%83%B3%E3%82%B0python/824441051/

輪郭抽出
https://hk29.hatenablog.jp/entry/2020/02/01/162533

白色抽出
https://temari.co.jp/blog/2017/11/13/opencv-4/

トリミング
https://teratail.com/questions/219340

inpaint関数
https://lp-tech.net/articles/kb4bO/view?page=2

Pillowライブラリ
https://water2litter.net/rum/post/python_pil_paste/