ゲーム画面でのアイテム認識と戦略決定

【概要】 物体検出AI「YOLO11」を用いたゲームAI実装を体験する。YOLO11を活用し、ゲーム画面内の物体をリアルタイムで検出・分析するシステムを構築する。プレイヤー、敵、アイテムの認識から戦略決定まで、AIがゲーム状況を理解する過程を実際のコードで学習する。Windows環境での実行手順、プログラムコード、実験アイデアを含む。


目次

1. 概要

主要技術:YOLO11(You Only Look Once version 11)

論文:「YOLO11: An Overview of the Key Architectural Enhancements」(2024年)

新規性・特徴:C3k2ブロックによる高速推論と高精度を実現し、リアルタイム物体検出において従来モデルを上回る性能を達成。C3k2ブロックは、畳み込み層とクロスステージ部分接続を組み合わせたアーキテクチャで、計算量を削減しながら特徴抽出能力を維持する。ゲームAI、監視システム、自動運転などに応用可能。

リアルタイムゲームでは高速な状況認識が必要であり、YOLO11の高速推論能力がゲームAIの意思決定に適している。

体験価値:実際のゲーム画面を模擬した環境でYOLO11の物体検出能力を体験し、AIがどのようにゲーム状況を認識・分析するかを学習できる。

利用可能なYOLO11モデル

YOLO11には以下のモデルが利用可能である:

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

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

以下のコマンドを実行する(管理者権限のコマンドプロンプトで実行)。


pip install ultralytics opencv-python numpy matplotlib japanize-matplotlib
YOLO11ゲームAI Visionデモ

3. プログラムコード

YOLO11は実世界の物体を検出するため、ゲーム要素を既存の検出可能クラスにマッピングする。personクラスは位置で役割を区別し、日用品クラスをアイテムとして活用する。


# YOLO11ゲームAI Visionデモ
# 特徴技術名: YOLO11 (You Only Look Once version 11)
# 出典: Ultralytics YOLO11 Documentation, https://docs.ultralytics.com/models/yolo11/ (2024)
# 特徴機能: C3k2ブロックによる高速リアルタイム物体検出。単一推論で複数物体の位置とクラスを同時検出
# 学習済みモデル: yolo11n.pt (Nano版、約2.6MB、COCOデータセット80クラス対応、https://github.com/ultralytics/assets/releases/)
# 方式設計:
#   - 関連利用技術: OpenCV (画像処理・表示)、NumPy (数値計算)、Matplotlib (結果可視化)、japanize-matplotlib (日本語表示)
#   - 入力と出力: 入力: プログラム内で生成されるゲーム風シミュレーション画像、出力: 物体検出結果の可視化画像とテキスト情報
#   - 処理手順: 1.ゲームシーン生成 2.YOLO11による物体検出 3.検出結果の解析 4.ゲーム戦略決定 5.結果可視化
#   - 前処理、後処理: 前処理: なし(RGB画像をそのまま入力)、後処理: 検出結果のフィルタリング(信頼度による)
#   - 追加処理: ゲームロジックによる物体間距離計算と戦略決定アルゴリズム
#   - 調整を必要とする設定値: DANGER_DISTANCE (危険判定距離、デフォルト100ピクセル)
# 将来方策: 検出物体間の最適距離を自動学習するため、複数回の実行結果から統計的に最適値を算出する機能
# その他の重要事項: デモ用のシミュレーション環境のため、実際のゲーム画面での動作には調整が必要
# 前準備:
#   - pip install ultralytics opencv-python numpy matplotlib japanize-matplotlib

import cv2
import numpy as np
import time
import os

try:
    from ultralytics import YOLO
except ImportError:
    print('Error: ultralyticsライブラリがインストールされていません')
    print('pip install ultralytics を実行してください')
    exit()

try:
    import matplotlib.pyplot as plt
    import japanize_matplotlib
except ImportError:
    print('Error: matplotlib または japanize-matplotlib がインストールされていません')
    print('pip install matplotlib japanize-matplotlib を実行してください')
    exit()

# 定数定義
RANDOM_SEED = 42
PLAYER_COLOR = (0, 255, 0)
ENEMY_COLOR = (0, 0, 255)
ITEM_COLOR = (255, 255, 0)
VEHICLE_COLOR = (128, 128, 128)
TEXT_COLOR = (255, 255, 255)
DANGER_DISTANCE = 100  # 調整可能な設定値:危険判定距離(ピクセル)
ATTACK_DISTANCE = 200  # 攻撃判定距離(ピクセル)
WIN_SCORE = 50  # 勝利スコア
WIN_SURVIVAL_FRAMES = 5  # 勝利に必要な生存フレーム数
DANGER_COUNT_LIMIT = 3  # 危険状態の許容回数

# ゲームオブジェクトの設定
SCENE_WIDTH = 640  # シーン幅
SCENE_HEIGHT = 480  # シーン高さ
PLAYER_POS = (100, 300)  # プレイヤー初期位置
PLAYER_SIZE = (50, 100)  # プレイヤーサイズ(幅、高さ)
ENEMY_BASE_POS = (400, 250)  # 敵の基準位置
ENEMY_SIZE = (50, 100)  # 敵のサイズ(幅、高さ)
ENEMY_MOVE_SPEED = 50  # 敵の移動速度(ピクセル/秒)
ITEM_BASE_POS = (300, 200)  # アイテムの基準位置
ITEM_RADIUS = 20  # アイテムの半径
ITEM_AMPLITUDE = 50  # アイテムの振幅
ITEM_FREQUENCY = 0.5  # アイテムの振動周波数(Hz)
VEHICLE_POS = (500, 380)  # 車両の位置
VEHICLE_SIZE = (80, 40)  # 車両のサイズ(幅、高さ)

# 表示設定
FONT_SCALE = 0.5  # 小さいテキスト用
FONT_SCALE_LARGE = 0.8  # 大きいテキスト用
FONT_SCALE_MEDIUM = 0.6  # 中サイズテキスト用
FONT_THICKNESS = 1  # 通常のテキスト
FONT_THICKNESS_BOLD = 2  # 太字テキスト
FRAME_DELAY = 30  # フレーム表示待機時間(ミリ秒)

# 乱数シード設定
np.random.seed(RANDOM_SEED)


def draw_text(img, text, pos, scale=FONT_SCALE, color=TEXT_COLOR, thickness=FONT_THICKNESS):
    """テキスト描画の共通関数"""
    cv2.putText(img, text, pos, cv2.FONT_HERSHEY_SIMPLEX, scale, color, thickness)


class GameAIVision:
    def __init__(self):
        print('YOLO11モデルを初期化中...')
        try:
            self.model = YOLO('yolo11n.pt')
            print('YOLO11モデルの初期化完了')
        except Exception as e:
            print(f'Error: YOLO11モデルの初期化に失敗しました: {e}')
            exit()

        self.frame_count = 0
        self.score = 0
        self.survival_frames = 0
        self.danger_count = 0
        self.results_log = []
        self.last_print_time = time.time()
        self.start_time = time.time()

    def create_game_scene(self):
        scene = np.ones((SCENE_HEIGHT, SCENE_WIDTH, 3), dtype=np.uint8) * 50
        elapsed_time = time.time() - self.start_time

        # プレイヤーキャラクター
        x1, y1 = PLAYER_POS
        x2, y2 = x1 + PLAYER_SIZE[0], y1 + PLAYER_SIZE[1]
        cv2.rectangle(scene, (x1, y1), (x2, y2), PLAYER_COLOR, -1)
        draw_text(scene, 'Player', (x1 - 10, y1 - 10))

        # 敵キャラクター(時間ベースの動き)
        enemy_x = ENEMY_BASE_POS[0] + int(elapsed_time * ENEMY_MOVE_SPEED) % 100
        enemy_y = ENEMY_BASE_POS[1]
        cv2.rectangle(scene, (enemy_x, enemy_y),
                     (enemy_x + ENEMY_SIZE[0], enemy_y + ENEMY_SIZE[1]),
                     ENEMY_COLOR, -1)
        draw_text(scene, 'Enemy', (enemy_x - 10, enemy_y - 10))

        # アイテム(時間ベースの動き)
        item_x = ITEM_BASE_POS[0]
        item_y = ITEM_BASE_POS[1] + int(np.sin(elapsed_time * 2 * np.pi * ITEM_FREQUENCY) * ITEM_AMPLITUDE)
        cv2.circle(scene, (item_x, item_y), ITEM_RADIUS, ITEM_COLOR, -1)
        draw_text(scene, 'Item', (item_x - 20, item_y - 20))

        # 車両
        x, y = VEHICLE_POS
        pts = np.array([[x, y], [x + VEHICLE_SIZE[0], y],
                       [x + VEHICLE_SIZE[0], y + VEHICLE_SIZE[1]],
                       [x, y + VEHICLE_SIZE[1]]], np.int32)
        cv2.fillPoly(scene, [pts], VEHICLE_COLOR)
        draw_text(scene, 'Vehicle', (x - 10, y - 10))

        return scene

    def detect_objects(self, frame):
        results = self.model(frame, verbose=False)
        detections = []

        for r in results:
            boxes = r.boxes
            if boxes is not None:
                for box in boxes:
                    x1, y1, x2, y2 = box.xyxy[0].cpu().numpy()
                    conf = box.conf[0].cpu().numpy()
                    cls = int(box.cls[0].cpu().numpy())
                    class_name = self.model.names[cls]

                    detections.append({
                        'bbox': [int(x1), int(y1), int(x2), int(y2)],
                        'confidence': float(conf),
                        'class': class_name,
                        'center': [(x1 + x2) / 2, (y1 + y2) / 2]
                    })

        return detections

    def analyze_game_state(self, detections):
        game_state = {
            'player_pos': None,
            'enemies': [],
            'items': [],
            'vehicles': [],
            'strategy': '',
            'game_status': 'PLAYING'
        }

        # 検出結果の分類
        for det in detections:
            center = det['center']
            cls = det['class']

            if cls == 'person':
                if center[0] < 200:  # 左側をプレイヤーと仮定
                    game_state['player_pos'] = center
                else:
                    game_state['enemies'].append(center)
            elif cls in ['bottle', 'cup', 'bowl']:
                game_state['items'].append(center)
            elif cls in ['car', 'truck', 'bus']:
                game_state['vehicles'].append(center)

        # 戦略決定とスコア計算
        if game_state['player_pos'] and game_state['enemies']:
            enemy_pos = game_state['enemies'][0]
            distance = np.sqrt((enemy_pos[0] - game_state['player_pos'][0])**2 +
                             (enemy_pos[1] - game_state['player_pos'][1])**2)

            if distance < DANGER_DISTANCE:
                self.danger_count += 1
                game_state['strategy'] = 'DANGER: 敵が近すぎます'
                if self.danger_count >= DANGER_COUNT_LIMIT:
                    game_state['game_status'] = 'ゲームオーバー'
            elif distance < ATTACK_DISTANCE:
                game_state['strategy'] = 'ATTACK: 敵が接近中'
                self.danger_count = 0
            else:
                game_state['strategy'] = 'EXPLORE: アイテム探索'
                self.danger_count = 0
                self.survival_frames += 1  # 安全な距離の時のみカウント
        elif game_state['items']:
            game_state['strategy'] = 'COLLECT: アイテム発見'
            self.score += 10
            # アイテム収集時は生存フレーム数を増加させない
        else:
            game_state['strategy'] = 'PATROL: 脅威なし'
            self.survival_frames += 1  # 脅威がない時もカウント

        # 勝利判定
        if self.score >= WIN_SCORE or self.survival_frames >= WIN_SURVIVAL_FRAMES:
            game_state['game_status'] = '勝利'

        return game_state

    def visualize_results(self, frame, detections, game_state):
        result_frame = frame.copy()

        # 検出枠を描画
        for det in detections:
            x1, y1, x2, y2 = det['bbox']
            color = PLAYER_COLOR if det['class'] == 'person' and det['center'][0] < 200 else ENEMY_COLOR
            cv2.rectangle(result_frame, (x1, y1), (x2, y2), color, 2)
            label = f"{det['class']} {det['confidence']:.2f}"
            draw_text(result_frame, label, (x1, y1-10), FONT_SCALE, color, FONT_THICKNESS_BOLD)

        # ゲーム情報を表示
        draw_text(result_frame, f"Strategy: {game_state['strategy']}",
                 (10, 30), FONT_SCALE_LARGE, TEXT_COLOR, FONT_THICKNESS_BOLD)
        draw_text(result_frame, f"Score: {self.score} | Survival: {self.survival_frames}",
                 (10, 55), FONT_SCALE_MEDIUM, (0, 255, 255), FONT_THICKNESS_BOLD)
        draw_text(result_frame, f"Status: {game_state['game_status']}",
                 (10, 80), FONT_SCALE_MEDIUM, (255, 255, 0), FONT_THICKNESS_BOLD)

        # 統計情報
        info_text = f"Frame: {self.frame_count} | Objects: {len(detections)}"
        draw_text(result_frame, info_text, (10, 460), FONT_SCALE_MEDIUM, TEXT_COLOR)

        return result_frame

    def save_results(self):
        try:
            with open('result.txt', 'w', encoding='utf-8') as f:
                f.write('YOLO11リアルタイム物体検出デモ - 実行結果\n')
                f.write('=' * 50 + '\n')
                for log in self.results_log:
                    f.write(log + '\n')
                f.write(f'\n最終スコア: {self.score}点\n')
                f.write(f'最終生存フレーム数: {self.survival_frames}フレーム\n')
            print('result.txtに保存しました')
        except Exception as e:
            print(f'Error: 結果の保存に失敗しました: {e}')

    def run_demo(self, num_frames=30):
        print('YOLO11を使用したゲームAI Visionデモを開始します...')
        print('操作方法: qキーで終了')
        print('=' * 50)

        # プログラムの概要表示
        print('\n【プログラム概要】')
        print('YOLO11による物体検出を使用したゲーム画面解析デモ')
        print('シミュレーション環境でゲームAIの戦略決定を実演します')
        print('\n【ゲームルール】')
        print('- プレイヤー(緑)は敵(赤)を避けながらアイテム(黄)を収集')
        print(f'- {WIN_SCORE}ポイント獲得または{WIN_SURVIVAL_FRAMES}フレーム生存で勝利')
        print(f'- 敵との距離が{DANGER_DISTANCE}ピクセル未満になると危険状態')
        print('=' * 50 + '\n')

        # Matplotlib用の図を準備
        fig, axes = plt.subplots(2, 3, figsize=(15, 10))
        axes = axes.flatten()
        saved_frames = []

        for i in range(num_frames):
            self.frame_count = i

            # ゲームシーンを生成
            scene = self.create_game_scene()

            # 物体検出
            start_time = time.time()
            detections = self.detect_objects(scene)
            inference_time = (time.time() - start_time) * 1000

            # ゲーム状態分析
            game_state = self.analyze_game_state(detections)

            # 結果表示
            result = self.visualize_results(scene, detections, game_state)

            # OpenCVウィンドウでリアルタイム表示
            cv2.imshow('YOLO11 Game AI Vision Demo', result)

            # 1秒間隔でprint出力
            current_time = time.time()
            if current_time - self.last_print_time >= 1.0:
                log_text = f'フレーム {i}: 推論時間 {inference_time:.2f}ms, スコア {self.score}点, 戦略: {game_state["strategy"]}'
                print(log_text)
                self.results_log.append(log_text)
                self.last_print_time = current_time

            # Matplotlib用に最初の6フレームを保存
            if i < 6:
                saved_frames.append((result.copy(), game_state['strategy']))

            # キー入力待機
            if cv2.waitKey(FRAME_DELAY) & 0xFF == ord('q'):
                print('\nユーザーによる中断')
                break

            # ゲーム終了判定
            if game_state['game_status'] in ['勝利', 'ゲームオーバー']:
                print(f'\n*** {game_state["game_status"]} ***')
                print(f'最終スコア: {self.score}点')
                self.results_log.append(f'\n*** {game_state["game_status"]} ***')
                self.results_log.append(f'最終スコア: {self.score}点')
                break

        cv2.destroyAllWindows()

        # Matplotlibで結果を表示
        for idx, (frame, strategy) in enumerate(saved_frames):
            axes[idx].imshow(cv2.cvtColor(frame, cv2.COLOR_BGR2RGB))
            axes[idx].set_title(f'フレーム {idx}: {strategy}')
            axes[idx].axis('off')

        # 未使用のサブプロットを非表示
        for j in range(len(saved_frames), 6):
            axes[j].axis('off')

        plt.tight_layout()
        plt.savefig('game_ai_yolo11_results.png', dpi=150)
        print('\n結果を game_ai_yolo11_results.png に保存しました')

        # 結果をテキストファイルに保存
        self.save_results()

        # 結果表示
        print('\n【実行結果の見方】')
        print('- 各フレームでYOLO11が物体を検出し、ゲーム戦略を決定')
        print('- 緑枠:プレイヤー、赤枠:敵または検出物')
        print('- OpenCVウィンドウでリアルタイム表示')
        print('- result.txtに詳細ログを保存')

        plt.show()


# メイン処理
ai = GameAIVision()
ai.run_demo()

4. 使用方法と実行結果の理解

  1. 上記のプログラムを実行する
  2. 初回実行時はYOLO11モデル(yolo11n.pt)が自動的にダウンロードされる。実行結果として、5フレームのゲーム画面解析結果が表示され、game_ai_yolo11_results.pngとして保存される。

    推論時間はAIが1フレームを処理する時間で、ゲームでは33ミリ秒以下(30FPS)が理想的である。これより遅いとゲームの反応性が低下する。

5. 実験・探求のアイデア

実験要素

  1. ゲームパラメータの調整:DANGER_DISTANCE、ATTACK_DISTANCE、WIN_SCOREなどの定数を変更し、ゲームの難易度とAI戦略の変化を観察
  2. 物体配置の変更:create_game_scene()メソッド内の座標を変更し、異なるゲームレイアウトでの検出精度を確認
  3. 検出クラスの拡張:'bottle', 'cup', 'bowl'以外のクラスをアイテムとして認識させ、検出の多様性を確認

体験・実験・探求のアイデア

  1. 動的シーン対応:np.roll()の値を大きくして高速移動をシミュレートし、物体追跡の限界を探る
  2. マルチオブジェクト戦略:複数の敵やアイテムを配置し、優先順位付けアルゴリズムを実装。最適な意思決定戦略を探求
  3. カスタムクラスの活用:YOLO11が検出可能な80クラスから、ゲームに適した新しい要素(動物、食べ物、道具など)を追加し、より複雑なゲームシナリオを構築