用 OpenCV 实现云顶之弈英雄识别:从截图到英雄 ID 的完整拆解
摘要
本文基于一个云顶之弈 AI 阵容项目,聚焦讲解其中“英雄识别”模块的实现。核心代码位于 tft_screen_capture.py,整体方案使用 OpenCV 完成截图类型判断、英雄头像区域定位、模板加载、HSV 颜色直方图粗筛、灰度 NCC 精匹配,最终从游戏截图中识别出英雄 ID 和置信度。
一、项目中的英雄识别模块定位
在这个项目里,英雄识别的核心目标很明确:
输入:一张云顶之弈相关截图
输出:截图中出现的英雄 ID、简称、位置和匹配置信度
对应的核心文件是:
tft_screen_capture.py
与英雄识别直接相关的流程主要包括:
模板资源加载
-> 截图模式判断
-> 英雄候选区域检测
-> 裁剪英雄头像
-> HSV 直方图粗筛
-> 灰度 NCC 精匹配
-> 输出英雄识别结果
本文将围绕这条链路展开。
二、英雄模板从哪里来
英雄识别依赖本地模板图片,模板目录是:
tft_assets/champions/
模板文件名通常类似:
TFT16_Draven.png
TFT16_Ahri.png
TFT16_Garen.png
这些文件名和最终识别出的英雄 ID 是一致的。也就是说,如果模板匹配命中 TFT16_Draven.png,识别结果中的英雄 ID 就是:
TFT16_Draven
项目通过 tft_fetch_assets.py 从英雄数据库中读取 apiName,再下载对应的 TFT 专属英雄头像。这里有一个很重要的点:模板必须使用 TFT 专属头像,而不是普通英雄联盟立绘。
普通 LoL 立绘和 TFT 棋盘头像差异很大,如果模板源错误,后续匹配分数会明显下降,甚至导致识别完全失败。
三、模板加载:把英雄头像统一成可匹配特征
tft_screen_capture.py 中的 _load_templates() 负责加载英雄模板。
核心目标有三个:
读取本地英雄 PNG
处理透明通道
统一缩放到 64x64
预计算灰度图和颜色直方图
关键代码逻辑如下:
c64 = cv2.resize(color, (TEMPLATE_SIZE, TEMPLATE_SIZE)) g64 = cv2.cvtColor(c64, cv2.COLOR_BGR2GRAY) _champ_templates_gray[stem] = g64 _champ_templates_color[stem] = c64 _champ_templates_hist[stem] = _compute_hist(c64)这里缓存了两类英雄模板特征:
_champ_templates_gray:灰度模板,用于 NCC 精匹配
_champ_templates_hist:HSV 直方图模板,用于粗筛候选英雄
透明 PNG 的处理
很多英雄头像模板带有 alpha 透明通道。如果直接读取,透明区域可能会变成黑色背景,而截图中的头像背景并不是纯黑。
项目做了一个实用处理:把透明区域合成到中性灰背景上。
NEUTRAL_GREY = 128 alpha = a.astype(np.float32) / 255.0 bg = np.full_like(b, NEUTRAL_GREY, dtype=np.float32) def blend(ch): return (ch.astype(np.float32) * alpha + bg * (1 - alpha)).astype(np.uint8)这样可以降低模板背景和截图背景不一致带来的匹配误差。
四、截图模式判断:不同截图先分流
英雄可能出现在不同类型的截图中,例如棋盘页、结算页、阵容总览页等。项目没有把所有截图都交给同一套规则,而是先判断截图模式。
统一入口是 recognize():
if mode == "auto": mode = detect_screenshot_mode(img) if mode == "lineup": return recognize_lineup(img, champ_threshold, item_threshold, debug) if mode == "global": return recognize_global(img, champ_threshold, item_threshold, debug) if mode == "duel": return recognize_duel(img, champ_threshold, item_threshold, debug) return recognize_from_array(img, champ_threshold, item_threshold, debug)虽然项目支持多种模式,但本文只关注这些模式里共同的英雄识别思想:
先定位头像区域,再识别头像是谁
不同模式的差异主要在于“如何找到头像区域”。
五、棋盘模式:如何找到英雄头像区域
在标准棋盘模式中,英雄头像通常位于带颜色边框的六边形格子里。项目通过 HSV 颜色空间检测边框,从而定位英雄候选框。
核心函数是:
detect_hero_boxes(img)它会先把截图转换成 HSV:
hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV) combined_mask = np.zeros((h, w), dtype=np.uint8)然后根据预设的边框颜色范围生成 mask:
for lo, hi in BORDER_RANGES: mask = cv2.inRange(hsv, lo, hi) combined_mask = cv2.bitwise_or(combined_mask, mask)这里的边框颜色范围包括青色、紫色、金色、蓝色等常见英雄框颜色。
随后进行形态学处理,让边框区域更连续:
kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (5, 5)) combined_mask = cv2.morphologyEx(combined_mask, cv2.MORPH_CLOSE, kernel) combined_mask = cv2.dilate(combined_mask, kernel, iterations=2)接着提取轮廓:
contours, _ = cv2.findContours( combined_mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE )拿到候选框后,还会做多层过滤:
面积过滤
最小尺寸过滤
宽高比过滤
UI 图标过滤
NMS 去重
相对面积过滤
最终返回一组英雄头像候选框:
[(x, y, w, h), ...]这一步的作用是把“整张截图识别问题”缩小成“多个头像小图识别问题”。
六、裁剪英雄头像:去掉边框干扰
检测到英雄框后,项目不会直接拿整个框去匹配模板,而是会去掉一部分边缘区域。
原因是英雄框包含边框、特效、棋盘背景等干扰信息,而真正用于识别英雄的是内部头像区域。
代码中使用 INNER_MARGIN 控制裁剪边距:
INNER_MARGIN = 0.08实际裁剪逻辑类似:
mx = int(bw * INNER_MARGIN) my = int(bh * INNER_MARGIN) inner = img[y + my:y + bh - my, x + mx:x + bw - mx]得到的 inner 就是后续送入 identify_champion() 的英雄头像区域。
七、英雄识别核心:identify_champion()
真正判断英雄是谁的函数是:
identify_champion(crop, threshold=MATCH_THRESHOLD)它的返回值是:
(champion_stem, score)例如:
("TFT16_Draven", 0.782
如果没有达到阈值,则返回:
("", score)
默认英雄匹配阈值是:
MATCH_THRESHOLD = 0.45这个函数内部主要分成三步:
统一尺寸和亮度
HSV 颜色直方图粗筛
灰度 NCC 精匹配
八、第一步:统一尺寸和亮度
传入的头像裁剪图大小可能不一致,所以先统一缩放到 64x64:
crop_bgr = cv2.resize(crop, (TEMPLATE_SIZE, TEMPLATE_SIZE)) crop_gray = cv2.cvtColor(crop_bgr, cv2.COLOR_BGR2GRAY).astype(np.float32)然后做亮度标准化:
g_min, g_max = crop_gray.min(), crop_gray.max() if g_max - g_min > 10: crop_gray = (crop_gray - g_min) / (g_max - g_min) * 255.0这一步可以减少截图亮度、压缩、缩放差异带来的影响。
九、第二步:HSV 颜色直方图粗筛
如果每次都对所有英雄模板做精匹配,效率较低,也容易受到局部噪声影响。项目先用颜色直方图做一轮粗筛。
待识别头像会先计算 HSV 直方图:
query_hist = _compute_hist(crop_bgr)_compute_hist() 的特征不只是全局颜色,还包括四分块区域颜色:
全局 H/S/V 直方图
左上、右上、左下、右下四个区域的 H/S/V 直方图
这样做的好处是,既能利用整体颜色,又能保留一定空间分布。例如有些英雄头像主色相近,但脸部、头发、背景在不同区域的颜色分布不同,四分块直方图可以提供额外区分度。
然后和所有英雄模板直方图比较:
for stem, tmpl_hist in _champ_templates_hist.items(): sim = float(cv2.compareHist(query_hist, tmpl_hist, cv2.HISTCMP_CORREL)) hist_scores.append((sim, stem))排序后取前 15 个候选:
hist_scores.sort(reverse=True) candidates = [stem for _, stem in hist_scores[:15]]这一步不会直接决定最终英雄,只是缩小候选范围。
十、第三步:灰度 NCC 精匹配
对颜色粗筛出来的候选英雄,项目再做灰度 NCC 精匹配。
NCC 可以理解为归一化相关性,它关注两张图的结构相似程度,并且对整体亮度偏移有一定鲁棒性。
代码中会先把待识别头像拉平成向量,并减去均值:
crop_flat = crop_gray.flatten() crop_flat = crop_flat - crop_flat.mean() crop_norm = np.linalg.norm(crop_flat)模板也做同样处理:
tmpl_flat = _champ_templates_gray[stem].astype(np.float32).flatten() tmpl_flat = tmpl_flat - tmpl_flat.mean() tmpl_norm = np.linalg.norm(tmpl_flat)然后计算归一化点积:
ncc = float(np.dot(crop_flat, tmpl_flat) / (crop_norm * tmpl_norm))这一步就是在比较截图头像和模板头像的灰度结构相似度。
十一、融合分数:颜色相似度 + 结构相似度
最终分数不是单独使用直方图,也不是单独使用 NCC,而是二者加权融合:
fused = 0.4 * max(h_sim, 0.0) + 0.6 * max(ncc, 0.0)也就是说:
颜色相似度占 40%
灰度结构相似度占 60%
最终选取得分最高的英雄:
if fused > best_score: best_score = fused best_name = stem如果最高分超过阈值,就输出该英雄 ID:
if best_score >= threshold: return best_name, best_score return "", best_score这就是项目中英雄识别的核心算法。
十二、识别结果如何组织
识别完成后,项目会把英雄信息组装成结构化字典。
只看英雄识别相关字段,主要包括:
{ "id": stem, "short_id": stem.replace("TFT16_", "").replace("TFT_", "") if stem else "", "name_en": stem.replace("TFT16_", "").replace("TFT_", "") if stem else f"unknown_{x}_{y}", "position": {"row": row, "col": col}, "_score": round(score, 3), "_box": [x, y, bw, bh], }其中:
id 完整英雄 ID,例如 TFT16_Draven
short_id 简短英雄名,例如 Draven
position 英雄在棋盘中的位置
_score 模板匹配置信度
_box 英雄头像候选框坐标
这一步非常关键,因为后续模块不需要再关心图像,只需要处理结构化英雄数据。
十三、调试与阈值标定
英雄识别不可能一开始就适配所有截图,因此项目提供了调试和标定能力。
常用命令如下:
python tft_screen_capture.py screenshot.png --debug
python tft_screen_capture.py screenshot.png --diagnose
python tft_screen_capture.py screenshot.png --calibrate --known Draven Kindred Leona
其中和英雄识别最相关的是:
--debug 输出标注图,查看英雄框是否检测正确
--diagnose 查看模板加载、候选框检测和匹配分数
--calibrate 扫描不同阈值,辅助选择合适的 MATCH_THRESHOLD
阈值标定逻辑会测试多个阈值:
for t in [0.35, 0.40, 0.45, 0.50, 0.55, 0.60, 0.65, 0.70]: result = recognize(img, champ_threshold=t)如果用户提供已知英雄列表,还会计算 Precision、Recall 和 F1,帮助选择更合适的阈值。
十四、当前英雄识别技术进度
只看英雄识别部分,目前项目已经完成了以下能力:
已支持英雄模板自动加载
已支持透明 PNG 背景合成
已支持多截图模式下的英雄识别分流
已支持棋盘模式英雄候选框检测
已支持横向阵容图头像定位
已支持全局阵容图小头像滑窗检测
已支持 HSV 颜色直方图粗筛
已支持灰度 NCC 精匹配
已支持英雄 ID、short_id、位置和置信度输出
已支持 debug、diagnose、calibrate 调试工具
从技术进度上看,这套英雄识别已经具备完整闭环:
模板准备
-> 头像定位
-> 英雄匹配
-> 置信度输出
-> 调试标定
后续如果继续优化英雄识别,可以重点做这些方向:
建立真实截图测试集,统计不同截图类型下的准确率
针对相似头像英雄增加二次判别策略
优化 global 模式下小头像滑窗检测速度
对不同分辨率截图做更稳定的坐标归一化
增加误识别样本回放和阈值自动推荐机制
十五、总结
这个项目的英雄识别模块,本质上是一套基于 OpenCV 的模板匹配系统。它并不是简单地拿截图和模板硬比对,而是先通过截图模式判断和候选框检测缩小识别范围,再通过 HSV 颜色直方图筛出候选英雄,最后使用灰度 NCC 做精匹配,并输出英雄 ID、位置和置信度。
完整英雄识别链路可以总结为:
英雄模板加载
-> 截图模式判断
-> 英雄头像定位
-> 头像区域裁剪
-> HSV 粗筛候选英雄
-> NCC 精匹配确定英雄 ID
-> 输出结构化识别结果
这套方案最大的特点是工程上非常可控:模板可更新、阈值可标定、过程可调试、输出可结构化。对于云顶之弈这种 UI 相对稳定、头像资源明确的游戏场景,用 OpenCV 做英雄识别是一条很实用的路线。
