統計的背景モデルによる微細変化検出

【概要】動画やウェブカメラの映像から微細な変化を検出するプログラムである。適応的混合ガウス背景モデル(MOG)とCLAHE照明正規化を用いて、人の通過による明るさ変化や影の変化など、通常の動体検出では見落とされる小さな変化を捉える。感度調整機能やリアルタイム表示機能を備える。

【目次】

  1. プログラム利用ガイド
  2. Python開発環境、ライブラリ類
  3. プログラムコードの説明
  4. 実験・研究スキルの基礎:Windowsで学ぶ微細変化検出実験

プログラム利用ガイド

1. このプログラムの利用シーン

動画やウェブカメラの映像から、微細な変化を検出するためのソフトウェアである。人の通過による壁の明るさ変化、物体の移動や出現、影の変化など、通常の動体検出では見落とされるような小さな変化を捉えることができる。監視カメラの映像解析、実験環境での変化の記録、侵入検知システムなどに活用できる。

2. 主な機能

3. 基本的な使い方

  1. 起動と入力の選択

    プログラムを起動すると、入力ソースの選択画面が表示される。キーボードで0(動画ファイル)、1(ウェブカメラ)、2(サンプル動画)のいずれかを入力し、Enterキーを押す。0を選択した場合は、ファイル選択ダイアログが表示される。

  2. 初期化の待機

    最初の30フレームで通常の状態を学習する。この間、画面に「初期化中... n/30」と表示される。初期化が完了すると、変化検出が開始される。

  3. 感度の調整

    「感度調整」ウィンドウのスライダーをマウスで動かし、検出感度を調整する。値を小さくすると微細な変化まで検出し、大きくすると明確な変化のみを検出する。

  4. 終了方法

    映像が表示されている画面を選択した状態で、キーボードのqキーを押す。または「感度調整」ウィンドウの「終了」ボタンをクリックする。初期化完了後に変化検出が行われた場合、処理結果がresult.txtファイルに保存される。

4. 便利な機能

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 --accept-source-agreements --accept-package-agreements
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 --id Codeium.Windsurf -e --silent --accept-source-agreements --accept-package-agreements

関連する外部ページ

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

必要なライブラリをシステム領域にインストール

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


pip install opencv-python numpy pillow

プログラムコードの説明

1. 概要

このプログラムは、動画フレームから微細な変化領域を検出する。各ピクセルを混合ガウスモデルで表現し、統計的背景モデルを継続的に更新することで、照明変化や影などの微細な明るさ変化を検出する。

2. 主要技術

適応的混合ガウス背景モデル(Mixture of Gaussians, MOG)

StaufferとGrimsonが1999年に提案した背景モデリング手法である[1]。この手法では、各ピクセルをK個のガウス分布の混合として表現し、オンライン近似による更新を行う。各ガウス分布の重みωと標準偏差σの比を計算し、この比が大きい成分ほど安定した背景である可能性が高いと判断される。新しいフレームの各ピクセルについて、統計的な閾値判定により背景か前景かを分類する。

適応的ヒストグラム均等化(CLAHE)

Zuiderveldが1994年に開発した照明正規化手法である[2]。画像を矩形領域(タイル)に分割し、各領域でヒストグラム均等化を実行する。ヒストグラムのクリップ限界を設定することでノイズの過度な増幅を防ぎ、タイル間の境界は双線形補間で滑らかに接続する。

3. 技術的特徴

4. 実装の特色

このプログラムは動画のリアルタイム処理に対応し、以下の特徴を持つ。

入力と初期化

検出処理と後処理

インターフェースと出力

5. 参考文献

[1] Stauffer, C., & Grimson, W. E. L. (1999). Adaptive background mixture models for real-time tracking. Proceedings. 1999 IEEE Computer Society Conference on Computer Vision and Pattern Recognition, 2, 246-252. https://ieeexplore.ieee.org/document/784637

[2] Zuiderveld, K. (1994). Contrast limited adaptive histogram equalization. In P. S. Heckbert (Ed.), Graphics Gems IV (pp. 474-485). Academic Press Professional.

ソースコード


# プログラム名: 統計的背景モデル(MOG)による微細変化検出プログラム
# 特徴技術名: 適応的統計背景モデル(混合ガウス:MOG, Stauffer & Grimson 1999)
# 出典: Stauffer, C., & Grimson, W. E. L. (1999). Adaptive background mixture models for real-time tracking. CVPR 1999
# 特徴機能: 各ピクセルをK成分の混合ガウスでモデル化し、ω/σに基づく背景集合とkσマッチ判定で照明変化に頑健な微細変化検出を実現
# 学習済みモデル: 使用なし
# 方式設計:
#   - 関連利用技術:
#     - OpenCV: 画像処理ライブラリ(形態学的処理、輪郭検出、色空間変換、CLAHE)
#     - NumPy: 数値計算ライブラリ(統計処理、行列演算)
#     - tkinter: GUIライブラリ(感度調整インターフェース)
#     - CLAHE: 局所的適応的ヒストグラム均等化による照明正規化
#     - 統計的背景モデル: 混合ガウス(MOG)による背景モデル構築
#   - 入力と出力: 入力: 動画(ユーザは「0:動画ファイル,1:カメラ,2:サンプル動画」のメニューで選択.0:動画ファイルの場合はtkinterでファイル選択.1の場合はOpenCVでカメラが開く.2の場合はhttps://raw.githubusercontent.com/opencv/opencv/master/samples/data/vtest.aviを使用)、出力: OpenCV画面でリアルタイム表示(変化領域を赤色オーバーレイ)、各フレームごとにFPSと変化率をprint()表示、終了時にresult.txtに保存
#   - 処理手順:
#     1. 入力フレームの取得とガウシアンフィルタ適用
#     2. 照明正規化処理(LabのLチャネルにCLAHE)
#     3. MOG(混合ガウス)による背景判定(ω/σ降順の累積重みTで背景集合を定義)
#     4. kσマッチ判定による変化検出(感度パラメータで制御)
#     5. 形態学的処理によるノイズ除去
#     6. 最小領域フィルタによる小領域除去
#   - 前処理、後処理:
#     - 前処理: ガウシアンフィルタによるノイズ除去、CLAHE照明正規化
#     - 後処理: 形態学的オープニング・クロージング、最小領域フィルタ
#   - 追加処理:
#     - 適応的背景更新: 学習率αとρ=α·ηに基づくMOG更新(S&G 1999)
#     - 統計的閾値: 標準偏差を用いたkσマッチ閾値
#     - バウンディングボックス描画: 検出領域の可視化と領域情報の表示
#   - 調整を必要とする設定値:
#     - sensitivity(基本感度): 変化検出の閾値を制御(0.5-3.0、デフォルト1.5)
#     - min_change_area(最小変化領域): ノイズ除去のための最小面積(10-200ピクセル、デフォルト50)
# 将来方策: sensitivity値の自動調整機能。初期30フレームの標準偏差分布を分析し、ヒストグラムの分散から背景の複雑さを定量化。分散値に基づいてsensitivity値を0.5-3.0の範囲で自動設定
# その他の重要事項: 微細な影や明るさ変化も検出可能な設計
# 前準備:
#   - pip install opencv-python numpy pillow

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

# 設定値(調整可能なパラメータ)
SENSITIVITY = 1.5  # 変化検出の感度(0.5-3.0、値が大きいほど感度が低い)
MIN_CHANGE_AREA = 50  # 最小変化領域(ピクセル、小さいノイズを除去)
LEARNING_RATE = 0.01  # 背景モデルの学習率α(0.001-0.1、値が大きいほど背景の更新が速い)
BUFFER_SIZE = 30  # 背景モデル初期化用のフレーム数
GAUSSIAN_KERNEL = 5  # ガウシアンフィルタのカーネルサイズ(3,5,7など奇数)
MORPH_KERNEL = 3  # 形態学的処理のカーネルサイズ(3,5,7など奇数)
CLAHE_CLIP_LIMIT = 2.0  # CLAHEのクリップリミット(1.0-4.0)
CLAHE_GRID_SIZE = 8  # CLAHEのグリッドサイズ(通常8)
STD_OFFSET = 1.0  # 初期分散の下限(ゼロ除算防止)

# MOG設定
K_COMPONENTS = 5     # 混合成分数(3〜5が一般的)
BG_RATIO_T = 0.7     # 背景とみなす累積重みの閾値T(0.7〜0.9)
INIT_VAR = 225.0     # 初期分散(15^2)
MIN_VAR = 64.0       # 分散の下限(8^2)

# グローバル変数
sensitivity = SENSITIVITY
min_area = MIN_CHANGE_AREA
learning_rate = LEARNING_RATE
running = True
is_camera = False  # 入力種別のフラグ(choiceから設定)

# 背景モデル(MOG)
gmm_initialized = False
gmm_weights = None   # 形状: (H, W, K)
gmm_means = None     # 形状: (H, W, K, 3)
gmm_vars = None      # 形状: (H, W, K)
frame_buffer = []    # 初期化用フレームバッファ(Lab画像のfloat32)

# 結果記録用
frame_count = 0
results_log = []

# CLAHEオブジェクトを事前に作成
clahe = cv2.createCLAHE(clipLimit=CLAHE_CLIP_LIMIT, tileGridSize=(CLAHE_GRID_SIZE, CLAHE_GRID_SIZE))

# フォント設定
FONT_PATH = 'C:/Windows/Fonts/meiryo.ttc'
FONT_SIZE = 20
try:
    font = ImageFont.truetype(FONT_PATH, FONT_SIZE)
    use_japanese_font = True
except:
    use_japanese_font = False


def add_japanese_text(img, text, position, color=(0, 255, 0)):
    """OpenCV画像に日本語テキストを追加"""
    if not use_japanese_font:
        cv2.putText(img, text, position, cv2.FONT_HERSHEY_SIMPLEX, 0.7, color, 2)
        return img
    img_pil = Image.fromarray(cv2.cvtColor(img, cv2.COLOR_BGR2RGB))
    draw = ImageDraw.Draw(img_pil)
    draw.text(position, text, font=font, fill=color[::-1])  # BGRからRGBに変換
    return cv2.cvtColor(np.array(img_pil), cv2.COLOR_RGB2BGR)


def initialize_gmm_from_buffer():
    """BUFFER_SIZE枚のフレームからMOG初期化"""
    global gmm_initialized, gmm_weights, gmm_means, gmm_vars, frame_buffer
    stack = np.stack(frame_buffer, axis=0).astype(np.float32)  # (N, H, W, 3)
    mean0 = stack.mean(axis=0)                                 # (H, W, 3)
    var0 = np.maximum(stack.var(axis=0), STD_OFFSET)           # (H, W, 3)
    var0_scalar = var0.mean(axis=2)                            # (H, W) 等方分散
    H, W = mean0.shape[:2]
    gmm_weights = np.zeros((H, W, K_COMPONENTS), np.float32)
    gmm_weights[:, :, 0] = 1.0
    gmm_means = np.broadcast_to(mean0[:, :, None, :], (H, W, K_COMPONENTS, 3)).copy()
    gmm_vars = np.broadcast_to(var0_scalar[:, :, None], (H, W, K_COMPONENTS)).copy()
    gmm_vars = np.maximum(gmm_vars, MIN_VAR)
    gmm_initialized = True
    frame_buffer = []


def mog_foreground_and_update(lab_float):
    """MOG(S&G 1999)に基づく前景マスク生成とパラメータ更新"""
    global gmm_weights, gmm_means, gmm_vars
    # 差分と距離
    diff = lab_float[:, :, None, :] - gmm_means                # (H,W,K,3)
    d2 = np.sum(diff * diff, axis=3)                           # (H,W,K)
    sigma2 = np.maximum(gmm_vars, MIN_VAR)
    sigma = np.sqrt(sigma2)
    thr2 = (sensitivity * sigma) ** 2
    matches = d2 <= thr2                                       # (H,W,K) bool

    # 背景集合(更新前):ω/σ降順の累積重みT
    rank = gmm_weights / np.maximum(sigma, 1e-6)
    order = np.argsort(-rank, axis=2)                          # (H,W,K)
    weights_sorted = np.take_along_axis(gmm_weights, order, axis=2)
    cumw = np.cumsum(weights_sorted, axis=2)
    bg_mask_sorted = cumw < BG_RATIO_T
    cross = cumw >= BG_RATIO_T
    first_idx = np.argmax(cross, axis=2)
    has_cross = cross.any(axis=2)
    rr, cc = np.where(has_cross)
    if rr.size > 0:
        bg_mask_sorted[rr, cc, first_idx[rr, cc]] = True
    inv = np.argsort(order, axis=2)
    bg_mask = np.take_along_axis(bg_mask_sorted, inv, axis=2)  # (H,W,K) bool

    # 前景判定(更新前の背景成分と一致していない画素)
    is_bg = (matches & bg_mask).any(axis=2)                    # (H,W)
    fg_mask = np.where(is_bg, 0, 255).astype(np.uint8)

    # 更新(Stauffer & Grimson 1999)
    alpha = learning_rate
    gmm_weights *= (1.0 - alpha)

    # マッチ成分のインデックス(最も重いもの)
    w_masked = np.where(matches, gmm_weights, -1.0)
    m_idx = np.argmax(w_masked, axis=2)                        # (H,W)
    matched_any = matches.any(axis=2)
    r_m, c_m = np.where(matched_any)
    if r_m.size > 0:
        k_m = m_idx[r_m, c_m]

        mu_old = gmm_means[r_m, c_m, k_m, :]                   # (M,3)
        var_old = np.maximum(gmm_vars[r_m, c_m, k_m], MIN_VAR) # (M,)
        x = lab_float[r_m, c_m, :]                             # (M,3)
        d = x - mu_old
        d2_m = np.sum(d * d, axis=1)                           # (M,)

        # ρ = α · η(x|μ, σ^2 I), d=3
        norm = ((2.0 * np.pi) ** 1.5) * (np.sqrt(var_old) ** 3)
        eta = np.exp(-0.5 * d2_m / var_old) / np.maximum(norm, 1e-12)
        rho = np.clip(alpha * eta, 0.0, 1.0)

        # μ, σ^2 の更新
        gmm_means[r_m, c_m, k_m, :] = mu_old + (rho[:, None] * d)
        gmm_vars[r_m, c_m, k_m] = np.clip(var_old + rho * (d2_m - var_old), MIN_VAR, 1e6)
        gmm_weights[r_m, c_m, k_m] += alpha

    # 正規化
    sumw = np.sum(gmm_weights, axis=2, keepdims=True)
    gmm_weights = gmm_weights / np.maximum(sumw, 1e-12)

    # 非マッチ画素は最小重み成分を置換
    unmatched = ~matched_any
    r_u, c_u = np.where(unmatched)
    if r_u.size > 0:
        repl = np.argmin(gmm_weights[r_u, c_u, :], axis=1)
        gmm_means[r_u, c_u, repl, :] = lab_float[r_u, c_u, :]
        gmm_vars[r_u, c_u, repl] = INIT_VAR
        gmm_weights[r_u, c_u, repl] = alpha
        sw = np.sum(gmm_weights[r_u, c_u, :], axis=1, keepdims=True)
        gmm_weights[r_u, c_u, :] /= np.maximum(sw, 1e-12)

    # ω/σ降順に並べ替え
    rank_new = gmm_weights / np.sqrt(np.maximum(gmm_vars, MIN_VAR))
    order_new = np.argsort(-rank_new, axis=2)
    gmm_weights = np.take_along_axis(gmm_weights, order_new, axis=2)
    gmm_vars = np.take_along_axis(gmm_vars, order_new, axis=2)
    gmm_means = np.take_along_axis(gmm_means, order_new[:, :, :, None], axis=2)

    return fg_mask


def video_frame_processing(frame):
    global frame_count, frame_buffer, gmm_initialized
    current_time = time.time()
    frame_count += 1

    # ガウシアンフィルタによるノイズ除去
    blurred = cv2.GaussianBlur(frame, (GAUSSIAN_KERNEL, GAUSSIAN_KERNEL), 0)
    # Lab色空間に変換
    lab = cv2.cvtColor(blurred, cv2.COLOR_BGR2Lab)

    # Lチャンネルに対してCLAHE適用(照明正規化)
    l_channel = lab[:, :, 0]
    lab[:, :, 0] = clahe.apply(l_channel)

    # 背景モデルの初期化
    if not gmm_initialized:
        frame_buffer.append(lab.astype(np.float32))
        n = len(frame_buffer)
        output = frame.copy()
        if n >= BUFFER_SIZE:
            initialize_gmm_from_buffer()
            msg = f'初期化完了: {BUFFER_SIZE}/{BUFFER_SIZE}'
        else:
            msg = f'初期化中... {n}/{BUFFER_SIZE}'
        output = add_japanese_text(output, msg, (10, 30))
        result = f'初期化ステータス: {msg}'
        return output, result, current_time

    # 前景マスク生成(MOG)と更新
    lab_float = lab.astype(np.float32)
    fg_mask = mog_foreground_and_update(lab_float)

    # 形態学的処理
    kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (MORPH_KERNEL, MORPH_KERNEL))
    opened = cv2.morphologyEx(fg_mask, cv2.MORPH_OPEN, kernel)
    closed = cv2.morphologyEx(opened, cv2.MORPH_CLOSE, kernel)

    # 最小領域フィルタとバウンディングボックス
    contours, _ = cv2.findContours(closed, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
    filtered_mask = np.zeros_like(closed)
    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))

    # 変化領域を赤色でオーバーレイ
    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)

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

    # テキスト表示
    output = add_japanese_text(output, f'変化率: {change_rate:.2f}%', (10, 30))
    output = add_japanese_text(output, f'感度: {sensitivity:.1f}', (10, 60))
    output = add_japanese_text(output, f'検出領域数: {len(bounding_boxes)}', (10, 90))

    # 結果文字列の作成
    result = f'変化率: {change_rate:.2f}%, 領域数: {len(bounding_boxes)}'

    return output, result, current_time


def update_sensitivity(value):
    global sensitivity
    sensitivity = float(value)


def video_processing_thread(cap):
    global running, frame_count, results_log
    try:
        prev_t = time.perf_counter()
        while running:
            ret, frame = cap.read()
            if not ret:
                break

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

            now_t = time.perf_counter()
            dt = now_t - prev_t
            fps = (1.0 / dt) if dt > 0 else 0.0
            prev_t = now_t

            if is_camera:
                print(datetime.fromtimestamp(current_time).strftime("%Y-%m-%d %H:%M:%S.%f")[:-3],
                      f'FPS: {fps:.2f},', result)
            else:
                print(frame_count, f'FPS: {fps:.2f},', result)

            results_log.append(f'FPS: {fps:.2f}, {result}')

            if cv2.waitKey(1) & 0xFF == ord('q'):
                running = False
                request_gui_stop()
                break
    finally:
        cap.release()
        cv2.destroyAllWindows()


# ガイダンス表示
print('\n=== 統計的背景モデル(MOG)による微細変化検出システム ===')
print('概要: 動画から微細な明るさ変化を含むすべての変化を検出します')
print('操作方法:')
print('  q キー: プログラム終了')
print('  感度調整ウィンドウで検出感度を調整可能')
print('注意事項:')
print('  初期化に約30フレーム必要です')
print('  検出結果は自動的にresult.txtに保存されます')

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

choice = input("選択: ")

# tkinterのメインウィンドウを作成
root = tk.Tk()
root.withdraw()

def stop_processing():
    global running
    running = False
    root.quit()

def request_gui_stop():
    try:
        root.after(0, root.quit)
    except:
        pass

if choice == '0':
    path = filedialog.askopenfilename()
    root.deiconify()
    if not path:
        exit()
    cap = cv2.VideoCapture(path)
    is_camera = False
elif choice == '1':
    root.deiconify()
    cap = cv2.VideoCapture(0, cv2.CAP_DSHOW)
    if not cap.isOpened():
        cap = cv2.VideoCapture(0)
    cap.set(cv2.CAP_PROP_BUFFERSIZE, 1)
    is_camera = True
else:
    root.deiconify()
    # サンプル動画ダウンロード・処理
    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)
    is_camera = False

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

# 感度調整ウィンドウを作成
control_window = tk.Toplevel(root)
control_window.title('感度調整')
control_window.geometry('300x200')

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

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

value_label = Label(control_window, text='')
value_label.pack()

stop_button = Button(control_window, text='終了', command=stop_processing)
stop_button.pack(pady=10)

def update_value_label():
    if running:
        value_label.config(text=f'現在の値: {sensitivity:.1f}')
        control_window.after(100, update_value_label)

update_value_label()

# 動画処理を別スレッドで開始
video_thread = threading.Thread(target=video_processing_thread, args=(cap,))
video_thread.daemon = True
video_thread.start()

print('\n=== 動画処理開始 ===')
print('操作方法:')
print('  q キー: プログラム終了')

# tkinterのメインループ
root.mainloop()

# スレッドの終了を待つ
video_thread.join()

print('\n=== プログラム終了 ===')
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に保存しました')

実験・研究スキルの基礎:Windowsで学ぶ微細変化検出実験

1. 実験・研究のスキル構成要素

実験や研究を行うには、以下の5つの構成要素を理解する必要がある。

1.1 実験用データ

このプログラムでは、動画ファイルまたはウェブカメラの映像を実験用データとして使用する。

1.2 実験計画

何を明らかにするために実験を行うのかを定める。

計画例

1.3 プログラム

実験を実施するためのツールである。このプログラムは適応的混合ガウス背景モデル(MOG)とCLAHE照明正規化を使用している。

1.4 プログラムの機能

このプログラムは複数のパラメータで微細変化検出を制御する。

入力パラメータ

出力情報

スライダーの動作

1.5 検証(結果の確認と考察)

プログラムの実行結果を観察し、パラメータの影響を考察する。

基本認識

観察のポイント

2. 間違いの原因と対処方法

動画ファイルが開けない
  • 原因:ファイルパスが正しくない、またはコーデックが対応していない
  • 対処方法:選択2(サンプル動画)で動作を確認してから、自分の動画ファイルを試す
ウェブカメラが開けない
  • 原因:カメラが他のアプリケーションで使用中、またはアクセス許可がない
  • 対処方法:他のカメラアプリを終了し、カメラのアクセス許可を確認する
初期化が完了しない(30フレームで止まる)
  • 原因:動画が30フレーム未満、またはカメラ映像が取得できていない
  • 対処方法:カメラの接続を確認する。動画の場合は別のファイルを試す
感度を変えても検出結果が変化しない
  • 原因:元々変化がほとんどないシーン、または変化幅が極端に大きいか小さい
  • 対処方法:感度パラメータを0.5から3.0まで大きく変化させて観察する。動きのあるシーンで試す
明らかな変化が検出されない
  • 原因:感度パラメータの値が大きすぎる、または背景モデルが変化に追従している
  • 対処方法:感度パラメータを0.5程度に設定する(値を小さくして敏感にする)。それでも検出されない場合は、変化が緩やかすぎて背景として学習されている可能性がある
画面全体が変化領域として検出される
  • 原因:感度パラメータの値が小さすぎる、またはカメラの揺れ
  • 対処方法:感度パラメータを1.5~2.0程度に設定する(値を大きくして反応を鈍くする)。カメラを固定する
小さなノイズが大量に検出される
  • 原因:最小変化領域の設定が小さすぎる
  • 対処方法:MIN_CHANGE_AREAを100~200に増やす。ソースコード内のパラメータを変更して実験する
照明が変化すると誤検出が増える
  • 原因:照明変化が急激すぎて背景モデルが追従できていない
  • 対処方法:これは正常な動作である。学習率(LEARNING_RATE)を0.02~0.05に上げると追従が速くなるが、本来の変化も見逃しやすくなるトレードオフがある

3. 実験レポートのサンプル

感度パラメータが微細変化検出に与える影響

実験目的

屋内環境で人の通過による微細な変化を検出しながら、ノイズによる誤検出を最小化するための感度設定を見つける。

実験計画

最小変化領域をxxxピクセルに固定し、感度パラメータを変化させて最適値を探す。

実験方法

プログラムを実行し、スライダーを操作しながら以下の基準で評価する。

実験結果

感度値 検出領域数 変化率(%) 正検出 誤検出 見逃し 総合評価
xxx xxx xxx xxx xxx xxx xxx
xxx xxx xxx xxx xxx xxx xxx
xxx xxx xxx xxx xxx xxx xxx
xxx xxx xxx xxx xxx xxx xxx
xxx xxx xxx xxx xxx xxx xxx

考察

結論

(例文)本実験の屋内環境においては、感度値xxx~xxxが最もバランスの取れた設定であった。微細な変化も含めてすべて記録したい場合はxxx、明確な変化のみを検出したい場合はxxxが適切である。また、初期化フェーズ(最初のxxxフレーム)で背景モデルが適切に構築されることが重要であり、カメラを固定し、人が映らない状態で開始する必要性が確認できた。実験を通じて、照明条件や環境に応じた適切なパラメータ調整の重要性を理解できた。