傘はどこへ行ったのか

# あらまし
とある雨の日、私は残業していました。

その日の最終退勤は私のようでした。

残業にほとほと疲れて、勤怠をきって、ふと傘立てをみると傘が一本もない・・・、おかしい、私は傘を持ってきたはずなのに・・・

がんばって残業したのに、この仕打ち・・・

そこで、私は、間違って傘を持っていってしまった人を見つけるソリューションを考えることにしたのです。

# 環境
## 全体構成
全体の構成はRaspberry Pi2にセンサーをつけて、センサーが反応したら、カメラモジュールを使って3枚の写真を間を取って撮影し、撮影したものをS3へアップするというよくある構成です。

写真をそのままS3へ上げると何か言われそうだと思ったので、今回は顔部分にモザイクをかけてアップロードすることにしました。

## ハードウェア
- Raspberry Pi2
- 人感センサー
- カメラモジュール

見た目はこんな感じになります
![DSC_0011.JPG](https://qiita-image-store.s3.amazonaws.com/0/65020/eafd7d7b-1d5a-e4bc-6e79-84c03093e6f3.jpeg)


## ソフトウェア
- Debian 8
- Python2.7
- boto3
- OpenCV2
- picamera

## クラウド
- Amazon S3

## 人感センサー
正式名称は「焦電型赤外線（人感）センサーモジュール」というようです。
SB412Aで検索すると出てきます。
真ん中にネジがあるほうをこちらに向けて、左からGPIOの3.3V、18番ピン、GNDへとジャンパ線で直接つなぎました。
間違ってつなげると、焦げ臭いにおいがします。

![DSC_0009.JPG](https://qiita-image-store.s3.amazonaws.com/0/65020/4219e617-c9d9-0e95-62e3-6a5fbe688e3c.jpeg)


## カメラ
Raspberry Pi カメラモジュール V2
たぶんRaspberry Piで一番よく使われているタイプのものだと思います。
https://www.robotshop.com/jp/ja/raspberry-pi-camera-module-v2.html?gclid=CjwKCAiAsejRBRB3EiwAZft7sMC5OTEd6WE8igFKzrkBXFaztD7rxaD_WFuL3tfLks_ypujOZlXj3xoC6oIQAvD_BwE

# プログラム
## 概要
一定の間隔で人感センサーからの信号を読み取り(monitor.py)、反応があった場合はカメラで複数回写真を撮り(camera.py)、画像を顔認識(face.py)しモザイクをかけ、S3ヘアップロード(uploader.py)します。

## 下準備
1. picamera、OpenCV、boto3をインストール
2. boto3を動かすにはawsのaccess_key_idとaws_secrete_access_keyが必要になるので[こちら](http://docs.aws.amazon.com/ja_jp/cli/latest/userguide/cli-chap-getting-started.html)を参考にして準備してください
3. config.iniへ画像をアップロードするバケット名を設定する
4. 顔認識にOpenCVに付属している学習済みの分類機を使うので、haarcascade_frontalface_default.xmlをコピー

## config.ini
アップロード先のバケット名を設定します。

```
[s3]
bucket_name = 
```

## monitor.py
プログラム全体のエントリポイントになります。

```py:monitor.py
import ConfigParser

import RPi.GPIO as GPIO
from time import sleep

from face import find_face_with_mosaic
from camera import Camera
from uploader import Uploader


def main():
    monitor()


def get_bucket_name():
    config = ConfigParser.ConfigParser()
    config.read('config.ini')
    return config.get('S3', 'bucket_name')
    

def monitor():
    bucket_name = get_bucket_name()
    
    GPIO.setmode(GPIO.BCM)
    GPIO.setup(18, GPIO.IN)

    # 2枚の写真を0.5秒の間隔をあけて撮影
    cam = Camera(2, 0.5)

    while True:
        # 人感センサーに反応があった場合1が返ってきます
        if GPIO.input(18):
            print("monitor!!!")

            # カメラから撮った写真のパスをもらう
            paths = cam.capture()

            # 写真を顔認識にかける
            find_faces(paths)

            # S3へアップロード
            upload(bucket_name, paths)
        # 1秒間間を空ける
        sleep(1)

        
def find_faces(paths):
    for path in paths:
        find_face_with_mosaic(path)
        

def upload(bucket_name, paths):
    uploader = Uploader(bucket_name)
    for path in paths:
        uploader.upload(path, path)
        print('%s uploaded' % path)
        

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

## camera.py
カメラモジュールでの撮影をするクラスです。

```py:camera.py
import datetime
import os
import time

import picamera


class Camera(object):

    def __init__(self, num, delay):
        self._num = num
        self._delay = delay
        self._camera = picamera.PiCamera()

    def capture(self):
        # 現在時刻のディレクトリを作成
        dir_path = 'img/' + self._get_now_string()
        self._make_dir(dir_path)
        return self._sequential_shooting(dir_path)

    def _get_now_string(self):
        now = datetime.datetime.now()
        l = [now.year, now.month, now.day, now.hour, now.minute, now.second]
        l = map(str, l)
        return '-'.join(l)
    
    def _make_dir(self, path):
        os.mkdir(path)

    def _sequential_shooting(self, dir_path):
        paths = []

        # monitor.pyから指定された回数だけ写真を撮り、作成したディレクトリに保存します
        for n in xrange(self._num):
            path = dir_path + '/{}.jpg'.format(str(n))
            paths.append(path)
            self._capture(path)
            time.sleep(self._delay)

        return paths

    # Raspberry Piだとフルサイズの画像を処理するには時間がかかるのでデフォルトでサイズを3分の1にする
    def _capture(self, path, size=(640, 360)):
        self._camera.capture(path, resize=size)
        print('capture %s' % path)
```

## face.py
顔認識して、モザイクをかける処理です。
他のモジュールに比べて、全体的に雑です・・・

```py:face.py
import cv2
from cv2 import CascadeClassifier


def find_face_with_mosaic(img_path):
    classifier = create_classifier()

    # 画像にグレーアウト処理をかける
    origin, grayed = read_and_gray(img_path)
    faces = detect(classifier, grayed)

    # 顔だと認識された部分の数だけモザイク処理をかけます
    for (x, y, w, h) in faces:
        # 顔だと認識された部分を切り取って、mozaic関数へ渡す
        face = origin[y: y + h, x: x + w]
        mozaiced = mozaic(face)
 
        # モザイクがかけられた顔部分を元の画像にはめ込む
        origin[y: y + h, x: x + w] = mozaiced
        cv2.imwrite(img_path, origin)
        break


def create_classifier():
    return cv2.CascadeClassifier('haarcascade_frontalface_default.xml')


def read_and_gray(path):
    img = cv2.imread(path)
    grayed = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
    return img, grayed


def detect(classifier, grayed_img):
    return classifier.detectMultiScale(grayed_img, 1.1, 5)


# 画像を縮小拡大することでモザイクをかける
# 今回は5分の1に縮小拡大する
def mozaic(img):
    cut = img.shape[:2][:: -1]
    shrinked = cv2.resize(img, (int(cut[0]/ 5), int(cut[0] / 5)))
    exp = cv2.resize(shrinked, cut, interpolation=cv2.INTER_NEAREST)
    return exp
```

## uploader.py
S3へ画像をアップロードするクラス。

```py:uploader.py
import boto3


class Uploader(object):

    def __init__(self, bucket_name):
        self._bucket = self._get_bucket(bucket_name)

    def _get_bucket(self, bucket_name):
        s3 = boto3.resource('s3')
        return s3.Bucket(bucket_name)
        
    def upload(self, local_path, upload_path):
        self._bucket.put_object(
            ACL='public-read',
            Key=upload_path,
            Body=open(local_path, 'rb'),
            ContentType='image/jpeg')
```

# まとめ
Raspberry Piがあればお手軽(センサー、ジャンパ線含めて1000円もかからない)に誰が傘を持って行ったのか監視する仕組みを作れました。
OpenCVでの顔検出処理は、最近のPCなら割と一瞬ですが、Raspberry Pi2程度のスペックだと思ったより時間がかかります。顔認識処理、アップロード処理はなんらかしらの方法で非同期にするのが現実的でしょうか。

OpenCVの顔認識はそのまま使うと横顔などをほぼ認識してくれないようで、自分でディープラーニングを使って0から顔認識のライブラリを構築しようとしたんですが、今回は時間が足りずにできませんでした。

実運用してないので、写真を撮る間隔など、実運用をさせようと思うと細かい調整が必要かと思います。

この仕組みで傘を間違って持って行ってしまう人が減るといいな・・・