LoginSignup
26
18

More than 3 years have passed since last update.

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

Posted at

概要

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

すると不意にピサの斜塔を垂直にしたい衝動にかられたので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

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

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 a.jpg

左がmaskで, 右がmasked_img

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

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()を実行す

最後に描画するという流れになっている
(参照リファレンス)

a.jpg

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

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

a.jpg

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

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

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)でコーナーに囲まれている四角形の面積を取得することができる

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

そしてminAreaRect(max_idx)とすることで最大面積の四角形のみ抽出することができる
pisa_rect.jpg

傾いてるね!

Let's Vertical

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

回転矩形のトリミング

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

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

不自然で面白いですね

おったてる

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

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

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が水平方向の線とのなす角を示すので,+90している
    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

ハイ完璧!!!












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

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

画像補完

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は入力画像とそれと同じサイズのマスク画像が必要です
このマスク画像中の非ゼロの値を持つ画素が修復するべき場所を表しています
すなわち,修復したい箇所を白色にして,残りは黒色に二値化処理することで補完することができます

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

a.jpg a.jpg

左がINPAINT_TELEA,右がINPAINT_NS

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

おったてる(Take2)

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

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

ぴ,ぴ,ピサが立った!!!

乱立させてみる

1.jpg  2.jpg
3.jpg 4.jpg
10.png 5.jpg
7.jpg

さいごに

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

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

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

みんなも,Let' vertical!

おまけ

ヴァーティカルマイケル

m.jpg

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

参考資料

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/

26
18
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
26
18