Py
수업 시작 전 · Python 3.12.10 설치
Windows 64비트 기준입니다. 2단원(사물인식)에서 이미 설치했다면 이 단계는 건너뛰어도 됩니다.
기존 버전 확인: 이미 Python이 설치되어 있다면 CMD에서 py -0p를 입력해 설치된 버전을 확인할 수 있습니다. 다른 버전은 삭제하지 않아도 되며, 수업에서는 py -3.12를 사용합니다.
1
설치 파일 다운로드python-3.12.10-amd64.exe 파일을 다운로드한 뒤 실행합니다.2
PATH 항목 체크설치 첫 화면 아래의 Add python.exe to PATH를 반드시 체크합니다.3
Install NowInstall Now를 눌러 설치하고, 완료 화면이 나타날 때까지 기다립니다.4
CMD에서 확인새 CMD를 열고 py -3.12 --version을 입력해 Python 3.12.10이 표시되는지 확인합니다.중요: 기존에 다른 Python 버전이 설치되어 있어도 먼저 삭제할 필요는 없습니다. Python 3.12.10 설치 후 기존 CMD 창을 닫고 새 CMD 창을 연 다음 py -3.12 방식으로 수업을 진행합니다.
오늘 만들 프로그램
1. 손모양 데이터 수집웹캠으로 가위·바위·보 손모양 사진을 직접 찍어 모읍니다.
2. 모델 학습모은 사진으로 가위바위보 인식 모델 파일을 만듭니다.
3. 실시간 대결 게임카운트다운이 끝나는 순간, 웹캠 속 내 손모양과 컴퓨터가 무작위로 말한 손모양을 동시에 비교해 승패를 자동 판정합니다.
이번 수업의 핵심 아이디어: 사람끼리 가위바위보를 할 때도 "가위바위보!" 구호가 끝나는 순간 동시에 손을 내밉니다. 이 프로그램도 똑같습니다. 화면에 3·2·1 카운트다운을 띄우고, 카운트다운이 0이 되는 바로 그 순간 ① 웹캠 사진으로 내 손모양을 예측하고 ② 그와 동시에 컴퓨터가 무작위로 가위·바위·보 중 하나를 "말"(텍스트)로 선택합니다. 그 다음 규칙에 따라 승패를 계산해 화면에 보여줍니다.
1단계. 실습 폴더 만들기
폴더 위치: 학교 PC의 바탕화면(Desktop)이 OneDrive 등으로 변경된 경우를 피하기 위해 실습 폴더를 %USERPROFILE% 아래에 만듭니다. 예: C:\Users\학생계정\rps_ai_game_app
Linux 터미널 폴더 생성 명령어
cd $HOME mkdir rps_ai_game_app cd rps_ai_game_app mkdir rps_data mkdir model ls -la
2단계. Python과 필요한 라이브러리 설치
2단원(사물인식)에서 이미 설치했다면 그대로 사용할 수 있습니다. 학교 PC에서는 pip 대신 py -3.12 -m pip 방식으로 설치하는 것이 더 안정적입니다.
Python / pip 확인
python3 --version python3 -m pip --version
필요한 라이브러리 설치
⚠️ 라즈베리파이 아키텍처와 OS 버전에 따라 이 설치 명령은 실패할 수 있습니다. Raspberry Pi OS 패키지와 가상환경 버전을 우선 확인하세요.
python3 -m pip --version # 시스템 Python에 직접 pip 업그레이드 금지 python3 -m pip install numpy python3 -m pip install opencv-contrib-python python3 -m pip install scikit-learn joblib
참고: 1단원(얼굴인식)·2단원(사물인식)과 버전을 동일하게(opencv-contrib-python 4.11.0.86) 맞춥니다. opencv-python과 opencv-contrib-python을 같이 설치하면 서로 충돌해 cv2가 깨지는 경우가 있습니다.
설치 확인 명령어
python3 -c "import cv2, numpy, sklearn, joblib; print('설치 확인 완료'); print('OpenCV:', cv2.__version__)"3단계. 웹캠 테스트 파일 만들기
파일 만드는 방법: CMD에서 notepad 0_webcam_test.py를 실행해 새 파일을 만들고, 아래 전체 코드를 붙여넣은 뒤 Ctrl+S로 저장합니다. nano 편집기을 닫고 CMD에서 py -3.12 0_webcam_test.py으로 실행합니다.
파일 만들기 명령어
cd $HOME/rps_ai_game_app nano 0_webcam_test.py
0_webcam_test.py 전체 코드
import cv2
# 0단계: 노트북 웹캠 테스트
cap = cv2.VideoCapture(0, cv2.CAP_DSHOW)
if not cap.isOpened():
cap = cv2.VideoCapture(0)
if not cap.isOpened():
print("웹캠을 열 수 없습니다.")
print("Windows 설정 > 개인정보 및 보안 > 카메라 권한을 확인하세요.")
print("Zoom, Teams, 카메라 앱처럼 웹캠을 사용 중인 프로그램도 닫아주세요.")
exit()
print("웹캠 테스트를 시작합니다. 화면이 뜨면 성공입니다. 종료하려면 q 키를 누르세요.")
while True:
ret, frame = cap.read()
if not ret:
print("웹캠 화면을 가져오지 못했습니다.")
break
frame = cv2.flip(frame, 1)
cv2.imshow("Webcam Test", frame)
if cv2.waitKey(1) & 0xFF == ord("q"):
break
cap.release()
cv2.destroyAllWindows()
실행 명령어
python3 0_webcam_test.py
4단계. 가위·바위·보 손모양 데이터 수집 파일 만들기
왜 로봇그램 이미지셋이 아니라 직접 촬영하나요? 손모양은 사람마다, 조명마다, 배경마다 다르게 찍힙니다. 지금 수업하는 노트북 웹캠·손·조명으로 직접 데이터를 모아야 실제 게임에서 더 정확하게 인식됩니다.
파일 만드는 방법: CMD에서 notepad 1_collect_rps_images.py를 실행해 새 파일을 만들고, 아래 전체 코드를 붙여넣은 뒤 Ctrl+S로 저장합니다.
파일 만들기 명령어
cd $HOME/rps_ai_game_app nano 1_collect_rps_images.py
1_collect_rps_images.py 전체 코드
import os
import cv2
# 1단계: 가위/바위/보 손모양 이미지 수집
DATA_DIR = "rps_data"
CLASSES = {"1": "가위", "2": "바위", "3": "보"}
# OpenCV 화면 글자(cv2.putText)는 한글을 지원하지 않아 물음표로 깨지므로, 화면 표시용 영어 이름을 따로 둡니다.
DISPLAY_NAME = {"가위": "Scissors", "바위": "Rock", "보": "Paper"}
def save_image_korean_path(path, image):
# cv2.imwrite는 Windows에서 한글이 섞인 경로를 제대로 저장하지 못할 때가 있어
# 메모리에서 jpg로 인코딩한 뒤 파이썬의 파일쓰기로 직접 저장합니다.
ok, buf = cv2.imencode(".jpg", image)
if not ok:
return False
with open(path, "wb") as f:
f.write(buf.tobytes())
return True
for name in CLASSES.values():
os.makedirs(os.path.join(DATA_DIR, name), exist_ok=True)
counts = {name: len(os.listdir(os.path.join(DATA_DIR, name))) for name in CLASSES.values()}
cap = cv2.VideoCapture(0, cv2.CAP_DSHOW)
if not cap.isOpened():
cap = cv2.VideoCapture(0)
if not cap.isOpened():
print("웹캠을 열 수 없습니다.")
exit()
print("손 모양을 중앙 사각형 안에 넣고 숫자 키를 누르면 그 모양으로 저장됩니다.")
print("1: 가위 2: 바위 3: 보 q: 종료")
while True:
ret, frame = cap.read()
if not ret:
break
frame = cv2.flip(frame, 1)
h, w = frame.shape[:2]
box_size = min(w, h) // 2
x1, y1 = w // 2 - box_size // 2, h // 2 - box_size // 2
x2, y2 = x1 + box_size, y1 + box_size
cv2.rectangle(frame, (x1, y1), (x2, y2), (0, 255, 0), 2)
info = " ".join(f"{DISPLAY_NAME[name]}:{counts[name]}" for name in CLASSES.values())
cv2.putText(frame, info, (20, 30), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (255, 255, 255), 2)
cv2.putText(frame, "1:Gawi(scissors) 2:Bawi(rock) 3:Bo(paper) q:quit", (20, h - 20),
cv2.FONT_HERSHEY_SIMPLEX, 0.6, (200, 200, 200), 1)
cv2.imshow("RPS Data Collection", frame)
key = cv2.waitKey(1) & 0xFF
if key == ord("q"):
break
key_char = chr(key) if key < 256 else ""
if key_char in CLASSES:
name = CLASSES[key_char]
roi = frame[y1:y2, x1:x2]
counts[name] += 1
file_path = os.path.join(DATA_DIR, name, f"{counts[name]}.jpg")
if save_image_korean_path(file_path, roi):
print(f"{name} 저장: {file_path}")
else:
counts[name] -= 1
print(f"[오류] 저장 실패: {file_path}")
cap.release()
cv2.destroyAllWindows()
실행 명령어
python3 1_collect_rps_images.py
권장 촬영 방법: 손모양별로 최소 40~60장 이상 모으세요. 손의 각도를 조금씩 바꾸고(좌우로 살짝 회전), 화면 안에서 손 위치도 살짝씩 옮기고, 가능하면 배경이나 조명도 바꿔가며 찍으면 실제 게임에서 더 안정적으로 인식됩니다.
배경 주의: 사각형 박스 안에 손 말고 다른 물건이 함께 찍히면 모델이 손 모양 대신 배경을 학습해버릴 수 있습니다. 가능하면 벽이나 단색 배경 앞에서 촬영하세요.
왜 코드에 save_image_korean_path라는 함수가 있나요? 폴더 이름이 rps_data/가위처럼 한글입니다. 그런데 OpenCV의 cv2.imwrite는 Windows에서 한글(유니코드)이 섞인 경로에 저장할 때 에러 없이 조용히 실패하는 경우가 있습니다 — 키를 눌러도, 콘솔에 아무 경고도 없이 파일만 안 만들어지는 바로 그 증상입니다. 그래서 이미지를 메모리에서 먼저 jpg로 인코딩한 뒤, 파이썬의 기본 파일쓰기(open(path, "wb"))로 직접 저장하도록 바꿨습니다. 이 방식은 한글 경로에서도 안전하게 동작합니다.
5단계. 모델 학습 파일 만들기
파일 만드는 방법: CMD에서 notepad 2_train_rps_model.py를 실행해 새 파일을 만들고, 아래 전체 코드를 붙여넣은 뒤 Ctrl+S로 저장합니다.
파일 만들기 명령어
cd $HOME/rps_ai_game_app nano 2_train_rps_model.py
2_train_rps_model.py 전체 코드
import os
import cv2
import joblib
import numpy as np
from sklearn.model_selection import train_test_split
from sklearn.neighbors import KNeighborsClassifier
from sklearn.metrics import accuracy_score, classification_report, confusion_matrix
# 2단계: 가위/바위/보 손모양 이미지로 인식 모델 학습
DATA_DIR = "rps_data"
MODEL_DIR = "model"
MODEL_PATH = os.path.join(MODEL_DIR, "rps_model.joblib")
LABEL_PATH = os.path.join(MODEL_DIR, "labels.txt")
REPORT_PATH = os.path.join(MODEL_DIR, "training_report.txt")
os.makedirs(MODEL_DIR, exist_ok=True)
IMAGE_EXTS = (".jpg", ".jpeg", ".png", ".bmp", ".webp")
def extract_features(image):
# 색상 특징 + 모양(윤곽선) 특징을 숫자로 변환합니다.
image = cv2.resize(image, (96, 96))
hsv = cv2.cvtColor(image, cv2.COLOR_BGR2HSV)
hist = cv2.calcHist([hsv], [0, 1, 2], None, [8, 8, 8], [0, 180, 0, 256, 0, 256])
hist = cv2.normalize(hist, hist).flatten()
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
gray = cv2.resize(gray, (64, 64))
hog = cv2.HOGDescriptor(_winSize=(64,64), _blockSize=(16,16), _blockStride=(8,8), _cellSize=(8,8), _nbins=9)
hog_feature = hog.compute(gray).flatten()
return np.concatenate([hist, hog_feature])
def load_image_korean_path(path):
# cv2.imread는 Windows에서 한글이 섞인 경로를 제대로 읽지 못할 때가 있어
# 파이썬이 직접 읽은 바이트를 OpenCV 이미지로 디코딩합니다.
data = np.fromfile(path, dtype=np.uint8)
if data.size == 0:
return None
return cv2.imdecode(data, cv2.IMREAD_COLOR)
def load_dataset():
X, y = [], []
if not os.path.exists(DATA_DIR):
print("rps_data 폴더가 없습니다. 먼저 1_collect_rps_images.py 로 사진을 모으세요.")
exit()
class_names = sorted([name for name in os.listdir(DATA_DIR) if os.path.isdir(os.path.join(DATA_DIR, name))])
if len(class_names) == 0:
print("rps_data 안에 손모양별 폴더가 없습니다. 예: rps_data/가위, rps_data/바위, rps_data/보")
exit()
print("발견한 손모양 클래스:", class_names)
for class_name in class_names:
class_dir = os.path.join(DATA_DIR, class_name)
count = 0
for file_name in os.listdir(class_dir):
if not file_name.lower().endswith(IMAGE_EXTS):
continue
image_path = os.path.join(class_dir, file_name)
image = load_image_korean_path(image_path)
if image is None:
continue
X.append(extract_features(image))
y.append(class_name)
count += 1
print(f"{class_name}: {count}장")
if len(X) == 0:
print("학습할 이미지가 없습니다.")
exit()
return np.array(X), np.array(y), class_names
X, y, class_names = load_dataset()
print("/n총 이미지 수:", len(X))
print("클래스 수:", len(class_names))
try:
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.25, random_state=42, stratify=y)
except ValueError:
print("클래스별 이미지 수가 적어 전체 데이터로 학습/평가합니다.")
print("손모양별 사진을 40장 이상으로 늘린 뒤 다시 실행하는 것을 권장합니다.")
X_train, X_test, y_train, y_test = X, X, y, y
model = KNeighborsClassifier(n_neighbors=3, weights="distance")
model.fit(X_train, y_train)
pred = model.predict(X_test)
acc = accuracy_score(y_test, pred)
report = classification_report(y_test, pred, zero_division=0)
cm = confusion_matrix(y_test, pred, labels=class_names)
print("/n==============================")
print("모델 학습 완료")
print(f"테스트 인식률: {acc * 100:.2f}%")
print("==============================")
print("/n[분류 리포트]/n", report)
print("[혼동 행렬]")
print(class_names)
print(cm)
joblib.dump({"model": model, "class_names": class_names, "feature_info": "HSV color histogram + HOG"}, MODEL_PATH)
with open(LABEL_PATH, "w", encoding="utf-8") as f:
for name in class_names:
f.write(name + "/n")
with open(REPORT_PATH, "w", encoding="utf-8") as f:
f.write(f"테스트 인식률: {acc * 100:.2f}%/n/n")
f.write("[클래스 목록]/n" + str(class_names) + "/n/n")
f.write("[분류 리포트]/n" + report + "/n")
f.write("[혼동 행렬]/n" + str(cm) + "/n")
print("/n저장된 모델 파일:", MODEL_PATH)
print("저장된 라벨 파일:", LABEL_PATH)
print("저장된 리포트:", REPORT_PATH)
모델 학습 실행 명령어
cd $HOME/rps_ai_game_app python3 2_train_rps_model.py
성공하면 model/rps_model.joblib, model/labels.txt, model/training_report.txt가 만들어집니다. 이 rps_model.joblib 파일이 바로 오늘 완성한 "학습된 모델 파일"입니다 — 6단계 게임이 이 파일을 불러와서 사용합니다.
training_report.txt의 혼동 행렬(confusion matrix) 읽는 법: 행(row)은 "실제 손모양", 열(column)은 "모델이 예측한 손모양"입니다. 대각선이 아닌 칸에 숫자가 많다면 그 두 손모양이 자주 헷갈린다는 뜻입니다. 가위바위보는 세 손모양 모두 같은 피부색이라 2단원 사물인식 때처럼 색만으로 구분하기 어렵고, 결국 손가락 개수·윤곽선 모양(HOG 특징)에 더 의존하게 됩니다. 그래서 손의 각도나 조명에 따라 가위↔보, 바위↔가위처럼 서로 헷갈리는 경우가 자주 생깁니다.
생각해보기 — 사물인식과 뭐가 다를까? 2단원에서는 물건마다 색·질감이 뚜렷이 달라 HSV 색상 특징만으로도 꽤 잘 구분됐습니다. 그런데 가위·바위·보는 전부 "손"이라서 색은 거의 같고 모양(손가락 개수와 윤곽선)만 다릅니다. 같은 알고리즘(KNN)과 같은 특징(HSV+HOG)을 썼는데도, 데이터의 성격에 따라 어떤 특징이 더 중요하게 작용하는지가 달라진다는 것을 training_report.txt의 정확도를 2단원 결과와 비교하며 확인해보세요.
6단계. 가위바위보 AI 게임 만들기 (핵심)
게임 진행 방식: 스페이스바를 누르면 화면에 3 → 2 → 1 → SHOOT! 카운트다운이 나타납니다(OpenCV 화면 글자는 한글을 표시하지 못해 영어로 씁니다. 콘솔에는 한글로 안내가 출력됩니다). 카운트다운이 끝나는 바로 그 순간, 프로그램은 ① 중앙 사각형 안의 웹캠 화면으로 내 손모양을 모델에 입력해 예측하고 ② 그와 동시에 컴퓨터가 가위·바위·보 중 하나를 무작위로 골라 단어로 정합니다. 사람이 "가위바위보!" 구호에 맞춰 동시에 손을 내는 것과 똑같은 타이밍 구조입니다.
승패 판정 규칙: 가위는 보를 이기고, 바위는 가위를 이기고, 보는 바위를 이깁니다. 같은 모양이면 무승부입니다. 예를 들어 내가 가위를 냈는데 컴퓨터가 보라고 말하면, 프로그램이 자동으로 "내가 이겼다"고 판단해 화면에 승리를 표시합니다.
파일 만드는 방법: CMD에서 notepad 3_rps_game.py를 실행해 새 파일을 만들고, 아래 전체 코드를 붙여넣은 뒤 Ctrl+S로 저장합니다.
파일 만들기 명령어
cd $HOME/rps_ai_game_app nano 3_rps_game.py
3_rps_game.py 전체 코드
import os
import time
import random
import joblib
import cv2
import numpy as np
# 3단계: 가위바위보 AI 게임 (사용자 vs 컴퓨터, 동시 판정)
MODEL_PATH = "model/rps_model.joblib"
if not os.path.exists(MODEL_PATH):
print("모델 파일이 없습니다. 먼저 python3 2_train_rps_model.py 를 실행하세요.")
exit()
bundle = joblib.load(MODEL_PATH)
model = bundle["model"]
class_names = bundle["class_names"] # 예: ['가위', '바위', '보']
CHOICES = ["가위", "바위", "보"]
CONFIDENCE_THRESHOLD = 50 # 이 값보다 confidence가 낮으면 점수에 반영하지 않고 다시 시도합니다.
# OpenCV 화면 글자(cv2.putText)는 한글을 지원하지 않아 물음표로 깨지므로, 화면 표시용 영어 이름을 따로 둡니다.
DISPLAY_NAME = {"가위": "Scissors", "바위": "Rock", "보": "Paper"}
VERDICT_DISPLAY = {"승리": "YOU WIN!", "패배": "YOU LOSE", "무승부": "DRAW"}
def extract_features(image):
image = cv2.resize(image, (96, 96))
hsv = cv2.cvtColor(image, cv2.COLOR_BGR2HSV)
hist = cv2.calcHist([hsv], [0, 1, 2], None, [8, 8, 8], [0, 180, 0, 256, 0, 256])
hist = cv2.normalize(hist, hist).flatten()
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
gray = cv2.resize(gray, (64, 64))
hog = cv2.HOGDescriptor(_winSize=(64, 64), _blockSize=(16, 16), _blockStride=(8, 8), _cellSize=(8, 8), _nbins=9)
return np.concatenate([hist, hog.compute(gray).flatten()])
def predict_hand(image):
feature = extract_features(image).reshape(1, -1)
pred = model.predict(feature)[0]
probs = model.predict_proba(feature)[0]
confidence = float(np.max(probs)) * 100
return pred, confidence
def judge(user, computer):
# 가위는 보를 이기고, 바위는 가위를 이기고, 보는 바위를 이깁니다.
if user == computer:
return "무승부"
beats = {"가위": "보", "바위": "가위", "보": "바위"}
if beats.get(user) == computer:
return "승리"
return "패배"
cap = cv2.VideoCapture(0, cv2.CAP_DSHOW)
if not cap.isOpened():
cap = cv2.VideoCapture(0)
if not cap.isOpened():
print("웹캠을 열 수 없습니다.")
exit()
STATE_WAIT, STATE_COUNT, STATE_RESULT = "wait", "count", "result"
state = STATE_WAIT
count_start = 0.0
COUNT_SECONDS = 3
result_start = 0.0
RESULT_SECONDS = 3
user_choice, computer_choice, verdict = "", "", ""
user_confidence = 0.0
win, lose, draw = 0, 0, 0
print("스페이스바(또는 s)를 누르면 카운트다운이 시작됩니다.")
print("카운트다운이 끝나는 순간 손 모양을 보여주세요. q: 종료, r: 점수 초기화")
print(f"확신도(confidence)가 {CONFIDENCE_THRESHOLD}% 미만이면 점수에 반영하지 않고 다시 시도합니다.")
while True:
ret, frame = cap.read()
if not ret:
break
frame = cv2.flip(frame, 1)
h, w = frame.shape[:2]
box_size = min(w, h) // 2
x1, y1 = w // 2 - box_size // 2, h // 2 - box_size // 2
x2, y2 = x1 + box_size, y1 + box_size
roi = frame[y1:y2, x1:x2]
now = time.time()
if state == STATE_COUNT:
remaining = COUNT_SECONDS - (now - count_start)
if remaining > 0:
text = str(int(remaining) + 1)
color = (0, 255, 255)
else:
# 카운트다운 종료 - 바로 이 순간 동시에 판정합니다.
pred, conf = predict_hand(roi)
user_choice, user_confidence = pred, conf
if conf < CONFIDENCE_THRESHOLD:
# 모델이 확신하지 못하면 점수에 반영하지 않고 다시 시도하도록 안내합니다.
computer_choice = ""
verdict = "재시도"
else:
computer_choice = random.choice(CHOICES)
verdict = judge(user_choice, computer_choice)
if verdict == "승리":
win += 1
elif verdict == "패배":
lose += 1
else:
draw += 1
state = STATE_RESULT
result_start = now
text, color = "SHOOT!", (0, 255, 255)
cv2.rectangle(frame, (x1, y1), (x2, y2), (0, 255, 255), 3)
cv2.putText(frame, text, (x1 + box_size // 2 - 60, y1 + box_size // 2),
cv2.FONT_HERSHEY_SIMPLEX, 2.0, color, 4)
elif state == STATE_RESULT:
if verdict == "재시도":
box_color = (0, 165, 255)
else:
box_color = (0, 255, 0) if verdict == "승리" else (0, 0, 255) if verdict == "패배" else (0, 255, 255)
cv2.rectangle(frame, (x1, y1), (x2, y2), box_color, 3)
cv2.putText(frame, f"ME: {DISPLAY_NAME.get(user_choice, user_choice)} ({user_confidence:.0f}%)", (20, 40),
cv2.FONT_HERSHEY_SIMPLEX, 0.9, (255, 255, 255), 2)
if verdict == "재시도":
cv2.putText(frame, "COMPUTER: (confidence too low)", (20, 80),
cv2.FONT_HERSHEY_SIMPLEX, 0.8, (255, 255, 255), 2)
cv2.putText(frame, "CONFIDENCE LOW - TRY AGAIN", (20, 135),
cv2.FONT_HERSHEY_SIMPLEX, 1.1, box_color, 3)
else:
cv2.putText(frame, f"COMPUTER: {DISPLAY_NAME.get(computer_choice, computer_choice)}", (20, 80),
cv2.FONT_HERSHEY_SIMPLEX, 0.9, (255, 255, 255), 2)
cv2.putText(frame, VERDICT_DISPLAY.get(verdict, verdict), (20, 135),
cv2.FONT_HERSHEY_SIMPLEX, 1.5, box_color, 3)
if now - result_start >= RESULT_SECONDS:
state = STATE_WAIT
else:
cv2.rectangle(frame, (x1, y1), (x2, y2), (255, 255, 255), 2)
cv2.putText(frame, "Press SPACE to start", (x1, y1 - 15),
cv2.FONT_HERSHEY_SIMPLEX, 0.8, (255, 255, 255), 2)
cv2.putText(frame, f"WIN {win} LOSE {lose} DRAW {draw}", (20, h - 20),
cv2.FONT_HERSHEY_SIMPLEX, 0.8, (0, 255, 0), 2)
cv2.putText(frame, "SPACE: start r: reset score q: quit", (20, h - 50),
cv2.FONT_HERSHEY_SIMPLEX, 0.6, (200, 200, 200), 1)
cv2.imshow("Rock Paper Scissors - AI Game", frame)
key = cv2.waitKey(1) & 0xFF
if key == ord("q"):
break
if key == ord("r"):
win, lose, draw = 0, 0, 0
if (key == ord(" ") or key == ord("s")) and state == STATE_WAIT:
state = STATE_COUNT
count_start = now
cap.release()
cv2.destroyAllWindows()
print(f"최종 전적 - 승:{win} 패:{lose} 무:{draw}")
실행 명령어
cd $HOME/rps_ai_game_app python3 3_rps_game.py
왜 카운트다운이 끝나는 순간에만 판정하나요? 사람이 가위바위보를 할 때 "말하는 중"에는 손을 안 보여주고, 구호가 끝나는 "그 순간"에만 동시에 내는 것과 같은 원리입니다. 매 프레임마다 계속 판정하면 손을 준비하는 중간 동작까지 결과로 잡혀 버립니다.
왜 confidence가 낮으면 점수에 안 넣나요? 모델이 "이 손모양이 가위인지 바위인지 30%만 확신한다"고 말하는데 그걸 그대로 정답 처리해버리면, 승패가 '내 손모양'이 아니라 '모델의 애매한 추측'으로 결정되어 버립니다. 그래서 코드의 CONFIDENCE_THRESHOLD = 50 보다 confidence가 낮으면 점수에 반영하지 않고 "CONFIDENCE LOW - TRY AGAIN"만 보여줍니다. 이 숫자를 70으로 올리면 더 엄격해지고, 30으로 내리면 더 관대해집니다 — 직접 바꿔보며 체감해보세요.
7단계. 모델 정확도 자동 측정 + confusion 분석
왜 이 단계가 필요한가요? 6단계 게임은 "이겼다/졌다"만 보여주지만, 그 뒤에는 모델이 내 손모양을 얼마나 정확히 맞혔는지가 숨어 있습니다. 2단원(사물인식)에서 목표 사물의 인식률을 측정했던 것처럼, 이번에는 목표 손모양 하나를 정해 모델이 0.5초마다 얼마나 정확하게 맞히는지, 틀릴 때는 주로 어떤 손모양으로 착각하는지(confusion)를 직접 측정합니다.
파일 만드는 방법: CMD에서 notepad 4_measure_rps_accuracy.py를 실행해 새 파일을 만들고, 아래 전체 코드를 붙여넣은 뒤 Ctrl+S로 저장합니다.
파일 만들기 명령어
cd $HOME/rps_ai_game_app nano 4_measure_rps_accuracy.py
4_measure_rps_accuracy.py 전체 코드
import os
import cv2
import joblib
import numpy as np
import time
from collections import deque, Counter
# 4단계: 가위바위보 모델 정확도 자동 측정 + confusion 분석
MODEL_PATH = "model/rps_model.joblib"
if not os.path.exists(MODEL_PATH):
print("모델 파일이 없습니다. 먼저 python3 2_train_rps_model.py 를 실행하세요.")
exit()
bundle = joblib.load(MODEL_PATH)
model = bundle["model"]
class_names = bundle["class_names"]
# OpenCV 화면 글자(cv2.putText)는 한글을 지원하지 않아 물음표로 깨지므로, 화면 표시용 영어 이름을 따로 둡니다.
DISPLAY_NAME = {"가위": "Scissors", "바위": "Rock", "보": "Paper"}
print("학습된 손모양 목록:")
for name in class_names:
print("-", name)
target = input("/n정확도를 측정할 손모양을 입력하세요 (예: 가위): ").strip()
matched_target = None
for name in class_names:
if name.strip() == target.strip():
matched_target = name
break
if matched_target is None:
print("입력한 손모양이 학습 목록에 없습니다. 다음 중 하나를 정확히 입력하세요:")
for name in class_names:
print("-", name)
exit()
target = matched_target
def extract_features(image):
image = cv2.resize(image, (96, 96))
hsv = cv2.cvtColor(image, cv2.COLOR_BGR2HSV)
hist = cv2.calcHist([hsv], [0, 1, 2], None, [8, 8, 8], [0, 180, 0, 256, 0, 256])
hist = cv2.normalize(hist, hist).flatten()
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
gray = cv2.resize(gray, (64, 64))
hog = cv2.HOGDescriptor(_winSize=(64, 64), _blockSize=(16, 16), _blockStride=(8, 8), _cellSize=(8, 8), _nbins=9)
return np.concatenate([hist, hog.compute(gray).flatten()])
def predict_hand(image):
feature = extract_features(image).reshape(1, -1)
pred = model.predict(feature)[0]
probs = model.predict_proba(feature)[0]
confidence = float(np.max(probs)) * 100
return pred, confidence
def rate_color(rate):
if rate >= 70:
return (0, 200, 0)
if rate >= 40:
return (0, 200, 255)
return (0, 0, 255)
def draw_bar(frame, x, y, w, h, ratio, color):
ratio = max(0.0, min(1.0, ratio))
cv2.rectangle(frame, (x, y), (x + w, y + h), (70, 70, 70), -1)
cv2.rectangle(frame, (x, y), (x + int(w * ratio), y + h), color, -1)
cv2.rectangle(frame, (x, y), (x + w, y + h), (255, 255, 255), 1)
cap = cv2.VideoCapture(0, cv2.CAP_DSHOW)
if not cap.isOpened():
cap = cv2.VideoCapture(0)
if not cap.isOpened():
print("웹캠을 열 수 없습니다.")
exit()
WINDOW_SIZE = 20
recent = deque(maxlen=WINDOW_SIZE)
total = 0
correct = 0
confidence_sum = 0.0
confusion_counter = Counter()
high_confidence_wrong = 0
last_check = 0
interval = 0.5
print("/n정확도 측정을 시작합니다.")
print("r: 지금까지 구간 리포트 출력 후 초기화 / q: 종료")
def print_segment_report():
rate = (correct / total * 100) if total > 0 else 0
avg_conf = (confidence_sum / total) if total > 0 else 0
print("/n----- 구간 리포트 -----")
print(f"목표 손모양: {target}")
print(f"정답 횟수 / 전체 횟수: {correct}/{total}")
print(f"정확도: {rate:.1f}%")
print(f"평균 confidence: {avg_conf:.1f}%")
if confusion_counter:
most_common, cnt = confusion_counter.most_common(1)[0]
print(f"가장 많이 헷갈린 손모양: '{target}' -> '{most_common}' ({cnt}회)")
print("전체 혼동 내역:", dict(confusion_counter))
print(f"자신있게 틀린 횟수(확신 60% 이상인데 오답): {high_confidence_wrong}")
print("-----------------------/n")
while True:
ret, frame = cap.read()
if not ret:
break
frame = cv2.flip(frame, 1)
h, w = frame.shape[:2]
box_size = min(w, h) // 2
x1, y1 = w // 2 - box_size // 2, h // 2 - box_size // 2
x2, y2 = x1 + box_size, y1 + box_size
roi = frame[y1:y2, x1:x2]
pred, confidence = predict_hand(roi)
is_correct = pred == target
now = time.time()
if now - last_check >= interval:
total += 1
confidence_sum += confidence
recent.append(1 if is_correct else 0)
if is_correct:
correct += 1
else:
confusion_counter[pred] += 1
if confidence >= 60:
high_confidence_wrong += 1
last_check = now
overall_rate = (correct / total * 100) if total > 0 else 0
recent_rate = (sum(recent) / len(recent) * 100) if recent else 0
box_color = (0, 255, 0) if is_correct else (0, 0, 255)
cv2.rectangle(frame, (x1, y1), (x2, y2), box_color, 3)
overlay = frame.copy()
cv2.rectangle(overlay, (0, 0), (w, 175), (20, 20, 20), -1)
frame = cv2.addWeighted(overlay, 0.55, frame, 0.45, 0)
cv2.putText(frame, f"Target: {DISPLAY_NAME.get(target, target)} | Pred: {DISPLAY_NAME.get(pred, pred)} ({confidence:.0f}%)", (20, 30),
cv2.FONT_HERSHEY_SIMPLEX, 0.7, (255, 255, 255), 2)
cv2.putText(frame, f"Recent {len(recent)}/{WINDOW_SIZE}: {recent_rate:.0f}%", (20, 62),
cv2.FONT_HERSHEY_SIMPLEX, 0.7, rate_color(recent_rate), 2)
draw_bar(frame, 260, 48, 200, 18, recent_rate / 100, rate_color(recent_rate))
cv2.putText(frame, f"Overall: {overall_rate:.0f}% ({correct}/{total})", (20, 94),
cv2.FONT_HERSHEY_SIMPLEX, 0.7, rate_color(overall_rate), 2)
draw_bar(frame, 260, 80, 200, 18, overall_rate / 100, rate_color(overall_rate))
cv2.putText(frame, f"Confidence now: {confidence:.0f}%", (20, 126),
cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 255, 255), 2)
draw_bar(frame, 260, 112, 200, 18, confidence / 100, (0, 255, 255))
cv2.putText(frame, "r: segment report + reset q: quit", (20, 158),
cv2.FONT_HERSHEY_SIMPLEX, 0.6, (200, 200, 200), 1)
cv2.imshow("RPS Accuracy Test", frame)
key = cv2.waitKey(1) & 0xFF
if key == ord("r"):
print_segment_report()
total, correct, confidence_sum = 0, 0, 0.0
recent.clear()
confusion_counter.clear()
high_confidence_wrong = 0
print("측정값을 초기화했습니다.")
if key == ord("q"):
break
cap.release()
cv2.destroyAllWindows()
print_segment_report()
print("측정을 종료했습니다.")
실행 명령어
cd $HOME/rps_ai_game_app python3 4_measure_rps_accuracy.py
화면 읽는 법: 중앙 사각형 테두리가 초록이면 지금 이 순간 목표 손모양으로 정확히 인식 중, 빨강이면 다른 손모양으로 인식 중입니다. Recent 20회 막대는 지금 자세·조명에서의 실력을, Overall 막대는 측정 시작(또는 마지막 r)부터 전체 누적 정확도를 보여줍니다.
r 키를 누르면 지금까지의 구간 리포트가 CMD에 먼저 출력됩니다. 여기에는 평균 confidence뿐 아니라 가장 많이 헷갈린 손모양(예: 가위 → 보로 몇 번 착각했는지)과 자신있게 틀린 횟수(confidence 60% 이상인데 오답)가 함께 나옵니다. 이 숫자가 높다면 학습 데이터에 그 두 손모양을 헷갈릴 만한 사진이 섞여 있었다는 뜻입니다.
8단계. 비교 실험 — 조건을 바꾸며 확인하기
7단계 자동 측정 프로그램을 이용해 조건을 바꿔가며 비교해 보세요. r을 누르면 구간 리포트가 출력된 뒤 초기화되므로, 조건을 바꿀 때마다 눌러 기록을 남기세요.
| 실험 | 방법 | Overall 정확도(%) | 평균 confidence(%) | 가장 많이 헷갈린 손모양 |
|---|---|---|---|---|
| ① 기본 조건 | 데이터 수집 때와 같은 자리·조명에서 20~30초 측정 → r | |||
| ② 손 위치/크기 바꾸기 | 사각형 박스 안에서 손을 더 작게/크게, 위치를 옮겨가며 측정 → r | |||
| ③ 조명 바꾸기 | ①과 같은 손모양, 조명만 어둡게/밝게 바꿔 측정 → r |
③번에서 정확도가 크게 떨어진다면, 이 모델이 "조명 변화에 약하다"는 걸 직접 증명한 것입니다. "가장 많이 헷갈린 손모양" 칸이 매번 같은 손모양이라면(예: 항상 보로 착각), 4단계에서 그 두 손모양의 사진을 더 다양하게 보강한 뒤 5단계를 다시 실행하는 게 좋다는 신호입니다.
잘 안 될 때
| 증상 | 해결 |
|---|---|
| 웹캠이 안 열림 | Windows 카메라 권한을 켜고 Zoom/Teams/카메라 앱을 닫습니다. |
| No module named cv2 | py -3.12 -m pip install opencv-contrib-python==4.11.0.86을 실행한 뒤 CMD를 다시 엽니다. |
| 모델 파일이 없다고 나옴 | py -3.12 2_train_rps_model.py를 먼저 실행해 model/rps_model.joblib을 만듭니다. |
| rps_data 안에 이미지가 없다고 나옴 | rps_data/가위/1.jpg처럼 손모양별 폴더 안에 이미지가 있어야 합니다. 4단계 수집 프로그램을 먼저 실행하세요. |
| 4단계에서 숫자 키를 눌러도 rps_data 폴더 안에 이미지가 생기지 않음 | 한글 폴더 경로에서 cv2.imwrite가 조용히 실패하는 Windows OpenCV의 알려진 문제입니다. 이 교재의 최신 코드는 save_image_korean_path 함수로 이 문제를 우회했으니, 코드를 다시 전체 복사해서 덮어쓴 뒤 재실행하세요. 또한 CMD에서 cd %USERPROFILE%\rps_ai_game_app로 이동한 뒤 실행했는지, 숫자 키를 누르기 전에 웹캠 창을 한 번 클릭해 포커스를 줬는지도 확인하세요(콘솔 창에 포커스가 있으면 키 입력이 전달되지 않습니다). |
| 항상 같은 손모양으로만 인식됨 | 손모양별 사진 수가 너무 적거나 한쪽으로 치우친 경우입니다. 부족한 손모양의 사진을 더 찍고(4단계) 다시 학습(5단계)하세요. |
| 카운트다운 중 화면이 멈춘 것처럼 보임 | 정상입니다. 카운트다운 숫자가 1초마다 바뀌며, 0이 되는 순간 예측과 판정이 한 번에 일어납니다. |
| pip 명령을 찾을 수 없음 | pip 대신 py -3.12 -m pip을 사용합니다. |
| 웹캠 화면의 글자가 ??????처럼 깨져 보임 | 정상입니다. OpenCV의 cv2.putText는 한글(유니코드) 폰트를 지원하지 않아 화면에 직접 그리는 글자는 한글이 전부 물음표로 깨집니다. 그래서 이 교재의 코드는 화면에는 Scissors/Rock/Paper처럼 영어로 표시하고, CMD 콘솔의 print() 출력과 폴더 이름(가위/바위/보)은 그대로 한글을 사용합니다. 코드를 바꿀 필요는 없습니다. |
수업 끝나고 전체 삭제하기
Linux 터미널 삭제 명령어
cd $HOME rmdir /s /q rps_ai_game_app ls -la
주의: 삭제 명령은 되돌리기 어렵습니다.