Demonstration
Project code
import cv2
import mediapipe as mp
import numpy as np
# Initialize MediaPipe Hand Tracking
mp_draw = mp.solutions.drawing_utils
mp_hands = mp.solutions.hands
# Function to get all hand joint coordinates
def get_hand_j(hand_landmarks):
joint_coords = [(landmark.x, landmark.y) for landmark in hand_landmarks.landmark]
return joint_coords
# Function to convert normalized coordinates to pixel coordinates
def norm_to_pixel_coords(x, y, width, height):
return int(x * width), int(y * height)
# Ball properties
ball_color = (0, 255, 0) # Green (B, G, R)
# Function to set the starting position of a ball
def start_point(x, y):
return (x, y)
# Parameters for ball movement and interpolation
spd = 5
interp_fact = 0.2 # Interpolation factor for the ball's position, between 0 and 1.
# Initialize OpenCV window
cv2.namedWindow("Maze", cv2.WINDOW_NORMAL)
cv2.resizeWindow("Maze", 1000, 800)
cap = cv2.VideoCapture(0)
width, height = 800, 600
# Store previous joint coordinates
prev_joint_coords = []
def is_colliding(x, y, maze_rectangles, ball_r):
for rect in maze_rectangles:
x1, y1, x2, y2 = rect
if (x - ball_r < x2) and (x + ball_r > x1) and (y - ball_r < y2) and (y + ball_r > y1):
return True
return False
# Create function for maze 1(Easy)
def maze_1(frame):
ball_r = 50
max_dist = ball_r + 7
maze_rectangles = [
(0, 0, 500, 200),
(0, 400, 800, 600),
(700, 0, 800, 400),
]
end_line = (500, 50, 700, 50)
for rect in maze_rectangles:
x1, y1, x2, y2 = rect
cv2.rectangle(frame, (x1, y1), (x2, y2), (250, 0, 0), -1)
cv2.line(frame, (end_line[0], end_line[1]), (end_line[2], end_line[3]), (0, 0, 255), 2)
return maze_rectangles, ball_positions_maze_1, end_line, ball_r, max_dist
#Medium
def maze_2(frame):
ball_r = 20
max_dist = ball_r + 7
maze_rectangles = [
(700, 100, 800, 600),
(400, 100, 700, 200),
(500, 300, 600, 500),
(400, 200, 500, 300),
(100, 300, 600, 400),
(100, 400, 200, 500),
(300, 500, 400, 600),
(0, 0, 200, 100),
(100, 100, 300, 200),
]
end_line = (730, 0, 730, 100)
for rect in maze_rectangles:
x1, y1, x2, y2 = rect
cv2.rectangle(frame, (x1, y1), (x2, y2), (250, 0, 0), -1)
cv2.line(frame, (end_line[0], end_line[1]), (end_line[2], end_line[3]), (0, 0, 255), 2)
return maze_rectangles, ball_positions_maze_2, end_line, ball_r, max_dist
#hard
def maze_3(frame):
ball_r = 10
max_dist = ball_r + 10
maze_rectangles = [
(0, 0, 50, 600),
(100, 0, 150, 500),
(200, 100, 250, 600),
(300, 0, 350, 500),
(400, 100, 450, 600),
(500, 0, 550, 500),
(600, 100, 650, 600),
(700, 0, 750, 500),
(50, 0, 800, 50),
(50, 550, 800, 600),
]
end_line = (750, 120, 800, 120)
for rect in maze_rectangles:
x1, y1, x2, y2 = rect
cv2.rectangle(frame, (x1, y1), (x2, y2), (250, 0, 0), -1)
cv2.line(frame, (end_line[0], end_line[1]), (end_line[2], end_line[3]), (0, 0, 255), 2)
return maze_rectangles, ball_positions_maze_3, end_line, ball_r, max_dist
#Custom maze slot, the above presets as a basis for your own.
#Remember to add the starting position at line 206
def maze_4(frame):
#ball_r = ?
# max_dist = ball_r + 7
#maze_rectangles = [
# ?
#]
#end_line = ? # Example end line
for rect in maze_rectangles:
x1, y1, x2, y2 = rect
cv2.rectangle(frame, (x1, y1), (x2, y2), (250, 0, 0), -1)
cv2.line(frame, (end_line[0], end_line[1]), (end_line[2], end_line[3]), (0, 0, 255), 2)
return maze_rectangles, ball_positions_maze_4, end_line, ball_r, max_dist
selected_maze = 0 # value used to switch between mazes
#used to record mouse positions
global mouse_x, mouse_y
mouse_x, mouse_y = 0, 0
#setting starting menu
def draw_menu(frame):
font = cv2.FONT_HERSHEY_SIMPLEX
cv2.putText(frame, 'Select your maze difficulty!', (200, 50), font, 1, (179, 67, 196), 2, cv2.LINE_AA)
boldness = 2 if 50 < mouse_y < 100 else 1
cv2.putText(frame, 'Beginner', (50, 100), font, 1, (255, 255, 255), boldness, cv2.LINE_AA)
boldness = 2 if 100 < mouse_y < 150 else 1
cv2.putText(frame, 'Intermediate', (50, 150), font, 1, (255, 255, 255), boldness, cv2.LINE_AA)
boldness = 2 if 150 < mouse_y < 200 else 1
cv2.putText(frame, 'Hard', (50, 200), font, 1, (255, 255, 255), boldness, cv2.LINE_AA)
boldness = 2 if 200 < mouse_y < 250 else 1
cv2.putText(frame, 'Custom', (50, 250), font, 1, (255, 255, 255), boldness, cv2.LINE_AA)
boldness = 1
cv2.putText(frame, 'Your objective is to get the green ball', (85, 360), font, 1, (56, 216, 99), boldness,
cv2.LINE_AA)
cv2.putText(frame, ' to the red finish line!', (200, 400), font, 1, (56, 216, 99), boldness, cv2.LINE_AA)
cv2.putText(frame, 'To exit press the SpaceBar', (200, 550), font, 1, (70, 34, 188), boldness, cv2.LINE_AA)
#setting victory screen
def victory_menu(frame):
font = cv2.FONT_HERSHEY_SIMPLEX
cv2.putText(frame, 'CONGATULATIONS!', (260, 50), font, 1, (179, 67, 196), 2, cv2.LINE_AA)
cv2.putText(frame, 'You completed the maze!', (200, 100), font, 1, (179, 67, 196), 2, cv2.LINE_AA)
boldness = 4 if 255 < mouse_y < 330 else 1
cv2.putText(frame, 'Play Again?', (320, 300), font, 1, (255, 255, 255), boldness, cv2.LINE_AA)
boldness = 4
cv2.putText(frame, 'To exit press the SpaceBar', (200, 550), font, 1, (70, 34, 188), boldness, cv2.LINE_AA)
# Setting up mouse events for selections
def mouse_event(event, x, y, flags, param):
global selected_maze, mouse_x, mouse_y, state
mouse_x, mouse_y = x, y
if event == cv2.EVENT_LBUTTONDOWN:
if state == 'menu':
if 50 < y < 100:
selected_maze = 1
elif 100 < y < 150:
selected_maze = 2
elif 150 < y < 200:
selected_maze = 3
elif 200 < y < 250:
selected_maze = 4
elif state == 'victory':
if 255 < y < 330:
state = 'menu'
# Create a blank frame for menu
menu_frame = np.full((height, width, 3), (192, 192, 192), dtype=np.uint8)
#initialize callback events for the maze game window
cv2.setMouseCallback("Maze", mouse_event)
#set starting state
state = 'menu'
#Setup and initalize Mediapipe hands
hands = mp_hands.Hands(static_image_mode=False, max_num_hands=2, min_detection_confidence=0.5)
prev_joint_coords_hand1 = []
prev_joint_coords_hand2 = []
#Main loop
while True:
if state == 'menu':
ball_positions_maze_1 = [start_point(65, 300)]
ball_positions_maze_2 = [start_point(550, 250)]
ball_positions_maze_3 = [start_point(75, 90)]
#custom maze starting position
ball_positions_maze_4 = [start_point(550, 250)]
menu_frame.fill(192) # Reset menu frame
draw_menu(menu_frame)
cv2.imshow("Maze", menu_frame)
if selected_maze in [1, 2, 3, 4]: #check for selected maze
state = 'game'
if cv2.waitKey(1) & 0xFF == ord(' '):
break
elif state == 'victory':
menu_frame.fill(192) # Reset menu frame
victory_menu(menu_frame)
cv2.imshow("Maze", menu_frame)
if cv2.waitKey(1) & 0xFF == ord(' '):
break
elif state == 'game':
ret, cam_frame = cap.read()
if not ret:
print("Failed to read frame.")
break
# Flipping camera for intuitiveness
cam_frame = cv2.flip(cam_frame, 1)
rgb_frame = cv2.cvtColor(cam_frame, cv2.COLOR_BGR2RGB)
# Process the frame with MediaPipe Hands
result = hands.process(rgb_frame)
frame = np.full((height, width, 3), (192, 192, 192), dtype=np.uint8)
# Loading the selected maze
if selected_maze == 1:
maze_rectangles, ball_pos, end_line, ball_r, max_dist = maze_1(frame)
elif selected_maze == 2:
maze_rectangles, ball_pos, end_line, ball_r, max_dist = maze_2(frame)
elif selected_maze == 3:
maze_rectangles, ball_pos, end_line, ball_r, max_dist = maze_3(frame)
elif selected_maze == 4:
maze_rectangles, ball_pos, end_line, ball_r, max_dist = maze_4(frame)
# If hand landmarks are detected
if result.multi_hand_landmarks:
for hand_idx, hand_landmarks in enumerate(result.multi_hand_landmarks):
# Get all hand joint coordinates
joint_coords = get_hand_j(hand_landmarks)
# Convert normalized coordinates to pixel coordinates
joint_coords = [norm_to_pixel_coords(x, y, width, height) for x, y in joint_coords]
# Store the current joint coordinates as previous joint coordinates for the next frame
if hand_idx == 0: # For the first hand
prev_joint_coords = prev_joint_coords_hand1
else: # For the second hand
prev_joint_coords = prev_joint_coords_hand2
# Updating ball positions based on hand movement
for b_index, ball_position in enumerate(ball_pos):
for i, (joint_x, joint_y) in enumerate(joint_coords):
if len(prev_joint_coords) > i:
prev_joint_x, prev_joint_y = prev_joint_coords[i]
dx, dy = joint_x - ball_position[0], joint_y - ball_position[1]
distance = np.sqrt(dx * dx + dy * dy)
# Initializing new_x and new_y values
new_x = ball_position[0]
new_y = ball_position[1]
if distance < max_dist:
new_x = int(ball_position[0] + spd * (joint_x - prev_joint_x))
new_y = int(ball_position[1] + spd * (joint_y - prev_joint_y))
# Making sure the ball stays within frame boundaries
new_x = max(ball_r, min(new_x, width - ball_r))
new_y = max(ball_r, min(new_y, height - ball_r))
if not is_colliding(new_x, new_y, maze_rectangles, ball_r):
# Update the ball position position using linear interpolation
interpolated_x = int(ball_position[0] + interp_fact * (new_x - ball_position[0]))
interpolated_y = int(ball_position[1] + interp_fact * (new_y - ball_position[1]))
ball_pos[b_index] = (interpolated_x, interpolated_y)
# Checking if the ball crosses the end line
if ((end_line[0] - ball_r / 4 <= new_x <= end_line[0] + ball_r / 4) and
(end_line[1] - ball_r / 4 <= new_y <= end_line[3] + ball_r / 4)):
selected_maze = 0
print("state chang - menu")
#resetting back to victory state
state = 'victory'
break
# Store the current joint coordinates as previous joint coordinates for the next frame
if hand_idx == 0: # For the first hand
prev_joint_coords_hand1 = joint_coords
else: # For the second hand
prev_joint_coords_hand2 = joint_coords
# Draw the new hand design ( colour is changeable and so is the thickness of the line although it doenst affect to gameplay yet)
line_color = (0, 0, 255)
line_thickness = 2
for connection in mp_hands.HAND_CONNECTIONS:
start_idx, end_idx = connection
start_x, start_y = joint_coords[start_idx]
end_x, end_y = joint_coords[end_idx]
cv2.line(frame, (start_x, start_y), (end_x, end_y), line_color, line_thickness)
# Drawing ball ( is able to draw more than 1 :) )
for ball_position in ball_pos:
cv2.circle(frame, ball_position, ball_r, ball_color, -1)
# Showing the frame
cv2.imshow("Maze", frame)
# exiting if spacebar is pressed
if cv2.waitKey(1) & 0xFF == ord(" "):
break
# Exiting
cap.release()
cv2.destroyAllWindows()