Add harnesses from latest MarkBench

This commit is contained in:
Derek Hirotsu
2023-09-07 12:30:36 -07:00
parent fd46b4c661
commit a46021215c
433 changed files with 25274 additions and 0 deletions

View File

@@ -0,0 +1,72 @@
# Shadow of the Tomb Raider
## TODO's
- Iteration within the same game process.
- Accept resolution as a separate argument to the harness.
## Prerequisites
- Python 3.10+
- Shadow of the Tomb Raider installed.
## Setup
1. Follow the setup instructions for the framework. If you have done so, all required python dependencies *should* be installed.
2. Install Shadow of the Tomb Raider from steam.
1. Location does not matter, this harness uses steam to launch the game.
## Configuration
Below is an example use of this harness as a test in a benchmark configuration.
```yaml
...
...
tests:
- name: shadowofthetombraider
executable: "shadowofthetombraider.py"
process_name: "SOTTR.exe"
asset_paths:
- 'harness/shadowofthetombraider/run'
args:
- "--preset medium"
- "--resolution 1920,1080
```
__name__ : _(required)_ name of the test. This much match the name of a directory in the harness folder so the framework
can find the executable and any supplementary files.
__executable__ : _(required)_ the entry point to the test harness. In this case a python script.
__process_name__ : _(required)_ The process name that should be the target for FPS recording (ex: PresentMon).
__asset_paths__: _(optional)_ list of files to aggregate copies of after a successful test run. If a directory path is
given, the contents are copied.
__args__ : _(optional)_ list of arguments to be appended to the command to execute. All the arguments will be passed to
the executable when invoked by the framework.
### Arguments
|flag|required|what?|notes
|--|--|--|--|
|--preset|No|Graphics preset to load for test|See the `presets` folder to determine options. If none provided, the current settings will be used|
|--resolution|No|Display settings to load for test|If none provided, current display settings will be used|
#### Presets
This harness requires a single argument for the option `preset`. This is the graphics presets that are found in the game. They are represented in YAML in the folder **presets**. To select one you take the prefix of the name of the file, and the harness will find the corresponding YAML file.
For example if I pass in the argument `--preset medium` to the harness. The harness will load the settings in `presets/medium.presets.yaml`. You can also create and supply a custom preset if you wish.
#### Resolution
Resolution is expected to be givin in the format `height,width` so for example `1920,1080`. An error will be thrown if that format is not provided.
## Common Issues
1. "Steam cannot sync with cloud"
- A steam modal between test runs (when repeated) will come up.
- If you are monitoring the test, you can simply manually close the modal and the test should continue normally.
- The best solution is to disable cloud syncing for all steam games on the test bench.
2. "Image could not be found within timeout"
- Sometimes running the harness on a new test bench + display combination will not work right away.
- Try a different template set, or add a new one to recitify this problem.
- If a new template set isn't working, something else is probably bugging out.
- This harness won't support resolutions that aren't native aspect ratios to the display.

View File

@@ -0,0 +1,215 @@
import logging
import os
import time
from enum import Enum
import cv2
import imutils
import numpy as np
import pyautogui as gui
import mss
import pydirectinput as user
DEFAULT_MATCH_THRESHOLD = 0.9
DEFAULT_INTERVAL = 2 # seconds
DEFAULT_TIMEOUT = 60 # seconds
# path relative to script
script_dir = os.path.dirname(os.path.realpath(__file__))
images_dir = os.path.join(script_dir, "images")
dir16x9 = os.path.join(images_dir, "16x9")
dir16x10 = os.path.join(images_dir, "16x10")
templates = {
"load_menu_play": {
"16x9": cv2.imread(os.path.join(images_dir, "load_menu_play.png"), cv2.IMREAD_UNCHANGED),
"16x10": cv2.imread(os.path.join(images_dir, "load_menu_play.png"), cv2.IMREAD_UNCHANGED)
},
"load_menu_play_orange": {
"16x9": cv2.imread(os.path.join(images_dir, "play_orange.png"), cv2.IMREAD_UNCHANGED),
"16x10": cv2.imread(os.path.join(images_dir, "play_orange.png"), cv2.IMREAD_UNCHANGED)
},
"menu_options": {
"16x9": cv2.imread(os.path.join(dir16x9, "options_1080.png"), cv2.IMREAD_UNCHANGED),
"16x10": cv2.imread(os.path.join(dir16x10, "options.png"), cv2.IMREAD_UNCHANGED)
},
"menu_options_save_game": {
"16x9": cv2.imread(os.path.join(dir16x9, "options_1080_savedgame.png"), cv2.IMREAD_UNCHANGED),
"16x10": cv2.imread(os.path.join(dir16x10, "options_savedgame.png"), cv2.IMREAD_UNCHANGED)
},
"menu_options_highlighted": {
"16x9": cv2.imread(os.path.join(dir16x9, "options2_1080.png"), cv2.IMREAD_UNCHANGED),
"16x10": cv2.imread(os.path.join(dir16x10, "options2.png"), cv2.IMREAD_UNCHANGED)
},
"menu_options_save_game_highlighted": {
"16x9": cv2.imread(os.path.join(dir16x9, "options2_1080_savegame.png"), cv2.IMREAD_UNCHANGED),
"16x10": cv2.imread(os.path.join(dir16x10, "options2_savegame.png"), cv2.IMREAD_UNCHANGED)
},
"menu_graphics": {
"16x9": cv2.imread(os.path.join(dir16x9, "graphics_1080.png"), cv2.IMREAD_UNCHANGED),
"16x10": cv2.imread(os.path.join(dir16x10, "graphics_button.png"), cv2.IMREAD_UNCHANGED)
},
"menu_graphics_tab": {
"16x9": cv2.imread(os.path.join(dir16x9, "graphics_tab_1080.png"), cv2.IMREAD_UNCHANGED),
"16x10": cv2.imread(os.path.join(dir16x10, "graphics_tab.png"), cv2.IMREAD_UNCHANGED)
},
"run_benchmark": {
"16x9": cv2.imread(os.path.join(dir16x9, "run_1080.png"), cv2.IMREAD_UNCHANGED),
"16x10": cv2.imread(os.path.join(dir16x10, "run.png"), cv2.IMREAD_UNCHANGED)
},
"results_header": {
"16x9": cv2.imread(os.path.join(dir16x9, "results_1080.png"), cv2.IMREAD_UNCHANGED),
"16x10": cv2.imread(os.path.join(dir16x10, "results.png"), cv2.IMREAD_UNCHANGED)
}
}
def get_template(name, set="16x9"):
return templates[name][set]
class ClickType(Enum):
SINGLE = 0 # uses .click()
DOUBLE = 1 # uses .doubleclick()
HARD = 2 # uses mouse.down() and mouse.up()
def get_middle_of_rect(top_left_corner, height, width):
x = top_left_corner[0] + (width / 2)
y = top_left_corner[1] + (height / 2)
return int(x), int(y) # round to avoid fractional pixels
def click(top_left_corner, img):
click_loc = get_middle_of_rect(top_left_corner, img.shape[0], img.shape[1])
logging.info(f"Clicking {click_loc}")
user.click(click_loc[0], click_loc[1])
def double_click(top_left_corner, img):
click_loc = get_middle_of_rect(top_left_corner, img.shape[0], img.shape[1])
logging.info(f"Double clicking {click_loc}")
user.doubleClick(click_loc[0], click_loc[1])
def hard_click(top_left_corner, img):
click_loc = get_middle_of_rect(top_left_corner, img.shape[0], img.shape[1])
user.moveTo(click_loc[0], click_loc[1])
user.mouseDown()
user.mouseUp()
def wait_and_click(template_name, name, click_type: ClickType = ClickType.SINGLE, timeout=DEFAULT_TIMEOUT):
logging.info(f"Waiting to find and click on {name}")
img, img_loc = wait_for_image_on_screen(template_name, timeout=timeout)
if click_type == ClickType.SINGLE:
click(img_loc, img)
elif click_type == ClickType.DOUBLE:
double_click(img_loc, img)
elif click_type == ClickType.HARD:
hard_click(img_loc, img)
else:
raise ValueError("Unknown click type")
class ImageNotFoundTimeout(Exception):
pass
class ImageNotFound(Exception):
pass
def gcd(a, b):
return a if b == 0 else gcd(b, a % b)
def aspect_ratio(w, h):
denom = int(gcd(w, h))
x = int(w / denom)
y = int(h / denom)
if x == 8 and y == 5:
return "16x10"
elif x == 16 and y == 9:
return "16x9"
def locate_on_screen(template_name, threshold=DEFAULT_MATCH_THRESHOLD, debug=1):
with mss.mss() as sct:
monitor_1 = sct.monitors[1] # Identify the display to capture
screen = np.array(sct.grab(monitor_1))
screen = cv2.cvtColor(screen, cv2.COLOR_RGB2BGR)
screen = cv2.cvtColor(screen, cv2.COLOR_BGR2RGB)
(h, w) = screen.shape[:2]
r = aspect_ratio(w, h)
needle = get_template(template_name, r)
return needle, locate_in_image(needle, screen, threshold=DEFAULT_MATCH_THRESHOLD, debug=0)
# This approach was largely inspired by the article
# https://pyimagesearch.com/2015/01/26/multi-scale-template-matching-using-python-opencv/
def locate_in_image(needle, haystack, threshold=DEFAULT_MATCH_THRESHOLD, debug=0):
(tH, tW) = needle.shape[:2]
if debug:
cv2.imshow("Looking For", needle)
cv2.waitKey(0)
for scale in np.linspace(0.2, 1.0, 20)[::-1]:
# resize the image according to the scale, and keep track
# of the ratio of the resizing
resized = imutils.resize(haystack, width=int(haystack.shape[1] * scale), inter=cv2.INTER_NEAREST)
r = haystack.shape[1] / float(resized.shape[1])
# if the resized image is smaller than the template, then break
# from the loop
if resized.shape[0] < tH or resized.shape[1] < tW:
break
result = cv2.matchTemplate(resized, needle, cv2.TM_CCOEFF_NORMED)
(_, maxVal, _, maxLoc) = cv2.minMaxLoc(result)
if debug:
# draw a bounding box around the detected region
# clone = np.dstack([edged, edged, edged])
cv2.rectangle(resized, (maxLoc[0], maxLoc[1]),
(maxLoc[0] + tW, maxLoc[1] + tH), (0, 0, 255), 2)
cv2.imshow("Searching", resized)
cv2.waitKey(0)
if maxVal >= threshold:
found = (maxVal, maxLoc, r)
# unpack the bookkeeping variable and compute the (x, y) coordinates
# of the bounding box based on the resized ratio
(_, maxLoc, r) = found
(startX, startY) = (int(maxLoc[0] * r), int(maxLoc[1] * r))
(endX, endY) = (int((maxLoc[0] + tW) * r), int((maxLoc[1] + tH) * r))
if debug:
# draw a bounding box around the detected result and display the image
cv2.rectangle(haystack, (startX, startY), (endX, endY), (0, 0, 255), 2)
cv2.imshow("Found", haystack)
cv2.waitKey(0)
return (startX, startY)
raise ImageNotFound("Image not found on screen")
def wait_for_image_on_screen(template_name, match_threshold=DEFAULT_MATCH_THRESHOLD, interval=DEFAULT_INTERVAL,
timeout=DEFAULT_TIMEOUT):
"""Function that will wait for an image to appear on screen. This function will check every
interval for a match that meets is greater than the match threshold. The function will raise
an error if the image is not found within the timeout given. Will return the location
of the image if found"""
t0 = time.time()
t1 = t0
while not t1 - t0 > timeout:
try:
img, loc = locate_on_screen(template_name, match_threshold)
return img, loc
except ImageNotFound:
pass
time.sleep(interval)
t1 = time.time()
raise ImageNotFoundTimeout("Could not find image on screen within timeout")

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 MiB

View File

@@ -0,0 +1,15 @@
friendly_name: "Shadow of the Tomb Raider"
executable: "shadowofthetombraider.py"
process_name: "SOTTR.exe"
# default recording delay to reduce capturing menus during setup, this should be revisited every test bench as loading times may be different
recording_delay: 60
asset_paths:
- "harness/shadowofthetombraider/run"
options:
- name: preset
type: select
# presets disabled in favor of manual process for now
# values: [current, low, medium, high, highest]
values: [current]
tooltip: Don't forget to set graphics settings!

2984
shadowofthetombraider/poetry.lock generated Normal file

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,48 @@
name: high
settings:
- name: texture_quality
reg_key: TextureQuality
value: 2
- name: texture_filtering
reg_key: TextureFiltering
value: 2
- name: shadow_quality
reg_key: ShadowQuality
value: 2
- name: ambient_occlusion
reg_key: AmbientOcclusionQuality
value: 1
- name: depth_of_field
reg_key: DOFQuality
value: 1
- name: level_of_detail
reg_key: LevelOfDetail
value: 1
- name: tesselation
reg_key: Tessellation
value: 1
- name: bloom
reg_key: Bloom
value: 1
- name: motion_blur
reg_key: MotionBlur
value: 1
- name: screen_space_reflections
reg_key: ScreenSpaceReflections
value: 1
- name: screen_space_contact_shadows
reg_key: ScreenSpaceContactShadows
value: 0
- name: pure_hair
reg_key: TressFX
value: 1
- name: volumetric_lighting
reg_key: VolumetricLighting
value: 1
- name: lens_flares
reg_key: LensFlares
value: 1
- name: screen_effects
reg_key: ScreenEffects
value: 1

View File

@@ -0,0 +1,48 @@
name: highest
settings:
- name: texture_quality
reg_key: TextureQuality
value: 3
- name: texture_filtering
reg_key: TextureFiltering
value: 3
- name: shadow_quality
reg_key: ShadowQuality
value: 3
- name: ambient_occlusion
reg_key: AmbientOcclusionQuality
value: 2
- name: depth_of_field
reg_key: DOFQuality
value: 2
- name: level_of_detail
reg_key: LevelOfDetail
value: 2
- name: tesselation
reg_key: Tessellation
value: 1
- name: bloom
reg_key: Bloom
value: 1
- name: motion_blur
reg_key: MotionBlur
value: 1
- name: screen_space_reflections
reg_key: ScreenSpaceReflections
value: 1
- name: screen_space_contact_shadows
reg_key: ScreenSpaceContactShadows
value: 1
- name: pure_hair
reg_key: TressFX
value: 1
- name: volumetric_lighting
reg_key: VolumetricLighting
value: 1
- name: lens_flares
reg_key: LensFlares
value: 1
- name: screen_effects
reg_key: ScreenEffects
value: 1

View File

@@ -0,0 +1,48 @@
name: low
settings:
- name: texture_quality
reg_key: TextureQuality
value: 0
- name: texture_filtering
reg_key: TextureFiltering
value: 0
- name: shadow_quality
reg_key: ShadowQuality
value: 1
- name: ambient_occlusion
reg_key: AmbientOcclusionQuality
value: 0
- name: depth_of_field
reg_key: DOFQuality
value: 0
- name: level_of_detail
reg_key: LevelOfDetail
value: 0
- name: tesselation
reg_key: Tessellation
value: 0
- name: bloom
reg_key: Bloom
value: 1
- name: motion_blur
reg_key: MotionBlur
value: 0
- name: screen_space_reflections
reg_key: ScreenSpaceReflections
value: 0
- name: screen_space_contact_shadows
reg_key: ScreenSpaceContactShadows
value: 0
- name: pure_hair
reg_key: TressFX
value: 0
- name: volumetric_lighting
reg_key: VolumetricLighting
value: 1
- name: lens_flares
reg_key: LensFlares
value: 1
- name: screen_effects
reg_key: ScreenEffects
value: 1

View File

@@ -0,0 +1,48 @@
name: medium
settings:
- name: texture_quality
reg_key: TextureQuality
value: 1
- name: texture_filtering
reg_key: TextureFiltering
value: 1
- name: shadow_quality
reg_key: ShadowQuality
value: 1
- name: ambient_occlusion
reg_key: AmbientOcclusionQuality
value: 1
- name: depth_of_field
reg_key: DOFQuality
value: 1
- name: level_of_detail
reg_key: LevelOfDetail
value: 1
- name: tesselation
reg_key: Tessellation
value: 0
- name: bloom
reg_key: Bloom
value: 1
- name: motion_blur
reg_key: MotionBlur
value: 1
- name: screen_space_reflections
reg_key: ScreenSpaceReflections
value: 1
- name: screen_space_contact_shadows
reg_key: ScreenSpaceContactShadows
value: 0
- name: pure_hair
reg_key: TressFX
value: 1
- name: volumetric_lighting
reg_key: VolumetricLighting
value: 1
- name: lens_flares
reg_key: LensFlares
value: 1
- name: screen_effects
reg_key: ScreenEffects
value: 1

View File

@@ -0,0 +1,21 @@
[tool.poetry]
name = "shadowofthetombraider-harness"
version = "0.1.0"
description = ""
authors = ["Nikolas Harris <nikolas@linusmediagroup.com"]
[tool.poetry.dependencies]
python = "^3.10"
PyAutoGUI = "^0.9.53"
PyDirectInput = "^1.0.4"
opencv-python = "^4.5.5"
Pillow = "^9.1.1"
psutil = "^5.9.1"
PyYAML = "^6.0"
imutils = "^0.5.4"
[tool.poetry.dev-dependencies]
[build-system]
requires = ["poetry-core>=1.0.0"]
build-backend = "poetry.core.masonry.api"

View File

@@ -0,0 +1,18 @@
import winreg
def get_reg(name) -> any:
reg_path = r'SOFTWARE\Eidos Montreal\Shadow of the Tomb Raider\Graphics'
try:
registry_key = winreg.OpenKey(winreg.HKEY_CURRENT_USER, reg_path, 0,
winreg.KEY_READ)
value, _ = winreg.QueryValueEx(registry_key, name)
winreg.CloseKey(registry_key)
return value
except WindowsError:
return None
def get_resolution() -> tuple[int]:
width = get_reg("FullscreenWidth")
height = get_reg("FullscreenHeight")
return (height, width)

View File

@@ -0,0 +1,117 @@
from subprocess import Popen
import sys
from cv2_utils import *
from shadow_of_the_tomb_raider_utils import get_resolution
sys.path.insert(1, os.path.join(sys.path[0], '..'))
from harness_utils.logging import *
from harness_utils.process import terminate_processes
from harness_utils.steam import get_run_game_id_command, DEFAULT_EXECUTABLE_PATH as steam_path
STEAM_GAME_ID = 750920
SCRIPT_DIRECTORY = os.path.dirname(os.path.realpath(__file__))
LOG_DIRECTORY = os.path.join(SCRIPT_DIRECTORY, "run")
def start_game():
steam_run_arg = get_run_game_id_command(STEAM_GAME_ID)
logging.info(steam_path + " " + steam_run_arg)
return Popen([steam_path, steam_run_arg])
def clickOptions(options):
"""
If the game is freshly installed the main menu has less options thus the options button
looks different. We also account for if the options button is highlighted if the mouse
is hovering over it.
"""
if options is None or len(options) == 0:
return
try:
wait_and_click(options[0], "graphics options", ClickType.HARD)
except:
clickOptions(options[1:])
def run_benchmark():
"""
Start game via Steam and enter fullscreen mode
"""
t1 = time.time()
game_process = start_game()
try:
wait_and_click('load_menu_play', "play button", timeout=30)
except:
wait_and_click('load_menu_play_orange', "play button", timeout=30)
"""
Wait for game to load and enter graphics submenu
"""
optionImages = [
'menu_options_save_game',
'menu_options_save_game_highlighted',
'menu_options',
'menu_options_highlighted',
]
clickOptions(optionImages)
wait_and_click('menu_graphics', "graphics options", ClickType.HARD)
time.sleep(2) # let the menu transition
screen = gui.screenshot(os.path.join(LOG_DIRECTORY, "display_settings.png"))
wait_and_click('menu_graphics_tab', "graphics tab", ClickType.HARD)
screen = gui.screenshot(os.path.join(LOG_DIRECTORY, "graphics_settings.png"))
"""
Start the benchmark!
"""
t2 = time.time()
logging.info(f"Harness setup took {round((t2 - t1), 2)} seconds")
user.press("r")
start_time = time.time()
"""
Wait for benchmark to complete
"""
time.sleep(180)
wait_for_image_on_screen('results_header', DEFAULT_MATCH_THRESHOLD, interval=2,
timeout=60)
end_time = time.time()
logging.info(f"Benchark took {round((end_time - start_time), 2)} seconds")
screen = gui.screenshot(os.path.join(LOG_DIRECTORY, "results.png"))
# Exit
terminate_processes("SOTTR")
return start_time, end_time
setup_log_directory(LOG_DIRECTORY)
logging.basicConfig(filename=f'{LOG_DIRECTORY}/harness.log',
format=DEFAULT_LOGGING_FORMAT,
datefmt=DEFAULT_DATE_FORMAT,
level=logging.DEBUG)
console = logging.StreamHandler()
formatter = logging.Formatter(DEFAULT_LOGGING_FORMAT)
console.setFormatter(formatter)
logging.getLogger('').addHandler(console)
try:
start_time, end_time = run_benchmark()
height, width = get_resolution()
result = {
"resolution": f"{width}x{height}",
"graphics_preset": "current",
"start_time": seconds_to_milliseconds(start_time),
"end_time": seconds_to_milliseconds(end_time)
}
write_report_json(LOG_DIRECTORY, "report.json", result)
except Exception as e:
logging.error("Something went wrong running the benchmark!")
logging.exception(e)
terminate_processes("SOTTR")
exit(1)

View File

@@ -0,0 +1,23 @@
import cv2
import os
from cv2_utils import *
script_dir = os.path.dirname(os.path.realpath(__file__))
images_dir = os.path.join(script_dir, "images")
test_images_dir = os.path.join(images_dir, "tests")
test_menus = {
"mainmenu_2k": cv2.imread(os.path.join(test_images_dir, "mainmenu_2k.png"), cv2.IMREAD_UNCHANGED),
"graphicsmenu_2k": cv2.imread(os.path.join(test_images_dir, "graphicsmenu_2k.png"), cv2.IMREAD_UNCHANGED),
"mainmenu_4k": cv2.imread(os.path.join(test_images_dir, "mainmenu_4k.png"), cv2.IMREAD_UNCHANGED),
"mainmenu_1": cv2.imread(os.path.join(test_images_dir, "menu1.png"), cv2.IMREAD_UNCHANGED),
"mainmenu_2": cv2.imread(os.path.join(test_images_dir, "menu2.png"), cv2.IMREAD_UNCHANGED),
"mainmenu_3": cv2.imread(os.path.join(test_images_dir, "menu3.png"), cv2.IMREAD_UNCHANGED),
"mainmenu_4": cv2.imread(os.path.join(test_images_dir, "menu4.png"), cv2.IMREAD_UNCHANGED),
"16:10MainMenu": cv2.imread(os.path.join(test_images_dir, "main_menu_1920x1200.png"), cv2.IMREAD_UNCHANGED)
}
found2 = locate_in_image(get_template('menu_options'), test_menus['mainmenu_4k'], threshold=0.8, debug=0)
print(found2)