Skip to content
目录

海图识别

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

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

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

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

sh
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 可以寻找数据中的峰值点,参考文档

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

python
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

sh
[ 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 暴力求解所有直线的最近点,得到重合点 的坐标:

sh
[-201.33197146  129.0958336]

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

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

python
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 函数,它可以根据透视,裁切出关于网格中心相对位置的图片,统一缩放到特定大小,这样就可以愉快地使用模板匹配了。

python
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=(100100))
    out.paste(image,tuple(np.array(loca) * 105))
out

crop_scale

海域信息解析

未完待续