Skip to content

Batch OCR 缓存读取侧未校验计划归属,导致被 only_rec 排除的内联子识别因 sub_name 重名读到他人缓存而恒定失败 #1418

Description

@sunyink

问题类型 / Issue Type

一般 Bug / General Bug

问题描述及复现步骤 / Problem Description & Reproduction Steps

摘要:Recognizer::ocr 读取 batch OCR 缓存时只按 name 匹配,不校验该子识别是否已在 try_add_ocr_node 中被排除。结果是:一个因 only_rec: true 被正确排除出 batch 计划的内联子识别,因 sub_name 与同一 next 列表中另一个内联子识别重名,读到了对方写入的缓存,识别恒定失败。

下面把「已确认的事实」和「我的推测」分开写。


一、问题(已确认部分)

以下每一条都有日志或源码为证,不含猜测。

1. 复现结构

同一个 next 列表里有两个 And 节点,各自带一个内联 OCR 子识别,两者的名称都是 OCR

  • 节点 A QuickHunt_NoAP_Rice:内联子识别未写 sub_name,取默认名 OCR;普通 det+rec;roi [667,536,78,39]expected ["\\d"]
  • 节点 B QuickHunt_QuickBattleButton:内联子识别显式写 "sub_name": "OCR"only_rec: trueroi [1029,632,89,33]expected ["快速狩猎"]

两者 OCR 参数完全不同,但共用同一个缓存 key OCR

2. 实际发生的事

prepare_batch_ocr 的计划里只有 A 的 OCR(B 因 only_rec 被正确排除,没有进 entries):

[Recognizer.cpp][L710][prefetch_batch_ocr] prefetch_batch_ocr completed
  [entries=[{"name":"OCR"},{"name":"QuickHunt_FastBattleMAXTouchAdventureRoute"}]]
  [*ocr_batch_cache_={"OCR":[],"QuickHunt_FastBattleMAXTouchAdventureRoute":[...]}]

此处 key OCR 写入的是 A 的 roi 的 det 结果,本轮为空数组。

随后轮到 B 识别,它虽然不在计划内,却命中了这个缓存:

[Recognizer.cpp][L371][and_] And recognition [name=QuickHunt_QuickBattleButton] [param->all_of.size()=2]
[Recognizer.cpp][L393][and_] And: run inline sub recognition [inline_sub.type=OCR] [inline_sub.sub_name=OCR]
[Recognizer.cpp][L267][ocr]  OCR using batch cache [name=OCR] [cached=[]]
[OCRer.cpp][L139][handle_cached] cache is empty
[OCRer.cpp][L90][analyze]     OCR [cache_=[]] [all_results_=[]] [filtered_results_=[]] [best_result_=null]
                              [param_.only_rec=true] [param_.expected=["快速狩猎"]]
[Recognizer.cpp][L403][and_] And: sub recognition failed
[Recognizer.cpp][L422][and_] And recognition failed [name=QuickHunt_QuickBattleButton]

B 的目标文字在画面上清晰可见,其 roi [1029,632,89,33] 精确框住了目标(draw 图可另附)。这是纯假阴性。

3. 同一节点的对照组(同一份日志内)

时刻 缓存状态 结果
18:46:23.989 cache_=null(该轮 batch 未触发) 快速狩猎 score 0.9998,成功
18:46:41.353 起,连续 21 次 batch cache [cached=[]] all_results_=[],全部失败
# 未走 batch,走 only_rec 正常路径 —— 成功
[OCRer.cpp][L90][analyze] OCR [cache_=null]
  [all_results_=[{"box":[1029,632,89,33],"score":0.999784,"text":"快速狩猎"}]]
  [best_result_={"box":[1029,632,89,33],"score":0.999784,"text":"快速狩猎"}]
  [param_.only_rec=true] [param_.expected=["快速狩猎"]]

同一节点、同一 expected、同一 roi、同一屏画面,只因该轮 batch 是否触发而结论相反。

统计上是确定性的,非偶发:该日志中 cache is empty21 次,全部name=OCR;缓存中 key OCR 为空 21 次,非空 0 次。之后流程在此处死循环。

4. 源码定位

写入侧 source/MaaFramework/Task/PipelineTask.cpp:380(v5.12.2)——已正确排除 only_rec

if (param.only_rec) {
    // 这玩意 Batch 出来结果顺序可能是乱的,不知道哪个是哪个
    // 我猜的,没试过,后面有空再看看
    return;
}

读取侧 source/MaaFramework/Task/Component/Recognizer.cpp:265(v5.12.2)——没有对应的排除

if (ocr_batch_cache_ && ocr_batch_cache_->contains(name)) {
    const auto& cached = ocr_batch_cache_->at(name);
    LogDebug << "OCR using batch cache" << VAR(name) << VAR(cached);
    return build_result(name, "OCR", OCRer(image_, rois, param, cached, resource()->ocr_res().recer(param.model), name));
}

这里只用 name 做查找,既不校验该子识别是否在本轮 batch 计划内,也不校验缓存条目的 roi/param 是否与当前子识别一致。于是被写入侧排除的节点,在读取侧被重新拖回 batch 路径,同时丢失了它自己 only_rec 的执行路径(cached 构造不传 deter,是另一条分支)。

同样的问题也适用于其它被 try_add_ocr_node 排除的情形(color_filter 非空、model 不匹配、PreTask ROI 依赖)——它们都可能因重名读到不属于自己的缓存。

5. key 的取值空间没有唯一性保证

collect_ocr_from_sub_recognitionsPipelineTask.cpp:433)中,两种子识别写法取的 key 来源不同:

if (auto* node_name = std::get_if<std::string>(&sub)) {
    // 引用式:用被引用节点的真实节点名 —— 节点名唯一,同名必同参,安全
    collect_ocr_from_reco(ctx, sub_opt->name, sub_opt->reco_type, sub_opt->reco_param);
}
else {
    // 内联式:用 sub_name —— 自由文本,无任何唯一性约束
    collect_ocr_from_reco(ctx, inline_sub.sub_name, inline_sub.type, inline_sub.param);
}

内联子识别的 sub_name 与流水线节点名共用一个扁平命名空间,且未写 sub_name 时取默认名 OCR —— 只要同一 next 列表里出现两个内联 OCR 子识别,就极易撞上。

6. 与既有 PR / Issue 的关系

我没有找到覆盖本例(读取侧不校验计划归属)的既有 issue,若有重复请直接关闭。


二、推测(未经验证,仅供参考)

以下是我的猜想,没有实测验证,请以维护者判断为准。

  1. 修复方向的猜测。 感觉读取侧需要知道「本轮 batch 计划包含哪些条目」。可能的做法:把 BatchOCRPlan(或其 node_names)传给 Recognizer,读取时先确认 name 确实在计划内再用缓存;或者在 OCRCache 的 value 里连带存一份写入时的 param 指纹,读取时比对不一致就回退真实识别。哪种更合适我判断不了。

  2. 关于「空缓存」的语义。 handle_cached 目前把「缓存存在但为空」直接当作「识别无结果」。我不确定这是否是有意设计——如果 batch 预取本身可能对某些 roi 漏检,那么「预取为空」和「确实没有文字」似乎不宜等同。但这一条可能只是我不了解设计意图。

  3. 可能不止 only_rec 如第 4 节所述,color_filter / model 不匹配 / PreTask ROI 这几个排除分支在读取侧同样没有保护。我没有实际构造这几种场景验证,只是读代码推断。

  4. 关于命名约束。 我们这侧的直接触发条件是「A 未写 sub_name 取了默认名 OCR,B 显式写了 sub_name: "OCR"」。我们会自查并把内联 sub_name 改为唯一命名来规避。但即便使用方全部规范命名,只要两处恰好取了同一个名字且参数不同,问题依旧会复现,所以我倾向于认为框架侧仍需要一层保护——不过这属于设计取舍,可能维护者有不同看法。


MaaFramework 版本 / Version

v5.12.2(日志中框架自报 Version v5.12.2 / Built at Jul 19 2026 14:59:13

另已核对:v5.9.2 的 Recognizer.cpp:265 与 v5.12.2 逐字相同,only_rec 排除也在同一位置,故该版本应同样受影响。batch OCR 自 v5.7.0 引入(v5.6.0 中 ocr_batch_cache_ 不存在)。

日志文件 / Log Files

关键片段已内联在上文第 2、3 节。完整 maafw.log(约 1.8 MB)与 draw 调试图可按需补充上传。

Pipeline JSON

{
    "QuickHunt_CollectMapAdventureRouteDoubleCk1": {
        "recognition": "TemplateMatch",
        "roi": [413, 592, 159, 110],
        "template": ["DoubleExp.png", "DoubleGold.png"],
        "green_mask": true,
        "threshold": 0.8,
        "next": [
            "QuickHunt_NoAP_Rice",
            "QuickHunt_FastBattleMAXTouchAdventureRoute",
            "[JumpBack]QuickHunt_QuickBattleButton"
        ]
    },

    "QuickHunt_NoAP_Rice": {
        "recognition": "And",
        "all_of": [
            {
                "recognition": "OCR",
                "roi": [667, 536, 78, 39],
                "expected": ["\\d"]
            },
            "Sub_Ocr_Red_Clr"
        ],
        "next": ["QuickHunt_CollectMap_Initialize"]
    },

    "QuickHunt_FastBattleMAXTouchAdventureRoute": {
        "recognition": "OCR",
        "expected": ["MAX"]
    },

    "QuickHunt_QuickBattleButton": {
        "desc": "本例的受害者:only_rec 已被 batch 正确排除,却因 sub_name 撞名读到了别人的空缓存",
        "recognition": "And",
        "all_of": [
            {
                "sub_name": "OCR",
                "recognition": "OCR",
                "roi": [1029, 632, 89, 33],
                "expected": ["快速狩猎"],
                "only_rec": true
            },
            "Sub_Ocr_Enable_Clr"
        ],
        "action": "Click",
        "post_wait_freezes": 500
    },
    "Sub_Ocr_Enable_Clr": {
        "desc": "[检测]Ocr按钮背景/文字,亮色",
        "recognition": "ColorMatch",
        "roi": "OCR",
        "method": 40,
        "lower": [
            0,
            0,
            215
        ],
        "upper": [
            180,
            40,
            255
        ],
        "count": 40
    }
}

其他信息 / Additional Information

我们这侧会先通过统一内联 sub_name 命名来规避,不影响提交本报告。

感谢维护者的工作。如需完整日志、draw 图或进一步的复现配合,请随时告知。

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions