티스토리 뷰

집 주변에 오락실이 하나 있어서 가끔 틀린그림찾기 게임을 하곤 합니다. 


그런데 난이도 설정을 어떻게 했는지.. 할 때마다 너무 어렵게 느껴지는거에요 ㅠㅠ 
승부욕에 괜히 매직아이로 문제를 풀곤 하는데 게임을 하고 나면 눈이 그렇게 아플 수가 없습니다. 
하고 나서도 이게 뭐하는건가 싶고요 ㅋㅋ

 



열받아서(?) 파이썬으로 자동화 프로그램을 만들어봤습니다. 
그래봤자 오락실에서 써먹지는 못하겠지만 집에서라도 대리만족을 ^^;;

 

사용하는 패키지는 3개이며 용도는 이렇습니다.
1. Pillow : 원본 이미지와 대상 이미지의 비교 (틀린 부분 딱 나옴)
2. OpenCV : 틀린 부분의 외곽 검출 (어디를 클릭해야 할지 알 수 있음)
3. pyautogui : 자동으로 클릭하기

 

 


먼저 이미지 비교를 위해서 틀린그림찾기 게임을 구합니다. 

요즘엔 모바일용 어플이 많아서, PC 에서 모바일 게임을 즐길 수 있는 BlueStacks 라는 프로그램을 설치하고 틀린그림찾기 게임을 아무거나 다운로드 받아서 설치했어요.

 




게임을 실행시켜보면 이렇게 생겼습니다. 심플하죠?


틀린 부분을 찾아서 클릭을 하면 화면 중간 아래에 짙은 회색으로 보이는 부분이 하나씩 채워집니다.
게임마다 5개씩 틀린 부분이 있는 것 같아요.


코드에서는 이 이미지를 각각 왼쪽 / 오른쪽으로 나눠서 추출한 다음에 비교해주면 됩니다.
1px 이라도 달라지면 안되므로 주의해야 돼요!

두 장을 겹쳐놓고 비교해보니까 어디가 다른지 티가 확 나죠?



코드는 대충 이렇게 적으면 됩니다.

 

2개 이미지의 가로, 세로 크기 및 세로 기준 시작 위치를 정의해준 다음에 각 영역에 대해 스크린샷을 떠와서 두 이미지를 비교하는거에요. 참고로 y_pos 는 게임 화면 최상단의 까만 여백 공간입니다.

import pyautogui
from PIL import ImageChops

width, height = 956, 763
y_pos = 45

src = pyautogui.screenshot(region=(0, y_pos, width, height)) # 왼쪽 이미지 추출
dest = pyautogui.screenshot(region=(963, y_pos, width, height)) # 오른쪽 이미지 추출

diff = ImageChops.difference(src, dest) # 이미지 비교하기
diff.save('diff.jpg')


이렇게 해서 저장된 diff.jpg 파일을 열어보면 이렇게 틀린 부분이 짠!! 하고 표시가 됩니다.



이제 틀린 부분은 알아냈으니, openCV 를 이용해서 각 영역을 감싸는 네모를 그려줄게요. 
그러면 네모 기준으로 중심점 좌표를 클릭하는 방식으로 자동으로 게임을 할 수 있겠죠?

코드는 이렇게 하면 되고, 알아보기 쉽게 주석으로 설명을 함께 적겠습니다 :)

 

import cv2

# 앞에서 저장한 diff.jpg 이미지를 불러옵니다.
diff_img = cv2.imread('diff.jpg')

# 이 이미지를 조금 단순하게 하기 위해서 흑백으로 변환시켜줄게요.
gray = cv2.cvtColor(diff_img, cv2.COLOR_BGR2GRAY)

# 흑백 이미지로부터 틀린 영역의 모든 경계선을 찾습니다. 
# 이 때 RETR_EXTERNAL 이란 옵션은 각 영역에 대해 가장 바깥쪽의 경계선(외곽선)을 구하라는 의미입니다.
# 가령 ◎ 와 같이 동그라미 2개가 겹친 경우 안쪽 동그라미, 바깥쪽 동그라미 모두 경계선으로 구하게 되는데 
# 이 옵션을 적용하게 되면 바깥쪽 동그라미만 구하게 됩니다.
contours, _ = cv2.findContours(gray, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE)

# 틀린 영역에 그릴 사각형의 색은 초록색으로 해주고요
COLOR = (0, 200, 0) # 초록색

# 찾은 외곽선들을 순회하면서 그 외곽선을 감싸는 사각형(boundingRect)의 좌표 정보를 구합니다.
for cnt in contours:
    if cv2.contourArea(cnt) > 100: # 사각형 크기가 100 보다 큰 경우에만 그리기
        x, y, width, height = cv2.boundingRect(cnt)
        # 그리고 얻어온 좌표에 사각형을 두께 2로 그려주면 돼요.        
        cv2.rectangle(diff_img, (x, y), (x + width, y + height), COLOR, 2)

# 사각형을 그린 이미지를 윈도우에 표시합니다
cv2.imshow('diff', diff_img)

# 사용자가 키를 입력할때까지 프로그램을 종료하지 않고 기다려주는 문장들이에요
cv2.waitKey(0)
cv2.destroyAllWindows()


이를 실행시켜보면 아래와 같이 틀린 영역에 대해서 사각형을 그린 결과물을 볼 수 있어요.

 

깔끔하죠?

 


거의 다 끝났어요!

이제 저 부분을 하나씩 하나씩 클릭만 해주면 끝입니다. 
흐흐흐.. 이제.. 매직아이 안해도 돼요 ㅠㅠ

자동 클릭은 진짜 쉬운데요.
어디 클릭할지 좌표만 딱 알려주면 끝입니다.

앞에서 작성한 openCV 코드의 반복문 안에, 

cv2.rectangle(diff_img, (x, y), (x + width, y + height), COLOR, 2)

이 부분 아래에다가 다음 4줄을 추가해줄게요.

to_x = x + (width // 2)
to_y = y + (height // 2) + y_pos
pyautogui.moveTo(to_x, to_y, duration=0.15)
pyautogui.click(to_x, to_y)

  
여기서 to_x, to_y 정보가 바로 클릭할 x, y 좌표가 되는데요.

boundingRect() 를 통해 얻어온 각 사각형 영역의 왼쪽 위 좌표가 x, y 란 이름으로 저장되어 있으므로, 거기에 각 사각형의 가로크기, 세로크기의 절반을 더해주면 이미지의 딱 중심부분이 됩니다.

그러고 나서 pyautogui 라이브러리의 click() 만 호출해줘도 되는데, 잘 이동하는지 눈으로 체크하고자 마우스 커서를 이동하는 moveTo() 까지 넣어볼게요. 이 때 duration 은 마우스가 이동하는데 걸리는 시간을 0.15 초로 지정한다는 의미입니다.

 


한 번 실행해볼까요? (불필요한 부분 편집 O, 속도 조정 X)


아주 빠르고 정확하게 틀린 부분을 찾아서 딱 클릭합니다 ㅋ 만족스럽네요!


사실 너무 좋은 라이브러리들을 가지고 이런 장난에 사용하는 것이 쬐끔 미안스럽긴 하지만 그래도 파이썬 가지고 놀기에 좋은 주제인 것 같아서 소개드렸습니다. 공부하시는 분들에게 도움되셨으면 해요~ 

보다 자세한 설명이 필요하시다면 아래 영상을 참고해주세요 ^^

 

긴 글 읽어주셔서 감사합니다.

 

 

 


전체 소스 코드

import os, time
import pyautogui
from PIL import ImageChops

# 왼쪽(원본) 이미지
# 시작 좌표 (0, 45)

# 오른쪽(비교대상) 이미지
# 시작 좌표 (963, 45)

# 이미지 크기
# width 956
# height 763

width, height = 956, 763
y_pos = 45

src = pyautogui.screenshot(region=(0, y_pos, width, height))
src.save('src.jpg')

dest = pyautogui.screenshot(region=(963, y_pos, width, height))
dest.save('dest.jpg')

diff = ImageChops.difference(src, dest)
diff.save('diff.jpg')

# 파일 생성 대기
while not os.path.exists('diff.jpg'):
    time.sleep(1)

import cv2
src_img = cv2.imread('src.jpg')
dest_img = cv2.imread('dest.jpg')
diff_img = cv2.imread('diff.jpg')

gray = cv2.cvtColor(diff_img, cv2.COLOR_BGR2GRAY)
contours, _ = cv2.findContours(gray, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE)

COLOR = (0, 200, 0)
for cnt in contours:
    if cv2.contourArea(cnt) > 100:
        x, y, width, height = cv2.boundingRect(cnt)
        cv2.rectangle(src_img, (x, y), (x + width, y + height), COLOR, 2)
        cv2.rectangle(dest_img, (x, y), (x + width, y + height), COLOR, 2)
        cv2.rectangle(diff_img, (x, y), (x + width, y + height), COLOR, 2)

        to_x = x + (width // 2)
        to_y = y + (height // 2) + y_pos
        pyautogui.moveTo(to_x, to_y, duration=0.15)
        pyautogui.click(to_x, to_y)        
        
cv2.imshow('src', src_img)
cv2.imshow('dest', dest_img)
cv2.imshow('diff', diff_img)

cv2.waitKey(0)
cv2.destroyAllWindows()

 

 


참고사항

 

일부 PC 에서 외곽선이 전체 이미지 영역으로 잡히는 경우가 있습니다.
픽셀 단위로 분석을 해 본 결과 동일한 이미지 영역임에도 불구하고 왼쪽, 오른쪽 이미지의 픽셀이 미세하게 다른 경우가 있네요. (블루스택의 해상도 설정 관련 문제일지도 모르겠습니다)

해결 방법으로는, 우리 코드에서 틀린 이미지(diff) 를 흑백으로 변환한 이미지(gray) 의 값이 0~255 범위를 가지고 0 에 가까울수록 어두워지는데, 많이 어두운 값(25 이하)은 제외시켜버리고 그보다 큰 값만 유효한 것으로 판단하도록 threshold 를 적용하면 됩니다.

코드는 다음과 같이 흑백으로 변환하는 부분의 다음 줄에 한 줄을 더 추가해주시면 됩니다.

gray = cv2.cvtColor(diff_img, cv2.COLOR_BGR2GRAY)
gray = (gray > 25) * gray # 이 줄 추가
댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/04   »
1 2 3 4 5 6
7 8 9 10 11 12 13
14 15 16 17 18 19 20
21 22 23 24 25 26 27
28 29 30
글 보관함