YOLOv12による人物検出・ByteTrackによる追跡とTTAの機能付き(ソースコードと説明と利用ガイド)

ツール利用ガイド

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

動画ファイルやカメラ映像から人物を自動検出するソフトウェアである。監視カメラの映像解析、人流計測、映像コンテンツの人物抽出などに活用できる。

2. 主な機能

3. 基本的な使い方

  1. 起動とモデル選択:

    プログラム起動後、使用するモデルサイズ(n/s/m/l/x)を選択する。

  2. 入力ソースの選択:

    キーボードで 0(動画ファイル)、1(カメラ)、2(サンプル動画)のいずれかを入力する。

  3. 検出処理の実行:

    映像が表示され、検出された人物にバウンディングボックスと追跡IDが表示される。

  4. 終了方法:

    映像表示画面でキーボードの q キーを押す。

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
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/

Gitのインストール

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


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

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

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


pip install -U torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu126
pip install ultralytics opencv-python numpy pillow boxmot

YOLOv12による人物検出・ByteTrackによる追跡とTTAの機能付き

概要

このプログラムは、YOLOv12モデルを使用して動画やカメラ映像から人物をリアルタイムで検出する。COCOデータセットの80クラスからPersonクラスのみを抽出し、検出した人物をバウンディングボックスで表示する。

主要技術

YOLOv12 (You Only Look Once version 12)

2025年にTianらが発表した物体検出モデル[1]。Area Attention機構により、従来のAttention機構と比較して計算コストを削減しながら、CNNベースモデルと同等の処理速度を実現する。

ByteTrack

Zhangらが2022年に開発した物体追跡アルゴリズム[2]。カルマンフィルタによる動き予測とハンガリアンアルゴリズムによる対応付けを組み合わせ、検出結果に追跡IDを付与する。

技術的特徴

実装の特色

参考文献

[1] Tian, Y., Ye, Q., & Doermann, D. (2025). YOLOv12: Attention-Centric Real-Time Object Detectors. arXiv preprint arXiv:2502.12524. https://github.com/sunsmarterjie/yolov12

[2] Zhang, Y., et al. (2022). ByteTrack: Multi-Object Tracking by Associating Every Detection Box. ECCV 2022. https://arxiv.org/abs/2110.06864

[3] Simonyan, K., & Zisserman, A. (2015). Very Deep Convolutional Networks for Large-Scale Image Recognition. ICLR 2015.

ソースコード


# プログラム名: YOLOv12による人物検出・ByteTrackによる追跡とTTAの機能付き
# 特徴技術名: YOLOv12 (You Only Look Once version 12)
# 出典: Tian, Yunjie and Ye, Qixiang and Doermann, David (2025). YOLOv12: Attention-Centric Real-Time Object Detectors. arXiv preprint arXiv:2502.12524. GitHub: https://github.com/sunsmarterjie/yolov12
# 特徴機能: Area Attention機構による物体検出、TTA、ByteTrack追跡
# 学習済みモデル: yolo12n.pt (COCOデータセットで事前学習済み、80クラス対応、nanoバリアント、手動配置が必要)で、クラス0が人物(person)を検出可能
#   モデルサイズ選択可能(デフォルト:n):
#   n (nano): yolo12n.pt - 軽量
#   s (small): yolo12s.pt - 軽量
#   m (medium): yolo12m.pt - バランス型
#   l (large): yolo12l.pt - 精度重視
#   x (extra large): yolo12x.pt - 最大精度
# 方式設計:
#   - 関連利用技術:
#     - PyTorch: 深層学習フレームワーク、CUDA対応によるGPU加速
#     - OpenCV: 画像処理、カメラ制御、描画処理、動画入出力管理
#     - ByteTrack: カルマンフィルタとハンガリアンアルゴリズムによる高精度物体追跡(boxmotパッケージ版)
#     - TTA (Test Time Augmentation): 複数の画像変換で推論し結果を統合
#   - 入力と出力: 入力: 動画(ユーザは「0:動画ファイル,1:カメラ,2:サンプル動画」のメニューで選択.0:動画ファイルの場合はtkinterでファイル選択.1の場合はOpenCVでカメラが開く.2の場合はhttps://raw.githubusercontent.com/opencv/opencv/master/samples/data/vtest.aviを使用)、出力: OpenCV画面でリアルタイム表示(検出したオブジェクトをバウンディングボックスで表示)、フレーム毎のprint()による処理結果表示、プログラム終了時にresult.txtファイルに保存
#   - 処理手順: 1.フレーム取得、2.前処理(CLAHE)、3.TTA適用、4.YOLOv12推論実行、5.信頼度閾値による選別、6.ByteTrack追跡、7.バウンディングボックス描画
#   - 前処理、後処理: 前処理:CLAHE適用。YOLOv12内部で自動実行(640x640リサイズ、正規化)。後処理:信頼度による閾値フィルタリング、ByteTrack追跡による検出結果の安定化とID管理
#   - 追加処理: CUDA/CPU自動検出機能により、GPU搭載環境では自動的に処理実行。検出結果の信頼度降順ソートにより重要な検出を優先表示。TTA - 水平反転による推論結果の統合
#   - 調整を必要とする設定値: CONF_THRESH(オブジェクト検出信頼度閾値、デフォルト0.5)- 値を上げると誤検出が減少するが検出漏れが増加、TTA_ENABLED(TTAの有効/無効、デフォルトTrue)
# 将来方策: CONF_THRESHの動的調整機能。フレーム毎の検出数を監視し、検出数が閾値を超えた場合は信頼度を上げ、検出数が少ない場合は下げる適応的制御の実装
# その他の重要事項: Windows環境専用設計、CUDA対応GPU推奨(自動検出・CPUフォールバック機能付き)、初回実行時は学習済みモデルの手動配置が必要
# 特徴技術および学習済モデルの利用制限: YOLOv12はAGPL-3.0ライセンス。商用利用にはUltralyticsのエンタープライズライセンスが必要。学習済みモデル(COCO)は研究・教育目的での利用を推奨。ByteTrackはMITライセンス。必ず利用者自身で最新の利用制限を確認すること。
# 前準備:
#   - pip install -U torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu126
#   - pip install ultralytics opencv-python numpy pillow boxmot

import cv2
import tkinter as tk
from tkinter import filedialog
import torch
import torchvision
import numpy as np
import time
import urllib.request
from ultralytics import YOLO
from datetime import datetime
import sys
import io
from PIL import Image, ImageDraw, ImageFont
from boxmot import ByteTrack
import threading

# Windows文字エンコーディング設定
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8', line_buffering=True)

# GPU/CPU自動選択
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
print(f'デバイス: {str(device)}')

# GPU使用時の最適化
if device.type == 'cuda':
    torch.backends.cudnn.benchmark = True

# ===== 設定・定数管理 =====
# モデル情報
MODEL_INFO = {
    'n': {'name': 'Nano', 'desc': '軽量', 'params': '3.2M'},
    's': {'name': 'Small', 'desc': '軽量', 'params': '11.2M'},
    'm': {'name': 'Medium', 'desc': 'バランス型', 'params': '25.9M'},
    'l': {'name': 'Large', 'desc': '精度重視', 'params': '43.7M'},
    'x': {'name': 'Extra Large', 'desc': '最大精度', 'params': '68.2M'}
}

# Personクラスのみの設定
PERSON_CLASS_ID = 0
PERSON_CLASS_NAME = 'person'
PERSON_CLASS_NAME_JP = '人'

# Personクラス用の色(BGR形式)- デフォルト色(トラッカー無効時用)
PERSON_COLOR = (0, 255, 0)  # 緑色

# IDから色を生成する関数
def get_color_from_id(track_id):
    """IDをハッシュ化してHSV色空間の色を生成(高視認性)"""
    hue = int((track_id * 37) % 180)
    hsv = np.uint8([[[hue, 255, 255]]])
    bgr = cv2.cvtColor(hsv, cv2.COLOR_HSV2BGR)[0][0]
    return (int(bgr[0]), int(bgr[1]), int(bgr[2]))

# BGR→RGB色変換のヘルパー関数
def bgr_to_rgb(color_bgr):
    """BGRカラーをRGBカラーに変換"""
    return (color_bgr[2], color_bgr[1], color_bgr[0])

# 日本語フォント設定(フォントサイズの分離管理)
FONT_PATH = 'C:/Windows/Fonts/meiryo.ttc'
FONT_SIZE_MAIN = 16
FONT_SIZE_SMALL = 12
font_main = ImageFont.truetype(FONT_PATH, FONT_SIZE_MAIN)
font_small = ImageFont.truetype(FONT_PATH, FONT_SIZE_SMALL)

SAMPLE_URL = 'https://raw.githubusercontent.com/opencv/opencv/master/samples/data/vtest.avi'
SAMPLE_FILE = 'vtest.avi'

# 調整可能な設定値
CONF_THRESH = 0.5
IOU_THRESH = 0.45
CLAHE_CLIP_LIMIT = 2.0
CLAHE_TILE_SIZE = (8, 8)
WINDOW_NAME = "YOLOv12 Object Detection (CLAHE)"
TTA_CONF_BOOST = 0.03
NMS_THRESHOLD = 0.6
TTA_ENABLED = True
USE_TRACKER = True

# CLAHEオブジェクトをグローバルスコープで一度だけ定義
clahe = cv2.createCLAHE(clipLimit=CLAHE_CLIP_LIMIT, tileGridSize=CLAHE_TILE_SIZE)

# グローバル変数
frame_count = 0
results_log = []
person_count = 0
model = None
tracker = None

# ===== スレッド化されたフレーム取得 =====
class ThreadedVideoCapture:
    """スレッド化されたVideoCapture(常に最新フレームを取得)"""
    def __init__(self, src, is_camera=False):
        if is_camera:
            self.cap = cv2.VideoCapture(src, cv2.CAP_DSHOW)
            fourcc = cv2.VideoWriter_fourcc('M', 'J', 'P', 'G')
            self.cap.set(cv2.CAP_PROP_FOURCC, fourcc)
            self.cap.set(cv2.CAP_PROP_FPS, 60)
        else:
            self.cap = cv2.VideoCapture(src)

        self.grabbed, self.frame = self.cap.read()
        self.stopped = False
        self.lock = threading.Lock()
        self.thread = threading.Thread(target=self.update, args=())
        self.thread.daemon = True
        self.thread.start()

    def update(self):
        """バックグラウンドでフレームを取得し続ける"""
        while not self.stopped:
            grabbed, frame = self.cap.read()
            with self.lock:
                self.grabbed = grabbed
                if grabbed:
                    self.frame = frame

    def read(self):
        """最新フレームを返す"""
        with self.lock:
            return self.grabbed, self.frame.copy() if self.grabbed else (self.grabbed, None)

    def isOpened(self):
        return self.cap.isOpened()

    def get(self, prop):
        return self.cap.get(prop)

    def release(self):
        self.stopped = True
        self.thread.join()
        self.cap.release()

def display_program_header():
    """プログラム概要表示"""
    print('=== YOLOv12オブジェクト検出プログラム (CLAHE) ===')
    print('概要: リアルタイムでオブジェクトを検出し、バウンディングボックスで表示します')
    print('機能: YOLOv12によるオブジェクト検出(Person検出専用)')
    print('技術: CLAHE (コントラスト強化) + ByteTrack による追跡 + TTA (Test Time Augmentation)')
    print('操作: qキーで終了')
    print('出力: 各フレームでの処理結果表示、終了時にresult.txt保存')
    print()

# ===== 共通処理関数 =====
def draw_texts_with_pillow(bgr_frame, texts):
    """テキスト描画, texts: list of dict with keys {text, org, color, font_type}"""
    if font_main is None:
        return bgr_frame

    img_pil = Image.fromarray(cv2.cvtColor(bgr_frame, cv2.COLOR_BGR2RGB))
    draw = ImageDraw.Draw(img_pil)

    for item in texts:
        text = item['text']
        x, y = item['org']
        color = item['color']
        font_type = item.get('font_type', 'main')
        font = font_main if font_type == 'main' else font_small
        draw.text((x, y), text, font=font, fill=color)

    return cv2.cvtColor(np.array(img_pil), cv2.COLOR_RGB2BGR)

# ===== TTA機能 =====
def normal_inference(frame, model, conf):
    """通常の推論処理"""
    results = model(frame, device=device, verbose=False)

    objects = []
    if results[0].boxes is not None:
        boxes = results[0].boxes.xywh.cpu().numpy()
        confs = results[0].boxes.conf.cpu().numpy()
        classes = results[0].boxes.cls.cpu().numpy()

        valid_indices = (confs > conf) & (classes == PERSON_CLASS_ID)
        if np.any(valid_indices):
            boxes = boxes[valid_indices]
            confs = confs[valid_indices]
            classes = classes[valid_indices]

            boxes_xyxy = boxes.copy()
            boxes_xyxy[:, 0] = boxes[:, 0] - boxes[:, 2] / 2
            boxes_xyxy[:, 1] = boxes[:, 1] - boxes[:, 3] / 2
            boxes_xyxy[:, 2] = boxes[:, 0] + boxes[:, 2] / 2
            boxes_xyxy[:, 3] = boxes[:, 1] + boxes[:, 3] / 2

            sorted_indices = np.argsort(confs)[::-1]
            boxes_xyxy = boxes_xyxy[sorted_indices]
            confs = confs[sorted_indices]
            classes = classes[sorted_indices]

            for i, (box, conf_val, cls) in enumerate(zip(boxes_xyxy, confs, classes)):
                x1, y1, x2, y2 = map(int, box)
                object_data = {
                    'box': (x1, y1, x2, y2),
                    'detection_conf': conf_val,
                    'class_id': PERSON_CLASS_ID,
                    'class_name': PERSON_CLASS_NAME
                }
                objects.append(object_data)

    return objects

def apply_tta_inference(frame, model, conf):
    """Test Time Augmentation (TTA)を適用した推論"""
    frame_width = frame.shape[1]

    flipped_frame = cv2.flip(frame, 1)

    results = model([frame, flipped_frame], verbose=False)

    all_boxes = []
    all_confs = []
    all_classes = []

    if results[0].boxes is not None and len(results[0].boxes) > 0:
        boxes_orig = results[0].boxes.xyxy
        confs_orig = results[0].boxes.conf
        classes_orig = results[0].boxes.cls
        person_indices = classes_orig == PERSON_CLASS_ID
        if person_indices.sum() > 0:
            all_boxes.append(boxes_orig[person_indices])
            all_confs.append(confs_orig[person_indices])
            all_classes.append(classes_orig[person_indices])

    if len(results) > 1 and results[1].boxes is not None and len(results[1].boxes) > 0:
        boxes_flipped = results[1].boxes.xyxy.clone()
        confs_flipped = results[1].boxes.conf
        classes_flipped = results[1].boxes.cls

        person_indices = classes_flipped == PERSON_CLASS_ID
        if person_indices.sum() > 0:
            boxes_flipped = boxes_flipped[person_indices]
            confs_flipped = confs_flipped[person_indices]
            classes_flipped = classes_flipped[person_indices]

            if boxes_flipped.shape[0] > 0:
                # 水平反転画像での検出結果を元の画像座標系に変換
                # x1, x2 の大小関係を保つ必要がある
                x1_flipped = boxes_flipped[:, 0].clone()
                x2_flipped = boxes_flipped[:, 2].clone()
                # 元の画像座標系での新しい座標
                boxes_flipped[:, 0] = frame_width - 1 - x2_flipped  # 新しいx1(左端)
                boxes_flipped[:, 2] = frame_width - 1 - x1_flipped  # 新しいx2(右端)

            all_boxes.append(boxes_flipped)
            all_confs.append(confs_flipped)
            all_classes.append(classes_flipped)

    if len(all_boxes) == 0:
        return []

    all_boxes = torch.cat(all_boxes, dim=0)
    all_confs = torch.cat(all_confs, dim=0)
    all_classes = torch.cat(all_classes, dim=0)

    valid_indices = all_confs > conf
    if valid_indices.sum() > 0:
        all_boxes = all_boxes[valid_indices]
        all_confs = all_confs[valid_indices]
        all_classes = all_classes[valid_indices]

        nms_indices = torchvision.ops.nms(all_boxes, all_confs, iou_threshold=NMS_THRESHOLD)
        final_boxes = all_boxes[nms_indices].cpu().numpy()
        final_confs = all_confs[nms_indices].cpu().numpy()
        final_classes = all_classes[nms_indices].cpu().numpy()

        sorted_indices = np.argsort(final_confs)[::-1]
        final_boxes = final_boxes[sorted_indices]
        final_confs = final_confs[sorted_indices]
        final_classes = final_classes[sorted_indices]

        objects = []
        for i in range(len(final_confs)):
            x1, y1, x2, y2 = map(int, final_boxes[i])
            conf_boost = TTA_CONF_BOOST if TTA_ENABLED else 0
            object_data = {
                'box': (x1, y1, x2, y2),
                'detection_conf': min(1.0, final_confs[i] + conf_boost),
                'class_id': PERSON_CLASS_ID,
                'class_name': PERSON_CLASS_NAME
            }
            objects.append(object_data)

        return objects

    return []

def apply_tta_if_enabled(frame, model, conf):
    """TTA機能を条件付きで適用"""
    if not TTA_ENABLED:
        return normal_inference(frame, model, conf)
    return apply_tta_inference(frame, model, conf)

# ===== トラッキング機能 =====
def apply_bytetrack(objects, frame):
    """ByteTrackerを使用したトラッキング処理"""
    global tracker

    if len(objects) > 0:
        dets_array = np.array([[obj['box'][0], obj['box'][1], obj['box'][2], obj['box'][3],
                                obj['detection_conf'], obj['class_id']]
                               for obj in objects])
    else:
        dets_array = np.empty((0, 6))

    tracks = tracker.update(dets_array, frame)

    tracked_objects = []
    if len(tracks) > 0:
        for track in tracks:
            if len(track) >= 7:
                x1, y1, x2, y2, track_id, conf, cls = track[:7]
                tracked_objects.append({
                    'box': (int(x1), int(y1), int(x2), int(y2)),
                    'track_id': int(track_id),
                    'detection_conf': float(conf),
                    'class_id': PERSON_CLASS_ID,
                    'name': PERSON_CLASS_NAME
                })
    return tracked_objects

def apply_tracking_if_enabled(objects, frame):
    """トラッキング機能を条件付きで適用"""
    if not USE_TRACKER:
        return objects
    return apply_bytetrack(objects, frame)

# ===== 物体検出タスク固有の処理 =====
def draw_detection_results(frame, objects):
    """物体検出の描画処理"""
    for i, obj in enumerate(objects):
        x1, y1, x2, y2 = obj['box']

        if USE_TRACKER and 'track_id' in obj:
            color = get_color_from_id(obj['track_id'])
        else:
            color = PERSON_COLOR

        cv2.rectangle(frame, (x1, y1), (x2, y2), color, 2)

    texts_to_draw = []
    for obj in objects:
        x1, y1, x2, y2 = obj['box']

        track_id = obj.get('track_id', 0) if USE_TRACKER else 0
        if USE_TRACKER and track_id > 0:
            label = f"ID:{track_id} {PERSON_CLASS_NAME_JP}: {obj['detection_conf']:.1%}"
            color = get_color_from_id(track_id)
        else:
            label = f"{PERSON_CLASS_NAME_JP}: {obj['detection_conf']:.1%}"
            color = PERSON_COLOR

        texts_to_draw.append({
            'text': label,
            'org': (x1, y1-10),
            'color': bgr_to_rgb(color),
            'font_type': 'main'
        })

    frame = draw_texts_with_pillow(frame, texts_to_draw)

    tta_status = "TTA:ON" if TTA_ENABLED else "TTA:OFF"
    tracker_status = "ByteTrack:ON" if USE_TRACKER else "ByteTrack:OFF"
    info1 = f'YOLOv12 ({MODEL_INFO[MODEL_SIZE]["name"]}) | Frame: {frame_count} | Objects: {len(objects)} | {tta_status} | {tracker_status}'
    info2 = 'Press: q=Quit'
    cv2.putText(frame, info1, (10, 30), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (255, 255, 255), 2)
    cv2.putText(frame, info2, (10, 60), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (255, 255, 0), 1)

    return frame

def format_detection_output(objects):
    """物体検出の出力フォーマット"""
    if objects:
        results = []
        for obj in objects:
            x1, y1, x2, y2 = obj['box']
            conf = obj['detection_conf']
            if USE_TRACKER and 'track_id' in obj:
                results.append(f'id={obj["track_id"]},conf={conf:.3f},box=[{x1},{y1},{x2},{y2}]')
            else:
                results.append(f'conf={conf:.3f},box=[{x1},{y1},{x2},{y2}]')
        result = f'count={len(objects)}; ' + ' | '.join(results)
    else:
        result = 'count=0'
    return result

def detect_objects(frame):
    """共通の検出処理(CLAHE、推論、検出を実行)"""
    global model

    yuv_img = cv2.cvtColor(frame, cv2.COLOR_BGR2YUV)
    yuv_img[:,:,0] = clahe.apply(yuv_img[:,:,0])
    clahe_frame = cv2.cvtColor(yuv_img, cv2.COLOR_YUV2BGR)

    objects = apply_tta_if_enabled(clahe_frame, model, CONF_THRESH)

    return objects

def process_video_frame(frame, timestamp_ms, is_camera):
    """動画用ラッパー"""
    objects = detect_objects(frame)

    tracked_objects = apply_tracking_if_enabled(objects, frame)

    global person_count
    person_count += len(tracked_objects)

    frame = draw_detection_results(frame, tracked_objects)

    result = format_detection_output(tracked_objects)

    return frame, result

def video_frame_processing(frame, timestamp_ms, is_camera):
    """動画フレーム処理(標準形式)"""
    global frame_count
    current_time = time.time()
    frame_count += 1

    processed_frame, result = process_video_frame(frame, timestamp_ms, is_camera)
    return processed_frame, result, current_time

# プログラムヘッダー表示
display_program_header()

# ===== ByteTrackとTTAの有効化選択 =====
print('ByteTrackとTTA (Test Time Augmentation)の設定を選択してください:')
print('1: ByteTrack, TTA 無効化')
print('2: ByteTrack, TTA 有効化')

while True:
    feature_choice = input('選択 (1/2): ')
    if feature_choice == '1':
        TTA_ENABLED = False
        USE_TRACKER = False
        print('\nByteTrack: 無効')
        print('TTA: 無効\n')
        break
    elif feature_choice == '2':
        TTA_ENABLED = True
        USE_TRACKER = True
        print('\nByteTrack: 有効')
        print('TTA: 有効\n')
        break
    else:
        print('無効な選択です。1または2を入力してください。')

if USE_TRACKER:
    tracker = ByteTrack()

# ===== モデル選択 =====
while True:
    print('使用するYOLOv12モデルを選択してください:')
    for key, val in MODEL_INFO.items():
        print(f"  {key}: {val['name']} ({val['params']} params)")

    choice = input("選択 (n/s/m/l/x): ").lower()
    if choice in MODEL_INFO:
        MODEL_SIZE = choice
        break
    else:
        print("\n無効な選択です。もう一度入力してください。\n")

MODEL_NAME = f'yolo12{MODEL_SIZE}.pt'
print(f"\nモデル '{MODEL_INFO[MODEL_SIZE]['name']}' を使用します。\n")

# システム初期化
print('システム初期化中...')

if device.type == 'cuda':
    print(f'GPU検出: {torch.cuda.get_device_name(0)}')
    print(f'CUDA バージョン: {torch.version.cuda}')

try:
    print(f'YOLOv12 ({MODEL_INFO[MODEL_SIZE]["name"]}) モデルを初期化中...')
    model = YOLO(MODEL_NAME)
    print(f'YOLOv12 ({MODEL_INFO[MODEL_SIZE]["name"]}) モデルの初期化が完了しました')
    print(f'モデルサイズ: {MODEL_SIZE} ({MODEL_INFO[MODEL_SIZE]["name"]}={MODEL_INFO[MODEL_SIZE]["desc"]})')
except Exception as e:
    print(f'YOLOv12 ({MODEL_INFO[MODEL_SIZE]["name"]}) モデルの初期化に失敗しました')
    print(f'エラー: {e}')
    print(f"ヒント: '{MODEL_NAME}' ファイルがプログラムと同じディレクトリに配置されているか確認してください。")
    raise SystemExit(1)

print('初期化完了')
print()

if TTA_ENABLED:
    print("\nTest Time Augmentation (TTA): 有効")
    print("  - 水平反転による推論結果の統合")
    print(f"  - 信頼度ブースト値: {TTA_CONF_BOOST}")
    print(f"  - NMS閾値: {NMS_THRESHOLD}")
else:
    print("\nTest Time Augmentation (TTA): 無効")

if USE_TRACKER:
    print("\nByteTrack: 有効")
    print("  - カルマンフィルタによる動き予測")
else:
    print("\nByteTrack: 無効")

# 入力選択
print("\n入力ソースを選択してください:")
print('0: 動画ファイル')
print('1: カメラ')
print('2: サンプル動画')
choice = input('選択: ')

is_camera = (choice == '1')

if choice == '0':
    root = tk.Tk()
    root.withdraw()
    path = filedialog.askopenfilename()
    if not path:
        raise SystemExit(1)
    cap = cv2.VideoCapture(path)
elif choice == '1':
    cap = ThreadedVideoCapture(0, is_camera=True)
else:
    print('サンプル動画をダウンロード中...')
    urllib.request.urlretrieve(SAMPLE_URL, SAMPLE_FILE)
    cap = cv2.VideoCapture(SAMPLE_FILE)

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

# フレームレートの取得とタイムスタンプ増分の計算
if is_camera:
    actual_fps = cap.get(cv2.CAP_PROP_FPS)
    print(f'カメラのfps: {actual_fps}')
    timestamp_increment = int(1000 / actual_fps) if actual_fps > 0 else 33
else:
    video_fps = cap.get(cv2.CAP_PROP_FPS)
    timestamp_increment = int(1000 / video_fps) if video_fps > 0 else 33

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

start_time = time.time()
last_info_time = start_time
info_interval = 10.0
timestamp_ms = 0
total_processing_time = 0.0

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

        timestamp_ms += timestamp_increment

        processing_start = time.time()
        processed_frame, result, current_time = video_frame_processing(frame, timestamp_ms, is_camera)
        processing_time = time.time() - processing_start
        total_processing_time += processing_time
        cv2.imshow(WINDOW_NAME, processed_frame)

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

            results_log.append(result)

        # 情報提供(カメラモードのみ、info_interval秒ごと)
        if is_camera:
            elapsed = current_time - last_info_time
            if elapsed >= info_interval:
                total_elapsed = current_time - start_time
                actual_fps = frame_count / total_elapsed if total_elapsed > 0 else 0
                avg_processing_time = (total_processing_time / frame_count * 1000) if frame_count > 0 else 0
                print(f'[情報] 経過時間: {total_elapsed:.1f}秒, 処理フレーム数: {frame_count}, 実測fps: {actual_fps:.1f}, 平均処理時間: {avg_processing_time:.1f}ms')
                last_info_time = current_time

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

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

    if results_log:
        with open('result.txt', 'w', encoding='utf-8') as f:
            f.write('=== YOLOv12 Person検出結果 ===\n')
            f.write(f'使用モデル: YOLOv12 {MODEL_INFO[MODEL_SIZE]["name"]} ({MODEL_NAME})\n')
            f.write(f'処理フレーム数: {frame_count}\n')
            f.write(f'使用デバイス: {str(device).upper()}\n')
            if device.type == 'cuda':
                f.write(f'GPU: {torch.cuda.get_device_name(0)}\n')
            f.write(f'画像処理: CLAHE適用(YUV色空間)\n')
            f.write(f'TTA (Test Time Augmentation): {"有効" if TTA_ENABLED else "無効"}\n')
            if TTA_ENABLED:
                f.write(f'  - NMS閾値: {NMS_THRESHOLD}\n')
                f.write(f'  - 信頼度ブースト: {TTA_CONF_BOOST}\n')
            f.write(f'ByteTrack: {"有効" if USE_TRACKER else "無効"}\n')
            f.write(f'信頼度閾値: {CONF_THRESH}\n')
            f.write(f'\n検出されたクラス: {PERSON_CLASS_NAME_JP} ({PERSON_CLASS_NAME})\n')
            f.write(f'総検出回数: {person_count}回\n')
            if is_camera:
                f.write('形式: タイムスタンプ, 検出結果\n')
            else:
                f.write('形式: フレーム番号, 検出結果\n')
            f.write('\n')
            for i, result in enumerate(results_log, 1):
                if is_camera:
                    f.write(f'{result}\n')
                else:
                    f.write(f'Frame {i}: {result}\n')
        print('処理結果をresult.txtに保存しました')
        print(f'Person検出総数: {person_count}')