---
# System prepended metadata

title: AI Reflex
tags: [隨機森林, OpenCV, 機器人學習]

---

![c6ce8d0b9a7b28f9c2dee8171da98b8f ](https://hackmd.io/_uploads/rkJ--Z1xex.jpg)


# 簡介
這是結合openCV與機器學習的猜拳遊戲，透過鏡頭辨識玩家手勢，並使用隨機森林模型進行判斷與對戰。系統包含訓練與對戰兩階段，支援三種難度，提供即時互動與AI對戰體驗。
# 需下載的東東
* 下載
```
!pip install opencv-python # 下載opencv
!pip install numpy # 下載numpy
!pip install scikit-learn # 下載sklearn
```
* IDE(Win跟Mac) 版：
    * 打開IDE，找到terminal把下面的東西塞進去
    ![螢幕擷取畫面 2025-04-30 004410](https://hackmd.io/_uploads/HJcfMYC1ee.png)
* Win 10/11 版：
    * win + R 打開執行，輸入cmd或powershell(都一樣啦)，一樣把下面的東西塞進去
    ![螢幕擷取畫面 2025-04-30 004525](https://hackmd.io/_uploads/SyYEGK0Jxe.png) 
    或
    ![螢幕擷取畫面 2025-04-30 004556](https://hackmd.io/_uploads/S1yLfK01ee.png)
    打開長這樣
    ![螢幕擷取畫面 2025-04-30 004628](https://hackmd.io/_uploads/BJGnzY0ygx.png)

* Mac 版：
    * Command + space，繼續把下面的東西塞進去
   *~~窩與不知道，沒錢買Macbook~~*
# 初始化+模組導入
```
import cv2  # 影像+UI
import numpy as np  # 數值+陣列
from sklearn.ensemble import RandomForestClassifier  # 隨機森林
import random  # 隨機
import time  # 倒數計時
from collections import deque, Counter  # deque 記憶玩家手勢，Counter 出現次數

# 玩家手勢清單
player = ['Rock', 'Paper', 'Scissors']
num_classes = len(player)  # 三種手勢的總數
player_score = 0  # 玩家分數
ai_score = 0  # AI 分數
round_count = 0  # 對戰輪數

# 訓練資料變數
training_data = []  # 儲存擷取的特徵向量
training_labels = []  # 對應手勢標籤
training_phase = True  # 是否為訓練階段
training_round = 0  # 訓練第幾輪
difficulty = None  # 難度尚未選擇

# 指定訓練的手勢順序（每種手勢各5次）
gesture_order = ['Rock'] * 5 + ['Paper'] * 5 + ['Scissors'] * 5
label_to_index = {'Rock': 0, 'Paper': 1, 'Scissors': 2}  # 手勢對應的索引

# 開啟攝影機
cap = cv2.VideoCapture(0)
```
# 特徵get與模型訓練
```
# 擷取紅框中的影像特徵
def extract_features(image):
    gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)  # 轉灰階
    _, binary = cv2.threshold(gray, 127, 255, cv2.THRESH_BINARY)  # 二值化 (0, 1)
    contours, _ = cv2.findContours(binary, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)  # 找輪廓
    if len(contours) > 0:
        largest_contour = max(contours, key=cv2.contourArea)  # 找最大輪廓
        area = cv2.contourArea(largest_contour)  # 面積
        perimeter = cv2.arcLength(largest_contour, True)  # 周長
        x, y, w, h = cv2.boundingRect(largest_contour)  # 外接矩形
        return np.array([area, perimeter, w/h])  # 回傳特徵向量
    return np.array([0, 0, 1])  # 若沒找到輪廓，回傳預設值

# 訓練模型
def train_model():
    if len(training_data) > 0:
        X = np.array(training_data)
        y = np.array(training_labels)
        clf = RandomForestClassifier(n_estimators=100, random_state=42)  # 建立模型
        clf.fit(X, y)  # 訓練
        return clf
    return None
```
#  AI出招策略
```
# 紀錄玩家歷史手勢，最多記 10 次
player_memory = deque(maxlen=10)

# Hard
def predict_next_move():
    if len(player_memory) < 3:
        return random.randint(0, num_classes - 1)  # 資料不足，隨機猜
    most_common = Counter(player_memory).most_common(1)[0][0]  # 找出最常出現的手勢
    return (most_common + 1) % num_classes  # 出剋制那招的手勢

# 根據難度決定 AI 出招
def ai_choice(difficulty, last_player_choice=None):
    if difficulty == 0:  # Easy 100% 隨機
        return random.randint(0, num_classes - 1)
    elif difficulty == 1:  # Normal：50% 模仿、50% 隨機
        if last_player_choice is not None and random.random() < 0.5:
            return (last_player_choice + 1) % num_classes
        return random.randint(0, num_classes - 1)
    else:  # Hard：80% 預測、20% 隨機
        if random.random() < 0.8:
            return predict_next_move()
        return random.randint(0, num_classes - 1)
```
# 勝負+UI
```
# 判定勝負與加分
def determine_winner(player_idx, ai_idx):
    global player_score, ai_score
    if player_idx == ai_idx:
        return "Tie!"
    if (player_idx == 0 and ai_idx == 2) or (player_idx == 1 and ai_idx == 0) or (player_idx == 2 and ai_idx == 1):
        player_score += 1
        return "U win!"
    ai_score += 1
    return "AI win!"

# 選擇UI
def draw_difficulty_ui(frame, mouse_x, mouse_y):
    height, width = frame.shape[:2]
    cv2.rectangle(frame, (0, 0), (width, height), (50, 50, 50), -1)
    cv2.putText(frame, "Choose difficulty", (width//2-100, 100),
                cv2.FONT_HERSHEY_SIMPLEX, 1.5, (255, 255, 255), 3)

    btn_width, btn_height = 290, 80
    btn_spacing = 50
    start_y = height//2 - 100
    buttons = [
        ("Easy (press 0)", (0, 255, 0)),
        ("Normal (press 1)", (0, 255, 255)),
        ("Hard (press 2)", (0, 0, 255))
    ]

    for i, (text, color) in enumerate(buttons):
        x1 = width//2 - btn_width//2
        y1 = start_y + i * (btn_height + btn_spacing)
        x2 = x1 + btn_width
        y2 = y1 + btn_height
        hover = x1 <= mouse_x <= x2 and y1 <= mouse_y <= y2
        btn_color = (150, 150, 150) if hover else color
        cv2.rectangle(frame, (x1, y1), (x2, y2), btn_color, -1)
        cv2.rectangle(frame, (x1, y1), (x2, y2), (255, 255, 255), 2)
        cv2.putText(frame, text, (x1+10, y1 + 50),
                    cv2.FONT_HERSHEY_SIMPLEX, 1, (255, 255, 255), 2)
    return buttons, btn_width, btn_height, start_y, btn_spacing

# 遊戲UI
def draw_game_ui(frame, countdown, player_choice, ai_choice, result, training_phase, difficulty):
    height, width = frame.shape[:2]
    cv2.rectangle(frame, (0, 0), (width, 100), (50, 50, 50), -1)
    cv2.rectangle(frame, (0, height-50), (width, height), (50, 50, 50), -1)
    if training_phase:
        cv2.putText(frame, f"training phase: No {training_round+1}/15 bureau", (10, 40),
                    cv2.FONT_HERSHEY_SIMPLEX, 1, (255, 255, 0), 2)
        cv2.putText(frame, f"please show: {gesture_order[training_round]}", (10, 80),
                    cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 255, 255), 2)
    else:
        difficulty_text = ["Easy", "Normal", "Hard"][difficulty]
        cv2.putText(frame, f"player: {player_score}", (10, 40),
                    cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 255, 0), 2)
        cv2.putText(frame, f"AI: {ai_score}", (width-100, 40),
                    cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 0, 255), 2)
        cv2.putText(frame, f"round: {round_count} (difficulty: {difficulty_text})", (width//50, 20),
                    cv2.FONT_HERSHEY_SIMPLEX, 1, (255, 255, 255), 2)
    if countdown > 0:
        cv2.putText(frame, f"Countdown: {countdown}", (width//250, height-70),
                    cv2.FONT_HERSHEY_SIMPLEX, 0.8, (0, 255, 255), 2)
    if player_choice and ai_choice and not training_phase:
        cv2.putText(frame, f"U: {player_choice}", (10, 80),
                    cv2.FONT_HERSHEY_SIMPLEX, 0.8, (0, 255, 0), 2)
        cv2.putText(frame, f"AI: {ai_choice}", (width-100, 80),
                    cv2.FONT_HERSHEY_SIMPLEX, 0.8, (0, 0, 255), 2)
        cv2.putText(frame, result, (width//2-50, height//2),
                    cv2.FONT_HERSHEY_SIMPLEX, 1, (255, 255, 255), 2)

# 滑鼠選難度
def mouse_callback(event, x, y, flags, param):
    global difficulty
    if event == cv2.EVENT_LBUTTONDOWN and difficulty is None:
        buttons, btn_width, btn_height, start_y, btn_spacing = param
        for i in range(len(buttons)):
            x1 = width//2 - btn_width//2
            y1 = start_y + i * (btn_height + btn_spacing)
            x2 = x1 + btn_width
            y2 = y1 + btn_height
            if x1 <= x <= x2 and y1 <= y <= y2:
                difficulty = i
                break
```
# 主程式
```
# 滑鼠點擊
cv2.namedWindow("Rock Paper Scissors")
mouse_x, mouse_y = 0, 0
cv2.setMouseCallback("Rock Paper Scissors", mouse_callback,
                     draw_difficulty_ui(np.zeros((480, 640, 3), dtype=np.uint8), 0, 0))

last_time = time.time()  # 初始時間
countdown = 3  # 倒數秒數
player_choice = None  # 玩家本輪選擇
ai_move = None  # AI 本輪選擇
result = ""  # 本輪結果

while True:
    ret, frame = cap.read()  # 攝影機讀影像
    if not ret:
        break

    height, width = frame.shape[:2]  # 畫面尺寸
    roi = frame[100:400, 100:400]  # 定義紅框區域 (玩家出手區)
    cv2.rectangle(frame, (100, 100), (400, 400), (255, 0, 0), 2)  # 繪製紅框

    key = cv2.waitKey(1) & 0xFF  # 讀取鍵盤輸入
    if key == 27:  # 按下 ESC 離開
        break
    elif key in [ord('0'), ord('1'), ord('2')]:  # 選擇難度
        difficulty = int(chr(key))

    # 如果難度尚未選擇，顯示難度選單
    if difficulty is None:
        draw_difficulty_ui(frame, mouse_x, mouse_y)
        cv2.imshow("Rock Paper Scissors", frame)
        continue

    # 倒數控制邏輯
    current_time = time.time()
    if current_time - last_time >= 1:  # 每秒倒數
        countdown -= 1
        last_time = current_time

        if countdown == 0:
            features = extract_features(roi)  # 擷取紅框特徵
            if training_phase:
                label = label_to_index[gesture_order[training_round]]  # 取得目前應該輸入的手勢標籤
                training_data.append(features)  # 加入訓練資料
                training_labels.append(label)
                training_round += 1
                if training_round >= len(gesture_order):  # 全部訓練完畢
                    clf = train_model()  # 訓練模型
                    training_phase = False  # 進入正式對戰階段
            else:
                if clf is not None:
                    player_prediction = clf.predict([features])[0]  # 預測玩家手勢
                    player_memory.append(player_prediction)  # 加入記憶
                    ai_idx = ai_choice(difficulty, player_prediction)  # AI 出招
                    player_choice = player[player_prediction]
                    ai_move = player[ai_idx]
                    result = determine_winner(player_prediction, ai_idx)  # 判定勝負
                    round_count += 1
            countdown = 4  # 重設倒數時間

    # 畫出當前畫面（遊戲階段 or 訓練階段）
    draw_game_ui(frame, countdown, player_choice, ai_move,
                 result, training_phase, difficulty)

    # 顯示畫面
    cv2.imshow("Rock Paper Scissors", frame)

# 結束後釋放資源
cap.release()
cv2.destroyAllWindows()
```
# IDE

我只有用Vscode跟Pycharm，其他的窩不知道，但應該不會有問題(除非你用競程專用IDE E.g.CP editor)
![窩不知道](https://hackmd.io/_uploads/r1b5PKAkgx.jpg)

# 版本
查版本，cmd key
```
pip show 你想查的模組
```

我是用
Python **3.12.9** (不是Python2
OpenCV **4.11.0.86**
NumPY **1.26.4**
Sklearn **1.6.1**
######## 不要北七去亂下載其他version，應該不會有問題 ########
# 硬體
因為CPU的頻率和暫存記憶體會有差，所以我提供我的硬體
> CPU - Intel® Core™ i5-14500 
> MB - PRO B760M-A DDR4 II
> RAM - DDR4 3200Hz 32GB
> GPU - INNO3D GeForce RTX™ 4070 TWIN X2 OC WHITE


