Files
my-botty-tools/tools/asset_extractor.py

199 lines
6.0 KiB
Python

"""
D2R Asset Extractor
Runs on your local Windows machine. Captures D2R, saves screenshot.
You then send the screenshot to the AI agent for analysis.
AI returns bounding boxes -> run crop.py to extract PNGs.
Usage:
Run: python asset_extractor.py
F1: Capture D2R screen -> screenshots/debug/latest.png
F2: Crop entities from screenshots/debug/latest_annotations.json
F3: List existing assets
F12: Exit
Workflow:
1. Run this script in the botty conda env
2. F1 to capture
3. Tell your AI agent to analyze screenshots/debug/latest.png
4. AI writes screenshots/debug/latest_annotations.json with bounding boxes
5. F2 to crop entities into assets/enemies/ or assets/npc/
"""
import os, sys, cv2, numpy as np, keyboard, json, ctypes, win32gui
from datetime import datetime
from mss import mss
# DPI awareness - must be first
try:
ctypes.windll.shcore.SetProcessDpiAwareness(2)
except:
try:
ctypes.windll.shcore.SetProcessDpiAwareness(1)
except:
pass
# Fix tesserocr DLLs
if sys.platform == "win32":
_dll = os.path.join(os.path.dirname(os.path.dirname(sys.executable)), "Library", "bin")
if os.path.isdir(_dll):
os.add_dll_directory(_dll)
sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), "src"))
BASE = os.path.dirname(os.path.abspath(__file__))
SAVE_DIR = os.path.join(BASE, "screenshots", "debug")
ENEMIES_DIR = os.path.join(BASE, "assets", "enemies")
NPC_DIR = os.path.join(BASE, "assets", "npc")
for d in [SAVE_DIR, ENEMIES_DIR, NPC_DIR]:
os.makedirs(d, exist_ok=True)
LATEST_PATH = os.path.join(SAVE_DIR, "latest.png")
ANNOTATIONS_PATH = os.path.join(SAVE_DIR, "latest_annotations.json")
# Known NPC names for routing
NPC_NAMES = {
'akara', 'charsi', 'kashya', 'cain', 'drognan', 'lysander',
'fara', 'ormus', 'tyrael', 'jamella', 'halbu', 'qual_kehk',
'qual-kehk', 'qualkehk', 'malah', 'larzuk', 'anya'
}
def find_d2r():
hwnds = []
def cb(h, r):
title = win32gui.GetWindowText(h)
if 'diablo' in title.lower() and win32gui.IsWindowVisible(h):
r.append(h)
win32gui.EnumWindows(cb, hwnds)
return hwnds[0] if hwnds else None
def grab():
"""Grab D2R client area. Resizes to 1280x720 if needed."""
hwnd = find_d2r()
if not hwnd:
print(" [ERROR] D2R not found. Is it running and visible?")
return None
client = win32gui.GetClientRect(hwnd)
w, h = client[2] - client[0], client[3] - client[1]
screen_pos = win32gui.ClientToScreen(hwnd, (0, 0))
with mss() as sct:
region = {
'top': screen_pos[1],
'left': screen_pos[0],
'width': w,
'height': h
}
sct_img = sct.grab(region)
img = np.array(sct_img)[:, :, :3] # BGRA -> BGR
if w != 1280 or h != 720:
img = cv2.resize(img, (1280, 720), interpolation=cv2.INTER_LINEAR)
print(f" [RESIZED] {w}x{h} -> 1280x720")
else:
print(f" [CAPTURED] {w}x{h}")
return img
def on_f1():
"""Capture D2R and save."""
print("\n[=== CAPTURING ===]")
img = grab()
if not img:
return
cv2.imwrite(LATEST_PATH, img)
print(f" [SAVED] {LATEST_PATH}")
print(f" Now ask your AI agent to analyze: {LATEST_PATH}")
print(f" AI should write: {ANNOTATIONS_PATH}")
print(' Format: [{"name":"skeleton","x":100,"y":200,"w":60,"h":80}, ...]')
def on_f2():
"""Crop entities from latest capture using annotations JSON."""
print("\n[=== CROPPING ENTITIES ===]")
if not os.path.exists(LATEST_PATH):
print(" [ERROR] No capture found. Press F1 first.")
return
if not os.path.exists(ANNOTATIONS_PATH):
print(" [ERROR] No annotations found.")
print(f" Create: {ANNOTATIONS_PATH}")
print(' [{"name":"skeleton","x":100,"y":200,"w":60,"h":80}, ...]')
return
img = cv2.imread(LATEST_PATH)
with open(ANNOTATIONS_PATH) as f:
entities = json.load(f)
print(f" Image: {img.shape[1]}x{img.shape[0]}, Entities: {len(entities)}")
saved = 0
for ent in entities:
name = ent['name'].lower().replace(' ', '_')
x, y = int(ent['x']), int(ent['y'])
w, h = int(ent['w']), int(ent['h'])
i_w, i_h = img.shape[1], img.shape[0]
# Crop with 5px padding
pad = 5
x1, y1 = max(0, x - pad), max(0, y - pad)
x2, y2 = min(i_w, x + w + pad), min(i_h, y + h + pad)
crop = img[y1:y2, x1:x2]
# Route to npc or enemies folder
if name in NPC_NAMES:
save_dir = NPC_DIR
else:
save_dir = ENEMIES_DIR
# Auto-number duplicates
fname = f"{name}.png"
save_path = os.path.join(save_dir, fname)
variant = 1
while os.path.exists(save_path):
variant += 1
fname = f"{name}_{variant}.png"
save_path = os.path.join(save_dir, fname)
cv2.imwrite(save_path, crop)
print(f" [SAVED] {save_path} ({crop.shape[1]}x{crop.shape[0]})")
saved += 1
print(f"\n Total: {saved} assets cropped.")
def on_f3():
"""List existing assets."""
print("\n[=== ASSETS INVENTORY ===]")
for label, d in [("enemies", ENEMIES_DIR), ("npc", NPC_DIR)]:
if os.path.isdir(d):
files = sorted(os.listdir(d))
print(f"\n assets/{label}/ ({len(files)} files):")
for f in files:
sz = os.path.getsize(os.path.join(d, f))
print(f" {f} ({sz}b)")
else:
print(f"\n assets/{label}/ - EMPTY")
def run():
print("=== D2R Asset Extractor ===")
print(" F1 - Capture D2R screen")
print(" F2 - Crop entities from annotations")
print(" F3 - List assets")
print(" F12 - Exit")
print("Ready.")
keyboard.add_hotkey('f1', on_f1)
keyboard.add_hotkey('f2', on_f2)
keyboard.add_hotkey('f3', on_f3)
keyboard.add_hotkey('f12', lambda: (print("\nBye."), sys.exit(0)))
keyboard.wait()
if __name__ == "__main__":
run()