From 7a35ae0b385abdc674fa7892dd4684c764035174 Mon Sep 17 00:00:00 2001 From: ElectricityMachine <47489506+ElectricityMachine@users.noreply.github.com> Date: Thu, 2 Nov 2023 23:45:13 -0400 Subject: [PATCH] Initial numpy support Further numpy improvements Final numpy conversion --- script.py | 276 +++++++++++++++++++++++++++++------------------------- 1 file changed, 151 insertions(+), 125 deletions(-) diff --git a/script.py b/script.py index dd7dacb..da01111 100644 --- a/script.py +++ b/script.py @@ -16,6 +16,8 @@ import mouse import pyperclip import win32gui +import threading +import numpy as np from settings import AVG_FPS, VERSION, AVG_PING, DEBUG_ENABLED, UPDATE_CHECK_ENABLED, COLORS from keyboard import add_hotkey, press_and_release from keyboard import wait as keyboard_wait @@ -30,9 +32,10 @@ def update_check(): - logging.debug("Checking for updates") + logging.debug("update_check: called") """Fetch the latest release version from the GitHub repo and inform the user if an update is available""" # TODO: Implement better version check functionality instead of just difference in strings + # TODO: Use async for this function somehow, so we don't block. if not UPDATE_CHECK_ENABLED: return URL = "https://api.github.com/repos/ElectricityMachine/SCR-SGPlus/releases/latest" @@ -48,25 +51,6 @@ def update_check(): print(colorama.Fore.WHITE) -def color_approx_eq(color1: tuple, color2: tuple, tolerance: int = 7): - logging.debug("Color approx eq") - """Check if a color is equal to another color within a given value - - Args: - color1 (tuple): First RGB color to check against - color2 (tuple): Second RGB color to check against - tolerance (int, optional): How many units of R, G, or B to tolerate. Defaults to 7. - - Returns: - bool: Whether or not the colors are approximately equal to eachother - """ - return ( - abs(color1[0] - color2[0]) <= tolerance - and abs(color1[1] - color2[1]) <= tolerance - and abs(color1[2] - color2[2]) <= tolerance - ) - - def screen_grab(x: int, y: int, width: int, height: int): """Return a screenshot of the user's screen given some bounding box @@ -95,14 +79,17 @@ def mouseclick_left() -> None: mouse.release("left") +def move_mouse(x: int, y: int, speed=1): + autoit.mouse_move(x, y, speed) + + def sleep_frames(frames: int, minwait=0) -> None: - logging.debug(f"Started sleeping for {frames} frames") + logging.debug(f"Sleeping for {frames} frame(s)") time.sleep(max((frames * one_frame_time), minwait)) - logging.debug("Finished sleep") def check_able_to_run(callback): - logging.debug("Checking if able to run...") + logging.debug("check_able_to_run: called") def wrapper(*args): if ( @@ -111,10 +98,10 @@ def wrapper(*args): and callback is not None and callable(callback) ): - logging.debug("We're able to run") + logging.debug("check_able_to_run: able to run") return callback(*args) else: - logging.debug("Not able to run! Return None") + logging.debug("check_able_to_run: not able to run, returning") return None return wrapper @@ -122,38 +109,34 @@ def wrapper(*args): @check_able_to_run def click_signal(sig: str): - logging.debug("Click_signal executed") + logging.debug("click_signal: called") coord = mouse.get_position() mouseclick_left() time.sleep(one_frame_time * 3) if scan_for_dialog("signal", coord[0], coord[1]): - logging.debug("Signal dialog found in click_signal func") + logging.debug("click_signal: scan_for_dialog returned true") time.sleep(one_frame_time * 2) press_and_release(sig) time.sleep(AVG_PING / 4_000) press_and_release("backspace") -def move_mouse(x: int, y: int, speed=1): - autoit.mouse_move(x, y, speed) - - @check_able_to_run def click_rollback() -> None: - logging.debug("click_rollback") + logging.debug("click_rollback: called") mousex, mousey = mouse.get_position() mouseclick_left() sleep_frames(2) if scan_for_dialog("exitcamera"): - logging.debug("Found exitcamera in click_rollback, don't do anything") + logging.debug("click_rollback: scan_for_dialog(exitcamera) returned true") return elif scan_for_dialog("signal", mousex, mousey): - logging.debug("Open side menu for signal") + logging.debug("click_rollback: scan_for_dialog(signal) returned true, pressing enter") press_and_release("enter") else: logging.debug("return path in click_rollback") return - logging.debug("Made it out of the if-elif path in click_rollback") + logging.debug("click_rollback: execute main body outside if-elif path") window = win32gui.GetForegroundWindow() rect = win32gui.GetClientRect(window) @@ -170,7 +153,7 @@ def click_rollback() -> None: move_mouse(x=rollback_position[0], y=rollback_position[1], speed=2) mouseclick_left() - sleep_frames(3) # Ensure mouseclick registers + sleep_frames(3) press_and_release("backspace, backspace") move_mouse(mousex, mousey, speed=1) return @@ -178,33 +161,34 @@ def click_rollback() -> None: @check_able_to_run def click_camera() -> None: - logging.debug("click_camera ran") + logging.debug("click_camera: called") global signal_mouse_coords if scan_for_dialog("exitcamera"): - logging.debug("exitcamera found in click_camera function") - for _ in range(3): + logging.debug("click_camera: scan_for_dialog(exitcamera) returned true, pressing backspace twice") + for _ in range(2): press_and_release("backspace") if signal_mouse_coords: move_mouse(signal_mouse_coords[0], signal_mouse_coords[1], speed=1) return + logging.debug("click_camera: exitcamera dialog not found, executing main body") signal_mouse_coords = mouse.get_position() mouseclick_left() - sleep_frames(2, 0.02) + sleep_frames(2) if scan_for_dialog("signal"): - logging.debug("signal scan_for_dialog found") + logging.debug("click_camera: signal scan_for_dialog found") press_and_release("enter") camera_y = 0.92133 elif scan_for_dialog("uncontrolled"): - logging.debug("uncontrolled signal found in click_camera") + logging.debug("click_camera: uncontrolled signal found in click_camera") press_and_release("enter") - sleep_frames(2) + # sleep_frames(2) camera_y = 0.80137 if scan_for_dialog("viewcamera") == 0 else 0.92133 x = "lower number" if camera_y == 0.80137 else "upper number" - logging.debug(f"uncontrolled scan_for_dialog true in click_camera with x-value of {x}") + logging.debug(f"click_camera: uncontrolled scan_for_dialog true in click_camera with x-value of {x}") else: - logging.debug("return none path in click_camera") + logging.debug("click_camera: return none path in click_camera") return - logging.debug("made it outside if-elif path in click_camera") + logging.debug("click_camera: outside if-elif path") window = win32gui.GetForegroundWindow() rect = win32gui.GetClientRect(window) @@ -232,7 +216,6 @@ def calculate_zone_screen(window_width: int, window_height: int) -> tuple: zone_screen_height = math.ceil(ZONE_SCREEN_HEIGHT_RATIO * window_height) zone_screen_width = math.ceil(zone_screen_height * ZONE_SCREEN_WIDTH_RATIO) zone_screen_x = math.ceil(window_width / 2 - zone_screen_width / 2) - logging.debug(f"zone screen params: {zone_screen_height, zone_screen_width, zone_screen_x}") return zone_screen_height, zone_screen_width, zone_screen_x @@ -240,12 +223,13 @@ def toggle_disable() -> None: global enabled logging.debug(f"toggle_disable called: enabled is {enabled}") enabled = not enabled - winsound.Beep(500, 100) if enabled else winsound.Beep(400, 100) + beep = threading.Thread(target=lambda: winsound.Beep(500, 100) if enabled else winsound.Beep(400, 100)) + beep.start() @check_able_to_run def scan_for_dialog(type: str, mousex=0, mousey=0): - logging.debug("scan_for_dialog called") + logging.debug("scan_for_dialog: called") if mousex is mousey and mousex == 0: mousex, mousey = mouse.get_position() window = win32gui.GetForegroundWindow() @@ -266,54 +250,8 @@ def scan_for_dialog(type: str, mousex=0, mousey=0): return find_camera_buttons(h, w, window) -def find_camera_buttons(h: int, w: int, window): - logging.debug("find_camera_button called") - zone_screen_height, zone_screen_width, zone_screen_x = calculate_zone_screen(w, h) - - camerabutton_height = math.ceil(h * 0.125 * 0.375) - camerabutton_width = math.ceil(camerabutton_height * 2 / 0.375) - camerabutton_x = zone_screen_width * 0.79760 + zone_screen_x - camerabutton_y = h * 0.80629 - - screen_cords = win32gui.ClientToScreen(window, (int(camerabutton_x), int(camerabutton_y))) - capture = screen_grab( - screen_cords[0], - screen_cords[1], - camerabutton_width, - camerabutton_height * 2, - ).convert("RGB") - width, height = capture.size - uppershelf = capture.crop((0, 0, width, 3)) - lowershelf = capture.crop((0, height * 0.94, width, height)) - - imagesToProcess = [uppershelf, lowershelf] - for image in imagesToProcess: - for i in range(math.ceil(image.width * 0.2)): - if image.height == 0: - break - r, g, b = image.getpixel((i, image.height - 1)) - if color_approx_eq((r, g, b), COLORS["COLOR_VIEWCAMERA"]): - logging.debug("found COLOR_VIEWCAMERA in find_camera_buttons") - return 0 if image == imagesToProcess[0] else 1 - logging.debug("no find_camera_buttons found") - return False - - -def check_for_white_pixels(image): - logging.debug("check for white pixels") - white_pixels = 0 - for i in range(math.ceil(image.width)): - for i2 in range(math.ceil(image.height)): - r, g, b = image.getpixel((i, i2)) - if color_approx_eq((r, g, b), (COLORS["COLOR_DIALOG_WHITE"])): - white_pixels += 1 - if white_pixels / image.width >= 0.2: - logging.debug("white pixels > 20% found") - return True - - def find_uncontrolled_sig_dialog(h: int, mousex: int, mousey: int) -> bool: - logging.debug("find uncontrolled sig dialog called") + logging.debug("find_uncontrolled_sig_dialog: called") dialogbox_height = math.ceil(h * 0.125) dialogbox_width = math.ceil(dialogbox_height * 2) dialogbox_x = mousex - dialogbox_width / 2 @@ -332,16 +270,16 @@ def find_uncontrolled_sig_dialog(h: int, mousex: int, mousey: int) -> bool: imagesToProcess = [lowershelf, uppershelf] for image in imagesToProcess: - logging.debug("iterating images in uncontrolled sig dialog loop") - if check_for_white_pixels(image): - logging.debug("check for white pixels true in uncontrolled sig dialog loop") + logging.debug("find_uncontrolled_sig_dialog: iterating images") + if check_color_percentage_single(image, COLORS["COLOR_DIALOG_WHITE"]): + logging.debug("find_uncontrolled_sig_dialog: image loop: numpy white pixels returned success") return True - logging.debug("return false path uncontrolled signal") + logging.debug("find_uncontrolled_sig_dialog: return false path") return False def find_controlled_sig_dialog(h: int, mousex: int, mousey: int) -> bool: - logging.debug("find_controlled_sig called") + logging.debug("find_controlled_sig: called") dialogbox_height = math.ceil(h * 0.125) dialogbox_width = math.ceil(dialogbox_height * 2) dialogbox_x = mousex - dialogbox_width / 2 @@ -356,23 +294,117 @@ def find_controlled_sig_dialog(h: int, mousex: int, mousey: int) -> bool: lowershelf = lower.crop((0, height * 0.66, width, height * 0.66 + 3)) uppershelf = upper.crop((0, upperh * 0.4, upperw, upperh * 0.4 + 2)) imagesToProcess = [lowershelf, uppershelf] - logging.debug("made it to generator") + logging.debug("find_controlled_sig_dialog: made it to generator") result = any( - image.height != 0 and check_for_white_pixels(image) and check_for_colored_pixels_in_signal_dialog(image) + check_color_percentage_single(image, COLORS["COLOR_DIALOG_WHITE"], threshold=0.01) + and check_color_multiple(image, COLORS["COLOR_DIALOG_BUTTONS"]) for image in imagesToProcess - ) - logging.debug(f"result: {result}") + ) # this doesn't run if check for white pixels is false. must change TODO + logging.debug(f"find_controlled_sig_dialog: result: {result}") return result -def check_for_colored_pixels_in_signal_dialog(image) -> bool: - logging.debug("check_for_colored_pixels called") - for i in range(math.ceil(image.width / 1.6)): - r, g, b = image.getpixel((i, image.height - 1)) - for val in COLORS["COLOR_DIALOG_BUTTONS"]: - if color_approx_eq((r, g, b), (val)): - logging.debug("found matching color, return true") +def find_camera_buttons(h: int, w: int, window): + logging.debug("find_camera_button: called") + zone_screen_height, zone_screen_width, zone_screen_x = calculate_zone_screen(w, h) + + camerabutton_height = math.ceil(h * 0.125 * 0.375) + camerabutton_width = math.ceil(camerabutton_height * 2 / 0.375) + camerabutton_x = zone_screen_width * 0.79760 + zone_screen_x + camerabutton_y = h * 0.80629 + + screen_cords = win32gui.ClientToScreen(window, (int(camerabutton_x), int(camerabutton_y))) + capture = screen_grab( + screen_cords[0], + screen_cords[1], + camerabutton_width, + camerabutton_height * 2, + ).convert("RGB") + width, height = capture.size + uppershelf = capture.crop((0, 0, width, 3)) + lowershelf = capture.crop((0, height * 0.94, width, height)) + + imagesToProcess = [uppershelf, lowershelf] + for image in imagesToProcess: + if check_color_single(image, COLORS["VIEW_CAMERA_BUTTON"]): + logging.debug( + f"View camera button found. We got {0 if image==imagesToProcess[0] else 1} (0=upper, 1=lower)" + ) + return 0 if image == imagesToProcess[0] else 1 + logging.debug("find_camera_buttons: none found") + return False + + +def color_approx_eq_np(color1: tuple, color2: tuple, threshold=10) -> bool: + """Check if a color is equal to another color within a given value + + Args: + color1 (tuple): First RGB color to check against + color2 (tuple): Second RGB color to check against + tolerance (int, optional): How many units of R, G, or B to tolerate. Defaults to 10. + + Returns: + bool: Whether or not the colors are approximately equal to eachother + """ + # Get the absolute value of the difference between the arrays + diff = np.abs(np.array(color1) - np.array(color2)) + # Check if within threshold + return np.all(diff <= threshold) + + +def check_color_single(image, color, threshold=7) -> bool: + logging.debug("check_color_single: called") + arr = np.array(image) + + # Iterate over the y-axis + for i in range(arr.shape[0]): + # Iterate over the x-axis + for j in range(arr.shape[1]): + if color_approx_eq_np(arr[i, j], color, threshold): + logging.debug("check_color_single: colors similar, return True") return True + logging.debug("check_color_single: no similar colors found, returning False") + return False + + +def check_color_multiple(image, colors: list, threshold=7) -> bool: + logging.debug("check_color_multiple: called") + arr = np.array(image) + + # Iterate over the y-axis + for i in range(arr.shape[0]): + # Iterate over the x-axis + for j in range(arr.shape[1]): + # Get the tuple from the element + r, g, b = arr[i, j] + for color in colors: + if color_approx_eq_np(arr[i, j], color, threshold): + logging.debug("check_colored_pixels_np: colors similar, return True") + return True + logging.debug("check_colored_pixels_np: no similar colors found, returning False") + return False + + +def check_color_percentage_single(image, color: tuple, compareThreshold=7, threshold=0.05) -> bool: + logging.debug("check_color_percentage_single: called") + matching_pixels = 0 + arr = np.array(image) + + # Iterate over the y-axis + for i in range(arr.shape[0]): + # Iterate over the x-axis + for j in range(arr.shape[1]): + # Get the tuple from the element + r, g, b = arr[i, j] + if color_approx_eq_np((r, g, b), color, compareThreshold): + matching_pixels += 1 + if matching_pixels / arr.size >= threshold: + logging.debug(f"check_color_percentage_single: matching pixels > {threshold * 10 ** 2}% found") + return True + logging.debug( + f"check_color_percentage_single: not enough white pixels found for array size. numpixels: {matching_pixels/arr.size}" + ) + return False def find_exit_cam_button(w: int, bbox: list[int, int, int, int], window): @@ -397,15 +429,7 @@ def find_exit_cam_button(w: int, bbox: list[int, int, int, int], window): lowershelf = capture.crop((0, height / 2, width, height / 2 + 2)) imagesToProcess = [lowershelf] - for image in imagesToProcess: - if image.height == 0: - break - for i in range(math.ceil(image.width / 1.6)): - r, g, b = image.getpixel((i, image.height - 1)) - if color_approx_eq((r, g, b), (COLORS["COLOR_CAMERA_EXIT"])): - return True - - return False + return all(check_color_single(image, COLORS["COLOR_CAMERA_EXIT"]) for image in imagesToProcess) @check_able_to_run @@ -425,14 +449,16 @@ def send_zone_message(zone: str): "G": "Zone G (James St. to Esterfield) is now under manual signalling control.", } - winsound.Beep(600, 200) + beep = threading.Thread(target=lambda: winsound.Beep(600, 200)) + beep.start() pyperclip.copy(switch.get(zone)) @check_able_to_run def enabled_warning(): """Play a warning sound if script is enabled""" - winsound.Beep(640, 300) + beep = threading.Thread(target=lambda: winsound.Beep(640, 300)) + beep.start() if __name__ == "__main__": @@ -455,8 +481,8 @@ def enabled_warning(): colorama.init() - if update_check: + if UPDATE_CHECK_ENABLED: # TODO: remove from update_check in favor of here update_check() winsound.Beep(500, 200) - print("SG+ Successfully Initialized") + logging.info("SG+ Successfully Initialized") keyboard_wait()