YOLOV5로 Hangman 게임을 만들어보자 #4-문제 풀기

이영준·2022년 6월 6일
0
post-thumbnail

문제를 띄워주는 것이 완료됐으니, 본격적으로 main코드에서 직접 사용자가 알파벳 값들을 입력해서 문제를 풀어보게 할 것이다.

🤮전체 main 코드


import copy
import os, random
from picture import *
from gameplay import *

CANVAS = 'hangman/canvas.png'
MOSAIC_RATE = 20
points = 0


for i in range(5):
    all_right = True
    IMAGE = "./photos/" + random.choice(os.listdir("./photos"))
    try:
        erase_mat()
    except:
        pass

    object_cords1 = object_cords(IMAGE)  # 원래 이미지의 좌표값과 레이블을 구한다
    removing_list = []
    for x in object_cords1:
        if x['confidence'] < 0.5:
            removing_list.append(x)
    for val in removing_list:
        object_cords1.remove(val)


    while object_cords1:
        new_object_cords = copy.deepcopy(object_cords1)


        dst, shrinked_cords = picture_yolo_mosaic(IMAGE, MOSAIC_RATE,
                                                  new_object_cords)  # 이미지를 모자이크 하고, 사진을 축소하는 과정에서 객체들의 캔버스의 절대 좌표값도 구한다


        hangman = show_quiz()  # 행맨 캔버스를 띄운다
        cv2.putText(hangman, "Letters used:", (80, 540), \
                    cv2.FONT_HERSHEY_SIMPLEX, 1, \
                    (0, 0, 0), 2)
        erase_area(hangman)
        draw_points(hangman, points)
        cv2.imshow("Hangman", hangman)

        show_question(dst, hangman)  # 문제를 보여주고 다시 띄운다

        chosen_value = keyboard_chooseobj()
        if not chosen_value:
            break
        chosen_answer = chosen_value['name']
        object_cords_index = shrinked_cords.index(chosen_value)
        chosen_answer_lower = chosen_answer.lower()
        char_rects = get_char_coords(chosen_answer)
        draw_blank_rects(chosen_answer, char_rects, hangman)

        chars_entered = []
        letter_x = 300
        letter_y = 540

        index = 0
        while True:
            letter_count = 0
            for letters in chosen_answer.strip():
                if letters in chars_entered:
                    letter_count += 1
            if letter_count == len(chosen_answer.replace(' ', '')):
                points += 10
                erase_area(hangman)
                cv2.imshow("Hangman", hangman)
                draw_points(hangman, points)
                cv2.imshow("Hangman", hangman)
                draw_won(hangman)
                cv2.waitKey()
                letter_x = 300
                letter_y = 540
                chars_entered = []
                del object_cords1[object_cords_index]
                # shrinked_cords.remove(chosen_value)
                break
            key = cv2.waitKey()
            if 65 <= key <= 90 or 97 <= key <= 122:
                key = chr(key).lower()
                if key not in chars_entered:
                    chars_entered.append(key)
                    cv2.putText(hangman, key, (letter_x, letter_y), \
                                cv2.FONT_HERSHEY_SIMPLEX, 1, \
                                (255, 0, 0), 2)
                    letter_x += 30
                    erase_area(hangman)
                    draw_points(hangman, points)
                    cv2.imshow("Hangman", hangman)
                    if key in chosen_answer_lower:
                        displayLetter(hangman, key, chosen_answer_lower, char_rects)
                    else:
                        draw_hangman(index, hangman)
                        index += 1
                        if index == 4:
                            hangman1 = copy.deepcopy(hangman)
                            hangman1 = draw_hint(hangman1)
                            cv2.imshow("Hangman", hangman1)
                            cv2.waitKey(1000)

                            object_cords2 = copy.deepcopy(object_cords1)
                            delete_val = object_cords2[object_cords_index]
                            del object_cords2[object_cords_index]
                            delete_val_list = []
                            delete_val_list.append(delete_val)
                            dst, a = picture_yolo_mosaic(IMAGE, MOSAIC_RATE, object_cords2)
                            show_question(dst, hangman)
                            delete_val_list = shrink_object_cords(IMAGE, delete_val_list)
                            box_on_hangman(delete_val_list, hangman, (255, 255, 255))
                            points -= 5
                            erase_area(hangman)
                            draw_points(hangman, points)
                            cv2.imshow("Hangman", hangman)
                            cv2.waitKey(1000)
                        if index == 6:
                            del object_cords1[object_cords_index]
                            hangman2 = copy.deepcopy(hangman)
                            hangman2 = draw_wrong(hangman2)
                            points -= 10
                            all_right = False
                            erase_area(hangman)
                            draw_points(hangman, points)
                            cv2.imshow("Hangman", hangman2)
                            cv2.waitKey()
                            break
            elif key == 27:
                break

    # cv2.waitKey(0)
    # if chosen_answer:
    #     reveal_answer(chosen_answer, hangman, char_rects)

    if all_right:
        points+=30
        erase_area(hangman)
        draw_points(hangman, points)
        cv2.imshow("Hangman", hangman)
        show_answer(hangman)
    end_key = cv2.waitKeyEx()
    if end_key == 0x1B:
        break

erase_all_area(hangman)
draw_final_points(hangman, points)
cv2.imshow("Hangman", hangman)
cv2.waitKey()
cv2.destroyAllWindows()
erase_mat()
import copy
import os, random
from picture import *
from gameplay import *

CANVAS = 'hangman/canvas.png'
MOSAIC_RATE = 20
points = 0
answer_label = ""

📌헤더 선언

앞서 설명한 함수들의 정의를 picture.py와 gameplay.py에 정의하였기 때문에 이들을 가져왔으며, 그림판으로 그린 canvas파일을 CANVAS로 정의하였고, 모자이크 율과 초기 점수를 선언하였다.

import copy
import os, random
from picture import *
from gameplay import *
CANVAS = 'hangman/canvas.png'
MOSAIC_RATE = 20
points = 0

깊은 복사를 통해 캔버스를 수정할 일이 있기 때문에 copy를 import하고, 디렉토리 내의 파일을 삭제 하는 등 작업을 위해 os를, 사진을 무작위로 선택하기 위해 random을 import하였다.

📌퀴즈를 위한 사전 작업

for i in range(5):
    all_right = True
    IMAGE = "./photos/" + random.choice(os.listdir("./photos"))
    try:
        erase_mat()
    except:
        pass

for문을 통해 5번의 문제를 주어지는 것으로 하였고, 한 문제에서 모든 객체 값을 맞춘다면 추가 점수를 주기 위해 all_right 변수를 추가하였다.
IMAGE = "./photos/" + random.choice(os.listdir("./photos"))는 photos디렉토리 내의 무작위 파일을 이미지 파일로 받아온다. 파이토치에서 받아온 yolo모델은 탐지한 사진들의 정답 사진
을 runs/detect 디렉토리 밑에 저장한다. 따라서 저장이 큰 의미가 없다고 생각하여 erase_mat 함수를 추가적으로 정의하여, 지울 사진이 있는 경우에 한해 erase_mat()을 실행하였다.

🔑디렉토리 내의 의미없는 사진 지우기

def erase_mat():
    shutil.rmtree('runs/detect//')

object_cords1 = object_cords(IMAGE)  # 원래 이미지의 좌표값과 레이블을 구한다
    removing_list = []
    for x in object_cords1:
        if x['confidence'] < 0.5:
            removing_list.append(x)
    for val in removing_list:
        object_cords1.remove(val)

다음으로 무작위로 받은 사진의 좌표값 리스트 중에서, 탐지의 신뢰도가 0.5보다 적은 경우에는 해당 객체의 좌표값, 레이블 등을 삭제하여 박스쳐질 일이 없도록 하였다.

📌문제 띄우기

     while object_cords1:
        new_object_cords = copy.deepcopy(object_cords1)


        dst, shrinked_cords = picture_yolo_mosaic(IMAGE, MOSAIC_RATE,
                                                  new_object_cords)  # 이미지를 모자이크 하고, 사진을 축소하는 과정에서 객체들의 캔버스의 절대 좌표값도 구한다


        hangman = show_quiz()  # 행맨 캔버스를 띄운다
        cv2.putText(hangman, "Letters used:", (80, 540), \
                    cv2.FONT_HERSHEY_SIMPLEX, 1, \
                    (0, 0, 0), 2)
        erase_area(hangman)
        draw_points(hangman, points)
        cv2.imshow("Hangman", hangman)

        show_question(dst, hangman)  # 문제를 보여주고 다시 띄운다

앞선 포스트에서 설명한 문제를 띄워주는 코드를 여기서 실행한다.
erase_area(hangman)함수는

def erase_area(img):
    cv2.rectangle(img, (800, 40), (1200, 80), (255, 255, 255), -1)

"You're Right!" , "You're Wrong!", "Hint!" 등등 설명을 위해 캔버스에 글씨를 쓰는 과정에서 글씨가 겹쳐 보이지 않도록 rectangle 함수로 덮어주는 역할을 한다.(쉽게 말해 글씨위에 덮고 글씨를 다시 쓰는 것,,, 더 효율적인 방법이 분명 있을텐데^^ㅎㅎ)

그리고 draw_points함수는 points를 매개변수로 가져와 캔버스에 점수를 보여주는 역할을 하는 함수이다.

def draw_points(img, points):
    cv2.putText(img, "POINTS : " + str(points), (800, 60), \
                cv2.FONT_HERSHEY_SIMPLEX, 0.7, \
                (0, 255, 0), 2)
    return img

📌키보드 입력받아 알파벳들을 캔버스에 표시하기


        chosen_value = keyboard_chooseobj()
        if not chosen_value:
            break
        chosen_answer = chosen_value['name']
        object_cords_index = shrinked_cords.index(chosen_value)
        chosen_answer_lower = chosen_answer.lower()
        char_rects = get_char_coords(chosen_answer)
        draw_blank_rects(chosen_answer, char_rects, hangman)
        chars_entered = []
        letter_x = 300
        letter_y = 540

먼저 keyboard_chooseobj()로 받은 객체의 값의 정보들을 저장한다.
chosen_answer은 레이블 값, object_cords_index는 그 객체의 인덱스 값을 받아주고, char_rects는 레이블 문자열의 박스 좌표를 저장한다.
이를 통해 draw_blank_rects로 문제 박스를 그려준다. letter_x와 letter_y는 입력 받는 알파벳 값들을 넣어주는 x,y좌표를 뜻한다.


		index = 0
        while True:
            letter_count = 0
            for letters in chosen_answer.strip():
                if letters in chars_entered:
                    letter_count += 1
            if letter_count == len(chosen_answer.replace(' ', '')):
                points += 10
                erase_area(hangman)
                cv2.imshow("Hangman", hangman)
                draw_points(hangman, points)
                cv2.imshow("Hangman", hangman)
                draw_won(hangman)
                cv2.waitKey()
                letter_x = 300
                letter_y = 540
                chars_entered = []
                del object_cords1[object_cords_index]
                # shrinked_cords.remove(chosen_value)
                break

우선 문자열의 공백을 없애주고, 알파벳이 맞을 때마다 count를 더해준다. 객체를 모두 맞혔을 경우 points에 10을 더하고 draw_won함수

def draw_won(img):
    cv2.putText(img, "YOU'RE RIGHT! PRESS ENTER TO MOVE ON", (120, 60), \
                cv2.FONT_HERSHEY_SIMPLEX, 0.7, \
                (0, 255, 0), 2)
    cv2.imshow("Hangman", img)

를 통해 맞혔음을 사용자에게 보여준다.
마지막으로 letter_x,letter_y,chars_entered를 모두 초기화해주고 object_cords1에 있는 이미 맞춘 객체를 삭제해준다.


            key = cv2.waitKey()
            if 65 <= key <= 90 or 97 <= key <= 122:
                key = chr(key).lower()
                if key not in chars_entered:
                    chars_entered.append(key)
                    cv2.putText(hangman, key, (letter_x, letter_y), \
                                cv2.FONT_HERSHEY_SIMPLEX, 1, \
                                (255, 0, 0), 2)
                    letter_x += 30
                    erase_area(hangman)
                    draw_points(hangman, points)
                    cv2.imshow("Hangman", hangman)

정답이 아직 모두 입력되지 않은 경우에는 계속 키보드를 입력받고 아직 입력 받지 않은 key값들을 chars_entered에 저장하여 일정 간격으로 사용자에게 이미 입력을 한 알파벳들을 보여준다.

📌맞는 알파벳을 박스에 표시하고 Hangman 그리기

                    if key in chosen_answer_lower:
                        displayLetter(hangman, key, chosen_answer_lower, char_rects)
                    else:
                        draw_hangman(index, hangman)
                        index += 1

내가 입력한 알파벳이 문자열에 있다면 displayLetter함수를 통해 박스 안에 알파벳을 그려준다.

def displayLetter(img, letter, word, char_rects):
    for i in range(len(word)):
        if word[i] == letter:
            top_left, bottom_right = char_rects[i]
            cv2.putText(img, word[i], \
                        (top_left[0], bottom_right[1]), \
                        cv2.FONT_HERSHEY_SIMPLEX, \
                        1, (0, 0, 0), 2)
    cv2.imshow("Hangman", img)
    return img

그리고 틀렸다면, draw_hangman을 통해 틀린 개수에 따른 행맨의 모양을 그려주고 몇 번 틀렸는지를 저장하는 index의 값을 올려준다.

🔑틀릴 때마다 hangman 그려주기

def draw_hangman(index, img):
    if index == 0:
        cv2.circle(img, (245, 190), 40, (0, 0, 0), thickness=2, \
                   lineType=cv2.LINE_AA)
        cv2.imshow("Hangman", img)       
    if index == 1:
        cv2.line(img, (245, 230), (245, 320), \
                 (0, 0, 0), thickness=2, \
                 lineType=cv2.LINE_AA)
        cv2.imshow("Hangman", img)
    if index == 2:
        cv2.line(img, (245, 270), (185, 200), \
                 (0, 0, 0), thickness=2, \
                 lineType=cv2.LINE_AA)
        cv2.imshow("Hangman", img)
    if index == 3:
        cv2.line(img, (245, 270), (305, 200), \
                 (0, 0, 0), thickness=2, \
                 lineType=cv2.LINE_AA)
        cv2.imshow("Hangman", img)
    if index == 4:
        cv2.line(img, (245, 320), (185, 360), \
                 (0, 0, 0), thickness=2, \
                 lineType=cv2.LINE_AA)
        cv2.imshow("Hangman", img)
    if index == 5:
        cv2.circle(img, (245, 190), 40, (0, 0, 255), thickness=2, \
                   lineType=cv2.LINE_AA)
        cv2.line(img, (245, 230), (245, 320), \
                 (0, 0, 255), thickness=2, \
                 lineType=cv2.LINE_AA)
        cv2.line(img, (245, 270), (185, 200), \
                 (0, 0, 255), thickness=2, \
                 lineType=cv2.LINE_AA)
        cv2.line(img, (245, 270), (305, 200), \
                 (0, 0, 255), thickness=2, \
                 lineType=cv2.LINE_AA)
        cv2.line(img, (245, 320), (185, 360), \
                 (0, 0, 255), thickness=2, \
                 lineType=cv2.LINE_AA)
        cv2.line(img, (245, 320), (305, 360), \
                 (0, 0, 255), thickness=2, \
                 lineType=cv2.LINE_AA)
        reveal_answer(chosen_answer, hangman, char_rects)
        cv2.imshow("Hangman", img)

index에 따라 hangman을 머리, 몸통, 양 팔, 양 다리 순으로 순차적으로 그려주고, 6번을 틀리게 되면은(첫번째로 틀렸을 때를 index를 0으로 했으므로, 6번을 틀리면 index가 5이다) hangman 색을 빨강으로 하여 캔버스에 그린다. 여기서 reveal_answer은 정답을 빈칸에 표시해주는 것이다.

def reveal_answer(word, img, char_rects):
    for i in range(len(word)):
        top_left, bottom_right = char_rects[i]
        cv2.putText(img, word[i], (top_left[0], bottom_right[1]), \
                    cv2.FONT_HERSHEY_SIMPLEX, \
                    1, (255, 0, 0), 2)
    cv2.imshow("Hangman", img)
    return img

📌4번 틀리면 힌트를 주기

                        if index == 4:
                            hangman1 = copy.deepcopy(hangman)
                            hangman1 = draw_hint(hangman1)
                            cv2.imshow("Hangman", hangman1)
                            cv2.waitKey(1000)

                            object_cords2 = copy.deepcopy(object_cords1)
                            delete_val = object_cords2[object_cords_index]
                            del object_cords2[object_cords_index]
                            delete_val_list = []
                            delete_val_list.append(delete_val)
                            dst, a = picture_yolo_mosaic(IMAGE, MOSAIC_RATE, object_cords2)
                            show_question(dst, hangman)
                            delete_val_list = shrink_object_cords(IMAGE, delete_val_list)
                            box_on_hangman(delete_val_list, hangman, (255, 255, 255))
                            points -= 5
                            erase_area(hangman)
                            draw_points(hangman, points)
                            cv2.imshow("Hangman", hangman)
                            cv2.waitKey(1000)

사용자가 해당 객체에서 알파벳을 4번을 틀린다면, 그 객체의 모자이크 부분을 풀어준다. 먼저 기존에 모든 모자이크가 된 사진에 영향이 가지 않기 위해 hangman1 = copy.deepcopy(hangman)으로 다른 hangman mat에 모자이크를 풀었고, object_cords 역시 모자이크를 풀 객체를 리스트에서 제외하기 위하여 깊은 복사를 하였다.

그 다음 draw_hint(hangman1)함수로 힌트를 준다는 표시를 캔버스에 하였다.

def draw_hint(img):
    cv2.putText(img, "HINT!, MOSAIC CLEARED", (120, 60), \
                cv2.FONT_HERSHEY_SIMPLEX, 1, \
                (0, 255, 0), 2)
    return img

새로 좌표값들을 저장한 object_cords2를 이용해 새로 모자이크를 하여 show_question(dst,hangman)으로 문제를 다시 보여줬고, 제외된 모자이크가 풀린 객체는 선택된 객체가 원래 하얀 박스가 쳐져 있으므로 이는 그대로 유지하기 위하여 box_on_hangman함수를 만들어 해당 좌표에만 하얀 박스가 쳐지도록 하였다.

def box_on_hangman(shrinked_cords, hangman, color):
    for val in shrinked_cords:
        hangman = cv2.rectangle(hangman, (int(val['xmin']) + 576, int(val['ymin']) + 100),
                                (int(val['xmax']) + 576, int(val['ymax']) + 100), color, 2)
    cv2.imshow("Hangman", hangman)

또한 힌트가 보여졌을 경우에는 5점 감점을 하였고 (points-=5), erase_area(hangman)로 기존의 point가 표시된 부분을 덮은 후 draw_points(hangman, points)로 새로운 점수를 출력하였다.

📌6번 모두 틀리면 다음으로 넘어가기

                        if index == 6:
                            del object_cords1[object_cords_index]
                            hangman2 = copy.deepcopy(hangman)
                            hangman2 = draw_wrong(hangman2)
                            points -= 10
                            all_right = False
                            erase_area(hangman)
                            draw_points(hangman, points)
                            cv2.imshow("Hangman", hangman2)
                            cv2.waitKey()
                            break

6번의 기회를 모두 소진하면, 최종적으로 이 객체는 '틀린' 것이 된다. 그러면 object_cords1에서 해당 객체를 제외하고, 새로 깊은 복사를 한 hangman2(글씨 위에 글씨가 안써지도록 깊은복사) 위에 draw_wrong함수로 틀렸다는 것을 그려 사용자에게 표시해준다.

def draw_wrong(img):
    cv2.putText(img, "WRONG! CHECK ANSWER AND PRESS ENTER TO MOVE ON", (120, 60), \
                cv2.FONT_HERSHEY_SIMPLEX, 0.7, \
                (0, 255, 0), 2)
    return img

points-=10 으로 점수는 10점이 감점되고, 한 문제에서 틀린 객체가 있으므로 all_right = False로 변경해준다. 마지막으로 바뀐 점수를 캔버스에 그려준다.

cv2.waitKey()에서 기보드 입력를 받아 다음 문제로 넘어간다

📌한 문제가 끝났을 때


    if all_right:
        points+=30
        erase_area(hangman)
        draw_points(hangman, points)
        cv2.imshow("Hangman", hangman)
        show_answer(hangman)
    else:
        show_answer(hangman)
    end_key = cv2.waitKeyEx()
    if end_key == 0x1B:
        break

문제의 모든 객체를 틀리지 않을 경우 all_right가 그대로 True로 저장되어 있다. 이 경우에는 30점을 추가로 준다.

또한 문제 하나가 모두 끝나면 show_answer함수를 통해

신뢰도와 바운딩 박스가 있는 정답화면을 한 번 더 보여주고 다음문제로 넘어간다.

def show_answer(hangman):
    answer_img = resize_picture('runs/detect/exp/image0.jpg')
    height, width, channel = answer_img.shape
    hangman[100:height + 100, 576:576 + width] = answer_img
    cv2.imshow("Hangman", hangman)

Yolov5모델이 자동으로 객체탐지를 시행한 사진을 runs/detect/exp/image0.jpg으로 저장하기 때문에 위 사진을 크기를 줄여서(picture_yolo_mosaic에서 줄인 만큼) 캔버스에 그려준다.

def show_answer(hangman):
    answer_img = resize_picture('runs/detect/exp/image0.jpg')
    height, width, channel = answer_img.shape
    hangman[100:height + 100, 576:576 + width] = answer_img
    cv2.imshow("Hangman", hangman)

이렇게 한 문제가 끝난 후 다시 윗줄의 for i in range(5)반복문으로 돌아가 코드를 반복한다.

📌모든 문제를 다 풀었을 때

erase_all_area(hangman)
draw_final_points(hangman, points)
cv2.imshow("Hangman", hangman)
cv2.waitKey()
cv2.destroyAllWindows()
erase_mat()

erase_all_area(hangman)으로

def erase_all_area(img):
    cv2.rectangle(img, (0, 0), (1200, 648), (255, 255, 255), -1)

모든 캔버스 구역을 하얗게 덮어주고 그 위에 draw_final_points로 마지막 최종 점수를 적어준다.

📌끝!

사진으로 하는 Hangman 코드들의 설명을 이것으로 마치겠다. 여기서 아이디어 발표에서 했던 추가 기능들 (힌트를 edge detection을 통해 제공하는 것 등)을 모두 구현하지는 못했지만, UI 사용에 큰 불편함 없이 깔끔하게 5문제를 풀 수 있는 단순하지만 단순하지만은 않은 게임을 완성했다.

소스는 모두 https://github.com/leeyjwinter/opencv_yolov5-hangman 에서 받을 수 있으며, 파이토치 모듈 사용을 위한 환경을 따로 설정해줘야 한다.

profile
컴퓨터와 교육 그사이 어딘가

0개의 댓글