13,171 字
66 分钟

MapLocator:终末地小地图定位

文章
技术

MaaEnd MapLocator 小地图定位的流程、各模块设计与参数来源。

MapLocator 是 MaaEnd 的定位模块,寻路、采集和日常任务都从它取坐标。本文描述当前实现,并结合 PR 记录说明各部分的设计原因。

输入与输出

输入是游戏画面中的小地图区域。1280×720 分辨率下位于画面左上角,起点 (49, 51),尺寸 118×120。

小地图在画面中的位置

输出包括:

  • zone:当前所在地图
  • x、y:亚像素精度的坐标
  • score:匹配分数
  • angle:角色朝向,正上方为 0°,顺时针增加,范围 [0, 360),识别失败时为 −1

镜头朝向 camRot 是另一个解决方案,不属于本文的设计。它是一个单独的模型,由明欣在 #5627「添加小地图摄像机朝向识别」中加入。

整体流程

每次调用 locate 传入一帧小地图,按下图处理:

mermaid
flowchart TD
    IN([locate:一帧小地图]) --> VIS{{箭头识别:箭头可见?}}
    VIS -->|否,5 秒内| BLK([ScreenBlocked])
    VIS -->|是,或已遮挡超过 5 秒| MASK[模板掩膜]
    MASK --> HAS{{有可信的追踪状态?}}
    HAS -->|是| T1
    HAS -->|否| G1

    subgraph TRACK [追踪]
        T1[运动预测<br/>预测点四周 220×220 窗] --> T2[NCC、连续亚像素精修]
        T2 --> T3{{校验}}
        T3 -->|拒绝| T4[路径热力图再匹配<br/>双策略互证、运动仲裁]
    end

    subgraph GLOBAL [全局搜索]
        G1[YOLO 区域分类]:::bg --> G2[格子窗口 394×394 粗扫<br/>常规策略、路径热力图并行]:::bg
        G2 --> G3[峰值四周 154×154 精修<br/>NCC、连续亚像素]:::bg
        G3 --> G4{{双策略互证、先验点窗口<br/>远跳拒绝、冷启动共识}}
    end

    T2 -.->|score 低于 0.75| PRE([后台提前启动全局搜索]):::bg
    T4 -->|遮挡、贴边、传送| TOG([本帧转入全局搜索])
    T4 -->|判定条件、距离| HOLD([交出上一帧坐标<br/>丢失计数 +1])
    G1 -->|None| LOST([TrackingLost])

    T3 -->|通过| OUT
    T4 -->|通过| OUT
    G4 -->|通过| OUT
    G4 -->|未通过| LOST
    OUT[输出稳定(死区)] --> HEAD{{朝向来源 heading_source}}
    HEAD -->|"character(默认)"| ARW2(位置 + 角色朝向<br/>取自箭头识别)
    HEAD -->|camera| CAM(位置 + 镜头朝向<br/>取自 camRot)
    HEAD ~~~ MID[ ]:::hidden

    classDef bg stroke-dasharray: 4 3
    classDef hidden fill:none,stroke:none

朝向来源由导航的 heading_source 决定:

  • character(默认):使用小地图中心箭头识别出的角色朝向。箭头识别失败时视为小地图被遮挡,该帧返回 ScreenBlocked,见遮挡处理。
  • camera:使用 camRot 模型给出的镜头朝向。它以本帧的定位结果为参考推理,需要先得到位置;这一模式不依赖箭头,跳过遮挡检查。

虚线框的步骤在工作线程上执行,主线程等待结果。帝江号整张图为一个类别,追踪和全局搜索都直接使用路径热力图策略。图中省略了追踪期间每 3 秒一次的后台 YOLO 推理,它只用于检测区域切换,见与全局搜索的衔接。

步骤章节
箭头识别、遮挡判断遮挡处理、朝向
模板掩膜模板掩膜
峰值是否可信相关面指标
YOLO 区域分类YOLO 区域分类
格子窗口、粗扫与精修、冷启动共识、远跳拒绝全局搜索
运动预测、追踪窗、校验追踪
输出稳定输出稳定
连续亚像素精修、模板缩放精度
路径热力图、双策略互证、运动仲裁帝江号与双策略互证
各步耗时性能

坐标系与地图划分

游戏不提供世界坐标。MapLocator 以预先制作的底图为参照,底图像素位置即坐标。

类型说明
Base四号谷地(ValleyIV_Base)、武陵(Wuling_Base)两张大地图
Tier室内分层,每层一张
小地图独立副本与帝江号(OMVBase),各一张

定位轨迹

匹配难点

对小地图做模板匹配、取最大值,任何一帧都能得到一个坐标。要让这个坐标正确、稳定、能直接交给导航,需要处理以下问题:

  • 遮挡:剧情横幅、任务提示、弹窗挡住小地图时,matchTemplate 照样返回最大值,#4987「【选剑演武】偶发性寻路失败」即由此引起。实测遮挡自行消失需要 2.8 到 4.2 秒,而导航起步时只等定位 10 秒,一直拒绝也会卡住。见遮挡处理。
  • 叠加内容:图标、路线、任务区域圈、滑索箭头链、视野扇形、自建设备群都不在底图中,未探索区域在小地图上发黑。图标多的帧,一半以上的像素要被掩膜去除;淤积点、矿点周围扎堆的设施导致过定位失败(#5833、#5809、#5571)。见模板掩膜、训练数据合成。
  • 渲染差异:实机地图层的对比度和纹理与底图不同。14 张实机帧的对比度增益在 0.63 到 0.93 之间;合成帧加上 σ 0.12 的斑驳后,模型对正确格子的置信度从 99.8% 降到 19.0%,与 #5571 实机帧的 16.6% 接近。见训练数据合成。
  • 透明区域:小地图透出身后的 3D 画面。透出亮色场景的一帧,峰值离正确位置 58 像素,正确位置的相关值为 −0.10;帝江号上常规策略的峰值与次峰同为 0.54,位置偏 26 像素。见相关面指标、路径热力图策略。
  • 相似地形与暗区假峰:草地、平整的工业区、荒地大量重复,NCC 还会放大暗区的微弱纹理。一帧在正确位置得 0.960,2170 像素外得 0.831,980 像素外几乎全黑的区域得 0.817。见全图搜索。
  • 分数无法跨帧比较:图标多的帧在正确位置只有 0.78,低于上一条那帧在 2170 像素外的 0.831,固定阈值分不开对错。见相关面指标。
  • 所在地图未知:游戏不提供当前地图,大地图 2 张、室内 74 层、独立副本与帝江号各用一张底图,传送和进出室内时随时切换。见YOLO 区域分类。
  • 错误连续胜出:相邻帧画面相似,同一个假峰可能连续多帧胜出(#2290「修复 MapLocator 冷启动定位漂移」)。见结果校验。
  • 跳变:#4197「环境监测任务——旧庭树,失败」中定位跳变,角色偏离约两百米并掉入坑中。按运动连续性仲裁之后,预测点一旦被错误结果带偏,正确结果又会被持续否决(#4950)。见帝江号与双策略互证。
  • 抖动:整数坐标的平均误差 0.380 像素,大于 0.25 像素的速度死区;逐帧搜索缩放时,武陵 31 帧中 6 帧选到 0.98 或 1.02,尺度切换带来坐标抖动(#2421「修复 tracking 状态路径抖动问题」)。见精度。
  • 时延:导航每帧都调用定位。#4889「reduce slow-path latency」之前,全部调用的 P99 为 112.78 ms,全局搜索成功的 P99 为 148.49 ms。见性能。
五类难匹配的帧

上图依次为:自建设备群;地形本身特征少;任务区域黄圈;滑索箭头链;室内显示楼层图,与大地图不对应。

演进:从 FakeCV 到 C++

最初的实现是 Go 写的 FakeCV,约 900 行,原因是 Go 缺少可用的 OpenCV 绑定。源码见 #352「小地图定位」force push 前的最后一个提交 38df2d4(fakecv.go、locator.go)。它的做法:

  • 模板的每个有效像素存为一个 probe(坐标、RGB、饱和度、梯度)
  • 匹配分数为加权像素差,权重 (梯度/255)^2,平坦区域下限 0.1
  • 额外比较 R−G 与 B−G 的差,超过 45 的部分乘 15 计入分数
  • 用绝对分数、第一第二名差距、Z-score 三项中满足两项作为接受条件

FakeCV 的图标颜色阈值沿用至今,暖色条件 r>100 && g>100 && min(r,g)-b>40 原样保留在 C++ 的掩膜函数中。

FakeCV 退役的主要原因是开发成本。缩放、掩膜、模板匹配等算子都要在 Go 里手写,性能也要自己优化(goroutine 分块并行、超过当前最优即提前终止、粗细两级步长),等于把 OpenCV 重造一遍。cpp-algo 即将可用,在 C++ 里直接调用原生 OpenCV 省事得多。

匹配算法随之换成归一化互相关(NCC)。像素差没有归一化,雾、洞穴、夜晚等场景下小地图整体对比度变化,分数无法跨场景比较;NCC 对模板和搜索区分别减均值、除标准差,亮度和对比度变化在公式中抵消。OpenCV 的 TM_CCOEFF_NORMED 就是现成的 NCC,内部有 FFT 和积分图加速,直接调用比在 Go 里再手写一次 NCC 合理得多。

#867「cppMapLocator」(2026-03)将定位迁移到 C++,结构为“YOLO 先验定界 + CV 局部精搜 + 运动学状态机”,此后的改动均在这一结构内进行。

遮挡处理

全局搜索时 YOLO 输出 None,该帧直接返回 TrackingLost,不做匹配,追踪状态保持不变。

YOLO 无法识别所有遮挡,因此另以角色箭头作为判据(#5216「小地图箭头被遮挡时拒绝该帧」):正常画面中小地图中心始终有角色箭头,箭头识别失败(angle = −1)时,该帧直接返回 ScreenBlocked,跳过匹配和追踪状态更新。该改动针对 #4987「【选剑演武】偶发性寻路失败」。这项检查可由调用方关闭:#6087「允许配置寻路使用镜头朝向或角色朝向」之后,使用镜头朝向的导航不依赖箭头,会关闭它。

拒绝的最长时间为 5 秒,超时后恢复定位,避免长期遮挡的点位卡死。实测遮挡自行消失需要 2.8 到 4.2 秒,5 秒留有余量;上限取决于导航起步时等待定位的 10 秒预算。

追踪时 score < 0.4 也视为遮挡。

模板掩膜

匹配使用 OpenCV 的带掩膜 matchTemplate(TM_CCOEFF_NORMED),在灰度图上计算。掩膜为 0 的像素不参与均值、方差和相关计算。

GenerateMinimapMask 按顺序生成五层:

cpp
// ① 圆形:半径 min(w,h)/2 - borderMargin,去除外圈 UI
cv::circle(baseMask, center, radius, cv::Scalar(255), -1);
// ② 彩色图标:暖色或冷色,椭圆核膨胀
if ((r > 100 && g > 100 && std::min(r, g) - b > cfg.iconDiffThreshold) || (b > 140 && b > r + 50)) {
    colorRow[x] = 255;
}
// ③ 白色 UI:纯 255,及 HSV 中 V>200 且 S<60 的像素,膨胀
// ④ 中心圆:去除角色箭头
// ⑤ 暗部:灰度低于阈值的像素
cv::threshold(gray, darkMask, cfg.minimapDarkMaskThreshold, 255, cv::THRESH_BINARY_INV);
五层掩膜

红色为每一层新去除的像素。最右为处理后实际参与匹配的灰度模板,棋盘格为去除的部分;该帧经过五层后,圆内剩余 56% 的像素。

  • 图标和白色 UI 周围有半透明发光和抗锯齿边缘,只去除本体会残留彩边,灰度化后形成强假边缘,因此需要膨胀。
  • 未探索区域在小地图上接近黑色,底图对应位置却有完整地形,因此去除暗部。

Base 与 Tier 使用两套参数:

参数BaseTier
外圈边距108
中心圆半径188
暗部阈值2015
白色膨胀核119
HSV 白色开关

Tier 关闭 HSV 白色,因为室内地板为浅灰白色,开启后地板会被整片当作 UI 去除。帝江号的地图为黑底白色走廊,暗部和白色两层都会去除地图本身,因此使用单独的路径热力图策略,见帝江号与双策略互证。

掩膜生成后,模板四周权重全为 0 的边会被裁掉,以减少 matchTemplate 的计算量(#4917「全局搜索补足窗口边距」)。

相关面指标

matchTemplate 在窗口内输出一张相关面,从中提取以下指标:

指标定义
peak相关面最大值
second将峰值周围 (2ex+1)×(2ex+1) 区域置为 −2 后的最大值,ex = max(3, min(w,h)/10);118×120 模板对应 23×23
deltapeak − second
PSR(peak − mean) / std,mean 与 std 在去除峰值区域后计算

计算 second 时去除峰值邻域,是为了避免次高点总是落在峰的相邻像素上。

delta 和 PSR 从两个角度衡量峰值是否可信:

  • delta 是峰值比峰值邻域以外的最好位置高出多少。delta 接近 0 时,窗口内另有一处几乎同样匹配的位置,无法确定哪一个正确。
  • PSR 是峰值高出相关面整体水平多少个标准差。背景越集中、峰越突出,PSR 越高。
三种相关面

右列以峰值为中心显示相关面,圆圈为峰值,叉为次峰。虚线框为实际搜索的格子窗口,指标均在框内计算;框外淡化部分仅作参照,浅灰为底图范围之外。

场景peakseconddeltaPSR有效像素
图标多,地形独特0.780.320.468.93563
地形轮廓简单0.960.770.193.46372
透明区域透出亮色场景0.500.460.041.66134
  • 第一行一半以上的像素被图标掩膜去除,分数较低,峰值尖锐,背景低。
  • 第二行分数高,峰值区域宽,左侧另有一处 0.77 的次峰,别处也有相近的轮廓。
  • 第三行小地图大片透明,透出的场景很亮,峰值与次峰位于同一条高亮带上,分数接近,结果不可信。
delta 与 PSR

上图为同样三个场景的两个指标。左列是沿峰值指向次峰方向的相关面剖面,灰色为峰值邻域,红色竖线为 delta;右列是窗口内去除峰值邻域后全部相关值的分布,橙色箭头从均值指向峰值,它以标准差为单位的长度即 PSR。

  • 第一行背景集中在 0 附近,标准差 0.09,峰值高出均值 8.9 个标准差;95 像素外的次峰只有 0.32,delta 为 0.46。
  • 第二行背景分布宽,标准差 0.30,PSR 为 3.4;128 像素外的另一处位置达到 0.77,delta 为 0.19。
  • 第三行次峰紧挨峰值邻域的边界,与峰值处在同一段平缓的坡上,delta 为 0.04。峰值较宽时次峰会这样落在主峰的坡上,delta 反映的是峰的陡峭程度。该窗口内 43% 的位置底图方差为 0,相关值记为 −1(直方图最左端),标准差被拉大到 0.55,PSR 为 1.6。

第三行的峰值位置是错的:

第三行峰值与正确位置

左起依次为本帧、同一地点透出暗色场景的另一帧、本帧峰值处的底图、正确位置的底图(掩膜外压暗)、两处位置的关系。另一帧在正确位置得到 0.84;本帧的峰值离正确位置 58 像素,在正确位置只有 −0.10。两帧的有效像素中约 40% 落在底图的透明处,透明处在底图上是黑色:透出的场景偏暗时与底图一致,偏亮时与底图相反,整体相关被拉成负值。

score、delta、PSR 三项同时偏低,落在下面判定条件的拒绝档:追踪时低于 0.60 的硬下限;全局搜索时低于 0.55,不算通过。

追踪时的判定条件:

cpp
bool ambiguous = score < 0.60 || (score < 0.80 && (psr < 6.0 || delta < 0.02));

即 score ≥ 0.80 直接接受;0.60 ≤ score < 0.80 时要求 PSR ≥ 6.0 且 delta ≥ 0.02;score < 0.60 一律拒绝。上表三行分别对应三档。0.60 的硬下限由 #2421「修复 tracking 状态路径抖动问题」加入。以上阈值均根据日志调整得到。

PSR 与窗口大小相关:窗口越大,背景越复杂,标准差越大,PSR 越低。追踪窗口为预测点四周 50 像素加半个模板(见搜索窗),阈值在该尺寸下调定,窗口尺寸变化时需要重新调整。

YOLO 区域分类

全图搜索

在整张底图上直接匹配存在两个问题。

第一个问题是远处的假峰。

远处的假峰

该帧正确位置分数 0.960,2170 像素外有一处 0.831,980 像素外有一处 0.817。980 像素处几乎全黑:63.5% 的像素透明,其余灰度在 0 到 19 之间。NCC 除以标准差后,这类暗区的微弱明暗变化被放大到与正常地形相同的幅度(第五张为拉伸对比度后的效果),从而与模板部分吻合。掩膜只作用于模板一侧,底图的暗区无法通过掩膜排除。

第二个问题是统计量失效。底图中大片透明区域方差为 0,带掩膜 NCC 结果为 NaN,统一置为 −1。这些 −1 使整张相关面的标准差偏大,所有帧的 PSR(定义见相关面指标)都落在 2.3 到 2.8 之间,无法区分好坏。限定在小窗口内搜索时,同一批帧的 PSR 分布在 1.6 到 8.9,具备区分度。

两个问题的根源都是搜索范围太大,缩小范围的工作交给了 YOLO:

  • #867「cppMapLocator」引入 YOLO 时,它只识别当前所在的区域与 Tier(ZoneId_Level_Tier),用来决定在哪张底图上搜索。CV 在整张底图上先降采样做金字塔粗搜,再回原图精搜。降采样后小地图细节丢失,假峰反而增多。
  • YOLO 本身是分类模型,能分出 Tier,也就能分得更细。#1326「引入 YOLO ROI 约束与全链路异步流水线」把大地图切成 160×160 的格子,每个格子作为一个类别,YOLO 直接判断小地图落在哪一格,CV 在这一格扩窗后的范围内搜索,金字塔粗搜随之移除。
  • #3236「remove tryLegacyCoarseSearch」移除了 YOLO 不可用时的整图粗搜回退。当前 YOLO 未给出可信结果时,该帧直接返回定位失败。

搜索窗从整张底图缩到几百像素见方,远处的假峰落在窗外,PSR 也恢复了区分度,搜索耗时同步下降。定位能力的上限由此落在分类模型上:模型随训练数据增长,新的干扰补充样本即可覆盖;手写的匹配规则只能针对失败情形逐个修补。格子划分和训练数据见下文,搜索窗见全局搜索。

类别划分

大地图按 160×160 切成格子,每个格子一个类别。Tier、独立副本、帝江号每张图一个类别。另有 None 类表示画面中没有可用的小地图。

类别数量
大地图格子177
Tier74
小地图4
None1
合计256

模型为 YOLO26s-cls。输入将 118×120 的小地图贴到 128×128 画布中央,再用直径 106 的圆形掩膜遮挡外圈,通过 ONNX Runtime 推理。

训练数据合成

合成与训练代码见 Endfield-Map-CLS:preprocess.py 离线合成样本,train.py 在训练时再做一轮在线变换。

实机截图难以大规模标注坐标,训练样本全部由底图合成。以 8 像素步长在底图上滑窗取中心点。大地图格子类的每个中心点先出一张黑底、不加任何增强的干净样本作为锚点;其余样本的中心随机偏移 ±5 像素(不越出所属格子)、旋转 ±0.5°,裁块后逐步叠加小地图中的各类元素。

合成流水线

最右侧为同一格子的实机帧,用于对照。实机渲染与底图并不完全一致,亮度、对比度、纹理、叠加的 UI 都可能存在差异,任何一项都可能干扰模型。离线增强各自覆盖一类可能的差异,按表中顺序执行,部分参数范围参考实机帧取值:

步骤概率参数覆盖的差异
轻度光照扰动15%其中 15% 做增益 0.97–1.03、偏移 ±2,3% 做 σ 0.2–0.4 模糊整体亮度的轻微浮动
色调收拢50%增益 0.60–0.95,向 70–130 的支点收拢地图层对比度可能被压缩,图标亮度不变。参考 14 张实机帧:增益 0.63–0.93,偏移 +5 至 +45
地图层模糊30%σ 0.4–1.0实机地图层的细纹理可能与底图不同
斑驳50%1–2 像素块的乘性亮度噪声,σ 0.04–0.15实机地图层可能带块状颗粒
视野扇形85%白色半透明扇面,方向随机,半角 25°–36°,顶点透明度 0.35–0.60,沿半径线性淡出,30–48 像素处降到 0相机视野扇面,方向与指针朝向无关。参考 7 张实机帧
路线、滑索、图标85%黄、白路线各 70%;滑索箭头链 70%;图标 2–6 个,30% 染成蓝色;地标 40%小地图上叠加的各类 UI。滑索尺寸参考 Tier388、Tier321 实机帧:箭头宽约 5 像素,间距约 5 像素
玩家指针、圆形遮罩100%指针朝向随机,遮罩直径 106

色调收拢、模糊、斑驳、视野扇形只作用于地图层,路线和图标在之后叠加,保持清晰。另有几类按固定配额生成的样本:

  • 区域圈:额外生成主样本数 50% 的样本,必定带任务区域圈,黄圈 75%、蓝圈 25%,半径 28–40,透明度 0.20–0.34,35% 的圈心贴近小地图边缘。
  • 密集图标:约 4.3% 的主样本叠加 1–2 个图标簇(每簇 8–16 个),约 0.85% 叠加 3–4 个更大的图标簇(每簇 18–28 个)并画两遍路线。
  • 中心设施簇:约 3.4% 的主样本在指针附近堆 12–32 个设施图标(2–4 种),其中 60% 再加 2–5 条供电桩电线。对应玩家在淤积点、矿点周围扎堆建设施的画面,见 #5833「基质失败」、#5809「基质失败」、#5571「刷基质识别失败,疑似淤积点识别不到,概率出现」。
  • Tier 上下文:Tier 样本四周以 0.28 的不透明度叠加上层地图。Tier 类另划出一部分样本做中心图标簇,5–8 个大号图标靠近指针,其中 35% 改用设施簇,65% 同时带区域圈。
  • 透明区域背景:黑底、程序化纹理、结构纹理三类轮换,同一中心点依次见到三种背景。纹理背景 50% 压暗到均值 12–60。实机透明区域透出的场景可能被小地图底板压暗,5 张透明帧中 4 张亮度均值在 27–38;背景亮度均匀随机时,“四周暗且有纹理”会成为 Tier 类独有的线索。

斑驳和视野扇形对模型影响很大:

  • 在一张合成帧的地图层上加 σ 0.12、2 像素块的斑驳,模型对正确格子的置信度从 99.8% 降到 19.0%,与实机帧的 16.6% 接近。反过来把实机帧的地图层做 σ 1 模糊,#5571 的失败帧从 13% 回到 95%。
  • 训练不加视野扇形时,模型对扇形的响应不稳定。同一帧擦除扇形后,同一次训练得到的两个权重,置信度一个上升 11 个百分点,一个下降 22 个百分点。

在线变换每轮重新随机,Ultralytics 自带的裁剪与翻转关闭:

  • 每张样本随机平移 ±6 像素。其中 40% 不放大、平移取整,不经插值,像素原样进入网络;其余放大 1.00–1.195 倍。放大的插值会抹平 1–2 像素的斑驳纹理(高频 RMS 从 16.8 降到 12.9,低于干净底图的 14.0),原样像素的分支让模型见到实机强度的纹理。平移上限 6 像素小于滑窗步长 8 像素,中心仍落在原格子内。
  • 水平镜像 50%。镜像画面在实机中不会出现,保留它是因为表现稳定的模型恰好对镜像不变,镜像训练让模型学到与像素排布无关的结构特征。
  • 颜色抖动:亮度、对比度 ±0.4,饱和度 ±0.7,色相 ±0.015(Ultralytics 默认值)。

None 类、难例与验证集

None 类的素材是 49 张非地图截图,涵盖商店、加载、错误弹窗、帝江号各舱室、区域建设菜单、百科,以及“其他地图追踪中”等横幅。每张在随机且不重复的位置裁出 61–62 块,同样经过离线增强,不叠加玩家指针,合计 3000 张。

实战中上报的失败帧放入 error_images,仅进入训练集,每张至少重复 5 次或达到该类样本数的 5%。

验证集为固定的 50 张实机帧,不参与训练,用于选取 best.pt 和早停。合成数据上的验证分数只反映模型对合成数据的拟合程度。

全局搜索

没有可信的上一帧位置时进入全局搜索:YOLO 确定格子,在格子窗口内粗扫并精修,score ≥ 0.55 视为通过。

YOLO 给出格子后,CV 在格子四周各扩展 64 像素(infer_margin)、再补 53 像素的窗口内搜索,共 394×394;模板裁到掩膜外接框(99×99)后匹配,候选位置 296×296,模板中心可达格子 ±67(见搜索窗补边)。整张图为一个类别的区域在整张图内搜索。

搜索窗补边(#4917)

matchTemplate 输出的每个点对应模板左上角的一个位置,因此模板中心只能落在搜索窗向内收缩半个模板的范围内。模板裁掉零权重边后有效边长约 103,向内收缩 51,名义 64 像素的余量实际只有 14 左右。YOLO 报到相邻格子时,正确位置不在搜索范围内,搜索结果必然错误,且无法通过分数阈值识别。

补边前后

修复方式是将搜索窗额外补半个模板:

cpp
constexpr int kMinMaskBorderMargin = 8;

// 模板中心只能落在搜索窗内缩 模板/2 的地方,搜索窗要按这个量补边,infer_margin 才是真的可达余量
int GlobalSearchRoiPad(const cv::Size& templSize)
{
    const int discSide = std::min(templSize.width, templSize.height) - 2 * kMinMaskBorderMargin + 1;
    const int side = std::clamp(discSide, 1, std::max(templSize.width, templSize.height));
    return static_cast<int>(std::ceil(side / 2.0)) + 1;
}

118×120 的模板补 53 像素,模板中心可达范围为格子 ±67。同一 PR 将全局搜索改为两步:在格子窗口内粗扫确定位置,再在最高点四周 24 像素的小窗内精修坐标。

先验点搜索(#5559)

调用方已知大致位置时(如滑索结束后),可传入先验点 search_hints。全局搜索在每个先验点附近额外搜索一个小窗,与 YOLO 格子窗口比较原始峰值,取较高者。该机制同样用于处理 YOLO 报到相邻格子的情况。

结果校验

错误坐标会让导航朝错误方向移动,代价高于等待一帧,因此结果在交出前需要通过一系列校验。追踪结果的校验见校验与失败处理。

冷启动共识(#2290「修复 MapLocator 冷启动定位漂移」):最近 3 次全局结果都在最新一次的 12 像素以内,才交出结果并开始追踪;score ≥ 0.85 时跳过此步。该机制用于过滤偶发的假峰。相邻帧画面高度相似,同一个错误位置可能连续多帧胜出,因此共识只能保证结果稳定。

远跳拒绝(#2290):追踪丢失后重新全局搜索时,结果距上一个已知位置超过 80 像素则拒绝,score ≥ 0.85 时除外。

软门槛:所有结果都低于 0.55 时,最高的原始峰仍会继续进入远跳检查和冷启动共识。部分点位所有帧都无法过线,严格执行会导致永远无法定位。

追踪

已有可信的上一帧位置时,每帧只在预测位置附近的小窗口内匹配,跳过 YOLO 和格子窗口搜索。连续移动时相邻帧的位移只有几像素,小窗口足以覆盖,远处的相似地形也落在窗外。

进入条件

同时满足以下条件时走追踪,否则本帧走全局搜索:

  • 已有当前区域和上一帧位置
  • 丢失计数不超过 max_lost_frames(默认 3,帝江号为 10)
  • 调用方没有要求强制全局搜索(force_global_search),指定的区域(expected_zone_id)与当前区域一致

运动预测

预测点为上一帧位置加上速度乘以距上一帧的时间。距上一帧超过 5 秒时不做外推,直接以上一帧位置为预测点。

速度用 EMA 平滑,α = 0.5。仅在上一帧被正常接受、帧间隔为 16 ms 到 5 秒、score ≥ 0.70 时更新;位移小于 0.25 像素时速度置零。低分帧不参与更新,避免一次错误匹配把预测点带偏。

搜索窗

搜索窗是以预测点为中心的正方形,边长为 2 × (50 + 模板长边 × 缩放 / 2)。118×120 的模板对应 220×220;ValleyIV_Base 的缩放为 15/16,对应 212×212。模板在窗内滑动,模板中心可达预测点水平 ±51、垂直 ±50 像素。窗口超出底图的部分用透明像素补齐。

追踪窗的候选位置为 103×101,约为全局搜索(296×296)的 12%。

追踪窗与全局搜索窗

上一帧位置和预测偏差为示意值。中图蓝框为同一帧全局搜索的窗口,黄框为追踪窗,点线框为该帧对应的 YOLO 格子。右侧为全局搜索窗内的相关面,每个像素对应模板中心的一个候选位置;黄色虚线框是追踪窗覆盖的部分,框内数值与追踪窗单独匹配的结果相同。峰值离预测点约 11 像素,在追踪窗内;左侧 0.77 的次峰落在追踪窗之外,追踪时不参与比较。

匹配使用与全局搜索相同的模板掩膜和 NCC,峰值同样做连续亚像素精修(见亚像素)。

校验与失败处理

追踪结果依次检查:

  1. 峰值贴近窗口边缘(1 像素以内):真实位置可能在窗口外,拒绝。
  2. 与上一帧位置换算出的速度超过 40 px/s:视为传送,拒绝。
  3. score < 0.4:视为遮挡,拒绝。
  4. 不满足相关面指标中的判定条件,拒绝。
  5. 距上一帧超过 25 像素且 score < 0.78,拒绝。

第 5 条与第 4 条中 0.60 的硬下限同由 #2421「修复 tracking 状态路径抖动问题」加入,用于抑制低置信度帧造成的路径抖动。

普通区域的追踪被拒绝且 score > 0.1 时,先在同一窗口内用路径热力图策略再匹配一次,两路结果互证或按运动连续性仲裁,见帝江号与双策略互证。帝江号本身使用路径热力图策略,接受条件见路径热力图策略。

仍未通过时按拒绝原因处理:

  • 遮挡、贴边、传送:本帧转入全局搜索。
  • 仅判定条件不满足或距离异常:交出上一帧的测量坐标,丢失计数加一。连续丢失超过 max_lost_frames 后不再进入追踪,转入全局搜索。

#5901「删除 isHeld 状态, 不可信帧交上一帧坐标并记丢失」移除了 isHeld 标记。此前这类帧带有该标记,各调用方处理方式不一致;现在它们与正常帧以相同形式交出,不确定性体现在丢失计数中。

与全局搜索的衔接

追踪 score < 0.75 或被拒绝时,后台立即开始本帧的全局搜索(工作线程不少于 6 个时)。追踪最终失败时直接取用这次结果,省去串行等待,见 #4889「reduce slow-path latency」。

追踪期间 YOLO 每 3 秒在后台推理一次,只用于检测区域切换。识别出的区域与当前区域不同时,追踪器立即失效,转入全局搜索。

输出稳定

静止时亚像素坐标存在零点几像素的抖动,会被导航误判为移动。score ≥ 0.80 且与上一次输出的距离小于 0.40 像素时,输出保持不变。

精度

亚像素

#2566「重新引入亚像素精度定位」之前输出坐标为整数,#2566 加入亚像素精修,同时加入上述两个死区。

matchTemplate 只在整数位置计算分数,相关面是一张整数网格。模板的真实位置一般落在两个整数之间,直接取最高的格点,每个轴最多差 0.5 像素。

相关面的一维切面

左图沿横向切开相关面的峰。黑点是 matchTemplate 的输出;灰线是把底图平移非整数距离后逐点重新计算的分数,在整数处与黑点重合。真实位置为 +0.376,整数峰输出 0。右图放大峰顶。示例样本的合成方式同下方的对比实验。

精修分两步。

抛物线拟合:取整数峰和左右两个相邻格点,过这三点作抛物线,以顶点为横坐标,纵向同样处理。偏移量为 0.5 ×(左 − 右)/(左 − 2 × 中 + 右),限制在 ±0.5 以内。这一步只用到已有的三个格点,开销可以忽略。相关面的峰形与抛物线有差异,顶点通常偏向整数峰(按下方方式另合成 80 个样本,约 85% 的坐标轴如此),右图中偏了 0.056 像素。全局搜索的粗扫只做这一步。

连续求极大:分数可以在任意亚像素位置直接计算。用 cv::remap 三次插值把底图平移 (tx, ty),再按 TM_CCOEFF_NORMED 的公式计算带权 ZNCC。位移为整数时,结果与 matchTemplate 的输出逐位相同,也就是图中的灰线。以抛物线顶点为起点,在这个函数上找最大值:

cpp
// 在整点峰周围连续求极大。目标函数与 TM_CCOEFF_NORMED 同式(整点处两者逐位相同),
// 区别是底图按亚像素位置重采样,因此不受相关面格点的限制
std::optional<RefinedPeak> RefinePeakContinuous(...);
模式搜索的轨迹

找最大值用模式搜索:沿 x、y 两个方向依次试探正负一步,分数提高就移过去,继续用同一步长;四个方向都没有提高时步长减半。初始步长 0.25,步长小于 0.01 或求值达到 40 次时停止,位移限制在整数峰 ±1 像素以内。图中起点为抛物线结果,误差 0.083;0.25 和 0.125 两轮四个方向都没有提高,步长减到 0.0625 后先右后上各移一步,之后以 0.031 和 0.016 的步长各移一步,终点误差 0.010,共 29 次求值。精修窗和追踪都做这一步。

另一种常见做法是将相关面三次插值放大 20 倍后取最大值。它插值的对象仍是整数格点上的值,结果也量化到 0.05 像素。

四种方法在 80 个合成样本上的对比。样本为武陵底图按随机亚像素偏移做 Lanczos 平移,叠加 0.6 到 0.95 的色调增益和 σ 0.08 的乘性噪声:

方法平均误差
整数峰0.380 px
抛物线0.068 px
相关面三次插值 ×200.036 px
连续求极大(当前)0.018 px

连续求极大的 p90 误差为 0.031 像素。整数峰的平均误差大于 0.25 像素的速度死区。

模板缩放(#5019 改为按区域固定)

#5019 之前缩放逐帧搜索:

  • 全局搜索:0.90 到 1.10,共 11 个尺度
  • 追踪:在当前尺度附近试 ±0.02、±0.04;新尺度领先 0.03 以上才切换,切换时位置变化不超过 2 像素

逐帧搜索缩放引入了一个额外自由度,会拟合噪声。武陵的真实比例为 1.0,31 帧中 25 帧选中 1.00,6 帧选中 0.98 或 1.02,胜出尺度的分数比 1.00 最多高 0.018。尺度切换会改变模板尺寸和中心位置,导致坐标抖动。#2421「修复 tracking 状态路径抖动问题」处理的即是尺度反复切换导致的抖动,当时的方案是加入滞回。

四号谷地的分数-缩放曲线

四号谷地 8 帧中,7 帧的最优尺度落在 0.93 到 0.95 之间,分布在 15/16 = 0.9375 附近;#4255「新版本已知问题若干」中的一帧(灰色虚线)为 1.035。曲线在 1.0 处有明显凹陷:1.0 是唯一不需要插值的尺度,其他尺度的模板经插值平滑后与平滑的底图更接近,分数偏高。

缩放比例在底图制作时就已确定。Tier 图与小地图比例一致,Base 图中只有四号谷地放大了 16/15。测量方法是将四号谷地的 Tier 图在其所属 Base 图上匹配,取峰值所在尺度:

cpp
inline double ZoneTemplateScale(const std::string& zoneId)
{
    return zoneId == "ValleyIV_Base" ? 15.0 / 16.0 : 1.0;
}

连续求极大与固定缩放在同一个 PR(#5019)中完成,关联以下 issue:

  • #4983「环境监测净水温控装置任务失败」
  • #4987「【选剑演武】偶发性寻路失败」
  • #4962「选剑演武跳崖任务失败」
  • #4930「选剑演武寻路失败」
  • #4903「选剑演武失败和自动采集路线13稳定失败」

帝江号与双策略互证

路径热力图策略

帝江号的小地图有大片透明区域,身后的 3D 实机画面会透过来,与底图无关的像素大量混入模板,常规策略无法定位。

路径热力图把图像换成「像不像路面」的单通道图:计算每个像素到路面颜色 (B237, G233, R228) 的曼哈顿距离 d,d < 180 时映射为 255 − d·255/180,否则为 0,再做 5×5 高斯模糊。底图透明的部分直接置 0。

路径热力图的计算

上图为 MaaEnd 测试集中的一张帝江号截图。采样像素中,胶囊房间和走廊边缘接近路面颜色,映射为 217 和 128;浅灰、深灰区域和视野扇形离路面颜色很远,映射为 0。紫色图标的 B 通道为 255,d = 181,刚好落在阈值之外。下行是小地图和底图对应位置各自做同样变换的结果,两边只剩走廊和房间的形状,路径热力图策略在这两张图上做 NCC。这一策略的掩膜关闭了暗部剔除和 HSV 白色剔除,走廊像素得以保留。

常规策略与路径热力图策略的匹配对比

同一张截图下,两种策略分别在整张帝江号底图上匹配,上下两行各为一种策略的完整过程。搜索图中白色虚线框为正确位置,最右列是匹配位置处的底图,绿色为小地图中接近路面颜色的像素。

  • 常规策略:掩膜保留的 3000 个像素大多是透出的画面,白色走廊被 HSV 白色掩膜剔除。峰值 0.54 与次峰 0.54 无法区分,峰值位置比正确位置偏下 26 像素,绿色与底图走廊错开。
  • 路径热力图策略:模板只剩走廊,峰值 0.84,绿色与底图的胶囊形房间和横向走廊重合。次峰 0.61 在同一条走廊上,距峰值 12 像素。

热力图的 NCC 分数普遍偏低,接受条件为分段规则:

cpp
bool accept = (score >= 0.85)
           || (score >= 0.70 && delta >= 0.25 && psr >= 2.0)
           || (score >= 0.42 && delta >= 0.04 && psr >= 3.8)
           || (score >= 0.40 && delta >= 0.05 && psr >= 3.8);

均不满足时计算倒角距离(Chamfer):对模板和匹配区域分别提取 Canny 边缘(100/200),对匹配区域的边缘做距离变换,计算模板边缘上的平均距离。小于 4.5 像素时视为走廊形状吻合,分数提升到 0.43 并接受。帝江号允许连续丢失 10 帧。

双策略互证

普通区域追踪失败且 score > 0.1 时,在同一窗口内用路径热力图策略再匹配一次。两路分数均 ≥ 0.45 且坐标差不超过 4 像素时,视为可信。全局搜索有相同机制,分数门槛 0.50。

运动仲裁(#4207)

两路结果不一致且存在运动预测时,取离预测点较近的一路,距离不超过 25 像素即接受。该机制修复了 #4197「环境监测任务——旧庭树,失败」:环境监测任务中定位跳变,角色偏离约两百米并掉入坑中。

仲裁死锁回收(#4950)

预测位置被错误结果带偏后,指向正确位置的主策略会因距离预测过远而持续被否决。回收条件:

  • 主策略连续 5 帧被否决
  • 每次结果相互距离在 6 像素以内
  • 主策略分数不低于路径热力图策略

满足时判定预测错误,采纳主策略结果并清零速度,避免这次修正被计为高速位移。

朝向

朝向由小地图中心的角色箭头计算,角度以正上方为 0°,顺时针增大:

  1. 在中心 24×24 区域内取 220 到 255 的白色像素,选取离中心最近的轮廓,距离超过 5 像素视为识别失败。
  2. 箭头仅十余像素,锯齿明显,先三次插值放大 16 倍再二值化。
  3. 取最大轮廓的质心,用 minEnclosingTriangle 拟合外接三角形。
  4. 用凸包和 approxPolyDP 将轮廓化简为三个角,ε 取凸包周长的 0.05、0.04、0.06、0.03、0.08、0.02、0.10 倍依次尝试,直到恰好得到三个角。
  5. 三个角中最尖的角指示前方,从外接三角形中选取与该方向最一致的顶点作为尖端。化简不出三个角时,取离质心最远的顶点。
  6. 朝向为质心指向尖端的方向。
朝向计算流程

图为 MaaEnd 测试集中的一帧。中心 24×24 内有 6 个白色轮廓,绿色为离中心最近的箭头,红色为周围图标的白色部分,都在 5 像素范围(黄色虚线圆)之外。化简后的三个角为 45°、63°、72°,最尖的 45° 指向左侧,外接三角形中与之方向最一致的是左侧顶点,结果为 281.3°。

尖端判据(#4576)

#4576「箭头尖端最小内角判据」之前,尖端取外接三角形中离质心最远的顶点。小地图上的箭头白色部分接近等边三角形,尖端和后角到质心的距离接近;箭头尾部有缺口,质心随之前移,部分角度下后角离质心比尖端更远,方向随之反转。该问题见 #4497「赠送干员礼物…&干员识别偶遇bug」中附带的送礼走位 bug。内角只由轮廓形状决定,尖端的内角明显小于两个后角。

旧判据在实测帧上的反向

图为下文实测中旧判据反向的全部 4 帧。上行为中心 24×24,红色虚线为旧判据的结果,绿色为当前判据的结果,参照角度的来源见实测。下行为放大后的轮廓:蓝色为外接三角形,蓝色数字为顶点到质心的距离(原始像素);橙色为化简得到的三个角。4 帧中离质心最远的都是后角,比尖端远 0.1 到 0.4 像素。尖端的内角为 42° 到 45°,后角为 56° 到 80°。

将箭头图标以 0.5° 为步长旋转一周并合成到纯色背景上,共 720 个角度:

尖端选取方法平均误差p99误差 > 30° 的帧数
离质心最远的顶点(旧)3.74°126°12
最尖的角(当前)1.71°5.6°0
逐角度误差

旧判据的 12 次反向出现在 32° 和 55° 附近,并每隔 90° 重复一次,与像素网格的四重对称一致。当前判据在所有角度上的误差都在 5.7° 以内。

实测

截图没有朝向真值,参照角度用另一种方法得到:将箭头图标以 1° 为步长旋转,在小地图中心附近做模板匹配,取分数最高的角度。测试集中有 86 帧在文件名中记录了朝向,参照角度与记录值平均相差 2.6°,最大 8°。

样本为 MaaEnd 测试集和区域分类验证集中的大世界截图,去重后 135 帧。另有一帧来自 #4255,小地图位置与标准布局不同,箭头偏离中心区域,未计入。

尖端选取方法平均误差中位数p99最大误差 > 30° 的帧数
离质心最远的顶点(旧)5.75°1.83°133°134.8°4
最尖的角(当前)1.93°1.83°5.3°6.6°0

误差中包含参照本身的偏差。两种判据在其余 131 帧上结果相同。135 帧都化简出了三个角,尖端的内角为 38° 到 50°,后角最小 55°。

实测帧的朝向

按朝向大致均匀选取的 12 帧,绿色箭头为当前判据的结果,括号内为参照角度。

性能

测量环境

不同机器上的绝对耗时差异很大,下文的数字只在同一环境内可比,各阶段之间的比例更有参考价值。

项目配置
CPUApple M5,10 核(4 性能核 + 6 能效核)
内存24 GB
系统macOS 27.0
编译Apple clang 21.0.0,-O2 -DNDEBUG,与发布包 RelWithDebInfo 的优化级别相同
依赖OpenCV 4.12.0,ONNX Runtime 1.19.2
模型YOLO 区域分类模型、camRot 模型,与 v2 随附的相同,均已加载
线程按核数自动取值:匹配工作线程 10 个,全局搜索并发上限 3,YOLO 推理 4 线程,camRot 推理 2 线程
代码MaaEnd v2 bcc75f3,MapLocator 最后一次改动为 c2c713d(2026-10-01)
样本MaaEnd 测试集中 111 张 1280×720 截图

在 MapLocator 各阶段的入口和出口记录时间戳与所在线程,定位逻辑不变。正式计时前先预热 5 次。

  • 全局搜索:每次调用前清空追踪状态,每帧调用 3 次,共 333 次。255 次成功;72 次 score 低于 0.85,状态已清空,停在冷启动共识的收集阶段,不交出坐标;6 次箭头被遮挡。
  • 追踪:每帧先用全局搜索建立追踪,再对同一帧定位 10 次,共 1090 次,另有 2 帧无法建立追踪。同一帧重复定位时位移为零,追踪窗的尺寸与连续移动时相同。
  • 每次调用后等待 150 ms(全局搜索)或 60 ms(追踪),后台任务跑完再开始下一次,避免与下一次调用争抢线程。

单帧时间线

三种路径的单帧时间线

三种路径各取一次耗时接近中位数的调用。每行是一个线程,横轴为自 locate 调用起的时间,主线程上的虚线表示在等待其他线程的结果。

  • 追踪成功:箭头识别在独立线程上同时开始,主线程先等它判断是否遮挡(约 0.4 ms),随后生成模板掩膜,在 220×220 的追踪窗内做 NCC 和连续亚像素精修,不到 10 ms 得到坐标。之后的 13 ms 是 camRot 推理。
  • 追踪被拒:常规策略的结果没有通过校验,主线程在同一窗口内用路径热力图再匹配一次(深灰为倒角距离检查),两路结果按运动仲裁后接受。常规策略的 score 低于 0.75,精修一结束,工作线程就开始了本帧的全局搜索(YOLO 和两种策略的粗扫)。追踪最终被接受,这次全局搜索的结果没有用上。
  • 全局搜索成功:没有追踪状态,调用一开始工作线程 1 就做 YOLO 推理,随后生成模板特征。工作线程 2 做常规策略粗扫,工作线程 1 同时投机执行路径热力图粗扫。常规粗扫在 20 ms 左右结束,接着在峰值四周 154×154 的精修窗内做 NCC 和连续亚像素精修,结果通过,路径热力图的结果丢弃。主线程在这 25 ms 内一直等待,之后是 camRot。

各阶段耗时

三种路径的分阶段耗时

上图为三种路径在主线程上各阶段耗时的中位数。全局搜索的 YOLO、NCC、亚像素精修在工作线程上执行,这里计的是主线程等待它们的时间。各段中位数之和与总耗时的中位数接近。

各阶段单独计时:

阶段线程中位p90
箭头识别独立线程0.4 ms0.5 ms
模板掩膜与特征主线程或工作线程0.6 ms0.7 ms
追踪 NCC(220×220)主线程4.3 ms5.9 ms
追踪连续亚像素精修主线程4.1 ms5.7 ms
追踪路径热力图 NCC主线程6.0 ms7.3 ms
追踪路径热力图连续亚像素精修主线程5.3 ms7.5 ms
YOLO 推理工作线程4.3 ms6.5 ms
粗扫 NCC(394×394,常规策略)工作线程13.0 ms17.1 ms
粗扫 NCC(394×394,路径热力图)工作线程12.9 ms16.9 ms
精修 NCC(154×154)工作线程1.8 ms2.2 ms
精修连续亚像素精修工作线程3.3 ms4.4 ms
camRot主线程13.5 ms16.1 ms

单次 locate 的总耗时:

路径次数中位p90p99不含 camRot 的中位
追踪成功97023.0 ms28.3 ms67.0 ms9.8 ms
双策略追踪12038.7 ms43.9 ms54.2 ms23.9 ms
全局搜索成功25539.3 ms51.8 ms93.5 ms25.0 ms
全局搜索,冷启动共识收集中7225.3 ms32.5 ms42.8 ms不执行 camRot
  • camRot 是单项最大的开销。它属于镜头朝向这条旁路,模型已加载时,每个带坐标的结果都在主线程同步推理一次。定位本身的耗时看「不含 camRot」一列,追踪成功为 9.8 ms。
  • 连续亚像素精修与 NCC 耗时相当。matchTemplate 借助 FFT 和积分图一次算出整个窗口的相关面;连续求极大每次求值都要重采样底图、重算一次带权 ZNCC,最多 40 次(见亚像素)。精修窗的 NCC 只要 1.8 ms,连续求极大仍要 3.3 ms。
  • 格子窗口粗扫 13.0 ms,是追踪窗 NCC 的 3 倍,与两个窗口的面积比 394² / 220² ≈ 3.2 一致。追踪省下的主要就是这部分,以及 YOLO 推理。
  • 双策略追踪比追踪成功多出约 14 ms,来自同一窗口内的第二次匹配:路径热力图 NCC 6.0 ms,连续亚像素精修 5.3 ms,都比常规策略慢 1 到 2 ms。
  • 路径热力图粗扫与常规粗扫在两个工作线程上同时执行。常规策略通过时它的结果被丢弃,代价是多占一个线程的 CPU 时间,主线程等待的只有常规粗扫。
  • 追踪期间每 3 秒一次的后台 YOLO 推理与主线程争抢 CPU。赶上后台推理的 30 次追踪成功,中位耗时为 24.3 ms,比全部追踪成功的中位多 1.3 ms。
  • 相关面指标(peak、second、delta、PSR)的计算在追踪窗上约 0.04 ms,在格子窗口上约 0.3 ms。
  • 整张图为一个类别的区域在整张图上搜索。Tier 底图小,粗扫中位 5.6 ms(9 次);帝江号的路径热力图整图粗扫中位 12.7 ms(3 次)。

慢路径优化(#4889)

#4889「reduce slow-path latency」优化了追踪失败、转入全局搜索的慢路径。测试使用 43.8 秒连续移动录屏,同机 A/B,各运行两轮,共 438 次调用。这组数据测于 #5019 之前,当时全局搜索每帧要在 11 个尺度上匹配,数字只用于前后对比:

指标优化前优化后
全部调用 P99112.78 ms64.26 ms
双策略追踪 P99101.55 ms60.31 ms
全局搜索成功 P99148.49 ms97.73 ms
逐帧强制全局 P9985.63 ms61.26 ms
逐帧强制全局平均51.59 ms39.49 ms

主要措施:

  • 按优先级调度的线程池,追踪任务优先于全局搜索
  • 同一帧的模板特征只计算一次
  • YOLO 推理与全局搜索的候选准备并行
  • 8 线程及以上的机器上,路径热力图策略提前投机执行,不需要时取消
  • 追踪期间 YOLO 在后台异步运行并限频,仅用于检测区域切换

上方时间线中,追踪被拒时提前启动的全局搜索、全局搜索中投机执行的路径热力图粗扫,都来自这个 PR。

代码位于 MaaEnd 仓库 agent/cpp-algo/source/MapLocator。

MapLocator:终末地小地图定位

https://tail.lemonmiaow.xyz/posts/maplocator-design/
作者 Lemon_miaow
发布于 2026-10-02
许可协议 CC BY-NC-ND 4.0