Initial pixel hunter implementation
This commit is contained in:
commit
f41285b7a4
10
.gitignore
vendored
Normal file
10
.gitignore
vendored
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
.venv/
|
||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
|
||||||
|
build/
|
||||||
|
dist/
|
||||||
|
*.spec
|
||||||
|
|
||||||
|
.idea/
|
||||||
|
.vscode/
|
||||||
10
actions.py
Normal file
10
actions.py
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import pyautogui
|
||||||
|
|
||||||
|
|
||||||
|
def move_to(x, y):
|
||||||
|
pyautogui.moveTo(x, y, duration=0.15)
|
||||||
|
pyautogui.keyDown('ctrl')
|
||||||
|
try:
|
||||||
|
pyautogui.click()
|
||||||
|
finally:
|
||||||
|
pyautogui.keyUp('ctrl')
|
||||||
55
capture.py
Normal file
55
capture.py
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
import cv2
|
||||||
|
import numpy as np
|
||||||
|
import tkinter as tk
|
||||||
|
from PIL import ImageGrab
|
||||||
|
|
||||||
|
|
||||||
|
class CaptureOverlay:
|
||||||
|
def __init__(self, parent, output_path='template.png'):
|
||||||
|
self.parent = parent
|
||||||
|
self.output_path = output_path
|
||||||
|
self.saved = False
|
||||||
|
self.root = tk.Toplevel(parent)
|
||||||
|
self.root.attributes('-fullscreen', True)
|
||||||
|
self.root.attributes('-alpha', 0.3)
|
||||||
|
self.root.configure(bg='black')
|
||||||
|
self.root.wait_visibility()
|
||||||
|
self.root.attributes('-topmost', True)
|
||||||
|
|
||||||
|
self.start_x = self.start_y = None
|
||||||
|
self.rect = None
|
||||||
|
self.canvas = tk.Canvas(self.root, cursor='cross', highlightthickness=0)
|
||||||
|
self.canvas.pack(fill='both', expand=True)
|
||||||
|
|
||||||
|
self.canvas.bind('<ButtonPress-1>', self.on_press)
|
||||||
|
self.canvas.bind('<B1-Motion>', self.on_drag)
|
||||||
|
self.canvas.bind('<ButtonRelease-1>', self.on_release)
|
||||||
|
|
||||||
|
self.result = None
|
||||||
|
self.parent.wait_window(self.root)
|
||||||
|
|
||||||
|
def on_press(self, e):
|
||||||
|
self.start_x, self.start_y = e.x, e.y
|
||||||
|
self.rect = self.canvas.create_rectangle(e.x, e.y, e.x, e.y, outline='red', width=3)
|
||||||
|
|
||||||
|
def on_drag(self, e):
|
||||||
|
self.canvas.coords(self.rect, self.start_x, self.start_y, e.x, e.y)
|
||||||
|
|
||||||
|
def on_release(self, e):
|
||||||
|
x1 = min(self.start_x, e.x)
|
||||||
|
y1 = min(self.start_y, e.y)
|
||||||
|
x2 = max(self.start_x, e.x)
|
||||||
|
y2 = max(self.start_y, e.y)
|
||||||
|
w, h = x2 - x1, y2 - y1
|
||||||
|
|
||||||
|
if w < 10 or h < 10:
|
||||||
|
self.root.destroy()
|
||||||
|
return
|
||||||
|
|
||||||
|
self.root.withdraw()
|
||||||
|
self.root.update()
|
||||||
|
img = ImageGrab.grab(bbox=(x1, y1, x2, y2))
|
||||||
|
img = cv2.cvtColor(np.array(img), cv2.COLOR_RGB2BGR)
|
||||||
|
self.saved = bool(cv2.imwrite(self.output_path, img))
|
||||||
|
|
||||||
|
self.root.destroy()
|
||||||
10
config.json
Normal file
10
config.json
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"search_mode": "purple_frame",
|
||||||
|
"confidence": 0.72,
|
||||||
|
"scan_interval_ms": 300,
|
||||||
|
"template_path": "target.png",
|
||||||
|
"frame_hsv_lower": [125, 60, 120],
|
||||||
|
"frame_hsv_upper": [150, 255, 255],
|
||||||
|
"frame_min_size": 40,
|
||||||
|
"frame_min_rectangularity": 0.8
|
||||||
|
}
|
||||||
64
gui.py
Normal file
64
gui.py
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
import tkinter as tk
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
|
||||||
|
class LogWindow:
|
||||||
|
def __init__(self):
|
||||||
|
self.root = tk.Tk()
|
||||||
|
self.root.title('Screen Clicker')
|
||||||
|
self.root.geometry('420x220+0+0')
|
||||||
|
self.root.attributes('-topmost', True)
|
||||||
|
self.root.protocol('WM_DELETE_WINDOW', self.on_close)
|
||||||
|
|
||||||
|
self.running = True
|
||||||
|
self.screenshot_requested = False
|
||||||
|
self.capture_requested = False
|
||||||
|
|
||||||
|
self.text = tk.Text(
|
||||||
|
self.root, bg='#1a1a2e', fg='#e0e0e0',
|
||||||
|
font=('Consolas', 10), state='disabled', relief='flat'
|
||||||
|
)
|
||||||
|
self.text.pack(fill='both', expand=True)
|
||||||
|
|
||||||
|
controls = tk.Frame(self.root)
|
||||||
|
controls.pack(fill='x')
|
||||||
|
tk.Button(
|
||||||
|
controls,
|
||||||
|
text='From screenshot',
|
||||||
|
command=self.request_screenshot,
|
||||||
|
).pack(side='left', padx=4, pady=4)
|
||||||
|
tk.Button(
|
||||||
|
controls,
|
||||||
|
text='Capture screen',
|
||||||
|
command=self.request_capture,
|
||||||
|
).pack(side='left', padx=4, pady=4)
|
||||||
|
tk.Button(
|
||||||
|
controls,
|
||||||
|
text='Stop',
|
||||||
|
command=self.on_close,
|
||||||
|
).pack(side='right', padx=4, pady=4)
|
||||||
|
|
||||||
|
def log(self, msg):
|
||||||
|
ts = datetime.now().strftime('%H:%M:%S')
|
||||||
|
self.text.configure(state='normal')
|
||||||
|
self.text.insert('end', f'[{ts}] {msg}\n')
|
||||||
|
self.text.see('end')
|
||||||
|
self.text.configure(state='disabled')
|
||||||
|
try:
|
||||||
|
self.root.update()
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def update(self):
|
||||||
|
if self.running:
|
||||||
|
self.root.update()
|
||||||
|
|
||||||
|
def request_screenshot(self):
|
||||||
|
self.screenshot_requested = True
|
||||||
|
|
||||||
|
def request_capture(self):
|
||||||
|
self.capture_requested = True
|
||||||
|
|
||||||
|
def on_close(self):
|
||||||
|
self.running = False
|
||||||
|
self.root.destroy()
|
||||||
148
main.py
Normal file
148
main.py
Normal file
@ -0,0 +1,148 @@
|
|||||||
|
import json
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
import time
|
||||||
|
import cv2
|
||||||
|
import tkinter as tk
|
||||||
|
import ctypes
|
||||||
|
|
||||||
|
|
||||||
|
def enable_dpi_awareness():
|
||||||
|
"""Keep screenshot pixels and mouse coordinates in the same coordinate system."""
|
||||||
|
try:
|
||||||
|
ctypes.windll.shcore.SetProcessDpiAwareness(2)
|
||||||
|
except Exception:
|
||||||
|
try:
|
||||||
|
ctypes.windll.user32.SetProcessDPIAware()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
enable_dpi_awareness()
|
||||||
|
|
||||||
|
os.chdir(os.path.dirname(sys.executable)
|
||||||
|
if getattr(sys, 'frozen', False)
|
||||||
|
else os.path.dirname(__file__))
|
||||||
|
|
||||||
|
from capture import CaptureOverlay
|
||||||
|
from scanner import Scanner
|
||||||
|
from actions import move_to
|
||||||
|
from gui import LogWindow
|
||||||
|
from screenshot_selector import select_from_screenshot
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
log = None
|
||||||
|
try:
|
||||||
|
with open('config.json') as f:
|
||||||
|
config = json.load(f)
|
||||||
|
|
||||||
|
log = LogWindow()
|
||||||
|
if not os.path.exists(config['template_path']):
|
||||||
|
log.log('Choose a screenshot, then mark the area to find...')
|
||||||
|
if not select_from_screenshot(log.root, config['template_path']):
|
||||||
|
log.log('Selection cancelled.')
|
||||||
|
return
|
||||||
|
|
||||||
|
scanner = Scanner(config)
|
||||||
|
log.log(f'Template loaded: {scanner.tw}x{scanner.th}')
|
||||||
|
sys.stderr.write(f'[DEBUG] Scanner loaded template {scanner.tw}x{scanner.th}\n')
|
||||||
|
|
||||||
|
recapture_flag = [False]
|
||||||
|
clicked = False
|
||||||
|
|
||||||
|
try:
|
||||||
|
import keyboard
|
||||||
|
keyboard.add_hotkey('ctrl+shift+r', lambda: recapture_flag.__setitem__(0, True))
|
||||||
|
keyboard.add_hotkey(
|
||||||
|
'ctrl+shift+s',
|
||||||
|
lambda: setattr(log, 'screenshot_requested', True),
|
||||||
|
)
|
||||||
|
log.log('Ctrl+Shift+S = choose from screenshot')
|
||||||
|
log.log('Ctrl+Shift+R = capture from screen')
|
||||||
|
sys.stderr.write('[DEBUG] hotkey registered\n')
|
||||||
|
except Exception as e:
|
||||||
|
log.log(f'Hotkey not available (run as admin?): {e}')
|
||||||
|
sys.stderr.write(f'[DEBUG] hotkey failed: {e}\n')
|
||||||
|
|
||||||
|
log.log('Scanning started')
|
||||||
|
sys.stderr.write('[DEBUG] main loop started\n')
|
||||||
|
|
||||||
|
cycle = 0
|
||||||
|
while log.running:
|
||||||
|
if log.screenshot_requested:
|
||||||
|
log.screenshot_requested = False
|
||||||
|
log.log('Choose a screenshot and mark the exact target...')
|
||||||
|
if select_from_screenshot(log.root, config['template_path']):
|
||||||
|
scanner = Scanner(config)
|
||||||
|
clicked = False
|
||||||
|
log.log(f'New template loaded: {scanner.tw}x{scanner.th}')
|
||||||
|
else:
|
||||||
|
log.log('Selection cancelled; keeping the previous template.')
|
||||||
|
continue
|
||||||
|
|
||||||
|
if log.capture_requested or recapture_flag[0]:
|
||||||
|
log.capture_requested = False
|
||||||
|
recapture_flag[0] = False
|
||||||
|
log.log('Recapture: select new area...')
|
||||||
|
capture = CaptureOverlay(log.root, config['template_path'])
|
||||||
|
if not capture.saved:
|
||||||
|
log.log('Recapture cancelled; keeping the previous template.')
|
||||||
|
continue
|
||||||
|
scanner.template = cv2.imread(config['template_path'])
|
||||||
|
if scanner.template is None:
|
||||||
|
log.log('Error: new template not saved')
|
||||||
|
else:
|
||||||
|
scanner.th, scanner.tw = scanner.template.shape[:2]
|
||||||
|
log.log('New template saved!')
|
||||||
|
|
||||||
|
screen = scanner.grab_screen()
|
||||||
|
result = scanner.find(screen)
|
||||||
|
cycle += 1
|
||||||
|
|
||||||
|
if result and not clicked:
|
||||||
|
x, y = result
|
||||||
|
log.log(f'Match: ({x}, {y}) score={scanner.last_score:.3f}')
|
||||||
|
sys.stderr.write(f'[MATCH] score={scanner.last_score:.3f}\n')
|
||||||
|
move_to(x, y)
|
||||||
|
log.log(f'Ctrl+clicked at ({x}, {y})')
|
||||||
|
clicked = True
|
||||||
|
elif not result:
|
||||||
|
clicked = False
|
||||||
|
if cycle % 10 == 0:
|
||||||
|
sys.stderr.write(f'[SCAN] score={scanner.last_score:.3f} (need {config["confidence"]})\n')
|
||||||
|
if scanner.last_score > 0.05:
|
||||||
|
log.log(f'Best score: {scanner.last_score:.3f} (need {config["confidence"]})')
|
||||||
|
|
||||||
|
if not log.running:
|
||||||
|
break
|
||||||
|
log.update()
|
||||||
|
time.sleep(config['scan_interval_ms'] / 1000)
|
||||||
|
|
||||||
|
except (KeyboardInterrupt, tk.TclError):
|
||||||
|
pass
|
||||||
|
except Exception as e:
|
||||||
|
msg = f'[FATAL] {e}'
|
||||||
|
sys.stderr.write(msg + '\n')
|
||||||
|
if log is not None:
|
||||||
|
try:
|
||||||
|
log.log(msg)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
raise
|
||||||
|
finally:
|
||||||
|
try:
|
||||||
|
import keyboard
|
||||||
|
keyboard.unhook_all_hotkeys()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if log is not None and log.running:
|
||||||
|
try:
|
||||||
|
log.log('Stopped')
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
||||||
BIN
reference.png
Normal file
BIN
reference.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.3 MiB |
6
requirements.txt
Normal file
6
requirements.txt
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
opencv-python
|
||||||
|
pyautogui
|
||||||
|
numpy
|
||||||
|
keyboard
|
||||||
|
Pillow
|
||||||
|
pyinstaller
|
||||||
14
run_debug.bat
Normal file
14
run_debug.bat
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
@echo off
|
||||||
|
cd /d "%~dp0"
|
||||||
|
if not exist ".venv\Scripts\python.exe" (
|
||||||
|
echo [ERROR] Local Python environment not found: .venv
|
||||||
|
echo Recreate it and install requirements before starting.
|
||||||
|
goto end
|
||||||
|
)
|
||||||
|
|
||||||
|
".venv\Scripts\python.exe" main.py
|
||||||
|
|
||||||
|
:end
|
||||||
|
echo.
|
||||||
|
echo Press any key to exit...
|
||||||
|
pause >nul
|
||||||
85
scanner.py
Normal file
85
scanner.py
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
import cv2
|
||||||
|
import numpy as np
|
||||||
|
from PIL import ImageGrab
|
||||||
|
|
||||||
|
|
||||||
|
class Scanner:
|
||||||
|
def __init__(self, config):
|
||||||
|
self.config = config
|
||||||
|
self.template = cv2.imread(config['template_path'])
|
||||||
|
if self.template is None:
|
||||||
|
raise FileNotFoundError(f"Template not found: {config['template_path']}")
|
||||||
|
self.th, self.tw = self.template.shape[:2]
|
||||||
|
self.last_score = 0.0
|
||||||
|
self.last_box = None
|
||||||
|
|
||||||
|
def find(self, screen):
|
||||||
|
if self.config.get('search_mode') == 'purple_frame':
|
||||||
|
return self.find_purple_frame(screen)
|
||||||
|
|
||||||
|
screen_h, screen_w = screen.shape[:2]
|
||||||
|
if self.tw > screen_w or self.th > screen_h:
|
||||||
|
self.last_score = 0.0
|
||||||
|
self.last_box = None
|
||||||
|
return None
|
||||||
|
|
||||||
|
res = cv2.matchTemplate(screen, self.template, cv2.TM_CCOEFF_NORMED)
|
||||||
|
_, max_val, _, max_loc = cv2.minMaxLoc(res)
|
||||||
|
self.last_score = max_val
|
||||||
|
if max_val >= self.config['confidence']:
|
||||||
|
left, top = max_loc
|
||||||
|
self.last_box = (left, top, self.tw, self.th)
|
||||||
|
cx = left + self.tw // 2
|
||||||
|
cy = top + self.th // 2
|
||||||
|
return (cx, cy)
|
||||||
|
self.last_box = None
|
||||||
|
return None
|
||||||
|
|
||||||
|
def find_purple_frame(self, screen):
|
||||||
|
"""Find the rectangular purple highlight, ignoring everything inside it."""
|
||||||
|
hsv = cv2.cvtColor(screen, cv2.COLOR_BGR2HSV)
|
||||||
|
lower = np.array(
|
||||||
|
self.config.get('frame_hsv_lower', [125, 60, 120]),
|
||||||
|
dtype=np.uint8,
|
||||||
|
)
|
||||||
|
upper = np.array(
|
||||||
|
self.config.get('frame_hsv_upper', [150, 255, 255]),
|
||||||
|
dtype=np.uint8,
|
||||||
|
)
|
||||||
|
mask = cv2.inRange(hsv, lower, upper)
|
||||||
|
kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (5, 5))
|
||||||
|
mask = cv2.morphologyEx(mask, cv2.MORPH_CLOSE, kernel)
|
||||||
|
|
||||||
|
contours, _ = cv2.findContours(
|
||||||
|
mask,
|
||||||
|
cv2.RETR_EXTERNAL,
|
||||||
|
cv2.CHAIN_APPROX_SIMPLE,
|
||||||
|
)
|
||||||
|
min_size = int(self.config.get('frame_min_size', 40))
|
||||||
|
min_rectangularity = float(
|
||||||
|
self.config.get('frame_min_rectangularity', 0.80)
|
||||||
|
)
|
||||||
|
candidates = []
|
||||||
|
|
||||||
|
for contour in contours:
|
||||||
|
x, y, w, h = cv2.boundingRect(contour)
|
||||||
|
if w < min_size or h < min_size:
|
||||||
|
continue
|
||||||
|
rectangularity = cv2.contourArea(contour) / float(w * h)
|
||||||
|
if rectangularity < min_rectangularity:
|
||||||
|
continue
|
||||||
|
candidates.append((w * h, rectangularity, x, y, w, h))
|
||||||
|
|
||||||
|
if not candidates:
|
||||||
|
self.last_score = 0.0
|
||||||
|
self.last_box = None
|
||||||
|
return None
|
||||||
|
|
||||||
|
_, rectangularity, x, y, w, h = max(candidates)
|
||||||
|
self.last_score = rectangularity
|
||||||
|
self.last_box = (x, y, w, h)
|
||||||
|
return (x + w // 2, y + h // 2)
|
||||||
|
|
||||||
|
def grab_screen(self):
|
||||||
|
img = ImageGrab.grab()
|
||||||
|
return cv2.cvtColor(np.array(img), cv2.COLOR_RGB2BGR)
|
||||||
109
screenshot_selector.py
Normal file
109
screenshot_selector.py
Normal file
@ -0,0 +1,109 @@
|
|||||||
|
import tkinter as tk
|
||||||
|
from tkinter import filedialog
|
||||||
|
from PIL import Image, ImageTk
|
||||||
|
|
||||||
|
|
||||||
|
class ScreenshotSelector:
|
||||||
|
def __init__(self, parent, output_path, initial_path=None):
|
||||||
|
self.parent = parent
|
||||||
|
self.output_path = output_path
|
||||||
|
self.saved = False
|
||||||
|
self.start_x = None
|
||||||
|
self.start_y = None
|
||||||
|
self.rect = None
|
||||||
|
|
||||||
|
path = initial_path
|
||||||
|
if not path:
|
||||||
|
path = filedialog.askopenfilename(
|
||||||
|
parent=parent,
|
||||||
|
title='Choose a reference screenshot',
|
||||||
|
filetypes=[
|
||||||
|
('Images', '*.png *.jpg *.jpeg *.bmp *.webp'),
|
||||||
|
('All files', '*.*'),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
if not path:
|
||||||
|
return
|
||||||
|
|
||||||
|
self.image = Image.open(path).convert('RGB')
|
||||||
|
max_w = max(300, parent.winfo_screenwidth() - 100)
|
||||||
|
max_h = max(300, parent.winfo_screenheight() - 160)
|
||||||
|
self.scale = min(1.0, max_w / self.image.width, max_h / self.image.height)
|
||||||
|
shown_size = (
|
||||||
|
max(1, round(self.image.width * self.scale)),
|
||||||
|
max(1, round(self.image.height * self.scale)),
|
||||||
|
)
|
||||||
|
self.shown = self.image.resize(shown_size, Image.Resampling.LANCZOS)
|
||||||
|
|
||||||
|
self.root = tk.Toplevel(parent)
|
||||||
|
self.root.title('Select the object or area to find')
|
||||||
|
self.root.attributes('-topmost', True)
|
||||||
|
self.root.protocol('WM_DELETE_WINDOW', self.root.destroy)
|
||||||
|
|
||||||
|
label = tk.Label(
|
||||||
|
self.root,
|
||||||
|
text='Drag a rectangle around the exact object/area to find',
|
||||||
|
padx=8,
|
||||||
|
pady=6,
|
||||||
|
)
|
||||||
|
label.pack(fill='x')
|
||||||
|
|
||||||
|
self.canvas = tk.Canvas(
|
||||||
|
self.root,
|
||||||
|
width=shown_size[0],
|
||||||
|
height=shown_size[1],
|
||||||
|
cursor='cross',
|
||||||
|
highlightthickness=0,
|
||||||
|
)
|
||||||
|
self.canvas.pack()
|
||||||
|
self.photo = ImageTk.PhotoImage(self.shown)
|
||||||
|
self.canvas.create_image(0, 0, image=self.photo, anchor='nw')
|
||||||
|
self.canvas.bind('<ButtonPress-1>', self.on_press)
|
||||||
|
self.canvas.bind('<B1-Motion>', self.on_drag)
|
||||||
|
self.canvas.bind('<ButtonRelease-1>', self.on_release)
|
||||||
|
|
||||||
|
self.root.grab_set()
|
||||||
|
self.parent.wait_window(self.root)
|
||||||
|
|
||||||
|
def on_press(self, event):
|
||||||
|
self.start_x, self.start_y = event.x, event.y
|
||||||
|
if self.rect is not None:
|
||||||
|
self.canvas.delete(self.rect)
|
||||||
|
self.rect = self.canvas.create_rectangle(
|
||||||
|
event.x,
|
||||||
|
event.y,
|
||||||
|
event.x,
|
||||||
|
event.y,
|
||||||
|
outline='#ff00ff',
|
||||||
|
width=3,
|
||||||
|
)
|
||||||
|
|
||||||
|
def on_drag(self, event):
|
||||||
|
if self.rect is not None:
|
||||||
|
self.canvas.coords(
|
||||||
|
self.rect,
|
||||||
|
self.start_x,
|
||||||
|
self.start_y,
|
||||||
|
event.x,
|
||||||
|
event.y,
|
||||||
|
)
|
||||||
|
|
||||||
|
def on_release(self, event):
|
||||||
|
x1, x2 = sorted((self.start_x, event.x))
|
||||||
|
y1, y2 = sorted((self.start_y, event.y))
|
||||||
|
if x2 - x1 < 10 or y2 - y1 < 10:
|
||||||
|
return
|
||||||
|
|
||||||
|
box = (
|
||||||
|
max(0, round(x1 / self.scale)),
|
||||||
|
max(0, round(y1 / self.scale)),
|
||||||
|
min(self.image.width, round(x2 / self.scale)),
|
||||||
|
min(self.image.height, round(y2 / self.scale)),
|
||||||
|
)
|
||||||
|
self.image.crop(box).save(self.output_path)
|
||||||
|
self.saved = True
|
||||||
|
self.root.destroy()
|
||||||
|
|
||||||
|
|
||||||
|
def select_from_screenshot(parent, output_path, initial_path=None):
|
||||||
|
return ScreenshotSelector(parent, output_path, initial_path).saved
|
||||||
BIN
target.png
Normal file
BIN
target.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1023 KiB |
BIN
template.png
Normal file
BIN
template.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.3 MiB |
Loading…
Reference in New Issue
Block a user