Raspberry Pi AI Cameraで居眠りしている人、見～つけた

<!-----------------
記事本文は下記レビュー観点を意識したうえで、自由に記載してください。 
 - [ ] 非公開情報はないか
 - [ ] 社内用語が使われていないか
 - [ ] 共通ワーディングを利用しているか
 - [ ] 技術的間違いがないか
 - [ ] リンク切れを起こしていないか
 - [ ] 政治、宗教等センシティブな領域に言及していないか (CATOPE対応)
 - [ ] 文体の統一/誤字チェック（JustRight対応）
 - [ ] その他不適切な表現・内容が含まれていないか
 - [ ]  問い合わせに関する文言を記載しているか  　
------------------->  

こんにちは！  
ソニーセミコンダクタソリューションズ の西村と申します。
Raspberry Pi AI Cameraを使った居眠り検知アプリを作ったので、記事を投稿します。

# この記事の要約
- Raspberry Pi AI Cameraを使うと居眠り検知アプリが簡単に作れます
- Raspberry Pi AI Cameraを使ったアプリ開発の一連の流れを説明します
- Raspberry Pi AI Camera、AITRIOSに興味がある～これから触る予定がある方をメインにした記事です

# はじめに
Raspberry Pi AI Cameraが発売されたので、色々遊んでみようと思い、アプリを開発してみました！  
前回の[自宅モニタリングアプリ](https://qiita.com/SSS-AtsushiNishimura/items/49058923518bf91729ce)に引き続き、今回は居眠り検知アプリになります。  
私はどちらかと言うと学生時代に居眠りをしてしまう方だったのですが、先生としては居眠りされるのは心地良いものではないですよね…  
あとは混んでいる飲食店でついつい寝てしまっている人もたまに見かけたりします。  
そんな時に、居眠りしてますよ～って分かると、色々と対応しやすいですよね！  
とにかく自らを律する意味でも、居眠り検知アプリを作っていきたいと思います…！  

# 本文
## 0. 今回の作業の流れ
プログラミングや AI の高度な知識などは不要ですが GitやPythonなどを一部使用する点について、あらかじめご了承ください。

	1. Raspberry Pi AI Cameraのセットアップ
    2. Raspberry Pi AI Cameraから骨格推定の実施
    3. アプリ作成

## 1. Raspberry Pi AI Cameraのセットアップ
まずはRaspberry Pi AI Cameraのセットアップになります。  

具体的な手順の説明については省略しますが、[こちらの記事](https://qiita.com/daiki0321/items/4223611adb453d6c7aa2)に詳細に記載されていますので、よろしければ参考にしてみてください。  
2章の最後まで終わっていれば、本記事用のセットアップとしては完了です。

## 2. Raspberry Pi AI Cameraから骨格推定の実施
つづいて、サンプルを動かしてRaspberry Pi AI Cameraで骨格を推定できるか試してみましょう！  

0から骨格推定&画面への表示を実装する場合、かなりの手間と時間が必要だと思います。  
ですが、つい最近、弊社から[Application Module Library](https://github.com/SonySemiconductorSolutions/aitrios-rpi-application-module-library/tree/6c775fb636d5934ed21c324b26e5947aeec25578)なるものがリリースされまして、それを使えば簡単にサンプルが作れちゃうそうです。  
折角ですので、使わせていただこうと思います！

まずはGitHubからリポジトリをクローンします。

    git clone https://github.com/SonySemiconductorSolutions/aitrios-rpi-application-module-library.git
    cd aitrios-rpi-application-module-library

&nbsp;  
つづいて、READMEの  
　1. Development environment setup.  
　2. Building the Python wheel.  
に記載されている下記のコマンドを実行してください。   

    # 1. Ensure that your Raspberry Pi runs the latest software:
    sudo apt update && sudo apt full-upgrade
    sudo apt install imx500-all
    sudo apt install python3-opencv python3-munkres python3-picamera2

    # 2. Create your project virtual environment and install modlib.
    python -m venv .venv --system-site-packages
    . .venv/bin/activate
    pip install modlib-<version>-py3-none-any.whl
 
&nbsp;  
つぎに、SleepDetection.pyファイルを新規作成して、リポジトリ内のexamples/aicam/posenet.pyのコードを丸ごとコピペします。  

最後に、下記のコマンドを実行してみてください。

    . .venv/bin/activate && python SleepDetection.py


![KeyPoint.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3993347/f25a388f-0ae9-440c-bc82-3ad535fa46ca.png)
&nbsp;  

いかがでしょう！  
簡単な操作でしたが、しっかりと骨格推定できている様子が分かると思います！  
顔を伏せた状態&マスクを着用していても、しっかりと認識できていると思います！  
これで、居眠りしている人もバッチリ検知できますね！  


## 3. アプリ作成
2章で触ったサンプルの一部を変更して、実際に居眠り検知アプリを作成していきます！  
今回は、  

    骨格推定時のKeyPointの座標がほとんど動かない場合  

に居眠りをしたと判定します。  
他にも目が閉じているかどうかを判断するなど、色々方法はあると思いますので、皆さんお好みの方法で実装して頂ければと思います！  
それでは下記の流れで実装していきます。

    3.1. 骨格推定データを保存 
    ↓  
    3.2. 前回と現在のフレームで推定したKeyPointの座標の距離を計算  
    ↓  
    3.3. 距離が閾値を超えているかの判定
         a. 距離が閾値未満なら、motionless_countを+1する  
         b. 距離が閾値以上なら、motionless_countを0にする  
    ↓  
    3.4. motionless_countが閾値を超えたら、居眠りと判定
    ↓
    3.5. UI表示

### 3.1. 骨格推定データを保存
まずは骨格推定したデータを保存します。  
poses_recordというリストにデータを追加していきます。  
前回と現在のフレームを比較するためリストの長さを2までとしていますが、必要に応じて変更して頂ければと思います。  
先程のプログラムから + となっている部分を追加しました。 

```python:SleepDetection.py
    from modlib.apps import Annotator
    from modlib.devices import AiCamera
    from modlib.models.zoo import Posenet

    device = AiCamera()
    model = Posenet()
    device.deploy(model)

    annotator = Annotator()

+   poses_record = []

    with device as stream:
        for frame in stream:
            detections = frame.detections[frame.detections.confidence > 0.5]
            annotator.annotate_poses(frame, detections)
            frame.display()

+           poses = frame.detections
+           poses_record.append(poses)
+           if len(poses_record) > 2:
+               poses_record.pop(0)
```


### 3.2. 前回と現在のフレームで推定したKeyPointの座標の距離を計算
つづいて、前回と現在のフレームで推定したKeyPointの座標の距離を計算します。  
新たにtrack_motionless()という関数を用意します。

```python:SleepDetection.py    
    KEYPOINT_NAME = [
        "nose",             #0
        "leftEye",          #1
        "rightEye",         #2
        "leftEar",          #3
        "rightEar",         #4
        "leftShoulder",     #5
        "rightShoulder",    #6
        "leftElbow",        #7
        "rightElbow",       #8
        "leftWrist",        #9
        "rightWrist",       #10
        "leftHip",          #11
        "rightHip",         #12
        "leftKnee",         #13
        "rightKnee",        #14
        "leftAnkle",        #15
        "rightAnkle"        #16
    ]

    # Track motionless
    def track_motionless(w, h, poses_record, motionless_count):
        for keypoint_idx in range(len(KEYPOINT_NAME)):
            x0 = int(poses_record[0].keypoints[0][2 * keypoint_idx + 1] * w)
            y0 = int(poses_record[0].keypoints[0][2 * keypoint_idx] * h)
            x1 = int(poses_record[1].keypoints[0][2 * keypoint_idx + 1] * w)
            y1 = int(poses_record[1].keypoints[0][2 * keypoint_idx] * h)
                
            distance = (x1 - x0)**2 + (y1 - y0)**2
```

前回データ (=poses_record[0]) と今回データ (=poses_record[1]) からそれぞれx,y座標にアクセスし、距離を計算します。  
取得できるKeyPointは全部で17種類もあるそうです！  
今回はとくに使用するKeyPointは指定しませんが、例えばleftEyeとrightEyeを認識した時だけ処理させるような制限を付けても良さそうです。

### 3.3. 距離が閾値を超えているかの判定
つづいて、距離が閾値を超えているかの判定を行います。   
KeyPointの座標がほとんど動かないフレーム数を数える変数として、motionless_countを用意します。 

    a. 距離が閾値未満なら、motionless_countを+1する  
    b. 距離が閾値以上なら、motionless_countを0にする  

居眠りして前回と座標が大きく変わらない場合は、motionless_countを増やします。  
座標が変わらないことを判定するために、閾値としてDISTANCE_THRESHOLDを用意していますが、必要に応じて変更してください。  
ただし、KeyPointが1つしか取れてない場合はデータとして信憑性があまり無いため、距離が閾値未満のKeyPointが複数ある場合にのみmotionless_countを加算します。  
これにより、そもそも人が映っていない場合にも対応できます。  

```python:SleepDetection.py
    KEYPOINT_THRESHOLD = 5
    DISTANCE_THRESHOLD = 100
    KEYPOINT_SCORE_THRESHOLD = 0.5

    # Track motionless
    def track_motionless(w, h, poses_record, motionless_count):
        keypoint_count = 0
        for keypoint_idx in range(len(KEYPOINT_NAME)):
            if (poses_record[0].keypoint_scores[0][keypoint_idx] >= KEYPOINT_SCORE_THRESHOLD and 
                poses_record[1].keypoint_scores[0][keypoint_idx] >= KEYPOINT_SCORE_THRESHOLD):
                x0 = int(poses_record[0].keypoints[0][2 * keypoint_idx + 1] * w)
                y0 = int(poses_record[0].keypoints[0][2 * keypoint_idx] * h)
                x1 = int(poses_record[1].keypoints[0][2 * keypoint_idx + 1] * w)
                y1 = int(poses_record[1].keypoints[0][2 * keypoint_idx] * h)
                
                distance = (x1 - x0)**2 + (y1 - y0)**2
                if distance < DISTANCE_THRESHOLD:
                    keypoint_count += 1
        
        if keypoint_count >= KEYPOINT_THRESHOLD:
            print("[INFO] Detected 5 or more KeyPoints.")
            motionless_count += 1
        else:
            motionless_count = 0
        
        return motionless_count
```

### 3.4. motionless_countが閾値を超えたら、居眠りと判定
つづいて、motionless_countが閾値を超えている場合に居眠りと判定する処理です。  
とはいっても大したことはせず、下記を追加するだけになります。

```python:SleepDetection.py
    if motionless_count > SLEEP_THRESHOLD:
        print("[INFO] Detect Sleep")
```

### 3.5. UI表示
最後はUIとして居眠りしていることを分かりやすく表示しましょう！  
今回はOpenCVを使用して画像に「SLEEP」と表示しています。  
他のパッケージを利用したり、音を鳴らすなどの他の手段を利用したりしても良いと思います！  
ここでは、UI表示に関係する部分だけを抜き出して記載します。  

```python:SleepDetection.py
    import cv2

    TEXT = "SLEEP"

    def display_text(image):
    print("[INFO] Display Text")
    cv2.putText(image,
            text=TEXT,
            org=(100, 50),
            fontFace=cv2.FONT_HERSHEY_SIMPLEX,
            fontScale=2.0,
            color=(0, 0, 255),
            thickness=3) 
```
![DetectSleep_mosaic.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3993347/82603df8-fad9-46d5-b6b8-794de6241e61.png)

### 全体のプログラム
最後に全体のプログラムを載せておきます。  
プログラムは[GitHub](https://github.com/SonySemiconductorSolutions/aitrios-rpi-sandbox)上にも公開されていますので、そちらも参考にしてください。

```python:SleepDetection.py
    import time
    import numpy as np
    from modlib.apps import Annotator
    from modlib.devices import AiCamera
    from modlib.models.zoo import Posenet
    import cv2

    # Definition of const
    KEYPOINT_NAME = [
        "nose",             #0
        "leftEye",          #1
        "rightEye",         #2
        "leftEar",          #3
        "rightEar",         #4
        "leftShoulder",     #5
        "rightShoulder",    #6
        "leftElbow",        #7
        "rightElbow",       #8
        "leftWrist",        #9
        "rightWrist",       #10
        "leftHip",          #11
        "rightHip",         #12
        "leftKnee",         #13
        "rightKnee",        #14
        "leftAnkle",        #15
        "rightAnkle"        #16
    ]


    KEYPOINT_THRESHOLD = 5
    DISTANCE_THRESHOLD = 100
    KEYPOINT_SCORE_THRESHOLD = 0.5
    SLEEP_THRESHOLD = 10
    PAUSE_AFTER_DETECTION = 0.5  # (seconds)
    TEXT = "SLEEP"


    # Track motionless
    def track_motionless(w, h, poses_record, motionless_count):
        keypoint_count = 0
        # Calculate the coordinate difference between the previous frame and the current frame for each KeyPoint.
        for keypoint_idx in range(len(KEYPOINT_NAME)):
            if (poses_record[0].keypoint_scores[0][keypoint_idx] >= KEYPOINT_SCORE_THRESHOLD and 
                poses_record[1].keypoint_scores[0][keypoint_idx] >= KEYPOINT_SCORE_THRESHOLD):
                x0 = int(poses_record[0].keypoints[0][2 * keypoint_idx + 1] * w)
                y0 = int(poses_record[0].keypoints[0][2 * keypoint_idx] * h)
                x1 = int(poses_record[1].keypoints[0][2 * keypoint_idx + 1] * w)
                y1 = int(poses_record[1].keypoints[0][2 * keypoint_idx] * h)
                print(f"{KEYPOINT_NAME[keypoint_idx]}   x:{x0}, y:{y0}")
                
                distance = (x1 - x0)**2 + (y1 - y0)**2
                if distance < DISTANCE_THRESHOLD:
                    keypoint_count += 1
        
        if keypoint_count >= KEYPOINT_THRESHOLD:
            print("[INFO] Detected 5 or more KeyPoints.")
            motionless_count += 1
        else:
            motionless_count = 0
        
        return motionless_count

    # Display Text
    def display_text(image):
        print("[INFO] Display Text")
        cv2.putText(image,
                text=TEXT,
                org=(100, 50),
                fontFace=cv2.FONT_HERSHEY_SIMPLEX,
                fontScale=2.0,
                color=(0, 0, 255),
                thickness=3) 


    # Detection by modlib
    def detect_sleep():
        print("[INFO] Starting sleep detection.")
        device = AiCamera()
        model = Posenet()
        device.deploy(model)

        annotator = Annotator()

        last_detection_time = 0
        motionless_count = 0
        poses_record = []

        with device as stream:
            for frame in stream:
                current_time = time.time()
                if current_time - last_detection_time > PAUSE_AFTER_DETECTION:
                    last_detection_time = time.time()

                    h, w, _ = frame.image.shape

                    poses = frame.detections
                    poses_record.append(poses)
                    if len(poses_record) > 2:
                        poses_record.pop(0)
                        motionless_count = track_motionless(w, h, poses_record, motionless_count)

                    print(f"Motionless Count: {motionless_count}")
                    if motionless_count > SLEEP_THRESHOLD:
                        print("[INFO] Detect Sleep")
                        display_text(frame.image)

                    annotator.annotate_poses(frame, frame.detections)
                    frame.display()

    # Main
    if __name__ == "__main__":
        detect_sleep()
```

下記のコマンドで実行できますので、興味がある方は試してみてください！

    . .venv/bin/activate && python SleepDetection.py

# 困った時は

もし、記事の途中でうまくいかなかった場合は、気軽にこの記事にコメントいただいたり、以下のサポートのページもご覧ください。  
コメントのお返事にはお時間を頂く可能性もありますがご了承ください。

<!-----------------RasPiに関する記事が含まれる場合は下記を追加してください。------------------->  
RaspberryPiに関連する疑問がある場合は、下記フォーラムをご確認、ご活用ください。
- [Raspberry Pi Forums](https://forums.raspberrypi.com/)

# 終わりに

今回の記事ではRaspberry Pi AI Cameraで居眠り検知アプリを作ってみました。  
正直なところ、居眠り検知をするためにわざわざ専用のカメラやアプリを買うのは気が引けますが、Raspberry Pi AI Cameraであれば他にも色んな用途に使えるので、飽きてしまった時にも安心ですね！  
ネタアプリを作りたくなってしまう方にはすごくおすすめしたいです！笑  
&nbsp;  
他にも色々な用途に活用できると思いますので、よろしければぜひ皆さんもお試しください！  
「こんなアプリもできるんじゃない？」などのアイデアもぜひぜひお待ちしています！