From 76e664f79e97cdbcf74e28665215fd8389e87b3e Mon Sep 17 00:00:00 2001 From: VolandSZ <{E-MAIL}> Date: Wed, 9 Sep 2026 00:46:24 +0300 Subject: [PATCH] Fix bottom chests detection via anchor-based scaling and hybrid OCR verification --- bot_core.py | 66 ++++++++++++++++++++++++++++++----------------------- 1 file changed, 38 insertions(+), 28 deletions(-) diff --git a/bot_core.py b/bot_core.py index ab0a942..c8dd782 100644 --- a/bot_core.py +++ b/bot_core.py @@ -7,6 +7,7 @@ Supports resolution-independent relative coordinates and adaptive template scali from enum import Enum import logging import os +import re import time from typing import Dict, Optional, Tuple import cv2 @@ -298,49 +299,58 @@ class DirtyLeagueBot: def scan_bottom_chests(self, frame: np.ndarray) -> dict: """ - Scans chest slots 1..4 in the bottom bar of TOWER_LOBBY using relative coordinates. - Supports any resolution dynamically. + Scans chest slots 1..4 in the bottom bar of TOWER_LOBBY. + Bottom collection bar is anchored to Bottom-Left in Unity Canvas. + Uses height scaling and centered crops with hybrid template + OCR detection. """ - fh, fw = frame.shape[:2] - rel_base_x = 0.1367 - rel_pitch_x = 0.0555 - rel_y = 0.9024 - rel_w = 0.0547 - rel_h = 0.0341 - scale = self.get_frame_scale(frame) tpl_open = self.get_scaled_template("status_open", scale) tpl_vacant = self.get_scaled_template("status_vacant", scale) + base_x = 630 + pitch_x = 213 + base_y = 1885 + half_w = int(90 * scale) + half_h = int(35 * scale) + results = {} for i in range(4): slot_id = i + 1 - sx = int((rel_base_x + i * rel_pitch_x) * fw) - sy = int(rel_y * fh) - sw = int(rel_w * fw) - sh = int(rel_h * fh) - status_crop = frame[sy:sy + sh, sx:sx + sw] + cx = int((base_x + i * pitch_x) * scale) + cy = int(base_y * scale) + + y1 = max(0, cy - half_h) + y2 = min(frame.shape[0], cy + half_h) + x1 = max(0, cx - half_w) + x2 = min(frame.shape[1], cx + half_w) + status_crop = frame[y1:y2, x1:x2] status = "TIMER" + # 1. Template matching for OPEN + is_open = False if tpl_open is not None and status_crop.shape[0] >= tpl_open.shape[0] and status_crop.shape[1] >= tpl_open.shape[1]: res_open = cv2.matchTemplate(status_crop, tpl_open, cv2.TM_CCOEFF_NORMED) _, max_v_open, _, _ = cv2.minMaxLoc(res_open) - if max_v_open >= 0.75: - status = "OPEN" + if max_v_open >= 0.70: + is_open = True - if status != "OPEN" and tpl_vacant is not None and status_crop.shape[0] >= tpl_vacant.shape[0] and status_crop.shape[1] >= tpl_vacant.shape[1]: - res_vacant = cv2.matchTemplate(status_crop, tpl_vacant, cv2.TM_CCOEFF_NORMED) - _, max_v_vacant, _, _ = cv2.minMaxLoc(res_vacant) - if max_v_vacant >= 0.75: - status = "VACANT" + # 2. OCR fallback/confirmation for OPEN + txt = ocr_reader.read_text(status_crop).lower() + if is_open or "open" in txt: + status = "OPEN" + else: + # 3. Check for VACANT + if tpl_vacant is not None and status_crop.shape[0] >= tpl_vacant.shape[0] and status_crop.shape[1] >= tpl_vacant.shape[1]: + res_vacant = cv2.matchTemplate(status_crop, tpl_vacant, cv2.TM_CCOEFF_NORMED) + _, max_v_vacant, _, _ = cv2.minMaxLoc(res_vacant) + if max_v_vacant >= 0.70: + status = "VACANT" + if status != "VACANT" and ("vacan" in txt or txt == ""): + if not re.search(r"\d", txt): + status = "VACANT" - if status != "OPEN" and status != "VACANT": - txt = ocr_reader.read_text(status_crop).lower() - if "vacan" in txt: - status = "VACANT" - - click_cx = sx + sw // 2 - click_cy = sy - int(0.035 * fh) + click_cx = cx + click_cy = cy - int(80 * scale) results[slot_id] = { "status": status, "click_pos": (click_cx, click_cy)