"""Lightweight image analysis for live capture feedback. All stats are computed on a small downsampled copy so the demo loop stays fast at full 1080p capture. """ from collections import Counter from PIL import Image ANALYSIS_SIZE = (320, 180) _TOP_COLORS = 5 # Harvesting is detected from the single pixel at this window coordinate. # Harvesting shows a light tan (~#f2c285, luma ~201); idle shows dark brown # (~#664c33, luma ~81). HARVEST_PIXEL = (1710, 1068) HARVEST_LUMA_THRESHOLD = 140 # The small numeric readout just before each bar at the bottom of the panel. # Regions are (x0, y0, x1, y1) in captured-window pixel coordinates. y0 starts # below the panel's top border line (y=1032) which would confuse OCR. NUMBER_REGIONS = { "mana": (0, 1033, 62, 1048), "food": (195, 1033, 226, 1048), "health": (355, 1033, 389, 1048), "inventory": (520, 1033, 552, 1048), "action_points": (610, 1033, 714, 1048), } _OCR_SCALE = 3 try: import pytesseract except ImportError: pytesseract = None def _luma(pixel): r, g, b = pixel return round(0.299 * r + 0.587 * g + 0.114 * b) def load_image(path): """Load a PNG as RGB; return None if the file is unreadable/corrupt.""" try: return Image.open(path).convert("RGB") except OSError: return None def is_harvesting(image): """Return whether the player is harvesting, based on the harvest pixel.""" rgb = image.convert("RGB") color = rgb.getpixel(HARVEST_PIXEL) lum = _luma(color) return { "is_harvesting": lum >= HARVEST_LUMA_THRESHOLD, "color": "#%02x%02x%02x" % color, "luma": lum, } def _ocr_number(image, region): """OCR a single number region; return int or None if it can't be read.""" if pytesseract is None: return None crop = image.convert("L").crop(region) crop = crop.resize((crop.width * _OCR_SCALE, crop.height * _OCR_SCALE)) crop = crop.point(lambda p: 0 if p < 140 else 255) try: text = pytesseract.image_to_string( crop, config="--psm 7 -c tessedit_char_whitelist=0123456789", ) except Exception: return None digits = "".join(ch for ch in text if ch.isdigit()) return int(digits) if digits else None def read_bar_values(image): """Return the numeric readout before each bar, or None if unreadable.""" rgb = image.convert("RGB") return { name: _ocr_number(rgb, region) for name, region in NUMBER_REGIONS.items() } def _pixels(image): px = image.load() w, h = image.size return [px[x, y] for y in range(h) for x in range(w)] def summarize(image): """Return size, mean RGB, and dominant colors of a captured frame.""" small = image.convert("RGB").resize(ANALYSIS_SIZE) pixels = _pixels(small) n = len(pixels) total = [0, 0, 0] for r, g, b in pixels: total[0] += r total[1] += g total[2] += b mean = tuple(round(c / n) for c in total) top = Counter(pixels).most_common(_TOP_COLORS) dominant = [ {"hex": "#%02x%02x%02x" % color, "fraction": round(count / n, 3)} for color, count in top ] return {"size": (image.width, image.height), "mean_rgb": mean, "dominant": dominant} def difference(a, b): """Sum of absolute channel differences between two same-size RGB images.""" if a.size != b.size: return None pa, pb = a.load(), b.load() w, h = a.size diff = 0 for y in range(h): for x in range(w): ca, cb = pa[x, y], pb[x, y] diff += abs(ca[0] - cb[0]) + abs(ca[1] - cb[1]) + abs(ca[2] - cb[2]) return diff