97 KiB
开始 Alas 的应用场景 Alas 是为长时间运行,甚至是 7*24 运行而设计的,这一点与市面上多数针对手机的脚本不同。Alas 在早期已经放弃了对安卓真机的支持,同时也放弃了对除 1280x720 以外的分辨率的支持。
放弃安卓真机的原因是:
安卓机在长时间运行下,容易出现黑屏/假死的情况 部分安卓机型截图会压缩,早期代码中有不少基于颜色识别的方法 需要另外占用一台安卓机。由于 ocr 模型的存在,将 Alas 迁移至安卓存在困难 使用 1280x720 分辨率的原因是:
720p 在图像清晰度和截图耗时之间有较好的平衡,480p 下海图识别正确率明显下降,1080p 截图耗时几乎翻倍 异型屏没有统一的标准,适配非 16:9 分辨率带来的维护成本和获得的收益不符 基本运作模式 在低级的脚本中,往往充斥着这样的代码:
python click(XXXX) sleep(2) click(YYYY) sleep(3) 这样的代码稳定性很差,如果游戏卡顿,或者脚本需要对低配设备优化,就得延长等待的间隔,最后等待变得越来越长。很多时候,脚本慢,并不是因为截图慢,语言运行慢,而是因为开发者写了大量的固定时长的等待。
对于 Alas 而言,“快” 是最主要的目标。
针对快慢设备的兼容问题,Alas 使用了这样的运作模式,也希望开发者使用它,以减少对 sleep() 的依赖。这种模式在高配电脑上可以运行得很快,在低配电脑上也有很好的兼容性,它可以在点击失败时自动重试,我们也不再需要关心点击的执行顺序。
以进入地图为例:
python while 1: self.device.screenshot()
if self.appear_then_click(ENTRANCE):
continue
if self.appear_then_click(MAP_PREPARATION):
continue
if self.appear_then_click(FLEET_PREPARATION):
continue
# End
if self.handle_in_map_with_enemy_searching():
break
处理死循环 在上面示例代码中,如果陷入死循环,Alas 会抛出异常。
GameStuckError 无操作连续截图超过 1 分钟。战斗中和客户端启动中,将延长至 5 分钟。 GameTooManyClickError 最后 15 次操作中,有一项操作 >= 12 次,或有两项操作都 >= 6 次。 这两个异常只会在最顶层捕获。捕获后,Alas 会将 log 和最近截图保存在单独的文件夹,并处理其中可能会暴露用户身份的信息,包括混淆路径名称,遮挡游戏昵称等。处理完成后 Alas 停止。
GameStuckError
GameTooManyClickError sh 2023-06-10 17:14:48.651 | WARNING | Wait too long 2023-06-10 17:14:48.653 | WARNING | Waiting for {'FOO_BAR'} 2023-06-10 17:14:48.851 | INFO | [Package_name] com.bilibili.azurlane 2023-06-10 17:14:48.853 | ERROR | GameStuckError: Wait too long 2023-06-10 17:14:48.858 | WARNING | Saving error: ./log/error/1145141919810 性能优化 与一般认知不同,开发者在编写 Alas 时,不需要特别注意性能优化。因为在 Alas 运行时,超过 99% 的时间是在等待模拟器截图。
在配置过关的电脑上,截图耗时约 350 ms,而 Alas 处理只花费约 2.5 ms。在海图识别或者 OCR 时,Alas 耗时也不过 100-180 ms。
基于图片的 assets 管理 手动写坐标会给后期维护带来麻烦,因为没人知道这个坐标是在哪里。它也会大量占据开发者的时间,以至于脚本的规模受限。
python area = (790, 275, 911, 321) Alas 编写了一个简单粗暴的代码生成器 dev_tools/button_extract.py,来管理 assets ,它优势在:
打开图片即可方便地查看这个区域在哪里,以及这个区域所包含的内容,方便后期维护。 设置好 PhotoShop 动作后,制作一张 assets,比手动输入坐标快。 多服务器适配。 可以在 IDE 中使用自动补全。
OCR Alas 使用了 cnocr 作为 OCR 库,也针对碧蓝航线内的字体训练了两个 OCR 模型。许多脚本都迈不过 OCR 这道坎,需要依赖在线 OCR,但是在 Alas 里,你可以大量地调用 OCR。
以识别石油量为例:
python OCR_OIL = Digit(OCR_OIL, name='OCR_OIL', letter=(247, 247, 247), threshold=128) oil = OCR_OIL.ocr(self.device.image) 注释 使用 Google 注释规范,例如:
python """ Re-focus to the center of a grid.
Args: tolerance (float): 0 to 0.5. If None, use MAP_GRID_CENTER_TOLERANCE
Returns: bool: Map swiped. """ 同时增加 Pages,说明函数进出时的游戏界面,可以是 UI Page,也可以是 Button。(旧代码中可能缺少 Pages 注释)
python """ Pages: in: page_moewfficer out: MEOWFFICER_BUY """ 在注释中,应当全部使用简体中文。
尽量做到:
一个函数的注释占 1/3 ~ 1/2 一个函数不超过一个屏幕 一个 .py 文件不超过 500 行。
调试 Alas 的入口文件有两个:调度器 alas.py 和网页后端 gui.py。由于很多游戏玩法会消耗游戏帐号上的大量资源,或者消耗大量时间,甚至一些内容一天只能操作一次,调试的时候不能像平时使用一样执行完整流程。大多数时候调试都是脱离游戏,根据一张或者几张游戏截图进行的。为此,Alas 所有的模块都是可以独立运行的,不依赖 GUI 也不依赖用户配置。
调试一个 Button 假设 Alas 无法识别 SOS 模块中的 SIGNAL_LIST_CHECK。
那么你需要将游戏切换到 SOS 信号列表的界面,截图(使用模拟器的截图功能即可)。
在 module/sos/sos.py 文件的末尾添加这些并运行:
当然,你也可以新建一个文件,导入 SOS 模块和相关 assets,并在 if name == 'main': 下运行,这会更加严谨。不加 if name == 'main': 问题不大,因为 Alas 是单进程单线程的。
python az = CampaignSos('alas', task='Sos') az.image_file = r'xxxxx.png'
print(az.appear(SIGNAL_LIST_CHECK)) sh INFO | +---------------------------------------------+ INFO | | START | INFO | +---------------------------------------------+ INFO | [Server] cn read: ./config\alas.json read: ./module/config/argument/args.json INFO | Bind task Sos INFO | <<< DEVICE >>> INFO | [Adb_binary] adb.exe INFO | already connected to 127.0.0.1:59865 newCommandTimeout updated to 168h0m0s True 结果是 True,识别没有问题。同时调试过程中产生的 log 会单独保存,比如直接运行 test.py,log 将保存至 log/YYYY-MM-DD_test.log。
逐行讲解。
az = CampaignSos('alas', task='Sos') 创建 CampaignSos 对象。Alas 模块都继承自 ModuleBase 类,这是 ModuleBase 的定义。
python def init(self, config, device=None, task=None): config:用户配置名称,对应 config/{config}.json。也可以使用 template。 device:Device 对象,一般不需要输入,会根据 config 自动创建。 task:任务名称。对应任务的用户设置将绑定到变量上。一般来说,可以绑定为 Alas,也就是用户界面中的 Alas设置,包含模拟器 Serial,游戏包名等。 az.image_file = r'xxxxx.png' 读取本地文件,存入 Alas 的截图缓存中,免去从模拟器截图。简单粗暴却又实用,这是它的定义。
python @property def image_file(self): return self._image_file
@image_file.setter def image_file(self, value): """ 用于开发。 从本地文件系统加载图像并将其设置为 self.device.image。 测试一个图像,不需要从模拟器上进行截图。 """ if isinstance(value, np.ndarray): value = Image.fromarray(value) elif isinstance(value, str): value = Image.open(value).convert('RGB')
self.device.image = value
调试其他服务器 在导入任何 Alas 内容之前,切换服务器。
python import module.config.server as server server.server = 'en' 调试一个识别函数 假设我们正在重构剧情选项的识别,收集到了不同选项数量的图片,希望测试 _story_option_buttons() 是否都能正确识别。
python from module.statistics.utils import load_folder from module.handler.info_handler import InfoHandler
存放截图的目录
folder = r'xxxxx' az = InfoHandler('alas', task='Alas') for file in load_folder(folder).values(): az.image_file = file print(az._story_option_buttons()) sh [STORY_OPTION_1_OF_2, STORY_OPTION_2_OF_2] [STORY_OPTION_1_OF_2, STORY_OPTION_2_OF_2] [STORY_OPTION_1_OF_2, STORY_OPTION_2_OF_2] [STORY_OPTION_1_OF_1] [STORY_OPTION_1_OF_1] 调试一个方法 假设你想了解 Alas 的章节切换是如何工作的。
python from module.campaign.campaign_ui import CampaignUI
在运行之前,需要手动将游戏切换至主线章节界面。
az = CampaignUI('alas', task='Alas')
出于优化目的,Alas 许多方法会复用上一张截图,因此需要先截一张图。
az.device.screenshot()
切换至第 7 章。
az.campaign_ensure_chapter(index=7) 与大多数脚本需要先切换至第一章对齐,再切换至需要的章节不同,campaign_ensure_chapter 是基于 OCR 的,不会有多余的操作。你也可以故意搞事,比如手动点击反方向,观察 Alas 是如何纠错的。
在编写模块时,为测试提供方便 Alas 所有的模块都是可以独立运行的,不依赖 GUI 也不依赖用户配置,每个模块通常只有一个方法是依赖用户配置的,这一点在开发新模块的时候尤其需要注意。
以委托模块 module.commission 为例。在 commission.py 中,只有 run() 方法是给调度器使用的,其他方法例如收委托 commission_receive 和派委托 commission_start 都可以直接调用。不会因为某个选项没有开启,导致测试内容被跳过。
一些方法也会提供两份。例如,大世界中的 fleet_repair 必然会将舰队移动至港口并维修。handle_port_repair 则会根据用户配置和当前舰队的血量决定是否进行维修。
调试海图识别 python from PIL import Image
from module.config.config import AzurLaneConfig from module.map_detection.view import *
截图文件
file = r'xxxxx'
class Config: # 把地图文件中的 Config 粘贴到这里 pass
md = View(AzurLaneConfig('template').merge(Config())) image = np.array(Image.open(file).convert('RGB'))
如果 log 里有 homo_storage 的值,可以在这里载入。
sto = (...)
md.backend.load_homography(storage=sto)
md.load(image) md.predict() md.show() md.backend.draw()
工具类 这里介绍 module/base/utils.py 下的函数。
random_normal_distribution_int(a, b, n=3) 在区间 [a, b) 内产生符合正态分布的随机数,原理是取多个随机数的平均值来模拟正态分布,Alas 中绝大部分随机数由这个函数产生。n 值是随机数的数量,值越大分布越集中。
python import matplotlib.pyplot as plt data = [random_normal_distribution_int(0, 100) for _ in range (1000)] plt.hist(data, bins=50, edgecolor="black", alpha=0.7)
random_rectangle_point(area) 在区域内产生符合二维正态分布的随机点,通常在点击操作中使用。下图展示在 BATTLE_PREPARATION 上产生的随机点。(背景图片由 Photoshop 添加)
python fig = plt.figure(figsize=(11,4)) plt.axis([-10, 210, -10, 70]) data = np.array([random_rectangle_point((0, 0, 200, 60)) for _ in range (500)]) x, y = data.T plt.scatter(x,y,s=2, alpha=0.5)
random_rectangle_vector(vector, box, random_range=(0, 0, 0, 0), padding=15) 在区域按二维正态分布放置一个向量,通常在滑动操作中使用。
box 是放置向量的区域,random_range 是给滑动向量加的随机值,padding 是到边缘的最小距离。
random_line_segments(p1, p2, n, random_range=(0, 0, 0, 0)) 在两点之间插入中间值,通常在滑动操作中使用。
Area operations 区域操作的函数有:area_offset,area_pad,point_in_area,area_in_area,area_cross_area。
location 和 node相互转换:node2location,location2node。
关于 Alas 中的命名:
point:是含 2 个元素的 tuple。(x, y):指屏幕上的一个点。原点是屏幕左上角,x 方向沿屏幕水平向右递增,y 方向沿屏幕竖直向下递增。 area:是含 4 个元素的 tuple。四个元素的值是:(upper_left_x, upper_left_y, bottom_right_x, bottom_right_y)。指屏幕上的矩形区域。 location:是含 2 个元素的 tuple。(x, y):指游戏海域中的网格坐标,(0, 0) 是海图最左上角的格子,也就是 A1。 node:是 str。比如 E3,指游戏海域中的网格坐标。node 相比 location 更容易阅读,所以在逻辑编写和日志中一般使用 node,运行时使用 location。 crop(image, area) 裁切图片,image 需要是 numpy 数组,相当于 pillow 库中的 crop。当裁切区域超出图片大小时,显示为黑色,这点与 pillow 一致。以下两种裁切方式效果相同。裁切大量图片时,操作 numpy 数组会比较快。
python image = self.device.image.crop(area) python image = crop(np.array(self.device.image), area) get_color(image, area) 计算区域的平均颜色。
color_similarity(color1, color2) 计算两个颜色之间的差值。这里使用了 PhotoShop 中魔棒的容差的算法。先计算 RGB 的差值,容差等于最大正差值减最小负差值。使用容差而非 RGB 的简单相减,是为了方便在 PS 中查看图片颜色,同时也更符合人眼对颜色的感知。
python Tolerance = Max(Positive(difference_rgb)) + Max(- Negative(difference_rgb)) color_similar(color1, color2, threshold=10) 判断颜色是否相似,判断容差是否小于阈值。
color_similar_1d(image, color, threshold=10) 在一维数组上判断颜色是否相似。
color_similarity_2d(image, color) 计算二维数组上的颜色差值。注意,返回的 numpy 数组中,255 代表完全相等,数值越小颜色相差越大。在计算二维图片的颜色差值时,使用了 opencv 计算,速度是使用 numpy 的 3 倍以上。这个函数也是一个常用函数,一般用来进行简单的颜色计数。
python image
python Image.fromarray(color_similarity_2d(image, (255, 77, 82)))
python np.sum(color_similarity_2d(image, (255, 77, 82)) > 221) > 100 sh True extract_letters(image, letter=(255, 255, 255), threshold=128) 将含文字的图片转换为白底黑字的图片,threshold 越小,背景越白。除了 cnocr 的预训练的模型外,Alas 中的 ocr 模型都是使用浅色背景和深色字体训练的。所以需要使用 extract_letters 进行预处理。Ocr 类已经封装了这一步,你不需要进行额外的操作,只需要输入字体的颜色即可。
python image
python Image.fromarray(extract_letters(image, letter=(173, 247, 74), threshold=128))
extract_white_letters(image, threshold=128) 与 extract_letters 基本相同,但针对白色字体,若颜色不是黑白灰,输出的颜色会更浅。
red*overlay_transparency(color1, color2, red=247) 假设 color_2 是由 color_1 叠加一个半透明的红色色块得到的,即 color_2 = color_1 * (1 - alpha) + (red, 0, 0) _ alpha,然后计算红色色块的不透明度,Alas 根据不透明度是否超过阈值来判断空袭、伏击和索敌的动画. 当颜色较深时(40 以下)结果不准确。
MAP_AIR_RAID 空袭
python red_overlay_transparency( MAP_AIR_RAID.color, get_color(self.device.image, MAP_AIR_RAID.area) ) sh 0.0 0.5289717702159051 0.5291491967723716 0.5146182778247053 0.5292370071164753 0.0023695248896050164 MAP_AMBUSH 伏击
python red_overlay_transparency( MAP_AMBUSH.color, get_color(self.device.image, MAP_AMBUSH.area) ) sh 0.0 -0.16425414634481073 0.44761270077818777 0.4374642521684831 0.4481979863753585 0.444644697332672 0.06879890918910499 0.05134165622907348 0.05226963663451904 -0.007069039078106552 0.0006615745474228571 MAP_ENEMY_SEARCHING 索敌
python red_overlay_transparency( MAP_ENEMY_SEARCHING.color, get_color(self.device.image, MAP_ENEMY_SEARCHING.area ) sh 0.0 -0.17394681611846782 -0.1864791203625967 0.7091334931036758 0.7207363797991225 0.7653337334792214 0.40077538826295694 0.045037725366474775 -0.12609578098579324 color_bar_percentage(image, area, prev_color, reverse=False, starter=0, threshold=30) 计算进度条的百分比,可以计算纯色的、渐变色的、甚至是不连续的有遮挡的进度条。用于检测地图通关百分比,演习时敌我的血条,强化角色时的装填属性等。有一定的误差,满进度时,可能返回 0.99 或 0.98。执行耗时较长, 约 10ms,取决于长度和复杂度。
python self._calculate_hp(image, area=DEFENDER_HP_AREA.area, reverse=False) sh 0.7603833865814696
装饰器 这里介绍 module/base/decorators.py 下的装饰器。
@Config.when() 让一个函数在特定的设置情况下运行,常用于同一函数在不同服务器间的切换。需要实例拥有 config 属性,且为 AzurLaneConfig 对象。任何继承 ModuleBase 的类都可以使用这个装饰器。
当指定的属性值为 None 时,表示任何情况下均可运行。它的定义应后于其他函数,也就是这个函数要写在最下面。当 config 中的值不满足任何一个装饰器的要求时,显示 warning,并运行最后一个定义的函数.
python from module.base.decorator import Config from module.base.base import ModuleBase
class AnotherModule(ModuleBase): @Config.when(SERVER='en') def function(self): # 此方法将仅在 EN 服务器中调用 pass
@Config.when(SERVER=None)
def function(self):
# 此方法将在其他服务器中调用
pass
选择的属性不仅限于 SERVER,可以是 AzurLaneConfig 中的任何属性:
python
module/config/config.py
DEVICE_CONTROL_METHOD = 'uiautomator2' # ADB, uiautomator2, minitouch python @Config.when(DEVICE_CONTROL_METHOD='minitouch') def _commission_swipe(self, distance=190): pass
@Config.when(DEVICE_CONTROL_METHOD=None) def _commission_swipe(self, distance=300): pass 也可以选择一个或多个属性:
python @Config.when(POOR_MAP_DATA=True, MAP_CLEAR_ALL_THIS_TIME=False) def battle_function(self): pass
@Config.when(MAP_CLEAR_ALL_THIS_TIME=True) def battle_function(self): pass
@Config.when(MAP_CLEAR_ALL_THIS_TIME=False, POOR_MAP_DATA=False) def battle_function(self): pass @cached_property 缓存属性。用 cached_property 装饰的类属性,只会计算一次。来自 cached_property。
python @cached_property def bug_threshold(self): return random_normal_distribution_int(55, 105, n=2) python self.bug_threshold self.bug_threshold sh 87 87 重新计算属性:
python del self.dict['bug_threshold'] self.bug_threshold sh 74 @timer 打印函数运行的耗时,这个装饰器在 module/base/timer.py 里。精度约 0.5ms,在调试使用。
python @timer def do_something(): pass python do_something() do_something: 0.123456789 s @function_drop(rate=0.5, default=None) 随机执行或者不执行某个函数,可以在测试中模拟模拟器卡顿。
日志 这里介绍 module/logger.py。
格式 日志格式: %(asctime)s.%(msecs)03d | %(levelname)s | %(message)s。 时间格式: %Y-%m-%d %H:%M:%S。 例子: 2020-09-11 08:35:59.460 | INFO | XXXXXXXX。
Log 会打印在控制台上,也会写入到文件中,位置为 f'./log/{datetime.date.today()}_{pyw_name}.txt'
logger 支持基本的调用,比如 logger.info,logger.warning。此外,还有一些额外的函数,用来增强 log 文件的可读性。
关于日志 目前,Alas 的 log 需要人工阅读,为了 log 的可读性,请避免大量使用 logger.hr(),logger.warning(),感叹号 ( ! ) 等表示强调的字符。强调是一个相对的概念,我们之所以看到某个事物被强调了,并不是因为它本身被强调了,而是因为存在大量其他事物并没有被强调。如果你强调了所有的事物,那么相当于你没有强调任何一个事物。
logger.hr(title, level=0) 仅在脚本开始运行时使用。
sh 2020-01-01 00:00:00.000 | INFO | +---------------------------------------------+ 2020-01-01 00:00:00.000 | INFO | | TITLE | 2020-01-01 00:00:00.000 | INFO | +---------------------------------------------+ logger.hr(title, level=1) 表示开始执行 GUI 中的某个功能。
sh 2020-01-01 00:00:00.000 | INFO | ==================== TITLE ==================== logger.hr(title, level=2) 表示功能的某个阶段的开始。比如开始第一场战斗。
sh 2020-01-01 00:00:00.000 | INFO | -------------------- TITLE -------------------- logger.hr(title, level=3) 表示功能的某个细分阶段的开始。
sh 2020-01-01 00:00:00.000 | INFO | <<< TITLE >>> logger.attr(name, text) 用于打印属性值.
sh 2020-01-01 00:00:00.000 | INFO | [name] text logger.attr_align(name, text, front='', align=22) 用于打印属性值,有一定的格式。一般用在海图识别的属性打印中。
sh 2020-09-11 02:16:51.542 | INFO | vanishpoint: ( 635,-1676) 2020-09-11 02:16:51.543 | INFO | distant_point: (-2245,-1676) 2020-09-11 02:16:51.568 | INFO | 0.109s * Horizontal: 6 (6 inner,2 edge) 2020-09-11 02:16:51.568 | INFO | Edges: /_\ Vertical: 9 (9 inner,2 edge) 2020-09-11 02:16:51.617 | INFO | tile_center: 0.955 (good match) 2020-09-11 02:16:51.627 | INFO | 0.058s _ edgelines: 3 hori,2 vert 2020-09-11 02:16:51.627 | INFO | Edges: /*\ homo_loca: ( 24, 54) 2020-09-11 02:16:51.630 | INFO | center_loca: (3,2) 2020-09-11 02:16:51.630 | INFO | camera_corrected: A1 -> D3 2020-09-11 02:16:51.630 | INFO | Camera: D3 Misc 如果在 Alas 的子目录中运行任意文件,但该文件导入了 logger,就会将运行目录切换至 Alas 的根目录。这样在调试或者运行 dev_tools 的时候,就不会因为忘记切换目录而导致找不到模块。
异常 介绍 module/exception.py 中的异常。
CampaignEnd 关卡战斗结束
抛出
BOSS Clear. BOSS 战斗结束。 In stage. 战斗结束后退出至章节界面。 Image is in stage 对章节界面执行海图识别。 Withdraw 关卡撤退。 捕获
结束当前关卡出击。 MapDetectionError 海域地图识别错误
抛出
Vanish point and distant point too close 仅海图识别方法为 perspective 时,拟合得到的灭点和距点距离太近,拟合结果对于一点透视的计算无意义。通常是因为网格线太少,导致拟合出错,此时需要调整识别的参数。 No data feed to load_homography, please input at least one. 仅海图识别方法为 homography 时,未输入任何初始化透视参数。 Failed to find a free tile 仅海图识别方法为 homography 时,无法找到定位点。通常是因为识别的图像并不是海域地图的图像,或者地图地面的杂物较多影响识别。 Camera outside map: offset=({x}, {y}) 海图识别时,镜头(画面中心)在地图外。(x, y) 是超出地图边界的距离。 捕获
若信息为 Camera outside map: offset=({x}, {y}),将镜头重新对准至最近的海域网格内,并重新识别。 MapWalkError 无法移动至目标点。目标点超出舰队的移动距离,或者通往目标点的道路被敌舰阻挡。
抛出
walk_out_of_step 在舰队移动时,在第一个消息框(INFO_BAR_1)出现 移动 二字。 捕获
捕捉到第一次 MapWalkError 时,尝试降低舰队步长(FLEET_STEP)至 1,也就是一格一格地移动。若降低舰队步长后仍然捕捉到 MapWalkError,通常是因为敌人信息丢失,舰队位置错误,镜头位置错误。直接设置舰队步长为 1,一般不能解决问题。 MapEnemyMoved 敌人已经移动,需要重新进行地图扫描。
抛出
计算移动回合,若当前回合后精英敌人会移动,对已移动敌人的进行扫描,并抛出。玩家舰队移动 2 次或 3 次后,精英敌人移动。具体是 2 次还是 3 次,根据敌人不同而不同,可以查看 expedition_data_template.lua 中的 ai_mov 属性。若使用 map_extractor.py 生成地图文件,将自动提取至 MOVABLE_ENEMY_TURN。 捕获
捕捉后,重新执行当前 battle_function,上限 10 次。超出次数后,抛出 ScriptError('No combat executed.') CampaignNameError 无法识别关卡名称。
抛出
Stage not found: {name} 无法在当前界面找到所请求的关卡名称。 无法在当前界面找到所请求的章节序号。 捕获
捕捉后重新识别,上限 20 次。超出次数后,抛出 ScriptEnd('Campaign name error') ScriptError 发生脚本内部错误,且无法处理。若开启 ENABLE_EXCEPTION,撤退,不抛出异常。
抛出
No combat executed. 未攻击敌人。当前 battle_function 执行后未攻击任何一个敌人。 Battle function exhausted. battle_function 耗尽。执行了超过 20 次 battle_function,尝试攻击了超过 20 个敌人。 {key} filter switch object does not exist in module/retire/dock.py 船坞筛选条件未在 module/retire/dock.py 中定义。 No book found. 在战术学院,检测不到任何一本技能书。 Login failed more than 3 游戏登录失败次数超过 3。 'No ocr-tool found, please install tesseract by yourself and make sure to set correct env vars.' 仅日服,未安装 tesseract。 No jpn found in tesseract langs, please install japanese data files. 仅日服,tesseract 中未安装 jpn 训练数据。 ScriptEnd 脚本运行结束。
抛出
Reach condition: {self.config.STOP_IF_MAP_REACH} 触发用户设置中的停止条件。 Campaign name error 无法识别关卡名称,无法进入关卡。 捕获
Script end 停止脚本的运行。 GameStuckError 游戏卡死。
抛出
Wait too long 等待时间过长。超过 60 秒和 60 张截图后无任何操作,或在战斗中超过 300 秒和 300 张截图后无任何操作,抛出。(若识别对象包含BATTLE_STATUS_S 或 PAUSE,认为在战斗中。) Triggered commission list flashing bug 执行委托时,失败次数超过 3,可能触发游戏内的委托列表闪烁 bug,导致无法委托开始。 捕获
仅在使用 GUI 运行时,重启游戏。在重启时,若再次发生 GameStuckError,不再捕捉,通常是因为服务器在维护,或者网络连接已断开。 GameTooManyClickError 点击游戏内同一按钮, 或者执行相同滑动的次数过多。
抛出
Too many click for a button: {button} 点击或滑动某个按钮次数过多。若在最后 15 次点击中,包含 12 次当前点击,则抛出。 捕获
仅在登录时,重启游戏,最大次数 3。超过次数后,抛出 ScriptError('Login failed more than 3') GameNotRunningError 游戏未运行。
抛出
Game not running 在无法识别的界面启动 Alas,且游戏未运行。 捕获
仅在使用 GUI 运行时,重启游戏。 AscreencapError 调用 ascreencap 发生错误。
抛出
Repositioning byte pointer failed, corrupted aScreenCap data received 无法定位 ascreencap 数据中的图片。 aScreenCap header verification failure, corrupted image received. 接收到的数据有误。 捕获
重新安装 ascreencap。 Exit without error Alas 停止运行,不报错。通常是用户设置有误或者使用方式不对。
sh Not supported screen size: {width}x{height} Alas requires 1280x720 不支持的分辨率,Alas 需要在 1280x720 下运行。
sh Received a pure black screenshot Color: {color} 截图为纯黑色。通常是设备处于锁屏状态,或者当前模拟器不支持当前截图方式。
sh Map file not found: campaign.{folder}.{name} 未找到地图文件。通常是用户出击未适配的地图,或者运行目录有误。
sh Unable to goto page_main Starting from current page is not supported Supported page: {[str(page) for page in self.ui_pages]} Supported page: Any page with a "HOME" button on the upper-right 无法前往游戏主界面,不支持从当前游戏界面启动。Alas 可以自动切换到需要的游戏界面, 但是只允许在这些界面下启动:主界面,出击,编队,演习,每日,活动,SP 活动,任务领取。共斗活动。Alas 也可以在右上角有 “一键回港” 按钮的界面下启动。游戏中大部分界面都有这个按钮,除了主界面本身,后宅,指挥喵。
sh UI route too long 寻找到的游戏界面切换路径过长。防止自动切换游戏界面时进入死循环,快速占满内存。已经修复。
sh Unknown raid mode: {mode} 不支持的共斗活动难度。
sh Unable to connect %s' % serial ADB 无法连接至该模拟器。
sh No ship retired, exit This may happens because wrong options of one click retirement in game 仅使用一键退役时,未退役任何船只,通常是游戏内的一键退役设置有误。
sh No ship retired, exit This may happens because some filters are set in dock 仅使用传统退役时,未退役任何船只,通常是在游戏内的船坞设置了别的筛选条件。
sh Frame body does not strt with JPEG header 仅使用 minicap 截图时,接收到的数据不是 JPEG 图像。
sh Mob fleet [{self.FLEET_1}] and boss fleet [{self.FLEET_2}] is the same They should to be set to different fleets 将道中队和 BOSS 队设置为同一队。企图使用骚操作绕过 Alas 必须使用两队的限制,最终也会在 BOSS 出现时报错,所以在保存设置时增加了检查。
sh You should use 2 fleets from chapter 7 to 13 Current: mob fleet [{self.FLEET_1}], boss fleet [{self.FLEET_2}] 在 7 到 13 章需要使用两队。解释同上。
sh Ocr model not prepared: {model_dir} 未找到 OCR 模型,或 OCR 模型不符合要求。
sh Enemy detection template not found: {name} 未找到精英敌人的识别模板,通常是开发者适配地图时有遗漏,或者命名有误。
sh No suitable version of aScreenCap lib is available Please use ADB or uiautomator2 screenshot instead acreencap 不支持这个模拟器或这个安卓版本,请使用 ADB 或 uiautomator2 截图。
用于识别的类 这里介绍 Alas 中用于识别的类。
Button 在 Alas 中,用于识别的图片称为 Assets,经过 button_extract 提取后,得到 Button 对象。
Button.appear_on(self, image, threshold=10) 判断在图片上是否出现当前 button,使用平均颜色识别。
Button.match(self, image, offset=30, threshold=0.85) 判断在图片上是否出现当前 button,使用模板匹配识别。offset 表示搜索的范围,为整数时上下搜索,为 tuple 时在四周搜索。匹配成功后会设置 _button_offset,表示识别的区域相对原始区域的偏移,将影响未来的点击区域。
首次调用时,会重新读取 assets 文件并缓存。
Button.button 产生一个在点击区域内的随机点。原始的点击区域加上 _button_offset 得到当前的点击范围。碧蓝航线曾经有过 确认 和 取消 按钮的上下抖动,_button_offset 可以自动处理这个问题。
Button.load_color(self, image) 重新从截图中载入 button 的颜色和图像。只在伏击空袭的识别中使用。
添加一个 Button Button 对象按模块保存于 ./asset 目录下,按钮定义于每个模块的 asset.py 文件中。比如 BATTLE_PREPARATION 的 Assets 图片是这样的:
在 assets.py 中,它是这样的:
python BATTLE_PREPARATION = Button(area=(1043, 607, 1241, 667), color=(234, 179, 97), button=(1043, 607, 1241, 667), file='./assets/combat/BATTLE_PREPARATION.png') area 是识别的区域,color 是平均颜色,button 是出现后的点击区域,file 是 assets 文件的位置。
注意,所有的 asset.py 都是由 button_extract 生成的,不要手动去修改它。
假设我们希望添加一个 确定 按钮,它出现于潜艇信号扫描时。
截图,使用模拟器自带的截图工具截图,并保证分辨率是 1280x720
将图片复制到 ./asset 下相应的目录中,更改文件名,比如 SEARCH_CONFIRM.png
拖动至 Photoshop 中打开
使用选区工具框选按钮区域
播放动作
直接点击 播放动作 的按钮,自动执行图片处理的操作。处理完的图片是这样的:
下面以 Photoshop CS6 为例。当然,你也可以使用别的软件来进行下面的操作,能把剩下的区域涂黑就行。使用 PS 只是因为它有录制动作的功能,会更快一些。
在第一次操作时,需要按照以下步骤添加动作。
在菜单栏的 窗口 中,点击 动作 ,弹出动作窗口。
在添加动作之前,最好备份当前图片,因为接下来需要记录的操作是不可逆的。
在动作窗口中,点击新建动作的图标,按照自己的喜好命名,比如 button_image。点击 记录 ,注意灰色的圆圈变红了,这表示动作录制开始了。 在图片区域单击鼠标右键,点击 选择反向 。 在菜单栏的 编辑 中,点击 填充 。 在弹出的填充选项窗口中,填充内容使用 黑色,填充模式选择 正常,不透明度选择 100,点击 确定。 在菜单栏的 文件 中,点击 保存。 在菜单栏的 文件 中,点击 关闭。 在动作窗口中,单击停止录制的图标,此时动作录制停止。 录制完成后,会得到动作如下,以后就可以直接使用,不需要手动操作了。
(可选) 添加属性覆盖图片 一个按钮具有三个属性:
area,按钮识别的区域。 color,按钮的颜色。 button,按钮出现后的点击区域。 假如添在同一目录下放置图片文件 SEARCH_CONFIRM.BUTTON.png ,并按照刚才描述的方法处理图片。那么这张图片的 button 属性将覆盖 SEARCH_CONFIRM.png 的 button 属性。
这是一个非常有用的特性,因为脚本通常需要判断截图中出现的元素,然后点击按钮,需要判断的地方和需要点击的地方可能不出于同一位置。
运行 button_extract sh python -m dev_tools.button_extract ButtonGrid 生成 Button 的二维阵列。
origin 是最左上角 button 的坐标,delta 是每个 button 移动的距离,button_shape 是每个 button 的大小,grid_shape 是网格的大小。
ButtonGrid.buttons(self) 将网格展平为 list。
Button.getitem(self, item) 获取某个位置的 Button。
Template 模板图片。Template 需要以 TEMPLATE_ 开头,不需要像处理 Button 一样处理,直接裁切即可,但同样需要运行 button_extract。首次调用时,会重新读取 assets 文件并缓存。
Template 可以是 GIF 图片,GIF 中的每一帧都会用于匹配。
Template.match(self, image, similarity=0.85) 模板匹配。
Template.match_result(self, image) 模板匹配。返回相似度和最相似点。
Template.match_multi(self, image, similarity=0.85) 多点模板匹配,自动合并相邻的点。返回一个 list 的 Button 对象。
添加用于敌人识别的模板图片 首先,我们不能直接裁切截图来制作模板图片,因为地图中的物体是有透视的,而模板匹配是对图片的缩放敏感的。我们需要使用 dev_tools/relative_crop.py 来获取图片。其中的 get_relative_image 可以根据透视裁剪出相对位置的图片,并放大到固定的大小。
下图展示了 self.get_relative_image((-1, -1, 1, 0), output_shape=(120, 60)) 的裁切区域。
编辑 dev_tools/relative_crop.py,粘贴地图文件中的设置 python class Config: """ 将地图文件的配置粘贴到这里 """ pass 修改保存目录和截图文件路径 python
保存临时图像的文件夹
folder = './screenshots/temp/'
把截图放在这里
file = './screenshots/TEMPLATEAMBUSH_EVADE_FAILED.png' 运行 relative_crop python python -m dev_tools.relative_crop 在保存目录里找到对应格子的图片,在图片中裁切出需要的模板。将模板图片放置于 assets//template 目录下,文件名需以 TEMPLATE 开头。
运行 button_extract
添加识别动态敌人的 GIF 模板图片 在活动永夜幻光(event_20200723_cn)中,游戏里的精英敌人身上会覆盖一层动态的黑色烟雾,影响常规模板匹配,因此添加了对 GIF 模板的支持。原理是先大量截图并裁切,再去重,将剩下的图片储存为 GIF,用 GIF 中的帧当作多个模板去匹配一个敌人。即便没有黑雾,使用多模板匹配,也能提高识别正确率。
进入关卡,找到精英敌人。
编辑 dev_tools/relative_record.py,粘贴地图文件中的设置
python class Config: """ 将地图文件的配置粘贴到这里 """ pass 修改设置 python CONFIG = 'alas' # 要加载的配置文件。 FOLDER = '' # 要保存的文件夹。 NAME = 'Deutschland' # 塞壬名称, 图片将保存在 / 中 NODE = 'D5' # 要裁剪的当前视角下的格子,不是在整个地图中的格子。 运行 dev_tools/relative_record.py 这会执行 300 次截图,然后使用 get_relative_image 函数裁切出特定格子中的塞壬图像。得到第一张截图时,会弹出裁切的预览,需要人工确认是否裁切到了正确的格子。如果裁到别的格子上,要停止运行,修改 NODE,再重新运行。
运行 dev_tools/relative_record_gif.py 暴力搜索帧数最少的 gif 模板,生成在 {FOLDER}/{NAME}_gif 。然后手动挑选帧数较少且能反应主要特征的 gif。如果有符合要求的,跳转至第 9 步,否则继续第 6 步,改用人工优化。
观察精英敌人 用 PhotoShop 打开 / 文件夹下的第一张图片,在游戏中观察精英敌人的上下呼吸运动。通常小人的身体是上下运动的,四肢是旋转的。模板匹配对缩放和旋转的识别很差,所以需要找到小人身上的不旋转的区域来裁切出模板,这个区域称为 AREA。AREA 选择时的一些经验:
不要选择小人的面部(容易误判)或眼睛(小人会眨眼睛)。 不要包含小人身后的海面(海面颜色会变化)。 AREA 的长宽都应大于 15 像素(减少误判)。 可以接受部分图像是旋转的。 例如在塞壬航母的中,选择了身体作为模板。(左:300 张截图中的前 30 张,右:提取的 GIF 模板)
编辑 dev_tools/relative_record_gif.py,修改设置 python
FOLDER = ''
NAME = 'Deutschland'
AREA = (32, 32, 54, 52) # 要裁剪的区域,选择一个东西不会旋转太多的区域。 THRESHOLD = 0.92 # 如果模板和现有模板之间的相似度大于 THRESHOLD,这个模板将会被删除。实际检测中的阈值是 0.85,为了更高的准确度,这里的阈值应该高于 0.85。 FOLDER 和 NAME 从 relative_crop 中导入,一般不需要修改。
运行 dev_tools/relative_record_gif.py。运行时会打印哪一张截图产生了新的模板,在 300 张截图中得到的模板数量应小于 5。产生的模板数量越少越好,过多的模板会拖慢识别速度,也意味着裁切区域不对。此时需要重复第 5 步至第 7 步,人工优化 AREA。
使用新的模板。将新的 GIF 模板复制到 ./assets//template 文件夹中,然后运行 button_extract。在地图文件的设置中开启塞壬识别,并设置模板名称。
python MAP_HAS_SIREN = True MAP_SIREN_TEMPLATE = ['U101', 'U73', 'U552'] U101 是第 3 步中的 NAME,表示使用 TEMPLATE_U101.gif 或 TEMPLATE_U101.png。它是大小写敏感的,如果在运行时找不到模板文件,会提示 Enemy detection template not found: {name}。
游戏内容识别 self.appear(self, button, offset=0, interval=0, threshold=None) 判断 button 是否出现在画面中
offset
button 的偏移量。假设 button.area=(100, 200, 300, 400),offset=(30, 20),那么 Alas 将在 (100-30, 200-20, 300+30, 400+30) 的区域内搜索按钮。
offset=None 时,使用平均颜色识别 Button.appear_on()
设置 offset 后,使用模板匹配识别 Template.match()
interval
按钮出现间隔。按钮出现后的若干秒内,对这个按钮的识别返回 False。一般设置 2 或 3 秒,能避免连击。
self.appear_then_click() 判断 button 是否出现在画面中,出现了就点击。
appear_then_click() 的本质是:
python if self.appear(button): self.device.click(button) 它们的连续调用会带来一个非常方便的特性:在设置了 offset 的情况下,即便游戏内的按钮相对 asset 图片有所移动,Alas 也能点击被移动后的按钮。这会降低 assets 维护成本,避免把时间浪费在应对游戏 UI 的微调上。它的原理是,上面已经提到过的 Button.match() 在匹配成功时会设置 _button_offset,而 self.device.click() 也会使用 _button_offset。
image_color_count(self, button, color, threshold=221, count=50) 判断颜色相似的像素的个数。有时候土办法会意外地好用。
其他方法 python wait_until_appear() wait_until_appear_then_click() wait_until_disappear() wait_until_stable() 已经较少使用。
不要把 wait_until_stable() 当作 sleep() 使用,建议优化逻辑以减少对等待的依赖。
操作游戏界面的类和方法 这里介绍操作游戏 UI 的类和方法。
Switch 操作游戏内的开关。使用场景:三种阵型的切换,周回模式开关等。
以切换普通图困难图为例:
python
创建开关
MODE_SWITCH_1 = Switch('Mode_switch_1')
定义状态
MODE_SWITCH_1.add_status('normal', SWITCH_1_NORMAL, sleep=STAGE_SHOWN_WAIT) MODE_SWITCH_1.add_status('hard', SWITCH_1_HARD, sleep=STAGE_SHOWN_WAIT)
class Test(ModuleBase): def test(self): # 判断开关是否出现在画面上 MODE_SWITCH_1.appear(main=self) # 获取当前状态 status = MODE_SWITCH_1.get(main=self) # 切换至某个状态 MODE_SWITCH_1.set('normal', main=self) Scroll 操作游戏内的滚动条。
以委托列表滚动条为例:
python
创建滚动条
COMMISSION_SCROLL = Scroll(COMMISSION_SCROLL_AREA, color=(247, 211, 66), name='COMMISSION_SCROLL')
class Test(ModuleBase): def test(self): # 判断是否有滚动条 COMMISSION_SCROLL.appear(main=self) # 获取当前位置 posi = COMMISSION_SCROLL.cal_position(main=self) # 判断是否在顶端或底端 COMMISSION_SCROLL.at_top(main=self) COMMISSION_SCROLL.at_bottom(main=self) # 拖拽至指定位置 COMMISSION_SCROLL.set(0.5, main=self) # 拖拽至顶端或底端 # 与直接拖拽至指定位置的区别是,这两个会拖出头,以保证滚动条到达端部 COMMISSION_SCROLL.set_top(main=self) COMMISSION_SCROLL.set_bottom(main=self) # 翻页,往下翻0.5页,翻页只是大致准确 COMMISSION_SCROLL.drag_page(0.5, main=self) # 往上或往下翻一页,由于翻页只是大致准确,默认翻的是0.8页,以防止漏掉一些内容 COMMISSION_SCROLL.prev_page(main=self) COMMISSION_SCROLL.next_page(main=self) NavBar 操作游戏内的标签页。
以建造页面的左侧边栏为例:
python
每个标签的 Button 阵列
gacha_side_navbar = ButtonGrid( origin=(21, 126), delta=(0, 98), button_shape=(60, 80), grid_shape=(1, 5), name='GACHA_SIDE_NAVBAR')
创建标签页
GACHA = Navbar(grids=gacha_side_navbar, active_color=(247, 255, 173), inactive_color=(140, 162, 181))
class Test(ModuleBase): def test(self): # 获取标签页的序号 current = GACHA.get_active(main=self) # 获取标签实际出现的个数 total = GACHA.get_total(main=self) # 切换至指定的标签页,上下左右设置一个即可 GACHA.set(main=self, left=None, right=None, upper=None, bottom=None) # 切换至从下往上数的第二个标签 GACHA.set(main=self, bottom=2) Page 游戏界面类。
以收获界面为例:
python
创建 Page 对象,使用的 check_button 需要是唯一的
如果画面中出现了 REWARD_CHECK,Alas 就认为现在在 page_reward 界面中
page_reward = Page(REWARD_CHECK)
定义界面切换
点击什么按钮,会到达什么界面
page_reward.link(button=REWARD_GOTO_MAIN, destination=page_main) page_main.link(button=MAIN_GOTO_REWARD, destination=page_reward) 界面切换 Alas 的游戏界面切换是自动的,带有寻路的。Alas 可以在绝大多数的游戏界面下启动,并沿着最短路径切换到它需要的游戏界面,与许多脚本都需要在主界面中启动不同。
所有操作游戏界面的方法都包含重试。
ui_click() 点击 UI 上的按钮,切换至下一界面。相当于是封装了:
python while 1: self.device.screenshot() if self.appear(appear_button): self.device.click(click_button) if self.appear(check_button): break appear_button:如果出现了这个按钮,就点击 click_button click_button:需要点击的按钮,为设置时,等于 appear_button check_button:如果出现了这个按钮,认为界面切换完成 additional:点击可能会出现的按钮,例如弹窗确认 ui_get_current_page() 获取当前界面。self.ui_current 即是当前界面。
Alas 启动时会调用这个方法,如果游戏未启动,抛出 GameNotRunningError,被顶层捕获后,启动游戏并继续。
ui_goto() 沿最短路径切换到指定界面,即便歪去别的界面也能自动纠正。
ui_ensure() 相当于 ui_get_current_page() + ui_goto()
ui_ensure_index() 地图章节翻页,包含重试以防止没翻到或者翻过头。使用场景:设置地图章节,选择队伍,设置猫箱购买数量等。
index:目标数字 letter:当前数字,可以是获取当前数字的方法,也可以是 OCR 类 prev_button:往前的按钮 next_button:往后的按钮 fast:True 则一次性点完,不对再重试。False 则点一下识别一下。False 用在队伍选择上,因为如果少了一队,数字就不连续了。 ui_goto_main() 前往主界面
ui_back() 点返回箭头
ui_additional() 处理各种可能在界面切换期间出现的弹窗。
OCR 这里介绍 OCR (光学字符识别)。
Alas 使用了 cnocr 作为 OCR 库,也针对碧蓝航线内的字体训练了两个 OCR 模型。许多脚本都迈不过 OCR 这道坎,需要依赖在线 OCR,但是在 Alas 里,你可以大量地调用 OCR。
需要注意的是 OCR 是无法达到 100% 正确率的,在调用时需要注意异常处理。
预训练模型 Alas 里有 3 个 OCR 模型:
cnocr:默认模型,支持中英文 python
Folder: ./bin/cnocr_models/cnocr
Size: 9.51MB
Model: densenet-lite-gru
Epoch: 39
Validation accuracy: 99.04%
Font: Various
Charset: Number, English character, Chinese character, symbols,
_num_classes: 6426
azur_lane:针对碧蓝航线数字和字母,仿照 cnocr 默认模型训练 python
Folder: ./bin/cnocr_models/azur_lane
Size: 3.25MB
Model: densenet-lite-gru
Epoch: 15
Validation accuracy: 99.43%
Font: Impact, AgencyFB-Regular, MStiffHeiHK-UltraBold
Charset: 0123456789ABCDEFGHIJKLMNPQRSTUVWXYZ:/- (Letter 'O' and is not included)
_num_classes: 39
jp:针对日文,同样仿照 cnocr 默认模型训练 python
Folder: ./bin/cnocr_models/jp
Size: 6.35MB
具体信息忘了(逃 对 cnocr 的修改 Alas 对 cnocr 进行了简单的修改,module/ocr/al_ocr.py 覆写了一些 cnocr 的方法:
_assert_and_prepare_model_files
取消了自动下载默认模型
_preprocess_img_array
图片预处理改成纯 opencv 实现,提高速度
init
模型懒加载
_gen_line_pred_chars
去除了准确率在 0.5 以下的字符输出
debug
增加了一个展示预处理后的图片的方法
Ocr 通用的 OCR 类
以关卡名称 OCR 为例:
python
创建 Ocr 对象
可以是个 button,也可以是一个 list 的 button
ocr = Ocr(buttons, name='campaign', letter=(255, 255, 255), threshold=128, alphabet='0123456789ABCDEFGHIJKLMNPQRSTUVWXYZ-')
获取识别结果
result = ocr.ocr(image) Digit 识别数字。返回 int
如果识别结果不能转换为 int,返回 0。
DigitCounter 识别数字计数,例如 14/15,返回 (14, 1, 15)。
如果识别结果不符合 {x}/{y} 的格式,返回 (0, 0, 0)。
Duration 识别时长,例如 08:00:00,返回 datetime.timedelta 对象。
如果识别结果不符合 {h}:{m}:{s} 的格式,返回时长为 0 的 datetime.timedelta 对象。
OCR 这里介绍 OCR (光学字符识别)。
Alas 使用了 cnocr 作为 OCR 库,也针对碧蓝航线内的字体训练了两个 OCR 模型。许多脚本都迈不过 OCR 这道坎,需要依赖在线 OCR,但是在 Alas 里,你可以大量地调用 OCR。
需要注意的是 OCR 是无法达到 100% 正确率的,在调用时需要注意异常处理。
预训练模型 Alas 里有 3 个 OCR 模型:
cnocr:默认模型,支持中英文 python
Folder: ./bin/cnocr_models/cnocr
Size: 9.51MB
Model: densenet-lite-gru
Epoch: 39
Validation accuracy: 99.04%
Font: Various
Charset: Number, English character, Chinese character, symbols,
_num_classes: 6426
azur_lane:针对碧蓝航线数字和字母,仿照 cnocr 默认模型训练 python
Folder: ./bin/cnocr_models/azur_lane
Size: 3.25MB
Model: densenet-lite-gru
Epoch: 15
Validation accuracy: 99.43%
Font: Impact, AgencyFB-Regular, MStiffHeiHK-UltraBold
Charset: 0123456789ABCDEFGHIJKLMNPQRSTUVWXYZ:/- (Letter 'O' and is not included)
_num_classes: 39
jp:针对日文,同样仿照 cnocr 默认模型训练 python
Folder: ./bin/cnocr_models/jp
Size: 6.35MB
具体信息忘了(逃 对 cnocr 的修改 Alas 对 cnocr 进行了简单的修改,module/ocr/al_ocr.py 覆写了一些 cnocr 的方法:
_assert_and_prepare_model_files
取消了自动下载默认模型
_preprocess_img_array
图片预处理改成纯 opencv 实现,提高速度
init
模型懒加载
_gen_line_pred_chars
去除了准确率在 0.5 以下的字符输出
debug
增加了一个展示预处理后的图片的方法
Ocr 通用的 OCR 类
以关卡名称 OCR 为例:
python
创建 Ocr 对象
可以是个 button,也可以是一个 list 的 button
ocr = Ocr(buttons, name='campaign', letter=(255, 255, 255), threshold=128, alphabet='0123456789ABCDEFGHIJKLMNPQRSTUVWXYZ-')
获取识别结果
result = ocr.ocr(image) Digit 识别数字。返回 int
如果识别结果不能转换为 int,返回 0。
DigitCounter 识别数字计数,例如 14/15,返回 (14, 1, 15)。
如果识别结果不符合 {x}/{y} 的格式,返回 (0, 0, 0)。
Duration 识别时长,例如 08:00:00,返回 datetime.timedelta 对象。
如果识别结果不符合 {h}:{m}:{s} 的格式,返回时长为 0 的 datetime.timedelta 对象。
4.4 State Loop 在低级的脚本中,往往充斥着 “点击-等待” 模式的代码:
python click(XXXX) sleep(2) click(YYYY) sleep(3) 这样的代码稳定性很差,如果游戏卡顿,或者脚本需要对低配设备优化,就得延长等待的间隔,最后等待变得越来越长。很多时候,脚本慢,并不是因为截图慢,语言运行慢,而是因为开发者写了大量的固定时长的等待。
在 Alas 内使用 “点击-等待” 模式是禁止的,所有对游戏的操作都必须使用 “状态循环” 。
针对快慢设备的兼容问题,Alas 使用了这样的运作模式,也希望开发者使用它,以减少对 sleep() 的依赖。这种模式在高配电脑上可以运行得很快,在低配电脑上也有很好的兼容性,它可以在点击失败时自动重试,我们也不再需要关心点击的执行顺序。
Alas状态机 Alas 状态机并没有一个正式的称呼,它是在多年的游戏脚本实践中逐渐形成的代码结构,你可以叫它 “状态循环” 也可以叫它 “Alas状态机”。Alas状态机写出来像这样:
python while 1: self.device.screenshot()
if self.appear_then_click(ENTRANCE):
continue
if self.appear_then_click(MAP_PREPARATION):
continue
if self.appear_then_click(FLEET_PREPARATION):
continue
# End
if self.handle_in_map_with_enemy_searching():
break
Alas 状态机的目标是在最不利的运行环境下保持高鲁棒性和高运行速度。
假设对游戏的控制随机不生效,假设游戏截图随机损坏,假设用户设备随机卡顿 在上述场景下保持运行的鲁棒性,并且做到当前硬件条件下的最快 状态循环,顾名思义就是把游戏内的不同 UI 界面视为 “状态”,根据不同状态对游戏进行相应的操作。收到游戏截图后遍历所有状态,执行相应操作。
状态循环是对经典状态机的简化,因为在经典状态机中我们需要定义状态切换表,而在游戏脚本中游戏的 UI 状态是游戏定义的,比如点击了地图入口(ENTRANCE)那游戏马上就会展示地图准备按钮(MAP_PREPARATION)。如果我们需要编写状态切换表,那么必须也只能和游戏里的状态切换一样,这其实是重复劳动。
因此 Alas 状态机选择了遍历所有状态,Alas状态机其实就是,在一个循环里遍历所有需要处理的状态,判断到就处理,判断到结束状态就跳出,反正游戏自身会保证所有状态是顺序执行的,不需要我们操心。
串联状态循环 当你编写了多个状态循环之后,像线性流程一样串联起每个状态循环就可以了。
其中的 enter_map, execute_a_battle, auto_search_execute_a_battle 函数内部都是状态循环。
python def run(self): self.enter_map(self.ENTRANCE, mode=self.config.Campaign_Mode)
# Run
for _ in range(20):
try:
if not self.map_is_auto_search:
self.execute_a_battle()
else:
self.auto_search_execute_a_battle()
except CampaignEnd:
logger.hr('Campaign end')
return True
状态循环的性能优化 与一般认知不同,开发者在编写 Alas 时不需要特别注意性能优化。因为在 Alas 运行时超过 99% 的时间是在等待模拟器截图,Alas 状态机遍历所有状态的开销是忽略不计的。在配置过关的电脑上,截图耗时约 350 ms,而 Alas 处理只花费约 2.5 ms。在海图识别或者 OCR 时,Alas 耗时也不过 100-180 ms。
忽略遍历状态开销的关键在于缩小识别区域。游戏脚本的识别最普遍使用的方式就是模板匹配,减少模板匹配开销的关键,就是缩小模板图片的大小和缩小搜索区域的大小。游戏内元素往往都出现在固定位置或者某个位置的附近,那么我们就可以只在已知位置的附近进行搜索。Alas 框架已经包含了这项优化,不需要特别在意,但如果你是自己的脚本就千万要注意不要去匹配一整张图片。
一般而言,人的反应速度是 300ms,如果你的脚本从游戏画面出现到执行游戏操作的耗时小于 300ms,用户就会感觉你的脚本运行速度快,感觉脚本操作比自己手动操作快。
随着电脑硬件更新、模拟器更新、截图方式的换代,现在大部分用户的 Alas 截图耗时在 50ms ~ 100ms,特定组合更是只有 5ms ~ 10ms。为了防止截图速度过快导致 CPU 占用增加,Alas 中的 Screenshot._screenshot_interval 计时器默认会把 对设备发起截图操作 的间隔限制到 300ms。发起间隔的意思是,假如截图耗时 50ms 处理耗时 10ms,Alas 会 sleep 240ms 再进行下一次截图。
复用截图 加速状态循环的另一个关键点是复用上一张游戏截图,也是就是状态循环需要设置 skip_first_screenshot。大部分 Alas 方法都会复用上一张截图,这样连续调用的时候就不会产生多余的截图操作。
python def map_offensive(self, skip_first_screenshot=True): """ Pages: in: in_map, MAP_OFFENSIVE out: combat_appear """ while 1: if skip_first_screenshot: skip_first_screenshot = False else: self.device.screenshot() # Do something pass 如果每一步都复用了上一张截图,那么第一张截图从何而来呢?在运行之前,需要显式地执行一次 self.device.screenshot() 。
python self = Combat('alas') self.device.screenshot() self.map_offensive() self.combat() 在状态循环中的interval参数 状态循环由三部分组成:截图、判断退出、判断点击。识别点击一般使用 appear_then_click 方法。
python def dorm_view_reset(self, skip_first_screenshot=True): logger.info('Dorm view reset') while 1: if skip_first_screenshot: skip_first_screenshot = False else: self.device.screenshot() if self.appear(DORM_MANAGE_CHECK, offset=(20, 20)): break if self.appear_then_click(DORM_MANAGE, offset=(20, 20), interval=2): continue if self.ui_additional(): continue if self.appear_then_click(DORM_FURNITURE_CONFIRM, offset=(30, 30), interval=2): continue 我们一般会给 appear_then_click 方法加上 interval 参数,表示两次点击之间的最短间隔(秒)。
以 self.appear_then_click(DORM_FURNITURE_CONFIRM, interval=2) 为例:
当 DORM_FURNITURE_CONFIRM 首次出现时,interval 不生效,按钮会被点击。 点击之后的 2s 内,interval 生效,Alas 不检查 DORM_FURNITURE_CONFIRM 是否出现,即便它出现也不会触发点击。 2s 过去后,interval 失效,Alas 将重新开始检查 DORM_FURNITURE_CONFIRM。 interval 的作用是防止连击。游戏客户端对点击是需要一些时间响应的,虽然大部分时候响应非常快,但是这个时间不能被忽略。假如没有 interval,点击第一次之后游戏仍然停留在先前的页面(可能在等待服务器响应),那么就会触发二次点击。
大部分情况下,你只需要给所有的操作都设置上 interval。间隔一般取 2/3/5 都可以,interval=2 意味着这个状态循环在延迟<2s 的设备上能稳定运行,这已经涵盖绝大多数设备了。interval 的取值不建议过大,这样在点击不生效的时候状态循环可以快速重试。
interval 的实现其实是一个全局计时器字典 self.interval_timer,如果你在两个连续的状态循环之间共用了同一个 assets,那么就需要清除上一个状态循环遗留的 interval。
python self.intervalclear(DORM_CHECK) handle系方法 handle*() 方法是约定的命名(当然实际命名有点混乱) ,定义对一系列状态的处理,以便在不同的状态循环中复用。
handle系方法只返回 bool,True 表示在方法内对游戏进行了操作,需要状态循环获取新的游戏截图。
python def handle_blessing(self): """ Returns: bool: If handled """ if self.is_page_choose_blessing(): logger.hr('Choose blessing', level=2) selector = RogueBlessingSelector(self) selector.recognize_and_select() return True if self.is_page_choose_curio(): logger.hr('Choose curio', level=2) selector = RogueCurioSelector(self) selector.recognize_and_select() return True if self.is_page_choose_bonus(): logger.hr('Choose bonus', level=2) selector = RogueBonusSelector(self) selector.recognize_and_select() return True if self.handle_blessing_popup(): return True
return False
像这样使用
python def _domain_exit_wait_next(self, skip_first_screenshot=True): while 1: if skip_first_screenshot: skip_first_screenshot = False else: self.device.screenshot()
# End
...
if self.handle_blessing():
continue
if self.handle_popup_confirm():
continue
if self.handle_reward():
continue
if self.handle_get_character():
continue
退出状态循环 将退出条件放到点击的前面,按照 截图-判断退出-判断点击 的顺序。
当设备慢到一定程度(截图耗时比 interval 还要长)的时候,状态循环依然能够退出,不会一直在点击。
python while 1: if skip_first_screenshot: skip_first_screenshot = False else: self.device.screenshot() # End if self.appear(...): break # Click if self.appear_then_click(..., interval=2): continue if self.handle_popup_confirm(): continue 常见错误:在状态循环中加入sleep 状态循环兼容快设备及慢设备,加入 sleep 其实是一种自我安慰,除了让运行速度变慢之外并不会有其他影响,开发者需要改变 “脚本速度快,出错概率就高” 的认知。
解决办法,直接删除即可。
python def _storage_enter_disassemble(self, skip_first_screenshot=True): """ Pages: in: page_storage, any out: page_storage, disassemble, DISASSEMBLE_CANCEL """ logger.info('storage enter disassemble') while 1: if skip_first_screenshot: skip_first_screenshot = False else: self.device.screenshot()
if self.appear(DISASSEMBLE_CANCEL, offset=(20, 20)):
break
# equipment -> disassemble
if self.appear_then_click(DISASSEMBLE, offset=(20, 20), interval=3):
self.device.sleep(1)
continue
# material -> equipment
if self._storage_in_material(interval=3):
logger.info('_storage_in_material -> EQUIPMENT_ENTER')
continue
# design -> equipment
if self.appear(STORAGE_CHECK, offset=(20, 20), interval=3):
logger.info('STORAGE_CHECK -> EQUIPMENT_ENTER')
self.device.click(EQUIPMENT_ENTER)
continue
常见错误:等价点击等待 避免使用 wait_until_stable 方法和 confirm_timer 形式,这些写法实际非常屎。
python direction = drag.reverse_direction(direction) drag.drag_page(direction, self, (0.4, 0.4)) entry = self._get_path_click(path, direction) self.wait_until_stable(CLICK_THE_HUNT_LEFT if direction == 'left' else CLICK_THE_HUNT_RIGHT, timer=Timer(0, count=0), timeout=Timer(1.5, count=5)) python def _guild_lobby_collect(self, skip_first_screenshot=True): confirm_timer = Timer(1.5, count=3).start() while 1: if skip_first_screenshot: skip_first_screenshot = False else: self.device.screenshot()
# End
if self.appear(GUILD_CHECK, offset=(20, 20)):
if confirm_timer.reached():
break
else:
confirm_timer.reset()
常见错误:循环未首先截图 因为状态循环需要跳过第一次截图,所以就有了将截图放在最后的写法。这种写法非常容易导致无新截图的死循环并且难以发现,还是 skip_first_screenshot 的写法比较好。
python while 1: if self.appear(DOCK_CHECK, offset=(20, 20)): break elif self.appear_then_click(RETIRE_APPEAR_1, offset=30): continue elif self.appear_then_click(RETIRE_APPEAR_2, offset=30): continue elif self.appear_then_click(RETIRE_APPEAR_3, offset=30): continue else: self.device.screenshot() 常见错误:使用负面条件 不能使用负面条件(if not appear(xxx))作为状态循环的退出条件或是触发点击的条件。因为在游戏动画中、加载但是还未完全加载的情况下,不属于已知状态,非常容易触发 False 条件。
解决办法:完全使用正面条件(if appear())。
python def use_fuel(self, skip_first_screenshot=True): logger.info("Use Fuel") while 1: if skip_first_screenshot: skip_first_screenshot = False else: self.device.screenshot()
if not self.appear(FUEL) and not self.appear(FUEL_SELECTED):
logger.info("No fuel found")
return
if self.appear(FUEL_SELECTED):
break
if self.appear_then_click(FUEL):
continue
if self.appear_then_click(FUEL_ENTRANCE):
continue
if self.handle_reward():
break
常见错误:使用带操作的语句退出循环 带操作的语句(appear_then_click,handle系方法)不能作为状态循环的退出条件,否则这个操作就不包含在状态循环内了,点击失败的时候也没有重试。
解决办法,只使用 appear() 作为退出条件。
python while 1: self.device.screenshot() if self.deal_popup(): continue # choose game if self.appear_then_click(GAME_NEW_YEAR_BATTLE, offset=(5, 5)): break if swipe_interval.reached(): self.device.swipe_vector(...) swipe_interval.reset() 常见错误:使用带interval的识别退出循环 interval 是用来控制点击间隔的,退出识别没有对游戏的操作,因此不需要设置 interval。
解决办法:删除 interval。
python
if self.appear(GET_ITEMS_1, interval=1) or
self.appear(GET_ITEMS_2, interval=1) or
self.appear(GET_ITEMS_3, interval=1):
success = True
break
常见错误:嵌套使用状态循环
Alas 状态机希望开发者不再关心状态的执行顺序,因此嵌套使用状态循环是错误的。(下面的代码中, ui_click 是一个状态循环)
解决办法很简单,把子循环中的全部内容放到父循环中即可,保证只有一个循环。
python def shop_refresh(self, skip_first_screenshot=True): while 1: if skip_first_screenshot: skip_first_screenshot = False else: self.device.screenshot()
if self.appear_then_click(SHOP_REFRESH, interval=3):
continue
if self.appear(SHOP_BUY_CONFIRM_MISTAKE, interval=3, offset=(200, 200)) \
and self.appear(POPUP_CONFIRM, offset=(3, 30)):
self.ui_click(SHOP_CLICK_SAFE_AREA,
appear_button=POPUP_CONFIRM, check_button=BACK_ARROW,
offset=(20, 30), skip_first_screenshot=True)
if self.handle_popup_confirm('SHOP_REFRESH_CONFIRM'):
continue
当然,规矩是死的人是活的,如果情况复杂并且你很清楚自己在干什么,嵌套使用也是可以接受的。
在 Alas 的退役模块中,handle_retirement() 封装了对退役弹窗的处理,这个方法会插入到所有战斗的状态循环中,处理退役弹窗需要点击弹窗然后在船坞退役角色。这是一个典型的嵌套。
python def handle_retirement(self): if self._unable_to_enhance: if self.appear_then_click(RETIRE_APPEAR_1, offset=(20, 20), interval=3): if self.appear(IN_RETIREMENT_CHECK, offset=(20, 20), interval=10): elif self.config.Retirement_RetireMode == 'enhance': if self.appear_then_click(RETIRE_APPEAR_3, offset=(20, 20), interval=3): if self.appear(DOCK_CHECK, offset=(20, 20), interval=10): else: if self.appear_then_click(RETIRE_APPEAR_1, offset=(20, 20), interval=3): if self.appear(IN_RETIREMENT_CHECK, offset=(20, 20), interval=10): self._retire_handler() return True return False 其中的 _retire_handler() 包含多个状态循环:retire_ships_one_click(), _retirement_quit() 等。
python def _retire_handler(self, mode=None): """ Pages: in: IN_RETIREMENT_CHECK out: the page before retirement popup """ if mode == 'one_click_retire': total = self.retire_ships_one_click() if not total: logger.warning(...) self.dock_filter_set() self.dock_favourite_set(False) total = self.retire_ships_one_click() total += self.retire_gems_farming_flagships(keep_one=total > 0) elif mode == 'old_retire': ... self._retirement_quit() return total 嵌套状态循环需要注意的地方是,子循环需要退出其内部状态(_retirement_quit),让父循环能够继续处理。
地图识别 想必你已经阅读过 Alas 的海图识别的文档。在识别之后,module/map_detection/view.py 将两种识别方法封装了起来,并整合了敌人识别。
局部地图识别 View 对象的识别结果,称为局部地图(Local Map)。局部地图的识别只需要一张游戏截图,并且可以在 Alas 外运行,你可以使用以下代码来调试 Alas 的海图识别。
python class Config: # 在此处粘贴你的配置 pass
这里是图片文件路径
file = ''
md = View(AzurLaneConfig('template').merge(Config())) image = np.array(Image.open(file).convert('RGB'))
如果你的日志中有任何 “homo_storage”,请在此处粘贴并取消注释
sto = ((8, 5), [(355, 117), (1265, 117), (288, 559), (1417, 559)])
md.backend.load_homography(storage=sto)
md.load(image) md.predict() md.show() md.backend.draw() View(config, mode='main') 创建一个 View 对象,需要 AzurLaneConfig,模式可选普通图 main 或者大世界 os。
默认情况下,局部地图识别使用 Homography 方法识别,你可以在 config 里指定识别方法
python DETECTION_BACKEND = 'perspective' # Or 'homography' 在 Homography 方法下,第一次识别会调用 Perspective 方法计算透视变换参数,并缓存至 homo_storage。一般而言,海域内的透视不会变化,Alas 会一直使用同一个 View 对象,同一个 View 对象也会使用同一个 homo_storage,直到任务切换。如果第一次计算的透视变换参数不正确(比如镜头位于角落),会影响到接下来的识别。而在 Perspective 方法下,不会有这个影响。
load(self, image) 载入一张截图,完成海图识别,获得以下属性:
grids:海域中的每一个格子,dict, key: (x, y), value: Grid object。 shape:局部海域的大小,(x, y)。 center_loca:镜头中心对准的格子的坐标,(x, y)。 center_offset:镜头中心偏离格子中心的向量,在计算滑动距离时使用。 swipe_base:反映海域中网格大小的量,在计算滑动距离时使用。 predict(self) 识别地图中的敌人,问号,己方舰队等。
update(self, image) 在不重新进行海图识别的情况下,将图片载入至所有格子内。
select(self, **kwargs) 选择海域中任何你感兴趣格子,返回 SelectedGrids 对象。
创建全局地图 这里介绍 module/map/map_base.py 内 CampaignBase 类构造的全局地图。
创建全局地图 早期,地图信息都是手动编写的,以 7-2 的地图为例:
现在,交由 dev_tools/map_extractor.py 根据游戏解包数据自动生成就可以了,开发者只需要检查是否有错误,或进行优化。
python
MAP = CampaignMap('7-2')
MAP.shape = 'H5'
MAP.map_data = '''
ME ++ ME -- ME ME -- SP
MM ++ ++ MM -- -- ME --
ME -- ME MB ME -- ME MM
-- ME -- MM -- ME ++ ++
SP -- ME ME -- ME ++ ++
'''
MAP.weight_data = '''
40 30 30 30 30 30 30 30
20 20 20 10 20 20 20 20
10 10 10 10 10 10 10 10
20 20 20 10 20 20 20 20
30 30 30 30 30 30 30 30
'''
MAP.camera_data = ['D3', 'D2']
MAP.camera_data_spawn_point = ['D3', 'D2']
MAP.spawn_data = [
{'battle': 0, 'enemy': 3},
{'battle': 1, 'enemy': 2, 'mystery': 1},
{'battle': 2, 'enemy': 2, 'mystery': 1},
{'battle': 3, 'enemy': 1, 'mystery': 2},
{'battle': 4, 'enemy': 1},
{'battle': 5, 'boss': 1},
]
A1, B1, C1, D1, E1, F1, G1, H1,
A2, B2, C2, D2, E2, F2, G2, H2,
A3, B3, C3, D3, E3, F3, G3, H3,
A4, B4, C4, D4, E4, F4, G4, H4,
A5, B5, C5, D5, E5, F5, G5, H5 = MAP.flatten()
shape
地图网格大小,未设置时,根据 map_data 产生
map_data 地图中所有网格的信息,以 7-2 为例
地图信息定义为
python MAP.map_data = ''' ME ++ ME -- ME ME -- SP MM ++ ++ MM -- -- ME -- ME -- ME MB ME -- ME MM -- ME -- MM -- ME ++ ++ SP -- ME ME -- ME ++ ++ ''' 这些符号的含义,在 module.map_detection.grid_info.py 中。
print_name property_name description ++ is_land fleet can't go to land -- is_sea sea __ submarine spawn point SP is_spawn_point fleet may spawns here ME may_enemy enemy may spawns here MB may_boss boss may spawns here MM may_mystery mystery may spawns here MA may_ammo fleet can get ammo here MS may_siren Siren/Elite enemy spawn map_data_loop 周回模式下的地图信息,部分活动图在周回模式前后有变化,需要额外定义。
camera_data Alas 会将画面中心对准这些格子,来扫描海域。未定义时,根据 shape 生成。
python MAP.camera_data = ['D4', 'E4', 'E2'] 镜头信息需要覆盖到所有会刷怪的格子 镜头信息需要尽可能地少,位置尽可能接近,来加快扫描速度 在大部分地图中,有效的区域只有 (-3, -1, 3, 2),也就是镜头中心左右三排以内,往上一行到往下两行的区域。往上第二行,由于地图 buff 会遮挡敌人星级,一般视为无效。但如果刚好那个格子不会刷怪,可以视作有效,从而减少 camera_data
camera_data_spawn_point 在进入地图后的第一次扫描,Alas 会扫描舰队刷新点,来找到己方舰队的位置。由于全图扫描在扫描到所有预期的敌人之后便会停止(早停,early stop),这可能会漏掉舰队刷新点。camera_data_spawn_point 可以强迫 Alas 在第一次扫描时经过这些点。
camera_data_spawn_point 通常是 camera_data 中,可以看到舰队刷新点的格子。
spawn_data 定义每一战过后的敌人刷新信息。由于全图扫描具有早停(early stop)的特性,spawn_data 需要尽可能地准确来触发早停。否则 Alas 会经过 camera_data 中所有定义的点。
例如 7-2
python MAP.spawn_data = [ {'battle': 0, 'enemy': 3}, {'battle': 1, 'enemy': 2, 'mystery': 1}, {'battle': 2, 'enemy': 2, 'mystery': 1}, {'battle': 3, 'enemy': 1, 'mystery': 2}, {'battle': 4, 'enemy': 1}, {'battle': 5, 'boss': 1}, ] spawn_data_loop 周回模式下的敌人刷新信息,部分活动图在周回模式前后有变化,需要额外定义。
weight_data 每个格子的权重,优先选择值权重小的点。默认情况下,Alas 将优先选择权重小的点,权重相同时,再选择距离短的点。
权重需要是 1-99 之间的整数。
python MAP.weight_data = ''' 40 30 30 30 30 30 30 30 20 20 20 10 20 20 20 20 10 10 10 10 10 10 10 10 20 20 20 10 20 20 20 20 30 30 30 30 30 30 30 30 ''' wall_data 穹顶下的圣咏曲(event_20200521_cn)中定义 “光之壁”。“光之壁” 将阻挡舰队从一个格子去往另一个格子。
以 D2 为例,· 代表格子,用 - 和 | 定义 “光之壁”。这将在寻路中断开两个格子之间的连接。
python MAP.wall_data = """ · · | · · · · · · · · · , + , · · · · · · · · · · | · , + + + + + , · · | · · | · | · · · · | · · , +---+---+ | | + , · · · | · · | · · · · | · | · , +---+ +---+ +---+ +-- , · · · · · · · · · · · , , · · · · · · · · · · · , , · · · · · · · · · · · , """ portal_data 蝶海梦花 (event_20200917_cn) 活动中定义 “传送门”。玩家舰队到达至 “传送门” 的一端后,将被移动至另一端,同时镜头重新对准当前舰队。
以 HT5 为例,定义两个传送门之间的连接,这将在寻路中额外连接两个格子。
python MAP.portal_data = [('D3', 'F3'), ('G4', 'G6'), ('F7', 'D7'), ('C6', 'C4')] land_based_data 复刻峡湾间的星辰(event_20200921_en)活动中定义 “岸防炮”。“岸防炮” 会炮击它朝向的三格以内,玩家舰队经过时会被打断,前往岸防炮四周的格子可摧毁岸防炮。
以 SP3 为例,定义岸防炮的位置和朝向。
python MAP.land_based_data = 'H7', 'up'], ['D5', 'left'], ['G3', 'down'], ['C2', 'right' maze_data 复兴的赞美诗(event_20210421_cn)活动中定义 “迷宫”。“迷宫“ 会产生 ”障碍“ 阻挡玩家移动,玩家舰队每移动 3 次,迷宫改变一次。
以 D2 为例,定义一个循环里,迷宫产生的障碍。障碍将在寻路中,与陆地一样,被 Alas 视为不可到达的海域。
python MAP.maze_data = [('A4', 'I6'), ('F9', 'D1', 'E5'), ('A7', 'C9', 'G1', 'I3')] fortress_data 碧海光粼(event_20210916_cn)活动中定义 “机关”,玩家需要击败所有机关中的敌人,才能解锁被锁定的格子,通往 BOSS 点。
以 D3 为例,定义机关位置和被锁定的格子,机关将被视作精英(siren),在机关未解锁之前,被锁定的格子视作不可到达。
python MAP.fortress_data = [('B5', 'E2', 'H5', 'E8'), 'G3'] 展平地图 这是为后续编写索敌逻辑做的准备
python
A1, B1, C1, D1, E1, F1, G1, H1,
A2, B2, C2, D2, E2, F2, G2, H2,
A3, B3, C3, D3, E3, F3, G3, H3,
A4, B4, C4, D4, E4, F4, G4, H4,
A5, B5, C5, D5, E5, F5, G5, H5,
= MAP.flatten()
可以使用以下代码生成
python shape = 'H5' def location2node(location): return chr(location[0] + 64 + 1) + str(location[1] + 1) def node2location(node): return ord(node[0]) % 32 - 1, int(node[1]) - 1 shape = node2location(shape.upper()) for y in range(shape[1]+1): text = ', '.join([location2node((x, y)) for x in range(shape[0]+1)]) + ', \' print(text) print(' = MAP.flatten()') 创建全局地图 这里介绍 module/map/map_base.py 内 CampaignBase 类构造的全局地图。
创建全局地图 早期,地图信息都是手动编写的,以 7-2 的地图为例:
现在,交由 dev_tools/map_extractor.py 根据游戏解包数据自动生成就可以了,开发者只需要检查是否有错误,或进行优化。
python
MAP = CampaignMap('7-2')
MAP.shape = 'H5'
MAP.map_data = '''
ME ++ ME -- ME ME -- SP
MM ++ ++ MM -- -- ME --
ME -- ME MB ME -- ME MM
-- ME -- MM -- ME ++ ++
SP -- ME ME -- ME ++ ++
'''
MAP.weight_data = '''
40 30 30 30 30 30 30 30
20 20 20 10 20 20 20 20
10 10 10 10 10 10 10 10
20 20 20 10 20 20 20 20
30 30 30 30 30 30 30 30
'''
MAP.camera_data = ['D3', 'D2']
MAP.camera_data_spawn_point = ['D3', 'D2']
MAP.spawn_data = [
{'battle': 0, 'enemy': 3},
{'battle': 1, 'enemy': 2, 'mystery': 1},
{'battle': 2, 'enemy': 2, 'mystery': 1},
{'battle': 3, 'enemy': 1, 'mystery': 2},
{'battle': 4, 'enemy': 1},
{'battle': 5, 'boss': 1},
]
A1, B1, C1, D1, E1, F1, G1, H1,
A2, B2, C2, D2, E2, F2, G2, H2,
A3, B3, C3, D3, E3, F3, G3, H3,
A4, B4, C4, D4, E4, F4, G4, H4,
A5, B5, C5, D5, E5, F5, G5, H5 = MAP.flatten()
shape
地图网格大小,未设置时,根据 map_data 产生
map_data 地图中所有网格的信息,以 7-2 为例
地图信息定义为
python MAP.map_data = ''' ME ++ ME -- ME ME -- SP MM ++ ++ MM -- -- ME -- ME -- ME MB ME -- ME MM -- ME -- MM -- ME ++ ++ SP -- ME ME -- ME ++ ++ ''' 这些符号的含义,在 module.map_detection.grid_info.py 中。
print_name property_name description ++ is_land fleet can't go to land -- is_sea sea __ submarine spawn point SP is_spawn_point fleet may spawns here ME may_enemy enemy may spawns here MB may_boss boss may spawns here MM may_mystery mystery may spawns here MA may_ammo fleet can get ammo here MS may_siren Siren/Elite enemy spawn map_data_loop 周回模式下的地图信息,部分活动图在周回模式前后有变化,需要额外定义。
camera_data Alas 会将画面中心对准这些格子,来扫描海域。未定义时,根据 shape 生成。
python MAP.camera_data = ['D4', 'E4', 'E2'] 镜头信息需要覆盖到所有会刷怪的格子 镜头信息需要尽可能地少,位置尽可能接近,来加快扫描速度 在大部分地图中,有效的区域只有 (-3, -1, 3, 2),也就是镜头中心左右三排以内,往上一行到往下两行的区域。往上第二行,由于地图 buff 会遮挡敌人星级,一般视为无效。但如果刚好那个格子不会刷怪,可以视作有效,从而减少 camera_data
camera_data_spawn_point 在进入地图后的第一次扫描,Alas 会扫描舰队刷新点,来找到己方舰队的位置。由于全图扫描在扫描到所有预期的敌人之后便会停止(早停,early stop),这可能会漏掉舰队刷新点。camera_data_spawn_point 可以强迫 Alas 在第一次扫描时经过这些点。
camera_data_spawn_point 通常是 camera_data 中,可以看到舰队刷新点的格子。
spawn_data 定义每一战过后的敌人刷新信息。由于全图扫描具有早停(early stop)的特性,spawn_data 需要尽可能地准确来触发早停。否则 Alas 会经过 camera_data 中所有定义的点。
例如 7-2
python MAP.spawn_data = [ {'battle': 0, 'enemy': 3}, {'battle': 1, 'enemy': 2, 'mystery': 1}, {'battle': 2, 'enemy': 2, 'mystery': 1}, {'battle': 3, 'enemy': 1, 'mystery': 2}, {'battle': 4, 'enemy': 1}, {'battle': 5, 'boss': 1}, ] spawn_data_loop 周回模式下的敌人刷新信息,部分活动图在周回模式前后有变化,需要额外定义。
weight_data 每个格子的权重,优先选择值权重小的点。默认情况下,Alas 将优先选择权重小的点,权重相同时,再选择距离短的点。
权重需要是 1-99 之间的整数。
python MAP.weight_data = ''' 40 30 30 30 30 30 30 30 20 20 20 10 20 20 20 20 10 10 10 10 10 10 10 10 20 20 20 10 20 20 20 20 30 30 30 30 30 30 30 30 ''' wall_data 穹顶下的圣咏曲(event_20200521_cn)中定义 “光之壁”。“光之壁” 将阻挡舰队从一个格子去往另一个格子。
以 D2 为例,· 代表格子,用 - 和 | 定义 “光之壁”。这将在寻路中断开两个格子之间的连接。
python MAP.wall_data = """ · · | · · · · · · · · · , + , · · · · · · · · · · | · , + + + + + , · · | · · | · | · · · · | · · , +---+---+ | | + , · · · | · · | · · · · | · | · , +---+ +---+ +---+ +-- , · · · · · · · · · · · , , · · · · · · · · · · · , , · · · · · · · · · · · , """ portal_data 蝶海梦花 (event_20200917_cn) 活动中定义 “传送门”。玩家舰队到达至 “传送门” 的一端后,将被移动至另一端,同时镜头重新对准当前舰队。
以 HT5 为例,定义两个传送门之间的连接,这将在寻路中额外连接两个格子。
python MAP.portal_data = [('D3', 'F3'), ('G4', 'G6'), ('F7', 'D7'), ('C6', 'C4')] land_based_data 复刻峡湾间的星辰(event_20200921_en)活动中定义 “岸防炮”。“岸防炮” 会炮击它朝向的三格以内,玩家舰队经过时会被打断,前往岸防炮四周的格子可摧毁岸防炮。
以 SP3 为例,定义岸防炮的位置和朝向。
python MAP.land_based_data = 'H7', 'up'], ['D5', 'left'], ['G3', 'down'], ['C2', 'right' maze_data 复兴的赞美诗(event_20210421_cn)活动中定义 “迷宫”。“迷宫“ 会产生 ”障碍“ 阻挡玩家移动,玩家舰队每移动 3 次,迷宫改变一次。
以 D2 为例,定义一个循环里,迷宫产生的障碍。障碍将在寻路中,与陆地一样,被 Alas 视为不可到达的海域。
python MAP.maze_data = [('A4', 'I6'), ('F9', 'D1', 'E5'), ('A7', 'C9', 'G1', 'I3')] fortress_data 碧海光粼(event_20210916_cn)活动中定义 “机关”,玩家需要击败所有机关中的敌人,才能解锁被锁定的格子,通往 BOSS 点。
以 D3 为例,定义机关位置和被锁定的格子,机关将被视作精英(siren),在机关未解锁之前,被锁定的格子视作不可到达。
python MAP.fortress_data = [('B5', 'E2', 'H5', 'E8'), 'G3'] 展平地图 这是为后续编写索敌逻辑做的准备
python
A1, B1, C1, D1, E1, F1, G1, H1,
A2, B2, C2, D2, E2, F2, G2, H2,
A3, B3, C3, D3, E3, F3, G3, H3,
A4, B4, C4, D4, E4, F4, G4, H4,
A5, B5, C5, D5, E5, F5, G5, H5,
= MAP.flatten()
可以使用以下代码生成
python shape = 'H5' def location2node(location): return chr(location[0] + 64 + 1) + str(location[1] + 1) def node2location(node): return ord(node[0]) % 32 - 1, int(node[1]) - 1 shape = node2location(shape.upper()) for y in range(shape[1]+1): text = ', '.join([location2node((x, y)) for x in range(shape[0]+1)]) + ', \' print(text) print(' = MAP.flatten()') GUI 配置 这里介绍 Alas 的 GUI 的设置生成,以及如何在代码中使用它们。修改 Alas 的选项不需要具备前端知识,因为 GUI 中的选项是靠 json 生成的。
下面的文件均在 module/config 中,为了防止路径过长,会省略 module/config。
选项生成 GUI 中选项储存在 argument/args.json 中,它有 3 个层级。
任务(task):每个任务可以包含多个选项组。 选项组(group):每个选项选项组可以包含选项。 选项(argument):每个选项会有以下属性: 属性名 备注 type 类型 value 值 option (可选)下拉菜单列表中的元素 validate (可选)校验类型,比如时间格式的校验 display (可选)显示类型,比如禁止更改或在界面上隐藏 args.json 是由 config_updater.py 生成的,每次修改选项后,都应该运行 config_updater.py 来生成,而不是手动修改 args.json。
需要开发者手动编写的有:
task.yaml:定义每个任务所包含的选项组 argument.yaml:定义每个选项组包含的选项,以及每个选项的属性 override.yaml:覆盖某些特殊选项的值,比如硬编码服务器刷新时间 gui.yaml:定义 GUI 上会出现的其他文本 由 config_updater.py 自动生成的文件有:
args.json:用于生成 GUI menu.json:生成 GUI 侧边栏的任务卡组 config_generated.py:提供 IDE 的代码自动提示 template.json:默认用户设置 i18n/{lang}.json:翻译文件 完整生成流程以 config_updater.py 末尾的注释为准。
sh task.yaml -+----------------> menu.json argument.yaml -+-> args.json ---> config_generated.py override.yaml -+ | gui.yaml --------| || (old) i18n/.json --------\========> i18n/.json (old) template.json ---------========> template.json 新增选项 假设我们需要新增一个选项:每天购买 X 个指挥喵,它位于 指挥喵 任务中的 指挥喵 选项组中。
在 argument/argument.yaml 中定义选项组 指挥喵(Meowfficer) 和选项 每天购买 X 个指挥喵(BuyAmount):
yaml Meowfficer: BuyAmount: type: input value: 1 在这里填写的值(value)将成为选项默认值。
选项定义的简写 你也可以只填写默认值,这是非常方便的:
yaml Meowfficer: BuyAmount: 1 经过 config_updater.py 生成,将得到:
json "Meowfficer": { "BuyAmount": { "type": "input", "value": 1 } } config_updater.py 将会根据默认值的数据类型生成 选项类型(type),生成规则如下,实际以 data_to_type() 函数为准:
选项 type 默认值为 bool checkbox 选项的定义含有 options 属性 select 选项名称包含 Filter textarea 其余全部 input 覆盖自动生成的属性 另外,手动填写的属性会优先于自动生成的属性。比如按照上面的定义,选项 每天购买 X 个指挥喵(BuyAmount) 的 选项类型(type) 应该是 input,你但也可以强行指定 type 为 textarea:
yaml Meowfficer: BuyAmount: type: textarea value: 1 传递特殊属性 除了 type、value、option、validate 这几个基本属性外,你还可以定义其他属性:
yaml Meowfficer: BuyAmount: something_special: 100 value: 1 其他属性会被复制到 args.json 中:
json "Meowfficer": { "BuyAmount": { "type": "input", "value": 1, "something_special": 100 } } 新增任务 假设我们需要新增一个任务:指挥喵(Meowfficer)
在 argument/task.yaml 中定义任务所包含的选项组,一个任务应当包含 调度器设置(Scheduler)
yaml Meowfficer:
- Scheduler
- Meowfficer 任务调度优先级 修改 config_manual.py 中的 SCHEDULER_PRIORITY 值,将 Meowfficer 插入其中。
SCHEDULER_PRIORITY 是一个过滤器,用大于号 > 连接任务,靠前的任务会先被调度器执行。基本的任务排序规则是:重启 > 收菜类 > 每日收获类 > 每日出击类 > 正常出击类 > 纯消磨时间类。为防止用户盲目地把自己想运行的任务放到前面,导致收菜类任务被延迟,SCHEDULER_PRIORITY 不会暴露到 GUI 中,需要开发者手动编写。
运行任务 在根目录下的 alas.py AzurLaneAutoScript 类中添加任务的函数
python def meowfficer(self): from module.meowfficer.meowfficer import RewardMeowfficer RewardMeowfficer(config=self.config, device=self.device).run() 覆盖选项 在 argument/override.yaml 中覆盖一些选项,比如硬编码任务 大舰队(Guild) 的服务器刷新时间。
yaml Guild: Scheduler: SuccessInterval: 30 FailureInterval: 30 ServerUpdate: 00:00, 06:00, 12:00, 18:00, 21:00 选项被覆盖后,不会出现在 GUI 中。
调用选项 在 argument/argument.yaml 中定义的选项,会以 {group}_{argument} 的格式生成于 config_generated.py 中。
Alas 会绑定 config_generated.py 中的变量与用户设置中的值,例如在任务 指挥喵(Meowfficer) 中,根据 task.yaml 中的定义,绑定了选项组 调度器设置(Scheduler) 和 指挥喵(Meowfficer)。这个绑定是双向的和自动的。
访问用户设置 访问刚刚添加的选项 每天购买 X 个指挥喵(BuyAmount) :
python print(self.config.Meowfficer_BuyAmount)
1
修改这个变量的值,Alas 也会同步修改用户配置中的值:
python self.config.Meowfficer_BuyAmount = 15 sh INFO 23:54:29.501 │ Save config ./config\alas.json, Meowfficer.Meowfficer.Meowfficer_BuyAmount=15 如果要修改多个变量,应该使用 multi_set(),这只会触发一次文件保存:
python with self.config.multi_set(): self.config.Meowfficer_BuyAmount = 15 self.config.Meowfficer_FortChoreMeowfficer = True sh INFO 23:57:15.458 │ Save config ./config\alas.json, Meowfficer.Meowfficer.BuyAmount=15, Meowfficer.Meowfficer.FortChoreMeowfficer=True 访问未绑定的用户设置 一般来说,一个任务只需要访问它下属的选项组,不需要访问其他任务的选项。
比如任务 指挥喵(Meowfficer) 中并未包含选项组 出击设置(Campaign),访问这些选项得到的将是默认值,修改它也不会同步到用户设置中。
python print(self.config.Campaign_Name) sh 7-2 但如果确实有需要,比如大世界,可以通过 self.config.data 访问完整的用户设置。
python from datetime import datetime from module.config.utils import deep_get, deep_set
获取任务 隐秘海域(OpsiObscure) 的下一次运行时间
value = deep_get(self.config.data, keys='OpsiObscure.Scheduler.NextRun', default=datetime(2020, 1, 1, 0, 0))
修改任务 隐秘海域(OpsiObscure) 的下一次运行时间
self.config.modified['OpsiObscure.Scheduler.NextRun'] = datetime(2022, 2, 21, 19, 32)
保存
self.config.update() 覆盖用户设置 强制地并在当前任务下永久地覆盖某些设置,但不会写入用户设置中
python self.config.override( Submarine_Fleet=1, Submarine_Mode='every_combat' ) 临时地覆盖某些用户设置
python backup = self.config.temporary(Campaign_UseAutoSearch=True) 然后回退
python backup.recover() 增加翻译 在 i18n/{lang}.json 下是翻译文件,每个任务、选项组和选项都会有名称(name)和帮助(help)两个属性。未添加翻译的条目默认是选项的路径。
json "Meowfficer": { "BuyAmount": { "name": "Meowfficer.BuyAmount.name", "help": "Meowfficer.BuyAmount.help" } } 你可以在 GUI 的侧边栏 - 开发 - 翻译 处修改它。
注意:Alas 没有专人负责翻译,所以谁添加了选项,谁就负责全语言的翻译。
更好地命名选项 Alas 希望选项翻译更接近自然语言,使得选项名称和选项值连起来能成为一个完整句子,比如这里是 每天购买 X 个指挥喵 而不是 猫箱购买数量:X。这似乎有些怪怪的,但 每月 X 号后,不再开启新的大舰队作战 会比 大舰队作战开启阈值:X 更容易理解。
海图识别 本文的视频版,方便理解:
[碧蓝航线]海图识别-从屏幕内容到海域信息(上)
[碧蓝航线]海图识别-从屏幕内容到海域信息(下)
海图识别 是一个碧蓝航线脚本的核心。如果只是单纯地使用 模板匹配 (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=(100,100)) out.paste(image,tuple(np.array(loca) _ 105)) out crop_scale
海域信息解析 未完待续