Refactoring

- Change saved previews to PNG
  - Split lib.gui.utils
  - Disable flake8 on __init__ imports
This commit is contained in:
torzdf 2022-10-15 19:17:24 +01:00
parent 47867a0dd4
commit f32f460e3f
9 changed files with 1548 additions and 1503 deletions

View File

@ -1,12 +1,12 @@
#!/usr/bin python3
""" The Faceswap GUI """
from lib.gui.command import CommandNotebook # noqa
from lib.gui.custom_widgets import ConsoleOut, StatusBar # noqa
from lib.gui.display import DisplayNotebook # noqa
from lib.gui.options import CliOptions # noqa
from lib.gui.menu import MainMenuBar, TaskBar # noqa
from lib.gui.project import LastSession # noqa
from lib.gui.utils import (get_config, get_images, initialize_config, initialize_images, # noqa
from lib.gui.command import CommandNotebook
from lib.gui.custom_widgets import ConsoleOut, StatusBar
from lib.gui.display import DisplayNotebook
from lib.gui.options import CliOptions
from lib.gui.menu import MainMenuBar, TaskBar
from lib.gui.project import LastSession
from lib.gui.utils import (get_config, get_images, initialize_config, initialize_images,
preview_trigger)
from lib.gui.wrapper import ProcessWrapper # noqa
from lib.gui.wrapper import ProcessWrapper

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,7 @@
#!/usr/bin python3
""" Utilities for the Faceswap GUI """
from .config import get_config, initialize_config, PATHCACHE
from .file_handler import FileHandler
from .image import get_images, initialize_images, preview_trigger
from .misc import LongRunningTask

420
lib/gui/utils/config.py Normal file
View File

@ -0,0 +1,420 @@
#!/usr/bin python3
""" Global configuration optiopns for the Faceswap GUI """
import logging
import os
import sys
import tkinter as tk
from dataclasses import dataclass, field
from typing import Any, cast, Dict, Optional, Tuple, TYPE_CHECKING, Union
from lib.gui._config import Config as UserConfig
from lib.gui.project import Project, Tasks
from lib.gui.theme import Style
from .file_handler import FileHandler
if TYPE_CHECKING:
from lib.gui.options import CliOptions
from lib.gui.custom_widgets import StatusBar
from lib.gui.command import CommandNotebook
from lib.gui.command import ToolsNotebook
logger = logging.getLogger(__name__) # pylint: disable=invalid-name
PATHCACHE = os.path.join(os.path.realpath(os.path.dirname(sys.argv[0])), "lib", "gui", ".cache")
_CONFIG: Optional["Config"] = None
def initialize_config(root: tk.Tk,
cli_opts: "CliOptions",
statusbar: "StatusBar") -> Optional["Config"]:
""" Initialize the GUI Master :class:`Config` and add to global constant.
This should only be called once on first GUI startup. Future access to :class:`Config`
should only be executed through :func:`get_config`.
Parameters
----------
root: :class:`tkinter.Tk`
The root Tkinter object
cli_opts: :class:`lib.gui.options.CliOptions`
The command line options object
statusbar: :class:`lib.gui.custom_widgets.StatusBar`
The GUI Status bar
Returns
-------
:class:`Config` or ``None``
``None`` if the config has already been initialized otherwise the global configuration
options
"""
global _CONFIG # pylint: disable=global-statement
if _CONFIG is not None:
return None
logger.debug("Initializing config: (root: %s, cli_opts: %s, "
"statusbar: %s)", root, cli_opts, statusbar)
_CONFIG = Config(root, cli_opts, statusbar)
return _CONFIG
def get_config() -> "Config":
""" Get the Master GUI configuration.
Returns
-------
:class:`Config`
The Master GUI Config
"""
assert _CONFIG is not None
return _CONFIG
@dataclass
class _GuiObjects:
""" Data class for commonly accessed GUI Objects """
cli_opts: "CliOptions"
tk_vars: Dict[str, Union[tk.BooleanVar, tk.StringVar]]
project: Project
tasks: Tasks
status_bar: "StatusBar"
default_options: Dict[str, Dict[str, Any]] = field(default_factory=dict)
command_notebook: Optional["CommandNotebook"] = None
class Config():
""" The centralized configuration class for holding items that should be made available to all
parts of the GUI.
This class should be initialized on GUI startup through :func:`initialize_config`. Any further
access to this class should be through :func:`get_config`.
Parameters
----------
root: :class:`tkinter.Tk`
The root Tkinter object
cli_opts: :class:`lib.gui.options.CliOpts`
The command line options object
statusbar: :class:`lib.gui.custom_widgets.StatusBar`
The GUI Status bar
"""
def __init__(self, root: tk.Tk, cli_opts: "CliOptions", statusbar: "StatusBar") -> None:
logger.debug("Initializing %s: (root %s, cli_opts: %s, statusbar: %s)",
self.__class__.__name__, root, cli_opts, statusbar)
self._default_font = cast(dict, tk.font.nametofont("TkDefaultFont").configure())["family"]
self._constants = dict(
root=root,
scaling_factor=self._get_scaling(root),
default_font=self._default_font)
self._gui_objects = _GuiObjects(
cli_opts=cli_opts,
tk_vars=self._set_tk_vars(),
project=Project(self, FileHandler),
tasks=Tasks(self, FileHandler),
status_bar=statusbar)
self._user_config = UserConfig(None)
self._style = Style(self.default_font, root, PATHCACHE)
self._user_theme = self._style.user_theme
logger.debug("Initialized %s", self.__class__.__name__)
# Constants
@property
def root(self) -> tk.Tk:
""" :class:`tkinter.Tk`: The root tkinter window. """
return self._constants["root"]
@property
def scaling_factor(self) -> float:
""" float: The scaling factor for current display. """
return self._constants["scaling_factor"]
@property
def pathcache(self) -> str:
""" str: The path to the GUI cache folder """
return PATHCACHE
# GUI Objects
@property
def cli_opts(self) -> "CliOptions":
""" :class:`lib.gui.options.CliOptions`: The command line options for this GUI Session. """
return self._gui_objects.cli_opts
@property
def tk_vars(self) -> Dict[str, Union[tk.StringVar, tk.BooleanVar]]:
""" dict: The global tkinter variables. """
return self._gui_objects.tk_vars
@property
def project(self) -> Project:
""" :class:`lib.gui.project.Project`: The project session handler. """
return self._gui_objects.project
@property
def tasks(self) -> Tasks:
""" :class:`lib.gui.project.Tasks`: The session tasks handler. """
return self._gui_objects.tasks
@property
def default_options(self) -> Dict[str, Dict[str, Any]]:
""" dict: The default options for all tabs """
return self._gui_objects.default_options
@property
def statusbar(self) -> "StatusBar":
""" :class:`lib.gui.custom_widgets.StatusBar`: The GUI StatusBar
:class:`tkinter.ttk.Frame`. """
return self._gui_objects.status_bar
@property
def command_notebook(self) -> Optional["CommandNotebook"]:
""" :class:`lib.gui.command.CommandNotebook`: The main Faceswap Command Notebook. """
return self._gui_objects.command_notebook
# Convenience GUI Objects
@property
def tools_notebook(self) -> "ToolsNotebook":
""" :class:`lib.gui.command.ToolsNotebook`: The Faceswap Tools sub-Notebook. """
assert self.command_notebook is not None
return self.command_notebook.tools_notebook
@property
def modified_vars(self) -> Dict[str, "tk.BooleanVar"]:
""" dict: The command notebook modified tkinter variables. """
assert self.command_notebook is not None
return self.command_notebook.modified_vars
@property
def _command_tabs(self) -> Dict[str, int]:
""" dict: Command tab titles with their IDs. """
assert self.command_notebook is not None
return self.command_notebook.tab_names
@property
def _tools_tabs(self) -> Dict[str, int]:
""" dict: Tools command tab titles with their IDs. """
assert self.command_notebook is not None
return self.command_notebook.tools_tab_names
# Config
@property
def user_config(self) -> UserConfig:
""" dict: The GUI config in dict form. """
return self._user_config
@property
def user_config_dict(self) -> Dict[str, Any]: # TODO Dataclass
""" dict: The GUI config in dict form. """
return self._user_config.config_dict
@property
def user_theme(self) -> Dict[str, Any]: # TODO Dataclass
""" dict: The GUI theme selection options. """
return self._user_theme
@property
def default_font(self) -> Tuple[str, int]:
""" tuple: The selected font as configured in user settings. First item is the font (`str`)
second item the font size (`int`). """
font = self.user_config_dict["font"]
font = self._default_font if font == "default" else font
return (font, self.user_config_dict["font_size"])
@staticmethod
def _get_scaling(root) -> float:
""" Get the display DPI.
Returns
-------
float:
The scaling factor
"""
dpi = root.winfo_fpixels("1i")
scaling = dpi / 72.0
logger.debug("dpi: %s, scaling: %s'", dpi, scaling)
return scaling
def set_default_options(self) -> None:
""" Set the default options for :mod:`lib.gui.projects`
The Default GUI options are stored on Faceswap startup.
Exposed as the :attr:`_default_opts` for a project cannot be set until after the main
Command Tabs have been loaded.
"""
default = self.cli_opts.get_option_values()
logger.debug(default)
self._gui_objects.default_options = default
self.project.set_default_options()
def set_command_notebook(self, notebook: "CommandNotebook") -> None:
""" Set the command notebook to the :attr:`command_notebook` attribute
and enable the modified callback for :attr:`project`.
Parameters
----------
notebook: :class:`lib.gui.command.CommandNotebook`
The main command notebook for the Faceswap GUI
"""
logger.debug("Setting commane notebook: %s", notebook)
self._gui_objects.command_notebook = notebook
self.project.set_modified_callback()
def set_active_tab_by_name(self, name: str) -> None:
""" Sets the :attr:`command_notebook` or :attr:`tools_notebook` to active based on given
name.
Parameters
----------
name: str
The name of the tab to set active
"""
assert self.command_notebook is not None
name = name.lower()
if name in self._command_tabs:
tab_id = self._command_tabs[name]
logger.debug("Setting active tab to: (name: %s, id: %s)", name, tab_id)
self.command_notebook.select(tab_id)
elif name in self._tools_tabs:
self.command_notebook.select(self._command_tabs["tools"])
tab_id = self._tools_tabs[name]
logger.debug("Setting active Tools tab to: (name: %s, id: %s)", name, tab_id)
self.tools_notebook.select()
else:
logger.debug("Name couldn't be found. Setting to id 0: %s", name)
self.command_notebook.select(0)
def set_modified_true(self, command: str) -> None:
""" Set the modified variable to ``True`` for the given command in :attr:`modified_vars`.
Parameters
----------
command: str
The command to set the modified state to ``True``
"""
tkvar = self.modified_vars.get(command, None)
if tkvar is None:
logger.debug("No tkvar for command: '%s'", command)
return
tkvar.set(True)
logger.debug("Set modified var to True for: '%s'", command)
def refresh_config(self) -> None:
""" Reload the user config from file. """
self._user_config = UserConfig(None)
def set_cursor_busy(self, widget: Optional[tk.Widget] = None) -> None:
""" Set the root or widget cursor to busy.
Parameters
----------
widget: tkinter object, optional
The widget to set busy cursor for. If the provided value is ``None`` then sets the
cursor busy for the whole of the GUI. Default: ``None``.
"""
logger.debug("Setting cursor to busy. widget: %s", widget)
component = self.root if widget is None else widget
component.config(cursor="watch") # type: ignore
component.update_idletasks()
def set_cursor_default(self, widget: Optional[tk.Widget] = None) -> None:
""" Set the root or widget cursor to default.
Parameters
----------
widget: tkinter object, optional
The widget to set default cursor for. If the provided value is ``None`` then sets the
cursor busy for the whole of the GUI. Default: ``None``
"""
logger.debug("Setting cursor to default. widget: %s", widget)
component = self.root if widget is None else widget
component.config(cursor="") # type: ignore
component.update_idletasks()
@staticmethod
def _set_tk_vars() -> Dict[str, Union[tk.StringVar, tk.BooleanVar]]:
""" Set the global tkinter variables stored for easy access in :class:`Config`.
The variables are available through :attr:`tk_vars`.
"""
display = tk.StringVar()
display.set("")
runningtask = tk.BooleanVar()
runningtask.set(False)
istraining = tk.BooleanVar()
istraining.set(False)
actioncommand = tk.StringVar()
actioncommand.set("")
generatecommand = tk.StringVar()
generatecommand.set("")
console_clear = tk.BooleanVar()
console_clear.set(False)
refreshgraph = tk.BooleanVar()
refreshgraph.set(False)
updatepreview = tk.BooleanVar()
updatepreview.set(False)
analysis_folder = tk.StringVar()
analysis_folder.set("")
tk_vars: Dict[str, Union[tk.StringVar, tk.BooleanVar]] = dict(
display=display,
runningtask=runningtask,
istraining=istraining,
action=actioncommand,
generate=generatecommand,
console_clear=console_clear,
refreshgraph=refreshgraph,
updatepreview=updatepreview,
analysis_folder=analysis_folder)
logger.debug(tk_vars)
return tk_vars
def set_root_title(self, text: Optional[str] = None) -> None:
""" Set the main title text for Faceswap.
The title will always begin with 'Faceswap.py'. Additional text can be appended.
Parameters
----------
text: str, optional
Additional text to be appended to the GUI title bar. Default: ``None``
"""
title = "Faceswap.py"
title += f" - {text}" if text is not None and text else ""
self.root.title(title)
def set_geometry(self, width: int, height: int, fullscreen: bool = False) -> None:
""" Set the geometry for the root tkinter object.
Parameters
----------
width: int
The width to set the window to (prior to scaling)
height: int
The height to set the window to (prior to scaling)
fullscreen: bool, optional
Whether to set the window to full-screen mode. If ``True`` then :attr:`width` and
:attr:`height` are ignored. Default: ``False``
"""
self.root.tk.call("tk", "scaling", self.scaling_factor)
if fullscreen:
initial_dimensions = (self.root.winfo_screenwidth(), self.root.winfo_screenheight())
else:
initial_dimensions = (round(width * self.scaling_factor),
round(height * self.scaling_factor))
if fullscreen and sys.platform in ("win32", "darwin"):
self.root.state('zoomed')
elif fullscreen:
self.root.attributes('-zoomed', True)
else:
self.root.geometry(f"{str(initial_dimensions[0])}x{str(initial_dimensions[1])}+80+80")
logger.debug("Geometry: %sx%s", *initial_dimensions)

View File

@ -0,0 +1,342 @@
#!/usr/bin/env python3
""" File browser utility functions for the Faceswap GUI. """
import logging
import platform
import sys
import tkinter as tk
from tkinter import filedialog
from typing import cast, Dict, IO, List, Optional, Tuple, Union
if sys.version_info < (3, 8):
from typing_extensions import Literal
else:
from typing import Literal
logger = logging.getLogger(__name__) # pylint: disable=invalid-name
_FILETYPE = Literal["default", "alignments", "config_project", "config_task",
"config_all", "csv", "image", "ini", "state", "log", "video"]
_HANDLETYPE = Literal["open", "save", "filename", "filename_multi", "save_filename",
"context", "dir"]
class FileHandler(): # pylint:disable=too-few-public-methods
""" Handles all GUI File Dialog actions and tasks.
Parameters
----------
handle_type: ['open', 'save', 'filename', 'filename_multi', 'save_filename', 'context', 'dir']
The type of file dialog to return. `open` and `save` will perform the open and save actions
and return the file. `filename` returns the filename from an `open` dialog.
`filename_multi` allows for multi-selection of files and returns a list of files selected.
`save_filename` returns the filename from a `save as` dialog. `context` is a context
sensitive parameter that returns a certain dialog based on the current options. `dir` asks
for a folder location.
file_type: ['default', 'alignments', 'config_project', 'config_task', 'config_all', 'csv', \
'image', 'ini', 'state', 'log', 'video']
The type of file that this dialog is for. `default` allows selection of any files. Other
options limit the file type selection
title: str, optional
The title to display on the file dialog. If `None` then the default title will be used.
Default: ``None``
initial_folder: str, optional
The folder to initially open with the file dialog. If `None` then tkinter will decide.
Default: ``None``
initial_file: str, optional
The filename to set with the file dialog. If `None` then tkinter no initial filename is.
specified. Default: ``None``
command: str, optional
Required for context handling file dialog, otherwise unused. Default: ``None``
action: str, optional
Required for context handling file dialog, otherwise unused. Default: ``None``
variable: str, optional
Required for context handling file dialog, otherwise unused. The variable to associate
with this file dialog. Default: ``None``
Attributes
----------
return_file: str or object
The return value from the file dialog
Example
-------
>>> handler = FileHandler('filename', 'video', title='Select a video...')
>>> video_file = handler.return_file
>>> print(video_file)
'/path/to/selected/video.mp4'
"""
def __init__(self,
handle_type: _HANDLETYPE,
file_type: _FILETYPE,
title: Optional[str] = None,
initial_folder: Optional[str] = None,
initial_file: Optional[str] = None,
command: Optional[str] = None,
action: Optional[str] = None,
variable: Optional[str] = None) -> None:
logger.debug("Initializing %s: (handle_type: '%s', file_type: '%s', title: '%s', "
"initial_folder: '%s', initial_file: '%s', command: '%s', action: '%s', "
"variable: %s)", self.__class__.__name__, handle_type, file_type, title,
initial_folder, initial_file, command, action, variable)
self._handletype = handle_type
self._dummy_master = self._set_dummy_master()
self._defaults = self._set_defaults()
self._kwargs = self._set_kwargs(title,
initial_folder,
initial_file,
file_type,
command,
action,
variable)
self.return_file = getattr(self, f"_{self._handletype.lower()}")()
self._remove_dummy_master()
logger.debug("Initialized %s", self.__class__.__name__)
@property
def _filetypes(self) -> Dict[str, List[Tuple[str, str]]]:
""" dict: The accepted extensions for each file type for opening/saving """
all_files = ("All files", "*.*")
filetypes = dict(
default=[all_files],
alignments=[("Faceswap Alignments", "*.fsa"), all_files],
config_project=[("Faceswap Project files", "*.fsw"), all_files],
config_task=[("Faceswap Task files", "*.fst"), all_files],
config_all=[("Faceswap Project and Task files", "*.fst *.fsw"), all_files],
csv=[("Comma separated values", "*.csv"), all_files],
image=[("Bitmap", "*.bmp"),
("JPG", "*.jpeg *.jpg"),
("PNG", "*.png"),
("TIFF", "*.tif *.tiff"),
all_files],
ini=[("Faceswap config files", "*.ini"), all_files],
json=[("JSON file", "*.json"), all_files],
model=[("Keras model files", "*.h5"), all_files],
state=[("State files", "*.json"), all_files],
log=[("Log files", "*.log"), all_files],
video=[("Audio Video Interleave", "*.avi"),
("Flash Video", "*.flv"),
("Matroska", "*.mkv"),
("MOV", "*.mov"),
("MP4", "*.mp4"),
("MPEG", "*.mpeg *.mpg *.ts *.vob"),
("WebM", "*.webm"),
("Windows Media Video", "*.wmv"),
all_files])
# Add in multi-select options and upper case extensions for Linux
for key in filetypes:
if platform.system() == "Linux":
filetypes[key] = [item
if item[0] == "All files"
else (item[0], f"{item[1]} {item[1].upper()}")
for item in filetypes[key]]
if len(filetypes[key]) > 2:
multi = [f"{key.title()} Files"]
multi.append(" ".join([ftype[1]
for ftype in filetypes[key] if ftype[0] != "All files"]))
filetypes[key].insert(0, cast(Tuple[str, str], tuple(multi)))
return filetypes
@property
def _contexts(self) -> Dict[str, Dict[str, Union[str, Dict[str, str]]]]:
"""dict: Mapping of commands, actions and their corresponding file dialog for context
handle types. """
return dict(effmpeg=dict(input={"extract": "filename",
"gen-vid": "dir",
"get-fps": "filename",
"get-info": "filename",
"mux-audio": "filename",
"rescale": "filename",
"rotate": "filename",
"slice": "filename"},
output={"extract": "dir",
"gen-vid": "save_filename",
"get-fps": "nothing",
"get-info": "nothing",
"mux-audio": "save_filename",
"rescale": "save_filename",
"rotate": "save_filename",
"slice": "save_filename"}))
@classmethod
def _set_dummy_master(cls) -> Optional[tk.Frame]:
""" Add an option to force black font on Linux file dialogs KDE issue that displays light
font on white background).
This is a pretty hacky solution, but tkinter does not allow direct editing of file dialogs,
so we create a dummy frame and add the foreground option there, so that the file dialog can
inherit the foreground.
Returns
-------
tkinter.Frame or ``None``
The dummy master frame for Linux systems, otherwise ``None``
"""
if platform.system().lower() == "linux":
frame = tk.Frame()
frame.option_add("*foreground", "black")
retval: Optional[tk.Frame] = frame
else:
retval = None
return retval
def _remove_dummy_master(self) -> None:
""" Destroy the dummy master widget on Linux systems. """
if platform.system().lower() != "linux" or self._dummy_master is None:
return
self._dummy_master.destroy()
del self._dummy_master
self._dummy_master = None
def _set_defaults(self) -> Dict[str, Optional[str]]:
""" Set the default file type for the file dialog. Generally the first found file type
will be used, but this is overridden if it is not appropriate.
Returns
-------
dict:
The default file extension for each file type
"""
defaults: Dict[str, Optional[str]] = {
key: next(ext for ext in val[0][1].split(" ")).replace("*", "")
for key, val in self._filetypes.items()}
defaults["default"] = None
defaults["video"] = ".mp4"
defaults["image"] = ".png"
logger.debug(defaults)
return defaults
def _set_kwargs(self,
title: Optional[str],
initial_folder: Optional[str],
initial_file: Optional[str],
file_type: _FILETYPE,
command: Optional[str],
action: Optional[str],
variable: Optional[str] = None
) -> Dict[str, Union[None, tk.Frame, str, List[Tuple[str, str]]]]:
""" Generate the required kwargs for the requested file dialog browser.
Parameters
----------
title: str
The title to display on the file dialog. If `None` then the default title will be used.
initial_folder: str
The folder to initially open with the file dialog. If `None` then tkinter will decide.
initial_file: str
The filename to set with the file dialog. If `None` then tkinter no initial filename
is.
file_type: ['default', 'alignments', 'config_project', 'config_task', 'config_all', \
'csv', 'image', 'ini', 'state', 'log', 'video']
The type of file that this dialog is for. `default` allows selection of any files.
Other options limit the file type selection
command: str
Required for context handling file dialog, otherwise unused.
action: str
Required for context handling file dialog, otherwise unused.
variable: str, optional
Required for context handling file dialog, otherwise unused. The variable to associate
with this file dialog. Default: ``None``
Returns
-------
dict:
The key word arguments for the file dialog to be launched
"""
logger.debug("Setting Kwargs: (title: %s, initial_folder: %s, initial_file: '%s', "
"file_type: '%s', command: '%s': action: '%s', variable: '%s')",
title, initial_folder, initial_file, file_type, command, action, variable)
kwargs: Dict[str, Union[None, tk.Frame, str,
List[Tuple[str, str]]]] = dict(master=self._dummy_master)
if self._handletype.lower() == "context":
assert command is not None and action is not None and variable is not None
self._set_context_handletype(command, action, variable)
if title is not None:
kwargs["title"] = title
if initial_folder is not None:
kwargs["initialdir"] = initial_folder
if initial_file is not None:
kwargs["initialfile"] = initial_file
if self._handletype.lower() in (
"open", "save", "filename", "filename_multi", "save_filename"):
kwargs["filetypes"] = self._filetypes[file_type]
if self._defaults.get(file_type):
kwargs['defaultextension'] = self._defaults[file_type]
if self._handletype.lower() == "save":
kwargs["mode"] = "w"
if self._handletype.lower() == "open":
kwargs["mode"] = "r"
logger.debug("Set Kwargs: %s", kwargs)
return kwargs
def _set_context_handletype(self, command: str, action: str, variable: str) -> None:
""" Sets the correct handle type based on context.
Parameters
----------
command: str
The command that is being executed. Used to look up the context actions
action: str
The action that is being performed. Used to look up the correct file dialog
variable: str
The variable associated with this file dialog
"""
if self._contexts[command].get(variable, None) is not None:
handletype = cast(Dict[str, Dict[str, Dict[str, str]]],
self._contexts)[command][variable][action]
else:
handletype = cast(Dict[str, Dict[str, str]],
self._contexts)[command][action]
logger.debug(handletype)
self._handletype = cast(_HANDLETYPE, handletype)
def _open(self) -> Optional[IO]:
""" Open a file. """
logger.debug("Popping Open browser")
return filedialog.askopenfile(**self._kwargs) # type: ignore
def _save(self) -> Optional[IO]:
""" Save a file. """
logger.debug("Popping Save browser")
return filedialog.asksaveasfile(**self._kwargs) # type: ignore
def _dir(self) -> str:
""" Get a directory location. """
logger.debug("Popping Dir browser")
return filedialog.askdirectory(**self._kwargs)
def _savedir(self) -> str:
""" Get a save directory location. """
logger.debug("Popping SaveDir browser")
return filedialog.askdirectory(**self._kwargs)
def _filename(self) -> str:
""" Get an existing file location. """
logger.debug("Popping Filename browser")
return filedialog.askopenfilename(**self._kwargs)
def _filename_multi(self) -> Tuple[str, ...]:
""" Get multiple existing file locations. """
logger.debug("Popping Filename browser")
return filedialog.askopenfilenames(**self._kwargs)
def _save_filename(self) -> str:
""" Get a save file location. """
logger.debug("Popping Save Filename browser")
return filedialog.asksaveasfilename(**self._kwargs)
@staticmethod
def _nothing() -> None: # pylint: disable=useless-return
""" Method that does nothing, used for disabling open/save pop up. """
logger.debug("Popping Nothing browser")
return

659
lib/gui/utils/image.py Normal file
View File

@ -0,0 +1,659 @@
#!/usr/bin python3
""" Utilities for handling images in the Faceswap GUI """
import logging
import os
import sys
from typing import cast, Dict, List, Optional, Sequence, Tuple, Union
import numpy as np
from PIL import Image, ImageDraw, ImageTk
from .config import get_config, PATHCACHE
if sys.version_info < (3, 8):
from typing_extensions import Literal
else:
from typing import Literal
logger = logging.getLogger(__name__) # pylint: disable=invalid-name
_IMAGES: Optional["Images"] = None
_PREVIEW_TRIGGER: Optional["PreviewTrigger"] = None
def initialize_images() -> None:
""" Initialize the :class:`Images` handler and add to global constant.
This should only be called once on first GUI startup. Future access to :class:`Images`
handler should only be executed through :func:`get_images`.
"""
global _IMAGES # pylint: disable=global-statement
if _IMAGES is not None:
return
logger.debug("Initializing images")
_IMAGES = Images()
def get_images() -> "Images":
""" Get the Master GUI Images handler.
Returns
-------
:class:`Images`
The Master GUI Images handler
"""
assert _IMAGES is not None
return _IMAGES
class Images():
""" The centralized image repository for holding all icons and images required by the GUI.
This class should be initialized on GUI startup through :func:`initialize_images`. Any further
access to this class should be through :func:`get_images`.
"""
def __init__(self) -> None:
logger.debug("Initializing %s", self.__class__.__name__)
self._pathpreview = os.path.join(PATHCACHE, "preview")
self._pathoutput: Optional[str] = None
self._batch_mode = False
self._previewoutput: Optional[Tuple[Image.Image, ImageTk.PhotoImage]] = None
self._previewtrain: Dict[str, List[Union[Image.Image,
ImageTk.PhotoImage,
None,
float]]] = {}
self._previewcache: Dict[str, Union[None, float, np.ndarray, List[str]]] = dict(
modified=None, # cache for extract and convert
images=None,
filenames=[],
placeholder=None)
self._errcount = 0
self._icons = self._load_icons()
logger.debug("Initialized %s", self.__class__.__name__)
@property
def previewoutput(self) -> Optional[Tuple[Image.Image, ImageTk.PhotoImage]]:
""" Tuple or ``None``: First item in the tuple is the extract or convert preview image
(:class:`PIL.Image`), the second item is the image in a format that tkinter can display
(:class:`PIL.ImageTK.PhotoImage`).
The value of the property is ``None`` if no extract or convert task is running or there are
no files available in the output folder. """
return self._previewoutput
@property
def previewtrain(self) -> Dict[str, List[Union[Image.Image, ImageTk.PhotoImage, None, float]]]:
""" dict or ``None``: The training preview images. Dictionary key is the image name
(`str`). Dictionary values are a `list` of the training image (:class:`PIL.Image`), the
image formatted for tkinter display (:class:`PIL.ImageTK.PhotoImage`), the last
modification time of the image (`float`).
The value of this property is ``None`` if training is not running or there are no preview
images available.
"""
return self._previewtrain
@property
def icons(self) -> Dict[str, ImageTk.PhotoImage]:
""" dict: The faceswap icons for all parts of the GUI. The dictionary key is the icon
name (`str`) the value is the icon sized and formatted for display
(:class:`PIL.ImageTK.PhotoImage`).
Example
-------
>>> icons = get_images().icons
>>> save = icons["save"]
>>> button = ttk.Button(parent, image=save)
>>> button.pack()
"""
return self._icons
@staticmethod
def _load_icons() -> Dict[str, ImageTk.PhotoImage]:
""" Scan the icons cache folder and load the icons into :attr:`icons` for retrieval
throughout the GUI.
Returns
-------
dict:
The icons formatted as described in :attr:`icons`
"""
size = get_config().user_config_dict.get("icon_size", 16)
size = int(round(size * get_config().scaling_factor))
icons: Dict[str, ImageTk.PhotoImage] = {}
pathicons = os.path.join(PATHCACHE, "icons")
for fname in os.listdir(pathicons):
name, ext = os.path.splitext(fname)
if ext != ".png":
continue
img = Image.open(os.path.join(pathicons, fname))
img = ImageTk.PhotoImage(img.resize((size, size), resample=Image.HAMMING))
icons[name] = img
logger.debug(icons)
return icons
def set_faceswap_output_path(self, location: str, batch_mode: bool = False) -> None:
""" Set the path that will contain the output from an Extract or Convert task.
Required so that the GUI can fetch output images to display for return in
:attr:`previewoutput`.
Parameters
----------
location: str
The output location that has been specified for an Extract or Convert task
batch_mode: bool
``True`` if extracting in batch mode otherwise False
"""
self._pathoutput = location
self._batch_mode = batch_mode
def delete_preview(self) -> None:
""" Delete the preview files in the cache folder and reset the image cache.
Should be called when terminating tasks, or when Faceswap starts up or shuts down.
"""
logger.debug("Deleting previews")
for item in os.listdir(self._pathpreview):
if item.startswith(".gui_training_preview") and item.endswith(".jpg"):
fullitem = os.path.join(self._pathpreview, item)
logger.debug("Deleting: '%s'", fullitem)
os.remove(fullitem)
for fname in cast(List[str], self._previewcache["filenames"]):
if os.path.basename(fname) == ".gui_preview.jpg":
logger.debug("Deleting: '%s'", fname)
try:
os.remove(fname)
except FileNotFoundError:
logger.debug("File does not exist: %s", fname)
self._clear_image_cache()
def _clear_image_cache(self) -> None:
""" Clear all cached images. """
logger.debug("Clearing image cache")
self._pathoutput = None
self._batch_mode = False
self._previewoutput = None
self._previewtrain = {}
self._previewcache = dict(modified=None, # cache for extract and convert
images=None,
filenames=[],
placeholder=None)
@staticmethod
def _get_images(image_path: str) -> List[str]:
""" Get the images stored within the given directory.
Parameters
----------
image_path: str
The folder containing images to be scanned
Returns
-------
list:
The image filenames stored within the given folder
"""
logger.debug("Getting images: '%s'", image_path)
if not os.path.isdir(image_path):
logger.debug("Folder does not exist")
return []
files = [os.path.join(image_path, f)
for f in os.listdir(image_path) if f.lower().endswith((".png", ".jpg"))]
logger.debug("Image files: %s", files)
return files
def load_latest_preview(self, thumbnail_size: int, frame_dims: Tuple[int, int]) -> None:
""" Load the latest preview image for extract and convert.
Retrieves the latest preview images from the faceswap output folder, resizes to thumbnails
and lays out for display. Places the images into :attr:`previewoutput` for loading into
the display panel.
Parameters
----------
thumbnail_size: int
The size of each thumbnail that should be created
frame_dims: tuple
The (width (`int`), height (`int`)) of the display panel that will display the preview
"""
logger.debug("Loading preview image: (thumbnail_size: %s, frame_dims: %s)",
thumbnail_size, frame_dims)
assert self._pathoutput is not None
image_path = self._get_newest_folder() if self._batch_mode else self._pathoutput
image_files = self._get_images(image_path)
gui_preview = os.path.join(self._pathoutput, ".gui_preview.jpg")
if not image_files or (len(image_files) == 1 and gui_preview not in image_files):
logger.debug("No preview to display")
return
# Filter to just the gui_preview if it exists in folder output
image_files = [gui_preview] if gui_preview in image_files else image_files
logger.debug("Image Files: %s", len(image_files))
image_files = self._get_newest_filenames(image_files)
if not image_files:
return
if not self._load_images_to_cache(image_files, frame_dims, thumbnail_size):
logger.debug("Failed to load any preview images")
if gui_preview in image_files:
# Reset last modified for failed loading of a gui preview image so it is picked
# up next time
self._previewcache["modified"] = None
return
if image_files == [gui_preview]:
# Delete the preview image so that the main scripts know to output another
logger.debug("Deleting preview image")
os.remove(image_files[0])
show_image = self._place_previews(frame_dims)
if not show_image:
self._previewoutput = None
return
logger.debug("Displaying preview: %s", self._previewcache["filenames"])
self._previewoutput = (show_image, ImageTk.PhotoImage(show_image))
def _get_newest_folder(self) -> str:
""" Obtain the most recent folder created in the extraction output folder when processing
in batch mode.
Returns
-------
str
The most recently modified folder within the parent output folder. If no folders have
been created, returns the parent output folder
"""
assert self._pathoutput is not None
folders = [] if not os.path.exists(self._pathoutput) else [
os.path.join(self._pathoutput, folder)
for folder in os.listdir(self._pathoutput)
if os.path.isdir(os.path.join(self._pathoutput, folder))]
folders.sort(key=os.path.getmtime)
retval = folders[-1] if folders else self._pathoutput
logger.debug("sorted folders: %s, return value: %s", folders, retval)
return retval
def _get_newest_filenames(self, image_files: List[str]) -> List[str]:
""" Return image filenames that have been modified since the last check.
Parameters
----------
image_files: list
The list of image files to check the modification date for
Returns
-------
list:
A list of images that have been modified since the last check
"""
if self._previewcache["modified"] is None:
retval = image_files
else:
retval = [fname for fname in image_files
if os.path.getmtime(fname) > cast(float, self._previewcache["modified"])]
if not retval:
logger.debug("No new images in output folder")
else:
self._previewcache["modified"] = max(os.path.getmtime(img) for img in retval)
logger.debug("Number new images: %s, Last Modified: %s",
len(retval), self._previewcache["modified"])
return retval
def _load_images_to_cache(self,
image_files: List[str],
frame_dims: Tuple[int, int],
thumbnail_size: int) -> bool:
""" Load preview images to the image cache.
Load new images and append to cache, filtering the cache the number of thumbnails that will
fit inside the display panel.
Parameters
----------
image_files: list
A list of new image files that have been modified since the last check
frame_dims: tuple
The (width (`int`), height (`int`)) of the display panel that will display the preview
thumbnail_size: int
The size of each thumbnail that should be created
Returns
-------
bool
``True`` if images were successfully loaded to cache otherwise ``False``
"""
logger.debug("Number image_files: %s, frame_dims: %s, thumbnail_size: %s",
len(image_files), frame_dims, thumbnail_size)
num_images = (frame_dims[0] // thumbnail_size) * (frame_dims[1] // thumbnail_size)
logger.debug("num_images: %s", num_images)
if num_images == 0:
return False
samples: List[np.ndarray] = []
start_idx = len(image_files) - num_images if len(image_files) > num_images else 0
show_files = sorted(image_files, key=os.path.getctime)[start_idx:]
dropped_files = []
for fname in show_files:
try:
img = Image.open(fname)
except PermissionError as err:
logger.debug("Permission error opening preview file: '%s'. Original error: %s",
fname, str(err))
dropped_files.append(fname)
continue
except Exception as err: # pylint:disable=broad-except
# Swallow any issues with opening an image rather than spamming console
# Can happen when trying to read partially saved images
logger.debug("Error opening preview file: '%s'. Original error: %s",
fname, str(err))
dropped_files.append(fname)
continue
width, height = img.size
scaling = thumbnail_size / max(width, height)
logger.debug("image width: %s, height: %s, scaling: %s", width, height, scaling)
try:
img = img.resize((int(width * scaling), int(height * scaling)))
except OSError as err:
# Image only gets loaded when we call a method, so may error on partial loads
logger.debug("OS Error resizing preview image: '%s'. Original error: %s",
fname, err)
dropped_files.append(fname)
continue
samples.append(self._pad_and_border(img, thumbnail_size))
return self._process_samples(samples,
[fname for fname in show_files if fname not in dropped_files],
num_images)
def _pad_and_border(self, image: Image.Image, size: int) -> np.ndarray:
""" Pad rectangle images to a square and draw borders
Parameters
----------
image: :class:`PIL.Image`
The image to process
size: int
The size of the image as it should be displayed
Returns
-------
:class:`PIL.Image`:
The processed image
"""
if image.size[0] != image.size[1]:
# Pad to square
new_img = Image.new("RGB", (size, size))
new_img.paste(image, ((size - image.size[0]) // 2, (size - image.size[1]) // 2))
image = new_img
draw = ImageDraw.Draw(image)
draw.rectangle(((0, 0), (size, size)), outline="#E5E5E5", width=1)
retval = np.array(image)
logger.trace("image shape: %s", retval.shape) # type: ignore
return retval
def _process_samples(self,
samples: List[np.ndarray],
filenames: List[str],
num_images: int) -> bool:
""" Process the latest sample images into a displayable image.
Parameters
----------
samples: list
The list of extract/convert preview images to display
filenames: list
The full path to the filenames corresponding to the images
num_images: int
The number of images that should be displayed
Returns
-------
bool
``True`` if samples succesfully compiled otherwise ``False``
"""
asamples = np.array(samples)
if not np.any(asamples):
logger.debug("No preview images collected.")
return False
self._previewcache["filenames"] = (cast(List[str], self._previewcache["filenames"]) +
filenames)[-num_images:]
cache = cast(Optional[np.ndarray], self._previewcache["images"])
if cache is None:
logger.debug("Creating new cache")
cache = asamples[-num_images:]
else:
logger.debug("Appending to existing cache")
cache = np.concatenate((cache, asamples))[-num_images:]
self._previewcache["images"] = cache
logger.debug("Cache shape: %s", cast(np.ndarray, self._previewcache["images"]).shape)
return True
def _place_previews(self, frame_dims: Tuple[int, int]) -> Image.Image:
""" Format the preview thumbnails stored in the cache into a grid fitting the display
panel.
Parameters
----------
frame_dims: tuple
The (width (`int`), height (`int`)) of the display panel that will display the preview
Returns
-------
:class:`PIL.Image`:
The final preview display image
"""
if self._previewcache.get("images", None) is None:
logger.debug("No images in cache. Returning None")
return None
samples = cast(np.ndarray, self._previewcache["images"]).copy()
num_images, thumbnail_size = samples.shape[:2]
if self._previewcache["placeholder"] is None:
self._create_placeholder(thumbnail_size)
logger.debug("num_images: %s, thumbnail_size: %s", num_images, thumbnail_size)
cols, rows = frame_dims[0] // thumbnail_size, frame_dims[1] // thumbnail_size
logger.debug("cols: %s, rows: %s", cols, rows)
if cols == 0 or rows == 0:
logger.debug("Cols or Rows is zero. No items to display")
return None
remainder = (cols * rows) - num_images
if remainder != 0:
logger.debug("Padding sample display. Remainder: %s", remainder)
placeholder = np.concatenate([np.expand_dims(
cast(np.ndarray, self._previewcache["placeholder"]), 0)] * remainder)
samples = np.concatenate((samples, placeholder))
display = np.vstack([np.hstack(cast(Sequence, samples[row * cols: (row + 1) * cols]))
for row in range(rows)])
logger.debug("display shape: %s", display.shape)
return Image.fromarray(display)
def _create_placeholder(self, thumbnail_size: int) -> None:
""" Create a placeholder image for when there are fewer thumbnails available
than columns to display them.
Parameters
----------
thumbnail_size: int
The size of the thumbnail that the placeholder should replicate
"""
logger.debug("Creating placeholder. thumbnail_size: %s", thumbnail_size)
placeholder = Image.new("RGB", (thumbnail_size, thumbnail_size))
draw = ImageDraw.Draw(placeholder)
draw.rectangle(((0, 0), (thumbnail_size, thumbnail_size)), outline="#E5E5E5", width=1)
placeholder = np.array(placeholder)
self._previewcache["placeholder"] = placeholder
logger.debug("Created placeholder. shape: %s", placeholder.shape)
def load_training_preview(self) -> None:
""" Load the training preview images.
Reads the training image currently stored in the cache folder and loads them to
:attr:`previewtrain` for retrieval in the GUI.
"""
logger.debug("Loading Training preview images")
image_files = self._get_images(self._pathpreview)
modified = None
if not image_files:
logger.debug("No preview to display")
self._previewtrain = {}
return
for img in image_files:
modified = os.path.getmtime(img) if modified is None else modified
name = os.path.basename(img)
name = os.path.splitext(name)[0]
name = name[name.rfind("_") + 1:].title()
try:
logger.debug("Displaying preview: '%s'", img)
size = self._get_current_size(name)
self._previewtrain[name] = [Image.open(img), None, modified]
self.resize_image(name, size)
self._errcount = 0
except ValueError:
# This is probably an error reading the file whilst it's
# being saved so ignore it for now and only pick up if
# there have been multiple consecutive fails
logger.warning("Unable to display preview: (image: '%s', attempt: %s)",
img, self._errcount)
if self._errcount < 10:
self._errcount += 1
else:
logger.error("Error reading the preview file for '%s'", img)
print(f"Error reading the preview file for {name}")
del self._previewtrain[name]
def _get_current_size(self, name: str) -> Optional[Tuple[int, int]]:
""" Return the size of the currently displayed training preview image.
Parameters
----------
name: str
The name of the training image to get the size for
Returns
-------
width: int
The width of the training image
height: int
The height of the training image
"""
logger.debug("Getting size: '%s'", name)
if not self._previewtrain.get(name):
return None
img = cast(Image.Image, self._previewtrain[name][1])
if not img:
return None
logger.debug("Got size: (name: '%s', width: '%s', height: '%s')",
name, img.width(), img.height())
return img.width(), img.height()
def resize_image(self, name: str, frame_dims: Optional[Tuple[int, int]]) -> None:
""" Resize the training preview image based on the passed in frame size.
If the canvas that holds the preview image changes, update the image size
to fit the new canvas and refresh :attr:`previewtrain`.
Parameters
----------
name: str
The name of the training image to be resized
frame_dims: tuple, optional
The (width (`int`), height (`int`)) of the display panel that will display the preview.
``None`` if the frame dimensions are not known.
"""
logger.debug("Resizing image: (name: '%s', frame_dims: %s", name, frame_dims)
displayimg = cast(Image.Image, self._previewtrain[name][0])
if frame_dims:
frameratio = float(frame_dims[0]) / float(frame_dims[1])
imgratio = float(displayimg.size[0]) / float(displayimg.size[1])
if frameratio <= imgratio:
scale = frame_dims[0] / float(displayimg.size[0])
size = (frame_dims[0], int(displayimg.size[1] * scale))
else:
scale = frame_dims[1] / float(displayimg.size[1])
size = (int(displayimg.size[0] * scale), frame_dims[1])
logger.debug("Scaling: (scale: %s, size: %s", scale, size)
# Hacky fix to force a reload if it happens to find corrupted
# data, probably due to reading the image whilst it is partially
# saved. If it continues to fail, then eventually raise.
for i in range(0, 1000):
try:
displayimg = displayimg.resize(size, Image.ANTIALIAS)
except OSError:
if i == 999:
raise
continue
break
self._previewtrain[name][1] = ImageTk.PhotoImage(displayimg)
class PreviewTrigger():
""" Triggers to indicate to underlying Faceswap process that the preview image should
be updated.
Writes a file to the cache folder that is picked up by the main process.
"""
def __init__(self) -> None:
logger.debug("Initializing: %s", self.__class__.__name__)
self._trigger_files = dict(update=os.path.join(PATHCACHE, ".preview_trigger"),
mask_toggle=os.path.join(PATHCACHE, ".preview_mask_toggle"))
logger.debug("Initialized: %s (trigger_files: %s)",
self.__class__.__name__, self._trigger_files)
def set(self, trigger_type: Literal["update", "mask_toggle"]):
""" Place the trigger file into the cache folder
Parameters
----------
trigger_type: ["update", "mask_toggle"]
The type of action to trigger. 'update': Full preview update. 'mask_toggle': toggle
mask on and off
"""
trigger = self._trigger_files[trigger_type]
if not os.path.isfile(trigger):
with open(trigger, "w", encoding="utf8"):
pass
logger.debug("Set preview trigger: %s", trigger)
def clear(self, trigger_type: Optional[Literal["update", "mask_toggle"]] = None) -> None:
""" Remove the trigger file from the cache folder.
Parameters
----------
trigger_type: ["update", "mask_toggle", ``None``], optional
The trigger to clear. 'update': Full preview update. 'mask_toggle': toggle mask on
and off. ``None`` - clear all triggers. Default: ``None``
"""
if trigger_type is None:
triggers = list(self._trigger_files.values())
else:
triggers = [self._trigger_files[trigger_type]]
for trigger in triggers:
if os.path.isfile(trigger):
os.remove(trigger)
logger.debug("Removed preview trigger: %s", trigger)
def preview_trigger() -> PreviewTrigger:
""" Set the global preview trigger if it has not already been set and return.
Returns
-------
:class:`PreviewTrigger`
The trigger to indicate to the main faceswap process that it should perform a training
preview update
"""
global _PREVIEW_TRIGGER # pylint:disable=global-statement
if _PREVIEW_TRIGGER is None:
_PREVIEW_TRIGGER = PreviewTrigger()
return _PREVIEW_TRIGGER

107
lib/gui/utils/misc.py Normal file
View File

@ -0,0 +1,107 @@
#!/usr/bin/env python3
""" Miscellaneous Utility functions for the GUI. Includes LongRunningTask object """
import logging
import sys
from threading import Event, Thread
from typing import (Any, Callable, cast, Dict, Optional, Tuple, Type, TYPE_CHECKING)
from queue import Queue
from .config import get_config
if TYPE_CHECKING:
from types import TracebackType
from lib.multithreading import _ErrorType
logger = logging.getLogger(__name__) # pylint: disable=invalid-name
class LongRunningTask(Thread):
""" Runs long running tasks in a background thread to prevent the GUI from becoming
unresponsive.
This is sub-classed from :class:`Threading.Thread` so check documentation there for base
parameters. Additional parameters listed below.
Parameters
----------
widget: tkinter object, optional
The widget that this :class:`LongRunningTask` is associated with. Used for setting the busy
cursor in the correct location. Default: ``None``.
"""
_target: Callable
_args: Tuple
_kwargs: Dict[str, Any]
_name: str
def __init__(self,
target: Optional[Callable] = None,
name: Optional[str] = None,
args: Tuple = (),
kwargs: Optional[Dict[str, Any]] = None,
*,
daemon: bool = True,
widget=None):
logger.debug("Initializing %s: (target: %s, name: %s, args: %s, kwargs: %s, "
"daemon: %s)", self.__class__.__name__, target, name, args, kwargs,
daemon)
super().__init__(target=target, name=name, args=args, kwargs=kwargs,
daemon=daemon)
self.err: "_ErrorType" = None
self._widget = widget
self._config = get_config()
self._config.set_cursor_busy(widget=self._widget)
self._complete = Event()
self._queue: Queue = Queue()
logger.debug("Initialized %s", self.__class__.__name__,)
@property
def complete(self) -> Event:
""" :class:`threading.Event`: Event is set if the thread has completed its task,
otherwise it is unset.
"""
return self._complete
def run(self) -> None:
""" Commence the given task in a background thread. """
try:
if self._target:
retval = self._target(*self._args, **self._kwargs)
self._queue.put(retval)
except Exception: # pylint: disable=broad-except
self.err = cast(Tuple[Type[BaseException], BaseException, "TracebackType"],
sys.exc_info())
assert self.err is not None
logger.debug("Error in thread (%s): %s", self._name,
self.err[1].with_traceback(self.err[2]))
finally:
self._complete.set()
# Avoid a ref-cycle if the thread is running a function with
# an argument that has a member that points to the thread.
del self._target, self._args, self._kwargs
def get_result(self) -> Any:
""" Return the result from the given task.
Returns
-------
varies:
The result of the thread will depend on the given task. If a call is made to
:func:`get_result` prior to the thread completing its task then ``None`` will be
returned
"""
if not self._complete.is_set():
logger.warning("Aborting attempt to retrieve result from a LongRunningTask that is "
"still running")
return None
if self.err:
logger.debug("Error caught in thread")
self._config.set_cursor_default(widget=self._widget)
raise self.err[1].with_traceback(self.err[2])
logger.debug("Getting result from thread")
retval = self._queue.get()
logger.debug("Got result from thread")
self._config.set_cursor_default(widget=self._widget)
return retval

View File

@ -494,22 +494,21 @@ class Train(): # pylint:disable=too-few-public-methods
The preview image to be displayed and/or written out
name: str, optional
The name of the image for saving or display purposes. If an empty string is passed
then it will automatically be names. Default: ""
then it will automatically be named. Default: ""
"""
logger.debug("Updating preview: (name: %s)", name)
try:
scriptpath = os.path.realpath(os.path.dirname(sys.argv[0]))
if self._args.write_image:
logger.debug("Saving preview to disk")
img = "training_preview.jpg"
img = "training_preview.png"
imgfile = os.path.join(scriptpath, img)
cv2.imwrite(imgfile, image) # pylint: disable=no-member
logger.debug("Saved preview to: '%s'", img)
if self._args.redirect_gui:
logger.debug("Generating preview for GUI")
img = ".gui_training_preview.jpg"
imgfile = os.path.join(scriptpath, "lib", "gui",
".cache", "preview", img)
img = ".gui_training_preview.png"
imgfile = os.path.join(scriptpath, "lib", "gui", ".cache", "preview", img)
cv2.imwrite(imgfile, image) # pylint: disable=no-member
logger.debug("Generated preview for GUI: '%s'", imgfile)
if self._args.preview:

View File

@ -4,6 +4,7 @@ max-complexity=10
statistics = True
count = True
exclude = .git, __pycache__
per-file-ignores = __init__.py:F401
[mypy]
[mypy-cv2.*]