iso-bot/engine/screen/capture.py

220 lines
No EOL
7 KiB
Python

"""Screen capture utilities for taking game screenshots.
Provides efficient screenshot capture using multiple backends (mss, PIL)
with support for specific regions and window targeting.
"""
from typing import Tuple, Optional, Dict, Any
from dataclasses import dataclass
import time
import logging
import numpy as np
from PIL import Image, ImageGrab
import mss
import cv2
logger = logging.getLogger(__name__)
@dataclass
class ScreenRegion:
"""Defines a rectangular region of the screen to capture."""
x: int
y: int
width: int
height: int
@property
def bounds(self) -> Tuple[int, int, int, int]:
"""Return region as (left, top, right, bottom) tuple."""
return (self.x, self.y, self.x + self.width, self.y + self.height)
@property
def mss_bounds(self) -> Dict[str, int]:
"""Return region in MSS format."""
return {
"top": self.y,
"left": self.x,
"width": self.width,
"height": self.height,
}
class ScreenCapture:
"""High-performance screen capture with multiple backends."""
def __init__(self, backend: str = "mss", monitor: int = 1):
"""Initialize screen capture.
Args:
backend: Capture backend ("mss" or "pil")
monitor: Monitor number to capture from (1-indexed)
"""
self.backend = backend
self.monitor = monitor
self._mss_instance: Optional[mss.mss] = None
self._monitor_info: Optional[Dict[str, int]] = None
if backend == "mss":
self._initialize_mss()
def _initialize_mss(self) -> None:
"""Initialize MSS backend."""
try:
self._mss_instance = mss.mss()
monitors = self._mss_instance.monitors
if self.monitor >= len(monitors):
logger.warning(f"Monitor {self.monitor} not found, using primary")
self.monitor = 1
self._monitor_info = monitors[self.monitor]
logger.info(f"Initialized MSS capture for monitor {self.monitor}: "
f"{self._monitor_info['width']}x{self._monitor_info['height']}")
except Exception as e:
logger.error(f"Failed to initialize MSS: {e}")
self.backend = "pil"
def capture_screen(self, region: Optional[ScreenRegion] = None) -> np.ndarray:
"""Capture screenshot of screen or region.
Args:
region: Specific region to capture, or None for full screen
Returns:
Screenshot as numpy array in BGR format (for OpenCV compatibility)
"""
try:
if self.backend == "mss":
return self._capture_mss(region)
else:
return self._capture_pil(region)
except Exception as e:
logger.error(f"Screen capture failed: {e}")
# Fallback to empty image
return np.zeros((100, 100, 3), dtype=np.uint8)
def _capture_mss(self, region: Optional[ScreenRegion]) -> np.ndarray:
"""Capture using MSS backend."""
if not self._mss_instance:
raise RuntimeError("MSS not initialized")
if region:
monitor = region.mss_bounds
else:
monitor = self._monitor_info or self._mss_instance.monitors[self.monitor]
# MSS returns BGRA format
screenshot = self._mss_instance.grab(monitor)
img_array = np.frombuffer(screenshot.rgb, dtype=np.uint8)
img_array = img_array.reshape((screenshot.height, screenshot.width, 3))
# Convert RGB to BGR for OpenCV
return cv2.cvtColor(img_array, cv2.COLOR_RGB2BGR)
def _capture_pil(self, region: Optional[ScreenRegion]) -> np.ndarray:
"""Capture using PIL backend."""
if region:
bbox = region.bounds
else:
bbox = None
# PIL returns RGB format
screenshot = ImageGrab.grab(bbox=bbox)
img_array = np.array(screenshot)
# Convert RGB to BGR for OpenCV
return cv2.cvtColor(img_array, cv2.COLOR_RGB2BGR)
def save_screenshot(self, filename: str, region: Optional[ScreenRegion] = None) -> bool:
"""Save screenshot to file.
Args:
filename: Output filename
region: Region to capture, or None for full screen
Returns:
True if successful, False otherwise
"""
try:
img = self.capture_screen(region)
return cv2.imwrite(filename, img)
except Exception as e:
logger.error(f"Failed to save screenshot: {e}")
return False
def get_screen_size(self) -> Tuple[int, int]:
"""Get screen dimensions.
Returns:
(width, height) tuple
"""
if self.backend == "mss" and self._monitor_info:
return (self._monitor_info["width"], self._monitor_info["height"])
else:
# Use PIL as fallback
screenshot = ImageGrab.grab()
return screenshot.size
def find_window(self, window_title: str) -> Optional[ScreenRegion]:
"""Find window by title and return its region.
Args:
window_title: Partial or full window title to search for
Returns:
ScreenRegion if window found, None otherwise
Note:
This is a placeholder - actual implementation would use
platform-specific window enumeration (e.g., Windows API, X11)
"""
# TODO: Implement window finding
logger.warning("Window finding not implemented yet")
return None
def benchmark_capture(self, iterations: int = 100) -> Dict[str, float]:
"""Benchmark capture performance.
Args:
iterations: Number of captures to perform
Returns:
Performance statistics
"""
logger.info(f"Benchmarking {self.backend} backend ({iterations} iterations)")
start_time = time.perf_counter()
for _ in range(iterations):
self.capture_screen()
end_time = time.perf_counter()
total_time = end_time - start_time
avg_time = total_time / iterations
fps = iterations / total_time
stats = {
"backend": self.backend,
"iterations": iterations,
"total_time": total_time,
"avg_time_ms": avg_time * 1000,
"fps": fps,
}
logger.info(f"Benchmark results: {avg_time*1000:.2f}ms avg, {fps:.1f} FPS")
return stats
def __enter__(self):
"""Context manager entry."""
return self
def __exit__(self, exc_type, exc_val, exc_tb):
"""Context manager exit."""
if self._mss_instance:
self._mss_instance.close()