토스 다른 글자 찾기 자동화

 

2024년 10월 토스에서 한글날 이벤트로 진행한 다른 글자 찾기 이벤트를 CV를 이용해 자동화 해보고자 진행한 프로젝트 입니다.

이벤트 화면에 들어가면 아래와 같은 화면에서 다른 글씨를 찾게 됩니다.

png

위 이미지를 예로 보게 된다면 재촉과 같이 생긴 단어들 중 재쵹을 시간 안에 눌러줘야 합니다.

처음에는 직접 손으로 눌러보고자 했지만 3단계, 4단계를 넘어가며 5단계를 마주한 순간 “아.. 이건 정말 운이 좋아야 할 수 있겠구나”라는 말이 나올 정도로 제한 시간이 짧아졌습니다.

그렇다면 당시 Mac OS에 추가된 기능인 아이폰 미러링 기능을 사용하여 맥에서 자동으로 화면을 클릭해 문제를 풀어보자는 생각이 들었습니다.

프로젝트 흐름

  1. 맥을 이용해 아이폰의 화면을 미러링 합니다.
  2. 맥에서 CV를 실행하여 아이폰 화면에 있는 글자를 수집합니다.
  3. 수집된 글자를 카드 단위로 쪼개고 각 카드 중 다른 글자가 있는 위치를 출력합니다.
  4. 출력이 정상적이라면 해당 위치로 마우스를 이동해 클릭합니다.

이렇게 프로젝트 흐름을 구성하고 코드 작성을 시작했습니다.

파이썬을 이용한 자동화

사용한 전역 변수는 아래와 같습니다.

bbox = (155, 339, 441, 676)  # 화면에서 캡처할 영역 설정
grid_size = (3, 3)  # 그리드 크기 설정 (4x4)

기본적으로 캡처할 박스 위치와 각 문제의 그리드를 지정해둡니다.

이번 토스 이벤트의 경우에는 문제의 정답을 맞힌 뒤에 다음 버튼을 누르지 않으면 넘어가지 않았기 때문에 이런 식으로 전역 변수에 그리드 크기를 지정해두고 코드를 사용했습니다.

def capture_screen(bbox):
    screen = capture_screen(bbox)
	
	"""
    화면의 특정 영역을 캡처하는 함수.
    bbox: (x1, y1, x2, y2) 영역
    """
    screen = np.array(ImageGrab.grab(bbox=bbox))
    screen = cv2.cvtColor(screen, cv2.COLOR_RGB2BGR)  # PIL 이미지를 OpenCV 이미지 형식으로 변환
    return screen

이후 bbox 영역에 맞춰서 화면을 캡처 후 screen 변수에 저장해 줍니다.

추후 사용할 extract_card_images 함수에서 해상도를 기준으로 영역을 자르기 때문에 정확한 위치 지정이 필요합니다.

# 카드 이미지를 그리드로 나누기
card_images = extract_card_images(screen, grid_size)

def extract_card_images(image, grid_size=(4, 4)):
    """
    주어진 이미지를 그리드 크기에 따라 카드 이미지를 분리하는 함수.
    
    image: 입력 이미지 (numpy array)
    grid_size: 그리드 크기 (행, 열)
    """
    h, w, _ = image.shape
    card_height = h // grid_size[0]
    card_width = w // grid_size[1]

    card_images = []
    
    for row in range(grid_size[0]):
        for col in range(grid_size[1]):
            x_start = col * card_width
            y_start = row * card_height
            card_img = image[y_start:y_start+card_height, x_start:x_start+card_width]
            card_images.append(card_img)

    return card_images

위와 함수에서는 이미지를 카드 크기로 나눠줍니다.

입력 이미지와 그리드 크기를 인자로 받고 그리드 크기에 맞도록 자른 후 card_images 배열에 저장해 줍니다.

# 다른 이미지를 찾기
different_index, differences = find_most_different_image(card_images)
print(f"다른 이미지는 인덱스 {different_index}에 위치합니다.")

def find_most_different_image(card_images):
    """
    각 카드별로 다른 카드들과의 차이를 모두 합산하여,
    차이점의 합이 가장 큰 카드를 반환하는 함수.
    """
    num_cards = len(card_images)
    total_diff_scores = [0] * num_cards  # 각 카드별로 다른 카드들과의 차이점 합계
    check_count = [0] * num_cards


    for i in range(num_cards):
        maxItem = -1

        for j in range(num_cards):
            if i != j:  # 자기 자신과 비교하는 것은 제외
                # 카드 i와 j의 차이를 계산
                diff = compare_images(card_images[i], card_images[j])
                if diff > total_diff_scores[i]:
                    maxItem = j
                    total_diff_scores[i] = diff

        check_count[maxItem] += 1

    #print(check_count.index(max(check_count)) + 1)

    # 차이점 합계가 가장 큰 카드를 찾음
    most_different_index = np.argmax(total_diff_scores)
    return check_count.index(max(check_count)) + 1, total_diff_scores

def compare_images(image1, image2):
    """
    두 이미지를 비교하여 차이점의 정도를 계산하는 함수.
    """
    # 이미지를 그레이스케일로 변환

    image1 = cv2.resize(image1, None, fx=2, fy=2, interpolation=cv2.INTER_LINEAR)
    image2 = cv2.resize(image2, None, fx=2, fy=2, interpolation=cv2.INTER_LINEAR)

    gray1 = cv2.cvtColor(image1, cv2.COLOR_BGR2GRAY)
    gray2 = cv2.cvtColor(image2, cv2.COLOR_BGR2GRAY)

    # 두 이미지의 절대 차이 계산
    diff = cv2.absdiff(gray1, gray2)

    # 차이의 정도를 정규화하여 합산
    diff_score = np.sum(diff)
    
    return diff_score

마지막으로 위 함수를 이용하여 차이점이 가장 큰 카드의 위치를 반환해 줍니다.

자른 이미지들의 배열을 돌면서 각 이미지를 그레이스케일로 변환하고 두 이미지의 절대 차이를 계산 후 점수를 합해줍니다.

이렇게 나온 결과에서 가장 많은 점수를 받은 항목이 다른 글자로 판단되게 됩니다.

for idx, card in enumerate(card_images):
	cv2.imshow(f"Card {idx + 1}", card)

다만 제작하고 보니 큰 문제점이 있었습니다.

프로젝트를 시작하기 전에는 해당 프로그램이 손글씨가 아니기 때문에 CV를 통해서도 손쉽게 인식이 가능할 것이라고 생각했지만 단계가 올라가면서 글씨가 작아지고 우유, 유유와 같이 거의 다르지 않은 단어들이 나오면서 CV의 성능이 기대만큼 나오지 않았습니다.

이로 인해 원래 찾아야 하는 답이 아닌 다른 칸을 찾거나 못 찾는 경우가 왕왕 발생하여 5단계에서 더 진행을 하지 못하였습니다.

추후 다시 이 프로젝트를 진행하게 된다면 CV가 아닌 구글의 Cloud Vision API로 리팩토링 해보고 싶습니다.