ChatGPTを駆使して、Raspberry PiとTensorflowを使ってドア監視をできるようにした

chatGPTを駆使して、玄関の鍵が閉まっているかどうかを監視するプログラムを作りました。

# はじめに
## 今回の要約
- 自分はとにかく玄関の鍵を閉め忘れることが多く、家族にも怒られていた
- Raspberry Piとwebカメラが家にあった
- 色々調べたところ、機械学習の部分はGoogle Cloud Visionで無料実装できる
    - すでに先駆者のブログは存在する：https://r-kurain.hatenablog.com/entry/2019/10/17/211134
- ある程度ライブラリが古い部分や、多少わかるOpenCVへのリライトについては、ChatGPTにすべて任せれば実装できるのではないか
- ある程度構想がまとまった段階で、chatGPTにソースコードの書き換えを依頼するほうが意図した書き換えを行ってくれる。
    - ちょっとずつ書き換えていけばいくほど、過去の積み重ねを忘れてしまいがち
- 自分のように、普段はコードを書かない人間でも、chatGPTならそのハードルを簡単に超えることができる。
    - 「雑に作る」範囲なら

## 本プログラムのソースコード
source: https://gist.github.com/iamakawa/b3206ef33bc5d13be332ab9edaf5becc

https://gist.github.com/iamakawa/b3206ef33bc5d13be332ab9edaf5becc

- 上記ブログの方が公開しているgithubをforkする形で公開しています

## 開発環境・全体の構成
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/90313/a50fba1e-661c-9161-6427-c8362b54e161.png)


## slackのメッセージ
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/90313/47497c9c-3b67-b5c5-46d2-0cc84f8e6a22.png)

## ドアを検知している様子
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/90313/66738f8e-c6e4-b1e7-8d0a-b5394e463c60.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/90313/1f882c92-e942-05a5-7fc7-1fb4159959ff.png)

# 実装
## raspberrypiとwebカメラの取り付け
- 下記のようにカメラを配置。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/90313/38841044-ee7c-2644-3e52-9bc3c687ca9b.png)

## モデル学習のための画像の定点撮影
- 以下の手順でプログラムを構築。
    - https://chat.openai.com/share/dc5404a2-8e27-409b-9827-12f23dd6eaef
```
raspiにUSBカメラを接続して、1分間に1回、画像を撮影したいです。
どのような処理をしたら良いでしょうか。

###
ありがとうございました。他のプログラムとの兼ね合いで、PiCameraを使用したいです。
具体的には、下記のようなプログラムを挿入し、画像の解像度を合わせたいと考えています。
この処理ができるよう、提示いただいたプログラムを書き換えてもらえますか。
＝＝＝
camera = PiCamera()
    camera.resolution = (640, 480)

###
ありがとうございます。すみませんが、picameraではなく、openCVのソリューションを使用して実施したいです。
cv2.videocapture、cv2.resizeを使用してもらえませんか。

###
ありがとうございます、助かります。
実行結果を確認したいので、画像の書き込みが完了したら、結果をprintしてもらえますか。
```

- 最終的に得られたプログラム
    - (1分に1度だと、ほぼ同じ画像になってしまったため、最終的には10分に1度くらいでモデルを作成した)

```
import cv2
import time

# VideoCapture オブジェクトの初期化
cap = cv2.VideoCapture(0)

try:
    while True:
        # カメラからフレームを読み込む
        ret, frame = cap.read()

        if ret:
            # フレームをリサイズ
            resized_frame = cv2.resize(frame, (640, 480))

            # 現在のタイムスタンプをファイル名に使用
            filename = time.strftime("%Y-%m-%d_%H:%M:%S.jpg")

            # 画像を保存
            cv2.imwrite(filename, resized_frame)

            # 保存した画像のファイル名を表示
            print(f"画像保存完了: {filename}")

        # 1分待機
        time.sleep(60)

finally:
    # カメラリソースを解放
    cap.release()
```

## tensorflowモデルの作成
- Google Cloud Visionを用いてtensorflowのモデルを作成。
- 画像に対してopen、closeそれぞれのモデルを割り当てるような形。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/90313/9f727895-1de3-09f2-bd35-be5c3a413a22.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/90313/02c9b73f-c424-b973-b3bd-0e7336eac980.png)

## コア部分のリライト
- chatGPTに以下のような文章を投げました。
    - https://chat.openai.com/share/10481392-fa39-4b4e-8c64-8260716667cc

```
以下のプログラムを、下記の方針で書き直してください。
・tensorflow v1.4の基準で書かれているので、v2で書いてください。
・tflite形式のファイルが読み込めるようにしてください。
・ラベル付は"open","close"です。それらが認識できるようにしてください。
・picameraではなく、opencvのライブラリを使用してカメラ画像を読み取ってください。
・tfliteモデルの指定を、arg[1]ではなく、コード内で記載してください。

＝＝＝
(上記、githubのコードを直貼り)

```

- 以下、出力したソースコード。一発動作。
    - slackのソースコードのみ、自作のスクリプトに一部変更している

```
import cv2
import numpy as np
import tensorflow as tf
import signal
import slacker

# 定数定義
DOOR_UNLOCKED = 0
DOOR_LOCKED = 1
SLACK_TOKEN = 'YOUR_TOKEN_XXXXXXXXXXXXXXXXXXXXXXX'
slack = slacker.Slacker(SLACK_TOKEN)

# tfliteモデルファイルの指定
MODEL_PATH = 'model.tflite'

# tfliteモデルの読み込み
interpreter = tf.lite.Interpreter(model_path=MODEL_PATH)
interpreter.allocate_tensors()

# 入力と出力の詳細を取得
input_details = interpreter.get_input_details()
output_details = interpreter.get_output_details()

# Slackにメッセージを送る関数
def send_message_and_upload(img_data, door_status, score):
    if door_status == DOOR_LOCKED:
        message = f'The door is LOCKED {score}'
    else:
        message = f'The door is UNLOCKED {score}'
    slack.files.upload(img_data, channels=['general'], initial_comment=message)

# ドアの状態を検出する関数
def detect_door(img, interpreter):
    # 画像の前処理
    input_shape = input_details[0]['shape']
    img = cv2.resize(img, (input_shape[1], input_shape[2]))
    img = np.expand_dims(img, axis=0).astype(np.float32)

    interpreter.set_tensor(input_details[0]['index'], img)
    interpreter.invoke()

    # 結果の取得
    scores = interpreter.get_tensor(output_details[2]['index'])
    classes = interpreter.get_tensor(output_details[1]['index'])
    
    if classes[0][0] == 0 and scores[0][0] > 0.5:  # "close"のラベルIDとスコアの閾値
        return (DOOR_LOCKED, 'close', scores[0][0])
    else:
        return (DOOR_UNLOCKED, 'open', scores[0][0])

force_notify = False

# シグナルハンドラ
def sig_handler(signum, frame):
    print('RECEIVE SIGHUP')
    global force_notify
    force_notify = True

signal.signal(signal.SIGHUP, sig_handler)

# メイン関数
def main():
    print('----- loaded ---')

    # OpenCVでカメラ初期化
    cap = cv2.VideoCapture(0)

    if not cap.isOpened():
        print("カメラが開けません")
        exit()

    print('----- camera ready ---')
    prev_status = DOOR_UNLOCKED
    global force_notify

    while True:
        ret, frame = cap.read()
        if not ret:
            print("カメラから画像を取得できません")
            break

        door_status, top_class, top_score = detect_door(frame, interpreter)
        if (prev_status != door_status) or force_notify:
            send_message_and_upload(frame, door_status, top_score)
            print(f'STATUS: {door_status}, Class: {top_class}, Score: {top_score}')

            force_notify = False

        prev_status = door_status

    cap.release()

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

## しかし、暴走
- チャタリングが起きている模様
    - しかし、メッセージが何かを読み取れない
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/90313/0008e292-4a32-8032-8483-a94e5b4dac72.png)


## 測定している範囲がわかるようになりたい
- chatGPTに以下のように問い合わせ。
    - https://chat.openai.com/share/c2852db9-cf1a-44a0-89e2-c8814f07dccc

```
以下のpythonプログラムを書いてください。
・opencvでカメラ画像を読み出し
・読みだした画像に、範囲を指定して矩形を描画
・矩形の左上に文字を描画、文字と文字色は変数で別途指定
・描画した画像を表示
```

- 下記のように実装してくれました。一発動作。
- 最初の画像を生成して保存してくれるようになった

```
import cv2

# カメラの設定
cap = cv2.VideoCapture(0)

# テキストと色の設定
text = "テキスト"
font_color = (255, 255, 255)  # 白色

while True:
    # カメラから画像を読み込み
    ret, frame = cap.read()

    if not ret:
        break

    # 矩形の描画
    top_left_corner = (100, 100)
    bottom_right_corner = (300, 300)
    rectangle_color = (0, 255, 0)  # 緑色
    cv2.rectangle(frame, top_left_corner, bottom_right_corner, rectangle_color, 3)

    # テキストの描画
    font = cv2.FONT_HERSHEY_SIMPLEX
    cv2.putText(frame, text, (top_left_corner[0], top_left_corner[1] - 10), font, 1, font_color, 2, cv2.LINE_AA)

    # 画像の表示
    cv2.imshow('Camera', frame)

    # 'q'キーで終了
    if cv2.waitKey(1) & 0xFF == ord('q'):
        break

# リソースの解放
cap.release()
cv2.destroyAllWindows()
```

## その結果わかったこと

- チャタリングを起こしていた
- 本来想定していた範囲よりも非常に小さな面積で描画していた
    - 微小空間に対しても「ドアの状態が切り替わった」と判定してしまうため、「open」→「close」と状態がどんどん切り替わってしまう
- 画像の輝度が日中・夜に対して大きく変わるため、誤判定が起きやすかった
    - openと誤判定していたときの画像。ほぼ真っ暗で何も見えない
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/90313/4d65c5d2-1af5-1bdd-d1fb-1657b8d816eb.png)


## 最終的な対策
- tensorflowの描画面積が一定以上の量(threshold_area)でないと判定しないようにした
    - カメラの位置、判定したい領域は常に固定であるため
- 日中は撮影した画像の輝度を変更し、2回に分けて判定を行うようにした
    - 元の画像より輝度を上げて判定した
- 上記2つの判定が、複数回連続で実施したときのみ、はじめて状態遷移が認められるようにした

# おわりに、蛇足
## 現在
- 無事動いています。
- 「あれ、鍵閉めたっけ」を、スマホで確認できるようになって、毎日のちょっとした不安がなくなった。
- 「雑に作れる」自信がついた。
- これからももっと作っていきたい

## 実装部分の紆余曲折：色監視
- 最初は、ドアノブに原色のシールを貼って認識できないかを考えていた

https://twitter.com/iori_ama/status/1743182316493762716

- しかし、以下の理由で断念
    - タックシールを認識できるほど、近くにカメラを設置できない
    - 色(RGB,HSV)の判定だとあまりにも誤判定が多かった

## その他のセンサー、測定方法の選定

- リードスイッチ(磁石)
    - サムターンに何かを取り付けることが嫌だった
    - 自宅に猫がおり、いたずらされることを懸念。可能な限り、扉の周りに何かを起きたくなかった。
- 赤外線センサ
    - 「人が通った」という事実は測定できるが、結局鍵の開閉に対して何かを判定できるロジックを構築するのは難しい
- ドアを上からではなく、遠くから撮影する
    - 本当は正面からドアを撮影する画角で検討する予定だったが、webカメラが重く、両面テープで支えきれなかったため断念

## 追記 2/8
- スマートロック買えば済む話だったのでは?
    - 私の目的が「鍵を操作する」ことではなかったので今回は採用しませんでした。
    - 「wi-fiを通じて鍵を操作できる」というのは、個人的にはセキュリティホールのように感じているため、あまり採用したくなかった、というのが実際のところです。
    - 鍵が閉まっているかどうか、わかるだけで良かったのです