
# 簡介
這是結合openCV與機器學習的猜拳遊戲,透過鏡頭辨識玩家手勢,並使用隨機森林模型進行判斷與對戰。系統包含訓練與對戰兩階段,支援三種難度,提供即時互動與AI對戰體驗。
# 需下載的東東
* 下載
```
!pip install opencv-python # 下載opencv
!pip install numpy # 下載numpy
!pip install scikit-learn # 下載sklearn
```
* IDE(Win跟Mac) 版:
* 打開IDE,找到terminal把下面的東西塞進去

* Win 10/11 版:
* win + R 打開執行,輸入cmd或powershell(都一樣啦),一樣把下面的東西塞進去

或

打開長這樣

* 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)

# 版本
查版本,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