Files
climperor/fetch_cdn_templates.py
T
vosonandCursor f32d24b8f8 Initial commit: 上分帝(Climperor)
从 dota2-draft-vision 迁出并定名,作为天梯选将识别项目起点。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-26 11:47:39 +08:00

85 lines
3.0 KiB
Python

"""Download all hero portraits from Steam CDN as fallback templates.
Usage:
python fetch_cdn_templates.py
Writes:
heroes.json - hero id / key / English name table (from OpenDota constants)
templates/cdn/{key}.png - face-centered square crop resized to canonical size
"""
import json
from pathlib import Path
import cv2
import numpy as np
import requests
from common import ROOT, TEMPLATES_CDN, load_config
# Valve's own feed: ids, localized names, primary attribute. The hero-selection
# grid groups by that attribute and sorts by that localized name, so taking both
# from the same source is what lets grid.py place every cell without matching.
HEROES_URL = "https://www.dota2.com/datafeed/herolist?language={lang}"
IMG_URL = "https://cdn.cloudflare.steamstatic.com/apps/dota2/images/dota_react/heroes/{key}.png"
ATTRS = {0: "str", 1: "agi", 2: "int", 3: "all"}
# The top bar shows a fixed window of the landscape hero art, not a centred
# square. These bounds were fitted against 15 portraits captured in game:
# they lift the mean match score from 0.65 to 0.94. Stored as fractions of
# the source width so they hold whatever size the CDN serves.
CROP_X0, CROP_X1 = 38 / 256, (38 + 182) / 256
def fetch_heroes(lang: str = "schinese") -> list[dict]:
data = requests.get(HEROES_URL.format(lang=lang), timeout=30).json()
heroes = data.get("result", {}).get("data", {}).get("heroes") or data.get("heroes")
if not heroes:
raise SystemExit("hero list came back empty")
return heroes
def main() -> None:
cfg = load_config()
size = cfg["canonical_size"]
TEMPLATES_CDN.mkdir(parents=True, exist_ok=True)
print("fetching hero list from the Dota 2 data feed...")
heroes = fetch_heroes()
table = []
ok, fail = 0, 0
for h in heroes:
key = h["name"].removeprefix("npc_dota_hero_")
table.append({
"id": h["id"],
"key": key,
"name": h["name_english_loc"],
"attr": ATTRS.get(h["primary_attr"], "all"),
"name_loc": h["name_loc"],
})
out = TEMPLATES_CDN / f"{key}.png"
if out.exists():
ok += 1
continue
try:
raw = requests.get(IMG_URL.format(key=key), timeout=30).content
img = cv2.imdecode(np.frombuffer(raw, np.uint8), cv2.IMREAD_COLOR)
if img is None:
raise ValueError("decode failed")
iw = img.shape[1]
window = img[:, int(iw * CROP_X0) : int(iw * CROP_X1)]
cv2.imwrite(str(out), cv2.resize(window, (size, size), interpolation=cv2.INTER_AREA))
ok += 1
except Exception as e: # noqa: BLE001
print(f" FAILED {key}: {e}")
fail += 1
with open(ROOT / "heroes.json", "w", encoding="utf-8") as f:
json.dump(sorted(table, key=lambda t: t["id"]), f, ensure_ascii=False, indent=1)
print(f"done: {ok} templates, {fail} failures, {len(table)} heroes in heroes.json")
if __name__ == "__main__":
main()