OpenCVで生活の乱れを採点してみた


# はじめに
生活の乱れを採点することで、生活改善につなげたいと思います。
この頃、部屋の片付けが面倒になってきたので、
部屋に設置したカメラの映像から、部屋の汚さをリアルタイム採点してみました。

# 作ったもの
汚部屋メーター
![obeya.movie.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/18646/20042ead-b8f4-1cd5-5922-d1161bf10dbf.gif)

- 左上の数字：汚部屋スコア
    - 点数が高いほど汚い
- 左上の英字：3段階判定
    - [CLEAN] きれい
    - [MESSY] 散らかっている
    - [OBEYA] 汚部屋
- 緑色の線
    - 散らかっている場所

# 使ったもの
- Python 3.5.4
- OpenCV 4.1.1.26
- MacOS 10.14.6
- USBカメラ ELECOM UCAM-C310FBBK

# 処理フロー
## 1. 画像処理
|    |    |
|:---|:---|
|<img width="400" alt="obeya_pic4.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/18646/6765673a-3a43-8524-95c7-f23d77fe1c45.png">|1-1. カメラからフレームを読み込みます。<br>参考：[動画を扱う — OpenCV-Python Tutorials 1 documentation](http://labs.eecs.tottori-u.ac.jp/sd/Member/oyamada/OpenCV/html/py_tutorials/py_gui/py_video_display/py_video_display.html#id3)|
|<img width="400" alt="obeya_pic3.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/18646/db9bb005-6e8b-cb10-f19f-d9c360fd7569.png">|1-2. グレースケールに変換します。<br>`cv2.cvtColor()`|
|<img width="400" alt="obeya_pic2.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/18646/978d078f-55e6-b5f5-0ff7-2a4fc967ed3c.png">|1-3. Canny法によるエッジ検出を行います。輪郭線を強調した画像が取得できます。<br>`cv2.Canny()`|

## 2. ベクトル・数値処理
### 2-1. 輪郭線の抽出
`1-3` の画像から、`cv2.findContours()`で輪郭線のベクトルデータを取得します。

### 2-2. ちょうどよい長さの輪郭線を選別
`2-1`で取得したベクトルを for 文に突っ込むと、輪郭線1本分のデータが得られるので、`cv2.arcLength()`を使って輪郭線の長さを取得します。
長すぎる、または短すぎる輪郭線は捨てちゃいます。

### 2-3. スコアの集計
`2-2`で得られた輪郭線の長さを単純に足し算して、点数を計算します。

## 3. 完成
最後に、`1-1` で読み込んだ元の画像、`2-2`で選別された輪郭線、`2-3`で計算したスコアなどを重ね合わせて出力すれば完成です。
<img width="400" alt="obeya_pic.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/18646/c2f04f42-9ca6-c202-1aec-52dc3f29e676.png">





# ソースコード
```python

import cv2
import numpy as np
import math
from collections import deque
from statistics import  median

FONT = cv2.FONT_HERSHEY_SIMPLEX
RED = (0, 0, 255)
GREEN = (0, 255, 0)
BLUE = (255, 0, 0)
ORANGE = (0, 130, 255)
SCORE_AP = (0, 80)
SCORE_FONT_SIZE = 2
SCORE_FONT_WEIGHT = 8

score_queue = deque([])


def get_score_summary(total_arc_length):
    score_queue.append(math.floor(total_arc_length / 100))
    if len(score_queue) > 30:
        score_queue.popleft()

    score = median(score_queue)

    if score > 200:
        return score, '[OBEYA]', RED
    elif score > 150:
        return score, '[MESSY]', ORANGE
    else:
        return score, '[CLEAN]', BLUE


def is_obeya_contour(arc_length):
    return 100 < arc_length < 900


def show_obeya_score(frame):
    score_frame = frame.copy()

    gray_frame = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
    canny_frame = cv2.Canny(gray_frame, 120, 200)
    contours, hierarchy = cv2.findContours(canny_frame, cv2.RETR_TREE, cv2.CHAIN_APPROX_NONE )

    total_arc_length = 0
    for contour in contours:
        arc_length = cv2.arcLength(contour, True)
        if is_obeya_contour(arc_length):
            cv2.drawContours(score_frame, contour, -1, GREEN, 3)
            total_arc_length += arc_length

    score, decision, color = get_score_summary(total_arc_length)
    cv2.putText(score_frame, ' '.join([str(score), decision]),
            SCORE_AP, FONT, SCORE_FONT_SIZE, color, SCORE_FONT_WEIGHT, cv2.LINE_AA)

    cv2.imshow('frame: contour', score_frame)


if __name__ == '__main__':
    cap = cv2.VideoCapture(0)
    while(True):
        ret, frame = cap.read()
        show_obeya_score(frame)
        if cv2.waitKey(1) & 0xFF == ord('q'):
            break
    cap.release()
    cv2.destroyAllWindows()

```