Layumi ReID と RT-DETRv2 による人物再識別(ソースコードと説明と利用ガイド)

【概要説明】 [PDF], [パワーポイント]

プログラム利用ガイド

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

動画映像内の複数人物を自動検出し、各人物の外観特徴に基づいて継続的に識別・追跡するシステムである。監視カメラシステムでの人物追跡、小売店舗での顧客行動分析、空港や駅での人流監視、スポーツ映像での選手追跡などの用途に適用できる。人物の外観情報を512次元の特徴ベクトルとして数値化し、フレーム間での同一人物判定を実行する。

2. 主な機能

3. 基本的な使い方

プログラム起動後、入力選択画面で以下のいずれかを選択する。

処理開始後は自動的に人物検出と特徴抽出が実行され、画面に検出結果が表示される。各人物には固有のPerson IDが割り当てられ、バウンディングボックスの色で区別される。コンソールには詳細な処理結果がフレーム単位で出力される。

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/

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

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


pip install -U torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu126
pip install transformers opencv-python pillow requests gdown timm

Layumi ReID + RT-DETRv2 統合人物再識別システム

概要

このプログラムは、Layumi's Person Re-ID BaselineにSwin TransformerとRT-DETRv2を統合した人物再識別システムである。動画フレームから複数の人物を自動検出し、各人物の512次元特徴ベクトルを抽出して外観ベース識別を行う。リアルタイム処理に対応し、人物の追跡と履歴管理機能を備える。

主要技術

RT-DETRv2 (Real-Time Detection Transformer v2)

RT-DETRv2は2024年に発表されたリアルタイム物体検出用Transformerアーキテクチャである[1]。従来のYOLOシリーズを凌駕する検出精度を維持しながら、リアルタイム処理を実現する。変形可能な注意機構による多スケール特徴抽出と、クエリ選択により高精度な人物検出を行う。COCO 2017データセットで事前学習されている。このプログラムでは、人物クラス(PERSON_CLASS_ID = 0)の検出に特化した処理を実行する。

Swin Transformer

Swin Transformerは2021年にMicrosoftが発表した階層的視覚Transformerである[2]。シフテッドウィンドウ自己注意機構を有している。局所ウィンドウ内での自己注意計算とウィンドウシフトによるグローバル情報交換を組み合わせることで、特徴抽出を行う。ImageNet事前学習済みモデルをバックボーンとして使用する。

Layumi's Person Re-ID Baseline

Layumi's Person Re-ID BaselineはPyTorchベースの人物再識別フレームワークである[3]。Market-1501データセットで事前学習されており、Rank@1=88.24%、mAP=70.68%の性能を達成する。BNNeck構造を含むClassBlockにより、512次元の識別用特徴ベクトルを生成する。

Market-1501データセット

Market-1501は人物再識別の標準ベンチマークデータセットである[4]。1501人の歩行者を6台のカメラで撮影した32,668枚の画像で構成される。清華大学のスーパーマーケット前で収集され、各人物が最低2台のカメラで撮影されている。Deformable Part Model(DPM)を使用した自動検出により生成されており、現実的な検出環境を模擬する。

技術的特徴

このプログラムは段階的処理を行う。第1段階でRT-DETRv2が人物検出を行い、信頼度閾値(PERSON_CONFIDENCE_THRESHOLD = 0.5)以上の人物領域を特定する。第2段階で各人物領域をクロップ (crop) し、Swin Transformerバックボーンを用いてLayumi ReIDモデルが512次元特徴ベクトルを抽出する。第3段階で空間追跡と外観特徴マッチングを組み合わせた統合追跡により人物IDを割り当てる。

特徴抽出には前処理として224×224ピクセルへのリサイズとImageNet正規化を適用する。ClassBlock内のBNNeck構造により識別性の高い特徴表現を学習する。人物間の類似度計算にはコサイン類似度とユークリッド距離を使用し、閾値ベースマッチング(REID_SIMILARITY_THRESHOLD = 0.5)により同一人物を判定する。

実装の特色

プログラムはGPU/CPU自動選択機能を備え、CUDA対応環境では高速処理を実現する。人物ID、検出信頼度、ReID類似度を日本語で画面表示する。履歴管理システムにより、検出した全人物の特徴ベクトル履歴をJSON形式で保存し、セッション間での人物識別精度向上を図る。

入力ソースとして動画ファイル、ウェブカメラ、サンプル動画に対応する。リアルタイム処理中は各人物に固有の色でバウンディングボックスを描画し、視覚的な区別を可能にする。処理結果は詳細なログとして保存され、フレーム番号、検出人数、特徴ベクトル統計、人物間類似度を含む包括的な分析データを提供する。

参考文献

[1] Lv, W., Zhao, Y., Chang, Q., Huang, K., Wang, G., & Liu, Y. (2024). RT-DETRv2: Improved Baseline with Bag-of-Freebies for Real-Time Detection Transformer. https://arxiv.org/abs/2407.17140

[2] Liu, Z., Lin, Y., Cao, Y., Hu, H., Wei, Y., Zhang, Z., Lin, S., & Guo, B. (2021). Swin Transformer: Hierarchical Vision Transformer using Shifted Windows. In Proceedings of the IEEE/CVF International Conference on Computer Vision (ICCV), 10012-10022. https://arxiv.org/abs/2103.14030

[3] Layumi/Person_reID_baseline_pytorch. GitHub repository. https://github.com/layumi/Person_reID_baseline_pytorch

[4] Zheng, L., Shen, L., Tian, L., Wang, S., Wang, J., & Tian, Q. (2015). Scalable Person Re-identification: A Benchmark. In Proceedings of the IEEE International Conference on Computer Vision (ICCV), 1116-1124.

ソースコード


# Layumi ReID + RT-DETRv2 統合人物再識別システム
# 統合技術: Layumi's Person Re-ID Baseline + Swin Transformer + RT-DETRv2
# 出典:
#   - Layumi ReID: https://github.com/layumi/Person_reID_baseline_pytorch
#   - Swin Transformer: Liu, Z., et al. (2021). Swin transformer: Hierarchical vision transformer using shifted windows.
#   - RT-DETRv2: W. Lv, et al. (2024). RT-DETRv2: Improved Baseline with Bag-of-Freebies for Real-Time Detection Transformer.
# 統合機能: RT-DETRv2による複数人物検出 + 各人物のLayumi ReID特徴抽出 + 外観ベース人物識別
# 学習済みモデル:
#   - Layumi ReID: Market-1501学習済み(751人分の人体特徴)
#   - Swin Transformer: ImageNet事前学習
#   - RT-DETRv2: COCO 2017学習済み(人物検出)
# 方式設計:
#   入力: 動画フレーム(ユーザ選択:動画ファイル/カメラ/サンプル動画)
#   出力: 複数人物の個別特徴ベクトル、人物間類似度、追跡結果、画面表示、ログ保存
#   処理手順: RT-DETRv2人物検出→各人物領域クロップ→Layumi ReID特徴抽出→外観ベース識別・追跡→結果表示
# 前準備:
# pip install -U torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu126
# pip install transformers opencv-python pillow requests gdown timm

import cv2
import tkinter as tk
from tkinter import filedialog
import urllib.request
import torch
import torch.nn as nn
import torchvision.transforms as transforms
from PIL import Image, ImageDraw, ImageFont
import timm
import time
from datetime import datetime
import os
import numpy as np
import json
from collections import defaultdict, deque
from transformers import RTDetrV2ForObjectDetection, RTDetrImageProcessor

# 統合システム設定値
PERSON_CONFIDENCE_THRESHOLD = 0.5  # 人物検出信頼度閾値
MIN_PERSON_SIZE = 20  # 最小人物サイズ(ピクセル)
PERSON_CLASS_ID = 0  # COCOデータセットにおけるPersonクラスのID
PERSON_TRACKING_THRESHOLD = 200  # 人物追跡のための距離閾値(ピクセル)
REID_SIMILARITY_THRESHOLD = 0.5  # ReID外観識別の類似度閾値

# gdownモジュールのインポート処理
try:
    import gdown
    GDOWN_AVAILABLE = True
except ImportError:
    GDOWN_AVAILABLE = False
    print("警告: gdownモジュールが見つかりません。pip install gdownを実行してください。")

# 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

# 日本語フォント設定
FONT_PATH = 'C:/Windows/Fonts/meiryo.ttc'
FONT_SIZE = 20
try:
    font_japanese = ImageFont.truetype(FONT_PATH, FONT_SIZE)
except:
    font_japanese = None
    print("日本語フォントの読み込みに失敗しました。英語表示になります。")

# グローバル変数
frame_count = 0
results_log = []
person_feature_history = defaultdict(list)  # 各人物の特徴ベクトル履歴
previous_persons = []
next_person_id = 0
HISTORY_FILE = 'person_reid_history.json'

def load_person_history():
    """開始時に人物履歴データを読み込み"""
    global person_feature_history, next_person_id

    if os.path.exists(HISTORY_FILE):
        print(f"履歴データファイルを読み込み中: {HISTORY_FILE}")
        with open(HISTORY_FILE, 'r', encoding='utf-8') as f:
            data = json.load(f)

        # 履歴データの復元
        for person_id_str, history_list in data.get('person_history', {}).items():
            person_id = int(person_id_str)
            for entry in history_list:
                person_feature_history[person_id].append({
                    'frame': entry['frame'],
                    'feature_vector': np.array(entry['feature_vector']),
                    'timestamp': entry['timestamp']
                })

        # 次のID番号を設定
        if person_feature_history:
            next_person_id = max(person_feature_history.keys()) + 1

        print(f"履歴データを読み込みました: {len(person_feature_history)}人分")
        for pid, hist in person_feature_history.items():
            print(f"  Person{pid}: {len(hist)}フレーム分の特徴履歴")
    else:
        print("新規セッション開始")

def save_person_history():
    """終了時に人物履歴データを保存"""
    if person_feature_history:
        print(f"履歴データファイルを保存中: {HISTORY_FILE}")
        # NumPy配列をリストに変換して保存
        history_data = {}
        for person_id, history_list in person_feature_history.items():
            history_data[str(person_id)] = []
            for entry in history_list:
                history_data[str(person_id)].append({
                    'frame': entry['frame'],
                    'feature_vector': entry['feature_vector'].tolist(),
                    'timestamp': entry['timestamp']
                })

        save_data = {
            'person_history': history_data,
            'total_persons': len(person_feature_history),
            'last_session': datetime.now().isoformat()
        }

        with open(HISTORY_FILE, 'w', encoding='utf-8') as f:
            json.dump(save_data, f, indent=2, ensure_ascii=False)

        print(f"履歴データを保存しました: {HISTORY_FILE}")
        total_entries = sum(len(hist) for hist in person_feature_history.values())
        print(f"  総人物数: {len(person_feature_history)}人")
        print(f"  総履歴エントリ数: {total_entries}件")

class ClassBlock(nn.Module):
    """BNNeck構造を含む分類ブロック
    参考: Luo, H., Gu, Y., Liao, X., Lai, S., & Jiang, W. (2019).
    Bag of tricks and a strong baseline for deep person re-identification."""
    def __init__(self, input_dim, class_num, droprate, relu=False, bnorm=True, linear=512, return_f=False):
        super(ClassBlock, self).__init__()
        self.return_f = return_f
        add_block = []
        if linear > 0:
            add_block += [nn.Linear(input_dim, linear)]
        else:
            linear = input_dim
        if bnorm:
            add_block += [nn.BatchNorm1d(linear)]  # BNNeck
        if relu:
            add_block += [nn.LeakyReLU(0.1)]
        if droprate > 0:
            add_block += [nn.Dropout(p=droprate)]
        add_block = nn.Sequential(*add_block)

        # Kaiming初期化
        for m in add_block.modules():
            classname = m.__class__.__name__
            if classname.find('Linear') != -1:
                nn.init.kaiming_normal_(m.weight, a=0, mode='fan_out')
                nn.init.constant_(m.bias, 0.0)
            elif classname.find('Conv') != -1:
                nn.init.kaiming_normal_(m.weight, a=0, mode='fan_in')
                if m.bias is not None:
                    nn.init.constant_(m.bias, 0.0)
            elif classname.find('BatchNorm') != -1:
                if m.affine:
                    nn.init.constant_(m.weight, 1.0)
                    nn.init.constant_(m.bias, 0.0)

        classifier = []
        classifier += [nn.Linear(linear, class_num)]
        classifier = nn.Sequential(*classifier)

        for m in classifier.modules():
            classname = m.__class__.__name__
            if classname.find('Linear') != -1:
                nn.init.normal_(m.weight, std=0.001)
                if m.bias is not None:
                    nn.init.constant_(m.bias, 0.0)

        self.linear_num = linear
        self.add_block = add_block
        self.classifier = classifier

    def forward(self, x):
        x = self.add_block(x)
        if self.return_f:
            f = x
            x = self.classifier(x)
            return [x, f]
        else:
            x = self.classifier(x)
            return x

def apply_global_pooling(x, avgpool1d, avgpool2d):
    """次元に応じた適切なGlobal Average Poolingを適用"""
    if x.dim() == 4:  # (batch_size, H, W, num_features) - Swin Transformer出力
        return x.mean(dim=[1, 2])  # (batch_size, num_features)
    elif x.dim() == 3:
        x = avgpool1d(x.permute((0, 2, 1)))
        return x.view(x.size(0), x.size(1))
    else:
        x = avgpool2d(x.permute((0, 3, 1, 2)))
        return x.view(x.size(0), x.size(1))

class ft_net_swin(nn.Module):
    """Layumi's ReID Baseline with Swin Transformer backbone"""
    def __init__(self, class_num=751, droprate=0.5, stride=2, circle=False, linear_num=512):
        super(ft_net_swin, self).__init__()
        model_ft = timm.create_model('swin_base_patch4_window7_224.ms_in1k', pretrained=True, drop_path_rate=0.2)
        model_ft.head = nn.Sequential()
        self.model = model_ft
        self.circle = circle
        self.avgpool1d = nn.AdaptiveAvgPool1d(1)
        self.avgpool2d = nn.AdaptiveAvgPool2d((1,1))
        self.classifier = ClassBlock(1024, class_num, droprate, linear=linear_num, return_f=circle)

    def forward(self, x):
        x = self.model.forward_features(x)
        x = apply_global_pooling(x, self.avgpool1d, self.avgpool2d)
        x = self.classifier(x)
        return x

# Layumi ReIDモデルの初期化
print('Layumi ReID + Swin Transformerモデル読み込み中...')
model = ft_net_swin(class_num=751, linear_num=512, circle=False).to(device)

# 学習済みモデルのダウンロードとロード
model_path = 'layumi_swin_pretrained.pth'
if not os.path.exists(model_path):
    if not GDOWN_AVAILABLE:
        print("gdownが利用できないため、学習済みモデルをダウンロードできません。")
        model_path = None
    else:
        print("Layumi's ReID Baseline (Market-1501学習済み) モデルをダウンロード中...")
        try:
            file_id = '1XVEYb0TN2SbBYOqf8SzazfYZlpH9CxyE'
            gdown.download(f'https://drive.google.com/uc?id={file_id}', model_path, quiet=False)
            print("ダウンロード完了(751人分の人体特徴を学習済み)")
        except Exception as e:
            print(f"ダウンロードエラー: {e}")
            print("ImageNet事前学習のみで実行します")
            model_path = None

if model_path and os.path.exists(model_path):
    try:
        checkpoint = torch.load(model_path, map_location=device)
        model.load_state_dict(checkpoint, strict=False)
        print("Layumi's ReID Baseline (Market-1501) モデルをロード完了")
    except Exception as e:
        print(f"モデルロードエラー: {e}")
        print("ImageNet事前学習のみで実行")

model.eval()

# RT-DETRv2モデル読み込み
print('RT-DETRv2モデル読み込み中...')
try:
    rtdetr_model = RTDetrV2ForObjectDetection.from_pretrained("PekingU/rtdetr_v2_r50vd")
    rtdetr_processor = RTDetrImageProcessor.from_pretrained("PekingU/rtdetr_v2_r50vd")
    rtdetr_model.to(device)
    rtdetr_model.eval()
    print('RT-DETRv2モデル読み込み完了')
except Exception as e:
    print(f'RT-DETRv2モデル読み込み失敗: {e}')
    exit(1)

# ReID用画像変換
transform = transforms.Compose([
    transforms.Resize((224, 224), interpolation=Image.BICUBIC),
    transforms.ToTensor(),
    transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])
])

def calculate_cosine_similarity(feat1, feat2):
    """コサイン類似度の計算"""
    feat1_norm = feat1 / np.linalg.norm(feat1)
    feat2_norm = feat2 / np.linalg.norm(feat2)
    return np.dot(feat1_norm, feat2_norm)

def calculate_euclidean_distance(feat1, feat2):
    """ユークリッド距離の計算"""
    return np.linalg.norm(feat1 - feat2)

def calculate_person_distance(person1, person2):
    """2つの人物間の中心点距離を計算"""
    x1_center = (person1['bbox'][0] + person1['bbox'][2]) / 2
    y1_center = (person1['bbox'][1] + person1['bbox'][3]) / 2
    x2_center = (person2['bbox'][0] + person2['bbox'][2]) / 2
    y2_center = (person2['bbox'][1] + person2['bbox'][3]) / 2

    distance = np.sqrt((x1_center - x2_center)**2 + (y1_center - y2_center)**2)

    # サイズ変化も考慮
    area1 = (person1['bbox'][2] - person1['bbox'][0]) * (person1['bbox'][3] - person1['bbox'][1])
    area2 = (person2['bbox'][2] - person2['bbox'][0]) * (person2['bbox'][3] - person2['bbox'][1])
    size_ratio = abs(area1 - area2) / max(area1, area2)

    if size_ratio > 0.5:
        distance *= (1 + size_ratio)

    return distance

def detect_persons_rtdetr(frame):
    """RT-DETRv2による複数人物検出"""
    frame_pil = Image.fromarray(cv2.cvtColor(frame, cv2.COLOR_BGR2RGB))
    inputs = rtdetr_processor(images=frame_pil, return_tensors='pt')
    inputs = {k: v.to(device) for k, v in inputs.items()}

    with torch.no_grad():
        outputs = rtdetr_model(**inputs)

    target_sizes = torch.tensor([frame.shape[:2]]).to(device)
    results = rtdetr_processor.post_process_object_detection(
        outputs, target_sizes=target_sizes, threshold=PERSON_CONFIDENCE_THRESHOLD
    )[0]

    persons = []
    if len(results['labels']) > 0:
        boxes = results['boxes'].cpu().numpy()
        scores = results['scores'].cpu().numpy()
        labels = results['labels'].cpu().numpy()

        person_indices = labels == PERSON_CLASS_ID

        if np.any(person_indices):
            person_boxes = boxes[person_indices]
            person_scores = scores[person_indices]

            sorted_indices = np.argsort(person_scores)[::-1]
            person_boxes = person_boxes[sorted_indices]
            person_scores = person_scores[sorted_indices]

            h, w = frame.shape[:2]
            for box, score in zip(person_boxes, person_scores):
                x1, y1, x2, y2 = int(box[0]), int(box[1]), int(box[2]), int(box[3])

                # 座標範囲の修正
                x1 = max(0, min(x1, w-1))
                y1 = max(0, min(y1, h-1))
                x2 = max(x1+1, min(x2, w))
                y2 = max(y1+1, min(y2, h))

                if (x2 - x1) >= MIN_PERSON_SIZE and (y2 - y1) >= MIN_PERSON_SIZE:
                    persons.append({
                        'bbox': (x1, y1, x2, y2),
                        'confidence': float(score)
                    })

    return persons

def extract_reid_features(frame, bbox):
    """指定された人物領域からReID特徴ベクトルを抽出"""
    x1, y1, x2, y2 = bbox

    h, w = frame.shape[:2]
    padding = 20
    crop_x1 = max(0, x1 - padding)
    crop_y1 = max(0, y1 - padding)
    crop_x2 = min(w, x2 + padding)
    crop_y2 = min(h, y2 + padding)

    # 座標の妥当性を保証
    crop_x2 = max(crop_x1 + 10, crop_x2)
    crop_y2 = max(crop_y1 + 10, crop_y2)

    cropped_person = frame[crop_y1:crop_y2, crop_x1:crop_x2]

    pil_image = Image.fromarray(cv2.cvtColor(cropped_person, cv2.COLOR_BGR2RGB))
    input_tensor = transform(pil_image).unsqueeze(0).to(device)

    with torch.no_grad():
        outputs = model(input_tensor)
        if isinstance(outputs, list):
            features = outputs[1]
        else:
            features = outputs

        feature_vector = features.cpu().numpy().flatten()

    return feature_vector

# 履歴の平均を使った改良版マッチング関数
def find_matching_person_by_appearance(new_feature, person_feature_history):
    best_match_id = None
    best_similarity = -1

    for person_id, history in person_feature_history.items():
        if len(history) > 0:
            # 最新5フレームの特徴ベクトルを平均化
            recent_features = [h['feature_vector'] for h in history[-5:]]
            avg_feature = np.mean(recent_features, axis=0)
            similarity = calculate_cosine_similarity(new_feature, avg_feature)

            if similarity > best_similarity:
                best_similarity = similarity
                best_match_id = person_id

    return best_match_id, best_similarity

def assign_person_ids_with_reid(detected_persons, frame):
    """ReID特徴を使用した高精度人物ID割り当て"""
    global previous_persons, next_person_id

    # 各人物のReID特徴抽出
    for person in detected_persons:
        feature_vector = extract_reid_features(frame, person['bbox'])
        person['feature_vector'] = feature_vector

    if not previous_persons:
        # 初回検出時
        for i, person in enumerate(detected_persons):
            person['person_id'] = next_person_id
            next_person_id += 1
    else:
        # Phase 1: 空間追跡による対応付け
        assigned_ids = set()
        for person in detected_persons:
            best_match_id = None
            min_distance = float('inf')

            for prev_person in previous_persons:
                if prev_person['person_id'] not in assigned_ids:
                    distance = calculate_person_distance(person, prev_person)
                    if distance < PERSON_TRACKING_THRESHOLD and distance < min_distance:
                        min_distance = distance
                        best_match_id = prev_person['person_id']

            if best_match_id is not None:
                person['person_id'] = best_match_id
                assigned_ids.add(best_match_id)
            else:
                person['person_id'] = None  # 一時的に未割り当て

        # Phase 2: ReID外観特徴による対応付け
        for person in detected_persons:
            if person['person_id'] is None:
                match_id, similarity = find_matching_person_by_appearance(
                    person['feature_vector'], person_feature_history
                )

                if match_id is not None and match_id not in assigned_ids:
                    person['person_id'] = match_id
                    person['reid_similarity'] = similarity
                    assigned_ids.add(match_id)
                else:
                    person['person_id'] = next_person_id
                    next_person_id += 1

    previous_persons = detected_persons.copy()
    return detected_persons

def save_person_features(person_features):
    """人物の特徴ベクトルを履歴に保存"""
    for person in person_features:
        person_id = person['person_id']
        person_feature_history[person_id].append({
            'frame': frame_count,
            'feature_vector': person['feature_vector'],
            'timestamp': time.time()
        })
        # 履歴制限を撤廃 - ReID精度向上のため全履歴を保持

def compare_persons_similarity(person_features):
    """現在フレーム内の人物間類似度比較"""
    similarities = []

    for i, person1 in enumerate(person_features):
        for j, person2 in enumerate(person_features[i+1:], i+1):
            cosine_sim = calculate_cosine_similarity(
                person1['feature_vector'],
                person2['feature_vector']
            )
            similarities.append({
                'person1_id': person1['person_id'],
                'person2_id': person2['person_id'],
                'similarity': cosine_sim
            })

    return similarities

def get_person_color(person_id):
    """Person IDに基づいて区別しやすい色を生成(BGR形式)"""
    # HSV色空間で色相を分散させて区別しやすい色を生成
    hue = (person_id * 137) % 360  # 黄金角を使って色相を分散
    saturation = 255
    value = 255

    # HSVをBGRに変換
    hsv = np.uint8([[[hue // 2, saturation, value]]])  # OpenCVは色相が0-179
    bgr = cv2.cvtColor(hsv, cv2.COLOR_HSV2BGR)[0][0]
    return tuple(map(int, bgr))
    """OpenCV画像に日本語テキストを描画"""
    if font_japanese is None:
        cv2.putText(img, text, position, cv2.FONT_HERSHEY_SIMPLEX, 0.6, 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_japanese, fill=color[::-1])
    return cv2.cvtColor(np.array(img_pil), cv2.COLOR_RGB2BGR)

def video_frame_processing(frame):
    """統合人物再識別システムのメイン処理"""
    global frame_count

    current_time = time.time()
    frame_count += 1

    # Phase 1: RT-DETRv2による人物検出
    detected_persons = detect_persons_rtdetr(frame)

    if not detected_persons:
        result = f"フレーム{frame_count}: 人物検出なし"
        cv2.putText(frame, "No Person Detected", (10, 30), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 0, 255), 2)
        return frame, result, current_time

    # Phase 2: ReID特徴抽出と人物ID割り当て
    detected_persons = assign_person_ids_with_reid(detected_persons, frame)

    # Phase 3: 特徴ベクトル保存
    save_person_features(detected_persons)

    # Phase 4: 人物間類似度比較
    similarities = compare_persons_similarity(detected_persons)

    # 画面表示
    result_texts = []

    for person in detected_persons:
        x1, y1, x2, y2 = person['bbox']
        person_id = person['person_id']

        # Person ID別に色を取得
        person_color = get_person_color(person_id)

        # バウンディングボックス描画
        cv2.rectangle(frame, (x1, y1), (x2, y2), person_color, 2)

        # Person ID表示
        cv2.putText(frame, f"Person{person_id}", (x1, y1-10),
                   cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 255, 0), 2)

        # 信頼度表示
        cv2.putText(frame, f"Det:{person['confidence']:.2f}", (x1, y1-30),
                   cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 255, 0), 1)

        # ReID類似度表示(外観マッチングされた場合)
        if 'reid_similarity' in person:
            cv2.putText(frame, f"ReID:{person['reid_similarity']:.2f}", (x1, y1-50),
                       cv2.FONT_HERSHEY_SIMPLEX, 0.5, (255, 0, 0), 1)

        # 特徴ベクトル統計
        feat_mean = np.mean(person['feature_vector'])
        feat_norm = np.linalg.norm(person['feature_vector'])
        result_texts.append(f"Person{person_id}(検出信頼度:{person['confidence']:.3f}, 特徴平均:{feat_mean:.3f}, L2ノルム:{feat_norm:.3f})")

    # 人物間類似度表示
    if similarities:
        similarity_texts = []
        for sim in similarities:
            similarity_texts.append(f"Person{sim['person1_id']}-Person{sim['person2_id']}類似度:{sim['similarity']:.3f}")
        result_texts.extend(similarity_texts)

    # 全体統計表示
    cv2.putText(frame, f"Detected: {len(detected_persons)} person(s)",
               (10, 30), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 255, 0), 2)

    result = f"フレーム{frame_count}: " + " | ".join(result_texts)
    return frame, result, current_time

print("\n" + "="*80)
print("Layumi ReID + RT-DETRv2 統合人物再識別システム")
print("="*80)
print(f"人物検出信頼度閾値: {PERSON_CONFIDENCE_THRESHOLD}")
print(f"最小人物サイズ: {MIN_PERSON_SIZE}px")
print(f"人物追跡閾値: {PERSON_TRACKING_THRESHOLD}px")
print(f"ReID類似度閾値: {REID_SIMILARITY_THRESHOLD}")
print("="*80)

# 履歴データ読み込み
load_person_history()

print("\n入力選択:")
print("0: 動画ファイル")
print("1: カメラ")
print("2: サンプル動画")

choice = input("選択: ")

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("処理開始。ESCキーまたはqキーで終了します。")

try:
    while True:
        ret, frame = cap.read()
        if not ret:
            print("フレーム読み取り終了またはエラー")
            break

        # 統合人物再識別システム処理
        processed_frame, result, timestamp = video_frame_processing(frame)

        # 結果をログに追加
        results_log.append({
            'timestamp': timestamp,
            'frame_number': frame_count,
            'result': result
        })

        # コンソール出力
        print(result)

        # 画面表示
        cv2.imshow('Layumi ReID + RT-DETRv2 統合人物再識別システム', processed_frame)

        # ESCキー(27)またはqキーで終了
        key = cv2.waitKey(1) & 0xFF
        if key == 27 or key == ord('q'):
            break

except KeyboardInterrupt:
    print("\nプログラム中断(Ctrl+C)")

finally:
    # リソース解放
    cap.release()
    cv2.destroyAllWindows()

    # 人物履歴データ保存
    save_person_history()

    # 結果ログの保存
    if results_log:
        timestamp_str = datetime.now().strftime("%Y%m%d_%H%M%S")
        results_filename = f"integrated_person_reid_results_{timestamp_str}.json"

        # 人物履歴統計
        person_stats = {}
        for person_id, history in person_feature_history.items():
            person_stats[f"Person{person_id}"] = {
                'total_detections': len(history),
                'first_appearance_frame': history[0]['frame'] if history else None,
                'last_appearance_frame': history[-1]['frame'] if history else None
            }

        with open(results_filename, 'w', encoding='utf-8') as f:
            json.dump({
                'system_info': {
                    'model': 'Layumi ReID + Swin Transformer + RT-DETRv2',
                    'total_frames': frame_count,
                    'processing_time': time.time() - results_log[0]['timestamp'] if results_log else 0,
                    'feature_dimension': 512,
                    'detection_threshold': PERSON_CONFIDENCE_THRESHOLD,
                    'reid_threshold': REID_SIMILARITY_THRESHOLD,
                    'device': str(device)
                },
                'person_statistics': person_stats,
                'frame_results': results_log
            }, f, indent=2, ensure_ascii=False)

        print(f"\n=== 処理完了 ===")
        print(f"総フレーム数: {frame_count}")
        print(f"結果ログ保存: {results_filename}")
        print(f"検出された人物数: {len(person_feature_history)}")

        # 統計サマリー出力
        if person_feature_history:
            print("\n=== 人物別統計 ===")
            for person_id, history in person_feature_history.items():
                print(f"Person{person_id}: {len(history)}回検出, "
                      f"フレーム{history[0]['frame']}-{history[-1]['frame']}")

            # 全体的な類似度統計
            all_similarities = []
            for person_id, history in person_feature_history.items():
                if len(history) > 1:
                    for i in range(1, len(history)):
                        similarity = calculate_cosine_similarity(
                            history[i-1]['feature_vector'],
                            history[i]['feature_vector']
                        )
                        all_similarities.append(similarity)

            if all_similarities:
                avg_similarity = np.mean(all_similarities)
                print(f"\n同一人物の平均類似度: {avg_similarity:.4f}")
                print(f"類似度範囲: {min(all_similarities):.4f} - {max(all_similarities):.4f}")

        print("\n=== システム解析完了 ===")
        print("統合技術:")
        print("- RT-DETRv2: 複数人物検出(COCO学習済み)")
        print("- Layumi ReID: 512次元特徴抽出(Market-1501学習済み)")
        print("- Swin Transformer: 階層的視覚特徴バックボーン")
        print("- 統合追跡: 空間追跡 + 外観識別による高精度人物再識別")