Fix bottom chests detection via anchor-based scaling and hybrid OCR verification
This commit is contained in:
66
bot_core.py
66
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)
|
||||
|
||||
Reference in New Issue
Block a user