Merge branch 'dev'

This commit is contained in:
XcantloadX 2025-03-29 22:14:15 +08:00
commit 88e4282c28
37 changed files with 829 additions and 356 deletions

View File

@ -15,6 +15,12 @@
3. 打开 VSCode 设置,搜索 `imageComments.pathMode` 并设置为 `relativeToWorkspace`
4. 编译资源:在 VSCode 中选择“Terminal” -> “Run Task” -> “Make R.py”并执行。
## 运行单个任务
如果使用 VSCode只需要在运行配置里选择 `Python: Current Module` 即可。
如果使用 PyCharm按照下图新建一个 Run Configuration
![pycharm_run_config.png](images/pycharm_run_config.png)
## 打包 & 安装
```bash

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

View File

@ -252,8 +252,7 @@ def calc(image: MatLike, card_count: int):
from kotonebot.backend.debug.mock import MockDevice
from kotonebot.backend.context import device, init_context, manual_context, inject_context
from kotonebot.tasks.actions.in_purodyuusu import handle_recommended_card, skill_card_count
from kotonebot.backend.context import device, init_context, manual_context
from kotonebot.backend.util import Profiler
init_context()
mock = MockDevice()

Binary file not shown.

Before

Width:  |  Height:  |  Size: 221 B

View File

@ -591,7 +591,7 @@ class ContextColor:
def find_all(self, *args, **kwargs):
return color_find_all(ContextStackVars.ensure_current().screenshot, *args, **kwargs)
@deprecated('使用 kotonebot.backend.debug 模块替代')
class ContextDebug:
def __init__(self, context: 'Context'):
self.__context = context
@ -817,6 +817,7 @@ def use_screenshot(*args: MatLike | None) -> MatLike:
return device.screenshot()
WaitBeforeType = Literal['screenshot']
@deprecated('使用普通 sleep 代替')
def wait(at_least: float = 0.3, *, before: WaitBeforeType) -> None:
global next_wait, next_wait_time
if before == 'screenshot':

View File

@ -1,6 +1,7 @@
import logging
from typing import Callable, ParamSpec, TypeVar, overload, Concatenate, Literal
from dataclasses import dataclass
from typing_extensions import deprecated
import cv2
from cv2.typing import MatLike
@ -108,6 +109,7 @@ def action(
...
@overload
@deprecated('使用普通 while 循环代替')
def action(
name: str,
*,

View File

@ -6,7 +6,7 @@ from logging import Logger
from types import CodeType
from dataclasses import dataclass
from typing import Annotated, Any, Callable, Concatenate, Sequence, TypeVar, ParamSpec, Literal, Protocol, cast
from typing_extensions import Self
from typing_extensions import deprecated
from dataclasses import dataclass
@ -74,6 +74,7 @@ class DispatcherContext:
"""是否即将结束运行"""
return self.finished
@deprecated('使用 SimpleDispatcher 类或 while 循环替代')
def dispatcher(
func: Callable[Concatenate[DispatcherContext, P], R],
*,

View File

@ -4,7 +4,8 @@ import logging
import unicodedata
from functools import lru_cache
from dataclasses import dataclass
from typing_extensions import Self
import warnings
from typing_extensions import Self, deprecated
from typing import Callable, NamedTuple
import cv2
@ -129,6 +130,7 @@ class TextComparator:
def __repr__(self) -> str:
return f'{self.name}("{self.text}")'
@deprecated("即将移除")
@lru_cache(maxsize=1000)
def fuzz(text: str) -> TextComparator:
"""返回 fuzzy 算法的字符串匹配函数。"""
@ -404,6 +406,7 @@ class Ocr:
:return: 找到的文本如果未找到则返回 None
"""
if hint is not None:
warnings.warn("使用 `rect` 参数代替")
if ret := self.find(img, text, rect=hint):
logger.debug(f"find: {text} SUCCESS [hint={hint}]")
return ret
@ -441,6 +444,7 @@ class Ocr:
"""
# HintBox 处理
if hint is not None:
warnings.warn("使用 `rect` 参数代替")
result = self.find_all(img, texts, rect=hint, pad=pad)
if all(result):
return result

View File

@ -1,17 +1,20 @@
from typing import Protocol
from typing import Protocol, Literal
import cv2
import numpy as np
from cv2.typing import MatLike
ImageFormat = Literal['bgr', 'hsv']
class PreprocessorProtocol(Protocol):
"""预处理协议。用于 Image 与 Ocr 中的 `preprocessor` 参数。"""
def process(self, image: MatLike) -> MatLike:
def process(self, image: MatLike, *, format: ImageFormat = 'bgr') -> MatLike:
"""
预处理图像
:param image: 输入图像格式为 BGR
:param image: 输入图像
:param format: 输入图像的格式可选值为 'bgr' 'hsv'
:return: 预处理后的图像格式不限
"""
...
@ -29,10 +32,73 @@ class HsvColorFilter(PreprocessorProtocol):
self.upper = np.array(upper)
self.name = name
def process(self, image: MatLike) -> MatLike:
hsv = cv2.cvtColor(image, cv2.COLOR_BGR2HSV)
def process(self, image: MatLike, *, format: ImageFormat = 'bgr') -> MatLike:
if format == 'bgr':
hsv = cv2.cvtColor(image, cv2.COLOR_BGR2HSV)
elif format == 'hsv':
hsv = image
else:
raise ValueError(f'Invalid format: {format}')
mask = cv2.inRange(hsv, self.lower, self.upper)
return mask
def __repr__(self) -> str:
return f'HsvColorFilter(for color "{self.name}" with range {self.lower} - {self.upper})'
class HsvColorRemover(PreprocessorProtocol):
"""去除指定范围内的 HSV 颜色。"""
def __init__(
self,
lower: tuple[int, int, int],
upper: tuple[int, int, int],
*,
name: str | None = None,
):
self.lower = np.array(lower)
self.upper = np.array(upper)
self.name = name
def process(self, image: MatLike, *, format: ImageFormat = 'bgr') -> MatLike:
if format == 'bgr':
hsv = cv2.cvtColor(image, cv2.COLOR_BGR2HSV)
elif format == 'hsv':
hsv = image
else:
raise ValueError(f'Invalid format: {format}')
mask = cv2.inRange(hsv, self.lower, self.upper)
mask = cv2.bitwise_not(mask)
result = cv2.bitwise_and(image, image, mask=mask)
return result
def __repr__(self) -> str:
return f'HsvColorRemover(for color "{self.name}" with range {self.lower} - {self.upper})'
class HsvColorsRemover(PreprocessorProtocol):
"""去除多个指定范围内的 HSV 颜色。"""
def __init__(
self,
colors: list[tuple[tuple[int, int, int], tuple[int, int, int]]],
*,
name: str | None = None,
):
self.colors = colors
self.name = name
self.__preprocessors = [
HsvColorRemover(color[0], color[1], name=name) for color in colors
]
def process(self, image: MatLike, *, format: ImageFormat = 'bgr') -> MatLike:
if format == 'bgr':
hsv = cv2.cvtColor(image, cv2.COLOR_BGR2HSV)
elif format == 'hsv':
hsv = image
else:
raise ValueError(f'Invalid format: {format}')
for p in self.__preprocessors:
hsv = p.process(hsv, format='hsv')
return hsv
def __repr__(self) -> str:
return f'HsvColorsRemover(for colors {self.colors})'

35
kotonebot/debug_entry.py Normal file
View File

@ -0,0 +1,35 @@
import runpy
import logging
import argparse
from kotonebot.tasks.common import BaseConfig
def run_script(script_path: str) -> None:
"""
使用 runpy 运行指定的 Python 脚本
Args:
script_path: Python 脚本的路径
"""
# 获取模块名
module_name = script_path.strip('.py').replace('\\', '/').strip('/').replace('/', '.')
print(f"正在运行脚本: {script_path}")
# 运行脚本
from kotonebot.backend.context import init_context
logging.basicConfig(level=logging.INFO, format='[%(asctime)s] [%(levelname)s] [%(name)s] [%(funcName)s] [%(lineno)d] %(message)s')
logging.getLogger('kotonebot').setLevel(logging.DEBUG)
init_context(config_type=BaseConfig)
runpy.run_module(module_name, run_name="__main__")
def main():
parser = argparse.ArgumentParser(description='运行指定的 Python 脚本')
parser.add_argument('script_path', help='要运行的 Python 脚本路径')
args = parser.parse_args()
run_script(args.script_path)
if __name__ == '__main__':
main()

View File

@ -5,9 +5,9 @@ from cv2.typing import MatLike
from .. import R
from ..game_ui import WhiteFilter
from kotonebot.util import Countdown, Interval
from kotonebot import device, image, color, user, rect_expand, until, action, sleep, use_screenshot
from kotonebot.util import Interval
from kotonebot.tasks.game_ui import WhiteFilter
from kotonebot import device, image, user, action, use_screenshot
logger = logging.getLogger(__name__)
@ -31,13 +31,13 @@ def handle_unread_commu(img: MatLike | None = None) -> bool:
ret = False
logger.info('Check and skip commu')
img = use_screenshot(img)
skip_btn = image.find(R.Common.ButtonCommuFastforward, preprocessors=[WhiteFilter()])
skip_btn = image.find(R.Common.ButtonCommuSkip, preprocessors=[WhiteFilter()])
if skip_btn is None:
logger.info('No fast forward button found. Not at a commu.')
logger.info('No skip button found. Not at a commu.')
return ret
ret = True
logger.debug('Fast forward button found. Check commu')
logger.debug('Skip button found. Check commu')
it = Interval()
while True:

View File

@ -1,14 +1,9 @@
import logging
from typing import Callable
from .. import R
from .loading import loading
from kotonebot.util import Interval
from ..game_ui import toolbar_home
from kotonebot import device, image, action, cropped, until, sleep
from kotonebot.errors import UnrecoverableError
from kotonebot.tasks.game_ui import toolbar_home
from kotonebot import device, image, action, until, sleep
logger = logging.getLogger(__name__)
@ -78,6 +73,5 @@ def goto_shop():
until(at_daily_shop, critical=True)
if __name__ == "__main__":
import time
goto_home()

View File

@ -0,0 +1,34 @@
import logging
from pathlib import Path
from datetime import datetime, timedelta
from kotonebot import task
logger = logging.getLogger(__name__)
@task('清理日志')
def clear_logs():
"""清理 logs 目录下超过 7 天的日志文件"""
log_dir = Path('logs')
if not log_dir.exists():
return
now = datetime.now()
cutoff_date = now - timedelta(days=7)
logger.info('Clearing logs...')
for file in log_dir.glob('*.log'):
try:
mtime = datetime.fromtimestamp(file.stat().st_mtime)
if mtime < cutoff_date:
file.unlink()
logger.info(f'Removed file {file}.')
except Exception as e:
logger.error(f'Failed to remove {file}: {e}.')
logger.info('Clearing logs done.')
if __name__ == '__main__':
from kotonebot.backend.context import init_context, manual_context
init_context()
manual_context().begin()
clear_logs()

View File

View File

@ -1,9 +1,9 @@
"""收取活动费"""
import logging
from . import R
from .common import conf
from .actions.scenes import at_home, goto_home
from .. import R
from ..common import conf
from ..actions.scenes import at_home, goto_home
from kotonebot import task, device, image, color
logger = logging.getLogger(__name__)

View File

@ -1,9 +1,9 @@
"""领取礼物(邮箱)"""
import logging
from . import R
from .common import conf
from .actions.scenes import at_home, goto_home
from .. import R
from ..common import conf
from ..actions.scenes import at_home, goto_home
from kotonebot import device, image, task, color, rect_expand, sleep
logger = logging.getLogger(__name__)

View File

@ -3,10 +3,10 @@ import logging
from typing import Literal
from datetime import timedelta
from . import R
from .common import conf
from .actions.loading import wait_loading_end
from .actions.scenes import at_home, goto_home
from .. import R
from ..common import conf
from ..actions.loading import wait_loading_end
from ..actions.scenes import at_home, goto_home
from kotonebot import task, device, image, action, ocr, contains, cropped, rect_expand, color, sleep, regex
logger = logging.getLogger(__name__)

View File

@ -1,11 +1,12 @@
"""扭蛋机,支持任意次数的任意扭蛋类型"""
import logging
from kotonebot import task, action, device, image, sleep, Interval
from .. import R
from ..common import conf
from ..game_ui.scrollable import Scrollable
from ..actions.scenes import at_home, goto_home
from kotonebot.backend.image import TemplateMatchResult
from . import R
from .common import conf
from .actions.scenes import at_home, goto_home
from kotonebot import task, action, device, image, sleep, Interval
logger = logging.getLogger(__name__)
@ -107,15 +108,8 @@ def capsule_toys():
draw_capsule_toys(buttons[1], conf().capsule_toys.sense_capsule_toys_count)
# 划到第二页
device.swipe(
R.Daily.CapsuleToys.NextPageStartPoint.x,
R.Daily.CapsuleToys.NextPageStartPoint.y,
R.Daily.CapsuleToys.NextPageEndPoint.x,
R.Daily.CapsuleToys.NextPageEndPoint.y,
duration=2.0 # 划慢点,确保精确定位
# FIXME: adb不支持swipe duration失效
)
sleep(1) # 等待滑动静止由于swipe duration失效所以这里需要手动等待
sc = Scrollable()
sc.next(page=1)
# 处理逻辑扭蛋扭蛋和非凡扭蛋
buttons = get_capsule_toys_draw_buttons()
@ -129,8 +123,4 @@ def capsule_toys():
draw_capsule_toys(buttons[1], conf().capsule_toys.anomaly_capsule_toys_count)
if __name__ == '__main__':
import logging
logging.basicConfig(level=logging.INFO, format='[%(asctime)s] [%(levelname)s] [%(name)s] [%(funcName)s] [%(lineno)d] %(message)s')
logger.setLevel(logging.DEBUG)
capsule_toys()
capsule_toys()

View File

@ -1,10 +1,10 @@
"""领取社团奖励,并尽可能地给其他人送礼物"""
import logging
from . import R
from .common import conf
from .actions.scenes import at_home, goto_home
from kotonebot.tasks.game_ui import toolbar_menu
from .. import R
from ..common import conf
from ..game_ui import toolbar_menu
from ..actions.scenes import at_home, goto_home
from kotonebot import task, device, image, sleep, ocr
logger = logging.getLogger(__name__)

View File

@ -2,11 +2,11 @@
import logging
from gettext import gettext as _
from . import R
from .common import conf
from .game_ui import WhiteFilter
from .actions.scenes import at_home, goto_home
from .actions.loading import wait_loading_end
from .. import R
from ..common import conf
from ..game_ui import WhiteFilter
from ..actions.scenes import at_home, goto_home
from ..actions.loading import wait_loading_end
from kotonebot import device, image, ocr, color, action, task, user, rect_expand, sleep, contains
logger = logging.getLogger(__name__)

View File

@ -1,10 +1,10 @@
"""领取任务奖励"""
import logging
from . import R
from .common import conf, Priority
from .actions.loading import wait_loading_end
from .actions.scenes import at_home, goto_home
from .. import R
from ..common import conf, Priority
from ..actions.loading import wait_loading_end
from ..actions.scenes import at_home, goto_home
from kotonebot import device, image, color, task, action, rect_expand, sleep
logger = logging.getLogger(__name__)

View File

@ -3,12 +3,12 @@ import logging
from typing import Optional
from typing_extensions import deprecated
from . import R
from .common import conf, DailyMoneyShopItems
from .. import R
from ..common import conf, DailyMoneyShopItems
from kotonebot.util import cropped
from kotonebot import task, device, image, ocr, action, sleep
from kotonebot.backend.dispatch import DispatcherContext, SimpleDispatcher, dispatcher
from .actions.scenes import goto_home, goto_shop, at_daily_shop
from ..actions.scenes import goto_home, goto_shop, at_daily_shop
logger = logging.getLogger(__name__)

View File

@ -1,10 +1,11 @@
"""升级一张支援卡,优先升级低等级支援卡"""
import logging
from .. import R
from ..common import conf
from ..game_ui.scrollable import Scrollable
from ..actions.scenes import at_home, goto_home
from kotonebot import task, device, image, sleep
from . import R
from .common import conf
from .actions.scenes import at_home, goto_home
logger = logging.getLogger(__name__)
@ -31,19 +32,8 @@ def upgrade_support_card():
device.click(image.expect_wait(R.Common.ButtonIdolSupportCard, timeout=5))
sleep(2)
# TODO: 将下面硬编码的值塞到合适的地方
# 往下滑,划到最底部
for _ in range(5):
device.swipe(
R.Daily.SupportCard.DragDownStartPoint.x,
R.Daily.SupportCard.DragDownStartPoint.y,
R.Daily.SupportCard.DragDownEndPoint.x,
R.Daily.SupportCard.DragDownEndPoint.y,
duration=1.0
)
sleep(0.1)
sleep(1.5)
Scrollable().to(1)
# 点击左上角第一张支援卡
# 点击位置百分比: (0.18, 0.34)

View File

@ -1,246 +0,0 @@
from dataclasses import dataclass
from typing import Literal, NamedTuple, overload
import cv2
import numpy as np
from cv2.typing import MatLike
from kotonebot.backend.image import TemplateMatchResult
from . import R
from kotonebot import action, device, color, image, ocr, sleep
from kotonebot.backend.color import HsvColor
from kotonebot.util import Rect
from kotonebot.backend.core import HintBox, Image
from kotonebot.backend.preprocessor import HsvColorFilter
@action('按钮是否禁用', screenshot_mode='manual-inherit')
def button_state(*, target: Image | None = None, rect: Rect | None = None) -> bool | None:
"""
判断按钮是否处于禁用状态
:param rect: 按钮的矩形区域必须包括文字或图标部分
:param target: 按钮目标模板
"""
img = device.screenshot()
if rect is not None:
_rect = rect
elif target is not None:
result = image.find(target)
if result is None:
return None
_rect = result.rect
else:
raise ValueError('Either rect or target must be provided.')
if color.find('#babcbd', rect=_rect):
return False
elif color.find('#ffffff', rect=_rect):
return True
else:
raise ValueError(f'Unknown button state: {img}')
def web2cv(hsv: HsvColor):
return (int(hsv[0]/360*180), int(hsv[1]/100*255), int(hsv[2]/100*255))
WHITE_LOW = (0, 0, 200)
WHITE_HIGH = (180, 30, 255)
PINK_TARGET = (335, 78, 95)
PINK_LOW = (300, 70, 90)
PINK_HIGH = (350, 80, 100)
BLUE_TARGET = (210, 88, 93)
BLUE_LOW = (200, 80, 90)
BLUE_HIGH = (220, 90, 100)
YELLOW_TARGET = (39, 81, 97)
YELLOW_LOW = (30, 70, 90)
YELLOW_HIGH = (45, 90, 100)
DEFAULT_COLORS = [
(web2cv(PINK_LOW), web2cv(PINK_HIGH)),
(web2cv(YELLOW_LOW), web2cv(YELLOW_HIGH)),
(web2cv(BLUE_LOW), web2cv(BLUE_HIGH)),
]
def filter_rectangles(
img: MatLike,
color_ranges: tuple[HsvColor, HsvColor],
aspect_ratio_threshold: float,
area_threshold: int,
rect: Rect | None = None
) -> list[Rect]:
"""
过滤出指定颜色并执行轮廓查找返回符合要求的轮廓的 bound box
返回结果按照 y 坐标排序
"""
img_hsv =cv2.cvtColor(img, cv2.COLOR_BGR2HSV)
white_mask = cv2.inRange(img_hsv, np.array(color_ranges[0]), np.array(color_ranges[1]))
contours, _ = cv2.findContours(white_mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
result_rects = []
for contour in contours:
x, y, w, h = cv2.boundingRect(contour)
# 如果不在指定范围内,跳过
if rect is not None:
rect_x1, rect_y1, rect_w, rect_h = rect
rect_x2 = rect_x1 + rect_w
rect_y2 = rect_y1 + rect_h
if not (
x >= rect_x1 and
y >= rect_y1 and
x + w <= rect_x2 and
y + h <= rect_y2
):
continue
aspect_ratio = w / h
area = cv2.contourArea(contour)
if aspect_ratio >= aspect_ratio_threshold and area >= area_threshold:
result_rects.append((x, y, w, h))
result_rects.sort(key=lambda x: x[1])
return result_rects
@dataclass
class EventButton:
rect: Rect
selected: bool
description: str
title: str
# 参考图片:
# [screenshots/produce/action_study3.png]
# TODO: CommuEventButtonUI 需要能够识别不可用的按钮
class CommuEventButtonUI:
"""
此类用于识别培育中交流中出现的事件/效果里的按钮
例如外出おでかけ冲刺周课程选择这两个页面的选择按钮
"""
def __init__(
self,
selected_colors: list[tuple[HsvColor, HsvColor]] = DEFAULT_COLORS,
rect: HintBox = R.InPurodyuusu.BoxCommuEventButtonsArea
):
"""
:param selected_colors: 按钮选中后的主题色
:param rect: 识别范围
"""
self.color_ranges = selected_colors
self.rect = rect
@action('交流事件按钮.识别选中', screenshot_mode='manual-inherit')
def selected(self, description: bool = True, title: bool = False) -> EventButton | None:
img = device.screenshot()
for i, color_range in enumerate(self.color_ranges):
rects = filter_rectangles(img, color_range, 7, 500, rect=self.rect)
if len(rects) > 0:
desc_text = self.description() if description else ''
title_text = ocr.ocr(rect=rects[0]).squash().text if title else ''
return EventButton(rects[0], True, desc_text, title_text)
return None
@action('交流事件按钮.识别按钮', screenshot_mode='manual-inherit')
def all(self, description: bool = True, title: bool = False) -> list[EventButton]:
"""
识别所有按钮的位置以及选中后的描述文本
前置条件当前显示了交流事件按钮\n
结束状态-
:param description: 是否识别描述文本
:param title: 是否识别标题
"""
img = device.screenshot()
rects = filter_rectangles(img, (WHITE_LOW, WHITE_HIGH), 7, 500, rect=self.rect)
if not rects:
return []
selected = self.selected()
result: list[EventButton] = []
for rect in rects:
desc_text = ''
title_text = ''
if title:
title_text = ocr.ocr(rect=rect).squash().text
if description:
device.click(rect)
sleep(0.15)
device.screenshot()
desc_text = self.description()
result.append(EventButton(rect, False, desc_text, title_text))
# 修改最后一次点击的按钮为 selected 状态
if len(result) > 0:
result[-1].selected = True
if selected is not None:
result.append(selected)
selected.selected = False
result.sort(key=lambda x: x.rect[1])
return result
@action('交流事件按钮.识别描述', screenshot_mode='manual-inherit')
def description(self) -> str:
"""
识别当前选中按钮的描述文本
前置条件有选中按钮\n
结束状态-
"""
img = device.screenshot()
rects = filter_rectangles(img, (WHITE_LOW, WHITE_HIGH), 3, 1000, rect=self.rect)
rects.sort(key=lambda x: x[1])
# TODO: 这里 rects 可能为空,需要加入判断重试
ocr_result = ocr.raw().ocr(img, rect=rects[0])
return ocr_result.squash().text
class WhiteFilter(HsvColorFilter):
"""
匹配时只匹配图像和模板中的白色部分
此类用于识别空心/透明背景的白色图标或文字
"""
def __init__(self):
super().__init__(WHITE_LOW, WHITE_HIGH)
@overload
def toolbar_home(critical: Literal[False] = False) -> TemplateMatchResult | None:
"""寻找工具栏上的首页按钮。"""
...
@overload
def toolbar_home(critical: Literal[True]) -> TemplateMatchResult:
"""寻找工具栏上的首页按钮。若未找到,则抛出异常。"""
...
@action('工具栏按钮.寻找首页', screenshot_mode='manual-inherit')
def toolbar_home(critical: bool = False):
device.screenshot()
if critical:
return image.expect_wait(R.Common.ButtonToolbarHome, preprocessors=[WhiteFilter()])
else:
return image.find(R.Common.ButtonToolbarHome, preprocessors=[WhiteFilter()])
@overload
def toolbar_menu(critical: Literal[False] = False) -> TemplateMatchResult | None:
"""寻找工具栏上的菜单按钮。"""
...
@overload
def toolbar_menu(critical: Literal[True]) -> TemplateMatchResult:
"""寻找工具栏上的菜单按钮。若未找到,则抛出异常。"""
...
@action('工具栏按钮.寻找菜单', screenshot_mode='manual-inherit')
def toolbar_menu(critical: bool = False):
device.screenshot()
if critical:
return image.expect_wait(R.Common.ButtonToolbarMenu, preprocessors=[WhiteFilter()])
else:
return image.find(R.Common.ButtonToolbarMenu, preprocessors=[WhiteFilter()])
if __name__ == '__main__':
from pprint import pprint as print
from kotonebot.backend.context import init_context, manual_context, device
init_context()
manual_context().begin()
print(toolbar_home())

View File

@ -0,0 +1,3 @@
from .toolbar import toolbar_home, toolbar_menu
from .commu_event_buttons import CommuEventButtonUI
from .common import WhiteFilter

View File

@ -0,0 +1,101 @@
from dataclasses import dataclass
from typing import Literal, overload
import cv2
import numpy as np
from cv2.typing import MatLike
from kotonebot.backend.image import TemplateMatchResult
from kotonebot.tasks import R
from kotonebot import action, color, image
from kotonebot.backend.color import HsvColor
from kotonebot.util import Rect
from kotonebot.backend.core import Image
from kotonebot.backend.preprocessor import HsvColorFilter
def filter_rectangles(
img: MatLike,
color_ranges: tuple[HsvColor, HsvColor],
aspect_ratio_threshold: float,
area_threshold: int,
rect: Rect | None = None
) -> list[Rect]:
"""
过滤出指定颜色并执行轮廓查找返回符合要求的轮廓的 bound box
返回结果按照 y 坐标排序
"""
img_hsv =cv2.cvtColor(img, cv2.COLOR_BGR2HSV)
white_mask = cv2.inRange(img_hsv, np.array(color_ranges[0]), np.array(color_ranges[1]))
contours, _ = cv2.findContours(white_mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
result_rects = []
for contour in contours:
x, y, w, h = cv2.boundingRect(contour)
# 如果不在指定范围内,跳过
if rect is not None:
rect_x1, rect_y1, rect_w, rect_h = rect
rect_x2 = rect_x1 + rect_w
rect_y2 = rect_y1 + rect_h
if not (
x >= rect_x1 and
y >= rect_y1 and
x + w <= rect_x2 and
y + h <= rect_y2
):
continue
aspect_ratio = w / h
area = cv2.contourArea(contour)
if aspect_ratio >= aspect_ratio_threshold and area >= area_threshold:
result_rects.append((x, y, w, h))
result_rects.sort(key=lambda x: x[1])
return result_rects
@action('按钮是否禁用', screenshot_mode='manual-inherit')
def button_state(*, target: Image | None = None, rect: Rect | None = None) -> bool | None:
"""
判断按钮是否处于禁用状态
:param rect: 按钮的矩形区域必须包括文字或图标部分
:param target: 按钮目标模板
"""
img = device.screenshot()
if rect is not None:
_rect = rect
elif target is not None:
result = image.find(target)
if result is None:
return None
_rect = result.rect
else:
raise ValueError('Either rect or target must be provided.')
if color.find('#babcbd', rect=_rect):
return False
elif color.find('#ffffff', rect=_rect):
return True
else:
raise ValueError(f'Unknown button state: {img}')
WHITE_LOW = (0, 0, 200)
WHITE_HIGH = (180, 30, 255)
class WhiteFilter(HsvColorFilter):
"""
匹配时只匹配图像和模板中的白色部分
此类用于识别空心/透明背景的白色图标或文字
"""
def __init__(self):
super().__init__(WHITE_LOW, WHITE_HIGH)
if __name__ == '__main__':
from pprint import pprint as print
from kotonebot.backend.context import init_context, manual_context, device
init_context()
manual_context().begin()

View File

@ -0,0 +1,121 @@
from dataclasses import dataclass
from kotonebot.tasks import R
from kotonebot.backend.core import HintBox
from kotonebot.backend.color import HsvColor
from kotonebot import action, device, ocr, sleep, Rect
from .common import filter_rectangles, WHITE_LOW, WHITE_HIGH
@dataclass
class EventButton:
rect: Rect
selected: bool
description: str
title: str
def web2cv(hsv: HsvColor):
return (int(hsv[0]/360*180), int(hsv[1]/100*255), int(hsv[2]/100*255))
PINK_TARGET = (335, 78, 95)
PINK_LOW = (300, 70, 90)
PINK_HIGH = (350, 80, 100)
BLUE_TARGET = (210, 88, 93)
BLUE_LOW = (200, 80, 90)
BLUE_HIGH = (220, 90, 100)
YELLOW_TARGET = (39, 81, 97)
YELLOW_LOW = (30, 70, 90)
YELLOW_HIGH = (45, 90, 100)
DEFAULT_COLORS = [
(web2cv(PINK_LOW), web2cv(PINK_HIGH)),
(web2cv(YELLOW_LOW), web2cv(YELLOW_HIGH)),
(web2cv(BLUE_LOW), web2cv(BLUE_HIGH)),
]
# 参考图片:
# [screenshots/produce/action_study3.png]
# TODO: CommuEventButtonUI 需要能够识别不可用的按钮
class CommuEventButtonUI:
"""
此类用于识别培育中交流中出现的事件/效果里的按钮
例如外出おでかけ冲刺周课程选择这两个页面的选择按钮
"""
def __init__(
self,
selected_colors: list[tuple[HsvColor, HsvColor]] = DEFAULT_COLORS,
rect: HintBox = R.InPurodyuusu.BoxCommuEventButtonsArea
):
"""
:param selected_colors: 按钮选中后的主题色
:param rect: 识别范围
"""
self.color_ranges = selected_colors
self.rect = rect
@action('交流事件按钮.识别选中', screenshot_mode='manual-inherit')
def selected(self, description: bool = True, title: bool = False) -> EventButton | None:
img = device.screenshot()
for i, color_range in enumerate(self.color_ranges):
rects = filter_rectangles(img, color_range, 7, 500, rect=self.rect)
if len(rects) > 0:
desc_text = self.description() if description else ''
title_text = ocr.ocr(rect=rects[0]).squash().text if title else ''
return EventButton(rects[0], True, desc_text, title_text)
return None
@action('交流事件按钮.识别按钮', screenshot_mode='manual-inherit')
def all(self, description: bool = True, title: bool = False) -> list[EventButton]:
"""
识别所有按钮的位置以及选中后的描述文本
前置条件当前显示了交流事件按钮\n
结束状态-
:param description: 是否识别描述文本
:param title: 是否识别标题
"""
img = device.screenshot()
rects = filter_rectangles(img, (WHITE_LOW, WHITE_HIGH), 7, 500, rect=self.rect)
if not rects:
return []
selected = self.selected()
result: list[EventButton] = []
for rect in rects:
desc_text = ''
title_text = ''
if title:
title_text = ocr.ocr(rect=rect).squash().text
if description:
device.click(rect)
sleep(0.15)
device.screenshot()
desc_text = self.description()
result.append(EventButton(rect, False, desc_text, title_text))
# 修改最后一次点击的按钮为 selected 状态
if len(result) > 0:
result[-1].selected = True
if selected is not None:
result.append(selected)
selected.selected = False
result.sort(key=lambda x: x.rect[1])
return result
@action('交流事件按钮.识别描述', screenshot_mode='manual-inherit')
def description(self) -> str:
"""
识别当前选中按钮的描述文本
前置条件有选中按钮\n
结束状态-
"""
img = device.screenshot()
rects = filter_rectangles(img, (WHITE_LOW, WHITE_HIGH), 3, 1000, rect=self.rect)
rects.sort(key=lambda x: x[1])
# TODO: 这里 rects 可能为空,需要加入判断重试
ocr_result = ocr.raw().ocr(img, rect=rects[0])
return ocr_result.squash().text

View File

@ -0,0 +1,323 @@
import logging
import time
from typing import Literal
import cv2
import numpy as np
from cv2.typing import MatLike, Rect
from kotonebot import device, color, action
from kotonebot.backend.core import HintBox
from kotonebot.backend.preprocessor import HsvColorFilter
logger = logging.getLogger(__name__)
# 暗色系滚动条阈值。bitwise_not = True
# 例:金币商店、金币扭蛋页面
THRESHOLD_DARK_FULL = 240 # 滚动条+滚动条背景
THRESHOLD_DARK_FOREGROUND = 190 # 仅滚动条
# 亮色系滚动条阈值。bitwise_not = False
# 例:每日任务、音乐播放器选歌页面
THRESHOLD_LIGHT_FULL = 140 # 滚动条+滚动条背景(效果不佳)
THRESHOLD_LIGHT_FOREGROUND = 220 # 仅滚动条
def find_scroll_bar(img: MatLike, threshold: int, bitwise_not: bool = False) -> Rect | None:
"""
寻找给定图像中的滚动条
基于二值化+轮廓查找实现
:param img: 输入图像图像必须中存在滚动条否则无法保证结果是什么
:param threshold: 二值化阈值
:param bitwise_not: 是否对二值化结果取反
:return: 滚动条的矩形区域 `(x, y, w, h)`如果未找到则返回 None
"""
# 灰度、二值化、查找轮廓
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
_, binary = cv2.threshold(gray, threshold, 255, cv2.THRESH_BINARY)
if bitwise_not:
binary = cv2.bitwise_not(binary)
# cv2.imshow('binary', binary)
contours, _ = cv2.findContours(binary, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
# 找出所有可能是滚动条的轮廓:
# 宽高比 < 0.5,且形似矩形
filtered_contours = []
for contour in contours:
x, y, w, h = cv2.boundingRect(contour)
contour_area = cv2.contourArea(contour)
rect_area = w * h
if w/h < 0.5 and contour_area / rect_area > 0.6:
filtered_contours.append((contour, (x, y, w, h)))
# 找出最长的轮廓
if filtered_contours:
longest_contour = max(filtered_contours, key=lambda c: c[1][3])
return longest_contour[1]
return None
def find_scroll_bar2(img: MatLike) -> Rect | None:
"""
寻找给定图像中的滚动条
基于边缘检测+轮廓查找实现
:param img: 输入图像图像必须中存在滚动条否则无法保证结果是什么
:return: 滚动条的矩形区域 `(x, y, w, h)`如果未找到则返回 None
"""
# 高斯模糊、边缘检测
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
gray = cv2.GaussianBlur(gray, (5, 5), 0)
edges = cv2.Canny(gray, 50, 70)
# cv2.imshow('edges', cv2.resize(edges, (0, 0), fx=0.5, fy=0.5))
# 膨胀
kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (3, 3))
dilated = cv2.dilate(edges, kernel, iterations=1)
# cv2.imshow('dilated', cv2.resize(dilated, (0, 0), fx=0.5, fy=0.5))
# 轮廓检测
contours, _ = cv2.findContours(dilated, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
# 找出最可能是滚动条的轮廓:
# 宽高比 < 0.5,且形似矩形,且最长
rects = []
for contour in contours:
x, y, w, h = cv2.boundingRect(contour)
contour_area = cv2.contourArea(contour)
rect_area = w * h
if w/h < 0.5 and contour_area / rect_area > 0.6:
rects.append((x, y, w, h))
if rects:
longest_rect = max(rects, key=lambda r: r[2] * r[3])
return longest_rect
return None
class ScrollableIterator:
def __init__(self, scrollable: 'Scrollable', delta_pixels: int):
self.scrollable = scrollable
self.delta_pixels = delta_pixels
def __iter__(self):
return self
def __next__(self):
if self.scrollable.position >= 1:
raise StopIteration
self.scrollable.by(pixels=self.delta_pixels)
return self.scrollable.position
class Scrollable:
"""
此类用于处理游戏内的可滚动容器
```python
sc = Scrollable()
sc.to(0) # 滚动到最开始
sc.to(0.5) # 滚动到中间
sc.to(1) # 滚动到最后
sc.by(0.1) # 滚动10%
sc.by(pixels=100) # 滚动100px
sc.page_count # 滚动页数
sc.position # 当前滚动位置
# 以步长 10% 开始滚动,直到滚动到最后
for _ in sc(0.1):
print(sc.position)
```
"""
def __init__(
self,
scrollbar_rect: HintBox | None = None,
color_schema: Literal['light', 'dark'] = 'light',
*,
at_start_threshold: float = 0.01,
at_end_threshold: float = 0.99,
auto_update: bool = True
):
"""
:param auto_update: 在每次滑动后是否自动更新滚动数据
"""
self.color_schema = color_schema
self.scrollbar_rect = scrollbar_rect
self.position: float = 0
"""当前滚动位置。范围 [0, 1]"""
self.thumb_height: int | None = None
"""滚动条把手高度"""
self.thumb_position: tuple[int, int] | None = None
"""滚动条把手位置"""
self.track_position: tuple[int, int] | None = None
"""滚动轨道位置"""
self.track_height: int | None = None
"""滚动轨道高度"""
self.page_count: int | None = None
"""滚动页数"""
self.auto_update = auto_update
"""是否自动更新滚动数据"""
self.at_start_threshold = at_start_threshold
self.at_end_threshold = at_end_threshold
if color_schema == 'dark':
raise NotImplementedError('Dark color schema is not implemented yet.')
@action('滚动.更新数据', screenshot_mode='manual-inherit')
def update(self) -> bool:
"""
立即更新滚动数据
:return: 是否更新成功
"""
img = device.screenshot()
if self.scrollbar_rect is None:
logger.debug('Finding scrollbar rect...')
self.scrollbar_rect = find_scroll_bar2(img)
if self.scrollbar_rect is None:
logger.warning('Unable to find scrollbar. (1)')
return False
logger.debug('Scrollbar rect found.')
x, y, w, h = self.scrollbar_rect
scroll_img = img[y:y+h, x:x+w]
# 灰度、二值化
gray = cv2.cvtColor(scroll_img, cv2.COLOR_BGR2GRAY)
_, binary = cv2.threshold(gray, 150, 255, cv2.THRESH_BINARY)
# 0 = 滚动条255 = 背景
# 计算滚动位置
positions = np.where(binary == 0)[0]
if len(positions) > 0:
self.track_position = (int(x), int(y))
self.track_height = int(h)
self.thumb_height = int(positions[-1] - positions[0])
self.thumb_position = (int(x), int(y + positions[0]))
self.position = float(positions[-1] / h)
self.page_count = int(h / self.thumb_height)
logger.debug(f'Scrollbar height: {self.thumb_height}, position: {self.position}')
if self.position < self.at_start_threshold:
self.position = 0
elif self.position > self.at_end_threshold:
self.position = 1
return True
else:
logger.warning('Unable to find scrollbar. (2)')
return False
@action('滚动.下一页', screenshot_mode='manual-inherit')
def next(self, *, page: float) -> bool:
"""
滚动到下一页
:param page: 滚动页数
:return: 是否滚动成功
"""
logger.debug('Scrolling to next page.')
if not self.thumb_height:
self.update()
if not self.thumb_height or not self.thumb_position:
logger.warning('Unable to update scrollbar data.')
return False
if self.position >= 1:
logger.debug('Already at the end of the scrollbar.')
return False
delta = int(self.thumb_height * page)
self.by(pixels=delta)
return True
@action('滚动.滚动', screenshot_mode='manual-inherit')
def by(self, percentage: float | None = None, *, pixels: int | None = None) -> bool:
"""
滚动指定距离
:param percentage: 滚动距离范围 [-1, 1]
:param pixels: 滚动距离单位为像素此参数优先级高于 percentage
:return: 是否滚动成功
"""
if percentage is not None and (percentage > 1 or percentage < -1):
raise ValueError('percentage must be in range [-1, 1].')
if pixels is not None and pixels < 0:
raise ValueError('pixels must be positive.')
if not self.thumb_height or not self.thumb_position or not self.track_height:
self.update()
if not self.thumb_height or not self.thumb_position or not self.track_height:
logger.warning('Unable to update scrollbar data.')
return False
x, src_y = self.thumb_position
src_y += self.thumb_height // 2
if pixels is not None:
dst_y = src_y + pixels
logger.debug(f'Scrolling by {pixels} px...')
elif percentage is not None:
logger.debug(f'Scrolling by {percentage}...')
dst_y = src_y + int(self.track_height * percentage)
else:
raise ValueError('Either percentage or pixels must be provided.')
device.swipe(x, src_y, x, dst_y, 0.3)
time.sleep(0.2)
if self.auto_update:
self.update()
return True
@action('滚动.滚动到', screenshot_mode='manual-inherit')
def to(self, position: float) -> bool:
"""
滚动到指定位置
:param position: 目标位置范围 [0, 1]
:return: 是否滚动成功
"""
if position > 1 or position < 0:
raise ValueError('position must be in range [0, 1].')
logger.debug(f'Scrolling to {position}...')
if not self.thumb_height or not self.thumb_position or not self.track_height or not self.track_position:
self.update()
if not self.thumb_height or not self.thumb_position or not self.track_height or not self.track_position:
logger.warning('Unable to update scrollbar data.')
return False
x, y = self.track_position
tx, ty = self.thumb_position
ty += self.thumb_height // 2
target_y = y + int(self.track_height * position)
device.swipe(tx, ty, x, target_y, 0.3)
time.sleep(0.2)
if self.auto_update:
self.update()
return True
def __call__(self, step_percentage: float) -> ScrollableIterator:
"""
以指定步长滚动
:param step_percentage: 步长范围 [-1, 1]
:return: 一个迭代器迭代时滚动指定步长
"""
if not self.track_height:
self.update()
if not self.track_height:
raise ValueError('Unable to update scrollbar data.')
return ScrollableIterator(self, int(self.track_height * step_percentage))
if __name__ == '__main__':
from kotonebot.backend.context import init_context, manual_context, device
init_context()
manual_context().begin()
from kotonebot import device
import cv2
from kotonebot.util import cv2_imread
import time
logger.setLevel(logging.DEBUG)
device.screenshot()
sc = Scrollable(color_schema='light')
sc.update()
sc.to(0)
print(sc.page_count)
pg = sc.page_count
assert pg is not None
for _ in sc(4 / (pg * 12) * 0.8):
print(sc.position)
cv2.waitKey(0)
cv2.destroyAllWindows()

View File

@ -0,0 +1,43 @@
from typing import Literal, overload
from kotonebot.backend.image import TemplateMatchResult
from kotonebot.tasks import R
from .common import WhiteFilter
from kotonebot import action, device, image
@overload
def toolbar_home(critical: Literal[False] = False) -> TemplateMatchResult | None:
"""寻找工具栏上的首页按钮。"""
...
@overload
def toolbar_home(critical: Literal[True]) -> TemplateMatchResult:
"""寻找工具栏上的首页按钮。若未找到,则抛出异常。"""
...
@action('工具栏按钮.寻找首页', screenshot_mode='manual-inherit')
def toolbar_home(critical: bool = False):
device.screenshot()
if critical:
return image.expect_wait(R.Common.ButtonToolbarHome, preprocessors=[WhiteFilter()])
else:
return image.find(R.Common.ButtonToolbarHome, preprocessors=[WhiteFilter()])
@overload
def toolbar_menu(critical: Literal[False] = False) -> TemplateMatchResult | None:
"""寻找工具栏上的菜单按钮。"""
...
@overload
def toolbar_menu(critical: Literal[True]) -> TemplateMatchResult:
"""寻找工具栏上的菜单按钮。若未找到,则抛出异常。"""
...
@action('工具栏按钮.寻找菜单', screenshot_mode='manual-inherit')
def toolbar_menu(critical: bool = False):
device.screenshot()
if critical:
return image.expect_wait(R.Common.ButtonToolbarMenu, preprocessors=[WhiteFilter()])
else:
return image.find(R.Common.ButtonToolbarMenu, preprocessors=[WhiteFilter()])

View File

View File

@ -1,23 +1,19 @@
from typing import Literal
from logging import getLogger
from kotonebot.backend.preprocessor import HsvColorFilter
from kotonebot.tasks.actions.loading import loading
from .. import R
from kotonebot import (
ocr,
device,
contains,
image,
regex,
action,
sleep,
Interval,
)
from ..game_ui import CommuEventButtonUI, WhiteFilter
from kotonebot.tasks.game_ui import WhiteFilter, CommuEventButtonUI
from .pdorinku import acquire_pdorinku
from kotonebot.backend.dispatch import SimpleDispatcher
from kotonebot.tasks.actions.commu import handle_unread_commu
from kotonebot.util import measure_time

View File

@ -1,4 +1,3 @@
import math
import time
import logging
from typing_extensions import deprecated, assert_never
@ -10,19 +9,19 @@ import numpy as np
from cv2.typing import MatLike
from .. import R
from . import loading
from .scenes import at_home
from ..actions import loading
from ..actions.scenes import at_home
from ..util.trace import trace
from ..game_ui import WhiteFilter
from .commu import handle_unread_commu
from ..actions.commu import handle_unread_commu
from ..common import ProduceAction, RecommendCardDetectionMode, conf
from kotonebot.errors import UnrecoverableError
from kotonebot.backend.context.context import use_screenshot
from .common import until_acquisition_clear, acquisitions, commut_event
from kotonebot.util import AdaptiveWait, Countdown, Interval, crop, cropped
from ..produce.common import until_acquisition_clear, acquisitions, commut_event
from kotonebot.util import Countdown, Interval, crop, cropped
from kotonebot.backend.dispatch import DispatcherContext, SimpleDispatcher
from kotonebot import ocr, device, contains, image, regex, action, sleep, color, Rect, wait
from .non_lesson_actions import (
from ..produce.non_lesson_actions import (
enter_allowance, allowance_available,
study_available, enter_study,
is_rest_available, rest,
@ -418,10 +417,16 @@ def until_action_scene(week_first: bool = False):
R.InPurodyuusu.ButtonFinalPracticeDance # 离考试剩余一周
]):
logger.info("Action scene not detected. Retry...")
if acquisitions():
continue
# commu_event 和 acquisitions 顺序不能颠倒。
# 在 PRO 培育初始饮料、技能卡二选一事件时,右下方的
# 快进按钮会被视为交流。如果先执行 acquisitions()
# 会因为命中交流而 continuecommut_event() 永远
# 不会执行。
# [screenshots/produce/in_produce/initial_commu_event.png]
if week_first and commut_event():
continue
if acquisitions():
continue
sleep(0.2)
else:
logger.info("Now at action scene.")
@ -1168,7 +1173,6 @@ if __name__ == '__main__':
file_handler.setFormatter(logging.Formatter('[%(asctime)s] [%(levelname)s] [%(name)s] %(message)s'))
logging.getLogger().addHandler(file_handler)
from kotonebot.util import Profiler
from kotonebot.backend.context import init_context, manual_context
from ..common import BaseConfig
from kotonebot.backend.debug import debug

View File

@ -5,11 +5,11 @@
"""
from logging import getLogger
from kotonebot.tasks.common import conf
from .. import R
from .common import acquisitions
from ..game_ui import CommuEventButtonUI
from ..common import conf
from ..produce.common import acquisitions
from ..game_ui.commu_event_buttons import CommuEventButtonUI
from kotonebot.util import Interval
from kotonebot.errors import UnrecoverableError
from kotonebot import device, image, action, sleep
@ -67,7 +67,7 @@ def enter_study():
elif target == 'vocal':
logger.debug("Clicking on lesson vocal.")
device.double_click(image.expect(R.InPurodyuusu.TextSelfStudyVocal))
from .in_purodyuusu import until_practice_scene, practice
from ..produce.in_purodyuusu import until_practice_scene, practice
logger.info("Entering practice scene.")
until_practice_scene()
logger.info("Executing practice.")

View File

@ -3,15 +3,16 @@ from itertools import cycle
from typing import Optional, Literal
from kotonebot.backend.context.context import wait
from kotonebot.tasks.game_ui.scrollable import Scrollable
from kotonebot.ui import user
from kotonebot.util import Countdown, Interval
from kotonebot.backend.dispatch import SimpleDispatcher
from . import R
from .common import conf, PIdol
from .actions.scenes import at_home, goto_home
from .actions.in_purodyuusu import hajime_pro, hajime_regular, resume_regular_produce
from kotonebot import device, image, ocr, task, action, sleep, equals, contains
from .. import R
from ..common import conf, PIdol
from ..actions.scenes import at_home, goto_home
from ..produce.in_purodyuusu import hajime_pro, hajime_regular, resume_regular_produce
from kotonebot import device, image, ocr, task, action, sleep, contains
logger = logging.getLogger(__name__)
@ -70,7 +71,7 @@ def select_idol(target_titles: list[str] | PIdol):
found = False
max_tries = 5
tries = 0
# TODO: 加入 ScrollBar 类,判断滚动条进度
sc = Scrollable()
# 找到目标偶像
while not found:
# 首先检查当前选中的是不是已经是目标
@ -90,8 +91,9 @@ def select_idol(target_titles: list[str] | PIdol):
if tries > max_tries:
break
# 翻页
device.swipe(x1=100, x2=100, y1=max_y, y2=min_y)
sleep(2)
# device.swipe(x1=100, x2=100, y1=max_y, y2=min_y)
sc.next(page=0.8)
sleep(0.4)
device.screenshot()
results = image.find_all(R.Produce.IconPIdolLevel)
results.sort(key=lambda r: tuple(r.position))

View File

@ -7,6 +7,7 @@ import cProfile
from importlib import resources
from functools import lru_cache
from typing import Literal, Callable, TYPE_CHECKING, TypeGuard
from typing_extensions import deprecated
import cv2
from cv2.typing import MatLike
@ -30,6 +31,7 @@ def is_rect(rect: typing.Any) -> TypeGuard[Rect]:
def is_point(point: typing.Any) -> TypeGuard[Point]:
return isinstance(point, typing.Sequence) and len(point) == 2 and all(isinstance(i, int) for i in point)
@deprecated('使用 HintBox 类与 Devtool 工具替代')
def crop(img: MatLike, /, x1: float = 0, y1: float = 0, x2: float = 1, y2: float = 1) -> MatLike:
"""
按比例裁剪图像
@ -47,6 +49,7 @@ def crop(img: MatLike, /, x1: float = 0, y1: float = 0, x2: float = 1, y2: float
y2_px = int(h * y2)
return img[y1_px:y2_px, x1_px:x2_px]
@deprecated('使用 numpy 的切片替代')
def crop_rect(img: MatLike, rect: Rect) -> MatLike:
"""
按范围裁剪图像
@ -89,6 +92,7 @@ class DeviceHookContextManager:
if self.click_hook_before is not None:
self.device.click_hooks_before.remove(self.click_hook_before)
@deprecated('使用 HintBox 类与 Devtool 工具替代')
def cropped(
device: 'Device',
x1: float = 0,

Binary file not shown.

After

Width:  |  Height:  |  Size: 758 KiB