# 海图识别

本文的视频版,方便理解:

[碧蓝航线]海图识别-从屏幕内容到海域信息(上) (opens new window)

[碧蓝航线]海图识别-从屏幕内容到海域信息(下) (opens new window)

海图识别 是一个碧蓝航线脚本的核心. 如果只是单纯地使用 模板匹配 (Template matching) 来进行索敌, 就不可避免地会出现 BOSS被小怪堵住 的情况. AzurLaneAutoScript 提供了一个更好的海图识别方法, 在 module.map 中, 你将可以得到完整的海域信息, 比如:

2020-03-10 22:09:03.830 | INFO |    A  B  C  D  E  F  G  H
2020-03-10 22:09:03.830 | INFO | 1 -- ++ 2E -- -- -- -- --
2020-03-10 22:09:03.830 | INFO | 2 -- ++ ++ MY -- -- 2E --
2020-03-10 22:09:03.830 | INFO | 3 == -- FL -- -- -- 2E MY
2020-03-10 22:09:03.830 | INFO | 4 -- == -- -- -- -- ++ ++
2020-03-10 22:09:03.830 | INFO | 5 -- -- -- 2E -- 2E ++ ++

module.map 主要由以下文件构成:

  • perspective.py 透视解析
  • grids.py 海域信息解析
  • camera.py 镜头移动
  • fleet.py 舰队移动
  • map.py 索敌逻辑

# 一点透视

在理解 AzurLaneAutoScript 是如何进行海图识别之前, 需要快速了解一下 一点透视 的基本原理. 碧蓝航线的海图是一个一点透视的网格, 解析海图的透视, 需要计算出灭点和距点.

在一点透视中:

  • 所有的水平线的透视仍为水平线.
  • 所有的垂直线相交于一点, 称为 灭点 . 灭点距离网格越远, 垂直线的透视越接近垂直.

vanish_point

  • 所有的对角线相交于一点, 称为 距点 . 距点离灭点越远, 网格越扁长. 距点和灭点在同一水平线上. 距点其实有两个, 它们关于灭点对称, 图中画出的是位于灭点左边的距点.

distant_point

# 截图预处理

preprocess

拿到一张截图之后, load_image 函数会进行以下处理

  • 裁切出用于可以用于识别的区域.
  • 去色, 这里使用了 Photoshop 里的去色算法, (MAX(R, G, B) + MIN(R, G, B)) // 2
  • 去UI, 这里使用 overlay.png .
  • 反相

(上面的图是反相前的结果, 反相后的图过于恐怖, 就不放了)

# 网格识别

# 网格线识别

网格线, 是一条 20% 透明度的黑色线, 在 720p 下, 有3至4像素粗. 在旧UI时, 只需要把图像上下左右移动一个像素, 再除以原图像, 便可以得到网格线. 新UI的海图格子增加了白色框, 白色框有透明度渐变, 增加了识别难度.

find_peaks 函数使用了 scipy.signal.find_peaks 来寻找网格线. scipy.signal.find_peaks 可以寻找数据中的峰值点 : https://docs.scipy.org/doc/scipy/reference/generated/scipy.signal.find_peaks.html

截取 height == 370 处图像, 使用以下参数:

FIND_PEAKS_PARAMETERS = {
    'height': (150, 255 - 40),
    'width': 2,
    'prominence': 10,
    'distance': 35,
}

find_peaks

可以看出, 有一些被遮挡的没有识别出来, 还有很多识别错误, 不过问题不大.

然后扫描每一行, 绘制出图像. (出于性能优化, 实际中会把图像展平至一维再识别, 这将缩短时间消耗至约 1/4.)

peaks

至此, 我们得到了四幅图像, 分别对应 垂直的网格内线 水平的网格内线 垂直的网格边线 水平的网格边线 . 这一过程在 I7-8700k 上需要花费约 0.13 s, 整个海图识别流程将花费约 0.15 s.

注意, 识别内线和边线所使用的参数是不一样的. 不同的地图, 应该使用对应的参数, 如果偷懒的话, 也可以使用默认参数, 默认参数是针对 7-2 的, 可以在第七章中使用, 甚至可以用到 北境序曲 D3 .

# 网格线拟合

hough_lines 函数使用了 cv2.HoughLines 来识别直线, 可以得到四组直线.

hough_lines_1

垂直的网格内线 为例, 可以看到, 识别结果有一些歪的线.

我们在图片中间拉一条水平线, 称为 MID_Y (如果要修正水平线, 就拉垂直线), 交于垂直线, 交点称为 mid , 如果 mid 之间的距离小于3, 就认为这些线是相近线, 并用他们的平均值代表他们. 这样就修正了结果.

# 灭点拟合

我们知道, 在一点透视中所有垂直线相交于灭点, 但是网格识别的结果是有误差的, 不能直接求直线的交点.

_vanish_point_value 函数用于计算, 某一点到所有垂直线的距离, 并用 scipy.optimize.brute 暴力解出离直线组最近的点, 它就是 灭点 . 这个曲面描绘了点到垂直线的距离和. 为了在求解是能大胆抛弃距离较远的线, 在求距离是加了 log 函数.

vanish_point_distance

得到灭点后, 还记得之前的 mid 吗, 将它们连接至灭点, 作为垂直线. 这是对结果的第二次修正.

# 距点拟合

将最初得到的垂直线和水平线相交, 得到交点. 我们知道距点和灭点在同一水平线上, 在这条水平线上取点, 将所有交点连接至这点, 得到斜线, _distant_point_value 函数将计算斜线的 mid 之间的距离, 同样使用 scipy.optimize.brute 暴力解出距离最小的点, 它就是 距点 .

如果将斜线绘制出来, 会有这样的图像, 虽然有很多错误的斜线, 但确实求出了正确的距点.

diatant_point_links

# 网格线清洗

经过以上步骤, 我们得到了以下网格线, 大体正确, 但是有错误.

mid_cleanse_before

取垂直线的 mid ,

[ 185.63733413  315.65944444  441.62998244  446.89313842  573.6301653
  686.40881027  701.20376316  830.27394123  959.00511191 1087.91874026
 1220.58809477]

因为每个格子都是等宽的, 所以 mid 理论上是一个等差数列, 但实际识别结果可能有错误的项, 也可能有缺失的项. 我们用一次函数表达这个关系 y = a * x + b. 由于错误和缺失, 这里的 x 不一定是项数 n , 但只要没有10个以上的错误或者缺失, 就会有 x ∈ [n - 10, n + 10] .

接下来, 把表达式改写为 b = -x * a + y , 其中 x ∈ [n - 10, n + 10] . 如果把a当作自变量, 把b当作因变量, 那么这是一组直线, 它有 11 * 21 条. 把它们描绘出来:

mid_cleanse_lines_with_circle

可以发现, 用橙色圈起来的地方有多条直线重合, 我们称为 重合点 (coincident_point). 那些错误的 mid 产生的直线无法与其他直线交于重合点, 自然被剔除.

使用 scipy.optimize.brute 暴力求解所有直线的最近点, 得到重合点 的坐标

[-201.33197146  129.0958336]

因此一次函数就是 y = 129.0958336 * x - 201.33197146 .

在计算点到直线的距离时, 使用了以下函数:

distance = 1 / (1 + np.exp(9 / distance) / distance)

这个函数将削弱距离较远的直线的影响, 鼓励优化器选择局部最优解.

mid_cleanse_function

如何处理水平线?

距点作任意一条直线, 与水平线相交. 将得到的交点与灭点连接, 就完成了水平线到垂直线的映射. 处理完再映射回水平线即可.

mid_cleanse_convert

最后, 以海图或者屏幕为边界生成 mid , 此时缺失的 mid 也得到了填充. 重新连接至灭点, 完成了垂直线的清洗.

绘制出网格识别的结果:

mid_cleanse_after

# 网格裁切

事实上, 海域中的舰娘, 敌人, 问号等, 都是固定在网格中心的图片, 只不过这些图片会因为透视产生缩放而已. 注意, 仅仅是缩放, 图片不会因为透视产生变形, 产生变形的只有地面的红框和黄框.

crop_basic

grid_predictor.py 中提供了 get_relative_image 函数, 它可以根据透视, 裁切出关于网格中心相对位置的图片, 统一缩放到特定大小, 这样就可以愉快地使用模板匹配了.

from PIL import Image
from module.config.config import cfg
i = Image.open(file)
grids = Grids(i, cfg)
out = Image.new('RGB', tuple((grids.shape + 1) * 105 - 5))
for loca, grid in grids.grids.items():
    image = grid.get_relative_image(
    	(-0.415 - 0.7, -0.62 - 0.7, -0.415, -0.62), output_shape=(100, 100))
    out.paste(image, tuple(np.array(loca) * 105))
out

crop_scale

# 海域信息解析

未完待续