【爆速】OpenCVで複数の物体検出　－ラズパイ－

[「背景差分で物体検出をしてみた」の記事](https://qiita.com/komiya-m/items/af30a055b5d81310f6e7)が面白くて、「複数の物体」でも
検出できるのか検証してみました。

<blockquote class="twitter-tweet" data-lang="ja"><p lang="ja" dir="ltr">ディープラーニングを使わずに、ラズパイで複数の「物体検出」を<br>実装しました。OpenCVを使っております。 <a href="https://t.co/5DQjPO1sfH">pic.twitter.com/5DQjPO1sfH</a></p>&mdash; shinmura0 (@shinmura0) <a href="https://twitter.com/shinmura0/status/1131758716817104902?ref_src=twsrc%5Etfw">2019年5月24日</a></blockquote>
<script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

#はじめに
きっかけは、ラズパイで**2つのディープラーニングモデル**を動かしていたときのこと。
予想通り、速度は激遅で使いものになりませんでした。

そのため、OpenCVで物体検出できないか？と考え実装してみました。

本稿では、ディープラーニングを使わないOpenCVによる**複数の物体検出**を行ってみます。

#OpenCVによる物体検出
まずは、背景写真を用意します。

![1.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/264781/1bd3c8a2-2713-40c8-b9ee-f02487d84efb.jpeg)


そして、フィルターによる前処理を行います。

```py
import cv2
img1 = cv2.imread("background.jpg")
img1 = cv2.GaussianBlur(img1, (5, 5), 0)
```

次に、物体検出させたい写真を用意します。
![3.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/264781/8c01dc4b-3204-2e19-ec2f-cc037cf37bab.jpeg)

同じく前処理をしておきます。

```py
img1 = cv2.imread("test.jpg")
img2 = cv2.GaussianBlur(img2, (5, 5), 0)
```

そして、OpenCVの機能で物体検出をさせます。

```py
fgbg = cv2.bgsegm.createBackgroundSubtractorMOG()
fgmask = fgbg.apply(np.uint8(img1))
fgmask = fgbg.apply(np.uint8(img2))
```

```py
cv2.imshow(fgmask)
```

![ダウンロード.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/264781/ce724192-33eb-11fb-8ec1-75bf308e2783.png)


恐ろしく簡単に検出することができました。
しかも、爆速です。

#k-means法によるクラスタリング
次に得られたモノクロマップで物体を分割します。

今回は、k-means法によるクラスタリングを行います。
本当は混合ガウス分布の方が良いのですが、処理を少しでも軽くするために
k-means法（正確にはk-means++）を使います。

得られたマップに対し、k-means法を適用します。

```py
from sklearn.cluster import KMeans
Y, X = np.where(fgmask > 200)
y = KMeans(n_clusters=2, random_state=0).fit_predict(np.array([X,Y]).T)
```

これも爆速で実行することができます。

一応可視化します。

```py
import matplotlib.pyplot as plt
plt.scatter(X, -Y+288, c=y)
plt.xlim(0,288)
plt.ylim(0,288)
plt.show()
```

![ダウンロード (2).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/264781/68057367-c79d-e81f-ec33-6bc11c9be7a2.png)

あとは、クラスタに合わせてバウンディングボックスを作ります。

```py
def get_x_y_limit(Y, X, result, cluster):
    NO = np.where(result==cluster)
    x_max = np.max(X[NO])
    x_min = np.min(X[NO])
    y_max = np.max(Y[NO])
    y_min = np.min(Y[NO])

    x_max = int(x_max)
    x_min = int(x_min)
    y_max = int(y_max)
    y_min = int(y_min)
    return x_min, y_min, x_max, y_max

def bounding_box(img, x_min, y_min, x_max, y_max):
    img = cv2.rectangle(img, (x_min, y_min), (x_max, y_max), (0, 255, 0), 5)
    return img
```

```py
x_min, y_min, x_max, y_max = get_x_y_limit(Y, X, y, 0)
img2 = bounding_box(img2, x_min, y_min, x_max, y_max)

x_min, y_min, x_max, y_max = get_x_y_limit(Y, X, y, 1)
img2 = bounding_box(img2, x_min, y_min, x_max, y_max)
```

```py
cv2_imshow(img2)
```

![ダウンロード (3).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/264781/a0872330-5b95-ec2a-08dd-5fbdbdb0dced.png)


注目すべきは「矢印」の部分。
影が映り込んでいるのにも関わらず、検出されない点が素晴らしいです。

単に背景写真と差分をとると、どうしても影が検出されてしまうのですが、
ここは天下のOpenCV、さすがです！

#制約
ただし、本手法は制約があります。
それは、「物体が一つでも必ず**複数に分割されてしまう**」ということです。

具体例を挙げます。

今回は手を二つ認識させたいため、クラスタリングの数を「2」に設定します。
手が二つある写真は、先ほど見ていただいたとおり、手が別々に検出されます。

![ダウンロード (3).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/264781/ac58cf81-98f0-70c0-5c80-df922c9ee2f2.png)

ところが、手が一つの写真を入れても二つの物体が検出されてしまいます。

![ダウンロード.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/264781/cfc6049e-2f1f-c85e-2e0d-fb21af5c4e5d.png)


ここはk-means法を使っている以上、しょうがないところです。

また、k-means法は特性上、距離が近いものを同じ物体と認識します。
従って、よく言われる[半月が重なったような形状](http://neuro-educator.com/ml10/)ではうまく検出することが
できません。

「物体数が決まっていない」、もしくは「複雑な形状を検出したい」場合は、
[DBSCAN](https://scikit-learn.org/stable/modules/clustering.html)を使うと良いかもしれません。

#ラズパイで実行
ラズパイでリアルタイムに動かすコードは[こちら](https://github.com/shinmura0/OpenCV-Object-Detection/tree/master)に置きました。
main_opencv.pyを実行し、「p」キーを押せば、以下のようにリアルタイムで
検出することができます。

<blockquote class="twitter-tweet" data-lang="ja"><p lang="ja" dir="ltr">ディープラーニングを使わずに、ラズパイで複数の「物体検出」を<br>実装しました。OpenCVを使っております。 <a href="https://t.co/5DQjPO1sfH">pic.twitter.com/5DQjPO1sfH</a></p>&mdash; shinmura0 (@shinmura0) <a href="https://twitter.com/shinmura0/status/1131758716817104902?ref_src=twsrc%5Etfw">2019年5月24日</a></blockquote>
<script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

本気を出せば、13FPSくらいになります。
終了したい場合は、「q」キーを押してください。

ただし、単にopencvをインストールしただけだと動かず、以下のようにラズパイで
contribをインストールする必要がありました。

```
sudo -H pip3 install opencv-contrib-python==3.4.3.18
```

バージョンはお好みで指定してください。

#まとめ
+ ラズパイでも、OpenCVを使えば爆速で**複数の物体検出ができる。**
+ ただし、固定カメラ限定で、かつ**事前に**検出したい物体数を決めておく必要がある。
+ 本技術を応用すれば、物体検出で必要な**アノテーションデータも手軽に作成する**ことができる。