Fix bottom chests detection via anchor-based scaling and hybrid OCR verification

This commit is contained in:
VolandSZ
2026-09-09 00:46:24 +03:00
parent 34cb8ddb9f
commit 76e664f79e

View File

@ -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)