Raspberryのカメラモジュールで動画から動体検知

Raspberry PI と言えば、色々な電子工作　という事で、前回の記事　↓

https://qiita.com/MasahiroShouya/items/bda32efd0c6460b6b307

において、カメラモジュールを使って静止画を扱ってます。
今回は動画を取得して、前回と似たような事を試してみます。

**Step1：前回同様、カメラで静止画（JPG）を保存**
カメラモジュールの接続確認や動作確認の為、以下の手順から静止画の取得で、問題が無い事を確認していきましょう。
sudo apt-get update
sudo apt-get upgrade
sudo apt-get install libcap-dev
sudo apt-get install libcamera-dev
sudo apt-get install python3-picamera2
sudo mkdir /test
sudo mkdir /test/camera
sudo chmod 777 -R /test
cd /test/camera

準備が出来たらプログラムを作りましょう。
vi createjpg.py

```diff_python:createjpg.py
import picamera2
import time

# カメラの設定
picam2 = picamera2.Picamera2()
picam2.start_preview()
config = picam2.create_preview_configuration(main={"size": (1920, 1080)})
picam2.configure(config)
picam2.start()

# 写真を撮影して保存
time.sleep(2)  # プレビュー開始後、2秒待つ
picam2.capture_file("image.jpg")  # 画像を image.jpg として保存

# カメラを停止
picam2.stop()
```

プログラムが出来たら実行してみましょう。
python createjpg.py

実行後、撮影したJPGファイルを確認しましょう。
ls -l　（image.jpg の存在を確認しましょう）
カメラ撮影により、JPGファイルが作成されたのが解ります。
ここまで正常であれば、カメラの調子や接続等に、問題が無い事が確認されます。


**Step2：動画を撮影しましょう**
必要なモジュールをインストールします

sudo apt-get install python3-opencv
sudo apt-get install ffmpeg

準備が出来たらプログラムを作りましょう。
vi createmv.py

```diff_python:createmv.py
import picamera2
import cv2
import time
import subprocess
import numpy as np

# カメラの設定
picam2 = picamera2.Picamera2()
config = picam2.create_preview_configuration(main={"size": (1920, 1080)})
picam2.configure(config)
picam2.start()

# 動画の保存先とファイル名
video_path = "output.mp4"

# FFmpegコマンドの作成
command = [
    'ffmpeg',
    '-y',
    '-f', 'rawvideo',
    '-vcodec','rawvideo',
    '-s', '1920x1080',
    '-pix_fmt', 'bgr24',
    '-r', '20',
    '-i', '-',
    '-an',
    '-vcodec', 'h264_v4l2m2m', # ハードウェアエンコーダを使用
    '-b:v', '2M', # ビットレートを設定（必要に応じて調整）
    video_path
]

# FFmpegプロセスを開始
process = subprocess.Popen(command, stdin=subprocess.PIPE)

# 5秒間動画をキャプチャ
start_time = time.time()
while time.time() - start_time < 5:
    frame = picam2.capture_array()
    # BGRに変換
    frame = cv2.cvtColor(frame, cv2.COLOR_RGB2BGR) # RGBからBGRへ変換
    process.stdin.write(frame.tobytes())

# 後処理
process.stdin.close()
process.wait()
picam2.stop()
```
※「ハードウェアエンコーダ」の部分は、RasPI４以降の推奨値です。RasPI３以前の推奨値は、「h264_omx」この辺りでしょうか？

プログラムが出来たら実行してみましょう。
python createmv.py

撮影したmp4ファイルを確認しましょう。
ls -l　（output.mp4 の存在を確認しましょう）
カメラ撮影により、mp4ファイルが作成されたのが解ります。


**Step3：動画に動体があるか検知します**
必要なプログラムを用意しましょう。
vi chkmv.py

```diff_python:chkmv.py
import cv2
import time

def analyze_motion(video_path):

    cap = cv2.VideoCapture(video_path)
    if not cap.isOpened():
        print("Error: Could not open video.")
        return

    # 背景差分法の準備
    fgbg = cv2.createBackgroundSubtractorMOG2()

    # 各物体の情報を格納するリスト
    objects = []

    frame_rate = cap.get(cv2.CAP_PROP_FPS)
    frame_width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
    frame_height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))

    while True:
        ret, frame = cap.read()
        if not ret:
            break

        # 前処理：グレースケール変換、ぼかし
        gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
        gray = cv2.GaussianBlur(gray, (5, 5), 0)

        # 背景差分、二値化、輪郭抽出
        fgmask = fgbg.apply(gray)
        _, thresh = cv2.threshold(fgmask, 200, 255, cv2.THRESH_BINARY)
        contours, _ = cv2.findContours(thresh, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)

        # 各輪郭に対して処理
        for contour in contours:
            # 小さすぎる輪郭は無視
            if cv2.contourArea(contour) < 500: # 500は閾値。必要に応じて調整
                continue

            x, y, w, h = cv2.boundingRect(contour)
            area = w * h

            # 新しい物体か既存の物体か判定
            matched = False
            for obj in objects:
                if abs(x - obj['x']) < 50 and abs(y - obj['y']) < 50: # 50は閾値。必要に応じて調整
                    obj['end_time'] = cap.get(cv2.CAP_PROP_POS_MSEC) / 1000 # 経過時間を更新
                    obj['max_area'] = max(obj['max_area'], area)
                    matched = True
                    break

            # 新しい物体の場合
            if not matched:
                objects.append({
                    'x': x,
                    'y': y,
                    'start_time': cap.get(cv2.CAP_PROP_POS_MSEC) / 1000,
                    'end_time': cap.get(cv2.CAP_PROP_POS_MSEC) / 1000,
                    'max_area': area
                })
    cap.release()

    # 結果の出力
    for i, obj in enumerate(objects):
        duration = abs(obj['end_time'] - obj['start_time'])
        if duration == 0:
            continue
        print(f"Object {i+1}: Max Area = {obj['max_area']} pixels, Duration = {duration:.2f} seconds")

if __name__ == "__main__":
    video_path = "output.mp4" # 動画ファイルのパスを指定
    analyze_motion(video_path)
```
※「cv2.contourArea(contour) < 500:」 輪郭の面積の閾値です。小さすぎるノイズを除去するために使用します。
※「abs(x - obj['x']) < 50 and abs(y - obj['y']) < 50:」 物体が同一か否かを判断する閾値です。物体の移動速度に合わせて調整します。

プログラムが出来たら実行してみましょう。
python chkmv.py
※ 動作の検知された一覧が表示されたら完了です。
（動画に動体が検知されない場合は、何も表示されませんよ～）
以上で終了です。　お疲れさまでした～