mirror of
https://github.com/LmeSzinc/StarRailCopilot.git
synced 2024-11-29 19:08:50 +00:00
commit
8f2bfa625d
BIN
assets/character/FuXuan.png
Normal file
BIN
assets/character/FuXuan.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 19 KiB |
BIN
assets/character/Lynx.png
Normal file
BIN
assets/character/Lynx.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 19 KiB |
BIN
assets/mask/MASK_MAP_INTERACT.png
Normal file
BIN
assets/mask/MASK_MAP_INTERACT.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 4.1 KiB |
BIN
assets/share/daily/reward/MASK_DAILY_QUEST.png
Normal file
BIN
assets/share/daily/reward/MASK_DAILY_QUEST.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 7.9 KiB |
Binary file not shown.
Before Width: | Height: | Size: 246 KiB After Width: | Height: | Size: 226 KiB |
@ -152,20 +152,21 @@ class ModuleBase:
|
|||||||
prev_image = image
|
prev_image = image
|
||||||
timer.reset()
|
timer.reset()
|
||||||
|
|
||||||
def image_crop(self, button):
|
def image_crop(self, button, copy=True):
|
||||||
"""Extract the area from image.
|
"""Extract the area from image.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
button(Button, tuple): Button instance or area tuple.
|
button(Button, tuple): Button instance or area tuple.
|
||||||
|
copy:
|
||||||
"""
|
"""
|
||||||
if isinstance(button, Button):
|
if isinstance(button, Button):
|
||||||
return crop(self.device.image, button.area)
|
return crop(self.device.image, button.area, copy=copy)
|
||||||
elif isinstance(button, ButtonWrapper):
|
elif isinstance(button, ButtonWrapper):
|
||||||
return crop(self.device.image, button.area)
|
return crop(self.device.image, button.area, copy=copy)
|
||||||
elif hasattr(button, 'area'):
|
elif hasattr(button, 'area'):
|
||||||
return crop(self.device.image, button.area)
|
return crop(self.device.image, button.area, copy=copy)
|
||||||
else:
|
else:
|
||||||
return crop(self.device.image, button)
|
return crop(self.device.image, button, copy=copy)
|
||||||
|
|
||||||
def image_color_count(self, button, color, threshold=221, count=50):
|
def image_color_count(self, button, color, threshold=221, count=50):
|
||||||
"""
|
"""
|
||||||
@ -178,9 +179,14 @@ class ModuleBase:
|
|||||||
Returns:
|
Returns:
|
||||||
bool:
|
bool:
|
||||||
"""
|
"""
|
||||||
image = self.image_crop(button)
|
if isinstance(button, np.ndarray):
|
||||||
mask = color_similarity_2d(image, color=color) > threshold
|
image = button
|
||||||
return np.sum(mask) > count
|
else:
|
||||||
|
image = self.image_crop(button, copy=False)
|
||||||
|
mask = color_similarity_2d(image, color=color)
|
||||||
|
cv2.inRange(mask, threshold, 255, dst=mask)
|
||||||
|
sum_ = cv2.countNonZero(mask)
|
||||||
|
return sum_ > count
|
||||||
|
|
||||||
def image_color_button(self, area, color, color_threshold=250, encourage=5, name='COLOR_BUTTON'):
|
def image_color_button(self, area, color, color_threshold=250, encourage=5, name='COLOR_BUTTON'):
|
||||||
"""
|
"""
|
||||||
|
@ -350,6 +350,7 @@
|
|||||||
"Clara",
|
"Clara",
|
||||||
"DanHeng",
|
"DanHeng",
|
||||||
"DanHengImbibitorLunae",
|
"DanHengImbibitorLunae",
|
||||||
|
"FuXuan",
|
||||||
"Gepard",
|
"Gepard",
|
||||||
"Herta",
|
"Herta",
|
||||||
"Himeko",
|
"Himeko",
|
||||||
@ -358,6 +359,7 @@
|
|||||||
"Kafka",
|
"Kafka",
|
||||||
"Luka",
|
"Luka",
|
||||||
"Luocha",
|
"Luocha",
|
||||||
|
"Lynx",
|
||||||
"March7th",
|
"March7th",
|
||||||
"Natasha",
|
"Natasha",
|
||||||
"Pela",
|
"Pela",
|
||||||
|
@ -53,7 +53,7 @@ class GeneratedConfig:
|
|||||||
|
|
||||||
# Group `DungeonSupport`
|
# Group `DungeonSupport`
|
||||||
DungeonSupport_Use = 'when_daily' # always_use, when_daily, do_not_use
|
DungeonSupport_Use = 'when_daily' # always_use, when_daily, do_not_use
|
||||||
DungeonSupport_Character = 'FirstCharacter' # FirstCharacter, Arlan, Asta, Bailu, Blade, Bronya, Clara, DanHeng, DanHengImbibitorLunae, Gepard, Herta, Himeko, Hook, JingYuan, Kafka, Luka, Luocha, March7th, Natasha, Pela, Qingque, Sampo, Seele, Serval, SilverWolf, Sushang, Tingyun, TrailblazerDestruction, TrailblazerPreservation, Welt, Yanqing, Yukong
|
DungeonSupport_Character = 'FirstCharacter' # FirstCharacter, Arlan, Asta, Bailu, Blade, Bronya, Clara, DanHeng, DanHengImbibitorLunae, FuXuan, Gepard, Herta, Himeko, Hook, JingYuan, Kafka, Luka, Luocha, Lynx, March7th, Natasha, Pela, Qingque, Sampo, Seele, Serval, SilverWolf, Sushang, Tingyun, TrailblazerDestruction, TrailblazerPreservation, Welt, Yanqing, Yukong
|
||||||
|
|
||||||
# Group `DungeonStorage`
|
# Group `DungeonStorage`
|
||||||
DungeonStorage_TrailblazePower = {}
|
DungeonStorage_TrailblazePower = {}
|
||||||
|
@ -87,7 +87,7 @@ class ConfigGenerator:
|
|||||||
options=[dungeon.name for dungeon in DungeonList.instances.values() if dungeon.is_Cavern_of_Corrosion])
|
options=[dungeon.name for dungeon in DungeonList.instances.values() if dungeon.is_Cavern_of_Corrosion])
|
||||||
# Insert characters
|
# Insert characters
|
||||||
from tasks.character.keywords import CharacterList
|
from tasks.character.keywords import CharacterList
|
||||||
unsupported_characters = ["FuXuan", "Lynx"]
|
unsupported_characters = []
|
||||||
characters = [character.name for character in CharacterList.instances.values()
|
characters = [character.name for character in CharacterList.instances.values()
|
||||||
if character.name not in unsupported_characters]
|
if character.name not in unsupported_characters]
|
||||||
option_add(keys='DungeonSupport.Character.option', options=characters)
|
option_add(keys='DungeonSupport.Character.option', options=characters)
|
||||||
|
@ -350,6 +350,7 @@
|
|||||||
"Clara": "Clara",
|
"Clara": "Clara",
|
||||||
"DanHeng": "Dan Heng",
|
"DanHeng": "Dan Heng",
|
||||||
"DanHengImbibitorLunae": "Dan Heng • Imbibitor Lunae",
|
"DanHengImbibitorLunae": "Dan Heng • Imbibitor Lunae",
|
||||||
|
"FuXuan": "Fu Xuan",
|
||||||
"Gepard": "Gepard",
|
"Gepard": "Gepard",
|
||||||
"Herta": "Herta",
|
"Herta": "Herta",
|
||||||
"Himeko": "Himeko",
|
"Himeko": "Himeko",
|
||||||
@ -358,6 +359,7 @@
|
|||||||
"Kafka": "Kafka",
|
"Kafka": "Kafka",
|
||||||
"Luka": "Luka",
|
"Luka": "Luka",
|
||||||
"Luocha": "Luocha",
|
"Luocha": "Luocha",
|
||||||
|
"Lynx": "Lynx",
|
||||||
"March7th": "March 7th",
|
"March7th": "March 7th",
|
||||||
"Natasha": "Natasha",
|
"Natasha": "Natasha",
|
||||||
"Pela": "Pela",
|
"Pela": "Pela",
|
||||||
|
@ -350,6 +350,7 @@
|
|||||||
"Clara": "クラーラ",
|
"Clara": "クラーラ",
|
||||||
"DanHeng": "丹恒",
|
"DanHeng": "丹恒",
|
||||||
"DanHengImbibitorLunae": "丹恒・飲月",
|
"DanHengImbibitorLunae": "丹恒・飲月",
|
||||||
|
"FuXuan": "符玄",
|
||||||
"Gepard": "ジェパード",
|
"Gepard": "ジェパード",
|
||||||
"Herta": "ヘルタ",
|
"Herta": "ヘルタ",
|
||||||
"Himeko": "姫子",
|
"Himeko": "姫子",
|
||||||
@ -358,6 +359,7 @@
|
|||||||
"Kafka": "カフカ",
|
"Kafka": "カフカ",
|
||||||
"Luka": "ルカ",
|
"Luka": "ルカ",
|
||||||
"Luocha": "羅刹",
|
"Luocha": "羅刹",
|
||||||
|
"Lynx": "リンクス",
|
||||||
"March7th": "三月なのか",
|
"March7th": "三月なのか",
|
||||||
"Natasha": "ナターシャ",
|
"Natasha": "ナターシャ",
|
||||||
"Pela": "ペラ",
|
"Pela": "ペラ",
|
||||||
|
@ -350,6 +350,7 @@
|
|||||||
"Clara": "克拉拉",
|
"Clara": "克拉拉",
|
||||||
"DanHeng": "丹恒",
|
"DanHeng": "丹恒",
|
||||||
"DanHengImbibitorLunae": "丹恒•饮月",
|
"DanHengImbibitorLunae": "丹恒•饮月",
|
||||||
|
"FuXuan": "符玄",
|
||||||
"Gepard": "杰帕德",
|
"Gepard": "杰帕德",
|
||||||
"Herta": "黑塔",
|
"Herta": "黑塔",
|
||||||
"Himeko": "姬子",
|
"Himeko": "姬子",
|
||||||
@ -358,6 +359,7 @@
|
|||||||
"Kafka": "卡芙卡",
|
"Kafka": "卡芙卡",
|
||||||
"Luka": "卢卡",
|
"Luka": "卢卡",
|
||||||
"Luocha": "罗刹",
|
"Luocha": "罗刹",
|
||||||
|
"Lynx": "玲可",
|
||||||
"March7th": "三月七",
|
"March7th": "三月七",
|
||||||
"Natasha": "娜塔莎",
|
"Natasha": "娜塔莎",
|
||||||
"Pela": "佩拉",
|
"Pela": "佩拉",
|
||||||
|
@ -350,6 +350,7 @@
|
|||||||
"Clara": "克拉拉",
|
"Clara": "克拉拉",
|
||||||
"DanHeng": "丹恆",
|
"DanHeng": "丹恆",
|
||||||
"DanHengImbibitorLunae": "丹恆•飲月",
|
"DanHengImbibitorLunae": "丹恆•飲月",
|
||||||
|
"FuXuan": "符玄",
|
||||||
"Gepard": "傑帕德",
|
"Gepard": "傑帕德",
|
||||||
"Herta": "黑塔",
|
"Herta": "黑塔",
|
||||||
"Himeko": "姬子",
|
"Himeko": "姬子",
|
||||||
@ -358,6 +359,7 @@
|
|||||||
"Kafka": "卡芙卡",
|
"Kafka": "卡芙卡",
|
||||||
"Luka": "盧卡",
|
"Luka": "盧卡",
|
||||||
"Luocha": "羅剎",
|
"Luocha": "羅剎",
|
||||||
|
"Lynx": "玲可",
|
||||||
"March7th": "三月七",
|
"March7th": "三月七",
|
||||||
"Natasha": "娜塔莎",
|
"Natasha": "娜塔莎",
|
||||||
"Pela": "佩拉",
|
"Pela": "佩拉",
|
||||||
|
@ -389,6 +389,11 @@ class Duration(Ocr):
|
|||||||
}[lang]
|
}[lang]
|
||||||
return re.compile(regex_str)
|
return re.compile(regex_str)
|
||||||
|
|
||||||
|
def after_process(self, result):
|
||||||
|
result = super().after_process(result)
|
||||||
|
result = result.strip('.,。,')
|
||||||
|
return result
|
||||||
|
|
||||||
def format_result(self, result: str) -> timedelta:
|
def format_result(self, result: str) -> timedelta:
|
||||||
"""
|
"""
|
||||||
Do OCR on a duration, such as `18d 2h 13m 30s`, `2h`, `13m 30s`, `9s`
|
Do OCR on a duration, such as `18d 2h 13m 30s`, `2h`, `13m 30s`, `9s`
|
||||||
|
@ -106,7 +106,7 @@ class Combat(CombatInteract, CombatPrepare, CombatState, CombatTeam, CombatSuppo
|
|||||||
continue
|
continue
|
||||||
if self.appear(COMBAT_TEAM_PREPARE):
|
if self.appear(COMBAT_TEAM_PREPARE):
|
||||||
self.interval_reset(COMBAT_PREPARE)
|
self.interval_reset(COMBAT_PREPARE)
|
||||||
self._map_A_timer.reset()
|
self.map_A_timer.reset()
|
||||||
if self.appear(COMBAT_PREPARE, interval=2):
|
if self.appear(COMBAT_PREPARE, interval=2):
|
||||||
if not self.handle_combat_prepare():
|
if not self.handle_combat_prepare():
|
||||||
return False
|
return False
|
||||||
|
@ -210,13 +210,23 @@ DAILY_QUEST_RIGHT_END = ButtonWrapper(
|
|||||||
button=(401, 259, 411, 632),
|
button=(401, 259, 411, 632),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
MASK_DAILY_QUEST = ButtonWrapper(
|
||||||
|
name='MASK_DAILY_QUEST',
|
||||||
|
share=Button(
|
||||||
|
file='./assets/share/daily/reward/MASK_DAILY_QUEST.png',
|
||||||
|
area=(117, 308, 1173, 630),
|
||||||
|
search=(97, 288, 1193, 650),
|
||||||
|
color=(208, 208, 208),
|
||||||
|
button=(117, 308, 1173, 630),
|
||||||
|
),
|
||||||
|
)
|
||||||
OCR_DAILY_QUEST = ButtonWrapper(
|
OCR_DAILY_QUEST = ButtonWrapper(
|
||||||
name='OCR_DAILY_QUEST',
|
name='OCR_DAILY_QUEST',
|
||||||
share=Button(
|
share=Button(
|
||||||
file='./assets/share/daily/reward/OCR_DAILY_QUEST.png',
|
file='./assets/share/daily/reward/OCR_DAILY_QUEST.png',
|
||||||
area=(117, 257, 1173, 630),
|
area=(117, 308, 1173, 630),
|
||||||
search=(97, 237, 1193, 650),
|
search=(97, 288, 1193, 650),
|
||||||
color=(208, 206, 202),
|
color=(204, 202, 199),
|
||||||
button=(117, 257, 1173, 630),
|
button=(117, 308, 1173, 630),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
@ -1,9 +1,12 @@
|
|||||||
|
import cv2
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
|
||||||
from module.base.timer import Timer
|
from module.base.timer import Timer
|
||||||
from module.logger import *
|
from module.base.utils import crop
|
||||||
|
from module.logger import logger
|
||||||
from module.ocr.ocr import Ocr, OcrResultButton
|
from module.ocr.ocr import Ocr, OcrResultButton
|
||||||
from module.ocr.utils import split_and_pair_buttons
|
from module.ocr.utils import split_and_pair_buttons
|
||||||
|
from tasks.battle_pass.keywords import KEYWORD_BATTLE_PASS_QUEST
|
||||||
from tasks.daily.assets.assets_daily_reward import *
|
from tasks.daily.assets.assets_daily_reward import *
|
||||||
from tasks.daily.camera import CameraUI
|
from tasks.daily.camera import CameraUI
|
||||||
from tasks.daily.keywords import (
|
from tasks.daily.keywords import (
|
||||||
@ -19,12 +22,18 @@ from tasks.dungeon.keywords import KEYWORDS_DUNGEON_TAB
|
|||||||
from tasks.dungeon.ui import DungeonUI
|
from tasks.dungeon.ui import DungeonUI
|
||||||
from tasks.item.consumable_usage import ConsumableUsageUI
|
from tasks.item.consumable_usage import ConsumableUsageUI
|
||||||
from tasks.item.relics import RelicsUI
|
from tasks.item.relics import RelicsUI
|
||||||
from tasks.battle_pass.keywords import KEYWORD_BATTLE_PASS_QUEST
|
|
||||||
|
|
||||||
|
|
||||||
class DailyQuestOcr(Ocr):
|
class DailyQuestOcr(Ocr):
|
||||||
def __init__(self, button: ButtonWrapper, lang=None, name=None):
|
merge_thres_y = 20
|
||||||
super().__init__(button, lang, name)
|
|
||||||
|
def pre_process(self, image):
|
||||||
|
image = super().pre_process(image)
|
||||||
|
image = crop(image, OCR_DAILY_QUEST.area)
|
||||||
|
mask = MASK_DAILY_QUEST.matched_button.image
|
||||||
|
# Remove "+200Activity"
|
||||||
|
cv2.bitwise_and(image, mask, dst=image)
|
||||||
|
return image
|
||||||
|
|
||||||
def after_process(self, result):
|
def after_process(self, result):
|
||||||
result = super().after_process(result)
|
result = super().after_process(result)
|
||||||
@ -91,8 +100,7 @@ class DailyQuestUI(DungeonUI):
|
|||||||
|
|
||||||
def _ocr_single_page(self) -> list[OcrResultButton]:
|
def _ocr_single_page(self) -> list[OcrResultButton]:
|
||||||
ocr = DailyQuestOcr(OCR_DAILY_QUEST)
|
ocr = DailyQuestOcr(OCR_DAILY_QUEST)
|
||||||
ocr.merge_thres_y = 20
|
results = ocr.matched_ocr(self.device.image, [DailyQuestState, DailyQuest], direct_ocr=True)
|
||||||
results = ocr.matched_ocr(self.device.image, [DailyQuestState, DailyQuest])
|
|
||||||
if len(results) < 8:
|
if len(results) < 8:
|
||||||
logger.warning(f"Recognition failed at {8 - len(results)} quests on one page")
|
logger.warning(f"Recognition failed at {8 - len(results)} quests on one page")
|
||||||
|
|
||||||
|
@ -248,7 +248,7 @@ class Dungeon(DungeonUI, DungeonEvent, Combat):
|
|||||||
area = area_offset((-50, -150, 0, 0), offset=self.config.ASSETS_RESOLUTION)
|
area = area_offset((-50, -150, 0, 0), offset=self.config.ASSETS_RESOLUTION)
|
||||||
|
|
||||||
skip_first_screenshot = True
|
skip_first_screenshot = True
|
||||||
self._map_A_timer.reset()
|
self.map_A_timer.reset()
|
||||||
handled = False
|
handled = False
|
||||||
while 1:
|
while 1:
|
||||||
if skip_first_screenshot:
|
if skip_first_screenshot:
|
||||||
@ -265,7 +265,7 @@ class Dungeon(DungeonUI, DungeonEvent, Combat):
|
|||||||
logger.info(f'No destructible object')
|
logger.info(f'No destructible object')
|
||||||
if not handled:
|
if not handled:
|
||||||
break
|
break
|
||||||
if self._map_A_timer.reached():
|
if self.map_A_timer.reached():
|
||||||
break
|
break
|
||||||
|
|
||||||
return handled
|
return handled
|
||||||
|
@ -4,7 +4,7 @@ from module.base.timer import Timer
|
|||||||
from module.logger import logger
|
from module.logger import logger
|
||||||
from tasks.map.assets.assets_map_control import ROTATION_SWIPE_AREA
|
from tasks.map.assets.assets_map_control import ROTATION_SWIPE_AREA
|
||||||
from tasks.map.control.joystick import JoystickContact, MapControlJoystick
|
from tasks.map.control.joystick import JoystickContact, MapControlJoystick
|
||||||
from tasks.map.control.waypoint import Waypoint, WaypointRun, WaypointStraightRun, ensure_waypoint
|
from tasks.map.control.waypoint import Waypoint, ensure_waypoint
|
||||||
from tasks.map.minimap.minimap import Minimap
|
from tasks.map.minimap.minimap import Minimap
|
||||||
from tasks.map.resource.const import diff_to_180_180
|
from tasks.map.resource.const import diff_to_180_180
|
||||||
|
|
||||||
@ -76,7 +76,7 @@ class MapControl(MapControlJoystick):
|
|||||||
self,
|
self,
|
||||||
contact: JoystickContact,
|
contact: JoystickContact,
|
||||||
waypoint: Waypoint,
|
waypoint: Waypoint,
|
||||||
end_point_opt=True,
|
end_opt=True,
|
||||||
skip_first_screenshot=False
|
skip_first_screenshot=False
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
@ -88,7 +88,7 @@ class MapControl(MapControlJoystick):
|
|||||||
`with JoystickContact(self) as contact:`
|
`with JoystickContact(self) as contact:`
|
||||||
waypoint:
|
waypoint:
|
||||||
Position to goto, (x, y)
|
Position to goto, (x, y)
|
||||||
end_point_opt:
|
end_opt:
|
||||||
True to enable endpoint optimizations,
|
True to enable endpoint optimizations,
|
||||||
character will smoothly approach target position
|
character will smoothly approach target position
|
||||||
skip_first_screenshot:
|
skip_first_screenshot:
|
||||||
@ -98,7 +98,7 @@ class MapControl(MapControlJoystick):
|
|||||||
self.device.stuck_record_clear()
|
self.device.stuck_record_clear()
|
||||||
self.device.click_record_clear()
|
self.device.click_record_clear()
|
||||||
|
|
||||||
end_point_opt = end_point_opt and waypoint.end_point_opt
|
end_opt = end_opt and waypoint.end_opt
|
||||||
allow_2x_run = waypoint.speed in ['2x_run']
|
allow_2x_run = waypoint.speed in ['2x_run']
|
||||||
allow_straight_run = waypoint.speed in ['2x_run', 'straight_run']
|
allow_straight_run = waypoint.speed in ['2x_run', 'straight_run']
|
||||||
allow_run = waypoint.speed in ['2x_run', 'straight_run', 'run']
|
allow_run = waypoint.speed in ['2x_run', 'straight_run', 'run']
|
||||||
@ -117,20 +117,20 @@ class MapControl(MapControlJoystick):
|
|||||||
self.minimap.update(self.device.image)
|
self.minimap.update(self.device.image)
|
||||||
|
|
||||||
# Arrive
|
# Arrive
|
||||||
if self.minimap.is_position_near(waypoint.position, threshold=waypoint.get_threshold(end_point_opt)):
|
if self.minimap.is_position_near(waypoint.position, threshold=waypoint.get_threshold(end_opt)):
|
||||||
logger.info(f'Arrive {waypoint}')
|
logger.info(f'Arrive {waypoint}')
|
||||||
break
|
break
|
||||||
|
|
||||||
# Switch run case
|
# Switch run case
|
||||||
diff = self.minimap.position_diff(waypoint.position)
|
diff = self.minimap.position_diff(waypoint.position)
|
||||||
if end_point_opt:
|
if end_opt:
|
||||||
if allow_2x_run and diff < 20:
|
if allow_2x_run and diff < 20:
|
||||||
logger.info(f'Approaching target, diff={round(diff, 1)}, disallow 2x_run')
|
logger.info(f'Approaching target, diff={round(diff, 1)}, disallow 2x_run')
|
||||||
allow_2x_run = False
|
allow_2x_run = False
|
||||||
if allow_straight_run and diff < 15:
|
if allow_straight_run and diff < 15:
|
||||||
logger.info(f'Approaching target, diff={round(diff, 1)}, disallow straight_run')
|
logger.info(f'Approaching target, diff={round(diff, 1)}, disallow straight_run')
|
||||||
direction_interval = Timer(0.2)
|
direction_interval = Timer(0.2)
|
||||||
self._map_2x_run_timer.reset()
|
self.map_2x_run_timer.reset()
|
||||||
allow_straight_run = False
|
allow_straight_run = False
|
||||||
if allow_run and diff < 7:
|
if allow_run and diff < 7:
|
||||||
logger.info(f'Approaching target, diff={round(diff, 1)}, disallow run')
|
logger.info(f'Approaching target, diff={round(diff, 1)}, disallow run')
|
||||||
@ -160,7 +160,7 @@ class MapControl(MapControlJoystick):
|
|||||||
direction_interval.reset()
|
direction_interval.reset()
|
||||||
self.handle_map_2x_run(run=True)
|
self.handle_map_2x_run(run=True)
|
||||||
elif allow_straight_run:
|
elif allow_straight_run:
|
||||||
# Run with 2x_run button
|
# Run straight forward
|
||||||
# - Set rotation once
|
# - Set rotation once
|
||||||
# - Continuous fine-tuning direction
|
# - Continuous fine-tuning direction
|
||||||
# - Disable 2x_run
|
# - Disable 2x_run
|
||||||
@ -204,7 +204,7 @@ class MapControl(MapControlJoystick):
|
|||||||
|
|
||||||
def goto(
|
def goto(
|
||||||
self,
|
self,
|
||||||
waypoints,
|
*waypoints,
|
||||||
skip_first_screenshot=True
|
skip_first_screenshot=True
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
@ -218,8 +218,6 @@ class MapControl(MapControlJoystick):
|
|||||||
skip_first_screenshot:
|
skip_first_screenshot:
|
||||||
"""
|
"""
|
||||||
logger.hr('Goto', level=1)
|
logger.hr('Goto', level=1)
|
||||||
if not isinstance(waypoints, list):
|
|
||||||
waypoints = [waypoints]
|
|
||||||
waypoints = [ensure_waypoint(point) for point in waypoints]
|
waypoints = [ensure_waypoint(point) for point in waypoints]
|
||||||
logger.info(f'Go along {len(waypoints)} waypoints')
|
logger.info(f'Go along {len(waypoints)} waypoints')
|
||||||
end_list = [False for _ in waypoints]
|
end_list = [False for _ in waypoints]
|
||||||
@ -231,35 +229,36 @@ class MapControl(MapControlJoystick):
|
|||||||
self._goto(
|
self._goto(
|
||||||
contact=contact,
|
contact=contact,
|
||||||
waypoint=point,
|
waypoint=point,
|
||||||
end_point_opt=end,
|
end_opt=end,
|
||||||
skip_first_screenshot=skip_first_screenshot
|
skip_first_screenshot=skip_first_screenshot
|
||||||
)
|
)
|
||||||
skip_first_screenshot = True
|
skip_first_screenshot = True
|
||||||
|
|
||||||
end_point = waypoints[-1]
|
end_point = waypoints[-1]
|
||||||
if end_point.end_point_rotation is not None:
|
if end_point.end_rotation is not None:
|
||||||
self.rotation_set(end_point.end_point_rotation, threshold=end_point.end_point_rotation_threshold)
|
logger.hr('End rotation', level=1)
|
||||||
|
self.rotation_set(end_point.end_rotation, threshold=end_point.end_rotation_threshold)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
# Control test in Himeko trail
|
# Control test in Himeko trial
|
||||||
# Must manually enter Himeko trail first and dismiss popup
|
# Must manually enter Himeko trial first and dismiss popup
|
||||||
self = MapControl('alas')
|
self = MapControl('src')
|
||||||
self.minimap.set_plane('Jarilo_BackwaterPass', floor='F1')
|
self.minimap.set_plane('Jarilo_BackwaterPass', floor='F1')
|
||||||
self.device.screenshot()
|
self.device.screenshot()
|
||||||
self.minimap.init_position((519, 359))
|
self.minimap.init_position((519, 359))
|
||||||
# Visit 3 items
|
# Visit 3 items
|
||||||
self.goto([
|
self.goto(
|
||||||
WaypointRun((577.6, 363.4)),
|
Waypoint((577.6, 363.4)),
|
||||||
])
|
)
|
||||||
self.goto([
|
self.goto(
|
||||||
WaypointStraightRun((577.5, 369.4), end_point_rotation=200),
|
Waypoint((577.5, 369.4), end_rotation=200),
|
||||||
])
|
)
|
||||||
self.goto([
|
self.goto(
|
||||||
WaypointRun((581.5, 387.3)),
|
Waypoint((581.5, 387.3)),
|
||||||
WaypointRun((577.4, 411.5)),
|
Waypoint((577.4, 411.5)),
|
||||||
])
|
)
|
||||||
# Goto boss
|
# Goto boss
|
||||||
self.goto([
|
self.goto(
|
||||||
WaypointStraightRun((607.6, 425.3)),
|
Waypoint((607.6, 425.3)),
|
||||||
])
|
)
|
||||||
|
@ -1,6 +1,9 @@
|
|||||||
import math
|
import math
|
||||||
from functools import cached_property
|
from functools import cached_property
|
||||||
|
|
||||||
|
import cv2
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
from module.base.timer import Timer
|
from module.base.timer import Timer
|
||||||
from module.device.method.maatouch import MaatouchBuilder
|
from module.device.method.maatouch import MaatouchBuilder
|
||||||
from module.device.method.minitouch import CommandBuilder, insert_swipe, random_normal_distribution
|
from module.device.method.minitouch import CommandBuilder, insert_swipe, random_normal_distribution
|
||||||
@ -36,10 +39,8 @@ class JoystickContact:
|
|||||||
Can not lift finger when:
|
Can not lift finger when:
|
||||||
- Process is force terminated
|
- Process is force terminated
|
||||||
"""
|
"""
|
||||||
builder = self.builder
|
|
||||||
if self.is_downed:
|
if self.is_downed:
|
||||||
builder.up().commit()
|
self.up()
|
||||||
builder.send()
|
|
||||||
logger.info('JoystickContact ends')
|
logger.info('JoystickContact ends')
|
||||||
else:
|
else:
|
||||||
logger.info('JoystickContact ends but it was never downed')
|
logger.info('JoystickContact ends but it was never downed')
|
||||||
@ -98,6 +99,13 @@ class JoystickContact:
|
|||||||
point = (int(round(point[0])), int(round(point[1])))
|
point = (int(round(point[0])), int(round(point[1])))
|
||||||
return point
|
return point
|
||||||
|
|
||||||
|
def up(self):
|
||||||
|
builder = self.builder
|
||||||
|
if self.is_downed:
|
||||||
|
builder.up().commit()
|
||||||
|
builder.send()
|
||||||
|
self.prev_point = None
|
||||||
|
|
||||||
def set(self, direction, run=True):
|
def set(self, direction, run=True):
|
||||||
"""
|
"""
|
||||||
Set joystick to given position
|
Set joystick to given position
|
||||||
@ -110,6 +118,13 @@ class JoystickContact:
|
|||||||
point = JoystickContact.direction2screen(direction, run=run)
|
point = JoystickContact.direction2screen(direction, run=run)
|
||||||
builder = self.builder
|
builder = self.builder
|
||||||
|
|
||||||
|
if self.is_downed and not self.main.joystick_speed():
|
||||||
|
if self.main.joystick_lost_timer.reached():
|
||||||
|
logger.warning(f'Joystick contact lost: {self.main.joystick_lost_timer}, re-down')
|
||||||
|
self.up()
|
||||||
|
else:
|
||||||
|
self.main.joystick_lost_timer.reset()
|
||||||
|
|
||||||
if self.is_downed:
|
if self.is_downed:
|
||||||
points = insert_swipe(p0=self.prev_point, p3=point, speed=20)
|
points = insert_swipe(p0=self.prev_point, p3=point, speed=20)
|
||||||
for point in points[1:]:
|
for point in points[1:]:
|
||||||
@ -121,20 +136,56 @@ class JoystickContact:
|
|||||||
# Character starts moving, RUN button is still unavailable in a short time.
|
# Character starts moving, RUN button is still unavailable in a short time.
|
||||||
# Assume available in 0.3s
|
# Assume available in 0.3s
|
||||||
# We still have reties if 0.3s is incorrect.
|
# We still have reties if 0.3s is incorrect.
|
||||||
self.main._map_2x_run_timer.set_current(0.7)
|
self.main.map_2x_run_timer.set_current(0.7)
|
||||||
|
self.main.joystick_lost_timer.reset()
|
||||||
|
|
||||||
self.prev_point = point
|
self.prev_point = point
|
||||||
|
|
||||||
|
|
||||||
class MapControlJoystick(UI):
|
class MapControlJoystick(UI):
|
||||||
_map_A_timer = Timer(1)
|
map_A_timer = Timer(1)
|
||||||
_map_E_timer = Timer(1)
|
map_E_timer = Timer(1)
|
||||||
_map_2x_run_timer = Timer(1)
|
map_2x_run_timer = Timer(1)
|
||||||
|
|
||||||
|
joystick_lost_timer = Timer(1, count=2)
|
||||||
|
|
||||||
@cached_property
|
@cached_property
|
||||||
def joystick_center(self) -> tuple[float, float]:
|
def joystick_center(self) -> tuple[int, int]:
|
||||||
x1, y1, x2, y2 = JOYSTICK.area
|
x1, y1, x2, y2 = JOYSTICK.area
|
||||||
return (x1 + x2) / 2, (y1 + y2) / 2
|
return int((x1 + x2) // 2), int((y1 + y2) // 2)
|
||||||
|
|
||||||
|
@cached_property
|
||||||
|
def DirectionRemapData(self):
|
||||||
|
d = JoystickContact.RADIUS_RUN[1] * 2
|
||||||
|
mx = np.zeros((d, d), dtype=np.float32)
|
||||||
|
my = np.zeros((d, d), dtype=np.float32)
|
||||||
|
for i in range(d):
|
||||||
|
for j in range(d):
|
||||||
|
mx[i, j] = d / 2 + i / 2 * np.cos(2 * np.pi * j / d)
|
||||||
|
my[i, j] = d / 2 + i / 2 * np.sin(2 * np.pi * j / d)
|
||||||
|
return mx, my
|
||||||
|
|
||||||
|
def joystick_speed(self) -> str:
|
||||||
|
"""
|
||||||
|
Returns:
|
||||||
|
str: 'run', 'walk', ''
|
||||||
|
"""
|
||||||
|
# About 1.5ms
|
||||||
|
x, y = self.joystick_center
|
||||||
|
radius = JoystickContact.RADIUS_RUN[1]
|
||||||
|
image = self.image_crop((x - radius, y - radius, x + radius, y + radius), copy=False)
|
||||||
|
image = cv2.remap(image, *self.DirectionRemapData, cv2.INTER_CUBIC)
|
||||||
|
|
||||||
|
# 190~205
|
||||||
|
run = image[185:210, :]
|
||||||
|
if self.image_color_count(run, color=(223, 199, 145), threshold=221, count=100):
|
||||||
|
return 'run'
|
||||||
|
# 90~100
|
||||||
|
walk = image[85:105, :]
|
||||||
|
if self.image_color_count(walk, color=(235, 235, 235), threshold=221, count=50):
|
||||||
|
return 'walk'
|
||||||
|
|
||||||
|
return ''
|
||||||
|
|
||||||
def map_get_technique_points(self):
|
def map_get_technique_points(self):
|
||||||
"""
|
"""
|
||||||
@ -162,9 +213,9 @@ class MapControlJoystick(UI):
|
|||||||
Returns:
|
Returns:
|
||||||
bool: If clicked.
|
bool: If clicked.
|
||||||
"""
|
"""
|
||||||
if self._map_A_timer.reached():
|
if self.map_A_timer.reached():
|
||||||
self.device.click(A_BUTTON)
|
self.device.click(A_BUTTON)
|
||||||
self._map_A_timer.reset()
|
self.map_A_timer.reset()
|
||||||
return True
|
return True
|
||||||
|
|
||||||
return False
|
return False
|
||||||
@ -177,9 +228,9 @@ class MapControlJoystick(UI):
|
|||||||
Returns:
|
Returns:
|
||||||
bool: If clicked.
|
bool: If clicked.
|
||||||
"""
|
"""
|
||||||
if self._map_E_timer.reached():
|
if self.map_E_timer.reached():
|
||||||
self.device.click(E_BUTTON)
|
self.device.click(E_BUTTON)
|
||||||
self._map_E_timer.reset()
|
self.map_E_timer.reset()
|
||||||
return True
|
return True
|
||||||
|
|
||||||
return False
|
return False
|
||||||
@ -194,13 +245,13 @@ class MapControlJoystick(UI):
|
|||||||
"""
|
"""
|
||||||
is_running = self.image_color_count(RUN_BUTTON, color=(208, 183, 138), threshold=221, count=100)
|
is_running = self.image_color_count(RUN_BUTTON, color=(208, 183, 138), threshold=221, count=100)
|
||||||
|
|
||||||
if run and not is_running and self._map_2x_run_timer.reached():
|
if run and not is_running and self.map_2x_run_timer.reached():
|
||||||
self.device.click(RUN_BUTTON)
|
self.device.click(RUN_BUTTON)
|
||||||
self._map_2x_run_timer.reset()
|
self.map_2x_run_timer.reset()
|
||||||
return True
|
return True
|
||||||
if not run and is_running and self._map_2x_run_timer.reached():
|
if not run and is_running and self.map_2x_run_timer.reached():
|
||||||
self.device.click(RUN_BUTTON)
|
self.device.click(RUN_BUTTON)
|
||||||
self._map_2x_run_timer.reset()
|
self.map_2x_run_timer.reset()
|
||||||
return True
|
return True
|
||||||
|
|
||||||
return False
|
return False
|
||||||
|
@ -13,17 +13,17 @@ class Waypoint:
|
|||||||
endpoint_threshold: int = 3
|
endpoint_threshold: int = 3
|
||||||
# Max move speed, '2x_run', 'straight_run', 'run', 'walk'
|
# Max move speed, '2x_run', 'straight_run', 'run', 'walk'
|
||||||
# See MapControl._goto() for details of each speed level
|
# See MapControl._goto() for details of each speed level
|
||||||
speed: str = '2x_run'
|
speed: str = 'straight_run'
|
||||||
|
|
||||||
"""
|
"""
|
||||||
The following attributes are only be used if this waypoint is the end point of goto()
|
The following attributes are only be used if this waypoint is the end point of goto()
|
||||||
"""
|
"""
|
||||||
# True to enable endpoint optimizations, character will smoothly approach target position
|
# True to enable endpoint optimizations, character will smoothly approach target position
|
||||||
# False to stop all controls at arrive
|
# False to stop all controls at arrive
|
||||||
end_point_opt: bool = True
|
end_opt: bool = True
|
||||||
# Set rotation after arrive, 0~360
|
# Set rotation after arrive, 0~360
|
||||||
end_point_rotation: int = None
|
end_rotation: int = None
|
||||||
end_point_rotation_threshold: int = 15
|
end_rotation_threshold: int = 15
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f'Waypoint({self.position})'
|
return f'Waypoint({self.position})'
|
||||||
|
347
tasks/map/interact/aim.py
Normal file
347
tasks/map/interact/aim.py
Normal file
@ -0,0 +1,347 @@
|
|||||||
|
import cv2
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
from module.base.decorator import cached_property, del_cached_property
|
||||||
|
from module.base.utils import Points, image_size, load_image
|
||||||
|
from module.config.utils import dict_to_kv
|
||||||
|
from module.logger import logger
|
||||||
|
from tasks.base.ui import UI
|
||||||
|
|
||||||
|
|
||||||
|
def inrange(image, lower=0, upper=255):
|
||||||
|
"""
|
||||||
|
Get the coordinates of pixels in range.
|
||||||
|
Equivalent to `np.array(np.where(lower <= image <= upper))` but faster.
|
||||||
|
Note that this method will change `image`.
|
||||||
|
|
||||||
|
`cv2.findNonZero()` is faster than `np.where`
|
||||||
|
points = np.array(np.where(y > 24)).T[:, ::-1]
|
||||||
|
points = np.array(cv2.findNonZero((y > 24).astype(np.uint8)))[:, 0, :]
|
||||||
|
|
||||||
|
`cv2.inRange(y, 24)` is faster than `y > 24`
|
||||||
|
cv2.inRange(y, 24, 255, dst=y)
|
||||||
|
y = y > 24
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
np.ndarray: Shape (N, 2)
|
||||||
|
E.g. [[x1, y1], [x2, y2], ...]
|
||||||
|
"""
|
||||||
|
cv2.inRange(image, lower, upper, dst=image)
|
||||||
|
try:
|
||||||
|
return np.array(cv2.findNonZero(image))[:, 0, :]
|
||||||
|
except IndexError:
|
||||||
|
# Empty result
|
||||||
|
# IndexError: too many indices for array: array is 0-dimensional, but 3 were indexed
|
||||||
|
return np.array([])
|
||||||
|
|
||||||
|
|
||||||
|
def subtract_blur(image, radius=3, negative=False):
|
||||||
|
"""
|
||||||
|
If you care performance more than quality:
|
||||||
|
- radius=3, use medianBlur
|
||||||
|
- radius=5,7,9,11, use GaussianBlur
|
||||||
|
- radius>11, use stackBlur (requires opencv >= 4.7.0)
|
||||||
|
|
||||||
|
Args:
|
||||||
|
image:
|
||||||
|
radius:
|
||||||
|
negative:
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
np.ndarray:
|
||||||
|
"""
|
||||||
|
if radius <= 3:
|
||||||
|
blur = cv2.medianBlur(image, radius)
|
||||||
|
elif radius <= 11:
|
||||||
|
blur = cv2.GaussianBlur(image, (radius, radius), 0)
|
||||||
|
else:
|
||||||
|
blur = cv2.stackBlur(image, (radius, radius), 0)
|
||||||
|
|
||||||
|
if negative:
|
||||||
|
cv2.subtract(blur, image, dst=blur)
|
||||||
|
else:
|
||||||
|
cv2.subtract(image, blur, dst=blur)
|
||||||
|
return blur
|
||||||
|
|
||||||
|
|
||||||
|
def remove_border(image, radius):
|
||||||
|
"""
|
||||||
|
Paint edge pixels black.
|
||||||
|
No returns, changes are written to `image`
|
||||||
|
|
||||||
|
Args:
|
||||||
|
image:
|
||||||
|
radius:
|
||||||
|
"""
|
||||||
|
width, height = image_size(image)
|
||||||
|
image[:, :radius + 1] = 0
|
||||||
|
image[:, width - radius:] = 0
|
||||||
|
image[:radius + 1, :] = 0
|
||||||
|
image[height - radius:, :] = 0
|
||||||
|
|
||||||
|
|
||||||
|
def create_circle(min_radius, max_radius):
|
||||||
|
"""
|
||||||
|
Create a circle with min_radius <= R <= max_radius.
|
||||||
|
1 represents circle, 0 represents background
|
||||||
|
|
||||||
|
Args:
|
||||||
|
min_radius:
|
||||||
|
max_radius:
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
np.ndarray:
|
||||||
|
"""
|
||||||
|
circle = np.ones((max_radius * 2 + 1, max_radius * 2 + 1), dtype=np.uint8)
|
||||||
|
center = np.array((max_radius, max_radius))
|
||||||
|
points = np.array(np.meshgrid(np.arange(circle.shape[0]), np.arange(circle.shape[1]))).T
|
||||||
|
distance = np.linalg.norm(points - center, axis=2)
|
||||||
|
circle[distance < min_radius] = 0
|
||||||
|
circle[distance > max_radius] = 0
|
||||||
|
return circle
|
||||||
|
|
||||||
|
|
||||||
|
def draw_circle(image, circle, points):
|
||||||
|
"""
|
||||||
|
Add a circle onto image.
|
||||||
|
No returns, changes are written to `image`
|
||||||
|
|
||||||
|
Args:
|
||||||
|
image:
|
||||||
|
circle: Created from create_circle()
|
||||||
|
points: (x, y), center of the circle to draw
|
||||||
|
"""
|
||||||
|
width, height = image_size(circle)
|
||||||
|
x1 = -int(width // 2)
|
||||||
|
y1 = -int(height // 2)
|
||||||
|
x2 = width + x1
|
||||||
|
y2 = height + y1
|
||||||
|
for point in points:
|
||||||
|
x, y = point
|
||||||
|
# Fancy index is faster
|
||||||
|
index = image[y + y1:y + y2, x + x1:x + x2]
|
||||||
|
# print(index.shape)
|
||||||
|
cv2.add(index, circle, dst=index)
|
||||||
|
|
||||||
|
|
||||||
|
class Aim:
|
||||||
|
radius_enemy = (24, 25)
|
||||||
|
radius_item = (8, 10)
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.debug = False
|
||||||
|
|
||||||
|
self.draw_item = None
|
||||||
|
self.draw_enemy = None
|
||||||
|
self.points_item = None
|
||||||
|
self.points_enemy = None
|
||||||
|
|
||||||
|
def clear_image_cache(self):
|
||||||
|
self.draw_item = None
|
||||||
|
self.draw_enemy = None
|
||||||
|
self.points_item = None
|
||||||
|
self.points_enemy = None
|
||||||
|
del_cached_property(self, 'aimed_enemy')
|
||||||
|
del_cached_property(self, 'aimed_item')
|
||||||
|
|
||||||
|
@cached_property
|
||||||
|
def mask_interact(self):
|
||||||
|
return load_image('./assets/mask/MASK_MAP_INTERACT.png')
|
||||||
|
|
||||||
|
@cached_property
|
||||||
|
def circle_enemy(self):
|
||||||
|
return create_circle(*self.radius_enemy)
|
||||||
|
|
||||||
|
@cached_property
|
||||||
|
def circle_item(self):
|
||||||
|
return create_circle(*self.radius_item)
|
||||||
|
|
||||||
|
# @timer
|
||||||
|
def predict_enemy(self, h, v):
|
||||||
|
min_radius, max_radius = self.radius_enemy
|
||||||
|
width, height = image_size(v)
|
||||||
|
|
||||||
|
# Get white circle `y`
|
||||||
|
y = subtract_blur(h, 3, negative=False)
|
||||||
|
cv2.inRange(h, 168, 255, h)
|
||||||
|
cv2.bitwise_and(y, h, dst=y)
|
||||||
|
# Get red glow `v`
|
||||||
|
cv2.inRange(v, 168, 255, dst=v)
|
||||||
|
|
||||||
|
kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (5, 5))
|
||||||
|
cv2.dilate(v, kernel, dst=v)
|
||||||
|
# Remove noise and leave red circle only
|
||||||
|
cv2.bitwise_and(y, v, dst=y)
|
||||||
|
# cv2.imshow('predict_enemy', y)
|
||||||
|
# Remove game UI
|
||||||
|
cv2.bitwise_and(y, self.mask_interact, dst=y)
|
||||||
|
# Remove points on the edge, or draw_circle() will overflow
|
||||||
|
remove_border(y, max_radius)
|
||||||
|
|
||||||
|
# Get all pixels
|
||||||
|
points = inrange(y, lower=18)
|
||||||
|
if points.shape[0] > 1000:
|
||||||
|
logger.warning(f'AimDetector.predict_enemy() too many points to draw: {points.shape}')
|
||||||
|
# Draw circles
|
||||||
|
draw = np.zeros((height, width), dtype=np.uint8)
|
||||||
|
draw_circle(draw, self.circle_enemy, points)
|
||||||
|
if self.debug:
|
||||||
|
self.draw_enemy = cv2.multiply(draw, 4)
|
||||||
|
subtract_blur(draw, 3)
|
||||||
|
|
||||||
|
# Find peaks
|
||||||
|
points = inrange(draw, lower=36)
|
||||||
|
points = Points(points).group(threshold=10)
|
||||||
|
if points.shape[0] > 3:
|
||||||
|
logger.warning(f'AimDetector.predict_enemy() too many peaks: {points.shape}')
|
||||||
|
self.points_enemy = points
|
||||||
|
# print(points)
|
||||||
|
return points
|
||||||
|
|
||||||
|
# @timer
|
||||||
|
def predict_item(self, v):
|
||||||
|
min_radius, max_radius = self.radius_item
|
||||||
|
width, height = image_size(v)
|
||||||
|
|
||||||
|
# Get white circle `y`
|
||||||
|
y = subtract_blur(v, 9)
|
||||||
|
white = cv2.inRange(v, 112, 144)
|
||||||
|
cv2.bitwise_and(y, white, dst=y)
|
||||||
|
# Get cyan glow `v`
|
||||||
|
cv2.inRange(v, 0, 84, dst=v)
|
||||||
|
|
||||||
|
kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (5, 5))
|
||||||
|
cv2.dilate(v, kernel, dst=v)
|
||||||
|
|
||||||
|
# Remove noise and leave cyan circle only
|
||||||
|
cv2.bitwise_and(y, v, dst=y)
|
||||||
|
# Remove game UI
|
||||||
|
cv2.bitwise_and(y, self.mask_interact, dst=y)
|
||||||
|
# Remove points on the edge, or draw_circle() will overflow
|
||||||
|
remove_border(y, max_radius)
|
||||||
|
|
||||||
|
# Get all pixels
|
||||||
|
points = inrange(y, lower=18)
|
||||||
|
# print(points.shape)
|
||||||
|
if points.shape[0] > 1000:
|
||||||
|
logger.warning(f'AimDetector.predict_item() too many points to draw: {points.shape}')
|
||||||
|
# Draw circles
|
||||||
|
draw = np.zeros((height, width), dtype=np.uint8)
|
||||||
|
draw_circle(draw, self.circle_item, points)
|
||||||
|
if self.debug:
|
||||||
|
self.draw_item = cv2.multiply(draw, 2)
|
||||||
|
subtract_blur(draw, 7)
|
||||||
|
|
||||||
|
# Find peaks
|
||||||
|
points = inrange(draw, lower=64)
|
||||||
|
points = Points(points).group(threshold=10)
|
||||||
|
if points.shape[0] > 3:
|
||||||
|
logger.warning(f'AimDetector.predict_item() too many peaks: {points.shape}')
|
||||||
|
self.points_item = points
|
||||||
|
# print(points)
|
||||||
|
return points
|
||||||
|
|
||||||
|
# @timer
|
||||||
|
def predict(self, image, enemy=True, item=True, show_log=True, debug=False):
|
||||||
|
"""
|
||||||
|
Predict `aim` on image, costs about 10.0~10.5ms.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
image:
|
||||||
|
enemy: True to predict enemy
|
||||||
|
item: True to predict item
|
||||||
|
show_log:
|
||||||
|
debug: True to show AimDetector image
|
||||||
|
"""
|
||||||
|
self.debug = debug
|
||||||
|
self.clear_image_cache()
|
||||||
|
if isinstance(image, str):
|
||||||
|
image = load_image(image)
|
||||||
|
|
||||||
|
# 1.5~2.0ms
|
||||||
|
yuv = cv2.cvtColor(image, cv2.COLOR_RGB2YUV)
|
||||||
|
v = yuv[:, :, 2]
|
||||||
|
h = yuv[:, :, 0]
|
||||||
|
# 4.0~4.5ms
|
||||||
|
if enemy:
|
||||||
|
self.predict_enemy(h.copy(), v.copy())
|
||||||
|
# 3.0~3.5ms
|
||||||
|
if item:
|
||||||
|
self.predict_item(v.copy())
|
||||||
|
|
||||||
|
if show_log:
|
||||||
|
kv = {}
|
||||||
|
if self.aimed_enemy:
|
||||||
|
kv['enemy'] = self.aimed_enemy
|
||||||
|
if self.aimed_item:
|
||||||
|
kv['item'] = self.aimed_item
|
||||||
|
if kv:
|
||||||
|
logger.info(f'Aimed: {dict_to_kv(kv)}')
|
||||||
|
if debug:
|
||||||
|
self.show_aim()
|
||||||
|
|
||||||
|
def show_aim(self):
|
||||||
|
if self.draw_enemy is None:
|
||||||
|
if self.draw_item is None:
|
||||||
|
return
|
||||||
|
else:
|
||||||
|
r = g = b = self.draw_item
|
||||||
|
else:
|
||||||
|
if self.draw_item is None:
|
||||||
|
r = g = b = self.draw_enemy
|
||||||
|
else:
|
||||||
|
r = self.draw_enemy
|
||||||
|
g = b = self.draw_item
|
||||||
|
|
||||||
|
image = cv2.merge([b, g, r])
|
||||||
|
|
||||||
|
cv2.imshow('AimDetector', image)
|
||||||
|
cv2.waitKey(1)
|
||||||
|
|
||||||
|
@cached_property
|
||||||
|
def aimed_enemy(self) -> tuple[int, int] | None:
|
||||||
|
if self.points_enemy is None:
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
_ = self.points_enemy[1]
|
||||||
|
logger.warning(f'Multiple aimed enemy found, using first point of {self.points_enemy}')
|
||||||
|
except IndexError:
|
||||||
|
pass
|
||||||
|
try:
|
||||||
|
point = self.points_enemy[0]
|
||||||
|
return tuple(point)
|
||||||
|
except IndexError:
|
||||||
|
return None
|
||||||
|
|
||||||
|
@cached_property
|
||||||
|
def aimed_item(self) -> tuple[int, int] | None:
|
||||||
|
if self.points_item is None:
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
_ = self.points_item[1]
|
||||||
|
logger.warning(f'Multiple aimed item found, using first point of {self.points_item}')
|
||||||
|
except IndexError:
|
||||||
|
pass
|
||||||
|
try:
|
||||||
|
point = self.points_item[0]
|
||||||
|
return tuple(point)
|
||||||
|
except IndexError:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
class AimDetectorMixin(UI):
|
||||||
|
@cached_property
|
||||||
|
def aim(self):
|
||||||
|
return Aim()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
"""
|
||||||
|
Test
|
||||||
|
"""
|
||||||
|
self = AimDetectorMixin('src')
|
||||||
|
self.device.disable_stuck_detection()
|
||||||
|
|
||||||
|
while 1:
|
||||||
|
self.device.screenshot()
|
||||||
|
self.aim.predict(self.device.image, debug=True)
|
Loading…
Reference in New Issue
Block a user