Initial commit: 上分帝(Climperor)
从 dota2-draft-vision 迁出并定名,作为天梯选将识别项目起点。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+174
@@ -0,0 +1,174 @@
|
||||
"""Recognize the 10 drafted heroes from a strategy-time screenshot.
|
||||
|
||||
Usage:
|
||||
python recognize.py samples/shot2.png
|
||||
python recognize.py samples/shot2.png --truth tinker,earthshaker,...,drow_ranger
|
||||
|
||||
Outputs per-slot JSON with top-1 hero, score and margin; slots failing the
|
||||
confidence gate are reported as null. With --truth, prints accuracy and saves
|
||||
misrecognized crops to failures/ for later labeling.
|
||||
|
||||
recognize_image() is the reusable entry point used by gsi_watch.py.
|
||||
"""
|
||||
|
||||
import json
|
||||
import sys
|
||||
import time
|
||||
|
||||
import cv2
|
||||
import numpy as np
|
||||
|
||||
from common import (
|
||||
ROOT,
|
||||
crop_slot,
|
||||
has_ranked_overlay,
|
||||
load_config,
|
||||
load_template_library,
|
||||
match_score,
|
||||
ranked_match_mask,
|
||||
)
|
||||
|
||||
FAILURES_DIR = ROOT / "failures"
|
||||
PREVIEW_DIR = ROOT / "preview"
|
||||
|
||||
|
||||
def write_sheet(img: np.ndarray, results: list[dict], cfg: dict) -> str:
|
||||
"""Contact sheet of every slot with its predicted hero, for eyeballing."""
|
||||
scale = 2
|
||||
tiles = []
|
||||
for r in results:
|
||||
crop = crop_slot(img, cfg["slots"][r["slot"] - 1], cfg)
|
||||
if crop is None:
|
||||
continue
|
||||
tile = cv2.resize(crop, None, fx=scale, fy=scale, interpolation=cv2.INTER_LANCZOS4)
|
||||
label = np.zeros((54, tile.shape[1], 3), np.uint8)
|
||||
name = r["hero"] or f"?{r['raw_best']}"
|
||||
colour = (120, 255, 120) if r["hero"] else (120, 200, 255)
|
||||
cv2.putText(label, f"{r['slot']} {name[:16]}", (4, 20),
|
||||
cv2.FONT_HERSHEY_SIMPLEX, 0.42, colour, 1, cv2.LINE_AA)
|
||||
cv2.putText(label, f"s{r['score']:.2f} m{r['margin']:.2f}", (4, 42),
|
||||
cv2.FONT_HERSHEY_SIMPLEX, 0.42, (170, 170, 170), 1, cv2.LINE_AA)
|
||||
stack = np.vstack([tile, label])
|
||||
tiles.append(cv2.copyMakeBorder(stack, 2, 2, 2, 2, cv2.BORDER_CONSTANT, value=(60, 60, 60)))
|
||||
|
||||
PREVIEW_DIR.mkdir(exist_ok=True)
|
||||
out = PREVIEW_DIR / "recognize_sheet.png"
|
||||
cv2.imwrite(str(out), np.hstack(tiles))
|
||||
return str(out)
|
||||
|
||||
|
||||
def recognize_slot(crop, library, cfg, mask=None):
|
||||
"""Return (best_hero, best_score, margin, scored list)."""
|
||||
penalty = cfg["match"]["cdn_penalty"]
|
||||
best_per_hero: dict[str, float] = {}
|
||||
for hero, source, tmpl in library:
|
||||
s = match_score(crop, tmpl, mask)
|
||||
if source == "cdn":
|
||||
s -= penalty
|
||||
if s > best_per_hero.get(hero, -2.0):
|
||||
best_per_hero[hero] = s
|
||||
ranked = sorted(best_per_hero.items(), key=lambda kv: kv[1], reverse=True)
|
||||
if not ranked:
|
||||
return None, 0.0, 0.0, []
|
||||
top1 = ranked[0]
|
||||
margin = top1[1] - ranked[1][1] if len(ranked) > 1 else 1.0
|
||||
return top1[0], top1[1], margin, ranked[:3]
|
||||
|
||||
|
||||
def recognize_image(img: np.ndarray, cfg: dict | None = None, library=None) -> dict:
|
||||
"""Recognize all slots in a full-screen frame.
|
||||
|
||||
cfg and library are accepted so a long-running caller can load the
|
||||
template library once instead of on every frame.
|
||||
|
||||
Ranked matchmaking draws a title bar + medal over every portrait; when
|
||||
that overlay is detected we match only the unoccluded face region.
|
||||
"""
|
||||
cfg = cfg if cfg is not None else load_config()
|
||||
library = library if library is not None else load_template_library()
|
||||
|
||||
t0 = time.perf_counter()
|
||||
min_score = cfg["match"]["min_score"]
|
||||
min_margin = cfg["match"]["min_margin"]
|
||||
ranked_ui = has_ranked_overlay(img, cfg)
|
||||
mask = ranked_match_mask(cfg["canonical_size"], cfg) if ranked_ui else None
|
||||
|
||||
results = []
|
||||
for slot in cfg["slots"]:
|
||||
crop = crop_slot(img, slot, cfg)
|
||||
if crop is None:
|
||||
results.append({"slot": slot["index"], "hero": None, "score": 0, "margin": 0, "top3": []})
|
||||
continue
|
||||
hero, score, margin, top3 = recognize_slot(crop, library, cfg, mask)
|
||||
passed = score >= min_score and margin >= min_margin
|
||||
results.append(
|
||||
{
|
||||
"slot": slot["index"],
|
||||
"hero": hero if passed else None,
|
||||
"raw_best": hero,
|
||||
"score": round(score, 3),
|
||||
"margin": round(margin, 3),
|
||||
"top3": [[h, round(s, 3)] for h, s in top3],
|
||||
}
|
||||
)
|
||||
elapsed = time.perf_counter() - t0
|
||||
|
||||
return {
|
||||
"radiant": results[:5],
|
||||
"dire": results[5:],
|
||||
"slots": results,
|
||||
"recognized": sum(1 for r in results if r["hero"]),
|
||||
"ranked_overlay": ranked_ui,
|
||||
"library_size": len(library),
|
||||
"elapsed_ms": round(elapsed * 1000),
|
||||
}
|
||||
|
||||
|
||||
def main() -> None:
|
||||
if len(sys.argv) < 2:
|
||||
sys.exit(__doc__)
|
||||
image_path = sys.argv[1]
|
||||
truth = None
|
||||
if "--truth" in sys.argv:
|
||||
truth = sys.argv[sys.argv.index("--truth") + 1].split(",")
|
||||
if len(truth) != 10:
|
||||
sys.exit(f"--truth expects 10 comma-separated keys, got {len(truth)}")
|
||||
|
||||
img = cv2.imread(image_path)
|
||||
if img is None:
|
||||
sys.exit(f"cannot read image: {image_path}")
|
||||
cfg = load_config()
|
||||
if not cfg["slots"]:
|
||||
sys.exit("config.json has no slots - run calibrate.py first")
|
||||
library = load_template_library()
|
||||
if not library:
|
||||
sys.exit("template library is empty - run fetch_cdn_templates.py and/or build_library.py")
|
||||
|
||||
out = recognize_image(img, cfg, library)
|
||||
results = out.pop("slots")
|
||||
print(json.dumps(out, ensure_ascii=False, indent=1))
|
||||
|
||||
if "--sheet" in sys.argv:
|
||||
print(f"sheet: {write_sheet(img, results, cfg)}")
|
||||
|
||||
if truth:
|
||||
FAILURES_DIR.mkdir(exist_ok=True)
|
||||
stamp = time.strftime("%Y%m%d_%H%M%S")
|
||||
correct = 0
|
||||
for r, expected in zip(results, truth):
|
||||
expected = expected.strip()
|
||||
got = r["hero"]
|
||||
ok = got == expected
|
||||
correct += ok
|
||||
mark = "OK " if ok else "ERR"
|
||||
print(f"{mark} slot {r['slot']}: expected={expected} got={got} (raw={r.get('raw_best')} score={r['score']} margin={r['margin']})")
|
||||
if not ok:
|
||||
slot_cfg = cfg["slots"][r["slot"] - 1]
|
||||
crop = crop_slot(img, slot_cfg, cfg)
|
||||
if crop is not None:
|
||||
cv2.imwrite(str(FAILURES_DIR / f"{stamp}_s{r['slot']}_{expected}.png"), crop)
|
||||
print(f"accuracy: {correct}/10, misses saved to failures/ (filename contains the correct key)")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user