Launch botty from town or main menu; fix character recovery after restart/death (#494)
* Add ability to launch botty from either main menu or town * Fix game recovery routine is stuck forever on character selection screen * Move _char_template to class variable to be able to share among instances * fix character recovery and character select, support offline/online. WIP Co-authored-by: Vladimir Makayev <[email protected]> Co-authored-by: mgleed <[email protected]>
This commit is contained in:
committed by
GitHub
co-authored by
Vladimir Makayev
mgleed
parent
c57e820d63
commit
c358282e68
Binary file not shown.
|
After Width: | Height: | Size: 13 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 10 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 10 KiB |
+5
-8
@@ -20,11 +20,6 @@ screen_width=1280
|
||||
screen_height=720
|
||||
center_x=640
|
||||
center_y=360
|
||||
ingame_ref_x=625
|
||||
ingame_ref_y=669
|
||||
offline_width=105
|
||||
char_selection_center_x=1050
|
||||
char_selection_center_y=270
|
||||
; some distance
|
||||
skill_bar_height=67
|
||||
save_and_exit_x=633
|
||||
@@ -121,13 +116,15 @@ no_pickup=5,530,240,65
|
||||
repair_needed=1000,0,280,280
|
||||
wp_act_roi=42,66,365,53
|
||||
stash_btn_roi=20,52,412,53
|
||||
offline_tab= 1100,10,150,40
|
||||
char_selection_top=1047,50,160,37
|
||||
char_selection_all=1042,45,170,550
|
||||
enemy_info=440,15,405,150
|
||||
character_select=1033, 44, 226, 554
|
||||
character_online_status=1033, 19, 226, 25
|
||||
; character_sub_roi is with respect to matched active character template
|
||||
character_name_sub_roi=8, 22, 192, 32
|
||||
cube_area_roi=167,209,113,152
|
||||
cube_btn_roi=160,368,125,57
|
||||
|
||||
|
||||
[path]
|
||||
; static pathes in format: x0,y0, x1,y1, x2,y2, ...
|
||||
pindle_safe_dist=921,35, 1123,70, 960,88
|
||||
|
||||
+51
-17
@@ -9,7 +9,6 @@ from copy import copy
|
||||
from typing import Union
|
||||
from collections import OrderedDict
|
||||
from transmute import Transmute
|
||||
|
||||
from utils.misc import wait
|
||||
from game_stats import GameStats
|
||||
from logger import Logger
|
||||
@@ -19,8 +18,9 @@ from template_finder import TemplateFinder
|
||||
from char import IChar
|
||||
from item import ItemFinder
|
||||
from item.pickit import PickIt
|
||||
from ui import UiManager
|
||||
from ui import UiManager, char_selector
|
||||
from ui import BeltManager
|
||||
from ui import CharSelector
|
||||
from pather import Pather, Location
|
||||
from npc_manager import NpcManager
|
||||
from health_manager import HealthManager
|
||||
@@ -34,13 +34,15 @@ from char.basic import Basic
|
||||
from char.basic_ranged import Basic_Ranged
|
||||
|
||||
from run import Pindle, ShenkEld, Trav, Nihlathak, Arcane, Diablo
|
||||
from town import TownManager, A1, A2, A3, A4, A5
|
||||
from town import TownManager, A1, A2, A3, A4, A5, town_manager
|
||||
|
||||
# Added for dclone ip hunt
|
||||
from messages import Messenger
|
||||
from utils.dclone_ip import get_d2r_game_ip
|
||||
|
||||
class Bot:
|
||||
_MAIN_MENU_MARKERS = ["MAIN_MENU_TOP_LEFT","MAIN_MENU_TOP_LEFT_DARK"]
|
||||
|
||||
def __init__(self, screen: Screen, game_stats: GameStats, template_finder: TemplateFinder, pick_corpse: bool = False):
|
||||
self._screen = screen
|
||||
self._game_stats = game_stats
|
||||
@@ -123,11 +125,15 @@ class Bot:
|
||||
self._current_threads = []
|
||||
self._no_stash_counter = 0
|
||||
self._ran_no_pickup = False
|
||||
self._char_selector = CharSelector(self._screen, self._template_finder)
|
||||
|
||||
# Create State Machine
|
||||
self._states=['hero_selection', 'town', 'pindle', 'shenk', 'trav', 'nihlathak', 'arcane', 'diablo']
|
||||
self._states=['initialization','hero_selection', 'town', 'pindle', 'shenk', 'trav', 'nihlathak', 'arcane', 'diablo']
|
||||
self._transitions = [
|
||||
{ 'trigger': 'create_game', 'source': 'hero_selection', 'dest': 'town', 'before': "on_create_game"},
|
||||
{ 'trigger': 'init', 'source': 'initialization', 'dest': '=','before': "on_init"},
|
||||
{ 'trigger': 'select_character', 'source': 'initialization', 'dest': 'hero_selection', 'before': "on_select_character"},
|
||||
{ 'trigger': 'start_from_town', 'source': ['initialization', 'hero_selection'], 'dest': 'town', 'before': "on_start_from_town"},
|
||||
{ 'trigger': 'create_game', 'source': 'hero_selection', 'dest': '=', 'before': "on_create_game"},
|
||||
# Tasks within town
|
||||
{ 'trigger': 'maintenance', 'source': 'town', 'dest': 'town', 'before': "on_maintenance"},
|
||||
# Different runs
|
||||
@@ -139,16 +145,16 @@ class Bot:
|
||||
{ 'trigger': 'run_diablo', 'source': 'town', 'dest': 'nihlathak', 'before': "on_run_diablo"},
|
||||
# End run / game
|
||||
{ 'trigger': 'end_run', 'source': ['shenk', 'pindle', 'nihlathak', 'trav', 'arcane', 'diablo'], 'dest': 'town', 'before': "on_end_run"},
|
||||
{ 'trigger': 'end_game', 'source': ['town', 'shenk', 'pindle', 'nihlathak', 'trav', 'arcane', 'diablo','end_run'], 'dest': 'hero_selection', 'before': "on_end_game"},
|
||||
{ 'trigger': 'end_game', 'source': ['town', 'shenk', 'pindle', 'nihlathak', 'trav', 'arcane', 'diablo','end_run'], 'dest': 'initialization', 'before': "on_end_game"},
|
||||
]
|
||||
self.machine = Machine(model=self, states=self._states, initial="hero_selection", transitions=self._transitions, queued=True)
|
||||
self.machine = Machine(model=self, states=self._states, initial="initialization", transitions=self._transitions, queued=True)
|
||||
self._transmute = Transmute(self._screen, self._template_finder, self._game_stats, self._ui_manager)
|
||||
|
||||
|
||||
def draw_graph(self):
|
||||
# Draw the whole graph, graphviz binaries must be installed and added to path for this!
|
||||
from transitions.extensions import GraphMachine
|
||||
self.machine = GraphMachine(model=self, states=self._states, initial="hero_selection", transitions=self._transitions, queued=True)
|
||||
self.machine = GraphMachine(model=self, states=self._states, initial="initialization", transitions=self._transitions, queued=True)
|
||||
self.machine.get_graph().draw('my_state_diagram.png', prog='dot')
|
||||
|
||||
def get_belt_manager(self) -> BeltManager:
|
||||
@@ -158,7 +164,7 @@ class Bot:
|
||||
return self._curr_loc
|
||||
|
||||
def start(self):
|
||||
self.trigger('create_game')
|
||||
self.trigger_or_stop('init')
|
||||
|
||||
def stop(self):
|
||||
self._stopping = True
|
||||
@@ -196,14 +202,42 @@ class Bot:
|
||||
break
|
||||
return not found_unfinished_run
|
||||
|
||||
def on_create_game(self):
|
||||
keyboard.release(self._config.char["stand_still"])
|
||||
# Start a game from hero selection
|
||||
self._game_stats.log_start_game()
|
||||
self._template_finder.search_and_wait(["MAIN_MENU_TOP_LEFT","MAIN_MENU_TOP_LEFT_DARK"], roi=self._config.ui_roi["main_menu_top_left"])
|
||||
if not self._ui_manager.start_game(): return
|
||||
self._curr_loc = self._town_manager.wait_for_town_spawn()
|
||||
def _rebuild_as_asset_to_trigger(trigger_to_assets: dict):
|
||||
result = {}
|
||||
for key in trigger_to_assets.keys():
|
||||
for asset in trigger_to_assets[key]:
|
||||
result[asset] = key
|
||||
return result
|
||||
|
||||
def on_init(self):
|
||||
keyboard.release(self._config.char["stand_still"])
|
||||
transition_to_screens = Bot._rebuild_as_asset_to_trigger({
|
||||
"select_character": Bot._MAIN_MENU_MARKERS,
|
||||
"start_from_town": town_manager.TOWN_MARKERS,
|
||||
})
|
||||
match = self._template_finder.search_and_wait(list(transition_to_screens.keys()), best_match=True)
|
||||
self.trigger_or_stop(transition_to_screens[match.name])
|
||||
|
||||
def on_select_character(self):
|
||||
if self._config.general['restart_d2r_when_stuck']:
|
||||
# Make sure the correct char is selected
|
||||
if self._char_selector.has_char_template_saved():
|
||||
self._char_selector.select_char()
|
||||
else:
|
||||
self._char_selector.save_char_online_status()
|
||||
self._char_selector.save_char_template()
|
||||
|
||||
self.trigger_or_stop("create_game")
|
||||
|
||||
def on_create_game(self):
|
||||
# Start a game from hero selection
|
||||
self._template_finder.search_and_wait(Bot._MAIN_MENU_MARKERS, roi=self._config.ui_roi["main_menu_top_left"])
|
||||
if not self._ui_manager.start_game(): return
|
||||
self.trigger_or_stop("start_from_town")
|
||||
|
||||
def on_start_from_town(self):
|
||||
self._game_stats.log_start_game()
|
||||
self._curr_loc = self._town_manager.wait_for_town_spawn()
|
||||
# Check for the current game ip and pause if we are able to obtain the hot ip
|
||||
if self._config.dclone["region_ips"] != "" and self._config.dclone["dclone_hotip"] != "":
|
||||
cur_game_ip = get_d2r_game_ip()
|
||||
@@ -349,7 +383,7 @@ class Bot:
|
||||
if self._config.general["randomize_runs"]:
|
||||
self.shuffle_runs()
|
||||
wait(0.2, 0.5)
|
||||
self.trigger_or_stop("create_game")
|
||||
self.trigger_or_stop("init")
|
||||
|
||||
def on_end_run(self):
|
||||
if not self._config.char["pre_buff_every_run"]:
|
||||
|
||||
+3
-13
@@ -14,7 +14,6 @@ from health_manager import HealthManager
|
||||
from logger import Logger
|
||||
from messages import Messenger
|
||||
from screen import Screen
|
||||
from ui.char_selector import CharSelector
|
||||
from utils.restart import restart_game
|
||||
from utils.misc import kill_thread, set_d2r_always_on_top, restore_d2r_window_visibility
|
||||
|
||||
@@ -34,17 +33,8 @@ class GameController:
|
||||
self.game_controller_thread = None
|
||||
self.bot_thread = None
|
||||
self.bot = None
|
||||
self.char_selector = None
|
||||
|
||||
def run_bot(self, pick_corpse: bool = False):
|
||||
if self._config.general['restart_d2r_when_stuck']:
|
||||
# Make sure the correct char is selected
|
||||
if self.char_selector.has_char_template_saved():
|
||||
Logger.info("Selecting original char")
|
||||
self.char_selector.select_char()
|
||||
else:
|
||||
Logger.info("Saving top-most char as template")
|
||||
self.char_selector.save_char_template()
|
||||
def run_bot(self, pick_corpse: bool = False):
|
||||
# Start bot thread
|
||||
self.bot = Bot(self.screen, self.game_stats, self.template_finder, pick_corpse)
|
||||
self.bot_thread = threading.Thread(target=self.bot.start)
|
||||
@@ -100,6 +90,7 @@ class GameController:
|
||||
if restart_game(self._config.general["d2r_path"]):
|
||||
self.game_stats.log_end_game(failed=max_game_length_reached)
|
||||
if self.setup_screen():
|
||||
self.template_finder = TemplateFinder(self.screen)
|
||||
self.start_health_manager_thread()
|
||||
self.start_death_manager_thread()
|
||||
self.game_recovery = GameRecovery(self.screen, self.death_manager, self.template_finder)
|
||||
@@ -124,8 +115,7 @@ class GameController:
|
||||
self.start_health_manager_thread()
|
||||
self.start_death_manager_thread()
|
||||
self.game_recovery = GameRecovery(self.screen, self.death_manager, self.template_finder)
|
||||
self.game_stats = GameStats()
|
||||
self.char_selector = CharSelector(self.screen, self.template_finder)
|
||||
self.game_stats = GameStats()
|
||||
self.start_game_controller_thread()
|
||||
self.is_running = True
|
||||
|
||||
|
||||
+4
-16
@@ -35,19 +35,6 @@ class GameRecovery:
|
||||
# lets just see if you might already be at hero selection
|
||||
found = self._template_finder.search(["MAIN_MENU_TOP_LEFT","MAIN_MENU_TOP_LEFT_DARK"], self._screen.grab(), roi=self._config.ui_roi["main_menu_top_left"]).valid
|
||||
if found:
|
||||
offline_tab = self._template_finder.search(["OFFLINE_TAB","OFFLINE_TAB_DARK"], self._screen.grab(), roi=self._config.ui_roi["offline_tab"], threshold=0.8, best_match = True)
|
||||
if offline_tab.valid:
|
||||
if offline_tab.name == "OFFLINE_TAB_DARK":
|
||||
#We need to press escape before clicking the tab
|
||||
keyboard.send("esc")
|
||||
time.sleep(0.2)
|
||||
# Right now it selects the tab to the left of offline
|
||||
offline_width = self._config.ui_pos["offline_width"]
|
||||
tab = (offline_tab.center[0] - offline_width , offline_tab.center[1])
|
||||
x, y = self._screen.convert_screen_to_monitor(tab)
|
||||
mouse.move(x, y, randomize=8)
|
||||
time.sleep(0.5)
|
||||
mouse.click(button="left")
|
||||
return True
|
||||
# would have been too easy, maybe we have died?
|
||||
if self._death_manager.handle_death_screen():
|
||||
@@ -70,6 +57,7 @@ if __name__ == "__main__":
|
||||
keyboard.add_hotkey('f12', lambda: os._exit(1))
|
||||
keyboard.wait("f11")
|
||||
screen = Screen()
|
||||
death_manager = DeathManager(screen)
|
||||
game_recovery = GameRecovery(screen, death_manager)
|
||||
game_recovery.go_to_hero_selection()
|
||||
template_finder = TemplateFinder(screen)
|
||||
death_manager = DeathManager(screen, template_finder)
|
||||
game_recovery = GameRecovery(screen, death_manager, template_finder)
|
||||
print(game_recovery.go_to_hero_selection())
|
||||
|
||||
@@ -9,8 +9,16 @@ from ui import UiManager
|
||||
from town import IAct, A1, A2, A3, A4, A5
|
||||
from utils.misc import wait
|
||||
|
||||
TOWN_MARKERS = [
|
||||
"A5_TOWN_0", "A5_TOWN_1",
|
||||
"A4_TOWN_4", "A4_TOWN_5",
|
||||
"A3_TOWN_0", "A3_TOWN_1",
|
||||
"A2_TOWN_0", "A2_TOWN_1", "A2_TOWN_10",
|
||||
"A1_TOWN_1", "A1_TOWN_3"
|
||||
]
|
||||
|
||||
class TownManager:
|
||||
|
||||
def __init__(self, template_finder: TemplateFinder, ui_manager: UiManager, item_finder: ItemFinder, a1: A1, a2: A2, a3: A3, a4: A4, a5: A5):
|
||||
self._config = Config()
|
||||
self._template_finder = template_finder
|
||||
@@ -44,13 +52,7 @@ class TownManager:
|
||||
:param time_out: Optional float value for time out in seconds, defaults to None
|
||||
:return: Location of the town (e.g. Location.A4_TOWN_START) or None if nothing was found within time_out time
|
||||
"""
|
||||
template_match = self._template_finder.search_and_wait([
|
||||
"A5_TOWN_0", "A5_TOWN_1",
|
||||
"A4_TOWN_4", "A4_TOWN_5",
|
||||
"A3_TOWN_0", "A3_TOWN_1",
|
||||
"A2_TOWN_0", "A2_TOWN_1", "A2_TOWN_10",
|
||||
"A1_TOWN_1", "A1_TOWN_3"
|
||||
], best_match=True, time_out=time_out)
|
||||
template_match = self._template_finder.search_and_wait(TOWN_MARKERS, best_match=True, time_out=time_out)
|
||||
if template_match.valid:
|
||||
return TownManager.get_act_from_location(template_match.name)
|
||||
return None
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
from .ui_manager import UiManager
|
||||
from .belt_manager import BeltManager
|
||||
from .char_selector import CharSelector
|
||||
|
||||
+94
-27
@@ -1,52 +1,114 @@
|
||||
from operator import is_
|
||||
import cv2
|
||||
import numpy as np
|
||||
import time
|
||||
|
||||
from config import Config
|
||||
from utils.custom_mouse import mouse
|
||||
from utils.misc import cut_roi, wait
|
||||
from utils.misc import cut_roi, roi_center, wait, is_in_roi
|
||||
from screen import Screen
|
||||
from template_finder import TemplateFinder
|
||||
|
||||
from logger import Logger
|
||||
|
||||
class CharSelector:
|
||||
_last_char_template = None
|
||||
_online_character = None
|
||||
|
||||
def __init__(self, screen: Screen, template_finder: TemplateFinder):
|
||||
self._config = Config()
|
||||
self._char_template = None
|
||||
self._screen = screen
|
||||
self._template_finder = template_finder
|
||||
|
||||
def has_char_template_saved(self):
|
||||
return self._char_template is not None
|
||||
return CharSelector._last_char_template is not None
|
||||
|
||||
def save_char_template(self):
|
||||
img = self._screen.grab()
|
||||
# TODO: check which character is currently selected rather than assuming the top one is the correct one
|
||||
self._char_template = cut_roi(img, self._config.ui_roi["char_selection_top"])
|
||||
active_character = self._template_finder.search("CHARACTER_ACTIVE", img, roi = self._config.ui_roi["character_select"], threshold = 0.8)
|
||||
if active_character.valid:
|
||||
x, y, w, h = self._config.ui_roi["character_name_sub_roi"]
|
||||
x, y = x + active_character.region[0], y + active_character.region[1]
|
||||
char_template = cut_roi(img, [x, y, w, h])
|
||||
if self._config.general["info_screenshots"]:
|
||||
cv2.imwrite("./info_screenshots/saved_character_" + time.strftime("%Y%m%d_%H%M%S") + ".png", char_template)
|
||||
Logger.debug(f"Saved character template")
|
||||
else:
|
||||
Logger.error("save_char_template: Could not save character template")
|
||||
return
|
||||
CharSelector._last_char_template = char_template
|
||||
|
||||
def save_char_online_status(self):
|
||||
online_tabs = self._template_finder.search(["CHARACTER_STATE_OFFLINE","CHARACTER_STATE_ONLINE"], self._screen.grab(), roi=self._config.ui_roi["character_online_status"], threshold=0.8, best_match = True)
|
||||
if online_tabs.valid:
|
||||
if online_tabs.name == "CHARACTER_STATE_ONLINE":
|
||||
online_status = True
|
||||
else:
|
||||
online_status = False
|
||||
Logger.debug(f"Saved online status. Online={online_status}")
|
||||
else:
|
||||
Logger.error("save_char_online_status: Could not determine character's online status")
|
||||
return
|
||||
CharSelector._online_character = online_status
|
||||
|
||||
def select_online_tab(self, region: tuple[int, int, int, int] = None, center: tuple[int, int] = None):
|
||||
btn_width = center[0] - region[0]
|
||||
if CharSelector._online_character:
|
||||
Logger.debug(f"Selecting online tab")
|
||||
x = region[0] + (btn_width / 2)
|
||||
else:
|
||||
Logger.debug(f"Selecting offline tab")
|
||||
x = region[0] + (3 * btn_width / 2)
|
||||
pos = self._screen.convert_screen_to_monitor((x, center[1]))
|
||||
# move cursor to result and select
|
||||
mouse.move(*pos)
|
||||
wait(0.4, 0.6)
|
||||
mouse.click(button="left")
|
||||
wait(0.4, 0.6)
|
||||
|
||||
def select_char(self):
|
||||
if self._char_template is not None:
|
||||
if CharSelector._last_char_template is not None:
|
||||
online_tabs = self._template_finder.search(["CHARACTER_STATE_OFFLINE","CHARACTER_STATE_ONLINE"], self._screen.grab(), roi=self._config.ui_roi["character_online_status"], threshold=0.8, best_match = True)
|
||||
if online_tabs.valid:
|
||||
if online_tabs.name == "CHARACTER_STATE_ONLINE" and (not CharSelector._online_character):
|
||||
self.select_online_tab(online_tabs.region, online_tabs.center)
|
||||
elif online_tabs.name == "CHARACTER_STATE_OFFLINE" and (CharSelector._online_character):
|
||||
self.select_online_tab(online_tabs.region, online_tabs.center)
|
||||
wait(1, 1.5)
|
||||
else:
|
||||
Logger.error("select_char: Could not find online/offline tabs")
|
||||
return
|
||||
img = self._screen.grab()
|
||||
active_char = self._template_finder.search("CHARACTER_ACTIVE", img, roi = self._config.ui_roi["character_select"], threshold = 0.8, normalize_monitor = True)
|
||||
if not active_char.valid:
|
||||
Logger.error("select_char: Could not find highlighted profile")
|
||||
return
|
||||
scrolls_attempts = 0
|
||||
while scrolls_attempts < 2: # 2 scrolls should suffice to see all possible characters
|
||||
template_result = self._template_finder.search(
|
||||
self._char_template,
|
||||
self._screen.grab(),
|
||||
threshold=0.8,
|
||||
roi=self._config.ui_roi["char_selection_all"],
|
||||
normalize_monitor=True
|
||||
)
|
||||
if template_result.valid:
|
||||
# move cursor to result and select
|
||||
mouse.move(*template_result.center)
|
||||
wait(0.5)
|
||||
mouse.click(button="left")
|
||||
break
|
||||
while scrolls_attempts < 2:
|
||||
if scrolls_attempts > 0:
|
||||
img = self._screen.grab()
|
||||
desired_char = self._template_finder.search(CharSelector._last_char_template, img, roi = self._config.ui_roi["character_select"], threshold = 0.8, normalize_monitor = True)
|
||||
if desired_char.valid:
|
||||
if is_in_roi(active_char.region, desired_char.center) and scrolls_attempts == 0:
|
||||
Logger.debug("Saved character template found and already highlighted, continue")
|
||||
return
|
||||
else:
|
||||
Logger.debug("Selecting saved character")
|
||||
mouse.move(*desired_char.center)
|
||||
wait(0.4, 0.6)
|
||||
mouse.click(button="left")
|
||||
wait(0.4), 0.6
|
||||
return
|
||||
else:
|
||||
Logger.debug("Highlighted profile found but saved character not in view, scroll")
|
||||
# We can scroll the characters only if we have the mouse in the char names selection so move the mouse there
|
||||
pos = (self._config.ui_pos["char_selection_center_x"], self._config.ui_pos["char_selection_center_y"])
|
||||
pos = self._screen.convert_screen_to_monitor(pos)
|
||||
mouse.move(*pos)
|
||||
wait(0.5)
|
||||
center = roi_center(self._config.ui_roi["character_select"])
|
||||
center = self._screen.convert_screen_to_monitor(center)
|
||||
mouse.move(*center)
|
||||
wait(0.4, 0.6)
|
||||
mouse.wheel(-14)
|
||||
scrolls_attempts += 1
|
||||
wait(0.5)
|
||||
return False
|
||||
wait(0.4, 0.6)
|
||||
Logger.error(f"select_char: unable to find saved profile after {scrolls_attempts} scroll attempts")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
@@ -59,4 +121,9 @@ if __name__ == "__main__":
|
||||
selector = CharSelector(screen, tf)
|
||||
if not selector.has_char_template_saved():
|
||||
selector.save_char_template()
|
||||
print("Move D2R window, select another character and press F11 to continue")
|
||||
keyboard.wait("f11")
|
||||
screen = Screen()
|
||||
tf = TemplateFinder(screen)
|
||||
selector = CharSelector(screen,tf)
|
||||
selector.select_char()
|
||||
|
||||
+1
-1
@@ -112,7 +112,7 @@ def erode_to_black(img: np.ndarray, threshold: int = 14):
|
||||
img = cv2.bitwise_and(img, mask_color_r)
|
||||
return img
|
||||
|
||||
def roi_center(roi: list[float] = None, scale: float = 1):
|
||||
def roi_center(roi: list[float] = None):
|
||||
x, y, w, h = roi
|
||||
return round(x + w/2), round(y + h/2)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user