Initial commit: 上分帝(Climperor)
从 dota2-draft-vision 迁出并定名,作为天梯选将识别项目起点。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,84 @@
|
||||
"""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()
|
||||
Reference in New Issue
Block a user