輪郭から四角形を検出 

OpenCVで輪郭を取得することはできますが、円などハフ変換の記事は
インターネット上にたくさんありますが、四角形を検出するところまでの記事が
ネット上では少なかったので書いて見ようかと思いました。
自分のブログでもCannyでエッジを取得して判定する記事を書きましたが
二値化処理が少し複雑で、エッセンスの部分が分かりにくかったため、
さらにシンプルなものを掲載しようかと思います。

#処理の流れ
プログラムはPythonとOpenCVを使います。

* 画像を読み込む
* 画像のグレイスケール・二値化
* 輪郭を取得
* 輪郭の近似
近似条件の近似カーブ最大距離を輪郭長の0.02とする（0.02は任意)
  近似処理にはcv2.approxPolyDP()を使用します。

* 各オブジェクトの輪郭四角判定

&emsp;1. 角が４つある
&emsp;2. 面積値が条件以上ある
&emsp;3. 凸形状である
&emsp;&emsp;isContourConvex()を使用する
&emsp;4. 各辺のなす角の最大コサインが0.3 以下。
&emsp;&emsp;三角関数コサインが0.3を取っているのは、下図を見ての調整となります。
&emsp;&emsp;正方形はコサイン1.0が理想値ではあります。

**三角関数グラフ**
&emsp;&emsp;![sin_cos_graph.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/391380/b12bb8b6-7413-66e8-e191-f875a426c4cc.png)


#サンプルコード

```python:findSquares.py
import cv2
import math
import numpy as np

# pt0-> pt1およびpt0-> pt2からの
# ベクトル間の角度の余弦(コサイン)を算出
def angle(pt1, pt2, pt0) -> float:
    dx1 = float(pt1[0,0] - pt0[0,0])
    dy1 = float(pt1[0,1] - pt0[0,1])
    dx2 = float(pt2[0,0] - pt0[0,0])
    dy2 = float(pt2[0,1] - pt0[0,1])
    v = math.sqrt((dx1*dx1 + dy1*dy1)*(dx2*dx2 + dy2*dy2) )
    return (dx1*dx2 + dy1*dy2)/ v

# 画像上の四角形を検出
def findSquares(bin_image, image, cond_area = 1000):
    # 輪郭取得
    contours, _ = cv2.findContours(bin_image, cv2.RETR_LIST, cv2.CHAIN_APPROX_SIMPLE)
    for i, cnt in enumerate(contours):
        # 輪郭の周囲に比例する精度で輪郭を近似する
        arclen = cv2.arcLength(cnt, True)
        approx = cv2.approxPolyDP(cnt, arclen*0.02, True)

        #四角形の輪郭は、近似後に4つの頂点があります。
        #比較的広い領域が凸状になります。

        # 凸性の確認 
        area = abs(cv2.contourArea(approx))
        if approx.shape[0] == 4 and area > cond_area and cv2.isContourConvex(approx) :
            maxCosine = 0

            for j in range(2, 5):
                # 辺間の角度の最大コサインを算出
                cosine = abs(angle(approx[j%4], approx[j-2], approx[j-1]))
                maxCosine = max(maxCosine, cosine)

            # すべての角度の余弦定理が小さい場合
            #（すべての角度は約90度です）次に、quandrangeを書き込みます
            # 結果のシーケンスへの頂点
            if maxCosine < 0.3 :
                # 四角判定!!
                rcnt = approx.reshape(-1,2)
                cv2.polylines(image, [rcnt], True, (0,0,255), thickness=2, lineType=cv2.LINE_8)
    return image

def main():
    image = cv2.imread('shapes_image.png', cv2.IMREAD_COLOR)
    if image is None :
        exit(1)
    gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
    _, bw = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY | cv2.THRESH_OTSU)
    rimage = findSquares(bw, image)
    cv2.imshow('Square Detector', rimage)
    c = cv2.waitKey()
    return 0;

if __name__ == '__main__':
    main()
```


#入力画像
![shapes_image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/391380/83526d88-9b2f-ef2a-5a16-b5a4df7005b8.png)
上記図形から四角形を検出します。

#結果画像
![finded_squares.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/391380/62def2a9-bb82-0d09-f7b3-a06521330a9f.png)
輪郭を赤で囲ったところが、検出した四角形です。
しきい値は甘めになっているので、左下の台形も検出しています。

#動作環境
Windows10 
Anaconda 3
Python 3.9.0
OpenCV 4.4.0
numpy 1.19.2

#まとめ
このやり方はdocs.opencv.org - samples/cpp/squares.cppのサンプルコードを見て、
それを参考にしています。
幾何学計算だけなので、このトリックはそれほど難しくなく、うまいやり方だなと
感心しました。
実際に画像処理として使うには、二値化又はエッジ検出と輪郭近似の調整が
肝となるかと思っています。


#参考
[Emotion Explorer - OpenCV 四角形の検出](https://emotionexplorer.blog.fc2.com/blog-entry-230.html)
[docs.opencv.org - samples/cpp/squares.cpp](https://docs.opencv.org/4.4.0/db/d00/samples_2cpp_2squares_8cpp-example.html)
[docs.opencv.org - Structural Analysis and Shape Descriptors](https://docs.opencv.org/4.4.0/d3/dc0/group__imgproc__shape.html)