游戏自动化要识别卡片,单用 OCR 不行,单用 CLIP 也不行。
OCR 准是准,慢;而且它的命根子是”卡面有清晰文字”——很多技能卡花体字带遮挡,识别率能给你跌成过山车。CLIP 倒是快,但前提是”见过”——头一次遇到的卡,记忆库里压根没特征,找个鬼。
后来灵光一闪,这俩其实可以串起来嘛:日常识别让 CLIP 顶着,识别不出来的,让 OCR 上去救场一次。OCR 拿到结果之后回头把这张图喂给 CLIP “学一下”——下次再遇到同样的卡,CLIP 直接就认出来了。
整个流程长这样:
1 2 3 4 5 6 7 8 9 截图 → YOLO 检测 → CLIP 尝试识别 → 命中? → 直接使用 ↓ 未命中 OCR 识别文字 ↓ 数据库匹配 ↓ 让 CLIP 学习这张图 ↓ 持久化到记忆库
CLIP 记忆库本身的工程细节我另写了一篇,这里专门讲两个东西的协作。
学习路径 完整的”学一张新卡”长这样:先让 CLIP 试一手,命中就跳过,没命中才轮到 OCR:
1 2 3 4 5 6 7 8 9 10 11 12 def learn_card (self, app, card_frame, card_list ): for card in card_list: existing_id = self ._try_clip_identify(app, card.frame) if existing_id: continue learned_id = self ._learn_via_ocr(app, card.frame) if learned_id: self .clip_manager.add_to_memory(card.frame, learned_id)
CLIP 识别那一段套了个 try/except——因为 CLIP 失败属于”正常情况”,库为空、相似度不够都会返回 None,没必要往外抛异常吓人:
1 2 3 4 5 6 7 8 def _try_clip_identify (self, app, card_frame ): try : result = self .clip_manager.retrieve(card_frame) if result is not None : return result.payload.id except Exception as e: logger.debug(f"CLIP identify failed: {e} " ) return None
OCR 这边稍微讲究点——不是把识别到的字直接当卡名(那也太天真了),而是拿去数据库里搜:
1 2 3 4 5 6 7 8 9 10 11 def _learn_via_ocr (self, app, card_image ): ocr_result = ocr_service.ocr(card_image) if not ocr_result or not ocr_result.results: return None for item in ocr_result.results: if len (item.text) >= 3 : status, db_result = database.search(item.text) if status and db_result: return db_result.id return None
len(item.text) >= 3 这个限制不能少。OCR 看到图标、装饰元素也会硬识别,给你来一堆”●““◇”“+”这种单字符——要是不卡长度,数据库搜索分分钟被这些噪声淹没。三个字以上,基本上误识别就能挡掉大头。
运行时识别 学习路径主要发生在首次扫描或者版本更新那会儿。日常跑的时候走的是另一条道——CLIP 优先,没命中才回头去学一下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 def identify_element (self, screenshot, element_frame ): result = self .clip_manager.retrieve(element_frame) if result is not None and result.similarity > 0.96 : return result.payload self .learn_element(screenshot, element_frame) result = self .clip_manager.retrieve(element_frame) if result is not None : return result.payload return None
可能你会问:学完了为啥还要 retrieve 一次?
因为 learn_element 内部会判断 OCR 是不是成功——成功了才往 CLIP 里灌,失败就啥都不做。所以学完到底有没有可用特征,得靠下一次 retrieve 兜底确认。
几个权衡 阈值定在 0.96,意思就是模棱两可的边缘情况,统统按”未命中”处理。
这是故意的。错认比漏认代价高太多了——漏认顶多就是触发一次 OCR 兜底,慢个几百毫秒;错认就麻烦了,后面整个自动化流程都得跟着跑错路径,那是真的会出事。
OCR 的开销其实不小,单次几百毫秒打底。所以 CLIP 的命中率,直接决定了用户体验的快慢。新版本卡刚出那阵子,命中率会暴跌,体感就是”咋这么慢”。等用户跑过两三次自动化把记忆库灌满,速度自己就回来了。
还有个不太显眼的好处:因为 OCR 只在 CLIP 失手时才上场,可以放心用那些又慢又准的 OCR 引擎(比如 macOS 的 Vision),不用在”速度”和”识别质量”之间做痛苦取舍。