BackgroundSubtractorMOG2による微細変化検出(ソースコードと実行結果)

Python開発環境,ライブラリ類

ここでは、最低限の事前準備について説明する。機械学習や深層学習を行う場合は、NVIDIA CUDA、Visual Studio、Cursorなどを追加でインストールすると便利である。これらについては別ページ https://www.kkaneko.jp/cc/dev/aiassist.htmlで詳しく解説しているので、必要に応じて参照してください。

Python 3.12 のインストール

インストール済みの場合は実行不要。

管理者権限でコマンドプロンプトを起動(手順:Windowsキーまたはスタートメニュー > cmd と入力 > 右クリック > 「管理者として実行」)し、以下を実行する。管理者権限は、wingetの--scope machineオプションでシステム全体にソフトウェアをインストールするために必要である。

REM Python をシステム領域にインストール
winget install --scope machine --id Python.Python.3.12 -e --silent
REM Python のパス設定
set "PYTHON_PATH=C:\Program Files\Python312"
set "PYTHON_SCRIPTS_PATH=C:\Program Files\Python312\Scripts"
echo "%PATH%" | find /i "%PYTHON_PATH%" >nul
if errorlevel 1 setx PATH "%PATH%;%PYTHON_PATH%" /M >nul
echo "%PATH%" | find /i "%PYTHON_SCRIPTS_PATH%" >nul
if errorlevel 1 setx PATH "%PATH%;%PYTHON_SCRIPTS_PATH%" /M >nul

関連する外部ページ

Python の公式ページ: https://www.python.org/

AI エディタ Windsurf のインストール

Pythonプログラムの編集・実行には、AI エディタの利用を推奨する。ここでは,Windsurfのインストールを説明する。

管理者権限でコマンドプロンプトを起動(手順:Windowsキーまたはスタートメニュー > cmd と入力 > 右クリック > 「管理者として実行」)し、以下を実行して、Windsurfをシステム全体にインストールする。管理者権限は、wingetの--scope machineオプションでシステム全体にソフトウェアをインストールするために必要となる。

winget install --scope machine Codeium.Windsurf -e --silent

関連する外部ページ

Windsurf の公式ページ: https://windsurf.com/

必要なライブラリのインストール

コマンドプロンプトを管理者として実行(手順:Windowsキーまたはスタートメニュー > cmd と入力 > 右クリック > 「管理者として実行」)し、以下を実行する


pip install opencv-python numpy pillow

BackgroundSubtractorMOG2による微細変化検出プログラム

主要技術

ソースコード


# プログラム名: BackgroundSubtractorMOG2による動画変化検出プログラム
# 特徴技術名: OpenCV BackgroundSubtractorMOG2(混合ガウス分布モデル)
# 出典: Zivkovic, Z. (2004), Zivkovic & van der Heijden (2006), Stauffer & Grimson (1999)
# 概要: 動画からの変化領域を検出し、赤色オーバーレイとバウンディングボックスで可視化する
# 学習済みモデル: 使用なし
# 方式設計:
#   - 利用技術:
#     - OpenCV: BackgroundSubtractorMOG2、形態学的処理、輪郭検出
#     - NumPy: 行列演算
#     - tkinter: 感度調整用インターフェース
#     - Pillow: 日本語テキスト表示
#   - 入出力:
#     - 入力: 0=動画ファイル(tkinterで選択)、1=カメラ(DirectShow)、2=サンプル動画(URLからダウンロード)
#     - 出力: OpenCVウィンドウにリアルタイム表示、各フレームごとに変化率と検出領域情報を標準出力、終了時にresult.txtへ保存
#   - 処理手順:
#     1. ガウシアンフィルタによる前処理
#     2. MOG2による前景マスク生成(detectShadowsは無効)
#     3. 形態学的オープニングと最小面積フィルタ
#     4. 変化領域の重畳表示とバウンディングボックス描画
#   - 調整パラメータ:
#     - sensitivity: 0.5–6.0(値が大きいほど検出感度が上がる)。varThresholdに反映
#     - min_change_area: 最小変化領域の面積(ピクセル単位)。デフォルト50
# 備考:
#   - detectShadowsは無効化(影を別値で扱わない設定)
#   - CAP_PROP_BUFFERSIZEの効果はバックエンド実装に依存する
#   - OpenCV 4.xを前提とする(findContoursの戻り値が2要素である前提)
# 前準備:
#   - pip install opencv-python numpy pillow

import cv2
import tkinter as tk
from tkinter import filedialog, Scale, Label, Button
import numpy as np
import time
import urllib.request
from datetime import datetime
from PIL import Image, ImageDraw, ImageFont
import threading
import os

# 設定値(調整可能なパラメータ)
HISTORY = 500              # 背景モデルの履歴フレーム数
VAR_THRESHOLD_BASE = 16    # 基本ピクセルマッチング閾値(MOG2のvarThreshold算出に利用)
DETECT_SHADOWS = False     # 影検出の有効/無効(本プログラムでは無効)
SENSITIVITY = 1.5          # 変化検出の感度(0.5–6.0、値が大きいほど検出感度が上がる)
MIN_CHANGE_AREA = 50       # 最小変化領域(ピクセル単位)
GAUSSIAN_KERNEL = 3        # ガウシアンフィルタのカーネルサイズ(奇数)
MORPH_KERNEL = 3           # 形態学的処理のカーネルサイズ(奇数)

# フォント設定
FONT_PATH = 'C:/Windows/Fonts/meiryo.ttc'
FONT_SIZE = 20

# グローバル変数
frame_count = 0
results_log = []
mog2 = None
sensitivity = SENSITIVITY
min_area = MIN_CHANGE_AREA
gui_active = False
sensitivity_updated = False

def update_sensitivity(value):
    """GUIスライダからの感度変更"""
    global sensitivity, mog2, sensitivity_updated
    sensitivity = float(value)
    if mog2 is not None:
        var_threshold = VAR_THRESHOLD_BASE / sensitivity
        mog2.setVarThreshold(var_threshold)
        sensitivity_updated = True

def start_gui():
    """感度調整用GUIを別スレッドで起動"""
    global gui_active

    def gui_thread():
        global gui_active
        root = tk.Tk()
        root.title('感度調整')
        root.geometry('300x150')

        label = Label(root, text='Sensitivity (感度)')
        label.pack(pady=10)

        scale = Scale(root, from_=0.5, to=6.0, resolution=0.1,
                     orient='horizontal', length=250, command=update_sensitivity)
        scale.set(sensitivity)
        scale.pack(pady=10)

        value_label = Label(root, text=f'現在の値: {sensitivity:.1f}')
        value_label.pack()

        def update_label():
            value_label.config(text=f'現在の値: {sensitivity:.1f}')
            root.after(100, update_label)

        update_label()

        def on_closing():
            global gui_active
            gui_active = False
            root.destroy()

        root.protocol("WM_DELETE_WINDOW", on_closing)
        gui_active = True
        root.mainloop()

    thread = threading.Thread(target=gui_thread, daemon=True)
    thread.start()
    time.sleep(0.5)  # GUI起動待機

def video_frame_processing(frame):
    """1フレーム分の処理"""
    global frame_count, mog2, sensitivity_updated
    current_time = time.time()
    frame_count += 1

    # 前処理: ノイズ低減
    blurred = cv2.GaussianBlur(frame, (GAUSSIAN_KERNEL, GAUSSIAN_KERNEL), 0)

    # 背景差分器の初期化(最初のフレームで1回)
    if mog2 is None:
        var_threshold = VAR_THRESHOLD_BASE / sensitivity
        mog2 = cv2.createBackgroundSubtractorMOG2(
            history=HISTORY,
            varThreshold=var_threshold,
            detectShadows=DETECT_SHADOWS
        )
        print(f'MOG2初期化: history={HISTORY}, varThreshold={var_threshold:.2f}, detectShadows={DETECT_SHADOWS}')

    # 感度更新の通知
    if sensitivity_updated:
        var_threshold = VAR_THRESHOLD_BASE / sensitivity
        print(f'感度更新: {sensitivity:.1f} (varThreshold={var_threshold:.2f})')
        sensitivity_updated = False

    # 前景マスク生成
    fgmask = mog2.apply(blurred)

    # 後処理: 形態学的オープニングによるノイズ除去
    kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (MORPH_KERNEL, MORPH_KERNEL))
    cleaned = cv2.morphologyEx(fgmask, cv2.MORPH_OPEN, kernel)

    # 小領域除去とバウンディングボックス抽出
    contours, _ = cv2.findContours(cleaned, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
    filtered_mask = np.zeros(cleaned.shape, dtype=np.uint8)
    bounding_boxes = []

    for contour in contours:
        area = cv2.contourArea(contour)
        if area >= min_area:
            cv2.drawContours(filtered_mask, [contour], -1, 255, -1)
            x, y, w, h = cv2.boundingRect(contour)
            bounding_boxes.append((x, y, w, h, area))

    # 変化率の算出
    total_pixels = frame.shape[0] * frame.shape[1]
    change_pixels_filtered = cv2.countNonZero(filtered_mask)
    change_rate = (change_pixels_filtered / total_pixels) * 100

    # 可視化: 変化領域を赤チャネルに重畳
    output = frame.copy()
    overlay = np.zeros_like(frame)
    overlay[:, :, 2] = filtered_mask
    output = cv2.addWeighted(output, 0.7, overlay, 0.3, 0)

    # バウンディングボックス描画
    for x, y, w, h, area in bounding_boxes:
        cv2.rectangle(output, (x, y), (x + w, y + h), (0, 255, 0), 2)

    # 画面表示テキスト(日本語対応)
    font = ImageFont.truetype(FONT_PATH, FONT_SIZE)
    img_pil = Image.fromarray(cv2.cvtColor(output, cv2.COLOR_BGR2RGB))
    draw = ImageDraw.Draw(img_pil)

    draw.text((10, 30), f'変化率: {change_rate:.2f}%', font=font, fill=(0, 255, 0))
    draw.text((10, 60), f'感度: {sensitivity:.1f}', font=font, fill=(0, 255, 0))
    draw.text((10, 90), f'検出領域数: {len(bounding_boxes)}', font=font, fill=(0, 255, 0))

    output = cv2.cvtColor(np.array(img_pil), cv2.COLOR_RGB2BGR)

    # 結果文字列の生成
    result = f'変化率: {change_rate:.2f}%, 検出領域数: {len(bounding_boxes)}'
    if bounding_boxes:
        areas_str = ', '.join([f'{area:.0f}px' for _, _, _, _, area in bounding_boxes[:3]])
        result += f', 領域面積: {areas_str}'
        if len(bounding_boxes) > 3:
            result += f' 他{len(bounding_boxes)-3}領域'

    return output, result, current_time

# 起動時の案内表示
print('========================================')
print('BackgroundSubtractorMOG2による変化検出システム')
print('========================================')
print('')
print('【概要説明】')
print('動画内の動きや変化を検出し、リアルタイムで可視化します。')
print('混合ガウス分布モデルを用いて背景を学習し、前景を抽出します。')
print('')
print('【操作方法】')
print('・qキー: プログラム終了')
print('・感度調整: 別ウィンドウのスライダーで0.5〜6.0の範囲で調整')
print('')
print('【注意事項】')
print('・初期フレームは背景学習に使用されます')
print('・照明変化に対してロバストですが、急激な変化には時間がかかります')
print('')
print('========================================')

print("0: 動画ファイル")
print("1: カメラ")
print("2: サンプル動画")

choice = input("選択: ")

# 感度調整GUIの起動
print('\n感度調整ウィンドウを起動中...')
start_gui()

if choice == '0':
    root = tk.Tk()
    root.withdraw()
    path = filedialog.askopenfilename()
    if not path:
        exit()
    cap = cv2.VideoCapture(path)
elif choice == '1':
    cap = cv2.VideoCapture(0, cv2.CAP_DSHOW)
    if not cap.isOpened():
        cap = cv2.VideoCapture(0)
    cap.set(cv2.CAP_PROP_BUFFERSIZE, 1)
else:
    # サンプル動画ダウンロード・処理
    SAMPLE_URL = 'https://raw.githubusercontent.com/opencv/opencv/master/samples/data/vtest.avi'
    SAMPLE_FILE = 'vtest.avi'
    urllib.request.urlretrieve(SAMPLE_URL, SAMPLE_FILE)
    cap = cv2.VideoCapture(SAMPLE_FILE)

if not cap.isOpened():
    print('動画ファイル・カメラを開けませんでした')
    exit()

# メイン処理
print('\n=== 動画処理開始 ===')
print('操作方法:')
print('  q キー: プログラム終了')
print('  感度調整: 別ウィンドウのスライダーを使用')

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

        MAIN_FUNC_DESC = "変化検出 (MOG2)"
        processed_frame, result, current_time = video_frame_processing(frame)
        cv2.imshow(MAIN_FUNC_DESC, processed_frame)

        if choice == '1':  # カメラの場合
            print(datetime.fromtimestamp(current_time).strftime("%Y-%m-%d %H:%M:%S.%f")[:-3], result)
        else:  # 動画ファイルの場合
            print(frame_count, result)

        results_log.append(result)

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

finally:
    print('\n=== プログラム終了 ===')
    cap.release()
    cv2.destroyAllWindows()

    # サンプルファイルの削除
    if choice == '2' and os.path.exists('vtest.avi'):
        os.remove('vtest.avi')

    if results_log:
        with open('result.txt', 'w', encoding='utf-8') as f:
            f.write('=== 結果 ===\n')
            f.write(f'処理フレーム数: {frame_count}\n')
            f.write('\n')
            f.write('\n'.join(results_log))
        print(f'\n処理結果をresult.txtに保存しました')