본문 바로가기
프로젝트(Project) 모음

AI 프로젝트 , 두더지 게임 with Mediapipe and Python

by eteo 2022. 4. 24.

Language : Python

Library : Mediapipe

 

이 프로젝트는 웹캠 화면에 나타난 두더지를 잡아 제한시간 내에 점수를 내는 게임입니다.

Mediapipe Pose를 통해 인식된 손과 발의 좌표가 랜덤으로 출력되는 두더지 이미지 좌표범위와 일치할때 두더지를 잡을 수 있습니다.

 

 

 

 

처음 실행 시 화면에 Clap to start a game이라고 표시했다. 사용자가 적당한 거리를 두고 시작할 수 있도록 화면에 빨간 타원을 그리고 원 안에 얼굴을 위치시키라고 안내문을 적었다. 박수를치면 30초가 주어지고 게임 시작.

 

 

 

손(중지)의 좌표가 두더지이미지좌표 범위 안에 들어가면 score++하고 spark이미지를 오버레이한 후 랜덤좌표를 다시 받아서 두더지가 새로운 곳에 나타날 수 있게 한다. 제한시간이 지나면 화면에 Game Over문구와 현재 Score가 뜨며 게임이 종료된다.

 

 

아마 박수를 친 위치에 두더지가 있었어서 1점부터 시작하나 보다

 

일어서서 하는 버전. 사용자가 발끝까지 다 보이는 위치에서 게임을 시작할 수 있게 사람 실루엣을 그려놓았다. 역시 박수를 쳐야 게임이 시작한다.

 

 

 

우연히도 손이 위치한 곳에 두더지가 생성이 되서 점수가 6에서 8로 바로 오른 것 같다..이 오류는 잡아내려면 로직 수정이 좀 필요할 듯하다.

이 버전은 두더지를 발로 찰 수도 있다. 양손 또는 양발(발끝)이 두더지 좌표 범위 내에 들어오면 두더지가 잡힌다. 덕분에 게임을 더욱 역동적으로 즐길 수 있다.

 

 

 

while cap.isOpened(): 문 내에 있는 코드 中 좌표를 받는 부분.

# Extract landmarks
try:
    landmarks = results.pose_landmarks.landmark

    # Get coordinates
    rightindex = [landmarks[mp_pose.PoseLandmark.LEFT_INDEX.value].x,landmarks[mp_pose.PoseLandmark.LEFT_INDEX.value].y]
    leftindex = [landmarks[mp_pose.PoseLandmark.RIGHT_INDEX.value].x,landmarks[mp_pose.PoseLandmark.RIGHT_INDEX.value].y]       

    righthand = [rightindex[0]*w, rightindex[1]*h]
    lefthand = [leftindex[0]*w, leftindex[1]*h]

tracking 된 신체 각 랜드마크는 x, y, z, visibility 4개의 값을 가지고 있다.

 

x, y : 0~1 사이의 값으로 0,0이면 화면 최상단좌측이고 0.5,0.5면 화면의 중간 1,1이면 화면 최하단우측이다. 따라서 [rightindex[0]*w, rightindex[1]*h] 이런식으로 화면크기를 곱해서 화면상 좌표로 바꿔준다. w=640, h=480

 

z : 깊이. 카메라와의 절대적 거리는 아니고 hip을 기준으로 hip과 같은 깊이에 있으면 0, hip보다 카메라에 가까워지면 음수값으로 점점 작아지며, hip보다 카메라에서 멀어지면 점점커지는데, 3d값이라 그런지 x, y보다는 정확도가 좀 떨어지는 것 같았다. 위 코드에는 없지만 일어서서 하는 버전에는 z value를 사용했는데 왼쪽손목과 오른쪽손목이 동일선상에 있을 때(양쪽 z값 차의 절대값이 20미만, 값이 소수점이라 z에 100을 곱해 눈에 익은 숫자로 만들어줬다)만 박수로 인정하는 식이다.

 

visibility : 가시성. 0~1 사이의 값으로 얼마나 화면에 잘 보이냐를 나타낸다. 이번 프로젝트에선 안썼다.

 

변수명은 rightindex인데 왜 LEFT_INDEX cordinates를 받냐고하면 내가 위에서 cv2.flip으로 이미지반전을 시켰더니 오른쪽, 왼쪽을 거꾸로 읽어서 그렇다.

 

그리고 landmarks[대괄호 안에 들어가는 부분].x 을 랜드마크를 대표하는 숫자로 대체할 수 도 있다. 랜드마크 번호는 미디어파이프 공식 홈페이지를 참조하고, 아래와 같이 간략하게 작성 가능하다.

 

pose landmarks

rightfootindex = [landmarks[31].x, landmarks[31].y]
leftfootindex = [landmarks[32].x, landmarks[32].y]
rightfoot = [rightfootindex[0]*w, rightfootindex[1]*h]
leftfoot = [leftfootindex[0]*w, leftfootindex[1]*h]

 


 

game_start_event == false:일때만 실행되는 구문

if game_start_event == False:
    cv2.ellipse(image, (w//2, h//2-50), (72, 90) ,0 ,0, 360, (0,0,255), 0)
    cv2.putText(image, 'Clap to start a Game',
                (w//2-300, h//2-85),
                cv2.FONT_HERSHEY_SCRIPT_SIMPLEX, 2, (51, 102, 153), 3, cv2.LINE_AA)
    cv2.putText(image, 'Please keep some distance or adjust your webcam',
                (w//2-210, h//2+130),
                cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 0, 0), 1, cv2.LINE_AA)
    cv2.putText(image, 'to locate your face in circle',
                (w//2-110, h//2+160),
                cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 0, 0), 1, cv2.LINE_AA)
    overlay(image, w//2-230, h//2-185, 50, 50, clap_image)

    if get_distance(righthand, lefthand) < 80 and ( w//2-100 < nose[0] < w//2+100 and h//2-100 < nose[1] < h//2+100):
        game_start_event = True
        start_time = time.time()
  • cv2.ellipse 는 타원을 그리는 함수
  • cv2.putText 는 이미지에 문자 출력하는 함수
  • overlay는 투명한 부분이 있는 이미지를 오버레이하는 함수로 나도코딩 opencv 강좌를 참고하였음. https://www.youtube.com/watch?v=XK3eU9egll8
  • 밑의 if문은 nose의 좌표가 화면 중앙 쯤(일어서서하는 버전은 화면 상단 쯤)에 위치하고 양 손목좌표 사이의 거리가 80 미만(일어서서하는 버전은 20 미만)일 때 game_start_event 를 True로 바꾸고 start_time에 현재 시간을 저장한다.
  • # 두 포인트 사이 거리 구하는 함수↓
def get_distance(point, point1):
    x, y = point
    x1, y1 = point1
    distance = math.sqrt((x1 - x)**2 + (y1 - y)**2)
    return distance

 


프로젝트에선 투명한 부분이 있는 이미지만 사용했는데, 만약 투명한 부분이 없는 일반이미지를 영상위에 출력하려면

mole_image = cv2.imread('mole_tr100.png')
moleh, molew, _ = mole_image.shape

cv2.imread로 폴더 내 있는 이미지를 읽어오고 .shape을 통해 이미지의 height, width를 알아두고 아래와 같이 좌표를 지정해 이미지를 그릴 수 있다.

image[ry0:ry0+moleh, rx0:rx0+molew] = mole_image

.shape은 이미지의 height, width, channel 3개의 값을 읽을 수 있는데 채널은 단색이면 1, 다색이면 RGB 3, 투명한부분이 있으면 RGBA 4이다.

 

투명도가 있는 이미지라고 해도 위와 같이 읽어오면 디폴트로 채널 3개만 읽어온다.

 

알파값 포함 읽어오려면 아래처럼 , cv2.IMREAD_UNCHANGED 를 추가해 줘야 한다.

mole_image = cv2.imread('mole_tr100.png', cv2.IMREAD_UNCHANGED)

 

 


 

 

 

game_start_event == True이고 time_remaining > 0: 일 때 실행되는 구문 중 일부

참고로 time_remaining은 위에서 99로 초기화해줬고 이 아래엔 Score와 Time left 표시, 두더지 이미지 overlay하고, elif 남은 시간이 0초가 되었을 때 game_over_event를 True 바꾸는 내용 등 이 있다.

if game_start_event == True and time_remaining > 0:
    time_remaining = int(time_given - (present_time - start_time))

    if (rx0-50 < righthand[0] < rx0+50 and ry0-50 < righthand[1] < ry0+50) or (rx0-50 < lefthand[0] < rx0+50 and ry0-50 < lefthand[1] < ry0+50) :
        overlay(image, rx0, ry0, 50, 50, shine_image)
        score += 1
        rx0=random.randint(50, 590)
        ry0=random.randint(50, 430)
        r0.append(rx0)
        r0.append(ry0)

 

  • time_given은 30으로 해줬고 present_time 은 while문 안에서 계속 현재시각을 받아오는데 start_time은 게임이 시작한 순간에 저장되므로 time_remaining 은 정수값으로 30초부터 카운트다운 된다. 
  • 그 아래 if문이 어느쪽이든 한 손의 중지 인덱스가 두더지 이미지가 그려진 좌표 범위(100x100 크기) 안에 들어오면 실행되는 구문이다. shining spark이미지를 오버레이하고 score를 1점 증가, 그리고 두더지 이미지를 그릴 좌표를 random.randint 함수로 새로 받아 이제 새로운 곳에 두더지가 그려진다. 왜 범위값이 화면크기인 0~640, 0~480이 아니고 50~590, 50~480 이냐면 나도코딩님의 overlay함수를 그대로 사용하려다 보니 그렇게 됐다.

 

 


 

 

공홈 예제 안에 있던 Render detections 부분. 없는게 게임이 더 재밌는 것 같아 주석처리 했다.

# Render detections

# mp_drawing.draw_landmarks(
#     image,
#     results.pose_landmarks,
#     mp_pose.POSE_CONNECTIONS,
#     landmark_drawing_spec=mp_drawing_styles.get_default_pose_landmarks_style())

 

cv2.imshow 읽어들인 웹캠영상을 윈도우창에 보여주는 부분으로 윈도우창의 title은 Whack-A-Mole로 하였고 cv2.resize 화면크기를 두 배 키웠다.

cv2.imshow('Whack-A-Mole Game with Mediapipe Pose', cv2.resize(image, None, fx=2.0, fy=2.0))

 

프로젝트에 사용한 이미지는 구글에서 mole transparent image, spark/shine transparent image 등으로 검색하여 free for personal use인 이미지를 다운 받아 사용하였고 이미지 사이즈 편집은 아래 사이트를 통해 하였다.

https://pixlr.com/kr/

 

이후 효과음이 있으면 더 재밌어질 거 같아서 pygame 모듈을 한번 import 해보았으나 프로그램이 무거워져 바로 다시 지웠다.

# import pygame
# pygame.init()
# whack_sound = pygame.mixer.Sound('boing.wav')
# whack_sound.play()

 

 

 

참고영상

https://www.youtube.com/watch?v=06TE_U21FK4 

https://www.youtube.com/watch?v=XK3eU9egll8 

 

 

 

+ 해당 프로젝트는 파이썬 문법도 모르면서 어찌저찌 짠거라 코드가 많이 비루합니다. 깃허브에 공개되어 있고 출처표기 없이 자유롭게 사용하시면 됩니다.