【CV2】OpenCVで学ぶモーション検出と追跡：フレームの差分、MOG2、オプティカルフローの解説

# OpenCVで学ぶモーション検出と追跡：フレームの差分、MOG2、オプティカルフローの解説

前回のブログ記事では、OpenCVの基本的な画像処理について説明しました：

- [【CV2】: HSV vs RGB: イメージ処理におけるHSVの理解と活用](https://qiita.com/JarvisSan22/items/ded6a766d3ace781bed4)
-  [【CV2】： 画像とは何か、画像の明るさとコントラストを調整しよう](https://qiita.com/JarvisSan22/items/c86b28071dbc0343287a)

OpenCVは画像処理だけでなく、動画のフレーム処理も得意としています。`cap = cv2.VideoCapture(video_file)`を使えば、動画を1フレームずつ分析できます。

今回は、渋谷スクランブル交差点のパブリックライブストリームから抽出した動画を使って、動き検出と追跡の方法を紹介します。同じ動画を使って試したい方は、こちらからダウンロードできます：[[動画ダウンロードリンク](https://drive.google.com/file/d/1RPd4s_TicjVZ0hRyH24YBDtys_zNZ8Gk/view?usp=sharing)]

## 動画の準備と表示
まずは動画を読み込んで、メタデータを取得しましょう。フレームサイズ、総フレーム数、FPS（1秒あたりのフレーム数）は、正確な処理と保存に重要な情報です。
```python
import cv2 

video_file = "./shibuy_test_20240705_063826.avi"
cap = cv2.VideoCapture(video_file)

# フレームのプロパティを取得
frame_count = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
frame_width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
frame_height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
fps = cap.get(cv2.CAP_PROP_FPS)

print(f"総フレーム数: {frame_count}, 幅: {frame_width}, 高さ: {frame_height}, FPS: {fps}")

```

通常、動画は`cap`変数として設定し、`cv2.CAP_PROP_*`でメタデータを取得します。

## 動画の表示

動画を表示するには、フレームを順番に読み込んで`cv2.imshow`で表示するwhileループを使います。スムーズに終了できるように、必ず終了オプションを含めましょう。

```python
while cap.isOpened():
    ret, frame = cap.read()
    if ret:
        cv2.imshow('動画再生', frame)
        # "q"キーで終了
        if cv2.waitKey(1) & 0xFF == ord('q'):
            break
    else:
        break

cap.release()
cv2.destroyAllWindows()
```

[![IMAGE Shibuya Crossing video](http://img.youtube.com/vi/SrGE54LJEDY/0.jpg)](http://www.youtube.com/watch?v=SrGE54LJEDY)

# 動きの検出方法
## 方法1：フレーム差分法

一番シンプルな方法は、連続する2つのフレームを比較することです。`cv2.absdiff()`という関数を使うと、2つのフレームの違いを見つけることができます。

```python
frame_1 = cap.read()[1]
cap.set(cv2.CAP_PROP_POS_FRAMES, 5)  # 5フレーム目にスキップ
frame_2 = cap.read()[1]

delta_frame = cv2.absdiff(frame_1, frame_2)
cv2.imshow('差分フレーム', delta_frame)
cv2.waitKey(0)
cv2.destroyAllWindows()
```

この方法を動画全体に適用するには：
```python
prev_frame = cap.read()[1]
while cap.isOpened():
    ret, frame = cap.read()
    if ret:
        delta_frame = cv2.absdiff(prev_frame, frame)
        cv2.imshow('差分フレーム', delta_frame)
        prev_frame = frame
        if cv2.waitKey(1) & 0xFF == ord('q'):
            break
    else:
        break
```

処理した動画を保存するには、`cv2.VideoWriter()`を使います。

```python
rec = cv2.VideoWriter('./output_delta.mp4',cv2.VideoWriter_fourcc(*'mp4v'),fps,(frame_width,frame_height),True)

prev_frame=cap.read()[1]
while(cap.isOpened()):
    ret, frame = cap.read()
    if ret == True:
        delta_frame = cv2.absdiff(prev_frame,frame)
        cv2.imshow('差分フレーム', delta_frame)
        prev_frame=frame
        rec.write(delta_frame)
        if cv2.waitKey(1) & 0xFF == ord('q'):
            break
    else:
        break

cap.release()
cv2.destroyAllWindows()
rec.release()
```

[![IMAGE Shibuya Crossing delta](http://img.youtube.com/vi/WxYwRp9AcD0/0.jpg)](http://www.youtube.com/watch?v=WxYwRp9AcD0)

![Delta Frame_screenshot_01.12.2024.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/999291/7a07b8e1-dac0-f487-4a64-a4990727977b.png)


fps、frame_width、frame_heightは、最終的なビデオのサイズに問題が生じないように、`cap`の値から使用するのが良い方法です。もう一つのオプションは、出力フォーマットを変更することだ。現在、`cv2.VideoWriter_fourcc(*'mp4v')`によってmp4に設定されていますが、`mov`や`avi`のような他のフォーマットも使用することができます。例えば1チャンネルのグレー画像を持っている場合、これは`False`に設定する必要がある。そうしないと正しく保存されず、最後には何もない 「1kb 」のファイルができてしまう（これはよくあることだ）。

## 方法2：背景差分法（MOG2）

もう少し賢い方法として、`cv2.createBackgroundSubtractorMOG2`を使う方法があります。これは、動かないものと動くものを自動的に区別してくれます！

```python 
fgbg = cv2.createBackgroundSubtractorMOG2(history=500, varThreshold=16, detectShadows=True)

while cap.isOpened():
    ret, frame = cap.read()
    if ret:
        fgmask = fgbg.apply(frame)
        cv2.imshow('背景差分', fgmask)
        if cv2.waitKey(1) & 0xFF == ord('q'):
            break
    else:
        break
```

[![IMAGE ALT TEXT HERE](http://img.youtube.com/vi/4RBn2IWbsOM/0.jpg)](http://www.youtube.com/watch?v=4RBn2IWbsOM)


MOG2のパラメータ説明：

- history: 背景モデルの構築に使用するフレーム数（デフォルト：500）
- varThreshold: ピクセルが背景かどうかを判断する閾値（デフォルト：16）
- detectShadows: 影を検出するかどうか（デフォルト：True）

この方法を使うと、動きがより明るくなり、人体が強調され、横断歩道を歩いて渡る人の違いも見える。傘を差している人のように。しかし、このバージョンでは、フレームごとに背景のアーチファクトがあり、ノイズが多くなっています。

影はfgbgオブジェクトで検出されるため、白一色のオブジェクトとは別の値が設定される。
`fgmask[fgmask != 255] = 0`
ノイズを減らすためのHSV処理：
```python
def process_frame(frame):
    frame = cv2.cvtColor(frame, cv2.COLOR_BGR2HSV)[:,:,2]
    frame = cv2.GaussianBlur(frame, (5, 5), 1)
    return frame
```



[![IMAGE ALT TEXT HERE](http://img.youtube.com/vi/MpCgIG1yrk0/0.jpg)](http://www.youtube.com/watch?v=MpCgIG1yrk0)


## 方法3：オプティカルフローによる動き追跡

これまでのセクションでは、フレーム間のピクセル値の変化に基づいた動きの検出を行いました。しかし、動きの検出だけでは、個々の物体がどのように移動しているかの詳細な情報は得られません。OpenCV の `cv2.calcOpticalFlowPyrLK` は、特定のポイントをビデオフレーム間で追跡するための強力なツールです。この関数は、光学フロー推定のための[ Lucas-Kanade法](https://labs.eecs.tottori-u.ac.jp/sd/Member/oyamada/OpenCV/html/py_tutorials/py_video/py_lucas_kanade/py_lucas_kanade.html) を使用しており、渋谷交差点のような賑やかなシーンで動的な物体を追跡するのに非常に効果的です。

```python
import cv2
import numpy as np

# Shi-Tomasi コーナー検出用のパラメータ
feature_params = dict(maxCorners=100, qualityLevel=0.3, minDistance=7, blockSize=7)

# Lucas-Kanade 光学フローのパラメータ
lk_params = dict(winSize=(15, 15), maxLevel=2, criteria=(cv2.TERM_CRITERIA_EPS | cv2.TERM_CRITERIA_COUNT, 10, 0.03))

# 動画キャプチャと出力設定
cap = cv2.VideoCapture("shibuya_traffic.mp4")
fourcc = cv2.VideoWriter_fourcc(*'mp4v')
rec = cv2.VideoWriter("output_tracking.mp4", fourcc, 30.0, (int(cap.get(3)), int(cap.get(4))))

# 最初のフレームを読み込んで追跡ポイントを初期化
ret, old_frame = cap.read()
old_gray = cv2.cvtColor(old_frame, cv2.COLOR_BGR2GRAY)
p0 = cv2.goodFeaturesToTrack(old_gray, mask=None, **feature_params)

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

    # 現在のフレームをグレースケールに変換
    frame_gray = cv2.cvtColor(frame.copy(), cv2.COLOR_BGR2GRAY)

    # 光学フローを計算
    p1, st, err = cv2.calcOpticalFlowPyrLK(old_gray, frame_gray, p0, None, **lk_params)

    # 追跡に成功したポイントを選択
    good_new = p1[st == 1]
    good_old = p0[st == 1]

    # 追跡ポイントを描画
    for i, (new, old) in enumerate(zip(good_new, good_old)):
        a, b = map(int, new.ravel())
        c, d = map(int, old.ravel())
        cv2.circle(frame, (a, b), 5, (0, 0, 255), -1)
        cv2.line(frame, (a, b), (c, d), (0, 255, 0), 2)

    # 前のフレームとポイントを更新
    old_gray = frame_gray.copy()
    p0 = good_new.reshape(-1, 1, 2)

    # フレームを書き込み表示
    rec.write(frame)
    cv2.imshow("Tracked Motion", frame)

    if cv2.waitKey(1) & 0xFF == ord('q'):
        break

cap.release()
rec.release()
cv2.destroyAllWindows()


```

[![IMAGE ALT TEXT HERE](http://img.youtube.com/vi/uIElCwj5hTM/0.jpg)](http://www.youtube.com/watch?v=uIElCwj5hTM)

追跡ポイントの設定
追跡を始める前に、最初のキーポイントを設定する必要があります。これは cv2.goodFeaturesToTrack 関数を使用して行います。
```python
p0 = cv2.goodFeaturesToTrack(old_gray, mask=None, **feature_params)
```
`feature_params` のパラメータ:
- `maxCorners`: 検出する最大のコーナー数（キーポイント数）。値を増やすと多くのポイントを追跡できますが、計算コストが上がります。
- `qualityLevel`: 検出するコーナーの最低品質（0～1）。低い値は弱いコーナーも検出しますが、追跡の信頼性が低下する場合があります。
- `minDistance`: 検出されたポイント間の最小ユークリッド距離。小さい値はポイントの密度を増やしますが、計算が複雑になります。
- `blockSize`: コーナー検出に使用される近傍領域のサイズ。小さい値は細かいディテールを検出しますが、ノイズの影響を受けやすくなります。


Lucas-Kanade 光学フローのパラメータ
```python
lk_params = dict(winSize=(15, 15), maxLevel=2, criteria=(cv2.TERM_CRITERIA_EPS | cv2.TERM_CRITERIA_COUNT, 10, 0.03))
```
- winSize: 各ピラミッドレベルの探索ウィンドウのサイズを定義します。小さいウィンドウは精度が高まりますが、大きな動きを見逃す可能性があります。
- maxLevel: 光学フロー推定に使用するピラミッドレベルの数を設定します。レベルを増やすと大きな動きを追跡できますが、計算コストが上がります。
- criteria: 反復探索アルゴリズムの終了条件:
  - - EPS: 探索で十分小さい変化が得られたら停止。
  - -  COUNT: 一定の反復回数で停止。

例えば、渋谷交差点のような複雑なシーンでは、パラメータを調整することで追跡結果を向上できます。例えば、コーナー数を増やし、探索ウィンドウサイズを小さくすることで、動きの精度を高めることができます。

```python
# 最適化されたパラメータ
feature_params = dict(maxCorners=500, qualityLevel=0.3, minDistance=7, blockSize=7)
lk_params = dict(winSize=(7, 7), maxLevel=5, criteria=(cv2.TERM_CRITERIA_EPS | cv2.TERM_CRITERIA_COUNT, 10, 0.03))

```
これらの設定を用いることで、より詳細で正確な動きの追跡が可能になります。

[![IMAGE ALT TEXT HERE](http://img.youtube.com/vi/bOrTlF64ZfE/0.jpg)](http://www.youtube.com/watch?v=bOrTlF64ZfE)



# 結論

この記事では、OpenCV を使った 3 つの強力な動きの検出および追跡方法について解説しました。それらは、「フレームデルタ法」「背景差分法」「`cv2.calcOpticalFlowPyrLK` を使用した光学フロー法」です。それぞれの方法には強みと理想的な使用ケースがあり、一般的な動きの検出から特定の物体の正確な追跡まで対応できます。

フレームデルタ法は、連続するフレームを比較するためのシンプルなアプローチを提供します。一方、背景差分法は、静的な背景から動的な前景オブジェクトを分離するための強力な方法です。最後に、光学フロー法は、フレームごとに個々のポイントを追跡することで、動きを詳細に追跡することを可能にします。これらの技術を理解し、実装することで、複雑なシーン（例えば渋谷交差点のような賑やかな場所）においても、動きを効果的に分析し、可視化することができます。

# 宿題
フレームデルタ法と背景差分法を光学フロー法と組み合わせてみましょう。以下の課題に挑戦してみてください。

課題の目的
- フレームデルタ法と MOG2（背景差分法）の強みを組み合わせて、フレームを事前処理し、その後光学フローを用いて追跡精度を向上させる。

完成したコードや出力動画のリンクを私に送ったり、コメントで共有してください。これらの方法をどのように組み合わせたか、そしてプロセスを通じて得られた洞察をぜひ教えてください！