"""
image_clicker.py
화면에서 특정 이미지를 찾아 클릭하는 간단한 자동화 툴
지원:
- template matching (fast, scale/rotation에 민감)
- ORB keypoint matching (slower, scale/rotation에 강인)
주의:
- Windows에서 DPI 스케일(디스플레이 배율)이 100%가 아니면 좌표 보정이 필요할 수 있습니다.
- 자동 클릭은 시스템/앱에 따라 문제를 일으킬 수 있으니 책임 하에 사용하세요.
"""
import time
import cv2
import numpy as np
import pyautogui
import mss
import keyboard
import sys
from typing import Tuple, Optional
# ----------------- 설정 -----------------
TEMPLATE_PATH = "template.png" # 찾을 이미지 파일 경로
METHOD = "template" # "template" or "orb"
THRESHOLD = 0.8 # template: 0.7-0.95 권장, orb: 매칭 키포인트 비율 판단시 사용
CLICK_DELAY = 0.05 # 클릭 후 대기 (초)
LOOP_DELAY = 0.2 # 화면 캡처 반복 주기 (초)
SHOW_PREVIEW = True # 화면과 매칭 결과를 윈도우로 미리보기
FIND_MULTI = False # True면 여러 개 찾고 가장 먼저 클릭하지 않고 모두 클릭함
MAX_RESULTS = 5 # template 모드에서 최대 결과수 (NMS 적용)
CLICK_BUTTON = "left" # 'left' or 'right' or 'middle'
QUIT_HOTKEY = ("ctrl", "shift", "q") # 누르면 종료
# ---------------------------------------
# Helper: 체크 quit hotkey
def quit_pressed():
return all(keyboard.is_pressed(k) for k in QUIT_HOTKEY)
# 화면 캡처 (mss) -> numpy BGR 이미지
def grab_screen() -> np.ndarray:
with mss.mss() as sct:
monitor = sct.monitors[1] # 기본 모니터(전체)
sct_img = sct.grab(monitor)
img = np.array(sct_img) # BGRA
img = cv2.cvtColor(img, cv2.COLOR_BGRA2BGR)
return img
# Template matching (multi or single)
def find_by_template(screen_bgr: np.ndarray, template_bgr: np.ndarray, threshold=0.8, max_results=5):
screen_gray = cv2.cvtColor(screen_bgr, cv2.COLOR_BGR2GRAY)
tmpl_gray = cv2.cvtColor(template_bgr, cv2.COLOR_BGR2GRAY)
res = cv2.matchTemplate(screen_gray, tmpl_gray, cv2.TM_CCOEFF_NORMED)
loc = np.where(res >= threshold)
w, h = tmpl_gray.shape[::-1]
rects = []
scores = []
for pt in zip(*loc[::-1]):
rects.append([int(pt[0]), int(pt[1]), int(w), int(h)])
scores.append(res[pt[1], pt[0]])
# Non-maximum suppression to reduce overlapping boxes
if len(rects) == 0:
return []
boxes = np.array(rects)
scores = np.array(scores)
# NMS
indices = cv2.dnn.NMSBoxes(rects, scores.tolist(), score_threshold=threshold, nms_threshold=0.3)
found = []
if len(indices) > 0:
for i in indices.flatten()[:max_results]:
x, y, w, h = rects[i]
found.append((x, y, w, h, float(scores[i])))
else:
# fallback: take best matches up to max_results
order = np.argsort(-scores)[:max_results]
for i in order:
x, y, w, h = rects[i]
found.append((x, y, w, h, float(scores[i])))
return found
# ORB matching
def find_by_orb(screen_bgr: np.ndarray, template_bgr: np.ndarray, min_match_count=8):
screen_gray = cv2.cvtColor(screen_bgr, cv2.COLOR_BGR2GRAY)
tmpl_gray = cv2.cvtColor(template_bgr, cv2.COLOR_BGR2GRAY)
orb = cv2.ORB_create(1000)
kp1, des1 = orb.detectAndCompute(tmpl_gray, None)
kp2, des2 = orb.detectAndCompute(screen_gray, None)
if des1 is None or des2 is None:
return []
bf = cv2.BFMatcher(cv2.NORM_HAMMING, crossCheck=False)
matches = bf.knnMatch(des1, des2, k=2)
# Lowe's ratio test
good = []
for m_n in matches:
if len(m_n) != 2:
continue
m, n = m_n
if m.distance < 0.75 * n.distance:
good.append(m)
if len(good) < min_match_count:
return []
# Homography to find bounding box
src_pts = np.float32([kp1[m.queryIdx].pt for m in good]).reshape(-1, 1, 2)
dst_pts = np.float32([kp2[m.trainIdx].pt for m in good]).reshape(-1, 1, 2)
M, mask = cv2.findHomography(src_pts, dst_pts, cv2.RANSAC, 5.0)
h, w = tmpl_gray.shape
if M is None:
return []
pts = np.float32([[0, 0], [w, 0], [w, h], [0, h]]).reshape(-1, 1, 2)
dst = cv2.perspectiveTransform(pts, M)
# bounding rect
x_coords = dst[:, 0, 0]
y_coords = dst[:, 0, 1]
x, y = int(x_coords.min()), int(y_coords.min())
ww, hh = int(x_coords.max() - x), int(y_coords.max() - y)
# confidence: ratio of inliers
inliers = mask.ravel().sum() if mask is not None else 0
conf = float(inliers) / len(good) if len(good) > 0 else 0.0
return [(x, y, ww, hh, conf)]
# 클릭 (중앙 좌표)
def click_box(box: Tuple[int, int, int, int], button="left"):
x, y, w, h = box
cx = x + w // 2
cy = y + h // 2
# pyautogui uses screen coordinates; ensure failsafe off if you want
try:
pyautogui.click(cx, cy, button=button)
except Exception as e:
print("Click failed:", e)
def main():
# load template
tmpl = cv2.imread(TEMPLATE_PATH)
if tmpl is None:
print(f"템플릿 파일을 열 수 없습니다: {TEMPLATE_PATH}")
sys.exit(1)
print("템플릿 로드 완료:", TEMPLATE_PATH)
print("Method:", METHOD)
print("종료 단축키:", "+".join(QUIT_HOTKEY).upper())
last_click_time = 0
while True:
if quit_pressed():
print("종료 단축키 눌림. 프로그램 종료.")
break
screen = grab_screen()
found = []
if METHOD == "template":
found = find_by_template(screen, tmpl, threshold=THRESHOLD, max_results=MAX_RESULTS)
elif METHOD == "orb":
found = find_by_orb(screen, tmpl)
else:
print("Unknown method:", METHOD)
break
# 클릭 동작
if found:
if FIND_MULTI:
for (x, y, w, h, score) in found:
print(f"Found @ {x},{y},{w}x{h} conf={score:.3f} -> clicking")
click_box((x, y, w, h), button=CLICK_BUTTON)
time.sleep(CLICK_DELAY)
else:
x, y, w, h, score = found[0]
# 클릭 빈도 제어 (안전)
now = time.time()
if now - last_click_time > CLICK_DELAY:
print(f"Found @ {x},{y} size={w}x{h} conf={score:.3f} -> clicking")
click_box((x, y, w, h), button=CLICK_BUTTON)
last_click_time = now
# 보여주기 (디버그용)
if SHOW_PREVIEW:
vis = screen.copy()
for (x, y, w, h, score) in found:
cv2.rectangle(vis, (x, y), (x + w, y + h), (0, 255, 0), 2)
cv2.putText(vis, f"{score:.2f}", (x, y - 6), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0,255,0), 2)
cv2.imshow("Image Clicker Preview (press ESC to close preview, hotkey to quit)", vis)
# ESC to close preview window only
if cv2.waitKey(1) & 0xFF == 27:
cv2.destroyWindow("Image Clicker Preview (press ESC to close preview, hotkey to quit)")
# continue running headless
time.sleep(LOOP_DELAY)
if __name__ == "__main__":
try:
main()
except KeyboardInterrupt:
print("KeyboardInterrupt -> 종료")
finally:
cv2.destroyAllWindows()
To embed this project on your website, copy the following code and paste it into your website's HTML: