Initial project structure: reusable isometric bot engine with D2R implementation
This commit is contained in:
commit
e0282a7111
44 changed files with 3433 additions and 0 deletions
0
engine/vision/__init__.py
Normal file
0
engine/vision/__init__.py
Normal file
87
engine/vision/color.py
Normal file
87
engine/vision/color.py
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
"""Color and pixel analysis utilities.
|
||||
|
||||
Provides tools for reading health/mana bars, detecting UI states
|
||||
via color sampling, and pixel-level game state detection.
|
||||
"""
|
||||
|
||||
from typing import Tuple, Optional, List
|
||||
import logging
|
||||
|
||||
import numpy as np
|
||||
import cv2
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ColorAnalyzer:
|
||||
"""Analyze pixel colors and UI bar states."""
|
||||
|
||||
@staticmethod
|
||||
def get_pixel_color(screen: np.ndarray, x: int, y: int) -> Tuple[int, int, int]:
|
||||
"""Get BGR color at pixel position."""
|
||||
return tuple(screen[y, x].tolist())
|
||||
|
||||
@staticmethod
|
||||
def get_pixel_hsv(screen: np.ndarray, x: int, y: int) -> Tuple[int, int, int]:
|
||||
"""Get HSV color at pixel position."""
|
||||
hsv = cv2.cvtColor(screen[y:y+1, x:x+1], cv2.COLOR_BGR2HSV)
|
||||
return tuple(hsv[0, 0].tolist())
|
||||
|
||||
@staticmethod
|
||||
def color_matches(
|
||||
color: Tuple[int, int, int],
|
||||
target: Tuple[int, int, int],
|
||||
tolerance: int = 20,
|
||||
) -> bool:
|
||||
"""Check if a color matches target within tolerance."""
|
||||
return all(abs(c - t) <= tolerance for c, t in zip(color, target))
|
||||
|
||||
@staticmethod
|
||||
def read_bar_percentage(
|
||||
screen: np.ndarray,
|
||||
bar_region: Tuple[int, int, int, int],
|
||||
filled_color_hsv: Tuple[Tuple[int, int, int], Tuple[int, int, int]],
|
||||
) -> float:
|
||||
"""Read a horizontal bar's fill percentage (health, mana, xp, etc.).
|
||||
|
||||
Args:
|
||||
screen: Screenshot in BGR
|
||||
bar_region: (x, y, width, height) of the bar
|
||||
filled_color_hsv: (lower_hsv, upper_hsv) range of the filled portion
|
||||
|
||||
Returns:
|
||||
Fill percentage 0.0 to 1.0
|
||||
"""
|
||||
x, y, w, h = bar_region
|
||||
bar = screen[y:y+h, x:x+w]
|
||||
hsv = cv2.cvtColor(bar, cv2.COLOR_BGR2HSV)
|
||||
|
||||
lower, upper = filled_color_hsv
|
||||
mask = cv2.inRange(hsv, np.array(lower), np.array(upper))
|
||||
|
||||
# Scan columns left to right to find the fill boundary
|
||||
col_fill = np.mean(mask, axis=0) / 255.0
|
||||
|
||||
# Find the rightmost column that's mostly filled
|
||||
threshold = 0.3
|
||||
filled_cols = np.where(col_fill > threshold)[0]
|
||||
|
||||
if len(filled_cols) == 0:
|
||||
return 0.0
|
||||
|
||||
return (filled_cols[-1] + 1) / w
|
||||
|
||||
@staticmethod
|
||||
def sample_region_dominant_color(
|
||||
screen: np.ndarray,
|
||||
region: Tuple[int, int, int, int],
|
||||
) -> Tuple[int, int, int]:
|
||||
"""Get the dominant BGR color in a region."""
|
||||
x, y, w, h = region
|
||||
roi = screen[y:y+h, x:x+w]
|
||||
pixels = roi.reshape(-1, 3).astype(np.float32)
|
||||
|
||||
criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 10, 1.0)
|
||||
_, labels, centers = cv2.kmeans(pixels, 1, None, criteria, 3, cv2.KMEANS_RANDOM_CENTERS)
|
||||
|
||||
return tuple(centers[0].astype(int).tolist())
|
||||
140
engine/vision/detector.py
Normal file
140
engine/vision/detector.py
Normal file
|
|
@ -0,0 +1,140 @@
|
|||
"""Object and UI element detection using computer vision.
|
||||
|
||||
Provides high-level detection for game elements using template matching,
|
||||
color filtering, and contour analysis.
|
||||
"""
|
||||
|
||||
from typing import List, Optional, Tuple
|
||||
from dataclasses import dataclass
|
||||
import logging
|
||||
|
||||
import numpy as np
|
||||
import cv2
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@dataclass
|
||||
class Detection:
|
||||
"""Represents a detected object/element on screen."""
|
||||
|
||||
x: int
|
||||
y: int
|
||||
width: int
|
||||
height: int
|
||||
confidence: float
|
||||
label: str = ""
|
||||
|
||||
@property
|
||||
def center(self) -> Tuple[int, int]:
|
||||
return (self.x + self.width // 2, self.y + self.height // 2)
|
||||
|
||||
@property
|
||||
def bounds(self) -> Tuple[int, int, int, int]:
|
||||
return (self.x, self.y, self.x + self.width, self.y + self.height)
|
||||
|
||||
|
||||
class ElementDetector:
|
||||
"""Detects game UI elements and objects via computer vision."""
|
||||
|
||||
def __init__(self, confidence_threshold: float = 0.8):
|
||||
self.confidence_threshold = confidence_threshold
|
||||
self._templates: dict[str, np.ndarray] = {}
|
||||
|
||||
def load_template(self, name: str, image_path: str) -> None:
|
||||
"""Load a template image for matching."""
|
||||
template = cv2.imread(image_path, cv2.IMREAD_COLOR)
|
||||
if template is None:
|
||||
raise FileNotFoundError(f"Template not found: {image_path}")
|
||||
self._templates[name] = template
|
||||
logger.debug(f"Loaded template '{name}': {template.shape}")
|
||||
|
||||
def find_template(
|
||||
self, screen: np.ndarray, template_name: str,
|
||||
method: int = cv2.TM_CCOEFF_NORMED,
|
||||
) -> Optional[Detection]:
|
||||
"""Find best match of a template in the screen image."""
|
||||
if template_name not in self._templates:
|
||||
logger.error(f"Unknown template: {template_name}")
|
||||
return None
|
||||
|
||||
template = self._templates[template_name]
|
||||
result = cv2.matchTemplate(screen, template, method)
|
||||
_, max_val, _, max_loc = cv2.minMaxLoc(result)
|
||||
|
||||
if max_val >= self.confidence_threshold:
|
||||
h, w = template.shape[:2]
|
||||
return Detection(
|
||||
x=max_loc[0], y=max_loc[1],
|
||||
width=w, height=h,
|
||||
confidence=max_val, label=template_name,
|
||||
)
|
||||
return None
|
||||
|
||||
def find_all_templates(
|
||||
self, screen: np.ndarray, template_name: str,
|
||||
method: int = cv2.TM_CCOEFF_NORMED,
|
||||
) -> List[Detection]:
|
||||
"""Find all matches of a template above confidence threshold."""
|
||||
if template_name not in self._templates:
|
||||
return []
|
||||
|
||||
template = self._templates[template_name]
|
||||
h, w = template.shape[:2]
|
||||
result = cv2.matchTemplate(screen, template, method)
|
||||
|
||||
locations = np.where(result >= self.confidence_threshold)
|
||||
detections = []
|
||||
|
||||
for pt in zip(*locations[::-1]):
|
||||
detections.append(Detection(
|
||||
x=pt[0], y=pt[1], width=w, height=h,
|
||||
confidence=result[pt[1], pt[0]], label=template_name,
|
||||
))
|
||||
|
||||
# Non-maximum suppression (simple distance-based)
|
||||
return self._nms(detections, distance_threshold=min(w, h) // 2)
|
||||
|
||||
def find_by_color(
|
||||
self, screen: np.ndarray, lower_hsv: Tuple[int, int, int],
|
||||
upper_hsv: Tuple[int, int, int], min_area: int = 100,
|
||||
label: str = "",
|
||||
) -> List[Detection]:
|
||||
"""Find objects by HSV color range."""
|
||||
hsv = cv2.cvtColor(screen, cv2.COLOR_BGR2HSV)
|
||||
mask = cv2.inRange(hsv, np.array(lower_hsv), np.array(upper_hsv))
|
||||
|
||||
contours, _ = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
|
||||
|
||||
detections = []
|
||||
for contour in contours:
|
||||
area = cv2.contourArea(contour)
|
||||
if area >= min_area:
|
||||
x, y, w, h = cv2.boundingRect(contour)
|
||||
detections.append(Detection(
|
||||
x=x, y=y, width=w, height=h,
|
||||
confidence=area / (w * h), label=label,
|
||||
))
|
||||
|
||||
return detections
|
||||
|
||||
def _nms(self, detections: List[Detection], distance_threshold: int) -> List[Detection]:
|
||||
"""Simple non-maximum suppression by distance."""
|
||||
if not detections:
|
||||
return []
|
||||
|
||||
detections.sort(key=lambda d: d.confidence, reverse=True)
|
||||
kept = []
|
||||
|
||||
for det in detections:
|
||||
too_close = False
|
||||
for k in kept:
|
||||
dx = abs(det.center[0] - k.center[0])
|
||||
dy = abs(det.center[1] - k.center[1])
|
||||
if dx < distance_threshold and dy < distance_threshold:
|
||||
too_close = True
|
||||
break
|
||||
if not too_close:
|
||||
kept.append(det)
|
||||
|
||||
return kept
|
||||
Loading…
Add table
Add a link
Reference in a new issue