perspective - LmeSzinc/AzurLaneAutoScript GitHub Wiki
海图识别
本文的视频版,方便理解:
海图识别
是一个碧蓝航线脚本的核心. 如果只是单纯地使用 模板匹配 (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
是如何进行海图识别之前, 需要快速了解一下 一点透视
的基本原理. 碧蓝航线的海图是一个一点透视的网格, 解析海图的透视, 需要计算出灭点和距点.
在一点透视中:
- 所有的水平线的透视仍为水平线.
- 所有的垂直线相交于一点, 称为
灭点
. 灭点距离网格越远, 垂直线的透视越接近垂直.
- 所有的对角线相交于一点, 称为
距点
. 距点离灭点越远, 网格越扁长. 距点和灭点在同一水平线上. 距点其实有两个, 它们关于灭点对称, 图中画出的是位于灭点左边的距点.
截图预处理
拿到一张截图之后, 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,
}
可以看出, 有一些被遮挡的没有识别出来, 还有很多识别错误, 不过问题不大.
然后扫描每一行, 绘制出图像. (出于性能优化, 实际中会把图像展平至一维再识别, 这将缩短时间消耗至约 1/4.)
至此, 我们得到了四幅图像, 分别对应 垂直的网格内线
水平的网格内线
垂直的网格边线
水平的网格边线
. 这一过程在 I7-8700k
上需要花费约 0.13 s, 整个海图识别流程将花费约 0.15 s.
注意, 识别内线和边线所使用的参数是不一样的. 不同的地图, 应该使用对应的参数, 如果偷懒的话, 也可以使用默认参数, 默认参数是针对 7-2
的, 可以在第七章中使用, 甚至可以用到 北境序曲 D3
.
网格线拟合
hough_lines
函数使用了 cv2.HoughLines
来识别直线, 可以得到四组直线.
以 垂直的网格内线
为例, 可以看到, 识别结果有一些歪的线.
我们在图片中间拉一条水平线, 称为 MID_Y
(如果要修正水平线, 就拉垂直线), 交于垂直线, 交点称为 mid
, 如果 mid
之间的距离小于3, 就认为这些线是相近线, 并用他们的平均值代表他们. 这样就修正了结果.
灭点拟合
我们知道, 在一点透视中所有垂直线相交于灭点, 但是网格识别的结果是有误差的, 不能直接求直线的交点.
_vanish_point_value
函数用于计算, 某一点到所有垂直线的距离, 并用 scipy.optimize.brute
暴力解出离直线组最近的点, 它就是 灭点
. 这个曲面描绘了点到垂直线的距离和. 为了在求解是能大胆抛弃距离较远的线, 在求距离是加了 log
函数.
得到灭点后, 还记得之前的 mid
吗, 将它们连接至灭点, 作为垂直线. 这是对结果的第二次修正.
距点拟合
将最初得到的垂直线和水平线相交, 得到交点. 我们知道距点和灭点在同一水平线上, 在这条水平线上取点, 将所有交点连接至这点, 得到斜线, _distant_point_value
函数将计算斜线的 mid
之间的距离, 同样使用 scipy.optimize.brute
暴力解出距离最小的点, 它就是 距点
.
如果将斜线绘制出来, 会有这样的图像, 虽然有很多错误的斜线, 但确实求出了正确的距点.
网格线清洗
经过以上步骤, 我们得到了以下网格线, 大体正确, 但是有错误.
取垂直线的 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 条. 把它们描绘出来:
可以发现, 用橙色圈起来的地方有多条直线重合, 我们称为 重合点
(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
, 此时缺失的 mid
也得到了填充. 重新连接至灭点, 完成了垂直线的清洗.
绘制出网格识别的结果:
网格裁切
事实上, 海域中的舰娘, 敌人, 问号等, 都是固定在网格中心的图片, 只不过这些图片会因为透视产生缩放而已. 注意, 仅仅是缩放, 图片不会因为透视产生变形, 产生变形的只有地面的红框和黄框.
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
海域信息解析
未完待续