diff --git a/.pylintrc b/.pylintrc index 6456a12..5e7888d 100644 --- a/.pylintrc +++ b/.pylintrc @@ -573,7 +573,7 @@ contextmanager-decorators=contextlib.contextmanager # List of members which are set dynamically and missed by pylint inference # system, and so shouldn't trigger E1101 when accessed. Python regular # expressions are accepted. -generated-members= +generated-members=cv2 # Tells whether to warn about missing members when the owner of the attribute # is inferred to be None. diff --git a/clashroyalebuildabot/__init__.py b/clashroyalebuildabot/__init__.py index 1eabef1..2fb8fde 100644 --- a/clashroyalebuildabot/__init__.py +++ b/clashroyalebuildabot/__init__.py @@ -1,6 +1,5 @@ # Exports for clashroyalebuildabot from . import constants -from . import debugger from .bot import Bot from .detectors import CardDetector from .detectors import Detector @@ -13,9 +12,11 @@ from .namespaces import Screens from .namespaces import State from .namespaces import Units +from .visualizer import Visualizer __all__ = [ "constants", + "Visualizer", "Cards", "Units", "State", diff --git a/clashroyalebuildabot/bot/bot.py b/clashroyalebuildabot/bot/bot.py index 769808b..5917ea6 100644 --- a/clashroyalebuildabot/bot/bot.py +++ b/clashroyalebuildabot/bot/bot.py @@ -25,6 +25,7 @@ from clashroyalebuildabot.detectors.detector import Detector from clashroyalebuildabot.emulator.emulator import Emulator from clashroyalebuildabot.namespaces import Screens +from clashroyalebuildabot.visualizer import Visualizer class Bot: @@ -41,8 +42,13 @@ def __init__(self, actions, auto_start=True, debug=False, visualize=False): raise ValueError(f"Must provide 8 cards but was given: {cards}") self.cards_to_actions = dict(zip(cards, actions)) - self.detector = Detector(cards=cards, debug=self.debug, visualize=self.visualize) - self.emulator = Emulator() + config_path = os.path.join(SRC_DIR, "config.yaml") + with open(config_path, encoding="utf-8") as file: + config = yaml.safe_load(file) + + self.visualizer = Visualizer(**config["visuals"]) + self.emulator = Emulator(**config["adb"]) + self.detector = Detector(cards=cards) self.state = None @staticmethod @@ -112,6 +118,8 @@ def get_actions(self): def set_state(self): screenshot = self.emulator.take_screenshot() self.state = self.detector.run(screenshot) + if self.visualizer is not None: + self.visualizer.run(screenshot, self.state) def play_action(self, action): card_centre = self._get_card_centre(action.index) diff --git a/clashroyalebuildabot/config.yaml b/clashroyalebuildabot/config.yaml index bd24a6e..ff5d57f 100644 --- a/clashroyalebuildabot/config.yaml +++ b/clashroyalebuildabot/config.yaml @@ -13,3 +13,8 @@ adb: # The serial number of your device # Use adb devices to obtain it device_serial: emulator-5554 + +visuals: + save_labels: False + save_images: False + show_images: False diff --git a/clashroyalebuildabot/debugger.py b/clashroyalebuildabot/debugger.py deleted file mode 100644 index f35482d..0000000 --- a/clashroyalebuildabot/debugger.py +++ /dev/null @@ -1,100 +0,0 @@ -from dataclasses import asdict -import os - -from PIL import ImageDraw -from PIL import ImageFont - -from clashroyalebuildabot.constants import CARD_CONFIG -from clashroyalebuildabot.constants import LABELS_DIR -from clashroyalebuildabot.constants import SCREENSHOTS_DIR -from clashroyalebuildabot.namespaces.numbers import NumberDetection -from clashroyalebuildabot.namespaces.units import NAME2UNIT - - -class Debugger: - _COLOUR_AND_RGBA = [ - ["navy", (0, 38, 63, 127)], - ["blue", (0, 120, 210, 127)], - ["aqua", (115, 221, 252, 127)], - ["teal", (15, 205, 202, 127)], - ["olive", (52, 153, 114, 127)], - ["green", (0, 204, 84, 127)], - ["lime", (1, 255, 127, 127)], - ["yellow", (255, 216, 70, 127)], - ["orange", (255, 125, 57, 127)], - ["red", (255, 47, 65, 127)], - ["maroon", (135, 13, 75, 127)], - ["fuchsia", (246, 0, 184, 127)], - ["purple", (179, 17, 193, 127)], - ["gray", (168, 168, 168, 127)], - ["silver", (220, 220, 220, 127)], - ] - - def __init__(self): - os.makedirs(SCREENSHOTS_DIR, exist_ok=True) - os.makedirs(LABELS_DIR, exist_ok=True) - self.font = ImageFont.load_default() - self.unit_names = [unit["name"] for unit in list(NAME2UNIT.values())] - - @staticmethod - def _write_label(image, state, basename): - labels = [] - for det in state.allies + state.enemies: - bbox = det.position.bbox - xc = (bbox[0] + bbox[2]) / (2 * image.width) - yc = (bbox[1] + bbox[3]) / (2 * image.height) - w = (bbox[2] - bbox[0]) / image.width - h = (bbox[3] - bbox[1]) / image.height - label = f"{det.unit.name} {xc} {yc} {w} {h}" - labels.append(label) - - with open( - os.path.join(LABELS_DIR, f"{basename}.txt"), "w", encoding="utf-8" - ) as f: - f.write("\n".join(labels)) - - def _draw_text(self, d, bbox, text, rgba=(0, 0, 0, 255)): - text_width, text_height = d.textbbox((0, 0), text, font=self.font)[2:] - text_bbox = ( - bbox[0], - bbox[1] - text_height, - bbox[0] + text_width, - bbox[1], - ) - d.rectangle(text_bbox, fill=rgba) - d.rectangle(tuple(bbox), outline=rgba) - d.text(tuple(text_bbox[:2]), text=text, fill="white") - - def _draw_unit_bboxes(self, d, dets, prefix): - for det in dets: - colour_idx = self.unit_names.index(det.unit.name) % len( - self._COLOUR_AND_RGBA - ) - rgba = self._COLOUR_AND_RGBA[colour_idx][1] - self._draw_text( - d, det.position.bbox, f"{prefix}_{det.unit.name}", rgba - ) - - def _write_image(self, image, state, basename): - d = ImageDraw.Draw(image, "RGBA") - for det in asdict(state.numbers).values(): - det = NumberDetection(**det) - d.rectangle(det.bbox) - self._draw_text(d, det.bbox, str(det.number)) - - self._draw_unit_bboxes(d, state.allies, "ally") - self._draw_unit_bboxes(d, state.enemies, "enemy") - - for card, position in zip(state.cards, CARD_CONFIG): - d.rectangle(tuple(position)) - self._draw_text(d, position, card.name) - - image.save(os.path.join(SCREENSHOTS_DIR, f"{basename}.png")) - - def run(self, image, state): - n_screenshots = len(os.listdir(SCREENSHOTS_DIR)) - n_labels = len(os.listdir(LABELS_DIR)) - basename = max(n_labels, n_screenshots) + 1 - - self._write_image(image, state, basename) - self._write_label(image, state, basename) diff --git a/clashroyalebuildabot/detectors/detector.py b/clashroyalebuildabot/detectors/detector.py index f6998c9..6f57813 100644 --- a/clashroyalebuildabot/detectors/detector.py +++ b/clashroyalebuildabot/detectors/detector.py @@ -3,8 +3,6 @@ from loguru import logger from clashroyalebuildabot.constants import MODELS_DIR -from clashroyalebuildabot.debugger import Debugger -from clashroyalebuildabot.visualizer import Visualizer from clashroyalebuildabot.detectors.card_detector import CardDetector from clashroyalebuildabot.detectors.number_detector import NumberDetector from clashroyalebuildabot.detectors.screen_detector import ScreenDetector @@ -15,15 +13,13 @@ class Detector: DECK_SIZE = 8 - def __init__(self, cards, debug=False, visualize=False): + def __init__(self, cards): if len(cards) != self.DECK_SIZE: raise ValueError( f"You must specify all {self.DECK_SIZE} of your cards" ) self.cards = cards - self.debug = debug - self.visualize = visualize self.card_detector = CardDetector(self.cards) self.number_detector = NumberDetector( @@ -34,15 +30,6 @@ def __init__(self, cards, debug=False, visualize=False): ) self.screen_detector = ScreenDetector() - self.debugger = None - if self.debug: - self.debugger = Debugger() - - self.visualizer = None - if visualize: - self.visualizer = Visualizer() - - def run(self, image): logger.debug("Setting state...") cards, ready = self.card_detector.run(image) @@ -51,10 +38,5 @@ def run(self, image): screen = self.screen_detector.run(image) state = State(allies, enemies, numbers, cards, ready, screen) - if self.debugger is not None: - self.debugger.run(image, state) - - if self.visualizer is not None: - self.visualizer.run(image, state) return state diff --git a/clashroyalebuildabot/emulator/emulator.py b/clashroyalebuildabot/emulator/emulator.py index 69b68cd..1a7c593 100644 --- a/clashroyalebuildabot/emulator/emulator.py +++ b/clashroyalebuildabot/emulator/emulator.py @@ -13,14 +13,12 @@ import av from loguru import logger import requests -import yaml from clashroyalebuildabot.constants import ADB_DIR from clashroyalebuildabot.constants import ADB_PATH from clashroyalebuildabot.constants import EMULATOR_DIR from clashroyalebuildabot.constants import SCREENSHOT_HEIGHT from clashroyalebuildabot.constants import SCREENSHOT_WIDTH -from clashroyalebuildabot.constants import SRC_DIR class KThread(threading.Thread): @@ -97,13 +95,9 @@ def ignored(*exceptions): class Emulator: - def __init__(self): - config_path = os.path.join(SRC_DIR, "config.yaml") - with open(config_path, encoding="utf-8") as file: - config = yaml.safe_load(file) - - adb_config = config["adb"] - self.serial, self.ip = [adb_config[s] for s in ["device_serial", "ip"]] + def __init__(self, device_serial, ip): + self.device_serial = device_serial + self.ip = ip self.video_socket = None self.screenshot_thread = None @@ -173,7 +167,7 @@ def quit(self): kill_pid(self.scrcpy_proc.pid) def _run_command(self, command): - command = [ADB_PATH, "-s", self.serial, *command] + command = [ADB_PATH, "-s", self.device_serial, *command] logger.debug(" ".join(command)) try: start_time = time.time() @@ -217,7 +211,7 @@ def _start_scrcpy(self): command = [ ADB_PATH, "-s", - self.serial, + self.device_serial, "shell", "CLASSPATH=/data/local/tmp/scrcpy-server.jar", "app_process", diff --git a/clashroyalebuildabot/visualizer.py b/clashroyalebuildabot/visualizer.py index 6ada675..ecb232b 100644 --- a/clashroyalebuildabot/visualizer.py +++ b/clashroyalebuildabot/visualizer.py @@ -1,12 +1,16 @@ -import cv2 -import numpy as np +from dataclasses import asdict +import os +import cv2 from loguru import logger - +import numpy as np from PIL import ImageDraw from PIL import ImageFont from clashroyalebuildabot.constants import CARD_CONFIG +from clashroyalebuildabot.constants import LABELS_DIR +from clashroyalebuildabot.constants import SCREENSHOTS_DIR +from clashroyalebuildabot.namespaces.numbers import NumberDetection from clashroyalebuildabot.namespaces.units import NAME2UNIT @@ -29,12 +33,37 @@ class Visualizer: ["silver", (220, 220, 220, 127)], ] - def __init__(self): + def __init__(self, save_labels, save_images, show_images): + self.save_labels = save_labels + self.save_images = save_images + self.show_images = show_images + self.font = ImageFont.load_default() self.unit_names = [unit["name"] for unit in list(NAME2UNIT.values())] - cv2.namedWindow("Visualizer", cv2.WINDOW_NORMAL) - logger.info("Visualizer initialized.") + os.makedirs(LABELS_DIR, exist_ok=True) + os.makedirs(SCREENSHOTS_DIR, exist_ok=True) + + if self.show_images: + cv2.namedWindow("Visualizer", cv2.WINDOW_NORMAL) + logger.info("Visualizer initialized") + + @staticmethod + def _write_label(image, state, basename): + labels = [] + for det in state.allies + state.enemies: + bbox = det.position.bbox + xc = (bbox[0] + bbox[2]) / (2 * image.width) + yc = (bbox[1] + bbox[3]) / (2 * image.height) + w = (bbox[2] - bbox[0]) / image.width + h = (bbox[3] - bbox[1]) / image.height + label = f"{det.unit.name} {xc} {yc} {w} {h}" + labels.append(label) + + with open( + os.path.join(LABELS_DIR, f"{basename}.txt"), "w", encoding="utf-8" + ) as f: + f.write("\n".join(labels)) def _draw_text(self, d, bbox, text, rgba=(0, 0, 0, 255)): text_width, text_height = d.textbbox((0, 0), text, font=self.font)[2:] @@ -58,11 +87,12 @@ def _draw_unit_bboxes(self, d, dets, prefix): d, det.position.bbox, f"{prefix}_{det.unit.name}", rgba ) - def annotate_image(self, image, state): + def _annotate_image(self, image, state): d = ImageDraw.Draw(image, "RGBA") - for v in state.numbers.values(): - d.rectangle(tuple(v["bounding_box"])) - self._draw_text(d, v["bounding_box"], str(v["number"])) + for det in asdict(state.numbers).values(): + det = NumberDetection(**det) + d.rectangle(det.bbox) + self._draw_text(d, det.bbox, str(det.number)) self._draw_unit_bboxes(d, state.allies, "ally") self._draw_unit_bboxes(d, state.enemies, "enemy") @@ -74,7 +104,23 @@ def annotate_image(self, image, state): return image def run(self, image, state): - annotated_image = self.annotate_image(image, state) - annotated_image = np.array(annotated_image) - cv2.imshow("Visualizer", cv2.cvtColor(annotated_image, cv2.COLOR_RGB2BGR)) - cv2.waitKey(1) + n_screenshots = len(os.listdir(SCREENSHOTS_DIR)) + n_labels = len(os.listdir(LABELS_DIR)) + basename = max(n_labels, n_screenshots) + 1 + + if self.save_labels: + self._write_label(image, state, basename) + + if not self.save_images and not self.show_images: + return + + annotated_image = self._annotate_image(image, state) + + if self.save_images: + annotated_image.save( + os.path.join(SCREENSHOTS_DIR, f"{basename}.png") + ) + + if self.show_images: + cv2.imshow("Visualizer", np.array(annotated_image)[..., ::-1]) + cv2.waitKey(1) diff --git a/main.py b/main.py index 567ec5d..7aa89b8 100644 --- a/main.py +++ b/main.py @@ -41,7 +41,7 @@ def main(): MinipekkaAction, MusketeerAction, } - bot = Bot(actions=actions, debug=False, visualize=False) + bot = Bot(actions=actions) bot.run() diff --git a/platform-tools-latest-windows.zip b/platform-tools-latest-windows.zip deleted file mode 100644 index 98897e9..0000000 Binary files a/platform-tools-latest-windows.zip and /dev/null differ