بیاید یه هوش مصنوعی ساده و ۱۰۰ درصد برنده توی بازی سنگ کاغذ قیچی بسازیم

میخوایم چیکار کنیم : مثل خارجیا سه بار دستمون رو بالا پایین میاریم و بار سوم حرکتی رو که میخوایم میزنیم.هوش مصنوعی هم دقیقا موقعی که میخوایم حرکت رو بازی کنیم اون حرکت رو تشخیص میده و حرکتی رو میزنه که همیشه برنده باشه، به طوری که ذهن متوجه نشه که آقا تقلب کردن :)

پروژه جذاب سنگ کاغذ قیچی
پروژه جذاب سنگ کاغذ قیچی

خب بزارید این پروژه رو بشکنیم به بخش های کوچیک تا قدم به قدم انجامش بدیم.

در قدم اول ما نیاز داریم که کلا دست رو تشخیص بدیم.خوشبختانه یه کتابخونه قدرتمند و بی رقیب برای اینکار وجود داره : مدیاپایپ(یکی از پروژه های اوپن سورس گوگل)

# Initialize MediaPipe Hands.
mp_hands = mp.solutions.hands
hands = mp_hands.Hands(
static_image_mode=False,
max_num_hands=1,
min_detection_confidence=0.4,
min_tracking_confidence=0.4,
)



حالا ما دستمون رو تشخیص میدیم و بخش جذابش اینه که کتابخونه مدیاپایپ روی دست نقطه گذاری میکنه و مختصات نقاط رو هم به ما میده.

نقاطی که روی دست قرار میگیرند
نقاطی که روی دست قرار میگیرند

حالا وظیفه بعدی اینه که مختصات یکی از این نقاط رو توی ویدیو دنبال کنیم.و بررسی کنیم که اگر دست داشت پایین میومد و میزان پایین اومدنش از یه عددی بیشتر شد یدونه به شمارنده بازی(counter) اضافه کنیم.(توی این پروژه من مختصات wrist که نقطه پایین دست هست رو دنبال میکنم و اون عدد خاص هم بعد از یه سری آزمون و خطا 170 قرار دادم).الگوریتم زیر این کارو برای ما انجام میده :

counter = 0
yHigh = 1000
yChanges = 0
y = 0

if results.multi_hand_landmarks:
for hand_landmarks in results.multi_hand_landmarks:
landmarks = hand_landmarks.landmark
wrist = hand_landmarks.landmark[mp_hands.HandLandmark.WRIST]
x, y = int(wrist.x * width), int(wrist.y * height)

if y < yHigh:
yHigh = y
else:
yChanges = y - yHigh
if yChanges > 170:
counter += 1
yChanges = 0
yHigh = 1000

خب الان شمارنده ما میشه 1 و 2 و وقتی که میخواد بشه 3 دقیقا لحظه ای هست که ما باید حرکتمون رو بین سنگ کاغذ قیچی شروع کنیم به نشون دادن و دقیقا از همین فریم عکس برداری میکنیم برای اینکه بدیمش به شبکه عصبی و پیش بینی کنیم که چه حرکتی هست.

و از اینجا تازه بخش چالشیمون شروع میشه.تصویر زیر یه مثال از تصویر برداری یک فریم هست که حرکت کاغذ رو نشون میده و مسلما میتونید حدس بزنید چرا این بخش چالشیه.

فریم تصویر برداری شده
فریم تصویر برداری شده

دو تا چالش اساسی راجع به این فریم وجود داره :

1-دست در حال حرکته و توی 90 درصد موارد فریمی که تصویر برداری میشه مثل بالا شفاف نیست و کار شبکه رو سخت میکنه
2-این بازی بایستی بتونه توی هر محیطی انجام بشه و مسلما هر محیطی بک گراند خاصی داره و این بک گراند میتونه توی پیش بینی های شبکه عصبی تاثیر منفی بزاره(که قطعا میزاره)

بعد از تلاش های فراوان و خروجی های مختلف و ناکارآمد یه راه حل جالب و قشنگ به ذهنم رسید.

راه حل درخشان بنده : با توجه به اینکه کتابخونه مدیاپایپ خیلی قدرتمنده و عملا بدون توجه به حرکت دست میتونه اون رو با دقت بالایی تشخیص بده و از طرفی روی دست نقطه گذاری میکنه، میتونیم از این قابلیت استفاده مثمرثمری داشته باشیم.اونم اینکه به جای تصویر خود دست، صرفا تصویر این نقاط رو برای آموزش شبکه عصبیمون استفاده کنیم.

نقاط دست در فریم بالا
نقاط دست در فریم بالا

خب بیاین با این روش قدم به قدم شبکمون رو آماده کنیم.

1-جمع کردن دیتاست: با کمک گرفتن از چت جی پی تی عزیزمون یه کد تمیز برای جمع کردن دیتاستی از نقاط دست فراهم کردیم.توی کد زیر مقدار IMG_SIZE عدد 5000 هست به این معنا که از هر حالت سنگ و کاغذ و قیچی 5000 تا نمونه قراره بگیریم.به این صورت که با شروع برنامه شما با انتخاب یکی از حروف s یا p یا r که حرف اول سنگ و کاغذ و قیچی هست(مسلما به انگلیسی دیگه :)) برنامه شروع به کار میکنه، دستتون رو میگیرید جلوش به صورتی که میخواید و بهتره که دستتون رو دائما حرکت بدید تا حالات مختلف یک وضعیت مثل سنگ نمونه برداری بشه و شبکه بتونه حالات بیشتری رو پیش بینی کنه.

import cv2
import mediapipe as mp
import numpy as np
import os

# Initialize Mediapipe
mp_hands = mp.solutions.hands
mp_drawing = mp.solutions.drawing_utils
hands = mp_hands.Hands(static_image_mode=False, max_num_hands=1, min_detection_confidence=0.7)

# Create directories for storing the images
os.makedirs('rock', exist_ok=True)
os.makedirs('paper', exist_ok=True)
os.makedirs('scissors', exist_ok=True)

# Count of each gesture image
IMG_SIZE = 5000

# Function to preprocess landmarks and save as image
def preprocess_and_save_landmarks(landmarks, frame, save_path, img_size=128):
# Get the frame dimensions
h, w, _ = frame.shape

# Get the bounding box of the hand
x_min = min([landmark.x for landmark in landmarks])
x_max = max([landmark.x for landmark in landmarks])
y_min = min([landmark.y for landmark in landmarks])
y_max = max([landmark.y for landmark in landmarks])

# Calculate the center and size of the bounding box
x_center = (x_min + x_max) / 2
y_center = (y_min + y_max) / 2
bbox_size = max(x_max - x_min, y_max - y_min)

# Calculate the scaling factor and translate the landmarks
scale = img_size / bbox_size
black_bg = np.zeros((img_size, img_size, 3), dtype=np.uint8)
for landmark in landmarks:
x = int((landmark.x - x_center) * scale + img_size / 2)
y = int((landmark.y - y_center) * scale + img_size / 2)
cv2.circle(black_bg, (x, y), 2, (255, 0, 0), -1)

cv2.imwrite(save_path, black_bg)

# Open the webcam
cap = cv2.VideoCapture(0)

gesture = None
img_count = 0
save_dir = ""

while True:
success, frame = cap.read()
if not success:
break

# Flip the frame horizontally for a later selfie-view display
frame = cv2.flip(frame, 1)

# Convert the BGR image to RGB
rgb_frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)

# Process the frame and detect hands
results = hands.process(rgb_frame)

# Display instructions
if gesture is None:
cv2.putText(frame, "Press 'r' for Rock, 'p' for Paper, 's' for Scissors",
(10, 30), cv2.FONT_HERSHEY_SIMPLEX, 1, (255, 0, 0), 2, cv2.LINE_AA)
else:
cv2.putText(frame, f"Capturing {gesture} images: {img_count}/5000",
(10, 30), cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 255, 0), 2, cv2.LINE_AA)

if results.multi_hand_landmarks and gesture is not None and img_count < IMG_SIZE:
for hand_landmarks in results.multi_hand_landmarks:
# Draw hand landmarks on the frame
mp_drawing.draw_landmarks(frame, hand_landmarks, mp_hands.HAND_CONNECTIONS)

# Extract the landmarks
landmarks = hand_landmarks.landmark

# Save the preprocessed landmarks as an image
save_path = os.path.join(save_dir, f"{gesture}_{img_count:04d}.png")
preprocess_and_save_landmarks(landmarks, frame, save_path)
img_count += 1

if img_count >= IMG_SIZE:
gesture = None
img_count = 0

# Display the frame
cv2.imshow('Capture Hand Gestures', frame)

# Handle keypress events
key = cv2.waitKey(1)
if key == ord('r'):
gesture = 'rock'
save_dir = 'rock'
elif key == ord('p'):
gesture = 'paper'
save_dir = 'paper'
elif key == ord('s'):
gesture = 'scissors'
save_dir = 'scissors'
elif key == ord('q'):
break

# Release the webcam and close the window
cap.release()
cv2.destroyAllWindows()

2-ساخت و آموزش شبکه عصبی

باز به پیشنهاد چت جی پی تی جان مدلی با ساختار زیر رو ساختیم :

ساختار شبکه عصبی
ساختار شبکه عصبی

و بعد از آموزش این شبکه با 12000 تصویر آموزشی و تست اون با 3000 تصویر تستی به دقت 100 درصد توی هر دو رسیدیم.

و عملا کار دیگه جَمعه...

تنها کاری که مونده اینه که مدلمون رو سیو کنیم و توی برنامه اصلی از اون برای پیش بینی حالت دست وقتی که شمارنده به 3 میرسه استفاده کنیم و متناسب با اون حرکت هوش مصنوعی رو مشخص کنیم.

به همین سادگی و خوشمزگی. اگر دوست داشتین نسخه کامل کد رو ببنید به گیت هاب بنده سری بزنید.

ضمن اینکه اگر میخواید نسخه ویدیوییش رو هم ببنید میتونید این ویدیو رو از کانال یوتیوب من مشاهده کنید.

ممنون از وقتی که برای مطالعه گذاشتید...