Graduation Project Submission CCI – Thanos Trachanis


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






Leave a Reply

Your email address will not be published. Required fields are marked *