Skip to content

Commit

Permalink
更新 | 大部分机型现在可以无视背景色的影响
Browse files Browse the repository at this point in the history
新增 | 全局配置文件
新增 | 支持自定义游戏窗口标题和类名
新增 | 支持窗口裁剪
新增 | 支持快速切换模板
新增 | 现在可以自定义进度条的阈值
优化 | 改善代码组织结构和变量命名
优化 | 将耗时操作移动到单独的进程,大幅提高运行效率
优化 | 改变显示和隐藏叠加层的实现方式
  • Loading branch information
Mufanc committed Sep 13, 2021
1 parent 7ab84f1 commit ba8532d
Show file tree
Hide file tree
Showing 28 changed files with 534 additions and 471 deletions.
9 changes: 7 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
# 虚拟环境
/venv/
/detects/clips/*

# 打包产生的文件
/build/
/dist/
/dist/

# 临时截图
/detects/*/clips/*
150 changes: 110 additions & 40 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,25 +1,36 @@
## 原神钓鱼辅助工具
## 原神钓鱼辅助工具 v3.0.1 - 船新升级

✨作者正在努力重构代码中……会尽快带给大家一个更完美的脚本✨

> **「您只需抛出鱼竿,然后我们会帮您搞定一切」**
> <div style="display: flex; justify-content: center"><b>「您只需抛出鱼竿,然后我们会帮您搞定一切」</b></div>
>
> * 如果你觉得这个脚本好用,请点一个 Star⭐,你的 Star 就是作者更新最大的动力
* 感谢 [@hgjazhgj](https://github.com/hgjazhgj) 提供使用 Alpha 通道的思路

* 感谢 [@SwetyCore](https://github.com/SwetyCore) 编写的无需管理员权限版本

#### 效果展示

![](images/demo.gif)

* 演示视频:
* [哔哩哔哩(链接挂了)](https://www.bilibili.com/video/BV1q64y1h7Wu)
* [YouTube](https://youtu.be/lhUBmbiG1Oc)
#### 演示视频:

* [哔哩哔哩(链接挂了)](https://www.bilibili.com/video/BV1q64y1h7Wu)

* [YouTube](https://youtu.be/lhUBmbiG1Oc)

✨欢迎大家在 Issues 中分享自己的配置文件✨

✨也祝各位早日钓到精五鱼叉✨

### 更新内容:

* 将耗时操作放到单独的进程中执行,大幅提高运行效率

* 大部分电脑都可以使用 **Alpha 模式**提高检测的准确率(无法用于云游戏)

### 这个脚本有什么特色?

* 直接在游戏画面上通过叠加层显示信息,直截了当,便于调试
* 游戏中按下 <kbd>Alt</kbd> + <kbd>小键盘「.」</kbd> 来显示/隐藏叠加层

* 使用相对距离定位进度条,不会因为 ui 布局变化而影响检测效果

Expand All @@ -29,80 +40,131 @@

### 使用教程:

> 💡 Release 版本现已发布,下载后直接解压即可使用,[点击这里](https://github.com/Mufanc/Genshin-SmartFishingRod/releases/latest) 跳转到下载页
#### 方式一:下载

> 💡 Release 版本(v2.0.3)现已发布,下载后直接解压即可使用,[点击这里](https://github.com/Mufanc/Genshin-SmartFishingRod/releases/latest) 跳转到下载页
>
> ![](images/quick-start.png)
#### 方式二:手动通过代码运行

* 首先下载项目代码到本地

```shell
git clone https://github.com/Mufanc/Genshin-SmartFishingRod.git
cd Genshin-SmartFishingRod
```

#### 然后检查您的游戏设置中是否能选择 **1600x900** 这一尺寸的窗口
* 然后检查您的游戏设置中是否能选择 **1600x900** 这一尺寸的窗口

* -> 有
=> 有

1. 进入游戏设置,将画面大小改为 1600x900,此时游戏窗口应当没有边框
> 1. 进入游戏设置,将画面大小改为 1600x900,此时游戏窗口应当没有边框
>
> 2. 运行 `python main.py`(脚本会自动申请管理员权限)
>
> 3. 选择合适位置抛下鱼竿,等待脚本自动完成钓鱼
=> 没有

> 很遗憾,现有的配置文件并不能完美支持你的电脑。但请不要灰心,您可以参照 [下面的教程](#关于-detectsyml) 构建自己的配置文件
### 快捷键

2. 运行 `python main.py`(脚本会自动申请管理员权限)
* <kbd>Alt + .</kbd>

隐藏 / 显示叠加层(隐藏后**仍可**自动钓鱼)

3. 选择合适位置抛下鱼竿,等待脚本自动完成钓鱼
* <kbd>Alt + 小键盘【1-9】</kbd>

按叠加层上框定的区域对游戏进行截图,善用此功能可以很方便地创建自己的**模板文件**

* -> 没有
* <kbd>Alt + 小键盘 0</kbd>

&emsp;&emsp;很遗憾,现有的配置文件并不能完美支持你的电脑。但请不要灰心,您可以参照下面的教程构建自己的配置文件
弹出一个窗口,可以在此快速划定一些检测区域,可以很方便地生成自己的**配置文件** **(尚未实现)**

### 关于 `detects/detects.yml`
### 关于 `detects.yml`

&emsp;&emsp;该配置文件中存储着一些图片检测和坐标查找相关的选项:
* 该配置文件中存储着一些图片检测和坐标查找相关的选项:

```yaml
detects:
# use for: 1600x900_dpi100_SMAA

templates:
- name: button
convert: 'gray' # 颜色转换,会通过 cv2.COLOR_BGR2{{convert.upper()}} 进行转换
rect: { left: 0.83, top: 0.88, right: 0.13, bottom: 0.03 } # 标注的矩形框
mode: match # 可选 match(匹配) | find(大图找小图)
threshold: 0.95 # 置信度,超过此阈值时认为匹配成功
rect: { left: 0.83, top: 0.88, right: 0.13, bottom: 0.03 } # 识别区域
threshold: 0.95
template: button.png

- name: hook
rect: { left: 0.49, top: 0.1, right: 0.49, bottom: 0.78 }
mode: find
threshold: 0.85
rect: { left: 0.49, top: 0.1, right: 0.49, bottom: 0.76 }
threshold: 0.7
template: hook.png

progress: # 进度条相关
progress:
# 进度条的相对宽高
width: 0.26
height: 0.027
offset: 0.053 # 进度条中心点到鱼钩图案中心点的高度
frame-color: [ 192, 255, 255 ] # 金色滑框和游标的颜色【BGR】
threshold: 0.04 # 金色像素点数量在进度框中占比达到阈值时,才认为这是一个合法的进度条
sp: [ 6, 18 ] # 在 Y 轴方向上金色像素点数量达到一级时,判定为滑框;数量达到二级时,判定为游标

# 进度条中心点到鱼钩图案中心点的高度
offset: 0.053

# 其它相关设定
frame-color: [ 180, 225, 225 ] # BGR
threshold: 0.035
sp: [ 6, 18 ]

```

* **detects**
#### templates

&emsp;&emsp;指定一个待识别 / 查找的区域,其 `rect` 属性中按比例存储了区域的位置信息,比如左上四分之一方框可以表示为:
描述了所有用于匹配的模板图片信息,其中每个元素的属性解释如下:

* **name**

该模板图片的名称

* **rect**

描述方式类似 css 中的 `position: fixed`,指定一个待识别的区域,其 `rect` 属性中按比例存储了区域的位置信息,比如游戏画面的左上四分之一范围可以表示为:

```yaml
rect: { left: 0, top: 0, right: 0.5, bottom: 0.5 }
```
&emsp;&emsp;`threshold` 和 `template` 必须同时指定或均不指定,当不指定时,脚本仅会在屏幕上划定一块区域,并为其赋予一个下标 `[i]`,可以通过按下快捷键 <kbd>Alt</kbd> + <kbd>小键盘对应数字</kbd> 对该区域快速截图,并保存到 `detects/clips/` 文件夹下
* **threshold**
置信度阈值,当区域内最优匹配与模板相似度不小于此阈值时,认为匹配成功
* **template**
模板图片的文件名,注意模板图须放在模板文件夹下的 `images/` 文件夹下。**该属性为非必须指定**,当不指定时,脚本仅在游戏画面上标注对应区域以供使用快捷键截图,并不会做任何匹配

#### progress

* **width**、**height**

* **progress**
描述进度条的宽度和高度,均为关于游戏画面大小的相对表示(例如 `width: 0.5` 就是画面的一半宽)

&emsp;&emsp;用于提高进度条识别准确度的一些配置,脚本通过检测其下方的鱼钩图标,再进行对应坐标换算的方式定位进度条,其 `width`、`height`、`offset` 属性均为与 `detects` 项中相同的比例表示方式。这里着重介绍一下 `sp` 属性的作用
* **offset**

&emsp;&emsp;钓鱼时出现的金色游标和滑框并不像进度条本身一样半透明或颜色会发生变化,其颜色始终为不透明的金色 `#ffffc0`,故通过统计金色像素在 y 轴方向上出现的数量,加上合适的阈值,便可推断出滑框和游标的位置
进度条中心与「鱼钩」图标的相对距离

&emsp;&emsp;`sp` 为一个二元数组,设某一横坐标 `x` 下 y 轴方向金色像素数目为 `n`,则当 `sp[0] <= n < sp[1]` 时,认为这是一个滑框的左边界或右边界,而当 `n >= sp[1]` 时,则认为该位置是「更长」的游标。用截图工具截图并设法放大计数,便可得到 `sp` 的最佳取值
* **frame-color**

### 一些技巧
游标和滑框的主要颜色,**注意是按 BGR 表示**

* 自动钓鱼时,将检测鱼钩图样的黄色方框置于**偏深色**背景下,有助于提高稳定性
* **threshold**

进度条的判定阈值,当 `frame-color` 在框定的区域内占比达到该阈值时,认为进度条已出现

* **sp**

一个二元数组,设某一横坐标 x 下 y 轴方向 `frame-color` 颜色像素数目为 n,则当 `sp[0] <= n < sp[1]` 时,认为这是一个滑框的左边界或右边界,而当 `n >= sp[1]` 时,则认为该位置是游标。用截图工具截图并设法放大计数,便可得到 sp 的最佳取值

### 无法使用 Alpha 模式时的一些调用技巧

* 将右下角检测上钩的区域置于水面偏蓝绿色背景上,检测鱼钩图样的黄色方框置于偏深色背景上,有助于提高检测效果

* 如果在雪山、踏鞴砂等特殊钓点出现无法自动收竿的情况,请尝试更改阈值或使用针对性的匹配图样

Expand All @@ -111,3 +173,11 @@ rect: { left: 0, top: 0, right: 0.5, bottom: 0.5 }
* 脚本需要管理员权限是因为**游戏以管理员权限启动**,若无管理员权限则无法模拟鼠标动作

* **脚本并未修改游戏内存及文件数据,而是类似连点器这样使用 PostMessage 向窗口发送鼠标事件,但仍然存在被检测到的可能,如果你很担心被封号,请不要使用该脚本**

## Todo

* [ ] 使用快捷键 <kbd>Alt + 0</kbd> 快速生成一个配置文件

* [ ] 使用 [Likianta 的打包工具](https://github.com/Likianta/pyportable-installer) 代替 pyinstaller

* [ ] 添加更多的配置文件模板
4 changes: 4 additions & 0 deletions automaton/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
from .detector import Detector
from .genshin import Genshin
from .hotkey import Hotkey
from .overlay import Overlay
130 changes: 130 additions & 0 deletions automaton/detector.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
import cv2
import numpy as np
import yaml
from loguru import logger

from configs import configs

COLOR_RED = (0, 0, 255)
COLOR_GREEN = (0, 255, 0)
COLOR_BLUE = (255, 0, 0)
COLOR_YELLOW = (0, 255, 255)
COLOR_MAGENTA = (255, 0, 255)


class Detector(object):
def __init__(self):
self.font = cv2.FONT_HERSHEY_COMPLEX
self.model_name = configs["use-model"]
filepath = f'detects/{self.model_name}/detects.yml'

with open(filepath, 'r', encoding='utf-8') as fp:
self.configs = yaml.safe_load(fp)

for item in self.configs['templates']:
if 'template' not in item:
continue
item['template'] = cv2.imread(f'detects/{self.model_name}/images/{item["template"]}')

@staticmethod
def parse_rect(shape, rect):
x1 = int(rect['left'] * shape[1])
y1 = int(rect['top'] * shape[0])
x2 = shape[1] - int(rect['right'] * shape[1])
y2 = shape[0] - int(rect['bottom'] * shape[0])
return x1, y1, x2, y2

def clip_image(self, image, _index):
index = _index - 1
if index >= len(self.configs['templates']):
logger.warning(f'No such template with index "{_index}"')
return
height, width = image.shape[:2]
template = self.configs['templates'][index]
x1, y1, x2, y2 = self.parse_rect((height, width), template['rect'])
filepath = f'detects/{self.model_name}/clips/{template["name"]}.png'
cv2.imwrite(filepath, image[y1:y2, x1:x2])
logger.info(f'Screenshot for "{template["name"]}" saved to {filepath}')

def mark(self, image, x1, y1, x2, y2, text, color):
cv2.rectangle(image, (x1, y1), (x2, y2), color, 2)
if not text:
return
rect, _ = cv2.getTextSize(text, self.font, 0.5, 2)
if y1 < rect[1]:
cv2.putText(image, text, ((x1 + x2 - rect[0]) // 2, y2 + rect[1]), self. font, 0.5, color, 2)
else:
cv2.putText(image, text, ((x1 + x2 - rect[0]) // 2, y1 - rect[1]), self.font, 0.5, color, 2)

def match_template(self, image): # 模板匹配
cover = np.zeros(image.shape, dtype=np.uint8)
height, width = image.shape[:2]
groups = {}

for i, item in enumerate(self.configs['templates']):
x1, y1, x2, y2 = self.parse_rect((height, width), item['rect'])
hint = f'[{i+1}] {item["name"]}'
if 'template' in item:
target = image[y1:y2, x1:x2]
template = item['template']
self.mark(cover, x1, y1, x2, y2, '', COLOR_YELLOW)

result = cv2.matchTemplate(target, template, cv2.TM_SQDIFF_NORMED)
min_val, _, min_loc, _ = cv2.minMaxLoc(result)
similarity = 1 - min_val # 计算相似度
min_loc = min_loc[0] + x1, min_loc[1] + y1 # 还原到绝对坐标
min_rect = (*min_loc, min_loc[0] + template.shape[1], min_loc[1] + template.shape[0])

hint += f' {similarity * 100:.2f}%'
if similarity >= item['threshold']:
color = COLOR_GREEN
groups[item['name']] = min_rect
else:
color = COLOR_RED

self.mark(cover, *min_rect, hint, color)
else:
self.mark(cover, x1, y1, x2, y2, hint, COLOR_BLUE)

return groups, cover

def match_progress(self, image, hook_pos, cover): # 进度条匹配
progress_config = self.configs['progress']

img_height, img_width = image.shape[:2]
center_x = (hook_pos[0] + hook_pos[2]) // 2
center_y = (hook_pos[1] + hook_pos[3]) // 2 - int(img_height * progress_config['offset'])

width = int(img_width * progress_config['width'])
height = int(img_height * progress_config['height'])
x1, x2 = center_x - width // 2, center_x + width // 2
y1, y2 = center_y - height // 2, center_y + height // 2
progress = image[y1:y2, x1:x2]

frame_color = progress_config['frame-color']
mask = np.all(progress == frame_color, axis=2)
count = np.sum(mask)

self.mark(cover, x1, y1, x2, y2, f'Progress [{count}/{width * height}]', (255, 0, 0))
if (count / (width * height)) > progress_config['threshold']:
sample = np.sum(mask, axis=0)
sp = progress_config['sp']
frame_pos, cursor_pos = [], []
for i, val in enumerate(sample):
if sp[0] <= val < sp[1]:
frame_pos.append(i)
elif val >= sp[1]:
cursor_pos.append(i)
if frame_pos and cursor_pos:
frame_pos, cursor_pos = [min(frame_pos), max(frame_pos)], [min(cursor_pos), max(cursor_pos)]
self.mark(cover, x1 + frame_pos[0], y1, x1 + frame_pos[1], y2, 'Frame', (255, 0, 255))
cursor_x = sum(cursor_pos) // 2
if frame_pos[0] <= cursor_x <= frame_pos[1]:
color = (0, 255, 0)
else:
color = (0, 0, 255)
self.mark(cover, x1 + cursor_pos[0], y1, x1 + cursor_pos[1], y2, 'Cursor', color)

# 如果未达到位置则需要点击
return cursor_x < frame_pos[0] + (frame_pos[1] - frame_pos[0]) * configs['progress-threshold']
return None
Loading

0 comments on commit ba8532d

Please sign in to comment.