[OpenCV][Python3]検出した輪郭を描画し、輪郭線を近似して滑らかにする

# はじめに
輪郭が滑らかな物体に関しては、輪郭検出処理のみでも十分滑らかな輪郭が検出できる場合もある。しかしながら今回は、複雑な輪郭を検出し、近似して滑らかな輪郭にする必要があった。
そこで、OpenCV、Pythonを使用して被写体の輪郭線を描画後に近似している記事が見当たらなかったため記事にしちゃった。

# 環境
Python 3.7.0
opencv-python 3.4.3.18
numpy         1.15.2

# 処理の流れ
1. 元画像の読み込み
2. HSV色空間に変換
3. H、S、Vそれぞれの要素のみの画像を取得
4. 上記の使えそうな画像のヒストグラムを平坦化
5. 二値化
6. モーフィング処理（今回はクロージング）
7. 輪郭検出
8. 輪郭近似
9. 輪郭描画

本記事ではOpenCVの基本的な使い方の部分に関しては解説していません。
特に「8. 輪郭近似」に関して詳しく書きます。
その他の処理は他の記事などを参考にしてください。
また、一通りの処理を実装したプログラムを下記にアップロードしています。参考になれば。（途中でコメントを英語にするのが面倒になったため半端に英語です:cloud_lightning:）
Github：https://github.com/yoshihiroKani/ContourDetectionAndApproximation/tree/master

# 本題
まず元画像はこれです。
<img src="https://qiita-image-store.s3.amazonaws.com/0/205347/31e92e9f-5040-2ddd-84f9-cd9adcb3d84c.png" width=50%>
雲の輪郭を取得していきます。複雑な輪郭なのでやり甲斐がありそうですね。
それではやっていきましょ～

## 「処理の流れ - 3」<br>H、S、Vそれぞれの要素のみの画像を取得
H、S、Vの画像はそれぞれ以下のようになりました。

**H**
<img src="https://qiita-image-store.s3.amazonaws.com/0/205347/5768eebc-412e-e974-da29-e76438093d3c.png" width=50%>

**S**
<img src="https://qiita-image-store.s3.amazonaws.com/0/205347/7d01ae59-1269-d4ff-0331-781f3aeefbdd.png" width=50%>

**V**
![v_screenshot_16.10.2018.png](https://qiita-image-store.s3.amazonaws.com/0/205347/86bf0e4f-6600-8747-9400-cef26a232c7f.png)

## 「処理の流れ - 4」<br>上記の使えそうな画像のヒストグラムを平坦化
これを実施すると場合によりますが輪郭が更にクッキリします。
![hist_s_img_screenshot_16.10.2018.png](https://qiita-image-store.s3.amazonaws.com/0/205347/800a0a1f-9c95-f71a-8a60-f1e3a66f7296.png)

## 「処理の流れ - 5」<br>二値化
![result_bin_screenshot_16.10.2018.png](https://qiita-image-store.s3.amazonaws.com/0/205347/6280f96b-48ba-af0a-5742-fa7163781110.png)
（画像の枠は付けるのが面倒なので勘弁）

## 「処理の流れ - 6」<br>モーフィング処理（今回はクロージング）
![result_morphing_screenshot_16.10.2018.png](https://qiita-image-store.s3.amazonaws.com/0/205347/8134c9c5-15e6-36ca-047e-f6f7c1614709.png)
今回は画像で分かりやすいようにフィルタの値を大きくしています。実際はフィルタの形や大きさを変えたり、何度か繰り返しかけて見てくださいね。フィルタの大きさに関してはGithubの下記のコードの`(10, 10)`の部分を調整してください。形は`MORPH_RECT`これを別の変数に変えてね。（ちな、OpenCV初心者だと分かりづらいけど`MORPH_RECT`これはOpenCVに標準で用意されている定数です。あとはググって。）

```
## フィルタの設定
kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (10, 10))
```

## 「処理の流れ - 7」<br>輪郭検出
`findContours()`を使用しましょう。
今回は下記のようにしてます。

```
# 輪郭検出
tmp_img, contours, _ = cv2.findContours(
    result_morphing, cv2.RETR_LIST, cv2.CHAIN_APPROX_SIMPLE)
```

アプローチの方法は簡単に思いつく限りでも3通りありました。

1. モーフィング処理を行う。（上記「6. モーフィング処理（今回はクロージング）」にて既出）
2. 輪郭線を近似する。
3. 輪郭線の描画の太さを太くするw

####　1. モーフィング処理を行う
モーフィング処理の仕方でも輪郭の滑らかさは変わります。例えば、「20x20の矩形フィルタをかける場合」と、「20x20の十字形フィルタをかける場合」を画像で比較したものが以下です。(因みに、赤枠の辺りを拡大しています。)

（拡大部分）
<img src="https://qiita-image-store.s3.amazonaws.com/0/205347/3570a277-0063-c831-3b36-ff6e8d07948e.png" width=50%>

**20x20の矩形フィルタの結果**
![2020rect.png](https://qiita-image-store.s3.amazonaws.com/0/205347/56c50d1e-94dc-f7dc-f809-5799b57366fd.png)

**20x20の十字形フィルタの結果**
![2020cross.png](https://qiita-image-store.s3.amazonaws.com/0/205347/04583948-8560-4819-cfe2-043e034ebedf.png)

一長一短ですね。「雲の大まかな形を滑らかな輪郭で取りたい」場合は上、「雲の形をできる限り詳細に取りたい」場合は下といった感じでしょうか。
因みに、今回の被写体である雲は輪郭がクッキリしていないため分かりづらいですが（「分かりづらいですが」じゃねえわじゃあ何でこれにした）、雲の薄い部分まで取りたい場合は下記の二値化の閾値（`threshold()`の第二引数）を調整しましょう。（今は`200`なので、HSVのSのみのグレースケール画像の200以上のピクセルを雲として処理しています。）

```
# 二値化
_, result_bin = cv2.threshold(
    s_img, 200, 255, cv2.THRESH_BINARY)
```

更に下の画像のジャギーを減らすために2x2の矩形フィルタをかけてみました。

**20x20の十字形フィルタ→2x2の矩形フィルタの結果**
![2020cross_22rect.png](https://qiita-image-store.s3.amazonaws.com/0/205347/33f78ca7-0401-e821-a1ca-41dbfe694967.png)

んー…？内側の輪郭が取れるようになっていますが、ディテールが失われている部分もありますね。
まあまあ。手法の紹介にはなったかな（？）。
因みに、ある領域の内側の輪郭を取るかどうかの設定は以下の`RETR_TREE`部分でしています。

```
# 輪郭検出
tmp_img, contours, _ = cv2.findContours(
    result_morphing, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)
```
ここが分かりやすい。
参考：https://qiita.com/anyamaru/items/fd3d894966a98098376c

####　2. 輪郭線を近似する
今回のメインはこれ。上記「1. モーフィング処理を行う」で取り上げた「20x20の十字形フィルタをかける場合」の画像の輪郭線を近似してみましょう。
該当するソースコードは以下。

```
epsilon = 0.0001*cv2.arcLength(cnt,True)
approx.append(cv2.approxPolyDP(cnt,epsilon,True))
```
epsilonは「実際の輪郭と近似輪郭の最大距離を表し，近似の精度を表すパラメータ」（下記参考サイトより引用）

参考：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

epsilon=0.00005の場合
![approx.png](https://qiita-image-store.s3.amazonaws.com/0/205347/a2dbacf6-2869-4f16-cbad-cd5d070bfd98.png)

epsilon=0.0001の場合
![approx_0.0001.png](https://qiita-image-store.s3.amazonaws.com/0/205347/e0e53db6-4d64-b35f-4c81-f23d942a3b56.png)

ん？滑らか？滑らかとは？
曲線近似する方法もあるかも。もし曲線近似する方法を見つけた、知ってるという優しい天才イケメンがいたらどうかコメントで教えてください。
今回の方法は、大まかな形が直線的なではあるものの輪郭が入り組んでいるもの（川とか道路とか）を直線に近似したい時には使えそうですね。

####　3. 輪郭線の描画の太さを太くする
これは輪郭線のデータ自体はいじりません。ただ描画する線を太くするだけですが、細い線より太い線の方が当然滑らかには見える…はず。`drawContours()`の最後の引数が線の太さです。

```
cv2.drawContours(cp_org_img_for_draw, large_contours, -1, (255, 0, 0), 1)
```
これ以前の画像は全て`1`で設定した結果です。
試しに上記「epsilon=0.00005の場合」の画像をベースに`4`くらいに変えてみます。
![line_4.png](https://qiita-image-store.s3.amazonaws.com/0/205347/6c7a6daf-513e-ea07-391e-e1bb17c055d1.png)

…線の角は取れていますが、当然ディテールは潰れるところもあります。

# まとめ
今回は境界線を取得するにあたって、なぜか境界線が曖昧な画像を選択してしまいましたが、もっと空と雲の境界を明確に取得したい場合は閾値の設定を変更すれば対応できると思います。

以上です。