Merge pull request #182 from LmeSzinc/bug_fix

Bug fix
This commit is contained in:
LmeSzinc 2023-11-03 18:58:41 +08:00 committed by GitHub
commit c77d849faa
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
27 changed files with 332 additions and 114 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 105 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

View File

Before

Width:  |  Height:  |  Size: 82 KiB

After

Width:  |  Height:  |  Size: 82 KiB

View File

Before

Width:  |  Height:  |  Size: 6.7 KiB

After

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

View File

@ -89,12 +89,15 @@ def readable_time(before: str) -> str:
elif diff < 60:
# < 1 min
return t("Gui.Dashboard.JustNow")
elif diff < 3600:
elif diff < 5400:
# < 90 min
return t("Gui.Dashboard.MinutesAgo", time=int(diff // 60))
elif diff < 86400:
elif diff < 129600:
# < 36 hours
return t("Gui.Dashboard.HoursAgo", time=int(diff // 3600))
elif diff < 1296000:
# < 15 days
return t("Gui.Dashboard.DaysAgo", time=int(diff // 86400))
else:
# > 15 days
# >= 15 days
return t("Gui.Dashboard.LongTimeAgo")

View File

@ -455,7 +455,11 @@ def get_localstorage(key):
def re_fullmatch(pattern, string):
if pattern == "datetime":
pattern = RE_DATETIME
try:
datetime.datetime.fromisoformat(string)
return True
except ValueError:
return False
# elif:
return re.fullmatch(pattern=pattern, string=string)

View File

@ -13,6 +13,23 @@ BATTLE_PASS_NOTIFICATION = ButtonWrapper(
button=(895, 595, 1180, 630),
),
)
GET_LIGHT_CONE = ButtonWrapper(
name='GET_LIGHT_CONE',
cn=Button(
file='./assets/cn/base/popup/GET_LIGHT_CONE.png',
area=(205, 321, 242, 339),
search=(185, 301, 262, 359),
color=(130, 130, 131),
button=(205, 321, 242, 339),
),
en=Button(
file='./assets/en/base/popup/GET_LIGHT_CONE.png',
area=(260, 322, 306, 338),
search=(240, 302, 326, 358),
color=(147, 147, 148),
button=(260, 322, 306, 338),
),
)
GET_REWARD = ButtonWrapper(
name='GET_REWARD',
share=Button(

View File

@ -96,3 +96,20 @@ class PopupHandler(ModuleBase):
return True
return False
def handle_get_light_cone(self, interval=2) -> bool:
"""
Popup when getting a light cone from Echo of War.
Args:
interval:
Returns:
If handled.
"""
if self.appear(GET_LIGHT_CONE, interval=interval):
logger.info(f'{GET_LIGHT_CONE} -> {GET_REWARD}')
self.device.click(GET_REWARD)
return True
return False

View File

@ -321,6 +321,8 @@ class UI(MainPage):
return True
if self.handle_monthly_card_reward():
return True
if self.handle_get_light_cone():
return True
if self.appear(COMBAT_PREPARE, interval=5):
logger.info(f'UI additional: {COMBAT_PREPARE} -> {CLOSE}')
self.device.click(CLOSE)

View File

@ -13,6 +13,16 @@ DUNGEON_COMBAT_INTERACT = ButtonWrapper(
button=(750, 411, 997, 448),
),
)
DUNGEON_COMBAT_INTERACT_TEXT = ButtonWrapper(
name='DUNGEON_COMBAT_INTERACT_TEXT',
share=Button(
file='./assets/share/combat/interact/DUNGEON_COMBAT_INTERACT_TEXT.png',
area=(790, 391, 1055, 456),
search=(770, 371, 1075, 476),
color=(47, 51, 53),
button=(790, 391, 1055, 456),
),
)
MAP_LOADING = ButtonWrapper(
name='MAP_LOADING',
share=Button(

View File

@ -65,27 +65,6 @@ class Combat(CombatInteract, CombatPrepare, CombatState, CombatTeam, CombatSuppo
return False
def combat_enter_from_map(self, skip_first_screenshot=True):
"""
Pages:
in: page_main, DUNGEON_COMBAT_INTERACT
out: COMBAT_PREPARE
"""
logger.info('Combat enter from map')
while 1:
if skip_first_screenshot:
skip_first_screenshot = False
else:
self.device.screenshot()
if self.appear(COMBAT_PREPARE):
# Confirm page loaded
if self.image_color_count(COMBAT_PREPARE.button, color=(230, 230, 230), threshold=240, count=400):
logger.info(f'At {COMBAT_PREPARE}')
break
if self.handle_combat_interact():
continue
def combat_prepare(self, team=1, support_character: str = None):
"""
Args:
@ -306,6 +285,8 @@ class Combat(CombatInteract, CombatPrepare, CombatState, CombatTeam, CombatSuppo
logger.info(f'{COMBAT_AGAIN} -> {COMBAT_EXIT}')
self.device.click(COMBAT_EXIT)
continue
if self.handle_get_light_cone():
continue
def is_trailblaze_power_exhausted(self) -> bool:
flag = self.config.stored.TrailblazePower.value < self.combat_wave_cost

View File

@ -1,6 +1,8 @@
from module.base.utils import color_similar, get_color
from module.logger import logger
from tasks.base.ui import UI
from tasks.combat.assets.assets_combat_interact import DUNGEON_COMBAT_INTERACT, MAP_LOADING
from tasks.combat.assets.assets_combat_prepare import COMBAT_PREPARE
from tasks.map.assets.assets_map_control import A_BUTTON
@ -15,6 +17,27 @@ class CombatInteract(UI):
return False
def combat_enter_from_map(self, skip_first_screenshot=True):
"""
Pages:
in: page_main, DUNGEON_COMBAT_INTERACT
out: COMBAT_PREPARE
"""
logger.info('Combat enter from map')
while 1:
if skip_first_screenshot:
skip_first_screenshot = False
else:
self.device.screenshot()
if self.appear(COMBAT_PREPARE):
# Confirm page loaded
if self.image_color_count(COMBAT_PREPARE.button, color=(230, 230, 230), threshold=240, count=400):
logger.info(f'At {COMBAT_PREPARE}')
break
if self.handle_combat_interact():
continue
def is_map_loading(self):
if self.appear(MAP_LOADING, similarity=0.75):
return True

View File

@ -35,18 +35,11 @@ OCR_DOUBLE_EVENT_REMAIN = ButtonWrapper(
)
OCR_DOUBLE_EVENT_REMAIN_AT_COMBAT = ButtonWrapper(
name='OCR_DOUBLE_EVENT_REMAIN_AT_COMBAT',
cn=Button(
file='./assets/cn/dungeon/event/OCR_DOUBLE_EVENT_REMAIN_AT_COMBAT.png',
area=(872, 515, 1192, 538),
search=(852, 495, 1212, 558),
color=(171, 139, 76),
button=(872, 515, 1192, 538),
),
en=Button(
file='./assets/en/dungeon/event/OCR_DOUBLE_EVENT_REMAIN_AT_COMBAT.png',
area=(814, 510, 1246, 530),
search=(794, 490, 1266, 550),
color=(194, 158, 86),
button=(814, 510, 1246, 530),
share=Button(
file='./assets/share/dungeon/event/OCR_DOUBLE_EVENT_REMAIN_AT_COMBAT.png',
area=(799, 460, 1268, 626),
search=(779, 440, 1280, 646),
color=(59, 53, 48),
button=(799, 460, 1268, 626),
),
)

View File

@ -0,0 +1,35 @@
from module.base.button import Button, ButtonWrapper
# This file was auto-generated, do not modify it manually. To generate:
# ``` python -m dev_tools.button_extract ```
OCR_SIMUNI_POINT = ButtonWrapper(
name='OCR_SIMUNI_POINT',
share=Button(
file='./assets/share/dungeon/state/OCR_SIMUNI_POINT.png',
area=(580, 237, 860, 277),
search=(560, 217, 880, 297),
color=(163, 170, 252),
button=(580, 237, 860, 277),
),
)
OCR_SIMUNI_POINT_OFFSET = ButtonWrapper(
name='OCR_SIMUNI_POINT_OFFSET',
share=Button(
file='./assets/share/dungeon/state/OCR_SIMUNI_POINT_OFFSET.png',
area=(685, 250, 717, 273),
search=(583, 187, 883, 387),
color=(199, 200, 250),
button=(685, 250, 717, 273),
),
)
OCR_STAMINA = ButtonWrapper(
name='OCR_STAMINA',
share=Button(
file='./assets/share/dungeon/state/OCR_STAMINA.png',
area=(675, 11, 1181, 64),
search=(655, 0, 1201, 84),
color=(75, 89, 125),
button=(675, 11, 1181, 64),
),
)

View File

@ -53,26 +53,6 @@ OCR_DUNGEON_NAV = ButtonWrapper(
button=(117, 182, 423, 641),
),
)
OCR_SIMUNI_POINT = ButtonWrapper(
name='OCR_SIMUNI_POINT',
share=Button(
file='./assets/share/dungeon/ui/OCR_SIMUNI_POINT.png',
area=(580, 237, 820, 277),
search=(560, 217, 840, 297),
color=(166, 168, 252),
button=(580, 237, 820, 277),
),
)
OCR_SIMUNI_POINT_OFFSET = ButtonWrapper(
name='OCR_SIMUNI_POINT_OFFSET',
share=Button(
file='./assets/share/dungeon/ui/OCR_SIMUNI_POINT_OFFSET.png',
area=(685, 250, 717, 273),
search=(583, 187, 883, 387),
color=(199, 200, 250),
button=(685, 250, 717, 273),
),
)
OCR_WEEKLY_LIMIT = ButtonWrapper(
name='OCR_WEEKLY_LIMIT',
share=Button(

View File

@ -45,8 +45,12 @@ class Dungeon(DungeonUI, DungeonEvent, Combat):
logger.info(f'Dungeon: {dungeon}, team={team}, wave_limit={wave_limit}, support_character={support_character}')
if not skip_ui_switch:
self.dungeon_tab_goto(KEYWORDS_DUNGEON_TAB.Survival_Index)
self.dungeon_goto(dungeon)
interact = self.get_dungeon_interact()
if interact is not None and interact == dungeon:
logger.info('Already nearby dungeon')
else:
self.dungeon_tab_goto(KEYWORDS_DUNGEON_TAB.Survival_Index)
self.dungeon_goto(dungeon)
if dungeon == KEYWORDS_DUNGEON_LIST.Stagnant_Shadow_Blaze:
if self.handle_destructible_around_blaze():
@ -59,7 +63,7 @@ class Dungeon(DungeonUI, DungeonEvent, Combat):
if (dungeon.is_Calyx_Golden or dungeon.is_Calyx_Crimson) and \
self.running_double and self.config.stored.DungeonDouble.calyx > 0:
calyx = self.get_double_event_remain_at_combat()
if calyx < self.config.stored.DungeonDouble.calyx:
if calyx is not None and calyx < self.config.stored.DungeonDouble.calyx:
self.config.stored.DungeonDouble.calyx = calyx
wave_limit = calyx
if calyx == 0:
@ -67,7 +71,7 @@ class Dungeon(DungeonUI, DungeonEvent, Combat):
if dungeon.is_Cavern_of_Corrosion and self.running_double and \
self.config.stored.DungeonDouble.relic > 0:
relic = self.get_double_event_remain_at_combat()
if relic < self.config.stored.DungeonDouble.relic:
if relic is not None and relic < self.config.stored.DungeonDouble.relic:
self.config.stored.DungeonDouble.relic = relic
wave_limit = relic
if relic == 0:

View File

@ -45,14 +45,13 @@ class DungeonEvent(UI):
def has_double_event_at_combat(self) -> bool:
"""
TODO: OCR_DOUBLE_EVENT_REMAIN_AT_COMBAT of relic may be different from that of calyx
Pages:
in: COMBAT_PREPARE
"""
has = self.image_color_count(
OCR_DOUBLE_EVENT_REMAIN_AT_COMBAT,
color=(252, 209, 123),
threshold=195, count=1000
color=(231, 188, 103),
threshold=240, count=1000
)
logger.attr('Double event at combat', has)
return has
@ -74,14 +73,23 @@ class DungeonEvent(UI):
logger.attr('Double event remain', remain)
return remain
def get_double_event_remain_at_combat(self) -> int:
def get_double_event_remain_at_combat(self) -> int | None:
"""
Pages:
in: COMBAT_PREPARE
"""
remain = 0
if self.has_double_event_at_combat():
remain = self._get_double_event_remain(
OCR_DOUBLE_EVENT_REMAIN_AT_COMBAT)
logger.attr('Double event remain at combat', remain)
return remain
if not self.has_double_event_at_combat():
logger.attr('Double event remain at combat', 0)
return 0
ocr = DoubleEventOcr(OCR_DOUBLE_EVENT_REMAIN_AT_COMBAT)
for row in ocr.detect_and_ocr(self.device.image):
if '/' not in row.ocr_text:
continue
remain, _, total = ocr.format_result(row.ocr_text)
if total in [3, 12]:
logger.attr('Double event remain at combat', remain)
return remain
logger.warning('Double event appears but failed to get remain')
logger.attr('Double event remain at combat', None)
return None

122
tasks/dungeon/state.py Normal file
View File

@ -0,0 +1,122 @@
import threading
from module.base.timer import Timer
from module.base.utils import crop
from module.logger import logger
from module.ocr.ocr import DigitCounter
from tasks.base.ui import UI
from tasks.dungeon.assets.assets_dungeon_state import OCR_SIMUNI_POINT, OCR_SIMUNI_POINT_OFFSET, OCR_STAMINA
class OcrSimUniPoint(DigitCounter):
def after_process(self, result):
result = super().after_process(result)
result = result.replace('O', '0').replace('o', '0')
return result
class DungeonState(UI):
def dungeon_get_simuni_point(self, image=None) -> int:
"""
Page:
in: page_guide, Survival_Index, Simulated_Universe
"""
logger.info('Get simulated universe points')
if image is None:
image = self.device.image
_ = OCR_SIMUNI_POINT_OFFSET.match_template(image)
OCR_SIMUNI_POINT.load_offset(OCR_SIMUNI_POINT_OFFSET)
area = (
OCR_SIMUNI_POINT.area[0],
OCR_SIMUNI_POINT.button[1],
OCR_SIMUNI_POINT.area[2],
OCR_SIMUNI_POINT.button[3],
)
ocr = OcrSimUniPoint(OCR_SIMUNI_POINT)
value, _, total = ocr.ocr_single_line(crop(image, area), direct_ocr=True)
if total and value <= total:
logger.attr('SimulatedUniverse', f'{value}/{total}')
self.config.stored.SimulatedUniverse.set(value, total)
return value
else:
logger.warning(f'Invalid SimulatedUniverse points: {value}/{total}')
return 0
def dungeon_update_stamina(self, image=None, skip_first_screenshot=True):
"""
Returns:
bool: If success
Pages:
in: page_guild, Survival_Index, Simulated_Universe
or page_rogue, LEVEL_CONFIRM
or rogue, REWARD_CLOSE
"""
ocr = DigitCounter(OCR_STAMINA)
timeout = Timer(1, count=2).start()
if image is None:
image = self.device.image
else:
skip_first_screenshot = True
while 1:
if skip_first_screenshot:
skip_first_screenshot = False
else:
self.device.screenshot()
stamina = (0, 0, 0)
immersifier = (0, 0, 0)
if timeout.reached():
logger.warning('dungeon_update_stamina() timeout')
return False
for row in ocr.detect_and_ocr(image):
if row.ocr_text.isdigit():
continue
if row.ocr_text == '+':
continue
if '/' not in row.ocr_text:
continue
data = ocr.format_result(row.ocr_text)
if data[2] == self.config.stored.TrailblazePower.FIXED_TOTAL:
stamina = data
if data[2] == self.config.stored.Immersifier.FIXED_TOTAL:
immersifier = data
if stamina[2] > 0 and immersifier[2] > 0:
break
if image is not None:
logger.warning('dungeon_update_stamina() ended')
return
stamina = stamina[0]
immersifier = immersifier[0]
logger.attr('TrailblazePower', stamina)
logger.attr('Imersifier', immersifier)
with self.config.multi_set():
self.config.stored.TrailblazePower.value = stamina
self.config.stored.Immersifier.value = immersifier
return True
def dungeon_update_simuni(self):
"""
Update rogue weekly points, stamina, immersifier
Run in a new thread to be faster as data is not used immediately
Page:
in: page_guide, Survival_Index, Simulated_Universe
"""
logger.info('dungeon_update_simuni')
def func(image):
logger.info('Update thread start')
with self.config.multi_set():
self.dungeon_get_simuni_point(image)
# self.dungeon_update_stamina(image)
thread = threading.Thread(target=func, args=(self.device.image,))
thread.start()

View File

@ -6,24 +6,28 @@ from module.base.base import ModuleBase
from module.base.button import ClickButton
from module.base.timer import Timer
from module.base.utils import get_color
from module.exception import ScriptError
from module.logger import logger
from module.ocr.ocr import DigitCounter, Ocr, OcrResultButton
from module.ocr.ocr import Ocr, OcrResultButton
from module.ocr.utils import split_and_pair_button_attr
from module.ui.draggable_list import DraggableList
from module.ui.switch import Switch
from tasks.base.page import page_guide
from tasks.base.ui import UI
from tasks.combat.assets.assets_combat_interact import DUNGEON_COMBAT_INTERACT, DUNGEON_COMBAT_INTERACT_TEXT
from tasks.combat.assets.assets_combat_prepare import COMBAT_PREPARE
from tasks.combat.interact import CombatInteract
from tasks.dungeon.assets.assets_dungeon_ui import *
from tasks.dungeon.keywords import (
DungeonList,
DungeonNav,
DungeonTab,
KEYWORDS_DUNGEON_ENTRANCE,
KEYWORDS_DUNGEON_LIST,
KEYWORDS_DUNGEON_NAV,
KEYWORDS_DUNGEON_TAB
)
from tasks.dungeon.keywords.classes import DungeonEntrance
from tasks.dungeon.state import DungeonState
class DungeonTabSwitch(Switch):
@ -78,13 +82,6 @@ class OcrDungeonList(Ocr):
return result
class OcrSimUniPoint(DigitCounter):
def after_process(self, result):
result = super().after_process(result)
result = result.replace('O', '0').replace('o', '0')
return result
class OcrDungeonListLimitEntrance(OcrDungeonList):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
@ -123,7 +120,7 @@ DUNGEON_LIST = DraggableDungeonList(
ocr_class=OcrDungeonList, search_button=OCR_DUNGEON_LIST)
class DungeonUI(UI):
class DungeonUI(DungeonState, CombatInteract):
def dungeon_tab_goto(self, state: DungeonTab):
"""
Args:
@ -233,31 +230,6 @@ class DungeonUI(UI):
logger.info('No Forgotten_Hall in list skip waiting')
return False
def dungeon_get_simuni_point(self) -> int:
"""
Page:
in: page_guide, Survival_Index, Simulated_Universe
"""
logger.info('Get simulated universe points')
_ = self.appear(OCR_SIMUNI_POINT_OFFSET)
OCR_SIMUNI_POINT.load_offset(OCR_SIMUNI_POINT_OFFSET)
area = (
OCR_SIMUNI_POINT.area[0],
OCR_SIMUNI_POINT.button[1],
OCR_SIMUNI_POINT.area[2],
OCR_SIMUNI_POINT.button[3],
)
ocr = OcrSimUniPoint(OCR_SIMUNI_POINT)
value, _, total = ocr.ocr_single_line(self.image_crop(area), direct_ocr=True)
if total and value <= total:
logger.attr('SimulatedUniverse', f'{value}/{total}')
self.config.stored.SimulatedUniverse.set(value, total)
return value
else:
logger.warning(f'Invalid SimulatedUniverse points: {value}/{total}')
return 0
def _dungeon_nav_goto(self, dungeon: DungeonList, skip_first_screenshot=True):
"""
Equivalent to `DUNGEON_NAV_LIST.select_row(dungeon.dungeon_nav, main=self)`
@ -301,7 +273,7 @@ class DungeonUI(UI):
logger.info('DUNGEON_NAV_LIST at top')
# Update points if possible
if DUNGEON_NAV_LIST.is_row_selected(button, main=self):
self.dungeon_get_simuni_point()
self.dungeon_update_simuni()
else:
# To start from any list states.
logger.info('DUNGEON_NAV_LIST not at top')
@ -404,6 +376,53 @@ class DungeonUI(UI):
logger.warning(f'Cannot find dungeon entrance of {dungeon}')
continue
def get_dungeon_interact(self) -> DungeonList | None:
"""
Pages:
in: page_main
"""
if not self.appear(DUNGEON_COMBAT_INTERACT):
logger.info('No dungeon interact')
return None
ocr = OcrDungeonList(DUNGEON_COMBAT_INTERACT_TEXT)
result = ocr.detect_and_ocr(self.device.image)
result = ' '.join([row.ocr_text for row in result])
# Calyx (Crimson): Bud of XXX -> Bud of XXX
result = re.sub(r'Calyx\s*\(.*?\):*', '', result)
# Stagnant Shadow: Shap XXX -> Shape of XXX
result = re.sub(r'Stagnant\s*Shadow[:\s]*\w*', 'Shape of', result)
# Cavern of Corrosion: Pa XXX -> Path of XXX
result = re.sub(r'Cavern\s*of\s*Corrosion[:\s]*\w*', 'Path of', result)
# Echo of War: XXX -> XXX
result = re.sub(r'Echo\s*of\s*War:*', '', result)
# Divine See -> Divine Seed
result = re.sub(r'Divine\s*\w*', 'Divine Seed', result)
# Destructio Beginning -> Destruction's Beginning
result = re.sub(r"Destruct[a-zA-Z0-9_']*", "Destruction's", result)
# Dungeons
try:
dungeon = DungeonList.find(result)
logger.attr('DungeonInteract', dungeon)
return dungeon
except ScriptError:
pass
# Simulated Universe returns Simulated_Universe_World_1
try:
dungeon = DungeonNav.find(result)
if dungeon == KEYWORDS_DUNGEON_NAV.Simulated_Universe:
dungeon = KEYWORDS_DUNGEON_LIST.Simulated_Universe_World_1
logger.attr('DungeonInteract', dungeon)
return dungeon
except ScriptError:
pass
# Unknown
logger.attr('DungeonInteract', None)
return None
def dungeon_goto(self, dungeon: DungeonList):
"""
Returns:

View File

@ -16,6 +16,7 @@ from module.base.utils import (
rgb2yuv
)
from module.logger import logger
from tasks.map.interact.aim import subtract_blur
from tasks.map.minimap.utils import (
convolve,
cubic_find_maximum,
@ -223,7 +224,7 @@ class Minimap(MapResource):
scale = self.DIRECTION_ROTATION_SCALE * self.DIRECTION_SEARCH_SCALE
mapping = cv2.resize(image, None, fx=scale, fy=scale, interpolation=cv2.INTER_NEAREST)
result = cv2.matchTemplate(self.ArrowRotateMap, mapping, cv2.TM_CCOEFF_NORMED)
result = cv2.subtract(result, cv2.GaussianBlur(result, (5, 5), 0))
subtract_blur(result, 5)
_, sim, _, loca = cv2.minMaxLoc(result)
loca = np.array(loca) / self.DIRECTION_SEARCH_SCALE // (self.DIRECTION_RADIUS * 2)
degree = int((loca[0] + loca[1] * 8) * 5)
@ -240,7 +241,7 @@ class Minimap(MapResource):
precise_map = self.ArrowRotateMapAll[row[0]:row[1], :]
result = cv2.matchTemplate(precise_map, mapping, cv2.TM_CCOEFF_NORMED)
result = cv2.subtract(result, cv2.GaussianBlur(result, (5, 5), 0))
subtract_blur(result, 5)
def to_map(x):
return int((x * self.DIRECTION_RADIUS * 2) * self.POSITION_SEARCH_SCALE)
@ -271,11 +272,10 @@ class Minimap(MapResource):
# Extract
minimap = self.get_minimap(image, radius=self.MINIMAP_RADIUS)
_, _, v = cv2.split(rgb2yuv(minimap))
image = rgb2yuv(minimap)[:, :, 2].copy()
image = cv2.subtract(128, v)
image = cv2.GaussianBlur(image, (3, 3), 0)
cv2.subtract(128, image, dst=image)
cv2.GaussianBlur(image, (3, 3), 0, dst=image)
# Expand circle into rectangle
remap = cv2.remap(image, *self.RotationRemapData, cv2.INTER_LINEAR)[d * 1 // 10:d * 6 // 10].astype(np.float32)
remap = cv2.resize(remap, None, fx=scale, fy=scale, interpolation=cv2.INTER_LINEAR)