Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add reset image and clips recording options #15

Open
wants to merge 20 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added resources/record_active.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added resources/record_idle.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
8 changes: 7 additions & 1 deletion src/pilgrim_autosplitter.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
import os
import platform
import sys
import time


class PilgrimAutosplitter:
Expand Down Expand Up @@ -93,7 +94,7 @@ def __init__(self) -> None:

self.splitter = Splitter()
if settings.get_bool("START_WITH_VIDEO"):
self.splitter.start()
self.splitter.restart()

self.ui_controller = UIController(self.app, self.splitter)

Expand All @@ -115,6 +116,11 @@ def main():
pilgrim_autosplitter.app.aboutToQuit.connect(
pilgrim_autosplitter.splitter.safe_exit_all_threads
)
# Wait for any singleshot QTimers started by widgets to finish.
# Right now, this includes only the double click timer in some
# ui_main_window widgets. If we quit while a timer is running, it
# can cause a segfault, so we want to prevent that.
pilgrim_autosplitter.app.aboutToQuit.connect(lambda sec=0.2: time.sleep(sec))

print("Starting...")
pilgrim_autosplitter.app.exec()
Expand Down
22 changes: 19 additions & 3 deletions src/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@
COMPARISON_FRAME_HEIGHT = 240

# Pilgrim Autosplitter's current version number
VERSION_NUMBER = "v1.0.7"
VERSION_NUMBER = "v1.1.0"

# The URL of Pilgrim Autosplitter's GitHub repo
REPO_URL = "https://github.com/pilgrimtabby/pilgrim-autosplitter/"
Expand Down Expand Up @@ -141,7 +141,7 @@ def set_program_vals(settings: QSettings = settings) -> None:

Populates settings with default values if they have not yet been set.

Ensures that LAST_IMAGE_DIR points to an existing path.
Ensures that LAST_IMAGE_DIR and LAST_RECORD_DIR point to existing paths.

Ensures that the program starts in full view if START_WITH_VIDEO is false,
since the user would have to exit minimal view to turn the video on anyway.
Expand All @@ -151,12 +151,13 @@ def set_program_vals(settings: QSettings = settings) -> None:
home_dir = get_home_dir()

# Unset hotkeys if upgrading from <=v1.0.6 because of hotkey implementation
# updates
# updates. Set a default reset wait for the same reason.
last_version = get_str("LAST_VERSION", settings)
if last_version == "None":
last_version = "v1.0.0"
if not version_ge(last_version, "v1.0.7"):
unset_hotkey_bindings()
set_value("DEFAULT_RESET_WAIT", 0.0, settings)

if not get_bool("SETTINGS_SET", settings):
# Indicate that default settings have been populated
Expand All @@ -165,6 +166,9 @@ def set_program_vals(settings: QSettings = settings) -> None:
# Set hotkeys to default values
unset_hotkey_bindings()

# Turn off recording splits as clips by default
set_value("RECORD_CLIPS", False, settings)

# The default minimum match percent needed to force a split action
set_value("DEFAULT_THRESHOLD", 0.90, settings)

Expand All @@ -174,12 +178,18 @@ def set_program_vals(settings: QSettings = settings) -> None:
# The default pause (seconds) after a split
set_value("DEFAULT_PAUSE", 1.0, settings)

# The default wait time before looking for reset image
set_value("DEFAULT_RESET_WAIT", 0.0, settings)

# The FPS used by splitter and ui_controller
set_value("FPS", 30, settings)

# The location of split images
set_value("LAST_IMAGE_DIR", home_dir, settings)

# The location of recordings
set_value("LAST_RECORD_DIR", home_dir, settings)

# Determine whether screenshots should be opened using the machine's
# default image viewer after capture
set_value("OPEN_SCREENSHOT_ON_CAPTURE", False, settings)
Expand Down Expand Up @@ -221,6 +231,12 @@ def set_program_vals(settings: QSettings = settings) -> None:
if not last_image_dir.startswith(home_dir) or not Path(last_image_dir).is_dir():
set_value("LAST_IMAGE_DIR", home_dir, settings)

# Make sure recordings dir exists and is within the user's home dir
# (This limits i/o to user-controlled areas)
last_record_dir = get_str("LAST_RECORD_DIR", settings)
if not last_record_dir.startswith(home_dir) or not Path(last_record_dir).is_dir():
set_value("LAST_RECORD_DIR", home_dir, settings)

# Always start in full view if video doesn't come on automatically
if not get_bool("START_WITH_VIDEO", settings):
set_value("SHOW_MIN_VIEW", False, settings)
Expand Down
145 changes: 117 additions & 28 deletions src/splitter/split_dir.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@
import re
from multiprocessing import freeze_support
from multiprocessing.dummy import Pool as ThreadPool
from typing import List, Tuple
from typing import List, Optional, Tuple

import cv2
import numpy
Expand Down Expand Up @@ -69,8 +69,8 @@ class SplitDir:
"""

def __init__(self):
"""Initialize a list of SplitImage objects and set flags accordingly."""
self.list = self._get_split_images()
"""Get split images and reset image and set flags accordingly."""
self.list, self.reset_image = self._get_split_images()
if len(self.list) > 0:
self.current_image_index = 0
self.current_loop = 1
Expand Down Expand Up @@ -107,8 +107,9 @@ def previous_split_image(self) -> None:
self.current_loop -= 1

def reset_split_images(self) -> None:
"""Rebuild the split image list and reset flags."""
new_list = self._get_split_images()
"""Rebuild split image list, refresh reset image, and reset flags."""
new_list, new_reset_image = self._get_split_images()
self.reset_image = new_reset_image
if len(new_list) == 0:
self.list = []
self.current_image_index = None
Expand Down Expand Up @@ -144,35 +145,40 @@ def set_default_pause(self) -> None:
image.pause_duration = default_pause

def resize_images(self) -> None:
"""Regenerate each split image's pixmap.
"""Regenerate the reset image's and each split image's pixmap.

Useful when changing aspect ratios, since the size of the pixmap can
change.
"""
for image in self.list:
image.pixmap = image.get_pixmap()

if self.reset_image is not None:
self.reset_image.pixmap = self.reset_image.get_pixmap()

###############
# #
# Private API #
# #
###############

def _get_split_images(self) -> List["_SplitImage"]:
"""Get a list of SplitImage objects from a directory.
def _get_split_images(self) -> Tuple[List["_SplitImage"], Optional["_SplitImage"]]:
"""Get a list of SplitImage objects from a directory, including a reset
image if present.

Currently supported image types include .png, .jpg, and .jpeg. Only
.png is tested, and it is the only recommended image format.
Only image type currently supported is .png. Other types could easily
be supported, it's just a matter of doing it.

Use multiprocessing.dummy.Pool to construct the split images list. This
cuts the time spent making the list by a factor of ten, which matters a
lot when there are lots of images.

Returns:
List[_SplitImage]: The list of SplitImage objects.
_SplitImage | None: The reset image, if present.
"""

def get_split_image(index: int, path: str):
def get_split_image(index: int, path: str) -> None:
"""Get a single SplitImage object and put it in split_images.

Test if the image at the index in self.list is the same image by
Expand Down Expand Up @@ -208,17 +214,23 @@ def get_split_image(index: int, path: str):

dir_path = settings.get_str("LAST_IMAGE_DIR")
if not pathlib.Path(dir_path).is_dir():
return [] # The directory doesn't exist; return an empty list
return [], None # The directory doesn't exist; return an empty list

image_paths = sorted(glob.glob(f"{dir_path}/*.png"))

image_paths = sorted(
glob.glob(f"{dir_path}/*.png")
+ glob.glob(f"{dir_path}/*.jpg")
+ glob.glob(f"{dir_path}/*.jpeg")
)
# Get the reset image if it exists, remove it from the main list
reset_image_path = reset_image = None
for path in image_paths:
if "{r}" in path:
reset_image_path = path
break
if reset_image_path is not None:
image_paths.remove(reset_image_path)
reset_image = self._SplitImage(reset_image_path)

list_length = len(image_paths)
if list_length == 0:
return [] # The list is empty; return an empty list
return [], reset_image # The list is empty; return an empty list
else:
# Initialize split_images with the same number of indexes as images
split_images = [None] * (list_length)
Expand All @@ -232,7 +244,7 @@ def get_split_image(index: int, path: str):
pool.starmap(func=get_split_image, iterable=indexes_and_paths)
pool.close()
pool.join()
return split_images
return split_images, reset_image

class _SplitImage:
"""Store and modify details attributes of a single split image.
Expand Down Expand Up @@ -281,14 +293,16 @@ def __init__(self, image_path: str) -> None:
self._raw_image = self._get_raw_image()
self.last_modified = os.path.getmtime(self._path)
self.name = pathlib.Path(image_path).stem
self.stripped_name = self._get_stripped_name()
self.image, self.mask = self.get_image_and_mask()
self.max_dist = self._get_max_dist()
self.pixmap = self.get_pixmap()
self.below_flag, self.dummy_flag, self.pause_flag = (
self.below_flag, self.dummy_flag, self.pause_flag, self.reset_flag = (
self._get_flags_from_name()
)
self.delay_duration, self.delay_is_default = self._get_delay_from_name()
self.pause_duration, self.pause_is_default = self._get_pause_from_name()
self.reset_wait_duration = self._get_reset_wait_from_name()
self.threshold, self.threshold_is_default = self._get_threshold_from_name()
self.loops, self.loops_is_default = self._get_loops_from_name()

Expand Down Expand Up @@ -395,6 +409,33 @@ def _get_raw_image(self) -> numpy.ndarray:
"""
return cv2.imread(self._path, cv2.IMREAD_UNCHANGED)

def _get_stripped_name(self) -> None:
"""Get the text name of the split minus all flags and settings.

Returns:
str: The name without any flags or settings.
"""
# Find all flags and settings in the name
flags = re.findall(r"\{.*?\}", self.name)
delays = re.findall(r"\#.*?\#", self.name)
pauses = re.findall(r"\[.*?\]", self.name)
thresholds = re.findall(r"\(.*?\)", self.name)
loops = re.findall(r"\@.*?\@", self.name)

non_name_text = [flags, delays, pauses, thresholds, loops]
stripped_name = self.name

# Remove the extra text
for category in non_name_text:
for entry in category:
stripped_name = stripped_name.replace(entry, "")

# Remove any trailing leftover underscores
while stripped_name.endswith("_"):
stripped_name = stripped_name[:-1]

return stripped_name

def _get_max_dist(self) -> float:
"""Calculate the maximum possible Euclidean distance from an image.

Expand Down Expand Up @@ -468,9 +509,9 @@ def _has_alpha_channel(self, image: numpy.ndarray) -> bool:
except IndexError:
return False

def _get_flags_from_name(self) -> Tuple[bool, bool, bool]:
"""Get split image's below, dummy, and pause flags by reading the
filename.
def _get_flags_from_name(self) -> Tuple[bool, bool, bool, bool]:
"""Get split image's below, dummy, pause, and reset flags by
reading the filename.

A below flag is indicated with a b between brackets, like this:
_{b}_ (the splitter will not consider a match found until the
Expand All @@ -486,17 +527,29 @@ def _get_flags_from_name(self) -> Tuple[bool, bool, bool]:
next split, but to press the pause hotkey instead of the split
hotkey)

A reset flag is indicated with an r between brackets, like this:
_{r}_ (the splitter will tell the ui_controller to press the reset
hotkey and go back to the first split)

A split cannot be a dummy split and a pause split, because a dummy
split implies no hotkey press. If both flags are set, the pause
flag is removed.
flag is removed. In addition, the only flag that can be used in
conjunction with the reset flag is the below flag.

Returns:
Tuple[bool, bool, bool]: below_flag, dummy_flag, and
pause_flag, respectively (True if set, False if not).
Tuple[bool, bool, bool, bool]: below_flag, dummy_flag, pause_
flag, and reset_flag, respectively (True if set, False if not).
"""
flags = re.findall(r"_\{([bdp]+?)\}", self.name)
flags = re.findall(r"_\{([bdpr]+?)\}", self.name)

# Remove flags that can't go together
if "d" in flags and "p" in flags:
flags.remove("p")
if "r" in flags:
if "d" in flags:
flags.remove("d")
if "p" in flags:
flags.remove("p")

if "b" in flags:
below_flag = True
Expand All @@ -510,8 +563,12 @@ def _get_flags_from_name(self) -> Tuple[bool, bool, bool]:
pause_flag = True
else:
pause_flag = False
if "r" in flags:
reset_flag = True
else:
reset_flag = False

return below_flag, dummy_flag, pause_flag
return below_flag, dummy_flag, pause_flag, reset_flag

def _get_delay_from_name(self) -> float:
"""Set split image's delay duration before split by reading
Expand Down Expand Up @@ -561,6 +618,38 @@ def _get_pause_from_name(self) -> float:
pause = float(pause[1])
return max(min(pause, MAX_LOOPS_AND_WAIT), 1), False

def _get_reset_wait_from_name(self) -> Optional[float]:
"""Set reset image's wait time, the time the splitter waits before
beginning to look for the rest image. If this is 0, the splitter
starts looking for the reset image right after the first split.

Reset wait duration is set in the filename by placing a float in
between percent signs, like this: %20% (the splitter won't look for
the reset image for 20 seconds after it normally would).

Using is_digit guarantees that this method will not return negative
numbers, which is what we want.

There is no minimum reset wait time, but it cannot be greater than
MAX_LOOPS_AND_WAIT.

Returns:
float: The reset wait time indicated in the filename. Default
is 0. Returns None if the image isn't a split image.
"""
if not self.reset_flag:
return None

reset_wait = re.search(r"_\%(.+?)\%", self.name)
if (
reset_wait is None
or not str(reset_wait[1]).replace(".", "", 1).isdigit()
):
return settings.get_float("DEFAULT_RESET_WAIT")

reset_wait = float(reset_wait[1])
return max(min(reset_wait, MAX_LOOPS_AND_WAIT), 0)

def _get_threshold_from_name(self) -> float:
"""Set split image's threshold match percent by reading filename
flags.
Expand Down
Loading